Skip to content

Commit

Permalink
feat: 支持超级管理员新增用户
Browse files Browse the repository at this point in the history
  • Loading branch information
zgqgit committed Jul 3, 2024
1 parent 4d5c889 commit e8707e1
Show file tree
Hide file tree
Showing 7 changed files with 138 additions and 37 deletions.
6 changes: 6 additions & 0 deletions src/backend/bisheng/api/errcode/base.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from fastapi.exceptions import HTTPException

from bisheng.api.v1.schemas import UnifiedResponseModel


Expand All @@ -10,6 +12,10 @@ class BaseErrorCode:
def return_resp(cls, msg: str = None, data: any = None) -> UnifiedResponseModel:
return UnifiedResponseModel(status_code=cls.Code, status_message=msg or cls.Msg, data=data)

@classmethod
def http_exception(cls, msg: str = None) -> Exception:
return HTTPException(status_code=cls.Code, detail=msg or cls.Msg)


class UnAuthorizedError(BaseErrorCode):
Code: int = 403
Expand Down
10 changes: 10 additions & 0 deletions src/backend/bisheng/api/errcode/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,16 @@ class UserLoginOfflineError(BaseErrorCode):
Msg: str = '您的账户已在另一设备上登录,此设备上的会话已被注销。\n如果这不是您本人的操作,请尽快修改您的账户密码。'


class UserNameAlreadyExistError(BaseErrorCode):
Code: int = 10605
Msg: str = '用户名已存在'


class UserNeedGroupAndRoleError(BaseErrorCode):
Code: int = 10606
Msg: str = '用户组和角色不能为空'


class UserGroupNotDeleteError(BaseErrorCode):
Code: int = 10610
Msg: str = '用户组内还有用户,不能删除'
66 changes: 57 additions & 9 deletions src/backend/bisheng/api/services/user_service.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
import json

import functools
from base64 import b64decode
from typing import List

import rsa
from fastapi import HTTPException, Depends, Request
from fastapi_jwt_auth import AuthJWT

from bisheng.api.JWT import ACCESS_TOKEN_EXPIRE_TIME
from bisheng.api.errcode.user import UserLoginOfflineError
from bisheng.api.errcode.base import UnAuthorizedError
from bisheng.api.errcode.user import UserLoginOfflineError, UserNameAlreadyExistError, UserNeedGroupAndRoleError
from bisheng.api.utils import md5_hash
from bisheng.api.v1.schemas import CreateUserReq
from bisheng.cache.redis import redis_client
from bisheng.database.models.assistant import Assistant, AssistantDao
from bisheng.database.models.flow import Flow, FlowDao, FlowRead
Expand All @@ -13,11 +20,8 @@
from bisheng.database.models.user import User, UserDao
from bisheng.database.models.user_group import UserGroupDao
from bisheng.database.models.user_role import UserRoleDao
from fastapi import HTTPException, Depends
from fastapi_jwt_auth import AuthJWT

from bisheng.settings import settings
from bisheng.utils.constants import USER_CURRENT_SESSION
from bisheng.utils.constants import USER_CURRENT_SESSION, RSA_KEY


class UserPayload:
Expand Down Expand Up @@ -87,6 +91,41 @@ def check_groups_admin(self, group_ids: List[int]) -> bool:
return False


class UserService:

@classmethod
def decrypt_md5_password(cls, password: str):
if value := redis_client.get(RSA_KEY):
private_key = value[1]
password = md5_hash(rsa.decrypt(b64decode(password), private_key).decode('utf-8'))
else:
password = md5_hash(password)
return password

@classmethod
def create_user(cls, request: Request, login_user: UserPayload, req_data: CreateUserReq):
"""
创建用户
"""
exists_user = UserDao.get_user_by_username(req_data.user_name)
if exists_user:
# 抛出异常
raise UserNameAlreadyExistError.http_exception()
user = User(
user_name=req_data.user_name,
password=cls.decrypt_md5_password(req_data.password),
)
group_ids = []
role_ids = []
for one in req_data.group_roles:
group_ids.append(one.group_id)
role_ids.extend(one.role_ids)
if not group_ids or not role_ids:
raise UserNeedGroupAndRoleError.http_exception()
user = UserDao.add_user_with_groups_and_roles(user, group_ids, role_ids)
return user


def sso_login():
pass

