feat(09-02): implement exact backend identity migrations
- Add the admin jti table, reset cutoff, and Winter indexes without AutoMigrate - Reapply the developer and publisher seed idempotently and allow repeated role codes
This commit is contained in:
@@ -21,40 +21,6 @@ const (
|
|||||||
msgServerError = "Server error"
|
msgServerError = "Server error"
|
||||||
)
|
)
|
||||||
|
|
||||||
// BackendUserRole is the Winter backend_user_roles row.
|
|
||||||
type BackendUserRole struct {
|
|
||||||
ID uint `gorm:"column:id;primaryKey"`
|
|
||||||
Name string `gorm:"column:name"`
|
|
||||||
Code string `gorm:"column:code"`
|
|
||||||
Description string `gorm:"column:description"`
|
|
||||||
Permissions string `gorm:"column:permissions"`
|
|
||||||
IsSystem bool `gorm:"column:is_system"`
|
|
||||||
CreatedAt time.Time `gorm:"column:created_at"`
|
|
||||||
UpdatedAt time.Time `gorm:"column:updated_at"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (BackendUserRole) TableName() string { return "backend_user_roles" }
|
|
||||||
|
|
||||||
// BackendUser is the Winter backend_users row. It is not a frontend user.
|
|
||||||
type BackendUser struct {
|
|
||||||
ID uint `gorm:"column:id;primaryKey"`
|
|
||||||
FirstName string `gorm:"column:first_name"`
|
|
||||||
LastName string `gorm:"column:last_name"`
|
|
||||||
Login string `gorm:"column:login"`
|
|
||||||
Email string `gorm:"column:email"`
|
|
||||||
Password string `gorm:"column:password"`
|
|
||||||
IsActivated bool `gorm:"column:is_activated"`
|
|
||||||
IsSuperuser bool `gorm:"column:is_superuser"`
|
|
||||||
RoleID *uint `gorm:"column:role_id"`
|
|
||||||
LastLogin *time.Time `gorm:"column:last_login"`
|
|
||||||
CreatedAt time.Time `gorm:"column:created_at"`
|
|
||||||
UpdatedAt time.Time `gorm:"column:updated_at"`
|
|
||||||
DeletedAt gorm.DeletedAt `gorm:"column:deleted_at"`
|
|
||||||
Role BackendUserRole
|
|
||||||
}
|
|
||||||
|
|
||||||
func (BackendUser) TableName() string { return "backend_users" }
|
|
||||||
|
|
||||||
// BackendUsers loads activated backend principals. It never reads frontend users.
|
// BackendUsers loads activated backend principals. It never reads frontend users.
|
||||||
type BackendUsers struct {
|
type BackendUsers struct {
|
||||||
DB *gorm.DB
|
DB *gorm.DB
|
||||||
|
|||||||
@@ -4,11 +4,55 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"git.golem15.com/golem15/summercms/bouncer"
|
"git.golem15.com/golem15/summercms/bouncer"
|
||||||
"git.golem15.com/golem15/summercms/pact"
|
"git.golem15.com/golem15/summercms/pact"
|
||||||
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// BackendUserRole is the Winter backend_user_roles row.
|
||||||
|
type BackendUserRole struct {
|
||||||
|
ID uint `gorm:"column:id;primaryKey"`
|
||||||
|
Name string `gorm:"column:name"`
|
||||||
|
Code string `gorm:"column:code"`
|
||||||
|
Description string `gorm:"column:description"`
|
||||||
|
Permissions string `gorm:"column:permissions"`
|
||||||
|
IsSystem bool `gorm:"column:is_system"`
|
||||||
|
CreatedAt time.Time `gorm:"column:created_at"`
|
||||||
|
UpdatedAt time.Time `gorm:"column:updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (BackendUserRole) TableName() string { return "backend_user_roles" }
|
||||||
|
|
||||||
|
// BackendUser is the Winter backend_users row. It is not a frontend user.
|
||||||
|
// Nullable Winter columns are mapped so a copied row round-trips without a
|
||||||
|
// schema transform. TokensValidAfter is the admin reset cutoff, not a Winter column.
|
||||||
|
type BackendUser struct {
|
||||||
|
ID uint `gorm:"column:id;primaryKey"`
|
||||||
|
FirstName string `gorm:"column:first_name"`
|
||||||
|
LastName string `gorm:"column:last_name"`
|
||||||
|
Login string `gorm:"column:login"`
|
||||||
|
Email string `gorm:"column:email"`
|
||||||
|
Password string `gorm:"column:password"`
|
||||||
|
ActivationCode string `gorm:"column:activation_code"`
|
||||||
|
PersistCode string `gorm:"column:persist_code"`
|
||||||
|
ResetPasswordCode string `gorm:"column:reset_password_code"`
|
||||||
|
Permissions string `gorm:"column:permissions"`
|
||||||
|
IsActivated bool `gorm:"column:is_activated"`
|
||||||
|
IsSuperuser bool `gorm:"column:is_superuser"`
|
||||||
|
RoleID *uint `gorm:"column:role_id"`
|
||||||
|
ActivatedAt *time.Time `gorm:"column:activated_at"`
|
||||||
|
LastLogin *time.Time `gorm:"column:last_login"`
|
||||||
|
CreatedAt time.Time `gorm:"column:created_at"`
|
||||||
|
UpdatedAt time.Time `gorm:"column:updated_at"`
|
||||||
|
DeletedAt gorm.DeletedAt `gorm:"column:deleted_at"`
|
||||||
|
TokensValidAfter *time.Time `gorm:"column:tokens_valid_after"`
|
||||||
|
Role BackendUserRole
|
||||||
|
}
|
||||||
|
|
||||||
|
func (BackendUser) TableName() string { return "backend_users" }
|
||||||
|
|
||||||
// Option is a dropdown choice shared with later schema plans.
|
// Option is a dropdown choice shared with later schema plans.
|
||||||
type Option = pact.Option
|
type Option = pact.Option
|
||||||
|
|
||||||
|
|||||||
@@ -5,25 +5,30 @@ import (
|
|||||||
"gorm.io/gorm"
|
"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 and
|
// BackendAdminMigrations creates Winter-shaped backend identity tables and
|
||||||
// seeds the developer and publisher system roles. History is isolated under
|
// seeds the developer and publisher system roles. History is isolated under
|
||||||
// the summercms.cabana plugin id.
|
// 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{
|
var BackendAdminMigrations = []*gormigrate.Migration{
|
||||||
{
|
{
|
||||||
ID: "202609240001_backend_admin_identity",
|
ID: "202609240001_backend_admin_identity",
|
||||||
Migrate: func(tx *gorm.DB) error {
|
Migrate: func(tx *gorm.DB) error {
|
||||||
stmts := []string{
|
stmts := []string{
|
||||||
`CREATE TABLE backend_user_roles (
|
`CREATE TABLE IF NOT EXISTS backend_user_roles (
|
||||||
id SERIAL PRIMARY KEY,
|
id SERIAL PRIMARY KEY,
|
||||||
name TEXT NOT NULL UNIQUE,
|
name TEXT NOT NULL UNIQUE,
|
||||||
code TEXT UNIQUE,
|
code TEXT,
|
||||||
description TEXT,
|
description TEXT,
|
||||||
permissions TEXT,
|
permissions TEXT,
|
||||||
is_system BOOLEAN NOT NULL DEFAULT FALSE,
|
is_system BOOLEAN NOT NULL DEFAULT FALSE,
|
||||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||||
)`,
|
)`,
|
||||||
`CREATE TABLE backend_users (
|
`CREATE TABLE IF NOT EXISTS backend_users (
|
||||||
id SERIAL PRIMARY KEY,
|
id SERIAL PRIMARY KEY,
|
||||||
first_name TEXT,
|
first_name TEXT,
|
||||||
last_name TEXT,
|
last_name TEXT,
|
||||||
@@ -41,14 +46,24 @@ var BackendAdminMigrations = []*gormigrate.Migration{
|
|||||||
last_login TIMESTAMPTZ,
|
last_login TIMESTAMPTZ,
|
||||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
deleted_at TIMESTAMPTZ
|
deleted_at TIMESTAMPTZ,
|
||||||
|
tokens_valid_after TIMESTAMPTZ
|
||||||
)`,
|
)`,
|
||||||
`CREATE INDEX backend_users_role_id_index ON backend_users (role_id)`,
|
`CREATE TABLE IF NOT EXISTS backend_jwt_blacklist (
|
||||||
`CREATE INDEX backend_users_deleted_at_index ON backend_users (deleted_at)`,
|
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)
|
`INSERT INTO backend_user_roles (name, code, description, permissions, is_system)
|
||||||
VALUES
|
VALUES
|
||||||
('Developer', 'developer', 'Site administrator with access to developer tools.', '{}', TRUE),
|
('Developer', 'developer', 'Site administrator with access to developer tools.', '{}', TRUE),
|
||||||
('Publisher', 'publisher', 'Site editor with access to publishing tools.', '{}', TRUE)`,
|
('Publisher', 'publisher', 'Site editor with access to publishing tools.', '{}', TRUE)
|
||||||
|
ON CONFLICT (name) DO NOTHING`,
|
||||||
}
|
}
|
||||||
for _, stmt := range stmts {
|
for _, stmt := range stmts {
|
||||||
if err := tx.Exec(stmt).Error; err != nil {
|
if err := tx.Exec(stmt).Error; err != nil {
|
||||||
@@ -58,10 +73,16 @@ VALUES
|
|||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
Rollback: func(tx *gorm.DB) error {
|
Rollback: func(tx *gorm.DB) error {
|
||||||
if err := tx.Exec(`DROP TABLE IF EXISTS backend_users`).Error; err != nil {
|
for _, stmt := range []string{
|
||||||
return err
|
`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 tx.Exec(`DROP TABLE IF EXISTS backend_user_roles`).Error
|
return nil
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user