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 @@ - + {@render children?.()} diff --git a/webapp/src/routes/+page.svelte b/webapp/src/routes/+page.svelte index cc88df0..e61118a 100644 --- a/webapp/src/routes/+page.svelte +++ b/webapp/src/routes/+page.svelte @@ -1,2 +1,22 @@ -

Welcome to SvelteKit

-

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