feat(role): add role_code across schema and APIs

This commit is contained in:
Chris
2026-04-03 15:49:22 +08:00
parent 94cec746cb
commit 405000ded5
15 changed files with 91 additions and 6 deletions

View File

@@ -4,7 +4,7 @@ PORT=8000
DB_HOST=127.0.0.1
DB_PORT=54321
DB_NAME=member_center
DB_NAME=member.ose.tw
DB_USER=member_ose
DB_PASSWORD=Dmrax5bKDf
@@ -29,7 +29,7 @@ KEYCLOAK_ADMIN_CLIENT_ID=member-backend
KEYCLOAK_ADMIN_CLIENT_SECRET=hat8BmxlP0eZ7CXuKbV4HwQ3abLHzAJ9
KEYCLOAK_ADMIN_REALM=master
PUBLIC_FRONTEND_ORIGINS=https://member.ose.tw,https://mkt.ose.tw,https://admin.ose.tw
PUBLIC_FRONTEND_ORIGINS=http://127.0.0.1:5173,http://localhost:5173,https://member.ose.tw,https://mkt.ose.tw,https://admin.ose.tw
INTERNAL_SHARED_SECRET=CHANGE_ME
MEMBER_REQUIRED_REALM_ROLES=admin,manager
ADMIN_REQUIRED_REALM_ROLES=admin,manager

View File

