Skip to content

Commit

Permalink
Created scripts, code and documentation for client and server.
Browse files Browse the repository at this point in the history
  • Loading branch information
hvalfangst committed Nov 9, 2024
1 parent efdd681 commit 3987d99
Show file tree
Hide file tree
Showing 69 changed files with 391 additions and 355 deletions.
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/.env_oauth
client/.env_oauth
/infra/terraform.tfvars
/.idea/.gitignore
/infra/.terraform.lock.hcl
Expand Down
243 changes: 221 additions & 22 deletions README.md

Large diffs are not rendered by default.

1 change: 0 additions & 1 deletion client/__init__.py

This file was deleted.

2 changes: 0 additions & 2 deletions client/config/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
# client/config/__init__.py

from .oauth import oauth_settings

__all__ = ["oauth_settings"]
25 changes: 13 additions & 12 deletions client/config/oauth.py
Original file line number Diff line number Diff line change
@@ -1,41 +1,42 @@
# client/config/oauth.py

from dotenv import load_dotenv
from fastapi import HTTPException
from pydantic_settings import BaseSettings
from client import logger
from client.logger import logger

load_dotenv()


class OAuthSettings(BaseSettings):
AZURE_CLIENT_ID: str
AZURE_CLIENT_SECRET: str
AZURE_TENANT_ID: str
API_SCOPE: str
REDIRECT_URI: str
SCOPES: str

class Config:
env_file = ".env_oauth"
env_file = "client/.env_oauth"


def initialize_oauth_settings():
try:
# Create an instance of OAuthSettings
internal_oauth_settings = OAuthSettings()
settings = OAuthSettings()

# Check if the required OAuth fields are set
if not internal_oauth_settings.AZURE_CLIENT_ID or not internal_oauth_settings.AZURE_CLIENT_SECRET or not internal_oauth_settings.AZURE_TENANT_ID or not internal_oauth_settings.API_SCOPE:
logger.logger.error("One or more required OAuth environment variables are missing.")
if not settings.AZURE_CLIENT_ID or not settings.AZURE_CLIENT_SECRET \
or not settings.AZURE_TENANT_ID or not settings.REDIRECT_URI\
or not settings.SCOPES:
logger.error("One or more required OAuth environment variables are missing.")
raise HTTPException(status_code=500,
detail="Configuration error: Required OAuth environment variables are missing.")

logger.logger.info("OAuth settings loaded successfully.")
return internal_oauth_settings
logger.info("OAuth settings loaded successfully.")
return settings
except FileNotFoundError:
logger.logger.critical(".env file not found.")
logger.critical(".env file not found.")
raise HTTPException(status_code=500, detail="Configuration error: .env file not found.")
except Exception as e:
logger.logger.critical(f"Error loading OAuth settings: {e}")
logger.critical(f"Error loading OAuth settings: {e}")
raise HTTPException(status_code=500,
detail="Configuration error: An error occurred while loading OAuth settings.")

Expand Down
12 changes: 0 additions & 12 deletions client/logger.py

This file was deleted.

3 changes: 3 additions & 0 deletions client/logger/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from .logger import logger

__all__ = ["logger"]
8 changes: 8 additions & 0 deletions client/logger/logger.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import logging

logging.basicConfig(
level=logging.INFO,
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
)

logger = logging.getLogger("logger")
4 changes: 2 additions & 2 deletions client/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
from client.routers import auth, heroes

app = FastAPI(
title="Hero API",
description="An API to manage heroes secure by OAuth 2.0 auth code flow",
title="Hvalfangst Client",
description="Client accessing our server deployed on Azure Web Apps secured by OAuth 2.0 authorization code flow with OIDC",
version="1.0.0"
)

Expand Down
6 changes: 2 additions & 4 deletions client/models/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
# client/models/__init__.py
from .hero import Hero

from .dnd_hero import DnDHero, AbilityScores, SkillProficiencies, Equipment, Spell

__all__ = ["DnDHero", "AbilityScores", "SkillProficiencies", "Equipment", "Spell"]
__all__ = ["Hero"]
12 changes: 0 additions & 12 deletions client/models/ability_scores.py

This file was deleted.

34 changes: 0 additions & 34 deletions client/models/dnd_hero.py

This file was deleted.

10 changes: 0 additions & 10 deletions client/models/equipment.py

This file was deleted.

19 changes: 19 additions & 0 deletions client/models/hero.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
from typing import Optional
from pydantic import BaseModel


class Hero(BaseModel):
id: str
name: str
race: str
class_: str # Avoids conflict with the Python `class` keyword
level: int
background: Optional[str] = None
alignment: Optional[str] = None
hit_points: int
armor_class: int
speed: int
personality_traits: Optional[str] = None
ideals: Optional[str] = None
bonds: Optional[str] = None
flaws: Optional[str] = None
24 changes: 0 additions & 24 deletions client/models/skill_proficiencies.py

This file was deleted.

13 changes: 0 additions & 13 deletions client/models/spell.py

This file was deleted.

