basic frontend implementation of new token system
This commit is contained in:
parent
e985eb941c
commit
f17bac399a
@ -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,6 +33,12 @@ 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) {
|
||||
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 {
|
||||
tokenheader := request.Header.Get("Token")
|
||||
|
||||
id := -1
|
||||
@ -46,6 +53,15 @@ func AddHandler(action string, apiNode string, perm uint8, handler func(ctx Cont
|
||||
|
||||
// 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 {
|
||||
@ -53,11 +69,6 @@ func AddHandler(action string, apiNode string, perm uint8, handler func(ctx Cont
|
||||
ctx.Error("Unknown server Error occured")
|
||||
writer.WriteHeader(501)
|
||||
}
|
||||
} else {
|
||||
ctx.Error("insufficient permissions")
|
||||
writer.WriteHeader(501)
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
func ServerInit() {
|
||||
|
@ -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,
|
||||
})
|
||||
|
||||
@ -91,17 +89,11 @@ func InitOAuth() {
|
||||
|
||||
type ResponseType struct {
|
||||
Token Token
|
||||
Username string
|
||||
UserPerm uint8
|
||||
}
|
||||
|
||||
ctx.Json(ResponseType{
|
||||
Token: Token{
|
||||
ctx.Json(Token{
|
||||
Token: token,
|
||||
ExpiresAt: expires,
|
||||
},
|
||||
Username: t.Username,
|
||||
UserPerm: rightid,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
168
src/App.tsx
168
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 (
|
||||
<LoginContextProvider>
|
||||
<Switch>
|
||||
<Route path='/login'>
|
||||
<AuthenticationPage
|
||||
onSuccessLogin={(): void => {
|
||||
this.setState({password: false});
|
||||
// this.setState({password: false});
|
||||
// reinit general infos
|
||||
this.initialAPICall();
|
||||
// this.initialAPICall();
|
||||
}}
|
||||
/>
|
||||
);
|
||||
} else if (this.state.password === false) {
|
||||
return (
|
||||
<Router>
|
||||
<div className={style.app}>
|
||||
</Route>
|
||||
<Route path='/media'>
|
||||
{this.navBar()}
|
||||
{this.routing()}
|
||||
</div>
|
||||
</Router>
|
||||
<MyRouter />
|
||||
</Route>
|
||||
</Switch>
|
||||
</LoginContextProvider>
|
||||
);
|
||||
} else {
|
||||
return <>still loading...</>;
|
||||
}
|
||||
|
||||
// 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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const MyRouter = (): JSX.Element => {
|
||||
const match = useRouteMatch();
|
||||
|
||||
/**
|
||||
* render the react router elements
|
||||
*/
|
||||
routing(): JSX.Element {
|
||||
return (
|
||||
<Switch>
|
||||
<Route path='/random'>
|
||||
<Route exact path={`${match.url}/random`}>
|
||||
<RandomPage />
|
||||
</Route>
|
||||
<Route path='/categories'>
|
||||
<Route path={`${match.url}/categories`}>
|
||||
<CategoryPage />
|
||||
</Route>
|
||||
<Route path='/settings'>
|
||||
<Route path={`${match.url}/settings`}>
|
||||
<SettingsPage />
|
||||
</Route>
|
||||
<Route exact path='/player/:id'>
|
||||
<Route exact path={`${match.url}/player/:id`}>
|
||||
<Player />
|
||||
</Route>
|
||||
<Route exact path='/actors'>
|
||||
<Route exact path={`${match.url}/actors`}>
|
||||
<ActorOverviewPage />
|
||||
</Route>
|
||||
<Route path='/actors/:id'>
|
||||
<Route exact path={`${match.url}/actors/:id`}>
|
||||
<ActorPage />
|
||||
</Route>
|
||||
|
||||
{GlobalInfos.isTVShowEnabled() ? (
|
||||
<Route path='/tvshows'>
|
||||
<Route exact path={`${match.url}/tvshows`}>
|
||||
<TVShowPage />
|
||||
</Route>
|
||||
) : null}
|
||||
|
||||
{GlobalInfos.isTVShowEnabled() ? (
|
||||
<Route exact path='/tvplayer/:id'>
|
||||
<Route exact path={`${match.url}/tvplayer/:id`}>
|
||||
<TVPlayer />
|
||||
</Route>
|
||||
) : null}
|
||||
|
||||
<Route path='/'>
|
||||
<Route path={`${match.url}/`}>
|
||||
<HomePage />
|
||||
</Route>
|
||||
</Switch>
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default App;
|
||||
|
@ -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}>
|
||||
|
@ -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>
|
||||
<BrowserRouter>
|
||||
<App />
|
||||
</BrowserRouter>
|
||||
</React.StrictMode>,
|
||||
document.getElementById('root')
|
||||
);
|
||||
|
@ -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);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -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 {
|
||||
const CategoryPage = (): JSX.Element => {
|
||||
const match = useRouteMatch();
|
||||
|
||||
console.log(match.url);
|
||||
|
||||
return (
|
||||
<Switch>
|
||||
<Route path='/categories/:id'>
|
||||
<Route exact path={`${match.url}/:id`}>
|
||||
<CategoryViewWR />
|
||||
</Route>
|
||||
<Route path='/categories'>
|
||||
<Route exact path={`${match.url}/`}>
|
||||
<TagView />
|
||||
</Route>
|
||||
</Switch>
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default CategoryPage;
|
||||
|
@ -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('/');
|
||||
}
|
||||
);
|
||||
}
|
||||
|
@ -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>
|
||||
)}
|
||||
|
@ -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 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='/settings/general'>
|
||||
<NavLink to='/media/settings/general'>
|
||||
<div className={style.SettingSidebarElement}>General</div>
|
||||
</NavLink>
|
||||
<NavLink to='/settings/movies'>
|
||||
<NavLink to='/media/settings/movies'>
|
||||
<div className={style.SettingSidebarElement}>Movies</div>
|
||||
</NavLink>
|
||||
{GlobalInfos.isTVShowEnabled() ? (
|
||||
<NavLink to='/settings/tv'>
|
||||
<NavLink to='/media/settings/tv'>
|
||||
<div className={style.SettingSidebarElement}>TV Shows</div>
|
||||
</NavLink>
|
||||
) : null}
|
||||
</div>
|
||||
<div className={style.SettingsContent}>
|
||||
<Switch>
|
||||
<Route path='/settings/general'>
|
||||
<Route path={`${match.url}/general`}>
|
||||
<GeneralSettings />
|
||||
</Route>
|
||||
<Route path='/settings/movies'>
|
||||
<Route path={`${match.url}/movies`}>
|
||||
<MovieSettings />
|
||||
</Route>
|
||||
{GlobalInfos.isTVShowEnabled() ? (
|
||||
<Route path='/settings/tv'>
|
||||
<Route path={`${match.url}/tv`}>
|
||||
<span />
|
||||
</Route>
|
||||
) : null}
|
||||
<Route path='/settings'>
|
||||
<Redirect to='/settings/general' />
|
||||
<Route path={`${match.url}/`}>
|
||||
<Redirect to='/media/settings/general' />
|
||||
</Route>
|
||||
</Switch>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default SettingsPage;
|
||||
|
@ -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',
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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 '';
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
55
src/utils/context/Cookie.ts
Normal file
55
src/utils/context/Cookie.ts
Normal 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 '';
|
||||
}
|
||||
}
|
34
src/utils/context/LoginContext.ts
Normal file
34
src/utils/context/LoginContext.ts
Normal 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
|
||||
});
|
105
src/utils/context/LoginContextProvider.tsx
Normal file
105
src/utils/context/LoginContextProvider.tsx
Normal 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 <></>;
|
||||
}
|
||||
};
|
Loading…
Reference in New Issue
Block a user