aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMohamedBassem <me@mbassem.com>2024-03-18 12:08:53 +0000
committerMohamedBassem <me@mbassem.com>2024-03-18 12:08:53 +0000
commit549520919c482e72cdf7adae5ba852d1b6cbe5aa (patch)
tree8599e55fe214f673685f9dbab7ea26f796748e01
parent60467f1d7fdc63e8ec3b10ad0d183248cebac4ee (diff)
downloadkarakeep-549520919c482e72cdf7adae5ba852d1b6cbe5aa.tar.zst
feature(web): Add the ability to change passwords
-rw-r--r--apps/web/app/dashboard/settings/page.tsx2
-rw-r--r--apps/web/components/dashboard/settings/ChangePassword.tsx132
-rw-r--r--packages/trpc/routers/users.ts42
-rw-r--r--packages/trpc/types/users.ts15
4 files changed, 182 insertions, 9 deletions
diff --git a/apps/web/app/dashboard/settings/page.tsx b/apps/web/app/dashboard/settings/page.tsx
index 3d54d2d7..71712eb9 100644
--- a/apps/web/app/dashboard/settings/page.tsx
+++ b/apps/web/app/dashboard/settings/page.tsx
@@ -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>
);
diff --git a/apps/web/components/dashboard/settings/ChangePassword.tsx b/apps/web/components/dashboard/settings/ChangePassword.tsx
new file mode 100644
index 00000000..d976f3e4
--- /dev/null
+++ b/apps/web/components/dashboard/settings/ChangePassword.tsx
@@ -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>
+ );
+}
diff --git a/packages/trpc/routers/users.ts b/packages/trpc/routers/users.ts
index 1a851b05..db06c0ad 100644
--- a/packages/trpc/routers/users.ts
+++ b/packages/trpc/routers/users.ts
@@ -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
@@ -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({
@@ -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 };
}),
});
diff --git a/packages/trpc/types/users.ts b/packages/trpc/types/users.ts
index c2fe182a..3026337a 100644
--- a/packages/trpc/types/users.ts
+++ b/packages/trpc/types/users.ts
@@ -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"],
+ });