fix(api-gateway): 修复尾斜杠重定向循环与 DEV_MODE 旁路

- main.go: 禁用 RedirectTrailingSlash,为 classes/iam/teacher 双注册无尾斜杠与通配符路由

- auth.go: DEV_MODE=true 时接受 Bearer dev-token 注入开发用户

- config.go: 新增 DevMode 配置项与 getEnvBool 工具

- page.tsx: 开发模式请求携带 Authorization: Bearer dev-token

- .env.example: 添加 DEV_MODE=false 默认值与生产警告
This commit is contained in:
SpecialX
2026-07-08 15:11:47 +08:00
parent a4ec5b72c5
commit e5902ca2b3
7 changed files with 171 additions and 55 deletions

View File

@@ -1,6 +1,6 @@
'use client';
"use client";
import { useState, useEffect, useCallback } from 'react';
import { useState, useEffect, useCallback } from "react";
interface ClassItem {
id: string;
@@ -20,24 +20,28 @@ interface ApiResponse<T> {
export default function HomePage() {
const [classes, setClasses] = useState<ClassItem[]>([]);
const [loading, setLoading] = useState(false);
const [name, setName] = useState('');
const [gradeId, setGradeId] = useState('550e8400-e29b-41d4-a716-446655440000');
const [description, setDescription] = useState('');
const [name, setName] = useState("");
const [gradeId, setGradeId] = useState(
"550e8400-e29b-41d4-a716-446655440000",
);
const [description, setDescription] = useState("");
const [error, setError] = useState<string | null>(null);
const fetchClasses = useCallback(async () => {
setLoading(true);
setError(null);
try {
const res = await fetch('/api/v1/classes');
const res = await fetch("/api/v1/classes", {
headers: { Authorization: "Bearer dev-token" },
});
const json: ApiResponse<ClassItem[]> = await res.json();
if (json.success && json.data) {
setClasses(json.data);
} else {
setError(json.error?.message || 'Failed to load');
setError(json.error?.message || "Failed to load");
}
} catch (e) {
setError(e instanceof Error ? e.message : 'Network error');
setError(e instanceof Error ? e.message : "Network error");
} finally {
setLoading(false);
}
@@ -51,49 +55,61 @@ export default function HomePage() {
e.preventDefault();
if (!name.trim()) return;
try {
const res = await fetch('/api/v1/classes', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer dev-token' },
const res = await fetch("/api/v1/classes", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: "Bearer dev-token",
},
body: JSON.stringify({ name, gradeId, description }),
});
const json = await res.json();
if (!json.success) {
setError(json.error?.message || 'Create failed');
setError(json.error?.message || "Create failed");
return;
}
setName('');
setDescription('');
setName("");
setDescription("");
await fetchClasses();
} catch (e) {
setError(e instanceof Error ? e.message : 'Network error');
setError(e instanceof Error ? e.message : "Network error");
}
};
const handleDelete = async (id: string) => {
try {
const res = await fetch(`/api/v1/classes/${id}`, {
method: 'DELETE',
headers: { 'Authorization': 'Bearer dev-token' },
method: "DELETE",
headers: { Authorization: "Bearer dev-token" },
});
const json = await res.json();
if (!json.success) {
setError(json.error?.message || 'Delete failed');
setError(json.error?.message || "Delete failed");
return;
}
await fetchClasses();
} catch (e) {
setError(e instanceof Error ? e.message : 'Network error');
setError(e instanceof Error ? e.message : "Network error");
}
};
return (
<div className="min-h-screen" style={{ background: 'var(--bg-paper)' }}>
<header className="border-b" style={{ borderColor: 'var(--color-rule)' }}>
<div className="min-h-screen" style={{ background: "var(--bg-paper)" }}>
<header className="border-b" style={{ borderColor: "var(--color-rule)" }}>
<div className="max-w-6xl mx-auto px-8 py-6">
<h1 className="text-3xl" style={{ fontFamily: 'var(--font-serif)', color: 'var(--color-ink)' }}>
<h1
className="text-3xl"
style={{
fontFamily: "var(--font-serif)",
color: "var(--color-ink)",
}}
>
</h1>
<p className="mt-1 text-sm" style={{ color: 'var(--color-ink-muted)' }}>
<p
className="mt-1 text-sm"
style={{ color: "var(--color-ink-muted)" }}
>
P1 - classes CRUD
</p>
</div>
@@ -102,11 +118,19 @@ export default function HomePage() {
<main className="max-w-6xl mx-auto px-8 py-8 grid grid-cols-12 gap-8">
{/* 左侧:创建表单 */}
<aside className="col-span-4">
<h2 className="text-xl mb-4" style={{ fontFamily: 'var(--font-serif)' }}></h2>
<h2
className="text-xl mb-4"
style={{ fontFamily: "var(--font-serif)" }}
>
</h2>
<div className="rule-thin mb-4" />
<form onSubmit={handleCreate} className="space-y-4">
<div>
<label className="block text-xs uppercase tracking-wide mb-1" style={{ color: 'var(--color-ink-muted)' }}>
<label
className="block text-xs uppercase tracking-wide mb-1"
style={{ color: "var(--color-ink-muted)" }}
>
</label>
<input
@@ -114,13 +138,19 @@ export default function HomePage() {
value={name}
onChange={(e) => setName(e.target.value)}
className="w-full px-3 py-2 bg-transparent border-b focus:outline-none focus:border-b-2"
style={{ borderColor: 'var(--color-rule)', borderRadius: '6px 6px 0 0' }}
style={{
borderColor: "var(--color-rule)",
borderRadius: "6px 6px 0 0",
}}
placeholder="如:高三(1)班"
required
/>
</div>
<div>
<label className="block text-xs uppercase tracking-wide mb-1" style={{ color: 'var(--color-ink-muted)' }}>
<label
className="block text-xs uppercase tracking-wide mb-1"
style={{ color: "var(--color-ink-muted)" }}
>
ID
</label>
<input
@@ -128,25 +158,34 @@ export default function HomePage() {
value={gradeId}
onChange={(e) => setGradeId(e.target.value)}
className="w-full px-3 py-2 bg-transparent border-b text-sm font-mono"
style={{ borderColor: 'var(--color-rule)', borderRadius: '6px 6px 0 0' }}
style={{
borderColor: "var(--color-rule)",
borderRadius: "6px 6px 0 0",
}}
/>
</div>
<div>
<label className="block text-xs uppercase tracking-wide mb-1" style={{ color: 'var(--color-ink-muted)' }}>
<label
className="block text-xs uppercase tracking-wide mb-1"
style={{ color: "var(--color-ink-muted)" }}
>
</label>
<textarea
value={description}
onChange={(e) => setDescription(e.target.value)}
className="w-full px-3 py-2 bg-transparent border-b resize-none"
style={{ borderColor: 'var(--color-rule)', borderRadius: '6px 6px 0 0' }}
style={{
borderColor: "var(--color-rule)",
borderRadius: "6px 6px 0 0",
}}
rows={3}
/>
</div>
<button
type="submit"
className="px-4 py-2 text-white text-sm tracking-wide transition-opacity hover:opacity-90"
style={{ background: 'var(--color-accent)', borderRadius: '6px' }}
style={{ background: "var(--color-accent)", borderRadius: "6px" }}
>
</button>
@@ -156,16 +195,19 @@ export default function HomePage() {
{/* 中间:班级列表(纸面)*/}
<section className="col-span-8">
<div className="flex items-baseline justify-between mb-4">
<h2 className="text-xl" style={{ fontFamily: 'var(--font-serif)' }}>
<h2 className="text-xl" style={{ fontFamily: "var(--font-serif)" }}>
<span className="ml-2 text-sm font-sans" style={{ color: 'var(--color-ink-muted)' }}>
<span
className="ml-2 text-sm font-sans"
style={{ color: "var(--color-ink-muted)" }}
>
{classes.length}
</span>
</h2>
<button
onClick={fetchClasses}
className="text-xs uppercase tracking-wide hover:opacity-70"
style={{ color: 'var(--color-accent)' }}
style={{ color: "var(--color-accent)" }}
>
</button>
@@ -173,37 +215,65 @@ export default function HomePage() {
<div className="rule-thin mb-6" />
{error && (
<div className="mark-left mb-4 py-2" style={{ borderColor: 'var(--color-accent)' }}>
<p className="text-sm" style={{ color: 'var(--color-accent)' }}>{error}</p>
<div
className="mark-left mb-4 py-2"
style={{ borderColor: "var(--color-accent)" }}
>
<p className="text-sm" style={{ color: "var(--color-accent)" }}>
{error}
</p>
</div>
)}
{loading ? (
<p className="text-sm" style={{ color: 'var(--color-ink-muted)' }}>...</p>
<p className="text-sm" style={{ color: "var(--color-ink-muted)" }}>
...
</p>
) : classes.length === 0 ? (
<p className="text-sm italic" style={{ color: 'var(--color-ink-muted)' }}>
<p
className="text-sm italic"
style={{ color: "var(--color-ink-muted)" }}
>
</p>
) : (
<ul className="space-y-0">
{classes.map((cls) => (
<li key={cls.id} className="py-4 grid grid-cols-12 gap-4 items-baseline" style={{ borderBottom: '1px solid var(--color-rule)' }}>
<li
key={cls.id}
className="py-4 grid grid-cols-12 gap-4 items-baseline"
style={{ borderBottom: "1px solid var(--color-rule)" }}
>
<div className="col-span-7">
<h3 className="text-lg" style={{ fontFamily: 'var(--font-serif)', color: 'var(--color-ink)' }}>
<h3
className="text-lg"
style={{
fontFamily: "var(--font-serif)",
color: "var(--color-ink)",
}}
>
{cls.name}
</h3>
{cls.description && (
<p className="mt-1 text-sm" style={{ color: 'var(--color-ink-muted)' }}>{cls.description}</p>
<p
className="mt-1 text-sm"
style={{ color: "var(--color-ink-muted)" }}
>
{cls.description}
</p>
)}
</div>
<div className="col-span-3 text-xs font-mono" style={{ color: 'var(--color-ink-muted)' }}>
<div
className="col-span-3 text-xs font-mono"
style={{ color: "var(--color-ink-muted)" }}
>
{cls.id.slice(0, 8)}...
</div>
<div className="col-span-2 text-right">
<button
onClick={() => handleDelete(cls.id)}
className="text-xs uppercase tracking-wide hover:opacity-70"
style={{ color: 'var(--color-ink-muted)' }}
style={{ color: "var(--color-ink-muted)" }}
>
</button>