mirror of
https://github.com/fosrl/pangolin.git
synced 2025-07-25 21:25:06 +02:00
minor visual improvements
This commit is contained in:
parent
c857a9bd76
commit
0e99e2b62b
17 changed files with 285 additions and 270 deletions
|
@ -4,7 +4,7 @@ Pangolin is a self-hosted tunneled reverse proxy management server with identity
|
||||||
|
|
||||||
### Installation and Documentation
|
### 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)
|
- [Full Documentation](https://docs.fossorial.io)
|
||||||
|
|
||||||
## Preview
|
## Preview
|
||||||
|
@ -112,7 +112,7 @@ Pangolin was inspired by several existing projects and concepts:
|
||||||
- **Cloudflare Tunnels**:
|
- **Cloudflare Tunnels**:
|
||||||
A similar approach to proxying private resources securely, but Pangolin is a self-hosted alternative, giving you full control over your infrastructure.
|
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 Pangolin’s centralized authentication system for proxies, enabling robust user and role management.
|
These projects inspired Pangolin’s centralized authentication system for proxies, enabling robust user and role management.
|
||||||
|
|
||||||
## Licensing
|
## Licensing
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
app:
|
app:
|
||||||
base_url: https://fossorial.io
|
base_url: https://proxy.example.com
|
||||||
log_level: debug
|
log_level: info
|
||||||
save_logs: false
|
save_logs: false
|
||||||
|
|
||||||
server:
|
server:
|
||||||
|
@ -20,7 +20,7 @@ traefik:
|
||||||
|
|
||||||
gerbil:
|
gerbil:
|
||||||
start_port: 51820
|
start_port: 51820
|
||||||
base_endpoint: fossorial.io
|
base_endpoint: proxy.example.com
|
||||||
use_subdomain: false
|
use_subdomain: false
|
||||||
block_size: 16
|
block_size: 16
|
||||||
subnet_group: 10.0.0.0/8
|
subnet_group: 10.0.0.0/8
|
||||||
|
@ -33,9 +33,9 @@ rate_limits:
|
||||||
email:
|
email:
|
||||||
smtp_host: host.hoster.net
|
smtp_host: host.hoster.net
|
||||||
smtp_port: 587
|
smtp_port: 587
|
||||||
smtp_user: no-reply@example.io
|
smtp_user: no-reply@example.com
|
||||||
smtp_pass: aaaaaaaaaaaaaaaaaa
|
smtp_pass: aaaaaaaaaaaaaaaaaa
|
||||||
no_reply: no-reply@example.io
|
no_reply: no-reply@example.com
|
||||||
|
|
||||||
users:
|
users:
|
||||||
server_admin:
|
server_admin:
|
||||||
|
@ -46,3 +46,4 @@ flags:
|
||||||
require_email_verification: true
|
require_email_verification: true
|
||||||
disable_signup_without_invite: true
|
disable_signup_without_invite: true
|
||||||
disable_user_create_org: true
|
disable_user_create_org: true
|
||||||
|
|
||||||
|
|
|
@ -5,6 +5,7 @@ import { z } from "zod";
|
||||||
import { fromError } from "zod-validation-error";
|
import { fromError } from "zod-validation-error";
|
||||||
import { __DIRNAME, APP_PATH } from "@server/lib/consts";
|
import { __DIRNAME, APP_PATH } from "@server/lib/consts";
|
||||||
import { loadAppVersion } from "@server/lib/loadAppVersion";
|
import { loadAppVersion } from "@server/lib/loadAppVersion";
|
||||||
|
import { passwordSchema } from "@server/auth/passwordSchema";
|
||||||
|
|
||||||
const portSchema = z.number().positive().gt(0).lte(65535);
|
const portSchema = z.number().positive().gt(0).lte(65535);
|
||||||
|
|
||||||
|
@ -53,17 +54,17 @@ const environmentSchema = z.object({
|
||||||
}),
|
}),
|
||||||
email: z
|
email: z
|
||||||
.object({
|
.object({
|
||||||
smtp_host: z.string().optional(),
|
smtp_host: z.string(),
|
||||||
smtp_port: portSchema.optional(),
|
smtp_port: portSchema,
|
||||||
smtp_user: z.string().optional(),
|
smtp_user: z.string(),
|
||||||
smtp_pass: z.string().optional(),
|
smtp_pass: z.string(),
|
||||||
no_reply: z.string().email().optional()
|
no_reply: z.string().email(),
|
||||||
})
|
})
|
||||||
.optional(),
|
.optional(),
|
||||||
users: z.object({
|
users: z.object({
|
||||||
server_admin: z.object({
|
server_admin: z.object({
|
||||||
email: z.string().email(),
|
email: z.string().email(),
|
||||||
password: z.string()
|
password: passwordSchema
|
||||||
})
|
})
|
||||||
}),
|
}),
|
||||||
flags: z
|
flags: z
|
||||||
|
|
|
@ -158,7 +158,7 @@ export default function DeleteRoleForm({
|
||||||
</CredenzaDescription>
|
</CredenzaDescription>
|
||||||
</CredenzaHeader>
|
</CredenzaHeader>
|
||||||
<CredenzaBody>
|
<CredenzaBody>
|
||||||
<div className="space-y-8">
|
<div className="space-y-4">
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<p>
|
<p>
|
||||||
You're about to delete the{" "}
|
You're about to delete the{" "}
|
||||||
|
|
|
@ -180,7 +180,7 @@ export default function InviteUserForm({ open, setOpen }: InviteUserFormProps) {
|
||||||
</CredenzaDescription>
|
</CredenzaDescription>
|
||||||
</CredenzaHeader>
|
</CredenzaHeader>
|
||||||
<CredenzaBody>
|
<CredenzaBody>
|
||||||
<div className="space-y-8">
|
<div className="space-y-4">
|
||||||
{!inviteLink && (
|
{!inviteLink && (
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form
|
<form
|
||||||
|
|
|
@ -84,7 +84,7 @@ export default function GeneralPage() {
|
||||||
const res = await api.get<AxiosResponse<ListOrgsResponse>>(
|
const res = await api.get<AxiosResponse<ListOrgsResponse>>(
|
||||||
`/orgs`
|
`/orgs`
|
||||||
);
|
);
|
||||||
|
|
||||||
if (res.status === 200) {
|
if (res.status === 200) {
|
||||||
if (res.data.data.orgs.length > 0) {
|
if (res.data.data.orgs.length > 0) {
|
||||||
const orgId = res.data.data.orgs[0].orgId;
|
const orgId = res.data.data.orgs[0].orgId;
|
||||||
|
|
|
@ -6,7 +6,7 @@ import { useToast } from "@app/hooks/useToast";
|
||||||
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 { AxiosResponse } from "axios";
|
import { AxiosResponse } from "axios";
|
||||||
import { formatAxiosError } from "@app/lib/api";;
|
import { formatAxiosError } from "@app/lib/api";
|
||||||
import {
|
import {
|
||||||
GetResourceAuthInfoResponse,
|
GetResourceAuthInfoResponse,
|
||||||
GetResourceWhitelistResponse,
|
GetResourceWhitelistResponse,
|
||||||
|
@ -383,7 +383,7 @@ export default function ResourceAuthenticationPage() {
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="space-y-12">
|
<div className="space-y-12">
|
||||||
<section className="space-y-8 lg:max-w-2xl">
|
<section className="space-y-4 lg:max-w-2xl">
|
||||||
<SettingsSectionTitle
|
<SettingsSectionTitle
|
||||||
title="Users & Roles"
|
title="Users & Roles"
|
||||||
description="Configure which users and roles can visit this resource"
|
description="Configure which users and roles can visit this resource"
|
||||||
|
@ -397,9 +397,7 @@ export default function ResourceAuthenticationPage() {
|
||||||
defaultChecked={resource.sso}
|
defaultChecked={resource.sso}
|
||||||
onCheckedChange={(val) => setSsoEnabled(val)}
|
onCheckedChange={(val) => setSsoEnabled(val)}
|
||||||
/>
|
/>
|
||||||
<Label htmlFor="sso-toggle">
|
<Label htmlFor="sso-toggle">Use Platform SSO</Label>
|
||||||
Use Platform SSO
|
|
||||||
</Label>
|
|
||||||
</div>
|
</div>
|
||||||
<span className="text-muted-foreground text-sm">
|
<span className="text-muted-foreground text-sm">
|
||||||
Existing users will only have to login once for all
|
Existing users will only have to login once for all
|
||||||
|
@ -414,120 +412,134 @@ export default function ResourceAuthenticationPage() {
|
||||||
)}
|
)}
|
||||||
className="space-y-4"
|
className="space-y-4"
|
||||||
>
|
>
|
||||||
<FormField
|
{ssoEnabled && (
|
||||||
control={usersRolesForm.control}
|
<>
|
||||||
name="roles"
|
<FormField
|
||||||
render={({ field }) => (
|
control={usersRolesForm.control}
|
||||||
<FormItem className="flex flex-col items-start">
|
name="roles"
|
||||||
<FormLabel>Roles</FormLabel>
|
render={({ field }) => (
|
||||||
<FormControl>
|
<FormItem className="flex flex-col items-start">
|
||||||
{/* @ts-ignore */}
|
<FormLabel>Roles</FormLabel>
|
||||||
<TagInput
|
<FormControl>
|
||||||
{...field}
|
{/* @ts-ignore */}
|
||||||
activeTagIndex={
|
<TagInput
|
||||||
activeRolesTagIndex
|
{...field}
|
||||||
}
|
activeTagIndex={
|
||||||
setActiveTagIndex={
|
activeRolesTagIndex
|
||||||
setActiveRolesTagIndex
|
}
|
||||||
}
|
setActiveTagIndex={
|
||||||
placeholder="Enter a role"
|
setActiveRolesTagIndex
|
||||||
tags={
|
}
|
||||||
usersRolesForm.getValues()
|
placeholder="Enter a role"
|
||||||
.roles
|
tags={
|
||||||
}
|
usersRolesForm.getValues()
|
||||||
setTags={(newRoles) => {
|
.roles
|
||||||
usersRolesForm.setValue(
|
}
|
||||||
"roles",
|
setTags={(newRoles) => {
|
||||||
newRoles as [
|
usersRolesForm.setValue(
|
||||||
Tag,
|
"roles",
|
||||||
...Tag[]
|
newRoles as [
|
||||||
]
|
Tag,
|
||||||
);
|
...Tag[]
|
||||||
}}
|
]
|
||||||
enableAutocomplete={true}
|
);
|
||||||
autocompleteOptions={allRoles}
|
}}
|
||||||
allowDuplicates={false}
|
enableAutocomplete={
|
||||||
restrictTagsToAutocompleteOptions={
|
true
|
||||||
true
|
}
|
||||||
}
|
autocompleteOptions={
|
||||||
sortTags={true}
|
allRoles
|
||||||
styleClasses={{
|
}
|
||||||
tag: {
|
allowDuplicates={false}
|
||||||
body: "bg-muted hover:bg-accent text-foreground py-2 px-3 rounded-full"
|
restrictTagsToAutocompleteOptions={
|
||||||
},
|
true
|
||||||
input: "text-base md:text-sm border-none bg-transparent text-inherit placeholder:text-inherit shadow-none",
|
}
|
||||||
inlineTagsContainer:
|
sortTags={true}
|
||||||
"bg-transparent p-2"
|
styleClasses={{
|
||||||
}}
|
tag: {
|
||||||
/>
|
body: "bg-muted hover:bg-accent text-foreground py-2 px-3 rounded-full"
|
||||||
</FormControl>
|
},
|
||||||
<FormDescription>
|
input: "text-base md:text-sm border-none bg-transparent text-inherit placeholder:text-inherit shadow-none",
|
||||||
These roles will be able to access
|
inlineTagsContainer:
|
||||||
this resource. Admins can always
|
"bg-transparent p-2"
|
||||||
access this resource.
|
}}
|
||||||
</FormDescription>
|
/>
|
||||||
<FormMessage />
|
</FormControl>
|
||||||
</FormItem>
|
<FormDescription>
|
||||||
)}
|
These roles will be able to
|
||||||
/>
|
access this resource. Admins
|
||||||
<FormField
|
can always access this
|
||||||
control={usersRolesForm.control}
|
resource.
|
||||||
name="users"
|
</FormDescription>
|
||||||
render={({ field }) => (
|
<FormMessage />
|
||||||
<FormItem className="flex flex-col items-start">
|
</FormItem>
|
||||||
<FormLabel>Users</FormLabel>
|
)}
|
||||||
<FormControl>
|
/>
|
||||||
{/* @ts-ignore */}
|
<FormField
|
||||||
<TagInput
|
control={usersRolesForm.control}
|
||||||
{...field}
|
name="users"
|
||||||
activeTagIndex={
|
render={({ field }) => (
|
||||||
activeUsersTagIndex
|
<FormItem className="flex flex-col items-start">
|
||||||
}
|
<FormLabel>Users</FormLabel>
|
||||||
setActiveTagIndex={
|
<FormControl>
|
||||||
setActiveUsersTagIndex
|
{/* @ts-ignore */}
|
||||||
}
|
<TagInput
|
||||||
placeholder="Enter a user"
|
{...field}
|
||||||
tags={
|
activeTagIndex={
|
||||||
usersRolesForm.getValues()
|
activeUsersTagIndex
|
||||||
.users
|
}
|
||||||
}
|
setActiveTagIndex={
|
||||||
setTags={(newUsers) => {
|
setActiveUsersTagIndex
|
||||||
usersRolesForm.setValue(
|
}
|
||||||
"users",
|
placeholder="Enter a user"
|
||||||
newUsers as [
|
tags={
|
||||||
Tag,
|
usersRolesForm.getValues()
|
||||||
...Tag[]
|
.users
|
||||||
]
|
}
|
||||||
);
|
setTags={(newUsers) => {
|
||||||
}}
|
usersRolesForm.setValue(
|
||||||
enableAutocomplete={true}
|
"users",
|
||||||
autocompleteOptions={allUsers}
|
newUsers as [
|
||||||
allowDuplicates={false}
|
Tag,
|
||||||
restrictTagsToAutocompleteOptions={
|
...Tag[]
|
||||||
true
|
]
|
||||||
}
|
);
|
||||||
sortTags={true}
|
}}
|
||||||
styleClasses={{
|
enableAutocomplete={
|
||||||
tag: {
|
true
|
||||||
body: "bg-muted hover:bg-accent text-foreground py-2 px-3 rounded-full"
|
}
|
||||||
},
|
autocompleteOptions={
|
||||||
input: "text-base md:text-sm border-none bg-transparent text-inherit placeholder:text-inherit shadow-none",
|
allUsers
|
||||||
inlineTagsContainer:
|
}
|
||||||
"bg-transparent p-2"
|
allowDuplicates={false}
|
||||||
}}
|
restrictTagsToAutocompleteOptions={
|
||||||
/>
|
true
|
||||||
</FormControl>
|
}
|
||||||
<FormDescription>
|
sortTags={true}
|
||||||
Users added here will be able to
|
styleClasses={{
|
||||||
access this resource. A user will
|
tag: {
|
||||||
always have access to a resource if
|
body: "bg-muted hover:bg-accent text-foreground py-2 px-3 rounded-full"
|
||||||
they have a role that has access to
|
},
|
||||||
it.
|
input: "text-base md:text-sm border-none bg-transparent text-inherit placeholder:text-inherit shadow-none",
|
||||||
</FormDescription>
|
inlineTagsContainer:
|
||||||
<FormMessage />
|
"bg-transparent p-2"
|
||||||
</FormItem>
|
}}
|
||||||
)}
|
/>
|
||||||
/>
|
</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.
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
loading={loadingSaveUsersRoles}
|
loading={loadingSaveUsersRoles}
|
||||||
|
@ -541,7 +553,7 @@ export default function ResourceAuthenticationPage() {
|
||||||
|
|
||||||
<Separator />
|
<Separator />
|
||||||
|
|
||||||
<section className="space-y-8 lg:max-w-2xl">
|
<section className="space-y-4 lg:max-w-2xl">
|
||||||
<SettingsSectionTitle
|
<SettingsSectionTitle
|
||||||
title="Authentication Methods"
|
title="Authentication Methods"
|
||||||
description="Allow access to the resource via additional auth methods"
|
description="Allow access to the resource via additional auth methods"
|
||||||
|
@ -617,101 +629,108 @@ export default function ResourceAuthenticationPage() {
|
||||||
|
|
||||||
<Separator />
|
<Separator />
|
||||||
|
|
||||||
<section className="space-y-8 lg:max-w-2xl">
|
<section className="space-y-4 lg:max-w-2xl">
|
||||||
{env.EMAIL_ENABLED === "true" && (
|
{env.EMAIL_ENABLED === "true" && (
|
||||||
<>
|
<>
|
||||||
<div>
|
<div>
|
||||||
<div className="flex items-center space-x-2 mb-2">
|
<div className="flex items-center space-x-2 mb-2">
|
||||||
<Switch
|
<Switch
|
||||||
id="whitelist-toggle"
|
id="whitelist-toggle"
|
||||||
defaultChecked={
|
defaultChecked={
|
||||||
resource.emailWhitelistEnabled
|
resource.emailWhitelistEnabled
|
||||||
}
|
}
|
||||||
onCheckedChange={(val) =>
|
onCheckedChange={(val) =>
|
||||||
setWhitelistEnabled(val)
|
setWhitelistEnabled(val)
|
||||||
}
|
}
|
||||||
/>
|
|
||||||
<Label htmlFor="whitelist-toggle">
|
|
||||||
Email Whitelist
|
|
||||||
</Label>
|
|
||||||
</div>
|
|
||||||
<span className="text-muted-foreground text-sm">
|
|
||||||
Enable resource whitelist to require email-based
|
|
||||||
authentication (one-time passwords) for resource
|
|
||||||
access.
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{whitelistEnabled && (
|
|
||||||
<Form {...whitelistForm}>
|
|
||||||
<form className="space-y-4">
|
|
||||||
<FormField
|
|
||||||
control={whitelistForm.control}
|
|
||||||
name="emails"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem className="flex flex-col items-start">
|
|
||||||
<FormLabel>
|
|
||||||
Whitelisted Emails
|
|
||||||
</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
{/* @ts-ignore */}
|
|
||||||
<TagInput
|
|
||||||
{...field}
|
|
||||||
activeTagIndex={
|
|
||||||
activeEmailTagIndex
|
|
||||||
}
|
|
||||||
validateTag={(tag) => {
|
|
||||||
return z
|
|
||||||
.string()
|
|
||||||
.email()
|
|
||||||
.safeParse(tag)
|
|
||||||
.success;
|
|
||||||
}}
|
|
||||||
setActiveTagIndex={
|
|
||||||
setActiveEmailTagIndex
|
|
||||||
}
|
|
||||||
placeholder="Enter an email"
|
|
||||||
tags={
|
|
||||||
whitelistForm.getValues()
|
|
||||||
.emails
|
|
||||||
}
|
|
||||||
setTags={(newRoles) => {
|
|
||||||
whitelistForm.setValue(
|
|
||||||
"emails",
|
|
||||||
newRoles as [
|
|
||||||
Tag,
|
|
||||||
...Tag[]
|
|
||||||
]
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
allowDuplicates={false}
|
|
||||||
sortTags={true}
|
|
||||||
styleClasses={{
|
|
||||||
tag: {
|
|
||||||
body: "bg-muted hover:bg-accent text-foreground py-2 px-3 rounded-full"
|
|
||||||
},
|
|
||||||
input: "text-base md:text-sm border-none bg-transparent text-inherit placeholder:text-inherit shadow-none",
|
|
||||||
inlineTagsContainer:
|
|
||||||
"bg-transparent p-2"
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
/>
|
||||||
</form>
|
<Label htmlFor="whitelist-toggle">
|
||||||
</Form>
|
Email Whitelist
|
||||||
)}
|
</Label>
|
||||||
|
</div>
|
||||||
|
<span className="text-muted-foreground text-sm">
|
||||||
|
Enable resource whitelist to require
|
||||||
|
email-based authentication (one-time
|
||||||
|
passwords) for resource access.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<Button
|
{whitelistEnabled && (
|
||||||
loading={loadingSaveWhitelist}
|
<Form {...whitelistForm}>
|
||||||
disabled={loadingSaveWhitelist}
|
<form className="space-y-4">
|
||||||
onClick={saveWhitelist}
|
<FormField
|
||||||
>
|
control={whitelistForm.control}
|
||||||
Save Whitelist
|
name="emails"
|
||||||
</Button>
|
render={({ field }) => (
|
||||||
</>
|
<FormItem className="flex flex-col items-start">
|
||||||
)}
|
<FormLabel>
|
||||||
|
Whitelisted Emails
|
||||||
|
</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
{/* @ts-ignore */}
|
||||||
|
<TagInput
|
||||||
|
{...field}
|
||||||
|
activeTagIndex={
|
||||||
|
activeEmailTagIndex
|
||||||
|
}
|
||||||
|
validateTag={(
|
||||||
|
tag
|
||||||
|
) => {
|
||||||
|
return z
|
||||||
|
.string()
|
||||||
|
.email()
|
||||||
|
.safeParse(
|
||||||
|
tag
|
||||||
|
).success;
|
||||||
|
}}
|
||||||
|
setActiveTagIndex={
|
||||||
|
setActiveEmailTagIndex
|
||||||
|
}
|
||||||
|
placeholder="Enter an email"
|
||||||
|
tags={
|
||||||
|
whitelistForm.getValues()
|
||||||
|
.emails
|
||||||
|
}
|
||||||
|
setTags={(
|
||||||
|
newRoles
|
||||||
|
) => {
|
||||||
|
whitelistForm.setValue(
|
||||||
|
"emails",
|
||||||
|
newRoles as [
|
||||||
|
Tag,
|
||||||
|
...Tag[]
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
allowDuplicates={
|
||||||
|
false
|
||||||
|
}
|
||||||
|
sortTags={true}
|
||||||
|
styleClasses={{
|
||||||
|
tag: {
|
||||||
|
body: "bg-muted hover:bg-accent text-foreground py-2 px-3 rounded-full"
|
||||||
|
},
|
||||||
|
input: "text-base md:text-sm border-none bg-transparent text-inherit placeholder:text-inherit shadow-none",
|
||||||
|
inlineTagsContainer:
|
||||||
|
"bg-transparent p-2"
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Button
|
||||||
|
loading={loadingSaveWhitelist}
|
||||||
|
disabled={loadingSaveWhitelist}
|
||||||
|
onClick={saveWhitelist}
|
||||||
|
>
|
||||||
|
Save Whitelist
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -304,8 +304,8 @@ export default function ReverseProxyTargets(props: {
|
||||||
{row.original.method}
|
{row.original.method}
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="http">HTTP</SelectItem>
|
<SelectItem value="http">http</SelectItem>
|
||||||
<SelectItem value="https">HTTPS</SelectItem>
|
<SelectItem value="https">https</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
)
|
)
|
||||||
|
@ -412,7 +412,7 @@ export default function ReverseProxyTargets(props: {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="space-y-12">
|
<div className="space-y-12">
|
||||||
<section className="space-y-8">
|
<section className="space-y-4">
|
||||||
<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"
|
||||||
|
@ -431,14 +431,14 @@ export default function ReverseProxyTargets(props: {
|
||||||
|
|
||||||
<hr />
|
<hr />
|
||||||
|
|
||||||
<section className="space-y-8">
|
<section className="space-y-4">
|
||||||
<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-8">
|
<div className="space-y-4">
|
||||||
<Form {...addTargetForm}>
|
<Form {...addTargetForm}>
|
||||||
<form
|
<form
|
||||||
onSubmit={addTargetForm.handleSubmit(
|
onSubmit={addTargetForm.handleSubmit(
|
||||||
|
@ -470,18 +470,18 @@ export default function ReverseProxyTargets(props: {
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="http">
|
<SelectItem value="http">
|
||||||
HTTP
|
http
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
<SelectItem value="https">
|
<SelectItem value="https">
|
||||||
HTTPS
|
https
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormDescription>
|
{/* <FormDescription> */}
|
||||||
Choose the method for how
|
{/* Choose the method for how */}
|
||||||
the target is accessed.
|
{/* the target is accessed. */}
|
||||||
</FormDescription>
|
{/* </FormDescription> */}
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
|
@ -497,10 +497,9 @@ export default function ReverseProxyTargets(props: {
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input id="ip" {...field} />
|
<Input id="ip" {...field} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormDescription>
|
{/* <FormDescription> */}
|
||||||
Enter the IP address of the
|
{/* Use the IP of the resource on your private network if using Newt, or the peer IP if using raw WireGuard. */}
|
||||||
target.
|
{/* </FormDescription> */}
|
||||||
</FormDescription>
|
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
|
@ -519,10 +518,10 @@ export default function ReverseProxyTargets(props: {
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormDescription>
|
{/* <FormDescription> */}
|
||||||
Specify the port number for
|
{/* Specify the port number for */}
|
||||||
the target.
|
{/* the target. */}
|
||||||
</FormDescription>
|
{/* </FormDescription> */}
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -125,7 +125,7 @@ export default function GeneralForm() {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="space-y-12 lg:max-w-2xl">
|
<div className="space-y-12 lg:max-w-2xl">
|
||||||
<section className="space-y-8">
|
<section className="space-y-4">
|
||||||
<SettingsSectionTitle
|
<SettingsSectionTitle
|
||||||
title="General Settings"
|
title="General Settings"
|
||||||
description="Configure the general settings for this resource"
|
description="Configure the general settings for this resource"
|
||||||
|
|
|
@ -199,7 +199,7 @@ PersistentKeepalive = 5`
|
||||||
const newtConfig = `newt --id ${siteDefaults?.newtId} --secret ${siteDefaults?.newtSecret} --endpoint ${proto}//${siteDefaults?.endpoint}`;
|
const newtConfig = `newt --id ${siteDefaults?.newtId} --secret ${siteDefaults?.newtSecret} --endpoint ${proto}//${siteDefaults?.endpoint}`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-8">
|
<div className="space-y-4">
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form
|
<form
|
||||||
onSubmit={form.handleSubmit(onSubmit)}
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
|
|
|
@ -67,7 +67,7 @@ export default function GeneralPage() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="space-y-8 max-w-xl">
|
<div className="space-y-4 max-w-xl">
|
||||||
<SettingsSectionTitle
|
<SettingsSectionTitle
|
||||||
title="General Settings"
|
title="General Settings"
|
||||||
description="Configure the general settings for this site"
|
description="Configure the general settings for this site"
|
||||||
|
|
|
@ -37,7 +37,7 @@ export default function DashboardLoginForm({
|
||||||
return (
|
return (
|
||||||
<Card className="w-full max-w-md">
|
<Card className="w-full max-w-md">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>Login</CardTitle>
|
<CardTitle>Welcome to Pangolin</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
Enter your credentials to access your dashboard
|
Enter your credentials to access your dashboard
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
|
|
|
@ -401,7 +401,7 @@ export default function ResourceAuthPortal(props: ResourceAuthPortalProps) {
|
||||||
disabled={loadingLogin}
|
disabled={loadingLogin}
|
||||||
>
|
>
|
||||||
<LockIcon className="w-4 h-4 mr-2" />
|
<LockIcon className="w-4 h-4 mr-2" />
|
||||||
Login with PIN
|
Log in with PIN
|
||||||
</Button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
</Form>
|
</Form>
|
||||||
|
@ -456,7 +456,7 @@ export default function ResourceAuthPortal(props: ResourceAuthPortalProps) {
|
||||||
disabled={loadingLogin}
|
disabled={loadingLogin}
|
||||||
>
|
>
|
||||||
<LockIcon className="w-4 h-4 mr-2" />
|
<LockIcon className="w-4 h-4 mr-2" />
|
||||||
Login with Password
|
Log In with Password
|
||||||
</Button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
</Form>
|
</Form>
|
||||||
|
|
|
@ -91,7 +91,7 @@ export default function InviteStatusCard({
|
||||||
);
|
);
|
||||||
} else if (type === "wrong_user") {
|
} else if (type === "wrong_user") {
|
||||||
return (
|
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") {
|
} else if (type === "user_does_not_exist") {
|
||||||
return <Button onClick={goToSignup}>Create an account</Button>;
|
return <Button onClick={goToSignup}>Create an account</Button>;
|
||||||
|
|
|
@ -5,10 +5,6 @@ import { Toaster } from "@/components/ui/toaster";
|
||||||
import { ThemeProvider } from "@app/providers/ThemeProvider";
|
import { ThemeProvider } from "@app/providers/ThemeProvider";
|
||||||
import EnvProvider from "@app/providers/EnvProvider";
|
import EnvProvider from "@app/providers/EnvProvider";
|
||||||
import { Separator } from "@app/components/ui/separator";
|
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 = {
|
export const metadata: Metadata = {
|
||||||
title: `Dashboard - Pangolin`,
|
title: `Dashboard - Pangolin`,
|
||||||
|
@ -26,7 +22,7 @@ export default async function RootLayout({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<html suppressHydrationWarning>
|
<html suppressHydrationWarning>
|
||||||
<body className={`${font.className}`}>
|
<body className={`${font.className} min-h-screen flex flex-col`}>
|
||||||
<ThemeProvider
|
<ThemeProvider
|
||||||
attribute="class"
|
attribute="class"
|
||||||
defaultTheme="system"
|
defaultTheme="system"
|
||||||
|
@ -34,23 +30,22 @@ export default async function RootLayout({
|
||||||
disableTransitionOnChange
|
disableTransitionOnChange
|
||||||
>
|
>
|
||||||
<EnvProvider
|
<EnvProvider
|
||||||
// it's import not to pass all of process.env here in case of secrets
|
|
||||||
// select only the necessary ones
|
|
||||||
env={{
|
env={{
|
||||||
NEXT_PORT: process.env.NEXT_PORT as string,
|
NEXT_PORT: process.env.NEXT_PORT as string,
|
||||||
SERVER_EXTERNAL_PORT: process.env
|
SERVER_EXTERNAL_PORT: process.env
|
||||||
.SERVER_EXTERNAL_PORT as string,
|
.SERVER_EXTERNAL_PORT as string,
|
||||||
ENVIRONMENT: process.env.ENVIRONMENT as string,
|
ENVIRONMENT: process.env.ENVIRONMENT as string,
|
||||||
EMAIL_ENABLED: process.env.EMAIL_ENABLED as string,
|
EMAIL_ENABLED: process.env.EMAIL_ENABLED as string,
|
||||||
// optional
|
|
||||||
DISABLE_USER_CREATE_ORG:
|
DISABLE_USER_CREATE_ORG:
|
||||||
process.env.DISABLE_USER_CREATE_ORG,
|
process.env.DISABLE_USER_CREATE_ORG,
|
||||||
DISABLE_SIGNUP_WITHOUT_INVITE:
|
DISABLE_SIGNUP_WITHOUT_INVITE:
|
||||||
process.env.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">
|
<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="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">
|
<div className="whitespace-nowrap">
|
||||||
|
|
|
@ -130,7 +130,7 @@ export default function LoginForm({ redirect, onLogin }: LoginFormProps) {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-8">
|
<div className="space-y-4">
|
||||||
{!mfaRequested && (
|
{!mfaRequested && (
|
||||||
<Form {...form}>
|
<Form {...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}` : ""}`}
|
href={`/auth/reset-password${form.getValues().email ? `?email=${form.getValues().email}` : ""}`}
|
||||||
className="text-sm text-muted-foreground"
|
className="text-sm text-muted-foreground"
|
||||||
>
|
>
|
||||||
Forgot password?
|
Forgot your password?
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -279,7 +279,7 @@ export default function LoginForm({ redirect, onLogin }: LoginFormProps) {
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
>
|
>
|
||||||
<LockIcon className="w-4 h-4 mr-2" />
|
<LockIcon className="w-4 h-4 mr-2" />
|
||||||
Login
|
Log In
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
@ -293,7 +293,7 @@ export default function LoginForm({ redirect, onLogin }: LoginFormProps) {
|
||||||
mfaForm.reset();
|
mfaForm.reset();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Back to Login
|
Back to Log In
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -10,9 +10,9 @@ const alertVariants = cva(
|
||||||
variant: {
|
variant: {
|
||||||
default: "bg-background text-foreground",
|
default: "bg-background text-foreground",
|
||||||
destructive:
|
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:
|
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: {
|
defaultVariants: {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue