import datetime import json import typing from typing import Union import arrow from fastapi import status from fastapi.requests import Request from fastapi.exceptions import HTTPException from fastapi.responses import JSONResponse from databases import ( Companies, Staff, Duties, Departments, Employees, BuildLivingSpace, BuildParts, Build, Duty, Event2Occupant, Event2Employee, Users, UsersTokens, OccupantTypes, RelationshipEmployee2Build, ) from api_services import ( redis_cli, send_email, get_object_via_access_key, get_object_via_user_uu_id, save_access_token_to_redis, update_selected_to_redis, password_is_changed_template, change_your_password_template, ) from api_configs import ApiStatic, Auth from api_events.events.abstract_class import MethodToEvent, ActionsSchema from api_objects import ( OccupantToken, CompanyToken, EmployeeTokenObject, OccupantTokenObject, ) from api_library.date_time_actions.date_functions import system_arrow from databases.no_sql_models.login_handlers import load_user_with_erp_details from api_validations.validations_request import ( Login, Logout, ChangePassword, Remember, Forgot, CreatePassword, OccupantSelection, EmployeeSelection, ) class AuthenticationLoginEventMethods(MethodToEvent): event_type = "LOGIN" event_description = "Login via domain and access key : [email] | [phone]" event_category = "AUTHENTICATION" __event_keys__ = { "e672846d-cc45-4d97-85d5-6f96747fac67": "authentication_login_with_domain_and_creds", } __event_validation__ = { "e672846d-cc45-4d97-85d5-6f96747fac67": "authentication_login_with_domain_and_creds", } @classmethod def authentication_login_with_domain_and_creds( cls, data: Login, request, ): access_dict = Users.login_user_with_credentials(data=data, request=request) found_user = access_dict.get("user", None) if not found_user: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid credentials" ) return JSONResponse( content={ "completed": True, "message": "User is logged in successfully", "access_token": access_dict.get("access_token"), "refresh_token": access_dict.get("refresher_token"), "access_object": access_dict.get("access_object"), "user": found_user.get_dict(), }, status_code=status.HTTP_200_OK, ) class AuthenticationSelectEventMethods(MethodToEvent): event_type = "LOGIN" event_description = "Select Employee Duty or Occupant Type" event_category = "AUTHENTICATION" __event_keys__ = { "cee96b9b-8487-4e9f-aaed-2e8c79687bf9": "authentication_select_company_or_occupant_type", } __event_validation__ = { "cee96b9b-8487-4e9f-aaed-2e8c79687bf9": "authentication_select_company_or_occupant_type", } @classmethod def authentication_select_company_or_occupant_type( cls, request: Request, data: Union[EmployeeSelection, OccupantSelection], token_dict: Union[EmployeeTokenObject, OccupantTokenObject], ): from api_objects import OccupantToken, CompanyToken if isinstance(token_dict, EmployeeTokenObject): if data.company_uu_id not in token_dict.companies_uu_id_list: return JSONResponse( content={ "completed": False, "message": "Company is not found in users company list", }, status_code=status.HTTP_401_UNAUTHORIZED, ) if selected_company := Companies.filter_one( Companies.uu_id == data.company_uu_id, ).data: department_ids = [ department.id for department in Departments.filter_all( Departments.company_id == selected_company.id, ).data ] duties_ids = [ duties.id for duties in Duties.filter_all( Duties.company_id == selected_company.id, ).data ] staff_ids = [ staff.id for staff in Staff.filter_all( Staff.duties_id.in_(duties_ids), ).data ] employee = Employees.filter_one( Employees.people_id == token_dict.person_id, Employees.staff_id.in_(staff_ids), ).data reachable_event_list_id = Event2Employee.get_event_id_by_employee_id( employee_id=employee.id ) staff = Staff.filter_one( Staff.id == employee.staff_id, ).data duties = Duties.filter_one( Duties.id == staff.duties_id, ).data department = Departments.filter_one( Departments.id == duties.department_id, ).data bulk_id = Duty.filter_by_one(system=True, duty_code="BULK").data bulk_duty_id = Duties.filter_by_one( company_id=selected_company.id, duties_id=bulk_id.id, **Duties.valid_record_dict, ).data update_selected_to_redis( request=request, add_payload=CompanyToken( company_uu_id=selected_company.uu_id.__str__(), company_id=selected_company.id, department_id=department.id, department_uu_id=department.uu_id.__str__(), duty_id=duties.id, duty_uu_id=duties.uu_id.__str__(), bulk_duties_id=bulk_duty_id.id, staff_id=staff.id, staff_uu_id=staff.uu_id.__str__(), employee_id=employee.id, employee_uu_id=employee.uu_id.__str__(), reachable_event_list_id=reachable_event_list_id, ), ) return JSONResponse( content={ "completed": True, "message": "Company is selected successfully", }, status_code=status.HTTP_200_OK, ) elif isinstance(token_dict, OccupantTokenObject): occupant_type = OccupantTypes.filter_by_one( system=True, uu_id=data.occupant_uu_id ).data if not occupant_type: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Occupant Type is not found", ) build_part = BuildParts.filter_by_one( system=True, uu_id=data.build_part_uu_id ).data if not build_part: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Build Part is not found", ) build = Build.filter_one( Build.id == build_part.build_id, ).data related_company = RelationshipEmployee2Build.filter_one( RelationshipEmployee2Build.member_id == build.id, ).data company_related = Companies.filter_one( Companies.id == related_company.company_id, ).data responsible_employee = Employees.filter_one( Employees.id == related_company.employee_id, ).data if selected_occupant_type := BuildLivingSpace.filter_one( BuildLivingSpace.occupant_type == occupant_type.id, BuildLivingSpace.person_id == token_dict.person_id, BuildLivingSpace.build_parts_id == build_part.id, ).data: reachable_event_list_id = ( Event2Occupant.get_event_id_by_build_living_space_id( build_living_space_id=selected_occupant_type.id ) ) update_selected_to_redis( request=request, add_payload=OccupantToken( living_space_id=selected_occupant_type.id, living_space_uu_id=selected_occupant_type.uu_id.__str__(), occupant_type_id=occupant_type.id, occupant_type_uu_id=occupant_type.uu_id.__str__(), occupant_type=occupant_type.occupant_type, build_id=build.id, build_uuid=build.uu_id.__str__(), build_part_id=build_part.id, build_part_uuid=build_part.uu_id.__str__(), responsible_employee_id=responsible_employee.id, responsible_employee_uuid=responsible_employee.uu_id.__str__(), responsible_company_id=company_related.id, responsible_company_uuid=company_related.uu_id.__str__(), reachable_event_list_id=reachable_event_list_id, ), ) return JSONResponse( content={ "completed": True, "message": "Occupant is selected successfully", }, status_code=status.HTTP_200_OK, ) return JSONResponse( content={"completed": False, "message": "Invalid data provided"}, status_code=status.HTTP_418_IM_A_TEAPOT, ) class AuthenticationCheckTokenEventMethods(MethodToEvent): event_type = "LOGIN" event_description = "Check Token is valid for user" event_category = "AUTHENTICATION" __event_keys__ = { "73d77e45-a33f-4f12-909e-3b56f00d8a12": "authentication_check_token_is_valid", } __event_validation__ = { "73d77e45-a33f-4f12-909e-3b56f00d8a12": "authentication_check_token_is_valid", } @classmethod def authentication_check_token_is_valid( cls, request, ): if get_object_via_access_key(request=request): return JSONResponse( content={"completed": True, "message": "Access Token is valid"}, status_code=status.HTTP_200_OK, ) return JSONResponse( content={"completed": False, "message": "Access Token is NOT valid"}, status_code=status.HTTP_401_UNAUTHORIZED, ) class AuthenticationRefreshEventMethods(MethodToEvent): event_type = "LOGIN" event_description = ( "Refresher Token for refreshing access token without credentials" ) event_category = "AUTHENTICATION" __event_keys__ = { "48379bb2-ba81-4d8e-a9dd-58837cfcbf67": "authentication_refresh_user_info", } __event_validation__ = { "48379bb2-ba81-4d8e-a9dd-58837cfcbf67": "authentication_refresh_user_info", } @classmethod def authentication_refresh_user_info( cls, request, token_dict: typing.Union[EmployeeSelection, OccupantSelection], ): access_token = str(request.headers.get(Auth.ACCESS_TOKEN_TAG)) if token_user := get_object_via_access_key(request=request): if found_user := Users.filter_one( Users.uu_id == token_user.get("uu_id") ).data: user_token = UsersTokens.filter_one( UsersTokens.domain == found_user.domain_name, UsersTokens.user_id == found_user.id, UsersTokens.token_type == "RememberMe", ).data access_dict = { "access_token": access_token, "refresh_token": getattr(user_token, "token", None), } return JSONResponse( content={ "completed": True, "message": "User is logged in successfully via refresh token", "data": load_user_with_erp_details(found_user, access_dict), }, status_code=status.HTTP_200_OK, ) return JSONResponse( content={"completed": False, "message": "Invalid data", "data": {}}, status_code=status.HTTP_401_UNAUTHORIZED, ) class AuthenticationChangePasswordEventMethods(MethodToEvent): event_type = "LOGIN" event_description = "Change password with access token implemented on request headers without password reset token" event_category = "AUTHENTICATION" __event_keys__ = { "f09f7c1a-bee6-4e32-8444-962ec8f39091": "authentication_change_password", } __event_validation__ = { "f09f7c1a-bee6-4e32-8444-962ec8f39091": "authentication_change_password", } @classmethod def authentication_change_password( cls, data: ChangePassword, token_dict: typing.Union[EmployeeTokenObject, OccupantTokenObject], ): if isinstance(token_dict, EmployeeTokenObject): if found_user := Users.filter_one(Users.uu_id == token_dict.uu_id).data: if found_user.check_password(data.old_password): found_user.set_password(data.new_password) return JSONResponse( content={ "completed": True, "message": "Password is changed successfully", }, status_code=status.HTTP_200_OK, ) return JSONResponse( content={ "completed": False, "message": "Old password is not correct", }, status_code=status.HTTP_401_UNAUTHORIZED, ) return JSONResponse( content={"completed": False, "message": "Invalid data"}, status_code=status.HTTP_401_UNAUTHORIZED, ) class AuthenticationCreatePasswordEventMethods(MethodToEvent): event_type = "LOGIN" event_description = "Create password with password reset token requested via email" event_category = "AUTHENTICATION" __event_keys__ = { "c519f9af-92e1-47b2-abf7-5a3316d075f7": "authentication_create_password", } __event_validation__ = { "c519f9af-92e1-47b2-abf7-5a3316d075f7": "authentication_create_password", } @classmethod def authentication_create_password(cls, data: CreatePassword): if not data.re_password == data.password: raise HTTPException( status_code=status.HTTP_406_NOT_ACCEPTABLE, detail="Password must match" ) if found_user := Users.filter_one( Users.password_token == data.password_token ).data: found_user.create_password(found_user=found_user, password=data.password) found_user.password_token = "" found_user.save() send_email_completed = send_email( subject=f"Dear {found_user.user_tag}, your password has been changed.", receivers=[str(found_user.email)], html=password_is_changed_template(user_name=found_user.user_tag), ) if not send_email_completed: raise HTTPException( status_code=400, detail="Email can not be sent. Try again later" ) return JSONResponse( content={ "completed": True, "message": "Password is created successfully", "data": found_user.get_dict(), }, status_code=status.HTTP_200_OK, ) return JSONResponse( content={ "completed": False, "message": "Record not found", "data": {}, }, status_code=status.HTTP_202_ACCEPTED, ) class AuthenticationDisconnectUserEventMethods(MethodToEvent): event_type = "LOGIN" event_description = "Disconnect all sessions of user in access token" event_category = "AUTHENTICATION" __event_keys__ = { "8b586848-2fb3-4161-abbe-642157eec7ce": "authentication_disconnect_user", } __event_validation__ = { "8b586848-2fb3-4161-abbe-642157eec7ce": "authentication_disconnect_user", } @classmethod def authentication_disconnect_user( cls, data: Logout, token_dict: Union[EmployeeTokenObject, OccupantTokenObject] ): found_user = Users.filter_one(Users.uu_id == token_dict.user_uu_id).data if not found_user: return JSONResponse( content={ "completed": False, "message": "Invalid data", "data": None, }, status_code=status.HTTP_202_ACCEPTED, ) if already_tokens := get_object_via_user_uu_id(user_id=str(found_user.uu_id)): for key, token_user in already_tokens.items(): redis_cli.delete(key) selected_user = Users.filter_one( Users.uu_id == token_user.get("uu_id"), ).data selected_user.remove_refresher_token( domain=data.domain, disconnect=True ) return JSONResponse( content={ "completed": True, "message": "All sessions are disconnected", "data": selected_user.get_dict(), }, status_code=status.HTTP_200_OK, ) return JSONResponse( content={"completed": False, "message": "Invalid data", "data": None}, status_code=status.HTTP_202_ACCEPTED, ) class AuthenticationLogoutEventMethods(MethodToEvent): event_type = "LOGIN" event_description = "Logout only single session of user which domain is provided" event_category = "AUTHENTICATION" __event_keys__ = { "5cc22e4e-a0f7-4077-be41-1871feb3dfd1": "authentication_logout_user", } __event_validation__ = { "5cc22e4e-a0f7-4077-be41-1871feb3dfd1": "authentication_logout_user", } @classmethod def authentication_logout_user( cls, request: Request, data: Logout, token_dict: dict = None ): token_user = None if already_tokens := get_object_via_access_key(request=request): for key in already_tokens: token_user = json.loads(redis_cli.get(key) or {}) if token_user.get("domain") == data.domain: redis_cli.delete(key) selected_user = Users.filter_one( Users.uu_id == token_user.get("uu_id"), ).data selected_user.remove_refresher_token(domain=data.domain) return JSONResponse( content={ "completed": True, "message": "Session is logged out", "data": token_user, }, status_code=status.HTTP_200_OK, ) return JSONResponse( content={ "completed": False, "message": "Logout is not successfully completed", "data": None, }, status_code=status.HTTP_202_ACCEPTED, ) class AuthenticationRefreshTokenEventMethods(MethodToEvent): event_type = "LOGIN" event_description = "Refresh access token with refresher token" event_category = "AUTHENTICATION" __event_keys__ = { "c90f3334-10c9-4181-b5ff-90d98a0287b2": "authentication_refresher_token", } __event_validation__ = { "c90f3334-10c9-4181-b5ff-90d98a0287b2": "authentication_refresher_token", } @classmethod def authentication_refresher_token( cls, request: Request, data: Remember, token_dict: dict = None ): token_refresher = UsersTokens.filter_by_one( token=data.refresh_token, domain=data.domain, **UsersTokens.valid_record_dict, ).data if not token_refresher: return JSONResponse( content={"completed": False, "message": "Invalid data", "data": {}}, status_code=status.HTTP_202_ACCEPTED, ) if found_user := Users.filter_one( Users.id == token_refresher.user_id, ).data: found_user: Users = found_user access_key = save_access_token_to_redis( request=request, found_user=found_user, domain=data.domain ) found_user.last_agent = request.headers.get("User-Agent", None) found_user.last_platform = request.headers.get("Origin", None) found_user.last_remote_addr = getattr( request, "remote_addr", None ) or request.headers.get("X-Forwarded-For", None) found_user.last_seen = str(system_arrow.now()) return JSONResponse( content={ "completed": True, "message": "User is logged in successfully via refresher token", "data": load_user_with_erp_details( found_user, { "access_token": access_key, "refresh_token": data.refresh_token, }, ), }, status_code=status.HTTP_200_OK, ) return JSONResponse( content={"completed": False, "message": "Invalid data", "data": {}}, status_code=status.HTTP_202_ACCEPTED, ) class AuthenticationForgotPasswordEventMethods(MethodToEvent): event_type = "LOGIN" event_description = "Send an email to user for a valid password reset token" event_category = "AUTHENTICATION" __event_keys__ = { "e3ca6e24-b9f8-4127-949c-3bfa364e3513": "authentication_forgot_password", } __event_validation__ = { "e3ca6e24-b9f8-4127-949c-3bfa364e3513": "authentication_forgot_password", } @classmethod def authentication_forgot_password( cls, request: Request, data: Forgot, ): found_user: Users = Users.check_user_exits( access_key=data.access_key, domain=data.domain ) forgot_key = save_access_token_to_redis( request=request, found_user=found_user, domain=data.domain ) forgot_link = ApiStatic.forgot_link(forgot_key=forgot_key) send_email_completed = send_email( subject=f"Dear {found_user.user_tag}, your forgot password link has been sent.", receivers=[str(found_user.email)], html=change_your_password_template( user_name=found_user.user_tag, forgot_link=forgot_link ), ) if not send_email_completed: raise HTTPException( status_code=400, detail="Email can not be sent. Try again later" ) found_user.password_token = forgot_key found_user.password_token_is_valid = str(system_arrow.shift(days=1)) found_user.save() return JSONResponse( content={ "completed": True, "message": "Password is change link is sent to your email or phone", "data": {}, }, status_code=status.HTTP_200_OK, ) class AuthenticationResetPasswordEventMethods(MethodToEvent): event_type = "UPDATE" __event_keys__ = { "af9e121e-24bb-44ac-a616-471d5754360e": "authentication_reset_password", } @classmethod def authentication_reset_password(cls, data: Forgot): from sqlalchemy import or_ found_user = Users.query.filter( or_( Users.email == str(data.access_key).lower(), Users.phone_number == str(data.access_key).replace(" ", ""), ), ).first() if not found_user: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="Given access key or domain is not matching with the any user record.", ) reset_password_token = found_user.reset_password_token(found_user=found_user) send_email_completed = send_email( subject=f"Dear {found_user.user_tag}, a password reset request has been received.", receivers=[str(found_user.email)], html=change_your_password_template( user_name=found_user.user_tag, forgot_link=ApiStatic.forgot_link(forgot_key=reset_password_token), ), ) if not send_email_completed: raise found_user.raise_http_exception( status_code=400, message="Email can not be sent. Try again later" ) return JSONResponse( content={ "completed": True, "message": "Password change link is sent to your email or phone", "data": found_user.get_dict(), }, status_code=status.HTTP_200_OK, ) class AuthenticationDownloadAvatarEventMethods(MethodToEvent): event_type = "LOGIN" event_description = "Download avatar icon and profile info of user" event_category = "AUTHENTICATION" __event_keys__ = { "c140cd5f-307f-4046-a93e-3ade032a57a7": "authentication_download_avatar", } __event_validation__ = { "c140cd5f-307f-4046-a93e-3ade032a57a7": "authentication_download_avatar", } @classmethod def authentication_download_avatar( cls, token_dict: Union[EmployeeTokenObject, OccupantTokenObject] ): if found_user := Users.filter_one(Users.id == token_dict.user_id).data: expired_starts = str( system_arrow.now() - system_arrow.get(str(found_user.expiry_ends)) ) expired_int = ( system_arrow.now() - system_arrow.get(str(found_user.expiry_ends)) ).days return JSONResponse( content={ "completed": True, "message": "Avatar and profile is shared via user credentials", "data": { "lang": token_dict.lang, "full_name": found_user.person.full_name, "avatar": found_user.avatar, "remember_me": found_user.remember_me, "expiry_ends": str(found_user.expiry_ends), "expired_str": expired_starts, "expired_int": int(expired_int), }, }, status_code=status.HTTP_200_OK, ) return JSONResponse( content={"completed": False, "message": "Invalid data", "data": {}}, status_code=status.HTTP_202_ACCEPTED, ) AuthenticationLoginEventMethod = AuthenticationLoginEventMethods( action=ActionsSchema(endpoint="/authentication/login") ) AuthenticationSelectEventMethod = AuthenticationSelectEventMethods( action=ActionsSchema(endpoint="/authentication/select") ) AuthenticationCheckTokenEventMethod = AuthenticationCheckTokenEventMethods( action=ActionsSchema(endpoint="/authentication/valid") ) AuthenticationRefreshEventMethod = AuthenticationRefreshEventMethods( action=ActionsSchema(endpoint="/authentication/refresh") ) AuthenticationChangePasswordEventMethod = AuthenticationChangePasswordEventMethods( action=ActionsSchema(endpoint="/authentication/change_password") ) AuthenticationCreatePasswordEventMethod = AuthenticationCreatePasswordEventMethods( action=ActionsSchema(endpoint="/authentication/create_password") ) AuthenticationDisconnectUserEventMethod = AuthenticationDisconnectUserEventMethods( action=ActionsSchema(endpoint="/authentication/disconnect") ) AuthenticationLogoutEventMethod = AuthenticationLogoutEventMethods( action=ActionsSchema(endpoint="/authentication/logout") ) AuthenticationRefreshTokenEventMethod = AuthenticationRefreshTokenEventMethods( action=ActionsSchema(endpoint="/authentication/refresher") ) AuthenticationForgotPasswordEventMethod = AuthenticationForgotPasswordEventMethods( action=ActionsSchema(endpoint="/authentication/forgot") ) AuthenticationDownloadAvatarEventMethod = AuthenticationDownloadAvatarEventMethods( action=ActionsSchema(endpoint="/authentication/avatar") ) AuthenticationResetPasswordEventMethod = AuthenticationResetPasswordEventMethods( action=ActionsSchema(endpoint="/authentication/reset_password") ) # UserLogger.log_error( # str( # dict( # user_id=found_user.id, # domain=data.domain, # access_key=token_user.get("access_input"), # agent=request.headers.get("User-Agent", None), # ip=getattr(request, "remote_addr", None) # or request.headers.get("X-Forwarded-For", None), # platform=request.headers.get("Origin", None), # login_date=datetime.datetime.utcnow().__str__(), # is_login=False, # ) # ) # ) # UserLogger.log_error( # str( # dict( # user_id=found_user.id, # domain=data.domain, # access_key=data.access_key, # agent=request.headers.get("User-Agent", None), # ip=getattr(request, "remote_addr", None) # or request.headers.get("X-Forwarded-For", None), # platform=request.headers.get("Origin", None), # login_date=str(DateTimeLocal.now()), # is_login=False, # ) # ) # ) # UserLogger.log_error( # str( # dict( # user_id=found_user.id, # domain=data.domain, # access_key="via_refresher", # agent=request.headers.get("User-Agent", None), # ip=getattr(request, "remote_addr", None) # or request.headers.get("X-Forwarded-For", None), # platform=request.headers.get("Origin", None), # login_date=datetime.datetime.utcnow().__str__(), # is_login=False, # ) # ) # ) # UserLogger.log_error( # str( # dict( # user_id=selected_user.id, # domain=data.domain, # access_key=token_user.get("access_input"), # agent=request.headers.get("User-Agent", None), # ip=getattr(request, "remote_addr", None) # or request.headers.get("X-Forwarded-For", None), # platform=request.headers.get("Origin", None), # login_date=datetime.datetime.utcnow().__str__(), # is_login=False, # ) # ) # )