From d30e2cefc036a4d05a13f75b8fdd993a651803c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mike=20Schw=C3=B6rer?= Date: Sun, 20 Nov 2022 17:19:11 +0100 Subject: [PATCH] firebase via REST (less dependencies) --- server/api/apierr/enums.go | 2 + server/api/handler/api.go | 136 ++++++++++++------------ server/api/handler/message.go | 50 ++++----- server/cmd/scnserver/main.go | 11 +- server/common/ginresp/resp.go | 72 ++++++++----- server/config.go | 114 ++++++++++++-------- server/firebase/FBOAuth.go | 179 ++++++++++++++++++++++++++++++++ server/firebase/firebase.go | 125 ++++++++++++++-------- server/go.mod | 22 ---- server/go.sum | 148 +------------------------- server/jobs/DeliveryRetryJob.go | 5 +- server/logic/appcontext.go | 19 +++- server/logic/application.go | 21 ++-- server/logic/permissions.go | 10 +- server/logic/simplecontext.go | 1 + 15 files changed, 519 insertions(+), 396 deletions(-) create mode 100644 server/firebase/FBOAuth.go diff --git a/server/api/apierr/enums.go b/server/api/apierr/enums.go index 58c72fa..d58891b 100644 --- a/server/api/apierr/enums.go +++ b/server/api/apierr/enums.go @@ -4,6 +4,8 @@ type APIError int //goland:noinspection GoSnakeCaseUsage const ( + UNDEFINED APIError = -1 + NO_ERROR APIError = 0000 MISSING_UID APIError = 1101 diff --git a/server/api/handler/api.go b/server/api/handler/api.go index 724713c..626fed4 100644 --- a/server/api/handler/api.go +++ b/server/api/handler/api.go @@ -61,17 +61,17 @@ func (h APIHandler) CreateUser(g *gin.Context) ginresp.HTTPResponse { } else if b.ClientType == string(models.ClientTypeIOS) { clientType = models.ClientTypeIOS } else { - return ginresp.InternAPIError(400, apierr.INVALID_CLIENTTYPE, "Invalid ClientType", nil) + return ginresp.InternAPIError(g, 400, apierr.INVALID_CLIENTTYPE, "Invalid ClientType", nil) } if b.ProToken != nil { ptok, err := h.app.VerifyProToken(*b.ProToken) if err != nil { - return ginresp.InternAPIError(500, apierr.FAILED_VERIFY_PRO_TOKEN, "Failed to query purchase status", err) + return ginresp.InternAPIError(g, 500, apierr.FAILED_VERIFY_PRO_TOKEN, "Failed to query purchase status", err) } if !ptok { - return ginresp.InternAPIError(400, apierr.INVALID_PRO_TOKEN, "Purchase token could not be verified", nil) + return ginresp.InternAPIError(g, 400, apierr.INVALID_PRO_TOKEN, "Purchase token could not be verified", nil) } } @@ -81,13 +81,13 @@ func (h APIHandler) CreateUser(g *gin.Context) ginresp.HTTPResponse { err := h.database.ClearFCMTokens(ctx, b.FCMToken) if err != nil { - return ginresp.InternAPIError(500, apierr.DATABASE_ERROR, "Failed to clear existing fcm tokens", err) + return ginresp.InternAPIError(g, 500, apierr.DATABASE_ERROR, "Failed to clear existing fcm tokens", err) } if b.ProToken != nil { err := h.database.ClearProTokens(ctx, *b.ProToken) if err != nil { - return ginresp.InternAPIError(500, apierr.DATABASE_ERROR, "Failed to clear existing fcm tokens", err) + return ginresp.InternAPIError(g, 500, apierr.DATABASE_ERROR, "Failed to clear existing fcm tokens", err) } } @@ -98,12 +98,12 @@ func (h APIHandler) CreateUser(g *gin.Context) ginresp.HTTPResponse { userobj, err := h.database.CreateUser(ctx, readKey, sendKey, adminKey, b.ProToken, username) if err != nil { - return ginresp.InternAPIError(500, apierr.DATABASE_ERROR, "Failed to create user in db", err) + return ginresp.InternAPIError(g, 500, apierr.DATABASE_ERROR, "Failed to create user in db", err) } _, err = h.database.CreateClient(ctx, userobj.UserID, clientType, b.FCMToken, b.AgentModel, b.AgentVersion) if err != nil { - return ginresp.InternAPIError(500, apierr.DATABASE_ERROR, "Failed to create user in db", err) + return ginresp.InternAPIError(g, 500, apierr.DATABASE_ERROR, "Failed to create user in db", err) } return ctx.FinishSuccess(ginresp.JSON(http.StatusOK, userobj.JSON())) @@ -141,10 +141,10 @@ func (h APIHandler) GetUser(g *gin.Context) ginresp.HTTPResponse { user, err := h.database.GetUser(ctx, u.UserID) if err == sql.ErrNoRows { - return ginresp.InternAPIError(404, apierr.USER_NOT_FOUND, "User not found", err) + return ginresp.InternAPIError(g, 404, apierr.USER_NOT_FOUND, "User not found", err) } if err != nil { - return ginresp.InternAPIError(500, apierr.DATABASE_ERROR, "Failed to query user", err) + return ginresp.InternAPIError(g, 500, apierr.DATABASE_ERROR, "Failed to query user", err) } return ctx.FinishSuccess(ginresp.JSON(http.StatusOK, user.JSON())) @@ -194,34 +194,34 @@ func (h APIHandler) UpdateUser(g *gin.Context) ginresp.HTTPResponse { err := h.database.UpdateUserUsername(ctx, u.UserID, b.Username) if err != nil { - return ginresp.InternAPIError(500, apierr.DATABASE_ERROR, "Failed to update user", err) + return ginresp.InternAPIError(g, 500, apierr.DATABASE_ERROR, "Failed to update user", err) } } if b.ProToken != nil { ptok, err := h.app.VerifyProToken(*b.ProToken) if err != nil { - return ginresp.InternAPIError(500, apierr.FAILED_VERIFY_PRO_TOKEN, "Failed to query purchase status", err) + return ginresp.InternAPIError(g, 500, apierr.FAILED_VERIFY_PRO_TOKEN, "Failed to query purchase status", err) } if !ptok { - return ginresp.InternAPIError(400, apierr.INVALID_PRO_TOKEN, "Purchase token could not be verified", nil) + return ginresp.InternAPIError(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.InternAPIError(500, apierr.DATABASE_ERROR, "Failed to clear existing fcm tokens", err) + return ginresp.InternAPIError(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.InternAPIError(500, apierr.DATABASE_ERROR, "Failed to update user", err) + return ginresp.InternAPIError(g, 500, apierr.DATABASE_ERROR, "Failed to update user", err) } } user, err := h.database.GetUser(ctx, u.UserID) if err != nil { - return ginresp.InternAPIError(500, apierr.DATABASE_ERROR, "Failed to query (updated) user", err) + return ginresp.InternAPIError(g, 500, apierr.DATABASE_ERROR, "Failed to query (updated) user", err) } return ctx.FinishSuccess(ginresp.JSON(http.StatusOK, user.JSON())) @@ -262,7 +262,7 @@ func (h APIHandler) ListClients(g *gin.Context) ginresp.HTTPResponse { clients, err := h.database.ListClients(ctx, u.UserID) if err != nil { - return ginresp.InternAPIError(500, apierr.DATABASE_ERROR, "Failed to query clients", err) + return ginresp.InternAPIError(g, 500, apierr.DATABASE_ERROR, "Failed to query clients", err) } res := langext.ArrMap(clients, func(v models.Client) models.ClientJSON { return v.JSON() }) @@ -304,10 +304,10 @@ func (h APIHandler) GetClient(g *gin.Context) ginresp.HTTPResponse { client, err := h.database.GetClient(ctx, u.UserID, u.ClientID) if err == sql.ErrNoRows { - return ginresp.InternAPIError(404, apierr.CLIENT_NOT_FOUND, "Client not found", err) + return ginresp.InternAPIError(g, 404, apierr.CLIENT_NOT_FOUND, "Client not found", err) } if err != nil { - return ginresp.InternAPIError(500, apierr.DATABASE_ERROR, "Failed to query client", err) + return ginresp.InternAPIError(g, 500, apierr.DATABASE_ERROR, "Failed to query client", err) } return ctx.FinishSuccess(ginresp.JSON(http.StatusOK, client.JSON())) @@ -354,7 +354,7 @@ func (h APIHandler) AddClient(g *gin.Context) ginresp.HTTPResponse { } else if b.ClientType == string(models.ClientTypeIOS) { clientType = models.ClientTypeIOS } else { - return ginresp.InternAPIError(400, apierr.INVALID_CLIENTTYPE, "Invalid ClientType", nil) + return ginresp.InternAPIError(g, 400, apierr.INVALID_CLIENTTYPE, "Invalid ClientType", nil) } if permResp := ctx.CheckPermissionUserAdmin(u.UserID); permResp != nil { @@ -363,7 +363,7 @@ func (h APIHandler) AddClient(g *gin.Context) ginresp.HTTPResponse { client, err := h.database.CreateClient(ctx, u.UserID, clientType, b.FCMToken, b.AgentModel, b.AgentVersion) if err != nil { - return ginresp.InternAPIError(500, apierr.DATABASE_ERROR, "Failed to create user in db", err) + return ginresp.InternAPIError(g, 500, apierr.DATABASE_ERROR, "Failed to create user in db", err) } return ctx.FinishSuccess(ginresp.JSON(http.StatusOK, client.JSON())) @@ -403,15 +403,15 @@ func (h APIHandler) DeleteClient(g *gin.Context) ginresp.HTTPResponse { client, err := h.database.GetClient(ctx, u.UserID, u.ClientID) if err == sql.ErrNoRows { - return ginresp.InternAPIError(404, apierr.CLIENT_NOT_FOUND, "Client not found", err) + return ginresp.InternAPIError(g, 404, apierr.CLIENT_NOT_FOUND, "Client not found", err) } if err != nil { - return ginresp.InternAPIError(500, apierr.DATABASE_ERROR, "Failed to query client", err) + return ginresp.InternAPIError(g, 500, apierr.DATABASE_ERROR, "Failed to query client", err) } err = h.database.DeleteClient(ctx, u.ClientID) if err != nil { - return ginresp.InternAPIError(500, apierr.DATABASE_ERROR, "Failed to delete client", err) + return ginresp.InternAPIError(g, 500, apierr.DATABASE_ERROR, "Failed to delete client", err) } return ctx.FinishSuccess(ginresp.JSON(http.StatusOK, client.JSON())) @@ -452,7 +452,7 @@ func (h APIHandler) ListChannels(g *gin.Context) ginresp.HTTPResponse { clients, err := h.database.ListChannels(ctx, u.UserID) if err != nil { - return ginresp.InternAPIError(500, apierr.DATABASE_ERROR, "Failed to query channels", err) + return ginresp.InternAPIError(g, 500, apierr.DATABASE_ERROR, "Failed to query channels", err) } res := langext.ArrMap(clients, func(v models.Channel) models.ChannelJSON { return v.JSON() }) @@ -494,10 +494,10 @@ func (h APIHandler) GetChannel(g *gin.Context) ginresp.HTTPResponse { channel, err := h.database.GetChannel(ctx, u.UserID, u.ChannelID) if err == sql.ErrNoRows { - return ginresp.InternAPIError(404, apierr.CHANNEL_NOT_FOUND, "Channel not found", err) + return ginresp.InternAPIError(g, 404, apierr.CHANNEL_NOT_FOUND, "Channel not found", err) } if err != nil { - return ginresp.InternAPIError(500, apierr.DATABASE_ERROR, "Failed to query channel", err) + return ginresp.InternAPIError(g, 500, apierr.DATABASE_ERROR, "Failed to query channel", err) } return ctx.FinishSuccess(ginresp.JSON(http.StatusOK, channel.JSON())) @@ -558,33 +558,33 @@ func (h APIHandler) ListChannelMessages(g *gin.Context) ginresp.HTTPResponse { channel, err := h.database.GetChannel(ctx, u.ChannelUserID, u.ChannelID) if err == sql.ErrNoRows { - return ginresp.InternAPIError(404, apierr.CHANNEL_NOT_FOUND, "Channel not found", err) + return ginresp.InternAPIError(g, 404, apierr.CHANNEL_NOT_FOUND, "Channel not found", err) } if err != nil { - return ginresp.InternAPIError(500, apierr.DATABASE_ERROR, "Failed to query channel", err) + return ginresp.InternAPIError(g, 500, apierr.DATABASE_ERROR, "Failed to query channel", err) } userid := *ctx.GetPermissionUserID() sub, err := h.database.GetSubscriptionBySubscriber(ctx, userid, channel.ChannelID) if err == sql.ErrNoRows { - return ginresp.InternAPIError(401, apierr.USER_AUTH_FAILED, "You are not authorized for this action", nil) + return ginresp.InternAPIError(g, 401, apierr.USER_AUTH_FAILED, "You are not authorized for this action", nil) } if err != nil { - return ginresp.InternAPIError(500, apierr.DATABASE_ERROR, "Failed to query subscription", err) + return ginresp.InternAPIError(g, 500, apierr.DATABASE_ERROR, "Failed to query subscription", err) } if !sub.Confirmed { - return ginresp.InternAPIError(401, apierr.USER_AUTH_FAILED, "You are not authorized for this action", nil) + return ginresp.InternAPIError(g, 401, apierr.USER_AUTH_FAILED, "You are not authorized for this action", nil) } tok, err := cursortoken.Decode(langext.Coalesce(q.NextPageToken, "")) if err != nil { - return ginresp.InternAPIError(500, apierr.PAGETOKEN_ERROR, "Failed to decode next_page_token", err) + return ginresp.InternAPIError(g, 500, apierr.PAGETOKEN_ERROR, "Failed to decode next_page_token", err) } messages, npt, err := h.database.ListChannelMessages(ctx, channel.ChannelID, pageSize, tok) if err != nil { - return ginresp.InternAPIError(500, apierr.DATABASE_ERROR, "Failed to query messages", err) + return ginresp.InternAPIError(g, 500, apierr.DATABASE_ERROR, "Failed to query messages", err) } var res []models.MessageJSON @@ -632,7 +632,7 @@ func (h APIHandler) ListUserSubscriptions(g *gin.Context) ginresp.HTTPResponse { clients, err := h.database.ListSubscriptionsByOwner(ctx, u.UserID) if err != nil { - return ginresp.InternAPIError(500, apierr.DATABASE_ERROR, "Failed to query channels", err) + return ginresp.InternAPIError(g, 500, apierr.DATABASE_ERROR, "Failed to query channels", err) } res := langext.ArrMap(clients, func(v models.Subscription) models.SubscriptionJSON { return v.JSON() }) @@ -677,15 +677,15 @@ func (h APIHandler) ListChannelSubscriptions(g *gin.Context) ginresp.HTTPRespons _, err := h.database.GetChannel(ctx, u.UserID, u.ChannelID) if err == sql.ErrNoRows { - return ginresp.InternAPIError(404, apierr.CHANNEL_NOT_FOUND, "Channel not found", err) + return ginresp.InternAPIError(g, 404, apierr.CHANNEL_NOT_FOUND, "Channel not found", err) } if err != nil { - return ginresp.InternAPIError(500, apierr.DATABASE_ERROR, "Failed to query channels", err) + return ginresp.InternAPIError(g, 500, apierr.DATABASE_ERROR, "Failed to query channels", err) } clients, err := h.database.ListSubscriptionsByChannel(ctx, u.ChannelID) if err != nil { - return ginresp.InternAPIError(500, apierr.DATABASE_ERROR, "Failed to query channels", err) + return ginresp.InternAPIError(g, 500, apierr.DATABASE_ERROR, "Failed to query channels", err) } res := langext.ArrMap(clients, func(v models.Subscription) models.SubscriptionJSON { return v.JSON() }) @@ -727,14 +727,14 @@ func (h APIHandler) GetSubscription(g *gin.Context) ginresp.HTTPResponse { subscription, err := h.database.GetSubscription(ctx, u.SubscriptionID) if err == sql.ErrNoRows { - return ginresp.InternAPIError(404, apierr.SUBSCRIPTION_NOT_FOUND, "Subscription not found", err) + return ginresp.InternAPIError(g, 404, apierr.SUBSCRIPTION_NOT_FOUND, "Subscription not found", err) } if err != nil { - return ginresp.InternAPIError(500, apierr.DATABASE_ERROR, "Failed to query subscription", err) + return ginresp.InternAPIError(g, 500, apierr.DATABASE_ERROR, "Failed to query subscription", err) } if subscription.SubscriberUserID != u.UserID { - return ginresp.InternAPIError(401, apierr.USER_AUTH_FAILED, "You are not authorized for this action", nil) + return ginresp.InternAPIError(g, 401, apierr.USER_AUTH_FAILED, "You are not authorized for this action", nil) } return ctx.FinishSuccess(ginresp.JSON(http.StatusOK, subscription.JSON())) @@ -774,19 +774,19 @@ func (h APIHandler) CancelSubscription(g *gin.Context) ginresp.HTTPResponse { subscription, err := h.database.GetSubscription(ctx, u.SubscriptionID) if err == sql.ErrNoRows { - return ginresp.InternAPIError(404, apierr.SUBSCRIPTION_NOT_FOUND, "Subscription not found", err) + return ginresp.InternAPIError(g, 404, apierr.SUBSCRIPTION_NOT_FOUND, "Subscription not found", err) } if err != nil { - return ginresp.InternAPIError(500, apierr.DATABASE_ERROR, "Failed to query subscription", err) + return ginresp.InternAPIError(g, 500, apierr.DATABASE_ERROR, "Failed to query subscription", err) } if subscription.SubscriberUserID != u.UserID && subscription.ChannelOwnerUserID != u.UserID { - return ginresp.InternAPIError(401, apierr.USER_AUTH_FAILED, "You are not authorized for this action", nil) + return ginresp.InternAPIError(g, 401, apierr.USER_AUTH_FAILED, "You are not authorized for this action", nil) } err = h.database.DeleteSubscription(ctx, u.SubscriptionID) if err != nil { - return ginresp.InternAPIError(500, apierr.DATABASE_ERROR, "Failed to delete subscription", err) + return ginresp.InternAPIError(g, 500, apierr.DATABASE_ERROR, "Failed to delete subscription", err) } return ctx.FinishSuccess(ginresp.JSON(http.StatusOK, subscription.JSON())) @@ -835,19 +835,19 @@ func (h APIHandler) CreateSubscription(g *gin.Context) ginresp.HTTPResponse { channel, err := h.database.GetChannelByName(ctx, b.ChannelOwnerUserID, h.app.NormalizeChannelName(b.Channel)) if err != nil { - return ginresp.InternAPIError(500, apierr.DATABASE_ERROR, "Failed to query channel", err) + return ginresp.InternAPIError(g, 500, apierr.DATABASE_ERROR, "Failed to query channel", err) } if channel == nil { - return ginresp.InternAPIError(400, apierr.CHANNEL_NOT_FOUND, "Channel not found", err) + return ginresp.InternAPIError(g, 400, apierr.CHANNEL_NOT_FOUND, "Channel not found", err) } if channel.OwnerUserID != u.UserID && (q.ChanSubscribeKey == nil || *q.ChanSubscribeKey != channel.SubscribeKey) { - ginresp.InternAPIError(401, apierr.USER_AUTH_FAILED, "You are not authorized for this action", nil) + ginresp.InternAPIError(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.InternAPIError(500, apierr.DATABASE_ERROR, "Failed to create subscription", err) + return ginresp.InternAPIError(g, 500, apierr.DATABASE_ERROR, "Failed to create subscription", err) } return ctx.FinishSuccess(ginresp.JSON(http.StatusOK, sub.JSON())) @@ -891,26 +891,26 @@ func (h APIHandler) UpdateSubscription(g *gin.Context) ginresp.HTTPResponse { subscription, err := h.database.GetSubscription(ctx, u.SubscriptionID) if err == sql.ErrNoRows { - return ginresp.InternAPIError(404, apierr.SUBSCRIPTION_NOT_FOUND, "Subscription not found", err) + return ginresp.InternAPIError(g, 404, apierr.SUBSCRIPTION_NOT_FOUND, "Subscription not found", err) } if err != nil { - return ginresp.InternAPIError(500, apierr.DATABASE_ERROR, "Failed to query subscription", err) + return ginresp.InternAPIError(g, 500, apierr.DATABASE_ERROR, "Failed to query subscription", err) } if subscription.ChannelOwnerUserID != u.UserID { - return ginresp.InternAPIError(401, apierr.USER_AUTH_FAILED, "You are not authorized for this action", nil) + return ginresp.InternAPIError(g, 401, apierr.USER_AUTH_FAILED, "You are not authorized for this action", nil) } if b.Confirmed != nil { err = h.database.UpdateSubscriptionConfirmed(ctx, u.SubscriptionID, *b.Confirmed) if err != nil { - return ginresp.InternAPIError(500, apierr.DATABASE_ERROR, "Failed to update subscription", err) + return ginresp.InternAPIError(g, 500, apierr.DATABASE_ERROR, "Failed to update subscription", err) } } subscription, err = h.database.GetSubscription(ctx, u.SubscriptionID) if err != nil { - return ginresp.InternAPIError(500, apierr.DATABASE_ERROR, "Failed to query subscription", err) + return ginresp.InternAPIError(g, 500, apierr.DATABASE_ERROR, "Failed to query subscription", err) } return ctx.FinishSuccess(ginresp.JSON(http.StatusOK, subscription.JSON())) @@ -968,17 +968,17 @@ func (h APIHandler) ListMessages(g *gin.Context) ginresp.HTTPResponse { tok, err := cursortoken.Decode(langext.Coalesce(q.NextPageToken, "")) if err != nil { - return ginresp.InternAPIError(500, apierr.PAGETOKEN_ERROR, "Failed to decode next_page_token", err) + return ginresp.InternAPIError(g, 500, apierr.PAGETOKEN_ERROR, "Failed to decode next_page_token", err) } err = h.database.UpdateUserLastRead(ctx, userid) if err != nil { - return ginresp.InternAPIError(500, apierr.DATABASE_ERROR, "Failed to update last-read", err) + return ginresp.InternAPIError(g, 500, apierr.DATABASE_ERROR, "Failed to update last-read", err) } messages, npt, err := h.database.ListMessages(ctx, userid, pageSize, tok) if err != nil { - return ginresp.InternAPIError(500, apierr.DATABASE_ERROR, "Failed to query messages", err) + return ginresp.InternAPIError(g, 500, apierr.DATABASE_ERROR, "Failed to query messages", err) } var res []models.MessageJSON @@ -1026,10 +1026,10 @@ func (h APIHandler) GetMessage(g *gin.Context) ginresp.HTTPResponse { msg, err := h.database.GetMessage(ctx, u.MessageID) if err == sql.ErrNoRows { - return ginresp.InternAPIError(404, apierr.MESSAGE_NOT_FOUND, "message not found", err) + return ginresp.InternAPIError(g, 404, apierr.MESSAGE_NOT_FOUND, "message not found", err) } if err != nil { - return ginresp.InternAPIError(500, apierr.DATABASE_ERROR, "Failed to query message", err) + return ginresp.InternAPIError(g, 500, apierr.DATABASE_ERROR, "Failed to query message", err) } if !ctx.CheckPermissionMessageReadDirect(msg) { @@ -1040,21 +1040,21 @@ func (h APIHandler) GetMessage(g *gin.Context) ginresp.HTTPResponse { if uid := ctx.GetPermissionUserID(); uid != nil && ctx.IsPermissionUserRead() { sub, err := h.database.GetSubscriptionBySubscriber(ctx, *uid, msg.ChannelID) if err != nil { - return ginresp.InternAPIError(500, apierr.DATABASE_ERROR, "Failed to query subscription", err) + return ginresp.InternAPIError(g, 500, apierr.DATABASE_ERROR, "Failed to query subscription", err) } if sub == nil { // not subbed - return ginresp.InternAPIError(401, apierr.USER_AUTH_FAILED, "You are not authorized for this action", nil) + return ginresp.InternAPIError(g, 401, apierr.USER_AUTH_FAILED, "You are not authorized for this action", nil) } if !sub.Confirmed { // sub not confirmed - return ginresp.InternAPIError(401, apierr.USER_AUTH_FAILED, "You are not authorized for this action", nil) + return ginresp.InternAPIError(g, 401, apierr.USER_AUTH_FAILED, "You are not authorized for this action", nil) } // => perm okay } else { // auth-key is not set or not a user:x variant - return ginresp.InternAPIError(401, apierr.USER_AUTH_FAILED, "You are not authorized for this action", nil) + return ginresp.InternAPIError(g, 401, apierr.USER_AUTH_FAILED, "You are not authorized for this action", nil) } } @@ -1095,29 +1095,29 @@ func (h APIHandler) DeleteMessage(g *gin.Context) ginresp.HTTPResponse { msg, err := h.database.GetMessage(ctx, u.MessageID) if err == sql.ErrNoRows { - return ginresp.InternAPIError(404, apierr.MESSAGE_NOT_FOUND, "message not found", err) + return ginresp.InternAPIError(g, 404, apierr.MESSAGE_NOT_FOUND, "message not found", err) } if err != nil { - return ginresp.InternAPIError(500, apierr.DATABASE_ERROR, "Failed to query message", err) + return ginresp.InternAPIError(g, 500, apierr.DATABASE_ERROR, "Failed to query message", err) } if !ctx.CheckPermissionMessageReadDirect(msg) { - return ginresp.InternAPIError(401, apierr.USER_AUTH_FAILED, "You are not authorized for this action", nil) + return ginresp.InternAPIError(g, 401, apierr.USER_AUTH_FAILED, "You are not authorized for this action", nil) } err = h.database.DeleteMessage(ctx, msg.SCNMessageID) if err != nil { - return ginresp.InternAPIError(500, apierr.DATABASE_ERROR, "Failed to delete message", err) + return ginresp.InternAPIError(g, 500, apierr.DATABASE_ERROR, "Failed to delete message", err) } err = h.database.CancelPendingDeliveries(ctx, msg.SCNMessageID) if err != nil { - return ginresp.InternAPIError(500, apierr.DATABASE_ERROR, "Failed to cancel deliveries", err) + return ginresp.InternAPIError(g, 500, apierr.DATABASE_ERROR, "Failed to cancel deliveries", err) } return ctx.FinishSuccess(ginresp.JSON(http.StatusOK, msg.FullJSON())) } func (h APIHandler) SendMessage(g *gin.Context) ginresp.HTTPResponse { - return ginresp.NotImplemented() //TODO + return ginresp.NotImplemented(g) //TODO } diff --git a/server/api/handler/message.go b/server/api/handler/message.go index 7f57516..0d39a57 100644 --- a/server/api/handler/message.go +++ b/server/api/handler/message.go @@ -77,7 +77,7 @@ func (h MessageHandler) SendMessageCompat(g *gin.Context) ginresp.HTTPResponse { data := dataext.ObjectMerge(f, q) - return h.sendMessageInternal(ctx, data.UserID, data.UserKey, langext.Ptr(h.app.DefaultChannel), nil, data.Title, data.Content, data.Priority, data.UserMessageID, data.SendTimestamp) + return h.sendMessageInternal(g, ctx, data.UserID, data.UserKey, langext.Ptr(h.app.DefaultChannel), nil, data.Title, data.Content, data.Priority, data.UserMessageID, data.SendTimestamp) } @@ -132,11 +132,11 @@ func (h MessageHandler) SendMessage(g *gin.Context) ginresp.HTTPResponse { data := dataext.ObjectMerge(b, q) - return h.sendMessageInternal(ctx, data.UserID, data.UserKey, data.Channel, data.ChanKey, data.Title, data.Content, data.Priority, data.UserMessageID, data.SendTimestamp) + return h.sendMessageInternal(g, ctx, data.UserID, data.UserKey, data.Channel, data.ChanKey, data.Title, data.Content, data.Priority, data.UserMessageID, data.SendTimestamp) } -func (h MessageHandler) sendMessageInternal(ctx *logic.AppContext, UserID *int64, UserKey *string, Channel *string, ChanKey *string, Title *string, Content *string, Priority *int, UserMessageID *string, SendTimestamp *float64) ginresp.HTTPResponse { +func (h MessageHandler) sendMessageInternal(g *gin.Context, ctx *logic.AppContext, UserID *int64, UserKey *string, Channel *string, ChanKey *string, Title *string, Content *string, Priority *int, UserMessageID *string, SendTimestamp *float64) ginresp.HTTPResponse { type response struct { Success bool `json:"success"` ErrorID apierr.APIError `json:"error"` @@ -158,25 +158,25 @@ func (h MessageHandler) sendMessageInternal(ctx *logic.AppContext, UserID *int64 } if UserID == nil { - return ginresp.SendAPIError(400, apierr.MISSING_UID, 101, "Missing parameter [[user_id]]", nil) + return ginresp.SendAPIError(g, 400, apierr.MISSING_UID, 101, "Missing parameter [[user_id]]", nil) } if UserKey == nil { - return ginresp.SendAPIError(400, apierr.MISSING_UID, 102, "Missing parameter [[user_token]]", nil) + return ginresp.SendAPIError(g, 400, apierr.MISSING_UID, 102, "Missing parameter [[user_token]]", nil) } if Title == nil { - return ginresp.SendAPIError(400, apierr.MISSING_UID, 103, "Missing parameter [[title]]", nil) + return ginresp.SendAPIError(g, 400, apierr.MISSING_UID, 103, "Missing parameter [[title]]", nil) } if SendTimestamp != nil && mathext.Abs(*SendTimestamp-float64(time.Now().Unix())) > (24*time.Hour).Seconds() { - return ginresp.SendAPIError(400, apierr.TIMESTAMP_OUT_OF_RANGE, -1, "The timestamp mus be within 24 hours of now()", nil) + return ginresp.SendAPIError(g, 400, apierr.TIMESTAMP_OUT_OF_RANGE, -1, "The timestamp mus be within 24 hours of now()", nil) } if Priority != nil && (*Priority != 0 && *Priority != 1 && *Priority != 2) { - return ginresp.SendAPIError(400, apierr.INVALID_PRIO, 105, "Invalid priority", nil) + return ginresp.SendAPIError(g, 400, apierr.INVALID_PRIO, 105, "Invalid priority", nil) } if len(*Title) == 0 { - return ginresp.SendAPIError(400, apierr.NO_TITLE, 103, "No title specified", nil) + return ginresp.SendAPIError(g, 400, apierr.NO_TITLE, 103, "No title specified", nil) } if UserMessageID != nil && len(*UserMessageID) > 64 { - return ginresp.SendAPIError(400, apierr.USR_MSG_ID_TOO_LONG, -1, "MessageID too long (64 characters)", nil) + return ginresp.SendAPIError(g, 400, apierr.USR_MSG_ID_TOO_LONG, -1, "MessageID too long (64 characters)", nil) } channelName := h.app.DefaultChannel @@ -186,23 +186,23 @@ func (h MessageHandler) sendMessageInternal(ctx *logic.AppContext, UserID *int64 user, err := h.database.GetUser(ctx, *UserID) if err == sql.ErrNoRows { - return ginresp.SendAPIError(400, apierr.USER_NOT_FOUND, -1, "User not found", nil) + return ginresp.SendAPIError(g, 400, apierr.USER_NOT_FOUND, -1, "User not found", nil) } if err != nil { - return ginresp.SendAPIError(500, apierr.DATABASE_ERROR, -1, "Failed to query user", err) + return ginresp.SendAPIError(g, 500, apierr.DATABASE_ERROR, -1, "Failed to query user", err) } if len(*Title) > 120 { - return ginresp.SendAPIError(400, apierr.TITLE_TOO_LONG, 103, "Title too long (120 characters)", nil) + return ginresp.SendAPIError(g, 400, apierr.TITLE_TOO_LONG, 103, "Title too long (120 characters)", nil) } if Content != nil && len(*Content) > user.MaxContentLength() { - return ginresp.SendAPIError(400, apierr.CONTENT_TOO_LONG, 104, fmt.Sprintf("Content too long (%d characters; max := %d characters)", len(*Content), user.MaxContentLength()), nil) + return ginresp.SendAPIError(g, 400, apierr.CONTENT_TOO_LONG, 104, fmt.Sprintf("Content too long (%d characters; max := %d characters)", len(*Content), user.MaxContentLength()), nil) } if UserMessageID != nil { msg, err := h.database.GetMessageByUserMessageID(ctx, *UserMessageID) if err != nil { - return ginresp.SendAPIError(500, apierr.DATABASE_ERROR, -1, "Failed to query existing message", err) + return ginresp.SendAPIError(g, 500, apierr.DATABASE_ERROR, -1, "Failed to query existing message", err) } if msg != nil { return ctx.FinishSuccess(ginresp.JSON(http.StatusOK, response{ @@ -221,12 +221,12 @@ func (h MessageHandler) sendMessageInternal(ctx *logic.AppContext, UserID *int64 } if user.QuotaRemainingToday() <= 0 { - return ginresp.SendAPIError(403, apierr.QUOTA_REACHED, -1, fmt.Sprintf("Daily quota reached (%d)", user.QuotaPerDay()), nil) + return ginresp.SendAPIError(g, 403, apierr.QUOTA_REACHED, -1, fmt.Sprintf("Daily quota reached (%d)", user.QuotaPerDay()), nil) } channel, err := h.app.GetOrCreateChannel(ctx, *UserID, channelName) if err != nil { - return ginresp.SendAPIError(500, apierr.DATABASE_ERROR, -1, "Failed to query/create channel", err) + return ginresp.SendAPIError(g, 500, apierr.DATABASE_ERROR, -1, "Failed to query/create channel", err) } selfChanAdmin := *UserID == channel.OwnerUserID && *UserKey == user.AdminKey @@ -234,7 +234,7 @@ func (h MessageHandler) sendMessageInternal(ctx *logic.AppContext, UserID *int64 forgChanSend := *UserID != channel.OwnerUserID && ChanKey != nil && *ChanKey == channel.SendKey if !selfChanAdmin && !selfChanSend && !forgChanSend { - return ginresp.SendAPIError(401, apierr.USER_AUTH_FAILED, 102, fmt.Sprintf("Daily quota reached (%d)", user.QuotaPerDay()), nil) + return ginresp.SendAPIError(g, 401, apierr.USER_AUTH_FAILED, 102, fmt.Sprintf("Daily quota reached (%d)", user.QuotaPerDay()), nil) } var sendTimestamp *time.Time = nil @@ -246,28 +246,28 @@ func (h MessageHandler) sendMessageInternal(ctx *logic.AppContext, UserID *int64 msg, err := h.database.CreateMessage(ctx, *UserID, channel, sendTimestamp, *Title, Content, priority, UserMessageID) if err != nil { - return ginresp.SendAPIError(500, apierr.DATABASE_ERROR, -1, "Failed to create message in db", err) + return ginresp.SendAPIError(g, 500, apierr.DATABASE_ERROR, -1, "Failed to create message in db", err) } subscriptions, err := h.database.ListSubscriptionsByChannel(ctx, channel.ChannelID) if err != nil { - return ginresp.SendAPIError(500, apierr.DATABASE_ERROR, -1, "Failed to query subscriptions", err) + return ginresp.SendAPIError(g, 500, apierr.DATABASE_ERROR, -1, "Failed to query subscriptions", err) } err = h.database.IncUserMessageCounter(ctx, user) if err != nil { - return ginresp.SendAPIError(500, apierr.DATABASE_ERROR, -1, "Failed to inc user msg-counter", err) + return ginresp.SendAPIError(g, 500, apierr.DATABASE_ERROR, -1, "Failed to inc user msg-counter", err) } err = h.database.IncChannelMessageCounter(ctx, channel) if err != nil { - return ginresp.SendAPIError(500, apierr.DATABASE_ERROR, -1, "Failed to inc channel msg-counter", err) + return ginresp.SendAPIError(g, 500, apierr.DATABASE_ERROR, -1, "Failed to inc channel msg-counter", err) } for _, sub := range subscriptions { clients, err := h.database.ListClients(ctx, sub.SubscriberUserID) if err != nil { - return ginresp.SendAPIError(500, apierr.DATABASE_ERROR, -1, "Failed to query clients", err) + return ginresp.SendAPIError(g, 500, apierr.DATABASE_ERROR, -1, "Failed to query clients", err) } if !sub.Confirmed { @@ -280,12 +280,12 @@ func (h MessageHandler) sendMessageInternal(ctx *logic.AppContext, UserID *int64 if err != nil { _, err = h.database.CreateRetryDelivery(ctx, client, msg) if err != nil { - return ginresp.SendAPIError(500, apierr.DATABASE_ERROR, -1, "Failed to create delivery", err) + return ginresp.SendAPIError(g, 500, apierr.DATABASE_ERROR, -1, "Failed to create delivery", err) } } else { _, err = h.database.CreateSuccessDelivery(ctx, client, msg, *fcmDelivID) if err != nil { - return ginresp.SendAPIError(500, apierr.DATABASE_ERROR, -1, "Failed to create delivery", err) + return ginresp.SendAPIError(g, 500, apierr.DATABASE_ERROR, -1, "Failed to create delivery", err) } } diff --git a/server/cmd/scnserver/main.go b/server/cmd/scnserver/main.go index 55a3aac..b219ee1 100644 --- a/server/cmd/scnserver/main.go +++ b/server/cmd/scnserver/main.go @@ -28,18 +28,23 @@ func main() { app := logic.NewApp(sqlite) if err := app.Migrate(); err != nil { - panic(err) + log.Fatal().Err(err).Msg("failed to migrate DB") + return } ginengine := ginext.NewEngine(conf) router := api.NewRouter(app) - fb := firebase.NewFirebaseApp() + fb, err := firebase.NewFirebase(conf) + if err != nil { + log.Fatal().Err(err).Msg("failed to init firebase") + return + } jobRetry := jobs.NewDeliveryRetryJob(app) - app.Init(conf, ginengine, &fb, []logic.Job{jobRetry}) + app.Init(conf, ginengine, fb, []logic.Job{jobRetry}) router.Init(ginengine) diff --git a/server/common/ginresp/resp.go b/server/common/ginresp/resp.go index 1db84c3..46a82ee 100644 --- a/server/common/ginresp/resp.go +++ b/server/common/ginresp/resp.go @@ -6,7 +6,6 @@ import ( "fmt" "github.com/gin-gonic/gin" "github.com/rs/zerolog/log" - "net/http" "runtime/debug" ) @@ -67,39 +66,60 @@ func Text(sc int, data string) HTTPResponse { } func InternalError(e error) HTTPResponse { - log.Error().Err(e).Msg("[InternalError] " + e.Error()) - - return &jsonHTTPResponse{statusCode: http.StatusInternalServerError, data: apiError{Success: false, Error: int(apierr.INTERNAL_EXCEPTION), Message: e.Error()}} + return createApiError(nil, "InternalError", 500, apierr.INTERNAL_EXCEPTION, 0, e.Error(), e) } -func InternAPIError(status int, errorid apierr.APIError, msg string, e error) HTTPResponse { - log.Error().Int("errorid", int(errorid)).Err(e).Msg("[InternAPIError] " + msg) +func InternAPIError(g *gin.Context, status int, errorid apierr.APIError, msg string, e error) HTTPResponse { + return createApiError(g, "InternAPIError", status, errorid, 0, msg, e) +} + +func SendAPIError(g *gin.Context, status int, errorid apierr.APIError, highlight int, msg string, e error) HTTPResponse { + return createApiError(g, "SendAPIError", status, errorid, highlight, msg, e) +} + +func NotImplemented(g *gin.Context) HTTPResponse { + return createApiError(g, "NotImplemented", 500, apierr.UNDEFINED, 0, "Not Implemented", nil) +} + +func createApiError(g *gin.Context, ident string, status int, errorid apierr.APIError, highlight int, msg string, e error) HTTPResponse { + reqUri := "" + if g != nil && g.Request != nil { + reqUri = g.Request.Method + " :: " + g.Request.RequestURI + } + + log.Error(). + Int("errorid", int(errorid)). + Int("highlight", highlight). + Str("uri", reqUri). + AnErr("err", e). + Stack(). + Msg(fmt.Sprintf("[%s] %s", ident, msg)) if scn.Conf.ReturnRawErrors { - return &jsonHTTPResponse{statusCode: status, data: apiError{Success: false, Error: int(errorid), Message: msg, RawError: fmt.Sprintf("%+v", e), Trace: string(debug.Stack())}} + return &jsonHTTPResponse{ + statusCode: status, + data: apiError{ + Success: false, + Error: int(errorid), + ErrorHighlight: highlight, + Message: msg, + RawError: fmt.Sprintf("%+v", e), + Trace: string(debug.Stack()), + }, + } } else { - return &jsonHTTPResponse{statusCode: status, data: apiError{Success: false, Error: int(errorid), Message: msg}} + return &jsonHTTPResponse{ + statusCode: status, + data: apiError{ + Success: false, + Error: int(errorid), + ErrorHighlight: highlight, + Message: msg, + }, + } } } func CompatAPIError(errid int, msg string) HTTPResponse { - log.Error().Int("errid", errid).Msg("[CompatAPIError] " + msg) - return &jsonHTTPResponse{statusCode: 200, data: compatAPIError{Success: false, ErrorID: errid, Message: msg}} } - -func SendAPIError(status int, errorid apierr.APIError, highlight int, msg string, e error) HTTPResponse { - log.Error().Int("errorid", int(errorid)).Int("highlight", highlight).Err(e).Msg("[SendAPIError] " + msg) - - if scn.Conf.ReturnRawErrors { - return &jsonHTTPResponse{statusCode: status, data: apiError{Success: false, Error: int(errorid), ErrorHighlight: highlight, Message: msg, RawError: fmt.Sprintf("%+v", e), Trace: string(debug.Stack())}} - } else { - return &jsonHTTPResponse{statusCode: status, data: apiError{Success: false, Error: int(errorid), ErrorHighlight: highlight, Message: msg}} - } -} - -func NotImplemented() HTTPResponse { - log.Error().Msg("[NotImplemented]") - - return &jsonHTTPResponse{statusCode: http.StatusInternalServerError, data: apiError{Success: false, Error: -1, ErrorHighlight: 0, Message: "Not Implemented"}} -} diff --git a/server/config.go b/server/config.go index 8107e23..4078f91 100644 --- a/server/config.go +++ b/server/config.go @@ -7,65 +7,95 @@ import ( ) type Config struct { - Namespace string - GinDebug bool - ServerIP string - ServerPort string - DBFile string - RequestTimeout time.Duration - ReturnRawErrors bool + Namespace string + GinDebug bool + ServerIP string + ServerPort string + DBFile string + RequestTimeout time.Duration + ReturnRawErrors bool + FirebaseProjectID string + FirebaseTokenURI string + FirebasePrivKeyID string + FirebaseClientMail string + FirebasePrivateKey string } var Conf Config var configLocHost = Config{ - Namespace: "local-host", - GinDebug: true, - ServerIP: "0.0.0.0", - ServerPort: "8080", - DBFile: ".run-data/db.sqlite3", - RequestTimeout: 16 * time.Second, - ReturnRawErrors: true, + Namespace: "local-host", + GinDebug: true, + ServerIP: "0.0.0.0", + ServerPort: "8080", + DBFile: ".run-data/db.sqlite3", + RequestTimeout: 16 * time.Second, + ReturnRawErrors: true, + FirebaseProjectID: "simplecloudnotifier-ea7ef", + FirebaseTokenURI: "https://oauth2.googleapis.com/token", + FirebasePrivKeyID: "5bfab19fca25034e87c5b3bd1a4334499d2d1f85", + FirebaseClientMail: "firebase-adminsdk-42grv@simplecloudnotifier-ea7ef.iam.gserviceaccount.com", + FirebasePrivateKey: "-----BEGIN PRIVATE KEY-----\nMIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQD2NWOQDcalRdkp\nHtQHABLlu3GMBQBJrGiCxzOZhi/lLwrw2MJEmg1VFz6TVkX2z3SCzXCPOgGriM70\nuWCNLyZQvUng7u6/WH9hlpCg0vJpkw6BvOBt1zYu3gbb5M0SKEOR+lDVccEjAnT4\nexebXdJHJcbaYAcPnBQ9tgP+cozQBnr2EfxYL0bGMgiH9fErJSGMBDFI996uUW9a\nbtfkZ/XpZqYAvyGQMEjknGnQ8t8PHAnsS9dc1PXSWfBvz07ba3fkypWcpTsIYUiZ\nSpwTLV8awihKHJuphoTWb4x6p/ijop05qr1p3fe8gZd9qOGgALe+JT4IBLgNYKrP\nLMSKH3TdAgMBAAECggEAdFcWDOP1kfNHgl7G4efvBg9kwD08vZNybxmiEFGQIEPy\nb4x9f90rn6G0N/r0ZIPzEjvxjDxkvaGP6aQPM6er+0r2tgsxVcmDp6F2Bgin86tB\nl5ygkEa5m7vekdmz7XiJNVmLCNEP6nMmwqOnrArRaj03kcj+jSm7hs2TZZDLaSA5\nf+2q7h0jaU7Nm0ZwCNJqfPJEGdu1J3fR29Ej0rI8N0w/BuYRet1VYDO09lquqOPS\n0WirOOWV6eyqijqRT+RCt0vVzAppS6guhN7J7RS0V9GLJ/13sdvHuJy/WTjBb7gQ\na6QTo8D3yYF+cn3+0BmgP55uW7N6tsYwXIRZcTI3IQKBgQD+tDKMx0puZu+8zTX9\nC2oHSb4Frl2xq17ZpbkfFmOBPWfQbAHNiQTUoQlzCOQM6QejykXFvfsddP7EY2tL\npgLUrBh81wSCAOOo19vYwQB3YKa5ZZucKxh2VxFSefL/+BYHijFb0mWBj5HmqWS6\n7l6IYT3L04aRK9kxj0Cg6L/z6wKBgQD3dh/kQlPemfdxRpZUJ6WEE5x3Bv7WjLop\nnWgE02Pk8+DB+s50GD3nOR276ADCYS6OkBsgfMkwhhKWZigiEoK9DMul5n587jc9\no5AalZN3IbBGAoXk+u3g1GC9bOY3454K6IJyhehDTImEFyfm00qfUL8fMNcdEx8O\nnwxtyRawVwKBgGqsnd9IOGw0wIOajtoERcv3npZSiPs4guk092uFvPcL+MbZ9YdX\ns6Y6K/L57klZ79ExjjdbcijML0ehO/ba+KSJz1e51jF8ndzBS1pkuwVEfY94dsvZ\nYM1vednJKXT7On696h5C6DBzKPAqUf3Yh88mqvMLDHkQnE6daLv7vykxAoGAOPmA\ndDx1NO48E1+OIwgRyqv9PUZmDB3Qit5L4biN6lvgJqlJOV+PeRokZ2wOKLLZVkeF\nh2BTrhFgXDJfESEz6rT0eljsTHVIUK/E8On5Ttd5z1SrYUII3NfpAhP9mWaVr6tC\nxX1hMYWAr+Ho9PM23iFoL5U+IdqSLvqdkPVYfPcCgYB1ANKNYPIJNx/wLxYWNS0r\nI98HwKfv2TxxE/l+2459NMMHY5wlpFl7MNoeK2SdY+ghWPlxC6u5Nxpnk+bZ8TJe\np7U2nY0SQDLCmPgGWs3KBb/zR49X2b7JS3CXXqQSrLxBe2phZg6kE5nB6NPUDc/i\n6WG8tG20rCfgwlXeXl0+Ow==\n-----END PRIVATE KEY-----\n", } var configLocDocker = Config{ - Namespace: "local-docker", - GinDebug: true, - ServerIP: "0.0.0.0", - ServerPort: "80", - DBFile: "/data/scn_docker.sqlite3", - RequestTimeout: 16 * time.Second, - ReturnRawErrors: true, + Namespace: "local-docker", + GinDebug: true, + ServerIP: "0.0.0.0", + ServerPort: "80", + DBFile: "/data/scn_docker.sqlite3", + RequestTimeout: 16 * time.Second, + ReturnRawErrors: true, + FirebaseProjectID: "simplecloudnotifier-ea7ef", + FirebaseTokenURI: "https://oauth2.googleapis.com/token", + FirebasePrivKeyID: "5bfab19fca25034e87c5b3bd1a4334499d2d1f85", + FirebaseClientMail: "firebase-adminsdk-42grv@simplecloudnotifier-ea7ef.iam.gserviceaccount.com", + FirebasePrivateKey: "TODO", } var configDev = Config{ - Namespace: "develop", - GinDebug: true, - ServerIP: "0.0.0.0", - ServerPort: "80", - DBFile: "/data/scn.sqlite3", - RequestTimeout: 16 * time.Second, - ReturnRawErrors: true, + Namespace: "develop", + GinDebug: true, + ServerIP: "0.0.0.0", + ServerPort: "80", + DBFile: "/data/scn.sqlite3", + RequestTimeout: 16 * time.Second, + ReturnRawErrors: true, + FirebaseProjectID: "simplecloudnotifier-ea7ef", + FirebaseTokenURI: "https://oauth2.googleapis.com/token", + FirebasePrivKeyID: "5bfab19fca25034e87c5b3bd1a4334499d2d1f85", + FirebaseClientMail: "firebase-adminsdk-42grv@simplecloudnotifier-ea7ef.iam.gserviceaccount.com", + FirebasePrivateKey: "TODO", } var configStag = Config{ - Namespace: "staging", - GinDebug: true, - ServerIP: "0.0.0.0", - ServerPort: "80", - DBFile: "/data/scn.sqlite3", - RequestTimeout: 16 * time.Second, - ReturnRawErrors: true, + Namespace: "staging", + GinDebug: true, + ServerIP: "0.0.0.0", + ServerPort: "80", + DBFile: "/data/scn.sqlite3", + RequestTimeout: 16 * time.Second, + ReturnRawErrors: true, + FirebaseProjectID: "simplecloudnotifier-ea7ef", + FirebaseTokenURI: "https://oauth2.googleapis.com/token", + FirebasePrivKeyID: "5bfab19fca25034e87c5b3bd1a4334499d2d1f85", + FirebaseClientMail: "firebase-adminsdk-42grv@simplecloudnotifier-ea7ef.iam.gserviceaccount.com", + FirebasePrivateKey: "TODO", } var configProd = Config{ - Namespace: "production", - GinDebug: false, - ServerIP: "0.0.0.0", - ServerPort: "80", - DBFile: "/data/scn.sqlite3", - RequestTimeout: 16 * time.Second, - ReturnRawErrors: false, + Namespace: "production", + GinDebug: false, + ServerIP: "0.0.0.0", + ServerPort: "80", + DBFile: "/data/scn.sqlite3", + RequestTimeout: 16 * time.Second, + ReturnRawErrors: false, + FirebaseProjectID: "simplecloudnotifier-ea7ef", + FirebaseTokenURI: "https://oauth2.googleapis.com/token", + FirebasePrivKeyID: "5bfab19fca25034e87c5b3bd1a4334499d2d1f85", + FirebaseClientMail: "firebase-adminsdk-42grv@simplecloudnotifier-ea7ef.iam.gserviceaccount.com", + FirebasePrivateKey: "TODO", } var allConfig = []Config{ diff --git a/server/firebase/FBOAuth.go b/server/firebase/FBOAuth.go new file mode 100644 index 0000000..cfb365c --- /dev/null +++ b/server/firebase/FBOAuth.go @@ -0,0 +1,179 @@ +package firebase + +import ( + "context" + "crypto" + "crypto/rand" + "crypto/rsa" + "crypto/sha256" + "crypto/x509" + "encoding/base64" + "encoding/json" + "encoding/pem" + "errors" + "fmt" + "github.com/gin-gonic/gin" + "gogs.mikescher.com/BlackForestBytes/goext/langext" + "gogs.mikescher.com/BlackForestBytes/goext/timeext" + "io" + "net/http" + "net/url" + "strings" + "time" +) + +type FBOAuth2 struct { + client *http.Client + + scopes []string + tokenURL string + privateKeyID string + clientMail string + + currToken *string + tokenExpiry *time.Time + privateKey *rsa.PrivateKey +} + +func NewAuth(tokenURL string, privKeyID string, cmail string, pemstr string) (*FBOAuth2, error) { + + pkey, err := decodePemKey(pemstr) + if err != nil { + return nil, err + } + + return &FBOAuth2{ + client: &http.Client{Timeout: 3 * time.Second}, + tokenURL: tokenURL, + privateKey: pkey, + privateKeyID: privKeyID, + clientMail: cmail, + scopes: []string{ + "https://www.googleapis.com/auth/cloud-platform", + "https://www.googleapis.com/auth/datastore", + "https://www.googleapis.com/auth/devstorage.full_control", + "https://www.googleapis.com/auth/firebase", + "https://www.googleapis.com/auth/identitytoolkit", + "https://www.googleapis.com/auth/userinfo.email", + }, + }, nil +} + +func decodePemKey(pemstr string) (*rsa.PrivateKey, error) { + var raw []byte + + block, _ := pem.Decode([]byte(pemstr)) + + if block != nil { + raw = block.Bytes + } else { + raw = []byte(pemstr) + } + + pkey8, err1 := x509.ParsePKCS8PrivateKey(raw) + if err1 == nil { + privkey, ok := pkey8.(*rsa.PrivateKey) + if !ok { + return nil, errors.New("private key is invalid") + } + return privkey, nil + } + + pkey1, err2 := x509.ParsePKCS1PrivateKey(raw) + if err2 == nil { + return pkey1, nil + } + + return nil, errors.New(fmt.Sprintf("failed to parse private-key: [ %v | %v ]", err1, err2)) +} + +func (a *FBOAuth2) Token(ctx context.Context) (string, error) { + if a.currToken == nil || a.tokenExpiry == nil || a.tokenExpiry.Before(time.Now()) { + err := a.Refresh(ctx) + if err != nil { + return "", err + } + } + + return *a.currToken, nil +} + +func (a *FBOAuth2) Refresh(ctx context.Context) error { + + assertion, err := a.encodeAssertion(a.privateKey) + if err != nil { + return err + } + + body := url.Values{ + "assertion": []string{assertion}, + "grant_type": []string{"urn:ietf:params:oauth:grant-type:jwt-bearer"}, + }.Encode() + + req, err := http.NewRequestWithContext(ctx, "POST", a.tokenURL, strings.NewReader(body)) + if err != nil { + return err + } + + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + + reqNow := time.Now() + + resp, err := a.client.Do(req) + if err != nil { + return err + } + defer func() { _ = resp.Body.Close() }() + + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + if bstr, err := io.ReadAll(resp.Body); err == nil { + return errors.New(fmt.Sprintf("Auth-Request returned %d: %s", resp.StatusCode, string(bstr))) + } else { + return errors.New(fmt.Sprintf("Auth-Request returned %d", resp.StatusCode)) + } + } + + respBodyBin, err := io.ReadAll(resp.Body) + if err != nil { + return err + } + + var respBody struct { + AccessToken string `json:"access_token"` + TokenType string `json:"token_type"` + ExpiresIn int `json:"expires_in"` + } + if err := json.Unmarshal(respBodyBin, &respBody); err != nil { + return err + } + + a.currToken = langext.Ptr(respBody.AccessToken) + a.tokenExpiry = langext.Ptr(reqNow.Add(timeext.FromSeconds(respBody.ExpiresIn))) + + return nil +} + +func (a *FBOAuth2) encodeAssertion(key *rsa.PrivateKey) (string, error) { + headBin, err := json.Marshal(gin.H{"alg": "RS256", "typ": "JWT", "kid": a.privateKeyID}) + if err != nil { + return "", err + } + head := base64.RawURLEncoding.EncodeToString(headBin) + + now := time.Now().Add(-10 * time.Second) // jwt hack against unsynced clocks + + claimBin, err := json.Marshal(gin.H{"iss": a.clientMail, "scope": strings.Join(a.scopes, " "), "aud": a.tokenURL, "exp": now.Add(time.Hour).Unix(), "iat": now.Unix()}) + if err != nil { + return "", err + } + claim := base64.RawURLEncoding.EncodeToString(claimBin) + + checksum := sha256.New() + checksum.Write([]byte(head + "." + claim)) + sig, err := rsa.SignPKCS1v15(rand.Reader, key, crypto.SHA256, checksum.Sum(nil)) + if err != nil { + return "", err + } + + return head + "." + claim + "." + base64.RawURLEncoding.EncodeToString(sig), nil +} diff --git a/server/firebase/firebase.go b/server/firebase/firebase.go index b8a5362..bb46d0c 100644 --- a/server/firebase/firebase.go +++ b/server/firebase/firebase.go @@ -1,40 +1,44 @@ package firebase import ( + scn "blackforestbytes.com/simplecloudnotifier" "blackforestbytes.com/simplecloudnotifier/models" + "bytes" "context" _ "embed" - fb "firebase.google.com/go" - "firebase.google.com/go/messaging" + "encoding/json" + "errors" + "fmt" + "github.com/gin-gonic/gin" "github.com/rs/zerolog/log" "gogs.mikescher.com/BlackForestBytes/goext/langext" - "google.golang.org/api/option" + "io" + "net/http" "strconv" + "time" ) -//go:embed scnserviceaccountkey.json -var scnserviceaccountkey []byte +// https://firebase.google.com/docs/cloud-messaging/send-message#rest +// https://firebase.google.com/docs/cloud-messaging/auth-server -type App struct { - app *fb.App - messaging *messaging.Client +type FBConnector struct { + fbProject string + client http.Client + auth *FBOAuth2 } -func NewFirebaseApp() App { - opt := option.WithCredentialsJSON(scnserviceaccountkey) - app, err := fb.NewApp(context.Background(), nil, opt) +func NewFirebase(conf scn.Config) (*FBConnector, error) { + + fbauth, err := NewAuth(conf.FirebaseTokenURI, conf.FirebaseProjectID, conf.FirebaseClientMail, conf.FirebasePrivateKey) if err != nil { - log.Error().Err(err).Msg("failed to init firebase app") - } - msg, err := app.Messaging(context.Background()) - if err != nil { - log.Error().Err(err).Msg("failed to init messaging client") - } - log.Info().Msg("Initialized Firebase") - return App{ - app: app, - messaging: msg, + return nil, err } + + return &FBConnector{ + fbProject: conf.FirebaseProjectID, + client: http.Client{Timeout: 5 * time.Second}, + auth: fbauth, + }, nil } type Notification struct { @@ -46,10 +50,12 @@ type Notification struct { Priority int } -func (fb App) SendNotification(ctx context.Context, client models.Client, msg models.Message) (string, error) { +func (fb FBConnector) SendNotification(ctx context.Context, client models.Client, msg models.Message) (string, error) { - n := messaging.Message{ - Data: map[string]string{ + uri := "https://fcm.googleapis.com/v1/projects/" + fb.fbProject + "/messages:send" + + jsonBody := gin.H{ + "data": gin.H{ "scn_msg_id": strconv.FormatInt(msg.SCNMessageID, 10), "usr_msg_id": langext.Coalesce(msg.UserMessageID, ""), "client_id": strconv.FormatInt(client.ClientID, 10), @@ -59,29 +65,64 @@ func (fb App) SendNotification(ctx context.Context, client models.Client, msg mo "title": msg.Title, "body": langext.Coalesce(msg.TrimmedContent(), ""), }, - Notification: &messaging.Notification{ - Title: msg.Title, - Body: msg.ShortContent(), + "token": *client.FCMToken, + "android": gin.H{ + "priority": "high", }, - Android: nil, - APNS: nil, - Webpush: nil, - FCMOptions: nil, - Token: *client.FCMToken, - Topic: "", - Condition: "", + "apns": gin.H{}, } - if client.Type == models.ClientTypeIOS { - n.APNS = nil - } else if client.Type == models.ClientTypeAndroid { - n.Notification = nil - n.Android = &messaging.AndroidConfig{Priority: "high"} + jsonBody["notification"] = gin.H{ + "title": msg.Title, + "body": msg.ShortContent(), + } } - res, err := fb.messaging.Send(ctx, &n) + bytesBody, err := json.Marshal(gin.H{"message": jsonBody}) if err != nil { - log.Error().Err(err).Msg("failed to send push") + return "", err } - return res, err + + request, err := http.NewRequestWithContext(ctx, "POST", uri, bytes.NewBuffer(bytesBody)) + if err != nil { + return "", err + } + + tok, err := fb.auth.Token(ctx) + if err != nil { + log.Err(err).Msg("Refreshing FB token failed") + return "", err + } + + request.Header.Set("Authorization", "Bearer "+tok) + request.Header.Set("Content-Type", "application/json") + request.Header.Set("Accept", "application/json") + + response, err := fb.client.Do(request) + if err != nil { + return "", err + } + defer func() { _ = response.Body.Close() }() + + if response.StatusCode < 200 || response.StatusCode >= 300 { + if bstr, err := io.ReadAll(response.Body); err == nil { + return "", errors.New(fmt.Sprintf("FCM-Request returned %d: %s", response.StatusCode, string(bstr))) + } else { + return "", errors.New(fmt.Sprintf("FCM-Request returned %d", response.StatusCode)) + } + } + + respBodyBin, err := io.ReadAll(response.Body) + if err != nil { + return "", err + } + + var respBody struct { + Name string `json:"name"` + } + if err := json.Unmarshal(respBodyBin, &respBody); err != nil { + return "", err + } + + return respBody.Name, nil } diff --git a/server/go.mod b/server/go.mod index 753d7b2..e500e88 100644 --- a/server/go.mod +++ b/server/go.mod @@ -3,24 +3,15 @@ module blackforestbytes.com/simplecloudnotifier go 1.19 require ( - firebase.google.com/go v3.13.0+incompatible github.com/blockloop/scan v1.3.0 github.com/gin-gonic/gin v1.8.1 github.com/mattn/go-sqlite3 v1.14.16 github.com/rs/zerolog v1.28.0 github.com/swaggo/swag v1.8.7 gogs.mikescher.com/BlackForestBytes/goext v0.0.21 - google.golang.org/api v0.103.0 ) require ( - cloud.google.com/go v0.105.0 // indirect - cloud.google.com/go/compute v1.12.1 // indirect - cloud.google.com/go/compute/metadata v0.2.1 // indirect - cloud.google.com/go/firestore v1.8.0 // indirect - cloud.google.com/go/iam v0.7.0 // indirect - cloud.google.com/go/longrunning v0.3.0 // indirect - cloud.google.com/go/storage v1.28.0 // indirect github.com/KyleBanks/depth v1.2.1 // indirect github.com/PuerkitoBio/purell v1.1.1 // indirect github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect @@ -33,12 +24,7 @@ require ( github.com/go-playground/universal-translator v0.18.0 // indirect github.com/go-playground/validator/v10 v10.10.0 // indirect github.com/goccy/go-json v0.9.7 // indirect - github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e // indirect - github.com/golang/protobuf v1.5.2 // indirect github.com/google/go-cmp v0.5.9 // indirect - github.com/google/uuid v1.3.0 // indirect - github.com/googleapis/enterprise-certificate-proxy v0.2.0 // indirect - github.com/googleapis/gax-go/v2 v2.7.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/leodido/go-urn v1.2.1 // indirect @@ -49,19 +35,11 @@ require ( github.com/modern-go/reflect2 v1.0.2 // indirect github.com/pelletier/go-toml/v2 v2.0.1 // indirect github.com/ugorji/go/codec v1.2.7 // indirect - go.opencensus.io v0.24.0 // indirect golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97 // indirect golang.org/x/net v0.2.0 // indirect - golang.org/x/oauth2 v0.2.0 // indirect - golang.org/x/sync v0.1.0 // indirect golang.org/x/sys v0.2.0 // indirect golang.org/x/text v0.4.0 // indirect - golang.org/x/time v0.0.0-20220609170525-579cf78fd858 // indirect golang.org/x/tools v0.1.12 // indirect - golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect - google.golang.org/appengine v1.6.7 // indirect - google.golang.org/genproto v0.0.0-20221118155620-16455021b5e6 // indirect - google.golang.org/grpc v1.51.0 // indirect google.golang.org/protobuf v1.28.1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect ) diff --git a/server/go.sum b/server/go.sum index 210b146..384c598 100644 --- a/server/go.sum +++ b/server/go.sum @@ -1,25 +1,3 @@ -cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.105.0 h1:DNtEKRBAAzeS4KyIory52wWHuClNaXJ5x1F7xa4q+5Y= -cloud.google.com/go v0.105.0/go.mod h1:PrLgOJNe5nfE9UMxKxgXj4mD3voiP+YQ6gdt6KMFOKM= -cloud.google.com/go/compute v1.12.1 h1:gKVJMEyqV5c/UnpzjjQbo3Rjvvqpr9B1DFSbJC4OXr0= -cloud.google.com/go/compute v1.12.1/go.mod h1:e8yNOBcBONZU1vJKCvCoDw/4JQsA0dpM4x/6PIIOocU= -cloud.google.com/go/compute/metadata v0.2.1 h1:efOwf5ymceDhK6PKMnnrTHP4pppY5L22mle96M1yP48= -cloud.google.com/go/compute/metadata v0.2.1/go.mod h1:jgHgmJd2RKBGzXqF5LR2EZMGxBkeanZ9wwa75XHJgOM= -cloud.google.com/go/firestore v1.8.0 h1:HokMB9Io0hAyYzlGFeFVMgE3iaPXNvaIsDx5JzblGLI= -cloud.google.com/go/firestore v1.8.0/go.mod h1:r3KB8cAdRIe8znzoPWLw8S6gpDVd9treohhn8b09424= -cloud.google.com/go/iam v0.6.0 h1:nsqQC88kT5Iwlm4MeNGTpfMWddp6NB/UOLFTH6m1QfQ= -cloud.google.com/go/iam v0.6.0/go.mod h1:+1AH33ueBne5MzYccyMHtEKqLE4/kJOibtffMHDMFMc= -cloud.google.com/go/iam v0.7.0 h1:k4MuwOsS7zGJJ+QfZ5vBK8SgHBAvYN/23BWsiihJ1vs= -cloud.google.com/go/iam v0.7.0/go.mod h1:H5Br8wRaDGNc8XP3keLc4unfUUZeyH3Sfl9XpQEYOeg= -cloud.google.com/go/longrunning v0.1.1 h1:y50CXG4j0+qvEukslYFBCrzaXX0qpFbBzc3PchSu/LE= -cloud.google.com/go/longrunning v0.1.1/go.mod h1:UUFxuDWkv22EuY93jjmDMFT5GPQKeFVJBIF6QlTqdsE= -cloud.google.com/go/longrunning v0.3.0 h1:NjljC+FYPV3uh5/OwWT6pVU+doBqMg2x/rZlE+CamDs= -cloud.google.com/go/longrunning v0.3.0/go.mod h1:qth9Y41RRSUE69rDcOn6DdK3HfQfsUI0YSmW3iIlLJc= -cloud.google.com/go/storage v1.28.0 h1:DLrIZ6xkeZX6K70fU/boWx5INJumt6f+nwwWSHXzzGY= -cloud.google.com/go/storage v1.28.0/go.mod h1:qlgZML35PXA3zoEnIkiPLY4/TOkUleufRlu6qmcf7sI= -firebase.google.com/go v3.13.0+incompatible h1:3TdYC3DDi6aHn20qoRkxwGqNgdjtblwVAyRLQwGn/+4= -firebase.google.com/go v3.13.0+incompatible/go.mod h1:xlah6XbEyW6tbfSklcfe5FHJIwjt8toICdV5Wh9ptHs= -github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI= @@ -28,18 +6,11 @@ github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= github.com/blockloop/scan v1.3.0 h1:p8xnajpGA3d/V6o23IBFdQ764+JnNJ+PQj+OwT+rkdg= github.com/blockloop/scan v1.3.0/go.mod h1:qd+3w68+o7m5Xhj9X5SlJH2rbFyK8w0WT47Rkuer010= -github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= -github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/coreos/go-systemd/v22 v22.3.3-0.20220203105225-a9a7ef127534/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= -github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= github.com/gin-gonic/gin v1.8.1 h1:4+fr/el88TOO3ewCmQr8cx/CtZ/umlIRIs5M4NTNjf8= @@ -65,41 +36,11 @@ github.com/go-playground/validator/v10 v10.10.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXS github.com/goccy/go-json v0.9.7 h1:IcB+Aqpx/iMHu5Yooh7jEzJk1JZ7Pjtmys2ukPr7EeM= github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= -github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e h1:1r7pUrabqp18hOBcwBwiTsbnFeTZHV9eER/QT5JVZxY= -github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= -github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= -github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= -github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= -github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= -github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= -github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= -github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= -github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= -github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/martian/v3 v3.2.1 h1:d8MncMlErDFTwQGBK1xhv026j9kqhvw1Qv9IbWT1VLQ= -github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= -github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/googleapis/enterprise-certificate-proxy v0.2.0 h1:y8Yozv7SZtlU//QXbezB6QkpuE6jMD2/gfzk4AftXjs= -github.com/googleapis/enterprise-certificate-proxy v0.2.0/go.mod h1:8C0jb7/mgJe/9KK8Lm7X9ctZC2t60YyIpYEI16jx0Qg= -github.com/googleapis/gax-go/v2 v2.7.0 h1:IcsPKeInNvYi7eqSaDjiZqDDKu5rsmunY0Y1YupQSSQ= -github.com/googleapis/gax-go/v2 v2.7.0/go.mod h1:TEop28CZZQ2y+c0VxMUmu1lV+fQx57QpBWsYpwqHJx8= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= @@ -136,7 +77,6 @@ github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsK github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8= github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= @@ -144,64 +84,25 @@ github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/rs/zerolog v1.28.0 h1:MirSo27VyNi7RJYP3078AA1+Cyzd2GB66qy3aUHvsWY= github.com/rs/zerolog v1.28.0/go.mod h1:NILgTygv/Uej1ra5XxGf82ZFSLk58MFGAUS2o6usyD0= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= -github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/swaggo/swag v1.8.7 h1:2K9ivTD3teEO+2fXV6zrZKDqk5IuU2aJtBDo8U7omWU= github.com/swaggo/swag v1.8.7/go.mod h1:ezQVUUhly8dludpVk+/PuwJWvLLanB13ygV5Pr9enSk= github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M= github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0= github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY= -go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= -go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= -gogs.mikescher.com/BlackForestBytes/goext v0.0.17 h1:jsfbvII7aa0SH9qY0fnXBdtNnQe1YY3DgXDThEwLICc= -gogs.mikescher.com/BlackForestBytes/goext v0.0.17/go.mod h1:TMBOjo3FRFh/GiTT0z3nwLmgcFJB87oSF2VMs4XUCTQ= -gogs.mikescher.com/BlackForestBytes/goext v0.0.18 h1:fprrLoAPGdI4ObveHR1DjiP9WhlTJppWtjqMA6ZkyS8= -gogs.mikescher.com/BlackForestBytes/goext v0.0.18/go.mod h1:TMBOjo3FRFh/GiTT0z3nwLmgcFJB87oSF2VMs4XUCTQ= -gogs.mikescher.com/BlackForestBytes/goext v0.0.19 h1:IvCHlIHDviHQXntZFTNdV7qNq5yQnSEMxF8LA0Tf3IY= -gogs.mikescher.com/BlackForestBytes/goext v0.0.19/go.mod h1:TMBOjo3FRFh/GiTT0z3nwLmgcFJB87oSF2VMs4XUCTQ= -gogs.mikescher.com/BlackForestBytes/goext v0.0.20 h1:HxJ0iZ838TQnp/a+/DNajdZjZkV43OsK4VbHarOiHTs= -gogs.mikescher.com/BlackForestBytes/goext v0.0.20/go.mod h1:TMBOjo3FRFh/GiTT0z3nwLmgcFJB87oSF2VMs4XUCTQ= gogs.mikescher.com/BlackForestBytes/goext v0.0.21 h1:OibsssmorZsTdFYRiQFlkXtjUYweQg9SBkWO40ONe0Y= gogs.mikescher.com/BlackForestBytes/goext v0.0.21/go.mod h1:TMBOjo3FRFh/GiTT0z3nwLmgcFJB87oSF2VMs4XUCTQ= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97 h1:/UOmuWzQfxxo9UtlXMwuQU8CMgg1eZXqTRwkSQJWKOI= golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= -golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 h1:6zppjxzCulZykYSLyVDYbneBfbaBIQPYMevg0bEwv2s= -golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= -golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM= golang.org/x/net v0.2.0 h1:sZfSu1wtKLGlWI4ZZayP0ck9Y73K1ynO6gqzTdBVdPU= golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= -golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.2.0 h1:GtQkldQ9m7yvzCL1V+LrYow3Khe0eJH0w7RbX/VbaIU= -golang.org/x/oauth2 v0.2.0/go.mod h1:Cwn6afJ8jrQwYMxQDTpISoXmXW9I6qF6vDeuuoX3Ibs= -golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= -golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -211,58 +112,16 @@ golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.2.0 h1:ljd4t30dBnAvMZaQCevtY0xLLD0A+bRZXbgLMLU1F/A= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.4.0 h1:BrVqGRd7+k1DiOgtnFvAkoQEWQvBc25ouMJM6429SFg= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/time v0.0.0-20220609170525-579cf78fd858 h1:Dpdu/EMxGMFgq0CeYMh4fazTD2vtlZRYE7wyynxJb9U= -golang.org/x/time v0.0.0-20220609170525-579cf78fd858/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= -golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.1.12 h1:VveCTK38A2rkS8ZqFY25HIDFscX5X9OoEhJd3quQmXU= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 h1:H2TDz8ibqkAF6YGhCdN3jS9O0/s90v0rJh3X/OLHEUk= -golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= -google.golang.org/api v0.103.0 h1:9yuVqlu2JCvcLg9p8S3fcFLZij8EPSyvODIY1rkMizQ= -google.golang.org/api v0.103.0/go.mod h1:hGtW6nK1AC+d9si/UBhw8Xli+QMOf6xyNAyJw4qU9w0= -google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= -google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= -google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= -google.golang.org/genproto v0.0.0-20221027153422-115e99e71e1c h1:QgY/XxIAIeccR+Ca/rDdKubLIU9rcJ3xfy1DC/Wd2Oo= -google.golang.org/genproto v0.0.0-20221027153422-115e99e71e1c/go.mod h1:CGI5F/G+E5bKwmfYo09AXuVN4dD894kIKUFmVbP2/Fo= -google.golang.org/genproto v0.0.0-20221118155620-16455021b5e6 h1:a2S6M0+660BgMNl++4JPlcAO/CjkqYItDEZwkoDQK7c= -google.golang.org/genproto v0.0.0-20221118155620-16455021b5e6/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg= -google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= -google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= -google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= -google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= -google.golang.org/grpc v1.50.1 h1:DS/BukOZWp8s6p4Dt/tOaJaTQyPyOoCcrjroHuCeLzY= -google.golang.org/grpc v1.50.1/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI= -google.golang.org/grpc v1.51.0 h1:E1eGv1FTqoLIdnBCZufiSHgKjlqG6fKFf6pPWtMTh8U= -google.golang.org/grpc v1.51.0/go.mod h1:wgNDFcnuBGmxLKI/qn4T+m5BtEBYXJPvibbUPsAIPww= -google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= -google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= -google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= -google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= -google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= -google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= -google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w= google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -276,8 +135,5 @@ gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/server/jobs/DeliveryRetryJob.go b/server/jobs/DeliveryRetryJob.go index 5aea16d..4f61362 100644 --- a/server/jobs/DeliveryRetryJob.go +++ b/server/jobs/DeliveryRetryJob.go @@ -54,7 +54,7 @@ func (j *DeliveryRetryJob) mainLoop() { func (j *DeliveryRetryJob) run() bool { defer func() { if rec := recover(); rec != nil { - log.Error().Interface("rec", rec).Msg("Recovered panic in DeliveryRetryJob") + log.Error().Interface("recover", rec).Msg("Recovered panic in DeliveryRetryJob") } }() @@ -101,7 +101,7 @@ func (j *DeliveryRetryJob) redeliver(ctx *logic.SimpleContext, delivery models.D } fcmDelivID, err := j.app.DeliverMessage(ctx, client, msg) - if err != nil { + if err == nil { err = j.app.Database.SetDeliverySuccess(ctx, delivery, *fcmDelivID) if err != nil { log.Err(err).Int64("SCNMessageID", delivery.SCNMessageID).Int64("DeliveryID", delivery.DeliveryID).Msg("Failed to update delivery") @@ -115,6 +115,7 @@ func (j *DeliveryRetryJob) redeliver(ctx *logic.SimpleContext, delivery models.D ctx.RollbackTransaction() return } + log.Warn().Int64("SCNMessageID", delivery.SCNMessageID).Int64("DeliveryID", delivery.DeliveryID).Msg("Delivery failed after retries (set to FAILURE)") } else { err = j.app.Database.SetDeliveryRetry(ctx, delivery) if err != nil { diff --git a/server/logic/appcontext.go b/server/logic/appcontext.go index 1ccd06a..bc58e36 100644 --- a/server/logic/appcontext.go +++ b/server/logic/appcontext.go @@ -7,6 +7,7 @@ import ( "context" "database/sql" "errors" + "github.com/gin-gonic/gin" "github.com/rs/zerolog/log" "time" ) @@ -17,15 +18,17 @@ type AppContext struct { cancelled bool transaction *sql.Tx permissions PermissionSet + ginContext *gin.Context } -func CreateAppContext(innerCtx context.Context, cancelFn context.CancelFunc) *AppContext { +func CreateAppContext(g *gin.Context, innerCtx context.Context, cancelFn context.CancelFunc) *AppContext { return &AppContext{ inner: innerCtx, cancelFunc: cancelFn, cancelled: false, transaction: nil, permissions: NewEmptyPermissions(), + ginContext: g, } } @@ -48,16 +51,24 @@ func (ac *AppContext) Value(key any) any { func (ac *AppContext) Cancel() { ac.cancelled = true if ac.transaction != nil { - log.Error().Msg("Rollback transaction") + log.Error().Str("uri", ac.RequestURI()).Msg("Rollback transaction (ctx-cancel)") err := ac.transaction.Rollback() if err != nil { - panic("failed to rollback transaction: " + err.Error()) + log.Err(err).Stack().Msg("Failed to rollback transaction") } ac.transaction = nil } ac.cancelFunc() } +func (ac *AppContext) RequestURI() string { + if ac.ginContext != nil && ac.ginContext.Request != nil { + return ac.ginContext.Request.Method + " :: " + ac.ginContext.Request.RequestURI + } else { + return "" + } +} + func (ac *AppContext) FinishSuccess(res ginresp.HTTPResponse) ginresp.HTTPResponse { if ac.cancelled { panic("Cannot finish a cancelled request") @@ -65,7 +76,7 @@ func (ac *AppContext) FinishSuccess(res ginresp.HTTPResponse) ginresp.HTTPRespon if ac.transaction != nil { err := ac.transaction.Commit() if err != nil { - return ginresp.InternAPIError(500, apierr.COMMIT_FAILED, "Failed to comit changes to DB", err) + return ginresp.InternAPIError(ac.ginContext, 500, apierr.COMMIT_FAILED, "Failed to comit changes to DB", err) } ac.transaction = nil } diff --git a/server/logic/application.go b/server/logic/application.go index 3a30540..8c09aa1 100644 --- a/server/logic/application.go +++ b/server/logic/application.go @@ -27,7 +27,7 @@ type Application struct { Config scn.Config Gin *gin.Engine Database *db.Database - Firebase *firebase.App + Firebase *firebase.FBConnector DefaultChannel string Jobs []Job } @@ -39,7 +39,7 @@ func NewApp(db *db.Database) *Application { } } -func (app *Application) Init(cfg scn.Config, g *gin.Engine, fb *firebase.App, jobs []Job) { +func (app *Application) Init(cfg scn.Config, g *gin.Engine, fb *firebase.FBConnector, jobs []Job) { app.Config = cfg app.Gin = g app.Firebase = fb @@ -122,37 +122,37 @@ func (app *Application) StartRequest(g *gin.Context, uri any, query any, body an if uri != nil { if err := g.ShouldBindUri(uri); err != nil { - return nil, langext.Ptr(ginresp.InternAPIError(400, apierr.BINDFAIL_URI_PARAM, "Failed to read uri", err)) + return nil, langext.Ptr(ginresp.InternAPIError(g, 400, apierr.BINDFAIL_URI_PARAM, "Failed to read uri", err)) } } if query != nil { if err := g.ShouldBindQuery(query); err != nil { - return nil, langext.Ptr(ginresp.InternAPIError(400, apierr.BINDFAIL_QUERY_PARAM, "Failed to read query", err)) + return nil, langext.Ptr(ginresp.InternAPIError(g, 400, apierr.BINDFAIL_QUERY_PARAM, "Failed to read query", err)) } } - if body != nil { + if body != nil && g.Request.Header.Get("Content-Type") == "application/javascript" { if err := g.ShouldBindJSON(body); err != nil { - return nil, langext.Ptr(ginresp.InternAPIError(400, apierr.BINDFAIL_BODY_PARAM, "Failed to read body", err)) + return nil, langext.Ptr(ginresp.InternAPIError(g, 400, apierr.BINDFAIL_BODY_PARAM, "Failed to read body", err)) } } - if form != nil { + if form != nil && g.Request.Header.Get("Content-Type") == "multipart/form-data" { if err := g.ShouldBindWith(form, binding.Form); err != nil { - return nil, langext.Ptr(ginresp.InternAPIError(400, apierr.BINDFAIL_BODY_PARAM, "Failed to read multipart-form", err)) + return nil, langext.Ptr(ginresp.InternAPIError(g, 400, apierr.BINDFAIL_BODY_PARAM, "Failed to read multipart-form", err)) } } ictx, cancel := context.WithTimeout(context.Background(), app.Config.RequestTimeout) - actx := CreateAppContext(ictx, cancel) + actx := CreateAppContext(g, ictx, cancel) authheader := g.GetHeader("Authorization") perm, err := app.getPermissions(actx, authheader) if err != nil { cancel() - return nil, langext.Ptr(ginresp.InternAPIError(400, apierr.PERM_QUERY_FAIL, "Failed to determine permissions", err)) + return nil, langext.Ptr(ginresp.InternAPIError(g, 400, apierr.PERM_QUERY_FAIL, "Failed to determine permissions", err)) } actx.permissions = perm @@ -245,6 +245,7 @@ func (app *Application) DeliverMessage(ctx context.Context, client models.Client if client.FCMToken != nil { fcmDelivID, err := app.Firebase.SendNotification(ctx, client, msg) if err != nil { + log.Warn().Int64("SCNMessageID", msg.SCNMessageID).Int64("ClientID", client.ClientID).Err(err).Msg("FCM Delivery failed") return nil, err } return langext.Ptr(fcmDelivID), nil diff --git a/server/logic/permissions.go b/server/logic/permissions.go index 190c536..c524da9 100644 --- a/server/logic/permissions.go +++ b/server/logic/permissions.go @@ -28,8 +28,6 @@ func NewEmptyPermissions() PermissionSet { } } -var respoNotAuthorized = ginresp.InternAPIError(401, apierr.USER_AUTH_FAILED, "You are not authorized for this action", nil) - func (ac *AppContext) CheckPermissionUserRead(userid int64) *ginresp.HTTPResponse { p := ac.permissions if p.UserID != nil && *p.UserID == userid && p.KeyType == PermKeyTypeUserRead { @@ -39,7 +37,7 @@ func (ac *AppContext) CheckPermissionUserRead(userid int64) *ginresp.HTTPRespons return nil } - return langext.Ptr(respoNotAuthorized) + return langext.Ptr(ginresp.InternAPIError(ac.ginContext, 401, apierr.USER_AUTH_FAILED, "You are not authorized for this action", nil)) } func (ac *AppContext) CheckPermissionRead() *ginresp.HTTPResponse { @@ -51,7 +49,7 @@ func (ac *AppContext) CheckPermissionRead() *ginresp.HTTPResponse { return nil } - return langext.Ptr(respoNotAuthorized) + return langext.Ptr(ginresp.InternAPIError(ac.ginContext, 401, apierr.USER_AUTH_FAILED, "You are not authorized for this action", nil)) } func (ac *AppContext) CheckPermissionUserAdmin(userid int64) *ginresp.HTTPResponse { @@ -60,13 +58,13 @@ func (ac *AppContext) CheckPermissionUserAdmin(userid int64) *ginresp.HTTPRespon return nil } - return langext.Ptr(respoNotAuthorized) + return langext.Ptr(ginresp.InternAPIError(ac.ginContext, 401, apierr.USER_AUTH_FAILED, "You are not authorized for this action", nil)) } func (ac *AppContext) CheckPermissionAny() *ginresp.HTTPResponse { p := ac.permissions if p.KeyType == PermKeyTypeNone { - return langext.Ptr(respoNotAuthorized) + return langext.Ptr(ginresp.InternAPIError(ac.ginContext, 401, apierr.USER_AUTH_FAILED, "You are not authorized for this action", nil)) } return nil diff --git a/server/logic/simplecontext.go b/server/logic/simplecontext.go index ac3a112..1d8ffc7 100644 --- a/server/logic/simplecontext.go +++ b/server/logic/simplecontext.go @@ -87,6 +87,7 @@ func (sc *SimpleContext) RollbackTransaction() { } err := sc.transaction.Rollback() if err != nil { + log.Err(err).Stack().Msg("Failed to rollback transaction") panic(err) } sc.transaction = nil