Hp137 commited on
Commit
5078e89
·
1 Parent(s): 20d48af

feat:Added gmail api

Browse files
src/core/config.py CHANGED
@@ -33,6 +33,14 @@ class Settings(BaseSettings):
33
  FERNET_KEY: str
34
  VERIFICATION_BASE_URL: str
35
 
 
 
 
 
 
 
 
 
36
  @computed_field
37
  @property
38
  def DATABASE_URL(self) -> PostgresDsn:
@@ -45,8 +53,9 @@ class Settings(BaseSettings):
45
  """Async DB URL"""
46
  return f"postgresql+asyncpg://{self.POSTGRES_USER}:{self.POSTGRES_PASSWORD}@{self.POSTGRES_HOST}/{self.POSTGRES_DB}"
47
 
48
- model_config = SettingsConfigDict(env_file=".env", case_sensitive=False)
49
-
 
50
 
51
 
52
  settings = Settings()
 
33
  FERNET_KEY: str
34
  VERIFICATION_BASE_URL: str
35
 
36
+ GOOGLE_CLIENT_ID: str
37
+ GOOGLE_CLIENT_SECRET: str
38
+ GOOGLE_REDIRECT_URI: str
39
+
40
+ AUTH_BASE: str = "https://accounts.google.com/o/oauth2/v2/auth"
41
+ TOKEN_URL: str = "https://oauth2.googleapis.com/token"
42
+ GMAIL_SEND_SCOPE: str = "https://www.googleapis.com/auth/gmail.send"
43
+
44
  @computed_field
45
  @property
46
  def DATABASE_URL(self) -> PostgresDsn:
 
53
  """Async DB URL"""
54
  return f"postgresql+asyncpg://{self.POSTGRES_USER}:{self.POSTGRES_PASSWORD}@{self.POSTGRES_HOST}/{self.POSTGRES_DB}"
55
 
56
+ model_config = SettingsConfigDict(
57
+ env_file=".env", case_sensitive=False, env_file_encoding="utf-8"
58
+ )
59
 
60
 
61
  settings = Settings()
src/main.py CHANGED
@@ -1,11 +1,10 @@
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
 
 
 
1
  from src.profile.router import router as profile
2
  from fastapi import FastAPI
3
 
4
  from src.core.database import init_db
5
  from src.home.router import router as home_router
6
  from src.auth.router import router as auth_router
7
+
8
 
9
  app = FastAPI(title="Yuvabe App API")
10
 
src/profile/router.py CHANGED
@@ -1,3 +1,10 @@
 
 
 
 
 
 
 
1
  from fastapi.routing import APIRouter
2
  from src.core.database import get_async_session
3
  from src.auth.utils import get_current_user
@@ -6,23 +13,66 @@ 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
- from fastapi import APIRouter, Depends
10
- from sqlmodel.ext.asyncio.session import AsyncSession
11
- from src.core.database import get_async_session
12
- from src.auth.utils import get_current_user
13
- from src.assets.schemas import BaseResponse
14
- from src.assets.service import list_user_assets
15
- from src.leave.utils import send_email
16
- from fastapi import APIRouter, Depends, HTTPException
17
  from sqlmodel import select
18
- from sqlmodel.ext.asyncio.session import AsyncSession
19
- from src.auth.utils import get_current_user
20
  from src.core.models import Users, Teams, Roles, UserTeamsRole
21
- from fastapi import BackgroundTasks
22
 
23
 
24
  router = APIRouter(prefix="/profile", tags=["Profile"])
25
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
26
 
27
  @router.get("/", response_model=BaseResponse)
