diff --git a/README.md b/README.md index d1789cb7..43573ada 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,7 @@ Mixan is a simple analytics tool for logging events on web and react-native. My ### GUI +* [ ] Fix tables on settings * [ ] Rename event label * [ ] Real time data (mostly screen_views stats) * [ ] Active users (5min, 10min, 30min) diff --git a/apps/web/src/components/Content.tsx b/apps/web/src/components/Content.tsx index 9818b936..6ce3164c 100644 --- a/apps/web/src/components/Content.tsx +++ b/apps/web/src/components/Content.tsx @@ -20,7 +20,7 @@ export function ContentHeader({ title, text, children }: ContentHeaderProps) { type ContentSectionProps = { title: string; - text: string; + text?: string | React.ReactNode; children: React.ReactNode; asCol?: boolean; }; @@ -41,7 +41,7 @@ export function ContentSection({ {title && (

{title}

-

{text}

+ {text &&

{text}

}
)}
{children}
diff --git a/apps/web/src/components/forms/InputError.tsx b/apps/web/src/components/forms/InputError.tsx new file mode 100644 index 00000000..fcf0744f --- /dev/null +++ b/apps/web/src/components/forms/InputError.tsx @@ -0,0 +1,9 @@ +type InputErrorProps = { message?: string }; + +export function InputError({ message }: InputErrorProps) { + if (!message) { + return null; + } + + return
{message}
; +} diff --git a/apps/web/src/components/user/ChangePassword.tsx b/apps/web/src/components/user/ChangePassword.tsx new file mode 100644 index 00000000..a0dc55bb --- /dev/null +++ b/apps/web/src/components/user/ChangePassword.tsx @@ -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; + +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({ + resolver: zodResolver(validator), + defaultValues: { + oldPassword: "", + password: "", + confirmPassword: "", + }, + }); + + return ( +
{ + mutation.mutate(values) + })} + className="flex flex-col divide-y divide-border" + > + + + + }> + + + }> + + + }> + + +
+ ); +} diff --git a/apps/web/src/pages/[organization]/settings/profile.tsx b/apps/web/src/pages/[organization]/settings/profile.tsx index 4a5d0025..d5d674fb 100644 --- a/apps/web/src/pages/[organization]/settings/profile.tsx +++ b/apps/web/src/pages/[organization]/settings/profile.tsx @@ -7,24 +7,36 @@ import { useEffect } from "react"; import { SettingsLayout } from "@/components/layouts/SettingsLayout"; import { toast } from "@/components/ui/use-toast"; 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; export default function Profile() { const query = api.user.current.useQuery(); const mutation = api.user.update.useMutation({ onSuccess() { toast({ - title: 'Profile updated', - description: 'Your profile has been updated.', - }) - query.refetch() + title: "Profile updated", + description: "Your profile has been updated.", + }); + query.refetch(); }, onError: handleError, }); const data = query.data; const { register, handleSubmit, reset, formState } = useForm({ + resolver: zodResolver(validator), defaultValues: { name: "", email: "", @@ -39,35 +51,29 @@ export default function Profile() { return ( -
mutation.mutate(values))} className="flex flex-col divide-y divide-border"> - - + mutation.mutate(values))} + className="flex flex-col divide-y divide-border" + > + + - + + ]}> - + ]}> - {/*
mutation.mutate(values))} className="flex flex-col divide-y divide-border"> - - - - - - - - - -
*/} +
+ +
); } diff --git a/apps/web/src/server/api/routers/user.ts b/apps/web/src/server/api/routers/user.ts index af731224..46d2a9a4 100644 --- a/apps/web/src/server/api/routers/user.ts +++ b/apps/web/src/server/api/routers/user.ts @@ -5,7 +5,7 @@ import { protectedProcedure, } from "@/server/api/trpc"; import { db } from "@/server/db"; -import { hashPassword } from "@/server/services/hash.service"; +import { hashPassword, verifyPassword } from "@/server/services/hash.service"; export const userRouter = createTRPCRouter({ 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') } - - if(user.password === input.password) { - throw new Error('New password cannot be the same as old password') - } - + return db.user.update({ where: { id: ctx.session.user.id diff --git a/apps/web/src/server/auth.ts b/apps/web/src/server/auth.ts index 3d79b981..f8e60666 100644 --- a/apps/web/src/server/auth.ts +++ b/apps/web/src/server/auth.ts @@ -8,7 +8,7 @@ import { import { db } from "@/server/db"; import Credentials from "next-auth/providers/credentials"; 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` @@ -55,18 +55,26 @@ export const authOptions: NextAuthOptions = { password: { label: "Password", type: "password" }, }, async authorize(credentials) { + if(!credentials?.password || !credentials?.email) { + return null + } + const user = await db.user.findFirst({ where: { email: credentials?.email }, }); - if (user) { - return { - ...user, - image: 'https://avatars.githubusercontent.com/u/18133?v=4' - }; - } else { - return null; + if(!user) { + return null } + + if(!await verifyPassword(credentials.password, user.password)) { + return null + } + + return { + ...user, + image: 'https://api.dicebear.com/7.x/adventurer/svg?seed=Abby' + }; }, }), /**