Files
Watcharr/server/routes.go
Mr b997b421fa Preload last season viewed (#609)
* Save requested season number in `LastViewedSeason` property

When season details are requested and a watchedId is passed as a query parameter, the requested season number is now saved in the watched entries `LastViewedSeason` property.

* SeasonsList: Restore activeSeason and update state after request with new lastViewedSeason value

* get season details route: add extra log for else case where no watchedId is found
2024-08-29 01:53:05 +00:00

1677 lines
48 KiB
Go

package main
import (
"log/slog"
"net/http"
"strconv"
"time"
"github.com/gin-contrib/cache"
"github.com/gin-contrib/cache/persistence"
"github.com/gin-gonic/gin"
"github.com/sbondCo/Watcharr/arr"
"github.com/sbondCo/Watcharr/game"
"gorm.io/gorm"
)
type ErrorResponse struct {
Error string `json:"error"`
}
type ValueRequest struct {
Value any `json:"value"`
}
type KeyValueRequest struct {
Key string `json:"key"`
Value any `json:"value"`
}
type BaseRouter struct {
db *gorm.DB
rg *gin.RouterGroup
ms *persistence.InMemoryStore
}
func newBaseRouter(db *gorm.DB, rg *gin.RouterGroup) *BaseRouter {
return &BaseRouter{
db: db,
rg: rg,
ms: persistence.NewInMemoryStore(time.Hour * 24),
}
}
// Since we cannot remove these setup routes after they are registered,
// each route/service should ensure we are still in setup before continuing.
// After server restart, these routes shouldn't exist if setup finished
// (currently it is finished if a user is created).
//
// Each controller can check ServerInSetup var first, then each service
// can double check what it needs to (eg create_admin service, registerFirstUser,
// will check that no users exist).
func (b *BaseRouter) addSetupRoutes() {
setup := b.rg.Group("/setup")
setup.POST("/create_admin", func(c *gin.Context) {
if !ServerInSetup {
c.JSON(http.StatusForbidden, ErrorResponse{Error: "not in setup"})
return
}
var user UserRegisterRequest
if c.ShouldBindJSON(&user) == nil {
response, err := registerFirstUser(&user, b.db)
if err != nil {
c.JSON(http.StatusForbidden, ErrorResponse{Error: err.Error()})
return
} else {
// Set in setup to false after first user registered successfully
ServerInSetup = false
}
c.JSON(http.StatusOK, response)
return
}
c.Status(400)
})
}
func (b *BaseRouter) addContentRoutes() {
content := b.rg.Group("/content").Use(AuthRequired(nil))
exp := time.Hour * 24
// Search for content
content.GET("/search/multi/:query", cache.CachePage(b.ms, exp, func(c *gin.Context) {
if c.Param("query") == "" {
c.JSON(http.StatusBadRequest, ErrorResponse{Error: "a query was not provided"})
return
}
pageQ := c.Query("page")
pageNum := 1
if pageQ != "" {
num, err := strconv.Atoi(pageQ)
if err != nil {
c.JSON(http.StatusBadRequest, ErrorResponse{Error: "query parameter 'page' is not a number"})
return
}
pageNum = num
}
content, err := searchContent(c.Param("query"), pageNum)
if err != nil {
c.JSON(http.StatusBadRequest, ErrorResponse{Error: err.Error()})
return
}
c.JSON(http.StatusOK, content)
}))
// Search for movies
content.GET("/search/movie/:query", cache.CachePage(b.ms, exp, func(c *gin.Context) {
if c.Param("query") == "" {
c.JSON(http.StatusBadRequest, ErrorResponse{Error: "a query was not provided"})
return
}
pageQ := c.Query("page")
pageNum := 1
if pageQ != "" {
num, err := strconv.Atoi(pageQ)
if err != nil {
c.JSON(http.StatusBadRequest, ErrorResponse{Error: "query parameter 'page' is not a number"})
return
}
pageNum = num
}
content, err := searchMovies(c.Param("query"), pageNum)
if err != nil {
c.JSON(http.StatusBadRequest, ErrorResponse{Error: err.Error()})
return
}
c.JSON(http.StatusOK, content)
}))
// Search for shows
content.GET("/search/tv/:query", cache.CachePage(b.ms, exp, func(c *gin.Context) {
if c.Param("query") == "" {
c.JSON(http.StatusBadRequest, ErrorResponse{Error: "a query was not provided"})
return
}
pageQ := c.Query("page")
pageNum := 1
if pageQ != "" {
num, err := strconv.Atoi(pageQ)
if err != nil {
c.JSON(http.StatusBadRequest, ErrorResponse{Error: "query parameter 'page' is not a number"})
return
}
pageNum = num
}
content, err := searchTv(c.Param("query"), pageNum)
if err != nil {
c.JSON(http.StatusBadRequest, ErrorResponse{Error: err.Error()})
return
}
c.JSON(http.StatusOK, content)
}))
// Search for people
content.GET("/search/person/:query", cache.CachePage(b.ms, exp, func(c *gin.Context) {
if c.Param("query") == "" {
c.JSON(http.StatusBadRequest, ErrorResponse{Error: "a query was not provided"})
return
}
pageQ := c.Query("page")
pageNum := 1
if pageQ != "" {
num, err := strconv.Atoi(pageQ)
if err != nil {
c.JSON(http.StatusBadRequest, ErrorResponse{Error: "query parameter 'page' is not a number"})
return
}
pageNum = num
}
content, err := searchPeople(c.Param("query"), pageNum)
if err != nil {
c.JSON(http.StatusBadRequest, ErrorResponse{Error: err.Error()})
return
}
c.JSON(http.StatusOK, content)
}))
// Get movie details (for movie page)
content.GET("/movie/:id", WhereaboutsRequired(), cache.CachePage(b.ms, exp, func(c *gin.Context) {
if c.Param("id") == "" {
c.Status(400)
return
}
content, err := movieDetails(b.db, c.Param("id"), c.MustGet("userCountry").(string), map[string]string{"append_to_response": "videos,watch/providers,similar"})
if err != nil {
c.JSON(http.StatusBadRequest, ErrorResponse{Error: err.Error()})
return
}
c.JSON(http.StatusOK, content)
}))
// Get movie cast
content.GET("/movie/:id/credits", cache.CachePage(b.ms, exp, func(c *gin.Context) {
if c.Param("id") == "" {
c.Status(400)
return
}
content, err := movieCredits(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, ErrorResponse{Error: err.Error()})
return
}
c.JSON(http.StatusOK, content)
}))
// Get tv details (for tv page)
content.GET("/tv/:id", WhereaboutsRequired(), cache.CachePage(b.ms, exp, func(c *gin.Context) {
if c.Param("id") == "" {
c.Status(400)
return
}
content, err := tvDetails(b.db, c.Param("id"), c.MustGet("userCountry").(string), map[string]string{"append_to_response": "videos,watch/providers,similar,external_ids,keywords"})
if err != nil {
c.JSON(http.StatusBadRequest, ErrorResponse{Error: err.Error()})
return
}
c.JSON(http.StatusOK, content)
}))
// Get tv cast
content.GET("/tv/:id/credits", cache.CachePage(b.ms, exp, func(c *gin.Context) {
if c.Param("id") == "" {
c.Status(400)
return
}
content, err := tvCredits(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, ErrorResponse{Error: err.Error()})
return
}
c.JSON(http.StatusOK, content)
}))
// Get season details
// Supports `watchedId` query parameter for saving the requested season as `LastViewedSeason`.
content.GET("/tv/:id/season/:num", func(c *gin.Context) {
if c.Param("id") == "" || c.Param("num") == "" {
c.Status(400)
return
}
content, err := seasonDetails(c.Param("id"), c.Param("num"))
if err != nil {
c.JSON(http.StatusBadRequest, ErrorResponse{Error: err.Error()})
return
}
// If a `watchedId` is passed, we should update it with this season
// number, so the LastViewedSeason field is up to date (this seemed
// better than making a new request for just saving this).
// We will attach a `watcharr-lastviewedseason-saved` header if
// this part succeeds so the client can decide on showing an error.
if watchedIdQ := c.Query("watchedId"); watchedIdQ != "" {
userId := c.MustGet("userId").(uint)
watchedId, err := strconv.ParseUint(watchedIdQ, 10, 64)
if err != nil {
slog.Error("get season details route: Processing watchedId param failed", "error", err.Error(), "id", watchedIdQ)
} else {
if seasonNum, err := strconv.ParseInt(c.Param("num"), 10, 64); err == nil {
if err = updateWatchedLastViewedSeason(b.db, userId, uint(watchedId), int(seasonNum)); err == nil {
c.Header("watcharr-lastviewedseason-saved", "1")
}
} else {
slog.Error("get season details route: Parsing season number as int failed", "error", err.Error(), "season_num", c.Param("num"))
}
}
} else {
slog.Debug("get season details route: No watchedId parameter found.. not doing anything.")
}
c.JSON(http.StatusOK, content)
})
// Get person details
content.GET("/person/:id", cache.CachePage(b.ms, exp, func(c *gin.Context) {
if c.Param("id") == "" {
c.Status(400)
return
}
content, err := personDetails(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, ErrorResponse{Error: err.Error()})
return
}
c.JSON(http.StatusOK, content)
}))
// Get person credits
content.GET("/person/:id/credits", cache.CachePage(b.ms, exp, func(c *gin.Context) {
if c.Param("id") == "" {
c.Status(400)
return
}
content, err := personCredits(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, ErrorResponse{Error: err.Error()})
return
}
c.JSON(http.StatusOK, content)
}))
// Discover movies
content.GET("/discover/movies", cache.CachePage(b.ms, exp, func(c *gin.Context) {
content, err := discoverMovies()
if err != nil {
c.JSON(http.StatusBadRequest, ErrorResponse{Error: err.Error()})
return
}
c.JSON(http.StatusOK, content)
}))
// Discover shows
content.GET("/discover/tv", cache.CachePage(b.ms, exp, func(c *gin.Context) {
content, err := discoverTv()
if err != nil {
c.JSON(http.StatusBadRequest, ErrorResponse{Error: err.Error()})
return
}
c.JSON(http.StatusOK, content)
}))
// Get all trending (movies, tv, people)
content.GET("/trending", cache.CachePage(b.ms, exp, func(c *gin.Context) {
content, err := allTrending()
if err != nil {
c.JSON(http.StatusBadRequest, ErrorResponse{Error: err.Error()})
return
}
c.JSON(http.StatusOK, content)
}))
// Upcoming Movies
content.GET("/upcoming/movies", cache.CachePage(b.ms, exp, func(c *gin.Context) {
content, err := upcomingMovies()
if err != nil {
c.JSON(http.StatusBadRequest, ErrorResponse{Error: err.Error()})
return
}
c.JSON(http.StatusOK, content)
}))
// Upcoming Tv
content.GET("/upcoming/tv", cache.CachePage(b.ms, exp, func(c *gin.Context) {
content, err := upcomingTv()
if err != nil {
c.JSON(http.StatusBadRequest, ErrorResponse{Error: err.Error()})
return
}
c.JSON(http.StatusOK, content)
}))
// Available regions for watch providers
content.GET("/regions", func(c *gin.Context) {
r, err := regions()
if err != nil {
c.JSON(http.StatusBadRequest, ErrorResponse{Error: err.Error()})
return
}
c.JSON(http.StatusOK, r)
})
}
func (b *BaseRouter) addGameRoutes() {
gamer := b.rg.Group("/game").Use(AuthRequired(nil))
exp := time.Hour * 24
igdb := &Config.TWITCH
igdb.OnTokenRefreshed(func() {
// Save new token to config when we refresh it.
slog.Debug("GameRoutes: token refreshed.. saving to config.")
if err := writeConfig(); err != nil {
slog.Error("GameRoutes: failed to save refreshed token to config.", "error", err)
}
})
err := igdb.Init()
// Save cfg if init succeeded, this will save our access token
if err == nil {
slog.Error("GameRoutes: Twitch init failed!", "error", err)
}
// Search for games
gamer.GET("/search/:query", cache.CachePage(b.ms, exp, func(c *gin.Context) {
if c.Param("query") == "" {
c.Status(400)
return
}
games, err := igdb.Search(c.Param("query"))
if err != nil {
c.JSON(http.StatusBadRequest, ErrorResponse{Error: err.Error()})
return
}
c.JSON(http.StatusOK, games)
}))
// Game details for game page
gamer.GET("/:id", cache.CachePage(b.ms, exp, func(c *gin.Context) {
if c.Param("id") == "" {
c.Status(400)
return
}
content, err := igdb.GameDetails(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, ErrorResponse{Error: err.Error()})
return
}
c.JSON(http.StatusOK, content)
}))
// Add game to played(watched) list
gamer.POST("/played", func(c *gin.Context) {
userId := c.MustGet("userId").(uint)
var ar PlayedAddRequest
err := c.ShouldBindJSON(&ar)
if err == nil {
response, err := addPlayed(b.db, igdb, userId, ar, ADDED_WATCHED)
if err != nil {
c.JSON(http.StatusForbidden, ErrorResponse{Error: err.Error()})
return
}
c.JSON(http.StatusOK, response)
return
}
c.AbortWithStatusJSON(http.StatusBadRequest, ErrorResponse{Error: err.Error()})
})
// IMPORTANT: Routes below only for admins!
gamer.Use(AuthRequired(b.db), AdminRequired())
{
gamer.POST("/config", func(c *gin.Context) {
var ar game.IGDB
err := c.ShouldBindJSON(&ar)
if err == nil {
err := saveTwitchConfig(ar)
if err != nil {
c.JSON(http.StatusForbidden, ErrorResponse{Error: err.Error()})
return
}
igdb = &Config.TWITCH
c.Status(http.StatusOK)
return
}
c.AbortWithStatusJSON(http.StatusBadRequest, ErrorResponse{Error: err.Error()})
})
}
}
func (b *BaseRouter) addWatchedRoutes() {
watched := b.rg.Group("/watched").Use(AuthRequired(nil))
watched.GET("", func(c *gin.Context) {
userId := c.MustGet("userId").(uint)
c.JSON(http.StatusOK, getWatched(b.db, userId))
})
watched.GET(":id/:username", func(c *gin.Context) {
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
if err != nil {
slog.Error("getPublicWatched route failed to convert id param to uint", "id", id)
c.Status(400)
return
}
response, err := getPublicWatched(b.db, uint(id), c.Param("username"))
if err != nil {
c.JSON(http.StatusForbidden, ErrorResponse{Error: err.Error()})
return
}
c.JSON(http.StatusOK, response)
})
watched.POST("", func(c *gin.Context) {
userId := c.MustGet("userId").(uint)
var ar WatchedAddRequest
err := c.ShouldBindJSON(&ar)
if err == nil {
response, err := addWatched(b.db, userId, ar, ADDED_WATCHED)
if err != nil {
c.JSON(http.StatusForbidden, ErrorResponse{Error: err.Error()})
return
}
c.JSON(http.StatusOK, response)
return
}
c.AbortWithStatusJSON(http.StatusBadRequest, ErrorResponse{Error: err.Error()})
})
watched.PUT(":id", func(c *gin.Context) {
id, err := strconv.Atoi(c.Param("id"))
if err != nil {
c.Status(400)
return
}
userId := c.MustGet("userId").(uint)
var ur WatchedUpdateRequest
err = c.ShouldBindJSON(&ur)
if err == nil {
response, err := updateWatched(b.db, userId, uint(id), ur)
if err != nil {
c.JSON(http.StatusForbidden, ErrorResponse{Error: err.Error()})
return
}
c.JSON(http.StatusOK, response)
return
}
c.AbortWithStatusJSON(http.StatusBadRequest, ErrorResponse{Error: err.Error()})
})
watched.DELETE(":id", func(c *gin.Context) {
id, err := strconv.Atoi(c.Param("id"))
if err == nil {
userId := c.MustGet("userId").(uint)
response, err := removeWatched(b.db, userId, uint(id))
if err != nil {
c.JSON(http.StatusForbidden, ErrorResponse{Error: err.Error()})
return
}
c.JSON(http.StatusOK, response)
return
}
c.AbortWithStatusJSON(http.StatusBadRequest, ErrorResponse{Error: err.Error()})
})
watched.POST("/season", func(c *gin.Context) {
userId := c.MustGet("userId").(uint)
var ar WatchedSeasonAddRequest
err := c.ShouldBindJSON(&ar)
if err == nil {
response, err := addWatchedSeason(b.db, userId, ar)
if err != nil {
c.JSON(http.StatusForbidden, ErrorResponse{Error: err.Error()})
return
}
c.JSON(http.StatusOK, response)
return
}
c.AbortWithStatusJSON(http.StatusBadRequest, ErrorResponse{Error: err.Error()})
})
watched.DELETE("/season/:id", func(c *gin.Context) {
id, err := strconv.Atoi(c.Param("id"))
if err != nil {
c.Status(400)
return
}
userId := c.MustGet("userId").(uint)
response, err := rmWatchedSeason(b.db, userId, uint(id))
if err != nil {
c.JSON(http.StatusForbidden, ErrorResponse{Error: err.Error()})
return
}
c.JSON(http.StatusOK, response)
})
watched.POST("/episode", func(c *gin.Context) {
userId := c.MustGet("userId").(uint)
var ar WatchedEpisodeAddRequest
err := c.ShouldBindJSON(&ar)
if err == nil {
response, err := addWatchedEpisodes(b.db, userId, ar)
if err != nil {
c.JSON(http.StatusForbidden, ErrorResponse{Error: err.Error()})
return
}
c.JSON(http.StatusOK, response)
return
}
c.AbortWithStatusJSON(http.StatusBadRequest, ErrorResponse{Error: err.Error()})
})
watched.DELETE("/episode/:id", func(c *gin.Context) {
id, err := strconv.Atoi(c.Param("id"))
if err != nil {
c.Status(400)
return
}
userId := c.MustGet("userId").(uint)
response, err := rmWatchedEpisode(b.db, userId, uint(id))
if err != nil {
c.JSON(http.StatusForbidden, ErrorResponse{Error: err.Error()})
return
}
c.JSON(http.StatusOK, response)
})
watched.POST(":id/tag/:tagId", func(c *gin.Context) {
id, err := strconv.Atoi(c.Param("id"))
if err != nil {
slog.Error("tag watched route failed to convert id param to int", "error", err)
c.Status(http.StatusBadRequest)
return
}
tagId, err := strconv.Atoi(c.Param("tagId"))
if err != nil {
slog.Error("tag watched route failed to convert tagId param to int", "error", err)
c.Status(http.StatusBadRequest)
return
}
userId := c.MustGet("userId").(uint)
err = addWatchedToTag(b.db, userId, uint(tagId), uint(id))
if err != nil {
c.JSON(http.StatusBadRequest, ErrorResponse{Error: err.Error()})
return
}
c.Status(http.StatusOK)
})
watched.DELETE(":id/tag/:tagId", func(c *gin.Context) {
id, err := strconv.Atoi(c.Param("id"))
if err != nil {
slog.Error("tag watched route failed to convert id param to int", "error", err)
c.Status(http.StatusBadRequest)
return
}
tagId, err := strconv.Atoi(c.Param("tagId"))
if err != nil {
slog.Error("tag watched route failed to convert tagId param to int", "error", err)
c.Status(http.StatusBadRequest)
return
}
userId := c.MustGet("userId").(uint)
err = rmWatchedFromTag(b.db, userId, uint(tagId), uint(id))
if err != nil {
c.JSON(http.StatusBadRequest, ErrorResponse{Error: err.Error()})
return
}
c.Status(http.StatusOK)
})
}
func (b *BaseRouter) addActivityRoutes() {
activity := b.rg.Group("/activity").Use(AuthRequired(nil))
activity.GET(":watchedId", func(c *gin.Context) {
watchedId, err := strconv.ParseUint(c.Param("watchedId"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, ErrorResponse{Error: "check watched id route param"})
return
}
userId := c.MustGet("userId").(uint)
activity, err := getActivity(b.db, userId, uint(watchedId))
if err != nil {
c.JSON(http.StatusInternalServerError, ErrorResponse{Error: err.Error()})
return
}
c.JSON(http.StatusOK, activity)
})
activity.POST("", func(c *gin.Context) {
userId := c.MustGet("userId").(uint)
var ar ActivityAddRequest
err := c.ShouldBindJSON(&ar)
if err == nil {
response, err := addActivity(b.db, userId, ar)
if err != nil {
c.JSON(http.StatusForbidden, ErrorResponse{Error: err.Error()})
return
}
c.JSON(http.StatusOK, response)
return
}
c.AbortWithStatusJSON(http.StatusBadRequest, ErrorResponse{Error: err.Error()})
})
activity.PUT(":id", func(c *gin.Context) {
userId := c.MustGet("userId").(uint)
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.Status(400)
return
}
var activityUpdateRequest ActivityUpdateRequest
err = c.ShouldBindJSON(&activityUpdateRequest)
if err == nil {
err = updateActivity(b.db, userId, uint(id), activityUpdateRequest)
if err != nil {
c.JSON(http.StatusForbidden, ErrorResponse{Error: err.Error()})
return
}
c.Status(http.StatusOK)
return
}
c.AbortWithStatusJSON(http.StatusBadRequest, ErrorResponse{Error: err.Error()})
})
activity.DELETE(":id", func(c *gin.Context) {
userId := c.MustGet("userId").(uint)
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.Status(400)
slog.Error("Could not process activity id when attempting a deletion", "error", err.Error(), "id", c.Param("id"))
return
}
err = deleteActivity(b.db, userId, uint(id))
if err != nil {
c.JSON(http.StatusForbidden, ErrorResponse{Error: err.Error()})
return
}
c.Status(http.StatusOK)
})
}
func (b *BaseRouter) addAuthRoutes() {
auth := b.rg.Group("/auth")
// Login
auth.POST("/", func(c *gin.Context) {
var user User
if c.ShouldBindJSON(&user) == nil {
response, err := login(&user, b.db)
if err != nil {
c.JSON(http.StatusForbidden, ErrorResponse{Error: err.Error()})
return
}
c.JSON(http.StatusOK, response)
return
}
c.Status(400)
})
// Jellyfin login
auth.POST("/jellyfin", func(c *gin.Context) {
var user User
if c.ShouldBindJSON(&user) == nil {
response, err := loginJellyfin(&user, b.db)
if err != nil {
c.JSON(http.StatusForbidden, ErrorResponse{Error: err.Error()})
return
}
c.JSON(http.StatusOK, response)
return
}
c.Status(400)
})
// Plex login
auth.POST("/plex", func(c *gin.Context) {
var plexRequest PlexLoginRequest
if c.ShouldBindJSON(&plexRequest) == nil {
response, err := loginPlex(&plexRequest, b.db)
if err != nil {
c.JSON(http.StatusForbidden, ErrorResponse{Error: err.Error()})
return
}
c.JSON(http.StatusOK, response)
return
}
c.Status(400)
})
// Register
auth.POST("/register", func(c *gin.Context) {
var user UserRegisterRequest
if c.ShouldBindJSON(&user) == nil {
response, err := register(&user, PERM_NONE, b.db)
if err != nil {
c.JSON(http.StatusForbidden, ErrorResponse{Error: err.Error()})
return
}
c.JSON(http.StatusOK, response)
return
}
c.Status(400)
})
// Get available auth providers
auth.GET("/available", func(c *gin.Context) {
availableAuthProviders := []string{}
if Config.JELLYFIN_HOST != "" {
availableAuthProviders = append(availableAuthProviders, "jellyfin")
}
if Config.PLEX_HOST != "" && Config.PLEX_MACHINE_ID != "" {
availableAuthProviders = append(availableAuthProviders, "plex")
}
c.JSON(http.StatusOK, &AvailableAuthProvidersResponse{
AvailableAuthProviders: availableAuthProviders,
SignupEnabled: Config.SIGNUP_ENABLED,
IsInSetup: ServerInSetup,
UseEmby: Config.USE_EMBY,
})
})
// IMPORTANT: Routes below here must be authenticated.
auth.Use(AuthRequired(nil))
{
// Request admin token
auth.GET("/admin_token", func(c *gin.Context) {
userId := c.MustGet("userId").(uint)
token, err := createOneUseToken(b.db, TOKENTYPE_ADMIN, userId)
if err != nil {
c.JSON(http.StatusForbidden, ErrorResponse{Error: err.Error()})
return
}
slog.Info("Admin token generated. Type this token into the web ui to gain admin access on your account.", "token", token, "generated_for", userId)
c.Status(http.StatusNoContent)
})
// Use admin token
auth.POST("/admin_token", func(c *gin.Context) {
userId := c.MustGet("userId").(uint)
var atr UseAdminTokenRequest
if c.ShouldBindJSON(&atr) == nil {
err := useAdminToken(&atr, b.db, userId)
if err != nil {
c.JSON(http.StatusForbidden, ErrorResponse{Error: err.Error()})
return
}
c.Status(http.StatusNoContent)
return
}
c.Status(400)
})
// Change password
auth.POST("/change_password", func(c *gin.Context) {
userId := c.MustGet("userId").(uint)
var pwds UserPasswordUpdateRequest
err := c.ShouldBindJSON(&pwds)
if err == nil {
err := userChangePassword(b.db, pwds, userId)
if err != nil {
c.JSON(http.StatusForbidden, ErrorResponse{Error: err.Error()})
return
}
c.Status(http.StatusOK)
return
}
c.AbortWithStatusJSON(http.StatusBadRequest, ErrorResponse{Error: err.Error()})
})
}
}
func (b *BaseRouter) addProfileRoutes() {
profile := b.rg.Group("/profile").Use(AuthRequired(nil))
// Get user profile details
profile.GET("", func(c *gin.Context) {
userId := c.MustGet("userId").(uint)
response, err := getProfile(b.db, userId)
if err != nil {
c.JSON(http.StatusForbidden, ErrorResponse{Error: err.Error()})
return
}
c.JSON(http.StatusOK, response)
})
}
func (b *BaseRouter) addJellyfinRoutes() {
jf := b.rg.Group("/jellyfin").Use(AuthRequired(b.db), JellyfinAccessRequired())
// Check if jf has item
jf.GET("/:type/:name/:tmdbId", func(c *gin.Context) {
userId := c.MustGet("userId").(uint)
userType := c.MustGet("userType").(UserType)
username := c.MustGet("username").(string)
userThirdPartyId := c.MustGet("userThirdPartyId").(string)
userThirdPartyAuth := c.MustGet("userThirdPartyAuth").(string)
response, err := jellyfinContentFind(userId, userType, username, userThirdPartyId, userThirdPartyAuth, c.Param("type"), c.Param("name"), c.Param("tmdbId"))
if err != nil {
c.JSON(http.StatusForbidden, ErrorResponse{Error: err.Error()})
return
}
c.JSON(http.StatusOK, response)
})
// Sync users jellyfin watched items to watchlist
jf.GET("/sync", func(c *gin.Context) {
userId := c.MustGet("userId").(uint)
userType := c.MustGet("userType").(UserType)
username := c.MustGet("username").(string)
userThirdPartyId := c.MustGet("userThirdPartyId").(string)
userThirdPartyAuth := c.MustGet("userThirdPartyAuth").(string)
response, err := jellyfinSyncWatched(b.db, userId, userType, username, userThirdPartyId, userThirdPartyAuth)
if err != nil {
c.JSON(http.StatusForbidden, ErrorResponse{Error: err.Error()})
return
}
c.JSON(http.StatusOK, response)
})
}
func (b *BaseRouter) addPlexRoutes() {
plex := b.rg.Group("/plex").Use(AuthRequired(b.db), PlexAccessRequired(b.db))
// Sync users plex watched items to watchlist
plex.GET("/sync", func(c *gin.Context) {
userId := c.MustGet("userId").(uint)
userPlexLocalAuth := c.MustGet("plexLocalAuthToken").(string)
response, err := plexSyncWatched(b.db, userId, userPlexLocalAuth)
if err != nil {
c.JSON(http.StatusForbidden, ErrorResponse{Error: err.Error()})
return
}
c.JSON(http.StatusOK, response)
})
}
func (b *BaseRouter) addUserRoutes() {
u := b.rg.Group("/user").Use(AuthRequired(b.db))
// Get current user info
u.GET("", func(c *gin.Context) {
userId := c.MustGet("userId").(uint)
response, err := getUserInfo(b.db, userId)
if err != nil {
c.JSON(http.StatusForbidden, ErrorResponse{Error: err.Error()})
return
}
c.JSON(http.StatusOK, response)
})
// Update current user settings
u.POST("/update", func(c *gin.Context) {
userId := c.MustGet("userId").(uint)
var ur UserSettings
err := c.ShouldBindJSON(&ur)
if err == nil {
response, err := userUpdate(b.db, userId, ur)
if err != nil {
c.JSON(http.StatusForbidden, ErrorResponse{Error: err.Error()})
return
}
c.JSON(http.StatusOK, response)
return
}
c.AbortWithStatusJSON(http.StatusBadRequest, ErrorResponse{Error: err.Error()})
})
// Get current user setting
u.GET("/settings", func(c *gin.Context) {
userId := c.MustGet("userId").(uint)
response, err := userGetSettings(b.db, userId)
if err != nil {
c.JSON(http.StatusForbidden, ErrorResponse{Error: err.Error()})
return
}
c.JSON(http.StatusOK, response)
})
// Search users
u.GET("/search/:query", func(c *gin.Context) {
userId := c.MustGet("userId").(uint)
response, err := userSearch(b.db, userId, c.Param("query"))
if err != nil {
c.JSON(http.StatusForbidden, ErrorResponse{Error: err.Error()})
return
}
c.JSON(http.StatusOK, response)
})
// Search users
u.GET("/public/:pubUserId/:pubUsername", func(c *gin.Context) {
id, err := strconv.Atoi(c.Param("pubUserId"))
if err != nil {
c.Status(400)
return
}
response, err := getUserPublicInfo(b.db, uint(id), c.Param("pubUsername"))
if err != nil {
c.JSON(http.StatusForbidden, ErrorResponse{Error: err.Error()})
return
}
c.JSON(http.StatusOK, response)
})
// Update bio
u.POST("/bio", func(c *gin.Context) {
userId := c.MustGet("userId").(uint)
var br UserBioUpdateRequest
err := c.ShouldBindJSON(&br)
if err == nil {
err := userUpdateBio(b.db, userId, br.NewBio)
if err != nil {
c.JSON(http.StatusInternalServerError, ErrorResponse{Error: err.Error()})
return
}
c.Status(http.StatusOK)
return
}
c.AbortWithStatusJSON(http.StatusBadRequest, ErrorResponse{Error: err.Error()})
})
// Upload avatar
u.POST("/avatar", func(c *gin.Context) {
userId := c.MustGet("userId").(uint)
response, err := uploadUserAvatar(c, b.db, userId)
if err != nil {
c.JSON(http.StatusInternalServerError, ErrorResponse{Error: err.Error()})
return
}
c.JSON(http.StatusOK, response)
})
}
func (b *BaseRouter) addFollowRoutes() {
f := b.rg.Group("/follow").Use(AuthRequired(b.db))
// Get users follows // TODO extend to support optionally passing user id as route param, default to current user
f.GET("", func(c *gin.Context) {
userId := c.MustGet("userId").(uint)
response, err := getFollows(b.db, userId)
if err != nil {
c.JSON(http.StatusBadRequest, ErrorResponse{Error: err.Error()})
return
}
c.JSON(http.StatusOK, response)
})
// Follow a user
f.POST("/:toFollowId", func(c *gin.Context) {
userId := c.MustGet("userId").(uint)
toFollowId, err := strconv.ParseUint(c.Param("toFollowId"), 10, 64)
if err != nil {
slog.Error("failed to convert toFollowId param to uint", "toFollowId", toFollowId)
c.Status(400)
return
}
response, err := followUser(b.db, userId, uint(toFollowId))
if err != nil {
c.JSON(http.StatusBadRequest, ErrorResponse{Error: err.Error()})
return
}
c.JSON(http.StatusOK, response)
})
// Unfollow a user
f.DELETE("/:toUnfollowId", func(c *gin.Context) {
userId := c.MustGet("userId").(uint)
toUnfollowId, err := strconv.ParseUint(c.Param("toUnfollowId"), 10, 64)
if err != nil {
slog.Error("failed to convert toUnfollowId param to uint", "toUnfollowId", toUnfollowId)
c.Status(400)
return
}
response, err := unfollowUser(b.db, userId, uint(toUnfollowId))
if err != nil {
c.JSON(http.StatusBadRequest, ErrorResponse{Error: err.Error()})
return
}
c.JSON(http.StatusOK, response)
})
// Get follows thoughts on content
f.GET("/thoughts/:type/:tmdbId", func(c *gin.Context) {
t := c.Param("type")
if t != "movie" && t != "tv" && t != "game" {
c.JSON(http.StatusBadRequest, ErrorResponse{Error: "only movie, tv or game types are supported"})
return
}
userId := c.MustGet("userId").(uint)
response, err := getFollowsThoughts(b.db, userId, t, c.Param("tmdbId"))
if err != nil {
c.JSON(http.StatusBadRequest, ErrorResponse{Error: err.Error()})
return
}
c.JSON(http.StatusOK, response)
})
}
func (b *BaseRouter) addImportRoutes() {
imprt := b.rg.Group("/import").Use(AuthRequired(nil))
imprt.POST("", func(c *gin.Context) {
userId := c.MustGet("userId").(uint)
var ar ImportRequest
err := c.ShouldBindJSON(&ar)
if err == nil {
response, err := importContent(b.db, userId, ar)
if err != nil {
c.JSON(http.StatusForbidden, ErrorResponse{Error: err.Error()})
return
}
c.JSON(http.StatusOK, response)
return
}
c.AbortWithStatusJSON(http.StatusBadRequest, ErrorResponse{Error: err.Error()})
})
imprt.POST("/trakt", func(c *gin.Context) {
userId := c.MustGet("userId").(uint)
var ar TraktImportRequest
err := c.ShouldBindJSON(&ar)
if err == nil {
response, err := traktImportWatched(b.db, userId, ar.Username)
if err != nil {
c.JSON(http.StatusForbidden, ErrorResponse{Error: err.Error()})
return
}
c.JSON(http.StatusOK, response)
return
}
c.AbortWithStatusJSON(http.StatusBadRequest, ErrorResponse{Error: err.Error()})
})
}
func (b *BaseRouter) addServerRoutes() {
server := b.rg.Group("/server").Use(AuthRequired(b.db), AdminRequired())
// Get server config (minus very sensitive fields, like JWT_SECRET)
server.GET("/config", func(c *gin.Context) {
// Return new ServerConfig with only the fields we want to show in settings ui
c.JSON(http.StatusOK, Config.GetSafe())
})
// Update config
server.POST("/config", func(c *gin.Context) {
var ur KeyValueRequest
err := c.ShouldBindJSON(&ur)
if err == nil {
err := updateConfig(ur.Key, ur.Value)
if err != nil {
c.JSON(http.StatusForbidden, ErrorResponse{Error: err.Error()})
return
}
c.Status(http.StatusOK)
return
}
c.AbortWithStatusJSON(http.StatusBadRequest, ErrorResponse{Error: err.Error()})
})
// Update plex host config
server.POST("/config/plex_host", func(c *gin.Context) {
var ur ValueRequest
err := c.ShouldBindJSON(&ur)
if err == nil {
resp, err := updateConfigPlexHost(ur.Value.(string))
if err != nil {
c.JSON(http.StatusForbidden, ErrorResponse{Error: err.Error()})
return
}
c.JSON(http.StatusOK, resp)
return
}
c.AbortWithStatusJSON(http.StatusBadRequest, ErrorResponse{Error: err.Error()})
})
// Get server stats
server.GET("/stats", cache.CachePage(b.ms, time.Minute*5, func(c *gin.Context) {
c.JSON(http.StatusOK, getServerStats(b.db))
}))
// Get all server users (for manage users page)
server.GET("/users", func(c *gin.Context) {
resp, err := getAllUsers(b.db)
if err != nil {
c.JSON(http.StatusInternalServerError, ErrorResponse{Error: err.Error()})
return
}
c.JSON(http.StatusOK, resp)
})
// Edit a user (for manage users page)
server.POST("/users/:id", func(c *gin.Context) {
userId, err := strconv.ParseUint(c.Param("id"), 10, 64)
if err != nil {
slog.Error("/users/:id failed to parse id as a uint", "error", err)
c.JSON(http.StatusInternalServerError, ErrorResponse{Error: "failed to parse id"})
return
}
var ur UpdateUserRequest
err = c.ShouldBindJSON(&ur)
if err == nil {
err := manageUser(b.db, uint(userId), ur)
if err != nil {
c.JSON(http.StatusInternalServerError, ErrorResponse{Error: err.Error()})
return
}
c.Status(http.StatusOK)
return
}
c.AbortWithStatusJSON(http.StatusBadRequest, ErrorResponse{Error: err.Error()})
})
}
func (b *BaseRouter) addFeatureRoutes() {
feature := b.rg.Group("/features").Use(AuthRequired(b.db))
// Get enabled features (aka functionality)
feature.GET("", func(c *gin.Context) {
c.JSON(http.StatusOK, getEnabledFeatures(c.GetInt("userPermissions")))
})
}
func (b *BaseRouter) addSonarrRoutes() {
s := b.rg.Group("/arr/son").Use(AuthRequired(b.db))
// Routes are manually given AdminRequired or PermRequired middleware.
// Test configuration
s.POST("/test", AdminRequired(), func(c *gin.Context) {
var ur ArrTestParams
err := c.ShouldBindJSON(&ur)
if err == nil {
resp, err := testSonarr(ur)
if err != nil {
c.JSON(http.StatusBadRequest, ErrorResponse{Error: err.Error()})
return
}
c.JSON(http.StatusOK, resp)
return
}
c.AbortWithStatusJSON(http.StatusBadRequest, ErrorResponse{Error: err.Error()})
})
// Used to get config for specific server (quality profile, root folder, etc)
s.GET("/config/:name", PermRequired(PERM_REQUEST_CONTENT), func(c *gin.Context) {
server, err := getSonarr(c.Param("name"))
if err != nil {
c.JSON(http.StatusBadRequest, ErrorResponse{Error: err.Error()})
return
}
resp, err := testSonarr(ArrTestParams{Host: server.Host, Key: server.Key})
if err != nil {
c.JSON(http.StatusBadRequest, ErrorResponse{Error: err.Error()})
return
}
c.JSON(http.StatusOK, resp)
})
// Add sonarr server into config
s.POST("/add", AdminRequired(), func(c *gin.Context) {
var ur SonarrSettings
err := c.ShouldBindJSON(&ur)
if err == nil {
err := addSonarr(ur)
if err != nil {
c.JSON(http.StatusBadRequest, ErrorResponse{Error: err.Error()})
return
}
c.Status(http.StatusOK)
return
}
c.AbortWithStatusJSON(http.StatusBadRequest, ErrorResponse{Error: err.Error()})
})
// Edit sonarr servers config
s.POST("/edit", AdminRequired(), func(c *gin.Context) {
var ur SonarrSettings
err := c.ShouldBindJSON(&ur)
if err == nil {
err := editSonarr(ur)
if err != nil {
c.JSON(http.StatusBadRequest, ErrorResponse{Error: err.Error()})
return
}
c.Status(http.StatusOK)
return
}
c.AbortWithStatusJSON(http.StatusBadRequest, ErrorResponse{Error: err.Error()})
})
// Remove sonarr server
s.POST("/rm/:name", AdminRequired(), func(c *gin.Context) {
err := rmSonarr(c.Param("name"))
if err != nil {
c.JSON(http.StatusBadRequest, ErrorResponse{Error: err.Error()})
return
}
c.Status(http.StatusOK)
})
// Get safe config for all sonarr servers
s.GET("", PermRequired(PERM_REQUEST_CONTENT), func(c *gin.Context) {
response := getSonarrsSafe()
c.JSON(http.StatusOK, response)
})
// Request a show
s.POST("/request", PermRequired(PERM_REQUEST_CONTENT), func(c *gin.Context) {
var ur arr.SonarrRequest
err := c.ShouldBindJSON(&ur)
if err == nil {
userId := c.MustGet("userId").(uint)
perms := c.GetInt("userPermissions")
response, err := createSonarrRequest(b.db, userId, perms, ur)
if err != nil {
c.JSON(http.StatusBadRequest, ErrorResponse{Error: err.Error()})
return
}
c.JSON(http.StatusOK, response)
return
}
c.AbortWithStatusJSON(http.StatusBadRequest, ErrorResponse{Error: err.Error()})
})
s.GET("/request/:tmdbId", PermRequired(PERM_REQUEST_CONTENT), func(c *gin.Context) {
tmdbId, err := strconv.Atoi(c.Param("tmdbId"))
if err != nil {
slog.Error("Couldn't parse tmdbId", "tmdbId", tmdbId)
c.Status(400)
return
}
response, err := getArrRequestByTmdbId(b.db, SHOW, tmdbId)
if err != nil {
c.JSON(http.StatusBadRequest, ErrorResponse{Error: err.Error()})
return
}
c.JSON(http.StatusOK, response)
})
s.POST("/request/approve/:id", PermRequired(PERM_ADMIN), func(c *gin.Context) {
var ur arr.SonarrRequest
err := c.ShouldBindJSON(&ur)
if err == nil {
requestId, err := strconv.Atoi(c.Param("id"))
if err != nil {
slog.Error("Couldn't parse request id", "request_id", requestId)
c.Status(400)
return
}
response, err := approveSonarrRequest(b.db, uint(requestId), ur)
if err != nil {
c.JSON(http.StatusBadRequest, ErrorResponse{Error: err.Error()})
return
}
c.JSON(http.StatusOK, response)
return
}
c.AbortWithStatusJSON(http.StatusBadRequest, ErrorResponse{Error: err.Error()})
})
s.GET("/status/:serverName/:arrId", PermRequired(PERM_REQUEST_CONTENT), func(c *gin.Context) {
response, err := getSonarrQueueDetails(c.Param("serverName"), c.Param("arrId"))
if err != nil {
if err.Error() == "no details found" {
c.Status(http.StatusNoContent) // Item not found in queue.. missing
return
}
c.JSON(http.StatusBadRequest, ErrorResponse{Error: err.Error()})
return
}
c.JSON(http.StatusOK, response)
})
s.GET("/info/:requestId", PermRequired(PERM_REQUEST_CONTENT), func(c *gin.Context) {
requestId, err := strconv.ParseUint(c.Param("requestId"), 10, 64)
if err != nil {
slog.Error("/info/:requestId - requestId could not be parsed", "requestId", requestId)
c.Status(http.StatusBadRequest)
return
}
response, err := getSonarrRequestInfo(b.db, uint(requestId))
if err != nil {
if err.Error() == "request deleted" {
c.JSON(http.StatusNotFound, ErrorResponse{Error: "request deleted"})
return
}
c.JSON(http.StatusBadRequest, ErrorResponse{Error: err.Error()})
return
}
c.JSON(http.StatusOK, response)
})
}
func (b *BaseRouter) addRadarrRoutes() {
s := b.rg.Group("/arr/rad").Use(AuthRequired(b.db))
// Routes are manually given AdminRequired or PermRequired middleware.
// Test configuration
s.POST("/test", AdminRequired(), func(c *gin.Context) {
var ur ArrTestParams
err := c.ShouldBindJSON(&ur)
if err == nil {
resp, err := testRadarr(ur)
if err != nil {
c.JSON(http.StatusBadRequest, ErrorResponse{Error: err.Error()})
return
}
c.JSON(http.StatusOK, resp)
return
}
c.AbortWithStatusJSON(http.StatusBadRequest, ErrorResponse{Error: err.Error()})
})
// Get config for specific server
s.GET("/config/:name", PermRequired(PERM_REQUEST_CONTENT), func(c *gin.Context) {
server, err := getRadarr(c.Param("name"))
if err != nil {
c.JSON(http.StatusBadRequest, ErrorResponse{Error: err.Error()})
return
}
resp, err := testRadarr(ArrTestParams{Host: server.Host, Key: server.Key})
if err != nil {
c.JSON(http.StatusBadRequest, ErrorResponse{Error: err.Error()})
return
}
c.JSON(http.StatusOK, resp)
})
s.POST("/add", AdminRequired(), func(c *gin.Context) {
var ur RadarrSettings
err := c.ShouldBindJSON(&ur)
if err == nil {
err := addRadarr(ur)
if err != nil {
c.JSON(http.StatusBadRequest, ErrorResponse{Error: err.Error()})
return
}
c.Status(http.StatusOK)
return
}
c.AbortWithStatusJSON(http.StatusBadRequest, ErrorResponse{Error: err.Error()})
})
s.POST("/edit", AdminRequired(), func(c *gin.Context) {
var ur RadarrSettings
err := c.ShouldBindJSON(&ur)
if err == nil {
err := editRadarr(ur)
if err != nil {
c.JSON(http.StatusBadRequest, ErrorResponse{Error: err.Error()})
return
}
c.Status(http.StatusOK)
return
}
c.AbortWithStatusJSON(http.StatusBadRequest, ErrorResponse{Error: err.Error()})
})
s.POST("/rm/:name", AdminRequired(), func(c *gin.Context) {
err := rmRadarr(c.Param("name"))
if err != nil {
c.JSON(http.StatusBadRequest, ErrorResponse{Error: err.Error()})
return
}
c.Status(http.StatusOK)
})
s.GET("", PermRequired(PERM_REQUEST_CONTENT), func(c *gin.Context) {
response := getRadarrsSafe()
c.JSON(http.StatusOK, response)
})
s.POST("/request", PermRequired(PERM_REQUEST_CONTENT), func(c *gin.Context) {
var ur arr.RadarrRequest
err := c.ShouldBindJSON(&ur)
if err == nil {
userId := c.MustGet("userId").(uint)
perms := c.GetInt("userPermissions")
response, err := createRadarrRequest(b.db, userId, perms, ur)
if err != nil {
c.JSON(http.StatusBadRequest, ErrorResponse{Error: err.Error()})
return
}
c.JSON(http.StatusOK, response)
return
}
c.AbortWithStatusJSON(http.StatusBadRequest, ErrorResponse{Error: err.Error()})
})
s.GET("/request/:tmdbId", PermRequired(PERM_REQUEST_CONTENT), func(c *gin.Context) {
tmdbId, err := strconv.Atoi(c.Param("tmdbId"))
if err != nil {
slog.Error("Couldn't parse tmdbId", "tmdbId", tmdbId)
c.Status(400)
return
}
response, err := getArrRequestByTmdbId(b.db, MOVIE, tmdbId)
if err != nil {
c.JSON(http.StatusBadRequest, ErrorResponse{Error: err.Error()})
return
}
c.JSON(http.StatusOK, response)
})
s.POST("/request/approve/:id", PermRequired(PERM_ADMIN), func(c *gin.Context) {
var ur arr.RadarrRequest
err := c.ShouldBindJSON(&ur)
if err == nil {
requestId, err := strconv.Atoi(c.Param("id"))
if err != nil {
slog.Error("Couldn't parse request id", "request_id", requestId)
c.Status(400)
return
}
response, err := approveRadarrRequest(b.db, uint(requestId), ur)
if err != nil {
c.JSON(http.StatusBadRequest, ErrorResponse{Error: err.Error()})
return
}
c.JSON(http.StatusOK, response)
return
}
c.AbortWithStatusJSON(http.StatusBadRequest, ErrorResponse{Error: err.Error()})
})
s.GET("/status/:serverName/:arrId", PermRequired(PERM_REQUEST_CONTENT), func(c *gin.Context) {
response, err := getRadarrQueueDetails(c.Param("serverName"), c.Param("arrId"))
if err != nil {
if err.Error() == "no details found" {
c.Status(http.StatusNoContent) // Item not found in queue.. missing
return
}
c.JSON(http.StatusBadRequest, ErrorResponse{Error: err.Error()})
return
}
c.JSON(http.StatusOK, response)
})
s.GET("/info/:requestId", PermRequired(PERM_REQUEST_CONTENT), func(c *gin.Context) {
requestId, err := strconv.ParseUint(c.Param("requestId"), 10, 64)
if err != nil {
slog.Error("/info/:requestId - requestId could not be parsed", "requestId", requestId)
c.Status(http.StatusBadRequest)
return
}
response, err := getRadarrRequestInfo(b.db, uint(requestId))
if err != nil {
if err.Error() == "request deleted" {
c.JSON(http.StatusNotFound, ErrorResponse{Error: "request deleted"})
return
}
c.JSON(http.StatusBadRequest, ErrorResponse{Error: err.Error()})
return
}
c.JSON(http.StatusOK, response)
})
}
func (b *BaseRouter) addArrRequestRoutes() {
s := b.rg.Group("/arr/request").Use(AuthRequired(b.db))
// Get all requests (for manage_requests view), only for admins.
s.GET("/", AdminRequired(), func(c *gin.Context) {
response, err := getArrRequests(b.db)
if err != nil {
c.JSON(http.StatusBadRequest, ErrorResponse{Error: err.Error()})
return
}
c.JSON(http.StatusOK, response)
})
// Deny a request (for manage_requests view), only for admins.
s.POST("/deny/:id", AdminRequired(), func(c *gin.Context) {
requestId, err := strconv.Atoi(c.Param("id"))
if err != nil {
slog.Error("Couldn't parse request id", "request_id", requestId)
c.Status(400)
return
}
err = denyArrRequest(b.db, uint(requestId))
if err != nil {
c.JSON(http.StatusInternalServerError, ErrorResponse{Error: err.Error()})
return
}
c.Status(http.StatusOK)
})
}
func (b *BaseRouter) addJobRoutes() {
job := b.rg.Group("/job").Use(AuthRequired(nil))
// Uses wildcard so it still works in cases where the job id includes a /.
// (yes i changed this instead of not allowing a / when we generate a job id becuz easier)
job.GET("/*id", func(c *gin.Context) {
userId := c.MustGet("userId").(uint)
// When we get id param, don't include first letter, which will be the beginning '/'.
response, err := getJob(c.Param("id")[1:], userId)
if err != nil {
c.JSON(http.StatusForbidden, ErrorResponse{Error: err.Error()})
return
}
c.JSON(http.StatusOK, *response)
})
}
func (b *BaseRouter) addTaskRoutes() {
task := b.rg.Group("/task").Use(AuthRequired(b.db), AdminRequired())
task.GET("/", func(c *gin.Context) {
response := getAllTasks()
c.JSON(http.StatusOK, response)
})
task.PUT(":name", func(c *gin.Context) {
if c.Param("name") == "" {
c.JSON(http.StatusBadRequest, ErrorResponse{Error: "no task name provided"})
return
}
var rr TaskRescheduleRequest
err := c.ShouldBindJSON(&rr)
if err == nil {
err := rescheduleTask(c.Param("name"), rr)
if err != nil {
c.JSON(http.StatusInternalServerError, ErrorResponse{Error: err.Error()})
return
}
c.Status(http.StatusOK)
return
}
c.AbortWithStatusJSON(http.StatusBadRequest, ErrorResponse{Error: err.Error()})
})
}
func (b *BaseRouter) addTagRoutes() {
tag := b.rg.Group("/tag").Use(AuthRequired(nil))
// Get list of all our tags.
tag.GET("", func(c *gin.Context) {
userId := c.MustGet("userId").(uint)
tags, err := getTags(b.db, userId)
if err != nil {
c.JSON(http.StatusInternalServerError, ErrorResponse{Error: err.Error()})
return
}
c.JSON(http.StatusOK, tags)
})
// // Get all items within one of our tags.
// tag.GET(":id", func(c *gin.Context) {
// id, err := strconv.Atoi(c.Param("id"))
// if err != nil {
// slog.Error("getTag route failed to convert id param to int", "error", err)
// c.Status(http.StatusBadRequest)
// return
// }
// userId := c.MustGet("userId").(uint)
// tags, err := getTag(b.db, userId, uint(id))
// if err != nil {
// c.JSON(http.StatusInternalServerError, ErrorResponse{Error: err.Error()})
// return
// }
// c.JSON(http.StatusOK, tags)
// })
// Create a tag.
tag.POST("", func(c *gin.Context) {
userId := c.MustGet("userId").(uint)
var tr TagAddRequest
err := c.ShouldBindJSON(&tr)
if err == nil {
response, err := addTag(b.db, userId, tr)
if err != nil {
c.JSON(http.StatusInternalServerError, ErrorResponse{Error: err.Error()})
return
}
c.JSON(http.StatusOK, response)
return
}
c.AbortWithStatusJSON(http.StatusBadRequest, ErrorResponse{Error: err.Error()})
})
// Update a tag.
tag.PUT(":id", func(c *gin.Context) {
userId := c.MustGet("userId").(uint)
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.Status(400)
slog.Error("tag update rote: failed to process tag id.", "error", err.Error(), "id", c.Param("id"))
return
}
var tr TagAddRequest
err = c.ShouldBindJSON(&tr)
if err == nil {
err := updateTag(b.db, userId, uint(id), tr)
if err != nil {
c.JSON(http.StatusInternalServerError, ErrorResponse{Error: err.Error()})
return
}
c.Status(http.StatusOK)
return
}
c.AbortWithStatusJSON(http.StatusBadRequest, ErrorResponse{Error: err.Error()})
})
tag.DELETE(":id", func(c *gin.Context) {
userId := c.MustGet("userId").(uint)
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.Status(400)
slog.Error("tag delete rote: failed to process tag id.", "error", err.Error(), "id", c.Param("id"))
return
}
err = deleteTag(b.db, userId, uint(id))
if err != nil {
c.JSON(http.StatusInternalServerError, ErrorResponse{Error: err.Error()})
return
}
c.Status(http.StatusOK)
})
}