fix: finalize unified schema and correct permission snapshot mapping

This commit is contained in:
Chris
2026-03-30 02:22:27 +08:00
parent 42f9124f77
commit d79ed7c6fc
5 changed files with 131 additions and 30 deletions

View File

@@ -1,4 +1,4 @@
from sqlalchemy import and_, delete, or_, select from sqlalchemy import and_, delete, literal, or_, select
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from app.models.company import Company from app.models.company import Company
@@ -16,6 +16,7 @@ class PermissionsRepository:
def list_by_user(self, user_id: str, authentik_sub: str) -> list[tuple[str, str, str | None, str, str]]: def list_by_user(self, user_id: str, authentik_sub: str) -> list[tuple[str, str, str | None, str, str]]:
direct_stmt = ( direct_stmt = (
select( select(
literal("direct"),
UserScopePermission.scope_type, UserScopePermission.scope_type,
Company.company_key, Company.company_key,
Site.site_key, Site.site_key,
@@ -30,6 +31,7 @@ class PermissionsRepository:
) )
group_stmt = ( group_stmt = (
select( select(
literal("group"),
PermissionGroupPermission.scope_type, PermissionGroupPermission.scope_type,
PermissionGroupPermission.scope_id, PermissionGroupPermission.scope_id,
PermissionGroupPermission.system, PermissionGroupPermission.system,
@@ -44,10 +46,11 @@ class PermissionsRepository:
result: list[tuple[str, str, str | None, str, str]] = [] result: list[tuple[str, str, str | None, str, str]] = []
dedup = set() dedup = set()
for row in rows: for row in rows:
if len(row) == 5: source = row[0]
scope_type, scope_id, system_key, module_key, action = row if source == "group":
_, scope_type, scope_id, system_key, module_key, action = row
else: else:
scope_type, company_key, site_key, module_key, action = row _, scope_type, company_key, site_key, module_key, action = row
scope_id = company_key if scope_type == "company" else site_key scope_id = company_key if scope_type == "company" else site_key
system_key = module_key.split(".", 1)[0] if isinstance(module_key, str) and "." in module_key else None system_key = module_key.split(".", 1)[0] if isinstance(module_key, str) and "." in module_key else None
key = (scope_type, scope_id or "", system_key, module_key, action) key = (scope_type, scope_id or "", system_key, module_key, action)

View File

@@ -2,30 +2,13 @@ BEGIN;
CREATE EXTENSION IF NOT EXISTS pgcrypto; CREATE EXTENSION IF NOT EXISTS pgcrypto;
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'record_status') THEN
CREATE TYPE record_status AS ENUM ('active','inactive');
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
$$;
CREATE TABLE IF NOT EXISTS users ( CREATE TABLE IF NOT EXISTS users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(), id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
authentik_sub TEXT NOT NULL UNIQUE, authentik_sub TEXT NOT NULL UNIQUE,
authentik_user_id INTEGER, authentik_user_id INTEGER,
email TEXT UNIQUE, email TEXT UNIQUE,
display_name TEXT, display_name TEXT,
status record_status NOT NULL DEFAULT 'active', status VARCHAR(16) NOT NULL DEFAULT 'active',
is_active BOOLEAN NOT NULL DEFAULT TRUE, is_active BOOLEAN NOT NULL DEFAULT TRUE,
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()
@@ -43,7 +26,7 @@ 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,
name TEXT NOT NULL, name TEXT NOT NULL,
status record_status NOT NULL DEFAULT 'active', status VARCHAR(16) NOT NULL DEFAULT 'active',
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()
); );
@@ -53,7 +36,7 @@ CREATE TABLE IF NOT EXISTS sites (
site_key TEXT NOT NULL UNIQUE, site_key TEXT NOT NULL UNIQUE,
company_id UUID NOT NULL REFERENCES companies(id) ON DELETE CASCADE, company_id UUID NOT NULL REFERENCES companies(id) ON DELETE CASCADE,
name TEXT NOT NULL, name TEXT NOT NULL,
status record_status NOT NULL DEFAULT 'active', status VARCHAR(16) NOT NULL DEFAULT 'active',
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()
); );
@@ -62,7 +45,7 @@ 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,
name TEXT NOT NULL, name TEXT NOT NULL,
status record_status NOT NULL DEFAULT 'active', status VARCHAR(16) NOT NULL DEFAULT 'active',
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()
); );
@@ -71,7 +54,7 @@ CREATE TABLE IF NOT EXISTS modules (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(), id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
module_key TEXT NOT NULL UNIQUE, module_key TEXT NOT NULL UNIQUE,
name TEXT NOT NULL, name TEXT NOT NULL,
status record_status NOT NULL DEFAULT 'active', status VARCHAR(16) NOT NULL DEFAULT 'active',
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()
); );
@@ -93,8 +76,8 @@ 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,
module_id UUID NOT NULL REFERENCES modules(id) ON DELETE CASCADE, module_id UUID NOT NULL REFERENCES modules(id) ON DELETE CASCADE,
action permission_action NOT NULL, action VARCHAR(32) NOT NULL,
scope_type scope_type NOT NULL, scope_type VARCHAR(16) NOT NULL,
company_id UUID REFERENCES companies(id) ON DELETE CASCADE, company_id UUID REFERENCES companies(id) ON DELETE CASCADE,
site_id UUID REFERENCES sites(id) ON DELETE CASCADE, site_id UUID REFERENCES sites(id) ON DELETE CASCADE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
@@ -113,7 +96,7 @@ 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,
name TEXT NOT NULL, name TEXT NOT NULL,
status record_status NOT NULL DEFAULT 'active', status VARCHAR(16) NOT NULL DEFAULT 'active',
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()
); );
@@ -142,7 +125,7 @@ CREATE TABLE IF NOT EXISTS api_clients (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(), id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
client_key TEXT NOT NULL UNIQUE, client_key TEXT NOT NULL UNIQUE,
name TEXT NOT NULL, name TEXT NOT NULL,
status client_status NOT NULL DEFAULT 'active', status VARCHAR(16) NOT NULL DEFAULT 'active',
api_key_hash TEXT NOT NULL, api_key_hash TEXT NOT NULL,
allowed_origins JSONB NOT NULL DEFAULT '[]'::jsonb, allowed_origins JSONB NOT NULL DEFAULT '[]'::jsonb,
allowed_ips JSONB NOT NULL DEFAULT '[]'::jsonb, allowed_ips JSONB NOT NULL DEFAULT '[]'::jsonb,

View File

@@ -0,0 +1,27 @@
BEGIN;
-- users / master tables
ALTER TABLE users ALTER COLUMN status TYPE VARCHAR(16) USING status::text;
ALTER TABLE companies ALTER COLUMN status TYPE VARCHAR(16) USING status::text;
ALTER TABLE sites ALTER COLUMN status TYPE VARCHAR(16) USING status::text;
ALTER TABLE systems ALTER COLUMN status TYPE VARCHAR(16) USING status::text;
ALTER TABLE modules ALTER COLUMN status TYPE VARCHAR(16) USING status::text;
ALTER TABLE permission_groups ALTER COLUMN status TYPE VARCHAR(16) USING status::text;
-- api_clients
ALTER TABLE api_clients ALTER COLUMN status TYPE VARCHAR(16) USING status::text;
-- user scoped permissions
ALTER TABLE user_scope_permissions ALTER COLUMN action TYPE VARCHAR(32) USING action::text;
ALTER TABLE user_scope_permissions ALTER COLUMN scope_type TYPE VARCHAR(16) USING scope_type::text;
-- keep check constraint compatible with varchar
ALTER TABLE user_scope_permissions DROP CONSTRAINT IF EXISTS user_scope_permissions_check;
ALTER TABLE user_scope_permissions
ADD CONSTRAINT user_scope_permissions_check
CHECK (
((scope_type = 'company' AND company_id IS NOT NULL AND site_id IS NULL)
OR (scope_type = 'site' AND site_id IS NOT NULL AND company_id IS NULL))
);
COMMIT;

View File

@@ -0,0 +1,87 @@
# Frontend 交辦清單Schema v2
## 目標
前端要改成對應後端新模型:
- 公司companies
- 品牌站台sites
- 會員users
- 系統/模組systems/modules
- 權限群組permission-groups
## 既有頁面要調整
### 1) 權限管理頁 `/admin/permissions`
- Grant/Revoke payload 改為:
- `scope_type`: `company``site`
- `scope_id`: `company_key``site_key`
- `system`: 必填(例如 `mkt`
- `module`: 選填(空值代表系統層權限)
- `action`
- 表單新增 `system` 欄位。
- `module` 改成可選。
### 2) 我的權限頁 `/me/permissions`
- 顯示欄位改為:
- `scope_type`
- `scope_id`
- `system`
- `module`
- `action`
## 新增頁面
### 3) 系統管理 `/admin/systems`
- 列表:`GET /admin/systems`
- 新增:`POST /admin/systems`
### 4) 模組管理 `/admin/modules`
- 列表:`GET /admin/modules`
- 新增:`POST /admin/modules`
- `system_key`
- `module_key`
- `name`
### 5) 公司管理 `/admin/companies`
- 列表:`GET /admin/companies`
- 新增:`POST /admin/companies`
### 6) 站台管理 `/admin/sites`
- 列表:`GET /admin/sites`
- 新增:`POST /admin/sites`
- `company_key`
### 7) 會員列表 `/admin/members`
- 列表:`GET /admin/members`
### 8) 權限群組 `/admin/permission-groups`
- 群組列表/新增:
- `GET /admin/permission-groups`
- `POST /admin/permission-groups`
- 群組綁會員:
- `POST /admin/permission-groups/{group_key}/members/{authentik_sub}`
- `DELETE /admin/permission-groups/{group_key}/members/{authentik_sub}`
- 群組授權:
- `POST /admin/permission-groups/{group_key}/permissions/grant`
- `POST /admin/permission-groups/{group_key}/permissions/revoke`
## 共用資料載入(下拉選單)
權限表單應先載入:
- systems: `GET /admin/systems`
- modules: `GET /admin/modules`
- companies: `GET /admin/companies`
- sites: `GET /admin/sites`
## 認證(管理 API
所有 `/admin/*` API 一律帶:
- `X-Client-Key`
- `X-API-Key`
本地測試可用:
- `X-Client-Key: admin-frontend`
- `X-API-Key: dev-admin-key-123`
## 驗收條件
- 可以新增 system/module/company/site。
- 可以做 user 直接 grant/revoke。
- 可以建立 permission-group、加會員、做群組 grant/revoke。
- `/me/permissions/snapshot` 能看到直接權限 + 群組權限(去重後)。

View File

@@ -14,6 +14,7 @@
- `docs/TASKPLAN_FRONTEND.md` - `docs/TASKPLAN_FRONTEND.md`
- `docs/TASKPLAN_BACKEND.md` - `docs/TASKPLAN_BACKEND.md`
- `docs/ORG_MEMBER_MANAGEMENT_PLAN.md`(公司組織/會員管理規劃) - `docs/ORG_MEMBER_MANAGEMENT_PLAN.md`(公司組織/會員管理規劃)
- `docs/FRONTEND_HANDOFF_SCHEMA_V2.md`(前端交辦清單,直接給另一隻 AI
## SQL 與配置 ## SQL 與配置
- `backend/scripts/init_schema.sql` - `backend/scripts/init_schema.sql`