import bcrypt from 'bcrypt'; import sql from '$lib/db/db.server'; import { error } from '@sveltejs/kit'; import { deleteLogo, saveAvatar, saveLogo } from '$lib/index.server'; import { EmploymentType, type User, type Company, type Tag, type Posting, type Application } from '$lib/types'; import { sendEmployerNotificationEmail } from '$lib/emailer.server'; import fs from 'fs'; import path from 'path'; export async function createUser(user: User): Promise { 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, 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 updateUser(user: User): Promise { 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}, ${user.perms !== undefined ? sql`perms = ${user.perms},` : sql``} ${user.active !== undefined ? sql`active = ${user.active},` : sql``} ${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 { const [user] = await sql` SELECT id, username, password_hash, perms, active, company_id AS "companyId" FROM users WHERE username = ${username} `; if (!user) { return null; } if (await bcrypt.compare(password, user.password_hash)) { delete user.password_hash; return user; } return null; } // should require MANAGE_USERS permission export async function getUsers(searchQuery: string | null = null): Promise { 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 (users); } export async function getCompanies(searchQuery: string | null = null): Promise { return sql` SELECT id, name, description, website, company_code AS "companyCode", created_at AT TIME ZONE 'UTC' AS "createdAt" FROM companies WHERE name ILIKE ${searchQuery ? `%${searchQuery}%` : '%'}; `; } // should require MANAGE_USERS permission export async function getUser(id: number): Promise { 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; } export async function getUserWithCompany(id: number): Promise { 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; } export async function getUserWithCompanyAndApplications( id: number ): Promise<{ user: User; applications: Application[] }> { const data = await sql` WITH company_data AS ( SELECT id, name, description, website, created_at AT TIME ZONE 'UTC' AS "createdAt" FROM companies WHERE id = (SELECT company_id FROM users WHERE id = ${id}) ), user_data AS ( SELECT id, username, perms, email, phone, full_name AS "fullName", created_at AT TIME ZONE 'UTC' AS "createdAt", last_signin AT TIME ZONE 'UTC' AS "lastSignIn", active FROM users WHERE "id" = ${id} ), application_data AS ( SELECT id, posting_id AS "postingId", (SELECT title FROM postings WHERE id = posting_id) AS "postingTitle", created_at AT TIME ZONE 'UTC' AS "createdAt" FROM applications WHERE "user_id" = ${id} ) SELECT ( SELECT row_to_json(company_data) FROM company_data ) AS company, ( SELECT row_to_json(user_data) FROM user_data ) AS user, ( SELECT json_agg(row_to_json(application_data)) FROM application_data ) AS applications; `; if (!data) { error(404, 'User not found'); } let user = data[0].user; user.company = data[0].company; user.createdAt = new Date(user.createdAt); user.lastSignIn = new Date(user.lastSignIn); if (user.company) { user.company.createdAt = new Date(user.company.createdAt); } let applications = data[0].applications; if (applications) { applications.forEach((application: { createdAt: string | number | Date }) => { application.createdAt = new Date(application.createdAt); }); } return { user: user, applications: applications }; } // should require MANAGE_USERS permission export async function deleteUser(id: number): Promise { await sql` DELETE FROM users WHERE id = ${id}; `; } // should require MANAGE_TAGS permission export async function getTags(searchQuery: string | null): Promise { return sql` SELECT id, display_name as "displayName", created_at AT TIME ZONE 'UTC' AS "createdAt" FROM tags WHERE display_name ILIKE ${searchQuery ? `%${searchQuery}%` : '%'}; `; } export async function updateLastSignin(username: string): Promise { await sql` UPDATE users SET last_signin = NOW() WHERE username = ${username}; `; } export async function createCompany(company: Company): Promise { const response = await sql` INSERT INTO companies (name, description, website, created_at, company_code) VALUES (${company.name}, ${company.description}, ${company.website}, NOW(), generate_company_code(CAST(CURRVAL('companies_id_seq') AS INT))) RETURNING id; `; await saveLogo(company); return response[0].id; } export async function editCompany(company: Company): Promise { const response = await sql` UPDATE companies SET name = ${company.name}, description = ${company.description}, website = ${company.website} WHERE id = ${company.id} RETURNING id; `; await saveLogo(company); return response[0].id; } export async function deleteCompany(id: number): Promise { await sql` DELETE FROM companies WHERE id = ${id}; `; await deleteLogo({ id: id }); } export async function getCompany(id: number): Promise { const [company] = await sql` SELECT id, name, description, website, created_at AS "createdAt", company_code AS "companyCode" FROM companies WHERE id = ${id}; `; if (!company) { error(404, 'Company not found'); } return company; } export async function getCompanyFullData( id: number ): Promise<{ company: Company; users: User[]; postings: Posting[] }> { const data = await sql` WITH company_data AS ( SELECT id, name, description, website, created_at AT TIME ZONE 'UTC' AS "createdAt" FROM companies WHERE id = ${id} ), user_data AS ( SELECT id, username, email, phone, full_name AS "fullName" FROM users WHERE "company_id" = ${id} ), posting_data AS ( SELECT id, title, description, employer_id AS "employerId", address, employment_type AS "employmentType", wage, link, tag_ids AS "tagIds", created_at AT TIME ZONE 'UTC' AS "createdAt", updated_at AT TIME ZONE 'UTC' AS "updatedAt", flyer_link AS "flyerLink" FROM postings WHERE "company_id" = ${id} ) SELECT ( SELECT row_to_json(company_data) FROM company_data ) AS company, ( SELECT json_agg(row_to_json(user_data)) FROM user_data ) AS users, ( SELECT json_agg(row_to_json(posting_data)) FROM posting_data ) AS postings; `; if (!data) { error(404, 'Company not found'); } if (data[0].company) { data[0].company.createdAt = new Date(data[0].company.createdAt); } if (data[0].postings) { data[0].postings.forEach( (posting: { createdAt: string | number | Date; updatedAt: string | number | Date; tagIds: number[] | undefined; tags: { id: number; displayName: null; createdAt: null }[]; }) => { posting.createdAt = new Date(posting.createdAt); posting.updatedAt = new Date(posting.updatedAt); if (posting.tagIds) { posting.tagIds?.forEach((tagId: number) => { posting.tags.push({ id: tagId, displayName: null, createdAt: null }); }); } delete posting.tagIds; } ); } return { company: data[0].company, users: data[0].users, postings: data[0].postings }; } export async function createPosting(posting: Posting): Promise { if (posting.tagIds === null || posting.tagIds === undefined) { posting.tagIds = []; } posting.tags?.forEach((tag) => { posting.tagIds?.push(tag.id); }); if (posting.companyId === null || posting.companyId === undefined) { if (posting.company) { posting.companyId = posting.company.id; } else { posting.companyId = null; } } const response = await sql` INSERT INTO postings (title, description, employer_id, address, employment_type, wage, link, tag_ids, created_at, updated_at, flyer_link, company_id) VALUES (${posting.title}, ${posting.description}, ${posting.employerId}, ${posting.address}, ${posting.employmentType}, ${posting.wage}, ${posting.link}, ${posting.tagIds}, NOW(), NOW(), ${posting.flyerLink}, ${posting.companyId}) RETURNING id; `; return response[0].id; } export async function editPosting(posting: Posting): Promise { if (posting.tagIds === null || posting.tagIds === undefined) { posting.tagIds = []; } posting.tags?.forEach((tag) => { posting.tagIds?.push(tag.id); }); if (posting.companyId === null || posting.companyId === undefined) { if (posting.company) { posting.companyId = posting.company.id; } else { posting.companyId = null; } } const response = await sql` UPDATE postings SET title = ${posting.title}, description = ${posting.description}, employer_id = ${posting.employerId}, address = ${posting.address}, employment_type = ${posting.employmentType}, wage = ${posting.wage}, link = ${posting.link}, tag_ids = ${posting.tagIds}, updated_at = NOW(), flyer_link = ${posting.flyerLink}, company_id = ${posting.companyId} WHERE id = ${posting.id} RETURNING id; `; return response[0].id; } export async function deletePosting(id: number): Promise { await sql` DELETE FROM postings WHERE id = ${id}; `; } export async function getCompanyEmployers( id: number ): Promise<{ company: Company; users: User[] }> { const data = await sql` WITH company_data AS ( SELECT id, name, description, website, created_at AT TIME ZONE 'UTC' AS "createdAt", company_code AS "companyCode" FROM companies WHERE id = ${id} ), user_data AS (SELECT id, username, email, phone, full_name AS "fullName", created_at AT TIME ZONE 'UTC' AS "createdAt", last_signin AT TIME ZONE 'UTC' AS "lastSignIn", company_id as "companyId" FROM users WHERE "company_id" = ${id}) SELECT ( SELECT row_to_json(company_data) FROM company_data ) AS company, ( SELECT json_agg(row_to_json(user_data)) FROM user_data ) AS users; `; if (!data) { error(404, 'Company not found'); } if (data[0].users) { data[0].users.forEach( (user: { company: { id: any }; companyId: any; createdAt: string | number | Date; lastSignIn: string | number | Date; }) => { user.company = { id: user.companyId }; user.createdAt = new Date(user.createdAt); user.lastSignIn = new Date(user.lastSignIn); delete user.companyId; } ); } return { company: data[0].company, users: data[0].users }; } export async function getCompanyEmployersAndRequests( id: number ): Promise<{ company: Company; employers: User[]; requests: User[] }> { const data = await sql` WITH company_data AS ( SELECT id, name, description, website, created_at AT TIME ZONE 'UTC' AS "createdAt", company_code AS "companyCode" FROM companies WHERE id = ${id} ), employer_data AS (SELECT id, username, email, phone, full_name AS "fullName", created_at AT TIME ZONE 'UTC' AS "createdAt", last_signin AT TIME ZONE 'UTC' AS "lastSignIn", company_id as "companyId" FROM users WHERE "company_id" = ${id}), request_data AS (SELECT id, username, email, phone, full_name AS "fullName", created_at AT TIME ZONE 'UTC' AS "createdAt", last_signin AT TIME ZONE 'UTC' AS "lastSignIn", company_id as "companyId" FROM users WHERE "company_code" = (SELECT company_code FROM companies WHERE id = ${id})) SELECT ( SELECT row_to_json(company_data) FROM company_data ) AS company, ( SELECT json_agg(row_to_json(employer_data)) FROM employer_data ) AS employers, ( SELECT json_agg(row_to_json(request_data)) FROM request_data ) AS requests; `; if (!data) { error(404, 'Company not found'); } if (data[0].employers) { data[0].employers.forEach( (user: { company: { id: any }; companyId: any; createdAt: string | number | Date; lastSignIn: string | number | Date; }) => { user.company = { id: user.companyId }; user.createdAt = new Date(user.createdAt); user.lastSignIn = new Date(user.lastSignIn); delete user.companyId; } ); } if (data[0].requests) { data[0].requests.forEach( (user: { company: { id: any }; companyId: any; createdAt: string | number | Date; lastSignIn: string | number | Date; }) => { user.company = { id: user.companyId }; user.createdAt = new Date(user.createdAt); user.lastSignIn = new Date(user.lastSignIn); delete user.companyId; } ); } return { company: data[0].company, employers: data[0].employers, requests: data[0].requests }; } export async function removeEmployerFromCompany(companyId: number, userId: number): Promise { await sql` UPDATE users SET company_id = NULL, company_code = NULL WHERE id = ${userId}; `; } export async function addEmployerToCompany(companyId: number, userId: number): Promise { await sql` UPDATE users SET company_id = ${companyId} WHERE id = ${userId}; `; } export async function getPostings(searchQuery: string | null = null): Promise { const postings = await sql` SELECT p.id, p.title, p.description, p.employer_id AS "employerId", p.address, p.employment_type AS "employmentType", p.wage, p.link, p.tag_ids AS "tagIds", p.created_at AT TIME ZONE 'UTC' AS "createdAt", p.updated_at AT TIME ZONE 'UTC' AS "updatedAt", p.flyer_link AS "flyerLink", p.company_id AS "companyId", c.name AS "companyName" FROM postings p LEFT JOIN companies c ON p.company_id = c.id WHERE title ILIKE ${searchQuery ? `%${searchQuery}%` : '%'}; `; postings.forEach((posting) => { posting.company = {}; if (posting.companyName) { posting.company.name = posting.companyName; } delete posting.companyName; posting.tags = []; posting.employmentType = EmploymentType[posting.employmentType as keyof typeof EmploymentType]; if (posting.tagIds) { posting.tagIds?.forEach((tagId: number) => { posting.tags.push({ id: tagId, displayName: null, createdAt: null }); }); } delete posting.tagIds; }); return (postings); } export async function getPosting(id: number): Promise { const data = await sql` SELECT id, title, description, employer_id AS "employerId", address, employment_type AS "employmentType", wage, link, tag_ids AS "tagIds", created_at AT TIME ZONE 'UTC' AS "createdAt", updated_at AT TIME ZONE 'UTC' AS "updatedAt", flyer_link AS "flyerLink", company_id AS "companyId" FROM postings WHERE id = ${id}; `; const posting = data[0]; posting.tags = []; posting.employmentType = EmploymentType[posting.employmentType as keyof typeof EmploymentType]; if (posting.tagIds) { posting.tagIds?.forEach((tagId: number) => { posting.tags.push({ id: tagId, displayName: null, createdAt: null }); }); } delete posting.tagIds; if (posting.createdAt) { posting.createdAt = new Date(posting.createdAt); } if (posting.updatedAt) { posting.updatedAt = new Date(posting.updatedAt); } return posting; } export async function getPostingWithCompanyUser(id: number): Promise { const data = await sql` WITH company_data AS ( SELECT id, name, description, website, created_at AS "createdAt" FROM companies WHERE id = (SELECT company_id FROM postings WHERE id = ${id}) ), user_data AS ( SELECT username, email, phone, full_name AS "fullName" FROM users WHERE "company_id" = (SELECT company_id FROM postings WHERE id = ${id}) ), posting_data AS ( SELECT id, title, description, employer_id AS "employerId", address, employment_type AS "employmentType", wage, link, tag_ids AS "tagIds", created_at AT TIME ZONE 'UTC' AS "createdAt", updated_at AT TIME ZONE 'UTC' AS "updatedAt", flyer_link AS "flyerLink" FROM postings WHERE id = ${id} ) SELECT ( SELECT row_to_json(company_data) FROM company_data ) AS company, ( SELECT row_to_json(user_data) FROM user_data ) AS user, ( SELECT row_to_json(posting_data) FROM posting_data ) AS posting; `; if (!data) { error(404, 'Posting not found'); } let posting = data[0].posting; posting.company = data[0].company; posting.employer = data[0].user; if (posting.createdAt) { posting.createdAt = new Date(posting.createdAt); } if (posting.updatedAt) { posting.updatedAt = new Date(posting.updatedAt); } return posting; } export async function createApplication(application: Application): Promise { const response = await sql` INSERT INTO applications (posting_id, user_id, candidate_statement, created_at) VALUES (${application.postingId}, ${application.userId}, ${application.candidateStatement}, NOW()) RETURNING id; `; sendEmployerNotificationEmail(application.postingId).catch((err) => { console.error('Failed to send employer notification email: ', err); }); return response[0].id; } export async function deleteApplication(id: number): Promise { const response = await sql` DELETE FROM applications WHERE id = ${id}; `; } export async function deleteApplicationWithUser( applicationId: number, userId: number ): Promise { console.log(applicationId, userId); const response = await sql` DELETE FROM applications WHERE id = ${applicationId} AND user_id = ${userId}; `; } export async function getApplications(postingId: number): Promise { const data = await sql` SELECT a.id, a.candidate_statement AS "candidateStatement", a.created_at AS "createdAt", u.id AS "userId", u.username, u.email, u.phone, u.full_name AS "fullName" FROM applications a JOIN users u ON a.user_id = u.id WHERE a.posting_id = ${postingId}; `; data.forEach((application) => { application.createdAt = new Date(application.createdAt); application.user = { id: application.userId, username: application.username, email: application.email, phone: application.phone, fullName: application.fullName, resume: fs.existsSync( path.join(process.cwd(), 'static', 'uploads', 'resumes', `${application.userId}.pdf`) ) }; delete application.userId; delete application.username; delete application.email; delete application.phone; delete application.fullName; }); return (data); } export async function setUserCompanyId(userId: number, companyId: number): Promise { await sql` UPDATE users SET company_id = ${companyId}, company_code = (SELECT company_code FROM companies WHERE id = ${companyId}) WHERE id = ${userId}; `; } export async function getNotificationInfo( postingId: number ): Promise<{ title: string; emails: string[] }> { const data = await sql` WITH posting_data AS ( SELECT title, company_id FROM postings WHERE id = ${postingId} ), user_emails AS ( SELECT email FROM users WHERE company_id = (SELECT company_id FROM posting_data) ) SELECT (SELECT title FROM posting_data) AS title, (SELECT json_agg(email) FROM user_emails) AS emails; `; if (!data || !data[0]) { error(404, 'Posting not found'); } return { title: data[0].title, emails: data[0].emails }; }