From f2e992e0bdce197f89973b41954205947a23de02 Mon Sep 17 00:00:00 2001 From: kirukib Date: Fri, 7 Aug 2026 16:12:43 +0300 Subject: [PATCH] Add multi-method login UI and document auth method contracts. Surface Google, email, phone, and Telegram on login (mocked), polish the brand panel and fixes for Button/Menu, and keep the backend spec sheet aligned. Co-authored-by: Cursor --- docs/backend/OVERVIEW.md | 6 +- docs/backend/endpoints/README.md | 2 +- docs/backend/endpoints/auth.md | 26 +- docs/features/INDEX.md | 2 +- docs/features/auth-roles.md | 13 +- src/app/[locale]/login/page.tsx | 343 ++++++++++++++++++++++----- src/components/layout/app-topbar.tsx | 47 ++-- src/components/ui/button.tsx | 5 + src/components/ui/dropdown-menu.tsx | 20 +- src/messages/am.json | 19 +- src/messages/en.json | 19 +- 11 files changed, 392 insertions(+), 110 deletions(-) diff --git a/docs/backend/OVERVIEW.md b/docs/backend/OVERVIEW.md index 1a07895..5abf16d 100644 --- a/docs/backend/OVERVIEW.md +++ b/docs/backend/OVERVIEW.md @@ -28,10 +28,10 @@ For single-org sessions, `orgId` is implicit from JWT/session. Multi-org users ( | Mode (this phase) | Description | |-------------------|-------------| -| Mock demo | `/login` **test-user dropdown** (`demo_user_id`) sets session cookie with user + roles + org; legacy `demo_role` still accepted | -| Production (future) | OAuth2 / magic link / SSO — TBD with platform auth | +| Mock demo | `/login` offers **Google, email, phone, Telegram** (UI mocked) plus a **test-user dropdown**; any path sets session via `demo_user_id` | +| Production (future) | Google OIDC, email magic-link/OTP, SMS OTP, Telegram Login Widget / Mini App — see [`endpoints/auth.md`](endpoints/auth.md) | -Demo personas: `user_super`, `user_hr`, `user_fin`, `user_mem` — see [`endpoints/auth.md`](endpoints/auth.md) (`GET /v1/auth/demo-profiles`). +Demo personas: `user_super`, `user_hr`, `user_fin`, `user_mem` — see `GET /v1/auth/demo-profiles`. ### Session shape diff --git a/docs/backend/endpoints/README.md b/docs/backend/endpoints/README.md index c87ca17..c0ad83d 100644 --- a/docs/backend/endpoints/README.md +++ b/docs/backend/endpoints/README.md @@ -8,7 +8,7 @@ REST-style API contract for Gishen-B2B. All paths prefixed with `/v1` unless not | Group | Doc | Description | |-------|-----|-------------| -| Auth | [auth.md](auth.md) | Demo login personas, session, locale, `/me/profile` | +| Auth | [auth.md](auth.md) | Demo login + planned Google/email/phone/Telegram, session, locale, `/me/profile` | | Organisation | [org.md](org.md) | Org profile, registration, departments, verification | | Members | [members.md](members.md) | CRUD, import, invites, join, pharmacy ID card | | Packages | [packages.md](packages.md) | Benefit plans | diff --git a/docs/backend/endpoints/auth.md b/docs/backend/endpoints/auth.md index cf2f327..c5c510c 100644 --- a/docs/backend/endpoints/auth.md +++ b/docs/backend/endpoints/auth.md @@ -31,6 +31,30 @@ Legacy: `{ "demo_role": "HR_ADMIN", "locale": "en" }` Production (future): `{ "email", "password" }` or OAuth code exchange. +### Planned auth methods (UI present; production TBD) + +| Method | Future flow | Mock phase | +|--------|-------------|------------| +| Google | OAuth 2.0 / OIDC | Button → toast → demo persona session | +| Email | Magic link or password OTP | Same | +| Phone | SMS OTP (E.164) | Same | +| Telegram | Telegram Login Widget / Mini App deep link | Same | + +Demo continues to use `demo_user_id` until platform auth is wired. + +### Future endpoints (stubs) + +| Method | Path | Notes | +|--------|------|-------| +| `GET` | `/v1/auth/oauth/google/start` | Redirect to Google; callback exchanges code | +| `POST` | `/v1/auth/email/otp/start` | Body `{ "email" }` — send OTP / magic link | +| `POST` | `/v1/auth/email/otp/verify` | Body `{ "email", "code" }` — returns session | +| `POST` | `/v1/auth/phone/otp/start` | Body `{ "phone" }` E.164 | +| `POST` | `/v1/auth/phone/otp/verify` | Body `{ "phone", "code" }` | +| `POST` | `/v1/auth/telegram/callback` | Telegram Login Widget payload verification | + +All production methods return the same session envelope as `POST /v1/auth/login`. + ### Response 200 ```json @@ -279,7 +303,7 @@ Updated profile object. | Route | Purpose | |-------|---------| -| `/login` | Yimaru-style login — test-user dropdown + locale switcher | +| `/login` | Sign-in methods (Google, email, phone, Telegram — mocked) + demo persona dropdown + locale | | `/profile` | Own profile — avatar, org, role switch, sign out | ## Clinical withhold notes diff --git a/docs/features/INDEX.md b/docs/features/INDEX.md index 20c2bac..39a63e9 100644 --- a/docs/features/INDEX.md +++ b/docs/features/INDEX.md @@ -10,7 +10,7 @@ Feature → pages → entities → endpoints mapping for Gishen-B2B §3 features | Feature | Pages | Entities | Endpoints | Feature doc | |---------|-------|----------|-----------|---------------| -| Auth & roles | `/login` | SessionUser | `auth.md` | [auth-roles.md](auth-roles.md) | +| Auth & roles | `/login` (Google / email / phone / Telegram UI + demo personas) | SessionUser | `auth.md` | [auth-roles.md](auth-roles.md) | | Profile | `/profile` | SessionUser | `auth.md` (`/v1/me/profile`) | [profile.md](profile.md) | | Quick search | Portal topbar (Cmd/Ctrl+K) | — | — | [quick-search.md](quick-search.md) | | Portal layout | All portal (PageShell, DetailPageLayout, Bc, StatCard, Tabs) | — | — | [portal-layout.md](portal-layout.md) | diff --git a/docs/features/auth-roles.md b/docs/features/auth-roles.md index afabba1..4435b7c 100644 --- a/docs/features/auth-roles.md +++ b/docs/features/auth-roles.md @@ -8,7 +8,7 @@ Demo login with role selector, session management, and role-filtered navigation. | Route | Purpose | |-------|---------| -| `/login` | Yimaru-style split login; **test-user dropdown** (demo personas) + locale switcher; register/join CTAs | +| `/login` | Sign-in methods (Google, email, phone, Telegram — **mocked**) + demo persona dropdown + locale switcher; register/join CTAs | | `/profile` | Own account — see [profile.md](profile.md) | ## Roles @@ -34,11 +34,12 @@ Demo login with role selector, session management, and role-filtered navigation. ## Demo login UX -1. User picks a **test user** from the dropdown (not only a role): Selam Super, Hanna HR, Fikru Finance, Abebe Member. -2. Optional locale toggle (en/am) on the login panel. -3. Continue sets session via `loginAsUser(id)` and redirects to that user’s home path. -4. Topbar menu: Profile → `/profile`; "Switch demo user" → `/login`. -5. Multi-role demo (`user_super`) can switch active role on `/profile`. +1. Optional: **Google / Email / Phone / Telegram** (mock) — toast note, then signs in as the selected demo persona. +2. Or pick a **test user** from the dropdown: Selam Super, Hanna HR, Fikru Finance, Abebe Member. +3. Optional locale toggle (en/am) on the login panel. +4. Continue / method buttons set session via `loginAsUser(id)` and redirect to that user’s home path. +5. Topbar menu: Profile → `/profile`; "Switch demo user" → `/login`. +6. Multi-role demo (`user_super`) can switch active role on `/profile`. Pre-seeded demo data: single mock org (`org_yaltopia` / Yaltopia) with realistic members, packages, spend. diff --git a/src/app/[locale]/login/page.tsx b/src/app/[locale]/login/page.tsx index 3fed72e..fdc88d5 100644 --- a/src/app/[locale]/login/page.tsx +++ b/src/app/[locale]/login/page.tsx @@ -1,8 +1,9 @@ "use client"; -import { useState } from "react"; +import { useState, type ReactNode } from "react"; import Image from "next/image"; import { useTranslations } from "next-intl"; +import { toast } from "sonner"; import { brand } from "@/data/brand"; import { useAuth } from "@/providers/auth-provider"; import { Link, useRouter } from "@/i18n/routing"; @@ -17,11 +18,143 @@ import { SelectTrigger, SelectValue, } from "@/components/ui/select"; -import { Building2, Handshake, KeyRound } from "lucide-react"; +import { + ArrowRight, + Building2, + Handshake, + KeyRound, + Mail, + Package, + Phone, + Users, + Wallet, + type LucideIcon, +} from "lucide-react"; import { UserAvatar } from "@/components/shared/user-avatar"; +import { cn } from "@/lib/utils"; const PERSONAS = listDemoPersonas(); +const SIDE_FEATURES: { + icon: LucideIcon; + key: "sideFeaturePeople" | "sideFeaturePackages" | "sideFeatureFinance"; +}[] = [ + { icon: Users, key: "sideFeaturePeople" }, + { icon: Package, key: "sideFeaturePackages" }, + { icon: Wallet, key: "sideFeatureFinance" }, +]; + +type AuthMethod = "google" | "email" | "phone" | "telegram"; + +function GoogleMark({ className }: { className?: string }) { + return ( + + + + + + + ); +} + +function TelegramMark({ className }: { className?: string }) { + return ( + + + + ); +} + +function GetStartedLink({ + href, + icon: Icon, + title, + hint, + primary, +}: { + href: string; + icon: LucideIcon; + title: string; + hint: string; + primary?: boolean; +}) { + return ( + + + + + + {title} + {hint} + + + + ); +} + +function AuthMethodButton({ + label, + icon, + onClick, + disabled, +}: { + label: string; + icon: ReactNode; + onClick: () => void; + disabled?: boolean; +}) { + return ( + + ); +} + export default function LoginPage() { const t = useTranslations(); const { loginAsUser } = useAuth(); @@ -31,20 +164,22 @@ export default function LoginPage() { const selected = PERSONAS.find((p) => p.id === personaId) ?? PERSONAS[0]; - function handleContinue() { + function completeLogin() { if (!selected) return; setSubmitting(true); const next = loginAsUser(selected.id); router.push(ROLE_META[next.activeRole].homePath); } + function handleMethod(method: AuthMethod) { + const methodLabel = t(`login.${method}`); + toast.message(t("login.demoMethodNote", { method: methodLabel })); + completeLogin(); + } + return (
- {/* Atmospheric background */} -
+
- {/* Left brand panel — Yimaru-style split */} -
+ + +
@@ -101,7 +281,7 @@ export default function LoginPage() {
-

+

{t("login.eyebrow")}

@@ -113,6 +293,49 @@ export default function LoginPage() {

+
+

+ {t("login.signInWith")} +

+
+ } + disabled={submitting || !selected} + onClick={() => handleMethod("google")} + /> + } + disabled={submitting || !selected} + onClick={() => handleMethod("email")} + /> + } + disabled={submitting || !selected} + onClick={() => handleMethod("phone")} + /> + } + disabled={submitting || !selected} + onClick={() => handleMethod("telegram")} + /> +
+
+ +
+
+
+
+
+ + {t("login.orDemo")} + +
+
+