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.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
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user