feat: full clinic signup
This commit is contained in:
parent
80c61dc127
commit
25e105e714
|
|
@ -1,7 +1,11 @@
|
||||||
from fastapi import APIRouter, Depends
|
from fastapi import APIRouter, Depends, Security
|
||||||
from middleware.auth_dependency import auth_required
|
from middleware.auth_dependency import auth_required
|
||||||
|
from fastapi.security import HTTPBearer
|
||||||
|
|
||||||
from .endpoints import clinics, doctors, calender, appointments, patients, admin, auth
|
# Import the security scheme
|
||||||
|
bearer_scheme = HTTPBearer(scheme_name="Bearer Authentication")
|
||||||
|
|
||||||
|
from .endpoints import clinics, doctors, calender, appointments, patients, admin, auth, s3
|
||||||
|
|
||||||
api_router = APIRouter()
|
api_router = APIRouter()
|
||||||
# api_router.include_router(twilio.router, prefix="/twilio")
|
# api_router.include_router(twilio.router, prefix="/twilio")
|
||||||
|
|
@ -10,5 +14,10 @@ api_router.include_router(doctors.router, prefix="/doctors", tags=["doctors"])
|
||||||
api_router.include_router(calender.router, prefix="/calender", tags=["calender"])
|
api_router.include_router(calender.router, prefix="/calender", tags=["calender"])
|
||||||
api_router.include_router(appointments.router, prefix="/appointments", tags=["appointments"])
|
api_router.include_router(appointments.router, prefix="/appointments", tags=["appointments"])
|
||||||
api_router.include_router(patients.router, prefix="/patients", tags=["patients"])
|
api_router.include_router(patients.router, prefix="/patients", tags=["patients"])
|
||||||
api_router.include_router(admin.router, prefix="/admin", dependencies=[Depends(auth_required)], tags=["admin"])
|
api_router.include_router(
|
||||||
|
admin.router,
|
||||||
|
prefix="/admin",
|
||||||
|
dependencies=[Depends(auth_required)],
|
||||||
|
tags=["admin"])
|
||||||
api_router.include_router(auth.router, prefix="/auth", tags=["auth"])
|
api_router.include_router(auth.router, prefix="/auth", tags=["auth"])
|
||||||
|
api_router.include_router(s3.router, dependencies=[Depends(auth_required)], prefix="/s3", tags=["s3"])
|
||||||
|
|
@ -7,17 +7,17 @@ router = APIRouter()
|
||||||
|
|
||||||
@router.post("/login")
|
@router.post("/login")
|
||||||
async def login(email: str, password: str):
|
async def login(email: str, password: str):
|
||||||
response = await AuthService().login(email, password)
|
token = await AuthService().login(email, password)
|
||||||
return ApiResponse(
|
return ApiResponse(
|
||||||
data=response,
|
data=token,
|
||||||
message="Login successful"
|
message="Login successful"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@router.post("/register")
|
@router.post("/register")
|
||||||
async def register(user_data: UserCreate):
|
async def register(user_data: UserCreate):
|
||||||
response = await AuthService().register(user_data)
|
await AuthService().register(user_data)
|
||||||
return ApiResponse(
|
return ApiResponse(
|
||||||
data=response,
|
data="OK",
|
||||||
message="User registered successfully"
|
message="User registered successfully"
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
from fastapi import APIRouter, status
|
||||||
|
from fastapi import Request
|
||||||
|
from services.s3Service import upload_file as upload_file_service
|
||||||
|
from enums.enums import S3FolderNameEnum
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from schemas.ApiResponse import ApiResponse
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
@router.post("/", status_code=status.HTTP_200_OK)
|
||||||
|
async def upload_file(request: Request, folder: S3FolderNameEnum, file_name: str, clinic_id: Optional[str] = None):
|
||||||
|
userId = request.state.user["id"]
|
||||||
|
resp = await upload_file_service(userId, folder, file_name, clinic_id)
|
||||||
|
return ApiResponse(data=resp, message="File uploaded successfully")
|
||||||
|
|
@ -7,12 +7,16 @@ class AppointmentStatus(Enum):
|
||||||
CANCELLED = "cancelled"
|
CANCELLED = "cancelled"
|
||||||
COMPLETED = "completed"
|
COMPLETED = "completed"
|
||||||
|
|
||||||
|
class Integration(Enum):
|
||||||
|
BP = "bp"
|
||||||
|
MEDICAL_DIRECTOR = "medical_director"
|
||||||
|
|
||||||
|
|
||||||
class ClinicStatus(Enum):
|
class ClinicStatus(Enum):
|
||||||
ACTIVE = "active"
|
ACTIVE = "active"
|
||||||
INACTIVE = "inactive"
|
INACTIVE = "inactive"
|
||||||
UNDER_REVIEW = "under_review"
|
UNDER_REVIEW = "under_review"
|
||||||
REQUESTED_DOCTOR = "requested_doctor"
|
REQUESTED_DOC = "requested_doc"
|
||||||
REJECTED = "rejected"
|
REJECTED = "rejected"
|
||||||
PAYMENT_DUE = "payment_due"
|
PAYMENT_DUE = "payment_due"
|
||||||
|
|
||||||
|
|
@ -35,3 +39,7 @@ class UserType(Enum):
|
||||||
class Integration(Enum):
|
class Integration(Enum):
|
||||||
BP = "bp"
|
BP = "bp"
|
||||||
MEDICAL_DIRECTOR = "medical_director"
|
MEDICAL_DIRECTOR = "medical_director"
|
||||||
|
|
||||||
|
class S3FolderNameEnum(str, Enum):
|
||||||
|
PROFILE = "profile"
|
||||||
|
ASSETS = "assets"
|
||||||
27
main.py
27
main.py
|
|
@ -1,19 +1,13 @@
|
||||||
import dotenv
|
import dotenv
|
||||||
from fastapi import FastAPI
|
from fastapi import FastAPI, Security
|
||||||
from contextlib import asynccontextmanager
|
from contextlib import asynccontextmanager
|
||||||
import logging
|
import logging
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
|
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
||||||
|
|
||||||
# db
|
# db
|
||||||
from database import Base, engine
|
from database import Base, engine
|
||||||
|
|
||||||
# IMPORTANT: Import all models to register them with SQLAlchemy
|
|
||||||
# from models.Clinics import Clinics
|
|
||||||
# from models.Doctors import Doctors
|
|
||||||
# from models.Patients import Patients
|
|
||||||
# from models.Appointments import Appointments
|
|
||||||
# from models.Calendar import Calenders
|
|
||||||
|
|
||||||
# routers
|
# routers
|
||||||
from apis import api_router
|
from apis import api_router
|
||||||
|
|
||||||
|
|
@ -43,8 +37,25 @@ async def lifespan(app: FastAPI):
|
||||||
logger.info("Stopping application")
|
logger.info("Stopping application")
|
||||||
|
|
||||||
|
|
||||||
|
# Define the security scheme
|
||||||
|
bearer_scheme = HTTPBearer(scheme_name="Bearer Authentication")
|
||||||
|
|
||||||
app = FastAPI(
|
app = FastAPI(
|
||||||
lifespan=lifespan,
|
lifespan=lifespan,
|
||||||
|
title="Twillio Voice API",
|
||||||
|
description="API for Twillio Voice application",
|
||||||
|
version="1.0.0",
|
||||||
|
# Define security scheme for Swagger UI
|
||||||
|
openapi_tags=[
|
||||||
|
{"name": "admin", "description": "Admin operations requiring authentication"},
|
||||||
|
{"name": "auth", "description": "Authentication operations"},
|
||||||
|
{"name": "clinics", "description": "Operations with clinics"},
|
||||||
|
{"name": "doctors", "description": "Operations with doctors"},
|
||||||
|
{"name": "calender", "description": "Calendar operations"},
|
||||||
|
{"name": "appointments", "description": "Operations with appointments"},
|
||||||
|
{"name": "patients", "description": "Operations with patients"}
|
||||||
|
],
|
||||||
|
swagger_ui_parameters={"defaultModelsExpandDepth": -1}
|
||||||
)
|
)
|
||||||
|
|
||||||
# CORS middleware
|
# CORS middleware
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ async def auth_required(request: Request ,credentials: HTTPAuthorizationCredenti
|
||||||
raise HTTPException(status_code=401, detail="Invalid authentication token")
|
raise HTTPException(status_code=401, detail="Invalid authentication token")
|
||||||
|
|
||||||
# Get user from database
|
# Get user from database
|
||||||
user = UserServices().get_user(payload["user_id"])
|
user = UserServices().get_user(payload["id"])
|
||||||
|
|
||||||
# set user to request state
|
# set user to request state
|
||||||
request.state.user = user
|
request.state.user = user
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
"""updated clinic enum status
|
||||||
|
|
||||||
|
Revision ID: 827c736d4aeb
|
||||||
|
Revises: 928001a9d80f
|
||||||
|
Create Date: 2025-05-12 15:36:42.117900
|
||||||
|
|
||||||
|
"""
|
||||||
|
from typing import Sequence, Union
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision: str = '827c736d4aeb'
|
||||||
|
down_revision: Union[str, None] = '928001a9d80f'
|
||||||
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
"""Upgrade schema."""
|
||||||
|
# Update the enum type to replace 'requested_doctor' with 'requested_doc'
|
||||||
|
op.execute("ALTER TYPE clinicstatus RENAME VALUE 'REQUESTED_DOCTOR' TO 'REQUESTED_DOC'")
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
"""Downgrade schema."""
|
||||||
|
# Revert the enum type change back to 'requested_doctor' from 'requested_doc'
|
||||||
|
op.execute("ALTER TYPE clinicstatus RENAME VALUE 'REQUESTED_DOC' TO 'REQUESTED_DOCTOR'")
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
@ -0,0 +1,110 @@
|
||||||
|
"""updated clinic
|
||||||
|
|
||||||
|
Revision ID: 928001a9d80f
|
||||||
|
Revises:
|
||||||
|
Create Date: 2025-05-12 14:19:15.351582
|
||||||
|
|
||||||
|
"""
|
||||||
|
from typing import Sequence, Union
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision: str = '928001a9d80f'
|
||||||
|
down_revision: Union[str, None] = None
|
||||||
|
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.add_column('appointments', sa.Column('deleted_at', sa.DateTime(timezone=True), nullable=True))
|
||||||
|
op.add_column('calenders', sa.Column('deleted_at', sa.DateTime(timezone=True), nullable=True))
|
||||||
|
op.add_column('clinics', sa.Column('emergency_phone', sa.String(), nullable=True))
|
||||||
|
op.add_column('clinics', sa.Column('fax', sa.String(), nullable=True))
|
||||||
|
# Create the integration enum type first
|
||||||
|
integration_type = sa.Enum('BP', 'MEDICAL_DIRECTOR', name='integration')
|
||||||
|
integration_type.create(op.get_bind())
|
||||||
|
op.add_column('clinics', sa.Column('integration', integration_type, nullable=True))
|
||||||
|
op.add_column('clinics', sa.Column('pms_id', sa.String(), nullable=True))
|
||||||
|
op.add_column('clinics', sa.Column('practice_name', sa.String(), nullable=True))
|
||||||
|
op.add_column('clinics', sa.Column('logo', sa.String(), nullable=True))
|
||||||
|
op.add_column('clinics', sa.Column('country', sa.String(), nullable=True))
|
||||||
|
op.add_column('clinics', sa.Column('postal_code', sa.String(), nullable=True))
|
||||||
|
op.add_column('clinics', sa.Column('city', sa.String(), nullable=True))
|
||||||
|
op.add_column('clinics', sa.Column('state', sa.String(), nullable=True))
|
||||||
|
op.add_column('clinics', sa.Column('abn_doc', sa.String(), nullable=True))
|
||||||
|
op.add_column('clinics', sa.Column('abn_number', sa.String(), nullable=True))
|
||||||
|
op.add_column('clinics', sa.Column('contract_doc', sa.String(), nullable=True))
|
||||||
|
op.add_column('clinics', sa.Column('clinic_phone', sa.String(), nullable=True))
|
||||||
|
op.add_column('clinics', sa.Column('is_clinic_phone_enabled', sa.Boolean(), nullable=True))
|
||||||
|
op.add_column('clinics', sa.Column('other_info', sa.String(), nullable=True))
|
||||||
|
op.add_column('clinics', sa.Column('greeting_msg', sa.String(), nullable=True))
|
||||||
|
op.add_column('clinics', sa.Column('voice_model', sa.String(), nullable=True))
|
||||||
|
op.add_column('clinics', sa.Column('voice_model_provider', sa.String(), nullable=True))
|
||||||
|
op.add_column('clinics', sa.Column('voice_model_gender', sa.String(), nullable=True))
|
||||||
|
op.add_column('clinics', sa.Column('scenarios', sa.String(), nullable=True))
|
||||||
|
op.add_column('clinics', sa.Column('general_info', sa.String(), nullable=True))
|
||||||
|
# Create the clinicstatus enum type first
|
||||||
|
clinic_status_type = sa.Enum('ACTIVE', 'INACTIVE', 'UNDER_REVIEW', 'REQUESTED_DOCTOR', 'REJECTED', 'PAYMENT_DUE', name='clinicstatus')
|
||||||
|
clinic_status_type.create(op.get_bind())
|
||||||
|
op.add_column('clinics', sa.Column('status', clinic_status_type, nullable=True))
|
||||||
|
op.add_column('clinics', sa.Column('domain', sa.String(), nullable=True))
|
||||||
|
op.add_column('clinics', sa.Column('deleted_at', sa.DateTime(timezone=True), nullable=True))
|
||||||
|
op.create_unique_constraint(None, 'clinics', ['emergency_phone'])
|
||||||
|
op.add_column('doctors', sa.Column('deleted_at', sa.DateTime(timezone=True), nullable=True))
|
||||||
|
op.add_column('patients', sa.Column('deleted_at', sa.DateTime(timezone=True), nullable=True))
|
||||||
|
op.add_column('users', sa.Column('profile_pic', sa.String(), nullable=True))
|
||||||
|
op.add_column('users', sa.Column('deleted_at', sa.DateTime(timezone=True), nullable=True))
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
"""Downgrade schema."""
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_column('users', 'deleted_at')
|
||||||
|
op.drop_column('users', 'profile_pic')
|
||||||
|
op.drop_column('patients', 'deleted_at')
|
||||||
|
op.drop_column('doctors', 'deleted_at')
|
||||||
|
op.drop_constraint(None, 'clinics', type_='unique')
|
||||||
|
op.drop_column('clinics', 'deleted_at')
|
||||||
|
op.drop_column('clinics', 'domain')
|
||||||
|
|
||||||
|
# Drop the status column first
|
||||||
|
op.drop_column('clinics', 'status')
|
||||||
|
# Then drop the enum type
|
||||||
|
sa.Enum(name='clinicstatus').drop(op.get_bind())
|
||||||
|
|
||||||
|
op.drop_column('clinics', 'general_info')
|
||||||
|
op.drop_column('clinics', 'scenarios')
|
||||||
|
op.drop_column('clinics', 'voice_model_gender')
|
||||||
|
op.drop_column('clinics', 'voice_model_provider')
|
||||||
|
op.drop_column('clinics', 'voice_model')
|
||||||
|
op.drop_column('clinics', 'greeting_msg')
|
||||||
|
op.drop_column('clinics', 'other_info')
|
||||||
|
op.drop_column('clinics', 'is_clinic_phone_enabled')
|
||||||
|
op.drop_column('clinics', 'clinic_phone')
|
||||||
|
op.drop_column('clinics', 'contract_doc')
|
||||||
|
op.drop_column('clinics', 'abn_number')
|
||||||
|
op.drop_column('clinics', 'abn_doc')
|
||||||
|
op.drop_column('clinics', 'state')
|
||||||
|
op.drop_column('clinics', 'city')
|
||||||
|
op.drop_column('clinics', 'postal_code')
|
||||||
|
op.drop_column('clinics', 'country')
|
||||||
|
op.drop_column('clinics', 'logo')
|
||||||
|
op.drop_column('clinics', 'practice_name')
|
||||||
|
op.drop_column('clinics', 'pms_id')
|
||||||
|
|
||||||
|
# Drop the integration column first
|
||||||
|
op.drop_column('clinics', 'integration')
|
||||||
|
# Then drop the enum type
|
||||||
|
sa.Enum(name='integration').drop(op.get_bind())
|
||||||
|
|
||||||
|
op.drop_column('clinics', 'fax')
|
||||||
|
op.drop_column('clinics', 'emergency_phone')
|
||||||
|
op.drop_column('calenders', 'deleted_at')
|
||||||
|
op.drop_column('appointments', 'deleted_at')
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
@ -1,7 +1,9 @@
|
||||||
from sqlalchemy import Column, Integer, String
|
from sqlalchemy import Column, Integer, String, Boolean
|
||||||
from sqlalchemy.orm import relationship
|
from sqlalchemy.orm import relationship
|
||||||
|
|
||||||
from database import Base
|
from database import Base
|
||||||
|
from enums.enums import Integration, ClinicStatus
|
||||||
|
from sqlalchemy import Enum
|
||||||
from .CustomBase import CustomBase
|
from .CustomBase import CustomBase
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -11,7 +13,32 @@ class Clinics(Base, CustomBase):
|
||||||
id = Column(Integer, primary_key=True, index=True)
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
name = Column(String)
|
name = Column(String)
|
||||||
address = Column(String, nullable=True)
|
address = Column(String, nullable=True)
|
||||||
phone = Column(String, unique=True, index=True)
|
phone = Column(String, unique=True, index=True) # clinic phone
|
||||||
|
emergency_phone = Column(String, unique=True, nullable=True)
|
||||||
email = Column(String, unique=True, index=True, nullable=True)
|
email = Column(String, unique=True, index=True, nullable=True)
|
||||||
|
fax = Column(String, nullable=True)
|
||||||
|
integration = Column(Enum(Integration))
|
||||||
|
pms_id = Column(String, nullable=True)
|
||||||
|
practice_name = Column(String, nullable=True)
|
||||||
|
logo = Column(String, nullable=True)
|
||||||
|
country = Column(String, nullable=True)
|
||||||
|
postal_code = Column(String, nullable=True)
|
||||||
|
city = Column(String, nullable=True)
|
||||||
|
state = Column(String, nullable=True)
|
||||||
|
abn_doc = Column(String, nullable=True)
|
||||||
|
abn_number = Column(String, nullable=True)
|
||||||
|
contract_doc = Column(String, nullable=True)
|
||||||
|
clinic_phone = Column(String, nullable=True) # AI Receptionist Phone
|
||||||
|
is_clinic_phone_enabled = Column(Boolean, default=False)
|
||||||
|
other_info = Column(String, nullable=True)
|
||||||
|
greeting_msg = Column(String, nullable=True)
|
||||||
|
voice_model = Column(String, nullable=True)
|
||||||
|
voice_model_provider = Column(String, nullable=True)
|
||||||
|
voice_model_gender = Column(String, nullable=True)
|
||||||
|
scenarios = Column(String, nullable=True)
|
||||||
|
general_info = Column(String, nullable=True)
|
||||||
|
status = Column(Enum(ClinicStatus))
|
||||||
|
domain = Column(String, nullable=True) # unique for each clinic
|
||||||
|
|
||||||
|
|
||||||
doctors = relationship("Doctors", back_populates="clinic")
|
doctors = relationship("Doctors", back_populates="clinic")
|
||||||
|
|
|
||||||
|
|
@ -12,3 +12,4 @@ class Users(Base, CustomBase):
|
||||||
password = Column(String)
|
password = Column(String)
|
||||||
clinicRole = Column(Enum(ClinicUserRoles), nullable=True)
|
clinicRole = Column(Enum(ClinicUserRoles), nullable=True)
|
||||||
userType = Column(Enum(UserType), nullable=True)
|
userType = Column(Enum(UserType), nullable=True)
|
||||||
|
profile_pic = Column(String, nullable=True)
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import List, Optional
|
from typing import List, Optional
|
||||||
from pydantic import BaseModel, EmailStr
|
from pydantic import BaseModel, EmailStr
|
||||||
from enums.enums import AppointmentStatus, ClinicUserRoles, UserType
|
from enums.enums import AppointmentStatus, ClinicUserRoles, UserType, Integration
|
||||||
|
|
||||||
|
|
||||||
# Base schemas (shared attributes for create/read operations)
|
# Base schemas (shared attributes for create/read operations)
|
||||||
|
|
@ -11,6 +11,26 @@ class ClinicBase(BaseModel):
|
||||||
address: Optional[str] = None
|
address: Optional[str] = None
|
||||||
phone: str
|
phone: str
|
||||||
email: Optional[EmailStr] = None
|
email: Optional[EmailStr] = None
|
||||||
|
integration: Integration
|
||||||
|
pms_id: str
|
||||||
|
practice_name: str
|
||||||
|
logo: Optional[str] = None
|
||||||
|
country: Optional[str] = None
|
||||||
|
postal_code: Optional[str] = None
|
||||||
|
city: Optional[str] = None
|
||||||
|
state: Optional[str] = None
|
||||||
|
abn_doc: Optional[str] = None
|
||||||
|
abn_number: Optional[str] = None
|
||||||
|
contract_doc: Optional[str] = None
|
||||||
|
clinic_phone: Optional[str] = None
|
||||||
|
is_clinic_phone_enabled: Optional[bool] = None
|
||||||
|
other_info: Optional[str] = None
|
||||||
|
greeting_msg: Optional[str] = None
|
||||||
|
voice_model: Optional[str] = None
|
||||||
|
voice_model_provider: Optional[str] = None
|
||||||
|
voice_model_gender: Optional[str] = None
|
||||||
|
scenarios: Optional[str] = None
|
||||||
|
general_info: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
class DoctorBase(BaseModel):
|
class DoctorBase(BaseModel):
|
||||||
|
|
|
||||||
|
|
@ -32,5 +32,8 @@ class AppointmentCreateWithNames(BaseModel):
|
||||||
status: AppointmentStatus = AppointmentStatus.CONFIRMED
|
status: AppointmentStatus = AppointmentStatus.CONFIRMED
|
||||||
|
|
||||||
|
|
||||||
class UserCreate(UserBase):
|
class UserCreate(BaseModel):
|
||||||
pass
|
# User data sent from frontend
|
||||||
|
user: UserBase
|
||||||
|
# Clinic data sent from frontend
|
||||||
|
clinic: ClinicBase
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,6 @@ class AuthService:
|
||||||
token = create_jwt_token(user_dict)
|
token = create_jwt_token(user_dict)
|
||||||
return token
|
return token
|
||||||
|
|
||||||
async def register(self, user_data: UserCreate) -> str:
|
async def register(self, user_data: UserCreate) -> None:
|
||||||
user = await self.user_service.create_user(user_data)
|
await self.user_service.create_user(user_data)
|
||||||
token = create_jwt_token(user)
|
return
|
||||||
return token
|
|
||||||
|
|
@ -0,0 +1,149 @@
|
||||||
|
from enum import Enum
|
||||||
|
from typing import Optional, Dict, Any
|
||||||
|
import os
|
||||||
|
from datetime import datetime
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
|
import boto3
|
||||||
|
from botocore.config import Config
|
||||||
|
from botocore.exceptions import ClientError
|
||||||
|
from fastapi import HTTPException
|
||||||
|
from pydantic_settings import BaseSettings
|
||||||
|
from enums.enums import S3FolderNameEnum
|
||||||
|
from exceptions.business_exception import BusinessValidationException
|
||||||
|
|
||||||
|
|
||||||
|
class Settings(BaseSettings):
|
||||||
|
AWS_REGION: str
|
||||||
|
AWS_ACCESS_KEY: str
|
||||||
|
AWS_SECRET_KEY: str
|
||||||
|
AWS_BUCKET_NAME: str
|
||||||
|
AWS_S3_EXPIRES: int = 60 * 60 # Default 1 hour
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
env_file = ".env"
|
||||||
|
extra = "ignore" # Allow extra fields from environment
|
||||||
|
|
||||||
|
class S3Service:
|
||||||
|
def __init__(self):
|
||||||
|
self.settings = Settings()
|
||||||
|
self.bucket_name = self.settings.AWS_BUCKET_NAME
|
||||||
|
self.s3 = boto3.client(
|
||||||
|
's3',
|
||||||
|
region_name=self.settings.AWS_REGION,
|
||||||
|
aws_access_key_id=self.settings.AWS_ACCESS_KEY,
|
||||||
|
aws_secret_access_key=self.settings.AWS_SECRET_KEY,
|
||||||
|
config=Config(signature_version='s3v4')
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_s3_service():
|
||||||
|
return S3Service()
|
||||||
|
|
||||||
|
async def upload_file(
|
||||||
|
user_id: str,
|
||||||
|
folder: S3FolderNameEnum,
|
||||||
|
file_name: str,
|
||||||
|
clinic_id: Optional[str] = None
|
||||||
|
) -> Dict[str, str]:
|
||||||
|
"""
|
||||||
|
Generate a pre-signed URL for uploading a file to S3.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
user_id: The ID of the user
|
||||||
|
folder: The folder enum to store the file in
|
||||||
|
file_name: The name of the file
|
||||||
|
clinic_id: Optional design ID for assets
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict containing the URLs and key information
|
||||||
|
"""
|
||||||
|
s3_service = get_s3_service()
|
||||||
|
if folder == S3FolderNameEnum.ASSETS and not clinic_id:
|
||||||
|
raise BusinessValidationException("Clinic id is required!")
|
||||||
|
|
||||||
|
if folder != S3FolderNameEnum.PROFILE and not user_id:
|
||||||
|
raise BusinessValidationException("User id is required!")
|
||||||
|
|
||||||
|
timestamp = int(datetime.now().timestamp() * 1000)
|
||||||
|
|
||||||
|
if folder == S3FolderNameEnum.PROFILE:
|
||||||
|
key = f"common/{S3FolderNameEnum.PROFILE.value}/{user_id}/{timestamp}_{file_name}"
|
||||||
|
else:
|
||||||
|
key = f"common/{S3FolderNameEnum.ASSETS.value}/clinic/{clinic_id}/{timestamp}_{file_name}"
|
||||||
|
|
||||||
|
try:
|
||||||
|
put_url = s3_service.s3.generate_presigned_url(
|
||||||
|
ClientMethod='put_object',
|
||||||
|
Params={
|
||||||
|
'Bucket': s3_service.bucket_name,
|
||||||
|
'Key': key,
|
||||||
|
},
|
||||||
|
ExpiresIn=s3_service.settings.AWS_S3_EXPIRES
|
||||||
|
)
|
||||||
|
|
||||||
|
get_url = s3_service.s3.generate_presigned_url(
|
||||||
|
ClientMethod='get_object',
|
||||||
|
Params={
|
||||||
|
'Bucket': s3_service.bucket_name,
|
||||||
|
'Key': key,
|
||||||
|
},
|
||||||
|
ExpiresIn=s3_service.settings.AWS_S3_EXPIRES
|
||||||
|
)
|
||||||
|
|
||||||
|
url = urlparse(put_url)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"api_url": put_url,
|
||||||
|
"key": key,
|
||||||
|
"location": f"{url.scheme}://{url.netloc}/{key}",
|
||||||
|
"get_url": get_url,
|
||||||
|
}
|
||||||
|
except ClientError as e:
|
||||||
|
print(f"Error generating pre-signed URL: {e}")
|
||||||
|
raise BusinessValidationException(str(e))
|
||||||
|
|
||||||
|
async def get_signed_url(key: str) -> str:
|
||||||
|
"""
|
||||||
|
Generate a pre-signed URL for retrieving a file from S3.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
key: The key of the file in S3
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The pre-signed URL for getting the object
|
||||||
|
"""
|
||||||
|
s3_service = get_s3_service()
|
||||||
|
try:
|
||||||
|
url = s3_service.s3.generate_presigned_url(
|
||||||
|
ClientMethod='get_object',
|
||||||
|
Params={
|
||||||
|
'Bucket': s3_service.bucket_name,
|
||||||
|
'Key': key,
|
||||||
|
},
|
||||||
|
ExpiresIn=3600 # 1 hour
|
||||||
|
)
|
||||||
|
return url
|
||||||
|
except ClientError as e:
|
||||||
|
print(f"Error in get_signed_url: {e}")
|
||||||
|
raise BusinessValidationException(str(e))
|
||||||
|
|
||||||
|
def get_file_key(url: str) -> str:
|
||||||
|
"""
|
||||||
|
Extract the file key from a URL or return the key if already provided.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
url: The URL or key
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The file key
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
if not url.startswith("http://") and not url.startswith("https://"):
|
||||||
|
return url
|
||||||
|
|
||||||
|
parsed_url = urlparse(url)
|
||||||
|
return parsed_url.path.lstrip('/')
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error in get_file_key: {e}")
|
||||||
|
raise BusinessValidationException(str(e))
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
from sqlalchemy.exc import IntegrityError
|
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
from sqlalchemy import or_
|
|
||||||
|
|
||||||
from database import get_db
|
from database import get_db
|
||||||
from models.Users import Users
|
from models.Users import Users
|
||||||
from exceptions.validation_exception import ValidationException
|
from exceptions.validation_exception import ValidationException
|
||||||
from schemas.ResponseSchemas import UserResponse
|
from schemas.ResponseSchemas import UserResponse
|
||||||
|
from models import Clinics
|
||||||
|
from enums.enums import ClinicStatus
|
||||||
from utils.password_utils import hash_password
|
from utils.password_utils import hash_password
|
||||||
from schemas.CreateSchemas import UserCreate
|
from schemas.CreateSchemas import UserCreate
|
||||||
from exceptions.resource_not_found_exception import ResourceNotFoundException
|
from exceptions.resource_not_found_exception import ResourceNotFoundException
|
||||||
|
|
@ -17,11 +17,13 @@ class UserServices:
|
||||||
self.db: Session = next(get_db())
|
self.db: Session = next(get_db())
|
||||||
|
|
||||||
async def create_user(self, user_data: UserCreate):
|
async def create_user(self, user_data: UserCreate):
|
||||||
|
# Start a transaction
|
||||||
try:
|
try:
|
||||||
|
user = user_data.user
|
||||||
# Check if user with same username or email exists
|
# Check if user with same username or email exists
|
||||||
existing_user = (
|
existing_user = (
|
||||||
self.db.query(Users)
|
self.db.query(Users)
|
||||||
.filter(Users.email == user_data.email.lower())
|
.filter(Users.email == user.email.lower())
|
||||||
.first()
|
.first()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -32,29 +34,72 @@ class UserServices:
|
||||||
|
|
||||||
# Create a new user instance
|
# Create a new user instance
|
||||||
new_user = Users(
|
new_user = Users(
|
||||||
username=user_data.username,
|
username=user.username,
|
||||||
email=user_data.email.lower(),
|
email=user.email.lower(),
|
||||||
password=hash_password(user_data.password),
|
password=hash_password(user.password),
|
||||||
clinicRole=user_data.clinicRole,
|
clinicRole=user.clinicRole,
|
||||||
userType=user_data.userType,
|
userType=user.userType,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Add to database and commit
|
# Add user to database but don't commit yet
|
||||||
self.db.add(new_user)
|
self.db.add(new_user)
|
||||||
|
|
||||||
|
# Get clinic data
|
||||||
|
clinic = user_data.clinic
|
||||||
|
|
||||||
|
# cross verify domain, in db
|
||||||
|
# Convert to lowercase and keep only alphabetic characters, hyphens, and underscores
|
||||||
|
domain = ''.join(char for char in clinic.name.lower() if char.isalpha() or char == '-' or char == '_')
|
||||||
|
existing_clinic = self.db.query(Clinics).filter(Clinics.domain == domain).first()
|
||||||
|
|
||||||
|
if existing_clinic:
|
||||||
|
# This will trigger rollback in the exception handler
|
||||||
|
raise ValidationException("Clinic with same domain already exists")
|
||||||
|
|
||||||
|
# Create clinic instance
|
||||||
|
new_clinic = Clinics(
|
||||||
|
name=clinic.name,
|
||||||
|
address=clinic.address,
|
||||||
|
phone=clinic.phone,
|
||||||
|
email=clinic.email,
|
||||||
|
integration=clinic.integration,
|
||||||
|
pms_id=clinic.pms_id,
|
||||||
|
practice_name=clinic.practice_name,
|
||||||
|
logo=clinic.logo,
|
||||||
|
country=clinic.country,
|
||||||
|
postal_code=clinic.postal_code,
|
||||||
|
city=clinic.city,
|
||||||
|
state=clinic.state,
|
||||||
|
abn_doc=clinic.abn_doc,
|
||||||
|
abn_number=clinic.abn_number,
|
||||||
|
contract_doc=clinic.contract_doc,
|
||||||
|
clinic_phone=clinic.clinic_phone,
|
||||||
|
is_clinic_phone_enabled=clinic.is_clinic_phone_enabled,
|
||||||
|
other_info=clinic.other_info,
|
||||||
|
greeting_msg=clinic.greeting_msg,
|
||||||
|
voice_model=clinic.voice_model,
|
||||||
|
voice_model_provider=clinic.voice_model_provider,
|
||||||
|
voice_model_gender=clinic.voice_model_gender,
|
||||||
|
scenarios=clinic.scenarios,
|
||||||
|
general_info=clinic.general_info,
|
||||||
|
status=ClinicStatus.UNDER_REVIEW,
|
||||||
|
domain=domain,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Add clinic to database
|
||||||
|
self.db.add(new_clinic)
|
||||||
|
|
||||||
|
# Now commit both user and clinic in a single transaction
|
||||||
self.db.commit()
|
self.db.commit()
|
||||||
self.db.refresh(new_user)
|
|
||||||
|
|
||||||
user_dict = new_user.__dict__.copy()
|
return
|
||||||
|
|
||||||
user_response = UserResponse(**user_dict).model_dump()
|
|
||||||
|
|
||||||
return user_response
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error("Error creating user", e)
|
logger.error(f"Error creating user: {str(e)}")
|
||||||
|
# Rollback the transaction if any error occurs
|
||||||
self.db.rollback()
|
self.db.rollback()
|
||||||
if e.__class__ == ValidationException:
|
if isinstance(e, ValidationException):
|
||||||
raise ValidationException(e.message)
|
raise ValidationException(e.message)
|
||||||
if e.__class__ == ResourceNotFoundException:
|
if isinstance(e, ResourceNotFoundException):
|
||||||
raise ResourceNotFoundException(e.message)
|
raise ResourceNotFoundException(e.message)
|
||||||
raise e
|
raise e
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue