Skip to content

Commit

Permalink
feature(web): Add the ability to change passwords
Browse files Browse the repository at this point in the history
  • Loading branch information
MohamedBassem committed Mar 18, 2024
1 parent 60467f1 commit 5495209
Show file tree
Hide file tree
Showing 4 changed files with 182 additions and 9 deletions.
2 changes: 2 additions & 0 deletions apps/web/app/dashboard/settings/page.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import ApiKeySettings from "@/components/dashboard/settings/ApiKeySettings";
import { ChangePassword } from "@/components/dashboard/settings/ChangePassword";

export default async function Settings() {
return (
<div className="m-4 flex flex-col space-y-2 rounded-md border bg-white p-4">
<p className="text-2xl">Settings</p>
<ChangePassword />
<ApiKeySettings />
</div>
);
Expand Down
132 changes: 132 additions & 0 deletions apps/web/components/dashboard/settings/ChangePassword.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
"use client";

import type { z } from "zod";
import { ActionButton } from "@/components/ui/action-button";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { toast } from "@/components/ui/use-toast";
import { api } from "@/lib/trpc";
import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";

import { zChangePasswordSchema } from "@hoarder/trpc/types/users";

export function ChangePassword() {
const form = useForm<z.infer<typeof zChangePasswordSchema>>({
resolver: zodResolver(zChangePasswordSchema),
defaultValues: {
currentPassword: "",
newPassword: "",
newPasswordConfirm: "",
},
});

const mutator = api.users.changePassword.useMutation({
onSuccess: () => {
toast({ description: "Password changed successfully" });
form.reset();
},
onError: (e) => {
if (e.data?.code == "UNAUTHORIZED") {
toast({
description: "Your current password is incorrect",
variant: "destructive",
});
} else {
toast({ description: "Something went wrong", variant: "destructive" });
}
},
});

async function onSubmit(value: z.infer<typeof zChangePasswordSchema>) {
mutator.mutate({
currentPassword: value.currentPassword,
newPassword: value.newPassword,
});
}

return (
<div className="w-full pt-4">
<span className="text-xl">Change Password</span>
<hr className="my-2" />
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
className="flex w-1/2 flex-col gap-2 pt-4"
>
<FormField
control={form.control}
name="currentPassword"
render={({ field }) => {
return (
<FormItem className="flex-1">
<FormLabel>Current Password</FormLabel>
<FormControl>
<Input
type="password"
placeholder="Current Password"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
);
}}
/>
<FormField
control={form.control}
name="newPassword"
render={({ field }) => {
return (
<FormItem className="flex-1">
<FormLabel>New Password</FormLabel>
<FormControl>
<Input
type="password"
placeholder="New Password"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
);
}}
/>
<FormField
control={form.control}
name="newPasswordConfirm"
render={({ field }) => {
return (
<FormItem className="flex-1">
<FormLabel>Confirm New Password</FormLabel>
<FormControl>
<Input
type="Password"
placeholder="Confirm New Password"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
);
}}
/>
<ActionButton
className="h-full"
type="submit"
loading={mutator.isPending}
>
Save
</ActionButton>
</form>
</Form>
</div>
);
}
42 changes: 34 additions & 8 deletions packages/trpc/routers/users.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
import { zSignUpSchema } from "../types/users";
import { TRPCError } from "@trpc/server";
import { count, eq } from "drizzle-orm";
import invariant from "tiny-invariant";
import { z } from "zod";

import { SqliteError } from "@hoarder/db";
import { users } from "@hoarder/db/schema";

import { hashPassword, validatePassword } from "../auth";
import {
adminProcedure,
authedProcedure,
publicProcedure,
router,
} from "../index";
import { SqliteError } from "@hoarder/db";
import { z } from "zod";
import { hashPassword } from "../auth";
import { TRPCError } from "@trpc/server";
import { users } from "@hoarder/db/schema";
import { count, eq } from "drizzle-orm";
import { zSignUpSchema } from "../types/users";

export const usersAppRouter = router({
create: publicProcedure
Expand Down Expand Up @@ -83,6 +86,29 @@ export const usersAppRouter = router({
});
return { users };
}),
changePassword: authedProcedure
.input(
z.object({
currentPassword: z.string(),
newPassword: z.string(),
}),
)
.mutation(async ({ input, ctx }) => {
invariant(ctx.user.email, "A user always has an email specified");
let user;
try {
user = await validatePassword(ctx.user.email, input.currentPassword);
} catch (e) {
throw new TRPCError({ code: "UNAUTHORIZED" });
}
invariant(user.id, ctx.user.id);
await ctx.db
.update(users)
.set({
password: await hashPassword(input.newPassword),
})
.where(eq(users.id, ctx.user.id));
}),
delete: adminProcedure
.input(
z.object({
Expand All @@ -103,7 +129,7 @@ export const usersAppRouter = router({
email: z.string().nullish(),
}),
)
.query(async ({ ctx }) => {
.query(({ ctx }) => {
return { id: ctx.user.id, name: ctx.user.name, email: ctx.user.email };
}),
});
15 changes: 14 additions & 1 deletion packages/trpc/types/users.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,26 @@
import { z } from "zod";

const PASSWORD_MAX_LENGTH = 100;

export const zSignUpSchema = z
.object({
name: z.string().min(1, { message: "Name can't be empty" }),
email: z.string().email(),
password: z.string().min(8),
password: z.string().min(8).max(PASSWORD_MAX_LENGTH),
confirmPassword: z.string(),
})
.refine((data) => data.password === data.confirmPassword, {
message: "Passwords don't match",
path: ["confirmPassword"],
});

export const zChangePasswordSchema = z
.object({
currentPassword: z.string(),
newPassword: z.string().min(8).max(PASSWORD_MAX_LENGTH),
newPasswordConfirm: z.string(),
})
.refine((data) => data.newPassword === data.newPasswordConfirm, {
message: "Passwords don't match",
path: ["newPasswordConfirm"],
});

0 comments on commit 5495209

Please sign in to comment.