set resource password and remove resource password from dashboard

This commit is contained in:
Milo Schwartz 2024-11-19 00:05:04 -05:00
parent ab6d59c163
commit cfce3dabb3
No known key found for this signature in database
10 changed files with 457 additions and 164 deletions

View file

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

View file

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

View file

@ -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>
</>
);
}

View file

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

View file

@ -1,10 +1,17 @@
"use client"; "use client";
import { useState } from "react"; import { useEffect, useState } from "react";
import { Card } from "@/components/ui/card"; import { Card } from "@/components/ui/card";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { Button } from "@/components/ui/button"; 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 { useOrgContext } from "@app/hooks/useOrgContext";
import { useResourceContext } from "@app/hooks/useResourceContext"; import { useResourceContext } from "@app/hooks/useResourceContext";
import Link from "next/link"; import Link from "next/link";
@ -15,7 +22,7 @@ export default function ResourceInfoBox({}: ResourceInfoBoxType) {
const [copied, setCopied] = useState(false); const [copied, setCopied] = useState(false);
const { org } = useOrgContext(); const { org } = useOrgContext();
const { resource } = useResourceContext(); const { resource, authInfo } = useResourceContext();
const fullUrl = `${resource.ssl ? "https" : "http"}://${ const fullUrl = `${resource.ssl ? "https" : "http"}://${
resource.subdomain resource.subdomain
@ -70,7 +77,7 @@ export default function ResourceInfoBox({}: ResourceInfoBoxType) {
</Button> </Button>
</div> </div>
<p className="mt-3"> {/* <p className="mt-3">
To create a proxy to your private services,{" "} To create a proxy to your private services,{" "}
<Link <Link
href={`/${org.org.orgId}/settings/resources/${resource.resourceId}/connectivity`} href={`/${org.org.orgId}/settings/resources/${resource.resourceId}/connectivity`}
@ -79,7 +86,29 @@ export default function ResourceInfoBox({}: ResourceInfoBoxType) {
add targets add targets
</Link>{" "} </Link>{" "}
to this resource 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> </AlertDescription>
</Alert> </Alert>
</Card> </Card>

View file

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

View file

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

View file

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

View file

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

View file

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