From a7520769d02e786291b8e609d12a15129ee72539 Mon Sep 17 00:00:00 2001
From: Jonathan Styles <github@jontyms.com>
Date: Sun, 17 Mar 2024 22:14:12 -0400
Subject: [PATCH 1/9] Reworked Config Switched config to a type validated
 pydantic setting class Introduces breaking changes to config file add support
 for bitwarden secrets manager

---
 config.yml             |  16 +++--
 index.py               |  84 +++++++++++++-------------
 requirements.txt       |   4 ++
 routes/admin.py        |  35 ++++++-----
 routes/api.py          |  14 ++---
 routes/infra.py        |  26 ++++----
 routes/stripe.py       |  20 +++---
 routes/wallet.py       |   6 +-
 util/approve.py        |  22 +++----
 util/authentication.py |  34 +++++------
 util/discord.py        |   8 +--
 util/email.py          |  15 ++---
 util/forms.py          |  10 +++
 util/options.py        | 134 ++++++++++++++++++++++++++++++++++-------
 14 files changed, 260 insertions(+), 168 deletions(-)
 create mode 100644 util/forms.py

diff --git a/config.yml b/config.yml
index 85f9838..aeebfbc 100644
--- a/config.yml
+++ b/config.yml
@@ -2,9 +2,8 @@
 jwt:
     secret:
     algorithm: "HS256"
-    lifetime:  # (in seconds)
-        user: 9072000  # 15 weeks
-        sudo: 86400    # 1 day
+    lifetime_user: 9072000  # 15 weeks
+    lifetime_sudo: 86400    # 1 day
 
 http:
     domain: join.hackucf.org
@@ -28,12 +27,11 @@ discord:
 
 
 stripe:
-    api_key: ""
-    webhook_secret: ""
-    price_id: ""
-    url:
-        success: "https://join.hackucf.org/final/"
-        failure: "https://join.hackucf.org/pay/"
+    api_key:
+    webhook_secret:
+    price_id:
+    url_success: "https://join.hackucf.org/final/"
+    url_failure: "https://join.hackucf.org/pay/"
 
 aws:
     dynamodb:
diff --git a/index.py b/index.py
index b1bf11a..9e6919c 100644
--- a/index.py
+++ b/index.py
@@ -29,18 +29,19 @@
 # Import the page rendering library
 from util.kennelish import Kennelish
 # Import options
-from util.options import Options
+from util.options import Settings
+from util.forms import Forms
 
 ### TODO: TEMP
 os.environ["OAUTHLIB_INSECURE_TRANSPORT"] = "0"
 ###
 
+
 logging.basicConfig(level=logging.DEBUG,
                     format='%(asctime)s - %(levelname)s - %(name)s - %(message)s',
                     datefmt='%Y-%m-%d %H:%M:%S')
 logger = logging.getLogger(__name__)
 
-options = Options.fetch()
 
 # Initiate FastAPI.
 app = FastAPI()
