chore(db): finish Prisma cutover to Drizzle Kit tooling
CI / check (push) Failing after 9s
CI / release (push) Skipped
CI / deploy (push) Skipped

Move CMS SQL to drizzle/migrations, drop prisma packages/schema, wire drizzle-kit scripts, and regenerate schema names from src/db/schema.ts.

Co-authored-by: Cursor <[email protected]>
This commit is contained in:
SimoandCursor committed 2026-08-01 15:02:21 +02:00
1 parent d8199ea1e4
commit ba82789166
47 files changed
+365 -2787

No files matched your search

View File
Whitespace-only changes.
+1
View File
@@ -0,0 +1 @@
Draft SQL from `pnpm db:generate` (drizzle-kit). Never apply these automatically — copy reviewed statements into drizzle/migrations/ as numbered CMS migrations, then `pnpm db:migrate`.
@@ -0,0 +1,10 @@
-- CMS-owned settings table. Idempotent: safe to re-run.
-- Mirrors AtomCMS's Laravel `website_settings` (key UNIQUE, value TEXT).
CREATE TABLE IF NOT EXISTS website_settings (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
`key` VARCHAR(255) NOT NULL,
value TEXT NOT NULL,
comment VARCHAR(255) NULL,
PRIMARY KEY (id),
UNIQUE KEY website_settings_key_unique (`key`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+6
View File
@@ -0,0 +1,6 @@
-- Fortify-style 2FA columns on the emulator users table. Idempotent (MariaDB).
-- AtomCMS installs already have these; this only adds them when missing.
ALTER TABLE users
ADD COLUMN IF NOT EXISTS two_factor_secret TEXT NULL,
ADD COLUMN IF NOT EXISTS two_factor_recovery_codes TEXT NULL,
ADD COLUMN IF NOT EXISTS two_factor_confirmed_at TIMESTAMP NULL;
@@ -0,0 +1,7 @@
-- CMS password reset tokens (Laravel-compatible). Idempotent.
CREATE TABLE IF NOT EXISTS password_resets (
email VARCHAR(255) NOT NULL,
token VARCHAR(255) NOT NULL,
created_at TIMESTAMP NULL,
PRIMARY KEY (email)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,8 @@
-- radio_listener_points was created as a stub (id + timestamps only) but the
-- /radio/leaderboard query joins users on user_id and orders by points. Add the
-- real columns. Idempotent (MariaDB ADD COLUMN / INDEX IF NOT EXISTS).
ALTER TABLE radio_listener_points
ADD COLUMN IF NOT EXISTS user_id INT NOT NULL DEFAULT 0,
ADD COLUMN IF NOT EXISTS points INT NOT NULL DEFAULT 0;
CREATE INDEX IF NOT EXISTS radio_listener_points_points_idx ON radio_listener_points (points);
@@ -0,0 +1,13 @@
-- CMS-owned editable content boxes (AtomCMS WriteableBox). Rendered on the
-- public site and managed from housekeeping. Idempotent (MariaDB).
CREATE TABLE IF NOT EXISTS website_writeable_boxes (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
title VARCHAR(255) NOT NULL,
icon VARCHAR(255) NULL,
content TEXT NOT NULL,
position INT NOT NULL DEFAULT 0,
is_active TINYINT(1) NOT NULL DEFAULT 1,
created_at TIMESTAMP NULL,
updated_at TIMESTAMP NULL,
PRIMARY KEY (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+11
View File
@@ -0,0 +1,11 @@
-- Successful-login history (the AtomCMS "session logs" equivalent; our auth is
-- stateless JWT so there's no sessions table). Idempotent (MariaDB).
CREATE TABLE IF NOT EXISTS website_login_logs (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
user_id INT NOT NULL,
ip VARCHAR(45) NOT NULL,
user_agent VARCHAR(512) NULL,
created_at TIMESTAMP NULL,
PRIMARY KEY (id),
KEY website_login_logs_user_id_idx (user_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
@@ -0,0 +1,4 @@
-- 0008_add_bans_user_id_index.sql
-- Adds an index on bans.user_id to speed up ban lookups by user (used by
-- the access guard on every page request for logged-in users).
CREATE INDEX IF NOT EXISTS `bans_user_id_index` ON `bans` (`user_id`);
@@ -0,0 +1,29 @@
-- 0009_radio_contests_giveaways_columns.sql
-- Fills in the empty RadioContests and RadioGiveaways tables with meaningful
-- columns. These tables were created in migration 0004 with only id/timestamps.
ALTER TABLE `radio_contests`
ADD COLUMN IF NOT EXISTS `title` VARCHAR(255) NOT NULL AFTER `id`,
ADD COLUMN IF NOT EXISTS `description` TEXT NULL AFTER `title`,
ADD COLUMN IF NOT EXISTS `prize` VARCHAR(255) NULL AFTER `description`,
ADD COLUMN IF NOT EXISTS `start_date` TIMESTAMP(0) NULL AFTER `prize`,
ADD COLUMN IF NOT EXISTS `end_date` TIMESTAMP(0) NULL AFTER `start_date`,
ADD COLUMN IF NOT EXISTS `winner_id` BIGINT UNSIGNED NULL AFTER `end_date`,
ADD COLUMN IF NOT EXISTS `is_active` BOOLEAN NOT NULL DEFAULT true AFTER `winner_id`,
ADD COLUMN IF NOT EXISTS `created_by` BIGINT UNSIGNED NULL AFTER `is_active`,
MODIFY COLUMN `created_at` TIMESTAMP(0) NULL AFTER `created_by`,
MODIFY COLUMN `updated_at` TIMESTAMP(0) NULL AFTER `created_at`;
ALTER TABLE `radio_giveaways`
ADD COLUMN IF NOT EXISTS `title` VARCHAR(255) NOT NULL AFTER `id`,
ADD COLUMN IF NOT EXISTS `description` TEXT NULL AFTER `title`,
ADD COLUMN IF NOT EXISTS `prize` VARCHAR(255) NULL AFTER `description`,
ADD COLUMN IF NOT EXISTS `prize_amount` INT NOT NULL DEFAULT 0 AFTER `prize`,
ADD COLUMN IF NOT EXISTS `prize_currency` VARCHAR(50) NULL AFTER `prize_amount`,
ADD COLUMN IF NOT EXISTS `start_date` TIMESTAMP(0) NULL AFTER `prize_currency`,
ADD COLUMN IF NOT EXISTS `end_date` TIMESTAMP(0) NULL AFTER `start_date`,
ADD COLUMN IF NOT EXISTS `winner_id` BIGINT UNSIGNED NULL AFTER `end_date`,
ADD COLUMN IF NOT EXISTS `is_active` BOOLEAN NOT NULL DEFAULT true AFTER `winner_id`,
ADD COLUMN IF NOT EXISTS `created_by` BIGINT UNSIGNED NULL AFTER `is_active`,
MODIFY COLUMN `created_at` TIMESTAMP(0) NULL AFTER `created_by`,
MODIFY COLUMN `updated_at` TIMESTAMP(0) NULL AFTER `created_at`;
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,37 @@
-- Normalize ACL discriminator casing and ensure every emulator rank has a CMS role.
UPDATE `acl_model_permissions` SET `model_type` = 'Role' WHERE LOWER(`model_type`) = 'role';
UPDATE `acl_model_roles` SET `model_type` = 'User' WHERE LOWER(`model_type`) = 'user';
INSERT INTO `acl_roles` (`slug`, `title`, `description`)
SELECT CONCAT('rank_', pr.id), COALESCE(NULLIF(pr.rank_name, ''), CONCAT('Rank ', pr.id)), 'CMS role synchronized from permission_ranks'
FROM `permission_ranks` pr
WHERE NOT EXISTS (SELECT 1 FROM `acl_roles` ar WHERE ar.slug = CONCAT('rank_', pr.id));
-- Preserve the former moderator fallback as explicit ACL data.
INSERT INTO `acl_model_permissions` (`model_type`, `model_id`, `permission_id`)
SELECT 'Role', ar.id, ap.id
FROM `permission_ranks` pr
JOIN `acl_roles` ar ON ar.slug = CONCAT('rank_', pr.id)
JOIN `acl_permissions` ap ON (
pr.id >= 3 AND (
ap.slug = 'mod.dashboard' OR ap.slug = 'mod.actions' OR
(ap.slug LIKE 'mod.%' AND ap.slug LIKE '%.view')
)
)
WHERE NOT EXISTS (
SELECT 1 FROM `acl_model_permissions` amp
WHERE amp.model_type = 'Role' AND amp.model_id = ar.id AND amp.permission_id = ap.id
);
-- Preserve the former administrator read-only fallback as explicit ACL data.
INSERT INTO `acl_model_permissions` (`model_type`, `model_id`, `permission_id`)
SELECT 'Role', ar.id, ap.id
FROM `permission_ranks` pr
JOIN `acl_roles` ar ON ar.slug = CONCAT('rank_', pr.id)
JOIN `acl_permissions` ap ON (
pr.id >= 6 AND (ap.slug = 'admin.dashboard' OR (ap.slug LIKE 'admin.%' AND ap.slug LIKE '%.view'))
)
WHERE NOT EXISTS (
SELECT 1 FROM `acl_model_permissions` amp
WHERE amp.model_type = 'Role' AND amp.model_id = ar.id AND amp.permission_id = ap.id
);
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,35 @@
UPDATE `acl_model_permissions` SET `model_type` = 'Role' WHERE LOWER(`model_type`) = 'role';
UPDATE `acl_model_roles` SET `model_type` = 'User' WHERE LOWER(`model_type`) = 'user';
INSERT INTO `acl_permissions` (`slug`, `title`) VALUES
('admin.assets.import', 'Import assets'),
('admin.permissions.manage', 'Manage permissions')
ON DUPLICATE KEY UPDATE `title` = VALUES(`title`);
INSERT INTO `acl_roles` (`slug`, `title`, `description`)
SELECT CONCAT('rank_', pr.id), COALESCE(NULLIF(pr.rank_name, ''), CONCAT('Rank ', pr.id)), 'CMS role synchronized from permission_ranks'
FROM `permission_ranks` pr
WHERE NOT EXISTS (SELECT 1 FROM `acl_roles` ar WHERE ar.slug = CONCAT('rank_', pr.id));
INSERT INTO `acl_model_permissions` (`model_type`, `model_id`, `permission_id`)
SELECT 'Role', ar.id, ap.id
FROM `permission_ranks` pr
JOIN `acl_roles` ar ON ar.slug = CONCAT('rank_', pr.id)
CROSS JOIN `acl_permissions` ap
WHERE pr.id = (SELECT MAX(id) FROM `permission_ranks`)
AND ap.slug IN ('admin.permissions.manage', 'admin.assets.import')
AND NOT EXISTS (
SELECT 1 FROM `acl_model_permissions` amp
WHERE amp.model_type = 'Role' AND amp.model_id = ar.id AND amp.permission_id = ap.id
);
INSERT INTO `acl_model_permissions` (`model_type`, `model_id`, `permission_id`)
SELECT 'Role', ar.id, ap.id
FROM `website_housekeeping_permissions` whp
JOIN `acl_permissions` ap ON ap.slug = whp.permission
JOIN `permission_ranks` pr ON pr.id >= whp.min_rank
JOIN `acl_roles` ar ON ar.slug = CONCAT('rank_', pr.id)
WHERE NOT EXISTS (
SELECT 1 FROM `acl_model_permissions` amp
WHERE amp.model_type = 'Role' AND amp.model_id = ar.id AND amp.permission_id = ap.id
);
@@ -0,0 +1,33 @@
INSERT INTO `acl_permissions` (`slug`, `title`) VALUES
('admin.radio.view', 'View radio admin'),
('admin.radio.edit', 'Edit radio admin')
ON DUPLICATE KEY UPDATE `title` = VALUES(`title`);
-- Rank >= 6 gets radio view (matches other admin.*.view grants from 0012).
INSERT INTO `acl_model_permissions` (`model_type`, `model_id`, `permission_id`)
SELECT 'Role', ar.id, ap.id
FROM `permission_ranks` pr
JOIN `acl_roles` ar ON ar.slug = CONCAT('rank_', pr.id)
JOIN `acl_permissions` ap ON ap.slug = 'admin.radio.view'
WHERE pr.id >= 6
AND NOT EXISTS (
SELECT 1 FROM `acl_model_permissions` amp
WHERE amp.model_type = 'Role'
AND amp.model_id = ar.id
AND amp.permission_id = ap.id
);
-- Highest rank gets radio edit (same pattern as assets.import / permissions.manage).
INSERT INTO `acl_model_permissions` (`model_type`, `model_id`, `permission_id`)
SELECT 'Role', ar.id, ap.id
FROM `permission_ranks` pr
JOIN `acl_roles` ar ON ar.slug = CONCAT('rank_', pr.id)
CROSS JOIN `acl_permissions` ap
WHERE pr.id = (SELECT MAX(id) FROM `permission_ranks`)
AND ap.slug = 'admin.radio.edit'
AND NOT EXISTS (
SELECT 1 FROM `acl_model_permissions` amp
WHERE amp.model_type = 'Role'
AND amp.model_id = ar.id
AND amp.permission_id = ap.id
);
@@ -0,0 +1,6 @@
ALTER TABLE `website_housekeeping_permissions`
ADD COLUMN `group_name` VARCHAR(100) DEFAULT NULL AFTER `description`,
ADD COLUMN `depends_on` VARCHAR(255) DEFAULT NULL AFTER `group_name`,
ADD COLUMN `updated_by` INT DEFAULT NULL AFTER `updated_at`;
-- Migration tracking (no model change needed for audit log usage)
@@ -0,0 +1,4 @@
-- CMS JWT invalidation counter on users (stateless NextAuth sessions).
-- Idempotent (MariaDB).
ALTER TABLE users
ADD COLUMN IF NOT EXISTS website_jwt_version INT NOT NULL DEFAULT 0;
@@ -0,0 +1,69 @@
-- Repair admin ACL so ranks that can open the panel also see/use admin sections.
-- Root cause: sidebar filters by ACL; seed only granted some admin.*.view rows,
-- and later sync from housekeeping could leave mid/high ranks incomplete.
-- 1) Every rank that already has admin.dashboard gets ALL admin.* permissions.
INSERT INTO `acl_model_permissions` (`model_type`, `model_id`, `permission_id`)
SELECT 'Role', ar.id, ap.id
FROM `acl_roles` ar
JOIN `acl_permissions` ap ON ap.slug LIKE 'admin.%'
WHERE EXISTS (
SELECT 1
FROM `acl_model_permissions` amp
JOIN `acl_permissions` apdash ON apdash.id = amp.permission_id
WHERE amp.model_type = 'Role'
AND amp.model_id = ar.id
AND apdash.slug = 'admin.dashboard'
)
AND NOT EXISTS (
SELECT 1
FROM `acl_model_permissions` amp2
WHERE amp2.model_type = 'Role'
AND amp2.model_id = ar.id
AND amp2.permission_id = ap.id
);
-- 2) Safety net matching original seed: ranks >= 6 get all admin.*.view + dashboard.
INSERT INTO `acl_model_permissions` (`model_type`, `model_id`, `permission_id`)
SELECT 'Role', ar.id, ap.id
FROM `permission_ranks` pr
JOIN `acl_roles` ar ON ar.slug = CONCAT('rank_', pr.id)
JOIN `acl_permissions` ap ON (
ap.slug = 'admin.dashboard'
OR (ap.slug LIKE 'admin.%' AND ap.slug LIKE '%.view')
)
WHERE pr.id >= 6
AND NOT EXISTS (
SELECT 1
FROM `acl_model_permissions` amp
WHERE amp.model_type = 'Role'
AND amp.model_id = ar.id
AND amp.permission_id = ap.id
);
-- 3) Ranks >= 7 also get edit/manage/execute tools used by the sidebar.
INSERT INTO `acl_model_permissions` (`model_type`, `model_id`, `permission_id`)
SELECT 'Role', ar.id, ap.id
FROM `permission_ranks` pr
JOIN `acl_roles` ar ON ar.slug = CONCAT('rank_', pr.id)
JOIN `acl_permissions` ap ON (
(ap.slug LIKE 'admin.%' AND ap.slug LIKE '%.edit')
OR ap.slug IN (
'admin.permissions.manage',
'admin.rcon.execute',
'admin.assets.import',
'admin.export',
'admin.analytics.export',
'admin.users.ban',
'admin.users.reset_password',
'admin.room.delete'
)
)
WHERE pr.id >= 7
AND NOT EXISTS (
SELECT 1
FROM `acl_model_permissions` amp
WHERE amp.model_type = 'Role'
AND amp.model_id = ar.id
AND amp.permission_id = ap.id
);
@@ -0,0 +1,9 @@
CREATE TABLE IF NOT EXISTS `website_login_logs` (
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
`user_id` INT NOT NULL,
`ip` VARCHAR(45) NOT NULL,
`user_agent` VARCHAR(512) DEFAULT NULL,
`created_at` TIMESTAMP NULL DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `website_login_logs_user_id_index` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;