Add KeyToken authorization

This commit is contained in:
Mike Schwörer 2023-04-21 21:45:16 +02:00
parent 16f6ab4861
commit b1bd278f9b
Signed by: Mikescher
GPG Key ID: D3C7172E0A70F8CF
49 changed files with 3109 additions and 1313 deletions

View File

@ -10,12 +10,17 @@ HASH=$(shell git rev-parse HEAD)
build: swagger fmt
mkdir -p _build
rm -f ./_build/scn_backend
go generate ./...
CGO_ENABLED=1 go build -v -o _build/scn_backend -tags "timetzdata sqlite_fts5 sqlite_foreign_keys" ./cmd/scnserver
run: build
mkdir -p .run-data
_build/scn_backend
gow:
# go install github.com/mitranim/gow@latest
gow run blackforestbytes.com/portfoliomanager2/cmd/server
docker: build
[ ! -f "DOCKER_GIT_INFO" ] || rm DOCKER_GIT_INFO
git rev-parse --abbrev-ref HEAD >> DOCKER_GIT_INFO

View File

@ -49,11 +49,27 @@
- ios purchase verification
- re-add ack labels as compat table for v1 api user
- [X] re-add ack labels as compat table for v1 api user
- return channel as "[..] asdf" in compat methods (mark clients as compat and send compat FB to them...)
(then we can replace the old server without switching phone clients)
(still needs switching of the send-script)
-
- do not use uuidgen in bash script (potetnially not installed) - use `head /dev/urandom | tr -dc A-Za-z0-9 | head -c 13 `
- move to KeyToken model
* [X] User can have multiple keys with different permissions
* [X] compat simply uses default-keys
* [X] CRUD routes for keys
* [X] KeyToken.messagecounter
* [ ] update old-data migration to create keys
* [ ] unit tests
- We no longer have a route to reshuffle all keys (previously in updateUser), add a /user/:uid/keys/reset ?
Would delete all existing keys and create 3 new ones?
- the explanation of user_id and key in ./website is now wrong (was already wrong and is even wronger now that there are multiple KeyToken's with permissions etc)
- swagger broken?
#### PERSONAL
@ -84,4 +100,4 @@
#### FUTURE
- Remove compat, especially do not create compat id for every new message...
- Remove compat, especially do not create compat id for every new message...

View File

@ -0,0 +1,295 @@
package main
import (
"fmt"
"gogs.mikescher.com/BlackForestBytes/goext/langext"
"gogs.mikescher.com/BlackForestBytes/goext/rext"
"io"
"os"
"regexp"
"strings"
)
type EnumDefVal struct {
VarName string
Value string
Description *string
}
type EnumDef struct {
File string
EnumTypeName string
Type string
Values []EnumDefVal
}
var rexPackage = rext.W(regexp.MustCompile("^package\\s+(?P<name>[A-Za-z0-9_]+)\\s*$"))
var rexEnumDef = rext.W(regexp.MustCompile("^\\s*type\\s+(?P<name>[A-Za-z0-9_]+)\\s+(?P<type>[A-Za-z0-9_]+)\\s*//\\s*(@enum:type).*$"))
var rexValueDef = rext.W(regexp.MustCompile("^\\s*(?P<name>[A-Za-z0-9_]+)\\s+(?P<type>[A-Za-z0-9_]+)\\s*=\\s*(?P<value>(\"[A-Za-z0-9_]+\"|[0-9]+))\\s*(//(?P<descr>.*))?.*$"))
func main() {
dest := os.Args[2]
wd, err := os.Getwd()
errpanic(err)
files, err := os.ReadDir(wd)
errpanic(err)
allEnums := make([]EnumDef, 0)
pkgname := ""
for _, f := range files {
if !strings.HasSuffix(f.Name(), ".go") {
continue
}
fmt.Printf("========= %s =========\n\n", f.Name())
fileEnums, pn := processFile(f.Name())
fmt.Printf("\n")
allEnums = append(allEnums, fileEnums...)
if pn != "" {
pkgname = pn
}
}
if pkgname == "" {
panic("no package name found in any file")
}
errpanic(os.WriteFile(dest, []byte(fmtOutput(allEnums, pkgname)), 0o755))
}
func errpanic(err error) {
if err != nil {
panic(err)
}
}
func processFile(fn string) ([]EnumDef, string) {
file, err := os.Open(fn)
errpanic(err)
defer func() { errpanic(file.Close()) }()
bin, err := io.ReadAll(file)
errpanic(err)
lines := strings.Split(string(bin), "\n")
enums := make([]EnumDef, 0)
pkgname := ""
for i, line := range lines {
if i == 0 && strings.HasPrefix(line, "// Code generated by") {
break
}
if match, ok := rexPackage.MatchFirst(line); i == 0 && ok {
pkgname = match.GroupByName("name").Value()
continue
}
if match, ok := rexEnumDef.MatchFirst(line); ok {
def := EnumDef{
File: fn,
EnumTypeName: match.GroupByName("name").Value(),
Type: match.GroupByName("type").Value(),
Values: make([]EnumDefVal, 0),
}
enums = append(enums, def)
fmt.Printf("Found enum definition { '%s' -> '%s' }\n", def.EnumTypeName, def.Type)
}
if match, ok := rexValueDef.MatchFirst(line); ok {
typename := match.GroupByName("type").Value()
def := EnumDefVal{
VarName: match.GroupByName("name").Value(),
Value: match.GroupByName("value").Value(),
Description: match.GroupByNameOrEmpty("descr").ValueOrNil(),
}
found := false
for i, v := range enums {
if v.EnumTypeName == typename {
enums[i].Values = append(enums[i].Values, def)
found = true
if def.Description != nil {
fmt.Printf("Found enum value [%s] for '%s' ('%s')\n", def.Value, def.VarName, *def.Description)
} else {
fmt.Printf("Found enum value [%s] for '%s'\n", def.Value, def.VarName)
}
break
}
}
if !found {
fmt.Printf("Found non-enum value [%s] for '%s' ( looks like enum value, but no matching @enum:type )\n", def.Value, def.VarName)
}
}
}
return enums, pkgname
}
func fmtOutput(enums []EnumDef, pkgname string) string {
str := "// Code generated by permissions_gen.sh DO NOT EDIT.\n"
str += "\n"
str += "package " + pkgname + "\n"
str += "\n"
str += "import \"gogs.mikescher.com/BlackForestBytes/goext/langext\"" + "\n"
str += "\n"
str += "type Enum interface {" + "\n"
str += " Valid() bool" + "\n"
str += " ValuesAny() []any" + "\n"
str += " ValuesMeta() []EnumMetaValue" + "\n"
str += " VarName() string" + "\n"
str += "}" + "\n"
str += "" + "\n"
str += "type StringEnum interface {" + "\n"
str += " Enum" + "\n"
str += " String() string" + "\n"
str += "}" + "\n"
str += "" + "\n"
str += "type DescriptionEnum interface {" + "\n"
str += " Enum" + "\n"
str += " Description() string" + "\n"
str += "}" + "\n"
str += "\n"
str += "type EnumMetaValue struct {" + "\n"
str += " VarName string `json:\"varName\"`" + "\n"
str += " Value any `json:\"value\"`" + "\n"
str += " Description *string `json:\"description\"`" + "\n"
str += "}" + "\n"
str += "\n"
for _, enumdef := range enums {
hasDescr := langext.ArrAll(enumdef.Values, func(val EnumDefVal) bool { return val.Description != nil })
hasStr := enumdef.Type == "string"
str += "// ================================ " + enumdef.EnumTypeName + " ================================" + "\n"
str += "//" + "\n"
str += "// File: " + enumdef.File + "\n"
str += "// StringEnum: " + langext.Conditional(hasStr, "true", "false") + "\n"
str += "// DescrEnum: " + langext.Conditional(hasDescr, "true", "false") + "\n"
str += "//" + "\n"
str += "" + "\n"
str += "var __" + enumdef.EnumTypeName + "Values = []" + enumdef.EnumTypeName + "{" + "\n"
for _, v := range enumdef.Values {
str += " " + v.VarName + "," + "\n"
}
str += "}" + "\n"
str += "" + "\n"
if hasDescr {
str += "var __" + enumdef.EnumTypeName + "Descriptions = map[" + enumdef.EnumTypeName + "]string{" + "\n"
for _, v := range enumdef.Values {
str += " " + v.VarName + ": \"" + strings.TrimSpace(*v.Description) + "\"," + "\n"
}
str += "}" + "\n"
str += "" + "\n"
}
str += "var __" + enumdef.EnumTypeName + "Varnames = map[" + enumdef.EnumTypeName + "]string{" + "\n"
for _, v := range enumdef.Values {
str += " " + v.VarName + ": \"" + v.VarName + "\"," + "\n"
}
str += "}" + "\n"
str += "" + "\n"
str += "func (e " + enumdef.EnumTypeName + ") Valid() bool {" + "\n"
str += " return langext.InArray(e, __" + enumdef.EnumTypeName + "Values)" + "\n"
str += "}" + "\n"
str += "" + "\n"
str += "func (e " + enumdef.EnumTypeName + ") Values() []" + enumdef.EnumTypeName + " {" + "\n"
str += " return __" + enumdef.EnumTypeName + "Values" + "\n"
str += "}" + "\n"
str += "" + "\n"
str += "func (e " + enumdef.EnumTypeName + ") ValuesAny() []any {" + "\n"
str += " return langext.ArrCastToAny(__" + enumdef.EnumTypeName + "Values)" + "\n"
str += "}" + "\n"
str += "" + "\n"
str += "func (e " + enumdef.EnumTypeName + ") ValuesMeta() []EnumMetaValue {" + "\n"
str += " return []EnumMetaValue{" + "\n"
for _, v := range enumdef.Values {
if hasDescr {
str += " " + fmt.Sprintf("EnumMetaValue{VarName: \"%s\", Value: %s, Description: langext.Ptr(\"%s\")},", v.VarName, v.VarName, strings.TrimSpace(*v.Description)) + "\n"
} else {
str += " " + fmt.Sprintf("EnumMetaValue{VarName: \"%s\", Value: %s, Description: nil},", v.VarName, v.VarName) + "\n"
}
}
str += " }" + "\n"
str += "}" + "\n"
str += "" + "\n"
if hasStr {
str += "func (e " + enumdef.EnumTypeName + ") String() string {" + "\n"
str += " return string(e)" + "\n"
str += "}" + "\n"
str += "" + "\n"
}
if hasDescr {
str += "func (e " + enumdef.EnumTypeName + ") Description() string {" + "\n"
str += " if d, ok := __" + enumdef.EnumTypeName + "Descriptions[e]; ok {" + "\n"
str += " return d" + "\n"
str += " }" + "\n"
str += " return \"\"" + "\n"
str += "}" + "\n"
str += "" + "\n"
}
str += "func (e " + enumdef.EnumTypeName + ") VarName() string {" + "\n"
str += " if d, ok := __" + enumdef.EnumTypeName + "Varnames[e]; ok {" + "\n"
str += " return d" + "\n"
str += " }" + "\n"
str += " return \"\"" + "\n"
str += "}" + "\n"
str += "" + "\n"
str += "func Parse" + enumdef.EnumTypeName + "(vv string) (" + enumdef.EnumTypeName + ", bool) {" + "\n"
str += " for _, ev := range __" + enumdef.EnumTypeName + "Values {" + "\n"
str += " if string(ev) == vv {" + "\n"
str += " return ev, true" + "\n"
str += " }" + "\n"
str += " }" + "\n"
str += " return \"\", false" + "\n"
str += "}" + "\n"
str += "" + "\n"
str += "func " + enumdef.EnumTypeName + "Values() []" + enumdef.EnumTypeName + " {" + "\n"
str += " return __" + enumdef.EnumTypeName + "Values" + "\n"
str += "}" + "\n"
str += "" + "\n"
str += "func " + enumdef.EnumTypeName + "ValuesMeta() []EnumMetaValue {" + "\n"
str += " return []EnumMetaValue{" + "\n"
for _, v := range enumdef.Values {
if hasDescr {
str += " " + fmt.Sprintf("EnumMetaValue{VarName: \"%s\", Value: %s, Description: langext.Ptr(\"%s\")},", v.VarName, v.VarName, strings.TrimSpace(*v.Description)) + "\n"
} else {
str += " " + fmt.Sprintf("EnumMetaValue{VarName: \"%s\", Value: %s, Description: nil},", v.VarName, v.VarName) + "\n"
}
}
str += " }" + "\n"
str += "}" + "\n"
str += "" + "\n"
}
return str
}

View File

@ -1,6 +1,6 @@
package apierr
type APIError int
type APIError int //@enum:type
//goland:noinspection GoSnakeCaseUsage
const (
@ -37,11 +37,13 @@ const (
SUBSCRIPTION_NOT_FOUND APIError = 1304
MESSAGE_NOT_FOUND APIError = 1305
SUBSCRIPTION_USER_MISMATCH APIError = 1306
KEY_NOT_FOUND APIError = 1307
USER_AUTH_FAILED APIError = 1311
NO_DEVICE_LINKED APIError = 1401
CHANNEL_ALREADY_EXISTS APIError = 1501
CANNOT_SELFDELETE_KEY APIError = 1511
QUOTA_REACHED APIError = 2101

View File

@ -1,6 +1,6 @@
package apihighlight
type ErrHighlight int
type ErrHighlight int //@enum:type
//goland:noinspection GoSnakeCaseUsage
const (

View File

@ -108,6 +108,11 @@ func createRequestLog(g *gin.Context, t0 time.Time, ctr int, resp HTTPResponse,
permObj, hasPerm := g.Get("perm")
hasTok := false
if hasPerm {
hasTok = permObj.(models.PermissionSet).Token != nil
}
return models.RequestLog{
Method: g.Request.Method,
URI: g.Request.URL.String(),
@ -117,8 +122,9 @@ func createRequestLog(g *gin.Context, t0 time.Time, ctr int, resp HTTPResponse,
RequestBodySize: int64(len(reqbody)),
RequestContentType: ct,
RemoteIP: g.RemoteIP(),
UserID: langext.ConditionalFn10(hasPerm, func() *models.UserID { return permObj.(models.PermissionSet).UserID }, nil),
Permissions: langext.ConditionalFn10(hasPerm, func() *string { return langext.Ptr(string(permObj.(models.PermissionSet).KeyType)) }, nil),
TokenID: langext.ConditionalFn10(hasTok, func() *models.KeyTokenID { return langext.Ptr(permObj.(models.PermissionSet).Token.KeyTokenID) }, nil),
UserID: langext.ConditionalFn10(hasTok, func() *models.UserID { return langext.Ptr(permObj.(models.PermissionSet).Token.OwnerUserID) }, nil),
Permissions: langext.ConditionalFn10(hasTok, func() *string { return langext.Ptr(permObj.(models.PermissionSet).Token.Permissions.String()) }, nil),
ResponseStatuscode: langext.ConditionalFn10(resp != nil, func() *int64 { return langext.Ptr(int64(resp.Statuscode())) }, nil),
ResponseBodySize: langext.ConditionalFn10(strrespbody != nil, func() *int64 { return langext.Ptr(int64(len(*respbody))) }, nil),
ResponseBody: strrespbody,

File diff suppressed because it is too large Load Diff

View File

@ -39,17 +39,17 @@ type pingResponseInfo struct {
// Ping swaggerdoc
//
// @Summary Simple endpoint to test connection (any http method)
// @Tags Common
// @Summary Simple endpoint to test connection (any http method)
// @Tags Common
//
// @Success 200 {object} pingResponse
// @Failure 500 {object} ginresp.apiError
// @Success 200 {object} pingResponse
// @Failure 500 {object} ginresp.apiError
//
// @Router /api/ping [get]
// @Router /api/ping [post]
// @Router /api/ping [put]
// @Router /api/ping [delete]
// @Router /api/ping [patch]
// @Router /api/ping [get]
// @Router /api/ping [post]
// @Router /api/ping [put]
// @Router /api/ping [delete]
// @Router /api/ping [patch]
func (h CommonHandler) Ping(g *gin.Context) ginresp.HTTPResponse {
buf := new(bytes.Buffer)
_, _ = buf.ReadFrom(g.Request.Body)
@ -69,14 +69,14 @@ func (h CommonHandler) Ping(g *gin.Context) ginresp.HTTPResponse {
// DatabaseTest swaggerdoc
//
// @Summary Check for a working database connection
// @ID api-common-dbtest
// @Tags Common
// @Summary Check for a working database connection
// @ID api-common-dbtest
// @Tags Common
//
// @Success 200 {object} handler.DatabaseTest.response
// @Failure 500 {object} ginresp.apiError
// @Success 200 {object} handler.DatabaseTest.response
// @Failure 500 {object} ginresp.apiError
//
// @Router /api/db-test [post]
// @Router /api/db-test [post]
func (h CommonHandler) DatabaseTest(g *gin.Context) ginresp.HTTPResponse {
type response struct {
Success bool `json:"success"`
@ -105,14 +105,14 @@ func (h CommonHandler) DatabaseTest(g *gin.Context) ginresp.HTTPResponse {
// Health swaggerdoc
//
// @Summary Server Health-checks
// @ID api-common-health
// @Tags Common
// @Summary Server Health-checks
// @ID api-common-health
// @Tags Common
//
// @Success 200 {object} handler.Health.response
// @Failure 500 {object} ginresp.apiError
// @Success 200 {object} handler.Health.response
// @Failure 500 {object} ginresp.apiError
//
// @Router /api/health [get]
// @Router /api/health [get]
func (h CommonHandler) Health(g *gin.Context) ginresp.HTTPResponse {
type response struct {
Status string `json:"status"`
@ -163,17 +163,17 @@ func (h CommonHandler) Health(g *gin.Context) ginresp.HTTPResponse {
// Sleep swaggerdoc
//
// @Summary Return 200 after x seconds
// @ID api-common-sleep
// @Tags Common
// @Summary Return 200 after x seconds
// @ID api-common-sleep
// @Tags Common
//
// @Param secs path number true "sleep delay (in seconds)"
// @Param secs path number true "sleep delay (in seconds)"
//
// @Success 200 {object} handler.Sleep.response
// @Failure 400 {object} ginresp.apiError
// @Failure 500 {object} ginresp.apiError
// @Success 200 {object} handler.Sleep.response
// @Failure 400 {object} ginresp.apiError
// @Failure 500 {object} ginresp.apiError
//
// @Router /api/sleep/{secs} [post]
// @Router /api/sleep/{secs} [post]
func (h CommonHandler) Sleep(g *gin.Context) ginresp.HTTPResponse {
type uri struct {
Seconds float64 `uri:"secs"`

View File

@ -29,22 +29,22 @@ func NewCompatHandler(app *logic.Application) CompatHandler {
// SendMessageCompat swaggerdoc
//
// @Deprecated
// @Deprecated
//
// @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
// @Tags External
// @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
// @Tags External
//
// @Param query_data query handler.SendMessageCompat.combined false " "
// @Param form_data formData handler.SendMessageCompat.combined false " "
// @Param query_data query handler.SendMessageCompat.combined false " "
// @Param form_data formData handler.SendMessageCompat.combined false " "
//
// @Success 200 {object} handler.SendMessageCompat.response
// @Failure 400 {object} ginresp.apiError
// @Failure 401 {object} ginresp.apiError
// @Failure 403 {object} ginresp.apiError
// @Failure 500 {object} ginresp.apiError
// @Success 200 {object} handler.SendMessageCompat.response
// @Failure 400 {object} ginresp.apiError
// @Failure 401 {object} ginresp.apiError
// @Failure 403 {object} ginresp.apiError
// @Failure 500 {object} ginresp.apiError
//
// @Router /send.php [POST]
// @Router /send.php [POST]
func (h MessageHandler) SendMessageCompat(g *gin.Context) ginresp.HTTPResponse {
type combined struct {
UserID *int64 `json:"user_id" form:"user_id"`
@ -86,7 +86,7 @@ func (h MessageHandler) SendMessageCompat(g *gin.Context) ginresp.HTTPResponse {
return ginresp.SendAPIError(g, 400, apierr.USER_NOT_FOUND, hl.USER_ID, "User not found (compat)", nil)
}
okResp, errResp := h.sendMessageInternal(g, ctx, langext.Ptr(models.UserID(*newid)), data.UserKey, nil, nil, data.Title, data.Content, data.Priority, data.UserMessageID, data.SendTimestamp, nil)
okResp, errResp := h.sendMessageInternal(g, ctx, langext.Ptr(models.UserID(*newid)), data.UserKey, nil, data.Title, data.Content, data.Priority, data.UserMessageID, data.SendTimestamp, nil)
if errResp != nil {
return *errResp
} else {
@ -122,24 +122,24 @@ func (h MessageHandler) SendMessageCompat(g *gin.Context) ginresp.HTTPResponse {
// Register swaggerdoc
//
// @Summary Register a new account
// @ID compat-register
// @Tags API-v1
// @Summary Register a new account
// @ID compat-register
// @Tags API-v1
//
// @Deprecated
// @Deprecated
//
// @Param fcm_token query string true "the (android) fcm token"
// @Param pro query string true "if the user is a paid account" Enums(true, false)
// @Param pro_token query string true "the (android) IAP token"
// @Param fcm_token query string true "the (android) fcm token"
// @Param pro query string true "if the user is a paid account" Enums(true, false)
// @Param pro_token query string true "the (android) IAP token"
//
// @Param fcm_token formData string true "the (android) fcm token"
// @Param pro formData string true "if the user is a paid account" Enums(true, false)
// @Param pro_token formData string true "the (android) IAP token"
// @Param fcm_token formData string true "the (android) fcm token"
// @Param pro formData string true "if the user is a paid account" Enums(true, false)
// @Param pro_token formData string true "the (android) IAP token"
//
// @Success 200 {object} handler.Register.response
// @Failure default {object} ginresp.compatAPIError
// @Success 200 {object} handler.Register.response
// @Failure default {object} ginresp.compatAPIError
//
// @Router /api/register.php [get]
// @Router /api/register.php [get]
func (h CompatHandler) Register(g *gin.Context) ginresp.HTTPResponse {
type query struct {
FCMToken *string `json:"fcm_token" form:"fcm_token"`
@ -195,8 +195,6 @@ func (h CompatHandler) Register(g *gin.Context) ginresp.HTTPResponse {
}
}
readKey := h.app.GenerateRandomAuthKey()
sendKey := h.app.GenerateRandomAuthKey()
adminKey := h.app.GenerateRandomAuthKey()
err := h.database.ClearFCMTokens(ctx, *data.FCMToken)
@ -211,11 +209,16 @@ func (h CompatHandler) Register(g *gin.Context) ginresp.HTTPResponse {
}
}
user, err := h.database.CreateUser(ctx, readKey, sendKey, adminKey, data.ProToken, nil)
user, err := h.database.CreateUser(ctx, data.ProToken, nil)
if err != nil {
return ginresp.CompatAPIError(0, "Failed to create user in db")
}
_, err = h.database.CreateKeyToken(ctx, "CompatKey", user.UserID, true, make([]models.ChannelID, 0), models.TokenPermissionList{models.PermAdmin}, adminKey)
if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to create admin-key in db", err)
}
_, err = h.database.CreateClient(ctx, user.UserID, models.ClientTypeAndroid, *data.FCMToken, "compat", "compat")
if err != nil {
return ginresp.CompatAPIError(0, "Failed to create client in db")
@ -230,7 +233,7 @@ func (h CompatHandler) Register(g *gin.Context) ginresp.HTTPResponse {
Success: true,
Message: "New user registered",
UserID: oldid,
UserKey: user.AdminKey,
UserKey: adminKey,
QuotaUsed: user.QuotaUsedToday(),
QuotaMax: user.QuotaPerDay(),
IsPro: user.IsPro,
@ -239,22 +242,22 @@ func (h CompatHandler) Register(g *gin.Context) ginresp.HTTPResponse {
// Info swaggerdoc
//
// @Summary Get information about the current user
// @ID compat-info
// @Tags API-v1
// @Summary Get information about the current user
// @ID compat-info
// @Tags API-v1
//
// @Deprecated
// @Deprecated
//
// @Param user_id query string true "the user_id"
// @Param user_key query string true "the user_key"
// @Param user_id query string true "the user_id"
// @Param user_key query string true "the user_key"
//
// @Param user_id formData string true "the user_id"
// @Param user_key formData string true "the user_key"
// @Param user_id formData string true "the user_id"
// @Param user_key formData string true "the user_key"
//
// @Success 200 {object} handler.Info.response
// @Failure default {object} ginresp.compatAPIError
// @Success 200 {object} handler.Info.response
// @Failure default {object} ginresp.compatAPIError
//
// @Router /api/info.php [get]
// @Router /api/info.php [get]
func (h CompatHandler) Info(g *gin.Context) ginresp.HTTPResponse {
type query struct {
UserID *int64 `json:"user_id" form:"user_id"`
@ -305,7 +308,14 @@ func (h CompatHandler) Info(g *gin.Context) ginresp.HTTPResponse {
return ginresp.CompatAPIError(0, "Failed to query user")
}
if user.AdminKey != *data.UserKey {
keytok, err := h.database.GetKeyTokenByToken(ctx, *data.UserKey)
if err == sql.ErrNoRows {
return ginresp.CompatAPIError(204, "Authentification failed")
}
if err != nil {
return ginresp.CompatAPIError(0, "Failed to query token")
}
if !keytok.IsAdmin(user.UserID) {
return ginresp.CompatAPIError(204, "Authentification failed")
}
@ -320,7 +330,7 @@ func (h CompatHandler) Info(g *gin.Context) ginresp.HTTPResponse {
Success: true,
Message: "ok",
UserID: *data.UserID,
UserKey: user.AdminKey,
UserKey: keytok.Token,
QuotaUsed: user.QuotaUsedToday(),
QuotaMax: user.QuotaPerDay(),
IsPro: langext.Conditional(user.IsPro, 1, 0),
@ -331,24 +341,24 @@ func (h CompatHandler) Info(g *gin.Context) ginresp.HTTPResponse {
// Ack swaggerdoc
//
// @Summary Acknowledge that a message was received
// @ID compat-ack
// @Tags API-v1
// @Summary Acknowledge that a message was received
// @ID compat-ack
// @Tags API-v1
//
// @Deprecated
// @Deprecated
//
// @Param user_id query string true "the user_id"
// @Param user_key query string true "the user_key"
// @Param scn_msg_id query string true "the message id"
// @Param user_id query string true "the user_id"
// @Param user_key query string true "the user_key"
// @Param scn_msg_id query string true "the message id"
//
// @Param user_id formData string true "the user_id"
// @Param user_key formData string true "the user_key"
// @Param scn_msg_id formData string true "the message id"
// @Param user_id formData string true "the user_id"
// @Param user_key formData string true "the user_key"
// @Param scn_msg_id formData string true "the message id"
//
// @Success 200 {object} handler.Ack.response
// @Failure default {object} ginresp.compatAPIError
// @Success 200 {object} handler.Ack.response
// @Failure default {object} ginresp.compatAPIError
//
// @Router /api/ack.php [get]
// @Router /api/ack.php [get]
func (h CompatHandler) Ack(g *gin.Context) ginresp.HTTPResponse {
type query struct {
UserID *int64 `json:"user_id" form:"user_id"`
@ -398,7 +408,14 @@ func (h CompatHandler) Ack(g *gin.Context) ginresp.HTTPResponse {
return ginresp.CompatAPIError(0, "Failed to query user")
}
if user.AdminKey != *data.UserKey {
keytok, err := h.database.GetKeyTokenByToken(ctx, *data.UserKey)
if err == sql.ErrNoRows {
return ginresp.CompatAPIError(204, "Authentification failed")
}
if err != nil {
return ginresp.CompatAPIError(0, "Failed to query token")
}
if !keytok.IsAdmin(user.UserID) {
return ginresp.CompatAPIError(204, "Authentification failed")
}
@ -432,22 +449,22 @@ func (h CompatHandler) Ack(g *gin.Context) ginresp.HTTPResponse {
// Requery swaggerdoc
//
// @Summary Return all not-acknowledged messages
// @ID compat-requery
// @Tags API-v1
// @Summary Return all not-acknowledged messages
// @ID compat-requery
// @Tags API-v1
//
// @Deprecated
// @Deprecated
//
// @Param user_id query string true "the user_id"
// @Param user_key query string true "the user_key"
// @Param user_id query string true "the user_id"
// @Param user_key query string true "the user_key"
//
// @Param user_id formData string true "the user_id"
// @Param user_key formData string true "the user_key"
// @Param user_id formData string true "the user_id"
// @Param user_key formData string true "the user_key"
//
// @Success 200 {object} handler.Requery.response
// @Failure default {object} ginresp.compatAPIError
// @Success 200 {object} handler.Requery.response
// @Failure default {object} ginresp.compatAPIError
//
// @Router /api/requery.php [get]
// @Router /api/requery.php [get]
func (h CompatHandler) Requery(g *gin.Context) ginresp.HTTPResponse {
type query struct {
UserID *int64 `json:"user_id" form:"user_id"`
@ -493,7 +510,14 @@ func (h CompatHandler) Requery(g *gin.Context) ginresp.HTTPResponse {
return ginresp.CompatAPIError(0, "Failed to query user")
}
if user.AdminKey != *data.UserKey {
keytok, err := h.database.GetKeyTokenByToken(ctx, *data.UserKey)
if err == sql.ErrNoRows {
return ginresp.CompatAPIError(204, "Authentification failed")
}
if err != nil {
return ginresp.CompatAPIError(0, "Failed to query token")
}
if !keytok.IsAdmin(user.UserID) {
return ginresp.CompatAPIError(204, "Authentification failed")
}
@ -536,24 +560,24 @@ func (h CompatHandler) Requery(g *gin.Context) ginresp.HTTPResponse {
// Update swaggerdoc
//
// @Summary Set the fcm-token (android)
// @ID compat-update
// @Tags API-v1
// @Summary Set the fcm-token (android)
// @ID compat-update
// @Tags API-v1
//
// @Deprecated
// @Deprecated
//
// @Param user_id query string true "the user_id"
// @Param user_key query string true "the user_key"
// @Param fcm_token query string true "the (android) fcm token"
// @Param user_id query string true "the user_id"
// @Param user_key query string true "the user_key"
// @Param fcm_token query string true "the (android) fcm token"
//
// @Param user_id formData string true "the user_id"
// @Param user_key formData string true "the user_key"
// @Param fcm_token formData string true "the (android) fcm token"
// @Param user_id formData string true "the user_id"
// @Param user_key formData string true "the user_key"
// @Param fcm_token formData string true "the (android) fcm token"
//
// @Success 200 {object} handler.Update.response
// @Failure default {object} ginresp.compatAPIError
// @Success 200 {object} handler.Update.response
// @Failure default {object} ginresp.compatAPIError
//
// @Router /api/update.php [get]
// @Router /api/update.php [get]
func (h CompatHandler) Update(g *gin.Context) ginresp.HTTPResponse {
type query struct {
UserID *int64 `json:"user_id" form:"user_id"`
@ -603,7 +627,14 @@ func (h CompatHandler) Update(g *gin.Context) ginresp.HTTPResponse {
return ginresp.CompatAPIError(0, "Failed to query user")
}
if user.AdminKey != *data.UserKey {
keytok, err := h.database.GetKeyTokenByToken(ctx, *data.UserKey)
if err == sql.ErrNoRows {
return ginresp.CompatAPIError(204, "Authentification failed")
}
if err != nil {
return ginresp.CompatAPIError(0, "Failed to query token")
}
if !keytok.IsAdmin(user.UserID) {
return ginresp.CompatAPIError(204, "Authentification failed")
}
@ -613,10 +644,13 @@ func (h CompatHandler) Update(g *gin.Context) ginresp.HTTPResponse {
}
newAdminKey := h.app.GenerateRandomAuthKey()
newReadKey := h.app.GenerateRandomAuthKey()
newSendKey := h.app.GenerateRandomAuthKey()
err = h.database.UpdateUserKeys(ctx, user.UserID, newSendKey, newReadKey, newAdminKey)
_, err = h.database.CreateKeyToken(ctx, "CompatKey", user.UserID, true, make([]models.ChannelID, 0), models.TokenPermissionList{models.PermAdmin}, newAdminKey)
if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to create admin-key in db", err)
}
err = h.database.DeleteKeyToken(ctx, keytok.KeyTokenID)
if err != nil {
return ginresp.CompatAPIError(0, "Failed to update keys")
}
@ -648,7 +682,7 @@ func (h CompatHandler) Update(g *gin.Context) ginresp.HTTPResponse {
Success: true,
Message: "user updated",
UserID: *data.UserID,
UserKey: user.AdminKey,
UserKey: newAdminKey,
QuotaUsed: user.QuotaUsedToday(),
QuotaMax: user.QuotaPerDay(),
IsPro: langext.Conditional(user.IsPro, 1, 0),
@ -657,24 +691,24 @@ func (h CompatHandler) Update(g *gin.Context) ginresp.HTTPResponse {
// Expand swaggerdoc
//
// @Summary Get a whole (potentially truncated) message
// @ID compat-expand
// @Tags API-v1
// @Summary Get a whole (potentially truncated) message
// @ID compat-expand
// @Tags API-v1
//
// @Deprecated
// @Deprecated
//
// @Param user_id query string true "The user_id"
// @Param user_key query string true "The user_key"
// @Param scn_msg_id query string true "The message-id"
// @Param user_id query string true "The user_id"
// @Param user_key query string true "The user_key"
// @Param scn_msg_id query string true "The message-id"
//
// @Param user_id formData string true "The user_id"
// @Param user_key formData string true "The user_key"
// @Param scn_msg_id formData string true "The message-id"
// @Param user_id formData string true "The user_id"
// @Param user_key formData string true "The user_key"
// @Param scn_msg_id formData string true "The message-id"
//
// @Success 200 {object} handler.Expand.response
// @Failure default {object} ginresp.compatAPIError
// @Success 200 {object} handler.Expand.response
// @Failure default {object} ginresp.compatAPIError
//
// @Router /api/expand.php [get]
// @Router /api/expand.php [get]
func (h CompatHandler) Expand(g *gin.Context) ginresp.HTTPResponse {
type query struct {
UserID *int64 `json:"user_id" form:"user_id"`
@ -723,7 +757,14 @@ func (h CompatHandler) Expand(g *gin.Context) ginresp.HTTPResponse {
return ginresp.CompatAPIError(0, "Failed to query user")
}
if user.AdminKey != *data.UserKey {
keytok, err := h.database.GetKeyTokenByToken(ctx, *data.UserKey)
if err == sql.ErrNoRows {
return ginresp.CompatAPIError(204, "Authentification failed")
}
if err != nil {
return ginresp.CompatAPIError(0, "Failed to query token")
}
if !keytok.IsAdmin(user.UserID) {
return ginresp.CompatAPIError(204, "Authentification failed")
}
@ -760,26 +801,26 @@ func (h CompatHandler) Expand(g *gin.Context) ginresp.HTTPResponse {
// Upgrade swaggerdoc
//
// @Summary Upgrade a free account to a paid account
// @ID compat-upgrade
// @Tags API-v1
// @Summary Upgrade a free account to a paid account
// @ID compat-upgrade
// @Tags API-v1
//
// @Deprecated
// @Deprecated
//
// @Param user_id query string true "the user_id"
// @Param user_key query string true "the user_key"
// @Param pro query string true "if the user is a paid account" Enums(true, false)
// @Param pro_token query string true "the (android) IAP token"
// @Param user_id query string true "the user_id"
// @Param user_key query string true "the user_key"
// @Param pro query string true "if the user is a paid account" Enums(true, false)
// @Param pro_token query string true "the (android) IAP token"
//
// @Param user_id formData string true "the user_id"
// @Param user_key formData string true "the user_key"
// @Param pro formData string true "if the user is a paid account" Enums(true, false)
// @Param pro_token formData string true "the (android) IAP token"
// @Param user_id formData string true "the user_id"
// @Param user_key formData string true "the user_key"
// @Param pro formData string true "if the user is a paid account" Enums(true, false)
// @Param pro_token formData string true "the (android) IAP token"
//
// @Success 200 {object} handler.Upgrade.response
// @Failure default {object} ginresp.compatAPIError
// @Success 200 {object} handler.Upgrade.response
// @Failure default {object} ginresp.compatAPIError
//
// @Router /api/upgrade.php [get]
// @Router /api/upgrade.php [get]
func (h CompatHandler) Upgrade(g *gin.Context) ginresp.HTTPResponse {
type query struct {
UserID *int64 `json:"user_id" form:"user_id"`
@ -835,7 +876,14 @@ func (h CompatHandler) Upgrade(g *gin.Context) ginresp.HTTPResponse {
return ginresp.CompatAPIError(0, "Failed to query user")
}
if user.AdminKey != *data.UserKey {
keytok, err := h.database.GetKeyTokenByToken(ctx, *data.UserKey)
if err == sql.ErrNoRows {
return ginresp.CompatAPIError(204, "Authentification failed")
}
if err != nil {
return ginresp.CompatAPIError(0, "Failed to query token")
}
if !keytok.IsAdmin(user.UserID) {
return ginresp.CompatAPIError(204, "Authentification failed")
}

View File

@ -40,28 +40,27 @@ func NewMessageHandler(app *logic.Application) MessageHandler {
// SendMessage swaggerdoc
//
// @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
// @Tags External
// @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
// @Tags External
//
// @Param query_data query handler.SendMessage.combined false " "
// @Param post_body body handler.SendMessage.combined false " "
// @Param form_body formData handler.SendMessage.combined false " "
// @Param query_data query handler.SendMessage.combined false " "
// @Param post_body body handler.SendMessage.combined false " "
// @Param form_body formData handler.SendMessage.combined false " "
//
// @Success 200 {object} handler.SendMessage.response
// @Failure 400 {object} ginresp.apiError
// @Failure 401 {object} ginresp.apiError "The user_id was not found or 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 500 {object} ginresp.apiError "An internal server error occurred - try again later"
// @Success 200 {object} handler.SendMessage.response
// @Failure 400 {object} ginresp.apiError
// @Failure 401 {object} ginresp.apiError "The user_id was not found or 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 500 {object} ginresp.apiError "An internal server error occurred - try again later"
//
// @Router / [POST]
// @Router /send [POST]
// @Router / [POST]
// @Router /send [POST]
func (h MessageHandler) SendMessage(g *gin.Context) ginresp.HTTPResponse {
type combined struct {
UserID *models.UserID `json:"user_id" form:"user_id" example:"7725" `
UserKey *string `json:"user_key" form:"user_key" example:"P3TNH8mvv14fm" `
KeyToken *string `json:"key" form:"key" example:"P3TNH8mvv14fm" `
Channel *string `json:"channel" form:"channel" example:"test" `
ChanKey *string `json:"chan_key" form:"chan_key" example:"qhnUbKcLgp6tg" `
Title *string `json:"title" form:"title" example:"Hello World" `
Content *string `json:"content" form:"content" example:"This is a message" `
Priority *int `json:"priority" form:"priority" example:"1" enums:"0,1,2" `
@ -95,7 +94,7 @@ func (h MessageHandler) SendMessage(g *gin.Context) ginresp.HTTPResponse {
// query has highest prio, then form, then json
data := dataext.ObjectMerge(dataext.ObjectMerge(b, f), q)
okResp, errResp := h.sendMessageInternal(g, ctx, data.UserID, data.UserKey, data.Channel, data.ChanKey, data.Title, data.Content, data.Priority, data.UserMessageID, data.SendTimestamp, data.SenderName)
okResp, errResp := h.sendMessageInternal(g, ctx, data.UserID, data.KeyToken, data.Channel, data.Title, data.Content, data.Priority, data.UserMessageID, data.SendTimestamp, data.SenderName)
if errResp != nil {
return *errResp
} else {
@ -129,7 +128,7 @@ func (h MessageHandler) SendMessage(g *gin.Context) ginresp.HTTPResponse {
}
}
func (h MessageHandler) sendMessageInternal(g *gin.Context, ctx *logic.AppContext, UserID *models.UserID, UserKey *string, Channel *string, ChanKey *string, Title *string, Content *string, Priority *int, UserMessageID *string, SendTimestamp *float64, SenderName *string) (*SendMessageResponse, *ginresp.HTTPResponse) {
func (h MessageHandler) sendMessageInternal(g *gin.Context, ctx *logic.AppContext, UserID *models.UserID, Key *string, Channel *string, Title *string, Content *string, Priority *int, UserMessageID *string, SendTimestamp *float64, SenderName *string) (*SendMessageResponse, *ginresp.HTTPResponse) {
if Title != nil {
Title = langext.Ptr(strings.TrimSpace(*Title))
}
@ -140,8 +139,8 @@ func (h MessageHandler) sendMessageInternal(g *gin.Context, ctx *logic.AppContex
if UserID == nil {
return nil, langext.Ptr(ginresp.SendAPIError(g, 400, apierr.MISSING_UID, hl.USER_ID, "Missing parameter [[user_id]]", nil))
}
if UserKey == nil {
return nil, langext.Ptr(ginresp.SendAPIError(g, 400, apierr.MISSING_TOK, hl.USER_KEY, "Missing parameter [[user_token]]", nil))
if Key == nil {
return nil, langext.Ptr(ginresp.SendAPIError(g, 400, apierr.MISSING_TOK, hl.USER_KEY, "Missing parameter [[key]]", nil))
}
if Title == nil {
return nil, langext.Ptr(ginresp.SendAPIError(g, 400, apierr.MISSING_TITLE, hl.TITLE, "Missing parameter [[title]]", nil))
@ -224,32 +223,13 @@ func (h MessageHandler) sendMessageInternal(g *gin.Context, ctx *logic.AppContex
return nil, langext.Ptr(ginresp.SendAPIError(g, 403, apierr.QUOTA_REACHED, hl.NONE, fmt.Sprintf("Daily quota reached (%d)", user.QuotaPerDay()), nil))
}
var channel models.Channel
if ChanKey != nil {
// foreign channel (+ channel send-key)
foreignChan, err := h.database.GetChannelByNameAndSendKey(ctx, channelInternalName, *ChanKey)
if err != nil {
return nil, langext.Ptr(ginresp.SendAPIError(g, 500, apierr.DATABASE_ERROR, hl.NONE, "Failed to query (foreign) channel", err))
}
if foreignChan == nil {
return nil, langext.Ptr(ginresp.SendAPIError(g, 400, apierr.CHANNEL_NOT_FOUND, hl.CHANNEL, "(Foreign) Channel not found", err))
}
channel = *foreignChan
} else {
// own channel
channel, err = h.app.GetOrCreateChannel(ctx, *UserID, channelDisplayName, channelInternalName)
if err != nil {
return nil, langext.Ptr(ginresp.SendAPIError(g, 500, apierr.DATABASE_ERROR, hl.NONE, "Failed to query/create (owned) channel", err))
}
channel, err := h.app.GetOrCreateChannel(ctx, *UserID, channelDisplayName, channelInternalName)
if err != nil {
return nil, langext.Ptr(ginresp.SendAPIError(g, 500, apierr.DATABASE_ERROR, hl.NONE, "Failed to query/create (owned) channel", err))
}
selfChanAdmin := *UserID == channel.OwnerUserID && *UserKey == user.AdminKey
selfChanSend := *UserID == channel.OwnerUserID && *UserKey == user.SendKey
forgChanSend := *UserID != channel.OwnerUserID && ChanKey != nil && *ChanKey == channel.SendKey
if !selfChanAdmin && !selfChanSend && !forgChanSend {
keytok, permResp := ctx.CheckPermissionSend(channel, *Key)
if permResp != nil {
return nil, langext.Ptr(ginresp.SendAPIError(g, 401, apierr.USER_AUTH_FAILED, hl.USER_KEY, "You are not authorized for this action", nil))
}
@ -287,6 +267,11 @@ func (h MessageHandler) sendMessageInternal(g *gin.Context, ctx *logic.AppContex
return nil, langext.Ptr(ginresp.SendAPIError(g, 500, apierr.DATABASE_ERROR, hl.NONE, "Failed to inc channel msg-counter", err))
}
err = h.database.IncKeyTokenMessageCounter(ctx, keytok.KeyTokenID)
if err != nil {
return nil, langext.Ptr(ginresp.SendAPIError(g, 500, apierr.DATABASE_ERROR, hl.NONE, "Failed to inc token msg-counter", err))
}
for _, sub := range subscriptions {
clients, err := h.database.ListClients(ctx, sub.SubscriberUserID)
if err != nil {

View File

@ -37,17 +37,17 @@ func NewRouter(app *logic.Application) *Router {
// Init swaggerdocs
//
// @title SimpleCloudNotifier API
// @version 2.0
// @description API for SCN
// @host scn.blackforestbytes.com
// @title SimpleCloudNotifier API
// @version 2.0
// @description API for SCN
// @host scn.blackforestbytes.com
//
// @tag.name External
// @tag.name API-v1
// @tag.name API-v2
// @tag.name Common
// @tag.name External
// @tag.name API-v1
// @tag.name API-v2
// @tag.name Common
//
// @BasePath /
// @BasePath /
func (r *Router) Init(e *gin.Engine) error {
if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
@ -127,6 +127,12 @@ func (r *Router) Init(e *gin.Engine) error {
apiv2.GET("/users/:uid", r.Wrap(r.apiHandler.GetUser))
apiv2.PATCH("/users/:uid", r.Wrap(r.apiHandler.UpdateUser))
apiv2.GET("/users/:uid/keys", r.Wrap(r.apiHandler.ListUserKeys))
apiv2.POST("/users/:uid/keys", r.Wrap(r.apiHandler.CreateUserKey))
apiv2.GET("/users/:uid/keys/:kid", r.Wrap(r.apiHandler.GetUserKey))
apiv2.PATCH("/users/:uid/keys/:kid", r.Wrap(r.apiHandler.UpdateUserKey))
apiv2.DELETE("/users/:uid/keys/:kid", r.Wrap(r.apiHandler.DeleteUserKey))
apiv2.GET("/users/:uid/clients", r.Wrap(r.apiHandler.ListClients))
apiv2.GET("/users/:uid/clients/:cid", r.Wrap(r.apiHandler.GetClient))
apiv2.POST("/users/:uid/clients", r.Wrap(r.apiHandler.AddClient))

View File

@ -12,36 +12,36 @@ import (
type Config struct {
Namespace string
BaseURL string `env:"SCN_URL"`
GinDebug bool `env:"SCN_GINDEBUG"`
LogLevel zerolog.Level `env:"SCN_LOGLEVEL"`
ServerIP string `env:"SCN_IP"`
ServerPort string `env:"SCN_PORT"`
DBMain DBConfig `env:"SCN_DB_MAIN"`
DBRequests DBConfig `env:"SCN_DB_REQUESTS"`
DBLogs DBConfig `env:"SCN_DB_LOGS"`
RequestTimeout time.Duration `env:"SCN_REQUEST_TIMEOUT"`
RequestMaxRetry int `env:"SCN_REQUEST_MAXRETRY"`
RequestRetrySleep time.Duration `env:"SCN_REQUEST_RETRYSLEEP"`
Cors bool `env:"SCN_CORS"`
ReturnRawErrors bool `env:"SCN_ERROR_RETURN"`
DummyFirebase bool `env:"SCN_DUMMY_FB"`
DummyGoogleAPI bool `env:"SCN_DUMMY_GOOG"`
FirebaseTokenURI string `env:"SCN_FB_TOKENURI"`
FirebaseProjectID string `env:"SCN_FB_PROJECTID"`
FirebasePrivKeyID string `env:"SCN_FB_PRIVATEKEYID"`
FirebaseClientMail string `env:"SCN_FB_CLIENTEMAIL"`
FirebasePrivateKey string `env:"SCN_FB_PRIVATEKEY"`
GoogleAPITokenURI string `env:"SCN_GOOG_TOKENURI"`
GoogleAPIPrivKeyID string `env:"SCN_GOOG_PRIVATEKEYID"`
GoogleAPIClientMail string `env:"SCN_GOOG_CLIENTEMAIL"`
GoogleAPIPrivateKey string `env:"SCN_GOOG_PRIVATEKEY"`
GooglePackageName string `env:"SCN_GOOG_PACKAGENAME"`
GoogleProProductID string `env:"SCN_GOOG_PROPRODUCTID"`
ReqLogEnabled bool `env:"SCN_REQUESTLOG_ENABLED"`
ReqLogMaxBodySize int `env:"SCN_REQUESTLOG_MAXBODYSIZE"`
ReqLogHistoryMaxCount int `env:"SCN_REQUESTLOG_HISTORY_MAXCOUNT"`
ReqLogHistoryMaxDuration time.Duration `env:"SCN_REQUESTLOG_HISTORY_MAXDURATION"`
BaseURL string `env:"URL"`
GinDebug bool `env:"GINDEBUG"`
LogLevel zerolog.Level `env:"LOGLEVEL"`
ServerIP string `env:"IP"`
ServerPort string `env:"PORT"`
DBMain DBConfig `env:"DB_MAIN"`
DBRequests DBConfig `env:"DB_REQUESTS"`
DBLogs DBConfig `env:"DB_LOGS"`
RequestTimeout time.Duration `env:"REQUEST_TIMEOUT"`
RequestMaxRetry int `env:"REQUEST_MAXRETRY"`
RequestRetrySleep time.Duration `env:"REQUEST_RETRYSLEEP"`
Cors bool `env:"CORS"`
ReturnRawErrors bool `env:"ERROR_RETURN"`
DummyFirebase bool `env:"DUMMY_FB"`
DummyGoogleAPI bool `env:"DUMMY_GOOG"`
FirebaseTokenURI string `env:"FB_TOKENURI"`
FirebaseProjectID string `env:"FB_PROJECTID"`
FirebasePrivKeyID string `env:"FB_PRIVATEKEYID"`
FirebaseClientMail string `env:"FB_CLIENTEMAIL"`
FirebasePrivateKey string `env:"FB_PRIVATEKEY"`
GoogleAPITokenURI string `env:"GOOG_TOKENURI"`
GoogleAPIPrivKeyID string `env:"GOOG_PRIVATEKEYID"`
GoogleAPIClientMail string `env:"GOOG_CLIENTEMAIL"`
GoogleAPIPrivateKey string `env:"GOOG_PRIVATEKEY"`
GooglePackageName string `env:"GOOG_PACKAGENAME"`
GoogleProProductID string `env:"GOOG_PROPRODUCTID"`
ReqLogEnabled bool `env:"REQUESTLOG_ENABLED"`
ReqLogMaxBodySize int `env:"REQUESTLOG_MAXBODYSIZE"`
ReqLogHistoryMaxCount int `env:"REQUESTLOG_HISTORY_MAXCOUNT"`
ReqLogHistoryMaxDuration time.Duration `env:"REQUESTLOG_HISTORY_MAXDURATION"`
}
type DBConfig struct {
@ -430,7 +430,7 @@ func GetConfig(ns string) (Config, bool) {
}
if cfn, ok := allConfig[ns]; ok {
c := cfn()
err := confext.ApplyEnvOverrides(&c, "_")
err := confext.ApplyEnvOverrides("SCN_", &c, "_")
if err != nil {
panic(err)
}

View File

@ -8,7 +8,7 @@ import (
"time"
)
type Mode string
type Mode string //@enum:type
const (
CTMStart = "START"

View File

@ -32,31 +32,6 @@ func (db *Database) GetChannelByName(ctx TxContext, userid models.UserID, chanNa
return &channel, nil
}
func (db *Database) GetChannelByNameAndSendKey(ctx TxContext, chanName string, sendKey string) (*models.Channel, error) {
tx, err := ctx.GetOrCreateTransaction(db)
if err != nil {
return nil, err
}
rows, err := tx.Query(ctx, "SELECT * FROM channels WHERE internal_name = :chan_name OR send_key = :send_key LIMIT 1", sq.PP{
"chan_name": chanName,
"send_key": sendKey,
})
if err != nil {
return nil, err
}
channel, err := models.DecodeChannel(rows)
if err == sql.ErrNoRows {
return nil, nil
}
if err != nil {
return nil, err
}
return &channel, nil
}
func (db *Database) GetChannelByID(ctx TxContext, chanid models.ChannelID) (*models.Channel, error) {
tx, err := ctx.GetOrCreateTransaction(db)
if err != nil {
@ -81,7 +56,7 @@ func (db *Database) GetChannelByID(ctx TxContext, chanid models.ChannelID) (*mod
return &channel, nil
}
func (db *Database) CreateChannel(ctx TxContext, userid models.UserID, dispName string, intName string, subscribeKey string, sendKey string) (models.Channel, error) {
func (db *Database) CreateChannel(ctx TxContext, userid models.UserID, dispName string, intName string, subscribeKey string) (models.Channel, error) {
tx, err := ctx.GetOrCreateTransaction(db)
if err != nil {
return models.Channel{}, err
@ -91,15 +66,14 @@ func (db *Database) CreateChannel(ctx TxContext, userid models.UserID, dispName
channelid := models.NewChannelID()
_, err = tx.Exec(ctx, "INSERT INTO channels (channel_id, owner_user_id, display_name, internal_name, description_name, subscribe_key, send_key, timestamp_created) VALUES (:cid, :ouid, :dnam, :inam, :hnam, :subkey, :sendkey, :ts)", sq.PP{
"cid": channelid,
"ouid": userid,
"dnam": dispName,
"inam": intName,
"hnam": nil,
"subkey": subscribeKey,
"sendkey": sendKey,
"ts": time2DB(now),
_, err = tx.Exec(ctx, "INSERT INTO channels (channel_id, owner_user_id, display_name, internal_name, description_name, subscribe_key, timestamp_created) VALUES (:cid, :ouid, :dnam, :inam, :hnam, :subkey, :ts)", sq.PP{
"cid": channelid,
"ouid": userid,
"dnam": dispName,
"inam": intName,
"hnam": nil,
"subkey": subscribeKey,
"ts": time2DB(now),
})
if err != nil {
return models.Channel{}, err
@ -111,7 +85,6 @@ func (db *Database) CreateChannel(ctx TxContext, userid models.UserID, dispName
DisplayName: dispName,
InternalName: intName,
SubscribeKey: subscribeKey,
SendKey: sendKey,
TimestampCreated: now,
TimestampLastSent: nil,
MessagesSent: 0,
@ -244,23 +217,6 @@ func (db *Database) IncChannelMessageCounter(ctx TxContext, channel models.Chann
return nil
}
func (db *Database) UpdateChannelSendKey(ctx TxContext, channelid models.ChannelID, newkey string) error {
tx, err := ctx.GetOrCreateTransaction(db)
if err != nil {
return err
}
_, err = tx.Exec(ctx, "UPDATE channels SET send_key = :key WHERE channel_id = :cid", sq.PP{
"key": newkey,
"cid": channelid,
})
if err != nil {
return err
}
return nil
}
func (db *Database) UpdateChannelSubscribeKey(ctx TxContext, channelid models.ChannelID, newkey string) error {
tx, err := ctx.GetOrCreateTransaction(db)
if err != nil {

View File

@ -0,0 +1,227 @@
package primary
import (
"blackforestbytes.com/simplecloudnotifier/models"
"database/sql"
"gogs.mikescher.com/BlackForestBytes/goext/langext"
"gogs.mikescher.com/BlackForestBytes/goext/sq"
"strings"
"time"
)
func (db *Database) CreateKeyToken(ctx TxContext, name string, owner models.UserID, allChannels bool, channels []models.ChannelID, permissions models.TokenPermissionList, token string) (models.KeyToken, error) {
tx, err := ctx.GetOrCreateTransaction(db)
if err != nil {
return models.KeyToken{}, err
}
now := time.Now().UTC()
keyTokenid := models.NewKeyTokenID()
_, err = tx.Exec(ctx, "INSERT INTO keytokens (keytoken_id, name, timestamp_created, owner_user_id, all_channels, channels, token, permissions) VALUES (:tid, :nam, :tsc, :owr, :all, :cha, :tok, :prm)", sq.PP{
"tid": keyTokenid,
"nam": name,
"tsc": time2DB(now),
"owr": owner.String(),
"all": bool2DB(allChannels),
"cha": strings.Join(langext.ArrMap(channels, func(v models.ChannelID) string { return v.String() }), ";"),
"tok": token,
"prm": permissions.String(),
})
if err != nil {
return models.KeyToken{}, err
}
return models.KeyToken{
KeyTokenID: keyTokenid,
Name: name,
TimestampCreated: now,
TimestampLastUsed: nil,
OwnerUserID: owner,
AllChannels: allChannels,
Channels: channels,
Token: token,
Permissions: permissions,
MessagesSent: 0,
}, nil
}
func (db *Database) ListKeyTokens(ctx TxContext, ownerID models.UserID) ([]models.KeyToken, error) {
tx, err := ctx.GetOrCreateTransaction(db)
if err != nil {
return nil, err
}
rows, err := tx.Query(ctx, "SELECT * FROM keytokens WHERE owner_user_id = :uid ORDER BY keytokens.timestamp_created DESC, keytokens.keytoken_id ASC", sq.PP{"uid": ownerID})
if err != nil {
return nil, err
}
data, err := models.DecodeKeyTokens(rows)
if err != nil {
return nil, err
}
return data, nil
}
func (db *Database) GetKeyToken(ctx TxContext, userid models.UserID, keyTokenid models.KeyTokenID) (models.KeyToken, error) {
tx, err := ctx.GetOrCreateTransaction(db)
if err != nil {
return models.KeyToken{}, err
}
rows, err := tx.Query(ctx, "SELECT * FROM keytokens WHERE owner_user_id = :uid AND keytoken_id = :cid LIMIT 1", sq.PP{
"uid": userid,
"cid": keyTokenid,
})
if err != nil {
return models.KeyToken{}, err
}
keyToken, err := models.DecodeKeyToken(rows)
if err != nil {
return models.KeyToken{}, err
}
return keyToken, nil
}
func (db *Database) GetKeyTokenByToken(ctx TxContext, key string) (*models.KeyToken, error) {
tx, err := ctx.GetOrCreateTransaction(db)
if err != nil {
return nil, err
}
rows, err := tx.Query(ctx, "SELECT * FROM keytokens WHERE token = :key LIMIT 1", sq.PP{"key": key})
if err != nil {
return nil, err
}
user, err := models.DecodeKeyToken(rows)
if err == sql.ErrNoRows {
return nil, nil
}
if err != nil {
return nil, err
}
return &user, nil
}
func (db *Database) DeleteKeyToken(ctx TxContext, keyTokenid models.KeyTokenID) error {
tx, err := ctx.GetOrCreateTransaction(db)
if err != nil {
return err
}
_, err = tx.Exec(ctx, "DELETE FROM keytokens WHERE keytoken_id = :tid", sq.PP{"tid": keyTokenid})
if err != nil {
return err
}
return nil
}
func (db *Database) UpdateKeyTokenName(ctx TxContext, keyTokenid models.KeyTokenID, name string) error {
tx, err := ctx.GetOrCreateTransaction(db)
if err != nil {
return err
}
_, err = tx.Exec(ctx, "UPDATE keytokens SET name = :nam WHERE keytoken_id = :tid", sq.PP{
"nam": name,
"tid": keyTokenid,
})
if err != nil {
return err
}
return nil
}
func (db *Database) UpdateKeyTokenPermissions(ctx TxContext, keyTokenid models.KeyTokenID, perm models.TokenPermissionList) error {
tx, err := ctx.GetOrCreateTransaction(db)
if err != nil {
return err
}
_, err = tx.Exec(ctx, "UPDATE keytokens SET permissions = :prm WHERE keytoken_id = :tid", sq.PP{
"tid": keyTokenid,
"prm": perm.String(),
})
if err != nil {
return err
}
return nil
}
func (db *Database) UpdateKeyTokenAllChannels(ctx TxContext, keyTokenid models.KeyTokenID, allChannels bool) error {
tx, err := ctx.GetOrCreateTransaction(db)
if err != nil {
return err
}
_, err = tx.Exec(ctx, "UPDATE keytokens SET all_channels = :all WHERE keytoken_id = :tid", sq.PP{
"tid": keyTokenid,
"all": bool2DB(allChannels),
})
if err != nil {
return err
}
return nil
}
func (db *Database) UpdateKeyTokenChannels(ctx TxContext, keyTokenid models.KeyTokenID, channels []models.ChannelID) error {
tx, err := ctx.GetOrCreateTransaction(db)
if err != nil {
return err
}
_, err = tx.Exec(ctx, "UPDATE keytokens SET channels = :cha WHERE keytoken_id = :tid", sq.PP{
"tid": keyTokenid,
"cha": strings.Join(langext.ArrMap(channels, func(v models.ChannelID) string { return v.String() }), ";"),
})
if err != nil {
return err
}
return nil
}
func (db *Database) IncKeyTokenMessageCounter(ctx TxContext, keyTokenid models.KeyTokenID) error {
tx, err := ctx.GetOrCreateTransaction(db)
if err != nil {
return err
}
_, err = tx.Exec(ctx, "UPDATE keytokens SET messages_sent = messages_sent + 1, timestamp_lastused = :ts WHERE keytoken_id = :tid", sq.PP{
"ts": time2DB(time.Now()),
"tid": keyTokenid,
})
if err != nil {
return err
}
return nil
}
func (db *Database) UpdateKeyTokenLastUsed(ctx TxContext, keyTokenid models.KeyTokenID) error {
tx, err := ctx.GetOrCreateTransaction(db)
if err != nil {
return err
}
_, err = tx.Exec(ctx, "UPDATE keytokens SET timestamp_lastused = :ts WHERE keytoken_id = :tid", sq.PP{
"ts": time2DB(time.Now()),
"tid": keyTokenid,
})
if err != nil {
return err
}
return nil
}

View File

@ -4,10 +4,6 @@ CREATE TABLE users
username TEXT NULL DEFAULT NULL,
send_key TEXT NOT NULL,
read_key TEXT NOT NULL,
admin_key TEXT NOT NULL,
timestamp_created INTEGER NOT NULL,
timestamp_lastread INTEGER NULL DEFAULT NULL,
timestamp_lastsent INTEGER NULL DEFAULT NULL,
@ -25,6 +21,29 @@ CREATE TABLE users
CREATE UNIQUE INDEX "idx_users_protoken" ON users (pro_token) WHERE pro_token IS NOT NULL;
CREATE TABLE keytokens
(
keytoken_id TEXT NOT NULL,
timestamp_created INTEGER NOT NULL,
timestamp_lastused INTEGER NULL DEFAULT NULL,
name TEXT NOT NULL,
owner_user_id TEXT NOT NULL,
all_channels INTEGER CHECK(all_channels IN (0, 1)) NOT NULL,
channels TEXT NOT NULL,
token TEXT NOT NULL,
permissions TEXT NOT NULL,
messages_sent INTEGER NOT NULL DEFAULT '0',
PRIMARY KEY (keytoken_id)
) STRICT;
CREATE UNIQUE INDEX "idx_keytokens_token" ON keytokens (token);
CREATE TABLE clients
(
client_id TEXT NOT NULL,
@ -55,7 +74,6 @@ CREATE TABLE channels
description_name TEXT NULL,
subscribe_key TEXT NOT NULL,
send_key TEXT NOT NULL,
timestamp_created INTEGER NOT NULL,
timestamp_lastsent INTEGER NULL DEFAULT NULL,

View File

@ -3,12 +3,11 @@ package primary
import (
scn "blackforestbytes.com/simplecloudnotifier"
"blackforestbytes.com/simplecloudnotifier/models"
"database/sql"
"gogs.mikescher.com/BlackForestBytes/goext/sq"
"time"
)
func (db *Database) CreateUser(ctx TxContext, readKey string, sendKey string, adminKey string, protoken *string, username *string) (models.User, error) {
func (db *Database) CreateUser(ctx TxContext, protoken *string, username *string) (models.User, error) {
tx, err := ctx.GetOrCreateTransaction(db)
if err != nil {
return models.User{}, err
@ -18,12 +17,9 @@ func (db *Database) CreateUser(ctx TxContext, readKey string, sendKey string, ad
userid := models.NewUserID()
_, err = tx.Exec(ctx, "INSERT INTO users (user_id, username, read_key, send_key, admin_key, is_pro, pro_token, timestamp_created) VALUES (:uid, :un, :rk, :sk, :ak, :pro, :tok, :ts)", sq.PP{
_, err = tx.Exec(ctx, "INSERT INTO users (user_id, username, is_pro, pro_token, timestamp_created) VALUES (:uid, :un, :pro, :tok, :ts)", sq.PP{
"uid": userid,
"un": username,
"rk": readKey,
"sk": sendKey,
"ak": adminKey,
"pro": bool2DB(protoken != nil),
"tok": protoken,
"ts": time2DB(now),
@ -35,9 +31,6 @@ func (db *Database) CreateUser(ctx TxContext, readKey string, sendKey string, ad
return models.User{
UserID: userid,
Username: username,
ReadKey: readKey,
SendKey: sendKey,
AdminKey: adminKey,
TimestampCreated: now,
TimestampLastRead: nil,
TimestampLastSent: nil,
@ -63,28 +56,6 @@ func (db *Database) ClearProTokens(ctx TxContext, protoken string) error {
return nil
}
func (db *Database) GetUserByKey(ctx TxContext, key string) (*models.User, error) {
tx, err := ctx.GetOrCreateTransaction(db)
if err != nil {
return nil, err
}
rows, err := tx.Query(ctx, "SELECT * FROM users WHERE admin_key = :key OR send_key = :key OR read_key = :key LIMIT 1", sq.PP{"key": key})
if err != nil {
return nil, err
}
user, err := models.DecodeUser(rows)
if err == sql.ErrNoRows {
return nil, nil
}
if err != nil {
return nil, err
}
return &user, nil
}
func (db *Database) GetUser(ctx TxContext, userid models.UserID) (models.User, error) {
tx, err := ctx.GetOrCreateTransaction(db)
if err != nil {
@ -177,73 +148,3 @@ func (db *Database) UpdateUserLastRead(ctx TxContext, userid models.UserID) erro
return nil
}
func (db *Database) UpdateUserKeys(ctx TxContext, userid models.UserID, sendKey string, readKey string, adminKey string) error {
tx, err := ctx.GetOrCreateTransaction(db)
if err != nil {
return err
}
_, err = tx.Exec(ctx, "UPDATE users SET send_key = :sk, read_key = :rk, admin_key = :ak WHERE user_id = :uid", sq.PP{
"sk": sendKey,
"rk": readKey,
"ak": adminKey,
"uid": userid,
})
if err != nil {
return err
}
return nil
}
func (db *Database) UpdateUserSendKey(ctx TxContext, userid models.UserID, newkey string) error {
tx, err := ctx.GetOrCreateTransaction(db)
if err != nil {
return err
}
_, err = tx.Exec(ctx, "UPDATE users SET send_key = :sk WHERE user_id = :uid", sq.PP{
"sk": newkey,
"uid": userid,
})
if err != nil {
return err
}
return nil
}
func (db *Database) UpdateUserReadKey(ctx TxContext, userid models.UserID, newkey string) error {
tx, err := ctx.GetOrCreateTransaction(db)
if err != nil {
return err
}
_, err = tx.Exec(ctx, "UPDATE users SET read_key = :rk WHERE user_id = :uid", sq.PP{
"rk": newkey,
"uid": userid,
})
if err != nil {
return err
}
return nil
}
func (db *Database) UpdateUserAdminKey(ctx TxContext, userid models.UserID, newkey string) error {
tx, err := ctx.GetOrCreateTransaction(db)
if err != nil {
return err
}
_, err = tx.Exec(ctx, "UPDATE users SET admin_key = :ak WHERE user_id = :uid", sq.PP{
"ak": newkey,
"uid": userid,
})
if err != nil {
return err
}
return nil
}

View File

@ -9,7 +9,7 @@ require (
github.com/jmoiron/sqlx v1.3.5
github.com/mattn/go-sqlite3 v1.14.16
github.com/rs/zerolog v1.28.0
gogs.mikescher.com/BlackForestBytes/goext v0.0.59
gogs.mikescher.com/BlackForestBytes/goext v0.0.103
gopkg.in/loremipsum.v1 v1.1.0
)

View File

@ -81,6 +81,8 @@ gogs.mikescher.com/BlackForestBytes/goext v0.0.58 h1:W53yfHhpFQS13zgtzCjfJQ42WG0
gogs.mikescher.com/BlackForestBytes/goext v0.0.58/go.mod h1:ZEXyKUr8t0EKdPN1FYdk0klY7N8OwXxipGE9lWgpVE8=
gogs.mikescher.com/BlackForestBytes/goext v0.0.59 h1:3bHSjqgty9yp0EIyqwGAb06ZS7bLvm806zRj6j+WOEE=
gogs.mikescher.com/BlackForestBytes/goext v0.0.59/go.mod h1:ZEXyKUr8t0EKdPN1FYdk0klY7N8OwXxipGE9lWgpVE8=
gogs.mikescher.com/BlackForestBytes/goext v0.0.103 h1:CkRVpRrTlq9k3mdTNGQAr4cxaXHsKdUJNjHt5Maas4k=
gogs.mikescher.com/BlackForestBytes/goext v0.0.103/go.mod h1:w8JlyUHpoOJmW5GxsiheZkFh3vn8Mp80ynSVOFLszL0=
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.4.0 h1:UVQgzMY87xqpKNgb+kDsll2Igd33HszWHFLmpaRMq/8=
golang.org/x/crypto v0.4.0/go.mod h1:3quD/ATkf6oY+rnes5c3ExXTbLc8mueNue5/DoinL80=

View File

@ -35,7 +35,7 @@ func NewAndroidPublisherAPI(conf scn.Config) (AndroidPublisherClient, error) {
}, nil
}
type PurchaseType int
type PurchaseType int //@enum:type
const (
PurchaseTypeTest PurchaseType = 0 // i.e. purchased from a license testing account
@ -43,14 +43,14 @@ const (
PurchaseTypeRewarded PurchaseType = 2 // i.e. from watching a video ad instead of paying
)
type ConsumptionState int
type ConsumptionState int //@enum:type
const (
ConsumptionStateYetToBeConsumed ConsumptionState = 0
ConsumptionStateConsumed ConsumptionState = 1
)
type PurchaseState int
type PurchaseState int //@enum:type
const (
PurchaseStatePurchased PurchaseState = 0
@ -58,7 +58,7 @@ const (
PurchaseStatePending PurchaseState = 2
)
type AcknowledgementState int
type AcknowledgementState int //@enum:type
const (
AcknowledgementStateYetToBeAcknowledged AcknowledgementState = 0

View File

@ -14,6 +14,7 @@ import (
)
type AppContext struct {
app *Application
inner context.Context
cancelFunc context.CancelFunc
cancelled bool
@ -22,8 +23,9 @@ type AppContext struct {
ginContext *gin.Context
}
func CreateAppContext(g *gin.Context, innerCtx context.Context, cancelFn context.CancelFunc) *AppContext {
func CreateAppContext(app *Application, g *gin.Context, innerCtx context.Context, cancelFn context.CancelFunc) *AppContext {
return &AppContext{
app: app,
inner: innerCtx,
cancelFunc: cancelFn,
cancelled: false,

View File

@ -248,7 +248,7 @@ func (app *Application) StartRequest(g *gin.Context, uri any, query any, body an
}
ictx, cancel := context.WithTimeout(context.Background(), app.Config.RequestTimeout)
actx := CreateAppContext(g, ictx, cancel)
actx := CreateAppContext(app, g, ictx, cancel)
authheader := g.GetHeader("Authorization")
@ -280,19 +280,19 @@ func (app *Application) getPermissions(ctx *AppContext, hdr string) (models.Perm
key := strings.TrimSpace(hdr[4:])
user, err := app.Database.Primary.GetUserByKey(ctx, key)
tok, err := app.Database.Primary.GetKeyTokenByToken(ctx, key)
if err != nil {
return models.PermissionSet{}, err
}
if user != nil && user.SendKey == key {
return models.PermissionSet{UserID: langext.Ptr(user.UserID), KeyType: models.PermKeyTypeUserSend}, nil
}
if user != nil && user.ReadKey == key {
return models.PermissionSet{UserID: langext.Ptr(user.UserID), KeyType: models.PermKeyTypeUserRead}, nil
}
if user != nil && user.AdminKey == key {
return models.PermissionSet{UserID: langext.Ptr(user.UserID), KeyType: models.PermKeyTypeUserAdmin}, nil
if tok != nil {
err = app.Database.Primary.UpdateKeyTokenLastUsed(ctx, tok.KeyTokenID)
if err != nil {
return models.PermissionSet{}, err
}
return models.PermissionSet{Token: tok}, nil
}
return models.NewEmptyPermissions(), nil
@ -309,9 +309,8 @@ func (app *Application) GetOrCreateChannel(ctx *AppContext, userid models.UserID
}
subscribeKey := app.GenerateRandomAuthKey()
sendKey := app.GenerateRandomAuthKey()
newChan, err := app.Database.Primary.CreateChannel(ctx, userid, displayChanName, intChanName, subscribeKey, sendKey)
newChan, err := app.Database.Primary.CreateChannel(ctx, userid, displayChanName, intChanName, subscribeKey)
if err != nil {
return models.Channel{}, err
}

View File

@ -4,94 +4,116 @@ import (
"blackforestbytes.com/simplecloudnotifier/api/apierr"
"blackforestbytes.com/simplecloudnotifier/api/ginresp"
"blackforestbytes.com/simplecloudnotifier/models"
"database/sql"
"gogs.mikescher.com/BlackForestBytes/goext/langext"
)
func (ac *AppContext) CheckPermissionUserRead(userid models.UserID) *ginresp.HTTPResponse {
p := ac.permissions
if p.UserID != nil && *p.UserID == userid && p.KeyType == models.PermKeyTypeUserRead {
return nil
}
if p.UserID != nil && *p.UserID == userid && p.KeyType == models.PermKeyTypeUserAdmin {
if p.Token != nil && p.Token.IsUserRead(userid) {
return nil
}
return langext.Ptr(ginresp.APIError(ac.ginContext, 401, apierr.USER_AUTH_FAILED, "You are not authorized for this action", nil))
}
func (ac *AppContext) CheckPermissionRead() *ginresp.HTTPResponse {
func (ac *AppContext) CheckPermissionSelfAllMessagesRead() *ginresp.HTTPResponse {
p := ac.permissions
if p.UserID != nil && p.KeyType == models.PermKeyTypeUserRead {
if p.Token != nil && p.Token.IsAllMessagesRead(p.Token.OwnerUserID) {
return nil
}
if p.UserID != nil && p.KeyType == models.PermKeyTypeUserAdmin {
return langext.Ptr(ginresp.APIError(ac.ginContext, 401, apierr.USER_AUTH_FAILED, "You are not authorized for this action", nil))
}
func (ac *AppContext) CheckPermissionAllMessagesRead(userid models.UserID) *ginresp.HTTPResponse {
p := ac.permissions
if p.Token != nil && p.Token.IsAllMessagesRead(userid) {
return nil
}
return langext.Ptr(ginresp.APIError(ac.ginContext, 401, apierr.USER_AUTH_FAILED, "You are not authorized for this action", nil))
}
func (ac *AppContext) CheckPermissionChanMessagesRead(channel models.Channel) *ginresp.HTTPResponse {
p := ac.permissions
if p.Token != nil && p.Token.IsChannelMessagesRead(channel.ChannelID) {
if channel.OwnerUserID == p.Token.OwnerUserID {
return nil // owned channel
} else {
sub, err := ac.app.Database.Primary.GetSubscriptionBySubscriber(ac, p.Token.OwnerUserID, channel.ChannelID)
if err == sql.ErrNoRows {
return langext.Ptr(ginresp.APIError(ac.ginContext, 401, apierr.USER_AUTH_FAILED, "You are not authorized for this action", nil))
}
if err != nil {
return langext.Ptr(ginresp.APIError(ac.ginContext, 500, apierr.DATABASE_ERROR, "Failed to query subscription", err))
}
if !sub.Confirmed {
return langext.Ptr(ginresp.APIError(ac.ginContext, 401, apierr.USER_AUTH_FAILED, "You are not authorized for this action", nil))
}
}
}
return langext.Ptr(ginresp.APIError(ac.ginContext, 401, apierr.USER_AUTH_FAILED, "You are not authorized for this action", nil))
}
func (ac *AppContext) CheckPermissionUserAdmin(userid models.UserID) *ginresp.HTTPResponse {
p := ac.permissions
if p.UserID != nil && *p.UserID == userid && p.KeyType == models.PermKeyTypeUserAdmin {
if p.Token != nil && p.Token.IsAdmin(userid) {
return nil
}
return langext.Ptr(ginresp.APIError(ac.ginContext, 401, apierr.USER_AUTH_FAILED, "You are not authorized for this action", nil))
}
func (ac *AppContext) CheckPermissionSend() *ginresp.HTTPResponse {
p := ac.permissions
if p.UserID != nil && p.KeyType == models.PermKeyTypeUserSend {
return nil
func (ac *AppContext) CheckPermissionSend(channel models.Channel, key string) (*models.KeyToken, *ginresp.HTTPResponse) {
keytok, err := ac.app.Database.Primary.GetKeyTokenByToken(ac, key)
if err != nil {
return nil, langext.Ptr(ginresp.APIError(ac.ginContext, 500, apierr.DATABASE_ERROR, "Failed to query token", err))
}
if p.UserID != nil && p.KeyType == models.PermKeyTypeUserAdmin {
return nil
if keytok == nil {
return nil, langext.Ptr(ginresp.APIError(ac.ginContext, 401, apierr.USER_AUTH_FAILED, "You are not authorized for this action", nil))
}
return langext.Ptr(ginresp.APIError(ac.ginContext, 401, apierr.USER_AUTH_FAILED, "You are not authorized for this action", nil))
if keytok.IsChannelMessagesSend(channel) {
return keytok, nil
}
return nil, langext.Ptr(ginresp.APIError(ac.ginContext, 401, apierr.USER_AUTH_FAILED, "You are not authorized for this action", nil))
}
func (ac *AppContext) CheckPermissionAny() *ginresp.HTTPResponse {
func (ac *AppContext) CheckPermissionMessageRead(msg models.Message) bool {
p := ac.permissions
if p.KeyType == models.PermKeyTypeNone {
return langext.Ptr(ginresp.APIError(ac.ginContext, 401, apierr.USER_AUTH_FAILED, "You are not authorized for this action", nil))
}
return nil
}
func (ac *AppContext) CheckPermissionMessageReadDirect(msg models.Message) bool {
p := ac.permissions
if p.UserID != nil && msg.OwnerUserID == *p.UserID && p.KeyType == models.PermKeyTypeUserRead {
return true
}
if p.UserID != nil && msg.OwnerUserID == *p.UserID && p.KeyType == models.PermKeyTypeUserAdmin {
if p.Token != nil && p.Token.IsChannelMessagesRead(msg.ChannelID) {
return true
}
return false
}
func (ac *AppContext) CheckPermissionAny() *ginresp.HTTPResponse {
p := ac.permissions
if p.Token == nil {
return langext.Ptr(ginresp.APIError(ac.ginContext, 401, apierr.USER_AUTH_FAILED, "You are not authorized for this action", nil))
}
return nil
}
func (ac *AppContext) GetPermissionUserID() *models.UserID {
if ac.permissions.UserID == nil {
if ac.permissions.Token == nil {
return nil
} else {
return langext.Ptr(*ac.permissions.UserID)
return langext.Ptr(ac.permissions.Token.OwnerUserID)
}
}
func (ac *AppContext) IsPermissionUserRead() bool {
p := ac.permissions
return p.KeyType == models.PermKeyTypeUserRead || p.KeyType == models.PermKeyTypeUserAdmin
}
func (ac *AppContext) IsPermissionUserSend() bool {
p := ac.permissions
return p.KeyType == models.PermKeyTypeUserSend || p.KeyType == models.PermKeyTypeUserAdmin
}
func (ac *AppContext) IsPermissionUserAdmin() bool {
p := ac.permissions
return p.KeyType == models.PermKeyTypeUserAdmin
func (ac *AppContext) GetPermissionKeyTokenID() *models.KeyTokenID {
if ac.permissions.Token == nil {
return nil
} else {
return langext.Ptr(ac.permissions.Token.KeyTokenID)
}
}

View File

@ -14,7 +14,6 @@ type Channel struct {
DisplayName string
DescriptionName *string
SubscribeKey string
SendKey string
TimestampCreated time.Time
TimestampLastSent *time.Time
MessagesSent int
@ -28,7 +27,6 @@ func (c Channel) JSON(includeKey bool) ChannelJSON {
DisplayName: c.DisplayName,
DescriptionName: c.DescriptionName,
SubscribeKey: langext.Conditional(includeKey, langext.Ptr(c.SubscribeKey), nil),
SendKey: langext.Conditional(includeKey, langext.Ptr(c.SendKey), nil),
TimestampCreated: c.TimestampCreated.Format(time.RFC3339Nano),
TimestampLastSent: timeOptFmt(c.TimestampLastSent, time.RFC3339Nano),
MessagesSent: c.MessagesSent,
@ -65,7 +63,6 @@ type ChannelJSON struct {
DisplayName string `json:"display_name"`
DescriptionName *string `json:"description_name"`
SubscribeKey *string `json:"subscribe_key"` // can be nil, depending on endpoint
SendKey *string `json:"send_key"` // can be nil, depending on endpoint
TimestampCreated string `json:"timestamp_created"`
TimestampLastSent *string `json:"timestamp_lastsent"`
MessagesSent int `json:"messages_sent"`
@ -98,8 +95,7 @@ func (c ChannelDB) Model() Channel {
DisplayName: c.DisplayName,
DescriptionName: c.DescriptionName,
SubscribeKey: c.SubscribeKey,
SendKey: c.SendKey,
TimestampCreated: time.UnixMilli(c.TimestampCreated),
TimestampCreated: timeFromMilli(c.TimestampCreated),
TimestampLastSent: timeOptFromMilli(c.TimestampLastSent),
MessagesSent: c.MessagesSent,
}

View File

@ -7,7 +7,7 @@ import (
"time"
)
type ClientType string
type ClientType string //@enum:type
const (
ClientTypeAndroid ClientType = "ANDROID"
@ -62,7 +62,7 @@ func (c ClientDB) Model() Client {
UserID: c.UserID,
Type: c.Type,
FCMToken: c.FCMToken,
TimestampCreated: time.UnixMilli(c.TimestampCreated),
TimestampCreated: timeFromMilli(c.TimestampCreated),
AgentModel: c.AgentModel,
AgentVersion: c.AgentVersion,
}

View File

@ -7,7 +7,7 @@ import (
"time"
)
type DeliveryStatus string
type DeliveryStatus string //@enum:type
const (
DeliveryStatusRetry DeliveryStatus = "RETRY"
@ -79,7 +79,7 @@ func (d DeliveryDB) Model() Delivery {
MessageID: d.MessageID,
ReceiverUserID: d.ReceiverUserID,
ReceiverClientID: d.ReceiverClientID,
TimestampCreated: time.UnixMilli(d.TimestampCreated),
TimestampCreated: timeFromMilli(d.TimestampCreated),
TimestampFinalized: timeOptFromMilli(d.TimestampFinalized),
Status: d.Status,
RetryCount: d.RetryCount,

View File

@ -0,0 +1,242 @@
// Code generated by permissions_gen.sh DO NOT EDIT.
package models
import "gogs.mikescher.com/BlackForestBytes/goext/langext"
type Enum interface {
Valid() bool
ValuesAny() []any
ValuesMeta() []EnumMetaValue
VarName() string
}
type StringEnum interface {
Enum
String() string
}
type DescriptionEnum interface {
Enum
Description() string
}
type EnumMetaValue struct {
VarName string `json:"varName"`
Value any `json:"value"`
Description *string `json:"description"`
}
// ================================ ClientType ================================
//
// File: client.go
// StringEnum: true
// DescrEnum: false
//
var __ClientTypeValues = []ClientType{
ClientTypeAndroid,
ClientTypeIOS,
}
var __ClientTypeVarnames = map[ClientType]string{
ClientTypeAndroid: "ClientTypeAndroid",
ClientTypeIOS: "ClientTypeIOS",
}
func (e ClientType) Valid() bool {
return langext.InArray(e, __ClientTypeValues)
}
func (e ClientType) Values() []ClientType {
return __ClientTypeValues
}
func (e ClientType) ValuesAny() []any {
return langext.ArrCastToAny(__ClientTypeValues)
}
func (e ClientType) ValuesMeta() []EnumMetaValue {
return []EnumMetaValue{
EnumMetaValue{VarName: "ClientTypeAndroid", Value: ClientTypeAndroid, Description: nil},
EnumMetaValue{VarName: "ClientTypeIOS", Value: ClientTypeIOS, Description: nil},
}
}
func (e ClientType) String() string {
return string(e)
}
func (e ClientType) VarName() string {
if d, ok := __ClientTypeVarnames[e]; ok {
return d
}
return ""
}
func ParseClientType(vv string) (ClientType, bool) {
for _, ev := range __ClientTypeValues {
if string(ev) == vv {
return ev, true
}
}
return "", false
}
func ClientTypeValues() []ClientType {
return __ClientTypeValues
}
func ClientTypeValuesMeta() []EnumMetaValue {
return []EnumMetaValue{
EnumMetaValue{VarName: "ClientTypeAndroid", Value: ClientTypeAndroid, Description: nil},
EnumMetaValue{VarName: "ClientTypeIOS", Value: ClientTypeIOS, Description: nil},
}
}
// ================================ DeliveryStatus ================================
//
// File: delivery.go
// StringEnum: true
// DescrEnum: false
//
var __DeliveryStatusValues = []DeliveryStatus{
DeliveryStatusRetry,
DeliveryStatusSuccess,
DeliveryStatusFailed,
}
var __DeliveryStatusVarnames = map[DeliveryStatus]string{
DeliveryStatusRetry: "DeliveryStatusRetry",
DeliveryStatusSuccess: "DeliveryStatusSuccess",
DeliveryStatusFailed: "DeliveryStatusFailed",
}
func (e DeliveryStatus) Valid() bool {
return langext.InArray(e, __DeliveryStatusValues)
}
func (e DeliveryStatus) Values() []DeliveryStatus {
return __DeliveryStatusValues
}
func (e DeliveryStatus) ValuesAny() []any {
return langext.ArrCastToAny(__DeliveryStatusValues)
}
func (e DeliveryStatus) ValuesMeta() []EnumMetaValue {
return []EnumMetaValue{
EnumMetaValue{VarName: "DeliveryStatusRetry", Value: DeliveryStatusRetry, Description: nil},
EnumMetaValue{VarName: "DeliveryStatusSuccess", Value: DeliveryStatusSuccess, Description: nil},
EnumMetaValue{VarName: "DeliveryStatusFailed", Value: DeliveryStatusFailed, Description: nil},
}
}
func (e DeliveryStatus) String() string {
return string(e)
}
func (e DeliveryStatus) VarName() string {
if d, ok := __DeliveryStatusVarnames[e]; ok {
return d
}
return ""
}
func ParseDeliveryStatus(vv string) (DeliveryStatus, bool) {
for _, ev := range __DeliveryStatusValues {
if string(ev) == vv {
return ev, true
}
}
return "", false
}
func DeliveryStatusValues() []DeliveryStatus {
return __DeliveryStatusValues
}
func DeliveryStatusValuesMeta() []EnumMetaValue {
return []EnumMetaValue{
EnumMetaValue{VarName: "DeliveryStatusRetry", Value: DeliveryStatusRetry, Description: nil},
EnumMetaValue{VarName: "DeliveryStatusSuccess", Value: DeliveryStatusSuccess, Description: nil},
EnumMetaValue{VarName: "DeliveryStatusFailed", Value: DeliveryStatusFailed, Description: nil},
}
}
// ================================ TokenPerm ================================
//
// File: keytoken.go
// StringEnum: true
// DescrEnum: false
//
var __TokenPermValues = []TokenPerm{
PermAdmin,
PermChannelRead,
PermChannelSend,
PermUserRead,
}
var __TokenPermVarnames = map[TokenPerm]string{
PermAdmin: "PermAdmin",
PermChannelRead: "PermChannelRead",
PermChannelSend: "PermChannelSend",
PermUserRead: "PermUserRead",
}
func (e TokenPerm) Valid() bool {
return langext.InArray(e, __TokenPermValues)
}
func (e TokenPerm) Values() []TokenPerm {
return __TokenPermValues
}
func (e TokenPerm) ValuesAny() []any {
return langext.ArrCastToAny(__TokenPermValues)
}
func (e TokenPerm) ValuesMeta() []EnumMetaValue {
return []EnumMetaValue{
EnumMetaValue{VarName: "PermAdmin", Value: PermAdmin, Description: nil},
EnumMetaValue{VarName: "PermChannelRead", Value: PermChannelRead, Description: nil},
EnumMetaValue{VarName: "PermChannelSend", Value: PermChannelSend, Description: nil},
EnumMetaValue{VarName: "PermUserRead", Value: PermUserRead, Description: nil},
}
}
func (e TokenPerm) String() string {
return string(e)
}
func (e TokenPerm) VarName() string {
if d, ok := __TokenPermVarnames[e]; ok {
return d
}
return ""
}
func ParseTokenPerm(vv string) (TokenPerm, bool) {
for _, ev := range __TokenPermValues {
if string(ev) == vv {
return ev, true
}
}
return "", false
}
func TokenPermValues() []TokenPerm {
return __TokenPermValues
}
func TokenPermValuesMeta() []EnumMetaValue {
return []EnumMetaValue{
EnumMetaValue{VarName: "PermAdmin", Value: PermAdmin, Description: nil},
EnumMetaValue{VarName: "PermChannelRead", Value: PermChannelRead, Description: nil},
EnumMetaValue{VarName: "PermChannelSend", Value: PermChannelSend, Description: nil},
EnumMetaValue{VarName: "PermUserRead", Value: PermUserRead, Description: nil},
}
}

View File

@ -40,6 +40,7 @@ const (
prefixSubscriptionID = "SUB"
prefixClientID = "CLN"
prefixRequestID = "REQ"
prefixKeyTokenID = "TOK"
)
var (
@ -50,6 +51,7 @@ var (
regexSubscriptionID = generateRegex(prefixSubscriptionID)
regexClientID = generateRegex(prefixClientID)
regexRequestID = generateRegex(prefixRequestID)
regexKeyTokenID = generateRegex(prefixKeyTokenID)
)
func generateRegex(prefix string) rext.Regex {
@ -375,3 +377,35 @@ func (id RequestID) CheckString() string {
func (id RequestID) Regex() rext.Regex {
return regexRequestID
}
// ------------------------------------------------------------
type KeyTokenID string
func NewKeyTokenID() KeyTokenID {
return KeyTokenID(generateID(prefixKeyTokenID))
}
func (id KeyTokenID) Valid() error {
return validateID(prefixKeyTokenID, string(id))
}
func (id KeyTokenID) String() string {
return string(id)
}
func (id KeyTokenID) Prefix() string {
return prefixKeyTokenID
}
func (id KeyTokenID) Raw() string {
return getRawData(prefixKeyTokenID, string(id))
}
func (id KeyTokenID) CheckString() string {
return getCheckString(prefixKeyTokenID, string(id))
}
func (id KeyTokenID) Regex() rext.Regex {
return regexKeyTokenID
}

View File

@ -0,0 +1,160 @@
package models
import (
"github.com/jmoiron/sqlx"
"gogs.mikescher.com/BlackForestBytes/goext/langext"
"gogs.mikescher.com/BlackForestBytes/goext/sq"
"strings"
"time"
)
type TokenPerm string //@enum:type
const (
PermAdmin TokenPerm = "A"
PermChannelRead TokenPerm = "CR"
PermChannelSend TokenPerm = "CS"
PermUserRead TokenPerm = "UR"
)
type TokenPermissionList []TokenPerm
func (e TokenPermissionList) Any(p ...TokenPerm) bool {
for _, v1 := range e {
for _, v2 := range p {
if v1 == v2 {
return true
}
}
}
return false
}
func (e TokenPermissionList) String() string {
return strings.Join(langext.ArrMap(e, func(v TokenPerm) string { return string(v) }), ";")
}
func ParseTokenPermissionList(input string) TokenPermissionList {
r := make([]TokenPerm, 0, len(input))
for _, v := range strings.Split(input, ";") {
if vv, ok := ParseTokenPerm(v); ok {
r = append(r, vv)
}
}
return r
}
type KeyToken struct {
KeyTokenID KeyTokenID
Name string
TimestampCreated time.Time
TimestampLastUsed *time.Time
OwnerUserID UserID
AllChannels bool
Channels []ChannelID // can also be owned by other user (needs active subscription)
Token string
Permissions TokenPermissionList
MessagesSent int
}
func (k KeyToken) IsUserRead(uid UserID) bool {
return k.OwnerUserID == uid && k.Permissions.Any(PermAdmin, PermUserRead)
}
func (k KeyToken) IsAllMessagesRead(uid UserID) bool {
return k.OwnerUserID == uid && k.AllChannels == true && k.Permissions.Any(PermAdmin, PermChannelRead)
}
func (k KeyToken) IsChannelMessagesRead(cid ChannelID) bool {
return (k.AllChannels == true || langext.InArray(cid, k.Channels)) && k.Permissions.Any(PermAdmin, PermChannelRead)
}
func (k KeyToken) IsAdmin(uid UserID) bool {
return k.OwnerUserID == uid && k.Permissions.Any(PermAdmin)
}
func (k KeyToken) IsChannelMessagesSend(c Channel) bool {
return (k.AllChannels == true || langext.InArray(c.ChannelID, k.Channels)) && k.OwnerUserID == c.OwnerUserID && k.Permissions.Any(PermAdmin, PermChannelSend)
}
func (k KeyToken) JSON() KeyTokenJSON {
return KeyTokenJSON{
KeyTokenID: k.KeyTokenID,
Name: k.Name,
TimestampCreated: k.TimestampCreated,
TimestampLastUsed: k.TimestampLastUsed,
OwnerUserID: k.OwnerUserID,
AllChannels: k.AllChannels,
Channels: k.Channels,
Permissions: k.Permissions.String(),
MessagesSent: k.MessagesSent,
}
}
type KeyTokenJSON struct {
KeyTokenID KeyTokenID `json:"keytoken_id"`
Name string `json:"name"`
TimestampCreated time.Time `json:"timestamp_created"`
TimestampLastUsed *time.Time `json:"timestamp_lastused"`
OwnerUserID UserID `json:"owner_user_id"`
AllChannels bool `json:"all_channels"`
Channels []ChannelID `json:"channels"`
Permissions string `json:"permissions"`
MessagesSent int `json:"messages_sent"`
}
type KeyTokenWithTokenJSON struct {
KeyTokenJSON
Token string `json:"token"`
}
func (j KeyTokenJSON) WithToken(tok string) any {
return KeyTokenWithTokenJSON{
KeyTokenJSON: j,
Token: tok,
}
}
type KeyTokenDB struct {
KeyTokenID KeyTokenID `db:"keytoken_id"`
Name string `db:"name"`
TimestampCreated int64 `db:"timestamp_created"`
TimestampLastUsed *int64 `db:"timestamp_lastused"`
OwnerUserID UserID `db:"owner_user_id"`
AllChannels bool `db:"all_channels"`
Channels string `db:"channels"`
Token string `db:"token"`
Permissions string `db:"permissions"`
MessagesSent int `db:"messages_sent"`
}
func (k KeyTokenDB) Model() KeyToken {
return KeyToken{
KeyTokenID: k.KeyTokenID,
Name: k.Name,
TimestampCreated: timeFromMilli(k.TimestampCreated),
TimestampLastUsed: timeOptFromMilli(k.TimestampLastUsed),
OwnerUserID: k.OwnerUserID,
AllChannels: k.AllChannels,
Channels: langext.ArrMap(strings.Split(k.Channels, ";"), func(v string) ChannelID { return ChannelID(v) }),
Token: k.Token,
Permissions: ParseTokenPermissionList(k.Permissions),
MessagesSent: k.MessagesSent,
}
}
func DecodeKeyToken(r *sqlx.Rows) (KeyToken, error) {
data, err := sq.ScanSingle[KeyTokenDB](r, sq.SModeFast, sq.Safe, true)
if err != nil {
return KeyToken{}, err
}
return data.Model(), nil
}
func DecodeKeyTokens(r *sqlx.Rows) ([]KeyToken, error) {
data, err := sq.ScanAll[KeyTokenDB](r, sq.SModeFast, sq.Safe, true)
if err != nil {
return nil, err
}
return langext.ArrMap(data, func(v KeyTokenDB) KeyToken { return v.Model() }), nil
}

View File

@ -135,7 +135,7 @@ func (m MessageDB) Model() Message {
ChannelID: m.ChannelID,
SenderName: m.SenderName,
SenderIP: m.SenderIP,
TimestampReal: time.UnixMilli(m.TimestampReal),
TimestampReal: timeFromMilli(m.TimestampReal),
TimestampClient: timeOptFromMilli(m.TimestampClient),
Title: m.Title,
Content: m.Content,

View File

@ -1,22 +1,11 @@
package models
type PermKeyType string
const (
PermKeyTypeNone PermKeyType = "NONE" // (nothing)
PermKeyTypeUserSend PermKeyType = "USER_SEND" // send-messages
PermKeyTypeUserRead PermKeyType = "USER_READ" // send-messages, list-messages, read-user
PermKeyTypeUserAdmin PermKeyType = "USER_ADMIN" // send-messages, list-messages, read-user, delete-messages, update-user
)
type PermissionSet struct {
UserID *UserID
KeyType PermKeyType
Token *KeyToken // KeyToken.Permissions
}
func NewEmptyPermissions() PermissionSet {
return PermissionSet{
UserID: nil,
KeyType: PermKeyTypeNone,
Token: nil,
}
}

View File

@ -18,6 +18,7 @@ type RequestLog struct {
RequestBodySize int64
RequestContentType string
RemoteIP string
TokenID *KeyTokenID
UserID *UserID
Permissions *string
ResponseStatuscode *int64
@ -44,6 +45,7 @@ func (c RequestLog) JSON() RequestLogJSON {
RequestBodySize: c.RequestBodySize,
RequestContentType: c.RequestContentType,
RemoteIP: c.RemoteIP,
TokenID: c.TokenID,
UserID: c.UserID,
Permissions: c.Permissions,
ResponseStatuscode: c.ResponseStatuscode,
@ -71,6 +73,7 @@ func (c RequestLog) DB() RequestLogDB {
RequestBodySize: c.RequestBodySize,
RequestContentType: c.RequestContentType,
RemoteIP: c.RemoteIP,
TokenID: c.TokenID,
UserID: c.UserID,
Permissions: c.Permissions,
ResponseStatuscode: c.ResponseStatuscode,
@ -88,53 +91,55 @@ func (c RequestLog) DB() RequestLogDB {
}
type RequestLogJSON struct {
RequestID RequestID `json:"requestLog_id"`
Method string `json:"method"`
URI string `json:"uri"`
UserAgent *string `json:"user_agent"`
Authentication *string `json:"authentication"`
RequestBody *string `json:"request_body"`
RequestBodySize int64 `json:"request_body_size"`
RequestContentType string `json:"request_content_type"`
RemoteIP string `json:"remote_ip"`
UserID *UserID `json:"userid"`
Permissions *string `json:"permissions"`
ResponseStatuscode *int64 `json:"response_statuscode"`
ResponseBodySize *int64 `json:"response_body_size"`
ResponseBody *string `json:"response_body"`
ResponseContentType string `json:"response_content_type"`
RetryCount int64 `json:"retry_count"`
Panicked bool `json:"panicked"`
PanicStr *string `json:"panic_str"`
ProcessingTime float64 `json:"processing_time"`
TimestampCreated string `json:"timestamp_created"`
TimestampStart string `json:"timestamp_start"`
TimestampFinish string `json:"timestamp_finish"`
RequestID RequestID `json:"requestLog_id"`
Method string `json:"method"`
URI string `json:"uri"`
UserAgent *string `json:"user_agent"`
Authentication *string `json:"authentication"`
RequestBody *string `json:"request_body"`
RequestBodySize int64 `json:"request_body_size"`
RequestContentType string `json:"request_content_type"`
RemoteIP string `json:"remote_ip"`
TokenID *KeyTokenID `json:"token_id"`
UserID *UserID `json:"userid"`
Permissions *string `json:"permissions"`
ResponseStatuscode *int64 `json:"response_statuscode"`
ResponseBodySize *int64 `json:"response_body_size"`
ResponseBody *string `json:"response_body"`
ResponseContentType string `json:"response_content_type"`
RetryCount int64 `json:"retry_count"`
Panicked bool `json:"panicked"`
PanicStr *string `json:"panic_str"`
ProcessingTime float64 `json:"processing_time"`
TimestampCreated string `json:"timestamp_created"`
TimestampStart string `json:"timestamp_start"`
TimestampFinish string `json:"timestamp_finish"`
}
type RequestLogDB struct {
RequestID RequestID `db:"requestLog_id"`
Method string `db:"method"`
URI string `db:"uri"`
UserAgent *string `db:"user_agent"`
Authentication *string `db:"authentication"`
RequestBody *string `db:"request_body"`
RequestBodySize int64 `db:"request_body_size"`
RequestContentType string `db:"request_content_type"`
RemoteIP string `db:"remote_ip"`
UserID *UserID `db:"userid"`
Permissions *string `db:"permissions"`
ResponseStatuscode *int64 `db:"response_statuscode"`
ResponseBodySize *int64 `db:"response_body_size"`
ResponseBody *string `db:"response_body"`
ResponseContentType string `db:"request_content_type"`
RetryCount int64 `db:"retry_count"`
Panicked int64 `db:"panicked"`
PanicStr *string `db:"panic_str"`
ProcessingTime int64 `db:"processing_time"`
TimestampCreated int64 `db:"timestamp_created"`
TimestampStart int64 `db:"timestamp_start"`
TimestampFinish int64 `db:"timestamp_finish"`
RequestID RequestID `db:"requestLog_id"`
Method string `db:"method"`
URI string `db:"uri"`
UserAgent *string `db:"user_agent"`
Authentication *string `db:"authentication"`
RequestBody *string `db:"request_body"`
RequestBodySize int64 `db:"request_body_size"`
RequestContentType string `db:"request_content_type"`
RemoteIP string `db:"remote_ip"`
TokenID *KeyTokenID `db:"token_id"`
UserID *UserID `db:"userid"`
Permissions *string `db:"permissions"`
ResponseStatuscode *int64 `db:"response_statuscode"`
ResponseBodySize *int64 `db:"response_body_size"`
ResponseBody *string `db:"response_body"`
ResponseContentType string `db:"request_content_type"`
RetryCount int64 `db:"retry_count"`
Panicked int64 `db:"panicked"`
PanicStr *string `db:"panic_str"`
ProcessingTime int64 `db:"processing_time"`
TimestampCreated int64 `db:"timestamp_created"`
TimestampStart int64 `db:"timestamp_start"`
TimestampFinish int64 `db:"timestamp_finish"`
}
func (c RequestLogDB) Model() RequestLog {
@ -158,9 +163,9 @@ func (c RequestLogDB) Model() RequestLog {
Panicked: c.Panicked != 0,
PanicStr: c.PanicStr,
ProcessingTime: timeext.FromMilliseconds(c.ProcessingTime),
TimestampCreated: time.UnixMilli(c.TimestampCreated),
TimestampStart: time.UnixMilli(c.TimestampStart),
TimestampFinish: time.UnixMilli(c.TimestampFinish),
TimestampCreated: timeFromMilli(c.TimestampCreated),
TimestampStart: timeFromMilli(c.TimestampStart),
TimestampFinish: timeFromMilli(c.TimestampFinish),
}
}

View File

@ -7,6 +7,13 @@ import (
"time"
)
// [!] subscriptions are read-access to channels,
//
// The set of subscriptions specifies which messages the ListMessages() API call returns
// also single messages/channels that are subscribed can be queries
//
// (use keytokens for write-access)
type Subscription struct {
SubscriptionID SubscriptionID
SubscriberUserID UserID
@ -56,7 +63,7 @@ func (s SubscriptionDB) Model() Subscription {
ChannelOwnerUserID: s.ChannelOwnerUserID,
ChannelID: s.ChannelID,
ChannelInternalName: s.ChannelInternalName,
TimestampCreated: time.UnixMilli(s.TimestampCreated),
TimestampCreated: timeFromMilli(s.TimestampCreated),
Confirmed: s.Confirmed != 0,
}
}

View File

@ -11,9 +11,6 @@ import (
type User struct {
UserID UserID
Username *string
SendKey string
ReadKey string
AdminKey string
TimestampCreated time.Time
TimestampLastRead *time.Time
TimestampLastSent *time.Time
@ -28,9 +25,6 @@ func (u User) JSON() UserJSON {
return UserJSON{
UserID: u.UserID,
Username: u.Username,
ReadKey: u.ReadKey,
SendKey: u.SendKey,
AdminKey: u.AdminKey,
TimestampCreated: u.TimestampCreated.Format(time.RFC3339Nano),
TimestampLastRead: timeOptFmt(u.TimestampLastRead, time.RFC3339Nano),
TimestampLastSent: timeOptFmt(u.TimestampLastSent, time.RFC3339Nano),
@ -43,10 +37,13 @@ func (u User) JSON() UserJSON {
}
}
func (u User) JSONWithClients(clients []Client) UserJSONWithClients {
return UserJSONWithClients{
func (u User) JSONWithClients(clients []Client, ak string, sk string, rk string) UserJSONWithClientsAndKeys {
return UserJSONWithClientsAndKeys{
UserJSON: u.JSON(),
Clients: langext.ArrMap(clients, func(v Client) ClientJSON { return v.JSON() }),
SendKey: sk,
ReadKey: rk,
AdminKey: ak,
}
}
@ -114,9 +111,6 @@ func (u User) MaxTimestampDiffHours() int {
type UserJSON struct {
UserID UserID `json:"user_id"`
Username *string `json:"username"`
ReadKey string `json:"read_key"`
SendKey string `json:"send_key"`
AdminKey string `json:"admin_key"`
TimestampCreated string `json:"timestamp_created"`
TimestampLastRead *string `json:"timestamp_lastread"`
TimestampLastSent *string `json:"timestamp_lastsent"`
@ -128,17 +122,17 @@ type UserJSON struct {
DefaultChannel string `json:"default_channel"`
}
type UserJSONWithClients struct {
type UserJSONWithClientsAndKeys struct {
UserJSON
Clients []ClientJSON `json:"clients"`
Clients []ClientJSON `json:"clients"`
SendKey string `json:"send_key"`
ReadKey string `json:"read_key"`
AdminKey string `json:"admin_key"`
}
type UserDB struct {
UserID UserID `db:"user_id"`
Username *string `db:"username"`
SendKey string `db:"send_key"`
ReadKey string `db:"read_key"`
AdminKey string `db:"admin_key"`
TimestampCreated int64 `db:"timestamp_created"`
TimestampLastRead *int64 `db:"timestamp_lastread"`
TimestampLastSent *int64 `db:"timestamp_lastsent"`
@ -153,10 +147,7 @@ func (u UserDB) Model() User {
return User{
UserID: u.UserID,
Username: u.Username,
SendKey: u.SendKey,
ReadKey: u.ReadKey,
AdminKey: u.AdminKey,
TimestampCreated: time.UnixMilli(u.TimestampCreated),
TimestampCreated: timeFromMilli(u.TimestampCreated),
TimestampLastRead: timeOptFromMilli(u.TimestampLastRead),
TimestampLastSent: timeOptFromMilli(u.TimestampLastSent),
MessagesSent: u.MessagesSent,

View File

@ -5,6 +5,8 @@ import (
"time"
)
//go:generate go run ../_gen/enum-generate.go -- enums_gen.go
func timeOptFmt(t *time.Time, fmt string) *string {
if t == nil {
return nil
@ -19,3 +21,7 @@ func timeOptFromMilli(millis *int64) *time.Time {
}
return langext.Ptr(time.UnixMilli(*millis))
}
func timeFromMilli(millis int64) time.Time {
return time.UnixMilli(millis)
}

View File

@ -17,12 +17,6 @@
],
"summary": "Send a new message",
"parameters": [
{
"type": "string",
"example": "qhnUbKcLgp6tg",
"name": "chan_key",
"in": "query"
},
{
"type": "string",
"example": "test",
@ -35,6 +29,12 @@
"name": "content",
"in": "query"
},
{
"type": "string",
"example": "P3TNH8mvv14fm",
"name": "key",
"in": "query"
},
{
"type": "string",
"example": "db8b0e6a-a08c-4646",
@ -76,12 +76,6 @@
"name": "user_id",
"in": "query"
},
{
"type": "string",
"example": "P3TNH8mvv14fm",
"name": "user_key",
"in": "query"
},
{
"description": " ",
"name": "post_body",
@ -90,12 +84,6 @@
"$ref": "#/definitions/handler.SendMessage.combined"
}
},
{
"type": "string",
"example": "qhnUbKcLgp6tg",
"name": "chan_key",
"in": "formData"
},
{
"type": "string",
"example": "test",
@ -108,6 +96,12 @@
"name": "content",
"in": "formData"
},
{
"type": "string",
"example": "P3TNH8mvv14fm",
"name": "key",
"in": "formData"
},
{
"type": "string",
"example": "db8b0e6a-a08c-4646",
@ -148,12 +142,6 @@
"example": "7725",
"name": "user_id",
"in": "formData"
},
{
"type": "string",
"example": "P3TNH8mvv14fm",
"name": "user_key",
"in": "formData"
}
],
"responses": {
@ -1034,7 +1022,7 @@
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/models.UserJSONWithClients"
"$ref": "#/definitions/models.UserJSONWithClientsAndKeys"
}
},
"400": {
@ -1052,6 +1040,282 @@
}
}
},
"/api/v2/users/:uid/keys": {
"get": {
"description": "The request must be done with an ADMIN key, the returned keys are without their token.",
"tags": [
"API-v2"
],
"summary": "List keys of the user",
"operationId": "api-tokenkeys-list",
"parameters": [
{
"type": "integer",
"description": "UserID",
"name": "uid",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/handler.ListUserKeys.response"
}
},
"400": {
"description": "supplied values/parameters cannot be parsed / are invalid",
"schema": {
"$ref": "#/definitions/ginresp.apiError"
}
},
"401": {
"description": "user is not authorized / has missing permissions",
"schema": {
"$ref": "#/definitions/ginresp.apiError"
}
},
"404": {
"description": "message not found",
"schema": {
"$ref": "#/definitions/ginresp.apiError"
}
},
"500": {
"description": "internal server error",
"schema": {
"$ref": "#/definitions/ginresp.apiError"
}
}
}
},
"post": {
"tags": [
"API-v2"
],
"summary": "Create a new key",
"operationId": "api-tokenkeys-create",
"parameters": [
{
"type": "integer",
"description": "UserID",
"name": "uid",
"in": "path",
"required": true
},
{
"description": " ",
"name": "post_body",
"in": "body",
"schema": {
"$ref": "#/definitions/handler.CreateUserKey.body"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/models.KeyTokenJSON"
}
},
"400": {
"description": "supplied values/parameters cannot be parsed / are invalid",
"schema": {
"$ref": "#/definitions/ginresp.apiError"
}
},
"401": {
"description": "user is not authorized / has missing permissions",
"schema": {
"$ref": "#/definitions/ginresp.apiError"
}
},
"404": {
"description": "message not found",
"schema": {
"$ref": "#/definitions/ginresp.apiError"
}
},
"500": {
"description": "internal server error",
"schema": {
"$ref": "#/definitions/ginresp.apiError"
}
}
}
}
},
"/api/v2/users/:uid/keys/:kid": {
"get": {
"description": "The request must be done with an ADMIN key, the returned key does not include its token.",
"tags": [
"API-v2"
],
"summary": "Get a single key",
"operationId": "api-tokenkeys-get",
"parameters": [
{
"type": "integer",
"description": "UserID",
"name": "uid",
"in": "path",
"required": true
},
{
"type": "integer",
"description": "TokenKeyID",
"name": "kid",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/models.KeyTokenJSON"
}
},
"400": {
"description": "supplied values/parameters cannot be parsed / are invalid",
"schema": {
"$ref": "#/definitions/ginresp.apiError"
}
},
"401": {
"description": "user is not authorized / has missing permissions",
"schema": {
"$ref": "#/definitions/ginresp.apiError"
}
},
"404": {
"description": "message not found",
"schema": {
"$ref": "#/definitions/ginresp.apiError"
}
},
"500": {
"description": "internal server error",
"schema": {
"$ref": "#/definitions/ginresp.apiError"
}
}
}
},
"delete": {
"description": "Cannot be used to delete the key used in the request itself",
"tags": [
"API-v2"
],
"summary": "Delete a key",
"operationId": "api-tokenkeys-delete",
"parameters": [
{
"type": "integer",
"description": "UserID",
"name": "uid",
"in": "path",
"required": true
},
{
"type": "integer",
"description": "TokenKeyID",
"name": "kid",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/models.KeyTokenJSON"
}
},
"400": {
"description": "supplied values/parameters cannot be parsed / are invalid",
"schema": {
"$ref": "#/definitions/ginresp.apiError"
}
},
"401": {
"description": "user is not authorized / has missing permissions",
"schema": {
"$ref": "#/definitions/ginresp.apiError"
}
},
"404": {
"description": "message not found",
"schema": {
"$ref": "#/definitions/ginresp.apiError"
}
},
"500": {
"description": "internal server error",
"schema": {
"$ref": "#/definitions/ginresp.apiError"
}
}
}
},
"patch": {
"tags": [
"API-v2"
],
"summary": "Update a key",
"operationId": "api-tokenkeys-update",
"parameters": [
{
"type": "integer",
"description": "UserID",
"name": "uid",
"in": "path",
"required": true
},
{
"type": "integer",
"description": "TokenKeyID",
"name": "kid",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/models.KeyTokenJSON"
}
},
"400": {
"description": "supplied values/parameters cannot be parsed / are invalid",
"schema": {
"$ref": "#/definitions/ginresp.apiError"
}
},
"401": {
"description": "user is not authorized / has missing permissions",
"schema": {
"$ref": "#/definitions/ginresp.apiError"
}
},
"404": {
"description": "message not found",
"schema": {
"$ref": "#/definitions/ginresp.apiError"
}
},
"500": {
"description": "internal server error",
"schema": {
"$ref": "#/definitions/ginresp.apiError"
}
}
}
}
},
"/api/v2/users/{uid}": {
"get": {
"tags": [
@ -2081,12 +2345,6 @@
],
"summary": "Send a new message",
"parameters": [
{
"type": "string",
"example": "qhnUbKcLgp6tg",
"name": "chan_key",
"in": "query"
},
{
"type": "string",
"example": "test",
@ -2099,6 +2357,12 @@
"name": "content",
"in": "query"
},
{
"type": "string",
"example": "P3TNH8mvv14fm",
"name": "key",
"in": "query"
},
{
"type": "string",
"example": "db8b0e6a-a08c-4646",
@ -2140,12 +2404,6 @@
"name": "user_id",
"in": "query"
},
{
"type": "string",
"example": "P3TNH8mvv14fm",
"name": "user_key",
"in": "query"
},
{
"description": " ",
"name": "post_body",
@ -2154,12 +2412,6 @@
"$ref": "#/definitions/handler.SendMessage.combined"
}
},
{
"type": "string",
"example": "qhnUbKcLgp6tg",
"name": "chan_key",
"in": "formData"
},
{
"type": "string",
"example": "test",
@ -2172,6 +2424,12 @@
"name": "content",
"in": "formData"
},
{
"type": "string",
"example": "P3TNH8mvv14fm",
"name": "key",
"in": "formData"
},
{
"type": "string",
"example": "db8b0e6a-a08c-4646",
@ -2212,12 +2470,6 @@
"example": "7725",
"name": "user_id",
"in": "formData"
},
{
"type": "string",
"example": "P3TNH8mvv14fm",
"name": "user_key",
"in": "formData"
}
],
"responses": {
@ -2370,6 +2622,97 @@
}
},
"definitions": {
"apierr.APIError": {
"type": "integer",
"enum": [
-1,
0,
1101,
1102,
1103,
1104,
1105,
1106,
1121,
1151,
1152,
1153,
1161,
1171,
1201,
1202,
1203,
1204,
1205,
1206,
1207,
1208,
1251,
1301,
1302,
1303,
1304,
1305,
1306,
1307,
1311,
1401,
1501,
1511,
2101,
3001,
3002,
9901,
9902,
9903,
9904,
9905
],
"x-enum-varnames": [
"UNDEFINED",
"NO_ERROR",
"MISSING_UID",
"MISSING_TOK",
"MISSING_TITLE",
"INVALID_PRIO",
"REQ_METHOD",
"INVALID_CLIENTTYPE",
"PAGETOKEN_ERROR",
"BINDFAIL_QUERY_PARAM",
"BINDFAIL_BODY_PARAM",
"BINDFAIL_URI_PARAM",
"INVALID_BODY_PARAM",
"INVALID_ENUM_VALUE",
"NO_TITLE",
"TITLE_TOO_LONG",
"CONTENT_TOO_LONG",
"USR_MSG_ID_TOO_LONG",
"TIMESTAMP_OUT_OF_RANGE",
"SENDERNAME_TOO_LONG",
"CHANNEL_TOO_LONG",
"CHANNEL_DESCRIPTION_TOO_LONG",
"CHANNEL_NAME_WOULD_CHANGE",
"USER_NOT_FOUND",
"CLIENT_NOT_FOUND",
"CHANNEL_NOT_FOUND",
"SUBSCRIPTION_NOT_FOUND",
"MESSAGE_NOT_FOUND",
"SUBSCRIPTION_USER_MISMATCH",
"KEY_NOT_FOUND",
"USER_AUTH_FAILED",
"NO_DEVICE_LINKED",
"CHANNEL_ALREADY_EXISTS",
"CANNOT_SELFDELETE_KEY",
"QUOTA_REACHED",
"FAILED_VERIFY_PRO_TOKEN",
"INVALID_PRO_TOKEN",
"FIREBASE_COM_FAILED",
"FIREBASE_COM_ERRORED",
"INTERNAL_EXCEPTION",
"PANIC",
"NOT_IMPLEMENTED"
]
},
"ginresp.apiError": {
"type": "object",
"properties": {
@ -2492,6 +2835,32 @@
}
}
},
"handler.CreateUserKey.body": {
"type": "object",
"required": [
"all_channels",
"channels",
"name",
"permissions"
],
"properties": {
"all_channels": {
"type": "boolean"
},
"channels": {
"type": "array",
"items": {
"type": "string"
}
},
"name": {
"type": "string"
},
"permissions": {
"type": "string"
}
}
},
"handler.DatabaseTest.response": {
"type": "object",
"properties": {
@ -2630,6 +2999,17 @@
}
}
},
"handler.ListUserKeys.response": {
"type": "object",
"properties": {
"tokens": {
"type": "array",
"items": {
"$ref": "#/definitions/models.KeyTokenJSON"
}
}
}
},
"handler.ListUserSubscriptions.response": {
"type": "object",
"properties": {
@ -2690,10 +3070,6 @@
"handler.SendMessage.combined": {
"type": "object",
"properties": {
"chan_key": {
"type": "string",
"example": "qhnUbKcLgp6tg"
},
"channel": {
"type": "string",
"example": "test"
@ -2702,6 +3078,10 @@
"type": "string",
"example": "This is a message"
},
"key": {
"type": "string",
"example": "P3TNH8mvv14fm"
},
"msg_id": {
"type": "string",
"example": "db8b0e6a-a08c-4646"
@ -2730,10 +3110,6 @@
"user_id": {
"type": "string",
"example": "7725"
},
"user_key": {
"type": "string",
"example": "P3TNH8mvv14fm"
}
}
},
@ -2744,7 +3120,7 @@
"type": "integer"
},
"error": {
"type": "integer"
"$ref": "#/definitions/apierr.APIError"
},
"is_pro": {
"type": "boolean"
@ -2779,7 +3155,7 @@
"type": "integer"
},
"error": {
"type": "integer"
"$ref": "#/definitions/apierr.APIError"
},
"is_pro": {
"type": "boolean"
@ -2936,10 +3312,6 @@
"owner_user_id": {
"type": "string"
},
"send_key": {
"description": "can be nil, depending on endpoint",
"type": "string"
},
"subscribe_key": {
"description": "can be nil, depending on endpoint",
"type": "string"
@ -2974,13 +3346,24 @@
"type": "string"
},
"type": {
"type": "string"
"$ref": "#/definitions/models.ClientType"
},
"user_id": {
"type": "string"
}
}
},
"models.ClientType": {
"type": "string",
"enum": [
"ANDROID",
"IOS"
],
"x-enum-varnames": [
"ClientTypeAndroid",
"ClientTypeIOS"
]
},
"models.CompatMessage": {
"type": "object",
"properties": {
@ -3007,6 +3390,41 @@
}
}
},
"models.KeyTokenJSON": {
"type": "object",
"properties": {
"all_channels": {
"type": "boolean"
},
"channels": {
"type": "array",
"items": {
"type": "string"
}
},
"keytoken_id": {
"type": "string"
},
"messages_sent": {
"type": "integer"
},
"name": {
"type": "string"
},
"owner_user_id": {
"type": "string"
},
"permissions": {
"type": "string"
},
"timestamp_created": {
"type": "string"
},
"timestamp_lastused": {
"type": "string"
}
}
},
"models.MessageJSON": {
"type": "object",
"properties": {
@ -3080,9 +3498,6 @@
"models.UserJSON": {
"type": "object",
"properties": {
"admin_key": {
"type": "string"
},
"default_channel": {
"type": "string"
},
@ -3101,12 +3516,6 @@
"quota_used": {
"type": "integer"
},
"read_key": {
"type": "string"
},
"send_key": {
"type": "string"
},
"timestamp_created": {
"type": "string"
},
@ -3124,7 +3533,7 @@
}
}
},
"models.UserJSONWithClients": {
"models.UserJSONWithClientsAndKeys": {
"type": "object",
"properties": {
"admin_key": {

View File

@ -1,5 +1,93 @@
basePath: /
definitions:
apierr.APIError:
enum:
- -1
- 0
- 1101
- 1102
- 1103
- 1104
- 1105
- 1106
- 1121
- 1151
- 1152
- 1153
- 1161
- 1171
- 1201
- 1202
- 1203
- 1204
- 1205
- 1206
- 1207
- 1208
- 1251
- 1301
- 1302
- 1303
- 1304
- 1305
- 1306
- 1307
- 1311
- 1401
- 1501
- 1511
- 2101
- 3001
- 3002
- 9901
- 9902
- 9903
- 9904
- 9905
type: integer
x-enum-varnames:
- UNDEFINED
- NO_ERROR
- MISSING_UID
- MISSING_TOK
- MISSING_TITLE
- INVALID_PRIO
- REQ_METHOD
- INVALID_CLIENTTYPE
- PAGETOKEN_ERROR
- BINDFAIL_QUERY_PARAM
- BINDFAIL_BODY_PARAM
- BINDFAIL_URI_PARAM
- INVALID_BODY_PARAM
- INVALID_ENUM_VALUE
- NO_TITLE
- TITLE_TOO_LONG
- CONTENT_TOO_LONG
- USR_MSG_ID_TOO_LONG
- TIMESTAMP_OUT_OF_RANGE
- SENDERNAME_TOO_LONG
- CHANNEL_TOO_LONG
- CHANNEL_DESCRIPTION_TOO_LONG
- CHANNEL_NAME_WOULD_CHANGE
- USER_NOT_FOUND
- CLIENT_NOT_FOUND
- CHANNEL_NOT_FOUND
- SUBSCRIPTION_NOT_FOUND
- MESSAGE_NOT_FOUND
- SUBSCRIPTION_USER_MISMATCH
- KEY_NOT_FOUND
- USER_AUTH_FAILED
- NO_DEVICE_LINKED
- CHANNEL_ALREADY_EXISTS
- CANNOT_SELFDELETE_KEY
- QUOTA_REACHED
- FAILED_VERIFY_PRO_TOKEN
- INVALID_PRO_TOKEN
- FIREBASE_COM_FAILED
- FIREBASE_COM_ERRORED
- INTERNAL_EXCEPTION
- PANIC
- NOT_IMPLEMENTED
ginresp.apiError:
properties:
errhighlight:
@ -80,6 +168,24 @@ definitions:
username:
type: string
type: object
handler.CreateUserKey.body:
properties:
all_channels:
type: boolean
channels:
items:
type: string
type: array
name:
type: string
permissions:
type: string
required:
- all_channels
- channels
- name
- permissions
type: object
handler.DatabaseTest.response:
properties:
libVersion:
@ -169,6 +275,13 @@ definitions:
page_size:
type: integer
type: object
handler.ListUserKeys.response:
properties:
tokens:
items:
$ref: '#/definitions/models.KeyTokenJSON'
type: array
type: object
handler.ListUserSubscriptions.response:
properties:
subscriptions:
@ -208,15 +321,15 @@ definitions:
type: object
handler.SendMessage.combined:
properties:
chan_key:
example: qhnUbKcLgp6tg
type: string
channel:
example: test
type: string
content:
example: This is a message
type: string
key:
example: P3TNH8mvv14fm
type: string
msg_id:
example: db8b0e6a-a08c-4646
type: string
@ -239,16 +352,13 @@ definitions:
user_id:
example: "7725"
type: string
user_key:
example: P3TNH8mvv14fm
type: string
type: object
handler.SendMessage.response:
properties:
errhighlight:
type: integer
error:
type: integer
$ref: '#/definitions/apierr.APIError'
is_pro:
type: boolean
message:
@ -271,7 +381,7 @@ definitions:
errhighlight:
type: integer
error:
type: integer
$ref: '#/definitions/apierr.APIError'
is_pro:
type: boolean
message:
@ -373,9 +483,6 @@ definitions:
type: integer
owner_user_id:
type: string
send_key:
description: can be nil, depending on endpoint
type: string
subscribe_key:
description: can be nil, depending on endpoint
type: string
@ -399,10 +506,18 @@ definitions:
timestamp_created:
type: string
type:
type: string
$ref: '#/definitions/models.ClientType'
user_id:
type: string
type: object
models.ClientType:
enum:
- ANDROID
- IOS
type: string
x-enum-varnames:
- ClientTypeAndroid
- ClientTypeIOS
models.CompatMessage:
properties:
body:
@ -420,6 +535,29 @@ definitions:
usr_msg_id:
type: string
type: object
models.KeyTokenJSON:
properties:
all_channels:
type: boolean
channels:
items:
type: string
type: array
keytoken_id:
type: string
messages_sent:
type: integer
name:
type: string
owner_user_id:
type: string
permissions:
type: string
timestamp_created:
type: string
timestamp_lastused:
type: string
type: object
models.MessageJSON:
properties:
channel_id:
@ -468,8 +606,6 @@ definitions:
type: object
models.UserJSON:
properties:
admin_key:
type: string
default_channel:
type: string
is_pro:
@ -482,10 +618,6 @@ definitions:
type: integer
quota_used:
type: integer
read_key:
type: string
send_key:
type: string
timestamp_created:
type: string
timestamp_lastread:
@ -497,7 +629,7 @@ definitions:
username:
type: string
type: object
models.UserJSONWithClients:
models.UserJSONWithClientsAndKeys:
properties:
admin_key:
type: string
@ -544,10 +676,6 @@ paths:
description: All parameter can be set via query-parameter or the json body.
Only UserID, UserKey and Title are required
parameters:
- example: qhnUbKcLgp6tg
in: query
name: chan_key
type: string
- example: test
in: query
name: channel
@ -556,6 +684,10 @@ paths:
in: query
name: content
type: string
- example: P3TNH8mvv14fm
in: query
name: key
type: string
- example: db8b0e6a-a08c-4646
in: query
name: msg_id
@ -584,19 +716,11 @@ paths:
in: query
name: user_id
type: string
- example: P3TNH8mvv14fm
in: query
name: user_key
type: string
- description: ' '
in: body
name: post_body
schema:
$ref: '#/definitions/handler.SendMessage.combined'
- example: qhnUbKcLgp6tg
in: formData
name: chan_key
type: string
- example: test
in: formData
name: channel
@ -605,6 +729,10 @@ paths:
in: formData
name: content
type: string
- example: P3TNH8mvv14fm
in: formData
name: key
type: string
- example: db8b0e6a-a08c-4646
in: formData
name: msg_id
@ -633,10 +761,6 @@ paths:
in: formData
name: user_id
type: string
- example: P3TNH8mvv14fm
in: formData
name: user_key
type: string
responses:
"200":
description: OK
@ -1240,7 +1364,7 @@ paths:
"200":
description: OK
schema:
$ref: '#/definitions/models.UserJSONWithClients'
$ref: '#/definitions/models.UserJSONWithClientsAndKeys'
"400":
description: supplied values/parameters cannot be parsed / are invalid
schema:
@ -1252,6 +1376,193 @@ paths:
summary: Create a new user
tags:
- API-v2
/api/v2/users/:uid/keys:
get:
description: The request must be done with an ADMIN key, the returned keys are
without their token.
operationId: api-tokenkeys-list
parameters:
- description: UserID
in: path
name: uid
required: true
type: integer
responses:
"200":
description: OK
schema:
$ref: '#/definitions/handler.ListUserKeys.response'
"400":
description: supplied values/parameters cannot be parsed / are invalid
schema:
$ref: '#/definitions/ginresp.apiError'
"401":
description: user is not authorized / has missing permissions
schema:
$ref: '#/definitions/ginresp.apiError'
"404":
description: message not found
schema:
$ref: '#/definitions/ginresp.apiError'
"500":
description: internal server error
schema:
$ref: '#/definitions/ginresp.apiError'
summary: List keys of the user
tags:
- API-v2
post:
operationId: api-tokenkeys-create
parameters:
- description: UserID
in: path
name: uid
required: true
type: integer
- description: ' '
in: body
name: post_body
schema:
$ref: '#/definitions/handler.CreateUserKey.body'
responses:
"200":
description: OK
schema:
$ref: '#/definitions/models.KeyTokenJSON'
"400":
description: supplied values/parameters cannot be parsed / are invalid
schema:
$ref: '#/definitions/ginresp.apiError'
"401":
description: user is not authorized / has missing permissions
schema:
$ref: '#/definitions/ginresp.apiError'
"404":
description: message not found
schema:
$ref: '#/definitions/ginresp.apiError'
"500":
description: internal server error
schema:
$ref: '#/definitions/ginresp.apiError'
summary: Create a new key
tags:
- API-v2
/api/v2/users/:uid/keys/:kid:
delete:
description: Cannot be used to delete the key used in the request itself
operationId: api-tokenkeys-delete
parameters:
- description: UserID
in: path
name: uid
required: true
type: integer
- description: TokenKeyID
in: path
name: kid
required: true
type: integer
responses:
"200":
description: OK
schema:
$ref: '#/definitions/models.KeyTokenJSON'
"400":
description: supplied values/parameters cannot be parsed / are invalid
schema:
$ref: '#/definitions/ginresp.apiError'
"401":
description: user is not authorized / has missing permissions
schema:
$ref: '#/definitions/ginresp.apiError'
"404":
description: message not found
schema:
$ref: '#/definitions/ginresp.apiError'
"500":
description: internal server error
schema:
$ref: '#/definitions/ginresp.apiError'
summary: Delete a key
tags:
- API-v2
get:
description: The request must be done with an ADMIN key, the returned key does
not include its token.
operationId: api-tokenkeys-get
parameters:
- description: UserID
in: path
name: uid
required: true
type: integer
- description: TokenKeyID
in: path
name: kid
required: true
type: integer
responses:
"200":
description: OK
schema:
$ref: '#/definitions/models.KeyTokenJSON'
"400":
description: supplied values/parameters cannot be parsed / are invalid
schema:
$ref: '#/definitions/ginresp.apiError'
"401":
description: user is not authorized / has missing permissions
schema:
$ref: '#/definitions/ginresp.apiError'
"404":
description: message not found
schema:
$ref: '#/definitions/ginresp.apiError'
"500":
description: internal server error
schema:
$ref: '#/definitions/ginresp.apiError'
summary: Get a single key
tags:
- API-v2
patch:
operationId: api-tokenkeys-update
parameters:
- description: UserID
in: path
name: uid
required: true
type: integer
- description: TokenKeyID
in: path
name: kid
required: true
type: integer
responses:
"200":
description: OK
schema:
$ref: '#/definitions/models.KeyTokenJSON'
"400":
description: supplied values/parameters cannot be parsed / are invalid
schema:
$ref: '#/definitions/ginresp.apiError'
"401":
description: user is not authorized / has missing permissions
schema:
$ref: '#/definitions/ginresp.apiError'
"404":
description: message not found
schema:
$ref: '#/definitions/ginresp.apiError'
"500":
description: internal server error
schema:
$ref: '#/definitions/ginresp.apiError'
summary: Update a key
tags:
- API-v2
/api/v2/users/{uid}:
get:
operationId: api-user-get
@ -1956,10 +2267,6 @@ paths:
description: All parameter can be set via query-parameter or the json body.
Only UserID, UserKey and Title are required
parameters:
- example: qhnUbKcLgp6tg
in: query
name: chan_key
type: string
- example: test
in: query
name: channel
@ -1968,6 +2275,10 @@ paths:
in: query
name: content
type: string
- example: P3TNH8mvv14fm
in: query
name: key
type: string
- example: db8b0e6a-a08c-4646
in: query
name: msg_id
@ -1996,19 +2307,11 @@ paths:
in: query
name: user_id
type: string
- example: P3TNH8mvv14fm
in: query
name: user_key
type: string
- description: ' '
in: body
name: post_body
schema:
$ref: '#/definitions/handler.SendMessage.combined'
- example: qhnUbKcLgp6tg
in: formData
name: chan_key
type: string
- example: test
in: formData
name: channel
@ -2017,6 +2320,10 @@ paths:
in: formData
name: content
type: string
- example: P3TNH8mvv14fm
in: formData
name: key
type: string
- example: db8b0e6a-a08c-4646
in: formData
name: msg_id
@ -2045,10 +2352,6 @@ paths:
in: formData
name: user_id
type: string
- example: P3TNH8mvv14fm
in: formData
name: user_key
type: string
responses:
"200":
description: OK

View File

@ -439,18 +439,6 @@ func TestChannelUpdate(t *testing.T) {
tt.AssertEqual(t, "channels.send_key", chan0["send_key"], chan1["send_key"])
}
// [4] renew send_key
tt.RequestAuthPatch[tt.Void](t, admintok, baseUrl, fmt.Sprintf("/api/v2/users/%s/channels/%s", uid, chanid), gin.H{
"send_key": true,
})
{
chan1 := tt.RequestAuthGet[gin.H](t, admintok, baseUrl, fmt.Sprintf("/api/v2/users/%s/channels/%s", uid, chanid))
tt.AssertNotEqual(t, "channels.subscribe_key", chan0["subscribe_key"], chan1["subscribe_key"])
tt.AssertNotEqual(t, "channels.send_key", chan0["send_key"], chan1["send_key"])
}
// [5] update description_name
tt.RequestAuthPatch[tt.Void](t, admintok, baseUrl, fmt.Sprintf("/api/v2/users/%s/channels/%s", uid, chanid), gin.H{

View File

@ -32,7 +32,6 @@ func TestGetClient(t *testing.T) {
r1 := tt.RequestAuthGet[gin.H](t, admintok, baseUrl, "/api/v2/users/"+uid)
tt.AssertEqual(t, "uid", uid, fmt.Sprintf("%v", r1["user_id"]))
tt.AssertEqual(t, "admin_key", admintok, r1["admin_key"])
tt.AssertEqual(t, "username", nil, r1["username"])
type rt2 struct {

View File

@ -671,10 +671,10 @@ func TestCompatRequery(t *testing.T) {
tt.AssertEqual(t, "new_ack", 1, a5["new_ack"])
r6 := tt.RequestPost[gin.H](t, baseUrl, "/", gin.H{
"user_id": useridnew,
"user_key": userkey,
"title": "HelloWorld_001",
"msg_id": "r6",
"user_id": useridnew,
"key": userkey,
"title": "HelloWorld_001",
"msg_id": "r6",
})
tt.AssertEqual(t, "success", true, r6["success"])

View File

@ -54,9 +54,9 @@ func TestDeleteMessage(t *testing.T) {
admintok := r0["admin_key"].(string)
msg1 := tt.RequestPost[gin.H](t, baseUrl, "/", gin.H{
"user_key": sendtok,
"user_id": uid,
"title": "Message_1",
"key": sendtok,
"user_id": uid,
"title": "Message_1",
})
tt.RequestAuthGet[tt.Void](t, admintok, baseUrl, "/api/v2/messages/"+fmt.Sprintf("%v", msg1["scn_msg_id"]))
@ -82,10 +82,10 @@ func TestDeleteMessageAndResendUsrMsgId(t *testing.T) {
admintok := r0["admin_key"].(string)
msg1 := tt.RequestPost[gin.H](t, baseUrl, "/", gin.H{
"user_key": sendtok,
"user_id": uid,
"title": "Message_1",
"msg_id": "bef8dd3d-078e-4f89-abf4-5258ad22a2e4",
"key": sendtok,
"user_id": uid,
"title": "Message_1",
"msg_id": "bef8dd3d-078e-4f89-abf4-5258ad22a2e4",
})
tt.AssertEqual(t, "suppress_send", false, msg1["suppress_send"])
@ -93,10 +93,10 @@ func TestDeleteMessageAndResendUsrMsgId(t *testing.T) {
tt.RequestAuthGet[tt.Void](t, admintok, baseUrl, "/api/v2/messages/"+fmt.Sprintf("%v", msg1["scn_msg_id"]))
msg2 := tt.RequestPost[gin.H](t, baseUrl, "/", gin.H{
"user_key": sendtok,
"user_id": uid,
"title": "Message_1",
"msg_id": "bef8dd3d-078e-4f89-abf4-5258ad22a2e4",
"key": sendtok,
"user_id": uid,
"title": "Message_1",
"msg_id": "bef8dd3d-078e-4f89-abf4-5258ad22a2e4",
})
tt.AssertEqual(t, "suppress_send", true, msg2["suppress_send"])
@ -106,10 +106,10 @@ func TestDeleteMessageAndResendUsrMsgId(t *testing.T) {
// even though message is deleted, we still get a `suppress_send` on send_message
msg3 := tt.RequestPost[gin.H](t, baseUrl, "/", gin.H{
"user_key": sendtok,
"user_id": uid,
"title": "Message_1",
"msg_id": "bef8dd3d-078e-4f89-abf4-5258ad22a2e4",
"key": sendtok,
"user_id": uid,
"title": "Message_1",
"msg_id": "bef8dd3d-078e-4f89-abf4-5258ad22a2e4",
})
tt.AssertEqual(t, "suppress_send", true, msg3["suppress_send"])
@ -123,9 +123,9 @@ func TestGetMessageSimple(t *testing.T) {
data := tt.InitDefaultData(t, ws)
msgOut := tt.RequestPost[gin.H](t, baseUrl, "/", gin.H{
"user_key": data.User[0].SendKey,
"user_id": data.User[0].UID,
"title": "Message_1",
"key": data.User[0].SendKey,
"user_id": data.User[0].UID,
"title": "Message_1",
})
msgIn := tt.RequestAuthGet[gin.H](t, data.User[0].AdminKey, baseUrl, "/api/v2/messages/"+fmt.Sprintf("%v", msgOut["scn_msg_id"]))
@ -163,7 +163,7 @@ func TestGetMessageFull(t *testing.T) {
content := tt.ShortLipsum0(2)
msgOut := tt.RequestPost[gin.H](t, baseUrl, "/", gin.H{
"user_key": data.User[0].SendKey,
"key": data.User[0].SendKey,
"user_id": data.User[0].UID,
"title": "Message_1",
"content": content,

View File

@ -31,21 +31,21 @@ func TestSendSimpleMessageJSON(t *testing.T) {
sendtok := r0["send_key"].(string)
msg1 := tt.RequestPost[gin.H](t, baseUrl, "/", gin.H{
"user_key": sendtok,
"user_id": uid,
"title": "HelloWorld_001",
"key": sendtok,
"user_id": uid,
"title": "HelloWorld_001",
})
tt.RequestPostShouldFail(t, baseUrl, "/", gin.H{
"user_key": readtok,
"user_id": uid,
"title": "HelloWorld_001",
"key": readtok,
"user_id": uid,
"title": "HelloWorld_001",
}, 401, apierr.USER_AUTH_FAILED)
tt.RequestPostShouldFail(t, baseUrl, "/", gin.H{
"user_key": "asdf",
"user_id": uid,
"title": "HelloWorld_001",
"key": "asdf",
"user_id": uid,
"title": "HelloWorld_001",
}, 401, apierr.USER_AUTH_FAILED)
tt.AssertEqual(t, "messageCount", 1, len(pusher.Data))
@ -82,7 +82,7 @@ func TestSendSimpleMessageQuery(t *testing.T) {
admintok := r0["admin_key"].(string)
sendtok := r0["send_key"].(string)
msg1 := tt.RequestPost[gin.H](t, baseUrl, fmt.Sprintf("/?user_id=%s&user_key=%s&title=%s", uid, sendtok, url.QueryEscape("Hello World 2134")), nil)
msg1 := tt.RequestPost[gin.H](t, baseUrl, fmt.Sprintf("/?user_id=%s&key=%s&title=%s", uid, sendtok, url.QueryEscape("Hello World 2134")), nil)
tt.AssertEqual(t, "messageCount", 1, len(pusher.Data))
tt.AssertStrRepEqual(t, "msg.title", "Hello World 2134", pusher.Last().Message.Title)
@ -119,9 +119,9 @@ func TestSendSimpleMessageForm(t *testing.T) {
sendtok := r0["send_key"].(string)
msg1 := tt.RequestPost[gin.H](t, baseUrl, "/", tt.FormData{
"user_key": sendtok,
"user_id": uid,
"title": "Hello World 9999 [$$$]",
"key": sendtok,
"user_id": uid,
"title": "Hello World 9999 [$$$]",
})
tt.AssertEqual(t, "messageCount", 1, len(pusher.Data))
@ -157,10 +157,10 @@ func TestSendSimpleMessageFormAndQuery(t *testing.T) {
uid := r0["user_id"].(string)
sendtok := r0["send_key"].(string)
msg1 := tt.RequestPost[gin.H](t, baseUrl, fmt.Sprintf("/?user_id=%s&user_key=%s&title=%s", uid, sendtok, url.QueryEscape("1111111")), tt.FormData{
"user_key": "ERR",
"user_id": "999999",
"title": "2222222",
msg1 := tt.RequestPost[gin.H](t, baseUrl, fmt.Sprintf("/?user_id=%s&key=%s&title=%s", uid, sendtok, url.QueryEscape("1111111")), tt.FormData{
"key": "ERR",
"user_id": "999999",
"title": "2222222",
})
tt.AssertEqual(t, "messageCount", 1, len(pusher.Data))
@ -185,10 +185,10 @@ func TestSendSimpleMessageJSONAndQuery(t *testing.T) {
sendtok := r0["send_key"].(string)
// query overwrite body
msg1 := tt.RequestPost[gin.H](t, baseUrl, fmt.Sprintf("/?user_id=%s&user_key=%s&title=%s", uid, sendtok, url.QueryEscape("1111111")), gin.H{
"user_key": "ERR",
"user_id": models.NewUserID(),
"title": "2222222",
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",
"user_id": models.NewUserID(),
"title": "2222222",
})
tt.AssertEqual(t, "messageCount", 1, len(pusher.Data))
@ -215,15 +215,15 @@ func TestSendSimpleMessageAlt1(t *testing.T) {
sendtok := r0["send_key"].(string)
msg1 := tt.RequestPost[gin.H](t, baseUrl, "/send", gin.H{
"user_key": sendtok,
"user_id": uid,
"title": "HelloWorld_001",
"key": sendtok,
"user_id": uid,
"title": "HelloWorld_001",
})
tt.RequestPostShouldFail(t, baseUrl, "/send", gin.H{
"user_key": readtok,
"user_id": uid,
"title": "HelloWorld_001",
"key": readtok,
"user_id": uid,
"title": "HelloWorld_001",
}, 401, apierr.USER_AUTH_FAILED)
tt.AssertEqual(t, "messageCount", 1, len(pusher.Data))
@ -261,10 +261,10 @@ func TestSendContentMessage(t *testing.T) {
sendtok := r0["send_key"].(string)
msg1 := tt.RequestPost[gin.H](t, baseUrl, "/", gin.H{
"user_key": sendtok,
"user_id": uid,
"title": "HelloWorld_042",
"content": "I am Content\nasdf",
"key": sendtok,
"user_id": uid,
"title": "HelloWorld_042",
"content": "I am Content\nasdf",
})
tt.AssertEqual(t, "messageCount", 1, len(pusher.Data))
@ -306,7 +306,7 @@ func TestSendWithSendername(t *testing.T) {
admintok := r0["admin_key"].(string)
msg1 := tt.RequestPost[gin.H](t, baseUrl, "/", gin.H{
"user_key": sendtok,
"key": sendtok,
"user_id": uid,
"title": "HelloWorld_xyz",
"content": "Unicode: 日本 - yäy\000\n\t\x00...",
@ -360,10 +360,10 @@ func TestSendLongContent(t *testing.T) {
}
msg1 := tt.RequestPost[gin.H](t, baseUrl, "/", gin.H{
"user_key": sendtok,
"user_id": uid,
"title": "HelloWorld_042",
"content": longContent,
"key": sendtok,
"user_id": uid,
"title": "HelloWorld_042",
"content": longContent,
})
tt.AssertEqual(t, "messageCount", 1, len(pusher.Data))
@ -416,10 +416,10 @@ func TestSendTooLongContent(t *testing.T) {
}
tt.RequestPostShouldFail(t, baseUrl, "/", gin.H{
"user_key": sendtok,
"user_id": uid,
"title": "HelloWorld_042",
"content": longContent,
"key": sendtok,
"user_id": uid,
"title": "HelloWorld_042",
"content": longContent,
}, 400, apierr.CONTENT_TOO_LONG)
}
@ -445,10 +445,10 @@ func TestSendLongContentPro(t *testing.T) {
}
tt.RequestPost[tt.Void](t, baseUrl, "/", gin.H{
"user_key": sendtok,
"user_id": uid,
"title": "HelloWorld_042",
"content": longContent,
"key": sendtok,
"user_id": uid,
"title": "HelloWorld_042",
"content": longContent,
})
}
@ -459,10 +459,10 @@ func TestSendLongContentPro(t *testing.T) {
}
tt.RequestPost[tt.Void](t, baseUrl, "/", gin.H{
"user_key": sendtok,
"user_id": uid,
"title": "HelloWorld_042",
"content": longContent,
"key": sendtok,
"user_id": uid,
"title": "HelloWorld_042",
"content": longContent,
})
}
@ -474,10 +474,10 @@ func TestSendLongContentPro(t *testing.T) {
}
tt.RequestPost[tt.Void](t, baseUrl, "/", gin.H{
"user_key": sendtok,
"user_id": uid,
"title": "HelloWorld_042",
"content": longContent,
"key": sendtok,
"user_id": uid,
"title": "HelloWorld_042",
"content": longContent,
})
}
@ -488,10 +488,10 @@ func TestSendLongContentPro(t *testing.T) {
}
tt.RequestPost[tt.Void](t, baseUrl, "/", gin.H{
"user_key": sendtok,
"user_id": uid,
"title": "HelloWorld_042",
"content": longContent,
"key": sendtok,
"user_id": uid,
"title": "HelloWorld_042",
"content": longContent,
})
}
@ -502,10 +502,10 @@ func TestSendLongContentPro(t *testing.T) {
}
tt.RequestPostShouldFail(t, baseUrl, "/", gin.H{
"user_key": sendtok,
"user_id": uid,
"title": "HelloWorld_042",
"content": longContent,
"key": sendtok,
"user_id": uid,
"title": "HelloWorld_042",
"content": longContent,
}, 400, apierr.CONTENT_TOO_LONG)
}
}
@ -525,9 +525,9 @@ func TestSendTooLongTitle(t *testing.T) {
sendtok := r0["send_key"].(string)
tt.RequestPostShouldFail(t, baseUrl, "/", gin.H{
"user_key": sendtok,
"user_id": uid,
"title": "1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890",
"key": sendtok,
"user_id": uid,
"title": "1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890",
}, 400, apierr.TITLE_TOO_LONG)
}
@ -549,11 +549,11 @@ func TestSendIdempotent(t *testing.T) {
sendtok := r0["send_key"].(string)
msg1 := tt.RequestPost[gin.H](t, baseUrl, "/", gin.H{
"user_key": sendtok,
"user_id": uid,
"title": "Hello SCN",
"content": "mamma mia",
"msg_id": "c0235a49-dabc-4cdc-a0ce-453966e0c2d5",
"key": sendtok,
"user_id": uid,
"title": "Hello SCN",
"content": "mamma mia",
"msg_id": "c0235a49-dabc-4cdc-a0ce-453966e0c2d5",
})
tt.AssertEqual(t, "messageCount", 1, len(pusher.Data))
@ -571,11 +571,11 @@ func TestSendIdempotent(t *testing.T) {
tt.AssertEqual(t, "len(messages)", 1, len(msgList1.Messages))
msg2 := tt.RequestPost[gin.H](t, baseUrl, "/", gin.H{
"user_key": sendtok,
"user_id": uid,
"title": "Hello again",
"content": "mother mia",
"msg_id": "c0235a49-dabc-4cdc-a0ce-453966e0c2d5",
"key": sendtok,
"user_id": uid,
"title": "Hello again",
"content": "mother mia",
"msg_id": "c0235a49-dabc-4cdc-a0ce-453966e0c2d5",
})
tt.AssertEqual(t, "messageCount", 1, len(pusher.Data))
@ -590,11 +590,11 @@ func TestSendIdempotent(t *testing.T) {
tt.AssertEqual(t, "len(messages)", 1, len(msgList2.Messages))
msg3 := tt.RequestPost[gin.H](t, baseUrl, "/", gin.H{
"user_key": sendtok,
"user_id": uid,
"title": "Hello third",
"content": "let me go",
"msg_id": "3238e68e-c1ea-44ce-b21b-2576614082b5",
"key": sendtok,
"user_id": uid,
"title": "Hello third",
"content": "let me go",
"msg_id": "3238e68e-c1ea-44ce-b21b-2576614082b5",
})
tt.AssertEqual(t, "messageCount", 2, len(pusher.Data))
@ -628,10 +628,10 @@ func TestSendWithPriority(t *testing.T) {
{
msg1 := tt.RequestPost[gin.H](t, baseUrl, "/", gin.H{
"user_key": sendtok,
"user_id": uid,
"title": "M_001",
"content": "TestSendWithPriority#001",
"key": sendtok,
"user_id": uid,
"title": "M_001",
"content": "TestSendWithPriority#001",
})
tt.AssertEqual(t, "messageCount", 1, len(pusher.Data))
@ -646,7 +646,7 @@ func TestSendWithPriority(t *testing.T) {
{
msg2 := tt.RequestPost[gin.H](t, baseUrl, "/", gin.H{
"user_key": sendtok,
"key": sendtok,
"user_id": uid,
"title": "M_002",
"content": "TestSendWithPriority#002",
@ -665,7 +665,7 @@ func TestSendWithPriority(t *testing.T) {
{
msg3 := tt.RequestPost[gin.H](t, baseUrl, "/", gin.H{
"user_key": sendtok,
"key": sendtok,
"user_id": uid,
"title": "M_003",
"content": "TestSendWithPriority#003",
@ -684,7 +684,7 @@ func TestSendWithPriority(t *testing.T) {
{
msg4 := tt.RequestPost[gin.H](t, baseUrl, "/", gin.H{
"user_key": sendtok,
"key": sendtok,
"user_id": uid,
"title": "M_004",
"content": "TestSendWithPriority#004",
@ -720,7 +720,7 @@ func TestSendInvalidPriority(t *testing.T) {
admintok := r0["admin_key"].(string)
tt.RequestPostShouldFail(t, baseUrl, "/", gin.H{
"user_key": sendtok,
"key": sendtok,
"user_id": uid,
"title": "(title)",
"content": "(content)",
@ -728,7 +728,7 @@ func TestSendInvalidPriority(t *testing.T) {
}, 400, apierr.INVALID_PRIO)
tt.RequestPostShouldFail(t, baseUrl, "/", gin.H{
"user_key": sendtok,
"key": sendtok,
"user_id": uid,
"title": "(title)",
"content": "(content)",
@ -736,7 +736,7 @@ func TestSendInvalidPriority(t *testing.T) {
}, 400, apierr.INVALID_PRIO)
tt.RequestPostShouldFail(t, baseUrl, "/", gin.H{
"user_key": sendtok,
"key": sendtok,
"user_id": uid,
"title": "(title)",
"content": "(content)",
@ -744,7 +744,7 @@ func TestSendInvalidPriority(t *testing.T) {
}, 400, apierr.INVALID_PRIO)
tt.RequestPostShouldFail(t, baseUrl, "/", gin.H{
"user_key": admintok,
"key": admintok,
"user_id": uid,
"title": "(title)",
"content": "(content)",
@ -752,7 +752,7 @@ func TestSendInvalidPriority(t *testing.T) {
}, 400, apierr.INVALID_PRIO)
tt.RequestPostShouldFail(t, baseUrl, "/", gin.H{
"user_key": admintok,
"key": admintok,
"user_id": uid,
"title": "(title)",
"content": "(content)",
@ -760,7 +760,7 @@ func TestSendInvalidPriority(t *testing.T) {
}, 400, apierr.INVALID_PRIO)
tt.RequestPostShouldFail(t, baseUrl, "/", gin.H{
"user_key": admintok,
"key": admintok,
"user_id": uid,
"title": "(title)",
"content": "(content)",
@ -768,7 +768,7 @@ func TestSendInvalidPriority(t *testing.T) {
}, 400, apierr.INVALID_PRIO)
tt.RequestPostShouldFail(t, baseUrl, "/", tt.FormData{
"user_key": sendtok,
"key": sendtok,
"user_id": uid,
"title": "(title)",
"content": "(content)",
@ -776,7 +776,7 @@ func TestSendInvalidPriority(t *testing.T) {
}, 400, apierr.INVALID_PRIO)
tt.RequestPostShouldFail(t, baseUrl, "/", tt.FormData{
"user_key": sendtok,
"key": sendtok,
"user_id": uid,
"title": "(title)",
"content": "(content)",
@ -784,7 +784,7 @@ func TestSendInvalidPriority(t *testing.T) {
}, 400, apierr.INVALID_PRIO)
tt.RequestPostShouldFail(t, baseUrl, "/", tt.FormData{
"user_key": sendtok,
"key": sendtok,
"user_id": uid,
"title": "(title)",
"content": "(content)",
@ -792,7 +792,7 @@ func TestSendInvalidPriority(t *testing.T) {
}, 400, apierr.INVALID_PRIO)
tt.RequestPostShouldFail(t, baseUrl, "/", tt.FormData{
"user_key": admintok,
"key": admintok,
"user_id": uid,
"title": "(title)",
"content": "(content)",
@ -800,7 +800,7 @@ func TestSendInvalidPriority(t *testing.T) {
}, 400, apierr.INVALID_PRIO)
tt.RequestPostShouldFail(t, baseUrl, "/", tt.FormData{
"user_key": admintok,
"key": admintok,
"user_id": uid,
"title": "(title)",
"content": "(content)",
@ -808,7 +808,7 @@ func TestSendInvalidPriority(t *testing.T) {
}, 400, apierr.INVALID_PRIO)
tt.RequestPostShouldFail(t, baseUrl, "/", tt.FormData{
"user_key": admintok,
"key": admintok,
"user_id": uid,
"title": "(title)",
"content": "(content)",
@ -838,7 +838,7 @@ func TestSendWithTimestamp(t *testing.T) {
ts := time.Now().Unix() - int64(time.Hour.Seconds())
msg1 := tt.RequestPost[gin.H](t, baseUrl, "/", tt.FormData{
"user_key": sendtok,
"key": sendtok,
"user_id": fmt.Sprintf("%s", uid),
"title": "TTT",
"timestamp": fmt.Sprintf("%d", ts),
@ -892,83 +892,83 @@ func TestSendInvalidTimestamp(t *testing.T) {
sendtok := r0["send_key"].(string)
tt.RequestPostShouldFail(t, baseUrl, "/", tt.FormData{
"user_key": sendtok,
"key": sendtok,
"user_id": fmt.Sprintf("%s", uid),
"title": "TTT",
"timestamp": "-10000",
}, 400, apierr.TIMESTAMP_OUT_OF_RANGE)
tt.RequestPostShouldFail(t, baseUrl, "/", tt.FormData{
"user_key": sendtok,
"key": sendtok,
"user_id": fmt.Sprintf("%s", uid),
"title": "TTT",
"timestamp": "0",
}, 400, apierr.TIMESTAMP_OUT_OF_RANGE)
tt.RequestPostShouldFail(t, baseUrl, "/", tt.FormData{
"user_key": sendtok,
"key": sendtok,
"user_id": fmt.Sprintf("%s", uid),
"title": "TTT",
"timestamp": fmt.Sprintf("%d", time.Now().Unix()-int64(25*time.Hour.Seconds())),
}, 400, apierr.TIMESTAMP_OUT_OF_RANGE)
tt.RequestPostShouldFail(t, baseUrl, "/", tt.FormData{
"user_key": sendtok,
"key": sendtok,
"user_id": fmt.Sprintf("%s", uid),
"title": "TTT",
"timestamp": fmt.Sprintf("%d", time.Now().Unix()+int64(25*time.Hour.Seconds())),
}, 400, apierr.TIMESTAMP_OUT_OF_RANGE)
tt.RequestPostShouldFail(t, baseUrl, "/", gin.H{
"user_key": sendtok,
"key": sendtok,
"user_id": uid,
"title": "TTT",
"timestamp": -10000,
}, 400, apierr.TIMESTAMP_OUT_OF_RANGE)
tt.RequestPostShouldFail(t, baseUrl, "/", gin.H{
"user_key": sendtok,
"key": sendtok,
"user_id": uid,
"title": "TTT",
"timestamp": 0,
}, 400, apierr.TIMESTAMP_OUT_OF_RANGE)
tt.RequestPostShouldFail(t, baseUrl, "/", gin.H{
"user_key": sendtok,
"key": sendtok,
"user_id": uid,
"title": "TTT",
"timestamp": time.Now().Unix() - int64(25*time.Hour.Seconds()),
}, 400, apierr.TIMESTAMP_OUT_OF_RANGE)
tt.RequestPostShouldFail(t, baseUrl, "/", gin.H{
"user_key": sendtok,
"key": sendtok,
"user_id": uid,
"title": "TTT",
"timestamp": time.Now().Unix() + int64(25*time.Hour.Seconds()),
}, 400, apierr.TIMESTAMP_OUT_OF_RANGE)
tt.RequestPostShouldFail(t, baseUrl, fmt.Sprintf("/?user_key=%s&user_id=%s&title=%s&timestamp=%d",
tt.RequestPostShouldFail(t, baseUrl, fmt.Sprintf("/?key=%s&user_id=%s&title=%s&timestamp=%d",
sendtok,
uid,
"TTT",
-10000,
), nil, 400, apierr.TIMESTAMP_OUT_OF_RANGE)
tt.RequestPostShouldFail(t, baseUrl, fmt.Sprintf("/?user_key=%s&user_id=%s&title=%s&timestamp=%d",
tt.RequestPostShouldFail(t, baseUrl, fmt.Sprintf("/?key=%s&user_id=%s&title=%s&timestamp=%d",
sendtok,
uid,
"TTT",
0,
), nil, 400, apierr.TIMESTAMP_OUT_OF_RANGE)
tt.RequestPostShouldFail(t, baseUrl, fmt.Sprintf("/?user_key=%s&user_id=%s&title=%s&timestamp=%d",
tt.RequestPostShouldFail(t, baseUrl, fmt.Sprintf("/?key=%s&user_id=%s&title=%s&timestamp=%d",
sendtok,
uid,
"TTT",
time.Now().Unix()-int64(25*time.Hour.Seconds()),
), nil, 400, apierr.TIMESTAMP_OUT_OF_RANGE)
tt.RequestPostShouldFail(t, baseUrl, fmt.Sprintf("/?user_key=%s&user_id=%s&title=%s&timestamp=%d",
tt.RequestPostShouldFail(t, baseUrl, fmt.Sprintf("/?key=%s&user_id=%s&title=%s&timestamp=%d",
sendtok,
uid,
"TTT",
@ -1003,9 +1003,9 @@ func TestSendToNewChannel(t *testing.T) {
}
tt.RequestPost[gin.H](t, baseUrl, "/", gin.H{
"user_key": sendtok,
"user_id": uid,
"title": "M0",
"key": sendtok,
"user_id": uid,
"title": "M0",
})
{
@ -1015,11 +1015,11 @@ func TestSendToNewChannel(t *testing.T) {
}
tt.RequestPost[gin.H](t, baseUrl, "/", gin.H{
"user_key": sendtok,
"user_id": uid,
"title": "M1",
"content": tt.ShortLipsum0(4),
"channel": "main",
"key": sendtok,
"user_id": uid,
"title": "M1",
"content": tt.ShortLipsum0(4),
"channel": "main",
})
{
@ -1029,11 +1029,11 @@ func TestSendToNewChannel(t *testing.T) {
}
tt.RequestPost[gin.H](t, baseUrl, "/", gin.H{
"user_key": sendtok,
"user_id": uid,
"title": "M2",
"content": tt.ShortLipsum0(4),
"channel": "test",
"key": sendtok,
"user_id": uid,
"title": "M2",
"content": tt.ShortLipsum0(4),
"channel": "test",
})
{
@ -1043,10 +1043,10 @@ func TestSendToNewChannel(t *testing.T) {
}
tt.RequestPost[gin.H](t, baseUrl, "/", gin.H{
"user_key": sendtok,
"user_id": uid,
"title": "M3",
"channel": "test",
"key": sendtok,
"user_id": uid,
"title": "M3",
"channel": "test",
})
{
@ -1082,9 +1082,9 @@ func TestSendToManualChannel(t *testing.T) {
}
tt.RequestPost[gin.H](t, baseUrl, "/", gin.H{
"user_key": sendtok,
"user_id": uid,
"title": "M0",
"key": sendtok,
"user_id": uid,
"title": "M0",
})
{
@ -1094,11 +1094,11 @@ func TestSendToManualChannel(t *testing.T) {
}
tt.RequestPost[gin.H](t, baseUrl, "/", gin.H{
"user_key": sendtok,
"user_id": uid,
"title": "M1",
"content": tt.ShortLipsum0(4),
"channel": "main",
"key": sendtok,
"user_id": uid,
"title": "M1",
"content": tt.ShortLipsum0(4),
"channel": "main",
})
{
@ -1119,11 +1119,11 @@ func TestSendToManualChannel(t *testing.T) {
}
tt.RequestPost[gin.H](t, baseUrl, "/", gin.H{
"user_key": sendtok,
"user_id": uid,
"title": "M2",
"content": tt.ShortLipsum0(4),
"channel": "test",
"key": sendtok,
"user_id": uid,
"title": "M2",
"content": tt.ShortLipsum0(4),
"channel": "test",
})
{
@ -1133,10 +1133,10 @@ func TestSendToManualChannel(t *testing.T) {
}
tt.RequestPost[gin.H](t, baseUrl, "/", gin.H{
"user_key": sendtok,
"user_id": uid,
"title": "M3",
"channel": "test",
"key": sendtok,
"user_id": uid,
"title": "M3",
"channel": "test",
})
{
@ -1161,24 +1161,24 @@ func TestSendToTooLongChannel(t *testing.T) {
sendtok := r0["send_key"].(string)
tt.RequestPost[tt.Void](t, baseUrl, "/", gin.H{
"user_key": sendtok,
"user_id": uid,
"title": "M3",
"channel": "12345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890",
"key": sendtok,
"user_id": uid,
"title": "M3",
"channel": "12345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890",
})
tt.RequestPost[tt.Void](t, baseUrl, "/", gin.H{
"user_key": sendtok,
"user_id": uid,
"title": "M3",
"channel": "123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890",
"key": sendtok,
"user_id": uid,
"title": "M3",
"channel": "123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890",
})
tt.RequestPostShouldFail(t, baseUrl, "/", gin.H{
"user_key": sendtok,
"user_id": uid,
"title": "M3",
"channel": "1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901",
"key": sendtok,
"user_id": uid,
"title": "M3",
"channel": "1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901",
}, 400, apierr.CHANNEL_TOO_LONG)
}
@ -1203,9 +1203,9 @@ func TestQuotaExceededNoPro(t *testing.T) {
{
msg1 := tt.RequestPost[gin.H](t, baseUrl, "/", gin.H{
"user_key": sendtok,
"user_id": uid,
"title": tt.ShortLipsum0(2),
"key": sendtok,
"user_id": uid,
"title": tt.ShortLipsum0(2),
})
tt.AssertStrRepEqual(t, "quota.msg.1", 1, msg1["quota"])
tt.AssertStrRepEqual(t, "quota.msg.1", 50, msg1["quota_max"])
@ -1222,9 +1222,9 @@ func TestQuotaExceededNoPro(t *testing.T) {
for i := 0; i < 48; i++ {
tt.RequestPost[gin.H](t, baseUrl, "/", gin.H{
"user_key": sendtok,
"user_id": uid,
"title": tt.ShortLipsum0(2),
"key": sendtok,
"user_id": uid,
"title": tt.ShortLipsum0(2),
})
}
@ -1237,9 +1237,9 @@ func TestQuotaExceededNoPro(t *testing.T) {
}
msg50 := tt.RequestPost[gin.H](t, baseUrl, "/", gin.H{
"user_key": sendtok,
"user_id": uid,
"title": tt.ShortLipsum0(2),
"key": sendtok,
"user_id": uid,
"title": tt.ShortLipsum0(2),
})
tt.AssertStrRepEqual(t, "quota.msg.50", 50, msg50["quota"])
tt.AssertStrRepEqual(t, "quota.msg.50", 50, msg50["quota_max"])
@ -1253,9 +1253,9 @@ func TestQuotaExceededNoPro(t *testing.T) {
}
tt.RequestPostShouldFail(t, baseUrl, "/", gin.H{
"user_key": sendtok,
"user_id": uid,
"title": tt.ShortLipsum0(2),
"key": sendtok,
"user_id": uid,
"title": tt.ShortLipsum0(2),
}, 403, apierr.QUOTA_REACHED)
}
@ -1281,9 +1281,9 @@ func TestQuotaExceededPro(t *testing.T) {
{
msg1 := tt.RequestPost[gin.H](t, baseUrl, "/", gin.H{
"user_key": sendtok,
"user_id": uid,
"title": tt.ShortLipsum0(2),
"key": sendtok,
"user_id": uid,
"title": tt.ShortLipsum0(2),
})
tt.AssertStrRepEqual(t, "quota.msg.1", 1, msg1["quota"])
tt.AssertStrRepEqual(t, "quota.msg.1", 1000, msg1["quota_max"])
@ -1300,9 +1300,9 @@ func TestQuotaExceededPro(t *testing.T) {
for i := 0; i < 998; i++ {
tt.RequestPost[gin.H](t, baseUrl, "/", gin.H{
"user_key": sendtok,
"user_id": uid,
"title": tt.ShortLipsum0(2),
"key": sendtok,
"user_id": uid,
"title": tt.ShortLipsum0(2),
})
}
@ -1315,9 +1315,9 @@ func TestQuotaExceededPro(t *testing.T) {
}
msg50 := tt.RequestPost[gin.H](t, baseUrl, "/", gin.H{
"user_key": sendtok,
"user_id": uid,
"title": tt.ShortLipsum0(2),
"key": sendtok,
"user_id": uid,
"title": tt.ShortLipsum0(2),
})
tt.AssertStrRepEqual(t, "quota.msg.1000", 1000, msg50["quota"])
tt.AssertStrRepEqual(t, "quota.msg.1000", 1000, msg50["quota_max"])
@ -1331,9 +1331,9 @@ func TestQuotaExceededPro(t *testing.T) {
}
tt.RequestPostShouldFail(t, baseUrl, "/", gin.H{
"user_key": sendtok,
"user_id": uid,
"title": tt.ShortLipsum0(2),
"key": sendtok,
"user_id": uid,
"title": tt.ShortLipsum0(2),
}, 403, apierr.QUOTA_REACHED)
}
@ -1361,9 +1361,9 @@ func TestSendParallel(t *testing.T) {
sem <- tt.Void{}
}()
tt.RequestPost[gin.H](t, baseUrl, "/", gin.H{
"user_key": sendtok,
"user_id": uid,
"title": tt.ShortLipsum0(2),
"key": sendtok,
"user_id": uid,
"title": tt.ShortLipsum0(2),
})
}()
}

View File

@ -19,7 +19,6 @@ func TestCreateUserNoClient(t *testing.T) {
tt.AssertEqual(t, "len(clients)", 0, len(r0["clients"].([]any)))
uid := fmt.Sprintf("%v", r0["user_id"])
admintok := r0["admin_key"].(string)
readtok := r0["read_key"].(string)
sendtok := r0["send_key"].(string)
@ -29,7 +28,6 @@ func TestCreateUserNoClient(t *testing.T) {
r1 := tt.RequestAuthGet[gin.H](t, readtok, baseUrl, "/api/v2/users/"+uid)
tt.AssertEqual(t, "uid", uid, fmt.Sprintf("%v", r1["user_id"]))
tt.AssertEqual(t, "admin_key", admintok, r1["admin_key"])
}
func TestCreateUserDummyClient(t *testing.T) {
@ -52,7 +50,6 @@ func TestCreateUserDummyClient(t *testing.T) {
r1 := tt.RequestAuthGet[gin.H](t, admintok, baseUrl, "/api/v2/users/"+uid)
tt.AssertEqual(t, "uid", uid, fmt.Sprintf("%v", r1["user_id"]))
tt.AssertEqual(t, "admin_key", admintok, r1["admin_key"])
tt.AssertEqual(t, "username", nil, r1["username"])
type rt2 struct {
@ -92,7 +89,6 @@ func TestCreateUserWithUsername(t *testing.T) {
r1 := tt.RequestAuthGet[gin.H](t, admintok, baseUrl, "/api/v2/users/"+uid)
tt.AssertEqual(t, "uid", uid, fmt.Sprintf("%v", r1["user_id"]))
tt.AssertEqual(t, "admin_key", admintok, r1["admin_key"])
tt.AssertEqual(t, "username", "my_user", r1["username"])
}
@ -188,65 +184,6 @@ func TestFailedUgradeUserToPro(t *testing.T) {
tt.RequestAuthPatchShouldFail(t, admintok0, baseUrl, "/api/v2/users/"+uid0, gin.H{"pro_token": "@INVALID"}, 400, apierr.INVALID_PRO_TOKEN)
}
func TestRecreateKeys(t *testing.T) {
_, baseUrl, stop := tt.StartSimpleWebserver(t)
defer stop()
r0 := tt.RequestPost[gin.H](t, baseUrl, "/api/v2/users", gin.H{
"agent_model": "DUMMY_PHONE",
"agent_version": "4X",
"client_type": "ANDROID",
"fcm_token": "DUMMY_FCM",
})
tt.AssertEqual(t, "username", nil, r0["username"])
uid := fmt.Sprintf("%v", r0["user_id"])
admintok := r0["admin_key"].(string)
readtok := r0["read_key"].(string)
sendtok := r0["send_key"].(string)
tt.RequestAuthPatchShouldFail(t, readtok, baseUrl, "/api/v2/users/"+uid, gin.H{"read_key": true}, 401, apierr.USER_AUTH_FAILED)
tt.RequestAuthPatchShouldFail(t, sendtok, baseUrl, "/api/v2/users/"+uid, gin.H{"read_key": true}, 401, apierr.USER_AUTH_FAILED)
r1 := tt.RequestAuthPatch[gin.H](t, admintok, baseUrl, "/api/v2/users/"+uid, gin.H{})
tt.AssertEqual(t, "admin_key", admintok, r1["admin_key"])
tt.AssertEqual(t, "read_key", readtok, r1["read_key"])
tt.AssertEqual(t, "send_key", sendtok, r1["send_key"])
r2 := tt.RequestAuthPatch[gin.H](t, admintok, baseUrl, "/api/v2/users/"+uid, gin.H{"read_key": true})
tt.AssertEqual(t, "admin_key", admintok, r2["admin_key"])
tt.AssertNotEqual(t, "read_key", readtok, r2["read_key"])
tt.AssertEqual(t, "send_key", sendtok, r2["send_key"])
readtok = r2["read_key"].(string)
r3 := tt.RequestAuthPatch[gin.H](t, admintok, baseUrl, "/api/v2/users/"+uid, gin.H{"read_key": true, "send_key": true})
tt.AssertEqual(t, "admin_key", admintok, r3["admin_key"])
tt.AssertNotEqual(t, "read_key", readtok, r3["read_key"])
tt.AssertNotEqual(t, "send_key", sendtok, r3["send_key"])
readtok = r3["read_key"].(string)
sendtok = r3["send_key"].(string)
r4 := tt.RequestAuthGet[gin.H](t, admintok, baseUrl, "/api/v2/users/"+uid)
tt.AssertEqual(t, "admin_key", admintok, r4["admin_key"])
tt.AssertEqual(t, "read_key", readtok, r4["read_key"])
tt.AssertEqual(t, "send_key", sendtok, r4["send_key"])
r5 := tt.RequestAuthPatch[gin.H](t, admintok, baseUrl, "/api/v2/users/"+uid, gin.H{"admin_key": true})
tt.AssertNotEqual(t, "admin_key", admintok, r5["admin_key"])
tt.AssertEqual(t, "read_key", readtok, r5["read_key"])
tt.AssertEqual(t, "send_key", sendtok, r5["send_key"])
admintokNew := r5["admin_key"].(string)
tt.RequestAuthGetShouldFail(t, admintok, baseUrl, "/api/v2/users/"+uid, 401, apierr.USER_AUTH_FAILED)
r6 := tt.RequestAuthGet[gin.H](t, admintokNew, baseUrl, "/api/v2/users/"+uid)
tt.AssertEqual(t, "admin_key", admintokNew, r6["admin_key"])
tt.AssertEqual(t, "read_key", readtok, r6["read_key"])
tt.AssertEqual(t, "send_key", sendtok, r6["send_key"])
}
func TestDeleteUser(t *testing.T) {
t.SkipNow() // TODO DeleteUser Not implemented

View File

@ -352,9 +352,9 @@ func InitDefaultData(t *testing.T, ws *logic.Application) DefData {
body["user_id"] = users[mex.User].UID
switch mex.Key {
case AKEY:
body["user_key"] = users[mex.User].AdminKey
body["key"] = users[mex.User].AdminKey
case SKEY:
body["user_key"] = users[mex.User].SendKey
body["key"] = users[mex.User].SendKey
}
if mex.Content != "" {
body["content"] = mex.Content

View File

@ -23,7 +23,7 @@
<pre>
curl \
--data "user_id=${userid}" \
--data "user_key=${userkey}" \
--data "key=${key}" \
--data "title=${message_title}" \
--data "content=${message_body}" \
--data "priority=${0|1|2}" \
@ -36,7 +36,7 @@ curl \
<pre>
curl \
--data "user_id={userid}" \
--data "user_key={userkey}" \
--data "key={key}" \
--data "title={message_title}" \
{{config|baseURL}}/</pre>

View File

@ -26,11 +26,11 @@
</p>
<p>
To receive them you will need to install the <a href="https://play.google.com/store/apps/details?id=com.blackforestbytes.simplecloudnotifier">SimpleCloudNotifier</a> app from the play store.
When you open the app you can click on the account tab to see you unique <code>user_id</code> and <code>user_key</code>.
When you open the app you can click on the account tab to see you unique <code>user_id</code> and <code>key</code>.
These two values are used to identify and authenticate your device so that send messages can be routed to your phone.
</p>
<p>
You can at any time generate a new <code>user_key</code> in the app and invalidate the old one.
You can at any time generate a new <code>key</code> in the app and invalidate the old one.
</p>
<p>
There is also a <a href="/">web interface</a> for this API to manually send notifications to your phone or to test your setup.
@ -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).
</p>
<p>
You <i>need</i> to supply a valid <code>[user_id, user_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>[user_id, key]</code> pair and a <code>title</code> for your message, all other parameter are optional.
</p>
</div>
@ -90,7 +90,7 @@
</tr>
<tr>
<td data-label="Statuscode">401 (Unauthorized)</td>
<td data-label="Explanation">The user_id was not found or the user_key is wrong</td>
<td data-label="Explanation">The user_id was not found or the key is wrong</td>
</tr>
<tr>
<td data-label="Statuscode">403 (Forbidden)</td>
@ -126,7 +126,7 @@
</p>
<pre>curl \
--data "user_id={userid}" \
--data "user_key={userkey}" \
--data "key={key}" \
--data "title={message_title}" \
--data "content={message_content}" \
{{config|baseURL}}/</pre>
@ -144,7 +144,7 @@
</p>
<pre>curl \
--data "user_id={userid}" \
--data "user_key={userkey}" \
--data "key={key}" \
--data "title={message_title}" \
--data "priority={0|1|2}" \
{{config|baseURL}}/</pre>
@ -159,7 +159,7 @@
</p>
<pre>curl \
--data "user_id={userid}" \
--data "user_key={userkey}" \
--data "key={key}" \
--data "title={message_title}" \
--data "channel={my_channel}" \
{{config|baseURL}}/</pre>
@ -179,7 +179,7 @@
</p>
<pre>curl \
--data "user_id={userid}" \
--data "user_key={userkey}" \
--data "key={key}" \
--data "title={message_title}" \
--data "msg_id={message_id}" \
{{config|baseURL}}/</pre>
@ -198,7 +198,7 @@
</p>
<pre>curl \
--data "user_id={userid}" \
--data "user_key={userkey}" \
--data "key={key}" \
--data "title={message_title}" \
--data "timestamp={unix_timestamp}" \
{{config|baseURL}}/</pre>

View File

@ -23,7 +23,7 @@ function send()
let data = new FormData();
data.append('user_id', uid.value);
data.append('user_key', key.value);
data.append('key', key.value);
if (tit.value !== '') data.append('title', tit.value);
if (cnt.value !== '') data.append('content', cnt.value);
if (pio.value !== '') data.append('priority', pio.value);

View File

@ -89,7 +89,7 @@ usage<span style="color: #f92672">()</span> <span style="color: #f92672">{</span
--output /dev/null <span style="color: #ae81ff">\</span>
--write-out <span style="color: #e6db74">&quot;%{http_code}&quot;</span> <span style="color: #ae81ff">\</span>
--data <span style="color: #e6db74">&quot;user_id=$user_id&quot;</span> <span style="color: #ae81ff">\</span>
--data <span style="color: #e6db74">&quot;user_key=$user_key&quot;</span> <span style="color: #ae81ff">\</span>
--data <span style="color: #e6db74">&quot;key=$key&quot;</span> <span style="color: #ae81ff">\</span>
--data <span style="color: #e6db74">&quot;title=$title&quot;</span> <span style="color: #ae81ff">\</span>
--data <span style="color: #e6db74">&quot;timestamp=$sendtime&quot;</span> <span style="color: #ae81ff">\</span>
--data <span style="color: #e6db74">&quot;content=$content&quot;</span> <span style="color: #ae81ff">\</span>

View File

@ -26,7 +26,7 @@
<span style="color: #008800; font-style: italic"># INSERT YOUR DATA HERE #</span>
<span style="color: #008800; font-style: italic">################################################################################</span>
user_id=<span style="color: #0000FF">&quot;999&quot;</span>
user_key=<span style="color: #0000FF">&quot;??&quot;</span>
key=<span style="color: #0000FF">&quot;??&quot;</span>
<span style="color: #008800; font-style: italic">################################################################################</span>
usage() {
@ -89,7 +89,7 @@ content=<span style="color: #0000FF">&quot;&quot;</span>
--output /dev/null <span style="color: #0000FF">\</span>
--write-out <span style="color: #0000FF">&quot;%{http_code}&quot;</span> <span style="color: #0000FF">\</span>
--data <span style="color: #0000FF">&quot;user_id=$user_id&quot;</span> <span style="color: #0000FF">\</span>
--data <span style="color: #0000FF">&quot;user_key=$user_key&quot;</span> <span style="color: #0000FF">\</span>
--data <span style="color: #0000FF">&quot;key=$key&quot;</span> <span style="color: #0000FF">\</span>
--data <span style="color: #0000FF">&quot;title=$title&quot;</span> <span style="color: #0000FF">\</span>
--data <span style="color: #0000FF">&quot;timestamp=$sendtime&quot;</span> <span style="color: #0000FF">\</span>
--data <span style="color: #0000FF">&quot;content=$content&quot;</span> <span style="color: #0000FF">\</span>