added profile page

This commit is contained in:
biersoeckli
2024-10-26 06:51:14 +00:00
parent 43f2c981dd
commit 54a1a0fff2
8 changed files with 198 additions and 6 deletions

View File

@@ -36,7 +36,7 @@ export function NavBar() {
<DropdownMenuContent className="w-56">
<Link href="/profile">
<DropdownMenuItem>
Profil
Profile
</DropdownMenuItem>
</Link>
<DropdownMenuSeparator />

View File

@@ -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);
});

34
src/app/profile/page.tsx Normal file
View File

@@ -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 (
<div className="flex-1 space-y-4 p-8 pt-6">
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem>
<BreadcrumbLink href="/profile">Profile</BreadcrumbLink>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
<PageTitle
title={'Profile'}
subtitle={`View or edit your Profile information and configure your authentication.`}>
</PageTitle>
<ProfilePasswordChange />
</div>
)
}

View File

@@ -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<ProfilePasswordChangeModel>({
resolver: zodResolver(profilePasswordChangeZodModel)
});
const [state, formAction] = useFormState((state: ServerActionResult<any, any>, payload: ProfilePasswordChangeModel) =>
changePassword(state, payload), FormUtils.getInitialFormState<typeof profilePasswordChangeZodModel>());
useEffect(() => {
if (state.status === 'success') {
toast.success('Password updated successfully');
form.setValue('oldPassword', '');
form.setValue('newPassword', '');
form.setValue('confirmNewPassword', '');
form.clearErrors();
}
FormUtils.mapValidationErrorsToForm<typeof profilePasswordChangeZodModel>(state, form)
}, [state]);
const sourceTypeField = form.watch();
return <>
<Card>
<CardHeader>
<CardTitle>Password</CardTitle>
<CardDescription>Change your existing login password.</CardDescription>
</CardHeader>
<Form {...form}>
<form action={(e) => form.handleSubmit((data) => {
return formAction(data);
})()}>
<CardContent className="space-y-4">
<FormField
control={form.control}
name="oldPassword"
render={({ field }) => (
<FormItem>
<FormLabel>Current Password</FormLabel>
<FormControl>
<Input type="password" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="newPassword"
render={({ field }) => (
<FormItem>
<FormLabel>New Password</FormLabel>
<FormControl>
<Input type="password" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="confirmNewPassword"
render={({ field }) => (
<FormItem>
<FormLabel>Confirm new Password</FormLabel>
<FormControl>
<Input type="password" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</CardContent>
<CardFooter className="gap-4">
<SubmitButton>Change Password</SubmitButton>
<p className="text-red-500">{state?.message}</p>
</CardFooter>
</form>
</Form >
</Card >
</>;
}

View File

@@ -1,5 +1,5 @@
import { Session } from "next-auth";
export interface UserSession extends Session {
id?: string;
export interface UserSession {
email: string;
}

View File

@@ -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<typeof profilePasswordChangeZodModel>;

View File

@@ -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,

View File

@@ -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<UserSession | null> {
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<UserSession> {