diff --git a/apis/endpoints/admin.py b/apis/endpoints/admin.py index 56622a8..09b3f0b 100644 --- a/apis/endpoints/admin.py +++ b/apis/endpoints/admin.py @@ -1,8 +1,11 @@ -from fastapi import APIRouter, Request, status +from fastapi import APIRouter, Request from services.clinicServices import ClinicServices from schemas.UpdateSchemas import ClinicStatusUpdate from schemas.ApiResponse import ApiResponse +from schemas.BaseSchemas import CreateSuperAdmin, ResetPasswordBase +from services.authService import AuthService +from utils.constants import DEFAULT_LIMIT, DEFAULT_PAGE router = APIRouter() @@ -11,3 +14,18 @@ router = APIRouter() def update_clinic_status(req:Request, data: ClinicStatusUpdate): response = ClinicServices().update_clinic_status(req.state.user, data.clinic_id, data.status, data.documentStatus, data.rejection_reason) return ApiResponse(data=response, message="Clinic status updated successfully") + + +@router.post("/user") +def create_user(req:Request, user_data: CreateSuperAdmin): + AuthService().create_super_admin(req.state.user, user_data) + return ApiResponse(data="OK", message="User created successfully") + + +@router.get("/") +def get_users(req:Request, limit:int = DEFAULT_LIMIT, page:int = DEFAULT_PAGE, search:str = ""): + if page < 1: + page = 1 + offset = (page - 1) * limit + users = AuthService().get_admins(req.state.user, limit, offset, search) + return ApiResponse(data=users, message="Users retrieved successfully") diff --git a/apis/endpoints/auth.py b/apis/endpoints/auth.py index bef15c6..5798810 100644 --- a/apis/endpoints/auth.py +++ b/apis/endpoints/auth.py @@ -2,7 +2,7 @@ from fastapi import APIRouter, BackgroundTasks from services.authService import AuthService from schemas.CreateSchemas import UserCreate from schemas.ApiResponse import ApiResponse -from schemas.BaseSchemas import AuthBase, AuthOTP +from schemas.BaseSchemas import AuthBase, AuthOTP, ResetPasswordBase from services.clinicServices import ClinicServices from http import HTTPStatus @@ -34,6 +34,17 @@ def get_latest_clinic_id(): ) +@router.post('/admin/forget-password') +def forget_password(email: str): + AuthService().forget_password(email) + return ApiResponse(data="OK", message="Password reset email sent successfully") + +@router.post('/admin/reset-password') +def reset_password(data: ResetPasswordBase): + AuthService().reset_password(data.token, data.password) + return ApiResponse(data="OK", message="Password reset successfully") + + @router.post("/send-otp") def send_otp(email: str): AuthService().send_otp(email) diff --git a/main.py b/main.py index a0c82cf..01c2024 100644 --- a/main.py +++ b/main.py @@ -14,6 +14,7 @@ from apis import api_router # middleware from middleware.ErrorHandlerMiddleware import ErrorHandlerMiddleware, configure_exception_handlers from middleware.CustomRequestTypeMiddleware import TextPlainMiddleware +from services.emailService import EmailService dotenv.load_dotenv() @@ -69,6 +70,8 @@ configure_exception_handlers(app) @app.get("/") async def hello_world(): + # email_service = EmailService() + # email_service.createTemplate() return {"Hello": "World"} diff --git a/migrations/versions/48785e34c37c_updated_user_table2.py b/migrations/versions/48785e34c37c_updated_user_table2.py new file mode 100644 index 0000000..4c91fd3 --- /dev/null +++ b/migrations/versions/48785e34c37c_updated_user_table2.py @@ -0,0 +1,35 @@ +"""updated_user_table2 + +Revision ID: 48785e34c37c +Revises: 7d1c821a7e05 +Create Date: 2025-05-21 13:54:54.833038 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = '48785e34c37c' +down_revision: Union[str, None] = '7d1c821a7e05' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + """Upgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + # op.drop_index('ix_users_username', table_name='users') + # op.create_index(op.f('ix_users_username'), 'users', ['username'], unique=False) + pass + # ### end Alembic commands ### + + +def downgrade() -> None: + """Downgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.drop_index(op.f('ix_users_username'), table_name='users') + op.create_index('ix_users_username', 'users', ['username'], unique=True) + # ### end Alembic commands ### diff --git a/migrations/versions/7d1c821a7e05_updated_user_table.py b/migrations/versions/7d1c821a7e05_updated_user_table.py new file mode 100644 index 0000000..de5c194 --- /dev/null +++ b/migrations/versions/7d1c821a7e05_updated_user_table.py @@ -0,0 +1,32 @@ +"""updated_user_table + +Revision ID: 7d1c821a7e05 +Revises: ec157808ef2a +Create Date: 2025-05-21 13:51:39.680812 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = '7d1c821a7e05' +down_revision: Union[str, None] = 'ec157808ef2a' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + """Upgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.alter_column('users', 'mobile', nullable=True) + # ### end Alembic commands ### + + +def downgrade() -> None: + """Downgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.alter_column('users', 'mobile', nullable=False) + # ### end Alembic commands ### diff --git a/models/ResetPasswordTokens.py b/models/ResetPasswordTokens.py new file mode 100644 index 0000000..28a0d62 --- /dev/null +++ b/models/ResetPasswordTokens.py @@ -0,0 +1,11 @@ +from sqlalchemy import Column, Integer, String +from database import Base +from .CustomBase import CustomBase + + +class ResetPasswordTokens(Base, CustomBase): + __tablename__ = "reset_password_tokens" + id = Column(Integer, primary_key=True) + email = Column(String) + token = Column(String) + \ No newline at end of file diff --git a/models/Users.py b/models/Users.py index 3cddf1b..ba3e76c 100644 --- a/models/Users.py +++ b/models/Users.py @@ -14,7 +14,7 @@ class Users(Base, CustomBase): clinicRole = Column(Enum(ClinicUserRoles), nullable=True) userType = Column(Enum(UserType), nullable=True) profile_pic = Column(String, nullable=True) - mobile = Column(String) + mobile = Column(String, nullable=True) # Notification relationships sent_notifications = relationship("Notifications", foreign_keys="Notifications.sender_id", back_populates="sender") diff --git a/models/__init__.py b/models/__init__.py index b516c44..05e91f9 100644 --- a/models/__init__.py +++ b/models/__init__.py @@ -14,6 +14,7 @@ from .BlockedEmail import BlockedEmail from .SignupPricingMaster import SignupPricingMaster from .ClinicFileVerifications import ClinicFileVerifications from .OTP import OTP +from .ResetPasswordTokens import ResetPasswordTokens __all__ = [ "Users", @@ -31,5 +32,6 @@ __all__ = [ "BlockedEmail", "SignupPricingMaster", "ClinicFileVerifications", - "OTP" + "OTP", + "ResetPasswordTokens" ] diff --git a/schemas/BaseSchemas.py b/schemas/BaseSchemas.py index 226490b..a9ede96 100644 --- a/schemas/BaseSchemas.py +++ b/schemas/BaseSchemas.py @@ -11,7 +11,6 @@ class SNSBase(BaseModel): TopicArn: str SubscribeURL: str Message: str - class AuthOTP(BaseModel): email: EmailStr @@ -33,6 +32,15 @@ class AuthBase(BaseModel): email: EmailStr password: str + +class ResetPasswordBase(BaseModel): + token: str + password: str + +class CreateSuperAdmin(BaseModel): + username:str + email:EmailStr + # Base schemas (shared attributes for create/read operations) class ClinicBase(BaseModel): name: str @@ -101,7 +109,7 @@ class UserBase(BaseModel): password: str clinicRole: Optional[ClinicUserRoles] = None userType: Optional[UserType] = None - mobile: str + mobile: Optional[str] = None class ClinicDoctorBase(BaseModel): diff --git a/services/authService.py b/services/authService.py index 345c722..e5d36b2 100644 --- a/services/authService.py +++ b/services/authService.py @@ -1,17 +1,28 @@ +from operator import or_ +import os +import dotenv + +dotenv.load_dotenv() + +from interface.common_response import CommonResponse +from schemas.ResponseSchemas import UserResponse import datetime import json import urllib.request -from sqlalchemy.orm import Session from services.jwtService import create_jwt_token from services.userServices import UserServices from models import BlockedEmail from services.emailService import EmailService from exceptions.validation_exception import ValidationException from models import OTP +from enums.enums import UserType +from models import Users +from exceptions.resource_not_found_exception import ResourceNotFoundException +from models import ResetPasswordTokens from utils.constants import generateOTP -from utils.password_utils import verify_password +from utils.password_utils import generate_reset_password_token, generate_secure_password, hash_password, verify_password from schemas.CreateSchemas import UserCreate -from schemas.BaseSchemas import AuthBase, AuthOTP +from schemas.BaseSchemas import AuthBase, AuthOTP, CreateSuperAdmin from exceptions.unauthorized_exception import UnauthorizedException from database import get_db @@ -23,6 +34,7 @@ class AuthService: self.user_service = UserServices() self.db = next(get_db()) self.email_service = EmailService() + self.url = os.getenv("FRONTEND_URL") def login(self, data: AuthBase) -> str: @@ -106,4 +118,111 @@ class AuthService: # self.db.delete(db_otp) # self.db.commit() - return \ No newline at end of file + return + + + def get_admins(self, user, limit:int, offset:int, search:str): + try: + if user["userType"] != UserType.SUPER_ADMIN: + raise UnauthorizedException("User is not authorized to perform this action") + + admins = self.db.query(Users).filter(Users.userType == UserType.SUPER_ADMIN) + + total = self.db.query(Users).filter(Users.userType == UserType.SUPER_ADMIN).count() + + if search: + admins = admins.filter( + or_( + Users.username.contains(search), + Users.email.contains(search), + ) + ) + + total = admins.count() + + admins = admins.limit(limit).offset(offset).all() + + response = [UserResponse(**admin.__dict__.copy()) for admin in admins] + + common_response = CommonResponse(data=response, total=total) + + return common_response + except Exception as e: + raise e + + def create_super_admin(self, user, data: CreateSuperAdmin): + + if user["userType"] != UserType.SUPER_ADMIN: + raise UnauthorizedException("User is not authorized to perform this action") + + password = generate_secure_password() + hashed_password = hash_password(password) + + # check if username and email are unique + existing_user = ( + self.db.query(Users) + .filter( + Users.email == data.email.lower(), + ) + .first() + ) + + if existing_user: + raise ValidationException("User with same email already exists") + + user = Users( + username=data.username.lower(), + email=data.email.lower(), + password=hashed_password, + userType=UserType.SUPER_ADMIN, + ) + + + self.db.add(user) + self.db.commit() + + + LOGIN_URL = self.url + + # send email to user + self.email_service.send_new_admin_email(data.email, password, LOGIN_URL) + + return + + + def forget_password(self, email: str): + user = self.db.query(Users).filter(Users.email == email.lower()).first() + + if not user: + raise ResourceNotFoundException("User not found") + + # get reset password token + reset_password_token = generate_reset_password_token() + + reset_password = ResetPasswordTokens(email=email, token=reset_password_token) + self.db.add(reset_password) + self.db.commit() + + reset_password_url = f"{self.url}/auth/reset-password?token={reset_password_token}" + + self.email_service.send_reset_password_email(email, reset_password_url) + + return + + def reset_password(self, token: str, password: str): + reset_password = self.db.query(ResetPasswordTokens).filter(ResetPasswordTokens.token == token).first() + + if not reset_password: + raise ResourceNotFoundException("Reset password token not found") + + user = self.db.query(Users).filter(Users.email == reset_password.email).first() + + if not user: + raise ResourceNotFoundException("User not found") + + user.password = hash_password(password) + self.db.delete(reset_password) + self.db.commit() + + return + \ No newline at end of file diff --git a/services/emailService.py b/services/emailService.py index fc868d3..547fd75 100644 --- a/services/emailService.py +++ b/services/emailService.py @@ -67,10 +67,45 @@ class EmailService: "TextPart": "Dear User, Congratulations! Your clinic {{name}} has been approved. Thank you, Team 24x7 AIHR" } - self.client.create_template(Template=otp_template) - self.client.create_template(Template=new_clinic_template) - self.client.create_template(Template=reject_clinic_template) - self.client.create_template(Template=apporve_clinic_template) + new_admin_template = { + "TemplateName": "newAdmin", + "SubjectPart": "Login Credentials", + "HtmlPart": """ +
Dear User,
+Your login credentials are:
+Email: {{email}}
+Password: {{password}}
+Use the following link to login:
+Login URL: {{login_url}}
+Thank you,
Team 24x7 AIHR
Dear User,
+You have requested to reset your password. Please use the following link to reset your password:
+Reset Password URL: {{reset_password_url}}
+Thank you,
Team 24x7 AIHR