Merge branch 'passwordpage' into 'master'
PasswordPage if enabled in settings See merge request lukas/openmediacenter!40
This commit is contained in:
commit
ba2704b285
@ -16,6 +16,7 @@ const (
|
|||||||
TagNode = iota
|
TagNode = iota
|
||||||
SettingsNode = iota
|
SettingsNode = iota
|
||||||
ActorNode = iota
|
ActorNode = iota
|
||||||
|
InitNode = iota
|
||||||
)
|
)
|
||||||
|
|
||||||
type actionStruct struct {
|
type actionStruct struct {
|
||||||
@ -42,6 +43,9 @@ func ServerInit(port uint16) {
|
|||||||
http.Handle(APIPREFIX+"/settings", oauth.ValidateToken(settingsHandler))
|
http.Handle(APIPREFIX+"/settings", oauth.ValidateToken(settingsHandler))
|
||||||
http.Handle(APIPREFIX+"/actor", oauth.ValidateToken(actorHandler))
|
http.Handle(APIPREFIX+"/actor", oauth.ValidateToken(actorHandler))
|
||||||
|
|
||||||
|
// initialization api calls to check if password is neccessaray
|
||||||
|
http.Handle(APIPREFIX+"/init", http.HandlerFunc(initHandler))
|
||||||
|
|
||||||
// initialize oauth service and add corresponding auth routes
|
// initialize oauth service and add corresponding auth routes
|
||||||
oauth.InitOAuth()
|
oauth.InitOAuth()
|
||||||
|
|
||||||
@ -85,6 +89,10 @@ func settingsHandler(rw http.ResponseWriter, req *http.Request) {
|
|||||||
handlefunc(rw, req, SettingsNode)
|
handlefunc(rw, req, SettingsNode)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func initHandler(rw http.ResponseWriter, req *http.Request) {
|
||||||
|
handlefunc(rw, req, InitNode)
|
||||||
|
}
|
||||||
|
|
||||||
func handlefunc(rw http.ResponseWriter, req *http.Request, node int) {
|
func handlefunc(rw http.ResponseWriter, req *http.Request, node int) {
|
||||||
// only allow post requests
|
// only allow post requests
|
||||||
if req.Method != "POST" {
|
if req.Method != "POST" {
|
||||||
|
33
apiGo/api/Init.go
Normal file
33
apiGo/api/Init.go
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"openmediacenter/apiGo/database/settings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func AddInitHandlers() {
|
||||||
|
passwordNeeded()
|
||||||
|
}
|
||||||
|
|
||||||
|
func passwordNeeded() {
|
||||||
|
AddHandler("loadInitialData", InitNode, nil, func() []byte {
|
||||||
|
sett := settings.LoadSettings()
|
||||||
|
|
||||||
|
type InitialDataTypeResponse struct {
|
||||||
|
DarkMode bool
|
||||||
|
Pasword bool
|
||||||
|
MediacenterName string
|
||||||
|
VideoPath string
|
||||||
|
}
|
||||||
|
|
||||||
|
res := InitialDataTypeResponse{
|
||||||
|
DarkMode: sett.DarkMode,
|
||||||
|
Pasword: sett.Pasword != "-1",
|
||||||
|
MediacenterName: sett.Mediacenter_name,
|
||||||
|
VideoPath: sett.VideoPath,
|
||||||
|
}
|
||||||
|
|
||||||
|
str, _ := json.Marshal(res)
|
||||||
|
return str
|
||||||
|
})
|
||||||
|
}
|
@ -1,8 +1,6 @@
|
|||||||
package api
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"openmediacenter/apiGo/api/types"
|
"openmediacenter/apiGo/api/types"
|
||||||
"openmediacenter/apiGo/database"
|
"openmediacenter/apiGo/database"
|
||||||
"openmediacenter/apiGo/videoparser"
|
"openmediacenter/apiGo/videoparser"
|
||||||
@ -15,41 +13,6 @@ func AddSettingsHandlers() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func getSettingsFromDB() {
|
func getSettingsFromDB() {
|
||||||
AddHandler("loadInitialData", SettingsNode, nil, func() []byte {
|
|
||||||
query := "SELECT DarkMode, password, mediacenter_name, video_path from settings"
|
|
||||||
|
|
||||||
type InitialDataType struct {
|
|
||||||
DarkMode int
|
|
||||||
Pasword int
|
|
||||||
Mediacenter_name string
|
|
||||||
VideoPath string
|
|
||||||
}
|
|
||||||
|
|
||||||
result := InitialDataType{}
|
|
||||||
|
|
||||||
err := database.QueryRow(query).Scan(&result.DarkMode, &result.Pasword, &result.Mediacenter_name, &result.VideoPath)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println("error while parsing db data: " + err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
type InitialDataTypeResponse struct {
|
|
||||||
DarkMode bool
|
|
||||||
Pasword bool
|
|
||||||
Mediacenter_name string
|
|
||||||
VideoPath string
|
|
||||||
}
|
|
||||||
|
|
||||||
res := InitialDataTypeResponse{
|
|
||||||
DarkMode: result.DarkMode != 0,
|
|
||||||
Pasword: result.Pasword != -1,
|
|
||||||
Mediacenter_name: result.Mediacenter_name,
|
|
||||||
VideoPath: result.VideoPath,
|
|
||||||
}
|
|
||||||
|
|
||||||
str, _ := json.Marshal(res)
|
|
||||||
return str
|
|
||||||
})
|
|
||||||
|
|
||||||
AddHandler("loadGeneralSettings", SettingsNode, nil, func() []byte {
|
AddHandler("loadGeneralSettings", SettingsNode, nil, func() []byte {
|
||||||
result := database.GetSettings()
|
result := database.GetSettings()
|
||||||
return jsonify(result)
|
return jsonify(result)
|
||||||
|
56
apiGo/api/oauth/CustomClientStore.go
Normal file
56
apiGo/api/oauth/CustomClientStore.go
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
package oauth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"gopkg.in/oauth2.v3"
|
||||||
|
"openmediacenter/apiGo/database/settings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CustomClientStore struct {
|
||||||
|
oauth2.ClientStore
|
||||||
|
}
|
||||||
|
|
||||||
|
type CustomClientInfo struct {
|
||||||
|
oauth2.ClientInfo
|
||||||
|
ID string
|
||||||
|
Secret string
|
||||||
|
Domain string
|
||||||
|
UserID string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewCustomStore() oauth2.ClientStore {
|
||||||
|
s := new(CustomClientStore)
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *CustomClientStore) GetByID(id string) (oauth2.ClientInfo, error) {
|
||||||
|
password := settings.GetPassword()
|
||||||
|
if password == nil {
|
||||||
|
defaultpassword := "openmediacenter"
|
||||||
|
password = &defaultpassword
|
||||||
|
}
|
||||||
|
|
||||||
|
clientinfo := CustomClientInfo{
|
||||||
|
ID: "openmediacenter",
|
||||||
|
Secret: *password,
|
||||||
|
Domain: "http://localhost:8081",
|
||||||
|
UserID: "openmediacenter",
|
||||||
|
}
|
||||||
|
|
||||||
|
return &clientinfo, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *CustomClientInfo) GetID() string {
|
||||||
|
return a.ID
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *CustomClientInfo) GetSecret() string {
|
||||||
|
return a.Secret
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *CustomClientInfo) GetDomain() string {
|
||||||
|
return a.Domain
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *CustomClientInfo) GetUserID() string {
|
||||||
|
return a.UserID
|
||||||
|
}
|
@ -3,7 +3,7 @@ package oauth
|
|||||||
import (
|
import (
|
||||||
"gopkg.in/oauth2.v3/errors"
|
"gopkg.in/oauth2.v3/errors"
|
||||||
"gopkg.in/oauth2.v3/manage"
|
"gopkg.in/oauth2.v3/manage"
|
||||||
"gopkg.in/oauth2.v3/models"
|
//"gopkg.in/oauth2.v3/models"
|
||||||
"gopkg.in/oauth2.v3/server"
|
"gopkg.in/oauth2.v3/server"
|
||||||
"gopkg.in/oauth2.v3/store"
|
"gopkg.in/oauth2.v3/store"
|
||||||
"log"
|
"log"
|
||||||
@ -17,15 +17,19 @@ func InitOAuth() {
|
|||||||
// token store
|
// token store
|
||||||
manager.MustTokenStorage(store.NewMemoryTokenStore())
|
manager.MustTokenStorage(store.NewMemoryTokenStore())
|
||||||
|
|
||||||
clientStore := store.NewClientStore()
|
//clientStore := store.NewClientStore()
|
||||||
// todo we need to check here if a password is enabled in db -- when yes set it here!
|
//// todo we need to check here if a password is enabled in db -- when yes set it here!
|
||||||
clientStore.Set("openmediacenter", &models.Client{
|
//clientStore.Set("openmediacenter", &models.Client{
|
||||||
ID: "openmediacenter",
|
// ID: "openmediacenter",
|
||||||
Secret: "openmediacenter",
|
// Secret: "openmediacenter",
|
||||||
Domain: "http://localhost:8081",
|
// Domain: "http://localhost:8081",
|
||||||
})
|
//})
|
||||||
|
//
|
||||||
|
//manager.MapClientStorage(clientStore)
|
||||||
|
|
||||||
|
strtest := NewCustomStore()
|
||||||
|
manager.MapClientStorage(strtest)
|
||||||
|
|
||||||
manager.MapClientStorage(clientStore)
|
|
||||||
srv = server.NewServer(server.NewConfig(), manager)
|
srv = server.NewServer(server.NewConfig(), manager)
|
||||||
srv.SetClientInfoHandler(server.ClientFormHandler)
|
srv.SetClientInfoHandler(server.ClientFormHandler)
|
||||||
manager.SetRefreshTokenCfg(manage.DefaultRefreshTokenCfg)
|
manager.SetRefreshTokenCfg(manage.DefaultRefreshTokenCfg)
|
||||||
|
49
apiGo/database/settings/DBSettings.go
Normal file
49
apiGo/database/settings/DBSettings.go
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
package settings
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"openmediacenter/apiGo/database"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GetPassword() *string {
|
||||||
|
pwd := LoadSettings().Pasword
|
||||||
|
if pwd == "-1" {
|
||||||
|
return nil
|
||||||
|
} else {
|
||||||
|
return &pwd
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type SettingsType struct {
|
||||||
|
DarkMode bool
|
||||||
|
Pasword string
|
||||||
|
Mediacenter_name string
|
||||||
|
VideoPath string
|
||||||
|
}
|
||||||
|
|
||||||
|
func LoadSettings() *SettingsType {
|
||||||
|
query := "SELECT DarkMode, password, mediacenter_name, video_path from settings"
|
||||||
|
|
||||||
|
type RawSettingsType struct {
|
||||||
|
DarkMode int
|
||||||
|
Pasword string
|
||||||
|
Mediacenter_name string
|
||||||
|
VideoPath string
|
||||||
|
}
|
||||||
|
|
||||||
|
result := RawSettingsType{}
|
||||||
|
|
||||||
|
err := database.QueryRow(query).Scan(&result.DarkMode, &result.Pasword, &result.Mediacenter_name, &result.VideoPath)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("error while parsing db data: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
res := SettingsType{
|
||||||
|
DarkMode: result.DarkMode != 0,
|
||||||
|
Pasword: result.Pasword,
|
||||||
|
Mediacenter_name: result.Mediacenter_name,
|
||||||
|
VideoPath: result.VideoPath,
|
||||||
|
}
|
||||||
|
|
||||||
|
return &res
|
||||||
|
}
|
@ -3,7 +3,6 @@ module openmediacenter/apiGo
|
|||||||
go 1.16
|
go 1.16
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/go-session/session v3.1.2+incompatible
|
|
||||||
github.com/go-sql-driver/mysql v1.5.0
|
github.com/go-sql-driver/mysql v1.5.0
|
||||||
gopkg.in/oauth2.v3 v3.12.0
|
gopkg.in/oauth2.v3 v3.12.0
|
||||||
)
|
)
|
||||||
|
@ -11,7 +11,6 @@ github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga
|
|||||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||||
github.com/gavv/httpexpect v2.0.0+incompatible h1:1X9kcRshkSKEjNJJxX9Y9mQ5BRfbxU5kORdjhlA1yX8=
|
github.com/gavv/httpexpect v2.0.0+incompatible h1:1X9kcRshkSKEjNJJxX9Y9mQ5BRfbxU5kORdjhlA1yX8=
|
||||||
github.com/gavv/httpexpect v2.0.0+incompatible/go.mod h1:x+9tiU1YnrOvnB725RkpoLv1M62hOWzwo5OXotisrKc=
|
github.com/gavv/httpexpect v2.0.0+incompatible/go.mod h1:x+9tiU1YnrOvnB725RkpoLv1M62hOWzwo5OXotisrKc=
|
||||||
github.com/go-session/session v3.1.2+incompatible h1:yStchEObKg4nk2F7JGE7KoFIrA/1Y078peagMWcrncg=
|
|
||||||
github.com/go-session/session v3.1.2+incompatible/go.mod h1:8B3iivBQjrz/JtC68Np2T1yBBLxTan3mn/3OM0CyRt0=
|
github.com/go-session/session v3.1.2+incompatible/go.mod h1:8B3iivBQjrz/JtC68Np2T1yBBLxTan3mn/3OM0CyRt0=
|
||||||
github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs=
|
github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs=
|
||||||
github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
|
github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
|
||||||
|
@ -26,6 +26,7 @@ func main() {
|
|||||||
api.AddSettingsHandlers()
|
api.AddSettingsHandlers()
|
||||||
api.AddTagHandlers()
|
api.AddTagHandlers()
|
||||||
api.AddActorsHandlers()
|
api.AddActorsHandlers()
|
||||||
|
api.AddInitHandlers()
|
||||||
|
|
||||||
api.ServerInit(8081)
|
api.ServerInit(8081)
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import App from './App';
|
import App from './App';
|
||||||
import {shallow} from 'enzyme';
|
import {shallow} from 'enzyme';
|
||||||
|
import GlobalInfos from "./utils/GlobalInfos";
|
||||||
|
|
||||||
describe('<App/>', function () {
|
describe('<App/>', function () {
|
||||||
it('renders without crashing ', function () {
|
it('renders without crashing ', function () {
|
||||||
@ -10,34 +11,37 @@ describe('<App/>', function () {
|
|||||||
|
|
||||||
it('renders title', () => {
|
it('renders title', () => {
|
||||||
const wrapper = shallow(<App/>);
|
const wrapper = shallow(<App/>);
|
||||||
|
wrapper.setState({password: false});
|
||||||
expect(wrapper.find('.navbrand').text()).toBe('OpenMediaCenter');
|
expect(wrapper.find('.navbrand').text()).toBe('OpenMediaCenter');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('are navlinks correct', function () {
|
it('are navlinks correct', function () {
|
||||||
const wrapper = shallow(<App/>);
|
const wrapper = shallow(<App/>);
|
||||||
|
wrapper.setState({password: false});
|
||||||
expect(wrapper.find('.navitem')).toHaveLength(4);
|
expect(wrapper.find('.navitem')).toHaveLength(4);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('test initial fetch from api', done => {
|
it('test initial fetch from api', done => {
|
||||||
global.fetch = global.prepareFetchApi({
|
callAPIMock({
|
||||||
generalSettingsLoaded: true,
|
MediacenterName: 'testname'
|
||||||
passwordsupport: true,
|
})
|
||||||
mediacentername: 'testname'
|
|
||||||
});
|
GlobalInfos.enableDarkTheme = jest.fn((r) => {})
|
||||||
|
|
||||||
const wrapper = shallow(<App/>);
|
const wrapper = shallow(<App/>);
|
||||||
|
|
||||||
|
|
||||||
const func = jest.fn();
|
|
||||||
wrapper.instance().setState = func;
|
|
||||||
|
|
||||||
expect(global.fetch).toBeCalledTimes(1);
|
|
||||||
|
|
||||||
process.nextTick(() => {
|
process.nextTick(() => {
|
||||||
expect(func).toBeCalledTimes(1);
|
expect(document.title).toBe('testname');
|
||||||
|
|
||||||
global.fetch.mockClear();
|
global.fetch.mockClear();
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('test render of password page', function () {
|
||||||
|
const wrapper = shallow(<App/>);
|
||||||
|
wrapper.setState({password: true});
|
||||||
|
|
||||||
|
expect(wrapper.find('AuthenticationPage')).toHaveLength(1);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
91
src/App.tsx
91
src/App.tsx
@ -9,7 +9,7 @@ import style from './App.module.css';
|
|||||||
|
|
||||||
import SettingsPage from './pages/SettingsPage/SettingsPage';
|
import SettingsPage from './pages/SettingsPage/SettingsPage';
|
||||||
import CategoryPage from './pages/CategoryPage/CategoryPage';
|
import CategoryPage from './pages/CategoryPage/CategoryPage';
|
||||||
import {APINode, callAPI} from './utils/Api';
|
import {APINode, apiTokenValid, callApiUnsafe, refreshAPIToken} from './utils/Api';
|
||||||
import {NoBackendConnectionPopup} from './elements/Popups/NoBackendConnectionPopup/NoBackendConnectionPopup';
|
import {NoBackendConnectionPopup} from './elements/Popups/NoBackendConnectionPopup/NoBackendConnectionPopup';
|
||||||
|
|
||||||
import {BrowserRouter as Router, NavLink, Route, Switch} from 'react-router-dom';
|
import {BrowserRouter as Router, NavLink, Route, Switch} from 'react-router-dom';
|
||||||
@ -17,10 +17,10 @@ import Player from './pages/Player/Player';
|
|||||||
import ActorOverviewPage from './pages/ActorOverviewPage/ActorOverviewPage';
|
import ActorOverviewPage from './pages/ActorOverviewPage/ActorOverviewPage';
|
||||||
import ActorPage from './pages/ActorPage/ActorPage';
|
import ActorPage from './pages/ActorPage/ActorPage';
|
||||||
import {SettingsTypes} from './types/ApiTypes';
|
import {SettingsTypes} from './types/ApiTypes';
|
||||||
|
import AuthenticationPage from "./pages/AuthenticationPage/AuthenticationPage";
|
||||||
|
|
||||||
interface state {
|
interface state {
|
||||||
generalSettingsLoaded: boolean;
|
password: boolean | null; // null if uninitialized - true if pwd needed false if not needed
|
||||||
passwordsupport: boolean;
|
|
||||||
mediacentername: string;
|
mediacentername: string;
|
||||||
onapierror: boolean;
|
onapierror: boolean;
|
||||||
}
|
}
|
||||||
@ -31,30 +31,48 @@ interface state {
|
|||||||
class App extends React.Component<{}, state> {
|
class App extends React.Component<{}, state> {
|
||||||
constructor(props: {}) {
|
constructor(props: {}) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
|
let pwdneeded: boolean | null = null;
|
||||||
|
|
||||||
|
if (apiTokenValid()) {
|
||||||
|
pwdneeded = false;
|
||||||
|
} else {
|
||||||
|
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 = {
|
this.state = {
|
||||||
generalSettingsLoaded: false,
|
|
||||||
passwordsupport: false,
|
|
||||||
mediacentername: 'OpenMediaCenter',
|
mediacentername: 'OpenMediaCenter',
|
||||||
onapierror: false
|
onapierror: false,
|
||||||
|
password: pwdneeded
|
||||||
};
|
};
|
||||||
|
|
||||||
|
GlobalInfos.onThemeChange(() => {
|
||||||
|
this.forceUpdate();
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
initialAPICall(): void {
|
initialAPICall(): void {
|
||||||
// this is the first api call so if it fails we know there is no connection to backend
|
// 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) => {
|
callApiUnsafe(APINode.Init, {action: 'loadInitialData'}, (result: SettingsTypes.initialApiCallData) => {
|
||||||
// set theme
|
// set theme
|
||||||
GlobalInfos.enableDarkTheme(result.DarkMode);
|
GlobalInfos.enableDarkTheme(result.DarkMode);
|
||||||
|
|
||||||
GlobalInfos.setVideoPath(result.VideoPath);
|
GlobalInfos.setVideoPath(result.VideoPath);
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
generalSettingsLoaded: true,
|
mediacentername: result.MediacenterName,
|
||||||
passwordsupport: result.Password,
|
|
||||||
mediacentername: result.Mediacenter_name,
|
|
||||||
onapierror: false
|
onapierror: false
|
||||||
});
|
});
|
||||||
// set tab title to received mediacenter name
|
// set tab title to received mediacenter name
|
||||||
document.title = result.Mediacenter_name;
|
document.title = result.MediacenterName;
|
||||||
}, error => {
|
}, error => {
|
||||||
this.setState({onapierror: true});
|
this.setState({onapierror: true});
|
||||||
});
|
});
|
||||||
@ -70,23 +88,44 @@ class App extends React.Component<{}, state> {
|
|||||||
// add the main theme to the page body
|
// add the main theme to the page body
|
||||||
document.body.className = themeStyle.backgroundcolor;
|
document.body.className = themeStyle.backgroundcolor;
|
||||||
|
|
||||||
return (
|
if (this.state.password === true) {
|
||||||
<Router>
|
return (
|
||||||
<div className={style.app}>
|
<AuthenticationPage submit={(password): void => {
|
||||||
<div className={[style.navcontainer, themeStyle.backgroundcolor, themeStyle.textcolor, themeStyle.hrcolor].join(' ')}>
|
refreshAPIToken((error) => {
|
||||||
<div className={style.navbrand}>{this.state.mediacentername}</div>
|
if (error !== '') {
|
||||||
<NavLink className={[style.navitem, themeStyle.navitem].join(' ')} to={'/'} activeStyle={{opacity: '0.85'}}>Home</NavLink>
|
console.log("wrong password!!!");
|
||||||
<NavLink className={[style.navitem, themeStyle.navitem].join(' ')} to={'/random'} activeStyle={{opacity: '0.85'}}>Random
|
} else {
|
||||||
Video</NavLink>
|
this.setState({password: false});
|
||||||
|
}
|
||||||
|
}, password);
|
||||||
|
}}/>
|
||||||
|
);
|
||||||
|
} else if (this.state.password === false) {
|
||||||
|
return (
|
||||||
|
<Router>
|
||||||
|
<div className={style.app}>
|
||||||
|
<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'}}>Home</NavLink>
|
||||||
|
<NavLink className={[style.navitem, themeStyle.navitem].join(' ')} to={'/random'}
|
||||||
|
activeStyle={{opacity: '0.85'}}>Random
|
||||||
|
Video</NavLink>
|
||||||
|
|
||||||
<NavLink className={[style.navitem, themeStyle.navitem].join(' ')} to={'/categories'} activeStyle={{opacity: '0.85'}}>Categories</NavLink>
|
<NavLink className={[style.navitem, themeStyle.navitem].join(' ')} to={'/categories'}
|
||||||
<NavLink className={[style.navitem, themeStyle.navitem].join(' ')} to={'/settings'} activeStyle={{opacity: '0.85'}}>Settings</NavLink>
|
activeStyle={{opacity: '0.85'}}>Categories</NavLink>
|
||||||
|
<NavLink className={[style.navitem, themeStyle.navitem].join(' ')} to={'/settings'}
|
||||||
|
activeStyle={{opacity: '0.85'}}>Settings</NavLink>
|
||||||
|
</div>
|
||||||
|
{this.routing()}
|
||||||
</div>
|
</div>
|
||||||
{this.routing()}
|
{this.state.onapierror ? this.ApiError() : null}
|
||||||
</div>
|
</Router>
|
||||||
{this.state.onapierror ? this.ApiError() : null}
|
);
|
||||||
</Router>
|
} else {
|
||||||
);
|
return (<>still loading...</>);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
routing(): JSX.Element {
|
routing(): JSX.Element {
|
||||||
|
52
src/pages/AuthenticationPage/AuthenticationPage.module.css
Normal file
52
src/pages/AuthenticationPage/AuthenticationPage.module.css
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
.main {
|
||||||
|
background-color: #00b3ff;
|
||||||
|
margin-left: calc(50% - 125px);
|
||||||
|
margin-top: 5%;
|
||||||
|
padding-bottom: 15px;
|
||||||
|
width: 250px;
|
||||||
|
text-align: center;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loginText {
|
||||||
|
font-size: xx-large;
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
font-weight: bolder;
|
||||||
|
}
|
||||||
|
|
||||||
|
.openmediacenterlabel {
|
||||||
|
margin-top: 5%;
|
||||||
|
text-align: center;
|
||||||
|
font-size: xxx-large;
|
||||||
|
font-weight: bold;
|
||||||
|
text-transform: capitalize;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input {
|
||||||
|
margin-left: 10px;
|
||||||
|
margin-right: 10px;
|
||||||
|
width: calc(100% - 20px);
|
||||||
|
background: transparent;
|
||||||
|
border-width: 0 0 1px 0;
|
||||||
|
color: #505050;
|
||||||
|
border-color: #505050;
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 25px;
|
||||||
|
font-size: larger;
|
||||||
|
}
|
||||||
|
|
||||||
|
::placeholder {
|
||||||
|
color: #505050;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
*:focus {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input:focus {
|
||||||
|
color: black;
|
||||||
|
border-color: black;
|
||||||
|
}
|
21
src/pages/AuthenticationPage/AuthenticationPage.test.js
Normal file
21
src/pages/AuthenticationPage/AuthenticationPage.test.js
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import AuthenticationPage from './AuthenticationPage';
|
||||||
|
import {shallow} from 'enzyme';
|
||||||
|
|
||||||
|
describe('<AuthenticationPage/>', function () {
|
||||||
|
it('renders without crashing ', function () {
|
||||||
|
const wrapper = shallow(<AuthenticationPage submit={() => {}}/>);
|
||||||
|
wrapper.unmount();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('test button click', function () {
|
||||||
|
let pass;
|
||||||
|
const func = jest.fn((pwd) => {pass = pwd});
|
||||||
|
const wrapper = shallow(<AuthenticationPage submit={func}/>);
|
||||||
|
wrapper.setState({pwdText: 'testpwd'});
|
||||||
|
wrapper.find('Button').simulate('click');
|
||||||
|
|
||||||
|
expect(func).toHaveBeenCalledTimes(1);
|
||||||
|
expect(pass).toBe('testpwd');
|
||||||
|
});
|
||||||
|
});
|
46
src/pages/AuthenticationPage/AuthenticationPage.tsx
Normal file
46
src/pages/AuthenticationPage/AuthenticationPage.tsx
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
import React from "react";
|
||||||
|
import {Button} from "../../elements/GPElements/Button";
|
||||||
|
import style from './AuthenticationPage.module.css'
|
||||||
|
|
||||||
|
interface state {
|
||||||
|
pwdText: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface props {
|
||||||
|
submit: (password: string) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
class AuthenticationPage extends React.Component<props, state> {
|
||||||
|
constructor(props: props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
pwdText: ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render(): JSX.Element {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className={style.openmediacenterlabel}>OpenMediaCenter</div>
|
||||||
|
<div className={style.main}>
|
||||||
|
<div className={style.loginText}>Login</div>
|
||||||
|
<div>
|
||||||
|
<input className={style.input}
|
||||||
|
placeholder='Password'
|
||||||
|
type='password'
|
||||||
|
onChange={(ch): void => this.setState({pwdText: ch.target.value})}
|
||||||
|
value={this.state.pwdText}/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Button title='Submit' onClick={(): void => {
|
||||||
|
this.props.submit(this.state.pwdText);
|
||||||
|
}}/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AuthenticationPage;
|
@ -37,6 +37,7 @@ global.prepareFailingFetchApi = () => {
|
|||||||
global.callAPIMock = (resonse) => {
|
global.callAPIMock = (resonse) => {
|
||||||
const helpers = require('./utils/Api');
|
const helpers = require('./utils/Api');
|
||||||
helpers.callAPI = jest.fn().mockImplementation((_, __, func1) => {func1(resonse);});
|
helpers.callAPI = jest.fn().mockImplementation((_, __, func1) => {func1(resonse);});
|
||||||
|
helpers.callApiUnsafe = jest.fn().mockImplementation((_, __, func1) => {func1(resonse);});
|
||||||
};
|
};
|
||||||
|
|
||||||
// code to run before each test
|
// code to run before each test
|
||||||
|
@ -33,7 +33,7 @@ export namespace SettingsTypes {
|
|||||||
export interface initialApiCallData {
|
export interface initialApiCallData {
|
||||||
DarkMode: boolean;
|
DarkMode: boolean;
|
||||||
Password: boolean;
|
Password: boolean;
|
||||||
Mediacenter_name: string;
|
MediacenterName: string;
|
||||||
VideoPath: string;
|
VideoPath: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -47,13 +47,14 @@ interface ApiBaseRequest {
|
|||||||
let apiToken = ''
|
let apiToken = ''
|
||||||
|
|
||||||
// a callback que to be called after api token refresh
|
// a callback que to be called after api token refresh
|
||||||
let callQue: (() => void)[] = []
|
let callQue: ((error: string) => void)[] = []
|
||||||
// flag to check wheter a api refresh is currently pending
|
// flag to check wheter a api refresh is currently pending
|
||||||
let refreshInProcess = false;
|
let refreshInProcess = false;
|
||||||
// store the expire seconds of token
|
// store the expire seconds of token
|
||||||
let expireSeconds = -1;
|
let expireSeconds = -1;
|
||||||
|
|
||||||
interface APIToken {
|
interface APIToken {
|
||||||
|
error?: string;
|
||||||
access_token: string;
|
access_token: string;
|
||||||
expires_in: number;
|
expires_in: number;
|
||||||
scope: string;
|
scope: string;
|
||||||
@ -63,8 +64,9 @@ interface APIToken {
|
|||||||
/**
|
/**
|
||||||
* refresh the api token or use that one in cookie if still valid
|
* refresh the api token or use that one in cookie if still valid
|
||||||
* @param callback to be called after successful refresh
|
* @param callback to be called after successful refresh
|
||||||
|
* @param password
|
||||||
*/
|
*/
|
||||||
export function refreshAPIToken(callback: () => void): void {
|
export function refreshAPIToken(callback: (error: string) => void, password?: string): void {
|
||||||
callQue.push(callback);
|
callQue.push(callback);
|
||||||
|
|
||||||
// check if already is a token refresh is in process
|
// check if already is a token refresh is in process
|
||||||
@ -76,30 +78,26 @@ export function refreshAPIToken(callback: () => void): void {
|
|||||||
refreshInProcess = true;
|
refreshInProcess = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// check if a cookie with token is available
|
if (apiTokenValid()) {
|
||||||
const token = getTokenCookie();
|
console.log("token still valid...")
|
||||||
if (token !== null) {
|
callFuncQue('');
|
||||||
// check if token is at least valid for the next minute
|
return;
|
||||||
if (token.expire > (new Date().getTime() / 1000) + 60) {
|
|
||||||
apiToken = token.token;
|
|
||||||
expireSeconds = token.expire;
|
|
||||||
callback();
|
|
||||||
console.log("token still valid...")
|
|
||||||
callFuncQue();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append("grant_type", "client_credentials");
|
formData.append("grant_type", "client_credentials");
|
||||||
formData.append("client_id", "openmediacenter");
|
formData.append("client_id", "openmediacenter");
|
||||||
formData.append("client_secret", 'openmediacenter');
|
formData.append("client_secret", password ? password : 'openmediacenter');
|
||||||
formData.append("scope", 'all');
|
formData.append("scope", 'all');
|
||||||
|
|
||||||
|
|
||||||
fetch(getBackendDomain() + '/token', {method: 'POST', body: formData})
|
fetch(getBackendDomain() + '/token', {method: 'POST', body: formData})
|
||||||
.then((response) => response.json()
|
.then((response) => response.json()
|
||||||
.then((result: APIToken) => {
|
.then((result: APIToken) => {
|
||||||
|
if (result.error) {
|
||||||
|
callFuncQue(result.error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
console.log(result)
|
console.log(result)
|
||||||
// set api token
|
// set api token
|
||||||
apiToken = result.access_token;
|
apiToken = result.access_token;
|
||||||
@ -107,17 +105,32 @@ export function refreshAPIToken(callback: () => void): void {
|
|||||||
expireSeconds = (new Date().getTime() / 1000) + result.expires_in;
|
expireSeconds = (new Date().getTime() / 1000) + result.expires_in;
|
||||||
setTokenCookie(apiToken, expireSeconds);
|
setTokenCookie(apiToken, expireSeconds);
|
||||||
// call all handlers and release flag
|
// call all handlers and release flag
|
||||||
callFuncQue();
|
callFuncQue('');
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function apiTokenValid(): boolean {
|
||||||
|
// check if a cookie with token is available
|
||||||
|
const token = getTokenCookie();
|
||||||
|
if (token !== null) {
|
||||||
|
// check if token is at least valid for the next minute
|
||||||
|
if (token.expire > (new Date().getTime() / 1000) + 60) {
|
||||||
|
apiToken = token.token;
|
||||||
|
expireSeconds = token.expire;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* call all qued callbacks
|
* call all qued callbacks
|
||||||
*/
|
*/
|
||||||
function callFuncQue(): void {
|
function callFuncQue(error: string): void {
|
||||||
// call all pending handlers
|
// call all pending handlers
|
||||||
callQue.map(func => {
|
callQue.map(func => {
|
||||||
return func();
|
return func(error);
|
||||||
})
|
})
|
||||||
// reset pending que
|
// reset pending que
|
||||||
callQue = []
|
callQue = []
|
||||||
@ -221,6 +234,25 @@ export function callAPI<T>(apinode: APINode,
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* make a public unsafe api call (without token) -- use as rare as possible only for initialization (eg. check if pwd is neccessary)
|
||||||
|
* @param apinode
|
||||||
|
* @param fd
|
||||||
|
* @param callback
|
||||||
|
*/
|
||||||
|
export function callApiUnsafe<T>(apinode: APINode, fd: ApiBaseRequest, callback: (_: T) => void, errorcallback?: (_: string) => void): void {
|
||||||
|
fetch(getAPIDomain() + apinode, {method: 'POST', body: JSON.stringify(fd),}).then((response) => {
|
||||||
|
if (response.status !== 200) {
|
||||||
|
console.log('Error: ' + response.statusText);
|
||||||
|
// todo place error popup here
|
||||||
|
} else {
|
||||||
|
response.json().then((result: T) => {
|
||||||
|
callback(result);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}).catch(reason => errorcallback ? errorcallback(reason) : {})
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A backend api call
|
* A backend api call
|
||||||
* @param apinode which api backend handler to call
|
* @param apinode which api backend handler to call
|
||||||
@ -249,5 +281,6 @@ export enum APINode {
|
|||||||
Settings = 'settings',
|
Settings = 'settings',
|
||||||
Tags = 'tags',
|
Tags = 'tags',
|
||||||
Actor = 'actor',
|
Actor = 'actor',
|
||||||
Video = 'video'
|
Video = 'video',
|
||||||
|
Init = 'init'
|
||||||
}
|
}
|
||||||
|
@ -23,6 +23,9 @@ class StaticInfos {
|
|||||||
*/
|
*/
|
||||||
enableDarkTheme(enable = true): void {
|
enableDarkTheme(enable = true): void {
|
||||||
this.darktheme = enable;
|
this.darktheme = enable;
|
||||||
|
this.handlers.map(func => {
|
||||||
|
return func();
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -33,6 +36,11 @@ class StaticInfos {
|
|||||||
return this.isDarkTheme() ? darktheme : lighttheme;
|
return this.isDarkTheme() ? darktheme : lighttheme;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handlers: (() => void)[] = [];
|
||||||
|
onThemeChange(func: () => void): void {
|
||||||
|
this.handlers.push(func);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* set the current videopath
|
* set the current videopath
|
||||||
* @param vidpath videopath with beginning and ending slash
|
* @param vidpath videopath with beginning and ending slash
|
||||||
|
Loading…
Reference in New Issue
Block a user