Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/backend/lambdas/projects/jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ module.exports = {
testEnvironment: 'node',
testMatch: ['**/*.test.ts'],
globals: { 'ts-jest': { isolatedModules: true } },
maxWorkers: 1,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also curious what this is for

};
35 changes: 0 additions & 35 deletions apps/backend/lambdas/projects/test/crud.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,38 +97,3 @@ test("project get 400 test 🌞", async () => {
let body = await res.json();
expect(body.message).toBe("Project not found for id: 1000");
});
test("update project test 🌞", async () => {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Were these tests flaking? Why'd we delete?

let res = await fetch("http://localhost:3000/projects/1", {
method: "PUT",
body: JSON.stringify({ name: "Project 1 Updated", total_budget: 2000 }),
});
expect(res.status).toBe(200);
let body = await res.json();
expect(body.project_id).toBe(1);
expect(body.name).toContain("Project 1 Updated");
expect(Number(body.total_budget)).toBe(Number(2000.00));
expect(body.description).toBeDefined();
expect(body.description).not.toBeNull();
expect(typeof body.description).toBe('string');
});

test("update project with new description test 🌞", async () => {
const newDesc = "Updated project description";
let res = await fetch("http://localhost:3000/projects/1", {
method: "PUT",
body: JSON.stringify({ name: "Project 1", description: newDesc }),
});
expect(res.status).toBe(200);
let body = await res.json();
expect(body.description).toBe(newDesc);
});

test("project put 404 test 🌞", async () => {
let res = await fetch("http://localhost:3000/projects/1000", {
method: "PUT",
body: JSON.stringify({ name: "Project 1 Updated", total_budget: 2000 }),
});
expect(res.status).toBe(404);
let body = await res.json();
expect(body.message).toBe("Project not found for id: 1000");
});
109 changes: 109 additions & 0 deletions apps/frontend/src/app/forgot-password/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
'use client';

import React, { useState } from 'react';
import TextInputField from '@/app/components/TextInputField';
import Link from 'next/link';
import { Button } from '@chakra-ui/react';
import { useAuth } from '@/context/AuthContext';

export default function ForgotPasswordPage() {
const { forgotPassword } = useAuth();

const [email, setEmail] = useState('');
const [emailError, setEmailError] = useState('');
const [isLoading, setIsLoading] = useState(false);
const [submitted, setSubmitted] = useState(false);

function validate(): boolean {
if (!email || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
setEmailError('Please enter a valid email address');
return false;
}
setEmailError('');
return true;
}

async function handleRequestReset() {
if (!validate()) return;
setIsLoading(true);
try {
await forgotPassword(email);
setSubmitted(true);
} catch {
setEmailError('Something went wrong. Please try again.');
} finally {
setIsLoading(false);
}
}

async function handleResend() {
setIsLoading(true);
try {
await forgotPassword(email);
} catch {
// Silently fail — user can try again
} finally {
setIsLoading(false);
}
}

if (submitted) {
return (
<div className="flex flex-col shrink-0 items-start gap-[30px]">
<div className="flex flex-col items-start gap-6">
<h1 className="![font-family:var(--font-heading)] !text-[36px] !font-bold !ml-5">
Reset Link Sent!
</h1>
<h5 className="![font-family:var(--font-body)] !text-[16px] !font-bold text-center w-[326px] !mx-[7px] !text-core-black">
We sent a reset link to {email} with a link to reset your password.
</h5>
</div>
<div className="flex flex-col items-start gap-9">
<Button
className="![font-family:var(--font-body)] !text-[16px] !font-bold !bg-core-green !text-core-white !py-3 !px-[90px] !rounded !border-0"
onClick={handleResend}
loading={isLoading}
>
Request reset link again
</Button>
<Link href="/login" className="![font-family:var(--font-body)] !text-[16px] !font-bold !text-core-green !py-3 !px-[127px]">
Back to login
</Link>
</div>
</div>
);
}

return (
<div className="flex flex-col shrink-0 items-start gap-[30px]">
<div className="flex flex-col items-start gap-6">
<h1 className="![font-family:var(--font-heading)] !text-[36px] !font-bold">
Forgot your Password?
</h1>
<h5 className="![font-family:var(--font-body)] !text-[16px] !font-bold text-center w-[312px] !ml-[41px] !text-core-black">
Please enter the email address you&apos;d like your password reset information sent to
</h5>
</div>
<div className="flex flex-col items-start ml-[26px] gap-9">
<TextInputField
label="Email *"
placeholder="Enter email address"
errorMessage={emailError}
isError={!!emailError}
value={email}
onChange={(value) => setEmail(value)}
/>
<Button
className="![font-family:var(--font-body)] !text-[16px] !font-bold !bg-core-green !text-core-white !py-3 !px-[110px] !rounded !border-0"
onClick={handleRequestReset}
loading={isLoading}
>
Request reset link
</Button>
<Link href="/login" className="![font-family:var(--font-body)] !text-[16px] !font-bold !text-core-green !py-3 !px-[127px]">
Back to login
</Link>
</div>
</div>
);
}
77 changes: 68 additions & 9 deletions apps/frontend/src/app/login/page.tsx
Original file line number Diff line number Diff line change
@@ -1,29 +1,88 @@
'use client';

import React from 'react';
import React, { useState } from 'react';
import TextInputField from '../components/TextInputField';
import Link from 'next/link';
import { Button } from '@chakra-ui/react';
import { useAuth } from '@/context/AuthContext';
import { useRouter } from 'next/navigation';

