diff --git a/src/freeauth/query_api.py b/src/freeauth/query_api.py index 835dadd..0ecf596 100644 --- a/src/freeauth/query_api.py +++ b/src/freeauth/query_api.py @@ -22,6 +22,8 @@ # 'src/freeauth/organizations/queries/query_org_types.edgeql' # 'src/freeauth/roles/queries/query_organization_roles.edgeql' # 'src/freeauth/users/queries/resign_user.edgeql' +# 'src/freeauth/roles/queries/role_bind_users.edgeql' +# 'src/freeauth/roles/queries/role_unbind_users.edgeql' # 'src/freeauth/auth/queries/send_code.edgeql' # 'src/freeauth/auth/queries/sign_in.edgeql' # 'src/freeauth/auth/queries/sign_up.edgeql' @@ -250,6 +252,26 @@ class QueryOrganizationRolesResult(NoPydanticValidation): is_department_role: bool +@dataclasses.dataclass +class RoleBindUsersResult(NoPydanticValidation): + id: uuid.UUID + name: str | None + username: str | None + email: str | None + mobile: str | None + departments: list[RoleBindUsersResultDepartmentsItem] + is_deleted: bool + created_at: datetime.datetime + last_login_at: datetime.datetime | None + + +@dataclasses.dataclass +class RoleBindUsersResultDepartmentsItem(NoPydanticValidation): + id: uuid.UUID + code: str | None + name: str + + @dataclasses.dataclass class SendCodeResult(NoPydanticValidation): id: uuid.UUID @@ -296,20 +318,13 @@ class UpdateUserRolesResult(NoPydanticValidation): username: str | None email: str | None mobile: str | None - departments: list[UpdateUserRolesResultDepartmentsItem] + departments: list[RoleBindUsersResultDepartmentsItem] roles: list[UpdateUserRolesResultRolesItem] is_deleted: bool created_at: datetime.datetime last_login_at: datetime.datetime | None -@dataclasses.dataclass -class UpdateUserRolesResultDepartmentsItem(NoPydanticValidation): - id: uuid.UUID - code: str | None - name: str - - @dataclasses.dataclass class UpdateUserRolesResultRolesItem(NoPydanticValidation): id: uuid.UUID @@ -1080,6 +1095,80 @@ async def resign_user( ) +async def role_bind_users( + executor: edgedb.AsyncIOExecutor, + *, + user_ids: list[uuid.UUID], + role_ids: list[uuid.UUID], +) -> list[RoleBindUsersResult]: + return await executor.query( + """\ + WITH + user_ids := >$user_ids, + role_ids := >$role_ids + SELECT ( + UPDATE User FILTER .id in array_unpack(user_ids) + SET { + roles += ( + SELECT Role + FILTER .id IN array_unpack(role_ids) + ) + } + ) { + name, + username, + email, + mobile, + departments := ( + SELECT .directly_organizations { id, code, name } + ), + is_deleted, + created_at, + last_login_at + };\ + """, + user_ids=user_ids, + role_ids=role_ids, + ) + + +async def role_unbind_users( + executor: edgedb.AsyncIOExecutor, + *, + user_ids: list[uuid.UUID], + role_ids: list[uuid.UUID], +) -> list[RoleBindUsersResult]: + return await executor.query( + """\ + WITH + user_ids := >$user_ids, + role_ids := >$role_ids + SELECT ( + UPDATE User FILTER .id in array_unpack(user_ids) + SET { + roles -= ( + SELECT Role + FILTER .id IN array_unpack(role_ids) + ) + } + ) { + name, + username, + email, + mobile, + departments := ( + SELECT .directly_organizations { id, code, name } + ), + is_deleted, + created_at, + last_login_at + };\ + """, + user_ids=user_ids, + role_ids=role_ids, + ) + + async def send_code( executor: edgedb.AsyncIOExecutor, *, diff --git a/src/freeauth/roles/dataclasses.py b/src/freeauth/roles/dataclasses.py index 4c35e4e..b98667b 100644 --- a/src/freeauth/roles/dataclasses.py +++ b/src/freeauth/roles/dataclasses.py @@ -97,3 +97,19 @@ class OrganizationRoleQueryBody: "支持按角色状态进行过滤,true 代表已禁用的角色,false 代表已启用的角色" ), ) + + +@dataclass(config=BaseModelConfig) +class RoleUserBody: + role_ids: list[uuid.UUID] = Field( + ..., + title="角色 ID 列表", + description="可设置一个或多个角色D", + min_items=1, + ) + user_ids: list[uuid.UUID] = Field( + ..., + title="用户 ID 列表", + description="待添加的用户 ID 列表", + min_items=1, + ) diff --git a/src/freeauth/roles/endpoints.py b/src/freeauth/roles/endpoints.py index 5622d0b..58266c7 100644 --- a/src/freeauth/roles/endpoints.py +++ b/src/freeauth/roles/endpoints.py @@ -13,11 +13,14 @@ CreateRoleResult, DeleteRoleResult, QueryOrganizationRolesResult, + RoleBindUsersResult, UpdateRoleStatusResult, create_role, delete_role, get_role_by_id_or_code, query_organization_roles, + role_bind_users, + role_unbind_users, update_role, update_role_status, ) @@ -27,6 +30,7 @@ RolePostBody, RolePutBody, RoleStatusBody, + RoleUserBody, ) from .dependencies import parse_role_id_or_code, validate_organization_ids @@ -227,3 +231,102 @@ async def get_organization_roles( role_type=body.role_type, is_deleted=body.is_deleted, ) + + +@router.post( + "/roles/{role_id}/users", + tags=["角色管理"], + summary="获取角色绑定用户列表", + description="获取指定角色下绑定的用户,分页获取,支持关键字搜索、排序", +) +async def get_members_in_organization( + body: QueryBody, + role_id: uuid.UUID, + client: edgedb.AsyncIOClient = Depends(get_edgedb_client), +) -> PaginatedData: + result = await client.query_single_json( + f"""\ + WITH + page := $page ?? 1, + per_page := $per_page ?? 20, + q := $q, + role := ( + SELECT Role FILTER .id = $role_id + ), + users := ( + SELECT ( + role.users + ) FILTER ( + true IF not EXISTS q ELSE + .name ?? '' ILIKE q OR + .username ?? '' ILIKE q OR + .mobile ?? '' ILIKE q OR + .email ?? '' ILIKE q + ) + ), + total := count(users) + SELECT ( + total := total, + per_page := per_page, + page := page, + last := math::ceil(total / per_page), + rows := array_agg(( + SELECT users {{ + id, + name, + username, + email, + mobile, + departments := ( + SELECT .directly_organizations {{ + id, + code, + name + }} + ), + is_deleted, + created_at, + last_login_at + }} + ORDER BY {body.ordering_expr} + OFFSET (page - 1) * per_page + LIMIT per_page + )) + );\ + """, + q=f"%{body.q}%" if body.q else None, + page=body.page, + per_page=body.per_page, + role_id=role_id, + ) + return PaginatedData.parse_raw(result) + + +@router.post( + "/roles/bind_users", + tags=["角色管理"], + summary="添加用户", + description="关联一个或多个用户到角色", +) +async def bind_users_to_roles( + body: RoleUserBody, + client: edgedb.AsyncIOClient = Depends(get_edgedb_client), +) -> list[RoleBindUsersResult]: + return await role_bind_users( + client, user_ids=body.user_ids, role_ids=body.role_ids + ) + + +@router.post( + "/roles/unbind_users", + tags=["角色管理"], + summary="添加用户", + description="关联一个或多个用户到角色", +) +async def unbind_users_to_roles( + body: RoleUserBody, + client: edgedb.AsyncIOClient = Depends(get_edgedb_client), +) -> list[RoleBindUsersResult]: + return await role_unbind_users( + client, user_ids=body.user_ids, role_ids=body.role_ids + ) diff --git a/src/freeauth/roles/queries/role_bind_users.edgeql b/src/freeauth/roles/queries/role_bind_users.edgeql new file mode 100644 index 0000000..224b38a --- /dev/null +++ b/src/freeauth/roles/queries/role_bind_users.edgeql @@ -0,0 +1,23 @@ +WITH + user_ids := >$user_ids, + role_ids := >$role_ids +SELECT ( + UPDATE User FILTER .id in array_unpack(user_ids) + SET { + roles += ( + SELECT Role + FILTER .id IN array_unpack(role_ids) + ) + } +) { + name, + username, + email, + mobile, + departments := ( + SELECT .directly_organizations { id, code, name } + ), + is_deleted, + created_at, + last_login_at +}; diff --git a/src/freeauth/roles/queries/role_unbind_users.edgeql b/src/freeauth/roles/queries/role_unbind_users.edgeql new file mode 100644 index 0000000..a65a51a --- /dev/null +++ b/src/freeauth/roles/queries/role_unbind_users.edgeql @@ -0,0 +1,23 @@ +WITH + user_ids := >$user_ids, + role_ids := >$role_ids +SELECT ( + UPDATE User FILTER .id in array_unpack(user_ids) + SET { + roles -= ( + SELECT Role + FILTER .id IN array_unpack(role_ids) + ) + } +) { + name, + username, + email, + mobile, + departments := ( + SELECT .directly_organizations { id, code, name } + ), + is_deleted, + created_at, + last_login_at +};