diff --git a/docs/api/admin-registration.md b/docs/api/admin-registration.md index 5e37a07..9d449b3 100644 --- a/docs/api/admin-registration.md +++ b/docs/api/admin-registration.md @@ -16,7 +16,13 @@ Content-Type: application/json { "name": "Admin Name", - "email": "admin@example.com" + "email": "admin@example.com", + "passkey": { + "id": "credential-id-from-webauthn", + "publicKey": "base64-encoded-public-key", + "counter": 0, + "deviceName": "MacBook Pro" + } } ``` @@ -108,3 +114,8 @@ Content-Type: application/json - Confirmation tokens expire after 10 minutes - Email sending is marked as TODO in the service layer - After confirmation, admin becomes `isActive: true` and `confirmed: true` +- Passkey is stored during registration and linked to the admin account +- The passkey `id` field is the credential ID from WebAuthn registration +- The passkey `publicKey` field must be base64 encoded +- The passkey `counter` field defaults to 0 if not provided +- The passkey `deviceName` field defaults to "Unknown Device" if not provided diff --git a/docs/entity-relationship-diagram.md b/docs/entity-relationship-diagram.md index 2af0afb..c4c629b 100644 --- a/docs/entity-relationship-diagram.md +++ b/docs/entity-relationship-diagram.md @@ -4,12 +4,32 @@ Please note that this is currently the **unencrypted data model**. Appointments, Notes, Attachments and Answers of course have to be encrypted before being stored in the database. +### Central database + ```mermaid erDiagram - ADMINS { + ADMIN { uuid id PK string email - string password + string name + timestamp created_at + timestamp updated_at + timestamp last_login_at + boolean is_active + boolean confirmed + string token "Email confirmation token" + timestamp token_valid_until + } + + ADMIN_PASSKEY { + string id PK "WebAuthn credential ID" + uuid admin_id FK + string public_key "Base64 encoded" + int counter + string device_name "MacBook Pro, YubiKey 5, etc." + timestamp created_at + timestamp updated_at + timestamp last_used_at } TENANT { @@ -17,17 +37,32 @@ erDiagram string short_name "Used as subdomain" string long_name text description "Optional" - blob logo "PNG, JPEG, GIF, or WEBP" - string brand_color - string default_language - int max_channels - int max_team_members - int auto_delete_days "Days after appointment to auto-delete" - boolean require_email "Requires clients to enter their e-mail address" - boolean require_phone "Requires clients to enter a phone number" - boolean url "Requires clients to enter a phone number" + bytea logo "PNG, JPEG, GIF, or WEBP" + string database_url "Connection string for tenant DB" + timestamp created_at + timestamp updated_at } + TENANT_CONFIG { + uuid id PK + uuid tenant_id FK + string name "Configuration key" + enum type "BOOLEAN, NUMBER, STRING" + string value "Configuration value as text" + timestamp created_at + timestamp updated_at + } + + + ADMIN ||--o{ ADMIN_PASSKEY : "has" + TENANT ||--o{ TENANT_CONFIG : "has" +``` + +### Tenant-specific database + +```mermaid +erDiagram + CLIENT { uuid id PK string hash_key "Generated from email" @@ -148,53 +183,69 @@ erDiagram ## Database Architecture Notes - **Multi-tenancy**: Each tenant operates with a separate database instance +- **Central vs. Tenant Databases**: + - Central database contains: `ADMIN`, `ADMIN_PASSKEY`, `TENANT`, `TENANT_CONFIG` + - Tenant-specific databases contain: `CLIENT`, `STAFF`, `CHANNEL`, `APPOINTMENT`, etc. - **No explicit tenant references**: Since each tenant has its own database, foreign key relationships don't need to reference the tenant - **Encryption**: The system uses end-to-end encryption with public/private key pairs for clients and staff - **Hash-based identification**: Both clients and staff are identified by hash keys derived from their credentials +- **WebAuthn Authentication**: Admins use WebAuthn passkeys for secure authentication - **Flexible appointments**: Appointments support multiple notes and attachments for comprehensive record keeping ## Entity Descriptions -### TENANT +### ADMIN (Central Database) -Central configuration entity (exists in a separate management database, not in tenant-specific databases) +System administrators who manage the platform and tenants. Uses WebAuthn for secure authentication. -### CLIENT +### ADMIN_PASSKEY (Central Database) + +WebAuthn credentials for admin authentication. Each admin can have multiple passkeys (different devices). + +### TENANT (Central Database) + +Central configuration entity for each tenant organization. Contains database connection information for tenant isolation. + +### TENANT_CONFIG (Central Database) + +Flexible configuration system for tenant-specific settings. Each tenant can have multiple typed configuration entries. + +### CLIENT (Tenant Database) End-to-end encrypted client records with optional email for notifications -### STAFF +### STAFF (Tenant Database) Practice staff members with minimal required information for privacy -### CHANNEL +### CHANNEL (Tenant Database) Represents bookable resources such as rooms, machines, or personnel that appointments can be scheduled for -### APPOINTMENT +### APPOINTMENT (Tenant Database) Core booking entity with flexible status management and expiry handling, now linked to specific channels -### NOTE +### NOTE (Tenant Database) Text-based annotations attached to appointments -### ATTACHMENT +### ATTACHMENT (Tenant Database) File attachments (documents, images, etc.) associated with appointments -### QUESTIONNAIRE +### QUESTIONNAIRE (Tenant Database) Collection of questions associated with a specific channel, can be activated/deactivated -### QUESTION +### QUESTION (Tenant Database) Individual questions that can be reused across multiple questionnaires, supporting different answer types -### QUESTIONNAIRE_QUESTION +### QUESTIONNAIRE_QUESTION (Tenant Database) Junction table linking questionnaires to questions with ordering information -### QUESTIONNAIRE_ANSWER +### QUESTIONNAIRE_ANSWER (Tenant Database) Stores answers provided by clients for specific appointments and questionnaires diff --git a/src/routes/api/admin/register/+server.ts b/src/routes/api/admin/register/+server.ts index 33b5ee7..d981472 100644 --- a/src/routes/api/admin/register/+server.ts +++ b/src/routes/api/admin/register/+server.ts @@ -23,9 +23,20 @@ registerOpenAPIRoute("/admin/register", "POST", { format: "email", description: "Admin's email address", example: "admin@example.com" + }, + passkey: { + type: "object", + description: "WebAuthn passkey data", + properties: { + id: { type: "string", description: "Credential ID from WebAuthn" }, + publicKey: { type: "string", description: "Base64 encoded public key" }, + counter: { type: "integer", description: "Signature counter", default: 0 }, + deviceName: { type: "string", description: "Device name for identification", example: "MacBook Pro" } + }, + required: ["id", "publicKey"] } }, - required: ["name", "email"] + required: ["name", "email", "passkey"] } } } @@ -84,14 +95,37 @@ registerOpenAPIRoute("/admin/register", "POST", { }); export const POST: RequestHandler = async ({ request }) => { + const log = logger.setContext("API"); + try { const body = await request.json(); + log.debug("Creating admin account with passkey", { + email: body.email, + passkeyId: body.passkey?.id, + deviceName: body.passkey?.deviceName + }); + + // Create admin account const admin = await AdminAccountService.createAdmin({ name: body.name, email: body.email }); + // Add the passkey to the admin account + await AdminAccountService.addPasskey(admin.id, { + id: body.passkey.id, + publicKey: body.passkey.publicKey, + counter: body.passkey.counter || 0, + deviceName: body.passkey.deviceName || "Unknown Device" + }); + + log.debug("Admin account and passkey created successfully", { + adminId: admin.id, + email: admin.email, + passkeyId: body.passkey.id + }); + return json( { message: "Admin account created successfully. Please check your email for confirmation.", @@ -101,7 +135,6 @@ export const POST: RequestHandler = async ({ request }) => { { status: 201 } ); } catch (error) { - const log = logger.setContext("API"); log.error("Admin registration error:", JSON.stringify(error || "?")); if (error instanceof ValidationError) {