1
This commit is contained in:
parent
1ad9880a14
commit
bc16d2f074
@ -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.db.database import get_db
|
||||
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.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.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")
|
||||
async def activate_account(req: RegisterRequest, db: AsyncSession = Depends(get_db)):
|
||||
# 1. Check if email is already in use
|
||||
|
||||
@ -16,3 +16,9 @@ class RegisterRequest(BaseModel):
|
||||
class ChangePasswordRequest(BaseModel):
|
||||
old_password: str
|
||||
new_password: str
|
||||
|
||||
|
||||
class InviteCodeClassPreview(BaseModel):
|
||||
id: int
|
||||
name: str
|
||||
cohort_year: int
|
||||
|
||||
@ -173,6 +173,11 @@ async def validate_registration(
|
||||
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:
|
||||
result = await db.execute(
|
||||
select(User)
|
||||
|
||||
@ -6,16 +6,25 @@ import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
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 type { LoginResponse } from "@/lib/types";
|
||||
|
||||
type InviteCodeClassPreview = {
|
||||
id: number;
|
||||
name: string;
|
||||
cohort_year: number;
|
||||
};
|
||||
|
||||
export default function ActivatePage() {
|
||||
const [inviteCode, setInviteCode] = useState("");
|
||||
const [studentId, setStudentId] = useState("");
|
||||
const [email, setEmail] = useState("");
|
||||
const [password, setPassword] = 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 [loading, setLoading] = useState(false);
|
||||
const router = useRouter();
|
||||
@ -28,6 +37,32 @@ export default function ActivatePage() {
|
||||
}
|
||||
}, [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) => {
|
||||
e.preventDefault();
|
||||
setError("");
|
||||
@ -98,6 +133,17 @@ export default function ActivatePage() {
|
||||
onChange={(e) => setInviteCode(e.target.value.toUpperCase())}
|
||||
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 className="space-y-2">
|
||||
<Label htmlFor="studentId">学号</Label>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user