-- Local dev only. Mirrors the table definitions in sql/admin/001_init.sql - -- keep the two in step - and seeds a ready-to-use dev account on top. USE nachklang_admin; 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; -- --------------------------------------------------------------------------- -- Dev seed: dev@nachklang.art / devpassword, with every app permission. -- Local only - this account exists nowhere but in this container. -- -- The password hash is better-auth's own scrypt format (salt:hash), produced -- with better-auth 1.7.2's hashPassword(). Regenerate it if better-auth ever -- changes that format; a hash it cannot parse shows up as "invalid password" -- on an otherwise correct sign-in. -- -- `issuer` must be exactly 'local:credential' - it is how better-auth 1.7 -- recognises a local password account when signing in. -- --------------------------------------------------------------------------- INSERT INTO `user` (`id`, `name`, `email`, `emailVerified`, `disabled`) VALUES ('dev-user-0000-0000-0000-000000000001', 'Dev Admin', 'dev@nachklang.art', 1, 0); INSERT INTO `account` (`id`, `issuer`, `accountId`, `providerId`, `userId`, `password`) VALUES ( 'dev-acct-0000-0000-0000-000000000001', 'local:credential', 'dev-user-0000-0000-0000-000000000001', 'credential', 'dev-user-0000-0000-0000-000000000001', 'e6a0485feb04b8fa64453db87badd8e1:85aaffd845e1e44fabc5be97d684c0f533845ed11088bd3f4d533ef277ead71da8372b5c6a72c7eae2d48e3270c50e2d13cffa4fcfb0b5cec456100b8f007ed2' ); INSERT INTO `user_app_permissions` (`user_id`, `app`, `role`) VALUES ('dev-user-0000-0000-0000-000000000001', 'calendar', 'access'), ('dev-user-0000-0000-0000-000000000001', 'feedback', 'access'), ('dev-user-0000-0000-0000-000000000001', 'tickets', 'access'), ('dev-user-0000-0000-0000-000000000001', 'admin', 'access');