TalentSync

AI resume builder and ATS optimizer for job seekers. Tailored resumes, outreach, and interview prep from one profile.

Authentication API

FastAPI owns sign-in. Routes live under /api/v1/auth in backend/app/routes/auth.py. The Next.js app rewrites /api/v1/:path* to the backend so cookies stay first-party.

This is the job-seeker TalentSync app. NextAuth is gone.

Repository layout#

  • backend/app/routes/auth.py
  • backend/app/services/auth/oauth.py
  • backend/app/services/auth/service.py
  • backend/app/services/auth/tokens.py
  • backend/app/core/deps.py
  • backend/app/core/settings.py
  • frontend/lib/session.ts
  • frontend/components/providers/session-provider.tsx
  • frontend/next.config.js (rewrite)

Building blocks#

  • Google OAuth authorize + callback
  • Access JWT cookie + opaque refresh cookie
  • GET /me, POST /refresh, POST /logout
  • POST /update-role, DELETE /delete-account
  • Password routes gated by CREDENTIALS_AUTH_ENABLED (410 by default)

Cookie names: COOKIE_ACCESS_NAME (ts_access_token), COOKIE_REFRESH_NAME (ts_refresh_token).

How it fits together#

Endpoints#

Prefix: /api/v1/auth. Mounted in backend/app/main.py.

Live (Google product)#

Method Path Auth Notes
GET /oauth/{provider} no provider must be google
GET /oauth/{provider}/callback no sets cookies, redirects
GET /google no alias of /oauth/google
GET /google/callback no alias of /oauth/google/callback
POST /refresh refresh cookie rotates session
POST /logout refresh cookie optional revokes + clears cookies
GET /me access cookie or Bearer current user
POST /update-role yes body { role }
DELETE /delete-account yes cascade delete + clear cookies

410 unless CREDENTIALS_AUTH_ENABLED#

POST /register, /verify-email, /resend-verification, /login, /forgot-password, /reset-password.

Message: Password sign-in has been removed. Continue with Google instead. Code CREDENTIALS_AUTH_DISABLED.

Session cookies#

Access cookie path /. Refresh cookie path /api/v1/auth. SameSite from COOKIE_SAME_SITE (default lax). COOKIE_SECURE off in local dev.

issue_session returns both tokens. JSON still includes accessToken for non-browser clients.

Login#

Product path is Google only.

GET /me after that:

json
{
  "user": {
    "id": "...",
    "name": "...",
    "email": "...",
    "image": "...",
    "role": "USER",
    "isVerified": true,
    "roleSelectionRequired": true
  }
}

roleSelectionRequired is true when roleSelectedAt is null and an OAuth account exists.

Logout#

POST /logout reads the refresh cookie, revokes the Session row, deletes both cookies. Client signOut() in session-provider.tsx POSTs this then navigates.

Refresh#

POST /refresh requires ts_refresh_token. rotate_session rewrites the row, stores previousToken, and issues a new pair. Replay of an old refresh clears cookies and 401s.

The session provider refreshes on a 401 from /me and on a 15-minute timer.

Role update and account deletion#

POST /update-role with { "role": "USER" | "RECRUITER" | "ADMIN" } via auth_service.update_role.

DELETE /delete-account deletes the user (cascades Account, Session, domain rows) and clears cookies.

Avatar writes are POST /api/v1/user/update-avatar in backend/app/routes/user.py, not this router.

Dependencies#

Troubleshooting#

  • 400 unsupported provider: only google.
  • 400 Google not configured: missing GOOGLE_CLIENT_ID / GOOGLE_CLIENT_SECRET.
  • Callback ?error=oauth: bad state, missing code, or token exchange failure.
  • ?error=oauth_email_taken: email already on a local account.
  • 401 no session to refresh: refresh cookie not sent (path/domain).
  • 410 on /login: credentials auth is off. Use Google.