Expand Down Expand Up @@ -224,8 +263,17 @@ async def get_login_user(authorize: AuthJWT = Depends()) -> UserPayload:
if not settings.get_system_login_method().allow_multi_login:
# 获取access_token
current_token = redis_client.get(USER_CURRENT_SESSION.format(user.user_id))
# 登录被挤下线了,状态码是200, 内部的status_code是403
# 登录被挤下线了,http状态码是200, status_code是特殊code
if current_token != authorize._token:
raise HTTPException(status_code=UserLoginOfflineError.Code,
detail=UserLoginOfflineError.Msg)
raise UserLoginOfflineError.http_exception()
return user


async def get_admin_user(authorize: AuthJWT = Depends()) -> UserPayload:
"""
获取超级管理账号,非超级管理员用户,抛出异常
"""
login_user = await get_login_user(authorize)
if not login_user.is_admin():
raise UnAuthorizedError.http_exception()
return login_user
8 changes: 7 additions & 1 deletion src/backend/bisheng/api/utils.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import hashlib
import json
import xml.dom.minidom
from pathlib import Path
Expand All @@ -12,7 +13,6 @@

from bisheng.api.v1.schemas import StreamData
from bisheng.database.base import session_getter
from bisheng.database.models.role_access import AccessType, RoleAccess
from bisheng.database.models.variable_value import Variable
from bisheng.graph.graph.base import Graph
from bisheng.utils.logger import logger
Expand Down Expand Up @@ -426,3 +426,9 @@ def get_request_ip(request: Request) -> str:
if x_forwarded_for:
return x_forwarded_for.split(',')[0]
return request.client.host


def md5_hash(original_string: str):
md5 = hashlib.md5()
md5.update(original_string.encode('utf-8'))
return md5.hexdigest()
11 changes: 11 additions & 0 deletions src/backend/bisheng/api/v1/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -306,3 +306,14 @@ class TestToolReq(BaseModel):
api_key: Optional[str] = Field(default='', description="api key")

request_params: Dict = Field(default=None, description="用户填写的请求参数")


class GroupAndRoles(BaseModel):
group_id: int
role_ids: List[int]


class CreateUserReq(BaseModel):
user_name: str = Field(max_length=30, description='用户名')
password: str = Field(description='密码')
group_roles: List[GroupAndRoles] = Field(description='要加入的用户组和角色列表')
54 changes: 29 additions & 25 deletions src/backend/bisheng/api/v1/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,19 @@
import random
import string
import uuid
from base64 import b64decode, b64encode
from base64 import b64encode
from datetime import datetime
from io import BytesIO
from typing import Annotated, Dict, List, Optional
from uuid import UUID

import rsa
from fastapi import APIRouter, Depends, HTTPException, Query, Body, Request
from fastapi.encoders import jsonable_encoder
from fastapi.security import OAuth2PasswordBearer
from fastapi_jwt_auth import AuthJWT
from sqlalchemy import and_, func
from sqlmodel import delete, select

from bisheng.api.errcode.base import UnAuthorizedError
from bisheng.api.errcode.user import (UserNotPasswordError, UserPasswordExpireError,
Expand All @@ -19,8 +25,8 @@
from bisheng.api.services.audit_log import AuditLogService
from bisheng.api.services.captcha import verify_captcha
from bisheng.api.services.user_service import (UserPayload, gen_user_jwt, gen_user_role, get_login_user,
get_assistant_list_by_access)
from bisheng.api.v1.schemas import UnifiedResponseModel, resp_200
get_assistant_list_by_access, get_admin_user, UserService)
from bisheng.api.v1.schemas import UnifiedResponseModel, resp_200, CreateUserReq
from bisheng.cache.redis import redis_client
from bisheng.database.base import session_getter
from bisheng.database.models.flow import Flow
Expand All @@ -35,28 +41,13 @@
from bisheng.utils.constants import CAPTCHA_PREFIX, RSA_KEY, USER_PASSWORD_ERROR, USER_CURRENT_SESSION
from bisheng.utils.logger import logger
from captcha.image import ImageCaptcha
from fastapi import APIRouter, Depends, HTTPException, Query, Body, Request
from fastapi.encoders import jsonable_encoder
from fastapi.security import OAuth2PasswordBearer
from fastapi_jwt_auth import AuthJWT
from sqlalchemy import and_, func
from sqlmodel import delete, select

# build router
router = APIRouter(prefix='', tags=['User'])

oauth2_scheme = OAuth2PasswordBearer(tokenUrl='token')


