feat:Added leave/assets
Browse files- .gitattributes +35 -0
- Dockerfile +11 -0
- README.md +10 -1
- alembic/versions/b33e3b5b7af9_added_roles.py +33 -0
- src/assets/feed_db_script.py +132 -0
- src/assets/router.py +30 -0
- src/assets/schemas.py +31 -0
- src/assets/service.py +11 -0
- src/auth/router.py +18 -17
- src/auth/service.py +30 -23
- src/auth/utils.py +72 -22
- src/leave/router.py +91 -0
- src/leave/schemas.py +0 -0
- src/leave/utils.py +50 -0
- src/main.py +9 -0
- src/profile/router.py +20 -0
- src/profile/schemas.py +11 -0
- src/profile/service.py +65 -2
.gitattributes
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
*.7z filter=lfs diff=lfs merge=lfs -text
|
| 2 |
+
*.arrow filter=lfs diff=lfs merge=lfs -text
|
| 3 |
+
*.bin filter=lfs diff=lfs merge=lfs -text
|
| 4 |
+
*.bz2 filter=lfs diff=lfs merge=lfs -text
|
| 5 |
+
*.ckpt filter=lfs diff=lfs merge=lfs -text
|
| 6 |
+
*.ftz filter=lfs diff=lfs merge=lfs -text
|
| 7 |
+
*.gz filter=lfs diff=lfs merge=lfs -text
|
| 8 |
+
*.h5 filter=lfs diff=lfs merge=lfs -text
|
| 9 |
+
*.joblib filter=lfs diff=lfs merge=lfs -text
|
| 10 |
+
*.lfs.* filter=lfs diff=lfs merge=lfs -text
|
| 11 |
+
*.mlmodel filter=lfs diff=lfs merge=lfs -text
|
| 12 |
+
*.model filter=lfs diff=lfs merge=lfs -text
|
| 13 |
+
*.msgpack filter=lfs diff=lfs merge=lfs -text
|
| 14 |
+
*.npy filter=lfs diff=lfs merge=lfs -text
|
| 15 |
+
*.npz filter=lfs diff=lfs merge=lfs -text
|
| 16 |
+
*.onnx filter=lfs diff=lfs merge=lfs -text
|
| 17 |
+
*.ot filter=lfs diff=lfs merge=lfs -text
|
| 18 |
+
*.parquet filter=lfs diff=lfs merge=lfs -text
|
| 19 |
+
*.pb filter=lfs diff=lfs merge=lfs -text
|
| 20 |
+
*.pickle filter=lfs diff=lfs merge=lfs -text
|
| 21 |
+
*.pkl filter=lfs diff=lfs merge=lfs -text
|
| 22 |
+
*.pt filter=lfs diff=lfs merge=lfs -text
|
| 23 |
+
*.pth filter=lfs diff=lfs merge=lfs -text
|
| 24 |
+
*.rar filter=lfs diff=lfs merge=lfs -text
|
| 25 |
+
*.safetensors filter=lfs diff=lfs merge=lfs -text
|
| 26 |
+
saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
| 27 |
+
*.tar.* filter=lfs diff=lfs merge=lfs -text
|
| 28 |
+
*.tar filter=lfs diff=lfs merge=lfs -text
|
| 29 |
+
*.tflite filter=lfs diff=lfs merge=lfs -text
|
| 30 |
+
*.tgz filter=lfs diff=lfs merge=lfs -text
|
| 31 |
+
*.wasm filter=lfs diff=lfs merge=lfs -text
|
| 32 |
+
*.xz filter=lfs diff=lfs merge=lfs -text
|
| 33 |
+
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
+
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
+
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
Dockerfile
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.10
|
| 2 |
+
|
| 3 |
+
WORKDIR /app
|
| 4 |
+
|
| 5 |
+
COPY . .
|
| 6 |
+
|
| 7 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 8 |
+
|
| 9 |
+
EXPOSE 7860
|
| 10 |
+
|
| 11 |
+
CMD ["uvicorn", "src.main:app", "--host", "0.0.0.0", "--port", "7860"]
|
README.md
CHANGED
|
@@ -1 +1,10 @@
|
|
| 1 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: Yuvabe Backend App
|
| 3 |
+
emoji: 👁
|
| 4 |
+
colorFrom: pink
|
| 5 |
+
colorTo: red
|
| 6 |
+
sdk: docker
|
| 7 |
+
pinned: false
|
| 8 |
+
---
|
| 9 |
+
|
| 10 |
+
Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
|
alembic/versions/b33e3b5b7af9_added_roles.py
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Added roles
|
| 2 |
+
|
| 3 |
+
Revision ID: b33e3b5b7af9
|
| 4 |
+
Revises: e8066533b622
|
| 5 |
+
Create Date: 2025-11-16 21:10:02.038255
|
| 6 |
+
|
| 7 |
+
"""
|
| 8 |
+
from typing import Sequence, Union
|
| 9 |
+
|
| 10 |
+
from alembic import op
|
| 11 |
+
import sqlalchemy as sa
|
| 12 |
+
import sqlmodel.sql.sqltypes
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
# revision identifiers, used by Alembic.
|
| 16 |
+
revision: str = 'b33e3b5b7af9'
|
| 17 |
+
down_revision: Union[str, Sequence[str], None] = 'e8066533b622'
|
| 18 |
+
branch_labels: Union[str, Sequence[str], None] = None
|
| 19 |
+
depends_on: Union[str, Sequence[str], None] = None
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
def upgrade() -> None:
|
| 23 |
+
"""Upgrade schema."""
|
| 24 |
+
# ### commands auto generated by Alembic - please adjust! ###
|
| 25 |
+
pass
|
| 26 |
+
# ### end Alembic commands ###
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
def downgrade() -> None:
|
| 30 |
+
"""Downgrade schema."""
|
| 31 |
+
# ### commands auto generated by Alembic - please adjust! ###
|
| 32 |
+
pass
|
| 33 |
+
# ### end Alembic commands ###
|
src/assets/feed_db_script.py
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from src.auth.utils import hash_password
|
| 2 |
+
from datetime import date
|
| 3 |
+
from sqlmodel import Session
|
| 4 |
+
|
| 5 |
+
from src.core.database import engine
|
| 6 |
+
from src.core.models import Users, Teams, Roles, UserTeamsRole
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
# ------------------------
|
| 10 |
+
# 1. Seed Users
|
| 11 |
+
# ------------------------
|
| 12 |
+
def seed_users(session: Session):
|
| 13 |
+
users = [
|
| 14 |
+
Users(
|
| 15 |
+
email_id="[email protected]",
|
| 16 |
+
password=hash_password("Yuvabe"),
|
| 17 |
+
user_name="ragul",
|
| 18 |
+
dob=date(2001, 5, 21),
|
| 19 |
+
address="Chennai",
|
| 20 |
+
profile_picture="ragul.png",
|
| 21 |
+
),
|
| 22 |
+
Users(
|
| 23 |
+
email_id="[email protected]",
|
| 24 |
+
password=hash_password("Yuvabe"),
|
| 25 |
+
user_name="Shri",
|
| 26 |
+
dob=date(1999, 3, 14),
|
| 27 |
+
address="Chennai",
|
| 28 |
+
profile_picture="shri.png",
|
| 29 |
+
),
|
| 30 |
+
Users(
|
| 31 |
+
email_id="[email protected]",
|
| 32 |
+
password=hash_password("Yuvabe"),
|
| 33 |
+
user_name="Sathish",
|
| 34 |
+
dob=date(1998, 7, 10),
|
| 35 |
+
address="Chennai",
|
| 36 |
+
profile_picture="Sathish.png",
|
| 37 |
+
),
|
| 38 |
+
Users(
|
| 39 |
+
email_id="[email protected]",
|
| 40 |
+
password=hash_password("Yuvabe"),
|
| 41 |
+
user_name="Deepika",
|
| 42 |
+
dob=date(1997, 2, 5),
|
| 43 |
+
address="Chennai",
|
| 44 |
+
profile_picture="deepika.png",
|
| 45 |
+
),
|
| 46 |
+
]
|
| 47 |
+
|
| 48 |
+
session.add_all(users)
|
| 49 |
+
session.commit()
|
| 50 |
+
print("Users added.")
|
| 51 |
+
return users
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
# ------------------------
|
| 55 |
+
# 2. Seed Teams
|
| 56 |
+
# ------------------------
|
| 57 |
+
def seed_teams(session: Session):
|
| 58 |
+
teams = [
|
| 59 |
+
Teams(name="Tech Team"),
|
| 60 |
+
Teams(name="HR Team"),
|
| 61 |
+
]
|
| 62 |
+
session.add_all(teams)
|
| 63 |
+
session.commit()
|
| 64 |
+
print("Teams added.")
|
| 65 |
+
return teams
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
# ------------------------
|
| 69 |
+
# 3. Seed Roles
|
| 70 |
+
# ------------------------
|
| 71 |
+
def seed_roles(session: Session):
|
| 72 |
+
roles = [
|
| 73 |
+
Roles(name="Developer"),
|
| 74 |
+
Roles(name="Team Lead"),
|
| 75 |
+
Roles(name="HR Manager"),
|
| 76 |
+
]
|
| 77 |
+
session.add_all(roles)
|
| 78 |
+
session.commit()
|
| 79 |
+
print("Roles added.")
|
| 80 |
+
return roles
|
| 81 |
+
|
| 82 |
+
|
| 83 |
+
# ------------------------
|
| 84 |
+
# 4. Map Users → Teams → Roles
|
| 85 |
+
# ------------------------
|
| 86 |
+
def seed_user_teams_roles(session: Session, users, teams, roles):
|
| 87 |
+
mappings = [
|
| 88 |
+
# Hari → Tech Team → Developer
|
| 89 |
+
UserTeamsRole(
|
| 90 |
+
user_id=users[0].id, # Hari
|
| 91 |
+
team_id=teams[0].id, # Tech Team
|
| 92 |
+
role_id=roles[0].id, # Developer
|
| 93 |
+
),
|
| 94 |
+
# Shri → Tech Team → Team Lead
|
| 95 |
+
UserTeamsRole(
|
| 96 |
+
user_id=users[1].id, # Shri
|
| 97 |
+
team_id=teams[0].id, # Tech Team
|
| 98 |
+
role_id=roles[1].id, # Team Lead
|
| 99 |
+
),
|
| 100 |
+
# HR Keerthana
|
| 101 |
+
UserTeamsRole(
|
| 102 |
+
user_id=users[2].id, # Keerthana
|
| 103 |
+
team_id=teams[1].id, # HR Team
|
| 104 |
+
role_id=roles[2].id, # HR Manager
|
| 105 |
+
),
|
| 106 |
+
# HR Deepika
|
| 107 |
+
UserTeamsRole(
|
| 108 |
+
user_id=users[3].id, # Deepika
|
| 109 |
+
team_id=teams[1].id, # HR Team
|
| 110 |
+
role_id=roles[2].id, # HR Manager
|
| 111 |
+
),
|
| 112 |
+
]
|
| 113 |
+
|
| 114 |
+
session.add_all(mappings)
|
| 115 |
+
session.commit()
|
| 116 |
+
print("User-Team-Role mappings added.")
|
| 117 |
+
|
| 118 |
+
|
| 119 |
+
# ------------------------
|
| 120 |
+
# 5. Master Runner
|
| 121 |
+
# ------------------------
|
| 122 |
+
def run_all_seeds():
|
| 123 |
+
with Session(engine) as session:
|
| 124 |
+
users = seed_users(session)
|
| 125 |
+
teams = seed_teams(session)
|
| 126 |
+
roles = seed_roles(session)
|
| 127 |
+
seed_user_teams_roles(session, users, teams, roles)
|
| 128 |
+
print("All data seeded successfully!")
|
| 129 |
+
|
| 130 |
+
|
| 131 |
+
if __name__ == "__main__":
|
| 132 |
+
run_all_seeds()
|
src/assets/router.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from fastapi import APIRouter, Depends
|
| 2 |
+
from sqlmodel.ext.asyncio.session import AsyncSession
|
| 3 |
+
from src.core.database import get_async_session
|
| 4 |
+
from src.auth.utils import get_current_user
|
| 5 |
+
from src.assets.schemas import BaseResponse
|
| 6 |
+
from src.assets.service import list_user_assets
|
| 7 |
+
|
| 8 |
+
router = APIRouter(prefix="/assets", tags=["Assets"])
|
| 9 |
+
|
| 10 |
+
@router.get("/", response_model=BaseResponse)
|
| 11 |
+
async def get_assets(
|
| 12 |
+
user_id: str = Depends(get_current_user),
|
| 13 |
+
session: AsyncSession = Depends(get_async_session)
|
| 14 |
+
):
|
| 15 |
+
assets = await list_user_assets(session, user_id)
|
| 16 |
+
|
| 17 |
+
data = {
|
| 18 |
+
"assets": [
|
| 19 |
+
{
|
| 20 |
+
"id": a.id,
|
| 21 |
+
"name": a.name,
|
| 22 |
+
"type": a.type,
|
| 23 |
+
"status": a.status,
|
| 24 |
+
}
|
| 25 |
+
for a in assets
|
| 26 |
+
]
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
return {"code": 200, "data": data}
|
| 30 |
+
|
src/assets/schemas.py
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import Optional
|
| 2 |
+
from pydantic import BaseModel
|
| 3 |
+
import uuid
|
| 4 |
+
from enum import Enum
|
| 5 |
+
|
| 6 |
+
class AssetStatus(str, Enum):
|
| 7 |
+
ACTIVE = "Active"
|
| 8 |
+
UNAVAILABLE = "Unavailable"
|
| 9 |
+
ON_REQUEST = "On Request"
|
| 10 |
+
IN_SERVICE = "In Service"
|
| 11 |
+
|
| 12 |
+
class AssetCreateRequest(BaseModel):
|
| 13 |
+
name: str
|
| 14 |
+
type: str
|
| 15 |
+
status: Optional[AssetStatus] = AssetStatus.UNAVAILABLE
|
| 16 |
+
|
| 17 |
+
class AssetUpdateRequest(BaseModel):
|
| 18 |
+
name: Optional[str] = None
|
| 19 |
+
type: Optional[str] = None
|
| 20 |
+
status: Optional[AssetStatus] = None
|
| 21 |
+
|
| 22 |
+
class AssetResponse(BaseModel):
|
| 23 |
+
id: uuid.UUID
|
| 24 |
+
user_id: uuid.UUID
|
| 25 |
+
name: str
|
| 26 |
+
type: str
|
| 27 |
+
status: AssetStatus
|
| 28 |
+
|
| 29 |
+
class BaseResponse(BaseModel):
|
| 30 |
+
code: int
|
| 31 |
+
data: dict
|
src/assets/service.py
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import uuid
|
| 2 |
+
from typing import List
|
| 3 |
+
from sqlmodel import select
|
| 4 |
+
from sqlmodel.ext.asyncio.session import AsyncSession
|
| 5 |
+
from src.core.models import Assets
|
| 6 |
+
|
| 7 |
+
async def list_user_assets(session: AsyncSession, user_id: str) -> List[Assets]:
|
| 8 |
+
q = await session.exec(
|
| 9 |
+
select(Assets).where(Assets.user_id == uuid.UUID(user_id))
|
| 10 |
+
)
|
| 11 |
+
return q.all()
|
src/auth/router.py
CHANGED
|
@@ -8,7 +8,6 @@ from sqlmodel.ext.asyncio.session import AsyncSession
|
|
| 8 |
from src.auth.service import (
|
| 9 |
create_user,
|
| 10 |
verify_email,
|
| 11 |
-
send_verification_link,
|
| 12 |
login_user,
|
| 13 |
)
|
| 14 |
from src.auth.utils import get_current_user
|
|
@@ -37,26 +36,26 @@ async def signup(
|
|
| 37 |
raise HTTPException(status_code=400, detail=str(e))
|
| 38 |
|
| 39 |
|
| 40 |
-
@router.post("/send-verification", response_model=BaseResponse)
|
| 41 |
-
async def send_verification(
|
| 42 |
-
|
| 43 |
-
):
|
| 44 |
-
|
| 45 |
-
|
| 46 |
|
| 47 |
-
|
| 48 |
-
|
| 49 |
|
| 50 |
|
| 51 |
-
@router.get("/verify-email", response_model=BaseResponse)
|
| 52 |
-
async def verify_email_route(
|
| 53 |
-
|
| 54 |
-
):
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
|
| 59 |
-
|
| 60 |
|
| 61 |
|
| 62 |
@router.post("/login", response_model=BaseResponse)
|
|
@@ -115,6 +114,8 @@ async def get_home(
|
|
| 115 |
"name": user.user_name,
|
| 116 |
"email": user.email_id,
|
| 117 |
"is_verified": user.is_verified,
|
|
|
|
|
|
|
| 118 |
},
|
| 119 |
"home_data": {
|
| 120 |
"announcements": ["Welcome!", "New protocol released"],
|
|
|
|
| 8 |
from src.auth.service import (
|
| 9 |
create_user,
|
| 10 |
verify_email,
|
|
|
|
| 11 |
login_user,
|
| 12 |
)
|
| 13 |
from src.auth.utils import get_current_user
|
|
|
|
| 36 |
raise HTTPException(status_code=400, detail=str(e))
|
| 37 |
|
| 38 |
|
| 39 |
+
# @router.post("/send-verification", response_model=BaseResponse)
|
| 40 |
+
# async def send_verification(
|
| 41 |
+
# payload: SendVerificationRequest, session: AsyncSession = Depends(get_async_session)
|
| 42 |
+
# ):
|
| 43 |
+
# if not payload.email:
|
| 44 |
+
# raise HTTPException(status_code=400, detail="Email is required")
|
| 45 |
|
| 46 |
+
# response = await send_verification_link(session, payload.email)
|
| 47 |
+
# return {"code": 200, "data": response}
|
| 48 |
|
| 49 |
|
| 50 |
+
# @router.get("/verify-email", response_model=BaseResponse)
|
| 51 |
+
# async def verify_email_route(
|
| 52 |
+
# token: str, session: AsyncSession = Depends(get_async_session)
|
| 53 |
+
# ):
|
| 54 |
+
# response = await verify_email(session, token)
|
| 55 |
+
# access_token = response["access_token"]
|
| 56 |
+
# redirect_url = f"yuvabe://verified?token={access_token}"
|
| 57 |
|
| 58 |
+
# return RedirectResponse(url=redirect_url)
|
| 59 |
|
| 60 |
|
| 61 |
@router.post("/login", response_model=BaseResponse)
|
|
|
|
| 114 |
"name": user.user_name,
|
| 115 |
"email": user.email_id,
|
| 116 |
"is_verified": user.is_verified,
|
| 117 |
+
"dob": user.dob.isoformat() if user.dob else None,
|
| 118 |
+
"profile_picture": user.profile_picture
|
| 119 |
},
|
| 120 |
"home_data": {
|
| 121 |
"announcements": ["Welcome!", "New protocol released"],
|
src/auth/service.py
CHANGED
|
@@ -6,7 +6,6 @@ from src.auth.utils import (
|
|
| 6 |
verify_verification_token,
|
| 7 |
create_access_token,
|
| 8 |
hash_password,
|
| 9 |
-
send_verification_email,
|
| 10 |
create_verification_token,
|
| 11 |
)
|
| 12 |
from src.core.models import Users
|
|
@@ -17,6 +16,10 @@ from sqlmodel.ext.asyncio.session import AsyncSession
|
|
| 17 |
|
| 18 |
async def create_user(session: AsyncSession, name: str, email: str, password: str):
|
| 19 |
"""Create user without sending email"""
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
user = await session.exec(select(Users).where(Users.email_id == email))
|
| 21 |
existing_user = user.first()
|
| 22 |
if existing_user:
|
|
@@ -26,7 +29,7 @@ async def create_user(session: AsyncSession, name: str, email: str, password: st
|
|
| 26 |
user_name=name,
|
| 27 |
email_id=email,
|
| 28 |
password=hash_password(password),
|
| 29 |
-
is_verified=
|
| 30 |
)
|
| 31 |
|
| 32 |
session.add(new_user)
|
|
@@ -57,32 +60,32 @@ async def create_user(session: AsyncSession, name: str, email: str, password: st
|
|
| 57 |
}
|
| 58 |
|
| 59 |
|
| 60 |
-
async def send_verification_link(session: Session, email: str):
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
|
| 65 |
-
|
| 66 |
-
|
| 67 |
|
| 68 |
-
|
| 69 |
-
|
| 70 |
|
| 71 |
-
|
| 72 |
-
|
| 73 |
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
|
| 87 |
|
| 88 |
async def verify_email(session: Session, token: str):
|
|
@@ -116,6 +119,10 @@ async def verify_email(session: Session, token: str):
|
|
| 116 |
|
| 117 |
|
| 118 |
async def login_user(session: Session, email: str, password: str):
|
|
|
|
|
|
|
|
|
|
|
|
|
| 119 |
users = await session.exec(select(Users).where(Users.email_id == email))
|
| 120 |
user = users.first()
|
| 121 |
|
|
|
|
| 6 |
verify_verification_token,
|
| 7 |
create_access_token,
|
| 8 |
hash_password,
|
|
|
|
| 9 |
create_verification_token,
|
| 10 |
)
|
| 11 |
from src.core.models import Users
|
|
|
|
| 16 |
|
| 17 |
async def create_user(session: AsyncSession, name: str, email: str, password: str):
|
| 18 |
"""Create user without sending email"""
|
| 19 |
+
|
| 20 |
+
if not email.lower().endswith("@yuvabe.com"):
|
| 21 |
+
raise HTTPException(status_code=400, detail="Enter you're Yuvabe email ID")
|
| 22 |
+
|
| 23 |
user = await session.exec(select(Users).where(Users.email_id == email))
|
| 24 |
existing_user = user.first()
|
| 25 |
if existing_user:
|
|
|
|
| 29 |
user_name=name,
|
| 30 |
email_id=email,
|
| 31 |
password=hash_password(password),
|
| 32 |
+
is_verified=True,
|
| 33 |
)
|
| 34 |
|
| 35 |
session.add(new_user)
|
|
|
|
| 60 |
}
|
| 61 |
|
| 62 |
|
| 63 |
+
# async def send_verification_link(session: Session, email: str):
|
| 64 |
+
# """Send verification email for an existing user."""
|
| 65 |
+
# result = await session.exec(select(Users).where(Users.email_id == email))
|
| 66 |
+
# user = result.first()
|
| 67 |
|
| 68 |
+
# if not user:
|
| 69 |
+
# raise HTTPException(status_code=404, detail="User not found")
|
| 70 |
|
| 71 |
+
# if user.is_verified:
|
| 72 |
+
# raise HTTPException(status_code=400, detail="User is already verified")
|
| 73 |
|
| 74 |
+
# # Create a token using existing user ID (opaque token)
|
| 75 |
+
# token = create_verification_token(str(user.id))
|
| 76 |
|
| 77 |
+
# try:
|
| 78 |
+
# send_verification_email(email, token)
|
| 79 |
+
# except Exception as e:
|
| 80 |
+
# raise HTTPException(
|
| 81 |
+
# status_code=500, detail=f"Failed to send verification email: {str(e)}"
|
| 82 |
+
# )
|
| 83 |
|
| 84 |
+
# return {
|
| 85 |
+
# "message": "Verification link sent successfully",
|
| 86 |
+
# "user_id": str(user.id),
|
| 87 |
+
# "email": user.email_id,
|
| 88 |
+
# }
|
| 89 |
|
| 90 |
|
| 91 |
async def verify_email(session: Session, token: str):
|
|
|
|
| 119 |
|
| 120 |
|
| 121 |
async def login_user(session: Session, email: str, password: str):
|
| 122 |
+
|
| 123 |
+
if not email.lower().endswith("@yuvabe.com"):
|
| 124 |
+
raise HTTPException(status_code=400, detail="Enter you're Yuvabe email ID")
|
| 125 |
+
|
| 126 |
users = await session.exec(select(Users).where(Users.email_id == email))
|
| 127 |
user = users.first()
|
| 128 |
|
src/auth/utils.py
CHANGED
|
@@ -3,6 +3,8 @@ import smtplib
|
|
| 3 |
import os
|
| 4 |
import uuid
|
| 5 |
from email.mime.text import MIMEText
|
|
|
|
|
|
|
| 6 |
from passlib.context import CryptContext
|
| 7 |
from src.core.database import get_async_session
|
| 8 |
from sqlmodel.ext.asyncio.session import AsyncSession
|
|
@@ -18,6 +20,7 @@ from src.core.config import settings
|
|
| 18 |
SECRET_KEY = settings.SECRET_KEY
|
| 19 |
ALGORITHM = settings.JWT_ALGORITHM
|
| 20 |
ACCESS_TOKEN_EXPIRE_MINUTES = settings.JWT_EXPIRE
|
|
|
|
| 21 |
|
| 22 |
SMTP_SERVER = settings.EMAIL_SERVER
|
| 23 |
SMTP_PORT = settings.EMAIL_PORT
|
|
@@ -50,28 +53,75 @@ def create_access_token(data: dict):
|
|
| 50 |
return encoded_jwt
|
| 51 |
|
| 52 |
|
| 53 |
-
def send_verification_email(to_email: str, token: str):
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 75 |
|
| 76 |
|
| 77 |
fernet = Fernet(FERNET_KEY.encode())
|
|
|
|
| 3 |
import os
|
| 4 |
import uuid
|
| 5 |
from email.mime.text import MIMEText
|
| 6 |
+
import logging
|
| 7 |
+
import traceback
|
| 8 |
from passlib.context import CryptContext
|
| 9 |
from src.core.database import get_async_session
|
| 10 |
from sqlmodel.ext.asyncio.session import AsyncSession
|
|
|
|
| 20 |
SECRET_KEY = settings.SECRET_KEY
|
| 21 |
ALGORITHM = settings.JWT_ALGORITHM
|
| 22 |
ACCESS_TOKEN_EXPIRE_MINUTES = settings.JWT_EXPIRE
|
| 23 |
+
logger = logging.getLogger(__name__)
|
| 24 |
|
| 25 |
SMTP_SERVER = settings.EMAIL_SERVER
|
| 26 |
SMTP_PORT = settings.EMAIL_PORT
|
|
|
|
| 53 |
return encoded_jwt
|
| 54 |
|
| 55 |
|
| 56 |
+
# def send_verification_email(to_email: str, token: str):
|
| 57 |
+
# """Send verification email using smtplib with detailed debug logs."""
|
| 58 |
+
# subject = f"Verify your {settings.APP_NAME} Account"
|
| 59 |
+
# verification_link = f"{VERIFICATION_BASE_URL}/auth/verify-email?token={token}"
|
| 60 |
+
|
| 61 |
+
# body = f"""
|
| 62 |
+
# Hi,
|
| 63 |
+
|
| 64 |
+
# Please verify your {settings.APP_NAME} account by clicking the link below:
|
| 65 |
+
# {verification_link}
|
| 66 |
+
|
| 67 |
+
# This link will expire in 24 hours.
|
| 68 |
+
|
| 69 |
+
# Regards,
|
| 70 |
+
# {settings.APP_NAME} Team
|
| 71 |
+
# """
|
| 72 |
+
|
| 73 |
+
# msg = MIMEText(body)
|
| 74 |
+
# msg["Subject"] = subject
|
| 75 |
+
# msg["From"] = SMTP_EMAIL
|
| 76 |
+
# msg["To"] = to_email
|
| 77 |
+
|
| 78 |
+
# logger.info("🟢 Starting send_verification_email()")
|
| 79 |
+
# logger.info(f"📨 To: {to_email}")
|
| 80 |
+
# logger.info(f"📤 SMTP Server: {SMTP_SERVER}:{SMTP_PORT}")
|
| 81 |
+
|
| 82 |
+
# try:
|
| 83 |
+
# logger.info("🔌 Connecting to SMTP server...")
|
| 84 |
+
# with smtplib.SMTP(SMTP_SERVER, SMTP_PORT, timeout=30) as server:
|
| 85 |
+
# logger.info("✅ Connected successfully.")
|
| 86 |
+
|
| 87 |
+
# logger.info("🔒 Starting TLS...")
|
| 88 |
+
# server.starttls()
|
| 89 |
+
# logger.info("✅ TLS secured.")
|
| 90 |
+
|
| 91 |
+
# logger.info("🔑 Logging in to SMTP server...")
|
| 92 |
+
# server.login(SMTP_EMAIL, SMTP_PASSWORD)
|
| 93 |
+
# logger.info("✅ Logged in successfully.")
|
| 94 |
+
|
| 95 |
+
# # Send email
|
| 96 |
+
# logger.info("📧 Sending email message...")
|
| 97 |
+
# server.send_message(msg)
|
| 98 |
+
# logger.info(f"✅ Email successfully sent to {to_email}")
|
| 99 |
+
|
| 100 |
+
# except smtplib.SMTPAuthenticationError as e:
|
| 101 |
+
# logger.error("❌ Authentication failed — check email or app password.")
|
| 102 |
+
# logger.error(f"Error details: {e}")
|
| 103 |
+
# logger.error(traceback.format_exc())
|
| 104 |
+
# raise
|
| 105 |
+
# except smtplib.SMTPConnectError as e:
|
| 106 |
+
# logger.error("❌ Could not connect to SMTP server.")
|
| 107 |
+
# logger.error(f"Error details: {e}")
|
| 108 |
+
# logger.error(traceback.format_exc())
|
| 109 |
+
# raise
|
| 110 |
+
# except smtplib.SMTPRecipientsRefused as e:
|
| 111 |
+
# logger.error("❌ Recipient address refused.")
|
| 112 |
+
# logger.error(f"Error details: {e}")
|
| 113 |
+
# logger.error(traceback.format_exc())
|
| 114 |
+
# raise
|
| 115 |
+
# except smtplib.SMTPException as e:
|
| 116 |
+
# logger.error("❌ General SMTP error occurred.")
|
| 117 |
+
# logger.error(f"Error details: {e}")
|
| 118 |
+
# logger.error(traceback.format_exc())
|
| 119 |
+
# raise
|
| 120 |
+
# except Exception as e:
|
| 121 |
+
# logger.error("❌ Unknown error occurred while sending verification email.")
|
| 122 |
+
# logger.error(f"Error details: {e}")
|
| 123 |
+
# logger.error(traceback.format_exc())
|
| 124 |
+
# raise
|
| 125 |
|
| 126 |
|
| 127 |
fernet = Fernet(FERNET_KEY.encode())
|
src/leave/router.py
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from src.leave.utils import send_email
|
| 2 |
+
from fastapi import APIRouter, Depends, HTTPException
|
| 3 |
+
from sqlmodel import select
|
| 4 |
+
from sqlmodel.ext.asyncio.session import AsyncSession
|
| 5 |
+
from src.auth.utils import get_current_user
|
| 6 |
+
from src.core.database import get_async_session
|
| 7 |
+
from src.core.models import Users, Teams, Roles, UserTeamsRole
|
| 8 |
+
from src.auth.schemas import BaseResponse
|
| 9 |
+
from fastapi import BackgroundTasks
|
| 10 |
+
|
| 11 |
+
router = APIRouter(prefix="/leave", tags=["leave"])
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
@router.get("/contacts", response_model=BaseResponse)
|
| 15 |
+
async def get_leave_contacts(
|
| 16 |
+
current_user=Depends(get_current_user),
|
| 17 |
+
session: AsyncSession = Depends(get_async_session),
|
| 18 |
+
):
|
| 19 |
+
# get_current_user returns a STRING user_id
|
| 20 |
+
user_id = current_user
|
| 21 |
+
|
| 22 |
+
if not user_id:
|
| 23 |
+
raise HTTPException(status_code=400, detail="Invalid user token")
|
| 24 |
+
|
| 25 |
+
# 1) Get user's team
|
| 26 |
+
stmt = select(UserTeamsRole).where(UserTeamsRole.user_id == user_id)
|
| 27 |
+
ut = (await session.exec(stmt)).first()
|
| 28 |
+
|
| 29 |
+
if not ut:
|
| 30 |
+
raise HTTPException(status_code=404, detail="User-Team mapping not found")
|
| 31 |
+
|
| 32 |
+
# 2) Get Team Lead role
|
| 33 |
+
lead_role = (
|
| 34 |
+
await session.exec(select(Roles).where(Roles.name == "Team Lead"))
|
| 35 |
+
).first()
|
| 36 |
+
|
| 37 |
+
if not lead_role:
|
| 38 |
+
raise HTTPException(status_code=500, detail="Team Lead role not found")
|
| 39 |
+
|
| 40 |
+
# 3) Find Team Lead user in same team
|
| 41 |
+
lead_user = (
|
| 42 |
+
await session.exec(
|
| 43 |
+
select(Users)
|
| 44 |
+
.join(UserTeamsRole)
|
| 45 |
+
.where(UserTeamsRole.team_id == ut.team_id)
|
| 46 |
+
.where(UserTeamsRole.role_id == lead_role.id)
|
| 47 |
+
)
|
| 48 |
+
).all()
|
| 49 |
+
|
| 50 |
+
if not lead_user:
|
| 51 |
+
raise HTTPException(status_code=404, detail="Team lead not found")
|
| 52 |
+
|
| 53 |
+
to_email = ", ".join([u.email_id for u in lead_user])
|
| 54 |
+
|
| 55 |
+
# 4) HR CC emails
|
| 56 |
+
hr_team = (await session.exec(select(Teams).where(Teams.name == "HR Team"))).first()
|
| 57 |
+
|
| 58 |
+
cc = []
|
| 59 |
+
if hr_team:
|
| 60 |
+
hr_users = (
|
| 61 |
+
await session.exec(
|
| 62 |
+
select(Users)
|
| 63 |
+
.join(UserTeamsRole)
|
| 64 |
+
.where(UserTeamsRole.team_id == hr_team.id)
|
| 65 |
+
)
|
| 66 |
+
).all()
|
| 67 |
+
|
| 68 |
+
cc = [str(row.email_id) for row in hr_users]
|
| 69 |
+
|
| 70 |
+
return BaseResponse(code=200, message="success", data={"to": to_email, "cc": cc})
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
@router.post("/send", response_model=BaseResponse)
|
| 74 |
+
async def send_leave_email(
|
| 75 |
+
payload: dict,
|
| 76 |
+
background: BackgroundTasks,
|
| 77 |
+
current_user=Depends(get_current_user),
|
| 78 |
+
):
|
| 79 |
+
from_email = payload.get("from_email")
|
| 80 |
+
to_email = payload.get("to")
|
| 81 |
+
cc = payload.get("cc", [])
|
| 82 |
+
subject = payload.get("subject")
|
| 83 |
+
body = payload.get("body")
|
| 84 |
+
|
| 85 |
+
if not subject or not body:
|
| 86 |
+
raise HTTPException(status_code=400, detail="Subject and body required")
|
| 87 |
+
|
| 88 |
+
# send in background so API returns fast
|
| 89 |
+
background.add_task(send_email, to_email, subject, body, cc, from_email)
|
| 90 |
+
|
| 91 |
+
return BaseResponse(code=200, message="Leave request sent", data=None)
|
src/leave/schemas.py
ADDED
|
File without changes
|
src/leave/utils.py
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# src/core/email_utils.py
|
| 2 |
+
import smtplib
|
| 3 |
+
from email.message import EmailMessage
|
| 4 |
+
from src.core.config import settings
|
| 5 |
+
from typing import List
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
SMTP_HOST = settings.EMAIL_SERVER
|
| 9 |
+
SMTP_PORT = settings.EMAIL_PORT
|
| 10 |
+
SMTP_USER = settings.EMAIL_USERNAME
|
| 11 |
+
SMTP_PASS = settings.EMAIL_PASSWORD
|
| 12 |
+
FROM_DEFAULT = settings.EMAIL_USERNAME
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
def send_email(
|
| 16 |
+
to_email: str, subject: str, body: str, cc: list[str] = None, from_email: str = None
|
| 17 |
+
):
|
| 18 |
+
"""
|
| 19 |
+
Gmail cannot send as another user.
|
| 20 |
+
So we set 'From' = your Gmail, but 'Reply-To' = user email.
|
| 21 |
+
"""
|
| 22 |
+
|
| 23 |
+
cc = cc or []
|
| 24 |
+
|
| 25 |
+
msg = EmailMessage()
|
| 26 |
+
msg["Subject"] = subject
|
| 27 |
+
|
| 28 |
+
# Always send FROM your SMTP account
|
| 29 |
+
msg["From"] = settings.EMAIL_USERNAME
|
| 30 |
+
|
| 31 |
+
# Show this as reply address
|
| 32 |
+
if from_email:
|
| 33 |
+
msg["Reply-To"] = from_email
|
| 34 |
+
|
| 35 |
+
msg["To"] = to_email
|
| 36 |
+
|
| 37 |
+
if cc:
|
| 38 |
+
msg["Cc"] = ", ".join(cc)
|
| 39 |
+
|
| 40 |
+
msg.set_content(body)
|
| 41 |
+
|
| 42 |
+
try:
|
| 43 |
+
with smtplib.SMTP(settings.EMAIL_SERVER, settings.EMAIL_PORT) as server:
|
| 44 |
+
server.starttls()
|
| 45 |
+
server.login(settings.EMAIL_USERNAME, settings.EMAIL_PASSWORD)
|
| 46 |
+
|
| 47 |
+
server.send_message(msg)
|
| 48 |
+
|
| 49 |
+
except Exception as e:
|
| 50 |
+
raise Exception(f"Email sending failed: {str(e)}")
|
src/main.py
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
|
|
|
|
|
| 1 |
from fastapi import FastAPI
|
| 2 |
|
| 3 |
from src.core.database import init_db
|
| 4 |
from src.home.router import router as home_router
|
| 5 |
from src.auth.router import router as auth_router
|
|
|
|
| 6 |
|
| 7 |
app = FastAPI(title="Yuvabe App API")
|
| 8 |
|
|
@@ -12,6 +15,12 @@ init_db()
|
|
| 12 |
|
| 13 |
app.include_router(auth_router)
|
| 14 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
|
| 16 |
@app.get("/")
|
| 17 |
def root():
|
|
|
|
| 1 |
+
from src.assets.router import router as assets
|
| 2 |
+
from src.profile.router import router as profile
|
| 3 |
from fastapi import FastAPI
|
| 4 |
|
| 5 |
from src.core.database import init_db
|
| 6 |
from src.home.router import router as home_router
|
| 7 |
from src.auth.router import router as auth_router
|
| 8 |
+
from src.leave.router import router as leave
|
| 9 |
|
| 10 |
app = FastAPI(title="Yuvabe App API")
|
| 11 |
|
|
|
|
| 15 |
|
| 16 |
app.include_router(auth_router)
|
| 17 |
|
| 18 |
+
app.include_router(profile)
|
| 19 |
+
|
| 20 |
+
app.include_router(assets)
|
| 21 |
+
|
| 22 |
+
app.include_router(leave)
|
| 23 |
+
|
| 24 |
|
| 25 |
@app.get("/")
|
| 26 |
def root():
|
src/profile/router.py
CHANGED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from fastapi.routing import APIRouter
|
| 2 |
+
from src.core.database import get_async_session
|
| 3 |
+
from src.auth.utils import get_current_user
|
| 4 |
+
from src.auth.schemas import BaseResponse
|
| 5 |
+
from sqlalchemy.ext.asyncio.session import AsyncSession
|
| 6 |
+
from fastapi.params import Depends
|
| 7 |
+
from .schemas import UpdateProfileRequest
|
| 8 |
+
from src.profile.service import update_user_profile
|
| 9 |
+
|
| 10 |
+
router = APIRouter(prefix="/profile", tags=["Profile"])
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
@router.put("/update-profile", response_model=BaseResponse)
|
| 14 |
+
async def update_profile(
|
| 15 |
+
payload: UpdateProfileRequest,
|
| 16 |
+
user_id: str = Depends(get_current_user),
|
| 17 |
+
session: AsyncSession = Depends(get_async_session),
|
| 18 |
+
):
|
| 19 |
+
result = await update_user_profile(session, user_id, payload)
|
| 20 |
+
return {"code": 200, "data": result}
|
src/profile/schemas.py
CHANGED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from pydantic import BaseModel, EmailStr
|
| 2 |
+
from typing import Optional
|
| 3 |
+
|
| 4 |
+
class UpdateProfileRequest(BaseModel):
|
| 5 |
+
name: Optional[str] = None
|
| 6 |
+
email: Optional[EmailStr] = None
|
| 7 |
+
dob: Optional[str] = None
|
| 8 |
+
address: Optional[str] = None
|
| 9 |
+
|
| 10 |
+
current_password: Optional[str] = None
|
| 11 |
+
new_password: Optional[str] = None
|
src/profile/service.py
CHANGED
|
@@ -1,2 +1,65 @@
|
|
| 1 |
-
from
|
| 2 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from datetime import datetime
|
| 2 |
+
import uuid
|
| 3 |
+
from fastapi import HTTPException
|
| 4 |
+
from passlib.context import CryptContext
|
| 5 |
+
from src.core.models import Users
|
| 6 |
+
|
| 7 |
+
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
async def update_user_profile(session, user_id: str, data):
|
| 11 |
+
user = await session.get(Users, uuid.UUID(user_id))
|
| 12 |
+
|
| 13 |
+
if not user:
|
| 14 |
+
raise HTTPException(status_code=404, detail="User not found")
|
| 15 |
+
|
| 16 |
+
# --- Update Name ---
|
| 17 |
+
if data.name:
|
| 18 |
+
user.user_name = data.name
|
| 19 |
+
|
| 20 |
+
# --- Update Email ---
|
| 21 |
+
if data.email:
|
| 22 |
+
user.email_id = data.email
|
| 23 |
+
|
| 24 |
+
# --- Update DOB ---
|
| 25 |
+
if data.dob:
|
| 26 |
+
try:
|
| 27 |
+
# Convert DD.MM.YYYY → Python date
|
| 28 |
+
parsed_date = datetime.strptime(data.dob, "%d.%m.%Y").date()
|
| 29 |
+
user.dob = parsed_date
|
| 30 |
+
except:
|
| 31 |
+
raise HTTPException(
|
| 32 |
+
status_code=400, detail="DOB must be in DD.MM.YYYY format"
|
| 33 |
+
)
|
| 34 |
+
|
| 35 |
+
# --- Update Address ---
|
| 36 |
+
if data.address:
|
| 37 |
+
user.address = data.address
|
| 38 |
+
|
| 39 |
+
# --- Change Password ---
|
| 40 |
+
if data.new_password:
|
| 41 |
+
if not data.current_password:
|
| 42 |
+
raise HTTPException(status_code=400, detail="Current password required")
|
| 43 |
+
|
| 44 |
+
# Verify old password
|
| 45 |
+
if not pwd_context.verify(data.current_password, user.password):
|
| 46 |
+
raise HTTPException(status_code=400, detail="Incorrect current password")
|
| 47 |
+
|
| 48 |
+
# Set new password
|
| 49 |
+
user.password = pwd_context.hash(data.new_password)
|
| 50 |
+
|
| 51 |
+
# Commit changes
|
| 52 |
+
await session.commit()
|
| 53 |
+
await session.refresh(user)
|
| 54 |
+
|
| 55 |
+
return {
|
| 56 |
+
"message": "Profile updated successfully",
|
| 57 |
+
"user": {
|
| 58 |
+
"id": str(user.id),
|
| 59 |
+
"name": user.user_name,
|
| 60 |
+
"email": user.email_id,
|
| 61 |
+
"dob": user.dob.isoformat() if user.dob else None,
|
| 62 |
+
"address": user.address,
|
| 63 |
+
"is_verified": user.is_verified,
|
| 64 |
+
},
|
| 65 |
+
}
|