feat: add devMode configuration to runtime settings and update LoginPage for theme management

This commit is contained in:
Linus Rath
2026-03-11 16:01:31 +01:00
parent 40e7fa3776
commit d09022378b
3 changed files with 251 additions and 180 deletions
+70 -4
View File
@@ -7,19 +7,23 @@ import { useTranslations } from "next-intl";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { useAuthStore } from "@/stores/auth-store";
import { useThemeStore } from "@/stores/theme-store";
import { useConfig } from "@/hooks/use-config";
import { cn } from "@/lib/utils";
import { Mail, AlertCircle, Loader2, X, Info, Eye, EyeOff, LogIn } from "lucide-react";
import { Mail, AlertCircle, Loader2, X, Info, Eye, EyeOff, LogIn, Sun, Moon, Monitor } from "lucide-react";
import { discoverOAuth, type OAuthMetadata } from "@/lib/oauth/discovery";
import { generateCodeVerifier, generateCodeChallenge, generateState } from "@/lib/oauth/pkce";
import { OAUTH_SCOPES } from "@/lib/oauth/tokens";
const APP_VERSION = "1.1.2";
export default function LoginPage() {
const router = useRouter();
const t = useTranslations("login");
const params = useParams();
const { login, isLoading, error, clearError, isAuthenticated } = useAuthStore();
const { appName, jmapServerUrl: serverUrl, oauthEnabled, oauthClientId, oauthIssuerUrl, rememberMeEnabled, isLoading: configLoading, error: configError } = useConfig();
const { theme, setTheme, initializeTheme } = useThemeStore();
const { appName, jmapServerUrl: serverUrl, oauthEnabled, oauthClientId, oauthIssuerUrl, rememberMeEnabled, devMode, isLoading: configLoading, error: configError } = useConfig();
const [formData, setFormData] = useState({
username: "",
@@ -46,6 +50,10 @@ export default function LoginPage() {
const totpInputRef = useRef<HTMLInputElement>(null);
const prevError = useRef<string | null>(null);
useEffect(() => {
initializeTheme();
}, [initializeTheme]);
useEffect(() => {
if (serverUrl) {
document.title = appName;
@@ -295,8 +303,34 @@ export default function LoginPage() {
}
};
const handleDevLogin = async () => {
const success = await login(serverUrl, "dev@localhost", "dev");
if (success) {
router.push('/');
}
};
const themeIcon = theme === 'light' ? Sun : theme === 'dark' ? Moon : Monitor;
const ThemeIcon = themeIcon;
return (
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-background via-background to-muted/20">
<div className="min-h-screen flex flex-col items-center justify-center bg-gradient-to-br from-background via-background to-muted/20 relative">
{/* Theme toggle - top right */}
<div className="absolute top-4 right-4">
<button
type="button"
onClick={() => {
const next = theme === 'light' ? 'dark' : theme === 'dark' ? 'system' : 'light';
setTheme(next);
}}
className="p-2.5 rounded-lg bg-secondary/60 hover:bg-secondary border border-border/50 text-muted-foreground hover:text-foreground transition-colors"
aria-label={`Theme: ${theme}`}
title={`Theme: ${theme}`}
>
<ThemeIcon className="w-4 h-4" />
</button>
</div>
<div className="w-full max-w-sm mx-auto px-4">
{/* Logo */}
<div className="text-center mb-12">
@@ -342,7 +376,33 @@ export default function LoginPage() {
</div>
)}
{/* Login Form */}
{/* Dev Mode: One-click login */}
{devMode ? (
<div className="space-y-4">
<Button
type="button"
className="w-full h-12 font-medium text-base bg-primary hover:bg-primary/90 transition-all duration-200 shadow-lg shadow-primary/20"
onClick={handleDevLogin}
disabled={isLoading}
>
{isLoading ? (
<div className="flex items-center gap-2">
<Loader2 className="w-4 h-4 animate-spin" />
{t("signing_in")}
</div>
) : (
<div className="flex items-center gap-2">
<LogIn className="w-4 h-4" />
{t("sign_in")}
</div>
)}
</Button>
<p className="text-center text-xs text-muted-foreground">
Dev mode logging in as dev@localhost
</p>
</div>
) : (
/* Login Form */
<form
onSubmit={handleSubmit}
className={cn("space-y-4", shakeError && "animate-shake")}
@@ -525,6 +585,12 @@ export default function LoginPage() {
</div>
)}
</form>
)}
</div>
{/* Version number - bottom center */}
<div className="absolute bottom-4 text-xs text-muted-foreground/50">
v{APP_VERSION}
</div>
</div>
);
+1
View File
@@ -22,5 +22,6 @@ export async function GET() {
oauthClientId: process.env.OAUTH_CLIENT_ID || '',
oauthIssuerUrl: process.env.OAUTH_ISSUER_URL || '',
rememberMeEnabled: !!process.env.SESSION_SECRET,
devMode: process.env.DEV_MOCK_JMAP === 'true',
});
}
+4
View File
@@ -9,6 +9,7 @@ interface ConfigData {
oauthClientId: string;
oauthIssuerUrl: string;
rememberMeEnabled: boolean;
devMode: boolean;
}
interface AppConfig extends ConfigData {
@@ -65,6 +66,7 @@ export function useConfig(): AppConfig {
oauthClientId: configCache?.oauthClientId || '',
oauthIssuerUrl: configCache?.oauthIssuerUrl || '',
rememberMeEnabled: configCache?.rememberMeEnabled || false,
devMode: configCache?.devMode || false,
isLoading: !configCache,
error: null,
});
@@ -79,6 +81,7 @@ export function useConfig(): AppConfig {
oauthClientId: configCache.oauthClientId,
oauthIssuerUrl: configCache.oauthIssuerUrl,
rememberMeEnabled: configCache.rememberMeEnabled,
devMode: configCache.devMode,
isLoading: false,
error: null,
});
@@ -94,6 +97,7 @@ export function useConfig(): AppConfig {
oauthClientId: data.oauthClientId,
oauthIssuerUrl: data.oauthIssuerUrl,
rememberMeEnabled: data.rememberMeEnabled,
devMode: data.devMode,
isLoading: false,
error: null,
});