Files
Attendance/apps/worker/README.md
T

134 lines
8.4 KiB
Markdown

# Attendance worker
Cloudflare Worker API for **attendance** — [Better Auth](https://better-auth.com) on [D1](https://developers.cloudflare.com/d1/) served through an [Elysia](https://elysiajs.com) app using the experimental Cloudflare Worker adapter. Transactional email is rendered with [React Email](https://react.email) and delivered via [Resend](https://resend.com) (console in local dev).
## Stack
- **Framework**: [Elysia](https://elysiajs.com/integrations/cloudflare-worker.html) (`elysia/adapter/cloudflare-worker`) — mounts `auth.handler` at `/api/auth`, adds an `auth: true` route macro that injects typed `user`/`session`.
- **Auth**: Better Auth `1.6.26` — email/password, GitHub & Google social sign-in, plus `username`, `magicLink`, `twoFactor` and `organization` plugins.
- **Organizations**: Better Auth `organization` plugin with default owner/admin/member roles and native teams. Organizations may contain up to 25 teams and 100 accounts per team; dynamic roles remain disabled. Any authenticated user may create up to 10 organizations; 100 members and 100 pending invitations per organization; invitations expire after 48h; re-inviting an address cancels its previous pending invitation; deletion is owner-only and enabled.
- **Email**: React Email templates rendered on the Worker (`@react-email/render` workerd build) and delivered over plain `fetch` to the Resend REST API. In local development (no `RESEND_API_KEY`), envelopes are printed to the worker console instead.
- **Database**: Drizzle ORM on the D1 `DB` binding. Better Auth uses its generated schema in `src/db/auth-schema.ts`; attendance members, cameras, event history, and daily counters live in `src/db/attendance-schema.ts`.
## Project layout
```
src/
index.ts # Elysia app (entrypoint)
env.d.ts # Type augmentation for secret bindings
db/
index.ts # Typed Drizzle client for env.DB
schema.ts # Barrel exported to the runtime Drizzle client/Kit
auth-schema.ts # Better Auth + organization schema (CLI-generated)
attendance-schema.ts # Members, cameras, attendance events + daily summaries
lib/
auth/
auth.ts # Runtime auth instance (Drizzle adapter)
auth-options.ts # Shared auth config (plugins, providers, CORS origins)
auth.generate.ts # CLI-only Drizzle config for `auth generate`
invitation-email.ts# organization.sendInvitationEmail callback (URL + delivery)
magic-link-email.ts# magicLink.sendMagicLink callback
email/
render-email.ts # React Email → { html, text }
send-email.ts # Transport selection (Resend vs console)
types.ts # EmailEnvelope / EmailTransport / EmailDeliveryError
transports/
resend.ts # Resend REST API over fetch (idempotency-key aware)
console.ts # Local-dev-only console transport
emails/
components/EmailLayout.tsx
OrganizationInvitationEmail.tsx
MagicLinkEmail.tsx
drizzle/
0000_*.sql # Better Auth + organization migration
0001_*.sql # Attendance domain migration
0002_*.sql # Organization teams + account profile fields
meta/ # Drizzle schema snapshots and journal
drizzle.config.ts # Drizzle Kit schema/output configuration
wrangler.jsonc # D1 binding, vars, compat flags
```
## Local development
```bash
vp install
cp .dev.vars.example .dev.vars # fill in BETTER_AUTH_SECRET etc.
vp run dev # http://localhost:8787
```
Quick smoke test:
```bash
curl -X POST http://localhost:8787/api/auth/sign-up/email \
-H "Content-Type: application/json" \
-d '{"email":"[email protected]","password":"password123","name":"A"}'
```
Organization endpoints require an `Origin` header, e.g. `-H "Origin: http://localhost:5173"`.
## Invitation emails
Better Auth does **not** send invitation emails or generate invitation URLs — the app does. `organization.sendInvitationEmail` (in `lib/auth/invitation-email.ts`):
1. Builds `{WEB_APP_URL}/invitations/{invitationId}`.
2. Renders `emails/OrganizationInvitationEmail.tsx` to HTML + plain text.
3. Delivers via `sendEmail` (Resend in production, console in local dev).
The web app route `/invitations/:invitationId` loads the invitation by ID (Better Auth validates the session email), lets the user accept/decline, then explicitly activates the joined organization. Magic links reuse the same render + transport pipeline.
Invitation IDs and auth links are never logged outside local development; the console transport exists only for `localhost`/`127.0.0.1` runs.
## Migrations
The barrel in `src/db/schema.ts` exports the generated auth schema and the hand-maintained attendance schema. Drizzle Kit generates committed SQL in `drizzle/`, and Wrangler applies it to D1 outside the request lifecycle.
```bash
vp run db:generate # generate SQL after a schema change
vp run db:check # validate migration history
vp run db:migrate:local # apply to local dev DB (.wrangler/state)
vp run db:migrate:remote # apply to the configured remote D1 database
```
When Better Auth options or plugins change, regenerate only `auth-schema.ts`, review it, then generate a migration. Attendance tables stay isolated so the CLI cannot overwrite them:
```bash
vp run auth:schema
vp run db:generate
vp run db:migrate:local
```
`auth.generate.ts` exists because the CLI runs in Node without the Worker's D1 binding. It shares `auth-options.ts` with runtime auth and supplies an inert typed D1 client; schema generation does not execute database queries.
For a new remote environment, create the D1 database and replace the placeholder `database_id` in `wrangler.jsonc` before running the remote migration:
```bash
wrangler d1 create attendance-db
```
## Secrets & env
| Binding | Type | Notes |
| ------------------------------------------- | ----------- | --------------------------------------------------------------------------------------------------------------------------------------- |
| `DB` | D1 | `attendance-db` |
| `BETTER_AUTH_URL` | var | Base URL, `http://localhost:8787` in dev |
| `TRUSTED_ORIGINS` | var | Comma-separated extra CORS origins (e.g. `http://localhost:5173`) |
| `WEB_APP_URL` | var | Web app base URL used to build invitation/magic-link URLs (e.g. `http://localhost:5173`) |
| `EMAIL_FROM` | var | Sender for transactional email, e.g. `Attendance <[email protected]>` |
| `EMAIL_REPLY_TO` | var | Optional reply-to for transactional email |
| `BETTER_AUTH_SECRET` | **secret** | `wrangler secret put BETTER_AUTH_SECRET`; ≥32 chars |
| `RESEND_API_KEY` | **secret** | `wrangler secret put RESEND_API_KEY`; enables production email delivery. Without it, email is only printed to the console in local dev. |
| `GITHUB_CLIENT_ID` / `GITHUB_CLIENT_SECRET` | **secrets** | optional — enables GitHub sign-in |
| `GOOGLE_CLIENT_ID` / `GOOGLE_CLIENT_SECRET` | **secrets** | optional — enables Google sign-in |
> `src/lib/auth/auth-options.ts` builds `socialProviders` conditionally, so providers are only registered when their secrets are set.
## Deploy
```bash
vp run cf-typegen # after changing wrangler.jsonc bindings
wrangler secret put BETTER_AUTH_SECRET
wrangler secret put RESEND_API_KEY # required for production email
vp run db:migrate:remote
vp run deploy
```