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
22 changes: 20 additions & 2 deletions packages/ui/src/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -444,7 +444,7 @@ function MasterSliders({
export default function Home() {
const [configRev, setConfigRev] = useState(0);
const config = useConfig(configRev);
const { user, token, checked, login } = useAuth();
const { user, token, checked, endedSession, lastUser, login, sessionEnded } = useAuth();
const { connection, grid, orientation, playlistState, settings, send } = useSocket(
config?.simulatorUrl ?? null,
token,
Expand Down Expand Up @@ -490,6 +490,14 @@ export default function Home() {
});
}, []);

// A token the server no longer accepts can only reconnect into the same
// error, so stop retrying against it and hand the operator the login screen.
useEffect(() => {
if (token && connection.state === 'down' && connection.cause === 'sessionExpired') {
sessionEnded();
}
}, [token, connection.state, connection.cause, sessionEnded]);

// Sync slider state from server on initial connect
const settingsSyncedRef = useRef(false);
useEffect(() => {
Expand Down Expand Up @@ -773,7 +781,17 @@ export default function Home() {
}

if (!user) {
return <LoginScreen onLogin={login} />;
return (
<LoginScreen
onLogin={login}
defaultUsername={lastUser}
notice={
endedSession
? 'Your session ended — it expired, was revoked, or the show switched project.'
: undefined
}
/>
);
}

/* ---------- PHONE LAYOUT ---------- */
Expand Down
15 changes: 13 additions & 2 deletions packages/ui/src/components/login-screen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,14 @@ import { FormEvent, useState } from 'react';

interface LoginScreenProps {
onLogin: (username: string, token: string) => void;
/** Why this screen appeared, when it wasn't the operator's own sign-out. */
notice?: string;
/** Prefilled username — whoever was signed in on this device last. */
defaultUsername?: string;
}

export function LoginScreen({ onLogin }: LoginScreenProps) {
const [username, setUsername] = useState('');
export function LoginScreen({ onLogin, notice, defaultUsername = '' }: LoginScreenProps) {
const [username, setUsername] = useState(defaultUsername);
const [password, setPassword] = useState('');
const [error, setError] = useState('');
const [loading, setLoading] = useState(false);
Expand Down Expand Up @@ -59,6 +63,13 @@ export function LoginScreen({ onLogin }: LoginScreenProps) {
</p>
</div>

{/* Why we're back here (session expired, revoked, or project switched) */}
{notice && (
<p className='text-center text-sm' style={{ color: '#d9a441' }}>
{notice}
</p>
)}

{/* Username */}
<input
type="text"
Expand Down
23 changes: 21 additions & 2 deletions packages/ui/src/lib/use-auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,17 @@ function decodePayload(token: string): { sub: string } | null {
}
}

/** Last username signed in on this device — prefilled after a session ends so
* getting back in is one field, not two. */
const LAST_USER_KEY = 'wg_last_user';

export function useAuth() {
const [user, setUser] = useState<string | null>(null);
const [token, setToken] = useState<string | null>(null);
const [checked, setChecked] = useState(false);
/** Set when a signed-in session stopped being valid, so the login screen can
* say why it is showing rather than looking like a random logout. */
const [endedSession, setEndedSession] = useState(false);

useEffect(() => {
const stored = localStorage.getItem('wg_token');
Expand All @@ -36,17 +43,29 @@ export function useAuth() {

const login = useCallback((username: string, jwt: string) => {
localStorage.setItem('wg_token', jwt);
localStorage.setItem(LAST_USER_KEY, username);
localStorage.removeItem('wg_user');
setToken(jwt);
setUser(username);
setEndedSession(false);
}, []);

const logout = useCallback(() => {
const clear = useCallback((ended: boolean) => {
localStorage.removeItem('wg_token');
localStorage.removeItem('wg_user');
setToken(null);
setUser(null);
setEndedSession(ended);
}, []);

return { user, token, checked, login, logout };
const logout = useCallback(() => clear(false), [clear]);

/** Drop a token the server no longer accepts (expired, revoked, or issued for
* another project) and fall back to the login screen — a dead token can only
* ever reconnect into the same error. */
const sessionEnded = useCallback(() => clear(true), [clear]);

const lastUser = typeof window === 'undefined' ? '' : localStorage.getItem(LAST_USER_KEY) ?? '';

return { user, token, checked, endedSession, lastUser, login, logout, sessionEnded };
}
Loading