start dev

This commit is contained in:
Drake Marino 2026-01-18 08:19:01 -06:00
parent 93cfd87821
commit 144cd4787e
18 changed files with 837 additions and 32 deletions

202
package-lock.json generated
View File

@ -7,6 +7,12 @@
"": {
"name": "fbla26",
"version": "0.0.1",
"dependencies": {
"bcrypt": "^6.0.0",
"dotenv": "^17.2.3",
"jsonwebtoken": "^9.0.3",
"postgres": "^3.4.7"
},
"devDependencies": {
"@eslint/compat": "^1.4.0",
"@eslint/js": "^9.36.0",
@ -2280,6 +2286,20 @@
"dev": true,
"license": "MIT"
},
"node_modules/bcrypt": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-6.0.0.tgz",
"integrity": "sha512-cU8v/EGSrnH+HnxV2z0J7/blxH8gq7Xh2JFT6Aroax7UohdmiJJlxApMxtKfuI7z68NvvVcmR78k2LbT6efhRg==",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
"node-addon-api": "^8.3.0",
"node-gyp-build": "^4.8.4"
},
"engines": {
"node": ">= 18"
}
},
"node_modules/brace-expansion": {
"version": "1.1.12",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
@ -2304,6 +2324,12 @@
"node": ">=8"
}
},
"node_modules/buffer-equal-constant-time": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
"integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==",
"license": "BSD-3-Clause"
},
"node_modules/cac": {
"version": "6.7.14",
"resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz",
@ -2555,6 +2581,27 @@
"dev": true,
"license": "MIT"
},
"node_modules/dotenv": {
"version": "17.2.3",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.3.tgz",
"integrity": "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==",
"license": "BSD-2-Clause",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://dotenvx.com"
}
},
"node_modules/ecdsa-sig-formatter": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
"integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
"license": "Apache-2.0",
"dependencies": {
"safe-buffer": "^5.0.1"
}
},
"node_modules/enhanced-resolve": {
"version": "5.18.3",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz",
@ -3262,6 +3309,49 @@
"dev": true,
"license": "MIT"
},
"node_modules/jsonwebtoken": {
"version": "9.0.3",
"resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz",
"integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==",
"license": "MIT",
"dependencies": {
"jws": "^4.0.1",
"lodash.includes": "^4.3.0",
"lodash.isboolean": "^3.0.3",
"lodash.isinteger": "^4.0.4",
"lodash.isnumber": "^3.0.3",
"lodash.isplainobject": "^4.0.6",
"lodash.isstring": "^4.0.1",
"lodash.once": "^4.0.0",
"ms": "^2.1.1",
"semver": "^7.5.4"
},
"engines": {
"node": ">=12",
"npm": ">=6"
}
},
"node_modules/jwa": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz",
"integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==",
"license": "MIT",
"dependencies": {
"buffer-equal-constant-time": "^1.0.1",
"ecdsa-sig-formatter": "1.0.11",
"safe-buffer": "^5.0.1"
}
},
"node_modules/jws": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz",
"integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==",
"license": "MIT",
"dependencies": {
"jwa": "^2.0.1",
"safe-buffer": "^5.0.1"
}
},
"node_modules/keyv": {
"version": "4.5.4",
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
@ -3575,6 +3665,42 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/lodash.includes": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
"integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==",
"license": "MIT"
},
"node_modules/lodash.isboolean": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz",
"integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==",
"license": "MIT"
},
"node_modules/lodash.isinteger": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz",
"integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==",
"license": "MIT"
},
"node_modules/lodash.isnumber": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz",
"integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==",
"license": "MIT"
},
"node_modules/lodash.isplainobject": {
"version": "4.0.6",
"resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
"integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==",
"license": "MIT"
},
"node_modules/lodash.isstring": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz",
"integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==",
"license": "MIT"
},
"node_modules/lodash.merge": {
"version": "4.6.2",
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
@ -3582,6 +3708,12 @@
"dev": true,
"license": "MIT"
},
"node_modules/lodash.once": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
"integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==",
"license": "MIT"
},
"node_modules/loupe": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz",
@ -3716,7 +3848,6 @@
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"dev": true,
"license": "MIT"
},
"node_modules/nanoid": {
@ -3745,6 +3876,26 @@
"dev": true,
"license": "MIT"
},
"node_modules/node-addon-api": {
"version": "8.5.0",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.5.0.tgz",
"integrity": "sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A==",
"license": "MIT",
"engines": {
"node": "^18 || ^20 || >= 21"
}
},
"node_modules/node-gyp-build": {
"version": "4.8.4",
"resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz",
"integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==",
"license": "MIT",
"bin": {
"node-gyp-build": "bin.js",
"node-gyp-build-optional": "optional.js",
"node-gyp-build-test": "build-test.js"
}
},
"node_modules/optionator": {
"version": "0.9.4",
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
@ -4041,6 +4192,19 @@
"node": ">=4"
}
},
"node_modules/postgres": {
"version": "3.4.7",
"resolved": "https://registry.npmjs.org/postgres/-/postgres-3.4.7.tgz",
"integrity": "sha512-Jtc2612XINuBjIl/QTWsV5UvE8UHuNblcO3vVADSrKsrc6RqGX6lOW1cEo3CM2v0XG4Nat8nI+YM7/f26VxXLw==",
"license": "Unlicense",
"engines": {
"node": ">=12"
},
"funding": {
"type": "individual",
"url": "https://github.com/sponsors/porsager"
}
},
"node_modules/prelude-ls": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
@ -4366,11 +4530,30 @@
"node": ">=6"
}
},
"node_modules/safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT"
},
"node_modules/semver": {
"version": "7.7.3",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
"integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
"dev": true,
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
@ -5122,21 +5305,6 @@
"node": ">=18"
}
},
"node_modules/yaml": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz",
"integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==",
"dev": true,
"license": "ISC",
"optional": true,
"peer": true,
"bin": {
"yaml": "bin.mjs"
},
"engines": {
"node": ">= 14.6"
}
},
"node_modules/yocto-queue": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",