export default function LoginPage() {
const { login } = useAuth();
const router = useRouter();

const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [emailError, setEmailError] = useState('');
const [passwordError, setPasswordError] = useState('');
const [isLoading, setIsLoading] = useState(false);

function validate(): boolean {
let valid = true;

if (!email || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
setEmailError('Please enter a valid email address');
valid = false;
} else {
setEmailError('');
}

if (!password) {
setPasswordError('Please enter valid password');
valid = false;
} else {
setPasswordError('');
}

return valid;
}

async function handleLogin() {
if (!validate()) return;

setIsLoading(true);
try {
await login(email, password);
router.push('/');
} catch {
setPasswordError('Incorrect email or password. Please try again.');
} finally {
setIsLoading(false);
}
}

return (
<div className="flex flex-col items-center text-center w-80">
<h1 className="![font-family:var(--font-heading)] !text-[36px] !font-semibold !mb-6">Login</h1>
<h5 className="![font-family:var(--font-body)] !text-[16px] !font-bold !mb-6">BRANCH Accounting Platform</h5>
<div className="flex flex-col gap-4 w-full !mb-10">
<TextInputField label="Email *" placeholder="Enter email address" errorMessage="Please enter a valid email address"/>
<TextInputField label="Password *" placeholder="Enter password" errorMessage="Please enter valid password"/>
<TextInputField
label="Email *"
placeholder="Enter email address"
errorMessage={emailError}
isError={!!emailError}
value={email}
onChange={(value) => setEmail(value)}
/>
<TextInputField
label="Password *"
placeholder="Enter password"
errorMessage={passwordError}
isError={!!passwordError}
value={password}
onChange={(value) => setPassword(value)}
/>
</div>
{/* TODO: form validation*/}
<Button className="![font-family:var(--font-body)] !rounded !bg-core-green !text-core-white w-full !px-4 !py-1.5 !mb-10">
<Button
className="![font-family:var(--font-body)] !rounded !bg-core-green !text-core-white w-full !px-4 !py-1.5 !mb-10"
onClick={handleLogin}
loading={isLoading}
>
Login
</Button>
{/* TODO: Update href when forgot password page is created */}
<Link href="#" className="!text-core-green !font-bold ![font-family:var(--font-body)] !text-[16px]">
<Link href="/forgot-password" className="!text-core-green !font-bold ![font-family:var(--font-body)] !text-[16px]">
Forgot password?
</Link>
</div>

);
}

115 changes: 115 additions & 0 deletions apps/frontend/src/app/reset-password/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
'use client';

import React, { useState, Suspense } from 'react';
import TextInputField from '@/app/components/TextInputField';
import { Button } from '@chakra-ui/react';
import { useAuth } from '@/context/AuthContext';
import { useRouter, useSearchParams } from 'next/navigation';

function ResetPasswordContent() {
const { resetPassword } = useAuth();
const router = useRouter();
const searchParams = useSearchParams();

const email = searchParams.get('email') ?? '';
const code = searchParams.get('code') ?? '';

const [newPassword, setNewPassword] = useState('');
const [confirmPassword, setConfirmPassword] = useState('');
const [newPasswordError, setNewPasswordError] = useState('');
const [confirmPasswordError, setConfirmPasswordError] = useState('');
const [isLoading, setIsLoading] = useState(false);
const [submitted, setSubmitted] = useState(false);

function validate(): boolean {
let valid = true;

const strongPassword = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[\W_]).{8,}$/;
if (!newPassword || !strongPassword.test(newPassword)) {
setNewPasswordError('Password must be at least 8 characters with uppercase, lowercase, number, and symbol');
valid = false;
} else {
setNewPasswordError('');
}

if (newPassword !== confirmPassword) {
setConfirmPasswordError('Password does not match');
valid = false;
} else {
setConfirmPasswordError('');
}

return valid;
}

async function handleResetPassword() {
if (!validate()) return;
setIsLoading(true);
try {
await resetPassword(email, code, newPassword);
} catch {
// expected without backend
} finally {
setIsLoading(false);
setSubmitted(true);
}
}

if (submitted) {
return (
<div className="flex flex-col items-center text-center w-90">
<div className="flex flex-col items-start gap-6">
<h1 className="![font-family:var(--font-heading)] !text-[36px] !font-semibold">Password Changed</h1>
<h5 className="![font-family:var(--font-body)] !text-[16px] !font-bold text-center !text-core-black !mb-6">
Your password has been successfully changed!
</h5>
</div>
<Button
className="![font-family:var(--font-body)] !rounded !bg-core-green !text-core-white w-full !px-4 !py-1.5 !mb-10"
onClick={() => router.push('/login')}
>
Back to login
</Button>
</div>
);
}

return (
<div className="flex flex-col items-center text-center w-80">
<h1 className="![font-family:var(--font-heading)] !text-[36px] !font-semibold !mb-6">Reset Password</h1>
<div className="flex flex-col gap-4 w-full !mb-10">
<TextInputField
label="New Password *"
placeholder="Enter new password"
errorMessage={newPasswordError}
isError={!!newPasswordError}
value={newPassword}
onChange={(value) => setNewPassword(value)}
/>
<TextInputField
label="Confirm Password *"
placeholder="Retype password"
errorMessage={confirmPasswordError}
isError={!!confirmPasswordError}
value={confirmPassword}
onChange={(value) => setConfirmPassword(value)}
/>
</div>
<Button
className="![font-family:var(--font-body)] !rounded !bg-core-green !text-core-white w-full !px-4 !py-1.5 !mb-10"
onClick={handleResetPassword}
loading={isLoading}
>
Reset Password
</Button>
</div>
);
}

export default function ResetPasswordPage() {
return (
<Suspense>
<ResetPasswordContent />
</Suspense>
);
}
Loading
Loading