Skip to content

Commit ef355ba

Browse files
authored
Merge pull request #10 from drewpayment/feat/fix-login-and-registration
refactor(models/employees.ts): add getEmployeeByEmail function to ret…
2 parents 7832d49 + c304154 commit ef355ba

File tree

5 files changed

+178
-151
lines changed

5 files changed

+178
-151
lines changed

src/lib/drizzle/postgres/models/employees.ts

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { drizzleClient as db } from '$lib/drizzle/postgres/client';
22
import { and, eq } from 'drizzle-orm';
33
import { employee, employeeCodes, employeeNotes, employeeProfile } from '../schema';
4-
import type { Employee, EmployeeProfile, InsertEmployee, InsertEmployeeCode, InsertEmployeeNotes, InsertEmployeeProfile, SelectEmployee, SelectEmployeeCode } from '$lib/drizzle/postgres/db.model';
4+
import type { Employee, EmployeeProfile, InsertEmployee, InsertEmployeeCode, InsertEmployeeNotes, InsertEmployeeProfile, SelectEmployee, SelectEmployeeCode, SelectEmployeeProfile } from '$lib/drizzle/postgres/db.model';
55
import { nanoid } from 'nanoid';
66
import { error } from '@sveltejs/kit';
77

@@ -28,6 +28,56 @@ const getEmployees = async (clientId: string, isCommissionable = false): Promise
2828
}
2929
}
3030

