Initial commit

This commit is contained in:
Chris Milne 2026-04-06 12:41:49 +01:00
commit 376a7a9fe5
71 changed files with 2326 additions and 0 deletions

7
src/user/config.py Normal file
View file

@ -0,0 +1,7 @@
"""
Configurations for user module
Configurations:
- List: Description
- Configs: Description
"""

7
src/user/constants.py Normal file
View file

@ -0,0 +1,7 @@
"""
Constants and error codes for user module
Constants:
- List: Description
- Consts: Description
"""

11
src/user/dependencies.py Normal file
View file

@ -0,0 +1,11 @@
"""
Router dependencies for user module
Classes:
- List: Description
- Classes: Description
Functions:
- List: Description
- Functions: Description
"""

7
src/user/exceptions.py Normal file
View file

@ -0,0 +1,7 @@
"""
Module specific exceptions for user module
Exceptions:
- List: Description
- Exceptions: Description
"""

19
src/user/models.py Normal file
View file

@ -0,0 +1,19 @@
"""
Database models for user module
Models:
- User - id[pk], email, first_name, last_name, oidc_id
"""
from sqlalchemy import Column, Integer, String
from src.database import Base
class User(Base):
__tablename__ = "user"
id = Column(Integer, primary_key=True)
email = Column(String)
first_name = Column(String)
last_name = Column(String)
oidc_id = Column(String, index=True, unique=True)

133
src/user/router.py Normal file
View file

@ -0,0 +1,133 @@
"""
Router endpoints for user module
Endpoints:
- [get]/me/claims - Retrieves user's OIDC claims
- [get]/me/db - Retrieves the user data from the db that corresponds to the current OIDC user
- [get]/me/orgs - Retrieves all organisations associated with the current user
- [get]/me/orgs/admin - Retrieves only admin organisations for the current user
- [get]/{user_id} - Retrieves a specific user by their ID
- [get]/{user_id}/orgs - Retrieves all organisations associated with a specific user
- [get]/{user_id}/orgs/admin - Retrieves only admin organisations for a specific user
- [delete]/{user_id} - Deletes a user from the db by their db ID
"""
from fastapi import APIRouter, HTTPException
from fastapi.params import Path
from sqlalchemy.sql import exists
from src.user.models import User
from src.user.schemas import UserResponse, OIDCUser, OrgResponse
from src.organisation.models import OrgUsers, Organisation
from src.auth.service import claims_dependency
from src.database import db_dependency
router = APIRouter(
prefix="/user",
tags=["user"],
)
@router.get("/me/claims")
async def current_user_claims(user: claims_dependency):
return user
@router.get("/me/db", response_model=OIDCUser)
async def current_user(user: claims_dependency, db: db_dependency):
db_id = user.get("db_id", None)
if db_id is None:
raise HTTPException(status_code=404, detail="User not found in db")
user_model = (db.query(User).filter(User.id == db_id).first())
if user_model is None:
raise HTTPException(status_code=404, detail="User not found")
return user_model
@router.get("/me/orgs", response_model=list[OrgResponse])
async def get_current_organisations(db: db_dependency, user: claims_dependency):
user_id = user.get("db_id", None)
if user_id is None:
raise HTTPException(status_code=404, detail="User not found")
user_exists = db.query(exists().where(User.id == user_id)).scalar()
if not user_exists:
raise HTTPException(status_code=404, detail="User not found")
org_user_models = (db.query(OrgUsers.org_id, OrgUsers.is_admin, Organisation.name)
.join(OrgUsers, Organisation.id == OrgUsers.org_id)
.filter(OrgUsers.user_id == user_id)
.all()
)
return org_user_models
@router.get("/me/orgs/admin", response_model=list[OrgResponse])
async def get_current_admin_organisations(db: db_dependency, user: claims_dependency):
user_id = user.get("db_id", None)
if user_id is None:
raise HTTPException(status_code=404, detail="User not found")
user_exists = db.query(exists().where(User.id == user_id)).scalar()
if not user_exists:
raise HTTPException(status_code=404, detail="User not found")
org_user_models = (db.query(OrgUsers.org_id, OrgUsers.is_admin, Organisation.name)
.join(OrgUsers, Organisation.id == OrgUsers.org_id)
.filter(OrgUsers.user_id == user_id)
.filter(OrgUsers.is_admin == True)
.all()
)
return org_user_models
@router.get("/{user_id}", response_model=UserResponse)
async def get_user_by_id(user_id: int, db: db_dependency):
user_model = (db.query(User).filter(User.id == user_id).first())
if user_model is None:
raise HTTPException(status_code=404, detail="User not found")
return user_model
@router.get("/{user_id}/orgs", response_model=list[OrgResponse])
async def get_organisations(db: db_dependency, user_id: int = Path(gt=0)):
user_exists = db.query(exists().where(User.id == user_id)).scalar()
if not user_exists:
raise HTTPException(status_code=404, detail="User not found")
org_user_models = (db.query(OrgUsers.org_id, OrgUsers.is_admin, Organisation.name)
.join(OrgUsers, Organisation.id == OrgUsers.org_id)
.filter(OrgUsers.user_id == user_id)
.all()
)
return org_user_models
@router.get("/{user_id}/orgs/admin", response_model=list[OrgResponse])
async def get_admin_organisations(db: db_dependency, user_id: int = Path(gt=0)):
user_exists = db.query(exists().where(User.id == user_id)).scalar()
if not user_exists:
raise HTTPException(status_code=404, detail="User not found")
org_user_models = (db.query(OrgUsers.org_id, OrgUsers.is_admin, Organisation.name)
.join(OrgUsers, Organisation.id == OrgUsers.org_id)
.filter(OrgUsers.user_id == user_id)
.filter(OrgUsers.is_admin == True)
.all()
)
return org_user_models
@router.delete("/{user_id}")
async def delete_user_by_id(user_id: int, db: db_dependency):
user_model = (db.query(User).filter(User.id == user_id).first())
if user_model is None:
raise HTTPException(status_code=404, detail="User not found")
db.delete(user_model)
db.commit()

