-
-
Notifications
You must be signed in to change notification settings - Fork 13
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Added authentication with JWT tokens, including a few new environment…
… variables for configuration
- Loading branch information
1 parent
30df304
commit 0939741
Showing
13 changed files
with
609 additions
and
47 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,108 @@ | ||
interface AuthConfig { | ||
authRequired: boolean; | ||
} | ||
|
||
interface LoginResponse { | ||
token: string; | ||
} | ||
|
||
declare global { | ||
interface Window { | ||
BASE_PATH?: string; | ||
} | ||
} | ||
|
||
const getBasePath = (): string => { | ||
return window?.BASE_PATH?.endsWith('/') ? window.BASE_PATH.slice(0, -1) : window.BASE_PATH || ''; | ||
}; | ||
|
||
const BASE_PATH = getBasePath(); | ||
export const AUTH_API_URL = `${BASE_PATH}/api/auth`; | ||
|
||
interface ApiError extends Error { | ||
status?: number; | ||
} | ||
|
||
export const AUTH_ERROR_EVENT = 'bytestash:auth_error'; | ||
export const authErrorEvent = new CustomEvent(AUTH_ERROR_EVENT); | ||
|
||
const handleResponse = async (response: Response) => { | ||
if (response.status === 401 || response.status === 403) { | ||
window.dispatchEvent(authErrorEvent); | ||
|
||
const error = new Error('Authentication required') as ApiError; | ||
error.status = response.status; | ||
throw error; | ||
} | ||
|
||
if (!response.ok) { | ||
const text = await response.text(); | ||
console.error('Error response body:', text); | ||
const error = new Error(`Request failed: ${response.status} ${response.statusText}`) as ApiError; | ||
error.status = response.status; | ||
throw error; | ||
} | ||
|
||
return response; | ||
}; | ||
|
||
const getHeaders = (includeAuth = false) => { | ||
const headers: Record<string, string> = { | ||
'Content-Type': 'application/json', | ||
}; | ||
|
||
if (includeAuth) { | ||
const token = localStorage.getItem('token'); | ||
if (token) { | ||
headers['Authorization'] = `Bearer ${token}`; | ||
} | ||
} | ||
|
||
return headers; | ||
}; | ||
|
||
export const getAuthConfig = async (): Promise<AuthConfig> => { | ||
try { | ||
const response = await fetch(`${AUTH_API_URL}/config`); | ||
await handleResponse(response); | ||
return response.json(); | ||
} catch (error) { | ||
console.error('Error fetching auth config:', error); | ||
throw error; | ||
} | ||
}; | ||
|
||
export const verifyToken = async (): Promise<boolean> => { | ||
try { | ||
const response = await fetch(`${AUTH_API_URL}/verify`, { | ||
headers: getHeaders(true) | ||
}); | ||
|
||
if (response.status === 401 || response.status === 403) { | ||
return false; | ||
} | ||
|
||
const data = await response.json(); | ||
return data.valid; | ||
} catch (error) { | ||
console.error('Error verifying token:', error); | ||
return false; | ||
} | ||
}; | ||
|
||
export const login = async (username: string, password: string): Promise<string> => { | ||
try { | ||
const response = await fetch(`${AUTH_API_URL}/login`, { | ||
method: 'POST', | ||
headers: getHeaders(), | ||
body: JSON.stringify({ username, password }) | ||
}); | ||
|
||
await handleResponse(response); | ||
const data: LoginResponse = await response.json(); | ||
return data.token; | ||
} catch (error) { | ||
console.error('Error logging in:', error); | ||
throw error; | ||
} | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,84 @@ | ||
import React, { useState } from 'react'; | ||
import { useAuth } from '../../context/AuthContext'; | ||
import { login } from '../../api/auth'; | ||
|
||
const LoginPage: React.FC = () => { | ||
const [username, setUsername] = useState(''); | ||
const [password, setPassword] = useState(''); | ||
const [error, setError] = useState(''); | ||
const [isLoading, setIsLoading] = useState(false); | ||
const { login: setAuth } = useAuth(); | ||
|
||
const handleSubmit = async (e: React.FormEvent) => { | ||
e.preventDefault(); | ||
setIsLoading(true); | ||
setError(''); | ||
|
||
try { | ||
const token = await login(username, password); | ||
setAuth(token); | ||
} catch (err) { | ||
setError('Invalid username or password'); | ||
} finally { | ||
setIsLoading(false); | ||
} | ||
}; | ||
|
||
return ( | ||
<div className="min-h-screen bg-gray-900 flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8"> | ||
<div className="max-w-md w-full space-y-8"> | ||
<div> | ||
<h2 className="mt-6 text-center text-3xl font-bold text-white"> | ||
ByteStash | ||
</h2> | ||
<p className="mt-2 text-center text-sm text-gray-400"> | ||
Please sign in to continue | ||
</p> | ||
</div> | ||
<form className="mt-8 space-y-6" onSubmit={handleSubmit}> | ||
{error && ( | ||
<div className="text-red-500 text-sm text-center"> | ||
{error} | ||
</div> | ||
)} | ||
<div className="rounded-md shadow-sm -space-y-px"> | ||
<div> | ||
<input | ||
type="text" | ||
required | ||
className="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-700 placeholder-gray-500 text-white bg-gray-800 rounded-t-md focus:outline-none focus:ring-blue-500 focus:border-blue-500 focus:z-10 sm:text-sm" | ||
placeholder="Username" | ||
value={username} | ||
onChange={(e) => setUsername(e.target.value)} | ||
disabled={isLoading} | ||
/> | ||
</div> | ||
<div> | ||
<input | ||
type="password" | ||
required | ||
className="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-700 placeholder-gray-500 text-white bg-gray-800 rounded-b-md focus:outline-none focus:ring-blue-500 focus:border-blue-500 focus:z-10 sm:text-sm" | ||
placeholder="Password" | ||
value={password} | ||
onChange={(e) => setPassword(e.target.value)} | ||
disabled={isLoading} | ||
/> | ||
</div> | ||
</div> | ||
|
||
<div> | ||
<button | ||
type="submit" | ||
className="group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed" | ||
disabled={isLoading} | ||
> | ||
{isLoading ? 'Signing in...' : 'Sign in'} | ||
</button> | ||
</div> | ||
</form> | ||
</div> | ||
</div> | ||
); | ||
}; | ||
|
||
export default LoginPage; |
Oops, something went wrong.