diff --git a/src/db.js b/src/db.js index ba3280e..a2e7f97 100644 --- a/src/db.js +++ b/src/db.js @@ -18,16 +18,18 @@ export async function initDb() { ); CREATE TABLE IF NOT EXISTS apps ( - id SERIAL PRIMARY KEY, - slug TEXT UNIQUE NOT NULL, - name TEXT NOT NULL, - description TEXT, - base_path TEXT NOT NULL, - icon TEXT NOT NULL DEFAULT 'ClipboardList', - theme_color TEXT NOT NULL DEFAULT '#1e3a5f', - category VARCHAR(100), - active BOOLEAN NOT NULL DEFAULT TRUE, - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() + id SERIAL PRIMARY KEY, + slug TEXT UNIQUE NOT NULL, + name TEXT NOT NULL, + description TEXT, + base_path TEXT NOT NULL, + icon TEXT NOT NULL DEFAULT 'ClipboardList', + theme_color TEXT NOT NULL DEFAULT '#1e3a5f', + category VARCHAR(100), + internal_host TEXT, + internal_port INTEGER DEFAULT 3080, + active BOOLEAN NOT NULL DEFAULT TRUE, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE TABLE IF NOT EXISTS user_app_perms ( @@ -36,22 +38,79 @@ export async function initDb() { granted_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), PRIMARY KEY (user_id, app_id) ); + + CREATE TABLE IF NOT EXISTS roles ( + id SERIAL PRIMARY KEY, + name TEXT UNIQUE NOT NULL, + slug TEXT UNIQUE NOT NULL, + description TEXT, + is_default BOOLEAN NOT NULL DEFAULT FALSE, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() + ); + + CREATE TABLE IF NOT EXISTS role_app_perms ( + role_id INTEGER NOT NULL REFERENCES roles(id) ON DELETE CASCADE, + app_id INTEGER NOT NULL REFERENCES apps(id) ON DELETE CASCADE, + PRIMARY KEY (role_id, app_id) + ); + + CREATE TABLE IF NOT EXISTS user_roles ( + user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, + role_id INTEGER NOT NULL REFERENCES roles(id) ON DELETE CASCADE, + granted_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + source TEXT NOT NULL DEFAULT 'manual' + CHECK (source IN ('manual', 'workforce_department', 'default')), + PRIMARY KEY (user_id, role_id) + ); + + CREATE TABLE IF NOT EXISTS workforce_department_roles ( + department_id TEXT NOT NULL PRIMARY KEY, + department_name TEXT NOT NULL, + role_id INTEGER NOT NULL REFERENCES roles(id) ON DELETE CASCADE + ); + + CREATE TABLE IF NOT EXISTS pending_registrations ( + id SERIAL PRIMARY KEY, + email TEXT NOT NULL, + wf_user_id TEXT NOT NULL, + wf_name TEXT NOT NULL, + pin_hash TEXT NOT NULL, + attempts INTEGER NOT NULL DEFAULT 0, + expires_at TIMESTAMPTZ NOT NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() + ); `) - // Add category column to existing installs + // Migrations for existing installs await pool.query(`ALTER TABLE apps ADD COLUMN IF NOT EXISTS category VARCHAR(100)`) + await pool.query(`ALTER TABLE apps ADD COLUMN IF NOT EXISTS internal_host TEXT`) + await pool.query(`ALTER TABLE apps ADD COLUMN IF NOT EXISTS internal_port INTEGER DEFAULT 3080`) + await pool.query(`ALTER TABLE users ADD COLUMN IF NOT EXISTS workforce_user_id TEXT`) + await pool.query(`CREATE UNIQUE INDEX IF NOT EXISTS users_workforce_user_id_idx ON users (workforce_user_id) WHERE workforce_user_id IS NOT NULL`) + await pool.query(`CREATE UNIQUE INDEX IF NOT EXISTS pending_reg_email_idx ON pending_registrations (email)`) - // Seed built-in apps + // Seed built-in apps — internal_host/port used by management for health checks and auto-deploy await pool.query(` - INSERT INTO apps (slug, name, description, base_path, icon, theme_color, category) + INSERT INTO apps (slug, name, description, base_path, icon, theme_color, category, internal_host, internal_port) VALUES - ('noticeboard', 'Noticeboard', 'Staff notices and announcements', '/notices', 'ClipboardList', '#1e3a5f', NULL), - ('kitchen', 'Kitchen Flash','Invoice processing and GP tracking', '/kitchen', 'ChefHat', '#e85d04', 'Kitchen'), - ('cashup', 'Cash Up', 'Hotel daily cashing up', '/cashup', 'Banknote', '#6b2d8b', 'Finance'), - ('housekeeping','Housekeeping', 'Room status and task management', '/hk', 'BedDouble', '#2d6a4f', 'Housekeeping'), - ('forecasting', 'Forecasting', 'Revenue forecasting and reporting', '/forecast','TrendingUp', '#0077b6', 'Finance'), - ('rates', 'Rate Scraper', 'Competitor rate monitoring', '/rates', 'Tag', '#7b4f00', 'Finance') - ON CONFLICT (slug) DO UPDATE SET icon = EXCLUDED.icon, category = EXCLUDED.category + ('noticeboard', 'Noticeboard', 'Staff notices and announcements', '/notices', 'ClipboardList', '#1e3a5f', NULL, '10.10.10.112', 3080), + ('kitchen', 'Kitchen Flash','Invoice processing and GP tracking', '/kitchen', 'ChefHat', '#e85d04', 'Kitchen', '10.10.10.110', 3080), + ('cashup', 'Cash Up', 'Hotel daily cashing up', '/cashup', 'Banknote', '#6b2d8b', 'Finance', '10.10.10.117', 3083), + ('housekeeping','Housekeeping', 'Room status and task management', '/hk', 'BedDouble', '#2d6a4f', 'Housekeeping', '10.10.10.114', 3080), + ('forecasting', 'Forecasting', 'Revenue forecasting and reporting', '/forecast','TrendingUp', '#0077b6', 'Finance', '10.10.10.113', 3080), + ('rates', 'Rate Scraper', 'Competitor rate monitoring', '/rates', 'Tag', '#7b4f00', 'Finance', '10.10.10.115', 3080) + ON CONFLICT (slug) DO UPDATE SET + icon = EXCLUDED.icon, + category = EXCLUDED.category, + internal_host = EXCLUDED.internal_host, + internal_port = EXCLUDED.internal_port + `) + + // Seed default Staff role + await pool.query(` + INSERT INTO roles (name, slug, description, is_default) + VALUES ('Staff', 'staff', 'Default role assigned to all self-registered employees', TRUE) + ON CONFLICT (slug) DO NOTHING `) // Seed first admin user if table is empty