From b1bd278f9b083bbf6004b768b88bd7d3d06b7803 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Mike=20Schw=C3=B6rer?=
Date: Fri, 21 Apr 2023 21:45:16 +0200
Subject: [PATCH] Add KeyToken authorization
---
scnserver/Makefile | 5 +
scnserver/TODO.md | 22 +-
scnserver/_gen/enum-generate.go | 295 ++++++
scnserver/api/apierr/enums.go | 4 +-
scnserver/api/apihighlight/highlights.go | 2 +-
scnserver/api/ginresp/wrapper.go | 10 +-
scnserver/api/handler/api.go | 918 +++++++++++-------
scnserver/api/handler/common.go | 58 +-
scnserver/api/handler/compat.go | 282 +++---
scnserver/api/handler/message.go | 71 +-
scnserver/api/router.go | 24 +-
scnserver/config.go | 62 +-
scnserver/db/cursortoken/token.go | 2 +-
scnserver/db/impl/primary/channels.go | 62 +-
scnserver/db/impl/primary/keytokens.go | 227 +++++
scnserver/db/impl/primary/schema/schema_3.ddl | 28 +-
scnserver/db/impl/primary/users.go | 103 +-
scnserver/go.mod | 2 +-
scnserver/go.sum | 2 +
scnserver/google/androidPublisher.go | 8 +-
scnserver/logic/appcontext.go | 4 +-
scnserver/logic/application.go | 23 +-
scnserver/logic/permissions.go | 110 ++-
scnserver/models/channel.go | 6 +-
scnserver/models/client.go | 4 +-
scnserver/models/delivery.go | 4 +-
scnserver/models/enums_gen.go | 242 +++++
scnserver/models/ids.go | 34 +
scnserver/models/keytoken.go | 160 +++
scnserver/models/message.go | 2 +-
scnserver/models/permissions.go | 15 +-
scnserver/models/requestlog.go | 99 +-
scnserver/models/subscription.go | 9 +-
scnserver/models/user.go | 31 +-
scnserver/models/utils.go | 6 +
scnserver/swagger/swagger.json | 557 +++++++++--
scnserver/swagger/swagger.yaml | 407 +++++++-
scnserver/test/channel_test.go | 12 -
scnserver/test/client_test.go | 1 -
scnserver/test/compat_test.go | 8 +-
scnserver/test/message_test.go | 38 +-
scnserver/test/send_test.go | 366 +++----
scnserver/test/user_test.go | 63 --
scnserver/test/util/factory.go | 4 +-
scnserver/website/api.html | 4 +-
scnserver/website/api_more.html | 18 +-
scnserver/website/js/logic.js | 2 +-
scnserver/website/scn_send.dark.html | 2 +-
scnserver/website/scn_send.light.html | 4 +-
49 files changed, 3109 insertions(+), 1313 deletions(-)
create mode 100644 scnserver/_gen/enum-generate.go
create mode 100644 scnserver/db/impl/primary/keytokens.go
create mode 100644 scnserver/models/enums_gen.go
create mode 100644 scnserver/models/keytoken.go
diff --git a/scnserver/Makefile b/scnserver/Makefile
index cfa3de5..b647083 100644
--- a/scnserver/Makefile
+++ b/scnserver/Makefile
@@ -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
diff --git a/scnserver/TODO.md b/scnserver/TODO.md
index d7e49b5..fb9ebb6 100644
--- a/scnserver/TODO.md
+++ b/scnserver/TODO.md
@@ -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...
\ No newline at end of file
+ - Remove compat, especially do not create compat id for every new message...
diff --git a/scnserver/_gen/enum-generate.go b/scnserver/_gen/enum-generate.go
new file mode 100644
index 0000000..6e84a5a
--- /dev/null
+++ b/scnserver/_gen/enum-generate.go
@@ -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[A-Za-z0-9_]+)\\s*$"))
+
+var rexEnumDef = rext.W(regexp.MustCompile("^\\s*type\\s+(?P[A-Za-z0-9_]+)\\s+(?P[A-Za-z0-9_]+)\\s*//\\s*(@enum:type).*$"))
+
+var rexValueDef = rext.W(regexp.MustCompile("^\\s*(?P[A-Za-z0-9_]+)\\s+(?P[A-Za-z0-9_]+)\\s*=\\s*(?P(\"[A-Za-z0-9_]+\"|[0-9]+))\\s*(//(?P.*))?.*$"))
+
+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
+}
diff --git a/scnserver/api/apierr/enums.go b/scnserver/api/apierr/enums.go
index a981cf2..283c9d6 100644
--- a/scnserver/api/apierr/enums.go
+++ b/scnserver/api/apierr/enums.go
@@ -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
diff --git a/scnserver/api/apihighlight/highlights.go b/scnserver/api/apihighlight/highlights.go
index 3e4fe9b..c6ce7e4 100644
--- a/scnserver/api/apihighlight/highlights.go
+++ b/scnserver/api/apihighlight/highlights.go
@@ -1,6 +1,6 @@
package apihighlight
-type ErrHighlight int
+type ErrHighlight int //@enum:type
//goland:noinspection GoSnakeCaseUsage
const (
diff --git a/scnserver/api/ginresp/wrapper.go b/scnserver/api/ginresp/wrapper.go
index f1e9018..8d56c22 100644
--- a/scnserver/api/ginresp/wrapper.go
+++ b/scnserver/api/ginresp/wrapper.go
@@ -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,
diff --git a/scnserver/api/handler/api.go b/scnserver/api/handler/api.go
index aeec617..858e1b6 100644
--- a/scnserver/api/handler/api.go
+++ b/scnserver/api/handler/api.go
@@ -30,17 +30,17 @@ func NewAPIHandler(app *logic.Application) APIHandler {
// CreateUser swaggerdoc
//
-// @Summary Create a new user
-// @ID api-user-create
-// @Tags API-v2
+// @Summary Create a new user
+// @ID api-user-create
+// @Tags API-v2
//
-// @Param post_body body handler.CreateUser.body false " "
+// @Param post_body body handler.CreateUser.body false " "
//
-// @Success 200 {object} models.UserJSONWithClients
-// @Failure 400 {object} ginresp.apiError "supplied values/parameters cannot be parsed / are invalid"
-// @Failure 500 {object} ginresp.apiError "internal server error"
+// @Success 200 {object} models.UserJSONWithClientsAndKeys
+// @Failure 400 {object} ginresp.apiError "supplied values/parameters cannot be parsed / are invalid"
+// @Failure 500 {object} ginresp.apiError "internal server error"
//
-// @Router /api/v2/users [POST]
+// @Router /api/v2/users [POST]
func (h APIHandler) CreateUser(g *gin.Context) ginresp.HTTPResponse {
type body struct {
FCMToken string `json:"fcm_token"`
@@ -111,13 +111,28 @@ func (h APIHandler) CreateUser(g *gin.Context) ginresp.HTTPResponse {
username = langext.Ptr(h.app.NormalizeUsername(*username))
}
- userobj, err := h.database.CreateUser(ctx, readKey, sendKey, adminKey, b.ProToken, username)
+ userobj, err := h.database.CreateUser(ctx, b.ProToken, username)
if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to create user in db", err)
}
+ _, err = h.database.CreateKeyToken(ctx, "AdminKey (default)", userobj.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.CreateKeyToken(ctx, "SendKey (default)", userobj.UserID, true, make([]models.ChannelID, 0), models.TokenPermissionList{models.PermChannelSend}, sendKey)
+ if err != nil {
+ return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to create send-key in db", err)
+ }
+
+ _, err = h.database.CreateKeyToken(ctx, "ReadKey (default)", userobj.UserID, true, make([]models.ChannelID, 0), models.TokenPermissionList{models.PermUserRead, models.PermChannelRead}, readKey)
+ if err != nil {
+ return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to create read-key in db", err)
+ }
+
if b.NoClient {
- return ctx.FinishSuccess(ginresp.JSON(http.StatusOK, userobj.JSONWithClients(make([]models.Client, 0))))
+ return ctx.FinishSuccess(ginresp.JSON(http.StatusOK, userobj.JSONWithClients(make([]models.Client, 0), adminKey, sendKey, readKey)))
} else {
err := h.database.DeleteClientsByFCM(ctx, b.FCMToken)
if err != nil {
@@ -129,26 +144,26 @@ func (h APIHandler) CreateUser(g *gin.Context) ginresp.HTTPResponse {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to create client in db", err)
}
- return ctx.FinishSuccess(ginresp.JSON(http.StatusOK, userobj.JSONWithClients([]models.Client{client})))
+ return ctx.FinishSuccess(ginresp.JSON(http.StatusOK, userobj.JSONWithClients([]models.Client{client}, adminKey, sendKey, readKey)))
}
}
// GetUser swaggerdoc
//
-// @Summary Get a user
-// @ID api-user-get
-// @Tags API-v2
+// @Summary Get a user
+// @ID api-user-get
+// @Tags API-v2
//
-// @Param uid path int true "UserID"
+// @Param uid path int true "UserID"
//
-// @Success 200 {object} models.UserJSON
-// @Failure 400 {object} ginresp.apiError "supplied values/parameters cannot be parsed / are invalid"
-// @Failure 401 {object} ginresp.apiError "user is not authorized / has missing permissions"
-// @Failure 404 {object} ginresp.apiError "user not found"
-// @Failure 500 {object} ginresp.apiError "internal server error"
+// @Success 200 {object} models.UserJSON
+// @Failure 400 {object} ginresp.apiError "supplied values/parameters cannot be parsed / are invalid"
+// @Failure 401 {object} ginresp.apiError "user is not authorized / has missing permissions"
+// @Failure 404 {object} ginresp.apiError "user not found"
+// @Failure 500 {object} ginresp.apiError "internal server error"
//
-// @Router /api/v2/users/{uid} [GET]
+// @Router /api/v2/users/{uid} [GET]
func (h APIHandler) GetUser(g *gin.Context) ginresp.HTTPResponse {
type uri struct {
UserID models.UserID `uri:"uid" binding:"entityid"`
@@ -178,36 +193,33 @@ func (h APIHandler) GetUser(g *gin.Context) ginresp.HTTPResponse {
// UpdateUser swaggerdoc
//
-// @Summary (Partially) update a user
-// @Description The body-values are optional, only send the ones you want to update
-// @ID api-user-update
-// @Tags API-v2
+// @Summary (Partially) update a user
+// @Description The body-values are optional, only send the ones you want to update
+// @ID api-user-update
+// @Tags API-v2
//
-// @Param uid path int true "UserID"
+// @Param uid path int true "UserID"
//
-// @Param username body string false "Change the username (send an empty string to clear it)"
-// @Param pro_token body string false "Send a verification of permium purchase"
-// @Param read_key body string false "Send `true` to create a new read_key"
-// @Param send_key body string false "Send `true` to create a new send_key"
-// @Param admin_key body string false "Send `true` to create a new admin_key"
+// @Param username body string false "Change the username (send an empty string to clear it)"
+// @Param pro_token body string false "Send a verification of permium purchase"
+// @Param read_key body string false "Send `true` to create a new read_key"
+// @Param send_key body string false "Send `true` to create a new send_key"
+// @Param admin_key body string false "Send `true` to create a new admin_key"
//
-// @Success 200 {object} models.UserJSON
-// @Failure 400 {object} ginresp.apiError "supplied values/parameters cannot be parsed / are invalid"
-// @Failure 401 {object} ginresp.apiError "user is not authorized / has missing permissions"
-// @Failure 404 {object} ginresp.apiError "user not found"
-// @Failure 500 {object} ginresp.apiError "internal server error"
+// @Success 200 {object} models.UserJSON
+// @Failure 400 {object} ginresp.apiError "supplied values/parameters cannot be parsed / are invalid"
+// @Failure 401 {object} ginresp.apiError "user is not authorized / has missing permissions"
+// @Failure 404 {object} ginresp.apiError "user not found"
+// @Failure 500 {object} ginresp.apiError "internal server error"
//
-// @Router /api/v2/users/{uid} [PATCH]
+// @Router /api/v2/users/{uid} [PATCH]
func (h APIHandler) UpdateUser(g *gin.Context) ginresp.HTTPResponse {
type uri struct {
UserID models.UserID `uri:"uid" binding:"entityid"`
}
type body struct {
- Username *string `json:"username"`
- ProToken *string `json:"pro_token"`
- RefreshReadKey *bool `json:"read_key"`
- RefreshSendKey *bool `json:"send_key"`
- RefreshAdminKey *bool `json:"admin_key"`
+ Username *string `json:"username"`
+ ProToken *string `json:"pro_token"`
}
var u uri
@@ -262,33 +274,6 @@ func (h APIHandler) UpdateUser(g *gin.Context) ginresp.HTTPResponse {
}
}
- if langext.Coalesce(b.RefreshSendKey, false) {
- newkey := h.app.GenerateRandomAuthKey()
-
- err := h.database.UpdateUserSendKey(ctx, u.UserID, newkey)
- if err != nil {
- return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to update user", err)
- }
- }
-
- if langext.Coalesce(b.RefreshReadKey, false) {
- newkey := h.app.GenerateRandomAuthKey()
-
- err := h.database.UpdateUserReadKey(ctx, u.UserID, newkey)
- if err != nil {
- return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to update user", err)
- }
- }
-
- if langext.Coalesce(b.RefreshAdminKey, false) {
- newkey := h.app.GenerateRandomAuthKey()
-
- err := h.database.UpdateUserAdminKey(ctx, u.UserID, newkey)
- if err != nil {
- return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to update user", err)
- }
- }
-
user, err := h.database.GetUser(ctx, u.UserID)
if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query (updated) user", err)
@@ -299,18 +284,18 @@ func (h APIHandler) UpdateUser(g *gin.Context) ginresp.HTTPResponse {
// ListClients swaggerdoc
//
-// @Summary List all clients
-// @ID api-clients-list
-// @Tags API-v2
+// @Summary List all clients
+// @ID api-clients-list
+// @Tags API-v2
//
-// @Param uid path int true "UserID"
+// @Param uid path int true "UserID"
//
-// @Success 200 {object} handler.ListClients.response
-// @Failure 400 {object} ginresp.apiError "supplied values/parameters cannot be parsed / are invalid"
-// @Failure 401 {object} ginresp.apiError "user is not authorized / has missing permissions"
-// @Failure 500 {object} ginresp.apiError "internal server error"
+// @Success 200 {object} handler.ListClients.response
+// @Failure 400 {object} ginresp.apiError "supplied values/parameters cannot be parsed / are invalid"
+// @Failure 401 {object} ginresp.apiError "user is not authorized / has missing permissions"
+// @Failure 500 {object} ginresp.apiError "internal server error"
//
-// @Router /api/v2/users/{uid}/clients [GET]
+// @Router /api/v2/users/{uid}/clients [GET]
func (h APIHandler) ListClients(g *gin.Context) ginresp.HTTPResponse {
type uri struct {
UserID models.UserID `uri:"uid" binding:"entityid"`
@@ -342,20 +327,20 @@ func (h APIHandler) ListClients(g *gin.Context) ginresp.HTTPResponse {
// GetClient swaggerdoc
//
-// @Summary Get a single client
-// @ID api-clients-get
-// @Tags API-v2
+// @Summary Get a single client
+// @ID api-clients-get
+// @Tags API-v2
//
-// @Param uid path int true "UserID"
-// @Param cid path int true "ClientID"
+// @Param uid path int true "UserID"
+// @Param cid path int true "ClientID"
//
-// @Success 200 {object} models.ClientJSON
-// @Failure 400 {object} ginresp.apiError "supplied values/parameters cannot be parsed / are invalid"
-// @Failure 401 {object} ginresp.apiError "user is not authorized / has missing permissions"
-// @Failure 404 {object} ginresp.apiError "client not found"
-// @Failure 500 {object} ginresp.apiError "internal server error"
+// @Success 200 {object} models.ClientJSON
+// @Failure 400 {object} ginresp.apiError "supplied values/parameters cannot be parsed / are invalid"
+// @Failure 401 {object} ginresp.apiError "user is not authorized / has missing permissions"
+// @Failure 404 {object} ginresp.apiError "client not found"
+// @Failure 500 {object} ginresp.apiError "internal server error"
//
-// @Router /api/v2/users/{uid}/clients/{cid} [GET]
+// @Router /api/v2/users/{uid}/clients/{cid} [GET]
func (h APIHandler) GetClient(g *gin.Context) ginresp.HTTPResponse {
type uri struct {
UserID models.UserID `uri:"uid" binding:"entityid"`
@@ -386,20 +371,20 @@ func (h APIHandler) GetClient(g *gin.Context) ginresp.HTTPResponse {
// AddClient swaggerdoc
//
-// @Summary Add a new clients
-// @ID api-clients-create
-// @Tags API-v2
+// @Summary Add a new clients
+// @ID api-clients-create
+// @Tags API-v2
//
-// @Param uid path int true "UserID"
+// @Param uid path int true "UserID"
//
-// @Param post_body body handler.AddClient.body false " "
+// @Param post_body body handler.AddClient.body false " "
//
-// @Success 200 {object} models.ClientJSON
-// @Failure 400 {object} ginresp.apiError "supplied values/parameters cannot be parsed / are invalid"
-// @Failure 401 {object} ginresp.apiError "user is not authorized / has missing permissions"
-// @Failure 500 {object} ginresp.apiError "internal server error"
+// @Success 200 {object} models.ClientJSON
+// @Failure 400 {object} ginresp.apiError "supplied values/parameters cannot be parsed / are invalid"
+// @Failure 401 {object} ginresp.apiError "user is not authorized / has missing permissions"
+// @Failure 500 {object} ginresp.apiError "internal server error"
//
-// @Router /api/v2/users/{uid}/clients [POST]
+// @Router /api/v2/users/{uid}/clients [POST]
func (h APIHandler) AddClient(g *gin.Context) ginresp.HTTPResponse {
type uri struct {
UserID models.UserID `uri:"uid" binding:"entityid"`
@@ -447,20 +432,20 @@ func (h APIHandler) AddClient(g *gin.Context) ginresp.HTTPResponse {
// DeleteClient swaggerdoc
//
-// @Summary Delete a client
-// @ID api-clients-delete
-// @Tags API-v2
+// @Summary Delete a client
+// @ID api-clients-delete
+// @Tags API-v2
//
-// @Param uid path int true "UserID"
-// @Param cid path int true "ClientID"
+// @Param uid path int true "UserID"
+// @Param cid path int true "ClientID"
//
-// @Success 200 {object} models.ClientJSON
-// @Failure 400 {object} ginresp.apiError "supplied values/parameters cannot be parsed / are invalid"
-// @Failure 401 {object} ginresp.apiError "user is not authorized / has missing permissions"
-// @Failure 404 {object} ginresp.apiError "client not found"
-// @Failure 500 {object} ginresp.apiError "internal server error"
+// @Success 200 {object} models.ClientJSON
+// @Failure 400 {object} ginresp.apiError "supplied values/parameters cannot be parsed / are invalid"
+// @Failure 401 {object} ginresp.apiError "user is not authorized / has missing permissions"
+// @Failure 404 {object} ginresp.apiError "client not found"
+// @Failure 500 {object} ginresp.apiError "internal server error"
//
-// @Router /api/v2/users/{uid}/clients/{cid} [DELETE]
+// @Router /api/v2/users/{uid}/clients/{cid} [DELETE]
func (h APIHandler) DeleteClient(g *gin.Context) ginresp.HTTPResponse {
type uri struct {
UserID models.UserID `uri:"uid" binding:"entityid"`
@@ -496,26 +481,26 @@ func (h APIHandler) DeleteClient(g *gin.Context) ginresp.HTTPResponse {
// ListChannels swaggerdoc
//
-// @Summary List channels of a user (subscribed/owned/all)
-// @Description The possible values for 'selector' are:
-// @Description - "owned" Return all channels of the user
-// @Description - "subscribed" Return all channels that the user is subscribing to
-// @Description - "all" Return channels that the user owns or is subscribing
-// @Description - "subscribed_any" Return all channels that the user is subscribing to (even unconfirmed)
-// @Description - "all_any" Return channels that the user owns or is subscribing (even unconfirmed)
+// @Summary List channels of a user (subscribed/owned/all)
+// @Description The possible values for 'selector' are:
+// @Description - "owned" Return all channels of the user
+// @Description - "subscribed" Return all channels that the user is subscribing to
+// @Description - "all" Return channels that the user owns or is subscribing
+// @Description - "subscribed_any" Return all channels that the user is subscribing to (even unconfirmed)
+// @Description - "all_any" Return channels that the user owns or is subscribing (even unconfirmed)
//
-// @ID api-channels-list
-// @Tags API-v2
+// @ID api-channels-list
+// @Tags API-v2
//
-// @Param uid path int true "UserID"
-// @Param selector query string false "Filter channels (default: owned)" Enums(owned, subscribed, all, subscribed_any, all_any)
+// @Param uid path int true "UserID"
+// @Param selector query string false "Filter channels (default: owned)" Enums(owned, subscribed, all, subscribed_any, all_any)
//
-// @Success 200 {object} handler.ListChannels.response
-// @Failure 400 {object} ginresp.apiError "supplied values/parameters cannot be parsed / are invalid"
-// @Failure 401 {object} ginresp.apiError "user is not authorized / has missing permissions"
-// @Failure 500 {object} ginresp.apiError "internal server error"
+// @Success 200 {object} handler.ListChannels.response
+// @Failure 400 {object} ginresp.apiError "supplied values/parameters cannot be parsed / are invalid"
+// @Failure 401 {object} ginresp.apiError "user is not authorized / has missing permissions"
+// @Failure 500 {object} ginresp.apiError "internal server error"
//
-// @Router /api/v2/users/{uid}/channels [GET]
+// @Router /api/v2/users/{uid}/channels [GET]
func (h APIHandler) ListChannels(g *gin.Context) ginresp.HTTPResponse {
type uri struct {
UserID models.UserID `uri:"uid" binding:"entityid"`
@@ -594,20 +579,20 @@ func (h APIHandler) ListChannels(g *gin.Context) ginresp.HTTPResponse {
// GetChannel swaggerdoc
//
-// @Summary Get a single channel
-// @ID api-channels-get
-// @Tags API-v2
+// @Summary Get a single channel
+// @ID api-channels-get
+// @Tags API-v2
//
-// @Param uid path int true "UserID"
-// @Param cid path int true "ChannelID"
+// @Param uid path int true "UserID"
+// @Param cid path int true "ChannelID"
//
-// @Success 200 {object} models.ChannelWithSubscriptionJSON
-// @Failure 400 {object} ginresp.apiError "supplied values/parameters cannot be parsed / are invalid"
-// @Failure 401 {object} ginresp.apiError "user is not authorized / has missing permissions"
-// @Failure 404 {object} ginresp.apiError "channel not found"
-// @Failure 500 {object} ginresp.apiError "internal server error"
+// @Success 200 {object} models.ChannelWithSubscriptionJSON
+// @Failure 400 {object} ginresp.apiError "supplied values/parameters cannot be parsed / are invalid"
+// @Failure 401 {object} ginresp.apiError "user is not authorized / has missing permissions"
+// @Failure 404 {object} ginresp.apiError "channel not found"
+// @Failure 500 {object} ginresp.apiError "internal server error"
//
-// @Router /api/v2/users/{uid}/channels/{cid} [GET]
+// @Router /api/v2/users/{uid}/channels/{cid} [GET]
func (h APIHandler) GetChannel(g *gin.Context) ginresp.HTTPResponse {
type uri struct {
UserID models.UserID `uri:"uid" binding:"entityid"`
@@ -638,20 +623,20 @@ func (h APIHandler) GetChannel(g *gin.Context) ginresp.HTTPResponse {
// CreateChannel swaggerdoc
//
-// @Summary Create a new (empty) channel
-// @ID api-channels-create
-// @Tags API-v2
+// @Summary Create a new (empty) channel
+// @ID api-channels-create
+// @Tags API-v2
//
-// @Param uid path int true "UserID"
-// @Param post_body body handler.CreateChannel.body false " "
+// @Param uid path int true "UserID"
+// @Param post_body body handler.CreateChannel.body false " "
//
-// @Success 200 {object} models.ChannelWithSubscriptionJSON
-// @Failure 400 {object} ginresp.apiError "supplied values/parameters cannot be parsed / are invalid"
-// @Failure 401 {object} ginresp.apiError "user is not authorized / has missing permissions"
-// @Failure 409 {object} ginresp.apiError "channel already exists"
-// @Failure 500 {object} ginresp.apiError "internal server error"
+// @Success 200 {object} models.ChannelWithSubscriptionJSON
+// @Failure 400 {object} ginresp.apiError "supplied values/parameters cannot be parsed / are invalid"
+// @Failure 401 {object} ginresp.apiError "user is not authorized / has missing permissions"
+// @Failure 409 {object} ginresp.apiError "channel already exists"
+// @Failure 500 {object} ginresp.apiError "internal server error"
//
-// @Router /api/v2/users/{uid}/channels [POST]
+// @Router /api/v2/users/{uid}/channels [POST]
func (h APIHandler) CreateChannel(g *gin.Context) ginresp.HTTPResponse {
type uri struct {
UserID models.UserID `uri:"uid" binding:"entityid"`
@@ -705,9 +690,8 @@ func (h APIHandler) CreateChannel(g *gin.Context) ginresp.HTTPResponse {
}
subscribeKey := h.app.GenerateRandomAuthKey()
- sendKey := h.app.GenerateRandomAuthKey()
- channel, err := h.database.CreateChannel(ctx, u.UserID, channelDisplayName, channelInternalName, subscribeKey, sendKey)
+ channel, err := h.database.CreateChannel(ctx, u.UserID, channelDisplayName, channelInternalName, subscribeKey)
if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to create channel", err)
}
@@ -731,24 +715,24 @@ func (h APIHandler) CreateChannel(g *gin.Context) ginresp.HTTPResponse {
// UpdateChannel swaggerdoc
//
-// @Summary (Partially) update a channel
-// @ID api-channels-update
-// @Tags API-v2
+// @Summary (Partially) update a channel
+// @ID api-channels-update
+// @Tags API-v2
//
-// @Param uid path int true "UserID"
-// @Param cid path int true "ChannelID"
+// @Param uid path int true "UserID"
+// @Param cid path int true "ChannelID"
//
-// @Param subscribe_key body string false "Send `true` to create a new subscribe_key"
-// @Param send_key body string false "Send `true` to create a new send_key"
-// @Param display_name body string false "Change the cahnnel display-name (only chnages to lowercase/uppercase are allowed - internal_name must stay the same)"
+// @Param subscribe_key body string false "Send `true` to create a new subscribe_key"
+// @Param send_key body string false "Send `true` to create a new send_key"
+// @Param display_name body string false "Change the cahnnel display-name (only chnages to lowercase/uppercase are allowed - internal_name must stay the same)"
//
-// @Success 200 {object} models.ChannelWithSubscriptionJSON
-// @Failure 400 {object} ginresp.apiError "supplied values/parameters cannot be parsed / are invalid"
-// @Failure 401 {object} ginresp.apiError "user is not authorized / has missing permissions"
-// @Failure 404 {object} ginresp.apiError "channel not found"
-// @Failure 500 {object} ginresp.apiError "internal server error"
+// @Success 200 {object} models.ChannelWithSubscriptionJSON
+// @Failure 400 {object} ginresp.apiError "supplied values/parameters cannot be parsed / are invalid"
+// @Failure 401 {object} ginresp.apiError "user is not authorized / has missing permissions"
+// @Failure 404 {object} ginresp.apiError "channel not found"
+// @Failure 500 {object} ginresp.apiError "internal server error"
//
-// @Router /api/v2/users/{uid}/channels/{cid} [PATCH]
+// @Router /api/v2/users/{uid}/channels/{cid} [PATCH]
func (h APIHandler) UpdateChannel(g *gin.Context) ginresp.HTTPResponse {
type uri struct {
UserID models.UserID `uri:"uid" binding:"entityid"`
@@ -756,7 +740,6 @@ func (h APIHandler) UpdateChannel(g *gin.Context) ginresp.HTTPResponse {
}
type body struct {
RefreshSubscribeKey *bool `json:"subscribe_key"`
- RefreshSendKey *bool `json:"send_key"`
DisplayName *string `json:"display_name"`
DescriptionName *string `json:"description_name"`
}
@@ -789,15 +772,6 @@ func (h APIHandler) UpdateChannel(g *gin.Context) ginresp.HTTPResponse {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query user", err)
}
- if langext.Coalesce(b.RefreshSendKey, false) {
- newkey := h.app.GenerateRandomAuthKey()
-
- err := h.database.UpdateChannelSendKey(ctx, u.ChannelID, newkey)
- if err != nil {
- return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to update channel", err)
- }
- }
-
if langext.Coalesce(b.RefreshSubscribeKey, false) {
newkey := h.app.GenerateRandomAuthKey()
@@ -855,25 +829,25 @@ func (h APIHandler) UpdateChannel(g *gin.Context) ginresp.HTTPResponse {
// ListChannelMessages swaggerdoc
//
-// @Summary List messages of a channel
-// @Description The next_page_token is an opaque token, the special value "@start" (or empty-string) is the beginning and "@end" is the end
-// @Description Simply start the pagination without a next_page_token and get the next page by calling this endpoint with the returned next_page_token of the last query
-// @Description If there are no more entries the token "@end" will be returned
-// @Description By default we return long messages with a trimmed body, if trimmed=false is supplied we return full messages (this reduces the max page_size)
-// @ID api-channel-messages
-// @Tags API-v2
+// @Summary List messages of a channel
+// @Description The next_page_token is an opaque token, the special value "@start" (or empty-string) is the beginning and "@end" is the end
+// @Description Simply start the pagination without a next_page_token and get the next page by calling this endpoint with the returned next_page_token of the last query
+// @Description If there are no more entries the token "@end" will be returned
+// @Description By default we return long messages with a trimmed body, if trimmed=false is supplied we return full messages (this reduces the max page_size)
+// @ID api-channel-messages
+// @Tags API-v2
//
-// @Param query_data query handler.ListChannelMessages.query false " "
-// @Param uid path int true "UserID"
-// @Param cid path int true "ChannelID"
+// @Param query_data query handler.ListChannelMessages.query false " "
+// @Param uid path int true "UserID"
+// @Param cid path int true "ChannelID"
//
-// @Success 200 {object} handler.ListChannelMessages.response
-// @Failure 400 {object} ginresp.apiError "supplied values/parameters cannot be parsed / are invalid"
-// @Failure 401 {object} ginresp.apiError "user is not authorized / has missing permissions"
-// @Failure 404 {object} ginresp.apiError "channel not found"
-// @Failure 500 {object} ginresp.apiError "internal server error"
+// @Success 200 {object} handler.ListChannelMessages.response
+// @Failure 400 {object} ginresp.apiError "supplied values/parameters cannot be parsed / are invalid"
+// @Failure 401 {object} ginresp.apiError "user is not authorized / has missing permissions"
+// @Failure 404 {object} ginresp.apiError "channel not found"
+// @Failure 500 {object} ginresp.apiError "internal server error"
//
-// @Router /api/v2/users/{uid}/channels/{cid}/messages [GET]
+// @Router /api/v2/users/{uid}/channels/{cid}/messages [GET]
func (h APIHandler) ListChannelMessages(g *gin.Context) ginresp.HTTPResponse {
type uri struct {
ChannelUserID models.UserID `uri:"uid" binding:"entityid"`
@@ -905,10 +879,6 @@ func (h APIHandler) ListChannelMessages(g *gin.Context) ginresp.HTTPResponse {
pageSize := mathext.Clamp(langext.Coalesce(q.PageSize, 64), 1, maxPageSize)
- if permResp := ctx.CheckPermissionRead(); permResp != nil {
- return *permResp
- }
-
channel, err := h.database.GetChannel(ctx, u.ChannelUserID, u.ChannelID)
if err == sql.ErrNoRows {
return ginresp.APIError(g, 404, apierr.CHANNEL_NOT_FOUND, "Channel not found", err)
@@ -917,17 +887,8 @@ func (h APIHandler) ListChannelMessages(g *gin.Context) ginresp.HTTPResponse {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query channel", err)
}
- userid := *ctx.GetPermissionUserID()
-
- sub, err := h.database.GetSubscriptionBySubscriber(ctx, userid, channel.ChannelID)
- if err == sql.ErrNoRows {
- return ginresp.APIError(g, 401, apierr.USER_AUTH_FAILED, "You are not authorized for this action", nil)
- }
- if err != nil {
- return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query subscription", err)
- }
- if !sub.Confirmed {
- return ginresp.APIError(g, 401, apierr.USER_AUTH_FAILED, "You are not authorized for this action", nil)
+ if permResp := ctx.CheckPermissionChanMessagesRead(channel.Channel); permResp != nil {
+ return *permResp
}
tok, err := ct.Decode(langext.Coalesce(q.NextPageToken, ""))
@@ -956,27 +917,27 @@ func (h APIHandler) ListChannelMessages(g *gin.Context) ginresp.HTTPResponse {
// ListUserSubscriptions swaggerdoc
//
-// @Summary List all subscriptions of a user (incoming/owned)
-// @Description The possible values for 'selector' are:
-// @Description - "outgoing_all" All subscriptions (confirmed/unconfirmed) with the user as subscriber (= subscriptions he can use to read channels)
-// @Description - "outgoing_confirmed" Confirmed subscriptions with the user as subscriber
-// @Description - "outgoing_unconfirmed" Unconfirmed (Pending) subscriptions with the user as subscriber
-// @Description - "incoming_all" All subscriptions (confirmed/unconfirmed) from other users to channels of this user (= incoming subscriptions and subscription requests)
-// @Description - "incoming_confirmed" Confirmed subscriptions from other users to channels of this user
-// @Description - "incoming_unconfirmed" Unconfirmed subscriptions from other users to channels of this user (= requests)
+// @Summary List all subscriptions of a user (incoming/owned)
+// @Description The possible values for 'selector' are:
+// @Description - "outgoing_all" All subscriptions (confirmed/unconfirmed) with the user as subscriber (= subscriptions he can use to read channels)
+// @Description - "outgoing_confirmed" Confirmed subscriptions with the user as subscriber
+// @Description - "outgoing_unconfirmed" Unconfirmed (Pending) subscriptions with the user as subscriber
+// @Description - "incoming_all" All subscriptions (confirmed/unconfirmed) from other users to channels of this user (= incoming subscriptions and subscription requests)
+// @Description - "incoming_confirmed" Confirmed subscriptions from other users to channels of this user
+// @Description - "incoming_unconfirmed" Unconfirmed subscriptions from other users to channels of this user (= requests)
//
-// @ID api-user-subscriptions-list
-// @Tags API-v2
+// @ID api-user-subscriptions-list
+// @Tags API-v2
//
-// @Param uid path int true "UserID"
-// @Param selector query string true "Filter subscriptions (default: owner_all)" Enums(outgoing_all, outgoing_confirmed, outgoing_unconfirmed, incoming_all, incoming_confirmed, incoming_unconfirmed)
+// @Param uid path int true "UserID"
+// @Param selector query string true "Filter subscriptions (default: owner_all)" Enums(outgoing_all, outgoing_confirmed, outgoing_unconfirmed, incoming_all, incoming_confirmed, incoming_unconfirmed)
//
-// @Success 200 {object} handler.ListUserSubscriptions.response
-// @Failure 400 {object} ginresp.apiError "supplied values/parameters cannot be parsed / are invalid"
-// @Failure 401 {object} ginresp.apiError "user is not authorized / has missing permissions"
-// @Failure 500 {object} ginresp.apiError "internal server error"
+// @Success 200 {object} handler.ListUserSubscriptions.response
+// @Failure 400 {object} ginresp.apiError "supplied values/parameters cannot be parsed / are invalid"
+// @Failure 401 {object} ginresp.apiError "user is not authorized / has missing permissions"
+// @Failure 500 {object} ginresp.apiError "internal server error"
//
-// @Router /api/v2/users/{uid}/subscriptions [GET]
+// @Router /api/v2/users/{uid}/subscriptions [GET]
func (h APIHandler) ListUserSubscriptions(g *gin.Context) ginresp.HTTPResponse {
type uri struct {
UserID models.UserID `uri:"uid" binding:"entityid"`
@@ -1060,20 +1021,20 @@ func (h APIHandler) ListUserSubscriptions(g *gin.Context) ginresp.HTTPResponse {
// ListChannelSubscriptions swaggerdoc
//
-// @Summary List all subscriptions of a channel
-// @ID api-chan-subscriptions-list
-// @Tags API-v2
+// @Summary List all subscriptions of a channel
+// @ID api-chan-subscriptions-list
+// @Tags API-v2
//
-// @Param uid path int true "UserID"
-// @Param cid path int true "ChannelID"
+// @Param uid path int true "UserID"
+// @Param cid path int true "ChannelID"
//
-// @Success 200 {object} handler.ListChannelSubscriptions.response
-// @Failure 400 {object} ginresp.apiError "supplied values/parameters cannot be parsed / are invalid"
-// @Failure 401 {object} ginresp.apiError "user is not authorized / has missing permissions"
-// @Failure 404 {object} ginresp.apiError "channel not found"
-// @Failure 500 {object} ginresp.apiError "internal server error"
+// @Success 200 {object} handler.ListChannelSubscriptions.response
+// @Failure 400 {object} ginresp.apiError "supplied values/parameters cannot be parsed / are invalid"
+// @Failure 401 {object} ginresp.apiError "user is not authorized / has missing permissions"
+// @Failure 404 {object} ginresp.apiError "channel not found"
+// @Failure 500 {object} ginresp.apiError "internal server error"
//
-// @Router /api/v2/users/{uid}/channels/{cid}/subscriptions [GET]
+// @Router /api/v2/users/{uid}/channels/{cid}/subscriptions [GET]
func (h APIHandler) ListChannelSubscriptions(g *gin.Context) ginresp.HTTPResponse {
type uri struct {
UserID models.UserID `uri:"uid" binding:"entityid"`
@@ -1114,20 +1075,20 @@ func (h APIHandler) ListChannelSubscriptions(g *gin.Context) ginresp.HTTPRespons
// GetSubscription swaggerdoc
//
-// @Summary Get a single subscription
-// @ID api-subscriptions-get
-// @Tags API-v2
+// @Summary Get a single subscription
+// @ID api-subscriptions-get
+// @Tags API-v2
//
-// @Param uid path int true "UserID"
-// @Param sid path int true "SubscriptionID"
+// @Param uid path int true "UserID"
+// @Param sid path int true "SubscriptionID"
//
-// @Success 200 {object} models.SubscriptionJSON
-// @Failure 400 {object} ginresp.apiError "supplied values/parameters cannot be parsed / are invalid"
-// @Failure 401 {object} ginresp.apiError "user is not authorized / has missing permissions"
-// @Failure 404 {object} ginresp.apiError "subscription not found"
-// @Failure 500 {object} ginresp.apiError "internal server error"
+// @Success 200 {object} models.SubscriptionJSON
+// @Failure 400 {object} ginresp.apiError "supplied values/parameters cannot be parsed / are invalid"
+// @Failure 401 {object} ginresp.apiError "user is not authorized / has missing permissions"
+// @Failure 404 {object} ginresp.apiError "subscription not found"
+// @Failure 500 {object} ginresp.apiError "internal server error"
//
-// @Router /api/v2/users/{uid}/subscriptions/{sid} [GET]
+// @Router /api/v2/users/{uid}/subscriptions/{sid} [GET]
func (h APIHandler) GetSubscription(g *gin.Context) ginresp.HTTPResponse {
type uri struct {
UserID models.UserID `uri:"uid" binding:"entityid"`
@@ -1161,20 +1122,20 @@ func (h APIHandler) GetSubscription(g *gin.Context) ginresp.HTTPResponse {
// CancelSubscription swaggerdoc
//
-// @Summary Cancel (delete) subscription
-// @ID api-subscriptions-delete
-// @Tags API-v2
+// @Summary Cancel (delete) subscription
+// @ID api-subscriptions-delete
+// @Tags API-v2
//
-// @Param uid path int true "UserID"
-// @Param sid path int true "SubscriptionID"
+// @Param uid path int true "UserID"
+// @Param sid path int true "SubscriptionID"
//
-// @Success 200 {object} models.SubscriptionJSON
-// @Failure 400 {object} ginresp.apiError "supplied values/parameters cannot be parsed / are invalid"
-// @Failure 401 {object} ginresp.apiError "user is not authorized / has missing permissions"
-// @Failure 404 {object} ginresp.apiError "subscription not found"
-// @Failure 500 {object} ginresp.apiError "internal server error"
+// @Success 200 {object} models.SubscriptionJSON
+// @Failure 400 {object} ginresp.apiError "supplied values/parameters cannot be parsed / are invalid"
+// @Failure 401 {object} ginresp.apiError "user is not authorized / has missing permissions"
+// @Failure 404 {object} ginresp.apiError "subscription not found"
+// @Failure 500 {object} ginresp.apiError "internal server error"
//
-// @Router /api/v2/users/{uid}/subscriptions/{sid} [DELETE]
+// @Router /api/v2/users/{uid}/subscriptions/{sid} [DELETE]
func (h APIHandler) CancelSubscription(g *gin.Context) ginresp.HTTPResponse {
type uri struct {
UserID models.UserID `uri:"uid" binding:"entityid"`
@@ -1213,21 +1174,21 @@ func (h APIHandler) CancelSubscription(g *gin.Context) ginresp.HTTPResponse {
// CreateSubscription swaggerdoc
//
-// @Summary Create/Request a subscription
-// @Description Either [channel_owner_user_id, channel_internal_name] or [channel_id] must be supplied in the request body
-// @ID api-subscriptions-create
-// @Tags API-v2
+// @Summary Create/Request a subscription
+// @Description Either [channel_owner_user_id, channel_internal_name] or [channel_id] must be supplied in the request body
+// @ID api-subscriptions-create
+// @Tags API-v2
//
-// @Param uid path int true "UserID"
-// @Param query_data query handler.CreateSubscription.query false " "
-// @Param post_data body handler.CreateSubscription.body false " "
+// @Param uid path int true "UserID"
+// @Param query_data query handler.CreateSubscription.query false " "
+// @Param post_data body handler.CreateSubscription.body false " "
//
-// @Success 200 {object} models.SubscriptionJSON
-// @Failure 400 {object} ginresp.apiError "supplied values/parameters cannot be parsed / are invalid"
-// @Failure 401 {object} ginresp.apiError "user is not authorized / has missing permissions"
-// @Failure 500 {object} ginresp.apiError "internal server error"
+// @Success 200 {object} models.SubscriptionJSON
+// @Failure 400 {object} ginresp.apiError "supplied values/parameters cannot be parsed / are invalid"
+// @Failure 401 {object} ginresp.apiError "user is not authorized / has missing permissions"
+// @Failure 500 {object} ginresp.apiError "internal server error"
//
-// @Router /api/v2/users/{uid}/subscriptions [POST]
+// @Router /api/v2/users/{uid}/subscriptions [POST]
func (h APIHandler) CreateSubscription(g *gin.Context) ginresp.HTTPResponse {
type uri struct {
UserID models.UserID `uri:"uid" binding:"entityid"`
@@ -1302,21 +1263,21 @@ func (h APIHandler) CreateSubscription(g *gin.Context) ginresp.HTTPResponse {
// UpdateSubscription swaggerdoc
//
-// @Summary Update a subscription (e.g. confirm)
-// @ID api-subscriptions-update
-// @Tags API-v2
+// @Summary Update a subscription (e.g. confirm)
+// @ID api-subscriptions-update
+// @Tags API-v2
//
-// @Param uid path int true "UserID"
-// @Param sid path int true "SubscriptionID"
-// @Param post_data body handler.UpdateSubscription.body false " "
+// @Param uid path int true "UserID"
+// @Param sid path int true "SubscriptionID"
+// @Param post_data body handler.UpdateSubscription.body false " "
//
-// @Success 200 {object} models.SubscriptionJSON
-// @Failure 400 {object} ginresp.apiError "supplied values/parameters cannot be parsed / are invalid"
-// @Failure 401 {object} ginresp.apiError "user is not authorized / has missing permissions"
-// @Failure 404 {object} ginresp.apiError "subscription not found"
-// @Failure 500 {object} ginresp.apiError "internal server error"
+// @Success 200 {object} models.SubscriptionJSON
+// @Failure 400 {object} ginresp.apiError "supplied values/parameters cannot be parsed / are invalid"
+// @Failure 401 {object} ginresp.apiError "user is not authorized / has missing permissions"
+// @Failure 404 {object} ginresp.apiError "subscription not found"
+// @Failure 500 {object} ginresp.apiError "internal server error"
//
-// @Router /api/v2/users/{uid}/subscriptions/{sid} [PATCH]
+// @Router /api/v2/users/{uid}/subscriptions/{sid} [PATCH]
func (h APIHandler) UpdateSubscription(g *gin.Context) ginresp.HTTPResponse {
type uri struct {
UserID models.UserID `uri:"uid" binding:"entityid"`
@@ -1371,22 +1332,22 @@ func (h APIHandler) UpdateSubscription(g *gin.Context) ginresp.HTTPResponse {
// ListMessages swaggerdoc
//
-// @Summary List all (subscribed) messages
-// @Description The next_page_token is an opaque token, the special value "@start" (or empty-string) is the beginning and "@end" is the end
-// @Description Simply start the pagination without a next_page_token and get the next page by calling this endpoint with the returned next_page_token of the last query
-// @Description If there are no more entries the token "@end" will be returned
-// @Description By default we return long messages with a trimmed body, if trimmed=false is supplied we return full messages (this reduces the max page_size)
-// @ID api-messages-list
-// @Tags API-v2
+// @Summary List all (subscribed) messages
+// @Description The next_page_token is an opaque token, the special value "@start" (or empty-string) is the beginning and "@end" is the end
+// @Description Simply start the pagination without a next_page_token and get the next page by calling this endpoint with the returned next_page_token of the last query
+// @Description If there are no more entries the token "@end" will be returned
+// @Description By default we return long messages with a trimmed body, if trimmed=false is supplied we return full messages (this reduces the max page_size)
+// @ID api-messages-list
+// @Tags API-v2
//
-// @Param query_data query handler.ListMessages.query false " "
+// @Param query_data query handler.ListMessages.query false " "
//
-// @Success 200 {object} handler.ListMessages.response
-// @Failure 400 {object} ginresp.apiError "supplied values/parameters cannot be parsed / are invalid"
-// @Failure 401 {object} ginresp.apiError "user is not authorized / has missing permissions"
-// @Failure 500 {object} ginresp.apiError "internal server error"
+// @Success 200 {object} handler.ListMessages.response
+// @Failure 400 {object} ginresp.apiError "supplied values/parameters cannot be parsed / are invalid"
+// @Failure 401 {object} ginresp.apiError "user is not authorized / has missing permissions"
+// @Failure 500 {object} ginresp.apiError "internal server error"
//
-// @Router /api/v2/messages [GET]
+// @Router /api/v2/messages [GET]
func (h APIHandler) ListMessages(g *gin.Context) ginresp.HTTPResponse {
type query struct {
PageSize *int `json:"page_size" form:"page_size"`
@@ -1413,7 +1374,7 @@ func (h APIHandler) ListMessages(g *gin.Context) ginresp.HTTPResponse {
pageSize := mathext.Clamp(langext.Coalesce(q.PageSize, 64), 1, maxPageSize)
- if permResp := ctx.CheckPermissionRead(); permResp != nil {
+ if permResp := ctx.CheckPermissionSelfAllMessagesRead(); permResp != nil {
return *permResp
}
@@ -1454,22 +1415,22 @@ func (h APIHandler) ListMessages(g *gin.Context) ginresp.HTTPResponse {
// GetMessage swaggerdoc
//
-// @Summary Get a single message (untrimmed)
-// @Description The user must either own the message and request the resource with the READ or ADMIN Key
-// @Description Or the user must subscribe to the corresponding channel (and be confirmed) and request the resource with the READ or ADMIN Key
-// @Description The returned message is never trimmed
-// @ID api-messages-get
-// @Tags API-v2
+// @Summary Get a single message (untrimmed)
+// @Description The user must either own the message and request the resource with the READ or ADMIN Key
+// @Description Or the user must subscribe to the corresponding channel (and be confirmed) and request the resource with the READ or ADMIN Key
+// @Description The returned message is never trimmed
+// @ID api-messages-get
+// @Tags API-v2
//
-// @Param mid path int true "MessageID"
+// @Param mid path int true "MessageID"
//
-// @Success 200 {object} models.MessageJSON
-// @Failure 400 {object} ginresp.apiError "supplied values/parameters cannot be parsed / are invalid"
-// @Failure 401 {object} ginresp.apiError "user is not authorized / has missing permissions"
-// @Failure 404 {object} ginresp.apiError "message not found"
-// @Failure 500 {object} ginresp.apiError "internal server error"
+// @Success 200 {object} models.MessageJSON
+// @Failure 400 {object} ginresp.apiError "supplied values/parameters cannot be parsed / are invalid"
+// @Failure 401 {object} ginresp.apiError "user is not authorized / has missing permissions"
+// @Failure 404 {object} ginresp.apiError "message not found"
+// @Failure 500 {object} ginresp.apiError "internal server error"
//
-// @Router /api/v2/messages/{mid} [PATCH]
+// @Router /api/v2/messages/{mid} [PATCH]
func (h APIHandler) GetMessage(g *gin.Context) ginresp.HTTPResponse {
type uri struct {
MessageID models.MessageID `uri:"mid" binding:"entityid"`
@@ -1494,52 +1455,50 @@ func (h APIHandler) GetMessage(g *gin.Context) ginresp.HTTPResponse {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query message", err)
}
- if !ctx.CheckPermissionMessageReadDirect(msg) {
+ // either we have direct read permissions (it is our message + read/admin key)
+ // or we subscribe (+confirmed) to the channel and have read/admin key
- // either we have direct read permissions (it is our message + read/admin key)
- // or we subscribe (+confirmed) to the channel and have read/admin key
+ if ctx.CheckPermissionMessageRead(msg) {
+ return ctx.FinishSuccess(ginresp.JSON(http.StatusOK, msg.FullJSON()))
+ }
- if uid := ctx.GetPermissionUserID(); uid != nil && ctx.IsPermissionUserRead() {
- sub, err := h.database.GetSubscriptionBySubscriber(ctx, *uid, msg.ChannelID)
- if err != nil {
- return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query subscription", err)
- }
- if sub == nil {
- // not subbed
- return ginresp.APIError(g, 401, apierr.USER_AUTH_FAILED, "You are not authorized for this action", nil)
- }
- if !sub.Confirmed {
- // sub not confirmed
- return ginresp.APIError(g, 401, apierr.USER_AUTH_FAILED, "You are not authorized for this action", nil)
- }
- // => perm okay
-
- } else {
- // auth-key is not set or not a user:x variant
+ if uid := ctx.GetPermissionUserID(); uid != nil && ctx.CheckPermissionUserRead(*uid) == nil {
+ sub, err := h.database.GetSubscriptionBySubscriber(ctx, *uid, msg.ChannelID)
+ if err != nil {
+ return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query subscription", err)
+ }
+ if sub == nil {
+ // not subbed
+ return ginresp.APIError(g, 401, apierr.USER_AUTH_FAILED, "You are not authorized for this action", nil)
+ }
+ if !sub.Confirmed {
+ // sub not confirmed
return ginresp.APIError(g, 401, apierr.USER_AUTH_FAILED, "You are not authorized for this action", nil)
}
+ // => perm okay
+ return ctx.FinishSuccess(ginresp.JSON(http.StatusOK, msg.FullJSON()))
}
- return ctx.FinishSuccess(ginresp.JSON(http.StatusOK, msg.FullJSON()))
+ return ginresp.APIError(g, 401, apierr.USER_AUTH_FAILED, "You are not authorized for this action", nil)
}
// DeleteMessage swaggerdoc
//
-// @Summary Delete a single message
-// @Description The user must own the message and request the resource with the ADMIN Key
-// @ID api-messages-delete
-// @Tags API-v2
+// @Summary Delete a single message
+// @Description The user must own the message and request the resource with the ADMIN Key
+// @ID api-messages-delete
+// @Tags API-v2
//
-// @Param mid path int true "MessageID"
+// @Param mid path int true "MessageID"
//
-// @Success 200 {object} models.MessageJSON
-// @Failure 400 {object} ginresp.apiError "supplied values/parameters cannot be parsed / are invalid"
-// @Failure 401 {object} ginresp.apiError "user is not authorized / has missing permissions"
-// @Failure 404 {object} ginresp.apiError "message not found"
-// @Failure 500 {object} ginresp.apiError "internal server error"
+// @Success 200 {object} models.MessageJSON
+// @Failure 400 {object} ginresp.apiError "supplied values/parameters cannot be parsed / are invalid"
+// @Failure 401 {object} ginresp.apiError "user is not authorized / has missing permissions"
+// @Failure 404 {object} ginresp.apiError "message not found"
+// @Failure 500 {object} ginresp.apiError "internal server error"
//
-// @Router /api/v2/messages/{mid} [DELETE]
+// @Router /api/v2/messages/{mid} [DELETE]
func (h APIHandler) DeleteMessage(g *gin.Context) ginresp.HTTPResponse {
type uri struct {
MessageID models.MessageID `uri:"mid" binding:"entityid"`
@@ -1564,7 +1523,7 @@ func (h APIHandler) DeleteMessage(g *gin.Context) ginresp.HTTPResponse {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query message", err)
}
- if !ctx.CheckPermissionMessageReadDirect(msg) {
+ if !ctx.CheckPermissionMessageRead(msg) {
return ginresp.APIError(g, 401, apierr.USER_AUTH_FAILED, "You are not authorized for this action", nil)
}
@@ -1580,3 +1539,284 @@ func (h APIHandler) DeleteMessage(g *gin.Context) ginresp.HTTPResponse {
return ctx.FinishSuccess(ginresp.JSON(http.StatusOK, msg.FullJSON()))
}
+
+// ListUserKeys swaggerdoc
+//
+// @Summary List keys of the user
+// @Description The request must be done with an ADMIN key, the returned keys are without their token.
+// @ID api-tokenkeys-list
+// @Tags API-v2
+//
+// @Param uid path int true "UserID"
+//
+// @Success 200 {object} handler.ListUserKeys.response
+// @Failure 400 {object} ginresp.apiError "supplied values/parameters cannot be parsed / are invalid"
+// @Failure 401 {object} ginresp.apiError "user is not authorized / has missing permissions"
+// @Failure 404 {object} ginresp.apiError "message not found"
+// @Failure 500 {object} ginresp.apiError "internal server error"
+//
+// @Router /api/v2/users/:uid/keys [GET]
+func (h APIHandler) ListUserKeys(g *gin.Context) ginresp.HTTPResponse {
+ type uri struct {
+ UserID models.UserID `uri:"uid" binding:"entityid"`
+ }
+ type response struct {
+ Tokens []models.KeyTokenJSON `json:"tokens"`
+ }
+
+ var u uri
+ ctx, errResp := h.app.StartRequest(g, &u, nil, nil, nil)
+ if errResp != nil {
+ return *errResp
+ }
+ defer ctx.Cancel()
+
+ if permResp := ctx.CheckPermissionUserAdmin(u.UserID); permResp != nil {
+ return *permResp
+ }
+
+ clients, err := h.database.ListKeyTokens(ctx, u.UserID)
+ if err != nil {
+ return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query keys", err)
+ }
+
+ res := langext.ArrMap(clients, func(v models.KeyToken) models.KeyTokenJSON { return v.JSON() })
+
+ return ctx.FinishSuccess(ginresp.JSON(http.StatusOK, response{Tokens: res}))
+}
+
+// GetUserKey swaggerdoc
+//
+// @Summary Get a single key
+// @Description The request must be done with an ADMIN key, the returned key does not include its token.
+// @ID api-tokenkeys-get
+// @Tags API-v2
+//
+// @Param uid path int true "UserID"
+// @Param kid path int true "TokenKeyID"
+//
+// @Success 200 {object} models.KeyTokenJSON
+// @Failure 400 {object} ginresp.apiError "supplied values/parameters cannot be parsed / are invalid"
+// @Failure 401 {object} ginresp.apiError "user is not authorized / has missing permissions"
+// @Failure 404 {object} ginresp.apiError "message not found"
+// @Failure 500 {object} ginresp.apiError "internal server error"
+//
+// @Router /api/v2/users/:uid/keys/:kid [GET]
+func (h APIHandler) GetUserKey(g *gin.Context) ginresp.HTTPResponse {
+ type uri struct {
+ UserID models.UserID `uri:"uid" binding:"entityid"`
+ KeyID models.KeyTokenID `uri:"kid" binding:"entityid"`
+ }
+
+ var u uri
+ ctx, errResp := h.app.StartRequest(g, &u, nil, nil, nil)
+ if errResp != nil {
+ return *errResp
+ }
+ defer ctx.Cancel()
+
+ if permResp := ctx.CheckPermissionUserAdmin(u.UserID); permResp != nil {
+ return *permResp
+ }
+
+ client, err := h.database.GetKeyToken(ctx, u.UserID, u.KeyID)
+ if err == sql.ErrNoRows {
+ return ginresp.APIError(g, 404, apierr.KEY_NOT_FOUND, "Key not found", err)
+ }
+ if err != nil {
+ return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query client", err)
+ }
+
+ return ctx.FinishSuccess(ginresp.JSON(http.StatusOK, client.JSON()))
+}
+
+// UpdateUserKey swaggerdoc
+//
+// @Summary Update a key
+// @ID api-tokenkeys-update
+// @Tags API-v2
+//
+// @Param uid path int true "UserID"
+// @Param kid path int true "TokenKeyID"
+//
+// @Success 200 {object} models.KeyTokenJSON
+// @Failure 400 {object} ginresp.apiError "supplied values/parameters cannot be parsed / are invalid"
+// @Failure 401 {object} ginresp.apiError "user is not authorized / has missing permissions"
+// @Failure 404 {object} ginresp.apiError "message not found"
+// @Failure 500 {object} ginresp.apiError "internal server error"
+//
+// @Router /api/v2/users/:uid/keys/:kid [PATCH]
+func (h APIHandler) UpdateUserKey(g *gin.Context) ginresp.HTTPResponse {
+ type uri struct {
+ UserID models.UserID `uri:"uid" binding:"entityid"`
+ KeyID models.KeyTokenID `uri:"kid" binding:"entityid"`
+ }
+ type body struct {
+ Name *string `json:"name"`
+ AllChannels *bool `json:"all_channels"`
+ Channels *[]models.ChannelID `json:"channels"`
+ Permissions *string `json:"permissions"`
+ }
+
+ var u uri
+ var b body
+ ctx, errResp := h.app.StartRequest(g, &u, nil, &b, nil)
+ if errResp != nil {
+ return *errResp
+ }
+ defer ctx.Cancel()
+
+ if permResp := ctx.CheckPermissionUserAdmin(u.UserID); permResp != nil {
+ return *permResp
+ }
+
+ client, err := h.database.GetKeyToken(ctx, u.UserID, u.KeyID)
+ if err == sql.ErrNoRows {
+ return ginresp.APIError(g, 404, apierr.KEY_NOT_FOUND, "Key not found", err)
+ }
+ if err != nil {
+ return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query client", err)
+ }
+
+ if b.Name != nil {
+ err := h.database.UpdateKeyTokenName(ctx, u.KeyID, *b.Name)
+ if err != nil {
+ return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to update name", err)
+ }
+ }
+
+ if b.Permissions != nil {
+ err := h.database.UpdateKeyTokenPermissions(ctx, u.KeyID, models.ParseTokenPermissionList(*b.Permissions))
+ if err != nil {
+ return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to update permissions", err)
+ }
+ }
+
+ if b.AllChannels != nil {
+ err := h.database.UpdateKeyTokenAllChannels(ctx, u.KeyID, *b.AllChannels)
+ if err != nil {
+ return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to update all_channels", err)
+ }
+ }
+
+ if b.Channels != nil {
+ err := h.database.UpdateKeyTokenChannels(ctx, u.KeyID, *b.Channels)
+ if err != nil {
+ return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to update channels", err)
+ }
+ }
+
+ return ctx.FinishSuccess(ginresp.JSON(http.StatusOK, client.JSON()))
+}
+
+// CreateUserKey swaggerdoc
+//
+// @Summary Create a new key
+// @ID api-tokenkeys-create
+// @Tags API-v2
+//
+// @Param uid path int true "UserID"
+//
+// @Param post_body body handler.CreateUserKey.body false " "
+//
+// @Success 200 {object} models.KeyTokenJSON
+// @Failure 400 {object} ginresp.apiError "supplied values/parameters cannot be parsed / are invalid"
+// @Failure 401 {object} ginresp.apiError "user is not authorized / has missing permissions"
+// @Failure 404 {object} ginresp.apiError "message not found"
+// @Failure 500 {object} ginresp.apiError "internal server error"
+//
+// @Router /api/v2/users/:uid/keys [POST]
+func (h APIHandler) CreateUserKey(g *gin.Context) ginresp.HTTPResponse {
+ type uri struct {
+ UserID models.UserID `uri:"uid" binding:"entityid"`
+ }
+ type body struct {
+ Name string `json:"name" binding:"required"`
+ AllChannels *bool `json:"all_channels" binding:"required"`
+ Channels *[]models.ChannelID `json:"channels" binding:"required"`
+ Permissions *string `json:"permissions" binding:"required"`
+ }
+
+ var u uri
+ var b body
+ ctx, errResp := h.app.StartRequest(g, &u, nil, &b, nil)
+ if errResp != nil {
+ return *errResp
+ }
+ defer ctx.Cancel()
+
+ for _, c := range *b.Channels {
+ if err := c.Valid(); err != nil {
+ return ginresp.APIError(g, 400, apierr.INVALID_BODY_PARAM, "Invalid ChannelID", err)
+ }
+ }
+
+ if permResp := ctx.CheckPermissionUserAdmin(u.UserID); permResp != nil {
+ return *permResp
+ }
+
+ token := h.app.GenerateRandomAuthKey()
+
+ perms := models.ParseTokenPermissionList(*b.Permissions)
+
+ keytok, err := h.database.CreateKeyToken(ctx, b.Name, *ctx.GetPermissionUserID(), *b.AllChannels, *b.Channels, perms, token)
+ if err != nil {
+ return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to create keytoken in db", err)
+ }
+
+ return ctx.FinishSuccess(ginresp.JSON(http.StatusOK, keytok.JSON().WithToken(token)))
+}
+
+// DeleteUserKey swaggerdoc
+//
+// @Summary Delete a key
+// @Description Cannot be used to delete the key used in the request itself
+// @ID api-tokenkeys-delete
+// @Tags API-v2
+//
+// @Param uid path int true "UserID"
+// @Param kid path int true "TokenKeyID"
+//
+// @Success 200 {object} models.KeyTokenJSON
+// @Failure 400 {object} ginresp.apiError "supplied values/parameters cannot be parsed / are invalid"
+// @Failure 401 {object} ginresp.apiError "user is not authorized / has missing permissions"
+// @Failure 404 {object} ginresp.apiError "message not found"
+// @Failure 500 {object} ginresp.apiError "internal server error"
+//
+// @Router /api/v2/users/:uid/keys/:kid [DELETE]
+func (h APIHandler) DeleteUserKey(g *gin.Context) ginresp.HTTPResponse {
+ type uri struct {
+ UserID models.UserID `uri:"uid" binding:"entityid"`
+ KeyID models.KeyTokenID `uri:"kid" binding:"entityid"`
+ }
+
+ var u uri
+ ctx, errResp := h.app.StartRequest(g, &u, nil, nil, nil)
+ if errResp != nil {
+ return *errResp
+ }
+ defer ctx.Cancel()
+
+ if permResp := ctx.CheckPermissionUserAdmin(u.UserID); permResp != nil {
+ return *permResp
+ }
+
+ client, err := h.database.GetKeyToken(ctx, u.UserID, u.KeyID)
+ if err == sql.ErrNoRows {
+ return ginresp.APIError(g, 404, apierr.KEY_NOT_FOUND, "Key not found", err)
+ }
+ if err != nil {
+ return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query client", err)
+ }
+
+ if u.KeyID == *ctx.GetPermissionKeyTokenID() {
+ return ginresp.APIError(g, 404, apierr.CANNOT_SELFDELETE_KEY, "Cannot delete the currently used key", err)
+ }
+
+ err = h.database.DeleteKeyToken(ctx, u.KeyID)
+ if err != nil {
+ return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to delete client", err)
+ }
+
+ return ctx.FinishSuccess(ginresp.JSON(http.StatusOK, client.JSON()))
+}
diff --git a/scnserver/api/handler/common.go b/scnserver/api/handler/common.go
index a171e22..cc6258f 100644
--- a/scnserver/api/handler/common.go
+++ b/scnserver/api/handler/common.go
@@ -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"`
diff --git a/scnserver/api/handler/compat.go b/scnserver/api/handler/compat.go
index 4b4e4a9..f3244ee 100644
--- a/scnserver/api/handler/compat.go
+++ b/scnserver/api/handler/compat.go
@@ -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")
}
diff --git a/scnserver/api/handler/message.go b/scnserver/api/handler/message.go
index 080f5dc..9e0a893 100644
--- a/scnserver/api/handler/message.go
+++ b/scnserver/api/handler/message.go
@@ -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 {
diff --git a/scnserver/api/router.go b/scnserver/api/router.go
index 221cc15..436913b 100644
--- a/scnserver/api/router.go
+++ b/scnserver/api/router.go
@@ -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))
diff --git a/scnserver/config.go b/scnserver/config.go
index 1f6d2ef..ff2444b 100644
--- a/scnserver/config.go
+++ b/scnserver/config.go
@@ -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)
}
diff --git a/scnserver/db/cursortoken/token.go b/scnserver/db/cursortoken/token.go
index 7db0fd0..738c66d 100644
--- a/scnserver/db/cursortoken/token.go
+++ b/scnserver/db/cursortoken/token.go
@@ -8,7 +8,7 @@ import (
"time"
)
-type Mode string
+type Mode string //@enum:type
const (
CTMStart = "START"
diff --git a/scnserver/db/impl/primary/channels.go b/scnserver/db/impl/primary/channels.go
index 38dc24a..8f7768e 100644
--- a/scnserver/db/impl/primary/channels.go
+++ b/scnserver/db/impl/primary/channels.go
@@ -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 {
diff --git a/scnserver/db/impl/primary/keytokens.go b/scnserver/db/impl/primary/keytokens.go
new file mode 100644
index 0000000..b5c6a47
--- /dev/null
+++ b/scnserver/db/impl/primary/keytokens.go
@@ -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
+}
diff --git a/scnserver/db/impl/primary/schema/schema_3.ddl b/scnserver/db/impl/primary/schema/schema_3.ddl
index 707ed68..38527e1 100644
--- a/scnserver/db/impl/primary/schema/schema_3.ddl
+++ b/scnserver/db/impl/primary/schema/schema_3.ddl
@@ -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,
diff --git a/scnserver/db/impl/primary/users.go b/scnserver/db/impl/primary/users.go
index 525a77f..169a842 100644
--- a/scnserver/db/impl/primary/users.go
+++ b/scnserver/db/impl/primary/users.go
@@ -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
-}
diff --git a/scnserver/go.mod b/scnserver/go.mod
index 53b45e5..44a5e7d 100644
--- a/scnserver/go.mod
+++ b/scnserver/go.mod
@@ -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
)
diff --git a/scnserver/go.sum b/scnserver/go.sum
index 6457e10..c82c6ee 100644
--- a/scnserver/go.sum
+++ b/scnserver/go.sum
@@ -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=
diff --git a/scnserver/google/androidPublisher.go b/scnserver/google/androidPublisher.go
index c983221..24e0506 100644
--- a/scnserver/google/androidPublisher.go
+++ b/scnserver/google/androidPublisher.go
@@ -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
diff --git a/scnserver/logic/appcontext.go b/scnserver/logic/appcontext.go
index c738b9f..b91c383 100644
--- a/scnserver/logic/appcontext.go
+++ b/scnserver/logic/appcontext.go
@@ -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,
diff --git a/scnserver/logic/application.go b/scnserver/logic/application.go
index 4fc9721..c96b885 100644
--- a/scnserver/logic/application.go
+++ b/scnserver/logic/application.go
@@ -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
}
diff --git a/scnserver/logic/permissions.go b/scnserver/logic/permissions.go
index 3d6502b..bcae016 100644
--- a/scnserver/logic/permissions.go
+++ b/scnserver/logic/permissions.go
@@ -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)
+ }
}
diff --git a/scnserver/models/channel.go b/scnserver/models/channel.go
index e6a3d37..2e78fc8 100644
--- a/scnserver/models/channel.go
+++ b/scnserver/models/channel.go
@@ -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,
}
diff --git a/scnserver/models/client.go b/scnserver/models/client.go
index 6eee91b..25def19 100644
--- a/scnserver/models/client.go
+++ b/scnserver/models/client.go
@@ -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,
}
diff --git a/scnserver/models/delivery.go b/scnserver/models/delivery.go
index 733c979..ec4c8e3 100644
--- a/scnserver/models/delivery.go
+++ b/scnserver/models/delivery.go
@@ -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,
diff --git a/scnserver/models/enums_gen.go b/scnserver/models/enums_gen.go
new file mode 100644
index 0000000..1e98b87
--- /dev/null
+++ b/scnserver/models/enums_gen.go
@@ -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},
+ }
+}
+
diff --git a/scnserver/models/ids.go b/scnserver/models/ids.go
index 85c0d0d..8854977 100644
--- a/scnserver/models/ids.go
+++ b/scnserver/models/ids.go
@@ -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
+}
diff --git a/scnserver/models/keytoken.go b/scnserver/models/keytoken.go
new file mode 100644
index 0000000..121d7ad
--- /dev/null
+++ b/scnserver/models/keytoken.go
@@ -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
+}
diff --git a/scnserver/models/message.go b/scnserver/models/message.go
index 72fa0e1..a79c50a 100644
--- a/scnserver/models/message.go
+++ b/scnserver/models/message.go
@@ -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,
diff --git a/scnserver/models/permissions.go b/scnserver/models/permissions.go
index 7b709be..745d945 100644
--- a/scnserver/models/permissions.go
+++ b/scnserver/models/permissions.go
@@ -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,
}
}
diff --git a/scnserver/models/requestlog.go b/scnserver/models/requestlog.go
index eae9fff..bd4dc7f 100644
--- a/scnserver/models/requestlog.go
+++ b/scnserver/models/requestlog.go
@@ -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),
}
}
diff --git a/scnserver/models/subscription.go b/scnserver/models/subscription.go
index edacbe9..d51cd84 100644
--- a/scnserver/models/subscription.go
+++ b/scnserver/models/subscription.go
@@ -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,
}
}
diff --git a/scnserver/models/user.go b/scnserver/models/user.go
index 45462d0..fa0ee67 100644
--- a/scnserver/models/user.go
+++ b/scnserver/models/user.go
@@ -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,
diff --git a/scnserver/models/utils.go b/scnserver/models/utils.go
index 051705c..9feb20a 100644
--- a/scnserver/models/utils.go
+++ b/scnserver/models/utils.go
@@ -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)
+}
diff --git a/scnserver/swagger/swagger.json b/scnserver/swagger/swagger.json
index e9cf76c..bd5396e 100644
--- a/scnserver/swagger/swagger.json
+++ b/scnserver/swagger/swagger.json
@@ -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": {
diff --git a/scnserver/swagger/swagger.yaml b/scnserver/swagger/swagger.yaml
index 93e7192..e8eba5a 100644
--- a/scnserver/swagger/swagger.yaml
+++ b/scnserver/swagger/swagger.yaml
@@ -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
diff --git a/scnserver/test/channel_test.go b/scnserver/test/channel_test.go
index 49065f4..1f80e9e 100644
--- a/scnserver/test/channel_test.go
+++ b/scnserver/test/channel_test.go
@@ -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{
diff --git a/scnserver/test/client_test.go b/scnserver/test/client_test.go
index 22bc7ae..378b49f 100644
--- a/scnserver/test/client_test.go
+++ b/scnserver/test/client_test.go
@@ -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 {
diff --git a/scnserver/test/compat_test.go b/scnserver/test/compat_test.go
index be9ad52..c0687a0 100644
--- a/scnserver/test/compat_test.go
+++ b/scnserver/test/compat_test.go
@@ -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"])
diff --git a/scnserver/test/message_test.go b/scnserver/test/message_test.go
index e9fcc5c..7c8bb31 100644
--- a/scnserver/test/message_test.go
+++ b/scnserver/test/message_test.go
@@ -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,
diff --git a/scnserver/test/send_test.go b/scnserver/test/send_test.go
index 551ac46..64e3bac 100644
--- a/scnserver/test/send_test.go
+++ b/scnserver/test/send_test.go
@@ -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×tamp=%d",
+ tt.RequestPostShouldFail(t, baseUrl, fmt.Sprintf("/?key=%s&user_id=%s&title=%s×tamp=%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×tamp=%d",
+ tt.RequestPostShouldFail(t, baseUrl, fmt.Sprintf("/?key=%s&user_id=%s&title=%s×tamp=%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×tamp=%d",
+ tt.RequestPostShouldFail(t, baseUrl, fmt.Sprintf("/?key=%s&user_id=%s&title=%s×tamp=%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×tamp=%d",
+ tt.RequestPostShouldFail(t, baseUrl, fmt.Sprintf("/?key=%s&user_id=%s&title=%s×tamp=%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),
})
}()
}
diff --git a/scnserver/test/user_test.go b/scnserver/test/user_test.go
index 71cf882..49eeddd 100644
--- a/scnserver/test/user_test.go
+++ b/scnserver/test/user_test.go
@@ -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
diff --git a/scnserver/test/util/factory.go b/scnserver/test/util/factory.go
index acb1519..c2d3f8e 100644
--- a/scnserver/test/util/factory.go
+++ b/scnserver/test/util/factory.go
@@ -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
diff --git a/scnserver/website/api.html b/scnserver/website/api.html
index 5f7c39a..7b57029 100644
--- a/scnserver/website/api.html
+++ b/scnserver/website/api.html
@@ -23,7 +23,7 @@
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 \
curl \
--data "user_id={userid}" \
- --data "user_key={userkey}" \
+ --data "key={key}" \
--data "title={message_title}" \
{{config|baseURL}}/
diff --git a/scnserver/website/api_more.html b/scnserver/website/api_more.html
index 4958268..263f64a 100644
--- a/scnserver/website/api_more.html
+++ b/scnserver/website/api_more.html
@@ -26,11 +26,11 @@
To receive them you will need to install the SimpleCloudNotifier app from the play store.
- When you open the app you can click on the account tab to see you unique user_id
and user_key
.
+ When you open the app you can click on the account tab to see you unique user_id
and key
.
These two values are used to identify and authenticate your device so that send messages can be routed to your phone.
- You can at any time generate a new user_key
in the app and invalidate the old one.
+ You can at any time generate a new key
in the app and invalidate the old one.
There is also a web interface 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).
- You need to supply a valid [user_id, user_key]
pair and a title
for your message, all other parameter are optional.
+ You need to supply a valid [user_id, key]
pair and a title
for your message, all other parameter are optional.
@@ -90,7 +90,7 @@
401 (Unauthorized) |
- The user_id was not found or the user_key is wrong |
+ The user_id was not found or the key is wrong |
403 (Forbidden) |
@@ -126,7 +126,7 @@
curl \
--data "user_id={userid}" \
- --data "user_key={userkey}" \
+ --data "key={key}" \
--data "title={message_title}" \
--data "content={message_content}" \
{{config|baseURL}}/
@@ -144,7 +144,7 @@
curl \
--data "user_id={userid}" \
- --data "user_key={userkey}" \
+ --data "key={key}" \
--data "title={message_title}" \
--data "priority={0|1|2}" \
{{config|baseURL}}/
@@ -159,7 +159,7 @@
curl \
--data "user_id={userid}" \
- --data "user_key={userkey}" \
+ --data "key={key}" \
--data "title={message_title}" \
--data "channel={my_channel}" \
{{config|baseURL}}/
@@ -179,7 +179,7 @@
curl \
--data "user_id={userid}" \
- --data "user_key={userkey}" \
+ --data "key={key}" \
--data "title={message_title}" \
--data "msg_id={message_id}" \
{{config|baseURL}}/
@@ -198,7 +198,7 @@
curl \
--data "user_id={userid}" \
- --data "user_key={userkey}" \
+ --data "key={key}" \
--data "title={message_title}" \
--data "timestamp={unix_timestamp}" \
{{config|baseURL}}/
diff --git a/scnserver/website/js/logic.js b/scnserver/website/js/logic.js
index 53b03b3..f8e087e 100644
--- a/scnserver/website/js/logic.js
+++ b/scnserver/website/js/logic.js
@@ -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);
diff --git a/scnserver/website/scn_send.dark.html b/scnserver/website/scn_send.dark.html
index 2200abb..72583cb 100644
--- a/scnserver/website/scn_send.dark.html
+++ b/scnserver/website/scn_send.dark.html
@@ -89,7 +89,7 @@ usage() {\
--write-out "%{http_code}" \
--data "user_id=$user_id" \
- --data "user_key=$user_key" \
+ --data "key=$key" \
--data "title=$title" \
--data "timestamp=$sendtime" \
--data "content=$content" \
diff --git a/scnserver/website/scn_send.light.html b/scnserver/website/scn_send.light.html
index b8a8fa0..707f580 100644
--- a/scnserver/website/scn_send.light.html
+++ b/scnserver/website/scn_send.light.html
@@ -26,7 +26,7 @@
# INSERT YOUR DATA HERE #
################################################################################
user_id="999"
-user_key="??"
+key="??"
################################################################################
usage() {
@@ -89,7 +89,7 @@ content=""
--output /dev/null \
--write-out "%{http_code}" \
--data "user_id=$user_id" \
- --data "user_key=$user_key" \
+ --data "key=$key" \
--data "title=$title" \
--data "timestamp=$sendtime" \
--data "content=$content" \