From c204dc5a8b604b75e3a9f379b4c69f44d37e6c01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mike=20Schw=C3=B6rer?= Date: Tue, 16 Jul 2024 17:19:55 +0200 Subject: [PATCH] Refactor server to go-sqlite and ginext [WIP] --- scnserver/Makefile | 10 +- scnserver/TODO.md | 4 - scnserver/api/apierr/enums.go | 25 +- scnserver/api/ginresp/resp.go | 4 - scnserver/api/handler/apiChannel.go | 434 +++++----- scnserver/api/handler/apiClient.go | 219 ++--- scnserver/api/handler/apiKeyToken.go | 283 ++++--- scnserver/api/handler/apiMessage.go | 279 +++---- scnserver/api/handler/apiPreview.go | 85 +- scnserver/api/handler/apiSubscription.go | 387 ++++----- scnserver/api/handler/apiUser.go | 267 ++++--- scnserver/api/handler/common.go | 166 ++-- scnserver/api/handler/compat.go | 973 ++++++++++++----------- scnserver/api/handler/external.go | 110 +-- scnserver/api/handler/message.go | 44 +- scnserver/api/handler/website.go | 47 +- scnserver/api/router.go | 134 ++-- scnserver/api/wrapper.go | 195 ----- scnserver/cmd/dbhash/main.go | 7 +- scnserver/cmd/migrate/main.go | 2 +- scnserver/cmd/scnserver/main.go | 10 +- scnserver/db/impl/logs/database.go | 7 +- scnserver/db/impl/primary/database.go | 7 +- scnserver/db/impl/requests/database.go | 7 +- scnserver/go.mod | 2 +- scnserver/go.sum | 6 + scnserver/logic/appcontext.go | 7 +- scnserver/logic/application.go | 30 - scnserver/logic/message.go | 1 + scnserver/logic/permissions.go | 1 + scnserver/logic/request.go | 240 ++++++ scnserver/models/enums_gen.go | 2 +- scnserver/models/ids_gen.go | 2 +- scnserver/swagger/swagger.json | 252 +++++- scnserver/swagger/swagger.yaml | 247 ++++-- scnserver/test/requestlog_test.go | 23 + scnserver/test/util/log.go | 2 - scnserver/test/util/webserver.go | 9 +- 38 files changed, 2547 insertions(+), 1983 deletions(-) delete mode 100644 scnserver/api/wrapper.go create mode 100644 scnserver/logic/request.go diff --git a/scnserver/Makefile b/scnserver/Makefile index b677207..9a48b2c 100644 --- a/scnserver/Makefile +++ b/scnserver/Makefile @@ -5,6 +5,8 @@ PORT=9090 NAMESPACE=$(shell git rev-parse --abbrev-ref HEAD) HASH=$(shell git rev-parse HEAD) +TAGS="timetzdata sqlite_fts5 sqlite_foreign_keys" + .PHONY: test swagger pygmentize docker migrate dgi pygmentize lint docker SWAGGO_VERSION=v1.8.12 @@ -13,7 +15,7 @@ SWAGGO=github.com/swaggo/swag/cmd/swag@$(SWAGGO_VERSION) build: ids enums swagger pygmentize fmt mkdir -p _build rm -f ./_build/scn_backend - CGO_ENABLED=1 go build -v -o _build/scn_backend -tags "timetzdata sqlite_fts5 sqlite_foreign_keys" ./cmd/scnserver + CGO_ENABLED=1 go build -v -o _build/scn_backend -tags $(TAGS) ./cmd/scnserver enums: go generate models/enums.go @@ -27,7 +29,7 @@ run: build gow: which gow || go install github.com/mitranim/gow@latest - gow -e "go,mod,html,css,json,yaml,js" run -tags "timetzdata sqlite_fts5 sqlite_foreign_keys" blackforestbytes.com/simplecloudnotifier/cmd/scnserver + gow -e "go,mod,html,css,json,yaml,js" run -tags $(TAGS) blackforestbytes.com/simplecloudnotifier/cmd/scnserver dgi: [ ! -f "DOCKER_GIT_INFO" ] || rm DOCKER_GIT_INFO @@ -99,10 +101,10 @@ fmt: swagger-setup test: which gotestsum || go install gotest.tools/gotestsum@latest - gotestsum --format "testname" -- -tags="timetzdata sqlite_fts5 sqlite_foreign_keys" "./test" + gotestsum --format "testname" -- -tags $(TAGS) "./test" migrate: - CGO_ENABLED=1 go build -v -o _build/scn_migrate -tags "timetzdata sqlite_fts5 sqlite_foreign_keys" ./cmd/migrate + CGO_ENABLED=1 go build -v -o _build/scn_migrate -tags $(TAGS) ./cmd/migrate ./_build/scn_migrate lint: diff --git a/scnserver/TODO.md b/scnserver/TODO.md index 31eef31..4c1d3f6 100644 --- a/scnserver/TODO.md +++ b/scnserver/TODO.md @@ -12,10 +12,6 @@ - (!) use goext.ginWrapper - - (!) use goext.exerr - - - use bfcodegen (enums+id) - #### UNSURE - (?) default-priority for channels diff --git a/scnserver/api/apierr/enums.go b/scnserver/api/apierr/enums.go index fb2f7ec..a6ebe80 100644 --- a/scnserver/api/apierr/enums.go +++ b/scnserver/api/apierr/enums.go @@ -8,18 +8,19 @@ const ( NO_ERROR APIError = 0000 - MISSING_UID APIError = 1101 - MISSING_TOK APIError = 1102 - MISSING_TITLE APIError = 1103 - INVALID_PRIO APIError = 1104 - REQ_METHOD APIError = 1105 - INVALID_CLIENTTYPE APIError = 1106 - PAGETOKEN_ERROR APIError = 1121 - BINDFAIL_QUERY_PARAM APIError = 1151 - BINDFAIL_BODY_PARAM APIError = 1152 - BINDFAIL_URI_PARAM APIError = 1153 - INVALID_BODY_PARAM APIError = 1161 - INVALID_ENUM_VALUE APIError = 1171 + MISSING_UID APIError = 1101 + MISSING_TOK APIError = 1102 + MISSING_TITLE APIError = 1103 + INVALID_PRIO APIError = 1104 + REQ_METHOD APIError = 1105 + INVALID_CLIENTTYPE APIError = 1106 + PAGETOKEN_ERROR APIError = 1121 + BINDFAIL_QUERY_PARAM APIError = 1151 + BINDFAIL_BODY_PARAM APIError = 1152 + BINDFAIL_URI_PARAM APIError = 1153 + BINDFAIL_HEADER_PARAM APIError = 1152 + INVALID_BODY_PARAM APIError = 1161 + INVALID_ENUM_VALUE APIError = 1171 NO_TITLE APIError = 1201 TITLE_TOO_LONG APIError = 1202 diff --git a/scnserver/api/ginresp/resp.go b/scnserver/api/ginresp/resp.go index bb1b895..385ec4e 100644 --- a/scnserver/api/ginresp/resp.go +++ b/scnserver/api/ginresp/resp.go @@ -93,10 +93,6 @@ func SendAPIError(g *gin.Context, status int, errorid apierr.APIError, highlight return createApiError(g, "SendAPIError", status, errorid, highlight, msg, e) } -func NotImplemented(pctx ginext.PreContext) ginext.HTTPResponse { - return createApiError(g, "NotImplemented", 500, apierr.NOT_IMPLEMENTED, 0, "Not Implemented", nil) -} - func createApiError(g *gin.Context, ident string, status int, errorid apierr.APIError, highlight apihighlight.ErrHighlight, msg string, e error) ginext.HTTPResponse { reqUri := "" if g != nil && g.Request != nil { diff --git a/scnserver/api/handler/apiChannel.go b/scnserver/api/handler/apiChannel.go index 754d2a0..f3950c0 100644 --- a/scnserver/api/handler/apiChannel.go +++ b/scnserver/api/handler/apiChannel.go @@ -4,6 +4,7 @@ import ( "blackforestbytes.com/simplecloudnotifier/api/apierr" "blackforestbytes.com/simplecloudnotifier/api/ginresp" ct "blackforestbytes.com/simplecloudnotifier/db/cursortoken" + "blackforestbytes.com/simplecloudnotifier/logic" "blackforestbytes.com/simplecloudnotifier/models" "database/sql" "errors" @@ -56,67 +57,65 @@ func (h APIHandler) ListChannels(pctx ginext.PreContext) ginext.HTTPResponse { } defer ctx.Cancel() - ctx, errResp := h.app.StartRequest(g, &u, &q, nil, nil) - if errResp != nil { - return *errResp - } - defer ctx.Cancel() + return h.app.DoRequest(ctx, g, func(ctx *logic.AppContext, finishSuccess func(r ginext.HTTPResponse) ginext.HTTPResponse) ginext.HTTPResponse { - if permResp := ctx.CheckPermissionUserRead(u.UserID); permResp != nil { - return *permResp - } - - sel := strings.ToLower(langext.Coalesce(q.Selector, "owned")) - - var res []models.ChannelWithSubscriptionJSON - - if sel == "owned" { - - channels, err := h.database.ListChannelsByOwner(ctx, u.UserID, u.UserID) - if err != nil { - return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query channels", err) + if permResp := ctx.CheckPermissionUserRead(u.UserID); permResp != nil { + return *permResp } - res = langext.ArrMap(channels, func(v models.ChannelWithSubscription) models.ChannelWithSubscriptionJSON { return v.JSON(true) }) - } else if sel == "subscribed_any" { + sel := strings.ToLower(langext.Coalesce(q.Selector, "owned")) + + var res []models.ChannelWithSubscriptionJSON + + if sel == "owned" { + + channels, err := h.database.ListChannelsByOwner(ctx, u.UserID, u.UserID) + if err != nil { + return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query channels", err) + } + res = langext.ArrMap(channels, func(v models.ChannelWithSubscription) models.ChannelWithSubscriptionJSON { return v.JSON(true) }) + + } else if sel == "subscribed_any" { + + channels, err := h.database.ListChannelsBySubscriber(ctx, u.UserID, nil) + if err != nil { + return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query channels", err) + } + res = langext.ArrMap(channels, func(v models.ChannelWithSubscription) models.ChannelWithSubscriptionJSON { return v.JSON(false) }) + + } else if sel == "all_any" { + + channels, err := h.database.ListChannelsByAccess(ctx, u.UserID, nil) + if err != nil { + return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query channels", err) + } + res = langext.ArrMap(channels, func(v models.ChannelWithSubscription) models.ChannelWithSubscriptionJSON { return v.JSON(false) }) + + } else if sel == "subscribed" { + + channels, err := h.database.ListChannelsBySubscriber(ctx, u.UserID, langext.Ptr(true)) + if err != nil { + return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query channels", err) + } + res = langext.ArrMap(channels, func(v models.ChannelWithSubscription) models.ChannelWithSubscriptionJSON { return v.JSON(false) }) + + } else if sel == "all" { + + channels, err := h.database.ListChannelsByAccess(ctx, u.UserID, langext.Ptr(true)) + if err != nil { + return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query channels", err) + } + res = langext.ArrMap(channels, func(v models.ChannelWithSubscription) models.ChannelWithSubscriptionJSON { return v.JSON(false) }) + + } else { + + return ginresp.APIError(g, 400, apierr.INVALID_ENUM_VALUE, "Invalid value for the [selector] parameter", nil) - channels, err := h.database.ListChannelsBySubscriber(ctx, u.UserID, nil) - if err != nil { - return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query channels", err) } - res = langext.ArrMap(channels, func(v models.ChannelWithSubscription) models.ChannelWithSubscriptionJSON { return v.JSON(false) }) - } else if sel == "all_any" { + return finishSuccess(ginext.JSON(http.StatusOK, response{Channels: res})) - channels, err := h.database.ListChannelsByAccess(ctx, u.UserID, nil) - if err != nil { - return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query channels", err) - } - res = langext.ArrMap(channels, func(v models.ChannelWithSubscription) models.ChannelWithSubscriptionJSON { return v.JSON(false) }) - - } else if sel == "subscribed" { - - channels, err := h.database.ListChannelsBySubscriber(ctx, u.UserID, langext.Ptr(true)) - if err != nil { - return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query channels", err) - } - res = langext.ArrMap(channels, func(v models.ChannelWithSubscription) models.ChannelWithSubscriptionJSON { return v.JSON(false) }) - - } else if sel == "all" { - - channels, err := h.database.ListChannelsByAccess(ctx, u.UserID, langext.Ptr(true)) - if err != nil { - return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query channels", err) - } - res = langext.ArrMap(channels, func(v models.ChannelWithSubscription) models.ChannelWithSubscriptionJSON { return v.JSON(false) }) - - } else { - - return ginresp.APIError(g, 400, apierr.INVALID_ENUM_VALUE, "Invalid value for the [selector] parameter", nil) - - } - - return ctx.FinishSuccess(ginext.JSON(http.StatusOK, response{Channels: res})) + }) } // GetChannel swaggerdoc @@ -142,25 +141,29 @@ func (h APIHandler) GetChannel(pctx ginext.PreContext) ginext.HTTPResponse { } var u uri - ctx, g, errResp := h.app.StartRequest(pctx.URI(&u).Start()) + ctx, g, errResp := pctx.URI(&u).Start() if errResp != nil { return *errResp } defer ctx.Cancel() - if permResp := ctx.CheckPermissionUserRead(u.UserID); permResp != nil { - return *permResp - } + return h.app.DoRequest(ctx, g, func(ctx *logic.AppContext, finishSuccess func(r ginext.HTTPResponse) ginext.HTTPResponse) ginext.HTTPResponse { - channel, err := h.database.GetChannel(ctx, u.UserID, u.ChannelID, true) - if errors.Is(err, sql.ErrNoRows) { - return ginresp.APIError(g, 404, apierr.CHANNEL_NOT_FOUND, "Channel not found", err) - } - if err != nil { - return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query channel", err) - } + if permResp := ctx.CheckPermissionUserRead(u.UserID); permResp != nil { + return *permResp + } - return ctx.FinishSuccess(ginext.JSON(http.StatusOK, channel.JSON(true))) + channel, err := h.database.GetChannel(ctx, u.UserID, u.ChannelID, true) + if errors.Is(err, sql.ErrNoRows) { + return ginresp.APIError(g, 404, apierr.CHANNEL_NOT_FOUND, "Channel not found", err) + } + if err != nil { + return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query channel", err) + } + + return finishSuccess(ginext.JSON(http.StatusOK, channel.JSON(true))) + + }) } // CreateChannel swaggerdoc @@ -192,75 +195,78 @@ func (h APIHandler) CreateChannel(pctx ginext.PreContext) ginext.HTTPResponse { var u uri var b body - ctx, g, errResp := h.app.StartRequest(pctx.URI(&u).Body(&b).Start()) + ctx, g, errResp := pctx.URI(&u).Body(&b).Start() if errResp != nil { return *errResp } defer ctx.Cancel() - if permResp := ctx.CheckPermissionUserAdmin(u.UserID); permResp != nil { - return *permResp - } + return h.app.DoRequest(ctx, g, func(ctx *logic.AppContext, finishSuccess func(r ginext.HTTPResponse) ginext.HTTPResponse) ginext.HTTPResponse { - if b.Name == "" { - return ginresp.APIError(g, 400, apierr.INVALID_BODY_PARAM, "Missing parameter: name", nil) - } - - channelDisplayName := h.app.NormalizeChannelDisplayName(b.Name) - channelInternalName := h.app.NormalizeChannelInternalName(b.Name) - - channelExisting, err := h.database.GetChannelByName(ctx, u.UserID, channelInternalName) - if err != nil { - return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query channel", err) - } - - user, err := h.database.GetUser(ctx, u.UserID) - if errors.Is(err, sql.ErrNoRows) { - return ginresp.APIError(g, 400, apierr.USER_NOT_FOUND, "User not found", nil) - } - if err != nil { - return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query user", err) - } - - if len(channelDisplayName) > user.MaxChannelNameLength() { - return ginresp.APIError(g, 400, apierr.CHANNEL_TOO_LONG, fmt.Sprintf("Channel too long (max %d characters)", user.MaxChannelNameLength()), nil) - } - if len(strings.TrimSpace(channelDisplayName)) == 0 { - return ginresp.APIError(g, 400, apierr.CHANNEL_NAME_EMPTY, fmt.Sprintf("Channel displayname cannot be empty"), nil) - } - if len(channelInternalName) > user.MaxChannelNameLength() { - return ginresp.APIError(g, 400, apierr.CHANNEL_TOO_LONG, fmt.Sprintf("Channel too long (max %d characters)", user.MaxChannelNameLength()), nil) - } - if len(strings.TrimSpace(channelInternalName)) == 0 { - return ginresp.APIError(g, 400, apierr.CHANNEL_NAME_EMPTY, fmt.Sprintf("Channel internalname cannot be empty"), nil) - } - - if channelExisting != nil { - return ginresp.APIError(g, 409, apierr.CHANNEL_ALREADY_EXISTS, "Channel with this name already exists", nil) - } - - subscribeKey := h.app.GenerateRandomAuthKey() - - channel, err := h.database.CreateChannel(ctx, u.UserID, channelDisplayName, channelInternalName, subscribeKey, b.Description) - if err != nil { - return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to create channel", err) - } - - if langext.Coalesce(b.Subscribe, true) { - - sub, err := h.database.CreateSubscription(ctx, u.UserID, channel, true) - if err != nil { - return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to create subscription", err) + if permResp := ctx.CheckPermissionUserAdmin(u.UserID); permResp != nil { + return *permResp } - return ctx.FinishSuccess(ginext.JSON(http.StatusOK, channel.WithSubscription(langext.Ptr(sub)).JSON(true))) + if b.Name == "" { + return ginresp.APIError(g, 400, apierr.INVALID_BODY_PARAM, "Missing parameter: name", nil) + } - } else { + channelDisplayName := h.app.NormalizeChannelDisplayName(b.Name) + channelInternalName := h.app.NormalizeChannelInternalName(b.Name) - return ctx.FinishSuccess(ginext.JSON(http.StatusOK, channel.WithSubscription(nil).JSON(true))) + channelExisting, err := h.database.GetChannelByName(ctx, u.UserID, channelInternalName) + if err != nil { + return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query channel", err) + } - } + user, err := h.database.GetUser(ctx, u.UserID) + if errors.Is(err, sql.ErrNoRows) { + return ginresp.APIError(g, 400, apierr.USER_NOT_FOUND, "User not found", nil) + } + if err != nil { + return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query user", err) + } + if len(channelDisplayName) > user.MaxChannelNameLength() { + return ginresp.APIError(g, 400, apierr.CHANNEL_TOO_LONG, fmt.Sprintf("Channel too long (max %d characters)", user.MaxChannelNameLength()), nil) + } + if len(strings.TrimSpace(channelDisplayName)) == 0 { + return ginresp.APIError(g, 400, apierr.CHANNEL_NAME_EMPTY, fmt.Sprintf("Channel displayname cannot be empty"), nil) + } + if len(channelInternalName) > user.MaxChannelNameLength() { + return ginresp.APIError(g, 400, apierr.CHANNEL_TOO_LONG, fmt.Sprintf("Channel too long (max %d characters)", user.MaxChannelNameLength()), nil) + } + if len(strings.TrimSpace(channelInternalName)) == 0 { + return ginresp.APIError(g, 400, apierr.CHANNEL_NAME_EMPTY, fmt.Sprintf("Channel internalname cannot be empty"), nil) + } + + if channelExisting != nil { + return ginresp.APIError(g, 409, apierr.CHANNEL_ALREADY_EXISTS, "Channel with this name already exists", nil) + } + + subscribeKey := h.app.GenerateRandomAuthKey() + + channel, err := h.database.CreateChannel(ctx, u.UserID, channelDisplayName, channelInternalName, subscribeKey, b.Description) + if err != nil { + return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to create channel", err) + } + + if langext.Coalesce(b.Subscribe, true) { + + sub, err := h.database.CreateSubscription(ctx, u.UserID, channel, true) + if err != nil { + return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to create subscription", err) + } + + return finishSuccess(ginext.JSON(http.StatusOK, channel.WithSubscription(langext.Ptr(sub)).JSON(true))) + + } else { + + return finishSuccess(ginext.JSON(http.StatusOK, channel.WithSubscription(nil).JSON(true))) + + } + + }) } // UpdateChannel swaggerdoc @@ -296,84 +302,88 @@ func (h APIHandler) UpdateChannel(pctx ginext.PreContext) ginext.HTTPResponse { var u uri var b body - ctx, g, errResp := h.app.StartRequest(pctx.URI(&u).Body(&b).Start()) + ctx, g, errResp := pctx.URI(&u).Body(&b).Start() if errResp != nil { return *errResp } defer ctx.Cancel() - if permResp := ctx.CheckPermissionUserAdmin(u.UserID); permResp != nil { - return *permResp - } + return h.app.DoRequest(ctx, g, func(ctx *logic.AppContext, finishSuccess func(r ginext.HTTPResponse) ginext.HTTPResponse) ginext.HTTPResponse { - _, err := h.database.GetChannel(ctx, u.UserID, u.ChannelID, true) - if errors.Is(err, sql.ErrNoRows) { - return ginresp.APIError(g, 404, apierr.CHANNEL_NOT_FOUND, "Channel not found", err) - } - if err != nil { - return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query channel", err) - } + if permResp := ctx.CheckPermissionUserAdmin(u.UserID); permResp != nil { + return *permResp + } - user, err := h.database.GetUser(ctx, u.UserID) - if errors.Is(err, sql.ErrNoRows) { - return ginresp.APIError(g, 400, apierr.USER_NOT_FOUND, "User not found", nil) - } - if err != nil { - return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query user", err) - } - - if langext.Coalesce(b.RefreshSubscribeKey, false) { - newkey := h.app.GenerateRandomAuthKey() - - err := h.database.UpdateChannelSubscribeKey(ctx, u.ChannelID, newkey) + _, err := h.database.GetChannel(ctx, u.UserID, u.ChannelID, true) + if errors.Is(err, sql.ErrNoRows) { + return ginresp.APIError(g, 404, apierr.CHANNEL_NOT_FOUND, "Channel not found", err) + } if err != nil { - return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to update channel", err) - } - } - - if b.DisplayName != nil { - - newDisplayName := h.app.NormalizeChannelDisplayName(*b.DisplayName) - - if len(newDisplayName) > user.MaxChannelNameLength() { - return ginresp.APIError(g, 400, apierr.CHANNEL_TOO_LONG, fmt.Sprintf("Channel too long (max %d characters)", user.MaxChannelNameLength()), nil) + return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query channel", err) } - if len(strings.TrimSpace(newDisplayName)) == 0 { - return ginresp.APIError(g, 400, apierr.CHANNEL_NAME_EMPTY, fmt.Sprintf("Channel displayname cannot be empty"), nil) + user, err := h.database.GetUser(ctx, u.UserID) + if errors.Is(err, sql.ErrNoRows) { + return ginresp.APIError(g, 400, apierr.USER_NOT_FOUND, "User not found", nil) } - - err := h.database.UpdateChannelDisplayName(ctx, u.ChannelID, newDisplayName) if err != nil { - return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to update channel", err) + return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query user", err) } - } + if langext.Coalesce(b.RefreshSubscribeKey, false) { + newkey := h.app.GenerateRandomAuthKey() - if b.DescriptionName != nil { - - var descName *string = nil - if strings.TrimSpace(*b.DescriptionName) != "" { - descName = langext.Ptr(strings.TrimSpace(*b.DescriptionName)) + err := h.database.UpdateChannelSubscribeKey(ctx, u.ChannelID, newkey) + if err != nil { + return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to update channel", err) + } } - if descName != nil && len(*descName) > user.MaxChannelDescriptionLength() { - return ginresp.APIError(g, 400, apierr.CHANNEL_DESCRIPTION_TOO_LONG, fmt.Sprintf("Channel-Description too long (max %d characters)", user.MaxChannelDescriptionLength()), nil) + if b.DisplayName != nil { + + newDisplayName := h.app.NormalizeChannelDisplayName(*b.DisplayName) + + if len(newDisplayName) > user.MaxChannelNameLength() { + return ginresp.APIError(g, 400, apierr.CHANNEL_TOO_LONG, fmt.Sprintf("Channel too long (max %d characters)", user.MaxChannelNameLength()), nil) + } + + if len(strings.TrimSpace(newDisplayName)) == 0 { + return ginresp.APIError(g, 400, apierr.CHANNEL_NAME_EMPTY, fmt.Sprintf("Channel displayname cannot be empty"), nil) + } + + err := h.database.UpdateChannelDisplayName(ctx, u.ChannelID, newDisplayName) + if err != nil { + return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to update channel", err) + } + } - err := h.database.UpdateChannelDescriptionName(ctx, u.ChannelID, descName) + if b.DescriptionName != nil { + + var descName *string = nil + if strings.TrimSpace(*b.DescriptionName) != "" { + descName = langext.Ptr(strings.TrimSpace(*b.DescriptionName)) + } + + if descName != nil && len(*descName) > user.MaxChannelDescriptionLength() { + return ginresp.APIError(g, 400, apierr.CHANNEL_DESCRIPTION_TOO_LONG, fmt.Sprintf("Channel-Description too long (max %d characters)", user.MaxChannelDescriptionLength()), nil) + } + + err := h.database.UpdateChannelDescriptionName(ctx, u.ChannelID, descName) + if err != nil { + return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to update channel", err) + } + + } + + channel, err := h.database.GetChannel(ctx, u.UserID, u.ChannelID, true) if err != nil { - return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to update channel", err) + return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query (updated) channel", err) } - } + return finishSuccess(ginext.JSON(http.StatusOK, channel.JSON(true))) - channel, err := h.database.GetChannel(ctx, u.UserID, u.ChannelID, true) - if err != nil { - return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query (updated) channel", err) - } - - return ctx.FinishSuccess(ginext.JSON(http.StatusOK, channel.JSON(true))) + }) } // ListChannelMessages swaggerdoc @@ -416,50 +426,54 @@ func (h APIHandler) ListChannelMessages(pctx ginext.PreContext) ginext.HTTPRespo var u uri var q query - ctx, g, errResp := h.app.StartRequest(pctx.URI(&u).Query(&q).Start()) + ctx, g, errResp := pctx.URI(&u).Query(&q).Start() if errResp != nil { return *errResp } defer ctx.Cancel() - trimmed := langext.Coalesce(q.Trimmed, true) + return h.app.DoRequest(ctx, g, func(ctx *logic.AppContext, finishSuccess func(r ginext.HTTPResponse) ginext.HTTPResponse) ginext.HTTPResponse { - maxPageSize := langext.Conditional(trimmed, 16, 256) + trimmed := langext.Coalesce(q.Trimmed, true) - pageSize := mathext.Clamp(langext.Coalesce(q.PageSize, 64), 1, maxPageSize) + maxPageSize := langext.Conditional(trimmed, 16, 256) - channel, err := h.database.GetChannel(ctx, u.ChannelUserID, u.ChannelID, false) - if errors.Is(err, sql.ErrNoRows) { - return ginresp.APIError(g, 404, apierr.CHANNEL_NOT_FOUND, "Channel not found", err) - } - if err != nil { - return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query channel", err) - } + pageSize := mathext.Clamp(langext.Coalesce(q.PageSize, 64), 1, maxPageSize) - if permResp := ctx.CheckPermissionChanMessagesRead(channel.Channel); permResp != nil { - return *permResp - } + channel, err := h.database.GetChannel(ctx, u.ChannelUserID, u.ChannelID, false) + if errors.Is(err, sql.ErrNoRows) { + return ginresp.APIError(g, 404, apierr.CHANNEL_NOT_FOUND, "Channel not found", err) + } + if err != nil { + return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query channel", err) + } - tok, err := ct.Decode(langext.Coalesce(q.NextPageToken, "")) - if err != nil { - return ginresp.APIError(g, 400, apierr.PAGETOKEN_ERROR, "Failed to decode next_page_token", err) - } + if permResp := ctx.CheckPermissionChanMessagesRead(channel.Channel); permResp != nil { + return *permResp + } - filter := models.MessageFilter{ - ChannelID: langext.Ptr([]models.ChannelID{channel.ChannelID}), - } + tok, err := ct.Decode(langext.Coalesce(q.NextPageToken, "")) + if err != nil { + return ginresp.APIError(g, 400, apierr.PAGETOKEN_ERROR, "Failed to decode next_page_token", err) + } - messages, npt, err := h.database.ListMessages(ctx, filter, &pageSize, tok) - if err != nil { - return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query messages", err) - } + filter := models.MessageFilter{ + ChannelID: langext.Ptr([]models.ChannelID{channel.ChannelID}), + } - var res []models.MessageJSON - if trimmed { - res = langext.ArrMap(messages, func(v models.Message) models.MessageJSON { return v.TrimmedJSON() }) - } else { - res = langext.ArrMap(messages, func(v models.Message) models.MessageJSON { return v.FullJSON() }) - } + messages, npt, err := h.database.ListMessages(ctx, filter, &pageSize, tok) + if err != nil { + return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query messages", err) + } - return ctx.FinishSuccess(ginext.JSON(http.StatusOK, response{Messages: res, NextPageToken: npt.Token(), PageSize: pageSize})) + var res []models.MessageJSON + if trimmed { + res = langext.ArrMap(messages, func(v models.Message) models.MessageJSON { return v.TrimmedJSON() }) + } else { + res = langext.ArrMap(messages, func(v models.Message) models.MessageJSON { return v.FullJSON() }) + } + + return finishSuccess(ginext.JSON(http.StatusOK, response{Messages: res, NextPageToken: npt.Token(), PageSize: pageSize})) + + }) } diff --git a/scnserver/api/handler/apiClient.go b/scnserver/api/handler/apiClient.go index b672789..94f9414 100644 --- a/scnserver/api/handler/apiClient.go +++ b/scnserver/api/handler/apiClient.go @@ -3,6 +3,7 @@ package handler import ( "blackforestbytes.com/simplecloudnotifier/api/apierr" "blackforestbytes.com/simplecloudnotifier/api/ginresp" + "blackforestbytes.com/simplecloudnotifier/logic" "blackforestbytes.com/simplecloudnotifier/models" "database/sql" "errors" @@ -34,24 +35,28 @@ func (h APIHandler) ListClients(pctx ginext.PreContext) ginext.HTTPResponse { } var u uri - ctx, g, errResp := h.app.StartRequest(pctx.URI(&u).Start()) + ctx, g, errResp := pctx.URI(&u).Start() if errResp != nil { return *errResp } defer ctx.Cancel() - if permResp := ctx.CheckPermissionUserRead(u.UserID); permResp != nil { - return *permResp - } + return h.app.DoRequest(ctx, g, func(ctx *logic.AppContext, finishSuccess func(r ginext.HTTPResponse) ginext.HTTPResponse) ginext.HTTPResponse { - clients, err := h.database.ListClients(ctx, u.UserID) - if err != nil { - return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query clients", err) - } + if permResp := ctx.CheckPermissionUserRead(u.UserID); permResp != nil { + return *permResp + } - res := langext.ArrMap(clients, func(v models.Client) models.ClientJSON { return v.JSON() }) + clients, err := h.database.ListClients(ctx, u.UserID) + if err != nil { + return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query clients", err) + } - return ctx.FinishSuccess(ginext.JSON(http.StatusOK, response{Clients: res})) + res := langext.ArrMap(clients, func(v models.Client) models.ClientJSON { return v.JSON() }) + + return finishSuccess(ginext.JSON(http.StatusOK, response{Clients: res})) + + }) } // GetClient swaggerdoc @@ -77,25 +82,29 @@ func (h APIHandler) GetClient(pctx ginext.PreContext) ginext.HTTPResponse { } var u uri - ctx, g, errResp := h.app.StartRequest(pctx.URI(&u).Start()) + ctx, g, errResp := pctx.URI(&u).Start() if errResp != nil { return *errResp } defer ctx.Cancel() - if permResp := ctx.CheckPermissionUserRead(u.UserID); permResp != nil { - return *permResp - } + return h.app.DoRequest(ctx, g, func(ctx *logic.AppContext, finishSuccess func(r ginext.HTTPResponse) ginext.HTTPResponse) ginext.HTTPResponse { - client, err := h.database.GetClient(ctx, u.UserID, u.ClientID) - if errors.Is(err, sql.ErrNoRows) { - return ginresp.APIError(g, 404, apierr.CLIENT_NOT_FOUND, "Client not found", err) - } - if err != nil { - return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query client", err) - } + if permResp := ctx.CheckPermissionUserRead(u.UserID); permResp != nil { + return *permResp + } - return ctx.FinishSuccess(ginext.JSON(http.StatusOK, client.JSON())) + client, err := h.database.GetClient(ctx, u.UserID, u.ClientID) + if errors.Is(err, sql.ErrNoRows) { + return ginresp.APIError(g, 404, apierr.CLIENT_NOT_FOUND, "Client not found", err) + } + if err != nil { + return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query client", err) + } + + return finishSuccess(ginext.JSON(http.StatusOK, client.JSON())) + + }) } // AddClient swaggerdoc @@ -128,32 +137,36 @@ func (h APIHandler) AddClient(pctx ginext.PreContext) ginext.HTTPResponse { var u uri var b body - ctx, g, errResp := h.app.StartRequest(pctx.URI(&u).Body(&b).Start()) + ctx, g, errResp := pctx.URI(&u).Body(&b).Start() if errResp != nil { return *errResp } defer ctx.Cancel() - if !b.ClientType.Valid() { - return ginresp.APIError(g, 400, apierr.INVALID_CLIENTTYPE, "Invalid ClientType", nil) - } - clientType := b.ClientType + return h.app.DoRequest(ctx, g, func(ctx *logic.AppContext, finishSuccess func(r ginext.HTTPResponse) ginext.HTTPResponse) ginext.HTTPResponse { - if permResp := ctx.CheckPermissionUserAdmin(u.UserID); permResp != nil { - return *permResp - } + if !b.ClientType.Valid() { + return ginresp.APIError(g, 400, apierr.INVALID_CLIENTTYPE, "Invalid ClientType", nil) + } + clientType := b.ClientType - err := h.database.DeleteClientsByFCM(ctx, b.FCMToken) - if err != nil { - return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to delete existing clients in db", err) - } + if permResp := ctx.CheckPermissionUserAdmin(u.UserID); permResp != nil { + return *permResp + } - client, err := h.database.CreateClient(ctx, u.UserID, clientType, b.FCMToken, b.AgentModel, b.AgentVersion, b.Name) - if err != nil { - return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to create client in db", err) - } + err := h.database.DeleteClientsByFCM(ctx, b.FCMToken) + if err != nil { + return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to delete existing clients in db", err) + } - return ctx.FinishSuccess(ginext.JSON(http.StatusOK, client.JSON())) + client, err := h.database.CreateClient(ctx, u.UserID, clientType, b.FCMToken, b.AgentModel, b.AgentVersion, b.Name) + if err != nil { + return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to create client in db", err) + } + + return finishSuccess(ginext.JSON(http.StatusOK, client.JSON())) + + }) } // DeleteClient swaggerdoc @@ -179,30 +192,34 @@ func (h APIHandler) DeleteClient(pctx ginext.PreContext) ginext.HTTPResponse { } var u uri - ctx, g, errResp := h.app.StartRequest(pctx.URI(&u).Start()) + ctx, g, errResp := pctx.URI(&u).Start() if errResp != nil { return *errResp } defer ctx.Cancel() - if permResp := ctx.CheckPermissionUserAdmin(u.UserID); permResp != nil { - return *permResp - } + return h.app.DoRequest(ctx, g, func(ctx *logic.AppContext, finishSuccess func(r ginext.HTTPResponse) ginext.HTTPResponse) ginext.HTTPResponse { - client, err := h.database.GetClient(ctx, u.UserID, u.ClientID) - if errors.Is(err, sql.ErrNoRows) { - return ginresp.APIError(g, 404, apierr.CLIENT_NOT_FOUND, "Client not found", err) - } - if err != nil { - return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query client", err) - } + if permResp := ctx.CheckPermissionUserAdmin(u.UserID); permResp != nil { + return *permResp + } - err = h.database.DeleteClient(ctx, u.ClientID) - if err != nil { - return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to delete client", err) - } + client, err := h.database.GetClient(ctx, u.UserID, u.ClientID) + if errors.Is(err, sql.ErrNoRows) { + return ginresp.APIError(g, 404, apierr.CLIENT_NOT_FOUND, "Client not found", err) + } + if err != nil { + return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query client", err) + } - return ctx.FinishSuccess(ginext.JSON(http.StatusOK, client.JSON())) + err = h.database.DeleteClient(ctx, u.ClientID) + if err != nil { + return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to delete client", err) + } + + return finishSuccess(ginext.JSON(http.StatusOK, client.JSON())) + + }) } // UpdateClient swaggerdoc @@ -239,69 +256,73 @@ func (h APIHandler) UpdateClient(pctx ginext.PreContext) ginext.HTTPResponse { var u uri var b body - ctx, g, errResp := h.app.StartRequest(pctx.URI(&u).Body(&b).Start()) + ctx, g, errResp := pctx.URI(&u).Body(&b).Start() if errResp != nil { return *errResp } defer ctx.Cancel() - if permResp := ctx.CheckPermissionUserAdmin(u.UserID); permResp != nil { - return *permResp - } + return h.app.DoRequest(ctx, g, func(ctx *logic.AppContext, finishSuccess func(r ginext.HTTPResponse) ginext.HTTPResponse) ginext.HTTPResponse { - client, err := h.database.GetClient(ctx, u.UserID, u.ClientID) - if errors.Is(err, sql.ErrNoRows) { - return ginresp.APIError(g, 404, apierr.CLIENT_NOT_FOUND, "Client not found", err) - } - if err != nil { - return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query client", err) - } - - if b.FCMToken != nil && *b.FCMToken != client.FCMToken { - - err = h.database.DeleteClientsByFCM(ctx, *b.FCMToken) - if err != nil { - return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to delete existing clients in db", err) + if permResp := ctx.CheckPermissionUserAdmin(u.UserID); permResp != nil { + return *permResp } - err = h.database.UpdateClientFCMToken(ctx, u.ClientID, *b.FCMToken) - if err != nil { - return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to update client", err) + client, err := h.database.GetClient(ctx, u.UserID, u.ClientID) + if errors.Is(err, sql.ErrNoRows) { + return ginresp.APIError(g, 404, apierr.CLIENT_NOT_FOUND, "Client not found", err) } - } - - if b.AgentModel != nil { - err = h.database.UpdateClientAgentModel(ctx, u.ClientID, *b.AgentModel) if err != nil { - return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to update client", err) + return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query client", err) } - } - if b.AgentVersion != nil { - err = h.database.UpdateClientAgentVersion(ctx, u.ClientID, *b.AgentVersion) - if err != nil { - return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to update client", err) - } - } + if b.FCMToken != nil && *b.FCMToken != client.FCMToken { - if b.Name != nil { - if *b.Name == "" { - err = h.database.UpdateClientDescriptionName(ctx, u.ClientID, nil) + err = h.database.DeleteClientsByFCM(ctx, *b.FCMToken) if err != nil { - return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to update client", err) + return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to delete existing clients in db", err) } - } else { - err = h.database.UpdateClientDescriptionName(ctx, u.ClientID, langext.Ptr(*b.Name)) + + err = h.database.UpdateClientFCMToken(ctx, u.ClientID, *b.FCMToken) if err != nil { return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to update client", err) } } - } - client, err = h.database.GetClient(ctx, u.UserID, u.ClientID) - if err != nil { - return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query (updated) client", err) - } + if b.AgentModel != nil { + err = h.database.UpdateClientAgentModel(ctx, u.ClientID, *b.AgentModel) + if err != nil { + return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to update client", err) + } + } - return ctx.FinishSuccess(ginext.JSON(http.StatusOK, client.JSON())) + if b.AgentVersion != nil { + err = h.database.UpdateClientAgentVersion(ctx, u.ClientID, *b.AgentVersion) + if err != nil { + return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to update client", err) + } + } + + if b.Name != nil { + if *b.Name == "" { + err = h.database.UpdateClientDescriptionName(ctx, u.ClientID, nil) + if err != nil { + return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to update client", err) + } + } else { + err = h.database.UpdateClientDescriptionName(ctx, u.ClientID, langext.Ptr(*b.Name)) + if err != nil { + return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to update client", err) + } + } + } + + client, err = h.database.GetClient(ctx, u.UserID, u.ClientID) + if err != nil { + return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query (updated) client", err) + } + + return finishSuccess(ginext.JSON(http.StatusOK, client.JSON())) + + }) } diff --git a/scnserver/api/handler/apiKeyToken.go b/scnserver/api/handler/apiKeyToken.go index 636679e..69ea96f 100644 --- a/scnserver/api/handler/apiKeyToken.go +++ b/scnserver/api/handler/apiKeyToken.go @@ -3,6 +3,7 @@ package handler import ( "blackforestbytes.com/simplecloudnotifier/api/apierr" "blackforestbytes.com/simplecloudnotifier/api/ginresp" + "blackforestbytes.com/simplecloudnotifier/logic" "blackforestbytes.com/simplecloudnotifier/models" "database/sql" "errors" @@ -36,24 +37,28 @@ func (h APIHandler) ListUserKeys(pctx ginext.PreContext) ginext.HTTPResponse { } var u uri - ctx, g, errResp := h.app.StartRequest(pctx.URI(&u).Start()) + ctx, g, errResp := pctx.URI(&u).Start() if errResp != nil { return *errResp } defer ctx.Cancel() - if permResp := ctx.CheckPermissionUserAdmin(u.UserID); permResp != nil { - return *permResp - } + return h.app.DoRequest(ctx, g, func(ctx *logic.AppContext, finishSuccess func(r ginext.HTTPResponse) ginext.HTTPResponse) ginext.HTTPResponse { - toks, err := h.database.ListKeyTokens(ctx, u.UserID) - if err != nil { - return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query keys", err) - } + if permResp := ctx.CheckPermissionUserAdmin(u.UserID); permResp != nil { + return *permResp + } - res := langext.ArrMap(toks, func(v models.KeyToken) models.KeyTokenJSON { return v.JSON() }) + toks, err := h.database.ListKeyTokens(ctx, u.UserID) + if err != nil { + return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query keys", err) + } - return ctx.FinishSuccess(ginext.JSON(http.StatusOK, response{Keys: res})) + res := langext.ArrMap(toks, func(v models.KeyToken) models.KeyTokenJSON { return v.JSON() }) + + return finishSuccess(ginext.JSON(http.StatusOK, response{Keys: res})) + + }) } // GetCurrentUserKey swaggerdoc @@ -79,30 +84,34 @@ func (h APIHandler) GetCurrentUserKey(pctx ginext.PreContext) ginext.HTTPRespons } var u uri - ctx, g, errResp := h.app.StartRequest(pctx.URI(&u).Start()) + ctx, g, errResp := pctx.URI(&u).Start() if errResp != nil { return *errResp } defer ctx.Cancel() - if permResp := ctx.CheckPermissionAny(); permResp != nil { - return *permResp - } + return h.app.DoRequest(ctx, g, func(ctx *logic.AppContext, finishSuccess func(r ginext.HTTPResponse) ginext.HTTPResponse) ginext.HTTPResponse { - tokid := ctx.GetPermissionKeyTokenID() - if tokid == nil { - return ginresp.APIError(g, 400, apierr.USER_AUTH_FAILED, "Missing KeyTokenID in context", nil) - } + if permResp := ctx.CheckPermissionAny(); permResp != nil { + return *permResp + } - keytoken, err := h.database.GetKeyToken(ctx, u.UserID, *tokid) - if errors.Is(err, sql.ErrNoRows) { - return ginresp.APIError(g, 404, apierr.KEY_NOT_FOUND, "Key not found", err) - } - if err != nil { - return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query client", err) - } + tokid := ctx.GetPermissionKeyTokenID() + if tokid == nil { + return ginresp.APIError(g, 400, apierr.USER_AUTH_FAILED, "Missing KeyTokenID in context", nil) + } - return ctx.FinishSuccess(ginext.JSON(http.StatusOK, keytoken.JSON().WithToken(keytoken.Token))) + keytoken, err := h.database.GetKeyToken(ctx, u.UserID, *tokid) + if errors.Is(err, sql.ErrNoRows) { + return ginresp.APIError(g, 404, apierr.KEY_NOT_FOUND, "Key not found", err) + } + if err != nil { + return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query client", err) + } + + return finishSuccess(ginext.JSON(http.StatusOK, keytoken.JSON().WithToken(keytoken.Token))) + + }) } // GetUserKey swaggerdoc @@ -129,25 +138,29 @@ func (h APIHandler) GetUserKey(pctx ginext.PreContext) ginext.HTTPResponse { } var u uri - ctx, g, errResp := h.app.StartRequest(pctx.URI(&u).Start()) + ctx, g, errResp := pctx.URI(&u).Start() if errResp != nil { return *errResp } defer ctx.Cancel() - if permResp := ctx.CheckPermissionUserAdmin(u.UserID); permResp != nil { - return *permResp - } + return h.app.DoRequest(ctx, g, func(ctx *logic.AppContext, finishSuccess func(r ginext.HTTPResponse) ginext.HTTPResponse) ginext.HTTPResponse { - keytoken, err := h.database.GetKeyToken(ctx, u.UserID, u.KeyID) - if errors.Is(err, sql.ErrNoRows) { - return ginresp.APIError(g, 404, apierr.KEY_NOT_FOUND, "Key not found", err) - } - if err != nil { - return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query client", err) - } + if permResp := ctx.CheckPermissionUserAdmin(u.UserID); permResp != nil { + return *permResp + } - return ctx.FinishSuccess(ginext.JSON(http.StatusOK, keytoken.JSON())) + keytoken, err := h.database.GetKeyToken(ctx, u.UserID, u.KeyID) + if errors.Is(err, sql.ErrNoRows) { + return ginresp.APIError(g, 404, apierr.KEY_NOT_FOUND, "Key not found", err) + } + if err != nil { + return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query client", err) + } + + return finishSuccess(ginext.JSON(http.StatusOK, keytoken.JSON())) + + }) } // UpdateUserKey swaggerdoc @@ -182,70 +195,74 @@ func (h APIHandler) UpdateUserKey(pctx ginext.PreContext) ginext.HTTPResponse { var u uri var b body - ctx, g, errResp := h.app.StartRequest(pctx.URI(&u).Body(&b).Start()) + ctx, g, errResp := pctx.URI(&u).Body(&b).Start() if errResp != nil { return *errResp } defer ctx.Cancel() - if permResp := ctx.CheckPermissionUserAdmin(u.UserID); permResp != nil { - return *permResp - } + return h.app.DoRequest(ctx, g, func(ctx *logic.AppContext, finishSuccess func(r ginext.HTTPResponse) ginext.HTTPResponse) ginext.HTTPResponse { - keytoken, err := h.database.GetKeyToken(ctx, u.UserID, u.KeyID) - if errors.Is(err, sql.ErrNoRows) { - return ginresp.APIError(g, 404, apierr.KEY_NOT_FOUND, "Key not found", err) - } - if err != nil { - return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query client", err) - } + if permResp := ctx.CheckPermissionUserAdmin(u.UserID); permResp != nil { + return *permResp + } - if b.Name != nil { - err := h.database.UpdateKeyTokenName(ctx, u.KeyID, *b.Name) + keytoken, err := h.database.GetKeyToken(ctx, u.UserID, u.KeyID) + if errors.Is(err, sql.ErrNoRows) { + return ginresp.APIError(g, 404, apierr.KEY_NOT_FOUND, "Key not found", err) + } if err != nil { - return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to update name", err) - } - keytoken.Name = *b.Name - } - - if b.Permissions != nil { - if keytoken.KeyTokenID == *ctx.GetPermissionKeyTokenID() { - return ginresp.APIError(g, 400, apierr.CANNOT_SELFUPDATE_KEY, "Cannot update the currently used key", err) + return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query client", err) } - permlist := models.ParseTokenPermissionList(*b.Permissions) - err := h.database.UpdateKeyTokenPermissions(ctx, u.KeyID, permlist) - if err != nil { - return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to update permissions", err) - } - keytoken.Permissions = permlist - } - - if b.AllChannels != nil { - if keytoken.KeyTokenID == *ctx.GetPermissionKeyTokenID() { - return ginresp.APIError(g, 400, apierr.CANNOT_SELFUPDATE_KEY, "Cannot update the currently used key", err) + if b.Name != nil { + err := h.database.UpdateKeyTokenName(ctx, u.KeyID, *b.Name) + if err != nil { + return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to update name", err) + } + keytoken.Name = *b.Name } - err := h.database.UpdateKeyTokenAllChannels(ctx, u.KeyID, *b.AllChannels) - if err != nil { - return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to update all_channels", err) - } - keytoken.AllChannels = *b.AllChannels - } + if b.Permissions != nil { + if keytoken.KeyTokenID == *ctx.GetPermissionKeyTokenID() { + return ginresp.APIError(g, 400, apierr.CANNOT_SELFUPDATE_KEY, "Cannot update the currently used key", err) + } - if b.Channels != nil { - if keytoken.KeyTokenID == *ctx.GetPermissionKeyTokenID() { - return ginresp.APIError(g, 400, apierr.CANNOT_SELFUPDATE_KEY, "Cannot update the currently used key", err) + permlist := models.ParseTokenPermissionList(*b.Permissions) + err := h.database.UpdateKeyTokenPermissions(ctx, u.KeyID, permlist) + if err != nil { + return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to update permissions", err) + } + keytoken.Permissions = permlist } - err := h.database.UpdateKeyTokenChannels(ctx, u.KeyID, *b.Channels) - if err != nil { - return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to update channels", err) - } - keytoken.Channels = *b.Channels - } + if b.AllChannels != nil { + if keytoken.KeyTokenID == *ctx.GetPermissionKeyTokenID() { + return ginresp.APIError(g, 400, apierr.CANNOT_SELFUPDATE_KEY, "Cannot update the currently used key", err) + } - return ctx.FinishSuccess(ginext.JSON(http.StatusOK, keytoken.JSON())) + err := h.database.UpdateKeyTokenAllChannels(ctx, u.KeyID, *b.AllChannels) + if err != nil { + return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to update all_channels", err) + } + keytoken.AllChannels = *b.AllChannels + } + + if b.Channels != nil { + if keytoken.KeyTokenID == *ctx.GetPermissionKeyTokenID() { + return ginresp.APIError(g, 400, apierr.CANNOT_SELFUPDATE_KEY, "Cannot update the currently used key", err) + } + + err := h.database.UpdateKeyTokenChannels(ctx, u.KeyID, *b.Channels) + if err != nil { + return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to update channels", err) + } + keytoken.Channels = *b.Channels + } + + return finishSuccess(ginext.JSON(http.StatusOK, keytoken.JSON())) + + }) } // CreateUserKey swaggerdoc @@ -278,43 +295,47 @@ func (h APIHandler) CreateUserKey(pctx ginext.PreContext) ginext.HTTPResponse { var u uri var b body - ctx, g, errResp := h.app.StartRequest(pctx.URI(&u).Body(&b).Start()) + ctx, g, errResp := pctx.URI(&u).Body(&b).Start() if errResp != nil { return *errResp } defer ctx.Cancel() - channels := langext.Coalesce(b.Channels, make([]models.ChannelID, 0)) + return h.app.DoRequest(ctx, g, func(ctx *logic.AppContext, finishSuccess func(r ginext.HTTPResponse) ginext.HTTPResponse) ginext.HTTPResponse { - var allChan bool - if b.AllChannels == nil && b.Channels != nil { - allChan = false - } else if b.AllChannels == nil && b.Channels == nil { - allChan = true - } else { - allChan = *b.AllChannels - } + channels := langext.Coalesce(b.Channels, make([]models.ChannelID, 0)) - for _, c := range channels { - if err := c.Valid(); err != nil { - return ginresp.APIError(g, 400, apierr.INVALID_BODY_PARAM, "Invalid ChannelID", err) + var allChan bool + if b.AllChannels == nil && b.Channels != nil { + allChan = false + } else if b.AllChannels == nil && b.Channels == nil { + allChan = true + } else { + allChan = *b.AllChannels } - } - if permResp := ctx.CheckPermissionUserAdmin(u.UserID); permResp != nil { - return *permResp - } + for _, c := range channels { + if err := c.Valid(); err != nil { + return ginresp.APIError(g, 400, apierr.INVALID_BODY_PARAM, "Invalid ChannelID", err) + } + } - token := h.app.GenerateRandomAuthKey() + if permResp := ctx.CheckPermissionUserAdmin(u.UserID); permResp != nil { + return *permResp + } - perms := models.ParseTokenPermissionList(b.Permissions) + token := h.app.GenerateRandomAuthKey() - keytok, err := h.database.CreateKeyToken(ctx, b.Name, *ctx.GetPermissionUserID(), allChan, channels, perms, token) - if err != nil { - return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to create keytoken in db", err) - } + perms := models.ParseTokenPermissionList(b.Permissions) - return ctx.FinishSuccess(ginext.JSON(http.StatusOK, keytok.JSON().WithToken(token))) + keytok, err := h.database.CreateKeyToken(ctx, b.Name, *ctx.GetPermissionUserID(), allChan, channels, perms, token) + if err != nil { + return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to create keytoken in db", err) + } + + return finishSuccess(ginext.JSON(http.StatusOK, keytok.JSON().WithToken(token))) + + }) } // DeleteUserKey swaggerdoc @@ -341,32 +362,36 @@ func (h APIHandler) DeleteUserKey(pctx ginext.PreContext) ginext.HTTPResponse { } var u uri - ctx, g, errResp := h.app.StartRequest(pctx.URI(&u).Start()) + ctx, g, errResp := pctx.URI(&u).Start() if errResp != nil { return *errResp } defer ctx.Cancel() - if permResp := ctx.CheckPermissionUserAdmin(u.UserID); permResp != nil { - return *permResp - } + return h.app.DoRequest(ctx, g, func(ctx *logic.AppContext, finishSuccess func(r ginext.HTTPResponse) ginext.HTTPResponse) ginext.HTTPResponse { - client, err := h.database.GetKeyToken(ctx, u.UserID, u.KeyID) - if errors.Is(err, sql.ErrNoRows) { - return ginresp.APIError(g, 404, apierr.KEY_NOT_FOUND, "Key not found", err) - } - if err != nil { - return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query client", err) - } + if permResp := ctx.CheckPermissionUserAdmin(u.UserID); permResp != nil { + return *permResp + } - if u.KeyID == *ctx.GetPermissionKeyTokenID() { - return ginresp.APIError(g, 400, apierr.CANNOT_SELFDELETE_KEY, "Cannot delete the currently used key", err) - } + client, err := h.database.GetKeyToken(ctx, u.UserID, u.KeyID) + if errors.Is(err, sql.ErrNoRows) { + return ginresp.APIError(g, 404, apierr.KEY_NOT_FOUND, "Key not found", err) + } + if err != nil { + return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query client", err) + } - err = h.database.DeleteKeyToken(ctx, u.KeyID) - if err != nil { - return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to delete client", err) - } + if u.KeyID == *ctx.GetPermissionKeyTokenID() { + return ginresp.APIError(g, 400, apierr.CANNOT_SELFDELETE_KEY, "Cannot delete the currently used key", err) + } - return ctx.FinishSuccess(ginext.JSON(http.StatusOK, client.JSON())) + err = h.database.DeleteKeyToken(ctx, u.KeyID) + if err != nil { + return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to delete client", err) + } + + return finishSuccess(ginext.JSON(http.StatusOK, client.JSON())) + + }) } diff --git a/scnserver/api/handler/apiMessage.go b/scnserver/api/handler/apiMessage.go index 10f8e47..79392ec 100644 --- a/scnserver/api/handler/apiMessage.go +++ b/scnserver/api/handler/apiMessage.go @@ -1,6 +1,7 @@ package handler import ( + "blackforestbytes.com/simplecloudnotifier/logic" "database/sql" "errors" "gogs.mikescher.com/BlackForestBytes/goext/ginext" @@ -55,107 +56,111 @@ func (h APIHandler) ListMessages(pctx ginext.PreContext) ginext.HTTPResponse { } var q query - ctx, errResp := h.app.StartRequest(g, nil, &q, nil, nil) + ctx, g, errResp := pctx.Query(&q).Start() if errResp != nil { return *errResp } defer ctx.Cancel() - trimmed := langext.Coalesce(q.Trimmed, true) + return h.app.DoRequest(ctx, g, func(ctx *logic.AppContext, finishSuccess func(r ginext.HTTPResponse) ginext.HTTPResponse) ginext.HTTPResponse { - maxPageSize := langext.Conditional(trimmed, 16, 256) + trimmed := langext.Coalesce(q.Trimmed, true) - pageSize := mathext.Clamp(langext.Coalesce(q.PageSize, 64), 1, maxPageSize) + maxPageSize := langext.Conditional(trimmed, 16, 256) - if permResp := ctx.CheckPermissionSelfAllMessagesRead(); permResp != nil { - return *permResp - } + pageSize := mathext.Clamp(langext.Coalesce(q.PageSize, 64), 1, maxPageSize) - userid := *ctx.GetPermissionUserID() - - tok, err := ct.Decode(langext.Coalesce(q.NextPageToken, "")) - if err != nil { - return ginresp.APIError(g, 400, apierr.PAGETOKEN_ERROR, "Failed to decode next_page_token", err) - } - - err = h.database.UpdateUserLastRead(ctx, userid) - if err != nil { - return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to update last-read", err) - } - - filter := models.MessageFilter{ - ConfirmedSubscriptionBy: langext.Ptr(userid), - } - - if q.Filter != nil && strings.TrimSpace(*q.Filter) != "" { - filter.SearchString = langext.Ptr([]string{strings.TrimSpace(*q.Filter)}) - } - - if len(q.Channels) != 0 { - filter.ChannelNameCS = langext.Ptr(q.Channels) - } - - if len(q.ChannelIDs) != 0 { - cids := make([]models.ChannelID, 0, len(q.ChannelIDs)) - for _, v := range q.ChannelIDs { - cid := models.ChannelID(v) - if err = cid.Valid(); err != nil { - return ginresp.APIError(g, 400, apierr.BINDFAIL_QUERY_PARAM, "Invalid channel-id", err) - } - cids = append(cids, cid) + if permResp := ctx.CheckPermissionSelfAllMessagesRead(); permResp != nil { + return *permResp } - filter.ChannelID = &cids - } - if len(q.Senders) != 0 { - filter.SenderNameCS = langext.Ptr(q.Senders) - } + userid := *ctx.GetPermissionUserID() - if q.TimeBefore != nil { - t0, err := time.Parse(time.RFC3339, *q.TimeBefore) + tok, err := ct.Decode(langext.Coalesce(q.NextPageToken, "")) if err != nil { - return ginresp.APIError(g, 400, apierr.BINDFAIL_QUERY_PARAM, "Invalid before-time", err) + return ginresp.APIError(g, 400, apierr.PAGETOKEN_ERROR, "Failed to decode next_page_token", err) } - filter.TimestampCoalesceBefore = &t0 - } - if q.TimeAfter != nil { - t0, err := time.Parse(time.RFC3339, *q.TimeAfter) + err = h.database.UpdateUserLastRead(ctx, userid) if err != nil { - return ginresp.APIError(g, 400, apierr.BINDFAIL_QUERY_PARAM, "Invalid after-time", err) + return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to update last-read", err) } - filter.TimestampCoalesceAfter = &t0 - } - if len(q.Priority) != 0 { - filter.Priority = langext.Ptr(q.Priority) - } + filter := models.MessageFilter{ + ConfirmedSubscriptionBy: langext.Ptr(userid), + } - if len(q.KeyTokens) != 0 { - tids := make([]models.KeyTokenID, 0, len(q.KeyTokens)) - for _, v := range q.KeyTokens { - tid := models.KeyTokenID(v) - if err = tid.Valid(); err != nil { - return ginresp.APIError(g, 400, apierr.BINDFAIL_QUERY_PARAM, "Invalid keytoken-id", err) + if q.Filter != nil && strings.TrimSpace(*q.Filter) != "" { + filter.SearchString = langext.Ptr([]string{strings.TrimSpace(*q.Filter)}) + } + + if len(q.Channels) != 0 { + filter.ChannelNameCS = langext.Ptr(q.Channels) + } + + if len(q.ChannelIDs) != 0 { + cids := make([]models.ChannelID, 0, len(q.ChannelIDs)) + for _, v := range q.ChannelIDs { + cid := models.ChannelID(v) + if err = cid.Valid(); err != nil { + return ginresp.APIError(g, 400, apierr.BINDFAIL_QUERY_PARAM, "Invalid channel-id", err) + } + cids = append(cids, cid) } - tids = append(tids, tid) + filter.ChannelID = &cids } - filter.UsedKeyID = &tids - } - messages, npt, err := h.database.ListMessages(ctx, filter, &pageSize, tok) - if err != nil { - return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query messages", err) - } + if len(q.Senders) != 0 { + filter.SenderNameCS = langext.Ptr(q.Senders) + } - var res []models.MessageJSON - if trimmed { - res = langext.ArrMap(messages, func(v models.Message) models.MessageJSON { return v.TrimmedJSON() }) - } else { - res = langext.ArrMap(messages, func(v models.Message) models.MessageJSON { return v.FullJSON() }) - } + if q.TimeBefore != nil { + t0, err := time.Parse(time.RFC3339, *q.TimeBefore) + if err != nil { + return ginresp.APIError(g, 400, apierr.BINDFAIL_QUERY_PARAM, "Invalid before-time", err) + } + filter.TimestampCoalesceBefore = &t0 + } - return ctx.FinishSuccess(ginext.JSON(http.StatusOK, response{Messages: res, NextPageToken: npt.Token(), PageSize: pageSize})) + if q.TimeAfter != nil { + t0, err := time.Parse(time.RFC3339, *q.TimeAfter) + if err != nil { + return ginresp.APIError(g, 400, apierr.BINDFAIL_QUERY_PARAM, "Invalid after-time", err) + } + filter.TimestampCoalesceAfter = &t0 + } + + if len(q.Priority) != 0 { + filter.Priority = langext.Ptr(q.Priority) + } + + if len(q.KeyTokens) != 0 { + tids := make([]models.KeyTokenID, 0, len(q.KeyTokens)) + for _, v := range q.KeyTokens { + tid := models.KeyTokenID(v) + if err = tid.Valid(); err != nil { + return ginresp.APIError(g, 400, apierr.BINDFAIL_QUERY_PARAM, "Invalid keytoken-id", err) + } + tids = append(tids, tid) + } + filter.UsedKeyID = &tids + } + + messages, npt, err := h.database.ListMessages(ctx, filter, &pageSize, tok) + if err != nil { + return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query messages", err) + } + + var res []models.MessageJSON + if trimmed { + res = langext.ArrMap(messages, func(v models.Message) models.MessageJSON { return v.TrimmedJSON() }) + } else { + res = langext.ArrMap(messages, func(v models.Message) models.MessageJSON { return v.FullJSON() }) + } + + return finishSuccess(ginext.JSON(http.StatusOK, response{Messages: res, NextPageToken: npt.Token(), PageSize: pageSize})) + + }) } // GetMessage swaggerdoc @@ -182,50 +187,54 @@ func (h APIHandler) GetMessage(pctx ginext.PreContext) ginext.HTTPResponse { } var u uri - ctx, g, errResp := h.app.StartRequest(pctx.URI(&u).Start()) + ctx, g, errResp := pctx.URI(&u).Start() if errResp != nil { return *errResp } defer ctx.Cancel() - if permResp := ctx.CheckPermissionAny(); permResp != nil { - return *permResp - } + return h.app.DoRequest(ctx, g, func(ctx *logic.AppContext, finishSuccess func(r ginext.HTTPResponse) ginext.HTTPResponse) ginext.HTTPResponse { - msg, err := h.database.GetMessage(ctx, u.MessageID, false) - if errors.Is(err, sql.ErrNoRows) { - return ginresp.APIError(g, 404, apierr.MESSAGE_NOT_FOUND, "message not found", err) - } - if err != nil { - return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query message", err) - } + if permResp := ctx.CheckPermissionAny(); permResp != nil { + return *permResp + } - // either we have direct read permissions (it is our message + read/admin key) - // or we subscribe (+confirmed) to the channel and have read/admin key - - if ctx.CheckPermissionMessageRead(msg) { - return ctx.FinishSuccess(ginext.JSON(http.StatusOK, msg.FullJSON())) - } - - if uid := ctx.GetPermissionUserID(); uid != nil && ctx.CheckPermissionUserRead(*uid) == nil { - sub, err := h.database.GetSubscriptionBySubscriber(ctx, *uid, msg.ChannelID) + msg, err := h.database.GetMessage(ctx, u.MessageID, false) + if errors.Is(err, sql.ErrNoRows) { + return ginresp.APIError(g, 404, apierr.MESSAGE_NOT_FOUND, "message not found", err) + } if err != nil { - return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query subscription", err) - } - if sub == nil { - // not subbed - return ginresp.APIError(g, 401, apierr.USER_AUTH_FAILED, "You are not authorized for this action", nil) - } - if !sub.Confirmed { - // sub not confirmed - return ginresp.APIError(g, 401, apierr.USER_AUTH_FAILED, "You are not authorized for this action", nil) + return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query message", err) } - // => perm okay - return ctx.FinishSuccess(ginext.JSON(http.StatusOK, msg.FullJSON())) - } + // either we have direct read permissions (it is our message + read/admin key) + // or we subscribe (+confirmed) to the channel and have read/admin key - return ginresp.APIError(g, 401, apierr.USER_AUTH_FAILED, "You are not authorized for this action", nil) + if ctx.CheckPermissionMessageRead(msg) { + return finishSuccess(ginext.JSON(http.StatusOK, msg.FullJSON())) + } + + if uid := ctx.GetPermissionUserID(); uid != nil && ctx.CheckPermissionUserRead(*uid) == nil { + sub, err := h.database.GetSubscriptionBySubscriber(ctx, *uid, msg.ChannelID) + if err != nil { + return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query subscription", err) + } + if sub == nil { + // not subbed + return ginresp.APIError(g, 401, apierr.USER_AUTH_FAILED, "You are not authorized for this action", nil) + } + if !sub.Confirmed { + // sub not confirmed + return ginresp.APIError(g, 401, apierr.USER_AUTH_FAILED, "You are not authorized for this action", nil) + } + + // => perm okay + return finishSuccess(ginext.JSON(http.StatusOK, msg.FullJSON())) + } + + return ginresp.APIError(g, 401, apierr.USER_AUTH_FAILED, "You are not authorized for this action", nil) + + }) } // DeleteMessage swaggerdoc @@ -250,37 +259,41 @@ func (h APIHandler) DeleteMessage(pctx ginext.PreContext) ginext.HTTPResponse { } var u uri - ctx, g, errResp := h.app.StartRequest(pctx.URI(&u).Start()) + ctx, g, errResp := pctx.URI(&u).Start() if errResp != nil { return *errResp } defer ctx.Cancel() - if permResp := ctx.CheckPermissionAny(); permResp != nil { - return *permResp - } + return h.app.DoRequest(ctx, g, func(ctx *logic.AppContext, finishSuccess func(r ginext.HTTPResponse) ginext.HTTPResponse) ginext.HTTPResponse { - msg, err := h.database.GetMessage(ctx, u.MessageID, false) - if errors.Is(err, sql.ErrNoRows) { - return ginresp.APIError(g, 404, apierr.MESSAGE_NOT_FOUND, "message not found", err) - } - if err != nil { - return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query message", err) - } + if permResp := ctx.CheckPermissionAny(); permResp != nil { + return *permResp + } - if !ctx.CheckPermissionMessageDelete(msg) { - return ginresp.APIError(g, 401, apierr.USER_AUTH_FAILED, "You are not authorized for this action", nil) - } + msg, err := h.database.GetMessage(ctx, u.MessageID, false) + if errors.Is(err, sql.ErrNoRows) { + return ginresp.APIError(g, 404, apierr.MESSAGE_NOT_FOUND, "message not found", err) + } + if err != nil { + return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query message", err) + } - err = h.database.DeleteMessage(ctx, msg.MessageID) - if err != nil { - return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to delete message", err) - } + if !ctx.CheckPermissionMessageDelete(msg) { + return ginresp.APIError(g, 401, apierr.USER_AUTH_FAILED, "You are not authorized for this action", nil) + } - err = h.database.CancelPendingDeliveries(ctx, msg.MessageID) - if err != nil { - return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to cancel deliveries", err) - } + err = h.database.DeleteMessage(ctx, msg.MessageID) + if err != nil { + return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to delete message", err) + } - return ctx.FinishSuccess(ginext.JSON(http.StatusOK, msg.FullJSON())) + err = h.database.CancelPendingDeliveries(ctx, msg.MessageID) + if err != nil { + return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to cancel deliveries", err) + } + + return finishSuccess(ginext.JSON(http.StatusOK, msg.FullJSON())) + + }) } diff --git a/scnserver/api/handler/apiPreview.go b/scnserver/api/handler/apiPreview.go index d1df9fa..fa4da7b 100644 --- a/scnserver/api/handler/apiPreview.go +++ b/scnserver/api/handler/apiPreview.go @@ -3,6 +3,7 @@ package handler import ( "blackforestbytes.com/simplecloudnotifier/api/apierr" "blackforestbytes.com/simplecloudnotifier/api/ginresp" + "blackforestbytes.com/simplecloudnotifier/logic" "blackforestbytes.com/simplecloudnotifier/models" "database/sql" "errors" @@ -31,25 +32,29 @@ func (h APIHandler) GetUserPreview(pctx ginext.PreContext) ginext.HTTPResponse { } var u uri - ctx, g, errResp := h.app.StartRequest(pctx.URI(&u).Start()) + ctx, g, errResp := pctx.URI(&u).Start() if errResp != nil { return *errResp } defer ctx.Cancel() - if permResp := ctx.CheckPermissionAny(); permResp != nil { - return *permResp - } + return h.app.DoRequest(ctx, g, func(ctx *logic.AppContext, finishSuccess func(r ginext.HTTPResponse) ginext.HTTPResponse) ginext.HTTPResponse { - user, err := h.database.GetUser(ctx, u.UserID) - if errors.Is(err, sql.ErrNoRows) { - return ginresp.APIError(g, 404, apierr.USER_NOT_FOUND, "User not found", err) - } - if err != nil { - return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query user", err) - } + if permResp := ctx.CheckPermissionAny(); permResp != nil { + return *permResp + } - return ctx.FinishSuccess(ginext.JSON(http.StatusOK, user.JSONPreview())) + user, err := h.database.GetUser(ctx, u.UserID) + if errors.Is(err, sql.ErrNoRows) { + return ginresp.APIError(g, 404, apierr.USER_NOT_FOUND, "User not found", err) + } + if err != nil { + return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query user", err) + } + + return finishSuccess(ginext.JSON(http.StatusOK, user.JSONPreview())) + + }) } // GetChannelPreview swaggerdoc @@ -73,25 +78,29 @@ func (h APIHandler) GetChannelPreview(pctx ginext.PreContext) ginext.HTTPRespons } var u uri - ctx, g, errResp := h.app.StartRequest(pctx.URI(&u).Start()) + ctx, g, errResp := pctx.URI(&u).Start() if errResp != nil { return *errResp } defer ctx.Cancel() - if permResp := ctx.CheckPermissionAny(); permResp != nil { - return *permResp - } + return h.app.DoRequest(ctx, g, func(ctx *logic.AppContext, finishSuccess func(r ginext.HTTPResponse) ginext.HTTPResponse) ginext.HTTPResponse { - channel, err := h.database.GetChannelByID(ctx, u.ChannelID) - if errors.Is(err, sql.ErrNoRows) { - return ginresp.APIError(g, 404, apierr.CHANNEL_NOT_FOUND, "Channel not found", err) - } - if err != nil { - return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query channel", err) - } + if permResp := ctx.CheckPermissionAny(); permResp != nil { + return *permResp + } - return ctx.FinishSuccess(ginext.JSON(http.StatusOK, channel.JSONPreview())) + channel, err := h.database.GetChannelByID(ctx, u.ChannelID) + if errors.Is(err, sql.ErrNoRows) { + return ginresp.APIError(g, 404, apierr.CHANNEL_NOT_FOUND, "Channel not found", err) + } + if err != nil { + return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query channel", err) + } + + return finishSuccess(ginext.JSON(http.StatusOK, channel.JSONPreview())) + + }) } // GetUserKeyPreview swaggerdoc @@ -115,23 +124,27 @@ func (h APIHandler) GetUserKeyPreview(pctx ginext.PreContext) ginext.HTTPRespons } var u uri - ctx, g, errResp := h.app.StartRequest(pctx.URI(&u).Start()) + ctx, g, errResp := pctx.URI(&u).Start() if errResp != nil { return *errResp } defer ctx.Cancel() - if permResp := ctx.CheckPermissionAny(); permResp != nil { - return *permResp - } + return h.app.DoRequest(ctx, g, func(ctx *logic.AppContext, finishSuccess func(r ginext.HTTPResponse) ginext.HTTPResponse) ginext.HTTPResponse { - keytoken, err := h.database.GetKeyTokenByID(ctx, u.KeyID) - if errors.Is(err, sql.ErrNoRows) { - return ginresp.APIError(g, 404, apierr.KEY_NOT_FOUND, "Key not found", err) - } - if err != nil { - return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query client", err) - } + if permResp := ctx.CheckPermissionAny(); permResp != nil { + return *permResp + } - return ctx.FinishSuccess(ginext.JSON(http.StatusOK, keytoken.JSONPreview())) + keytoken, err := h.database.GetKeyTokenByID(ctx, u.KeyID) + if errors.Is(err, sql.ErrNoRows) { + return ginresp.APIError(g, 404, apierr.KEY_NOT_FOUND, "Key not found", err) + } + if err != nil { + return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query client", err) + } + + return finishSuccess(ginext.JSON(http.StatusOK, keytoken.JSONPreview())) + + }) } diff --git a/scnserver/api/handler/apiSubscription.go b/scnserver/api/handler/apiSubscription.go index e88fffa..2cabc9d 100644 --- a/scnserver/api/handler/apiSubscription.go +++ b/scnserver/api/handler/apiSubscription.go @@ -3,6 +3,7 @@ package handler import ( "blackforestbytes.com/simplecloudnotifier/api/apierr" "blackforestbytes.com/simplecloudnotifier/api/ginresp" + "blackforestbytes.com/simplecloudnotifier/logic" "blackforestbytes.com/simplecloudnotifier/models" "database/sql" "errors" @@ -64,71 +65,75 @@ func (h APIHandler) ListUserSubscriptions(pctx ginext.PreContext) ginext.HTTPRes var u uri var q query - ctx, g, errResp := h.app.StartRequest(pctx.URI(&u).Query(&q).Start()) + ctx, g, errResp := pctx.URI(&u).Query(&q).Start() if errResp != nil { return *errResp } defer ctx.Cancel() - if permResp := ctx.CheckPermissionUserRead(u.UserID); permResp != nil { - return *permResp - } + return h.app.DoRequest(ctx, g, func(ctx *logic.AppContext, finishSuccess func(r ginext.HTTPResponse) ginext.HTTPResponse) ginext.HTTPResponse { - filter := models.SubscriptionFilter{} - filter.AnyUserID = langext.Ptr(u.UserID) - - if q.Direction != nil { - if strings.EqualFold(*q.Direction, "incoming") { - filter.ChannelOwnerUserID = langext.Ptr([]models.UserID{u.UserID}) - } else if strings.EqualFold(*q.Direction, "outgoing") { - filter.SubscriberUserID = langext.Ptr([]models.UserID{u.UserID}) - } else if strings.EqualFold(*q.Direction, "both") { - // both - } else { - return ginresp.APIError(g, 400, apierr.BINDFAIL_QUERY_PARAM, "Invalid value for param 'direction'", nil) + if permResp := ctx.CheckPermissionUserRead(u.UserID); permResp != nil { + return *permResp } - } - if q.Confirmation != nil { - if strings.EqualFold(*q.Confirmation, "confirmed") { - filter.Confirmed = langext.PTrue - } else if strings.EqualFold(*q.Confirmation, "unconfirmed") { - filter.Confirmed = langext.PFalse - } else if strings.EqualFold(*q.Confirmation, "all") { - // both - } else { - return ginresp.APIError(g, 400, apierr.BINDFAIL_QUERY_PARAM, "Invalid value for param 'confirmation'", nil) + filter := models.SubscriptionFilter{} + filter.AnyUserID = langext.Ptr(u.UserID) + + if q.Direction != nil { + if strings.EqualFold(*q.Direction, "incoming") { + filter.ChannelOwnerUserID = langext.Ptr([]models.UserID{u.UserID}) + } else if strings.EqualFold(*q.Direction, "outgoing") { + filter.SubscriberUserID = langext.Ptr([]models.UserID{u.UserID}) + } else if strings.EqualFold(*q.Direction, "both") { + // both + } else { + return ginresp.APIError(g, 400, apierr.BINDFAIL_QUERY_PARAM, "Invalid value for param 'direction'", nil) + } } - } - if q.External != nil { - if strings.EqualFold(*q.External, "true") { - filter.SubscriberIsChannelOwner = langext.PFalse - } else if strings.EqualFold(*q.External, "false") { - filter.SubscriberIsChannelOwner = langext.PTrue - } else if strings.EqualFold(*q.External, "all") { - // both - } else { - return ginresp.APIError(g, 400, apierr.BINDFAIL_QUERY_PARAM, "Invalid value for param 'external'", nil) + if q.Confirmation != nil { + if strings.EqualFold(*q.Confirmation, "confirmed") { + filter.Confirmed = langext.PTrue + } else if strings.EqualFold(*q.Confirmation, "unconfirmed") { + filter.Confirmed = langext.PFalse + } else if strings.EqualFold(*q.Confirmation, "all") { + // both + } else { + return ginresp.APIError(g, 400, apierr.BINDFAIL_QUERY_PARAM, "Invalid value for param 'confirmation'", nil) + } } - } - if q.SubscriberUserID != nil { - filter.SubscriberUserID2 = langext.Ptr([]models.UserID{*q.SubscriberUserID}) - } + if q.External != nil { + if strings.EqualFold(*q.External, "true") { + filter.SubscriberIsChannelOwner = langext.PFalse + } else if strings.EqualFold(*q.External, "false") { + filter.SubscriberIsChannelOwner = langext.PTrue + } else if strings.EqualFold(*q.External, "all") { + // both + } else { + return ginresp.APIError(g, 400, apierr.BINDFAIL_QUERY_PARAM, "Invalid value for param 'external'", nil) + } + } - if q.ChannelOwnerUserID != nil { - filter.ChannelOwnerUserID2 = langext.Ptr([]models.UserID{*q.ChannelOwnerUserID}) - } + if q.SubscriberUserID != nil { + filter.SubscriberUserID2 = langext.Ptr([]models.UserID{*q.SubscriberUserID}) + } - res, err := h.database.ListSubscriptions(ctx, filter) - if err != nil { - return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query subscriptions", err) - } + if q.ChannelOwnerUserID != nil { + filter.ChannelOwnerUserID2 = langext.Ptr([]models.UserID{*q.ChannelOwnerUserID}) + } - jsonres := langext.ArrMap(res, func(v models.Subscription) models.SubscriptionJSON { return v.JSON() }) + res, err := h.database.ListSubscriptions(ctx, filter) + if err != nil { + return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query subscriptions", err) + } - return ctx.FinishSuccess(ginext.JSON(http.StatusOK, response{Subscriptions: jsonres})) + jsonres := langext.ArrMap(res, func(v models.Subscription) models.SubscriptionJSON { return v.JSON() }) + + return finishSuccess(ginext.JSON(http.StatusOK, response{Subscriptions: jsonres})) + + }) } // ListChannelSubscriptions swaggerdoc @@ -157,32 +162,36 @@ func (h APIHandler) ListChannelSubscriptions(pctx ginext.PreContext) ginext.HTTP } var u uri - ctx, g, errResp := h.app.StartRequest(pctx.URI(&u).Start()) + ctx, g, errResp := pctx.URI(&u).Start() if errResp != nil { return *errResp } defer ctx.Cancel() - if permResp := ctx.CheckPermissionUserRead(u.UserID); permResp != nil { - return *permResp - } + return h.app.DoRequest(ctx, g, func(ctx *logic.AppContext, finishSuccess func(r ginext.HTTPResponse) ginext.HTTPResponse) ginext.HTTPResponse { - _, err := h.database.GetChannel(ctx, u.UserID, u.ChannelID, true) - if errors.Is(err, sql.ErrNoRows) { - return ginresp.APIError(g, 404, apierr.CHANNEL_NOT_FOUND, "Channel not found", err) - } - if err != nil { - return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query channel", err) - } + if permResp := ctx.CheckPermissionUserRead(u.UserID); permResp != nil { + return *permResp + } - clients, err := h.database.ListSubscriptions(ctx, models.SubscriptionFilter{AnyUserID: langext.Ptr(u.UserID), ChannelID: langext.Ptr([]models.ChannelID{u.ChannelID})}) - if err != nil { - return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query subscriptions", err) - } + _, err := h.database.GetChannel(ctx, u.UserID, u.ChannelID, true) + if errors.Is(err, sql.ErrNoRows) { + return ginresp.APIError(g, 404, apierr.CHANNEL_NOT_FOUND, "Channel not found", err) + } + if err != nil { + return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query channel", err) + } - res := langext.ArrMap(clients, func(v models.Subscription) models.SubscriptionJSON { return v.JSON() }) + clients, err := h.database.ListSubscriptions(ctx, models.SubscriptionFilter{AnyUserID: langext.Ptr(u.UserID), ChannelID: langext.Ptr([]models.ChannelID{u.ChannelID})}) + if err != nil { + return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query subscriptions", err) + } - return ctx.FinishSuccess(ginext.JSON(http.StatusOK, response{Subscriptions: res})) + res := langext.ArrMap(clients, func(v models.Subscription) models.SubscriptionJSON { return v.JSON() }) + + return finishSuccess(ginext.JSON(http.StatusOK, response{Subscriptions: res})) + + }) } // GetSubscription swaggerdoc @@ -208,28 +217,32 @@ func (h APIHandler) GetSubscription(pctx ginext.PreContext) ginext.HTTPResponse } var u uri - ctx, g, errResp := h.app.StartRequest(pctx.URI(&u).Start()) + ctx, g, errResp := pctx.URI(&u).Start() if errResp != nil { return *errResp } defer ctx.Cancel() - if permResp := ctx.CheckPermissionUserRead(u.UserID); permResp != nil { - return *permResp - } + return h.app.DoRequest(ctx, g, func(ctx *logic.AppContext, finishSuccess func(r ginext.HTTPResponse) ginext.HTTPResponse) ginext.HTTPResponse { - subscription, err := h.database.GetSubscription(ctx, u.SubscriptionID) - if errors.Is(err, sql.ErrNoRows) { - return ginresp.APIError(g, 404, apierr.SUBSCRIPTION_NOT_FOUND, "Subscription not found", err) - } - if err != nil { - return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query subscription", err) - } - if subscription.SubscriberUserID != u.UserID && subscription.ChannelOwnerUserID != u.UserID { - return ginresp.APIError(g, 404, apierr.SUBSCRIPTION_USER_MISMATCH, "Subscription not found", nil) - } + if permResp := ctx.CheckPermissionUserRead(u.UserID); permResp != nil { + return *permResp + } - return ctx.FinishSuccess(ginext.JSON(http.StatusOK, subscription.JSON())) + subscription, err := h.database.GetSubscription(ctx, u.SubscriptionID) + if errors.Is(err, sql.ErrNoRows) { + return ginresp.APIError(g, 404, apierr.SUBSCRIPTION_NOT_FOUND, "Subscription not found", err) + } + if err != nil { + return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query subscription", err) + } + if subscription.SubscriberUserID != u.UserID && subscription.ChannelOwnerUserID != u.UserID { + return ginresp.APIError(g, 404, apierr.SUBSCRIPTION_USER_MISMATCH, "Subscription not found", nil) + } + + return finishSuccess(ginext.JSON(http.StatusOK, subscription.JSON())) + + }) } // CancelSubscription swaggerdoc @@ -255,33 +268,37 @@ func (h APIHandler) CancelSubscription(pctx ginext.PreContext) ginext.HTTPRespon } var u uri - ctx, g, errResp := h.app.StartRequest(pctx.URI(&u).Start()) + ctx, g, errResp := pctx.URI(&u).Start() if errResp != nil { return *errResp } defer ctx.Cancel() - if permResp := ctx.CheckPermissionUserAdmin(u.UserID); permResp != nil { - return *permResp - } + return h.app.DoRequest(ctx, g, func(ctx *logic.AppContext, finishSuccess func(r ginext.HTTPResponse) ginext.HTTPResponse) ginext.HTTPResponse { - subscription, err := h.database.GetSubscription(ctx, u.SubscriptionID) - if errors.Is(err, sql.ErrNoRows) { - return ginresp.APIError(g, 404, apierr.SUBSCRIPTION_NOT_FOUND, "Subscription not found", err) - } - if err != nil { - return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query subscription", err) - } - if subscription.SubscriberUserID != u.UserID && subscription.ChannelOwnerUserID != u.UserID { - return ginresp.APIError(g, 404, apierr.SUBSCRIPTION_USER_MISMATCH, "Subscription not found", nil) - } + if permResp := ctx.CheckPermissionUserAdmin(u.UserID); permResp != nil { + return *permResp + } - err = h.database.DeleteSubscription(ctx, u.SubscriptionID) - if err != nil { - return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to delete subscription", err) - } + subscription, err := h.database.GetSubscription(ctx, u.SubscriptionID) + if errors.Is(err, sql.ErrNoRows) { + return ginresp.APIError(g, 404, apierr.SUBSCRIPTION_NOT_FOUND, "Subscription not found", err) + } + if err != nil { + return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query subscription", err) + } + if subscription.SubscriberUserID != u.UserID && subscription.ChannelOwnerUserID != u.UserID { + return ginresp.APIError(g, 404, apierr.SUBSCRIPTION_USER_MISMATCH, "Subscription not found", nil) + } - return ctx.FinishSuccess(ginext.JSON(http.StatusOK, subscription.JSON())) + err = h.database.DeleteSubscription(ctx, u.SubscriptionID) + if err != nil { + return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to delete subscription", err) + } + + return finishSuccess(ginext.JSON(http.StatusOK, subscription.JSON())) + + }) } // CreateSubscription swaggerdoc @@ -317,76 +334,80 @@ func (h APIHandler) CreateSubscription(pctx ginext.PreContext) ginext.HTTPRespon var u uri var q query var b body - ctx, g, errResp := h.app.StartRequest(pctx.URI(&u).Query(&q).Body(&b).Start()) + ctx, g, errResp := pctx.URI(&u).Query(&q).Body(&b).Start() if errResp != nil { return *errResp } defer ctx.Cancel() - if permResp := ctx.CheckPermissionUserAdmin(u.UserID); permResp != nil { - return *permResp - } + return h.app.DoRequest(ctx, g, func(ctx *logic.AppContext, finishSuccess func(r ginext.HTTPResponse) ginext.HTTPResponse) ginext.HTTPResponse { - var channel models.Channel - - if b.ChannelOwnerUserID != nil && b.ChannelInternalName != nil && b.ChannelID == nil { - - channelInternalName := h.app.NormalizeChannelInternalName(*b.ChannelInternalName) - - outchannel, err := h.database.GetChannelByName(ctx, *b.ChannelOwnerUserID, channelInternalName) - if err != nil { - return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query channel", err) - } - if outchannel == nil { - return ginresp.APIError(g, 400, apierr.CHANNEL_NOT_FOUND, "Channel not found", err) + if permResp := ctx.CheckPermissionUserAdmin(u.UserID); permResp != nil { + return *permResp } - channel = *outchannel + var channel models.Channel - } else if b.ChannelOwnerUserID == nil && b.ChannelInternalName == nil && b.ChannelID != nil { + if b.ChannelOwnerUserID != nil && b.ChannelInternalName != nil && b.ChannelID == nil { - outchannel, err := h.database.GetChannelByID(ctx, *b.ChannelID) - if err != nil { - return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query channel", err) - } - if outchannel == nil { - return ginresp.APIError(g, 400, apierr.CHANNEL_NOT_FOUND, "Channel not found", err) - } + channelInternalName := h.app.NormalizeChannelInternalName(*b.ChannelInternalName) - channel = *outchannel - - } else { - - return ginresp.APIError(g, 400, apierr.INVALID_BODY_PARAM, "Must either supply [channel_owner_user_id, channel_internal_name] or [channel_id]", nil) - - } - - if channel.OwnerUserID != u.UserID && (q.ChanSubscribeKey == nil || *q.ChanSubscribeKey != channel.SubscribeKey) { - return ginresp.APIError(g, 401, apierr.USER_AUTH_FAILED, "You are not authorized for this action", nil) - } - - existingSub, err := h.database.GetSubscriptionBySubscriber(ctx, u.UserID, channel.ChannelID) - if err != nil { - return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query existing subscription", err) - } - if existingSub != nil { - if !existingSub.Confirmed && channel.OwnerUserID == u.UserID { - err = h.database.UpdateSubscriptionConfirmed(ctx, existingSub.SubscriptionID, true) + outchannel, err := h.database.GetChannelByName(ctx, *b.ChannelOwnerUserID, channelInternalName) if err != nil { - return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to update subscription", err) + return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query channel", err) } - existingSub.Confirmed = true + if outchannel == nil { + return ginresp.APIError(g, 400, apierr.CHANNEL_NOT_FOUND, "Channel not found", err) + } + + channel = *outchannel + + } else if b.ChannelOwnerUserID == nil && b.ChannelInternalName == nil && b.ChannelID != nil { + + outchannel, err := h.database.GetChannelByID(ctx, *b.ChannelID) + if err != nil { + return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query channel", err) + } + if outchannel == nil { + return ginresp.APIError(g, 400, apierr.CHANNEL_NOT_FOUND, "Channel not found", err) + } + + channel = *outchannel + + } else { + + return ginresp.APIError(g, 400, apierr.INVALID_BODY_PARAM, "Must either supply [channel_owner_user_id, channel_internal_name] or [channel_id]", nil) + } - return ctx.FinishSuccess(ginext.JSON(http.StatusOK, existingSub.JSON())) - } + if channel.OwnerUserID != u.UserID && (q.ChanSubscribeKey == nil || *q.ChanSubscribeKey != channel.SubscribeKey) { + return ginresp.APIError(g, 401, apierr.USER_AUTH_FAILED, "You are not authorized for this action", nil) + } - sub, err := h.database.CreateSubscription(ctx, u.UserID, channel, channel.OwnerUserID == u.UserID) - if err != nil { - return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to create subscription", err) - } + existingSub, err := h.database.GetSubscriptionBySubscriber(ctx, u.UserID, channel.ChannelID) + if err != nil { + return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query existing subscription", err) + } + if existingSub != nil { + if !existingSub.Confirmed && channel.OwnerUserID == u.UserID { + err = h.database.UpdateSubscriptionConfirmed(ctx, existingSub.SubscriptionID, true) + if err != nil { + return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to update subscription", err) + } + existingSub.Confirmed = true + } - return ctx.FinishSuccess(ginext.JSON(http.StatusOK, sub.JSON())) + return finishSuccess(ginext.JSON(http.StatusOK, existingSub.JSON())) + } + + sub, err := h.database.CreateSubscription(ctx, u.UserID, channel, channel.OwnerUserID == u.UserID) + if err != nil { + return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to create subscription", err) + } + + return finishSuccess(ginext.JSON(http.StatusOK, sub.JSON())) + + }) } // UpdateSubscription swaggerdoc @@ -417,43 +438,47 @@ func (h APIHandler) UpdateSubscription(pctx ginext.PreContext) ginext.HTTPRespon var u uri var b body - ctx, g, errResp := h.app.StartRequest(pctx.URI(&u).Body(&b).Start()) + ctx, g, errResp := pctx.URI(&u).Body(&b).Start() if errResp != nil { return *errResp } defer ctx.Cancel() - if permResp := ctx.CheckPermissionUserAdmin(u.UserID); permResp != nil { - return *permResp - } + return h.app.DoRequest(ctx, g, func(ctx *logic.AppContext, finishSuccess func(r ginext.HTTPResponse) ginext.HTTPResponse) ginext.HTTPResponse { - userid := *ctx.GetPermissionUserID() - - subscription, err := h.database.GetSubscription(ctx, u.SubscriptionID) - if errors.Is(err, sql.ErrNoRows) { - return ginresp.APIError(g, 404, apierr.SUBSCRIPTION_NOT_FOUND, "Subscription not found", err) - } - if err != nil { - return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query subscription", err) - } - if subscription.SubscriberUserID != u.UserID && subscription.ChannelOwnerUserID != u.UserID { - return ginresp.APIError(g, 404, apierr.SUBSCRIPTION_USER_MISMATCH, "Subscription not found", nil) - } - - if b.Confirmed != nil { - if subscription.ChannelOwnerUserID != userid { - return ginresp.APIError(g, 401, apierr.USER_AUTH_FAILED, "You are not authorized for this action", nil) + if permResp := ctx.CheckPermissionUserAdmin(u.UserID); permResp != nil { + return *permResp + } + + userid := *ctx.GetPermissionUserID() + + subscription, err := h.database.GetSubscription(ctx, u.SubscriptionID) + if errors.Is(err, sql.ErrNoRows) { + return ginresp.APIError(g, 404, apierr.SUBSCRIPTION_NOT_FOUND, "Subscription not found", err) } - err = h.database.UpdateSubscriptionConfirmed(ctx, u.SubscriptionID, *b.Confirmed) if err != nil { - return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to update subscription", err) + return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query subscription", err) + } + if subscription.SubscriberUserID != u.UserID && subscription.ChannelOwnerUserID != u.UserID { + return ginresp.APIError(g, 404, apierr.SUBSCRIPTION_USER_MISMATCH, "Subscription not found", nil) } - } - subscription, err = h.database.GetSubscription(ctx, u.SubscriptionID) - if err != nil { - return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query subscription", err) - } + if b.Confirmed != nil { + if subscription.ChannelOwnerUserID != userid { + return ginresp.APIError(g, 401, apierr.USER_AUTH_FAILED, "You are not authorized for this action", nil) + } + err = h.database.UpdateSubscriptionConfirmed(ctx, u.SubscriptionID, *b.Confirmed) + if err != nil { + return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to update subscription", err) + } + } - return ctx.FinishSuccess(ginext.JSON(http.StatusOK, subscription.JSON())) + subscription, err = h.database.GetSubscription(ctx, u.SubscriptionID) + if err != nil { + return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query subscription", err) + } + + return finishSuccess(ginext.JSON(http.StatusOK, subscription.JSON())) + + }) } diff --git a/scnserver/api/handler/apiUser.go b/scnserver/api/handler/apiUser.go index 2537ec2..b3a10c2 100644 --- a/scnserver/api/handler/apiUser.go +++ b/scnserver/api/handler/apiUser.go @@ -3,6 +3,7 @@ package handler import ( "blackforestbytes.com/simplecloudnotifier/api/apierr" "blackforestbytes.com/simplecloudnotifier/api/ginresp" + "blackforestbytes.com/simplecloudnotifier/logic" "blackforestbytes.com/simplecloudnotifier/models" "database/sql" "errors" @@ -39,99 +40,101 @@ func (h APIHandler) CreateUser(pctx ginext.PreContext) ginext.HTTPResponse { } var b body - ctx, g, errResp := h.app.StartRequest(pctx.Body(&b).Start()) + ctx, g, errResp := pctx.Body(&b).Start() if errResp != nil { return *errResp } defer ctx.Cancel() - var clientType models.ClientType - if !b.NoClient { - if b.FCMToken == "" { - return ginresp.APIError(g, 400, apierr.INVALID_CLIENTTYPE, "Missing FCMToken", nil) - } - if b.AgentVersion == "" { - return ginresp.APIError(g, 400, apierr.INVALID_CLIENTTYPE, "Missing AgentVersion", nil) - } - if b.ClientType == "" { - return ginresp.APIError(g, 400, apierr.INVALID_CLIENTTYPE, "Missing ClientType", nil) - } - if !b.ClientType.Valid() { - return ginresp.APIError(g, 400, apierr.BINDFAIL_BODY_PARAM, "Invalid ClientType", nil) - } - clientType = b.ClientType - } + return h.app.DoRequest(ctx, g, func(ctx *logic.AppContext, finishSuccess func(r ginext.HTTPResponse) ginext.HTTPResponse) ginext.HTTPResponse { - if b.ProToken != nil { - ptok, err := h.app.VerifyProToken(ctx, *b.ProToken) + var clientType models.ClientType + if !b.NoClient { + if b.FCMToken == "" { + return ginresp.APIError(g, 400, apierr.INVALID_CLIENTTYPE, "Missing FCMToken", nil) + } + if b.AgentVersion == "" { + return ginresp.APIError(g, 400, apierr.INVALID_CLIENTTYPE, "Missing AgentVersion", nil) + } + if b.ClientType == "" { + return ginresp.APIError(g, 400, apierr.INVALID_CLIENTTYPE, "Missing ClientType", nil) + } + if !b.ClientType.Valid() { + return ginresp.APIError(g, 400, apierr.BINDFAIL_BODY_PARAM, "Invalid ClientType", nil) + } + clientType = b.ClientType + } + + if b.ProToken != nil { + ptok, err := h.app.VerifyProToken(ctx, *b.ProToken) + if err != nil { + return ginresp.APIError(g, 500, apierr.FAILED_VERIFY_PRO_TOKEN, "Failed to query purchase status", err) + } + + if !ptok { + return ginresp.APIError(g, 400, apierr.INVALID_PRO_TOKEN, "Purchase token could not be verified", nil) + } + } + + readKey := h.app.GenerateRandomAuthKey() + sendKey := h.app.GenerateRandomAuthKey() + adminKey := h.app.GenerateRandomAuthKey() + + err := h.database.ClearFCMTokens(ctx, b.FCMToken) if err != nil { - return ginresp.APIError(g, 500, apierr.FAILED_VERIFY_PRO_TOKEN, "Failed to query purchase status", err) + return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to clear existing fcm tokens", err) } - if !ptok { - return ginresp.APIError(g, 400, apierr.INVALID_PRO_TOKEN, "Purchase token could not be verified", nil) + if b.ProToken != nil { + err := h.database.ClearProTokens(ctx, *b.ProToken) + if err != nil { + return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to clear existing pro tokens", err) + } } - } - readKey := h.app.GenerateRandomAuthKey() - sendKey := h.app.GenerateRandomAuthKey() - adminKey := h.app.GenerateRandomAuthKey() + username := b.Username + if username != nil { + username = langext.Ptr(h.app.NormalizeUsername(*username)) + } - err := h.database.ClearFCMTokens(ctx, b.FCMToken) - if err != nil { - return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to clear existing fcm tokens", err) - } - - if b.ProToken != nil { - err := h.database.ClearProTokens(ctx, *b.ProToken) + userobj, err := h.database.CreateUser(ctx, b.ProToken, username) if err != nil { - return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to clear existing pro tokens", err) + return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to create user in db", err) } - } - username := b.Username - if username != nil { - username = langext.Ptr(h.app.NormalizeUsername(*username)) - } - - userobj, err := h.database.CreateUser(ctx, b.ProToken, username) - if err != nil { - return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to create user in db", err) - } - - _, err = h.database.CreateKeyToken(ctx, "AdminKey (default)", userobj.UserID, true, make([]models.ChannelID, 0), models.TokenPermissionList{models.PermAdmin}, adminKey) - if err != nil { - return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to create admin-key in db", err) - } - - _, err = h.database.CreateKeyToken(ctx, "SendKey (default)", userobj.UserID, true, make([]models.ChannelID, 0), models.TokenPermissionList{models.PermChannelSend}, sendKey) - if err != nil { - return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to create send-key in db", err) - } - - _, err = h.database.CreateKeyToken(ctx, "ReadKey (default)", userobj.UserID, true, make([]models.ChannelID, 0), models.TokenPermissionList{models.PermUserRead, models.PermChannelRead}, readKey) - if err != nil { - return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to create read-key in db", err) - } - - log.Info().Msg(fmt.Sprintf("Sucessfully created new user %s (client: %v)", userobj.UserID, b.NoClient)) - - if b.NoClient { - return ctx.FinishSuccess(ginext.JSON(http.StatusOK, userobj.JSONWithClients(make([]models.Client, 0), adminKey, sendKey, readKey))) - } else { - err := h.database.DeleteClientsByFCM(ctx, b.FCMToken) + _, err = h.database.CreateKeyToken(ctx, "AdminKey (default)", userobj.UserID, true, make([]models.ChannelID, 0), models.TokenPermissionList{models.PermAdmin}, adminKey) if err != nil { - return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to delete existing clients in db", err) + return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to create admin-key in db", err) } - client, err := h.database.CreateClient(ctx, userobj.UserID, clientType, b.FCMToken, b.AgentModel, b.AgentVersion, b.ClientName) + _, err = h.database.CreateKeyToken(ctx, "SendKey (default)", userobj.UserID, true, make([]models.ChannelID, 0), models.TokenPermissionList{models.PermChannelSend}, sendKey) if err != nil { - return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to create client in db", err) + return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to create send-key in db", err) } - return ctx.FinishSuccess(ginext.JSON(http.StatusOK, userobj.JSONWithClients([]models.Client{client}, adminKey, sendKey, readKey))) - } + _, err = h.database.CreateKeyToken(ctx, "ReadKey (default)", userobj.UserID, true, make([]models.ChannelID, 0), models.TokenPermissionList{models.PermUserRead, models.PermChannelRead}, readKey) + if err != nil { + return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to create read-key in db", err) + } + log.Info().Msg(fmt.Sprintf("Sucessfully created new user %s (client: %v)", userobj.UserID, b.NoClient)) + + if b.NoClient { + return finishSuccess(ginext.JSON(http.StatusOK, userobj.JSONWithClients(make([]models.Client, 0), adminKey, sendKey, readKey))) + } else { + err := h.database.DeleteClientsByFCM(ctx, b.FCMToken) + if err != nil { + return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to delete existing clients in db", err) + } + + client, err := h.database.CreateClient(ctx, userobj.UserID, clientType, b.FCMToken, b.AgentModel, b.AgentVersion, b.ClientName) + if err != nil { + return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to create client in db", err) + } + + return finishSuccess(ginext.JSON(http.StatusOK, userobj.JSONWithClients([]models.Client{client}, adminKey, sendKey, readKey))) + } + }) } // GetUser swaggerdoc @@ -155,25 +158,30 @@ func (h APIHandler) GetUser(pctx ginext.PreContext) ginext.HTTPResponse { } var u uri - ctx, g, errResp := h.app.StartRequest(pctx.URI(&u).Start()) + ctx, g, errResp := pctx.URI(&u).Start() if errResp != nil { return *errResp } defer ctx.Cancel() - if permResp := ctx.CheckPermissionUserRead(u.UserID); permResp != nil { - return *permResp - } + return h.app.DoRequest(ctx, g, func(ctx *logic.AppContext, finishSuccess func(r ginext.HTTPResponse) ginext.HTTPResponse) ginext.HTTPResponse { - user, err := h.database.GetUser(ctx, u.UserID) - if errors.Is(err, sql.ErrNoRows) { - return ginresp.APIError(g, 404, apierr.USER_NOT_FOUND, "User not found", err) - } - if err != nil { - return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query user", err) - } + if permResp := ctx.CheckPermissionUserRead(u.UserID); permResp != nil { + return *permResp + } + + user, err := h.database.GetUser(ctx, u.UserID) + if errors.Is(err, sql.ErrNoRows) { + return ginresp.APIError(g, 404, apierr.USER_NOT_FOUND, "User not found", err) + } + if err != nil { + return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query user", err) + } + + return finishSuccess(ginext.JSON(http.StatusOK, user.JSON())) + + }) - return ctx.FinishSuccess(ginext.JSON(http.StatusOK, user.JSON())) } // UpdateUser swaggerdoc @@ -206,60 +214,63 @@ func (h APIHandler) UpdateUser(pctx ginext.PreContext) ginext.HTTPResponse { var u uri var b body - ctx, g, errResp := h.app.StartRequest(pctx.URI(&u).Body(&b).Start()) + ctx, g, errResp := pctx.URI(&u).Body(&b).Start() if errResp != nil { return *errResp } defer ctx.Cancel() - if permResp := ctx.CheckPermissionUserAdmin(u.UserID); permResp != nil { - return *permResp - } + return h.app.DoRequest(ctx, g, func(ctx *logic.AppContext, finishSuccess func(r ginext.HTTPResponse) ginext.HTTPResponse) ginext.HTTPResponse { - if b.Username != nil { - username := langext.Ptr(h.app.NormalizeUsername(*b.Username)) - if *username == "" { - username = nil + if permResp := ctx.CheckPermissionUserAdmin(u.UserID); permResp != nil { + return *permResp } - err := h.database.UpdateUserUsername(ctx, u.UserID, username) + if b.Username != nil { + username := langext.Ptr(h.app.NormalizeUsername(*b.Username)) + if *username == "" { + username = nil + } + + err := h.database.UpdateUserUsername(ctx, u.UserID, username) + if err != nil { + return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to update user", err) + } + } + + if b.ProToken != nil { + if *b.ProToken == "" { + err := h.database.UpdateUserProToken(ctx, u.UserID, nil) + if err != nil { + return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to update user", err) + } + } else { + ptok, err := h.app.VerifyProToken(ctx, *b.ProToken) + if err != nil { + return ginresp.APIError(g, 500, apierr.FAILED_VERIFY_PRO_TOKEN, "Failed to query purchase status", err) + } + + if !ptok { + return ginresp.APIError(g, 400, apierr.INVALID_PRO_TOKEN, "Purchase token could not be verified", nil) + } + + err = h.database.ClearProTokens(ctx, *b.ProToken) + if err != nil { + return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to clear existing fcm tokens", err) + } + + err = h.database.UpdateUserProToken(ctx, u.UserID, b.ProToken) + if err != nil { + return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to update user", err) + } + } + } + + user, err := h.database.GetUser(ctx, u.UserID) if err != nil { - return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to update user", err) + return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query (updated) user", err) } - } - if b.ProToken != nil { - if *b.ProToken == "" { - err := h.database.UpdateUserProToken(ctx, u.UserID, nil) - if err != nil { - return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to update user", err) - } - } else { - ptok, err := h.app.VerifyProToken(ctx, *b.ProToken) - if err != nil { - return ginresp.APIError(g, 500, apierr.FAILED_VERIFY_PRO_TOKEN, "Failed to query purchase status", err) - } - - if !ptok { - return ginresp.APIError(g, 400, apierr.INVALID_PRO_TOKEN, "Purchase token could not be verified", nil) - } - - err = h.database.ClearProTokens(ctx, *b.ProToken) - if err != nil { - return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to clear existing fcm tokens", err) - } - - err = h.database.UpdateUserProToken(ctx, u.UserID, b.ProToken) - if err != nil { - return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to update user", err) - } - } - } - - user, err := h.database.GetUser(ctx, u.UserID) - if err != nil { - return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query (updated) user", err) - } - - return ctx.FinishSuccess(ginext.JSON(http.StatusOK, user.JSON())) + return finishSuccess(ginext.JSON(http.StatusOK, user.JSON())) + }) } diff --git a/scnserver/api/handler/common.go b/scnserver/api/handler/common.go index 2f3d830..83e4e9f 100644 --- a/scnserver/api/handler/common.go +++ b/scnserver/api/handler/common.go @@ -58,19 +58,23 @@ func (h CommonHandler) Ping(pctx ginext.PreContext) ginext.HTTPResponse { } defer ctx.Cancel() - buf := new(bytes.Buffer) - _, _ = buf.ReadFrom(g.Request.Body) - resuestBody := buf.String() + return h.app.DoRequest(ctx, g, func(ctx *logic.AppContext, finishSuccess func(r ginext.HTTPResponse) ginext.HTTPResponse) ginext.HTTPResponse { + + buf := new(bytes.Buffer) + _, _ = buf.ReadFrom(g.Request.Body) + resuestBody := buf.String() + + return ginext.JSON(http.StatusOK, pingResponse{ + Message: "Pong", + Info: pingResponseInfo{ + Method: g.Request.Method, + Request: resuestBody, + Headers: g.Request.Header, + URI: g.Request.RequestURI, + Address: g.Request.RemoteAddr, + }, + }) - return ginext.JSON(http.StatusOK, pingResponse{ - Message: "Pong", - Info: pingResponseInfo{ - Method: g.Request.Method, - Request: resuestBody, - Headers: g.Request.Header, - URI: g.Request.RequestURI, - Address: g.Request.RemoteAddr, - }, }) } @@ -92,24 +96,28 @@ func (h CommonHandler) DatabaseTest(pctx ginext.PreContext) ginext.HTTPResponse SourceID string `json:"sourceID"` } - ctx, _, errResp := pctx.Start() + ctx, g, errResp := pctx.Start() if errResp != nil { return *errResp } defer ctx.Cancel() - libVersion, libVersionNumber, sourceID := sqlite3.Version() + return h.app.DoRequest(ctx, g, func(ctx *logic.AppContext, finishSuccess func(r ginext.HTTPResponse) ginext.HTTPResponse) ginext.HTTPResponse { - err := h.app.Database.Ping(ctx) - if err != nil { - return ginresp.InternalError(err) - } + libVersion, libVersionNumber, sourceID := sqlite3.Version() + + err := h.app.Database.Ping(ctx) + if err != nil { + return ginresp.InternalError(err) + } + + return ginext.JSON(http.StatusOK, response{ + Success: true, + LibVersion: libVersion, + LibVersionNumber: libVersionNumber, + SourceID: sourceID, + }) - return ginext.JSON(http.StatusOK, response{ - Success: true, - LibVersion: libVersion, - LibVersionNumber: libVersionNumber, - SourceID: sourceID, }) } @@ -128,52 +136,56 @@ func (h CommonHandler) Health(pctx ginext.PreContext) ginext.HTTPResponse { Status string `json:"status"` } - ctx, _, errResp := pctx.Start() + ctx, g, errResp := pctx.Start() if errResp != nil { return *errResp } defer ctx.Cancel() - _, libVersionNumber, _ := sqlite3.Version() + return h.app.DoRequest(ctx, g, func(ctx *logic.AppContext, finishSuccess func(r ginext.HTTPResponse) ginext.HTTPResponse) ginext.HTTPResponse { - if libVersionNumber < 3039000 { - return ginresp.InternalError(errors.New("sqlite version too low")) - } + _, libVersionNumber, _ := sqlite3.Version() - tctx := simplectx.CreateSimpleContext(ctx, nil) + if libVersionNumber < 3039000 { + return ginresp.InternalError(errors.New("sqlite version too low")) + } - err := h.app.Database.Ping(tctx) - if err != nil { - return ginresp.InternalError(err) - } + tctx := simplectx.CreateSimpleContext(ctx, nil) - for _, subdb := range h.app.Database.List() { - - uuidKey, _ := langext.NewHexUUID() - uuidWrite, _ := langext.NewHexUUID() - - err = subdb.WriteMetaString(tctx, uuidKey, uuidWrite) + err := h.app.Database.Ping(tctx) if err != nil { return ginresp.InternalError(err) } - uuidRead, err := subdb.ReadMetaString(tctx, uuidKey) - if err != nil { - return ginresp.InternalError(err) + for _, subdb := range h.app.Database.List() { + + uuidKey, _ := langext.NewHexUUID() + uuidWrite, _ := langext.NewHexUUID() + + err = subdb.WriteMetaString(tctx, uuidKey, uuidWrite) + if err != nil { + return ginresp.InternalError(err) + } + + uuidRead, err := subdb.ReadMetaString(tctx, uuidKey) + if err != nil { + return ginresp.InternalError(err) + } + + if uuidRead == nil || uuidWrite != *uuidRead { + return ginresp.InternalError(errors.New("writing into DB was not consistent")) + } + + err = subdb.DeleteMeta(tctx, uuidKey) + if err != nil { + return ginresp.InternalError(err) + } + } - if uuidRead == nil || uuidWrite != *uuidRead { - return ginresp.InternalError(errors.New("writing into DB was not consistent")) - } + return ginext.JSON(http.StatusOK, response{Status: "ok"}) - err = subdb.DeleteMeta(tctx, uuidKey) - if err != nil { - return ginresp.InternalError(err) - } - - } - - return ginext.JSON(http.StatusOK, response{Status: "ok"}) + }) } // Sleep swaggerdoc @@ -205,21 +217,25 @@ func (h CommonHandler) Sleep(pctx ginext.PreContext) ginext.HTTPResponse { } defer ctx.Cancel() - t0 := time.Now().Format(time.RFC3339Nano) + return h.app.DoRequest(ctx, g, func(ctx *logic.AppContext, finishSuccess func(r ginext.HTTPResponse) ginext.HTTPResponse) ginext.HTTPResponse { - var u uri - if err := g.ShouldBindUri(&u); err != nil { - return ginresp.APIError(g, 400, apierr.BINDFAIL_URI_PARAM, "Failed to read uri", err) - } + t0 := time.Now().Format(time.RFC3339Nano) - time.Sleep(timeext.FromSeconds(u.Seconds)) + var u uri + if err := g.ShouldBindUri(&u); err != nil { + return ginresp.APIError(g, 400, apierr.BINDFAIL_URI_PARAM, "Failed to read uri", err) + } - t1 := time.Now().Format(time.RFC3339Nano) + time.Sleep(timeext.FromSeconds(u.Seconds)) + + t1 := time.Now().Format(time.RFC3339Nano) + + return ginext.JSON(http.StatusOK, response{ + Start: t0, + End: t1, + Duration: u.Seconds, + }) - return ginext.JSON(http.StatusOK, response{ - Start: t0, - End: t1, - Duration: u.Seconds, }) } @@ -230,14 +246,18 @@ func (h CommonHandler) NoRoute(pctx ginext.PreContext) ginext.HTTPResponse { } defer ctx.Cancel() - return ginext.JSON(http.StatusNotFound, gin.H{ - "": "================ ROUTE NOT FOUND ================", - "FullPath": g.FullPath(), - "Method": g.Request.Method, - "URL": g.Request.URL.String(), - "RequestURI": g.Request.RequestURI, - "Proto": g.Request.Proto, - "Header": g.Request.Header, - "~": "================ ROUTE NOT FOUND ================", + return h.app.DoRequest(ctx, g, func(ctx *logic.AppContext, finishSuccess func(r ginext.HTTPResponse) ginext.HTTPResponse) ginext.HTTPResponse { + + return ginext.JSON(http.StatusNotFound, gin.H{ + "": "================ ROUTE NOT FOUND ================", + "FullPath": g.FullPath(), + "Method": g.Request.Method, + "URL": g.Request.URL.String(), + "RequestURI": g.Request.RequestURI, + "Proto": g.Request.Proto, + "Header": g.Request.Header, + "~": "================ ROUTE NOT FOUND ================", + }) + }) } diff --git a/scnserver/api/handler/compat.go b/scnserver/api/handler/compat.go index 7aa177c..9c571c9 100644 --- a/scnserver/api/handler/compat.go +++ b/scnserver/api/handler/compat.go @@ -72,39 +72,42 @@ func (h CompatHandler) SendMessage(pctx ginext.PreContext) ginext.HTTPResponse { var f combined var q combined - ctx, errResp := h.app.StartRequest(g, nil, &q, nil, &f, logic.RequestOptions{IgnoreWrongContentType: true}) + ctx, g, errResp := pctx.Query(&q).Form(&f).IgnoreWrongContentType().Start() if errResp != nil { return *errResp } defer ctx.Cancel() - data := dataext.ObjectMerge(f, q) + return h.app.DoRequest(ctx, g, func(ctx *logic.AppContext, finishSuccess func(r ginext.HTTPResponse) ginext.HTTPResponse) ginext.HTTPResponse { - newid, err := h.database.ConvertCompatID(ctx, langext.Coalesce(data.UserID, -1), "userid") - if err != nil { - return ginresp.SendAPIError(g, 500, apierr.DATABASE_ERROR, hl.NONE, "Failed to query userid", err) - } - if newid == nil { - return ginresp.SendAPIError(g, 400, apierr.USER_NOT_FOUND, hl.USER_ID, "User not found (compat)", nil) - } + data := dataext.ObjectMerge(f, q) - okResp, errResp := h.app.SendMessage(g, ctx, langext.Ptr(models.UserID(*newid)), data.UserKey, nil, data.Title, data.Content, data.Priority, data.UserMessageID, data.SendTimestamp, nil) - if errResp != nil { - return *errResp - } else { - return ctx.FinishSuccess(ginext.JSON(http.StatusOK, response{ - Success: true, - ErrorID: apierr.NO_ERROR, - ErrorHighlight: -1, - Message: langext.Conditional(okResp.MessageIsOld, "Message already sent", "Message sent"), - SuppressSend: okResp.MessageIsOld, - MessageCount: okResp.User.MessagesSent, - Quota: okResp.User.QuotaUsedToday(), - IsPro: okResp.User.IsPro, - QuotaMax: okResp.User.QuotaPerDay(), - SCNMessageID: okResp.CompatMessageID, - })) - } + newid, err := h.database.ConvertCompatID(ctx, langext.Coalesce(data.UserID, -1), "userid") + if err != nil { + return ginresp.SendAPIError(g, 500, apierr.DATABASE_ERROR, hl.NONE, "Failed to query userid", err) + } + if newid == nil { + return ginresp.SendAPIError(g, 400, apierr.USER_NOT_FOUND, hl.USER_ID, "User not found (compat)", nil) + } + + okResp, errResp := h.app.SendMessage(g, ctx, langext.Ptr(models.UserID(*newid)), data.UserKey, nil, data.Title, data.Content, data.Priority, data.UserMessageID, data.SendTimestamp, nil) + if errResp != nil { + return *errResp + } else { + return finishSuccess(ginext.JSON(http.StatusOK, response{ + Success: true, + ErrorID: apierr.NO_ERROR, + ErrorHighlight: -1, + Message: langext.Conditional(okResp.MessageIsOld, "Message already sent", "Message sent"), + SuppressSend: okResp.MessageIsOld, + MessageCount: okResp.User.MessagesSent, + Quota: okResp.User.QuotaUsedToday(), + IsPro: okResp.User.IsPro, + QuotaMax: okResp.User.QuotaPerDay(), + SCNMessageID: okResp.CompatMessageID, + })) + } + }) } // Register swaggerdoc @@ -145,86 +148,90 @@ func (h CompatHandler) Register(pctx ginext.PreContext) ginext.HTTPResponse { var datq query var datb query - ctx, errResp := h.app.StartRequest(g, nil, &datq, nil, &datb, logic.RequestOptions{IgnoreWrongContentType: true}) + ctx, g, errResp := pctx.Query(&datq).Body(&datb).IgnoreWrongContentType().Start() if errResp != nil { return *errResp } defer ctx.Cancel() - data := dataext.ObjectMerge(datb, datq) + return h.app.DoRequest(ctx, g, func(ctx *logic.AppContext, finishSuccess func(r ginext.HTTPResponse) ginext.HTTPResponse) ginext.HTTPResponse { - if data.FCMToken == nil { - return ginresp.CompatAPIError(0, "Missing parameter [[fcm_token]]") - } - if data.Pro == nil { - return ginresp.CompatAPIError(0, "Missing parameter [[pro]]") - } - if data.ProToken == nil { - return ginresp.CompatAPIError(0, "Missing parameter [[pro_token]]") - } + data := dataext.ObjectMerge(datb, datq) - if data.ProToken != nil { - data.ProToken = langext.Ptr("ANDROID|v1|" + *data.ProToken) - } + if data.FCMToken == nil { + return ginresp.CompatAPIError(0, "Missing parameter [[fcm_token]]") + } + if data.Pro == nil { + return ginresp.CompatAPIError(0, "Missing parameter [[pro]]") + } + if data.ProToken == nil { + return ginresp.CompatAPIError(0, "Missing parameter [[pro_token]]") + } - if *data.Pro != "true" { - data.ProToken = nil - } + if data.ProToken != nil { + data.ProToken = langext.Ptr("ANDROID|v1|" + *data.ProToken) + } - if data.ProToken != nil { - ptok, err := h.app.VerifyProToken(ctx, *data.ProToken) + if *data.Pro != "true" { + data.ProToken = nil + } + + if data.ProToken != nil { + ptok, err := h.app.VerifyProToken(ctx, *data.ProToken) + if err != nil { + return ginresp.CompatAPIError(0, "Failed to query purchase status") + } + + if !ptok { + return ginresp.CompatAPIError(0, "Purchase token could not be verified") + } + } + + adminKey := h.app.GenerateRandomAuthKey() + + err := h.database.ClearFCMTokens(ctx, *data.FCMToken) if err != nil { - return ginresp.CompatAPIError(0, "Failed to query purchase status") + return ginresp.CompatAPIError(0, "Failed to clear existing fcm tokens") } - if !ptok { - return ginresp.CompatAPIError(0, "Purchase token could not be verified") + if data.ProToken != nil { + err := h.database.ClearProTokens(ctx, *data.ProToken) + if err != nil { + return ginresp.CompatAPIError(0, "Failed to clear existing pro tokens") + } } - } - adminKey := h.app.GenerateRandomAuthKey() - - err := h.database.ClearFCMTokens(ctx, *data.FCMToken) - if err != nil { - return ginresp.CompatAPIError(0, "Failed to clear existing fcm tokens") - } - - if data.ProToken != nil { - err := h.database.ClearProTokens(ctx, *data.ProToken) + user, err := h.database.CreateUser(ctx, data.ProToken, nil) if err != nil { - return ginresp.CompatAPIError(0, "Failed to clear existing pro tokens") + return ginresp.CompatAPIError(0, "Failed to create user in db") } - } - user, err := h.database.CreateUser(ctx, data.ProToken, nil) - if err != nil { - return ginresp.CompatAPIError(0, "Failed to create user in db") - } + _, err = h.database.CreateKeyToken(ctx, "CompatKey", user.UserID, true, make([]models.ChannelID, 0), models.TokenPermissionList{models.PermAdmin}, adminKey) + if err != nil { + return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to create admin-key in db", err) + } - _, err = h.database.CreateKeyToken(ctx, "CompatKey", user.UserID, true, make([]models.ChannelID, 0), models.TokenPermissionList{models.PermAdmin}, adminKey) - if err != nil { - return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to create admin-key in db", err) - } + _, err = h.database.CreateClient(ctx, user.UserID, models.ClientTypeAndroid, *data.FCMToken, "compat", "compat", nil) + if err != nil { + return ginresp.CompatAPIError(0, "Failed to create client in db") + } - _, err = h.database.CreateClient(ctx, user.UserID, models.ClientTypeAndroid, *data.FCMToken, "compat", "compat", nil) - if err != nil { - return ginresp.CompatAPIError(0, "Failed to create client in db") - } + oldid, err := h.database.CreateCompatID(ctx, "userid", user.UserID.String()) + if err != nil { + return ginresp.SendAPIError(g, 500, apierr.DATABASE_ERROR, hl.NONE, "Failed to create userid", err) + } - oldid, err := h.database.CreateCompatID(ctx, "userid", user.UserID.String()) - if err != nil { - return ginresp.SendAPIError(g, 500, apierr.DATABASE_ERROR, hl.NONE, "Failed to create userid", err) - } + return finishSuccess(ginext.JSON(http.StatusOK, response{ + Success: true, + Message: "New user registered", + UserID: oldid, + UserKey: adminKey, + QuotaUsed: user.QuotaUsedToday(), + QuotaMax: user.QuotaPerDay(), + IsPro: user.IsPro, + })) - return ctx.FinishSuccess(ginext.JSON(http.StatusOK, response{ - Success: true, - Message: "New user registered", - UserID: oldid, - UserKey: adminKey, - QuotaUsed: user.QuotaUsedToday(), - QuotaMax: user.QuotaPerDay(), - IsPro: user.IsPro, - })) + }) } // Info swaggerdoc @@ -264,74 +271,78 @@ func (h CompatHandler) Info(pctx ginext.PreContext) ginext.HTTPResponse { var datq query var datb query - ctx, errResp := h.app.StartRequest(g, nil, &datq, nil, &datb, logic.RequestOptions{IgnoreWrongContentType: true}) + ctx, g, errResp := pctx.Query(&datq).Body(&datb).IgnoreWrongContentType().Start() if errResp != nil { return *errResp } defer ctx.Cancel() - data := dataext.ObjectMerge(datb, datq) + return h.app.DoRequest(ctx, g, func(ctx *logic.AppContext, finishSuccess func(r ginext.HTTPResponse) ginext.HTTPResponse) ginext.HTTPResponse { - if data.UserID == nil { - return ginresp.CompatAPIError(101, "Missing parameter [[user_id]]") - } - if data.UserKey == nil { - return ginresp.CompatAPIError(102, "Missing parameter [[user_key]]") - } + data := dataext.ObjectMerge(datb, datq) - useridCompNew, err := h.database.ConvertCompatID(ctx, *data.UserID, "userid") - if err != nil { - return ginresp.SendAPIError(g, 500, apierr.DATABASE_ERROR, hl.NONE, "Failed to query userid", err) - } - if useridCompNew == nil { - return ginresp.SendAPIError(g, 400, apierr.USER_NOT_FOUND, hl.USER_ID, "User not found (compat)", nil) - } + if data.UserID == nil { + return ginresp.CompatAPIError(101, "Missing parameter [[user_id]]") + } + if data.UserKey == nil { + return ginresp.CompatAPIError(102, "Missing parameter [[user_key]]") + } - user, err := h.database.GetUser(ctx, models.UserID(*useridCompNew)) - if errors.Is(err, sql.ErrNoRows) { - return ginresp.CompatAPIError(201, "User not found") - } - if err != nil { - return ginresp.CompatAPIError(0, "Failed to query user") - } + useridCompNew, err := h.database.ConvertCompatID(ctx, *data.UserID, "userid") + if err != nil { + return ginresp.SendAPIError(g, 500, apierr.DATABASE_ERROR, hl.NONE, "Failed to query userid", err) + } + if useridCompNew == nil { + return ginresp.SendAPIError(g, 400, apierr.USER_NOT_FOUND, hl.USER_ID, "User not found (compat)", nil) + } - keytok, err := h.database.GetKeyTokenByToken(ctx, *data.UserKey) - if err != nil { - return ginresp.CompatAPIError(0, "Failed to query token") - } - if keytok == nil { - return ginresp.CompatAPIError(204, "Authentification failed") - } - if !keytok.IsAdmin(user.UserID) { - return ginresp.CompatAPIError(204, "Authentification failed") - } + user, err := h.database.GetUser(ctx, models.UserID(*useridCompNew)) + if errors.Is(err, sql.ErrNoRows) { + return ginresp.CompatAPIError(201, "User not found") + } + if err != nil { + return ginresp.CompatAPIError(0, "Failed to query user") + } - clients, err := h.database.ListClients(ctx, user.UserID) - if err != nil { - return ginresp.CompatAPIError(0, "Failed to query clients") - } + keytok, err := h.database.GetKeyTokenByToken(ctx, *data.UserKey) + if err != nil { + return ginresp.CompatAPIError(0, "Failed to query token") + } + if keytok == nil { + return ginresp.CompatAPIError(204, "Authentification failed") + } + if !keytok.IsAdmin(user.UserID) { + return ginresp.CompatAPIError(204, "Authentification failed") + } - filter := models.MessageFilter{ - Sender: langext.Ptr([]models.UserID{user.UserID}), - CompatAcknowledged: langext.Ptr(false), - } + clients, err := h.database.ListClients(ctx, user.UserID) + if err != nil { + return ginresp.CompatAPIError(0, "Failed to query clients") + } - unackCount, err := h.database.CountMessages(ctx, filter) - if err != nil { - return ginresp.CompatAPIError(0, "Failed to query user") - } + filter := models.MessageFilter{ + Sender: langext.Ptr([]models.UserID{user.UserID}), + CompatAcknowledged: langext.Ptr(false), + } - return ctx.FinishSuccess(ginext.JSON(http.StatusOK, response{ - Success: true, - Message: "ok", - UserID: *data.UserID, - UserKey: keytok.Token, - QuotaUsed: user.QuotaUsedToday(), - QuotaMax: user.QuotaPerDay(), - IsPro: langext.Conditional(user.IsPro, 1, 0), - FCMSet: len(clients) > 0, - UnackCount: unackCount, - })) + unackCount, err := h.database.CountMessages(ctx, filter) + if err != nil { + return ginresp.CompatAPIError(0, "Failed to query user") + } + + return finishSuccess(ginext.JSON(http.StatusOK, response{ + Success: true, + Message: "ok", + UserID: *data.UserID, + UserKey: keytok.Token, + QuotaUsed: user.QuotaUsedToday(), + QuotaMax: user.QuotaPerDay(), + IsPro: langext.Conditional(user.IsPro, 1, 0), + FCMSet: len(clients) > 0, + UnackCount: unackCount, + })) + + }) } // Ack swaggerdoc @@ -369,77 +380,81 @@ func (h CompatHandler) Ack(pctx ginext.PreContext) ginext.HTTPResponse { var datq query var datb query - ctx, errResp := h.app.StartRequest(g, nil, &datq, nil, &datb, logic.RequestOptions{IgnoreWrongContentType: true}) + ctx, g, errResp := pctx.Query(&datq).Body(&datb).IgnoreWrongContentType().Start() if errResp != nil { return *errResp } defer ctx.Cancel() - data := dataext.ObjectMerge(datb, datq) + return h.app.DoRequest(ctx, g, func(ctx *logic.AppContext, finishSuccess func(r ginext.HTTPResponse) ginext.HTTPResponse) ginext.HTTPResponse { - if data.UserID == nil { - return ginresp.CompatAPIError(101, "Missing parameter [[user_id]]") - } - if data.UserKey == nil { - return ginresp.CompatAPIError(102, "Missing parameter [[user_key]]") - } - if data.MessageID == nil { - return ginresp.CompatAPIError(103, "Missing parameter [[scn_msg_id]]") - } + data := dataext.ObjectMerge(datb, datq) - useridCompNew, err := h.database.ConvertCompatID(ctx, *data.UserID, "userid") - if err != nil { - return ginresp.SendAPIError(g, 500, apierr.DATABASE_ERROR, hl.NONE, "Failed to query userid", err) - } - if useridCompNew == nil { - return ginresp.SendAPIError(g, 400, apierr.USER_NOT_FOUND, hl.USER_ID, fmt.Sprintf("User %d not found (compat)", *data.UserID), nil) - } - - user, err := h.database.GetUser(ctx, models.UserID(*useridCompNew)) - if errors.Is(err, sql.ErrNoRows) { - return ginresp.CompatAPIError(201, "User not found") - } - if err != nil { - return ginresp.CompatAPIError(0, "Failed to query user") - } - - keytok, err := h.database.GetKeyTokenByToken(ctx, *data.UserKey) - if err != nil { - return ginresp.CompatAPIError(0, "Failed to query token") - } - if keytok == nil { - return ginresp.CompatAPIError(204, "Authentification failed") - } - if !keytok.IsAdmin(user.UserID) { - return ginresp.CompatAPIError(204, "Authentification failed") - } - - messageIdComp, err := h.database.ConvertCompatID(ctx, *data.MessageID, "messageid") - if err != nil { - return ginresp.SendAPIError(g, 500, apierr.DATABASE_ERROR, hl.NONE, "Failed to query messageid", err) - } - if messageIdComp == nil { - return ginresp.SendAPIError(g, 400, apierr.MESSAGE_NOT_FOUND, hl.NONE, fmt.Sprintf("Message %d not found (compat)", *data.MessageID), nil) - } - - ackBefore, err := h.database.GetAck(ctx, models.MessageID(*messageIdComp)) - if err != nil { - return ginresp.SendAPIError(g, 500, apierr.DATABASE_ERROR, hl.NONE, "Failed to query ack", err) - } - - if !ackBefore { - err = h.database.SetAck(ctx, user.UserID, models.MessageID(*messageIdComp)) - if err != nil { - return ginresp.SendAPIError(g, 500, apierr.DATABASE_ERROR, hl.NONE, "Failed to set ack", err) + if data.UserID == nil { + return ginresp.CompatAPIError(101, "Missing parameter [[user_id]]") + } + if data.UserKey == nil { + return ginresp.CompatAPIError(102, "Missing parameter [[user_key]]") + } + if data.MessageID == nil { + return ginresp.CompatAPIError(103, "Missing parameter [[scn_msg_id]]") } - } - return ctx.FinishSuccess(ginext.JSON(http.StatusOK, response{ - Success: true, - Message: "ok", - PrevAckValue: langext.Conditional(ackBefore, 1, 0), - NewAckValue: 1, - })) + useridCompNew, err := h.database.ConvertCompatID(ctx, *data.UserID, "userid") + if err != nil { + return ginresp.SendAPIError(g, 500, apierr.DATABASE_ERROR, hl.NONE, "Failed to query userid", err) + } + if useridCompNew == nil { + return ginresp.SendAPIError(g, 400, apierr.USER_NOT_FOUND, hl.USER_ID, fmt.Sprintf("User %d not found (compat)", *data.UserID), nil) + } + + user, err := h.database.GetUser(ctx, models.UserID(*useridCompNew)) + if errors.Is(err, sql.ErrNoRows) { + return ginresp.CompatAPIError(201, "User not found") + } + if err != nil { + return ginresp.CompatAPIError(0, "Failed to query user") + } + + keytok, err := h.database.GetKeyTokenByToken(ctx, *data.UserKey) + if err != nil { + return ginresp.CompatAPIError(0, "Failed to query token") + } + if keytok == nil { + return ginresp.CompatAPIError(204, "Authentification failed") + } + if !keytok.IsAdmin(user.UserID) { + return ginresp.CompatAPIError(204, "Authentification failed") + } + + messageIdComp, err := h.database.ConvertCompatID(ctx, *data.MessageID, "messageid") + if err != nil { + return ginresp.SendAPIError(g, 500, apierr.DATABASE_ERROR, hl.NONE, "Failed to query messageid", err) + } + if messageIdComp == nil { + return ginresp.SendAPIError(g, 400, apierr.MESSAGE_NOT_FOUND, hl.NONE, fmt.Sprintf("Message %d not found (compat)", *data.MessageID), nil) + } + + ackBefore, err := h.database.GetAck(ctx, models.MessageID(*messageIdComp)) + if err != nil { + return ginresp.SendAPIError(g, 500, apierr.DATABASE_ERROR, hl.NONE, "Failed to query ack", err) + } + + if !ackBefore { + err = h.database.SetAck(ctx, user.UserID, models.MessageID(*messageIdComp)) + if err != nil { + return ginresp.SendAPIError(g, 500, apierr.DATABASE_ERROR, hl.NONE, "Failed to set ack", err) + } + } + + return finishSuccess(ginext.JSON(http.StatusOK, response{ + Success: true, + Message: "ok", + PrevAckValue: langext.Conditional(ackBefore, 1, 0), + NewAckValue: 1, + })) + + }) } // Requery swaggerdoc @@ -474,83 +489,87 @@ func (h CompatHandler) Requery(pctx ginext.PreContext) ginext.HTTPResponse { var datq query var datb query - ctx, errResp := h.app.StartRequest(g, nil, &datq, nil, &datb, logic.RequestOptions{IgnoreWrongContentType: true}) + ctx, g, errResp := pctx.Query(&datq).Body(&datb).IgnoreWrongContentType().Start() if errResp != nil { return *errResp } defer ctx.Cancel() - data := dataext.ObjectMerge(datb, datq) + return h.app.DoRequest(ctx, g, func(ctx *logic.AppContext, finishSuccess func(r ginext.HTTPResponse) ginext.HTTPResponse) ginext.HTTPResponse { - if data.UserID == nil { - return ginresp.CompatAPIError(101, "Missing parameter [[user_id]]") - } - if data.UserKey == nil { - return ginresp.CompatAPIError(102, "Missing parameter [[user_key]]") - } + data := dataext.ObjectMerge(datb, datq) - useridCompNew, err := h.database.ConvertCompatID(ctx, *data.UserID, "userid") - if err != nil { - return ginresp.SendAPIError(g, 500, apierr.DATABASE_ERROR, hl.NONE, "Failed to query userid", err) - } - if useridCompNew == nil { - return ginresp.SendAPIError(g, 400, apierr.USER_NOT_FOUND, hl.USER_ID, "User not found (compat)", nil) - } - - user, err := h.database.GetUser(ctx, models.UserID(*useridCompNew)) - if errors.Is(err, sql.ErrNoRows) { - return ginresp.CompatAPIError(201, "User not found") - } - if err != nil { - return ginresp.CompatAPIError(0, "Failed to query user") - } - - keytok, err := h.database.GetKeyTokenByToken(ctx, *data.UserKey) - if err != nil { - return ginresp.CompatAPIError(0, "Failed to query token") - } - if keytok == nil { - return ginresp.CompatAPIError(204, "Authentification failed") - } - if !keytok.IsAdmin(user.UserID) { - return ginresp.CompatAPIError(204, "Authentification failed") - } - - filter := models.MessageFilter{ - Sender: langext.Ptr([]models.UserID{user.UserID}), - CompatAcknowledged: langext.Ptr(false), - } - - msgs, _, err := h.database.ListMessages(ctx, filter, langext.Ptr(16), ct.Start()) - if err != nil { - return ginresp.CompatAPIError(0, "Failed to query user") - } - - compMsgs := make([]models.CompatMessage, 0, len(msgs)) - for _, v := range msgs { - - messageIdComp, err := h.database.ConvertToCompatIDOrCreate(ctx, "messageid", v.MessageID.String()) - if err != nil { - return ginresp.SendAPIError(g, 500, apierr.DATABASE_ERROR, hl.NONE, "Failed to query/create messageid", err) + if data.UserID == nil { + return ginresp.CompatAPIError(101, "Missing parameter [[user_id]]") + } + if data.UserKey == nil { + return ginresp.CompatAPIError(102, "Missing parameter [[user_key]]") } - compMsgs = append(compMsgs, models.CompatMessage{ - Title: v.Title, - Body: v.Content, - Priority: v.Priority, - Timestamp: v.Timestamp().Unix(), - UserMessageID: v.UserMessageID, - SCNMessageID: messageIdComp, - Trimmed: nil, - }) - } + useridCompNew, err := h.database.ConvertCompatID(ctx, *data.UserID, "userid") + if err != nil { + return ginresp.SendAPIError(g, 500, apierr.DATABASE_ERROR, hl.NONE, "Failed to query userid", err) + } + if useridCompNew == nil { + return ginresp.SendAPIError(g, 400, apierr.USER_NOT_FOUND, hl.USER_ID, "User not found (compat)", nil) + } - return ctx.FinishSuccess(ginext.JSON(http.StatusOK, response{ - Success: true, - Message: "ok", - Count: len(compMsgs), - Data: compMsgs, - })) + user, err := h.database.GetUser(ctx, models.UserID(*useridCompNew)) + if errors.Is(err, sql.ErrNoRows) { + return ginresp.CompatAPIError(201, "User not found") + } + if err != nil { + return ginresp.CompatAPIError(0, "Failed to query user") + } + + keytok, err := h.database.GetKeyTokenByToken(ctx, *data.UserKey) + if err != nil { + return ginresp.CompatAPIError(0, "Failed to query token") + } + if keytok == nil { + return ginresp.CompatAPIError(204, "Authentification failed") + } + if !keytok.IsAdmin(user.UserID) { + return ginresp.CompatAPIError(204, "Authentification failed") + } + + filter := models.MessageFilter{ + Sender: langext.Ptr([]models.UserID{user.UserID}), + CompatAcknowledged: langext.Ptr(false), + } + + msgs, _, err := h.database.ListMessages(ctx, filter, langext.Ptr(16), ct.Start()) + if err != nil { + return ginresp.CompatAPIError(0, "Failed to query user") + } + + compMsgs := make([]models.CompatMessage, 0, len(msgs)) + for _, v := range msgs { + + messageIdComp, err := h.database.ConvertToCompatIDOrCreate(ctx, "messageid", v.MessageID.String()) + if err != nil { + return ginresp.SendAPIError(g, 500, apierr.DATABASE_ERROR, hl.NONE, "Failed to query/create messageid", err) + } + + compMsgs = append(compMsgs, models.CompatMessage{ + Title: v.Title, + Body: v.Content, + Priority: v.Priority, + Timestamp: v.Timestamp().Unix(), + UserMessageID: v.UserMessageID, + SCNMessageID: messageIdComp, + Trimmed: nil, + }) + } + + return finishSuccess(ginext.JSON(http.StatusOK, response{ + Success: true, + Message: "ok", + Count: len(compMsgs), + Data: compMsgs, + })) + + }) } // Update swaggerdoc @@ -591,97 +610,101 @@ func (h CompatHandler) Update(pctx ginext.PreContext) ginext.HTTPResponse { var datq query var datb query - ctx, errResp := h.app.StartRequest(g, nil, &datq, nil, &datb, logic.RequestOptions{IgnoreWrongContentType: true}) + ctx, g, errResp := pctx.Query(&datq).Body(&datb).IgnoreWrongContentType().Start() if errResp != nil { return *errResp } defer ctx.Cancel() - data := dataext.ObjectMerge(datb, datq) + return h.app.DoRequest(ctx, g, func(ctx *logic.AppContext, finishSuccess func(r ginext.HTTPResponse) ginext.HTTPResponse) ginext.HTTPResponse { - if data.UserID == nil { - return ginresp.CompatAPIError(101, "Missing parameter [[user_id]]") - } - if data.UserKey == nil { - return ginresp.CompatAPIError(102, "Missing parameter [[user_key]]") - } + data := dataext.ObjectMerge(datb, datq) - useridCompNew, err := h.database.ConvertCompatID(ctx, *data.UserID, "userid") - if err != nil { - return ginresp.SendAPIError(g, 500, apierr.DATABASE_ERROR, hl.NONE, "Failed to query userid", err) - } - if useridCompNew == nil { - return ginresp.SendAPIError(g, 400, apierr.USER_NOT_FOUND, hl.USER_ID, "User not found (compat)", nil) - } + if data.UserID == nil { + return ginresp.CompatAPIError(101, "Missing parameter [[user_id]]") + } + if data.UserKey == nil { + return ginresp.CompatAPIError(102, "Missing parameter [[user_key]]") + } - user, err := h.database.GetUser(ctx, models.UserID(*useridCompNew)) - if errors.Is(err, sql.ErrNoRows) { - return ginresp.CompatAPIError(201, "User not found") - } - if err != nil { - return ginresp.CompatAPIError(0, "Failed to query user") - } + useridCompNew, err := h.database.ConvertCompatID(ctx, *data.UserID, "userid") + if err != nil { + return ginresp.SendAPIError(g, 500, apierr.DATABASE_ERROR, hl.NONE, "Failed to query userid", err) + } + if useridCompNew == nil { + return ginresp.SendAPIError(g, 400, apierr.USER_NOT_FOUND, hl.USER_ID, "User not found (compat)", nil) + } - keytok, err := h.database.GetKeyTokenByToken(ctx, *data.UserKey) - if err != nil { - return ginresp.CompatAPIError(0, "Failed to query token") - } - if keytok == nil { - return ginresp.CompatAPIError(204, "Authentification failed") - } - if !keytok.IsAdmin(user.UserID) { - return ginresp.CompatAPIError(204, "Authentification failed") - } + user, err := h.database.GetUser(ctx, models.UserID(*useridCompNew)) + if errors.Is(err, sql.ErrNoRows) { + return ginresp.CompatAPIError(201, "User not found") + } + if err != nil { + return ginresp.CompatAPIError(0, "Failed to query user") + } - clients, err := h.database.ListClients(ctx, user.UserID) - if err != nil { - return ginresp.CompatAPIError(0, "Failed to list clients") - } + keytok, err := h.database.GetKeyTokenByToken(ctx, *data.UserKey) + if err != nil { + return ginresp.CompatAPIError(0, "Failed to query token") + } + if keytok == nil { + return ginresp.CompatAPIError(204, "Authentification failed") + } + if !keytok.IsAdmin(user.UserID) { + return ginresp.CompatAPIError(204, "Authentification failed") + } - newAdminKey := h.app.GenerateRandomAuthKey() + clients, err := h.database.ListClients(ctx, user.UserID) + if err != nil { + return ginresp.CompatAPIError(0, "Failed to list clients") + } - _, err = h.database.CreateKeyToken(ctx, "CompatKey", user.UserID, true, make([]models.ChannelID, 0), models.TokenPermissionList{models.PermAdmin}, newAdminKey) - if err != nil { - return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to create admin-key in db", err) - } + newAdminKey := h.app.GenerateRandomAuthKey() - err = h.database.DeleteKeyToken(ctx, keytok.KeyTokenID) - if err != nil { - return ginresp.CompatAPIError(0, "Failed to update keys") - } + _, err = h.database.CreateKeyToken(ctx, "CompatKey", user.UserID, true, make([]models.ChannelID, 0), models.TokenPermissionList{models.PermAdmin}, newAdminKey) + if err != nil { + return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to create admin-key in db", err) + } - if data.FCMToken != nil { + err = h.database.DeleteKeyToken(ctx, keytok.KeyTokenID) + if err != nil { + return ginresp.CompatAPIError(0, "Failed to update keys") + } - for _, client := range clients { + if data.FCMToken != nil { - err = h.database.DeleteClient(ctx, client.ClientID) + for _, client := range clients { + + err = h.database.DeleteClient(ctx, client.ClientID) + if err != nil { + return ginresp.CompatAPIError(0, "Failed to delete client") + } + + } + + _, err = h.database.CreateClient(ctx, user.UserID, models.ClientTypeAndroid, *data.FCMToken, "compat", "compat", nil) if err != nil { - return ginresp.CompatAPIError(0, "Failed to delete client") + return ginresp.CompatAPIError(0, "Failed to create client") } } - _, err = h.database.CreateClient(ctx, user.UserID, models.ClientTypeAndroid, *data.FCMToken, "compat", "compat", nil) + user, err = h.database.GetUser(ctx, user.UserID) if err != nil { - return ginresp.CompatAPIError(0, "Failed to create client") + return ginresp.CompatAPIError(0, "Failed to query user") } - } + return finishSuccess(ginext.JSON(http.StatusOK, response{ + Success: true, + Message: "user updated", + UserID: *data.UserID, + UserKey: newAdminKey, + QuotaUsed: user.QuotaUsedToday(), + QuotaMax: user.QuotaPerDay(), + IsPro: langext.Conditional(user.IsPro, 1, 0), + })) - user, err = h.database.GetUser(ctx, user.UserID) - if err != nil { - return ginresp.CompatAPIError(0, "Failed to query user") - } - - return ctx.FinishSuccess(ginext.JSON(http.StatusOK, response{ - Success: true, - Message: "user updated", - UserID: *data.UserID, - UserKey: newAdminKey, - QuotaUsed: user.QuotaUsedToday(), - QuotaMax: user.QuotaPerDay(), - IsPro: langext.Conditional(user.IsPro, 1, 0), - })) + }) } // Expand swaggerdoc @@ -718,80 +741,84 @@ func (h CompatHandler) Expand(pctx ginext.PreContext) ginext.HTTPResponse { var datq query var datb query - ctx, errResp := h.app.StartRequest(g, nil, &datq, nil, &datb, logic.RequestOptions{IgnoreWrongContentType: true}) + ctx, g, errResp := pctx.Query(&datq).Body(&datb).IgnoreWrongContentType().Start() if errResp != nil { return *errResp } defer ctx.Cancel() - data := dataext.ObjectMerge(datb, datq) + return h.app.DoRequest(ctx, g, func(ctx *logic.AppContext, finishSuccess func(r ginext.HTTPResponse) ginext.HTTPResponse) ginext.HTTPResponse { - if data.UserID == nil { - return ginresp.CompatAPIError(101, "Missing parameter [[user_id]]") - } - if data.UserKey == nil { - return ginresp.CompatAPIError(102, "Missing parameter [[user_key]]") - } - if data.MessageID == nil { - return ginresp.CompatAPIError(103, "Missing parameter [[scn_msg_id]]") - } + data := dataext.ObjectMerge(datb, datq) - useridCompNew, err := h.database.ConvertCompatID(ctx, *data.UserID, "userid") - if err != nil { - return ginresp.SendAPIError(g, 500, apierr.DATABASE_ERROR, hl.NONE, "Failed to query userid", err) - } - if useridCompNew == nil { - return ginresp.SendAPIError(g, 400, apierr.USER_NOT_FOUND, hl.USER_ID, "User not found (compat)", nil) - } + if data.UserID == nil { + return ginresp.CompatAPIError(101, "Missing parameter [[user_id]]") + } + if data.UserKey == nil { + return ginresp.CompatAPIError(102, "Missing parameter [[user_key]]") + } + if data.MessageID == nil { + return ginresp.CompatAPIError(103, "Missing parameter [[scn_msg_id]]") + } - user, err := h.database.GetUser(ctx, models.UserID(*useridCompNew)) - if errors.Is(err, sql.ErrNoRows) { - return ginresp.CompatAPIError(201, "User not found") - } - if err != nil { - return ginresp.CompatAPIError(0, "Failed to query user") - } + useridCompNew, err := h.database.ConvertCompatID(ctx, *data.UserID, "userid") + if err != nil { + return ginresp.SendAPIError(g, 500, apierr.DATABASE_ERROR, hl.NONE, "Failed to query userid", err) + } + if useridCompNew == nil { + return ginresp.SendAPIError(g, 400, apierr.USER_NOT_FOUND, hl.USER_ID, "User not found (compat)", nil) + } - keytok, err := h.database.GetKeyTokenByToken(ctx, *data.UserKey) - if err != nil { - return ginresp.CompatAPIError(0, "Failed to query token") - } - if keytok == nil { - return ginresp.CompatAPIError(204, "Authentification failed") - } - if !keytok.IsAdmin(user.UserID) { - return ginresp.CompatAPIError(204, "Authentification failed") - } + user, err := h.database.GetUser(ctx, models.UserID(*useridCompNew)) + if errors.Is(err, sql.ErrNoRows) { + return ginresp.CompatAPIError(201, "User not found") + } + if err != nil { + return ginresp.CompatAPIError(0, "Failed to query user") + } - messageCompNew, err := h.database.ConvertCompatID(ctx, *data.MessageID, "messageid") - if err != nil { - return ginresp.SendAPIError(g, 500, apierr.DATABASE_ERROR, hl.NONE, "Failed to query messagid", err) - } - if messageCompNew == nil { - return ginresp.CompatAPIError(301, "Message not found") - } + keytok, err := h.database.GetKeyTokenByToken(ctx, *data.UserKey) + if err != nil { + return ginresp.CompatAPIError(0, "Failed to query token") + } + if keytok == nil { + return ginresp.CompatAPIError(204, "Authentification failed") + } + if !keytok.IsAdmin(user.UserID) { + return ginresp.CompatAPIError(204, "Authentification failed") + } - msg, err := h.database.GetMessage(ctx, models.MessageID(*messageCompNew), false) - if errors.Is(err, sql.ErrNoRows) { - return ginresp.CompatAPIError(301, "Message not found") - } - if err != nil { - return ginresp.CompatAPIError(0, "Failed to query message") - } + messageCompNew, err := h.database.ConvertCompatID(ctx, *data.MessageID, "messageid") + if err != nil { + return ginresp.SendAPIError(g, 500, apierr.DATABASE_ERROR, hl.NONE, "Failed to query messagid", err) + } + if messageCompNew == nil { + return ginresp.CompatAPIError(301, "Message not found") + } - return ctx.FinishSuccess(ginext.JSON(http.StatusOK, response{ - Success: true, - Message: "ok", - Data: models.CompatMessage{ - Title: msg.Title, - Body: msg.Content, - Trimmed: langext.Ptr(false), - Priority: msg.Priority, - Timestamp: msg.Timestamp().Unix(), - UserMessageID: msg.UserMessageID, - SCNMessageID: *data.MessageID, - }, - })) + msg, err := h.database.GetMessage(ctx, models.MessageID(*messageCompNew), false) + if errors.Is(err, sql.ErrNoRows) { + return ginresp.CompatAPIError(301, "Message not found") + } + if err != nil { + return ginresp.CompatAPIError(0, "Failed to query message") + } + + return finishSuccess(ginext.JSON(http.StatusOK, response{ + Success: true, + Message: "ok", + Data: models.CompatMessage{ + Title: msg.Title, + Body: msg.Content, + Trimmed: langext.Ptr(false), + Priority: msg.Priority, + Timestamp: msg.Timestamp().Unix(), + UserMessageID: msg.UserMessageID, + SCNMessageID: *data.MessageID, + }, + })) + + }) } // Upgrade swaggerdoc @@ -834,99 +861,103 @@ func (h CompatHandler) Upgrade(pctx ginext.PreContext) ginext.HTTPResponse { var datq query var datb query - ctx, errResp := h.app.StartRequest(g, nil, &datq, nil, &datb, logic.RequestOptions{IgnoreWrongContentType: true}) + ctx, g, errResp := pctx.Query(&datq).Body(&datb).IgnoreWrongContentType().Start() if errResp != nil { return *errResp } defer ctx.Cancel() - data := dataext.ObjectMerge(datb, datq) + return h.app.DoRequest(ctx, g, func(ctx *logic.AppContext, finishSuccess func(r ginext.HTTPResponse) ginext.HTTPResponse) ginext.HTTPResponse { - if data.UserID == nil { - return ginresp.CompatAPIError(101, "Missing parameter [[user_id]]") - } - if data.UserKey == nil { - return ginresp.CompatAPIError(102, "Missing parameter [[user_key]]") - } - if data.Pro == nil { - return ginresp.CompatAPIError(103, "Missing parameter [[pro]]") - } - if data.ProToken == nil { - return ginresp.CompatAPIError(104, "Missing parameter [[pro_token]]") - } + data := dataext.ObjectMerge(datb, datq) - useridCompNew, err := h.database.ConvertCompatID(ctx, *data.UserID, "userid") - if err != nil { - return ginresp.SendAPIError(g, 500, apierr.DATABASE_ERROR, hl.NONE, "Failed to query userid", err) - } - if useridCompNew == nil { - return ginresp.SendAPIError(g, 400, apierr.USER_NOT_FOUND, hl.USER_ID, "User not found (compat)", nil) - } + if data.UserID == nil { + return ginresp.CompatAPIError(101, "Missing parameter [[user_id]]") + } + if data.UserKey == nil { + return ginresp.CompatAPIError(102, "Missing parameter [[user_key]]") + } + if data.Pro == nil { + return ginresp.CompatAPIError(103, "Missing parameter [[pro]]") + } + if data.ProToken == nil { + return ginresp.CompatAPIError(104, "Missing parameter [[pro_token]]") + } - user, err := h.database.GetUser(ctx, models.UserID(*useridCompNew)) - if errors.Is(err, sql.ErrNoRows) { - return ginresp.CompatAPIError(201, "User not found") - } - if err != nil { - return ginresp.CompatAPIError(0, "Failed to query user") - } - - keytok, err := h.database.GetKeyTokenByToken(ctx, *data.UserKey) - if err != nil { - return ginresp.CompatAPIError(0, "Failed to query token") - } - if keytok == nil { - return ginresp.CompatAPIError(204, "Authentification failed") - } - if !keytok.IsAdmin(user.UserID) { - return ginresp.CompatAPIError(204, "Authentification failed") - } - - if data.ProToken != nil { - data.ProToken = langext.Ptr("ANDROID|v1|" + *data.ProToken) - } - - if *data.Pro != "true" { - data.ProToken = nil - } - - if data.ProToken != nil { - ptok, err := h.app.VerifyProToken(ctx, *data.ProToken) + useridCompNew, err := h.database.ConvertCompatID(ctx, *data.UserID, "userid") if err != nil { - return ginresp.CompatAPIError(0, "Failed to query purchase status") + return ginresp.SendAPIError(g, 500, apierr.DATABASE_ERROR, hl.NONE, "Failed to query userid", err) + } + if useridCompNew == nil { + return ginresp.SendAPIError(g, 400, apierr.USER_NOT_FOUND, hl.USER_ID, "User not found (compat)", nil) } - if !ptok { - return ginresp.CompatAPIError(0, "Purchase token could not be verified") + user, err := h.database.GetUser(ctx, models.UserID(*useridCompNew)) + if errors.Is(err, sql.ErrNoRows) { + return ginresp.CompatAPIError(201, "User not found") } - - err = h.database.ClearProTokens(ctx, *data.ProToken) if err != nil { - return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to clear existing fcm tokens", err) + return ginresp.CompatAPIError(0, "Failed to query user") } - err = h.database.UpdateUserProToken(ctx, user.UserID, langext.Ptr(*data.ProToken)) + keytok, err := h.database.GetKeyTokenByToken(ctx, *data.UserKey) if err != nil { - return ginresp.CompatAPIError(0, "Failed to update user") + return ginresp.CompatAPIError(0, "Failed to query token") } - } else { - err = h.database.UpdateUserProToken(ctx, user.UserID, nil) + if keytok == nil { + return ginresp.CompatAPIError(204, "Authentification failed") + } + if !keytok.IsAdmin(user.UserID) { + return ginresp.CompatAPIError(204, "Authentification failed") + } + + if data.ProToken != nil { + data.ProToken = langext.Ptr("ANDROID|v1|" + *data.ProToken) + } + + if *data.Pro != "true" { + data.ProToken = nil + } + + if data.ProToken != nil { + ptok, err := h.app.VerifyProToken(ctx, *data.ProToken) + if err != nil { + return ginresp.CompatAPIError(0, "Failed to query purchase status") + } + + if !ptok { + return ginresp.CompatAPIError(0, "Purchase token could not be verified") + } + + err = h.database.ClearProTokens(ctx, *data.ProToken) + if err != nil { + return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to clear existing fcm tokens", err) + } + + err = h.database.UpdateUserProToken(ctx, user.UserID, langext.Ptr(*data.ProToken)) + if err != nil { + return ginresp.CompatAPIError(0, "Failed to update user") + } + } else { + err = h.database.UpdateUserProToken(ctx, user.UserID, nil) + if err != nil { + return ginresp.CompatAPIError(0, "Failed to update user") + } + } + + user, err = h.database.GetUser(ctx, user.UserID) if err != nil { - return ginresp.CompatAPIError(0, "Failed to update user") + return ginresp.CompatAPIError(0, "Failed to query user") } - } - user, err = h.database.GetUser(ctx, user.UserID) - if err != nil { - return ginresp.CompatAPIError(0, "Failed to query user") - } + return finishSuccess(ginext.JSON(http.StatusOK, response{ + Success: true, + Message: "user updated", + UserID: *data.UserID, + QuotaUsed: user.QuotaUsedToday(), + QuotaMax: user.QuotaPerDay(), + IsPro: user.IsPro, + })) - return ctx.FinishSuccess(ginext.JSON(http.StatusOK, response{ - Success: true, - Message: "user updated", - UserID: *data.UserID, - QuotaUsed: user.QuotaUsedToday(), - QuotaMax: user.QuotaPerDay(), - IsPro: user.IsPro, - })) + }) } diff --git a/scnserver/api/handler/external.go b/scnserver/api/handler/external.go index ee578de..6f5725d 100644 --- a/scnserver/api/handler/external.go +++ b/scnserver/api/handler/external.go @@ -74,61 +74,65 @@ func (h ExternalHandler) UptimeKuma(pctx ginext.PreContext) ginext.HTTPResponse var b body var q query - ctx, httpErr := h.app.StartRequest(g, nil, &q, &b, nil) - if httpErr != nil { - return *httpErr - } - defer ctx.Cancel() - - if b.Heartbeat == nil { - return ginresp.APIError(g, 400, apierr.BINDFAIL_BODY_PARAM, "missing field 'heartbeat' in request body", nil) - } - if b.Monitor == nil { - return ginresp.APIError(g, 400, apierr.BINDFAIL_BODY_PARAM, "missing field 'monitor' in request body", nil) - } - if b.Msg == nil { - return ginresp.APIError(g, 400, apierr.BINDFAIL_BODY_PARAM, "missing field 'msg' in request body", nil) - } - - title := langext.Conditional(b.Heartbeat.Status == 1, fmt.Sprintf("Monitor %v is back online", b.Monitor.Name), fmt.Sprintf("Monitor %v went down!", b.Monitor.Name)) - - content := b.Heartbeat.Msg - - var timestamp *float64 = nil - if tz, err := time.LoadLocation(b.Heartbeat.Timezone); err == nil { - if ts, err := time.ParseInLocation("2006-01-02 15:04:05", b.Heartbeat.LocalDateTime, tz); err == nil { - timestamp = langext.Ptr(float64(ts.Unix())) - } - } - - var channel *string = nil - if q.Channel != nil { - channel = q.Channel - } - if q.ChannelUp != nil && b.Heartbeat.Status == 1 { - channel = q.ChannelUp - } - if q.ChannelDown != nil && b.Heartbeat.Status != 1 { - channel = q.ChannelDown - } - - var priority *int = nil - if q.Priority != nil { - priority = q.Priority - } - if q.PriorityUp != nil && b.Heartbeat.Status == 1 { - priority = q.PriorityUp - } - if q.PriorityDown != nil && b.Heartbeat.Status != 1 { - priority = q.PriorityDown - } - - okResp, errResp := h.app.SendMessage(g, ctx, q.UserID, q.KeyToken, channel, &title, &content, priority, nil, timestamp, q.SenderName) + ctx, g, errResp := pctx.Query(&q).Body(&b).Start() if errResp != nil { return *errResp } + defer ctx.Cancel() - return ctx.FinishSuccess(ginext.JSON(http.StatusOK, response{ - MessageID: okResp.Message.MessageID, - })) + return h.app.DoRequest(ctx, g, func(ctx *logic.AppContext, finishSuccess func(r ginext.HTTPResponse) ginext.HTTPResponse) ginext.HTTPResponse { + + if b.Heartbeat == nil { + return ginresp.APIError(g, 400, apierr.BINDFAIL_BODY_PARAM, "missing field 'heartbeat' in request body", nil) + } + if b.Monitor == nil { + return ginresp.APIError(g, 400, apierr.BINDFAIL_BODY_PARAM, "missing field 'monitor' in request body", nil) + } + if b.Msg == nil { + return ginresp.APIError(g, 400, apierr.BINDFAIL_BODY_PARAM, "missing field 'msg' in request body", nil) + } + + title := langext.Conditional(b.Heartbeat.Status == 1, fmt.Sprintf("Monitor %v is back online", b.Monitor.Name), fmt.Sprintf("Monitor %v went down!", b.Monitor.Name)) + + content := b.Heartbeat.Msg + + var timestamp *float64 = nil + if tz, err := time.LoadLocation(b.Heartbeat.Timezone); err == nil { + if ts, err := time.ParseInLocation("2006-01-02 15:04:05", b.Heartbeat.LocalDateTime, tz); err == nil { + timestamp = langext.Ptr(float64(ts.Unix())) + } + } + + var channel *string = nil + if q.Channel != nil { + channel = q.Channel + } + if q.ChannelUp != nil && b.Heartbeat.Status == 1 { + channel = q.ChannelUp + } + if q.ChannelDown != nil && b.Heartbeat.Status != 1 { + channel = q.ChannelDown + } + + var priority *int = nil + if q.Priority != nil { + priority = q.Priority + } + if q.PriorityUp != nil && b.Heartbeat.Status == 1 { + priority = q.PriorityUp + } + if q.PriorityDown != nil && b.Heartbeat.Status != 1 { + priority = q.PriorityDown + } + + okResp, errResp := h.app.SendMessage(g, ctx, q.UserID, q.KeyToken, channel, &title, &content, priority, nil, timestamp, q.SenderName) + if errResp != nil { + return *errResp + } + + return finishSuccess(ginext.JSON(http.StatusOK, response{ + MessageID: okResp.Message.MessageID, + })) + + }) } diff --git a/scnserver/api/handler/message.go b/scnserver/api/handler/message.go index d39fff3..a07246d 100644 --- a/scnserver/api/handler/message.go +++ b/scnserver/api/handler/message.go @@ -77,30 +77,34 @@ func (h MessageHandler) SendMessage(pctx ginext.PreContext) ginext.HTTPResponse var b combined var q combined var f combined - ctx, g, errResp := h.app.StartRequest(pctx.Form(&f).Query(&q).Body(&b).Start()) + ctx, g, errResp := pctx.Form(&f).Query(&q).Body(&b).IgnoreWrongContentType().Start() if errResp != nil { return *errResp } defer ctx.Cancel() - // query has highest prio, then form, then json - data := dataext.ObjectMerge(dataext.ObjectMerge(b, f), q) + return h.app.DoRequest(ctx, g, func(ctx *logic.AppContext, finishSuccess func(r ginext.HTTPResponse) ginext.HTTPResponse) ginext.HTTPResponse { - okResp, errResp := h.app.SendMessage(g, ctx, data.UserID, data.KeyToken, data.Channel, data.Title, data.Content, data.Priority, data.UserMessageID, data.SendTimestamp, data.SenderName) - if errResp != nil { - return *errResp - } else { - return ctx.FinishSuccess(ginext.JSON(http.StatusOK, response{ - Success: true, - ErrorID: apierr.NO_ERROR, - ErrorHighlight: -1, - Message: langext.Conditional(okResp.MessageIsOld, "Message already sent", "Message sent"), - SuppressSend: okResp.MessageIsOld, - MessageCount: okResp.User.MessagesSent, - Quota: okResp.User.QuotaUsedToday(), - IsPro: okResp.User.IsPro, - QuotaMax: okResp.User.QuotaPerDay(), - SCNMessageID: okResp.Message.MessageID, - })) - } + // query has highest prio, then form, then json + data := dataext.ObjectMerge(dataext.ObjectMerge(b, f), q) + + okResp, errResp := h.app.SendMessage(g, ctx, data.UserID, data.KeyToken, data.Channel, data.Title, data.Content, data.Priority, data.UserMessageID, data.SendTimestamp, data.SenderName) + if errResp != nil { + return *errResp + } else { + return finishSuccess(ginext.JSON(http.StatusOK, response{ + Success: true, + ErrorID: apierr.NO_ERROR, + ErrorHighlight: -1, + Message: langext.Conditional(okResp.MessageIsOld, "Message already sent", "Message sent"), + SuppressSend: okResp.MessageIsOld, + MessageCount: okResp.User.MessagesSent, + Quota: okResp.User.QuotaUsedToday(), + IsPro: okResp.User.IsPro, + QuotaMax: okResp.User.QuotaPerDay(), + SCNMessageID: okResp.Message.MessageID, + })) + } + + }) } diff --git a/scnserver/api/handler/website.go b/scnserver/api/handler/website.go index ae04686..8b9aefa 100644 --- a/scnserver/api/handler/website.go +++ b/scnserver/api/handler/website.go @@ -35,7 +35,9 @@ func (h WebsiteHandler) Index(pctx ginext.PreContext) ginext.HTTPResponse { } defer ctx.Cancel() - return h.serveAsset(g, "index.html", true) + return h.app.DoRequest(ctx, g, func(ctx *logic.AppContext, finishSuccess func(r ginext.HTTPResponse) ginext.HTTPResponse) ginext.HTTPResponse { + return h.serveAsset(g, "index.html", true) + }) } func (h WebsiteHandler) APIDocs(pctx ginext.PreContext) ginext.HTTPResponse { @@ -45,7 +47,9 @@ func (h WebsiteHandler) APIDocs(pctx ginext.PreContext) ginext.HTTPResponse { } defer ctx.Cancel() - return h.serveAsset(g, "api.html", true) + return h.app.DoRequest(ctx, g, func(ctx *logic.AppContext, finishSuccess func(r ginext.HTTPResponse) ginext.HTTPResponse) ginext.HTTPResponse { + return h.serveAsset(g, "api.html", true) + }) } func (h WebsiteHandler) APIDocsMore(pctx ginext.PreContext) ginext.HTTPResponse { @@ -55,7 +59,9 @@ func (h WebsiteHandler) APIDocsMore(pctx ginext.PreContext) ginext.HTTPResponse } defer ctx.Cancel() - return h.serveAsset(g, "api_more.html", true) + return h.app.DoRequest(ctx, g, func(ctx *logic.AppContext, finishSuccess func(r ginext.HTTPResponse) ginext.HTTPResponse) ginext.HTTPResponse { + return h.serveAsset(g, "api_more.html", true) + }) } func (h WebsiteHandler) MessageSent(pctx ginext.PreContext) ginext.HTTPResponse { @@ -65,7 +71,9 @@ func (h WebsiteHandler) MessageSent(pctx ginext.PreContext) ginext.HTTPResponse } defer ctx.Cancel() - return h.serveAsset(g, "message_sent.html", true) + return h.app.DoRequest(ctx, g, func(ctx *logic.AppContext, finishSuccess func(r ginext.HTTPResponse) ginext.HTTPResponse) ginext.HTTPResponse { + return h.serveAsset(g, "message_sent.html", true) + }) } func (h WebsiteHandler) FaviconIco(pctx ginext.PreContext) ginext.HTTPResponse { @@ -75,7 +83,9 @@ func (h WebsiteHandler) FaviconIco(pctx ginext.PreContext) ginext.HTTPResponse { } defer ctx.Cancel() - return h.serveAsset(g, "favicon.ico", false) + return h.app.DoRequest(ctx, g, func(ctx *logic.AppContext, finishSuccess func(r ginext.HTTPResponse) ginext.HTTPResponse) ginext.HTTPResponse { + return h.serveAsset(g, "favicon.ico", false) + }) } func (h WebsiteHandler) FaviconPNG(pctx ginext.PreContext) ginext.HTTPResponse { @@ -85,7 +95,9 @@ func (h WebsiteHandler) FaviconPNG(pctx ginext.PreContext) ginext.HTTPResponse { } defer ctx.Cancel() - return h.serveAsset(g, "favicon.png", false) + return h.app.DoRequest(ctx, g, func(ctx *logic.AppContext, finishSuccess func(r ginext.HTTPResponse) ginext.HTTPResponse) ginext.HTTPResponse { + return h.serveAsset(g, "favicon.png", false) + }) } func (h WebsiteHandler) Javascript(pctx ginext.PreContext) ginext.HTTPResponse { @@ -95,16 +107,19 @@ func (h WebsiteHandler) Javascript(pctx ginext.PreContext) ginext.HTTPResponse { } defer ctx.Cancel() - type uri struct { - Filename string `uri:"fn"` - } + return h.app.DoRequest(ctx, g, func(ctx *logic.AppContext, finishSuccess func(r ginext.HTTPResponse) ginext.HTTPResponse) ginext.HTTPResponse { - var u uri - if err := g.ShouldBindUri(&u); err != nil { - return ginext.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) - } + type uri struct { + Filename string `uri:"fn"` + } - return h.serveAsset(g, "js/"+u.Filename, false) + var u uri + if err := g.ShouldBindUri(&u); err != nil { + return ginext.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + } + + return h.serveAsset(g, "js/"+u.Filename, false) + }) } func (h WebsiteHandler) CSS(pctx ginext.PreContext) ginext.HTTPResponse { @@ -119,7 +134,9 @@ func (h WebsiteHandler) CSS(pctx ginext.PreContext) ginext.HTTPResponse { } defer ctx.Cancel() - return h.serveAsset(g, "css/"+u.Filename, false) + return h.app.DoRequest(ctx, g, func(ctx *logic.AppContext, finishSuccess func(r ginext.HTTPResponse) ginext.HTTPResponse) ginext.HTTPResponse { + return h.serveAsset(g, "css/"+u.Filename, false) + }) } func (h WebsiteHandler) serveAsset(g *gin.Context, fn string, repl bool) ginext.HTTPResponse { diff --git a/scnserver/api/router.go b/scnserver/api/router.go index adbeaf9..714a48d 100644 --- a/scnserver/api/router.go +++ b/scnserver/api/router.go @@ -1,13 +1,11 @@ package api import ( - "blackforestbytes.com/simplecloudnotifier/api/ginresp" "blackforestbytes.com/simplecloudnotifier/api/handler" "blackforestbytes.com/simplecloudnotifier/logic" "blackforestbytes.com/simplecloudnotifier/models" "blackforestbytes.com/simplecloudnotifier/swagger" "errors" - "github.com/gin-gonic/gin" "github.com/gin-gonic/gin/binding" "github.com/go-playground/validator/v10" "gogs.mikescher.com/BlackForestBytes/goext/ginext" @@ -61,119 +59,117 @@ func (r *Router) Init(e *ginext.GinWrapper) error { return errors.New("failed to add validators - wrong engine") } - wrap := func(fn ginext.WHandlerFunc) ginext.WHandlerFunc{return Wrap(r.app, fn)} - // ================ General (unversioned) ================ commonAPI := e.Routes().Group("/api") { - commonAPI.Any("/ping").Handle(wrap(r.commonHandler.Ping)) - commonAPI.POST("/db-test").Handle(wrap(r.commonHandler.DatabaseTest)) - commonAPI.GET("/health").Handle(wrap(r.commonHandler.Health)) - commonAPI.POST("/sleep/:secs").Handle(wrap(r.commonHandler.Sleep)) + commonAPI.Any("/ping").Handle(r.commonHandler.Ping) + commonAPI.POST("/db-test").Handle(r.commonHandler.DatabaseTest) + commonAPI.GET("/health").Handle(r.commonHandler.Health) + commonAPI.POST("/sleep/:secs").Handle(r.commonHandler.Sleep) } // ================ Swagger ================ docs := e.Routes().Group("/documentation") { - docs.GET("/swagger").Handle(wrap(ginext.RedirectTemporary("/documentation/swagger/"))) - docs.GET("/swagger/*sub").Handle(wrap(swagger.Handle)) + docs.GET("/swagger").Handle(ginext.RedirectTemporary("/documentation/swagger/")) + docs.GET("/swagger/*sub").Handle(swagger.Handle) } // ================ Website ================ frontend := e.Routes().Group("") { - frontend.GET("/").Handle(wrap(r.websiteHandler.Index)) - frontend.GET("/index.php").Handle(wrap(r.websiteHandler.Index)) - frontend.GET("/index.html").Handle(wrap(r.websiteHandler.Index)) - frontend.GET("/index").Handle(wrap(r.websiteHandler.Index)) + frontend.GET("/").Handle(r.websiteHandler.Index) + frontend.GET("/index.php").Handle(r.websiteHandler.Index) + frontend.GET("/index.html").Handle(r.websiteHandler.Index) + frontend.GET("/index").Handle(r.websiteHandler.Index) - frontend.GET("/api").Handle(wrap(r.websiteHandler.APIDocs)) - frontend.GET("/api.php").Handle(wrap(r.websiteHandler.APIDocs)) - frontend.GET("/api.html").Handle(wrap(r.websiteHandler.APIDocs)) + frontend.GET("/api").Handle(r.websiteHandler.APIDocs) + frontend.GET("/api.php").Handle(r.websiteHandler.APIDocs) + frontend.GET("/api.html").Handle(r.websiteHandler.APIDocs) - frontend.GET("/api_more").Handle(wrap(r.websiteHandler.APIDocsMore)) - frontend.GET("/api_more.php").Handle(wrap(r.websiteHandler.APIDocsMore)) - frontend.GET("/api_more.html").Handle(wrap(r.websiteHandler.APIDocsMore)) + frontend.GET("/api_more").Handle(r.websiteHandler.APIDocsMore) + frontend.GET("/api_more.php").Handle(r.websiteHandler.APIDocsMore) + frontend.GET("/api_more.html").Handle(r.websiteHandler.APIDocsMore) - frontend.GET("/message_sent").Handle(wrap(r.websiteHandler.MessageSent)) - frontend.GET("/message_sent.php").Handle(wrap(r.websiteHandler.MessageSent)) - frontend.GET("/message_sent.html").Handle(wrap(r.websiteHandler.MessageSent)) + frontend.GET("/message_sent").Handle(r.websiteHandler.MessageSent) + frontend.GET("/message_sent.php").Handle(r.websiteHandler.MessageSent) + frontend.GET("/message_sent.html").Handle(r.websiteHandler.MessageSent) - frontend.GET("/favicon.ico").Handle(wrap(r.websiteHandler.FaviconIco)) - frontend.GET("/favicon.png").Handle(wrap(r.websiteHandler.FaviconPNG)) + frontend.GET("/favicon.ico").Handle(r.websiteHandler.FaviconIco) + frontend.GET("/favicon.png").Handle(r.websiteHandler.FaviconPNG) - frontend.GET("/js/:fn").Handle(wrap(r.websiteHandler.Javascript)) - frontend.GET("/css/:fn").Handle(wrap(r.websiteHandler.CSS)) + frontend.GET("/js/:fn").Handle(r.websiteHandler.Javascript) + frontend.GET("/css/:fn").Handle(r.websiteHandler.CSS) } // ================ Compat (v1) ================ compat := e.Routes().Group("/api") { - compat.GET("/register.php").Handle(wrap(r.compatHandler.Register)) - compat.GET("/info.php").Handle(wrap(r.compatHandler.Info)) - compat.GET("/ack.php").Handle(wrap(r.compatHandler.Ack)) - compat.GET("/requery.php").Handle(wrap(r.compatHandler.Requery)) - compat.GET("/update.php").Handle(wrap(r.compatHandler.Update)) - compat.GET("/expand.php").Handle(wrap(r.compatHandler.Expand)) - compat.GET("/upgrade.php").Handle(wrap(r.compatHandler.Upgrade)) + compat.GET("/register.php").Handle(r.compatHandler.Register) + compat.GET("/info.php").Handle(r.compatHandler.Info) + compat.GET("/ack.php").Handle(r.compatHandler.Ack) + compat.GET("/requery.php").Handle(r.compatHandler.Requery) + compat.GET("/update.php").Handle(r.compatHandler.Update) + compat.GET("/expand.php").Handle(r.compatHandler.Expand) + compat.GET("/upgrade.php").Handle(r.compatHandler.Upgrade) } // ================ Manage API (v2) ================ apiv2 := e.Routes().Group("/api/v2/") { - apiv2.POST("/users").Handle(wrap(r.apiHandler.CreateUser)) - apiv2.GET("/users/:uid").Handle(wrap(r.apiHandler.GetUser)) - apiv2.PATCH("/users/:uid").Handle(wrap(r.apiHandler.UpdateUser)) + apiv2.POST("/users").Handle(r.apiHandler.CreateUser) + apiv2.GET("/users/:uid").Handle(r.apiHandler.GetUser) + apiv2.PATCH("/users/:uid").Handle(r.apiHandler.UpdateUser) - apiv2.GET("/users/:uid/keys").Handle(wrap(r.apiHandler.ListUserKeys)) - apiv2.POST("/users/:uid/keys").Handle(wrap(r.apiHandler.CreateUserKey)) - apiv2.GET("/users/:uid/keys/current").Handle(wrap(r.apiHandler.GetCurrentUserKey)) - apiv2.GET("/users/:uid/keys/:kid").Handle(wrap(r.apiHandler.GetUserKey)) - apiv2.PATCH("/users/:uid/keys/:kid").Handle(wrap(r.apiHandler.UpdateUserKey)) - apiv2.DELETE("/users/:uid/keys/:kid").Handle(wrap(r.apiHandler.DeleteUserKey)) + apiv2.GET("/users/:uid/keys").Handle(r.apiHandler.ListUserKeys) + apiv2.POST("/users/:uid/keys").Handle(r.apiHandler.CreateUserKey) + apiv2.GET("/users/:uid/keys/current").Handle(r.apiHandler.GetCurrentUserKey) + apiv2.GET("/users/:uid/keys/:kid").Handle(r.apiHandler.GetUserKey) + apiv2.PATCH("/users/:uid/keys/:kid").Handle(r.apiHandler.UpdateUserKey) + apiv2.DELETE("/users/:uid/keys/:kid").Handle(r.apiHandler.DeleteUserKey) - apiv2.GET("/users/:uid/clients").Handle(wrap(r.apiHandler.ListClients)) - apiv2.GET("/users/:uid/clients/:cid").Handle(wrap(r.apiHandler.GetClient)) - apiv2.PATCH("/users/:uid/clients/:cid").Handle(wrap(r.apiHandler.UpdateClient)) - apiv2.POST("/users/:uid/clients").Handle(wrap(r.apiHandler.AddClient)) - apiv2.DELETE("/users/:uid/clients/:cid").Handle(wrap(r.apiHandler.DeleteClient)) + apiv2.GET("/users/:uid/clients").Handle(r.apiHandler.ListClients) + apiv2.GET("/users/:uid/clients/:cid").Handle(r.apiHandler.GetClient) + apiv2.PATCH("/users/:uid/clients/:cid").Handle(r.apiHandler.UpdateClient) + apiv2.POST("/users/:uid/clients").Handle(r.apiHandler.AddClient) + apiv2.DELETE("/users/:uid/clients/:cid").Handle(r.apiHandler.DeleteClient) - apiv2.GET("/users/:uid/channels").Handle(wrap(r.apiHandler.ListChannels)) - apiv2.POST("/users/:uid/channels").Handle(wrap(r.apiHandler.CreateChannel)) - apiv2.GET("/users/:uid/channels/:cid").Handle(wrap(r.apiHandler.GetChannel)) - apiv2.PATCH("/users/:uid/channels/:cid").Handle(wrap(r.apiHandler.UpdateChannel)) - apiv2.GET("/users/:uid/channels/:cid/messages").Handle(wrap(r.apiHandler.ListChannelMessages)) - apiv2.GET("/users/:uid/channels/:cid/subscriptions").Handle(wrap(r.apiHandler.ListChannelSubscriptions)) + apiv2.GET("/users/:uid/channels").Handle(r.apiHandler.ListChannels) + apiv2.POST("/users/:uid/channels").Handle(r.apiHandler.CreateChannel) + apiv2.GET("/users/:uid/channels/:cid").Handle(r.apiHandler.GetChannel) + apiv2.PATCH("/users/:uid/channels/:cid").Handle(r.apiHandler.UpdateChannel) + apiv2.GET("/users/:uid/channels/:cid/messages").Handle(r.apiHandler.ListChannelMessages) + apiv2.GET("/users/:uid/channels/:cid/subscriptions").Handle(r.apiHandler.ListChannelSubscriptions) - apiv2.GET("/users/:uid/subscriptions").Handle(wrap(r.apiHandler.ListUserSubscriptions)) - apiv2.POST("/users/:uid/subscriptions").Handle(wrap(r.apiHandler.CreateSubscription)) - apiv2.GET("/users/:uid/subscriptions/:sid").Handle(wrap(r.apiHandler.GetSubscription)) - apiv2.DELETE("/users/:uid/subscriptions/:sid").Handle(wrap(r.apiHandler.CancelSubscription)) - apiv2.PATCH("/users/:uid/subscriptions/:sid").Handle(wrap(r.apiHandler.UpdateSubscription)) + apiv2.GET("/users/:uid/subscriptions").Handle(r.apiHandler.ListUserSubscriptions) + apiv2.POST("/users/:uid/subscriptions").Handle(r.apiHandler.CreateSubscription) + apiv2.GET("/users/:uid/subscriptions/:sid").Handle(r.apiHandler.GetSubscription) + apiv2.DELETE("/users/:uid/subscriptions/:sid").Handle(r.apiHandler.CancelSubscription) + apiv2.PATCH("/users/:uid/subscriptions/:sid").Handle(r.apiHandler.UpdateSubscription) - apiv2.GET("/messages").Handle(wrap(r.apiHandler.ListMessages)) - apiv2.GET("/messages/:mid").Handle(wrap(r.apiHandler.GetMessage)) - apiv2.DELETE("/messages/:mid").Handle(wrap(r.apiHandler.DeleteMessage)) + apiv2.GET("/messages").Handle(r.apiHandler.ListMessages) + apiv2.GET("/messages/:mid").Handle(r.apiHandler.GetMessage) + apiv2.DELETE("/messages/:mid").Handle(r.apiHandler.DeleteMessage) - apiv2.GET("/preview/users/:uid").Handle(wrap(r.apiHandler.GetUserPreview)) - apiv2.GET("/preview/keys/:kid").Handle(wrap(r.apiHandler.GetUserKeyPreview)) - apiv2.GET("/preview/channels/:cid").Handle(wrap(r.apiHandler.GetChannelPreview)) + apiv2.GET("/preview/users/:uid").Handle(r.apiHandler.GetUserPreview) + apiv2.GET("/preview/keys/:kid").Handle(r.apiHandler.GetUserKeyPreview) + apiv2.GET("/preview/channels/:cid").Handle(r.apiHandler.GetChannelPreview) } // ================ Send API (unversioned) ================ sendAPI := e.Routes().Group("") { - sendAPI.POST("/").Handle(wrap(r.messageHandler.SendMessage) - sendAPI.POST("/send").Handle(wrap(r.messageHandler.SendMessage) - sendAPI.POST("/send.php").Handle(wrap(r.compatHandler.SendMessage) + sendAPI.POST("/").Handle(r.messageHandler.SendMessage) + sendAPI.POST("/send").Handle(r.messageHandler.SendMessage) + sendAPI.POST("/send.php").Handle(r.compatHandler.SendMessage) - sendAPI.POST("/external/v1/uptime-kuma").Handle(wrap(r.externalHandler.UptimeKuma) + sendAPI.POST("/external/v1/uptime-kuma").Handle(r.externalHandler.UptimeKuma) } diff --git a/scnserver/api/wrapper.go b/scnserver/api/wrapper.go deleted file mode 100644 index 73111fb..0000000 --- a/scnserver/api/wrapper.go +++ /dev/null @@ -1,195 +0,0 @@ -package api - -import ( - scn "blackforestbytes.com/simplecloudnotifier" - "blackforestbytes.com/simplecloudnotifier/api/apierr" - "blackforestbytes.com/simplecloudnotifier/api/ginresp" - "blackforestbytes.com/simplecloudnotifier/models" - "errors" - "fmt" - "github.com/gin-gonic/gin" - "github.com/glebarez/go-sqlite" - "github.com/rs/zerolog/log" - "gogs.mikescher.com/BlackForestBytes/goext/dataext" - "gogs.mikescher.com/BlackForestBytes/goext/ginext" - "gogs.mikescher.com/BlackForestBytes/goext/langext" - "math/rand" - "runtime/debug" - "time" -) - -type RequestLogAcceptor interface { - InsertRequestLog(data models.RequestLog) -} - -func Wrap(rlacc RequestLogAcceptor, fn ginext.WHandlerFunc) ginext.WHandlerFunc { - - maxRetry := scn.Conf.RequestMaxRetry - retrySleep := scn.Conf.RequestRetrySleep - - return func(pctx *ginext.PreContext) { - - reqctx := g.Request.Context() - - if g.Request.Body != nil { - g.Request.Body = dataext.NewBufferedReadCloser(g.Request.Body) - } - - t0 := time.Now() - - for ctr := 1; ; ctr++ { - - wrap, stackTrace, panicObj := callPanicSafe(fn, g) - if panicObj != nil { - log.Error().Interface("panicObj", panicObj).Msg("Panic occured (in gin handler)") - log.Error().Msg(stackTrace) - wrap = ginresp.APIError(g, 500, apierr.PANIC, "A panic occured in the HTTP handler", errors.New(fmt.Sprintf("%+v\n\n@:\n%s", panicObj, stackTrace))) - } - - if g.Writer.Written() { - if scn.Conf.ReqLogEnabled { - rlacc.InsertRequestLog(createRequestLog(g, t0, ctr, nil, langext.Ptr("Writing in WrapperFunc is not supported"))) - } - panic("Writing in WrapperFunc is not supported") - } - - if ctr < maxRetry && isSqlite3Busy(wrap) { - log.Warn().Int("counter", ctr).Str("url", g.Request.URL.String()).Msg("Retry request (ErrBusy)") - - err := resetBody(g) - if err != nil { - panic(err) - } - - time.Sleep(time.Duration(int64(float64(retrySleep) * (0.5 + rand.Float64())))) - continue - } - - if reqctx.Err() == nil { - if scn.Conf.ReqLogEnabled { - rlacc.InsertRequestLog(createRequestLog(g, t0, ctr, wrap, nil)) - } - - if scw, ok := wrap.(ginext.InspectableHTTPResponse); ok { - - statuscode := scw.Statuscode() - if statuscode/100 != 2 { - log.Warn().Str("url", g.Request.Method+"::"+g.Request.URL.String()).Msg(fmt.Sprintf("Request failed with statuscode %d", statuscode)) - } - } else { - log.Warn().Str("url", g.Request.Method+"::"+g.Request.URL.String()).Msg(fmt.Sprintf("Request failed with statuscode [unknown]")) - } - - wrap.Write(g) - } - - return - } - - } - -} - -func createRequestLog(g *gin.Context, t0 time.Time, ctr int, resp ginext.HTTPResponse, panicstr *string) models.RequestLog { - - t1 := time.Now() - - ua := g.Request.UserAgent() - auth := g.Request.Header.Get("Authorization") - ct := g.Request.Header.Get("Content-Type") - - var reqbody []byte = nil - if g.Request.Body != nil { - brcbody, err := g.Request.Body.(dataext.BufferedReadCloser).BufferedAll() - if err == nil { - reqbody = brcbody - } - } - var strreqbody *string = nil - if len(reqbody) < scn.Conf.ReqLogMaxBodySize { - strreqbody = langext.Ptr(string(reqbody)) - } - - var respbody *string = nil - - var strrespbody *string = nil - if resp != nil { - if resp2, ok := resp.(ginext.InspectableHTTPResponse); ok { - respbody = resp2.BodyString(g) - if respbody != nil && len(*respbody) < scn.Conf.ReqLogMaxBodySize { - strrespbody = respbody - } - } - } - - permObj, hasPerm := g.Get("perm") - - hasTok := false - if hasPerm { - hasTok = permObj.(models.PermissionSet).Token != nil - } - - return models.RequestLog{ - Method: g.Request.Method, - URI: g.Request.URL.String(), - UserAgent: langext.Conditional(ua == "", nil, &ua), - Authentication: langext.Conditional(auth == "", nil, &auth), - RequestBody: strreqbody, - RequestBodySize: int64(len(reqbody)), - RequestContentType: ct, - RemoteIP: g.RemoteIP(), - KeyID: langext.ConditionalFn10(hasTok, func() *models.KeyTokenID { return langext.Ptr(permObj.(models.PermissionSet).Token.KeyTokenID) }, nil), - UserID: langext.ConditionalFn10(hasTok, func() *models.UserID { return langext.Ptr(permObj.(models.PermissionSet).Token.OwnerUserID) }, nil), - Permissions: langext.ConditionalFn10(hasTok, func() *string { return langext.Ptr(permObj.(models.PermissionSet).Token.Permissions.String()) }, nil), - ResponseStatuscode: langext.ConditionalFn10(resp != nil, func() *int64 { return langext.Ptr(int64(resp.Statuscode())) }, nil), - ResponseBodySize: langext.ConditionalFn10(strrespbody != nil, func() *int64 { return langext.Ptr(int64(len(*respbody))) }, nil), - ResponseBody: strrespbody, - ResponseContentType: langext.ConditionalFn10(resp != nil, func() string { return resp.ContentType() }, ""), - RetryCount: int64(ctr), - Panicked: panicstr != nil, - PanicStr: panicstr, - ProcessingTime: t1.Sub(t0), - TimestampStart: t0, - TimestampFinish: t1, - } -} - -func callPanicSafe(fn ginext.WHandlerFunc, g ginext.PreContext) (res ginext.HTTPResponse, stackTrace string, panicObj any) { - defer func() { - if rec := recover(); rec != nil { - res = nil - stackTrace = string(debug.Stack()) - panicObj = rec - } - }() - - res = fn(g) - return res, "", nil -} - -func resetBody(g *gin.Context) error { - if g.Request.Body == nil { - return nil - } - - err := g.Request.Body.(dataext.BufferedReadCloser).Reset() - if err != nil { - return err - } - - return nil -} - -func isSqlite3Busy(r ginext.HTTPResponse) bool { - if errwrap, ok := r.(interface{ Unwrap() error }); ok && errwrap != nil { - { - var s3err *sqlite.Error - if errors.As(errwrap.Unwrap(), &s3err) { - if s3err.Code() == 5 { // [5] == SQLITE_BUSY - return true - } - } - } - } - return false -} diff --git a/scnserver/cmd/dbhash/main.go b/scnserver/cmd/dbhash/main.go index f6e9015..4b1e491 100644 --- a/scnserver/cmd/dbhash/main.go +++ b/scnserver/cmd/dbhash/main.go @@ -3,8 +3,11 @@ package main import ( "blackforestbytes.com/simplecloudnotifier/db/schema" "context" + "database/sql" "fmt" + "github.com/glebarez/go-sqlite" "gogs.mikescher.com/BlackForestBytes/goext/exerr" + "gogs.mikescher.com/BlackForestBytes/goext/langext" "gogs.mikescher.com/BlackForestBytes/goext/sq" "time" ) @@ -15,7 +18,9 @@ func main() { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() - sqlite3.Version() // ensure slite3 loaded + if !langext.InArray("sqlite3", sql.Drivers()) { + sqlite.RegisterAsSQLITE3() + } fmt.Println() diff --git a/scnserver/cmd/migrate/main.go b/scnserver/cmd/migrate/main.go index 4e01cf5..1bd5894 100644 --- a/scnserver/cmd/migrate/main.go +++ b/scnserver/cmd/migrate/main.go @@ -135,7 +135,7 @@ func main() { if err != nil { panic(err) } - dbold := sq.NewDB(_dbold) + dbold := sq.NewDB(_dbold, sq.DBOptions{}) rowsUser, err := dbold.Query(ctx, "SELECT * FROM users", sq.PP{}) if err != nil { diff --git a/scnserver/cmd/scnserver/main.go b/scnserver/cmd/scnserver/main.go index 5dc44e9..1475a71 100644 --- a/scnserver/cmd/scnserver/main.go +++ b/scnserver/cmd/scnserver/main.go @@ -11,6 +11,7 @@ import ( "github.com/rs/zerolog/log" "gogs.mikescher.com/BlackForestBytes/goext/ginext" "gogs.mikescher.com/BlackForestBytes/goext/langext" + "time" ) func main() { @@ -33,10 +34,11 @@ func main() { } ginengine := ginext.NewEngine(ginext.Options{ - AllowCors: &conf.Cors, - GinDebug: &conf.GinDebug, - BufferBody: langext.PTrue, - Timeout: &conf.RequestTimeout, + AllowCors: &conf.Cors, + GinDebug: &conf.GinDebug, + BufferBody: langext.PTrue, + Timeout: langext.Ptr(time.Duration(int64(conf.RequestTimeout) * int64(conf.RequestMaxRetry))), + BuildRequestBindError: logic.BuildGinRequestError, }) router := api.NewRouter(app) diff --git a/scnserver/db/impl/logs/database.go b/scnserver/db/impl/logs/database.go index f12aefa..2fbcab7 100644 --- a/scnserver/db/impl/logs/database.go +++ b/scnserver/db/impl/logs/database.go @@ -26,7 +26,12 @@ type Database struct { func NewLogsDatabase(cfg server.Config) (*Database, error) { conf := cfg.DBLogs - url := fmt.Sprintf("file:%s?_journal=%s&_timeout=%d&_fk=%s&_busy_timeout=%d", conf.File, conf.Journal, conf.Timeout.Milliseconds(), langext.FormatBool(conf.CheckForeignKeys, "true", "false"), conf.BusyTimeout.Milliseconds()) + url := fmt.Sprintf("file:%s?_pragma=journal_mode(%s)&_pragma=timeout(%d)&_pragma=foreign_keys(%s)&_pragma=busy_timeout(%d)", + conf.File, + conf.Journal, + conf.Timeout.Milliseconds(), + langext.FormatBool(conf.CheckForeignKeys, "true", "false"), + conf.BusyTimeout.Milliseconds()) if !langext.InArray("sqlite3", sql.Drivers()) { sqlite.RegisterAsSQLITE3() diff --git a/scnserver/db/impl/primary/database.go b/scnserver/db/impl/primary/database.go index 0b835d9..f9a7c01 100644 --- a/scnserver/db/impl/primary/database.go +++ b/scnserver/db/impl/primary/database.go @@ -26,7 +26,12 @@ type Database struct { func NewPrimaryDatabase(cfg server.Config) (*Database, error) { conf := cfg.DBMain - url := fmt.Sprintf("file:%s?_journal=%s&_timeout=%d&_fk=%s&_busy_timeout=%d", conf.File, conf.Journal, conf.Timeout.Milliseconds(), langext.FormatBool(conf.CheckForeignKeys, "true", "false"), conf.BusyTimeout.Milliseconds()) + url := fmt.Sprintf("file:%s?_pragma=journal_mode(%s)&_pragma=timeout(%d)&_pragma=foreign_keys(%s)&_pragma=busy_timeout(%d)", + conf.File, + conf.Journal, + conf.Timeout.Milliseconds(), + langext.FormatBool(conf.CheckForeignKeys, "true", "false"), + conf.BusyTimeout.Milliseconds()) if !langext.InArray("sqlite3", sql.Drivers()) { sqlite.RegisterAsSQLITE3() diff --git a/scnserver/db/impl/requests/database.go b/scnserver/db/impl/requests/database.go index 9dea758..975af79 100644 --- a/scnserver/db/impl/requests/database.go +++ b/scnserver/db/impl/requests/database.go @@ -26,7 +26,12 @@ type Database struct { func NewRequestsDatabase(cfg server.Config) (*Database, error) { conf := cfg.DBRequests - url := fmt.Sprintf("file:%s?_journal=%s&_timeout=%d&_fk=%s&_busy_timeout=%d", conf.File, conf.Journal, conf.Timeout.Milliseconds(), langext.FormatBool(conf.CheckForeignKeys, "true", "false"), conf.BusyTimeout.Milliseconds()) + url := fmt.Sprintf("file:%s?_pragma=journal_mode(%s)&_pragma=timeout(%d)&_pragma=foreign_keys(%s)&_pragma=busy_timeout(%d)", + conf.File, + conf.Journal, + conf.Timeout.Milliseconds(), + langext.FormatBool(conf.CheckForeignKeys, "true", "false"), + conf.BusyTimeout.Milliseconds()) if !langext.InArray("sqlite3", sql.Drivers()) { sqlite.RegisterAsSQLITE3() diff --git a/scnserver/go.mod b/scnserver/go.mod index dd9b986..3abf237 100644 --- a/scnserver/go.mod +++ b/scnserver/go.mod @@ -12,7 +12,7 @@ require ( github.com/jmoiron/sqlx v1.4.0 github.com/mattn/go-sqlite3 v1.14.22 github.com/rs/zerolog v1.33.0 - gogs.mikescher.com/BlackForestBytes/goext v0.0.482 + gogs.mikescher.com/BlackForestBytes/goext v0.0.485 gopkg.in/loremipsum.v1 v1.1.2 ) diff --git a/scnserver/go.sum b/scnserver/go.sum index 5a9c70a..f368e3b 100644 --- a/scnserver/go.sum +++ b/scnserver/go.sum @@ -113,6 +113,12 @@ go.mongodb.org/mongo-driver v1.16.0 h1:tpRsfBJMROVHKpdGyc1BBEzzjDUWjItxbVSZ8Ls4B go.mongodb.org/mongo-driver v1.16.0/go.mod h1:oB6AhJQvFQL4LEHyXi6aJzQJtBiTQHiAd83l0GdFaiw= gogs.mikescher.com/BlackForestBytes/goext v0.0.482 h1:veU8oJdGZ9rjLB8sluagBduiBs3BbEDf60sGmEEv8lk= gogs.mikescher.com/BlackForestBytes/goext v0.0.482/go.mod h1:GxqLkJwPWQB5lVgWhmBPnx9RC+F0Dvi2xHKwfCmCQgM= +gogs.mikescher.com/BlackForestBytes/goext v0.0.483 h1:fxhe3U5bpkv1SvSae7F/ixPp7DUiRxga4Zvg82iQSsI= +gogs.mikescher.com/BlackForestBytes/goext v0.0.483/go.mod h1:GxqLkJwPWQB5lVgWhmBPnx9RC+F0Dvi2xHKwfCmCQgM= +gogs.mikescher.com/BlackForestBytes/goext v0.0.484 h1:fu60J83OBtnUkXCIt+dycHrin5OUmL1B46IY6GTQosw= +gogs.mikescher.com/BlackForestBytes/goext v0.0.484/go.mod h1:GxqLkJwPWQB5lVgWhmBPnx9RC+F0Dvi2xHKwfCmCQgM= +gogs.mikescher.com/BlackForestBytes/goext v0.0.485 h1:hjXxl7bwHkzYBpfsX81UZj929bKUDIoNFl0XQSvt4Qk= +gogs.mikescher.com/BlackForestBytes/goext v0.0.485/go.mod h1:GxqLkJwPWQB5lVgWhmBPnx9RC+F0Dvi2xHKwfCmCQgM= golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc= golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= diff --git a/scnserver/logic/appcontext.go b/scnserver/logic/appcontext.go index 92c50c4..067c7d2 100644 --- a/scnserver/logic/appcontext.go +++ b/scnserver/logic/appcontext.go @@ -71,7 +71,10 @@ func (ac *AppContext) Cancel() { } ac.transaction = nil } - ac.cancelFunc() + + if ac.cancelFunc != nil { + ac.cancelFunc() + } } func (ac *AppContext) RequestURI() string { @@ -82,7 +85,7 @@ func (ac *AppContext) RequestURI() string { } } -func (ac *AppContext) FinishSuccess(res ginext.HTTPResponse) ginext.HTTPResponse { +func (ac *AppContext) _FinishSuccess(res ginext.HTTPResponse) ginext.HTTPResponse { if ac.cancelled { panic("Cannot finish a cancelled request") } diff --git a/scnserver/logic/application.go b/scnserver/logic/application.go index 620b6aa..9869f6b 100644 --- a/scnserver/logic/application.go +++ b/scnserver/logic/application.go @@ -2,8 +2,6 @@ package logic import ( scn "blackforestbytes.com/simplecloudnotifier" - "blackforestbytes.com/simplecloudnotifier/api/apierr" - "blackforestbytes.com/simplecloudnotifier/api/ginresp" "blackforestbytes.com/simplecloudnotifier/db" "blackforestbytes.com/simplecloudnotifier/db/simplectx" "blackforestbytes.com/simplecloudnotifier/google" @@ -11,10 +9,8 @@ import ( "blackforestbytes.com/simplecloudnotifier/push" "context" "errors" - "github.com/gin-gonic/gin" "github.com/rs/zerolog/log" "gogs.mikescher.com/BlackForestBytes/goext/ginext" - "gogs.mikescher.com/BlackForestBytes/goext/langext" "gogs.mikescher.com/BlackForestBytes/goext/rext" "gogs.mikescher.com/BlackForestBytes/goext/syncext" "net" @@ -208,32 +204,6 @@ func (app *Application) Migrate() error { return app.Database.Migrate(ctx) } -type RequestOptions struct { - IgnoreWrongContentType bool -} - -func (app *Application) StartRequest(gectx *ginext.AppContext, g *gin.Context, r *ginext.HTTPResponse) (*AppContext, *gin.Context, *ginext.HTTPResponse) { - - if r != nil { - return nil, g, r - } - - actx := CreateAppContext(app, g, gectx, gectx.Cancel) - - authheader := g.GetHeader("Authorization") - - perm, err := app.getPermissions(actx, authheader) - if err != nil { - gectx.Cancel() - return nil, g, langext.Ptr(ginresp.APIError(g, 400, apierr.PERM_QUERY_FAIL, "Failed to determine permissions", err)) - } - - actx.permissions = perm - g.Set("perm", perm) - - return actx, g, nil -} - func (app *Application) NewSimpleTransactionContext(timeout time.Duration) *simplectx.SimpleContext { ictx, cancel := context.WithTimeout(context.Background(), timeout) return simplectx.CreateSimpleContext(ictx, cancel) diff --git a/scnserver/logic/message.go b/scnserver/logic/message.go index de4617e..cd0c2cc 100644 --- a/scnserver/logic/message.go +++ b/scnserver/logic/message.go @@ -10,6 +10,7 @@ import ( "fmt" "github.com/gin-gonic/gin" "github.com/rs/zerolog/log" + "gogs.mikescher.com/BlackForestBytes/goext/ginext" "gogs.mikescher.com/BlackForestBytes/goext/langext" "gogs.mikescher.com/BlackForestBytes/goext/mathext" "gogs.mikescher.com/BlackForestBytes/goext/timeext" diff --git a/scnserver/logic/permissions.go b/scnserver/logic/permissions.go index e9d48cf..ffa5dfe 100644 --- a/scnserver/logic/permissions.go +++ b/scnserver/logic/permissions.go @@ -6,6 +6,7 @@ import ( "blackforestbytes.com/simplecloudnotifier/models" "database/sql" "errors" + "gogs.mikescher.com/BlackForestBytes/goext/ginext" "gogs.mikescher.com/BlackForestBytes/goext/langext" ) diff --git a/scnserver/logic/request.go b/scnserver/logic/request.go new file mode 100644 index 0000000..fb21978 --- /dev/null +++ b/scnserver/logic/request.go @@ -0,0 +1,240 @@ +package logic + +import ( + scn "blackforestbytes.com/simplecloudnotifier" + "blackforestbytes.com/simplecloudnotifier/api/apierr" + "blackforestbytes.com/simplecloudnotifier/api/ginresp" + "blackforestbytes.com/simplecloudnotifier/models" + "context" + "errors" + "fmt" + "github.com/gin-gonic/gin" + "github.com/glebarez/go-sqlite" + "github.com/rs/zerolog/log" + "gogs.mikescher.com/BlackForestBytes/goext/dataext" + "gogs.mikescher.com/BlackForestBytes/goext/ginext" + "gogs.mikescher.com/BlackForestBytes/goext/langext" + "math/rand" + "runtime/debug" + "time" +) + +type RequestOptions struct { + IgnoreWrongContentType bool +} + +func (app *Application) DoRequest(gectx *ginext.AppContext, g *gin.Context, fn func(ctx *AppContext, finishSuccess func(r ginext.HTTPResponse) ginext.HTTPResponse) ginext.HTTPResponse) ginext.HTTPResponse { + + maxRetry := scn.Conf.RequestMaxRetry + retrySleep := scn.Conf.RequestRetrySleep + + reqctx := g.Request.Context() + + t0 := time.Now() + + for ctr := 1; ; ctr++ { + + ictx, cancel := context.WithTimeout(gectx, app.Config.RequestTimeout) + + actx := CreateAppContext(app, g, ictx, cancel) + + wrap, stackTrace, panicObj := callPanicSafe(func(ctx *AppContext, finishSuccess func(r ginext.HTTPResponse) ginext.HTTPResponse) ginext.HTTPResponse { + + authheader := g.GetHeader("Authorization") + + perm, err := app.getPermissions(actx, authheader) + if err != nil { + cancel() + return ginresp.APIError(g, 400, apierr.PERM_QUERY_FAIL, "Failed to determine permissions", err) + } + + actx.permissions = perm + g.Set("perm", perm) + + return fn(actx, finishSuccess) + + }, actx, actx._FinishSuccess) + if panicObj != nil { + log.Error().Interface("panicObj", panicObj).Msg("Panic occured (in gin handler)") + log.Error().Msg(stackTrace) + wrap = ginresp.APIError(g, 500, apierr.PANIC, "A panic occured in the HTTP handler", errors.New(fmt.Sprintf("%+v\n\n@:\n%s", panicObj, stackTrace))) + } + + if g.Writer.Written() { + if scn.Conf.ReqLogEnabled { + app.InsertRequestLog(createRequestLog(g, t0, ctr, nil, langext.Ptr("Writing in WrapperFunc is not supported"))) + } + panic("Writing in WrapperFunc is not supported") + } + + if ctr < maxRetry && isSqlite3Busy(wrap) { + log.Warn().Int("counter", ctr).Str("url", g.Request.URL.String()).Msg("Retry request (ErrBusy)") + + err := resetBody(g) + if err != nil { + panic(err) + } + + time.Sleep(time.Duration(int64(float64(retrySleep) * (0.5 + rand.Float64())))) + continue + } + + if reqctx.Err() == nil { + if scn.Conf.ReqLogEnabled { + app.InsertRequestLog(createRequestLog(g, t0, ctr, wrap, nil)) + } + + if scw, ok := wrap.(ginext.InspectableHTTPResponse); ok { + + statuscode := scw.Statuscode() + if statuscode/100 != 2 { + log.Warn().Str("url", g.Request.Method+"::"+g.Request.URL.String()).Msg(fmt.Sprintf("Request failed with statuscode %d", statuscode)) + } + } else { + log.Warn().Str("url", g.Request.Method+"::"+g.Request.URL.String()).Msg(fmt.Sprintf("Request failed with statuscode [unknown]")) + } + } + + return wrap + } + +} + +func callPanicSafe(fn func(ctx *AppContext, finishSuccess func(r ginext.HTTPResponse) ginext.HTTPResponse) ginext.HTTPResponse, actx *AppContext, fnFin func(r ginext.HTTPResponse) ginext.HTTPResponse) (res ginext.HTTPResponse, stackTrace string, panicObj any) { + defer func() { + if rec := recover(); rec != nil { + res = nil + stackTrace = string(debug.Stack()) + panicObj = rec + } + }() + + res = fn(actx, fnFin) + return res, "", nil +} + +func createRequestLog(g *gin.Context, t0 time.Time, ctr int, resp ginext.HTTPResponse, panicstr *string) models.RequestLog { + + t1 := time.Now() + + ua := g.Request.UserAgent() + auth := g.Request.Header.Get("Authorization") + ct := g.Request.Header.Get("Content-Type") + + var reqbody []byte = nil + if g.Request.Body != nil { + brcbody, err := g.Request.Body.(dataext.BufferedReadCloser).BufferedAll() + if err == nil { + reqbody = brcbody + } + } + var strreqbody *string = nil + if len(reqbody) < scn.Conf.ReqLogMaxBodySize { + strreqbody = langext.Ptr(string(reqbody)) + } + + var respbody *string = nil + + var strrespbody *string = nil + if resp != nil { + if resp2, ok := resp.(ginext.InspectableHTTPResponse); ok { + respbody = resp2.BodyString(g) + if respbody != nil && len(*respbody) < scn.Conf.ReqLogMaxBodySize { + strrespbody = respbody + } + } + } + + permObj, hasPerm := g.Get("perm") + + hasTok := false + if hasPerm { + hasTok = permObj.(models.PermissionSet).Token != nil + } + + var statuscode *int64 = nil + if resp != nil { + if resp2, ok := resp.(ginext.InspectableHTTPResponse); ok { + statuscode = langext.Ptr(int64(resp2.Statuscode())) + } + } + + var contentType = "" + if resp != nil { + if resp2, ok := resp.(ginext.InspectableHTTPResponse); ok { + contentType = resp2.ContentType() + } + } + + return models.RequestLog{ + Method: g.Request.Method, + URI: g.Request.URL.String(), + UserAgent: langext.Conditional(ua == "", nil, &ua), + Authentication: langext.Conditional(auth == "", nil, &auth), + RequestBody: strreqbody, + RequestBodySize: int64(len(reqbody)), + RequestContentType: ct, + RemoteIP: g.RemoteIP(), + KeyID: langext.ConditionalFn10(hasTok, func() *models.KeyTokenID { return langext.Ptr(permObj.(models.PermissionSet).Token.KeyTokenID) }, nil), + UserID: langext.ConditionalFn10(hasTok, func() *models.UserID { return langext.Ptr(permObj.(models.PermissionSet).Token.OwnerUserID) }, nil), + Permissions: langext.ConditionalFn10(hasTok, func() *string { return langext.Ptr(permObj.(models.PermissionSet).Token.Permissions.String()) }, nil), + ResponseStatuscode: statuscode, + ResponseBodySize: langext.ConditionalFn10(strrespbody != nil, func() *int64 { return langext.Ptr(int64(len(*respbody))) }, nil), + ResponseBody: strrespbody, + ResponseContentType: contentType, + RetryCount: int64(ctr), + Panicked: panicstr != nil, + PanicStr: panicstr, + ProcessingTime: t1.Sub(t0), + TimestampStart: t0, + TimestampFinish: t1, + } +} + +func resetBody(g *gin.Context) error { + if g.Request.Body == nil { + return nil + } + + err := g.Request.Body.(dataext.BufferedReadCloser).Reset() + if err != nil { + return err + } + + return nil +} + +func isSqlite3Busy(r ginext.HTTPResponse) bool { + if errwrap, ok := r.(interface{ Unwrap() error }); ok && errwrap != nil { + { + var s3err *sqlite.Error + if errors.As(errwrap.Unwrap(), &s3err) { + if s3err.Code() == 5 { // [5] == SQLITE_BUSY + return true + } + } + } + } + return false +} + +func BuildGinRequestError(g *gin.Context, fieldtype string, err error) ginext.HTTPResponse { + switch fieldtype { + case "URI": + return ginresp.APIError(g, 400, apierr.BINDFAIL_URI_PARAM, "Failed to read uri", err) + case "QUERY": + return ginresp.APIError(g, 400, apierr.BINDFAIL_QUERY_PARAM, "Failed to read query", err) + case "JSON": + return ginresp.APIError(g, 400, apierr.BINDFAIL_BODY_PARAM, "Failed to read JSON body", err) + case "BODY": + return ginresp.APIError(g, 400, apierr.BINDFAIL_BODY_PARAM, "Failed to read query", err) + case "FORM": + return ginresp.APIError(g, 400, apierr.BINDFAIL_BODY_PARAM, "Failed to read multipart-form / urlencoded-form", err) + case "HEADER": + return ginresp.APIError(g, 400, apierr.BINDFAIL_HEADER_PARAM, "Failed to read header", err) + case "INIT": + return ginresp.APIError(g, 400, apierr.INTERNAL_EXCEPTION, "Failed to init context", err) + default: + return ginresp.APIError(g, 400, apierr.INTERNAL_EXCEPTION, "Failed to init", err) + } +} diff --git a/scnserver/models/enums_gen.go b/scnserver/models/enums_gen.go index 7cec25e..d160741 100644 --- a/scnserver/models/enums_gen.go +++ b/scnserver/models/enums_gen.go @@ -5,7 +5,7 @@ package models import "gogs.mikescher.com/BlackForestBytes/goext/langext" import "gogs.mikescher.com/BlackForestBytes/goext/enums" -const ChecksumEnumGenerator = "fd2e0463f7720d853f7a3394352c084ac7d086e9e012caa3d3d70a6e83749970" // GoExtVersion: 0.0.482 +const ChecksumEnumGenerator = "ba14f2f5d0b0357f248dcbd12933de102c80f1e61be697a37ebb723609fc0c59" // GoExtVersion: 0.0.485 // ================================ ClientType ================================ // diff --git a/scnserver/models/ids_gen.go b/scnserver/models/ids_gen.go index 90df989..74eb55e 100644 --- a/scnserver/models/ids_gen.go +++ b/scnserver/models/ids_gen.go @@ -15,7 +15,7 @@ import "reflect" import "regexp" import "strings" -const ChecksumCharsetIDGenerator = "fd2e0463f7720d853f7a3394352c084ac7d086e9e012caa3d3d70a6e83749970" // GoExtVersion: 0.0.482 +const ChecksumCharsetIDGenerator = "ba14f2f5d0b0357f248dcbd12933de102c80f1e61be697a37ebb723609fc0c59" // GoExtVersion: 0.0.485 const idlen = 24 diff --git a/scnserver/swagger/swagger.json b/scnserver/swagger/swagger.json index cd5657e..fee7ea1 100644 --- a/scnserver/swagger/swagger.json +++ b/scnserver/swagger/swagger.json @@ -19,37 +19,61 @@ "parameters": [ { "type": "string", + "example": "test", + "name": "channel", + "in": "query" + }, + { + "type": "string", + "example": "This is a message", "name": "content", "in": "query" }, { "type": "string", + "example": "P3TNH8mvv14fm", + "name": "key", + "in": "query" + }, + { + "type": "string", + "example": "db8b0e6a-a08c-4646", "name": "msg_id", "in": "query" }, { + "enum": [ + 0, + 1, + 2 + ], "type": "integer", + "example": 1, "name": "priority", "in": "query" }, + { + "type": "string", + "example": "example-server", + "name": "sender_name", + "in": "query" + }, { "type": "number", + "example": 1669824037, "name": "timestamp", "in": "query" }, { "type": "string", + "example": "Hello World", "name": "title", "in": "query" }, - { - "type": "integer", - "name": "user_id", - "in": "query" - }, { "type": "string", - "name": "user_key", + "example": "7725", + "name": "user_id", "in": "query" }, { @@ -62,37 +86,61 @@ }, { "type": "string", + "example": "test", + "name": "channel", + "in": "formData" + }, + { + "type": "string", + "example": "This is a message", "name": "content", "in": "formData" }, { "type": "string", + "example": "P3TNH8mvv14fm", + "name": "key", + "in": "formData" + }, + { + "type": "string", + "example": "db8b0e6a-a08c-4646", "name": "msg_id", "in": "formData" }, { + "enum": [ + 0, + 1, + 2 + ], "type": "integer", + "example": 1, "name": "priority", "in": "formData" }, + { + "type": "string", + "example": "example-server", + "name": "sender_name", + "in": "formData" + }, { "type": "number", + "example": 1669824037, "name": "timestamp", "in": "formData" }, { "type": "string", + "example": "Hello World", "name": "title", "in": "formData" }, - { - "type": "integer", - "name": "user_id", - "in": "formData" - }, { "type": "string", - "name": "user_key", + "example": "7725", + "name": "user_id", "in": "formData" } ], @@ -2717,37 +2765,61 @@ "parameters": [ { "type": "string", + "example": "test", + "name": "channel", + "in": "query" + }, + { + "type": "string", + "example": "This is a message", "name": "content", "in": "query" }, { "type": "string", + "example": "P3TNH8mvv14fm", + "name": "key", + "in": "query" + }, + { + "type": "string", + "example": "db8b0e6a-a08c-4646", "name": "msg_id", "in": "query" }, { + "enum": [ + 0, + 1, + 2 + ], "type": "integer", + "example": 1, "name": "priority", "in": "query" }, + { + "type": "string", + "example": "example-server", + "name": "sender_name", + "in": "query" + }, { "type": "number", + "example": 1669824037, "name": "timestamp", "in": "query" }, { "type": "string", + "example": "Hello World", "name": "title", "in": "query" }, - { - "type": "integer", - "name": "user_id", - "in": "query" - }, { "type": "string", - "name": "user_key", + "example": "7725", + "name": "user_id", "in": "query" }, { @@ -2760,37 +2832,61 @@ }, { "type": "string", + "example": "test", + "name": "channel", + "in": "formData" + }, + { + "type": "string", + "example": "This is a message", "name": "content", "in": "formData" }, { "type": "string", + "example": "P3TNH8mvv14fm", + "name": "key", + "in": "formData" + }, + { + "type": "string", + "example": "db8b0e6a-a08c-4646", "name": "msg_id", "in": "formData" }, { + "enum": [ + 0, + 1, + 2 + ], "type": "integer", + "example": 1, "name": "priority", "in": "formData" }, + { + "type": "string", + "example": "example-server", + "name": "sender_name", + "in": "formData" + }, { "type": "number", + "example": 1669824037, "name": "timestamp", "in": "formData" }, { "type": "string", + "example": "Hello World", "name": "title", "in": "formData" }, - { - "type": "integer", - "name": "user_id", - "in": "formData" - }, { "type": "string", - "name": "user_key", + "example": "7725", + "name": "user_id", "in": "formData" } ], @@ -2839,72 +2935,120 @@ "parameters": [ { "type": "string", + "example": "test", + "name": "channel", + "in": "query" + }, + { + "type": "string", + "example": "This is a message", "name": "content", "in": "query" }, { "type": "string", + "example": "P3TNH8mvv14fm", + "name": "key", + "in": "query" + }, + { + "type": "string", + "example": "db8b0e6a-a08c-4646", "name": "msg_id", "in": "query" }, { + "enum": [ + 0, + 1, + 2 + ], "type": "integer", + "example": 1, "name": "priority", "in": "query" }, + { + "type": "string", + "example": "example-server", + "name": "sender_name", + "in": "query" + }, { "type": "number", + "example": 1669824037, "name": "timestamp", "in": "query" }, { "type": "string", + "example": "Hello World", "name": "title", "in": "query" }, { - "type": "integer", + "type": "string", + "example": "7725", "name": "user_id", "in": "query" }, { "type": "string", - "name": "user_key", - "in": "query" + "example": "test", + "name": "channel", + "in": "formData" }, { "type": "string", + "example": "This is a message", "name": "content", "in": "formData" }, { "type": "string", + "example": "P3TNH8mvv14fm", + "name": "key", + "in": "formData" + }, + { + "type": "string", + "example": "db8b0e6a-a08c-4646", "name": "msg_id", "in": "formData" }, { + "enum": [ + 0, + 1, + 2 + ], "type": "integer", + "example": 1, "name": "priority", "in": "formData" }, + { + "type": "string", + "example": "example-server", + "name": "sender_name", + "in": "formData" + }, { "type": "number", + "example": 1669824037, "name": "timestamp", "in": "formData" }, { "type": "string", + "example": "Hello World", "name": "title", "in": "formData" }, - { - "type": "integer", - "name": "user_id", - "in": "formData" - }, { "type": "string", - "name": "user_key", + "example": "7725", + "name": "user_id", "in": "formData" } ], @@ -2959,6 +3103,7 @@ 1151, 1152, 1153, + 1152, 1161, 1171, 1201, @@ -3004,6 +3149,7 @@ "BINDFAIL_QUERY_PARAM", "BINDFAIL_BODY_PARAM", "BINDFAIL_URI_PARAM", + "BINDFAIL_HEADER_PARAM", "INVALID_BODY_PARAM", "INVALID_ENUM_VALUE", "NO_TITLE", @@ -3401,26 +3547,46 @@ "handler.SendMessage.combined": { "type": "object", "properties": { + "channel": { + "type": "string", + "example": "test" + }, "content": { - "type": "string" + "type": "string", + "example": "This is a message" + }, + "key": { + "type": "string", + "example": "P3TNH8mvv14fm" }, "msg_id": { - "type": "string" + "type": "string", + "example": "db8b0e6a-a08c-4646" }, "priority": { - "type": "integer" + "type": "integer", + "enum": [ + 0, + 1, + 2 + ], + "example": 1 + }, + "sender_name": { + "type": "string", + "example": "example-server" }, "timestamp": { - "type": "number" + "type": "number", + "example": 1669824037 }, "title": { - "type": "string" + "type": "string", + "example": "Hello World" }, "user_id": { - "type": "integer" - }, - "user_key": { - "type": "string" + "type": "string", + "example": "7725" } } }, @@ -3449,7 +3615,7 @@ "type": "integer" }, "scn_msg_id": { - "type": "integer" + "type": "string" }, "success": { "type": "boolean" diff --git a/scnserver/swagger/swagger.yaml b/scnserver/swagger/swagger.yaml index 9a33c92..35c1b67 100644 --- a/scnserver/swagger/swagger.yaml +++ b/scnserver/swagger/swagger.yaml @@ -14,6 +14,7 @@ definitions: - 1151 - 1152 - 1153 + - 1152 - 1161 - 1171 - 1201 @@ -59,6 +60,7 @@ definitions: - BINDFAIL_QUERY_PARAM - BINDFAIL_BODY_PARAM - BINDFAIL_URI_PARAM + - BINDFAIL_HEADER_PARAM - INVALID_BODY_PARAM - INVALID_ENUM_VALUE - NO_TITLE @@ -327,19 +329,36 @@ definitions: type: object handler.SendMessage.combined: properties: + channel: + example: test + type: string content: + example: This is a message + type: string + key: + example: P3TNH8mvv14fm type: string msg_id: + example: db8b0e6a-a08c-4646 type: string priority: + enum: + - 0 + - 1 + - 2 + example: 1 type: integer + sender_name: + example: example-server + type: string timestamp: + example: 1669824037 type: number title: + example: Hello World type: string user_id: - type: integer - user_key: + example: "7725" type: string type: object handler.SendMessage.response: @@ -359,7 +378,7 @@ definitions: quota_max: type: integer scn_msg_id: - type: integer + type: string success: type: boolean suppress_send: @@ -785,52 +804,90 @@ paths: description: All parameter can be set via query-parameter or the json body. Only UserID, UserKey and Title are required parameters: - - in: query + - example: test + in: query + name: channel + type: string + - example: This is a message + in: query name: content type: string - - in: query + - example: P3TNH8mvv14fm + in: query + name: key + type: string + - example: db8b0e6a-a08c-4646 + in: query name: msg_id type: string - - in: query + - enum: + - 0 + - 1 + - 2 + example: 1 + in: query name: priority type: integer - - in: query + - example: example-server + in: query + name: sender_name + type: string + - example: 1669824037 + in: query name: timestamp type: number - - in: query + - example: Hello World + in: query name: title type: string - - in: query + - example: "7725" + in: query name: user_id - type: integer - - in: query - name: user_key type: string - description: ' ' in: body name: post_body schema: $ref: '#/definitions/handler.SendMessage.combined' - - in: formData + - example: test + in: formData + name: channel + type: string + - example: This is a message + in: formData name: content type: string - - in: formData + - example: P3TNH8mvv14fm + in: formData + name: key + type: string + - example: db8b0e6a-a08c-4646 + in: formData name: msg_id type: string - - in: formData + - enum: + - 0 + - 1 + - 2 + example: 1 + in: formData name: priority type: integer - - in: formData + - example: example-server + in: formData + name: sender_name + type: string + - example: 1669824037 + in: formData name: timestamp type: number - - in: formData + - example: Hello World + in: formData name: title type: string - - in: formData + - example: "7725" + in: formData name: user_id - type: integer - - in: formData - name: user_key type: string responses: "200": @@ -2630,52 +2687,90 @@ paths: description: All parameter can be set via query-parameter or the json body. Only UserID, UserKey and Title are required parameters: - - in: query + - example: test + in: query + name: channel + type: string + - example: This is a message + in: query name: content type: string - - in: query + - example: P3TNH8mvv14fm + in: query + name: key + type: string + - example: db8b0e6a-a08c-4646 + in: query name: msg_id type: string - - in: query + - enum: + - 0 + - 1 + - 2 + example: 1 + in: query name: priority type: integer - - in: query + - example: example-server + in: query + name: sender_name + type: string + - example: 1669824037 + in: query name: timestamp type: number - - in: query + - example: Hello World + in: query name: title type: string - - in: query + - example: "7725" + in: query name: user_id - type: integer - - in: query - name: user_key type: string - description: ' ' in: body name: post_body schema: $ref: '#/definitions/handler.SendMessage.combined' - - in: formData + - example: test + in: formData + name: channel + type: string + - example: This is a message + in: formData name: content type: string - - in: formData + - example: P3TNH8mvv14fm + in: formData + name: key + type: string + - example: db8b0e6a-a08c-4646 + in: formData name: msg_id type: string - - in: formData + - enum: + - 0 + - 1 + - 2 + example: 1 + in: formData name: priority type: integer - - in: formData + - example: example-server + in: formData + name: sender_name + type: string + - example: 1669824037 + in: formData name: timestamp type: number - - in: formData + - example: Hello World + in: formData name: title type: string - - in: formData + - example: "7725" + in: formData name: user_id - type: integer - - in: formData - name: user_key type: string responses: "200": @@ -2708,47 +2803,85 @@ paths: description: All parameter can be set via query-parameter or form-data body. Only UserID, UserKey and Title are required parameters: - - in: query + - example: test + in: query + name: channel + type: string + - example: This is a message + in: query name: content type: string - - in: query + - example: P3TNH8mvv14fm + in: query + name: key + type: string + - example: db8b0e6a-a08c-4646 + in: query name: msg_id type: string - - in: query + - enum: + - 0 + - 1 + - 2 + example: 1 + in: query name: priority type: integer - - in: query + - example: example-server + in: query + name: sender_name + type: string + - example: 1669824037 + in: query name: timestamp type: number - - in: query + - example: Hello World + in: query name: title type: string - - in: query + - example: "7725" + in: query name: user_id - type: integer - - in: query - name: user_key type: string - - in: formData + - example: test + in: formData + name: channel + type: string + - example: This is a message + in: formData name: content type: string - - in: formData + - example: P3TNH8mvv14fm + in: formData + name: key + type: string + - example: db8b0e6a-a08c-4646 + in: formData name: msg_id type: string - - in: formData + - enum: + - 0 + - 1 + - 2 + example: 1 + in: formData name: priority type: integer - - in: formData + - example: example-server + in: formData + name: sender_name + type: string + - example: 1669824037 + in: formData name: timestamp type: number - - in: formData + - example: Hello World + in: formData name: title type: string - - in: formData + - example: "7725" + in: formData name: user_id - type: integer - - in: formData - name: user_key type: string responses: "200": diff --git a/scnserver/test/requestlog_test.go b/scnserver/test/requestlog_test.go index f9bb0e3..6ad9e4f 100644 --- a/scnserver/test/requestlog_test.go +++ b/scnserver/test/requestlog_test.go @@ -124,3 +124,26 @@ func TestRequestLogSimple(t *testing.T) { } } + +func TestRequestLogAPI(t *testing.T) { + ws, baseUrl, stop := tt.StartSimpleWebserver(t) + defer stop() + + data := tt.InitDefaultData(t, ws) + time.Sleep(100 * time.Millisecond) + + ctx := ws.NewSimpleTransactionContext(5 * time.Second) + defer ctx.Cancel() + + rl1, _, err := ws.Database.Requests.ListRequestLogs(ctx, models.RequestLogFilter{}, nil, ct.Start()) + tt.TestFailIfErr(t, err) + time.Sleep(100 * time.Millisecond) + + tt.RequestAuthGet[gin.H](t, data.User[0].ReadKey, baseUrl, "/api/v2/users/"+data.User[0].UID) + + rl2, _, err := ws.Database.Requests.ListRequestLogs(ctx, models.RequestLogFilter{}, nil, ct.Start()) + tt.TestFailIfErr(t, err) + time.Sleep(100 * time.Millisecond) + + tt.AssertEqual(t, "requestlog.count", len(rl1)+1, len(rl2)) +} diff --git a/scnserver/test/util/log.go b/scnserver/test/util/log.go index 545b7c0..b757d43 100644 --- a/scnserver/test/util/log.go +++ b/scnserver/test/util/log.go @@ -12,7 +12,6 @@ func SetBufLogger() { buflogger = &BufferWriter{cw: createConsoleWriter()} log.Logger = createLogger(buflogger) gin.SetMode(gin.ReleaseMode) - ginext.SuppressGinLogs = true } func ClearBufLogger(dump bool) { @@ -23,7 +22,6 @@ func ClearBufLogger(dump bool) { log.Logger = createLogger(createConsoleWriter()) buflogger = nil gin.SetMode(gin.TestMode) - ginext.SuppressGinLogs = false if !dump { log.Info().Msgf("Suppressed %d logmessages / printf-statements", size) } diff --git a/scnserver/test/util/webserver.go b/scnserver/test/util/webserver.go index 2477eea..4a9ede0 100644 --- a/scnserver/test/util/webserver.go +++ b/scnserver/test/util/webserver.go @@ -7,6 +7,7 @@ import ( "blackforestbytes.com/simplecloudnotifier/jobs" "blackforestbytes.com/simplecloudnotifier/logic" "blackforestbytes.com/simplecloudnotifier/push" + "gogs.mikescher.com/BlackForestBytes/goext/ginext" "gogs.mikescher.com/BlackForestBytes/goext/langext" "os" "path/filepath" @@ -87,7 +88,13 @@ func StartSimpleWebserver(t *testing.T) (*logic.Application, string, func()) { TestFailErr(t, err) } - ginengine := ginext.NewEngine(scn.Conf) + ginengine := ginext.NewEngine(ginext.Options{ + AllowCors: &scn.Conf.Cors, + GinDebug: &scn.Conf.GinDebug, + BufferBody: langext.PTrue, + Timeout: langext.Ptr(time.Duration(int64(scn.Conf.RequestTimeout) * int64(scn.Conf.RequestMaxRetry))), + BuildRequestBindError: logic.BuildGinRequestError, + }) router := api.NewRouter(app)