mirror of
https://github.com/biersoeckli/QuickStack.git
synced 2026-02-17 18:28:48 -06:00
added profile page
This commit is contained in:
@@ -36,7 +36,7 @@ export function NavBar() {
|
||||
<DropdownMenuContent className="w-56">
|
||||
<Link href="/profile">
|
||||
<DropdownMenuItem>
|
||||
Profil
|
||||
Profile
|
||||
</DropdownMenuItem>
|
||||
</Link>
|
||||
<DropdownMenuSeparator />
|
||||
|
||||
18
src/app/profile/actions.ts
Normal file
18
src/app/profile/actions.ts
Normal 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
34
src/app/profile/page.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
97
src/app/profile/profile-password-change.tsx
Normal file
97
src/app/profile/profile-password-change.tsx
Normal 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 >
|
||||
|
||||
</>;
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Session } from "next-auth";
|
||||
|
||||
export interface UserSession extends Session {
|
||||
id?: string;
|
||||
export interface UserSession {
|
||||
email: string;
|
||||
}
|
||||
|
||||
9
src/model/update-password.model.ts
Normal file
9
src/model/update-password.model.ts
Normal 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>;
|
||||
@@ -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,
|
||||
|
||||
@@ -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> {
|
||||
|
||||
Reference in New Issue
Block a user