28
src/user/schemas.py Normal file
View file

@ -0,0 +1,28 @@
"""
Pydantic models for user module
Models:
- List: Description
- Models: Description
"""
from src.schemas import CustomBaseModel
from pydantic import Field
class OIDCUser(CustomBaseModel):
first_name: str
last_name: str
email: str
oidc_id: str
class UserResponse(CustomBaseModel):
first_name: str
last_name: str
email: str
class OrgResponse(CustomBaseModel):
org_id: int
name: str
is_admin: bool

35
src/user/service.py Normal file
View file

@ -0,0 +1,35 @@
"""
Module specific business logic for user module
Functions:
- add_user_to_db
Exports:
- add_user_to_db
"""
from authlib.jose import JWTClaims
from fastapi import HTTPException
from src.user.schemas import OIDCUser
from src.user.models import User
from src.database import get_db
async def add_user_to_db(user_claims: JWTClaims) -> int:
try:
valid_user = OIDCUser(first_name=user_claims["given_name"], last_name=user_claims["family_name"], email=user_claims["email"], oidc_id=user_claims["sub"])
except Exception as e:
print(e)
raise HTTPException(status_code=422, detail="Invalid or missing OIDC data")
db = next(get_db())
db_user = db.query(User).filter(User.oidc_id == valid_user.oidc_id).first()
if not db_user:
user_model = User(**valid_user.model_dump())
db.add(user_model)
db.commit()
return user_model.id
else:
# Verify details still match and update accordingly.
return db_user.id

11
src/user/utils.py Normal file
View file

@ -0,0 +1,11 @@
"""
Non-business logic reusable functions and classes for user module
Classes:
- List: Description
- Classes: Description
Functions:
- List: Description
- Functions: Description
"""