fix: finalize unified schema and correct permission snapshot mapping
This commit is contained in:
@@ -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)
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
27
backend/scripts/migrate_enum_to_text.sql
Normal file
27
backend/scripts/migrate_enum_to_text.sql
Normal 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;
|
||||||
87
docs/FRONTEND_HANDOFF_SCHEMA_V2.md
Normal file
87
docs/FRONTEND_HANDOFF_SCHEMA_V2.md
Normal 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` 能看到直接權限 + 群組權限(去重後)。
|
||||||
@@ -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`
|
||||||
|
|||||||
Reference in New Issue
Block a user