Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 21 additions & 6 deletions app/Controllers/Opportunities/job_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,20 @@
from Services.Opportunities.jobs_service import JobService
from Settings.logging_config import setup_logging
from db import get_session
from Schema.SQL.Enums.enums import Role
from Security.unified_dependencies import require_roles

logger = setup_logging()

router = APIRouter(prefix="/Dijkstra/v1/jobs", tags=["Jobs"])


@router.post("/", response_model=ReadJob)
def create_job(job_create: CreateJob, session: Session = Depends(get_session)):
def create_job(
job_create: CreateJob,
session: Session = Depends(get_session),
_: object = Depends(require_roles(Role.GLOBAL_ADMIN)),
):
service = JobService(session)
logger.info(f"Creating Job: {job_create.title}")
job = service.create_job(job_create)
Expand All @@ -22,7 +28,7 @@ def create_job(job_create: CreateJob, session: Session = Depends(get_session)):


@router.get("/{job_id}", response_model=ReadJob)
def get_job(job_id: UUID, session: Session = Depends(get_session)):
def get_job(job_id: UUID, session: Session = Depends(get_session), _: object = Depends(require_roles(Role.GLOBAL_ADMIN))):
service = JobService(session)
logger.info(f"Fetching Job with ID: {job_id}")
return service.get_job(job_id)
Expand Down Expand Up @@ -76,15 +82,24 @@ def autocomplete_jobs(
return service.autocomplete_jobs(query, field, limit)


@router.put("/{job_id}", response_model=ReadJob)
def update_job(job_id: UUID, job_update: UpdateJob, session: Session = Depends(get_session)):
@router.put("/{job_id}", response_model=ReadJob, dependencies=[Depends(require_roles(Role.GLOBAL_ADMIN))])
def update_job(
job_id: UUID,
job_update: UpdateJob,
session: Session = Depends(get_session),
_: object = Depends(require_roles(Role.GLOBAL_ADMIN)),
):
service = JobService(session)
logger.info(f"Updating Job ID: {job_id} with data: {job_update.dict(exclude_unset=True)}")
return service.update_job(job_id, job_update)


@router.delete("/{job_id}", response_model=ReadJob)
def delete_job(job_id: UUID, session: Session = Depends(get_session)):
@router.delete("/{job_id}", response_model=ReadJob, dependencies=[Depends(require_roles(Role.GLOBAL_ADMIN))])
def delete_job(
job_id: UUID,
session: Session = Depends(get_session),
_: object = Depends(require_roles(Role.GLOBAL_ADMIN)),
):
service = JobService(session)
logger.info(f"Deleting Job ID: {job_id}")
message = service.delete_job(job_id)
Expand Down
5 changes: 4 additions & 1 deletion app/Controllers/User/user_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
from Entities.UserDTOs.user_entity import CreateUser, ReadUserAuthDetails, UpdateUser, ReadUser, OnboardUser, OnboardCheckResponse, ReadUserCardDetails, ReadUserPersonalDetails, UpdateUserPersonalDetails
from Services.User.user_service import UserService
from Settings.logging_config import setup_logging
from Schema.SQL.Enums.enums import Role
from Security.unified_dependencies import require_roles
from db import get_session

logger = setup_logging()
Expand All @@ -27,7 +29,8 @@ def create_user(user_create: CreateUser, session: Session = Depends(get_session)
@router.get("/onboard", response_model=OnboardCheckResponse)
def check_onboarding(
username: str = Query(..., description="GitHub username to check"),
session: Session = Depends(get_session)
session: Session = Depends(get_session),
_: object = Depends(require_roles(Role.PERSONAL_WRITE))
):
"""
Check if a user has completed onboarding by GitHub username.
Expand Down
103 changes: 103 additions & 0 deletions app/Controllers/api_key_controller.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
from typing import List
from fastapi import APIRouter, Depends, HTTPException, status
from uuid import UUID
from sqlmodel import Session

from Entities.UserDTOs.api_key_entity import CreateAPIKey, ReadAPIKey, UpdateAPIKey, APIKeyResponse
from Services.User.api_key_service import APIKeyService
from Security.jwt_dependencies import get_current_user, get_current_user_with_token, TokenPayload
from Schema.SQL.Models.models import User
from Settings.logging_config import setup_logging
from db import get_session

logger = setup_logging()

router = APIRouter(prefix="/Dijkstra/v1/api-keys", tags=["API Keys"])


@router.post("/", response_model=APIKeyResponse)
def create_api_key(
create_data: CreateAPIKey,
user_and_token: tuple[User, TokenPayload] = Depends(get_current_user_with_token),
session: Session = Depends(get_session),
):
"""Create a new API key. Requires JWT authentication."""
current_user, token_payload = user_and_token
service = APIKeyService(session)
logger.info(f"Creating API key for user: {current_user.github_user_name}")

# Extract isDev from JWT token payload
is_dev = token_payload.is_dev or False

# Get user roles from database
user_roles = current_user.roles or []

api_key_response = service.create_api_key(
github_username=current_user.github_user_name,
user_roles=user_roles,
is_dev=is_dev,
create_data=create_data
)

logger.info(f"API key created successfully for user: {current_user.github_user_name}")
return api_key_response


@router.get("/", response_model=List[ReadAPIKey])
def list_api_keys(
current_user: User = Depends(get_current_user),
session: Session = Depends(get_session),
):
"""List all API keys for the authenticated user. Requires JWT authentication."""
service = APIKeyService(session)
logger.info(f"Listing API keys for user: {current_user.github_user_name}")

api_keys = service.list_api_keys(current_user.github_user_name)
logger.info(f"Found {len(api_keys)} API keys for user: {current_user.github_user_name}")
return api_keys


@router.delete("/{api_key_id}", response_model=dict)
def revoke_api_key(
api_key_id: UUID,
current_user: User = Depends(get_current_user),
session: Session = Depends(get_session),
):
"""Revoke (deactivate) an API key. Requires JWT authentication."""
service = APIKeyService(session)
logger.info(f"Revoking API key {api_key_id} for user: {current_user.github_user_name}")

result = service.revoke_api_key(api_key_id, current_user.github_user_name)
logger.info(f"API key {api_key_id} revoked successfully")
return result


@router.put("/{api_key_id}", response_model=ReadAPIKey)
def update_api_key(
api_key_id: UUID,
update_data: UpdateAPIKey,
user_and_token: tuple[User, TokenPayload] = Depends(get_current_user_with_token),
session: Session = Depends(get_session),
):
"""Update an API key. Requires JWT authentication."""
current_user, token_payload = user_and_token
service = APIKeyService(session)
logger.info(f"Updating API key {api_key_id} for user: {current_user.github_user_name}")

# Extract isDev from JWT token payload
is_dev = token_payload.is_dev or False

# Get user roles from database
user_roles = current_user.roles or []

updated_key = service.update_api_key(
api_key_id=api_key_id,
github_username=current_user.github_user_name,
user_roles=user_roles,
is_dev=is_dev,
update_data=update_data
)

logger.info(f"API key {api_key_id} updated successfully")
return updated_key

45 changes: 45 additions & 0 deletions app/Controllers/error_handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,14 @@
from Utils.Exceptions.opportunities_exceptions import FellowshipNotFound, InvalidTools, JobNotFound, OrganizationNotFound, ProjectOpportunityNotFound
from Utils.errors import raise_api_error
from Utils.Exceptions.user_exceptions import (
APIKeyExpired,
APIKeyInactive,
APIKeyNotFound,
CertificationNotFound,
EducationNotFound,
GitHubUsernameAlreadyExists,
GitHubUsernameNotFound,
InvalidAPIKeyRoles,
LeetcodeBadgeNotFound,
LeetcodeNotFound,
LeetcodeTagNotFound,
Expand All @@ -20,6 +24,7 @@
ProfileAlreadyExists,
ProfileNotFound,
ProjectsNotFound,
PublicationNotFound,
UserNotFound,
VolunteeringNotFound,
WorkExperienceNotFound,
Expand Down Expand Up @@ -353,4 +358,44 @@ async def education_not_found_handler(request: Request, exc: EducationNotFound):
error="Education not found",
detail=str(exc),
status=404,
)

@app.exception_handler(APIKeyNotFound)
async def api_key_not_found_handler(request: Request, exc: APIKeyNotFound):
logger.warning(f"API key not found: {exc.api_key_id}")
raise_api_error(
code=ErrorCodes.AUTH_ERROR,
error=ErrorCodes.AUTH_ERROR_A01,
detail=str(exc),
status=401,
)

@app.exception_handler(APIKeyExpired)
async def api_key_expired_handler(request: Request, exc: APIKeyExpired):
logger.warning("API key expired")
raise_api_error(
code=ErrorCodes.AUTH_ERROR,
error=ErrorCodes.AUTH_ERROR_A01,
detail=str(exc),
status=401,
)

@app.exception_handler(APIKeyInactive)
async def api_key_inactive_handler(request: Request, exc: APIKeyInactive):
logger.warning("API key inactive")
raise_api_error(
code=ErrorCodes.AUTH_ERROR,
error=ErrorCodes.AUTH_ERROR_A01,
detail=str(exc),
status=401,
)

@app.exception_handler(InvalidAPIKeyRoles)
async def invalid_api_key_roles_handler(request: Request, exc: InvalidAPIKeyRoles):
logger.warning(f"Invalid API key roles: {exc.requested_roles} not subset of {exc.user_roles}")
raise_api_error(
code=ErrorCodes.AUTH_ERROR,
error=ErrorCodes.AUTH_ERROR_A01,
detail=str(exc),
status=403,
)
44 changes: 44 additions & 0 deletions app/Entities/UserDTOs/api_key_entity.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
from typing import List, Optional
from uuid import UUID
from datetime import datetime
from pydantic import BaseModel

from Schema.SQL.Enums.enums import Role

# ----------------------
# Input DTOs
# ----------------------
class CreateAPIKey(BaseModel):
description: Optional[str] = None
expires_in: Optional[datetime] = None
roles: List[Role]

# ----------------------
# Output DTOs
# ----------------------
class ReadAPIKey(BaseModel):
id: UUID
created_at: datetime
updated_at: datetime
expires_in: Optional[datetime]
github_username: str
description: Optional[str]
active: bool
roles: List[Role]

class Config:
from_attributes = True

class UpdateAPIKey(BaseModel):
description: Optional[str] = None
active: Optional[bool] = None
roles: Optional[List[Role]] = None

class APIKeyResponse(BaseModel):
"""Response returned only when creating an API key - includes the plain key."""
key: str
created_at: datetime
expires_in: Optional[datetime]
description: Optional[str]
roles: List[Role]

9 changes: 7 additions & 2 deletions app/Entities/UserDTOs/user_entity.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from datetime import datetime
from pydantic import BaseModel, field_validator

from Schema.SQL.Enums.enums import Rank, Domain, Tools
from Schema.SQL.Enums.enums import Rank, Domain, Tools, Role

# ----------------------
# Input DTOs
Expand All @@ -27,6 +27,7 @@ class CreateUser(BaseModel):
dream_company_logo: Optional[str] = None
dream_position: Optional[str] = None
tools_to_learn: Optional[List[Tools]] = []
roles: Optional[List[Role]] = []

@field_validator('github_user_name')
def github_user_name_must_not_be_empty(cls, v):
Expand Down Expand Up @@ -66,6 +67,7 @@ class UpdateUser(BaseModel):
dream_company_logo: Optional[str] = None
dream_position: Optional[str] = None
tools_to_learn: Optional[List[Tools]] = None
roles: Optional[List[Role]] = None

@field_validator('github_user_name')
def github_user_name_must_not_be_empty(cls, v):
Expand Down Expand Up @@ -109,6 +111,7 @@ class ReadUser(BaseModel):
dream_company_logo: Optional[str]
dream_position: Optional[str]
tools_to_learn: Optional[List[Tools]]
roles: Optional[List[Role]]
created_at: datetime
updated_at: datetime

Expand Down Expand Up @@ -137,6 +140,7 @@ class OnboardUser(BaseModel):
dream_position: Optional[str] = None
primary_email: str
tools_to_learn: Optional[List[Tools]] = []
roles: Optional[List[Role]] = []

@field_validator('github_user_name', 'linkedin_user_name', 'leetcode_user_name')
def usernames_must_not_be_empty(cls, v):
Expand Down Expand Up @@ -260,4 +264,5 @@ class UpdateUserPersonalDetails(BaseModel):
class ReadUserAuthDetails(BaseModel):
github_user_name: str
user_id: UUID
profile_id: UUID
profile_id: UUID
roles: List[Role]
Loading