dev
ci / docker_image (push) Successful in 1m32s
ci / deploy (push) Successful in 16s

This commit is contained in:
2025-01-26 19:12:15 -06:00
parent 76c2680c60
commit be83b7570d
43 changed files with 3202 additions and 1022 deletions
+6 -5
View File
@@ -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
};
+166 -74
View File
@@ -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
View 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
View File
@@ -1 +0,0 @@
// place files you want to import through the `$lib` alias in this folder.
+21 -1
View File
@@ -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;
}
+18 -3
View File
@@ -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;
}