package lagoon import ( "fmt" "strings" "github.com/go-gormigrate/gormigrate/v2" "gorm.io/gorm" ) // BackendJWTBlacklistTable is the framework-owned jti table for admin tokens. // It is distinct from the frontend jwt_blacklist table. const BackendJWTBlacklistTable = "backend_jwt_blacklist" // BackendAdminMigrations creates Winter-shaped backend identity tables, seeds // the developer and publisher system roles and makes backend user emails // unique case-insensitively. History is isolated under the summercms.cabana // plugin id. DDL is re-runnable so the system-role seed stays idempotent if // the history row is removed. var BackendAdminMigrations = []*gormigrate.Migration{ { ID: "202609240001_backend_admin_identity", Migrate: func(tx *gorm.DB) error { stmts := []string{ `CREATE TABLE IF NOT EXISTS backend_user_roles ( id SERIAL PRIMARY KEY, name TEXT NOT NULL UNIQUE, code TEXT, description TEXT, permissions TEXT, is_system BOOLEAN NOT NULL DEFAULT FALSE, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() )`, `CREATE TABLE IF NOT EXISTS backend_users ( id SERIAL PRIMARY KEY, first_name TEXT, last_name TEXT, login TEXT NOT NULL UNIQUE, email TEXT NOT NULL UNIQUE, password TEXT NOT NULL, activation_code TEXT, persist_code TEXT, reset_password_code TEXT, permissions TEXT, is_activated BOOLEAN NOT NULL DEFAULT FALSE, is_superuser BOOLEAN NOT NULL DEFAULT FALSE, role_id INTEGER REFERENCES backend_user_roles(id), activated_at TIMESTAMPTZ, last_login TIMESTAMPTZ, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), deleted_at TIMESTAMPTZ, tokens_valid_after TIMESTAMPTZ )`, `CREATE TABLE IF NOT EXISTS backend_jwt_blacklist ( jti TEXT PRIMARY KEY, expires_at TIMESTAMPTZ NOT NULL, valid_until TIMESTAMPTZ NOT NULL )`, `CREATE INDEX IF NOT EXISTS backend_users_role_id_index ON backend_users (role_id)`, `CREATE INDEX IF NOT EXISTS backend_users_deleted_at_index ON backend_users (deleted_at)`, `CREATE INDEX IF NOT EXISTS backend_users_activation_code_index ON backend_users (activation_code)`, `CREATE INDEX IF NOT EXISTS backend_users_reset_password_code_index ON backend_users (reset_password_code)`, `CREATE INDEX IF NOT EXISTS backend_user_roles_code_index ON backend_user_roles (code)`, `INSERT INTO backend_user_roles (name, code, description, permissions, is_system) VALUES ('Developer', 'developer', 'Site administrator with access to developer tools.', '{}', TRUE), ('Publisher', 'publisher', 'Site editor with access to publishing tools.', '{}', TRUE) ON CONFLICT (name) DO NOTHING`, } for _, stmt := range stmts { if err := tx.Exec(stmt).Error; err != nil { return err } } return nil }, Rollback: func(tx *gorm.DB) error { for _, stmt := range []string{ `DROP TABLE IF EXISTS backend_jwt_blacklist`, `DROP TABLE IF EXISTS backend_users`, `DROP TABLE IF EXISTS backend_user_roles`, } { if err := tx.Exec(stmt).Error; err != nil { return err } } return nil }, }, { // Emails are matched case-insensitively at login and by admin:create, // so the table enforces the same rule. Rows copied from Winter may // hold two emails that differ only in case; the migration refuses to // run and names them instead of picking an account to drop. ID: "202610010001_backend_users_email_ci_unique", Migrate: func(tx *gorm.DB) error { var dupes []struct { Email string Logins string } if err := tx.Raw(`SELECT lower(email) AS email, string_agg(login, ', ' ORDER BY id) AS logins FROM backend_users GROUP BY lower(email) HAVING count(*) > 1 ORDER BY 1`).Scan(&dupes).Error; err != nil { return err } if len(dupes) > 0 { parts := make([]string, len(dupes)) for i, d := range dupes { parts[i] = fmt.Sprintf("%s (logins: %s)", d.Email, d.Logins) } return fmt.Errorf("lagoon: backend_users has emails that differ only in case: %s; change or remove the duplicates, then run migrate again", strings.Join(parts, "; ")) } return tx.Exec(`CREATE UNIQUE INDEX IF NOT EXISTS backend_users_email_lower_unique ON backend_users (lower(email))`).Error }, Rollback: func(tx *gorm.DB) error { return tx.Exec(`DROP INDEX IF EXISTS backend_users_email_lower_unique`).Error }, }, }