8 changes: 4 additions & 4 deletions client/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
fastapi==0.115.2 # FastAPI framework for building APIs
uvicorn==0.32.0 # ASGI server for running FastAPI apps
pydantic==2.9.2 # Data validation and parsing for FastAPI models
fastapi==0.115.2
uvicorn==0.32.0
pydantic==2.9.2
config~=0.5.1
dotenv~=0.0.5
python-dotenv==1.0.1
httpx==0.27.2
jwt==1.3.1
pyjwt==2.9.0
pydantic_settings==2.6.0
2 changes: 0 additions & 2 deletions client/routers/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1 @@
# client/routers/__init__.py

__all__ = ["heroes", "auth"]
9 changes: 3 additions & 6 deletions client/routers/auth.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
# client/routers/auth.py

from http.client import HTTPException
from fastapi import APIRouter, HTTPException, Request
from client.logger import logger
Expand All @@ -8,7 +6,6 @@
router = APIRouter()


# Example usage in the callback route
@router.get("/callback")
async def auth_callback(request: Request):
"""Callback handler for OpenID Connect flow."""
Expand All @@ -24,9 +21,9 @@ async def auth_callback(request: Request):
# Call the OpenID Connect handler function
try:
logger.info("Initiating OpenID Connect flow handling")
result = await handle_openid_connect_flow(code)
decoded_tokens = await handle_openid_connect_flow(code)
logger.info("OpenID Connect flow completed successfully")
return result
return decoded_tokens
except Exception as e:
logger.error(f"An error occurred during OpenID Connect flow: {str(e)}")
raise HTTPException(status_code=500, detail="Internal server error during OpenID Connect flow")
raise HTTPException(status_code=500, detail="Internal server error during OpenID Connect flow")
75 changes: 44 additions & 31 deletions client/routers/heroes.py
Original file line number Diff line number Diff line change
@@ -1,53 +1,66 @@
# client/routers/heroes.py

import os
from http.client import HTTPException
from typing import List

from fastapi import APIRouter
from fastapi import HTTPException
import httpx
from fastapi import APIRouter, HTTPException

from client.models.dnd_hero import DnDHero
from client.services.auth_service import verify_scope
from client.services.hero_service import HeroService
from client.logger import logger
from client.models import Hero
from client.services.token_storage import get_stored_token # Import get_stored_token function

router = APIRouter()
hero_service = HeroService()

# Set values based on environment variable or hard-coded URL
BACKEND_API_BASE_URL = os.getenv("HVALFANGST_API_URL", "https://hvalfangstlinuxwebapp.azurewebsites.net/api")


# Helper function to make HTTP requests to the backend API
async def request_backend(method: str, endpoint: str, json=None):
url = f"{BACKEND_API_BASE_URL}{endpoint}"

# Retrieve the access token from token storage
token_data = get_stored_token()
headers = {"Authorization": f"Bearer {token_data}"} if token_data else {}

# Log the request details
logger.info(f"Preparing {method} request to URL: {url}")
logger.info(f"Headers: {headers}")
logger.info(f"Payload: {json}")

try:
async with httpx.AsyncClient() as client:
response = await client.request(method, url, json=json, headers=headers)
response.raise_for_status()
logger.info(f"Request to {url} completed successfully with status code {response.status_code}")
return response.json()
except httpx.HTTPStatusError as e:
logger.error(f"HTTP error occurred for {method} request to {url}: {e.response.status_code} - {e.response.text}")
raise HTTPException(status_code=e.response.status_code, detail=e.response.text)
except Exception as e:
logger.error(f"An unexpected error occurred: {e}")
raise HTTPException(status_code=500, detail="An unexpected error occurred")


# POST: Create a new Hero
@router.post("/heroes/", response_model=DnDHero)
async def create_hero(hero: DnDHero):
return await hero_service.create_hero(hero)
@router.post("/heroes/", response_model=Hero)
async def create_hero(hero: Hero):
return await request_backend("POST", "/heroes/", json=hero.dict())


# GET: Retrieve a hero by ID
@router.get("/heroes/{hero_id}", response_model=DnDHero)
@router.get("/heroes/{hero_id}", response_model=Hero)
async def read_hero(hero_id: str):
hero = await hero_service.get_hero(hero_id)
if hero:
return hero
else:
raise HTTPException(status_code=404, detail="Hero not found")
return await request_backend("GET", f"/heroes/{hero_id}")


# GET: Retrieve all heroes
@router.get("/heroes/", response_model=List[DnDHero])
@router.get("/heroes/", response_model=List[Hero])
async def read_heroes():
await verify_scope(["Heroes.Read"])
return await hero_service.list_heroes()
return await request_backend("GET", "/heroes/")


# DELETE: Delete a hero by ID
@router.delete("/heroes/{hero_id}", response_model=dict)
async def delete_hero(hero_id: str):
success = await hero_service.delete_hero(hero_id)
if success:
return {"message": f"Hero with id '{hero_id}' deleted successfully"}
else:
raise HTTPException(status_code=404, detail="Hero not found")


# GET: Custom query to retrieve heroes with Fireball spell and AC < 20
@router.get("/heroes-fireball-low-ac", response_model=List[DnDHero])
async def get_fireball_heroes_with_low_ac():
return await hero_service.query_heroes_fireball_low_ac()
return await request_backend("DELETE", f"/heroes/{hero_id}")
Loading

0 comments on commit 3987d99

Please sign in to comment.