All files / src auth.tsx

86.36% Statements 38/44
70% Branches 14/20
85.71% Functions 12/14
97.14% Lines 34/35

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94        23x 23x 23x     5x 5x         5x 3x 3x       2x 2x         2x       2x                             23x     59x 59x     3x 3x 3x 3x       3x 3x 3x 3x 3x     59x     3x   2x 2x           3x 3x       59x         654x 654x 654x    
import { createContext, useContext, useState } from 'react'
import type { ReactNode } from 'react'
import { errorMessage } from './apiError'
 
export const TOKEN_KEY = 'auth_token'
const USERNAME_KEY = 'auth_username'
const API_BASE = import.meta.env.VITE_API_BASE ?? ''
 
async function loginRequest(username: string, password: string): Promise<string> {
	Iif (!username.trim() || !password) throw new Error('Enter a username and password')
	const res = await fetch(`${API_BASE}/api/v1/users/login`, {
		method: 'POST',
		headers: { 'content-type': 'application/json' },
		body: JSON.stringify({ username, password }),
	})
	if (!res.ok) throw new Error(await errorMessage(res))
	const data = (await res.json()) as { token: string }
	return data.token
}
 
async function registerRequest(username: string, password: string): Promise<void> {
	Iif (!username.trim() || !password) throw new Error('Enter a username and password')
	const res = await fetch(`${API_BASE}/api/v1/users/register`, {
		method: 'POST',
		headers: { 'content-type': 'application/json' },
		body: JSON.stringify({ username, password }),
	})
	Iif (!res.ok) throw new Error(await errorMessage(res))
}
 
async function logoutRequest(token: string): Promise<void> {
	await fetch(`${API_BASE}/api/v1/users/logout`, {
		method: 'POST',
		headers: { authorization: `Bearer ${token}` },
	})
}
 
type AuthContextValue = {
  token: string | null
  username: string | null
  signIn: (username: string, password: string) => Promise<void>
  register: (username: string, password: string) => Promise<void>
  updateUsername: (username: string) => void
  signOut: () => void
}
 
const AuthContext = createContext<AuthContextValue | null>(null)
 
export function AuthProvider({ children }: { children: ReactNode }) {
	const [token, setToken] = useState<string | null>(() => localStorage.getItem(TOKEN_KEY))
	const [username, setUsername] = useState<string | null>(() => localStorage.getItem(USERNAME_KEY))
 
	function persistSession(newToken: string, newUsername: string) {
		localStorage.setItem(TOKEN_KEY, newToken)
		localStorage.setItem(USERNAME_KEY, newUsername)
		setToken(newToken)
		setUsername(newUsername)
	}
 
	function clearSession() {
		localStorage.removeItem(TOKEN_KEY)
		localStorage.removeItem(USERNAME_KEY)
		sessionStorage.clear()
		setToken(null)
		setUsername(null)
	}
 
	const value: AuthContextValue = {
		token,
		username,
		signIn: async (u, p) => persistSession(await loginRequest(u, p), u),
		register: async (u, p) => {
			await registerRequest(u, p)
			persistSession(await loginRequest(u, p), u)
		},
		updateUsername: (u) => {
			if (token) persistSession(token, u)
		},
		signOut: () => {
			if (token) void logoutRequest(token).catch(() => {})
			clearSession()
		},
	}
 
	return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>
}
 
// eslint-disable-next-line react-refresh/only-export-components
export function useAuth() {
	const ctx = useContext(AuthContext)
	Iif (!ctx) throw new Error('useAuth must be used within AuthProvider')
	return ctx
}