View File

@ -41,5 +41,11 @@
"vite": "^7.1.7",
"vitest": "^3.2.4",
"vitest-browser-svelte": "^1.1.0"
},
"dependencies": {
"bcrypt": "^6.0.0",
"dotenv": "^17.2.3",
"jsonwebtoken": "^9.0.3",
"postgres": "^3.4.7"
}
}

View File

@ -1,2 +1,317 @@
@import 'tailwindcss';
@plugin '@tailwindcss/forms';
/* defaults */
:root {
--text-color: #000000;
--text-box-bg: #00000010;
--bg-color: #e9e9e9;
--hover-bg-color: #d0d0d0;
--separator-line-color: #a0a0a0;
--low-emphasis-text-color: #6b6b6b;
--primary-color: #1f96f3;
--dull-primary-color: #51aaf0;
--elevated-bg-color: #ffffff;
--bg-accent-color: #f4f4f4;
--danger-color: #ff2d2f;
--hyperlink-color: #3b82f6;
--nav-bg-color: #f0f0f0;
background: var(--bg-color);
color: var(--text-color);
}
.bg-color {
background-color: var(--bg-color);
}
.hover-bg-color:hover {
background-color: var(--hover-bg-color);
}
.low-emphasis-text {
color: var(--low-emphasis-text-color);
}
.low-emphasis-text-button {
color: var(--low-emphasis-text-color);
}
.low-emphasis-text-button:hover {
color: var(--text-color);
}
.primary-underline {
border-bottom: 2px solid var(--primary-color);
}
.top-border {
border-top: 1px solid var(--separator-line-color);
}
.bottom-border {
border-bottom: 1px solid var(--separator-line-color);
}
.left-border {
border-left: 1px solid var(--separator-line-color);
}
.right-border {
border-right: 1px solid var(--separator-line-color);
}
input[type='search'],
input[type='text'],
input[type='password'],
input[type='email'],
input[type='tel'],
input[type='number'],
textarea,
select {
@apply rounded-t;
background-color: var(--text-box-bg);
color: var(--text-color);
border: 0;
border-bottom: 2px solid var(--separator-line-color);
caret-color: var(--text-color);
}
input[type='search']:focus,
input[type='text']:focus,
input[type='password']:focus,
input[type='email']:focus,
input[type='tel']:focus,
input[type='number']:focus,
textarea:focus,
select:focus {
outline: 0 solid var(--text-color);
border-bottom: 2px solid var(--primary-color);
box-shadow: 0 0 0 0 var(--primary-color);
caret-color: var(--text-color);
}
input[type='search']:-webkit-autofill,
input[type='text']:-webkit-autofill,
input[type='password']:-webkit-autofill,
input[type='email']:-webkit-autofill,
input[type='tel']:-webkit-autofill,
input[type='number']:-webkit-autofill,
textarea:-webkit-autofill,
select:-webkit-autofill {
-webkit-text-fill-color: var(--text-color) !important;
background-clip: text !important;
caret-color: var(--text-color);
}
input[type='search'].outlined,
input[type='text'].outlined,
input[type='password'].outlined,
input[type='email'].outlined,
input[type='tel'].outlined,
input[type='number'].outlined,
textarea.outlined,
select.outlined {
@apply rounded;
border: 1px solid var(--separator-line-color);
}
input[type='search'].outlined:focus,
input[type='text'].outlined:focus,
input[type='password'].outlined:focus,
input[type='email'].outlined:focus,
input[type='tel'].outlined:focus,
input[type='number'].outlined:focus,
textarea.outlined:focus,
select.outlined:focus {
border: 1px solid var(--primary-color);
}
input[type='checkbox'] {
background-color: var(--text-box-bg);
border: 1px solid var(--separator-line-color);
border-radius: 4px;
}
input[type='checkbox']:focus {
box-shadow: none;
outline: none;
}
.text-box-bg {
background-color: var(--text-box-bg) !important;
}
.separator-borders {
border: 1px solid var(--separator-line-color);
}
.elevated {
background-color: var(--elevated-bg-color);
@apply shadow-lg;
}
.elevated-bg {
background-color: var(--elevated-bg-color);
}
.card {
@apply rounded-lg p-4 block shadow-lg;
background-color: var(--elevated-bg-color);
}
.nav-bg {
background-color: var(--nav-bg-color);
}
.primary-bg-color {
background-color: var(--primary-color);
}
.accent-bg-color {
background-color: var(--bg-accent-color);
}
.dull-primary-bg-color {
background-color: var(--dull-primary-color);
}
.dull-primary-text-color {
color: var(--dull-primary-color);
}
.dull-primary-border-color {
border-color: var(--dull-primary-color);
}
.primary-text-color {
color: var(--primary-color);
}
.danger-bg-color {
background-color: var(--danger-color);
}
.danger-color {
color: var(--danger-color);
}
.danger-border-color {
border-color: var(--danger-color);
}
.hover-hyperlink:hover {
color: var(--hyperlink-color);
text-decoration: underline var(--hyperlink-color);
}
.hyperlink-color {
color: var(--hyperlink-color);
}
.hyperlink-underline {
text-decoration: underline var(--hyperlink-color);
}
.center {
margin: 0 auto;
}
h1 {
font-size: xx-large;
}
h2 {
font-size: x-large;
}
h3 {
font-size: large;
}
button {
cursor: pointer;
}
button.normal, a.normal {
@apply px-2 py-1 leading-tight rounded-lg;
border-width: 1px;
border-color: var(--dull-primary-color);
}
button.large, a.large {
@apply px-4 py-3 leading-tight rounded-xl;
border-width: 1px;
border-color: var(--dull-primary-color);
}
button.wide {
@apply px-4 py-2 leading-tight rounded w-full;
}
button.text {
color: var(--dull-primary-color);
}
button.filled, a.filled {
background-color: var(--dull-primary-color);
}
button.danger {
background-color: var(--danger-color);
border-width: 0;
}
button.outlined-danger {
border-color: var(--danger-color);
}
button.text-danger {
color: var(--danger-color);
border-width: 0;
}
.switch {
display: flex;
border: 1px solid #aaa;
border-radius: 4px;
overflow: hidden;
}
.switch button {
flex: 1;
padding: 0.5rem 1rem;
border: none;
background: #f0f0f0;
cursor: pointer;
}
.switch button.selected {
background: #007acc;
color: white;
}
.modal {
display: none;
position: fixed;
z-index: 1;
left: 0;
top: 0;
width: 100%;
height: 100%;
overflow: auto;
background-color: rgb(0, 0, 0); /* Fallback color */
background-color: rgba(0, 0, 0, 0.6); /* Darken background */
}
.modal-content {
background-color: var(--bg-color);
margin: 10% auto;
padding: 16px;
border: 1px solid var(--separator-line-color);
max-width: 420px;
border-radius: 8px;
}
tr:nth-child(even) {
background-color: rgb(from var(--primary-color) r g b / 10%);
}

