dev
This commit is contained in:
parent
76c2680c60
commit
be83b7570d
3
.gitignore
vendored
3
.gitignore
vendored
@ -26,3 +26,6 @@ vite.config.ts.timestamp-*
|
||||
|
||||
# Postgres
|
||||
postgresql
|
||||
|
||||
# User uploads
|
||||
uploads
|
||||
|
||||
37
README.md
37
README.md
@ -1,38 +1,7 @@
|
||||
# sv
|
||||
## FBLA25
|
||||
|
||||
Everything you need to build a Svelte project, powered by [`sv`](https://github.com/sveltejs/cli).
|
||||
|
||||
## Creating a project
|
||||
|
||||
If you're seeing this, you've probably already done this step. Congrats!
|
||||
## Future Features
|
||||
|
||||
```bash
|
||||
# create a new project in the current directory
|
||||
npx sv create
|
||||
|
||||
# create a new project in my-app
|
||||
npx sv create my-app
|
||||
```
|
||||
|
||||
## Developing
|
||||
|
||||
Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
|
||||
# or start the server and open the app in a new browser tab
|
||||
npm run dev -- --open
|
||||
```
|
||||
|
||||
## Building
|
||||
|
||||
To create a production version of your app:
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
You can preview the production build with `npm run preview`.
|
||||
|
||||
> To deploy your app, you may need to install an [adapter](https://svelte.dev/docs/kit/adapters) for your target environment.
|
||||
- [ ] Separation of companies and employers
|
||||
1492
package-lock.json
generated
1492
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -16,10 +16,11 @@
|
||||
"devDependencies": {
|
||||
"@eslint/compat": "^1.2.3",
|
||||
"@sveltejs/adapter-auto": "^3.0.0",
|
||||
"@sveltejs/kit": "^2.9.0",
|
||||
"@sveltejs/kit": "^2.16.1",
|
||||
"@sveltejs/vite-plugin-svelte": "^5.0.0",
|
||||
"@types/bcrypt": "^5.0.2",
|
||||
"@types/jsonwebtoken": "^9.0.7",
|
||||
"@types/node-fetch": "^2.6.12",
|
||||
"eslint": "^9.7.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-svelte": "^2.36.0",
|
||||
|
||||
@ -1,11 +1,14 @@
|
||||
## Table of Permissions
|
||||
|
||||
| Permission Value | Description |
|
||||
|------------------|------------------------|
|
||||
| `00000001` | View postings |
|
||||
| `00000010` | View account |
|
||||
| `00000100` | Apply for jobs |
|
||||
| `00001000` | Submit postings |
|
||||
| `00010000` | Manage tags |
|
||||
| `00100000` | Manage postings |
|
||||
| `01000000` | Manage users |
|
||||
| Permission Value | Description |
|
||||
|---------------------|------------------|
|
||||
| `0b00000001`(bit 0) | View access |
|
||||
| `0b00000010`(bit 1) | Apply for jobs |
|
||||
| `0b00000100`(bit 2) | Submit postings |
|
||||
| `0b00001000`(bit 3) | Manage employers |
|
||||
| `0b00010000`(bit 4) | Manage tags |
|
||||
| `0b00100000`(bit 5) | Manage postings |
|
||||
| `0b01000000`(bit 6) | Manage users |
|
||||
| `0b10000000`(bit 7) | Manage companies |
|
||||
|
||||
**Note**: Users that are not logged in are assigned a permission value of `0b00000001` (View access only).
|
||||
169
src/app.css
169
src/app.css
@ -13,18 +13,20 @@
|
||||
--dull-primary-color: #51aaf0;
|
||||
--elevated-bg-color: #ffffff;
|
||||
--bg-accent-color: #f4f4f4;
|
||||
--danger-color: #ff2d2f;
|
||||
}
|
||||
|
||||
[data-theme='dark'] {
|
||||
--text-color: #f4f4f4;
|
||||
--bg-color: #080808;
|
||||
--hover-bg-color: #1f2937;
|
||||
--bg-color: #0c0c0c;
|
||||
--hover-bg-color: #1a1c1c;
|
||||
--separator-line-color: #1a2029;
|
||||
--low-emphasis-text-color: #999999;
|
||||
--primary-color: #1F96F3;
|
||||
--dull-primary-color: #1569ab;
|
||||
--elevated-bg-color: #0c0c0d;
|
||||
--elevated-bg-color: #101011;
|
||||
--bg-accent-color: #202020;
|
||||
--danger-color: #ff1d1f;
|
||||
}
|
||||
|
||||
body {
|
||||
@ -37,9 +39,8 @@ h1 {
|
||||
@apply text-4xl
|
||||
}
|
||||
|
||||
.elevated {
|
||||
background-color: var(--separator-line-color);
|
||||
@apply shadow-lg
|
||||
.bg-color {
|
||||
background-color: var(--bg-color);
|
||||
}
|
||||
|
||||
.hover-bg-color:hover {
|
||||
@ -50,7 +51,11 @@ h1 {
|
||||
color: var(--low-emphasis-text-color);
|
||||
}
|
||||
|
||||
.low-emphasis-text:hover {
|
||||
.low-emphasis-text-button {
|
||||
color: var(--low-emphasis-text-color);
|
||||
}
|
||||
|
||||
.low-emphasis-text-button:hover {
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
@ -84,18 +89,23 @@ h1 {
|
||||
'opsz' 20
|
||||
}
|
||||
|
||||
input[type='search'], input[type='text'], input[type='password'] {
|
||||
input[type='search'], input[type='text'], input[type='password'], input[type='email'], input[type='tel'] {
|
||||
background-color: var(--bg-color);
|
||||
color: var(--text-color);
|
||||
border: 1px solid var(--separator-line-color);
|
||||
}
|
||||
|
||||
input[type='search']:focus, input[type='text']:focus, input[type='password']:focus{
|
||||
input[type='search']:focus, input[type='text']:focus, input[type='password']:focus, input[type='email']:focus, input[type='tel']:focus {
|
||||
outline: 0 solid var(--text-color);
|
||||
border: 1px solid var(--primary-color);
|
||||
box-shadow: 0 0 0 0 var(--primary-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 {
|
||||
-webkit-text-fill-color: var(--text-color) !important;
|
||||
background-clip: text !important;
|
||||
}
|
||||
|
||||
.input-field {
|
||||
border: 1px solid var(--separator-line-color);
|
||||
border-radius: 4px;
|
||||
@ -181,7 +191,7 @@ th.left, td.left {
|
||||
padding-left: 0.5rem;
|
||||
}
|
||||
|
||||
.borders {
|
||||
.separator-borders {
|
||||
border: 1px solid var(--separator-line-color);
|
||||
}
|
||||
|
||||
@ -198,3 +208,142 @@ th.left, td.left {
|
||||
background-color: var(--dull-primary-color);
|
||||
}
|
||||
|
||||
.dull-primary-text-color {
|
||||
color: var(--dull-primary-color);
|
||||
}
|
||||
|
||||
.primary-text-color {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.accordion {
|
||||
cursor: pointer;
|
||||
width: 100%;
|
||||
transition: 0.2s;
|
||||
}
|
||||
|
||||
.accordion:hover, .active {
|
||||
background-color: var(--hover-bg-color);
|
||||
}
|
||||
|
||||
input[type='checkbox'] {
|
||||
background-color: var(--bg-color);
|
||||
border: 1px solid var(--separator-line-color);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
input[type='checkbox']:focus {
|
||||
box-shadow: none;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.danger-bg-color {
|
||||
background-color: var(--danger-color);
|
||||
}
|
||||
|
||||
.danger-border-color {
|
||||
border-color: var(--danger-color);
|
||||
}
|
||||
|
||||
/* The Modal (background) */
|
||||
.modal {
|
||||
display: none; /* Hidden by default */
|
||||
position: fixed; /* Stay in place */
|
||||
z-index: 1; /* Sit on top */
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%; /* Full width */
|
||||
height: 100%; /* Full height */
|
||||
overflow: auto; /* Enable scroll if needed */
|
||||
background-color: rgb(0,0,0); /* Fallback color */
|
||||
background-color: rgba(0,0,0,0.6); /* Black w/ opacity */
|
||||
}
|
||||
|
||||
/* Modal Content/Box */
|
||||
.modal-content {
|
||||
background-color: var(--bg-color);
|
||||
margin: 10% auto; /* 10% from the top and centered */
|
||||
padding: 16px;
|
||||
border: 1px solid var(--separator-line-color);
|
||||
max-width: 420px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* The Close Button */
|
||||
.close {
|
||||
color: #aaa;
|
||||
float: right;
|
||||
font-size: 28px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.close:hover,
|
||||
.close:focus {
|
||||
color: black;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
h2 {
|
||||
color: var(--text-color);
|
||||
@apply text-2xl
|
||||
}
|
||||
|
||||
.tooltip {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
/* Tooltip text */
|
||||
.tooltip .tooltip-text {
|
||||
visibility: hidden;
|
||||
background-color: var(--bg-accent-color);
|
||||
color: var(--text-color);
|
||||
text-align: center;
|
||||
padding: 4px;
|
||||
border-radius: 6px;
|
||||
|
||||
/* Position the tooltip text - see examples below! */
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
width: 100px;
|
||||
bottom: 120%;
|
||||
left: 50%;
|
||||
margin-left: -50px; /* Use half of the width (120/2 = 60), to center the tooltip */
|
||||
|
||||
opacity: 0;
|
||||
transition: opacity 0s linear 0.5s;
|
||||
}
|
||||
|
||||
.tooltip .tooltip-text::after {
|
||||
content: " ";
|
||||
position: absolute;
|
||||
top: 100%; /* At the bottom of the tooltip */
|
||||
left: 50%;
|
||||
margin-left: -5px;
|
||||
border-width: 5px;
|
||||
border-style: solid;
|
||||
border-color: var(--bg-accent-color) transparent transparent transparent;
|
||||
}
|
||||
|
||||
/* Show the tooltip text when you mouse over the tooltip container */
|
||||
.tooltip:hover .tooltip-text {
|
||||
visibility: visible;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.tooltip .tooltip-text:hover {
|
||||
visibility: hidden;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.hover-hyperlink:hover {
|
||||
color: var(--dull-primary-color);
|
||||
text-decoration: underline var(--dull-primary-color);;
|
||||
}
|
||||
|
||||
.max-char-length {
|
||||
max-width: 200ch;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
@ -1,9 +1,10 @@
|
||||
export const PERMISSIONS = {
|
||||
VIEW_POSTINGS: 0b00000001,
|
||||
VIEW_ACCOUNT: 0b00000010,
|
||||
APPLY_FOR_JOBS: 0b00000100,
|
||||
SUBMIT_POSTINGS: 0b00001000,
|
||||
VIEW: 0b00000001,
|
||||
APPLY_FOR_JOBS: 0b0000010,
|
||||
SUBMIT_POSTINGS: 0b0000100,
|
||||
MANAGE_EMPLOYERS: 0b00001000,
|
||||
MANAGE_TAGS: 0b00010000,
|
||||
MANAGE_POSTINGS: 0b00100000,
|
||||
MANAGE_USERS: 0b01000000
|
||||
MANAGE_USERS: 0b01000000,
|
||||
MANAGE_COMPANIES: 0b10000000
|
||||
};
|
||||
|
||||
@ -1,100 +1,192 @@
|
||||
import bcrypt from 'bcrypt';
|
||||
import sql from '$lib/db/db.server';
|
||||
import type { Cookies } from '@sveltejs/kit';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import { error } from '@sveltejs/kit';
|
||||
import { saveAvatar } from '$lib/index.server';
|
||||
|
||||
export async function createUser(username: string, password: string): Promise<void> {
|
||||
const password_hash: string = await bcrypt.hash(password, 12);
|
||||
const timestamp = new Date(Date.now()).toISOString();
|
||||
|
||||
console.log(timestamp);
|
||||
export async function createUser(user: User): Promise<number> {
|
||||
const password_hash: string = await bcrypt.hash(user.password!, 12);
|
||||
|
||||
const response = await sql`
|
||||
INSERT INTO users (username, password_hash, perms, created_at, last_signin, active)
|
||||
VALUES (${username}, ${password_hash}, 3, ${timestamp}, ${timestamp}, ${true});
|
||||
INSERT INTO users (username, password_hash, perms, created_at, last_signin, active, email, phone, full_name, company_code)
|
||||
VALUES (${user.username}, ${password_hash}, ${user.perms}, NOW(), NOW(), ${user.active}, ${user.email}, ${user.phone}, ${user.fullName}, ${user.companyCode})
|
||||
RETURNING id;
|
||||
`;
|
||||
|
||||
// TODO: handle custom image uploads
|
||||
user.id = response[0].id;
|
||||
await saveAvatar(user);
|
||||
|
||||
return response[0].id;
|
||||
}
|
||||
|
||||
export async function checkUserCreds(username: string, password: string): Promise<number> {
|
||||
export async function updateUser(user: User): Promise<number> {
|
||||
let password_hash: string | null = null;
|
||||
|
||||
// Hash the password if provided
|
||||
if (user.password) {
|
||||
password_hash = await bcrypt.hash(user.password, 12);
|
||||
}
|
||||
|
||||
// Construct the SQL query
|
||||
const response = await sql`UPDATE users
|
||||
SET
|
||||
username = ${user.username},
|
||||
perms = ${user.perms},
|
||||
active = ${user.active},
|
||||
${password_hash !== null ? sql`password_hash = ${password_hash},` : sql``}
|
||||
email = ${user.email},
|
||||
phone = ${user.phone},
|
||||
full_name = ${user.fullName},
|
||||
company_code = ${user.companyCode}
|
||||
WHERE id = ${user.id}
|
||||
RETURNING id;`;
|
||||
|
||||
await saveAvatar(user);
|
||||
|
||||
return response[0].id;
|
||||
}
|
||||
|
||||
export async function checkUserCreds(username: string, password: string): Promise<User | null> {
|
||||
const [user] = await sql`
|
||||
SELECT password_hash, perms
|
||||
SELECT id, password_hash, perms, active
|
||||
FROM users
|
||||
WHERE username = ${username}
|
||||
`;
|
||||
|
||||
if (!user) {
|
||||
return -1;
|
||||
return null;
|
||||
}
|
||||
if (await bcrypt.compare(password, user.password_hash)) {
|
||||
return user['perms'];
|
||||
return <User>{ id: user.id, perms: user.perms, active: user.active };
|
||||
}
|
||||
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;
|
||||
return null;
|
||||
}
|
||||
|
||||
// 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;
|
||||
export async function getUsers(searchQuery: string | null = null): Promise<User[]> {
|
||||
const users = await sql`
|
||||
SELECT id,
|
||||
username,
|
||||
perms,
|
||||
created_at AT TIME ZONE 'UTC' AS "createdAt",
|
||||
last_signin AT TIME ZONE 'UTC' AS "lastSignIn",
|
||||
active,
|
||||
email,
|
||||
phone,
|
||||
full_name AS "fullName",
|
||||
company_id
|
||||
FROM users
|
||||
WHERE username ILIKE ${searchQuery ? `%${searchQuery}%` : '%'};
|
||||
`;
|
||||
users.forEach((user) => {
|
||||
user.company = {
|
||||
id: user.company_id
|
||||
};
|
||||
delete user.company_id;
|
||||
});
|
||||
return <User[]>(<unknown>users);
|
||||
}
|
||||
|
||||
// should require MANAGE_USERS permission
|
||||
export async function getUser(id: number): Promise<User> {
|
||||
const [user] = await sql`
|
||||
SELECT id, username, perms,
|
||||
created_at AT TIME ZONE 'UTC' AS "createdAt",
|
||||
last_signin AT TIME ZONE 'UTC' AS "lastSignIn",
|
||||
active, email, phone, full_name AS "fullName", company_id, company_code
|
||||
FROM users
|
||||
WHERE id = ${id};
|
||||
`;
|
||||
if (!user) {
|
||||
error(404, 'User not found');
|
||||
}
|
||||
user.company = {
|
||||
id: user.company_id
|
||||
};
|
||||
delete user.company_id;
|
||||
return <User>user;
|
||||
}
|
||||
|
||||
export async function getUserWithCompany(id: number): Promise<User> {
|
||||
const [user] = await sql`
|
||||
SELECT
|
||||
u.id,
|
||||
u.username,
|
||||
u.perms,
|
||||
u.email,
|
||||
u.phone,
|
||||
u.full_name AS "fullName",
|
||||
u.created_at AT TIME ZONE 'UTC' AS "createdAt",
|
||||
u.last_signin AT TIME ZONE 'UTC' AS "lastSignIn",
|
||||
u.active,
|
||||
c.id AS company_id,
|
||||
c.name AS company_name,
|
||||
c.description AS company_description,
|
||||
c.website AS company_website,
|
||||
c.created_at AS company_created_at
|
||||
FROM
|
||||
users u
|
||||
LEFT JOIN
|
||||
companies c ON u.company_id = c.id
|
||||
WHERE
|
||||
u.id = ${id};
|
||||
`;
|
||||
if (!user) {
|
||||
error(404, 'User not found');
|
||||
}
|
||||
user.company = {
|
||||
id: user.company_id,
|
||||
name: user.company_name,
|
||||
description: user.company_description,
|
||||
website: user.company_website,
|
||||
createdAt: user.company_created_at
|
||||
};
|
||||
|
||||
// Remove the company-specific columns from the user object
|
||||
delete user.company_id;
|
||||
delete user.company_name;
|
||||
delete user.company_description;
|
||||
delete user.company_website;
|
||||
delete user.company_created_at;
|
||||
|
||||
return <User>user;
|
||||
}
|
||||
|
||||
// should require MANAGE_USERS permission
|
||||
export async function deleteUser(id: number): Promise<void> {
|
||||
const response = await sql`
|
||||
DELETE FROM users
|
||||
WHERE id = ${id};
|
||||
`;
|
||||
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;
|
||||
export async function getTags(searchQuery: string | null): Promise<Tag[]> {
|
||||
return sql<Tag[]>`
|
||||
SELECT id, display_name as "displayName", created_at AT TIME ZONE 'UTC' AS "createdAt"
|
||||
FROM tags
|
||||
WHERE display_name ILIKE ${searchQuery ? `%${searchQuery}%` : '%'};
|
||||
`;
|
||||
return tags.map(
|
||||
(tag): Tag => ({
|
||||
id: tag.id,
|
||||
display_name: tag.display_name
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
export async function updateLastSignin(username: string): Promise<void> {
|
||||
await sql`
|
||||
UPDATE users
|
||||
SET last_signin = NOW()
|
||||
WHERE username = ${username};
|
||||
`;
|
||||
}
|
||||
|
||||
export async function createCompany(
|
||||
name: string,
|
||||
description: string,
|
||||
website: string
|
||||
): Promise<number> {
|
||||
const response = await sql`
|
||||
INSERT INTO companies (name, description, website, created_at, company_code)
|
||||
VALUES (${name}, ${description}, ${website}, NOW(), generate_company_code(CAST(CURRVAL('companies_id_seq') AS INT)))
|
||||
RETURNING id;
|
||||
`;
|
||||
|
||||
return response[0].id;
|
||||
}
|
||||
|
||||
53
src/lib/index.server.ts
Normal file
53
src/lib/index.server.ts
Normal file
@ -0,0 +1,53 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import fetch from 'node-fetch';
|
||||
import { type Cookies, error } from '@sveltejs/kit';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import { getUserWithCompany } from '$lib/db/index.server';
|
||||
|
||||
// TODO: Handle saving custom avatar uploads
|
||||
export async function saveAvatar(user: User): Promise<void> {
|
||||
const url = `https://ui-avatars.com/api/?background=random&format=svg&name=${user.fullName ? encodeURIComponent(user.fullName) : encodeURIComponent(user.username)}`;
|
||||
const response = await fetch(url, { headers: { accept: 'image/svg+xml' } });
|
||||
const avatar = await response.text();
|
||||
const filePath = path.join('static', 'uploads', 'avatars', `${user.id}.svg`);
|
||||
fs.writeFileSync(filePath, avatar);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
export function getUserId(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' && 'id' in decoded) {
|
||||
return decoded['id'];
|
||||
}
|
||||
} catch (err) {
|
||||
error(403, 'Unauthorized');
|
||||
}
|
||||
}
|
||||
error(403, 'Unauthorized');
|
||||
}
|
||||
@ -1 +0,0 @@
|
||||
// place files you want to import through the `$lib` alias in this folder.
|
||||
@ -1 +1,21 @@
|
||||
export let userState = $state({ perms: 0b00000001 });
|
||||
import { PERMISSIONS } from '$lib/consts';
|
||||
|
||||
export let userState = $state({ perms: PERMISSIONS.VIEW, username: null, id: null });
|
||||
|
||||
export const userPerms = PERMISSIONS.VIEW | PERMISSIONS.APPLY_FOR_JOBS;
|
||||
export const employerPerms = PERMISSIONS.SUBMIT_POSTINGS | PERMISSIONS.MANAGE_EMPLOYERS;
|
||||
export const adminPerms =
|
||||
PERMISSIONS.MANAGE_TAGS |
|
||||
PERMISSIONS.MANAGE_POSTINGS |
|
||||
PERMISSIONS.MANAGE_USERS |
|
||||
PERMISSIONS.MANAGE_COMPANIES;
|
||||
|
||||
export function telFormatter(initial: string) {
|
||||
const num = initial.replace(/\D/g, '');
|
||||
initial =
|
||||
(num.length > 0 ? '(' : '') +
|
||||
num.substring(0, 3) +
|
||||
(num.length > 3 ? ') ' + num.substring(3, 6) : '') +
|
||||
(num.length > 6 ? '-' + num.substring(6, 10) : '');
|
||||
return initial;
|
||||
}
|
||||
|
||||
@ -1,13 +1,28 @@
|
||||
interface User {
|
||||
id: number | null;
|
||||
username: string;
|
||||
password: string | null;
|
||||
perms: number;
|
||||
created_at: Date | null;
|
||||
last_signin: Date | null;
|
||||
createdAt: Date | null;
|
||||
lastSignIn: Date | null;
|
||||
active: boolean | null;
|
||||
email: string | null;
|
||||
phone: string | null;
|
||||
fullName: string | null;
|
||||
company: Company | null;
|
||||
companyCode: string | null;
|
||||
}
|
||||
|
||||
interface Tag {
|
||||
id: number;
|
||||
display_name: string;
|
||||
displayName: string;
|
||||
createdAt: Date | null;
|
||||
}
|
||||
|
||||
interface Company {
|
||||
id: number;
|
||||
name: string;
|
||||
description: string;
|
||||
website: string;
|
||||
createdAt: Date | null;
|
||||
}
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
import { onMount } from 'svelte';
|
||||
import { userState } from '$lib/shared.svelte';
|
||||
import { PERMISSIONS } from '$lib/consts';
|
||||
import { updateUserState } from './utils.client';
|
||||
|
||||
let currentTheme: string = $state('');
|
||||
|
||||
@ -12,15 +13,12 @@
|
||||
}
|
||||
|
||||
function setTheme(theme: string) {
|
||||
const one_year = 60 * 60 * 24 * 365;
|
||||
document.cookie = `theme=${theme}; max-age=${one_year}; path=/`;
|
||||
const oneYear = 60 * 60 * 24 * 365;
|
||||
document.cookie = `theme=${theme}; max-age=${oneYear}; path=/`;
|
||||
document.documentElement.setAttribute('data-theme', theme);
|
||||
currentTheme = theme;
|
||||
}
|
||||
|
||||
const getCookieValue = (name: String) =>
|
||||
document.cookie.match('(^|;)\\s*' + name + '\\s*=\\s*([^;]+)')?.pop() || '';
|
||||
|
||||
onMount(() => {
|
||||
const savedTheme = document.documentElement.getAttribute('data-theme');
|
||||
if (savedTheme) {
|
||||
@ -31,18 +29,25 @@
|
||||
setTheme(currentTheme);
|
||||
}
|
||||
|
||||
const JWT = getCookieValue('jwt');
|
||||
if (JWT !== '') {
|
||||
userState.perms = JSON.parse(atob(JWT.split('.')[1])).perms;
|
||||
}
|
||||
updateUserState();
|
||||
});
|
||||
|
||||
if ((userState.perms & PERMISSIONS.MANAGE_POSTINGS) !== 0) {
|
||||
('/admin/postings');
|
||||
} else if ((userState.perms & PERMISSIONS.MANAGE_USERS) !== 0) {
|
||||
('/admin/employers');
|
||||
} else if ((userState.perms & PERMISSIONS.MANAGE_TAGS) !== 0) {
|
||||
('/admin/tags');
|
||||
} else if ((userState.perms & PERMISSIONS.MANAGE_COMPANIES) !== 0) {
|
||||
('/admin/companies');
|
||||
}
|
||||
|
||||
let { children } = $props();
|
||||
</script>
|
||||
|
||||
<link
|
||||
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,check,close,dark_mode,edit,group,info,light_mode,login,person,search,sell,visibility,visibility_off,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,arrow_drop_up,calendar_today,call,check,close,dark_mode,edit,group,info,light_mode,login,mail,person,search,sell,store,visibility,visibility_off,work"
|
||||
/>
|
||||
|
||||
<div class="bottom-border flex h-14 justify-between p-3 align-middle">
|
||||
@ -57,12 +62,24 @@
|
||||
/>
|
||||
</a>
|
||||
<a href="/about" class="hover-bg-color mr-1 rounded px-3 py-2 text-sm">About</a>
|
||||
{#if (userState.perms & PERMISSIONS.VIEW_POSTINGS) !== 0}
|
||||
{#if (userState.perms & PERMISSIONS.VIEW) !== 0}
|
||||
<a href="/listings" class="hover-bg-color mr-1 rounded px-3 py-2 text-sm">Listings</a>
|
||||
{/if}
|
||||
{#if (userState.perms & PERMISSIONS.MANAGE_POSTINGS) !== 0}
|
||||
<a href="/admin/postings" class="hover-bg-color mr-1 rounded px-3 py-2 text-sm"
|
||||
>Administration</a
|
||||
{#if (userState.perms & PERMISSIONS.VIEW) !== 0}
|
||||
<a href="/companies" class="hover-bg-color mr-1 rounded px-3 py-2 text-sm">Companies</a>
|
||||
{/if}
|
||||
{#if (userState.perms & (PERMISSIONS.MANAGE_POSTINGS | PERMISSIONS.MANAGE_TAGS | PERMISSIONS.MANAGE_USERS)) !== 0}
|
||||
<a
|
||||
href={(userState.perms & PERMISSIONS.MANAGE_POSTINGS) !== 0
|
||||
? '/admin/postings'
|
||||
: (userState.perms & PERMISSIONS.MANAGE_USERS) !== 0
|
||||
? '/admin/users'
|
||||
: (userState.perms & PERMISSIONS.MANAGE_TAGS) !== 0
|
||||
? '/admin/tags'
|
||||
: (userState.perms & PERMISSIONS.MANAGE_COMPANIES) !== 0
|
||||
? '/admin/companies'
|
||||
: '/admin'}
|
||||
class="hover-bg-color mr-1 rounded px-3 py-2 text-sm">Administration</a
|
||||
>
|
||||
{/if}
|
||||
</nav>
|
||||
@ -72,13 +89,9 @@
|
||||
{currentTheme === 'light' ? 'light_mode' : 'dark_mode'}
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
onclick={() =>
|
||||
(window.location.href =
|
||||
(userState.perms & PERMISSIONS.VIEW_ACCOUNT) !== 0 ? '/account' : '/signin')}
|
||||
>
|
||||
<button onclick={() => (window.location.href = userState.id !== null ? '/account' : '/signin')}>
|
||||
<span class="material-symbols-outlined hover-bg-color rounded-full p-1">
|
||||
{(userState.perms & PERMISSIONS.VIEW_ACCOUNT) !== 0 ? 'account_circle' : 'login'}
|
||||
{userState.id !== null ? 'account_circle' : 'login'}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@ -1,2 +1,8 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { updateUserState } from './utils.client';
|
||||
|
||||
onMount(() => {
|
||||
updateUserState();
|
||||
});
|
||||
</script>
|
||||
|
||||
10
src/routes/account/+page.server.ts
Normal file
10
src/routes/account/+page.server.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import type { PageServerLoad } from './$types';
|
||||
import { getUserWithCompany } from '$lib/db/index.server';
|
||||
import { error } from '@sveltejs/kit';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import { getUserId } from '$lib/index.server';
|
||||
|
||||
export const load: PageServerLoad = async ({ cookies }) => {
|
||||
const id = getUserId(cookies);
|
||||
return { user: await getUserWithCompany(id) };
|
||||
};
|
||||
@ -1,9 +1,132 @@
|
||||
<script>
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import type { PageProps } from './$types';
|
||||
|
||||
onMount(() => {
|
||||
if (!document.cookie.includes('jwt=')) {
|
||||
window.location.href = '/signin';
|
||||
}
|
||||
});
|
||||
|
||||
const dateFormatOptions: Intl.DateTimeFormatOptions = {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric'
|
||||
};
|
||||
|
||||
function avatarFallback(e: Event) {
|
||||
(e.target as HTMLImageElement).src =
|
||||
`https://ui-avatars.com/api/?background=random&format=svg&name=${data.user.fullName ? encodeURIComponent(data.user.fullName) : encodeURIComponent(data.user.username)}`;
|
||||
}
|
||||
|
||||
function logoFallback(e: Event) {
|
||||
(e.target as HTMLImageElement).src =
|
||||
`https://ui-avatars.com/api/?background=random&format=svg&name=${encodeURIComponent(data.user.company!.name!)}`;
|
||||
}
|
||||
|
||||
function signOut() {
|
||||
document.cookie = 'jwt=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;';
|
||||
window.location.href = '/signin';
|
||||
}
|
||||
|
||||
let { data, form }: PageProps = $props();
|
||||
</script>
|
||||
|
||||
<div class="base-container">
|
||||
<div class="content flex py-4">
|
||||
<div class="elevated separator-borders m-2 inline-block h-min min-w-max rounded align-top">
|
||||
<div class="inline-block p-4">
|
||||
<img
|
||||
id="avatar"
|
||||
class="mb-2 inline-block rounded-lg"
|
||||
src="/uploads/avatars/{data.user.id}.svg?timestamp=${Date.now()}"
|
||||
onerror={avatarFallback}
|
||||
alt="User avatar"
|
||||
height="240"
|
||||
width="240"
|
||||
/>
|
||||
{#if data.user.fullName}
|
||||
<h2 class="text-center font-semibold">{data.user.fullName}</h2>
|
||||
<h3 class="text-center">{data.user.username}</h3>
|
||||
{/if}
|
||||
{#if !data.user.fullName}
|
||||
<h2 class="text-center font-semibold">{data.user.username}</h2>
|
||||
{/if}
|
||||
</div>
|
||||
{#if data.user.email}
|
||||
<div class="top-border p-3">
|
||||
<span class="material-symbols-outlined align-middle">mail</span>
|
||||
<a class="hover-hyperlink" href="mailto:{data.user.email}">{data.user.email}</a>
|
||||
</div>
|
||||
{/if}
|
||||
{#if data.user.phone}
|
||||
<div class="top-border p-3">
|
||||
<span class="material-symbols-outlined align-middle">call</span>
|
||||
<a class="hover-hyperlink" href="tel:{data.user.phone}">{data.user.phone}</a>
|
||||
</div>
|
||||
{/if}
|
||||
{#if data.user.createdAt}
|
||||
<div class="top-border p-3">
|
||||
<span class="material-symbols-outlined align-middle">calendar_today</span>
|
||||
Joined {data.user.createdAt.toLocaleDateString('en-US', dateFormatOptions)}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="elevated separator-borders m-2 inline-block h-min w-full rounded">
|
||||
<div class="bottom-border flex place-content-between">
|
||||
<div class="p-3 font-semibold">User Details</div>
|
||||
<div class="flex">
|
||||
<a class="dull-primary-bg-color my-2 rounded-md px-2.5 py-1" href="/account/settings"
|
||||
>Edit account</a
|
||||
>
|
||||
<button class="danger-border-color m-2 rounded-md border px-2.5 py-1" onclick={signOut}
|
||||
>Sign out</button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-3">
|
||||
<div class="font-semibold">
|
||||
ID: <span class="font-normal">{data.user.id}</span>
|
||||
</div>
|
||||
<div class="font-semibold">
|
||||
Account active: <span class="material-symbols-outlined align-middle"
|
||||
>{data.user.active ? 'check' : 'cancel'}</span
|
||||
>
|
||||
</div>
|
||||
<div class="font-semibold">
|
||||
Last sign-in: <span class="font-normal"
|
||||
>{data.user.lastSignIn?.toLocaleDateString('en-US', dateFormatOptions)}</span
|
||||
>
|
||||
</div>
|
||||
{#if !data.user.company?.id}
|
||||
<div class="pb-2 font-semibold">
|
||||
Employer company: <span class="font-normal">N/A</span>
|
||||
</div>
|
||||
{/if}
|
||||
{#if data.user.company?.id}
|
||||
<div class="font-semibold">
|
||||
Employer company:
|
||||
<div class="top-border mt-2 p-3">
|
||||
<img
|
||||
id="logo"
|
||||
class="mb-2 inline-block rounded"
|
||||
src="/uploads/logos/{data.user.company.id}.svg?timestamp=${Date.now()}"
|
||||
alt="Company Logo"
|
||||
onerror={logoFallback}
|
||||
height="32"
|
||||
width="32"
|
||||
/>
|
||||
<div class="inline-block">
|
||||
<div>{data.user.company.name}</div>
|
||||
<div class="max-char-length font-normal">{data.user.company.description}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="top-border pt-2 font-semibold">
|
||||
Permissions: <span class="font-normal">{data.user.perms}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
63
src/routes/account/settings/+page.server.ts
Normal file
63
src/routes/account/settings/+page.server.ts
Normal file
@ -0,0 +1,63 @@
|
||||
import type { PageServerLoad } from './$types';
|
||||
import { deleteUser, getUser, getUserWithCompany, updateUser } from '$lib/db/index.server';
|
||||
import { type Actions, fail, redirect } from '@sveltejs/kit';
|
||||
import { PERMISSIONS } from '$lib/consts';
|
||||
import { getUserId } from '$lib/index.server';
|
||||
|
||||
export const load: PageServerLoad = async ({ cookies }) => {
|
||||
const id = getUserId(cookies);
|
||||
return { user: await getUserWithCompany(id) };
|
||||
};
|
||||
|
||||
export const actions: Actions = {
|
||||
submit: async ({ request, cookies }) => {
|
||||
const id = getUserId(cookies);
|
||||
const data = await request.formData();
|
||||
const username = data.get('username')?.toString().trim();
|
||||
let email: string | undefined | null = data.get('email')?.toString().trim();
|
||||
let phone: string | undefined | null = data.get('phone')?.toString().trim();
|
||||
let fullName: string | undefined | null = data.get('fullName')?.toString().trim();
|
||||
let companyCode: string | undefined | null = data
|
||||
.get('companyCode')
|
||||
?.toString()
|
||||
.toUpperCase()
|
||||
.trim();
|
||||
|
||||
if (email === '' || email == undefined) email = null;
|
||||
if (phone === '' || phone == undefined) phone = null;
|
||||
if (fullName === '' || fullName == undefined) fullName = null;
|
||||
if (companyCode === '' || companyCode == undefined) companyCode = null;
|
||||
|
||||
if (email && !email.includes('@')) {
|
||||
return fail(400, { errorMessage: 'Invalid email' });
|
||||
}
|
||||
|
||||
if (phone && !phone.match(/\((\d{3})\) \d{3}-\d{4}/)) {
|
||||
return fail(400, { errorMessage: 'Invalid phone number' });
|
||||
}
|
||||
|
||||
if (username && username !== '') {
|
||||
try {
|
||||
await updateUser(<User>{
|
||||
id: id,
|
||||
username: username,
|
||||
email: email,
|
||||
phone: phone,
|
||||
fullName: fullName,
|
||||
companyCode: companyCode
|
||||
});
|
||||
} catch (err) {
|
||||
return fail(500, { errorMessage: `Internal Server Error: ${err}` });
|
||||
}
|
||||
return redirect(301, `/account`);
|
||||
}
|
||||
},
|
||||
delete: async ({ cookies }) => {
|
||||
const id = getUserId(cookies);
|
||||
try {
|
||||
await deleteUser(id);
|
||||
} catch (err) {
|
||||
return fail(500, { errorMessage: `Internal Server Error: ${err}` });
|
||||
}
|
||||
}
|
||||
};
|
||||
198
src/routes/account/settings/+page.svelte
Normal file
198
src/routes/account/settings/+page.svelte
Normal file
@ -0,0 +1,198 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { enhance } from '$app/forms';
|
||||
import type { PageProps } from './$types';
|
||||
import { telFormatter } from '$lib/shared.svelte';
|
||||
let permsAccordions: boolean[] = [false, false, false];
|
||||
|
||||
onMount(() => {
|
||||
let acc = document.getElementsByClassName('accordion');
|
||||
for (let i = 0; i < acc.length; i++) {
|
||||
acc[i].addEventListener('click', function (this: HTMLElement, event: Event) {
|
||||
const target = event?.target as HTMLElement | null;
|
||||
if (target?.tagName === 'INPUT' && (target as HTMLInputElement).type === 'checkbox') {
|
||||
return; // Do nothing if it's the checkbox
|
||||
}
|
||||
|
||||
this.classList.toggle('active');
|
||||
permsAccordions[i] = !permsAccordions[i];
|
||||
|
||||
let panel = this.nextElementSibling as HTMLElement;
|
||||
if (panel.style.display === 'block') {
|
||||
panel.style.display = 'none';
|
||||
} else {
|
||||
panel.style.display = 'block';
|
||||
}
|
||||
});
|
||||
|
||||
let selectAllCheckbox = acc[i].querySelector('.select-all') as HTMLInputElement;
|
||||
selectAllCheckbox.addEventListener('change', function () {
|
||||
let checkboxes =
|
||||
this.parentElement!.parentElement!.parentElement!.nextElementSibling!.querySelectorAll(
|
||||
'.permCheckbox'
|
||||
) as NodeListOf<HTMLInputElement>;
|
||||
checkboxes.forEach((checkbox) => {
|
||||
checkbox.checked = selectAllCheckbox.checked;
|
||||
});
|
||||
});
|
||||
|
||||
let permCheckboxes = acc[i].nextElementSibling!.querySelectorAll(
|
||||
'.permCheckbox'
|
||||
) as NodeListOf<HTMLInputElement>;
|
||||
|
||||
permCheckboxes.forEach((checkbox) => {
|
||||
checkbox.addEventListener('change', function () {
|
||||
let allChecked = true;
|
||||
let someChecked = false;
|
||||
permCheckboxes.forEach((cb) => {
|
||||
if (cb.checked) {
|
||||
someChecked = true;
|
||||
} else {
|
||||
allChecked = false;
|
||||
}
|
||||
});
|
||||
selectAllCheckbox.checked = allChecked;
|
||||
selectAllCheckbox.indeterminate = !allChecked && someChecked;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const modal: HTMLElement = document.getElementById('deleteConfirmModal') as HTMLElement;
|
||||
window.onclick = function (event) {
|
||||
if (event.target == modal) {
|
||||
modal.style.display = 'none';
|
||||
}
|
||||
};
|
||||
document.getElementById('phone')?.addEventListener('input', function (this: HTMLInputElement) {
|
||||
this.value = telFormatter(this.value);
|
||||
});
|
||||
});
|
||||
|
||||
function openConfirm() {
|
||||
document.getElementById('deleteConfirmModal')!.style.display = 'block';
|
||||
}
|
||||
|
||||
function closeConfirm() {
|
||||
document.getElementById('deleteConfirmModal')!.style.display = 'none';
|
||||
}
|
||||
|
||||
let { data, form }: PageProps = $props();
|
||||
|
||||
let perms = data.user!.perms;
|
||||
</script>
|
||||
|
||||
<div class="base-container">
|
||||
<div class="content">
|
||||
<div class="elevated separator-borders m-4 rounded">
|
||||
<div class="bottom-border flex place-content-between">
|
||||
<div class="p-3 font-semibold">Edit Account</div>
|
||||
</div>
|
||||
<form method="POST" class="px-4" autocomplete="off" use:enhance>
|
||||
<div class="mt-4 text-sm font-semibold">
|
||||
Username
|
||||
<input
|
||||
type="text"
|
||||
name="username"
|
||||
id="username"
|
||||
value={data.user?.username}
|
||||
placeholder="Username"
|
||||
class="w-full rounded font-normal"
|
||||
/>
|
||||
</div>
|
||||
<div class="mt-4 text-sm font-semibold">
|
||||
Email (optional)
|
||||
<input
|
||||
type="email"
|
||||
name="email"
|
||||
id="email"
|
||||
value={data.user?.email}
|
||||
placeholder="Email"
|
||||
class="w-full rounded font-normal"
|
||||
/>
|
||||
</div>
|
||||
<div class="mt-4 text-sm font-semibold">
|
||||
Phone (optional)
|
||||
<input
|
||||
type="tel"
|
||||
name="phone"
|
||||
id="phone"
|
||||
value={data.user?.phone}
|
||||
placeholder="Phone"
|
||||
class="w-full rounded font-normal"
|
||||
pattern="([0-9]\{3}) [0-9]\{3}-[0-9]\{3}"
|
||||
/>
|
||||
</div>
|
||||
<div class="mt-4 text-sm font-semibold">
|
||||
Full name (optional)
|
||||
<input
|
||||
type="text"
|
||||
name="fullName"
|
||||
id="fullName"
|
||||
value={data.user?.fullName}
|
||||
placeholder="Full name"
|
||||
class="w-full rounded font-normal"
|
||||
/>
|
||||
</div>
|
||||
<div class="mt-4 text-sm font-semibold">
|
||||
Company code (optional)
|
||||
<input
|
||||
type="text"
|
||||
name="companyCode"
|
||||
id="companyCode"
|
||||
placeholder="Company code"
|
||||
value={data.user?.companyCode}
|
||||
class="w-full rounded font-normal"
|
||||
/>
|
||||
</div>
|
||||
<p class="low-emphasis-text pb-4">
|
||||
Enter a code here to join a company. You will still have to be approved before you can
|
||||
make any postings.
|
||||
</p>
|
||||
|
||||
{#if form?.errorMessage}
|
||||
<div class="mb-2 text-red-500">{form.errorMessage}</div>
|
||||
{/if}
|
||||
<div class="flex justify-between">
|
||||
<button
|
||||
class="dull-primary-bg-color mb-4 mt-2 rounded px-2 py-1"
|
||||
type="submit"
|
||||
formaction="?/submit">Update account</button
|
||||
>
|
||||
<button
|
||||
class="danger-bg-color mb-4 mt-2 rounded px-2 py-1"
|
||||
type="button"
|
||||
onclick={openConfirm}>Delete account</button
|
||||
>
|
||||
</div>
|
||||
<div id="deleteConfirmModal" class="modal">
|
||||
<div class="modal-content">
|
||||
<div class="mb-2 inline-flex w-full justify-between">
|
||||
<h2 class="font-semibold">Are you sure?</h2>
|
||||
<button class="material-symbols-outlined" onclick={closeConfirm}>close</button>
|
||||
</div>
|
||||
<p>This will permanently delete your account. This action cannot be undone.</p>
|
||||
<p>Please type "I understand" into the box below to confirm</p>
|
||||
<input
|
||||
type="text"
|
||||
name="confirm"
|
||||
id="confirm"
|
||||
placeholder="I understand"
|
||||
class="w-full rounded font-normal"
|
||||
pattern="I understand"
|
||||
/>
|
||||
<div class="mt-4 flex justify-between">
|
||||
<button class="danger-bg-color rounded px-2 py-1" type="submit" formaction="?/delete"
|
||||
>Delete account</button
|
||||
>
|
||||
<button
|
||||
class="separator-borders bg-color rounded px-2 py-1"
|
||||
type="button"
|
||||
onclick={closeConfirm}>Cancel</button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -1,31 +1,49 @@
|
||||
<script lang="ts">
|
||||
import '../../app.css';
|
||||
import { page } from '$app/stores';
|
||||
import { page } from '$app/state';
|
||||
import { userState } from '$lib/shared.svelte';
|
||||
import { PERMISSIONS } from '$lib/consts';
|
||||
|
||||
let { children } = $props();
|
||||
</script>
|
||||
|
||||
<div class="bottom-border h-10 pt-2 text-center">
|
||||
<a
|
||||
href="/admin/postings"
|
||||
class="p-2 {$page.url.pathname === '/admin/postings'
|
||||
? 'primary-underline font-bold'
|
||||
: 'low-emphasis-text'}"
|
||||
><span class="material-symbols-outlined align-bottom">work</span> Postings</a
|
||||
>
|
||||
<a
|
||||
href="/admin/users"
|
||||
class="p-2 {$page.url.pathname === '/admin/users'
|
||||
? 'primary-underline font-bold'
|
||||
: 'low-emphasis-text'}"
|
||||
><span class="material-symbols-outlined align-bottom">group</span> Users</a
|
||||
><a
|
||||
href="/admin/tags"
|
||||
class="{$page.url.pathname === '/admin/tags'
|
||||
? 'primary-underline font-bold'
|
||||
: 'low-emphasis-text'} p-2"
|
||||
><span class="material-symbols-outlined align-bottom">sell</span> Tags</a
|
||||
>
|
||||
{#if (userState.perms & PERMISSIONS.MANAGE_POSTINGS) !== 0}
|
||||
<a
|
||||
href="/admin/postings"
|
||||
class="p-2 {page.url.pathname.startsWith('/admin/postings')
|
||||
? 'primary-underline font-bold'
|
||||
: 'low-emphasis-text low-emphasis-text-button'}"
|
||||
><span class="material-symbols-outlined align-bottom">work</span> Postings</a
|
||||
>
|
||||
{/if}
|
||||
{#if (userState.perms & PERMISSIONS.MANAGE_USERS) !== 0}
|
||||
<a
|
||||
href="/admin/users"
|
||||
class="p-2 {page.url.pathname.startsWith('/admin/users')
|
||||
? 'primary-underline font-bold'
|
||||
: 'low-emphasis-text low-emphasis-text-button'}"
|
||||
><span class="material-symbols-outlined align-bottom">group</span> Users</a
|
||||
>
|
||||
{/if}
|
||||
{#if (userState.perms & PERMISSIONS.MANAGE_TAGS) !== 0}
|
||||
<a
|
||||
href="/admin/tags"
|
||||
class="{page.url.pathname.startsWith('/admin/tags')
|
||||
? 'primary-underline font-bold'
|
||||
: 'low-emphasis-text low-emphasis-text-button'} p-2"
|
||||
><span class="material-symbols-outlined align-bottom">sell</span> Tags</a
|
||||
>
|
||||
{/if}
|
||||
{#if (userState.perms & PERMISSIONS.MANAGE_COMPANIES) !== 0}
|
||||
<a
|
||||
href="/admin/companies"
|
||||
class="{page.url.pathname.startsWith('/admin/companies')
|
||||
? 'primary-underline font-bold'
|
||||
: 'low-emphasis-text low-emphasis-text-button'} p-2"
|
||||
><span class="material-symbols-outlined align-bottom">store</span> Companies</a
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="base-container">
|
||||
|
||||
@ -1,7 +1,17 @@
|
||||
<script>
|
||||
import { onMount } from 'svelte';
|
||||
import { userState } from '$lib/shared.svelte';
|
||||
import { PERMISSIONS } from '$lib/consts';
|
||||
|
||||
onMount(() => {
|
||||
window.location.href = '/admin/postings';
|
||||
if ((userState.perms & PERMISSIONS.MANAGE_POSTINGS) !== 0) {
|
||||
window.location.href = '/admin/postings';
|
||||
} else if ((userState.perms & PERMISSIONS.MANAGE_USERS) !== 0) {
|
||||
window.location.href = '/admin/employers';
|
||||
} else if ((userState.perms & PERMISSIONS.MANAGE_TAGS) !== 0) {
|
||||
window.location.href = '/admin/tags';
|
||||
} else if ((userState.perms & PERMISSIONS.MANAGE_COMPANIES) !== 0) {
|
||||
window.location.href = '/admin/companies';
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
0
src/routes/admin/companies/+page.server.ts
Normal file
0
src/routes/admin/companies/+page.server.ts
Normal file
0
src/routes/admin/companies/+page.svelte
Normal file
0
src/routes/admin/companies/+page.svelte
Normal file
0
src/routes/admin/companies/[company]/+page.svelte
Normal file
0
src/routes/admin/companies/[company]/+page.svelte
Normal file
0
src/routes/admin/companies/create/+page.svelte
Normal file
0
src/routes/admin/companies/create/+page.svelte
Normal file
0
src/routes/admin/companies/edit/+page.svelte
Normal file
0
src/routes/admin/companies/edit/+page.svelte
Normal file
@ -1,13 +1,15 @@
|
||||
import type { PageServerLoad } from './$types';
|
||||
import { getUserPerms, getTags } from '$lib/db/index.server';
|
||||
import { getTags } from '$lib/db/index.server';
|
||||
import { PERMISSIONS } from '$lib/consts';
|
||||
import { error } from '@sveltejs/kit';
|
||||
import { getUserPerms } from '$lib/index.server';
|
||||
|
||||
export const load: PageServerLoad = async ({ cookies }) => {
|
||||
export const load: PageServerLoad = async ({ cookies, url }) => {
|
||||
const search = url.searchParams.get('searchUsers');
|
||||
const perms = getUserPerms(cookies);
|
||||
if (perms >= 0 && (perms & PERMISSIONS.MANAGE_TAGS) > 0) {
|
||||
return {
|
||||
tags: await getTags()
|
||||
tags: await getTags(search)
|
||||
};
|
||||
}
|
||||
error(401, 'Unauthorized');
|
||||
|
||||
@ -1,15 +1,21 @@
|
||||
<script lang="ts">
|
||||
const dateFormatOptions: Intl.DateTimeFormatOptions = {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric'
|
||||
};
|
||||
|
||||
let { data } = $props();
|
||||
</script>
|
||||
|
||||
<div class="content">
|
||||
<div class="elevated borders m-4 rounded">
|
||||
<div class="elevated separator-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
|
||||
>Create new tag</a
|
||||
>
|
||||
</div>
|
||||
<form action="" class="px-4">
|
||||
@ -40,6 +46,7 @@
|
||||
<tr>
|
||||
<th class="left py-1">ID</th>
|
||||
<th class="py-1">Name</th>
|
||||
<th class="py-1">Created</th>
|
||||
<th class="py-1"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
@ -48,7 +55,8 @@
|
||||
{#each data.tags as tag}
|
||||
<tr>
|
||||
<td class="left">{tag.id}</td>
|
||||
<td>{tag.display_name}</td>
|
||||
<td>{tag.displayName}</td>
|
||||
<td>{tag.createdAt?.toLocaleDateString('en-US', dateFormatOptions)}</td>
|
||||
<td class="w-28 pr-1 text-end">
|
||||
<a
|
||||
class="hover-bg-color material-symbols-outlined icon-20 my-1 rounded p-1"
|
||||
|
||||
@ -1,14 +1,16 @@
|
||||
import type { PageServerLoad } from './$types';
|
||||
import { getUserPerms, getUsers } from '$lib/db/index.server';
|
||||
import { getUsers } from '$lib/db/index.server';
|
||||
import { PERMISSIONS } from '$lib/consts';
|
||||
import { error } from '@sveltejs/kit';
|
||||
import { getUserPerms } from '$lib/index.server';
|
||||
|
||||
export const load: PageServerLoad = async ({ cookies }) => {
|
||||
export const load: PageServerLoad = async ({ cookies, url }) => {
|
||||
const search = url.searchParams.get('searchUsers');
|
||||
const perms = getUserPerms(cookies);
|
||||
if (perms >= 0 && (perms & PERMISSIONS.MANAGE_USERS) > 0) {
|
||||
return {
|
||||
users: await getUsers()
|
||||
users: await getUsers(search)
|
||||
};
|
||||
}
|
||||
error(401, 'Unauthorized');
|
||||
error(403, 'Unauthorized');
|
||||
};
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { userPerms, employerPerms, adminPerms } from '$lib/shared.svelte';
|
||||
let { data } = $props();
|
||||
|
||||
const dateFormatOptions: Intl.DateTimeFormatOptions = {
|
||||
@ -6,17 +7,23 @@
|
||||
month: 'short',
|
||||
day: 'numeric'
|
||||
};
|
||||
// console.log(data);
|
||||
|
||||
function getRoleFromPerms(perms: number): string {
|
||||
if (perms & adminPerms) return 'Admin';
|
||||
if (perms & employerPerms) return 'Employer';
|
||||
if (perms & userPerms) return 'User';
|
||||
return 'None';
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="content">
|
||||
<div class="elevated borders m-4 rounded">
|
||||
<div class="elevated separator-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
|
||||
>Create new user</a
|
||||
>
|
||||
</div>
|
||||
<form action="" class="px-4">
|
||||
@ -45,13 +52,13 @@
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="left py-1">ID</th>
|
||||
<th class="left w-16 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>
|
||||
<th class="w-28 py-1"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@ -60,13 +67,12 @@
|
||||
<tr>
|
||||
<td class="left">{user.id}</td>
|
||||
<td>{user.username}</td>
|
||||
<td>{user.perms}</td>
|
||||
<td>{user.perms} ({getRoleFromPerms(user.perms)})</td>
|
||||
<td
|
||||
>{user.created_at?.toLocaleDateString('en-US', dateFormatOptions) ||
|
||||
'unknown'}</td
|
||||
>{user.createdAt?.toLocaleDateString('en-US', dateFormatOptions) || 'unknown'}</td
|
||||
>
|
||||
<td
|
||||
>{user.last_signin?.toLocaleDateString('en-US', dateFormatOptions) ||
|
||||
>{user.lastSignIn?.toLocaleDateString('en-US', dateFormatOptions) ||
|
||||
'unknown'}</td
|
||||
>
|
||||
<td class="material-symbols-outlined py-2">
|
||||
@ -74,12 +80,14 @@
|
||||
</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
|
||||
class="hover-bg-color material-symbols-outlined dull-primary-text-color icon-20 tooltip my-1 rounded p-1"
|
||||
href="/admin/users/{user.id}"
|
||||
>person<span class="tooltip-text font-sans text-sm">View account</span></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
|
||||
class="hover-bg-color material-symbols-outlined icon-20 dull-primary-text-color tooltip my-1 ml-1 mr-8 rounded p-1"
|
||||
href="/admin/users/{user.id}/edit"
|
||||
>edit<span class="tooltip-text font-sans text-sm">Edit account</span></a
|
||||
>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
16
src/routes/admin/users/[user]/+page.server.ts
Normal file
16
src/routes/admin/users/[user]/+page.server.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import type { PageServerLoad } from './$types';
|
||||
import { getUserWithCompany } from '$lib/db/index.server';
|
||||
import { PERMISSIONS } from '$lib/consts';
|
||||
import { error } from '@sveltejs/kit';
|
||||
import { getUserPerms } from '$lib/index.server';
|
||||
|
||||
export const load: PageServerLoad = async ({ cookies, params }) => {
|
||||
const id = parseInt(params.user);
|
||||
const perms = getUserPerms(cookies);
|
||||
if (perms >= 0 && (perms & PERMISSIONS.MANAGE_USERS) > 0) {
|
||||
return {
|
||||
user: await getUserWithCompany(id)
|
||||
};
|
||||
}
|
||||
error(403, 'Unauthorized');
|
||||
};
|
||||
151
src/routes/admin/users/[user]/+page.svelte
Normal file
151
src/routes/admin/users/[user]/+page.svelte
Normal file
@ -0,0 +1,151 @@
|
||||
<script lang="ts">
|
||||
import type { PageProps } from './$types';
|
||||
import { PERMISSIONS } from '$lib/consts';
|
||||
import { adminPerms, employerPerms, userPerms } from '$lib/shared.svelte';
|
||||
|
||||
const dateFormatOptions: Intl.DateTimeFormatOptions = {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric'
|
||||
};
|
||||
|
||||
function avatarFallback(e: Event) {
|
||||
(e.target as HTMLImageElement).src =
|
||||
`https://ui-avatars.com/api/?background=random&format=svg&name=${data.user.fullName ? encodeURIComponent(data.user.fullName) : encodeURIComponent(data.user.username)}`;
|
||||
}
|
||||
|
||||
function logoFallback(e: Event) {
|
||||
(e.target as HTMLImageElement).src =
|
||||
`https://ui-avatars.com/api/?background=random&format=svg&name=${encodeURIComponent(data.user.company!.name!)}`;
|
||||
}
|
||||
|
||||
let { data, form }: PageProps = $props();
|
||||
</script>
|
||||
|
||||
<div class="base-container">
|
||||
<div class="content flex py-4">
|
||||
<div class="elevated separator-borders m-2 inline-block h-min min-w-max rounded align-top">
|
||||
<div class="inline-block p-4">
|
||||
<img
|
||||
class="mb-2 inline-block rounded-lg"
|
||||
src="/uploads/avatars/{data.user.id}.svg?timestamp=${Date.now()}"
|
||||
alt="User avatar"
|
||||
onerror={avatarFallback}
|
||||
height="240"
|
||||
width="240"
|
||||
/>
|
||||
{#if data.user.fullName}
|
||||
<h2 class="text-center font-semibold">{data.user.fullName}</h2>
|
||||
<h3 class="text-center">{data.user.username}</h3>
|
||||
{/if}
|
||||
{#if !data.user.fullName}
|
||||
<h2 class="text-center font-semibold">{data.user.username}</h2>
|
||||
{/if}
|
||||
</div>
|
||||
{#if data.user.email}
|
||||
<div class="top-border p-3">
|
||||
<span class="material-symbols-outlined align-middle">mail</span>
|
||||
<a class="hover-hyperlink" href="mailto:{data.user.email}">{data.user.email}</a>
|
||||
</div>
|
||||
{/if}
|
||||
{#if data.user.phone}
|
||||
<div class="top-border p-3">
|
||||
<span class="material-symbols-outlined align-middle">call</span>
|
||||
<a class="hover-hyperlink" href="tel:{data.user.phone}">{data.user.phone}</a>
|
||||
</div>
|
||||
{/if}
|
||||
{#if data.user.createdAt}
|
||||
<div class="top-border p-3">
|
||||
<span class="material-symbols-outlined align-middle">calendar_today</span>
|
||||
Joined {data.user.createdAt.toLocaleDateString('en-US', dateFormatOptions)}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="elevated separator-borders m-2 inline-block h-min w-full rounded">
|
||||
<div class="bottom-border flex place-content-between">
|
||||
<div class="p-3 font-semibold">User Details</div>
|
||||
<a
|
||||
class="dull-primary-bg-color m-2 rounded-md px-2.5 py-1"
|
||||
href="/admin/users/{data.user.id}/edit">Edit user</a
|
||||
>
|
||||
</div>
|
||||
<div class="p-3">
|
||||
<div class="font-semibold">
|
||||
ID: <span class="font-normal">{data.user.id}</span>
|
||||
</div>
|
||||
<div class="font-semibold">
|
||||
Account active: <span class="material-symbols-outlined align-middle"
|
||||
>{data.user.active ? 'check' : 'cancel'}</span
|
||||
>
|
||||
</div>
|
||||
<div class="font-semibold">
|
||||
Last sign-in: <span class="font-normal"
|
||||
>{data.user.lastSignIn?.toLocaleDateString('en-US', dateFormatOptions)}</span
|
||||
>
|
||||
</div>
|
||||
{#if !data.user.company?.id}
|
||||
<div class="pb-2 font-semibold">
|
||||
Employer company: <span class="font-normal">N/A</span>
|
||||
</div>
|
||||
{/if}
|
||||
{#if data.user.company?.id}
|
||||
<div class="font-semibold">
|
||||
Employer company:
|
||||
<div class="top-border mt-2 p-3">
|
||||
<img
|
||||
class="mb-2 inline-block rounded-lg"
|
||||
src="/uploads/logos/{data.user.company.id}.svg?timestamp=${Date.now()}"
|
||||
alt="Company Logo"
|
||||
onerror={logoFallback}
|
||||
height="32"
|
||||
width="32"
|
||||
/>
|
||||
<div class="inline-block">
|
||||
<div>{data.user.company.name}</div>
|
||||
<div class="max-char-length font-normal">{data.user.company.description}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="top-border pt-2 font-semibold">
|
||||
Permissions: <span class="font-normal">{data.user.perms}</span>
|
||||
<div class="font-normal">
|
||||
{#if (data.user.perms & userPerms) !== 0}
|
||||
<p class="font-semibold">User permissions:</p>
|
||||
{#if (data.user.perms & PERMISSIONS.VIEW) !== 0}
|
||||
<p class="pl-4">View access</p>
|
||||
{/if}
|
||||
{#if (data.user.perms & PERMISSIONS.APPLY_FOR_JOBS) !== 0}
|
||||
<p class="pl-4">Apply for jobs</p>
|
||||
{/if}
|
||||
{/if}
|
||||
{#if (data.user.perms & employerPerms) !== 0}
|
||||
<p class="font-semibold">Employer permissions:</p>
|
||||
{#if (data.user.perms & PERMISSIONS.SUBMIT_POSTINGS) !== 0}
|
||||
<p class="pl-4">Submit postings</p>
|
||||
{/if}
|
||||
{#if (data.user.perms & PERMISSIONS.MANAGE_EMPLOYERS) !== 0}
|
||||
<p class="pl-4">Manage employers (of their company)</p>
|
||||
{/if}
|
||||
{/if}
|
||||
{#if (data.user.perms & adminPerms) !== 0}
|
||||
<p class="font-semibold">Admin permissions:</p>
|
||||
{#if (data.user.perms & PERMISSIONS.MANAGE_POSTINGS) !== 0}
|
||||
<p class="pl-4">Manage postings</p>
|
||||
{/if}
|
||||
{#if (data.user.perms & PERMISSIONS.MANAGE_USERS) !== 0}
|
||||
<p class="pl-4">Manage users</p>
|
||||
{/if}
|
||||
{#if (data.user.perms & PERMISSIONS.MANAGE_TAGS) !== 0}
|
||||
<p class="pl-4">Manage tags</p>
|
||||
{/if}
|
||||
{#if (data.user.perms & PERMISSIONS.MANAGE_COMPANIES) !== 0}
|
||||
<p class="pl-4">Manage companies</p>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
120
src/routes/admin/users/[user]/edit/+page.server.ts
Normal file
120
src/routes/admin/users/[user]/edit/+page.server.ts
Normal file
@ -0,0 +1,120 @@
|
||||
import { type Actions, error, fail, redirect } from '@sveltejs/kit';
|
||||
import { deleteUser, getUser, updateUser } from '$lib/db/index.server';
|
||||
import { PERMISSIONS } from '$lib/consts';
|
||||
import type { PageServerLoad } from './$types';
|
||||
import { getUserPerms } from '$lib/index.server';
|
||||
import { userPerms } from '$lib/shared.svelte';
|
||||
|
||||
export const load: PageServerLoad = async ({ cookies, params }) => {
|
||||
const id = parseInt(params.user);
|
||||
const perms = getUserPerms(cookies);
|
||||
if (perms >= 0 && (perms & PERMISSIONS.MANAGE_USERS) > 0) {
|
||||
return {
|
||||
user: await getUser(id)
|
||||
};
|
||||
}
|
||||
error(403, 'Unauthorized');
|
||||
};
|
||||
|
||||
export const actions: Actions = {
|
||||
submit: async ({ request, cookies, params }) => {
|
||||
const id = parseInt(params.user!);
|
||||
const data = await request.formData();
|
||||
const username = data.get('username')?.toString().trim();
|
||||
let password: string | undefined | null = data.get('password')?.toString().trim();
|
||||
const view = data.get('view')?.toString();
|
||||
const apply = data.get('apply')?.toString();
|
||||
const submitPostings = data.get('submitPostings')?.toString();
|
||||
const manageEmployers = data.get('manageEmployers')?.toString();
|
||||
const manageTags = data.get('manageTags')?.toString();
|
||||
const managePostings = data.get('managePostings')?.toString();
|
||||
const manageUsers = data.get('manageUsers')?.toString();
|
||||
const accountActive = data.get('accountActive')?.toString();
|
||||
let email: string | undefined | null = data.get('email')?.toString().trim();
|
||||
let phone: string | undefined | null = data.get('phone')?.toString().trim();
|
||||
let fullName: string | undefined | null = data.get('fullName')?.toString().trim();
|
||||
let companyCode: string | undefined | null = data
|
||||
.get('companyCode')
|
||||
?.toString()
|
||||
.toUpperCase()
|
||||
.trim();
|
||||
|
||||
if (password === '' || password == undefined) password = null;
|
||||
if (email === '' || email == undefined) email = null;
|
||||
if (phone === '' || phone == undefined) phone = null;
|
||||
if (fullName === '' || fullName == undefined) fullName = null;
|
||||
if (companyCode === '' || companyCode == undefined) companyCode = null;
|
||||
|
||||
if (email && !email.includes('@')) {
|
||||
return fail(400, { errorMessage: 'Invalid email' });
|
||||
}
|
||||
|
||||
if (phone && !phone.match(/\((\d{3})\) \d{3}-\d{4}/)) {
|
||||
return fail(400, { errorMessage: 'Invalid phone number' });
|
||||
}
|
||||
|
||||
let newUserPerms = 0;
|
||||
newUserPerms += PERMISSIONS.VIEW * (view === 'on' ? 1 : 0);
|
||||
newUserPerms += PERMISSIONS.APPLY_FOR_JOBS * (apply === 'on' ? 1 : 0);
|
||||
newUserPerms += PERMISSIONS.SUBMIT_POSTINGS * (submitPostings === 'on' ? 1 : 0);
|
||||
newUserPerms += PERMISSIONS.MANAGE_EMPLOYERS * (manageEmployers === 'on' ? 1 : 0);
|
||||
newUserPerms += PERMISSIONS.MANAGE_TAGS * (manageTags === 'on' ? 1 : 0);
|
||||
newUserPerms += PERMISSIONS.MANAGE_POSTINGS * (managePostings === 'on' ? 1 : 0);
|
||||
newUserPerms += PERMISSIONS.MANAGE_USERS * (manageUsers === 'on' ? 1 : 0);
|
||||
newUserPerms += PERMISSIONS.MANAGE_COMPANIES * (manageUsers === 'on' ? 1 : 0);
|
||||
|
||||
const requestPerms = getUserPerms(cookies);
|
||||
if (!(requestPerms >= 0 && (requestPerms & PERMISSIONS.MANAGE_USERS) > 0)) {
|
||||
return fail(403, { errorMessage: 'Unauthorized' });
|
||||
} else {
|
||||
if (((requestPerms | userPerms) & newUserPerms) !== newUserPerms) {
|
||||
return fail(403, {
|
||||
errorMessage: 'Cannot give a user higher permissions than yourself!'
|
||||
});
|
||||
} else {
|
||||
if (username && username !== '') {
|
||||
try {
|
||||
await updateUser(<User>{
|
||||
id: id,
|
||||
username: username,
|
||||
password: password,
|
||||
perms: newUserPerms,
|
||||
active: accountActive === 'on',
|
||||
email: email,
|
||||
phone: phone,
|
||||
fullName: fullName,
|
||||
companyCode: companyCode
|
||||
});
|
||||
} catch (err) {
|
||||
return fail(500, { errorMessage: `Internal Server Error: ${err}` });
|
||||
}
|
||||
return redirect(301, `/admin/users/${id}`);
|
||||
} else {
|
||||
return fail(400, { errorMessage: 'Missing username or password' });
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
delete: async ({ cookies, params }) => {
|
||||
const id = parseInt(params.user!);
|
||||
const userToDelete = await getUser(id);
|
||||
const deletePerms = userToDelete!.perms;
|
||||
const requestPerms = getUserPerms(cookies);
|
||||
if (!(requestPerms >= 0 && (requestPerms & PERMISSIONS.MANAGE_USERS) > 0)) {
|
||||
return fail(403, { errorMessage: 'Unauthorized' });
|
||||
} else {
|
||||
if ((requestPerms & deletePerms) !== deletePerms) {
|
||||
return fail(403, {
|
||||
errorMessage: 'Cannot delete a user with higher permissions than yourself!'
|
||||
});
|
||||
} else {
|
||||
try {
|
||||
await deleteUser(id);
|
||||
} catch (err) {
|
||||
return fail(500, { errorMessage: `Internal Server Error: ${err}` });
|
||||
}
|
||||
return redirect(301, '/admin/users');
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
420
src/routes/admin/users/[user]/edit/+page.svelte
Normal file
420
src/routes/admin/users/[user]/edit/+page.svelte
Normal file
@ -0,0 +1,420 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { enhance } from '$app/forms';
|
||||
import type { PageProps } from './$types';
|
||||
import { PERMISSIONS } from '$lib/consts';
|
||||
import { userPerms, employerPerms, adminPerms, telFormatter } from '$lib/shared.svelte';
|
||||
let permsAccordions: boolean[] = [false, false, false];
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
let acc = document.getElementsByClassName('accordion');
|
||||
for (let i = 0; i < acc.length; i++) {
|
||||
acc[i].addEventListener('click', function (this: HTMLElement, event: Event) {
|
||||
const target = event?.target as HTMLElement | null;
|
||||
if (target?.tagName === 'INPUT' && (target as HTMLInputElement).type === 'checkbox') {
|
||||
return; // Do nothing if it's the checkbox
|
||||
}
|
||||
|
||||
this.classList.toggle('active');
|
||||
permsAccordions[i] = !permsAccordions[i];
|
||||
|
||||
let panel = this.nextElementSibling as HTMLElement;
|
||||
if (panel.style.display === 'block') {
|
||||
panel.style.display = 'none';
|
||||
} else {
|
||||
panel.style.display = 'block';
|
||||
}
|
||||
});
|
||||
|
||||
let selectAllCheckbox = acc[i].querySelector('.select-all') as HTMLInputElement;
|
||||
selectAllCheckbox.addEventListener('change', function () {
|
||||
let checkboxes =
|
||||
this.parentElement!.parentElement!.parentElement!.nextElementSibling!.querySelectorAll(
|
||||
'.permCheckbox'
|
||||
) as NodeListOf<HTMLInputElement>;
|
||||
checkboxes.forEach((checkbox) => {
|
||||
checkbox.checked = selectAllCheckbox.checked;
|
||||
});
|
||||
});
|
||||
|
||||
let permCheckboxes = acc[i].nextElementSibling!.querySelectorAll(
|
||||
'.permCheckbox'
|
||||
) as NodeListOf<HTMLInputElement>;
|
||||
|
||||
permCheckboxes.forEach((checkbox) => {
|
||||
checkbox.addEventListener('change', function () {
|
||||
let allChecked = true;
|
||||
let someChecked = false;
|
||||
permCheckboxes.forEach((cb) => {
|
||||
if (cb.checked) {
|
||||
someChecked = true;
|
||||
} else {
|
||||
allChecked = false;
|
||||
}
|
||||
});
|
||||
selectAllCheckbox.checked = allChecked;
|
||||
selectAllCheckbox.indeterminate = !allChecked && someChecked;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const modal: HTMLElement = document.getElementById('deleteConfirmModal') as HTMLElement;
|
||||
window.onclick = function (event) {
|
||||
if (event.target == modal) {
|
||||
modal.style.display = 'none';
|
||||
}
|
||||
};
|
||||
document.getElementById('phone')?.addEventListener('input', function (this: HTMLInputElement) {
|
||||
this.value = telFormatter(this.value);
|
||||
});
|
||||
});
|
||||
|
||||
function openConfirm() {
|
||||
document.getElementById('deleteConfirmModal')!.style.display = 'block';
|
||||
}
|
||||
|
||||
function closeConfirm() {
|
||||
document.getElementById('deleteConfirmModal')!.style.display = 'none';
|
||||
}
|
||||
|
||||
let { data, form }: PageProps = $props();
|
||||
|
||||
let perms = data.user!.perms;
|
||||
</script>
|
||||
|
||||
<div class="content">
|
||||
<div class="elevated separator-borders m-4 rounded">
|
||||
<div class="bottom-border flex place-content-between">
|
||||
<div class="p-3 font-semibold">Update User</div>
|
||||
</div>
|
||||
<form method="POST" class="px-4" autocomplete="off" use:enhance>
|
||||
<div class="mt-4 text-sm font-semibold">
|
||||
Username <span class="text-red-500">*</span>
|
||||
<input
|
||||
type="text"
|
||||
name="username"
|
||||
id="username"
|
||||
value={data.user?.username}
|
||||
placeholder="Username"
|
||||
class="w-full rounded font-normal"
|
||||
/>
|
||||
</div>
|
||||
<div class="relative pt-4 text-sm font-semibold">
|
||||
Password
|
||||
<input
|
||||
type="password"
|
||||
name="password"
|
||||
id="password"
|
||||
placeholder="New password"
|
||||
class="w-full rounded font-normal"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onclick={showPassword}
|
||||
class="absolute right-2.5 -translate-y-1/2 transform pt-12"
|
||||
>
|
||||
<span class="material-symbols-outlined"
|
||||
>{passwordVisible ? 'visibility' : 'visibility_off'}</span
|
||||
>
|
||||
</button>
|
||||
</div>
|
||||
<div class="mt-4 text-sm font-semibold">
|
||||
Email (optional)
|
||||
<input
|
||||
type="email"
|
||||
name="email"
|
||||
id="email"
|
||||
value={data.user?.email}
|
||||
placeholder="Email"
|
||||
class="w-full rounded font-normal"
|
||||
/>
|
||||
</div>
|
||||
<div class="mt-4 text-sm font-semibold">
|
||||
Phone (optional)
|
||||
<input
|
||||
type="tel"
|
||||
name="phone"
|
||||
id="phone"
|
||||
value={data.user?.phone}
|
||||
placeholder="Phone"
|
||||
class="w-full rounded font-normal"
|
||||
pattern="([0-9]\{3}) [0-9]\{3}-[0-9]\{3}"
|
||||
/>
|
||||
</div>
|
||||
<div class="mt-4 text-sm font-semibold">
|
||||
Full name (optional)
|
||||
<input
|
||||
type="text"
|
||||
name="fullName"
|
||||
id="fullName"
|
||||
value={data.user?.fullName}
|
||||
placeholder="Full name"
|
||||
class="w-full rounded font-normal"
|
||||
/>
|
||||
</div>
|
||||
<div class="mt-4 text-sm font-semibold">
|
||||
Company code (optional)
|
||||
<input
|
||||
type="text"
|
||||
name="companyCode"
|
||||
id="companyCode"
|
||||
placeholder="Company code"
|
||||
value={data.user?.companyCode}
|
||||
class="w-full rounded font-normal"
|
||||
/>
|
||||
</div>
|
||||
<p class="low-emphasis-text">
|
||||
This code can be used to associate an employer with their company. If left blank, they will
|
||||
not be able to create any postings.
|
||||
</p>
|
||||
<div class="bg-color separator-borders mb-2 mt-4 rounded">
|
||||
<button
|
||||
class="accordion flex w-full place-content-between rounded p-2 text-left"
|
||||
type="button"
|
||||
>
|
||||
<span class="flex place-items-center">
|
||||
<span class="ml-1 mr-3"
|
||||
><input
|
||||
type="checkbox"
|
||||
name="userPerms"
|
||||
id="userPerms"
|
||||
class="select-all"
|
||||
checked={(perms & userPerms) === userPerms}
|
||||
indeterminate={(perms & userPerms) !== userPerms && (perms & userPerms) !== 0}
|
||||
/></span
|
||||
>User Permissions
|
||||
</span>
|
||||
<span class="material-symbols-outlined"
|
||||
>{permsAccordions[0] ? 'arrow_drop_up' : 'arrow_drop_down'}</span
|
||||
>
|
||||
</button>
|
||||
<div class="panel hidden p-2">
|
||||
<div>
|
||||
<div class="mb-1">
|
||||
<label for="view" class="flex place-items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="view"
|
||||
id="view"
|
||||
class="permCheckbox mx-1"
|
||||
checked={(perms & PERMISSIONS.VIEW) !== 0}
|
||||
/>
|
||||
<span class="ml-2">View access</span></label
|
||||
>
|
||||
</div>
|
||||
<div class="mb-1">
|
||||
<label for="apply" class="flex place-items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="apply"
|
||||
id="apply"
|
||||
class="permCheckbox mx-1"
|
||||
checked={(perms & PERMISSIONS.APPLY_FOR_JOBS) !== 0}
|
||||
/>
|
||||
<span class="ml-2">Apply for jobs</span></label
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-color separator-borders my-2 rounded">
|
||||
<button
|
||||
class="accordion flex w-full place-content-between rounded p-2 text-left"
|
||||
type="button"
|
||||
>
|
||||
<span class="flex place-items-center">
|
||||
<span class="ml-1 mr-3"
|
||||
><input
|
||||
type="checkbox"
|
||||
name="companyPerms"
|
||||
id="companyPerms"
|
||||
class="select-all"
|
||||
checked={(perms & employerPerms) === employerPerms}
|
||||
indeterminate={(perms & employerPerms) !== employerPerms &&
|
||||
(perms & employerPerms) !== 0}
|
||||
/></span
|
||||
>Company Permissions
|
||||
</span>
|
||||
<span class="material-symbols-outlined"
|
||||
>{permsAccordions[1] ? 'arrow_drop_up' : 'arrow_drop_down'}</span
|
||||
>
|
||||
</button>
|
||||
<div class="panel hidden p-2">
|
||||
<div>
|
||||
<div class="mb-1">
|
||||
<label for="submitPostings" class="flex place-items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="submitPostings"
|
||||
id="submitPostings"
|
||||
checked={(perms & PERMISSIONS.SUBMIT_POSTINGS) !== 0}
|
||||
class="permCheckbox mx-1"
|
||||
/>
|
||||
<span class="ml-2">Submit postings</span></label
|
||||
>
|
||||
</div>
|
||||
<div class="mb-1">
|
||||
<label for="manageEmployers" class="flex place-items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="manageEmployers"
|
||||
id="manageEmployers"
|
||||
checked={(perms & PERMISSIONS.MANAGE_EMPLOYERS) !== 0}
|
||||
class="permCheckbox mx-1"
|
||||
/>
|
||||
<span class="ml-2">Manage employers (within their company)</span></label
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-color separator-borders mt-2 rounded">
|
||||
<button
|
||||
class="accordion flex w-full place-content-between rounded p-2 text-left"
|
||||
type="button"
|
||||
>
|
||||
<span class="flex place-items-center">
|
||||
<span class="ml-1 mr-3"
|
||||
><input
|
||||
type="checkbox"
|
||||
name="adminPerms"
|
||||
id="adminPerms"
|
||||
class="select-all"
|
||||
checked={(perms & adminPerms) === adminPerms}
|
||||
indeterminate={(perms & adminPerms) !== adminPerms && (perms & adminPerms) !== 0}
|
||||
/></span
|
||||
>Admin Permissions
|
||||
</span>
|
||||
<span class="material-symbols-outlined"
|
||||
>{permsAccordions[0] ? 'arrow_drop_up' : 'arrow_drop_down'}</span
|
||||
>
|
||||
</button>
|
||||
<div class="panel hidden p-2">
|
||||
<div>
|
||||
<div class="mb-1">
|
||||
<label for="manageTags" class="flex place-items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="manageTags"
|
||||
id="manageTags"
|
||||
checked={(perms & PERMISSIONS.MANAGE_TAGS) !== 0}
|
||||
class="permCheckbox mx-1"
|
||||
/>
|
||||
<span class="ml-2">Manage tags</span></label
|
||||
>
|
||||
</div>
|
||||
<div class="mb-1">
|
||||
<label for="managePostings" class="flex place-items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="managePostings"
|
||||
id="managePostings"
|
||||
checked={(perms & PERMISSIONS.MANAGE_POSTINGS) !== 0}
|
||||
class="permCheckbox mx-1"
|
||||
/>
|
||||
<span class="ml-2">Manage postings</span></label
|
||||
>
|
||||
</div>
|
||||
<div class="mb-1">
|
||||
<label for="manageUsers" class="flex place-items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="manageUsers"
|
||||
id="manageUsers"
|
||||
checked={(perms & PERMISSIONS.MANAGE_USERS) !== 0}
|
||||
class="permCheckbox mx-1"
|
||||
/>
|
||||
<span class="ml-2">Manage users</span></label
|
||||
>
|
||||
</div>
|
||||
<div class="mb-1">
|
||||
<label for="manageCompanies" class="flex place-items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="manageCompanies"
|
||||
id="manageCompanies"
|
||||
checked={(perms & PERMISSIONS.MANAGE_COMPANIES) !== 0}
|
||||
class="permCheckbox mx-1"
|
||||
/>
|
||||
<span class="ml-2">Manage companies</span></label
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<label for="accountActive" class="flex place-items-center p-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="accountActive"
|
||||
id="accountActive"
|
||||
checked={data.user?.active}
|
||||
class="permCheckbox mx-1"
|
||||
/>
|
||||
<span class="ml-2">Account active</span></label
|
||||
>
|
||||
|
||||
{#if form?.errorMessage}
|
||||
<div class="mb-2 text-red-500">{form.errorMessage}</div>
|
||||
{/if}
|
||||
<div class="flex justify-between">
|
||||
<button
|
||||
class="dull-primary-bg-color mb-4 mt-2 rounded px-2 py-1"
|
||||
type="submit"
|
||||
formaction="?/submit">Update user</button
|
||||
>
|
||||
<button
|
||||
class="danger-bg-color mb-4 mt-2 rounded px-2 py-1"
|
||||
type="button"
|
||||
onclick={openConfirm}>Delete user</button
|
||||
>
|
||||
</div>
|
||||
</form>
|
||||
<form id="deleteConfirmModal" class="modal" method="POST" use:enhance>
|
||||
<div class="modal-content">
|
||||
<div class="mb-2 inline-flex w-full justify-between">
|
||||
<h2 class="font-semibold">Are you sure?</h2>
|
||||
<button class="material-symbols-outlined" onclick={closeConfirm}>close</button>
|
||||
</div>
|
||||
<p>
|
||||
This will permanently delete user <span class="font-semibold">{data.user?.username}.</span
|
||||
>
|
||||
</p>
|
||||
<p>Please type "I understand" into the box below to confirm</p>
|
||||
<input
|
||||
type="text"
|
||||
name="confirm"
|
||||
id="confirm"
|
||||
placeholder="I understand"
|
||||
class="w-full rounded font-normal"
|
||||
pattern="I understand"
|
||||
required
|
||||
/>
|
||||
<div class="mt-4 flex justify-between">
|
||||
<button class="danger-bg-color rounded px-2 py-1" type="submit" formaction="?/delete"
|
||||
>Delete user</button
|
||||
>
|
||||
<button
|
||||
class="separator-borders bg-color rounded px-2 py-1"
|
||||
type="button"
|
||||
onclick={closeConfirm}>Cancel</button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
88
src/routes/admin/users/create/+page.server.ts
Normal file
88
src/routes/admin/users/create/+page.server.ts
Normal file
@ -0,0 +1,88 @@
|
||||
import { type Actions, fail, redirect } from '@sveltejs/kit';
|
||||
import { createUser } from '$lib/db/index.server';
|
||||
import { PERMISSIONS } from '$lib/consts';
|
||||
import { getUserPerms } from '$lib/index.server';
|
||||
|
||||
export const actions: Actions = {
|
||||
submit: async ({ request, cookies }) => {
|
||||
const data = await request.formData();
|
||||
const username = data.get('username')?.toString().trim();
|
||||
const password = data.get('password')?.toString().trim();
|
||||
const view = data.get('view')?.toString();
|
||||
const apply = data.get('apply')?.toString();
|
||||
const submitPostings = data.get('submitPostings')?.toString();
|
||||
const manageEmployers = data.get('manageEmployers')?.toString();
|
||||
const manageTags = data.get('manageTags')?.toString();
|
||||
const managePostings = data.get('managePostings')?.toString();
|
||||
const manageUsers = data.get('manageUsers')?.toString();
|
||||
const accountActive = data.get('accountActive')?.toString();
|
||||
let email: string | undefined | null = data.get('email')?.toString().trim();
|
||||
let phone: string | undefined | null = data.get('phone')?.toString().trim();
|
||||
let fullName: string | undefined | null = data.get('fullName')?.toString().trim();
|
||||
let companyCode: string | undefined | null = data
|
||||
.get('companyCode')
|
||||
?.toString()
|
||||
.toUpperCase()
|
||||
.trim();
|
||||
|
||||
if (email === '' || email == undefined) email = null;
|
||||
if (phone === '' || phone == undefined) phone = null;
|
||||
if (fullName === '' || fullName == undefined) fullName = null;
|
||||
if (companyCode === '' || companyCode == undefined) companyCode = null;
|
||||
|
||||
if (email && !email.includes('@')) {
|
||||
return fail(400, { errorMessage: 'Invalid email' });
|
||||
}
|
||||
|
||||
if (phone && !phone.match(/[0-9]{3}-[0-9]{3}-[0-9]{4}/)) {
|
||||
return fail(400, { errorMessage: 'Invalid phone number' });
|
||||
}
|
||||
|
||||
let newUserPerms = 0;
|
||||
newUserPerms += PERMISSIONS.VIEW * (view === 'on' ? 1 : 0);
|
||||
newUserPerms += PERMISSIONS.APPLY_FOR_JOBS * (apply === 'on' ? 1 : 0);
|
||||
newUserPerms += PERMISSIONS.SUBMIT_POSTINGS * (submitPostings === 'on' ? 1 : 0);
|
||||
newUserPerms += PERMISSIONS.MANAGE_EMPLOYERS * (manageEmployers === 'on' ? 1 : 0);
|
||||
newUserPerms += PERMISSIONS.MANAGE_TAGS * (manageTags === 'on' ? 1 : 0);
|
||||
newUserPerms += PERMISSIONS.MANAGE_POSTINGS * (managePostings === 'on' ? 1 : 0);
|
||||
newUserPerms += PERMISSIONS.MANAGE_USERS * (manageUsers === 'on' ? 1 : 0);
|
||||
newUserPerms += PERMISSIONS.MANAGE_COMPANIES * (manageUsers === 'on' ? 1 : 0);
|
||||
|
||||
const requestPerms = getUserPerms(cookies);
|
||||
if (!(requestPerms >= 0 && (requestPerms & PERMISSIONS.MANAGE_USERS) > 0)) {
|
||||
return fail(403, { errorMessage: 'You cannot preform this action!' });
|
||||
} else {
|
||||
if ((requestPerms & newUserPerms) !== newUserPerms) {
|
||||
return fail(403, {
|
||||
errorMessage: 'Cannot create a user with higher permissions than yourself!'
|
||||
});
|
||||
} else {
|
||||
if (username && password && username !== '' && password !== '') {
|
||||
if (password.length < 8) {
|
||||
return fail(400, { errorMessage: 'Password must be at least 8 characters' });
|
||||
}
|
||||
let id = -1;
|
||||
try {
|
||||
id = await createUser(<User>{
|
||||
username: username,
|
||||
password: password,
|
||||
perms: newUserPerms,
|
||||
active: accountActive === 'on',
|
||||
email: email,
|
||||
phone: phone,
|
||||
fullName: fullName,
|
||||
companyCode: companyCode
|
||||
});
|
||||
} catch (err) {
|
||||
return fail(500, { errorMessage: `Internal Server Error: ${err}` });
|
||||
}
|
||||
if (id !== -1) {
|
||||
return redirect(301, `/admin/users/${id}`);
|
||||
}
|
||||
} else {
|
||||
return fail(400, { errorMessage: 'Missing username or password' });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
338
src/routes/admin/users/create/+page.svelte
Normal file
338
src/routes/admin/users/create/+page.svelte
Normal file
@ -0,0 +1,338 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { enhance } from '$app/forms';
|
||||
import type { PageProps } from './$types';
|
||||
import { telFormatter } from '$lib/shared.svelte';
|
||||
let permsAccordions: boolean[] = [false, false, false];
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
let acc = document.getElementsByClassName('accordion');
|
||||
for (let i = 0; i < acc.length; i++) {
|
||||
acc[i].addEventListener('click', function (this: HTMLElement, event: Event) {
|
||||
const target = event?.target as HTMLElement | null;
|
||||
if (target?.tagName === 'INPUT' && (target as HTMLInputElement).type === 'checkbox') {
|
||||
return; // Do nothing if it's the checkbox
|
||||
}
|
||||
|
||||
this.classList.toggle('active');
|
||||
permsAccordions[i] = !permsAccordions[i];
|
||||
|
||||
let panel = this.nextElementSibling as HTMLElement;
|
||||
if (panel.style.display === 'block') {
|
||||
panel.style.display = 'none';
|
||||
} else {
|
||||
panel.style.display = 'block';
|
||||
}
|
||||
});
|
||||
|
||||
let selectAllCheckbox = acc[i].querySelector('.select-all') as HTMLInputElement;
|
||||
// console.log(selectAllCheckbox);
|
||||
selectAllCheckbox.addEventListener('change', function () {
|
||||
let checkboxes =
|
||||
this.parentElement!.parentElement!.parentElement!.nextElementSibling!.querySelectorAll(
|
||||
'.permCheckbox'
|
||||
) as NodeListOf<HTMLInputElement>;
|
||||
// console.log(checkboxes);
|
||||
checkboxes.forEach((checkbox) => {
|
||||
checkbox.checked = selectAllCheckbox.checked;
|
||||
});
|
||||
});
|
||||
|
||||
let permCheckboxes = acc[i].nextElementSibling!.querySelectorAll(
|
||||
'.permCheckbox'
|
||||
) as NodeListOf<HTMLInputElement>;
|
||||
permCheckboxes.forEach((checkbox) => {
|
||||
checkbox.addEventListener('change', function () {
|
||||
let allChecked = true;
|
||||
let someChecked = false;
|
||||
permCheckboxes.forEach((cb) => {
|
||||
if (cb.checked) {
|
||||
someChecked = true;
|
||||
} else {
|
||||
allChecked = false;
|
||||
}
|
||||
});
|
||||
selectAllCheckbox.checked = allChecked;
|
||||
selectAllCheckbox.indeterminate = !allChecked && someChecked;
|
||||
});
|
||||
});
|
||||
}
|
||||
document.getElementById('phone')?.addEventListener('input', function (this: HTMLInputElement) {
|
||||
this.value = telFormatter(this.value);
|
||||
});
|
||||
});
|
||||
|
||||
let { data, form }: PageProps = $props();
|
||||
</script>
|
||||
|
||||
<div class="content">
|
||||
<div class="elevated separator-borders m-4 rounded">
|
||||
<div class="bottom-border flex place-content-between">
|
||||
<div class="p-3 font-semibold">Create new user</div>
|
||||
</div>
|
||||
<form method="POST" class="px-4" autocomplete="off" use:enhance>
|
||||
<div class="mt-4 text-sm font-semibold">
|
||||
Username <span class="text-red-500">*</span>
|
||||
<input
|
||||
type="text"
|
||||
name="username"
|
||||
id="username"
|
||||
placeholder="Username"
|
||||
class="w-full rounded font-normal"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div class="relative mt-4 text-sm font-semibold">
|
||||
Password <span class="text-red-500">*</span>
|
||||
<input
|
||||
type="password"
|
||||
name="password"
|
||||
id="password"
|
||||
placeholder="Password"
|
||||
class="w-full rounded font-normal"
|
||||
required
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onclick={showPassword}
|
||||
class="absolute right-2.5 -translate-y-1/2 transform pt-12"
|
||||
>
|
||||
<span class="material-symbols-outlined"
|
||||
>{passwordVisible ? 'visibility' : 'visibility_off'}</span
|
||||
>
|
||||
</button>
|
||||
</div>
|
||||
<div class="mt-4 text-sm font-semibold">
|
||||
Email (optional)
|
||||
<input
|
||||
type="email"
|
||||
name="email"
|
||||
id="email"
|
||||
placeholder="Email"
|
||||
class="w-full rounded font-normal"
|
||||
/>
|
||||
</div>
|
||||
<div class="mt-4 text-sm font-semibold">
|
||||
Phone (optional)
|
||||
<input
|
||||
type="tel"
|
||||
name="phone"
|
||||
id="phone"
|
||||
placeholder="Phone"
|
||||
class="w-full rounded font-normal"
|
||||
pattern="[0-9]{3}-[0-9]{3}-[0-9]{4}"
|
||||
/>
|
||||
</div>
|
||||
<div class="mt-4 text-sm font-semibold">
|
||||
Full name (optional)
|
||||
<input
|
||||
type="text"
|
||||
name="fullName"
|
||||
id="fullName"
|
||||
placeholder="Full Name"
|
||||
class="w-full rounded font-normal"
|
||||
/>
|
||||
</div>
|
||||
<div class="mt-4 text-sm font-semibold">
|
||||
Company code (optional)
|
||||
<input
|
||||
type="text"
|
||||
name="companyCode"
|
||||
id="companyCode"
|
||||
placeholder="Company code"
|
||||
class="w-full rounded font-normal"
|
||||
/>
|
||||
</div>
|
||||
<p class="low-emphasis-text">
|
||||
This code can be used to associate an employer with their company. If left blank, they will
|
||||
not be able to create any postings.
|
||||
</p>
|
||||
<div class="bg-color separator-borders mb-2 mt-4 rounded">
|
||||
<button
|
||||
class="accordion flex w-full place-content-between rounded p-2 text-left"
|
||||
type="button"
|
||||
>
|
||||
<span class="flex place-items-center">
|
||||
<span class="ml-1 mr-3"
|
||||
><input
|
||||
type="checkbox"
|
||||
name="userPerms"
|
||||
id="userPerms"
|
||||
class="select-all"
|
||||
indeterminate={true}
|
||||
/></span
|
||||
>User Permissions
|
||||
</span>
|
||||
<span class="material-symbols-outlined"
|
||||
>{permsAccordions[0] ? 'arrow_drop_up' : 'arrow_drop_down'}</span
|
||||
>
|
||||
</button>
|
||||
<div class="panel hidden p-2">
|
||||
<div>
|
||||
<div class="mb-1">
|
||||
<label for="view" class="flex place-items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="view"
|
||||
id="view"
|
||||
checked={true}
|
||||
class="permCheckbox mx-1"
|
||||
/>
|
||||
<span class="ml-2">View access</span></label
|
||||
>
|
||||
</div>
|
||||
<div class="mb-1">
|
||||
<label for="apply" class="flex place-items-center">
|
||||
<input type="checkbox" name="apply" id="apply" class="permCheckbox mx-1" />
|
||||
<span class="ml-2">Apply for jobs</span></label
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-color separator-borders my-2 rounded">
|
||||
<button
|
||||
class="accordion flex w-full place-content-between rounded p-2 text-left"
|
||||
type="button"
|
||||
>
|
||||
<span class="flex place-items-center">
|
||||
<span class="ml-1 mr-3"
|
||||
><input
|
||||
type="checkbox"
|
||||
name="companyPerms"
|
||||
id="companyPerms"
|
||||
class="select-all"
|
||||
/></span
|
||||
>Company Permissions
|
||||
</span>
|
||||
<span class="material-symbols-outlined"
|
||||
>{permsAccordions[1] ? 'arrow_drop_up' : 'arrow_drop_down'}</span
|
||||
>
|
||||
</button>
|
||||
<div class="panel hidden p-2">
|
||||
<div>
|
||||
<div class="mb-1">
|
||||
<label for="submitPostings" class="flex place-items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="submitPostings"
|
||||
id="submitPostings"
|
||||
class="permCheckbox mx-1"
|
||||
/>
|
||||
<span class="ml-2">Submit postings</span></label
|
||||
>
|
||||
</div>
|
||||
<div class="mb-1">
|
||||
<label for="manageEmployers" class="flex place-items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="manageEmployers"
|
||||
id="manageEmployers"
|
||||
class="permCheckbox mx-1"
|
||||
/>
|
||||
<span class="ml-2">Manage employers (within their company)</span></label
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-color separator-borders mt-2 rounded">
|
||||
<button
|
||||
class="accordion flex w-full place-content-between rounded p-2 text-left"
|
||||
type="button"
|
||||
>
|
||||
<span class="flex place-items-center">
|
||||
<span class="ml-1 mr-3"
|
||||
><input type="checkbox" name="adminPerms" id="adminPerms" class="select-all" /></span
|
||||
>Admin Permissions
|
||||
</span>
|
||||
<span class="material-symbols-outlined"
|
||||
>{permsAccordions[0] ? 'arrow_drop_up' : 'arrow_drop_down'}</span
|
||||
>
|
||||
</button>
|
||||
<div class="panel hidden p-2">
|
||||
<div>
|
||||
<div class="mb-1">
|
||||
<label for="manageTags" class="flex place-items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="manageTags"
|
||||
id="manageTags"
|
||||
class="permCheckbox mx-1"
|
||||
/>
|
||||
<span class="ml-2">Manage tags</span></label
|
||||
>
|
||||
</div>
|
||||
<div class="mb-1">
|
||||
<label for="managePostings" class="flex place-items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="managePostings"
|
||||
id="managePostings"
|
||||
class="permCheckbox mx-1"
|
||||
/>
|
||||
<span class="ml-2">Manage postings</span></label
|
||||
>
|
||||
</div>
|
||||
<div class="mb-1">
|
||||
<label for="manageUsers" class="flex place-items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="manageUsers"
|
||||
id="manageUsers"
|
||||
class="permCheckbox mx-1"
|
||||
/>
|
||||
<span class="ml-2">Manage users</span></label
|
||||
>
|
||||
</div>
|
||||
<div class="mb-1">
|
||||
<label for="manageCompanies" class="flex place-items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="manageCompanies"
|
||||
id="manageCompanies"
|
||||
class="permCheckbox mx-1"
|
||||
/>
|
||||
<span class="ml-2">Manage companies</span></label
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<label for="accountActive" class="flex place-items-center p-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="accountActive"
|
||||
id="accountActive"
|
||||
class="permCheckbox mx-1"
|
||||
checked
|
||||
/>
|
||||
<span class="ml-2">Account active</span></label
|
||||
>
|
||||
|
||||
{#if form?.errorMessage}
|
||||
<div class="mb-2 text-red-500">{form.errorMessage}</div>
|
||||
{/if}
|
||||
<button
|
||||
class="dull-primary-bg-color mb-4 mt-2 rounded px-2 py-1"
|
||||
type="submit"
|
||||
formaction="?/submit">Create user</button
|
||||
>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@ -0,0 +1,27 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
onMount(() => {
|
||||
if (document.cookie.includes('jwt=')) {
|
||||
window.location.href = '/account';
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="signin-container place-items-center pt-8">
|
||||
<div class="elevated separator-borders bg content rounded-md p-8">
|
||||
<h1 class=" text-weight-semibold mb-4 text-center">Register</h1>
|
||||
<h3>Are you an applicant or an employer?</h3>
|
||||
<a
|
||||
class="primary-bg-color mb-4 mt-6 block w-full rounded px-2 py-2 text-center"
|
||||
href="/register/user">Applicant</a
|
||||
>
|
||||
<div class="low-emphasis-text text-center text-3xl">OR</div>
|
||||
<a
|
||||
href="/register/employer"
|
||||
class="primary-bg-color mb-2 mt-4 block w-full rounded px-2 py-2 text-center">Employer</a
|
||||
>
|
||||
|
||||
<a href="/signin" class="low-emphasis-text-button">I already have an account.</a>
|
||||
</div>
|
||||
</div>
|
||||
57
src/routes/register/employer/+page.server.ts
Normal file
57
src/routes/register/employer/+page.server.ts
Normal file
@ -0,0 +1,57 @@
|
||||
import * as dotenv from 'dotenv';
|
||||
import { type Actions, type Cookies, fail, redirect } from '@sveltejs/kit';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import { createUser } from '$lib/db/index.server';
|
||||
|
||||
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: '30d' });
|
||||
cookies.set('jwt', JWT, { maxAge, path: '/', httpOnly: false });
|
||||
console.log(cookies.get('jwt'));
|
||||
}
|
||||
|
||||
export const actions: Actions = {
|
||||
register: async ({ request, cookies }) => {
|
||||
const data = await request.formData();
|
||||
const username = data.get('username')?.toString().trim();
|
||||
const password = data.get('password')?.toString().trim();
|
||||
const confirmPassword = data.get('confirmPassword')?.toString().trim();
|
||||
|
||||
if (
|
||||
username &&
|
||||
password &&
|
||||
confirmPassword &&
|
||||
username !== '' &&
|
||||
password !== '' &&
|
||||
confirmPassword !== ''
|
||||
) {
|
||||
if (password.length < 8) {
|
||||
return fail(400, { errorMessage: 'Password must be at least 8 characters' });
|
||||
}
|
||||
if (password === confirmPassword) {
|
||||
try {
|
||||
await createUser(<User>{ username: username, password: password, perms: 3 });
|
||||
} catch (err) {
|
||||
return fail(400, { errorMessage: `Internal Server Error: ${err}` });
|
||||
}
|
||||
setJWT(cookies, username, 1);
|
||||
throw redirect(303, '/');
|
||||
} else {
|
||||
return fail(400, { errorMessage: 'Passwords do not match' });
|
||||
}
|
||||
} else {
|
||||
return fail(400, { errorMessage: 'Missing username or password' });
|
||||
}
|
||||
}
|
||||
};
|
||||
@ -0,0 +1,55 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { enhance } from '$app/forms';
|
||||
import type { PageProps } from './$types';
|
||||
|
||||
onMount(() => {
|
||||
if (document.cookie.includes('jwt=')) {
|
||||
window.location.href = '/account';
|
||||
}
|
||||
});
|
||||
|
||||
// receive form data from server
|
||||
let { data, form }: PageProps = $props();
|
||||
</script>
|
||||
|
||||
<div class="signin-container place-items-center pt-8">
|
||||
<div class="elevated separator-borders bg content rounded-md p-8">
|
||||
<h1 class="text-weight-semibold mb-4 text-center">Register</h1>
|
||||
<p>This is for employers only!</p>
|
||||
<form method="POST" class="arrange-vertically" use:enhance>
|
||||
<input
|
||||
class="input-field my-4 w-full"
|
||||
type="text"
|
||||
placeholder="Username"
|
||||
name="username"
|
||||
required
|
||||
/>
|
||||
<input
|
||||
type="password"
|
||||
class="input-field my-4 w-full"
|
||||
placeholder="Password"
|
||||
name="password"
|
||||
required
|
||||
/>
|
||||
<input
|
||||
type="password"
|
||||
class="input-field mt-4 w-full"
|
||||
placeholder="Confirm password"
|
||||
name="confirmPassword"
|
||||
required
|
||||
/>
|
||||
|
||||
{#if form?.errorMessage}
|
||||
<div class="my-2 text-red-500">{form.errorMessage}</div>
|
||||
{/if}
|
||||
|
||||
<button
|
||||
class="primary-bg-color mt-8 w-full rounded px-2 py-2"
|
||||
type="submit"
|
||||
formaction="?/register">Create account</button
|
||||
>
|
||||
<a href="/signin" class="low-emphasis-text-button mt-2">I already have an account.</a>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
72
src/routes/register/user/+page.server.ts
Normal file
72
src/routes/register/user/+page.server.ts
Normal file
@ -0,0 +1,72 @@
|
||||
import * as dotenv from 'dotenv';
|
||||
import { type Actions, type Cookies, fail, redirect } from '@sveltejs/kit';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import { createUser } from '$lib/db/index.server';
|
||||
|
||||
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: '30d' });
|
||||
cookies.set('jwt', JWT, { maxAge, path: '/', httpOnly: false });
|
||||
console.log(cookies.get('jwt'));
|
||||
}
|
||||
|
||||
export const actions: Actions = {
|
||||
register: async ({ request, cookies }) => {
|
||||
const data = await request.formData();
|
||||
const username = data.get('username')?.toString().trim();
|
||||
const password = data.get('password')?.toString().trim();
|
||||
const confirmPassword = data.get('confirmPassword')?.toString().trim();
|
||||
let email: string | undefined | null = data.get('email')?.toString().trim();
|
||||
let fullName: string | undefined | null = data.get('fullName')?.toString().trim();
|
||||
if (email === '') email = null;
|
||||
if (fullName === '') fullName = null;
|
||||
|
||||
if (email && !email.includes('@')) {
|
||||
return fail(400, { errorMessage: 'Invalid email' });
|
||||
}
|
||||
|
||||
if (
|
||||
username &&
|
||||
password &&
|
||||
confirmPassword &&
|
||||
username !== '' &&
|
||||
password !== '' &&
|
||||
confirmPassword !== ''
|
||||
) {
|
||||
if (password.length < 8) {
|
||||
return fail(400, { errorMessage: 'Password must be at least 8 characters' });
|
||||
}
|
||||
if (password === confirmPassword) {
|
||||
try {
|
||||
await createUser(<User>{
|
||||
username: username,
|
||||
password: password,
|
||||
perms: 3,
|
||||
active: true,
|
||||
email: email,
|
||||
fullName: fullName
|
||||
});
|
||||
} catch (err) {
|
||||
return fail(400, { errorMessage: `Internal Server Error: ${err}` });
|
||||
}
|
||||
setJWT(cookies, username, 1);
|
||||
throw redirect(303, '/');
|
||||
} else {
|
||||
return fail(400, { errorMessage: 'Passwords do not match' });
|
||||
}
|
||||
} else {
|
||||
return fail(400, { errorMessage: 'Missing username or password' });
|
||||
}
|
||||
}
|
||||
};
|
||||
@ -0,0 +1,85 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { enhance } from '$app/forms';
|
||||
import type { PageProps } from './$types';
|
||||
|
||||
onMount(() => {
|
||||
if (document.cookie.includes('jwt=')) {
|
||||
window.location.href = '/account';
|
||||
}
|
||||
});
|
||||
|
||||
// receive form data from server
|
||||
let { data, form }: PageProps = $props();
|
||||
</script>
|
||||
|
||||
<div class="signin-container place-items-center pt-8">
|
||||
<div class="elevated separator-borders bg content rounded-md p-8">
|
||||
<h1 class="text-weight-semibold mb-4 text-center">Register</h1>
|
||||
<p>Create your account. Its free and only takes a minute!</p>
|
||||
<form method="POST" class="arrange-vertically" use:enhance>
|
||||
<div class="mt-4 text-sm font-semibold">
|
||||
Username <span class="text-red-500">*</span>
|
||||
<input
|
||||
type="text"
|
||||
name="username"
|
||||
id="username"
|
||||
placeholder="Username"
|
||||
class="input-field w-full font-normal"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div class="mt-4 text-sm font-semibold">
|
||||
Email (optional)
|
||||
<input
|
||||
type="text"
|
||||
name="email"
|
||||
id="email"
|
||||
placeholder="Email"
|
||||
class="input-field w-full font-normal"
|
||||
/>
|
||||
</div>
|
||||
<div class="mt-4 text-sm font-semibold">
|
||||
Full name (optional)
|
||||
<input
|
||||
type="text"
|
||||
name="fullName"
|
||||
id="fullName"
|
||||
placeholder="Full name"
|
||||
class="input-field w-full font-normal"
|
||||
/>
|
||||
</div>
|
||||
<div class="relative mt-4 text-sm font-semibold">
|
||||
Password <span class="text-red-500">*</span>
|
||||
<input
|
||||
type="password"
|
||||
class="input-field w-full font-normal"
|
||||
placeholder="Password"
|
||||
name="password"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div class="relative mt-4 text-sm font-semibold">
|
||||
Confirm password <span class="text-red-500">*</span>
|
||||
<input
|
||||
type="password"
|
||||
class="input-field w-full font-normal"
|
||||
placeholder="Password"
|
||||
name="confirmPassword"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
{#if form?.errorMessage}
|
||||
<div class="my-2 text-red-500">{form.errorMessage}</div>
|
||||
{/if}
|
||||
|
||||
<button
|
||||
class="primary-bg-color mt-8 w-full rounded px-2 py-2"
|
||||
type="submit"
|
||||
formaction="?/register">Create account</button
|
||||
>
|
||||
<a href="/signin" class="low-emphasis-text-button mt-2">I already have an account.</a>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@ -1,14 +1,15 @@
|
||||
import { checkUserCreds, createUser } from '$lib/db/index.server';
|
||||
import { checkUserCreds, createUser, updateLastSignin } 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) {
|
||||
function setJWT(cookies: Cookies, user: User) {
|
||||
const payload = {
|
||||
username: username,
|
||||
perms: perms
|
||||
username: user.username,
|
||||
perms: user.perms,
|
||||
id: user.id
|
||||
};
|
||||
|
||||
if (process.env.JWT_SECRET === undefined) {
|
||||
@ -18,42 +19,31 @@ function setJWT(cookies: Cookies, username: string, perms: number) {
|
||||
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 });
|
||||
console.log(cookies.get('jwt'));
|
||||
}
|
||||
|
||||
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' });
|
||||
}
|
||||
},
|
||||
|
||||
signin: async ({ request, cookies }) => {
|
||||
const data = await request.formData();
|
||||
const username = data.get('username')?.toString();
|
||||
const password = data.get('password')?.toString();
|
||||
const username = data.get('username')?.toString().trim();
|
||||
const password = data.get('password')?.toString().trim();
|
||||
|
||||
if (username && password) {
|
||||
const perms = await checkUserCreds(username, password);
|
||||
if (username && password && username !== '' && password !== '') {
|
||||
const user: User | null = await checkUserCreds(username, password);
|
||||
|
||||
if (perms === -1) {
|
||||
return fail(401, { errorMessage: 'Invalid username or password' });
|
||||
if (!user) {
|
||||
return fail(400, { errorMessage: 'Invalid username or password' });
|
||||
}
|
||||
|
||||
setJWT(cookies, username, perms);
|
||||
if (!user.active) {
|
||||
return fail(400, {
|
||||
errorMessage:
|
||||
'Account is disabled. Please contact your admin if you think this is a mistake.'
|
||||
});
|
||||
}
|
||||
|
||||
setJWT(cookies, user);
|
||||
await updateLastSignin(username);
|
||||
// redirect to home page
|
||||
// return { perms: perms };
|
||||
throw redirect(303, '/');
|
||||
} else {
|
||||
return fail(400, { errorMessage: 'Missing username or password' });
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
import type { ActionData } from './$types';
|
||||
import type { PageProps } from './$types';
|
||||
import { onMount } from 'svelte';
|
||||
import { enhance } from '$app/forms';
|
||||
|
||||
onMount(() => {
|
||||
if (document.cookie.includes('jwt=')) {
|
||||
@ -22,33 +23,38 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// receive form data from server
|
||||
let form: ActionData;
|
||||
let { data, form }: PageProps = $props();
|
||||
</script>
|
||||
|
||||
<div class="signin-container place-items-center pt-8">
|
||||
<div class="elevated content rounded-md p-8">
|
||||
<h1 class="is-size-3 has-text-weight-semibold my-4">Welcome Back!</h1>
|
||||
<form method="POST" class="arrange-vertically">
|
||||
<input
|
||||
class="input-field my-2 w-full"
|
||||
type="text"
|
||||
placeholder="Username"
|
||||
name="username"
|
||||
required
|
||||
/>
|
||||
<div class="relative w-full">
|
||||
<div class="separator-borders elevated content rounded-md p-8">
|
||||
<h1 class="text-weight-semibold mb-4 text-center">Welcome Back!</h1>
|
||||
<form method="POST" class="arrange-vertically" use:enhance>
|
||||
<div class="bg-color my-2 rounded">
|
||||
<input
|
||||
type={passwordVisible ? 'text' : 'password'}
|
||||
class="input-field my-2 w-full pr-10"
|
||||
placeholder="Password"
|
||||
name="password"
|
||||
class="input-field w-full"
|
||||
type="text"
|
||||
placeholder="Username"
|
||||
name="username"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div class="relative w-full">
|
||||
<div class="bg-color mt-4 rounded">
|
||||
<input
|
||||
type={passwordVisible ? 'text' : 'password'}
|
||||
class="input-field w-full pr-10"
|
||||
placeholder="Password"
|
||||
name="password"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onclick={showPassword}
|
||||
class="absolute right-2.5 top-8 -translate-y-1/2 transform"
|
||||
class="absolute right-2.5 top-6 -translate-y-1/2 transform"
|
||||
>
|
||||
<span class="material-symbols-outlined"
|
||||
>{passwordVisible ? 'visibility' : 'visibility_off'}</span
|
||||
@ -56,33 +62,18 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- TODO: fix -->
|
||||
{#if form?.errorMessage}
|
||||
<div class="has-text-danger my-2">{form.errorMessage}</div>
|
||||
<div class="my-2 text-red-500">{form.errorMessage}</div>
|
||||
{/if}
|
||||
|
||||
<button
|
||||
class="primary-bg-color mt-6 w-full rounded px-2 py-2"
|
||||
class="primary-bg-color mt-8 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>
|
||||
<a href="/register" class="low-emphasis-text-button mt-2"
|
||||
>Don't have an account? Register here.</a
|
||||
>
|
||||
</form>
|
||||
</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>-->
|
||||
|
||||
14
src/routes/utils.client.ts
Normal file
14
src/routes/utils.client.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import { userState } from '$lib/shared.svelte';
|
||||
|
||||
export const getCookieValue = (name: String) =>
|
||||
document.cookie.match('(^|;)\\s*' + name + '\\s*=\\s*([^;]+)')?.pop() || '';
|
||||
|
||||
export function updateUserState() {
|
||||
const JWT = getCookieValue('jwt');
|
||||
if (JWT !== '') {
|
||||
const state = JSON.parse(atob(JWT.split('.')[1]));
|
||||
userState.perms = state.perms;
|
||||
userState.username = state.username;
|
||||
userState.id = state.id;
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user