# Auth & onboarding

Registration, sign-in, password recovery, and name/university setup. Product rules: [../onboarding.md](../onboarding.md). How this hooks to checkout and dashboard: [00-how-everything-connects.md](./00-how-everything-connects.md).

---

PAGE: /login
Purpose: Sign in with Google or email/password.
Access: public (logged-in visitors are redirected by proxy to `/auth/home` with safe `next`)
Entry points: Header **Sign in**; protected-route bounce with `next`; signup **Sign in →**; forgot/reset return; checkout `buildLoginHref`; `/login?next=`; `/login?message=password_updated`; `/login?error=` from `/auth/callback`.

SECTION: Brand panel (desktop)
  Copy: `Your exam is coming.` `Your course is ready.` `Course-specific study packs.`
  Data shown: static

  ELEMENT: Gradeful (logo, aria-label `Gradeful homepage`)
    Action: navigate
    Destination: `/`
    Writes to: none
    Reads from: none

SECTION: Status banner
  Copy: `Password updated. Please sign in.`
  Data shown: query `message=password_updated` only

SECTION: Callback error
  Copy: allowlisted human messages from `lib/auth/callback-errors.ts` (`auth_failed`, `access_denied`, `otp_expired`, …). Raw IdP errors are never shown.
  Data shown: query `error`

SECTION: Form header
  Copy: `Welcome back` `Sign in with Google or the email you used to create your account.`
  Data shown: static

  ELEMENT: Continue with Google
    Action: submit
    Destination: Google OAuth then `/auth/callback?next=` (safe)
    Writes to: `auth.users` / session; new Google users also get `profiles` via trigger
    Reads from: none
    Errors: `Social sign-in is unavailable. Please try again.` `Unable to connect. Please check your connection.`

SECTION: Divider
  Copy: `or email`
  Data shown: static

SECTION: Email panel
  Copy: Label `Account email` placeholder `you@email.com`. Label `Password`.
  Data shown: form state

  ELEMENT: Forgot password?
    Action: navigate
    Destination: `/forgot-password`
    Writes to: none
    Reads from: none

  ELEMENT: Sign in
    Action: submit
    Destination: `/auth/home?next=` (safe)
    Writes to: Auth session only
    Reads from: none
    Errors: `Email or password is incorrect.` `Unable to connect. Please check your connection.`

SECTION: Footer
  Copy: `Don't have an account?`

  ELEMENT: Sign up →
    Action: navigate
    Destination: `/signup` or `/signup?next=…`
    Writes to: none
    Reads from: `next` query if present

Phone OTP is not offered. Hosted Gradeful had zero phone identities.

---

PAGE: /signup
Purpose: Create a student account (Google or email).
Access: public (logged-in redirected by proxy)
Entry points: Login **Sign up →**; marketing; course page **Start this course** (`buildSignupHref` to checkout); `/signup?next=`.

SECTION: Form header
  Copy: `Create your account` `Google or email. Then your name and university — enough to open the right course.`
  Data shown: static

  ELEMENT: Continue with Google
    Action: submit
    Destination: Google then `/auth/callback?next=…`
    Writes to: `auth.users`; `profiles` via `private.handle_new_user` (role stays `student`)
    Reads from: none
    Errors: `Social sign-up is unavailable. Please try again.` `Unable to connect. Please check your connection.`

SECTION: Email signup
  Copy: Label `Email` placeholder `you@email.com`. Label `Password`. Help `Use at least 8 characters. Avoid passwords that have shown up in a data breach.` Label `Confirm password`.
  Data shown: form state

  ELEMENT: Create account
    Action: submit
    Destination: in-place “Check your email”
    Writes to: `auth.users`; `profiles` via trigger
    Reads from: none
    Errors: `Enter a valid email address.` `Passwords do not match.` `We could not create your account. Try signing in instead.` `Unable to connect. Please check your connection.`

SECTION: Footer
  ELEMENT: Sign in →
    Destination: `/login` or `/login?next=…`

SECTION: Check your email
  Copy: `Check your email` `We sent a verification link to {submittedEmail}` `Didn't receive it?`
  ELEMENT: Resend → Success: `Verification email sent.`
  ELEMENT: Back to sign in → `/login` (+ optional `next`)

