handle auth correctly and added change password

This commit is contained in:
Carl-Gerhard Lindesvärd
2023-10-28 22:59:33 +02:00
parent e0cc9ef83b
commit aa5c881ec6
7 changed files with 149 additions and 44 deletions

View File

@@ -16,6 +16,7 @@ Mixan is a simple analytics tool for logging events on web and react-native. My
### GUI ### GUI
* [ ] Fix tables on settings
* [ ] Rename event label * [ ] Rename event label
* [ ] Real time data (mostly screen_views stats) * [ ] Real time data (mostly screen_views stats)
* [ ] Active users (5min, 10min, 30min) * [ ] Active users (5min, 10min, 30min)

View File

@@ -20,7 +20,7 @@ export function ContentHeader({ title, text, children }: ContentHeaderProps) {
type ContentSectionProps = { type ContentSectionProps = {
title: string; title: string;
text: string; text?: string | React.ReactNode;
children: React.ReactNode; children: React.ReactNode;
asCol?: boolean; asCol?: boolean;
}; };
@@ -41,7 +41,7 @@ export function ContentSection({
{title && ( {title && (
<div className="max-w-[50%]"> <div className="max-w-[50%]">
<h4 className="h4">{title}</h4> <h4 className="h4">{title}</h4>
<p className="text-sm text-muted-foreground">{text}</p> {text && <p className="text-sm text-muted-foreground">{text}</p>}
</div> </div>
)} )}
<div>{children}</div> <div>{children}</div>

View File

@@ -0,0 +1,9 @@
type InputErrorProps = { message?: string };
export function InputError({ message }: InputErrorProps) {
if (!message) {
return null;
}
return <div className="mt-1 text-sm text-red-600">{message}</div>;
}

View File

@@ -0,0 +1,85 @@
import { api, handleError } from "@/utils/api";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { ContentHeader, ContentSection } from "@/components/Content";
import { useForm } from "react-hook-form";
import { toast } from "@/components/ui/use-toast";
import { z } from "zod";
import { zodResolver } from "@hookform/resolvers/zod";
import { InputError } from "../forms/InputError";
const validator = z
.object({
oldPassword: z.string().min(1),
password: z.string().min(8),
confirmPassword: z.string().min(8),
})
.superRefine(({ confirmPassword, password }, ctx) => {
if (confirmPassword !== password) {
ctx.addIssue({
path: ["confirmPassword"],
code: "custom",
message: "The passwords did not match",
});
}
});
type IForm = z.infer<typeof validator>;
export function ChangePassword() {
const mutation = api.user.changePassword.useMutation({
onSuccess() {
toast({
title: "Success",
description: "You have updated your password",
});
},
onError: handleError,
});
const { register, handleSubmit, formState } = useForm<IForm>({
resolver: zodResolver(validator),
defaultValues: {
oldPassword: "",
password: "",
confirmPassword: "",
},
});
return (
<form
onSubmit={handleSubmit((values) => {
mutation.mutate(values)
})}
className="flex flex-col divide-y divide-border"
>
<ContentHeader
title="Change password"
text="Need to change your password?"
>
<Button type="submit" disabled={!formState.isDirty}>Change it!</Button>
</ContentHeader>
<ContentSection title="Old password" text={<InputError {...formState.errors.oldPassword}/>}>
<Input
type="password"
{...register("oldPassword")}
placeholder="Old password"
/>
</ContentSection>
<ContentSection title="New password" text={<InputError {...formState.errors.password}/>}>
<Input
type="password"
{...register("password")}
placeholder="New password"
/>
</ContentSection>
<ContentSection title="Confirm password" text={<InputError {...formState.errors.confirmPassword}/>}>
<Input
type="password"
{...register("confirmPassword")}
placeholder="Confirm password"
/>
</ContentSection>
</form>
);
}

View File

@@ -7,24 +7,36 @@ import { useEffect } from "react";
import { SettingsLayout } from "@/components/layouts/SettingsLayout"; import { SettingsLayout } from "@/components/layouts/SettingsLayout";
import { toast } from "@/components/ui/use-toast"; import { toast } from "@/components/ui/use-toast";
import { createServerSideProps } from "@/server/getServerSideProps"; import { createServerSideProps } from "@/server/getServerSideProps";
import { ChangePassword } from "@/components/user/ChangePassword";
import { z } from "zod";
import { zodResolver } from "@hookform/resolvers/zod";
import { InputError } from "@/components/forms/InputError";
export const getServerSideProps = createServerSideProps() export const getServerSideProps = createServerSideProps();
const validator = z.object({
name: z.string().min(2),
email: z.string().email(),
})
type IForm = z.infer<typeof validator>;
export default function Profile() { export default function Profile() {
const query = api.user.current.useQuery(); const query = api.user.current.useQuery();
const mutation = api.user.update.useMutation({ const mutation = api.user.update.useMutation({
onSuccess() { onSuccess() {
toast({ toast({
title: 'Profile updated', title: "Profile updated",
description: 'Your profile has been updated.', description: "Your profile has been updated.",
}) });
query.refetch() query.refetch();
}, },
onError: handleError, onError: handleError,
}); });
const data = query.data; const data = query.data;
const { register, handleSubmit, reset, formState } = useForm({ const { register, handleSubmit, reset, formState } = useForm({
resolver: zodResolver(validator),
defaultValues: { defaultValues: {
name: "", name: "",
email: "", email: "",
@@ -39,35 +51,29 @@ export default function Profile() {
return ( return (
<SettingsLayout> <SettingsLayout>
<form onSubmit={handleSubmit((values) => mutation.mutate(values))} className="flex flex-col divide-y divide-border"> <form
<ContentHeader onSubmit={handleSubmit((values) => mutation.mutate(values))}
title="Profile" className="flex flex-col divide-y divide-border"
text="View and update your profile"
> >
<Button type="submit" disabled={!formState.isDirty}>Save</Button> <ContentHeader title="Profile" text="View and update your profile">
<Button type="submit" disabled={!formState.isDirty}>
Save
</Button>
</ContentHeader> </ContentHeader>
<ContentSection title="Name" text="Your full name"> <ContentSection title="Name" text={[
"Your full name",
<InputError key="error" {...formState.errors.name} />
]}>
<Input {...register("name")} /> <Input {...register("name")} />
</ContentSection> </ContentSection>
<ContentSection title="Mail" text="Your email address"> <ContentSection title="Mail" text={["Your email address", <InputError key="error" {...formState.errors.email} />]}>
<Input {...register("email")} /> <Input {...register("email")} />
</ContentSection> </ContentSection>
</form> </form>
{/* <form onSubmit={handleSubmit((values) => mutation.mutate(values))} className="flex flex-col divide-y divide-border"> <div className="mt-8">
<ContentHeader <ChangePassword />
title="Change password" </div>
text="Need to change your password?"
>
<Button disabled={!formState.isDirty}>Change it!</Button>
</ContentHeader>
<ContentSection title="Name" text="Your full name">
<Input {...register("name")} />
</ContentSection>
<ContentSection title="Mail" text="Your email address">
<Input {...register("email")} />
</ContentSection>
</form> */}
</SettingsLayout> </SettingsLayout>
); );
} }

View File

@@ -5,7 +5,7 @@ import {
protectedProcedure, protectedProcedure,
} from "@/server/api/trpc"; } from "@/server/api/trpc";
import { db } from "@/server/db"; import { db } from "@/server/db";
import { hashPassword } from "@/server/services/hash.service"; import { hashPassword, verifyPassword } from "@/server/services/hash.service";
export const userRouter = createTRPCRouter({ export const userRouter = createTRPCRouter({
current: protectedProcedure.query(({ ctx }) => { current: protectedProcedure.query(({ ctx }) => {
@@ -47,14 +47,10 @@ export const userRouter = createTRPCRouter({
} }
}) })
if(user.password !== input.oldPassword) { if(!(await verifyPassword(input.oldPassword, user.password))) {
throw new Error('Old password is incorrect') throw new Error('Old password is incorrect')
} }
if(user.password === input.password) {
throw new Error('New password cannot be the same as old password')
}
return db.user.update({ return db.user.update({
where: { where: {
id: ctx.session.user.id id: ctx.session.user.id

View File

@@ -8,7 +8,7 @@ import {
import { db } from "@/server/db"; import { db } from "@/server/db";
import Credentials from "next-auth/providers/credentials"; import Credentials from "next-auth/providers/credentials";
import { createError } from "./exceptions"; import { createError } from "./exceptions";
import { verifyPassword } from "@/server/services/hash.service"; import { hashPassword, verifyPassword } from "@/server/services/hash.service";
/** /**
* Module augmentation for `next-auth` types. Allows us to add custom properties to the `session` * Module augmentation for `next-auth` types. Allows us to add custom properties to the `session`
@@ -55,18 +55,26 @@ export const authOptions: NextAuthOptions = {
password: { label: "Password", type: "password" }, password: { label: "Password", type: "password" },
}, },
async authorize(credentials) { async authorize(credentials) {
if(!credentials?.password || !credentials?.email) {
return null
}
const user = await db.user.findFirst({ const user = await db.user.findFirst({
where: { email: credentials?.email }, where: { email: credentials?.email },
}); });
if (user) { if(!user) {
return null
}
if(!await verifyPassword(credentials.password, user.password)) {
return null
}
return { return {
...user, ...user,
image: 'https://avatars.githubusercontent.com/u/18133?v=4' image: 'https://api.dicebear.com/7.x/adventurer/svg?seed=Abby'
}; };
} else {
return null;
}
}, },
}), }),
/** /**