diff --git a/package-lock.json b/package-lock.json index 4e53b95..bd9ed9b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,10 @@ "@tailwindcss/forms": "^0.5.9", "autoprefixer": "^10.4.20", "bcrypt": "^5.1.1", + "dotenv": "^16.4.7", "js-cookie": "^3.0.5", + "jsonwebtoken": "^9.0.2", + "postgres": "^3.4.5", "vitest": "^2.0.4" }, "devDependencies": { @@ -21,6 +24,7 @@ "@sveltejs/kit": "^2.9.0", "@sveltejs/vite-plugin-svelte": "^5.0.0", "@types/bcrypt": "^5.0.2", + "@types/jsonwebtoken": "^9.0.7", "eslint": "^9.7.0", "eslint-config-prettier": "^9.1.0", "eslint-plugin-svelte": "^2.36.0", @@ -1304,6 +1308,16 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/jsonwebtoken": { + "version": "9.0.7", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.7.tgz", + "integrity": "sha512-ugo316mmTYBl2g81zDFnZ7cfxlut3o+/EQdaP7J8QN2kY6lJ22hmQYCK5EHcJHbrW+dkCGSCPgbG8JtYj6qSrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/node": { "version": "22.10.5", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.5.tgz", @@ -1931,6 +1945,12 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "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", @@ -2210,12 +2230,33 @@ "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", "license": "MIT" }, + "node_modules/dotenv": { + "version": "16.4.7", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz", + "integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", "license": "MIT" }, + "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/electron-to-chromium": { "version": "1.5.71", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.71.tgz", @@ -3179,6 +3220,49 @@ "dev": true, "license": "MIT" }, + "node_modules/jsonwebtoken": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "license": "MIT", + "dependencies": { + "jws": "^3.2.2", + "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": "1.4.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", + "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "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": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "license": "MIT", + "dependencies": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -3257,6 +3341,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", @@ -3264,6 +3384,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.1.2", "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.2.tgz", @@ -3937,6 +4063,19 @@ "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", "license": "MIT" }, + "node_modules/postgres": { + "version": "3.4.5", + "resolved": "https://registry.npmjs.org/postgres/-/postgres-3.4.5.tgz", + "integrity": "sha512-cDWgoah1Gez9rN3H4165peY9qfpEo+SA61oQv65O3cRUE1pOEoJWwddwcqKE8XZYjbblOJlYDlLV4h67HrEVDg==", + "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", diff --git a/package.json b/package.json index 127adfc..9ff4d42 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "@sveltejs/kit": "^2.9.0", "@sveltejs/vite-plugin-svelte": "^5.0.0", "@types/bcrypt": "^5.0.2", + "@types/jsonwebtoken": "^9.0.7", "eslint": "^9.7.0", "eslint-config-prettier": "^9.1.0", "eslint-plugin-svelte": "^2.36.0", @@ -38,7 +39,10 @@ "@tailwindcss/forms": "^0.5.9", "autoprefixer": "^10.4.20", "bcrypt": "^5.1.1", + "dotenv": "^16.4.7", "js-cookie": "^3.0.5", + "jsonwebtoken": "^9.0.2", + "postgres": "^3.4.5", "vitest": "^2.0.4" } } diff --git a/permissions.md b/permissions.md new file mode 100644 index 0000000..ab18e4d --- /dev/null +++ b/permissions.md @@ -0,0 +1,9 @@ +## Table of Permissions + +| Permission Value | Description | +|------------------|------------------------| +| `00000001` | Read access | +| `00000010` | Submit postings access | +| `00000100` | Manage postings | +| `00001000` | Manage users | +| `00010000` | Apply | \ No newline at end of file diff --git a/src/app.css b/src/app.css index e0805c8..8786d08 100644 --- a/src/app.css +++ b/src/app.css @@ -44,4 +44,3 @@ h1 { background-color: var(--hover-bg-color); } - diff --git a/src/hooks.client.ts b/src/hooks.client.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/hooks.server.ts b/src/hooks.server.ts index 5043762..a73fb59 100644 --- a/src/hooks.server.ts +++ b/src/hooks.server.ts @@ -1,5 +1,24 @@ +import jwt from 'jsonwebtoken'; +import * as dotenv from 'dotenv'; + +dotenv.config({ path: '.env' }); + export const handle = async ({ event, resolve }) => { const theme = event.cookies.get('theme'); + const JWT = event.cookies.get('jwt'); + + if (process.env.JWT_SECRET === undefined) { + throw new Error('JWT_SECRET not defined'); + } + + if (JWT) { + try { + const decoded = jwt.verify(JWT, process.env.JWT_SECRET); + } catch (err) { + event.cookies.delete('jwt', { path: '/' }); + } + } + if (!theme) { return await resolve(event); } 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/index.server.ts b/src/lib/db/index.server.ts new file mode 100644 index 0000000..d5a6fa1 --- /dev/null +++ b/src/lib/db/index.server.ts @@ -0,0 +1,28 @@ +import bcrypt from 'bcrypt'; +import sql from '$lib/db/db.server'; + +export async function createUser(username: string, password: string): Promise { + const password_hash: string = await bcrypt.hash(password, 12); + + const response = await sql` + INSERT INTO users (username, password_hash, perms) + VALUES (${username}, ${password_hash}, 2); + `; + console.log(response); +} + +export async function checkUserCreds(username: string, password: string): Promise { + const [user] = await sql` + SELECT password_hash, perms + FROM users + WHERE username = ${username} + `; + + if (!user) { + return -1; + } + if (await bcrypt.compare(password, user.password_hash)) { + return user['perms']; + } + return -1; +} diff --git a/src/lib/db/index.ts b/src/lib/db/index.ts deleted file mode 100644 index d38801f..0000000 --- a/src/lib/db/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -import bcrypt from 'bcrypt'; - -export async function createUser(username: string, password: string): Promise { - const sql = `INSERT INTO users (username, password, perms) - VALUES ($username, $password, 0)`; - - const hash = await bcrypt.hash(password, 12); -} diff --git a/src/lib/shared.svelte.ts b/src/lib/shared.svelte.ts new file mode 100644 index 0000000..4d8045d --- /dev/null +++ b/src/lib/shared.svelte.ts @@ -0,0 +1 @@ +export let userState = $state({ permissions: 0b00000001 }); diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index 9c40de1..7643b7c 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -1,34 +1,46 @@ @@ -54,12 +66,17 @@
- +
diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index f5b87c7..0fbba99 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -1,6 +1,2 @@ -

