Initial commit: EatMe attendance tracker
Google-SSO PWA for bistro employee clock-in/out, admin employee management, and stats with CSV export. Express + SQLite backend, React + Zustand frontend in the light-mono-tui design language. Multi-stage Dockerfile, compose.yaml for image-based deploys, nginx reverse-proxy template, and an OKF documentation bundle in docs/. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
58
docs/architecture/auth-flow.md
Normal file
58
docs/architecture/auth-flow.md
Normal file
@@ -0,0 +1,58 @@
|
||||
---
|
||||
type: Architecture Overview
|
||||
title: Authentication & authorization
|
||||
description: Google ID-token verification, the session cookie, and the admin/employee role split.
|
||||
tags: [architecture, auth, security]
|
||||
timestamp: 2026-08-12T00:00:00Z
|
||||
---
|
||||
|
||||
# Authentication & authorization
|
||||
|
||||
## Sign-in
|
||||
|
||||
1. The frontend loads the Google Identity Services script
|
||||
(`https://accounts.google.com/gsi/client`, in `frontend/index.html`) and
|
||||
renders the button in `frontend/src/auth/GoogleButton.tsx`.
|
||||
2. Google returns a signed ID token to the browser (`credential`). The
|
||||
frontend does **not** trust it — it POSTs it to `POST /api/auth/google`.
|
||||
3. The backend verifies the token server-side with `google-auth-library`
|
||||
(`backend/src/auth/google.ts`, `verifyGoogleIdToken`), checking the
|
||||
audience against `GOOGLE_CLIENT_ID` and requiring `email_verified`.
|
||||
|
||||
## Role resolution
|
||||
|
||||
Two independent checks decide who gets in and as what role
|
||||
(`backend/src/routes/auth.ts`):
|
||||
|
||||
- **Admin**: the verified email is in the `ADMIN_EMAILS` env var
|
||||
(comma-separated, lowercased — `backend/src/env.ts`). Admins do **not**
|
||||
need a row in the `employees` table.
|
||||
- **Employee**: the verified email matches an **active** row in
|
||||
[employees](/docs/data-model/employees.md).
|
||||
|
||||
If neither matches, login is rejected with 403 ("This account is not
|
||||
registered as an EatMe employee"). This is how the employee whitelist is
|
||||
enforced — there's no self-service signup.
|
||||
|
||||
## Session
|
||||
|
||||
On success the backend signs its own JWT (`backend/src/auth/session.ts`,
|
||||
`issueSession`) containing `{ email, name, role, employeeId }`, and sets it
|
||||
as an httpOnly, `SameSite=Lax` cookie (`eatme_session`, 12h TTL). All
|
||||
subsequent `/api/*` requests carry that cookie
|
||||
(`frontend/src/api/client.ts` sends `credentials: 'include'`); the backend
|
||||
never re-derives role from the DB on every request — it trusts the signed
|
||||
cookie until it expires.
|
||||
|
||||
`backend/src/auth/middleware.ts` provides three guards used by the routers:
|
||||
|
||||
- `requireAuth` — valid session cookie present
|
||||
- `requireAdmin` — `role === 'admin'`
|
||||
- `requireEmployee` — `employeeId != null` (blocks admins with no employee
|
||||
profile from hitting `/api/attendance/*`)
|
||||
|
||||
## Related
|
||||
|
||||
- [System overview](./overview.md)
|
||||
- [Auth API](/docs/api/auth-routes.md)
|
||||
- [Employees table](/docs/data-model/employees.md)
|
||||
Reference in New Issue
Block a user