View File

@ -1,11 +1,14 @@
<!--suppress HtmlUnknownTarget -->
<!doctype html>
<html lang="en">
<head>
<head>
<meta charset="utf-8" />
<title>FBLA26</title>
<link href="%sveltekit.assets%/favicon.png" rel="icon" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover">
<div style="display: contents">%sveltekit.body%</div>
</body>
</head>
<body data-sveltekit-preload-data="hover">
<div style="display: contents">%sveltekit.body%</div>
</body>
</html>

View File

@ -0,0 +1,80 @@
import type { User } from '$lib/types';
import bcrypt from 'bcrypt';
import sql from '$lib/db/db.server';
import type { Cookies } from '@sveltejs/kit';
import jwt from 'jsonwebtoken';
export function setJWT(cookies: Cookies, user: User) {
const payload = {
id: user.id,
email: user.email,
name: user.name
};
if (process.env.JWT_SECRET === undefined) {
throw new Error('JWT_SECRET not defined');
}
if (process.env.BASE_URL === undefined) {
throw new Error('BASE_URL not defined');
}
const secure: boolean = process.env.BASE_URL?.includes('https://');
const maxAge = 60 * 60 * 24 * 30; // 30 days
const JWT = jwt.sign(payload, process.env.JWT_SECRET, { expiresIn: '30d' });
cookies.set('jwt', JWT, { maxAge, path: '/', httpOnly: false, secure });
}
// export function checkPerms(cookies: Cookies, perms: number): void {
// const JWT = cookies.get('jwt');
// if (!JWT) throw error(403, 'Unauthorized');
// if (process.env.JWT_SECRET === undefined) {
// throw new Error('JWT_SECRET not defined');
// }
// const user = jwt.verify(JWT, process.env.JWT_SECRET) as User;
// if ((user.perms & perms) !== perms) {
// throw error(403, 'Unauthorized');
// }
// }
//
// export function hasPerms(cookies: Cookies, perms: number): boolean {
// const JWT = cookies.get('jwt');
// if (!JWT) return false;
// if (process.env.JWT_SECRET === undefined) {
// throw new Error('JWT_SECRET not defined');
// }
// const user = jwt.verify(JWT, process.env.JWT_SECRET) as User;
// return (user.perms & perms) === perms;
// }
export async function login(email: string, password: string): Promise<User> {
try {
const [user] = await sql`
SELECT id, email, password_hash, perms, name
WHERE email = ${email};
`;
if (await bcrypt.compare(password, user.password_hash)) {
delete user.password_hash;
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
sql`
UPDATE users
SET last_signin = NOW()
WHERE id = ${user.id};
`;
return <User>user;
}
} catch {
throw Error('Error signing in ');
}
throw Error('Invalid email or password');
}
// await createUser(<User>{
// email: 'drake@marinodev.com',
// password: 'password',
// perms: 255,
// name: 'Drake'
// });

