From e5db66e74aa6d33707c6321d518d60c496b9784f Mon Sep 17 00:00:00 2001 From: Tyler Date: Wed, 17 Jun 2026 11:22:43 -0600 Subject: [PATCH] Add admin_users columns needed by create-user flow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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_ 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. --- .../0043_admin_users_extra_columns.sql | 70 +++++++++++++++++++ db/schema/brands.ts | 17 +++++ 2 files changed, 87 insertions(+) create mode 100644 db/migrations/0043_admin_users_extra_columns.sql diff --git a/db/migrations/0043_admin_users_extra_columns.sql b/db/migrations/0043_admin_users_extra_columns.sql new file mode 100644 index 0000000..0071c55 --- /dev/null +++ b/db/migrations/0043_admin_users_extra_columns.sql @@ -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); diff --git a/db/schema/brands.ts b/db/schema/brands.ts index 8ecded7..f3b0c32 100644 --- a/db/schema/brands.ts +++ b/db/schema/brands.ts @@ -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"),