added a user system
This commit is contained in:
@@ -28,14 +28,21 @@ from starlette.concurrency import run_in_threadpool
|
||||
from starlette.requests import ClientDisconnect
|
||||
|
||||
from .assessment import AssessmentService
|
||||
from .auth import AuthenticatedUser
|
||||
from .audio_metrics import AudioAnalysisError
|
||||
from .config import Settings
|
||||
from .models import (
|
||||
AssessmentResult,
|
||||
AuthRequest,
|
||||
AuthResponse,
|
||||
CourseSummary,
|
||||
SentenceBoundaryAdjust,
|
||||
SentenceBoundary,
|
||||
SentenceBoundaryDocument,
|
||||
SentenceTextUpdate,
|
||||
UserDubShareSummary,
|
||||
UserPublic,
|
||||
UserResultSummary,
|
||||
VideoDetailResponse,
|
||||
VideoListResponse,
|
||||
VideoSummary,
|
||||
@@ -124,6 +131,20 @@ def create_app(
|
||||
if expected and not hmac.compare_digest(x_client_key or "", expected):
|
||||
raise HTTPException(status_code=401, detail="A valid X-Client-Key header is required.")
|
||||
|
||||
def current_user(
|
||||
authorization: Optional[str] = Header(default=None),
|
||||
x_user_token: Optional[str] = Header(default=None),
|
||||
) -> AuthenticatedUser:
|
||||
token = x_user_token
|
||||
if authorization and authorization.lower().startswith("bearer "):
|
||||
token = authorization[7:].strip()
|
||||
if not token:
|
||||
raise HTTPException(status_code=401, detail="Login required.")
|
||||
row = video_repository.resolve_session(token)
|
||||
if row is None:
|
||||
raise HTTPException(status_code=401, detail="Session expired. Please sign in again.")
|
||||
return AuthenticatedUser(id=row["id"], username=row["username"], nickname=row["nickname"])
|
||||
|
||||
def find_document(video_hash: str) -> Optional[SentenceBoundaryDocument]:
|
||||
return video_repository.get_document(video_hash) or legacy_store.get(video_hash)
|
||||
|
||||
@@ -139,6 +160,86 @@ def create_app(
|
||||
"scoring_version": "asr-fluency-v1",
|
||||
}
|
||||
|
||||
@application.post("/api/v1/auth/register", response_model=AuthResponse, status_code=201)
|
||||
def register_user(payload: AuthRequest) -> AuthResponse:
|
||||
user = video_repository.create_user(
|
||||
username=payload.username,
|
||||
password=payload.password,
|
||||
nickname=payload.username,
|
||||
)
|
||||
if user is None:
|
||||
raise HTTPException(status_code=409, detail="Username is already taken.")
|
||||
token = video_repository.create_session(user["id"])
|
||||
return AuthResponse(token=token, user=UserPublic(**user))
|
||||
|
||||
@application.post("/api/v1/auth/login", response_model=AuthResponse)
|
||||
def login_user(payload: AuthRequest) -> AuthResponse:
|
||||
user = video_repository.authenticate_user(payload.username, payload.password)
|
||||
if user is None:
|
||||
raise HTTPException(status_code=401, detail="Incorrect username or password.")
|
||||
token = video_repository.create_session(user["id"])
|
||||
return AuthResponse(token=token, user=UserPublic(**user))
|
||||
|
||||
@application.get("/api/v1/auth/me", response_model=UserPublic)
|
||||
def get_authenticated_user(user: AuthenticatedUser = Depends(current_user)) -> UserPublic:
|
||||
row = video_repository.get_user_by_id(user.id)
|
||||
if row is None:
|
||||
raise HTTPException(status_code=404, detail="User was not found.")
|
||||
return UserPublic(**row)
|
||||
|
||||
@application.post("/api/v1/auth/logout", status_code=204)
|
||||
def logout_user(
|
||||
authorization: Optional[str] = Header(default=None),
|
||||
x_user_token: Optional[str] = Header(default=None),
|
||||
) -> Response:
|
||||
token = x_user_token
|
||||
if authorization and authorization.lower().startswith("bearer "):
|
||||
token = authorization[7:].strip()
|
||||
if token:
|
||||
video_repository.delete_session(token)
|
||||
return Response(status_code=204)
|
||||
|
||||
@application.get("/api/v1/courses", response_model=List[CourseSummary])
|
||||
def list_courses(user: AuthenticatedUser = Depends(current_user)) -> List[CourseSummary]:
|
||||
return [
|
||||
CourseSummary(
|
||||
video_hash=row["video_hash"],
|
||||
title=row["title"],
|
||||
duration_ms=row["duration_ms"],
|
||||
language=row["language"],
|
||||
sentence_count=row["sentence_count"],
|
||||
stream_url=_video_summary(row, service_settings).stream_url,
|
||||
enrolled=bool(row["enrolled"]),
|
||||
)
|
||||
for row in video_repository.list_courses(user.id)
|
||||
]
|
||||
|
||||
@application.post("/api/v1/courses/{video_hash}/enroll", status_code=204)
|
||||
def enroll_course(
|
||||
video_hash: str = SHA256_PATH,
|
||||
user: AuthenticatedUser = Depends(current_user),
|
||||
) -> Response:
|
||||
if not video_repository.enroll_course(user.id, video_hash):
|
||||
raise HTTPException(status_code=404, detail="Course is not available.")
|
||||
return Response(status_code=204)
|
||||
|
||||
@application.delete("/api/v1/courses/{video_hash}/enroll", status_code=204)
|
||||
def unenroll_course(
|
||||
video_hash: str = SHA256_PATH,
|
||||
user: AuthenticatedUser = Depends(current_user),
|
||||
) -> Response:
|
||||
if not video_repository.unenroll_course(user.id, video_hash):
|
||||
raise HTTPException(status_code=404, detail="Enrollment was not found.")
|
||||
return Response(status_code=204)
|
||||
|
||||
@application.get("/api/v1/me/results", response_model=List[UserResultSummary])
|
||||
def my_results(user: AuthenticatedUser = Depends(current_user)) -> List[UserResultSummary]:
|
||||
return [UserResultSummary(**row) for row in video_repository.list_user_results(user.id)]
|
||||
|
||||
@application.get("/api/v1/me/dub-shares", response_model=List[UserDubShareSummary])
|
||||
def my_dub_shares(user: AuthenticatedUser = Depends(current_user)) -> List[UserDubShareSummary]:
|
||||
return [UserDubShareSummary(**row) for row in video_repository.list_user_dub_shares(user.id)]
|
||||
|
||||
@application.get("/admin", include_in_schema=False)
|
||||
def admin_page() -> FileResponse:
|
||||
page = static_dir / "admin.html"
|
||||
@@ -181,11 +282,13 @@ def create_app(
|
||||
@application.get(
|
||||
"/api/v1/videos/{video_hash}/sentence-boundaries",
|
||||
response_model=SentenceBoundaryDocument,
|
||||
dependencies=[Depends(current_user)],
|
||||
)
|
||||
@application.get(
|
||||
"/api/v1/sentence-boundaries/{video_hash}",
|
||||
response_model=SentenceBoundaryDocument,
|
||||
include_in_schema=False,
|
||||
dependencies=[Depends(current_user)],
|
||||
)
|
||||
def get_sentence_boundaries(video_hash: str = SHA256_PATH) -> SentenceBoundaryDocument:
|
||||
document = find_document(video_hash)
|
||||
@@ -401,13 +504,13 @@ def create_app(
|
||||
@application.post(
|
||||
"/api/v1/videos/{video_hash}/sentences/{sentence_index}/assessments",
|
||||
response_model=AssessmentResult,
|
||||
dependencies=[Depends(require_client)],
|
||||
)
|
||||
async def assess_sentence(
|
||||
video_hash: str = SHA256_PATH,
|
||||
sentence_index: int = ApiPath(ge=0),
|
||||
audio: UploadFile = File(...),
|
||||
language: Optional[str] = Form(default=None),
|
||||
user: AuthenticatedUser = Depends(current_user),
|
||||
) -> AssessmentResult:
|
||||
document = find_document(video_hash)
|
||||
if document is None:
|
||||
@@ -433,6 +536,7 @@ def create_app(
|
||||
audio_path=temporary_path,
|
||||
language=language,
|
||||
retained_audio_filename=temporary_name if service_settings.keep_attempt_audio else None,
|
||||
user_id=user.id,
|
||||
)
|
||||
completed = True
|
||||
return result
|
||||
@@ -457,6 +561,7 @@ def create_app(
|
||||
segments: str = Form(...),
|
||||
scores: str = Form(default="[]"),
|
||||
files: List[UploadFile] = File(...),
|
||||
user: AuthenticatedUser = Depends(current_user),
|
||||
) -> Dict[str, Any]:
|
||||
try:
|
||||
segment_items = json.loads(segments)
|
||||
@@ -526,6 +631,7 @@ def create_app(
|
||||
video_hash=video_hash.lower(),
|
||||
title=title,
|
||||
segments=prepared_segments,
|
||||
user_id=user.id,
|
||||
)
|
||||
if share is None:
|
||||
raise HTTPException(status_code=404, detail="Video was not found.")
|
||||
|
||||
Reference in New Issue
Block a user