diff --git a/src/app/nav-bar.tsx b/src/app/nav-bar.tsx index dcc89cc..9c79c78 100644 --- a/src/app/nav-bar.tsx +++ b/src/app/nav-bar.tsx @@ -36,7 +36,7 @@ export function NavBar() { - Profil + Profile diff --git a/src/app/profile/actions.ts b/src/app/profile/actions.ts new file mode 100644 index 0000000..55f03a8 --- /dev/null +++ b/src/app/profile/actions.ts @@ -0,0 +1,18 @@ +'use server' + +import { ServiceException } from "@/model/service.exception.model"; +import { ProfilePasswordChangeModel, profilePasswordChangeZodModel } from "@/model/update-password.model"; +import userService from "@/server/services/user.service"; +import { getAuthUserSession, saveFormAction } from "@/server/utils/action-wrapper.utils"; + +export const changePassword = async (prevState: any, inputData: ProfilePasswordChangeModel) => + saveFormAction(inputData, profilePasswordChangeZodModel, async (validatedData) => { + if (validatedData.newPassword !== validatedData.confirmNewPassword) { + throw new ServiceException('New password and confirm password do not match.'); + } + if (validatedData.oldPassword === validatedData.newPassword) { + throw new ServiceException('New password cannot be the same as the old password.'); + } + const session = await getAuthUserSession(); + await userService.changePassword(session.email, validatedData.oldPassword, validatedData.newPassword); + }); diff --git a/src/app/profile/page.tsx b/src/app/profile/page.tsx new file mode 100644 index 0000000..351d9de --- /dev/null +++ b/src/app/profile/page.tsx @@ -0,0 +1,34 @@ +'use server' + +import { getAuthUserSession } from "@/server/utils/action-wrapper.utils"; +import projectService from "@/server/services/project.service"; +import { + Breadcrumb, + BreadcrumbItem, + BreadcrumbLink, + BreadcrumbList, +} from "@/components/ui/breadcrumb" +import PageTitle from "@/components/custom/page-title"; +import ProfilePasswordChange from "./profile-password-change"; + +export default async function ProjectPage() { + + await getAuthUserSession(); + const data = await projectService.getAllProjects(); + return ( +
+ + + + Profile + + + + + + +
+ ) +} diff --git a/src/app/profile/profile-password-change.tsx b/src/app/profile/profile-password-change.tsx new file mode 100644 index 0000000..867c41d --- /dev/null +++ b/src/app/profile/profile-password-change.tsx @@ -0,0 +1,97 @@ +'use client'; + +import { SubmitButton } from "@/components/custom/submit-button"; +import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"; +import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form"; +import { FormUtils } from "@/lib/form.utilts"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useForm } from "react-hook-form"; +import { useFormState } from "react-dom"; +import { ServerActionResult } from "@/model/server-action-error-return.model"; +import { Input } from "@/components/ui/input"; +import { useEffect } from "react"; +import { toast } from "sonner"; +import { ProfilePasswordChangeModel, profilePasswordChangeZodModel } from "@/model/update-password.model"; +import { changePassword } from "./actions"; + +export default function ProfilePasswordChange() { + const form = useForm({ + resolver: zodResolver(profilePasswordChangeZodModel) + }); + + const [state, formAction] = useFormState((state: ServerActionResult, payload: ProfilePasswordChangeModel) => + changePassword(state, payload), FormUtils.getInitialFormState()); + + useEffect(() => { + if (state.status === 'success') { + toast.success('Password updated successfully'); + form.setValue('oldPassword', ''); + form.setValue('newPassword', ''); + form.setValue('confirmNewPassword', ''); + form.clearErrors(); + } + FormUtils.mapValidationErrorsToForm(state, form) + }, [state]); + + const sourceTypeField = form.watch(); + return <> + + + Password + Change your existing login password. + +
+ form.handleSubmit((data) => { + return formAction(data); + })()}> + + ( + + Current Password + + + + + + )} + /> + ( + + New Password + + + + + + )} + /> + ( + + Confirm new Password + + + + + + )} + /> + + + Change Password +

{state?.message}

+
+
+ +
+ + ; +} \ No newline at end of file diff --git a/src/model/sim-session.model.ts b/src/model/sim-session.model.ts index c078686..72c1d9c 100644 --- a/src/model/sim-session.model.ts +++ b/src/model/sim-session.model.ts @@ -1,5 +1,5 @@ import { Session } from "next-auth"; -export interface UserSession extends Session { - id?: string; +export interface UserSession { + email: string; } diff --git a/src/model/update-password.model.ts b/src/model/update-password.model.ts new file mode 100644 index 0000000..eb7876c --- /dev/null +++ b/src/model/update-password.model.ts @@ -0,0 +1,9 @@ +import { z } from "zod"; + +export const profilePasswordChangeZodModel = z.object({ + oldPassword: z.string().trim().min(1), + newPassword: z.string().trim().min(6), + confirmNewPassword: z.string().trim().min(6) +}) + +export type ProfilePasswordChangeModel = z.infer; \ No newline at end of file diff --git a/src/server/services/user.service.ts b/src/server/services/user.service.ts index e96d1d3..ae8e36a 100644 --- a/src/server/services/user.service.ts +++ b/src/server/services/user.service.ts @@ -5,11 +5,40 @@ import dataAccess from "../adapter/db.client"; import { revalidateTag, unstable_cache } from "next/cache"; import { Tags } from "../utils/cache-tag-generator.utils"; import bcrypt from "bcrypt"; +import { ServiceException } from "@/model/service.exception.model"; const saltRounds = 10; export class UserService { + async changePassword(userMail: string, oldPassword: string, newPassword: string) { + try { + const user = await dataAccess.client.user.findUnique({ + where: { + email: userMail + } + }); + if (!user) { + throw new ServiceException("User not found"); + } + const isPasswordValid = await bcrypt.compare(oldPassword, user.password); + if (!isPasswordValid) { + throw new ServiceException("Old password is incorrect"); + } + const hashedPassword = await bcrypt.hash(newPassword, saltRounds); + await dataAccess.client.user.update({ + where: { + email: userMail + }, + data: { + password: hashedPassword + } + }); + } finally { + revalidateTag(Tags.users()); + } + } + async maptoDtoUser(user: User) { return { id: user.id, diff --git a/src/server/utils/action-wrapper.utils.ts b/src/server/utils/action-wrapper.utils.ts index 4fff04f..b0739ad 100644 --- a/src/server/utils/action-wrapper.utils.ts +++ b/src/server/utils/action-wrapper.utils.ts @@ -3,7 +3,7 @@ import { UserSession } from "@/model/sim-session.model"; import { getServerSession } from "next-auth"; import { ZodRawShape, ZodObject, objectUtil, baseObjectOutputType, z, ZodType } from "zod"; import { redirect } from "next/navigation"; -import { ServerActionResult, SuccessActionResult } from "@/model/server-action-error-return.model"; +import { ServerActionResult } from "@/model/server-action-error-return.model"; import { FormValidationException } from "@/model/form-validation-exception.model"; import { authOptions } from "@/lib/auth-options"; @@ -12,8 +12,13 @@ import { authOptions } from "@/lib/auth-options"; * use getAuthUserSession() if you want to throw an error if no user is logged in */ export async function getUserSession(): Promise { - const session = await getServerSession(authOptions) as UserSession | null; - return session; + const session = await getServerSession(authOptions); + if (!session) { + return null; + } + return { + email: session?.user?.email as string + }; } export async function getAuthUserSession(): Promise {