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" \