Files
API/sql/admin/001_init.sql
Paddy bf7be65b03
Jenkins Production Deployment
Add admin identity module: better-auth, per-app permissions, invitations (#12)
Reviewed-on: #12
Co-authored-by: Patrick Müller <mail@pmueller.me>
Co-committed-by: Patrick Müller <mail@pmueller.me>
2026-09-06 10:41:54 +00:00

151 lines
7.2 KiB
SQL

-- nachklang_admin: identity, sessions and per-app permissions for every
-- *.nachklang.art app.
--
-- The better-auth tables below (user, session, account, verification, passkey,
-- rateLimit) mirror what better-auth 1.7.2 derives from the configuration in
-- src/models/admin/admin.auth.ts, including the `disabled` additionalField on
-- `user` and the `rateLimit` table that rateLimit.storage='database' requires.
-- On every better-auth upgrade: re-derive the table list, diff it against this
-- file, and add a numbered migration - never edit this one in place.
--
-- Table and column names are better-auth's own ("camel" casing, so `rateLimit`
-- and `userId`). MariaDB on Linux compares table names case-sensitively, so the
-- casing here is load-bearing. The two Nachklang-owned tables at the bottom use
-- the snake_case convention of the rest of this repo's SQL.
CREATE TABLE IF NOT EXISTS `user` (
`id` VARCHAR(36) NOT NULL,
`name` VARCHAR(255) NOT NULL,
`email` VARCHAR(255) NOT NULL,
`emailVerified` TINYINT(1) NOT NULL DEFAULT 0,
`image` TEXT DEFAULT NULL,
`createdAt` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updatedAt` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
-- Nachklang addition, declared through better-auth's additionalFields so
-- the adapter knows about it. Disabling also revokes the user's sessions.
`disabled` TINYINT(1) NOT NULL DEFAULT 0,
PRIMARY KEY (`id`),
UNIQUE KEY `user_email` (`email`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `session` (
`id` VARCHAR(36) NOT NULL,
`expiresAt` DATETIME NOT NULL,
`token` VARCHAR(255) NOT NULL,
`createdAt` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updatedAt` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
`ipAddress` VARCHAR(255) DEFAULT NULL,
`userAgent` TEXT DEFAULT NULL,
`userId` VARCHAR(36) NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `session_token` (`token`),
KEY `session_user` (`userId`),
CONSTRAINT `session_user_fk` FOREIGN KEY (`userId`) REFERENCES `user` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `account` (
`id` VARCHAR(36) NOT NULL,
-- 1.7 addition: distinguishes a local credential account
-- ("local:credential") from an OAuth issuer. Written by better-auth.
`issuer` VARCHAR(255) NOT NULL,
`accountId` VARCHAR(255) NOT NULL,
`providerId` VARCHAR(255) NOT NULL,
`userId` VARCHAR(36) NOT NULL,
`accessToken` TEXT DEFAULT NULL,
`refreshToken` TEXT DEFAULT NULL,
`idToken` TEXT DEFAULT NULL,
`accessTokenExpiresAt` DATETIME DEFAULT NULL,
`refreshTokenExpiresAt` DATETIME DEFAULT NULL,
`scope` TEXT DEFAULT NULL,
`password` TEXT DEFAULT NULL,
`createdAt` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updatedAt` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `account_user` (`userId`),
KEY `account_provider` (`providerId`, `accountId`),
CONSTRAINT `account_user_fk` FOREIGN KEY (`userId`) REFERENCES `user` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- Password-reset and e-mail-verification tokens.
CREATE TABLE IF NOT EXISTS `verification` (
`id` VARCHAR(36) NOT NULL,
`identifier` VARCHAR(255) NOT NULL,
`value` TEXT NOT NULL,
`expiresAt` DATETIME NOT NULL,
`createdAt` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updatedAt` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `verification_identifier` (`identifier`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `passkey` (
`id` VARCHAR(36) NOT NULL,
`name` VARCHAR(255) DEFAULT NULL,
`publicKey` TEXT NOT NULL,
`userId` VARCHAR(36) NOT NULL,
`credentialID` VARCHAR(255) NOT NULL,
`counter` INT NOT NULL DEFAULT 0,
`deviceType` VARCHAR(255) NOT NULL,
`backedUp` TINYINT(1) NOT NULL DEFAULT 0,
`transports` VARCHAR(255) DEFAULT NULL,
`createdAt` DATETIME DEFAULT CURRENT_TIMESTAMP,
`aaguid` VARCHAR(255) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `passkey_user` (`userId`),
KEY `passkey_credential` (`credentialID`),
CONSTRAINT `passkey_user_fk` FOREIGN KEY (`userId`) REFERENCES `user` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- Required by rateLimit.storage = 'database' in admin.auth.ts. Passenger may
-- run several API instances, and an in-memory limiter would give each of them
-- its own budget.
CREATE TABLE IF NOT EXISTS `rateLimit` (
`id` VARCHAR(36) NOT NULL,
`key` VARCHAR(255) NOT NULL,
`count` INT NOT NULL DEFAULT 0,
-- Epoch milliseconds, not a DATETIME: better-auth stores a number here.
`lastRequest` BIGINT NOT NULL DEFAULT 0,
PRIMARY KEY (`id`),
UNIQUE KEY `rate_limit_key` (`key`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- ---------------------------------------------------------------------------
-- Nachklang-owned tables
-- ---------------------------------------------------------------------------
-- Which apps a user may administer. `admin` is just another app: holding it is
-- what lets someone manage users and invitations. A permission is (app, role);
-- `access` is the only role today, and the key admits several per app so finer
-- ones can be added by inserting rows rather than by migrating this table.
CREATE TABLE IF NOT EXISTS `user_app_permissions` (
`user_id` VARCHAR(36) NOT NULL,
`app` ENUM('calendar','feedback','tickets','admin') NOT NULL,
-- One row per (user, app, role). `access` means "may use this app at all"
-- and is the only role today; the key allows several per app so a finer
-- permission can be added later by inserting rows, not by migrating.
`role` VARCHAR(32) NOT NULL DEFAULT 'access',
`granted_by` VARCHAR(36) DEFAULT NULL,
`granted_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
-- (user_id, app) is the leftmost prefix of this key, so the per-request
-- permission lookup needs no separate index.
PRIMARY KEY (`user_id`, `app`, `role`),
CONSTRAINT `uap_user_fk` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- The only route to a new account: there is no public sign-up. Only the
-- SHA-256 of the token is stored, so a dump of this table hands out no access.
CREATE TABLE IF NOT EXISTS `invitations` (
`id` INT AUTO_INCREMENT PRIMARY KEY,
`email` VARCHAR(255) NOT NULL,
`name` VARCHAR(255) NOT NULL,
`token_hash` CHAR(64) NOT NULL,
`permissions` JSON NOT NULL,
`invited_by` VARCHAR(36) DEFAULT NULL,
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
`expires_at` DATETIME NOT NULL,
`accepted_at` DATETIME DEFAULT NULL,
`revoked_at` DATETIME DEFAULT NULL,
UNIQUE KEY `inv_token_hash` (`token_hash`),
KEY `inv_email` (`email`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;