From e985eb941c35ff91718612d029056812574bffd2 Mon Sep 17 00:00:00 2001 From: lukas Date: Thu, 16 Sep 2021 22:38:28 +0200 Subject: [PATCH 1/7] overwork most of how api works dont transmit handler within payload don't use oauth to gen token -- jwt instead --- apiGo/api/Actors.go | 53 ++++++------ apiGo/api/ApiBase.go | 113 ------------------------ apiGo/api/Helpers.go | 59 +------------ apiGo/api/Settings.go | 43 +++++---- apiGo/api/TVShows.go | 47 +++++----- apiGo/api/Tags.go | 55 ++++++------ apiGo/api/Video.go | 116 ++++++++++++------------- apiGo/api/api/ApiBase.go | 125 +++++++++++++++++++++++++++ apiGo/api/{ => api}/ApiBase_test.go | 6 +- apiGo/api/api/Auth.go | 119 +++++++++++++++++++++++++ apiGo/api/api/Context.go | 60 +++++++++++++ apiGo/api/api/Hash.go | 13 +++ apiGo/api/api/Helpers.go | 74 ++++++++++++++++ apiGo/api/oauth/CustomClientStore.go | 57 ------------ apiGo/api/oauth/Oauth.go | 62 ------------- apiGo/go.mod | 2 + apiGo/go.sum | 5 +- apiGo/main.go | 3 +- src/utils/Api.ts | 2 +- 19 files changed, 561 insertions(+), 453 deletions(-) delete mode 100644 apiGo/api/ApiBase.go create mode 100644 apiGo/api/api/ApiBase.go rename apiGo/api/{ => api}/ApiBase_test.go (84%) create mode 100644 apiGo/api/api/Auth.go create mode 100644 apiGo/api/api/Context.go create mode 100644 apiGo/api/api/Hash.go create mode 100644 apiGo/api/api/Helpers.go delete mode 100644 apiGo/api/oauth/CustomClientStore.go delete mode 100644 apiGo/api/oauth/Oauth.go diff --git a/apiGo/api/Actors.go b/apiGo/api/Actors.go index 1cc511f..df17514 100644 --- a/apiGo/api/Actors.go +++ b/apiGo/api/Actors.go @@ -2,6 +2,7 @@ package api import ( "fmt" + "openmediacenter/apiGo/api/api" "openmediacenter/apiGo/api/types" "openmediacenter/apiGo/database" ) @@ -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/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..1775ed0 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" @@ -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..0650abc 100644 --- a/apiGo/api/TVShows.go +++ b/apiGo/api/TVShows.go @@ -2,6 +2,7 @@ package api import ( "fmt" + "openmediacenter/apiGo/api/api" "openmediacenter/apiGo/config" "openmediacenter/apiGo/database" ) @@ -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..357ee25 100644 --- a/apiGo/api/Tags.go +++ b/apiGo/api/Tags.go @@ -2,6 +2,7 @@ package api import ( "fmt" + "openmediacenter/apiGo/api/api" "openmediacenter/apiGo/database" "regexp" ) @@ -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..aa31057 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" @@ -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..c33efc8 --- /dev/null +++ b/apiGo/api/api/ApiBase.go @@ -0,0 +1,125 @@ +package api + +import ( + "fmt" + "net/http" +) + +const ( + VideoNode = "video" + TagNode = "tag" + SettingsNode = "setting" + ActorNode = "actor" + TVShowNode = "tv" + LoginNode = "login" +) + +//type HandlerInfo struct { +// ID string +// Token string +// Data map[string]interface{} +//} + +//type actionStruct struct { +// Action string +//} + +//type Handler struct { +// action string +// handler api.PermUser, func(context api.Context) +// apiNode int +//} + +func AddHandler(action string, apiNode string, perm uint8, handler func(ctx Context)) { + http.Handle(fmt.Sprintf("/api/%s/%s", apiNode, action), http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { + tokenheader := request.Header.Get("Token") + + id := -1 + permid := PermUnauthorized + + // check token if token provided + if tokenheader != "" { + id, permid = TokenValid(request.Header.Get("Token")) + } + + ctx := &apicontext{writer: writer, responseWritten: false, request: request, userid: id, permid: permid} + + // check if rights are sufficient to perform the action + if permid <= perm { + handler(ctx) + + if !ctx.responseWritten { + // none of the response functions called so send default response + ctx.Error("Unknown server Error occured") + writer.WriteHeader(501) + } + } else { + ctx.Error("insufficient permissions") + writer.WriteHeader(501) + } + })) +} + +func ServerInit() { + // initialize auth service and add corresponding auth routes + InitOAuth() +} + +//func handleAPICall(action string, requestBody string, apiNode int, context api.Context) { +// 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/api/ApiBase_test.go similarity index 84% rename from apiGo/api/ApiBase_test.go rename to apiGo/api/api/ApiBase_test.go index d692f01..1d86191 100644 --- a/apiGo/api/ApiBase_test.go +++ b/apiGo/api/api/ApiBase_test.go @@ -11,7 +11,7 @@ func cleanUp() { func TestAddHandler(t *testing.T) { cleanUp() - AddHandler("test", ActorNode, func(info *HandlerInfo) []byte { + AddHandler("test", ActorNode, api.PermUser, func(context api.Context) { return nil }) if len(handlers) != 1 { @@ -23,7 +23,7 @@ func TestCallOfHandler(t *testing.T) { cleanUp() i := 0 - AddHandler("test", ActorNode, func(info *HandlerInfo) []byte { + AddHandler("test", ActorNode, api.PermUser, func(context api.Context) { i++ return nil }) @@ -39,7 +39,7 @@ func TestCallOfHandler(t *testing.T) { func TestDecodingOfArguments(t *testing.T) { cleanUp() - AddHandler("test", ActorNode, func(info *HandlerInfo) []byte { + AddHandler("test", ActorNode, api.PermUser, func(context api.Context) { var args struct { Test string TestInt int diff --git a/apiGo/api/api/Auth.go b/apiGo/api/api/Auth.go new file mode 100644 index 0000000..85aa07f --- /dev/null +++ b/apiGo/api/api/Auth.go @@ -0,0 +1,119 @@ +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 { + Username string + Password string + } + + if DecodeRequest(ctx.GetRequest(), &t) != nil { + fmt.Println("Error accured while decoding Testrequest!!") + } + + // empty check + if t.Password == "" || t.Username == "" { + ctx.Error("empty username or password") + return + } + + // generate Argon2 Hash of passed pwd + pwd := HashPassword(t.Password) + + var id uint + var name string + var rightid uint8 + + err := database.QueryRow("SELECT userId,userName,rightId FROM User WHERE userName=? AND password=?", t.Username, *pwd).Scan(&id, &name, &rightid) + if err != nil { + ctx.Error("unauthorized") + return + } + + expires := time.Now().Add(time.Hour * TokenExpireHours).Unix() + claims := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.StandardClaims{ + Issuer: strconv.Itoa(int(id)), + Subject: strconv.Itoa(int(rightid)), + 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 + Username string + UserPerm uint8 + } + + ctx.Json(ResponseType{ + Token: Token{ + Token: token, + ExpiresAt: expires, + }, + Username: t.Username, + UserPerm: rightid, + }) + }) +} + +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..d2bfb2d --- /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/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/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/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..d307415 100644 --- a/apiGo/main.go +++ b/apiGo/main.go @@ -5,6 +5,7 @@ import ( "log" "net/http" "openmediacenter/apiGo/api" + api2 "openmediacenter/apiGo/api/api" "openmediacenter/apiGo/config" "openmediacenter/apiGo/database" "openmediacenter/apiGo/static" @@ -35,7 +36,7 @@ func main() { // add the static files static.ServeStaticFiles() - api.ServerInit() + api2.ServerInit() fmt.Printf("OpenMediacenter server up and running on port %d\n", port) log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", port), nil)) diff --git a/src/utils/Api.ts b/src/utils/Api.ts index 5fb447d..55ba285 100644 --- a/src/utils/Api.ts +++ b/src/utils/Api.ts @@ -68,7 +68,7 @@ function generalAPICall( mytoken: string ): void { (async function (): Promise { - const response = await fetch(APIPREFIX + apinode, { + const response = await fetch(APIPREFIX + apinode + '/' + fd.action, { method: 'POST', body: JSON.stringify(fd), headers: new Headers({ From f17bac399a608d4dcf164510b3ece784b120a41b Mon Sep 17 00:00:00 2001 From: lukas Date: Sun, 19 Sep 2021 23:20:37 +0200 Subject: [PATCH 2/7] basic frontend implementation of new token system --- apiGo/api/api/ApiBase.go | 61 +++-- apiGo/api/api/Auth.go | 34 +-- src/App.tsx | 240 ++++++++---------- .../VideoContainer/VideoContainer.tsx | 2 +- src/index.tsx | 5 +- .../AuthenticationPage/AuthenticationPage.tsx | 28 +- src/pages/CategoryPage/CategoryPage.tsx | 32 +-- src/pages/CategoryPage/CategoryView.tsx | 5 +- src/pages/CategoryPage/TagView.tsx | 2 +- src/pages/SettingsPage/SettingsPage.tsx | 80 +++--- src/utils/Api.ts | 19 +- src/utils/TokenHandler.ts | 135 ---------- src/utils/TokenStore/CookieTokenStore.ts | 48 ---- src/utils/TokenStore/TokenStore.ts | 11 - src/utils/context/Cookie.ts | 55 ++++ src/utils/context/LoginContext.ts | 34 +++ src/utils/context/LoginContextProvider.tsx | 105 ++++++++ 17 files changed, 436 insertions(+), 460 deletions(-) delete mode 100644 src/utils/TokenHandler.ts delete mode 100644 src/utils/TokenStore/CookieTokenStore.ts delete mode 100644 src/utils/TokenStore/TokenStore.ts create mode 100644 src/utils/context/Cookie.ts create mode 100644 src/utils/context/LoginContext.ts create mode 100644 src/utils/context/LoginContextProvider.tsx diff --git a/apiGo/api/api/ApiBase.go b/apiGo/api/api/ApiBase.go index c33efc8..9bb9ee5 100644 --- a/apiGo/api/api/ApiBase.go +++ b/apiGo/api/api/ApiBase.go @@ -3,12 +3,13 @@ package api import ( "fmt" "net/http" + "openmediacenter/apiGo/database/settings" ) const ( VideoNode = "video" - TagNode = "tag" - SettingsNode = "setting" + TagNode = "tags" + SettingsNode = "settings" ActorNode = "actor" TVShowNode = "tv" LoginNode = "login" @@ -32,34 +33,44 @@ const ( func AddHandler(action string, apiNode string, perm uint8, handler func(ctx Context)) { http.Handle(fmt.Sprintf("/api/%s/%s", apiNode, action), http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { - tokenheader := request.Header.Get("Token") - - id := -1 - permid := PermUnauthorized - - // check token if token provided - if tokenheader != "" { - id, permid = TokenValid(request.Header.Get("Token")) - } - - ctx := &apicontext{writer: writer, responseWritten: false, request: request, userid: id, permid: permid} - - // check if rights are sufficient to perform the action - if permid <= perm { - handler(ctx) - - if !ctx.responseWritten { - // none of the response functions called so send default response - ctx.Error("Unknown server Error occured") - writer.WriteHeader(501) - } + srvPwd := settings.GetPassword() + if srvPwd == nil { + // no password set + ctx := &apicontext{writer: writer, responseWritten: false, request: request, userid: -1, permid: PermUnauthorized} + callHandler(ctx, handler, writer) } else { - ctx.Error("insufficient permissions") - writer.WriteHeader(501) + tokenheader := request.Header.Get("Token") + + id := -1 + permid := PermUnauthorized + + // check token if token provided + if tokenheader != "" { + id, permid = TokenValid(request.Header.Get("Token")) + } + + ctx := &apicontext{writer: writer, responseWritten: false, request: request, userid: id, permid: permid} + + // check if rights are sufficient to perform the action + if permid <= perm { + callHandler(ctx, handler, writer) + } else { + ctx.Error("insufficient permissions") + } } })) } +func callHandler(ctx *apicontext, handler func(ctx Context), writer http.ResponseWriter) { + handler(ctx) + + if !ctx.responseWritten { + // none of the response functions called so send default response + ctx.Error("Unknown server Error occured") + writer.WriteHeader(501) + } +} + func ServerInit() { // initialize auth service and add corresponding auth routes InitOAuth() diff --git a/apiGo/api/api/Auth.go b/apiGo/api/api/Auth.go index 85aa07f..9e50ddd 100644 --- a/apiGo/api/api/Auth.go +++ b/apiGo/api/api/Auth.go @@ -48,7 +48,6 @@ func TokenValid(token string) (int, uint8) { func InitOAuth() { AddHandler("login", LoginNode, PermUnauthorized, func(ctx Context) { var t struct { - Username string Password string } @@ -57,28 +56,27 @@ func InitOAuth() { } // empty check - if t.Password == "" || t.Username == "" { - ctx.Error("empty username or password") + if t.Password == "" { + ctx.Error("empty password") return } // generate Argon2 Hash of passed pwd - pwd := HashPassword(t.Password) + HashPassword(t.Password) + // todo use hashed password - var id uint - var name string - var rightid uint8 + var password string - err := database.QueryRow("SELECT userId,userName,rightId FROM User WHERE userName=? AND password=?", t.Username, *pwd).Scan(&id, &name, &rightid) - if err != nil { + err := database.QueryRow("SELECT password FROM settings WHERE 1").Scan(&password) + if err != nil || t.Password != password { ctx.Error("unauthorized") return } expires := time.Now().Add(time.Hour * TokenExpireHours).Unix() claims := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.StandardClaims{ - Issuer: strconv.Itoa(int(id)), - Subject: strconv.Itoa(int(rightid)), + Issuer: strconv.Itoa(int(0)), + Subject: strconv.Itoa(int(PermUser)), ExpiresAt: expires, }) @@ -90,18 +88,12 @@ func InitOAuth() { } type ResponseType struct { - Token Token - Username string - UserPerm uint8 + Token Token } - ctx.Json(ResponseType{ - Token: Token{ - Token: token, - ExpiresAt: expires, - }, - Username: t.Username, - UserPerm: rightid, + ctx.Json(Token{ + Token: token, + ExpiresAt: expires, }) }) } diff --git a/src/App.tsx b/src/App.tsx index d78d444..af62270 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -9,21 +9,17 @@ import style from './App.module.css'; import SettingsPage from './pages/SettingsPage/SettingsPage'; import CategoryPage from './pages/CategoryPage/CategoryPage'; -import {APINode, callAPI} from './utils/Api'; -import {BrowserRouter as Router, NavLink, Route, Switch} from 'react-router-dom'; +import {NavLink, Route, Switch, useRouteMatch} from 'react-router-dom'; import Player from './pages/Player/Player'; import ActorOverviewPage from './pages/ActorOverviewPage/ActorOverviewPage'; import ActorPage from './pages/ActorPage/ActorPage'; -import {SettingsTypes} from './types/ApiTypes'; import AuthenticationPage from './pages/AuthenticationPage/AuthenticationPage'; import TVShowPage from './pages/TVShowPage/TVShowPage'; import TVPlayer from './pages/TVShowPage/TVPlayer'; -import {CookieTokenStore} from './utils/TokenStore/CookieTokenStore'; -import {token} from './utils/TokenHandler'; +import {LoginContextProvider} from './utils/context/LoginContextProvider'; interface state { - password: boolean | null; // null if uninitialized - true if pwd needed false if not needed mediacentername: string; } @@ -34,100 +30,63 @@ class App extends React.Component<{}, state> { constructor(props: {}) { super(props); - token.init(new CookieTokenStore()); - - let pwdneeded: boolean | null = null; - - if (token.apiTokenValid()) { - pwdneeded = false; - } else { - token.refreshAPIToken((err) => { - if (err === 'invalid_client') { - this.setState({password: true}); - } else if (err === '') { - this.setState({password: false}); - } else { - console.log('unimplemented token error: ' + err); - } - }); - } - this.state = { - mediacentername: 'OpenMediaCenter', - password: pwdneeded + mediacentername: 'OpenMediaCenter' }; // force an update on theme change GlobalInfos.onThemeChange(() => { this.forceUpdate(); }); - - // set the hook to load passwordfield on global func call - GlobalInfos.loadPasswordPage = (callback?: () => void): void => { - // try refreshing the token - token.refreshAPIToken((err) => { - if (err !== '') { - this.setState({password: true}); - } else { - // call callback if request was successful - if (callback) { - callback(); - } - } - }, true); - }; - } - - initialAPICall(): void { - // this is the first api call so if it fails we know there is no connection to backend - callAPI(APINode.Settings, {action: 'loadInitialData'}, (result: SettingsTypes.initialApiCallData) => { - // set theme - GlobalInfos.enableDarkTheme(result.DarkMode); - - GlobalInfos.setVideoPaths(result.VideoPath, result.TVShowPath); - - GlobalInfos.setTVShowsEnabled(result.TVShowEnabled); - GlobalInfos.setFullDeleteEnabled(result.FullDeleteEnabled); - - this.setState({ - mediacentername: result.MediacenterName - }); - // set tab title to received mediacenter name - document.title = result.MediacenterName; - }); - } - - componentDidMount(): void { - this.initialAPICall(); } render(): JSX.Element { // add the main theme to the page body document.body.className = GlobalInfos.getThemeStyle().backgroundcolor; - if (this.state.password === true) { - // render authentication page if auth is neccessary - return ( - { - this.setState({password: false}); - // reinit general infos - this.initialAPICall(); - }} - /> - ); - } else if (this.state.password === false) { - return ( - -
+ return ( + + + + { + // this.setState({password: false}); + // reinit general infos + // this.initialAPICall(); + }} + /> + + {this.navBar()} - {this.routing()} -
-
- ); - } else { - return <>still loading...; - } + + + + + ); + + // 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 ( + // + //
+ // {this.navBar()} + // {this.routing()} + //
+ //
+ // ); + // } else { + // return <>still loading...; + // } } /** @@ -139,73 +98,84 @@ class App extends React.Component<{}, state> { return (
{this.state.mediacentername}
- + Home - + Random Video - + Categories {GlobalInfos.isTVShowEnabled() ? ( - + 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(); + + return ( + + + + + + + + + + + + + + + + + + + + + {GlobalInfos.isTVShowEnabled() ? ( + + + + ) : null} + + {GlobalInfos.isTVShowEnabled() ? ( + + + + ) : null} + + + + + + ); +}; + export default App; 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..04468c1 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,13 +1,16 @@ import React from 'react'; import ReactDOM from 'react-dom'; import App from './App'; +import {BrowserRouter} from 'react-router-dom'; // don't allow console logs within production env global.console.log = process.env.NODE_ENV !== 'development' ? (_: string | number | boolean): void => {} : global.console.log; ReactDOM.render( - + + + , document.getElementById('root') ); diff --git a/src/pages/AuthenticationPage/AuthenticationPage.tsx b/src/pages/AuthenticationPage/AuthenticationPage.tsx index 5a3a771..2b53d16 100644 --- a/src/pages/AuthenticationPage/AuthenticationPage.tsx +++ b/src/pages/AuthenticationPage/AuthenticationPage.tsx @@ -2,9 +2,11 @@ import React from 'react'; import {Button} from '../../elements/GPElements/Button'; import style from './AuthenticationPage.module.css'; import {addKeyHandler, removeKeyHandler} from '../../utils/ShortkeyHandler'; -import {token} from '../../utils/TokenHandler'; import {faTimes} from '@fortawesome/free-solid-svg-icons'; import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; +import {LoginContext, LoginState} from '../../utils/context/LoginContext'; +import {APINode, callApiUnsafe} from '../../utils/Api'; +import {cookie, Token} from '../../utils/context/Cookie'; interface state { pwdText: string; @@ -36,6 +38,8 @@ class AuthenticationPage extends React.Component { removeKeyHandler(this.keypress); } + static contextType = LoginContext; + render(): JSX.Element { return ( <> @@ -76,21 +80,17 @@ 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 + (e) => { + console.log(e); + } ); } diff --git a/src/pages/CategoryPage/CategoryPage.tsx b/src/pages/CategoryPage/CategoryPage.tsx index 2bed929..b019059 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,21 @@ import TagView from './TagView'; * Component for Category Page * Contains a Tag Overview and loads specific Tag videos in VideoContainer */ -class CategoryPage extends React.Component { - render(): JSX.Element { - return ( - - - - - - - - - ); - } -} +const CategoryPage = (): JSX.Element => { + const match = useRouteMatch(); + + console.log(match.url); + + 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/SettingsPage/SettingsPage.tsx b/src/pages/SettingsPage/SettingsPage.tsx index dac2d8b..4d7efd0 100644 --- a/src/pages/SettingsPage/SettingsPage.tsx +++ b/src/pages/SettingsPage/SettingsPage.tsx @@ -3,52 +3,52 @@ import MovieSettings from './MovieSettings'; import GeneralSettings from './GeneralSettings'; import style from './SettingsPage.module.css'; import GlobalInfos from '../../utils/GlobalInfos'; -import {NavLink, Redirect, Route, Switch} from 'react-router-dom'; +import {NavLink, Redirect, Route, Switch, useRouteMatch} from 'react-router-dom'; /** * The Settingspage handles all kinds of settings for the mediacenter * and is basically a wrapper for child-tabs */ -class SettingsPage extends React.Component { - render(): JSX.Element { - const themestyle = GlobalInfos.getThemeStyle(); - return ( -
-
-
Settings
- -
General
+const SettingsPage = (): JSX.Element => { + const themestyle = GlobalInfos.getThemeStyle(); + const match = useRouteMatch(); + + return ( +
+
+
Settings
+ +
General
+
+ +
Movies
+
+ {GlobalInfos.isTVShowEnabled() ? ( + +
TV Shows
- -
Movies
-
- {GlobalInfos.isTVShowEnabled() ? ( - -
TV Shows
-
- ) : null} -
-
- - - - - - - - {GlobalInfos.isTVShowEnabled() ? ( - - - - ) : null} - - - - -
+ ) : null}
- ); - } -} +
+ + + + + + + + {GlobalInfos.isTVShowEnabled() ? ( + + + + ) : null} + + + + +
+
+ ); +}; export default SettingsPage; diff --git a/src/utils/Api.ts b/src/utils/Api.ts index 55ba285..cf3a0a6 100644 --- a/src/utils/Api.ts +++ b/src/utils/Api.ts @@ -1,5 +1,5 @@ import GlobalInfos from './GlobalInfos'; -import {token} from './TokenHandler'; +import {cookie} from './context/Cookie'; const APIPREFIX: string = '/api/'; @@ -25,9 +25,7 @@ export function callAPI( 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 +41,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 +51,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 +60,16 @@ function generalAPICall( callback: (_: T) => void, errorcallback: (_: string) => void = (_: string): void => {}, unsafe: boolean, - json: boolean, - mytoken: string + json: boolean ): void { (async function (): Promise { + const tkn = cookie.Load(); const response = await fetch(APIPREFIX + apinode + '/' + fd.action, { method: 'POST', body: JSON.stringify(fd), headers: new Headers({ 'Content-Type': json ? 'application/json' : 'text/plain', - ...(!unsafe && {Authorization: 'Bearer ' + mytoken}) + ...(!unsafe && tkn !== null && {Token: tkn.Token}) }) }); @@ -110,6 +106,7 @@ function generalAPICall( // eslint-disable-next-line no-shadow export enum APINode { + Login = 'login', Settings = 'settings', Tags = 'tags', Actor = 'actor', 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/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..144441b --- /dev/null +++ b/src/utils/context/LoginContextProvider.tsx @@ -0,0 +1,105 @@ +import {LoginContext, LoginPerm, LoginState} from './LoginContext'; +import React, {FunctionComponent, useContext, useEffect, useState} from 'react'; +import {useHistory, useLocation} from 'react-router'; +import {cookie} from './Cookie'; +import {APINode, callAPI} from '../Api'; +import {SettingsTypes} from '../../types/ApiTypes'; +import GlobalInfos from '../GlobalInfos'; + +export const LoginContextProvider: FunctionComponent = (props): JSX.Element => { + let initialLoginState = LoginState.LoggedIn; + let initialUserPerm = LoginPerm.User; + + const t = cookie.Load(); + // we are already logged in so we can set the token and redirect to dashboard + if (t !== null) { + initialLoginState = LoginState.LoggedIn; + } + + const initialAPICall = (): void => { + // this is the first api call so if it fails we know there is no connection to backend + callAPI( + APINode.Settings, + {action: 'loadInitialData'}, + (result: SettingsTypes.initialApiCallData) => { + // set theme + GlobalInfos.enableDarkTheme(result.DarkMode); + + GlobalInfos.setVideoPaths(result.VideoPath, result.TVShowPath); + + GlobalInfos.setTVShowsEnabled(result.TVShowEnabled); + GlobalInfos.setFullDeleteEnabled(result.FullDeleteEnabled); + // + // this.setState({ + // mediacentername: result.MediacenterName + // }); + // set tab title to received mediacenter name + document.title = result.MediacenterName; + + setLoginState(LoginState.LoggedIn); + }, + (_) => { + setLoginState(LoginState.LoggedOut); + } + ); + }; + + useEffect(() => { + initialAPICall(); + }, []); + + const [loginState, setLoginState] = useState(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 <>; + } +}; From e71f262b792a60045166098bc3e9dcc355f745c2 Mon Sep 17 00:00:00 2001 From: lukas Date: Mon, 20 Sep 2021 12:20:22 +0200 Subject: [PATCH 3/7] new features context to render features correctly on change --- src/App.tsx | 46 ++++--------------- src/index.tsx | 5 +- .../AuthenticationPage/AuthenticationPage.tsx | 4 +- src/pages/Player/Player.tsx | 5 +- src/pages/SettingsPage/SettingsPage.tsx | 8 ++-- src/pages/TVShowPage/TVShowPage.tsx | 2 +- src/utils/Api.ts | 9 +--- src/utils/GlobalInfos.ts | 22 +-------- src/utils/context/FeatureContext.tsx | 32 +++++++++++++ src/utils/context/LoginContextProvider.tsx | 17 ++++--- 10 files changed, 67 insertions(+), 83 deletions(-) create mode 100644 src/utils/context/FeatureContext.tsx diff --git a/src/App.tsx b/src/App.tsx index af62270..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'; @@ -18,6 +18,7 @@ import AuthenticationPage from './pages/AuthenticationPage/AuthenticationPage'; import TVShowPage from './pages/TVShowPage/TVShowPage'; import TVPlayer from './pages/TVShowPage/TVPlayer'; import {LoginContextProvider} from './utils/context/LoginContextProvider'; +import {FeatureContext} from './utils/context/FeatureContext'; interface state { mediacentername: string; @@ -48,13 +49,7 @@ class App extends React.Component<{}, state> { - { - // this.setState({password: false}); - // reinit general infos - // this.initialAPICall(); - }} - /> + {this.navBar()} @@ -63,32 +58,10 @@ class App extends React.Component<{}, state> { ); - - // 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 ( - // - //
- // {this.navBar()} - // {this.routing()} - //
- //
- // ); - // } else { - // return <>still loading...; - // } } + static contextType = FeatureContext; + /** * render the top navigation bar */ @@ -115,7 +88,7 @@ class App extends React.Component<{}, state> { Categories - {GlobalInfos.isTVShowEnabled() ? ( + {this.context.TVShowEnabled ? ( { const MyRouter = (): JSX.Element => { const match = useRouteMatch(); + const features = useContext(FeatureContext); return ( @@ -159,13 +133,13 @@ const MyRouter = (): JSX.Element => { - {GlobalInfos.isTVShowEnabled() ? ( - + {features.TVShowEnabled ? ( + ) : null} - {GlobalInfos.isTVShowEnabled() ? ( + {features.TVShowEnabled ? ( diff --git a/src/index.tsx b/src/index.tsx index 04468c1..985ebbf 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -2,6 +2,7 @@ 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; @@ -9,7 +10,9 @@ global.console.log = process.env.NODE_ENV !== 'development' ? (_: string | numbe ReactDOM.render( - + + + , document.getElementById('root') diff --git a/src/pages/AuthenticationPage/AuthenticationPage.tsx b/src/pages/AuthenticationPage/AuthenticationPage.tsx index 2b53d16..8227eaa 100644 --- a/src/pages/AuthenticationPage/AuthenticationPage.tsx +++ b/src/pages/AuthenticationPage/AuthenticationPage.tsx @@ -13,9 +13,7 @@ interface state { wrongPWDInfo: boolean; } -interface Props { - onSuccessLogin: () => void; -} +interface Props {} class AuthenticationPage extends React.Component { constructor(props: Props) { 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.tsx b/src/pages/SettingsPage/SettingsPage.tsx index 4d7efd0..e6aa9d2 100644 --- a/src/pages/SettingsPage/SettingsPage.tsx +++ b/src/pages/SettingsPage/SettingsPage.tsx @@ -1,9 +1,10 @@ -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, useRouteMatch} from 'react-router-dom'; +import {FeatureContext} from '../../utils/context/FeatureContext'; /** * The Settingspage handles all kinds of settings for the mediacenter @@ -12,6 +13,7 @@ import {NavLink, Redirect, Route, Switch, useRouteMatch} from 'react-router-dom' const SettingsPage = (): JSX.Element => { const themestyle = GlobalInfos.getThemeStyle(); const match = useRouteMatch(); + const features = useContext(FeatureContext); return (
@@ -23,7 +25,7 @@ const SettingsPage = (): JSX.Element => {
Movies
- {GlobalInfos.isTVShowEnabled() ? ( + {features.TVShowEnabled ? (
TV Shows
@@ -37,7 +39,7 @@ const SettingsPage = (): JSX.Element => { - {GlobalInfos.isTVShowEnabled() ? ( + {features.TVShowEnabled ? ( 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/utils/Api.ts b/src/utils/Api.ts index cf3a0a6..1f30e16 100644 --- a/src/utils/Api.ts +++ b/src/utils/Api.ts @@ -1,4 +1,3 @@ -import GlobalInfos from './GlobalInfos'; import {cookie} from './context/Cookie'; const APIPREFIX: string = '/api/'; @@ -84,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) { 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/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/LoginContextProvider.tsx b/src/utils/context/LoginContextProvider.tsx index 144441b..1c5d418 100644 --- a/src/utils/context/LoginContextProvider.tsx +++ b/src/utils/context/LoginContextProvider.tsx @@ -5,18 +5,21 @@ 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; } - const initialAPICall = (): void => { + useEffect(() => { // this is the first api call so if it fails we know there is no connection to backend callAPI( APINode.Settings, @@ -27,9 +30,9 @@ export const LoginContextProvider: FunctionComponent = (props): JSX.Element => { GlobalInfos.setVideoPaths(result.VideoPath, result.TVShowPath); - GlobalInfos.setTVShowsEnabled(result.TVShowEnabled); - GlobalInfos.setFullDeleteEnabled(result.FullDeleteEnabled); - // + features.setTVShowEnabled(result.TVShowEnabled); + features.setVideosFullyDeleteable(result.FullDeleteEnabled); + // this.setState({ // mediacentername: result.MediacenterName // }); @@ -42,11 +45,7 @@ export const LoginContextProvider: FunctionComponent = (props): JSX.Element => { setLoginState(LoginState.LoggedOut); } ); - }; - - useEffect(() => { - initialAPICall(); - }, []); + }, [features]); const [loginState, setLoginState] = useState(initialLoginState); const [permission, setPermission] = useState(initialUserPerm); From ab0eab5085582e06dfe9b28cb33bd349cea0fe08 Mon Sep 17 00:00:00 2001 From: lukas Date: Mon, 20 Sep 2021 12:33:43 +0200 Subject: [PATCH 4/7] fix redirect path remove dead code --- apiGo/api/api/ApiBase.go | 75 ---------------------------- src/elements/ActorTile/ActorTile.tsx | 2 +- 2 files changed, 1 insertion(+), 76 deletions(-) diff --git a/apiGo/api/api/ApiBase.go b/apiGo/api/api/ApiBase.go index 9bb9ee5..8297d1b 100644 --- a/apiGo/api/api/ApiBase.go +++ b/apiGo/api/api/ApiBase.go @@ -15,22 +15,6 @@ const ( LoginNode = "login" ) -//type HandlerInfo struct { -// ID string -// Token string -// Data map[string]interface{} -//} - -//type actionStruct struct { -// Action string -//} - -//type Handler struct { -// action string -// handler api.PermUser, func(context api.Context) -// apiNode int -//} - 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() @@ -75,62 +59,3 @@ func ServerInit() { // initialize auth service and add corresponding auth routes InitOAuth() } - -//func handleAPICall(action string, requestBody string, apiNode int, context api.Context) { -// 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/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(() => {})}; } } From 70413ac8872969af6939065504c1986a91307dd9 Mon Sep 17 00:00:00 2001 From: lukas Date: Mon, 20 Sep 2021 18:04:48 +0200 Subject: [PATCH 5/7] fix tests and delete some useless tests --- apiGo/api/api/ApiBase_test.go | 72 ------------------- apiGo/config/Config_test.go | 9 +++ src/App.test.js | 23 +----- .../AuthenticationPage.test.js | 30 ++------ .../AuthenticationPage/AuthenticationPage.tsx | 5 +- src/pages/CategoryPage/CategoryPage.test.js | 10 --- src/pages/CategoryPage/CategoryPage.tsx | 2 - src/pages/Player/Player.test.js | 19 ++--- src/pages/SettingsPage/SettingsPage.test.js | 10 --- src/setupTests.js | 3 - 10 files changed, 23 insertions(+), 160 deletions(-) delete mode 100644 apiGo/api/api/ApiBase_test.go create mode 100644 apiGo/config/Config_test.go delete mode 100644 src/pages/CategoryPage/CategoryPage.test.js delete mode 100644 src/pages/SettingsPage/SettingsPage.test.js diff --git a/apiGo/api/api/ApiBase_test.go b/apiGo/api/api/ApiBase_test.go deleted file mode 100644 index 1d86191..0000000 --- a/apiGo/api/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, api.PermUser, func(context api.Context) { - 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, api.PermUser, func(context api.Context) { - 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, api.PermUser, func(context api.Context) { - 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/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/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/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 8227eaa..e2ff328 100644 --- a/src/pages/AuthenticationPage/AuthenticationPage.tsx +++ b/src/pages/AuthenticationPage/AuthenticationPage.tsx @@ -86,8 +86,9 @@ class AuthenticationPage extends React.Component { this.context.setLoginState(LoginState.LoggedIn); }, - (e) => { - console.log(e); + () => { + 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 b019059..3249e42 100644 --- a/src/pages/CategoryPage/CategoryPage.tsx +++ b/src/pages/CategoryPage/CategoryPage.tsx @@ -10,8 +10,6 @@ import TagView from './TagView'; const CategoryPage = (): JSX.Element => { const match = useRouteMatch(); - console.log(match.url); - return ( 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/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/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 }); From b10fbd6142571ee76e786efcfb7f25f8def9bea6 Mon Sep 17 00:00:00 2001 From: lukas Date: Mon, 20 Sep 2021 19:06:50 +0200 Subject: [PATCH 6/7] add some backend unit tests --- .gitlab-ci.yml | 2 +- apiGo/api/api/Context.go | 2 +- apiGo/api/api/Hash_test.go | 11 +++++++++++ apiGo/api/api/Helpers_test.go | 22 ++++++++++++++++++++++ 4 files changed, 35 insertions(+), 2 deletions(-) create mode 100644 apiGo/api/api/Hash_test.go create mode 100644 apiGo/api/api/Helpers_test.go 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/Context.go b/apiGo/api/api/Context.go index d2bfb2d..edde0ef 100644 --- a/apiGo/api/api/Context.go +++ b/apiGo/api/api/Context.go @@ -56,5 +56,5 @@ func (r *apicontext) Error(msg string) { } func (r *apicontext) Errorf(msg string, args ...interface{}) { - r.Error(fmt.Sprintf(msg, args)) + r.Error(fmt.Sprintf(msg, &args)) } 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_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) + } +} From 334c54be4af90d582d7f44a4ac8f906d515088c4 Mon Sep 17 00:00:00 2001 From: lukas Date: Tue, 21 Sep 2021 10:45:52 +0200 Subject: [PATCH 7/7] fix linter warnings one method to add handlers --- .eslintrc.js | 3 ++- apiGo/api/API.go | 9 +++++++++ apiGo/api/Actors.go | 2 +- apiGo/api/Settings.go | 2 +- apiGo/api/TVShows.go | 2 +- apiGo/api/Tags.go | 2 +- apiGo/api/Video.go | 2 +- apiGo/api/api/ApiBase.go | 5 ++++- apiGo/main.go | 28 +++++++++++++++++----------- src/pages/HomePage/HomePage.tsx | 1 - src/utils/Api.ts | 1 - 11 files changed, 37 insertions(+), 20 deletions(-) create mode 100644 apiGo/api/API.go 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/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 df17514..20bbc8b 100644 --- a/apiGo/api/Actors.go +++ b/apiGo/api/Actors.go @@ -7,7 +7,7 @@ import ( "openmediacenter/apiGo/database" ) -func AddActorsHandlers() { +func addActorsHandlers() { saveActorsToDB() getActorsFromDB() } diff --git a/apiGo/api/Settings.go b/apiGo/api/Settings.go index 1775ed0..fb1f1ce 100644 --- a/apiGo/api/Settings.go +++ b/apiGo/api/Settings.go @@ -11,7 +11,7 @@ import ( "strings" ) -func AddSettingsHandlers() { +func addSettingsHandlers() { saveSettingsToDB() getSettingsFromDB() reIndexHandling() diff --git a/apiGo/api/TVShows.go b/apiGo/api/TVShows.go index 0650abc..dcf0856 100644 --- a/apiGo/api/TVShows.go +++ b/apiGo/api/TVShows.go @@ -7,7 +7,7 @@ import ( "openmediacenter/apiGo/database" ) -func AddTvshowHandlers() { +func addTvshowHandlers() { // do not add handlers if tvshows not enabled if config.GetConfig().Features.DisableTVSupport { return diff --git a/apiGo/api/Tags.go b/apiGo/api/Tags.go index 357ee25..5c3bfb1 100644 --- a/apiGo/api/Tags.go +++ b/apiGo/api/Tags.go @@ -7,7 +7,7 @@ import ( "regexp" ) -func AddTagHandlers() { +func addTagHandlers() { getFromDB() addToDB() deleteFromDB() diff --git a/apiGo/api/Video.go b/apiGo/api/Video.go index aa31057..09886cb 100644 --- a/apiGo/api/Video.go +++ b/apiGo/api/Video.go @@ -11,7 +11,7 @@ import ( "strconv" ) -func AddVideoHandlers() { +func addVideoHandlers() { getVideoHandlers() loadVideosHandlers() addToVideoHandlers() diff --git a/apiGo/api/api/ApiBase.go b/apiGo/api/api/ApiBase.go index 8297d1b..375a4f0 100644 --- a/apiGo/api/api/ApiBase.go +++ b/apiGo/api/api/ApiBase.go @@ -55,7 +55,10 @@ func callHandler(ctx *apicontext, handler func(ctx Context), writer http.Respons } } -func ServerInit() { +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/main.go b/apiGo/main.go index d307415..b2a547e 100644 --- a/apiGo/main.go +++ b/apiGo/main.go @@ -2,19 +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() @@ -25,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() - api2.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/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/utils/Api.ts b/src/utils/Api.ts index 1f30e16..e0d344d 100644 --- a/src/utils/Api.ts +++ b/src/utils/Api.ts @@ -97,7 +97,6 @@ function generalAPICall( * API nodes definitions */ -// eslint-disable-next-line no-shadow export enum APINode { Login = 'login', Settings = 'settings',