disable eslint - new colors, and slimmer buttons/inputs??

This commit is contained in:
Milo Schwartz 2024-11-22 22:09:40 -05:00
parent bf04deb038
commit 5388c5d5b4
No known key found for this signature in database
20 changed files with 788 additions and 553 deletions

View file

@ -1,6 +0,0 @@
{
"extends": [
"next/core-web-vitals",
"next/typescript"
]
}

View file

@ -88,8 +88,6 @@
"drizzle-kit": "0.24.2", "drizzle-kit": "0.24.2",
"esbuild": "0.20.1", "esbuild": "0.20.1",
"esbuild-node-externals": "1.13.0", "esbuild-node-externals": "1.13.0",
"eslint": "^8",
"eslint-config-next": "15.0.1",
"postcss": "^8", "postcss": "^8",
"react-email": "3.0.1", "react-email": "3.0.1",
"tailwindcss": "^3.4.1", "tailwindcss": "^3.4.1",

View file

@ -90,7 +90,7 @@ export async function verifyResourceSession(
return allowed(res); return allowed(res);
} }
const redirectUrl = `${config.app.base_url}/auth/resource/${resource.resourceId}/login?redirect=${originalRequestURL}`; const redirectUrl = `${config.app.base_url}/${resource.orgId}/auth/resource/${resource.resourceId}?redirect=${originalRequestURL}`;
if (sso && sessions.session) { if (sso && sessions.session) {
const { session, user } = await validateSessionToken( const { session, user } = await validateSessionToken(

View file

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

View file

@ -0,0 +1,205 @@
"use client";
import { useState } from "react";
import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";
import * as z from "zod";
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { LockIcon, KeyIcon, UserIcon, Binary, Key, User } from "lucide-react";
const pinSchema = z.object({
pin: z
.string()
.length(6, { message: "PIN must be exactly 6 digits" })
.regex(/^\d+$/, { message: "PIN must only contain numbers" }),
});
const passwordSchema = z.object({
email: z.string().email({ message: "Please enter a valid email address" }),
password: z
.string()
.min(8, { message: "Password must be at least 8 characters long" }),
});
export default function ResourceAuthPortal() {
const [activeTab, setActiveTab] = useState("pin");
const pinForm = useForm<z.infer<typeof pinSchema>>({
resolver: zodResolver(pinSchema),
defaultValues: {
pin: "",
},
});
const passwordForm = useForm<z.infer<typeof passwordSchema>>({
resolver: zodResolver(passwordSchema),
defaultValues: {
email: "",
password: "",
},
});
const onPinSubmit = (values: z.infer<typeof pinSchema>) => {
console.log("PIN authentication", values);
// Implement PIN authentication logic here
};
const onPasswordSubmit = (values: z.infer<typeof passwordSchema>) => {
console.log("Password authentication", values);
// Implement password authentication logic here
};
const handleSSOAuth = () => {
console.log("SSO authentication");
// Implement SSO authentication logic here
};
return (
<div className="w-full max-w-md mx-auto">
<Card>
<CardHeader>
<CardTitle>Welcome Back</CardTitle>
<CardDescription>
Choose your preferred login method
</CardDescription>
</CardHeader>
<CardContent>
<Tabs value={activeTab} onValueChange={setActiveTab}>
<TabsList className="grid w-full grid-cols-3">
<TabsTrigger value="pin">
<Binary className="w-4 h-4 mr-1" /> PIN
</TabsTrigger>
<TabsTrigger value="password">
<Key className="w-4 h-4 mr-1" /> Password
</TabsTrigger>
<TabsTrigger value="sso">
<User className="w-4 h-4 mr-1" /> SSO
</TabsTrigger>
</TabsList>
<TabsContent value="pin">
<Form {...pinForm}>
<form
onSubmit={pinForm.handleSubmit(onPinSubmit)}
className="space-y-4"
>
<FormField
control={pinForm.control}
name="pin"
render={({ field }) => (
<FormItem>
<FormLabel>Enter PIN</FormLabel>
<FormControl>
<Input
placeholder="Enter your 6-digit PIN"
type="password"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit" className="w-full">
<KeyIcon className="w-4 h-4 mr-2" />
Login with PIN
</Button>
</form>
</Form>
</TabsContent>
<TabsContent value="password">
<Form {...passwordForm}>
<form
onSubmit={passwordForm.handleSubmit(
onPasswordSubmit
)}
className="space-y-4"
>
<FormField
control={passwordForm.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input
placeholder="Enter your email"
type="email"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={passwordForm.control}
name="password"
render={({ field }) => (
<FormItem>
<FormLabel>Password</FormLabel>
<FormControl>
<Input
placeholder="Enter your password"
type="password"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit" className="w-full">
<LockIcon className="w-4 h-4 mr-2" />
Login with Password
</Button>
</form>
</Form>
</TabsContent>
<TabsContent value="sso">
<div className="space-y-4">
<p className="text-sm text-muted-foreground">
Click the button below to login with your
organization's SSO provider.
</p>
<Button
onClick={handleSSOAuth}
className="w-full"
>
<UserIcon className="w-4 h-4 mr-2" />
Login with SSO
</Button>
</div>
</TabsContent>
</Tabs>
</CardContent>
<CardFooter className="flex justify-center">
<p className="text-sm text-muted-foreground">
Don't have an account?{" "}
<a href="#" className="underline">
Sign up
</a>
</p>
</CardFooter>
</Card>
</div>
);
}

View file

@ -0,0 +1,17 @@
import ResourceAuthPortal from "./components/ResourceAuthPortal";
export default async function ResourceAuthPage(props: {
params: Promise<{ resourceId: number; orgId: string }>;
}) {
const params = await props.params;
console.log(params);
return (
<>
<div className="p-3 md:mt-32">
<ResourceAuthPortal />
</div>
</>
);
}

View file

@ -32,22 +32,22 @@ 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 { ShieldCheck } from "lucide-react";
import SetResourcePasswordForm from "./components/SetResourcePasswordForm"; import SetResourcePasswordForm from "./components/SetResourcePasswordForm";
import { Separator } from "@app/components/ui/separator";
const UsersRolesFormSchema = z.object({ const UsersRolesFormSchema = z.object({
roles: z.array( roles: z.array(
z.object({ z.object({
id: z.string(), id: z.string(),
text: z.string(), text: z.string(),
}) }),
), ),
users: z.array( users: z.array(
z.object({ z.object({
id: z.string(), id: z.string(),
text: z.string(), text: z.string(),
}) }),
), ),
}); });
@ -60,10 +60,10 @@ export default function ResourceAuthenticationPage() {
const [pageLoading, setPageLoading] = useState(true); const [pageLoading, setPageLoading] = useState(true);
const [allRoles, setAllRoles] = useState<{ id: string; text: string }[]>( const [allRoles, setAllRoles] = useState<{ id: string; text: string }[]>(
[] [],
); );
const [allUsers, setAllUsers] = useState<{ id: string; text: string }[]>( const [allUsers, setAllUsers] = useState<{ id: string; text: string }[]>(
[] [],
); );
const [activeRolesTagIndex, setActiveRolesTagIndex] = useState< const [activeRolesTagIndex, setActiveRolesTagIndex] = useState<
number | null number | null
@ -73,7 +73,7 @@ export default function ResourceAuthenticationPage() {
>(null); >(null);
const [ssoEnabled, setSsoEnabled] = useState(resource.sso); const [ssoEnabled, setSsoEnabled] = useState(resource.sso);
const [blockAccess, setBlockAccess] = useState(resource.blockAccess); // const [blockAccess, setBlockAccess] = useState(resource.blockAccess);
const [loadingSaveUsersRoles, setLoadingSaveUsersRoles] = useState(false); const [loadingSaveUsersRoles, setLoadingSaveUsersRoles] = useState(false);
const [loadingRemoveResourcePassword, setLoadingRemoveResourcePassword] = const [loadingRemoveResourcePassword, setLoadingRemoveResourcePassword] =
@ -96,16 +96,16 @@ export default function ResourceAuthenticationPage() {
resourceUsersResponse, resourceUsersResponse,
] = await Promise.all([ ] = await Promise.all([
api.get<AxiosResponse<ListRolesResponse>>( api.get<AxiosResponse<ListRolesResponse>>(
`/org/${org?.org.orgId}/roles` `/org/${org?.org.orgId}/roles`,
), ),
api.get<AxiosResponse<ListResourceRolesResponse>>( api.get<AxiosResponse<ListResourceRolesResponse>>(
`/resource/${resource.resourceId}/roles` `/resource/${resource.resourceId}/roles`,
), ),
api.get<AxiosResponse<ListUsersResponse>>( api.get<AxiosResponse<ListUsersResponse>>(
`/org/${org?.org.orgId}/users` `/org/${org?.org.orgId}/users`,
), ),
api.get<AxiosResponse<ListResourceUsersResponse>>( api.get<AxiosResponse<ListResourceUsersResponse>>(
`/resource/${resource.resourceId}/users` `/resource/${resource.resourceId}/users`,
), ),
]); ]);
@ -115,7 +115,7 @@ export default function ResourceAuthenticationPage() {
id: role.roleId.toString(), id: role.roleId.toString(),
text: role.name, text: role.name,
})) }))
.filter((role) => role.text !== "Admin") .filter((role) => role.text !== "Admin"),
); );
usersRolesForm.setValue( usersRolesForm.setValue(
@ -125,14 +125,14 @@ export default function ResourceAuthenticationPage() {
id: i.roleId.toString(), id: i.roleId.toString(),
text: i.name, text: i.name,
})) }))
.filter((role) => role.text !== "Admin") .filter((role) => role.text !== "Admin"),
); );
setAllUsers( setAllUsers(
usersResponse.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,
})) })),
); );
usersRolesForm.setValue( usersRolesForm.setValue(
@ -140,7 +140,7 @@ export default function ResourceAuthenticationPage() {
resourceUsersResponse.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,
})) })),
); );
setPageLoading(false); setPageLoading(false);
@ -151,7 +151,7 @@ export default function ResourceAuthenticationPage() {
title: "Failed to fetch data", title: "Failed to fetch data",
description: formatAxiosError( description: formatAxiosError(
e, e,
"An error occurred while fetching the data" "An error occurred while fetching the data",
), ),
}); });
} }
@ -161,7 +161,7 @@ export default function ResourceAuthenticationPage() {
}, []); }, []);
async function onSubmitUsersRoles( async function onSubmitUsersRoles(
data: z.infer<typeof UsersRolesFormSchema> data: z.infer<typeof UsersRolesFormSchema>,
) { ) {
try { try {
setLoadingSaveUsersRoles(true); setLoadingSaveUsersRoles(true);
@ -175,7 +175,6 @@ export default function ResourceAuthenticationPage() {
}), }),
api.post(`/resource/${resource.resourceId}`, { api.post(`/resource/${resource.resourceId}`, {
sso: ssoEnabled, sso: ssoEnabled,
blockAccess,
}), }),
]; ];
@ -200,7 +199,7 @@ export default function ResourceAuthenticationPage() {
title: "Failed to set roles", title: "Failed to set roles",
description: formatAxiosError( description: formatAxiosError(
e, e,
"An error occurred while setting the roles" "An error occurred while setting the roles",
), ),
}); });
} finally { } finally {
@ -231,7 +230,7 @@ export default function ResourceAuthenticationPage() {
title: "Error removing resource password", title: "Error removing resource password",
description: formatAxiosError( description: formatAxiosError(
e, e,
"An error occurred while removing the resource password" "An error occurred while removing the resource password",
), ),
}); });
}) })
@ -258,22 +257,8 @@ export default function ResourceAuthenticationPage() {
/> />
)} )}
<div className="space-y-6 lg:max-w-2xl"> <div className="space-y-12 lg:max-w-2xl">
{/* <div> <section className="space-y-6">
<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)"
@ -300,7 +285,7 @@ export default function ResourceAuthenticationPage() {
<Form {...usersRolesForm}> <Form {...usersRolesForm}>
<form <form
onSubmit={usersRolesForm.handleSubmit( onSubmit={usersRolesForm.handleSubmit(
onSubmitUsersRoles onSubmitUsersRoles,
)} )}
className="space-y-6" className="space-y-6"
> >
@ -313,18 +298,24 @@ export default function ResourceAuthenticationPage() {
<FormControl> <FormControl>
<TagInput <TagInput
{...field} {...field}
activeTagIndex={activeRolesTagIndex} activeTagIndex={
activeRolesTagIndex
}
setActiveTagIndex={ setActiveTagIndex={
setActiveRolesTagIndex setActiveRolesTagIndex
} }
placeholder="Enter a role" placeholder="Enter a role"
tags={ tags={
usersRolesForm.getValues().roles usersRolesForm.getValues()
.roles
} }
setTags={(newRoles) => { setTags={(newRoles) => {
usersRolesForm.setValue( usersRolesForm.setValue(
"roles", "roles",
newRoles as [Tag, ...Tag[]] newRoles as [
Tag,
...Tag[],
],
); );
}} }}
enableAutocomplete={true} enableAutocomplete={true}
@ -336,7 +327,7 @@ export default function ResourceAuthenticationPage() {
sortTags={true} sortTags={true}
styleClasses={{ styleClasses={{
tag: { tag: {
body: "bg-muted hover:bg-accent text-foreground p-2", body: "bg-muted hover:bg-accent text-foreground py-2 px-3 rounded-full",
}, },
input: "border-none bg-transparent text-inherit placeholder:text-inherit shadow-none", input: "border-none bg-transparent text-inherit placeholder:text-inherit shadow-none",
inlineTagsContainer: inlineTagsContainer:
@ -345,9 +336,9 @@ export default function ResourceAuthenticationPage() {
/> />
</FormControl> </FormControl>
<FormDescription> <FormDescription>
Users with these roles will be able to Users with these roles will be able
access this resource. Admins can always to access this resource. Admins can
access this resource. always access this resource.
</FormDescription> </FormDescription>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
@ -362,18 +353,24 @@ export default function ResourceAuthenticationPage() {
<FormControl> <FormControl>
<TagInput <TagInput
{...field} {...field}
activeTagIndex={activeUsersTagIndex} activeTagIndex={
activeUsersTagIndex
}
setActiveTagIndex={ setActiveTagIndex={
setActiveUsersTagIndex setActiveUsersTagIndex
} }
placeholder="Enter a user" placeholder="Enter a user"
tags={ tags={
usersRolesForm.getValues().users usersRolesForm.getValues()
.users
} }
setTags={(newUsers) => { setTags={(newUsers) => {
usersRolesForm.setValue( usersRolesForm.setValue(
"users", "users",
newUsers as [Tag, ...Tag[]] newUsers as [
Tag,
...Tag[],
],
); );
}} }}
enableAutocomplete={true} enableAutocomplete={true}
@ -385,7 +382,7 @@ export default function ResourceAuthenticationPage() {
sortTags={true} sortTags={true}
styleClasses={{ styleClasses={{
tag: { tag: {
body: "bg-muted hover:bg-accent text-foreground p-2", body: "bg-muted hover:bg-accent text-foreground py-2 px-3 rounded-full",
}, },
input: "border-none bg-transparent text-inherit placeholder:text-inherit shadow-none", input: "border-none bg-transparent text-inherit placeholder:text-inherit shadow-none",
inlineTagsContainer: inlineTagsContainer:
@ -394,10 +391,11 @@ export default function ResourceAuthenticationPage() {
/> />
</FormControl> </FormControl>
<FormDescription> <FormDescription>
Users added here will be able to access Users added here will be able to
this resource. A user will always have access this resource. A user will
access to a resource if they have a role always have access to a resource if
that has access to it. they have a role that has access to
it.
</FormDescription> </FormDescription>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
@ -412,14 +410,17 @@ export default function ResourceAuthenticationPage() {
</Button> </Button>
</form> </form>
</Form> </Form>
</section>
<Separator />
<section className="space-y-6">
<SettingsSectionTitle <SettingsSectionTitle
title="Authentication Methods" title="Authentication Methods"
description="You can also allow users to access the resource via the below methods" description="You can also allow users to access the resource via the below methods"
size="1xl" size="1xl"
/> />
<div>
{authInfo?.password ? ( {authInfo?.password ? (
<div className="flex items-center space-x-4"> <div className="flex items-center space-x-4">
<div className="flex items-center text-green-500 space-x-2"> <div className="flex items-center text-green-500 space-x-2">
@ -447,7 +448,7 @@ export default function ResourceAuthenticationPage() {
</Button> </Button>
</div> </div>
)} )}
</div> </section>
</div> </div>
</> </>
); );

View file

@ -39,17 +39,38 @@ export default function ResourceInfoBox({}: ResourceInfoBoxType) {
}; };
return ( return (
<Card> <Card className="shadow-none">
<Alert> <Alert>
<InfoIcon className="h-4 w-4" /> <InfoIcon className="h-4 w-4" />
<AlertTitle className="font-semibold"> <AlertTitle className="font-semibold">
Resource Information Resource Information
</AlertTitle> </AlertTitle>
<AlertDescription className="mt-3"> <AlertDescription className="mt-3">
<p className="mb-2"> <div className="space-y-3">
The current full URL for this resource is: <div>
</p> {authInfo.password ||
<div className="flex items-center space-x-2 bg-muted p-2 rounded-md"> 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>
<div className="flex items-center space-x-2 bg-muted p-1 pl-3 rounded-md">
<LinkIcon className="h-4 w-4" /> <LinkIcon className="h-4 w-4" />
<a <a
href={fullUrl} href={fullUrl}
@ -87,27 +108,6 @@ export default function ResourceInfoBox({}: ResourceInfoBoxType) {
</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> </div>
</AlertDescription> </AlertDescription>
</Alert> </Alert>

View file

@ -51,6 +51,7 @@ import { useResourceContext } from "@app/hooks/useResourceContext";
import { ArrayElement } from "@server/types/ArrayElement"; import { ArrayElement } from "@server/types/ArrayElement";
import { Dot } from "lucide-react"; import { Dot } from "lucide-react";
import { formatAxiosError } from "@app/lib/utils"; import { formatAxiosError } from "@app/lib/utils";
import { Separator } from "@radix-ui/react-separator";
const addTargetSchema = z.object({ const addTargetSchema = z.object({
ip: z.string().ip(), ip: z.string().ip(),
@ -104,7 +105,7 @@ export default function ReverseProxyTargets(props: {
const fetchSites = async () => { const fetchSites = async () => {
try { try {
const res = await api.get<AxiosResponse<ListTargetsResponse>>( const res = await api.get<AxiosResponse<ListTargetsResponse>>(
`/resource/${params.resourceId}/targets` `/resource/${params.resourceId}/targets`,
); );
if (res.status === 200) { if (res.status === 200) {
@ -117,7 +118,7 @@ export default function ReverseProxyTargets(props: {
title: "Failed to fetch targets", title: "Failed to fetch targets",
description: formatAxiosError( description: formatAxiosError(
err, err,
"An error occurred while fetching targets" "An error occurred while fetching targets",
), ),
}); });
} finally { } finally {
@ -155,8 +156,8 @@ export default function ReverseProxyTargets(props: {
targets.map((target) => targets.map((target) =>
target.targetId === targetId target.targetId === targetId
? { ...target, ...data, updated: true } ? { ...target, ...data, updated: true }
: target : target,
) ),
); );
} }
@ -186,7 +187,7 @@ export default function ReverseProxyTargets(props: {
} else if (target.updated) { } else if (target.updated) {
const res = await api.post( const res = await api.post(
`/target/${target.targetId}`, `/target/${target.targetId}`,
data data,
); );
} }
@ -204,7 +205,7 @@ export default function ReverseProxyTargets(props: {
for (const targetId of targetsToRemove) { for (const targetId of targetsToRemove) {
await api.delete(`/target/${targetId}`); await api.delete(`/target/${targetId}`);
setTargets( setTargets(
targets.filter((target) => target.targetId !== targetId) targets.filter((target) => target.targetId !== targetId),
); );
} }
@ -221,7 +222,7 @@ export default function ReverseProxyTargets(props: {
title: "Operation failed", title: "Operation failed",
description: formatAxiosError( description: formatAxiosError(
err, err,
"An error occurred during the save operation" "An error occurred during the save operation",
), ),
}); });
} }
@ -346,8 +347,9 @@ export default function ReverseProxyTargets(props: {
} }
return ( return (
<div> <>
<div className="space-y-6"> <div className="space-y-12">
<section className="space-y-6 lg:max-w-2xl">
<SettingsSectionTitle <SettingsSectionTitle
title="SSL" title="SSL"
description="Setup SSL to secure your connections with LetsEncrypt certificates" description="Setup SSL to secure your connections with LetsEncrypt certificates"
@ -362,16 +364,23 @@ export default function ReverseProxyTargets(props: {
/> />
<Label htmlFor="ssl-toggle">Enable SSL (https)</Label> <Label htmlFor="ssl-toggle">Enable SSL (https)</Label>
</div> </div>
</section>
<hr className="lg:max-w-2xl" />
<section className="space-y-6">
<SettingsSectionTitle <SettingsSectionTitle
title="Targets" title="Targets"
description="Setup targets to route traffic to your services" description="Setup targets to route traffic to your services"
size="1xl" size="1xl"
/> />
<div className="space-y-6">
<Form {...addTargetForm}> <Form {...addTargetForm}>
<form <form
onSubmit={addTargetForm.handleSubmit(addTarget as any)} onSubmit={addTargetForm.handleSubmit(
addTarget as any,
)}
> >
<div className="grid grid-cols-2 md:grid-cols-4 gap-4"> <div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<FormField <FormField
@ -379,12 +388,15 @@ export default function ReverseProxyTargets(props: {
name="ip" name="ip"
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel>IP Address</FormLabel> <FormLabel>
IP Address
</FormLabel>
<FormControl> <FormControl>
<Input id="ip" {...field} /> <Input id="ip" {...field} />
</FormControl> </FormControl>
<FormDescription> <FormDescription>
Enter the IP address of the target Enter the IP address of the
target.
</FormDescription> </FormDescription>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
@ -399,10 +411,12 @@ export default function ReverseProxyTargets(props: {
<FormControl> <FormControl>
<Select <Select
{...field} {...field}
onValueChange={(value) => { onValueChange={(
value,
) => {
addTargetForm.setValue( addTargetForm.setValue(
"method", "method",
value value,
); );
}} }}
> >
@ -420,8 +434,8 @@ export default function ReverseProxyTargets(props: {
</Select> </Select>
</FormControl> </FormControl>
<FormDescription> <FormDescription>
Choose the method for how the target Choose the method for how
is accessed the target is accessed.
</FormDescription> </FormDescription>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
@ -442,8 +456,8 @@ export default function ReverseProxyTargets(props: {
/> />
</FormControl> </FormControl>
<FormDescription> <FormDescription>
Specify the port number for the Specify the port number for
target the target.
</FormDescription> </FormDescription>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
@ -496,19 +510,27 @@ export default function ReverseProxyTargets(props: {
<div className="rounded-md border"> <div className="rounded-md border">
<Table> <Table>
<TableHeader> <TableHeader>
{table.getHeaderGroups().map((headerGroup) => ( {table
.getHeaderGroups()
.map((headerGroup) => (
<TableRow key={headerGroup.id}> <TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => ( {headerGroup.headers.map(
<TableHead key={header.id}> (header) => (
<TableHead
key={header.id}
>
{header.isPlaceholder {header.isPlaceholder
? null ? null
: flexRender( : flexRender(
header.column.columnDef header
.column
.columnDef
.header, .header,
header.getContext() header.getContext(),
)} )}
</TableHead> </TableHead>
))} ),
)}
</TableRow> </TableRow>
))} ))}
</TableHeader> </TableHeader>
@ -516,11 +538,17 @@ export default function ReverseProxyTargets(props: {
{table.getRowModel().rows?.length ? ( {table.getRowModel().rows?.length ? (
table.getRowModel().rows.map((row) => ( table.getRowModel().rows.map((row) => (
<TableRow key={row.id}> <TableRow key={row.id}>
{row.getVisibleCells().map((cell) => ( {row
<TableCell key={cell.id}> .getVisibleCells()
.map((cell) => (
<TableCell
key={cell.id}
>
{flexRender( {flexRender(
cell.column.columnDef.cell, cell.column
cell.getContext() .columnDef
.cell,
cell.getContext(),
)} )}
</TableCell> </TableCell>
))} ))}
@ -532,7 +560,8 @@ export default function ReverseProxyTargets(props: {
colSpan={columns.length} colSpan={columns.length}
className="h-24 text-center" className="h-24 text-center"
> >
No targets. Add a target using the form. No targets. Add a target using
the form.
</TableCell> </TableCell>
</TableRow> </TableRow>
)} )}
@ -540,10 +569,16 @@ export default function ReverseProxyTargets(props: {
</Table> </Table>
</div> </div>
<Button onClick={saveAll} loading={loading} disabled={loading}> <Button
onClick={saveAll}
loading={loading}
disabled={loading}
>
Save Changes Save Changes
</Button> </Button>
</div> </div>
</section>
</div> </div>
</>
); );
} }

View file

@ -118,7 +118,8 @@ export default function GeneralForm() {
return ( return (
<> <>
<div className="lg:max-w-2xl space-y-6"> <div className="lg:max-w-2xl space-y-12">
<section className="space-y-6">
<SettingsSectionTitle <SettingsSectionTitle
title="General Settings" title="General Settings"
description="Configure the general settings for this resource" description="Configure the general settings for this resource"
@ -140,19 +141,14 @@ export default function GeneralForm() {
<Input {...field} /> <Input {...field} />
</FormControl> </FormControl>
<FormDescription> <FormDescription>
This is the display name of the resource This is the display name of the
resource.
</FormDescription> </FormDescription>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
)} )}
/> />
<SettingsSectionTitle
title="Domain"
description="Define the domain that users will use to access this resource"
size="1xl"
/>
<FormField <FormField
control={form.control} control={form.control}
name="subdomain" name="subdomain"
@ -172,10 +168,10 @@ export default function GeneralForm() {
} }
/> />
</FormControl> </FormControl>
{/* <FormDescription> <FormDescription>
This is the subdomain that will be used This is the subdomain that will be
to access the resource used to access the resource.
</FormDescription> */} </FormDescription>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
)} )}
@ -266,6 +262,7 @@ export default function GeneralForm() {
</Button> </Button>
</form> </form>
</Form> </Form>
</section>
</div> </div>
</> </>
); );

