From cfce3dabb3afc9e35180d34d6cf30fa471b286e6 Mon Sep 17 00:00:00 2001
From: Milo Schwartz
Date: Tue, 19 Nov 2024 00:05:04 -0500
Subject: [PATCH] set resource password and remove resource password from
dashboard
---
server/db/schema.ts | 2 +-
.../routers/resource/setResourcePassword.ts | 2 +-
.../components/SetResourcePasswordForm.tsx | 172 ++++++++++
.../[resourceId]/authentication/page.tsx | 307 ++++++++++--------
.../components/ResourceInfoBox.tsx | 39 ++-
.../[resourceId]/connectivity/page.tsx | 40 ++-
.../resources/[resourceId]/layout.tsx | 23 +-
src/components/SidebarSettings.tsx | 2 +-
src/contexts/resourceContext.ts | 5 +
src/providers/ResourceProvider.tsx | 29 +-
10 files changed, 457 insertions(+), 164 deletions(-)
create mode 100644 src/app/[orgId]/settings/resources/[resourceId]/authentication/components/SetResourcePasswordForm.tsx
diff --git a/server/db/schema.ts b/server/db/schema.ts
index d9fa02a6..9d3ac0e2 100644
--- a/server/db/schema.ts
+++ b/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),
diff --git a/server/routers/resource/setResourcePassword.ts b/server/routers/resource/setResourcePassword.ts
index f54de902..54fdcdf3 100644
--- a/server/routers/resource/setResourcePassword.ts
+++ b/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();
diff --git a/src/app/[orgId]/settings/resources/[resourceId]/authentication/components/SetResourcePasswordForm.tsx b/src/app/[orgId]/settings/resources/[resourceId]/authentication/components/SetResourcePasswordForm.tsx
new file mode 100644
index 00000000..521e76cf
--- /dev/null
+++ b/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;
+
+const defaultValues: Partial = {
+ 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({
+ resolver: zodResolver(setPasswordFormSchema),
+ defaultValues,
+ });
+
+ useEffect(() => {
+ if (!open) {
+ return;
+ }
+
+ form.reset();
+ }, [open]);
+
+ async function onSubmit(data: SetPasswordFormValues) {
+ setLoading(true);
+
+ api.post>(`/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 (
+ <>
+ {
+ setOpen(val);
+ setLoading(false);
+ form.reset();
+ }}
+ >
+
+
+ Set Password
+
+ Set a password to protect this resource
+
+
+
+
+
+
+
+
+ Enable Password Protection
+
+
+ Close
+
+
+
+
+ >
+ );
+}
diff --git a/src/app/[orgId]/settings/resources/[resourceId]/authentication/page.tsx b/src/app/[orgId]/settings/resources/[resourceId]/authentication/page.tsx
index 4947e91b..c4971345 100644
--- a/src/app/[orgId]/settings/resources/[resourceId]/authentication/page.tsx
+++ b/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>({
resolver: zodResolver(UsersRolesFormSchema),
@@ -77,103 +87,77 @@ export default function ResourceAuthenticationPage() {
});
useEffect(() => {
- api.get>(
- `/org/${org?.org.orgId}/roles`
- )
- .then((res) => {
+ const fetchData = async () => {
+ try {
+ const [
+ rolesResponse,
+ resourceRolesResponse,
+ usersResponse,
+ resourceUsersResponse,
+ ] = await Promise.all([
+ api.get>(
+ `/org/${org?.org.orgId}/roles`
+ ),
+ api.get>(
+ `/resource/${resource.resourceId}/roles`
+ ),
+ api.get>(
+ `/org/${org?.org.orgId}/users`
+ ),
+ api.get>(
+ `/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>(
- `/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>(
- `/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>(
- `/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 && (
+ {
+ setIsSetPasswordOpen(false);
+ updateAuthInfo({
+ password: true,
+ });
+ }}
+ />
+ )}
+
+ {/*
+
+ setBlockAccess(val)}
+ />
+ Block Access
+
+
+ When enabled, this will prevent anyone from accessing
+ the resource including SSO users.
+
+
*/}
+
+
+
+ setSsoEnabled(val)}
+ />
+ Allow SSO
+
+
+ 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.
+
+
+
-
- setBlockAccess(val)}
- />
- Block Access
-
-
- When enabled, all auth methods will be disabled and
- users will not able to access the resource. This is an
- override.
-
+ {authInfo?.password ? (
+
+
+
+ Password Protection Enabled
+
+
+ Remove Password
+
+
+ ) : (
+
+ setIsSetPasswordOpen(true)}
+ >
+ Add Password
+
+
+ )}
-
-
-
- setSsoEnabled(val)}
- />
- Allow SSO
-
-
- 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.
-
-
-
-
- Save Authentication
-
>
);
diff --git a/src/app/[orgId]/settings/resources/[resourceId]/components/ResourceInfoBox.tsx b/src/app/[orgId]/settings/resources/[resourceId]/components/ResourceInfoBox.tsx
index bf86fc94..238bfe10 100644
--- a/src/app/[orgId]/settings/resources/[resourceId]/components/ResourceInfoBox.tsx
+++ b/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) {
-
+ {/*
To create a proxy to your private services,{" "}
{" "}
to this resource
-
+
*/}
+
+
+ {authInfo.password ||
+ authInfo.pincode ||
+ authInfo.sso ? (
+
+
+
+ This resource is protected with at least one
+ auth method
+
+
+ ) : (
+
+
+
+ This resource is not protected with any auth
+ method. Anyone can access this resource.
+
+
+ )}
+
diff --git a/src/app/[orgId]/settings/resources/[resourceId]/connectivity/page.tsx b/src/app/[orgId]/settings/resources/[resourceId]/connectivity/page.tsx
index 1d96d071..04236872 100644
--- a/src/app/[orgId]/settings/resources/[resourceId]/connectivity/page.tsx
+++ b/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>(
+ try {
+ const res = await api.get>(
`/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 (
diff --git a/src/app/[orgId]/settings/resources/[resourceId]/layout.tsx b/src/app/[orgId]/settings/resources/[resourceId]/layout.tsx
index 90da54f6..05f9550b 100644
--- a/src/app/[orgId]/settings/resources/[resourceId]/layout.tsx
+++ b/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
>(
+ const res = await internal.get>(
`/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
+ >(`/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) {
/>
-
+
-
+
diff --git a/src/contexts/resourceContext.ts b/src/contexts/resourceContext.ts
index 40c43120..bb5501a6 100644
--- a/src/contexts/resourceContext.ts
+++ b/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
) => void;
+ updateAuthInfo: (
+ updatedAuthInfo: Partial
+ ) => void;
}
const ResourceContext = createContext(
diff --git a/src/providers/ResourceProvider.tsx b/src/providers/ResourceProvider.tsx
index cdf1c8d5..cd6229a4 100644
--- a/src/providers/ResourceProvider.tsx
+++ b/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(serverResource);
+ const [authInfo, setAuthInfo] =
+ useState(serverAuthInfo);
+
const updateResource = (updatedResource: Partial) => {
if (!resource) {
throw new Error("No resource to update");
@@ -33,8 +39,29 @@ export function ResourceProvider({
});
};
+ const updateAuthInfo = (
+ updatedAuthInfo: Partial
+ ) => {
+ if (!authInfo) {
+ throw new Error("No auth info to update");
+ }
+
+ setAuthInfo((prev) => {
+ if (!prev) {
+ return prev;
+ }
+
+ return {
+ ...prev,
+ ...updatedAuthInfo,
+ };
+ });
+ };
+
return (
-
+
{children}
);