diff --git a/permissions.md b/permissions.md index 21eb8a8..cd78606 100644 --- a/permissions.md +++ b/permissions.md @@ -4,7 +4,8 @@ |------------------|------------------------| | `00000001` | View postings | | `00000010` | View account | -| `00000100` | Submit postings access | -| `00001000` | Manage postings | -| `00010000` | Manage users | -| `00100000` | Apply | \ No newline at end of file +| `00000100` | Apply for jobs | +| `00001000` | Submit postings | +| `00010000` | Manage tags | +| `00100000` | Manage postings | +| `01000000` | Manage users | diff --git a/src/app.css b/src/app.css index 178764d..cac3552 100644 --- a/src/app.css +++ b/src/app.css @@ -2,22 +2,29 @@ @import 'tailwindcss/components'; @import 'tailwindcss/utilities'; + [data-theme='light'] { --text-color: #000000; --bg-color: #f4f4f4; --hover-bg-color: #e4e4f0; - --elevated-bg-color: #e0e0e0; + --separator-line-color: #e0e0e0; --low-emphasis-text-color: #6b6b6b; --primary-color: #1F96F3; + --dull-primary-color: #51aaf0; + --elevated-bg-color: #ffffff; + --bg-accent-color: #f4f4f4; } [data-theme='dark'] { --text-color: #f4f4f4; --bg-color: #080808; --hover-bg-color: #1f2937; - --elevated-bg-color: #1a202c; + --separator-line-color: #1a2029; --low-emphasis-text-color: #999999; --primary-color: #1F96F3; + --dull-primary-color: #1569ab; + --elevated-bg-color: #0c0c0d; + --bg-accent-color: #202020; } body { @@ -31,7 +38,7 @@ h1 { } .elevated { - background-color: var(--elevated-bg-color); + background-color: var(--separator-line-color); @apply shadow-lg } @@ -52,11 +59,11 @@ h1 { } .top-border { - border-top: 1px solid var(--elevated-bg-color); + border-top: 1px solid var(--separator-line-color); } .bottom-border { - border-bottom: 1px solid var(--elevated-bg-color); + border-bottom: 1px solid var(--separator-line-color); } .icon-16 { @@ -77,18 +84,27 @@ h1 { 'opsz' 20 } -input[type='search'] { +input[type='search'], input[type='text'], input[type='password'] { background-color: var(--bg-color); color: var(--text-color); - border: 1px solid var(--elevated-bg-color); + border: 1px solid var(--separator-line-color); } -input[type='search']:focus { +input[type='search']:focus, input[type='text']:focus, input[type='password']:focus{ outline: 0 solid var(--text-color); border: 1px solid var(--primary-color); box-shadow: 0 0 0 0 var(--primary-color); } +.input-field { + border: 1px solid var(--separator-line-color); + border-radius: 4px; +} + +.arrange-vertically > * { + display: block; +} + .search-bar { flex-grow: 1; display: flex; @@ -97,7 +113,7 @@ input[type='search']:focus { .search-bar input[type='search'] { flex-grow: 1; - border: 1px solid var(--elevated-bg-color); + border: 1px solid var(--separator-line-color); border-top-left-radius: 4px; border-bottom-left-radius: 4px; outline: none; @@ -107,9 +123,9 @@ input[type='search']:focus { .search-bar button { height: 36px; background: none; - border-top: 1px solid var(--elevated-bg-color); - border-right: 1px solid var(--elevated-bg-color); - border-bottom: 1px solid var(--elevated-bg-color); + border-top: 1px solid var(--separator-line-color); + border-right: 1px solid var(--separator-line-color); + border-bottom: 1px solid var(--separator-line-color); border-top-right-radius: 4px; border-bottom-right-radius: 4px; cursor: pointer; @@ -123,15 +139,62 @@ input[type='search']:focus { } -.container { +.base-container { width: 100%; max-width: 1280px; margin: 0 auto; } +.signin-container { + width: 100%; + max-width: 420px; + margin: 0 auto; +} + .content { width: 100%; @apply px-6; } +.search-cancel::-webkit-search-cancel-button { + -webkit-appearance: none; + background-color: var(--text-color); + -webkit-mask-image: url("data:image/svg+xml;utf8,"); + background-size: 20px 20px; + height: 20px; + width: 20px; +} + +table, .table { + /*border-collapse: separate;*/ + width: 100%; + /*text-align: start;*/ +} + +tr { + border-top: 1px solid var(--separator-line-color); + text-align: left; +} + +th.left, td.left { + padding-left: 0.5rem; +} + +.borders { + border: 1px solid var(--separator-line-color); +} + +.elevated { + background-color: var(--elevated-bg-color); + @apply shadow-lg +} + +.primary-bg-color { + background-color: var(--primary-color); +} + +.dull-primary-bg-color { + background-color: var(--dull-primary-color); +} + diff --git a/src/lib/consts.ts b/src/lib/consts.ts new file mode 100644 index 0000000..2d015a2 --- /dev/null +++ b/src/lib/consts.ts @@ -0,0 +1,9 @@ +export const PERMISSIONS = { + VIEW_POSTINGS: 0b00000001, + VIEW_ACCOUNT: 0b00000010, + APPLY_FOR_JOBS: 0b00000100, + SUBMIT_POSTINGS: 0b00001000, + MANAGE_TAGS: 0b00010000, + MANAGE_POSTINGS: 0b00100000, + MANAGE_USERS: 0b01000000 +}; diff --git a/src/lib/db/index.server.ts b/src/lib/db/index.server.ts index e0c92a2..927f29c 100644 --- a/src/lib/db/index.server.ts +++ b/src/lib/db/index.server.ts @@ -1,12 +1,17 @@ import bcrypt from 'bcrypt'; import sql from '$lib/db/db.server'; +import type { Cookies } from '@sveltejs/kit'; +import jwt from 'jsonwebtoken'; export async function createUser(username: string, password: string): Promise { const password_hash: string = await bcrypt.hash(password, 12); + const timestamp = new Date(Date.now()).toISOString(); + + console.log(timestamp); const response = await sql` - INSERT INTO users (username, password_hash, perms) - VALUES (${username}, ${password_hash}, 3); + INSERT INTO users (username, password_hash, perms, created_at, last_signin, active) + VALUES (${username}, ${password_hash}, 3, ${timestamp}, ${timestamp}, ${true}); `; } @@ -25,3 +30,71 @@ export async function checkUserCreds(username: string, password: string): Promis } return -1; } + +export function getUserPerms(cookies: Cookies): number { + if (process.env.JWT_SECRET === undefined) { + throw new Error('JWT_SECRET not defined'); + } + + const JWT = cookies.get('jwt'); + if (JWT) { + try { + const decoded = jwt.verify(JWT, process.env.JWT_SECRET); + if (typeof decoded === 'object' && 'perms' in decoded) { + return decoded['perms']; + } + } catch (err) { + return -1; + } + } + return -1; +} + +// should require MANAGE_USERS permission +export async function getUsers(): Promise { + const users = await sql< + { + id: number; + username: string; + perms: number; + created_at: Date; + last_signin: Date; + active: boolean; + }[] + >` + SELECT id, username, perms, + created_at AT TIME ZONE 'UTC' AS created_at, + last_signin AT TIME ZONE 'UTC' AS last_signin, + active + FROM users; + `; + return users.map( + (user): User => ({ + id: user.id, + username: user.username, + perms: user.perms, + created_at: user.created_at, + last_signin: user.last_signin, + active: user.active + }) + ); +} + +// should require MANAGE_TAGS permission +export async function getTags(): Promise { + const tags = await sql< + { + id: number; + display_name: string; + }[] + >` + SELECT id, display_name + FROM tags; + `; + return tags.map( + (tag): Tag => ({ + id: tag.id, + display_name: tag.display_name + }) + ); +} diff --git a/src/lib/types.ts b/src/lib/types.ts new file mode 100644 index 0000000..51fa73d --- /dev/null +++ b/src/lib/types.ts @@ -0,0 +1,13 @@ +interface User { + id: number | null; + username: string; + perms: number; + created_at: Date | null; + last_signin: Date | null; + active: boolean | null; +} + +interface Tag { + id: number; + display_name: string; +} diff --git a/src/routes/+error.svelte b/src/routes/+error.svelte new file mode 100644 index 0000000..cdd6aeb --- /dev/null +++ b/src/routes/+error.svelte @@ -0,0 +1,29 @@ + + +
+

+ {$page.status} +

+

Thats an error

+ {#if $page.status === 404} +

We cant seem to find the page you are looking for.

+

The address may be mistyped, or the page may have moved or been deleted.

+ {/if} + {#if $page.status === 403} +

You dont have access to this page!

+

Please contact your admin if you think this is a mistake

+ {/if} + {#if $page.status === 401} +

You must be signed-in to view this page!

+ {/if} + {#if $page.status === 500} +

This one is on our end...

+

We are working to resolve this as fast as possible.

+ {/if} + {#if $page.status !== 404 && $page.status !== 403 && $page.status !== 401 && $page.status !== 500} +

An unexpected error has occurred.

+

Please try again later

+ {/if} +
diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index 4fc1558..8373979 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -2,7 +2,7 @@ import '../app.css'; import { onMount } from 'svelte'; import { userState } from '$lib/shared.svelte'; - // import { userState } from '$lib/shared.svelte'; + import { PERMISSIONS } from '$lib/consts'; let currentTheme: string = $state(''); @@ -42,7 +42,7 @@
@@ -57,34 +57,28 @@ /> About - {#if (userState.perms & 0b00000001) !== 0} + {#if (userState.perms & PERMISSIONS.VIEW_POSTINGS) !== 0} Listings {/if} - {#if (userState.perms & 0b00001000) !== 0} - Administration {/if}
-
diff --git a/src/routes/account/+page.svelte b/src/routes/account/+page.svelte new file mode 100644 index 0000000..53a29dc --- /dev/null +++ b/src/routes/account/+page.svelte @@ -0,0 +1,9 @@ + diff --git a/src/routes/administration/+layout.svelte b/src/routes/admin/+layout.svelte similarity index 67% rename from src/routes/administration/+layout.svelte rename to src/routes/admin/+layout.svelte index 1d0908c..d6217cd 100644 --- a/src/routes/administration/+layout.svelte +++ b/src/routes/admin/+layout.svelte @@ -7,28 +7,27 @@ -
+
{@render children()}
diff --git a/src/routes/admin/+page.svelte b/src/routes/admin/+page.svelte new file mode 100644 index 0000000..493e7f7 --- /dev/null +++ b/src/routes/admin/+page.svelte @@ -0,0 +1,7 @@ + diff --git a/src/routes/administration/postings/+page.svelte b/src/routes/admin/postings/+page.svelte similarity index 100% rename from src/routes/administration/postings/+page.svelte rename to src/routes/admin/postings/+page.svelte diff --git a/src/routes/admin/tags/+page.server.ts b/src/routes/admin/tags/+page.server.ts new file mode 100644 index 0000000..6d41b1a --- /dev/null +++ b/src/routes/admin/tags/+page.server.ts @@ -0,0 +1,14 @@ +import type { PageServerLoad } from './$types'; +import { getUserPerms, getTags } from '$lib/db/index.server'; +import { PERMISSIONS } from '$lib/consts'; +import { error } from '@sveltejs/kit'; + +export const load: PageServerLoad = async ({ cookies }) => { + const perms = getUserPerms(cookies); + if (perms >= 0 && (perms & PERMISSIONS.MANAGE_TAGS) > 0) { + return { + tags: await getTags() + }; + } + error(401, 'Unauthorized'); +}; diff --git a/src/routes/admin/tags/+page.svelte b/src/routes/admin/tags/+page.svelte new file mode 100644 index 0000000..29f29b0 --- /dev/null +++ b/src/routes/admin/tags/+page.svelte @@ -0,0 +1,69 @@ + + +
+
+
+
+ Tag Management (Total: {data.tags?.length || 0}) +
+ Create New Tag +
+
+
+ + + +
+
+
+ + + + + + + + + + {#if data.tags !== undefined} + {#each data.tags as tag} + + + + + + {/each} + {/if} + +
IDName
{tag.id}{tag.display_name} + info + edit +
+
+
+
diff --git a/src/routes/admin/users/+page.server.ts b/src/routes/admin/users/+page.server.ts new file mode 100644 index 0000000..1893f00 --- /dev/null +++ b/src/routes/admin/users/+page.server.ts @@ -0,0 +1,14 @@ +import type { PageServerLoad } from './$types'; +import { getUserPerms, getUsers } from '$lib/db/index.server'; +import { PERMISSIONS } from '$lib/consts'; +import { error } from '@sveltejs/kit'; + +export const load: PageServerLoad = async ({ cookies }) => { + const perms = getUserPerms(cookies); + if (perms >= 0 && (perms & PERMISSIONS.MANAGE_USERS) > 0) { + return { + users: await getUsers() + }; + } + error(401, 'Unauthorized'); +}; diff --git a/src/routes/admin/users/+page.svelte b/src/routes/admin/users/+page.svelte new file mode 100644 index 0000000..7a7a72b --- /dev/null +++ b/src/routes/admin/users/+page.svelte @@ -0,0 +1,92 @@ + + +
+
+
+
+ User Account Management (Total: {data.users?.length || 0}) +
+ Create New User +
+
+
+ + + +
+
+
+ + + + + + + + + + + + + + {#if data.users !== undefined} + {#each data.users as user} + + + + + + + + + + {/each} + {/if} + +
IDUsernamePermissionsCreatedLast Sign-InActive
{user.id}{user.username}{user.perms}{user.created_at?.toLocaleDateString('en-US', dateFormatOptions) || + 'unknown'}{user.last_signin?.toLocaleDateString('en-US', dateFormatOptions) || + 'unknown'} + {user.active ? 'check' : 'close'} + + person + edit +
+
+
+
diff --git a/src/routes/administration/users/+page.svelte b/src/routes/administration/users/+page.svelte deleted file mode 100644 index f569a20..0000000 --- a/src/routes/administration/users/+page.svelte +++ /dev/null @@ -1,18 +0,0 @@ -
-
-
- - - -
-
-
diff --git a/src/routes/administration/tags/+page.svelte b/src/routes/register/+page.svelte similarity index 100% rename from src/routes/administration/tags/+page.svelte rename to src/routes/register/+page.svelte diff --git a/src/routes/register/employer/+page.svelte b/src/routes/register/employer/+page.svelte new file mode 100644 index 0000000..e69de29 diff --git a/src/routes/register/user/+page.svelte b/src/routes/register/user/+page.svelte new file mode 100644 index 0000000..e69de29 diff --git a/src/routes/signin/+page.server.ts b/src/routes/signin/+page.server.ts index a2e6794..caded21 100644 --- a/src/routes/signin/+page.server.ts +++ b/src/routes/signin/+page.server.ts @@ -38,7 +38,7 @@ export const actions: Actions = { } }, - login: async ({ request, cookies }) => { + signin: async ({ request, cookies }) => { const data = await request.formData(); const username = data.get('username')?.toString(); const password = data.get('password')?.toString(); diff --git a/src/routes/signin/+page.svelte b/src/routes/signin/+page.svelte index 45d1d32..50507fc 100644 --- a/src/routes/signin/+page.svelte +++ b/src/routes/signin/+page.svelte @@ -1,22 +1,88 @@ -
-

Sign In or Register

-
- - +
+ + + + + + + + + + + + + + + +