modify: auth 插件

This commit is contained in:
2026-07-23 17:45:44 +08:00
parent 95ef842d92
commit 2b7b8ee08c
21 changed files with 753 additions and 43 deletions
+52
View File
@@ -0,0 +1,52 @@
from pathlib import Path
from datetime import datetime
from chongming_worker import auto_bind_models
from chongming_worker.runtime import create_workers as _create_workers
from chongming_password import hash_password
from .router import *
auto_bind_models("auth")
async def create_workers(session, config, count=1, worker_manager=None):
from chongming_database import Database
from .global_var import set_global_var
from .models.auth import Users
from sqlalchemy import select
db_path = Path(config.get("data_dir", "./data")) / "auth.db"
db = Database(db_path)
await db.initialize()
# 初始化管理员
async with db.get_session() as sess:
select_stmt = select(Users) \
.where(Users.username == "admin") # type: ignore
result = await sess.execute(select_stmt)
user: Users | None = result.scalar_one_or_none()
if user is None:
user = Users(
username="admin",
hashed_password=hash_password("admin"),
email="admin@example.com",
created_at=datetime.now(),
updated_at=datetime.now(),
is_superuser=True
)
sess.add(user)
await sess.commit()
set_global_var("db", db)
return _create_workers(session, config, count, worker_manager)
async def cleanup(worker_item: str):
from .global_var import db
if db is not None:
await db.dispose()
print(f"{worker_item} 清理完毕")
+29
View File
@@ -0,0 +1,29 @@
from typing import Any
from chongming_config import get_config
from chongming_jwt import JWT
from chongming_database import Database
from .models import *
def set_global_var(name: str, value: Any) -> None:
"""
设置当前模块的全局变量。
Args:
name: 变量名(字符串)
value: 任意值
"""
globals()[name] = value
config = get_config()
db: Database | None = None
secret = config["security"]["secret_key"]
algorithm = config["security"]["algorithm"]
jwt = JWT(secret, algorithm)
__all__ = [
"db",
"jwt"
]
+33
View File
@@ -0,0 +1,33 @@
"""
模型导出 — auth
自动生成,请勿手动修改
"""
from .pydantic import AuthResponse
from .pydantic import ChangePasswordPostRequest
from .pydantic import ChangePasswordPostResponse
from .pydantic import LoginPostRequest
from .pydantic import LoginPostResponse
from .pydantic import MeGetRequest
from .pydantic import MeGetResponse
from .pydantic import RegisterPostRequest
from .pydantic import RegisterPostResponse
from .pydantic import UpdateProfilePostRequest
from .pydantic import UpdateProfilePostResponse
from .auth import Users
__all__ = [
"AuthResponse",
"ChangePasswordPostRequest",
"ChangePasswordPostResponse",
"LoginPostRequest",
"LoginPostResponse",
"MeGetRequest",
"MeGetResponse",
"RegisterPostRequest",
"RegisterPostResponse",
"UpdateProfilePostRequest",
"UpdateProfilePostResponse",
"Users",
]
+102
View File
@@ -0,0 +1,102 @@
import os
import time
from typing import Optional
from datetime import datetime
from uuid import uuid4
from sqlmodel import Field, SQLModel, Column
from sqlalchemy import func, DateTime
def primary_key_create():
return f"{uuid4()}_{os.getpid()}_{int(time.time())}"
class Users(SQLModel, table=True):
__tablename__ = "users" # type: ignore
# ---------- 主键 ----------
id: str = Field(
default_factory=primary_key_create,
primary_key=True,
index=True,
description="用户唯一ID(UUID格式)"
)
# ---------- 登录凭证(唯一索引) ----------
username: str = Field(
max_length=50,
unique=True,
index=True,
nullable=False,
description="登录用户名"
)
email: str = Field(
max_length=255,
unique=True,
index=True,
nullable=False,
description="电子邮箱"
)
hashed_password: str = Field(
max_length=255,
nullable=False,
description="加密后的密码(不存明文)"
)
# ---------- 个人信息(可选) ----------
full_name: Optional[str] = Field(
default=None,
max_length=100,
description="用户全名"
)
avatar_url: Optional[str] = Field(
default=None,
max_length=500,
description="头像图片链接"
)
phone: Optional[str] = Field(
default=None,
max_length=20,
description="手机号码"
)
# ---------- 安全与状态标识 ----------
is_active: bool = Field(
default=True,
description="账户是否启用(用于软禁用)"
)
is_verified: bool = Field(
default=False,
description="邮箱/手机是否已验证"
)
is_superuser: bool = Field(
default=False,
description="是否为超级管理员"
)
# ---------- 审计时间戳(数据库自动维护) ----------
created_at: datetime = Field(
sa_column=Column(
DateTime(timezone=True),
server_default=func.now(),
nullable=False,
comment="创建时间"
)
)
updated_at: datetime = Field(
sa_column=Column(
DateTime(timezone=True),
server_default=func.now(),
onupdate=func.now(),
nullable=False,
comment="最后更新时间"
)
)
# ---------- 最后登录记录(可选) ----------
last_login_at: Optional[datetime] = Field(
default=None,
sa_column=Column(DateTime(timezone=True), nullable=True),
description="最后一次登录时间"
)
+86
View File
@@ -0,0 +1,86 @@
"""
自动生成的 Pydantic 模型 — auth
来源: config/workers/auth/
"""
from __future__ import annotations
from typing import Any, Optional
from pydantic import BaseModel, Field
class ChangePasswordPostRequest(BaseModel):
"""修改密码 — 请求体"""
user_id: str = Field(...)
old_password: str = Field(...)
new_password: str = Field(...)
class ChangePasswordPostResponse(BaseModel):
"""修改密码 — 响应体"""
result: str = Field(...)
message: str = Field(...)
timestamp: Optional[float] = Field(default=None)
class LoginPostRequest(BaseModel):
"""登录 — 请求体"""
username: str = Field(...)
password: str = Field(...)
class LoginPostResponse(BaseModel):
"""登录 — 响应体"""
result: str = Field(...)
token: str = Field(...)
message: str = Field(...)
timestamp: Optional[float] = Field(default=None)
class MeGetRequest(BaseModel):
"""获取当前用户信息 — 请求体"""
user_id: str = Field(...)
class MeGetResponse(BaseModel):
"""获取当前用户信息 — 响应体"""
result: str = Field(...)
id: str = Field(...)
username: str = Field(...)
email: str = Field(...)
full_name: Optional[str] = Field(default=None)
avatar_url: Optional[str] = Field(default=None)
phone: Optional[str] = Field(default=None)
is_active: bool = Field(...)
is_verified: bool = Field(...)
is_superuser: bool = Field(...)
created_at: Optional[str] = Field(default=None)
updated_at: Optional[str] = Field(default=None)
last_login_at: Optional[str] = Field(default=None)
class RegisterPostRequest(BaseModel):
"""用户注册 — 请求体"""
username: str = Field(...)
password: str = Field(...)
email: str = Field(...)
full_name: str = Field(...)
phone: str = Field(...)
class RegisterPostResponse(BaseModel):
"""用户注册 — 响应体"""
result: str = Field(...)
message: str = Field(...)
user_id: Optional[str] = Field(default=None)
timestamp: Optional[float] = Field(default=None)
class UpdateProfilePostRequest(BaseModel):
"""更新用户资料 — 请求体"""
user_id: str = Field(...)
full_name: str = Field(...)
email: str = Field(...)
avatar_url: str = Field(...)
phone: str = Field(...)
class UpdateProfilePostResponse(BaseModel):
"""更新用户资料 — 响应体"""
result: str = Field(...)
message: str = Field(...)
timestamp: Optional[float] = Field(default=None)
# 所有响应模型的联合类型
AuthResponse = LoginPostResponse | ChangePasswordPostResponse | MeGetResponse | UpdateProfilePostResponse | RegisterPostResponse
+8
View File
@@ -0,0 +1,8 @@
from .login import login
from .register import register
from .me import me
from .update_profile import update_profile
from .change_password import change_password
__all__ = ["login", "register", "me", "update_profile", "change_password"]
+81
View File
@@ -0,0 +1,81 @@
import time
from datetime import datetime
from sqlalchemy import select
from chongming_worker import rpc_method
from chongming_password import hash_password, verify_password
from ..models.pydantic import (
ChangePasswordPostRequest,
ChangePasswordPostResponse
)
from ..models.auth import Users
@rpc_method(
subject="auth.change_password",
method="POST",
summary="修改密码",
response_model=ChangePasswordPostResponse,
)
async def change_password(change_password_post_request: ChangePasswordPostRequest) -> ChangePasswordPostResponse:
from ..global_var import db
if db is None:
raise RuntimeError("Database not initialized")
user_id = change_password_post_request.user_id
old_password = change_password_post_request.old_password
new_password = change_password_post_request.new_password
if not old_password:
return ChangePasswordPostResponse(
result="fail",
message="旧密码不能为空",
timestamp=time.time()
)
if not new_password:
return ChangePasswordPostResponse(
result="fail",
message="新密码不能为空",
timestamp=time.time()
)
if old_password == new_password:
return ChangePasswordPostResponse(
result="fail",
message="新密码不能与旧密码相同",
timestamp=time.time()
)
async with db.get_session() as session:
stmt = select(Users).filter_by(id=user_id)
result = await session.execute(stmt)
user: Users | None = result.scalar_one_or_none()
if user is None:
return ChangePasswordPostResponse(
result="fail",
message="用户不存在",
timestamp=time.time()
)
# 验证旧密码
if not verify_password(old_password, user.hashed_password):
return ChangePasswordPostResponse(
result="fail",
message="旧密码错误",
timestamp=time.time()
)
# 更新密码
user.hashed_password = hash_password(new_password)
user.updated_at = datetime.now()
session.add(user)
await session.commit()
return ChangePasswordPostResponse(
result="success",
message="密码修改成功",
timestamp=time.time()
)
+85
View File
@@ -0,0 +1,85 @@
import time
from datetime import datetime
import json
from sqlalchemy import select
from chongming_worker import rpc_method
from chongming_password import verify_password
from ..models.pydantic import (
LoginPostRequest,
LoginPostResponse
)
from ..models.auth import Users
@rpc_method(
subject="auth.login",
method="POST",
summary="登录",
response_model=LoginPostResponse,
)
async def login(login_post_request: LoginPostRequest) -> LoginPostResponse:
from ..global_var import jwt, db
if db is None:
raise RuntimeError("Database not initialized")
async with db.get_session() as session:
statement = select(Users).filter_by(
username=login_post_request.username,
)
result = await session.execute(statement)
user: Users | None = result.scalar_one_or_none()
if user is None:
return LoginPostResponse(
result="fail",
message="用户名错误",
token="",
timestamp=time.time()
)
if not verify_password(login_post_request.password, user.hashed_password):
return LoginPostResponse(
result="fail",
message="密码错误",
token="",
timestamp=time.time()
)
if not user.is_active:
return LoginPostResponse(
result="fail",
message="用户已经注销或被禁用",
token="",
timestamp=time.time()
)
user.last_login_at = datetime.now()
payload = {
"sub": user.id,
"username": user.username,
"email": user.email,
"full_name": user.full_name,
"avatar_url": user.avatar_url,
"phone": user.phone,
"is_verified": user.is_verified,
"is_superuser": user.is_superuser,
"created_at": user.created_at.isoformat(),
"updated_at": user.updated_at.isoformat(),
"last_login_at": user.last_login_at.isoformat()
}
token = jwt.encode(
json.dumps(payload),
expires_in_seconds=10800 # 3小时有效期
)
return LoginPostResponse(
result="success",
message="登录成功",
token=token,
timestamp=time.time()
)
+56
View File
@@ -0,0 +1,56 @@
import time
from sqlalchemy import select
from chongming_worker import rpc_method
from ..models.pydantic import (
MeGetRequest,
MeGetResponse
)
from ..models.auth import Users
@rpc_method(
subject="auth.me",
method="GET",
summary="获取当前用户信息",
response_model=MeGetResponse,
)
async def me(me_get_request: MeGetRequest) -> MeGetResponse:
from ..global_var import db
if db is None:
raise RuntimeError("Database not initialized")
async with db.get_session() as session:
stmt = select(Users).filter_by(id=me_get_request.user_id)
result = await session.execute(stmt)
user: Users | None = result.scalar_one_or_none()
if user is None:
return MeGetResponse(
result="fail",
id="",
username="",
email="",
is_active=False,
is_verified=False,
is_superuser=False,
)
return MeGetResponse(
result="success",
id=user.id,
username=user.username,
email=user.email,
full_name=user.full_name,
avatar_url=user.avatar_url,
phone=user.phone,
is_active=user.is_active,
is_verified=user.is_verified,
is_superuser=user.is_superuser,
created_at=user.created_at.isoformat() if user.created_at else None,
updated_at=user.updated_at.isoformat() if user.updated_at else None,
last_login_at=user.last_login_at.isoformat() if user.last_login_at else None,
)
+93
View File
@@ -0,0 +1,93 @@
import time
from datetime import datetime
from sqlalchemy import select
from chongming_worker import rpc_method
from chongming_password import hash_password
from ..models.pydantic import (
RegisterPostRequest,
RegisterPostResponse
)
from ..models.auth import Users
@rpc_method(
subject="auth.register",
method="POST",
summary="用户注册",
response_model=RegisterPostResponse,
)
async def register(register_post_request: RegisterPostRequest) -> RegisterPostResponse:
from ..global_var import db
if db is None:
raise RuntimeError("Database not initialized")
username = register_post_request.username.strip()
email = register_post_request.email.strip()
full_name = register_post_request.full_name.strip() or None
phone = register_post_request.phone.strip() or None
# 校验必填字段
if not username:
return RegisterPostResponse(
result="fail",
message="用户名不能为空",
timestamp=time.time()
)
if not email:
return RegisterPostResponse(
result="fail",
message="邮箱不能为空",
timestamp=time.time()
)
if not register_post_request.password:
return RegisterPostResponse(
result="fail",
message="密码不能为空",
timestamp=time.time()
)
async with db.get_session() as session:
# 检查用户名是否已存在
stmt = select(Users).filter_by(username=username)
result = await session.execute(stmt)
if result.scalar_one_or_none() is not None:
return RegisterPostResponse(
result="fail",
message="用户名已存在",
timestamp=time.time()
)
# 检查邮箱是否已存在
stmt = select(Users).filter_by(email=email)
result = await session.execute(stmt)
if result.scalar_one_or_none() is not None:
return RegisterPostResponse(
result="fail",
message="邮箱已被注册",
timestamp=time.time()
)
# 创建新用户
user = Users(
username=username,
hashed_password=hash_password(register_post_request.password),
email=email,
full_name=full_name,
phone=phone,
created_at=datetime.now(),
updated_at=datetime.now(),
)
session.add(user)
await session.commit()
await session.refresh(user)
return RegisterPostResponse(
result="success",
message="注册成功",
user_id=user.id,
timestamp=time.time()
)
+74
View File
@@ -0,0 +1,74 @@
import time
from datetime import datetime
from sqlalchemy import select
from chongming_worker import rpc_method
from ..models.pydantic import (
UpdateProfilePostRequest,
UpdateProfilePostResponse
)
from ..models.auth import Users
@rpc_method(
subject="auth.update_profile",
method="POST",
summary="更新用户资料",
response_model=UpdateProfilePostResponse,
)
async def update_profile(update_profile_post_request: UpdateProfilePostRequest) -> UpdateProfilePostResponse:
from ..global_var import db
if db is None:
raise RuntimeError("Database not initialized")
user_id = update_profile_post_request.user_id
async with db.get_session() as session:
stmt = select(Users).filter_by(id=user_id)
result = await session.execute(stmt)
user: Users | None = result.scalar_one_or_none()
if user is None:
return UpdateProfilePostResponse(
result="fail",
message="用户不存在",
timestamp=time.time()
)
# 更新字段(只更新非空值)
full_name = update_profile_post_request.full_name.strip()
email = update_profile_post_request.email.strip()
avatar_url = update_profile_post_request.avatar_url.strip()
phone = update_profile_post_request.phone.strip()
if full_name:
user.full_name = full_name
if email:
# 检查邮箱是否被其他用户占用
if email != user.email:
check_stmt = select(Users).filter_by(email=email)
check_result = await session.execute(check_stmt)
if check_result.scalar_one_or_none() is not None:
return UpdateProfilePostResponse(
result="fail",
message="邮箱已被其他用户使用",
timestamp=time.time()
)
user.email = email
if avatar_url:
user.avatar_url = avatar_url
if phone:
user.phone = phone
user.updated_at = datetime.now()
session.add(user)
await session.commit()
return UpdateProfilePostResponse(
result="success",
message="资料更新成功",
timestamp=time.time()
)
+10 -6
View File
@@ -15,7 +15,6 @@ Example Worker 插件 — 演示 @rpc_method + 显式模型绑定用法
- 装饰器 subject 中的操作名 → 模型前缀(驼峰)
example.add → Add, example.subtract → Subtract
"""
import modelvault
from pathlib import Path
from chongming_worker import auto_bind_models
@@ -37,14 +36,19 @@ auto_bind_models("example")
async def create_workers(session, config, count=1, worker_manager=None):
"""插件入口 — 委托给 chongming_worker 运行时创建 Worker 协程"""
from .global_var import set_global_var
from chongming_database import Database
db_path = Path(config.get("data_dir", "./data")) / "example.modelvault"
db_path.parent.mkdir(parents=True, exist_ok=True)
db = await modelvault.AsyncDatabase.open(str(db_path)) # type: ignore
db_path = Path(config.get("data_dir", "./data")) / "example.db"
db = Database(db_path)
await db.initialize()
set_global_var("db", db)
return _create_workers(session, config, count, worker_manager)
async def cleanup(worker_item: str):
print(f"{worker_item}关闭")
pass
from .global_var import db
if db is not None:
await db.dispose()
print(f"{worker_item} 清理完毕")
+6 -8
View File
@@ -1,9 +1,10 @@
import sys
from typing import Any, Callable, Awaitable, List
import modelvault
from typing import Any
from .models import *
from chongming_database import Database
db: Database | None = None
# 初始定义(可以保留 db 作为示例)
db: modelvault.AsyncDatabase | None = None # type: ignore
def set_global_var(name: str, value: Any) -> None:
"""
@@ -16,9 +17,6 @@ def set_global_var(name: str, value: Any) -> None:
globals()[name] = value
# 但这样会污染导出列表,不推荐自动添加。
__all__ = [
"db",
"set_global_var",
]
+3
View File
@@ -16,6 +16,8 @@ from .pydantic import MultiplyGetResponse
from .pydantic import SubtractGetRequest
from .pydantic import SubtractGetResponse
from .counter import Counter
__all__ = [
"AddGetRequest",
"AddGetResponse",
@@ -29,4 +31,5 @@ __all__ = [
"MultiplyGetResponse",
"SubtractGetRequest",
"SubtractGetResponse",
"Counter"
]
+6 -12
View File
@@ -1,14 +1,8 @@
# src/plugins/example/models/counter.py
from pydantic import BaseModel
import modelvault
from sqlmodel import Field, SQLModel
class Counter(BaseModel):
# 主键字段
__modelvault_primary_key__ = "name"
# 唯一索引
__modelvault_indexes__ = [
modelvault.models.unique("name"),
]
name: str # 计数器名称(例如 "visits")
value: int = 0
class Counter(SQLModel, table=True):
__tablename__ = "counter" # type: ignore
name: str = Field(primary_key=True, index=True)
value: int = Field(default=0)
+1
View File
@@ -4,6 +4,7 @@ from .multiply import multiply
from .divide import divide
from .count import count_get
__all__ = [
"add",
"add_post",
+3 -1
View File
@@ -32,8 +32,10 @@ async def add(add_get_request:AddGetRequest) -> AddGetResponse:
summary="Add two numbers via POST",
response_model=AddPostResponse,
)
async def add_post(add_post_request: AddPostRequest, session: zenoh.Session) -> AddPostResponse:
async def add_post(add_post_request: AddPostRequest, session: zenoh.Session, user_info: dict | None = None) -> AddPostResponse:
"""加法运算(POST)— 通过 JSON Body 传入 a、b,返回它们的和"""
print("----------user_info-----------")
print(user_info)
async with Client(session=session) as client:
add_result = await client.call(
worker_type="example",
+19 -10
View File
@@ -1,4 +1,4 @@
import modelvault
from sqlalchemy import select, update
from chongming_worker import rpc_method
@@ -15,19 +15,28 @@ from ..models.counter import Counter
async def count_get() -> CountGetResponse:
from ..global_var import db
# 确保数据库已初始化(如果未初始化,则同步初始化,但这里假设已就绪)
if db is None:
raise RuntimeError("Database not initialized")
async with db.get_session() as session:
statement = select(Counter).where(Counter.name == "visits") # type: ignore
result = await session.execute(statement)
counter = result.scalar_one_or_none()
counters = modelvault.models.async_collection(db, Counter) # type: ignore
async with db.transaction():
counter = await counters.get("visits")
if counter is None:
counter = Counter(name="visits", value=0)
await counters.insert(counter)
counter = Counter(name="visits", value=1)
session.add(counter)
await session.commit()
return CountGetResponse(name="visits", value=1)
else:
new_value = counter.value + 1
await counters.update("visits", {"value": new_value})
counter.value = new_value # 更新本地对象以便返回
return CountGetResponse(name=counter.name, value=counter.value) # type: ignore
update_stmt = (
update(Counter)
.where(Counter.name == "visits") # type: ignore
.values(value=new_value)
)
await session.execute(update_stmt)
await session.commit()
return CountGetResponse(name="visits", value=new_value)