From 7b1ea887397720c13a947eecca7dae3e82c44d34 Mon Sep 17 00:00:00 2001 From: Lee Jackson <130007945+Imagineer99@users.noreply.github.com> Date: Sun, 15 Mar 2026 23:02:58 +0000 Subject: [PATCH] studio: simplify auth UX to password-only login (#4305) * feat(studio): switch to password-only login and simplify first-time setup * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * fix: align change-password button state with validation rules --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Roland Tannous <115670425+rolandtannous@users.noreply.github.com> --- .../features/auth/components/auth-form.tsx | 202 +++++++++++------- 1 file changed, 122 insertions(+), 80 deletions(-) diff --git a/studio/frontend/src/features/auth/components/auth-form.tsx b/studio/frontend/src/features/auth/components/auth-form.tsx index 4433f29bd5..56ff0a8c2b 100644 --- a/studio/frontend/src/features/auth/components/auth-form.tsx +++ b/studio/frontend/src/features/auth/components/auth-form.tsx @@ -7,8 +7,8 @@ import { Label } from "@/components/ui/label"; import { Link, useNavigate } from "@tanstack/react-router"; import { Eye, EyeOff } from "lucide-react"; import { useEffect, useState } from "react"; -import type { FormEvent } from "react"; import type { ReactElement } from "react"; +import type { SyntheticEvent } from "react"; import { refreshSession } from "../api"; // Bootstrap credentials injected into index.html by the backend @@ -35,7 +35,6 @@ type AuthMode = "login" | "change-password"; type AuthStatusResponse = { initialized: boolean; - default_username: string; requires_password_change: boolean; }; @@ -72,13 +71,16 @@ type AuthFormProps = { mode: AuthMode; }; +const HIDDEN_LOGIN_USERNAME = "unsloth"; + export function AuthForm({ mode }: AuthFormProps): ReactElement | null { const navigate = useNavigate(); const isLoginMode = mode === "login"; const [showPassword, setShowPassword] = useState(false); - const [username, setUsername] = useState("unsloth"); + const username = HIDDEN_LOGIN_USERNAME; const [password, setPassword] = useState(""); const [newPassword, setNewPassword] = useState(""); + const [confirmPassword, setConfirmPassword] = useState(""); const [loading, setLoading] = useState(false); const [statusLoading, setStatusLoading] = useState(true); const [initialized, setInitialized] = useState(null); @@ -98,7 +100,6 @@ export function AuthForm({ mode }: AuthFormProps): ReactElement | null { const result = (await response.json()) as AuthStatusResponse; if (!canceled) { setInitialized(result.initialized); - setUsername(result.default_username); setRequiresPasswordChange(result.requires_password_change); // Redirect between login ↔ change-password based on server state @@ -152,9 +153,6 @@ export function AuthForm({ mode }: AuthFormProps): ReactElement | null { if (!isLoginMode && !password) { setPassword(bootstrap.password); } - if (bootstrap.username) { - setUsername(bootstrap.username); - } } }, []); @@ -171,27 +169,46 @@ export function AuthForm({ mode }: AuthFormProps): ReactElement | null { } else if (!isLoginMode && !requiresPasswordChange && !mustChangePassword()) { helperText = "Password already updated. Use the login screen."; } - const title = isLoginMode ? "Welcome back" : "Update your admin password"; - const subtitle = isLoginMode - ? "Sign in with the seeded admin account" - : "Use the default admin credentials, then choose a new password"; + const title = isLoginMode ? "Welcome back" : "Setup your account"; + const subtitle = isLoginMode + ? "Sign in with your password." + : "Choose your new password."; const submitLabel = isLoginMode ? "Login" : "Change password"; const showSwitchLink = !isLoginMode; - const switchText = "Password already changed? "; + const switchText = "Password already setup? "; const switchLinkTo = "/login"; const switchLinkText = "Back to login"; + const currentPassword = password || window.__UNSLOTH_BOOTSTRAP__?.password || ""; + const invalidChangePasswordForm = + !isLoginMode && + (newPassword.length < 8 || newPassword !== confirmPassword || currentPassword === newPassword); + const showPasswordMismatchWarning = + !isLoginMode && + newPassword.length > 0 && + confirmPassword.length > 0 && + newPassword !== confirmPassword; - async function handleSubmit(event: FormEvent) { + async function handleSubmit(event: SyntheticEvent) { event.preventDefault(); setError(null); - if (!isLoginMode && newPassword.length < 8) { - setError("New password must be at least 8 characters."); - return; - } - if (!isLoginMode && password === newPassword) { - setError("New password must be different from the default password."); - return; + if (!isLoginMode) { + if (!currentPassword) { + setError("Unable to initialize setup. Reload the page and try again."); + return; + } + if (newPassword.length < 8) { + setError("New password must be at least 8 characters."); + return; + } + if (newPassword !== confirmPassword) { + setError("Passwords do not match."); + return; + } + if (currentPassword === newPassword) { + setError("New password must be different from your current password."); + return; + } } setLoading(true); @@ -213,7 +230,7 @@ export function AuthForm({ mode }: AuthFormProps): ReactElement | null { } if (!accessToken) { - const bootstrapToken = await loginWithPassword(username, password); + const bootstrapToken = await loginWithPassword(username, currentPassword); storeAuthTokens( bootstrapToken.access_token, bootstrapToken.refresh_token, @@ -230,7 +247,7 @@ export function AuthForm({ mode }: AuthFormProps): ReactElement | null { Authorization: `Bearer ${accessToken}`, }, body: JSON.stringify({ - current_password: password, + current_password: currentPassword, new_password: newPassword, }), }); @@ -281,65 +298,90 @@ export function AuthForm({ mode }: AuthFormProps): ReactElement | null {

{subtitle}

-
- - setUsername(event.target.value)} - required - disabled={!isLoginMode} - /> -
- -
- -
- setPassword(event.target.value)} - minLength={8} - required - /> - + {isLoginMode && ( +
+ +
+ setPassword(event.target.value)} + minLength={8} + required + /> + +
- {isLoginMode ? null : ( -

Confirm the seeded password before choosing a new one.

- )} -
+ )} {!isLoginMode && ( -
- - setNewPassword(event.target.value)} - minLength={8} - required - /> -

Must be at least 8 characters and different from the default password.

-
+ <> +
+ +
+ setNewPassword(event.target.value)} + minLength={8} + required + /> + +
+
+
+ + setConfirmPassword(event.target.value)} + minLength={8} + required + /> +
+

+ {showPasswordMismatchWarning + ? "Please ensure passwords match." + : "Must be at least 8 characters."} +

+ )} {helperText && ( @@ -354,8 +396,8 @@ export function AuthForm({ mode }: AuthFormProps): ReactElement | null { loading || statusLoading || blockedByState || - password.length < 8 || - (!isLoginMode && newPassword.length < 8) + (isLoginMode && password.length < 8) || + invalidChangePasswordForm } > {loading ? "Please wait..." : submitLabel}