Merge branch 'dev' into feat/login-page-customization

This commit is contained in:
Fred KISSIE
2025-12-05 22:38:07 +01:00
275 changed files with 21920 additions and 6990 deletions

View File

@@ -154,7 +154,7 @@ export default async function OrgAuthPage(props: {
</Link>
</span>
</div>
<Card className="shadow-md w-full max-w-md">
<Card className="w-full max-w-md">
<CardHeader>
{branding?.logoUrl && (
<div className="flex flex-row items-center justify-center mb-3">

View File

@@ -1,5 +1,13 @@
import ThemeSwitcher from "@app/components/ThemeSwitcher";
import { Separator } from "@app/components/ui/separator";
import { priv } from "@app/lib/api";
import { pullEnv } from "@app/lib/pullEnv";
import { build } from "@server/build";
import { GetLicenseStatusResponse } from "@server/routers/license/types";
import { AxiosResponse } from "axios";
import { Metadata } from "next";
import { getTranslations } from "next-intl/server";
import { cache } from "react";
export const metadata: Metadata = {
title: `Auth - ${process.env.BRANDING_APP_NAME || "Pangolin"}`,
@@ -11,15 +19,117 @@ type AuthLayoutProps = {
};
export default async function AuthLayout({ children }: AuthLayoutProps) {
const env = pullEnv();
const t = await getTranslations();
let hideFooter = false;
if (build == "enterprise") {
const licenseStatusRes = await cache(
async () =>
await priv.get<AxiosResponse<GetLicenseStatusResponse>>(
"/license/status"
)
)();
if (
env.branding.hideAuthLayoutFooter &&
licenseStatusRes.data.data.isHostLicensed &&
licenseStatusRes.data.data.isLicenseValid
) {
hideFooter = true;
}
}
return (
<div className="h-full flex flex-col">
<div className="flex justify-end items-center p-3 space-x-2">
<ThemeSwitcher />
</div>
<div className="flex-1 flex items-center justify-center">
<div className="flex-1 flex md:items-center justify-center">
<div className="w-full max-w-md p-3">{children}</div>
</div>
{!hideFooter && (
<footer className="hidden md:block w-full mt-12 py-3 mb-6 px-4">
<div className="container mx-auto flex flex-wrap justify-center items-center h-3 space-x-4 text-xs text-neutral-400 dark:text-neutral-600">
<a
href="https://pangolin.net"
target="_blank"
rel="noopener noreferrer"
aria-label="Built by Fossorial"
className="flex items-center space-x-2 whitespace-nowrap"
>
<span>
© {new Date().getFullYear()} Fossorial, Inc.
</span>
</a>
<Separator orientation="vertical" />
<a
href="https://pangolin.net"
target="_blank"
rel="noopener noreferrer"
aria-label="Built by Fossorial"
className="flex items-center space-x-2 whitespace-nowrap"
>
<span>
{process.env.BRANDING_APP_NAME || "Pangolin"}
</span>
</a>
<Separator orientation="vertical" />
<span>
{build === "oss"
? t("communityEdition")
: build === "enterprise"
? t("enterpriseEdition")
: t("pangolinCloud")}
</span>
{build === "saas" && (
<>
<Separator orientation="vertical" />
<a
href="https://pangolin.net/terms-of-service.html"
target="_blank"
rel="noopener noreferrer"
aria-label="GitHub"
className="flex items-center space-x-2 whitespace-nowrap"
>
<span>{t("terms")}</span>
</a>
<Separator orientation="vertical" />
<a
href="https://pangolin.net/privacy-policy.html"
target="_blank"
rel="noopener noreferrer"
aria-label="GitHub"
className="flex items-center space-x-2 whitespace-nowrap"
>
<span>{t("privacy")}</span>
</a>
</>
)}
<Separator orientation="vertical" />
<a
href="https://docs.pangolin.net"
target="_blank"
rel="noopener noreferrer"
aria-label="Built by Fossorial"
className="flex items-center space-x-2 whitespace-nowrap"
>
<span>{t("docs")}</span>
</a>
<Separator orientation="vertical" />
<a
href="https://github.com/fosrl/pangolin"
target="_blank"
rel="noopener noreferrer"
aria-label="GitHub"
className="flex items-center space-x-2 whitespace-nowrap"
>
<span>{t("github")}</span>
</a>
</div>
</footer>
)}
</div>
);
}

View File

@@ -0,0 +1,34 @@
import { verifySession } from "@app/lib/auth/verifySession";
import { redirect } from "next/navigation";
import DeviceLoginForm from "@/components/DeviceLoginForm";
import { cache } from "react";
export const dynamic = "force-dynamic";
type Props = {
searchParams: Promise<{ code?: string }>;
};
export default async function DeviceLoginPage({ searchParams }: Props) {
const user = await verifySession({ forceLogin: true });
const params = await searchParams;
const code = params.code || "";
if (!user) {
const redirectDestination = code
? `/auth/login/device?code=${encodeURIComponent(code)}`
: "/auth/login/device";
redirect(`/auth/login?forceLogin=true&redirect=${encodeURIComponent(redirectDestination)}`);
}
const userName = user?.name || user?.username || "";
return (
<DeviceLoginForm
userEmail={user?.email || ""}
userName={userName}
initialCode={code}
/>
);
}

View File

@@ -0,0 +1,47 @@
"use client";
import { Card, CardContent, CardHeader } from "@/components/ui/card";
import BrandingLogo from "@app/components/BrandingLogo";
import { useEnvContext } from "@app/hooks/useEnvContext";
import { useLicenseStatusContext } from "@app/hooks/useLicenseStatusContext";
import { CheckCircle2 } from "lucide-react";
import { useTranslations } from "next-intl";
export default function DeviceAuthSuccessPage() {
const { env } = useEnvContext();
const { isUnlocked } = useLicenseStatusContext();
const t = useTranslations();
const logoWidth = isUnlocked()
? env.branding.logo?.authPage?.width || 175
: 175;
const logoHeight = isUnlocked()
? env.branding.logo?.authPage?.height || 58
: 58;
return (
<Card>
<CardHeader className="border-b">
<div className="flex flex-row items-center justify-center">
<BrandingLogo height={logoHeight} width={logoWidth} />
</div>
<div className="text-center space-y-1 pt-3">
<p className="text-muted-foreground">{t("deviceActivation")}</p>
</div>
</CardHeader>
<CardContent className="p-6">
<div className="flex flex-col items-center space-y-4">
<CheckCircle2 className="h-12 w-12 text-green-500" />
<div className="space-y-2">
<h3 className="text-xl font-bold text-center">
{t("deviceConnected")}
</h3>
<p className="text-center text-sm text-muted-foreground">
{t("deviceAuthorizedMessage")}
</p>
</div>
</div>
</CardContent>
</Card>
);
}

View File

@@ -25,12 +25,14 @@ export default async function Page(props: {
const user = await getUser({ skipCheckVerifyEmail: true });
const isInvite = searchParams?.redirect?.includes("/invite");
const forceLoginParam = searchParams?.forceLogin;
const forceLogin = forceLoginParam === "true";
const env = pullEnv();
const signUpDisabled = env.flags.disableSignupWithoutInvite;
if (user) {
if (user && !forceLogin) {
redirect("/");
}
@@ -53,7 +55,7 @@ export default async function Page(props: {
if (loginPageDomain) {
const redirectUrl = searchParams.redirect as string | undefined;
let url = `https://${loginPageDomain}/auth/org`;
if (redirectUrl) {
url += `?redirect=${redirectUrl}`;
@@ -96,7 +98,7 @@ export default async function Page(props: {
</div>
)}
<DashboardLoginForm redirect={redirectUrl} idps={loginIdps} />
<DashboardLoginForm redirect={redirectUrl} idps={loginIdps} forceLogin={forceLogin} />
{(!signUpDisabled || isInvite) && (
<p className="text-center text-muted-foreground mt-4">