diff --git a/README.md b/README.md index a1e85d9..a182cdc 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,11 @@ -# 项目介绍 +# Project Introduction

FastAPI-User-Auth

- FastAPI-User-Auth是一个基于Casbin简单而强大的FastAPI用户认证与授权库.
- 基于FastAPI-Amis-Admin并提供可自由拓展的可视化管理界面. + FastAPI-User-Auth is a simple and powerful FastAPI user authentication and authorization library based on Casbin.
+ It is deeply integrated with FastAPI-Amis-Admin and provides a customizable visual management interface.

@@ -25,35 +25,32 @@

- 源码 + Source Code · - 在线演示 + Online Demo · - 文档 + Documentation · - 文档打不开? + Documentation Not Accessible?

------ -`FastAPI-User-Auth`是一个基于 [FastAPI-Amis-Admin](https://github.com/amisadmin/fastapi_amis_admin) -的应用插件,与`FastAPI-Amis-Admin`深度结合,为其提供用户认证与授权. -基于Casbin的RBAC权限管理,支持多种验证方式,支持多种数据库,支持多种颗粒度的权限控制. - -### 权限类型 - -- **页面权限**: 控制用户是否可以访问某个菜单页面.如不可访问,则菜单不会显示,并且页面下的所有路由都不可访问. -- **动作权限**: 控制用户是否可以执行某个动作,按钮是否显示.如: 新增,更新,删除等. -- **字段权限**: 控制用户是否可以操作某个字段.如:列表展示字段,筛选字段,新增字段,更新字段等. -- **数据权限**: 控制用户可以操作的数据范围.如:只能操作自己创建的数据,只能操作最近7天的数据等. - -## 安装 +`FastAPI-User-Auth` is an application plugin based on [FastAPI-Amis-Admin](https://github.com/amisadmin/fastapi_amis_admin), +deeply integrated with `FastAPI-Amis-Admin`, providing user authentication and authorization. It is based on Casbin for RBAC (Role-Based Access Control) management, supporting various authentication methods, multiple databases, and different levels of permission control. +### Types of Permissions +- **Page Permission** : Controls whether a user can access a specific menu page. If not accessible, the menu will not be displayed, and all routes under the page will also be inaccessible. +- **Action Permission** : Controls whether a user can perform a specific action, such as adding, updating, deleting, etc., and whether the corresponding buttons are visible. +- **Field Permission** : Controls whether a user can operate on a specific field, such as display fields, filter fields, add fields, update fields, etc. +- **Data Permission** : Controls the data range a user can operate on, such as only being able to operate on self-created data or data from the last 7 days. +## Installation ```bash pip install fastapi-user-auth ``` -## 简单示例 + +## Simple Example ```python from fastapi import FastAPI @@ -62,149 +59,132 @@ from fastapi_user_auth.site import AuthAdminSite from starlette.requests import Request from sqlmodel import SQLModel -# 创建FastAPI应用 +# Create a FastAPI application app = FastAPI() -# 创建AdminSite实例 +# Create an AdminSite instance site = AuthAdminSite(settings=Settings(database_url='sqlite:///amisadmin.db?check_same_thread=False')) auth = site.auth -# 挂载后台管理系统 +# Mount the admin system site.mount_app(app) - -# 创建初始化数据库表 +# Create and initialize database tables @app.on_event("startup") async def startup(): await site.db.async_run_sync(SQLModel.metadata.create_all, is_session=False) - # 创建默认管理员,用户名: admin,密码: admin, 请及时修改密码!!! + # Create a default administrator, username: admin, password: admin, please change the password promptly!!! await auth.create_role_user("admin") - # 创建默认超级管理员,用户名: root,密码: root, 请及时修改密码!!! + # Create a default super administrator, username: root, password: root, please change the password promptly!!! await auth.create_role_user("root") - # 运行site的startup方法,加载casbin策略等 + # Run the startup method of the site, load casbin policies, etc. await site.router.startup() - # 添加一条默认的casbin规则 + # Add a default casbin rule if not auth.enforcer.enforce("u:admin", site.unique_id, "page", "page"): await auth.enforcer.add_policy("u:admin", site.unique_id, "page", "page", "allow") - -# 要求: 用户必须登录 +# Requires: User must be logged in @app.get("/auth/get_user") @auth.requires() def get_user(request: Request): return request.user - if __name__ == '__main__': import uvicorn uvicorn.run(app) - ``` -## 验证方式 - -### 装饰器 -- 推荐场景: 单个路由.支持同步/异步路由. +## Authentication Methods +### Decorators +- Recommended for: individual routes. Supports synchronous/asynchronous routes. ```python -# 要求: 用户必须登录 +# Requires: User must be logged in @app.get("/auth/user") @auth.requires() def user(request: Request): - return request.user # 当前请求用户对象. + return request.user # Current request user object. - -# 验证路由: 用户拥有admin角色 +# Route with validation: User has the 'admin' role @app.get("/auth/admin_roles") @auth.requires('admin') def admin_roles(request: Request): return request.user - -# 要求: 用户拥有vip角色 -# 支持同步/异步路由 +# Requires: User has the 'vip' role +# Supports synchronous/asynchronous routes @app.get("/auth/vip_roles") @auth.requires(['vip']) async def vip_roles(request: Request): return request.user - -# 要求: 用户拥有admin角色 或 vip角色 +# Requires: User has the 'admin' role or 'vip' role @app.get("/auth/admin_or_vip_roles") @auth.requires(roles=['admin', 'vip']) def admin_or_vip_roles(request: Request): return request.user - ``` -### 依赖项(推荐) -- 推荐场景: 单个路由,路由集合,FastAPI应用. +### Dependencies (Recommended) +- Recommended for: individual routes, route collections, FastAPI applications. ```python from fastapi import Depends from fastapi_user_auth.auth.models import User - -# 路由参数依赖项, 推荐使用此方式 +# Route parameter dependency, recommended to use this approach @app.get("/auth/admin_roles_depend_1") def admin_roles(user: User = Depends(auth.get_current_user)): return user # or request.user - -# 路径操作装饰器依赖项 +# Path operation decorator dependency @app.get("/auth/admin_roles_depend_2", dependencies=[Depends(auth.requires('admin')())]) def admin_roles(request: Request): return request.user - -# 全局依赖项 -# 在app应用下全部请求都要求拥有admin角色 +# Global dependency: All requests under the app require the 'admin' role app = FastAPI(dependencies=[Depends(auth.requires('admin')())]) - @app.get("/auth/admin_roles_depend_3") def admin_roles(request: Request): return request.user - ``` -### 中间件 -- 推荐场景: FastAPI应用 +### Middleware +- Recommended for: FastAPI applications ```python app = FastAPI() -# 在app应用下每条请求处理之前都附加`request.auth`和`request.user`对象 +# Attach `request.auth` and `request.user` objects to every request handled under the app auth.backend.attach_middleware(app) - ``` -### 直接调用 -- 推荐场景: 非路由方法 +### Direct Invocation +- Recommended for: non-route methods ```python from fastapi_user_auth.auth.models import User - async def get_request_user(request: Request) -> Optional[User]: - # user= await auth.get_current_user(request) + # user = await auth.get_current_user(request) if await auth.requires('admin', response=False)(request): return request.user else: return None - ``` -## Token存储后端 -`fastapi-user-auth` 支持多种token存储方式.默认为: `DbTokenStore`, 建议自定义修改为: `JwtTokenStore` +## Token Storage Backends +`fastapi-user-auth` supports multiple token storage methods. The default is `DbTokenStore`, but it is recommended to customize it to `JwtTokenStore`. ### JwtTokenStore - -- pip install fastapi-user-auth[jwt] +```bash +pip install fastapi-user-auth[jwt] +``` ```python from fastapi_user_auth.auth.backends.jwt import JwtTokenStore @@ -212,28 +192,28 @@ from sqlalchemy_database import Database from fastapi_user_auth.auth import Auth from fastapi_amis_admin.admin.site import AuthAdminSite -# 创建同步数据库引擎 +# Create a synchronous database engine db = Database.create(url="sqlite:///amisadmin.db?check_same_thread=False") -# 使用`JwtTokenStore`创建auth对象 +# Use `JwtTokenStore` to create the auth object auth = Auth( db=db, token_store=JwtTokenStore(secret_key='09d25e094faa6ca2556c818166b7a9563b93f7099f6f0f4caa6cf63b88e8d3e7') ) -# 将auth对象传入AdminSite +# Pass the auth object into AdminSite site = AuthAdminSite( settings=Settings(), db=db, auth=auth ) - ``` + ### DbTokenStore ```python -# 使用`DbTokenStore`创建auth对象 +# Use `DbTokenStore` to create the auth object from fastapi_user_auth.auth.backends.db import DbTokenStore auth = Auth( @@ -242,12 +222,14 @@ auth = Auth( ) ``` -### RedisTokenStore -- pip install fastapi-user-auth[redis] +### RedisTokenStore +```bash +pip install fastapi-user-auth[redis] +``` ```python -# 使用`RedisTokenStore`创建auth对象 +# Use `RedisTokenStore` to create the auth object from fastapi_user_auth.auth.backends.redis import RedisTokenStore from redis.asyncio import Redis @@ -257,26 +239,26 @@ auth = Auth( ) ``` -## RBAC模型 -本系统采用的`Casbin RBAC`模型,并运行基于角色的优先级策略. +## RBAC Model -- 权限可分配给角色,或者直接分配给用户. -- 用户可拥有多个角色. -- 角色可拥有多个子角色. -- 用户拥有的权限策略优先级高于所拥有角色的权限策略. +This system uses the `Casbin RBAC` model and operates based on a role-priority strategy. +- Permissions can be assigned to roles or directly to users. +- A user can have multiple roles. +- A role can have multiple sub-roles. +- The permission policy owned by a user takes precedence over the permission policy of the roles they possess. ```mermaid flowchart LR User -. m:n .-> Role User -. m:n .-> CasbinRule Role -. m:n .-> Role - Role -. m:n .-> CasbinRule + Role -. m:n .-> CasbinRule ``` -## 高级拓展 -### 拓展`User`模型 +## Advanced Extensions +### Extending the `User` Model ```python from datetime import date @@ -284,67 +266,60 @@ from datetime import date from fastapi_amis_admin.models.fields import Field from fastapi_user_auth.auth.models import User -# 自定义`User`模型,继承`User` -class MyUser(User, table = True): - point: float = Field(default = 0, title = '积分', description = '用户积分') - phone: str = Field(None, title = '手机号', max_length = 15) - parent_id: int = Field(None, title = "上级", foreign_key = "auth_user.id") - birthday: date = Field(None, title = "出生日期") - location: str = Field(None, title = "位置") +# Custom `User` model, inherits from `User` +class MyUser(User, table=True): + point: float = Field(default=0, title='Points', description='User points') + phone: str = Field(None, title='Phone Number', max_length=15) + parent_id: int = Field(None, title="Parent", foreign_key="auth_user.id") + birthday: date = Field(None, title="Birthday") + location: str = Field(None, title="Location") -# 使用自定义的`User`模型,创建auth对象 -auth = Auth(db = AsyncDatabase(engine), user_model = MyUser) +# Create the auth object using the custom `User` model +auth = Auth(db=AsyncDatabase(engine), user_model=MyUser) ``` -### 拓展`Role`模型 + +### Extending the `Role` Model ```python from fastapi_amis_admin.models.fields import Field from fastapi_user_auth.auth.models import Role - -# 自定义`Role`模型,继承`Role`; +# Custom `Role` model, inherits from `Role`; class MyRole(Role, table=True): - icon: str = Field(None, title='图标') - is_active: bool = Field(default=True, title="是否激活") - + icon: str = Field(None, title='Icon') + is_active: bool = Field(default=True, title="Is Active") ``` -### 自定义`UserAuthApp`默认管理类 -默认管理类均可通过继承重写替换. -例如: `UserLoginFormAdmin`,`UserRegFormAdmin`,`UserInfoFormAdmin`, -`UserAdmin`,`RoleAdmin` +### Customizing the Default Management Class for `UserAuthApp` + +Default management classes can be overridden by inheritance and reimplementation. For example: `UserLoginFormAdmin`, `UserRegFormAdmin`, `UserInfoFormAdmin`, `UserAdmin`, `RoleAdmin`. ```python -# 自定义模型管理类,继承重写对应的默认管理类 +# Custom model management class, inherit and override the corresponding default management class class MyRoleAdmin(admin.ModelAdmin): - page_schema = PageSchema(label='用户组管理', icon='fa fa-group') + page_schema = PageSchema(label='User Group Management', icon='fa fa-group') model = MyRole readonly_fields = ['key'] - -# 自定义用户认证应用,继承重写默认的用户认证应用 +# Custom user authentication application, inherit and override the default user authentication application class MyUserAuthApp(UserAuthApp): RoleAdmin = MyRoleAdmin - -# 自定义用户管理站点,继承重写默认的用户管理站点 +# Custom user management site, inherit and override the default user management site class MyAuthAdminSite(AuthAdminSite): UserAuthApp = MyUserAuthApp - -# 使用自定义的`AuthAdminSite`类,创建site对象 +# Create the site object using the custom `AuthAdminSite` class site = MyAuthAdminSite(settings, auth=auth) ``` -## ModelAdmin权限控制 - -### 字段权限 -- 继承`AuthFieldModelAdmin`类,即可实现字段权限控制.通过在后台分配用户和角色权限. - -- `perm_fields_exclude`: 指定不需要权限控制的字段. +## ModelAdmin Permission Control +### Field Permissions +- Inherit from `AuthFieldModelAdmin` class to implement field permission control. Assign user and role permissions in the admin interface. +- `perm_fields_exclude`: Specify fields that do not require permission control. ```python from fastapi_user_auth.mixins.admin import AuthFieldModelAdmin @@ -352,69 +327,64 @@ from fastapi_amis_admin.amis import PageSchema from fastapi_amis_admin.admin import FieldPermEnum class AuthFieldArticleAdmin(AuthFieldModelAdmin): - page_schema = PageSchema(label="文章管理") + page_schema = PageSchema(label="Article Management") model = Article - # 指定不需要权限控制的字段. + # Specify fields that do not require permission control perm_fields_exclude = { FieldPermEnum.CREATE: ["title", "description", "content"], } ``` -### 数据权限 -- 继承`AuthSelectModelAdmin`类,即可实现数据权限控制.通过在后台分配用户和角色权限. -- `select_permisions`: 指定查询数据权限. +### Data Permissions +- Inherit from `AuthSelectModelAdmin` class to implement data permission control. Assign user and role permissions in the admin interface. +- `select_permisions`: Specify data query permissions. ```python from fastapi_user_auth.mixins.admin import AuthSelectModelAdmin from fastapi_amis_admin.amis import PageSchema from fastapi_amis_admin.admin import RecentTimeSelectPerm, UserSelectPerm, SimpleSelectPerm - class AuthSelectArticleAdmin(AuthSelectModelAdmin): - page_schema = PageSchema(label="数据集控制文章管理") + page_schema = PageSchema(label="Data-Controlled Article Management") model = Article select_permissions = [ - # 最近7天创建的数据. reverse=True表示反向选择,即默认选择最近7天之内的数据 - RecentTimeSelectPerm(name="recent7_create", label="最近7天创建", td=60 * 60 * 24 * 7, reverse=True), - # 最近30天创建的数据 - RecentTimeSelectPerm(name="recent30_create", label="最近30天创建", td=60 * 60 * 24 * 30), - # 最近3天更新的数据 - RecentTimeSelectPerm(name="recent3_update", label="最近3天更新", td=60 * 60 * 24 * 3, time_column="update_time"), - # 只能选择自己创建的数据, reverse=True表示反向选择,即默认选择自己创建的数据 - UserSelectPerm(name="self_create", label="自己创建", user_column="user_id", reverse=True), - # # 只能选择自己更新的数据 - # UserSelectPerm(name="self_update", label="自己更新", user_column="update_by"), - # 只能选择已发布的数据 - SimpleSelectPerm(name="published", label="已发布", column="is_published", values=[True]), - # 只能选择状态为[1,2,3]的数据 - SimpleSelectPerm(name="status_1_2_3", label="状态为1_2_3", column="status", values=[1, 2, 3]), + # Data created within the last 7 days. reverse=True means select in reverse, i.e., by default select data from the last 7 days + RecentTimeSelectPerm(name="recent7_create", label="Recent 7 Days Created", td=60 * 60 * 24 * 7, reverse=True), + # Data created within the last 30 days + RecentTimeSelectPerm(name="recent30_create", label="Recent 30 Days Created", td=60 * 60 * 24 * 30), + # Data updated within the last 3 days + RecentTimeSelectPerm(name="recent3_update", label="Recent 3 Days Updated", td=60 * 60 * 24 * 3, time_column="update_time"), + # Can only select self-created data, reverse=True means select in reverse, i.e., by default select self-created data + UserSelectPerm(name="self_create", label="Self Created", user_column="user_id", reverse=True), + # Can only select self-updated data + # UserSelectPerm(name="self_update", label="Self Updated", user_column="update_by"), + # Can only select published data + SimpleSelectPerm(name="published", label="Published", column="is_published", values=[True]), + # Can only select data with status [1,2,3] + SimpleSelectPerm(name="status_1_2_3", label="Status 1, 2, 3", column="status", values=[1, 2, 3]), ] ``` -## 界面预览 +## Interface Preview - Open `http://127.0.0.1:8000/admin/auth/form/login` in your browser: -![Login](https://s2.loli.net/2022/03/20/SZy6sjaVlBT8gin.png) - +![Login](https://s2.loli.net/2022/03/20/SZy6sjaVlBT8gin.png) + - Open `http://127.0.0.1:8000/admin/` in your browser: -![ModelAdmin](https://s2.loli.net/2022/03/20/ItgFYGUONm1jCz5.png) - +![ModelAdmin](https://s2.loli.net/2022/03/20/ItgFYGUONm1jCz5.png) + - Open `http://127.0.0.1:8000/admin/docs` in your browser: -![Docs](https://s2.loli.net/2022/03/20/1GcCiPdmXayxrbH.png) - -## 许可协议 - -- `fastapi-amis-admin`基于`Apache2.0`开源免费使用,可以免费用于商业用途,但请在展示界面中明确显示关于FastAPI-Amis-Admin的版权信息. +![Docs](https://s2.loli.net/2022/03/20/1GcCiPdmXayxrbH.png) -## 鸣谢 - -感谢以下开发者对 FastAPI-User-Auth 作出的贡献: +## License +- `fastapi-amis-admin` is open source and free to use under the `Apache2.0` license. It can be used for free for commercial purposes, but please clearly display copyright information about FastAPI-Amis-Admin in the display interface. +## Acknowledgments +Thanks to the developers who contributed to FastAPI-User-Auth: - diff --git a/fastapi_user_auth/admin/actions.py b/fastapi_user_auth/admin/actions.py index 89297a4..6aa3290 100644 --- a/fastapi_user_auth/admin/actions.py +++ b/fastapi_user_auth/admin/actions.py @@ -9,6 +9,7 @@ from fastapi_amis_admin.amis.constants import LevelEnum from fastapi_amis_admin.crud.schema import BaseApiOut from fastapi_amis_admin.models import Field +from fastapi_amis_admin.utils.translation import i18n as _ from pydantic import BaseModel from pydantic.fields import ModelField from starlette.requests import Request @@ -39,7 +40,7 @@ def get_admin_select_permission_rows(admin: PageSchemaAdmin) -> List[Dict[str, A for perm in admin.select_permissions: rows.append( { - "label": "仅限数据-" + perm.label, + "label": _("Restricted to Data") + '-'+ perm.label, "rol": f"{admin.unique_id}#page:select:{perm.name}#page:select", "reverse": perm.reverse, } @@ -69,7 +70,7 @@ def get_admin_field_permission_rows(admin: PageSchemaAdmin, action: str) -> List return [] rows.append( { - "label": "全部", + "label": _("All"), "rol": f"{admin.unique_id}#page:{action}:*#page:{action}", } ) @@ -93,7 +94,7 @@ def __init__(self, admin, **kwargs): elif self.admin.model.__table__.name == User.__tablename__: self._subject = "u" else: - raise Exception("暂不支持的主体模型") + raise Exception(_("Unsupported subject model")) async def get_subject_by_id(self, item_id: str) -> str: # 从数据库获取用户选择的数据列表 @@ -117,7 +118,7 @@ class UpdateSubRolesAction(BaseSubAction): action = ActionType.Dialog( name="update_subject_roles", icon="fa fa-check", - tooltip="设置角色", + tooltip=_("Set Role"), dialog=amis.Dialog(), level=LevelEnum.warning, ) @@ -125,15 +126,15 @@ class UpdateSubRolesAction(BaseSubAction): class schema(BaseModel): role_keys: str = Field( None, - title="角色列表", + title=_("Role List"), amis_form_item=amis.Transfer( selectMode="table", resultListModeFollowSelect=True, columns=[ - # {"name": "key", "label": "角色标识"}, - {"name": "name", "label": "角色名称"}, - {"name": "desc", "label": "角色描述"}, - {"name": "role_names", "label": "子角色"}, + # {"name": "key", "label": _("Role Identifier")}, + {"name": "name", "label": _("Role Name")}, + {"name": "desc", "label": _("Role Description")}, + {"name": "role_names", "label": _("Sub Roles")}, ], source="", valueField="key", @@ -158,7 +159,7 @@ async def get_init_data(self, request: Request, **kwargs) -> BaseApiOut[Any]: return BaseApiOut(data=self.schema()) subject = await self.get_subject_by_id(item_id) if not subject: - return BaseApiOut(status=0, msg="暂不支持的模型") + return BaseApiOut(status=0, msg=_("Unsupported model")) role_keys = await self.site.auth.enforcer.get_roles_for_user(subject) return BaseApiOut(data=self.schema(role_keys=",".join(role_keys).replace("r:", ""))) @@ -166,10 +167,10 @@ async def handle(self, request: Request, item_id: List[str], data: schema, **kwa """更新角色Casbin权限""" subject = await self.get_subject_by_id(item_id[0]) if not subject: - return BaseApiOut(status=0, msg="暂不支持的模型") + return BaseApiOut(status=0, msg=_("Unsupported model")) identity = await self.site.auth.get_current_user_identity(request) or SystemUserEnum.GUEST if subject == "u:" + identity: - return BaseApiOut(status=0, msg="不能修改自己的权限") + return BaseApiOut(status=0, msg=_("Cannot modify your own permissions")) enforcer: AsyncEnforcer = self.site.auth.enforcer role_keys = [f"r:{role}" for role in data.role_keys.split(",") if role] if role_keys and identity not in [SystemUserEnum.ROOT, SystemUserEnum.ADMIN]: @@ -190,7 +191,7 @@ class BaseSubPermAction(BaseSubAction): action = ActionType.Dialog( name="view_subject_permissions", icon="fa fa-check", - tooltip="查看权限", + tooltip=_("View Permissions"), dialog=amis.Dialog(), level=LevelEnum.warning, ) @@ -199,7 +200,7 @@ class BaseSubPermAction(BaseSubAction): class schema(BaseModel): permissions: str = Field( None, - title="权限列表", + title=_("Permission List"), amis_form_item=amis.InputTree( multiple=True, source="", @@ -236,7 +237,7 @@ class ViewSubPagePermAction(BaseSubPermAction): action = ActionType.Dialog( name="view_subject_page_permissions", icon="fa fa-check", - tooltip="查看页面权限", + tooltip=_("View Page Permissions"), dialog=amis.Dialog(actions=[]), level=LevelEnum.warning, ) @@ -254,13 +255,14 @@ async def get_init_data(self, request: Request, **kwargs) -> BaseApiOut[Any]: return BaseApiOut(data=self.schema()) subject = await self.get_subject_by_id(item_id) if not subject: - return BaseApiOut(status=0, msg="暂不支持的模型") - permissions = await get_subject_page_permissions(self.site.auth.enforcer, subject=subject, implicit=self._implicit) + return BaseApiOut(status=0, msg=_("Unsupported model")) + permissions = await get_subject_page_permissions(self.site.auth.enforcer, subject=subject, + implicit=self._implicit) permissions = [perm.replace("#allow", "") for perm in permissions if perm.endswith("#allow")] return BaseApiOut(data=self.schema(permissions=",".join(permissions))) async def handle(self, request: Request, item_id: List[str], data: BaseModel, **kwargs): - return BaseApiOut(status=1, msg="请通过的【设置权限】更新设置!") + return BaseApiOut(status=1, msg=_("Please update settings through 'Set Permissions'!")) class UpdateSubDataPermAction(BaseSubPermAction): @@ -271,8 +273,8 @@ class UpdateSubDataPermAction(BaseSubPermAction): action = ActionType.Dialog( name="update_subject_data_permissions", icon="fa fa-gavel", - tooltip="更新数据权限", - dialog=amis.Dialog(actions=[amis.Action(actionType="submit", label="保存", close=False, primary=True)]), + tooltip=_("Update Data Permissions"), + dialog=amis.Dialog(actions=[amis.Action(actionType="submit", label=_("Save"), close=False, primary=True)]), level=LevelEnum.warning, ) @@ -280,9 +282,9 @@ class UpdateSubDataPermAction(BaseSubPermAction): class schema(BaseSubPermAction.schema): effect_matrix: list = Field( None, - title="当前权限", + title=_("Current Permissions"), amis_form_item=amis.MatrixCheckboxes( - rowLabel="权限名称", + rowLabel=_("Permission Name"), multiple=False, singleSelectMode="row", source="", @@ -291,9 +293,9 @@ class schema(BaseSubPermAction.schema): ) policy_matrix: list = Field( None, - title="权限配置", + title=_("Permission Configuration"), amis_form_item=amis.MatrixCheckboxes( - rowLabel="名称", + rowLabel=_("Name"), multiple=False, singleSelectMode="row", yCheckAll=True, @@ -326,22 +328,23 @@ async def _get_admin_action_options(request: Request, item_id: str): @self.router.get("/get_admin_action_perm_options", response_model=BaseApiOut) async def get_admin_action_perm_options( - request: Request, - permission: str = "", - item_id: str = "", - type: str = "policy", + request: Request, + permission: str = "", + item_id: str = "", + type: str = "policy", ): + from fastapi_amis_admin.utils.translation import i18n as _ # TODO: WFT ? columns = [ { - "label": "默认", + "label": _("Default"), "col": "default", }, { - "label": "是", + "label": _("Yes"), "col": "allow", }, { - "label": "否", + "label": _("No"), "col": "deny", }, ] @@ -392,7 +395,7 @@ async def handle(self, request: Request, item_id: List[str], data: BaseModel, ** subject = await self.get_subject_by_id(item_id[0]) identity = await self.site.auth.get_current_user_identity(request) or SystemUserEnum.GUEST if subject == "u:" + identity: - return BaseApiOut(status=0, msg="不能修改自己的权限") + return BaseApiOut(status=0, msg=_("Cannot modify your own permissions")) msg = await update_subject_data_permissions( self.site.auth.enforcer, subject=subject, @@ -410,7 +413,7 @@ class UpdateSubPagePermsAction(ViewSubPagePermAction): action = ActionType.Dialog( name="update_subject_page_permissions", icon="fa fa-gavel", - tooltip="更新页面权限", + tooltip=_("Update Page Permissions"), dialog=amis.Dialog(), level=LevelEnum.warning, ) @@ -419,10 +422,10 @@ async def handle(self, request: Request, item_id: List[str], data: BaseModel, ** """更新角色Casbin权限""" subject = await self.get_subject_by_id(item_id[0]) if not subject: - return BaseApiOut(status=0, msg="暂不支持的模型") + return BaseApiOut(status=0, msg=_("Unsupported model")) identity = await self.site.auth.get_current_user_identity(request) or SystemUserEnum.GUEST if subject == "u:" + identity: - return BaseApiOut(status=0, msg="不能修改自己的权限") + return BaseApiOut(status=0, msg=_("Cannot modify your own permissions")) # 权限列表 permissions = [perm for perm in data.permissions.split(",") if perm and perm.endswith("#page")] # 分割权限列表,去除空值 enforcer: AsyncEnforcer = self.site.auth.enforcer @@ -439,11 +442,11 @@ class CopyUserAuthLinkAction(ModelAction): action = amis.ActionType.Dialog( name="copy_user_auth_link", icon="fa fa-link", - tooltip="用户免登录链接", + tooltip=_("User Login-Free Link"), level=amis.LevelEnum.danger, dialog=amis.Dialog( size=amis.SizeEnum.md, - title="用户免登录链接", + title=_("User Login-Free Link"), ), ) form_init = True @@ -451,8 +454,8 @@ class CopyUserAuthLinkAction(ModelAction): class schema(UsernameMixin, PkMixin): auth_url: str = Field( - title="授权链接", - description="复制链接到浏览器打开即可免登录", + title=_("Authorization Link"), + description=_("Copy the link and open it in the browser to log in without credentials"), amis_form_item=amis.Static( copyable=True, ), @@ -470,8 +473,9 @@ async def get_init_data(self, request: Request, **kwargs) -> BaseApiOut[Any]: } token = await auth.backend.token_store.write_token(token_data) return BaseApiOut( - msg="操作成功", - data={**token_data, "auth_url": f"{str(request.base_url)[:-1]}{self.site.router_path}/login_by_token?token={token}"}, + msg=_("Operation successful"), + data={**token_data, + "auth_url": f"{str(request.base_url)[:-1]}{self.site.router_path}/login_by_token?token={token}"}, ) def register_router(self): diff --git a/fastapi_user_auth/admin/admin.py b/fastapi_user_auth/admin/admin.py index 5dfc075..8012c74 100644 --- a/fastapi_user_auth/admin/admin.py +++ b/fastapi_user_auth/admin/admin.py @@ -61,13 +61,14 @@ def attach_page_head(page: Page) -> Page: - desc = _("Amis is a low-code front-end framework that reduces page development effort and greatly improves efficiency") + desc = _( + "Amis is a low-code front-end framework that reduces page development effort and greatly improves efficiency") page.body = [ Html( html=f'
' - f'logoAmis Admin
' - f'
{desc}
' + f'logoAmis Admin' + f'
{desc}
' ), Grid(columns=[{"body": [page.body], "lg": 2, "md": 4, "valign": "middle"}], align="center", valign="middle"), ] @@ -149,7 +150,8 @@ class UserRegFormAdmin(FormAdmin): page_schema = None page_route_kwargs = {"name": "reg"} - async def handle(self, request: Request, data: SchemaUpdateT, **kwargs) -> BaseApiOut[BaseModel]: # self.schema_submit_out + async def handle(self, request: Request, data: SchemaUpdateT, **kwargs) -> BaseApiOut[ + BaseModel]: # self.schema_submit_out auth: Auth = request.auth if data.username.upper() in SystemUserEnum.__members__: return BaseApiOut(status=-1, msg=_("Username has been registered!"), data=None) @@ -271,15 +273,15 @@ class UserAdmin(AuthFieldModelAdmin, AuthSelectModelAdmin, SoftDeleteModelAdmin, lambda admin: UpdateSubPagePermsAction( admin=admin, name="update_subject_page_permissions", - tooltip="更新用户页面权限", + tooltip=_("Update User Page Permissions") ), lambda admin: UpdateSubDataPermAction( admin=admin, name="update_subject_data_permissions", - tooltip="更新用户数据权限", + tooltip=_("Update User Data Permissions") ), lambda admin: UpdateSubRolesAction( - admin=admin, name="update_subject_roles", tooltip="更新用户角色", icon="fa fa-user", flags="item" + admin=admin, name="update_subject_roles", tooltip=_("Update User Roles"), icon="fa fa-user", flags="item" ), lambda admin: CopyUserAuthLinkAction(admin), ] @@ -334,15 +336,15 @@ class RoleAdmin(AutoTimeModelAdmin, FootableModelAdmin): lambda admin: UpdateSubPagePermsAction( admin=admin, name="update_subject_page_permissions", - tooltip="更新角色页面权限", + tooltip=_("Update role page permissions"), ), lambda admin: UpdateSubDataPermAction( admin=admin, name="update_subject_data_permissions", - tooltip="更新角色数据权限", + tooltip=_("Update role data permissions"), ), lambda admin: UpdateSubRolesAction( - admin=admin, name="update_subject_roles", tooltip="更新子角色", icon="fa fa-user", flags="item" + admin=admin, name="update_subject_roles", tooltip=_("Update sub roles"), icon="fa fa-user", flags="item" ), ] @@ -364,13 +366,14 @@ class CasbinRuleAdmin(ReadOnlyModelAdmin): unique_id = "Auth>CasbinRuleAdmin" page_schema = PageSchema(label="CasbinRule", icon="fa fa-lock") model = CasbinRule - list_filter = [CasbinRule.ptype, CasbinRule.v0, CasbinRule.v1, CasbinRule.v2, CasbinRule.v3, CasbinRule.v4, CasbinRule.v5] + list_filter = [CasbinRule.ptype, CasbinRule.v0, CasbinRule.v1, CasbinRule.v2, CasbinRule.v3, CasbinRule.v4, + CasbinRule.v5] admin_action_maker = [ lambda admin: AdminAction( admin=admin, action=ActionType.Ajax( id="refresh", - label="刷新权限", + label=_("Refresh Permissions"), icon="fa fa-refresh", level=LevelEnum.success, api=f"GET:{admin.router_path}/load_policy", @@ -396,14 +399,14 @@ def register_router(self): async def _load_policy(): await self.load_policy() get_admin_action_options.cache_clear() # 清除系统菜单缓存 - return BaseApiOut(data="刷新成功") + return BaseApiOut(data=_("Refresh Successful")) return super().register_router() class LoginHistoryAdmin(ReadOnlyModelAdmin): unique_id = "Auth>LoginHistoryAdmin" - page_schema = PageSchema(label="登录历史", icon="fa fa-history") + page_schema = PageSchema(label=_("Login History"), icon="fa fa-history") model = LoginHistory search_fields = [LoginHistory.login_name, LoginHistory.ip, LoginHistory.login_status, LoginHistory.user_agent] list_display = [ diff --git a/fastapi_user_auth/admin/utils.py b/fastapi_user_auth/admin/utils.py index a8631fb..1006dd3 100644 --- a/fastapi_user_auth/admin/utils.py +++ b/fastapi_user_auth/admin/utils.py @@ -5,6 +5,7 @@ from casbin import AsyncEnforcer from fastapi_amis_admin.admin import FormAdmin, ModelAdmin, PageSchemaAdmin from fastapi_amis_admin.admin.admin import AdminGroup, BaseActionAdmin, BaseAdminSite +from fastapi_amis_admin.utils.translation import i18n as _ from fastapi_user_auth.auth.schemas import SystemUserEnum from fastapi_user_auth.utils.casbin import permission_encode, permission_enforce @@ -28,10 +29,10 @@ def get_admin_action_options( if isinstance(admin, BaseActionAdmin): item["children"] = [] if isinstance(admin, ModelAdmin): - item["children"].append({"label": "查看列表", "value": permission_encode(admin.unique_id, "page:list", "page")}) - item["children"].append({"label": "筛选列表", "value": permission_encode(admin.unique_id, "page:filter", "page")}) + item["children"].append({"label": _("View list"), "value": permission_encode(admin.unique_id, "page:list", "page")}) + item["children"].append({"label": _("Filter list"), "value": permission_encode(admin.unique_id, "page:filter", "page")}) elif isinstance(admin, FormAdmin) and "submit" not in admin.registered_admin_actions: - item["children"].append({"label": "提交", "value": permission_encode(admin.unique_id, "page:submit", "page")}) + item["children"].append({"label": _("Submit"), "value": permission_encode(admin.unique_id, "page:submit", "page")}) for admin_action in admin.registered_admin_actions.values(): # todo admin_action 下可能有多个action,需要遍历 item["children"].append( diff --git a/fastapi_user_auth/auth/auth.py b/fastapi_user_auth/auth/auth.py index 44dc166..3687776 100644 --- a/fastapi_user_auth/auth/auth.py +++ b/fastapi_user_auth/auth/auth.py @@ -70,13 +70,13 @@ class Auth(Generic[UserModelT]): backend: AuthBackend[UserModelT] = None def __init__( - self, - db: Union[AsyncDatabase, Database], - *, - token_store: BaseTokenStore = None, - user_model: Type[UserModelT] = User, - pwd_context: CryptContext = CryptContext(schemes=["bcrypt"], deprecated="auto"), - enforcer: AsyncEnforcer = None, + self, + db: Union[AsyncDatabase, Database], + *, + token_store: BaseTokenStore = None, + user_model: Type[UserModelT] = User, + pwd_context: CryptContext = CryptContext(schemes=["bcrypt"], deprecated="auto"), + enforcer: AsyncEnforcer = None, ): self.user_model = user_model or self.user_model assert self.user_model, "user_model is None" @@ -152,15 +152,16 @@ async def get_current_user(self, request: Request) -> Optional[UserModelT]: if "user" in request.scope: # 防止重复授权 return request.scope["user"] token_info = await self._get_token_info(request) - request.scope["user"]: UserModelT = await self.db.async_get(self.user_model, token_info.id) if token_info else None + request.scope["user"]: UserModelT = await self.db.async_get(self.user_model, + token_info.id) if token_info else None return request.scope["user"] def requires( - self, - roles: Union[str, Sequence[str]] = None, - status_code: int = 403, - redirect: str = None, - response: Union[bool, Response] = None, + self, + roles: Union[str, Sequence[str]] = None, + status_code: int = 403, + redirect: str = None, + response: Union[bool, Response] = None, ) -> Callable: # sourcery no-metrics # todo 优化 roles_ = (roles,) if not roles or isinstance(roles, str) else tuple(roles) @@ -173,8 +174,8 @@ async def has_requires(user: UserModelT) -> bool: return await self.has_role_for_user(user.username, roles_) async def depend( - request: Request, - user: UserModelT = Depends(self.get_current_user), + request: Request, + user: UserModelT = Depends(self.get_current_user), ) -> Union[bool, Response]: user_auth = request.scope.get("__user_auth__", None) if user_auth is None: @@ -289,29 +290,31 @@ async def create_role_user(self, role_key: str = "root", commit: bool = True) -> await self.db.async_commit() return user - async def request_login(self, request: Request, response: Response, username: str, password: str) -> BaseApiOut[UserLoginOut]: + async def request_login(self, request: Request, response: Response, username: str, password: str) -> BaseApiOut[ + UserLoginOut]: if request.scope.get("user"): return BaseApiOut(code=1, msg=_("User logged in!"), data=UserLoginOut.parse_obj(request.user)) user = await request.auth.authenticate_user(username=username, password=password) # 保存登录记录 ip = request.client.host # 获取真实ip # 获取代理ip - ips = [request.headers.get(key, "").strip() for key in ["x-forwarded-for", "x-real-ip", "x-client-ip", "remote-host"]] + ips = [request.headers.get(key, "").strip() for key in + ["x-forwarded-for", "x-real-ip", "x-client-ip", "remote-host"]] forwarded_for = ",".join([i for i in set(ips) if i and i != ip]) history = LoginHistory( user_id=user.id if user else None, login_name=username, ip=request.client.host, user_agent=request.headers.get("user-agent"), - login_status="登录成功", + login_status=_("Login Successful"), forwarded_for=forwarded_for, ) self.db.add(history) if not user: - history.login_status = "密码错误" + history.login_status = _("Incorrect Password") return BaseApiOut(status=-1, msg=_("Incorrect username or password!")) if not user.is_active: - history.login_status = "用户未激活" + history.login_status = _("User Not Activated") return BaseApiOut(status=-2, msg=_("Inactive user status!")) request.scope["user"] = user token_info = UserLoginOut.parse_obj(request.user) @@ -354,7 +357,8 @@ def __init__(self, auth: Auth = None): ) # oauth2 if self.route_gettoken: - self.router.dependencies.append(Depends(self.OAuth2(tokenUrl=f"{self.router_path}/gettoken", auto_error=False))) + self.router.dependencies.append( + Depends(self.OAuth2(tokenUrl=f"{self.router_path}/gettoken", auto_error=False))) self.router.add_api_route( "/gettoken", self.route_gettoken, @@ -390,7 +394,8 @@ async def user_logout(request: Request): @property def route_gettoken(self): - async def oauth_token(request: Request, response: Response, username: str = Form(...), password: str = Form(...)): + async def oauth_token(request: Request, response: Response, username: str = Form(...), + password: str = Form(...)): return await self.auth.request_login(request, response, username, password) return oauth_token diff --git a/fastapi_user_auth/auth/exceptions.py b/fastapi_user_auth/auth/exceptions.py index 4e7cf83..b3cc6d9 100644 --- a/fastapi_user_auth/auth/exceptions.py +++ b/fastapi_user_auth/auth/exceptions.py @@ -3,32 +3,35 @@ from fastapi import HTTPException from fastapi_amis_admin.crud import BaseApiOut from fastapi_amis_admin.models import IntegerChoices +from fastapi_amis_admin.utils.translation import i18n as _ class ErrorCode(IntegerChoices): - """常用错误码""" + """Common Error Codes""" - SUCCESS = (0, "成功") - FAIL = (1, "失败") - PARAMS_ERROR = (2, "参数错误") - RETRY = (10, "重试") - RETRY_LATER = (11, "稍后重试") - # 用户相关错误 - USER_NOT_FOUND = (40100, "用户不存在") - USER_PASSWORD_ERROR = (40101, "用户名或者密码错误") - USER_IS_EXIST = (40102, "用户已存在") - USER_NAME_IS_EXIST = (40103, "用户名已存在") - USER_MOBILE_IS_EXIST = (40104, "用户手机号已存在") - USER_EMAIL_IS_EXIST = (40105, "用户邮箱已存在") + SUCCESS = (0, _("Success")) + FAIL = (1, _("Failure")) + PARAMS_ERROR = (2, _("Parameter error")) + RETRY = (10, _("Retry")) + RETRY_LATER = (11, _("Retry later")) - # 用户权限相关 - USER_IS_NOT_LOGIN = (40200, "用户未登录") - USER_IS_NOT_ACTIVE = (40201, "用户未激活") - USER_PERMISSION_DENIED = (40203, "用户权限不足") - USER_IS_NOT_ADMIN = (40204, "用户不是管理员") - # 系统错误 - SYSTEM_ERROR = (50000, "系统错误") - SYSTEM_BUSY = (50001, "系统繁忙") + # User-related errors + USER_NOT_FOUND = (40100, _("User not found")) + USER_PASSWORD_ERROR = (40101, _("Username or password is incorrect")) + USER_IS_EXIST = (40102, _("User already exists")) + USER_NAME_IS_EXIST = (40103, _("Username already exists")) + USER_MOBILE_IS_EXIST = (40104, _("User mobile number already exists")) + USER_EMAIL_IS_EXIST = (40105, _("User email already exists")) + + # User permission related + USER_IS_NOT_LOGIN = (40200, _("User is not logged in")) + USER_IS_NOT_ACTIVE = (40201, _("User is not activated")) + USER_PERMISSION_DENIED = (40203, _("Insufficient user permissions")) + USER_IS_NOT_ADMIN = (40204, _("User is not an administrator")) + + # System errors + SYSTEM_ERROR = (50000, _("System error")) + SYSTEM_BUSY = (50001, _("System busy")) class ApiException(HTTPException): @@ -44,7 +47,7 @@ def __init__( class ApiError(ApiException): - """API异常基类""" + """API exception base class""" def __init__( self, @@ -63,6 +66,6 @@ def __init__( class AuthError(ApiError): - """认证异常""" + """Authentication exception""" pass diff --git a/fastapi_user_auth/auth/models.py b/fastapi_user_auth/auth/models.py index 57a48e6..98c654c 100644 --- a/fastapi_user_auth/auth/models.py +++ b/fastapi_user_auth/auth/models.py @@ -52,9 +52,9 @@ class Role(PkMixin, CUDTimeMixin, table=True): __tablename__ = "auth_role" - key: str = Field(title="角色标识", max_length=40, unique=True, index=True, nullable=False) - name: str = Field(default="", title="角色名称", max_length=40) - desc: str = Field(default="", title="角色描述", max_length=400, amis_form_item="textarea") + key: str = Field(title=_("Role Identifier"), max_length=40, unique=True, index=True, nullable=False) + name: str = Field(default="", title=_("Role Name"), max_length=40) + desc: str = Field(default="", title=_("Role Description"), max_length=400, amis_form_item="textarea") class CasbinRule(PkMixin, table=True): @@ -93,8 +93,8 @@ def __repr__(self) -> str: CasbinSubjectRolesQuery = ( select( CasbinRule.v0.label("subject"), - func.group_concat(Role.name).label("role_names"), - func.group_concat(Role.key).label("role_keys"), + func.array_agg(Role.name).label("role_names"), + func.array_agg(Role.key).label("role_keys"), ) .where(CasbinRule.ptype == "g") .outerjoin(Role, CasbinRule.v1 == "r:" + Role.key) # sqlalchemy#5275 @@ -104,7 +104,7 @@ def __repr__(self) -> str: UserRoleNameLabel = LabelField( CasbinSubjectRolesQuery.c.role_names.label("role_names"), - field=Field("", title="权限角色"), + field=Field("", title=_("Permission Role")), ) @@ -113,12 +113,13 @@ class LoginHistory(PkMixin, CreateTimeMixin, table=True): __tablename__ = "auth_login_history" - user_id: int = Field(None, title="用户ID", sa_column_args=(ForeignKey("auth_user.id", ondelete="CASCADE"),)) - login_name: str = Field("", title="登录名", max_length=20) - ip: str = Field("", title="登录IP", max_length=20) - ip_info: str = Field("", title="IP信息", max_length=255) - client: str = Field("", title="客户端", max_length=20) - user_agent: str = Field("", title="浏览器", max_length=400) - login_type: str = Field("", title="登录类型", max_length=20) - login_status: str = Field("登录成功", title="登录状态", max_length=20, description="登录成功,密码错误,账号被锁定等") - forwarded_for: str = Field("", title="转发IP", max_length=60) + user_id: int = Field(None, title=_("User ID"), sa_column_args=(ForeignKey("auth_user.id", ondelete="CASCADE"),)) + login_name: str = Field("", title=_("Login Name"), max_length=20) + ip: str = Field("", title=_("Login IP"), max_length=20) + ip_info: str = Field("", title=_("IP Information"), max_length=255) + client: str = Field("", title=_("Client"), max_length=20) + user_agent: str = Field("", title=_("Browser"), max_length=400) + login_type: str = Field("", title=_("Login Type"), max_length=20) + login_status: str = Field(_("Login Successful"), title=_("Login Status"), max_length=20, + description=_("Login successful, incorrect password, account locked, etc.")) + forwarded_for: str = Field("", title=_("Forwarded IP"), max_length=60) diff --git a/fastapi_user_auth/utils/sqlachemy_adapter.py b/fastapi_user_auth/utils/sqlachemy_adapter.py index 5675a1b..eb9475d 100644 --- a/fastapi_user_auth/utils/sqlachemy_adapter.py +++ b/fastapi_user_auth/utils/sqlachemy_adapter.py @@ -147,7 +147,7 @@ async def add_policies(self, sec: str, ptype: str, rules: Iterable[Tuple[str]]) """adds a policy rules to the storage.""" values = [] for rule in rules: - values.append(self.parse_rule(ptype, rule).dict()) + values.append(self.parse_rule(ptype, rule).dict(exclude={"id"})) if not values: return await self.db.async_execute(insert(self._db_class).values(values))