4 Commits

Author SHA1 Message Date
c66cd0568f Merge branch 'test/remove_userid_param' 2025-12-05 14:30:55 +01:00
0800d25b30 Remove required user_id param when sending messages 2025-12-05 14:30:44 +01:00
6d180aea38 Remove delete-channel from webapp 2025-12-04 09:16:47 +01:00
3c45191d11 fix broken links on non-owned channels
Some checks failed
Build Docker and Deploy / Build Docker Container (push) Successful in 1m7s
Build Docker and Deploy / Run Unit-Tests (push) Failing after 10m56s
Build Docker and Deploy / Deploy to Server (push) Has been skipped
2025-12-03 22:38:24 +01:00
20 changed files with 143 additions and 225 deletions

View File

@@ -1,6 +1,11 @@
package handler package handler
import ( import (
"database/sql"
"errors"
"fmt"
"net/http"
"blackforestbytes.com/simplecloudnotifier/api/apierr" "blackforestbytes.com/simplecloudnotifier/api/apierr"
hl "blackforestbytes.com/simplecloudnotifier/api/apihighlight" hl "blackforestbytes.com/simplecloudnotifier/api/apihighlight"
"blackforestbytes.com/simplecloudnotifier/api/ginresp" "blackforestbytes.com/simplecloudnotifier/api/ginresp"
@@ -8,13 +13,9 @@ import (
primarydb "blackforestbytes.com/simplecloudnotifier/db/impl/primary" primarydb "blackforestbytes.com/simplecloudnotifier/db/impl/primary"
"blackforestbytes.com/simplecloudnotifier/logic" "blackforestbytes.com/simplecloudnotifier/logic"
"blackforestbytes.com/simplecloudnotifier/models" "blackforestbytes.com/simplecloudnotifier/models"
"database/sql"
"errors"
"fmt"
"git.blackforestbytes.com/BlackForestBytes/goext/dataext" "git.blackforestbytes.com/BlackForestBytes/goext/dataext"
"git.blackforestbytes.com/BlackForestBytes/goext/ginext" "git.blackforestbytes.com/BlackForestBytes/goext/ginext"
"git.blackforestbytes.com/BlackForestBytes/goext/langext" "git.blackforestbytes.com/BlackForestBytes/goext/langext"
"net/http"
) )
type CompatHandler struct { type CompatHandler struct {
@@ -90,7 +91,7 @@ func (h CompatHandler) SendMessage(pctx ginext.PreContext) ginext.HTTPResponse {
return ginresp.SendAPIError(g, 400, apierr.USER_NOT_FOUND, hl.USER_ID, "User not found (compat)", nil) return ginresp.SendAPIError(g, 400, apierr.USER_NOT_FOUND, hl.USER_ID, "User not found (compat)", nil)
} }
okResp, errResp := h.app.SendMessage(g, ctx, langext.Ptr(models.UserID(*newid)), data.UserKey, nil, data.Title, data.Content, data.Priority, data.UserMessageID, data.SendTimestamp, nil) okResp, errResp := h.app.SendMessage(g, ctx, data.UserKey, nil, data.Title, data.Content, data.Priority, data.UserMessageID, data.SendTimestamp, nil)
if errResp != nil { if errResp != nil {
return *errResp return *errResp
} else { } else {

View File

@@ -1,16 +1,17 @@
package handler package handler
import ( import (
"fmt"
"net/http"
"time"
"blackforestbytes.com/simplecloudnotifier/api/apierr" "blackforestbytes.com/simplecloudnotifier/api/apierr"
"blackforestbytes.com/simplecloudnotifier/api/ginresp" "blackforestbytes.com/simplecloudnotifier/api/ginresp"
primarydb "blackforestbytes.com/simplecloudnotifier/db/impl/primary" primarydb "blackforestbytes.com/simplecloudnotifier/db/impl/primary"
"blackforestbytes.com/simplecloudnotifier/logic" "blackforestbytes.com/simplecloudnotifier/logic"
"blackforestbytes.com/simplecloudnotifier/models" "blackforestbytes.com/simplecloudnotifier/models"
"fmt"
"git.blackforestbytes.com/BlackForestBytes/goext/ginext" "git.blackforestbytes.com/BlackForestBytes/goext/ginext"
"git.blackforestbytes.com/BlackForestBytes/goext/langext" "git.blackforestbytes.com/BlackForestBytes/goext/langext"
"net/http"
"time"
) )
type ExternalHandler struct { type ExternalHandler struct {
@@ -36,14 +37,13 @@ func NewExternalHandler(app *logic.Application) ExternalHandler {
// //
// @Success 200 {object} handler.UptimeKuma.response // @Success 200 {object} handler.UptimeKuma.response
// @Failure 400 {object} ginresp.apiError // @Failure 400 {object} ginresp.apiError
// @Failure 401 {object} ginresp.apiError "The user_id was not found or the user_key is wrong" // @Failure 401 {object} ginresp.apiError "The user_key is wrong"
// @Failure 403 {object} ginresp.apiError "The user has exceeded its daily quota - wait 24 hours or upgrade your account" // @Failure 403 {object} ginresp.apiError "The user has exceeded its daily quota - wait 24 hours or upgrade your account"
// @Failure 500 {object} ginresp.apiError "An internal server error occurred - try again later" // @Failure 500 {object} ginresp.apiError "An internal server error occurred - try again later"
// //
// @Router /external/v1/uptime-kuma [POST] // @Router /external/v1/uptime-kuma [POST]
func (h ExternalHandler) UptimeKuma(pctx ginext.PreContext) ginext.HTTPResponse { func (h ExternalHandler) UptimeKuma(pctx ginext.PreContext) ginext.HTTPResponse {
type query struct { type query struct {
UserID *models.UserID `form:"user_id" example:"7725"`
KeyToken *string `form:"key" example:"P3TNH8mvv14fm"` KeyToken *string `form:"key" example:"P3TNH8mvv14fm"`
Channel *string `form:"channel"` Channel *string `form:"channel"`
ChannelUp *string `form:"channel_up"` ChannelUp *string `form:"channel_up"`
@@ -125,7 +125,7 @@ func (h ExternalHandler) UptimeKuma(pctx ginext.PreContext) ginext.HTTPResponse
priority = q.PriorityDown priority = q.PriorityDown
} }
okResp, errResp := h.app.SendMessage(g, ctx, q.UserID, q.KeyToken, channel, &title, &content, priority, nil, timestamp, q.SenderName) okResp, errResp := h.app.SendMessage(g, ctx, q.KeyToken, channel, &title, &content, priority, nil, timestamp, q.SenderName)
if errResp != nil { if errResp != nil {
return *errResp return *errResp
} }

View File

@@ -1,6 +1,8 @@
package handler package handler
import ( import (
"net/http"
"blackforestbytes.com/simplecloudnotifier/api/apierr" "blackforestbytes.com/simplecloudnotifier/api/apierr"
primarydb "blackforestbytes.com/simplecloudnotifier/db/impl/primary" primarydb "blackforestbytes.com/simplecloudnotifier/db/impl/primary"
"blackforestbytes.com/simplecloudnotifier/logic" "blackforestbytes.com/simplecloudnotifier/logic"
@@ -8,7 +10,6 @@ import (
"git.blackforestbytes.com/BlackForestBytes/goext/dataext" "git.blackforestbytes.com/BlackForestBytes/goext/dataext"
"git.blackforestbytes.com/BlackForestBytes/goext/ginext" "git.blackforestbytes.com/BlackForestBytes/goext/ginext"
"git.blackforestbytes.com/BlackForestBytes/goext/langext" "git.blackforestbytes.com/BlackForestBytes/goext/langext"
"net/http"
) )
type SendMessageResponse struct { type SendMessageResponse struct {
@@ -42,7 +43,7 @@ func NewMessageHandler(app *logic.Application) MessageHandler {
// //
// @Success 200 {object} handler.SendMessage.response // @Success 200 {object} handler.SendMessage.response
// @Failure 400 {object} ginresp.apiError // @Failure 400 {object} ginresp.apiError
// @Failure 401 {object} ginresp.apiError "The user_id was not found or the user_key is wrong" // @Failure 401 {object} ginresp.apiError "The user_key is wrong"
// @Failure 403 {object} ginresp.apiError "The user has exceeded its daily quota - wait 24 hours or upgrade your account" // @Failure 403 {object} ginresp.apiError "The user has exceeded its daily quota - wait 24 hours or upgrade your account"
// @Failure 500 {object} ginresp.apiError "An internal server error occurred - try again later" // @Failure 500 {object} ginresp.apiError "An internal server error occurred - try again later"
// //
@@ -50,7 +51,6 @@ func NewMessageHandler(app *logic.Application) MessageHandler {
// @Router /send [POST] // @Router /send [POST]
func (h MessageHandler) SendMessage(pctx ginext.PreContext) ginext.HTTPResponse { func (h MessageHandler) SendMessage(pctx ginext.PreContext) ginext.HTTPResponse {
type combined struct { type combined struct {
UserID *models.UserID `json:"user_id" form:"user_id" example:"7725" `
KeyToken *string `json:"key" form:"key" example:"P3TNH8mvv14fm" ` KeyToken *string `json:"key" form:"key" example:"P3TNH8mvv14fm" `
Channel *string `json:"channel" form:"channel" example:"test" ` Channel *string `json:"channel" form:"channel" example:"test" `
Title *string `json:"title" form:"title" example:"Hello World" ` Title *string `json:"title" form:"title" example:"Hello World" `
@@ -88,7 +88,7 @@ func (h MessageHandler) SendMessage(pctx ginext.PreContext) ginext.HTTPResponse
// query has highest prio, then form, then json // query has highest prio, then form, then json
data := dataext.ObjectMerge(dataext.ObjectMerge(b, f), q) data := dataext.ObjectMerge(dataext.ObjectMerge(b, f), q)
okResp, errResp := h.app.SendMessage(g, ctx, data.UserID, data.KeyToken, data.Channel, data.Title, data.Content, data.Priority, data.UserMessageID, data.SendTimestamp, data.SenderName) okResp, errResp := h.app.SendMessage(g, ctx, data.KeyToken, data.Channel, data.Title, data.Content, data.Priority, data.UserMessageID, data.SendTimestamp, data.SenderName)
if errResp != nil { if errResp != nil {
return *errResp return *errResp
} else { } else {

View File

@@ -1,12 +1,13 @@
package primary package primary
import ( import (
"time"
scn "blackforestbytes.com/simplecloudnotifier" scn "blackforestbytes.com/simplecloudnotifier"
"blackforestbytes.com/simplecloudnotifier/db" "blackforestbytes.com/simplecloudnotifier/db"
"blackforestbytes.com/simplecloudnotifier/models" "blackforestbytes.com/simplecloudnotifier/models"
"git.blackforestbytes.com/BlackForestBytes/goext/langext" "git.blackforestbytes.com/BlackForestBytes/goext/langext"
"git.blackforestbytes.com/BlackForestBytes/goext/sq" "git.blackforestbytes.com/BlackForestBytes/goext/sq"
"time"
) )
func (db *Database) CreateUser(ctx db.TxContext, protoken *string, username *string) (models.User, error) { func (db *Database) CreateUser(ctx db.TxContext, protoken *string, username *string) (models.User, error) {
@@ -63,6 +64,15 @@ func (db *Database) GetUser(ctx db.TxContext, userid models.UserID) (models.User
return sq.QuerySingle[models.User](ctx, tx, "SELECT * FROM users WHERE user_id = :uid AND deleted=0 LIMIT 1", sq.PP{"uid": userid}, sq.SModeExtended, sq.Safe) return sq.QuerySingle[models.User](ctx, tx, "SELECT * FROM users WHERE user_id = :uid AND deleted=0 LIMIT 1", sq.PP{"uid": userid}, sq.SModeExtended, sq.Safe)
} }
func (db *Database) GetUserByKey(ctx db.TxContext, key string) (models.User, error) {
tx, err := ctx.GetOrCreateTransaction(db)
if err != nil {
return models.User{}, err
}
return sq.QuerySingle[models.User](ctx, tx, "SELECT * FROM users WHERE EXISTS(SELECT keytokens.keytoken_id FROM keytokens WHERE keytokens.token = :tok AND users.user_id = keytokens.owner_user_id AND keytokens.deleted=0) AND users.deleted=0 LIMIT 1", sq.PP{"tok": key}, sq.SModeExtended, sq.Safe)
}
func (db *Database) GetUserOpt(ctx db.TxContext, userid models.UserID) (*models.User, error) { func (db *Database) GetUserOpt(ctx db.TxContext, userid models.UserID) (*models.User, error) {
tx, err := ctx.GetOrCreateTransaction(db) tx, err := ctx.GetOrCreateTransaction(db)
if err != nil { if err != nil {

View File

@@ -1,21 +1,22 @@
package logic package logic
import ( import (
"database/sql"
"errors"
"fmt"
"strings"
"time"
"blackforestbytes.com/simplecloudnotifier/api/apierr" "blackforestbytes.com/simplecloudnotifier/api/apierr"
hl "blackforestbytes.com/simplecloudnotifier/api/apihighlight" hl "blackforestbytes.com/simplecloudnotifier/api/apihighlight"
"blackforestbytes.com/simplecloudnotifier/api/ginresp" "blackforestbytes.com/simplecloudnotifier/api/ginresp"
"blackforestbytes.com/simplecloudnotifier/models" "blackforestbytes.com/simplecloudnotifier/models"
"database/sql"
"errors"
"fmt"
"git.blackforestbytes.com/BlackForestBytes/goext/ginext" "git.blackforestbytes.com/BlackForestBytes/goext/ginext"
"git.blackforestbytes.com/BlackForestBytes/goext/langext" "git.blackforestbytes.com/BlackForestBytes/goext/langext"
"git.blackforestbytes.com/BlackForestBytes/goext/mathext" "git.blackforestbytes.com/BlackForestBytes/goext/mathext"
"git.blackforestbytes.com/BlackForestBytes/goext/timeext" "git.blackforestbytes.com/BlackForestBytes/goext/timeext"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"strings"
"time"
) )
type SendMessageResponse struct { type SendMessageResponse struct {
@@ -25,7 +26,7 @@ type SendMessageResponse struct {
CompatMessageID int64 CompatMessageID int64
} }
func (app *Application) SendMessage(g *gin.Context, ctx *AppContext, UserID *models.UserID, Key *string, Channel *string, Title *string, Content *string, Priority *int, UserMessageID *string, SendTimestamp *float64, SenderName *string) (*SendMessageResponse, *ginext.HTTPResponse) { func (app *Application) SendMessage(g *gin.Context, ctx *AppContext, Key *string, Channel *string, Title *string, Content *string, Priority *int, UserMessageID *string, SendTimestamp *float64, SenderName *string) (*SendMessageResponse, *ginext.HTTPResponse) {
if Title != nil { if Title != nil {
Title = langext.Ptr(strings.TrimSpace(*Title)) Title = langext.Ptr(strings.TrimSpace(*Title))
} }
@@ -33,9 +34,6 @@ func (app *Application) SendMessage(g *gin.Context, ctx *AppContext, UserID *mod
UserMessageID = langext.Ptr(strings.TrimSpace(*UserMessageID)) UserMessageID = langext.Ptr(strings.TrimSpace(*UserMessageID))
} }
if UserID == nil {
return nil, langext.Ptr(ginresp.SendAPIError(g, 400, apierr.MISSING_UID, hl.USER_ID, "Missing parameter [[user_id]]", nil))
}
if Key == nil { if Key == nil {
return nil, langext.Ptr(ginresp.SendAPIError(g, 400, apierr.MISSING_TOK, hl.USER_KEY, "Missing parameter [[key]]", nil)) return nil, langext.Ptr(ginresp.SendAPIError(g, 400, apierr.MISSING_TOK, hl.USER_KEY, "Missing parameter [[key]]", nil))
} }
@@ -49,9 +47,9 @@ func (app *Application) SendMessage(g *gin.Context, ctx *AppContext, UserID *mod
return nil, langext.Ptr(ginresp.SendAPIError(g, 400, apierr.NO_TITLE, hl.TITLE, "No title specified", nil)) return nil, langext.Ptr(ginresp.SendAPIError(g, 400, apierr.NO_TITLE, hl.TITLE, "No title specified", nil))
} }
user, err := app.Database.Primary.GetUser(ctx, *UserID) user, err := app.Database.Primary.GetUserByKey(ctx, *Key)
if errors.Is(err, sql.ErrNoRows) { if errors.Is(err, sql.ErrNoRows) {
return nil, langext.Ptr(ginresp.SendAPIError(g, 400, apierr.USER_NOT_FOUND, hl.USER_ID, "User not found", err)) return nil, langext.Ptr(ginresp.SendAPIError(g, 401, apierr.USER_AUTH_FAILED, hl.USER_KEY, "Key not found or not valid", err))
} }
if err != nil { if err != nil {
return nil, langext.Ptr(ginresp.SendAPIError(g, 500, apierr.DATABASE_ERROR, hl.NONE, "Failed to query user", err)) return nil, langext.Ptr(ginresp.SendAPIError(g, 500, apierr.DATABASE_ERROR, hl.NONE, "Failed to query user", err))
@@ -126,7 +124,7 @@ func (app *Application) SendMessage(g *gin.Context, ctx *AppContext, UserID *mod
return nil, langext.Ptr(ginresp.SendAPIError(g, 403, apierr.QUOTA_REACHED, hl.NONE, fmt.Sprintf("Daily quota reached (%d)", user.QuotaPerDay()), nil)) return nil, langext.Ptr(ginresp.SendAPIError(g, 403, apierr.QUOTA_REACHED, hl.NONE, fmt.Sprintf("Daily quota reached (%d)", user.QuotaPerDay()), nil))
} }
channel, err := app.GetOrCreateChannel(ctx, *UserID, channelDisplayName, channelInternalName) channel, err := app.GetOrCreateChannel(ctx, user.UserID, channelDisplayName, channelInternalName)
if err != nil { if err != nil {
return nil, langext.Ptr(ginresp.SendAPIError(g, 500, apierr.DATABASE_ERROR, hl.NONE, "Failed to query/create (owned) channel", err)) return nil, langext.Ptr(ginresp.SendAPIError(g, 500, apierr.DATABASE_ERROR, hl.NONE, "Failed to query/create (owned) channel", err))
} }
@@ -145,7 +143,7 @@ func (app *Application) SendMessage(g *gin.Context, ctx *AppContext, UserID *mod
clientIP := g.ClientIP() clientIP := g.ClientIP()
msg, err := app.Database.Primary.CreateMessage(ctx, *UserID, channel, sendTimestamp, *Title, Content, priority, UserMessageID, clientIP, SenderName, keytok.KeyTokenID) msg, err := app.Database.Primary.CreateMessage(ctx, user.UserID, channel, sendTimestamp, *Title, Content, priority, UserMessageID, clientIP, SenderName, keytok.KeyTokenID)
if err != nil { if err != nil {
return nil, langext.Ptr(ginresp.SendAPIError(g, 500, apierr.DATABASE_ERROR, hl.NONE, "Failed to create message in db", err)) return nil, langext.Ptr(ginresp.SendAPIError(g, 500, apierr.DATABASE_ERROR, hl.NONE, "Failed to create message in db", err))
} }
@@ -176,7 +174,7 @@ func (app *Application) SendMessage(g *gin.Context, ctx *AppContext, UserID *mod
return nil, langext.Ptr(ginresp.SendAPIError(g, 500, apierr.DATABASE_ERROR, hl.NONE, "Failed to inc token msg-counter", err)) return nil, langext.Ptr(ginresp.SendAPIError(g, 500, apierr.DATABASE_ERROR, hl.NONE, "Failed to inc token msg-counter", err))
} }
log.Info().Msg(fmt.Sprintf("Sending new notification %s for user %s (to %d active subscriptions)", msg.MessageID, UserID, len(activeSubscriptions))) log.Info().Msg(fmt.Sprintf("Sending new notification %s for user %s (to %d active subscriptions)", msg.MessageID, user.UserID, len(activeSubscriptions)))
for _, sub := range activeSubscriptions { for _, sub := range activeSubscriptions {
clients, err := app.Database.Primary.ListClients(ctx, sub.SubscriberUserID) clients, err := app.Database.Primary.ListClients(ctx, sub.SubscriberUserID)

View File

@@ -1143,7 +1143,6 @@ func TestChannelMessageCounter(t *testing.T) {
tt.RequestPost[gin.H](t, baseUrl, "/", gin.H{ tt.RequestPost[gin.H](t, baseUrl, "/", gin.H{
"key": admintok, "key": admintok,
"user_id": uid,
"title": tt.ShortLipsum(1001, 1), "title": tt.ShortLipsum(1001, 1),
}) })
@@ -1172,7 +1171,6 @@ func TestChannelMessageCounter(t *testing.T) {
tt.RequestPost[gin.H](t, baseUrl, "/", gin.H{ tt.RequestPost[gin.H](t, baseUrl, "/", gin.H{
"key": admintok, "key": admintok,
"user_id": uid,
"title": tt.ShortLipsum(1002, 1), "title": tt.ShortLipsum(1002, 1),
}) })
@@ -1180,19 +1178,16 @@ func TestChannelMessageCounter(t *testing.T) {
tt.RequestPost[gin.H](t, baseUrl, "/", gin.H{ tt.RequestPost[gin.H](t, baseUrl, "/", gin.H{
"key": admintok, "key": admintok,
"user_id": uid,
"channel": "Chan1", "channel": "Chan1",
"title": tt.ShortLipsum(1003, 1), "title": tt.ShortLipsum(1003, 1),
}) })
tt.RequestPost[gin.H](t, baseUrl, "/", gin.H{ tt.RequestPost[gin.H](t, baseUrl, "/", gin.H{
"key": admintok, "key": admintok,
"user_id": uid,
"channel": "Chan2", "channel": "Chan2",
"title": tt.ShortLipsum(1004, 1), "title": tt.ShortLipsum(1004, 1),
}) })
tt.RequestPost[gin.H](t, baseUrl, "/", gin.H{ tt.RequestPost[gin.H](t, baseUrl, "/", gin.H{
"key": admintok, "key": admintok,
"user_id": uid,
"channel": "Chan2", "channel": "Chan2",
"title": tt.ShortLipsum(1005, 1), "title": tt.ShortLipsum(1005, 1),
}) })

View File

@@ -126,7 +126,6 @@ func TestTokenKeys(t *testing.T) {
msg1s := tt.RequestPost[gin.H](t, baseUrl, "/", gin.H{ msg1s := tt.RequestPost[gin.H](t, baseUrl, "/", gin.H{
"key": key7.Token, "key": key7.Token,
"user_id": data.UID,
"channel": "testchan1", "channel": "testchan1",
"title": "HelloWorld_001", "title": "HelloWorld_001",
}) })
@@ -137,14 +136,12 @@ func TestTokenKeys(t *testing.T) {
tt.RequestPostShouldFail(t, baseUrl, "/", gin.H{ tt.RequestPostShouldFail(t, baseUrl, "/", gin.H{
"key": key7.Token, "key": key7.Token,
"user_id": data.UID,
"channel": "testchan2", "channel": "testchan2",
"title": "HelloWorld_001", "title": "HelloWorld_001",
}, 401, apierr.USER_AUTH_FAILED) // wrong channel }, 401, apierr.USER_AUTH_FAILED) // wrong channel
tt.RequestPostShouldFail(t, baseUrl, "/", gin.H{ tt.RequestPostShouldFail(t, baseUrl, "/", gin.H{
"key": key7.Token, "key": key7.Token,
"user_id": data.UID,
"title": "HelloWorld_001", "title": "HelloWorld_001",
}, 401, apierr.USER_AUTH_FAILED) // no channel (=main) }, 401, apierr.USER_AUTH_FAILED) // no channel (=main)
@@ -161,7 +158,6 @@ func TestTokenKeys(t *testing.T) {
tt.RequestPostShouldFail(t, baseUrl, "/", gin.H{ tt.RequestPostShouldFail(t, baseUrl, "/", gin.H{
"key": key8.Token, "key": key8.Token,
"user_id": data.UID,
"title": "HelloWorld_001", "title": "HelloWorld_001",
}, 401, apierr.USER_AUTH_FAILED) // no send perm }, 401, apierr.USER_AUTH_FAILED) // no send perm
@@ -470,14 +466,12 @@ func TestTokenKeysPermissions(t *testing.T) {
tt.RequestPostShouldFail(t, baseUrl, "/", gin.H{ tt.RequestPostShouldFail(t, baseUrl, "/", gin.H{
"key": key7.Token, "key": key7.Token,
"user_id": data.UID,
"channel": "testchan2", "channel": "testchan2",
"title": "HelloWorld_001", "title": "HelloWorld_001",
}, 401, apierr.USER_AUTH_FAILED) // wrong channel }, 401, apierr.USER_AUTH_FAILED) // wrong channel
tt.RequestPostShouldFail(t, baseUrl, "/", gin.H{ tt.RequestPostShouldFail(t, baseUrl, "/", gin.H{
"key": key7.Token, "key": key7.Token,
"user_id": data.UID,
"title": "HelloWorld_001", "title": "HelloWorld_001",
}, 401, apierr.USER_AUTH_FAILED) // no channel (=main) }, 401, apierr.USER_AUTH_FAILED) // no channel (=main)
@@ -494,7 +488,6 @@ func TestTokenKeysPermissions(t *testing.T) {
tt.RequestPostShouldFail(t, baseUrl, "/", gin.H{ tt.RequestPostShouldFail(t, baseUrl, "/", gin.H{
"key": key8.Token, "key": key8.Token,
"user_id": data.UID,
"title": "HelloWorld_001", "title": "HelloWorld_001",
}, 401, apierr.USER_AUTH_FAILED) // no send perm }, 401, apierr.USER_AUTH_FAILED) // no send perm
@@ -551,7 +544,6 @@ func TestTokenKeysMessageCounter(t *testing.T) {
tt.RequestPost[gin.H](t, baseUrl, "/", gin.H{ tt.RequestPost[gin.H](t, baseUrl, "/", gin.H{
"key": admintok, "key": admintok,
"user_id": uid,
"title": tt.ShortLipsum(1001, 1), "title": tt.ShortLipsum(1001, 1),
}) })
@@ -559,7 +551,6 @@ func TestTokenKeysMessageCounter(t *testing.T) {
tt.RequestPost[gin.H](t, baseUrl, "/", gin.H{ tt.RequestPost[gin.H](t, baseUrl, "/", gin.H{
"key": admintok, "key": admintok,
"user_id": uid,
"title": tt.ShortLipsum(1002, 1), "title": tt.ShortLipsum(1002, 1),
}) })
@@ -567,7 +558,6 @@ func TestTokenKeysMessageCounter(t *testing.T) {
tt.RequestPost[gin.H](t, baseUrl, "/", gin.H{ tt.RequestPost[gin.H](t, baseUrl, "/", gin.H{
"key": sendtok, "key": sendtok,
"user_id": uid,
"title": tt.ShortLipsum(1002, 1), "title": tt.ShortLipsum(1002, 1),
}) })
@@ -575,19 +565,16 @@ func TestTokenKeysMessageCounter(t *testing.T) {
tt.RequestPost[gin.H](t, baseUrl, "/", gin.H{ tt.RequestPost[gin.H](t, baseUrl, "/", gin.H{
"key": sendtok, "key": sendtok,
"user_id": uid,
"channel": "Chan1", "channel": "Chan1",
"title": tt.ShortLipsum(1003, 1), "title": tt.ShortLipsum(1003, 1),
}) })
tt.RequestPost[gin.H](t, baseUrl, "/", gin.H{ tt.RequestPost[gin.H](t, baseUrl, "/", gin.H{
"key": sendtok, "key": sendtok,
"user_id": uid,
"channel": "Chan2", "channel": "Chan2",
"title": tt.ShortLipsum(1004, 1), "title": tt.ShortLipsum(1004, 1),
}) })
tt.RequestPost[gin.H](t, baseUrl, "/", gin.H{ tt.RequestPost[gin.H](t, baseUrl, "/", gin.H{
"key": sendtok, "key": sendtok,
"user_id": uid,
"channel": "Chan2", "channel": "Chan2",
"title": tt.ShortLipsum(1005, 1), "title": tt.ShortLipsum(1005, 1),
}) })
@@ -597,7 +584,6 @@ func TestTokenKeysMessageCounter(t *testing.T) {
tt.RequestPost[gin.H](t, baseUrl, "/", gin.H{ tt.RequestPost[gin.H](t, baseUrl, "/", gin.H{
"key": admintok, "key": admintok,
"user_id": uid,
"channel": "Chan2", "channel": "Chan2",
"title": tt.ShortLipsum(1004, 1), "title": tt.ShortLipsum(1004, 1),
}) })
@@ -606,7 +592,6 @@ func TestTokenKeysMessageCounter(t *testing.T) {
tt.RequestPost[gin.H](t, baseUrl, "/", gin.H{ tt.RequestPost[gin.H](t, baseUrl, "/", gin.H{
"key": admintok, "key": admintok,
"user_id": uid,
"title": tt.ShortLipsum(1002, 1), "title": tt.ShortLipsum(1002, 1),
}) })

View File

@@ -2,11 +2,13 @@ package test
import ( import (
"database/sql" "database/sql"
"os"
"testing"
tt "blackforestbytes.com/simplecloudnotifier/test/util"
"git.blackforestbytes.com/BlackForestBytes/goext/exerr" "git.blackforestbytes.com/BlackForestBytes/goext/exerr"
"git.blackforestbytes.com/BlackForestBytes/goext/langext" "git.blackforestbytes.com/BlackForestBytes/goext/langext"
"github.com/glebarez/go-sqlite" "github.com/glebarez/go-sqlite"
"os"
"testing"
) )
func TestMain(m *testing.M) { func TestMain(m *testing.M) {
@@ -20,3 +22,10 @@ func TestMain(m *testing.M) {
os.Exit(m.Run()) os.Exit(m.Run())
} }
func TestInitFactory(t *testing.T) {
ws, _, stop := tt.StartSimpleWebserver(t)
defer stop()
tt.InitDefaultData(t, ws)
}

View File

@@ -418,13 +418,11 @@ func TestDeleteMessage(t *testing.T) {
"fcm_token": "DUMMY_FCM", "fcm_token": "DUMMY_FCM",
}) })
uid := r0["user_id"].(string)
sendtok := r0["send_key"].(string) sendtok := r0["send_key"].(string)
admintok := r0["admin_key"].(string) admintok := r0["admin_key"].(string)
msg1 := tt.RequestPost[gin.H](t, baseUrl, "/", gin.H{ msg1 := tt.RequestPost[gin.H](t, baseUrl, "/", gin.H{
"key": sendtok, "key": sendtok,
"user_id": uid,
"title": "Message_1", "title": "Message_1",
}) })
@@ -446,13 +444,11 @@ func TestDeleteMessageAndResendUsrMsgId(t *testing.T) {
"fcm_token": "DUMMY_FCM", "fcm_token": "DUMMY_FCM",
}) })
uid := r0["user_id"].(string)
sendtok := r0["send_key"].(string) sendtok := r0["send_key"].(string)
admintok := r0["admin_key"].(string) admintok := r0["admin_key"].(string)
msg1 := tt.RequestPost[gin.H](t, baseUrl, "/", gin.H{ msg1 := tt.RequestPost[gin.H](t, baseUrl, "/", gin.H{
"key": sendtok, "key": sendtok,
"user_id": uid,
"title": "Message_1", "title": "Message_1",
"msg_id": "bef8dd3d-078e-4f89-abf4-5258ad22a2e4", "msg_id": "bef8dd3d-078e-4f89-abf4-5258ad22a2e4",
}) })
@@ -463,7 +459,6 @@ func TestDeleteMessageAndResendUsrMsgId(t *testing.T) {
msg2 := tt.RequestPost[gin.H](t, baseUrl, "/", gin.H{ msg2 := tt.RequestPost[gin.H](t, baseUrl, "/", gin.H{
"key": sendtok, "key": sendtok,
"user_id": uid,
"title": "Message_1", "title": "Message_1",
"msg_id": "bef8dd3d-078e-4f89-abf4-5258ad22a2e4", "msg_id": "bef8dd3d-078e-4f89-abf4-5258ad22a2e4",
}) })
@@ -476,7 +471,6 @@ func TestDeleteMessageAndResendUsrMsgId(t *testing.T) {
msg3 := tt.RequestPost[gin.H](t, baseUrl, "/", gin.H{ msg3 := tt.RequestPost[gin.H](t, baseUrl, "/", gin.H{
"key": sendtok, "key": sendtok,
"user_id": uid,
"title": "Message_1", "title": "Message_1",
"msg_id": "bef8dd3d-078e-4f89-abf4-5258ad22a2e4", "msg_id": "bef8dd3d-078e-4f89-abf4-5258ad22a2e4",
}) })
@@ -493,7 +487,6 @@ func TestGetMessageSimple(t *testing.T) {
msgOut := tt.RequestPost[gin.H](t, baseUrl, "/", gin.H{ msgOut := tt.RequestPost[gin.H](t, baseUrl, "/", gin.H{
"key": data.User[0].SendKey, "key": data.User[0].SendKey,
"user_id": data.User[0].UID,
"title": "Message_1", "title": "Message_1",
}) })
@@ -533,7 +526,6 @@ func TestGetMessageFull(t *testing.T) {
msgOut := tt.RequestPost[gin.H](t, baseUrl, "/", gin.H{ msgOut := tt.RequestPost[gin.H](t, baseUrl, "/", gin.H{
"key": data.User[0].SendKey, "key": data.User[0].SendKey,
"user_id": data.User[0].UID,
"title": "Message_1", "title": "Message_1",
"content": content, "content": content,
"channel": "demo-channel-007", "channel": "demo-channel-007",
@@ -948,7 +940,6 @@ func TestDeactivatedSubscriptionListMessages(t *testing.T) {
newMessageTitle := langext.RandBase62(48) newMessageTitle := langext.RandBase62(48)
tt.RequestPost[gin.H](t, baseUrl, "/", gin.H{ tt.RequestPost[gin.H](t, baseUrl, "/", gin.H{
"key": user15.AdminKey, "key": user15.AdminKey,
"user_id": user15.UID,
"channel": chanName, "channel": chanName,
"title": newMessageTitle, "title": newMessageTitle,
}) })
@@ -1122,7 +1113,6 @@ func TestActiveSubscriptionListMessages(t *testing.T) {
newMessageTitle := langext.RandBase62(48) newMessageTitle := langext.RandBase62(48)
tt.RequestPost[gin.H](t, baseUrl, "/", gin.H{ tt.RequestPost[gin.H](t, baseUrl, "/", gin.H{
"key": user15.AdminKey, "key": user15.AdminKey,
"user_id": user15.UID,
"channel": chanName, "channel": chanName,
"title": newMessageTitle, "title": newMessageTitle,
}) })
@@ -1176,7 +1166,6 @@ func TestUnconfirmedSubscriptionListMessages(t *testing.T) {
newMessageTitle := langext.RandBase62(48) newMessageTitle := langext.RandBase62(48)
tt.RequestPost[gin.H](t, baseUrl, "/", gin.H{ tt.RequestPost[gin.H](t, baseUrl, "/", gin.H{
"key": user15.AdminKey, "key": user15.AdminKey,
"user_id": user15.UID,
"channel": chanName, "channel": chanName,
"title": newMessageTitle, "title": newMessageTitle,
}) })
@@ -1229,7 +1218,7 @@ func TestListMessagesSubscriptionStatusAllInactiveSubscription(t *testing.T) {
subscriptionID, _ := tt.FindSubscriptionByChanName(t, baseUrl, user14, user15.UID, chanName) subscriptionID, _ := tt.FindSubscriptionByChanName(t, baseUrl, user14, user15.UID, chanName)
newMessageTitle := langext.RandBase62(48) newMessageTitle := langext.RandBase62(48)
tt.RequestPost[gin.H](t, baseUrl, "/", gin.H{"key": user15.AdminKey, "user_id": user15.UID, "channel": chanName, "title": newMessageTitle}) tt.RequestPost[gin.H](t, baseUrl, "/", gin.H{"key": user15.AdminKey, "channel": chanName, "title": newMessageTitle})
type msg struct { type msg struct {
MessageId string `json:"message_id"` MessageId string `json:"message_id"`
@@ -1282,7 +1271,7 @@ func TestListMessagesSubscriptionStatusAllNoSubscription(t *testing.T) {
chan2 := data.User[0].Channels[2] chan2 := data.User[0].Channels[2]
newMessageTitle := langext.RandBase62(48) newMessageTitle := langext.RandBase62(48)
tt.RequestPost[gin.H](t, baseUrl, "/", gin.H{"key": user0.AdminKey, "user_id": user0.UID, "channel": chan2.InternalName, "title": newMessageTitle}) tt.RequestPost[gin.H](t, baseUrl, "/", gin.H{"key": user0.AdminKey, "channel": chan2.InternalName, "title": newMessageTitle})
{ {
messages := tt.RequestAuthGet[mglist](t, user0.AdminKey, baseUrl, "/api/v2/messages") messages := tt.RequestAuthGet[mglist](t, user0.AdminKey, baseUrl, "/api/v2/messages")

View File

@@ -1,18 +1,18 @@
package test package test
import ( import (
"blackforestbytes.com/simplecloudnotifier/api/apierr"
"blackforestbytes.com/simplecloudnotifier/models"
"blackforestbytes.com/simplecloudnotifier/push"
tt "blackforestbytes.com/simplecloudnotifier/test/util"
"fmt" "fmt"
"git.blackforestbytes.com/BlackForestBytes/goext/langext"
"github.com/gin-gonic/gin"
"math/rand/v2" "math/rand/v2"
"net/url" "net/url"
"strings" "strings"
"testing" "testing"
"time" "time"
"blackforestbytes.com/simplecloudnotifier/api/apierr"
"blackforestbytes.com/simplecloudnotifier/push"
tt "blackforestbytes.com/simplecloudnotifier/test/util"
"git.blackforestbytes.com/BlackForestBytes/goext/langext"
"github.com/gin-gonic/gin"
) )
func TestSendSimpleMessageJSON(t *testing.T) { func TestSendSimpleMessageJSON(t *testing.T) {
@@ -28,26 +28,22 @@ func TestSendSimpleMessageJSON(t *testing.T) {
"fcm_token": "DUMMY_FCM", "fcm_token": "DUMMY_FCM",
}) })
uid := r0["user_id"].(string)
admintok := r0["admin_key"].(string) admintok := r0["admin_key"].(string)
readtok := r0["read_key"].(string) readtok := r0["read_key"].(string)
sendtok := r0["send_key"].(string) sendtok := r0["send_key"].(string)
msg1 := tt.RequestPost[gin.H](t, baseUrl, "/", gin.H{ msg1 := tt.RequestPost[gin.H](t, baseUrl, "/", gin.H{
"key": sendtok, "key": sendtok,
"user_id": uid,
"title": "HelloWorld_001", "title": "HelloWorld_001",
}) })
tt.RequestPostShouldFail(t, baseUrl, "/", gin.H{ tt.RequestPostShouldFail(t, baseUrl, "/", gin.H{
"key": readtok, "key": readtok,
"user_id": uid,
"title": "HelloWorld_001", "title": "HelloWorld_001",
}, 401, apierr.USER_AUTH_FAILED) }, 401, apierr.USER_AUTH_FAILED)
tt.RequestPostShouldFail(t, baseUrl, "/", gin.H{ tt.RequestPostShouldFail(t, baseUrl, "/", gin.H{
"key": "asdf", "key": "asdf",
"user_id": uid,
"title": "HelloWorld_001", "title": "HelloWorld_001",
}, 401, apierr.USER_AUTH_FAILED) }, 401, apierr.USER_AUTH_FAILED)
@@ -117,13 +113,11 @@ func TestSendSimpleMessageForm(t *testing.T) {
"fcm_token": "DUMMY_FCM", "fcm_token": "DUMMY_FCM",
}) })
uid := r0["user_id"].(string)
admintok := r0["admin_key"].(string) admintok := r0["admin_key"].(string)
sendtok := r0["send_key"].(string) sendtok := r0["send_key"].(string)
msg1 := tt.RequestPost[gin.H](t, baseUrl, "/", tt.FormData{ msg1 := tt.RequestPost[gin.H](t, baseUrl, "/", tt.FormData{
"key": sendtok, "key": sendtok,
"user_id": uid,
"title": "Hello World 9999 [$$$]", "title": "Hello World 9999 [$$$]",
}) })
@@ -190,7 +184,6 @@ func TestSendSimpleMessageJSONAndQuery(t *testing.T) {
// query overwrite body // query overwrite body
msg1 := tt.RequestPost[gin.H](t, baseUrl, fmt.Sprintf("/?user_id=%s&key=%s&title=%s", uid, sendtok, url.QueryEscape("1111111")), gin.H{ msg1 := tt.RequestPost[gin.H](t, baseUrl, fmt.Sprintf("/?user_id=%s&key=%s&title=%s", uid, sendtok, url.QueryEscape("1111111")), gin.H{
"key": "ERR", "key": "ERR",
"user_id": models.NewUserID(),
"title": "2222222", "title": "2222222",
}) })
@@ -212,20 +205,17 @@ func TestSendSimpleMessageAlt1(t *testing.T) {
"fcm_token": "DUMMY_FCM", "fcm_token": "DUMMY_FCM",
}) })
uid := r0["user_id"].(string)
admintok := r0["admin_key"].(string) admintok := r0["admin_key"].(string)
readtok := r0["read_key"].(string) readtok := r0["read_key"].(string)
sendtok := r0["send_key"].(string) sendtok := r0["send_key"].(string)
msg1 := tt.RequestPost[gin.H](t, baseUrl, "/send", gin.H{ msg1 := tt.RequestPost[gin.H](t, baseUrl, "/send", gin.H{
"key": sendtok, "key": sendtok,
"user_id": uid,
"title": "HelloWorld_001", "title": "HelloWorld_001",
}) })
tt.RequestPostShouldFail(t, baseUrl, "/send", gin.H{ tt.RequestPostShouldFail(t, baseUrl, "/send", gin.H{
"key": readtok, "key": readtok,
"user_id": uid,
"title": "HelloWorld_001", "title": "HelloWorld_001",
}, 401, apierr.USER_AUTH_FAILED) }, 401, apierr.USER_AUTH_FAILED)
@@ -259,13 +249,11 @@ func TestSendContentMessage(t *testing.T) {
"fcm_token": "DUMMY_FCM", "fcm_token": "DUMMY_FCM",
}) })
uid := r0["user_id"].(string)
admintok := r0["admin_key"].(string) admintok := r0["admin_key"].(string)
sendtok := r0["send_key"].(string) sendtok := r0["send_key"].(string)
msg1 := tt.RequestPost[gin.H](t, baseUrl, "/", gin.H{ msg1 := tt.RequestPost[gin.H](t, baseUrl, "/", gin.H{
"key": sendtok, "key": sendtok,
"user_id": uid,
"title": "HelloWorld_042", "title": "HelloWorld_042",
"content": "I am Content\nasdf", "content": "I am Content\nasdf",
}) })
@@ -304,13 +292,11 @@ func TestSendWithSendername(t *testing.T) {
"fcm_token": "DUMMY_FCM", "fcm_token": "DUMMY_FCM",
}) })
uid := r0["user_id"].(string)
sendtok := r0["send_key"].(string) sendtok := r0["send_key"].(string)
admintok := r0["admin_key"].(string) admintok := r0["admin_key"].(string)
msg1 := tt.RequestPost[gin.H](t, baseUrl, "/", gin.H{ msg1 := tt.RequestPost[gin.H](t, baseUrl, "/", gin.H{
"key": sendtok, "key": sendtok,
"user_id": uid,
"title": "HelloWorld_xyz", "title": "HelloWorld_xyz",
"content": "Unicode: 日本 - yäy\000\n\t\x00...", "content": "Unicode: 日本 - yäy\000\n\t\x00...",
"sender_name": "localhorst", "sender_name": "localhorst",
@@ -353,7 +339,6 @@ func TestSendLongContent(t *testing.T) {
"fcm_token": "DUMMY_FCM", "fcm_token": "DUMMY_FCM",
}) })
uid := r0["user_id"].(string)
admintok := r0["admin_key"].(string) admintok := r0["admin_key"].(string)
sendtok := r0["send_key"].(string) sendtok := r0["send_key"].(string)
@@ -364,7 +349,6 @@ func TestSendLongContent(t *testing.T) {
msg1 := tt.RequestPost[gin.H](t, baseUrl, "/", gin.H{ msg1 := tt.RequestPost[gin.H](t, baseUrl, "/", gin.H{
"key": sendtok, "key": sendtok,
"user_id": uid,
"title": "HelloWorld_042", "title": "HelloWorld_042",
"content": longContent, "content": longContent,
}) })

View File

@@ -402,7 +402,6 @@ func TestUserMessageCounter(t *testing.T) {
tt.RequestPost[gin.H](t, baseUrl, "/", gin.H{ tt.RequestPost[gin.H](t, baseUrl, "/", gin.H{
"key": admintok, "key": admintok,
"user_id": uid,
"title": tt.ShortLipsum(1001, 1), "title": tt.ShortLipsum(1001, 1),
}) })
@@ -411,7 +410,6 @@ func TestUserMessageCounter(t *testing.T) {
tt.RequestPost[gin.H](t, baseUrl, "/", gin.H{ tt.RequestPost[gin.H](t, baseUrl, "/", gin.H{
"key": admintok, "key": admintok,
"user_id": uid,
"title": tt.ShortLipsum(1002, 1), "title": tt.ShortLipsum(1002, 1),
}) })
@@ -419,17 +417,14 @@ func TestUserMessageCounter(t *testing.T) {
tt.RequestPost[gin.H](t, baseUrl, "/", gin.H{ tt.RequestPost[gin.H](t, baseUrl, "/", gin.H{
"key": admintok, "key": admintok,
"user_id": uid,
"title": tt.ShortLipsum(1003, 1), "title": tt.ShortLipsum(1003, 1),
}) })
tt.RequestPost[gin.H](t, baseUrl, "/", gin.H{ tt.RequestPost[gin.H](t, baseUrl, "/", gin.H{
"key": admintok, "key": admintok,
"user_id": uid,
"title": tt.ShortLipsum(1004, 1), "title": tt.ShortLipsum(1004, 1),
}) })
tt.RequestPost[gin.H](t, baseUrl, "/", gin.H{ tt.RequestPost[gin.H](t, baseUrl, "/", gin.H{
"key": admintok, "key": admintok,
"user_id": uid,
"title": tt.ShortLipsum(1005, 1), "title": tt.ShortLipsum(1005, 1),
}) })

View File

@@ -1,15 +1,16 @@
package util package util
import ( import (
"blackforestbytes.com/simplecloudnotifier/logic"
"fmt" "fmt"
"testing"
"time"
"blackforestbytes.com/simplecloudnotifier/logic"
"git.blackforestbytes.com/BlackForestBytes/goext/langext" "git.blackforestbytes.com/BlackForestBytes/goext/langext"
"git.blackforestbytes.com/BlackForestBytes/goext/timeext" "git.blackforestbytes.com/BlackForestBytes/goext/timeext"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"gopkg.in/loremipsum.v1" "gopkg.in/loremipsum.v1"
"testing"
"time"
) )
// # Generated by https://chat.openai.com/chat // # Generated by https://chat.openai.com/chat
@@ -393,7 +394,6 @@ func InitDefaultData(t *testing.T, ws *logic.Application) DefData {
for _, mex := range messageExamples { for _, mex := range messageExamples {
body := gin.H{} body := gin.H{}
body["title"] = mex.Title body["title"] = mex.Title
body["user_id"] = users[mex.User].UID
switch mex.Key { switch mex.Key {
case AKEY: case AKEY:
body["key"] = users[mex.User].AdminKey body["key"] = users[mex.User].AdminKey

View File

@@ -19,10 +19,9 @@
<a tabindex="-1" href="/" class="linkcaption"><h1>Simple Cloud Notifier</h1></a> <a tabindex="-1" href="/" class="linkcaption"><h1>Simple Cloud Notifier</h1></a>
<p>Get your user-id and user-key from the android or iOS app.<br/>And send notifications to your phone by performing a POST request against <code>{{config|baseURL}}/</code> from anywhere</p> <p>Get your user-key from the android or iOS app.<br/>And send notifications to your phone by performing a POST request against <code>{{config|baseURL}}/</code> from anywhere</p>
<pre> <pre>
curl \ curl \
--data "user_id=${userid}" \
--data "key=${key}" \ --data "key=${key}" \
--data "title=${message_title}" \ --data "title=${message_title}" \
--data "content=${message_body}" \ --data "content=${message_body}" \
@@ -35,7 +34,6 @@ curl \
<p>Most parameters are optional, you can send a message with only a title (default priority and channel will be used)</p> <p>Most parameters are optional, you can send a message with only a title (default priority and channel will be used)</p>
<pre> <pre>
curl \ curl \
--data "user_id={userid}" \
--data "key={key}" \ --data "key={key}" \
--data "title={message_title}" \ --data "title={message_title}" \
{{config|baseURL}}/</pre> {{config|baseURL}}/</pre>

View File

@@ -52,7 +52,7 @@
All Parameters can either directly be submitted as URL parameters or they can be put into the POST body (either multipart/form-data or JSON). All Parameters can either directly be submitted as URL parameters or they can be put into the POST body (either multipart/form-data or JSON).
</p> </p>
<p> <p>
You <i>need</i> to supply a valid <code>[user_id, key]</code> pair and a <code>title</code> for your message, all other parameter are optional. You <i>need</i> to supply a valid <code>key</code> and a <code>title</code> for your message, all other parameter are optional.
</p> </p>
</div> </div>
@@ -90,7 +90,7 @@
</tr> </tr>
<tr> <tr>
<td data-label="Statuscode">401 (Unauthorized)</td> <td data-label="Statuscode">401 (Unauthorized)</td>
<td data-label="Explanation">The user_id was not found, the key is wrong or the [user_id, key] combination does not have the SEND permissions on the specified channel</td> <td data-label="Explanation">The key is wrong or does not have the SEND permissions on the specified channel</td>
</tr> </tr>
<tr> <tr>
<td data-label="Statuscode">403 (Forbidden)</td> <td data-label="Statuscode">403 (Forbidden)</td>
@@ -125,7 +125,6 @@
If needed the content can be supplied in the <code>content</code> parameter. If needed the content can be supplied in the <code>content</code> parameter.
</p> </p>
<pre>curl \ <pre>curl \
--data "user_id={userid}" \
--data "key={key}" \ --data "key={key}" \
--data "title={message_title}" \ --data "title={message_title}" \
--data "content={message_content}" \ --data "content={message_content}" \
@@ -143,7 +142,6 @@
If no priority is supplied the message will get the default priority of 1. If no priority is supplied the message will get the default priority of 1.
</p> </p>
<pre>curl \ <pre>curl \
--data "user_id={userid}" \
--data "key={key}" \ --data "key={key}" \
--data "title={message_title}" \ --data "title={message_title}" \
--data "priority={0|1|2}" \ --data "priority={0|1|2}" \
@@ -158,7 +156,6 @@
Channel names are case-insensitive and can only contain letters, numbers, underscores and minuses ( <code>/[[:alnum:]\-_]+/</code> ) Channel names are case-insensitive and can only contain letters, numbers, underscores and minuses ( <code>/[[:alnum:]\-_]+/</code> )
</p> </p>
<pre>curl \ <pre>curl \
--data "user_id={userid}" \
--data "key={key}" \ --data "key={key}" \
--data "title={message_title}" \ --data "title={message_title}" \
--data "channel={my_channel}" \ --data "channel={my_channel}" \
@@ -229,7 +226,6 @@
The message_id is optional - but if you want to use it you need to supply it via the <code>msg_id</code> parameter. The message_id is optional - but if you want to use it you need to supply it via the <code>msg_id</code> parameter.
</p> </p>
<pre>curl \ <pre>curl \
--data "user_id={userid}" \
--data "key={key}" \ --data "key={key}" \
--data "title={message_title}" \ --data "title={message_title}" \
--data "msg_id={message_id}" \ --data "msg_id={message_id}" \
@@ -248,7 +244,6 @@
The custom timestamp must be within 48 hours of the current time. This parameter is only intended to supply a more precise value in case the message sending was delayed. The custom timestamp must be within 48 hours of the current time. This parameter is only intended to supply a more precise value in case the message sending was delayed.
</p> </p>
<pre>curl \ <pre>curl \
--data "user_id={userid}" \
--data "key={key}" \ --data "key={key}" \
--data "title={message_title}" \ --data "title={message_title}" \
--data "timestamp={unix_timestamp}" \ --data "timestamp={unix_timestamp}" \

View File

@@ -21,11 +21,6 @@
<a tabindex="-1" href="/" class="linkcaption"><h1>Simple Cloud Notifier</h1></a> <a tabindex="-1" href="/" class="linkcaption"><h1>Simple Cloud Notifier</h1></a>
<div class="row responsive-label">
<div class="col-sm-12 col-md-3"><label for="uid" class="doc">UserID</label></div>
<div class="col-sm-12 col-md"><input placeholder="UserID" id="uid" class="doc" type="text" pattern="USR[A-Za-z0-9]{21}"></div>
</div>
<div class="row responsive-label"> <div class="row responsive-label">
<div class="col-sm-12 col-md-3"><label for="ukey" class="doc">Authentification Key</label></div> <div class="col-sm-12 col-md-3"><label for="ukey" class="doc">Authentification Key</label></div>
<div class="col-sm-12 col-md"><input placeholder="Key" id="ukey" class="doc" type="text" pattern="[A-Za-z0-9]{64}"></div> <div class="col-sm-12 col-md"><input placeholder="Key" id="ukey" class="doc" type="text" pattern="[A-Za-z0-9]{64}"></div>

View File

@@ -8,20 +8,17 @@ function send()
me.classList.add("btn-disabled"); me.classList.add("btn-disabled");
let uid = document.getElementById("uid");
let key = document.getElementById("ukey"); let key = document.getElementById("ukey");
let tit = document.getElementById("tit"); let tit = document.getElementById("tit");
let cnt = document.getElementById("cnt"); let cnt = document.getElementById("cnt");
let pio = document.getElementById("prio"); let pio = document.getElementById("prio");
let cha = document.getElementById("chan"); let cha = document.getElementById("chan");
uid.classList.remove('input-invalid');
key.classList.remove('input-invalid'); key.classList.remove('input-invalid');
cnt.classList.remove('input-invalid'); cnt.classList.remove('input-invalid');
pio.classList.remove('input-invalid'); pio.classList.remove('input-invalid');
let data = new FormData(); let data = new FormData();
data.append('user_id', uid.value);
data.append('key', key.value); data.append('key', key.value);
if (tit.value !== '') data.append('title', tit.value); if (tit.value !== '') data.append('title', tit.value);
if (cnt.value !== '') data.append('content', cnt.value); if (cnt.value !== '') data.append('content', cnt.value);
@@ -40,7 +37,6 @@ function send()
let resp = JSON.parse(xhr.responseText); let resp = JSON.parse(xhr.responseText);
if (!resp.success || xhr.status !== 200) if (!resp.success || xhr.status !== 200)
{ {
if (resp.errhighlight === 101) uid.classList.add('input-invalid');
if (resp.errhighlight === 102) key.classList.add('input-invalid'); if (resp.errhighlight === 102) key.classList.add('input-invalid');
if (resp.errhighlight === 103) tit.classList.add('input-invalid'); if (resp.errhighlight === 103) tit.classList.add('input-invalid');
if (resp.errhighlight === 104) cnt.classList.add('input-invalid'); if (resp.errhighlight === 104) cnt.classList.add('input-invalid');
@@ -63,7 +59,6 @@ function send()
'&quota=' + resp.quota + '&quota=' + resp.quota +
'&quota_remain=' + (resp.quota_max-resp.quota) + '&quota_remain=' + (resp.quota_max-resp.quota) +
'&quota_max=' + resp.quota_max + '&quota_max=' + resp.quota_max +
'&preset_user_id=' + uid.value +
'&preset_user_key=' + key.value + '&preset_user_key=' + key.value +
'&preset_channel=' + cha.value; '&preset_channel=' + cha.value;
} }
@@ -89,7 +84,6 @@ window.addEventListener("load", function ()
const qp = new URLSearchParams(window.location.search); const qp = new URLSearchParams(window.location.search);
let btn = document.getElementById("btnSend"); let btn = document.getElementById("btnSend");
let uid = document.getElementById("uid");
let key = document.getElementById("ukey"); let key = document.getElementById("ukey");
let tit = document.getElementById("tit"); let tit = document.getElementById("tit");
let cnt = document.getElementById("cnt"); let cnt = document.getElementById("cnt");
@@ -100,7 +94,6 @@ window.addEventListener("load", function ()
if (qp.has('preset_priority')) pio.selectedIndex = parseInt(qp.get("preset_priority")); if (qp.has('preset_priority')) pio.selectedIndex = parseInt(qp.get("preset_priority"));
if (qp.has('preset_user_key')) key.value = qp.get("preset_user_key"); if (qp.has('preset_user_key')) key.value = qp.get("preset_user_key");
if (qp.has('preset_user_id')) uid.value = qp.get("preset_user_id");
if (qp.has('preset_title')) tit.value = qp.get("preset_title"); if (qp.has('preset_title')) tit.value = qp.get("preset_title");
if (qp.has('preset_content')) cnt.value = qp.get("preset_content"); if (qp.has('preset_content')) cnt.value = qp.get("preset_content");
if (qp.has('preset_channel')) cha.value = qp.get("preset_channel"); if (qp.has('preset_channel')) cha.value = qp.get("preset_channel");

View File

@@ -15,19 +15,6 @@
<span nz-icon nzType="edit"></span> <span nz-icon nzType="edit"></span>
Edit Edit
</button> </button>
<button
nz-button
nzType="primary"
nzDanger
nz-popconfirm
nzPopconfirmTitle="Are you sure you want to delete this channel? All messages and subscriptions will be lost."
nzPopconfirmPlacement="bottomRight"
(nzOnConfirm)="deleteChannel()"
[nzLoading]="deleting()"
>
<span nz-icon nzType="delete"></span>
Delete
</button>
</div> </div>
} }
</div> </div>

View File

@@ -62,8 +62,6 @@ export class ChannelDetailComponent implements OnInit {
subscriptions = signal<Subscription[]>([]); subscriptions = signal<Subscription[]>([]);
loading = signal(true); loading = signal(true);
loadingSubscriptions = signal(false); loadingSubscriptions = signal(false);
deleting = signal(false);
// Edit modal // Edit modal
showEditModal = signal(false); showEditModal = signal(false);
editDisplayName = ''; editDisplayName = '';
@@ -173,24 +171,6 @@ export class ChannelDetailComponent implements OnInit {
}); });
} }
// Delete channel
deleteChannel(): void {
const channel = this.channel();
const userId = this.authService.getUserId();
if (!channel || !userId) return;
this.deleting.set(true);
this.apiService.deleteChannel(userId, channel.channel_id).subscribe({
next: () => {
this.notification.success('Channel deleted');
this.router.navigate(['/channels']);
},
error: () => {
this.deleting.set(false);
}
});
}
// Regenerate keys // Regenerate keys
regenerateSubscribeKey(): void { regenerateSubscribeKey(): void {
const channel = this.channel(); const channel = this.channel();

View File

@@ -31,7 +31,7 @@
</thead> </thead>
<tbody> <tbody>
@for (channel of channels(); track channel.channel_id) { @for (channel of channels(); track channel.channel_id) {
<tr class="clickable-row" (click)="viewChannel(channel)"> <tr [class.clickable-row]="isOwned(channel)" (click)="isOwned(channel) && viewChannel(channel)">
<td> <td>
<div class="channel-name">{{ channel.display_name }}</div> <div class="channel-name">{{ channel.display_name }}</div>
@if (channel.description_name) { @if (channel.description_name) {

View File

@@ -78,6 +78,10 @@ export class ChannelListComponent implements OnInit {
return resolved?.displayName || ownerId; return resolved?.displayName || ownerId;
} }
isOwned(channel: ChannelWithSubscription): boolean {
return channel.owner_user_id === this.authService.getUserId();
}
viewChannel(channel: ChannelWithSubscription): void { viewChannel(channel: ChannelWithSubscription): void {
this.router.navigate(['/channels', channel.channel_id]); this.router.navigate(['/channels', channel.channel_id]);
} }