package routes import ( "aunefyren/wrapperr/files" "aunefyren/wrapperr/models" "aunefyren/wrapperr/modules" "aunefyren/wrapperr/utilities" "errors" "log" "net/http" "strings" "time" "github.com/gin-gonic/gin" ) // API route which retrieves the Wrapperr version and some minor details (application name, Plex-Auth...). func ApiGetWrapperrVersion(context *gin.Context) { configBool, err := files.GetConfigState() if err != nil { log.Println("Failed to retrieve configuration state. Error: " + err.Error()) context.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to retrieve configuration state."}) context.Abort() return } else if !configBool { context.JSON(http.StatusBadRequest, gin.H{"error": "Wrapperr is not configured."}) context.Abort() return } config, err := files.GetConfig() if err != nil { log.Println("Failed to load Wrapperr configuration. Error: " + err.Error()) context.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to load Wrapperr configuration."}) context.Abort() return } versionReply := models.WrapperrVersion{ WrapperrVersion: config.WrapperrVersion, ApplicationName: config.ApplicationName, PlexAuth: config.PlexAuth, WrapperrFrontPageTitle: config.WrapperrCustomize.WrapperrFrontPageTitle, WrapperrFrontPageSubtitle: config.WrapperrCustomize.WrapperrFrontPageSubtitle, ClientKey: config.ClientKey, WrapperrConfigured: configBool, WinterTheme: config.WinterTheme, Message: "Retrieved Wrapperr version.", Error: false, WrapperrRoot: config.WrapperrRoot, BasicAuth: config.BasicAuth, } ipString := utilities.GetOriginIPString(context) log.Println("Retrieved Wrapperr version." + ipString) context.JSON(http.StatusOK, versionReply) return } // API route which returns if whether or not a Wrapperr admin is configured. func ApiGetAdminState(context *gin.Context) { admin, err := files.GetAdminState() if err != nil { log.Println("Failed to load admin state. Error: " + err.Error()) context.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to load admin state."}) context.Abort() return } booleanReply := models.BooleanReply{ Message: "Retrieved Wrapperr version.", Error: false, Data: admin, } ipString := utilities.GetOriginIPString(context) log.Println("Retrieved Wrapperr admin state." + ipString) context.JSON(http.StatusOK, booleanReply) return } // API route which retrieves the Wrapperr settings needed for the front-end. func ApiGetFunctions(context *gin.Context) { config, err := files.GetConfig() if err != nil { log.Println("Failed to load Wrapperr configuration. Error: " + err.Error()) context.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to load Wrapperr configuration."}) context.Abort() return } functionReply := models.WrapperrFunctions{ WrapperrVersion: config.WrapperrVersion, PlexAuth: config.PlexAuth, WrapperrCustomize: config.WrapperrCustomize, CreateShareLinks: config.CreateShareLinks, } ipString := utilities.GetOriginIPString(context) log.Println("Retrieved Wrapperr functions." + ipString) context.JSON(http.StatusOK, functionReply) return } // API route used to create the admin account and claim the Wrapperr server func ApiCreateAdmin(context *gin.Context) { admin, err := files.GetAdminState() if err != nil { log.Println("Failed to load admin state. Error: " + err.Error()) context.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to load admin state."}) context.Abort() return } if admin { log.Println("Admin creation failed. Admin already configured.") context.JSON(http.StatusBadRequest, gin.H{"error": "Admin already configured."}) context.Abort() return } else { // Read payload from Post input var admin_payload models.AdminConfig if err := context.ShouldBindJSON(&admin_payload); err != nil { log.Println("Failed to parse request. Error: " + err.Error()) context.JSON(http.StatusBadRequest, gin.H{"error": "Failed to parse request."}) context.Abort() return } // Confirm username length if len(admin_payload.AdminUsername) < 4 { context.JSON(http.StatusBadRequest, gin.H{"error": "Admin username is too short. Four characters or more required."}) context.Abort() return } // Confirm password length if len(admin_payload.AdminPassword) < 8 { context.JSON(http.StatusBadRequest, gin.H{"error": "Admin password is too short. Eight characters or more required."}) context.Abort() return } // Hash new password hash, err := utilities.HashAndSalt(admin_payload.AdminPassword) if err != nil { errors.New("Admin creation failed. Could not hash new password. Error: " + err.Error()) context.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to hash your password."}) context.Abort() return } admin_payload.AdminPassword = hash // Save new admin config err = files.SaveAdminConfig(admin_payload) if err != nil { errors.New("Admin creation failed. Could not save configuration. Error: " + err.Error()) context.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save new admin."}) context.Abort() return } log.Println("New admin account created. Server is now claimed.") context.JSON(http.StatusCreated, gin.H{"message": "Admin created."}) return } } // API route which returns if whether or not Wrapperr is configured. func ApiWrapperrConfigured(context *gin.Context) { configBool, err := files.GetConfigState() if err != nil { log.Println("Failed to retrieve configuration state. Error: " + err.Error()) context.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to retrieve configuration state."}) context.Abort() return } else { booleanReply := models.BooleanReply{ Message: "Retrieved Wrapperr configuration state.", Error: false, Data: configBool, } ipString := utilities.GetOriginIPString(context) log.Println("Retrieved Wrapperr configuration state." + ipString) context.JSON(http.StatusOK, booleanReply) return } } // API route which trades admin login credentials for an admin JWT session token. Valid for three days. func ApiLogInAdmin(context *gin.Context) { admin, err := files.GetAdminState() if err != nil { log.Println("Failed to load admin state. Error: " + err.Error()) context.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to load admin state."}) context.Abort() return } config, err := files.GetConfig() if err != nil { log.Println("Failed to load Wrapperr configuration. Error: " + err.Error()) context.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to load Wrapperr configuration."}) context.Abort() return } if !admin { log.Println("Admin login failed. Admin is not configured.") context.JSON(http.StatusBadRequest, gin.H{"error": "Admin login failed. Admin is not configured."}) context.Abort() return } else { adminConfig, err := files.GetAdminConfig() if err != nil { log.Println("Failed to load Wrapperr admin configuration. Error: " + err.Error()) context.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to load Wrapperr admin configuration."}) context.Abort() return } var username string var password string if !config.BasicAuth { // Read payload from Post input var admin_payload models.AdminConfig if err := context.ShouldBindJSON(&admin_payload); err != nil { log.Println("Failed to parse request. Error: " + err.Error()) context.JSON(http.StatusBadRequest, gin.H{"error": "Failed to parse request."}) context.Abort() return } username = admin_payload.AdminUsername password = admin_payload.AdminPassword // Hash new password passwordValidity := utilities.ComparePasswords(adminConfig.AdminPassword, password) // Validate admin username and password if !passwordValidity || adminConfig.AdminUsername != username { ipString := utilities.GetOriginIPString(context) log.Println("Admin login failed. Incorrect admin username or password." + ipString) context.JSON(http.StatusUnauthorized, gin.H{"error": "Login failed. Username or password is incorrect."}) context.Abort() return } } else { err = utilities.ValidateBasicAuth(context, adminConfig) if err != nil { log.Println("Validate Basic auth. Error: " + err.Error()) context.JSON(http.StatusBadRequest, gin.H{"error": "Failed to validate HTTP Basic. You might need to restart your browser."}) context.Abort() return } } token, err := modules.CreateToken(adminConfig.AdminUsername, true, "") if err != nil { log.Println("Failed to create JWT token. Error: " + err.Error()) context.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create JWT token."}) context.Abort() return } stringReply := models.StringReply{ Message: "Login cookie created", Error: false, Data: token, } ipString := utilities.GetOriginIPString(context) log.Println("Created and retrieved admin login JWT Token." + ipString) context.JSON(http.StatusOK, stringReply) return } } // APi route which trades admin login credentials for an admin JWT session token. Valid for three days. func ApiGetTautulliConncection(context *gin.Context) { // Read payload from Post input var tautulli_connection models.TautulliConfig if err := context.ShouldBindJSON(&tautulli_connection); err != nil { log.Println("Failed to parse request. Error: " + err.Error()) context.JSON(http.StatusBadRequest, gin.H{"error": "Failed to parse request."}) context.Abort() return } if tautulli_connection.TautulliApiKey == "" || tautulli_connection.TautulliIP == "" || tautulli_connection.TautulliPort == 0 { log.Println("Cannot test Tautulli connection. Invalid Tautulli connection details received.") context.JSON(http.StatusBadRequest, gin.H{"error": "Tautulli connection details specified are invalid."}) context.Abort() return } tautulli_state, err := modules.TautulliTestConnection(tautulli_connection.TautulliPort, tautulli_connection.TautulliIP, tautulli_connection.TautulliHttps, tautulli_connection.TautulliRoot, tautulli_connection.TautulliApiKey) if err != nil { context.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) context.Abort() return } booleanReply := models.BooleanReply{ Message: "Tested Tautulli connection.", Error: false, Data: tautulli_state, } ipString := utilities.GetOriginIPString(context) log.Println("Tested Tautulli connection." + ipString) context.JSON(http.StatusOK, booleanReply) return } // Get shareable link func ApiGetShareLink(context *gin.Context) { config_bool, err := files.GetConfigState() if err != nil { log.Println("Failed to retrieve configuration state. Error: " + err.Error()) context.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to retrieve configuration state."}) context.Abort() return } else if !config_bool { context.JSON(http.StatusBadRequest, gin.H{"error": "Wrapperr is not configured."}) context.Abort() return } config, err := files.GetConfig() if err != nil { log.Println("Failed to load Wrapperr configuration. Error: " + err.Error()) context.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to load Wrapperr configuration."}) context.Abort() return } if !config.CreateShareLinks { log.Println("Shareable links are not enabled in the Wrapperr configuration.") context.JSON(http.StatusBadRequest, gin.H{"error": "Shareable links are not enabled in the Wrapperr configuration."}) context.Abort() return } // Read payload from Post input var link_payload models.WrapperrShareLinkGetRequest if err := context.ShouldBindJSON(&link_payload); err != nil { log.Println("Failed to parse request. Error: " + err.Error()) context.JSON(http.StatusBadRequest, gin.H{"error": "Failed to parse request."}) context.Abort() return } var fileName string var hash string hash_array := strings.Split(link_payload.Hash, "-") if len(hash_array) < 2 { log.Println("Failed to split hash string while looking for user ID.") context.JSON(http.StatusInternalServerError, gin.H{"error": "Invalid shared link."}) context.Abort() return } if config.PlexAuth { for j := 1; j < len(hash_array); j++ { if j != 1 { hash = hash + "-" } hash = hash + hash_array[j] } fileName = hash_array[0] } else { for j := 1; j < len(hash_array); j++ { if j != 1 { hash = hash + "-" } hash = hash + hash_array[j] } fileName = hash } share_link_object, err := files.GetLink(fileName) if err != nil { log.Println("Failed to get link file. Error: " + err.Error()) context.JSON(http.StatusInternalServerError, gin.H{"error": "Invalid shared link."}) context.Abort() return } currentTime := time.Now() linkTime, err := time.Parse("2006-01-02", share_link_object.Date) if err != nil { log.Println("Failed to parse link date. Error: " + err.Error()) context.JSON(http.StatusInternalServerError, gin.H{"error": "Invalid shared link."}) context.Abort() return } linkTime = linkTime.Add(7 * 24 * time.Hour) if !linkTime.Before(currentTime) && share_link_object.Hash == hash && !share_link_object.Expired { share_link_object.Message = "Shared link retrieved." share_link_object.Error = false ipString := utilities.GetOriginIPString(context) log.Println("Retrieved Wrapperr share link made by User ID/with hash: " + fileName + "." + ipString) context.JSON(http.StatusCreated, share_link_object) return } else { returnError := errors.New("Invalid shared link.") if linkTime.Before(currentTime) { share_link_object.Expired = true err = files.SaveLink(share_link_object, config.PlexAuth) if err != nil { log.Println("Failed to save share link. Error: " + err.Error()) } returnError = errors.New("This shared link has expired.") } log.Println("Failed to retrieve Wrapperr share link with hash: " + link_payload.Hash + ".") context.JSON(http.StatusBadRequest, returnError) return } } func ApiLoginPlexAuth(context *gin.Context) { config_bool, err := files.GetConfigState() if err != nil { log.Println("Failed to retrieve configuration state. Error: " + err.Error()) context.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to retrieve configuration state."}) context.Abort() return } else if !config_bool { context.JSON(http.StatusBadRequest, gin.H{"error": "Wrapperr is not configured."}) context.Abort() return } config, err := files.GetConfig() if err != nil { log.Println("Failed to load Wrapperr configuration. Error: " + err.Error()) context.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to load Wrapperr configuration."}) context.Abort() return } // Read payload from Post input var payload models.LoginPlexAuth if err := context.ShouldBindJSON(&payload); err != nil { log.Println("Failed to parse request. Error: " + err.Error()) context.JSON(http.StatusBadRequest, gin.H{"error": "Failed to parse request."}) context.Abort() return } // Confirm username length if payload.ID == 0 || payload.Code == "" { log.Println("Cannot retrieve Plex Auth login state. Invalid ID or Code received.") context.JSON(http.StatusUnauthorized, gin.H{"error": "Login ID and/or Code is invalid."}) context.Abort() return } plex_auth, err := modules.GetPlexAuthLogin(payload.ID, payload.Code, config.WrapperrVersion, config.ClientKey) if err != nil { log.Println("Failed to retrieve Plex Auth pin. Error: " + err.Error()) context.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to retrieve Plex Auth pin."}) context.Abort() return } if plex_auth.AuthToken == "" { log.Println("Plex Auth response invalid. No Authtoken received.") context.JSON(http.StatusUnauthorized, gin.H{"error": "Plex Auth response invalid."}) context.Abort() return } token, err := modules.CreateToken("Plex Auth", false, plex_auth.AuthToken) if err != nil { log.Println("Failed to create JWT token. Error: " + err.Error()) context.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create JWT token."}) context.Abort() return } string_reply := models.StringReply{ Message: "Login cookie created", Error: false, Data: token, } ipString := utilities.GetOriginIPString(context) log.Println("Created and retrieved Plex Auth login JWT Token." + ipString) context.JSON(http.StatusCreated, string_reply) return }