Add KeyToken authorization

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

View File

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

View File

@ -49,11 +49,27 @@
- ios purchase verification - 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...) - 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) (then we can replace the old server without switching phone clients)
(still needs switching of the send-script) (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 #### PERSONAL

View File

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

View File

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

View File

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

View File

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

View File

@ -36,7 +36,7 @@ func NewAPIHandler(app *logic.Application) APIHandler {
// //
// @Param post_body body handler.CreateUser.body false " " // @Param post_body body handler.CreateUser.body false " "
// //
// @Success 200 {object} models.UserJSONWithClients // @Success 200 {object} models.UserJSONWithClientsAndKeys
// @Failure 400 {object} ginresp.apiError "supplied values/parameters cannot be parsed / are invalid" // @Failure 400 {object} ginresp.apiError "supplied values/parameters cannot be parsed / are invalid"
// @Failure 500 {object} ginresp.apiError "internal server error" // @Failure 500 {object} ginresp.apiError "internal server error"
// //
@ -111,13 +111,28 @@ func (h APIHandler) CreateUser(g *gin.Context) ginresp.HTTPResponse {
username = langext.Ptr(h.app.NormalizeUsername(*username)) 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 { if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to create user in db", err) 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 { 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 { } else {
err := h.database.DeleteClientsByFCM(ctx, b.FCMToken) err := h.database.DeleteClientsByFCM(ctx, b.FCMToken)
if err != nil { if err != nil {
@ -129,7 +144,7 @@ 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 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)))
} }
} }
@ -205,9 +220,6 @@ func (h APIHandler) UpdateUser(g *gin.Context) ginresp.HTTPResponse {
type body struct { type body struct {
Username *string `json:"username"` Username *string `json:"username"`
ProToken *string `json:"pro_token"` ProToken *string `json:"pro_token"`
RefreshReadKey *bool `json:"read_key"`
RefreshSendKey *bool `json:"send_key"`
RefreshAdminKey *bool `json:"admin_key"`
} }
var u uri 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) user, err := h.database.GetUser(ctx, u.UserID)
if err != nil { if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query (updated) user", err) return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query (updated) user", err)
@ -705,9 +690,8 @@ func (h APIHandler) CreateChannel(g *gin.Context) ginresp.HTTPResponse {
} }
subscribeKey := h.app.GenerateRandomAuthKey() 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 { if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to create channel", err) return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to create channel", err)
} }
@ -756,7 +740,6 @@ func (h APIHandler) UpdateChannel(g *gin.Context) ginresp.HTTPResponse {
} }
type body struct { type body struct {
RefreshSubscribeKey *bool `json:"subscribe_key"` RefreshSubscribeKey *bool `json:"subscribe_key"`
RefreshSendKey *bool `json:"send_key"`
DisplayName *string `json:"display_name"` DisplayName *string `json:"display_name"`
DescriptionName *string `json:"description_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) 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) { if langext.Coalesce(b.RefreshSubscribeKey, false) {
newkey := h.app.GenerateRandomAuthKey() newkey := h.app.GenerateRandomAuthKey()
@ -905,10 +879,6 @@ func (h APIHandler) ListChannelMessages(g *gin.Context) ginresp.HTTPResponse {
pageSize := mathext.Clamp(langext.Coalesce(q.PageSize, 64), 1, maxPageSize) 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) channel, err := h.database.GetChannel(ctx, u.ChannelUserID, u.ChannelID)
if err == sql.ErrNoRows { if err == sql.ErrNoRows {
return ginresp.APIError(g, 404, apierr.CHANNEL_NOT_FOUND, "Channel not found", err) 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) return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query channel", err)
} }
userid := *ctx.GetPermissionUserID() if permResp := ctx.CheckPermissionChanMessagesRead(channel.Channel); permResp != nil {
return *permResp
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)
} }
tok, err := ct.Decode(langext.Coalesce(q.NextPageToken, "")) tok, err := ct.Decode(langext.Coalesce(q.NextPageToken, ""))
@ -1413,7 +1374,7 @@ func (h APIHandler) ListMessages(g *gin.Context) ginresp.HTTPResponse {
pageSize := mathext.Clamp(langext.Coalesce(q.PageSize, 64), 1, maxPageSize) pageSize := mathext.Clamp(langext.Coalesce(q.PageSize, 64), 1, maxPageSize)
if permResp := ctx.CheckPermissionRead(); permResp != nil { if permResp := ctx.CheckPermissionSelfAllMessagesRead(); permResp != nil {
return *permResp return *permResp
} }
@ -1494,12 +1455,14 @@ func (h APIHandler) GetMessage(g *gin.Context) ginresp.HTTPResponse {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query message", err) 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) // 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 // or we subscribe (+confirmed) to the channel and have read/admin key
if uid := ctx.GetPermissionUserID(); uid != nil && ctx.IsPermissionUserRead() { if ctx.CheckPermissionMessageRead(msg) {
return ctx.FinishSuccess(ginresp.JSON(http.StatusOK, msg.FullJSON()))
}
if uid := ctx.GetPermissionUserID(); uid != nil && ctx.CheckPermissionUserRead(*uid) == nil {
sub, err := h.database.GetSubscriptionBySubscriber(ctx, *uid, msg.ChannelID) sub, err := h.database.GetSubscriptionBySubscriber(ctx, *uid, msg.ChannelID)
if err != nil { if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query subscription", err) return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query subscription", err)
@ -1512,18 +1475,14 @@ func (h APIHandler) GetMessage(g *gin.Context) ginresp.HTTPResponse {
// sub not confirmed // sub not confirmed
return ginresp.APIError(g, 401, apierr.USER_AUTH_FAILED, "You are not authorized for this action", nil) return ginresp.APIError(g, 401, apierr.USER_AUTH_FAILED, "You are not authorized for this action", nil)
} }
// => perm okay // => perm okay
} else {
// auth-key is not set or not a user:x variant
return ginresp.APIError(g, 401, apierr.USER_AUTH_FAILED, "You are not authorized for this action", nil)
}
}
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 // DeleteMessage swaggerdoc
// //
// @Summary Delete a single message // @Summary Delete a single message
@ -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) 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) 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())) 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()))
}

View File

@ -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) 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 { if errResp != nil {
return *errResp return *errResp
} else { } else {
@ -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() adminKey := h.app.GenerateRandomAuthKey()
err := h.database.ClearFCMTokens(ctx, *data.FCMToken) 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 { if err != nil {
return ginresp.CompatAPIError(0, "Failed to create user in db") 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") _, err = h.database.CreateClient(ctx, user.UserID, models.ClientTypeAndroid, *data.FCMToken, "compat", "compat")
if err != nil { if err != nil {
return ginresp.CompatAPIError(0, "Failed to create client in db") 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, Success: true,
Message: "New user registered", Message: "New user registered",
UserID: oldid, UserID: oldid,
UserKey: user.AdminKey, UserKey: adminKey,
QuotaUsed: user.QuotaUsedToday(), QuotaUsed: user.QuotaUsedToday(),
QuotaMax: user.QuotaPerDay(), QuotaMax: user.QuotaPerDay(),
IsPro: user.IsPro, IsPro: user.IsPro,
@ -305,7 +308,14 @@ func (h CompatHandler) Info(g *gin.Context) ginresp.HTTPResponse {
return ginresp.CompatAPIError(0, "Failed to query user") 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") return ginresp.CompatAPIError(204, "Authentification failed")
} }
@ -320,7 +330,7 @@ func (h CompatHandler) Info(g *gin.Context) ginresp.HTTPResponse {
Success: true, Success: true,
Message: "ok", Message: "ok",
UserID: *data.UserID, UserID: *data.UserID,
UserKey: user.AdminKey, UserKey: keytok.Token,
QuotaUsed: user.QuotaUsedToday(), QuotaUsed: user.QuotaUsedToday(),
QuotaMax: user.QuotaPerDay(), QuotaMax: user.QuotaPerDay(),
IsPro: langext.Conditional(user.IsPro, 1, 0), IsPro: langext.Conditional(user.IsPro, 1, 0),
@ -398,7 +408,14 @@ func (h CompatHandler) Ack(g *gin.Context) ginresp.HTTPResponse {
return ginresp.CompatAPIError(0, "Failed to query user") 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") return ginresp.CompatAPIError(204, "Authentification failed")
} }
@ -493,7 +510,14 @@ func (h CompatHandler) Requery(g *gin.Context) ginresp.HTTPResponse {
return ginresp.CompatAPIError(0, "Failed to query user") 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") return ginresp.CompatAPIError(204, "Authentification failed")
} }
@ -603,7 +627,14 @@ func (h CompatHandler) Update(g *gin.Context) ginresp.HTTPResponse {
return ginresp.CompatAPIError(0, "Failed to query user") 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") return ginresp.CompatAPIError(204, "Authentification failed")
} }
@ -613,10 +644,13 @@ func (h CompatHandler) Update(g *gin.Context) ginresp.HTTPResponse {
} }
newAdminKey := h.app.GenerateRandomAuthKey() 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 { if err != nil {
return ginresp.CompatAPIError(0, "Failed to update keys") return ginresp.CompatAPIError(0, "Failed to update keys")
} }
@ -648,7 +682,7 @@ func (h CompatHandler) Update(g *gin.Context) ginresp.HTTPResponse {
Success: true, Success: true,
Message: "user updated", Message: "user updated",
UserID: *data.UserID, UserID: *data.UserID,
UserKey: user.AdminKey, UserKey: newAdminKey,
QuotaUsed: user.QuotaUsedToday(), QuotaUsed: user.QuotaUsedToday(),
QuotaMax: user.QuotaPerDay(), QuotaMax: user.QuotaPerDay(),
IsPro: langext.Conditional(user.IsPro, 1, 0), IsPro: langext.Conditional(user.IsPro, 1, 0),
@ -723,7 +757,14 @@ func (h CompatHandler) Expand(g *gin.Context) ginresp.HTTPResponse {
return ginresp.CompatAPIError(0, "Failed to query user") 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") return ginresp.CompatAPIError(204, "Authentification failed")
} }
@ -835,7 +876,14 @@ func (h CompatHandler) Upgrade(g *gin.Context) ginresp.HTTPResponse {
return ginresp.CompatAPIError(0, "Failed to query user") 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") return ginresp.CompatAPIError(204, "Authentification failed")
} }

View File

@ -59,9 +59,8 @@ func NewMessageHandler(app *logic.Application) MessageHandler {
func (h MessageHandler) SendMessage(g *gin.Context) ginresp.HTTPResponse { func (h MessageHandler) SendMessage(g *gin.Context) ginresp.HTTPResponse {
type combined struct { type combined struct {
UserID *models.UserID `json:"user_id" form:"user_id" example:"7725" ` 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" ` 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" ` Title *string `json:"title" form:"title" example:"Hello World" `
Content *string `json:"content" form:"content" example:"This is a message" ` Content *string `json:"content" form:"content" example:"This is a message" `
Priority *int `json:"priority" form:"priority" example:"1" enums:"0,1,2" ` 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 // query has highest prio, then form, then json
data := dataext.ObjectMerge(dataext.ObjectMerge(b, f), q) data := dataext.ObjectMerge(dataext.ObjectMerge(b, f), q)
okResp, errResp := h.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 { if errResp != nil {
return *errResp return *errResp
} else { } 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 { if Title != nil {
Title = langext.Ptr(strings.TrimSpace(*Title)) Title = langext.Ptr(strings.TrimSpace(*Title))
} }
@ -140,8 +139,8 @@ func (h MessageHandler) sendMessageInternal(g *gin.Context, ctx *logic.AppContex
if UserID == nil { if UserID == nil {
return nil, langext.Ptr(ginresp.SendAPIError(g, 400, apierr.MISSING_UID, hl.USER_ID, "Missing parameter [[user_id]]", nil)) return nil, langext.Ptr(ginresp.SendAPIError(g, 400, apierr.MISSING_UID, hl.USER_ID, "Missing parameter [[user_id]]", nil))
} }
if UserKey == nil { if Key == nil {
return nil, langext.Ptr(ginresp.SendAPIError(g, 400, apierr.MISSING_TOK, hl.USER_KEY, "Missing parameter [[user_token]]", nil)) return nil, langext.Ptr(ginresp.SendAPIError(g, 400, apierr.MISSING_TOK, hl.USER_KEY, "Missing parameter [[key]]", nil))
} }
if Title == nil { if Title == nil {
return nil, langext.Ptr(ginresp.SendAPIError(g, 400, apierr.MISSING_TITLE, hl.TITLE, "Missing parameter [[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)) 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 channel, err := h.app.GetOrCreateChannel(ctx, *UserID, channelDisplayName, channelInternalName)
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 { if err != nil {
return nil, langext.Ptr(ginresp.SendAPIError(g, 500, apierr.DATABASE_ERROR, hl.NONE, "Failed to query/create (owned) channel", err)) return nil, langext.Ptr(ginresp.SendAPIError(g, 500, apierr.DATABASE_ERROR, hl.NONE, "Failed to query/create (owned) channel", err))
} }
}
selfChanAdmin := *UserID == channel.OwnerUserID && *UserKey == user.AdminKey keytok, permResp := ctx.CheckPermissionSend(channel, *Key)
selfChanSend := *UserID == channel.OwnerUserID && *UserKey == user.SendKey if permResp != nil {
forgChanSend := *UserID != channel.OwnerUserID && ChanKey != nil && *ChanKey == channel.SendKey
if !selfChanAdmin && !selfChanSend && !forgChanSend {
return nil, langext.Ptr(ginresp.SendAPIError(g, 401, apierr.USER_AUTH_FAILED, hl.USER_KEY, "You are not authorized for this action", 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)) 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 { for _, sub := range subscriptions {
clients, err := h.database.ListClients(ctx, sub.SubscriberUserID) clients, err := h.database.ListClients(ctx, sub.SubscriberUserID)
if err != nil { if err != nil {

View File

@ -127,6 +127,12 @@ func (r *Router) Init(e *gin.Engine) error {
apiv2.GET("/users/:uid", r.Wrap(r.apiHandler.GetUser)) apiv2.GET("/users/:uid", r.Wrap(r.apiHandler.GetUser))
apiv2.PATCH("/users/:uid", r.Wrap(r.apiHandler.UpdateUser)) 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", r.Wrap(r.apiHandler.ListClients))
apiv2.GET("/users/:uid/clients/:cid", r.Wrap(r.apiHandler.GetClient)) apiv2.GET("/users/:uid/clients/:cid", r.Wrap(r.apiHandler.GetClient))
apiv2.POST("/users/:uid/clients", r.Wrap(r.apiHandler.AddClient)) apiv2.POST("/users/:uid/clients", r.Wrap(r.apiHandler.AddClient))

View File

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

View File

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

View File

@ -32,31 +32,6 @@ func (db *Database) GetChannelByName(ctx TxContext, userid models.UserID, chanNa
return &channel, nil 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) { func (db *Database) GetChannelByID(ctx TxContext, chanid models.ChannelID) (*models.Channel, error) {
tx, err := ctx.GetOrCreateTransaction(db) tx, err := ctx.GetOrCreateTransaction(db)
if err != nil { if err != nil {
@ -81,7 +56,7 @@ func (db *Database) GetChannelByID(ctx TxContext, chanid models.ChannelID) (*mod
return &channel, nil 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) tx, err := ctx.GetOrCreateTransaction(db)
if err != nil { if err != nil {
return models.Channel{}, err return models.Channel{}, err
@ -91,14 +66,13 @@ func (db *Database) CreateChannel(ctx TxContext, userid models.UserID, dispName
channelid := models.NewChannelID() 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{ _, 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, "cid": channelid,
"ouid": userid, "ouid": userid,
"dnam": dispName, "dnam": dispName,
"inam": intName, "inam": intName,
"hnam": nil, "hnam": nil,
"subkey": subscribeKey, "subkey": subscribeKey,
"sendkey": sendKey,
"ts": time2DB(now), "ts": time2DB(now),
}) })
if err != nil { if err != nil {
@ -111,7 +85,6 @@ func (db *Database) CreateChannel(ctx TxContext, userid models.UserID, dispName
DisplayName: dispName, DisplayName: dispName,
InternalName: intName, InternalName: intName,
SubscribeKey: subscribeKey, SubscribeKey: subscribeKey,
SendKey: sendKey,
TimestampCreated: now, TimestampCreated: now,
TimestampLastSent: nil, TimestampLastSent: nil,
MessagesSent: 0, MessagesSent: 0,
@ -244,23 +217,6 @@ func (db *Database) IncChannelMessageCounter(ctx TxContext, channel models.Chann
return nil 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 { func (db *Database) UpdateChannelSubscribeKey(ctx TxContext, channelid models.ChannelID, newkey string) error {
tx, err := ctx.GetOrCreateTransaction(db) tx, err := ctx.GetOrCreateTransaction(db)
if err != nil { if err != nil {

View File

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

View File

@ -4,10 +4,6 @@ CREATE TABLE users
username TEXT NULL DEFAULT NULL, 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_created INTEGER NOT NULL,
timestamp_lastread INTEGER NULL DEFAULT NULL, timestamp_lastread INTEGER NULL DEFAULT NULL,
timestamp_lastsent 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 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 CREATE TABLE clients
( (
client_id TEXT NOT NULL, client_id TEXT NOT NULL,
@ -55,7 +74,6 @@ CREATE TABLE channels
description_name TEXT NULL, description_name TEXT NULL,
subscribe_key TEXT NOT NULL, subscribe_key TEXT NOT NULL,
send_key TEXT NOT NULL,
timestamp_created INTEGER NOT NULL, timestamp_created INTEGER NOT NULL,
timestamp_lastsent INTEGER NULL DEFAULT NULL, timestamp_lastsent INTEGER NULL DEFAULT NULL,

View File

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

View File

@ -9,7 +9,7 @@ require (
github.com/jmoiron/sqlx v1.3.5 github.com/jmoiron/sqlx v1.3.5
github.com/mattn/go-sqlite3 v1.14.16 github.com/mattn/go-sqlite3 v1.14.16
github.com/rs/zerolog v1.28.0 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 gopkg.in/loremipsum.v1 v1.1.0
) )

View File

@ -81,6 +81,8 @@ gogs.mikescher.com/BlackForestBytes/goext v0.0.58 h1:W53yfHhpFQS13zgtzCjfJQ42WG0
gogs.mikescher.com/BlackForestBytes/goext v0.0.58/go.mod h1:ZEXyKUr8t0EKdPN1FYdk0klY7N8OwXxipGE9lWgpVE8= gogs.mikescher.com/BlackForestBytes/goext v0.0.58/go.mod h1:ZEXyKUr8t0EKdPN1FYdk0klY7N8OwXxipGE9lWgpVE8=
gogs.mikescher.com/BlackForestBytes/goext v0.0.59 h1:3bHSjqgty9yp0EIyqwGAb06ZS7bLvm806zRj6j+WOEE= 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.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.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 h1:UVQgzMY87xqpKNgb+kDsll2Igd33HszWHFLmpaRMq/8=
golang.org/x/crypto v0.4.0/go.mod h1:3quD/ATkf6oY+rnes5c3ExXTbLc8mueNue5/DoinL80= golang.org/x/crypto v0.4.0/go.mod h1:3quD/ATkf6oY+rnes5c3ExXTbLc8mueNue5/DoinL80=

View File

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

View File

@ -14,6 +14,7 @@ import (
) )
type AppContext struct { type AppContext struct {
app *Application
inner context.Context inner context.Context
cancelFunc context.CancelFunc cancelFunc context.CancelFunc
cancelled bool cancelled bool
@ -22,8 +23,9 @@ type AppContext struct {
ginContext *gin.Context 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{ return &AppContext{
app: app,
inner: innerCtx, inner: innerCtx,
cancelFunc: cancelFn, cancelFunc: cancelFn,
cancelled: false, cancelled: false,

View File

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

View File

@ -4,94 +4,116 @@ import (
"blackforestbytes.com/simplecloudnotifier/api/apierr" "blackforestbytes.com/simplecloudnotifier/api/apierr"
"blackforestbytes.com/simplecloudnotifier/api/ginresp" "blackforestbytes.com/simplecloudnotifier/api/ginresp"
"blackforestbytes.com/simplecloudnotifier/models" "blackforestbytes.com/simplecloudnotifier/models"
"database/sql"
"gogs.mikescher.com/BlackForestBytes/goext/langext" "gogs.mikescher.com/BlackForestBytes/goext/langext"
) )
func (ac *AppContext) CheckPermissionUserRead(userid models.UserID) *ginresp.HTTPResponse { func (ac *AppContext) CheckPermissionUserRead(userid models.UserID) *ginresp.HTTPResponse {
p := ac.permissions p := ac.permissions
if p.UserID != nil && *p.UserID == userid && p.KeyType == models.PermKeyTypeUserRead { if p.Token != nil && p.Token.IsUserRead(userid) {
return nil
}
if p.UserID != nil && *p.UserID == userid && p.KeyType == models.PermKeyTypeUserAdmin {
return nil return nil
} }
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) CheckPermissionRead() *ginresp.HTTPResponse { func (ac *AppContext) CheckPermissionSelfAllMessagesRead() *ginresp.HTTPResponse {
p := ac.permissions p := ac.permissions
if p.UserID != nil && p.KeyType == models.PermKeyTypeUserRead { if p.Token != nil && p.Token.IsAllMessagesRead(p.Token.OwnerUserID) {
return nil 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 nil
} }
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) 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 { func (ac *AppContext) CheckPermissionUserAdmin(userid models.UserID) *ginresp.HTTPResponse {
p := ac.permissions 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 nil
} }
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) CheckPermissionSend() *ginresp.HTTPResponse { 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 keytok == nil {
return nil, 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) CheckPermissionMessageRead(msg models.Message) bool {
p := ac.permissions p := ac.permissions
if p.UserID != nil && p.KeyType == models.PermKeyTypeUserSend { if p.Token != nil && p.Token.IsChannelMessagesRead(msg.ChannelID) {
return nil
}
if p.UserID != nil && p.KeyType == models.PermKeyTypeUserAdmin {
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) CheckPermissionAny() *ginresp.HTTPResponse {
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 {
return true return true
} }
return false 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 { func (ac *AppContext) GetPermissionUserID() *models.UserID {
if ac.permissions.UserID == nil { if ac.permissions.Token == nil {
return nil return nil
} else { } else {
return langext.Ptr(*ac.permissions.UserID) return langext.Ptr(ac.permissions.Token.OwnerUserID)
} }
} }
func (ac *AppContext) IsPermissionUserRead() bool { func (ac *AppContext) GetPermissionKeyTokenID() *models.KeyTokenID {
p := ac.permissions if ac.permissions.Token == nil {
return p.KeyType == models.PermKeyTypeUserRead || p.KeyType == models.PermKeyTypeUserAdmin return nil
} else {
return langext.Ptr(ac.permissions.Token.KeyTokenID)
} }
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
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,22 +1,11 @@
package models 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 { type PermissionSet struct {
UserID *UserID Token *KeyToken // KeyToken.Permissions
KeyType PermKeyType
} }
func NewEmptyPermissions() PermissionSet { func NewEmptyPermissions() PermissionSet {
return PermissionSet{ return PermissionSet{
UserID: nil, Token: nil,
KeyType: PermKeyTypeNone,
} }
} }

View File

@ -18,6 +18,7 @@ type RequestLog struct {
RequestBodySize int64 RequestBodySize int64
RequestContentType string RequestContentType string
RemoteIP string RemoteIP string
TokenID *KeyTokenID
UserID *UserID UserID *UserID
Permissions *string Permissions *string
ResponseStatuscode *int64 ResponseStatuscode *int64
@ -44,6 +45,7 @@ func (c RequestLog) JSON() RequestLogJSON {
RequestBodySize: c.RequestBodySize, RequestBodySize: c.RequestBodySize,
RequestContentType: c.RequestContentType, RequestContentType: c.RequestContentType,
RemoteIP: c.RemoteIP, RemoteIP: c.RemoteIP,
TokenID: c.TokenID,
UserID: c.UserID, UserID: c.UserID,
Permissions: c.Permissions, Permissions: c.Permissions,
ResponseStatuscode: c.ResponseStatuscode, ResponseStatuscode: c.ResponseStatuscode,
@ -71,6 +73,7 @@ func (c RequestLog) DB() RequestLogDB {
RequestBodySize: c.RequestBodySize, RequestBodySize: c.RequestBodySize,
RequestContentType: c.RequestContentType, RequestContentType: c.RequestContentType,
RemoteIP: c.RemoteIP, RemoteIP: c.RemoteIP,
TokenID: c.TokenID,
UserID: c.UserID, UserID: c.UserID,
Permissions: c.Permissions, Permissions: c.Permissions,
ResponseStatuscode: c.ResponseStatuscode, ResponseStatuscode: c.ResponseStatuscode,
@ -97,6 +100,7 @@ type RequestLogJSON struct {
RequestBodySize int64 `json:"request_body_size"` RequestBodySize int64 `json:"request_body_size"`
RequestContentType string `json:"request_content_type"` RequestContentType string `json:"request_content_type"`
RemoteIP string `json:"remote_ip"` RemoteIP string `json:"remote_ip"`
TokenID *KeyTokenID `json:"token_id"`
UserID *UserID `json:"userid"` UserID *UserID `json:"userid"`
Permissions *string `json:"permissions"` Permissions *string `json:"permissions"`
ResponseStatuscode *int64 `json:"response_statuscode"` ResponseStatuscode *int64 `json:"response_statuscode"`
@ -122,6 +126,7 @@ type RequestLogDB struct {
RequestBodySize int64 `db:"request_body_size"` RequestBodySize int64 `db:"request_body_size"`
RequestContentType string `db:"request_content_type"` RequestContentType string `db:"request_content_type"`
RemoteIP string `db:"remote_ip"` RemoteIP string `db:"remote_ip"`
TokenID *KeyTokenID `db:"token_id"`
UserID *UserID `db:"userid"` UserID *UserID `db:"userid"`
Permissions *string `db:"permissions"` Permissions *string `db:"permissions"`
ResponseStatuscode *int64 `db:"response_statuscode"` ResponseStatuscode *int64 `db:"response_statuscode"`
@ -158,9 +163,9 @@ func (c RequestLogDB) Model() RequestLog {
Panicked: c.Panicked != 0, Panicked: c.Panicked != 0,
PanicStr: c.PanicStr, PanicStr: c.PanicStr,
ProcessingTime: timeext.FromMilliseconds(c.ProcessingTime), ProcessingTime: timeext.FromMilliseconds(c.ProcessingTime),
TimestampCreated: time.UnixMilli(c.TimestampCreated), TimestampCreated: timeFromMilli(c.TimestampCreated),
TimestampStart: time.UnixMilli(c.TimestampStart), TimestampStart: timeFromMilli(c.TimestampStart),
TimestampFinish: time.UnixMilli(c.TimestampFinish), TimestampFinish: timeFromMilli(c.TimestampFinish),
} }
} }

View File

@ -7,6 +7,13 @@ import (
"time" "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 { type Subscription struct {
SubscriptionID SubscriptionID SubscriptionID SubscriptionID
SubscriberUserID UserID SubscriberUserID UserID
@ -56,7 +63,7 @@ func (s SubscriptionDB) Model() Subscription {
ChannelOwnerUserID: s.ChannelOwnerUserID, ChannelOwnerUserID: s.ChannelOwnerUserID,
ChannelID: s.ChannelID, ChannelID: s.ChannelID,
ChannelInternalName: s.ChannelInternalName, ChannelInternalName: s.ChannelInternalName,
TimestampCreated: time.UnixMilli(s.TimestampCreated), TimestampCreated: timeFromMilli(s.TimestampCreated),
Confirmed: s.Confirmed != 0, Confirmed: s.Confirmed != 0,
} }
} }

View File

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

View File

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

View File

@ -17,12 +17,6 @@
], ],
"summary": "Send a new message", "summary": "Send a new message",
"parameters": [ "parameters": [
{
"type": "string",
"example": "qhnUbKcLgp6tg",
"name": "chan_key",
"in": "query"
},
{ {
"type": "string", "type": "string",
"example": "test", "example": "test",
@ -35,6 +29,12 @@
"name": "content", "name": "content",
"in": "query" "in": "query"
}, },
{
"type": "string",
"example": "P3TNH8mvv14fm",
"name": "key",
"in": "query"
},
{ {
"type": "string", "type": "string",
"example": "db8b0e6a-a08c-4646", "example": "db8b0e6a-a08c-4646",
@ -76,12 +76,6 @@
"name": "user_id", "name": "user_id",
"in": "query" "in": "query"
}, },
{
"type": "string",
"example": "P3TNH8mvv14fm",
"name": "user_key",
"in": "query"
},
{ {
"description": " ", "description": " ",
"name": "post_body", "name": "post_body",
@ -90,12 +84,6 @@
"$ref": "#/definitions/handler.SendMessage.combined" "$ref": "#/definitions/handler.SendMessage.combined"
} }
}, },
{
"type": "string",
"example": "qhnUbKcLgp6tg",
"name": "chan_key",
"in": "formData"
},
{ {
"type": "string", "type": "string",
"example": "test", "example": "test",
@ -108,6 +96,12 @@
"name": "content", "name": "content",
"in": "formData" "in": "formData"
}, },
{
"type": "string",
"example": "P3TNH8mvv14fm",
"name": "key",
"in": "formData"
},
{ {
"type": "string", "type": "string",
"example": "db8b0e6a-a08c-4646", "example": "db8b0e6a-a08c-4646",
@ -148,12 +142,6 @@
"example": "7725", "example": "7725",
"name": "user_id", "name": "user_id",
"in": "formData" "in": "formData"
},
{
"type": "string",
"example": "P3TNH8mvv14fm",
"name": "user_key",
"in": "formData"
} }
], ],
"responses": { "responses": {
@ -1034,7 +1022,7 @@
"200": { "200": {
"description": "OK", "description": "OK",
"schema": { "schema": {
"$ref": "#/definitions/models.UserJSONWithClients" "$ref": "#/definitions/models.UserJSONWithClientsAndKeys"
} }
}, },
"400": { "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}": { "/api/v2/users/{uid}": {
"get": { "get": {
"tags": [ "tags": [
@ -2081,12 +2345,6 @@
], ],
"summary": "Send a new message", "summary": "Send a new message",
"parameters": [ "parameters": [
{
"type": "string",
"example": "qhnUbKcLgp6tg",
"name": "chan_key",
"in": "query"
},
{ {
"type": "string", "type": "string",
"example": "test", "example": "test",
@ -2099,6 +2357,12 @@
"name": "content", "name": "content",
"in": "query" "in": "query"
}, },
{
"type": "string",
"example": "P3TNH8mvv14fm",
"name": "key",
"in": "query"
},
{ {
"type": "string", "type": "string",
"example": "db8b0e6a-a08c-4646", "example": "db8b0e6a-a08c-4646",
@ -2140,12 +2404,6 @@
"name": "user_id", "name": "user_id",
"in": "query" "in": "query"
}, },
{
"type": "string",
"example": "P3TNH8mvv14fm",
"name": "user_key",
"in": "query"
},
{ {
"description": " ", "description": " ",
"name": "post_body", "name": "post_body",
@ -2154,12 +2412,6 @@
"$ref": "#/definitions/handler.SendMessage.combined" "$ref": "#/definitions/handler.SendMessage.combined"
} }
}, },
{
"type": "string",
"example": "qhnUbKcLgp6tg",
"name": "chan_key",
"in": "formData"
},
{ {
"type": "string", "type": "string",
"example": "test", "example": "test",
@ -2172,6 +2424,12 @@
"name": "content", "name": "content",
"in": "formData" "in": "formData"
}, },
{
"type": "string",
"example": "P3TNH8mvv14fm",
"name": "key",
"in": "formData"
},
{ {
"type": "string", "type": "string",
"example": "db8b0e6a-a08c-4646", "example": "db8b0e6a-a08c-4646",
@ -2212,12 +2470,6 @@
"example": "7725", "example": "7725",
"name": "user_id", "name": "user_id",
"in": "formData" "in": "formData"
},
{
"type": "string",
"example": "P3TNH8mvv14fm",
"name": "user_key",
"in": "formData"
} }
], ],
"responses": { "responses": {
@ -2370,6 +2622,97 @@
} }
}, },
"definitions": { "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": { "ginresp.apiError": {
"type": "object", "type": "object",
"properties": { "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": { "handler.DatabaseTest.response": {
"type": "object", "type": "object",
"properties": { "properties": {
@ -2630,6 +2999,17 @@
} }
} }
}, },
"handler.ListUserKeys.response": {
"type": "object",
"properties": {
"tokens": {
"type": "array",
"items": {
"$ref": "#/definitions/models.KeyTokenJSON"
}
}
}
},
"handler.ListUserSubscriptions.response": { "handler.ListUserSubscriptions.response": {
"type": "object", "type": "object",
"properties": { "properties": {
@ -2690,10 +3070,6 @@
"handler.SendMessage.combined": { "handler.SendMessage.combined": {
"type": "object", "type": "object",
"properties": { "properties": {
"chan_key": {
"type": "string",
"example": "qhnUbKcLgp6tg"
},
"channel": { "channel": {
"type": "string", "type": "string",
"example": "test" "example": "test"
@ -2702,6 +3078,10 @@
"type": "string", "type": "string",
"example": "This is a message" "example": "This is a message"
}, },
"key": {
"type": "string",
"example": "P3TNH8mvv14fm"
},
"msg_id": { "msg_id": {
"type": "string", "type": "string",
"example": "db8b0e6a-a08c-4646" "example": "db8b0e6a-a08c-4646"
@ -2730,10 +3110,6 @@
"user_id": { "user_id": {
"type": "string", "type": "string",
"example": "7725" "example": "7725"
},
"user_key": {
"type": "string",
"example": "P3TNH8mvv14fm"
} }
} }
}, },
@ -2744,7 +3120,7 @@
"type": "integer" "type": "integer"
}, },
"error": { "error": {
"type": "integer" "$ref": "#/definitions/apierr.APIError"
}, },
"is_pro": { "is_pro": {
"type": "boolean" "type": "boolean"
@ -2779,7 +3155,7 @@
"type": "integer" "type": "integer"
}, },
"error": { "error": {
"type": "integer" "$ref": "#/definitions/apierr.APIError"
}, },
"is_pro": { "is_pro": {
"type": "boolean" "type": "boolean"
@ -2936,10 +3312,6 @@
"owner_user_id": { "owner_user_id": {
"type": "string" "type": "string"
}, },
"send_key": {
"description": "can be nil, depending on endpoint",
"type": "string"
},
"subscribe_key": { "subscribe_key": {
"description": "can be nil, depending on endpoint", "description": "can be nil, depending on endpoint",
"type": "string" "type": "string"
@ -2974,13 +3346,24 @@
"type": "string" "type": "string"
}, },
"type": { "type": {
"type": "string" "$ref": "#/definitions/models.ClientType"
}, },
"user_id": { "user_id": {
"type": "string" "type": "string"
} }
} }
}, },
"models.ClientType": {
"type": "string",
"enum": [
"ANDROID",
"IOS"
],
"x-enum-varnames": [
"ClientTypeAndroid",
"ClientTypeIOS"
]
},
"models.CompatMessage": { "models.CompatMessage": {
"type": "object", "type": "object",
"properties": { "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": { "models.MessageJSON": {
"type": "object", "type": "object",
"properties": { "properties": {
@ -3080,9 +3498,6 @@
"models.UserJSON": { "models.UserJSON": {
"type": "object", "type": "object",
"properties": { "properties": {
"admin_key": {
"type": "string"
},
"default_channel": { "default_channel": {
"type": "string" "type": "string"
}, },
@ -3101,12 +3516,6 @@
"quota_used": { "quota_used": {
"type": "integer" "type": "integer"
}, },
"read_key": {
"type": "string"
},
"send_key": {
"type": "string"
},
"timestamp_created": { "timestamp_created": {
"type": "string" "type": "string"
}, },
@ -3124,7 +3533,7 @@
} }
} }
}, },
"models.UserJSONWithClients": { "models.UserJSONWithClientsAndKeys": {
"type": "object", "type": "object",
"properties": { "properties": {
"admin_key": { "admin_key": {

View File

@ -1,5 +1,93 @@
basePath: / basePath: /
definitions: 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: ginresp.apiError:
properties: properties:
errhighlight: errhighlight:
@ -80,6 +168,24 @@ definitions:
username: username:
type: string type: string
type: object 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: handler.DatabaseTest.response:
properties: properties:
libVersion: libVersion:
@ -169,6 +275,13 @@ definitions:
page_size: page_size:
type: integer type: integer
type: object type: object
handler.ListUserKeys.response:
properties:
tokens:
items:
$ref: '#/definitions/models.KeyTokenJSON'
type: array
type: object
handler.ListUserSubscriptions.response: handler.ListUserSubscriptions.response:
properties: properties:
subscriptions: subscriptions:
@ -208,15 +321,15 @@ definitions:
type: object type: object
handler.SendMessage.combined: handler.SendMessage.combined:
properties: properties:
chan_key:
example: qhnUbKcLgp6tg
type: string
channel: channel:
example: test example: test
type: string type: string
content: content:
example: This is a message example: This is a message
type: string type: string
key:
example: P3TNH8mvv14fm
type: string
msg_id: msg_id:
example: db8b0e6a-a08c-4646 example: db8b0e6a-a08c-4646
type: string type: string
@ -239,16 +352,13 @@ definitions:
user_id: user_id:
example: "7725" example: "7725"
type: string type: string
user_key:
example: P3TNH8mvv14fm
type: string
type: object type: object
handler.SendMessage.response: handler.SendMessage.response:
properties: properties:
errhighlight: errhighlight:
type: integer type: integer
error: error:
type: integer $ref: '#/definitions/apierr.APIError'
is_pro: is_pro:
type: boolean type: boolean
message: message:
@ -271,7 +381,7 @@ definitions:
errhighlight: errhighlight:
type: integer type: integer
error: error:
type: integer $ref: '#/definitions/apierr.APIError'
is_pro: is_pro:
type: boolean type: boolean
message: message:
@ -373,9 +483,6 @@ definitions:
type: integer type: integer
owner_user_id: owner_user_id:
type: string type: string
send_key:
description: can be nil, depending on endpoint
type: string
subscribe_key: subscribe_key:
description: can be nil, depending on endpoint description: can be nil, depending on endpoint
type: string type: string
@ -399,10 +506,18 @@ definitions:
timestamp_created: timestamp_created:
type: string type: string
type: type:
type: string $ref: '#/definitions/models.ClientType'
user_id: user_id:
type: string type: string
type: object type: object
models.ClientType:
enum:
- ANDROID
- IOS
type: string
x-enum-varnames:
- ClientTypeAndroid
- ClientTypeIOS
models.CompatMessage: models.CompatMessage:
properties: properties:
body: body:
@ -420,6 +535,29 @@ definitions:
usr_msg_id: usr_msg_id:
type: string type: string
type: object 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: models.MessageJSON:
properties: properties:
channel_id: channel_id:
@ -468,8 +606,6 @@ definitions:
type: object type: object
models.UserJSON: models.UserJSON:
properties: properties:
admin_key:
type: string
default_channel: default_channel:
type: string type: string
is_pro: is_pro:
@ -482,10 +618,6 @@ definitions:
type: integer type: integer
quota_used: quota_used:
type: integer type: integer
read_key:
type: string
send_key:
type: string
timestamp_created: timestamp_created:
type: string type: string
timestamp_lastread: timestamp_lastread:
@ -497,7 +629,7 @@ definitions:
username: username:
type: string type: string
type: object type: object
models.UserJSONWithClients: models.UserJSONWithClientsAndKeys:
properties: properties:
admin_key: admin_key:
type: string type: string
@ -544,10 +676,6 @@ paths:
description: All parameter can be set via query-parameter or the json body. description: All parameter can be set via query-parameter or the json body.
Only UserID, UserKey and Title are required Only UserID, UserKey and Title are required
parameters: parameters:
- example: qhnUbKcLgp6tg
in: query
name: chan_key
type: string
- example: test - example: test
in: query in: query
name: channel name: channel
@ -556,6 +684,10 @@ paths:
in: query in: query
name: content name: content
type: string type: string
- example: P3TNH8mvv14fm
in: query
name: key
type: string
- example: db8b0e6a-a08c-4646 - example: db8b0e6a-a08c-4646
in: query in: query
name: msg_id name: msg_id
@ -584,19 +716,11 @@ paths:
in: query in: query
name: user_id name: user_id
type: string type: string
- example: P3TNH8mvv14fm
in: query
name: user_key
type: string
- description: ' ' - description: ' '
in: body in: body
name: post_body name: post_body
schema: schema:
$ref: '#/definitions/handler.SendMessage.combined' $ref: '#/definitions/handler.SendMessage.combined'
- example: qhnUbKcLgp6tg
in: formData
name: chan_key
type: string
- example: test - example: test
in: formData in: formData
name: channel name: channel
@ -605,6 +729,10 @@ paths:
in: formData in: formData
name: content name: content
type: string type: string
- example: P3TNH8mvv14fm
in: formData
name: key
type: string
- example: db8b0e6a-a08c-4646 - example: db8b0e6a-a08c-4646
in: formData in: formData
name: msg_id name: msg_id
@ -633,10 +761,6 @@ paths:
in: formData in: formData
name: user_id name: user_id
type: string type: string
- example: P3TNH8mvv14fm
in: formData
name: user_key
type: string
responses: responses:
"200": "200":
description: OK description: OK
@ -1240,7 +1364,7 @@ paths:
"200": "200":
description: OK description: OK
schema: schema:
$ref: '#/definitions/models.UserJSONWithClients' $ref: '#/definitions/models.UserJSONWithClientsAndKeys'
"400": "400":
description: supplied values/parameters cannot be parsed / are invalid description: supplied values/parameters cannot be parsed / are invalid
schema: schema:
@ -1252,6 +1376,193 @@ paths:
summary: Create a new user summary: Create a new user
tags: tags:
- API-v2 - 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}: /api/v2/users/{uid}:
get: get:
operationId: api-user-get operationId: api-user-get
@ -1956,10 +2267,6 @@ paths:
description: All parameter can be set via query-parameter or the json body. description: All parameter can be set via query-parameter or the json body.
Only UserID, UserKey and Title are required Only UserID, UserKey and Title are required
parameters: parameters:
- example: qhnUbKcLgp6tg
in: query
name: chan_key
type: string
- example: test - example: test
in: query in: query
name: channel name: channel
@ -1968,6 +2275,10 @@ paths:
in: query in: query
name: content name: content
type: string type: string
- example: P3TNH8mvv14fm
in: query
name: key
type: string
- example: db8b0e6a-a08c-4646 - example: db8b0e6a-a08c-4646
in: query in: query
name: msg_id name: msg_id
@ -1996,19 +2307,11 @@ paths:
in: query in: query
name: user_id name: user_id
type: string type: string
- example: P3TNH8mvv14fm
in: query
name: user_key
type: string
- description: ' ' - description: ' '
in: body in: body
name: post_body name: post_body
schema: schema:
$ref: '#/definitions/handler.SendMessage.combined' $ref: '#/definitions/handler.SendMessage.combined'
- example: qhnUbKcLgp6tg
in: formData
name: chan_key
type: string
- example: test - example: test
in: formData in: formData
name: channel name: channel
@ -2017,6 +2320,10 @@ paths:
in: formData in: formData
name: content name: content
type: string type: string
- example: P3TNH8mvv14fm
in: formData
name: key
type: string
- example: db8b0e6a-a08c-4646 - example: db8b0e6a-a08c-4646
in: formData in: formData
name: msg_id name: msg_id
@ -2045,10 +2352,6 @@ paths:
in: formData in: formData
name: user_id name: user_id
type: string type: string
- example: P3TNH8mvv14fm
in: formData
name: user_key
type: string
responses: responses:
"200": "200":
description: OK description: OK

View File

@ -439,18 +439,6 @@ func TestChannelUpdate(t *testing.T) {
tt.AssertEqual(t, "channels.send_key", chan0["send_key"], chan1["send_key"]) 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 // [5] update description_name
tt.RequestAuthPatch[tt.Void](t, admintok, baseUrl, fmt.Sprintf("/api/v2/users/%s/channels/%s", uid, chanid), gin.H{ tt.RequestAuthPatch[tt.Void](t, admintok, baseUrl, fmt.Sprintf("/api/v2/users/%s/channels/%s", uid, chanid), gin.H{

View File

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

View File

@ -672,7 +672,7 @@ func TestCompatRequery(t *testing.T) {
r6 := tt.RequestPost[gin.H](t, baseUrl, "/", gin.H{ r6 := tt.RequestPost[gin.H](t, baseUrl, "/", gin.H{
"user_id": useridnew, "user_id": useridnew,
"user_key": userkey, "key": userkey,
"title": "HelloWorld_001", "title": "HelloWorld_001",
"msg_id": "r6", "msg_id": "r6",
}) })

View File

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

View File

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

View File

@ -19,7 +19,6 @@ func TestCreateUserNoClient(t *testing.T) {
tt.AssertEqual(t, "len(clients)", 0, len(r0["clients"].([]any))) tt.AssertEqual(t, "len(clients)", 0, len(r0["clients"].([]any)))
uid := fmt.Sprintf("%v", r0["user_id"]) uid := fmt.Sprintf("%v", r0["user_id"])
admintok := r0["admin_key"].(string)
readtok := r0["read_key"].(string) readtok := r0["read_key"].(string)
sendtok := r0["send_key"].(string) sendtok := r0["send_key"].(string)
@ -29,7 +28,6 @@ func TestCreateUserNoClient(t *testing.T) {
r1 := tt.RequestAuthGet[gin.H](t, readtok, baseUrl, "/api/v2/users/"+uid) 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, "uid", uid, fmt.Sprintf("%v", r1["user_id"]))
tt.AssertEqual(t, "admin_key", admintok, r1["admin_key"])
} }
func TestCreateUserDummyClient(t *testing.T) { 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) 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, "uid", uid, fmt.Sprintf("%v", r1["user_id"]))
tt.AssertEqual(t, "admin_key", admintok, r1["admin_key"])
tt.AssertEqual(t, "username", nil, r1["username"]) tt.AssertEqual(t, "username", nil, r1["username"])
type rt2 struct { type rt2 struct {
@ -92,7 +89,6 @@ func TestCreateUserWithUsername(t *testing.T) {
r1 := tt.RequestAuthGet[gin.H](t, admintok, baseUrl, "/api/v2/users/"+uid) 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, "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"]) 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) 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) { func TestDeleteUser(t *testing.T) {
t.SkipNow() // TODO DeleteUser Not implemented t.SkipNow() // TODO DeleteUser Not implemented

View File

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

View File

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

View File

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

View File

@ -23,7 +23,7 @@ function send()
let data = new FormData(); let data = new FormData();
data.append('user_id', uid.value); 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 (tit.value !== '') data.append('title', tit.value);
if (cnt.value !== '') data.append('content', cnt.value); if (cnt.value !== '') data.append('content', cnt.value);
if (pio.value !== '') data.append('priority', pio.value); if (pio.value !== '') data.append('priority', pio.value);

View File

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

View File

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