All files / src auth.tsx

86.36% Statements 38/44
65% Branches 13/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        21x 21x 21x     4x 4x         4x 2x 2x       1x 1x         1x       2x                             21x     37x 37x     2x 2x 2x 2x       2x 2x 2x 2x 2x     37x     3x   1x 1x           2x 2x       37x         394x 394x 394x    
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: () => {
			Eif (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
}