diff --git a/.env b/.env index 081fedd..be25dfe 100644 --- a/.env +++ b/.env @@ -14,4 +14,10 @@ CORS_ALLOWED_HOSTS="http://localhost:8081,http://localhost:8002" TOTP_SECRET=12345678901234567890 TOTP_DIGEST=sha1 TOTP_RETURN_DIGITS=8 -TOTP_TIME_STEP=30 \ No newline at end of file +TOTP_TIME_STEP=30 +GOOGLE_OAUTH_ID= +GOOGLE_OAUTH_CLIENT= +GOOGLE_OAUTH_SECRET= +GOOGLE_OAUTH_REDIRECT_RESPONSE=https://127.0.0.1:8000/auth/google-response +GOOGLE_OAUTH_JS_ORIGINS="http://127.0.0.1:8000,http://localhost:8081" +GOOGLE_OAUTH_SCOPES="https://www.googleapis.com/auth/userinfo.email,https://www.googleapis.com/auth/userinfo.profile,openid" \ No newline at end of file diff --git a/.gitignore b/.gitignore index 523b8d5..22ab278 100755 --- a/.gitignore +++ b/.gitignore @@ -40,12 +40,19 @@ __pycache__/ *py.cover #Js -*node_modules +package-lock.json #VSCode *.vs *.vscode +#Expo +.expo + +#npm +*node_modules +dist/ + # Visual Studio *appsettings.Development.json *.vspscc diff --git a/README.md b/README.md index dbd1ac0..f3c6f4a 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,7 @@ It is a microservice for user administration and authenication, using JWT tokens ## Requirements - Python 3.12+ - FastApi 0.124+ +- Google account and activate [Google cloud](https://console.cloud.google.com) to obtain OAuth2 config values (Optional) > [!IMPORTANT] > It is necessary to complete the configuration file(.env), and create the PEM files and place them in the root folder @@ -40,7 +41,7 @@ openssl rsa -in private_key.pem -pubout -out public_key.pem ``` 5. Set configuration file (.env) -The microservice uses mongoDB as its database, so the connection string and other configurations (mongodb, JWT, CORS, logs) must be included +The microservice uses mongoDB as its database, so the connection string and other configurations (mongodb, JWT, CORS, logs, Google OAuth2) must be included 6. Run local development server ```bash @@ -67,4 +68,4 @@ docker run -p 8000:80 --env-file .env auth-service:latest ``` > [!NOTE] -> Since the project is used for learning, it does not strictly follow the concept of microservices, where each microservice should have its own realm of responsability +> Since the project is used for learning, it does not strictly follow the concept of microservices, where each microservice should have its own realm of responsability and use different approaches. diff --git a/requirements.txt b/requirements.txt index ea18575..9a32ff9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,13 +1,42 @@ -pip -fastapi -uvicorn[standard] -pymongo[srv] -log2mongo -python-dotenv -python-multipart -pyjwt +annotated-doc==0.0.4 +annotated-types==0.7.0 +anyio==4.12.0 bcrypt==4.0.1 -passlib[bcrypt] -cryptography -dependency-injector -setuptools \ No newline at end of file +certifi==2026.1.4 +cffi==2.0.0 +charset-normalizer==3.4.4 +click==8.3.1 +cryptography==46.0.3 +dependency-injector==4.48.3 +dnspython==2.8.0 +fastapi==0.127.0 +google-auth==2.48.0 +google-auth-oauthlib==1.2.4 +h11==0.16.0 +httptools==0.7.1 +idna==3.11 +log2mongo==0.1.0 +oauthlib==3.3.1 +passlib==1.7.4 +pyasn1==0.6.2 +pyasn1_modules==0.4.2 +pycparser==2.23 +pydantic==2.12.5 +pydantic_core==2.41.5 +PyJWT==2.10.1 +pymongo==4.15.5 +python-dotenv==1.2.1 +python-multipart==0.0.21 +PyYAML==6.0.3 +requests==2.32.5 +requests-oauthlib==2.0.0 +rsa==4.9.1 +setuptools==80.9.0 +starlette==0.50.0 +typing-inspection==0.4.2 +typing_extensions==4.15.0 +urllib3==2.6.3 +uvicorn==0.40.0 +uvloop==0.22.1 +watchfiles==1.1.1 +websockets==15.0.1 diff --git a/src/dependency_injection/containers.py b/src/dependency_injection/containers.py index e997339..419c71c 100644 --- a/src/dependency_injection/containers.py +++ b/src/dependency_injection/containers.py @@ -17,6 +17,7 @@ class Container(containers.DeclarativeContainer): "src.routers.users_router", "src.routers.admin.users_router", "src.routers.admin.security_router", + "src.routers.oauth2_router", "src.services.user_service", "src.services.login_service", "src.services.jwt_service", diff --git a/src/main.py b/src/main.py index a1e8c2e..808cb36 100644 --- a/src/main.py +++ b/src/main.py @@ -1,10 +1,11 @@ +from datetime import datetime from fastapi import FastAPI from contextlib import asynccontextmanager from fastapi.middleware.cors import CORSMiddleware from dotenv import load_dotenv import os -from src.routers import auth_router, products_router, users_router +from src.routers import auth_router, products_router, users_router, oauth2_router from src.routers.admin import users_router as admin_user_router, security_router from src.middlewares.jwt_middleware import JWTMiddleware from src.middlewares.http_middleware import HttpMiddleware @@ -42,8 +43,9 @@ async def shutdown(): app.include_router(products_router.router) app.include_router(admin_user_router.router) app.include_router(security_router.router) +app.include_router(oauth2_router.router) #Root route @app.get("/") async def main(): - return { "message": "Learning python" } + return datetime.now() diff --git a/src/models/address_model.py b/src/models/address_model.py index a5dd614..3509ca2 100644 --- a/src/models/address_model.py +++ b/src/models/address_model.py @@ -5,14 +5,16 @@ from src.models.pydantic_objects import PyObjectId -def set_id(value) -> ObjectId: +def set_id(value): if not value: return ObjectId() + if not isinstance(value, ObjectId): + return ObjectId(value) return value class Address(BaseModel): - id: Annotated[Optional[PyObjectId], PlainValidator(set_id), Field(validate_default=True)] = Field(default=None, validation_alias="_id") + id: Annotated[Optional[PyObjectId], PlainValidator(set_id), Field(validate_default=True, serialization_alias="_id")] = Field(default=None, validation_alias="_id") country: str state: str colony: str diff --git a/src/models/totp_model.py b/src/models/totp_model.py new file mode 100644 index 0000000..d0bbc14 --- /dev/null +++ b/src/models/totp_model.py @@ -0,0 +1,5 @@ +from enum import Enum + +class TOTPOptions(str, Enum): + ascii = "ascii" + hex = "hex" \ No newline at end of file diff --git a/src/models/user_model.py b/src/models/user_model.py index 2513958..d58f15d 100644 --- a/src/models/user_model.py +++ b/src/models/user_model.py @@ -27,6 +27,7 @@ class User(BaseModel): twofactor_enabled: bool = False roles: Optional[List[str]] = list() address: Optional[List[Address]] = list() + issuer: Optional[str] = None online: bool = False disabled: bool = False created_at: datetime = datetime.now() diff --git a/src/models/user_picture.py b/src/models/user_picture.py index e4d98ce..fa32358 100644 --- a/src/models/user_picture.py +++ b/src/models/user_picture.py @@ -9,6 +9,7 @@ class UserPicture(BaseModel): id: Optional[PyObjectId] = Field(default= None, serialization_alias="_id") content_type: str | None = None picture: Optional[bytes] = None + picture_url: Optional[str] = None created_at: datetime = datetime.now() updated_at: Optional[datetime] = None diff --git a/src/routers/admin/security_router.py b/src/routers/admin/security_router.py index 9bc1f25..b67ac68 100644 --- a/src/routers/admin/security_router.py +++ b/src/routers/admin/security_router.py @@ -4,6 +4,7 @@ from src.dependency_injection.containers import Container from src.middlewares.auth_roles_jwt import JWTCustom +from src.models.totp_model import TOTPOptions import src.services.totp_service as securitySvc oauth2_scheme = JWTCustom(tokenUrl="/auth/sign-in") @@ -13,10 +14,11 @@ prefix="/security") totp_dependency = Annotated[securitySvc.TOTP, Depends(Provide[Container.totp])] -@router.get("/2fa-now") +@router.get("/2fa-now/{options}") @inject -async def get_2f_code(totp: totp_dependency, secret: Optional[str] = None): - totp_code = await totp.now(secret) +async def get_2f_code(totp: totp_dependency, options: TOTPOptions, secret: Optional[str] = None): + is_ascii = True if options is TOTPOptions.ascii else False + totp_code = await totp.now(secret, is_ascii) if totp_code: return Response(content=totp_code, media_type="plain/text") else: diff --git a/src/routers/admin/users_router.py b/src/routers/admin/users_router.py index 1bf5b1e..2c78cd1 100644 --- a/src/routers/admin/users_router.py +++ b/src/routers/admin/users_router.py @@ -6,7 +6,7 @@ from src.models.user_model import User from src.models.address_model import Address -from src.services.user_service import change_password, insert_address +from src.services.user_service import change_password, get_address, insert_address from src.dependencies import get_db from src.middlewares.auth_roles_jwt import JWTCustom from src.dependency_injection.containers import Container @@ -22,14 +22,12 @@ @router.get("/user", response_model=User) @inject -async def get_user(email: str, db: db_dependency, log: log_dependency): +async def get_user(email: str, db: db_dependency, log: log_dependency, response: Response) -> User | None: try: - log.logger.info(f"get user: {email}") user = await uSvc.get_user(email, db) if user: - return Response(content=user.model_dump_json(), media_type="application/json") - else: - return Response(status_code=404) + return user + response.status_code = 404 except Exception as e: log.logger.error(e) @@ -37,22 +35,21 @@ async def get_user(email: str, db: db_dependency, log: log_dependency): @inject async def get_users(db: db_dependency, log: log_dependency, response: Response) -> list[User] | None: try: - log.logger.info(f"get users: {db.name}") users = await uSvc.get_users(db) - if users == None: - response.status_code = 404 - return users + if users: + return users + response.status_code = 404 except Exception as e: log.logger.error(e) @router.post("/users") @inject -async def create_user(model: User, db: db_dependency, log: log_dependency): +async def create_user(model: User, db: db_dependency, log: log_dependency, response: Response): try: user = await uSvc.create_user(db, model) - if user is not None: + if user: return user - + response.status_code = 400 except Exception as e: log.logger.error(e) @@ -62,45 +59,63 @@ async def update_user(model: User, db: db_dependency, log: log_dependency, respo try: result = await uSvc.update_user(db, model) if result: - response.status_code = 404 + response.status_code = 200 + return + response.status_code = 400 except Exception as e: log.logger.error(e) @router.delete("/users") @inject -async def delete_user(email: str, db: db_dependency, log: log_dependency): +async def delete_user(email: str, db: db_dependency, log: log_dependency, response: Response): try: - result = await uSvc.deleted_user(db, email) - if result: - return True - + if await uSvc.deleted_user(db, email): + response.status_code = 200 + return + response.status_code = 400 except Exception as e: log.logger.error(e) -@router.get("/change-password") +@router.post("/user/change-password") @inject async def update_password(email: str, password: str, db: db_dependency, log: log_dependency, response: Response): try: - result = await change_password(db, email, password) - if not result: - response.status_code = 400 + if await change_password(db, email, password): + response.status_code = 200 + return + response.status_code = 400 + except Exception as e: + log.logger.error(e) + +@router.post("/users/address") +@inject +async def create_address(email: str, address: Address, db: db_dependency, log: log_dependency, response: Response) -> Address | None: + try: + new_address = await insert_address(email, address, db) + if new_address: + return new_address + response.status_code = 400 except Exception as e: log.logger.error(e) -@router.post("/users/address", response_model_by_alias = False) +@router.get("/user/address") @inject -async def create_address(email: str, address: Address, db: db_dependency, log: log_dependency): +async def get_addresses(email: str, db: db_dependency, log: log_dependency, response : Response) -> list[Address] | None: try: - result = await insert_address(db, email, address) - return result + addresses = await get_address(db, email) + if addresses: + return addresses + response.status_code = 404 except Exception as e: log.logger.error(e) @router.put("/user/address") @inject -async def update_address(email: str, address: Address, db: db_dependency, log: log_dependency): +async def update_address(email: str, address: Address, db: db_dependency, log: log_dependency, response: Response): try: - result = await uSvc.update_address(db, email, address) - return result + if await uSvc.update_address(db, email, address): + response.status_code = 200 + return + response.status_code = 400 except Exception as e: log.logger.error(e) \ No newline at end of file diff --git a/src/routers/auth_router.py b/src/routers/auth_router.py index 6a584d9..1dfdd73 100644 --- a/src/routers/auth_router.py +++ b/src/routers/auth_router.py @@ -16,7 +16,7 @@ router = APIRouter( tags=["auth"], prefix="/auth" - ) +) oauth2_scheme = JWTCustom(tokenUrl="/auth/sign-in") db_dependency = Annotated[MongoAsyncService, Depends(Provide[Container.database_client])] log_dependency = Annotated[log2mongo, Depends(Provide[Container.logging])] @@ -29,8 +29,14 @@ async def sign_up(model: SignUp, db: db_dependency, log: log_dependency, request log.logger.info(f"{ model.email } sign-up from ip: { client_host }") content = "" status_code = 0 - user = await create_user(db.database, User(name= model.name, email= model.email, password= model.password, disabled=False)) - if user is not None: + user = await create_user(db.database, User( + name = model.name, + email = model.email, + password = model.password, + issuer = '', + disabled = False) + ) + if user: content = user.model_dump_json() status_code = status.HTTP_200_OK else: @@ -51,7 +57,7 @@ async def sign_in(form_data: Annotated[OAuth2PasswordRequestForm, Depends()], db content = None status_code = status.HTTP_401_UNAUTHORIZED result, token = await login(form_data.username, form_data.password, db.database) - if token is not None: + if token: content = token.model_dump() status_code = status.HTTP_200_OK elif result and token is None: @@ -67,9 +73,9 @@ async def sign_in(form_data: Annotated[OAuth2PasswordRequestForm, Depends()], db async def validate_token(log: log_dependency, email: Annotated[str, Depends(oauth2_scheme)]): try: status_code = status.HTTP_401_UNAUTHORIZED - if email is not None: + if email: status_code = status.HTTP_200_OK except Exception as e: log.logger.error(e) finally: - return Response(email, status_code, media_type="text/plain") \ No newline at end of file + return Response(email, status_code, media_type = "text/plain") diff --git a/src/routers/oauth2_router.py b/src/routers/oauth2_router.py new file mode 100644 index 0000000..5c011b7 --- /dev/null +++ b/src/routers/oauth2_router.py @@ -0,0 +1,86 @@ +from typing import Annotated +from fastapi import APIRouter, Depends, HTTPException, Request, Response, status +from fastapi.responses import JSONResponse, RedirectResponse +from dependency_injector.wiring import Provide, inject +from log2mongo import log2mongo + +from src.middlewares.auth_jwt import JWTCustom +from src.models.user_model import User +from src.services.jwt_service import get_token_claims_unverify_signature +from src.services.mongodb_service import MongoAsyncService +from src.dependency_injection.containers import Container +from src.services.login_service import external_login +from src.services.oauth_google_service import get_auth_url, get_auth_response +from src.services.user_service import add_user_picture, create_user, get_user + +router = APIRouter( + tags=["OAuth2"], + prefix="/oauth" +) +oauth2_scheme = JWTCustom(tokenUrl="/auth/sign-in") +db_dependency = Annotated[MongoAsyncService, Depends(Provide[Container.database_client])] +log_dependency = Annotated[log2mongo, Depends(Provide[Container.logging])] + +@router.get("/google-url") +@inject +async def get_google_url(db: db_dependency, log: log_dependency, request: Request): + try: + log.logger.info(f" sign-up from ip: { request.client.host }") # type: ignore + content = "" + status_code = 0 + auth_url = await get_auth_url() + if auth_url: + content = auth_url + status_code = status.HTTP_307_TEMPORARY_REDIRECT + else: + content = "Unable to create user" + status_code = status.HTTP_400_BAD_REQUEST + + except Exception as e: + log.logger.error(e) + finally: + if status_code == status.HTTP_307_TEMPORARY_REDIRECT: + return RedirectResponse(content) + else: + return Response(content, status_code) + +@router.get("/google-response") +@inject +async def get_google_response(db: db_dependency, log: log_dependency, request: Request): + try: + log.logger.info(f"Google sign-up from ip: { request.client.host }") # type: ignore + content = "" + status_code = status.HTTP_401_UNAUTHORIZED + google_token = await get_auth_response(request.url.__str__()) + + if google_token: + token_data = await get_token_claims_unverify_signature(google_token.id_token.__str__()) # type: ignore + user = await get_user(token_data['email'], db.get_db()) + + if user is None: + user = await create_user(db.database, User( + name = token_data['given_name'], + last_name = token_data['family_name'], + email= token_data['email'], + email_verified = True, + password= token_data['at_hash'], + issuer = token_data['iss'], + disabled=False)) + + if user: + result = await add_user_picture(token_data['email'], db.get_db(), pic_url = token_data["picture"]) + + if user: + result, token = await external_login(user.email, token_data['iss'], db.database) + + if token: + content = token.model_dump() + status_code = status.HTTP_200_OK + + except Exception as e: + log.logger.error(e) + finally: + if status_code == status.HTTP_200_OK: + return JSONResponse(content, status_code) + else: + return Response(status_code = status_code) diff --git a/src/routers/users_router.py b/src/routers/users_router.py index 05dc4b0..e076182 100644 --- a/src/routers/users_router.py +++ b/src/routers/users_router.py @@ -9,33 +9,38 @@ from src.models.address_model import Address from src.dependency_injection.containers import Container from src.services.mongodb_service import MongoAsyncService -from src.services.user_service import change_password, insert_address +from src.services.user_service import change_password, insert_address, get_address from src.services.jwt_service import verify_token_from_requests from src.services.totp_service import TOTP import src.services.user_service as uSvc from src.dependencies import get_db oauth2_scheme = JWTCustom(tokenUrl="/auth/sign-in") -router = APIRouter(tags=["users"], dependencies=[Depends(oauth2_scheme)]) +router = APIRouter( + tags=["user"], + dependencies=[Depends(oauth2_scheme)]) #db_dependency = Annotated[AsyncDatabase, Depends(get_db)] db_dependency = Annotated[MongoAsyncService, Depends(Provide[Container.database_client])] totp_dependency = Annotated[TOTP, Depends(Provide[Container.totp])] # Route to add an users -@router.get("/user", response_model_by_alias = False) +@router.get("/user") @inject -async def get_user(db: db_dependency, email: Annotated[str, Depends(verify_token_from_requests)]) -> User | None: +async def get_user(db: db_dependency, email: Annotated[str, Depends(verify_token_from_requests)]): user = await uSvc.get_user(email, db.get_db()) - return user + if user: + return user + else: + return Response(status_code = status.HTTP_404_NOT_FOUND) @router.post("/user/img", response_model_by_alias = False) @inject async def add_user_image(file: UploadFile, db: db_dependency, email: Annotated[str, Depends(verify_token_from_requests)]): - result = await uSvc.add_user_picture(file, file.content_type, email, db.get_db()) + result = await uSvc.add_user_picture(email, db.get_db(), file = file, content_type = file.content_type) if result: - return JSONResponse("", status.HTTP_200_OK) + return Response(status_code= status.HTTP_200_OK) else: - return JSONResponse(None, status.HTTP_400_BAD_REQUEST) + return Response(status_code= status.HTTP_400_BAD_REQUEST) @router.get("/user/img", response_model_by_alias = False) @inject @@ -44,19 +49,24 @@ async def get_user_image(db: db_dependency, email: Annotated[str, Depends(verify if result and result.picture: return StreamingResponse(io.BytesIO(result.picture), media_type= result.content_type) else: - return JSONResponse("", status.HTTP_400_BAD_REQUEST) + return Response(status_code= status.HTTP_400_BAD_REQUEST) @router.put("/user") @inject async def update_user(db: db_dependency, model: User, email: Annotated[str, Depends(oauth2_scheme)]): - result = await uSvc.update_user(db.get_db(), model) - return result + user = await uSvc.update_user(db.get_db(), model) + if user: + return user + else: + return Response(status_code=404) -@router.get("/user/change-password") +@router.post("/user/change-password") @inject async def update_password(db: db_dependency, password: str, email: Annotated[str, Depends(oauth2_scheme)]): - result = await change_password(db.get_db(), email, password) - return result + if await change_password(db.get_db(), email, password): + return Response(status_code=status.HTTP_200_OK) + else: + return Response(status_code=status.HTTP_400_BAD_REQUEST) @router.get("/user/2fa") @inject @@ -65,27 +75,41 @@ async def get_2f_code(email: Annotated[str, Depends(oauth2_scheme)], totp: totp_ if totp_code: return Response(content=totp_code, media_type="plain/text") else: - return Response(status_code=404) + return Response(status_code = status.HTTP_404_NOT_FOUND) @router.get("/user/2fa-verify") @inject -async def get_2f_code_verify(code: str, email: Annotated[str, Depends(oauth2_scheme)], totp: totp_dependency): +async def verify_2f_code(code: str, email: Annotated[str, Depends(oauth2_scheme)], totp: totp_dependency): if await totp.verify(code, email, is_value_ascii = True): - return Response(status_code=200) + return Response(status_code = status.HTTP_200_OK) else: - return Response(status_code=401) + return Response(status_code = status.HTTP_401_UNAUTHORIZED) @router.post("/user/address") @inject -async def create_address(db: db_dependency, address: Address, email: Annotated[str, Depends(oauth2_scheme)]): - result = await insert_address(db.get_db(), email, address) - return result +async def create_address(db: db_dependency, model: Address, email: Annotated[str, Depends(oauth2_scheme)]): + address = await insert_address(email, model, db.get_db()) + if address: + return address + else: + return Response(status_code=status.HTTP_400_BAD_REQUEST) + +@router.get("/user/address") +@inject +async def get_addresses(db: db_dependency, email: Annotated[str, Depends(oauth2_scheme)]): + addresses = await get_address(db.get_db(), email) + if addresses: + return addresses + else: + return Response(status_code=404) @router.put("/user/address") @inject async def update_address(db: db_dependency, address: Address, email: Annotated[str, Depends(oauth2_scheme)]): - result = await uSvc.update_address(db.get_db(), email, address) - return result + if await uSvc.update_address(db.get_db(), email, address): + return Response(status_code=status.HTTP_200_OK) + else: + return Response(status_code=status.HTTP_400_BAD_REQUEST) @router.post("/user/change-status") @inject diff --git a/src/services/jwt_service.py b/src/services/jwt_service.py index bcecba4..dd0080f 100644 --- a/src/services/jwt_service.py +++ b/src/services/jwt_service.py @@ -91,6 +91,15 @@ async def get_email(token: str, crypto = crypto_service, log = log_service): try: payload = jwt.decode(token, str(os.environ["JWT_SECRET_KEY"]), os.environ["JWT_ALGORITHM"]) return await crypto.decrypt_text(payload.get("sub")) + except Exception as e: + log.logger.error(e) + raise e + +@inject +async def get_token_claims_unverify_signature(token: str, crypto = crypto_service, log = log_service): + try: + payload = jwt.decode(token, options = {"verify_signature" : False}) + return payload except Exception as e: log.logger.error(e) raise e \ No newline at end of file diff --git a/src/services/login_service.py b/src/services/login_service.py index 6fc9661..1b6abbb 100644 --- a/src/services/login_service.py +++ b/src/services/login_service.py @@ -24,3 +24,15 @@ async def login(username: str, password: str, db, crypto = crypto_service, log = except Exception as e: log.logger.error(e) return False, None + +@inject +async def external_login(username: str, issuer: str, db, crypto = crypto_service, log = logger): + try: + user = await get_user(username, db) + if user: + if user.issuer == issuer: + token = await create_token({ "sub": user.email, "name": user.name, "roles": user.roles }) + return True, Token(access_token = token, token_type = "bearer") + except Exception as e: + log.logger.error(e) + return False, None diff --git a/src/services/oauth_google_service.py b/src/services/oauth_google_service.py new file mode 100644 index 0000000..00508e0 --- /dev/null +++ b/src/services/oauth_google_service.py @@ -0,0 +1,47 @@ +import os +import google.oauth2.credentials +from log2mongo import log2mongo +import google_auth_oauthlib.flow +from dependency_injector.wiring import Provide, inject + +from src.dependency_injection.containers import Container + +log_service: log2mongo = Provide[Container.logging] + +@inject +async def get_auth_url(log = log_service): + try: + flow = google_auth_oauthlib.flow.Flow.from_client_config( + {"web":{"client_id":os.environ["GOOGLE_OAUTH_CLIENT"],"project_id":os.environ["GOOGLE_OAUTH_ID"],"auth_uri":"https://accounts.google.com/o/oauth2/auth","token_uri":"https://oauth2.googleapis.com/token","auth_provider_x509_cert_url":"https://www.googleapis.com/oauth2/v1/certs","client_secret":os.environ["GOOGLE_OAUTH_SECRET"],"javascript_origins":os.environ["GOOGLE_OAUTH_JS_ORIGINS"].split(',') if os.environ["GOOGLE_OAUTH_JS_ORIGINS"] else []}}, + scopes = os.environ["GOOGLE_OAUTH_SCOPES"].split(',') if os.environ["GOOGLE_OAUTH_SCOPES"] else [] + ) + flow.redirect_uri = os.environ["GOOGLE_OAUTH_REDIRECT_RESPONSE"] + + auth_url , state = flow.authorization_url( + acces_type = 'offline', + include_grand_scopes = 'true', + prompt = 'consent' + ) + + return auth_url + + except Exception as e: + log.logger.error(e) + +@inject +async def get_auth_response(url: str, log = log_service): + try: + credentials = None + flow = google_auth_oauthlib.flow.Flow.from_client_config( + {"web":{"client_id":os.environ["GOOGLE_OAUTH_CLIENT"],"project_id":os.environ["GOOGLE_OAUTH_ID"],"auth_uri":"https://accounts.google.com/o/oauth2/auth","token_uri":"https://oauth2.googleapis.com/token","auth_provider_x509_cert_url":"https://www.googleapis.com/oauth2/v1/certs","client_secret":os.environ["GOOGLE_OAUTH_SECRET"],"javascript_origins":os.environ["GOOGLE_OAUTH_JS_ORIGINS"].split(',') if os.environ["GOOGLE_OAUTH_JS_ORIGINS"] else []}}, + scopes = os.environ["GOOGLE_OAUTH_SCOPES"].split(',') if os.environ["GOOGLE_OAUTH_SCOPES"] else [] + ) + flow.redirect_uri = os.environ["GOOGLE_OAUTH_REDIRECT_RESPONSE"] + + auth_reponse = flow.fetch_token(authorization_response = url) + credentials = flow.credentials + + except Exception as e: + log.logger.error(e) + finally: + return credentials \ No newline at end of file diff --git a/src/services/otp_service.py b/src/services/otp_service.py index 7a4b277..5171fe7 100644 --- a/src/services/otp_service.py +++ b/src/services/otp_service.py @@ -10,7 +10,7 @@ def get_hmac_sha(self, secret: bytes, moving_factor: bytes, digest: str): def to_bytes(self, value: str, is_value_ascii: bool = False, is_value_hex: bool = False): if is_value_ascii: - return value.encode('ascii') + return value.encode() elif is_value_hex: return bytes(bytearray.fromhex(value)) else: diff --git a/src/services/user_service.py b/src/services/user_service.py index 5ed01a2..b1a3629 100644 --- a/src/services/user_service.py +++ b/src/services/user_service.py @@ -55,30 +55,49 @@ async def get_users(db: AsyncDatabase, log = log_service) -> list[User] | None: return users @inject -async def add_user_picture(file: UploadFile, content_type: str | None, email: str, db: AsyncDatabase, log = log_service) -> bool: +async def add_user_picture(email: str, db: AsyncDatabase, file: UploadFile | None = None, pic_url: str | None = None, content_type: str | None = None, log = log_service) -> bool: try: result = False user_bd = await db[users_collection].find_one({'email': email}) - if user_bd is not None: + + if user_bd: + user_picture = await db[users_pics_collection].find_one({'_id': user_bd["_id"]}) user_pic = UserPicture( - id = user_bd["_id"], - content_type = content_type + id = user_bd["_id"] ) - img = await file.read() - user_picture = await db[users_pics_collection].find_one({'_id': user_bd["_id"]}) - - if user_picture is not None: - query_filter = {"_id": user_bd["_id"]} - update_op = {"$set" : {"picture" : Binary(img), "content_type": content_type }} - op_result = await db[users_pics_collection].update_one(query_filter, update_op) - - if op_result.modified_count > 0: - result = True - else: - user_pic.picture = Binary(img) - op_result = await db[users_pics_collection].insert_one(user_pic.model_dump(by_alias=True)) - if op_result.inserted_id is not None: - result = True + + if file: + content_type = content_type + img = await file.read() + + if user_picture is not None: + query_filter = {"_id": user_bd["_id"]} + update_op = {"$set" : {"picture" : Binary(img), "content_type": content_type, "picture_url": "" }} + op_result = await db[users_pics_collection].update_one(query_filter, update_op) + + if op_result.modified_count > 0: + result = True + else: + user_pic.picture = Binary(img) + op_result = await db[users_pics_collection].insert_one(user_pic.model_dump(by_alias=True)) + + if op_result.inserted_id is not None: + result = True + elif pic_url: + if user_picture is not None: + query_filter = {"_id": user_bd["_id"]} + update_op = {"$set" : {"picture_url" : pic_url, "content_type": 'text/plain', "picture": None }} + op_result = await db[users_pics_collection].update_one(query_filter, update_op) + + if op_result.modified_count > 0: + result = True + else: + user_pic.picture_url = pic_url + op_result = await db[users_pics_collection].insert_one(user_pic.model_dump(by_alias=True)) + + if op_result.inserted_id is not None: + result = True + except Exception as e: log.logger.error(e) finally: @@ -187,9 +206,9 @@ async def change_password(db: AsyncDatabase, email: str, new_password: str, cryp return result @inject -async def insert_address(db: AsyncDatabase, email: str, address: Address, log = log_service) -> bool: +async def insert_address(email: str, address: Address, db: AsyncDatabase, log = log_service) -> Address | None: try: - result = False + result = None user_db = await db[users_collection].find_one({'email': email}) if user_db is not None: @@ -201,37 +220,42 @@ async def insert_address(db: AsyncDatabase, email: str, address: Address, log = updated_result = await db[users_collection].update_one(query_filter, update_op) if updated_result.modified_count > 0: - result = True + result = address except Exception as e: log.logger.error(e) finally: return result +@inject +async def get_address(db: AsyncDatabase, email: str, log = log_service) -> list[Address] | None: + try: + addresses = None + query = {'email': email} + projection = {"address": 1, "_id": 0} + result = await db[users_collection].find_one(query, projection) + addresses = result["address"] # type: ignore + + except Exception as e: + log.logger.error(e) + finally: + return addresses + @inject async def update_address(db: AsyncDatabase, email: str, address: Address, log = log_service) -> bool: try: result = False - user_db = await db[users_collection].find_one( - {"email": email }, - {"address": { "$elemMatch": { "_id": ObjectId(address.id) } } }) - - if user_db is not None: - user_db = user_db["address"] - m = address.model_dump(exclude={"id"}) - update_fields= {} - for key, value in m: - if key in user_db and user_db[key] != value: - update_fields[key]= value - elif key not in user_db: - update_fields[key]= value - - query_filter = {"email": email, "address.id": address.id } - update_op = {"$set" : {"address" : address.model_dump() }} - updated_result = await db[users_collection].update_one(query_filter, update_op) - - if updated_result.modified_count > 0: - result = True + updated_address = address.model_dump() + id = updated_address.pop("id") + updated_address.update({ "_id": id }) + + query_filter = {"email": email, "address._id": address.id } + update_op = {"$set" : {"address.$[elem]" : address.model_dump() }} + array_filter = [{"elem._id": address.id}] + updated_result = await db[users_collection].update_one(query_filter, update_op, array_filters = array_filter) + + if updated_result.matched_count > 0 or updated_result.modified_count > 0: + result = True except Exception as e: log.logger.error(e)