---

PAGE: /forgot-password
Purpose: Request a password-reset email.
Access: public
Entry points: Login **Forgot password?**

SECTION: Card
  Copy: `Reset your password` `Enter your email and we'll send you a reset link.` Label `Email` placeholder `you@email.com`.
  ELEMENT: ← Back to login → `/login`
  ELEMENT: Send reset link / Sending...
    Destination: none (`redirectTo` `/reset-password`)
    Success: `Check your email for a reset link.`
    Errors: `We could not send the reset link. Please try again.` `Unable to connect. Please check your connection.`

---

PAGE: /reset-password
Purpose: Set a new password from the email recovery link.
Access: public (needs recovery session or `?code=`)
Entry points: Reset email link.

SECTION: Invalid
  Copy: `This reset link is invalid or has expired.`
  ELEMENT: Request a new reset link → `/forgot-password`

SECTION: Ready form
  Copy: Label `New password`. Help `Use at least 8 characters.` Label `Confirm password`.
  ELEMENT: Update password / Updating...
    Destination: `/login?message=password_updated` after `signOut`
    Writes to: `auth.users` password hash
    Errors: `Passwords do not match.` `We could not update your password. Try again.` `Unable to connect. Please check your connection.`

---

PAGE: /auth/callback
Purpose: Exchange OAuth/email `code` for a session; optional welcome email; send the student through `/auth/home` with safe `next`.
Access: public (session is created here)
Entry points: Google OAuth; email verification link; Auth redirects with `?code=&next=`.

SECTION: No UI
  Copy: none. On failure: `/login?error={allowlisted}` plus preserved safe `next`.
  ELEMENT: none (route handler)
    Action: navigate
    Destination: `/auth/home?next=` on success; login error URL on failure
    Writes to: Auth session cookies. Welcome email via Resend if `profiles.created_at` within 5 seconds.
    Reads from: `profiles.created_at`, `profiles.full_name`

---

PAGE: /auth/home
Purpose: After login, send incomplete name/university profiles to `/welcome?next=` and everyone else to the preserved destination (course, checkout, or `/dashboard`).
Access: logged-in (anonymous → `/login`)
Entry points: Default after login/signup/OAuth; proxy redirect from `/login`/`/signup` when already logged in.

SECTION: No UI
  ELEMENT: none (route handler)
    Action: navigate
    Destination: `resolveLoggedInDestination`
    Reads from: `profiles.full_name`, `university`, `onboarding_completed_at`

---

PAGE: /welcome
Purpose: Required onboarding only: who are you (name) and which university offering (AUB/LAU). Preserve `next`.
Access: logged-in; incomplete onboarding. Anonymous → `/login?next=/welcome`. Already complete → preserved `next`, `/dashboard` if active access, otherwise `/`.
Entry points: `/auth/home`; workspace layout; checkout; direct URL.

SECTION: Brand panel (desktop)
  Copy: `Your university.` `Your courses.` `Twenty seconds. Then you're in — not a marketing page.`

SECTION: Intro
  Copy: `Almost in` `Tell us where you study.` `Name, university, major. We use this to show the right packs — nothing to verify here.`

SECTION: Who are you?
  Copy: Label `Your name` placeholder `Maya Haddad`. `How Gradeful greets you — not a public username.`

SECTION: What course do you need?
  Copy: `Which university?` `AUB and LAU use different course codes. This picks the matching offering.`
  ELEMENT: AUB / LAU cards
  If `next` is checkout/course: `We'll send you back to checkout for the course you already picked.` / `We'll send you back to that course.`
  If homepage: `After this you can pick a course. Major and campus can wait.` plus continue to `/courses`.

SECTION: Submit
  Copy: `Continue to checkout` / `Continue to your course` / `Find a course`
  Writes to: `profiles.full_name`, `university`, `onboarding_completed_at` (optional campus/major if present)
  Toasts: `Add the name you use at uni.` `Pick AUB or LAU so we show the right packs.` `Could not save that. Try again.`

Major, minors, and LAU campus are not on this form.
