minor visual improvements

This commit is contained in:
Milo Schwartz 2025-01-03 22:32:24 -05:00
parent c857a9bd76
commit 0e99e2b62b
No known key found for this signature in database
17 changed files with 285 additions and 270 deletions

View file

@ -4,7 +4,7 @@ Pangolin is a self-hosted tunneled reverse proxy management server with identity
### Installation and Documentation
- [Installation Instructions](https://docs.fossorial.io)
- [Installation Instructions](https://docs.fossorial.io/Getting%20Started/quick-install)
- [Full Documentation](https://docs.fossorial.io)
## Preview
@ -112,7 +112,7 @@ Pangolin was inspired by several existing projects and concepts:
- **Cloudflare Tunnels**:
A similar approach to proxying private resources securely, but Pangolin is a self-hosted alternative, giving you full control over your infrastructure.
- **Authentic and Authelia**:
- **Authentik and Authelia**:
These projects inspired Pangolins centralized authentication system for proxies, enabling robust user and role management.
## Licensing

View file

@ -1,6 +1,6 @@
app:
base_url: https://fossorial.io
log_level: debug
base_url: https://proxy.example.com
log_level: info
save_logs: false
server:
@ -20,7 +20,7 @@ traefik:
gerbil:
start_port: 51820
base_endpoint: fossorial.io
base_endpoint: proxy.example.com
use_subdomain: false
block_size: 16
subnet_group: 10.0.0.0/8
@ -33,9 +33,9 @@ rate_limits:
email:
smtp_host: host.hoster.net
smtp_port: 587
smtp_user: no-reply@example.io
smtp_user: no-reply@example.com
smtp_pass: aaaaaaaaaaaaaaaaaa
no_reply: no-reply@example.io
no_reply: no-reply@example.com
users:
server_admin:
@ -46,3 +46,4 @@ flags:
require_email_verification: true
disable_signup_without_invite: true
disable_user_create_org: true

View file

@ -5,6 +5,7 @@ import { z } from "zod";
import { fromError } from "zod-validation-error";
import { __DIRNAME, APP_PATH } from "@server/lib/consts";
import { loadAppVersion } from "@server/lib/loadAppVersion";
import { passwordSchema } from "@server/auth/passwordSchema";
const portSchema = z.number().positive().gt(0).lte(65535);
@ -53,17 +54,17 @@ const environmentSchema = z.object({
}),
email: z
.object({
smtp_host: z.string().optional(),
smtp_port: portSchema.optional(),
smtp_user: z.string().optional(),
smtp_pass: z.string().optional(),
no_reply: z.string().email().optional()
smtp_host: z.string(),
smtp_port: portSchema,
smtp_user: z.string(),
smtp_pass: z.string(),
no_reply: z.string().email(),
})
.optional(),
users: z.object({
server_admin: z.object({
email: z.string().email(),
password: z.string()
password: passwordSchema
})
}),
flags: z

View file

@ -158,7 +158,7 @@ export default function DeleteRoleForm({
</CredenzaDescription>
</CredenzaHeader>
<CredenzaBody>
<div className="space-y-8">
<div className="space-y-4">
<div className="space-y-4">
<p>
You're about to delete the{" "}

View file

@ -180,7 +180,7 @@ export default function InviteUserForm({ open, setOpen }: InviteUserFormProps) {
</CredenzaDescription>
</CredenzaHeader>
<CredenzaBody>
<div className="space-y-8">
<div className="space-y-4">
{!inviteLink && (
<Form {...form}>
<form

View file

@ -6,7 +6,7 @@ import { useToast } from "@app/hooks/useToast";
import { useOrgContext } from "@app/hooks/useOrgContext";
import { useResourceContext } from "@app/hooks/useResourceContext";
import { AxiosResponse } from "axios";
import { formatAxiosError } from "@app/lib/api";;
import { formatAxiosError } from "@app/lib/api";
import {
GetResourceAuthInfoResponse,
GetResourceWhitelistResponse,
@ -383,7 +383,7 @@ export default function ResourceAuthenticationPage() {
)}
<div className="space-y-12">
<section className="space-y-8 lg:max-w-2xl">
<section className="space-y-4 lg:max-w-2xl">
<SettingsSectionTitle
title="Users & Roles"
description="Configure which users and roles can visit this resource"
@ -397,9 +397,7 @@ export default function ResourceAuthenticationPage() {
defaultChecked={resource.sso}
onCheckedChange={(val) => setSsoEnabled(val)}
/>
<Label htmlFor="sso-toggle">
Use Platform SSO
</Label>
<Label htmlFor="sso-toggle">Use Platform SSO</Label>
</div>
<span className="text-muted-foreground text-sm">
Existing users will only have to login once for all
@ -414,6 +412,8 @@ export default function ResourceAuthenticationPage() {
)}
className="space-y-4"
>
{ssoEnabled && (
<>
<FormField
control={usersRolesForm.control}
name="roles"
@ -444,8 +444,12 @@ export default function ResourceAuthenticationPage() {
]
);
}}
enableAutocomplete={true}
autocompleteOptions={allRoles}
enableAutocomplete={
true
}
autocompleteOptions={
allRoles
}
allowDuplicates={false}
restrictTagsToAutocompleteOptions={
true
@ -462,9 +466,10 @@ export default function ResourceAuthenticationPage() {
/>
</FormControl>
<FormDescription>
These roles will be able to access
this resource. Admins can always
access this resource.
These roles will be able to
access this resource. Admins
can always access this
resource.
</FormDescription>
<FormMessage />
</FormItem>
@ -500,8 +505,12 @@ export default function ResourceAuthenticationPage() {
]
);
}}
enableAutocomplete={true}
autocompleteOptions={allUsers}
enableAutocomplete={
true
}
autocompleteOptions={
allUsers
}
allowDuplicates={false}
restrictTagsToAutocompleteOptions={
true
@ -518,16 +527,19 @@ export default function ResourceAuthenticationPage() {
/>
</FormControl>
<FormDescription>
Users added here will be able to
access this resource. A user will
always have access to a resource if
they have a role that has access to
it.
Users added here will be
able to access this
resource. A user will always
have access to a resource if
they have a role that has
access to it.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</>
)}
<Button
type="submit"
loading={loadingSaveUsersRoles}
@ -541,7 +553,7 @@ export default function ResourceAuthenticationPage() {
<Separator />
<section className="space-y-8 lg:max-w-2xl">
<section className="space-y-4 lg:max-w-2xl">
<SettingsSectionTitle
title="Authentication Methods"
description="Allow access to the resource via additional auth methods"
@ -617,7 +629,7 @@ export default function ResourceAuthenticationPage() {
<Separator />
<section className="space-y-8 lg:max-w-2xl">
<section className="space-y-4 lg:max-w-2xl">
{env.EMAIL_ENABLED === "true" && (
<>
<div>
@ -636,9 +648,9 @@ export default function ResourceAuthenticationPage() {
</Label>
</div>
<span className="text-muted-foreground text-sm">
Enable resource whitelist to require email-based
authentication (one-time passwords) for resource
access.
Enable resource whitelist to require
email-based authentication (one-time
passwords) for resource access.
</span>
</div>
@ -660,12 +672,15 @@ export default function ResourceAuthenticationPage() {
activeTagIndex={
activeEmailTagIndex
}
validateTag={(tag) => {
validateTag={(
tag
) => {
return z
.string()
.email()
.safeParse(tag)
.success;
.safeParse(
tag
).success;
}}
setActiveTagIndex={
setActiveEmailTagIndex
@ -675,7 +690,9 @@ export default function ResourceAuthenticationPage() {
whitelistForm.getValues()
.emails
}
setTags={(newRoles) => {
setTags={(
newRoles
) => {
whitelistForm.setValue(
"emails",
newRoles as [
@ -684,7 +701,9 @@ export default function ResourceAuthenticationPage() {
]
);
}}
allowDuplicates={false}
allowDuplicates={
false
}
sortTags={true}
styleClasses={{
tag: {

View file

@ -304,8 +304,8 @@ export default function ReverseProxyTargets(props: {
{row.original.method}
</SelectTrigger>
<SelectContent>
<SelectItem value="http">HTTP</SelectItem>
<SelectItem value="https">HTTPS</SelectItem>
<SelectItem value="http">http</SelectItem>
<SelectItem value="https">https</SelectItem>
</SelectContent>
</Select>
)
@ -412,7 +412,7 @@ export default function ReverseProxyTargets(props: {
return (
<>
<div className="space-y-12">
<section className="space-y-8">
<section className="space-y-4">
<SettingsSectionTitle
title="SSL"
description="Setup SSL to secure your connections with LetsEncrypt certificates"
@ -431,14 +431,14 @@ export default function ReverseProxyTargets(props: {
<hr />
<section className="space-y-8">
<section className="space-y-4">
<SettingsSectionTitle
title="Targets"
description="Setup targets to route traffic to your services"
size="1xl"
/>
<div className="space-y-8">
<div className="space-y-4">
<Form {...addTargetForm}>
<form
onSubmit={addTargetForm.handleSubmit(
@ -470,18 +470,18 @@ export default function ReverseProxyTargets(props: {
</SelectTrigger>
<SelectContent>
<SelectItem value="http">
HTTP
http
</SelectItem>
<SelectItem value="https">
HTTPS
https
</SelectItem>
</SelectContent>
</Select>
</FormControl>
<FormDescription>
Choose the method for how
the target is accessed.
</FormDescription>
{/* <FormDescription> */}
{/* Choose the method for how */}
{/* the target is accessed. */}
{/* </FormDescription> */}
<FormMessage />
</FormItem>
)}
@ -497,10 +497,9 @@ export default function ReverseProxyTargets(props: {
<FormControl>
<Input id="ip" {...field} />
</FormControl>
<FormDescription>
Enter the IP address of the
target.
</FormDescription>
{/* <FormDescription> */}
{/* Use the IP of the resource on your private network if using Newt, or the peer IP if using raw WireGuard. */}
{/* </FormDescription> */}
<FormMessage />
</FormItem>
)}
@ -519,10 +518,10 @@ export default function ReverseProxyTargets(props: {
required
/>
</FormControl>
<FormDescription>
Specify the port number for
the target.
</FormDescription>
{/* <FormDescription> */}
{/* Specify the port number for */}
{/* the target. */}
{/* </FormDescription> */}
<FormMessage />
</FormItem>
)}

View file

@ -125,7 +125,7 @@ export default function GeneralForm() {
return (
<>
<div className="space-y-12 lg:max-w-2xl">
<section className="space-y-8">
<section className="space-y-4">
<SettingsSectionTitle
title="General Settings"
description="Configure the general settings for this resource"

View file

@ -199,7 +199,7 @@ PersistentKeepalive = 5`
const newtConfig = `newt --id ${siteDefaults?.newtId} --secret ${siteDefaults?.newtSecret} --endpoint ${proto}//${siteDefaults?.endpoint}`;
return (
<div className="space-y-8">
<div className="space-y-4">
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}

View file

@ -67,7 +67,7 @@ export default function GeneralPage() {
return (
<>
<div className="space-y-8 max-w-xl">
<div className="space-y-4 max-w-xl">
<SettingsSectionTitle
title="General Settings"
description="Configure the general settings for this site"

View file

@ -37,7 +37,7 @@ export default function DashboardLoginForm({
return (
<Card className="w-full max-w-md">
<CardHeader>
<CardTitle>Login</CardTitle>
<CardTitle>Welcome to Pangolin</CardTitle>
<CardDescription>
Enter your credentials to access your dashboard
</CardDescription>

View file

@ -401,7 +401,7 @@ export default function ResourceAuthPortal(props: ResourceAuthPortalProps) {
disabled={loadingLogin}
>
<LockIcon className="w-4 h-4 mr-2" />
Login with PIN
Log in with PIN
</Button>
</form>
</Form>
@ -456,7 +456,7 @@ export default function ResourceAuthPortal(props: ResourceAuthPortalProps) {
disabled={loadingLogin}
>
<LockIcon className="w-4 h-4 mr-2" />
Login with Password
Log In with Password
</Button>
</form>
</Form>

View file

@ -91,7 +91,7 @@ export default function InviteStatusCard({
);
} else if (type === "wrong_user") {
return (
<Button onClick={goToLogin}>Login in as different user</Button>
<Button onClick={goToLogin}>Log in as different user</Button>
);
} else if (type === "user_does_not_exist") {
return <Button onClick={goToSignup}>Create an account</Button>;

View file

@ -5,10 +5,6 @@ import { Toaster } from "@/components/ui/toaster";
import { ThemeProvider } from "@app/providers/ThemeProvider";
import EnvProvider from "@app/providers/EnvProvider";
import { Separator } from "@app/components/ui/separator";
import { cache } from "react";
import { verifySession } from "@app/lib/auth/verifySession";
import Header from "@app/components/Header";
import UserProvider from "@app/providers/UserProvider";
export const metadata: Metadata = {
title: `Dashboard - Pangolin`,
@ -26,7 +22,7 @@ export default async function RootLayout({
return (
<html suppressHydrationWarning>
<body className={`${font.className}`}>
<body className={`${font.className} min-h-screen flex flex-col`}>
<ThemeProvider
attribute="class"
defaultTheme="system"
@ -34,23 +30,22 @@ export default async function RootLayout({
disableTransitionOnChange
>
<EnvProvider
// it's import not to pass all of process.env here in case of secrets
// select only the necessary ones
env={{
NEXT_PORT: process.env.NEXT_PORT as string,
SERVER_EXTERNAL_PORT: process.env
.SERVER_EXTERNAL_PORT as string,
ENVIRONMENT: process.env.ENVIRONMENT as string,
EMAIL_ENABLED: process.env.EMAIL_ENABLED as string,
// optional
DISABLE_USER_CREATE_ORG:
process.env.DISABLE_USER_CREATE_ORG,
DISABLE_SIGNUP_WITHOUT_INVITE:
process.env.DISABLE_SIGNUP_WITHOUT_INVITE
}}
>
{children}
{/* Main content */}
<div className="flex-grow">{children}</div>
{/* Footer */}
<footer className="w-full mt-12 py-3 mb-4">
<div className="container mx-auto flex flex-wrap justify-center items-center h-4 space-x-4 text-sm text-neutral-400 select-none">
<div className="whitespace-nowrap">

View file

@ -130,7 +130,7 @@ export default function LoginForm({ redirect, onLogin }: LoginFormProps) {
}
return (
<div className="space-y-8">
<div className="space-y-4">
{!mfaRequested && (
<Form {...form}>
<form
@ -179,7 +179,7 @@ export default function LoginForm({ redirect, onLogin }: LoginFormProps) {
href={`/auth/reset-password${form.getValues().email ? `?email=${form.getValues().email}` : ""}`}
className="text-sm text-muted-foreground"
>
Forgot password?
Forgot your password?
</Link>
</div>
</div>
@ -279,7 +279,7 @@ export default function LoginForm({ redirect, onLogin }: LoginFormProps) {
disabled={loading}
>
<LockIcon className="w-4 h-4 mr-2" />
Login
Log In
</Button>
)}
@ -293,7 +293,7 @@ export default function LoginForm({ redirect, onLogin }: LoginFormProps) {
mfaForm.reset();
}}
>
Back to Login
Back to Log In
</Button>
)}
</div>

View file

@ -10,9 +10,9 @@ const alertVariants = cva(
variant: {
default: "bg-background text-foreground",
destructive:
"border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
"border-destructive/50 bg-destructive/10 text-destructive dark:border-destructive [&>svg]:text-destructive",
success:
"border-green-500/50 text-green-500 dark:border-success [&>svg]:text-green-500",
"border-green-500/50 bg-green-500/10 text-green-500 dark:border-success [&>svg]:text-green-500",
},
},
defaultVariants: {