tags/grouping for API

This commit is contained in:
Mike Schwörer 2022-11-23 19:32:23 +01:00
parent 03c35d6446
commit 1bc847cdc9
Signed by: Mikescher
GPG Key ID: D3C7172E0A70F8CF
16 changed files with 1789 additions and 1442 deletions

View File

@ -2,7 +2,8 @@
//TODO //TODO
- remove fcm/goog_api keys from repo (and invalidate them !!) - Return client on register
- Register with noclient=true
- migration script for existing data - migration script for existing data
@ -14,6 +15,8 @@
- dark mode toggle for html - dark mode toggle for html
- app-store link in HTML - app-store link in HTML
- tests
- deploy - deploy
~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~

View File

@ -34,14 +34,15 @@ func NewAPIHandler(app *logic.Application) APIHandler {
// //
// @Summary Create a new user // @Summary Create a new user
// @ID api-user-create // @ID api-user-create
// @Tags API-v2
// //
// @Param post_body body handler.CreateUser.body false " " // @Param post_body body handler.CreateUser.body false " "
// //
// @Success 200 {object} models.UserJSON // @Success 200 {object} handler.sendMessageInternal.response
// @Failure 400 {object} ginresp.apiError // @Failure 400 {object} ginresp.apiError
// @Failure 500 {object} ginresp.apiError // @Failure 500 {object} ginresp.apiError
// //
// @Router /api-v2/users/ [POST] // @Router /api/users/ [POST]
func (h APIHandler) CreateUser(g *gin.Context) ginresp.HTTPResponse { func (h APIHandler) CreateUser(g *gin.Context) ginresp.HTTPResponse {
type body struct { type body struct {
FCMToken string `json:"fcm_token" binding:"required"` FCMToken string `json:"fcm_token" binding:"required"`
@ -117,6 +118,7 @@ func (h APIHandler) CreateUser(g *gin.Context) ginresp.HTTPResponse {
// //
// @Summary Get a user // @Summary Get a user
// @ID api-user-get // @ID api-user-get
// @Tags API-v2
// //
// @Param uid path int true "UserID" // @Param uid path int true "UserID"
// //
@ -126,7 +128,7 @@ func (h APIHandler) CreateUser(g *gin.Context) ginresp.HTTPResponse {
// @Failure 404 {object} ginresp.apiError // @Failure 404 {object} ginresp.apiError
// @Failure 500 {object} ginresp.apiError // @Failure 500 {object} ginresp.apiError
// //
// @Router /api-v2/users/{uid} [GET] // @Router /api/users/{uid} [GET]
func (h APIHandler) GetUser(g *gin.Context) ginresp.HTTPResponse { func (h APIHandler) GetUser(g *gin.Context) ginresp.HTTPResponse {
type uri struct { type uri struct {
UserID models.UserID `uri:"uid"` UserID models.UserID `uri:"uid"`
@ -159,6 +161,7 @@ func (h APIHandler) GetUser(g *gin.Context) ginresp.HTTPResponse {
// @Summary (Partially) update a user // @Summary (Partially) update a user
// @Description The body-values are optional, only send the ones you want to update // @Description The body-values are optional, only send the ones you want to update
// @ID api-user-update // @ID api-user-update
// @Tags API-v2
// //
// @Param uid path int true "UserID" // @Param uid path int true "UserID"
// //
@ -174,7 +177,7 @@ func (h APIHandler) GetUser(g *gin.Context) ginresp.HTTPResponse {
// @Failure 404 {object} ginresp.apiError // @Failure 404 {object} ginresp.apiError
// @Failure 500 {object} ginresp.apiError // @Failure 500 {object} ginresp.apiError
// //
// @Router /api-v2/users/{uid} [PATCH] // @Router /api/users/{uid} [PATCH]
func (h APIHandler) UpdateUser(g *gin.Context) ginresp.HTTPResponse { func (h APIHandler) UpdateUser(g *gin.Context) ginresp.HTTPResponse {
type uri struct { type uri struct {
UserID models.UserID `uri:"uid"` UserID models.UserID `uri:"uid"`
@ -271,6 +274,7 @@ func (h APIHandler) UpdateUser(g *gin.Context) ginresp.HTTPResponse {
// //
// @Summary List all clients // @Summary List all clients
// @ID api-clients-list // @ID api-clients-list
// @Tags API-v2
// //
// @Param uid path int true "UserID" // @Param uid path int true "UserID"
// //
@ -280,7 +284,7 @@ func (h APIHandler) UpdateUser(g *gin.Context) ginresp.HTTPResponse {
// @Failure 404 {object} ginresp.apiError // @Failure 404 {object} ginresp.apiError
// @Failure 500 {object} ginresp.apiError // @Failure 500 {object} ginresp.apiError
// //
// @Router /api-v2/users/{uid}/clients [GET] // @Router /api/users/{uid}/clients [GET]
func (h APIHandler) ListClients(g *gin.Context) ginresp.HTTPResponse { func (h APIHandler) ListClients(g *gin.Context) ginresp.HTTPResponse {
type uri struct { type uri struct {
UserID models.UserID `uri:"uid"` UserID models.UserID `uri:"uid"`
@ -314,6 +318,7 @@ func (h APIHandler) ListClients(g *gin.Context) ginresp.HTTPResponse {
// //
// @Summary Get a single clients // @Summary Get a single clients
// @ID api-clients-get // @ID api-clients-get
// @Tags API-v2
// //
// @Param uid path int true "UserID" // @Param uid path int true "UserID"
// @Param cid path int true "ClientID" // @Param cid path int true "ClientID"
@ -324,7 +329,7 @@ func (h APIHandler) ListClients(g *gin.Context) ginresp.HTTPResponse {
// @Failure 404 {object} ginresp.apiError // @Failure 404 {object} ginresp.apiError
// @Failure 500 {object} ginresp.apiError // @Failure 500 {object} ginresp.apiError
// //
// @Router /api-v2/users/{uid}/clients/{cid} [GET] // @Router /api/users/{uid}/clients/{cid} [GET]
func (h APIHandler) GetClient(g *gin.Context) ginresp.HTTPResponse { func (h APIHandler) GetClient(g *gin.Context) ginresp.HTTPResponse {
type uri struct { type uri struct {
UserID models.UserID `uri:"uid"` UserID models.UserID `uri:"uid"`
@ -357,6 +362,7 @@ func (h APIHandler) GetClient(g *gin.Context) ginresp.HTTPResponse {
// //
// @Summary Add a new clients // @Summary Add a new clients
// @ID api-clients-create // @ID api-clients-create
// @Tags API-v2
// //
// @Param uid path int true "UserID" // @Param uid path int true "UserID"
// //
@ -368,7 +374,7 @@ func (h APIHandler) GetClient(g *gin.Context) ginresp.HTTPResponse {
// @Failure 404 {object} ginresp.apiError // @Failure 404 {object} ginresp.apiError
// @Failure 500 {object} ginresp.apiError // @Failure 500 {object} ginresp.apiError
// //
// @Router /api-v2/users/{uid}/clients [POST] // @Router /api/users/{uid}/clients [POST]
func (h APIHandler) AddClient(g *gin.Context) ginresp.HTTPResponse { func (h APIHandler) AddClient(g *gin.Context) ginresp.HTTPResponse {
type uri struct { type uri struct {
UserID models.UserID `uri:"uid"` UserID models.UserID `uri:"uid"`
@ -413,6 +419,7 @@ func (h APIHandler) AddClient(g *gin.Context) ginresp.HTTPResponse {
// //
// @Summary Delete a client // @Summary Delete a client
// @ID api-clients-delete // @ID api-clients-delete
// @Tags API-v2
// //
// @Param uid path int true "UserID" // @Param uid path int true "UserID"
// @Param cid path int true "ClientID" // @Param cid path int true "ClientID"
@ -423,7 +430,7 @@ func (h APIHandler) AddClient(g *gin.Context) ginresp.HTTPResponse {
// @Failure 404 {object} ginresp.apiError // @Failure 404 {object} ginresp.apiError
// @Failure 500 {object} ginresp.apiError // @Failure 500 {object} ginresp.apiError
// //
// @Router /api-v2/users/{uid}/clients [POST] // @Router /api/users/{uid}/clients [POST]
func (h APIHandler) DeleteClient(g *gin.Context) ginresp.HTTPResponse { func (h APIHandler) DeleteClient(g *gin.Context) ginresp.HTTPResponse {
type uri struct { type uri struct {
UserID models.UserID `uri:"uid"` UserID models.UserID `uri:"uid"`
@ -467,6 +474,7 @@ func (h APIHandler) DeleteClient(g *gin.Context) ginresp.HTTPResponse {
// @Description - "subscribed_any" Return all channels that the user is subscribing to (even unconfirmed) // @Description - "subscribed_any" Return all channels that the user is subscribing to (even unconfirmed)
// @Description - "all_any" Return channels that the user owns or is subscribing (even unconfirmed) // @Description - "all_any" Return channels that the user owns or is subscribing (even unconfirmed)
// @ID api-channels-list // @ID api-channels-list
// @Tags API-v2
// //
// @Param uid path int true "UserID" // @Param uid path int true "UserID"
// @Param selector query string true "Filter channels (default: owned)" Enums(owned, subscribed, all, subscribed_any, all_any) // @Param selector query string true "Filter channels (default: owned)" Enums(owned, subscribed, all, subscribed_any, all_any)
@ -477,7 +485,7 @@ func (h APIHandler) DeleteClient(g *gin.Context) ginresp.HTTPResponse {
// @Failure 404 {object} ginresp.apiError // @Failure 404 {object} ginresp.apiError
// @Failure 500 {object} ginresp.apiError // @Failure 500 {object} ginresp.apiError
// //
// @Router /api-v2/users/{uid}/channels [GET] // @Router /api/users/{uid}/channels [GET]
func (h APIHandler) ListChannels(g *gin.Context) ginresp.HTTPResponse { func (h APIHandler) ListChannels(g *gin.Context) ginresp.HTTPResponse {
type uri struct { type uri struct {
UserID models.UserID `uri:"uid"` UserID models.UserID `uri:"uid"`
@ -546,6 +554,7 @@ func (h APIHandler) ListChannels(g *gin.Context) ginresp.HTTPResponse {
// //
// @Summary List all channels of a user // @Summary List all channels of a user
// @ID api-channels-get // @ID api-channels-get
// @Tags API-v2
// //
// @Param uid path int true "UserID" // @Param uid path int true "UserID"
// @Param cid path int true "ChannelID" // @Param cid path int true "ChannelID"
@ -556,7 +565,7 @@ func (h APIHandler) ListChannels(g *gin.Context) ginresp.HTTPResponse {
// @Failure 404 {object} ginresp.apiError // @Failure 404 {object} ginresp.apiError
// @Failure 500 {object} ginresp.apiError // @Failure 500 {object} ginresp.apiError
// //
// @Router /api-v2/users/{uid}/channels/{cid} [GET] // @Router /api/users/{uid}/channels/{cid} [GET]
func (h APIHandler) GetChannel(g *gin.Context) ginresp.HTTPResponse { func (h APIHandler) GetChannel(g *gin.Context) ginresp.HTTPResponse {
type uri struct { type uri struct {
UserID models.UserID `uri:"uid"` UserID models.UserID `uri:"uid"`
@ -589,6 +598,7 @@ func (h APIHandler) GetChannel(g *gin.Context) ginresp.HTTPResponse {
// //
// @Summary (Partially) update a channel // @Summary (Partially) update a channel
// @ID api-channels-update // @ID api-channels-update
// @Tags API-v2
// //
// @Param uid path int true "UserID" // @Param uid path int true "UserID"
// @Param cid path int true "ChannelID" // @Param cid path int true "ChannelID"
@ -602,7 +612,7 @@ func (h APIHandler) GetChannel(g *gin.Context) ginresp.HTTPResponse {
// @Failure 404 {object} ginresp.apiError // @Failure 404 {object} ginresp.apiError
// @Failure 500 {object} ginresp.apiError // @Failure 500 {object} ginresp.apiError
// //
// @Router /api-v2/users/{uid}/channels/{cid} [PATCH] // @Router /api/users/{uid}/channels/{cid} [PATCH]
func (h APIHandler) UpdateChannel(g *gin.Context) ginresp.HTTPResponse { func (h APIHandler) UpdateChannel(g *gin.Context) ginresp.HTTPResponse {
type uri struct { type uri struct {
UserID models.UserID `uri:"uid"` UserID models.UserID `uri:"uid"`
@ -667,6 +677,7 @@ func (h APIHandler) UpdateChannel(g *gin.Context) ginresp.HTTPResponse {
// @Description If there are no more entries the token "@end" will be returned // @Description If there are no more entries the token "@end" will be returned
// @Description By default we return long messages with a trimmed body, if trimmed=false is supplied we return full messages (this reduces the max page_size) // @Description By default we return long messages with a trimmed body, if trimmed=false is supplied we return full messages (this reduces the max page_size)
// @ID api-channel-messages // @ID api-channel-messages
// @Tags API-v2
// //
// @Param query_data query handler.ListChannelMessages.query false " " // @Param query_data query handler.ListChannelMessages.query false " "
// //
@ -676,7 +687,7 @@ func (h APIHandler) UpdateChannel(g *gin.Context) ginresp.HTTPResponse {
// @Failure 404 {object} ginresp.apiError // @Failure 404 {object} ginresp.apiError
// @Failure 500 {object} ginresp.apiError // @Failure 500 {object} ginresp.apiError
// //
// @Router /api-v2/users/{uid}/channels/{cid}/messages [GET] // @Router /api/users/{uid}/channels/{cid}/messages [GET]
func (h APIHandler) ListChannelMessages(g *gin.Context) ginresp.HTTPResponse { func (h APIHandler) ListChannelMessages(g *gin.Context) ginresp.HTTPResponse {
type uri struct { type uri struct {
ChannelUserID models.UserID `uri:"uid"` ChannelUserID models.UserID `uri:"uid"`
@ -757,6 +768,7 @@ func (h APIHandler) ListChannelMessages(g *gin.Context) ginresp.HTTPResponse {
// //
// @Summary List all channels of a user // @Summary List all channels of a user
// @ID api-user-subscriptions-list // @ID api-user-subscriptions-list
// @Tags API-v2
// //
// @Param uid path int true "UserID" // @Param uid path int true "UserID"
// //
@ -766,7 +778,7 @@ func (h APIHandler) ListChannelMessages(g *gin.Context) ginresp.HTTPResponse {
// @Failure 404 {object} ginresp.apiError // @Failure 404 {object} ginresp.apiError
// @Failure 500 {object} ginresp.apiError // @Failure 500 {object} ginresp.apiError
// //
// @Router /api-v2/users/{uid}/subscriptions [GET] // @Router /api/users/{uid}/subscriptions [GET]
func (h APIHandler) ListUserSubscriptions(g *gin.Context) ginresp.HTTPResponse { func (h APIHandler) ListUserSubscriptions(g *gin.Context) ginresp.HTTPResponse {
type uri struct { type uri struct {
UserID models.UserID `uri:"uid"` UserID models.UserID `uri:"uid"`
@ -800,6 +812,7 @@ func (h APIHandler) ListUserSubscriptions(g *gin.Context) ginresp.HTTPResponse {
// //
// @Summary List all subscriptions of a channel // @Summary List all subscriptions of a channel
// @ID api-chan-subscriptions-list // @ID api-chan-subscriptions-list
// @Tags API-v2
// //
// @Param uid path int true "UserID" // @Param uid path int true "UserID"
// @Param cid path int true "ChannelID" // @Param cid path int true "ChannelID"
@ -810,7 +823,7 @@ func (h APIHandler) ListUserSubscriptions(g *gin.Context) ginresp.HTTPResponse {
// @Failure 404 {object} ginresp.apiError // @Failure 404 {object} ginresp.apiError
// @Failure 500 {object} ginresp.apiError // @Failure 500 {object} ginresp.apiError
// //
// @Router /api-v2/users/{uid}/channels/{cid}/subscriptions [GET] // @Router /api/users/{uid}/channels/{cid}/subscriptions [GET]
func (h APIHandler) ListChannelSubscriptions(g *gin.Context) ginresp.HTTPResponse { func (h APIHandler) ListChannelSubscriptions(g *gin.Context) ginresp.HTTPResponse {
type uri struct { type uri struct {
UserID models.UserID `uri:"uid"` UserID models.UserID `uri:"uid"`
@ -853,6 +866,7 @@ func (h APIHandler) ListChannelSubscriptions(g *gin.Context) ginresp.HTTPRespons
// //
// @Summary Get a single subscription // @Summary Get a single subscription
// @ID api-subscriptions-get // @ID api-subscriptions-get
// @Tags API-v2
// //
// @Param uid path int true "UserID" // @Param uid path int true "UserID"
// @Param sid path int true "SubscriptionID" // @Param sid path int true "SubscriptionID"
@ -863,7 +877,7 @@ func (h APIHandler) ListChannelSubscriptions(g *gin.Context) ginresp.HTTPRespons
// @Failure 404 {object} ginresp.apiError // @Failure 404 {object} ginresp.apiError
// @Failure 500 {object} ginresp.apiError // @Failure 500 {object} ginresp.apiError
// //
// @Router /api-v2/users/{uid}/subscriptions/{sid} [GET] // @Router /api/users/{uid}/subscriptions/{sid} [GET]
func (h APIHandler) GetSubscription(g *gin.Context) ginresp.HTTPResponse { func (h APIHandler) GetSubscription(g *gin.Context) ginresp.HTTPResponse {
type uri struct { type uri struct {
UserID models.UserID `uri:"uid"` UserID models.UserID `uri:"uid"`
@ -900,6 +914,7 @@ func (h APIHandler) GetSubscription(g *gin.Context) ginresp.HTTPResponse {
// //
// @Summary Cancel (delete) subscription // @Summary Cancel (delete) subscription
// @ID api-subscriptions-delete // @ID api-subscriptions-delete
// @Tags API-v2
// //
// @Param uid path int true "UserID" // @Param uid path int true "UserID"
// @Param sid path int true "SubscriptionID" // @Param sid path int true "SubscriptionID"
@ -910,7 +925,7 @@ func (h APIHandler) GetSubscription(g *gin.Context) ginresp.HTTPResponse {
// @Failure 404 {object} ginresp.apiError // @Failure 404 {object} ginresp.apiError
// @Failure 500 {object} ginresp.apiError // @Failure 500 {object} ginresp.apiError
// //
// @Router /api-v2/users/{uid}/subscriptions/{sid} [DELETE] // @Router /api/users/{uid}/subscriptions/{sid} [DELETE]
func (h APIHandler) CancelSubscription(g *gin.Context) ginresp.HTTPResponse { func (h APIHandler) CancelSubscription(g *gin.Context) ginresp.HTTPResponse {
type uri struct { type uri struct {
UserID models.UserID `uri:"uid"` UserID models.UserID `uri:"uid"`
@ -952,6 +967,7 @@ func (h APIHandler) CancelSubscription(g *gin.Context) ginresp.HTTPResponse {
// //
// @Summary Creare/Request a subscription // @Summary Creare/Request a subscription
// @ID api-subscriptions-create // @ID api-subscriptions-create
// @Tags API-v2
// //
// @Param uid path int true "UserID" // @Param uid path int true "UserID"
// @Param query_data query handler.CreateSubscription.query false " " // @Param query_data query handler.CreateSubscription.query false " "
@ -963,7 +979,7 @@ func (h APIHandler) CancelSubscription(g *gin.Context) ginresp.HTTPResponse {
// @Failure 404 {object} ginresp.apiError // @Failure 404 {object} ginresp.apiError
// @Failure 500 {object} ginresp.apiError // @Failure 500 {object} ginresp.apiError
// //
// @Router /api-v2/users/{uid}/subscriptions [POST] // @Router /api/users/{uid}/subscriptions [POST]
func (h APIHandler) CreateSubscription(g *gin.Context) ginresp.HTTPResponse { func (h APIHandler) CreateSubscription(g *gin.Context) ginresp.HTTPResponse {
type uri struct { type uri struct {
UserID models.UserID `uri:"uid"` UserID models.UserID `uri:"uid"`
@ -1013,6 +1029,7 @@ func (h APIHandler) CreateSubscription(g *gin.Context) ginresp.HTTPResponse {
// //
// @Summary Update a subscription (e.g. confirm) // @Summary Update a subscription (e.g. confirm)
// @ID api-subscriptions-update // @ID api-subscriptions-update
// @Tags API-v2
// //
// @Param uid path int true "UserID" // @Param uid path int true "UserID"
// @Param sid path int true "SubscriptionID" // @Param sid path int true "SubscriptionID"
@ -1023,7 +1040,7 @@ func (h APIHandler) CreateSubscription(g *gin.Context) ginresp.HTTPResponse {
// @Failure 404 {object} ginresp.apiError // @Failure 404 {object} ginresp.apiError
// @Failure 500 {object} ginresp.apiError // @Failure 500 {object} ginresp.apiError
// //
// @Router /api-v2/users/{uid}/subscriptions/{sid} [PATCH] // @Router /api/users/{uid}/subscriptions/{sid} [PATCH]
func (h APIHandler) UpdateSubscription(g *gin.Context) ginresp.HTTPResponse { func (h APIHandler) UpdateSubscription(g *gin.Context) ginresp.HTTPResponse {
type uri struct { type uri struct {
UserID models.UserID `uri:"uid"` UserID models.UserID `uri:"uid"`
@ -1080,6 +1097,7 @@ func (h APIHandler) UpdateSubscription(g *gin.Context) ginresp.HTTPResponse {
// @Description If there are no more entries the token "@end" will be returned // @Description If there are no more entries the token "@end" will be returned
// @Description By default we return long messages with a trimmed body, if trimmed=false is supplied we return full messages (this reduces the max page_size) // @Description By default we return long messages with a trimmed body, if trimmed=false is supplied we return full messages (this reduces the max page_size)
// @ID api-messages-list // @ID api-messages-list
// @Tags API-v2
// //
// @Param query_data query handler.ListMessages.query false " " // @Param query_data query handler.ListMessages.query false " "
// //
@ -1089,7 +1107,7 @@ func (h APIHandler) UpdateSubscription(g *gin.Context) ginresp.HTTPResponse {
// @Failure 404 {object} ginresp.apiError // @Failure 404 {object} ginresp.apiError
// @Failure 500 {object} ginresp.apiError // @Failure 500 {object} ginresp.apiError
// //
// @Router /api-v2/messages [GET] // @Router /api/messages [GET]
func (h APIHandler) ListMessages(g *gin.Context) ginresp.HTTPResponse { func (h APIHandler) ListMessages(g *gin.Context) ginresp.HTTPResponse {
type query struct { type query struct {
PageSize *int `form:"page_size"` PageSize *int `form:"page_size"`
@ -1154,6 +1172,7 @@ func (h APIHandler) ListMessages(g *gin.Context) ginresp.HTTPResponse {
// @Description Or the user must subscribe to the corresponding channel (and be confirmed) and request the resource with the READ or ADMIN Key // @Description Or the user must subscribe to the corresponding channel (and be confirmed) and request the resource with the READ or ADMIN Key
// @Description The returned message is never trimmed // @Description The returned message is never trimmed
// @ID api-messages-get // @ID api-messages-get
// @Tags API-v2
// //
// @Param mid path int true "SCNMessageID" // @Param mid path int true "SCNMessageID"
// //
@ -1163,7 +1182,7 @@ func (h APIHandler) ListMessages(g *gin.Context) ginresp.HTTPResponse {
// @Failure 404 {object} ginresp.apiError // @Failure 404 {object} ginresp.apiError
// @Failure 500 {object} ginresp.apiError // @Failure 500 {object} ginresp.apiError
// //
// @Router /api-v2/messages/{mid} [PATCH] // @Router /api/messages/{mid} [PATCH]
func (h APIHandler) GetMessage(g *gin.Context) ginresp.HTTPResponse { func (h APIHandler) GetMessage(g *gin.Context) ginresp.HTTPResponse {
type uri struct { type uri struct {
MessageID models.SCNMessageID `uri:"mid"` MessageID models.SCNMessageID `uri:"mid"`
@ -1223,6 +1242,7 @@ func (h APIHandler) GetMessage(g *gin.Context) ginresp.HTTPResponse {
// @Summary Delete a single message // @Summary Delete a single message
// @Description The user must own the message and request the resource with the ADMIN Key // @Description The user must own the message and request the resource with the ADMIN Key
// @ID api-messages-delete // @ID api-messages-delete
// @Tags API-v2
// //
// @Param mid path int true "SCNMessageID" // @Param mid path int true "SCNMessageID"
// //
@ -1232,7 +1252,7 @@ func (h APIHandler) GetMessage(g *gin.Context) ginresp.HTTPResponse {
// @Failure 404 {object} ginresp.apiError // @Failure 404 {object} ginresp.apiError
// @Failure 500 {object} ginresp.apiError // @Failure 500 {object} ginresp.apiError
// //
// @Router /api-v2/messages/{mid} [PATCH] // @Router /api/messages/{mid} [PATCH]
func (h APIHandler) DeleteMessage(g *gin.Context) ginresp.HTTPResponse { func (h APIHandler) DeleteMessage(g *gin.Context) ginresp.HTTPResponse {
type uri struct { type uri struct {
MessageID models.SCNMessageID `uri:"mid"` MessageID models.SCNMessageID `uri:"mid"`
@ -1280,6 +1300,7 @@ func (h APIHandler) DeleteMessage(g *gin.Context) ginresp.HTTPResponse {
// @Description This is similar to the main route `POST -> https://scn.blackfrestbytes.com/` // @Description This is similar to the main route `POST -> https://scn.blackfrestbytes.com/`
// @Description But this route can change in the future, for long-living scripts etc. it's better to use the normal POST route // @Description But this route can change in the future, for long-living scripts etc. it's better to use the normal POST route
// @ID api-messages-create // @ID api-messages-create
// @Tags API-v2
// //
// @Param post_data query handler.CreateMessage.body false " " // @Param post_data query handler.CreateMessage.body false " "
// //
@ -1289,7 +1310,7 @@ func (h APIHandler) DeleteMessage(g *gin.Context) ginresp.HTTPResponse {
// @Failure 404 {object} ginresp.apiError // @Failure 404 {object} ginresp.apiError
// @Failure 500 {object} ginresp.apiError // @Failure 500 {object} ginresp.apiError
// //
// @Router /api-v2/messages [POST] // @Router /api/messages [POST]
func (h APIHandler) CreateMessage(g *gin.Context) ginresp.HTTPResponse { func (h APIHandler) CreateMessage(g *gin.Context) ginresp.HTTPResponse {
type body struct { type body struct {
Channel *string `json:"channel"` Channel *string `json:"channel"`

View File

@ -4,6 +4,7 @@ import (
"blackforestbytes.com/simplecloudnotifier/common/ginresp" "blackforestbytes.com/simplecloudnotifier/common/ginresp"
"blackforestbytes.com/simplecloudnotifier/logic" "blackforestbytes.com/simplecloudnotifier/logic"
"bytes" "bytes"
"errors"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
sqlite3 "github.com/mattn/go-sqlite3" sqlite3 "github.com/mattn/go-sqlite3"
"net/http" "net/http"
@ -33,13 +34,18 @@ type pingResponseInfo struct {
// Ping swaggerdoc // Ping swaggerdoc
// //
// @Summary Simple endpoint to test connection (any http method)
// @ID api-common-ping
// @Tags Common
//
// @Success 200 {object} pingResponse // @Success 200 {object} pingResponse
// @Failure 500 {object} ginresp.apiError // @Failure 500 {object} ginresp.apiError
// @Router /ping [get] //
// @Router /ping [post] // @Router /api/ping [get]
// @Router /ping [put] // @Router /api/ping [post]
// @Router /ping [delete] // @Router /api/ping [put]
// @Router /ping [patch] // @Router /api/ping [delete]
// @Router /api/ping [patch]
func (h CommonHandler) Ping(g *gin.Context) ginresp.HTTPResponse { func (h CommonHandler) Ping(g *gin.Context) ginresp.HTTPResponse {
buf := new(bytes.Buffer) buf := new(bytes.Buffer)
_, _ = buf.ReadFrom(g.Request.Body) _, _ = buf.ReadFrom(g.Request.Body)
@ -59,9 +65,14 @@ func (h CommonHandler) Ping(g *gin.Context) ginresp.HTTPResponse {
// DatabaseTest swaggerdoc // DatabaseTest swaggerdoc
// //
// @Summary Check for a wroking database connection
// @ID api-common-dbtest
// @Tags Common
//
// @Success 200 {object} handler.DatabaseTest.response // @Success 200 {object} handler.DatabaseTest.response
// @Failure 500 {object} ginresp.apiError // @Failure 500 {object} ginresp.apiError
// @Router /db-test [get] //
// @Router /api/db-test [get]
func (h CommonHandler) DatabaseTest(g *gin.Context) ginresp.HTTPResponse { func (h CommonHandler) DatabaseTest(g *gin.Context) ginresp.HTTPResponse {
type response struct { type response struct {
Success bool `json:"success"` Success bool `json:"success"`
@ -87,23 +98,42 @@ func (h CommonHandler) DatabaseTest(g *gin.Context) ginresp.HTTPResponse {
// Health swaggerdoc // Health swaggerdoc
// //
// @Summary Server Health-checks
// @ID api-common-health
// @Tags Common
//
// @Success 200 {object} handler.Health.response // @Success 200 {object} handler.Health.response
// @Failure 500 {object} ginresp.apiError // @Failure 500 {object} ginresp.apiError
// @Router /health [get] //
// @Router /api/health [get]
func (h CommonHandler) Health(*gin.Context) ginresp.HTTPResponse { func (h CommonHandler) Health(*gin.Context) ginresp.HTTPResponse {
type response struct { type response struct {
Status string `json:"status"` Status string `json:"status"`
} }
_, libVersionNumber, _ := sqlite3.Version()
if libVersionNumber < 3039000 {
ginresp.InternalError(errors.New("sqlite version too low"))
}
err := h.app.Database.Ping()
if err != nil {
return ginresp.InternalError(err)
}
return ginresp.JSON(http.StatusOK, response{Status: "ok"}) return ginresp.JSON(http.StatusOK, response{Status: "ok"})
} }
func (h CommonHandler) NoRoute(g *gin.Context) ginresp.HTTPResponse { func (h CommonHandler) NoRoute(g *gin.Context) ginresp.HTTPResponse {
return ginresp.JSON(http.StatusNotFound, gin.H{ return ginresp.JSON(http.StatusNotFound, gin.H{
"": "================ ROUTE NOT FOUND ================",
"FullPath": g.FullPath(), "FullPath": g.FullPath(),
"Method": g.Request.Method, "Method": g.Request.Method,
"URL": g.Request.URL.String(), "URL": g.Request.URL.String(),
"RequestURI": g.Request.RequestURI, "RequestURI": g.Request.RequestURI,
"Proto": g.Request.Proto, "Proto": g.Request.Proto,
"Header": g.Request.Header, "Header": g.Request.Header,
"~": "================ ROUTE NOT FOUND ================",
}) })
} }

View File

@ -28,6 +28,8 @@ func NewCompatHandler(app *logic.Application) CompatHandler {
// //
// @Summary Register a new account // @Summary Register a new account
// @ID compat-register // @ID compat-register
// @Tags API-v1
//
// @Deprecated // @Deprecated
// //
// @Param fcm_token query string true "the (android) fcm token" // @Param fcm_token query string true "the (android) fcm token"
@ -134,6 +136,8 @@ func (h CompatHandler) Register(g *gin.Context) ginresp.HTTPResponse {
// //
// @Summary Get information about the current user // @Summary Get information about the current user
// @ID compat-info // @ID compat-info
// @Tags API-v1
//
// @Deprecated // @Deprecated
// //
// @Param user_id query string true "the user_id" // @Param user_id query string true "the user_id"
@ -216,6 +220,8 @@ func (h CompatHandler) Info(g *gin.Context) ginresp.HTTPResponse {
// //
// @Summary Acknowledge that a message was received // @Summary Acknowledge that a message was received
// @ID compat-ack // @ID compat-ack
// @Tags API-v1
//
// @Deprecated // @Deprecated
// //
// @Param user_id query string true "the user_id" // @Param user_id query string true "the user_id"
@ -287,6 +293,8 @@ func (h CompatHandler) Ack(g *gin.Context) ginresp.HTTPResponse {
// //
// @Summary Return all not-acknowledged messages // @Summary Return all not-acknowledged messages
// @ID compat-requery // @ID compat-requery
// @Tags API-v1
//
// @Deprecated // @Deprecated
// //
// @Param user_id query string true "the user_id" // @Param user_id query string true "the user_id"
@ -352,6 +360,8 @@ func (h CompatHandler) Requery(g *gin.Context) ginresp.HTTPResponse {
// //
// @Summary Set the fcm-token (android) // @Summary Set the fcm-token (android)
// @ID compat-update // @ID compat-update
// @Tags API-v1
//
// @Deprecated // @Deprecated
// //
// @Param user_id query string true "the user_id" // @Param user_id query string true "the user_id"
@ -463,6 +473,8 @@ func (h CompatHandler) Update(g *gin.Context) ginresp.HTTPResponse {
// //
// @Summary Get a whole (potentially truncated) message // @Summary Get a whole (potentially truncated) message
// @ID compat-expand // @ID compat-expand
// @Tags API-v1
//
// @Deprecated // @Deprecated
// //
// @Param user_id query string true "The user_id" // @Param user_id query string true "The user_id"
@ -548,6 +560,8 @@ func (h CompatHandler) Expand(g *gin.Context) ginresp.HTTPResponse {
// //
// @Summary Upgrade a free account to a paid account // @Summary Upgrade a free account to a paid account
// @ID compat-upgrade // @ID compat-upgrade
// @Tags API-v1
//
// @Deprecated // @Deprecated
// //
// @Param user_id query string true "the user_id" // @Param user_id query string true "the user_id"

View File

@ -36,6 +36,7 @@ func NewMessageHandler(app *logic.Application) MessageHandler {
// //
// @Summary Send a new message (compatibility) // @Summary Send a new message (compatibility)
// @Description All parameter can be set via query-parameter or form-data body. Only UserID, UserKey and Title are required // @Description All parameter can be set via query-parameter or form-data body. Only UserID, UserKey and Title are required
// @Tags External
// //
// @Param query_data query handler.SendMessageCompat.query false " " // @Param query_data query handler.SendMessageCompat.query false " "
// @Param form_data formData handler.SendMessageCompat.form false " " // @Param form_data formData handler.SendMessageCompat.form false " "
@ -85,6 +86,7 @@ func (h MessageHandler) SendMessageCompat(g *gin.Context) ginresp.HTTPResponse {
// //
// @Summary Send a new message // @Summary Send a new message
// @Description All parameter can be set via query-parameter or the json body. Only UserID, UserKey and Title are required // @Description All parameter can be set via query-parameter or the json body. Only UserID, UserKey and Title are required
// @Tags External
// //
// @Param query_data query handler.SendMessage.query false " " // @Param query_data query handler.SendMessage.query false " "
// @Param post_body body handler.SendMessage.body false " " // @Param post_body body handler.SendMessage.body false " "

View File

@ -32,49 +32,65 @@ func NewRouter(app *logic.Application) *Router {
} }
// Init swaggerdocs // Init swaggerdocs
//
// @title SimpleCloudNotifier API // @title SimpleCloudNotifier API
// @version 2.0 // @version 2.0
// @description API for SCN // @description API for SCN
// @host scn.blackforestbytes.com // @host scn.blackforestbytes.com
//
// @tag.name Common
// @tag.name External
// @tag.name API-v1
// @tag.name API-v2
//
// @BasePath / // @BasePath /
func (r *Router) Init(e *gin.Engine) { func (r *Router) Init(e *gin.Engine) {
// ================ General ================ // ================ General ================
e.Any("/api/common/ping", ginresp.Wrap(r.commonHandler.Ping)) commonAPI := e.Group("/api")
e.POST("/api/common/db-test", ginresp.Wrap(r.commonHandler.DatabaseTest)) {
e.GET("/api/common/health", ginresp.Wrap(r.commonHandler.Health)) commonAPI.Any("/ping", ginresp.Wrap(r.commonHandler.Ping))
commonAPI.POST("/db-test", ginresp.Wrap(r.commonHandler.DatabaseTest))
commonAPI.GET("/health", ginresp.Wrap(r.commonHandler.Health))
}
// ================ Swagger ================ // ================ Swagger ================
e.GET("/documentation/swagger", ginext.RedirectTemporary("/documentation/swagger/")) docs := e.Group("/documentation")
e.GET("/documentation/swagger/", ginresp.Wrap(swagger.Handle)) {
e.GET("/documentation/swagger/:fn", ginresp.Wrap(swagger.Handle)) docs.GET("/swagger", ginext.RedirectTemporary("/documentation/swagger/"))
docs.GET("/swagger/", ginresp.Wrap(swagger.Handle))
docs.GET("/swagger/:fn", ginresp.Wrap(swagger.Handle))
}
// ================ Website ================ // ================ Website ================
e.GET("/", ginresp.Wrap(r.websiteHandler.Index)) frontend := e.Group("")
e.GET("/index.php", ginresp.Wrap(r.websiteHandler.Index)) {
e.GET("/index.html", ginresp.Wrap(r.websiteHandler.Index)) frontend.GET("/", ginresp.Wrap(r.websiteHandler.Index))
e.GET("/index", ginresp.Wrap(r.websiteHandler.Index)) frontend.GET("/index.php", ginresp.Wrap(r.websiteHandler.Index))
frontend.GET("/index.html", ginresp.Wrap(r.websiteHandler.Index))
frontend.GET("/index", ginresp.Wrap(r.websiteHandler.Index))
e.GET("/api", ginresp.Wrap(r.websiteHandler.APIDocs)) frontend.GET("/api", ginresp.Wrap(r.websiteHandler.APIDocs))
e.GET("/api.php", ginresp.Wrap(r.websiteHandler.APIDocs)) frontend.GET("/api.php", ginresp.Wrap(r.websiteHandler.APIDocs))
e.GET("/api.html", ginresp.Wrap(r.websiteHandler.APIDocs)) frontend.GET("/api.html", ginresp.Wrap(r.websiteHandler.APIDocs))
e.GET("/api_more", ginresp.Wrap(r.websiteHandler.APIDocsMore)) frontend.GET("/api_more", ginresp.Wrap(r.websiteHandler.APIDocsMore))
e.GET("/api_more.php", ginresp.Wrap(r.websiteHandler.APIDocsMore)) frontend.GET("/api_more.php", ginresp.Wrap(r.websiteHandler.APIDocsMore))
e.GET("/api_more.html", ginresp.Wrap(r.websiteHandler.APIDocsMore)) frontend.GET("/api_more.html", ginresp.Wrap(r.websiteHandler.APIDocsMore))
e.GET("/message_sent", ginresp.Wrap(r.websiteHandler.MessageSent)) frontend.GET("/message_sent", ginresp.Wrap(r.websiteHandler.MessageSent))
e.GET("/message_sent.php", ginresp.Wrap(r.websiteHandler.MessageSent)) frontend.GET("/message_sent.php", ginresp.Wrap(r.websiteHandler.MessageSent))
e.GET("/message_sent.html", ginresp.Wrap(r.websiteHandler.MessageSent)) frontend.GET("/message_sent.html", ginresp.Wrap(r.websiteHandler.MessageSent))
e.GET("/favicon.ico", ginresp.Wrap(r.websiteHandler.FaviconIco)) frontend.GET("/favicon.ico", ginresp.Wrap(r.websiteHandler.FaviconIco))
e.GET("/favicon.png", ginresp.Wrap(r.websiteHandler.FaviconPNG)) frontend.GET("/favicon.png", ginresp.Wrap(r.websiteHandler.FaviconPNG))
e.GET("/js/:fn", ginresp.Wrap(r.websiteHandler.Javascript)) frontend.GET("/js/:fn", ginresp.Wrap(r.websiteHandler.Javascript))
e.GET("/css/:fn", ginresp.Wrap(r.websiteHandler.CSS)) frontend.GET("/css/:fn", ginresp.Wrap(r.websiteHandler.CSS))
}
// ================ Compat (v1) ================ // ================ Compat (v1) ================
@ -91,7 +107,7 @@ func (r *Router) Init(e *gin.Engine) {
// ================ Manage API ================ // ================ Manage API ================
apiv2 := e.Group("/api-v2/") apiv2 := e.Group("/api/")
{ {
apiv2.POST("/users", ginresp.Wrap(r.apiHandler.CreateUser)) apiv2.POST("/users", ginresp.Wrap(r.apiHandler.CreateUser))

View File

@ -6,9 +6,9 @@ import (
"blackforestbytes.com/simplecloudnotifier/common" "blackforestbytes.com/simplecloudnotifier/common"
"blackforestbytes.com/simplecloudnotifier/common/ginext" "blackforestbytes.com/simplecloudnotifier/common/ginext"
"blackforestbytes.com/simplecloudnotifier/db" "blackforestbytes.com/simplecloudnotifier/db"
"blackforestbytes.com/simplecloudnotifier/firebase"
"blackforestbytes.com/simplecloudnotifier/jobs" "blackforestbytes.com/simplecloudnotifier/jobs"
"blackforestbytes.com/simplecloudnotifier/logic" "blackforestbytes.com/simplecloudnotifier/logic"
"blackforestbytes.com/simplecloudnotifier/push"
"fmt" "fmt"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
) )
@ -36,15 +36,20 @@ func main() {
router := api.NewRouter(app) router := api.NewRouter(app)
fb, err := firebase.NewFirebase(conf) var nc push.NotificationClient
if conf.DummyFirebase {
nc, err = push.NewDummy()
} else {
nc, err = push.NewFirebaseConn(conf)
if err != nil { if err != nil {
log.Fatal().Err(err).Msg("failed to init firebase") log.Fatal().Err(err).Msg("failed to init firebase")
return return
} }
}
jobRetry := jobs.NewDeliveryRetryJob(app) jobRetry := jobs.NewDeliveryRetryJob(app)
app.Init(conf, ginengine, fb, []logic.Job{jobRetry}) app.Init(conf, ginengine, nc, []logic.Job{jobRetry})
router.Init(ginengine) router.Init(ginengine)

View File

@ -15,6 +15,7 @@ type Config struct {
DBFile string DBFile string
RequestTimeout time.Duration RequestTimeout time.Duration
ReturnRawErrors bool ReturnRawErrors bool
DummyFirebase bool
FirebaseTokenURI string FirebaseTokenURI string
FirebaseProjectID string FirebaseProjectID string
FirebasePrivKeyID string FirebasePrivKeyID string
@ -24,7 +25,8 @@ type Config struct {
var Conf Config var Conf Config
var configLocHost = Config{ var configLocHost = func() Config {
return Config{
Namespace: "local-host", Namespace: "local-host",
GinDebug: true, GinDebug: true,
ServerIP: "0.0.0.0", ServerIP: "0.0.0.0",
@ -32,14 +34,17 @@ var configLocHost = Config{
DBFile: ".run-data/db.sqlite3", DBFile: ".run-data/db.sqlite3",
RequestTimeout: 16 * time.Second, RequestTimeout: 16 * time.Second,
ReturnRawErrors: true, ReturnRawErrors: true,
FirebaseTokenURI: "https://oauth2.googleapis.com/token", DummyFirebase: true,
FirebaseTokenURI: "",
FirebaseProjectID: "", FirebaseProjectID: "",
FirebasePrivKeyID: "", FirebasePrivKeyID: "",
FirebaseClientMail: "", FirebaseClientMail: "",
FirebasePrivateKey: "", FirebasePrivateKey: "",
}
} }
var configLocDocker = Config{ var configLocDocker = func() Config {
return Config{
Namespace: "local-docker", Namespace: "local-docker",
GinDebug: true, GinDebug: true,
ServerIP: "0.0.0.0", ServerIP: "0.0.0.0",
@ -47,14 +52,17 @@ var configLocDocker = Config{
DBFile: "/data/scn_docker.sqlite3", DBFile: "/data/scn_docker.sqlite3",
RequestTimeout: 16 * time.Second, RequestTimeout: 16 * time.Second,
ReturnRawErrors: true, ReturnRawErrors: true,
FirebaseTokenURI: "https://oauth2.googleapis.com/token", DummyFirebase: true,
FirebaseTokenURI: "",
FirebaseProjectID: "", FirebaseProjectID: "",
FirebasePrivKeyID: "", FirebasePrivKeyID: "",
FirebaseClientMail: "", FirebaseClientMail: "",
FirebasePrivateKey: "", FirebasePrivateKey: "",
}
} }
var configDev = Config{ var configDev = func() Config {
return Config{
Namespace: "develop", Namespace: "develop",
GinDebug: true, GinDebug: true,
ServerIP: "0.0.0.0", ServerIP: "0.0.0.0",
@ -62,14 +70,17 @@ var configDev = Config{
DBFile: "/data/scn.sqlite3", DBFile: "/data/scn.sqlite3",
RequestTimeout: 16 * time.Second, RequestTimeout: 16 * time.Second,
ReturnRawErrors: true, ReturnRawErrors: true,
DummyFirebase: false,
FirebaseTokenURI: "https://oauth2.googleapis.com/token", FirebaseTokenURI: "https://oauth2.googleapis.com/token",
FirebaseProjectID: confEnv("FB_PROJECTID"), FirebaseProjectID: confEnv("FB_PROJECTID"),
FirebasePrivKeyID: confEnv("FB_PRIVATEKEYID"), FirebasePrivKeyID: confEnv("FB_PRIVATEKEYID"),
FirebaseClientMail: confEnv("FB_CLIENTEMAIL"), FirebaseClientMail: confEnv("FB_CLIENTEMAIL"),
FirebasePrivateKey: confEnv("FB_PRIVATEKEY"), FirebasePrivateKey: confEnv("FB_PRIVATEKEY"),
}
} }
var configStag = Config{ var configStag = func() Config {
return Config{
Namespace: "staging", Namespace: "staging",
GinDebug: true, GinDebug: true,
ServerIP: "0.0.0.0", ServerIP: "0.0.0.0",
@ -77,14 +88,17 @@ var configStag = Config{
DBFile: "/data/scn.sqlite3", DBFile: "/data/scn.sqlite3",
RequestTimeout: 16 * time.Second, RequestTimeout: 16 * time.Second,
ReturnRawErrors: true, ReturnRawErrors: true,
DummyFirebase: false,
FirebaseTokenURI: "https://oauth2.googleapis.com/token", FirebaseTokenURI: "https://oauth2.googleapis.com/token",
FirebaseProjectID: confEnv("FB_PROJECTID"), FirebaseProjectID: confEnv("FB_PROJECTID"),
FirebasePrivKeyID: confEnv("FB_PRIVATEKEYID"), FirebasePrivKeyID: confEnv("FB_PRIVATEKEYID"),
FirebaseClientMail: confEnv("FB_CLIENTEMAIL"), FirebaseClientMail: confEnv("FB_CLIENTEMAIL"),
FirebasePrivateKey: confEnv("FB_PRIVATEKEY"), FirebasePrivateKey: confEnv("FB_PRIVATEKEY"),
}
} }
var configProd = Config{ var configProd = func() Config {
return Config{
Namespace: "production", Namespace: "production",
GinDebug: false, GinDebug: false,
ServerIP: "0.0.0.0", ServerIP: "0.0.0.0",
@ -92,29 +106,29 @@ var configProd = Config{
DBFile: "/data/scn.sqlite3", DBFile: "/data/scn.sqlite3",
RequestTimeout: 16 * time.Second, RequestTimeout: 16 * time.Second,
ReturnRawErrors: false, ReturnRawErrors: false,
DummyFirebase: false,
FirebaseTokenURI: "https://oauth2.googleapis.com/token", FirebaseTokenURI: "https://oauth2.googleapis.com/token",
FirebaseProjectID: confEnv("FB_PROJECTID"), FirebaseProjectID: confEnv("FB_PROJECTID"),
FirebasePrivKeyID: confEnv("FB_PRIVATEKEYID"), FirebasePrivKeyID: confEnv("FB_PRIVATEKEYID"),
FirebaseClientMail: confEnv("FB_CLIENTEMAIL"), FirebaseClientMail: confEnv("FB_CLIENTEMAIL"),
FirebasePrivateKey: confEnv("FB_PRIVATEKEY"), FirebasePrivateKey: confEnv("FB_PRIVATEKEY"),
}
} }
var allConfig = []Config{ var allConfig = map[string]func() Config{
configLocHost, "local-host": configLocHost,
configLocDocker, "local-docker": configLocDocker,
configDev, "develop": configDev,
configStag, "staging": configStag,
configProd, "production": configProd,
} }
func getConfig(ns string) (Config, bool) { func getConfig(ns string) (Config, bool) {
if ns == "" { if ns == "" {
return configLocHost, true return configLocHost(), true
}
for _, c := range allConfig {
if c.Namespace == ns {
return c, true
} }
if c, ok := allConfig[ns]; ok {
return c(), true
} }
return Config{}, false return Config{}, false
} }

View File

@ -1,12 +0,0 @@
{
"type": "service_account",
"project_id": "simplecloudnotifier-ea7ef",
"private_key_id": "5bfab19fca25034e87c5b3bd1a4334499d2d1f85",
"private_key": "-----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",
"client_email": "firebase-adminsdk-42grv@simplecloudnotifier-ea7ef.iam.gserviceaccount.com",
"client_id": "109837766844812362714",
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
"token_uri": "https://oauth2.googleapis.com/token",
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
"client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/firebase-adminsdk-42grv%40simplecloudnotifier-ea7ef.iam.gserviceaccount.com"
}

View File

@ -5,8 +5,8 @@ import (
"blackforestbytes.com/simplecloudnotifier/api/apierr" "blackforestbytes.com/simplecloudnotifier/api/apierr"
"blackforestbytes.com/simplecloudnotifier/common/ginresp" "blackforestbytes.com/simplecloudnotifier/common/ginresp"
"blackforestbytes.com/simplecloudnotifier/db" "blackforestbytes.com/simplecloudnotifier/db"
"blackforestbytes.com/simplecloudnotifier/firebase"
"blackforestbytes.com/simplecloudnotifier/models" "blackforestbytes.com/simplecloudnotifier/models"
"blackforestbytes.com/simplecloudnotifier/push"
"context" "context"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/gin-gonic/gin/binding" "github.com/gin-gonic/gin/binding"
@ -27,7 +27,7 @@ type Application struct {
Config scn.Config Config scn.Config
Gin *gin.Engine Gin *gin.Engine
Database *db.Database Database *db.Database
Firebase *firebase.FBConnector Firebase push.NotificationClient
Jobs []Job Jobs []Job
} }
@ -37,7 +37,7 @@ func NewApp(db *db.Database) *Application {
} }
} }
func (app *Application) Init(cfg scn.Config, g *gin.Engine, fb *firebase.FBConnector, jobs []Job) { func (app *Application) Init(cfg scn.Config, g *gin.Engine, fb push.NotificationClient, jobs []Job) {
app.Config = cfg app.Config = cfg
app.Gin = g app.Gin = g
app.Firebase = fb app.Firebase = fb

17
server/push/dummy.go Normal file
View File

@ -0,0 +1,17 @@
package push
import (
"blackforestbytes.com/simplecloudnotifier/models"
"context"
_ "embed"
)
type DummyConnector struct{}
func NewDummy() (NotificationClient, error) {
return &DummyConnector{}, nil
}
func (d DummyConnector) SendNotification(ctx context.Context, client models.Client, msg models.Message) (string, error) {
return "%DUMMY%", nil
}

View File

@ -1,4 +1,4 @@
package firebase package push
import ( import (
scn "blackforestbytes.com/simplecloudnotifier" scn "blackforestbytes.com/simplecloudnotifier"
@ -21,20 +21,20 @@ import (
// https://firebase.google.com/docs/cloud-messaging/send-message#rest // https://firebase.google.com/docs/cloud-messaging/send-message#rest
// https://firebase.google.com/docs/cloud-messaging/auth-server // https://firebase.google.com/docs/cloud-messaging/auth-server
type FBConnector struct { type FirebaseConnector struct {
fbProject string fbProject string
client http.Client client http.Client
auth *FBOAuth2 auth *FirebaseOAuth2
} }
func NewFirebase(conf scn.Config) (*FBConnector, error) { func NewFirebaseConn(conf scn.Config) (NotificationClient, error) {
fbauth, err := NewAuth(conf.FirebaseTokenURI, conf.FirebaseProjectID, conf.FirebaseClientMail, conf.FirebasePrivateKey) fbauth, err := NewAuth(conf.FirebaseTokenURI, conf.FirebaseProjectID, conf.FirebaseClientMail, conf.FirebasePrivateKey)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return &FBConnector{ return &FirebaseConnector{
fbProject: conf.FirebaseProjectID, fbProject: conf.FirebaseProjectID,
client: http.Client{Timeout: 5 * time.Second}, client: http.Client{Timeout: 5 * time.Second},
auth: fbauth, auth: fbauth,
@ -50,7 +50,7 @@ type Notification struct {
Priority int Priority int
} }
func (fb FBConnector) SendNotification(ctx context.Context, client models.Client, msg models.Message) (string, error) { func (fb FirebaseConnector) SendNotification(ctx context.Context, client models.Client, msg models.Message) (string, error) {
uri := "https://fcm.googleapis.com/v1/projects/" + fb.fbProject + "/messages:send" uri := "https://fcm.googleapis.com/v1/projects/" + fb.fbProject + "/messages:send"

View File

@ -0,0 +1,10 @@
package push
import (
"blackforestbytes.com/simplecloudnotifier/models"
"context"
)
type NotificationClient interface {
SendNotification(ctx context.Context, client models.Client, msg models.Message) (string, error)
}

View File

@ -1,4 +1,4 @@
package firebase package push
import ( import (
"context" "context"
@ -22,7 +22,7 @@ import (
"time" "time"
) )
type FBOAuth2 struct { type FirebaseOAuth2 struct {
client *http.Client client *http.Client
scopes []string scopes []string
@ -35,14 +35,14 @@ type FBOAuth2 struct {
privateKey *rsa.PrivateKey privateKey *rsa.PrivateKey
} }
func NewAuth(tokenURL string, privKeyID string, cmail string, pemstr string) (*FBOAuth2, error) { func NewAuth(tokenURL string, privKeyID string, cmail string, pemstr string) (*FirebaseOAuth2, error) {
pkey, err := decodePemKey(pemstr) pkey, err := decodePemKey(pemstr)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return &FBOAuth2{ return &FirebaseOAuth2{
client: &http.Client{Timeout: 3 * time.Second}, client: &http.Client{Timeout: 3 * time.Second},
tokenURL: tokenURL, tokenURL: tokenURL,
privateKey: pkey, privateKey: pkey,
@ -87,7 +87,7 @@ func decodePemKey(pemstr string) (*rsa.PrivateKey, error) {
return nil, errors.New(fmt.Sprintf("failed to parse private-key: [ %v | %v ]", err1, err2)) return nil, errors.New(fmt.Sprintf("failed to parse private-key: [ %v | %v ]", err1, err2))
} }
func (a *FBOAuth2) Token(ctx context.Context) (string, error) { func (a *FirebaseOAuth2) Token(ctx context.Context) (string, error) {
if a.currToken == nil || a.tokenExpiry == nil || a.tokenExpiry.Before(time.Now()) { if a.currToken == nil || a.tokenExpiry == nil || a.tokenExpiry.Before(time.Now()) {
err := a.Refresh(ctx) err := a.Refresh(ctx)
if err != nil { if err != nil {
@ -98,7 +98,7 @@ func (a *FBOAuth2) Token(ctx context.Context) (string, error) {
return *a.currToken, nil return *a.currToken, nil
} }
func (a *FBOAuth2) Refresh(ctx context.Context) error { func (a *FirebaseOAuth2) Refresh(ctx context.Context) error {
assertion, err := a.encodeAssertion(a.privateKey) assertion, err := a.encodeAssertion(a.privateKey)
if err != nil { if err != nil {
@ -153,7 +153,7 @@ func (a *FBOAuth2) Refresh(ctx context.Context) error {
return nil return nil
} }
func (a *FBOAuth2) encodeAssertion(key *rsa.PrivateKey) (string, error) { func (a *FirebaseOAuth2) encodeAssertion(key *rsa.PrivateKey) (string, error) {
headBin, err := json.Marshal(gin.H{"alg": "RS256", "typ": "JWT", "kid": a.privateKeyID}) headBin, err := json.Marshal(gin.H{"alg": "RS256", "typ": "JWT", "kid": a.privateKeyID})
if err != nil { if err != nil {
return "", err return "", err

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff