firebase via REST (less dependencies)

This commit is contained in:
Mike Schwörer 2022-11-20 17:19:11 +01:00
parent 08a93551e7
commit d30e2cefc0
Signed by: Mikescher
GPG Key ID: D3C7172E0A70F8CF
15 changed files with 519 additions and 396 deletions

View File

@ -4,6 +4,8 @@ type APIError int
//goland:noinspection GoSnakeCaseUsage
const (
UNDEFINED APIError = -1
NO_ERROR APIError = 0000
MISSING_UID APIError = 1101

View File

@ -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
}

View File

@ -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)
}
}

View File

@ -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)

View File

@ -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"}}
}

View File

@ -14,6 +14,11 @@ type Config struct {
DBFile string
RequestTimeout time.Duration
ReturnRawErrors bool
FirebaseProjectID string
FirebaseTokenURI string
FirebasePrivKeyID string
FirebaseClientMail string
FirebasePrivateKey string
}
var Conf Config
@ -26,6 +31,11 @@ var configLocHost = Config{
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{
@ -36,6 +46,11 @@ var configLocDocker = Config{
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{
@ -46,6 +61,11 @@ var configDev = Config{
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{
@ -56,6 +76,11 @@ var configStag = Config{
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{
@ -66,6 +91,11 @@ var configProd = Config{
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{

179
server/firebase/FBOAuth.go Normal file
View File

@ -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
}

View File

@ -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
}

View File

@ -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
)

View File

@ -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=

View File

@ -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 <max> retries (set to FAILURE)")
} else {
err = j.app.Database.SetDeliveryRetry(ctx, delivery)
if err != nil {

View File

@ -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
}

View File

@ -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

View File

@ -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

View File

@ -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