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:
-
-
+
+
- Open `http://127.0.0.1:8000/admin/` in your browser:
-
-
+
+
- Open `http://127.0.0.1:8000/admin/docs` in your browser:
-
-
-## 许可协议
-
-- `fastapi-amis-admin`基于`Apache2.0`开源免费使用,可以免费用于商业用途,但请在展示界面中明确显示关于FastAPI-Amis-Admin的版权信息.
+
-## 鸣谢
-
-感谢以下开发者对 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'
Amis Admin '
- f'{desc}
'
+ f'
Amis 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))