This commit is contained in:
aaron 2026-04-27 15:40:24 +08:00
parent 1ad9880a14
commit bc16d2f074
4 changed files with 77 additions and 3 deletions

View File

@ -7,13 +7,30 @@ from app.core.auth import hash_password, verify_password, create_access_token
from app.core.deps import get_current_user from app.core.deps import get_current_user
from app.db.database import get_db from app.db.database import get_db
from app.db.models import ClassMembership, User from app.db.models import ClassMembership, User
from app.schemas.auth import LoginRequest, RegisterRequest, ChangePasswordRequest from app.schemas.auth import (
ChangePasswordRequest,
InviteCodeClassPreview,
LoginRequest,
RegisterRequest,
)
from app.schemas.user import TokenResponse, UserOut, build_user_out from app.schemas.user import TokenResponse, UserOut, build_user_out
from app.services.member_activation_service import validate_registration from app.services.member_activation_service import get_class_by_invite_code, validate_registration
router = APIRouter(prefix="/api/auth", tags=["auth"]) router = APIRouter(prefix="/api/auth", tags=["auth"])
@router.get("/invite-code/{invite_code}", response_model=InviteCodeClassPreview)
async def preview_invite_code_class(invite_code: str, db: AsyncSession = Depends(get_db)):
class_ = await get_class_by_invite_code(db, invite_code.strip().upper())
if class_ is None:
raise HTTPException(status_code=404, detail="邀请码无效")
return InviteCodeClassPreview(
id=class_.id,
name=class_.name,
cohort_year=class_.cohort_year,
)
@router.post("/activate") @router.post("/activate")
async def activate_account(req: RegisterRequest, db: AsyncSession = Depends(get_db)): async def activate_account(req: RegisterRequest, db: AsyncSession = Depends(get_db)):
# 1. Check if email is already in use # 1. Check if email is already in use

View File

@ -16,3 +16,9 @@ class RegisterRequest(BaseModel):
class ChangePasswordRequest(BaseModel): class ChangePasswordRequest(BaseModel):
old_password: str old_password: str
new_password: str new_password: str
class InviteCodeClassPreview(BaseModel):
id: int
name: str
cohort_year: int

View File

@ -173,6 +173,11 @@ async def validate_registration(
return user, class_.id return user, class_.id
async def get_class_by_invite_code(db: AsyncSession, invite_code: str) -> Class_ | None:
result = await db.execute(select(Class_).where(Class_.invite_code == invite_code))
return result.scalar_one_or_none()
async def delete_inactive_member(db: AsyncSession, class_id: int, user_id: int) -> bool: async def delete_inactive_member(db: AsyncSession, class_id: int, user_id: int) -> bool:
result = await db.execute( result = await db.execute(
select(User) select(User)

View File

@ -6,16 +6,25 @@ import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { getErrorMessage, postAPI } from "@/lib/api"; import { fetchAPI, getErrorMessage, postAPI } from "@/lib/api";
import Link from "next/link"; import Link from "next/link";
import type { LoginResponse } from "@/lib/types"; import type { LoginResponse } from "@/lib/types";
type InviteCodeClassPreview = {
id: number;
name: string;
cohort_year: number;
};
export default function ActivatePage() { export default function ActivatePage() {
const [inviteCode, setInviteCode] = useState(""); const [inviteCode, setInviteCode] = useState("");
const [studentId, setStudentId] = useState(""); const [studentId, setStudentId] = useState("");
const [email, setEmail] = useState(""); const [email, setEmail] = useState("");
const [password, setPassword] = useState(""); const [password, setPassword] = useState("");
const [confirmPassword, setConfirmPassword] = useState(""); const [confirmPassword, setConfirmPassword] = useState("");
const [classPreview, setClassPreview] = useState<InviteCodeClassPreview | null>(null);
const [classLookupLoading, setClassLookupLoading] = useState(false);
const [classLookupError, setClassLookupError] = useState("");
const [error, setError] = useState(""); const [error, setError] = useState("");
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const router = useRouter(); const router = useRouter();
@ -28,6 +37,32 @@ export default function ActivatePage() {
} }
}, [searchParams]); }, [searchParams]);
useEffect(() => {
const normalizedCode = inviteCode.trim().toUpperCase();
if (!normalizedCode) {
setClassPreview(null);
setClassLookupError("");
setClassLookupLoading(false);
return;
}
const timer = window.setTimeout(async () => {
setClassLookupLoading(true);
setClassLookupError("");
try {
const res = await fetchAPI<InviteCodeClassPreview>(`/api/auth/invite-code/${encodeURIComponent(normalizedCode)}`);
setClassPreview(res);
} catch {
setClassPreview(null);
setClassLookupError("未找到对应班级,请确认邀请码是否正确");
} finally {
setClassLookupLoading(false);
}
}, 300);
return () => window.clearTimeout(timer);
}, [inviteCode]);
const handleSubmit = async (e: React.FormEvent) => { const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
setError(""); setError("");
@ -98,6 +133,17 @@ export default function ActivatePage() {
onChange={(e) => setInviteCode(e.target.value.toUpperCase())} onChange={(e) => setInviteCode(e.target.value.toUpperCase())}
required required
/> />
{classLookupLoading && (
<p className="text-xs text-[#9a7b68]">...</p>
)}
{!classLookupLoading && classPreview && (
<div className="rounded-2xl border border-[#d8c1a1] bg-[#fff4e3] px-3 py-2 text-sm text-[#6f4b32]">
{classPreview.name}
</div>
)}
{!classLookupLoading && classLookupError && (
<p className="text-xs text-red-600">{classLookupError}</p>
)}
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="studentId"></Label> <Label htmlFor="studentId"></Label>