Browse Source

enable 2fa flow

Milo Schwartz 6 tháng trước cách đây
mục cha
commit
9e50a580a5
33 tập tin đã thay đổi với 605 bổ sung2084 xóa
  1. 2 1
      package.json
  2. 9 0
      server/routers/auth/verifyTotp.ts
  3. 3 6
      src/app/[orgId]/layout.tsx
  4. 9 8
      src/app/[orgId]/settings/layout.tsx
  5. 5 5
      src/app/[orgId]/settings/share-links/components/CreateShareLinkForm.tsx
  6. 3 3
      src/app/[orgId]/settings/sites/components/SitesTable.tsx
  7. 0 176
      src/app/profile/account/account-form.tsx
  8. 0 18
      src/app/profile/account/page.tsx
  9. 0 164
      src/app/profile/appearance/appearance-form.tsx
  10. 0 18
      src/app/profile/appearance/page.tsx
  11. 0 132
      src/app/profile/display/display-form.tsx
  12. 0 17
      src/app/profile/display/page.tsx
  13. 36 0
      src/app/profile/general/layout_.tsx
  14. 14 0
      src/app/profile/general/page_.tsx
  15. 0 76
      src/app/profile/layout.tsx
  16. 74 0
      src/app/profile/layout_.tsx
  17. 0 222
      src/app/profile/notifications/notifications-form.tsx
  18. 0 17
      src/app/profile/notifications/page.tsx
  19. 0 17
      src/app/profile/page.tsx
  20. 5 0
      src/app/profile/page_.tsx
  21. 0 192
      src/app/profile/profile-form.tsx
  22. 291 0
      src/components/Enable2FaForm.tsx
  23. 101 95
      src/components/Header.tsx
  24. 7 1
      src/components/LoginForm.tsx
  25. 3 3
      src/components/TopbarNav.tsx
  26. 0 176
      src/components/account-form.tsx
  27. 0 179
      src/components/appearance-form.tsx
  28. 0 132
      src/components/display-form.tsx
  29. 0 222
      src/components/notifications-form.tsx
  30. 0 192
      src/components/profile-form.tsx
  31. 8 1
      src/contexts/userContext.ts
  32. 7 4
      src/hooks/useUserContext.ts
  33. 28 7
      src/providers/UserProvider.tsx

+ 2 - 1
package.json

@@ -60,6 +60,7 @@
         "node-fetch": "3.3.2",
         "node-fetch": "3.3.2",
         "nodemailer": "6.9.15",
         "nodemailer": "6.9.15",
         "oslo": "1.2.1",
         "oslo": "1.2.1",
+        "qrcode.react": "4.2.0",
         "react": "19.0.0-rc.1",
         "react": "19.0.0-rc.1",
         "react-dom": "19.0.0-rc.1",
         "react-dom": "19.0.0-rc.1",
         "react-hook-form": "7.53.0",
         "react-hook-form": "7.53.0",
