handle auth correctly and added change password
This commit is contained in:
@@ -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)
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
9
apps/web/src/components/forms/InputError.tsx
Normal file
9
apps/web/src/components/forms/InputError.tsx
Normal 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>;
|
||||||
|
}
|
||||||
85
apps/web/src/components/user/ChangePassword.tsx
Normal file
85
apps/web/src/components/user/ChangePassword.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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"
|
>
|
||||||
>
|
<ContentHeader title="Profile" text="View and update your profile">
|
||||||
<Button type="submit" disabled={!formState.isDirty}>Save</Button>
|
<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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 {
|
return null
|
||||||
...user,
|
|
||||||
image: 'https://avatars.githubusercontent.com/u/18133?v=4'
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(!await verifyPassword(credentials.password, user.password)) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...user,
|
||||||
|
image: 'https://api.dicebear.com/7.x/adventurer/svg?seed=Abby'
|
||||||
|
};
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user