Hello world

- -
-

Test Text

-

hello

-
+ diff --git a/src/routes/signin/+page.server.ts b/src/routes/signin/+page.server.ts index e69de29..9cbbebc 100644 --- a/src/routes/signin/+page.server.ts +++ b/src/routes/signin/+page.server.ts @@ -0,0 +1,61 @@ +import { checkUserCreds, createUser } from '$lib/db/index.server'; +import { fail, redirect, type Actions, type Cookies } from '@sveltejs/kit'; +import jwt from 'jsonwebtoken'; +import * as dotenv from 'dotenv'; + +dotenv.config({ path: '.env' }); + +function setJWT(cookies: Cookies, username: string, perms: number) { + const payload = { + username: username, + perms: perms + }; + + if (process.env.JWT_SECRET === undefined) { + throw new Error('JWT_SECRET not defined'); + } + + const maxAge = 60 * 60 * 24 * 30; // 30 days + const JWT = jwt.sign(payload, process.env.JWT_SECRET, { expiresIn: '30m' }); + cookies.set('jwt', JWT, { maxAge, path: '/' }); +} + +export const actions: Actions = { + register: async ({ request, cookies }) => { + const data = await request.formData(); + const username = data.get('username')?.toString(); + const password = data.get('password')?.toString(); + + if (username && password) { + try { + await createUser(username, password); + } catch (err) { + return fail(400, { errorMessage: `Internal Server Error: ${err}` }); + } + } else { + return fail(400, { errorMessage: 'Missing username or password' }); + } + }, + + login: async ({ request, cookies }) => { + const data = await request.formData(); + const username = data.get('username')?.toString(); + const password = data.get('password')?.toString(); + + if (username && password) { + const perms = await checkUserCreds(username, password); + + if (perms === -1) { + return fail(401, { errorMessage: 'Invalid username or password' }); + } + + setJWT(cookies, username, perms); + + // redirect to home page + // return { perms: perms }; + throw redirect(303, '/'); + } else { + return fail(400, { errorMessage: 'Missing username or password' }); + } + } +}; diff --git a/src/routes/signin/+page.svelte b/src/routes/signin/+page.svelte index e69de29..45d1d32 100644 --- a/src/routes/signin/+page.svelte +++ b/src/routes/signin/+page.svelte @@ -0,0 +1,22 @@ + + +
+

Sign In or Register

+
+ + + + + {#if form?.errorMessage} +
{form.errorMessage}
+ {/if} + + + +
+
diff --git a/vite.config.ts b/vite.config.ts index d76fc8a..c124795 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -2,8 +2,13 @@ import { defineConfig } from 'vitest/config'; import { sveltekit } from '@sveltejs/kit/vite'; export default defineConfig({ + // @ts-ignore plugins: [sveltekit()], + server: { + host: true + }, + test: { include: ['src/**/*.{test,spec}.{js,ts}'] }