basic frontend implementation of new token system

This commit is contained in:
lukas 2021-09-19 23:20:37 +02:00
parent e985eb941c
commit f17bac399a
17 changed files with 436 additions and 460 deletions

View File

@ -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()

View File

@ -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,
})
})
}

View File

@ -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 (
<AuthenticationPage
onSuccessLogin={(): void => {
this.setState({password: false});
// reinit general infos
this.initialAPICall();
}}
/>
);
} else if (this.state.password === false) {
return (
<Router>
<div className={style.app}>
return (
<LoginContextProvider>
<Switch>
<Route path='/login'>
<AuthenticationPage
onSuccessLogin={(): void => {
// this.setState({password: false});
// reinit general infos
// this.initialAPICall();
}}
/>
</Route>
<Route path='/media'>
{this.navBar()}
{this.routing()}
</div>
</Router>
);
} else {
return <>still loading...</>;
}
<MyRouter />
</Route>
</Switch>
</LoginContextProvider>
);
// if (this.state.password === true) {
// // render authentication page if auth is neccessary
// return (
// <AuthenticationPage
// onSuccessLogin={(): void => {
// this.setState({password: false});
// // reinit general infos
// this.initialAPICall();
// }}
// />
// );
// } else if (this.state.password === false) {
// return (
// <Router>
// <div className={style.app}>
// {this.navBar()}
// {this.routing()}
// </div>
// </Router>
// );
// } else {
// return <>still loading...</>;
// }
}
/**
@ -139,73 +98,84 @@ class App extends React.Component<{}, state> {
return (
<div className={[style.navcontainer, themeStyle.backgroundcolor, themeStyle.textcolor, themeStyle.hrcolor].join(' ')}>
<div className={style.navbrand}>{this.state.mediacentername}</div>
<NavLink className={[style.navitem, themeStyle.navitem].join(' ')} to={'/'} activeStyle={{opacity: '0.85'}}>
<NavLink className={[style.navitem, themeStyle.navitem].join(' ')} to={'/media'} activeStyle={{opacity: '0.85'}}>
Home
</NavLink>
<NavLink className={[style.navitem, themeStyle.navitem].join(' ')} to={'/random'} activeStyle={{opacity: '0.85'}}>
<NavLink
className={[style.navitem, themeStyle.navitem].join(' ')}
to={'/media/random'}
activeStyle={{opacity: '0.85'}}>
Random Video
</NavLink>
<NavLink className={[style.navitem, themeStyle.navitem].join(' ')} to={'/categories'} activeStyle={{opacity: '0.85'}}>
<NavLink
className={[style.navitem, themeStyle.navitem].join(' ')}
to={'/media/categories'}
activeStyle={{opacity: '0.85'}}>
Categories
</NavLink>
{GlobalInfos.isTVShowEnabled() ? (
<NavLink className={[style.navitem, themeStyle.navitem].join(' ')} to={'/tvshows'} activeStyle={{opacity: '0.85'}}>
<NavLink
className={[style.navitem, themeStyle.navitem].join(' ')}
to={'/media/tvshows'}
activeStyle={{opacity: '0.85'}}>
TV Shows
</NavLink>
) : null}
<NavLink className={[style.navitem, themeStyle.navitem].join(' ')} to={'/settings'} activeStyle={{opacity: '0.85'}}>
<NavLink
className={[style.navitem, themeStyle.navitem].join(' ')}
to={'/media/settings'}
activeStyle={{opacity: '0.85'}}>
Settings
</NavLink>
</div>
);
}
/**
* render the react router elements
*/
routing(): JSX.Element {
return (
<Switch>
<Route path='/random'>
<RandomPage />
</Route>
<Route path='/categories'>
<CategoryPage />
</Route>
<Route path='/settings'>
<SettingsPage />
</Route>
<Route exact path='/player/:id'>
<Player />
</Route>
<Route exact path='/actors'>
<ActorOverviewPage />
</Route>
<Route path='/actors/:id'>
<ActorPage />
</Route>
{GlobalInfos.isTVShowEnabled() ? (
<Route path='/tvshows'>
<TVShowPage />
</Route>
) : null}
{GlobalInfos.isTVShowEnabled() ? (
<Route exact path='/tvplayer/:id'>
<TVPlayer />
</Route>
) : null}
<Route path='/'>
<HomePage />
</Route>
</Switch>
);
}
}
const MyRouter = (): JSX.Element => {
const match = useRouteMatch();
return (
<Switch>
<Route exact path={`${match.url}/random`}>
<RandomPage />
</Route>
<Route path={`${match.url}/categories`}>
<CategoryPage />
</Route>
<Route path={`${match.url}/settings`}>
<SettingsPage />
</Route>
<Route exact path={`${match.url}/player/:id`}>
<Player />
</Route>
<Route exact path={`${match.url}/actors`}>
<ActorOverviewPage />
</Route>
<Route exact path={`${match.url}/actors/:id`}>
<ActorPage />
</Route>
{GlobalInfos.isTVShowEnabled() ? (
<Route exact path={`${match.url}/tvshows`}>
<TVShowPage />
</Route>
) : null}
{GlobalInfos.isTVShowEnabled() ? (
<Route exact path={`${match.url}/tvplayer/:id`}>
<TVPlayer />
</Route>
) : null}
<Route path={`${match.url}/`}>
<HomePage />
</Route>
</Switch>
);
};
export default App;

View File

@ -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}>

View File

@ -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(
<React.StrictMode>
<App />
<BrowserRouter>
<App />
</BrowserRouter>
</React.StrictMode>,
document.getElementById('root')
);

View File

