From f6f41913dca24dec184d1401d9d6760b0335ba9d Mon Sep 17 00:00:00 2001 From: bot_dev1 Date: Wed, 5 Aug 2026 02:06:06 +0000 Subject: [PATCH] =?UTF-8?q?feat(#150):=20PostgreSQL=20users=20=E8=A1=A8=20?= =?UTF-8?q?DDL=20+=20=E6=98=A0=E5=B0=84=EF=BC=88=E5=AF=B9=E9=BD=90=20#30?= =?UTF-8?q?=20schema=EF=BC=8Crole=20CHECK=20=E7=BA=A6=E6=9D=9F=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- core/auth/postgres_users_schema.py | 84 ++++++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 core/auth/postgres_users_schema.py diff --git a/core/auth/postgres_users_schema.py b/core/auth/postgres_users_schema.py new file mode 100644 index 0000000..37c2758 --- /dev/null +++ b/core/auth/postgres_users_schema.py @@ -0,0 +1,84 @@ +# -*- coding: utf-8 -*- +"""④-1 PostgreSQL users 表 DDL + 映射 —— issue #150 / #30 / PRD 8.2。 + +定义本地账号在 PostgreSQL 中的存储结构(#30 users 表 schema),并提供 +`UserStore` 与 PG 之间的映射。DDL 仅用标准库拼装(无 psycopg2 依赖), +实际接库时上层注入连接即可。 + +表结构(对齐 core/data-bus/postgres_schema.py 的命名风格): + + users ( + id BIGSERIAL PRIMARY KEY, + username VARCHAR(64) UNIQUE NOT NULL, + password_hash VARCHAR(255) NOT NULL, -- pbkdf2_sha256$$$ + role VARCHAR(16) NOT NULL DEFAULT 'readonly', -- readonly/engineer/admin + active BOOLEAN NOT NULL DEFAULT TRUE, + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + last_login_at TIMESTAMPTZ + ) + +索引:username 唯一索引(登录走 username);role 普通索引(用户管理筛选)。 +""" +from __future__ import annotations + +from typing import Any, Dict, List + +from .users import User, VALID_ROLES + +USERS_TABLE_DDL = """CREATE TABLE IF NOT EXISTS users ( + id BIGSERIAL PRIMARY KEY, + username VARCHAR(64) UNIQUE NOT NULL, + password_hash VARCHAR(255) NOT NULL, + role VARCHAR(16) NOT NULL DEFAULT 'readonly', + active BOOLEAN NOT NULL DEFAULT TRUE, + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + last_login_at TIMESTAMPTZ, + CONSTRAINT users_role_chk CHECK (role IN ('readonly','engineer','admin')) +); +CREATE INDEX IF NOT EXISTS idx_users_role ON users(role); +""" + + +def row_to_user(row: Any) -> User: + """PG 行(dict/tuple-like)→ User。row 需含 keys: id/username/password_hash/role/active/created_at/last_login_at。""" + def g(k, default=None): + if isinstance(row, dict): + return row.get(k, default) + return getattr(row, k, default) + created = g("created_at") + last = g("last_login_at") + # PG TIMESTAMPTZ → epoch(若为 datetime 有 timestamp()) + def to_epoch(v): + if v is None: + return None + if hasattr(v, "timestamp"): + return v.timestamp() + return v + return User( + id=int(g("id")), + username=str(g("username")), + password_hash=str(g("password_hash")), + role=str(g("role", "readonly")) or "readonly", + active=bool(g("active", True)), + created_at=to_epoch(created) or 0.0, + last_login_at=to_epoch(last), + ) + + +def user_to_row(user: User) -> Dict[str, Any]: + """User → PG 列字典(不含 id,用于 INSERT;UPDATE 时按 id 定位)。""" + return { + "username": user.username, + "password_hash": user.password_hash, + "role": user.role, + "active": user.active, + } + + +def validate_role(role: str) -> str: + if role not in VALID_ROLES: + raise ValueError("role must be one of %r" % (VALID_ROLES,)) + return role + + +__all__ = ["USERS_TABLE_DDL", "row_to_user", "user_to_row", "validate_role"]