9
src/lib/consts.ts Normal file
View File

@ -0,0 +1,9 @@
export const PERMISSIONS = {
VIEW: 0b00000001,
DO_TASKS: 0b00000010,
MANAGE_USERS: 0b00000100,
MANAGE_ITEMS: 0b00001000,
MANAGE_TASKS: 0b00010000
};
export const EXPIRE_REMINDER_DAYS = 30;

14
src/lib/db/db.server.ts Normal file
View File

@ -0,0 +1,14 @@
import postgres from 'postgres';
import * as dotenv from 'dotenv';
dotenv.config({ path: '.env' });
const sql = postgres({
host: process.env.POSTGRES_HOST,
port: parseInt(process.env.POSTGRES_PORT!),
database: process.env.POSTGRES_DB,
username: process.env.POSTGRES_USER,
password: process.env.POSTGRES_PASSWORD
});
export default sql;

View File

@ -0,0 +1,67 @@
import type { User } from '$lib/types';
import sql from '$lib/db/db.server';
import bcrypt from 'bcrypt';
// should require MANAGE_USERS permission
export async function getUsers(searchQuery: string | null = null): Promise<User[]> {
return sql`
SELECT id,
email,
perms,
name,
created_at AT TIME ZONE 'UTC' AS "createdAt",
last_signin AT TIME ZONE 'UTC' AS "lastSignIn"
FROM users
WHERE (${!searchQuery ? sql`TRUE` : sql`email ILIKE ${'%' + searchQuery + '%'} OR name ILIKE ${'%' + searchQuery + '%'}`});
`;
}
export async function getUser(id: number): Promise<User> {
return <User>(
await sql`
SELECT id,
email,
perms,
name,
created_at AT TIME ZONE 'UTC' AS "createdAt",
last_signin AT TIME ZONE 'UTC' AS "lastSignIn"
FROM users
WHERE (id = ${id}) LIMIT 1;
`
)[0];
}
export async function createUser(user: User) {
const passwordHash = await bcrypt.hash(user.password!, 12);
await sql`
INSERT INTO users (email, password_hash, perms, name, created_at, last_signin)
VALUES (${user.email}, ${passwordHash}, ${user.perms}, ${user.name}, NOW(), NOW())
RETURNING id;
`;
}
export async function editUser(user: User) {
let passwordHash: string | undefined;
if (user.password) {
passwordHash = await bcrypt.hash(user.password, 12);
}
await sql`
UPDATE users
SET
email = ${user.email},
${passwordHash ? sql`password_hash = ${passwordHash},` : sql``}
perms = ${user.perms},
name = ${user.name}
WHERE id = ${user.id!}
RETURNING id;
`;
}
export async function deleteUser(id: number) {
await sql`
DELETE FROM users
WHERE id = ${id}
`;
}