def decrypt_md5_password(password: str):
if value := redis_client.get(RSA_KEY):
private_key = value[1]
password = md5_hash(rsa.decrypt(b64decode(password), private_key).decode('utf-8'))
else:
password = md5_hash(password)
return password


@router.post('/user/regist', response_model=UnifiedResponseModel[UserRead], status_code=201)
async def regist(*, user: UserCreate):
# 验证码校验
Expand All @@ -71,7 +62,7 @@ async def regist(*, user: UserCreate):
if user_exists:
raise HTTPException(status_code=500, detail='账号已存在')
try:
db_user.password = decrypt_md5_password(user.password)
db_user.password = UserService.decrypt_md5_password(user.password)
# 判断下admin用户是否存在
admin = UserDao.get_user(1)
if admin:
Expand Down Expand Up @@ -128,7 +119,7 @@ async def login(*, request: Request, user: UserLogin, Authorize: AuthJWT = Depen
if not user.captcha_key or not await verify_captcha(user.captcha, user.captcha_key):
raise HTTPException(status_code=500, detail='验证码错误')

password = decrypt_md5_password(user.password)
password = UserService.decrypt_md5_password(user.password)

db_user = UserDao.get_user_by_username(user.user_name)
# 检查密码
Expand Down Expand Up @@ -848,7 +839,7 @@ async def reset_password(
if not login_user.check_groups_admin(user_group_ids):
raise HTTPException(status_code=403, detail='没有权限重置密码')

user_info.password = decrypt_md5_password(password)
user_info.password = UserService.decrypt_md5_password(password)
user_info.password_update_time = datetime.now()
UserDao.update_user(user_info)

Expand All @@ -868,13 +859,13 @@ async def change_password(*,
if not user_info.password:
return UserNotPasswordError.return_resp()

password = decrypt_md5_password(password)
password = UserService.decrypt_md5_password(password)

# 已登录用户告知是密码错误
if user_info.password != password:
return UserPasswordError.return_resp()

user_info.password = decrypt_md5_password(new_password)
user_info.password = UserService.decrypt_md5_password(new_password)
user_info.password_update_time = datetime.now()
UserDao.update_user(user_info)

Expand All @@ -895,17 +886,30 @@ async def change_password_public(*,
if not user_info.password:
return UserValidateError.return_resp()

if user_info.password != decrypt_md5_password(password):
if user_info.password != UserService.decrypt_md5_password(password):
return UserValidateError.return_resp()

user_info.password = decrypt_md5_password(new_password)
user_info.password = UserService.decrypt_md5_password(new_password)
user_info.password_update_time = datetime.now()
UserDao.update_user(user_info)

clear_error_password_key(username)
return resp_200()


@router.post('/user/create', status_code=200)
async def create_user(*,
request: Request,
admin_user: UserPayload = Depends(get_admin_user),
req: CreateUserReq):
"""
超级管理员创建用户
"""
logger.info(f'create_user username={admin_user.user_name}, username={req.user_name}')
data = UserService.create_user(request, admin_user, req)
return resp_200(data=data)


def md5_hash(string):
md5 = hashlib.md5()
md5.update(string.encode('utf-8'))
Expand Down
20 changes: 18 additions & 2 deletions src/backend/bisheng/database/models/user.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
from datetime import datetime
from typing import List, Optional

from bisheng.database.base import session_getter
from bisheng.database.models.base import SQLModelSerializable
from pydantic import validator
from sqlalchemy import Column, DateTime, text, func
from sqlmodel import Field, select

from bisheng.database.base import session_getter
from bisheng.database.models.base import SQLModelSerializable
from bisheng.database.models.role import DefaultRole, AdminRole
from bisheng.database.models.user_group import UserGroup
from bisheng.database.models.user_role import UserRole


Expand Down Expand Up @@ -160,3 +161,18 @@ def add_user_and_admin_role(cls, user: User) -> User:
session.commit()
session.refresh(user)
return user

@classmethod
def add_user_with_groups_and_roles(cls, user: User, group_ids: List[int], role_ids: List[int]) -> User:
with session_getter() as session:
session.add(user)
session.commit()
session.refresh(user)
for group_id in group_ids:
db_user_group = UserGroup(user_id=user.user_id, group_id=group_id)
session.add(db_user_group)
for role_id in role_ids:
db_user_role = UserRole(user_id=user.user_id, role_id=role_id)
session.add(db_user_role)
session.commit()
return user

0 comments on commit e8707e1

Please sign in to comment.