Add admin_users columns needed by create-user flow
Deploy to route.crispygoat.com / deploy (push) Successful in 4m27s
Deploy to route.crispygoat.com / deploy (push) Successful in 4m27s
The create-user action in src/actions/admin/users.ts was written against a richer admin_users schema than the one that landed in 0001_init.sql. The 0001 schema has just `name` plus the role-derived can_manage_<X> columns; the action also references display_name, phone_number, brand_id, can_manage_pickup/messages/refunds/users, active, must_change_password, auth_provider, auth_subject, and last_login. Result: 'column "display_name" of relation "admin_users" does not exist' on any create-user submit. Migration 0043 adds the missing columns with sensible defaults (ADD COLUMN IF NOT EXISTS, so re-runnable). The four extra can_manage_* flags are vestigial — getAdminUser() in lib/admin-permissions.ts derives per-user permissions from the role via permissionsForRole() and never reads them — but they exist so the create-user form's per-user toggles persist, and the migration header documents the cleanup target. The db/schema/brands.ts Drizzle adminUsers definition is extended in lockstep so the inferred TS types stay in sync with the table. brand_id is left as a denormalized column (the link table admin_user_brands is the source of truth and is what getAdminUser() reads); getAdminUsers() in the action joins on au.brand_id so keeping the column avoids rewriting the SELECT. Gitea CI runs scripts/migrate.js before the build, so this lands on prod automatically on the next push.
This commit is contained in:
@@ -0,0 +1,70 @@
|
||||
-- 0043_admin_users_extra_columns.sql
|
||||
--
|
||||
-- The application-layer admin user CRUD in `src/actions/admin/users.ts`
|
||||
-- was written for a richer `admin_users` schema than the one that
|
||||
-- landed in 0001_init.sql. The 0001 schema has just `name` plus the
|
||||
-- role-derived flag columns (`can_manage_orders`, `can_manage_products`,
|
||||
-- `can_manage_stops`, `can_manage_customers`, `can_manage_wholesale`,
|
||||
-- `can_manage_billing`, `can_manage_settings`, `can_manage_water_log`,
|
||||
-- `can_manage_time_tracking`, `can_manage_route_trace`,
|
||||
-- `can_manage_reports`, `can_manage_communications`).
|
||||
--
|
||||
-- The action also references these columns, which are not in 0001:
|
||||
--
|
||||
-- display_name, phone_number, brand_id,
|
||||
-- can_manage_pickup, can_manage_messages,
|
||||
-- can_manage_refunds, can_manage_users,
|
||||
-- active, must_change_password,
|
||||
-- auth_provider, auth_subject, last_login
|
||||
--
|
||||
-- This migration adds all of them so the create-user, list-users,
|
||||
-- update-user, and read-user flows work against the actual table.
|
||||
-- Re-runnable: each ALTER uses ADD COLUMN IF NOT EXISTS.
|
||||
--
|
||||
-- Notes on the new columns:
|
||||
--
|
||||
-- - `display_name` is what the UI shows in the user list / "logged in
|
||||
-- as" labels. The original `name` column from 0001 is left in place
|
||||
-- (Drizzle `adminUsers.name` still maps to it; `getAdminUser()` reads
|
||||
-- it) — `display_name` is the column the action's SQL touches so it
|
||||
-- stays the writable surface for the create-user form. Both can
|
||||
-- coexist; the next cleanup pass can collapse them.
|
||||
--
|
||||
-- - The four extra `can_manage_*` flags (pickup / messages / refunds /
|
||||
-- users) are not consulted by `getAdminUser()` — that lookup uses
|
||||
-- `permissionsForRole(role)` from `lib/admin-permissions.ts`, which
|
||||
-- derives flags from the user's role rather than from these
|
||||
-- columns. They are kept on the row so the create-user form's
|
||||
-- per-user permission toggles can persist; they simply do not yet
|
||||
-- affect runtime authorization. Cleanup target: either move them into
|
||||
-- a per-user permission table that the role-derived lookup merges
|
||||
-- in, or drop them from the form.
|
||||
--
|
||||
-- - `brand_id` is a denormalization of `admin_user_brands` (the link
|
||||
-- table is the source of truth for brand assignment, and
|
||||
-- `getAdminUser()` reads from it). The action writes both, which is
|
||||
-- redundant but not incorrect — keeping it makes the action's
|
||||
-- read-after-write (`getAdminUsers` joins on `au.brand_id`) work
|
||||
-- without rewriting the SELECT.
|
||||
--
|
||||
-- - `must_change_password` defaults to FALSE at the column level; the
|
||||
-- action sets it to TRUE on every create so the user is forced to
|
||||
-- set a new password on first sign-in.
|
||||
|
||||
ALTER TABLE admin_users
|
||||
ADD COLUMN IF NOT EXISTS display_name TEXT,
|
||||
ADD COLUMN IF NOT EXISTS phone_number TEXT,
|
||||
ADD COLUMN IF NOT EXISTS brand_id UUID REFERENCES brands(id) ON DELETE SET NULL,
|
||||
ADD COLUMN IF NOT EXISTS can_manage_pickup BOOLEAN NOT NULL DEFAULT true,
|
||||
ADD COLUMN IF NOT EXISTS can_manage_messages BOOLEAN NOT NULL DEFAULT true,
|
||||
ADD COLUMN IF NOT EXISTS can_manage_refunds BOOLEAN NOT NULL DEFAULT false,
|
||||
ADD COLUMN IF NOT EXISTS can_manage_users BOOLEAN NOT NULL DEFAULT false,
|
||||
ADD COLUMN IF NOT EXISTS active BOOLEAN NOT NULL DEFAULT true,
|
||||
ADD COLUMN IF NOT EXISTS must_change_password BOOLEAN NOT NULL DEFAULT false,
|
||||
ADD COLUMN IF NOT EXISTS auth_provider TEXT,
|
||||
ADD COLUMN IF NOT EXISTS auth_subject TEXT,
|
||||
ADD COLUMN IF NOT EXISTS last_login TIMESTAMPTZ;
|
||||
|
||||
-- Helpful index for the brand-name lookup the user list does
|
||||
-- (LEFT JOIN brands b ON b.id = au.brand_id).
|
||||
CREATE INDEX IF NOT EXISTS admin_users_brand_id_idx ON admin_users (brand_id);
|
||||
@@ -60,6 +60,23 @@ export const adminUsers = pgTable(
|
||||
userId: uuid("user_id"),
|
||||
email: text("email").notNull().unique(),
|
||||
name: text("name"),
|
||||
// The columns below were added in migration 0043. They back the
|
||||
// create-user / list-users / edit-user flows in
|
||||
// `src/actions/admin/users.ts`. See that migration's header for
|
||||
// notes on which of these are vestigial (the four can_manage_*
|
||||
// toggles) vs. read by the UI.
|
||||
displayName: text("display_name"),
|
||||
phoneNumber: text("phone_number"),
|
||||
brandId: uuid("brand_id"),
|
||||
canManagePickup: boolean("can_manage_pickup").notNull().default(true),
|
||||
canManageMessages: boolean("can_manage_messages").notNull().default(true),
|
||||
canManageRefunds: boolean("can_manage_refunds").notNull().default(false),
|
||||
canManageUsers: boolean("can_manage_users").notNull().default(false),
|
||||
active: boolean("active").notNull().default(true),
|
||||
mustChangePassword: boolean("must_change_password").notNull().default(false),
|
||||
authProvider: text("auth_provider"),
|
||||
authSubject: text("auth_subject"),
|
||||
lastLogin: timestamp("last_login", { withTimezone: true }),
|
||||
role: text("role", {
|
||||
enum: ["platform_admin", "brand_admin", "store_employee"],
|
||||
}).notNull().default("brand_admin"),
|
||||
|
||||
Reference in New Issue
Block a user