@@ -75,6 +75,8 @@ curl http://127.0.0.1:8000/healthz
- `GET /admin/members/{user_sub}/roles`
- `GET/POST/PATCH/DELETE /admin/api-clients`
> `roles` 現在包含 `role_code` 欄位(建議用於跨系統權限語意解析);`role_key` 保留為唯一識別鍵。
### Internal APIs (`X-Client-Key` + `X-API-Key`)
- `GET /internal/companies`
- `GET /internal/sites`
@@ -84,3 +86,10 @@ curl http://127.0.0.1:8000/healthz
- `POST /internal/users/upsert-by-sub`
- `GET /internal/users/{user_sub}/roles`
- `POST /internal/provider/users/ensure`
## DB Migration
- 既有 DB 升級(新增 `roles.role_code`
```bash
psql "$DATABASE_URL" -f scripts/migrate_add_role_code.sql
```

View File

@@ -71,6 +71,13 @@ def _generate_unique_key(prefix: str, exists_check) -> str:
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"failed_generate_{prefix.lower()}_key")
def _resolve_role_code(value: str | None, fallback_name: str) -> str:
candidate = (value or "").strip()
if candidate:
return candidate
return fallback_name.strip()
def _company_item(company) -> CompanyItem:
return CompanyItem(
id=company.id,
@@ -451,6 +458,7 @@ def list_roles(
RoleItem(
id=row.id,
role_key=row.role_key,
role_code=row.role_code,
system_key=system_map[row.system_id].system_key,
system_name=system_map[row.system_id].name,
name=row.name,
@@ -481,9 +489,11 @@ def create_role(payload: RoleCreateRequest, db: Session = Depends(get_db)) -> Ro
)
role_key = _generate_unique_key("RL", lambda key: roles_repo.get_by_key(key) is not None)
role_code = _resolve_role_code(payload.role_code, payload.name)
try:
row = roles_repo.create(
role_key=role_key,
role_code=role_code,
system_id=system.id,
name=payload.name,
description=payload.description,
@@ -496,6 +506,7 @@ def create_role(payload: RoleCreateRequest, db: Session = Depends(get_db)) -> Ro
return RoleItem(
id=row.id,
role_key=row.role_key,
role_code=row.role_code,
system_key=system.system_key,
system_name=system.name,
name=row.name,
@@ -527,6 +538,7 @@ def update_role(role_key: str, payload: RoleUpdateRequest, db: Session = Depends
system_id = system.id
target_system = system
next_provider_role_name = payload.name if payload.name is not None else role.name
next_role_code = _resolve_role_code(payload.role_code, next_provider_role_name)
next_description = payload.description if payload.description is not None else role.description
if target_system.id != old_system.id:
@@ -551,6 +563,7 @@ def update_role(role_key: str, payload: RoleUpdateRequest, db: Session = Depends
role = roles_repo.update(
role,
system_id=system_id,
role_code=next_role_code,
name=payload.name,
description=payload.description,
status=payload.status,
@@ -566,6 +579,7 @@ def update_role(role_key: str, payload: RoleUpdateRequest, db: Session = Depends
return RoleItem(
id=role.id,
role_key=role.role_key,
role_code=role.role_code,
system_key=system.system_key,
system_name=system.name,
name=role.name,
@@ -610,6 +624,7 @@ def list_system_roles(system_key: str, db: Session = Depends(get_db)) -> SystemR
RoleItem(
id=row.id,
role_key=row.role_key,
role_code=row.role_code,
system_key=system.system_key,
system_name=system.name,
name=row.name,
@@ -637,6 +652,7 @@ def list_site_roles(site_key: str, db: Session = Depends(get_db)) -> SiteRolesRe
SiteRoleItem(
id=site_role.id,
role_key=role.role_key,
role_code=role.role_code,
role_name=role.name,
system_key=system.system_key,
system_name=system.name,
@@ -972,6 +988,7 @@ def list_member_effective_roles(user_sub: str, db: Session = Depends(get_db)) ->
system_key=system.system_key,
system_name=system.name,
role_key=role.role_key,
role_code=role.role_code,
role_name=role.name,
)
for site, company, role, system in rows

View File

@@ -41,7 +41,7 @@ def upsert_user_by_sub(
)
def _build_user_role_rows(db: Session, user_sub: str) -> list[tuple[str, str, str, str, str, str, str, str]]:
def _build_user_role_rows(db: Session, user_sub: str) -> list[tuple[str, str, str, str, str, str, str, str, str]]:
users_repo = UsersRepository(db)
user_sites_repo = UserSitesRepository(db)
@@ -59,6 +59,7 @@ def _build_user_role_rows(db: Session, user_sub: str) -> list[tuple[str, str, st
system.system_key,
system.name,
role.role_key,
role.role_code,
role.name,
)
for site, company, role, system in rows
@@ -84,6 +85,7 @@ def get_user_roles(user_sub: str, db: Session = Depends(get_db)) -> InternalUser
system_key=system_key,
system_name=system_name,
role_key=role_key,
role_code=role_code,
role_name=role_name,
)
for (
@@ -94,6 +96,7 @@ def get_user_roles(user_sub: str, db: Session = Depends(get_db)) -> InternalUser
system_key,
system_name,
role_key,
role_code,
role_name,
) in rows
],

View File

@@ -68,6 +68,7 @@ def internal_list_roles(
InternalRoleItem(
id=i.id,
role_key=i.role_key,
role_code=i.role_code,
system_key=system_map[i.system_id].system_key,
system_name=system_map[i.system_id].name,
name=i.name,

View File

@@ -77,6 +77,7 @@ def get_my_permission_snapshot(
system.system_key,
system.name,
role.role_key,
role.role_code,
role.name,
)
for site, company, role, system in rows

View File

@@ -10,10 +10,14 @@ from app.db.base import Base
class Role(Base):
__tablename__ = "roles"
__table_args__ = (UniqueConstraint("system_id", "name", name="uq_roles_system_name"),)
__table_args__ = (
UniqueConstraint("system_id", "name", name="uq_roles_system_name"),
UniqueConstraint("system_id", "role_code", name="uq_roles_system_role_code"),
)
id: Mapped[str] = mapped_column(UUID(as_uuid=False), primary_key=True, default=lambda: str(uuid4()))
role_key: Mapped[str] = mapped_column(String(128), unique=True, nullable=False, index=True)
role_code: Mapped[str] = mapped_column(String(255), nullable=False, index=True)
system_id: Mapped[str] = mapped_column(UUID(as_uuid=False), ForeignKey("systems.id", ondelete="CASCADE"), nullable=False)
name: Mapped[str] = mapped_column(String(255), nullable=False)
description: Mapped[str | None] = mapped_column(String(1024))

View File

@@ -29,6 +29,7 @@ class RolesRepository:
pattern = f"%{keyword}%"
cond = or_(
Role.role_key.ilike(pattern),
Role.role_code.ilike(pattern),
Role.name.ilike(pattern),
Role.description.ilike(pattern),
)
@@ -48,6 +49,7 @@ class RolesRepository:
self,
*,
role_key: str,
role_code: str,
system_id: str,
name: str,
description: str | None,
@@ -55,6 +57,7 @@ class RolesRepository:
) -> Role:
item = Role(
role_key=role_key,
role_code=role_code,
system_id=system_id,
name=name,
description=description,
@@ -70,12 +73,15 @@ class RolesRepository:
item: Role,
*,
system_id: str | None = None,
role_code: str | None = None,
name: str | None = None,
description: str | None = None,
status: str | None = None,
) -> Role:
if system_id is not None:
item.system_id = system_id
if role_code is not None:
item.role_code = role_code
if name is not None:
item.name = name
if description is not None:

View File

@@ -74,6 +74,7 @@ class SystemItem(BaseModel):
class RoleCreateRequest(BaseModel):
system_key: str
role_code: str | None = None
name: str
description: str | None = None
status: str = "active"
@@ -81,6 +82,7 @@ class RoleCreateRequest(BaseModel):
class RoleUpdateRequest(BaseModel):
system_key: str | None = None
role_code: str | None = None
name: str | None = None
description: str | None = None
status: str | None = None
@@ -89,6 +91,7 @@ class RoleUpdateRequest(BaseModel):
class RoleItem(BaseModel):
id: str
role_key: str
role_code: str
system_key: str
system_name: str
name: str
@@ -138,6 +141,7 @@ class SiteRoleAssignRequest(BaseModel):
class SiteRoleItem(BaseModel):
id: str
role_key: str
role_code: str
role_name: str
system_key: str
system_name: str
@@ -163,6 +167,7 @@ class UserEffectiveRoleItem(BaseModel):
system_key: str
system_name: str
role_key: str
role_code: str
role_name: str

View File

@@ -18,6 +18,7 @@ class InternalSystemListResponse(BaseModel):
class InternalRoleItem(BaseModel):
id: str
role_key: str
role_code: str
system_key: str
system_name: str
name: str
@@ -99,6 +100,7 @@ class InternalUserRoleItem(BaseModel):
system_key: str
system_name: str
role_key: str
role_code: str
role_name: str

View File

@@ -9,6 +9,7 @@ class RoleSnapshotItem(BaseModel):
system_key: str
system_name: str
role_key: str
role_code: str
role_name: str

View File

@@ -250,6 +250,7 @@ def sync_from_provider(db: Session, *, force: bool = False) -> dict[str, int]:
role_key = _generate_unique_key("RL", lambda key: roles_repo.get_by_key(key) is not None)
roles_repo.create(
role_key=role_key,
role_code=role_name,
system_id=system.id,
name=role_name,
description=role_desc,
@@ -259,6 +260,7 @@ def sync_from_provider(db: Session, *, force: bool = False) -> dict[str, int]:
else:
roles_repo.update(
role,
role_code=role.role_code or role_name,
name=role_name,
description=role_desc,
status=role_status,
@@ -373,6 +375,7 @@ def sync_systems_from_provider(db: Session, *, force: bool = False) -> dict[str,
role_key = _generate_unique_key("RL", lambda key: roles_repo.get_by_key(key) is not None)
roles_repo.create(
role_key=role_key,
role_code=role_name,
system_id=system.id,
name=role_name,
description=role_desc,
@@ -382,6 +385,7 @@ def sync_systems_from_provider(db: Session, *, force: bool = False) -> dict[str,
else:
roles_repo.update(
role,
role_code=role.role_code or role_name,
name=role_name,
description=role_desc,
status=role_status,

View File

@@ -3,7 +3,7 @@ from app.schemas.permissions import RoleSnapshotItem, RoleSnapshotResponse
class PermissionService:
@staticmethod
def build_role_snapshot(user_sub: str, rows: list[tuple[str, str, str, str, str, str, str, str]]) -> RoleSnapshotResponse:
def build_role_snapshot(user_sub: str, rows: list[tuple[str, str, str, str, str, str, str, str, str]]) -> RoleSnapshotResponse:
return RoleSnapshotResponse(
user_sub=user_sub,
roles=[
@@ -15,6 +15,7 @@ class PermissionService:
system_key=system_key,
system_name=system_name,
role_key=role_key,
role_code=role_code,
role_name=role_name,
)
for (
@@ -25,6 +26,7 @@ class PermissionService:
system_key,
system_name,
role_key,
role_code,
role_name,
) in rows
],

View File

@@ -68,13 +68,15 @@ CREATE TABLE systems (
CREATE TABLE roles (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
role_key TEXT NOT NULL UNIQUE,
role_code TEXT NOT NULL,
system_id UUID NOT NULL REFERENCES systems(id) ON DELETE CASCADE,
name TEXT NOT NULL,
description TEXT,
status VARCHAR(16) NOT NULL DEFAULT 'active',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CONSTRAINT uq_roles_system_name UNIQUE (system_id, name)
CONSTRAINT uq_roles_system_name UNIQUE (system_id, name),
CONSTRAINT uq_roles_system_role_code UNIQUE (system_id, role_code)
);
CREATE TABLE site_roles (
@@ -126,6 +128,7 @@ CREATE INDEX idx_users_username ON users(username);
CREATE INDEX idx_users_email ON users(email);
CREATE INDEX idx_sites_company_id ON sites(company_id);
CREATE INDEX idx_roles_system_id ON roles(system_id);
CREATE INDEX idx_roles_role_code ON roles(role_code);
CREATE INDEX idx_site_roles_site_id ON site_roles(site_id);
CREATE INDEX idx_site_roles_role_id ON site_roles(role_id);
CREATE INDEX idx_user_sites_user_id ON user_sites(user_id);

View File

@@ -0,0 +1,27 @@
BEGIN;
ALTER TABLE roles
ADD COLUMN IF NOT EXISTS role_code TEXT;
UPDATE roles
SET role_code = name
WHERE role_code IS NULL OR btrim(role_code) = '';
ALTER TABLE roles
ALTER COLUMN role_code SET NOT NULL;
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1
FROM pg_constraint
WHERE conname = 'uq_roles_system_role_code'
) THEN
ALTER TABLE roles
ADD CONSTRAINT uq_roles_system_role_code UNIQUE (system_id, role_code);
END IF;
END$$;
CREATE INDEX IF NOT EXISTS idx_roles_role_code ON roles(role_code);
COMMIT;