@ -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<Props, state> {
removeKeyHandler(this.keypress);
}
static contextType = LoginContext;
render(): JSX.Element {
return (
<>
@ -76,21 +80,17 @@ class AuthenticationPage extends React.Component<Props, state> {
* 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);
}
);
}

View File

@ -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 (
<Switch>
<Route path='/categories/:id'>
<CategoryViewWR />
</Route>
<Route path='/categories'>
<TagView />
</Route>
</Switch>
);
}
}
const CategoryPage = (): JSX.Element => {
const match = useRouteMatch();
console.log(match.url);
return (
<Switch>
<Route exact path={`${match.url}/:id`}>
<CategoryViewWR />
</Route>
<Route exact path={`${match.url}/`}>
<TagView />
</Route>
</Switch>
);
};
export default CategoryPage;

View File

@ -119,9 +119,10 @@ export class CategoryView extends React.Component<CategoryViewProps, CategoryVie
this.videodata = result.Videos;
this.setState({loaded: true, TagName: result.TagName});
},
(_) => {
(e) => {
console.log(e);
// if there is an load error redirect to home page
this.props.history.push('/');
// this.props.history.push('/');
}
);
}

View File

@ -56,7 +56,7 @@ class TagView extends React.Component<Props, TagViewState> {
<DynamicContentContainer
data={this.state.loadedtags}
renderElement={(m): JSX.Element => (
<Link to={'/categories/' + m.TagId} key={m.TagId}>
<Link to={'/media/categories/' + m.TagId} key={m.TagId}>
<TagPreview name={m.TagName} />
</Link>
)}

View File

@ -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 (
<div>
<div className={style.SettingsSidebar + ' ' + themestyle.secbackground}>
<div className={style.SettingsSidebarTitle + ' ' + themestyle.lighttextcolor}>Settings</div>
<NavLink to='/settings/general'>
<div className={style.SettingSidebarElement}>General</div>
const SettingsPage = (): JSX.Element => {
const themestyle = GlobalInfos.getThemeStyle();
const match = useRouteMatch();
return (
<div>
<div className={style.SettingsSidebar + ' ' + themestyle.secbackground}>
<div className={style.SettingsSidebarTitle + ' ' + themestyle.lighttextcolor}>Settings</div>
<NavLink to='/media/settings/general'>
<div className={style.SettingSidebarElement}>General</div>
</NavLink>
<NavLink to='/media/settings/movies'>
<div className={style.SettingSidebarElement}>Movies</div>
</NavLink>
{GlobalInfos.isTVShowEnabled() ? (
<NavLink to='/media/settings/tv'>
<div className={style.SettingSidebarElement}>TV Shows</div>
</NavLink>
<NavLink to='/settings/movies'>
<div className={style.SettingSidebarElement}>Movies</div>
</NavLink>
{GlobalInfos.isTVShowEnabled() ? (
<NavLink to='/settings/tv'>
<div className={style.SettingSidebarElement}>TV Shows</div>
</NavLink>
) : null}
</div>
<div className={style.SettingsContent}>
<Switch>
<Route path='/settings/general'>
<GeneralSettings />
</Route>
<Route path='/settings/movies'>
<MovieSettings />
</Route>
{GlobalInfos.isTVShowEnabled() ? (
<Route path='/settings/tv'>
<span />
</Route>
) : null}
<Route path='/settings'>
<Redirect to='/settings/general' />
</Route>
</Switch>
</div>
) : null}
</div>
);
}
}
<div className={style.SettingsContent}>
<Switch>
<Route path={`${match.url}/general`}>
<GeneralSettings />
</Route>
<Route path={`${match.url}/movies`}>
<MovieSettings />
</Route>
{GlobalInfos.isTVShowEnabled() ? (
<Route path={`${match.url}/tv`}>
<span />
</Route>
) : null}
<Route path={`${match.url}/`}>
<Redirect to='/media/settings/general' />
</Route>
</Switch>
</div>
</div>
);
};
export default SettingsPage;

View File

@ -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<T>(
callback: (_: T) => void,
errorcallback: (_: string) => void = (_: string): void => {}
): void {
token.checkAPITokenValid((mytoken) => {
generalAPICall<T>(apinode, fd, callback, errorcallback, false, true, mytoken);
});
generalAPICall<T>(apinode, fd, callback, errorcallback, false, true);
}
/**
@ -43,7 +41,7 @@ export function callApiUnsafe<T>(
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<T>(
* @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<T>(
@ -64,16 +60,16 @@ function generalAPICall<T>(
callback: (_: T) => void,
errorcallback: (_: string) => void = (_: string): void => {},
unsafe: boolean,
json: boolean,
mytoken: string
json: boolean
): void {
(async function (): Promise<void> {
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<T>(
// eslint-disable-next-line no-shadow
export enum APINode {
Login = 'login',
Settings = 'settings',
Tags = 'tags',
Actor = 'actor',

View File

@ -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);
}
}
}

View File

@ -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 '';
}
}

View File

@ -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;
}

View File

@ -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 '';
}
}

View File

@ -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<LoginContextType>({
setLoginState(): void {},
setPerm(): void {},
logout: () => {},
loginstate: LoginState.LoggedOut,
permission: LoginPerm.User
});

View File

@ -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<LoginState>(initialLoginState);
const [permission, setPermission] = useState<LoginPerm>(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 <LoginContext.Provider value={value}>{props.children}</LoginContext.Provider>;
};
interface Props {
perm: LoginPerm;
}
/**
* Wrapper element to render children only if permissions are sufficient
*/
export const AuthorizedContext: FunctionComponent<Props> = (props): JSX.Element => {
const loginctx = useContext(LoginContext);
if (loginctx.permission <= props.perm) {
return props.children as JSX.Element;
} else {
return <></>;
}
};