21
src/lib/types.ts Normal file
View File

@ -0,0 +1,21 @@
export interface User {
id: number;
email: string;
phone: string;
password?: string;
name: string;
createdAt: Date;
lastSignIn: Date;
}
export interface Item {
id: number;
ownerEmail: string;
ownerPhone: string;
foundDate: Date;
approvedDate?: Date;
claimedDate?: Date;
title: string;
description: string;
transferred: boolean; // to L&F location
}

41
src/lib/utils.ts Normal file
View File

@ -0,0 +1,41 @@
import type { User } from '$lib/types';
export const getCookieValue = (name: string): string =>
document.cookie.match('(^|;)\\s*' + name + '\\s*=\\s*([^;]+)')?.pop() || '';
export const getUserFromJWT = (jwt: string): User => JSON.parse(atob(jwt.split('.')[1]));
// export const userData = () => {
// let userData: User | null = null;
// const cookieValue = getCookieValue('jwt');
// if (cookieValue) {
// userData = getUserFromJWT(cookieValue);
// }
// return userData;
// };
export function getFormString(data: FormData, key: string): string | undefined {
const value = data.get(key);
if (value === null) {
return undefined;
}
if (typeof value !== 'string') {
throw Error(`Incorrect input in field ${key}.`);
}
return value.trim();
}
export function getRequiredFormString(data: FormData, key: string) {
const value = data.get(key);
if (typeof value !== 'string') {
throw Error(`Missing required field ${key}.`);
}
return value.trim();
}
export const dateFormatOptions: Intl.DateTimeFormatOptions = {
year: 'numeric',
month: 'short',
day: 'numeric'
};

View File

