From 16fadb8663514ac1ea386cc5caf3b623129db231 Mon Sep 17 00:00:00 2001 From: rxdn <29165304+rxdn@users.noreply.github.com> Date: Wed, 13 Nov 2024 22:45:37 +0000 Subject: [PATCH] Make use of local storage --- app/http/endpoints/api/guilds.go | 151 ------------------ app/http/endpoints/api/reloadguilds.go | 53 +++--- app/http/endpoints/root/callback.go | 87 +++++----- .../endpoints/root/{IpHandler.go => ip.go} | 0 app/http/middleware/errorhandler.go | 3 + app/http/server.go | 1 - frontend/src/includes/Auth.svelte | 8 +- frontend/src/includes/Sidebar.svelte | 47 +++--- frontend/src/layouts/AdminLayout.svelte | 35 ++-- frontend/src/layouts/IndexLayout.svelte | 36 ++--- frontend/src/views/Index.svelte | 29 +--- frontend/src/views/LoginCallback.svelte | 9 ++ frontend/src/views/admin/BotStaff.svelte | 2 +- .../src/views/premium/SelectServers.svelte | 8 +- go.mod | 4 +- go.sum | 4 + utils/discord/auth.go | 119 -------------- utils/discord/endpoints.go | 121 -------------- utils/discord/endpoints/user/currentUser.go | 9 -- .../endpoints/user/currentUserGuilds.go | 9 -- utils/guildutils.go | 137 +++++++++++++++- 21 files changed, 293 insertions(+), 579 deletions(-) delete mode 100644 app/http/endpoints/api/guilds.go rename app/http/endpoints/root/{IpHandler.go => ip.go} (100%) delete mode 100644 utils/discord/auth.go delete mode 100644 utils/discord/endpoints.go delete mode 100644 utils/discord/endpoints/user/currentUser.go delete mode 100644 utils/discord/endpoints/user/currentUserGuilds.go diff --git a/app/http/endpoints/api/guilds.go b/app/http/endpoints/api/guilds.go deleted file mode 100644 index 70392dc..0000000 --- a/app/http/endpoints/api/guilds.go +++ /dev/null @@ -1,151 +0,0 @@ -package api - -import ( - "cmp" - "context" - dbclient "github.com/TicketsBot/GoPanel/database" - "github.com/TicketsBot/GoPanel/rpc/cache" - "github.com/TicketsBot/GoPanel/utils" - "github.com/TicketsBot/common/collections" - "github.com/TicketsBot/common/permission" - syncutils "github.com/TicketsBot/common/utils" - "github.com/TicketsBot/database" - "github.com/gin-gonic/gin" - "github.com/jackc/pgtype" - "golang.org/x/sync/errgroup" - "slices" - "sync" - "time" -) - -type wrappedGuild struct { - Id uint64 `json:"id,string"` - Name string `json:"name"` - Icon string `json:"icon"` - PermissionLevel permission.PermissionLevel `json:"permission_level"` -} - -func GetGuilds(c *gin.Context) { - userId := c.Keys["userid"].(uint64) - - // Get the guilds that the user is in, that the bot is also in - userGuilds, err := getGuildIntersection(userId) - if err != nil { - c.JSON(500, utils.ErrorJson(err)) - return - } - - wg := syncutils.NewChannelWaitGroup() - wg.Add(len(userGuilds)) - - ctx, cancel := context.WithTimeout(c, time.Second*10) - defer cancel() - - group, ctx := errgroup.WithContext(ctx) - - var mu sync.Mutex - guilds := make([]wrappedGuild, 0, len(userGuilds)) - for _, guild := range userGuilds { - guild := guild - - group.Go(func() error { - defer wg.Done() - - permLevel, err := utils.GetPermissionLevel(ctx, guild.GuildId, userId) - if err != nil { - return err - } - - mu.Lock() - guilds = append(guilds, wrappedGuild{ - Id: guild.GuildId, - Name: guild.Name, - Icon: guild.Icon, - PermissionLevel: permLevel, - }) - mu.Unlock() - - return nil - }) - } - - if err := group.Wait(); err != nil { - c.JSON(500, utils.ErrorJson(err)) - return - } - - // Sort the guilds by name, but put the guilds with permission_level=0 last - slices.SortFunc(guilds, func(a, b wrappedGuild) int { - if a.PermissionLevel == 0 && b.PermissionLevel > 0 { - return 1 - } else if a.PermissionLevel > 0 && b.PermissionLevel == 0 { - return -1 - } - - return cmp.Compare(a.Name, b.Name) - }) - - c.JSON(200, guilds) -} - -func getGuildIntersection(userId uint64) ([]database.UserGuild, error) { - // Get all the guilds that the user is in - userGuilds, err := dbclient.Client.UserGuilds.Get(context.Background(), userId) - if err != nil { - return nil, err - } - - guildIds := make([]uint64, len(userGuilds)) - for i, guild := range userGuilds { - guildIds[i] = guild.GuildId - } - - // Restrict the set of guilds to guilds that the bot is also in - botGuilds, err := getExistingGuilds(guildIds) - if err != nil { - return nil, err - } - - botGuildIds := collections.NewSet[uint64]() - for _, guildId := range botGuilds { - botGuildIds.Add(guildId) - } - - // Get the intersection of the two sets - intersection := make([]database.UserGuild, 0, len(botGuilds)) - for _, guild := range userGuilds { - if botGuildIds.Contains(guild.GuildId) { - intersection = append(intersection, guild) - } - } - - return intersection, nil -} - -func getExistingGuilds(userGuilds []uint64) ([]uint64, error) { - query := `SELECT "guild_id" from guilds WHERE "guild_id" = ANY($1);` - - userGuildsArray := &pgtype.Int8Array{} - if err := userGuildsArray.Set(userGuilds); err != nil { - return nil, err - } - - rows, err := cache.Instance.Query(context.Background(), query, userGuildsArray) - if err != nil { - return nil, err - } - - defer rows.Close() - - var existingGuilds []uint64 - for rows.Next() { - var guildId uint64 - if err := rows.Scan(&guildId); err != nil { - return nil, err - } - - existingGuilds = append(existingGuilds, guildId) - } - - return existingGuilds, nil -} diff --git a/app/http/endpoints/api/reloadguilds.go b/app/http/endpoints/api/reloadguilds.go index 2edd3e9..f52f7d7 100644 --- a/app/http/endpoints/api/reloadguilds.go +++ b/app/http/endpoints/api/reloadguilds.go @@ -1,30 +1,34 @@ package api import ( + "errors" "fmt" "github.com/TicketsBot/GoPanel/app/http/session" + "github.com/TicketsBot/GoPanel/config" "github.com/TicketsBot/GoPanel/redis" wrapper "github.com/TicketsBot/GoPanel/redis" "github.com/TicketsBot/GoPanel/utils" - "github.com/TicketsBot/GoPanel/utils/discord" "github.com/gin-gonic/gin" + "github.com/rxdn/gdl/rest" + "github.com/rxdn/gdl/rest/request" + "net/http" "time" ) -func ReloadGuildsHandler(ctx *gin.Context) { - userId := ctx.Keys["userid"].(uint64) +func ReloadGuildsHandler(c *gin.Context) { + userId := c.Keys["userid"].(uint64) key := fmt.Sprintf("tickets:dashboard:guildreload:%d", userId) res, err := redis.Client.SetNX(wrapper.DefaultContext(), key, 1, time.Second*10).Result() if err != nil { - ctx.JSON(500, utils.ErrorJson(err)) + _ = c.AbortWithError(http.StatusInternalServerError, err) return } if !res { ttl, err := redis.Client.TTL(wrapper.DefaultContext(), key).Result() if err != nil { - ctx.JSON(500, utils.ErrorJson(err)) + c.JSON(500, utils.ErrorJson(err)) return } @@ -33,19 +37,19 @@ func ReloadGuildsHandler(ctx *gin.Context) { ttl = 0 } - ctx.JSON(429, utils.ErrorStr("You're doing this too quickly: try again in %d seconds", int(ttl.Seconds()))) + c.JSON(429, utils.ErrorStr("You're doing this too quickly: try again in %d seconds", int(ttl.Seconds()))) return } store, err := session.Store.Get(userId) if err != nil { if err == session.ErrNoSession { - ctx.JSON(401, gin.H{ + c.JSON(401, gin.H{ "success": false, "auth": true, }) } else { - ctx.JSON(500, utils.ErrorJson(err)) + _ = c.AbortWithError(http.StatusInternalServerError, err) } return @@ -53,9 +57,9 @@ func ReloadGuildsHandler(ctx *gin.Context) { // What does this do? if store.Expiry > time.Now().Unix() { - res, err := discord.RefreshToken(store.RefreshToken) + res, err := rest.RefreshToken(c, nil, config.Conf.Oauth.Id, config.Conf.Oauth.Secret, store.RefreshToken) if err != nil { // Tell client to re-authenticate - ctx.JSON(200, gin.H{ + c.JSON(200, gin.H{ "success": false, "reauthenticate_required": true, }) @@ -67,20 +71,31 @@ func ReloadGuildsHandler(ctx *gin.Context) { store.Expiry = time.Now().Unix() + int64(res.ExpiresIn) if err := session.Store.Set(userId, store); err != nil { - ctx.JSON(500, utils.ErrorJson(err)) + _ = c.AbortWithError(http.StatusInternalServerError, err) return } } - if err := utils.LoadGuilds(store.AccessToken, userId); err != nil { - // TODO: Log to sentry - // Tell client to reauth, needs a 200 or client will display error - ctx.JSON(200, gin.H{ - "success": false, - "reauthenticate_required": true, - }) + guilds, err := utils.LoadGuilds(c, store.AccessToken, userId) + if err != nil { + var oauthError request.OAuthError + if errors.As(err, &oauthError) { + if oauthError.ErrorCode == "invalid_grant" { + // Tell client to reauth, needs a 200 or client will display error + c.JSON(http.StatusOK, gin.H{ + "success": false, + "reauthenticate_required": true, + }) + return + } + } + + _ = c.AbortWithError(http.StatusInternalServerError, err) return } - ctx.JSON(200, utils.SuccessResponse) + c.JSON(200, gin.H{ + "success": true, + "guilds": guilds, + }) } diff --git a/app/http/endpoints/root/callback.go b/app/http/endpoints/root/callback.go index c079b3c..787d0c1 100644 --- a/app/http/endpoints/root/callback.go +++ b/app/http/endpoints/root/callback.go @@ -2,54 +2,53 @@ package root import ( "context" + "errors" "fmt" + "github.com/TicketsBot/GoPanel/app" "github.com/TicketsBot/GoPanel/app/http/session" "github.com/TicketsBot/GoPanel/config" "github.com/TicketsBot/GoPanel/utils" - "github.com/TicketsBot/GoPanel/utils/discord" "github.com/gin-gonic/gin" "github.com/golang-jwt/jwt" "github.com/rxdn/gdl/rest" + "github.com/rxdn/gdl/rest/request" + "net/http" "strconv" + "strings" "time" ) -type ( - TokenData struct { - ClientId string `qs:"client_id"` - ClientSecret string `qs:"client_secret"` - GrantType string `qs:"grant_type"` - Code string `qs:"code"` - RedirectUri string `qs:"redirect_uri"` - Scope string `qs:"scope"` - } - - TokenResponse struct { - AccessToken string `json:"access_token"` - TokenType string `json:"token_type"` - ExpiresIn int `json:"expires_in"` - RefreshToken string `json:"refresh_token"` - Scope string `json:"scope"` - } -) - -func CallbackHandler(ctx *gin.Context) { - code, ok := ctx.GetQuery("code") +func CallbackHandler(c *gin.Context) { + code, ok := c.GetQuery("code") if !ok { - ctx.JSON(400, utils.ErrorStr("Discord provided invalid Oauth2 code")) + c.JSON(400, utils.ErrorStr("Missing code query parameter")) return } - res, err := discord.AccessToken(code) + res, err := rest.ExchangeCode(c, nil, config.Conf.Oauth.Id, config.Conf.Oauth.Secret, config.Conf.Oauth.RedirectUri, code) if err != nil { - ctx.JSON(500, utils.ErrorJson(err)) + var oauthError request.OAuthError + if errors.As(err, &oauthError) { + if oauthError.ErrorCode == "invalid_grant" { + c.JSON(400, utils.ErrorStr("Invalid code: try logging in again")) + return + } + } + + _ = c.AbortWithError(http.StatusInternalServerError, app.NewServerError(err)) + return + } + + scopes := strings.Split(res.Scope, " ") + if !utils.Contains(scopes, "identify") { + c.JSON(400, utils.ErrorStr("Missing identify scope")) return } // Get ID + name currentUser, err := rest.GetCurrentUser(context.Background(), fmt.Sprintf("Bearer %s", res.AccessToken), nil) if err != nil { - ctx.JSON(500, utils.ErrorJson(err)) + _ = c.AbortWithError(http.StatusInternalServerError, app.NewServerError(err)) return } @@ -62,31 +61,47 @@ func CallbackHandler(ctx *gin.Context) { HasGuilds: false, } - if err := utils.LoadGuilds(res.AccessToken, currentUser.Id); err == nil { + var guilds []utils.GuildDto + if utils.Contains(scopes, "guilds") { + guilds, err = utils.LoadGuilds(c, res.AccessToken, currentUser.Id) + if err != nil { + _ = c.AbortWithError(http.StatusInternalServerError, app.NewServerError(err)) + return + } + store.HasGuilds = true - } else { - ctx.JSON(500, utils.ErrorJson(err)) - return } token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ - "userid": strconv.FormatUint(currentUser.Id, 10), - "timestamp": time.Now(), + "userid": strconv.FormatUint(currentUser.Id, 10), + "sub": strconv.FormatUint(currentUser.Id, 10), + "iat": time.Now().Unix(), }) str, err := token.SignedString([]byte(config.Conf.Server.Secret)) if err != nil { - ctx.JSON(500, utils.ErrorJson(err)) + _ = c.AbortWithError(http.StatusInternalServerError, err) return } if err := session.Store.Set(currentUser.Id, store); err != nil { - ctx.JSON(500, utils.ErrorJson(err)) + _ = c.AbortWithError(http.StatusInternalServerError, err) return } - ctx.JSON(200, gin.H{ + resMap := gin.H{ "success": true, "token": str, - }) + "user_data": gin.H{ + "id": strconv.FormatUint(currentUser.Id, 10), + "username": currentUser.Username, + "avatar": currentUser.Avatar, + "admin": utils.Contains(config.Conf.Admins, currentUser.Id), + }, + } + if len(guilds) > 0 { + resMap["guilds"] = guilds + } + + c.JSON(http.StatusOK, resMap) } diff --git a/app/http/endpoints/root/IpHandler.go b/app/http/endpoints/root/ip.go similarity index 100% rename from app/http/endpoints/root/IpHandler.go rename to app/http/endpoints/root/ip.go diff --git a/app/http/middleware/errorhandler.go b/app/http/middleware/errorhandler.go index 7b669f9..6f6cb75 100644 --- a/app/http/middleware/errorhandler.go +++ b/app/http/middleware/errorhandler.go @@ -36,6 +36,7 @@ func ErrorHandler(c *gin.Context) { message = "An error occurred processing your request" } + c.Writer = cw.ResponseWriter c.JSON(-1, ErrorResponse{ Error: message, }) @@ -44,6 +45,8 @@ func ErrorHandler(c *gin.Context) { } if c.Writer.Status() >= 500 { + c.Writer = cw.ResponseWriter + c.JSON(-1, ErrorResponse{ Error: "An internal server error occurred", }) diff --git a/app/http/server.go b/app/http/server.go index 353e2d8..b92a47a 100644 --- a/app/http/server.go +++ b/app/http/server.go @@ -196,7 +196,6 @@ func StartServer(logger *zap.Logger, sm *livechat.SocketManager) { userGroup := router.Group("/user", middleware.AuthenticateToken, middleware.UpdateLastSeen) { - userGroup.GET("/guilds", api.GetGuilds) userGroup.POST("/guilds/reload", api.ReloadGuildsHandler) userGroup.GET("/permissionlevel", api.GetPermissionLevel) diff --git a/frontend/src/includes/Auth.svelte b/frontend/src/includes/Auth.svelte index 45971e3..2cf4d24 100644 --- a/frontend/src/includes/Auth.svelte +++ b/frontend/src/includes/Auth.svelte @@ -1,6 +1,6 @@
- {#if isWhitelabel} -