chore: consolidate full database schema into single init_schema.sql
This commit is contained in:
@@ -7,6 +7,15 @@ BEGIN
|
|||||||
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'record_status') THEN
|
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'record_status') THEN
|
||||||
CREATE TYPE record_status AS ENUM ('active','inactive');
|
CREATE TYPE record_status AS ENUM ('active','inactive');
|
||||||
END IF;
|
END IF;
|
||||||
|
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'client_status') THEN
|
||||||
|
CREATE TYPE client_status AS ENUM ('active','inactive');
|
||||||
|
END IF;
|
||||||
|
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'scope_type') THEN
|
||||||
|
CREATE TYPE scope_type AS ENUM ('company','site');
|
||||||
|
END IF;
|
||||||
|
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'permission_action') THEN
|
||||||
|
CREATE TYPE permission_action AS ENUM ('view','create','update','delete','manage');
|
||||||
|
END IF;
|
||||||
END
|
END
|
||||||
$$;
|
$$;
|
||||||
|
|
||||||
@@ -22,6 +31,14 @@ CREATE TABLE IF NOT EXISTS users (
|
|||||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||||
);
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS auth_sync_state (
|
||||||
|
user_id UUID PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
last_synced_at TIMESTAMPTZ,
|
||||||
|
source_version TEXT,
|
||||||
|
last_error TEXT,
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS companies (
|
CREATE TABLE IF NOT EXISTS companies (
|
||||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
company_key TEXT NOT NULL UNIQUE,
|
company_key TEXT NOT NULL UNIQUE,
|
||||||
@@ -31,6 +48,16 @@ CREATE TABLE IF NOT EXISTS companies (
|
|||||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||||
);
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS sites (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
site_key TEXT NOT NULL UNIQUE,
|
||||||
|
company_id UUID NOT NULL REFERENCES companies(id) ON DELETE CASCADE,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
status record_status NOT NULL DEFAULT 'active',
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS systems (
|
CREATE TABLE IF NOT EXISTS systems (
|
||||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
system_key TEXT NOT NULL UNIQUE,
|
system_key TEXT NOT NULL UNIQUE,
|
||||||
@@ -49,27 +76,19 @@ CREATE TABLE IF NOT EXISTS modules (
|
|||||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS sites (
|
-- legacy table: 保留相容舊流程
|
||||||
|
CREATE TABLE IF NOT EXISTS permissions (
|
||||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
site_key TEXT NOT NULL UNIQUE,
|
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||||
company_id UUID NOT NULL REFERENCES companies(id) ON DELETE CASCADE,
|
scope_type VARCHAR(32) NOT NULL,
|
||||||
name TEXT NOT NULL,
|
scope_id VARCHAR(128) NOT NULL,
|
||||||
status record_status NOT NULL DEFAULT 'active',
|
module VARCHAR(128) NOT NULL,
|
||||||
|
action VARCHAR(32) NOT NULL,
|
||||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
CONSTRAINT uq_permissions_user_scope_module_action
|
||||||
|
UNIQUE (user_id, scope_type, scope_id, module, action)
|
||||||
);
|
);
|
||||||
|
|
||||||
DO $$
|
|
||||||
BEGIN
|
|
||||||
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'scope_type') THEN
|
|
||||||
CREATE TYPE scope_type AS ENUM ('company','site');
|
|
||||||
END IF;
|
|
||||||
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'permission_action') THEN
|
|
||||||
CREATE TYPE permission_action AS ENUM ('view','create','update','delete','manage');
|
|
||||||
END IF;
|
|
||||||
END
|
|
||||||
$$;
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS user_scope_permissions (
|
CREATE TABLE IF NOT EXISTS user_scope_permissions (
|
||||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||||
@@ -90,14 +109,6 @@ ALTER TABLE user_scope_permissions
|
|||||||
OR (scope_type = 'site' AND site_id IS NOT NULL AND company_id IS NULL))
|
OR (scope_type = 'site' AND site_id IS NOT NULL AND company_id IS NULL))
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE UNIQUE INDEX IF NOT EXISTS uq_usp_company
|
|
||||||
ON user_scope_permissions(user_id, module_id, action, scope_type, company_id)
|
|
||||||
WHERE scope_type = 'company';
|
|
||||||
|
|
||||||
CREATE UNIQUE INDEX IF NOT EXISTS uq_usp_site
|
|
||||||
ON user_scope_permissions(user_id, module_id, action, scope_type, site_id)
|
|
||||||
WHERE scope_type = 'site';
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS permission_groups (
|
CREATE TABLE IF NOT EXISTS permission_groups (
|
||||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
group_key TEXT NOT NULL UNIQUE,
|
group_key TEXT NOT NULL UNIQUE,
|
||||||
@@ -123,17 +134,46 @@ CREATE TABLE IF NOT EXISTS permission_group_permissions (
|
|||||||
action TEXT NOT NULL,
|
action TEXT NOT NULL,
|
||||||
scope_type TEXT NOT NULL,
|
scope_type TEXT NOT NULL,
|
||||||
scope_id TEXT NOT NULL,
|
scope_id TEXT NOT NULL,
|
||||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
CONSTRAINT uq_pgp_group_rule UNIQUE (group_id, system, module, action, scope_type, scope_id)
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE UNIQUE INDEX IF NOT EXISTS uq_pgp_group_rule
|
CREATE TABLE IF NOT EXISTS api_clients (
|
||||||
ON permission_group_permissions(group_id, system, module, action, scope_type, scope_id);
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
client_key TEXT NOT NULL UNIQUE,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
status client_status NOT NULL DEFAULT 'active',
|
||||||
|
api_key_hash TEXT NOT NULL,
|
||||||
|
allowed_origins JSONB NOT NULL DEFAULT '[]'::jsonb,
|
||||||
|
allowed_ips JSONB NOT NULL DEFAULT '[]'::jsonb,
|
||||||
|
allowed_paths JSONB NOT NULL DEFAULT '[]'::jsonb,
|
||||||
|
rate_limit_per_min INTEGER,
|
||||||
|
expires_at TIMESTAMPTZ,
|
||||||
|
last_used_at TIMESTAMPTZ,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO systems (system_key, name, status)
|
||||||
|
VALUES ('member', 'Member Center', 'active')
|
||||||
|
ON CONFLICT (system_key) DO NOTHING;
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_users_authentik_sub ON users(authentik_sub);
|
CREATE INDEX IF NOT EXISTS idx_users_authentik_sub ON users(authentik_sub);
|
||||||
CREATE INDEX IF NOT EXISTS idx_sites_company_id ON sites(company_id);
|
CREATE INDEX IF NOT EXISTS idx_sites_company_id ON sites(company_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_permissions_user_id ON permissions(user_id);
|
||||||
CREATE INDEX IF NOT EXISTS idx_usp_user_id ON user_scope_permissions(user_id);
|
CREATE INDEX IF NOT EXISTS idx_usp_user_id ON user_scope_permissions(user_id);
|
||||||
CREATE INDEX IF NOT EXISTS idx_usp_module_id ON user_scope_permissions(module_id);
|
CREATE INDEX IF NOT EXISTS idx_usp_module_id ON user_scope_permissions(module_id);
|
||||||
CREATE INDEX IF NOT EXISTS idx_usp_company_id ON user_scope_permissions(company_id);
|
CREATE INDEX IF NOT EXISTS idx_usp_company_id ON user_scope_permissions(company_id);
|
||||||
CREATE INDEX IF NOT EXISTS idx_usp_site_id ON user_scope_permissions(site_id);
|
CREATE INDEX IF NOT EXISTS idx_usp_site_id ON user_scope_permissions(site_id);
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS uq_usp_company
|
||||||
|
ON user_scope_permissions(user_id, module_id, action, scope_type, company_id)
|
||||||
|
WHERE scope_type = 'company';
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS uq_usp_site
|
||||||
|
ON user_scope_permissions(user_id, module_id, action, scope_type, site_id)
|
||||||
|
WHERE scope_type = 'site';
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_api_clients_status ON api_clients(status);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_api_clients_expires_at ON api_clients(expires_at);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_systems_system_key ON systems(system_key);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_modules_module_key ON modules(module_key);
|
||||||
|
|
||||||
COMMIT;
|
COMMIT;
|
||||||
|
|||||||
@@ -1,106 +0,0 @@
|
|||||||
-- member_center: API 呼叫方白名單表
|
|
||||||
-- 位置: public schema
|
|
||||||
|
|
||||||
BEGIN;
|
|
||||||
|
|
||||||
CREATE EXTENSION IF NOT EXISTS pgcrypto;
|
|
||||||
|
|
||||||
DO $$
|
|
||||||
BEGIN
|
|
||||||
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'client_status') THEN
|
|
||||||
CREATE TYPE client_status AS ENUM ('active', 'inactive');
|
|
||||||
END IF;
|
|
||||||
END $$;
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS api_clients (
|
|
||||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
||||||
client_key TEXT NOT NULL UNIQUE,
|
|
||||||
name TEXT NOT NULL,
|
|
||||||
status client_status NOT NULL DEFAULT 'active',
|
|
||||||
|
|
||||||
-- 只存 hash,不存明文 key
|
|
||||||
api_key_hash TEXT NOT NULL,
|
|
||||||
|
|
||||||
-- 可先留空,之後再嚴格化
|
|
||||||
allowed_origins JSONB NOT NULL DEFAULT '[]'::jsonb,
|
|
||||||
allowed_ips JSONB NOT NULL DEFAULT '[]'::jsonb,
|
|
||||||
allowed_paths JSONB NOT NULL DEFAULT '[]'::jsonb,
|
|
||||||
|
|
||||||
rate_limit_per_min INTEGER,
|
|
||||||
expires_at TIMESTAMPTZ,
|
|
||||||
last_used_at TIMESTAMPTZ,
|
|
||||||
|
|
||||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
||||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_api_clients_status ON api_clients(status);
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_api_clients_expires_at ON api_clients(expires_at);
|
|
||||||
|
|
||||||
CREATE OR REPLACE FUNCTION set_updated_at_api_clients()
|
|
||||||
RETURNS TRIGGER AS $$
|
|
||||||
BEGIN
|
|
||||||
NEW.updated_at = NOW();
|
|
||||||
RETURN NEW;
|
|
||||||
END;
|
|
||||||
$$ LANGUAGE plpgsql;
|
|
||||||
|
|
||||||
DO $$
|
|
||||||
BEGIN
|
|
||||||
IF NOT EXISTS (
|
|
||||||
SELECT 1 FROM pg_trigger WHERE tgname = 'trg_api_clients_set_updated_at'
|
|
||||||
) THEN
|
|
||||||
CREATE TRIGGER trg_api_clients_set_updated_at
|
|
||||||
BEFORE UPDATE ON api_clients
|
|
||||||
FOR EACH ROW EXECUTE FUNCTION set_updated_at_api_clients();
|
|
||||||
END IF;
|
|
||||||
END $$;
|
|
||||||
|
|
||||||
-- 建議初始化 2~3 個 client(api_key_hash 先放占位,後續再更新)
|
|
||||||
INSERT INTO api_clients (
|
|
||||||
client_key,
|
|
||||||
name,
|
|
||||||
status,
|
|
||||||
api_key_hash,
|
|
||||||
allowed_origins,
|
|
||||||
allowed_ips,
|
|
||||||
allowed_paths,
|
|
||||||
rate_limit_per_min
|
|
||||||
)
|
|
||||||
VALUES
|
|
||||||
(
|
|
||||||
'mkt-backend',
|
|
||||||
'MKT Backend Service',
|
|
||||||
'active',
|
|
||||||
'REPLACE_WITH_BCRYPT_OR_ARGON2_HASH',
|
|
||||||
'[]'::jsonb,
|
|
||||||
'[]'::jsonb,
|
|
||||||
'["/internal/users/upsert-by-sub", "/internal/permissions"]'::jsonb,
|
|
||||||
600
|
|
||||||
),
|
|
||||||
(
|
|
||||||
'admin-frontend',
|
|
||||||
'Admin Frontend',
|
|
||||||
'active',
|
|
||||||
'REPLACE_WITH_BCRYPT_OR_ARGON2_HASH',
|
|
||||||
'["https://admin.ose.tw", "https://member.ose.tw"]'::jsonb,
|
|
||||||
'[]'::jsonb,
|
|
||||||
'["/admin"]'::jsonb,
|
|
||||||
300
|
|
||||||
),
|
|
||||||
(
|
|
||||||
'ops-local',
|
|
||||||
'Ops Local Tooling',
|
|
||||||
'inactive',
|
|
||||||
'REPLACE_WITH_BCRYPT_OR_ARGON2_HASH',
|
|
||||||
'[]'::jsonb,
|
|
||||||
'["127.0.0.1"]'::jsonb,
|
|
||||||
'["/internal", "/admin"]'::jsonb,
|
|
||||||
120
|
|
||||||
)
|
|
||||||
ON CONFLICT (client_key) DO NOTHING;
|
|
||||||
|
|
||||||
COMMIT;
|
|
||||||
|
|
||||||
-- 快速檢查
|
|
||||||
-- SELECT client_key, status, expires_at, created_at FROM api_clients ORDER BY client_key;
|
|
||||||
@@ -23,8 +23,8 @@
|
|||||||
- 前端任務進度與驗收條件
|
- 前端任務進度與驗收條件
|
||||||
- `docs/TASKPLAN_BACKEND.md`
|
- `docs/TASKPLAN_BACKEND.md`
|
||||||
- 後端任務進度與驗收條件
|
- 後端任務進度與驗收條件
|
||||||
- `docs/API_CLIENTS_SQL.sql`
|
- `backend/scripts/init_schema.sql`
|
||||||
- `api_clients` 白名單表與初始資料 SQL
|
- 一次建立完整 schema(含 `api_clients`)
|
||||||
- `docs/DB_SCHEMA_SNAPSHOT.md`
|
- `docs/DB_SCHEMA_SNAPSHOT.md`
|
||||||
- 目前資料庫 schema 快照(欄位/索引/約束)
|
- 目前資料庫 schema 快照(欄位/索引/約束)
|
||||||
|
|
||||||
|
|||||||
@@ -10,9 +10,9 @@ cp .env.example .env
|
|||||||
```
|
```
|
||||||
|
|
||||||
## 2. 建立資料表
|
## 2. 建立資料表
|
||||||
1. 先執行 `member.ose.tw/docs/API_CLIENTS_SQL.sql`
|
1. 先執行 `member.ose.tw/backend/scripts/init_schema.sql`(已含 `api_clients`)
|
||||||
2. 再執行 `member.ose.tw/backend/scripts/init_schema.sql`
|
2. 若是舊資料庫,補跑 `member.ose.tw/backend/scripts/migrate_align_company_site_member_system.sql`
|
||||||
3. 若是舊資料庫,補跑 `member.ose.tw/backend/scripts/migrate_add_authentik_user_id.sql`
|
3. 若是更舊資料庫,再補 `member.ose.tw/backend/scripts/migrate_add_authentik_user_id.sql`
|
||||||
|
|
||||||
## 3. 啟動服務
|
## 3. 啟動服務
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
- `docs/ORG_MEMBER_MANAGEMENT_PLAN.md`(公司組織/會員管理規劃)
|
- `docs/ORG_MEMBER_MANAGEMENT_PLAN.md`(公司組織/會員管理規劃)
|
||||||
|
|
||||||
## SQL 與配置
|
## SQL 與配置
|
||||||
- `docs/API_CLIENTS_SQL.sql`
|
- `backend/scripts/init_schema.sql`
|
||||||
- `docs/DB_SCHEMA_SNAPSHOT.md`
|
- `docs/DB_SCHEMA_SNAPSHOT.md`
|
||||||
|
|
||||||
## 給前端 AI 的一句話交接
|
## 給前端 AI 的一句話交接
|
||||||
|
|||||||
Reference in New Issue
Block a user