complete web device auth flow

This commit is contained in:
miloschwartz
2025-11-03 11:10:17 -08:00
parent da0196a308
commit e888b76747
28 changed files with 1151 additions and 68 deletions

View File

@@ -22,6 +22,10 @@ export default async function OrgPage(props: OrgPageProps) {
const orgId = params.orgId;
const env = pullEnv();
if (!orgId) {
redirect(`/`);
}
const getUser = cache(verifySession);
const user = await getUser();

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 { verifySession } from "@app/lib/auth/verifySession";
import { pullEnv } from "@app/lib/pullEnv";
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,6 +19,20 @@ type AuthLayoutProps = {
};
export default async function AuthLayout({ children }: AuthLayoutProps) {
const getUser = cache(verifySession);
const env = pullEnv();
const user = await getUser();
const t = await getTranslations();
const hideFooter = env.branding.hideAuthLayoutFooter || false;
const licenseStatusRes = await cache(
async () =>
await priv.get<AxiosResponse<GetLicenseStatusResponse>>(
"/license/status"
)
)();
const licenseStatus = licenseStatusRes.data.data;
return (
<div className="h-full flex flex-col">
<div className="flex justify-end items-center p-3 space-x-2">
@@ -20,6 +42,91 @@ export default async function AuthLayout({ children }: AuthLayoutProps) {
<div className="flex-1 flex items-center justify-center">
<div className="w-full max-w-md p-3">{children}</div>
</div>
{!(
hideFooter ||
(licenseStatus.isHostLicensed && licenseStatus.isLicenseValid)
) && (
<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" />
<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://github.com/fosrl/pangolin/blob/main/SECURITY.md"
target="_blank"
rel="noopener noreferrer"
aria-label="GitHub"
className="flex items-center space-x-2 whitespace-nowrap"
>
<span>{t("security")}</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" />
<span>{t("communityEdition")}</span>
<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,17 @@
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";
export default async function DeviceLoginPage() {
const getUser = cache(verifySession);
const user = await getUser();
if (!user) {
redirect("/auth/login?redirect=/auth/login/device");
}
return <DeviceLoginForm userEmail={user?.email || ""} />;
}

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-16 w-16 text-green-600" />
<div className="space-y-2">
<h2 className="text-2xl font-bold text-center">
{t("deviceConnected")}
</h2>
<p className="text-center text-sm text-muted-foreground">
{t("deviceAuthorizedMessage")}
</p>
</div>
</div>
</CardContent>
</Card>
);
}

View File

@@ -53,7 +53,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}`;

View File

@@ -80,7 +80,7 @@ export default async function Page(props: {
const lastOrgCookie = allCookies.get("pangolin-last-org")?.value;
const lastOrgExists = orgs.some((org) => org.orgId === lastOrgCookie);
if (lastOrgExists) {
if (lastOrgExists && lastOrgCookie) {
redirect(`/${lastOrgCookie}`);
} else {
let ownedOrg = orgs.find((org) => org.isOwner);