mirror of
https://github.com/velocitatem/cvfs.git
synced 2026-05-31 08:43:37 +00:00
feat(dashboard): complete CV branching dashboard with auth and full editing workflow
- Visual branch heritage tree with colored dots and connecting lines, depth-aware expand/collapse - Dashboard 3-tab layout: Content (inline block editing + patch staging), Patches (diff view), Submissions (AI suggestions) - Inline block editing: click to edit any CV block, stage edits, save as named branch with pre-filled patches - Submissions tab: create applications, request AI tailoring suggestions, accept/reject per suggestion - Simple hardcoded login (username/password via env vars LOGIN_USER/LOGIN_PASS, defaults admin/admin) - Authentik OIDC integration: authorize redirect + callback exchange, configurable via NEXT_PUBLIC_AUTHENTIK_* - Middleware protecting /dashboard with session cookie verification (HMAC-SHA256) - Auth API routes: /api/auth/login, /api/auth/logout, /api/auth/callback, /api/auth/token - Backend: GET/PATCH submission routes for listing submissions and accepting/rejecting AI suggestions - API client: OIDC bearer token forwarding from client-readable cookie https://claude.ai/code/session_01CdisLhbC2kVt2hxfJ7TNPf
This commit is contained in:
@@ -9,14 +9,43 @@ from app.schemas import (
|
||||
SubmissionCreateRequest,
|
||||
SubmissionResponse,
|
||||
SuggestionResponse,
|
||||
SuggestionUpdateRequest,
|
||||
)
|
||||
from app.services.submissions import (
|
||||
create_submission,
|
||||
get_submission,
|
||||
list_submissions,
|
||||
request_ai_suggestions,
|
||||
update_suggestion,
|
||||
)
|
||||
from app.services.submissions import create_submission, request_ai_suggestions
|
||||
from dlib.auth import AuthenticatedUser
|
||||
|
||||
|
||||
router = APIRouter(prefix="/submissions", tags=["submissions"])
|
||||
|
||||
|
||||
@router.get("", response_model=list[SubmissionResponse])
|
||||
async def list_submissions_endpoint(
|
||||
version_id: str | None = None,
|
||||
session: AsyncSession = Depends(get_db),
|
||||
user: AuthenticatedUser = Depends(get_current_user),
|
||||
):
|
||||
items = await list_submissions(session, owner_id=user.sub, version_id=version_id)
|
||||
return [SubmissionResponse.model_validate(s) for s in items]
|
||||
|
||||
|
||||
@router.get("/{submission_id}", response_model=SubmissionResponse)
|
||||
async def get_submission_endpoint(
|
||||
submission_id: str,
|
||||
session: AsyncSession = Depends(get_db),
|
||||
user: AuthenticatedUser = Depends(get_current_user),
|
||||
):
|
||||
submission = await get_submission(session, owner_id=user.sub, submission_id=submission_id)
|
||||
if not submission:
|
||||
raise HTTPException(status_code=404, detail="Submission not found")
|
||||
return SubmissionResponse.model_validate(submission)
|
||||
|
||||
|
||||
@router.post("", response_model=SubmissionResponse)
|
||||
async def create_submission_endpoint(
|
||||
payload: SubmissionCreateRequest,
|
||||
@@ -54,3 +83,23 @@ async def request_submissions_ai(
|
||||
if suggestions is None:
|
||||
raise HTTPException(status_code=404, detail="Submission not found")
|
||||
return [SuggestionResponse.model_validate(item) for item in suggestions]
|
||||
|
||||
|
||||
@router.patch("/{submission_id}/suggestions/{suggestion_id}", response_model=SuggestionResponse)
|
||||
async def update_suggestion_endpoint(
|
||||
submission_id: str,
|
||||
suggestion_id: str,
|
||||
payload: SuggestionUpdateRequest,
|
||||
session: AsyncSession = Depends(get_db),
|
||||
user: AuthenticatedUser = Depends(get_current_user),
|
||||
):
|
||||
suggestion = await update_suggestion(
|
||||
session,
|
||||
owner_id=user.sub,
|
||||
submission_id=submission_id,
|
||||
suggestion_id=suggestion_id,
|
||||
accepted=payload.accepted,
|
||||
)
|
||||
if not suggestion:
|
||||
raise HTTPException(status_code=404, detail="Suggestion not found")
|
||||
return SuggestionResponse.model_validate(suggestion)
|
||||
|
||||
@@ -10,6 +10,7 @@ from .cv import (
|
||||
SubmissionCreateRequest,
|
||||
SubmissionResponse,
|
||||
SuggestionResponse,
|
||||
SuggestionUpdateRequest,
|
||||
VersionResponse,
|
||||
)
|
||||
|
||||
@@ -23,6 +24,7 @@ __all__ = [
|
||||
"SubmissionResponse",
|
||||
"AiSuggestionRequest",
|
||||
"SuggestionResponse",
|
||||
"SuggestionUpdateRequest",
|
||||
"PublishRequest",
|
||||
"PublicAssetResponse",
|
||||
"PublicAssetLookupResponse",
|
||||
|
||||
@@ -123,3 +123,7 @@ class PublicAssetResponse(BaseModel):
|
||||
|
||||
class PublicAssetLookupResponse(BaseModel):
|
||||
asset: PublicAssetResponse
|
||||
|
||||
|
||||
class SuggestionUpdateRequest(BaseModel):
|
||||
accepted: bool
|
||||
|
||||
@@ -84,6 +84,55 @@ async def request_ai_suggestions(
|
||||
return created
|
||||
|
||||
|
||||
async def list_submissions(
|
||||
session: AsyncSession,
|
||||
*,
|
||||
owner_id: str,
|
||||
version_id: str | None = None,
|
||||
) -> list[Submission]:
|
||||
stmt = (
|
||||
select(Submission)
|
||||
.join(Submission.version)
|
||||
.join(CvVersion.document)
|
||||
.where(CvDocument.owner_id == owner_id)
|
||||
.options(selectinload(Submission.suggestions))
|
||||
)
|
||||
if version_id:
|
||||
stmt = stmt.where(Submission.version_id == version_id)
|
||||
result = await session.execute(stmt)
|
||||
return list(result.scalars().all())
|
||||
|
||||
|
||||
async def get_submission(
|
||||
session: AsyncSession, *, owner_id: str, submission_id: str
|
||||
) -> Submission | None:
|
||||
return await _get_submission_for_owner(session, owner_id, submission_id)
|
||||
|
||||
|
||||
async def update_suggestion(
|
||||
session: AsyncSession,
|
||||
*,
|
||||
owner_id: str,
|
||||
submission_id: str,
|
||||
suggestion_id: str,
|
||||
accepted: bool,
|
||||
) -> AiSuggestion | None:
|
||||
submission = await _get_submission_for_owner(session, owner_id, submission_id)
|
||||
if not submission:
|
||||
return None
|
||||
stmt = select(AiSuggestion).where(
|
||||
AiSuggestion.id == suggestion_id, AiSuggestion.submission_id == submission_id
|
||||
)
|
||||
result = await session.execute(stmt)
|
||||
suggestion = result.scalars().one_or_none()
|
||||
if not suggestion:
|
||||
return None
|
||||
suggestion.accepted = accepted
|
||||
await session.commit()
|
||||
await session.refresh(suggestion)
|
||||
return suggestion
|
||||
|
||||
|
||||
async def _get_version_for_owner(
|
||||
session: AsyncSession, owner_id: str, version_id: str
|
||||
) -> CvVersion | None:
|
||||
|
||||
Reference in New Issue
Block a user