@ -0,0 +1,10 @@
import type { LayoutServerLoad } from './$types';
import { getUserFromJWT } from '$lib/utils';
export const load: LayoutServerLoad = ({ cookies }) => {
const jwt = cookies.get('jwt');
if (jwt) {
return { selfUser: getUserFromJWT(jwt) };
}
return { selfUser: null };
};

View File

@ -1,12 +1,29 @@
<script lang="ts">
import '../app.css';
import favicon from '$lib/assets/favicon.svg';
let { children } = $props();
</script>
<svelte:head>
<link rel="icon" href={favicon} />
</svelte:head>
<link
href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..40,400,0,0&display=block&icon_names=account_circle,arrow_drop_down,arrow_drop_up,calendar_today,call,check,close,cloud_upload,dark_mode,delete,description,edit,group,home,info,light_mode,login,mail,menu,open_in_new,person,search,sell,store,upload,visibility,visibility_off,work"
rel="stylesheet"
/>
{@render children?.()}
<!--<header class="bottom-border flex justify-between">-->
<!-- <a class="material-symbols-outlined p-2 !text-3xl leading-none" href="/">home</a>-->
<!-- {#if data.selfUser}-->
<!-- <a class="material-symbols-outlined p-2 !text-3xl leading-none" href="/account"-->
<!-- >account_circle</a-->
<!-- >-->
<!-- {:else}-->
<!-- <a class="material-symbols-outlined p-2 !text-3xl leading-none" href="/signin">login</a>-->
<!-- {/if}-->
<!--</header>-->
<div style="max-width: 800px;" class="center">
{@render children?.()}
</div>
<footer>
</footer>

View File

@ -1,2 +1,6 @@
<h1>Welcome to SvelteKit</h1>
<p>Visit <a href="https://svelte.dev/docs/kit">svelte.dev/docs/kit</a> to read the documentation</p>

View File

View File

BIN
static/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

50
static/mdevtriangle.svg Normal file
View File

@ -0,0 +1,50 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="80mm"
height="80mm"
viewBox="0 0 80 80"
version="1.1"
id="svg1"
inkscape:version="1.3.2 (091e20ef0f, 2023-11-25, custom)"
sodipodi:docname="triangleblue.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="mm"
inkscape:zoom="2"
inkscape:cx="81"
inkscape:cy="106.5"
inkscape:window-width="1920"
inkscape:window-height="1046"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="layer1"
shape-rendering="crispEdges"
inkscape:export-bgcolor="#ffffffff" />
<defs
id="defs1" />
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1">
<path
d="m 40.063302,5.1541583 c -1.24713,0 -2.07854,1.440059 -2.07854,1.440059 l -4.57281,7.9203207 17.81611,30.858401 c 4.37185,-0.996148 6.86321,4.803857 3.14145,7.293026 -3.72178,2.489169 -8.13084,-2.028265 -5.54068,-5.688379 v 0 L 31.749122,17.394653 0.57092766,71.396858 c -0.554314,0.96004 -0.623535,1.80008 -0.207839,2.52011 0.41568997,0.72005 1.17784894,1.08004 2.28640594,1.08004 H 11.795099 L 29.611202,44.138609 c -3.04862,-3.288058 0.72867,-8.345647 4.74522,-6.367078 4.01656,1.97857 2.30888,8.055642 -2.15596,7.642556 v 0 L 15.120758,74.997008 h 62.356384 c 0,0 1.66283,0 2.2864,-1.08004 0.62353,-1.08005 -0.20784,-2.52011 -0.20784,-2.52011 l -4.57281,-7.92032 h -35.63222 c -1.32322,4.28422 -7.59186,3.5418 -7.88666,-0.92594 -0.29484,-4.467728 5.82195,-6.027375 7.69662,-1.95418 v 0 h 34.15941 L 42.141852,6.5942173 c 0,0 -0.83138,-1.440059 -2.07855,-1.440059"
style="display:inline;fill:#2096f3;fill-opacity:1;stroke:none;stroke-width:2.37548"
id="path395"
inkscape:label="triangle"
inkscape:transform-center-y="-11.666667" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

BIN
static/sdwLogo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB