chore(db): finish Prisma cutover to Drizzle Kit tooling
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:
1 parent
d8199ea1e4
commit
ba82789166
47 files changed
+365
-2787
No files matched your search
Whitespace-only changes.
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
Reference in new issue
Block a user