feat(worker): login-only client (admin creates users)

This commit is contained in:
Bas van Rossem
2026-06-17 17:53:20 +02:00
parent bd2d859e92
commit 7d200eb8fc
3 changed files with 8 additions and 50 deletions

View File

@@ -1,11 +1,10 @@
import { createContext, useCallback, useContext, useState, type ReactNode } from 'react'; import { createContext, useCallback, useContext, useState, type ReactNode } from 'react';
import { clearToken, getToken } from '../lib/auth-storage'; import { clearToken, getToken } from '../lib/auth-storage';
import { signIn as apiSignIn, signUp as apiSignUp } from '../lib/api'; import { signIn as apiSignIn } from '../lib/api';
interface AuthContextValue { interface AuthContextValue {
isAuthed: boolean; isAuthed: boolean;
signIn: (email: string, password: string) => Promise<void>; signIn: (email: string, password: string) => Promise<void>;
signUp: (email: string, password: string) => Promise<void>;
signOut: () => void; signOut: () => void;
} }
@@ -19,25 +18,13 @@ export function AuthProvider({ children }: { children: ReactNode }) {
setIsAuthed(true); setIsAuthed(true);
}, []); }, []);
const signUp = useCallback(
async (email: string, password: string) => {
// Register, then sign in to obtain the bearer token.
await apiSignUp(email, password);
await apiSignIn(email, password);
setIsAuthed(true);
},
[],
);
const signOut = useCallback(() => { const signOut = useCallback(() => {
clearToken(); clearToken();
setIsAuthed(false); setIsAuthed(false);
}, []); }, []);
return ( return (
<AuthContext.Provider value={{ isAuthed, signIn, signUp, signOut }}> <AuthContext.Provider value={{ isAuthed, signIn, signOut }}>{children}</AuthContext.Provider>
{children}
</AuthContext.Provider>
); );
} }

View File

@@ -5,7 +5,7 @@ export const API_URL = import.meta.env.VITE_API_URL ?? 'http://localhost:3000';
export class ApiError extends Error { export class ApiError extends Error {
constructor( constructor(
public status: number, public status: number,
message: string, message: string
) { ) {
super(message); super(message);
this.name = 'ApiError'; this.name = 'ApiError';
@@ -35,13 +35,3 @@ export async function signIn(email: string, password: string): Promise<void> {
if (!token) throw new ApiError(500, 'Geen token ontvangen'); if (!token) throw new ApiError(500, 'Geen token ontvangen');
setToken(token); setToken(token);
} }
// Sign up affordance for testing: POST /api/auth/sign-up/email.
export async function signUp(email: string, password: string): Promise<void> {
const res = await fetch(`${API_URL}/api/auth/sign-up/email`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password, name: email.split('@')[0] || 'Worker' }),
});
if (!res.ok) throw new ApiError(res.status, 'Registreren mislukt');
}

View File

@@ -1,30 +1,21 @@
import { useState, type FormEvent } from 'react'; import { useState, type FormEvent } from 'react';
import { useAuth } from '../auth/AuthContext'; import { useAuth } from '../auth/AuthContext';
type Mode = 'signin' | 'signup';
export default function Login() { export default function Login() {
const { signIn, signUp } = useAuth(); const { signIn } = useAuth();
const [mode, setMode] = useState<Mode>('signin');
const [email, setEmail] = useState(''); const [email, setEmail] = useState('');
const [password, setPassword] = useState(''); const [password, setPassword] = useState('');
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [busy, setBusy] = useState(false); const [busy, setBusy] = useState(false);
const submitLabel = mode === 'signin' ? 'Inloggen' : 'Registreren';
async function handleSubmit(e: FormEvent) { async function handleSubmit(e: FormEvent) {
e.preventDefault(); e.preventDefault();
setError(null); setError(null);
setBusy(true); setBusy(true);
try { try {
if (mode === 'signin') {
await signIn(email, password); await signIn(email, password);
} else {
await signUp(email, password);
}
} catch { } catch {
setError(mode === 'signin' ? 'Inloggen mislukt' : 'Registreren mislukt'); setError('Inloggen mislukt');
} finally { } finally {
setBusy(false); setBusy(false);
} }
@@ -49,26 +40,16 @@ export default function Login() {
<input <input
className="field-input" className="field-input"
type="password" type="password"
autoComplete={mode === 'signin' ? 'current-password' : 'new-password'} autoComplete="current-password"
value={password} value={password}
onChange={(e) => setPassword(e.target.value)} onChange={(e) => setPassword(e.target.value)}
/> />
</label> </label>
{error && <p className="login-error">{error}</p>} {error && <p className="login-error">{error}</p>}
<button className="btn-primary" type="submit" disabled={busy}> <button className="btn-primary" type="submit" disabled={busy}>
{submitLabel} Inloggen
</button> </button>
</form> </form>
<button
className="login-toggle"
type="button"
onClick={() => {
setMode((m) => (m === 'signin' ? 'signup' : 'signin'));
setError(null);
}}
>
{mode === 'signin' ? 'Nog geen account? Registreren' : 'Al een account? Inloggen'}
</button>
</div> </div>
); );
} }