Start of many new pages
This commit is contained in:
parent
95fb591b4b
commit
76c2680c60
@ -4,7 +4,8 @@
|
|||||||
|------------------|------------------------|
|
|------------------|------------------------|
|
||||||
| `00000001` | View postings |
|
| `00000001` | View postings |
|
||||||
| `00000010` | View account |
|
| `00000010` | View account |
|
||||||
| `00000100` | Submit postings access |
|
| `00000100` | Apply for jobs |
|
||||||
| `00001000` | Manage postings |
|
| `00001000` | Submit postings |
|
||||||
| `00010000` | Manage users |
|
| `00010000` | Manage tags |
|
||||||
| `00100000` | Apply |
|
| `00100000` | Manage postings |
|
||||||
|
| `01000000` | Manage users |
|
||||||
|
|||||||
89
src/app.css
89
src/app.css
@ -2,22 +2,29 @@
|
|||||||
@import 'tailwindcss/components';
|
@import 'tailwindcss/components';
|
||||||
@import 'tailwindcss/utilities';
|
@import 'tailwindcss/utilities';
|
||||||
|
|
||||||
|
|
||||||
[data-theme='light'] {
|
[data-theme='light'] {
|
||||||
--text-color: #000000;
|
--text-color: #000000;
|
||||||
--bg-color: #f4f4f4;
|
--bg-color: #f4f4f4;
|
||||||
--hover-bg-color: #e4e4f0;
|
--hover-bg-color: #e4e4f0;
|
||||||
--elevated-bg-color: #e0e0e0;
|
--separator-line-color: #e0e0e0;
|
||||||
--low-emphasis-text-color: #6b6b6b;
|
--low-emphasis-text-color: #6b6b6b;
|
||||||
--primary-color: #1F96F3;
|
--primary-color: #1F96F3;
|
||||||
|
--dull-primary-color: #51aaf0;
|
||||||
|
--elevated-bg-color: #ffffff;
|
||||||
|
--bg-accent-color: #f4f4f4;
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-theme='dark'] {
|
[data-theme='dark'] {
|
||||||
--text-color: #f4f4f4;
|
--text-color: #f4f4f4;
|
||||||
--bg-color: #080808;
|
--bg-color: #080808;
|
||||||
--hover-bg-color: #1f2937;
|
--hover-bg-color: #1f2937;
|
||||||
--elevated-bg-color: #1a202c;
|
--separator-line-color: #1a2029;
|
||||||
--low-emphasis-text-color: #999999;
|
--low-emphasis-text-color: #999999;
|
||||||
--primary-color: #1F96F3;
|
--primary-color: #1F96F3;
|
||||||
|
--dull-primary-color: #1569ab;
|
||||||
|
--elevated-bg-color: #0c0c0d;
|
||||||
|
--bg-accent-color: #202020;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
@ -31,7 +38,7 @@ h1 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.elevated {
|
.elevated {
|
||||||
background-color: var(--elevated-bg-color);
|
background-color: var(--separator-line-color);
|
||||||
@apply shadow-lg
|
@apply shadow-lg
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -52,11 +59,11 @@ h1 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.top-border {
|
.top-border {
|
||||||
border-top: 1px solid var(--elevated-bg-color);
|
border-top: 1px solid var(--separator-line-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.bottom-border {
|
.bottom-border {
|
||||||
border-bottom: 1px solid var(--elevated-bg-color);
|
border-bottom: 1px solid var(--separator-line-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-16 {
|
.icon-16 {
|
||||||
@ -77,18 +84,27 @@ h1 {
|
|||||||
'opsz' 20
|
'opsz' 20
|
||||||
}
|
}
|
||||||
|
|
||||||
input[type='search'] {
|
input[type='search'], input[type='text'], input[type='password'] {
|
||||||
background-color: var(--bg-color);
|
background-color: var(--bg-color);
|
||||||
color: var(--text-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);
|
outline: 0 solid var(--text-color);
|
||||||
border: 1px solid var(--primary-color);
|
border: 1px solid var(--primary-color);
|
||||||
box-shadow: 0 0 0 0 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 {
|
.search-bar {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -97,7 +113,7 @@ input[type='search']:focus {
|
|||||||
|
|
||||||
.search-bar input[type='search'] {
|
.search-bar input[type='search'] {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
border: 1px solid var(--elevated-bg-color);
|
border: 1px solid var(--separator-line-color);
|
||||||
border-top-left-radius: 4px;
|
border-top-left-radius: 4px;
|
||||||
border-bottom-left-radius: 4px;
|
border-bottom-left-radius: 4px;
|
||||||
outline: none;
|
outline: none;
|
||||||
@ -107,9 +123,9 @@ input[type='search']:focus {
|
|||||||
.search-bar button {
|
.search-bar button {
|
||||||
height: 36px;
|
height: 36px;
|
||||||
background: none;
|
background: none;
|
||||||
border-top: 1px solid var(--elevated-bg-color);
|
border-top: 1px solid var(--separator-line-color);
|
||||||
border-right: 1px solid var(--elevated-bg-color);
|
border-right: 1px solid var(--separator-line-color);
|
||||||
border-bottom: 1px solid var(--elevated-bg-color);
|
border-bottom: 1px solid var(--separator-line-color);
|
||||||
border-top-right-radius: 4px;
|
border-top-right-radius: 4px;
|
||||||
border-bottom-right-radius: 4px;
|
border-bottom-right-radius: 4px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
@ -123,15 +139,62 @@ input[type='search']:focus {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.container {
|
.base-container {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 1280px;
|
max-width: 1280px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.signin-container {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 420px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
.content {
|
.content {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
@apply px-6;
|
@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,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23777'><path d='M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z'/></svg>");
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
9
src/lib/consts.ts
Normal file
9
src/lib/consts.ts
Normal file
@ -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
|
||||||
|
};
|
||||||
@ -1,12 +1,17 @@
|
|||||||
import bcrypt from 'bcrypt';
|
import bcrypt from 'bcrypt';
|
||||||
import sql from '$lib/db/db.server';
|
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<void> {
|
export async function createUser(username: string, password: string): Promise<void> {
|
||||||
const password_hash: string = await bcrypt.hash(password, 12);
|
const password_hash: string = await bcrypt.hash(password, 12);
|
||||||
|
const timestamp = new Date(Date.now()).toISOString();
|
||||||
|
|
||||||
|
console.log(timestamp);
|
||||||
|
|
||||||
const response = await sql`
|
const response = await sql`
|
||||||
INSERT INTO users (username, password_hash, perms)
|
INSERT INTO users (username, password_hash, perms, created_at, last_signin, active)
|
||||||
VALUES (${username}, ${password_hash}, 3);
|
VALUES (${username}, ${password_hash}, 3, ${timestamp}, ${timestamp}, ${true});
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -25,3 +30,71 @@ export async function checkUserCreds(username: string, password: string): Promis
|
|||||||
}
|
}
|
||||||
return -1;
|
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<User[]> {
|
||||||
|
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<Tag[]> {
|
||||||
|
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
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
13
src/lib/types.ts
Normal file
13
src/lib/types.ts
Normal file
@ -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;
|
||||||
|
}
|
||||||
29
src/routes/+error.svelte
Normal file
29
src/routes/+error.svelte
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
<script>
|
||||||
|
import { page } from '$app/stores';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div style="padding-top: 32px" class="text-center">
|
||||||
|
<h1 class="text-9xl font-bold">
|
||||||
|
{$page.status}
|
||||||
|
</h1>
|
||||||
|
<h1>Thats an error</h1>
|
||||||
|
{#if $page.status === 404}
|
||||||
|
<p>We cant seem to find the page you are looking for.</p>
|
||||||
|
<p>The address may be mistyped, or the page may have moved or been deleted.</p>
|
||||||
|
{/if}
|
||||||
|
{#if $page.status === 403}
|
||||||
|
<p>You dont have access to this page!</p>
|
||||||
|
<p>Please contact your admin if you think this is a mistake</p>
|
||||||
|
{/if}
|
||||||
|
{#if $page.status === 401}
|
||||||
|
<p>You must be signed-in to view this page!</p>
|
||||||
|
{/if}
|
||||||
|
{#if $page.status === 500}
|
||||||
|
<p>This one is on our end...</p>
|
||||||
|
<p>We are working to resolve this as fast as possible.</p>
|
||||||
|
{/if}
|
||||||
|
{#if $page.status !== 404 && $page.status !== 403 && $page.status !== 401 && $page.status !== 500}
|
||||||
|
<p>An unexpected error has occurred.</p>
|
||||||
|
<p>Please try again later</p>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
@ -2,7 +2,7 @@
|
|||||||
import '../app.css';
|
import '../app.css';
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import { userState } from '$lib/shared.svelte';
|
import { userState } from '$lib/shared.svelte';
|
||||||
// import { userState } from '$lib/shared.svelte';
|
import { PERMISSIONS } from '$lib/consts';
|
||||||
|
|
||||||
let currentTheme: string = $state('');
|
let currentTheme: string = $state('');
|
||||||
|
|
||||||
@ -42,7 +42,7 @@
|
|||||||
|
|
||||||
<link
|
<link
|
||||||
rel="stylesheet"
|
rel="stylesheet"
|
||||||
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,dark_mode,group,light_mode,login,search,sell,work"
|
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,check,close,dark_mode,edit,group,info,light_mode,login,person,search,sell,visibility,visibility_off,work"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div class="bottom-border flex h-14 justify-between p-3 align-middle">
|
<div class="bottom-border flex h-14 justify-between p-3 align-middle">
|
||||||
@ -57,34 +57,28 @@
|
|||||||
/>
|
/>
|
||||||
</a>
|
</a>
|
||||||
<a href="/about" class="hover-bg-color mr-1 rounded px-3 py-2 text-sm">About</a>
|
<a href="/about" class="hover-bg-color mr-1 rounded px-3 py-2 text-sm">About</a>
|
||||||
{#if (userState.perms & 0b00000001) !== 0}
|
{#if (userState.perms & PERMISSIONS.VIEW_POSTINGS) !== 0}
|
||||||
<a href="/listings" class="hover-bg-color mr-1 rounded px-3 py-2 text-sm">Listings</a>
|
<a href="/listings" class="hover-bg-color mr-1 rounded px-3 py-2 text-sm">Listings</a>
|
||||||
{/if}
|
{/if}
|
||||||
{#if (userState.perms & 0b00001000) !== 0}
|
{#if (userState.perms & PERMISSIONS.MANAGE_POSTINGS) !== 0}
|
||||||
<a href="/administration/postings" class="hover-bg-color mr-1 rounded px-3 py-2 text-sm"
|
<a href="/admin/postings" class="hover-bg-color mr-1 rounded px-3 py-2 text-sm"
|
||||||
>Administration</a
|
>Administration</a
|
||||||
>
|
>
|
||||||
{/if}
|
{/if}
|
||||||
</nav>
|
</nav>
|
||||||
<div>
|
<div>
|
||||||
<button onclick={toggleTheme} class="">
|
<button onclick={toggleTheme} class="">
|
||||||
<span class="material-symbols-outlined hover-bg-color rounded-full p-1 dark:invisible">
|
<span class="material-symbols-outlined hover-bg-color rounded-full p-1">
|
||||||
{'light_mode'}
|
{currentTheme === 'light' ? 'light_mode' : 'dark_mode'}
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
<button onclick={toggleTheme} class="">
|
|
||||||
<span
|
|
||||||
class="material-symbols-outlined hover-bg-color invisible rounded-full p-1 dark:visible"
|
|
||||||
>
|
|
||||||
{'dark_mode'}
|
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onclick={() =>
|
onclick={() =>
|
||||||
(window.location.href = (userState.perms & 0b00000010) !== 0 ? '/account' : '/signin')}
|
(window.location.href =
|
||||||
|
(userState.perms & PERMISSIONS.VIEW_ACCOUNT) !== 0 ? '/account' : '/signin')}
|
||||||
>
|
>
|
||||||
<span class="material-symbols-outlined hover-bg-color rounded-full p-1">
|
<span class="material-symbols-outlined hover-bg-color rounded-full p-1">
|
||||||
{(userState.perms & 0b00000010) !== 0 ? 'account_circle' : 'login'}
|
{(userState.perms & PERMISSIONS.VIEW_ACCOUNT) !== 0 ? 'account_circle' : 'login'}
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
9
src/routes/account/+page.svelte
Normal file
9
src/routes/account/+page.svelte
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<script>
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
if (!document.cookie.includes('jwt=')) {
|
||||||
|
window.location.href = '/signin';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
@ -7,28 +7,27 @@
|
|||||||
|
|
||||||
<div class="bottom-border h-10 pt-2 text-center">
|
<div class="bottom-border h-10 pt-2 text-center">
|
||||||
<a
|
<a
|
||||||
href="/administration/postings"
|
href="/admin/postings"
|
||||||
class="p-2 {$page.url.pathname === '/administration/postings'
|
class="p-2 {$page.url.pathname === '/admin/postings'
|
||||||
? 'primary-underline font-bold'
|
? 'primary-underline font-bold'
|
||||||
: 'low-emphasis-text'}"
|
: 'low-emphasis-text'}"
|
||||||
><span class="material-symbols-outlined align-bottom">work</span> Postings</a
|
><span class="material-symbols-outlined align-bottom">work</span> Postings</a
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
href="/administration/users"
|
href="/admin/users"
|
||||||
class="p-2 {$page.url.pathname === '/administration/users'
|
class="p-2 {$page.url.pathname === '/admin/users'
|
||||||
? 'primary-underline font-bold'
|
? 'primary-underline font-bold'
|
||||||
: 'low-emphasis-text'}"
|
: 'low-emphasis-text'}"
|
||||||
><span class="material-symbols-outlined align-bottom">group</span> Users</a
|
><span class="material-symbols-outlined align-bottom">group</span> Users</a
|
||||||
>
|
><a
|
||||||
<a
|
href="/admin/tags"
|
||||||
href="/administration/tags"
|
class="{$page.url.pathname === '/admin/tags'
|
||||||
class="{$page.url.pathname === '/administration/tags'
|
|
||||||
? 'primary-underline font-bold'
|
? 'primary-underline font-bold'
|
||||||
: 'low-emphasis-text'} p-2"
|
: 'low-emphasis-text'} p-2"
|
||||||
><span class="material-symbols-outlined align-bottom">sell</span> Tags</a
|
><span class="material-symbols-outlined align-bottom">sell</span> Tags</a
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="container">
|
<div class="base-container">
|
||||||
{@render children()}
|
{@render children()}
|
||||||
</div>
|
</div>
|
||||||
7
src/routes/admin/+page.svelte
Normal file
7
src/routes/admin/+page.svelte
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<script>
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
window.location.href = '/admin/postings';
|
||||||
|
});
|
||||||
|
</script>
|
||||||
14
src/routes/admin/tags/+page.server.ts
Normal file
14
src/routes/admin/tags/+page.server.ts
Normal file
@ -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');
|
||||||
|
};
|
||||||
69
src/routes/admin/tags/+page.svelte
Normal file
69
src/routes/admin/tags/+page.svelte
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
let { data } = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="content">
|
||||||
|
<div class="elevated borders m-4 rounded">
|
||||||
|
<div class="bottom-border flex place-content-between">
|
||||||
|
<div class="p-3 font-semibold">
|
||||||
|
Tag Management (Total: {data.tags?.length || 0})
|
||||||
|
</div>
|
||||||
|
<a class="dull-primary-bg-color m-2 rounded-md px-2.5 py-1" href="/admin/tags/create"
|
||||||
|
>Create New Tag</a
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<form action="" class="px-4">
|
||||||
|
<div class="flex py-4">
|
||||||
|
<div class="search-bar">
|
||||||
|
<input
|
||||||
|
type="search"
|
||||||
|
name="searchTags"
|
||||||
|
id="searchTags"
|
||||||
|
placeholder="Search Tags"
|
||||||
|
class="search-cancel"
|
||||||
|
/>
|
||||||
|
<button><span class="material-symbols-outlined">search</span></button>
|
||||||
|
</div>
|
||||||
|
<button class="hover-bg-color mx-2 rounded py-2 pl-3 pr-2 text-sm"
|
||||||
|
>Filter<span class="material-symbols-outlined icon-20 align-middle">arrow_drop_down</span
|
||||||
|
></button
|
||||||
|
>
|
||||||
|
<button class="hover-bg-color rounded py-2 pl-3 pr-2 text-sm"
|
||||||
|
>Sort<span class="material-symbols-outlined icon-20 align-middle">arrow_drop_down</span
|
||||||
|
></button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<div class="table">
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th class="left py-1">ID</th>
|
||||||
|
<th class="py-1">Name</th>
|
||||||
|
<th class="py-1"></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{#if data.tags !== undefined}
|
||||||
|
{#each data.tags as tag}
|
||||||
|
<tr>
|
||||||
|
<td class="left">{tag.id}</td>
|
||||||
|
<td>{tag.display_name}</td>
|
||||||
|
<td class="w-28 pr-1 text-end">
|
||||||
|
<a
|
||||||
|
class="hover-bg-color material-symbols-outlined icon-20 my-1 rounded p-1"
|
||||||
|
href="/admin/tags/{tag.id}">info</a
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
class="hover-bg-color material-symbols-outlined icon-20 my-1 ml-1 mr-8 rounded p-1"
|
||||||
|
href="/admin/tags/{tag.id}/edit">edit</a
|
||||||
|
>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{/each}
|
||||||
|
{/if}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
14
src/routes/admin/users/+page.server.ts
Normal file
14
src/routes/admin/users/+page.server.ts
Normal file
@ -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');
|
||||||
|
};
|
||||||
92
src/routes/admin/users/+page.svelte
Normal file
92
src/routes/admin/users/+page.svelte
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
let { data } = $props();
|
||||||
|
|
||||||
|
const dateFormatOptions: Intl.DateTimeFormatOptions = {
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'short',
|
||||||
|
day: 'numeric'
|
||||||
|
};
|
||||||
|
// console.log(data);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="content">
|
||||||
|
<div class="elevated borders m-4 rounded">
|
||||||
|
<div class="bottom-border flex place-content-between">
|
||||||
|
<div class="p-3 font-semibold">
|
||||||
|
User Account Management (Total: {data.users?.length || 0})
|
||||||
|
</div>
|
||||||
|
<a class="dull-primary-bg-color m-2 rounded-md px-2.5 py-1" href="/admin/users/create"
|
||||||
|
>Create New User</a
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<form action="" class="px-4">
|
||||||
|
<div class="flex py-4">
|
||||||
|
<div class="search-bar">
|
||||||
|
<input
|
||||||
|
type="search"
|
||||||
|
name="searchUsers"
|
||||||
|
id="searchUsers"
|
||||||
|
placeholder="Search Users"
|
||||||
|
class="search-cancel"
|
||||||
|
/>
|
||||||
|
<button><span class="material-symbols-outlined">search</span></button>
|
||||||
|
</div>
|
||||||
|
<button class="hover-bg-color mx-2 rounded py-2 pl-3 pr-2 text-sm"
|
||||||
|
>Filter<span class="material-symbols-outlined icon-20 align-middle">arrow_drop_down</span
|
||||||
|
></button
|
||||||
|
>
|
||||||
|
<button class="hover-bg-color rounded py-2 pl-3 pr-2 text-sm"
|
||||||
|
>Sort<span class="material-symbols-outlined icon-20 align-middle">arrow_drop_down</span
|
||||||
|
></button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<div class="table">
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th class="left py-1">ID</th>
|
||||||
|
<th class="py-1">Username</th>
|
||||||
|
<th class="py-1">Permissions</th>
|
||||||
|
<th class="py-1">Created</th>
|
||||||
|
<th class="py-1">Last Sign-In</th>
|
||||||
|
<th class="py-1">Active</th>
|
||||||
|
<th class="py-1"></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{#if data.users !== undefined}
|
||||||
|
{#each data.users as user}
|
||||||
|
<tr>
|
||||||
|
<td class="left">{user.id}</td>
|
||||||
|
<td>{user.username}</td>
|
||||||
|
<td>{user.perms}</td>
|
||||||
|
<td
|
||||||
|
>{user.created_at?.toLocaleDateString('en-US', dateFormatOptions) ||
|
||||||
|
'unknown'}</td
|
||||||
|
>
|
||||||
|
<td
|
||||||
|
>{user.last_signin?.toLocaleDateString('en-US', dateFormatOptions) ||
|
||||||
|
'unknown'}</td
|
||||||
|
>
|
||||||
|
<td class="material-symbols-outlined py-2">
|
||||||
|
{user.active ? 'check' : 'close'}
|
||||||
|
</td>
|
||||||
|
<td class="w-28 pr-1 text-end">
|
||||||
|
<a
|
||||||
|
class="hover-bg-color material-symbols-outlined icon-20 my-1 rounded p-1"
|
||||||
|
href="/admin/users/{user.id}">person</a
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
class="hover-bg-color material-symbols-outlined icon-20 my-1 ml-1 mr-8 rounded p-1"
|
||||||
|
href="/admin/users/{user.id}/edit">edit</a
|
||||||
|
>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{/each}
|
||||||
|
{/if}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@ -1,18 +0,0 @@
|
|||||||
<div class="content">
|
|
||||||
<form action="">
|
|
||||||
<div class="flex py-4">
|
|
||||||
<div class="search-bar">
|
|
||||||
<input type="search" name="searchUsers" id="searchUsers" placeholder="Search Users" />
|
|
||||||
<button><span class="material-symbols-outlined">search</span></button>
|
|
||||||
</div>
|
|
||||||
<button class="hover-bg-color mx-2 mr-1 rounded px-3 py-2 text-sm"
|
|
||||||
>Filter<span class="material-symbols-outlined icon-20 align-middle">arrow_drop_down</span
|
|
||||||
></button
|
|
||||||
>
|
|
||||||
<button class="hover-bg-color mr-1 rounded px-3 py-2 text-sm"
|
|
||||||
>Sort<span class="material-symbols-outlined icon-20 align-middle">arrow_drop_down</span
|
|
||||||
></button
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
0
src/routes/register/employer/+page.svelte
Normal file
0
src/routes/register/employer/+page.svelte
Normal file
0
src/routes/register/user/+page.svelte
Normal file
0
src/routes/register/user/+page.svelte
Normal file
@ -38,7 +38,7 @@ export const actions: Actions = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
login: async ({ request, cookies }) => {
|
signin: async ({ request, cookies }) => {
|
||||||
const data = await request.formData();
|
const data = await request.formData();
|
||||||
const username = data.get('username')?.toString();
|
const username = data.get('username')?.toString();
|
||||||
const password = data.get('password')?.toString();
|
const password = data.get('password')?.toString();
|
||||||
|
|||||||
@ -1,22 +1,88 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { ActionData } from './$types';
|
import type { ActionData } from './$types';
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
if (document.cookie.includes('jwt=')) {
|
||||||
|
window.location.href = '/account';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let passwordVisible = $state(false);
|
||||||
|
|
||||||
|
function showPassword() {
|
||||||
|
const password = document.querySelector('input[name="password"]');
|
||||||
|
if (password) {
|
||||||
|
if (password.getAttribute('type') === 'password') {
|
||||||
|
password.setAttribute('type', 'text');
|
||||||
|
passwordVisible = true;
|
||||||
|
} else {
|
||||||
|
password.setAttribute('type', 'password');
|
||||||
|
passwordVisible = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
// receive form data from server
|
// receive form data from server
|
||||||
let form: ActionData;
|
let form: ActionData;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="container">
|
<div class="signin-container place-items-center pt-8">
|
||||||
<h1 class="is-size-3 has-text-weight-semibold my-4">Sign In or Register</h1>
|
<div class="elevated content rounded-md p-8">
|
||||||
<form method="POST">
|
<h1 class="is-size-3 has-text-weight-semibold my-4">Welcome Back!</h1>
|
||||||
<input class="input my-2" type="text" placeholder="Username" name="username" required />
|
<form method="POST" class="arrange-vertically">
|
||||||
<input class="input my-2" type="password" placeholder="Password" name="password" required />
|
<input
|
||||||
|
class="input-field my-2 w-full"
|
||||||
|
type="text"
|
||||||
|
placeholder="Username"
|
||||||
|
name="username"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<div class="relative w-full">
|
||||||
|
<input
|
||||||
|
type={passwordVisible ? 'text' : 'password'}
|
||||||
|
class="input-field my-2 w-full pr-10"
|
||||||
|
placeholder="Password"
|
||||||
|
name="password"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onclick={showPassword}
|
||||||
|
class="absolute right-2.5 top-8 -translate-y-1/2 transform"
|
||||||
|
>
|
||||||
|
<span class="material-symbols-outlined"
|
||||||
|
>{passwordVisible ? 'visibility' : 'visibility_off'}</span
|
||||||
|
>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- display error message -->
|
<!-- TODO: fix -->
|
||||||
{#if form?.errorMessage}
|
{#if form?.errorMessage}
|
||||||
<div class="has-text-danger my-2">{form.errorMessage}</div>
|
<div class="has-text-danger my-2">{form.errorMessage}</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<button class="button mr-3 mt-4" type="submit" formaction="?/register">Register</button>
|
<button
|
||||||
<button class="button is-primary mt-4" type="submit" formaction="?/login">Sign In</button>
|
class="primary-bg-color mt-6 w-full rounded px-2 py-2"
|
||||||
|
type="submit"
|
||||||
|
formaction="?/signin">Sign In</button
|
||||||
|
>
|
||||||
|
<a href="/register" class="low-emphasis-text mt-2">Don't have an account? Register here.</a>
|
||||||
</form>
|
</form>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!--<div class="container">-->
|
||||||
|
<!-- <h1 class="is-size-3 has-text-weight-semibold my-4">Sign In or Register</h1>-->
|
||||||
|
<!-- <form method="POST">-->
|
||||||
|
<!-- <input class="input my-2" type="text" placeholder="Username" name="username" required />-->
|
||||||
|
<!-- <input class="input my-2" type="password" placeholder="Password" name="password" required />-->
|
||||||
|
|
||||||
|
<!-- <!– display error message –>-->
|
||||||
|
<!-- {#if form?.errorMessage}-->
|
||||||
|
<!-- <div class="has-text-danger my-2">{form.errorMessage}</div>-->
|
||||||
|
<!-- {/if}-->
|
||||||
|
|
||||||
|
<!-- <button class="button mr-3 mt-4" type="submit" formaction="?/register">Register</button>-->
|
||||||
|
<!-- <button class="button is-primary mt-4" type="submit" formaction="?/login">Sign In</button>-->
|
||||||
|
<!-- </form>-->
|
||||||
|
<!--</div>-->
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user