From f17bac399a608d4dcf164510b3ece784b120a41b Mon Sep 17 00:00:00 2001 From: lukas Date: Sun, 19 Sep 2021 23:20:37 +0200 Subject: [PATCH] basic frontend implementation of new token system --- apiGo/api/api/ApiBase.go | 61 +++-- apiGo/api/api/Auth.go | 34 +-- src/App.tsx | 240 ++++++++---------- .../VideoContainer/VideoContainer.tsx | 2 +- src/index.tsx | 5 +- .../AuthenticationPage/AuthenticationPage.tsx | 28 +- src/pages/CategoryPage/CategoryPage.tsx | 32 +-- src/pages/CategoryPage/CategoryView.tsx | 5 +- src/pages/CategoryPage/TagView.tsx | 2 +- src/pages/SettingsPage/SettingsPage.tsx | 80 +++--- src/utils/Api.ts | 19 +- src/utils/TokenHandler.ts | 135 ---------- src/utils/TokenStore/CookieTokenStore.ts | 48 ---- src/utils/TokenStore/TokenStore.ts | 11 - src/utils/context/Cookie.ts | 55 ++++ src/utils/context/LoginContext.ts | 34 +++ src/utils/context/LoginContextProvider.tsx | 105 ++++++++ 17 files changed, 436 insertions(+), 460 deletions(-) delete mode 100644 src/utils/TokenHandler.ts delete mode 100644 src/utils/TokenStore/CookieTokenStore.ts delete mode 100644 src/utils/TokenStore/TokenStore.ts create mode 100644 src/utils/context/Cookie.ts create mode 100644 src/utils/context/LoginContext.ts create mode 100644 src/utils/context/LoginContextProvider.tsx diff --git a/apiGo/api/api/ApiBase.go b/apiGo/api/api/ApiBase.go index c33efc8..9bb9ee5 100644 --- a/apiGo/api/api/ApiBase.go +++ b/apiGo/api/api/ApiBase.go @@ -3,12 +3,13 @@ package api import ( "fmt" "net/http" + "openmediacenter/apiGo/database/settings" ) const ( VideoNode = "video" - TagNode = "tag" - SettingsNode = "setting" + TagNode = "tags" + SettingsNode = "settings" ActorNode = "actor" TVShowNode = "tv" LoginNode = "login" @@ -32,34 +33,44 @@ const ( func AddHandler(action string, apiNode string, perm uint8, handler func(ctx Context)) { http.Handle(fmt.Sprintf("/api/%s/%s", apiNode, action), http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { - tokenheader := request.Header.Get("Token") - - id := -1 - permid := PermUnauthorized - - // check token if token provided - if tokenheader != "" { - id, permid = TokenValid(request.Header.Get("Token")) - } - - ctx := &apicontext{writer: writer, responseWritten: false, request: request, userid: id, permid: permid} - - // check if rights are sufficient to perform the action - if permid <= perm { - handler(ctx) - - if !ctx.responseWritten { - // none of the response functions called so send default response - ctx.Error("Unknown server Error occured") - writer.WriteHeader(501) - } + srvPwd := settings.GetPassword() + if srvPwd == nil { + // no password set + ctx := &apicontext{writer: writer, responseWritten: false, request: request, userid: -1, permid: PermUnauthorized} + callHandler(ctx, handler, writer) } else { - ctx.Error("insufficient permissions") - writer.WriteHeader(501) + tokenheader := request.Header.Get("Token") + + id := -1 + permid := PermUnauthorized + + // check token if token provided + if tokenheader != "" { + id, permid = TokenValid(request.Header.Get("Token")) + } + + ctx := &apicontext{writer: writer, responseWritten: false, request: request, userid: id, permid: permid} + + // check if rights are sufficient to perform the action + if permid <= perm { + callHandler(ctx, handler, writer) + } else { + ctx.Error("insufficient permissions") + } } })) } +func callHandler(ctx *apicontext, handler func(ctx Context), writer http.ResponseWriter) { + handler(ctx) + + if !ctx.responseWritten { + // none of the response functions called so send default response + ctx.Error("Unknown server Error occured") + writer.WriteHeader(501) + } +} + func ServerInit() { // initialize auth service and add corresponding auth routes InitOAuth() diff --git a/apiGo/api/api/Auth.go b/apiGo/api/api/Auth.go index 85aa07f..9e50ddd 100644 --- a/apiGo/api/api/Auth.go +++ b/apiGo/api/api/Auth.go @@ -48,7 +48,6 @@ func TokenValid(token string) (int, uint8) { func InitOAuth() { AddHandler("login", LoginNode, PermUnauthorized, func(ctx Context) { var t struct { - Username string Password string } @@ -57,28 +56,27 @@ func InitOAuth() { } // empty check - if t.Password == "" || t.Username == "" { - ctx.Error("empty username or password") + if t.Password == "" { + ctx.Error("empty password") return } // generate Argon2 Hash of passed pwd - pwd := HashPassword(t.Password) + HashPassword(t.Password) + // todo use hashed password - var id uint - var name string - var rightid uint8 + var password string - err := database.QueryRow("SELECT userId,userName,rightId FROM User WHERE userName=? AND password=?", t.Username, *pwd).Scan(&id, &name, &rightid) - if err != nil { + err := database.QueryRow("SELECT password FROM settings WHERE 1").Scan(&password) + if err != nil || t.Password != password { ctx.Error("unauthorized") return } expires := time.Now().Add(time.Hour * TokenExpireHours).Unix() claims := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.StandardClaims{ - Issuer: strconv.Itoa(int(id)), - Subject: strconv.Itoa(int(rightid)), + Issuer: strconv.Itoa(int(0)), + Subject: strconv.Itoa(int(PermUser)), ExpiresAt: expires, }) @@ -90,18 +88,12 @@ func InitOAuth() { } type ResponseType struct { - Token Token - Username string - UserPerm uint8 + Token Token } - ctx.Json(ResponseType{ - Token: Token{ - Token: token, - ExpiresAt: expires, - }, - Username: t.Username, - UserPerm: rightid, + ctx.Json(Token{ + Token: token, + ExpiresAt: expires, }) }) } diff --git a/src/App.tsx b/src/App.tsx index d78d444..af62270 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -9,21 +9,17 @@ import style from './App.module.css'; import SettingsPage from './pages/SettingsPage/SettingsPage'; import CategoryPage from './pages/CategoryPage/CategoryPage'; -import {APINode, callAPI} from './utils/Api'; -import {BrowserRouter as Router, NavLink, Route, Switch} from 'react-router-dom'; +import {NavLink, Route, Switch, useRouteMatch} from 'react-router-dom'; import Player from './pages/Player/Player'; import ActorOverviewPage from './pages/ActorOverviewPage/ActorOverviewPage'; import ActorPage from './pages/ActorPage/ActorPage'; -import {SettingsTypes} from './types/ApiTypes'; import AuthenticationPage from './pages/AuthenticationPage/AuthenticationPage'; import TVShowPage from './pages/TVShowPage/TVShowPage'; import TVPlayer from './pages/TVShowPage/TVPlayer'; -import {CookieTokenStore} from './utils/TokenStore/CookieTokenStore'; -import {token} from './utils/TokenHandler'; +import {LoginContextProvider} from './utils/context/LoginContextProvider'; interface state { - password: boolean | null; // null if uninitialized - true if pwd needed false if not needed mediacentername: string; } @@ -34,100 +30,63 @@ class App extends React.Component<{}, state> { constructor(props: {}) { super(props); - token.init(new CookieTokenStore()); - - let pwdneeded: boolean | null = null; - - if (token.apiTokenValid()) { - pwdneeded = false; - } else { - token.refreshAPIToken((err) => { - if (err === 'invalid_client') { - this.setState({password: true}); - } else if (err === '') { - this.setState({password: false}); - } else { - console.log('unimplemented token error: ' + err); - } - }); - } - this.state = { - mediacentername: 'OpenMediaCenter', - password: pwdneeded + mediacentername: 'OpenMediaCenter' }; // force an update on theme change GlobalInfos.onThemeChange(() => { this.forceUpdate(); }); - - // set the hook to load passwordfield on global func call - GlobalInfos.loadPasswordPage = (callback?: () => void): void => { - // try refreshing the token - token.refreshAPIToken((err) => { - if (err !== '') { - this.setState({password: true}); - } else { - // call callback if request was successful - if (callback) { - callback(); - } - } - }, true); - }; - } - - initialAPICall(): void { - // this is the first api call so if it fails we know there is no connection to backend - callAPI(APINode.Settings, {action: 'loadInitialData'}, (result: SettingsTypes.initialApiCallData) => { - // set theme - GlobalInfos.enableDarkTheme(result.DarkMode); - - GlobalInfos.setVideoPaths(result.VideoPath, result.TVShowPath); - - GlobalInfos.setTVShowsEnabled(result.TVShowEnabled); - GlobalInfos.setFullDeleteEnabled(result.FullDeleteEnabled); - - this.setState({ - mediacentername: result.MediacenterName - }); - // set tab title to received mediacenter name - document.title = result.MediacenterName; - }); - } - - componentDidMount(): void { - this.initialAPICall(); } render(): JSX.Element { // add the main theme to the page body document.body.className = GlobalInfos.getThemeStyle().backgroundcolor; - if (this.state.password === true) { - // render authentication page if auth is neccessary - return ( - { - this.setState({password: false}); - // reinit general infos - this.initialAPICall(); - }} - /> - ); - } else if (this.state.password === false) { - return ( - -
+ return ( + + + + { + // this.setState({password: false}); + // reinit general infos + // this.initialAPICall(); + }} + /> + + {this.navBar()} - {this.routing()} -
-
- ); - } else { - return <>still loading...; - } + + + + + ); + + // if (this.state.password === true) { + // // render authentication page if auth is neccessary + // return ( + // { + // this.setState({password: false}); + // // reinit general infos + // this.initialAPICall(); + // }} + // /> + // ); + // } else if (this.state.password === false) { + // return ( + // + //
+ // {this.navBar()} + // {this.routing()} + //
+ //
+ // ); + // } else { + // return <>still loading...; + // } } /** @@ -139,73 +98,84 @@ class App extends React.Component<{}, state> { return (
{this.state.mediacentername}
- + Home - + Random Video - + Categories {GlobalInfos.isTVShowEnabled() ? ( - + TV Shows ) : null} - + Settings
); } - - /** - * render the react router elements - */ - routing(): JSX.Element { - return ( - - - - - - - - - - - - - - - - - - - - - {GlobalInfos.isTVShowEnabled() ? ( - - - - ) : null} - - {GlobalInfos.isTVShowEnabled() ? ( - - - - ) : null} - - - - - - ); - } } +const MyRouter = (): JSX.Element => { + const match = useRouteMatch(); + + return ( + + + + + + + + + + + + + + + + + + + + + {GlobalInfos.isTVShowEnabled() ? ( + + + + ) : null} + + {GlobalInfos.isTVShowEnabled() ? ( + + + + ) : null} + + + + + + ); +}; + export default App; diff --git a/src/elements/VideoContainer/VideoContainer.tsx b/src/elements/VideoContainer/VideoContainer.tsx index 53d3e59..e06c525 100644 --- a/src/elements/VideoContainer/VideoContainer.tsx +++ b/src/elements/VideoContainer/VideoContainer.tsx @@ -26,7 +26,7 @@ const VideoContainer = (props: Props): JSX.Element => { ); }} name={el.MovieName} - linkPath={'/player/' + el.MovieId} + linkPath={'/media/player/' + el.MovieId} /> )} data={props.data}> diff --git a/src/index.tsx b/src/index.tsx index 78e0469..04468c1 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,13 +1,16 @@ import React from 'react'; import ReactDOM from 'react-dom'; import App from './App'; +import {BrowserRouter} from 'react-router-dom'; // don't allow console logs within production env global.console.log = process.env.NODE_ENV !== 'development' ? (_: string | number | boolean): void => {} : global.console.log; ReactDOM.render( - + + + , document.getElementById('root') ); diff --git a/src/pages/AuthenticationPage/AuthenticationPage.tsx b/src/pages/AuthenticationPage/AuthenticationPage.tsx index 5a3a771..2b53d16 100644 --- a/src/pages/AuthenticationPage/AuthenticationPage.tsx +++ b/src/pages/AuthenticationPage/AuthenticationPage.tsx @@ -2,9 +2,11 @@ import React from 'react'; import {Button} from '../../elements/GPElements/Button'; import style from './AuthenticationPage.module.css'; import {addKeyHandler, removeKeyHandler} from '../../utils/ShortkeyHandler'; -import {token} from '../../utils/TokenHandler'; import {faTimes} from '@fortawesome/free-solid-svg-icons'; import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; +import {LoginContext, LoginState} from '../../utils/context/LoginContext'; +import {APINode, callApiUnsafe} from '../../utils/Api'; +import {cookie, Token} from '../../utils/context/Cookie'; interface state { pwdText: string; @@ -36,6 +38,8 @@ class AuthenticationPage extends React.Component { removeKeyHandler(this.keypress); } + static contextType = LoginContext; + render(): JSX.Element { return ( <> @@ -76,21 +80,17 @@ class AuthenticationPage extends React.Component { * request a new token and check if pwd was valid */ authenticate(): void { - token.refreshAPIToken( - (error) => { - if (error !== '') { - this.setState({wrongPWDInfo: true}); + callApiUnsafe( + APINode.Login, + {action: 'login', Password: this.state.pwdText}, + (r: Token) => { + cookie.Store(r); - // set timeout to make the info auto-disappearing - setTimeout(() => { - this.setState({wrongPWDInfo: false}); - }, 2000); - } else { - this.props.onSuccessLogin(); - } + this.context.setLoginState(LoginState.LoggedIn); }, - true, - this.state.pwdText + (e) => { + console.log(e); + } ); } diff --git a/src/pages/CategoryPage/CategoryPage.tsx b/src/pages/CategoryPage/CategoryPage.tsx index 2bed929..b019059 100644 --- a/src/pages/CategoryPage/CategoryPage.tsx +++ b/src/pages/CategoryPage/CategoryPage.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import {Route, Switch} from 'react-router-dom'; +import {Route, Switch, useRouteMatch} from 'react-router-dom'; import {CategoryViewWR} from './CategoryView'; import TagView from './TagView'; @@ -7,19 +7,21 @@ import TagView from './TagView'; * Component for Category Page * Contains a Tag Overview and loads specific Tag videos in VideoContainer */ -class CategoryPage extends React.Component { - render(): JSX.Element { - return ( - - - - - - - - - ); - } -} +const CategoryPage = (): JSX.Element => { + const match = useRouteMatch(); + + console.log(match.url); + + return ( + + + + + + + + + ); +}; export default CategoryPage; diff --git a/src/pages/CategoryPage/CategoryView.tsx b/src/pages/CategoryPage/CategoryView.tsx index 1ac5de9..0570be8 100644 --- a/src/pages/CategoryPage/CategoryView.tsx +++ b/src/pages/CategoryPage/CategoryView.tsx @@ -119,9 +119,10 @@ export class CategoryView extends React.Component { + (e) => { + console.log(e); // if there is an load error redirect to home page - this.props.history.push('/'); + // this.props.history.push('/'); } ); } diff --git a/src/pages/CategoryPage/TagView.tsx b/src/pages/CategoryPage/TagView.tsx index da47e02..25d264b 100644 --- a/src/pages/CategoryPage/TagView.tsx +++ b/src/pages/CategoryPage/TagView.tsx @@ -56,7 +56,7 @@ class TagView extends React.Component { ( - + )} diff --git a/src/pages/SettingsPage/SettingsPage.tsx b/src/pages/SettingsPage/SettingsPage.tsx index dac2d8b..4d7efd0 100644 --- a/src/pages/SettingsPage/SettingsPage.tsx +++ b/src/pages/SettingsPage/SettingsPage.tsx @@ -3,52 +3,52 @@ import MovieSettings from './MovieSettings'; import GeneralSettings from './GeneralSettings'; import style from './SettingsPage.module.css'; import GlobalInfos from '../../utils/GlobalInfos'; -import {NavLink, Redirect, Route, Switch} from 'react-router-dom'; +import {NavLink, Redirect, Route, Switch, useRouteMatch} from 'react-router-dom'; /** * The Settingspage handles all kinds of settings for the mediacenter * and is basically a wrapper for child-tabs */ -class SettingsPage extends React.Component { - render(): JSX.Element { - const themestyle = GlobalInfos.getThemeStyle(); - return ( -
-
-
Settings
- -
General
+const SettingsPage = (): JSX.Element => { + const themestyle = GlobalInfos.getThemeStyle(); + const match = useRouteMatch(); + + return ( +
+
+
Settings
+ +
General
+
+ +
Movies
+
+ {GlobalInfos.isTVShowEnabled() ? ( + +
TV Shows
- -
Movies
-
- {GlobalInfos.isTVShowEnabled() ? ( - -
TV Shows
-
- ) : null} -
-
- - - - - - - - {GlobalInfos.isTVShowEnabled() ? ( - - - - ) : null} - - - - -
+ ) : null}
- ); - } -} +
+ + + + + + + + {GlobalInfos.isTVShowEnabled() ? ( + + + + ) : null} + + + + +
+
+ ); +}; export default SettingsPage; diff --git a/src/utils/Api.ts b/src/utils/Api.ts index 55ba285..cf3a0a6 100644 --- a/src/utils/Api.ts +++ b/src/utils/Api.ts @@ -1,5 +1,5 @@ import GlobalInfos from './GlobalInfos'; -import {token} from './TokenHandler'; +import {cookie} from './context/Cookie'; const APIPREFIX: string = '/api/'; @@ -25,9 +25,7 @@ export function callAPI( callback: (_: T) => void, errorcallback: (_: string) => void = (_: string): void => {} ): void { - token.checkAPITokenValid((mytoken) => { - generalAPICall(apinode, fd, callback, errorcallback, false, true, mytoken); - }); + generalAPICall(apinode, fd, callback, errorcallback, false, true); } /** @@ -43,7 +41,7 @@ export function callApiUnsafe( callback: (_: T) => void, errorcallback?: (_: string) => void ): void { - generalAPICall(apinode, fd, callback, errorcallback, true, true, ''); + generalAPICall(apinode, fd, callback, errorcallback, true, true); } /** @@ -53,9 +51,7 @@ export function callApiUnsafe( * @param callback the callback with PLAIN text reply from backend */ export function callAPIPlain(apinode: APINode, fd: ApiBaseRequest, callback: (_: string) => void): void { - token.checkAPITokenValid((mytoken) => { - generalAPICall(apinode, fd, callback, () => {}, false, false, mytoken); - }); + generalAPICall(apinode, fd, callback, () => {}, false, false); } function generalAPICall( @@ -64,16 +60,16 @@ function generalAPICall( callback: (_: T) => void, errorcallback: (_: string) => void = (_: string): void => {}, unsafe: boolean, - json: boolean, - mytoken: string + json: boolean ): void { (async function (): Promise { + const tkn = cookie.Load(); const response = await fetch(APIPREFIX + apinode + '/' + fd.action, { method: 'POST', body: JSON.stringify(fd), headers: new Headers({ 'Content-Type': json ? 'application/json' : 'text/plain', - ...(!unsafe && {Authorization: 'Bearer ' + mytoken}) + ...(!unsafe && tkn !== null && {Token: tkn.Token}) }) }); @@ -110,6 +106,7 @@ function generalAPICall( // eslint-disable-next-line no-shadow export enum APINode { + Login = 'login', Settings = 'settings', Tags = 'tags', Actor = 'actor', diff --git a/src/utils/TokenHandler.ts b/src/utils/TokenHandler.ts deleted file mode 100644 index ad32915..0000000 --- a/src/utils/TokenHandler.ts +++ /dev/null @@ -1,135 +0,0 @@ -import {TokenStore} from './TokenStore/TokenStore'; - -export namespace token { - // store api token - empty if not set - let apiToken = ''; - - // a callback que to be called after api token refresh - let callQue: ((error: string) => void)[] = []; - // flag to check wheter a api refresh is currently pending - let refreshInProcess = false; - // store the expire seconds of token - let expireSeconds = -1; - - let tokenStore: TokenStore; - let APiHost: string = '/'; - - export function init(ts: TokenStore, apiHost?: string): void { - tokenStore = ts; - if (apiHost) { - APiHost = apiHost; - } - } - - /** - * refresh the api token or use that one in cookie if still valid - * @param callback to be called after successful refresh - * @param password - * @param force - */ - export function refreshAPIToken(callback: (error: string) => void, force?: boolean, password?: string): void { - callQue.push(callback); - - // check if already is a token refresh is in process - if (refreshInProcess) { - // if yes return - return; - } else { - // if not set flat - refreshInProcess = true; - } - - if (apiTokenValid() && !force) { - console.log('token still valid...'); - callFuncQue(''); - return; - } - - const formData = new FormData(); - formData.append('grant_type', 'client_credentials'); - formData.append('client_id', 'openmediacenter'); - formData.append('client_secret', password ? password : 'openmediacenter'); - formData.append('scope', 'all'); - - interface APIToken { - error?: string; - // eslint-disable-next-line camelcase - access_token: string; // no camel case allowed because of backendlib - // eslint-disable-next-line camelcase - expires_in: number; // no camel case allowed because of backendlib - scope: string; - // eslint-disable-next-line camelcase - token_type: string; // no camel case allowed because of backendlib - } - - console.log(APiHost); - - fetch(APiHost + 'token', {method: 'POST', body: formData}) - .then((response) => - response.json().then((result: APIToken) => { - if (result.error) { - callFuncQue(result.error); - return; - } - // set api token - apiToken = result.access_token; - // set expire time - expireSeconds = new Date().getTime() / 1000 + result.expires_in; - // setTokenCookie(apiToken, expireSeconds); - tokenStore.setToken({accessToken: apiToken, expireTime: expireSeconds, tokenType: '', scope: ''}); - // call all handlers and release flag - callFuncQue(''); - }) - ) - .catch((e) => { - callback(e); - }); - } - - export function apiTokenValid(): boolean { - // check if a cookie with token is available - // const token = getTokenCookie(); - const tmptoken = tokenStore.loadToken(); - if (tmptoken !== null) { - // check if token is at least valid for the next minute - if (tmptoken.expireTime > new Date().getTime() / 1000 + 60) { - apiToken = tmptoken.accessToken; - expireSeconds = tmptoken.expireTime; - - return true; - } - } - return false; - } - - /** - * call all qued callbacks - */ - function callFuncQue(error: string): void { - // call all pending handlers - callQue.map((func) => { - return func(error); - }); - // reset pending que - callQue = []; - // release flag to be able to start new refresh - refreshInProcess = false; - } - - /** - * check if api token is valid -- if not request new one - * when finished call callback - * @param callback function to be called afterwards - */ - export function checkAPITokenValid(callback: (mytoken: string) => void): void { - // check if token is valid and set - if (apiToken === '' || expireSeconds <= new Date().getTime() / 1000) { - console.log('token not valid...'); - refreshAPIToken(() => { - callback(apiToken); - }); - } else { - callback(apiToken); - } - } -} diff --git a/src/utils/TokenStore/CookieTokenStore.ts b/src/utils/TokenStore/CookieTokenStore.ts deleted file mode 100644 index efdff79..0000000 --- a/src/utils/TokenStore/CookieTokenStore.ts +++ /dev/null @@ -1,48 +0,0 @@ -import {Token, TokenStore} from './TokenStore'; - -export class CookieTokenStore extends TokenStore { - loadToken(): Token | null { - const token = this.decodeCookie('token'); - const expireInString = this.decodeCookie('token_expire'); - const expireIn = parseInt(expireInString, 10); - - if (expireIn !== 0 && token !== '') { - return {accessToken: token, expireTime: expireIn, scope: '', tokenType: ''}; - } else { - return null; - } - } - - /** - * set the cookie for the currently gotten token - * @param token the token to set - */ - setToken(token: Token): void { - let d = new Date(); - d.setTime(token.expireTime * 1000); - console.log('token set' + d.toUTCString()); - let expires = 'expires=' + d.toUTCString(); - document.cookie = 'token=' + token.accessToken + ';' + expires + ';path=/'; - document.cookie = 'token_expire=' + token.expireTime + ';' + expires + ';path=/'; - } - - /** - * decode a simple cookie with key specified - * @param key cookie key - */ - decodeCookie(key: string): string { - let name = key + '='; - let decodedCookie = decodeURIComponent(document.cookie); - let ca = decodedCookie.split(';'); - for (let i = 0; i < ca.length; i++) { - let c = ca[i]; - while (c.charAt(0) === ' ') { - c = c.substring(1); - } - if (c.indexOf(name) === 0) { - return c.substring(name.length, c.length); - } - } - return ''; - } -} diff --git a/src/utils/TokenStore/TokenStore.ts b/src/utils/TokenStore/TokenStore.ts deleted file mode 100644 index 7d36fb4..0000000 --- a/src/utils/TokenStore/TokenStore.ts +++ /dev/null @@ -1,11 +0,0 @@ -export interface Token { - accessToken: string; - expireTime: number; // second time when token will be invalidated - scope: string; - tokenType: string; -} - -export abstract class TokenStore { - abstract loadToken(): Token | null; - abstract setToken(token: Token): void; -} diff --git a/src/utils/context/Cookie.ts b/src/utils/context/Cookie.ts new file mode 100644 index 0000000..fe17073 --- /dev/null +++ b/src/utils/context/Cookie.ts @@ -0,0 +1,55 @@ +export interface Token { + Token: string; + ExpiresAt: number; +} + +export namespace cookie { + const jwtcookiename = 'jwt'; + + export function Store(data: Token): void { + const d = new Date(); + d.setTime(data.ExpiresAt * 1000); + const expires = 'expires=' + d.toUTCString(); + + document.cookie = jwtcookiename + '=' + JSON.stringify(data) + ';' + expires + ';path=/'; + } + + export function Load(): Token | null { + const datastr = decodeCookie(jwtcookiename); + if (datastr === '') { + return null; + } + + try { + return JSON.parse(datastr); + } catch (e) { + // if cookie not decodeable delete it and return null + Delete(); + return null; + } + } + + export function Delete(): void { + document.cookie = `${jwtcookiename}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;`; + } + + /** + * decode a simple cookie with key specified + * @param key cookie key + */ + function decodeCookie(key: string): string { + let name = key + '='; + let decodedCookie = decodeURIComponent(document.cookie); + let ca = decodedCookie.split(';'); + for (let i = 0; i < ca.length; i++) { + let c = ca[i]; + while (c.charAt(0) === ' ') { + c = c.substring(1); + } + if (c.indexOf(name) === 0) { + return c.substring(name.length, c.length); + } + } + return ''; + } +} diff --git a/src/utils/context/LoginContext.ts b/src/utils/context/LoginContext.ts new file mode 100644 index 0000000..c749114 --- /dev/null +++ b/src/utils/context/LoginContext.ts @@ -0,0 +1,34 @@ +import React from 'react'; + +/** + * global context definitions + */ + +export enum LoginState { + LoggedIn, + LoggedOut +} + +export enum LoginPerm { + Admin, + User +} + +export interface LoginContextType { + logout: () => void; + setPerm: (permission: LoginPerm) => void; + loginstate: LoginState; + setLoginState: (state: LoginState) => void; + permission: LoginPerm; +} + +/** + * A global context providing a way to interact with user login states + */ +export const LoginContext = React.createContext({ + setLoginState(): void {}, + setPerm(): void {}, + logout: () => {}, + loginstate: LoginState.LoggedOut, + permission: LoginPerm.User +}); diff --git a/src/utils/context/LoginContextProvider.tsx b/src/utils/context/LoginContextProvider.tsx new file mode 100644 index 0000000..144441b --- /dev/null +++ b/src/utils/context/LoginContextProvider.tsx @@ -0,0 +1,105 @@ +import {LoginContext, LoginPerm, LoginState} from './LoginContext'; +import React, {FunctionComponent, useContext, useEffect, useState} from 'react'; +import {useHistory, useLocation} from 'react-router'; +import {cookie} from './Cookie'; +import {APINode, callAPI} from '../Api'; +import {SettingsTypes} from '../../types/ApiTypes'; +import GlobalInfos from '../GlobalInfos'; + +export const LoginContextProvider: FunctionComponent = (props): JSX.Element => { + let initialLoginState = LoginState.LoggedIn; + let initialUserPerm = LoginPerm.User; + + const t = cookie.Load(); + // we are already logged in so we can set the token and redirect to dashboard + if (t !== null) { + initialLoginState = LoginState.LoggedIn; + } + + const initialAPICall = (): void => { + // this is the first api call so if it fails we know there is no connection to backend + callAPI( + APINode.Settings, + {action: 'loadInitialData'}, + (result: SettingsTypes.initialApiCallData) => { + // set theme + GlobalInfos.enableDarkTheme(result.DarkMode); + + GlobalInfos.setVideoPaths(result.VideoPath, result.TVShowPath); + + GlobalInfos.setTVShowsEnabled(result.TVShowEnabled); + GlobalInfos.setFullDeleteEnabled(result.FullDeleteEnabled); + // + // this.setState({ + // mediacentername: result.MediacenterName + // }); + // set tab title to received mediacenter name + document.title = result.MediacenterName; + + setLoginState(LoginState.LoggedIn); + }, + (_) => { + setLoginState(LoginState.LoggedOut); + } + ); + }; + + useEffect(() => { + initialAPICall(); + }, []); + + const [loginState, setLoginState] = useState(initialLoginState); + const [permission, setPermission] = useState(initialUserPerm); + + const hist = useHistory(); + const loc = useLocation(); + + // trigger redirect on loginstate change + useEffect(() => { + if (loginState === LoginState.LoggedIn) { + // if we arent already in dashboard tree we want to redirect to default dashboard page + console.log('redirecting to dashboard' + loc.pathname); + if (!loc.pathname.startsWith('/media')) { + hist.replace('/media'); + } + } else { + if (!loc.pathname.startsWith('/login')) { + hist.replace('/login'); + } + } + }, [hist, loc.pathname, loginState]); + + const value = { + logout: (): void => { + setLoginState(LoginState.LoggedOut); + cookie.Delete(); + }, + setPerm: (perm: LoginPerm): void => { + setPermission(perm); + }, + setLoginState: (state: LoginState): void => { + setLoginState(state); + }, + loginstate: loginState, + permission: permission + }; + + return {props.children}; +}; + +interface Props { + perm: LoginPerm; +} + +/** + * Wrapper element to render children only if permissions are sufficient + */ +export const AuthorizedContext: FunctionComponent = (props): JSX.Element => { + const loginctx = useContext(LoginContext); + + if (loginctx.permission <= props.perm) { + return props.children as JSX.Element; + } else { + return <>; + } +};