View file

@ -116,7 +116,7 @@ export default async function ResourceLayout(props: ResourceLayoutProps) {
sidebarNavItems={sidebarNavItems} sidebarNavItems={sidebarNavItems}
limitWidth={false} limitWidth={false}
> >
<div className="mb-8"> <div className="mb-8 lg:max-w-2xl">
<ResourceInfoBox /> <ResourceInfoBox />
</div> </div>
{children} {children}

View file

@ -206,6 +206,7 @@ sh get-docker.sh`;
<FormLabel>Name</FormLabel> <FormLabel>Name</FormLabel>
<FormControl> <FormControl>
<Input <Input
autoComplete="off"
placeholder="Site name" placeholder="Site name"
{...field} {...field}
/> />

View file

@ -2,83 +2,65 @@
@tailwind components; @tailwind components;
@tailwind utilities; @tailwind utilities;
@layer base { @layer base {
:root { :root {
--background: 0 0% 100%; --background: 0 0% 100%;
--foreground: 211.58 18.45% 20.2%; --foreground: 20 14.3% 4.1%;
--card: 0 0% 100%;
--primary: 14.59 24.83% 29.22%; --card-foreground: 20 14.3% 4.1%;
--primary-foreground: 0 0% 100%;
--card: 20 15.79% 96.27%;
--card-foreground: 0 0% 0%;
--popover: 0 0% 100%; --popover: 0 0% 100%;
--popover-foreground: 30 28.57% 2.75%; --popover-foreground: 20 14.3% 4.1%;
--primary: 24.6 95% 53.1%;
--secondary: 25 16.07% 43.92%; --primary-foreground: 60 9.1% 97.8%;
--secondary-foreground: 0 0% 100%; --secondary: 60 4.8% 95.9%;
--secondary-foreground: 24 9.8% 10%;
--muted: 20 15.79% 96.27%; --muted: 60 4.8% 95.9%;
--muted-foreground: 0 0% 34.12%; --muted-foreground: 25 5.3% 44.7%;
--accent: 60 4.8% 95.9%;
--accent: 0 0% 86.67%; --accent-foreground: 24 9.8% 10%;
--accent-foreground: 24 23.81% 4.12%;
--destructive: 0 84.2% 60.2%; --destructive: 0 84.2% 60.2%;
--destructive-foreground: 210 40% 98%; --destructive-foreground: 60 9.1% 97.8%;
--border: 20 5.9% 90%;
--border: 12 6.67% 85.29%; --input: 20 5.9% 90%;
--input: 12 6.67% 85.29%; --ring: 24.6 95% 53.1%;
--ring: 24.71 31.29% 31.96%; --radius: 0.75rem;
--chart-1: 12 76% 61%;
--chart-1: 23.64 23.74% 27.25%; --chart-2: 173 58% 39%;
--chart-2: 23.57 14.43% 38.04%; --chart-3: 197 37% 24%;
--chart-3: 22.86 8.71% 52.75%; --chart-4: 43 74% 66%;
--chart-4: 23.33 8.82% 60%; --chart-5: 27 87% 67%;
--chart-5: 24 8.98% 67.25%;
--radius: 0.35rem;
} }
.dark { .dark {
--background: 0 0% 11.76%; --background: 20 14.3% 4.1%;
--foreground: 204 6.67% 85.29%; --foreground: 60 9.1% 97.8%;
--card: 20 14.3% 4.1%;
--primary: 14.21 25.68% 29.02%; --card-foreground: 60 9.1% 97.8%;
--primary-foreground: 228 13.51% 92.75%; --popover: 20 14.3% 4.1%;
--popover-foreground: 60 9.1% 97.8%;
--card: 0 0% 9.41%; --primary: 20.5 90.2% 48.2%;
--card-foreground: 204 6.67% 85.29%; --primary-foreground: 60 9.1% 97.8%;
--secondary: 12 6.5% 15.1%;
--popover: 0 0% 11.76%; --secondary-foreground: 60 9.1% 97.8%;
--popover-foreground: 24 9.09% 89.22%; --muted: 12 6.5% 15.1%;
--muted-foreground: 24 5.4% 63.9%;
--secondary: 12.63 15.97% 23.33%; --accent: 12 6.5% 15.1%;
--secondary-foreground: 0 0% 100%; --accent-foreground: 60 9.1% 97.8%;
--destructive: 0 72.2% 50.6%;
--muted: 0 0% 9.41%; --destructive-foreground: 60 9.1% 97.8%;
--muted-foreground: 212.73 5.31% 59.41%; --border: 12 6.5% 15.1%;
--input: 12 6.5% 15.1%;
--accent: 0 2.17% 18.04%; --ring: 20.5 90.2% 48.2%;
--accent-foreground: 24 9.09% 89.22%; --chart-1: 220 70% 50%;
--chart-2: 160 60% 45%;
--destructive: 0 84.2% 60.2%; --chart-3: 30 80% 55%;
--destructive-foreground: 210 40% 98%; --chart-4: 280 65% 60%;
--chart-5: 340 75% 55%;
--border: 240 2.86% 27.45%;
--input: 240 2.86% 27.45%;
--ring: 23.64 23.74% 27.25%;
--chart-1: 23.64 23.74% 27.25%;
--chart-2: 23.57 23.73% 23.14%;
--chart-3: 24.55 24.44% 17.65%;
--chart-4: 23.33 23.68% 14.9%;
--chart-5: 24 23.81% 12.35%;
--radius: 0.35rem;
} }
} }
@layer base { @layer base {
* { * {
@apply border-border; @apply border-border;

View file

@ -1,6 +1,6 @@
import type { Metadata } from "next"; import type { Metadata } from "next";
import "./globals.css"; import "./globals.css";
import { Fira_Sans, Inter, Noto_Sans_Mono, Roboto_Mono } from "next/font/google"; import { IBM_Plex_Sans, Work_Sans } from "next/font/google";
import { Toaster } from "@/components/ui/toaster"; import { Toaster } from "@/components/ui/toaster";
import { ThemeProvider } from "@app/providers/ThemeProvider"; import { ThemeProvider } from "@app/providers/ThemeProvider";
@ -9,8 +9,11 @@ export const metadata: Metadata = {
description: "", description: "",
}; };
const font = Inter({ subsets: ["latin"] }); // const font = Inter({ subsets: ["latin"] });
// const font = Noto_Sans_Mono({ subsets: ["latin"] }); // const font = Noto_Sans_Mono({ subsets: ["latin"] });
const font = Work_Sans({ subsets: ["latin"] });
// const font = Space_Grotesk({subsets: ["latin"]})
// const font = IBM_Plex_Sans({subsets: ["latin"], weight: "400"})
export default async function RootLayout({ export default async function RootLayout({
children, children,

View file

@ -10,7 +10,9 @@ export default function SettingsSectionTitle({
size, size,
}: SettingsSectionTitleProps) { }: SettingsSectionTitleProps) {
return ( return (
<div className="space-y-0.5 select-none mb-6"> <div
className={`space-y-0.5 select-none ${!size || size === "2xl" ? "mb-12" : ""}`}
>
<h2 <h2
className={`text-${ className={`text-${
size ? size : "2xl" size ? size : "2xl"

View file

@ -81,7 +81,7 @@ export function SidebarNav({
</div> </div>
<nav <nav
className={cn( className={cn(
"hidden lg:flex space-x-2 lg:flex-col lg:space-x-0 lg:space-y-1", "hidden lg:flex space-x-2 lg:flex-col lg:space-x-0 lg:space-y-3",
disabled && "opacity-50 pointer-events-none", disabled && "opacity-50 pointer-events-none",
className className
)} )}

View file

@ -4,7 +4,7 @@ import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
const alertVariants = cva( const alertVariants = cva(
"relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground", "relative w-full rounded-lg p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground",
{ {
variants: { variants: {
variant: { variant: {

View file

@ -23,10 +23,10 @@ const buttonVariants = cva(
link: "text-primary underline-offset-4 hover:underline", link: "text-primary underline-offset-4 hover:underline",
}, },
size: { size: {
default: "h-10 px-4 py-2", default: "h-9 px-4 py-2",
sm: "h-9 rounded-md px-3", sm: "h-8 rounded-md px-3",
lg: "h-11 rounded-md px-8", lg: "h-10 rounded-md px-8",
icon: "h-10 w-10", icon: "h-9 w-9",
}, },
}, },
defaultVariants: { defaultVariants: {

View file

@ -10,7 +10,7 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
<input <input
type={type} type={type}
className={cn( className={cn(
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50", "flex h-9 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className className
)} )}
ref={ref} ref={ref}

View file

@ -19,7 +19,7 @@ const SelectTrigger = React.forwardRef<
<SelectPrimitive.Trigger <SelectPrimitive.Trigger
ref={ref} ref={ref}
className={cn( className={cn(
"flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1", "flex h-9 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
className className
)} )}
{...props} {...props}