@@ -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$<iter>$<salt>$<hash>
|
||||
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"]
|
||||
Reference in New Issue
Block a user