(init): Keys and Prefix Management

This commit is contained in:
2026-06-21 23:56:50 -04:00
commit 80c747226f
50 changed files with 4631 additions and 0 deletions
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 Dilan Gilluly
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+7
View File
@@ -0,0 +1,7 @@
# Ticket Auction Manager
This is Ticket Auction Manager. A project I (Dilan Gilluly) am working on as a hobby project. It's main scope is to manage in person penny socials or benefit auctions.
## Under Development
This project is **NOT COMPLETE** and is under **active development**. Also I am not a programmer by profession so please forgive any beginner mistakes I make.
+2
View File
@@ -0,0 +1,2 @@
data
*/__pycache__
+27
View File
@@ -0,0 +1,27 @@
import os
from pathlib import Path
import sqlite3
tam_data_dir = os.getenv("TAM_DATA_DIR", "./data")
def session():
data_dir = Path(tam_data_dir)
if not data_dir.is_dir():
data_dir.mkdir(parents=True)
db_file = data_dir / "tam-remote.db"
conn = sqlite3.connect(db_file)
cur = conn.cursor()
return conn, cur
def init_db():
conn, cur = session()
cur.execute("CREATE TABLE IF NOT EXISTS auth_keys (auth_key TEXT PRIMARY KEY, description TEXT)")
cur.execute("CREATE TABLE IF NOT EXISTS prefixes (prefix TEXT PRIMARY KEY, color TEXT, weight INTEGER)")
conn.commit()
conn.close()
class RepoTemplate:
def __init__(self):
self.conn, self.cur = session()
def __del__(self):
self.conn.close()
+31
View File
@@ -0,0 +1,31 @@
from dataclasses import dataclass
from db import init_db
from fastapi import FastAPI, Header
import os
from routers import imp_routers
from system.auth import AuthRepo
tam_env = os.getenv("TAM_ENV", "prod")
@dataclass
class MainRoute:
whoami: str = "TAM Server"
authenticated: bool = False
healthy: bool = True
init_db()
app = FastAPI(
title="TAM Server",
description="Server for Ticket Auction Manager",
version="0.0.1",
docs_url=None if tam_env == "prod" else "/docs",
redoc_url=None,
openapi_url=None if tam_env == "prod" else "/openapi.json"
)
@app.get("/api")
def get_main_route(tam_key: str = Header("")):
return MainRoute(authenticated=AuthRepo().check_key(tam_key))
imp_routers(app)
+7
View File
@@ -0,0 +1,7 @@
from fastapi import FastAPI
from system.auth import auth_router
from system.prefixes import prefix_router
def imp_routers(app: FastAPI):
app.include_router(auth_router)
app.include_router(prefix_router)
Binary file not shown.
Binary file not shown.
+83
View File
@@ -0,0 +1,83 @@
from dataclasses import dataclass
from db import RepoTemplate
from fastapi import APIRouter, Header, HTTPException, status
import os
import random
import string
tam_pwd = os.getenv("TAM_PWD", "dbob16")
ex_pwd = HTTPException(status.HTTP_401_UNAUTHORIZED, "Invalid Password")
ex_key = HTTPException(status.HTTP_401_UNAUTHORIZED, "Invalid Key")
rdm_str_choice = string.ascii_uppercase + string.digits
def check_pw(in_pw: str):
if tam_pwd != in_pw:
raise ex_pwd
def gen_key():
return "".join(random.choice(rdm_str_choice) for _ in range(32))
@dataclass
class AuthReq:
description: str = ""
@dataclass
class AuthKey:
auth_key: str = ""
description: str = ""
class AuthRepo(RepoTemplate):
def get_all_keys(self):
self.cur.execute("SELECT * FROM auth_keys ORDER BY description, auth_key")
results = self.cur.fetchall()
return [AuthKey(*r) for r in results]
def create_key(self, desciption: str = ""):
while True:
new_key = gen_key()
self.cur.execute("SELECT * FROM auth_keys WHERE auth_key = ?", (new_key,))
results = self.cur.fetchall()
if len(results) == 0:
break
self.cur.execute("INSERT INTO auth_keys VALUES (?, ?) RETURNING *", (new_key, desciption))
rtn_key = self.cur.fetchone()
self.conn.commit()
return AuthKey(*rtn_key)
def del_key(self, auth_key: str = ""):
self.cur.execute("DELETE FROM auth_keys WHERE auth_key = ? RETURNING *", (auth_key,))
results = self.cur.fetchall()
self.conn.commit()
if len(results) > 0:
return AuthKey(*results[0])
else:
return AuthKey(auth_key, "")
def check_key(self, auth_key: str = ""):
self.cur.execute("SELECT * FROM auth_keys WHERE auth_key = ?", (auth_key,))
results = self.cur.fetchall()
if results:
return True
else:
return False
def verify_key(self, auth_key: str = ""):
self.cur.execute("SELECT * FROM auth_keys WHERE auth_key = ?", (auth_key,))
results = self.cur.fetchone()
if results:
return True
else:
raise ex_key
auth_router = APIRouter(prefix="/api/auth")
@auth_router.get("")
def get_auth_keys(tam_pw: str = Header("")) -> list[AuthKey]:
check_pw(tam_pw)
return AuthRepo().get_all_keys()
@auth_router.post("")
def create_auth_key(auth_req: AuthReq, tam_pw: str = Header("")) -> AuthKey:
check_pw(tam_pw)
return AuthRepo().create_key(auth_req.description)
@auth_router.delete("")
def delete_auth_key(key_to_del: str, tam_pw: str = Header("")) -> AuthKey:
check_pw(tam_pw)
return AuthRepo().del_key(key_to_del)
+48
View File
@@ -0,0 +1,48 @@
from dataclasses import dataclass
from db import RepoTemplate
from .auth import AuthRepo
from fastapi import APIRouter, Header
@dataclass
class Prefix:
prefix: str
color: str = ""
weight: int = 0
class PrefixRepo(RepoTemplate):
def get_all_prefixes(self):
self.cur.execute("SELECT * FROM prefixes ORDER BY weight, prefix")
results = self.cur.fetchall()
return [Prefix(*r) for r in results]
def post_prefixes(self, ps: list[Prefix]):
for p in ps:
self.cur.execute("""INSERT INTO prefixes VALUES (?, ?, ?)
ON CONFLICT (prefix) DO UPDATE SET
color = EXCLUDED.color, weight = EXCLUDED.weight""", (p.prefix, p.color, p.weight))
self.conn.commit()
return ps
def del_prefix(self, p: str):
self.cur.execute("DELETE FROM prefixes WHERE prefix = ? RETURNING *", (p,))
result = self.cur.fetchone()
self.conn.commit()
if result:
return Prefix(*result)
else:
return Prefix(p, "gray", 1)
prefix_router = APIRouter(prefix="/api/prefixes")
@prefix_router.get("")
def get_all_prefixes(tam_key: str = Header("")):
AuthRepo().verify_key(tam_key)
return PrefixRepo().get_all_prefixes()
@prefix_router.post("")
def post_prefixes(ps: list[Prefix], tam_key: str = Header("")):
AuthRepo().verify_key(tam_key)
return PrefixRepo().post_prefixes(ps)
@prefix_router.delete("")
def delete_prefixes(p: str, tam_key: str = Header("")):
AuthRepo().verify_key(tam_key)
return PrefixRepo().del_prefix(p)
+1
View File
@@ -0,0 +1 @@
TAM_ENV=dev fastapi dev app/main.py
+2
View File
@@ -0,0 +1,2 @@
# Drizzle
DATABASE_URL=local.db
+28
View File
@@ -0,0 +1,28 @@
node_modules
# Output
.output
.vercel
.netlify
.wrangler
/.svelte-kit
/build
# OS
.DS_Store
Thumbs.db
# Env
.env
.env.*
!.env.example
!.env.test
# Vite
vite.config.js.timestamp-*
vite.config.ts.timestamp-*
# SQLite
*.db
# My Additions
data
+1
View File
@@ -0,0 +1 @@
engine-strict=true
+10
View File
@@ -0,0 +1,10 @@
# Package Managers
package-lock.json
pnpm-lock.yaml
yarn.lock
bun.lock
bun.lockb
# Miscellaneous
/static/
/drizzle/
+16
View File
@@ -0,0 +1,16 @@
{
"useTabs": true,
"singleQuote": true,
"trailingComma": "none",
"printWidth": 100,
"plugins": ["prettier-plugin-svelte", "prettier-plugin-tailwindcss"],
"overrides": [
{
"files": "*.svelte",
"options": {
"parser": "svelte"
}
}
],
"tailwindStylesheet": "./src/routes/layout.css"
}
+8
View File
@@ -0,0 +1,8 @@
{
"recommendations": [
"svelte.svelte-vscode",
"esbenp.prettier-vscode",
"dbaeumer.vscode-eslint",
"bradlc.vscode-tailwindcss"
]
}
+5
View File
@@ -0,0 +1,5 @@
{
"files.associations": {
"*.css": "tailwindcss"
}
}
+42
View File
@@ -0,0 +1,42 @@
# sv
Everything you need to build a Svelte project, powered by [`sv`](https://github.com/sveltejs/cli).
## Creating a project
If you're seeing this, you've probably already done this step. Congrats!
```sh
# create a new project
npx sv create my-app
```
To recreate this project with the same configuration:
```sh
# recreate this project
pnpm dlx sv@0.16.1 create --template minimal --no-types --add prettier eslint tailwindcss="plugins:none" sveltekit-adapter="adapter:node" drizzle="database:sqlite+sqlite:better-sqlite3" --install pnpm ./client
```
## Developing
Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
```sh
npm run dev
# or start the server and open the app in a new browser tab
npm run dev -- --open
```
## Building
To create a production version of your app:
```sh
npm run build
```
You can preview the production build with `npm run preview`.
> To deploy your app, you may need to install an [adapter](https://svelte.dev/docs/kit/adapters) for your target environment.
+11
View File
@@ -0,0 +1,11 @@
import { defineConfig } from 'drizzle-kit';
const dbURL = (process.env.TAM_DATA_DIR || "./data") + "/tam-local.db"
export default defineConfig({
schema: './src/lib/server/db/schema.js',
dialect: 'sqlite',
dbCredentials: { url: dbURL },
verbose: true,
strict: true
});
+30
View File
@@ -0,0 +1,30 @@
import prettier from 'eslint-config-prettier';
import path from 'node:path';
import js from '@eslint/js';
import svelte from 'eslint-plugin-svelte';
import { defineConfig, includeIgnoreFile } from 'eslint/config';
import globals from 'globals';
const gitignorePath = path.resolve(import.meta.dirname, '.gitignore');
export default defineConfig([
includeIgnoreFile(gitignorePath),
js.configs.recommended,
svelte.configs.recommended,
prettier,
svelte.configs.prettier,
{
languageOptions: { globals: { ...globals.browser, ...globals.node } }
},
{
files: ['**/*.svelte', '**/*.svelte.js'],
languageOptions: { parserOptions: {} }
},
{
// Override or add rule settings here, such as:
// 'svelte/button-has-type': 'error'
rules: {}
}
]);
+13
View File
@@ -0,0 +1,13 @@
{
"extends": "./.svelte-kit/tsconfig.json",
"compilerOptions": {
"allowJs": true,
"checkJs": false,
"moduleResolution": "bundler"
}
// Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias
// except $lib which is handled by https://svelte.dev/docs/kit/configuration#files
//
// If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes
// from the referenced tsconfig.json - TypeScript does not merge them in
}
+43
View File
@@ -0,0 +1,43 @@
{
"name": "tam-client",
"private": true,
"version": "0.0.1",
"type": "module",
"scripts": {
"dev": "vite dev",
"build": "vite build",
"preview": "vite preview",
"prepare": "svelte-kit sync || echo ''",
"lint": "prettier --check . && eslint .",
"format": "prettier --write .",
"db:push": "drizzle-kit push",
"db:generate": "drizzle-kit generate",
"db:migrate": "drizzle-kit migrate",
"db:studio": "drizzle-kit studio"
},
"devDependencies": {
"@eslint/js": "^10.0.1",
"@sveltejs/adapter-node": "^5.5.4",
"@sveltejs/kit": "^2.63.0",
"@sveltejs/vite-plugin-svelte": "^7.1.2",
"@tailwindcss/vite": "^4.3.0",
"@types/better-sqlite3": "^7.6.13",
"@types/node": "^24",
"drizzle-kit": "^0.31.10",
"drizzle-orm": "^0.45.2",
"eslint": "^10.4.1",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-svelte": "^3.19.0",
"globals": "^17.6.0",
"prettier": "^3.8.3",
"prettier-plugin-svelte": "^4.1.0",
"prettier-plugin-tailwindcss": "^0.8.0",
"svelte": "^5.56.1",
"tailwindcss": "^4.3.0",
"vite": "^8.0.16"
},
"dependencies": {
"better-sqlite3": "^12.10.0",
"hotkeys-js": "^4.0.4"
}
}
+3454
View File
File diff suppressed because it is too large Load Diff
+4
View File
@@ -0,0 +1,4 @@
allowBuilds:
'@tailwindcss/oxide': true
better-sqlite3: true
esbuild: true
+12
View File
@@ -0,0 +1,12 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="text-scale" content="scale" />
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover">
<div style="display: contents">%sveltekit.body%</div>
</body>
</html>
+22
View File
@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="77.149315mm"
height="34.676437mm"
viewBox="0 0 77.149315 34.676437"
version="1.1"
id="svg1"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<g
id="layer1"
transform="translate(-6.761509,-12.267066)">
<path
id="rect1"
style="fill:#0000ff;stroke:#ffffff;stroke-width:0.264583"
d="m 11.932255,12.399408 a 4.9817667,4.9817667 0 0 1 0.06511,0.800468 4.9817667,4.9817667 0 0 1 -4.9821245,4.981608 4.9817667,4.9817667 0 0 1 -0.12144,-0.0016 v 7.822261 a 4.9817667,4.9817667 0 0 1 1.86707,3.888135 4.9817667,4.9817667 0 0 1 -1.86707,3.888135 v 7.793323 a 4.9817667,4.9817667 0 0 1 4.9593875,4.981608 4.9817667,4.9817667 0 0 1 -0.0067,0.257865 h 67.265145 a 4.9817667,4.9817667 0 0 1 4.666898,-4.031795 v -8.383984 a 4.9817667,4.9817667 0 0 1 -2.524394,-4.333069 4.9817667,4.9817667 0 0 1 2.524394,-4.333586 V 18.364886 A 4.9817667,4.9817667 0 0 1 79.192765,13.39878 4.9817667,4.9817667 0 0 1 79.29405,12.399358 Z m 1.894974,5.25291 H 37.69864 v 9.300207 L 30.647908,24.164065 29.616963,40.45506 22.323868,39.939846 21.808654,22.92228 12.84176,24.266384 Z m 28.999264,2.274796 h 5.303552 l 4.713408,17.589624 h -3.092318 l -2.294434,-8.561751 h -4.924764 l -2.13682,7.973673 h -3.461288 z m 15.329297,0.690397 h 4.715474 l 2.69751,10.068119 2.689241,-10.036596 h 5.370732 l 4.734595,17.669722 h -3.493327 l -3.205489,-11.963094 -4.104659,7.109127 h -3.240629 l -3.600814,-6.236829 -2.535763,9.462989 -4.413684,0.298689 z m -14.297836,2.550749 -1.473295,3.244763 h 6.025989 L 47.540934,23.16826 Z" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

@@ -0,0 +1,13 @@
<script>
import { resolve } from '$app/paths';
import { bS } from '../styles';
let { children } = $props();
</script>
<div id="header_bar" class="p-1 flex flex-row gap-1 items-center">
<a href={resolve('/')} class="{bS.gray} font-bold">Main Menu</a>
{#if children}
{@render children()}
{/if}
</div>
+19
View File
@@ -0,0 +1,19 @@
export const tS = {
green: 'text-green-800 font-bold',
yellow: 'text-yellow-800 font-bold',
red: 'text-red-800 font-bold'
}
export const bS = {
gray: 'px-2 py-1 bg-gray-300 border border-gray-800 rounded hover:bg-gray-400 cursor-pointer',
white: 'px-2 py-1 bg-white border border-gray-700 rounded hover:bg-gray-200 cursor-pointer',
blue: 'px-2 py-1 bg-blue-300 border border-gray-700 rounded hover:bg-blue-400 cursor-pointer',
yellow: 'px-2 py-1 bg-yellow-300 border border-gray-700 rounded hover:bg-yellow-400 cursor-pointer',
green: 'px-2 py-1 bg-green-300 border border-gray-700 rounded hover:bg-green-400 cursor-pointer',
orange: 'px-2 py-1 bg-orange-300 border border-gray-700 rounded hover:bg-orange-400 cursor-pointer',
red: 'px-2 py-1 bg-red-300 border border-gray-700 rounded hover:bg-red-400 cursor-pointer'
}
export const iS = {
normal: 'p-1 border border-black'
}
+1
View File
@@ -0,0 +1 @@
// place files you want to import through the `$lib` alias in this folder.
+10
View File
@@ -0,0 +1,10 @@
import { drizzle } from 'drizzle-orm/better-sqlite3';
import Database from 'better-sqlite3';
import * as schema from './schema';
import { env } from '$env/dynamic/private';
const dbURL = (env.TAM_DATA_DIR || "./data") + "/tam-local.db";
const client = new Database(dbURL);
export const db = drizzle(client, { schema });
+7
View File
@@ -0,0 +1,7 @@
import { integer, sqliteTable, text } from 'drizzle-orm/sqlite-core';
export const prefixes = sqliteTable('prefixes', {
prefix: text('prefix').primaryKey(),
color: text('color'),
weight: integer('weight')
});
+38
View File
@@ -0,0 +1,38 @@
import { env } from '$env/dynamic/private';
import { readFileSync, writeFileSync, mkdirSync } from 'fs';
const defaultSettings = {
remote_server: '',
remote_key: '',
remote_port: '8000',
remote_tls: false,
default_pref: 'CALL',
venue_name: 'TAM'
};
export const getSettings = () => {
const settingsPath = (env.TAM_DATA_DIR || './data') + '/settings.json';
try {
const settings = JSON.parse(readFileSync(settingsPath, 'utf-8'));
return settings;
} catch {
mkdirSync(env.TAM_DATA_DIR || './data', {recursive: true})
writeFileSync(settingsPath, JSON.stringify(defaultSettings, null, 2), 'utf-8');
return defaultSettings;
}
};
export const setSettings = (nS) => {
const settingsPath = (env.TAM_DATA_DIR || './data') + '/settings.json';
const cS = getSettings();
const newSettings = JSON.stringify({ ...cS, ...nS }, null, 2);
writeFileSync(settingsPath, newSettings, 'utf-8');
return 'Settings written successfully!';
};
export const getPath = (sO) => {
if (!sO.remote_server) return undefined;
const prefix = sO.remote_tls ? 'https://' : 'http://';
const connStr = prefix + sO.remote_server + ':' + sO.remote_port;
return connStr;
};
+12
View File
@@ -0,0 +1,12 @@
<script>
import './layout.css';
import favicon from '$lib/assets/favicon.svg';
let { children } = $props();
</script>
<svelte:head>
<link rel="icon" href={favicon} />
<link rel="manifest" href="/manifest.json">
</svelte:head>
{@render children()}
+8
View File
@@ -0,0 +1,8 @@
import { getSettings } from '$lib/server/settings';
export const load = async ({ fetch }) => {
const settings = getSettings();
const res = await fetch('/api');
const data = await res.json();
return { ...data, venueName: settings.venue_name };
};
+64
View File
@@ -0,0 +1,64 @@
<script>
import { tS, bS } from '$lib/client/styles.js';
import { browser } from '$app/environment';
import { resolve } from '$app/paths';
import hotkeys from 'hotkeys-js';
const pageTitle = 'Main Menu | TAM'
const { data } = $props();
let adminMode = $state(false);
const status = $derived.by(() => {
if (data.whoami === 'TAM Server') {
return {
mode: 'Remote',
auth: data.authenticated ? 'green' : 'red',
healthy: data.healthy ? 'green' : 'red'
};
} else if (data.whoami === 'TAM Client') {
return {
mode: 'Standalone'
};
} else {
return {
mode: 'Unknown'
}
}
})
if (browser) {
hotkeys.filter = () => {return true};
hotkeys('alt+a', (event) => {
event.preventDefault();
adminMode = !adminMode;
})
}
</script>
<svelte:head>
<title>{pageTitle}</title>
</svelte:head>
<div class="p-1" id="app_container">
<h1 class="text-xl font-bold">{pageTitle}</h1>
<p class="text-lg italic">{data.venueName}</p>
{#if adminMode}
<div id="admin_mode">
<h2 class="text-lg font-bold">Admin Mode:</h2>
<div class="flex flex-row gap-1">
<a href={resolve('/settings')} class={bS.gray}>Settings</a>
</div>
</div>
{/if}
<div id="footer">
<div>Mode: {status.mode}</div>
{#if data.authenticated !== undefined}
<div>Authenticated: <span class={tS[status.auth]}>{data.authenticated ? 'Yes' : 'No'}</span></div>
{/if}
{#if data.healthy !== undefined}
<div>Server Healthy: <span class={tS[status.healthy]}>{data.healthy ? 'Yes' : 'No'}</span></div>
{/if}
</div>
</div>
+23
View File
@@ -0,0 +1,23 @@
import { getSettings, getPath } from '$lib/server/settings';
import { json, error } from '@sveltejs/kit';
export const GET = async () => {
const s = getSettings();
if (s.remote_server) {
const connStr = getPath(s);
try {
const res = await fetch(`${connStr}/api`, {
headers: { 'TAM-KEY': s.remote_key }
});
if (!res.ok) throw error(res.status);
const data = await res.json()
return json(data);
} catch {
const data = { whoami: 'TAM Server', authenticated: false, healthy: false };
return json(data);
}
} else {
const data = { whoami: 'TAM Client' };
return json(data);
}
};
+67
View File
@@ -0,0 +1,67 @@
import { getSettings, getPath } from "$lib/server/settings";
import { json, error } from "@sveltejs/kit";
export const GET = async ({ request }) => {
const tamPwd = request.headers.get('TAM-PWD');
const s = getSettings();
if (s.remote_server) {
const connStr = getPath(s);
try {
const res = await fetch(`${connStr}/api/auth`, {
headers: { 'TAM-PW': tamPwd }
});
if (!res.ok) throw error(res.status);
const data = await res.json();
return json(data);
} catch {
throw error(502)
}
} else {
throw error(500, 'Not configured.')
}
}
export const POST = async ({ request }) => {
const tamPwd = request.headers.get('TAM-PWD');
const reqBody = await request.json();
const s = getSettings();
if (s.remote_server) {
const connStr = getPath(s);
try {
const res = await fetch(`${connStr}/api/auth`, {
headers: { 'TAM-PW': tamPwd, 'Content-Type': 'application/json' },
method: 'POST',
body: JSON.stringify(reqBody)
});
if (!res.ok) throw error(res.status);
const data = await res.json();
return json(data);
} catch {
throw error(502);
}
} else {
throw error(500, 'Not configured.')
}
}
export const DELETE = async ({ request, url }) => {
const tamPwd = request.headers.get('TAM-PWD');
const keyToDel = url.searchParams.get('key_to_del');
const s = getSettings();
if (s.remote_server) {
const connStr = getPath(s);
try {
const res = await fetch(`${connStr}/api/auth?key_to_del=${keyToDel}`, {
headers: { 'TAM-PW': tamPwd },
method: 'DELETE'
})
if (!res.ok) throw error(res.status)
const data = await res.json();
return json(data);
} catch {
throw error(502);
}
} else {
throw error(500, 'Not configured.')
}
}
+52
View File
@@ -0,0 +1,52 @@
import { getSettings, getPath } from "$lib/server/settings"
import { json, error } from "@sveltejs/kit";
import { db } from "$lib/server/db";
import { prefixes } from "$lib/server/db/schema";
import { sql, eq } from "drizzle-orm";
export const GET = async () => {
const s = getSettings();
if (s.remote_server) {
const connStr = getPath(s);
const res = await fetch(`${connStr}/api/prefixes`, {
headers: { 'TAM-KEY': s.remote_key }
});
if (!res.ok) throw error(res.status);
const data = await res.json();
return json(data);
} else {
const data = await db.select().from(prefixes).orderBy(prefixes.weight, prefixes.prefix);
return json(data);
}
}
export const POST = async ({ request }) => {
const reqData = await request.json();
const s = getSettings();
if (s.remote_server) {
const connStr = getPath(s);
const res = await fetch(`${connStr}/api/prefixes`, {
method: 'POST',
headers: { 'TAM-KEY': s.remote_key, 'Content-Type': 'application/json' },
body: JSON.stringify(reqData)
})
if (!res.ok) throw error(res.status);
}
await db.insert(prefixes).values(reqData).onConflictDoUpdate({ target: prefixes.prefix, set: { color: sql`EXCLUDED.color`, weight: sql`EXCLUDED.weight` } });
return json(reqData);
}
export const DELETE = async ({ url }) => {
const p = url.searchParams.get('p');
const s = getSettings();
if (s.remote_server) {
const connStr = getPath(s);
const res = await fetch(`${connStr}/api/prefixes?p=${p}`, {
method: 'DELETE',
headers: { 'TAM-KEY': s.remote_key }
});
if (!res.ok) throw error(res.status);
}
await db.delete(prefixes).where(eq(prefixes.prefix, p));
return json({ message: "Deleted successfully."});
}
@@ -0,0 +1,8 @@
import { setSettings } from "$lib/server/settings";
import { json } from "@sveltejs/kit";
export const POST = async ({ request }) => {
const newSettings = await request.json();
setSettings(newSettings);
return json(newSettings);
}
+1
View File
@@ -0,0 +1 @@
@import 'tailwindcss';
@@ -0,0 +1,6 @@
import { getSettings } from "$lib/server/settings";
export const load = () => {
const settings = getSettings();
return { settings };
}
+85
View File
@@ -0,0 +1,85 @@
<script>
import { onMount } from 'svelte';
import { resolve } from '$app/paths'
import { bS, iS, tS } from '$lib/client/styles';
import HeaderBar from '$lib/client/components/HeaderBar.svelte'
let { data } = $props();
let settings = $state({});
let status = $state({
message: '',
color: 'green'
})
const pageTitle = 'Settings | TAM';
onMount(() => {
settings = {...data.settings};
})
</script>
<svelte:head>
<title>{pageTitle}</title>
</svelte:head>
<HeaderBar>
<div>Settings Sections:</div>
{#if data.settings.remote_server}
<a href={resolve('/settings/auth-keys')} class={bS.gray}>Auth Keys</a>
{/if}
<a href={resolve('/settings/prefixes')} class={bS.gray}>Prefixes</a>
</HeaderBar>
<div id="app_container" class="p-1">
<h1 class="text-xl font-bold">{pageTitle}</h1>
<div class="flex flex-col gap-1 w-full py-1">
<h2 class="text-lg font-bold">Remote Mode:</h2>
<div class="flex flex-row gap-1 items-center">
<div>Remote Server:</div>
<input type="text" class={iS.normal} bind:value={settings.remote_server}>
</div>
<div class="flex flex-row gap-1 items-center">
<div>Remote Port:</div>
<input type="text" class={iS.normal} bind:value={settings.remote_port}>
</div>
<div class="flex flex-row gap-1 items-center">
<div>Remote TLS:</div>
<button class={bS.gray} onclick={() => {
settings.remote_tls = !settings.remote_tls
}}>{settings.remote_tls ? 'Yes' : 'No'}</button>
</div>
<h2 class="text-lg font-bold">Default Preferences:</h2>
<div class="flex flex-row gap-1 items-center">
<div>Contact Preference:</div>
<button class={bS.gray} onclick={() => {
settings.default_pref === 'CALL' ? settings.default_pref = 'TEXT' : settings.default_pref = 'CALL';
}}>{settings.default_pref}</button>
</div>
<div class="flex flex-row gap-1 items-center">
<div>Venue Name:</div>
<input type="text" class={iS.normal} bind:value={settings.venue_name}>
</div>
<div class="flex flex-row gap-1 items-center">
<button class={bS.gray} onclick={async () => {
const res = await fetch('/api/settings', {
method: 'POST',
body: JSON.stringify(settings),
headers: {'Content-Type': 'application/json'}
})
if (!res.ok) {
status.message = `Error Code: ${res.status}`;
status.color = 'red';
} else {
const resData = await res.json();
settings = {...resData};
status.message = 'Settings saved successfully!';
status.color = 'green';
}
}}>Save</button>
<button class={bS.gray}>Cancel</button>
</div>
<div>
<p class={tS[status.color]}>{status.message}</p>
</div>
</div>
</div>
@@ -0,0 +1,6 @@
import { getSettings } from "$lib/server/settings";
export const load = () => {
const s = getSettings();
return { authKey: s.remote_key };
}
@@ -0,0 +1,138 @@
<script>
import HeaderBar from '$lib/client/components/HeaderBar.svelte';
import { resolve } from '$app/paths';
import { bS, iS } from '$lib/client/styles';
let { data } = $props();
const pageTitle = 'Auth Keys | TAM';
let auth = $state({
toggle: false,
pwd: ''
});
let authKeys = $state([]);
let newDesc = $state('');
let curKey = $state('');
</script>
<svelte:head>
<title>{pageTitle}</title>
</svelte:head>
<HeaderBar>
<a href={resolve('/settings')} class={bS.gray}>Back to Settings</a>
<div>Other Settings:</div>
</HeaderBar>
<div id="app_container" class="p-1">
<h1 class="text-xl font-bold">{pageTitle}</h1>
{#if !auth.toggle}
<div class="flex flex-row gap-1 items-center py-1">
<div>Password:</div>
<input type="password" class={iS.normal} bind:value={auth.pwd} />
<button
class={bS.gray}
onclick={async () => {
const res = await fetch('/api/auth', {
headers: { 'TAM-PWD': auth.pwd }
});
if (res.ok) {
auth.toggle = true;
const resData = await res.json();
authKeys = [...resData];
curKey = data.authKey;
} else {
auth.pwd = 'Invalid Password!';
setTimeout(() => (auth.pwd = ''), 3000);
}
}}>Login</button
>
</div>
{:else}
<table class="w-full my-1">
<thead class="text-left">
<tr>
<th>AuthKey</th>
<th>Description</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{#each authKeys as key (key.auth_key)}
<tr>
<td>{'*'.repeat(key.auth_key.length - 4) + key.auth_key.slice(-4)}</td>
<td>{key.description}</td>
<td
><div class="flex flex-row gap-1 items-center">
{#if curKey == key.auth_key}
<div>(Cur)</div>
{:else}
<button
class={bS.gray}
onclick={async () => {
const res = await fetch('/api/settings', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ remote_key: key.auth_key })
});
if (res.ok) {
curKey = key.auth_key;
}
}}>Use</button
>
{/if}
<button
class={bS.red}
onclick={async () => {
const res = await fetch(`/api/auth?key_to_del=${key.auth_key}`, {
method: 'DELETE',
headers: { 'TAM-PWD': auth.pwd }
});
if (res.ok) {
setTimeout(() => {
const newKeys = authKeys.filter(i => i.auth_key !== key.auth_key);
authKeys = [...newKeys];
}, 1)
}
}}>Delete</button
>
</div></td
>
</tr>
{/each}
<tr>
<td>New</td>
<td><input type="text" class="{iS.normal} w-full" bind:value={newDesc} /></td>
<td>
<button
class={bS.gray}
onclick={async () => {
if (newDesc) {
const res = await fetch('/api/auth', {
method: 'POST',
headers: { 'TAM-PWD': auth.pwd, 'Content-Type': 'application/json' },
body: JSON.stringify({ description: newDesc })
});
if (res.ok) {
newDesc = '';
const data = await res.json();
authKeys = [...authKeys, data];
}
}
}}>New Key</button
>
</td>
</tr>
</tbody>
</table>
{/if}
</div>
<style>
table td,
table th {
border: solid black 1px;
padding: 0.25rem;
}
</style>
@@ -0,0 +1,5 @@
export const load = async ({ fetch }) => {
const res = await fetch('/api/prefixes');
const prefixes = await res.json();
return { prefixes };
}
@@ -0,0 +1,109 @@
<script>
import { onMount } from "svelte";
import HeaderBar from "$lib/client/components/HeaderBar.svelte";
import { bS, iS } from "$lib/client/styles";
import { resolve } from "$app/paths";
let { data } = $props();
const pageTitle = 'Prefixes | TAM';
let prefixes = $state([]);
let editPrefix = $state({prefix: '', color: 'white', weight: 1})
onMount(() => {
prefixes = [...data.prefixes];
})
</script>
<svelte:head>
<title>{pageTitle}</title>
</svelte:head>
<HeaderBar>
<a href={resolve('/settings')} class={bS.gray}>Back to Settings</a>
</HeaderBar>
<div id="app_container" class="p-1">
<h1 class="text-xl font-bold">{pageTitle}</h1>
<div class="flex flex-row gap-1 py-1 items-center">
<div class="flex flex-col gap-1">
<div>Prefix</div>
<input type="text" class={iS.normal} bind:value={editPrefix.prefix}>
</div>
<div class="flex flex-col gap-1">
<div>Color</div>
<select name="prefix_color" class={iS.normal} bind:value={editPrefix.color}>
<option value="white">White</option>
<option value="blue">Blue</option>
<option value="yellow">Yellow</option>
<option value="green">Green</option>
<option value="orange">Orange</option>
<option value="red">Red</option>
</select>
</div>
<div class="flex flex-col gap-1">
<div>Weight</div>
<input type="number" class={iS.normal} bind:value={editPrefix.weight}>
</div>
<div class="flex flex-col gap-1">
<div>Actions</div>
<button class={bS[editPrefix.color]} onclick={async () => {
if (editPrefix.prefix) {
const req = await fetch('/api/prefixes', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify([editPrefix])
});
if (req.ok) window.location.reload();
}
}}>Add/Change</button>
</div>
</div>
<div class="flex flex-row gap-1 py-1 items-center">
<div>Colors:</div>
<button class={bS.white} onclick={() => editPrefix.color = "white"}>White</button>
<button class={bS.blue} onclick={() => editPrefix.color = "blue"}>Blue</button>
<button class={bS.yellow} onclick={() => editPrefix.color = "yellow"}>Yellow</button>
<button class={bS.green} onclick={() => editPrefix.color = "green"}>Green</button>
<button class={bS.orange} onclick={() => editPrefix.color = "orange"}>Orange</button>
<button class={bS.red} onclick={() => editPrefix.color = "red"}>Red</button>
</div>
<table class="w-full">
<thead class="text-left">
<tr>
<th>Prefix</th>
<th>Color</th>
<th>Weight</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{#each prefixes as prefix (prefix.prefix)}
<tr>
<td>{prefix.prefix}</td>
<td>{prefix.color.charAt(0).toUpperCase() + prefix.color.slice(1)}</td>
<td>{prefix.weight}</td>
<td>
<div class="flex flex-row gap-1 items-center">
<button class={bS[prefix.color]} onclick={() => editPrefix = {...prefix}}>Edit</button>
<button class={bS[prefix.color]} onclick={async () => {
const res = await fetch(`/api/prefixes?p=${prefix.prefix}`, {
method: 'DELETE'
});
if (res.ok) window.location.reload();
}}>Delete</button>
</div>
</td>
</tr>
{/each}
</tbody>
</table>
</div>
<style>
table td, table th {
border: solid black 1px;
padding: 0.25rem
}
</style>
+4
View File
@@ -0,0 +1,4 @@
{
"short_name": "TAM",
"name": "Ticket Auction Manager"
}
+3
View File
@@ -0,0 +1,3 @@
# allow crawling everything by default
User-agent: *
Disallow:
+24
View File
@@ -0,0 +1,24 @@
import tailwindcss from '@tailwindcss/vite';
import adapter from '@sveltejs/adapter-node';
import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vite';
export default defineConfig({
plugins: [
tailwindcss(),
sveltekit({
compilerOptions: {
// Force runes mode for the project, except for libraries. Can be removed in svelte 6.
runes: ({ filename }) =>
filename.split(/[/\\]/).includes('node_modules') ? undefined : true
},
adapter: adapter(),
typescript: {
config: (config) => ({
...config,
include: [...config.include, '../drizzle.config.js']
})
}
})
]
});