diff --git a/api/.gitignore b/api/.gitignore
new file mode 100644
index 0000000..4d58c66
--- /dev/null
+++ b/api/.gitignore
@@ -0,0 +1,2 @@
+__pycache__
+*/__pycache__
diff --git a/api/data/config.ini b/api/data/config.ini
new file mode 100644
index 0000000..f726a5c
--- /dev/null
+++ b/api/data/config.ini
@@ -0,0 +1,7 @@
+[db]
+host = localhost
+port = 3306
+user = tam3
+password = tam3
+database = tam3
+
diff --git a/api/db.py b/api/db.py
new file mode 100644
index 0000000..f0398d7
--- /dev/null
+++ b/api/db.py
@@ -0,0 +1,10 @@
+from settings import read_config
+from mysql.connector import connect
+
+
+def session():
+ config = read_config()
+ conn = connect(**config["db"])
+ cur = conn.cursor()
+ return conn, cur
+
diff --git a/api/exceptions.py b/api/exceptions.py
new file mode 100644
index 0000000..666d02d
--- /dev/null
+++ b/api/exceptions.py
@@ -0,0 +1,9 @@
+from fastapi import HTTPException, status
+
+bad_key = HTTPException(
+ status_code=status.HTTP_401_UNAUTHORIZED, detail="API Key is bad, very bad."
+)
+
+not_found = HTTPException(
+ status_code=status.HTTP_404_NOT_FOUND, detail="Resource not found."
+)
diff --git a/api/key.py b/api/key.py
new file mode 100755
index 0000000..025e4bf
--- /dev/null
+++ b/api/key.py
@@ -0,0 +1,48 @@
+#!/bin/env python3
+
+import string
+from sys import argv
+
+from repos import ApiKeyRepo
+
+rdm_str = string.ascii_lowercase + string.digits
+
+
+def generate():
+ if len(argv) < 3:
+ print("Please put name after the generate verb.")
+ quit()
+ new_key = ApiKeyRepo().create_api(argv[2])
+ print(new_key)
+
+
+def list_keys():
+ result_keys = ApiKeyRepo().get_all()
+ for key in result_keys:
+ print(f"pc_name: {key.pc_name}")
+ print(key.api_key)
+ print("\n")
+
+
+def delete_key():
+ if len(argv) < 3:
+ print("Please put api key to delete after the delete verb.")
+ quit()
+ del_status = ApiKeyRepo().delete(argv[2])
+ print(del_status)
+
+
+if len(argv) < 2:
+ print("Please put action after api.py such as generate, list, or remove.")
+ quit()
+else:
+ action = argv[1]
+
+match action:
+ case "generate":
+ generate()
+ case "list":
+ list_keys()
+ case "delete":
+ delete_key()
+
diff --git a/api/main.py b/api/main.py
index e69de29..cad5c63 100644
--- a/api/main.py
+++ b/api/main.py
@@ -0,0 +1,13 @@
+#!/bin/env python3
+
+from fastapi import FastAPI
+from sys import argv
+
+from routers.prefixes import prefix_router
+
+if argv[1] == "run":
+ app = FastAPI(title="TAM3 API Server", docs_url=None, redoc_url=None)
+else:
+ app = FastAPI(title="TAM3 API Server")
+
+app.include_router(prefix_router)
diff --git a/api/repos/__init__.py b/api/repos/__init__.py
new file mode 100644
index 0000000..6c7cd2b
--- /dev/null
+++ b/api/repos/__init__.py
@@ -0,0 +1 @@
+from .api_keys import ApiKey, ApiKeyRepo
diff --git a/api/repos/api_keys.py b/api/repos/api_keys.py
new file mode 100644
index 0000000..e7cd116
--- /dev/null
+++ b/api/repos/api_keys.py
@@ -0,0 +1,43 @@
+import random as r
+import string
+from dataclasses import dataclass
+from .template import Repo
+
+rdm_set = string.ascii_lowercase + string.digits
+
+
+@dataclass
+class ApiKey:
+ api_key: str
+ pc_name: str = ""
+
+
+class ApiKeyRepo(Repo):
+ def check_api(self, api_key: str) -> bool:
+ self.cur.execute("SELECT * FROM api_keys WHERE api_key = %s", (api_key,))
+ result = self.cur.fetchone()
+ if result:
+ return True
+ else:
+ return False
+
+ def create_api(self, name: str) -> str:
+ while True:
+ new_key = "".join(r.choice(rdm_set) for i in range(16))
+ if not self.check_api(new_key):
+ break
+ self.cur.execute("INSERT INTO api_keys VALUES (%s, %s)", (new_key, name))
+ self.conn.commit()
+ return new_key
+
+ def get_all(self) -> list[ApiKey]:
+ self.cur.execute("SELECT * FROM api_keys")
+ results = self.cur.fetchall()
+ if not results:
+ return []
+ return [ApiKey(*r) for r in results]
+
+ def delete(self, api_key: str) -> str:
+ self.cur.execute("DELETE FROM api_keys WHERE api_key = %s", (api_key,))
+ self.conn.commit()
+ return "Key deleted successfully."
diff --git a/api/repos/prefixes.py b/api/repos/prefixes.py
new file mode 100644
index 0000000..b1efed2
--- /dev/null
+++ b/api/repos/prefixes.py
@@ -0,0 +1,39 @@
+from .template import Repo
+from dataclasses import dataclass
+from exceptions import not_found
+
+
+@dataclass
+class Prefix:
+ name: str
+ color: str = ""
+ weight: int = 0
+
+
+class PrefixRepo(Repo):
+ def get_all(self) -> list[Prefix]:
+ self.cur.execute("SELECT * FROM prefixes ORDER BY weight, name")
+ results = self.cur.fetchall()
+ if not results:
+ return []
+ return [Prefix(*r) for r in results]
+
+ def get_one(self, prefix_name: str):
+ self.cur.execute("SELECT * FROM prefixes WHERE name = %s", (prefix_name,))
+ result = self.cur.fetchone()
+ if not result:
+ raise not_found
+ return Prefix(*result)
+
+ def add_one(self, prefix: Prefix) -> str:
+ self.cur.execute(
+ "REPLACE INTO prefixes VALUES (%s, %s, %s)",
+ (prefix.name, prefix.color, prefix.weight),
+ )
+ self.conn.commit()
+ return "Prefix inserted successfully."
+
+ def del_one(self, prefix_name: str) -> str:
+ self.cur.execute("DELETE FROM prefixes WHERE name = %s", (prefix_name,))
+ self.conn.commit()
+ return "Prefix deleted successfully."
diff --git a/api/repos/template.py b/api/repos/template.py
new file mode 100644
index 0000000..481b342
--- /dev/null
+++ b/api/repos/template.py
@@ -0,0 +1,6 @@
+from db import session
+
+
+class Repo:
+ def __init__(self):
+ self.conn, self.cur = session()
diff --git a/api/routers/__init__.py b/api/routers/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/api/routers/prefixes.py b/api/routers/prefixes.py
new file mode 100644
index 0000000..0d8367c
--- /dev/null
+++ b/api/routers/prefixes.py
@@ -0,0 +1,26 @@
+from fastapi import APIRouter
+from repos.prefixes import Prefix, PrefixRepo
+
+prefix_router = APIRouter(prefix="/api/prefixes")
+
+
+@prefix_router.get("/")
+def get_all_prefixes():
+ return PrefixRepo().get_all()
+
+
+@prefix_router.get("/{prefix_name}/")
+def get_one_prefix(prefix_name: str):
+ return PrefixRepo().get_one(prefix_name)
+
+
+@prefix_router.post("/")
+def post_one_prefix(p: Prefix):
+ rep_detail = PrefixRepo().add_one(p)
+ return {"detail": rep_detail}
+
+
+@prefix_router.delete("/")
+def del_one_prefix(prefix_name: str):
+ rep_detail = PrefixRepo().del_one(prefix_name)
+ return {"detail": rep_detail}
diff --git a/api/settings.py b/api/settings.py
new file mode 100644
index 0000000..a8d7055
--- /dev/null
+++ b/api/settings.py
@@ -0,0 +1,25 @@
+import os
+from pathlib import Path
+from configparser import ConfigParser
+
+data_path = Path(os.getenv("TAM3_DATA_PATH", "data"))
+data_path.mkdir(exist_ok=True)
+
+
+def read_config():
+ config = ConfigParser()
+ config_path = data_path / "config.ini"
+ if config_path.is_file():
+ config.read(config_path)
+ return config
+ else:
+ config["db"] = {
+ "host": os.getenv("TAM3_DB_HOST", "localhost"),
+ "port": os.getenv("TAM3_DB_PORT", "3306"),
+ "user": os.getenv("TAM3_DB_USER", "tam3"),
+ "password": os.getenv("TAM3_DB_PASSWD", "tam3"),
+ "database": os.getenv("TAM3_DB_DATABASE", "tam3"),
+ }
+ with open(config_path, "w") as f:
+ config.write(f)
+ return config
diff --git a/db/compose.yml b/db/compose.yml
new file mode 100644
index 0000000..8dc8695
--- /dev/null
+++ b/db/compose.yml
@@ -0,0 +1,26 @@
+services:
+ db:
+ image: mariadb:lts
+ restart: always
+ hostname: mariadb
+ container_name: mariadb
+ environment:
+ MARIADB_ROOT_PASSWORD: dbob16
+ MARIADB_DATABASE: tam3
+ MARIADB_USER: tam3
+ MARIADB_PASSWORD: tam3
+ volumes:
+ - "tam3-db:/var/lib/mysql"
+ - "./schema.sql:/docker-entrypoint-initdb.d/schema.sql:ro"
+ ports:
+ - 127.0.0.1:3306:3306
+ adminer:
+ image: adminer
+ restart: always
+ hostname: adminer
+ container_name: adminer
+ ports:
+ - 127.0.0.1:8080:8080
+
+volumes:
+ tam3-db:
\ No newline at end of file
diff --git a/db/schema.sql b/db/schema.sql
new file mode 100644
index 0000000..76325cb
--- /dev/null
+++ b/db/schema.sql
@@ -0,0 +1,40 @@
+CREATE TABLE IF NOT EXISTS api_keys (
+ `api_key` VARCHAR(255),
+ `pc_name` VARCHAR(255)
+);
+
+CREATE TABLE IF NOT EXISTS prefixes (
+ `name` VARCHAR(255),
+ `color` VARCHAR(255),
+ `weight` INT DEFAULT 0
+);
+
+CREATE TABLE IF NOT EXISTS tickets (
+ `prefix` VARCHAR(255),
+ `t_id` INT,
+ `first_name` VARCHAR(255),
+ `last_name` VARCHAR(255),
+ `phone_number` VARCHAR(255),
+ `preference` VARCHAR(20),
+ PRIMARY KEY (`prefix`, `t_id`)
+);
+
+CREATE TABLE IF NOT EXISTS baskets (
+ `prefix` VARCHAR(255),
+ `b_id` INT,
+ `description` VARCHAR(255),
+ `donors` VARCHAR(255),
+ `winning_ticket` INT
+);
+
+CREATE VIEW IF NOT EXISTS combined AS
+SELECT b.prefix, b.b_id, b.winning_ticket, CONCAT(t.last_name, ", ", t.first_name) AS winner
+FROM baskets b LEFT JOIN tickets t
+ON b.prefix = t.prefix AND b.winning_ticket = t.t_id
+ORDER BY b.prefix, b.b_id;
+
+CREATE VIEW IF NOT EXISTS report AS
+SELECT b.prefix, CONCAT(t.last_name, ", ", t.first_name) AS winner_name, t.phone_number, t.preference, b.b_id, b.winning_ticket, b.description
+FROM baskets b LEFT JOIN tickets t
+ON b.prefix = t.prefix AND b.winning_ticket = t.t_id
+ORDER BY b.prefix, winner_name, t.phone_number, t.preference, b.b_id;
\ No newline at end of file
diff --git a/webapp/src/hooks.server.js b/webapp/src/hooks.server.js
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/webapp/src/hooks.server.js
@@ -0,0 +1 @@
+
diff --git a/webapp/src/lib/css/main.css b/webapp/src/lib/css/main.css
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/webapp/src/lib/css/main.css
@@ -0,0 +1 @@
+
diff --git a/webapp/src/routes/+layout.svelte b/webapp/src/routes/+layout.svelte
index ff4c201..5a2edde 100644
--- a/webapp/src/routes/+layout.svelte
+++ b/webapp/src/routes/+layout.svelte
@@ -1,11 +1,12 @@
Visit svelte.dev/docs/kit to read the documentation
+ + + + + diff --git a/webapp/src/routes/api/prefixes/+server.svelte b/webapp/src/routes/api/prefixes/+server.svelte new file mode 100644 index 0000000..e69de29