diff --git a/web/liveion/components/login.tsx b/web/liveion/components/login.tsx new file mode 100644 index 00000000..85967fc7 --- /dev/null +++ b/web/liveion/components/login.tsx @@ -0,0 +1,38 @@ +import { useState } from 'preact/hooks'; +import { TargetedEvent } from 'preact/compat'; + +import * as api from '../../shared/api'; +import { alertError } from '../../shared/utils'; + +export interface LoginProps { + onSuccess?: (token: string) => void; +} + +export function Login({ onSuccess }: LoginProps) { + const [token, setToken] = useState(''); + + const onTokenSubmit = async (e: TargetedEvent) => { + e.preventDefault(); + const tk = token.indexOf(' ') < 0 ? `Bearer ${token}` : token; + api.setAuthToken(tk); + try { + await api.getStreams(); + onSuccess?.(token); + } catch (e) { + api.setAuthToken(''); + alertError(e); + } + }; + + return ( +
+ Authorization Required +
+ Token + setToken(e.currentTarget?.value)} /> +
+ +
+
+ ); +} diff --git a/web/liveion/liveion.tsx b/web/liveion/liveion.tsx index 5dbf6a8d..0fb89343 100644 --- a/web/liveion/liveion.tsx +++ b/web/liveion/liveion.tsx @@ -1,11 +1,30 @@ +import { useState } from 'preact/hooks'; + +import * as api from '../shared/api'; import { Live777Logo } from '../shared/components/live777-logo'; import { StreamsTable } from '../shared/components/streams-table'; +import { TokenContext } from '../shared/context'; +import { useNeedAuthorization } from '../shared/hooks/use-need-authorization'; + +import { Login } from './components/login'; export function Liveion() { + const [token, setToken] = useState(''); + const [needsAuthorizaiton, setNeedsAuthorization] = useNeedAuthorization(api); + + const onLoginSuccess = (t: string) => { + setToken(t); + setNeedsAuthorization(false); + }; + return ( - <> + - - + {needsAuthorizaiton ? ( + + ) : ( + + )} + ); } diff --git a/web/liveman/api.ts b/web/liveman/api.ts index 519bdf56..abc6b8c6 100644 --- a/web/liveman/api.ts +++ b/web/liveman/api.ts @@ -1,35 +1,15 @@ import wretch from 'wretch'; import type { Stream } from '../shared/api'; +import { makeAuthorizationMiddleware } from '../shared/authorization-middleware'; -const unauthorizedCallbacks: (() => void)[] = []; +const authMiddleware = makeAuthorizationMiddleware(); -export function addUnauthorizedCallback(cb: () => void) { - unauthorizedCallbacks.push(cb); -} - -export function removeUnauthorizedCallback(cb: () => void) { - const i = unauthorizedCallbacks.indexOf(cb); - if (i >= 0) { - unauthorizedCallbacks.splice(i, 1); - } -} +const w = wretch().middlewares([authMiddleware]); -const base = wretch().middlewares([ - (next) => async (url, opts) => { - const res = await next(url, opts); - if (res.status === 401) { - unauthorizedCallbacks.forEach(cb => cb()); - } - return res; - } -]); - -let w = base; - -export function setAuthToken(token: string) { - w = base.auth(token); -} +export const setAuthToken = authMiddleware.setAuthorization; +export const addUnauthorizedCallback = authMiddleware.addUnauthorizedCallback; +export const removeUnauthorizedCallback = authMiddleware.removeUnauthorizedCallback; export interface LoginResponse { token_type: string; diff --git a/web/liveman/components/login.tsx b/web/liveman/components/login.tsx index 2711b367..b5ec8fd6 100644 --- a/web/liveman/components/login.tsx +++ b/web/liveman/components/login.tsx @@ -1,9 +1,9 @@ import { useState } from 'preact/hooks'; import { TargetedEvent } from 'preact/compat'; -import { WretchError } from 'wretch/resolver'; import * as livemanApi from '../api'; import * as sharedApi from '../../shared/api'; +import { alertError } from '../../shared/utils'; function useInput(label: string, type = 'text') { const [value, setValue] = useState(''); @@ -24,16 +24,6 @@ enum AuthorizeType { Password = 'Password', Token = 'Token' } -function alertError(e: unknown) { - if (e instanceof WretchError) { - alert(e.text); - } else if (e instanceof Error) { - alert(e.message); - } else { - alert(e); - } -} - export interface LoginProps { onSuccess?: (token: string) => void; } @@ -71,6 +61,8 @@ export function Login({ onSuccess }: LoginProps) { await livemanApi.getNodes(); onSuccess?.(token); } catch (e) { + livemanApi.setAuthToken(''); + sharedApi.setAuthToken(''); alertError(e); } }; diff --git a/web/liveman/liveman.tsx b/web/liveman/liveman.tsx index 13ccfe80..dd272f5a 100644 --- a/web/liveman/liveman.tsx +++ b/web/liveman/liveman.tsx @@ -1,27 +1,24 @@ -import { useCallback, useEffect, useRef, useState } from 'preact/hooks'; - -import { addUnauthorizedCallback, removeUnauthorizedCallback } from './api'; +import { useCallback, useRef, useState } from 'preact/hooks'; import type { Stream } from '../shared/api'; +import { TokenContext } from '../shared/context'; import { Live777Logo } from '../shared/components/live777-logo'; import { StreamsTable } from '../shared/components/streams-table'; -import { TokenContext } from '../shared/context'; +import { useNeedAuthorization } from '../shared/hooks/use-need-authorization'; +import * as api from './api'; import { Login } from './components/login'; import { NodesTable } from './components/nodes-table'; import { type IStreamTokenDialog, StreamTokenDialog } from './components/dialog-token'; export function Liveman() { const [token, setToken] = useState(''); - const [needsAuthorizaiton, setNeedsAuthorizaiton] = useState(false); - const unauthorizedCallback = useCallback(() => { - setNeedsAuthorizaiton(true); - }, []); + const [needsAuthorizaiton, setNeedsAuthorization] = useNeedAuthorization(api); - useEffect(() => { - addUnauthorizedCallback(unauthorizedCallback); - return () => removeUnauthorizedCallback(unauthorizedCallback); - }, []); + const onLoginSuccess = (t: string) => { + setToken(t); + setNeedsAuthorization(false); + }; const refStreamTokenDialog = useRef(null); const renderCreateToken = useCallback((stream: Stream) => { @@ -35,12 +32,7 @@ export function Liveman() { {needsAuthorizaiton ? ( <> - { - setToken(t); - setNeedsAuthorizaiton(false); - }} - /> + ) : ( <> diff --git a/web/shared/api.ts b/web/shared/api.ts index 4acca3bd..60e6ad7a 100644 --- a/web/shared/api.ts +++ b/web/shared/api.ts @@ -1,12 +1,14 @@ import wretch from 'wretch'; -const base = wretch(); +import { makeAuthorizationMiddleware } from '../shared/authorization-middleware'; -let w = base; +const authMiddleware = makeAuthorizationMiddleware(); -export function setAuthToken(token: string) { - w = base.auth(token); -} +const w = wretch().middlewares([authMiddleware]); + +export const setAuthToken = authMiddleware.setAuthorization; +export const addUnauthorizedCallback = authMiddleware.addUnauthorizedCallback; +export const removeUnauthorizedCallback = authMiddleware.removeUnauthorizedCallback; export function deleteSession(streamId: string, clientId: string) { return w.url(`/session/${streamId}/${clientId}`).delete(); diff --git a/web/shared/authorization-middleware.ts b/web/shared/authorization-middleware.ts new file mode 100644 index 00000000..c29f15b8 --- /dev/null +++ b/web/shared/authorization-middleware.ts @@ -0,0 +1,50 @@ +import { ConfiguredMiddleware } from 'wretch'; + +const Authorization = 'Authorization'; + +export type UnauthorizedCallback = () => void; + +interface AuthorizationContext { + token: string | null; + callbacks: UnauthorizedCallback[]; +} + +export interface AuthorizationCallbacks { + setAuthorization: (token: string) => void; + addUnauthorizedCallback: (cb: UnauthorizedCallback) => void; + removeUnauthorizedCallback: (cb: UnauthorizedCallback) => boolean; +} + +type AuthorizationMiddleware = ConfiguredMiddleware & AuthorizationCallbacks; + +export function makeAuthorizationMiddleware(): AuthorizationMiddleware { + const ctx: AuthorizationContext = { + token: null, + callbacks: [] + }; + const middleware: AuthorizationMiddleware = (next) => async (url, opts) => { + if (ctx.token) { + if (typeof opts.headers !== 'object') { + opts.headers = { [Authorization]: ctx.token }; + } else { + opts.headers[Authorization] = ctx.token; + } + } + const res = await next(url, opts); + if (res.status === 401) { + ctx.callbacks.forEach(cb => cb()); + } + return res; + }; + middleware.setAuthorization = token => ctx.token = token; + middleware.addUnauthorizedCallback = cb => ctx.callbacks.push(cb); + middleware.removeUnauthorizedCallback = cb => { + const i = ctx.callbacks.indexOf(cb); + if (i >= 0) { + ctx.callbacks.splice(i, 1); + return true; + } + return false; + }; + return middleware; +}; diff --git a/web/shared/hooks/use-need-authorization.ts b/web/shared/hooks/use-need-authorization.ts new file mode 100644 index 00000000..568824c8 --- /dev/null +++ b/web/shared/hooks/use-need-authorization.ts @@ -0,0 +1,22 @@ +import { useCallback, useEffect, useState } from 'preact/hooks'; + +import { type UnauthorizedCallback } from '../authorization-middleware'; + +interface AuthorizationCallbacks { + addUnauthorizedCallback: (cb: UnauthorizedCallback) => void; + removeUnauthorizedCallback: (cb: UnauthorizedCallback) => boolean; +} + +export function useNeedAuthorization(auth: AuthorizationCallbacks) { + const needsAuthorizaiton = useState(false); + const unauthorizedCallback = useCallback(() => { + needsAuthorizaiton[1](true); + }, []); + + useEffect(() => { + auth.addUnauthorizedCallback(unauthorizedCallback); + return () => auth.removeUnauthorizedCallback(unauthorizedCallback); + }, []); + + return needsAuthorizaiton; +} diff --git a/web/shared/utils.ts b/web/shared/utils.ts index 9ca2c21a..3bfc6587 100644 --- a/web/shared/utils.ts +++ b/web/shared/utils.ts @@ -1,3 +1,5 @@ +import { WretchError } from 'wretch/resolver'; + export const formatTime = (timestamp: number) => new Date(timestamp).toLocaleString('zh-CN', { year: 'numeric', month: '2-digit', @@ -25,3 +27,13 @@ export const nextSeqId = (prefix: string, existingIds: string[]) => { } return newId; }; + +export function alertError(e: unknown) { + if (e instanceof WretchError) { + alert(e.text); + } else if (e instanceof Error) { + alert(e.message); + } else { + alert(e); + } +}