Skip to content

Commit

Permalink
Merge pull request #238 from binbat/feat/web-live777-token-auth
Browse files Browse the repository at this point in the history
feat(web): live777 token auth webui
  • Loading branch information
a-wing authored Oct 20, 2024
2 parents 6f464aa + 3224882 commit 697286e
Show file tree
Hide file tree
Showing 9 changed files with 170 additions and 63 deletions.
38 changes: 38 additions & 0 deletions web/liveion/components/login.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<fieldset>
<legend>Authorization Required</legend>
<form onSubmit={onTokenSubmit}>
<span class="inline-block min-w-24 font-bold">Token</span>
<input value={token} onInput={e => setToken(e.currentTarget?.value)} />
<br />
<input type="submit" value="Login" />
</form>
</fieldset>
);
}
25 changes: 22 additions & 3 deletions web/liveion/liveion.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<>
<TokenContext.Provider value={{ token }}>
<Live777Logo />
<StreamsTable showCascade />
</>
{needsAuthorizaiton ? (
<Login onSuccess={onLoginSuccess} />
) : (
<StreamsTable showCascade />
)}
</TokenContext.Provider>
);
}
32 changes: 6 additions & 26 deletions web/liveman/api.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
14 changes: 3 additions & 11 deletions web/liveman/components/login.tsx
Original file line number Diff line number Diff line change
@@ -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('');
Expand All @@ -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;
}
Expand Down Expand Up @@ -71,6 +61,8 @@ export function Login({ onSuccess }: LoginProps) {
await livemanApi.getNodes();
onSuccess?.(token);
} catch (e) {
livemanApi.setAuthToken('');
sharedApi.setAuthToken('');
alertError(e);
}
};
Expand Down
28 changes: 10 additions & 18 deletions web/liveman/liveman.tsx
Original file line number Diff line number Diff line change
@@ -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<IStreamTokenDialog>(null);
const renderCreateToken = useCallback((stream: Stream) => {
Expand All @@ -35,12 +32,7 @@ export function Liveman() {
<Live777Logo />
{needsAuthorizaiton ? (
<>
<Login
onSuccess={t => {
setToken(t);
setNeedsAuthorizaiton(false);
}}
/>
<Login onSuccess={onLoginSuccess} />
</>
) : (
<>
Expand Down
12 changes: 7 additions & 5 deletions web/shared/api.ts
Original file line number Diff line number Diff line change
@@ -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();
Expand Down
50 changes: 50 additions & 0 deletions web/shared/authorization-middleware.ts
Original file line number Diff line number Diff line change
@@ -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;
};
22 changes: 22 additions & 0 deletions web/shared/hooks/use-need-authorization.ts
Original file line number Diff line number Diff line change
@@ -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;
}
12 changes: 12 additions & 0 deletions web/shared/utils.ts
Original file line number Diff line number Diff line change
@@ -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',
Expand Down Expand Up @@ -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);
}
}

0 comments on commit 697286e

Please sign in to comment.