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 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 Pangolin’s centralized authentication system for proxies, enabling robust user and role management.
|
||||
|
||||
## Licensing
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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{" "}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -84,7 +84,7 @@ export default function GeneralPage() {
|
|||
const res = await api.get<AxiosResponse<ListOrgsResponse>>(
|
||||
`/orgs`
|
||||
);
|
||||
|
||||
|
||||
if (res.status === 200) {
|
||||
if (res.data.data.orgs.length > 0) {
|
||||
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 { 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,120 +412,134 @@ export default function ResourceAuthenticationPage() {
|
|||
)}
|
||||
className="space-y-4"
|
||||
>
|
||||
<FormField
|
||||
control={usersRolesForm.control}
|
||||
name="roles"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-col items-start">
|
||||
<FormLabel>Roles</FormLabel>
|
||||
<FormControl>
|
||||
{/* @ts-ignore */}
|
||||
<TagInput
|
||||
{...field}
|
||||
activeTagIndex={
|
||||
activeRolesTagIndex
|
||||
}
|
||||
setActiveTagIndex={
|
||||
setActiveRolesTagIndex
|
||||
}
|
||||
placeholder="Enter a role"
|
||||
tags={
|
||||
usersRolesForm.getValues()
|
||||
.roles
|
||||
}
|
||||
setTags={(newRoles) => {
|
||||
usersRolesForm.setValue(
|
||||
"roles",
|
||||
newRoles as [
|
||||
Tag,
|
||||
...Tag[]
|
||||
]
|
||||
);
|
||||
}}
|
||||
enableAutocomplete={true}
|
||||
autocompleteOptions={allRoles}
|
||||
allowDuplicates={false}
|
||||
restrictTagsToAutocompleteOptions={
|
||||
true
|
||||
}
|
||||
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>
|
||||
<FormDescription>
|
||||
These roles will be able to access
|
||||
this resource. Admins can always
|
||||
access this resource.
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={usersRolesForm.control}
|
||||
name="users"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-col items-start">
|
||||
<FormLabel>Users</FormLabel>
|
||||
<FormControl>
|
||||
{/* @ts-ignore */}
|
||||
<TagInput
|
||||
{...field}
|
||||
activeTagIndex={
|
||||
activeUsersTagIndex
|
||||
}
|
||||
setActiveTagIndex={
|
||||
setActiveUsersTagIndex
|
||||
}
|
||||
placeholder="Enter a user"
|
||||
tags={
|
||||
usersRolesForm.getValues()
|
||||
.users
|
||||
}
|
||||
setTags={(newUsers) => {
|
||||
usersRolesForm.setValue(
|
||||
"users",
|
||||
newUsers as [
|
||||
Tag,
|
||||
...Tag[]
|
||||
]
|
||||
);
|
||||
}}
|
||||
enableAutocomplete={true}
|
||||
autocompleteOptions={allUsers}
|
||||
allowDuplicates={false}
|
||||
restrictTagsToAutocompleteOptions={
|
||||
true
|
||||
}
|
||||
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>
|
||||
<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>
|
||||
)}
|
||||
/>
|
||||
{ssoEnabled && (
|
||||
<>
|
||||
<FormField
|
||||
control={usersRolesForm.control}
|
||||
name="roles"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-col items-start">
|
||||
<FormLabel>Roles</FormLabel>
|
||||
<FormControl>
|
||||
{/* @ts-ignore */}
|
||||
<TagInput
|
||||
{...field}
|
||||
activeTagIndex={
|
||||
activeRolesTagIndex
|
||||
}
|
||||
setActiveTagIndex={
|
||||
setActiveRolesTagIndex
|
||||
}
|
||||
placeholder="Enter a role"
|
||||
tags={
|
||||
usersRolesForm.getValues()
|
||||
.roles
|
||||
}
|
||||
setTags={(newRoles) => {
|
||||
usersRolesForm.setValue(
|
||||
"roles",
|
||||
newRoles as [
|
||||
Tag,
|
||||
...Tag[]
|
||||
]
|
||||
);
|
||||
}}
|
||||
enableAutocomplete={
|
||||
true
|
||||
}
|
||||
autocompleteOptions={
|
||||
allRoles
|
||||
}
|
||||
allowDuplicates={false}
|
||||
restrictTagsToAutocompleteOptions={
|
||||
true
|
||||
}
|
||||
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>
|
||||
<FormDescription>
|
||||
These roles will be able to
|
||||
access this resource. Admins
|
||||
can always access this
|
||||
resource.
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={usersRolesForm.control}
|
||||
name="users"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-col items-start">
|
||||
<FormLabel>Users</FormLabel>
|
||||
<FormControl>
|
||||
{/* @ts-ignore */}
|
||||
<TagInput
|
||||
{...field}
|
||||
activeTagIndex={
|
||||
activeUsersTagIndex
|
||||
}
|
||||
setActiveTagIndex={
|
||||
setActiveUsersTagIndex
|
||||
}
|
||||
placeholder="Enter a user"
|
||||
tags={
|
||||
usersRolesForm.getValues()
|
||||
.users
|
||||
}
|
||||
setTags={(newUsers) => {
|
||||
usersRolesForm.setValue(
|
||||
"users",
|
||||
newUsers as [
|
||||
Tag,
|
||||
...Tag[]
|
||||
]
|
||||
);
|
||||
}}
|
||||
enableAutocomplete={
|
||||
true
|
||||
}
|
||||
autocompleteOptions={
|
||||
allUsers
|
||||
}
|
||||
allowDuplicates={false}
|
||||
restrictTagsToAutocompleteOptions={
|
||||
true
|
||||
}
|
||||
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>
|
||||
<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
|
||||
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,101 +629,108 @@ export default function ResourceAuthenticationPage() {
|
|||
|
||||
<Separator />
|
||||
|
||||
<section className="space-y-8 lg:max-w-2xl">
|
||||
{env.EMAIL_ENABLED === "true" && (
|
||||
<>
|
||||
<div>
|
||||
<div className="flex items-center space-x-2 mb-2">
|
||||
<Switch
|
||||
id="whitelist-toggle"
|
||||
defaultChecked={
|
||||
resource.emailWhitelistEnabled
|
||||
}
|
||||
onCheckedChange={(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>
|
||||
)}
|
||||
<section className="space-y-4 lg:max-w-2xl">
|
||||
{env.EMAIL_ENABLED === "true" && (
|
||||
<>
|
||||
<div>
|
||||
<div className="flex items-center space-x-2 mb-2">
|
||||
<Switch
|
||||
id="whitelist-toggle"
|
||||
defaultChecked={
|
||||
resource.emailWhitelistEnabled
|
||||
}
|
||||
onCheckedChange={(val) =>
|
||||
setWhitelistEnabled(val)
|
||||
}
|
||||
/>
|
||||
</form>
|
||||
</Form>
|
||||
)}
|
||||
<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>
|
||||
|
||||
<Button
|
||||
loading={loadingSaveWhitelist}
|
||||
disabled={loadingSaveWhitelist}
|
||||
onClick={saveWhitelist}
|
||||
>
|
||||
Save Whitelist
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
{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>
|
||||
</Form>
|
||||
)}
|
||||
|
||||
<Button
|
||||
loading={loadingSaveWhitelist}
|
||||
disabled={loadingSaveWhitelist}
|
||||
onClick={saveWhitelist}
|
||||
>
|
||||
Save Whitelist
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</section>
|
||||
</div>
|
||||
</>
|
||||
|
|
|
@ -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>
|
||||
)}
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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)}
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>;
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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: {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue