Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion apps/web/src/app/api/auth/discord/callback/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export async function GET(request: NextRequest) {

const cookieStore = await cookies();
const storedState = cookieStore.get("oauth_state")?.value;
const returnTo = safeRelativePath(cookieStore.get("oauth_return_to")?.value, "/dashboard");
const returnToCookie = cookieStore.get("oauth_return_to")?.value;

// Clear the one-shot CSRF cookies regardless of outcome.
cookieStore.delete("oauth_state");
Expand All @@ -40,6 +40,8 @@ export async function GET(request: NextRequest) {
const host = await requestHostname();
const onCustomDomain = Boolean(host) && !isPrimaryHostname(host!);
const base = onCustomDomain ? `https://${host}` : appBaseUrl();
// The dashboard is primary-only; default custom-domain logins to the guild home.
const returnTo = safeRelativePath(returnToCookie, onCustomDomain ? "/" : "/dashboard");
const errorUrl = new URL("/?auth=error", base);

if (!code || !state || !storedState || state !== storedState) {
Expand Down
13 changes: 10 additions & 3 deletions apps/web/src/app/api/auth/discord/login/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,19 @@ export const dynamic = "force-dynamic";
export async function GET(request: NextRequest) {
const state = crypto.randomUUID();

const host = await requestHostname();
const onCustomDomain = host != null && !isPrimaryHostname(host);

// Same-origin relative redirect only (rejects "//", backslashes, control chars).
const returnTo = safeRelativePath(request.nextUrl.searchParams.get("returnTo"), "/dashboard");
// The dashboard lives only on the primary host, so on a custom domain default
// to the guild's branded home ("/") instead of "/dashboard".
const returnTo = safeRelativePath(
request.nextUrl.searchParams.get("returnTo"),
onCustomDomain ? "/" : "/dashboard",
);

const host = await requestHostname();
let app: { clientId: string; redirectUri: string } | undefined;
if (host && !isPrimaryHostname(host)) {
if (onCustomDomain) {
const oauth = await resolveHostOAuth(host);
if (!oauth) {
// No usable per-guild OAuth app for this custom domain — fall back to the
Expand Down
26 changes: 25 additions & 1 deletion apps/web/src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import { SiteFooter } from "@/components/site-footer";
import { SiteHeader } from "@/components/site-header";
import { ThemeProvider } from "@/components/theme-provider";
import { getCurrentUser } from "@/lib/auth";
import { logoUrl, parseBranding } from "@/lib/branding";
import { getGuildByDomain, isPrimaryHostname, requestHostname } from "@/lib/custom-domain";
import { getDirection, getLocale } from "@/i18n";
import "./globals.css";

Expand All @@ -28,7 +30,7 @@ const spaceMono = Space_Mono({
display: "swap",
});

export const metadata: Metadata = {
const DEFAULT_METADATA: Metadata = {
title: "MSK Forms: application forms with a status loop",
description:
"Build application forms, collect submissions, and let applicants track their status live. With a Discord bot any server can invite.",
Expand All @@ -38,6 +40,28 @@ export const metadata: Metadata = {
},
};

/**
* Host-aware metadata: on a guild's verified custom domain, use that guild's
* name and (if set) its logo as the browser favicon, so the tab looks like the
* guild's own site. Falls back to the MSK Forms defaults everywhere else.
*/
export async function generateMetadata(): Promise<Metadata> {
const host = await requestHostname();
if (!host || isPrimaryHostname(host)) return DEFAULT_METADATA;

const guild = await getGuildByDomain(host);
if (!guild) return DEFAULT_METADATA;

const logo = logoUrl(guild.id, parseBranding(guild.branding));
return {
...DEFAULT_METADATA,
title: guild.name,
...(logo
? { icons: { icon: [{ url: logo, type: "image/webp" }], apple: logo } }
: {}),
};
}

export default async function RootLayout({
children,
}: Readonly<{ children: React.ReactNode }>) {
Expand Down