Update environment configuration and enhance Docker setup for production deployment

- Changed NODE_ENV from development to production in .env.example for production readiness.
- Updated WEBUI_URL to point to the new public URL behind Traefik.
- Added internal and external networks in docker-compose.yml for improved service isolation and routing.
- Removed direct port exposure for the WebUI, ensuring it is only accessible through Traefik.
- Refactored landing and login pages in the WebUI to streamline navigation and error handling, redirecting users appropriately based on session status.
This commit is contained in:
TheOnlyMace
2026-07-22 18:53:53 +02:00
parent 08af99ed52
commit 496a8239a5
7 changed files with 223 additions and 160 deletions

View File

@@ -1,52 +1,12 @@
import { redirect } from 'next/navigation';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { getLocale, t } from '@/lib/i18n';
import { getSession } from '@/lib/session';
interface LoginPageProps {
searchParams: Promise<{ error?: string }>;
}
const KNOWN_ERRORS = ['access_denied', 'invalid_request', 'invalid_state', 'oauth_failed'] as const;
/** Alias for `/` so OAuth error redirects to `/login?error=…` keep working. */
export default async function LoginPage({ searchParams }: LoginPageProps) {
const session = await getSession();
if (session) {
redirect('/dashboard');
}
const { error } = await searchParams;
const locale = await getLocale();
const errorMessage =
error && (KNOWN_ERRORS as readonly string[]).includes(error)
? t(locale, `login.errors.${error}`)
: undefined;
return (
<main className="flex min-h-screen items-center justify-center px-4">
<Card className="w-full max-w-sm">
<CardHeader className="items-center text-center">
<div className="mb-2 flex size-12 items-center justify-center rounded-xl bg-primary text-lg font-semibold text-primary-foreground">
N
</div>
<CardTitle className="text-xl">{t(locale, 'login.title')}</CardTitle>
<CardDescription>{t(locale, 'login.subtitle')}</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
{errorMessage && (
<p className="rounded-md border border-destructive/30 bg-destructive/10 px-3 py-2 text-sm text-destructive">
{errorMessage}
</p>
)}
<a
href="/api/auth/login"
className="inline-flex h-10 w-full items-center justify-center gap-2 rounded-md bg-primary text-sm font-medium text-primary-foreground transition-colors hover:bg-primary/90"
>
{t(locale, 'login.cta')}
</a>
<p className="text-center text-xs text-muted-foreground">{t(locale, 'login.footer')}</p>
</CardContent>
</Card>
</main>
);
const target = error ? `/?error=${encodeURIComponent(error)}` : '/';
redirect(target);
}

View File

@@ -1,111 +1,17 @@
import { DASHBOARD_MODULES, type DashboardModuleGroup } from '@nexumi/shared';
import Image from 'next/image';
import Link from 'next/link';
import { SiteShell } from '@/components/site/site-shell';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { buildBotInviteUrl, env } from '@/lib/env';
import { getLocale, t } from '@/lib/i18n';
import { getPublicStats } from '@/lib/public-stats';
import { redirect } from 'next/navigation';
import { LoginCard } from '@/components/auth/login-card';
import { getSession } from '@/lib/session';
const GROUP_ORDER: DashboardModuleGroup[] = [
'moderation',
'engagement',
'community',
'utility',
'integrations'
];
export default async function LandingPage() {
const [locale, stats, session] = await Promise.all([getLocale(), getPublicStats(), getSession()]);
const inviteUrl = buildBotInviteUrl();
const isLoggedIn = Boolean(session);
return (
<SiteShell>
<section className="border-b border-border">
<div className="mx-auto flex w-full max-w-[1200px] flex-col gap-8 px-6 py-16 sm:py-20">
<div className="flex items-center gap-3">
<Image src="/nexumi-logo.svg" alt="" width={48} height={48} priority />
<h1 className="text-4xl font-semibold tracking-tight sm:text-5xl">Nexumi</h1>
</div>
<p className="max-w-2xl text-lg text-muted-foreground">{t(locale, 'landing.hero.subtitle')}</p>
<div className="flex flex-wrap gap-3">
<Button asChild size="lg">
<a href={inviteUrl} target="_blank" rel="noreferrer">
{t(locale, 'landing.hero.invite')}
</a>
</Button>
<Button asChild size="lg" variant="secondary">
<Link href={isLoggedIn ? '/dashboard' : '/login'}>
{isLoggedIn ? t(locale, 'landing.hero.dashboard') : t(locale, 'landing.hero.login')}
</Link>
</Button>
{env.SUPPORT_SERVER_URL ? (
<Button asChild size="lg" variant="outline">
<a href={env.SUPPORT_SERVER_URL} target="_blank" rel="noreferrer">
{t(locale, 'landing.hero.support')}
</a>
</Button>
) : null}
</div>
</div>
</section>
<section className="border-b border-border">
<div className="mx-auto grid w-full max-w-[1200px] gap-4 px-6 py-12 sm:grid-cols-2">
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium text-muted-foreground">
{t(locale, 'landing.stats.guilds')}
</CardTitle>
</CardHeader>
<CardContent className="text-3xl font-semibold">{stats.guildCount.toLocaleString(locale)}</CardContent>
</Card>
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium text-muted-foreground">
{t(locale, 'landing.stats.users')}
</CardTitle>
</CardHeader>
<CardContent className="text-3xl font-semibold">
{stats.approximateUserCount.toLocaleString(locale)}
</CardContent>
</Card>
</div>
</section>
<section>
<div className="mx-auto w-full max-w-[1200px] space-y-8 px-6 py-12">
<div>
<h2 className="text-2xl font-semibold">{t(locale, 'landing.features.title')}</h2>
<p className="mt-2 text-muted-foreground">{t(locale, 'landing.features.subtitle')}</p>
</div>
{GROUP_ORDER.map((group) => {
const modules = DASHBOARD_MODULES.filter((module) => module.group === group);
return (
<div key={group} className="space-y-3">
<h3 className="text-sm font-semibold uppercase tracking-wider text-muted-foreground">
{t(locale, `moduleGroups.${group}`)}
</h3>
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
{modules.map((module) => (
<Card key={module.id}>
<CardHeader className="pb-2">
<CardTitle className="text-base">{t(locale, `modules.${module.id}.label`)}</CardTitle>
</CardHeader>
<CardContent className="text-sm text-muted-foreground">
{t(locale, `modules.${module.id}.description`)}
</CardContent>
</Card>
))}
</div>
</div>
);
})}
</div>
</section>
</SiteShell>
);
interface HomePageProps {
searchParams: Promise<{ error?: string }>;
}
export default async function HomePage({ searchParams }: HomePageProps) {
const session = await getSession();
if (session) {
redirect('/dashboard');
}
const { error } = await searchParams;
return <LoginCard error={error} />;
}