Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
7e4e9e9
style: use datepicker instead of date textfield
colombefioren Aug 26, 2025
9237f86
refactor: fix prisma models to match openapi spec
colombefioren Aug 27, 2025
a2d4062
refactor: remove unnecessary endpoints (income categories)
colombefioren Aug 27, 2025
93473c2
refactor: remove income category from service layer
colombefioren Aug 27, 2025
ed7ed6e
refactor: update to use new field names (id)
colombefioren Aug 27, 2025
aeda380
refactor: remove category references
colombefioren Aug 27, 2025
93fe19d
refactor: simplify income routes by removing category endpoints
colombefioren Aug 27, 2025
f19ddcd
refactor: remove income category from types
colombefioren Aug 27, 2025
4512003
refactor: remove income category methods from services
colombefioren Aug 27, 2025
9434623
refactor: remove category field and set default date (because of the …
colombefioren Aug 27, 2025
d3a91cc
refactor: remove category display from incomelist
colombefioren Aug 27, 2025
a471bab
feat: make 1 to be the least amount when creating an income
colombefioren Aug 27, 2025
696b554
refactor: add a dash for when source is empty
colombefioren Aug 27, 2025
16db986
feat: required true to textfield source
colombefioren Aug 27, 2025
d17e039
refactor: make the default amount to be 1 instead of 0
colombefioren Aug 27, 2025
5b27bf3
Refactor/income logic (#20)
colombefioren Aug 27, 2025
2b46195
feat: auth middleware before granting the autorisation to fetch incomes
colombefioren Aug 27, 2025
f3fb671
fix: updated password field to be hashed_password
colombefioren Aug 27, 2025
c3c3f30
chore: fix merge conflicts
colombefioren Aug 27, 2025
7293c21
refactor: prisma models attributes to follow the precendent ones
colombefioren Aug 27, 2025
0d0c604
Merge branch 'refactor/income-logic' of https://github.com/Mathieu-bo…
colombefioren Aug 27, 2025
71aafee
fix: enable CORS with credentials for frontend and feat cookieparser …
colombefioren Aug 27, 2025
d33163e
feat: axios options with withCredentials true to allow cookie storage…
colombefioren Aug 27, 2025
0c73431
fix: name mismatch by turning createdAt to creation_date
colombefioren Aug 27, 2025
2429863
fix: made source field unrequired when users are creating an income
colombefioren Aug 27, 2025
91d1cff
fix(client): prevent duplicate query parameters in generated request
colombefioren Aug 27, 2025
faec33d
feat: enhance input validation in server
colombefioren Aug 27, 2025
4852c9f
feat: implement frontend value validation and improve error display
colombefioren Aug 28, 2025
818c756
chore: fix merge conflicts
colombefioren Aug 28, 2025
7c272e0
feat: add user profile update and password change service functions
colombefioren Aug 28, 2025
d56f9ce
feat: create user profile controller with get, update, and password c…
colombefioren Aug 28, 2025
74ec5f5
feat: add user profile routes with consistent validation pattern
colombefioren Aug 28, 2025
c477ba3
feat: user profile route in main app
colombefioren Aug 28, 2025
5ec13f6
feat: user service and add user profile methods to defaultservice via…
colombefioren Aug 28, 2025
53df101
feat: confirmPassword attribute in the change password request type
colombefioren Aug 28, 2025
07bf0b1
feat: user profile hook
colombefioren Aug 28, 2025
ca48f82
feat(frontend): implement user profile management
colombefioren Aug 28, 2025
ee23cf9
remove: unnecessary ui component spinner
colombefioren Aug 28, 2025
6cf7ddd
feat: user sotre in zustand
colombefioren Aug 28, 2025
2fe6be3
refactor: update dashboard header to use user store
colombefioren Aug 28, 2025
bb771ad
refactor: update profile page to use zustand user store
colombefioren Aug 28, 2025
21e9252
style: fix profile page padding
colombefioren Aug 28, 2025
4f9fd7f
chore: fix merge conflicts
colombefioren Aug 28, 2025
c95e0ea
refactor: better error display when users arent logged in
colombefioren Aug 28, 2025
56f6b40
refactor: allow users to provide empty first and last name
colombefioren Aug 28, 2025
04956eb
Merge branch 'dev' into feat/profile
Mathieu-bot Aug 29, 2025
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
16 changes: 9 additions & 7 deletions client/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import { Routes, Route, useLocation } from "react-router-dom";
import { ToastProvider } from "./ui";
import { IncomesPage } from "./pages/IncomesPage";
import { Incomes } from "./pages/Incomes";
import Sidebar from "./components/common/Sidebar";
import Dashboard from "./pages/Dashboard";
import BackgroundImage from "./components/common/BackgroundImage";
import { CreateIncomePage } from "./pages/CreateIncomePage";
import { EditIncomePage } from "./pages/EditIncomePage";
import { CreateIncome } from "./pages/CreateIncome";
import { EditIncome } from "./pages/EditIncome";
import Mascot from "./components/common/Mascot";
import { DashboardHeader } from "./components/common/Header";
import { Profile } from "./pages/Profile";

function App() {
/*
Expand All @@ -22,7 +23,7 @@ function App() {
pauseOnHover={true}
anchorOrigin={{ vertical: "top", horizontal: "right" }}
>
<div className="App min-h-screen relative">
<div className="App min-h-screen relative overflow-x-hidden">
{location.pathname.includes("/login") ||
location.pathname.includes("/register") ? null : (
<>
Expand All @@ -34,9 +35,10 @@ function App() {
<Mascot className="z-50" />
<Routes>
<Route path="/" element={<Dashboard />} />
<Route path="/incomes" element={<IncomesPage />} />
<Route path="/incomes/new" element={<CreateIncomePage />} />
<Route path="/incomes/:id/edit" element={<EditIncomePage />} />
<Route path="/incomes" element={<Incomes />} />
<Route path="/incomes/new" element={<CreateIncome />} />
<Route path="/incomes/:id/edit" element={<EditIncome />} />
<Route path="/profile" element={<Profile />} />
</Routes>
</div>
</ToastProvider>
Expand Down
36 changes: 34 additions & 2 deletions client/src/api/services/DefaultService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,8 @@ export class DefaultService {
}
/**
* List all user expenses
* @param start Start date
* @param end End date
* @param start Start date (ISO string)
* @param end End date (ISO string)
* @param category
* @param type
* @returns any List of expenses
Expand Down Expand Up @@ -383,4 +383,36 @@ export class DefaultService {
url: "/user/profile",
});
}
/**
* Update user profile
* @param requestBody
* @returns any Profile updated
* @throws ApiError
*/
public static putUserProfile(
requestBody: Record<string, any>
): CancelablePromise<any> {
return __request(OpenAPI, {
method: "PUT",
url: "/user/profile",
body: requestBody,
mediaType: "application/json",
});
}
/**
* Update user password
* @param requestBody
* @returns any Password updated
* @throws ApiError
*/
public static patchUserProfilePassword(
requestBody: Record<string, any>
): CancelablePromise<any> {
return __request(OpenAPI, {
method: "PATCH",
url: "/user/profile/password",
body: requestBody,
mediaType: "application/json",
});
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import React, { useState, useEffect } from "react";
import type { Income, IncomeFormData } from "../types/Income";
import { Button, TextField, Dialog, DatePicker } from "../ui";
import { useMascot } from "../hooks/useMascot";
import { validateIncomeData } from "../utils/validators";
import type { Income, IncomeFormData } from "../../types/Income";
import { Button, TextField, Dialog, DatePicker } from "../../ui";
import { useMascot } from "../../hooks/useMascot";
import { validateIncomeData } from "../../utils/validators";

interface IncomeFormProps {
income?: Income;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { forwardRef, useImperativeHandle } from "react";
import type { Income } from "../types/Income";
import { useIncomes } from "../hooks/useIncomes";
import { Button } from "../ui";
import type { Income } from "../../types/Income";
import { useIncomes } from "../../hooks/useIncomes";
import { Button } from "../../ui";

interface IncomeListProps {
startDate?: string;
Expand Down
195 changes: 195 additions & 0 deletions client/src/components/Profile/PasswordForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
import React, { useState } from "react";
import { Button, TextField } from "../../ui";
import type { ChangePasswordRequest } from "../../types/UserProfile";
import { createFieldChangeHandler } from "../../utils/formUtils";

interface PasswordFormProps {
onChangePassword: (
data: ChangePasswordRequest
) => Promise<{ success: boolean; error?: string }>;
loading?: boolean;
}

export const PasswordForm: React.FC<PasswordFormProps> = ({
onChangePassword,
loading = false,
}) => {
const [formData, setFormData] = useState({
currentPassword: "",
newPassword: "",
confirmPassword: "",
});
const [message, setMessage] = useState("");
const [formError, setFormError] = useState("");
const [fieldErrors, setFieldErrors] = useState<Record<string, string>>({});

const validateForm = (): boolean => {
const errors: Record<string, string> = {};

if (!formData.currentPassword) {
errors.currentPassword = "Current password is required";
}

if (formData.newPassword.length < 6) {
errors.newPassword = "Password must be at least 6 characters long";
} else if (!/(?=.*[A-Z])/.test(formData.newPassword)) {
errors.newPassword =
"Password must contain at least one uppercase letter";
} else if (!/(?=.*\d)/.test(formData.newPassword)) {
errors.newPassword = "Password must contain at least one number";
}

if (formData.newPassword !== formData.confirmPassword) {
errors.confirmPassword = "Passwords do not match";
}

setFieldErrors(errors);
return Object.keys(errors).length === 0;
};

const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setMessage("");
setFormError("");
setFieldErrors({});

if (!validateForm()) return;

const result = await onChangePassword({
currentPassword: formData.currentPassword,
newPassword: formData.newPassword,
});

if (result.success) {
setMessage("Password changed successfully!");
setFormData({
currentPassword: "",
newPassword: "",
confirmPassword: "",
});
setTimeout(() => setMessage(""), 3000);
} else {
const error = result.error || "Failed to change password";
setFormError(error);

if (
error.toLowerCase().includes("current") ||
error.toLowerCase().includes("incorrect")
) {
setFieldErrors((prev) => ({ ...prev, currentPassword: error }));
} else if (
error.toLowerCase().includes("new") ||
error.toLowerCase().includes("password")
) {
if (
error.toLowerCase().includes("uppercase") ||
error.toLowerCase().includes("number") ||
error.toLowerCase().includes("6") ||
error.toLowerCase().includes("character")
) {
setFieldErrors((prev) => ({ ...prev, newPassword: error }));
} else {
setFormError(error);
}
} else {
setFormError(error);
}
}
};

const handleCurrentPasswordChange = createFieldChangeHandler(
setFormData,
"currentPassword"
);
const handleNewPasswordChange = createFieldChangeHandler(
setFormData,
"newPassword"
);
const handleConfirmPasswordChange = createFieldChangeHandler(
setFormData,
"confirmPassword"
);

const clearFieldError = (fieldName: string) => {
if (fieldErrors[fieldName]) {
setFieldErrors((prev) => ({ ...prev, [fieldName]: "" }));
}
if (formError) setFormError("");
};

return (
<div className="bg-white rounded-lg shadow-md p-6">
<h2 className="text-2xl font-bold text-gray-800 mb-6">Change Password</h2>

<form onSubmit={handleSubmit} className="space-y-4">
<TextField
label="Current Password"
type="password"
value={formData.currentPassword}
onChange={handleCurrentPasswordChange}
onFocus={() => clearFieldError("currentPassword")}
placeholder="Enter current password"
variant="outlined"
size="medium"
fullWidth
error={!!fieldErrors.currentPassword}
helperText={fieldErrors.currentPassword}
/>

<TextField
label="New Password"
type="password"
value={formData.newPassword}
onChange={handleNewPasswordChange}
onFocus={() => clearFieldError("newPassword")}
placeholder="Enter new password"
variant="outlined"
size="medium"
fullWidth
error={!!fieldErrors.newPassword}
helperText={
fieldErrors.newPassword ||
"Must be at least 6 characters with 1 uppercase letter and 1 number"
}
/>

<TextField
label="Confirm New Password"
type="password"
value={formData.confirmPassword}
onChange={handleConfirmPasswordChange}
onFocus={() => clearFieldError("confirmPassword")}
placeholder="Confirm new password"
variant="outlined"
size="medium"
fullWidth
error={!!fieldErrors.confirmPassword}
helperText={fieldErrors.confirmPassword}
/>

{message && (
<div className="p-3 bg-green-100 border border-green-400 text-green-700 rounded-md">
{message}
</div>
)}

{formError && !Object.values(fieldErrors).some((error) => error) && (
<div className="p-3 bg-red-100 border border-red-400 text-red-700 rounded-md">
{formError}
</div>
)}

<Button
type="submit"
loading={loading}
loadingPosition="center"
fullWidth
size="large"
className="bg-blue-600 hover:bg-blue-700 text-white"
>
Change Password
</Button>
</form>
</div>
);
};
Loading