28
  async def get_assets(
@@ -56,7 +106,6 @@ async def update_profile(
56
  return {"code": 200, "data": result}
57
 
58
 
59
-
60
  @router.get("/contacts", response_model=BaseResponse)
61
  async def get_leave_contacts(
62
  current_user=Depends(get_current_user),
@@ -135,4 +184,3 @@ async def send_leave_email(
135
  background.add_task(send_email, to_email, subject, body, cc, from_email)
136
 
137
  return BaseResponse(code=200, message="Leave request sent", data=None)
138
-
 
1
+ from src.profile.service import send_email_service
2
+ from src.profile.schemas import SendMailRequest
3
+ from src.profile.utils import build_auth_url
4
+ from src.profile.utils import exchange_code_for_tokens
5
+ from src.profile.service import USER_TOKEN_STORE
6
+ from src.profile.utils import send_email
7
+ from src.profile.service import list_user_assets
8
  from fastapi.routing import APIRouter
9
  from src.core.database import get_async_session
10
  from src.auth.utils import get_current_user
 
13
  from fastapi.params import Depends
14
  from .schemas import UpdateProfileRequest
15
  from src.profile.service import update_user_profile
16
+ from fastapi import APIRouter, Depends, HTTPException, BackgroundTasks
 
 
 
 
 
 
 
17
  from sqlmodel import select
 
 
18
  from src.core.models import Users, Teams, Roles, UserTeamsRole
 
19
 
20
 
21
  router = APIRouter(prefix="/profile", tags=["Profile"])
22
 
23
+ # src/routers/gmail_oauth_router.py
24
+ from fastapi import APIRouter, Query, HTTPException
25
+ from fastapi.responses import RedirectResponse, JSONResponse
26
+ import httpx
27
+
28
+
29
+ router = APIRouter(prefix="/gmail", tags=["Gmail OAuth"])
30
+
31
+
32
+ @router.get("/login")
33
+ def google_login(state: str | None = Query(None)):
34
+ return RedirectResponse(build_auth_url(state))
35
+
36
+
37
+ @router.get("/callback")
38
+ async def google_callback(code: str | None = None, state: str | None = None):
39
+ if not code:
40
+ raise HTTPException(400, "Missing code")
41
+
42
+ token_data = await exchange_code_for_tokens(code)
43
+ access_token = token_data["access_token"]
44
+ refresh_token = token_data.get("refresh_token")
45
+
46
+ # Get user info
47
+ async with httpx.AsyncClient() as client:
48
+ r = await client.get(
49
+ "https://www.googleapis.com/oauth2/v3/userinfo",
50
+ headers={"Authorization": f"Bearer {access_token}"}
51
+ )
52
+ userinfo = r.json()
53
+
54
+ google_user_id = userinfo["sub"]
55
+ user_email = userinfo["email"]
56
+
57
+ USER_TOKEN_STORE[google_user_id] = {
58
+ "access_token": access_token,
59
+ "refresh_token": refresh_token,
60
+ "email": user_email
61
+ }
62
+
63
+ return JSONResponse({
64
+ "status": "ok",
65
+ "user_id": google_user_id,
66
+ "email": user_email,
67
+ "state": state,
68
+ })
69
+
70
+
71
+ @router.post("/send-mail")
72
+ async def send_mail(req: SendMailRequest):
73
+ return await send_email_service(req)
74
+
75
+
76
 
77
  @router.get("/", response_model=BaseResponse)
78
  async def get_assets(
 
106
  return {"code": 200, "data": result}
107
 
108
 
 
109
  @router.get("/contacts", response_model=BaseResponse)
110
  async def get_leave_contacts(
111
  current_user=Depends(get_current_user),
 
184
  background.add_task(send_email, to_email, subject, body, cc, from_email)
185
 
186
  return BaseResponse(code=200, message="Leave request sent", data=None)
 
src/profile/schemas.py CHANGED
@@ -3,22 +3,26 @@ from typing import Optional
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
@@ -26,6 +30,7 @@ class AssetResponse(BaseModel):
26
  type: str
27
  status: AssetStatus
28
 
 
29
  class BaseResponse(BaseModel):
30
  code: int
31
  data: dict
@@ -39,3 +44,11 @@ class UpdateProfileRequest(BaseModel):
39
 
40
  current_password: Optional[str] = None
41
  new_password: Optional[str] = None
 
 
 
 
 
 
 
 
 
3
  import uuid
4
  from enum import Enum
5
 
6
+
7
  class AssetStatus(str, Enum):
8
  ACTIVE = "Active"
9
  UNAVAILABLE = "Unavailable"
10
  ON_REQUEST = "On Request"
11
  IN_SERVICE = "In Service"
12
 
13
+
14
  class AssetCreateRequest(BaseModel):
15
  name: str
16
  type: str
17
  status: Optional[AssetStatus] = AssetStatus.UNAVAILABLE
18
 
19
+
20
  class AssetUpdateRequest(BaseModel):
21
  name: Optional[str] = None
22
  type: Optional[str] = None
23
  status: Optional[AssetStatus] = None
24
 
25
+
26
  class AssetResponse(BaseModel):
27
  id: uuid.UUID
28
  user_id: uuid.UUID
 
30
  type: str
31
  status: AssetStatus
32
 
33
+
34
  class BaseResponse(BaseModel):
35
  code: int
36
  data: dict
 
44
 
45
  current_password: Optional[str] = None
46
  new_password: Optional[str] = None
47
+
48
+
49
+ class SendMailRequest(BaseModel):
50
+ user_id: str
51
+ to: EmailStr
52
+ subject: str
53
+ body: str
54
+ from_name: Optional[str] = None
src/profile/service.py CHANGED
@@ -1,3 +1,6 @@
 
 
 
1
  from src.core.models import Assets
2
  from ast import List
3
  from datetime import datetime
@@ -10,10 +13,55 @@ from typing import List
10
  from sqlmodel import select
11
  from sqlmodel.ext.asyncio.session import AsyncSession
12
  from src.core.models import Assets
 
 
 
13
 
14
  pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
15
 
16
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
17
  async def update_user_profile(session, user_id: str, data):
18
  user = await session.get(Users, uuid.UUID(user_id))
19
 
@@ -71,8 +119,7 @@ async def update_user_profile(session, user_id: str, data):
71
  },
72
  }
73
 
 
74
  async def list_user_assets(session: AsyncSession, user_id: str) -> List[Assets]:
75
- q = await session.exec(
76
- select(Assets).where(Assets.user_id == uuid.UUID(user_id))
77
- )
78
- return q.all()
 
1
+ from src.profile.utils import build_raw_message
2
+ from src.profile.utils import refresh_access_token
3
+ from src.profile.schemas import SendMailRequest
4
  from src.core.models import Assets
5
  from ast import List
6
  from datetime import datetime
 
13
  from sqlmodel import select
14
  from sqlmodel.ext.asyncio.session import AsyncSession
15
  from src.core.models import Assets
16
+ import httpx
17
+ from src.core.config import settings
18
+
19
 
20
  pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
21
 
22
 
23
+ # In production, replace with DB storage
24
+ USER_TOKEN_STORE = {} # {google_user_id: {tokens}}
25
+
26
+
27
+ async def send_email_service(req: SendMailRequest):
28
+ record = USER_TOKEN_STORE.get(req.user_id)
29
+ if not record:
30
+ raise HTTPException(404, "User not logged in with Google OAuth")
31
+
32
+ access_token = record["access_token"]
33
+ refresh_token = record.get("refresh_token")
34
+
35
+ if not access_token and refresh_token:
36
+ new_tokens = await refresh_access_token(refresh_token)
37
+ access_token = new_tokens["access_token"]
38
+ record["access_token"] = access_token
39
+
40
+ if not access_token:
41
+ raise HTTPException(400, "Re-auth required")
42
+
43
+ raw = build_raw_message(
44
+ to_email=req.to,
45
+ subject=req.subject,
46
+ body=req.body,
47
+ from_name=req.from_name,
48
+ from_email=record["email"],
49
+ )
50
+
51
+ url = "https://gmail.googleapis.com/gmail/v1/users/me/messages/send"
52
+ payload = {"raw": raw}
53
+
54
+ async with httpx.AsyncClient() as client:
55
+ r = await client.post(
56
+ url, json=payload, headers={"Authorization": f"Bearer {access_token}"}
57
+ )
58
+
59
+ if r.status_code >= 400:
60
+ raise HTTPException(500, f"Gmail error: {r.text}")
61
+
62
+ return r.json()
63
+
64
+
65
  async def update_user_profile(session, user_id: str, data):
66
  user = await session.get(Users, uuid.UUID(user_id))
67
 
 
119
  },
120
  }
121
 
122
+
123
  async def list_user_assets(session: AsyncSession, user_id: str) -> List[Assets]:
124
+ q = await session.exec(select(Assets).where(Assets.user_id == uuid.UUID(user_id)))
125
+ return q.all()
 
 
src/profile/utils.py CHANGED
@@ -3,6 +3,12 @@ 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
@@ -11,6 +17,69 @@ 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
 
3
  from email.message import EmailMessage
4
  from src.core.config import settings
5
  from typing import List
6
+ import base64
7
+ import httpx
8
+ from typing import Dict, Optional
9
+ from src.core.config import settings
10
+ from urllib.parse import urlencode
11
+
12
 
13
 
14
  SMTP_HOST = settings.EMAIL_SERVER
 
17
  SMTP_PASS = settings.EMAIL_PASSWORD
18
  FROM_DEFAULT = settings.EMAIL_USERNAME
19
 
20
+ # src/utils/gmail_utils.py
21
+
22
+
23
+ def build_auth_url(state=None):
24
+ params = {
25
+ "client_id": settings.GOOGLE_CLIENT_ID,
26
+ "redirect_uri": settings.GOOGLE_REDIRECT_URI,
27
+ "response_type": "code",
28
+ "scope": settings.GMAIL_SEND_SCOPE + " openid email profile",
29
+ "access_type": "offline",
30
+ "prompt": "consent",
31
+ }
32
+
33
+ if state:
34
+ params["state"] = state
35
+
36
+ query = urlencode(params)
37
+ return f"{settings.AUTH_BASE}?{query}"
38
+
39
+
40
+ async def exchange_code_for_tokens(code: str) -> Dict:
41
+ data = {
42
+ "code": code,
43
+ "client_id": settings.GOOGLE_CLIENT_ID,
44
+ "client_secret": settings.GOOGLE_CLIENT_SECRET,
45
+ "redirect_uri": settings.GOOGLE_REDIRECT_URI,
46
+ "grant_type": "authorization_code",
47
+ }
48
+
49
+ async with httpx.AsyncClient() as client:
50
+ r = await client.post(settings.TOKEN_URL, data=data)
51
+ r.raise_for_status()
52
+ return r.json()
53
+
54
+
55
+ async def refresh_access_token(refresh_token: str) -> Dict:
56
+ data = {
57
+ "client_id": settings.GOOGLE_CLIENT_ID,
58
+ "client_secret": settings.GOOGLE_CLIENT_SECRET,
59
+ "refresh_token": refresh_token,
60
+ "grant_type": "refresh_token",
61
+ }
62
+
63
+ async with httpx.AsyncClient() as client:
64
+ r = await client.post(settings.TOKEN_URL, data=data)
65
+ r.raise_for_status()
66
+ return r.json()
67
+
68
+
69
+ def build_raw_message(
70
+ to_email: str, subject: str, body: str, from_name: Optional[str], from_email: str
71
+ ) -> str:
72
+
73
+ msg = EmailMessage()
74
+ sender = f"{from_name} <{from_email}>" if from_name else from_email
75
+ msg["From"] = sender
76
+ msg["To"] = to_email
77
+ msg["Subject"] = subject
78
+ msg.set_content(body)
79
+
80
+ raw_bytes = msg.as_bytes()
81
+ return base64.urlsafe_b64encode(raw_bytes).decode()
82
+
83
 
84
  def send_email(
85
  to_email: str, subject: str, body: str, cc: list[str] = None, from_email: str = None