nightly - 2025-09-20
This commit is contained in:
2
api/.gitignore
vendored
Normal file
2
api/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
__pycache__
|
||||||
|
*/__pycache__
|
||||||
7
api/data/config.ini
Normal file
7
api/data/config.ini
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
[db]
|
||||||
|
host = localhost
|
||||||
|
port = 3306
|
||||||
|
user = tam3
|
||||||
|
password = tam3
|
||||||
|
database = tam3
|
||||||
|
|
||||||
10
api/db.py
Normal file
10
api/db.py
Normal file
@@ -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
|
||||||
|
|
||||||
9
api/exceptions.py
Normal file
9
api/exceptions.py
Normal file
@@ -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."
|
||||||
|
)
|
||||||
48
api/key.py
Executable file
48
api/key.py
Executable file
@@ -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()
|
||||||
|
|
||||||
13
api/main.py
13
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)
|
||||||
|
|||||||
1
api/repos/__init__.py
Normal file
1
api/repos/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
from .api_keys import ApiKey, ApiKeyRepo
|
||||||
43
api/repos/api_keys.py
Normal file
43
api/repos/api_keys.py
Normal file
@@ -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."
|
||||||
39
api/repos/prefixes.py
Normal file
39
api/repos/prefixes.py
Normal file
@@ -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."
|
||||||
6
api/repos/template.py
Normal file
6
api/repos/template.py
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
from db import session
|
||||||
|
|
||||||
|
|
||||||
|
class Repo:
|
||||||
|
def __init__(self):
|
||||||
|
self.conn, self.cur = session()
|
||||||
0
api/routers/__init__.py
Normal file
0
api/routers/__init__.py
Normal file
26
api/routers/prefixes.py
Normal file
26
api/routers/prefixes.py
Normal file
@@ -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}
|
||||||
25
api/settings.py
Normal file
25
api/settings.py
Normal file
@@ -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
|
||||||
26
db/compose.yml
Normal file
26
db/compose.yml
Normal file
@@ -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:
|
||||||
40
db/schema.sql
Normal file
40
db/schema.sql
Normal file
@@ -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;
|
||||||
1
webapp/src/hooks.server.js
Normal file
1
webapp/src/hooks.server.js
Normal file
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
1
webapp/src/lib/css/main.css
Normal file
1
webapp/src/lib/css/main.css
Normal file
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
@@ -1,11 +1,12 @@
|
|||||||
<script>
|
<script>
|
||||||
import favicon from '$lib/assets/favicon.svg';
|
import favicon from "$lib/assets/favicon.svg";
|
||||||
|
import "$lib/css/main.css";
|
||||||
|
|
||||||
let { children } = $props();
|
let { children } = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
<link rel="icon" href={favicon} />
|
<link rel="icon" href={favicon} />
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
{@render children?.()}
|
{@render children?.()}
|
||||||
|
|||||||
@@ -1,2 +1,22 @@
|
|||||||
<h1>Welcome to SvelteKit</h1>
|
<script>
|
||||||
<p>Visit <a href="https://svelte.dev/docs/kit">svelte.dev/docs/kit</a> to read the documentation</p>
|
let prefix_name = $state("");
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="main-menu">
|
||||||
|
<h1>TAM3 - Main Menu</h1>
|
||||||
|
<div class="prefix-selector">
|
||||||
|
<select bind:value={prefix_name}></select>
|
||||||
|
</div>
|
||||||
|
<div class="form-buttons">
|
||||||
|
<a href="/tickets/" class="button">Tickets</a>
|
||||||
|
<a href="/baskets/" class="button">Baskets</a>
|
||||||
|
<a href="/drawing/" class="button">Drawing</a>
|
||||||
|
</div>
|
||||||
|
<button>Test</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.prefix-selector select {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
0
webapp/src/routes/api/prefixes/+server.svelte
Normal file
0
webapp/src/routes/api/prefixes/+server.svelte
Normal file
Reference in New Issue
Block a user