@@ -74,7 +75,6 @@
         "zod-validation-error": "3.4.0"
         "zod-validation-error": "3.4.0"
     },
     },
     "devDependencies": {
     "devDependencies": {
-        "react-email": "3.0.2",
         "@dotenvx/dotenvx": "1.14.2",
         "@dotenvx/dotenvx": "1.14.2",
         "@esbuild-plugins/tsconfig-paths": "0.1.2",
         "@esbuild-plugins/tsconfig-paths": "0.1.2",
         "@types/better-sqlite3": "7.6.11",
         "@types/better-sqlite3": "7.6.11",
@@ -92,6 +92,7 @@
         "esbuild": "0.20.1",
         "esbuild": "0.20.1",
         "esbuild-node-externals": "1.13.0",
         "esbuild-node-externals": "1.13.0",
         "postcss": "^8",
         "postcss": "^8",
+        "react-email": "3.0.2",
         "tailwindcss": "^3.4.1",
         "tailwindcss": "^3.4.1",
         "tsc-alias": "1.8.10",
         "tsc-alias": "1.8.10",
         "tsx": "4.19.1",
         "tsx": "4.19.1",

+ 9 - 0
server/routers/auth/verifyTotp.ts

@@ -92,6 +92,15 @@ export async function verifyTotp(
 
 
         // TODO: send email to user confirming two-factor authentication is enabled
         // TODO: send email to user confirming two-factor authentication is enabled
 
 
+        if (!valid) {
+            return next(
+                createHttpError(
+                    HttpCode.BAD_REQUEST,
+                    "Invalid two-factor authentication code"
+                )
+            );
+        }
+
         return response<VerifyTotpResponse>(res, {
         return response<VerifyTotpResponse>(res, {
             data: {
             data: {
                 valid,
                 valid,

+ 3 - 6
src/app/[orgId]/layout.tsx

@@ -30,8 +30,8 @@ export default async function OrgLayout(props: {
         const getOrgUser = cache(() =>
         const getOrgUser = cache(() =>
             internal.get<AxiosResponse<GetOrgUserResponse>>(
             internal.get<AxiosResponse<GetOrgUserResponse>>(
                 `/org/${orgId}/user/${user.userId}`,
                 `/org/${orgId}/user/${user.userId}`,
-                cookie,
-            ),
+                cookie
+            )
         );
         );
         const orgUser = await getOrgUser();
         const orgUser = await getOrgUser();
     } catch {
     } catch {
@@ -40,10 +40,7 @@ export default async function OrgLayout(props: {
 
 
     try {
     try {
         const getOrg = cache(() =>
         const getOrg = cache(() =>
-            internal.get<AxiosResponse<GetOrgResponse>>(
-                `/org/${orgId}`,
-                cookie,
-            ),
+            internal.get<AxiosResponse<GetOrgResponse>>(`/org/${orgId}`, cookie)
         );
         );
         await getOrg();
         await getOrg();
     } catch {
     } catch {

+ 9 - 8
src/app/[orgId]/settings/layout.tsx

@@ -1,7 +1,7 @@
 import { Metadata } from "next";
 import { Metadata } from "next";
-import { TopbarNav } from "./components/TopbarNav";
+import { TopbarNav } from "@app/components/TopbarNav";
 import { Cog, Combine, Link, Settings, Users, Waypoints } from "lucide-react";
 import { Cog, Combine, Link, Settings, Users, Waypoints } from "lucide-react";
-import Header from "./components/Header";
+import { Header } from "@app/components/Header";
 import { verifySession } from "@app/lib/auth/verifySession";
 import { verifySession } from "@app/lib/auth/verifySession";
 import { redirect } from "next/navigation";
 import { redirect } from "next/navigation";
 import { internal } from "@app/api";
 import { internal } from "@app/api";
@@ -10,6 +10,7 @@ import { GetOrgResponse, ListOrgsResponse } from "@server/routers/org";
 import { authCookieHeader } from "@app/api/cookies";
 import { authCookieHeader } from "@app/api/cookies";
 import { cache } from "react";
 import { cache } from "react";
 import { GetOrgUserResponse } from "@server/routers/user";
 import { GetOrgUserResponse } from "@server/routers/user";
+import UserProvider from "@app/providers/UserProvider";
 
 
 export const dynamic = "force-dynamic";
 export const dynamic = "force-dynamic";
 
 
@@ -99,17 +100,17 @@ export default async function SettingsLayout(props: SettingsLayoutProps) {
             <div className="w-full border-b bg-neutral-100 dark:bg-neutral-800 select-none sm:px-0 px-3 fixed top-0 z-10">
             <div className="w-full border-b bg-neutral-100 dark:bg-neutral-800 select-none sm:px-0 px-3 fixed top-0 z-10">
                 <div className="container mx-auto flex flex-col content-between">
                 <div className="container mx-auto flex flex-col content-between">
                     <div className="my-4">
                     <div className="my-4">
-                        <Header
-                            email={user.email}
-                            orgId={params.orgId}
-                            orgs={orgs}
-                        />
+                        <UserProvider user={user}>
+                            <Header orgId={params.orgId} orgs={orgs} />
+                        </UserProvider>
                     </div>
                     </div>
                     <TopbarNav items={topNavItems} orgId={params.orgId} />
                     <TopbarNav items={topNavItems} orgId={params.orgId} />
                 </div>
                 </div>
             </div>
             </div>
 
 
-            <div className="container mx-auto sm:px-0 px-3 pt-[165px]">{children}</div>
+            <div className="container mx-auto sm:px-0 px-3 pt-[165px]">
+                {children}
+            </div>
         </>
         </>
     );
     );
 }
 }

+ 5 - 5
src/app/[orgId]/settings/share-links/components/CreateShareLinkForm.tsx

@@ -226,7 +226,7 @@ export default function CreateShareLinkForm({
             >
             >
                 <CredenzaContent>
                 <CredenzaContent>
                     <CredenzaHeader>
                     <CredenzaHeader>
-                        <CredenzaTitle>Create Sharable Link</CredenzaTitle>
+                        <CredenzaTitle>Create Shareable Link</CredenzaTitle>
                         <CredenzaDescription>
                         <CredenzaDescription>
                             Anyone with this link can access the resource
                             Anyone with this link can access the resource
                         </CredenzaDescription>
                         </CredenzaDescription>
@@ -436,10 +436,10 @@ export default function CreateShareLinkForm({
                                                 Expiration time is how long the
                                                 Expiration time is how long the
                                                 link will be usable and provide
                                                 link will be usable and provide
                                                 access to the resource. After
                                                 access to the resource. After
-                                                this time, the link will expire
-                                                and no longer work, and users
-                                                who used this link will lose
-                                                access to the resource.
+                                                this time, the link will no
+                                                longer work, and users who used
+                                                this link will lose access to
+                                                the resource.
                                             </p>
                                             </p>
                                         </div>
                                         </div>
                                     </form>
                                     </form>

+ 3 - 3
src/app/[orgId]/settings/sites/components/SitesTable.tsx

@@ -195,14 +195,14 @@ export default function SitesTable({ sites, orgId }: SitesTableProps) {
                 if (originalRow.online) {
                 if (originalRow.online) {
                     return (
                     return (
                         <span className="text-green-500 flex items-center space-x-2">
                         <span className="text-green-500 flex items-center space-x-2">
-                            <Check className="w-4 h-4" />
+                            <div className="w-2 h-2 bg-green-500 rounded-full"></div>
                             <span>Online</span>
                             <span>Online</span>
                         </span>
                         </span>
                     );
                     );
                 } else {
                 } else {
                     return (
                     return (
-                        <span className="text-red-500 flex items-center space-x-2">
-                            <X className="w-4 h-4" />
+                        <span className="text-gray-500 flex items-center space-x-2">
+                            <div className="w-2 h-2 bg-gray-500 rounded-full"></div>
                             <span>Offline</span>
                             <span>Offline</span>
                         </span>
                         </span>
                     );
                     );

+ 0 - 176
src/app/profile/account/account-form.tsx

@@ -1,176 +0,0 @@
-"use client"
-
-import { zodResolver } from "@hookform/resolvers/zod"
-import { CalendarIcon, CaretSortIcon, CheckIcon } from "@radix-ui/react-icons"
-import { useForm } from "react-hook-form"
-import { z } from "zod"
-
-import { cn } from "@/lib/utils"
-import { toast } from "@/hooks/useToast"
-import { Button } from "@/components/ui/button"
-import {
-  Command,
-  CommandEmpty,
-  CommandGroup,
-  CommandInput,
-  CommandItem,
-  CommandList,
-} from "@/components/ui/command"
-import {
-  Form,
-  FormControl,
-  FormDescription,
-  FormField,
-  FormItem,
-  FormLabel,
-  FormMessage,
-} from "@/components/ui/form"
-import { Input } from "@/components/ui/input"
-import {
-  Popover,
-  PopoverContent,
-  PopoverTrigger,
-} from "@/components/ui/popover"
-
-const languages = [
-  { label: "English", value: "en" },
-  { label: "French", value: "fr" },
-  { label: "German", value: "de" },
-  { label: "Spanish", value: "es" },
-  { label: "Portuguese", value: "pt" },
-  { label: "Russian", value: "ru" },
-  { label: "Japanese", value: "ja" },
-  { label: "Korean", value: "ko" },
-  { label: "Chinese", value: "zh" },
-] as const
-
-const accountFormSchema = z.object({
-  name: z
-    .string()
-    .min(2, {
-      message: "Name must be at least 2 characters.",
-    })
-    .max(30, {
-      message: "Name must not be longer than 30 characters.",
-    }),
-  dob: z.date({
-    required_error: "A date of birth is required.",
-  }),
-  language: z.string({
-    required_error: "Please select a language.",
-  }),
-})
-
-type AccountFormValues = z.infer<typeof accountFormSchema>
-
-// This can come from your database or API.
-const defaultValues: Partial<AccountFormValues> = {
-  // name: "Your name",
-  // dob: new Date("2023-01-23"),
-}
-
-export function AccountForm() {
-  const form = useForm<AccountFormValues>({
-    resolver: zodResolver(accountFormSchema),
-    defaultValues,
-  })
-
-  function onSubmit(data: AccountFormValues) {
-    toast({
-      title: "You submitted the following values:",
-      description: (
-        <pre className="mt-2 w-[340px] rounded-md bg-slate-950 p-4">
-          <code className="text-white">{JSON.stringify(data, null, 2)}</code>
-        </pre>
-      ),
-    })
-  }
-
-  return (
-    <Form {...form}>
-      <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
-        <FormField
-          control={form.control}
-          name="name"
-          render={({ field }) => (
-            <FormItem>
-              <FormLabel>Name</FormLabel>
-              <FormControl>
-                <Input placeholder="Your name" {...field} />
-              </FormControl>
-              <FormDescription>
-                This is the name that will be displayed on your profile and in
-                emails.
-              </FormDescription>
-              <FormMessage />
-            </FormItem>
-          )}
-        />
-        <FormField
-          control={form.control}
-          name="language"
-          render={({ field }) => (
-            <FormItem className="flex flex-col">
-              <FormLabel>Language</FormLabel>
-              <Popover>
-                <PopoverTrigger asChild>
-                  <FormControl>
-                    <Button
-                      variant="outline"
-                      role="combobox"
-                      className={cn(
-                        "w-[200px] justify-between",
-                        !field.value && "text-muted-foreground"
-                      )}
-                    >
-                      {field.value
-                        ? languages.find(
-                            (language) => language.value === field.value
-                          )?.label
-                        : "Select language"}
-                      <CaretSortIcon className="ml-2 h-4 w-4 shrink-0 opacity-50" />
-                    </Button>
-                  </FormControl>
-                </PopoverTrigger>
-                <PopoverContent className="w-[200px] p-0">
-                  <Command>
-                    <CommandInput placeholder="Search language..." />
-                    <CommandList>
-                      <CommandEmpty>No language found.</CommandEmpty>
-                      <CommandGroup>
-                        {languages.map((language) => (
-                          <CommandItem
-                            value={language.label}
-                            key={language.value}
-                            onSelect={() => {
-                              form.setValue("language", language.value)
-                            }}
-                          >
-                            <CheckIcon
-                              className={cn(
-                                "mr-2 h-4 w-4",
-                                language.value === field.value
-                                  ? "opacity-100"
-                                  : "opacity-0"
-                              )}
-                            />
-                            {language.label}
-                          </CommandItem>
-                        ))}
-                      </CommandGroup>
-                    </CommandList>
-                  </Command>
-                </PopoverContent>
-              </Popover>
-              <FormDescription>
-                This is the language that will be used in the dashboard.
-              </FormDescription>
-              <FormMessage />
-            </FormItem>
-          )}
-        />
-        <Button type="submit">Update account</Button>
-      </form>
-    </Form>
-  )
-}

+ 0 - 18
src/app/profile/account/page.tsx

@@ -1,18 +0,0 @@
-import { Separator } from "@/components/ui/separator"
-import { AccountForm } from "./account-form"
-
-export default function SettingsAccountPage() {
-  return (
-    <div className="space-y-8">
-      <div>
-        <h3 className="text-lg font-medium">Account</h3>
-        <p className="text-sm text-muted-foreground">
-          Update your account settings. Set your preferred language and
-          timezone.
-        </p>
-      </div>
-      <Separator />
-      <AccountForm />
-    </div>
-  )
-}

+ 0 - 164
src/app/profile/appearance/appearance-form.tsx

@@ -1,164 +0,0 @@
-"use client"
-
-import { zodResolver } from "@hookform/resolvers/zod"
-import { ChevronDownIcon } from "@radix-ui/react-icons"
-import { useForm } from "react-hook-form"
-import { z } from "zod"
-
-import { cn } from "@/lib/utils"
-import { toast } from "@/hooks/useToast"
-import { Button, buttonVariants } from "@/components/ui/button"
-import {
-  Form,
-  FormControl,
-  FormDescription,
-  FormField,
-  FormItem,
-  FormLabel,
-  FormMessage,
-} from "@/components/ui/form"
-import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"
-
-const appearanceFormSchema = z.object({
-  theme: z.enum(["light", "dark"], {
-    required_error: "Please select a theme.",
-  }),
-  font: z.enum(["inter", "manrope", "system"], {
-    invalid_type_error: "Select a font",
-    required_error: "Please select a font.",
-  }),
-})
-
-type AppearanceFormValues = z.infer<typeof appearanceFormSchema>
-
-// This can come from your database or API.
-const defaultValues: Partial<AppearanceFormValues> = {
-  theme: "light",
-}
-
-export function AppearanceForm() {
-  const form = useForm<AppearanceFormValues>({
-    resolver: zodResolver(appearanceFormSchema),
-    defaultValues,
-  })
-
-  function onSubmit(data: AppearanceFormValues) {
-    toast({
-      title: "You submitted the following values:",
-      description: (
-        <pre className="mt-2 w-[340px] rounded-md bg-slate-950 p-4">
-          <code className="text-white">{JSON.stringify(data, null, 2)}</code>
-        </pre>
-      ),
-    })
-  }
-
-  return (
-    <Form {...form}>
-      <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
-        <FormField
-          control={form.control}
-          name="font"
-          render={({ field }) => (
-            <FormItem>
-              <FormLabel>Font</FormLabel>
-              <div className="relative w-max">
-                <FormControl>
-                  <select
-                    className={cn(
-                      buttonVariants({ variant: "outline" }),
-                      "w-[200px] appearance-none font-normal"
-                    )}
-                    {...field}
-                  >
-                    <option value="inter">Inter</option>
-                    <option value="manrope">Manrope</option>
-                    <option value="system">System</option>
-                  </select>
-                </FormControl>
-                <ChevronDownIcon className="absolute right-3 top-2.5 h-4 w-4 opacity-50" />
-              </div>
-              <FormDescription>
-                Set the font you want to use in the dashboard.
-              </FormDescription>
-              <FormMessage />
-            </FormItem>
-          )}
-        />
-        <FormField
-          control={form.control}
-          name="theme"
-          render={({ field }) => (
-            <FormItem className="space-y-1">
-              <FormLabel>Theme</FormLabel>
-              <FormDescription>
-                Select the theme for the dashboard.
-              </FormDescription>
-              <FormMessage />
-              <RadioGroup
-                onValueChange={field.onChange}
-                defaultValue={field.value}
-                className="grid max-w-md grid-cols-2 gap-8 pt-2"
-              >
-                <FormItem>
-                  <FormLabel className="[&:has([data-state=checked])>div]:border-primary">
-                    <FormControl>
-                      <RadioGroupItem value="light" className="sr-only" />
-                    </FormControl>
-                    <div className="items-center rounded-md border-2 border-muted p-1 hover:border-accent">
-                      <div className="space-y-2 rounded-sm bg-[#ecedef] p-2">
-                        <div className="space-y-2 rounded-md bg-white p-2 shadow-sm">
-                          <div className="h-2 w-[80px] rounded-lg bg-[#ecedef]" />
-                          <div className="h-2 w-[100px] rounded-lg bg-[#ecedef]" />
-                        </div>
-                        <div className="flex items-center space-x-2 rounded-md bg-white p-2 shadow-sm">
-                          <div className="h-4 w-4 rounded-full bg-[#ecedef]" />
-                          <div className="h-2 w-[100px] rounded-lg bg-[#ecedef]" />
-                        </div>
-                        <div className="flex items-center space-x-2 rounded-md bg-white p-2 shadow-sm">
-                          <div className="h-4 w-4 rounded-full bg-[#ecedef]" />
-                          <div className="h-2 w-[100px] rounded-lg bg-[#ecedef]" />
-                        </div>
-                      </div>
-                    </div>
-                    <span className="block w-full p-2 text-center font-normal">
-                      Light
-                    </span>
-                  </FormLabel>
-                </FormItem>
-                <FormItem>
-                  <FormLabel className="[&:has([data-state=checked])>div]:border-primary">
-                    <FormControl>
-                      <RadioGroupItem value="dark" className="sr-only" />
-                    </FormControl>
-                    <div className="items-center rounded-md border-2 border-muted bg-popover p-1 hover:bg-accent hover:text-accent-foreground">
-                      <div className="space-y-2 rounded-sm bg-slate-950 p-2">
-                        <div className="space-y-2 rounded-md bg-slate-800 p-2 shadow-sm">
-                          <div className="h-2 w-[80px] rounded-lg bg-slate-400" />
-                          <div className="h-2 w-[100px] rounded-lg bg-slate-400" />
-                        </div>
-                        <div className="flex items-center space-x-2 rounded-md bg-slate-800 p-2 shadow-sm">
-                          <div className="h-4 w-4 rounded-full bg-slate-400" />
-                          <div className="h-2 w-[100px] rounded-lg bg-slate-400" />
-                        </div>
-                        <div className="flex items-center space-x-2 rounded-md bg-slate-800 p-2 shadow-sm">
-                          <div className="h-4 w-4 rounded-full bg-slate-400" />
-                          <div className="h-2 w-[100px] rounded-lg bg-slate-400" />
-                        </div>
-                      </div>
-                    </div>
-                    <span className="block w-full p-2 text-center font-normal">
-                      Dark
-                    </span>
-                  </FormLabel>
-                </FormItem>
-              </RadioGroup>
-            </FormItem>
-          )}
-        />
-
-        <Button type="submit">Update preferences</Button>
-      </form>
-    </Form>
-  )
-}

+ 0 - 18
src/app/profile/appearance/page.tsx

@@ -1,18 +0,0 @@
-import { Separator } from "@/components/ui/separator"
-import { AppearanceForm } from "./appearance-form"
-
-export default function SettingsAppearancePage() {
-  return (
-    <div className="space-y-8">
-      <div>
-        <h3 className="text-lg font-medium">Appearance</h3>
-        <p className="text-sm text-muted-foreground">
-          Customize the appearance of the app. Automatically switch between day
-          and night themes.
-        </p>
-      </div>
-      <Separator />
-      <AppearanceForm />
-    </div>
-  )
-}

+ 0 - 132
src/app/profile/display/display-form.tsx

@@ -1,132 +0,0 @@
-"use client"
-
-import { zodResolver } from "@hookform/resolvers/zod"
-import { useForm } from "react-hook-form"
-import { z } from "zod"
-
-import { toast } from "@/hooks/useToast"
-import { Button } from "@/components/ui/button"
-import { Checkbox } from "@/components/ui/checkbox"
-import {
-  Form,
-  FormControl,
-  FormDescription,
-  FormField,
-  FormItem,
-  FormLabel,
-  FormMessage,
-} from "@/components/ui/form"
-
-const items = [
-  {
-    id: "recents",
-    label: "Recents",
-  },
-  {
-    id: "home",
-    label: "Home",
-  },
-  {
-    id: "applications",
-    label: "Applications",
-  },
-  {
-    id: "desktop",
-    label: "Desktop",
-  },
-  {
-    id: "downloads",
-    label: "Downloads",
-  },
-  {
-    id: "documents",
-    label: "Documents",
-  },
-] as const
-
-const displayFormSchema = z.object({
-  items: z.array(z.string()).refine((value) => value.some((item) => item), {
-    message: "You have to select at least one item.",
-  }),
-})
-
-type DisplayFormValues = z.infer<typeof displayFormSchema>
-
-// This can come from your database or API.
-const defaultValues: Partial<DisplayFormValues> = {
-  items: ["recents", "home"],
-}
-
-export function DisplayForm() {
-  const form = useForm<DisplayFormValues>({
-    resolver: zodResolver(displayFormSchema),
-    defaultValues,
-  })
-
-  function onSubmit(data: DisplayFormValues) {
-    toast({
-      title: "You submitted the following values:",
-      description: (
-        <pre className="mt-2 w-[340px] rounded-md bg-slate-950 p-4">
-          <code className="text-white">{JSON.stringify(data, null, 2)}</code>
-        </pre>
-      ),
-    })
-  }
-
-  return (
-    <Form {...form}>
-      <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
-        <FormField
-          control={form.control}
-          name="items"
-          render={() => (
-            <FormItem>
-              <div className="mb-4">
-                <FormLabel className="text-base">Sidebar</FormLabel>
-                <FormDescription>
-                  Select the items you want to display in the sidebar.
-                </FormDescription>
-              </div>
-              {items.map((item) => (
-                <FormField
-                  key={item.id}
-                  control={form.control}
-                  name="items"
-                  render={({ field }) => {
-                    return (
-                      <FormItem
-                        key={item.id}
-                        className="flex flex-row items-start space-x-3 space-y-0"
-                      >
-                        <FormControl>
-                          <Checkbox
-                            checked={field.value?.includes(item.id)}
-                            onCheckedChange={(checked) => {
-                              return checked
-                                ? field.onChange([...field.value, item.id])
-                                : field.onChange(
-                                    field.value?.filter(
-                                      (value) => value !== item.id
-                                    )
-                                  )
-                            }}
-                          />
-                        </FormControl>
-                        <FormLabel className="font-normal">
-                          {item.label}
-                        </FormLabel>
-                      </FormItem>
-                    )
-                  }}
-                />
-              ))}
-              <FormMessage />
-            </FormItem>
-          )}
-        />
-        <Button type="submit">Update display</Button>
-      </form>
-    </Form>
-  )
-}

+ 0 - 17
src/app/profile/display/page.tsx

@@ -1,17 +0,0 @@
-import { Separator } from "@/components/ui/separator"
-import { DisplayForm } from "./display-form"
-
-export default function SettingsDisplayPage() {
-  return (
-    <div className="space-y-8">
-      <div>
-        <h3 className="text-lg font-medium">Display</h3>
-        <p className="text-sm text-muted-foreground">
-          Turn items on or off to control what&apos;s displayed in the app.
-        </p>
-      </div>
-      <Separator />
-      <DisplayForm />
-    </div>
-  )
-}

+ 36 - 0
src/app/profile/general/layout_.tsx

@@ -0,0 +1,36 @@
+import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
+import { SidebarSettings } from "@app/components/SidebarSettings";
+import { verifySession } from "@app/lib/auth/verifySession";
+import UserProvider from "@app/providers/UserProvider";
+import { redirect } from "next/navigation";
+import { cache } from "react";
+
+type ProfileGeneralProps = {
+    children: React.ReactNode;
+};
+
+export default async function GeneralSettingsPage({
+    children
+}: ProfileGeneralProps) {
+    const getUser = cache(verifySession);
+    const user = await getUser();
+
+    if (!user) {
+        redirect(`/?redirect=/profile/general`);
+    }
+
+    const sidebarNavItems = [
+        {
+            title: "Authentication",
+            href: `/{orgId}/settings/general`
+        }
+    ];
+
+    return (
+        <>
+            <UserProvider user={user}>
+                {children}
+            </UserProvider>
+        </>
+    );
+}

+ 14 - 0
src/app/profile/general/page_.tsx

@@ -0,0 +1,14 @@
+"use client";
+
+import { useState } from "react";
+import Enable2FaForm from "./components/Enable2FaForm";
+
+export default function ProfileGeneralPage() {
+    const [open, setOpen] = useState(true);
+
+    return (
+        <>
+            <Enable2FaForm open={open} setOpen={setOpen} />
+        </>
+    );
+}

+ 0 - 76
src/app/profile/layout.tsx

@@ -1,76 +0,0 @@
-import { Metadata } from "next"
-import Image from "next/image"
-
-import { Separator } from "@/components/ui/separator"
-import { SidebarNav } from "@/components/sidebar-nav"
-import Header from "../[orgId]/settings/components/Header"
-
-export const metadata: Metadata = {
-  title: "Forms",
-  description: "Advanced form example using react-hook-form and Zod.",
-}
-
-const sidebarNavItems = [
-  {
-    title: "Profile",
-    href: "/configuration",
-  },
-  {
-    title: "Account",
-    href: "/configuration/account",
-  },
-  {
-    title: "Appearance",
-    href: "/configuration/appearance",
-  },
-  {
-    title: "Notifications",
-    href: "/configuration/notifications",
-  },
-  {
-    title: "Display",
-    href: "/configuration/display",
-  },
-]
-
-interface SettingsLayoutProps {
-  children: React.ReactNode
-}
-
-export default function SettingsLayout({ children }: SettingsLayoutProps) {
-  return (
-    <>
-      <div className="md:hidden">
-        <Image
-          src="/configuration/forms-light.png"
-          width={1280}
-          height={791}
-          alt="Forms"
-          className="block dark:hidden"
-        />
-        <Image
-          src="/configuration/forms-dark.png"
-          width={1280}
-          height={791}
-          alt="Forms"
-          className="hidden dark:block"
-        />
-      </div>
-      <div className="hidden space-y-8 p-10 pb-16 md:block">
-        <div className="space-y-0.5">
-          <h2 className="text-2xl font-bold tracking-tight">Settings</h2>
-          <p className="text-muted-foreground">
-            Manage your account settings and set e-mail preferences.
-          </p>
-        </div>
-        <Separator className="my-6" />
-        <div className="flex flex-col space-y-4 lg:flex-row lg:space-x-12 lg:space-y-0">
-          <aside className="-mx-4 lg:w-1/5">
-            <SidebarNav items={sidebarNavItems} />
-          </aside>
-          <div className="flex-1 lg:max-w-2xl">{children}</div>
-        </div>
-      </div>
-    </>
-  )
-}

+ 74 - 0
src/app/profile/layout_.tsx

@@ -0,0 +1,74 @@
+import { Metadata } from "next";
+import { verifySession } from "@app/lib/auth/verifySession";
+import { redirect } from "next/navigation";
+import { cache } from "react";
+import Header from "@app/components/Header";
+import { internal } from "@app/api";
+import { AxiosResponse } from "axios";
+import { ListOrgsResponse } from "@server/routers/org";
+import { authCookieHeader } from "@app/api/cookies";
+import { TopbarNav } from "@app/components/TopbarNav";
+import { Settings } from "lucide-react";
+
+export const dynamic = "force-dynamic";
+
+export const metadata: Metadata = {
+    title: `User Settings - Pangolin`,
+    description: ""
+};
+
+const topNavItems = [
+    {
+        title: "User Settings",
+        href: "/profile/general",
+        icon: <Settings className="h-4 w-4" />
+    }
+];
+
+interface SettingsLayoutProps {
+    children: React.ReactNode;
+    params: Promise<{}>;
+}
+
+export default async function SettingsLayout(props: SettingsLayoutProps) {
+    const { children } = props;
+
+    const getUser = cache(verifySession);
+    const user = await getUser();
+
+    if (!user) {
+        redirect(`/`);
+    }
+
+    const cookie = await authCookieHeader();
+
+    let orgs: ListOrgsResponse["orgs"] = [];
+    try {
+        const getOrgs = cache(() =>
+            internal.get<AxiosResponse<ListOrgsResponse>>(`/orgs`, cookie)
+        );
+        const res = await getOrgs();
+        if (res && res.data.data.orgs) {
+            orgs = res.data.data.orgs;
+        }
+    } catch (e) {
+        console.error("Error fetching orgs", e);
+    }
+
+    return (
+        <>
+            <div className="w-full border-b bg-neutral-100 dark:bg-neutral-800 select-none sm:px-0 px-3 fixed top-0 z-10">
+                <div className="container mx-auto flex flex-col content-between">
+                    <div className="my-4">
+                        <Header email={user.email} orgs={orgs} />
+                    </div>
+                    <TopbarNav items={topNavItems} />
+                </div>
+            </div>
+
+            <div className="container mx-auto sm:px-0 px-3 pt-[165px]">
+                {children}
+            </div>
+        </>
+    );
+}

+ 0 - 222
src/app/profile/notifications/notifications-form.tsx

@@ -1,222 +0,0 @@
-"use client"
-
-import Link from "next/link"
-import { zodResolver } from "@hookform/resolvers/zod"
-import { useForm } from "react-hook-form"
-import { z } from "zod"
-
-import { toast } from "@/hooks/useToast"
-import { Button } from "@/components/ui/button"
-import { Checkbox } from "@/components/ui/checkbox"
-import {
-  Form,
-  FormControl,
-  FormDescription,
-  FormField,
-  FormItem,
-  FormLabel,
-  FormMessage,
-} from "@/components/ui/form"
-import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"
-import { Switch } from "@/components/ui/switch"
-
-const notificationsFormSchema = z.object({
-  type: z.enum(["all", "mentions", "none"], {
-    required_error: "You need to select a notification type.",
-  }),
-  mobile: z.boolean().default(false).optional(),
-  communication_emails: z.boolean().default(false).optional(),
-  social_emails: z.boolean().default(false).optional(),
-  marketing_emails: z.boolean().default(false).optional(),
-  security_emails: z.boolean(),
-})
-
-type NotificationsFormValues = z.infer<typeof notificationsFormSchema>
-
-// This can come from your database or API.
-const defaultValues: Partial<NotificationsFormValues> = {
-  communication_emails: false,
-  marketing_emails: false,
-  social_emails: true,
-  security_emails: true,
-}
-
-export function NotificationsForm() {
-  const form = useForm<NotificationsFormValues>({
-    resolver: zodResolver(notificationsFormSchema),
-    defaultValues,
-  })
-
-  function onSubmit(data: NotificationsFormValues) {
-    toast({
-      title: "You submitted the following values:",
-      description: (
-        <pre className="mt-2 w-[340px] rounded-md bg-slate-950 p-4">
-          <code className="text-white">{JSON.stringify(data, null, 2)}</code>
-        </pre>
-      ),
-    })
-  }
-
-  return (
-    <Form {...form}>
-      <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
-        <FormField
-          control={form.control}
-          name="type"
-          render={({ field }) => (
-            <FormItem className="space-y-3">
-              <FormLabel>Notify me about...</FormLabel>
-              <FormControl>
-                <RadioGroup
-                  onValueChange={field.onChange}
-                  defaultValue={field.value}
-                  className="flex flex-col space-y-1"
-                >
-                  <FormItem className="flex items-center space-x-3 space-y-0">
-                    <FormControl>
-                      <RadioGroupItem value="all" />
-                    </FormControl>
-                    <FormLabel className="font-normal">
-                      All new messages
-                    </FormLabel>
-                  </FormItem>
-                  <FormItem className="flex items-center space-x-3 space-y-0">
-                    <FormControl>
-                      <RadioGroupItem value="mentions" />
-                    </FormControl>
-                    <FormLabel className="font-normal">
-                      Direct messages and mentions
-                    </FormLabel>
-                  </FormItem>
-                  <FormItem className="flex items-center space-x-3 space-y-0">
-                    <FormControl>
-                      <RadioGroupItem value="none" />
-                    </FormControl>
-                    <FormLabel className="font-normal">Nothing</FormLabel>
-                  </FormItem>
-                </RadioGroup>
-              </FormControl>
-              <FormMessage />
-            </FormItem>
-          )}
-        />
-        <div>
-          <h3 className="mb-4 text-lg font-medium">Email Notifications</h3>
-          <div className="space-y-4">
-            <FormField
-              control={form.control}
-              name="communication_emails"
-              render={({ field }) => (
-                <FormItem className="flex flex-row items-center justify-between rounded-lg border p-4">
-                  <div className="space-y-0.5">
-                    <FormLabel className="text-base">
-                      Communication emails
-                    </FormLabel>
-                    <FormDescription>
-                      Receive emails about your account activity.
-                    </FormDescription>
-                  </div>
-                  <FormControl>
-                    <Switch
-                      checked={field.value}
-                      onCheckedChange={field.onChange}
-                    />
-                  </FormControl>
-                </FormItem>
-              )}
-            />
-            <FormField
-              control={form.control}
-              name="marketing_emails"
-              render={({ field }) => (
-                <FormItem className="flex flex-row items-center justify-between rounded-lg border p-4">
-                  <div className="space-y-0.5">
-                    <FormLabel className="text-base">
-                      Marketing emails
-                    </FormLabel>
-                    <FormDescription>
-                      Receive emails about new products, features, and more.
-                    </FormDescription>
-                  </div>
-                  <FormControl>
-                    <Switch
-                      checked={field.value}
-                      onCheckedChange={field.onChange}
-                    />
-                  </FormControl>
-                </FormItem>
-              )}
-            />
-            <FormField
-              control={form.control}
-              name="social_emails"
-              render={({ field }) => (
-                <FormItem className="flex flex-row items-center justify-between rounded-lg border p-4">
-                  <div className="space-y-0.5">
-                    <FormLabel className="text-base">Social emails</FormLabel>
-                    <FormDescription>
-                      Receive emails for friend requests, follows, and more.
-                    </FormDescription>
-                  </div>
-                  <FormControl>
-                    <Switch
-                      checked={field.value}
-                      onCheckedChange={field.onChange}
-                    />
-                  </FormControl>
-                </FormItem>
-              )}
-            />
-            <FormField
-              control={form.control}
-              name="security_emails"
-              render={({ field }) => (
-                <FormItem className="flex flex-row items-center justify-between rounded-lg border p-4">
-                  <div className="space-y-0.5">
-                    <FormLabel className="text-base">Security emails</FormLabel>
-                    <FormDescription>
-                      Receive emails about your account activity and security.
-                    </FormDescription>
-                  </div>
-                  <FormControl>
-                    <Switch
-                      checked={field.value}
-                      onCheckedChange={field.onChange}
-                      disabled
-                      aria-readonly
-                    />
-                  </FormControl>
-                </FormItem>
-              )}
-            />
-          </div>
-        </div>
-        <FormField
-          control={form.control}
-          name="mobile"
-          render={({ field }) => (
-            <FormItem className="flex flex-row items-start space-x-3 space-y-0">
-              <FormControl>
-                <Checkbox
-                  checked={field.value}
-                  onCheckedChange={field.onChange}
-                />
-              </FormControl>
-              <div className="space-y-1 leading-none">
-                <FormLabel>
-                  Use different settings for my mobile devices
-                </FormLabel>
-                <FormDescription>
-                  You can manage your mobile notifications in the{" "}
-                  <Link href="/examples/forms">mobile settings</Link> page.
-                </FormDescription>
-              </div>
-            </FormItem>
-          )}
-        />
-        <Button type="submit">Update notifications</Button>
-      </form>
-    </Form>
-  )
-}

+ 0 - 17
src/app/profile/notifications/page.tsx

@@ -1,17 +0,0 @@
-import { Separator } from "@/components/ui/separator"
-import { NotificationsForm } from "./notifications-form"
-
-export default function SettingsNotificationsPage() {
-  return (
-    <div className="space-y-8">
-      <div>
-        <h3 className="text-lg font-medium">Notifications</h3>
-        <p className="text-sm text-muted-foreground">
-          Configure how you receive notifications.
-        </p>
-      </div>
-      <Separator />
-      <NotificationsForm />
-    </div>
-  )
-}

+ 0 - 17
src/app/profile/page.tsx

@@ -1,17 +0,0 @@
-import { Separator } from "@/components/ui/separator"
-import { ProfileForm } from "@app/components/profile-form"
-
-export default function SettingsProfilePage() {
-  return (
-    <div className="space-y-8">
-      <div>
-        <h3 className="text-lg font-medium">Profile</h3>
-        <p className="text-sm text-muted-foreground">
-          This is how others will see you on the site.
-        </p>
-      </div>
-      <Separator />
-      <ProfileForm />
-    </div>
-  )
-}

+ 5 - 0
src/app/profile/page_.tsx

@@ -0,0 +1,5 @@
+import { redirect } from "next/navigation";
+
+export default async function ProfilePage() {
+    redirect("/profile/general");
+}

+ 0 - 192
src/app/profile/profile-form.tsx

@@ -1,192 +0,0 @@
-"use client"
-
-import Link from "next/link"
-import { zodResolver } from "@hookform/resolvers/zod"
-import { useFieldArray, useForm } from "react-hook-form"
-import { z } from "zod"
-
-import { cn } from "@/lib/utils"
-import { toast } from "@/hooks/useToast"
-
-import { Button } from "@/components/ui/button"
-import {
-  Form,
-  FormControl,
-  FormDescription,
-  FormField,
-  FormItem,
-  FormLabel,
-  FormMessage,
-} from "@/components/ui/form"
-import { Input } from "@/components/ui/input"
-import {
-  Select,
-  SelectContent,
-  SelectItem,
-  SelectTrigger,
-  SelectValue,
-} from "@/components/ui/select"
-import { Textarea } from "@/components/ui/textarea"
-
-const profileFormSchema = z.object({
-  username: z
-    .string()
-    .min(2, {
-      message: "Username must be at least 2 characters.",
-    })
-    .max(30, {
-      message: "Username must not be longer than 30 characters.",
-    }),
-  email: z
-    .string({
-      required_error: "Please select an email to display.",
-    })
-    .email(),
-  bio: z.string().max(160).min(4),
-  urls: z
-    .array(
-      z.object({
-        value: z.string().url({ message: "Please enter a valid URL." }),
-      })
-    )
-    .optional(),
-})
-
-type ProfileFormValues = z.infer<typeof profileFormSchema>
-
-// This can come from your database or API.
-const defaultValues: Partial<ProfileFormValues> = {
-  bio: "I own a computer.",
-  urls: [
-    { value: "https://shadcn.com" },
-    { value: "http://twitter.com/shadcn" },
-  ],
-}
-
-export function ProfileForm() {
-  const form = useForm<ProfileFormValues>({
-    resolver: zodResolver(profileFormSchema),
-    defaultValues,
-    mode: "onChange",
-  })
-
-  const { fields, append } = useFieldArray({
-    name: "urls",
-    control: form.control,
-  })
-
-  function onSubmit(data: ProfileFormValues) {
-    toast({
-      title: "You submitted the following values:",
-      description: (
-        <pre className="mt-2 w-[340px] rounded-md bg-slate-950 p-4">
-          <code className="text-white">{JSON.stringify(data, null, 2)}</code>
-        </pre>
-      ),
-    })
-  }
-
-  return (
-    <Form {...form}>
-      <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
-        <FormField
-          control={form.control}
-          name="username"
-          render={({ field }) => (
-            <FormItem>
-              <FormLabel>Username</FormLabel>
-              <FormControl>
-                <Input placeholder="shadcn" {...field} />
-              </FormControl>
-              <FormDescription>
-                This is your public display name. It can be your real name or a
-                pseudonym. You can only change this once every 30 days.
-              </FormDescription>
-              <FormMessage />
-            </FormItem>
-          )}
-        />
-        <FormField
-          control={form.control}
-          name="email"
-          render={({ field }) => (
-            <FormItem>
-              <FormLabel>Email</FormLabel>
-              <Select onValueChange={field.onChange} defaultValue={field.value}>
-                <FormControl>
-                  <SelectTrigger>
-                    <SelectValue placeholder="Select a verified email to display" />
-                  </SelectTrigger>
-                </FormControl>
-                <SelectContent>
-                  <SelectItem value="m@example.com">m@example.com</SelectItem>
-                  <SelectItem value="m@google.com">m@google.com</SelectItem>
-                  <SelectItem value="m@support.com">m@support.com</SelectItem>
-                </SelectContent>
-              </Select>
-              <FormDescription>
-                You can manage verified email addresses in your{" "}
-                <Link href="/examples/forms">email settings</Link>.
-              </FormDescription>
-              <FormMessage />
-            </FormItem>
-          )}
-        />
-        <FormField
-          control={form.control}
-          name="bio"
-          render={({ field }) => (
-            <FormItem>
-              <FormLabel>Bio</FormLabel>
-              <FormControl>
-                <Textarea
-                  placeholder="Tell us a little bit about yourself"
-                  className="resize-none"
-                  {...field}
-                />
-              </FormControl>
-              <FormDescription>
-                You can <span>@mention</span> other users and organizations to
-                link to them.
-              </FormDescription>
-              <FormMessage />
-            </FormItem>
-          )}
-        />
-        <div>
-          {fields.map((field, index) => (
-            <FormField
-              control={form.control}
-              key={field.id}
-              name={`urls.${index}.value`}
-              render={({ field }) => (
-                <FormItem>
-                  <FormLabel className={cn(index !== 0 && "sr-only")}>
-                    URLs
-                  </FormLabel>
-                  <FormDescription className={cn(index !== 0 && "sr-only")}>
-                    Add links to your website, blog, or social media profiles.
-                  </FormDescription>
-                  <FormControl>
-                    <Input {...field} />
-                  </FormControl>
-                  <FormMessage />
-                </FormItem>
-              )}
-            />
-          ))}
-          <Button
-            type="button"
-            variant="outline"
-            size="sm"
-            className="mt-2"
-            onClick={() => append({ value: "" })}
-          >
-            Add URL
-          </Button>
-        </div>
-        <Button type="submit">Update profile</Button>
-      </form>
-    </Form>
-  )
-}

+ 291 - 0
src/components/Enable2FaForm.tsx

@@ -0,0 +1,291 @@
+"use client";
+
+import { useState } from "react";
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import { Label } from "@/components/ui/label";
+import { AlertCircle, CheckCircle2 } from "lucide-react";
+import { createApiClient } from "@app/api";
+import { useEnvContext } from "@app/hooks/useEnvContext";
+import { AxiosResponse } from "axios";
+import {
+    RequestTotpSecretBody,
+    RequestTotpSecretResponse,
+    VerifyTotpBody,
+    VerifyTotpResponse
+} from "@server/routers/auth";
+import { z } from "zod";
+import { useForm } from "react-hook-form";
+import { zodResolver } from "@hookform/resolvers/zod";
+import {
+    Form,
+    FormControl,
+    FormField,
+    FormItem,
+    FormLabel,
+    FormMessage
+} from "@app/components/ui/form";
+import {
+    Credenza,
+    CredenzaBody,
+    CredenzaClose,
+    CredenzaContent,
+    CredenzaDescription,
+    CredenzaFooter,
+    CredenzaHeader,
+    CredenzaTitle
+} from "@app/components/Credenza";
+import { useToast } from "@app/hooks/useToast";
+import { formatAxiosError } from "@app/lib/utils";
+import CopyTextBox from "@app/components/CopyTextBox";
+import { QRCodeSVG } from "qrcode.react";
+import { userUserContext } from "@app/hooks/useUserContext";
+
+const enableSchema = z.object({
+    password: z.string().min(1, { message: "Password is required" })
+});
+
+const confirmSchema = z.object({
+    code: z.string().length(6, { message: "Invalid code" })
+});
+
+type Enable2FaProps = {
+    open: boolean;
+    setOpen: (val: boolean) => void;
+};
+
+export default function Enable2FaForm({ open, setOpen }: Enable2FaProps) {
+    const [step, setStep] = useState(1);
+    const [secretKey, setSecretKey] = useState("");
+    const [verificationCode, setVerificationCode] = useState("");
+    const [error, setError] = useState("");
+    const [success, setSuccess] = useState(false);
+    const [loading, setLoading] = useState(false);
+    const [backupCodes, setBackupCodes] = useState<string[]>([]);
+
+    const { toast } = useToast();
+
+    const { user, updateUser } = userUserContext();
+
+    const api = createApiClient(useEnvContext());
+
+    const enableForm = useForm<z.infer<typeof enableSchema>>({
+        resolver: zodResolver(enableSchema),
+        defaultValues: {
+            password: ""
+        }
+    });
+
+    const confirmForm = useForm<z.infer<typeof confirmSchema>>({
+        resolver: zodResolver(confirmSchema),
+        defaultValues: {
+            code: ""
+        }
+    });
+
+    const request2fa = async (values: z.infer<typeof enableSchema>) => {
+        setLoading(true);
+
+        const res = await api
+            .post<AxiosResponse<RequestTotpSecretResponse>>(
+                `/auth/2fa/request`,
+                {
+                    password: values.password
+                } as RequestTotpSecretBody
+            )
+            .catch((e) => {
+                toast({
+                    title: "Unable to enable 2FA",
+                    description: formatAxiosError(
+                        e,
+                        "An error occurred while enabling 2FA"
+                    ),
+                    variant: "destructive"
+                });
+            });
+
+        if (res && res.data.data.secret) {
+            setSecretKey(res.data.data.secret);
+            setStep(2);
+        }
+
+        setLoading(false);
+    };
+
+    const confirm2fa = async (values: z.infer<typeof confirmSchema>) => {
+        setLoading(true);
+
+        const res = await api
+            .post<AxiosResponse<VerifyTotpResponse>>(`/auth/2fa/enable`, {
+                code: values.code
+            } as VerifyTotpBody)
+            .catch((e) => {
+                toast({
+                    title: "Unable to enable 2FA",
+                    description: formatAxiosError(
+                        e,
+                        "An error occurred while enabling 2FA"
+                    ),
+                    variant: "destructive"
+                });
+            });
+
+        if (res && res.data.data.valid) {
+            setBackupCodes(res.data.data.backupCodes || []);
+            updateUser({ twoFactorEnabled: true })
+            setStep(3);
+        }
+
+        setLoading(false);
+    };
+
+    const handleVerify = () => {
+        if (verificationCode.length !== 6) {
+            setError("Please enter a 6-digit code");
+            return;
+        }
+        if (verificationCode === "123456") {
+            setSuccess(true);
+            setStep(3);
+        } else {
+            setError("Invalid code. Please try again.");
+        }
+    };
+
+    return (
+        <Credenza
+            open={open}
+            onOpenChange={(val) => {
+                setOpen(val);
+                setLoading(false);
+            }}
+        >
+            <CredenzaContent>
+                <CredenzaHeader>
+                    <CredenzaTitle>
+                        Enable Two-factor Authentication
+                    </CredenzaTitle>
+                    <CredenzaDescription>
+                        Secure your account with an extra layer of protection
+                    </CredenzaDescription>
+                </CredenzaHeader>
+                <CredenzaBody>
+                    {step === 1 && (
+                        <Form {...enableForm}>
+                            <form
+                                onSubmit={enableForm.handleSubmit(request2fa)}
+                                className="space-y-4"
+                                id="form"
+                            >
+                                <div className="space-y-4">
+                                    <FormField
+                                        control={enableForm.control}
+                                        name="password"
+                                        render={({ field }) => (
+                                            <FormItem>
+                                                <FormLabel>Password</FormLabel>
+                                                <FormControl>
+                                                    <Input
+                                                        type="password"
+                                                        placeholder="Enter your password"
+                                                        {...field}
+                                                    />
+                                                </FormControl>
+                                                <FormMessage />
+                                            </FormItem>
+                                        )}
+                                    />
+                                </div>
+                            </form>
+                        </Form>
+                    )}
+
+                    {step === 2 && (
+                        <div className="space-y-4">
+                            <p>
+                                Scan this QR code with your authenticator app or
+                                enter the secret key manually:
+                            </p>
+                            <div className="w-64 h-64 mx-auto flex items-center justify-center">
+                                <QRCodeSVG value={secretKey} size={256} />
+                            </div>
+                            <div className="max-w-md mx-auto">
+                                <CopyTextBox
+                                    text={secretKey}
+                                    wrapText={false}
+                                />
+                            </div>
+
+                            <Form {...confirmForm}>
+                                <form
+                                    onSubmit={confirmForm.handleSubmit(
+                                        confirm2fa
+                                    )}
+                                    className="space-y-4"
+                                    id="form"
+                                >
+                                    <div className="space-y-4">
+                                        <FormField
+                                            control={confirmForm.control}
+                                            name="code"
+                                            render={({ field }) => (
+                                                <FormItem>
+                                                    <FormLabel>
+                                                        Verification Code
+                                                    </FormLabel>
+                                                    <FormControl>
+                                                        <Input
+                                                            type="code"
+                                                            placeholder="Enter the 6-digit code from your authenticator app"
+                                                            {...field}
+                                                        />
+                                                    </FormControl>
+                                                    <FormMessage />
+                                                </FormItem>
+                                            )}
+                                        />
+                                    </div>
+                                </form>
+                            </Form>
+                        </div>
+                    )}
+
+                    {step === 3 && (
+                        <div className="space-y-4 text-center">
+                            <CheckCircle2
+                                className="mx-auto text-green-500"
+                                size={48}
+                            />
+                            <p className="font-semibold text-lg">
+                                Two-Factor Authentication Enabled
+                            </p>
+                            <p>
+                                Your account is now more secure. Don't forget to
+                                save your backup codes.
+                            </p>
+
+                            <div className="max-w-md mx-auto">
+                                <CopyTextBox text={backupCodes.join("\n")} />
+                            </div>
+                        </div>
+                    )}
+                </CredenzaBody>
+                <CredenzaFooter>
+                    {(step === 1 || step === 2) && (
+                        <Button
+                            type="submit"
+                            form="form"
+                            loading={loading}
+                            disabled={loading}
+                        >
+                            Submit
+                        </Button>
+                    )}
+                    <CredenzaClose asChild>
+                        <Button variant="outline">Close</Button>
+                    </CredenzaClose>
+                </CredenzaFooter>
+            </CredenzaContent>
+        </Credenza>
+    );
+}

+ 101 - 95
src/app/[orgId]/settings/components/Header.tsx → src/components/Header.tsx

@@ -15,7 +15,6 @@ import {
 import {
 import {
     DropdownMenu,
     DropdownMenu,
     DropdownMenuContent,
     DropdownMenuContent,
-    DropdownMenuGroup,
     DropdownMenuItem,
     DropdownMenuItem,
     DropdownMenuLabel,
     DropdownMenuLabel,
     DropdownMenuSeparator,
     DropdownMenuSeparator,
@@ -26,14 +25,6 @@ import {
     PopoverContent,
     PopoverContent,
     PopoverTrigger
     PopoverTrigger
 } from "@app/components/ui/popover";
 } from "@app/components/ui/popover";
-import {
-    Select,
-    SelectContent,
-    SelectGroup,
-    SelectItem,
-    SelectTrigger,
-    SelectValue
-} from "@app/components/ui/select";
 import { useEnvContext } from "@app/hooks/useEnvContext";
 import { useEnvContext } from "@app/hooks/useEnvContext";
 import { useToast } from "@app/hooks/useToast";
 import { useToast } from "@app/hooks/useToast";
 import { cn, formatAxiosError } from "@app/lib/utils";
 import { cn, formatAxiosError } from "@app/lib/utils";
@@ -45,40 +36,39 @@ import {
     LogOut,
     LogOut,
     Moon,
     Moon,
     Plus,
     Plus,
-    Sun,
-    User
+    Sun
 } from "lucide-react";
 } from "lucide-react";
 import { useTheme } from "next-themes";
 import { useTheme } from "next-themes";
 import Link from "next/link";
 import Link from "next/link";
 import { useRouter } from "next/navigation";
 import { useRouter } from "next/navigation";
 import { useState } from "react";
 import { useState } from "react";
+import Enable2FaForm from "./Enable2FaForm";
+import { userUserContext } from "@app/hooks/useUserContext";
 
 
 type HeaderProps = {
 type HeaderProps = {
-    name?: string;
-    email: string;
-    orgId: string;
-    orgs: ListOrgsResponse["orgs"];
+    orgId?: string;
+    orgs?: ListOrgsResponse["orgs"];
 };
 };
 
 
-export default function Header({ email, orgId, name, orgs }: HeaderProps) {
+export function Header({ orgId, orgs }: HeaderProps) {
     const { toast } = useToast();
     const { toast } = useToast();
     const { setTheme, theme } = useTheme();
     const { setTheme, theme } = useTheme();
 
 
+    const { user, updateUser } = userUserContext();
+
     const [open, setOpen] = useState(false);
     const [open, setOpen] = useState(false);
     const [userTheme, setUserTheme] = useState<"light" | "dark" | "system">(
     const [userTheme, setUserTheme] = useState<"light" | "dark" | "system">(
         theme as "light" | "dark" | "system"
         theme as "light" | "dark" | "system"
     );
     );
 
 
+    const [openEnable2fa, setOpenEnable2fa] = useState(false);
+
     const router = useRouter();
     const router = useRouter();
 
 
     const api = createApiClient(useEnvContext());
     const api = createApiClient(useEnvContext());
 
 
     function getInitials() {
     function getInitials() {
-        if (name) {
-            const [firstName, lastName] = name.split(" ");
-            return `${firstName[0]}${lastName[0]}`;
-        }
-        return email.substring(0, 2).toUpperCase();
+        return user.email.substring(0, 2).toUpperCase();
     }
     }
 
 
     function logout() {
     function logout() {
@@ -102,6 +92,8 @@ export default function Header({ email, orgId, name, orgs }: HeaderProps) {
 
 
     return (
     return (
         <>
         <>
+            <Enable2FaForm open={openEnable2fa} setOpen={setOpenEnable2fa} />
+
             <div className="flex items-center justify-between">
             <div className="flex items-center justify-between">
                 <div className="flex items-center gap-4">
                 <div className="flex items-center gap-4">
                     <DropdownMenu>
                     <DropdownMenu>
@@ -128,15 +120,23 @@ export default function Header({ email, orgId, name, orgs }: HeaderProps) {
                                         Signed in as
                                         Signed in as
                                     </p>
                                     </p>
                                     <p className="text-xs leading-none text-muted-foreground">
                                     <p className="text-xs leading-none text-muted-foreground">
-                                        {email}
+                                        {user.email}
                                     </p>
                                     </p>
                                 </div>
                                 </div>
                             </DropdownMenuLabel>
                             </DropdownMenuLabel>
                             <DropdownMenuSeparator />
                             <DropdownMenuSeparator />
-                            <DropdownMenuItem>
-                                <User className="mr-2 h-4 w-4" />
-                                <span>User Settings</span>
-                            </DropdownMenuItem>
+                            {!user.twoFactorEnabled && (
+                                <DropdownMenuItem
+                                    onClick={() => setOpenEnable2fa(true)}
+                                >
+                                    <span>Enable Two-factor</span>
+                                </DropdownMenuItem>
+                            )}
+                            {user.twoFactorEnabled && (
+                                <DropdownMenuItem>
+                                    <span>Disable Two-factor</span>
+                                </DropdownMenuItem>
+                            )}
                             <DropdownMenuSeparator />
                             <DropdownMenuSeparator />
                             <DropdownMenuLabel>Theme</DropdownMenuLabel>
                             <DropdownMenuLabel>Theme</DropdownMenuLabel>
                             {(["light", "dark", "system"] as const).map(
                             {(["light", "dark", "system"] as const).map(
@@ -175,7 +175,7 @@ export default function Header({ email, orgId, name, orgs }: HeaderProps) {
                         </DropdownMenuContent>
                         </DropdownMenuContent>
                     </DropdownMenu>
                     </DropdownMenu>
                     <span className="truncate max-w-[150px] md:max-w-none font-medium">
                     <span className="truncate max-w-[150px] md:max-w-none font-medium">
-                        {name || email}
+                        {user.email}
                     </span>
                     </span>
                 </div>
                 </div>
 
 
@@ -197,82 +197,88 @@ export default function Header({ email, orgId, name, orgs }: HeaderProps) {
                         </div>
                         </div>
                     </div>
                     </div>
 
 
-                    <Popover open={open} onOpenChange={setOpen}>
-                        <PopoverTrigger asChild>
-                            <Button
-                                variant="outline"
-                                size="lg"
-                                role="combobox"
-                                aria-expanded={open}
-                                className="w-full md:w-[200px] h-12 px-3 py-4 bg-neutral hover:bg-neutral"
-                            >
-                                <div className="flex items-center justify-between w-full">
-                                    <div className="flex flex-col items-start">
-                                        <span className="font-bold text-sm">
-                                            Organization
-                                        </span>
-                                        <span className="text-sm text-muted-foreground">
-                                            {orgId
-                                                ? orgs.find(
-                                                      (org) =>
-                                                          org.orgId === orgId
-                                                  )?.name
-                                                : "Select organization..."}
-                                        </span>
+                    {orgs && (
+                        <Popover open={open} onOpenChange={setOpen}>
+                            <PopoverTrigger asChild>
+                                <Button
+                                    variant="outline"
+                                    size="lg"
+                                    role="combobox"
+                                    aria-expanded={open}
+                                    className="w-full md:w-[200px] h-12 px-3 py-4 bg-neutral hover:bg-neutral"
+                                >
+                                    <div className="flex items-center justify-between w-full">
+                                        <div className="flex flex-col items-start">
+                                            <span className="font-bold text-sm">
+                                                Organization
+                                            </span>
+                                            <span className="text-sm text-muted-foreground">
+                                                {orgId
+                                                    ? orgs?.find(
+                                                          (org) =>
+                                                              org.orgId ===
+                                                              orgId
+                                                      )?.name
+                                                    : "None selected"}
+                                            </span>
+                                        </div>
+                                        <ChevronsUpDown className="h-4 w-4 shrink-0 opacity-50" />
                                     </div>
                                     </div>
-                                    <ChevronsUpDown className="h-4 w-4 shrink-0 opacity-50" />
-                                </div>
-                            </Button>
-                        </PopoverTrigger>
-                        <PopoverContent className="[100px] md:w-[180px] p-0">
-                            <Command>
-                                <CommandInput placeholder="Search..." />
-                                <CommandEmpty>
-                                    No organizations found.
-                                </CommandEmpty>
-                                <CommandGroup heading="Create">
-                                    <CommandList>
-                                        <CommandItem
-                                            className="flex items-center cursor-pointer"
-                                            onSelect={(currentValue) => {
-                                                router.push("/setup");
-                                            }}
-                                        >
-                                            <Plus className="mr-2 h-4 w-4" />
-                                            New Organization
-                                        </CommandItem>
-                                    </CommandList>
-                                </CommandGroup>
-                                <CommandSeparator />
-                                <CommandGroup heading="Organizations">
-                                    <CommandList>
-                                        {orgs.map((org) => (
+                                </Button>
+                            </PopoverTrigger>
+                            <PopoverContent className="[100px] md:w-[180px] p-0">
+                                <Command>
+                                    <CommandInput placeholder="Search..." />
+                                    <CommandEmpty>
+                                        No organizations found.
+                                    </CommandEmpty>
+                                    <CommandGroup heading="Create">
+                                        <CommandList>
                                             <CommandItem
                                             <CommandItem
-                                                key={org.orgId}
                                                 onSelect={(currentValue) => {
                                                 onSelect={(currentValue) => {
-                                                    router.push(
-                                                        `/${org.orgId}/settings`
-                                                    );
+                                                    router.push("/setup");
                                                 }}
                                                 }}
                                             >
                                             >
-                                                <Check
-                                                    className={cn(
-                                                        "mr-2 h-4 w-4",
-                                                        orgId === org.orgId
-                                                            ? "opacity-100"
-                                                            : "opacity-0"
-                                                    )}
-                                                />
-                                                {org.name}
+                                                <Plus className="mr-2 h-4 w-4" />
+                                                New Organization
                                             </CommandItem>
                                             </CommandItem>
-                                        ))}
-                                    </CommandList>
-                                </CommandGroup>
-                            </Command>
-                        </PopoverContent>
-                    </Popover>
+                                        </CommandList>
+                                    </CommandGroup>
+                                    <CommandSeparator />
+                                    <CommandGroup heading="Organizations">
+                                        <CommandList>
+                                            {orgs.map((org) => (
+                                                <CommandItem
+                                                    key={org.orgId}
+                                                    onSelect={(
+                                                        currentValue
+                                                    ) => {
+                                                        router.push(
+                                                            `/${org.orgId}/settings`
+                                                        );
+                                                    }}
+                                                >
+                                                    <Check
+                                                        className={cn(
+                                                            "mr-2 h-4 w-4",
+                                                            orgId === org.orgId
+                                                                ? "opacity-100"
+                                                                : "opacity-0"
+                                                        )}
+                                                    />
+                                                    {org.name}
+                                                </CommandItem>
+                                            ))}
+                                        </CommandList>
+                                    </CommandGroup>
+                                </Command>
+                            </PopoverContent>
+                        </Popover>
+                    )}
                 </div>
                 </div>
             </div>
             </div>
         </>
         </>
     );
     );
 }
 }
+
+export default Header;

+ 7 - 1
src/components/LoginForm.tsx

@@ -214,7 +214,13 @@ export default function LoginForm({ redirect, onLogin }: LoginFormProps) {
                                     <FormLabel>Authenticator Code</FormLabel>
                                     <FormLabel>Authenticator Code</FormLabel>
                                     <FormControl>
                                     <FormControl>
                                         <div className="flex justify-center">
                                         <div className="flex justify-center">
-                                            <InputOTP maxLength={6} {...field} pattern={REGEXP_ONLY_DIGITS_AND_CHARS}>
+                                            <InputOTP
+                                                maxLength={6}
+                                                {...field}
+                                                pattern={
+                                                    REGEXP_ONLY_DIGITS_AND_CHARS
+                                                }
+                                            >
                                                 <InputOTPGroup>
                                                 <InputOTPGroup>
                                                     <InputOTPSlot index={0} />
                                                     <InputOTPSlot index={0} />
                                                     <InputOTPSlot index={1} />
                                                     <InputOTPSlot index={1} />

+ 3 - 3
src/app/[orgId]/settings/components/TopbarNav.tsx → src/components/TopbarNav.tsx

@@ -12,7 +12,7 @@ interface TopbarNavProps extends React.HTMLAttributes<HTMLElement> {
         icon: React.ReactNode;
         icon: React.ReactNode;
     }[];
     }[];
     disabled?: boolean;
     disabled?: boolean;
-    orgId: string;
+    orgId?: string;
 }
 }
 
 
 export function TopbarNav({
 export function TopbarNav({
@@ -36,10 +36,10 @@ export function TopbarNav({
             {items.map((item) => (
             {items.map((item) => (
                 <Link
                 <Link
                     key={item.href}
                     key={item.href}
-                    href={item.href.replace("{orgId}", orgId)}
+                    href={item.href.replace("{orgId}", orgId || "")}
                     className={cn(
                     className={cn(
                         "relative px-3 py-3 text-md",
                         "relative px-3 py-3 text-md",
-                        pathname.startsWith(item.href.replace("{orgId}", orgId))
+                        pathname.startsWith(item.href.replace("{orgId}", orgId || ""))
                             ? "border-b-2 border-primary text-primary font-medium"
                             ? "border-b-2 border-primary text-primary font-medium"
                             : "hover:text-primary text-muted-foreground font-medium",
                             : "hover:text-primary text-muted-foreground font-medium",
                         "whitespace-nowrap",
                         "whitespace-nowrap",

+ 0 - 176
src/components/account-form.tsx

@@ -1,176 +0,0 @@
-"use client"
-
-import { zodResolver } from "@hookform/resolvers/zod"
-import { CalendarIcon, CaretSortIcon, CheckIcon } from "@radix-ui/react-icons"
-import { useForm } from "react-hook-form"
-import { z } from "zod"
-
-import { cn } from "@/lib/utils"
-import { toast } from "@/hooks/useToast"
-import { Button } from "@/components/ui/button"
-import {
-  Command,
-  CommandEmpty,
-  CommandGroup,
-  CommandInput,
-  CommandItem,
-  CommandList,
-} from "@/components/ui/command"
-import {
-  Form,
-  FormControl,
-  FormDescription,
-  FormField,
-  FormItem,
-  FormLabel,
-  FormMessage,
-} from "@/components/ui/form"
-import { Input } from "@/components/ui/input"
-import {
-  Popover,
-  PopoverContent,
-  PopoverTrigger,
-} from "@/components/ui/popover"
-
-const languages = [
-  { label: "English", value: "en" },
-  { label: "French", value: "fr" },
-  { label: "German", value: "de" },
-  { label: "Spanish", value: "es" },
-  { label: "Portuguese", value: "pt" },
-  { label: "Russian", value: "ru" },
-  { label: "Japanese", value: "ja" },
-  { label: "Korean", value: "ko" },
-  { label: "Chinese", value: "zh" },
-] as const
-
-const accountFormSchema = z.object({
-  name: z
-    .string()
-    .min(2, {
-      message: "Name must be at least 2 characters.",
-    })
-    .max(30, {
-      message: "Name must not be longer than 30 characters.",
-    }),
-  dob: z.date({
-    required_error: "A date of birth is required.",
-  }),
-  language: z.string({
-    required_error: "Please select a language.",
-  }),
-})
-
-type AccountFormValues = z.infer<typeof accountFormSchema>
-
-// This can come from your database or API.
-const defaultValues: Partial<AccountFormValues> = {
-  // name: "Your name",
-  // dob: new Date("2023-01-23"),
-}
-
-export function AccountForm() {
-  const form = useForm<AccountFormValues>({
-    resolver: zodResolver(accountFormSchema),
-    defaultValues,
-  })
-
-  function onSubmit(data: AccountFormValues) {
-    toast({
-      title: "You submitted the following values:",
-      description: (
-        <pre className="mt-2 w-[340px] rounded-md bg-slate-950 p-4">
-          <code className="text-white">{JSON.stringify(data, null, 2)}</code>
-        </pre>
-      ),
-    })
-  }
-
-  return (
-    <Form {...form}>
-      <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
-        <FormField
-          control={form.control}
-          name="name"
-          render={({ field }) => (
-            <FormItem>
-              <FormLabel>Name</FormLabel>
-              <FormControl>
-                <Input placeholder="Your name" {...field} />
-              </FormControl>
-              <FormDescription>
-                This is the name that will be displayed on your profile and in
-                emails.
-              </FormDescription>
-              <FormMessage />
-            </FormItem>
-          )}
-        />
-        <FormField
-          control={form.control}
-          name="language"
-          render={({ field }) => (
-            <FormItem className="flex flex-col">
-              <FormLabel>Language</FormLabel>
-              <Popover>
-                <PopoverTrigger asChild>
-                  <FormControl>
-                    <Button
-                      variant="outline"
-                      role="combobox"
-                      className={cn(
-                        "w-[200px] justify-between",
-                        !field.value && "text-muted-foreground"
-                      )}
-                    >
-                      {field.value
-                        ? languages.find(
-                            (language) => language.value === field.value
-                          )?.label
-                        : "Select language"}
-                      <CaretSortIcon className="ml-2 h-4 w-4 shrink-0 opacity-50" />
-                    </Button>
-                  </FormControl>
-                </PopoverTrigger>
-                <PopoverContent className="w-[200px] p-0">
-                  <Command>
-                    <CommandInput placeholder="Search language..." />
-                    <CommandList>
-                      <CommandEmpty>No language found.</CommandEmpty>
-                      <CommandGroup>
-                        {languages.map((language) => (
-                          <CommandItem
-                            value={language.label}
-                            key={language.value}
-                            onSelect={() => {
-                              form.setValue("language", language.value)
-                            }}
-                          >
-                            <CheckIcon
-                              className={cn(
-                                "mr-2 h-4 w-4",
-                                language.value === field.value
-                                  ? "opacity-100"
-                                  : "opacity-0"
-                              )}
-                            />
-                            {language.label}
-                          </CommandItem>
-                        ))}
-                      </CommandGroup>
-                    </CommandList>
-                  </Command>
-                </PopoverContent>
-              </Popover>
-              <FormDescription>
-                This is the language that will be used in the dashboard.
-              </FormDescription>
-              <FormMessage />
-            </FormItem>
-          )}
-        />
-        <Button type="submit">Update account</Button>
-      </form>
-    </Form>
-  )
-}

+ 0 - 179
src/components/appearance-form.tsx

@@ -1,179 +0,0 @@
-"use client";
-
-import { zodResolver } from "@hookform/resolvers/zod";
-import { ChevronDownIcon } from "@radix-ui/react-icons";
-import { useForm } from "react-hook-form";
-import { z } from "zod";
-
-import { cn } from "@/lib/utils";
-import { toast } from "@/hooks/useToast";
-import { Button, buttonVariants } from "@/components/ui/button";
-import {
-    Form,
-    FormControl,
-    FormDescription,
-    FormField,
-    FormItem,
-    FormLabel,
-    FormMessage,
-} from "@/components/ui/form";
-import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
-import { useSiteContext } from "@app/hooks/useSiteContext";
-
-const appearanceFormSchema = z.object({
-    theme: z.enum(["light", "dark"], {
-        required_error: "Please select a theme.",
-    }),
-    font: z.enum(["inter", "manrope", "system"], {
-        invalid_type_error: "Select a font",
-        required_error: "Please select a font.",
-    }),
-});
-
-type AppearanceFormValues = z.infer<typeof appearanceFormSchema>;
-
-// This can come from your database or API.
-const defaultValues: Partial<AppearanceFormValues> = {
-    theme: "light",
-};
-
-export function AppearanceForm() {
-    const site = useSiteContext();
-
-    console.log(site);
-
-    const form = useForm<AppearanceFormValues>({
-        resolver: zodResolver(appearanceFormSchema),
-        defaultValues,
-    });
-
-    function onSubmit(data: AppearanceFormValues) {
-        toast({
-            title: "You submitted the following values:",
-            description: (
-                <pre className="mt-2 w-[340px] rounded-md bg-slate-950 p-4">
-                    <code className="text-white">
-                        {JSON.stringify(data, null, 2)}
-                    </code>
-                </pre>
-            ),
-        });
-    }
-
-    return (
-        <Form {...form}>
-            <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
-                <FormField
-                    control={form.control}
-                    name="font"
-                    render={({ field }) => (
-                        <FormItem>
-                            <FormLabel>Font</FormLabel>
-                            <div className="relative w-max">
-                                <FormControl>
-                                    <select
-                                        className={cn(
-                                            buttonVariants({
-                                                variant: "outline",
-                                            }),
-                                            "w-[200px] appearance-none font-normal"
-                                        )}
-                                        {...field}
-                                    >
-                                        <option value="inter">Inter</option>
-                                        <option value="manrope">Manrope</option>
-                                        <option value="system">System</option>
-                                    </select>
-                                </FormControl>
-                                <ChevronDownIcon className="absolute right-3 top-2.5 h-4 w-4 opacity-50" />
-                            </div>
-                            <FormDescription>
-                                Set the font you want to use in the dashboard.
-                            </FormDescription>
-                            <FormMessage />
-                        </FormItem>
-                    )}
-                />
-                <FormField
-                    control={form.control}
-                    name="theme"
-                    render={({ field }) => (
-                        <FormItem className="space-y-1">
-                            <FormLabel>Theme</FormLabel>
-                            <FormDescription>
-                                Select the theme for the dashboard.
-                            </FormDescription>
-                            <FormMessage />
-                            <RadioGroup
-                                onValueChange={field.onChange}
-                                defaultValue={field.value}
-                                className="grid max-w-md grid-cols-2 gap-8 pt-2"
-                            >
-                                <FormItem>
-                                    <FormLabel className="[&:has([data-state=checked])>div]:border-primary">
-                                        <FormControl>
-                                            <RadioGroupItem
-                                                value="light"
-                                                className="sr-only"
-                                            />
-                                        </FormControl>
-                                        <div className="items-center rounded-md border-2 border-muted p-1 hover:border-accent">
-                                            <div className="space-y-2 rounded-sm bg-[#ecedef] p-2">
-                                                <div className="space-y-2 rounded-md bg-white p-2 shadow-sm">
-                                                    <div className="h-2 w-[80px] rounded-lg bg-[#ecedef]" />
-                                                    <div className="h-2 w-[100px] rounded-lg bg-[#ecedef]" />
-                                                </div>
-                                                <div className="flex items-center space-x-2 rounded-md bg-white p-2 shadow-sm">
-                                                    <div className="h-4 w-4 rounded-full bg-[#ecedef]" />
-                                                    <div className="h-2 w-[100px] rounded-lg bg-[#ecedef]" />
-                                                </div>
-                                                <div className="flex items-center space-x-2 rounded-md bg-white p-2 shadow-sm">
-                                                    <div className="h-4 w-4 rounded-full bg-[#ecedef]" />
-                                                    <div className="h-2 w-[100px] rounded-lg bg-[#ecedef]" />
-                                                </div>
-                                            </div>
-                                        </div>
-                                        <span className="block w-full p-2 text-center font-normal">
-                                            Light
-                                        </span>
-                                    </FormLabel>
-                                </FormItem>
-                                <FormItem>
-                                    <FormLabel className="[&:has([data-state=checked])>div]:border-primary">
-                                        <FormControl>
-                                            <RadioGroupItem
-                                                value="dark"
-                                                className="sr-only"
-                                            />
-                                        </FormControl>
-                                        <div className="items-center rounded-md border-2 border-muted bg-popover p-1 hover:bg-accent hover:text-accent-foreground">
-                                            <div className="space-y-2 rounded-sm bg-slate-950 p-2">
-                                                <div className="space-y-2 rounded-md bg-slate-800 p-2 shadow-sm">
-                                                    <div className="h-2 w-[80px] rounded-lg bg-slate-400" />
-                                                    <div className="h-2 w-[100px] rounded-lg bg-slate-400" />
-                                                </div>
-                                                <div className="flex items-center space-x-2 rounded-md bg-slate-800 p-2 shadow-sm">
-                                                    <div className="h-4 w-4 rounded-full bg-slate-400" />
-                                                    <div className="h-2 w-[100px] rounded-lg bg-slate-400" />
-                                                </div>
-                                                <div className="flex items-center space-x-2 rounded-md bg-slate-800 p-2 shadow-sm">
-                                                    <div className="h-4 w-4 rounded-full bg-slate-400" />
-                                                    <div className="h-2 w-[100px] rounded-lg bg-slate-400" />
-                                                </div>
-                                            </div>
-                                        </div>
-                                        <span className="block w-full p-2 text-center font-normal">
-                                            Dark
-                                        </span>
-                                    </FormLabel>
-                                </FormItem>
-                            </RadioGroup>
-                        </FormItem>
-                    )}
-                />
-
-                <Button type="submit">Update preferences</Button>
-            </form>
-        </Form>
-    );
-}

+ 0 - 132
src/components/display-form.tsx

@@ -1,132 +0,0 @@
-"use client"
-
-import { zodResolver } from "@hookform/resolvers/zod"
-import { useForm } from "react-hook-form"
-import { z } from "zod"
-
-import { toast } from "@/hooks/useToast"
-import { Button } from "@/components/ui/button"
-import { Checkbox } from "@/components/ui/checkbox"
-import {
-  Form,
-  FormControl,
-  FormDescription,
-  FormField,
-  FormItem,
-  FormLabel,
-  FormMessage,
-} from "@/components/ui/form"
-
-const items = [
-  {
-    id: "recents",
-    label: "Recents",
-  },
-  {
-    id: "home",
-    label: "Home",
-  },
-  {
-    id: "applications",
-    label: "Applications",
-  },
-  {
-    id: "desktop",
-    label: "Desktop",
-  },
-  {
-    id: "downloads",
-    label: "Downloads",
-  },
-  {
-    id: "documents",
-    label: "Documents",
-  },
-] as const
-
-const displayFormSchema = z.object({
-  items: z.array(z.string()).refine((value) => value.some((item) => item), {
-    message: "You have to select at least one item.",
-  }),
-})
-
-type DisplayFormValues = z.infer<typeof displayFormSchema>
-
-// This can come from your database or API.
-const defaultValues: Partial<DisplayFormValues> = {
-  items: ["recents", "home"],
-}
-
-export function DisplayForm() {
-  const form = useForm<DisplayFormValues>({
-    resolver: zodResolver(displayFormSchema),
-    defaultValues,
-  })
-
-  function onSubmit(data: DisplayFormValues) {
-    toast({
-      title: "You submitted the following values:",
-      description: (
-        <pre className="mt-2 w-[340px] rounded-md bg-slate-950 p-4">
-          <code className="text-white">{JSON.stringify(data, null, 2)}</code>
-        </pre>
-      ),
-    })
-  }
-
-  return (
-    <Form {...form}>
-      <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
-        <FormField
-          control={form.control}
-          name="items"
-          render={() => (
-            <FormItem>
-              <div className="mb-4">
-                <FormLabel className="text-base">Sidebar</FormLabel>
-                <FormDescription>
-                  Select the items you want to display in the sidebar.
-                </FormDescription>
-              </div>
-              {items.map((item) => (
-                <FormField
-                  key={item.id}
-                  control={form.control}
-                  name="items"
-                  render={({ field }) => {
-                    return (
-                      <FormItem
-                        key={item.id}
-                        className="flex flex-row items-start space-x-3 space-y-0"
-                      >
-                        <FormControl>
-                          <Checkbox
-                            checked={field.value?.includes(item.id)}
-                            onCheckedChange={(checked) => {
-                              return checked
-                                ? field.onChange([...field.value, item.id])
-                                : field.onChange(
-                                    field.value?.filter(
-                                      (value) => value !== item.id
-                                    )
-                                  )
-                            }}
-                          />
-                        </FormControl>
-                        <FormLabel className="font-normal">
-                          {item.label}
-                        </FormLabel>
-                      </FormItem>
-                    )
-                  }}
-                />
-              ))}
-              <FormMessage />
-            </FormItem>
-          )}
-        />
-        <Button type="submit">Update display</Button>
-      </form>
-    </Form>
-  )
-}

+ 0 - 222
src/components/notifications-form.tsx

@@ -1,222 +0,0 @@
-"use client"
-
-import Link from "next/link"
-import { zodResolver } from "@hookform/resolvers/zod"
-import { useForm } from "react-hook-form"
-import { z } from "zod"
-
-import { toast } from "@/hooks/useToast"
-import { Button } from "@/components/ui/button"
-import { Checkbox } from "@/components/ui/checkbox"
-import {
-  Form,
-  FormControl,
-  FormDescription,
-  FormField,
-  FormItem,
-  FormLabel,
-  FormMessage,
-} from "@/components/ui/form"
-import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"
-import { Switch } from "@/components/ui/switch"
-
-const notificationsFormSchema = z.object({
-  type: z.enum(["all", "mentions", "none"], {
-    required_error: "You need to select a notification type.",
-  }),
-  mobile: z.boolean().default(false).optional(),
-  communication_emails: z.boolean().default(false).optional(),
-  social_emails: z.boolean().default(false).optional(),
-  marketing_emails: z.boolean().default(false).optional(),
-  security_emails: z.boolean(),
-})
-
-type NotificationsFormValues = z.infer<typeof notificationsFormSchema>
-
-// This can come from your database or API.
-const defaultValues: Partial<NotificationsFormValues> = {
-  communication_emails: false,
-  marketing_emails: false,
-  social_emails: true,
-  security_emails: true,
-}
-
-export function NotificationsForm() {
-  const form = useForm<NotificationsFormValues>({
-    resolver: zodResolver(notificationsFormSchema),
-    defaultValues,
-  })
-
-  function onSubmit(data: NotificationsFormValues) {
-    toast({
-      title: "You submitted the following values:",
-      description: (
-        <pre className="mt-2 w-[340px] rounded-md bg-slate-950 p-4">
-          <code className="text-white">{JSON.stringify(data, null, 2)}</code>
-        </pre>
-      ),
-    })
-  }
-
-  return (
-    <Form {...form}>
-      <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
-        <FormField
-          control={form.control}
-          name="type"
-          render={({ field }) => (
-            <FormItem className="space-y-3">
-              <FormLabel>Notify me about...</FormLabel>
-              <FormControl>
-                <RadioGroup
-                  onValueChange={field.onChange}
-                  defaultValue={field.value}
-                  className="flex flex-col space-y-1"
-                >
-                  <FormItem className="flex items-center space-x-3 space-y-0">
-                    <FormControl>
-                      <RadioGroupItem value="all" />
-                    </FormControl>
-                    <FormLabel className="font-normal">
-                      All new messages
-                    </FormLabel>
-                  </FormItem>
-                  <FormItem className="flex items-center space-x-3 space-y-0">
-                    <FormControl>
-                      <RadioGroupItem value="mentions" />
-                    </FormControl>
-                    <FormLabel className="font-normal">
-                      Direct messages and mentions
-                    </FormLabel>
-                  </FormItem>
-                  <FormItem className="flex items-center space-x-3 space-y-0">
-                    <FormControl>
-                      <RadioGroupItem value="none" />
-                    </FormControl>
-                    <FormLabel className="font-normal">Nothing</FormLabel>
-                  </FormItem>
-                </RadioGroup>
-              </FormControl>
-              <FormMessage />
-            </FormItem>
-          )}
-        />
-        <div>
-          <h3 className="mb-4 text-lg font-medium">Email Notifications</h3>
-          <div className="space-y-4">
-            <FormField
-              control={form.control}
-              name="communication_emails"
-              render={({ field }) => (
-                <FormItem className="flex flex-row items-center justify-between rounded-lg border p-4">
-                  <div className="space-y-0.5">
-                    <FormLabel className="text-base">
-                      Communication emails
-                    </FormLabel>
-                    <FormDescription>
-                      Receive emails about your account activity.
-                    </FormDescription>
-                  </div>
-                  <FormControl>
-                    <Switch
-                      checked={field.value}
-                      onCheckedChange={field.onChange}
-                    />
-                  </FormControl>
-                </FormItem>
-              )}
-            />
-            <FormField
-              control={form.control}
-              name="marketing_emails"
-              render={({ field }) => (
-                <FormItem className="flex flex-row items-center justify-between rounded-lg border p-4">
-                  <div className="space-y-0.5">
-                    <FormLabel className="text-base">
-                      Marketing emails
-                    </FormLabel>
-                    <FormDescription>
-                      Receive emails about new products, features, and more.
-                    </FormDescription>
-                  </div>
-                  <FormControl>
-                    <Switch
-                      checked={field.value}
-                      onCheckedChange={field.onChange}
-                    />
-                  </FormControl>
-                </FormItem>
-              )}
-            />
-            <FormField
-              control={form.control}
-              name="social_emails"
-              render={({ field }) => (
-                <FormItem className="flex flex-row items-center justify-between rounded-lg border p-4">
-                  <div className="space-y-0.5">
-                    <FormLabel className="text-base">Social emails</FormLabel>
-                    <FormDescription>
-                      Receive emails for friend requests, follows, and more.
-                    </FormDescription>
-                  </div>
-                  <FormControl>
-                    <Switch
-                      checked={field.value}
-                      onCheckedChange={field.onChange}
-                    />
-                  </FormControl>
-                </FormItem>
-              )}
-            />
-            <FormField
-              control={form.control}
-              name="security_emails"
-              render={({ field }) => (
-                <FormItem className="flex flex-row items-center justify-between rounded-lg border p-4">
-                  <div className="space-y-0.5">
-                    <FormLabel className="text-base">Security emails</FormLabel>
-                    <FormDescription>
-                      Receive emails about your account activity and security.
-                    </FormDescription>
-                  </div>
-                  <FormControl>
-                    <Switch
-                      checked={field.value}
-                      onCheckedChange={field.onChange}
-                      disabled
-                      aria-readonly
-                    />
-                  </FormControl>
-                </FormItem>
-              )}
-            />
-          </div>
-        </div>
-        <FormField
-          control={form.control}
-          name="mobile"
-          render={({ field }) => (
-            <FormItem className="flex flex-row items-start space-x-3 space-y-0">
-              <FormControl>
-                <Checkbox
-                  checked={field.value}
-                  onCheckedChange={field.onChange}
-                />
-              </FormControl>
-              <div className="space-y-1 leading-none">
-                <FormLabel>
-                  Use different settings for my mobile devices
-                </FormLabel>
-                <FormDescription>
-                  You can manage your mobile notifications in the{" "}
-                  <Link href="/examples/forms">mobile settings</Link> page.
-                </FormDescription>
-              </div>
-            </FormItem>
-          )}
-        />
-        <Button type="submit">Update notifications</Button>
-      </form>
-    </Form>
-  )
-}

+ 0 - 192
src/components/profile-form.tsx

@@ -1,192 +0,0 @@
-"use client"
-
-import Link from "next/link"
-import { zodResolver } from "@hookform/resolvers/zod"
-import { useFieldArray, useForm } from "react-hook-form"
-import { z } from "zod"
-
-import { cn } from "@/lib/utils"
-import { toast } from "@/hooks/useToast"
-
-import { Button } from "@/components/ui/button"
-import {
-  Form,
-  FormControl,
-  FormDescription,
-  FormField,
-  FormItem,
-  FormLabel,
-  FormMessage,
-} from "@/components/ui/form"
-import { Input } from "@/components/ui/input"
-import {
-  Select,
-  SelectContent,
-  SelectItem,
-  SelectTrigger,
-  SelectValue,
-} from "@/components/ui/select"
-import { Textarea } from "@/components/ui/textarea"
-
-const profileFormSchema = z.object({
-  username: z
-    .string()
-    .min(2, {
-      message: "Username must be at least 2 characters.",
-    })
-    .max(30, {
-      message: "Username must not be longer than 30 characters.",
-    }),
-  email: z
-    .string({
-      required_error: "Please select an email to display.",
-    })
-    .email(),
-  bio: z.string().max(160).min(4),
-  urls: z
-    .array(
-      z.object({
-        value: z.string().url({ message: "Please enter a valid URL." }),
-      })
-    )
-    .optional(),
-})
-
-type ProfileFormValues = z.infer<typeof profileFormSchema>
-
-// This can come from your database or API.
-const defaultValues: Partial<ProfileFormValues> = {
-  bio: "I own a computer.",
-  urls: [
-    { value: "https://shadcn.com" },
-    { value: "http://twitter.com/shadcn" },
-  ],
-}
-
-export function ProfileForm() {
-  const form = useForm<ProfileFormValues>({
-    resolver: zodResolver(profileFormSchema),
-    defaultValues,
-    mode: "onChange",
-  })
-
-  const { fields, append } = useFieldArray({
-    name: "urls",
-    control: form.control,
-  })
-
-  function onSubmit(data: ProfileFormValues) {
-    toast({
-      title: "You submitted the following values:",
-      description: (
-        <pre className="mt-2 w-[340px] rounded-md bg-slate-950 p-4">
-          <code className="text-white">{JSON.stringify(data, null, 2)}</code>
-        </pre>
-      ),
-    })
-  }
-
-  return (
-    <Form {...form}>
-      <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
-        <FormField
-          control={form.control}
-          name="username"
-          render={({ field }) => (
-            <FormItem>
-              <FormLabel>Username</FormLabel>
-              <FormControl>
-                <Input placeholder="shadcn" {...field} />
-              </FormControl>
-              <FormDescription>
-                This is your public display name. It can be your real name or a
-                pseudonym. You can only change this once every 30 days.
-              </FormDescription>
-              <FormMessage />
-            </FormItem>
-          )}
-        />
-        <FormField
-          control={form.control}
-          name="email"
-          render={({ field }) => (
-            <FormItem>
-              <FormLabel>Email</FormLabel>
-              <Select onValueChange={field.onChange} defaultValue={field.value}>
-                <FormControl>
-                  <SelectTrigger>
-                    <SelectValue placeholder="Select a verified email to display" />
-                  </SelectTrigger>
-                </FormControl>
-                <SelectContent>
-                  <SelectItem value="m@example.com">m@example.com</SelectItem>
-                  <SelectItem value="m@google.com">m@google.com</SelectItem>
-                  <SelectItem value="m@support.com">m@support.com</SelectItem>
-                </SelectContent>
-              </Select>
-              <FormDescription>
-                You can manage verified email addresses in your{" "}
-                <Link href="/examples/forms">email settings</Link>.
-              </FormDescription>
-              <FormMessage />
-            </FormItem>
-          )}
-        />
-        <FormField
-          control={form.control}
-          name="bio"
-          render={({ field }) => (
-            <FormItem>
-              <FormLabel>Bio</FormLabel>
-              <FormControl>
-                <Textarea
-                  placeholder="Tell us a little bit about yourself"
-                  className="resize-none"
-                  {...field}
-                />
-              </FormControl>
-              <FormDescription>
-                You can <span>@mention</span> other users and organizations to
-                link to them.
-              </FormDescription>
-              <FormMessage />
-            </FormItem>
-          )}
-        />
-        <div>
-          {fields.map((field, index) => (
-            <FormField
-              control={form.control}
-              key={field.id}
-              name={`urls.${index}.value`}
-              render={({ field }) => (
-                <FormItem>
-                  <FormLabel className={cn(index !== 0 && "sr-only")}>
-                    URLs
-                  </FormLabel>
-                  <FormDescription className={cn(index !== 0 && "sr-only")}>
-                    Add links to your website, blog, or social media profiles.
-                  </FormDescription>
-                  <FormControl>
-                    <Input {...field} />
-                  </FormControl>
-                  <FormMessage />
-                </FormItem>
-              )}
-            />
-          ))}
-          <Button
-            type="button"
-            variant="outline"
-            size="sm"
-            className="mt-2"
-            onClick={() => append({ value: "" })}
-          >
-            Add URL
-          </Button>
-        </div>
-        <Button type="submit">Update profile</Button>
-      </form>
-    </Form>
-  )
-}

+ 8 - 1
src/contexts/userContext.ts

@@ -1,4 +1,11 @@
 import { GetUserResponse } from "@server/routers/user";
 import { GetUserResponse } from "@server/routers/user";
 import { createContext } from "react";
 import { createContext } from "react";
 
 
-export const UserContext = createContext<GetUserResponse | null>(null);
+interface UserContextType {
+    user: GetUserResponse;
+    updateUser: (updatedUser: Partial<GetUserResponse>) => void;
+}
+
+const UserContext = createContext<UserContextType | undefined>(undefined);
+
+export default UserContext;

+ 7 - 4
src/hooks/useUserContext.ts

@@ -1,7 +1,10 @@
-import { UserContext } from "@app/contexts/userContext";
+import UserContext from "@app/contexts/userContext";
 import { useContext } from "react";
 import { useContext } from "react";
 
 
-export function useUserContext() {
-    const user = useContext(UserContext);
-    return user;
+export function userUserContext() {
+    const context = useContext(UserContext);
+    if (context === undefined) {
+        throw new Error("useUserContext must be used within a UserProvider");
+    }
+    return context;
 }
 }

+ 28 - 7
src/providers/UserProvider.tsx

@@ -1,16 +1,37 @@
 "use client";
 "use client";
 
 
-import { UserContext } from "@app/contexts/userContext";
+import UserContext from "@app/contexts/userContext";
 import { GetUserResponse } from "@server/routers/user";
 import { GetUserResponse } from "@server/routers/user";
-import { ReactNode } from "react";
+import { useState } from "react";
 
 
-type UserProviderProps = {
+interface UserProviderProps {
+    children: React.ReactNode;
     user: GetUserResponse;
     user: GetUserResponse;
-    children: ReactNode;
-};
+}
+
+export function UserProvider({ children, user: u }: UserProviderProps) {
+    const [user, setUser] = useState<GetUserResponse>(u);
+
+    const updateUser = (updatedUser: Partial<GetUserResponse>) => {
+        if (!user) {
+            throw new Error("No user to update");
+        }
+        setUser((prev) => {
+            if (!prev) {
+                return prev;
+            }
+            return {
+                ...prev,
+                ...updatedUser
+            };
+        });
+    };
 
 
-export function UserProvider({ user, children }: UserProviderProps) {
-    return <UserContext.Provider value={user}>{children}</UserContext.Provider>;
+    return (
+        <UserContext.Provider value={{ user: user, updateUser: updateUser }}>
+            {children}
+        </UserContext.Provider>
+    );
 }
 }
 
 
 export default UserProvider;
 export default UserProvider;