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 (
+
+ );
+}
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);
+ }
+}