+
Dataflow
+
{/* Source selector */}
diff --git a/ui/src/api.js b/ui/src/api.js
index 95aedf2..9db694e 100644
--- a/ui/src/api.js
+++ b/ui/src/api.js
@@ -1,7 +1,22 @@
const BASE = '/api'
+let _credentials = null // { user, pass }
+
+export function setCredentials(user, pass) {
+ _credentials = { user, pass }
+}
+
+export function clearCredentials() {
+ _credentials = null
+}
+
async function request(method, path, body, isFormData = false) {
const opts = { method, headers: {} }
+
+ if (_credentials) {
+ opts.headers['Authorization'] = `Basic ${btoa(`${_credentials.user}:${_credentials.pass}`)}`
+ }
+
if (body) {
if (isFormData) {
opts.body = body
@@ -10,7 +25,16 @@ async function request(method, path, body, isFormData = false) {
opts.body = JSON.stringify(body)
}
}
+
const res = await fetch(BASE + path, opts)
+
+ if (res.status === 401) {
+ clearCredentials()
+ const err = new Error('Unauthorized')
+ err.status = 401
+ throw err
+ }
+
const data = await res.json()
if (!res.ok) throw new Error(data.error || 'Request failed')
return data
diff --git a/ui/src/pages/Login.jsx b/ui/src/pages/Login.jsx
new file mode 100644
index 0000000..6c29393
--- /dev/null
+++ b/ui/src/pages/Login.jsx
@@ -0,0 +1,60 @@
+import { useState } from 'react'
+
+export default function Login({ onLogin }) {
+ const [user, setUser] = useState('')
+ const [pass, setPass] = useState('')
+ const [error, setError] = useState('')
+ const [loading, setLoading] = useState(false)
+
+ async function handleSubmit(e) {
+ e.preventDefault()
+ setError('')
+ setLoading(true)
+ try {
+ await onLogin(user, pass)
+ } catch {
+ setError('Invalid username or password')
+ } finally {
+ setLoading(false)
+ }
+ }
+
+ return (
+
+ )
+}