mirror of
https://github.com/fosrl/pangolin.git
synced 2025-05-19 08:37:48 +01:00
Merge branch 'dev' of https://github.com/fosrl/pangolin into dev
This commit is contained in:
commit
c8c756df28
21 changed files with 135 additions and 45 deletions
14
SECURITY.md
Normal file
14
SECURITY.md
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
# Security Policy
|
||||||
|
|
||||||
|
If you discover a security vulnerability, please follow the steps below to responsibly disclose it to us:
|
||||||
|
|
||||||
|
1. **Do not create a public GitHub issue or discussion post.** This could put the security of other users at risk.
|
||||||
|
2. Send a detailed report to [security@fossorial.io](mailto:security@fossorial.io) or send a **private** message to a maintainer on [Discord](https://discord.gg/HCJR8Xhme4). Include:
|
||||||
|
|
||||||
|
- Description and location of the vulnerability.
|
||||||
|
- Potential impact of the vulnerability.
|
||||||
|
- Steps to reproduce the vulnerability.
|
||||||
|
- Potential solutions to fix the vulnerability.
|
||||||
|
- Your name/handle and a link for recognition (optional).
|
||||||
|
|
||||||
|
We aim to address the issue as soon as possible.
|
|
@ -2,7 +2,11 @@
|
||||||
all: build
|
all: build
|
||||||
|
|
||||||
build:
|
build:
|
||||||
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o installer
|
CGO_ENABLED=0 GOOS=linux go build -o installer
|
||||||
|
|
||||||
|
all_arches:
|
||||||
|
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -o installer_linux_arm64
|
||||||
|
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o installer_linux_amd64
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
rm installer
|
rm installer
|
|
@ -1,6 +1,10 @@
|
||||||
{
|
{
|
||||||
"name": "@fosrl/pangolin",
|
"name": "@fosrl/pangolin",
|
||||||
|
<<<<<<< HEAD
|
||||||
"version": "1.0.0-beta.2",
|
"version": "1.0.0-beta.2",
|
||||||
|
=======
|
||||||
|
"version": "1.0.0-beta.3",
|
||||||
|
>>>>>>> c3d19454f7f5c5546a7efb47c23fc040dbbf94d2
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "Tunneled Reverse Proxy Management Server with Identity and Access Control and Dashboard UI",
|
"description": "Tunneled Reverse Proxy Management Server with Identity and Access Control and Dashboard UI",
|
||||||
|
|
|
@ -3,7 +3,15 @@ import yaml from "js-yaml";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { fromError } from "zod-validation-error";
|
import { fromError } from "zod-validation-error";
|
||||||
|
<<<<<<< HEAD
|
||||||
import { __DIRNAME, APP_PATH, configFilePath1, configFilePath2 } from "@server/lib/consts";
|
import { __DIRNAME, APP_PATH, configFilePath1, configFilePath2 } from "@server/lib/consts";
|
||||||
|
=======
|
||||||
|
import {
|
||||||
|
__DIRNAME,
|
||||||
|
configFilePath1,
|
||||||
|
configFilePath2
|
||||||
|
} from "@server/lib/consts";
|
||||||
|
>>>>>>> c3d19454f7f5c5546a7efb47c23fc040dbbf94d2
|
||||||
import { loadAppVersion } from "@server/lib/loadAppVersion";
|
import { loadAppVersion } from "@server/lib/loadAppVersion";
|
||||||
import { passwordSchema } from "@server/auth/passwordSchema";
|
import { passwordSchema } from "@server/auth/passwordSchema";
|
||||||
|
|
||||||
|
@ -11,9 +19,15 @@ const portSchema = z.number().positive().gt(0).lte(65535);
|
||||||
const hostnameSchema = z
|
const hostnameSchema = z
|
||||||
.string()
|
.string()
|
||||||
.regex(
|
.regex(
|
||||||
|
<<<<<<< HEAD
|
||||||
/^(?!-)[a-zA-Z0-9-]{1,63}(?<!-)(\.[a-zA-Z]{2,})*$/,
|
/^(?!-)[a-zA-Z0-9-]{1,63}(?<!-)(\.[a-zA-Z]{2,})*$/,
|
||||||
"Invalid hostname. Must be a valid hostname like 'localhost' or 'test.example.com'."
|
"Invalid hostname. Must be a valid hostname like 'localhost' or 'test.example.com'."
|
||||||
);
|
);
|
||||||
|
=======
|
||||||
|
/^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$|^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)+([A-Za-z]|[A-Za-z][A-Za-z0-9\-]*[A-Za-z0-9])$/
|
||||||
|
)
|
||||||
|
.or(z.literal("localhost"));
|
||||||
|
>>>>>>> c3d19454f7f5c5546a7efb47c23fc040dbbf94d2
|
||||||
|
|
||||||
const environmentSchema = z.object({
|
const environmentSchema = z.object({
|
||||||
app: z.object({
|
app: z.object({
|
||||||
|
|
|
@ -25,7 +25,7 @@ export default async function OrgLayout(props: {
|
||||||
const user = await getUser();
|
const user = await getUser();
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
redirect(`/?redirect=/${orgId}`);
|
redirect(`/`);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -26,7 +26,7 @@ export default async function GeneralSettingsPage({
|
||||||
const user = await getUser();
|
const user = await getUser();
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
redirect(`/?redirect=/${orgId}/settings/general`);
|
redirect(`/`);
|
||||||
}
|
}
|
||||||
|
|
||||||
let orgUser = null;
|
let orgUser = null;
|
||||||
|
|
|
@ -61,7 +61,7 @@ export default async function SettingsLayout(props: SettingsLayoutProps) {
|
||||||
const user = await getUser();
|
const user = await getUser();
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
redirect(`/?redirect=/${params.orgId}/`);
|
redirect(`/`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const cookie = await authCookieHeader();
|
const cookie = await authCookieHeader();
|
||||||
|
|
|
@ -13,6 +13,7 @@ import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
|
import { cleanRedirect } from "@app/lib/cleanRedirect";
|
||||||
|
|
||||||
type DashboardLoginFormProps = {
|
type DashboardLoginFormProps = {
|
||||||
redirect?: string;
|
redirect?: string;
|
||||||
|
@ -57,10 +58,9 @@ export default function DashboardLoginForm({
|
||||||
<LoginForm
|
<LoginForm
|
||||||
redirect={redirect}
|
redirect={redirect}
|
||||||
onLogin={() => {
|
onLogin={() => {
|
||||||
if (redirect && redirect.includes("http")) {
|
if (redirect) {
|
||||||
window.location.href = redirect;
|
const safe = cleanRedirect(redirect);
|
||||||
} else if (redirect) {
|
router.push(safe);
|
||||||
router.push(redirect);
|
|
||||||
} else {
|
} else {
|
||||||
router.push("/");
|
router.push("/");
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,7 @@ import { cache } from "react";
|
||||||
import DashboardLoginForm from "./DashboardLoginForm";
|
import DashboardLoginForm from "./DashboardLoginForm";
|
||||||
import { Mail } from "lucide-react";
|
import { Mail } from "lucide-react";
|
||||||
import { pullEnv } from "@app/lib/pullEnv";
|
import { pullEnv } from "@app/lib/pullEnv";
|
||||||
|
import { cleanRedirect } from "@app/lib/cleanRedirect";
|
||||||
|
|
||||||
export const dynamic = "force-dynamic";
|
export const dynamic = "force-dynamic";
|
||||||
|
|
||||||
|
@ -25,6 +26,11 @@ export default async function Page(props: {
|
||||||
redirect("/");
|
redirect("/");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let redirectUrl: string | undefined = undefined;
|
||||||
|
if (searchParams.redirect) {
|
||||||
|
redirectUrl = cleanRedirect(searchParams.redirect as string);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{isInvite && (
|
{isInvite && (
|
||||||
|
@ -42,16 +48,16 @@ export default async function Page(props: {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<DashboardLoginForm redirect={searchParams.redirect as string} />
|
<DashboardLoginForm redirect={redirectUrl} />
|
||||||
|
|
||||||
{(!signUpDisabled || isInvite) && (
|
{(!signUpDisabled || isInvite) && (
|
||||||
<p className="text-center text-muted-foreground mt-4">
|
<p className="text-center text-muted-foreground mt-4">
|
||||||
Don't have an account?{" "}
|
Don't have an account?{" "}
|
||||||
<Link
|
<Link
|
||||||
href={
|
href={
|
||||||
!searchParams.redirect
|
!redirectUrl
|
||||||
? `/auth/signup`
|
? `/auth/signup`
|
||||||
: `/auth/signup?redirect=${searchParams.redirect}`
|
: `/auth/signup?redirect=${redirectUrl}`
|
||||||
}
|
}
|
||||||
className="underline"
|
className="underline"
|
||||||
>
|
>
|
||||||
|
|
|
@ -43,6 +43,7 @@ import { createApiClient } from "@app/lib/api";
|
||||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||||
import { REGEXP_ONLY_DIGITS_AND_CHARS } from "input-otp";
|
import { REGEXP_ONLY_DIGITS_AND_CHARS } from "input-otp";
|
||||||
import { passwordSchema } from "@server/auth/passwordSchema";
|
import { passwordSchema } from "@server/auth/passwordSchema";
|
||||||
|
import { cleanRedirect } from "@app/lib/cleanRedirect";
|
||||||
|
|
||||||
const requestSchema = z.object({
|
const requestSchema = z.object({
|
||||||
email: z.string().email()
|
email: z.string().email()
|
||||||
|
@ -186,11 +187,9 @@ export default function ResetPasswordForm({
|
||||||
setSuccessMessage("Password reset successfully! Back to login...");
|
setSuccessMessage("Password reset successfully! Back to login...");
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (redirect && redirect.includes("http")) {
|
|
||||||
window.location.href = redirect;
|
|
||||||
}
|
|
||||||
if (redirect) {
|
if (redirect) {
|
||||||
router.push(redirect);
|
const safe = cleanRedirect(redirect);
|
||||||
|
router.push(safe);
|
||||||
} else {
|
} else {
|
||||||
router.push("/login");
|
router.push("/login");
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@ import { redirect } from "next/navigation";
|
||||||
import { cache } from "react";
|
import { cache } from "react";
|
||||||
import ResetPasswordForm from "./ResetPasswordForm";
|
import ResetPasswordForm from "./ResetPasswordForm";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
import { cleanRedirect } from "@app/lib/cleanRedirect";
|
||||||
|
|
||||||
export const dynamic = "force-dynamic";
|
export const dynamic = "force-dynamic";
|
||||||
|
|
||||||
|
@ -21,6 +22,11 @@ export default async function Page(props: {
|
||||||
redirect("/");
|
redirect("/");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let redirectUrl: string | undefined = undefined;
|
||||||
|
if (searchParams.redirect) {
|
||||||
|
redirectUrl = cleanRedirect(searchParams.redirect);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ResetPasswordForm
|
<ResetPasswordForm
|
||||||
|
@ -34,7 +40,7 @@ export default async function Page(props: {
|
||||||
href={
|
href={
|
||||||
!searchParams.redirect
|
!searchParams.redirect
|
||||||
? `/auth/signup`
|
? `/auth/signup`
|
||||||
: `/auth/signup?redirect=${searchParams.redirect}`
|
: `/auth/signup?redirect=${redirectUrl}`
|
||||||
}
|
}
|
||||||
className="underline"
|
className="underline"
|
||||||
>
|
>
|
||||||
|
|
|
@ -481,11 +481,7 @@ export default function ResourceAuthPortal(props: ResourceAuthPortalProps) {
|
||||||
className={`${numMethods <= 1 ? "mt-0" : ""}`}
|
className={`${numMethods <= 1 ? "mt-0" : ""}`}
|
||||||
>
|
>
|
||||||
<LoginForm
|
<LoginForm
|
||||||
redirect={
|
redirect={`/auth/resource/${props.resource.id}`}
|
||||||
typeof window !== "undefined"
|
|
||||||
? window.location.href
|
|
||||||
: ""
|
|
||||||
}
|
|
||||||
onLogin={async () =>
|
onLogin={async () =>
|
||||||
await handleSSOAuth()
|
await handleSSOAuth()
|
||||||
}
|
}
|
||||||
|
|
|
@ -55,7 +55,17 @@ export default async function ResourceAuthPage(props: {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const redirectUrl = searchParams.redirect || authInfo.url;
|
let redirectUrl = authInfo.url;
|
||||||
|
if (searchParams.redirect) {
|
||||||
|
try {
|
||||||
|
const serverResourceHost = new URL(authInfo.url).host;
|
||||||
|
const redirectHost = new URL(searchParams.redirect).host;
|
||||||
|
|
||||||
|
if (serverResourceHost === redirectHost) {
|
||||||
|
redirectUrl = searchParams.redirect;
|
||||||
|
}
|
||||||
|
} catch (e) {}
|
||||||
|
}
|
||||||
|
|
||||||
const hasAuth =
|
const hasAuth =
|
||||||
authInfo.password ||
|
authInfo.password ||
|
||||||
|
|
|
@ -30,6 +30,7 @@ import { formatAxiosError } from "@app/lib/api";
|
||||||
import { createApiClient } from "@app/lib/api";
|
import { createApiClient } from "@app/lib/api";
|
||||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
|
import { cleanRedirect } from "@app/lib/cleanRedirect";
|
||||||
|
|
||||||
type SignupFormProps = {
|
type SignupFormProps = {
|
||||||
redirect?: string;
|
redirect?: string;
|
||||||
|
@ -92,17 +93,17 @@ export default function SignupForm({
|
||||||
|
|
||||||
if (res.data?.data?.emailVerificationRequired) {
|
if (res.data?.data?.emailVerificationRequired) {
|
||||||
if (redirect) {
|
if (redirect) {
|
||||||
router.push(`/auth/verify-email?redirect=${redirect}`);
|
const safe = cleanRedirect(redirect);
|
||||||
|
router.push(`/auth/verify-email?redirect=${safe}`);
|
||||||
} else {
|
} else {
|
||||||
router.push("/auth/verify-email");
|
router.push("/auth/verify-email");
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (redirect && redirect.includes("http")) {
|
if (redirect) {
|
||||||
window.location.href = redirect;
|
const safe = cleanRedirect(redirect);
|
||||||
} else if (redirect) {
|
router.push(safe);
|
||||||
router.push(redirect);
|
|
||||||
} else {
|
} else {
|
||||||
router.push("/");
|
router.push("/");
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import SignupForm from "@app/app/auth/signup/SignupForm";
|
import SignupForm from "@app/app/auth/signup/SignupForm";
|
||||||
import { verifySession } from "@app/lib/auth/verifySession";
|
import { verifySession } from "@app/lib/auth/verifySession";
|
||||||
|
import { cleanRedirect } from "@app/lib/cleanRedirect";
|
||||||
import { pullEnv } from "@app/lib/pullEnv";
|
import { pullEnv } from "@app/lib/pullEnv";
|
||||||
import { Mail } from "lucide-react";
|
import { Mail } from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
@ -41,6 +42,11 @@ export default async function Page(props: {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let redirectUrl: string | undefined;
|
||||||
|
if (searchParams.redirect) {
|
||||||
|
redirectUrl = cleanRedirect(searchParams.redirect);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{isInvite && (
|
{isInvite && (
|
||||||
|
@ -59,7 +65,7 @@ export default async function Page(props: {
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<SignupForm
|
<SignupForm
|
||||||
redirect={searchParams.redirect as string}
|
redirect={redirectUrl}
|
||||||
inviteToken={inviteToken}
|
inviteToken={inviteToken}
|
||||||
inviteId={inviteId}
|
inviteId={inviteId}
|
||||||
/>
|
/>
|
||||||
|
@ -68,9 +74,9 @@ export default async function Page(props: {
|
||||||
Already have an account?{" "}
|
Already have an account?{" "}
|
||||||
<Link
|
<Link
|
||||||
href={
|
href={
|
||||||
!searchParams.redirect
|
!redirectUrl
|
||||||
? `/auth/login`
|
? `/auth/login`
|
||||||
: `/auth/login?redirect=${searchParams.redirect}`
|
: `/auth/login?redirect=${redirectUrl}`
|
||||||
}
|
}
|
||||||
className="underline"
|
className="underline"
|
||||||
>
|
>
|
||||||
|
|
|
@ -36,6 +36,7 @@ import { useRouter } from "next/navigation";
|
||||||
import { formatAxiosError } from "@app/lib/api";;
|
import { formatAxiosError } from "@app/lib/api";;
|
||||||
import { createApiClient } from "@app/lib/api";
|
import { createApiClient } from "@app/lib/api";
|
||||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||||
|
import { cleanRedirect } from "@app/lib/cleanRedirect";
|
||||||
|
|
||||||
const FormSchema = z.object({
|
const FormSchema = z.object({
|
||||||
email: z.string().email({ message: "Invalid email address" }),
|
email: z.string().email({ message: "Invalid email address" }),
|
||||||
|
@ -91,11 +92,9 @@ export default function VerifyEmailForm({
|
||||||
"Email successfully verified! Redirecting you..."
|
"Email successfully verified! Redirecting you..."
|
||||||
);
|
);
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (redirect && redirect.includes("http")) {
|
|
||||||
window.location.href = redirect;
|
|
||||||
}
|
|
||||||
if (redirect) {
|
if (redirect) {
|
||||||
router.push(redirect);
|
const safe = cleanRedirect(redirect);
|
||||||
|
router.push(safe);
|
||||||
} else {
|
} else {
|
||||||
router.push("/");
|
router.push("/");
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import VerifyEmailForm from "@app/app/auth/verify-email/VerifyEmailForm";
|
import VerifyEmailForm from "@app/app/auth/verify-email/VerifyEmailForm";
|
||||||
import { verifySession } from "@app/lib/auth/verifySession";
|
import { verifySession } from "@app/lib/auth/verifySession";
|
||||||
|
import { cleanRedirect } from "@app/lib/cleanRedirect";
|
||||||
import { pullEnv } from "@app/lib/pullEnv";
|
import { pullEnv } from "@app/lib/pullEnv";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { cache } from "react";
|
import { cache } from "react";
|
||||||
|
@ -27,11 +28,16 @@ export default async function Page(props: {
|
||||||
redirect("/");
|
redirect("/");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let redirectUrl: string | undefined;
|
||||||
|
if (searchParams.redirect) {
|
||||||
|
redirectUrl = cleanRedirect(searchParams.redirect as string);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<VerifyEmailForm
|
<VerifyEmailForm
|
||||||
email={user.email}
|
email={user.email}
|
||||||
redirect={searchParams.redirect as string}
|
redirect={redirectUrl}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
@ -6,6 +6,8 @@ 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 { pullEnv } from "@app/lib/pullEnv";
|
import { pullEnv } from "@app/lib/pullEnv";
|
||||||
|
import { BookOpenText } from "lucide-react";
|
||||||
|
import Image from "next/image";
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: `Dashboard - Pangolin`,
|
title: `Dashboard - Pangolin`,
|
||||||
|
@ -38,10 +40,10 @@ export default async function RootLayout({
|
||||||
<div className="flex-grow">{children}</div>
|
<div className="flex-grow">{children}</div>
|
||||||
|
|
||||||
{/* Footer */}
|
{/* Footer */}
|
||||||
<footer className="w-full mt-12 py-3 mb-6">
|
<footer className="w-full mt-12 py-3 mb-6 px-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-3 space-x-4 text-sm text-neutral-600 select-none">
|
||||||
<div className="whitespace-nowrap">
|
<div className="flex items-center space-x-2 whitespace-nowrap">
|
||||||
Pangolin
|
<span>Pangolin</span>
|
||||||
</div>
|
</div>
|
||||||
<Separator orientation="vertical" />
|
<Separator orientation="vertical" />
|
||||||
<div className="whitespace-nowrap">
|
<div className="whitespace-nowrap">
|
||||||
|
@ -60,7 +62,7 @@ export default async function RootLayout({
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
fill="currentColor"
|
fill="currentColor"
|
||||||
className="w-4 h-4"
|
className="w-3 h-3"
|
||||||
>
|
>
|
||||||
<path d="M12 0C5.37 0 0 5.373 0 12c0 5.303 3.438 9.8 8.207 11.385.6.11.82-.26.82-.577v-2.17c-3.338.726-4.042-1.61-4.042-1.61-.546-1.385-1.333-1.755-1.333-1.755-1.09-.744.082-.73.082-.73 1.205.085 1.84 1.24 1.84 1.24 1.07 1.835 2.807 1.305 3.492.997.107-.775.42-1.305.763-1.605-2.665-.305-5.467-1.335-5.467-5.93 0-1.31.468-2.382 1.236-3.22-.123-.303-.535-1.523.117-3.176 0 0 1.008-.322 3.3 1.23a11.52 11.52 0 013.006-.403c1.02.005 2.045.137 3.006.403 2.29-1.552 3.295-1.23 3.295-1.23.654 1.653.242 2.873.12 3.176.77.838 1.235 1.91 1.235 3.22 0 4.605-2.805 5.623-5.475 5.92.43.37.814 1.1.814 2.22v3.293c0 .32.217.693.825.576C20.565 21.795 24 17.298 24 12 24 5.373 18.627 0 12 0z" />
|
<path d="M12 0C5.37 0 0 5.373 0 12c0 5.303 3.438 9.8 8.207 11.385.6.11.82-.26.82-.577v-2.17c-3.338.726-4.042-1.61-4.042-1.61-.546-1.385-1.333-1.755-1.333-1.755-1.09-.744.082-.73.082-.73 1.205.085 1.84 1.24 1.84 1.24 1.07 1.835 2.807 1.305 3.492.997.107-.775.42-1.305.763-1.605-2.665-.305-5.467-1.335-5.467-5.93 0-1.31.468-2.382 1.236-3.22-.123-.303-.535-1.523.117-3.176 0 0 1.008-.322 3.3 1.23a11.52 11.52 0 013.006-.403c1.02.005 2.045.137 3.006.403 2.29-1.552 3.295-1.23 3.295-1.23.654 1.653.242 2.873.12 3.176.77.838 1.235 1.91 1.235 3.22 0 4.605-2.805 5.623-5.475 5.92.43.37.814 1.1.814 2.22v3.293c0 .32.217.693.825.576C20.565 21.795 24 17.298 24 12 24 5.373 18.627 0 12 0z" />
|
||||||
</svg>
|
</svg>
|
||||||
|
@ -70,10 +72,11 @@ export default async function RootLayout({
|
||||||
href="https://docs.fossorial.io/Pangolin/overview"
|
href="https://docs.fossorial.io/Pangolin/overview"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
aria-label="GitHub"
|
aria-label="Documentation"
|
||||||
className="flex items-center space-x-3 whitespace-nowrap"
|
className="flex items-center space-x-3 whitespace-nowrap"
|
||||||
>
|
>
|
||||||
<span>Docs</span>
|
<span>Documentation</span>
|
||||||
|
<BookOpenText className="w-3 h-3" />
|
||||||
</a>
|
</a>
|
||||||
{version && (
|
{version && (
|
||||||
<>
|
<>
|
||||||
|
|
|
@ -11,6 +11,7 @@ import { redirect } from "next/navigation";
|
||||||
import { cache } from "react";
|
import { cache } from "react";
|
||||||
import OrganizationLanding from "./components/OrganizationLanding";
|
import OrganizationLanding from "./components/OrganizationLanding";
|
||||||
import { pullEnv } from "@app/lib/pullEnv";
|
import { pullEnv } from "@app/lib/pullEnv";
|
||||||
|
import { cleanRedirect } from "@app/lib/cleanRedirect";
|
||||||
|
|
||||||
export const dynamic = "force-dynamic";
|
export const dynamic = "force-dynamic";
|
||||||
|
|
||||||
|
@ -29,7 +30,8 @@ export default async function Page(props: {
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
if (params.redirect) {
|
if (params.redirect) {
|
||||||
redirect(`/auth/login?redirect=${params.redirect}`);
|
const safe = cleanRedirect(params.redirect);
|
||||||
|
redirect(`/auth/login?redirect=${safe}`);
|
||||||
} else {
|
} else {
|
||||||
redirect(`/auth/login`);
|
redirect(`/auth/login`);
|
||||||
}
|
}
|
||||||
|
@ -40,7 +42,8 @@ export default async function Page(props: {
|
||||||
env.flags.emailVerificationRequired
|
env.flags.emailVerificationRequired
|
||||||
) {
|
) {
|
||||||
if (params.redirect) {
|
if (params.redirect) {
|
||||||
redirect(`/auth/verify-email?redirect=${params.redirect}`);
|
const safe = cleanRedirect(params.redirect);
|
||||||
|
redirect(`/auth/verify-email?redirect=${safe}`);
|
||||||
} else {
|
} else {
|
||||||
redirect(`/auth/verify-email`);
|
redirect(`/auth/verify-email`);
|
||||||
}
|
}
|
||||||
|
@ -80,6 +83,7 @@ export default async function Page(props: {
|
||||||
|
|
||||||
<div className="w-full max-w-md mx-auto md:mt-32 mt-4">
|
<div className="w-full max-w-md mx-auto md:mt-32 mt-4">
|
||||||
<OrganizationLanding
|
<OrganizationLanding
|
||||||
|
disableCreateOrg={env.flags.disableUserCreateOrg && !user.serverAdmin}
|
||||||
organizations={orgs.map((org) => ({
|
organizations={orgs.map((org) => ({
|
||||||
name: org.name,
|
name: org.name,
|
||||||
id: org.orgId
|
id: org.orgId
|
||||||
|
|
|
@ -41,7 +41,7 @@ import Image from 'next/image'
|
||||||
|
|
||||||
type LoginFormProps = {
|
type LoginFormProps = {
|
||||||
redirect?: string;
|
redirect?: string;
|
||||||
onLogin?: () => void;
|
onLogin?: () => void | Promise<void>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const formSchema = z.object({
|
const formSchema = z.object({
|
||||||
|
|
18
src/lib/cleanRedirect.ts
Normal file
18
src/lib/cleanRedirect.ts
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
type PatternConfig = {
|
||||||
|
name: string;
|
||||||
|
regex: RegExp;
|
||||||
|
};
|
||||||
|
|
||||||
|
const patterns: PatternConfig[] = [
|
||||||
|
{ name: "Invite Token", regex: /^\/invite\?token=[a-zA-Z0-9-]+$/ },
|
||||||
|
{ name: "Setup", regex: /^\/setup$/ },
|
||||||
|
{ name: "Resource Auth Portal", regex: /^\/auth\/resource\/\d+$/ }
|
||||||
|
];
|
||||||
|
|
||||||
|
export function cleanRedirect(input: string): string {
|
||||||
|
if (!input || typeof input !== "string") {
|
||||||
|
return "/";
|
||||||
|
}
|
||||||
|
const isAccepted = patterns.some((pattern) => pattern.regex.test(input));
|
||||||
|
return isAccepted ? input : "/";
|
||||||
|
}
|
Loading…
Reference in a new issue