shaapi 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- shaapi/__init__.py +3 -0
- shaapi/cli.py +97 -0
- shaapi/generator.py +114 -0
- shaapi/template/.dockerignore +37 -0
- shaapi/template/.env.template +59 -0
- shaapi/template/.gitattributes +12 -0
- shaapi/template/.gitignore +170 -0
- shaapi/template/.gitlab-ci.yml +89 -0
- shaapi/template/Dockerfile +59 -0
- shaapi/template/LICENSE +21 -0
- shaapi/template/README.md +206 -0
- shaapi/template/backend/.gitignore +164 -0
- shaapi/template/backend/__init__.py +0 -0
- shaapi/template/backend/alembic/README +1 -0
- shaapi/template/backend/alembic/env.py +102 -0
- shaapi/template/backend/alembic/script.py.mako +26 -0
- shaapi/template/backend/alembic/versions/2026_06_08_1024-64524c63b666_initial.py +143 -0
- shaapi/template/backend/alembic.ini +117 -0
- shaapi/template/backend/app/__init__.py +55 -0
- shaapi/template/backend/app/admin/__init__.py +1 -0
- shaapi/template/backend/app/admin/api/v1/__init__.py +0 -0
- shaapi/template/backend/app/admin/api/v1/auth.py +59 -0
- shaapi/template/backend/app/admin/api/v1/casbin.py +218 -0
- shaapi/template/backend/app/admin/api/v1/login_log.py +63 -0
- shaapi/template/backend/app/admin/api/v1/opera_log.py +61 -0
- shaapi/template/backend/app/admin/api/v1/role.py +108 -0
- shaapi/template/backend/app/admin/api/v1/user.py +47 -0
- shaapi/template/backend/app/admin/schema/casbin_rule.py +45 -0
- shaapi/template/backend/app/admin/schema/login_log.py +36 -0
- shaapi/template/backend/app/admin/schema/opera_log.py +43 -0
- shaapi/template/backend/app/admin/schema/role.py +36 -0
- shaapi/template/backend/app/admin/schema/sso.py +37 -0
- shaapi/template/backend/app/admin/schema/token.py +74 -0
- shaapi/template/backend/app/admin/schema/user.py +93 -0
- shaapi/template/backend/app/admin/service/auth_service.py +233 -0
- shaapi/template/backend/app/admin/service/casbin_service.py +135 -0
- shaapi/template/backend/app/admin/service/login_log_service.py +62 -0
- shaapi/template/backend/app/admin/service/opera_log_service.py +31 -0
- shaapi/template/backend/app/admin/service/role_service.py +79 -0
- shaapi/template/backend/app/admin/service/secure_token_service.py +60 -0
- shaapi/template/backend/app/admin/service/user_service.py +153 -0
- shaapi/template/backend/app/api.py +11 -0
- shaapi/template/backend/common/__init__.py +0 -0
- shaapi/template/backend/common/cloud_storage/__init__.py +11 -0
- shaapi/template/backend/common/cloud_storage/cloud_storage.py +180 -0
- shaapi/template/backend/common/dataclasses.py +52 -0
- shaapi/template/backend/common/email_conf/email.py +105 -0
- shaapi/template/backend/common/enums.py +144 -0
- shaapi/template/backend/common/exception/__init__.py +0 -0
- shaapi/template/backend/common/exception/errors.py +87 -0
- shaapi/template/backend/common/exception/exception_handler.py +280 -0
- shaapi/template/backend/common/log.py +123 -0
- shaapi/template/backend/common/model.py +68 -0
- shaapi/template/backend/common/pagination.py +83 -0
- shaapi/template/backend/common/response/__init__.py +0 -0
- shaapi/template/backend/common/response/response_code.py +158 -0
- shaapi/template/backend/common/response/response_schema.py +110 -0
- shaapi/template/backend/common/schema.py +144 -0
- shaapi/template/backend/common/security/jwt.py +203 -0
- shaapi/template/backend/common/security/rbac.py +98 -0
- shaapi/template/backend/common/security/sec_token.py +6 -0
- shaapi/template/backend/common/socketio/action.py +11 -0
- shaapi/template/backend/common/socketio/server.py +50 -0
- shaapi/template/backend/common/sso/base.py +69 -0
- shaapi/template/backend/common/sso/google.py +127 -0
- shaapi/template/backend/core/conf.py +208 -0
- shaapi/template/backend/core/path_conf.py +24 -0
- shaapi/template/backend/core/registrar.py +195 -0
- shaapi/template/backend/crud/__init__.py +1 -0
- shaapi/template/backend/crud/crud_base.py +35 -0
- shaapi/template/backend/crud/crud_casbin.py +46 -0
- shaapi/template/backend/crud/crud_login_log.py +58 -0
- shaapi/template/backend/crud/crud_opera_log.py +58 -0
- shaapi/template/backend/crud/crud_role.py +128 -0
- shaapi/template/backend/crud/crud_user.py +267 -0
- shaapi/template/backend/database/__init__.py +0 -0
- shaapi/template/backend/database/db_postgres.py +125 -0
- shaapi/template/backend/database/db_redis.py +62 -0
- shaapi/template/backend/entrypoint-api.sh +19 -0
- shaapi/template/backend/lang/en/app.py +18 -0
- shaapi/template/backend/lang/en/auth.py +10 -0
- shaapi/template/backend/lang/fr/app.py +18 -0
- shaapi/template/backend/lang/fr/auth.py +10 -0
- shaapi/template/backend/main.py +54 -0
- shaapi/template/backend/middleware/__init__.py +1 -0
- shaapi/template/backend/middleware/access_middleware.py +19 -0
- shaapi/template/backend/middleware/i18n_middleware.py +19 -0
- shaapi/template/backend/middleware/jwt_auth_middleware.py +73 -0
- shaapi/template/backend/middleware/opera_log_middleware.py +179 -0
- shaapi/template/backend/middleware/state_middleware.py +26 -0
- shaapi/template/backend/models/__init__.py +10 -0
- shaapi/template/backend/models/associations.py +20 -0
- shaapi/template/backend/models/casbin_rule.py +30 -0
- shaapi/template/backend/models/login_log.py +28 -0
- shaapi/template/backend/models/opera_log.py +36 -0
- shaapi/template/backend/models/role.py +27 -0
- shaapi/template/backend/models/user.py +30 -0
- shaapi/template/backend/seeder/json/admin.json +15 -0
- shaapi/template/backend/seeder/json/user.json +15 -0
- shaapi/template/backend/seeder/run.py +34 -0
- shaapi/template/backend/static/ip2region.xdb +0 -0
- shaapi/template/backend/templates/build/meet.html +169 -0
- shaapi/template/backend/templates/build/new_account.html +373 -0
- shaapi/template/backend/templates/build/reset-password.html +170 -0
- shaapi/template/backend/templates/build/test_email.html +25 -0
- shaapi/template/backend/templates/build/welcome-one-1.html +160 -0
- shaapi/template/backend/templates/build/welcome-one.html +178 -0
- shaapi/template/backend/templates/build/welcome-two.html +234 -0
- shaapi/template/backend/templates/index.html +0 -0
- shaapi/template/backend/templates/src/new_account.mjml +15 -0
- shaapi/template/backend/templates/src/reset_password.mjml +19 -0
- shaapi/template/backend/templates/src/test_email.mjml +11 -0
- shaapi/template/backend/templates/ws/ws.html +70 -0
- shaapi/template/backend/utils/demo_site.py +18 -0
- shaapi/template/backend/utils/encrypt.py +108 -0
- shaapi/template/backend/utils/health_check.py +34 -0
- shaapi/template/backend/utils/prometheus.py +135 -0
- shaapi/template/backend/utils/request_parse.py +110 -0
- shaapi/template/backend/utils/serializers.py +75 -0
- shaapi/template/backend/utils/timezone.py +51 -0
- shaapi/template/backend/utils/trace_id.py +7 -0
- shaapi/template/backend/utils/translator.py +28 -0
- shaapi/template/devops/scripts/deploy.sh +7 -0
- shaapi/template/devops/scripts/setup_env.sh +62 -0
- shaapi/template/docker-compose.monitoring.yml +63 -0
- shaapi/template/docker-compose.override.yml +12 -0
- shaapi/template/docker-compose.yml +90 -0
- shaapi/template/docker-run.sh +99 -0
- shaapi/template/etc/dashboards/fastapi-observability.json +1044 -0
- shaapi/template/etc/dashboards.yaml +10 -0
- shaapi/template/etc/grafana/datasource.yml +79 -0
- shaapi/template/etc/prometheus/prometheus.yml +52 -0
- shaapi/template/package-lock.json +2102 -0
- shaapi/template/package.json +16 -0
- shaapi/template/pyproject.toml +78 -0
- shaapi/template/uv.lock +2866 -0
- shaapi-0.1.0.dist-info/METADATA +92 -0
- shaapi-0.1.0.dist-info/RECORD +141 -0
- shaapi-0.1.0.dist-info/WHEEL +4 -0
- shaapi-0.1.0.dist-info/entry_points.txt +2 -0
- shaapi-0.1.0.dist-info/licenses/LICENCE +21 -0
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
from typing import Annotated, Union
|
|
2
|
+
from fastapi import APIRouter, Path, Query, Request
|
|
3
|
+
from backend.common.enums import Role
|
|
4
|
+
from backend.common.pagination import DependsPagination, paging_data
|
|
5
|
+
from backend.database.db_postgres import CurrentSession
|
|
6
|
+
from backend.utils.serializers import select_as_dict
|
|
7
|
+
|
|
8
|
+
from backend.app.admin.schema.user import GetUserInfoListDetails, UserUpdate
|
|
9
|
+
from backend.app.admin.service.user_service import user_service
|
|
10
|
+
from backend.common.response.response_schema import ResponseModel, response_base
|
|
11
|
+
from backend.common.security.jwt import DependsJwtAuth
|
|
12
|
+
|
|
13
|
+
router = APIRouter(prefix="/user", tags=["User"])
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
# @router.get(
|
|
18
|
+
# "/{pk}",
|
|
19
|
+
# summary="Get user by ID",
|
|
20
|
+
# )
|
|
21
|
+
# async def get_user(request: Request, pk: Annotated[int, Path(...)]) -> ResponseModel:
|
|
22
|
+
# user = await user_service.get_by_id(user_id=pk)
|
|
23
|
+
# data = GetUserInfoListDetails(**select_as_dict(user))
|
|
24
|
+
# return response_base.success(request=request, data=data)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@router.get(
|
|
28
|
+
"/",
|
|
29
|
+
summary="Get users pagination",
|
|
30
|
+
dependencies=[
|
|
31
|
+
DependsJwtAuth,
|
|
32
|
+
DependsPagination,
|
|
33
|
+
],
|
|
34
|
+
)
|
|
35
|
+
async def get_pagination_users(
|
|
36
|
+
request: Request,
|
|
37
|
+
db: CurrentSession,
|
|
38
|
+
query: Annotated[str | None, Query()] = None,
|
|
39
|
+
role: Annotated[Union[Role, str] | None, Query()] = None,
|
|
40
|
+
status: Annotated[bool | None, Query()] = None,
|
|
41
|
+
) -> ResponseModel:
|
|
42
|
+
user_select = await user_service.get_select(
|
|
43
|
+
q=query, role=role, status=status
|
|
44
|
+
)
|
|
45
|
+
page_data = await paging_data(db, user_select, GetUserInfoListDetails)
|
|
46
|
+
return response_base.success(request=request, data=page_data)
|
|
47
|
+
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
from pydantic import ConfigDict, Field
|
|
2
|
+
|
|
3
|
+
from backend.common.enums import MethodType
|
|
4
|
+
from backend.common.schema import SchemaBase
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class CreatePolicyParam(SchemaBase):
|
|
8
|
+
sub: str = Field(..., description='User x_id / Role x_id')
|
|
9
|
+
path: str = Field(..., description='api path')
|
|
10
|
+
method: MethodType = Field(default=MethodType.GET, description='Request method')
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class UpdatePolicyParam(CreatePolicyParam):
|
|
14
|
+
pass
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class DeletePolicyParam(CreatePolicyParam):
|
|
18
|
+
pass
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class DeleteAllPoliciesParam(SchemaBase):
|
|
22
|
+
uuid: str | None = None
|
|
23
|
+
role: str
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class CreateUserRoleParam(SchemaBase):
|
|
27
|
+
uuid: str = Field(..., description='User x_id')
|
|
28
|
+
role: str = Field(..., description='role')
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class DeleteUserRoleParam(CreateUserRoleParam):
|
|
32
|
+
pass
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class GetPolicyListDetails(SchemaBase):
|
|
36
|
+
model_config = ConfigDict(from_attributes=True)
|
|
37
|
+
|
|
38
|
+
id: int
|
|
39
|
+
ptype: str = Field(..., description='Rule type, p / g')
|
|
40
|
+
v0: str = Field(..., description='User uuid / Role')
|
|
41
|
+
v1: str = Field(..., description='api path / roles')
|
|
42
|
+
v2: str | None = None
|
|
43
|
+
v3: str | None = None
|
|
44
|
+
v4: str | None = None
|
|
45
|
+
v5: str | None = None
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
|
|
3
|
+
from pydantic import ConfigDict
|
|
4
|
+
|
|
5
|
+
from backend.common.schema import SchemaBase
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class LoginLogSchemaBase(SchemaBase):
|
|
9
|
+
user_x_id: str
|
|
10
|
+
email: str
|
|
11
|
+
status: int
|
|
12
|
+
ip: str
|
|
13
|
+
country: str | None
|
|
14
|
+
region: str | None
|
|
15
|
+
city: str | None
|
|
16
|
+
user_agent: str
|
|
17
|
+
browser: str | None
|
|
18
|
+
os: str | None
|
|
19
|
+
device: str | None
|
|
20
|
+
msg: str
|
|
21
|
+
login_time: datetime
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class CreateLoginLogParam(LoginLogSchemaBase):
|
|
25
|
+
pass
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class UpdateLoginLogParam(LoginLogSchemaBase):
|
|
29
|
+
pass
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class GetLoginLogListDetails(LoginLogSchemaBase):
|
|
33
|
+
model_config = ConfigDict(from_attributes=True)
|
|
34
|
+
|
|
35
|
+
id: int
|
|
36
|
+
created_time: datetime
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
|
|
3
|
+
from pydantic import ConfigDict, Field
|
|
4
|
+
|
|
5
|
+
from backend.common.enums import StatusType
|
|
6
|
+
from backend.common.schema import SchemaBase
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class OperaLogSchemaBase(SchemaBase):
|
|
10
|
+
trace_id: str
|
|
11
|
+
user_email: str | None = None
|
|
12
|
+
method: str
|
|
13
|
+
title: str
|
|
14
|
+
path: str
|
|
15
|
+
ip: str
|
|
16
|
+
country: str | None = None
|
|
17
|
+
region: str | None = None
|
|
18
|
+
city: str | None = None
|
|
19
|
+
user_agent: str
|
|
20
|
+
os: str | None = None
|
|
21
|
+
browser: str | None = None
|
|
22
|
+
device: str | None = None
|
|
23
|
+
args: dict | None = None
|
|
24
|
+
status: StatusType = Field(default=StatusType.enable)
|
|
25
|
+
code: str
|
|
26
|
+
msg: str | None = None
|
|
27
|
+
cost_time: float
|
|
28
|
+
opera_time: datetime
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class CreateOperaLogParam(OperaLogSchemaBase):
|
|
32
|
+
pass
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class UpdateOperaLogParam(OperaLogSchemaBase):
|
|
36
|
+
pass
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class GetOperaLogListDetails(OperaLogSchemaBase):
|
|
40
|
+
model_config = ConfigDict(from_attributes=True)
|
|
41
|
+
|
|
42
|
+
id: int
|
|
43
|
+
created_time: datetime
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
|
|
3
|
+
from pydantic import ConfigDict, Field
|
|
4
|
+
|
|
5
|
+
from backend.common.enums import RoleDataScopeType, StatusType
|
|
6
|
+
from backend.common.schema import SchemaBase
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class RoleSchemaBase(SchemaBase):
|
|
10
|
+
name: str
|
|
11
|
+
data_scope: RoleDataScopeType = Field(
|
|
12
|
+
default=RoleDataScopeType.custom, description='Permission ranges (1: all data permissions 2: custom data permissions)'
|
|
13
|
+
)
|
|
14
|
+
status: StatusType = Field(default=StatusType.enable)
|
|
15
|
+
remark: str | None = None
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class CreateRoleParam(RoleSchemaBase):
|
|
19
|
+
pass
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class UpdateRoleParam(RoleSchemaBase):
|
|
23
|
+
pass
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class UpdateRoleMenuParam(SchemaBase):
|
|
27
|
+
menus: list[int]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class GetRoleListDetails(RoleSchemaBase):
|
|
31
|
+
model_config = ConfigDict(from_attributes=True)
|
|
32
|
+
|
|
33
|
+
id: int
|
|
34
|
+
x_id: str
|
|
35
|
+
created_time: datetime
|
|
36
|
+
updated_time: datetime | None = None
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
from typing import Optional
|
|
3
|
+
|
|
4
|
+
from pydantic import AnyHttpUrl
|
|
5
|
+
from pydantic import BaseModel
|
|
6
|
+
from pydantic import EmailStr
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class SocialTypes(Enum):
|
|
10
|
+
facebook = "facebook"
|
|
11
|
+
google = "google"
|
|
12
|
+
msal = "msal"
|
|
13
|
+
linkedin = "linkedin"
|
|
14
|
+
github = "github"
|
|
15
|
+
gitlab = "gitlab"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class OAuthRedirectLink(BaseModel):
|
|
19
|
+
|
|
20
|
+
url: AnyHttpUrl
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class OAuthCodeResponseSchema(BaseModel):
|
|
24
|
+
code: str
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class OAuthTokenResponseSchema(BaseModel):
|
|
28
|
+
token: str
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class OAuthUserDataResponseSchema(BaseModel):
|
|
32
|
+
external_id: str
|
|
33
|
+
email: EmailStr
|
|
34
|
+
social_type: SocialTypes
|
|
35
|
+
img: Optional[AnyHttpUrl]
|
|
36
|
+
firstname: Optional[str]
|
|
37
|
+
lastname: Optional[str]
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
from backend.common.schema import SchemaBase
|
|
6
|
+
from backend.common.enums import Secure_token_type
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class GetRoleDetails(SchemaBase):
|
|
10
|
+
name: str
|
|
11
|
+
class Config:
|
|
12
|
+
from_attributes = True
|
|
13
|
+
|
|
14
|
+
class ConnectedUserInfo(SchemaBase):
|
|
15
|
+
|
|
16
|
+
id: int
|
|
17
|
+
x_id: str
|
|
18
|
+
profile_image: dict | None = None
|
|
19
|
+
firstname: str | None = None
|
|
20
|
+
lastname: str | None = None
|
|
21
|
+
phone: str | None = None
|
|
22
|
+
email: str
|
|
23
|
+
roles: list[GetRoleDetails] | None = None
|
|
24
|
+
country_code: str | None = None
|
|
25
|
+
|
|
26
|
+
class Config:
|
|
27
|
+
from_attributes = True
|
|
28
|
+
|
|
29
|
+
class ConnectedCompanyInfo(SchemaBase):
|
|
30
|
+
id: int
|
|
31
|
+
x_id: str
|
|
32
|
+
name: str | None = None
|
|
33
|
+
email: str
|
|
34
|
+
logo: Any | None = None
|
|
35
|
+
min_members: int | None = None
|
|
36
|
+
max_members: int | None = None
|
|
37
|
+
email_verified: bool | None = None
|
|
38
|
+
country_code: str | None = None
|
|
39
|
+
role: GetRoleDetails | None = None
|
|
40
|
+
|
|
41
|
+
class Config:
|
|
42
|
+
from_attributes = True
|
|
43
|
+
|
|
44
|
+
class GetSwaggerToken(SchemaBase):
|
|
45
|
+
access_token: str
|
|
46
|
+
token_type: str = 'Bearer'
|
|
47
|
+
user: Any
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class AccessTokenBase(SchemaBase):
|
|
51
|
+
access_token: str
|
|
52
|
+
access_token_type: str = 'Bearer'
|
|
53
|
+
access_token_expire_time: datetime
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class GetNewToken(AccessTokenBase):
|
|
57
|
+
pass
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class GetLoginToken(AccessTokenBase):
|
|
61
|
+
user: ConnectedUserInfo
|
|
62
|
+
pass
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class GetCompanyLoginToken(AccessTokenBase):
|
|
66
|
+
company: ConnectedCompanyInfo
|
|
67
|
+
pass
|
|
68
|
+
|
|
69
|
+
class Secure_token(SchemaBase):
|
|
70
|
+
token_type: Secure_token_type
|
|
71
|
+
token: str
|
|
72
|
+
user_x_id: str
|
|
73
|
+
used: bool = False
|
|
74
|
+
expiration: datetime | None = None
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
|
|
3
|
+
from pydantic import ConfigDict, EmailStr, Field, HttpUrl, model_validator
|
|
4
|
+
from typing_extensions import Self
|
|
5
|
+
|
|
6
|
+
from backend.common.enums import StatusType
|
|
7
|
+
from backend.common.schema import SchemaBase
|
|
8
|
+
from backend.app.admin.schema.role import GetRoleListDetails
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class UserBase(SchemaBase):
|
|
13
|
+
firstname: str | None = None
|
|
14
|
+
lastname: str | None = None
|
|
15
|
+
fullname: str | None = None
|
|
16
|
+
phone: str | None = None
|
|
17
|
+
pseudo: str | None = None
|
|
18
|
+
email_verified: bool = False
|
|
19
|
+
country_code: str | None = None
|
|
20
|
+
profile_image: dict | None = None
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class UserLoginSchema(SchemaBase):
|
|
24
|
+
email: EmailStr
|
|
25
|
+
password: str
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class UserRegister(SchemaBase):
|
|
29
|
+
password: str
|
|
30
|
+
email: EmailStr
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class UserUpdate(UserBase):
|
|
34
|
+
pass
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class UpdateUserParam(SchemaBase):
|
|
38
|
+
firstname: str | None = None
|
|
39
|
+
lastname: str | None = None
|
|
40
|
+
email: EmailStr | None = None
|
|
41
|
+
phone: str | None = None
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class UserResetPassword(SchemaBase):
|
|
45
|
+
new_password: str
|
|
46
|
+
confirm_password: str
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class UserInfoSchemaBase(UserBase):
|
|
50
|
+
email: EmailStr
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class GetUserInfoNoRelationDetail(UserInfoSchemaBase):
|
|
55
|
+
model_config = ConfigDict(from_attributes=True)
|
|
56
|
+
|
|
57
|
+
id: int
|
|
58
|
+
x_id: str
|
|
59
|
+
profile_image: dict | None = None
|
|
60
|
+
status: bool
|
|
61
|
+
join_time: datetime = None
|
|
62
|
+
last_login_time: datetime | None = None
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class GetUserInfoListDetails(GetUserInfoNoRelationDetail):
|
|
67
|
+
model_config = ConfigDict(from_attributes=True)
|
|
68
|
+
|
|
69
|
+
roles: list[GetRoleListDetails] | None = []
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class GetCurrentUserInfoDetail(GetUserInfoListDetails):
|
|
73
|
+
model_config = ConfigDict(from_attributes=True)
|
|
74
|
+
|
|
75
|
+
roles: list[GetRoleListDetails] | list[str] | None = []
|
|
76
|
+
|
|
77
|
+
@model_validator(mode='after')
|
|
78
|
+
def handel(self) -> Self:
|
|
79
|
+
"""Dealing with sectors and roles"""
|
|
80
|
+
roles = self.roles
|
|
81
|
+
if roles:
|
|
82
|
+
self.roles = [{'name': role.name} for role in roles] # type: ignore
|
|
83
|
+
return self
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class CurrentUserIns(GetUserInfoListDetails):
|
|
87
|
+
model_config = ConfigDict(from_attributes=True)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class UpdatePasswordParam(SchemaBase):
|
|
91
|
+
old_password: str
|
|
92
|
+
new_password: str
|
|
93
|
+
confirm_password: str
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
from fastapi import Request, Response
|
|
2
|
+
from fastapi.security import HTTPBasicCredentials
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from starlette.background import BackgroundTask, BackgroundTasks
|
|
5
|
+
|
|
6
|
+
from backend.crud.crud_user import user_dao
|
|
7
|
+
from backend.models.user import User
|
|
8
|
+
from backend.app.admin.schema.token import GetLoginToken, GetNewToken
|
|
9
|
+
from backend.app.admin.schema.user import UserLoginSchema, UserRegister
|
|
10
|
+
from backend.app.admin.service.login_log_service import LoginLogService
|
|
11
|
+
from backend.common.enums import LoginLogStatusType
|
|
12
|
+
from backend.common.exception import errors
|
|
13
|
+
from backend.common.security.jwt import (
|
|
14
|
+
create_access_token,
|
|
15
|
+
create_new_token,
|
|
16
|
+
create_refresh_token,
|
|
17
|
+
get_token,
|
|
18
|
+
jwt_decode,
|
|
19
|
+
password_verify,
|
|
20
|
+
)
|
|
21
|
+
from backend.core.conf import settings
|
|
22
|
+
from backend.database.db_postgres import async_db_session
|
|
23
|
+
from backend.database.db_redis import redis_client
|
|
24
|
+
from backend.utils.timezone import timezone
|
|
25
|
+
from backend.utils.translator import Translator
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class AuthService:
|
|
29
|
+
@staticmethod
|
|
30
|
+
async def swagger_login(*, obj: HTTPBasicCredentials) -> tuple[str, User]:
|
|
31
|
+
async with async_db_session.begin() as db:
|
|
32
|
+
current_user = await user_dao.get_by_email(db, obj.username)
|
|
33
|
+
if not current_user:
|
|
34
|
+
raise errors.NotFoundError(msg='Incorrect email or password')
|
|
35
|
+
elif not password_verify(f'{obj.password}{current_user.salt}', current_user.password):
|
|
36
|
+
raise errors.AuthorizationError(msg='Incorrect email or password')
|
|
37
|
+
elif not current_user.status:
|
|
38
|
+
raise errors.AuthorizationError(msg='The user has been locked out. Please contact the system useristrator.')
|
|
39
|
+
access_token = await create_access_token(str(current_user.x_id), False)
|
|
40
|
+
await user_dao.update_login_time(db, obj.username)
|
|
41
|
+
return access_token.access_token, current_user
|
|
42
|
+
|
|
43
|
+
@staticmethod
|
|
44
|
+
async def register(
|
|
45
|
+
*, request: Request, response: Response, obj: UserRegister, background_tasks: BackgroundTasks
|
|
46
|
+
) -> GetLoginToken:
|
|
47
|
+
translator = Translator(request.state.locale)
|
|
48
|
+
|
|
49
|
+
async with async_db_session.begin() as db:
|
|
50
|
+
if not obj.password:
|
|
51
|
+
raise errors.ForbiddenError(msg=translator.t('auth.enpty_password'))
|
|
52
|
+
|
|
53
|
+
email = await user_dao.get_by_email(db, obj.email)
|
|
54
|
+
if email:
|
|
55
|
+
raise errors.ForbiddenError(msg=translator.t('auth.exist'))
|
|
56
|
+
|
|
57
|
+
new_user = await user_dao.add(db, obj)
|
|
58
|
+
|
|
59
|
+
# current_user_id = new_user.x_id
|
|
60
|
+
# is_multi_login = new_user.is_multi_login
|
|
61
|
+
user_x_id = new_user.x_id
|
|
62
|
+
email = new_user.email
|
|
63
|
+
|
|
64
|
+
access_token = await create_access_token(str(user_x_id), False)
|
|
65
|
+
refresh_token = await create_refresh_token(str(user_x_id), False)
|
|
66
|
+
|
|
67
|
+
n_user = await user_dao.get_with_relation(db, x_id=user_x_id, populates=['roles'])
|
|
68
|
+
|
|
69
|
+
background_tasks.add_task(
|
|
70
|
+
LoginLogService.create,
|
|
71
|
+
**dict(
|
|
72
|
+
db=db,
|
|
73
|
+
request=request,
|
|
74
|
+
user_x_id=user_x_id,
|
|
75
|
+
email=email,
|
|
76
|
+
login_time=datetime.now(),
|
|
77
|
+
status=LoginLogStatusType.success.value,
|
|
78
|
+
msg=translator.t('auth.successful'),
|
|
79
|
+
)
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
# await user_dao.update_login_time(db, obj.email)
|
|
83
|
+
response.set_cookie(
|
|
84
|
+
key=settings.COOKIE_REFRESH_TOKEN_KEY,
|
|
85
|
+
value=refresh_token.refresh_token,
|
|
86
|
+
max_age=settings.COOKIE_REFRESH_TOKEN_EXPIRE_SECONDS,
|
|
87
|
+
expires=timezone.f_utc(refresh_token.refresh_token_expire_time),
|
|
88
|
+
httponly=True,
|
|
89
|
+
samesite='none'
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
data = GetLoginToken(
|
|
93
|
+
access_token=access_token.access_token,
|
|
94
|
+
access_token_expire_time=access_token.access_token_expire_time,
|
|
95
|
+
user=n_user, # type: ignore
|
|
96
|
+
)
|
|
97
|
+
return data
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
@staticmethod
|
|
101
|
+
async def login(
|
|
102
|
+
*, request: Request, response: Response, obj: UserLoginSchema, background_tasks: BackgroundTasks
|
|
103
|
+
) -> GetLoginToken:
|
|
104
|
+
translator = Translator(request.state.locale)
|
|
105
|
+
|
|
106
|
+
async with async_db_session.begin() as db:
|
|
107
|
+
try:
|
|
108
|
+
current_user = await user_dao.get_by_email(db, email=obj.email, populates=['roles'])
|
|
109
|
+
if not current_user:
|
|
110
|
+
raise errors.NotFoundError(msg=translator.t('auth.incorrect_credential'))
|
|
111
|
+
|
|
112
|
+
user_x_id = current_user.x_id
|
|
113
|
+
email = current_user.email
|
|
114
|
+
|
|
115
|
+
if not password_verify(obj.password + current_user.salt, current_user.password):
|
|
116
|
+
raise errors.AuthorizationError(msg=translator.t('auth.incorrect_credential'))
|
|
117
|
+
elif not current_user.status:
|
|
118
|
+
raise errors.AuthorizationError(msg=translator.t('auth.account_locked'))
|
|
119
|
+
|
|
120
|
+
access_token = await create_access_token(str(user_x_id), False)
|
|
121
|
+
refresh_token = await create_refresh_token(str(user_x_id), False)
|
|
122
|
+
|
|
123
|
+
except errors.NotFoundError as e:
|
|
124
|
+
raise errors.NotFoundError(msg=e.msg)
|
|
125
|
+
except (errors.AuthorizationError, errors.CustomError) as e:
|
|
126
|
+
task = BackgroundTask(
|
|
127
|
+
LoginLogService.create,
|
|
128
|
+
**dict(
|
|
129
|
+
db=db,
|
|
130
|
+
request=request,
|
|
131
|
+
user_x_id=user_x_id,
|
|
132
|
+
email=email,
|
|
133
|
+
login_time=datetime.now(),
|
|
134
|
+
status=LoginLogStatusType.fail.value,
|
|
135
|
+
msg=e.msg,
|
|
136
|
+
),
|
|
137
|
+
)
|
|
138
|
+
raise errors.AuthorizationError(msg=e.msg, background=task)
|
|
139
|
+
except Exception as e:
|
|
140
|
+
raise e
|
|
141
|
+
else:
|
|
142
|
+
background_tasks.add_task(
|
|
143
|
+
LoginLogService.create,
|
|
144
|
+
**dict(
|
|
145
|
+
db=db,
|
|
146
|
+
request=request,
|
|
147
|
+
user_x_id=user_x_id,
|
|
148
|
+
email=email,
|
|
149
|
+
login_time=datetime.now(),
|
|
150
|
+
status=LoginLogStatusType.success.value,
|
|
151
|
+
msg=translator.t('auth.successful'),
|
|
152
|
+
),
|
|
153
|
+
)
|
|
154
|
+
# await redis_client.delete(f'{user_settings.CAPTCHA_LOGIN_REDIS_PREFIX}:{request.state.ip}')
|
|
155
|
+
|
|
156
|
+
await user_dao.update_login_time(db, obj.email)
|
|
157
|
+
response.set_cookie(
|
|
158
|
+
key=settings.COOKIE_REFRESH_TOKEN_KEY,
|
|
159
|
+
value=refresh_token.refresh_token,
|
|
160
|
+
max_age=settings.COOKIE_REFRESH_TOKEN_EXPIRE_SECONDS,
|
|
161
|
+
expires=timezone.f_utc(refresh_token.refresh_token_expire_time),
|
|
162
|
+
httponly=True,
|
|
163
|
+
samesite='none'
|
|
164
|
+
)
|
|
165
|
+
await db.refresh(current_user)
|
|
166
|
+
data = GetLoginToken(
|
|
167
|
+
access_token=access_token.access_token,
|
|
168
|
+
access_token_expire_time=access_token.access_token_expire_time,
|
|
169
|
+
user=current_user, # type: ignore
|
|
170
|
+
)
|
|
171
|
+
return data
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
@staticmethod
|
|
175
|
+
async def new_token(*, request: Request, response: Response) -> GetNewToken:
|
|
176
|
+
translator = Translator(request.state.locale)
|
|
177
|
+
refresh_token = request.cookies.get(settings.COOKIE_REFRESH_TOKEN_KEY)
|
|
178
|
+
|
|
179
|
+
if not refresh_token:
|
|
180
|
+
raise errors.TokenError(msg=translator.t('auth.refresh_token_not_found'))
|
|
181
|
+
try:
|
|
182
|
+
user_x_id = jwt_decode(refresh_token)
|
|
183
|
+
except Exception:
|
|
184
|
+
raise errors.TokenError(msg=translator.t('auth.invalid_refresh_token'))
|
|
185
|
+
# if request.user.id != user_id:
|
|
186
|
+
# raise errors.TokenError(msg='Refresh Token is invalid')
|
|
187
|
+
async with async_db_session() as db:
|
|
188
|
+
current_user = await user_dao.get_by_x_id(db, user_x_id)
|
|
189
|
+
if not current_user:
|
|
190
|
+
raise errors.NotFoundError(msg=translator.t('auth.incorrect_credential'))
|
|
191
|
+
elif not current_user.status:
|
|
192
|
+
raise errors.AuthorizationError(msg=translator.t('auth.account_locked'))
|
|
193
|
+
current_token = get_token(request)
|
|
194
|
+
new_token = await create_new_token(
|
|
195
|
+
sub=str(current_user.x_id),
|
|
196
|
+
token=current_token,
|
|
197
|
+
refresh_token=refresh_token,
|
|
198
|
+
multi_login=False,
|
|
199
|
+
)
|
|
200
|
+
response.set_cookie(
|
|
201
|
+
key=settings.COOKIE_REFRESH_TOKEN_KEY,
|
|
202
|
+
value=new_token.new_refresh_token,
|
|
203
|
+
max_age=settings.COOKIE_REFRESH_TOKEN_EXPIRE_SECONDS,
|
|
204
|
+
expires=timezone.f_utc(new_token.new_refresh_token_expire_time),
|
|
205
|
+
httponly=True,
|
|
206
|
+
samesite='none'
|
|
207
|
+
)
|
|
208
|
+
data = GetNewToken(
|
|
209
|
+
access_token=new_token.new_access_token,
|
|
210
|
+
access_token_expire_time=new_token.new_access_token_expire_time,
|
|
211
|
+
)
|
|
212
|
+
return data
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
@staticmethod
|
|
216
|
+
async def logout(*, request: Request, response: Response) -> None:
|
|
217
|
+
token = get_token(request)
|
|
218
|
+
refresh_token = request.cookies.get(settings.COOKIE_REFRESH_TOKEN_KEY)
|
|
219
|
+
response.delete_cookie(settings.COOKIE_REFRESH_TOKEN_KEY)
|
|
220
|
+
# if request.user.is_multi_login:
|
|
221
|
+
# key = f'{settings.TOKEN_REDIS_PREFIX}:{request.user.id}:{token}'
|
|
222
|
+
# await redis_client.delete(key)
|
|
223
|
+
# if refresh_token:
|
|
224
|
+
# key = f'{settings.TOKEN_REFRESH_REDIS_PREFIX}:{request.user.id}:{refresh_token}'
|
|
225
|
+
# await redis_client.delete(key)
|
|
226
|
+
# else:
|
|
227
|
+
key_prefix = f'{settings.TOKEN_REDIS_PREFIX}:{request.user.x_id}:'
|
|
228
|
+
await redis_client.delete_prefix(key_prefix)
|
|
229
|
+
key_prefix = f'{settings.TOKEN_REFRESH_REDIS_PREFIX}:{request.user.x_id}:'
|
|
230
|
+
await redis_client.delete_prefix(key_prefix)
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
auth_service = AuthService()
|