diff --git a/.eslintrc.js b/.eslintrc.js index a6e3655..49f4aa6 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -97,6 +97,7 @@ module.exports = { rules: { "@typescript-eslint/no-explicit-any": "error", "@typescript-eslint/explicit-function-return-type": "error", + "@typescript-eslint/no-shadow": "warn", // General 'comma-dangle': [1, 'never'], // allow or disallow trailing commas @@ -182,7 +183,7 @@ module.exports = { 'no-catch-shadow': 1, // disallow the catch clause parameter name being the same as a variable in the outer scope (off by default in the node environment) 'no-delete-var': 1, // disallow deletion of variables 'no-label-var': 1, // disallow labels that share a name with a variable - 'no-shadow': 1, // disallow declaration of variables already declared in the outer scope + // 'no-shadow': 1, // disallow declaration of variables already declared in the outer scope 'no-shadow-restricted-names': 1, // disallow shadowing of names such as arguments 'no-undef': 2, // disallow use of undeclared variables unless mentioned in a /*global */ block 'no-undefined': 0, // disallow use of undefined variable (off by default) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 282e3c4..d16b7e7 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -62,7 +62,7 @@ Backend_Tests: stage: test script: - cd apiGo - - go get -u github.com/jstemmer/go-junit-report + - go install github.com/jstemmer/go-junit-report@v0.9.1 - go test -v ./... 2>&1 | go-junit-report -set-exit-code > report.xml needs: [] artifacts: diff --git a/apiGo/api/API.go b/apiGo/api/API.go new file mode 100644 index 0000000..020a7f1 --- /dev/null +++ b/apiGo/api/API.go @@ -0,0 +1,9 @@ +package api + +func AddHandlers() { + addVideoHandlers() + addSettingsHandlers() + addTagHandlers() + addActorsHandlers() + addTvshowHandlers() +} diff --git a/apiGo/api/Actors.go b/apiGo/api/Actors.go index 1cc511f..20bbc8b 100644 --- a/apiGo/api/Actors.go +++ b/apiGo/api/Actors.go @@ -2,11 +2,12 @@ package api import ( "fmt" + "openmediacenter/apiGo/api/api" "openmediacenter/apiGo/api/types" "openmediacenter/apiGo/database" ) -func AddActorsHandlers() { +func addActorsHandlers() { saveActorsToDB() getActorsFromDB() } @@ -23,9 +24,9 @@ func getActorsFromDB() { * @apiSuccess {string} .Name Actor Name * @apiSuccess {string} .Thumbnail Portrait Thumbnail */ - AddHandler("getAllActors", ActorNode, func(info *HandlerInfo) []byte { + api.AddHandler("getAllActors", api.ActorNode, api.PermUser, func(context api.Context) { query := "SELECT actor_id, name, thumbnail FROM actors" - return jsonify(readActorsFromResultset(database.Query(query))) + context.Json(readActorsFromResultset(database.Query(query))) }) /** @@ -41,20 +42,21 @@ func getActorsFromDB() { * @apiSuccess {string} .Name Actor Name * @apiSuccess {string} .Thumbnail Portrait Thumbnail */ - AddHandler("getActorsOfVideo", ActorNode, func(info *HandlerInfo) []byte { + api.AddHandler("getActorsOfVideo", api.ActorNode, api.PermUser, func(context api.Context) { var args struct { MovieId int } - if err := FillStruct(&args, info.Data); err != nil { - fmt.Println(err.Error()) - return nil + err := api.DecodeRequest(context.GetRequest(), &args) + if err != nil { + context.Text("failed to decode request") + return } query := fmt.Sprintf(`SELECT a.actor_id, name, thumbnail FROM actors_videos JOIN actors a on actors_videos.actor_id = a.actor_id WHERE actors_videos.video_id=%d`, args.MovieId) - return jsonify(readActorsFromResultset(database.Query(query))) + context.Json(readActorsFromResultset(database.Query(query))) }) /** @@ -74,13 +76,15 @@ func getActorsFromDB() { * @apiSuccess {string} Info.Name Actor Name * @apiSuccess {string} Info.Thumbnail Actor Thumbnail */ - AddHandler("getActorInfo", ActorNode, func(info *HandlerInfo) []byte { + api.AddHandler("getActorInfo", api.ActorNode, api.PermUser, func(context api.Context) { var args struct { ActorId int } - if err := FillStruct(&args, info.Data); err != nil { - fmt.Println(err.Error()) - return nil + + err := api.DecodeRequest(context.GetRequest(), &args) + if err != nil { + context.Error("unable to decode request") + return } query := fmt.Sprintf(`SELECT movie_id, movie_name FROM actors_videos @@ -99,7 +103,7 @@ func getActorsFromDB() { Info: actor, } - return jsonify(result) + context.Json(result) }) } @@ -112,19 +116,17 @@ func saveActorsToDB() { * * @apiParam {string} ActorName Name of new Actor * - * @apiSuccess {string} result 'success' if successfully or error message if not + * @apiSuccess {string} result 'success' if successfully or Error message if not */ - AddHandler("createActor", ActorNode, func(info *HandlerInfo) []byte { + api.AddHandler("createActor", api.ActorNode, api.PermUser, func(context api.Context) { var args struct { ActorName string } - if err := FillStruct(&args, info.Data); err != nil { - fmt.Println(err.Error()) - return nil - } + api.DecodeRequest(context.GetRequest(), &args) query := "INSERT IGNORE INTO actors (name) VALUES (?)" - return database.SuccessQuery(query, args.ActorName) + // todo bit ugly + context.Text(string(database.SuccessQuery(query, args.ActorName))) }) /** @@ -136,19 +138,20 @@ func saveActorsToDB() { * @apiParam {int} ActorId Id of Actor * @apiParam {int} MovieId Id of Movie to add to * - * @apiSuccess {string} result 'success' if successfully or error message if not + * @apiSuccess {string} result 'success' if successfully or Error message if not */ - AddHandler("addActorToVideo", ActorNode, func(info *HandlerInfo) []byte { + api.AddHandler("addActorToVideo", api.ActorNode, api.PermUser, func(context api.Context) { var args struct { ActorId int MovieId int } - if err := FillStruct(&args, info.Data); err != nil { - fmt.Println(err.Error()) - return nil + err := api.DecodeRequest(context.GetRequest(), &args) + if err != nil { + context.Error("unable to decode request") + return } query := fmt.Sprintf("INSERT IGNORE INTO actors_videos (actor_id, video_id) VALUES (%d,%d)", args.ActorId, args.MovieId) - return database.SuccessQuery(query) + context.Text(string(database.SuccessQuery(query))) }) } diff --git a/apiGo/api/ApiBase.go b/apiGo/api/ApiBase.go deleted file mode 100644 index 5120559..0000000 --- a/apiGo/api/ApiBase.go +++ /dev/null @@ -1,113 +0,0 @@ -package api - -import ( - "bytes" - "encoding/json" - "fmt" - "gopkg.in/oauth2.v3" - "net/http" - "openmediacenter/apiGo/api/oauth" -) - -const APIPREFIX = "/api" - -const ( - VideoNode = iota - TagNode = iota - SettingsNode = iota - ActorNode = iota - TVShowNode = iota -) - -type HandlerInfo struct { - ID string - Token string - Data map[string]interface{} -} - -type actionStruct struct { - Action string -} - -type Handler struct { - action string - handler func(info *HandlerInfo) []byte - apiNode int -} - -var handlers = make(map[string]Handler) - -func AddHandler(action string, apiNode int, h func(info *HandlerInfo) []byte) { - // append new handler to the handlers - handlers[fmt.Sprintf("%s/%d", action, apiNode)] = Handler{action, h, apiNode} -} - -func ServerInit() { - http.Handle(APIPREFIX+"/video", oauth.ValidateToken(handlefunc, VideoNode)) - http.Handle(APIPREFIX+"/tags", oauth.ValidateToken(handlefunc, TagNode)) - http.Handle(APIPREFIX+"/settings", oauth.ValidateToken(handlefunc, SettingsNode)) - http.Handle(APIPREFIX+"/actor", oauth.ValidateToken(handlefunc, ActorNode)) - http.Handle(APIPREFIX+"/tvshow", oauth.ValidateToken(handlefunc, TVShowNode)) - - // initialize oauth service and add corresponding auth routes - oauth.InitOAuth() -} - -func handleAPICall(action string, requestBody string, apiNode int, info *HandlerInfo) []byte { - handler, ok := handlers[fmt.Sprintf("%s/%d", action, apiNode)] - if !ok { - // handler doesn't exist! - fmt.Printf("no handler found for Action: %d/%s\n", apiNode, action) - return nil - } - - // check if info even exists - if info == nil { - info = &HandlerInfo{} - } - - // parse the arguments - var args map[string]interface{} - err := json.Unmarshal([]byte(requestBody), &args) - - if err != nil { - fmt.Printf("failed to decode arguments of action %s :: %s\n", action, requestBody) - } else { - // check if map has an action - if _, ok := args["action"]; ok { - delete(args, "action") - } - - info.Data = args - } - - // call the handler - return handler.handler(info) -} - -func handlefunc(rw http.ResponseWriter, req *http.Request, node int, tokenInfo *oauth2.TokenInfo) { - // only allow post requests - if req.Method != "POST" { - return - } - - buf := new(bytes.Buffer) - buf.ReadFrom(req.Body) - body := buf.String() - - var t actionStruct - err := json.Unmarshal([]byte(body), &t) - if err != nil { - fmt.Println("failed to read action from request! :: " + body) - } - - // load userid from received token object - id := (*tokenInfo).GetClientID() - - userinfo := &HandlerInfo{ - ID: id, - Token: (*tokenInfo).GetCode(), - } - - rw.Write(handleAPICall(t.Action, body, node, userinfo)) -} diff --git a/apiGo/api/ApiBase_test.go b/apiGo/api/ApiBase_test.go deleted file mode 100644 index d692f01..0000000 --- a/apiGo/api/ApiBase_test.go +++ /dev/null @@ -1,72 +0,0 @@ -package api - -import ( - "testing" -) - -func cleanUp() { - handlers = make(map[string]Handler) -} - -func TestAddHandler(t *testing.T) { - cleanUp() - - AddHandler("test", ActorNode, func(info *HandlerInfo) []byte { - return nil - }) - if len(handlers) != 1 { - t.Errorf("Handler insertion failed, got: %d handlers, want: %d.", len(handlers), 1) - } -} - -func TestCallOfHandler(t *testing.T) { - cleanUp() - - i := 0 - AddHandler("test", ActorNode, func(info *HandlerInfo) []byte { - i++ - return nil - }) - - // simulate the call of the api - handleAPICall("test", "", ActorNode, nil) - - if i != 1 { - t.Errorf("Unexpected number of Lambda calls : %d/1", i) - } -} - -func TestDecodingOfArguments(t *testing.T) { - cleanUp() - - AddHandler("test", ActorNode, func(info *HandlerInfo) []byte { - var args struct { - Test string - TestInt int - } - err := FillStruct(&args, info.Data) - if err != nil { - t.Errorf("Error parsing args: %s", err.Error()) - return nil - } - - if args.TestInt != 42 || args.Test != "myString" { - t.Errorf("Wrong parsing of argument parameters : %d/42 - %s/myString", args.TestInt, args.Test) - } - - return nil - }) - - // simulate the call of the api - handleAPICall("test", `{"Test":"myString","TestInt":42}`, ActorNode, nil) -} - -func TestNoHandlerCovers(t *testing.T) { - cleanUp() - - ret := handleAPICall("test", "", ActorNode, nil) - - if ret != nil { - t.Error("Expect nil return within unhandled api action") - } -} diff --git a/apiGo/api/Helpers.go b/apiGo/api/Helpers.go index 1696c57..282710c 100644 --- a/apiGo/api/Helpers.go +++ b/apiGo/api/Helpers.go @@ -2,10 +2,8 @@ package api import ( "database/sql" - "encoding/json" "fmt" "openmediacenter/apiGo/api/types" - "reflect" ) // MovieId - MovieName : pay attention to the order! @@ -33,7 +31,7 @@ func readTagsFromResultset(rows *sql.Rows) []types.Tag { var tag types.Tag err := rows.Scan(&tag.TagId, &tag.TagName) if err != nil { - panic(err.Error()) // proper error handling instead of panic in your app + panic(err.Error()) // proper Error handling instead of panic in your app } result = append(result, tag) } @@ -54,7 +52,7 @@ func readActorsFromResultset(rows *sql.Rows) []types.Actor { actor.Thumbnail = string(thumbnail) } if err != nil { - panic(err.Error()) // proper error handling instead of panic in your app + panic(err.Error()) // proper Error handling instead of panic in your app } result = append(result, actor) } @@ -70,7 +68,7 @@ func readTVshowsFromResultset(rows *sql.Rows) []types.TVShow { var vid types.TVShow err := rows.Scan(&vid.Id, &vid.Name) if err != nil { - panic(err.Error()) // proper error handling instead of panic in your app + panic(err.Error()) // proper Error handling instead of panic in your app } result = append(result, vid) } @@ -78,54 +76,3 @@ func readTVshowsFromResultset(rows *sql.Rows) []types.TVShow { return result } - -func jsonify(v interface{}) []byte { - // jsonify results - str, err := json.Marshal(v) - if err != nil { - fmt.Println("Error while Jsonifying return object: " + err.Error()) - } - return str -} - -// setField set a specific field of an object with an object provided -func setField(obj interface{}, name string, value interface{}) error { - structValue := reflect.ValueOf(obj).Elem() - structFieldValue := structValue.FieldByName(name) - - if !structFieldValue.IsValid() { - return fmt.Errorf("no such field: %s in obj", name) - } - - if !structFieldValue.CanSet() { - return fmt.Errorf("cannot set %s field value", name) - } - - structFieldType := structFieldValue.Type() - val := reflect.ValueOf(value) - - if structFieldType != val.Type() { - if val.Type().ConvertibleTo(structFieldType) { - // if type is convertible - convert and set - structFieldValue.Set(val.Convert(structFieldType)) - } else { - return fmt.Errorf("provided value %s type didn't match obj field type and isn't convertible", name) - } - } else { - // set value if type is the same - structFieldValue.Set(val) - } - - return nil -} - -// FillStruct fill a custom struct with objects of a map -func FillStruct(i interface{}, m map[string]interface{}) error { - for k, v := range m { - err := setField(i, k, v) - if err != nil { - return err - } - } - return nil -} diff --git a/apiGo/api/Settings.go b/apiGo/api/Settings.go index c5dedde..fb1f1ce 100644 --- a/apiGo/api/Settings.go +++ b/apiGo/api/Settings.go @@ -1,8 +1,7 @@ package api import ( - "encoding/json" - "fmt" + "openmediacenter/apiGo/api/api" "openmediacenter/apiGo/api/types" "openmediacenter/apiGo/config" "openmediacenter/apiGo/database" @@ -12,7 +11,7 @@ import ( "strings" ) -func AddSettingsHandlers() { +func addSettingsHandlers() { saveSettingsToDB() getSettingsFromDB() reIndexHandling() @@ -38,7 +37,7 @@ func getSettingsFromDB() { * @apiSuccess {uint32} Sizes.DifferentTags number of different tags available * @apiSuccess {uint32} Sizes.TagsAdded number of different tags added to videos */ - AddHandler("loadGeneralSettings", SettingsNode, func(info *HandlerInfo) []byte { + api.AddHandler("loadGeneralSettings", api.SettingsNode, api.PermUser, func(context api.Context) { result, _, sizes := database.GetSettings() var ret = struct { @@ -48,7 +47,7 @@ func getSettingsFromDB() { Settings: &result, Sizes: &sizes, } - return jsonify(ret) + context.Json(ret) }) /** @@ -64,7 +63,7 @@ func getSettingsFromDB() { * @apiSuccess {bool} DarkMode Darkmode enabled? * @apiSuccess {bool} TVShowEnabled is are TVShows enabled */ - AddHandler("loadInitialData", SettingsNode, func(info *HandlerInfo) []byte { + api.AddHandler("loadInitialData", api.SettingsNode, api.PermUser, func(context api.Context) { sett := settings.LoadSettings() type InitialDataTypeResponse struct { @@ -93,8 +92,7 @@ func getSettingsFromDB() { FullDeleteEnabled: config.GetConfig().Features.FullyDeletableVideos, } - str, _ := json.Marshal(res) - return str + context.Json(res) }) } @@ -112,13 +110,14 @@ func saveSettingsToDB() { * @apiParam {bool} TMDBGrabbing TMDB grabbing support to grab tag info and thumbnails * @apiParam {bool} DarkMode Darkmode enabled? * - * @apiSuccess {string} result 'success' if successfully or error message if not + * @apiSuccess {string} result 'success' if successfully or Error message if not */ - AddHandler("saveGeneralSettings", SettingsNode, func(info *HandlerInfo) []byte { + api.AddHandler("saveGeneralSettings", api.SettingsNode, api.PermUser, func(context api.Context) { var args types.SettingsType - if err := FillStruct(&args, info.Data); err != nil { - fmt.Println(err.Error()) - return nil + err := api.DecodeRequest(context.GetRequest(), &args) + if err != nil { + context.Error("unable to decode arguments") + return } query := ` @@ -130,9 +129,10 @@ func saveSettingsToDB() { TMDB_grabbing=?, DarkMode=? WHERE 1` - return database.SuccessQuery(query, + // todo avoid conversion + context.Text(string(database.SuccessQuery(query, args.VideoPath, args.EpisodePath, args.Password, - args.MediacenterName, args.TMDBGrabbing, args.DarkMode) + args.MediacenterName, args.TMDBGrabbing, args.DarkMode))) }) } @@ -144,11 +144,11 @@ func reIndexHandling() { * @apiName startReindex * @apiGroup Settings * - * @apiSuccess {string} result 'success' if successfully or error message if not + * @apiSuccess {string} result 'success' if successfully or Error message if not */ - AddHandler("startReindex", SettingsNode, func(info *HandlerInfo) []byte { + api.AddHandler("startReindex", api.SettingsNode, api.PermUser, func(context api.Context) { videoparser.StartReindex() - return database.ManualSuccessResponse(nil) + context.Text(string(database.ManualSuccessResponse(nil))) }) /** @@ -157,11 +157,11 @@ func reIndexHandling() { * @apiName startTVShowReindex * @apiGroup Settings * - * @apiSuccess {string} result 'success' if successfully or error message if not + * @apiSuccess {string} result 'success' if successfully or Error message if not */ - AddHandler("startTVShowReindex", SettingsNode, func(info *HandlerInfo) []byte { + api.AddHandler("startTVShowReindex", api.SettingsNode, api.PermUser, func(context api.Context) { videoparser.StartTVShowReindex() - return database.ManualSuccessResponse(nil) + context.Text(string(database.ManualSuccessResponse(nil))) }) /** @@ -170,8 +170,7 @@ func reIndexHandling() { * @apiName cleanupGravity * @apiGroup Settings */ - AddHandler("cleanupGravity", SettingsNode, func(info *HandlerInfo) []byte { + api.AddHandler("cleanupGravity", api.SettingsNode, api.PermUser, func(context api.Context) { videoparser.StartCleanup() - return nil }) } diff --git a/apiGo/api/TVShows.go b/apiGo/api/TVShows.go index 7004c61..dcf0856 100644 --- a/apiGo/api/TVShows.go +++ b/apiGo/api/TVShows.go @@ -2,11 +2,12 @@ package api import ( "fmt" + "openmediacenter/apiGo/api/api" "openmediacenter/apiGo/config" "openmediacenter/apiGo/database" ) -func AddTvshowHandlers() { +func addTvshowHandlers() { // do not add handlers if tvshows not enabled if config.GetConfig().Features.DisableTVSupport { return @@ -22,10 +23,10 @@ func AddTvshowHandlers() { * @apiSuccess {uint32} .Id tvshow id * @apiSuccess {string} .Name tvshow name */ - AddHandler("getTVShows", TVShowNode, func(info *HandlerInfo) []byte { + api.AddHandler("getTVShows", api.TVShowNode, api.PermUser, func(context api.Context) { query := "SELECT id, name FROM tvshow" rows := database.Query(query) - return jsonify(readTVshowsFromResultset(rows)) + context.Json(readTVshowsFromResultset(rows)) }) /** @@ -42,13 +43,14 @@ func AddTvshowHandlers() { * @apiSuccess {uint8} .Season Season number * @apiSuccess {uint8} .Episode Episode number */ - AddHandler("getEpisodes", TVShowNode, func(info *HandlerInfo) []byte { + api.AddHandler("getEpisodes", api.TVShowNode, api.PermUser, func(context api.Context) { var args struct { ShowID uint32 } - if err := FillStruct(&args, info.Data); err != nil { - fmt.Println(err.Error()) - return nil + err := api.DecodeRequest(context.GetRequest(), &args) + if err != nil { + context.Text("unable to decode request") + return } query := fmt.Sprintf("SELECT id, name, season, episode FROM tvshow_episodes WHERE tvshow_id=%d", args.ShowID) @@ -73,7 +75,7 @@ func AddTvshowHandlers() { episodes = append(episodes, ep) } - return jsonify(episodes) + context.Json(episodes) }) /** @@ -90,13 +92,14 @@ func AddTvshowHandlers() { * @apiSuccess {uint8} Episode Episode number * @apiSuccess {string} Path webserver path of video file */ - AddHandler("loadEpisode", TVShowNode, func(info *HandlerInfo) []byte { + api.AddHandler("loadEpisode", api.TVShowNode, api.PermUser, func(context api.Context) { var args struct { ID uint32 } - if err := FillStruct(&args, info.Data); err != nil { - fmt.Println(err.Error()) - return nil + err := api.DecodeRequest(context.GetRequest(), &args) + if err != nil { + context.Text("unable to decode argument") + return } query := fmt.Sprintf(` @@ -116,15 +119,16 @@ WHERE tvshow_episodes.id=%d`, args.ID) var filename string var foldername string - err := row.Scan(&ret.Name, &ret.Season, &ret.TVShowID, &ret.Episode, &filename, &foldername) + err = row.Scan(&ret.Name, &ret.Season, &ret.TVShowID, &ret.Episode, &filename, &foldername) if err != nil { fmt.Println(err.Error()) - return nil + context.Error(err.Error()) + return } ret.Path = foldername + "/" + filename - return jsonify(ret) + context.Json(ret) }) /** @@ -137,25 +141,26 @@ WHERE tvshow_episodes.id=%d`, args.ID) * * @apiSuccess {string} . Base64 encoded Thubnail */ - AddHandler("readThumbnail", TVShowNode, func(info *HandlerInfo) []byte { + api.AddHandler("readThumbnail", api.TVShowNode, api.PermUser, func(context api.Context) { var args struct { Id int } - if err := FillStruct(&args, info.Data); err != nil { - fmt.Println(err.Error()) - return nil + err := api.DecodeRequest(context.GetRequest(), &args) + if err != nil { + context.Text("unable to decode request") + return } var pic []byte query := fmt.Sprintf("SELECT thumbnail FROM tvshow WHERE id=%d", args.Id) - err := database.QueryRow(query).Scan(&pic) + err = database.QueryRow(query).Scan(&pic) if err != nil { fmt.Printf("the thumbnail of movie id %d couldn't be found", args.Id) - return nil + return } - return pic + context.Text(string(pic)) }) } diff --git a/apiGo/api/Tags.go b/apiGo/api/Tags.go index 13a6a54..5c3bfb1 100644 --- a/apiGo/api/Tags.go +++ b/apiGo/api/Tags.go @@ -2,11 +2,12 @@ package api import ( "fmt" + "openmediacenter/apiGo/api/api" "openmediacenter/apiGo/database" "regexp" ) -func AddTagHandlers() { +func addTagHandlers() { getFromDB() addToDB() deleteFromDB() @@ -22,16 +23,17 @@ func deleteFromDB() { * @apiParam {bool} [Force] force delete tag with its constraints * @apiParam {int} TagId id of tag to delete * - * @apiSuccess {string} result 'success' if successfully or error message if not + * @apiSuccess {string} result 'success' if successfully or Error message if not */ - AddHandler("deleteTag", TagNode, func(info *HandlerInfo) []byte { + api.AddHandler("deleteTag", api.TagNode, api.PermUser, func(context api.Context) { var args struct { TagId int Force bool } - if err := FillStruct(&args, info.Data); err != nil { - fmt.Println(err.Error()) - return nil + err := api.DecodeRequest(context.GetRequest(), &args) + if err != nil { + context.Text("unable to decode request") + return } // delete key constraints first @@ -41,23 +43,24 @@ func deleteFromDB() { // respond only if result not successful if err != nil { - return database.ManualSuccessResponse(err) + context.Text(string(database.ManualSuccessResponse(err))) + return } } query := fmt.Sprintf("DELETE FROM tags WHERE tag_id=%d", args.TagId) - err := database.Edit(query) + err = database.Edit(query) if err == nil { // return if successful - return database.ManualSuccessResponse(err) + context.Text(string(database.ManualSuccessResponse(err))) } else { - // check with regex if its the key constraint error + // check with regex if its the key constraint Error r := regexp.MustCompile("^.*a foreign key constraint fails.*$") if r.MatchString(err.Error()) { - return database.ManualSuccessResponse(fmt.Errorf("not empty tag")) + context.Text(string(database.ManualSuccessResponse(fmt.Errorf("not empty tag")))) } else { - return database.ManualSuccessResponse(err) + context.Text(string(database.ManualSuccessResponse(err))) } } }) @@ -74,9 +77,9 @@ func getFromDB() { * @apiSuccess {uint32} TagId * @apiSuccess {string} TagName name of the Tag */ - AddHandler("getAllTags", TagNode, func(info *HandlerInfo) []byte { + api.AddHandler("getAllTags", api.TagNode, api.PermUser, func(context api.Context) { query := "SELECT tag_id,tag_name from tags" - return jsonify(readTagsFromResultset(database.Query(query))) + context.Json(readTagsFromResultset(database.Query(query))) }) } @@ -89,19 +92,20 @@ func addToDB() { * * @apiParam {string} TagName name of the tag * - * @apiSuccess {string} result 'success' if successfully or error message if not + * @apiSuccess {string} result 'success' if successfully or Error message if not */ - AddHandler("createTag", TagNode, func(info *HandlerInfo) []byte { + api.AddHandler("createTag", api.TagNode, api.PermUser, func(context api.Context) { var args struct { TagName string } - if err := FillStruct(&args, info.Data); err != nil { - fmt.Println(err.Error()) - return nil + err := api.DecodeRequest(context.GetRequest(), &args) + if err != nil { + context.Text("unable to decode request") + return } query := "INSERT IGNORE INTO tags (tag_name) VALUES (?)" - return database.SuccessQuery(query, args.TagName) + context.Text(string(database.SuccessQuery(query, args.TagName))) }) /** @@ -113,19 +117,20 @@ func addToDB() { * @apiParam {int} TagId Tag id to add to video * @apiParam {int} MovieId Video Id of video to add tag to * - * @apiSuccess {string} result 'success' if successfully or error message if not + * @apiSuccess {string} result 'success' if successfully or Error message if not */ - AddHandler("addTag", TagNode, func(info *HandlerInfo) []byte { + api.AddHandler("addTag", api.TagNode, api.PermUser, func(context api.Context) { var args struct { MovieId int TagId int } - if err := FillStruct(&args, info.Data); err != nil { - fmt.Println(err.Error()) - return nil + err := api.DecodeRequest(context.GetRequest(), &args) + if err != nil { + context.Text("unable to decode request") + return } query := "INSERT IGNORE INTO video_tags(tag_id, video_id) VALUES (?,?)" - return database.SuccessQuery(query, args.TagId, args.MovieId) + context.Text(string(database.SuccessQuery(query, args.TagId, args.MovieId))) }) } diff --git a/apiGo/api/Video.go b/apiGo/api/Video.go index 4af0ba5..09886cb 100644 --- a/apiGo/api/Video.go +++ b/apiGo/api/Video.go @@ -1,9 +1,9 @@ package api import ( - "encoding/json" "fmt" "net/url" + "openmediacenter/apiGo/api/api" "openmediacenter/apiGo/api/types" "openmediacenter/apiGo/config" "openmediacenter/apiGo/database" @@ -11,7 +11,7 @@ import ( "strconv" ) -func AddVideoHandlers() { +func addVideoHandlers() { getVideoHandlers() loadVideosHandlers() addToVideoHandlers() @@ -31,14 +31,15 @@ func getVideoHandlers() { * @apiSuccess {String} Videos.MovieName Name of video * @apiSuccess {String} TagName Name of the Tag returned */ - AddHandler("getMovies", VideoNode, func(info *HandlerInfo) []byte { + api.AddHandler("getMovies", api.VideoNode, api.PermUser, func(context api.Context) { var args struct { Tag uint32 Sort uint8 } - if err := FillStruct(&args, info.Data); err != nil { - fmt.Println(err.Error()) - return nil + err := api.DecodeRequest(context.GetRequest(), &args) + if err != nil { + context.Text("unable to decode request") + return } const ( @@ -92,24 +93,22 @@ func getVideoHandlers() { var vid types.VideoUnloadedType err := rows.Scan(&vid.MovieId, &vid.MovieName, &name) if err != nil { - return nil + return } vids = append(vids, vid) } if rows.Close() != nil { - return nil + return } // if the tag id doesn't exist the query won't return a name if name == "" { - return nil + return } result.Videos = vids result.TagName = name - // jsonify results - str, _ := json.Marshal(result) - return str + context.Json(result) }) /** @@ -122,26 +121,27 @@ func getVideoHandlers() { * * @apiSuccess {string} . Base64 encoded Thubnail */ - AddHandler("readThumbnail", VideoNode, func(info *HandlerInfo) []byte { + api.AddHandler("readThumbnail", api.VideoNode, api.PermUser, func(context api.Context) { var args struct { Movieid int } - if err := FillStruct(&args, info.Data); err != nil { - fmt.Println(err.Error()) - return nil + err := api.DecodeRequest(context.GetRequest(), &args) + if err != nil { + context.Text("unable to decode request") + return } var pic []byte query := fmt.Sprintf("SELECT thumbnail FROM videos WHERE movie_id=%d", args.Movieid) - err := database.QueryRow(query).Scan(&pic) + err = database.QueryRow(query).Scan(&pic) if err != nil { fmt.Printf("the thumbnail of movie id %d couldn't be found", args.Movieid) - return nil + return } - return pic + context.Text(string(pic)) }) /** @@ -160,13 +160,13 @@ func getVideoHandlers() { * @apiSuccess {string} Videos.MovieName Video Name * @apiSuccess {int} Videos.MovieId Video ID */ - AddHandler("getRandomMovies", VideoNode, func(info *HandlerInfo) []byte { + api.AddHandler("getRandomMovies", api.VideoNode, api.PermUser, func(context api.Context) { var args struct { Number int } - if err := FillStruct(&args, info.Data); err != nil { - fmt.Println(err.Error()) - return nil + if api.DecodeRequest(context.GetRequest(), &args) != nil { + context.Text("unable to decode request") + return } var result struct { @@ -198,15 +198,13 @@ func getVideoHandlers() { var tag types.Tag err := rows.Scan(&tag.TagName, &tag.TagId) if err != nil { - panic(err.Error()) // proper error handling instead of panic in your app + panic(err.Error()) // proper Error handling instead of panic in your app } // append to final array result.Tags = append(result.Tags, tag) } - // jsonify results - str, _ := json.Marshal(result) - return str + context.Json(result) }) /** @@ -221,23 +219,19 @@ func getVideoHandlers() { * @apiSuccess {number} .MovieId Id of Video * @apiSuccess {String} .MovieName Name of video */ - AddHandler("getSearchKeyWord", VideoNode, func(info *HandlerInfo) []byte { + api.AddHandler("getSearchKeyWord", api.VideoNode, api.PermUser, func(context api.Context) { var args struct { KeyWord string } - if err := FillStruct(&args, info.Data); err != nil { - fmt.Println(err.Error()) - return nil + if api.DecodeRequest(context.GetRequest(), &args) != nil { + context.Text("unable to decode request") + return } query := fmt.Sprintf(`SELECT movie_id,movie_name FROM videos WHERE movie_name LIKE '%%%s%%' ORDER BY likes DESC, create_date DESC, movie_name`, args.KeyWord) - - result := readVideosFromResultset(database.Query(query)) - // jsonify results - str, _ := json.Marshal(result) - return str + context.Json(readVideosFromResultset(database.Query(query))) }) } @@ -273,13 +267,13 @@ func loadVideosHandlers() { * @apiSuccess {string} Actors.Name Actor Name * @apiSuccess {string} Actors.Thumbnail Portrait Thumbnail */ - AddHandler("loadVideo", VideoNode, func(info *HandlerInfo) []byte { + api.AddHandler("loadVideo", api.VideoNode, api.PermUser, func(context api.Context) { var args struct { MovieId int } - if err := FillStruct(&args, info.Data); err != nil { - fmt.Println(err.Error()) - return nil + if api.DecodeRequest(context.GetRequest(), &args) != nil { + context.Text("unable to decode request") + return } query := fmt.Sprintf(`SELECT movie_name,movie_url,movie_id,thumbnail,poster,likes,quality,length @@ -291,9 +285,9 @@ func loadVideosHandlers() { err := database.QueryRow(query).Scan(&res.MovieName, &res.MovieUrl, &res.MovieId, &thumbnail, &poster, &res.Likes, &res.Quality, &res.Length) if err != nil { - fmt.Printf("error getting full data list of videoid - %d", args.MovieId) + fmt.Printf("Error getting full data list of videoid - %d", args.MovieId) fmt.Println(err.Error()) - return nil + return } // we ned to urlencode the movieurl @@ -331,9 +325,7 @@ func loadVideosHandlers() { res.Actors = readActorsFromResultset(database.Query(query)) - // jsonify results - str, _ := json.Marshal(res) - return str + context.Json(res) }) /** @@ -349,7 +341,7 @@ func loadVideosHandlers() { * @apiSuccess {uint32} DifferentTags number of different Tags available * @apiSuccess {uint32} Tagged number of different Tags assigned */ - AddHandler("getStartData", VideoNode, func(info *HandlerInfo) []byte { + api.AddHandler("getStartData", api.VideoNode, api.PermUser, func(context api.Context) { var result types.StartData // query settings and infotile values query := ` @@ -383,9 +375,7 @@ func loadVideosHandlers() { _ = database.QueryRow(query).Scan(&result.VideoNr, &result.Tagged, &result.HDNr, &result.FullHdNr, &result.SDNr, &result.DifferentTags) - // jsonify results - str, _ := json.Marshal(result) - return str + context.Json(result) }) } @@ -398,19 +388,19 @@ func addToVideoHandlers() { * * @apiParam {int} MovieId ID of video * - * @apiSuccess {string} result 'success' if successfully or error message if not + * @apiSuccess {string} result 'success' if successfully or Error message if not */ - AddHandler("addLike", VideoNode, func(info *HandlerInfo) []byte { + api.AddHandler("addLike", api.VideoNode, api.PermUser, func(context api.Context) { var args struct { MovieId int } - if err := FillStruct(&args, info.Data); err != nil { - fmt.Println(err.Error()) - return nil + if api.DecodeRequest(context.GetRequest(), &args) != nil { + context.Text("unable to decode request") + return } query := fmt.Sprintf("update videos set likes = likes + 1 where movie_id = %d", args.MovieId) - return database.SuccessQuery(query) + context.Text(string(database.SuccessQuery(query))) }) /** @@ -422,16 +412,16 @@ func addToVideoHandlers() { * @apiParam {int} MovieId ID of video * @apiParam {bool} FullyDelete Delete video from disk? * - * @apiSuccess {string} result 'success' if successfully or error message if not + * @apiSuccess {string} result 'success' if successfully or Error message if not */ - AddHandler("deleteVideo", VideoNode, func(info *HandlerInfo) []byte { + api.AddHandler("deleteVideo", api.VideoNode, api.PermUser, func(context api.Context) { var args struct { MovieId int FullyDelete bool } - if err := FillStruct(&args, info.Data); err != nil { - fmt.Println(err.Error()) - return nil + if api.DecodeRequest(context.GetRequest(), &args) != nil { + context.Text("unable to decode request") + return } // delete tag constraints @@ -444,7 +434,7 @@ func addToVideoHandlers() { // respond only if result not successful if err != nil { - return database.ManualSuccessResponse(err) + context.Text(string(database.ManualSuccessResponse(err))) } // only allow deletion of video if cli flag is set, independent of passed api arg @@ -454,7 +444,7 @@ func addToVideoHandlers() { var vidpath string err := database.QueryRow(query).Scan(&vidpath) if err != nil { - return database.ManualSuccessResponse(err) + context.Text(string(database.ManualSuccessResponse(err))) } sett, videoprefix, _ := database.GetSettings() @@ -463,12 +453,12 @@ func addToVideoHandlers() { err = os.Remove(assembledPath) if err != nil { fmt.Printf("unable to delete file: %s -- %s\n", assembledPath, err.Error()) - return database.ManualSuccessResponse(err) + context.Text(string(database.ManualSuccessResponse(err))) } } // delete video row from db query = fmt.Sprintf("DELETE FROM videos WHERE movie_id=%d", args.MovieId) - return database.SuccessQuery(query) + context.Text(string(database.SuccessQuery(query))) }) } diff --git a/apiGo/api/api/ApiBase.go b/apiGo/api/api/ApiBase.go new file mode 100644 index 0000000..375a4f0 --- /dev/null +++ b/apiGo/api/api/ApiBase.go @@ -0,0 +1,64 @@ +package api + +import ( + "fmt" + "net/http" + "openmediacenter/apiGo/database/settings" +) + +const ( + VideoNode = "video" + TagNode = "tags" + SettingsNode = "settings" + ActorNode = "actor" + TVShowNode = "tv" + LoginNode = "login" +) + +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 + 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(port uint16) error { + // initialize auth service and add corresponding auth routes + InitOAuth() + + fmt.Printf("OpenMediacenter server up and running on port %d\n", port) + return http.ListenAndServe(fmt.Sprintf(":%d", port), nil) +} diff --git a/apiGo/api/api/Auth.go b/apiGo/api/api/Auth.go new file mode 100644 index 0000000..9e50ddd --- /dev/null +++ b/apiGo/api/api/Auth.go @@ -0,0 +1,111 @@ +package api + +import ( + "fmt" + "github.com/dgrijalva/jwt-go" + "gopkg.in/oauth2.v3" + "gopkg.in/oauth2.v3/server" + "net/http" + "openmediacenter/apiGo/database" + "strconv" + "time" +) + +var srv *server.Server + +const ( + PermAdmin uint8 = iota + PermUser uint8 = iota + PermUnauthorized uint8 = iota +) + +const SignKey = "89013f1753a6890c6090b09e3c23ff43" +const TokenExpireHours = 24 + +type Token struct { + Token string + ExpiresAt int64 +} + +func TokenValid(token string) (int, uint8) { + t, err := jwt.ParseWithClaims(token, &jwt.StandardClaims{}, func(token *jwt.Token) (interface{}, error) { + return []byte(SignKey), nil + }) + if err != nil { + return -1, PermUnauthorized + } + + claims := t.Claims.(*jwt.StandardClaims) + + id, err := strconv.Atoi(claims.Issuer) + permid, err := strconv.Atoi(claims.Subject) + if err != nil { + return -1, PermUnauthorized + } + return id, uint8(permid) +} + +func InitOAuth() { + AddHandler("login", LoginNode, PermUnauthorized, func(ctx Context) { + var t struct { + Password string + } + + if DecodeRequest(ctx.GetRequest(), &t) != nil { + fmt.Println("Error accured while decoding Testrequest!!") + } + + // empty check + if t.Password == "" { + ctx.Error("empty password") + return + } + + // generate Argon2 Hash of passed pwd + HashPassword(t.Password) + // todo use hashed password + + var password string + + 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(0)), + Subject: strconv.Itoa(int(PermUser)), + ExpiresAt: expires, + }) + + token, err := claims.SignedString([]byte(SignKey)) + if err != nil { + fmt.Println(err.Error()) + ctx.Error("failed to generate authorization token") + return + } + + type ResponseType struct { + Token Token + } + + ctx.Json(Token{ + Token: token, + ExpiresAt: expires, + }) + }) +} + +func ValidateToken(f func(rw http.ResponseWriter, req *http.Request, node int, tokenInfo *oauth2.TokenInfo), node int) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + tokeninfo, err := srv.ValidationBearerToken(r) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + f(w, r, node, &tokeninfo) + } +} diff --git a/apiGo/api/api/Context.go b/apiGo/api/api/Context.go new file mode 100644 index 0000000..edde0ef --- /dev/null +++ b/apiGo/api/api/Context.go @@ -0,0 +1,60 @@ +package api + +import ( + "fmt" + "net/http" +) + +type Context interface { + Json(t interface{}) + Text(msg string) + Error(msg string) + Errorf(msg string, args ...interface{}) + GetRequest() *http.Request + GetWriter() http.ResponseWriter + UserID() int +} + +type apicontext struct { + writer http.ResponseWriter + request *http.Request + responseWritten bool + userid int + permid uint8 +} + +func (r *apicontext) GetRequest() *http.Request { + return r.request +} + +func (r *apicontext) UserID() int { + return r.userid +} + +func (r *apicontext) GetWriter() http.ResponseWriter { + return r.writer +} + +func (r *apicontext) Json(t interface{}) { + r.writer.Write(Jsonify(t)) + r.responseWritten = true +} + +func (r *apicontext) Text(msg string) { + r.writer.Write([]byte(msg)) + r.responseWritten = true +} + +func (r *apicontext) Error(msg string) { + type Error struct { + Message string + } + + r.writer.WriteHeader(500) + r.writer.Write(Jsonify(Error{Message: msg})) + r.responseWritten = true +} + +func (r *apicontext) Errorf(msg string, args ...interface{}) { + r.Error(fmt.Sprintf(msg, &args)) +} diff --git a/apiGo/api/api/Hash.go b/apiGo/api/api/Hash.go new file mode 100644 index 0000000..7ddd8e5 --- /dev/null +++ b/apiGo/api/api/Hash.go @@ -0,0 +1,13 @@ +package api + +import ( + "encoding/hex" + "golang.org/x/crypto/argon2" +) + +func HashPassword(pwd string) *string { + // todo generate random salt + hash := argon2.IDKey([]byte(pwd), []byte(SignKey), 3, 64*1024, 2, 32) + hexx := hex.EncodeToString(hash) + return &hexx +} diff --git a/apiGo/api/api/Hash_test.go b/apiGo/api/api/Hash_test.go new file mode 100644 index 0000000..eee9f2b --- /dev/null +++ b/apiGo/api/api/Hash_test.go @@ -0,0 +1,11 @@ +package api + +import "testing" + +func TestHashlength(t *testing.T) { + h := HashPassword("test") + + if len(*h) != 64 { + t.Errorf("Invalid hash length: %d", len(*h)) + } +} diff --git a/apiGo/api/api/Helpers.go b/apiGo/api/api/Helpers.go new file mode 100644 index 0000000..0b4f338 --- /dev/null +++ b/apiGo/api/api/Helpers.go @@ -0,0 +1,74 @@ +package api + +import ( + "bytes" + "encoding/json" + "fmt" + "net/http" + "reflect" +) + +func Jsonify(v interface{}) []byte { + // Jsonify results + str, err := json.Marshal(v) + if err != nil { + fmt.Println("Error while Jsonifying return object: " + err.Error()) + } + return str +} + +// DecodeRequest decodes the request +func DecodeRequest(request *http.Request, arg interface{}) error { + buf := new(bytes.Buffer) + buf.ReadFrom(request.Body) + body := buf.String() + + err := json.Unmarshal([]byte(body), &arg) + if err != nil { + fmt.Println("JSON decode Error" + err.Error()) + } + + return err +} + +// setField set a specific field of an object with an object provided +func setField(obj interface{}, name string, value interface{}) error { + structValue := reflect.ValueOf(obj).Elem() + structFieldValue := structValue.FieldByName(name) + + if !structFieldValue.IsValid() { + return fmt.Errorf("no such field: %s in obj", name) + } + + if !structFieldValue.CanSet() { + return fmt.Errorf("cannot set %s field value", name) + } + + structFieldType := structFieldValue.Type() + val := reflect.ValueOf(value) + + if structFieldType != val.Type() { + if val.Type().ConvertibleTo(structFieldType) { + // if type is convertible - convert and set + structFieldValue.Set(val.Convert(structFieldType)) + } else { + return fmt.Errorf("provided value %s type didn't match obj field type and isn't convertible", name) + } + } else { + // set value if type is the same + structFieldValue.Set(val) + } + + return nil +} + +// FillStruct fill a custom struct with objects of a map +func FillStruct(i interface{}, m map[string]interface{}) error { + for k, v := range m { + err := setField(i, k, v) + if err != nil { + return err + } + } + return nil +} diff --git a/apiGo/api/api/Helpers_test.go b/apiGo/api/api/Helpers_test.go new file mode 100644 index 0000000..7d3ad2b --- /dev/null +++ b/apiGo/api/api/Helpers_test.go @@ -0,0 +1,22 @@ +package api + +import "testing" + +func TestJsonify(t *testing.T) { + var obj = struct { + ID uint32 + Str string + Boo bool + }{ + ID: 42, + Str: "teststr", + Boo: true, + } + + res := Jsonify(obj) + exp := `{"ID":42,"Str":"teststr","Boo":true}` + + if string(res) != exp { + t.Errorf("Invalid json response: %s !== %s", string(res), exp) + } +} diff --git a/apiGo/api/oauth/CustomClientStore.go b/apiGo/api/oauth/CustomClientStore.go deleted file mode 100644 index 414dfb6..0000000 --- a/apiGo/api/oauth/CustomClientStore.go +++ /dev/null @@ -1,57 +0,0 @@ -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 not set assign default password - 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 -} diff --git a/apiGo/api/oauth/Oauth.go b/apiGo/api/oauth/Oauth.go deleted file mode 100644 index 024b962..0000000 --- a/apiGo/api/oauth/Oauth.go +++ /dev/null @@ -1,62 +0,0 @@ -package oauth - -import ( - "gopkg.in/oauth2.v3" - "gopkg.in/oauth2.v3/errors" - "gopkg.in/oauth2.v3/manage" - "gopkg.in/oauth2.v3/server" - "gopkg.in/oauth2.v3/store" - "log" - "net/http" -) - -var srv *server.Server - -func InitOAuth() { - manager := manage.NewDefaultManager() - // token store - manager.MustTokenStorage(store.NewMemoryTokenStore()) - - // create new secretstore - clientStore := NewCustomStore() - manager.MapClientStorage(clientStore) - - srv = server.NewServer(server.NewConfig(), manager) - srv.SetClientInfoHandler(server.ClientFormHandler) - manager.SetRefreshTokenCfg(manage.DefaultRefreshTokenCfg) - - srv.SetInternalErrorHandler(func(err error) (re *errors.Response) { - log.Println("Internal Error:", err.Error()) - return - }) - - srv.SetResponseErrorHandler(func(re *errors.Response) { - log.Println("Response Error:", re.Error.Error()) - }) - - http.HandleFunc("/authorize", func(w http.ResponseWriter, r *http.Request) { - err := srv.HandleAuthorizeRequest(w, r) - if err != nil { - http.Error(w, err.Error(), http.StatusBadRequest) - } - }) - - http.HandleFunc("/token", func(w http.ResponseWriter, r *http.Request) { - err := srv.HandleTokenRequest(w, r) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - } - }) -} - -func ValidateToken(f func(rw http.ResponseWriter, req *http.Request, node int, tokenInfo *oauth2.TokenInfo), node int) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - tokeninfo, err := srv.ValidationBearerToken(r) - if err != nil { - http.Error(w, err.Error(), http.StatusBadRequest) - return - } - - f(w, r, node, &tokeninfo) - } -} diff --git a/apiGo/config/Config_test.go b/apiGo/config/Config_test.go new file mode 100644 index 0000000..4c25a83 --- /dev/null +++ b/apiGo/config/Config_test.go @@ -0,0 +1,9 @@ +package config + +import "testing" + +func TestSaveLoadConfig(t *testing.T) { + generateNewConfig("", "openmediacenter.cfg") + + Init() +} diff --git a/apiGo/go.mod b/apiGo/go.mod index 8c8f167..bb4b634 100644 --- a/apiGo/go.mod +++ b/apiGo/go.mod @@ -3,8 +3,10 @@ module openmediacenter/apiGo go 1.16 require ( + github.com/dgrijalva/jwt-go v3.2.0+incompatible github.com/go-sql-driver/mysql v1.5.0 github.com/pelletier/go-toml/v2 v2.0.0-beta.3 + golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 gopkg.in/oauth2.v3 v3.12.0 nhooyr.io/websocket v1.8.7 ) diff --git a/apiGo/go.sum b/apiGo/go.sum index 2427f80..79d3e8e 100644 --- a/apiGo/go.sum +++ b/apiGo/go.sum @@ -41,7 +41,6 @@ github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk= github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM= github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= @@ -50,7 +49,6 @@ github.com/imkira/go-interpol v1.1.0 h1:KIiKr0VSG2CUW1hl1jpiyuzuJeKUUpC8iM1AIE7N github.com/imkira/go-interpol v1.1.0/go.mod h1:z0h2/2T3XF8kyEPpRgJ3kmNv+C43p+I/CoI+jC3w2iA= github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns= github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88/go.mod h1:3w7q1U84EfirKl04SVQ/s7nPm1ZPhiXd34z40TNz36k= github.com/klauspost/compress v1.8.2/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= @@ -78,9 +76,7 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= -github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= -github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= @@ -125,6 +121,7 @@ github.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FB github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 h1:BHyfKlQyqbsFN5p3IfnEUduWvb9is428/nNb5L3U01M= github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM= github.com/yudai/pp v2.0.1+incompatible/go.mod h1:PuxR/8QJ7cyCkFp/aUDS+JY727OFEZkTdatxwunjIkc= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= diff --git a/apiGo/main.go b/apiGo/main.go index 76507f1..b2a547e 100644 --- a/apiGo/main.go +++ b/apiGo/main.go @@ -2,18 +2,19 @@ package main import ( "fmt" - "log" - "net/http" "openmediacenter/apiGo/api" + api2 "openmediacenter/apiGo/api/api" "openmediacenter/apiGo/config" "openmediacenter/apiGo/database" "openmediacenter/apiGo/static" "openmediacenter/apiGo/videoparser" + "os" + "os/signal" ) func main() { fmt.Println("init OpenMediaCenter server") - port := 8081 + const port uint16 = 8081 config.Init() @@ -24,19 +25,25 @@ func main() { database.InitDB() defer database.Close() - api.AddVideoHandlers() - api.AddSettingsHandlers() - api.AddTagHandlers() - api.AddActorsHandlers() - api.AddTvshowHandlers() + api.AddHandlers() videoparser.SetupSettingsWebsocket() // add the static files static.ServeStaticFiles() - api.ServerInit() + // init api + errc := make(chan error, 1) + go func() { + errc <- api2.ServerInit(port) + }() - fmt.Printf("OpenMediacenter server up and running on port %d\n", port) - log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", port), nil)) + sigs := make(chan os.Signal, 1) + signal.Notify(sigs, os.Interrupt) + select { + case err := <-errc: + fmt.Printf("failed to serve: %v\n", err) + case sig := <-sigs: + fmt.Printf("terminating server: %v\n", sig) + } } diff --git a/src/App.test.js b/src/App.test.js index 57f45ce..704d591 100644 --- a/src/App.test.js +++ b/src/App.test.js @@ -2,6 +2,7 @@ import React from 'react'; import App from './App'; import {shallow} from 'enzyme'; import GlobalInfos from "./utils/GlobalInfos"; +import {LoginContext} from './utils/context/LoginContext'; describe('', function () { it('renders without crashing ', function () { @@ -19,28 +20,6 @@ describe('', function () { const wrapper = shallow(); wrapper.setState({password: false}); expect(wrapper.find('.navitem')).toHaveLength(4); - - GlobalInfos.setTVShowsEnabled(true); - - wrapper.instance().forceUpdate(); - expect(wrapper.find('.navitem')).toHaveLength(5); - }); - - it('test initial fetch from api', done => { - callAPIMock({ - MediacenterName: 'testname' - }) - - GlobalInfos.enableDarkTheme = jest.fn((r) => {}) - - const wrapper = shallow(); - - process.nextTick(() => { - expect(document.title).toBe('testname'); - - global.fetch.mockClear(); - done(); - }); }); it('test render of password page', function () { diff --git a/src/App.tsx b/src/App.tsx index d78d444..3ca0d47 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, {useContext} from 'react'; import HomePage from './pages/HomePage/HomePage'; import RandomPage from './pages/RandomPage/RandomPage'; import GlobalInfos from './utils/GlobalInfos'; @@ -9,21 +9,18 @@ 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'; +import {FeatureContext} from './utils/context/FeatureContext'; interface state { - password: boolean | null; // null if uninitialized - true if pwd needed false if not needed mediacentername: string; } @@ -34,102 +31,37 @@ class App extends React.Component<{}, state> { constructor(props: {}) { super(props); - token.init(new CookieTokenStore()); - - let pwdneeded: boolean | null = null; - - if (token.apiTokenValid()) { - pwdneeded = false; - } else { - token.refreshAPIToken((err) => { - if (err === 'invalid_client') { - this.setState({password: true}); - } else if (err === '') { - this.setState({password: false}); - } else { - console.log('unimplemented token error: ' + err); - } - }); - } - this.state = { - mediacentername: 'OpenMediaCenter', - password: pwdneeded + mediacentername: 'OpenMediaCenter' }; // force an update on theme change GlobalInfos.onThemeChange(() => { this.forceUpdate(); }); - - // set the hook to load passwordfield on global func call - GlobalInfos.loadPasswordPage = (callback?: () => void): void => { - // try refreshing the token - token.refreshAPIToken((err) => { - if (err !== '') { - this.setState({password: true}); - } else { - // call callback if request was successful - if (callback) { - callback(); - } - } - }, true); - }; - } - - initialAPICall(): void { - // this is the first api call so if it fails we know there is no connection to backend - callAPI(APINode.Settings, {action: 'loadInitialData'}, (result: SettingsTypes.initialApiCallData) => { - // set theme - GlobalInfos.enableDarkTheme(result.DarkMode); - - GlobalInfos.setVideoPaths(result.VideoPath, result.TVShowPath); - - GlobalInfos.setTVShowsEnabled(result.TVShowEnabled); - GlobalInfos.setFullDeleteEnabled(result.FullDeleteEnabled); - - this.setState({ - mediacentername: result.MediacenterName - }); - // set tab title to received mediacenter name - document.title = result.MediacenterName; - }); - } - - componentDidMount(): void { - this.initialAPICall(); } render(): JSX.Element { // add the main theme to the page body document.body.className = GlobalInfos.getThemeStyle().backgroundcolor; - if (this.state.password === true) { - // render authentication page if auth is neccessary - return ( - { - this.setState({password: false}); - // reinit general infos - this.initialAPICall(); - }} - /> - ); - } else if (this.state.password === false) { - return ( - -
+ return ( + + + + + + {this.navBar()} - {this.routing()} -
-
- ); - } else { - return <>still loading...; - } + + + + + ); } + static contextType = FeatureContext; + /** * render the top navigation bar */ @@ -139,73 +71,85 @@ class App extends React.Component<{}, state> { return (
{this.state.mediacentername}
- + Home - + Random Video - + Categories - {GlobalInfos.isTVShowEnabled() ? ( - + {this.context.TVShowEnabled ? ( + TV Shows ) : null} - + Settings
); } - - /** - * render the react router elements - */ - routing(): JSX.Element { - return ( - - - - - - - - - - - - - - - - - - - - - {GlobalInfos.isTVShowEnabled() ? ( - - - - ) : null} - - {GlobalInfos.isTVShowEnabled() ? ( - - - - ) : null} - - - - - - ); - } } +const MyRouter = (): JSX.Element => { + const match = useRouteMatch(); + const features = useContext(FeatureContext); + + return ( + + + + + + + + + + + + + + + + + + + + + {features.TVShowEnabled ? ( + + + + ) : null} + + {features.TVShowEnabled ? ( + + + + ) : null} + + + + + + ); +}; + export default App; diff --git a/src/elements/ActorTile/ActorTile.tsx b/src/elements/ActorTile/ActorTile.tsx index 96b590c..5a9183e 100644 --- a/src/elements/ActorTile/ActorTile.tsx +++ b/src/elements/ActorTile/ActorTile.tsx @@ -21,7 +21,7 @@ class ActorTile extends React.Component { if (this.props.onClick) { return this.renderActorTile(this.props.onClick); } else { - return {this.renderActorTile(() => {})}; + return {this.renderActorTile(() => {})}; } } diff --git a/src/elements/VideoContainer/VideoContainer.tsx b/src/elements/VideoContainer/VideoContainer.tsx index 53d3e59..e06c525 100644 --- a/src/elements/VideoContainer/VideoContainer.tsx +++ b/src/elements/VideoContainer/VideoContainer.tsx @@ -26,7 +26,7 @@ const VideoContainer = (props: Props): JSX.Element => { ); }} name={el.MovieName} - linkPath={'/player/' + el.MovieId} + linkPath={'/media/player/' + el.MovieId} /> )} data={props.data}> diff --git a/src/index.tsx b/src/index.tsx index 78e0469..985ebbf 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,13 +1,19 @@ import React from 'react'; import ReactDOM from 'react-dom'; import App from './App'; +import {BrowserRouter} from 'react-router-dom'; +import {FeatureContextProvider} from './utils/context/FeatureContext'; // don't allow console logs within production env global.console.log = process.env.NODE_ENV !== 'development' ? (_: string | number | boolean): void => {} : global.console.log; ReactDOM.render( - + + + + + , document.getElementById('root') ); diff --git a/src/pages/AuthenticationPage/AuthenticationPage.test.js b/src/pages/AuthenticationPage/AuthenticationPage.test.js index fd46cb3..4e76c45 100644 --- a/src/pages/AuthenticationPage/AuthenticationPage.test.js +++ b/src/pages/AuthenticationPage/AuthenticationPage.test.js @@ -1,7 +1,6 @@ import React from 'react'; import AuthenticationPage from './AuthenticationPage'; import {shallow} from 'enzyme'; -import {token} from "../../utils/TokenHandler"; describe('', function () { it('renders without crashing ', function () { @@ -12,10 +11,8 @@ describe('', function () { it('test button click', function () { const func = jest.fn(); - const wrapper = shallow(); - wrapper.instance().authenticate = jest.fn(() => { - wrapper.instance().props.onSuccessLogin() - }); + const wrapper = shallow(); + wrapper.instance().authenticate = func; wrapper.setState({pwdText: 'testpwd'}); wrapper.find('Button').simulate('click'); @@ -23,33 +20,16 @@ describe('', function () { expect(func).toHaveBeenCalledTimes(1); }); - it('test fail authenticate', function () { + it('test keyenter', function () { const events = mockKeyPress(); - token.refreshAPIToken = jest.fn().mockImplementation((callback, force, pwd) => { - callback('there was an error') - }); - const wrapper = shallow(); - events.keyup({key: 'Enter'}); - - expect(wrapper.state().wrongPWDInfo).toBe(true); - }); - - it('test success authenticate', function () { - const events = mockKeyPress(); - const func = jest.fn() - - token.refreshAPIToken = jest.fn().mockImplementation((callback, force, pwd) => { - callback('') - }); - - const wrapper = shallow(); + const func = jest.fn(); + wrapper.instance().authenticate = func; events.keyup({key: 'Enter'}); - expect(wrapper.state().wrongPWDInfo).toBe(false); expect(func).toHaveBeenCalledTimes(1); }); }); diff --git a/src/pages/AuthenticationPage/AuthenticationPage.tsx b/src/pages/AuthenticationPage/AuthenticationPage.tsx index 5a3a771..e2ff328 100644 --- a/src/pages/AuthenticationPage/AuthenticationPage.tsx +++ b/src/pages/AuthenticationPage/AuthenticationPage.tsx @@ -2,18 +2,18 @@ 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; wrongPWDInfo: boolean; } -interface Props { - onSuccessLogin: () => void; -} +interface Props {} class AuthenticationPage extends React.Component { constructor(props: Props) { @@ -36,6 +36,8 @@ class AuthenticationPage extends React.Component { removeKeyHandler(this.keypress); } + static contextType = LoginContext; + render(): JSX.Element { return ( <> @@ -76,21 +78,18 @@ class AuthenticationPage extends React.Component { * request a new token and check if pwd was valid */ authenticate(): void { - token.refreshAPIToken( - (error) => { - if (error !== '') { - this.setState({wrongPWDInfo: true}); + callApiUnsafe( + APINode.Login, + {action: 'login', Password: this.state.pwdText}, + (r: Token) => { + cookie.Store(r); - // set timeout to make the info auto-disappearing - setTimeout(() => { - this.setState({wrongPWDInfo: false}); - }, 2000); - } else { - this.props.onSuccessLogin(); - } + this.context.setLoginState(LoginState.LoggedIn); }, - true, - this.state.pwdText + () => { + this.setState({wrongPWDInfo: true}); + setTimeout(() => this.setState({wrongPWDInfo: false}), 2000); + } ); } diff --git a/src/pages/CategoryPage/CategoryPage.test.js b/src/pages/CategoryPage/CategoryPage.test.js deleted file mode 100644 index 97e61d0..0000000 --- a/src/pages/CategoryPage/CategoryPage.test.js +++ /dev/null @@ -1,10 +0,0 @@ -import {shallow} from 'enzyme'; -import React from 'react'; -import CategoryPage from './CategoryPage'; - -describe('', function () { - it('renders without crashing ', function () { - const wrapper = shallow(); - wrapper.unmount(); - }); -}); diff --git a/src/pages/CategoryPage/CategoryPage.tsx b/src/pages/CategoryPage/CategoryPage.tsx index 2bed929..3249e42 100644 --- a/src/pages/CategoryPage/CategoryPage.tsx +++ b/src/pages/CategoryPage/CategoryPage.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import {Route, Switch} from 'react-router-dom'; +import {Route, Switch, useRouteMatch} from 'react-router-dom'; import {CategoryViewWR} from './CategoryView'; import TagView from './TagView'; @@ -7,19 +7,19 @@ import TagView from './TagView'; * Component for Category Page * Contains a Tag Overview and loads specific Tag videos in VideoContainer */ -class CategoryPage extends React.Component { - render(): JSX.Element { - return ( - - - - - - - - - ); - } -} +const CategoryPage = (): JSX.Element => { + const match = useRouteMatch(); + + return ( + + + + + + + + + ); +}; export default CategoryPage; diff --git a/src/pages/CategoryPage/CategoryView.tsx b/src/pages/CategoryPage/CategoryView.tsx index 1ac5de9..0570be8 100644 --- a/src/pages/CategoryPage/CategoryView.tsx +++ b/src/pages/CategoryPage/CategoryView.tsx @@ -119,9 +119,10 @@ export class CategoryView extends React.Component { + (e) => { + console.log(e); // if there is an load error redirect to home page - this.props.history.push('/'); + // this.props.history.push('/'); } ); } diff --git a/src/pages/CategoryPage/TagView.tsx b/src/pages/CategoryPage/TagView.tsx index da47e02..25d264b 100644 --- a/src/pages/CategoryPage/TagView.tsx +++ b/src/pages/CategoryPage/TagView.tsx @@ -56,7 +56,7 @@ class TagView extends React.Component { ( - + )} diff --git a/src/pages/HomePage/HomePage.tsx b/src/pages/HomePage/HomePage.tsx index 752e259..4a8be75 100644 --- a/src/pages/HomePage/HomePage.tsx +++ b/src/pages/HomePage/HomePage.tsx @@ -14,7 +14,6 @@ import {DefaultTags} from '../../types/GeneralTypes'; import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; import {faSortDown} from '@fortawesome/free-solid-svg-icons'; -// eslint-disable-next-line no-shadow export enum SortBy { date, likes, diff --git a/src/pages/Player/Player.test.js b/src/pages/Player/Player.test.js index 92206a8..5f38be2 100644 --- a/src/pages/Player/Player.test.js +++ b/src/pages/Player/Player.test.js @@ -3,12 +3,13 @@ import React from 'react'; import {Player} from './Player'; import {callAPI} from '../../utils/Api'; import GlobalInfos from "../../utils/GlobalInfos"; +import {LoginContext} from '../../utils/context/LoginContext'; describe('', function () { // help simulating id passed by url function instance() { - return shallow(); + return shallow(, {context: LoginContext}); } it('renders without crashing ', function () { @@ -88,23 +89,13 @@ describe('', function () { it('test fully delete popup rendering', function () { const wrapper = instance(); - // allow videos to be fully deletable - GlobalInfos.setFullDeleteEnabled(true); + wrapper.setContext({VideosFullyDeleteable: true}) wrapper.setState({deletepopupvisible: true}); expect(wrapper.find('ButtonPopup')).toHaveLength(1) }); - it('test delete popup rendering', function () { - const wrapper = instance(); - - GlobalInfos.setFullDeleteEnabled(false); - wrapper.setState({deletepopupvisible: true}); - - expect(wrapper.find('ButtonPopup')).toHaveLength(1) - }); - it('test delete button', () => { const wrapper = instance(); const callback = jest.fn(); @@ -112,7 +103,7 @@ describe('', function () { wrapper.setProps({history: {goBack: callback}}); callAPIMock({result: 'success'}) - GlobalInfos.setFullDeleteEnabled(false); + wrapper.setContext({VideosFullyDeleteable: false}) // request the popup to pop wrapper.find('.videoactions').find('Button').at(2).simulate('click'); @@ -125,7 +116,7 @@ describe('', function () { expect(callback).toHaveBeenCalledTimes(1); // now lets test if this works also with the fullydeletepopup - GlobalInfos.setFullDeleteEnabled(true); + wrapper.setContext({VideosFullyDeleteable: true}) // request the popup to pop wrapper.setState({deletepopupvisible: true}, () => { // click the first submit button diff --git a/src/pages/Player/Player.tsx b/src/pages/Player/Player.tsx index d777d84..768eece 100644 --- a/src/pages/Player/Player.tsx +++ b/src/pages/Player/Player.tsx @@ -22,6 +22,7 @@ import {Button} from '../../elements/GPElements/Button'; import {VideoTypes} from '../../types/ApiTypes'; import GlobalInfos from '../../utils/GlobalInfos'; import {ButtonPopup} from '../../elements/Popups/ButtonPopup/ButtonPopup'; +import {FeatureContext} from '../../utils/context/FeatureContext'; interface Props extends RouteComponentProps<{id: string}> {} @@ -65,6 +66,8 @@ export class Player extends React.Component { this.quickAddTag = this.quickAddTag.bind(this); } + static contextType = FeatureContext; + componentDidMount(): void { // initial fetch of current movie data this.fetchMovieData(); @@ -205,7 +208,7 @@ export class Player extends React.Component { } renderDeletePopup(): JSX.Element { - if (GlobalInfos.isVideoFulldeleteable()) { + if (this.context.VideosFullyDeleteable) { return ( this.setState({deletepopupvisible: false})} diff --git a/src/pages/SettingsPage/SettingsPage.test.js b/src/pages/SettingsPage/SettingsPage.test.js deleted file mode 100644 index e6459dc..0000000 --- a/src/pages/SettingsPage/SettingsPage.test.js +++ /dev/null @@ -1,10 +0,0 @@ -import {shallow} from 'enzyme'; -import React from 'react'; -import SettingsPage from './SettingsPage'; - -describe('', function () { - it('renders without crashing ', function () { - const wrapper = shallow(); - wrapper.unmount(); - }); -}); diff --git a/src/pages/SettingsPage/SettingsPage.tsx b/src/pages/SettingsPage/SettingsPage.tsx index dac2d8b..e6aa9d2 100644 --- a/src/pages/SettingsPage/SettingsPage.tsx +++ b/src/pages/SettingsPage/SettingsPage.tsx @@ -1,54 +1,56 @@ -import React from 'react'; +import React, {useContext} from 'react'; 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'; +import {FeatureContext} from '../../utils/context/FeatureContext'; /** * The Settingspage handles all kinds of settings for the mediacenter * and is basically a wrapper for child-tabs */ -class SettingsPage extends React.Component { - render(): JSX.Element { - const themestyle = GlobalInfos.getThemeStyle(); - return ( -
-
-
Settings
- -
General
+const SettingsPage = (): JSX.Element => { + const themestyle = GlobalInfos.getThemeStyle(); + const match = useRouteMatch(); + 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 <>; + } +};