31+
/**
32+
* Gets an employee by their email address
33+
*
34+
* @param email - The email address to look up
35+
* @returns Promise containing the Employee object if found, undefined if not found
36+
*/
37+
export const getEmployeeByEmail = async (email: string | undefined): Promise<SelectEmployee & { employeeProfile: SelectEmployeeProfile } | undefined> => {
38+
if (!email) {
39+
return undefined;
40+
}
41+
42+
try {
43+
const result = await db.transaction(async (tx) => {
44+
const profile = await tx.query.employeeProfile.findFirst({
45+
where: (profile, { eq }) => eq(profile.email, email),
46+
}) as SelectEmployeeProfile;
47+
48+
const employee = await tx.query.employee.findFirst({
49+
where: (employee, { eq }) => eq(employee.id, profile.employeeId),
50+
}) as Employee;
51+
52+
return {
53+
...employee,
54+
employeeProfile: profile,
55+
};
56+
});
57+
58+
return result;
59+
} catch (err) {
60+
console.error(err);
61+
return undefined;
62+
}
63+
};
64+
65+
66+
/**
67+
* Gets a list of employees for a given client ID with optional search filtering
68+
*
69+
* @param clientId - The ID of the client to get employees for
70+
* @param page - The page number to return (1-based)
71+
* @param take - The number of records to return per page
72+
* @param search - Optional search string to filter employees by first or last name
73+
* @returns Promise containing array of Employee objects and total count
74+
*
75+
* The search parameter will filter employees where either first name OR last name
76+
* contains the search string (case-insensitive). Only returns commissionable employees.
77+
*
78+
* Returns employee records with their associated profile and active employee codes.
79+
*/
80+
3181
export const searchEmployees = async (clientId: string, page: number, take: number, search: string | undefined): Promise<{ data: Employee[], count: number }> => {
3282
if (!clientId) {
3383
return { data: [] as Employee[], count: 0 };

src/routes/(app)/app/sales/import/+page.svelte

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,10 @@
33
import { createToast } from '$lib/components/Toast.svelte';
44
import { Button, Breadcrumb, BreadcrumbItem, Label, Fileupload, Table, TableBody, TableBodyRow, TableBodyCell, TableHead, TableHeadCell, Select } from 'flowbite-svelte';
55
import type { PageData } from './$types';
6-
import { read, utils } from 'xlsx';
7-
import type { ImportSalesResult } from '$lib/types/sale.model';
86
import type { ActionResult } from '@sveltejs/kit';
9-
import type { InsertSale } from '$lib/types/db.model';
107
import { formatCurrency, formatDate } from '$lib/utils/utils';
8+
import type { InsertSale } from '$lib/drizzle/postgres/db.model';
9+
import type { ImportSalesResult } from '$lib/drizzle/postgres/types/sale.model';
1110
1211
export let data: PageData;
1312
const { campaigns, employees } = data;

src/routes/(auth)/auth/login/+page.svelte

Lines changed: 15 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -39,13 +39,13 @@
3939

4040
<div class="container max-w-xl">
4141
<div class="text-center mb-4">
42-
<h1 class="text-3xl font-bold">Welcome back</h1>
42+
<h1 class="text-3xl font-bold">Welcome</h1>
4343
<p class="text-gray-600 dark:text-gray-100">Login into your account</p>
4444
</div>
4545

46-
<div class="p-8 border border-gray-300 dark:border-gray-600 rounded-xl shadow-sm">
46+
<div class="p-8 border bg-gray-300 dark:bg-gray-800 border-gray-300 dark:border-gray-600 rounded-xl shadow-sm">
4747
<form method="post" action="?/loginUser" use:enhance={submitLoginUser}>
48-
<div>
48+
<div class="mb-4">
4949
<label for="email" class="mb-0.5 text-text-900">Email</label>
5050
<input
5151
type="email"
@@ -57,7 +57,7 @@
5757
<InlineFormNotice feedback={getFeedbackObjectByPath(form?.feedbacks, 'email')} />
5858
</div>
5959

60-
<div>
60+
<div class="mb-4">
6161
<label for="password" class="mb-0.5 text-text-900">Password</label>
6262
<input
6363
type="password"
@@ -67,32 +67,25 @@
6767
/>
6868
<InlineFormNotice feedback={getFeedbackObjectByPath(form?.feedbacks, 'password')} />
6969

70-
<a href="/password-reset" class="text-sm text-right text-blue-600 underline"
70+
<a href="/password-reset" class="text-right text-blue-600 underline pt-2"
7171
>Forgot your password?</a
7272
>
7373
</div>
7474

7575
<SubmitButton {running} text="Login" />
7676
</form>
7777

78-
<div class="flex flex-col mt-12 space-y-4 social-logins">
79-
<!-- <a href="/oauth/github" class="rounded-md bg-gray-900 p-2.5 text-sm font-semibold text-white shadow-sm hover:bg-gray-800 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-600 flex justify-around items-center">Login with GitHub</a> -->
80-
<a
81-
href="/oauth/google"
82-
class="rounded-md bg-primary-600 dark:bg-primary-300 p-2.5 text-sm font-semibold text-white shadow-sm hover:bg-primary-500 dark:hover:bg-primary-300
83-
focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-600 flex justify-around items-center"
84-
>
85-
Login with Google
86-
</a>
78+
<div class="flex flex-col mt-6 space-y-4 social-logins">
79+
<div class="text-center">
80+
<p class="text-gray-600">
81+
First time here? <a
82+
href="/auth/signup"
83+
class="font-medium text-blue-600 underline">Start here</a
84+
>
85+
</p>
86+
</div>
8787
</div>
8888
</div>
8989
</div>
9090

91-
<div class="text-center">
92-
<p class="text-sm text-gray-600">
93-
You don't have an account yet? <a
94-
href="/auth/signup"
95-
class="font-medium text-blue-600 underline">Create one</a
96-
>
97-
</p>
98-
</div>
91+
Lines changed: 77 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,21 @@
1-
import { generateEmailVerificationToken } from '$lib/drizzle/postgres/models/tokens';
2-
import { createUser } from '$lib/drizzle/postgres/models/users';
3-
import { sendEmail } from '$lib/emails/send';
1+
import { createUser, getUserByEmail } from '$lib/drizzle/postgres/models/users';
42
import { getFeedbackObjects } from '$lib/utils/utils';
5-
import { fail, redirect } from '@sveltejs/kit';
3+
import { fail } from '@sveltejs/kit';
64
import { nanoid } from 'nanoid';
75
import { z } from 'zod';
86
import type { Actions } from './$types';
97
import { Argon2id } from 'oslo/password';
10-
import type { InsertUser, InsertUserKey, InsertUserProfile } from '$lib/drizzle/postgres/db.model';
11-
import { lucia } from '$lib/lucia/postgres';
12-
8+
import type { InsertUser, InsertUserKey, InsertUserProfile, SelectEmployee } from '$lib/drizzle/postgres/db.model';
9+
import { AuthUtils } from '$lib/utils/auth';
10+
import { dev } from '$app/environment';
11+
import { getEmployeeByEmail } from '$lib/drizzle/postgres/models/employees';
1312

1413
const signupUserSchema = z.object({
15-
firstName: z.string().optional(),
16-
lastName: z.string().optional(),
17-
email: z.string().email(),
18-
password: z.string().min(1)
14+
email: z.string().email()
1915
});
2016

2117
export const actions: Actions = {
22-
signupUser: async ({ locals, request, url, cookies }) => {
18+
signupUser: async ({ request, url }) => {
2319
const formData = Object.fromEntries(await request.formData());
2420
const signupUser = signupUserSchema.safeParse(formData);
2521

@@ -40,85 +36,54 @@ export const actions: Actions = {
4036
});
4137
}
4238

43-
const { firstName, lastName, email, password: inputPassword } = signupUser.data;
39+
const { email } = signupUser.data;
4440

45-
try {
46-
const insertUser = {
47-
id: nanoid(),
48-
email,
49-
} as InsertUser;
50-
51-
const hashedPassword = await new Argon2id().hash(inputPassword);
52-
53-
const insertUserKey = {
54-
id: nanoid(),
55-
userId: insertUser.id,
56-
hashedPassword,
57-
} as InsertUserKey;
41+
try {
42+
// Check if user exists
43+
const existingEmployee = await getEmployeeByEmail(email);
5844

59-
const insertUserProfile = {
60-
id: nanoid(),
61-
userId: insertUser.id,
62-
firstName,
63-
lastName,
64-
clientId: 'default',
65-
role: 'user',
66-
} as InsertUserProfile;
45+
if (existingEmployee) {
46+
let userId: string;
47+
const user = await getUserByEmail(email);
6748

68-
const result = await createUser(insertUser, insertUserKey, insertUserProfile);
69-
70-
if (!result.success) {
71-
return fail(500, {
72-
feedbacks: [
49+
if (!user) {
50+
userId = await createNewUserFromEmployee(email, existingEmployee);
51+
} else {
52+
userId = user.id;
53+
}
54+
55+
// User exists, send password reset
56+
const failure = await AuthUtils.sendPasswordResetLink(url.origin, email, userId);
57+
58+
if (failure) {
59+
throw new Error(failure.data.feedbacks[0].message);
60+
}
61+
62+
return {
63+
feedbacks: getFeedbackObjects([
7364
{
74-
type: 'error',
75-
title: 'Error creating user',
76-
message: 'An error occurred while creating your account. Please try again.'
65+
type: 'success',
66+
title: 'Password Reset Link Sent',
67+
message: 'If an account exists with this email, you will receive a password reset link.'
7768
}
78-
]
79-
});
69+
])
70+
};
8071
}
8172

82-
const session = await lucia.createSession(insertUser.id, {});
83-
const sessionCookie = lucia.createSessionCookie(session.id);
84-
85-
// Set session cookie
86-
cookies.set(sessionCookie.name, sessionCookie.value, {
87-
path: '.',
88-
...sessionCookie.attributes,
89-
});
90-
91-
// Send verification email
92-
const verificationToken = await generateEmailVerificationToken(insertUser.id);
93-
94-
const sender = 'Stacks <drew@verostack.dev>';
95-
const recipient = firstName ? `${firstName}` : email;
96-
const emailHtml = `Hello ${recipient},
97-
<br><br>
98-
Thank you for signing up to Stacks! Please click the link below to verify your email address:
99-
<br><br>
100-
<a href="${url.origin}/app/email-verification/${verificationToken}">Verify Email Address</a>
101-
<br>
102-
You can also copy directly into your browser:
103-
<br><br>
104-
<code>${url.origin}/app/email-verification/${verificationToken}</code>
105-
<br><br>
106-
Thanks,
107-
<br>
108-
Drew from Stacks`;
109-
110-
const signupEmail = await sendEmail({
111-
from: sender,
112-
to: email,
113-
subject: 'Verify Your Email Address',
114-
html: emailHtml
115-
});
116-
117-
if (signupEmail[0].type === 'error') {
118-
return fail(500, {
119-
feedbacks: signupEmail
120-
});
73+
if (dev) {
74+
console.log('User does not exist, sending email verification link');
12175
}
76+
77+
// If user doesn't exist, return same message to avoid email enumeration
78+
return {
79+
feedbacks: getFeedbackObjects([
80+
{
81+
type: 'success',
82+
title: 'Password Reset Link Sent',
83+
message: 'If an account exists with this email, you will receive a password reset link.'
84+
}
85+
])
86+
};
12287
} catch (e) {
12388
const feedbacks = getFeedbackObjects([
12489
{
@@ -132,7 +97,34 @@ export const actions: Actions = {
13297
feedbacks
13398
});
13499
}
135-
136-
redirect(302, '/app/email-verification');
137100
}
138101
};
102+
103+
const createNewUserFromEmployee = async (email: string, existingEmployee: SelectEmployee) => {
104+
// Create user record for existing employee
105+
const userId = nanoid();
106+
const newUser: InsertUser = {
107+
id: userId,
108+
email: email,
109+
emailVerified: false
110+
};
111+
112+
const newUserKey: InsertUserKey = {
113+
id: `email:${email}`,
114+
userId: userId,
115+
hashedPassword: await new Argon2id().hash(nanoid())
116+
};
117+
118+
const newUserProfile: InsertUserProfile = {
119+
id: nanoid(),
120+
userId: userId,
121+
clientId: existingEmployee.clientId,
122+
role: 'user',
123+
firstName: existingEmployee.firstName,
124+
lastName: existingEmployee.lastName
125+
};
126+
127+
await createUser(newUser, newUserKey, newUserProfile);
128+
129+
return userId;
130+
}

0 commit comments

Comments
 (0)