diff --git a/src/lib/either.ts b/src/lib/either.ts new file mode 100644 index 0000000..d8c2f8c --- /dev/null +++ b/src/lib/either.ts @@ -0,0 +1,47 @@ +export type Left = { + left: T; + right?: never; +}; + +export type Right = { + left?: never; + right: U; +}; + +export type Either = NonNullable | Right>; + +export function isLeft(e: Either): e is Left { + return e.left !== undefined; +} + +export function isRight(e: Either): e is Right { + return e.right !== undefined; +} + +export function left(value: T): Left { + return { left: value }; +} + +export function right(value: U): Right { + return { right: value }; +} + +export function match( + e: Either, + onLeft: (left: T) => void, + onRight: (right: U) => void, +) { + if (isLeft(e) && isRight(e)) { + throw new Error('Either<> cannot have both left and right values'); + } + + if (isLeft(e)) { + return onLeft(e.left); + } + + if (isRight(e)) { + return onRight(e.right); + } + + throw new Error('match() called on empty Either<>'); +} diff --git a/src/lib/utils.ts b/src/lib/utils.ts index ec6aa3e..d738a22 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -1,6 +1,7 @@ // SPDX-License-Identifier: MIT import { Temporal } from '@js-temporal/polyfill'; +import * as E from 'lib/either'; export function classNames(...args: any[]): string { const classes = []; @@ -30,36 +31,40 @@ export function classNames(...args: any[]): string { return classes.join(' '); } -export async function apiPost(url: string, data?: any): Promise { - return fetch(url, { - method: 'POST', - headers: { - Accept: 'application/json', - 'Content-Type': 'application/json', - }, - body: data ? JSON.stringify(data) : '', - }) - .then((res) => res.json()) - .catch((err) => { - // eslint-disable-next-line no-console - console.log(err); - return Promise.resolve({}); +async function performApiRequest( + url: string, + method: 'GET' | 'POST', + data?: D, +): Promise> { + try { + const response = await fetch(url, { + method, + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + }, + body: data ? JSON.stringify(data) : undefined, }); + + if (response.ok) { + const body = await response.text(); + return E.right(body.length > 0 ? JSON.parse(body) : {}); + } + + const body = await response.text(); + const error = body.length > 0 ? JSON.parse(body).error : 'Unknown error'; + return E.left(`${response.status} ${response.statusText}: ${error}`); + } catch (err) { + return E.left(`Network error: ${err}`); + } } -export async function apiGet(url: string): Promise { - return fetch(url, { - method: 'GET', - headers: { - Accept: 'application/json', - }, - }) - .then((res) => res.json()) - .catch((err) => { - // eslint-disable-next-line no-console - console.log(err); - return Promise.resolve({}); - }); +export async function apiPost(url: string, data?: D): Promise> { + return performApiRequest(url, 'POST', data); +} + +export async function apiGet(url: string): Promise> { + return performApiRequest(url, 'GET'); } export function formatTraffic(b: number): string { diff --git a/src/pages/api/login.ts b/src/pages/api/login.ts index 91073e4..7cbd4e2 100644 --- a/src/pages/api/login.ts +++ b/src/pages/api/login.ts @@ -57,6 +57,6 @@ export default withSessionRoute( (req.session as any).user = user.id; await req.session.save(); - res.status(200).json({ success: true }); + res.status(200).end(); }, ); diff --git a/src/pages/api/logout.ts b/src/pages/api/logout.ts index 38183e3..e640916 100644 --- a/src/pages/api/logout.ts +++ b/src/pages/api/logout.ts @@ -16,6 +16,6 @@ export default withSessionRoute( delete (req.session as any).user; await req.session.save(); - res.status(200).json({ success: true }); + res.status(200).end(); }, ); diff --git a/src/pages/index.tsx b/src/pages/index.tsx index fd5084d..bdf9666 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -6,6 +6,7 @@ import Interface from 'components/interface'; import { withSessionSsr } from 'lib/withSession'; import { apiGet } from 'lib/utils'; import type { WireguardInterface } from 'lib/wireguard'; +import * as E from 'lib/either'; interface IndexProps { user: string; @@ -13,19 +14,19 @@ interface IndexProps { export default function Index({ user }: IndexProps) { const [interfaces, setInterfaces] = useState([]); - const [fetchFailed, setFetchFailed] = useState(false); + const [fetchFailed, setFetchFailed] = useState(''); const fetchData = async () => { - const res = await apiGet('/api/interfaces'); + const res = await apiGet('/api/interfaces'); - if (!res || typeof res.error === 'string') { - // eslint-disable-next-line no-console - console.log(res); - setFetchFailed(true); - } else { - setFetchFailed(false); - setInterfaces(res); - } + E.match( + res, + setFetchFailed, + (ifs) => { + setFetchFailed(''); + setInterfaces(ifs); + }, + ); }; useEffect(() => { @@ -39,13 +40,18 @@ export default function Index({ user }: IndexProps) { {fetchFailed && ( <> -
- - Error: - - {' '} - Failed to fetch WireGuard info. Retrying in 2 seconds ... -
+
+ + + Error: + + {' '} + Failed to fetch WireGuard info. Retrying in 2 seconds ... + + + {fetchFailed} + +

)} diff --git a/src/pages/login.tsx b/src/pages/login.tsx index f6c6382..88c5851 100644 --- a/src/pages/login.tsx +++ b/src/pages/login.tsx @@ -4,25 +4,27 @@ import { useState, SyntheticEvent } from 'react'; import { useRouter } from 'next/router'; import BaseLayout from 'components/base-layout'; import { apiPost, classNames } from 'lib/utils'; +import * as E from 'lib/either'; export default function Login() { const router = useRouter(); const [loading, setLoading] = useState(false); const [username, setUsername] = useState(''); const [password, setPassword] = useState(''); - const [failedLogin, setFailedLogin] = useState(false); + const [failedLogin, setFailedLogin] = useState(''); const submit = async (e: SyntheticEvent) => { e.preventDefault(); - setFailedLogin(false); + setFailedLogin(''); setLoading(true); const res = await apiPost('/api/login', { username, password }); - if (res.success) { - router.push('/'); - } else { - setFailedLogin(true); - } + + E.match( + res, + setFailedLogin, + () => router.push('/'), + ); setLoading(false); };