Przeglądaj źródła

set resource password and remove resource password from dashboard

Milo Schwartz 8 miesięcy temu
rodzic
commit
cfce3dabb3

+ 1 - 1
server/db/schema.ts

@@ -44,7 +44,7 @@ export const resources = sqliteTable("resources", {
     blockAccess: integer("blockAccess", { mode: "boolean" })
         .notNull()
         .default(false),
-    sso: integer("sso", { mode: "boolean" }).notNull().default(false),
+    sso: integer("sso", { mode: "boolean" }).notNull().default(true),
     twoFactorEnabled: integer("twoFactorEnabled", { mode: "boolean" })
         .notNull()
         .default(false),

+ 1 - 1
server/routers/resource/setResourcePassword.ts

@@ -15,7 +15,7 @@ const setResourceAuthMethodsParamsSchema = z.object({
 
 const setResourceAuthMethodsBodySchema = z
     .object({
-        password: z.string().min(4).max(255).nullable(),
+        password: z.string().nullish(),
     })
     .strict();
 

+ 172 - 0
src/app/[orgId]/settings/resources/[resourceId]/authentication/components/SetResourcePasswordForm.tsx

@@ -0,0 +1,172 @@
+"use client";
+
+import api from "@app/api";
+import { Button } from "@app/components/ui/button";
+import {
+    Form,
+    FormControl,
+    FormDescription,
+    FormField,
+    FormItem,
+    FormLabel,
+    FormMessage,
+} from "@app/components/ui/form";
+import { Input } from "@app/components/ui/input";
+import { useToast } from "@app/hooks/useToast";
+import { zodResolver } from "@hookform/resolvers/zod";
+import { useEffect, useState } from "react";
+import { useForm } from "react-hook-form";
+import { z } from "zod";
+import {
+    Credenza,
+    CredenzaBody,
+    CredenzaClose,
+    CredenzaContent,
+    CredenzaDescription,
+    CredenzaFooter,
+    CredenzaHeader,
+    CredenzaTitle,
+} from "@app/components/Credenza";
+import { formatAxiosError } from "@app/lib/utils";
+import { AxiosResponse } from "axios";
+import { Resource } from "@server/db/schema";
+
+const setPasswordFormSchema = z.object({
+    password: z.string().min(4).max(100),
+});
+
+type SetPasswordFormValues = z.infer<typeof setPasswordFormSchema>;
+
+const defaultValues: Partial<SetPasswordFormValues> = {
+    password: "",
+};
+
+type SetPasswordFormProps = {
+    open: boolean;
+    setOpen: (open: boolean) => void;
+    resourceId: number;
+    onSetPassword?: () => void;
+};
+
+export default function SetResourcePasswordForm({
+    open,
+    setOpen,
+    resourceId,
+    onSetPassword,
+}: SetPasswordFormProps) {
+    const { toast } = useToast();
+
+    const [loading, setLoading] = useState(false);
+
+    const form = useForm<SetPasswordFormValues>({
+        resolver: zodResolver(setPasswordFormSchema),
+        defaultValues,
+    });
+
+    useEffect(() => {
+        if (!open) {
+            return;
+        }
+
+        form.reset();
+    }, [open]);
+
+    async function onSubmit(data: SetPasswordFormValues) {
+        setLoading(true);
+
+        api.post<AxiosResponse<Resource>>(`/resource/${resourceId}/password`, {
+            password: data.password,
+        })
+            .catch((e) => {
+                toast({
+                    variant: "destructive",
+                    title: "Error setting resource password",
+                    description: formatAxiosError(
+                        e,
+                        "An error occurred while setting the resource password"
+                    ),
+                });
+            })
+            .then(() => {
+                toast({
+                    title: "Resource password set",
+                    description:
+                        "The resource password has been set successfully",
+                });
+
+                if (onSetPassword) {
+                    onSetPassword();
+                }
+            })
+            .finally(() => setLoading(false));
+    }
+
+    return (
+        <>
+            <Credenza
+                open={open}
+                onOpenChange={(val) => {
+                    setOpen(val);
+                    setLoading(false);
+                    form.reset();
+                }}
+            >
+                <CredenzaContent>
+                    <CredenzaHeader>
+                        <CredenzaTitle>Set Password</CredenzaTitle>
+                        <CredenzaDescription>
+                            Set a password to protect this resource
+                        </CredenzaDescription>
+                    </CredenzaHeader>
+                    <CredenzaBody>
+                        <Form {...form}>
+                            <form
+                                onSubmit={form.handleSubmit(onSubmit)}
+                                className="space-y-4"
+                                id="set-password-form"
+                            >
+                                <FormField
+                                    control={form.control}
+                                    name="password"
+                                    render={({ field }) => (
+                                        <FormItem>
+                                            <FormLabel>Password</FormLabel>
+                                            <FormControl>
+                                                <Input
+                                                    autoComplete="off"
+                                                    type="password"
+                                                    placeholder="Your secure password"
+                                                    {...field}
+                                                />
+                                            </FormControl>
+                                            <FormDescription>
+                                                Users will be able to access
+                                                this resource by entering this
+                                                password. It must be at least 4
+                                                characters long.
+                                            </FormDescription>
+                                            <FormMessage />
+                                        </FormItem>
+                                    )}
+                                />
+                            </form>
+                        </Form>
+                    </CredenzaBody>
+                    <CredenzaFooter>
+                        <Button
+                            type="submit"
+                            form="set-password-form"
+                            loading={loading}
+                            disabled={loading}
+                        >
+                            Enable Password Protection
+                        </Button>
+                        <CredenzaClose asChild>
+                            <Button variant="outline">Close</Button>
+                        </CredenzaClose>
+                    </CredenzaFooter>
+                </CredenzaContent>
+            </Credenza>
+        </>
+    );
+}

+ 171 - 136
src/app/[orgId]/settings/resources/[resourceId]/authentication/page.tsx

@@ -9,11 +9,12 @@ import { useResourceContext } from "@app/hooks/useResourceContext";
 import { AxiosResponse } from "axios";
 import { formatAxiosError } from "@app/lib/utils";
 import {
+    GetResourceAuthInfoResponse,
     ListResourceRolesResponse,
     ListResourceUsersResponse,
 } from "@server/routers/resource";
 import { Button } from "@app/components/ui/button";
-import { z } from "zod";
+import { set, z } from "zod";
 import { Tag } from "emblor";
 import { useForm } from "react-hook-form";
 import { zodResolver } from "@hookform/resolvers/zod";
@@ -31,6 +32,9 @@ import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
 import { ListUsersResponse } from "@server/routers/user";
 import { Switch } from "@app/components/ui/switch";
 import { Label } from "@app/components/ui/label";
+import { Input } from "@app/components/ui/input";
+import { ShieldCheck } from "lucide-react";
+import SetResourcePasswordForm from "./components/SetResourcePasswordForm";
 
 const UsersRolesFormSchema = z.object({
     roles: z.array(
@@ -50,7 +54,10 @@ const UsersRolesFormSchema = z.object({
 export default function ResourceAuthenticationPage() {
     const { toast } = useToast();
     const { org } = useOrgContext();
-    const { resource, updateResource } = useResourceContext();
+    const { resource, updateResource, authInfo, updateAuthInfo } =
+        useResourceContext();
+
+    const [pageLoading, setPageLoading] = useState(true);
 
     const [allRoles, setAllRoles] = useState<{ id: string; text: string }[]>(
         []
@@ -69,7 +76,10 @@ export default function ResourceAuthenticationPage() {
     const [blockAccess, setBlockAccess] = useState(resource.blockAccess);
 
     const [loadingSaveUsersRoles, setLoadingSaveUsersRoles] = useState(false);
-    const [loadingSaveAuth, setLoadingSaveAuth] = useState(false);
+    const [loadingRemoveResourcePassword, setLoadingRemoveResourcePassword] =
+        useState(false);
+
+    const [isSetPasswordOpen, setIsSetPasswordOpen] = useState(false);
 
     const usersRolesForm = useForm<z.infer<typeof UsersRolesFormSchema>>({
         resolver: zodResolver(UsersRolesFormSchema),
@@ -77,103 +87,77 @@ export default function ResourceAuthenticationPage() {
     });
 
     useEffect(() => {
-        api.get<AxiosResponse<ListRolesResponse>>(
-            `/org/${org?.org.orgId}/roles`
-        )
-            .then((res) => {
+        const fetchData = async () => {
+            try {
+                const [
+                    rolesResponse,
+                    resourceRolesResponse,
+                    usersResponse,
+                    resourceUsersResponse,
+                ] = await Promise.all([
+                    api.get<AxiosResponse<ListRolesResponse>>(
+                        `/org/${org?.org.orgId}/roles`
+                    ),
+                    api.get<AxiosResponse<ListResourceRolesResponse>>(
+                        `/resource/${resource.resourceId}/roles`
+                    ),
+                    api.get<AxiosResponse<ListUsersResponse>>(
+                        `/org/${org?.org.orgId}/users`
+                    ),
+                    api.get<AxiosResponse<ListResourceUsersResponse>>(
+                        `/resource/${resource.resourceId}/users`
+                    ),
+                ]);
+
                 setAllRoles(
-                    res.data.data.roles
+                    rolesResponse.data.data.roles
                         .map((role) => ({
                             id: role.roleId.toString(),
                             text: role.name,
                         }))
                         .filter((role) => role.text !== "Admin")
                 );
-            })
-            .catch((e) => {
-                console.error(e);
-                toast({
-                    variant: "destructive",
-                    title: "Failed to fetch roles",
-                    description: formatAxiosError(
-                        e,
-                        "An error occurred while fetching the roles"
-                    ),
-                });
-            });
 
-        api.get<AxiosResponse<ListResourceRolesResponse>>(
-            `/resource/${resource.resourceId}/roles`
-        )
-            .then((res) => {
                 usersRolesForm.setValue(
                     "roles",
-                    res.data.data.roles
+                    resourceRolesResponse.data.data.roles
                         .map((i) => ({
                             id: i.roleId.toString(),
                             text: i.name,
                         }))
                         .filter((role) => role.text !== "Admin")
                 );
-            })
-            .catch((e) => {
-                console.error(e);
-                toast({
-                    variant: "destructive",
-                    title: "Failed to fetch roles",
-                    description: formatAxiosError(
-                        e,
-                        "An error occurred while fetching the roles"
-                    ),
-                });
-            });
 
-        api.get<AxiosResponse<ListUsersResponse>>(
-            `/org/${org?.org.orgId}/users`
-        )
-            .then((res) => {
                 setAllUsers(
-                    res.data.data.users.map((user) => ({
+                    usersResponse.data.data.users.map((user) => ({
                         id: user.id.toString(),
                         text: user.email,
                     }))
                 );
-            })
-            .catch((e) => {
-                console.error(e);
-                toast({
-                    variant: "destructive",
-                    title: "Failed to fetch users",
-                    description: formatAxiosError(
-                        e,
-                        "An error occurred while fetching the users"
-                    ),
-                });
-            });
 
-        api.get<AxiosResponse<ListResourceUsersResponse>>(
-            `/resource/${resource.resourceId}/users`
-        )
-            .then((res) => {
                 usersRolesForm.setValue(
                     "users",
-                    res.data.data.users.map((i) => ({
+                    resourceUsersResponse.data.data.users.map((i) => ({
                         id: i.userId.toString(),
                         text: i.email,
                     }))
                 );
-            })
-            .catch((e) => {
+
+                setPageLoading(false);
+            } catch (e) {
                 console.error(e);
                 toast({
                     variant: "destructive",
-                    title: "Failed to fetch users",
+                    title: "Failed to fetch data",
                     description: formatAxiosError(
                         e,
-                        "An error occurred while fetching the users"
+                        "An error occurred while fetching the data"
                     ),
                 });
-            });
+            }
+        };
+
+        fetchData();
     }, []);
 
     async function onSubmitUsersRoles(
@@ -181,12 +165,28 @@ export default function ResourceAuthenticationPage() {
     ) {
         try {
             setLoadingSaveUsersRoles(true);
-            await api.post(`/resource/${resource.resourceId}/roles`, {
-                roleIds: data.roles.map((i) => parseInt(i.id)),
+
+            const jobs = [
+                api.post(`/resource/${resource.resourceId}/roles`, {
+                    roleIds: data.roles.map((i) => parseInt(i.id)),
+                }),
+                api.post(`/resource/${resource.resourceId}/users`, {
+                    userIds: data.users.map((i) => i.id),
+                }),
+                api.post(`/resource/${resource.resourceId}`, {
+                    sso: ssoEnabled,
+                    blockAccess,
+                }),
+            ];
+
+            await Promise.all(jobs);
+
+            updateResource({
+                sso: ssoEnabled,
             });
 
-            await api.post(`/resource/${resource.resourceId}/users`, {
-                userIds: data.users.map((i) => i.id),
+            updateAuthInfo({
+                sso: ssoEnabled,
             });
 
             toast({
@@ -208,48 +208,95 @@ export default function ResourceAuthenticationPage() {
         }
     }
 
-    async function onSubmitAuth() {
-        try {
-            setLoadingSaveAuth(true);
+    function removeResourcePassword() {
+        setLoadingRemoveResourcePassword(true);
 
-            await api.post(`/resource/${resource.resourceId}`, {
-                sso: ssoEnabled,
-                blockAccess,
-            });
+        api.post(`/resource/${resource.resourceId}/password`, {
+            password: null,
+        })
+            .then(() => {
+                toast({
+                    title: "Resource password removed",
+                    description:
+                        "The resource password has been removed successfully",
+                });
 
-            updateResource({
-                blockAccess,
-                sso: ssoEnabled,
-            });
+                updateAuthInfo({
+                    password: false,
+                });
+            })
+            .catch((e) => {
+                toast({
+                    variant: "destructive",
+                    title: "Error removing resource password",
+                    description: formatAxiosError(
+                        e,
+                        "An error occurred while removing the resource password"
+                    ),
+                });
+            })
+            .finally(() => setLoadingRemoveResourcePassword(false));
+    }
 
-            toast({
-                title: "Saved successfully",
-                description: "Authentication settings have been saved",
-            });
-        } catch (e) {
-            console.error(e);
-            toast({
-                variant: "destructive",
-                title: "Failed to save authentication",
-                description: formatAxiosError(
-                    e,
-                    "An error occurred while saving the authentication"
-                ),
-            });
-        } finally {
-            setLoadingSaveAuth(false);
-        }
+    if (pageLoading) {
+        return <></>;
     }
 
     return (
         <>
+            {isSetPasswordOpen && (
+                <SetResourcePasswordForm
+                    open={isSetPasswordOpen}
+                    setOpen={setIsSetPasswordOpen}
+                    resourceId={resource.resourceId}
+                    onSetPassword={() => {
+                        setIsSetPasswordOpen(false);
+                        updateAuthInfo({
+                            password: true,
+                        });
+                    }}
+                />
+            )}
+
             <div className="space-y-6 lg:max-w-2xl">
+                {/* <div>
+                    <div className="flex items-center space-x-2 mb-2">
+                        <Switch
+                            id="block-toggle"
+                            defaultChecked={resource.blockAccess}
+                            onCheckedChange={(val) => setBlockAccess(val)}
+                        />
+                        <Label htmlFor="block-toggle">Block Access</Label>
+                    </div>
+                    <span className="text-muted-foreground text-sm">
+                        When enabled, this will prevent anyone from accessing
+                        the resource including SSO users.
+                    </span>
+                </div> */}
+
                 <SettingsSectionTitle
                     title="Users & Roles"
                     description="Configure who can visit this resource (only applicable if SSO is used)"
                     size="1xl"
                 />
 
+                <div>
+                    <div className="flex items-center space-x-2 mb-2">
+                        <Switch
+                            id="sso-toggle"
+                            defaultChecked={resource.sso}
+                            onCheckedChange={(val) => setSsoEnabled(val)}
+                        />
+                        <Label htmlFor="sso-toggle">Allow SSO</Label>
+                    </div>
+                    <span className="text-muted-foreground text-sm">
+                        Users will be able to access the resource if they're
+                        logged into the dashboard and have access to the
+                        resource. Users will only have to login once for all
+                        resources that have SSO enabled.
+                    </span>
+                </div>
+
                 <Form {...usersRolesForm}>
                     <form
                         onSubmit={usersRolesForm.handleSubmit(
@@ -368,51 +415,39 @@ export default function ResourceAuthenticationPage() {
 
                 <SettingsSectionTitle
                     title="Authentication Methods"
-                    description="Configure how users can authenticate to this resource"
+                    description="You can also allow users to access the resource via the below methods"
                     size="1xl"
                 />
 
                 <div>
-                    <div className="flex items-center space-x-2 mb-2">
-                        <Switch
-                            id="block-toggle"
-                            defaultChecked={resource.blockAccess}
-                            onCheckedChange={(val) => setBlockAccess(val)}
-                        />
-                        <Label htmlFor="block-toggle">Block Access</Label>
-                    </div>
-                    <span className="text-muted-foreground text-sm">
-                        When enabled, all auth methods will be disabled and
-                        users will not able to access the resource. This is an
-                        override.
-                    </span>
+                    {authInfo?.password ? (
+                        <div className="flex items-center space-x-4">
+                            <div className="flex items-center text-green-500 space-x-2">
+                                <ShieldCheck />
+                                <span>Password Protection Enabled</span>
+                            </div>
+                            <Button
+                                variant="gray"
+                                type="button"
+                                loading={loadingRemoveResourcePassword}
+                                disabled={loadingRemoveResourcePassword}
+                                onClick={removeResourcePassword}
+                            >
+                                Remove Password
+                            </Button>
+                        </div>
+                    ) : (
+                        <div>
+                            <Button
+                                variant="gray"
+                                type="button"
+                                onClick={() => setIsSetPasswordOpen(true)}
+                            >
+                                Add Password
+                            </Button>
+                        </div>
+                    )}
                 </div>
-
-                <div>
-                    <div className="flex items-center space-x-2 mb-2">
-                        <Switch
-                            id="sso-toggle"
-                            defaultChecked={resource.sso}
-                            onCheckedChange={(val) => setSsoEnabled(val)}
-                        />
-                        <Label htmlFor="sso-toggle">Allow SSO</Label>
-                    </div>
-                    <span className="text-muted-foreground text-sm">
-                        Users will be able to access the resource if they're
-                        logged into the dashboard and have access to the
-                        resource. Users will only have to login once for all
-                        resources that have SSO enabled.
-                    </span>
-                </div>
-
-                <Button
-                    type="button"
-                    onClick={onSubmitAuth}
-                    loading={loadingSaveAuth}
-                    disabled={loadingSaveAuth}
-                >
-                    Save Authentication
-                </Button>
             </div>
         </>
     );

+ 34 - 5
src/app/[orgId]/settings/resources/[resourceId]/components/ResourceInfoBox.tsx

@@ -1,10 +1,17 @@
 "use client";
 
-import { useState } from "react";
+import { useEffect, useState } from "react";
 import { Card } from "@/components/ui/card";
 import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
 import { Button } from "@/components/ui/button";
-import { InfoIcon, LinkIcon, CheckIcon, CopyIcon } from "lucide-react";
+import {
+    InfoIcon,
+    LinkIcon,
+    CheckIcon,
+    CopyIcon,
+    ShieldCheck,
+    ShieldOff,
+} from "lucide-react";
 import { useOrgContext } from "@app/hooks/useOrgContext";
 import { useResourceContext } from "@app/hooks/useResourceContext";
 import Link from "next/link";
@@ -15,7 +22,7 @@ export default function ResourceInfoBox({}: ResourceInfoBoxType) {
     const [copied, setCopied] = useState(false);
 
     const { org } = useOrgContext();
-    const { resource } = useResourceContext();
+    const { resource, authInfo } = useResourceContext();
 
     const fullUrl = `${resource.ssl ? "https" : "http"}://${
         resource.subdomain
@@ -70,7 +77,7 @@ export default function ResourceInfoBox({}: ResourceInfoBoxType) {
                         </Button>
                     </div>
 
-                    <p className="mt-3">
+                    {/* <p className="mt-3">
                         To create a proxy to your private services,{" "}
                         <Link
                             href={`/${org.org.orgId}/settings/resources/${resource.resourceId}/connectivity`}
@@ -79,7 +86,29 @@ export default function ResourceInfoBox({}: ResourceInfoBoxType) {
                             add targets
                         </Link>{" "}
                         to this resource
-                    </p>
+                    </p> */}
+
+                    <div className="mt-3">
+                        {authInfo.password ||
+                        authInfo.pincode ||
+                        authInfo.sso ? (
+                            <div className="flex items-center space-x-2 text-green-500">
+                                <ShieldCheck />
+                                <span>
+                                    This resource is protected with at least one
+                                    auth method
+                                </span>
+                            </div>
+                        ) : (
+                            <div className="flex items-center space-x-2 text-yellow-500">
+                                <ShieldOff />
+                                <span>
+                                    This resource is not protected with any auth
+                                    method. Anyone can access this resource.
+                                </span>
+                            </div>
+                        )}
+                    </div>
                 </AlertDescription>
             </Alert>
         </Card>

+ 24 - 16
src/app/[orgId]/settings/resources/[resourceId]/connectivity/page.tsx

@@ -88,6 +88,8 @@ export default function ReverseProxyTargets(props: {
 
     const [loading, setLoading] = useState(false);
 
+    const [pageLoading, setPageLoading] = useState(true);
+
     const addTargetForm = useForm({
         resolver: zodResolver(addTargetSchema),
         defaultValues: {
@@ -100,24 +102,26 @@ export default function ReverseProxyTargets(props: {
 
     useEffect(() => {
         const fetchSites = async () => {
-            const res = await api
-                .get<AxiosResponse<ListTargetsResponse>>(
+            try {
+                const res = await api.get<AxiosResponse<ListTargetsResponse>>(
                     `/resource/${params.resourceId}/targets`
-                )
-                .catch((err) => {
-                    console.error(err);
-                    toast({
-                        variant: "destructive",
-                        title: "Failed to fetch targets",
-                        description: formatAxiosError(
-                            err,
-                            "An error occurred while fetching targets"
-                        ),
-                    });
-                });
+                );
 
-            if (res && res.status === 200) {
-                setTargets(res.data.data.targets);
+                if (res.status === 200) {
+                    setTargets(res.data.data.targets);
+                }
+            } catch (err) {
+                console.error(err);
+                toast({
+                    variant: "destructive",
+                    title: "Failed to fetch targets",
+                    description: formatAxiosError(
+                        err,
+                        "An error occurred while fetching targets"
+                    ),
+                });
+            } finally {
+                setPageLoading(false);
             }
         };
         fetchSites();
@@ -337,6 +341,10 @@ export default function ReverseProxyTargets(props: {
         getFilteredRowModel: getFilteredRowModel(),
     });
 
+    if (pageLoading) {
+        return <></>;
+    }
+
     return (
         <div>
             <div className="space-y-6">

+ 20 - 3
src/app/[orgId]/settings/resources/[resourceId]/layout.tsx

@@ -1,6 +1,9 @@
 import ResourceProvider from "@app/providers/ResourceProvider";
 import { internal } from "@app/api";
-import { GetResourceAuthInfoResponse } from "@server/routers/resource";
+import {
+    GetResourceAuthInfoResponse,
+    GetResourceResponse,
+} from "@server/routers/resource";
 import { AxiosResponse } from "axios";
 import { redirect } from "next/navigation";
 import { authCookieHeader } from "@app/api/cookies";
@@ -23,9 +26,10 @@ export default async function ResourceLayout(props: ResourceLayoutProps) {
 
     const { children } = props;
 
+    let authInfo = null;
     let resource = null;
     try {
-        const res = await internal.get<AxiosResponse<GetResourceAuthInfoResponse>>(
+        const res = await internal.get<AxiosResponse<GetResourceResponse>>(
             `/resource/${params.resourceId}`,
             await authCookieHeader()
         );
@@ -38,6 +42,19 @@ export default async function ResourceLayout(props: ResourceLayoutProps) {
         redirect(`/${params.orgId}/settings/resources`);
     }
 
+    try {
+        const res = await internal.get<
+            AxiosResponse<GetResourceAuthInfoResponse>
+        >(`/resource/${resource.resourceId}/auth`, await authCookieHeader());
+        authInfo = res.data.data;
+    } catch {
+        redirect(`/${params.orgId}/settings/resources`);
+    }
+
+    if (!authInfo) {
+        redirect(`/${params.orgId}/settings/resources`);
+    }
+
     let org = null;
     try {
         const getOrg = cache(async () =>
@@ -94,7 +111,7 @@ export default async function ResourceLayout(props: ResourceLayoutProps) {
             />
 
             <OrgProvider org={org}>
-                <ResourceProvider resource={resource}>
+                <ResourceProvider resource={resource} authInfo={authInfo}>
                     <SidebarSettings
                         sidebarNavItems={sidebarNavItems}
                         limitWidth={false}

+ 1 - 1
src/components/SidebarSettings.tsx

@@ -22,7 +22,7 @@ export function SidebarSettings({
 }: SideBarSettingsProps) {
     return (
         <div className="space-y-6 0 pb-16k">
-            <div className="flex flex-col space-y-6 lg:flex-row lg:space-x-12 lg:space-y-0">
+            <div className="flex flex-col space-y-6 lg:flex-row lg:space-x-32 lg:space-y-0">
                 <aside className="-mx-4 lg:w-1/5">
                     <SidebarNav items={sidebarNavItems} disabled={disabled} />
                 </aside>

+ 5 - 0
src/contexts/resourceContext.ts

@@ -1,9 +1,14 @@
+import { GetResourceAuthInfoResponse } from "@server/routers/resource";
 import { GetResourceResponse } from "@server/routers/resource/getResource";
 import { createContext } from "react";
 
 interface ResourceContextType {
     resource: GetResourceResponse;
+    authInfo: GetResourceAuthInfoResponse;
     updateResource: (updatedResource: Partial<GetResourceResponse>) => void;
+    updateAuthInfo: (
+        updatedAuthInfo: Partial<GetResourceAuthInfoResponse>
+    ) => void;
 }
 
 const ResourceContext = createContext<ResourceContextType | undefined>(

+ 28 - 1
src/providers/ResourceProvider.tsx

@@ -1,21 +1,27 @@
 "use client";
 
 import ResourceContext from "@app/contexts/resourceContext";
+import { GetResourceAuthInfoResponse } from "@server/routers/resource";
 import { GetResourceResponse } from "@server/routers/resource/getResource";
 import { useState } from "react";
 
 interface ResourceProviderProps {
     children: React.ReactNode;
     resource: GetResourceResponse;
+    authInfo: GetResourceAuthInfoResponse;
 }
 
 export function ResourceProvider({
     children,
     resource: serverResource,
+    authInfo: serverAuthInfo,
 }: ResourceProviderProps) {
     const [resource, setResource] =
         useState<GetResourceResponse>(serverResource);
 
+    const [authInfo, setAuthInfo] =
+        useState<GetResourceAuthInfoResponse>(serverAuthInfo);
+
     const updateResource = (updatedResource: Partial<GetResourceResponse>) => {
         if (!resource) {
             throw new Error("No resource to update");
@@ -33,8 +39,29 @@ export function ResourceProvider({
         });
     };
 
+    const updateAuthInfo = (
+        updatedAuthInfo: Partial<GetResourceAuthInfoResponse>
+    ) => {
+        if (!authInfo) {
+            throw new Error("No auth info to update");
+        }
+
+        setAuthInfo((prev) => {
+            if (!prev) {
+                return prev;
+            }
+
+            return {
+                ...prev,
+                ...updatedAuthInfo,
+            };
+        });
+    };
+
     return (
-        <ResourceContext.Provider value={{ resource, updateResource }}>
+        <ResourceContext.Provider
+            value={{ resource, updateResource, authInfo, updateAuthInfo }}
+        >
             {children}
         </ResourceContext.Provider>
     );