@@ -60,9 +61,9 @@
         f"""clouds:
   hackucf_infra:
     auth:
-      auth_url: {options.get('infra', {}).get('horizon', '')}:5000
-      application_credential_id: {options.get('infra', {}).get('ad', {}).get('application_credential_id', '')}
-      application_credential_secret: {options.get('infra', {}).get('ad', {}).get('application_credential_secret', '')}
+      auth_url: {Settings().infra.horizon}:5000
+      application_credential_id: {Settings().infra.application_credential_id}
+      application_credential_secret: {Settings().infra.application_credential_secret.get_secret_value()}
     region_name: "hack-ucf-0"
     interface: "public"
     identity_api_version: 3
@@ -84,15 +85,15 @@ async def index(request: Request, token: Optional[str] = Cookie(None)):
     infra_email = None
 
     try:
-        payload = jwt.decode(
+        user_jwt = jwt.decode(
             token,
-            options.get("jwt").get("secret"),
-            algorithms=options.get("jwt").get("algorithm"),
+            Settings().jwt.secret.get_secret_value(),
+            algorithms=Settings().jwt.algorithm,
         )
-        is_full_member: bool = payload.get("is_full_member", False)
-        is_admin: bool = payload.get("sudo", False)
-        user_id: bool = payload.get("id", None)
-        infra_email: bool = payload.get("infra_email", None)
+        is_full_member: bool = user_jwt.get("is_full_member", False)
+        is_admin: bool = user_jwt.get("sudo", False)
+        user_id: bool = user_jwt.get("id", None)
+        infra_email: bool = user_jwt.get("infra_email", None)
     except Exception as e:
         logger.exception(e)
         pass
@@ -119,15 +120,14 @@ async def index(request: Request, token: Optional[str] = Cookie(None)):
 async def oauth_transformer(redir: str = "/join/2"):
     # Open redirect check
     hostname = urlparse(redir).netloc
-    if hostname != "" and hostname != options.get("http", {}).get(
-        "domain", "my.hackucf.org"
-    ):
+    print(hostname)
+    if hostname != "" and hostname != Settings().http.domain:
         redir = "/join/2"
 
     oauth = OAuth2Session(
-        options.get("discord").get("client_id"),
-        redirect_uri=options.get("discord").get("redirect_base") + "_redir",
-        scope=options.get("discord").get("scope"),
+        Settings().discord.client_id,
+        redirect_uri=Settings().discord.redirect_base + "_redir",
+        scope=Settings().discord.scope,
     )
     authorization_url, state = oauth.authorization_url(
         "https://discord.com/api/oauth2/authorize"
@@ -156,7 +156,7 @@ async def oauth_transformer_new(
 ):
     # AWS dependencies
     dynamodb = boto3.resource("dynamodb")
-    table = dynamodb.Table(options.get("aws").get("dynamodb").get("table"))
+    table = dynamodb.Table(Settings().aws.table)
 
     # Open redirect check
     if redir == "_redir":
@@ -164,9 +164,7 @@ async def oauth_transformer_new(
 
     hostname = urlparse(redir).netloc
 
-    if hostname != "" and hostname != options.get("http", {}).get(
-        "domain", "my.hackucf.org"
-    ):
+    if hostname != "" and hostname != Settings().http.domain:
         redir = "/join/2"
 
     if code is None:
@@ -179,15 +177,15 @@ async def oauth_transformer_new(
 
     # Get data from Discord
     oauth = OAuth2Session(
-        options.get("discord").get("client_id"),
-        redirect_uri=options.get("discord").get("redirect_base") + "_redir",
-        scope=options.get("discord")["scope"],
+        Settings().discord.client_id,
+        redirect_uri=Settings().discord.redirect_base + "_redir",
+        scope=Settings().discord.scope,
     )
 
     token = oauth.fetch_token(
         "https://discord.com/api/oauth2/token",
-        client_id=options.get("discord").get("client_id"),
-        client_secret=options.get("discord").get("secret"),
+        client_id=Settings().discord.client_id,
+        client_secret=Settings().discord.secret.get_secret_value(),
         # authorization_response=code
         code=code,
     )
@@ -235,20 +233,20 @@ async def oauth_transformer_new(
         # Make user join the Hack@UCF Discord, if it's their first rodeo.
         discord_id = str(discordData["id"])
         headers = {
-            "Authorization": f"Bot {options.get('discord', {}).get('bot_token')}",
+            "Authorization": f"Bot {Settings().discord.bot_token.get_secret_value()}",
             "Content-Type": "application/json",
             "X-Audit-Log-Reason": "Hack@UCF OnboardLite Bot",
         }
         put_join_guild = {"access_token": token["access_token"]}
         requests.put(
-            f"https://discordapp.com/api/guilds/{options.get('discord', {}).get('guild_id')}/members/{discord_id}",
+            f"https://discordapp.com/api/guilds/{Settings().discord.guild_id}/members/{discord_id}",
             headers=headers,
             data=json.dumps(put_join_guild),
         )
 
     data = {
         "id": member_id,
-        "discord_id": int(discordData["id"]),
+        "discord_id": discordData["id"],
         "discord": {
             "email": discordData["email"],
             "mfa": discordData["mfa_enabled"],
@@ -290,8 +288,8 @@ async def oauth_transformer_new(
     }
     bearer = jwt.encode(
         jwtData,
-        options.get("jwt").get("secret"),
-        algorithm=options.get("jwt").get("algorithm"),
+        Settings().jwt.secret.get_secret_value(),
+        algorithm=Settings().jwt.algorithm,
     )
     rr = RedirectResponse(redir, status_code=status.HTTP_302_FOUND)
     rr.set_cookie(key="token", value=bearer)
@@ -325,16 +323,16 @@ async def join(request: Request, token: Optional[str] = Cookie(None)):
 async def profile(
     request: Request,
     token: Optional[str] = Cookie(None),
-    payload: Optional[object] = {},
+    user_jwt: Optional[object] = {},
 ):
     # Get data from DynamoDB
     dynamodb = boto3.resource("dynamodb")
-    table = dynamodb.Table(options.get("aws").get("dynamodb").get("table"))
+    table = dynamodb.Table(Settings().aws.table)
 
-    user_data = table.get_item(Key={"id": payload.get("id")}).get("Item", None)
+    user_data = table.get_item(Key={"id": user_jwt.get("id")}).get("Item", None)
 
     # Re-run approval workflow.
-    Approve.approve_member(payload.get("id"))
+    Approve.approve_member(user_jwt.get("id"))
 
     return templates.TemplateResponse(
         "profile.html", {"request": request, "user_data": user_data}
@@ -351,20 +349,20 @@ async def profile(
 async def forms(
     request: Request,
     token: Optional[str] = Cookie(None),
-    payload: Optional[object] = {},
+    user_jwt: Optional[object] = {},
     num: str = 1,
 ):
     # AWS dependencies
     dynamodb = boto3.resource("dynamodb")
-    table = dynamodb.Table(options.get("aws").get("dynamodb").get("table"))
+    table = dynamodb.Table(Settings().aws.table)
 
     if num == "1":
         return RedirectResponse("/join/", status_code=status.HTTP_302_FOUND)
 
-    data = Options.get_form_body(num)
+    data = Forms.get_form_body(num)
 
     # Get data from DynamoDB
-    user_data = table.get_item(Key={"id": payload.get("id")}).get("Item", None)
+    user_data = table.get_item(Key={"id": user_jwt.get("id")}).get("Item", None)
 
     # Have Kennelish parse the data.
     body = Kennelish.parse(data, user_data)
@@ -374,9 +372,9 @@ async def forms(
         "form.html",
         {
             "request": request,
-            "icon": payload["pfp"],
-            "name": payload["name"],
-            "id": payload["id"],
+            "icon": user_jwt["pfp"],
+            "name": user_jwt["name"],
+            "id": user_jwt["id"],
             "body": body,
         },
     )
diff --git a/requirements.txt b/requirements.txt
index 9dd4a7a..1957628 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -74,3 +74,7 @@ uvloop==0.19.0
 virtualenv==20.25.1
 watchfiles==0.21.0
 websockets==12.0
+jinja2
+commonmark
+redis
+pydantic_settings
diff --git a/routes/admin.py b/routes/admin.py
index 3952a28..5d6bf7a 100644
--- a/routes/admin.py
+++ b/routes/admin.py
@@ -2,7 +2,7 @@
 
 import boto3
 from boto3.dynamodb.conditions import Attr
-from fastapi import APIRouter, Body, Cookie, Request, Response
+from fastapi import APIRouter, Body, Cookie, Request, Response, Depends
 from fastapi.encoders import jsonable_encoder
 from fastapi.templating import Jinja2Templates
 from jose import jwt
@@ -13,15 +13,18 @@
 from util.discord import Discord
 from util.email import Email
 from util.errors import Errors
-from util.options import Options
+from util.options import Settings
 
-options = Options.fetch()
 
 templates = Jinja2Templates(directory="templates")
 
 router = APIRouter(prefix="/admin", tags=["Admin"], responses=Errors.basic_http())
 
 
+@router.get("/test")
+async def test(request: Request):
+    return {"msg": Settings().jwt.secret.get_secret_value()}
+
 @router.get("/")
 @Authentication.admin
 async def admin(request: Request, token: Optional[str] = Cookie(None)):
@@ -30,8 +33,8 @@ async def admin(request: Request, token: Optional[str] = Cookie(None)):
     """
     payload = jwt.decode(
         token,
-        options.get("jwt").get("secret"),
-        algorithms=options.get("jwt").get("algorithm"),
+        Settings().jwt.secret.get_secret_value(),
+        algorithms=Settings().jwt.algorithm,
     )
     return templates.TemplateResponse(
         "admin_searcher.html",
@@ -66,7 +69,7 @@ async def get_infra(
 
     # Get user data
     dynamodb = boto3.resource("dynamodb")
-    table = dynamodb.Table(options.get("aws").get("dynamodb").get("table"))
+    table = dynamodb.Table(Settings().aws.table)
 
     user_data = table.get_item(Key={"id": member_id}).get("Item", None)
 
@@ -75,16 +78,16 @@ async def get_infra(
 
 We are happy to grant you Hack@UCF Private Cloud access!
 
-These credentials can be used to the Hack@UCF Private Cloud. This can be accessed at {options.get('infra', {}).get('horizon')} while on the CyberLab WiFi.
+These credentials can be used to the Hack@UCF Private Cloud. This can be accessed at {Settings().infra.horizon} while on the CyberLab WiFi.
 
 ```
 Username: {creds.get('username', 'Not Set')}
-Password: {creds.get('password', f"Please visit https://{options.get('http', {}).get('domain')}/profile and under Danger Zone, reset your Infra creds.")}
+Password: {creds.get('password', f"Please visit https://{Settings().http.domain}/profile and under Danger Zone, reset your Infra creds.")}
 ```
 
 By using the Hack@UCF Infrastructure, you agree to the following EULA located at https://help.hackucf.org/misc/eula
 
-The password for the `Cyberlab` WiFi is currently `{options.get('infra', {}).get('wifi')}`, but this is subject to change (and we'll let you know when that happens).
+The password for the `Cyberlab` WiFi is currently `{Settings().infra.wifi}`, but this is subject to change (and we'll let you know when that happens).
 
 Happy Hacking,
   - Hack@UCF Bot
@@ -112,7 +115,7 @@ async def get_refresh(
     Approve.approve_member(member_id)
 
     dynamodb = boto3.resource("dynamodb")
-    table = dynamodb.Table(options.get("aws").get("dynamodb").get("table"))
+    table = dynamodb.Table(Settings().aws.table)
     data = table.get_item(Key={"id": member_id}).get("Item", None)
 
     if not data:
@@ -135,7 +138,7 @@ async def admin_get_single(
         return {"data": {}, "error": "Missing ?member_id"}
 
     dynamodb = boto3.resource("dynamodb")
-    table = dynamodb.Table(options.get("aws").get("dynamodb").get("table"))
+    table = dynamodb.Table(Settings().aws.table)
     data = table.get_item(Key={"id": member_id}).get("Item", None)
 
     if not data:
@@ -159,7 +162,7 @@ async def admin_get_snowflake(
         return {"data": {}, "error": "Missing ?discord_id"}
 
     dynamodb = boto3.resource("dynamodb")
-    table = dynamodb.Table(options.get("aws").get("dynamodb").get("table"))
+    table = dynamodb.Table(Settings().aws.table)
     data = table.scan(FilterExpression=Attr("discord_id").eq(str(discord_id))).get(
         "Items"
     )
@@ -193,7 +196,7 @@ async def admin_post_discord_message(
         return {"data": {}, "error": "Missing ?member_id"}
 
     dynamodb = boto3.resource("dynamodb")
-    table = dynamodb.Table(options.get("aws").get("dynamodb").get("table"))
+    table = dynamodb.Table(Settings().aws.table)
     data = table.get_item(Key={"id": member_id}).get("Item", None)
 
     if not data:
@@ -222,7 +225,7 @@ async def admin_edit(
     member_id = input_data.id
 
     dynamodb = boto3.resource("dynamodb")
-    table = dynamodb.Table(options.get("aws").get("dynamodb").get("table"))
+    table = dynamodb.Table(Settings().aws.table)
     old_data = table.get_item(Key={"id": member_id}).get("Item", None)
 
     if not old_data:
@@ -252,7 +255,7 @@ async def admin_list(request: Request, token: Optional[str] = Cookie(None)):
     API endpoint that dumps all users as JSON.
     """
     dynamodb = boto3.resource("dynamodb")
-    table = dynamodb.Table(options.get("aws").get("dynamodb").get("table"))
+    table = dynamodb.Table(Settings().aws.table)
     data = table.scan().get("Items", None)
     return {"data": data}
 
@@ -264,7 +267,7 @@ async def admin_list_csv(request: Request, token: Optional[str] = Cookie(None)):
     API endpoint that dumps all users as CSV.
     """
     dynamodb = boto3.resource("dynamodb")
-    table = dynamodb.Table(options.get("aws").get("dynamodb").get("table"))
+    table = dynamodb.Table(Settings().aws.table)
     data = table.scan().get("Items", None)
 
     output = "Membership ID, First Name, Last Name, NID, Is Returning, Gender, Major, Class Standing, Shirt Size, Discord Username, Experience, Cyber Interests, Event Interest, Is C3 Interest, Comments, Ethics Form Timestamp, Minecraft, Infra Email\n"
diff --git a/routes/api.py b/routes/api.py
index 59fdfa5..450042b 100644
--- a/routes/api.py
+++ b/routes/api.py
@@ -12,9 +12,9 @@
 from util.authentication import Authentication
 from util.errors import Errors
 from util.kennelish import Kennelish, Transformer
-from util.options import Options
+from util.options import Settings
+
 
-options = Options.fetch()
 
 router = APIRouter(prefix="/api", tags=["API"], responses=Errors.basic_http())
 
@@ -47,7 +47,7 @@ async def get_root():
 
 @router.get("/form/{num}")
 async def get_form(num: str):
-    return Options.get_form_body(num)
+    return Forms.get_form_body(num)(num)
 
 
 """
@@ -65,10 +65,10 @@ async def get_form_html(
 ):
     # AWS dependencies
     dynamodb = boto3.resource("dynamodb")
-    table = dynamodb.Table(options.get("aws").get("dynamodb").get("table"))
+    table = dynamodb.Table(Settings().aws.table)
 
     # Get form object
-    data = Options.get_form_body(num)
+    data = Forms.get_form_body(num)(num)
 
     # Get data from DynamoDB
     user_data = table.get_item(Key={"id": user_jwt.get("id")}).get("Item", None)
@@ -93,7 +93,7 @@ async def post_form(
     num: str = 1,
 ):
     # Get Kennelish data
-    kennelish_data = Options.get_form_body(num)
+    kennelish_data = Forms.get_form_body(num)(num)
     model = Transformer.kennelish_to_pydantic(kennelish_data)
 
     # Parse and Validate inputs
@@ -142,7 +142,7 @@ async def post_form(
 
     # AWS dependencies
     dynamodb = boto3.resource("dynamodb")
-    table = dynamodb.Table(options.get("aws").get("dynamodb").get("table"))
+    table = dynamodb.Table(Settings().aws.table)
 
     # Push data back to DynamoDB
     try:
diff --git a/routes/infra.py b/routes/infra.py
index b8c23cf..efe7635 100644
--- a/routes/infra.py
+++ b/routes/infra.py
@@ -19,11 +19,11 @@
 from util.email import Email
 from util.errors import Errors
 from util.limiter import RateLimiter
-from util.options import Options
+from util.options import Settings
+
 
 logger = logging.getLogger(__name__)
 
-options = Options.fetch()
 
 templates = Jinja2Templates(directory="templates")
 
@@ -32,9 +32,7 @@
 tf = Terraform(working_dir="./")
 
 rate_limiter = RateLimiter(
-    options.get("redis").get("host"),
-    options.get("redis").get("port"),
-    options.get("redis").get("db"),
+    Settings().redis.host, Settings().redis.port, Settings().redis.db
 )
 
 rate_limiter.get_redis()
@@ -64,8 +62,8 @@ async def create_resource(project, callback_discord_id=None):
     logger.info(f"Creating resources for {proj_name}...")
 
     tf_vars = {
-        "username": options.get("infra", {}).get("ad", {}).get("username"),
-        "password": options.get("infra", {}).get("ad", {}).get("password"),
+        "application_credential_id": Settings().infra.application_credential_id,
+        "application_credential_secret": Settings().infra.application_credential_secret.get_secret_value(),
         "tenant_name": proj_name,
         "gbmname": shitty_database.get("gbmName"),
         "imageid": shitty_database.get("imageId"),
@@ -91,7 +89,7 @@ async def create_resource(project, callback_discord_id=None):
     if callback_discord_id:
         resource_create_msg = f"""Hello!
 
-Your requested virtual machine has been created! You can now view it at {options.get('infra', {}).get('horizon')}.
+Your requested virtual machine has been created! You can now view it at {Settings().infra.horizon}.
 
 Enjoy,
     - Hack@UCF Bot
@@ -226,7 +224,7 @@ async def get_teardown(request: Request, token: Optional[str] = Cookie(None)):
 
 
 """
-API endpoint to SET the one-click deploy settings.
+API endpoint to SET the one-click deploy Settings().
 """
 
 
@@ -241,7 +239,7 @@ async def get_options(
 
 
 """
-API endpoint to SET the one-click deploy settings.
+API endpoint to SET the one-click deploy Settings().
 """
 
 
@@ -290,7 +288,7 @@ async def get_infra(
 
     # Get user data
     dynamodb = boto3.resource("dynamodb")
-    table = dynamodb.Table(options.get("aws").get("dynamodb").get("table"))
+    table = dynamodb.Table(Settings().aws.table)
 
     user_data = table.get_item(Key={"id": member_id}).get("Item", None)
 
@@ -299,14 +297,14 @@ async def get_infra(
 
 You have requested to reset your Hack@UCF Infrastructure credentials. This change comes with new credentials.
 
-A reminder that you can use these credentials at {options.get('infra', {}).get('horizon')} while on the CyberLab WiFi.
+A reminder that you can use these credentials at {Settings().infra.horizon} while on the CyberLab WiFi.
 
 ```
 Username: {creds.get('username', 'Not Set')}
-Password: {creds.get('password', f"Please visit https://{options.get('http', {}).get('domain')}/profile and under Danger Zone, reset your Infra creds.")}
+Password: {creds.get('password', f"Please visit https://{Settings().http.domain}/profile and under Danger Zone, reset your Infra creds.")}
 ```
 
-The password for the `Cyberlab` WiFi is currently `{options.get('infra', {}).get('wifi')}`, but this is subject to change (and we'll let you know when that happens).
+The password for the `Cyberlab` WiFi is currently `{Settings().infra.wifi}`, but this is subject to change (and we'll let you know when that happens).
 
 By using the Hack@UCF Infrastructure, you agree to the following EULA located at https://help.hackucf.org/misc/eula
 
diff --git a/routes/stripe.py b/routes/stripe.py
index 16519a9..10551c7 100644
--- a/routes/stripe.py
+++ b/routes/stripe.py
@@ -11,9 +11,9 @@
 from util.approve import Approve
 from util.authentication import Authentication
 from util.errors import Errors
-from util.options import Options
+from util.options import Settings
+
 
-options = Options.fetch()
 templates = Jinja2Templates(directory="templates")
 
 
@@ -22,7 +22,7 @@
 router = APIRouter(prefix="/pay", tags=["API"], responses=Errors.basic_http())
 
 # Set Stripe API key.
-stripe.api_key = options.get("stripe").get("api_key")
+stripe.api_key = Settings().stripe.api_key.get_secret_value()
 
 
 """
@@ -39,7 +39,7 @@ async def get_root(
 ):
     # AWS dependencies
     dynamodb = boto3.resource("dynamodb")
-    table = dynamodb.Table(options.get("aws").get("dynamodb").get("table"))
+    table = dynamodb.Table(Settings().aws.table)
 
     # Get data from DynamoDB
     user_data = table.get_item(Key={"id": user_jwt.get("id")}).get("Item", None)
@@ -70,7 +70,7 @@ async def create_checkout_session(
 ):
     # AWS dependencies
     dynamodb = boto3.resource("dynamodb")
-    table = dynamodb.Table(options.get("aws").get("dynamodb").get("table"))
+    table = dynamodb.Table(Settings().aws.table)
 
     # Get data from DynamoDB
     user_data = table.get_item(Key={"id": user_jwt.get("id")}).get("Item", None)
@@ -81,14 +81,14 @@ async def create_checkout_session(
             line_items=[
                 {
                     # Provide the exact Price ID (for example, pr_1234) of the product you want to sell
-                    "price": options.get("stripe").get("price_id"),
+                    "price": Settings().stripe.price_id,
                     "quantity": 1,
                 },
             ],
             customer_email=stripe_email,
             mode="payment",
-            success_url=options.get("stripe").get("url").get("success"),
-            cancel_url=options.get("stripe").get("url").get("failure"),
+            success_url=Settings().stripe.url_success,
+            cancel_url=Settings().stripe.url_failure,
         )
     except Exception as e:
         return str(e)
@@ -101,7 +101,7 @@ async def webhook(request: Request):
     payload = await request.body()
     sig_header = request.headers.get("stripe-signature")
     event = None
-    endpoint_secret = options.get("stripe").get("webhook_secret")
+    endpoint_secret = Settings().stripe.webhook_secret.get_secret_value()
 
     try:
         event = stripe.Webhook.construct_event(payload, sig_header, endpoint_secret)
@@ -136,7 +136,7 @@ def pay_dues(session):
 
     # AWS dependencies
     dynamodb = boto3.resource("dynamodb")
-    table = dynamodb.Table(options.get("aws").get("dynamodb").get("table"))
+    table = dynamodb.Table(Settings().aws.table)
 
     # Get data from DynamoDB
     response = table.scan(FilterExpression=Attr("email").eq(customer_email)).get(
diff --git a/routes/wallet.py b/routes/wallet.py
index 05d32df..f3649fa 100644
--- a/routes/wallet.py
+++ b/routes/wallet.py
@@ -12,9 +12,9 @@
 from models.user import PublicContact
 from util.authentication import Authentication
 from util.errors import Errors
-from util.options import Options
+from util.options import Settings
+
 
-options = Options.fetch()
 
 router = APIRouter(
     prefix="/wallet", tags=["API", "MobileWallet"], responses=Errors.basic_http()
@@ -240,7 +240,7 @@ async def aapl_gen(
     user_jwt: Optional[object] = {},
 ):
     dynamodb = boto3.resource("dynamodb")
-    table = dynamodb.Table(options.get("aws").get("dynamodb").get("table"))
+    table = dynamodb.Table(Settings().aws.table)
 
     # Get data from DynamoDB
     user_data = table.get_item(Key={"id": user_jwt.get("id")}).get("Item", None)
diff --git a/util/approve.py b/util/approve.py
index f1cac08..2636944 100644
--- a/util/approve.py
+++ b/util/approve.py
@@ -8,12 +8,12 @@
 from util.discord import Discord
 from util.email import Email
 from util.horsepass import HorsePass
-from util.options import Options
+from util.options import Settings
 
 logger = logging.getLogger()
 
-options = Options.fetch()
-tf = Terraform(working_dir=options.get("infra", {}).get("tf_directory", "./"))
+
+tf = Terraform(working_dir=Settings().infra.tf_directory)
 
 """
 This function will ensure a member meets all requirements to be a member, and if so, creates an
@@ -43,7 +43,7 @@ def provision_infra(member_id, user_data=None):
 
         try:
             dynamodb = boto3.resource("dynamodb")
-            table = dynamodb.Table(options.get("aws").get("dynamodb").get("table"))
+            table = dynamodb.Table(Settings().aws.table)
             if not user_data:
                 user_data = table.get_item(Key={"id": member_id}).get("Item", None)
 
@@ -126,7 +126,7 @@ def provision_infra(member_id, user_data=None):
     def approve_member(member_id):
         logger.info(f"Re-running approval for {member_id}")
         dynamodb = boto3.resource("dynamodb")
-        table = dynamodb.Table(options.get("aws").get("dynamodb").get("table"))
+        table = dynamodb.Table(Settings().aws.table)
 
         user_data = table.get_item(Key={"id": member_id}).get("Item", None)
 
@@ -164,22 +164,22 @@ def approve_member(member_id):
 
             # Assign the Dues-Paying Member role
             Discord.assign_role(
-                discord_id, options.get("discord", {}).get("member_role")
+                discord_id, Settings().discord.member_role
             )
 
             # Send Discord message saying they are a member
             welcome_msg = f"""Hello {user_data.get('first_name')}, and welcome to Hack@UCF!
 
-This message is to confirm that your membership has processed successfully. You can access and edit your membership ID at https://{options.get('http', {}).get('domain')}/profile.
+This message is to confirm that your membership has processed successfully. You can access and edit your membership ID at https://{Settings().http.domain}/profile.
 
-These credentials can be used to the Hack@UCF Private Cloud, one of our many benefits of paying dues. This can be accessed at {options.get('infra', {}).get('horizon')} while on the CyberLab WiFi.
+These credentials can be used to the Hack@UCF Private Cloud, one of our many benefits of paying dues. This can be accessed at {Settings().infra.horizon} while on the CyberLab WiFi.
 
 ```yaml
 Username: {creds.get('username', 'Not Set')}
-Password: {creds.get('password', f"Please visit https://{options.get('http', {}).get('domain')}/profile and under Danger Zone, reset your Infra creds.")}
+Password: {creds.get('password', f"Please visit https://{Settings().http.domain}/profile and under Danger Zone, reset your Infra creds.")}
 ```
 
-The password for the `Cyberlab` WiFi is currently `{options.get('infra', {}).get('wifi')}`, but this is subject to change (and we'll let you know when that happens).
+The password for the `Cyberlab` WiFi is currently `{Settings().infra.wifi}`, but this is subject to change (and we'll let you know when that happens).
 
 By using the Hack@UCF Infrastructure, you agree to the following EULA located at https://help.hackucf.org/misc/eula
 
@@ -207,7 +207,7 @@ def approve_member(member_id):
 - Signed Ethics Form: {'✅' if user_data.get('ethics_form', {}).get('signtime', 0) != 0 else '❌'}
 - Paid $10 dues: ✅
 
-Please complete all of these to become a full member. Once you do, visit https://{options.get('http', {}).get('domain')}/profile to re-run this check.
+Please complete all of these to become a full member. Once you do, visit https://{Settings().http.domain}/profile to re-run this check.
 
 If you think you have completed all of these, please reach out to an Exec on the Hack@UCF Discord.
 
diff --git a/util/authentication.py b/util/authentication.py
index 3de44f2..090b1ba 100644
--- a/util/authentication.py
+++ b/util/authentication.py
@@ -8,9 +8,9 @@
 
 # Import options and errors
 from util.errors import Errors
-from util.options import Options
+from util.options import Settings
+
 
-options = Options.fetch()
 
 
 class Authentication:
@@ -28,13 +28,13 @@ async def wrapper(request: Request, token: Optional[str], *args, **kwargs):
                 )
 
             try:
-                payload = jwt.decode(
+                user_jwt = jwt.decode(
                     token,
-                    options.get("jwt").get("secret"),
-                    algorithms=options.get("jwt").get("algorithm"),
+                    Settings().jwt.secret.get_secret_value(),
+                    algorithms=Settings().jwt.algorithm,
                 )
-                is_admin: bool = payload.get("sudo", False)
-                creation_date: float = payload.get("issued", -1)
+                is_admin: bool = user_jwt.get("sudo", False)
+                creation_date: float = user_jwt.get("issued", -1)
             except Exception:
                 tr = Errors.generate(
                     request,
@@ -52,9 +52,7 @@ async def wrapper(request: Request, token: Optional[str], *args, **kwargs):
                     essay="If you think this is an error, please try logging in again.",
                 )
 
-            if time.time() > creation_date + options.get("jwt").get("lifetime").get(
-                "sudo"
-            ):
+            if time.time() > creation_date + Settings().jwt.lifetime_sudo:
                 return Errors.generate(
                     request,
                     403,
@@ -72,7 +70,7 @@ def member(func):
         async def wrapper_member(
             request: Request,
             token: Optional[str],
-            payload: Optional[object],
+            user_jwt: Optional[object],
             *args,
             **kwargs
         ):
@@ -84,12 +82,12 @@ async def wrapper_member(
                 )
 
             try:
-                payload = jwt.decode(
+                user_jwt = jwt.decode(
                     token,
-                    options.get("jwt").get("secret"),
-                    algorithms=options.get("jwt").get("algorithm"),
+                    Settings().jwt.secret.get_secret_value(),
+                    algorithms=Settings().jwt.algorithm,
                 )
-                creation_date: float = payload.get("issued", -1)
+                creation_date: float = user_jwt.get("issued", -1)
             except Exception:
                 tr = Errors.generate(
                     request,
@@ -99,9 +97,7 @@ async def wrapper_member(
                 tr.delete_cookie(key="token")
                 return tr
 
-            if time.time() > creation_date + options.get("jwt").get("lifetime").get(
-                "user"
-            ):
+            if time.time() > creation_date + Settings().jwt.lifetime_user:
                 return Errors.generate(
                     request,
                     403,
@@ -109,6 +105,6 @@ async def wrapper_member(
                     essay="Sessions last for about fifteen weeks. You need to re-log-in between semesters.",
                 )
 
-            return await func(request, token, payload, *args, **kwargs)
+            return await func(request, token, user_jwt, *args, **kwargs)
 
         return wrapper_member
diff --git a/util/discord.py b/util/discord.py
index 67d1609..bec58fb 100644
--- a/util/discord.py
+++ b/util/discord.py
@@ -2,12 +2,12 @@
 
 import requests
 
-from util.options import Options
+from util.options import Settings
+
 
-options = Options.fetch()
 
 headers = {
-    "Authorization": f"Bot {options.get('discord', {}).get('bot_token')}",
+    "Authorization": f"Bot {Settings().discord.bot_token.get_secret_value()}",
     "Content-Type": "application/json",
     "X-Audit-Log-Reason": "Hack@UCF OnboardLite Bot",
 }
@@ -25,7 +25,7 @@ def assign_role(discord_id, role_id):
         discord_id = str(discord_id)
 
         req = requests.put(
-            f"https://discord.com/api/guilds/{options.get('discord', {}).get('guild_id')}/members/{discord_id}/roles/{role_id}",
+            f"https://discord.com/api/guilds/{Settings().discord.guild_id}/members/{discord_id}/roles/{role_id}",
             headers=headers,
         )
 
diff --git a/util/email.py b/util/email.py
index 43705be..c7e7ee4 100644
--- a/util/email.py
+++ b/util/email.py
@@ -4,19 +4,12 @@
 
 import commonmark
 
-from util.options import Options
+from util.options import Settings
 
-options = Options.fetch()
 
-
-# from util.options import Options
-options = Options.fetch()
-
-email = options.get("email", {}).get("email", {})
-
-email = options.get("email", {}).get("email", {})
-password = options.get("email", {}).get("password", {})
-smtp_host = options.get("email", {}).get("smtp_server", {})
+email = Settings().email.email
+password = Settings().email.password.get_secret_value()
+smtp_host = Settings().email.smtp_server
 
 
 class Email:
diff --git a/util/forms.py b/util/forms.py
new file mode 100644
index 0000000..54db84a
--- /dev/null
+++ b/util/forms.py
@@ -0,0 +1,10 @@
+import json
+import os
+
+class Forms:
+    def get_form_body(file="1"):
+        try:
+            form_file = os.path.join(os.getcwd(), "forms", f"{file}.json")
+            return json.load(open(form_file, "r"))
+        except FileNotFoundError:
+            return {}
\ No newline at end of file
diff --git a/util/options.py b/util/options.py
index 48f5b12..6a347e1 100644
--- a/util/options.py
+++ b/util/options.py
@@ -1,31 +1,123 @@
-import json
 import os
-
+from pydantic import BaseModel, SecretStr, constr
+from pydantic_settings import BaseSettings
 import yaml
+import json
+import subprocess
+from functools import lru_cache
+
+#TODO
+# 1. Add constr to strings to enforce length and format
+# 2. Test Bitwarden overwrites
+
+yaml_settings = dict()
+
+here = os.path.abspath(os.path.dirname(__file__))
+with open(os.path.join(here, "../config/options.yml")) as f:
+    yaml_settings.update(yaml.load(f, Loader=yaml.FullLoader))
+def parse_json_to_dict(json_string):
+    data = json.loads(json_string)
+    return {item['key']: item['value'] for item in data}
+
+try:
+    command = f"bws secret list {yaml_settings['bws']['project_id']}"
+    bitwarden_raw = subprocess.check_output(command, shell=True, text=True)
+except Exception as e:
+    print(e)
+bitwarden_settings = parse_json_to_dict(bitwarden_raw)
+
+bitwarden_mapping = {
+    'discord_bot_token': ('discord', 'bot_token'),
+    'discord_client_id': ('discord', 'client_id'),
+    'discord_secret': ('discord', 'secret'),
+    'stripe_api_key': ('stripe', 'api_key'),
+    'stripe_webhook_secret': ('stripe', 'webhook_secret'),
+    'stripe_price_id': ('stripe', 'price_id'),
+    'email_password': ('email', 'password'),
+    'jwt_secret': ('jwt', 'secret'),
+    'infra_wifi': ('infra', 'wifi'),
+    'infra_application_credential_id': ('infra', 'application_credential_id'),
+    'infra_configuration_credential_secret': ('infra', 'application_credential_secret')
+}
+bitwarden_mapped = {}
+for bw_key, nested_keys in bitwarden_mapping.items():
+    if bw_key in bitwarden_settings:
+        top_key, nested_key = nested_keys
+        if top_key not in bitwarden_mapped:
+            bitwarden_mapped[top_key] = {}
+        bitwarden_mapped[top_key][nested_key] = bitwarden_settings[bw_key]
+
+for top_key, nested_dict in bitwarden_mapped.items():
+    if top_key in yaml_settings:
+        for nested_key, value in nested_dict.items():
+            yaml_settings[top_key][nested_key] = value
+
+class DiscordConfig(BaseModel):
+    bot_token: SecretStr
+    client_id: int
+    guild_id: int
+    member_role: int
+    redirect_base: str
+    scope: str
+    secret: SecretStr
+
+discord_config = DiscordConfig(**yaml_settings['discord'])
+class StripeConfig(BaseModel):
+    api_key: SecretStr
+    webhook_secret: SecretStr
+    price_id: str
+    url_success: str
+    url_failure: str
+stripe_config = StripeConfig(**yaml_settings['stripe']) 
 
+class EmailConfig(BaseModel):
+    smtp_server: str 
+    email: str
+    password: SecretStr
+email_config = EmailConfig(**yaml_settings['email']) 
 
-class Options:
-    def __init__(self):
-        super(Options, self).__init__
+class JwtConfig(BaseModel):
+    secret: SecretStr
+    algorithm: str
+    lifetime_user: int
+    lifetime_sudo: int
+jwt_config = JwtConfig(**yaml_settings['jwt'])
 
-    def fetch(path="config/options.yml"):
-        # Get file path
-        full_path = os.path.join(os.getcwd(), path)
+class DynamodbConfig(BaseModel):
+    table: str
+dynamodb_config = DynamodbConfig(**yaml_settings['aws']['dynamodb'])
 
-        # Load options.
-        with open(full_path, "r") as file:
-            options = yaml.safe_load(file)
+class InfraConfig(BaseModel):
+    wifi: str
+    horizon: str
+    application_credential_id: str
+    application_credential_secret: SecretStr
+    tf_directory: str
+infra_config = InfraConfig(**yaml_settings['infra'])
 
-        return options
+class RedisConfig(BaseModel):
+    host: str
+    port: int
+    db: int
+redis_config = RedisConfig(**yaml_settings['redis'])
 
-    def get(self, arg=None):
-        options = self.fetch()
+class HttpConfig(BaseModel):
+    domain: str 
+http_config = HttpConfig(**yaml_settings['http'])
 
-        return options.get(arg, None)
+class SingletonBaseSettingsMeta(type(BaseSettings), type):
+    _instances = {}
 
-    def get_form_body(file="1"):
-        try:
-            form_file = os.path.join(os.getcwd(), "forms", f"{file}.json")
-            return json.load(open(form_file, "r"))
-        except FileNotFoundError:
-            return {}
+    def __call__(cls, *args, **kwargs):
+        if cls not in cls._instances:
+            cls._instances[cls] = super().__call__(*args, **kwargs)
+        return cls._instances[cls]
+class Settings(BaseSettings, metaclass=SingletonBaseSettingsMeta):
+    discord: DiscordConfig = discord_config
+    stripe: StripeConfig = stripe_config
+    email: EmailConfig = email_config
+    jwt: JwtConfig = jwt_config
+    aws: DynamodbConfig = dynamodb_config
+    infra: InfraConfig = infra_config
+    redis: RedisConfig = redis_config
+    http: HttpConfig = http_config

From 5c036391e20bf47adbc4668b43d92e54a1122785 Mon Sep 17 00:00:00 2001
From: Jonathan Styles <github@jontyms.com>
Date: Mon, 18 Mar 2024 13:39:37 -0400
Subject: [PATCH 2/9] Reworked Options moved bitwarden to module fixed bugs

---
 config.yml      |   8 ++-
 index.py        |  10 +++-
 routes/admin.py |   4 --
 util/options.py | 152 +++++++++++++++++++++++++++++++++---------------
 4 files changed, 120 insertions(+), 54 deletions(-)

diff --git a/config.yml b/config.yml
index aeebfbc..055df64 100644
--- a/config.yml
+++ b/config.yml
@@ -1,4 +1,7 @@
 # Options for Onboard.
+bws:
+    project_id: 
+    enable: 
 jwt:
     secret:
     algorithm: "HS256"
@@ -11,9 +14,8 @@ http:
 infra:
     wifi: ""
     horizon: "https://horizon.hackucf.org"
-    ad:
-      application_credential_id:
-      application_credential_secret:
+    application_credential_id:
+    application_credential_secret:
     tf_directory: "./"
 
 discord:
diff --git a/index.py b/index.py
index 9e6919c..1df18d4 100644
--- a/index.py
+++ b/index.py
@@ -358,8 +358,16 @@ async def forms(
 
     if num == "1":
         return RedirectResponse("/join/", status_code=status.HTTP_302_FOUND)
+    try:
+        data = Forms.get_form_body(num)
+    except Exception:
+        return Errors.generate(
+            request,
+            404,
+            "Form not found",
+            essay="This form does not exist.",
+        )
 
-    data = Forms.get_form_body(num)
 
     # Get data from DynamoDB
     user_data = table.get_item(Key={"id": user_jwt.get("id")}).get("Item", None)
diff --git a/routes/admin.py b/routes/admin.py
index 5d6bf7a..6780536 100644
--- a/routes/admin.py
+++ b/routes/admin.py
@@ -21,10 +21,6 @@
 router = APIRouter(prefix="/admin", tags=["Admin"], responses=Errors.basic_http())
 
 
-@router.get("/test")
-async def test(request: Request):
-    return {"msg": Settings().jwt.secret.get_secret_value()}
-
 @router.get("/")
 @Authentication.admin
 async def admin(request: Request, token: Optional[str] = Cookie(None)):
diff --git a/util/options.py b/util/options.py
index 6a347e1..2217064 100644
--- a/util/options.py
+++ b/util/options.py
@@ -6,53 +6,76 @@
 import subprocess
 from functools import lru_cache
 
-#TODO
-# 1. Add constr to strings to enforce length and format
-# 2. Test Bitwarden overwrites
-
-yaml_settings = dict()
 
+def BitwardenConfig(settings_dict: dict):
+    '''
+    Takes a dict of settings loaded from yaml and adds the secrets from bitwarden to the settings dict.
+    The bitwarden secrets are mapped to the settings dict using the bitwarden_mapping dict.
+    The secrets are sourced based on a project id in the settings dict.
+    '''
+    try:
+        command = f"bws secret list {settings_dict['bws']['project_id']}"
+        bitwarden_raw = subprocess.check_output(command, shell=True, text=True)
+    except Exception as e:
+        print(e)
+    bitwarden_settings = parse_json_to_dict(bitwarden_raw)
+    
+    bitwarden_mapping = {
+        'discord_bot_token': ('discord', 'bot_token'),
+        'discord_client_id': ('discord', 'client_id'),
+        'discord_secret': ('discord', 'secret'),
+        'stripe_api_key': ('stripe', 'api_key'),
+        'stripe_webhook_secret': ('stripe', 'webhook_secret'),
+        'stripe_price_id': ('stripe', 'price_id'),
+        'email_password': ('email', 'password'),
+        'jwt_secret': ('jwt', 'secret'),
+        'infra_wifi': ('infra', 'wifi'),
+        'infra_application_credential_id': ('infra', 'application_credential_id'),
+        'infra_configuration_credential_secret': ('infra', 'application_credential_secret')
+    }
+    
+    bitwarden_mapped = {}
+    for bw_key, nested_keys in bitwarden_mapping.items():
+        if bw_key in bitwarden_settings:
+            top_key, nested_key = nested_keys
+            if top_key not in bitwarden_mapped:
+                bitwarden_mapped[top_key] = {}
+            bitwarden_mapped[top_key][nested_key] = bitwarden_settings[bw_key]
+    
+    for top_key, nested_dict in bitwarden_mapped.items():
+        if top_key in settings_dict:
+            for nested_key, value in nested_dict.items():
+                settings_dict[top_key][nested_key] = value
+    return settings_dict
+    
+settings_dict = dict()
+
+# Reads config from ../config/options.yml
 here = os.path.abspath(os.path.dirname(__file__))
 with open(os.path.join(here, "../config/options.yml")) as f:
-    yaml_settings.update(yaml.load(f, Loader=yaml.FullLoader))
+    settings_dict.update(yaml.load(f, Loader=yaml.FullLoader))
 def parse_json_to_dict(json_string):
     data = json.loads(json_string)
     return {item['key']: item['value'] for item in data}
 
-try:
-    command = f"bws secret list {yaml_settings['bws']['project_id']}"
-    bitwarden_raw = subprocess.check_output(command, shell=True, text=True)
-except Exception as e:
-    print(e)
-bitwarden_settings = parse_json_to_dict(bitwarden_raw)
-
-bitwarden_mapping = {
-    'discord_bot_token': ('discord', 'bot_token'),
-    'discord_client_id': ('discord', 'client_id'),
-    'discord_secret': ('discord', 'secret'),
-    'stripe_api_key': ('stripe', 'api_key'),
-    'stripe_webhook_secret': ('stripe', 'webhook_secret'),
-    'stripe_price_id': ('stripe', 'price_id'),
-    'email_password': ('email', 'password'),
-    'jwt_secret': ('jwt', 'secret'),
-    'infra_wifi': ('infra', 'wifi'),
-    'infra_application_credential_id': ('infra', 'application_credential_id'),
-    'infra_configuration_credential_secret': ('infra', 'application_credential_secret')
-}
-bitwarden_mapped = {}
-for bw_key, nested_keys in bitwarden_mapping.items():
-    if bw_key in bitwarden_settings:
-        top_key, nested_key = nested_keys
-        if top_key not in bitwarden_mapped:
-            bitwarden_mapped[top_key] = {}
-        bitwarden_mapped[top_key][nested_key] = bitwarden_settings[bw_key]
-
-for top_key, nested_dict in bitwarden_mapped.items():
-    if top_key in yaml_settings:
-        for nested_key, value in nested_dict.items():
-            yaml_settings[top_key][nested_key] = value
+# If bitwarden is enabled, add secrets to settings_dict
+if settings_dict.get('bws').get('enable'):
+    settings_dict = BitwardenConfig(settings_dict)
+
 
 class DiscordConfig(BaseModel):
+    """
+    Represents the configuration settings for Discord integration.
+
+    Attributes:
+        bot_token (SecretStr): The secret token for the Discord bot.
+        client_id (int): The client ID for the Discord application.
+        guild_id (int): The ID of the HackUCF discord server.
+        member_role (int): The ID of the role assigned to members.
+        redirect_base (str): The base URL for redirecting after authentication.
+        scope (str): The scope of permissions required for the Discord integration.
+        secret (SecretStr): The secret key for the Discord oauth.
+    """
     bot_token: SecretStr
     client_id: int
     guild_id: int
@@ -61,49 +84,86 @@ class DiscordConfig(BaseModel):
     scope: str
     secret: SecretStr
 
-discord_config = DiscordConfig(**yaml_settings['discord'])
+discord_config = DiscordConfig(**settings_dict['discord'])
 class StripeConfig(BaseModel):
+    """
+    Configuration class for Stripe integration.
+
+    Attributes:
+        api_key (SecretStr): The API key for Stripe.
+        webhook_secret (SecretStr): The webhook secret for Stripe.
+        price_id (str): The ID of the price for the product.
+        url_success (str): The URL to redirect to on successful payment.
+        url_failure (str): The URL to redirect to on failed payment.
+    """
     api_key: SecretStr
     webhook_secret: SecretStr
     price_id: str
     url_success: str
     url_failure: str
-stripe_config = StripeConfig(**yaml_settings['stripe']) 
+stripe_config = StripeConfig(**settings_dict['stripe']) 
 
 class EmailConfig(BaseModel):
+    """
+    Represents the configuration for an email.
+
+    Attributes:
+        smtp_server (str): The SMTP server address.
+        email (str): The email address to send from also used as the login username.
+        password (SecretStr): The password for the email account.
+    """
     smtp_server: str 
     email: str
     password: SecretStr
-email_config = EmailConfig(**yaml_settings['email']) 
+email_config = EmailConfig(**settings_dict['email']) 
 
 class JwtConfig(BaseModel):
+    """
+    Configuration class for JWT (JSON Web Token) settings.
+
+    Attributes:
+        secret (SecretStr): The secret key used for signing and verifying JWTs.
+        algorithm (str): The algorithm used for JWT encryption.
+        lifetime_user (int): The lifetime (in seconds) of a user JWT.
+        lifetime_sudo (int): The lifetime (in seconds) of a sudo JWT.
+    """
     secret: SecretStr
     algorithm: str
     lifetime_user: int
     lifetime_sudo: int
-jwt_config = JwtConfig(**yaml_settings['jwt'])
+jwt_config = JwtConfig(**settings_dict['jwt'])
 
 class DynamodbConfig(BaseModel):
     table: str
-dynamodb_config = DynamodbConfig(**yaml_settings['aws']['dynamodb'])
+dynamodb_config = DynamodbConfig(**settings_dict['aws']['dynamodb'])
 
 class InfraConfig(BaseModel):
+    """
+    Represents the infrastructure configuration.
+
+    Attributes:
+        wifi (str): The WiFi password used in welcome messages.
+        horizon (str): The url of the openstack horizon interface (also used to derive the keystone endpoint).
+        application_credential_id (str): The application credential ID used to provision users and projects.
+        application_credential_secret (SecretStr): The application credential secret used to provision users and projects.
+        tf_directory (str): The Terraform directory.
+    """
     wifi: str
     horizon: str
     application_credential_id: str
     application_credential_secret: SecretStr
     tf_directory: str
-infra_config = InfraConfig(**yaml_settings['infra'])
+infra_config = InfraConfig(**settings_dict['infra'])
 
 class RedisConfig(BaseModel):
     host: str
     port: int
     db: int
-redis_config = RedisConfig(**yaml_settings['redis'])
+redis_config = RedisConfig(**settings_dict['redis'])
 
 class HttpConfig(BaseModel):
     domain: str 
-http_config = HttpConfig(**yaml_settings['http'])
+http_config = HttpConfig(**settings_dict['http'])
 
 class SingletonBaseSettingsMeta(type(BaseSettings), type):
     _instances = {}

From 1023c853548a4c2205225516a7e049eb8bf7515d Mon Sep 17 00:00:00 2001
From: Jonathan Styles <github@jontyms.com>
Date: Mon, 18 Mar 2024 13:40:14 -0400
Subject: [PATCH 3/9] Fixed exception handling in authentication modules Fixed
 Forms to return proper execptions

---
 routes/api.py          | 20 +++++++++++++++-----
 util/authentication.py | 41 +++++++++++++++++++++++------------------
 util/forms.py          |  2 +-
 3 files changed, 39 insertions(+), 24 deletions(-)

diff --git a/routes/api.py b/routes/api.py
index 450042b..91262ed 100644
--- a/routes/api.py
+++ b/routes/api.py
@@ -3,7 +3,7 @@
 
 import boto3
 from botocore.exceptions import ClientError
-from fastapi import APIRouter, Cookie, Request
+from fastapi import APIRouter, Cookie, Request, HTTPException
 from fastapi.responses import HTMLResponse
 from pydantic import error_wrappers
 
@@ -13,6 +13,7 @@
 from util.errors import Errors
 from util.kennelish import Kennelish, Transformer
 from util.options import Settings
+from util.forms import Forms
 
 
 
@@ -47,7 +48,10 @@ async def get_root():
 
 @router.get("/form/{num}")
 async def get_form(num: str):
-    return Forms.get_form_body(num)(num)
+    try:
+        return Forms.get_form_body(num)
+    except FileNotFoundError:
+        return HTTPException(status_code=404, detail="Form not found")
 
 
 """
@@ -68,8 +72,10 @@ async def get_form_html(
     table = dynamodb.Table(Settings().aws.table)
 
     # Get form object
-    data = Forms.get_form_body(num)(num)
-
+    try:
+        data = Forms.get_form_body(num)
+    except FileNotFoundError:
+        return HTTPException(status_code=404, detail="Form not found")
     # Get data from DynamoDB
     user_data = table.get_item(Key={"id": user_jwt.get("id")}).get("Item", None)
 
@@ -93,7 +99,11 @@ async def post_form(
     num: str = 1,
 ):
     # Get Kennelish data
-    kennelish_data = Forms.get_form_body(num)(num)
+    try:
+        kennelish_data = Forms.get_form_body(num)
+    except FileNotFoundError:
+        return HTTPException(status_code=404, detail="Form not found")
+    
     model = Transformer.kennelish_to_pydantic(kennelish_data)
 
     # Parse and Validate inputs
diff --git a/util/authentication.py b/util/authentication.py
index 090b1ba..f59ea06 100644
--- a/util/authentication.py
+++ b/util/authentication.py
@@ -35,14 +35,17 @@ async def wrapper(request: Request, token: Optional[str], *args, **kwargs):
                 )
                 is_admin: bool = user_jwt.get("sudo", False)
                 creation_date: float = user_jwt.get("issued", -1)
-            except Exception:
-                tr = Errors.generate(
-                    request,
-                    403,
-                    "Invalid token provided. Please log in again (refresh the page) and try again.",
-                )
-                tr.delete_cookie(key="token")
-                return tr
+            except Exception as e:
+                if isinstance(e, jwt.JWTError) or isinstance(e, jwt.JWTClaimsError):
+                    tr = Errors.generate(
+                        request,
+                        403,
+                        "Invalid token provided. Please log in again (refresh the page) and try again.",
+                    )
+                    tr.delete_cookie(key="token")
+                    return tr
+                else:
+                    raise  # Re-raise exceptions that are not related to token validation
 
             if not is_admin:
                 return Errors.generate(
@@ -88,15 +91,18 @@ async def wrapper_member(
                     algorithms=Settings().jwt.algorithm,
                 )
                 creation_date: float = user_jwt.get("issued", -1)
-            except Exception:
-                tr = Errors.generate(
-                    request,
-                    403,
-                    "Invalid token provided. Please log in again (refresh the page) and try again.",
-                )
-                tr.delete_cookie(key="token")
-                return tr
-
+            except Exception as e:
+                if isinstance(e, jwt.JWTError) or isinstance(e, jwt.JWTClaimsError):
+                    tr = Errors.generate(
+                        request,
+                        403,
+                        "Invalid token provided. Please log in again (refresh the page) and try again.",
+                    )
+                    tr.delete_cookie(key="token")
+                    return tr
+                else:
+                    raise  # Re-raise exceptions that are not related to token validation
+    
             if time.time() > creation_date + Settings().jwt.lifetime_user:
                 return Errors.generate(
                     request,
@@ -104,7 +110,6 @@ async def wrapper_member(
                     "Session expired.",
                     essay="Sessions last for about fifteen weeks. You need to re-log-in between semesters.",
                 )
-
             return await func(request, token, user_jwt, *args, **kwargs)
 
         return wrapper_member
diff --git a/util/forms.py b/util/forms.py
index 54db84a..f1f0296 100644
--- a/util/forms.py
+++ b/util/forms.py
@@ -7,4 +7,4 @@ def get_form_body(file="1"):
             form_file = os.path.join(os.getcwd(), "forms", f"{file}.json")
             return json.load(open(form_file, "r"))
         except FileNotFoundError:
-            return {}
\ No newline at end of file
+            raise
\ No newline at end of file

From 1c578e6ae15894e985ac85ef94271cc4ec4fa231 Mon Sep 17 00:00:00 2001
From: Jonathan Styles <github@jontyms.com>
Date: Mon, 18 Mar 2024 13:43:04 -0400
Subject: [PATCH 4/9] removed unnecessary dependencies

---
 routes/admin.py | 2 +-
 util/options.py | 5 ++---
 2 files changed, 3 insertions(+), 4 deletions(-)

diff --git a/routes/admin.py b/routes/admin.py
index 6780536..7996a77 100644
--- a/routes/admin.py
+++ b/routes/admin.py
@@ -2,7 +2,7 @@
 
 import boto3
 from boto3.dynamodb.conditions import Attr
-from fastapi import APIRouter, Body, Cookie, Request, Response, Depends
+from fastapi import APIRouter, Body, Cookie, Request, Response
 from fastapi.encoders import jsonable_encoder
 from fastapi.templating import Jinja2Templates
 from jose import jwt
diff --git a/util/options.py b/util/options.py
index 2217064..fe8d6c3 100644
--- a/util/options.py
+++ b/util/options.py
@@ -4,7 +4,6 @@
 import yaml
 import json
 import subprocess
-from functools import lru_cache
 
 
 def BitwardenConfig(settings_dict: dict):
@@ -122,12 +121,12 @@ class JwtConfig(BaseModel):
     Configuration class for JWT (JSON Web Token) settings.
 
     Attributes:
-        secret (SecretStr): The secret key used for signing and verifying JWTs.
+        secret (SecretStr): The secret key used for signing and verifying JWTs.(min_length=32)
         algorithm (str): The algorithm used for JWT encryption.
         lifetime_user (int): The lifetime (in seconds) of a user JWT.
         lifetime_sudo (int): The lifetime (in seconds) of a sudo JWT.
     """
-    secret: SecretStr
+    secret: SecretStr = constr(min_length=32)
     algorithm: str
     lifetime_user: int
     lifetime_sudo: int

From 8da792bf66483fcecc55332be376e5bf7658911c Mon Sep 17 00:00:00 2001
From: Jonathan Styles <github@jontyms.com>
Date: Mon, 18 Mar 2024 14:33:56 -0400
Subject: [PATCH 5/9] Updated requirements

---
 requirements.txt | 7 ++-----
 1 file changed, 2 insertions(+), 5 deletions(-)

diff --git a/requirements.txt b/requirements.txt
index 1957628..806a7d9 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -48,7 +48,8 @@ pre-commit==3.6.2
 pyasn1==0.5.1
 pyasn1-modules==0.3.0
 pycparser==2.21
-pydantic==1.10.10
+pydantic==2.6.4
+pydantic_settings==2.2.1
 pyOpenSSL==22.1.0
 python-dateutil==2.9.0.post0
 python-dotenv==1.0.1
@@ -74,7 +75,3 @@ uvloop==0.19.0
 virtualenv==20.25.1
 watchfiles==0.21.0
 websockets==12.0
-jinja2
-commonmark
-redis
-pydantic_settings

From a5e9bc33ab62ad941d1bb7df252d5afca13d5aa0 Mon Sep 17 00:00:00 2001
From: Jonathan Styles <github@jontyms.com>
Date: Mon, 18 Mar 2024 14:35:44 -0400
Subject: [PATCH 6/9] Added logging statements

---
 index.py        | 1 -
 util/options.py | 5 ++++-
 2 files changed, 4 insertions(+), 2 deletions(-)

diff --git a/index.py b/index.py
index 1df18d4..4eabe1e 100644
--- a/index.py
+++ b/index.py
@@ -120,7 +120,6 @@ async def index(request: Request, token: Optional[str] = Cookie(None)):
 async def oauth_transformer(redir: str = "/join/2"):
     # Open redirect check
     hostname = urlparse(redir).netloc
-    print(hostname)
     if hostname != "" and hostname != Settings().http.domain:
         redir = "/join/2"
 
diff --git a/util/options.py b/util/options.py
index fe8d6c3..872de38 100644
--- a/util/options.py
+++ b/util/options.py
@@ -4,7 +4,9 @@
 import yaml
 import json
 import subprocess
+import logging
 
+logger = logging.getLogger(__name__)
 
 def BitwardenConfig(settings_dict: dict):
     '''
@@ -12,11 +14,12 @@ def BitwardenConfig(settings_dict: dict):
     The bitwarden secrets are mapped to the settings dict using the bitwarden_mapping dict.
     The secrets are sourced based on a project id in the settings dict.
     '''
+    logger.debug("Loading secrets from Bitwarden")
     try:
         command = f"bws secret list {settings_dict['bws']['project_id']}"
         bitwarden_raw = subprocess.check_output(command, shell=True, text=True)
     except Exception as e:
-        print(e)
+        logger.error(e)
     bitwarden_settings = parse_json_to_dict(bitwarden_raw)
     
     bitwarden_mapping = {

From 19a458661e61978900b999f4822866a6ff4fceb7 Mon Sep 17 00:00:00 2001
From: Jonathan Styles <github@jontyms.com>
Date: Mon, 18 Mar 2024 14:40:55 -0400
Subject: [PATCH 7/9] appease the pre-commit gods

---
 config.yml             | 10 +++++-----
 index.py               |  4 ++--
 routes/admin.py        |  1 -
 routes/api.py          |  8 +++-----
 routes/infra.py        |  1 -
 routes/stripe.py       |  1 -
 routes/wallet.py       |  2 --
 util/authentication.py |  4 +---
 util/discord.py        |  2 --
 util/email.py          |  1 -
 util/forms.py          |  3 ++-
 util/options.py        | 25 +++++++++++++------------
 12 files changed, 26 insertions(+), 36 deletions(-)

diff --git a/config.yml b/config.yml
index 055df64..055bc35 100644
--- a/config.yml
+++ b/config.yml
@@ -1,7 +1,7 @@
 # Options for Onboard.
 bws:
-    project_id: 
-    enable: 
+    project_id: ""
+    enable: ""
 jwt:
     secret:
     algorithm: "HS256"
@@ -29,9 +29,9 @@ discord:
 
 
 stripe:
-    api_key:
-    webhook_secret:
-    price_id:
+    api_key: ""
+    webhook_secret: ""
+    price_id: ""
     url_success: "https://join.hackucf.org/final/"
     url_failure: "https://join.hackucf.org/pay/"
 
diff --git a/index.py b/index.py
index 4eabe1e..f061429 100644
--- a/index.py
+++ b/index.py
@@ -26,18 +26,18 @@
 from util.authentication import Authentication
 # Import error handling
 from util.errors import Errors
+from util.forms import Forms
 # Import the page rendering library
 from util.kennelish import Kennelish
 # Import options
 from util.options import Settings
-from util.forms import Forms
 
 ### TODO: TEMP
 os.environ["OAUTHLIB_INSECURE_TRANSPORT"] = "0"
 ###
 
 
-logging.basicConfig(level=logging.DEBUG,
+logging.basicConfig(level=logging.INFO,
                     format='%(asctime)s - %(levelname)s - %(name)s - %(message)s',
                     datefmt='%Y-%m-%d %H:%M:%S')
 logger = logging.getLogger(__name__)
diff --git a/routes/admin.py b/routes/admin.py
index 7996a77..792d03d 100644
--- a/routes/admin.py
+++ b/routes/admin.py
@@ -15,7 +15,6 @@
 from util.errors import Errors
 from util.options import Settings
 
-
 templates = Jinja2Templates(directory="templates")
 
 router = APIRouter(prefix="/admin", tags=["Admin"], responses=Errors.basic_http())
diff --git a/routes/api.py b/routes/api.py
index 91262ed..91f21f8 100644
--- a/routes/api.py
+++ b/routes/api.py
@@ -3,7 +3,7 @@
 
 import boto3
 from botocore.exceptions import ClientError
-from fastapi import APIRouter, Cookie, Request, HTTPException
+from fastapi import APIRouter, Cookie, HTTPException, Request
 from fastapi.responses import HTMLResponse
 from pydantic import error_wrappers
 
@@ -11,11 +11,9 @@
 from models.user import PublicContact
 from util.authentication import Authentication
 from util.errors import Errors
+from util.forms import Forms
 from util.kennelish import Kennelish, Transformer
 from util.options import Settings
-from util.forms import Forms
-
-
 
 router = APIRouter(prefix="/api", tags=["API"], responses=Errors.basic_http())
 
@@ -103,7 +101,7 @@ async def post_form(
         kennelish_data = Forms.get_form_body(num)
     except FileNotFoundError:
         return HTTPException(status_code=404, detail="Form not found")
-    
+
     model = Transformer.kennelish_to_pydantic(kennelish_data)
 
     # Parse and Validate inputs
diff --git a/routes/infra.py b/routes/infra.py
index efe7635..cf10688 100644
--- a/routes/infra.py
+++ b/routes/infra.py
@@ -21,7 +21,6 @@
 from util.limiter import RateLimiter
 from util.options import Settings
 
-
 logger = logging.getLogger(__name__)
 
 
diff --git a/routes/stripe.py b/routes/stripe.py
index 10551c7..a6eb4c3 100644
--- a/routes/stripe.py
+++ b/routes/stripe.py
@@ -13,7 +13,6 @@
 from util.errors import Errors
 from util.options import Settings
 
-
 templates = Jinja2Templates(directory="templates")
 
 
diff --git a/routes/wallet.py b/routes/wallet.py
index f3649fa..0887a1c 100644
--- a/routes/wallet.py
+++ b/routes/wallet.py
@@ -14,8 +14,6 @@
 from util.errors import Errors
 from util.options import Settings
 
-
-
 router = APIRouter(
     prefix="/wallet", tags=["API", "MobileWallet"], responses=Errors.basic_http()
 )
diff --git a/util/authentication.py b/util/authentication.py
index f59ea06..790bb60 100644
--- a/util/authentication.py
+++ b/util/authentication.py
@@ -11,8 +11,6 @@
 from util.options import Settings
 
 
-
-
 class Authentication:
     def __init__(self):
         super(Authentication, self).__init__
@@ -102,7 +100,7 @@ async def wrapper_member(
                     return tr
                 else:
                     raise  # Re-raise exceptions that are not related to token validation
-    
+
             if time.time() > creation_date + Settings().jwt.lifetime_user:
                 return Errors.generate(
                     request,
diff --git a/util/discord.py b/util/discord.py
index bec58fb..e0bf36f 100644
--- a/util/discord.py
+++ b/util/discord.py
@@ -4,8 +4,6 @@
 
 from util.options import Settings
 
-
-
 headers = {
     "Authorization": f"Bot {Settings().discord.bot_token.get_secret_value()}",
     "Content-Type": "application/json",
diff --git a/util/email.py b/util/email.py
index c7e7ee4..843638f 100644
--- a/util/email.py
+++ b/util/email.py
@@ -6,7 +6,6 @@
 
 from util.options import Settings
 
-
 email = Settings().email.email
 password = Settings().email.password.get_secret_value()
 smtp_host = Settings().email.smtp_server
diff --git a/util/forms.py b/util/forms.py
index f1f0296..28f25c3 100644
--- a/util/forms.py
+++ b/util/forms.py
@@ -1,10 +1,11 @@
 import json
 import os
 
+
 class Forms:
     def get_form_body(file="1"):
         try:
             form_file = os.path.join(os.getcwd(), "forms", f"{file}.json")
             return json.load(open(form_file, "r"))
         except FileNotFoundError:
-            raise
\ No newline at end of file
+            raise
diff --git a/util/options.py b/util/options.py
index 872de38..b381dbd 100644
--- a/util/options.py
+++ b/util/options.py
@@ -1,10 +1,11 @@
+import json
+import logging
 import os
+import subprocess
+
+import yaml
 from pydantic import BaseModel, SecretStr, constr
 from pydantic_settings import BaseSettings
-import yaml
-import json
-import subprocess
-import logging
 
 logger = logging.getLogger(__name__)
 
@@ -21,7 +22,7 @@ def BitwardenConfig(settings_dict: dict):
     except Exception as e:
         logger.error(e)
     bitwarden_settings = parse_json_to_dict(bitwarden_raw)
-    
+
     bitwarden_mapping = {
         'discord_bot_token': ('discord', 'bot_token'),
         'discord_client_id': ('discord', 'client_id'),
@@ -35,7 +36,7 @@ def BitwardenConfig(settings_dict: dict):
         'infra_application_credential_id': ('infra', 'application_credential_id'),
         'infra_configuration_credential_secret': ('infra', 'application_credential_secret')
     }
-    
+
     bitwarden_mapped = {}
     for bw_key, nested_keys in bitwarden_mapping.items():
         if bw_key in bitwarden_settings:
@@ -43,13 +44,13 @@ def BitwardenConfig(settings_dict: dict):
             if top_key not in bitwarden_mapped:
                 bitwarden_mapped[top_key] = {}
             bitwarden_mapped[top_key][nested_key] = bitwarden_settings[bw_key]
-    
+
     for top_key, nested_dict in bitwarden_mapped.items():
         if top_key in settings_dict:
             for nested_key, value in nested_dict.items():
                 settings_dict[top_key][nested_key] = value
     return settings_dict
-    
+
 settings_dict = dict()
 
 # Reads config from ../config/options.yml
@@ -103,7 +104,7 @@ class StripeConfig(BaseModel):
     price_id: str
     url_success: str
     url_failure: str
-stripe_config = StripeConfig(**settings_dict['stripe']) 
+stripe_config = StripeConfig(**settings_dict['stripe'])
 
 class EmailConfig(BaseModel):
     """
@@ -114,10 +115,10 @@ class EmailConfig(BaseModel):
         email (str): The email address to send from also used as the login username.
         password (SecretStr): The password for the email account.
     """
-    smtp_server: str 
+    smtp_server: str
     email: str
     password: SecretStr
-email_config = EmailConfig(**settings_dict['email']) 
+email_config = EmailConfig(**settings_dict['email'])
 
 class JwtConfig(BaseModel):
     """
@@ -164,7 +165,7 @@ class RedisConfig(BaseModel):
 redis_config = RedisConfig(**settings_dict['redis'])
 
 class HttpConfig(BaseModel):
-    domain: str 
+    domain: str
 http_config = HttpConfig(**settings_dict['http'])
 
 class SingletonBaseSettingsMeta(type(BaseSettings), type):

From 761ba0dfa09d858e88fbce32ae2ec6dcb00da73f Mon Sep 17 00:00:00 2001
From: Jonathan Styles <github@jontyms.com>
Date: Mon, 18 Mar 2024 18:07:21 -0400
Subject: [PATCH 8/9] Renamed options.py to settings.py fixed command injection
 in settings module

---
 util/{options.py => settings.py} | 42 +++++++++++++++++---------------
 1 file changed, 23 insertions(+), 19 deletions(-)
 rename util/{options.py => settings.py} (86%)

diff --git a/util/options.py b/util/settings.py
similarity index 86%
rename from util/options.py
rename to util/settings.py
index b381dbd..12e4c25 100644
--- a/util/options.py
+++ b/util/settings.py
@@ -1,6 +1,7 @@
 import json
 import logging
 import os
+import re
 import subprocess
 
 import yaml
@@ -9,7 +10,7 @@
 
 logger = logging.getLogger(__name__)
 
-def BitwardenConfig(settings_dict: dict):
+def BitwardenConfig(settings: dict):
     '''
     Takes a dict of settings loaded from yaml and adds the secrets from bitwarden to the settings dict.
     The bitwarden secrets are mapped to the settings dict using the bitwarden_mapping dict.
@@ -17,10 +18,13 @@ def BitwardenConfig(settings_dict: dict):
     '''
     logger.debug("Loading secrets from Bitwarden")
     try:
-        command = f"bws secret list {settings_dict['bws']['project_id']}"
+        project_id = settings['bws']['project_id']
+        if bool(re.search('[^a-z0-9-]', project_id)):
+            raise ValueError("Invalid project id")
+        command = f"bws secret list {project_id}"
         bitwarden_raw = subprocess.check_output(command, shell=True, text=True)
     except Exception as e:
-        logger.error(e)
+        logger.exception(e)
     bitwarden_settings = parse_json_to_dict(bitwarden_raw)
 
     bitwarden_mapping = {
@@ -46,24 +50,24 @@ def BitwardenConfig(settings_dict: dict):
             bitwarden_mapped[top_key][nested_key] = bitwarden_settings[bw_key]
 
     for top_key, nested_dict in bitwarden_mapped.items():
-        if top_key in settings_dict:
+        if top_key in settings:
             for nested_key, value in nested_dict.items():
-                settings_dict[top_key][nested_key] = value
-    return settings_dict
+                settings[top_key][nested_key] = value
+    return settings
 
-settings_dict = dict()
+settings = dict()
 
 # Reads config from ../config/options.yml
 here = os.path.abspath(os.path.dirname(__file__))
 with open(os.path.join(here, "../config/options.yml")) as f:
-    settings_dict.update(yaml.load(f, Loader=yaml.FullLoader))
+    settings.update(yaml.load(f, Loader=yaml.FullLoader))
 def parse_json_to_dict(json_string):
     data = json.loads(json_string)
     return {item['key']: item['value'] for item in data}
 
-# If bitwarden is enabled, add secrets to settings_dict
-if settings_dict.get('bws').get('enable'):
-    settings_dict = BitwardenConfig(settings_dict)
+# If bitwarden is enabled, add secrets to settings
+if settings.get('bws').get('enable'):
+    settings = BitwardenConfig(settings)
 
 
 class DiscordConfig(BaseModel):
@@ -87,7 +91,7 @@ class DiscordConfig(BaseModel):
     scope: str
     secret: SecretStr
 
-discord_config = DiscordConfig(**settings_dict['discord'])
+discord_config = DiscordConfig(**settings['discord'])
 class StripeConfig(BaseModel):
     """
     Configuration class for Stripe integration.
@@ -104,7 +108,7 @@ class StripeConfig(BaseModel):
     price_id: str
     url_success: str
     url_failure: str
-stripe_config = StripeConfig(**settings_dict['stripe'])
+stripe_config = StripeConfig(**settings['stripe'])
 
 class EmailConfig(BaseModel):
     """
@@ -118,7 +122,7 @@ class EmailConfig(BaseModel):
     smtp_server: str
     email: str
     password: SecretStr
-email_config = EmailConfig(**settings_dict['email'])
+email_config = EmailConfig(**settings['email'])
 
 class JwtConfig(BaseModel):
     """
@@ -134,11 +138,11 @@ class JwtConfig(BaseModel):
     algorithm: str
     lifetime_user: int
     lifetime_sudo: int
-jwt_config = JwtConfig(**settings_dict['jwt'])
+jwt_config = JwtConfig(**settings['jwt'])
 
 class DynamodbConfig(BaseModel):
     table: str
-dynamodb_config = DynamodbConfig(**settings_dict['aws']['dynamodb'])
+dynamodb_config = DynamodbConfig(**settings['aws']['dynamodb'])
 
 class InfraConfig(BaseModel):
     """
@@ -156,17 +160,17 @@ class InfraConfig(BaseModel):
     application_credential_id: str
     application_credential_secret: SecretStr
     tf_directory: str
-infra_config = InfraConfig(**settings_dict['infra'])
+infra_config = InfraConfig(**settings['infra'])
 
 class RedisConfig(BaseModel):
     host: str
     port: int
     db: int
-redis_config = RedisConfig(**settings_dict['redis'])
+redis_config = RedisConfig(**settings['redis'])
 
 class HttpConfig(BaseModel):
     domain: str
-http_config = HttpConfig(**settings_dict['http'])
+http_config = HttpConfig(**settings['http'])
 
 class SingletonBaseSettingsMeta(type(BaseSettings), type):
     _instances = {}

From 32af7887a2ff35c179571cd68971c3cf31d31702 Mon Sep 17 00:00:00 2001
From: Jonathan Styles <github@jontyms.com>
Date: Mon, 18 Mar 2024 18:09:53 -0400
Subject: [PATCH 9/9] fixup! Renamed options.py to settings.py fixed command
 injection in settings module

---
 index.py               | 2 +-
 routes/admin.py        | 2 +-
 routes/api.py          | 2 +-
 routes/infra.py        | 2 +-
 routes/stripe.py       | 2 +-
 routes/wallet.py       | 2 +-
 util/approve.py        | 2 +-
 util/authentication.py | 2 +-
 util/discord.py        | 2 +-
 util/email.py          | 2 +-
 10 files changed, 10 insertions(+), 10 deletions(-)

diff --git a/index.py b/index.py
index f061429..315abcc 100644
--- a/index.py
+++ b/index.py
@@ -30,7 +30,7 @@
 # Import the page rendering library
 from util.kennelish import Kennelish
 # Import options
-from util.options import Settings
+from util.settings import Settings
 
 ### TODO: TEMP
 os.environ["OAUTHLIB_INSECURE_TRANSPORT"] = "0"
diff --git a/routes/admin.py b/routes/admin.py
index 792d03d..0522300 100644
--- a/routes/admin.py
+++ b/routes/admin.py
@@ -13,7 +13,7 @@
 from util.discord import Discord
 from util.email import Email
 from util.errors import Errors
-from util.options import Settings
+from util.settings import Settings
 
 templates = Jinja2Templates(directory="templates")
 
diff --git a/routes/api.py b/routes/api.py
index 91f21f8..dea1305 100644
--- a/routes/api.py
+++ b/routes/api.py
@@ -13,7 +13,7 @@
 from util.errors import Errors
 from util.forms import Forms
 from util.kennelish import Kennelish, Transformer
-from util.options import Settings
+from util.settings import Settings
 
 router = APIRouter(prefix="/api", tags=["API"], responses=Errors.basic_http())
 
diff --git a/routes/infra.py b/routes/infra.py
index cf10688..bed924e 100644
--- a/routes/infra.py
+++ b/routes/infra.py
@@ -19,7 +19,7 @@
 from util.email import Email
 from util.errors import Errors
 from util.limiter import RateLimiter
-from util.options import Settings
+from util.settings import Settings
 
 logger = logging.getLogger(__name__)
 
diff --git a/routes/stripe.py b/routes/stripe.py
index a6eb4c3..8dcf859 100644
--- a/routes/stripe.py
+++ b/routes/stripe.py
@@ -11,7 +11,7 @@
 from util.approve import Approve
 from util.authentication import Authentication
 from util.errors import Errors
-from util.options import Settings
+from util.settings import Settings
 
 templates = Jinja2Templates(directory="templates")
 
diff --git a/routes/wallet.py b/routes/wallet.py
index 0887a1c..4ebb59c 100644
--- a/routes/wallet.py
+++ b/routes/wallet.py
@@ -12,7 +12,7 @@
 from models.user import PublicContact
 from util.authentication import Authentication
 from util.errors import Errors
-from util.options import Settings
+from util.settings import Settings
 
 router = APIRouter(
     prefix="/wallet", tags=["API", "MobileWallet"], responses=Errors.basic_http()
diff --git a/util/approve.py b/util/approve.py
index 2636944..aeb362b 100644
--- a/util/approve.py
+++ b/util/approve.py
@@ -8,7 +8,7 @@
 from util.discord import Discord
 from util.email import Email
 from util.horsepass import HorsePass
-from util.options import Settings
+from util.settings import Settings
 
 logger = logging.getLogger()
 
diff --git a/util/authentication.py b/util/authentication.py
index 790bb60..60f4dc9 100644
--- a/util/authentication.py
+++ b/util/authentication.py
@@ -8,7 +8,7 @@
 
 # Import options and errors
 from util.errors import Errors
-from util.options import Settings
+from util.settings import Settings
 
 
 class Authentication:
diff --git a/util/discord.py b/util/discord.py
index e0bf36f..3f1e71e 100644
--- a/util/discord.py
+++ b/util/discord.py
@@ -2,7 +2,7 @@
 
 import requests
 
-from util.options import Settings
+from util.settings import Settings
 
 headers = {
     "Authorization": f"Bot {Settings().discord.bot_token.get_secret_value()}",
diff --git a/util/email.py b/util/email.py
index 843638f..8f36c53 100644
--- a/util/email.py
+++ b/util/email.py
@@ -4,7 +4,7 @@
 
 import commonmark
 
-from util.options import Settings
+from util.settings import Settings
 
 email = Settings().email.email
 password = Settings().email.password.get_secret_value()