-
-
Settings
-
- General
+const SettingsPage = (): JSX.Element => {
+ const themestyle = GlobalInfos.getThemeStyle();
+ const match = useRouteMatch();
+ const features = useContext(FeatureContext);
+
+ return (
+
+
+
Settings
+
+ General
+
+
+ Movies
+
+ {features.TVShowEnabled ? (
+
+ TV Shows
-
- Movies
-
- {GlobalInfos.isTVShowEnabled() ? (
-
- TV Shows
-
- ) : null}
-
-
-
-
-
-
-
-
-
- {GlobalInfos.isTVShowEnabled() ? (
-
-
-
- ) : null}
-
-
-
-
-
+ ) : null}
- );
- }
-}
+
+
+
+
+
+
+
+
+ {features.TVShowEnabled ? (
+
+
+
+ ) : null}
+
+
+
+
+
+
+ );
+};
export default SettingsPage;
diff --git a/src/pages/TVShowPage/TVShowPage.tsx b/src/pages/TVShowPage/TVShowPage.tsx
index eee8d71..2465ca1 100644
--- a/src/pages/TVShowPage/TVShowPage.tsx
+++ b/src/pages/TVShowPage/TVShowPage.tsx
@@ -72,7 +72,7 @@ export default function (): JSX.Element {
return (
-
+
diff --git a/src/setupTests.js b/src/setupTests.js
index 0ffa066..41b341b 100644
--- a/src/setupTests.js
+++ b/src/setupTests.js
@@ -6,8 +6,6 @@ import '@testing-library/jest-dom/extend-expect';
import {configure} from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
-import {CookieTokenStore} from "./utils/TokenStore/CookieTokenStore";
-import {token} from "./utils/TokenHandler";
configure({adapter: new Adapter()});
@@ -45,7 +43,6 @@ global.callAPIMock = (resonse) => {
global.beforeEach(() => {
// empty fetch response implementation for each test
global.fetch = prepareFetchApi({});
- token.init(new CookieTokenStore());
// todo with callAPIMock
});
diff --git a/src/utils/Api.ts b/src/utils/Api.ts
index 5fb447d..e0d344d 100644
--- a/src/utils/Api.ts
+++ b/src/utils/Api.ts
@@ -1,5 +1,4 @@
-import GlobalInfos from './GlobalInfos';
-import {token} from './TokenHandler';
+import {cookie} from './context/Cookie';
const APIPREFIX: string = '/api/';
@@ -25,9 +24,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 +40,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 +50,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 +59,16 @@ function generalAPICall(
callback: (_: T) => void,
errorcallback: (_: string) => void = (_: string): void => {},
unsafe: boolean,
- json: boolean,
- mytoken: string
+ json: boolean
): void {
(async function (): Promise {
- const response = await fetch(APIPREFIX + apinode, {
+ 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})
})
});
@@ -88,13 +83,7 @@ function generalAPICall(
}
} else if (response.status === 400) {
// Bad Request --> invalid token
- console.log('loading Password page.');
- // load password page
- if (GlobalInfos.loadPasswordPage) {
- GlobalInfos.loadPasswordPage(() => {
- callAPI(apinode, fd, callback, errorcallback);
- });
- }
+ console.log('bad request todo sth here');
} else {
console.log('Error: ' + response.statusText);
if (errorcallback) {
@@ -108,8 +97,8 @@ function generalAPICall(
* API nodes definitions
*/
-// eslint-disable-next-line no-shadow
export enum APINode {
+ Login = 'login',
Settings = 'settings',
Tags = 'tags',
Actor = 'actor',
diff --git a/src/utils/GlobalInfos.ts b/src/utils/GlobalInfos.ts
index 170a479..70f7495 100644
--- a/src/utils/GlobalInfos.ts
+++ b/src/utils/GlobalInfos.ts
@@ -49,6 +49,7 @@ class StaticInfos {
/**
* set the current videopath
* @param vidpath videopath with beginning and ending slash
+ * @param tvshowpath
*/
setVideoPaths(vidpath: string, tvshowpath: string): void {
this.videopath = vidpath;
@@ -68,27 +69,6 @@ class StaticInfos {
getTVShowPath(): string {
return this.tvshowpath;
}
-
- /**
- * load the Password page manually
- */
- loadPasswordPage: ((callback?: () => void) => void) | undefined = undefined;
-
- setTVShowsEnabled(TVShowEnabled: boolean): void {
- this.TVShowsEnabled = TVShowEnabled;
- }
-
- isTVShowEnabled(): boolean {
- return this.TVShowsEnabled;
- }
-
- setFullDeleteEnabled(FullDeleteEnabled: boolean): void {
- this.fullDeleteable = FullDeleteEnabled;
- }
-
- isVideoFulldeleteable(): boolean {
- return this.fullDeleteable;
- }
}
export default new StaticInfos();
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/FeatureContext.tsx b/src/utils/context/FeatureContext.tsx
new file mode 100644
index 0000000..88c084a
--- /dev/null
+++ b/src/utils/context/FeatureContext.tsx
@@ -0,0 +1,32 @@
+import React, {FunctionComponent, useState} from 'react';
+
+export interface FeatureContextType {
+ setTVShowEnabled: (enabled: boolean) => void;
+ TVShowEnabled: boolean;
+ setVideosFullyDeleteable: (fullyDeletable: boolean) => void;
+ VideosFullyDeleteable: boolean;
+}
+
+/**
+ * A global context providing a way to interact with user login states
+ */
+export const FeatureContext = React.createContext({
+ setTVShowEnabled: (_) => {},
+ TVShowEnabled: false,
+ setVideosFullyDeleteable: (_) => {},
+ VideosFullyDeleteable: false
+});
+
+export const FeatureContextProvider: FunctionComponent = (props): JSX.Element => {
+ const [tvshowenabled, settvshowenabled] = useState(false);
+ const [fullydeletablevids, setfullydeleteable] = useState(false);
+
+ const value: FeatureContextType = {
+ VideosFullyDeleteable: fullydeletablevids,
+ TVShowEnabled: tvshowenabled,
+ setTVShowEnabled: (e) => settvshowenabled(e),
+ setVideosFullyDeleteable: (e) => setfullydeleteable(e)
+ };
+
+ return {props.children};
+};
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..1c5d418
--- /dev/null
+++ b/src/utils/context/LoginContextProvider.tsx
@@ -0,0 +1,104 @@
+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';
+import {FeatureContext} from './FeatureContext';
+
+export const LoginContextProvider: FunctionComponent = (props): JSX.Element => {
+ let initialLoginState = LoginState.LoggedIn;
+ let initialUserPerm = LoginPerm.User;
+
+ const features = useContext(FeatureContext);
+
+ 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;
+ }
+
+ useEffect(() => {
+ // 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);
+
+ features.setTVShowEnabled(result.TVShowEnabled);
+ features.setVideosFullyDeleteable(result.FullDeleteEnabled);
+
+ // this.setState({
+ // mediacentername: result.MediacenterName
+ // });
+ // set tab title to received mediacenter name
+ document.title = result.MediacenterName;
+
+ setLoginState(LoginState.LoggedIn);
+ },
+ (_) => {
+ setLoginState(LoginState.LoggedOut);
+ }
+ );
+ }, [features]);
+
+ 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 <>>;
+ }
+};