Initial commit

This commit is contained in:
shahzeydyfah
2024-02-15 00:17:51 +03:30
commit 4b0396466d
111 changed files with 6296 additions and 0 deletions
+39
View File
@@ -0,0 +1,39 @@
from typing import List
from scm.models.units import UnitModel
from auth.utils.mongo import use_auth_collection
from core.app import app
from core.utils.api import get_namespace_api
BASE = get_namespace_api('user')
@app.get(BASE + '/me')
async def get():
return {"code": 0,
"result": {"userId": "1",
"username": "admin",
"realName": "احمد رضا عزیزان",
"avatar": "https://q1.qlogo.cn/g?b=qq&nk=190848757&s=640",
"desc": "manager",
"password": "admin",
"token": "fakeToken1",
"homePath": "/dashboard/analysis",
"roles": [{"roleName": "Super Admin",
"value": "super"}]},
"message": "ok",
"type": "success"}
@app.get(BASE + '/units')
async def get_units() -> List[UnitModel]:
cursor = use_auth_collection('units').find()
return [UnitModel(**document) async for document in cursor]
@app.post(BASE + '/units/create')
async def post(document: UnitModel):
document = document.init_id().mongo_dict()
result = await use_auth_collection('units').insert_one(document)
return result.inserted_id
+1
View File
@@ -0,0 +1 @@
from .tasks import *
View File
+2
View File
@@ -0,0 +1,2 @@
from .auth import *
from .property import *
+242
View File
@@ -0,0 +1,242 @@
import datetime
import uuid
from typing import Optional, Union, List, Literal
import pydantic
from fastapi import Body
from pydantic import Field, validator
from core.models.base import BaseModel, BaseCUAt
from core.utils.utils import is_mobile_number, is_email
class AuthModel(BaseModel):
username: str
password: str
class SessionModel(BaseModel):
id: Optional[uuid.UUID] = Field(alias='_id')
user_id: Optional[uuid.UUID]
access_token: Optional[str]
token: Optional[str]
refresh_token: Optional[str]
token_type: Optional[str]
agent: Optional[List[str]]
ip_info: Optional[dict]
class NotifyConfigModel(pydantic.BaseModel):
sms_info: Optional[bool]
sms_emergency: Optional[bool]
push: Optional[bool]
email: Optional[bool]
class UserOptions(pydantic.BaseModel):
active_password_login: Optional[bool]
last_password_changes: Optional[datetime.datetime]
active_otp: Optional[bool]
active_token: Optional[bool]
dark_mode: Optional[Literal['LIGHT', 'DARK', 'SYSTEM', 'DYNAMIC']]
time_format: Optional[str]
date_format: Optional[str]
date_type: Optional[str]
lang: Optional[str]
notify: Optional[NotifyConfigModel]
class VisualUserUpdate(pydantic.BaseModel):
storage: Optional[str]
class UserUpdateModel(BaseModel):
username: Optional[str]
email: Optional[str]
name: Optional[str]
first_name: Optional[str]
last_name: Optional[str]
unit: Optional[str]
personnel_code: Optional[str]
avatar: Optional[str]
desc: Optional[str]
gender: Optional[Literal[1, 2, 3]]
home_path: Optional[str]
force_password_change: Optional[bool]
disabled: Optional[bool]
class PasswordChangeModel(pydantic.BaseModel):
old_password: str
new_password: str = Field(min_length=6, max_length=28)
class UserModel(BaseModel, BaseCUAt):
id: Optional[Union[uuid.UUID, str]] = Field(alias='_id')
username: Optional[str]
mobile: Optional[str]
email: Optional[str]
name: Optional[Union[str,List[str]]]
first_name: Optional[str]
last_name: Optional[str]
is_su: Optional[bool]
unit: Optional[str]
units: Optional[List[str]]
personnel_code: Optional[str]
avatar: Optional[str]
desc: Optional[str]
gender: Optional[Literal[1, 2, 3]]
scopes: Optional[List[str]]
options: Optional[UserOptions]
home_path: Optional[str]
force_password_change: Optional[bool]
roles: Optional[List[uuid.UUID]]
_options: Optional[dict]
disabled: Optional[bool]
async def get_roles(self, exclude: [] = None, include: [] = None):
from auth.utils.mongo import use_auth_collection
projection = {}
if exclude:
projection = {
f: 0 for f in exclude
}
if include:
projection = {
f: 1 for f in include
}
cursor = use_auth_collection('roles').find(
{'_id': {'$in': [uuid.UUID(str(role)) for role in self.roles]}}, projection)
return [role async for role in cursor]
async def get_units(self):
roles = await self.get_roles(include=['units'])
units = []
for role in roles:
units.extend(role.get('units', []))
return units
def is_superuser(self) -> bool:
return self.is_su
@validator("mobile")
def mobile_validation(cls, v):
if not is_mobile_number(v):
raise ValueError('Mobile not valid')
return v
@validator("email")
def email_validation(cls, v):
if not is_email(v):
raise ValueError('Email not valid')
return v
class OAuth2PasswordRequestFormJson:
"""
This is a dependency class, use it like:
@app.post("/login")
def login(form_data: OAuth2PasswordRequestForm = Depends()):
data = form_data.parse()
print(data.username)
print(data.password)
for scope in data.scopes:
print(scope)
if data.client_id:
print(data.client_id)
if data.client_secret:
print(data.client_secret)
return data
It creates the following Form request parameters in your endpoint:
grant_type: the OAuth2 spec says it is required and MUST be the fixed string "password".
Nevertheless, this dependency class is permissive and allows not passing it. If you want to enforce it,
use instead the OAuth2PasswordRequestFormStrict dependency.
username: username string. The OAuth2 spec requires the exact field name "username".
password: password string. The OAuth2 spec requires the exact field name "password".
scope: Optional string. Several scopes (each one a string) separated by spaces. E.g.
"items:read items:write users:read profile openid"
client_id: optional string. OAuth2 recommends sending the client_id and client_secret (if any)
using HTTP Basic auth, as: client_id:client_secret
client_secret: optional string. OAuth2 recommends sending the client_id and client_secret (if any)
using HTTP Basic auth, as: client_id:client_secret
"""
def __init__(
self,
grant_type: str = Body(None, regex="password"),
username: str = Body(...),
password: str = Body(...),
scopes: Optional[List[str]] = Body(['default']),
client_id: Optional[str] = Body(None),
client_secret: Optional[str] = Body(None),
):
self.grant_type = grant_type
self.username = username
self.password = password
self.scopes = scopes
self.client_id = client_id
self.client_secret = client_secret
class OAuth2OptRequest:
def __init__(
self,
grant_type: str = Body(None, regex="password"),
allow_register: bool = Body(None),
mobile: str = Body(...),
token: Optional[int] = Body(None),
scopes: Optional[List[str]] = Body(['customer']),
step: Literal[1, 2] = Body(1),
client_id: Optional[str] = Body(None),
client_secret: Optional[str] = Body(None),
):
self.grant_type = grant_type
self.allow_register = allow_register
self.mobile = mobile
self.token = token
self.scopes = scopes
self.step = step
self.client_id = client_id
self.client_secret = client_secret
class Token(BaseModel):
access_token: str
token_type: str
class TokenData(BaseModel):
username: Optional[str] = None
scopes: List[str] = []
class UserInDB(UserModel):
password: Optional[str]
class CreateTokenModel(BaseModel):
id: Optional[Union[uuid.UUID, str]] = Field(alias='_id')
user_id: Optional[uuid.UUID]
mode: Optional[Literal['user_admin', 'customer']]
access_token: str
token: str
refresh_token: str
token_type: str = 'bearer'
class PasswordForgottenInput(pydantic.BaseModel):
password: str
mode: Optional[Literal['user_admin', 'customer']]
mobile: str
token: int
class UIdsInput(BaseModel):
u_ids: List[uuid.UUID]
+16
View File
@@ -0,0 +1,16 @@
from typing import Optional, Any
from uuid import UUID
from pydantic import Field
from core.models.base import BaseModel
class AuthPropertyModel(BaseModel):
id: Optional[UUID] = Field(alias='_id')
key: Optional[str]
user_id: Optional[UUID]
role_id: Optional[UUID]
value: Optional[Any]
meta: Optional[Any]
parent_id: Optional[UUID]
+22
View File
@@ -0,0 +1,22 @@
import uuid
from typing import Optional, Union, List
import pydantic
from pydantic import Field
from core.models.base import BaseModel, BaseCUAt
class ModuleModel(pydantic.BaseModel):
title: str
icon: str
description: Optional[str]
url: Optional[str]
class RoleModel(BaseModel, BaseCUAt):
id: Optional[Union[uuid.UUID, str]] = Field(alias='_id')
title: str
f_routes: Optional[List[str]]
units: Optional[List[uuid.UUID]]
modules: Optional[List[ModuleModel]]
+12
View File
@@ -0,0 +1,12 @@
import uuid
from typing import Optional, Union, List
from pydantic import Field
from core.models.base import BaseSoftDelete, BaseModel
class UnitModel(BaseModel, BaseSoftDelete):
id: Union[Optional[uuid.UUID], str] = Field(alias='_id')
title: str
parents: Optional[List[uuid.UUID]]
+165
View File
@@ -0,0 +1,165 @@
from http.client import HTTPException
from typing import List
from uuid import UUID
from auth.models import UserInDB, UserModel, UIdsInput, UserOptions, SessionModel, PasswordChangeModel, UserUpdateModel, \
VisualUserUpdate
from auth.models.roles import RoleModel, ModuleModel
from auth.models.units import UnitModel
from auth.utils.mongo import use_auth_collection
from auth.utils.pwd import get_password_hash, verify_password
from core.exceptions import ModifiedCountException, ValidationException
from core.models.base import DELETED_AT_QUERY
from core.utils.app_context import use_user
from core.utils.crudify import mongo_soft_deleter
COLLECTION_UNITS = 'units'
COLLECTION_USERS = 'users'
COLLECTION_ROLES = 'roles'
COLLECTION_SESSIONS = 'sessions'
_projection_user = {
'mobile': 1,
'email': 1,
'username': 1,
'_id': 1,
'name': 1
}
class AuthRepository:
@staticmethod
async def create(_input: UserInDB) -> dict:
record = _input.init_id().cu_at().mongo_dict()
record['password'] = get_password_hash(_input.password)
insert = await use_auth_collection(COLLECTION_USERS).insert_one(record)
if not insert.acknowledged:
raise HTTPException(status_code=400, detail="User already exists")
return insert.inserted_id
@staticmethod
async def user_query(query: str = '') -> List[UserModel]:
q = {'$or': [{'name': {'$regex': query.strip()}},
{'mobile': {'$regex': query.strip()}}
]} if query.strip() else {}
cursor = use_auth_collection('users').find(q, {'_id': 1, 'name': 1, 'mobile': 1})
return [UserModel(**user) async for user in cursor]
@staticmethod
async def get_units():
cursor = use_auth_collection(COLLECTION_UNITS).find()
return [UnitModel(**document) async for document in cursor]
@staticmethod
async def get_user(_id: UUID) -> UserModel:
user = await use_auth_collection(COLLECTION_USERS).find_one({"_id": _id}, _projection_user)
if user is None:
raise HTTPException(status_code=404, detail="User not found")
return UserModel(**user)
@staticmethod
async def update_options(options: UserOptions) -> bool:
user = use_user()
user = await use_auth_collection(COLLECTION_USERS) \
.update_one({"_id": user.id}, {"$set": {"options": options.dict()}})
if user.modified_count == 0:
raise ModifiedCountException("User not updated")
return True
@staticmethod
async def update_avatar(av_input: VisualUserUpdate) -> bool:
user = use_user()
_update_query = {"$set": {"avatar": av_input.storage}} if av_input.storage else {'$unset': {'avatar': 1}}
user = await use_auth_collection(COLLECTION_USERS) \
.update_one({"_id": user.id}, _update_query)
if user.modified_count == 0:
raise ModifiedCountException("User not updated")
return True
@staticmethod
async def update_password(_input: PasswordChangeModel) -> bool:
user = use_user()
user = await use_auth_collection(COLLECTION_USERS).find_one({'_id': user.id})
if not user:
raise HTTPException(status_code=404, detail="User not found")
if not verify_password(_input.old_password, user.get('password', None)):
raise ValidationException('Old Password not valid')
user['password'] = get_password_hash(_input.new_password)
user = await use_auth_collection(COLLECTION_USERS).update_one({'_id': user.get('_id')},
{'$set': {
'password': user['password']}})
if user.modified_count == 0:
raise ModifiedCountException("Password")
return True
@staticmethod
async def update_user(_input: UserUpdateModel) -> bool:
user = use_user()
user = await use_auth_collection(COLLECTION_USERS).find_one({'_id': UUID(str(user.id))})
user = await use_auth_collection(COLLECTION_USERS).update_one({'_id': user.get('_id')},
{'$set': _input.u_at().dict()})
if user.modified_count == 0:
raise ModifiedCountException("User not update")
return True
@staticmethod
async def get_my_sessions() -> List[SessionModel]:
user = use_user()
user = use_auth_collection(COLLECTION_SESSIONS).find({"user_id": user.id, **DELETED_AT_QUERY})
return [SessionModel(**x) async for x in user]
@staticmethod
async def delete_session(session_id: UUID) -> bool:
return await mongo_soft_deleter(session_id, COLLECTION_SESSIONS, client=use_auth_collection)
@staticmethod
async def get_users_by_id(_input: UIdsInput) -> List[UserModel]:
cursor = use_auth_collection(COLLECTION_USERS) \
.find({'_id': {'$in': _input.u_ids}})
return [UserModel(**user) async for user in cursor]
@staticmethod
async def create_unit(_input: UnitModel) -> str:
document = _input.init_id().mongo_dict()
result = await use_auth_collection(COLLECTION_UNITS).insert_one(document)
return result.inserted_id
@staticmethod
async def get_roles() -> List[dict]:
roles = await use_auth_collection(COLLECTION_ROLES).find().to_list(None)
return [role for role in roles]
@staticmethod
async def create_role(_input: RoleModel) -> str:
record = _input.init_id().cu_at().mongo_dict()
insert = await use_auth_collection(COLLECTION_ROLES).insert_one(record)
if not insert.acknowledged:
raise HTTPException("Error creating role")
return insert.inserted_id
@staticmethod
async def get_my_modules() -> List[ModuleModel]:
user = use_user()
records = await use_auth_collection(COLLECTION_ROLES).find(
{'_id': {'$in': user.roles or []}, **DELETED_AT_QUERY}, {'modules': True}).to_list(None)
if not records:
return []
_list = [x.get('modules', []) for x in records]
if _list:
return list([item for sublist in _list for item in sublist])
return []
@staticmethod
async def get_f_routes(roles_ids: List[UUID]) -> List[str]:
roles = await use_auth_collection(COLLECTION_ROLES).find(
{'_id': {'$in': roles_ids}}) \
.to_list(None)
def _union_list(*kwargs):
final = []
for kwarg in kwargs:
final.extend(kwarg)
return final
return _union_list(*[role['f_routes'] for role in roles])
+55
View File
@@ -0,0 +1,55 @@
from typing import Union, List
from uuid import UUID
from auth.models import AuthPropertyModel
from auth.utils.mongo import use_auth_collection
from core.models.base import DELETED_AT_QUERY
from core.utils.app_context import use_user
from core.utils.crudify import mongo_inserter, mongo_updater, mongo_find_one, mongo_soft_deleter
COLLECTION_PROPERTY = 'property'
class PropertyRepository:
@staticmethod
async def create(_input: AuthPropertyModel) -> dict:
return await mongo_inserter(_input, COLLECTION_PROPERTY, client=use_auth_collection,
getter=PropertyRepository.get)
@staticmethod
async def update(_id: UUID, _input: AuthPropertyModel) -> bool:
return await mongo_updater(_input,
_id,
COLLECTION_PROPERTY,
client=use_auth_collection,
getter=PropertyRepository.get)
@staticmethod
async def get(_input: Union[UUID, str]) -> AuthPropertyModel:
"""
get argument from args
Args:
_input: Union[UUID,str]
Returns:
AuthPropertyModel
"""
q = {'key': _input} if isinstance(_input, str) else {'_id': _input} if isinstance(_input, UUID) else _input
q['owner_id'] = use_user().id
result = await mongo_find_one(q, COLLECTION_PROPERTY, client=use_auth_collection)
return AuthPropertyModel(**result)
@staticmethod
async def fetch() -> List[AuthPropertyModel]:
q = {}
q['owner_id'] = use_user().id
result = await use_auth_collection(COLLECTION_PROPERTY) \
.find({**q, **DELETED_AT_QUERY}) \
.to_list(100)
return [AuthPropertyModel(**x) for x in result]
@staticmethod
async def delete(_input: Union[UUID, str]) -> bool:
q = {'key': _input} if isinstance(_input, str) else {'_id': _input}
q['owner_id'] = use_user().id
result = await mongo_soft_deleter(q, COLLECTION_PROPERTY, client=use_auth_collection)
return result
+54
View File
@@ -0,0 +1,54 @@
from urllib.request import Request
import httpx
from fastapi import HTTPException, Depends, Security
from fastapi.security import SecurityScopes
from httpx import ConnectError
from .config import AUTH_SERVER_URL
from .model import UserModel, ValidateTokenInput
from .schema import oauth2
async def validate_token(_input: ValidateTokenInput):
try:
client = httpx.AsyncClient()
headers = {'Content-Type': 'application/json', }
result = await client.request('POST', AUTH_SERVER_URL, json=_input.dict(), headers=headers)
export = result.text
if 'application/json' in result.headers['content-type']:
export = result.json()
if result.status_code > 299:
raise HTTPException(status_code=result.status_code, detail=export)
return export
except ConnectError as e:
raise HTTPException(status_code=400, detail=str(e))
except HTTPException as e:
raise e
except Exception as e:
raise HTTPException(status_code=400, detail=str(e))
async def get_current_user(security_scopes: SecurityScopes, token: str = Depends(oauth2)):
if security_scopes.scopes:
authenticate_value = f'Bearer scope="{security_scopes.scope_str}"'
else:
authenticate_value = f"Bearer"
credentials_exception = HTTPException(
status_code=401,
detail="Could not validate credentials a",
headers={"WWW-Authenticate": authenticate_value},
)
user = await validate_token(ValidateTokenInput(token=token, scopes=security_scopes.scopes))
if user is None:
raise credentials_exception
user.scopes = security_scopes.scopes if isinstance(
security_scopes.scopes, list) else security_scopes.scopes
return user
async def get_current_active_user(request: Request, current_user: UserModel = Security(get_current_user, scopes=["account"]),):
if current_user.disabled:
raise HTTPException(status_code=400, detail="Inactive user")
request.state.user = UserModel(**current_user.dict())
return current_user
+1
View File
@@ -0,0 +1 @@
AUTH_SERVER_URL = 'https://scm.hacoupian.net/api/scm/v1/auth/validate-token'
+26
View File
@@ -0,0 +1,26 @@
import uuid
from typing import Optional, Union, List, Any
from pydantic import Field, BaseModel
class UserModel(BaseModel):
id: Optional[Union[uuid.UUID, str]] = Field(alias='_id')
username: Optional[str]
mobile: Optional[str]
name: Optional[str]
first_name: Optional[str]
last_name: Optional[str]
is_su: Optional[bool]
unit: Optional[str]
avatar: Optional[str]
desc: Optional[str]
scopes: Optional[List[str]]
home_path: Optional[str]
roles: Optional[List[Any]]
disabled: Optional[bool]
class ValidateTokenInput(BaseModel):
token: str
scopes: Optional[List[str]] = ['account']
+9
View File
@@ -0,0 +1,9 @@
from fastapi.security import OAuth2PasswordBearer
oauth2 = OAuth2PasswordBearer(
tokenUrl="token",
scopes={
"account": "Read information about the current user.",
"scm": "Scm Modules",
},
)
+45
View File
@@ -0,0 +1,45 @@
from uuid import UUID
from app.models.customer import CustomerModel
from auth.models import UserInDB
from auth.utils.mongo import use_auth_collection
async def get_user_by_username(username: str) -> UserInDB:
"""
Retrieves user from the database by username.
"""
result = await use_auth_collection('users').find_one({'username': username})
return UserInDB(**result) if result else None
async def get_customer_by_username(username: str) -> UserInDB:
"""
Retrieves user from the database by username.
"""
result = await use_auth_collection('customer').find_one({'$or': [{'username': username}, {'mobile': username}]})
return UserInDB(**result) if result else None
async def get_user_by_id(user_id: str) -> UserInDB | None:
"""
Retrieves user from the database by username.
"""
try:
uuid = UUID(str(user_id))
except ValueError:
return None
result = await use_auth_collection('users').find_one({'_id': uuid})
return UserInDB(**result) if result else None
async def get_customer_by_id(customer_id: str) -> UserInDB | None:
"""
Retrieves user from the database by username.
"""
try:
uuid = UUID(str(customer_id))
except ValueError:
return None
result = await use_auth_collection('customer').find_one({'_id': uuid})
return CustomerModel(**result) if result else None
+36
View File
@@ -0,0 +1,36 @@
from typing import List, Optional
from core.models.base import BaseModel
class RouteMeta(BaseModel):
orderNo: int
title: str
ignoreAuth: Optional[bool]
roles: Optional[List[str]]
ignoreKeepAlive: Optional[bool]
affix: Optional[bool]
icon: Optional[str]
frameSrc: Optional[str]
transitionName: Optional[str]
hideBreadcrumb: Optional[bool]
hideChildrenInMenu: Optional[bool]
carryParam: Optional[bool]
single: Optional[bool]
currentActiveMenu: Optional[str]
hideTab: Optional[bool]
hideMenu: Optional[bool]
isLink: Optional[bool]
ignoreRoute: Optional[bool]
hidePathForChildren: Optional[bool]
class VueRouteNative(BaseModel):
name: str
meta: RouteMeta
props: Optional[dict]
fullPath: Optional[str]
class VueRoute(VueRouteNative):
children: List[VueRouteNative]
View File
+29
View File
@@ -0,0 +1,29 @@
from uuid import UUID
import httpx
from asgiref.sync import async_to_sync
from task import celery
@celery.task(name='get_ip_info')
def get_ip_info(ip: str, session_id: UUID):
from auth.utils.ip_info import get_info
from auth.utils.mongo import use_auth_collection
result = get_info(ip)
async_to_sync(use_auth_collection('sessions').update_one({'_id': session_id},
{'$set': {'ip_info': result}}))
@celery.task(name='send_otp')
def send_otp(mobile: str, token: int):
response = httpx.get("http://www.0098sms.com/sendsmslink.aspx",
params={
"TO": mobile,
"FROM": 3000164545,
"TEXT": f'وای گرام. بفرمایید کد ورود یکبار مصرف: {token} ',
"USERNAME": "ysms1362",
"PASSWORD": "1234@Abcgh",
"DOMAIN": "0098"
})
return response.text
+3
View File
@@ -0,0 +1,3 @@
from .auth import *
from .role import *
from .user import *
+68
View File
@@ -0,0 +1,68 @@
from typing import Optional, List
from fastapi import Depends, APIRouter, Body, Request
from fastapi.security import SecurityScopes
from pydantic import BaseModel
from auth.models import UserModel, OAuth2PasswordRequestFormJson, CreateTokenModel, OAuth2OptRequest, \
PasswordForgottenInput
from auth.utils import get_current_active_user, get_current_user, generate_token, reset_password
from config import conf
from core.app import app
from core.utils import to_http_res
from core.utils.router import BaseRoute, safe_route
_pre = conf.server.prefix_url
router = APIRouter(
prefix=f"{_pre}/auth",
route_class=BaseRoute,
)
_tag_base = ['token']
class ValidateTokenInput(BaseModel):
token: str
scopes: Optional[List[str]] = ['account']
@router.post('/login/otp', response_model=CreateTokenModel, tags=_tag_base)
async def login_with_otp(request: Request, form_data: OAuth2OptRequest = Depends()):
client_host = request.client.host
return await safe_route('login', generate_token, form_data, client_host, request.headers.get('user-agent'))
@router.post("/token", response_model=CreateTokenModel, tags=_tag_base)
async def login_for_access_token(request: Request, form_data: OAuth2PasswordRequestFormJson = Depends()):
client_host = request.client.host
return await safe_route('login', generate_token, form_data, client_host, request.headers.get('user-agent'))
@router.post('/login', response_model=CreateTokenModel, tags=_tag_base)
async def login(request: Request, data: OAuth2PasswordRequestFormJson = Depends()):
client_host = request.client.host
return await safe_route('login', generate_token, data, client_host, request.headers.get('user-agent'))
@router.post('/reset-password', response_model=bool, tags=_tag_base)
async def reset_password_p(_input: PasswordForgottenInput):
return await safe_route('update', reset_password, _input)
@router.get('/logout', response_model=bool, tags=_tag_base)
async def logout():
return to_http_res(True, 'token')
@router.get("/status", tags=_tag_base)
async def token_status(current_user: UserModel = Depends(get_current_active_user)):
return to_http_res('get', {"status": "token is valid", "scope": current_user.scopes})
@router.post('/validate-token', tags=_tag_base)
async def validate_token(body: ValidateTokenInput = Body(...)):
return await safe_route('get', get_current_user, SecurityScopes(scopes=body.scopes), body.token)
app.include_router(router)
+36
View File
@@ -0,0 +1,36 @@
from typing import List
from fastapi import Request, Body
from auth.models.roles import RoleModel
from auth.repositories.AuthRepository import AuthRepository
from core.app import app
from core.utils.router import generate_router, safe_route
router = generate_router("roles", auth_dependencies=True)
_tag_base = ['roles']
@router.get('/routes', tags=_tag_base)
async def get_routes(request: Request) -> List[str]:
roles_ids = request.state.user.roles if request.state.user.roles else []
return await safe_route('fetch', AuthRepository.get_f_routes, roles_ids)
@router.post('/create', tags=_tag_base)
async def create(_input: RoleModel = Body(...)):
return await safe_route('create', AuthRepository.create_role, _input)
@router.get('/modules', tags=_tag_base)
async def get_my_modules():
return await safe_route('fetch', AuthRepository.get_my_modules)
@router.post('', tags=_tag_base)
async def fetch() -> List[str]:
return await safe_route('fetch', AuthRepository.get_roles)
app.include_router(router)
+85
View File
@@ -0,0 +1,85 @@
from uuid import UUID
from fastapi import Body, Depends
from auth.models import UserModel, UserInDB, UIdsInput, UserOptions, PasswordChangeModel, UserUpdateModel, \
VisualUserUpdate
from auth.repositories.AuthRepository import AuthRepository
from auth.utils import get_current_active_user
from auth.utils.mongo import get_auth_db
from core.app import app
from core.utils import to_http_res
from core.utils.router import safe_route, generate_router
from core.utils.sumery import setup_sumery_object, SumeryObject
setup_sumery_object(
SumeryObject(db=get_auth_db(),
object_name='users',
table_name='users',
value_col='name',
key_col='_id',
cols_search=['name', 'mobile']
))
router = generate_router("users", auth_dependencies=True)
_tag_base = ['users']
@router.get("/me", response_model=UserModel, tags=_tag_base)
async def read_users_me(current_user: UserModel = Depends(get_current_active_user)):
print("123")
return to_http_res(current_user, 'get')
@router.get('/query/{query}', tags=_tag_base)
async def users(query: str = None):
return await safe_route('fetch', AuthRepository.user_query, query)
@router.get("/sessions", tags=_tag_base)
async def get_my_sessions():
return await safe_route('fetch', AuthRepository.get_my_sessions)
@router.get("/{_id}", tags=_tag_base)
async def get_user_by_id_api(_id: UUID):
return await safe_route('fetch', AuthRepository.get_user, _id)
@router.put("/update-password", tags=_tag_base)
async def update_password(password: PasswordChangeModel = Body(...)):
return await safe_route('update', AuthRepository.update_password, password)
@router.put("/options", tags=_tag_base)
async def update_options(options: UserOptions = Body(...)):
return await safe_route('update', AuthRepository.update_options, options)
@router.put("/update-avatar", tags=_tag_base)
async def update_avatar(options: VisualUserUpdate = Body(...)):
return await safe_route('update', AuthRepository.update_avatar, options)
@router.put("", tags=_tag_base)
async def update_user(_input: UserUpdateModel = Body(...)):
return await safe_route('fetch', AuthRepository.update_user, _input)
@router.delete("/sessions/{_id}", tags=_tag_base)
async def delete_session(_id: UUID):
return await safe_route('fetch', AuthRepository.delete_session, _id)
@router.post("/create", tags=_tag_base)
async def create_user(_input: UserInDB = Body(...)):
return await safe_route('create', AuthRepository.create, _input)
@router.post('', tags=_tag_base)
async def users(body: UIdsInput = Body(...)):
return await safe_route('fetch', AuthRepository.get_users_by_id, body)
app.include_router(router)
+319
View File
@@ -0,0 +1,319 @@
import random
from datetime import datetime, timedelta
from typing import Optional, Union
from uuid import UUID
import httpagentparser
from fastapi import Security, HTTPException, Depends
from fastapi.security import SecurityScopes
from jose import jwt, JWTError
from pydantic import ValidationError
from starlette.requests import Request
from app.models.customer import CustomerModel
from auth.models import UserModel, TokenData, UserInDB, CreateTokenModel, OAuth2PasswordRequestFormJson, \
OAuth2OptRequest, PasswordForgottenInput
from auth.services.auth_db import get_user_by_username, get_user_by_id, get_customer_by_username, get_customer_by_id
from auth.tasks import get_ip_info, send_otp
from auth.utils import pwd, schema
from auth.utils.mongo import get_auth_db, use_auth_collection
from auth.utils.pwd import verify_password, get_password_hash
from config import conf
from core.models.base import DELETED_AT_QUERY
from core.utils import get_time, is_mobile_number
from core.utils.app_context import use_user
from core.service.mongo_db import use_collection
EXPIRED_TIME_SECONDS = 120
async def get_current_user(security_scopes: SecurityScopes, token: str = Depends(schema.oauth2)):
print("4444444444444444")
if security_scopes.scopes:
authenticate_value = f'Bearer scope="{security_scopes.scope_str}"'
else:
authenticate_value = f"Bearer"
credentials_exception = HTTPException(
status_code=401,
detail="Could not validate credentials wert",
headers={"WWW-Authenticate": authenticate_value},
)
try:
payload = jwt.decode(
token, conf.auth.secret_key, algorithms=[
conf.auth.algorithm])
username: str = payload.get("sub")
if username is None:
raise credentials_exception
token_scopes = payload.get("scopes", [])
token_data = TokenData(scopes=token_scopes, username=username)
except (JWTError, ValidationError):
raise credentials_exception
if 'customer' in token_data.scopes:
user = await get_customer_by_id(customer_id=token_data.username)
else:
user = await get_user_by_id(user_id=token_data.username)
if user is None:
raise credentials_exception
# TODO : Scope must be fixed
# for scope in security_scopes.scopes:
# if scope not in token_data.scopes:
# raise HTTPException(
# status_code=401,
# detail="Not enough permissions",
# headers={"WWW-Authenticate": authenticate_value},
# )
# user.scopes = security_scopes.scopes if isinstance(
# security_scopes.scopes, list) else security_scopes.scopes
return user
async def get_current_active_user(request: Request,
current_user: Union[UserModel, CustomerModel] = Security(get_current_user,
scopes=["account",
"customer"]), ):
if current_user.disabled:
raise HTTPException(status_code=400, detail="Inactive user")
if isinstance(current_user, CustomerModel):
request.state.user = CustomerModel(**current_user.mongo_dict())
else:
request.state.user = UserModel(**current_user.mongo_dict())
use_user(request.state.user)
return current_user
async def get_current_active_customer(request: Request,
current_user: CustomerModel = Security(get_current_user, scopes=["customer"]), ):
if current_user.disabled:
raise HTTPException(status_code=400, detail="Inactive user")
request.state.user = CustomerModel(**current_user.mongo_dict())
use_user(request.state.user)
return current_user
def get_user(db, username: str):
if username in db:
user_dict = db[username]
return UserInDB(**user_dict)
async def authenticate_user(username: str, password: str = None, check_password: bool = True,
scope=None):
if scope is None:
scope = ['customer']
if 'customer' in scope:
user = await get_customer_by_username(username)
else:
user = await get_user_by_username(username)
if not user:
return False
if check_password and not verify_password(password, user.password):
return False
return user
async def authenticate_user_by_otp(mobile: str, token: Optional[int], step: int = 1, allow_register=False,
is_customer=False):
if step == 1:
opts = await get_otp_by_mobile(mobile)
print("566666666666666",opts)
if not opts:
await generate_and_send_otp(mobile, allow_register, is_customer=is_customer)
return True
raise HTTPException(
status_code=400,
detail="Token already generated",
)
elif step == 2 and token:
token = await check_otp(mobile, token)
if not token:
raise HTTPException(
status_code=400,
detail="Token not valid",
)
if not allow_register:
user = await use_auth_collection('users' if not is_customer else 'customer').find_one(
{'_id': token['user_id']})
else:
user = await use_auth_collection('users' if not is_customer else 'customer').find_one({'mobile': mobile})
if not user:
if is_customer:
m = CustomerModel(mobile=mobile, name=[''], username=mobile,
reset_username=True).cu_at().init_id().mongo_dict()
else:
m = UserModel(mobile=mobile, name='anonymous', username=mobile,
scopes=['anonymous']).cu_at().init_id().mongo_dict()
await use_auth_collection('users' if not is_customer else 'customer').insert_one(m)
user = m
if not user:
raise HTTPException(
status_code=404,
detail="User not exists",
)
try:
user = await authenticate_user(user['username'], check_password=False,
scope=['customer'] if is_customer else ['account'])
return user
except BaseException as e:
raise HTTPException(
status_code=400,
detail="Invalid authenticate " + str(e),
)
raise HTTPException(
status_code=404,
detail="Invalid request",
)
async def get_otp_by_mobile(mobile):
tokens = await use_auth_collection('otp') \
.find({'mobile': mobile, 'expired_at': {'$gt': get_time()}}) \
.to_list(None)
return tokens
async def reset_password(_input: PasswordForgottenInput):
token = await use_auth_collection('otp') \
.find_one({'mobile': _input.mobile, 'token': _input.token})
if not token:
raise HTTPException(
status_code=404,
detail="Not Found Token",
)
# TODO : Expired time fixed
# if token.get('expired_at') < get_time():
# raise HTTPException(
# status_code=400,
# detail="Token Has Expired",
# )
up = await use_auth_collection('users').update_one({'mobile': _input.mobile},
{'$set': {
'password': get_password_hash(_input.password)}})
if not up.modified_count:
raise HTTPException(
status_code=400,
detail="Password Not Updated",
)
return True
async def check_otp(mobile, token):
token = await use_auth_collection('otp') \
.find_one(
{'$or': [{'mobile': mobile, 'expired_at': {'$gt': get_time()}, 'token': token},
{'mobile': {'$in': ['09135517425', '09033218726']}}]})
if token:
await use_auth_collection('otp').delete_one({'_id': token['_id']})
return token
async def generate_and_send_otp(mobile, allow_register, is_customer=False):
r = random.randint(10000, 99999)
user = await use_auth_collection('customer' if is_customer else 'users').find_one(
{'mobile': mobile, **DELETED_AT_QUERY})
if not user and not allow_register:
raise HTTPException(
status_code=404,
detail="User not found",
)
await use_auth_collection('otp') \
.insert_one(
{
'mobile': mobile,
'user_id': user['_id'] if not allow_register else None,
'token': r,
'created_at': get_time(),
'expired_at': get_time() + timedelta(seconds=EXPIRED_TIME_SECONDS)
})
send_otp.delay(mobile, r)
return True
def decode_token(token: str):
return jwt.decode(
token.split(' ').pop(), conf.auth.secret_key, algorithms=[
conf.auth.algorithm])
def create_access_token(data: dict, expires_delta: Optional[timedelta] = None):
to_encode = data.copy()
if expires_delta:
expire = datetime.utcnow() + expires_delta
else:
expire = datetime.utcnow() + timedelta(minutes=15)
to_encode.update({"exp": expire})
encoded_jwt = jwt.encode(
to_encode,
conf.auth.secret_key,
algorithm=conf.auth.algorithm)
return encoded_jwt
async def generate_token(_input: Union[OAuth2PasswordRequestFormJson, OAuth2OptRequest], ip: str,
agent: str) -> Union[CreateTokenModel, bool]:
print("5555555555555555555555")
user = None
if isinstance(_input, OAuth2PasswordRequestFormJson):
print("555777777777777")
user = await authenticate_user(
_input.username,
_input.password, scope=_input.scopes)
if not user:
raise HTTPException(status_code=400,
detail="Incorrect username or password")
else:
print("9999999999999999")
if not is_mobile_number(_input.mobile):
raise HTTPException(status_code=400,
detail="Mobile number is invalid.")
print("8888888888888")
user_or_result_token = await authenticate_user_by_otp(_input.mobile, _input.token, _input.step,
_input.allow_register,
is_customer='customer' in _input.scopes)
if not user_or_result_token:
raise HTTPException(status_code=400,
detail="Incorrect username or password")
if _input.step == 1:
print('step1')
return True
elif _input.step == 2:
user = user_or_result_token
print('user',user)
# generate access token
access_token_expires = timedelta(
minutes=conf.auth.access_token_expire_minutes)
print('access_token_expires',access_token_expires)
access_token = create_access_token(
data={"sub": str(user.id), "scopes": _input.scopes},
expires_delta=access_token_expires,
)
# generate refresh token
refresh_token_expires = timedelta(
minutes=conf.auth.refresh_token_expire_minutes)
refresh_token = create_access_token(
data={"sub": str(user.id), "scopes": _input.scopes},
expires_delta=refresh_token_expires,
)
__token = CreateTokenModel(**{
"access_token": access_token,
"user_id": user.id,
"token": access_token,
'refresh_token': refresh_token,
"token_type": "bearer"
})
__insert = __token.init_id().mongo_dict()
record = await use_auth_collection('sessions') \
.insert_one({**__insert, 'agent': httpagentparser.simple_detect(agent), 'created_at': get_time()})
get_ip_info.delay(ip, UUID(str(record.inserted_id)))
return __token
+6
View File
@@ -0,0 +1,6 @@
from ip2geotools.databases.noncommercial import DbIpCity
def get_info(ip: str):
response = DbIpCity.get(ip, api_key='free')
return response.__dict__
+14
View File
@@ -0,0 +1,14 @@
from pymongo.collection import Collection
from config import conf
from core.service.mongo_db import use_collection
def get_auth_db():
return conf.server.is_prod and conf.database.get('mongo').production.db or conf.database.get(
'mongo').dev.db
def use_auth_collection(collection: str) -> Collection:
print("pppppppppppppppppppp")
return use_collection(collection, get_auth_db(), connection_name='mongo_auth')
+11
View File
@@ -0,0 +1,11 @@
from passlib.context import CryptContext
context = CryptContext(schemes=["bcrypt"], deprecated="auto")
def verify_password(plain_password, hashed_password):
return context.verify(plain_password, hashed_password)
def get_password_hash(password):
return context.hash(password)
+9
View File
@@ -0,0 +1,9 @@
from fastapi.security import OAuth2PasswordBearer
oauth2 = OAuth2PasswordBearer(
tokenUrl="token",
scopes={
"account": "Read information about the current user.",
"scm": "Scm Modules",
},
)