diff --git a/package-lock.json b/package-lock.json index 0ef2ae2..aafa63a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 06b85a0..4a714f4 100644 --- a/package.json +++ b/package.json @@ -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" } } diff --git a/src/app.css b/src/app.css index ffb96a1..a3e426a 100644 --- a/src/app.css +++ b/src/app.css @@ -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%); +} diff --git a/src/app.html b/src/app.html index f273cc5..4f4f16e 100644 --- a/src/app.html +++ b/src/app.html @@ -1,11 +1,14 @@ + - - - - %sveltekit.head% - - -
%sveltekit.body%
- + + + FBLA26 + + + %sveltekit.head% + + +
%sveltekit.body%
+ diff --git a/src/lib/auth/index.server.ts b/src/lib/auth/index.server.ts new file mode 100644 index 0000000..7ae5278 --- /dev/null +++ b/src/lib/auth/index.server.ts @@ -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 { + 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; + } + } catch { + throw Error('Error signing in '); + } + throw Error('Invalid email or password'); +} + +// await createUser({ +// email: 'drake@marinodev.com', +// password: 'password', +// perms: 255, +// name: 'Drake' +// }); diff --git a/src/lib/consts.ts b/src/lib/consts.ts new file mode 100644 index 0000000..c29f166 --- /dev/null +++ b/src/lib/consts.ts @@ -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; diff --git a/src/lib/db/db.server.ts b/src/lib/db/db.server.ts new file mode 100644 index 0000000..1113bc1 --- /dev/null +++ b/src/lib/db/db.server.ts @@ -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; diff --git a/src/lib/db/users.server.ts b/src/lib/db/users.server.ts new file mode 100644 index 0000000..327e483 --- /dev/null +++ b/src/lib/db/users.server.ts @@ -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 { + 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 { + return ( + 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} + `; +} diff --git a/src/lib/types.ts b/src/lib/types.ts new file mode 100644 index 0000000..84c1af1 --- /dev/null +++ b/src/lib/types.ts @@ -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 +} diff --git a/src/lib/utils.ts b/src/lib/utils.ts new file mode 100644 index 0000000..3fbc28a --- /dev/null +++ b/src/lib/utils.ts @@ -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' +}; diff --git a/src/routes/+layout.server.ts b/src/routes/+layout.server.ts new file mode 100644 index 0000000..2317225 --- /dev/null +++ b/src/routes/+layout.server.ts @@ -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 }; +}; diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index 8c56a3c..b87eae6 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -1,12 +1,29 @@ - - - + -{@render children?.()} + + + + + + + + + + + +
+ {@render children?.()} +
+ +
+ +
diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index cc88df0..8790510 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -1,2 +1,6 @@

Welcome to SvelteKit

Visit svelte.dev/docs/kit to read the documentation

+ + + + diff --git a/src/routes/signin/+page.server.ts b/src/routes/signin/+page.server.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/routes/signin/+page.svelte b/src/routes/signin/+page.svelte new file mode 100644 index 0000000..e69de29 diff --git a/static/favicon.png b/static/favicon.png new file mode 100644 index 0000000..04d3450 Binary files /dev/null and b/static/favicon.png differ diff --git a/static/mdevtriangle.svg b/static/mdevtriangle.svg new file mode 100644 index 0000000..418bb02 --- /dev/null +++ b/static/mdevtriangle.svg @@ -0,0 +1,50 @@ + + + + + + + + + + diff --git a/static/sdwLogo.png b/static/sdwLogo.png new file mode 100644 index 0000000..13947c6 Binary files /dev/null and b/static/sdwLogo.png differ