diff --git a/README.md b/README.md index fd428add..33bc90b1 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/config.example.yml b/config.example.yml index 9c66d2dd..b5a109f4 100644 --- a/config.example.yml +++ b/config.example.yml @@ -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 + diff --git a/server/lib/config.ts b/server/lib/config.ts index a282cca0..8fdc455d 100644 --- a/server/lib/config.ts +++ b/server/lib/config.ts @@ -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 diff --git a/src/app/[orgId]/settings/access/roles/DeleteRoleForm.tsx b/src/app/[orgId]/settings/access/roles/DeleteRoleForm.tsx index ae86726b..5c7c2c4b 100644 --- a/src/app/[orgId]/settings/access/roles/DeleteRoleForm.tsx +++ b/src/app/[orgId]/settings/access/roles/DeleteRoleForm.tsx @@ -158,7 +158,7 @@ export default function DeleteRoleForm({ -
+

You're about to delete the{" "} diff --git a/src/app/[orgId]/settings/access/users/InviteUserForm.tsx b/src/app/[orgId]/settings/access/users/InviteUserForm.tsx index 12772bac..be52f188 100644 --- a/src/app/[orgId]/settings/access/users/InviteUserForm.tsx +++ b/src/app/[orgId]/settings/access/users/InviteUserForm.tsx @@ -180,7 +180,7 @@ export default function InviteUserForm({ open, setOpen }: InviteUserFormProps) { -

+
{!inviteLink && (
>( `/orgs` ); - + if (res.status === 200) { if (res.data.data.orgs.length > 0) { const orgId = res.data.data.orgs[0].orgId; diff --git a/src/app/[orgId]/settings/resources/[resourceId]/authentication/page.tsx b/src/app/[orgId]/settings/resources/[resourceId]/authentication/page.tsx index 900f9863..ac525207 100644 --- a/src/app/[orgId]/settings/resources/[resourceId]/authentication/page.tsx +++ b/src/app/[orgId]/settings/resources/[resourceId]/authentication/page.tsx @@ -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() { )}
-
+
setSsoEnabled(val)} /> - +
Existing users will only have to login once for all @@ -414,120 +412,134 @@ export default function ResourceAuthenticationPage() { )} className="space-y-4" > - ( - - Roles - - {/* @ts-ignore */} - { - 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" - }} - /> - - - These roles will be able to access - this resource. Admins can always - access this resource. - - - - )} - /> - ( - - Users - - {/* @ts-ignore */} - { - 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" - }} - /> - - - 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. - - - - )} - /> + {ssoEnabled && ( + <> + ( + + Roles + + {/* @ts-ignore */} + { + 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" + }} + /> + + + These roles will be able to + access this resource. Admins + can always access this + resource. + + + + )} + /> + ( + + Users + + {/* @ts-ignore */} + { + 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" + }} + /> + + + 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. + + + + )} + /> + + )} - - )} + {whitelistEnabled && ( +
+ + ( + + + Whitelisted Emails + + + {/* @ts-ignore */} + { + 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" + }} + /> + + + )} + /> + + + )} + + + + )}
diff --git a/src/app/[orgId]/settings/resources/[resourceId]/connectivity/page.tsx b/src/app/[orgId]/settings/resources/[resourceId]/connectivity/page.tsx index fe91d2e7..14adbec1 100644 --- a/src/app/[orgId]/settings/resources/[resourceId]/connectivity/page.tsx +++ b/src/app/[orgId]/settings/resources/[resourceId]/connectivity/page.tsx @@ -304,8 +304,8 @@ export default function ReverseProxyTargets(props: { {row.original.method} - HTTP - HTTPS + http + https ) @@ -412,7 +412,7 @@ export default function ReverseProxyTargets(props: { return ( <>
-
+
-
+
-
+
- HTTP + http - HTTPS + https - - Choose the method for how - the target is accessed. - + {/* */} + {/* Choose the method for how */} + {/* the target is accessed. */} + {/* */} )} @@ -497,10 +497,9 @@ export default function ReverseProxyTargets(props: { - - Enter the IP address of the - target. - + {/* */} + {/* Use the IP of the resource on your private network if using Newt, or the peer IP if using raw WireGuard. */} + {/* */} )} @@ -519,10 +518,10 @@ export default function ReverseProxyTargets(props: { required /> - - Specify the port number for - the target. - + {/* */} + {/* Specify the port number for */} + {/* the target. */} + {/* */} )} diff --git a/src/app/[orgId]/settings/resources/[resourceId]/general/page.tsx b/src/app/[orgId]/settings/resources/[resourceId]/general/page.tsx index 3aa64a87..2b848447 100644 --- a/src/app/[orgId]/settings/resources/[resourceId]/general/page.tsx +++ b/src/app/[orgId]/settings/resources/[resourceId]/general/page.tsx @@ -125,7 +125,7 @@ export default function GeneralForm() { return ( <>
-
+
+
-
+
- Login + Welcome to Pangolin Enter your credentials to access your dashboard diff --git a/src/app/auth/resource/[resourceId]/ResourceAuthPortal.tsx b/src/app/auth/resource/[resourceId]/ResourceAuthPortal.tsx index 2cb6dcd8..8cc25aa5 100644 --- a/src/app/auth/resource/[resourceId]/ResourceAuthPortal.tsx +++ b/src/app/auth/resource/[resourceId]/ResourceAuthPortal.tsx @@ -401,7 +401,7 @@ export default function ResourceAuthPortal(props: ResourceAuthPortalProps) { disabled={loadingLogin} > - Login with PIN + Log in with PIN @@ -456,7 +456,7 @@ export default function ResourceAuthPortal(props: ResourceAuthPortalProps) { disabled={loadingLogin} > - Login with Password + Log In with Password diff --git a/src/app/invite/InviteStatusCard.tsx b/src/app/invite/InviteStatusCard.tsx index 235a7359..11b37621 100644 --- a/src/app/invite/InviteStatusCard.tsx +++ b/src/app/invite/InviteStatusCard.tsx @@ -91,7 +91,7 @@ export default function InviteStatusCard({ ); } else if (type === "wrong_user") { return ( - + ); } else if (type === "user_does_not_exist") { return ; diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 6b997204..5e52d184 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -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 ( - + - {children} + {/* Main content */} +
{children}
+ {/* Footer */}