Merge branch 'refactor_server'

This commit is contained in:
2024-09-16 16:23:15 +02:00
76 changed files with 4183 additions and 4183 deletions
+10
View File
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="deploymentTargetSelector">
<selectionStates>
<SelectionState runConfigName="app">
<option name="selectionMode" value="DROPDOWN" />
</SelectionState>
</selectionStates>
</component>
</project>
+263
View File
@@ -0,0 +1,263 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="direct_access_persist.xml">
<option name="deviceSelectionList">
<list>
<PersistentDeviceSelectionData>
<option name="api" value="27" />
<option name="brand" value="DOCOMO" />
<option name="codename" value="F01L" />
<option name="id" value="F01L" />
<option name="manufacturer" value="FUJITSU" />
<option name="name" value="F-01L" />
<option name="screenDensity" value="360" />
<option name="screenX" value="720" />
<option name="screenY" value="1280" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="28" />
<option name="brand" value="DOCOMO" />
<option name="codename" value="SH-01L" />
<option name="id" value="SH-01L" />
<option name="manufacturer" value="SHARP" />
<option name="name" value="AQUOS sense2 SH-01L" />
<option name="screenDensity" value="480" />
<option name="screenX" value="1080" />
<option name="screenY" value="2160" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="31" />
<option name="brand" value="samsung" />
<option name="codename" value="a51" />
<option name="id" value="a51" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="Galaxy A51" />
<option name="screenDensity" value="420" />
<option name="screenX" value="1080" />
<option name="screenY" value="2400" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="google" />
<option name="codename" value="akita" />
<option name="id" value="akita" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel 8a" />
<option name="screenDensity" value="420" />
<option name="screenX" value="1080" />
<option name="screenY" value="2400" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="33" />
<option name="brand" value="samsung" />
<option name="codename" value="b0q" />
<option name="id" value="b0q" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="Galaxy S22 Ultra" />
<option name="screenDensity" value="600" />
<option name="screenX" value="1440" />
<option name="screenY" value="3088" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="32" />
<option name="brand" value="google" />
<option name="codename" value="bluejay" />
<option name="id" value="bluejay" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel 6a" />
<option name="screenDensity" value="420" />
<option name="screenX" value="1080" />
<option name="screenY" value="2400" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="29" />
<option name="brand" value="samsung" />
<option name="codename" value="crownqlteue" />
<option name="id" value="crownqlteue" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="Galaxy Note9" />
<option name="screenDensity" value="420" />
<option name="screenX" value="2220" />
<option name="screenY" value="1080" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="samsung" />
<option name="codename" value="dm3q" />
<option name="id" value="dm3q" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="Galaxy S23 Ultra" />
<option name="screenDensity" value="600" />
<option name="screenX" value="1440" />
<option name="screenY" value="3088" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="33" />
<option name="brand" value="google" />
<option name="codename" value="felix" />
<option name="id" value="felix" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel Fold" />
<option name="screenDensity" value="420" />
<option name="screenX" value="2208" />
<option name="screenY" value="1840" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="33" />
<option name="brand" value="google" />
<option name="codename" value="felix_camera" />
<option name="id" value="felix_camera" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel Fold (Camera-enabled)" />
<option name="screenDensity" value="420" />
<option name="screenX" value="2208" />
<option name="screenY" value="1840" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="33" />
<option name="brand" value="samsung" />
<option name="codename" value="gts8uwifi" />
<option name="id" value="gts8uwifi" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="Galaxy Tab S8 Ultra" />
<option name="screenDensity" value="320" />
<option name="screenX" value="1848" />
<option name="screenY" value="2960" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="google" />
<option name="codename" value="husky" />
<option name="id" value="husky" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel 8 Pro" />
<option name="screenDensity" value="390" />
<option name="screenX" value="1008" />
<option name="screenY" value="2244" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="30" />
<option name="brand" value="motorola" />
<option name="codename" value="java" />
<option name="id" value="java" />
<option name="manufacturer" value="Motorola" />
<option name="name" value="G20" />
<option name="screenDensity" value="280" />
<option name="screenX" value="720" />
<option name="screenY" value="1600" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="33" />
<option name="brand" value="google" />
<option name="codename" value="lynx" />
<option name="id" value="lynx" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel 7a" />
<option name="screenDensity" value="420" />
<option name="screenX" value="1080" />
<option name="screenY" value="2400" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="31" />
<option name="brand" value="google" />
<option name="codename" value="oriole" />
<option name="id" value="oriole" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel 6" />
<option name="screenDensity" value="420" />
<option name="screenX" value="1080" />
<option name="screenY" value="2400" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="33" />
<option name="brand" value="google" />
<option name="codename" value="panther" />
<option name="id" value="panther" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel 7" />
<option name="screenDensity" value="420" />
<option name="screenX" value="1080" />
<option name="screenY" value="2400" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="31" />
<option name="brand" value="samsung" />
<option name="codename" value="q2q" />
<option name="id" value="q2q" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="Galaxy Z Fold3" />
<option name="screenDensity" value="420" />
<option name="screenX" value="1768" />
<option name="screenY" value="2208" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="samsung" />
<option name="codename" value="q5q" />
<option name="id" value="q5q" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="Galaxy Z Fold5" />
<option name="screenDensity" value="420" />
<option name="screenX" value="1812" />
<option name="screenY" value="2176" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="30" />
<option name="brand" value="google" />
<option name="codename" value="r11" />
<option name="id" value="r11" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel Watch" />
<option name="screenDensity" value="320" />
<option name="screenX" value="384" />
<option name="screenY" value="384" />
<option name="type" value="WEAR_OS" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="30" />
<option name="brand" value="google" />
<option name="codename" value="redfin" />
<option name="id" value="redfin" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel 5" />
<option name="screenDensity" value="440" />
<option name="screenX" value="1080" />
<option name="screenY" value="2340" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="google" />
<option name="codename" value="shiba" />
<option name="id" value="shiba" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel 8" />
<option name="screenDensity" value="420" />
<option name="screenX" value="1080" />
<option name="screenY" value="2400" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="33" />
<option name="brand" value="google" />
<option name="codename" value="tangorpro" />
<option name="id" value="tangorpro" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel Tablet" />
<option name="screenDensity" value="320" />
<option name="screenX" value="1600" />
<option name="screenY" value="2560" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="29" />
<option name="brand" value="samsung" />
<option name="codename" value="x1q" />
<option name="id" value="x1q" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="Galaxy S20" />
<option name="screenDensity" value="480" />
<option name="screenX" value="1440" />
<option name="screenY" value="3200" />
</PersistentDeviceSelectionData>
</list>
</option>
</component>
</project>
Binary file not shown.
BIN
View File
Binary file not shown.
Executable
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
Executable
BIN
View File
Binary file not shown.
+15 -1
View File
@@ -23,12 +23,26 @@
- [ ] Logout - [ ] Logout
- [ ] Send-page - [ ] Send-page
- [ ] Still @ERROR on scn-init, but no logs? - better persist error (write in SharedPrefs at error_$date=txt ?), also perhaps print first error line in scn-init notification?
----- -----
# TODO iOS specific
- [ ] payment / pro
- [ ] show notifiactions (foreground/background/etc)
- [ ] handle click-on-notifications should open message
- [ ] share message
- [ ] scan QR
-----
# TODO Server
- [ ] Switch server to sq style from faby - [ ] Switch server to sq style from faby
- [ ] switch from mattn to go-sqlite - [ ] switch from mattn to go-sqlite
- [ ] Single struct for model/db/json - [ ] Single struct for model/db/json
- [ ] use ginext
- [ ] use sq.Query | sq.Update | sq.InsertAndQuery | .... - [ ] use sq.Query | sq.Update | sq.InsertAndQuery | ....
- [ ] sq.DBOptions - enable CommentTrimmer and DefaultConverter - [ ] sq.DBOptions - enable CommentTrimmer and DefaultConverter
- [ ] run unit-tests... - [ ] run unit-tests...
+6 -4
View File
@@ -5,6 +5,8 @@ PORT=9090
NAMESPACE=$(shell git rev-parse --abbrev-ref HEAD) NAMESPACE=$(shell git rev-parse --abbrev-ref HEAD)
HASH=$(shell git rev-parse HEAD) HASH=$(shell git rev-parse HEAD)
TAGS="timetzdata sqlite_fts5 sqlite_foreign_keys"
.PHONY: test swagger pygmentize docker migrate dgi pygmentize lint docker .PHONY: test swagger pygmentize docker migrate dgi pygmentize lint docker
SWAGGO_VERSION=v1.8.12 SWAGGO_VERSION=v1.8.12
@@ -13,7 +15,7 @@ SWAGGO=github.com/swaggo/swag/cmd/swag@$(SWAGGO_VERSION)
build: ids enums swagger pygmentize fmt build: ids enums swagger pygmentize fmt
mkdir -p _build mkdir -p _build
rm -f ./_build/scn_backend rm -f ./_build/scn_backend
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 $(TAGS) ./cmd/scnserver
enums: enums:
go generate models/enums.go go generate models/enums.go
@@ -27,7 +29,7 @@ run: build
gow: gow:
which gow || go install github.com/mitranim/gow@latest which gow || go install github.com/mitranim/gow@latest
gow -e "go,mod,html,css,json,yaml,js" run -tags "timetzdata sqlite_fts5 sqlite_foreign_keys" blackforestbytes.com/simplecloudnotifier/cmd/scnserver gow -e "go,mod,html,css,json,yaml,js" run -tags $(TAGS) blackforestbytes.com/simplecloudnotifier/cmd/scnserver
dgi: dgi:
[ ! -f "DOCKER_GIT_INFO" ] || rm DOCKER_GIT_INFO [ ! -f "DOCKER_GIT_INFO" ] || rm DOCKER_GIT_INFO
@@ -99,10 +101,10 @@ fmt: swagger-setup
test: test:
which gotestsum || go install gotest.tools/gotestsum@latest which gotestsum || go install gotest.tools/gotestsum@latest
gotestsum --format "testname" -- -tags="timetzdata sqlite_fts5 sqlite_foreign_keys" "./test" gotestsum --format "testname" -- -tags $(TAGS) "./test"
migrate: migrate:
CGO_ENABLED=1 go build -v -o _build/scn_migrate -tags "timetzdata sqlite_fts5 sqlite_foreign_keys" ./cmd/migrate CGO_ENABLED=1 go build -v -o _build/scn_migrate -tags $(TAGS) ./cmd/migrate
./_build/scn_migrate ./_build/scn_migrate
lint: lint:
+2 -13
View File
@@ -10,11 +10,7 @@
- ios purchase verification - ios purchase verification
- (!) use goext.ginWrapper - exerr.New | exerr.Wrap
- (!) use goext.exerr
- use bfcodegen (enums+id)
#### UNSURE #### UNSURE
@@ -57,19 +53,12 @@
- Pagination for ListChannels / ListSubscriptions / ListClients / ListChannelSubscriptions / ListUserSubscriptions - Pagination for ListChannels / ListSubscriptions / ListClients / ListChannelSubscriptions / ListUserSubscriptions
- Use only single struct for DB|Model|JSON
* needs sq.Converter implementation
* needs to handle joined data
* rfctime.Time...
- use job superclass (copy from isi/bnet/?), reduce duplicate code - use job superclass (copy from isi/bnet/?), reduce duplicate code
- admin panel (especially errors and requests) - admin panel (especially errors and requests)
- cli app (?) - cli app (?)
- Use "github.com/glebarez/go-sqlite" instead of mattn3 (see ai-sig alarmserver)
#### FUTURE #### FUTURE
- Remove compat, especially do not create compat id for every new message... - Remove compat, especially do not create compat id for every new message...
+13 -12
View File
@@ -8,18 +8,19 @@ const (
NO_ERROR APIError = 0000 NO_ERROR APIError = 0000
MISSING_UID APIError = 1101 MISSING_UID APIError = 1101
MISSING_TOK APIError = 1102 MISSING_TOK APIError = 1102
MISSING_TITLE APIError = 1103 MISSING_TITLE APIError = 1103
INVALID_PRIO APIError = 1104 INVALID_PRIO APIError = 1104
REQ_METHOD APIError = 1105 REQ_METHOD APIError = 1105
INVALID_CLIENTTYPE APIError = 1106 INVALID_CLIENTTYPE APIError = 1106
PAGETOKEN_ERROR APIError = 1121 PAGETOKEN_ERROR APIError = 1121
BINDFAIL_QUERY_PARAM APIError = 1151 BINDFAIL_QUERY_PARAM APIError = 1151
BINDFAIL_BODY_PARAM APIError = 1152 BINDFAIL_BODY_PARAM APIError = 1152
BINDFAIL_URI_PARAM APIError = 1153 BINDFAIL_URI_PARAM APIError = 1153
INVALID_BODY_PARAM APIError = 1161 BINDFAIL_HEADER_PARAM APIError = 1152
INVALID_ENUM_VALUE APIError = 1171 INVALID_BODY_PARAM APIError = 1161
INVALID_ENUM_VALUE APIError = 1171
NO_TITLE APIError = 1201 NO_TITLE APIError = 1201
TITLE_TOO_LONG APIError = 1202 TITLE_TOO_LONG APIError = 1202
-21
View File
@@ -1,21 +0,0 @@
package ginext
import (
"github.com/gin-gonic/gin"
"net/http"
)
func CorsMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
c.Writer.Header().Set("Access-Control-Allow-Origin", "*")
c.Writer.Header().Set("Access-Control-Allow-Credentials", "true")
c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, accept, origin, Cache-Control, X-Requested-With")
c.Writer.Header().Set("Access-Control-Allow-Methods", "OPTIONS, GET, POST, PUT, PATCH, DELETE")
if c.Request.Method == "OPTIONS" {
c.AbortWithStatus(http.StatusOK)
} else {
c.Next()
}
}
}
-31
View File
@@ -1,31 +0,0 @@
package ginext
import (
scn "blackforestbytes.com/simplecloudnotifier"
"github.com/gin-gonic/gin"
)
var SuppressGinLogs = false
func NewEngine(cfg scn.Config) *gin.Engine {
engine := gin.New()
engine.RedirectFixedPath = false
engine.RedirectTrailingSlash = false
if cfg.Cors {
engine.Use(CorsMiddleware())
}
if cfg.GinDebug {
ginlogger := gin.Logger()
engine.Use(func(context *gin.Context) {
if SuppressGinLogs {
return
}
ginlogger(context)
})
}
return engine
}
-24
View File
@@ -1,24 +0,0 @@
package ginext
import (
"github.com/gin-gonic/gin"
"net/http"
)
func RedirectFound(newuri string) gin.HandlerFunc {
return func(g *gin.Context) {
g.Redirect(http.StatusFound, newuri)
}
}
func RedirectTemporary(newuri string) gin.HandlerFunc {
return func(g *gin.Context) {
g.Redirect(http.StatusTemporaryRedirect, newuri)
}
}
func RedirectPermanent(newuri string) gin.HandlerFunc {
return func(g *gin.Context) {
g.Redirect(http.StatusPermanentRedirect, newuri)
}
}
+41 -110
View File
@@ -7,114 +7,43 @@ import (
"fmt" "fmt"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"gogs.mikescher.com/BlackForestBytes/goext/ginext"
json "gogs.mikescher.com/BlackForestBytes/goext/gojson" json "gogs.mikescher.com/BlackForestBytes/goext/gojson"
"gogs.mikescher.com/BlackForestBytes/goext/langext" "gogs.mikescher.com/BlackForestBytes/goext/langext"
"runtime/debug" "runtime/debug"
"strings" "strings"
) )
type HTTPResponse interface { type cookieval struct {
Write(g *gin.Context) name string
Statuscode() int value string
BodyString() *string maxAge int
ContentType() string path string
domain string
secure bool
httpOnly bool
} }
type jsonHTTPResponse struct { type headerval struct {
statusCode int Key string
data any Val string
}
func (j jsonHTTPResponse) Write(g *gin.Context) {
g.Render(j.statusCode, json.GoJsonRender{Data: j.data, NilSafeSlices: true, NilSafeMaps: true})
}
func (j jsonHTTPResponse) Statuscode() int {
return j.statusCode
}
func (j jsonHTTPResponse) BodyString() *string {
v, err := json.Marshal(j.data)
if err != nil {
return nil
}
return langext.Ptr(string(v))
}
func (j jsonHTTPResponse) ContentType() string {
return "application/json"
}
type emptyHTTPResponse struct {
statusCode int
}
func (j emptyHTTPResponse) Write(g *gin.Context) {
g.Status(j.statusCode)
}
func (j emptyHTTPResponse) Statuscode() int {
return j.statusCode
}
func (j emptyHTTPResponse) BodyString() *string {
return nil
}
func (j emptyHTTPResponse) ContentType() string {
return ""
}
type textHTTPResponse struct {
statusCode int
data string
}
func (j textHTTPResponse) Write(g *gin.Context) {
g.String(j.statusCode, "%s", j.data)
}
func (j textHTTPResponse) Statuscode() int {
return j.statusCode
}
func (j textHTTPResponse) BodyString() *string {
return langext.Ptr(j.data)
}
func (j textHTTPResponse) ContentType() string {
return "text/plain"
}
type dataHTTPResponse struct {
statusCode int
data []byte
contentType string
}
func (j dataHTTPResponse) Write(g *gin.Context) {
g.Data(j.statusCode, j.contentType, j.data)
}
func (j dataHTTPResponse) Statuscode() int {
return j.statusCode
}
func (j dataHTTPResponse) BodyString() *string {
return langext.Ptr(string(j.data))
}
func (j dataHTTPResponse) ContentType() string {
return j.contentType
} }
type errorHTTPResponse struct { type errorHTTPResponse struct {
statusCode int statusCode int
data any data any
error error error error
headers []headerval
cookies []cookieval
} }
func (j errorHTTPResponse) Write(g *gin.Context) { func (j errorHTTPResponse) Write(g *gin.Context) {
for _, v := range j.headers {
g.Header(v.Key, v.Val)
}
for _, v := range j.cookies {
g.SetCookie(v.name, v.value, v.maxAge, v.path, v.domain, v.secure, v.httpOnly)
}
g.JSON(j.statusCode, j.data) g.JSON(j.statusCode, j.data)
} }
@@ -122,7 +51,7 @@ func (j errorHTTPResponse) Statuscode() int {
return j.statusCode return j.statusCode
} }
func (j errorHTTPResponse) BodyString() *string { func (j errorHTTPResponse) BodyString(g *gin.Context) *string {
v, err := json.Marshal(j.data) v, err := json.Marshal(j.data)
if err != nil { if err != nil {
return nil return nil
@@ -134,39 +63,41 @@ func (j errorHTTPResponse) ContentType() string {
return "application/json" return "application/json"
} }
func Status(sc int) HTTPResponse { func (j errorHTTPResponse) WithHeader(k string, v string) ginext.HTTPResponse {
return &emptyHTTPResponse{statusCode: sc} j.headers = append(j.headers, headerval{k, v})
return j
} }
func JSON(sc int, data any) HTTPResponse { func (j errorHTTPResponse) WithCookie(name string, value string, maxAge int, path string, domain string, secure bool, httpOnly bool) ginext.HTTPResponse {
return &jsonHTTPResponse{statusCode: sc, data: data} j.cookies = append(j.cookies, cookieval{name, value, maxAge, path, domain, secure, httpOnly})
return j
} }
func Data(sc int, contentType string, data []byte) HTTPResponse { func (j errorHTTPResponse) IsSuccess() bool {
return &dataHTTPResponse{statusCode: sc, contentType: contentType, data: data} return false
} }
func Text(sc int, data string) HTTPResponse { func (j errorHTTPResponse) Headers() []string {
return &textHTTPResponse{statusCode: sc, data: data} return langext.ArrMap(j.headers, func(v headerval) string { return v.Key + "=" + v.Val })
} }
func InternalError(e error) HTTPResponse { func (j errorHTTPResponse) Unwrap() error {
return j.error
}
func InternalError(e error) ginext.HTTPResponse {
return createApiError(nil, "InternalError", 500, apierr.INTERNAL_EXCEPTION, 0, e.Error(), e) return createApiError(nil, "InternalError", 500, apierr.INTERNAL_EXCEPTION, 0, e.Error(), e)
} }
func APIError(g *gin.Context, status int, errorid apierr.APIError, msg string, e error) HTTPResponse { func APIError(g *gin.Context, status int, errorid apierr.APIError, msg string, e error) ginext.HTTPResponse {
return createApiError(g, "APIError", status, errorid, 0, msg, e) return createApiError(g, "APIError", status, errorid, 0, msg, e)
} }
func SendAPIError(g *gin.Context, status int, errorid apierr.APIError, highlight apihighlight.ErrHighlight, msg string, e error) HTTPResponse { func SendAPIError(g *gin.Context, status int, errorid apierr.APIError, highlight apihighlight.ErrHighlight, msg string, e error) ginext.HTTPResponse {
return createApiError(g, "SendAPIError", status, errorid, highlight, msg, e) return createApiError(g, "SendAPIError", status, errorid, highlight, msg, e)
} }
func NotImplemented(g *gin.Context) HTTPResponse { func createApiError(g *gin.Context, ident string, status int, errorid apierr.APIError, highlight apihighlight.ErrHighlight, msg string, e error) ginext.HTTPResponse {
return createApiError(g, "NotImplemented", 500, apierr.NOT_IMPLEMENTED, 0, "Not Implemented", nil)
}
func createApiError(g *gin.Context, ident string, status int, errorid apierr.APIError, highlight apihighlight.ErrHighlight, msg string, e error) HTTPResponse {
reqUri := "" reqUri := ""
if g != nil && g.Request != nil { if g != nil && g.Request != nil {
reqUri = g.Request.Method + " :: " + g.Request.RequestURI reqUri = g.Request.Method + " :: " + g.Request.RequestURI
@@ -207,6 +138,6 @@ func createApiError(g *gin.Context, ident string, status int, errorid apierr.API
} }
} }
func CompatAPIError(errid int, msg string) HTTPResponse { func CompatAPIError(errid int, msg string) ginext.HTTPResponse {
return &jsonHTTPResponse{statusCode: 200, data: compatAPIError{Success: false, ErrorID: errid, Message: msg}} return ginext.JSON(200, compatAPIError{Success: false, ErrorID: errid, Message: msg})
} }
-191
View File
@@ -1,191 +0,0 @@
package ginresp
import (
scn "blackforestbytes.com/simplecloudnotifier"
"blackforestbytes.com/simplecloudnotifier/api/apierr"
"blackforestbytes.com/simplecloudnotifier/models"
"errors"
"fmt"
"github.com/gin-gonic/gin"
"github.com/mattn/go-sqlite3"
"github.com/rs/zerolog/log"
"gogs.mikescher.com/BlackForestBytes/goext/dataext"
"gogs.mikescher.com/BlackForestBytes/goext/langext"
"math/rand"
"runtime/debug"
"time"
)
type WHandlerFunc func(*gin.Context) HTTPResponse
type RequestLogAcceptor interface {
InsertRequestLog(data models.RequestLog)
}
func Wrap(rlacc RequestLogAcceptor, fn WHandlerFunc) gin.HandlerFunc {
maxRetry := scn.Conf.RequestMaxRetry
retrySleep := scn.Conf.RequestRetrySleep
return func(g *gin.Context) {
reqctx := g.Request.Context()
if g.Request.Body != nil {
g.Request.Body = dataext.NewBufferedReadCloser(g.Request.Body)
}
t0 := time.Now()
for ctr := 1; ; ctr++ {
wrap, stackTrace, panicObj := callPanicSafe(fn, g)
if panicObj != nil {
log.Error().Interface("panicObj", panicObj).Msg("Panic occured (in gin handler)")
log.Error().Msg(stackTrace)
wrap = APIError(g, 500, apierr.PANIC, "A panic occured in the HTTP handler", errors.New(fmt.Sprintf("%+v\n\n@:\n%s", panicObj, stackTrace)))
}
if g.Writer.Written() {
if scn.Conf.ReqLogEnabled {
rlacc.InsertRequestLog(createRequestLog(g, t0, ctr, nil, langext.Ptr("Writing in WrapperFunc is not supported")))
}
panic("Writing in WrapperFunc is not supported")
}
if ctr < maxRetry && isSqlite3Busy(wrap) {
log.Warn().Int("counter", ctr).Str("url", g.Request.URL.String()).Msg("Retry request (ErrBusy)")
err := resetBody(g)
if err != nil {
panic(err)
}
time.Sleep(time.Duration(int64(float64(retrySleep) * (0.5 + rand.Float64()))))
continue
}
if reqctx.Err() == nil {
if scn.Conf.ReqLogEnabled {
rlacc.InsertRequestLog(createRequestLog(g, t0, ctr, wrap, nil))
}
statuscode := wrap.Statuscode()
if statuscode/100 != 2 {
log.Warn().Str("url", g.Request.Method+"::"+g.Request.URL.String()).Msg(fmt.Sprintf("Request failed with statuscode %d", statuscode))
}
wrap.Write(g)
}
return
}
}
}
func createRequestLog(g *gin.Context, t0 time.Time, ctr int, resp HTTPResponse, panicstr *string) models.RequestLog {
t1 := time.Now()
ua := g.Request.UserAgent()
auth := g.Request.Header.Get("Authorization")
ct := g.Request.Header.Get("Content-Type")
var reqbody []byte = nil
if g.Request.Body != nil {
brcbody, err := g.Request.Body.(dataext.BufferedReadCloser).BufferedAll()
if err == nil {
reqbody = brcbody
}
}
var strreqbody *string = nil
if len(reqbody) < scn.Conf.ReqLogMaxBodySize {
strreqbody = langext.Ptr(string(reqbody))
}
var respbody *string = nil
var strrespbody *string = nil
if resp != nil {
respbody = resp.BodyString()
if respbody != nil && len(*respbody) < scn.Conf.ReqLogMaxBodySize {
strrespbody = respbody
}
}
permObj, hasPerm := g.Get("perm")
hasTok := false
if hasPerm {
hasTok = permObj.(models.PermissionSet).Token != nil
}
return models.RequestLog{
Method: g.Request.Method,
URI: g.Request.URL.String(),
UserAgent: langext.Conditional(ua == "", nil, &ua),
Authentication: langext.Conditional(auth == "", nil, &auth),
RequestBody: strreqbody,
RequestBodySize: int64(len(reqbody)),
RequestContentType: ct,
RemoteIP: g.RemoteIP(),
KeyID: langext.ConditionalFn10(hasTok, func() *models.KeyTokenID { return langext.Ptr(permObj.(models.PermissionSet).Token.KeyTokenID) }, nil),
UserID: langext.ConditionalFn10(hasTok, func() *models.UserID { return langext.Ptr(permObj.(models.PermissionSet).Token.OwnerUserID) }, nil),
Permissions: langext.ConditionalFn10(hasTok, func() *string { return langext.Ptr(permObj.(models.PermissionSet).Token.Permissions.String()) }, nil),
ResponseStatuscode: langext.ConditionalFn10(resp != nil, func() *int64 { return langext.Ptr(int64(resp.Statuscode())) }, nil),
ResponseBodySize: langext.ConditionalFn10(strrespbody != nil, func() *int64 { return langext.Ptr(int64(len(*respbody))) }, nil),
ResponseBody: strrespbody,
ResponseContentType: langext.ConditionalFn10(resp != nil, func() string { return resp.ContentType() }, ""),
RetryCount: int64(ctr),
Panicked: panicstr != nil,
PanicStr: panicstr,
ProcessingTime: t1.Sub(t0),
TimestampStart: t0,
TimestampFinish: t1,
}
}
func callPanicSafe(fn WHandlerFunc, g *gin.Context) (res HTTPResponse, stackTrace string, panicObj any) {
defer func() {
if rec := recover(); rec != nil {
res = nil
stackTrace = string(debug.Stack())
panicObj = rec
}
}()
res = fn(g)
return res, "", nil
}
func resetBody(g *gin.Context) error {
if g.Request.Body == nil {
return nil
}
err := g.Request.Body.(dataext.BufferedReadCloser).Reset()
if err != nil {
return err
}
return nil
}
func isSqlite3Busy(r HTTPResponse) bool {
if errwrap, ok := r.(*errorHTTPResponse); ok && errwrap != nil {
if errors.Is(errwrap.error, sqlite3.ErrBusy) {
return true
}
var s3err sqlite3.Error
if errors.As(errwrap.error, &s3err) {
if errors.Is(s3err.Code, sqlite3.ErrBusy) {
return true
}
}
}
return false
}
+233 -219
View File
@@ -4,11 +4,12 @@ import (
"blackforestbytes.com/simplecloudnotifier/api/apierr" "blackforestbytes.com/simplecloudnotifier/api/apierr"
"blackforestbytes.com/simplecloudnotifier/api/ginresp" "blackforestbytes.com/simplecloudnotifier/api/ginresp"
ct "blackforestbytes.com/simplecloudnotifier/db/cursortoken" ct "blackforestbytes.com/simplecloudnotifier/db/cursortoken"
"blackforestbytes.com/simplecloudnotifier/logic"
"blackforestbytes.com/simplecloudnotifier/models" "blackforestbytes.com/simplecloudnotifier/models"
"database/sql" "database/sql"
"errors" "errors"
"fmt" "fmt"
"github.com/gin-gonic/gin" "gogs.mikescher.com/BlackForestBytes/goext/ginext"
"gogs.mikescher.com/BlackForestBytes/goext/langext" "gogs.mikescher.com/BlackForestBytes/goext/langext"
"gogs.mikescher.com/BlackForestBytes/goext/mathext" "gogs.mikescher.com/BlackForestBytes/goext/mathext"
"net/http" "net/http"
@@ -37,7 +38,7 @@ import (
// @Failure 500 {object} ginresp.apiError "internal server error" // @Failure 500 {object} ginresp.apiError "internal server error"
// //
// @Router /api/v2/users/{uid}/channels [GET] // @Router /api/v2/users/{uid}/channels [GET]
func (h APIHandler) ListChannels(g *gin.Context) ginresp.HTTPResponse { func (h APIHandler) ListChannels(pctx ginext.PreContext) ginext.HTTPResponse {
type uri struct { type uri struct {
UserID models.UserID `uri:"uid" binding:"entityid"` UserID models.UserID `uri:"uid" binding:"entityid"`
} }
@@ -45,72 +46,72 @@ func (h APIHandler) ListChannels(g *gin.Context) ginresp.HTTPResponse {
Selector *string `json:"selector" form:"selector" enums:"owned,subscribed_any,all_any,subscribed,all"` Selector *string `json:"selector" form:"selector" enums:"owned,subscribed_any,all_any,subscribed,all"`
} }
type response struct { type response struct {
Channels []models.ChannelWithSubscriptionJSON `json:"channels"` Channels []models.ChannelWithSubscription `json:"channels"`
} }
var u uri var u uri
var q query var q query
ctx, errResp := h.app.StartRequest(g, &u, &q, nil, nil) ctx, g, errResp := pctx.URI(&u).Query(&q).Start()
if errResp != nil { if errResp != nil {
return *errResp return *errResp
} }
defer ctx.Cancel() defer ctx.Cancel()
if permResp := ctx.CheckPermissionUserRead(u.UserID); permResp != nil { return h.app.DoRequest(ctx, g, models.TLockRead, func(ctx *logic.AppContext, finishSuccess func(r ginext.HTTPResponse) ginext.HTTPResponse) ginext.HTTPResponse {
return *permResp
}
sel := strings.ToLower(langext.Coalesce(q.Selector, "owned")) if permResp := ctx.CheckPermissionUserRead(u.UserID); permResp != nil {
return *permResp
var res []models.ChannelWithSubscriptionJSON
if sel == "owned" {
channels, err := h.database.ListChannelsByOwner(ctx, u.UserID, u.UserID)
if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query channels", err)
} }
res = langext.ArrMap(channels, func(v models.ChannelWithSubscription) models.ChannelWithSubscriptionJSON { return v.JSON(true) })
} else if sel == "subscribed_any" { sel := strings.ToLower(langext.Coalesce(q.Selector, "owned"))
if sel == "owned" {
channels, err := h.database.ListChannelsByOwner(ctx, u.UserID, u.UserID)
if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query channels", err)
}
return finishSuccess(ginext.JSONWithFilter(http.StatusOK, response{Channels: channels}, "INCLUDE_KEY"))
} else if sel == "subscribed_any" {
channels, err := h.database.ListChannelsBySubscriber(ctx, u.UserID, nil)
if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query channels", err)
}
return finishSuccess(ginext.JSON(http.StatusOK, response{Channels: channels}))
} else if sel == "all_any" {
channels, err := h.database.ListChannelsByAccess(ctx, u.UserID, nil)
if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query channels", err)
}
return finishSuccess(ginext.JSON(http.StatusOK, response{Channels: channels}))
} else if sel == "subscribed" {
channels, err := h.database.ListChannelsBySubscriber(ctx, u.UserID, langext.Ptr(true))
if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query channels", err)
}
return finishSuccess(ginext.JSON(http.StatusOK, response{Channels: channels}))
} else if sel == "all" {
channels, err := h.database.ListChannelsByAccess(ctx, u.UserID, langext.Ptr(true))
if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query channels", err)
}
return finishSuccess(ginext.JSON(http.StatusOK, response{Channels: channels}))
} else {
return ginresp.APIError(g, 400, apierr.INVALID_ENUM_VALUE, "Invalid value for the [selector] parameter", nil)
channels, err := h.database.ListChannelsBySubscriber(ctx, u.UserID, nil)
if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query channels", err)
} }
res = langext.ArrMap(channels, func(v models.ChannelWithSubscription) models.ChannelWithSubscriptionJSON { return v.JSON(false) })
} else if sel == "all_any" { })
channels, err := h.database.ListChannelsByAccess(ctx, u.UserID, nil)
if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query channels", err)
}
res = langext.ArrMap(channels, func(v models.ChannelWithSubscription) models.ChannelWithSubscriptionJSON { return v.JSON(false) })
} else if sel == "subscribed" {
channels, err := h.database.ListChannelsBySubscriber(ctx, u.UserID, langext.Ptr(true))
if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query channels", err)
}
res = langext.ArrMap(channels, func(v models.ChannelWithSubscription) models.ChannelWithSubscriptionJSON { return v.JSON(false) })
} else if sel == "all" {
channels, err := h.database.ListChannelsByAccess(ctx, u.UserID, langext.Ptr(true))
if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query channels", err)
}
res = langext.ArrMap(channels, func(v models.ChannelWithSubscription) models.ChannelWithSubscriptionJSON { return v.JSON(false) })
} else {
return ginresp.APIError(g, 400, apierr.INVALID_ENUM_VALUE, "Invalid value for the [selector] parameter", nil)
}
return ctx.FinishSuccess(ginresp.JSON(http.StatusOK, response{Channels: res}))
} }
// GetChannel swaggerdoc // GetChannel swaggerdoc
@@ -122,39 +123,43 @@ func (h APIHandler) ListChannels(g *gin.Context) ginresp.HTTPResponse {
// @Param uid path string true "UserID" // @Param uid path string true "UserID"
// @Param cid path string true "ChannelID" // @Param cid path string true "ChannelID"
// //
// @Success 200 {object} models.ChannelWithSubscriptionJSON // @Success 200 {object} models.ChannelWithSubscription
// @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 401 {object} ginresp.apiError "user is not authorized / has missing permissions" // @Failure 401 {object} ginresp.apiError "user is not authorized / has missing permissions"
// @Failure 404 {object} ginresp.apiError "channel not found" // @Failure 404 {object} ginresp.apiError "channel not found"
// @Failure 500 {object} ginresp.apiError "internal server error" // @Failure 500 {object} ginresp.apiError "internal server error"
// //
// @Router /api/v2/users/{uid}/channels/{cid} [GET] // @Router /api/v2/users/{uid}/channels/{cid} [GET]
func (h APIHandler) GetChannel(g *gin.Context) ginresp.HTTPResponse { func (h APIHandler) GetChannel(pctx ginext.PreContext) ginext.HTTPResponse {
type uri struct { type uri struct {
UserID models.UserID `uri:"uid" binding:"entityid"` UserID models.UserID `uri:"uid" binding:"entityid"`
ChannelID models.ChannelID `uri:"cid" binding:"entityid"` ChannelID models.ChannelID `uri:"cid" binding:"entityid"`
} }
var u uri var u uri
ctx, errResp := h.app.StartRequest(g, &u, nil, nil, nil) ctx, g, errResp := pctx.URI(&u).Start()
if errResp != nil { if errResp != nil {
return *errResp return *errResp
} }
defer ctx.Cancel() defer ctx.Cancel()
if permResp := ctx.CheckPermissionUserRead(u.UserID); permResp != nil { return h.app.DoRequest(ctx, g, models.TLockRead, func(ctx *logic.AppContext, finishSuccess func(r ginext.HTTPResponse) ginext.HTTPResponse) ginext.HTTPResponse {
return *permResp
}
channel, err := h.database.GetChannel(ctx, u.UserID, u.ChannelID, true) if permResp := ctx.CheckPermissionUserRead(u.UserID); permResp != nil {
if errors.Is(err, sql.ErrNoRows) { return *permResp
return ginresp.APIError(g, 404, apierr.CHANNEL_NOT_FOUND, "Channel not found", err) }
}
if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query channel", err)
}
return ctx.FinishSuccess(ginresp.JSON(http.StatusOK, channel.JSON(true))) channel, err := h.database.GetChannel(ctx, u.UserID, u.ChannelID, true)
if errors.Is(err, sql.ErrNoRows) {
return ginresp.APIError(g, 404, apierr.CHANNEL_NOT_FOUND, "Channel not found", err)
}
if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query channel", err)
}
return finishSuccess(ginext.JSONWithFilter(http.StatusOK, channel, "INCLUDE_KEY"))
})
} }
// CreateChannel swaggerdoc // CreateChannel swaggerdoc
@@ -166,14 +171,14 @@ func (h APIHandler) GetChannel(g *gin.Context) ginresp.HTTPResponse {
// @Param uid path string true "UserID" // @Param uid path string true "UserID"
// @Param post_body body handler.CreateChannel.body false " " // @Param post_body body handler.CreateChannel.body false " "
// //
// @Success 200 {object} models.ChannelWithSubscriptionJSON // @Success 200 {object} models.ChannelWithSubscription
// @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 401 {object} ginresp.apiError "user is not authorized / has missing permissions" // @Failure 401 {object} ginresp.apiError "user is not authorized / has missing permissions"
// @Failure 409 {object} ginresp.apiError "channel already exists" // @Failure 409 {object} ginresp.apiError "channel already exists"
// @Failure 500 {object} ginresp.apiError "internal server error" // @Failure 500 {object} ginresp.apiError "internal server error"
// //
// @Router /api/v2/users/{uid}/channels [POST] // @Router /api/v2/users/{uid}/channels [POST]
func (h APIHandler) CreateChannel(g *gin.Context) ginresp.HTTPResponse { func (h APIHandler) CreateChannel(pctx ginext.PreContext) ginext.HTTPResponse {
type uri struct { type uri struct {
UserID models.UserID `uri:"uid" binding:"entityid"` UserID models.UserID `uri:"uid" binding:"entityid"`
} }
@@ -186,75 +191,78 @@ func (h APIHandler) CreateChannel(g *gin.Context) ginresp.HTTPResponse {
var u uri var u uri
var b body var b body
ctx, errResp := h.app.StartRequest(g, &u, nil, &b, nil) ctx, g, errResp := pctx.URI(&u).Body(&b).Start()
if errResp != nil { if errResp != nil {
return *errResp return *errResp
} }
defer ctx.Cancel() defer ctx.Cancel()
if permResp := ctx.CheckPermissionUserAdmin(u.UserID); permResp != nil { return h.app.DoRequest(ctx, g, models.TLockReadWrite, func(ctx *logic.AppContext, finishSuccess func(r ginext.HTTPResponse) ginext.HTTPResponse) ginext.HTTPResponse {
return *permResp
}
if b.Name == "" { if permResp := ctx.CheckPermissionUserAdmin(u.UserID); permResp != nil {
return ginresp.APIError(g, 400, apierr.INVALID_BODY_PARAM, "Missing parameter: name", nil) return *permResp
}
channelDisplayName := h.app.NormalizeChannelDisplayName(b.Name)
channelInternalName := h.app.NormalizeChannelInternalName(b.Name)
channelExisting, err := h.database.GetChannelByName(ctx, u.UserID, channelInternalName)
if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query channel", err)
}
user, err := h.database.GetUser(ctx, u.UserID)
if errors.Is(err, sql.ErrNoRows) {
return ginresp.APIError(g, 400, apierr.USER_NOT_FOUND, "User not found", nil)
}
if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query user", err)
}
if len(channelDisplayName) > user.MaxChannelNameLength() {
return ginresp.APIError(g, 400, apierr.CHANNEL_TOO_LONG, fmt.Sprintf("Channel too long (max %d characters)", user.MaxChannelNameLength()), nil)
}
if len(strings.TrimSpace(channelDisplayName)) == 0 {
return ginresp.APIError(g, 400, apierr.CHANNEL_NAME_EMPTY, fmt.Sprintf("Channel displayname cannot be empty"), nil)
}
if len(channelInternalName) > user.MaxChannelNameLength() {
return ginresp.APIError(g, 400, apierr.CHANNEL_TOO_LONG, fmt.Sprintf("Channel too long (max %d characters)", user.MaxChannelNameLength()), nil)
}
if len(strings.TrimSpace(channelInternalName)) == 0 {
return ginresp.APIError(g, 400, apierr.CHANNEL_NAME_EMPTY, fmt.Sprintf("Channel internalname cannot be empty"), nil)
}
if channelExisting != nil {
return ginresp.APIError(g, 409, apierr.CHANNEL_ALREADY_EXISTS, "Channel with this name already exists", nil)
}
subscribeKey := h.app.GenerateRandomAuthKey()
channel, err := h.database.CreateChannel(ctx, u.UserID, channelDisplayName, channelInternalName, subscribeKey, b.Description)
if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to create channel", err)
}
if langext.Coalesce(b.Subscribe, true) {
sub, err := h.database.CreateSubscription(ctx, u.UserID, channel, true)
if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to create subscription", err)
} }
return ctx.FinishSuccess(ginresp.JSON(http.StatusOK, channel.WithSubscription(langext.Ptr(sub)).JSON(true))) if b.Name == "" {
return ginresp.APIError(g, 400, apierr.INVALID_BODY_PARAM, "Missing parameter: name", nil)
}
} else { channelDisplayName := h.app.NormalizeChannelDisplayName(b.Name)
channelInternalName := h.app.NormalizeChannelInternalName(b.Name)
return ctx.FinishSuccess(ginresp.JSON(http.StatusOK, channel.WithSubscription(nil).JSON(true))) channelExisting, err := h.database.GetChannelByName(ctx, u.UserID, channelInternalName)
if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query channel", err)
}
} user, err := h.database.GetUser(ctx, u.UserID)
if errors.Is(err, sql.ErrNoRows) {
return ginresp.APIError(g, 400, apierr.USER_NOT_FOUND, "User not found", nil)
}
if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query user", err)
}
if len(channelDisplayName) > user.MaxChannelNameLength() {
return ginresp.APIError(g, 400, apierr.CHANNEL_TOO_LONG, fmt.Sprintf("Channel too long (max %d characters)", user.MaxChannelNameLength()), nil)
}
if len(strings.TrimSpace(channelDisplayName)) == 0 {
return ginresp.APIError(g, 400, apierr.CHANNEL_NAME_EMPTY, fmt.Sprintf("Channel displayname cannot be empty"), nil)
}
if len(channelInternalName) > user.MaxChannelNameLength() {
return ginresp.APIError(g, 400, apierr.CHANNEL_TOO_LONG, fmt.Sprintf("Channel too long (max %d characters)", user.MaxChannelNameLength()), nil)
}
if len(strings.TrimSpace(channelInternalName)) == 0 {
return ginresp.APIError(g, 400, apierr.CHANNEL_NAME_EMPTY, fmt.Sprintf("Channel internalname cannot be empty"), nil)
}
if channelExisting != nil {
return ginresp.APIError(g, 409, apierr.CHANNEL_ALREADY_EXISTS, "Channel with this name already exists", nil)
}
subscribeKey := h.app.GenerateRandomAuthKey()
channel, err := h.database.CreateChannel(ctx, u.UserID, channelDisplayName, channelInternalName, subscribeKey, b.Description)
if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to create channel", err)
}
if langext.Coalesce(b.Subscribe, true) {
sub, err := h.database.CreateSubscription(ctx, u.UserID, channel, true)
if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to create subscription", err)
}
return finishSuccess(ginext.JSONWithFilter(http.StatusOK, channel.WithSubscription(langext.Ptr(sub)), "INCLUDE_KEY"))
} else {
return finishSuccess(ginext.JSONWithFilter(http.StatusOK, channel.WithSubscription(nil), "INCLUDE_KEY"))
}
})
} }
// UpdateChannel swaggerdoc // UpdateChannel swaggerdoc
@@ -270,14 +278,14 @@ func (h APIHandler) CreateChannel(g *gin.Context) ginresp.HTTPResponse {
// @Param send_key body string false "Send `true` to create a new send_key" // @Param send_key body string false "Send `true` to create a new send_key"
// @Param display_name body string false "Change the cahnnel display-name (only chnages to lowercase/uppercase are allowed - internal_name must stay the same)" // @Param display_name body string false "Change the cahnnel display-name (only chnages to lowercase/uppercase are allowed - internal_name must stay the same)"
// //
// @Success 200 {object} models.ChannelWithSubscriptionJSON // @Success 200 {object} models.ChannelWithSubscription
// @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 401 {object} ginresp.apiError "user is not authorized / has missing permissions" // @Failure 401 {object} ginresp.apiError "user is not authorized / has missing permissions"
// @Failure 404 {object} ginresp.apiError "channel not found" // @Failure 404 {object} ginresp.apiError "channel not found"
// @Failure 500 {object} ginresp.apiError "internal server error" // @Failure 500 {object} ginresp.apiError "internal server error"
// //
// @Router /api/v2/users/{uid}/channels/{cid} [PATCH] // @Router /api/v2/users/{uid}/channels/{cid} [PATCH]
func (h APIHandler) UpdateChannel(g *gin.Context) ginresp.HTTPResponse { func (h APIHandler) UpdateChannel(pctx ginext.PreContext) ginext.HTTPResponse {
type uri struct { type uri struct {
UserID models.UserID `uri:"uid" binding:"entityid"` UserID models.UserID `uri:"uid" binding:"entityid"`
ChannelID models.ChannelID `uri:"cid" binding:"entityid"` ChannelID models.ChannelID `uri:"cid" binding:"entityid"`
@@ -290,84 +298,88 @@ func (h APIHandler) UpdateChannel(g *gin.Context) ginresp.HTTPResponse {
var u uri var u uri
var b body var b body
ctx, errResp := h.app.StartRequest(g, &u, nil, &b, nil) ctx, g, errResp := pctx.URI(&u).Body(&b).Start()
if errResp != nil { if errResp != nil {
return *errResp return *errResp
} }
defer ctx.Cancel() defer ctx.Cancel()
if permResp := ctx.CheckPermissionUserAdmin(u.UserID); permResp != nil { return h.app.DoRequest(ctx, g, models.TLockReadWrite, func(ctx *logic.AppContext, finishSuccess func(r ginext.HTTPResponse) ginext.HTTPResponse) ginext.HTTPResponse {
return *permResp
}
_, err := h.database.GetChannel(ctx, u.UserID, u.ChannelID, true) if permResp := ctx.CheckPermissionUserAdmin(u.UserID); permResp != nil {
if errors.Is(err, sql.ErrNoRows) { return *permResp
return ginresp.APIError(g, 404, apierr.CHANNEL_NOT_FOUND, "Channel not found", err) }
}
if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query channel", err)
}
user, err := h.database.GetUser(ctx, u.UserID) _, err := h.database.GetChannel(ctx, u.UserID, u.ChannelID, true)
if errors.Is(err, sql.ErrNoRows) { if errors.Is(err, sql.ErrNoRows) {
return ginresp.APIError(g, 400, apierr.USER_NOT_FOUND, "User not found", nil) return ginresp.APIError(g, 404, apierr.CHANNEL_NOT_FOUND, "Channel not found", err)
} }
if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query user", err)
}
if langext.Coalesce(b.RefreshSubscribeKey, false) {
newkey := h.app.GenerateRandomAuthKey()
err := h.database.UpdateChannelSubscribeKey(ctx, u.ChannelID, newkey)
if err != nil { if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to update channel", err) return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query channel", err)
}
}
if b.DisplayName != nil {
newDisplayName := h.app.NormalizeChannelDisplayName(*b.DisplayName)
if len(newDisplayName) > user.MaxChannelNameLength() {
return ginresp.APIError(g, 400, apierr.CHANNEL_TOO_LONG, fmt.Sprintf("Channel too long (max %d characters)", user.MaxChannelNameLength()), nil)
} }
if len(strings.TrimSpace(newDisplayName)) == 0 { user, err := h.database.GetUser(ctx, u.UserID)
return ginresp.APIError(g, 400, apierr.CHANNEL_NAME_EMPTY, fmt.Sprintf("Channel displayname cannot be empty"), nil) if errors.Is(err, sql.ErrNoRows) {
return ginresp.APIError(g, 400, apierr.USER_NOT_FOUND, "User not found", nil)
} }
err := h.database.UpdateChannelDisplayName(ctx, u.ChannelID, newDisplayName)
if err != nil { if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to update channel", err) return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query user", err)
} }
} if langext.Coalesce(b.RefreshSubscribeKey, false) {
newkey := h.app.GenerateRandomAuthKey()
if b.DescriptionName != nil { err := h.database.UpdateChannelSubscribeKey(ctx, u.ChannelID, newkey)
if err != nil {
var descName *string = nil return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to update channel", err)
if strings.TrimSpace(*b.DescriptionName) != "" { }
descName = langext.Ptr(strings.TrimSpace(*b.DescriptionName))
} }
if descName != nil && len(*descName) > user.MaxChannelDescriptionLength() { if b.DisplayName != nil {
return ginresp.APIError(g, 400, apierr.CHANNEL_DESCRIPTION_TOO_LONG, fmt.Sprintf("Channel-Description too long (max %d characters)", user.MaxChannelDescriptionLength()), nil)
newDisplayName := h.app.NormalizeChannelDisplayName(*b.DisplayName)
if len(newDisplayName) > user.MaxChannelNameLength() {
return ginresp.APIError(g, 400, apierr.CHANNEL_TOO_LONG, fmt.Sprintf("Channel too long (max %d characters)", user.MaxChannelNameLength()), nil)
}
if len(strings.TrimSpace(newDisplayName)) == 0 {
return ginresp.APIError(g, 400, apierr.CHANNEL_NAME_EMPTY, fmt.Sprintf("Channel displayname cannot be empty"), nil)
}
err := h.database.UpdateChannelDisplayName(ctx, u.ChannelID, newDisplayName)
if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to update channel", err)
}
} }
err := h.database.UpdateChannelDescriptionName(ctx, u.ChannelID, descName) if b.DescriptionName != nil {
var descName *string = nil
if strings.TrimSpace(*b.DescriptionName) != "" {
descName = langext.Ptr(strings.TrimSpace(*b.DescriptionName))
}
if descName != nil && len(*descName) > user.MaxChannelDescriptionLength() {
return ginresp.APIError(g, 400, apierr.CHANNEL_DESCRIPTION_TOO_LONG, fmt.Sprintf("Channel-Description too long (max %d characters)", user.MaxChannelDescriptionLength()), nil)
}
err := h.database.UpdateChannelDescriptionName(ctx, u.ChannelID, descName)
if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to update channel", err)
}
}
channel, err := h.database.GetChannel(ctx, u.UserID, u.ChannelID, true)
if err != nil { if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to update channel", err) return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query (updated) channel", err)
} }
} return finishSuccess(ginext.JSONWithFilter(http.StatusOK, channel, "INCLUDE_KEY"))
channel, err := h.database.GetChannel(ctx, u.UserID, u.ChannelID, true) })
if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query (updated) channel", err)
}
return ctx.FinishSuccess(ginresp.JSON(http.StatusOK, channel.JSON(true)))
} }
// ListChannelMessages swaggerdoc // ListChannelMessages swaggerdoc
@@ -391,7 +403,7 @@ func (h APIHandler) UpdateChannel(g *gin.Context) ginresp.HTTPResponse {
// @Failure 500 {object} ginresp.apiError "internal server error" // @Failure 500 {object} ginresp.apiError "internal server error"
// //
// @Router /api/v2/users/{uid}/channels/{cid}/messages [GET] // @Router /api/v2/users/{uid}/channels/{cid}/messages [GET]
func (h APIHandler) ListChannelMessages(g *gin.Context) ginresp.HTTPResponse { func (h APIHandler) ListChannelMessages(pctx ginext.PreContext) ginext.HTTPResponse {
type uri struct { type uri struct {
ChannelUserID models.UserID `uri:"uid" binding:"entityid"` ChannelUserID models.UserID `uri:"uid" binding:"entityid"`
ChannelID models.ChannelID `uri:"cid" binding:"entityid"` ChannelID models.ChannelID `uri:"cid" binding:"entityid"`
@@ -403,57 +415,59 @@ func (h APIHandler) ListChannelMessages(g *gin.Context) ginresp.HTTPResponse {
Trimmed *bool `json:"trimmed" form:"trimmed"` Trimmed *bool `json:"trimmed" form:"trimmed"`
} }
type response struct { type response struct {
Messages []models.MessageJSON `json:"messages"` Messages []models.Message `json:"messages"`
NextPageToken string `json:"next_page_token"` NextPageToken string `json:"next_page_token"`
PageSize int `json:"page_size"` PageSize int `json:"page_size"`
} }
var u uri var u uri
var q query var q query
ctx, errResp := h.app.StartRequest(g, &u, &q, nil, nil) ctx, g, errResp := pctx.URI(&u).Query(&q).Start()
if errResp != nil { if errResp != nil {
return *errResp return *errResp
} }
defer ctx.Cancel() defer ctx.Cancel()
trimmed := langext.Coalesce(q.Trimmed, true) return h.app.DoRequest(ctx, g, models.TLockRead, func(ctx *logic.AppContext, finishSuccess func(r ginext.HTTPResponse) ginext.HTTPResponse) ginext.HTTPResponse {
maxPageSize := langext.Conditional(trimmed, 16, 256) trimmed := langext.Coalesce(q.Trimmed, true)
pageSize := mathext.Clamp(langext.Coalesce(q.PageSize, 64), 1, maxPageSize) maxPageSize := langext.Conditional(trimmed, 16, 256)
channel, err := h.database.GetChannel(ctx, u.ChannelUserID, u.ChannelID, false) pageSize := mathext.Clamp(langext.Coalesce(q.PageSize, 64), 1, maxPageSize)
if errors.Is(err, sql.ErrNoRows) {
return ginresp.APIError(g, 404, apierr.CHANNEL_NOT_FOUND, "Channel not found", err)
}
if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query channel", err)
}
if permResp := ctx.CheckPermissionChanMessagesRead(channel.Channel); permResp != nil { channel, err := h.database.GetChannel(ctx, u.ChannelUserID, u.ChannelID, false)
return *permResp if errors.Is(err, sql.ErrNoRows) {
} return ginresp.APIError(g, 404, apierr.CHANNEL_NOT_FOUND, "Channel not found", err)
}
if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query channel", err)
}
tok, err := ct.Decode(langext.Coalesce(q.NextPageToken, "")) if permResp := ctx.CheckPermissionChanMessagesRead(channel.Channel); permResp != nil {
if err != nil { return *permResp
return ginresp.APIError(g, 400, apierr.PAGETOKEN_ERROR, "Failed to decode next_page_token", err) }
}
filter := models.MessageFilter{ tok, err := ct.Decode(langext.Coalesce(q.NextPageToken, ""))
ChannelID: langext.Ptr([]models.ChannelID{channel.ChannelID}), if err != nil {
} return ginresp.APIError(g, 400, apierr.PAGETOKEN_ERROR, "Failed to decode next_page_token", err)
}
messages, npt, err := h.database.ListMessages(ctx, filter, &pageSize, tok) filter := models.MessageFilter{
if err != nil { ChannelID: langext.Ptr([]models.ChannelID{channel.ChannelID}),
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query messages", err) }
}
var res []models.MessageJSON messages, npt, err := h.database.ListMessages(ctx, filter, &pageSize, tok)
if trimmed { if err != nil {
res = langext.ArrMap(messages, func(v models.Message) models.MessageJSON { return v.TrimmedJSON() }) return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query messages", err)
} else { }
res = langext.ArrMap(messages, func(v models.Message) models.MessageJSON { return v.FullJSON() })
}
return ctx.FinishSuccess(ginresp.JSON(http.StatusOK, response{Messages: res, NextPageToken: npt.Token(), PageSize: pageSize})) if trimmed {
res := langext.ArrMap(messages, func(v models.Message) models.Message { return v.Trim() })
return finishSuccess(ginext.JSON(http.StatusOK, response{Messages: res, NextPageToken: npt.Token(), PageSize: pageSize}))
} else {
return finishSuccess(ginext.JSON(http.StatusOK, response{Messages: messages, NextPageToken: npt.Token(), PageSize: pageSize}))
}
})
} }
+129 -110
View File
@@ -3,10 +3,11 @@ package handler
import ( import (
"blackforestbytes.com/simplecloudnotifier/api/apierr" "blackforestbytes.com/simplecloudnotifier/api/apierr"
"blackforestbytes.com/simplecloudnotifier/api/ginresp" "blackforestbytes.com/simplecloudnotifier/api/ginresp"
"blackforestbytes.com/simplecloudnotifier/logic"
"blackforestbytes.com/simplecloudnotifier/models" "blackforestbytes.com/simplecloudnotifier/models"
"database/sql" "database/sql"
"errors" "errors"
"github.com/gin-gonic/gin" "gogs.mikescher.com/BlackForestBytes/goext/ginext"
"gogs.mikescher.com/BlackForestBytes/goext/langext" "gogs.mikescher.com/BlackForestBytes/goext/langext"
"net/http" "net/http"
) )
@@ -25,33 +26,35 @@ import (
// @Failure 500 {object} ginresp.apiError "internal server error" // @Failure 500 {object} ginresp.apiError "internal server error"
// //
// @Router /api/v2/users/{uid}/clients [GET] // @Router /api/v2/users/{uid}/clients [GET]
func (h APIHandler) ListClients(g *gin.Context) ginresp.HTTPResponse { func (h APIHandler) ListClients(pctx ginext.PreContext) ginext.HTTPResponse {
type uri struct { type uri struct {
UserID models.UserID `uri:"uid" binding:"entityid"` UserID models.UserID `uri:"uid" binding:"entityid"`
} }
type response struct { type response struct {
Clients []models.ClientJSON `json:"clients"` Clients []models.Client `json:"clients"`
} }
var u uri var u uri
ctx, errResp := h.app.StartRequest(g, &u, nil, nil, nil) ctx, g, errResp := pctx.URI(&u).Start()
if errResp != nil { if errResp != nil {
return *errResp return *errResp
} }
defer ctx.Cancel() defer ctx.Cancel()
if permResp := ctx.CheckPermissionUserRead(u.UserID); permResp != nil { return h.app.DoRequest(ctx, g, models.TLockRead, func(ctx *logic.AppContext, finishSuccess func(r ginext.HTTPResponse) ginext.HTTPResponse) ginext.HTTPResponse {
return *permResp
}
clients, err := h.database.ListClients(ctx, u.UserID) if permResp := ctx.CheckPermissionUserRead(u.UserID); permResp != nil {
if err != nil { return *permResp
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query clients", err) }
}
res := langext.ArrMap(clients, func(v models.Client) models.ClientJSON { return v.JSON() }) clients, err := h.database.ListClients(ctx, u.UserID)
if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query clients", err)
}
return ctx.FinishSuccess(ginresp.JSON(http.StatusOK, response{Clients: res})) return finishSuccess(ginext.JSON(http.StatusOK, response{Clients: clients}))
})
} }
// GetClient swaggerdoc // GetClient swaggerdoc
@@ -63,39 +66,43 @@ func (h APIHandler) ListClients(g *gin.Context) ginresp.HTTPResponse {
// @Param uid path string true "UserID" // @Param uid path string true "UserID"
// @Param cid path string true "ClientID" // @Param cid path string true "ClientID"
// //
// @Success 200 {object} models.ClientJSON // @Success 200 {object} models.Client
// @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 401 {object} ginresp.apiError "user is not authorized / has missing permissions" // @Failure 401 {object} ginresp.apiError "user is not authorized / has missing permissions"
// @Failure 404 {object} ginresp.apiError "client not found" // @Failure 404 {object} ginresp.apiError "client not found"
// @Failure 500 {object} ginresp.apiError "internal server error" // @Failure 500 {object} ginresp.apiError "internal server error"
// //
// @Router /api/v2/users/{uid}/clients/{cid} [GET] // @Router /api/v2/users/{uid}/clients/{cid} [GET]
func (h APIHandler) GetClient(g *gin.Context) ginresp.HTTPResponse { func (h APIHandler) GetClient(pctx ginext.PreContext) ginext.HTTPResponse {
type uri struct { type uri struct {
UserID models.UserID `uri:"uid" binding:"entityid"` UserID models.UserID `uri:"uid" binding:"entityid"`
ClientID models.ClientID `uri:"cid" binding:"entityid"` ClientID models.ClientID `uri:"cid" binding:"entityid"`
} }
var u uri var u uri
ctx, errResp := h.app.StartRequest(g, &u, nil, nil, nil) ctx, g, errResp := pctx.URI(&u).Start()
if errResp != nil { if errResp != nil {
return *errResp return *errResp
} }
defer ctx.Cancel() defer ctx.Cancel()
if permResp := ctx.CheckPermissionUserRead(u.UserID); permResp != nil { return h.app.DoRequest(ctx, g, models.TLockRead, func(ctx *logic.AppContext, finishSuccess func(r ginext.HTTPResponse) ginext.HTTPResponse) ginext.HTTPResponse {
return *permResp
}
client, err := h.database.GetClient(ctx, u.UserID, u.ClientID) if permResp := ctx.CheckPermissionUserRead(u.UserID); permResp != nil {
if errors.Is(err, sql.ErrNoRows) { return *permResp
return ginresp.APIError(g, 404, apierr.CLIENT_NOT_FOUND, "Client 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())) client, err := h.database.GetClient(ctx, u.UserID, u.ClientID)
if errors.Is(err, sql.ErrNoRows) {
return ginresp.APIError(g, 404, apierr.CLIENT_NOT_FOUND, "Client not found", err)
}
if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query client", err)
}
return finishSuccess(ginext.JSON(http.StatusOK, client))
})
} }
// AddClient swaggerdoc // AddClient swaggerdoc
@@ -108,13 +115,13 @@ func (h APIHandler) GetClient(g *gin.Context) ginresp.HTTPResponse {
// //
// @Param post_body body handler.AddClient.body false " " // @Param post_body body handler.AddClient.body false " "
// //
// @Success 200 {object} models.ClientJSON // @Success 200 {object} models.Client
// @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 401 {object} ginresp.apiError "user is not authorized / has missing permissions" // @Failure 401 {object} ginresp.apiError "user is not authorized / has missing permissions"
// @Failure 500 {object} ginresp.apiError "internal server error" // @Failure 500 {object} ginresp.apiError "internal server error"
// //
// @Router /api/v2/users/{uid}/clients [POST] // @Router /api/v2/users/{uid}/clients [POST]
func (h APIHandler) AddClient(g *gin.Context) ginresp.HTTPResponse { func (h APIHandler) AddClient(pctx ginext.PreContext) ginext.HTTPResponse {
type uri struct { type uri struct {
UserID models.UserID `uri:"uid" binding:"entityid"` UserID models.UserID `uri:"uid" binding:"entityid"`
} }
@@ -128,32 +135,36 @@ func (h APIHandler) AddClient(g *gin.Context) ginresp.HTTPResponse {
var u uri var u uri
var b body var b body
ctx, errResp := h.app.StartRequest(g, &u, nil, &b, nil) ctx, g, errResp := pctx.URI(&u).Body(&b).Start()
if errResp != nil { if errResp != nil {
return *errResp return *errResp
} }
defer ctx.Cancel() defer ctx.Cancel()
if !b.ClientType.Valid() { return h.app.DoRequest(ctx, g, models.TLockReadWrite, func(ctx *logic.AppContext, finishSuccess func(r ginext.HTTPResponse) ginext.HTTPResponse) ginext.HTTPResponse {
return ginresp.APIError(g, 400, apierr.INVALID_CLIENTTYPE, "Invalid ClientType", nil)
}
clientType := b.ClientType
if permResp := ctx.CheckPermissionUserAdmin(u.UserID); permResp != nil { if !b.ClientType.Valid() {
return *permResp return ginresp.APIError(g, 400, apierr.INVALID_CLIENTTYPE, "Invalid ClientType", nil)
} }
clientType := b.ClientType
err := h.database.DeleteClientsByFCM(ctx, b.FCMToken) if permResp := ctx.CheckPermissionUserAdmin(u.UserID); permResp != nil {
if err != nil { return *permResp
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to delete existing clients in db", err) }
}
client, err := h.database.CreateClient(ctx, u.UserID, clientType, b.FCMToken, b.AgentModel, b.AgentVersion, b.Name) err := h.database.DeleteClientsByFCM(ctx, b.FCMToken)
if err != nil { if err != nil {
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 delete existing clients in db", err)
} }
return ctx.FinishSuccess(ginresp.JSON(http.StatusOK, client.JSON())) client, err := h.database.CreateClient(ctx, u.UserID, clientType, b.FCMToken, b.AgentModel, b.AgentVersion, b.Name)
if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to create client in db", err)
}
return finishSuccess(ginext.JSON(http.StatusOK, client))
})
} }
// DeleteClient swaggerdoc // DeleteClient swaggerdoc
@@ -165,44 +176,48 @@ func (h APIHandler) AddClient(g *gin.Context) ginresp.HTTPResponse {
// @Param uid path string true "UserID" // @Param uid path string true "UserID"
// @Param cid path string true "ClientID" // @Param cid path string true "ClientID"
// //
// @Success 200 {object} models.ClientJSON // @Success 200 {object} models.Client
// @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 401 {object} ginresp.apiError "user is not authorized / has missing permissions" // @Failure 401 {object} ginresp.apiError "user is not authorized / has missing permissions"
// @Failure 404 {object} ginresp.apiError "client not found" // @Failure 404 {object} ginresp.apiError "client not found"
// @Failure 500 {object} ginresp.apiError "internal server error" // @Failure 500 {object} ginresp.apiError "internal server error"
// //
// @Router /api/v2/users/{uid}/clients/{cid} [DELETE] // @Router /api/v2/users/{uid}/clients/{cid} [DELETE]
func (h APIHandler) DeleteClient(g *gin.Context) ginresp.HTTPResponse { func (h APIHandler) DeleteClient(pctx ginext.PreContext) ginext.HTTPResponse {
type uri struct { type uri struct {
UserID models.UserID `uri:"uid" binding:"entityid"` UserID models.UserID `uri:"uid" binding:"entityid"`
ClientID models.ClientID `uri:"cid" binding:"entityid"` ClientID models.ClientID `uri:"cid" binding:"entityid"`
} }
var u uri var u uri
ctx, errResp := h.app.StartRequest(g, &u, nil, nil, nil) ctx, g, errResp := pctx.URI(&u).Start()
if errResp != nil { if errResp != nil {
return *errResp return *errResp
} }
defer ctx.Cancel() defer ctx.Cancel()
if permResp := ctx.CheckPermissionUserAdmin(u.UserID); permResp != nil { return h.app.DoRequest(ctx, g, models.TLockReadWrite, func(ctx *logic.AppContext, finishSuccess func(r ginext.HTTPResponse) ginext.HTTPResponse) ginext.HTTPResponse {
return *permResp
}
client, err := h.database.GetClient(ctx, u.UserID, u.ClientID) if permResp := ctx.CheckPermissionUserAdmin(u.UserID); permResp != nil {
if errors.Is(err, sql.ErrNoRows) { return *permResp
return ginresp.APIError(g, 404, apierr.CLIENT_NOT_FOUND, "Client not found", err) }
}
if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query client", err)
}
err = h.database.DeleteClient(ctx, u.ClientID) client, err := h.database.GetClient(ctx, u.UserID, u.ClientID)
if err != nil { if errors.Is(err, sql.ErrNoRows) {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to delete client", err) return ginresp.APIError(g, 404, apierr.CLIENT_NOT_FOUND, "Client 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())) err = h.database.DeleteClient(ctx, u.ClientID)
if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to delete client", err)
}
return finishSuccess(ginext.JSON(http.StatusOK, client))
})
} }
// UpdateClient swaggerdoc // UpdateClient swaggerdoc
@@ -218,14 +233,14 @@ func (h APIHandler) DeleteClient(g *gin.Context) ginresp.HTTPResponse {
// @Param clientname body string false "Change the clientname (send an empty string to clear it)" // @Param clientname body string false "Change the clientname (send an empty string to clear it)"
// @Param pro_token body string false "Send a verification of premium purchase" // @Param pro_token body string false "Send a verification of premium purchase"
// //
// @Success 200 {object} models.ClientJSON // @Success 200 {object} models.Client
// @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 401 {object} ginresp.apiError "client is not authorized / has missing permissions" // @Failure 401 {object} ginresp.apiError "client is not authorized / has missing permissions"
// @Failure 404 {object} ginresp.apiError "client not found" // @Failure 404 {object} ginresp.apiError "client not found"
// @Failure 500 {object} ginresp.apiError "internal server error" // @Failure 500 {object} ginresp.apiError "internal server error"
// //
// @Router /api/v2/users/{uid}/clients/{cid} [PATCH] // @Router /api/v2/users/{uid}/clients/{cid} [PATCH]
func (h APIHandler) UpdateClient(g *gin.Context) ginresp.HTTPResponse { func (h APIHandler) UpdateClient(pctx ginext.PreContext) ginext.HTTPResponse {
type uri struct { type uri struct {
UserID models.UserID `uri:"uid" binding:"entityid"` UserID models.UserID `uri:"uid" binding:"entityid"`
ClientID models.ClientID `uri:"cid" binding:"entityid"` ClientID models.ClientID `uri:"cid" binding:"entityid"`
@@ -239,69 +254,73 @@ func (h APIHandler) UpdateClient(g *gin.Context) ginresp.HTTPResponse {
var u uri var u uri
var b body var b body
ctx, errResp := h.app.StartRequest(g, &u, nil, &b, nil) ctx, g, errResp := pctx.URI(&u).Body(&b).Start()
if errResp != nil { if errResp != nil {
return *errResp return *errResp
} }
defer ctx.Cancel() defer ctx.Cancel()
if permResp := ctx.CheckPermissionUserAdmin(u.UserID); permResp != nil { return h.app.DoRequest(ctx, g, models.TLockReadWrite, func(ctx *logic.AppContext, finishSuccess func(r ginext.HTTPResponse) ginext.HTTPResponse) ginext.HTTPResponse {
return *permResp
}
client, err := h.database.GetClient(ctx, u.UserID, u.ClientID) if permResp := ctx.CheckPermissionUserAdmin(u.UserID); permResp != nil {
if errors.Is(err, sql.ErrNoRows) { return *permResp
return ginresp.APIError(g, 404, apierr.CLIENT_NOT_FOUND, "Client not found", err)
}
if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query client", err)
}
if b.FCMToken != nil && *b.FCMToken != client.FCMToken {
err = h.database.DeleteClientsByFCM(ctx, *b.FCMToken)
if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to delete existing clients in db", err)
} }
err = h.database.UpdateClientFCMToken(ctx, u.ClientID, *b.FCMToken) client, err := h.database.GetClient(ctx, u.UserID, u.ClientID)
if err != nil { if errors.Is(err, sql.ErrNoRows) {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to update client", err) return ginresp.APIError(g, 404, apierr.CLIENT_NOT_FOUND, "Client not found", err)
} }
}
if b.AgentModel != nil {
err = h.database.UpdateClientAgentModel(ctx, u.ClientID, *b.AgentModel)
if err != nil { if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to update client", err) return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query client", err)
} }
}
if b.AgentVersion != nil { if b.FCMToken != nil && *b.FCMToken != client.FCMToken {
err = h.database.UpdateClientAgentVersion(ctx, u.ClientID, *b.AgentVersion)
if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to update client", err)
}
}
if b.Name != nil { err = h.database.DeleteClientsByFCM(ctx, *b.FCMToken)
if *b.Name == "" {
err = h.database.UpdateClientDescriptionName(ctx, u.ClientID, nil)
if err != nil { if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to update client", err) return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to delete existing clients in db", err)
} }
} else {
err = h.database.UpdateClientDescriptionName(ctx, u.ClientID, langext.Ptr(*b.Name)) err = h.database.UpdateClientFCMToken(ctx, u.ClientID, *b.FCMToken)
if err != nil { if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to update client", err) return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to update client", err)
} }
} }
}
client, err = h.database.GetClient(ctx, u.UserID, u.ClientID) if b.AgentModel != nil {
if err != nil { err = h.database.UpdateClientAgentModel(ctx, u.ClientID, *b.AgentModel)
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query (updated) client", err) if err != nil {
} return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to update client", err)
}
}
return ctx.FinishSuccess(ginresp.JSON(http.StatusOK, client.JSON())) if b.AgentVersion != nil {
err = h.database.UpdateClientAgentVersion(ctx, u.ClientID, *b.AgentVersion)
if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to update client", err)
}
}
if b.Name != nil {
if *b.Name == "" {
err = h.database.UpdateClientDescriptionName(ctx, u.ClientID, nil)
if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to update client", err)
}
} else {
err = h.database.UpdateClientDescriptionName(ctx, u.ClientID, langext.Ptr(*b.Name))
if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to update client", err)
}
}
}
client, err = h.database.GetClient(ctx, u.UserID, u.ClientID)
if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query (updated) client", err)
}
return finishSuccess(ginext.JSON(http.StatusOK, client))
})
} }
+165 -142
View File
@@ -3,10 +3,11 @@ package handler
import ( import (
"blackforestbytes.com/simplecloudnotifier/api/apierr" "blackforestbytes.com/simplecloudnotifier/api/apierr"
"blackforestbytes.com/simplecloudnotifier/api/ginresp" "blackforestbytes.com/simplecloudnotifier/api/ginresp"
"blackforestbytes.com/simplecloudnotifier/logic"
"blackforestbytes.com/simplecloudnotifier/models" "blackforestbytes.com/simplecloudnotifier/models"
"database/sql" "database/sql"
"errors" "errors"
"github.com/gin-gonic/gin" "gogs.mikescher.com/BlackForestBytes/goext/ginext"
"gogs.mikescher.com/BlackForestBytes/goext/langext" "gogs.mikescher.com/BlackForestBytes/goext/langext"
"net/http" "net/http"
) )
@@ -27,33 +28,35 @@ import (
// @Failure 500 {object} ginresp.apiError "internal server error" // @Failure 500 {object} ginresp.apiError "internal server error"
// //
// @Router /api/v2/users/{uid}/keys [GET] // @Router /api/v2/users/{uid}/keys [GET]
func (h APIHandler) ListUserKeys(g *gin.Context) ginresp.HTTPResponse { func (h APIHandler) ListUserKeys(pctx ginext.PreContext) ginext.HTTPResponse {
type uri struct { type uri struct {
UserID models.UserID `uri:"uid" binding:"entityid"` UserID models.UserID `uri:"uid" binding:"entityid"`
} }
type response struct { type response struct {
Keys []models.KeyTokenJSON `json:"keys"` Keys []models.KeyToken `json:"keys"`
} }
var u uri var u uri
ctx, errResp := h.app.StartRequest(g, &u, nil, nil, nil) ctx, g, errResp := pctx.URI(&u).Start()
if errResp != nil { if errResp != nil {
return *errResp return *errResp
} }
defer ctx.Cancel() defer ctx.Cancel()
if permResp := ctx.CheckPermissionUserAdmin(u.UserID); permResp != nil { return h.app.DoRequest(ctx, g, models.TLockRead, func(ctx *logic.AppContext, finishSuccess func(r ginext.HTTPResponse) ginext.HTTPResponse) ginext.HTTPResponse {
return *permResp
}
toks, err := h.database.ListKeyTokens(ctx, u.UserID) if permResp := ctx.CheckPermissionUserAdmin(u.UserID); permResp != nil {
if err != nil { return *permResp
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query keys", err) }
}
res := langext.ArrMap(toks, func(v models.KeyToken) models.KeyTokenJSON { return v.JSON() }) toks, err := h.database.ListKeyTokens(ctx, u.UserID)
if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query keys", err)
}
return ctx.FinishSuccess(ginresp.JSON(http.StatusOK, response{Keys: res})) return finishSuccess(ginext.JSON(http.StatusOK, response{Keys: toks}))
})
} }
// GetCurrentUserKey swaggerdoc // GetCurrentUserKey swaggerdoc
@@ -66,43 +69,47 @@ func (h APIHandler) ListUserKeys(g *gin.Context) ginresp.HTTPResponse {
// @Param uid path string true "UserID" // @Param uid path string true "UserID"
// @Param kid path string true "TokenKeyID" // @Param kid path string true "TokenKeyID"
// //
// @Success 200 {object} models.KeyTokenWithTokenJSON // @Success 200 {object} models.KeyToken
// @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 401 {object} ginresp.apiError "user is not authorized / has missing permissions" // @Failure 401 {object} ginresp.apiError "user is not authorized / has missing permissions"
// @Failure 404 {object} ginresp.apiError "message not found" // @Failure 404 {object} ginresp.apiError "message not found"
// @Failure 500 {object} ginresp.apiError "internal server error" // @Failure 500 {object} ginresp.apiError "internal server error"
// //
// @Router /api/v2/users/{uid}/keys/current [GET] // @Router /api/v2/users/{uid}/keys/current [GET]
func (h APIHandler) GetCurrentUserKey(g *gin.Context) ginresp.HTTPResponse { func (h APIHandler) GetCurrentUserKey(pctx ginext.PreContext) ginext.HTTPResponse {
type uri struct { type uri struct {
UserID models.UserID `uri:"uid" binding:"entityid"` UserID models.UserID `uri:"uid" binding:"entityid"`
} }
var u uri var u uri
ctx, errResp := h.app.StartRequest(g, &u, nil, nil, nil) ctx, g, errResp := pctx.URI(&u).Start()
if errResp != nil { if errResp != nil {
return *errResp return *errResp
} }
defer ctx.Cancel() defer ctx.Cancel()
if permResp := ctx.CheckPermissionAny(); permResp != nil { return h.app.DoRequest(ctx, g, models.TLockRead, func(ctx *logic.AppContext, finishSuccess func(r ginext.HTTPResponse) ginext.HTTPResponse) ginext.HTTPResponse {
return *permResp
}
tokid := ctx.GetPermissionKeyTokenID() if permResp := ctx.CheckPermissionAny(); permResp != nil {
if tokid == nil { return *permResp
return ginresp.APIError(g, 400, apierr.USER_AUTH_FAILED, "Missing KeyTokenID in context", nil) }
}
keytoken, err := h.database.GetKeyToken(ctx, u.UserID, *tokid) tokid := ctx.GetPermissionKeyTokenID()
if errors.Is(err, sql.ErrNoRows) { if tokid == nil {
return ginresp.APIError(g, 404, apierr.KEY_NOT_FOUND, "Key not found", err) return ginresp.APIError(g, 400, apierr.USER_AUTH_FAILED, "Missing KeyTokenID in context", nil)
} }
if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query client", err)
}
return ctx.FinishSuccess(ginresp.JSON(http.StatusOK, keytoken.JSON().WithToken(keytoken.Token))) keytoken, err := h.database.GetKeyToken(ctx, u.UserID, *tokid)
if errors.Is(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 finishSuccess(ginext.JSONWithFilter(http.StatusOK, keytoken, "INCLUDE_TOKEN"))
})
} }
// GetUserKey swaggerdoc // GetUserKey swaggerdoc
@@ -115,39 +122,43 @@ func (h APIHandler) GetCurrentUserKey(g *gin.Context) ginresp.HTTPResponse {
// @Param uid path string true "UserID" // @Param uid path string true "UserID"
// @Param kid path string true "TokenKeyID" // @Param kid path string true "TokenKeyID"
// //
// @Success 200 {object} models.KeyTokenJSON // @Success 200 {object} models.KeyToken
// @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 401 {object} ginresp.apiError "user is not authorized / has missing permissions" // @Failure 401 {object} ginresp.apiError "user is not authorized / has missing permissions"
// @Failure 404 {object} ginresp.apiError "message not found" // @Failure 404 {object} ginresp.apiError "message not found"
// @Failure 500 {object} ginresp.apiError "internal server error" // @Failure 500 {object} ginresp.apiError "internal server error"
// //
// @Router /api/v2/users/{uid}/keys/{kid} [GET] // @Router /api/v2/users/{uid}/keys/{kid} [GET]
func (h APIHandler) GetUserKey(g *gin.Context) ginresp.HTTPResponse { func (h APIHandler) GetUserKey(pctx ginext.PreContext) ginext.HTTPResponse {
type uri struct { type uri struct {
UserID models.UserID `uri:"uid" binding:"entityid"` UserID models.UserID `uri:"uid" binding:"entityid"`
KeyID models.KeyTokenID `uri:"kid" binding:"entityid"` KeyID models.KeyTokenID `uri:"kid" binding:"entityid"`
} }
var u uri var u uri
ctx, errResp := h.app.StartRequest(g, &u, nil, nil, nil) ctx, g, errResp := pctx.URI(&u).Start()
if errResp != nil { if errResp != nil {
return *errResp return *errResp
} }
defer ctx.Cancel() defer ctx.Cancel()
if permResp := ctx.CheckPermissionUserAdmin(u.UserID); permResp != nil { return h.app.DoRequest(ctx, g, models.TLockRead, func(ctx *logic.AppContext, finishSuccess func(r ginext.HTTPResponse) ginext.HTTPResponse) ginext.HTTPResponse {
return *permResp
}
keytoken, err := h.database.GetKeyToken(ctx, u.UserID, u.KeyID) if permResp := ctx.CheckPermissionUserAdmin(u.UserID); permResp != nil {
if errors.Is(err, sql.ErrNoRows) { return *permResp
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, keytoken.JSON())) keytoken, err := h.database.GetKeyToken(ctx, u.UserID, u.KeyID)
if errors.Is(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 finishSuccess(ginext.JSON(http.StatusOK, keytoken))
})
} }
// UpdateUserKey swaggerdoc // UpdateUserKey swaggerdoc
@@ -161,14 +172,14 @@ func (h APIHandler) GetUserKey(g *gin.Context) ginresp.HTTPResponse {
// //
// @Param post_body body handler.UpdateUserKey.body false " " // @Param post_body body handler.UpdateUserKey.body false " "
// //
// @Success 200 {object} models.KeyTokenJSON // @Success 200 {object} models.KeyToken
// @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 401 {object} ginresp.apiError "user is not authorized / has missing permissions" // @Failure 401 {object} ginresp.apiError "user is not authorized / has missing permissions"
// @Failure 404 {object} ginresp.apiError "message not found" // @Failure 404 {object} ginresp.apiError "message not found"
// @Failure 500 {object} ginresp.apiError "internal server error" // @Failure 500 {object} ginresp.apiError "internal server error"
// //
// @Router /api/v2/users/{uid}/keys/{kid} [PATCH] // @Router /api/v2/users/{uid}/keys/{kid} [PATCH]
func (h APIHandler) UpdateUserKey(g *gin.Context) ginresp.HTTPResponse { func (h APIHandler) UpdateUserKey(pctx ginext.PreContext) ginext.HTTPResponse {
type uri struct { type uri struct {
UserID models.UserID `uri:"uid" binding:"entityid"` UserID models.UserID `uri:"uid" binding:"entityid"`
KeyID models.KeyTokenID `uri:"kid" binding:"entityid"` KeyID models.KeyTokenID `uri:"kid" binding:"entityid"`
@@ -182,70 +193,74 @@ func (h APIHandler) UpdateUserKey(g *gin.Context) ginresp.HTTPResponse {
var u uri var u uri
var b body var b body
ctx, errResp := h.app.StartRequest(g, &u, nil, &b, nil) ctx, g, errResp := pctx.URI(&u).Body(&b).Start()
if errResp != nil { if errResp != nil {
return *errResp return *errResp
} }
defer ctx.Cancel() defer ctx.Cancel()
if permResp := ctx.CheckPermissionUserAdmin(u.UserID); permResp != nil { return h.app.DoRequest(ctx, g, models.TLockReadWrite, func(ctx *logic.AppContext, finishSuccess func(r ginext.HTTPResponse) ginext.HTTPResponse) ginext.HTTPResponse {
return *permResp
}
keytoken, err := h.database.GetKeyToken(ctx, u.UserID, u.KeyID) if permResp := ctx.CheckPermissionUserAdmin(u.UserID); permResp != nil {
if errors.Is(err, sql.ErrNoRows) { return *permResp
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 { keytoken, err := h.database.GetKeyToken(ctx, u.UserID, u.KeyID)
err := h.database.UpdateKeyTokenName(ctx, u.KeyID, *b.Name) if errors.Is(err, sql.ErrNoRows) {
return ginresp.APIError(g, 404, apierr.KEY_NOT_FOUND, "Key not found", err)
}
if err != nil { if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to update name", err) return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query client", err)
}
keytoken.Name = *b.Name
}
if b.Permissions != nil {
if keytoken.KeyTokenID == *ctx.GetPermissionKeyTokenID() {
return ginresp.APIError(g, 400, apierr.CANNOT_SELFUPDATE_KEY, "Cannot update the currently used key", err)
} }
permlist := models.ParseTokenPermissionList(*b.Permissions) if b.Name != nil {
err := h.database.UpdateKeyTokenPermissions(ctx, u.KeyID, permlist) err := h.database.UpdateKeyTokenName(ctx, u.KeyID, *b.Name)
if err != nil { if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to update permissions", err) return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to update name", err)
} }
keytoken.Permissions = permlist keytoken.Name = *b.Name
}
if b.AllChannels != nil {
if keytoken.KeyTokenID == *ctx.GetPermissionKeyTokenID() {
return ginresp.APIError(g, 400, apierr.CANNOT_SELFUPDATE_KEY, "Cannot update the currently used key", err)
} }
err := h.database.UpdateKeyTokenAllChannels(ctx, u.KeyID, *b.AllChannels) if b.Permissions != nil {
if err != nil { if keytoken.KeyTokenID == *ctx.GetPermissionKeyTokenID() {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to update all_channels", err) return ginresp.APIError(g, 400, apierr.CANNOT_SELFUPDATE_KEY, "Cannot update the currently used key", err)
} }
keytoken.AllChannels = *b.AllChannels
}
if b.Channels != nil { permlist := models.ParseTokenPermissionList(*b.Permissions)
if keytoken.KeyTokenID == *ctx.GetPermissionKeyTokenID() { err := h.database.UpdateKeyTokenPermissions(ctx, u.KeyID, permlist)
return ginresp.APIError(g, 400, apierr.CANNOT_SELFUPDATE_KEY, "Cannot update the currently used key", err) if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to update permissions", err)
}
keytoken.Permissions = permlist
} }
err := h.database.UpdateKeyTokenChannels(ctx, u.KeyID, *b.Channels) if b.AllChannels != nil {
if err != nil { if keytoken.KeyTokenID == *ctx.GetPermissionKeyTokenID() {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to update channels", err) return ginresp.APIError(g, 400, apierr.CANNOT_SELFUPDATE_KEY, "Cannot update the currently used key", err)
} }
keytoken.Channels = *b.Channels
}
return ctx.FinishSuccess(ginresp.JSON(http.StatusOK, keytoken.JSON())) 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)
}
keytoken.AllChannels = *b.AllChannels
}
if b.Channels != nil {
if keytoken.KeyTokenID == *ctx.GetPermissionKeyTokenID() {
return ginresp.APIError(g, 400, apierr.CANNOT_SELFUPDATE_KEY, "Cannot update the currently used key", err)
}
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)
}
keytoken.Channels = *b.Channels
}
return finishSuccess(ginext.JSON(http.StatusOK, keytoken))
})
} }
// CreateUserKey swaggerdoc // CreateUserKey swaggerdoc
@@ -258,14 +273,14 @@ func (h APIHandler) UpdateUserKey(g *gin.Context) ginresp.HTTPResponse {
// //
// @Param post_body body handler.CreateUserKey.body false " " // @Param post_body body handler.CreateUserKey.body false " "
// //
// @Success 200 {object} models.KeyTokenJSON // @Success 200 {object} models.KeyToken
// @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 401 {object} ginresp.apiError "user is not authorized / has missing permissions" // @Failure 401 {object} ginresp.apiError "user is not authorized / has missing permissions"
// @Failure 404 {object} ginresp.apiError "message not found" // @Failure 404 {object} ginresp.apiError "message not found"
// @Failure 500 {object} ginresp.apiError "internal server error" // @Failure 500 {object} ginresp.apiError "internal server error"
// //
// @Router /api/v2/users/{uid}/keys [POST] // @Router /api/v2/users/{uid}/keys [POST]
func (h APIHandler) CreateUserKey(g *gin.Context) ginresp.HTTPResponse { func (h APIHandler) CreateUserKey(pctx ginext.PreContext) ginext.HTTPResponse {
type uri struct { type uri struct {
UserID models.UserID `uri:"uid" binding:"entityid"` UserID models.UserID `uri:"uid" binding:"entityid"`
} }
@@ -278,43 +293,47 @@ func (h APIHandler) CreateUserKey(g *gin.Context) ginresp.HTTPResponse {
var u uri var u uri
var b body var b body
ctx, errResp := h.app.StartRequest(g, &u, nil, &b, nil) ctx, g, errResp := pctx.URI(&u).Body(&b).Start()
if errResp != nil { if errResp != nil {
return *errResp return *errResp
} }
defer ctx.Cancel() defer ctx.Cancel()
channels := langext.Coalesce(b.Channels, make([]models.ChannelID, 0)) return h.app.DoRequest(ctx, g, models.TLockReadWrite, func(ctx *logic.AppContext, finishSuccess func(r ginext.HTTPResponse) ginext.HTTPResponse) ginext.HTTPResponse {
var allChan bool channels := langext.Coalesce(b.Channels, make([]models.ChannelID, 0))
if b.AllChannels == nil && b.Channels != nil {
allChan = false
} else if b.AllChannels == nil && b.Channels == nil {
allChan = true
} else {
allChan = *b.AllChannels
}
for _, c := range channels { var allChan bool
if err := c.Valid(); err != nil { if b.AllChannels == nil && b.Channels != nil {
return ginresp.APIError(g, 400, apierr.INVALID_BODY_PARAM, "Invalid ChannelID", err) allChan = false
} else if b.AllChannels == nil && b.Channels == nil {
allChan = true
} else {
allChan = *b.AllChannels
} }
}
if permResp := ctx.CheckPermissionUserAdmin(u.UserID); permResp != nil { for _, c := range channels {
return *permResp if err := c.Valid(); err != nil {
} return ginresp.APIError(g, 400, apierr.INVALID_BODY_PARAM, "Invalid ChannelID", err)
}
}
token := h.app.GenerateRandomAuthKey() if permResp := ctx.CheckPermissionUserAdmin(u.UserID); permResp != nil {
return *permResp
}
perms := models.ParseTokenPermissionList(b.Permissions) token := h.app.GenerateRandomAuthKey()
keytok, err := h.database.CreateKeyToken(ctx, b.Name, *ctx.GetPermissionUserID(), allChan, channels, perms, token) perms := models.ParseTokenPermissionList(b.Permissions)
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))) keytok, err := h.database.CreateKeyToken(ctx, b.Name, *ctx.GetPermissionUserID(), allChan, channels, perms, token)
if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to create keytoken in db", err)
}
return finishSuccess(ginext.JSONWithFilter(http.StatusOK, keytok, "INCLUDE_TOKEN"))
})
} }
// DeleteUserKey swaggerdoc // DeleteUserKey swaggerdoc
@@ -327,46 +346,50 @@ func (h APIHandler) CreateUserKey(g *gin.Context) ginresp.HTTPResponse {
// @Param uid path string true "UserID" // @Param uid path string true "UserID"
// @Param kid path string true "TokenKeyID" // @Param kid path string true "TokenKeyID"
// //
// @Success 200 {object} models.KeyTokenJSON // @Success 200 {object} models.KeyToken
// @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 401 {object} ginresp.apiError "user is not authorized / has missing permissions" // @Failure 401 {object} ginresp.apiError "user is not authorized / has missing permissions"
// @Failure 404 {object} ginresp.apiError "message not found" // @Failure 404 {object} ginresp.apiError "message not found"
// @Failure 500 {object} ginresp.apiError "internal server error" // @Failure 500 {object} ginresp.apiError "internal server error"
// //
// @Router /api/v2/users/{uid}/keys/{kid} [DELETE] // @Router /api/v2/users/{uid}/keys/{kid} [DELETE]
func (h APIHandler) DeleteUserKey(g *gin.Context) ginresp.HTTPResponse { func (h APIHandler) DeleteUserKey(pctx ginext.PreContext) ginext.HTTPResponse {
type uri struct { type uri struct {
UserID models.UserID `uri:"uid" binding:"entityid"` UserID models.UserID `uri:"uid" binding:"entityid"`
KeyID models.KeyTokenID `uri:"kid" binding:"entityid"` KeyID models.KeyTokenID `uri:"kid" binding:"entityid"`
} }
var u uri var u uri
ctx, errResp := h.app.StartRequest(g, &u, nil, nil, nil) ctx, g, errResp := pctx.URI(&u).Start()
if errResp != nil { if errResp != nil {
return *errResp return *errResp
} }
defer ctx.Cancel() defer ctx.Cancel()
if permResp := ctx.CheckPermissionUserAdmin(u.UserID); permResp != nil { return h.app.DoRequest(ctx, g, models.TLockReadWrite, func(ctx *logic.AppContext, finishSuccess func(r ginext.HTTPResponse) ginext.HTTPResponse) ginext.HTTPResponse {
return *permResp
}
client, err := h.database.GetKeyToken(ctx, u.UserID, u.KeyID) if permResp := ctx.CheckPermissionUserAdmin(u.UserID); permResp != nil {
if errors.Is(err, sql.ErrNoRows) { return *permResp
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() { client, err := h.database.GetKeyToken(ctx, u.UserID, u.KeyID)
return ginresp.APIError(g, 400, apierr.CANNOT_SELFDELETE_KEY, "Cannot delete the currently used key", err) if errors.Is(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)
}
err = h.database.DeleteKeyToken(ctx, u.KeyID) if u.KeyID == *ctx.GetPermissionKeyTokenID() {
if err != nil { return ginresp.APIError(g, 400, apierr.CANNOT_SELFDELETE_KEY, "Cannot delete the currently used key", err)
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to delete client", err) }
}
return ctx.FinishSuccess(ginresp.JSON(http.StatusOK, client.JSON())) err = h.database.DeleteKeyToken(ctx, u.KeyID)
if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to delete client", err)
}
return finishSuccess(ginext.JSON(http.StatusOK, client))
})
} }
+153 -142
View File
@@ -1,8 +1,10 @@
package handler package handler
import ( import (
"blackforestbytes.com/simplecloudnotifier/logic"
"database/sql" "database/sql"
"errors" "errors"
"gogs.mikescher.com/BlackForestBytes/goext/ginext"
"net/http" "net/http"
"strings" "strings"
"time" "time"
@@ -11,7 +13,6 @@ import (
"blackforestbytes.com/simplecloudnotifier/api/ginresp" "blackforestbytes.com/simplecloudnotifier/api/ginresp"
ct "blackforestbytes.com/simplecloudnotifier/db/cursortoken" ct "blackforestbytes.com/simplecloudnotifier/db/cursortoken"
"blackforestbytes.com/simplecloudnotifier/models" "blackforestbytes.com/simplecloudnotifier/models"
"github.com/gin-gonic/gin"
"gogs.mikescher.com/BlackForestBytes/goext/langext" "gogs.mikescher.com/BlackForestBytes/goext/langext"
"gogs.mikescher.com/BlackForestBytes/goext/mathext" "gogs.mikescher.com/BlackForestBytes/goext/mathext"
) )
@@ -34,7 +35,7 @@ import (
// @Failure 500 {object} ginresp.apiError "internal server error" // @Failure 500 {object} ginresp.apiError "internal server error"
// //
// @Router /api/v2/messages [GET] // @Router /api/v2/messages [GET]
func (h APIHandler) ListMessages(g *gin.Context) ginresp.HTTPResponse { func (h APIHandler) ListMessages(pctx ginext.PreContext) ginext.HTTPResponse {
type query struct { type query struct {
PageSize *int `json:"page_size" form:"page_size"` PageSize *int `json:"page_size" form:"page_size"`
NextPageToken *string `json:"next_page_token" form:"next_page_token"` NextPageToken *string `json:"next_page_token" form:"next_page_token"`
@@ -49,113 +50,115 @@ func (h APIHandler) ListMessages(g *gin.Context) ginresp.HTTPResponse {
KeyTokens []string `json:"used_key" form:"used_key"` KeyTokens []string `json:"used_key" form:"used_key"`
} }
type response struct { type response struct {
Messages []models.MessageJSON `json:"messages"` Messages []models.Message `json:"messages"`
NextPageToken string `json:"next_page_token"` NextPageToken string `json:"next_page_token"`
PageSize int `json:"page_size"` PageSize int `json:"page_size"`
} }
var q query var q query
ctx, errResp := h.app.StartRequest(g, nil, &q, nil, nil) ctx, g, errResp := pctx.Query(&q).Start()
if errResp != nil { if errResp != nil {
return *errResp return *errResp
} }
defer ctx.Cancel() defer ctx.Cancel()
trimmed := langext.Coalesce(q.Trimmed, true) return h.app.DoRequest(ctx, g, models.TLockReadWrite, func(ctx *logic.AppContext, finishSuccess func(r ginext.HTTPResponse) ginext.HTTPResponse) ginext.HTTPResponse {
maxPageSize := langext.Conditional(trimmed, 16, 256) trimmed := langext.Coalesce(q.Trimmed, true)
pageSize := mathext.Clamp(langext.Coalesce(q.PageSize, 64), 1, maxPageSize) maxPageSize := langext.Conditional(trimmed, 16, 256)
if permResp := ctx.CheckPermissionSelfAllMessagesRead(); permResp != nil { pageSize := mathext.Clamp(langext.Coalesce(q.PageSize, 64), 1, maxPageSize)
return *permResp
}
userid := *ctx.GetPermissionUserID() if permResp := ctx.CheckPermissionSelfAllMessagesRead(); permResp != nil {
return *permResp
tok, err := ct.Decode(langext.Coalesce(q.NextPageToken, ""))
if err != nil {
return ginresp.APIError(g, 400, apierr.PAGETOKEN_ERROR, "Failed to decode next_page_token", err)
}
err = h.database.UpdateUserLastRead(ctx, userid)
if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to update last-read", err)
}
filter := models.MessageFilter{
ConfirmedSubscriptionBy: langext.Ptr(userid),
}
if q.Filter != nil && strings.TrimSpace(*q.Filter) != "" {
filter.SearchString = langext.Ptr([]string{strings.TrimSpace(*q.Filter)})
}
if len(q.Channels) != 0 {
filter.ChannelNameCS = langext.Ptr(q.Channels)
}
if len(q.ChannelIDs) != 0 {
cids := make([]models.ChannelID, 0, len(q.ChannelIDs))
for _, v := range q.ChannelIDs {
cid := models.ChannelID(v)
if err = cid.Valid(); err != nil {
return ginresp.APIError(g, 400, apierr.BINDFAIL_QUERY_PARAM, "Invalid channel-id", err)
}
cids = append(cids, cid)
} }
filter.ChannelID = &cids
}
if len(q.Senders) != 0 { userid := *ctx.GetPermissionUserID()
filter.SenderNameCS = langext.Ptr(q.Senders)
}
if q.TimeBefore != nil { tok, err := ct.Decode(langext.Coalesce(q.NextPageToken, ""))
t0, err := time.Parse(time.RFC3339, *q.TimeBefore)
if err != nil { if err != nil {
return ginresp.APIError(g, 400, apierr.BINDFAIL_QUERY_PARAM, "Invalid before-time", err) return ginresp.APIError(g, 400, apierr.PAGETOKEN_ERROR, "Failed to decode next_page_token", err)
} }
filter.TimestampCoalesceBefore = &t0
}
if q.TimeAfter != nil { err = h.database.UpdateUserLastRead(ctx, userid)
t0, err := time.Parse(time.RFC3339, *q.TimeAfter)
if err != nil { if err != nil {
return ginresp.APIError(g, 400, apierr.BINDFAIL_QUERY_PARAM, "Invalid after-time", err) return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to update last-read", err)
} }
filter.TimestampCoalesceAfter = &t0
}
if len(q.Priority) != 0 { filter := models.MessageFilter{
filter.Priority = langext.Ptr(q.Priority) ConfirmedSubscriptionBy: langext.Ptr(userid),
} }
if len(q.KeyTokens) != 0 { if q.Filter != nil && strings.TrimSpace(*q.Filter) != "" {
tids := make([]models.KeyTokenID, 0, len(q.KeyTokens)) filter.SearchString = langext.Ptr([]string{strings.TrimSpace(*q.Filter)})
for _, v := range q.KeyTokens { }
tid := models.KeyTokenID(v)
if err = tid.Valid(); err != nil { if len(q.Channels) != 0 {
return ginresp.APIError(g, 400, apierr.BINDFAIL_QUERY_PARAM, "Invalid keytoken-id", err) filter.ChannelNameCS = langext.Ptr(q.Channels)
}
if len(q.ChannelIDs) != 0 {
cids := make([]models.ChannelID, 0, len(q.ChannelIDs))
for _, v := range q.ChannelIDs {
cid := models.ChannelID(v)
if err = cid.Valid(); err != nil {
return ginresp.APIError(g, 400, apierr.BINDFAIL_QUERY_PARAM, "Invalid channel-id", err)
}
cids = append(cids, cid)
} }
tids = append(tids, tid) filter.ChannelID = &cids
} }
filter.UsedKeyID = &tids
}
messages, npt, err := h.database.ListMessages(ctx, filter, &pageSize, tok) if len(q.Senders) != 0 {
if err != nil { filter.SenderNameCS = langext.Ptr(q.Senders)
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query messages", err) }
}
var res []models.MessageJSON if q.TimeBefore != nil {
if trimmed { t0, err := time.Parse(time.RFC3339, *q.TimeBefore)
res = langext.ArrMap(messages, func(v models.Message) models.MessageJSON { return v.TrimmedJSON() }) if err != nil {
} else { return ginresp.APIError(g, 400, apierr.BINDFAIL_QUERY_PARAM, "Invalid before-time", err)
res = langext.ArrMap(messages, func(v models.Message) models.MessageJSON { return v.FullJSON() }) }
} filter.TimestampCoalesceBefore = &t0
}
return ctx.FinishSuccess(ginresp.JSON(http.StatusOK, response{Messages: res, NextPageToken: npt.Token(), PageSize: pageSize})) if q.TimeAfter != nil {
t0, err := time.Parse(time.RFC3339, *q.TimeAfter)
if err != nil {
return ginresp.APIError(g, 400, apierr.BINDFAIL_QUERY_PARAM, "Invalid after-time", err)
}
filter.TimestampCoalesceAfter = &t0
}
if len(q.Priority) != 0 {
filter.Priority = langext.Ptr(q.Priority)
}
if len(q.KeyTokens) != 0 {
tids := make([]models.KeyTokenID, 0, len(q.KeyTokens))
for _, v := range q.KeyTokens {
tid := models.KeyTokenID(v)
if err = tid.Valid(); err != nil {
return ginresp.APIError(g, 400, apierr.BINDFAIL_QUERY_PARAM, "Invalid keytoken-id", err)
}
tids = append(tids, tid)
}
filter.UsedKeyID = &tids
}
messages, npt, err := h.database.ListMessages(ctx, filter, &pageSize, tok)
if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query messages", err)
}
if trimmed {
res := langext.ArrMap(messages, func(v models.Message) models.Message { return v.PreMarshal().Trim() })
return finishSuccess(ginext.JSON(http.StatusOK, response{Messages: res, NextPageToken: npt.Token(), PageSize: pageSize}))
} else {
res := langext.ArrMap(messages, func(v models.Message) models.Message { return v.PreMarshal() })
return finishSuccess(ginext.JSON(http.StatusOK, response{Messages: res, NextPageToken: npt.Token(), PageSize: pageSize}))
}
})
} }
// GetMessage swaggerdoc // GetMessage swaggerdoc
@@ -169,63 +172,67 @@ func (h APIHandler) ListMessages(g *gin.Context) ginresp.HTTPResponse {
// //
// @Param mid path string true "MessageID" // @Param mid path string true "MessageID"
// //
// @Success 200 {object} models.MessageJSON // @Success 200 {object} models.Message
// @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 401 {object} ginresp.apiError "user is not authorized / has missing permissions" // @Failure 401 {object} ginresp.apiError "user is not authorized / has missing permissions"
// @Failure 404 {object} ginresp.apiError "message not found" // @Failure 404 {object} ginresp.apiError "message not found"
// @Failure 500 {object} ginresp.apiError "internal server error" // @Failure 500 {object} ginresp.apiError "internal server error"
// //
// @Router /api/v2/messages/{mid} [GET] // @Router /api/v2/messages/{mid} [GET]
func (h APIHandler) GetMessage(g *gin.Context) ginresp.HTTPResponse { func (h APIHandler) GetMessage(pctx ginext.PreContext) ginext.HTTPResponse {
type uri struct { type uri struct {
MessageID models.MessageID `uri:"mid" binding:"entityid"` MessageID models.MessageID `uri:"mid" binding:"entityid"`
} }
var u uri var u uri
ctx, errResp := h.app.StartRequest(g, &u, nil, nil, nil) ctx, g, errResp := pctx.URI(&u).Start()
if errResp != nil { if errResp != nil {
return *errResp return *errResp
} }
defer ctx.Cancel() defer ctx.Cancel()
if permResp := ctx.CheckPermissionAny(); permResp != nil { return h.app.DoRequest(ctx, g, models.TLockRead, func(ctx *logic.AppContext, finishSuccess func(r ginext.HTTPResponse) ginext.HTTPResponse) ginext.HTTPResponse {
return *permResp
}
msg, err := h.database.GetMessage(ctx, u.MessageID, false) if permResp := ctx.CheckPermissionAny(); permResp != nil {
if errors.Is(err, sql.ErrNoRows) { return *permResp
return ginresp.APIError(g, 404, apierr.MESSAGE_NOT_FOUND, "message not found", err) }
}
if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query message", err)
}
// either we have direct read permissions (it is our message + read/admin key) msg, err := h.database.GetMessage(ctx, u.MessageID, false)
// or we subscribe (+confirmed) to the channel and have read/admin key if errors.Is(err, sql.ErrNoRows) {
return ginresp.APIError(g, 404, apierr.MESSAGE_NOT_FOUND, "message not found", err)
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)
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 message", err)
}
if sub == nil {
// not subbed
return ginresp.APIError(g, 401, apierr.USER_AUTH_FAILED, "You are not authorized for this action", nil)
}
if !sub.Confirmed {
// sub not confirmed
return ginresp.APIError(g, 401, apierr.USER_AUTH_FAILED, "You are not authorized for this action", nil)
} }
// => perm okay // either we have direct read permissions (it is our message + read/admin key)
return ctx.FinishSuccess(ginresp.JSON(http.StatusOK, msg.FullJSON())) // or we subscribe (+confirmed) to the channel and have read/admin key
}
return ginresp.APIError(g, 401, apierr.USER_AUTH_FAILED, "You are not authorized for this action", nil) if ctx.CheckPermissionMessageRead(msg) {
return finishSuccess(ginext.JSON(http.StatusOK, msg.PreMarshal()))
}
if uid := ctx.GetPermissionUserID(); uid != nil && ctx.CheckPermissionUserRead(*uid) == nil {
sub, err := h.database.GetSubscriptionBySubscriber(ctx, *uid, msg.ChannelID)
if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query subscription", err)
}
if sub == nil {
// not subbed
return ginresp.APIError(g, 401, apierr.USER_AUTH_FAILED, "You are not authorized for this action", nil)
}
if !sub.Confirmed {
// sub not confirmed
return ginresp.APIError(g, 401, apierr.USER_AUTH_FAILED, "You are not authorized for this action", nil)
}
// => perm okay
return finishSuccess(ginext.JSON(http.StatusOK, msg.PreMarshal()))
}
return ginresp.APIError(g, 401, apierr.USER_AUTH_FAILED, "You are not authorized for this action", nil)
})
} }
// DeleteMessage swaggerdoc // DeleteMessage swaggerdoc
@@ -237,50 +244,54 @@ func (h APIHandler) GetMessage(g *gin.Context) ginresp.HTTPResponse {
// //
// @Param mid path string true "MessageID" // @Param mid path string true "MessageID"
// //
// @Success 200 {object} models.MessageJSON // @Success 200 {object} models.Message
// @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 401 {object} ginresp.apiError "user is not authorized / has missing permissions" // @Failure 401 {object} ginresp.apiError "user is not authorized / has missing permissions"
// @Failure 404 {object} ginresp.apiError "message not found" // @Failure 404 {object} ginresp.apiError "message not found"
// @Failure 500 {object} ginresp.apiError "internal server error" // @Failure 500 {object} ginresp.apiError "internal server error"
// //
// @Router /api/v2/messages/{mid} [DELETE] // @Router /api/v2/messages/{mid} [DELETE]
func (h APIHandler) DeleteMessage(g *gin.Context) ginresp.HTTPResponse { func (h APIHandler) DeleteMessage(pctx ginext.PreContext) ginext.HTTPResponse {
type uri struct { type uri struct {
MessageID models.MessageID `uri:"mid" binding:"entityid"` MessageID models.MessageID `uri:"mid" binding:"entityid"`
} }
var u uri var u uri
ctx, errResp := h.app.StartRequest(g, &u, nil, nil, nil) ctx, g, errResp := pctx.URI(&u).Start()
if errResp != nil { if errResp != nil {
return *errResp return *errResp
} }
defer ctx.Cancel() defer ctx.Cancel()
if permResp := ctx.CheckPermissionAny(); permResp != nil { return h.app.DoRequest(ctx, g, models.TLockReadWrite, func(ctx *logic.AppContext, finishSuccess func(r ginext.HTTPResponse) ginext.HTTPResponse) ginext.HTTPResponse {
return *permResp
}
msg, err := h.database.GetMessage(ctx, u.MessageID, false) if permResp := ctx.CheckPermissionAny(); permResp != nil {
if errors.Is(err, sql.ErrNoRows) { return *permResp
return ginresp.APIError(g, 404, apierr.MESSAGE_NOT_FOUND, "message not found", err) }
}
if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query message", err)
}
if !ctx.CheckPermissionMessageDelete(msg) { msg, err := h.database.GetMessage(ctx, u.MessageID, false)
return ginresp.APIError(g, 401, apierr.USER_AUTH_FAILED, "You are not authorized for this action", nil) if errors.Is(err, sql.ErrNoRows) {
} return ginresp.APIError(g, 404, apierr.MESSAGE_NOT_FOUND, "message not found", err)
}
if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query message", err)
}
err = h.database.DeleteMessage(ctx, msg.MessageID) if !ctx.CheckPermissionMessageDelete(msg) {
if err != nil { return ginresp.APIError(g, 401, apierr.USER_AUTH_FAILED, "You are not authorized for this action", nil)
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to delete message", err) }
}
err = h.database.CancelPendingDeliveries(ctx, msg.MessageID) err = h.database.DeleteMessage(ctx, msg.MessageID)
if err != nil { if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to cancel deliveries", err) return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to delete message", err)
} }
return ctx.FinishSuccess(ginresp.JSON(http.StatusOK, msg.FullJSON())) err = h.database.CancelPendingDeliveries(ctx, msg.MessageID)
if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to cancel deliveries", err)
}
return finishSuccess(ginext.JSON(http.StatusOK, msg.PreMarshal()))
})
} }
+56 -43
View File
@@ -3,10 +3,11 @@ package handler
import ( import (
"blackforestbytes.com/simplecloudnotifier/api/apierr" "blackforestbytes.com/simplecloudnotifier/api/apierr"
"blackforestbytes.com/simplecloudnotifier/api/ginresp" "blackforestbytes.com/simplecloudnotifier/api/ginresp"
"blackforestbytes.com/simplecloudnotifier/logic"
"blackforestbytes.com/simplecloudnotifier/models" "blackforestbytes.com/simplecloudnotifier/models"
"database/sql" "database/sql"
"errors" "errors"
"github.com/gin-gonic/gin" "gogs.mikescher.com/BlackForestBytes/goext/ginext"
"net/http" "net/http"
) )
@@ -18,38 +19,42 @@ import (
// //
// @Param uid path string true "UserID" // @Param uid path string true "UserID"
// //
// @Success 200 {object} models.UserPreviewJSON // @Success 200 {object} models.UserPreview
// @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 401 {object} ginresp.apiError "user is not authorized / has missing permissions" // @Failure 401 {object} ginresp.apiError "user is not authorized / has missing permissions"
// @Failure 404 {object} ginresp.apiError "user not found" // @Failure 404 {object} ginresp.apiError "user not found"
// @Failure 500 {object} ginresp.apiError "internal server error" // @Failure 500 {object} ginresp.apiError "internal server error"
// //
// @Router /api/v2/preview/users/{uid} [GET] // @Router /api/v2/preview/users/{uid} [GET]
func (h APIHandler) GetUserPreview(g *gin.Context) ginresp.HTTPResponse { func (h APIHandler) GetUserPreview(pctx ginext.PreContext) ginext.HTTPResponse {
type uri struct { type uri struct {
UserID models.UserID `uri:"uid" binding:"entityid"` UserID models.UserID `uri:"uid" binding:"entityid"`
} }
var u uri var u uri
ctx, errResp := h.app.StartRequest(g, &u, nil, nil, nil) ctx, g, errResp := pctx.URI(&u).Start()
if errResp != nil { if errResp != nil {
return *errResp return *errResp
} }
defer ctx.Cancel() defer ctx.Cancel()
if permResp := ctx.CheckPermissionAny(); permResp != nil { return h.app.DoRequest(ctx, g, models.TLockRead, func(ctx *logic.AppContext, finishSuccess func(r ginext.HTTPResponse) ginext.HTTPResponse) ginext.HTTPResponse {
return *permResp
}
user, err := h.database.GetUser(ctx, u.UserID) if permResp := ctx.CheckPermissionAny(); permResp != nil {
if errors.Is(err, sql.ErrNoRows) { return *permResp
return ginresp.APIError(g, 404, apierr.USER_NOT_FOUND, "User not found", err) }
}
if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query user", err)
}
return ctx.FinishSuccess(ginresp.JSON(http.StatusOK, user.JSONPreview())) user, err := h.database.GetUser(ctx, u.UserID)
if errors.Is(err, sql.ErrNoRows) {
return ginresp.APIError(g, 404, apierr.USER_NOT_FOUND, "User not found", err)
}
if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query user", err)
}
return finishSuccess(ginext.JSON(http.StatusOK, user.JSONPreview()))
})
} }
// GetChannelPreview swaggerdoc // GetChannelPreview swaggerdoc
@@ -60,38 +65,42 @@ func (h APIHandler) GetUserPreview(g *gin.Context) ginresp.HTTPResponse {
// //
// @Param cid path string true "ChannelID" // @Param cid path string true "ChannelID"
// //
// @Success 200 {object} models.ChannelPreviewJSON // @Success 200 {object} models.ChannelPreview
// @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 401 {object} ginresp.apiError "user is not authorized / has missing permissions" // @Failure 401 {object} ginresp.apiError "user is not authorized / has missing permissions"
// @Failure 404 {object} ginresp.apiError "channel not found" // @Failure 404 {object} ginresp.apiError "channel not found"
// @Failure 500 {object} ginresp.apiError "internal server error" // @Failure 500 {object} ginresp.apiError "internal server error"
// //
// @Router /api/v2/preview/channels/{cid} [GET] // @Router /api/v2/preview/channels/{cid} [GET]
func (h APIHandler) GetChannelPreview(g *gin.Context) ginresp.HTTPResponse { func (h APIHandler) GetChannelPreview(pctx ginext.PreContext) ginext.HTTPResponse {
type uri struct { type uri struct {
ChannelID models.ChannelID `uri:"cid" binding:"entityid"` ChannelID models.ChannelID `uri:"cid" binding:"entityid"`
} }
var u uri var u uri
ctx, errResp := h.app.StartRequest(g, &u, nil, nil, nil) ctx, g, errResp := pctx.URI(&u).Start()
if errResp != nil { if errResp != nil {
return *errResp return *errResp
} }
defer ctx.Cancel() defer ctx.Cancel()
if permResp := ctx.CheckPermissionAny(); permResp != nil { return h.app.DoRequest(ctx, g, models.TLockRead, func(ctx *logic.AppContext, finishSuccess func(r ginext.HTTPResponse) ginext.HTTPResponse) ginext.HTTPResponse {
return *permResp
}
channel, err := h.database.GetChannelByID(ctx, u.ChannelID) if permResp := ctx.CheckPermissionAny(); permResp != nil {
if errors.Is(err, sql.ErrNoRows) { return *permResp
return ginresp.APIError(g, 404, apierr.CHANNEL_NOT_FOUND, "Channel not found", err) }
}
if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query channel", err)
}
return ctx.FinishSuccess(ginresp.JSON(http.StatusOK, channel.JSONPreview())) channel, err := h.database.GetChannelByID(ctx, u.ChannelID)
if errors.Is(err, sql.ErrNoRows) {
return ginresp.APIError(g, 404, apierr.CHANNEL_NOT_FOUND, "Channel not found", err)
}
if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query channel", err)
}
return finishSuccess(ginext.JSON(http.StatusOK, channel.Preview()))
})
} }
// GetUserKeyPreview swaggerdoc // GetUserKeyPreview swaggerdoc
@@ -102,36 +111,40 @@ func (h APIHandler) GetChannelPreview(g *gin.Context) ginresp.HTTPResponse {
// //
// @Param kid path string true "TokenKeyID" // @Param kid path string true "TokenKeyID"
// //
// @Success 200 {object} models.KeyTokenPreviewJSON // @Success 200 {object} models.KeyTokenPreview
// @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 401 {object} ginresp.apiError "user is not authorized / has missing permissions" // @Failure 401 {object} ginresp.apiError "user is not authorized / has missing permissions"
// @Failure 404 {object} ginresp.apiError "message not found" // @Failure 404 {object} ginresp.apiError "message not found"
// @Failure 500 {object} ginresp.apiError "internal server error" // @Failure 500 {object} ginresp.apiError "internal server error"
// //
// @Router /api/v2/preview/keys/{kid} [GET] // @Router /api/v2/preview/keys/{kid} [GET]
func (h APIHandler) GetUserKeyPreview(g *gin.Context) ginresp.HTTPResponse { func (h APIHandler) GetUserKeyPreview(pctx ginext.PreContext) ginext.HTTPResponse {
type uri struct { type uri struct {
KeyID models.KeyTokenID `uri:"kid" binding:"entityid"` KeyID models.KeyTokenID `uri:"kid" binding:"entityid"`
} }
var u uri var u uri
ctx, errResp := h.app.StartRequest(g, &u, nil, nil, nil) ctx, g, errResp := pctx.URI(&u).Start()
if errResp != nil { if errResp != nil {
return *errResp return *errResp
} }
defer ctx.Cancel() defer ctx.Cancel()
if permResp := ctx.CheckPermissionAny(); permResp != nil { return h.app.DoRequest(ctx, g, models.TLockRead, func(ctx *logic.AppContext, finishSuccess func(r ginext.HTTPResponse) ginext.HTTPResponse) ginext.HTTPResponse {
return *permResp
}
keytoken, err := h.database.GetKeyTokenByID(ctx, u.KeyID) if permResp := ctx.CheckPermissionAny(); permResp != nil {
if errors.Is(err, sql.ErrNoRows) { return *permResp
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, keytoken.JSONPreview())) keytoken, err := h.database.GetKeyTokenByID(ctx, u.KeyID)
if errors.Is(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 finishSuccess(ginext.JSON(http.StatusOK, keytoken.Preview()))
})
} }
+215 -194
View File
@@ -3,10 +3,11 @@ package handler
import ( import (
"blackforestbytes.com/simplecloudnotifier/api/apierr" "blackforestbytes.com/simplecloudnotifier/api/apierr"
"blackforestbytes.com/simplecloudnotifier/api/ginresp" "blackforestbytes.com/simplecloudnotifier/api/ginresp"
"blackforestbytes.com/simplecloudnotifier/logic"
"blackforestbytes.com/simplecloudnotifier/models" "blackforestbytes.com/simplecloudnotifier/models"
"database/sql" "database/sql"
"errors" "errors"
"github.com/gin-gonic/gin" "gogs.mikescher.com/BlackForestBytes/goext/ginext"
"gogs.mikescher.com/BlackForestBytes/goext/langext" "gogs.mikescher.com/BlackForestBytes/goext/langext"
"net/http" "net/http"
"strings" "strings"
@@ -47,7 +48,7 @@ import (
// @Failure 500 {object} ginresp.apiError "internal server error" // @Failure 500 {object} ginresp.apiError "internal server error"
// //
// @Router /api/v2/users/{uid}/subscriptions [GET] // @Router /api/v2/users/{uid}/subscriptions [GET]
func (h APIHandler) ListUserSubscriptions(g *gin.Context) ginresp.HTTPResponse { func (h APIHandler) ListUserSubscriptions(pctx ginext.PreContext) ginext.HTTPResponse {
type uri struct { type uri struct {
UserID models.UserID `uri:"uid" binding:"entityid"` UserID models.UserID `uri:"uid" binding:"entityid"`
} }
@@ -59,76 +60,78 @@ func (h APIHandler) ListUserSubscriptions(g *gin.Context) ginresp.HTTPResponse {
ChannelOwnerUserID *models.UserID `json:"channel_owner_user_id" form:"channel_owner_user_id"` ChannelOwnerUserID *models.UserID `json:"channel_owner_user_id" form:"channel_owner_user_id"`
} }
type response struct { type response struct {
Subscriptions []models.SubscriptionJSON `json:"subscriptions"` Subscriptions []models.Subscription `json:"subscriptions"`
} }
var u uri var u uri
var q query var q query
ctx, errResp := h.app.StartRequest(g, &u, &q, nil, nil) ctx, g, errResp := pctx.URI(&u).Query(&q).Start()
if errResp != nil { if errResp != nil {
return *errResp return *errResp
} }
defer ctx.Cancel() defer ctx.Cancel()
if permResp := ctx.CheckPermissionUserRead(u.UserID); permResp != nil { return h.app.DoRequest(ctx, g, models.TLockRead, func(ctx *logic.AppContext, finishSuccess func(r ginext.HTTPResponse) ginext.HTTPResponse) ginext.HTTPResponse {
return *permResp
}
filter := models.SubscriptionFilter{} if permResp := ctx.CheckPermissionUserRead(u.UserID); permResp != nil {
filter.AnyUserID = langext.Ptr(u.UserID) return *permResp
if q.Direction != nil {
if strings.EqualFold(*q.Direction, "incoming") {
filter.ChannelOwnerUserID = langext.Ptr([]models.UserID{u.UserID})
} else if strings.EqualFold(*q.Direction, "outgoing") {
filter.SubscriberUserID = langext.Ptr([]models.UserID{u.UserID})
} else if strings.EqualFold(*q.Direction, "both") {
// both
} else {
return ginresp.APIError(g, 400, apierr.BINDFAIL_QUERY_PARAM, "Invalid value for param 'direction'", nil)
} }
}
if q.Confirmation != nil { filter := models.SubscriptionFilter{}
if strings.EqualFold(*q.Confirmation, "confirmed") { filter.AnyUserID = langext.Ptr(u.UserID)
filter.Confirmed = langext.PTrue
} else if strings.EqualFold(*q.Confirmation, "unconfirmed") { if q.Direction != nil {
filter.Confirmed = langext.PFalse if strings.EqualFold(*q.Direction, "incoming") {
} else if strings.EqualFold(*q.Confirmation, "all") { filter.ChannelOwnerUserID = langext.Ptr([]models.UserID{u.UserID})
// both } else if strings.EqualFold(*q.Direction, "outgoing") {
} else { filter.SubscriberUserID = langext.Ptr([]models.UserID{u.UserID})
return ginresp.APIError(g, 400, apierr.BINDFAIL_QUERY_PARAM, "Invalid value for param 'confirmation'", nil) } else if strings.EqualFold(*q.Direction, "both") {
// both
} else {
return ginresp.APIError(g, 400, apierr.BINDFAIL_QUERY_PARAM, "Invalid value for param 'direction'", nil)
}
} }
}
if q.External != nil { if q.Confirmation != nil {
if strings.EqualFold(*q.External, "true") { if strings.EqualFold(*q.Confirmation, "confirmed") {
filter.SubscriberIsChannelOwner = langext.PFalse filter.Confirmed = langext.PTrue
} else if strings.EqualFold(*q.External, "false") { } else if strings.EqualFold(*q.Confirmation, "unconfirmed") {
filter.SubscriberIsChannelOwner = langext.PTrue filter.Confirmed = langext.PFalse
} else if strings.EqualFold(*q.External, "all") { } else if strings.EqualFold(*q.Confirmation, "all") {
// both // both
} else { } else {
return ginresp.APIError(g, 400, apierr.BINDFAIL_QUERY_PARAM, "Invalid value for param 'external'", nil) return ginresp.APIError(g, 400, apierr.BINDFAIL_QUERY_PARAM, "Invalid value for param 'confirmation'", nil)
}
} }
}
if q.SubscriberUserID != nil { if q.External != nil {
filter.SubscriberUserID2 = langext.Ptr([]models.UserID{*q.SubscriberUserID}) if strings.EqualFold(*q.External, "true") {
} filter.SubscriberIsChannelOwner = langext.PFalse
} else if strings.EqualFold(*q.External, "false") {
filter.SubscriberIsChannelOwner = langext.PTrue
} else if strings.EqualFold(*q.External, "all") {
// both
} else {
return ginresp.APIError(g, 400, apierr.BINDFAIL_QUERY_PARAM, "Invalid value for param 'external'", nil)
}
}
if q.ChannelOwnerUserID != nil { if q.SubscriberUserID != nil {
filter.ChannelOwnerUserID2 = langext.Ptr([]models.UserID{*q.ChannelOwnerUserID}) filter.SubscriberUserID2 = langext.Ptr([]models.UserID{*q.SubscriberUserID})
} }
res, err := h.database.ListSubscriptions(ctx, filter) if q.ChannelOwnerUserID != nil {
if err != nil { filter.ChannelOwnerUserID2 = langext.Ptr([]models.UserID{*q.ChannelOwnerUserID})
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query subscriptions", err) }
}
jsonres := langext.ArrMap(res, func(v models.Subscription) models.SubscriptionJSON { return v.JSON() }) res, err := h.database.ListSubscriptions(ctx, filter)
if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query subscriptions", err)
}
return ctx.FinishSuccess(ginresp.JSON(http.StatusOK, response{Subscriptions: jsonres})) return finishSuccess(ginext.JSON(http.StatusOK, response{Subscriptions: res}))
})
} }
// ListChannelSubscriptions swaggerdoc // ListChannelSubscriptions swaggerdoc
@@ -147,42 +150,44 @@ func (h APIHandler) ListUserSubscriptions(g *gin.Context) ginresp.HTTPResponse {
// @Failure 500 {object} ginresp.apiError "internal server error" // @Failure 500 {object} ginresp.apiError "internal server error"
// //
// @Router /api/v2/users/{uid}/channels/{cid}/subscriptions [GET] // @Router /api/v2/users/{uid}/channels/{cid}/subscriptions [GET]
func (h APIHandler) ListChannelSubscriptions(g *gin.Context) ginresp.HTTPResponse { func (h APIHandler) ListChannelSubscriptions(pctx ginext.PreContext) ginext.HTTPResponse {
type uri struct { type uri struct {
UserID models.UserID `uri:"uid" binding:"entityid"` UserID models.UserID `uri:"uid" binding:"entityid"`
ChannelID models.ChannelID `uri:"cid" binding:"entityid"` ChannelID models.ChannelID `uri:"cid" binding:"entityid"`
} }
type response struct { type response struct {
Subscriptions []models.SubscriptionJSON `json:"subscriptions"` Subscriptions []models.Subscription `json:"subscriptions"`
} }
var u uri var u uri
ctx, errResp := h.app.StartRequest(g, &u, nil, nil, nil) ctx, g, errResp := pctx.URI(&u).Start()
if errResp != nil { if errResp != nil {
return *errResp return *errResp
} }
defer ctx.Cancel() defer ctx.Cancel()
if permResp := ctx.CheckPermissionUserRead(u.UserID); permResp != nil { return h.app.DoRequest(ctx, g, models.TLockRead, func(ctx *logic.AppContext, finishSuccess func(r ginext.HTTPResponse) ginext.HTTPResponse) ginext.HTTPResponse {
return *permResp
}
_, err := h.database.GetChannel(ctx, u.UserID, u.ChannelID, true) if permResp := ctx.CheckPermissionUserRead(u.UserID); permResp != nil {
if errors.Is(err, sql.ErrNoRows) { return *permResp
return ginresp.APIError(g, 404, apierr.CHANNEL_NOT_FOUND, "Channel not found", err) }
}
if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query channel", err)
}
clients, err := h.database.ListSubscriptions(ctx, models.SubscriptionFilter{AnyUserID: langext.Ptr(u.UserID), ChannelID: langext.Ptr([]models.ChannelID{u.ChannelID})}) _, err := h.database.GetChannel(ctx, u.UserID, u.ChannelID, true)
if err != nil { if errors.Is(err, sql.ErrNoRows) {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query subscriptions", err) return ginresp.APIError(g, 404, apierr.CHANNEL_NOT_FOUND, "Channel not found", err)
} }
if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query channel", err)
}
res := langext.ArrMap(clients, func(v models.Subscription) models.SubscriptionJSON { return v.JSON() }) subs, err := h.database.ListSubscriptions(ctx, models.SubscriptionFilter{AnyUserID: langext.Ptr(u.UserID), ChannelID: langext.Ptr([]models.ChannelID{u.ChannelID})})
if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query subscriptions", err)
}
return ctx.FinishSuccess(ginresp.JSON(http.StatusOK, response{Subscriptions: res})) return finishSuccess(ginext.JSON(http.StatusOK, response{Subscriptions: subs}))
})
} }
// GetSubscription swaggerdoc // GetSubscription swaggerdoc
@@ -194,42 +199,46 @@ func (h APIHandler) ListChannelSubscriptions(g *gin.Context) ginresp.HTTPRespons
// @Param uid path string true "UserID" // @Param uid path string true "UserID"
// @Param sid path string true "SubscriptionID" // @Param sid path string true "SubscriptionID"
// //
// @Success 200 {object} models.SubscriptionJSON // @Success 200 {object} models.Subscription
// @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 401 {object} ginresp.apiError "user is not authorized / has missing permissions" // @Failure 401 {object} ginresp.apiError "user is not authorized / has missing permissions"
// @Failure 404 {object} ginresp.apiError "subscription not found" // @Failure 404 {object} ginresp.apiError "subscription not found"
// @Failure 500 {object} ginresp.apiError "internal server error" // @Failure 500 {object} ginresp.apiError "internal server error"
// //
// @Router /api/v2/users/{uid}/subscriptions/{sid} [GET] // @Router /api/v2/users/{uid}/subscriptions/{sid} [GET]
func (h APIHandler) GetSubscription(g *gin.Context) ginresp.HTTPResponse { func (h APIHandler) GetSubscription(pctx ginext.PreContext) ginext.HTTPResponse {
type uri struct { type uri struct {
UserID models.UserID `uri:"uid" binding:"entityid"` UserID models.UserID `uri:"uid" binding:"entityid"`
SubscriptionID models.SubscriptionID `uri:"sid" binding:"entityid"` SubscriptionID models.SubscriptionID `uri:"sid" binding:"entityid"`
} }
var u uri var u uri
ctx, errResp := h.app.StartRequest(g, &u, nil, nil, nil) ctx, g, errResp := pctx.URI(&u).Start()
if errResp != nil { if errResp != nil {
return *errResp return *errResp
} }
defer ctx.Cancel() defer ctx.Cancel()
if permResp := ctx.CheckPermissionUserRead(u.UserID); permResp != nil { return h.app.DoRequest(ctx, g, models.TLockRead, func(ctx *logic.AppContext, finishSuccess func(r ginext.HTTPResponse) ginext.HTTPResponse) ginext.HTTPResponse {
return *permResp
}
subscription, err := h.database.GetSubscription(ctx, u.SubscriptionID) if permResp := ctx.CheckPermissionUserRead(u.UserID); permResp != nil {
if errors.Is(err, sql.ErrNoRows) { return *permResp
return ginresp.APIError(g, 404, apierr.SUBSCRIPTION_NOT_FOUND, "Subscription not found", err) }
}
if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query subscription", err)
}
if subscription.SubscriberUserID != u.UserID && subscription.ChannelOwnerUserID != u.UserID {
return ginresp.APIError(g, 404, apierr.SUBSCRIPTION_USER_MISMATCH, "Subscription not found", nil)
}
return ctx.FinishSuccess(ginresp.JSON(http.StatusOK, subscription.JSON())) subscription, err := h.database.GetSubscription(ctx, u.SubscriptionID)
if errors.Is(err, sql.ErrNoRows) {
return ginresp.APIError(g, 404, apierr.SUBSCRIPTION_NOT_FOUND, "Subscription not found", err)
}
if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query subscription", err)
}
if subscription.SubscriberUserID != u.UserID && subscription.ChannelOwnerUserID != u.UserID {
return ginresp.APIError(g, 404, apierr.SUBSCRIPTION_USER_MISMATCH, "Subscription not found", nil)
}
return finishSuccess(ginext.JSON(http.StatusOK, subscription))
})
} }
// CancelSubscription swaggerdoc // CancelSubscription swaggerdoc
@@ -241,47 +250,51 @@ func (h APIHandler) GetSubscription(g *gin.Context) ginresp.HTTPResponse {
// @Param uid path string true "UserID" // @Param uid path string true "UserID"
// @Param sid path string true "SubscriptionID" // @Param sid path string true "SubscriptionID"
// //
// @Success 200 {object} models.SubscriptionJSON // @Success 200 {object} models.Subscription
// @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 401 {object} ginresp.apiError "user is not authorized / has missing permissions" // @Failure 401 {object} ginresp.apiError "user is not authorized / has missing permissions"
// @Failure 404 {object} ginresp.apiError "subscription not found" // @Failure 404 {object} ginresp.apiError "subscription not found"
// @Failure 500 {object} ginresp.apiError "internal server error" // @Failure 500 {object} ginresp.apiError "internal server error"
// //
// @Router /api/v2/users/{uid}/subscriptions/{sid} [DELETE] // @Router /api/v2/users/{uid}/subscriptions/{sid} [DELETE]
func (h APIHandler) CancelSubscription(g *gin.Context) ginresp.HTTPResponse { func (h APIHandler) CancelSubscription(pctx ginext.PreContext) ginext.HTTPResponse {
type uri struct { type uri struct {
UserID models.UserID `uri:"uid" binding:"entityid"` UserID models.UserID `uri:"uid" binding:"entityid"`
SubscriptionID models.SubscriptionID `uri:"sid" binding:"entityid"` SubscriptionID models.SubscriptionID `uri:"sid" binding:"entityid"`
} }
var u uri var u uri
ctx, errResp := h.app.StartRequest(g, &u, nil, nil, nil) ctx, g, errResp := pctx.URI(&u).Start()
if errResp != nil { if errResp != nil {
return *errResp return *errResp
} }
defer ctx.Cancel() defer ctx.Cancel()
if permResp := ctx.CheckPermissionUserAdmin(u.UserID); permResp != nil { return h.app.DoRequest(ctx, g, models.TLockReadWrite, func(ctx *logic.AppContext, finishSuccess func(r ginext.HTTPResponse) ginext.HTTPResponse) ginext.HTTPResponse {
return *permResp
}
subscription, err := h.database.GetSubscription(ctx, u.SubscriptionID) if permResp := ctx.CheckPermissionUserAdmin(u.UserID); permResp != nil {
if errors.Is(err, sql.ErrNoRows) { return *permResp
return ginresp.APIError(g, 404, apierr.SUBSCRIPTION_NOT_FOUND, "Subscription not found", err) }
}
if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query subscription", err)
}
if subscription.SubscriberUserID != u.UserID && subscription.ChannelOwnerUserID != u.UserID {
return ginresp.APIError(g, 404, apierr.SUBSCRIPTION_USER_MISMATCH, "Subscription not found", nil)
}
err = h.database.DeleteSubscription(ctx, u.SubscriptionID) subscription, err := h.database.GetSubscription(ctx, u.SubscriptionID)
if err != nil { if errors.Is(err, sql.ErrNoRows) {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to delete subscription", err) return ginresp.APIError(g, 404, apierr.SUBSCRIPTION_NOT_FOUND, "Subscription not found", err)
} }
if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query subscription", err)
}
if subscription.SubscriberUserID != u.UserID && subscription.ChannelOwnerUserID != u.UserID {
return ginresp.APIError(g, 404, apierr.SUBSCRIPTION_USER_MISMATCH, "Subscription not found", nil)
}
return ctx.FinishSuccess(ginresp.JSON(http.StatusOK, subscription.JSON())) err = h.database.DeleteSubscription(ctx, u.SubscriptionID)
if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to delete subscription", err)
}
return finishSuccess(ginext.JSON(http.StatusOK, subscription))
})
} }
// CreateSubscription swaggerdoc // CreateSubscription swaggerdoc
@@ -295,13 +308,13 @@ func (h APIHandler) CancelSubscription(g *gin.Context) ginresp.HTTPResponse {
// @Param query_data query handler.CreateSubscription.query false " " // @Param query_data query handler.CreateSubscription.query false " "
// @Param post_data body handler.CreateSubscription.body false " " // @Param post_data body handler.CreateSubscription.body false " "
// //
// @Success 200 {object} models.SubscriptionJSON // @Success 200 {object} models.Subscription
// @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 401 {object} ginresp.apiError "user is not authorized / has missing permissions" // @Failure 401 {object} ginresp.apiError "user is not authorized / has missing permissions"
// @Failure 500 {object} ginresp.apiError "internal server error" // @Failure 500 {object} ginresp.apiError "internal server error"
// //
// @Router /api/v2/users/{uid}/subscriptions [POST] // @Router /api/v2/users/{uid}/subscriptions [POST]
func (h APIHandler) CreateSubscription(g *gin.Context) ginresp.HTTPResponse { func (h APIHandler) CreateSubscription(pctx ginext.PreContext) ginext.HTTPResponse {
type uri struct { type uri struct {
UserID models.UserID `uri:"uid" binding:"entityid"` UserID models.UserID `uri:"uid" binding:"entityid"`
} }
@@ -317,76 +330,80 @@ func (h APIHandler) CreateSubscription(g *gin.Context) ginresp.HTTPResponse {
var u uri var u uri
var q query var q query
var b body var b body
ctx, errResp := h.app.StartRequest(g, &u, &q, &b, nil) ctx, g, errResp := pctx.URI(&u).Query(&q).Body(&b).Start()
if errResp != nil { if errResp != nil {
return *errResp return *errResp
} }
defer ctx.Cancel() defer ctx.Cancel()
if permResp := ctx.CheckPermissionUserAdmin(u.UserID); permResp != nil { return h.app.DoRequest(ctx, g, models.TLockReadWrite, func(ctx *logic.AppContext, finishSuccess func(r ginext.HTTPResponse) ginext.HTTPResponse) ginext.HTTPResponse {
return *permResp
}
var channel models.Channel if permResp := ctx.CheckPermissionUserAdmin(u.UserID); permResp != nil {
return *permResp
if b.ChannelOwnerUserID != nil && b.ChannelInternalName != nil && b.ChannelID == nil {
channelInternalName := h.app.NormalizeChannelInternalName(*b.ChannelInternalName)
outchannel, err := h.database.GetChannelByName(ctx, *b.ChannelOwnerUserID, channelInternalName)
if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query channel", err)
}
if outchannel == nil {
return ginresp.APIError(g, 400, apierr.CHANNEL_NOT_FOUND, "Channel not found", err)
} }
channel = *outchannel var channel models.Channel
} else if b.ChannelOwnerUserID == nil && b.ChannelInternalName == nil && b.ChannelID != nil { if b.ChannelOwnerUserID != nil && b.ChannelInternalName != nil && b.ChannelID == nil {
outchannel, err := h.database.GetChannelByID(ctx, *b.ChannelID) channelInternalName := h.app.NormalizeChannelInternalName(*b.ChannelInternalName)
if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query channel", err)
}
if outchannel == nil {
return ginresp.APIError(g, 400, apierr.CHANNEL_NOT_FOUND, "Channel not found", err)
}
channel = *outchannel outchannel, err := h.database.GetChannelByName(ctx, *b.ChannelOwnerUserID, channelInternalName)
} else {
return ginresp.APIError(g, 400, apierr.INVALID_BODY_PARAM, "Must either supply [channel_owner_user_id, channel_internal_name] or [channel_id]", nil)
}
if channel.OwnerUserID != u.UserID && (q.ChanSubscribeKey == nil || *q.ChanSubscribeKey != channel.SubscribeKey) {
return ginresp.APIError(g, 401, apierr.USER_AUTH_FAILED, "You are not authorized for this action", nil)
}
existingSub, err := h.database.GetSubscriptionBySubscriber(ctx, u.UserID, channel.ChannelID)
if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query existing subscription", err)
}
if existingSub != nil {
if !existingSub.Confirmed && channel.OwnerUserID == u.UserID {
err = h.database.UpdateSubscriptionConfirmed(ctx, existingSub.SubscriptionID, true)
if err != nil { if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to update subscription", err) return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query channel", err)
} }
existingSub.Confirmed = true if outchannel == nil {
return ginresp.APIError(g, 400, apierr.CHANNEL_NOT_FOUND, "Channel not found", err)
}
channel = *outchannel
} else if b.ChannelOwnerUserID == nil && b.ChannelInternalName == nil && b.ChannelID != nil {
outchannel, err := h.database.GetChannelByID(ctx, *b.ChannelID)
if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query channel", err)
}
if outchannel == nil {
return ginresp.APIError(g, 400, apierr.CHANNEL_NOT_FOUND, "Channel not found", err)
}
channel = *outchannel
} else {
return ginresp.APIError(g, 400, apierr.INVALID_BODY_PARAM, "Must either supply [channel_owner_user_id, channel_internal_name] or [channel_id]", nil)
} }
return ctx.FinishSuccess(ginresp.JSON(http.StatusOK, existingSub.JSON())) if channel.OwnerUserID != u.UserID && (q.ChanSubscribeKey == nil || *q.ChanSubscribeKey != channel.SubscribeKey) {
} return ginresp.APIError(g, 401, apierr.USER_AUTH_FAILED, "You are not authorized for this action", nil)
}
sub, err := h.database.CreateSubscription(ctx, u.UserID, channel, channel.OwnerUserID == u.UserID) existingSub, err := h.database.GetSubscriptionBySubscriber(ctx, u.UserID, channel.ChannelID)
if err != nil { if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to create subscription", err) return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query existing subscription", err)
} }
if existingSub != nil {
if !existingSub.Confirmed && channel.OwnerUserID == u.UserID {
err = h.database.UpdateSubscriptionConfirmed(ctx, existingSub.SubscriptionID, true)
if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to update subscription", err)
}
existingSub.Confirmed = true
}
return ctx.FinishSuccess(ginresp.JSON(http.StatusOK, sub.JSON())) return finishSuccess(ginext.JSON(http.StatusOK, existingSub))
}
sub, err := h.database.CreateSubscription(ctx, u.UserID, channel, channel.OwnerUserID == u.UserID)
if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to create subscription", err)
}
return finishSuccess(ginext.JSON(http.StatusOK, sub))
})
} }
// UpdateSubscription swaggerdoc // UpdateSubscription swaggerdoc
@@ -399,14 +416,14 @@ func (h APIHandler) CreateSubscription(g *gin.Context) ginresp.HTTPResponse {
// @Param sid path string true "SubscriptionID" // @Param sid path string true "SubscriptionID"
// @Param post_data body handler.UpdateSubscription.body false " " // @Param post_data body handler.UpdateSubscription.body false " "
// //
// @Success 200 {object} models.SubscriptionJSON // @Success 200 {object} models.Subscription
// @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 401 {object} ginresp.apiError "user is not authorized / has missing permissions" // @Failure 401 {object} ginresp.apiError "user is not authorized / has missing permissions"
// @Failure 404 {object} ginresp.apiError "subscription not found" // @Failure 404 {object} ginresp.apiError "subscription not found"
// @Failure 500 {object} ginresp.apiError "internal server error" // @Failure 500 {object} ginresp.apiError "internal server error"
// //
// @Router /api/v2/users/{uid}/subscriptions/{sid} [PATCH] // @Router /api/v2/users/{uid}/subscriptions/{sid} [PATCH]
func (h APIHandler) UpdateSubscription(g *gin.Context) ginresp.HTTPResponse { func (h APIHandler) UpdateSubscription(pctx ginext.PreContext) ginext.HTTPResponse {
type uri struct { type uri struct {
UserID models.UserID `uri:"uid" binding:"entityid"` UserID models.UserID `uri:"uid" binding:"entityid"`
SubscriptionID models.SubscriptionID `uri:"sid" binding:"entityid"` SubscriptionID models.SubscriptionID `uri:"sid" binding:"entityid"`
@@ -417,43 +434,47 @@ func (h APIHandler) UpdateSubscription(g *gin.Context) ginresp.HTTPResponse {
var u uri var u uri
var b body var b body
ctx, errResp := h.app.StartRequest(g, &u, nil, &b, nil) ctx, g, errResp := pctx.URI(&u).Body(&b).Start()
if errResp != nil { if errResp != nil {
return *errResp return *errResp
} }
defer ctx.Cancel() defer ctx.Cancel()
if permResp := ctx.CheckPermissionUserAdmin(u.UserID); permResp != nil { return h.app.DoRequest(ctx, g, models.TLockReadWrite, func(ctx *logic.AppContext, finishSuccess func(r ginext.HTTPResponse) ginext.HTTPResponse) ginext.HTTPResponse {
return *permResp
}
userid := *ctx.GetPermissionUserID() if permResp := ctx.CheckPermissionUserAdmin(u.UserID); permResp != nil {
return *permResp
subscription, err := h.database.GetSubscription(ctx, u.SubscriptionID) }
if errors.Is(err, sql.ErrNoRows) {
return ginresp.APIError(g, 404, apierr.SUBSCRIPTION_NOT_FOUND, "Subscription not found", err) userid := *ctx.GetPermissionUserID()
}
if err != nil { subscription, err := h.database.GetSubscription(ctx, u.SubscriptionID)
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query subscription", err) if errors.Is(err, sql.ErrNoRows) {
} return ginresp.APIError(g, 404, apierr.SUBSCRIPTION_NOT_FOUND, "Subscription not found", err)
if subscription.SubscriberUserID != u.UserID && subscription.ChannelOwnerUserID != u.UserID {
return ginresp.APIError(g, 404, apierr.SUBSCRIPTION_USER_MISMATCH, "Subscription not found", nil)
}
if b.Confirmed != nil {
if subscription.ChannelOwnerUserID != userid {
return ginresp.APIError(g, 401, apierr.USER_AUTH_FAILED, "You are not authorized for this action", nil)
} }
err = h.database.UpdateSubscriptionConfirmed(ctx, u.SubscriptionID, *b.Confirmed)
if err != nil { if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to update subscription", err) return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query subscription", err)
}
if subscription.SubscriberUserID != u.UserID && subscription.ChannelOwnerUserID != u.UserID {
return ginresp.APIError(g, 404, apierr.SUBSCRIPTION_USER_MISMATCH, "Subscription not found", nil)
} }
}
subscription, err = h.database.GetSubscription(ctx, u.SubscriptionID) if b.Confirmed != nil {
if err != nil { if subscription.ChannelOwnerUserID != userid {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query subscription", err) return ginresp.APIError(g, 401, apierr.USER_AUTH_FAILED, "You are not authorized for this action", nil)
} }
err = h.database.UpdateSubscriptionConfirmed(ctx, u.SubscriptionID, *b.Confirmed)
if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to update subscription", err)
}
}
return ctx.FinishSuccess(ginresp.JSON(http.StatusOK, subscription.JSON())) subscription, err = h.database.GetSubscription(ctx, u.SubscriptionID)
if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query subscription", err)
}
return finishSuccess(ginext.JSON(http.StatusOK, subscription))
})
} }
+146 -135
View File
@@ -3,12 +3,13 @@ package handler
import ( import (
"blackforestbytes.com/simplecloudnotifier/api/apierr" "blackforestbytes.com/simplecloudnotifier/api/apierr"
"blackforestbytes.com/simplecloudnotifier/api/ginresp" "blackforestbytes.com/simplecloudnotifier/api/ginresp"
"blackforestbytes.com/simplecloudnotifier/logic"
"blackforestbytes.com/simplecloudnotifier/models" "blackforestbytes.com/simplecloudnotifier/models"
"database/sql" "database/sql"
"errors" "errors"
"fmt" "fmt"
"github.com/gin-gonic/gin"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"gogs.mikescher.com/BlackForestBytes/goext/ginext"
"gogs.mikescher.com/BlackForestBytes/goext/langext" "gogs.mikescher.com/BlackForestBytes/goext/langext"
"net/http" "net/http"
) )
@@ -21,12 +22,12 @@ import (
// //
// @Param post_body body handler.CreateUser.body false " " // @Param post_body body handler.CreateUser.body false " "
// //
// @Success 200 {object} models.UserJSONWithClientsAndKeys // @Success 200 {object} models.UserWithClientsAndKeys
// @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"
// //
// @Router /api/v2/users [POST] // @Router /api/v2/users [POST]
func (h APIHandler) CreateUser(g *gin.Context) ginresp.HTTPResponse { func (h APIHandler) CreateUser(pctx ginext.PreContext) ginext.HTTPResponse {
type body struct { type body struct {
FCMToken string `json:"fcm_token"` FCMToken string `json:"fcm_token"`
ProToken *string `json:"pro_token"` ProToken *string `json:"pro_token"`
@@ -39,99 +40,101 @@ func (h APIHandler) CreateUser(g *gin.Context) ginresp.HTTPResponse {
} }
var b body var b body
ctx, errResp := h.app.StartRequest(g, nil, nil, &b, nil) ctx, g, errResp := pctx.Body(&b).Start()
if errResp != nil { if errResp != nil {
return *errResp return *errResp
} }
defer ctx.Cancel() defer ctx.Cancel()
var clientType models.ClientType return h.app.DoRequest(ctx, g, models.TLockReadWrite, func(ctx *logic.AppContext, finishSuccess func(r ginext.HTTPResponse) ginext.HTTPResponse) ginext.HTTPResponse {
if !b.NoClient {
if b.FCMToken == "" {
return ginresp.APIError(g, 400, apierr.INVALID_CLIENTTYPE, "Missing FCMToken", nil)
}
if b.AgentVersion == "" {
return ginresp.APIError(g, 400, apierr.INVALID_CLIENTTYPE, "Missing AgentVersion", nil)
}
if b.ClientType == "" {
return ginresp.APIError(g, 400, apierr.INVALID_CLIENTTYPE, "Missing ClientType", nil)
}
if !b.ClientType.Valid() {
return ginresp.APIError(g, 400, apierr.BINDFAIL_BODY_PARAM, "Invalid ClientType", nil)
}
clientType = b.ClientType
}
if b.ProToken != nil { var clientType models.ClientType
ptok, err := h.app.VerifyProToken(ctx, *b.ProToken) if !b.NoClient {
if b.FCMToken == "" {
return ginresp.APIError(g, 400, apierr.INVALID_CLIENTTYPE, "Missing FCMToken", nil)
}
if b.AgentVersion == "" {
return ginresp.APIError(g, 400, apierr.INVALID_CLIENTTYPE, "Missing AgentVersion", nil)
}
if b.ClientType == "" {
return ginresp.APIError(g, 400, apierr.INVALID_CLIENTTYPE, "Missing ClientType", nil)
}
if !b.ClientType.Valid() {
return ginresp.APIError(g, 400, apierr.BINDFAIL_BODY_PARAM, "Invalid ClientType", nil)
}
clientType = b.ClientType
}
if b.ProToken != nil {
ptok, err := h.app.VerifyProToken(ctx, *b.ProToken)
if err != nil {
return ginresp.APIError(g, 500, apierr.FAILED_VERIFY_PRO_TOKEN, "Failed to query purchase status", err)
}
if !ptok {
return ginresp.APIError(g, 400, apierr.INVALID_PRO_TOKEN, "Purchase token could not be verified", nil)
}
}
readKey := h.app.GenerateRandomAuthKey()
sendKey := h.app.GenerateRandomAuthKey()
adminKey := h.app.GenerateRandomAuthKey()
err := h.database.ClearFCMTokens(ctx, b.FCMToken)
if err != nil { if err != nil {
return ginresp.APIError(g, 500, apierr.FAILED_VERIFY_PRO_TOKEN, "Failed to query purchase status", err) return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to clear existing fcm tokens", err)
} }
if !ptok { if b.ProToken != nil {
return ginresp.APIError(g, 400, apierr.INVALID_PRO_TOKEN, "Purchase token could not be verified", nil) err := h.database.ClearProTokens(ctx, *b.ProToken)
if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to clear existing pro tokens", err)
}
} }
}
readKey := h.app.GenerateRandomAuthKey() username := b.Username
sendKey := h.app.GenerateRandomAuthKey() if username != nil {
adminKey := h.app.GenerateRandomAuthKey() username = langext.Ptr(h.app.NormalizeUsername(*username))
}
err := h.database.ClearFCMTokens(ctx, b.FCMToken) userobj, err := h.database.CreateUser(ctx, b.ProToken, username)
if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to clear existing fcm tokens", err)
}
if b.ProToken != nil {
err := h.database.ClearProTokens(ctx, *b.ProToken)
if err != nil { if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to clear existing pro tokens", err) return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to create user in db", err)
} }
}
username := b.Username _, err = h.database.CreateKeyToken(ctx, "AdminKey (default)", userobj.UserID, true, make([]models.ChannelID, 0), models.TokenPermissionList{models.PermAdmin}, adminKey)
if username != nil {
username = langext.Ptr(h.app.NormalizeUsername(*username))
}
userobj, err := h.database.CreateUser(ctx, b.ProToken, username)
if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to create user in db", err)
}
_, err = h.database.CreateKeyToken(ctx, "AdminKey (default)", userobj.UserID, true, make([]models.ChannelID, 0), models.TokenPermissionList{models.PermAdmin}, adminKey)
if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to create admin-key in db", err)
}
_, err = h.database.CreateKeyToken(ctx, "SendKey (default)", userobj.UserID, true, make([]models.ChannelID, 0), models.TokenPermissionList{models.PermChannelSend}, sendKey)
if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to create send-key in db", err)
}
_, err = h.database.CreateKeyToken(ctx, "ReadKey (default)", userobj.UserID, true, make([]models.ChannelID, 0), models.TokenPermissionList{models.PermUserRead, models.PermChannelRead}, readKey)
if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to create read-key in db", err)
}
log.Info().Msg(fmt.Sprintf("Sucessfully created new user %s (client: %v)", userobj.UserID, b.NoClient))
if b.NoClient {
return ctx.FinishSuccess(ginresp.JSON(http.StatusOK, userobj.JSONWithClients(make([]models.Client, 0), adminKey, sendKey, readKey)))
} else {
err := h.database.DeleteClientsByFCM(ctx, b.FCMToken)
if err != nil { if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to delete existing clients in db", err) return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to create admin-key in db", err)
} }
client, err := h.database.CreateClient(ctx, userobj.UserID, clientType, b.FCMToken, b.AgentModel, b.AgentVersion, b.ClientName) _, err = h.database.CreateKeyToken(ctx, "SendKey (default)", userobj.UserID, true, make([]models.ChannelID, 0), models.TokenPermissionList{models.PermChannelSend}, sendKey)
if err != nil { if err != nil {
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 send-key in db", err)
} }
return ctx.FinishSuccess(ginresp.JSON(http.StatusOK, userobj.JSONWithClients([]models.Client{client}, adminKey, sendKey, readKey))) _, 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)
}
log.Info().Msg(fmt.Sprintf("Sucessfully created new user %s (client: %v)", userobj.UserID, b.NoClient))
if b.NoClient {
return finishSuccess(ginext.JSON(http.StatusOK, userobj.PreMarshal().WithClients(make([]models.Client, 0), adminKey, sendKey, readKey)))
} else {
err := h.database.DeleteClientsByFCM(ctx, b.FCMToken)
if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to delete existing clients in db", err)
}
client, err := h.database.CreateClient(ctx, userobj.UserID, clientType, b.FCMToken, b.AgentModel, b.AgentVersion, b.ClientName)
if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to create client in db", err)
}
return finishSuccess(ginext.JSON(http.StatusOK, userobj.PreMarshal().WithClients([]models.Client{client}, adminKey, sendKey, readKey)))
}
})
} }
// GetUser swaggerdoc // GetUser swaggerdoc
@@ -142,38 +145,43 @@ func (h APIHandler) CreateUser(g *gin.Context) ginresp.HTTPResponse {
// //
// @Param uid path string true "UserID" // @Param uid path string true "UserID"
// //
// @Success 200 {object} models.UserJSON // @Success 200 {object} models.User
// @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 401 {object} ginresp.apiError "user is not authorized / has missing permissions" // @Failure 401 {object} ginresp.apiError "user is not authorized / has missing permissions"
// @Failure 404 {object} ginresp.apiError "user not found" // @Failure 404 {object} ginresp.apiError "user not found"
// @Failure 500 {object} ginresp.apiError "internal server error" // @Failure 500 {object} ginresp.apiError "internal server error"
// //
// @Router /api/v2/users/{uid} [GET] // @Router /api/v2/users/{uid} [GET]
func (h APIHandler) GetUser(g *gin.Context) ginresp.HTTPResponse { func (h APIHandler) GetUser(pctx ginext.PreContext) ginext.HTTPResponse {
type uri struct { type uri struct {
UserID models.UserID `uri:"uid" binding:"entityid"` UserID models.UserID `uri:"uid" binding:"entityid"`
} }
var u uri var u uri
ctx, errResp := h.app.StartRequest(g, &u, nil, nil, nil) ctx, g, errResp := pctx.URI(&u).Start()
if errResp != nil { if errResp != nil {
return *errResp return *errResp
} }
defer ctx.Cancel() defer ctx.Cancel()
if permResp := ctx.CheckPermissionUserRead(u.UserID); permResp != nil { return h.app.DoRequest(ctx, g, models.TLockRead, func(ctx *logic.AppContext, finishSuccess func(r ginext.HTTPResponse) ginext.HTTPResponse) ginext.HTTPResponse {
return *permResp
}
user, err := h.database.GetUser(ctx, u.UserID) if permResp := ctx.CheckPermissionUserRead(u.UserID); permResp != nil {
if errors.Is(err, sql.ErrNoRows) { return *permResp
return ginresp.APIError(g, 404, apierr.USER_NOT_FOUND, "User not found", err) }
}
if err != nil { user, err := h.database.GetUser(ctx, u.UserID)
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query user", err) if errors.Is(err, sql.ErrNoRows) {
} return ginresp.APIError(g, 404, apierr.USER_NOT_FOUND, "User not found", err)
}
if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query user", err)
}
return finishSuccess(ginext.JSON(http.StatusOK, user.PreMarshal()))
})
return ctx.FinishSuccess(ginresp.JSON(http.StatusOK, user.JSON()))
} }
// UpdateUser swaggerdoc // UpdateUser swaggerdoc
@@ -188,14 +196,14 @@ func (h APIHandler) GetUser(g *gin.Context) ginresp.HTTPResponse {
// @Param username body string false "Change the username (send an empty string to clear it)" // @Param username body string false "Change the username (send an empty string to clear it)"
// @Param pro_token body string false "Send a verification of premium purchase" // @Param pro_token body string false "Send a verification of premium purchase"
// //
// @Success 200 {object} models.UserJSON // @Success 200 {object} models.User
// @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 401 {object} ginresp.apiError "user is not authorized / has missing permissions" // @Failure 401 {object} ginresp.apiError "user is not authorized / has missing permissions"
// @Failure 404 {object} ginresp.apiError "user not found" // @Failure 404 {object} ginresp.apiError "user not found"
// @Failure 500 {object} ginresp.apiError "internal server error" // @Failure 500 {object} ginresp.apiError "internal server error"
// //
// @Router /api/v2/users/{uid} [PATCH] // @Router /api/v2/users/{uid} [PATCH]
func (h APIHandler) UpdateUser(g *gin.Context) ginresp.HTTPResponse { func (h APIHandler) UpdateUser(pctx ginext.PreContext) ginext.HTTPResponse {
type uri struct { type uri struct {
UserID models.UserID `uri:"uid" binding:"entityid"` UserID models.UserID `uri:"uid" binding:"entityid"`
} }
@@ -206,60 +214,63 @@ func (h APIHandler) UpdateUser(g *gin.Context) ginresp.HTTPResponse {
var u uri var u uri
var b body var b body
ctx, errResp := h.app.StartRequest(g, &u, nil, &b, nil) ctx, g, errResp := pctx.URI(&u).Body(&b).Start()
if errResp != nil { if errResp != nil {
return *errResp return *errResp
} }
defer ctx.Cancel() defer ctx.Cancel()
if permResp := ctx.CheckPermissionUserAdmin(u.UserID); permResp != nil { return h.app.DoRequest(ctx, g, models.TLockReadWrite, func(ctx *logic.AppContext, finishSuccess func(r ginext.HTTPResponse) ginext.HTTPResponse) ginext.HTTPResponse {
return *permResp
}
if b.Username != nil { if permResp := ctx.CheckPermissionUserAdmin(u.UserID); permResp != nil {
username := langext.Ptr(h.app.NormalizeUsername(*b.Username)) return *permResp
if *username == "" {
username = nil
} }
err := h.database.UpdateUserUsername(ctx, u.UserID, username) if b.Username != nil {
username := langext.Ptr(h.app.NormalizeUsername(*b.Username))
if *username == "" {
username = nil
}
err := h.database.UpdateUserUsername(ctx, u.UserID, username)
if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to update user", err)
}
}
if b.ProToken != nil {
if *b.ProToken == "" {
err := h.database.UpdateUserProToken(ctx, u.UserID, nil)
if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to update user", err)
}
} else {
ptok, err := h.app.VerifyProToken(ctx, *b.ProToken)
if err != nil {
return ginresp.APIError(g, 500, apierr.FAILED_VERIFY_PRO_TOKEN, "Failed to query purchase status", err)
}
if !ptok {
return ginresp.APIError(g, 400, apierr.INVALID_PRO_TOKEN, "Purchase token could not be verified", nil)
}
err = h.database.ClearProTokens(ctx, *b.ProToken)
if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to clear existing fcm tokens", err)
}
err = h.database.UpdateUserProToken(ctx, u.UserID, b.ProToken)
if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to update user", err)
}
}
}
user, err := h.database.GetUser(ctx, u.UserID)
if err != nil { if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to update user", err) return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query (updated) user", err)
} }
}
if b.ProToken != nil { return finishSuccess(ginext.JSON(http.StatusOK, user.PreMarshal()))
if *b.ProToken == "" { })
err := h.database.UpdateUserProToken(ctx, u.UserID, nil)
if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to update user", err)
}
} else {
ptok, err := h.app.VerifyProToken(ctx, *b.ProToken)
if err != nil {
return ginresp.APIError(g, 500, apierr.FAILED_VERIFY_PRO_TOKEN, "Failed to query purchase status", err)
}
if !ptok {
return ginresp.APIError(g, 400, apierr.INVALID_PRO_TOKEN, "Purchase token could not be verified", nil)
}
err = h.database.ClearProTokens(ctx, *b.ProToken)
if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to clear existing fcm tokens", err)
}
err = h.database.UpdateUserProToken(ctx, u.UserID, b.ProToken)
if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to update user", err)
}
}
}
user, err := h.database.GetUser(ctx, u.UserID)
if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query (updated) user", err)
}
return ctx.FinishSuccess(ginresp.JSON(http.StatusOK, user.JSON()))
} }
+127 -82
View File
@@ -5,11 +5,12 @@ import (
"blackforestbytes.com/simplecloudnotifier/api/ginresp" "blackforestbytes.com/simplecloudnotifier/api/ginresp"
"blackforestbytes.com/simplecloudnotifier/db/simplectx" "blackforestbytes.com/simplecloudnotifier/db/simplectx"
"blackforestbytes.com/simplecloudnotifier/logic" "blackforestbytes.com/simplecloudnotifier/logic"
"blackforestbytes.com/simplecloudnotifier/models"
"bytes" "bytes"
"context"
"errors" "errors"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
sqlite3 "github.com/mattn/go-sqlite3" "github.com/mattn/go-sqlite3"
"gogs.mikescher.com/BlackForestBytes/goext/ginext"
"gogs.mikescher.com/BlackForestBytes/goext/langext" "gogs.mikescher.com/BlackForestBytes/goext/langext"
"gogs.mikescher.com/BlackForestBytes/goext/timeext" "gogs.mikescher.com/BlackForestBytes/goext/timeext"
"net/http" "net/http"
@@ -51,20 +52,30 @@ type pingResponseInfo struct {
// @Router /api/ping [put] // @Router /api/ping [put]
// @Router /api/ping [delete] // @Router /api/ping [delete]
// @Router /api/ping [patch] // @Router /api/ping [patch]
func (h CommonHandler) Ping(g *gin.Context) ginresp.HTTPResponse { func (h CommonHandler) Ping(pctx ginext.PreContext) ginext.HTTPResponse {
buf := new(bytes.Buffer) ctx, g, errResp := pctx.Start()
_, _ = buf.ReadFrom(g.Request.Body) if errResp != nil {
resuestBody := buf.String() return *errResp
}
defer ctx.Cancel()
return h.app.DoRequest(ctx, g, models.TLockRead, func(ctx *logic.AppContext, finishSuccess func(r ginext.HTTPResponse) ginext.HTTPResponse) ginext.HTTPResponse {
buf := new(bytes.Buffer)
_, _ = buf.ReadFrom(g.Request.Body)
resuestBody := buf.String()
return ginext.JSON(http.StatusOK, pingResponse{
Message: "Pong",
Info: pingResponseInfo{
Method: g.Request.Method,
Request: resuestBody,
Headers: g.Request.Header,
URI: g.Request.RequestURI,
Address: g.Request.RemoteAddr,
},
})
return ginresp.JSON(http.StatusOK, pingResponse{
Message: "Pong",
Info: pingResponseInfo{
Method: g.Request.Method,
Request: resuestBody,
Headers: g.Request.Header,
URI: g.Request.RequestURI,
Address: g.Request.RemoteAddr,
},
}) })
} }
@@ -78,7 +89,7 @@ func (h CommonHandler) Ping(g *gin.Context) ginresp.HTTPResponse {
// @Failure 500 {object} ginresp.apiError // @Failure 500 {object} ginresp.apiError
// //
// @Router /api/db-test [post] // @Router /api/db-test [post]
func (h CommonHandler) DatabaseTest(g *gin.Context) ginresp.HTTPResponse { func (h CommonHandler) DatabaseTest(pctx ginext.PreContext) ginext.HTTPResponse {
type response struct { type response struct {
Success bool `json:"success"` Success bool `json:"success"`
LibVersion string `json:"libVersion"` LibVersion string `json:"libVersion"`
@@ -86,21 +97,28 @@ func (h CommonHandler) DatabaseTest(g *gin.Context) ginresp.HTTPResponse {
SourceID string `json:"sourceID"` SourceID string `json:"sourceID"`
} }
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) ctx, g, errResp := pctx.Start()
defer cancel() if errResp != nil {
return *errResp
libVersion, libVersionNumber, sourceID := sqlite3.Version()
err := h.app.Database.Ping(ctx)
if err != nil {
return ginresp.InternalError(err)
} }
defer ctx.Cancel()
return h.app.DoRequest(ctx, g, models.TLockRead, func(ctx *logic.AppContext, finishSuccess func(r ginext.HTTPResponse) ginext.HTTPResponse) ginext.HTTPResponse {
libVersion, libVersionNumber, sourceID := sqlite3.Version()
err := h.app.Database.Ping(ctx)
if err != nil {
return ginresp.InternalError(err)
}
return ginext.JSON(http.StatusOK, response{
Success: true,
LibVersion: libVersion,
LibVersionNumber: libVersionNumber,
SourceID: sourceID,
})
return ginresp.JSON(http.StatusOK, response{
Success: true,
LibVersion: libVersion,
LibVersionNumber: libVersionNumber,
SourceID: sourceID,
}) })
} }
@@ -114,54 +132,61 @@ func (h CommonHandler) DatabaseTest(g *gin.Context) ginresp.HTTPResponse {
// @Failure 500 {object} ginresp.apiError // @Failure 500 {object} ginresp.apiError
// //
// @Router /api/health [get] // @Router /api/health [get]
func (h CommonHandler) Health(g *gin.Context) ginresp.HTTPResponse { func (h CommonHandler) Health(pctx ginext.PreContext) ginext.HTTPResponse {
type response struct { type response struct {
Status string `json:"status"` Status string `json:"status"`
} }
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) ctx, g, errResp := pctx.Start()
defer cancel() if errResp != nil {
return *errResp
_, libVersionNumber, _ := sqlite3.Version()
if libVersionNumber < 3039000 {
return ginresp.InternalError(errors.New("sqlite version too low"))
} }
defer ctx.Cancel()
tctx := simplectx.CreateSimpleContext(ctx, nil) return h.app.DoRequest(ctx, g, models.TLockReadWrite, func(ctx *logic.AppContext, finishSuccess func(r ginext.HTTPResponse) ginext.HTTPResponse) ginext.HTTPResponse {
err := h.app.Database.Ping(tctx) _, libVersionNumber, _ := sqlite3.Version()
if err != nil {
return ginresp.InternalError(err)
}
for _, subdb := range h.app.Database.List() { if libVersionNumber < 3039000 {
return ginresp.InternalError(errors.New("sqlite version too low"))
}
uuidKey, _ := langext.NewHexUUID() tctx := simplectx.CreateSimpleContext(ctx, nil)
uuidWrite, _ := langext.NewHexUUID()
err = subdb.WriteMetaString(tctx, uuidKey, uuidWrite) err := h.app.Database.Ping(tctx)
if err != nil { if err != nil {
return ginresp.InternalError(err) return ginresp.InternalError(err)
} }
uuidRead, err := subdb.ReadMetaString(tctx, uuidKey) for _, subdb := range h.app.Database.List() {
if err != nil {
return ginresp.InternalError(err) uuidKey, _ := langext.NewHexUUID()
uuidWrite, _ := langext.NewHexUUID()
err = subdb.WriteMetaString(tctx, uuidKey, uuidWrite)
if err != nil {
return ginresp.InternalError(err)
}
uuidRead, err := subdb.ReadMetaString(tctx, uuidKey)
if err != nil {
return ginresp.InternalError(err)
}
if uuidRead == nil || uuidWrite != *uuidRead {
return ginresp.InternalError(errors.New("writing into DB was not consistent"))
}
err = subdb.DeleteMeta(tctx, uuidKey)
if err != nil {
return ginresp.InternalError(err)
}
} }
if uuidRead == nil || uuidWrite != *uuidRead { return ginext.JSON(http.StatusOK, response{Status: "ok"})
return ginresp.InternalError(errors.New("writing into DB was not consistent"))
}
err = subdb.DeleteMeta(tctx, uuidKey) })
if err != nil {
return ginresp.InternalError(err)
}
}
return ginresp.JSON(http.StatusOK, response{Status: "ok"})
} }
// Sleep swaggerdoc // Sleep swaggerdoc
@@ -177,7 +202,7 @@ func (h CommonHandler) Health(g *gin.Context) ginresp.HTTPResponse {
// @Failure 500 {object} ginresp.apiError // @Failure 500 {object} ginresp.apiError
// //
// @Router /api/sleep/{secs} [post] // @Router /api/sleep/{secs} [post]
func (h CommonHandler) Sleep(g *gin.Context) ginresp.HTTPResponse { func (h CommonHandler) Sleep(pctx ginext.PreContext) ginext.HTTPResponse {
type uri struct { type uri struct {
Seconds float64 `uri:"secs"` Seconds float64 `uri:"secs"`
} }
@@ -187,33 +212,53 @@ func (h CommonHandler) Sleep(g *gin.Context) ginresp.HTTPResponse {
Duration float64 `json:"duration"` Duration float64 `json:"duration"`
} }
t0 := time.Now().Format(time.RFC3339Nano) ctx, g, errResp := pctx.Start()
if errResp != nil {
var u uri return *errResp
if err := g.ShouldBindUri(&u); err != nil {
return ginresp.APIError(g, 400, apierr.BINDFAIL_URI_PARAM, "Failed to read uri", err)
} }
defer ctx.Cancel()
time.Sleep(timeext.FromSeconds(u.Seconds)) return h.app.DoRequest(ctx, g, models.TLockRead, func(ctx *logic.AppContext, finishSuccess func(r ginext.HTTPResponse) ginext.HTTPResponse) ginext.HTTPResponse {
t1 := time.Now().Format(time.RFC3339Nano) t0 := time.Now().Format(time.RFC3339Nano)
var u uri
if err := g.ShouldBindUri(&u); err != nil {
return ginresp.APIError(g, 400, apierr.BINDFAIL_URI_PARAM, "Failed to read uri", err)
}
time.Sleep(timeext.FromSeconds(u.Seconds))
t1 := time.Now().Format(time.RFC3339Nano)
return ginext.JSON(http.StatusOK, response{
Start: t0,
End: t1,
Duration: u.Seconds,
})
return ginresp.JSON(http.StatusOK, response{
Start: t0,
End: t1,
Duration: u.Seconds,
}) })
} }
func (h CommonHandler) NoRoute(g *gin.Context) ginresp.HTTPResponse { func (h CommonHandler) NoRoute(pctx ginext.PreContext) ginext.HTTPResponse {
return ginresp.JSON(http.StatusNotFound, gin.H{ ctx, g, errResp := pctx.Start()
"": "================ ROUTE NOT FOUND ================", if errResp != nil {
"FullPath": g.FullPath(), return *errResp
"Method": g.Request.Method, }
"URL": g.Request.URL.String(), defer ctx.Cancel()
"RequestURI": g.Request.RequestURI,
"Proto": g.Request.Proto, return h.app.DoRequest(ctx, g, models.TLockRead, func(ctx *logic.AppContext, finishSuccess func(r ginext.HTTPResponse) ginext.HTTPResponse) ginext.HTTPResponse {
"Header": g.Request.Header,
"~": "================ ROUTE NOT FOUND ================", return ginext.JSON(http.StatusNotFound, gin.H{
"": "================ ROUTE NOT FOUND ================",
"FullPath": g.FullPath(),
"Method": g.Request.Method,
"URL": g.Request.URL.String(),
"RequestURI": g.Request.RequestURI,
"Proto": g.Request.Proto,
"Header": g.Request.Header,
"~": "================ ROUTE NOT FOUND ================",
})
}) })
} }
File diff suppressed because it is too large Load Diff
+59 -55
View File
@@ -7,7 +7,7 @@ import (
"blackforestbytes.com/simplecloudnotifier/logic" "blackforestbytes.com/simplecloudnotifier/logic"
"blackforestbytes.com/simplecloudnotifier/models" "blackforestbytes.com/simplecloudnotifier/models"
"fmt" "fmt"
"github.com/gin-gonic/gin" "gogs.mikescher.com/BlackForestBytes/goext/ginext"
"gogs.mikescher.com/BlackForestBytes/goext/langext" "gogs.mikescher.com/BlackForestBytes/goext/langext"
"net/http" "net/http"
"time" "time"
@@ -41,7 +41,7 @@ func NewExternalHandler(app *logic.Application) ExternalHandler {
// @Failure 500 {object} ginresp.apiError "An internal server error occurred - try again later" // @Failure 500 {object} ginresp.apiError "An internal server error occurred - try again later"
// //
// @Router /external/v1/uptime-kuma [POST] // @Router /external/v1/uptime-kuma [POST]
func (h ExternalHandler) UptimeKuma(g *gin.Context) ginresp.HTTPResponse { func (h ExternalHandler) UptimeKuma(pctx ginext.PreContext) ginext.HTTPResponse {
type query struct { type query struct {
UserID *models.UserID `form:"user_id" example:"7725"` UserID *models.UserID `form:"user_id" example:"7725"`
KeyToken *string `form:"key" example:"P3TNH8mvv14fm"` KeyToken *string `form:"key" example:"P3TNH8mvv14fm"`
@@ -74,61 +74,65 @@ func (h ExternalHandler) UptimeKuma(g *gin.Context) ginresp.HTTPResponse {
var b body var b body
var q query var q query
ctx, httpErr := h.app.StartRequest(g, nil, &q, &b, nil) ctx, g, errResp := pctx.Query(&q).Body(&b).Start()
if httpErr != nil {
return *httpErr
}
defer ctx.Cancel()
if b.Heartbeat == nil {
return ginresp.APIError(g, 400, apierr.BINDFAIL_BODY_PARAM, "missing field 'heartbeat' in request body", nil)
}
if b.Monitor == nil {
return ginresp.APIError(g, 400, apierr.BINDFAIL_BODY_PARAM, "missing field 'monitor' in request body", nil)
}
if b.Msg == nil {
return ginresp.APIError(g, 400, apierr.BINDFAIL_BODY_PARAM, "missing field 'msg' in request body", nil)
}
title := langext.Conditional(b.Heartbeat.Status == 1, fmt.Sprintf("Monitor %v is back online", b.Monitor.Name), fmt.Sprintf("Monitor %v went down!", b.Monitor.Name))
content := b.Heartbeat.Msg
var timestamp *float64 = nil
if tz, err := time.LoadLocation(b.Heartbeat.Timezone); err == nil {
if ts, err := time.ParseInLocation("2006-01-02 15:04:05", b.Heartbeat.LocalDateTime, tz); err == nil {
timestamp = langext.Ptr(float64(ts.Unix()))
}
}
var channel *string = nil
if q.Channel != nil {
channel = q.Channel
}
if q.ChannelUp != nil && b.Heartbeat.Status == 1 {
channel = q.ChannelUp
}
if q.ChannelDown != nil && b.Heartbeat.Status != 1 {
channel = q.ChannelDown
}
var priority *int = nil
if q.Priority != nil {
priority = q.Priority
}
if q.PriorityUp != nil && b.Heartbeat.Status == 1 {
priority = q.PriorityUp
}
if q.PriorityDown != nil && b.Heartbeat.Status != 1 {
priority = q.PriorityDown
}
okResp, errResp := h.app.SendMessage(g, ctx, q.UserID, q.KeyToken, channel, &title, &content, priority, nil, timestamp, q.SenderName)
if errResp != nil { if errResp != nil {
return *errResp return *errResp
} }
defer ctx.Cancel()
return ctx.FinishSuccess(ginresp.JSON(http.StatusOK, response{ return h.app.DoRequest(ctx, g, models.TLockReadWrite, func(ctx *logic.AppContext, finishSuccess func(r ginext.HTTPResponse) ginext.HTTPResponse) ginext.HTTPResponse {
MessageID: okResp.Message.MessageID,
})) if b.Heartbeat == nil {
return ginresp.APIError(g, 400, apierr.BINDFAIL_BODY_PARAM, "missing field 'heartbeat' in request body", nil)
}
if b.Monitor == nil {
return ginresp.APIError(g, 400, apierr.BINDFAIL_BODY_PARAM, "missing field 'monitor' in request body", nil)
}
if b.Msg == nil {
return ginresp.APIError(g, 400, apierr.BINDFAIL_BODY_PARAM, "missing field 'msg' in request body", nil)
}
title := langext.Conditional(b.Heartbeat.Status == 1, fmt.Sprintf("Monitor %v is back online", b.Monitor.Name), fmt.Sprintf("Monitor %v went down!", b.Monitor.Name))
content := b.Heartbeat.Msg
var timestamp *float64 = nil
if tz, err := time.LoadLocation(b.Heartbeat.Timezone); err == nil {
if ts, err := time.ParseInLocation("2006-01-02 15:04:05", b.Heartbeat.LocalDateTime, tz); err == nil {
timestamp = langext.Ptr(float64(ts.Unix()))
}
}
var channel *string = nil
if q.Channel != nil {
channel = q.Channel
}
if q.ChannelUp != nil && b.Heartbeat.Status == 1 {
channel = q.ChannelUp
}
if q.ChannelDown != nil && b.Heartbeat.Status != 1 {
channel = q.ChannelDown
}
var priority *int = nil
if q.Priority != nil {
priority = q.Priority
}
if q.PriorityUp != nil && b.Heartbeat.Status == 1 {
priority = q.PriorityUp
}
if q.PriorityDown != nil && b.Heartbeat.Status != 1 {
priority = q.PriorityDown
}
okResp, errResp := h.app.SendMessage(g, ctx, q.UserID, q.KeyToken, channel, &title, &content, priority, nil, timestamp, q.SenderName)
if errResp != nil {
return *errResp
}
return finishSuccess(ginext.JSON(http.StatusOK, response{
MessageID: okResp.Message.MessageID,
}))
})
} }
+26 -23
View File
@@ -2,12 +2,11 @@ package handler
import ( import (
"blackforestbytes.com/simplecloudnotifier/api/apierr" "blackforestbytes.com/simplecloudnotifier/api/apierr"
"blackforestbytes.com/simplecloudnotifier/api/ginresp"
primarydb "blackforestbytes.com/simplecloudnotifier/db/impl/primary" primarydb "blackforestbytes.com/simplecloudnotifier/db/impl/primary"
"blackforestbytes.com/simplecloudnotifier/logic" "blackforestbytes.com/simplecloudnotifier/logic"
"blackforestbytes.com/simplecloudnotifier/models" "blackforestbytes.com/simplecloudnotifier/models"
"github.com/gin-gonic/gin"
"gogs.mikescher.com/BlackForestBytes/goext/dataext" "gogs.mikescher.com/BlackForestBytes/goext/dataext"
"gogs.mikescher.com/BlackForestBytes/goext/ginext"
"gogs.mikescher.com/BlackForestBytes/goext/langext" "gogs.mikescher.com/BlackForestBytes/goext/langext"
"net/http" "net/http"
) )
@@ -49,7 +48,7 @@ func NewMessageHandler(app *logic.Application) MessageHandler {
// //
// @Router / [POST] // @Router / [POST]
// @Router /send [POST] // @Router /send [POST]
func (h MessageHandler) SendMessage(g *gin.Context) ginresp.HTTPResponse { func (h MessageHandler) SendMessage(pctx ginext.PreContext) ginext.HTTPResponse {
type combined struct { type combined struct {
UserID *models.UserID `json:"user_id" form:"user_id" example:"7725" ` UserID *models.UserID `json:"user_id" form:"user_id" example:"7725" `
KeyToken *string `json:"key" form:"key" example:"P3TNH8mvv14fm" ` KeyToken *string `json:"key" form:"key" example:"P3TNH8mvv14fm" `
@@ -78,30 +77,34 @@ func (h MessageHandler) SendMessage(g *gin.Context) ginresp.HTTPResponse {
var b combined var b combined
var q combined var q combined
var f combined var f combined
ctx, errResp := h.app.StartRequest(g, nil, &q, &b, &f, logic.RequestOptions{IgnoreWrongContentType: true}) ctx, g, errResp := pctx.Form(&f).Query(&q).Body(&b).IgnoreWrongContentType().Start()
if errResp != nil { if errResp != nil {
return *errResp return *errResp
} }
defer ctx.Cancel() defer ctx.Cancel()
// query has highest prio, then form, then json return h.app.DoRequest(ctx, g, models.TLockReadWrite, func(ctx *logic.AppContext, finishSuccess func(r ginext.HTTPResponse) ginext.HTTPResponse) ginext.HTTPResponse {
data := dataext.ObjectMerge(dataext.ObjectMerge(b, f), q)
okResp, errResp := h.app.SendMessage(g, ctx, data.UserID, data.KeyToken, data.Channel, data.Title, data.Content, data.Priority, data.UserMessageID, data.SendTimestamp, data.SenderName) // query has highest prio, then form, then json
if errResp != nil { data := dataext.ObjectMerge(dataext.ObjectMerge(b, f), q)
return *errResp
} else { okResp, errResp := h.app.SendMessage(g, ctx, data.UserID, data.KeyToken, data.Channel, data.Title, data.Content, data.Priority, data.UserMessageID, data.SendTimestamp, data.SenderName)
return ctx.FinishSuccess(ginresp.JSON(http.StatusOK, response{ if errResp != nil {
Success: true, return *errResp
ErrorID: apierr.NO_ERROR, } else {
ErrorHighlight: -1, return finishSuccess(ginext.JSON(http.StatusOK, response{
Message: langext.Conditional(okResp.MessageIsOld, "Message already sent", "Message sent"), Success: true,
SuppressSend: okResp.MessageIsOld, ErrorID: apierr.NO_ERROR,
MessageCount: okResp.User.MessagesSent, ErrorHighlight: -1,
Quota: okResp.User.QuotaUsedToday(), Message: langext.Conditional(okResp.MessageIsOld, "Message already sent", "Message sent"),
IsPro: okResp.User.IsPro, SuppressSend: okResp.MessageIsOld,
QuotaMax: okResp.User.QuotaPerDay(), MessageCount: okResp.User.MessagesSent,
SCNMessageID: okResp.Message.MessageID, Quota: okResp.User.QuotaUsedToday(),
})) IsPro: okResp.User.IsPro,
} QuotaMax: okResp.User.QuotaPerDay(),
SCNMessageID: okResp.Message.MessageID,
}))
}
})
} }
+95 -32
View File
@@ -3,10 +3,12 @@ package handler
import ( import (
"blackforestbytes.com/simplecloudnotifier/api/ginresp" "blackforestbytes.com/simplecloudnotifier/api/ginresp"
"blackforestbytes.com/simplecloudnotifier/logic" "blackforestbytes.com/simplecloudnotifier/logic"
"blackforestbytes.com/simplecloudnotifier/models"
"blackforestbytes.com/simplecloudnotifier/website" "blackforestbytes.com/simplecloudnotifier/website"
"errors" "errors"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"gogs.mikescher.com/BlackForestBytes/goext/ginext"
"gogs.mikescher.com/BlackForestBytes/goext/rext" "gogs.mikescher.com/BlackForestBytes/goext/rext"
"net/http" "net/http"
"regexp" "regexp"
@@ -27,60 +29,121 @@ func NewWebsiteHandler(app *logic.Application) WebsiteHandler {
} }
} }
func (h WebsiteHandler) Index(g *gin.Context) ginresp.HTTPResponse { func (h WebsiteHandler) Index(pctx ginext.PreContext) ginext.HTTPResponse {
return h.serveAsset(g, "index.html", true) ctx, g, errResp := pctx.Start()
if errResp != nil {
return *errResp
}
defer ctx.Cancel()
return h.app.DoRequest(ctx, g, models.TLockNone, func(ctx *logic.AppContext, finishSuccess func(r ginext.HTTPResponse) ginext.HTTPResponse) ginext.HTTPResponse {
return h.serveAsset(g, "index.html", true)
})
} }
func (h WebsiteHandler) APIDocs(g *gin.Context) ginresp.HTTPResponse { func (h WebsiteHandler) APIDocs(pctx ginext.PreContext) ginext.HTTPResponse {
return h.serveAsset(g, "api.html", true) ctx, g, errResp := pctx.Start()
if errResp != nil {
return *errResp
}
defer ctx.Cancel()
return h.app.DoRequest(ctx, g, models.TLockNone, func(ctx *logic.AppContext, finishSuccess func(r ginext.HTTPResponse) ginext.HTTPResponse) ginext.HTTPResponse {
return h.serveAsset(g, "api.html", true)
})
} }
func (h WebsiteHandler) APIDocsMore(g *gin.Context) ginresp.HTTPResponse { func (h WebsiteHandler) APIDocsMore(pctx ginext.PreContext) ginext.HTTPResponse {
return h.serveAsset(g, "api_more.html", true) ctx, g, errResp := pctx.Start()
if errResp != nil {
return *errResp
}
defer ctx.Cancel()
return h.app.DoRequest(ctx, g, models.TLockNone, func(ctx *logic.AppContext, finishSuccess func(r ginext.HTTPResponse) ginext.HTTPResponse) ginext.HTTPResponse {
return h.serveAsset(g, "api_more.html", true)
})
} }
func (h WebsiteHandler) MessageSent(g *gin.Context) ginresp.HTTPResponse { func (h WebsiteHandler) MessageSent(pctx ginext.PreContext) ginext.HTTPResponse {
return h.serveAsset(g, "message_sent.html", true) ctx, g, errResp := pctx.Start()
if errResp != nil {
return *errResp
}
defer ctx.Cancel()
return h.app.DoRequest(ctx, g, models.TLockNone, func(ctx *logic.AppContext, finishSuccess func(r ginext.HTTPResponse) ginext.HTTPResponse) ginext.HTTPResponse {
return h.serveAsset(g, "message_sent.html", true)
})
} }
func (h WebsiteHandler) FaviconIco(g *gin.Context) ginresp.HTTPResponse { func (h WebsiteHandler) FaviconIco(pctx ginext.PreContext) ginext.HTTPResponse {
return h.serveAsset(g, "favicon.ico", false) ctx, g, errResp := pctx.Start()
if errResp != nil {
return *errResp
}
defer ctx.Cancel()
return h.app.DoRequest(ctx, g, models.TLockNone, func(ctx *logic.AppContext, finishSuccess func(r ginext.HTTPResponse) ginext.HTTPResponse) ginext.HTTPResponse {
return h.serveAsset(g, "favicon.ico", false)
})
} }
func (h WebsiteHandler) FaviconPNG(g *gin.Context) ginresp.HTTPResponse { func (h WebsiteHandler) FaviconPNG(pctx ginext.PreContext) ginext.HTTPResponse {
return h.serveAsset(g, "favicon.png", false) ctx, g, errResp := pctx.Start()
if errResp != nil {
return *errResp
}
defer ctx.Cancel()
return h.app.DoRequest(ctx, g, models.TLockNone, func(ctx *logic.AppContext, finishSuccess func(r ginext.HTTPResponse) ginext.HTTPResponse) ginext.HTTPResponse {
return h.serveAsset(g, "favicon.png", false)
})
} }
func (h WebsiteHandler) Javascript(g *gin.Context) ginresp.HTTPResponse { func (h WebsiteHandler) Javascript(pctx ginext.PreContext) ginext.HTTPResponse {
ctx, g, errResp := pctx.Start()
if errResp != nil {
return *errResp
}
defer ctx.Cancel()
return h.app.DoRequest(ctx, g, models.TLockNone, func(ctx *logic.AppContext, finishSuccess func(r ginext.HTTPResponse) ginext.HTTPResponse) ginext.HTTPResponse {
type uri struct {
Filename string `uri:"fn"`
}
var u uri
if err := g.ShouldBindUri(&u); err != nil {
return ginext.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
}
return h.serveAsset(g, "js/"+u.Filename, false)
})
}
func (h WebsiteHandler) CSS(pctx ginext.PreContext) ginext.HTTPResponse {
type uri struct { type uri struct {
Filename string `uri:"fn"` Filename string `uri:"fn"`
} }
var u uri var u uri
if err := g.ShouldBindUri(&u); err != nil { ctx, g, errResp := pctx.URI(&u).Start()
return ginresp.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) if errResp != nil {
return *errResp
} }
defer ctx.Cancel()
return h.serveAsset(g, "js/"+u.Filename, false) return h.app.DoRequest(ctx, g, models.TLockNone, func(ctx *logic.AppContext, finishSuccess func(r ginext.HTTPResponse) ginext.HTTPResponse) ginext.HTTPResponse {
return h.serveAsset(g, "css/"+u.Filename, false)
})
} }
func (h WebsiteHandler) CSS(g *gin.Context) ginresp.HTTPResponse { func (h WebsiteHandler) serveAsset(g *gin.Context, fn string, repl bool) ginext.HTTPResponse {
type uri struct {
Filename string `uri:"fn"`
}
var u uri
if err := g.ShouldBindUri(&u); err != nil {
return ginresp.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
}
return h.serveAsset(g, "css/"+u.Filename, false)
}
func (h WebsiteHandler) serveAsset(g *gin.Context, fn string, repl bool) ginresp.HTTPResponse {
_data, err := website.Assets.ReadFile(fn) _data, err := website.Assets.ReadFile(fn)
if err != nil { if err != nil {
return ginresp.Status(http.StatusNotFound) return ginext.Status(http.StatusNotFound)
} }
data := string(_data) data := string(_data)
@@ -141,7 +204,7 @@ func (h WebsiteHandler) serveAsset(g *gin.Context, fn string, repl bool) ginresp
mime = "image/svg+xml" mime = "image/svg+xml"
} }
return ginresp.Data(http.StatusOK, mime, []byte(data)) return ginext.Data(http.StatusOK, mime, []byte(data))
} }
func (h WebsiteHandler) getReplConfig(key string) (string, bool) { func (h WebsiteHandler) getReplConfig(key string) (string, bool) {
+74 -80
View File
@@ -1,16 +1,14 @@
package api package api
import ( import (
"blackforestbytes.com/simplecloudnotifier/api/ginext"
"blackforestbytes.com/simplecloudnotifier/api/ginresp"
"blackforestbytes.com/simplecloudnotifier/api/handler" "blackforestbytes.com/simplecloudnotifier/api/handler"
"blackforestbytes.com/simplecloudnotifier/logic" "blackforestbytes.com/simplecloudnotifier/logic"
"blackforestbytes.com/simplecloudnotifier/models" "blackforestbytes.com/simplecloudnotifier/models"
"blackforestbytes.com/simplecloudnotifier/swagger" "blackforestbytes.com/simplecloudnotifier/swagger"
"errors" "errors"
"github.com/gin-gonic/gin"
"github.com/gin-gonic/gin/binding" "github.com/gin-gonic/gin/binding"
"github.com/go-playground/validator/v10" "github.com/go-playground/validator/v10"
"gogs.mikescher.com/BlackForestBytes/goext/ginext"
) )
type Router struct { type Router struct {
@@ -50,7 +48,7 @@ func NewRouter(app *logic.Application) *Router {
// @tag.name Common // @tag.name Common
// //
// @BasePath / // @BasePath /
func (r *Router) Init(e *gin.Engine) error { func (r *Router) Init(e *ginext.GinWrapper) error {
if v, ok := binding.Validator.Engine().(*validator.Validate); ok { if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
err := v.RegisterValidation("entityid", models.ValidateEntityID, true) err := v.RegisterValidation("entityid", models.ValidateEntityID, true)
@@ -63,129 +61,125 @@ func (r *Router) Init(e *gin.Engine) error {
// ================ General (unversioned) ================ // ================ General (unversioned) ================
commonAPI := e.Group("/api") commonAPI := e.Routes().Group("/api")
{ {
commonAPI.Any("/ping", r.Wrap(r.commonHandler.Ping)) commonAPI.Any("/ping").Handle(r.commonHandler.Ping)
commonAPI.POST("/db-test", r.Wrap(r.commonHandler.DatabaseTest)) commonAPI.POST("/db-test").Handle(r.commonHandler.DatabaseTest)
commonAPI.GET("/health", r.Wrap(r.commonHandler.Health)) commonAPI.GET("/health").Handle(r.commonHandler.Health)
commonAPI.POST("/sleep/:secs", r.Wrap(r.commonHandler.Sleep)) commonAPI.POST("/sleep/:secs").Handle(r.commonHandler.Sleep)
} }
// ================ Swagger ================ // ================ Swagger ================
docs := e.Group("/documentation") docs := e.Routes().Group("/documentation")
{ {
docs.GET("/swagger", ginext.RedirectTemporary("/documentation/swagger/")) docs.GET("/swagger").Handle(ginext.RedirectTemporary("/documentation/swagger/"))
docs.GET("/swagger/*sub", r.Wrap(swagger.Handle)) docs.GET("/swagger/*sub").Handle(swagger.Handle)
} }
// ================ Website ================ // ================ Website ================
frontend := e.Group("") frontend := e.Routes().Group("")
{ {
frontend.GET("/", r.Wrap(r.websiteHandler.Index)) frontend.GET("/").Handle(r.websiteHandler.Index)
frontend.GET("/index.php", r.Wrap(r.websiteHandler.Index)) frontend.GET("/index.php").Handle(r.websiteHandler.Index)
frontend.GET("/index.html", r.Wrap(r.websiteHandler.Index)) frontend.GET("/index.html").Handle(r.websiteHandler.Index)
frontend.GET("/index", r.Wrap(r.websiteHandler.Index)) frontend.GET("/index").Handle(r.websiteHandler.Index)
frontend.GET("/api", r.Wrap(r.websiteHandler.APIDocs)) frontend.GET("/api").Handle(r.websiteHandler.APIDocs)
frontend.GET("/api.php", r.Wrap(r.websiteHandler.APIDocs)) frontend.GET("/api.php").Handle(r.websiteHandler.APIDocs)
frontend.GET("/api.html", r.Wrap(r.websiteHandler.APIDocs)) frontend.GET("/api.html").Handle(r.websiteHandler.APIDocs)
frontend.GET("/api_more", r.Wrap(r.websiteHandler.APIDocsMore)) frontend.GET("/api_more").Handle(r.websiteHandler.APIDocsMore)
frontend.GET("/api_more.php", r.Wrap(r.websiteHandler.APIDocsMore)) frontend.GET("/api_more.php").Handle(r.websiteHandler.APIDocsMore)
frontend.GET("/api_more.html", r.Wrap(r.websiteHandler.APIDocsMore)) frontend.GET("/api_more.html").Handle(r.websiteHandler.APIDocsMore)
frontend.GET("/message_sent", r.Wrap(r.websiteHandler.MessageSent)) frontend.GET("/message_sent").Handle(r.websiteHandler.MessageSent)
frontend.GET("/message_sent.php", r.Wrap(r.websiteHandler.MessageSent)) frontend.GET("/message_sent.php").Handle(r.websiteHandler.MessageSent)
frontend.GET("/message_sent.html", r.Wrap(r.websiteHandler.MessageSent)) frontend.GET("/message_sent.html").Handle(r.websiteHandler.MessageSent)
frontend.GET("/favicon.ico", r.Wrap(r.websiteHandler.FaviconIco)) frontend.GET("/favicon.ico").Handle(r.websiteHandler.FaviconIco)
frontend.GET("/favicon.png", r.Wrap(r.websiteHandler.FaviconPNG)) frontend.GET("/favicon.png").Handle(r.websiteHandler.FaviconPNG)
frontend.GET("/js/:fn", r.Wrap(r.websiteHandler.Javascript)) frontend.GET("/js/:fn").Handle(r.websiteHandler.Javascript)
frontend.GET("/css/:fn", r.Wrap(r.websiteHandler.CSS)) frontend.GET("/css/:fn").Handle(r.websiteHandler.CSS)
} }
// ================ Compat (v1) ================ // ================ Compat (v1) ================
compat := e.Group("/api") compat := e.Routes().Group("/api")
{ {
compat.GET("/register.php", r.Wrap(r.compatHandler.Register)) compat.GET("/register.php").Handle(r.compatHandler.Register)
compat.GET("/info.php", r.Wrap(r.compatHandler.Info)) compat.GET("/info.php").Handle(r.compatHandler.Info)
compat.GET("/ack.php", r.Wrap(r.compatHandler.Ack)) compat.GET("/ack.php").Handle(r.compatHandler.Ack)
compat.GET("/requery.php", r.Wrap(r.compatHandler.Requery)) compat.GET("/requery.php").Handle(r.compatHandler.Requery)
compat.GET("/update.php", r.Wrap(r.compatHandler.Update)) compat.GET("/update.php").Handle(r.compatHandler.Update)
compat.GET("/expand.php", r.Wrap(r.compatHandler.Expand)) compat.GET("/expand.php").Handle(r.compatHandler.Expand)
compat.GET("/upgrade.php", r.Wrap(r.compatHandler.Upgrade)) compat.GET("/upgrade.php").Handle(r.compatHandler.Upgrade)
} }
// ================ Manage API (v2) ================ // ================ Manage API (v2) ================
apiv2 := e.Group("/api/v2/") apiv2 := e.Routes().Group("/api/v2/")
{ {
apiv2.POST("/users", r.Wrap(r.apiHandler.CreateUser)) apiv2.POST("/users").Handle(r.apiHandler.CreateUser)
apiv2.GET("/users/:uid", r.Wrap(r.apiHandler.GetUser)) apiv2.GET("/users/:uid").Handle(r.apiHandler.GetUser)
apiv2.PATCH("/users/:uid", r.Wrap(r.apiHandler.UpdateUser)) apiv2.PATCH("/users/:uid").Handle(r.apiHandler.UpdateUser)
apiv2.GET("/users/:uid/keys", r.Wrap(r.apiHandler.ListUserKeys)) apiv2.GET("/users/:uid/keys").Handle(r.apiHandler.ListUserKeys)
apiv2.POST("/users/:uid/keys", r.Wrap(r.apiHandler.CreateUserKey)) apiv2.POST("/users/:uid/keys").Handle(r.apiHandler.CreateUserKey)
apiv2.GET("/users/:uid/keys/current", r.Wrap(r.apiHandler.GetCurrentUserKey)) apiv2.GET("/users/:uid/keys/current").Handle(r.apiHandler.GetCurrentUserKey)
apiv2.GET("/users/:uid/keys/:kid", r.Wrap(r.apiHandler.GetUserKey)) apiv2.GET("/users/:uid/keys/:kid").Handle(r.apiHandler.GetUserKey)
apiv2.PATCH("/users/:uid/keys/:kid", r.Wrap(r.apiHandler.UpdateUserKey)) apiv2.PATCH("/users/:uid/keys/:kid").Handle(r.apiHandler.UpdateUserKey)
apiv2.DELETE("/users/:uid/keys/:kid", r.Wrap(r.apiHandler.DeleteUserKey)) apiv2.DELETE("/users/:uid/keys/:kid").Handle(r.apiHandler.DeleteUserKey)
apiv2.GET("/users/:uid/clients", r.Wrap(r.apiHandler.ListClients)) apiv2.GET("/users/:uid/clients").Handle(r.apiHandler.ListClients)
apiv2.GET("/users/:uid/clients/:cid", r.Wrap(r.apiHandler.GetClient)) apiv2.GET("/users/:uid/clients/:cid").Handle(r.apiHandler.GetClient)
apiv2.PATCH("/users/:uid/clients/:cid", r.Wrap(r.apiHandler.UpdateClient)) apiv2.PATCH("/users/:uid/clients/:cid").Handle(r.apiHandler.UpdateClient)
apiv2.POST("/users/:uid/clients", r.Wrap(r.apiHandler.AddClient)) apiv2.POST("/users/:uid/clients").Handle(r.apiHandler.AddClient)
apiv2.DELETE("/users/:uid/clients/:cid", r.Wrap(r.apiHandler.DeleteClient)) apiv2.DELETE("/users/:uid/clients/:cid").Handle(r.apiHandler.DeleteClient)
apiv2.GET("/users/:uid/channels", r.Wrap(r.apiHandler.ListChannels)) apiv2.GET("/users/:uid/channels").Handle(r.apiHandler.ListChannels)
apiv2.POST("/users/:uid/channels", r.Wrap(r.apiHandler.CreateChannel)) apiv2.POST("/users/:uid/channels").Handle(r.apiHandler.CreateChannel)
apiv2.GET("/users/:uid/channels/:cid", r.Wrap(r.apiHandler.GetChannel)) apiv2.GET("/users/:uid/channels/:cid").Handle(r.apiHandler.GetChannel)
apiv2.PATCH("/users/:uid/channels/:cid", r.Wrap(r.apiHandler.UpdateChannel)) apiv2.PATCH("/users/:uid/channels/:cid").Handle(r.apiHandler.UpdateChannel)
apiv2.GET("/users/:uid/channels/:cid/messages", r.Wrap(r.apiHandler.ListChannelMessages)) apiv2.GET("/users/:uid/channels/:cid/messages").Handle(r.apiHandler.ListChannelMessages)
apiv2.GET("/users/:uid/channels/:cid/subscriptions", r.Wrap(r.apiHandler.ListChannelSubscriptions)) apiv2.GET("/users/:uid/channels/:cid/subscriptions").Handle(r.apiHandler.ListChannelSubscriptions)
apiv2.GET("/users/:uid/subscriptions", r.Wrap(r.apiHandler.ListUserSubscriptions)) apiv2.GET("/users/:uid/subscriptions").Handle(r.apiHandler.ListUserSubscriptions)
apiv2.POST("/users/:uid/subscriptions", r.Wrap(r.apiHandler.CreateSubscription)) apiv2.POST("/users/:uid/subscriptions").Handle(r.apiHandler.CreateSubscription)
apiv2.GET("/users/:uid/subscriptions/:sid", r.Wrap(r.apiHandler.GetSubscription)) apiv2.GET("/users/:uid/subscriptions/:sid").Handle(r.apiHandler.GetSubscription)
apiv2.DELETE("/users/:uid/subscriptions/:sid", r.Wrap(r.apiHandler.CancelSubscription)) apiv2.DELETE("/users/:uid/subscriptions/:sid").Handle(r.apiHandler.CancelSubscription)
apiv2.PATCH("/users/:uid/subscriptions/:sid", r.Wrap(r.apiHandler.UpdateSubscription)) apiv2.PATCH("/users/:uid/subscriptions/:sid").Handle(r.apiHandler.UpdateSubscription)
apiv2.GET("/messages", r.Wrap(r.apiHandler.ListMessages)) apiv2.GET("/messages").Handle(r.apiHandler.ListMessages)
apiv2.GET("/messages/:mid", r.Wrap(r.apiHandler.GetMessage)) apiv2.GET("/messages/:mid").Handle(r.apiHandler.GetMessage)
apiv2.DELETE("/messages/:mid", r.Wrap(r.apiHandler.DeleteMessage)) apiv2.DELETE("/messages/:mid").Handle(r.apiHandler.DeleteMessage)
apiv2.GET("/preview/users/:uid", r.Wrap(r.apiHandler.GetUserPreview)) apiv2.GET("/preview/users/:uid").Handle(r.apiHandler.GetUserPreview)
apiv2.GET("/preview/keys/:kid", r.Wrap(r.apiHandler.GetUserKeyPreview)) apiv2.GET("/preview/keys/:kid").Handle(r.apiHandler.GetUserKeyPreview)
apiv2.GET("/preview/channels/:cid", r.Wrap(r.apiHandler.GetChannelPreview)) apiv2.GET("/preview/channels/:cid").Handle(r.apiHandler.GetChannelPreview)
} }
// ================ Send API (unversioned) ================ // ================ Send API (unversioned) ================
sendAPI := e.Group("") sendAPI := e.Routes().Group("")
{ {
sendAPI.POST("/", r.Wrap(r.messageHandler.SendMessage)) sendAPI.POST("/").Handle(r.messageHandler.SendMessage)
sendAPI.POST("/send", r.Wrap(r.messageHandler.SendMessage)) sendAPI.POST("/send").Handle(r.messageHandler.SendMessage)
sendAPI.POST("/send.php", r.Wrap(r.compatHandler.SendMessage)) sendAPI.POST("/send.php").Handle(r.compatHandler.SendMessage)
sendAPI.POST("/external/v1/uptime-kuma", r.Wrap(r.externalHandler.UptimeKuma)) sendAPI.POST("/external/v1/uptime-kuma").Handle(r.externalHandler.UptimeKuma)
} }
// ================ // ================
if r.app.Config.ReturnRawErrors { if r.app.Config.ReturnRawErrors {
e.NoRoute(r.Wrap(r.commonHandler.NoRoute)) e.NoRoute(r.commonHandler.NoRoute)
} }
// ================ // ================
return nil return nil
} }
func (r *Router) Wrap(fn ginresp.WHandlerFunc) gin.HandlerFunc {
return ginresp.Wrap(r.app, fn)
}
+9 -5
View File
@@ -3,9 +3,11 @@ package main
import ( import (
"blackforestbytes.com/simplecloudnotifier/db/schema" "blackforestbytes.com/simplecloudnotifier/db/schema"
"context" "context"
"database/sql"
"fmt" "fmt"
"github.com/mattn/go-sqlite3" "github.com/glebarez/go-sqlite"
"gogs.mikescher.com/BlackForestBytes/goext/exerr" "gogs.mikescher.com/BlackForestBytes/goext/exerr"
"gogs.mikescher.com/BlackForestBytes/goext/langext"
"gogs.mikescher.com/BlackForestBytes/goext/sq" "gogs.mikescher.com/BlackForestBytes/goext/sq"
"time" "time"
) )
@@ -16,12 +18,14 @@ func main() {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel() defer cancel()
sqlite3.Version() // ensure slite3 loaded if !langext.InArray("sqlite3", sql.Drivers()) {
sqlite.RegisterAsSQLITE3()
}
fmt.Println() fmt.Println()
for i := 2; i <= schema.PrimarySchemaVersion; i++ { for i := 2; i <= schema.PrimarySchemaVersion; i++ {
h0, err := sq.HashMattnSqliteSchema(ctx, schema.PrimarySchema[i].SQL) h0, err := sq.HashGoSqliteSchema(ctx, schema.PrimarySchema[i].SQL)
if err != nil { if err != nil {
h0 = "ERR" h0 = "ERR"
} }
@@ -29,7 +33,7 @@ func main() {
} }
for i := 1; i <= schema.RequestsSchemaVersion; i++ { for i := 1; i <= schema.RequestsSchemaVersion; i++ {
h0, err := sq.HashMattnSqliteSchema(ctx, schema.RequestsSchema[i].SQL) h0, err := sq.HashGoSqliteSchema(ctx, schema.RequestsSchema[i].SQL)
if err != nil { if err != nil {
h0 = "ERR" h0 = "ERR"
} }
@@ -37,7 +41,7 @@ func main() {
} }
for i := 1; i <= schema.LogsSchemaVersion; i++ { for i := 1; i <= schema.LogsSchemaVersion; i++ {
h0, err := sq.HashMattnSqliteSchema(ctx, schema.LogsSchema[i].SQL) h0, err := sq.HashGoSqliteSchema(ctx, schema.LogsSchema[i].SQL)
if err != nil { if err != nil {
h0 = "ERR" h0 = "ERR"
} }
+1 -1
View File
@@ -135,7 +135,7 @@ func main() {
if err != nil { if err != nil {
panic(err) panic(err)
} }
dbold := sq.NewDB(_dbold) dbold := sq.NewDB(_dbold, sq.DBOptions{})
rowsUser, err := dbold.Query(ctx, "SELECT * FROM users", sq.PP{}) rowsUser, err := dbold.Query(ctx, "SELECT * FROM users", sq.PP{})
if err != nil { if err != nil {
+10 -2
View File
@@ -3,13 +3,15 @@ package main
import ( import (
scn "blackforestbytes.com/simplecloudnotifier" scn "blackforestbytes.com/simplecloudnotifier"
"blackforestbytes.com/simplecloudnotifier/api" "blackforestbytes.com/simplecloudnotifier/api"
"blackforestbytes.com/simplecloudnotifier/api/ginext"
"blackforestbytes.com/simplecloudnotifier/google" "blackforestbytes.com/simplecloudnotifier/google"
"blackforestbytes.com/simplecloudnotifier/jobs" "blackforestbytes.com/simplecloudnotifier/jobs"
"blackforestbytes.com/simplecloudnotifier/logic" "blackforestbytes.com/simplecloudnotifier/logic"
"blackforestbytes.com/simplecloudnotifier/push" "blackforestbytes.com/simplecloudnotifier/push"
"fmt" "fmt"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"gogs.mikescher.com/BlackForestBytes/goext/ginext"
"gogs.mikescher.com/BlackForestBytes/goext/langext"
"time"
) )
func main() { func main() {
@@ -31,7 +33,13 @@ func main() {
return return
} }
ginengine := ginext.NewEngine(conf) ginengine := ginext.NewEngine(ginext.Options{
AllowCors: &conf.Cors,
GinDebug: &conf.GinDebug,
BufferBody: langext.PTrue,
Timeout: langext.Ptr(time.Duration(int64(conf.RequestTimeout) * int64(conf.RequestMaxRetry))),
BuildRequestBindError: logic.BuildGinRequestError,
})
router := api.NewRouter(app) router := api.NewRouter(app)
+14 -3
View File
@@ -5,12 +5,13 @@ import (
"blackforestbytes.com/simplecloudnotifier/db/dbtools" "blackforestbytes.com/simplecloudnotifier/db/dbtools"
"blackforestbytes.com/simplecloudnotifier/db/schema" "blackforestbytes.com/simplecloudnotifier/db/schema"
"blackforestbytes.com/simplecloudnotifier/db/simplectx" "blackforestbytes.com/simplecloudnotifier/db/simplectx"
"blackforestbytes.com/simplecloudnotifier/models"
"context" "context"
"database/sql" "database/sql"
"errors" "errors"
"fmt" "fmt"
"github.com/glebarez/go-sqlite"
"github.com/jmoiron/sqlx" "github.com/jmoiron/sqlx"
_ "github.com/mattn/go-sqlite3"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"gogs.mikescher.com/BlackForestBytes/goext/langext" "gogs.mikescher.com/BlackForestBytes/goext/langext"
"gogs.mikescher.com/BlackForestBytes/goext/sq" "gogs.mikescher.com/BlackForestBytes/goext/sq"
@@ -26,7 +27,16 @@ type Database struct {
func NewLogsDatabase(cfg server.Config) (*Database, error) { func NewLogsDatabase(cfg server.Config) (*Database, error) {
conf := cfg.DBLogs conf := cfg.DBLogs
url := fmt.Sprintf("file:%s?_journal=%s&_timeout=%d&_fk=%s&_busy_timeout=%d", conf.File, conf.Journal, conf.Timeout.Milliseconds(), langext.FormatBool(conf.CheckForeignKeys, "true", "false"), conf.BusyTimeout.Milliseconds()) url := fmt.Sprintf("file:%s?_pragma=journal_mode(%s)&_pragma=timeout(%d)&_pragma=foreign_keys(%s)&_pragma=busy_timeout(%d)",
conf.File,
conf.Journal,
conf.Timeout.Milliseconds(),
langext.FormatBool(conf.CheckForeignKeys, "true", "false"),
conf.BusyTimeout.Milliseconds())
if !langext.InArray("sqlite3", sql.Drivers()) {
sqlite.RegisterAsSQLITE3()
}
xdb, err := sqlx.Open("sqlite3", url) xdb, err := sqlx.Open("sqlite3", url)
if err != nil { if err != nil {
@@ -42,7 +52,8 @@ func NewLogsDatabase(cfg server.Config) (*Database, error) {
xdb.SetConnMaxIdleTime(60 * time.Minute) xdb.SetConnMaxIdleTime(60 * time.Minute)
} }
qqdb := sq.NewDB(xdb, sq.DBOptions{}) qqdb := sq.NewDB(xdb, sq.DBOptions{RegisterDefaultConverter: langext.PTrue, RegisterCommentTrimmer: langext.PTrue})
models.RegisterConverter(qqdb)
if conf.EnableLogger { if conf.EnableLogger {
qqdb.AddListener(dbtools.DBLogger{}) qqdb.AddListener(dbtools.DBLogger{})
+20 -79
View File
@@ -3,8 +3,6 @@ package primary
import ( import (
"blackforestbytes.com/simplecloudnotifier/db" "blackforestbytes.com/simplecloudnotifier/db"
"blackforestbytes.com/simplecloudnotifier/models" "blackforestbytes.com/simplecloudnotifier/models"
"database/sql"
"errors"
"gogs.mikescher.com/BlackForestBytes/goext/sq" "gogs.mikescher.com/BlackForestBytes/goext/sq"
"time" "time"
) )
@@ -15,23 +13,7 @@ func (db *Database) GetChannelByName(ctx db.TxContext, userid models.UserID, cha
return nil, err return nil, err
} }
rows, err := tx.Query(ctx, "SELECT * FROM channels WHERE owner_user_id = :uid AND internal_name = :nam LIMIT 1", sq.PP{ return sq.QuerySingleOpt[models.Channel](ctx, tx, "SELECT * FROM channels WHERE owner_user_id = :uid AND internal_name = :nam LIMIT 1", sq.PP{"uid": userid, "nam": chanName}, sq.SModeExtended, sq.Safe)
"uid": userid,
"nam": chanName,
})
if err != nil {
return nil, err
}
channel, err := models.DecodeChannel(ctx, tx, rows)
if errors.Is(err, sql.ErrNoRows) {
return nil, nil
}
if err != nil {
return nil, err
}
return &channel, nil
} }
func (db *Database) GetChannelByID(ctx db.TxContext, chanid models.ChannelID) (*models.Channel, error) { func (db *Database) GetChannelByID(ctx db.TxContext, chanid models.ChannelID) (*models.Channel, error) {
@@ -40,22 +22,7 @@ func (db *Database) GetChannelByID(ctx db.TxContext, chanid models.ChannelID) (*
return nil, err return nil, err
} }
rows, err := tx.Query(ctx, "SELECT * FROM channels WHERE channel_id = :cid LIMIT 1", sq.PP{ return sq.QuerySingleOpt[models.Channel](ctx, tx, "SELECT * FROM channels WHERE channel_id = :cid LIMIT 1", sq.PP{"cid": chanid}, sq.SModeExtended, sq.Safe)
"cid": chanid,
})
if err != nil {
return nil, err
}
channel, err := models.DecodeChannel(ctx, tx, rows)
if errors.Is(err, sql.ErrNoRows) {
return nil, nil
}
if err != nil {
return nil, err
}
return &channel, nil
} }
type CreateChanel struct { type CreateChanel struct {
@@ -72,14 +39,14 @@ func (db *Database) CreateChannel(ctx db.TxContext, userid models.UserID, dispNa
return models.Channel{}, err return models.Channel{}, err
} }
entity := models.ChannelDB{ entity := models.Channel{
ChannelID: models.NewChannelID(), ChannelID: models.NewChannelID(),
OwnerUserID: userid, OwnerUserID: userid,
DisplayName: dispName, DisplayName: dispName,
InternalName: intName, InternalName: intName,
SubscribeKey: subscribeKey, SubscribeKey: subscribeKey,
DescriptionName: description, DescriptionName: description,
TimestampCreated: time2DB(time.Now()), TimestampCreated: models.NowSCNTime(),
TimestampLastSent: nil, TimestampLastSent: nil,
MessagesSent: 0, MessagesSent: 0,
} }
@@ -89,7 +56,7 @@ func (db *Database) CreateChannel(ctx db.TxContext, userid models.UserID, dispNa
return models.Channel{}, err return models.Channel{}, err
} }
return entity.Model(), nil return entity, nil
} }
func (db *Database) ListChannelsByOwner(ctx db.TxContext, userid models.UserID, subUserID models.UserID) ([]models.ChannelWithSubscription, error) { func (db *Database) ListChannelsByOwner(ctx db.TxContext, userid models.UserID, subUserID models.UserID) ([]models.ChannelWithSubscription, error) {
@@ -100,20 +67,14 @@ func (db *Database) ListChannelsByOwner(ctx db.TxContext, userid models.UserID,
order := " ORDER BY channels.timestamp_created ASC, channels.channel_id ASC " order := " ORDER BY channels.timestamp_created ASC, channels.channel_id ASC "
rows, err := tx.Query(ctx, "SELECT channels.*, sub.* FROM channels LEFT JOIN subscriptions AS sub ON channels.channel_id = sub.channel_id AND sub.subscriber_user_id = :subuid WHERE owner_user_id = :ouid"+order, sq.PP{ sql := "SELECT channels.*, sub.* FROM channels LEFT JOIN subscriptions AS sub ON channels.channel_id = sub.channel_id AND sub.subscriber_user_id = :subuid WHERE owner_user_id = :ouid" + order
pp := sq.PP{
"ouid": userid, "ouid": userid,
"subuid": subUserID, "subuid": subUserID,
})
if err != nil {
return nil, err
} }
data, err := models.DecodeChannelsWithSubscription(ctx, tx, rows) return sq.QueryAll[models.ChannelWithSubscription](ctx, tx, sql, pp, sq.SModeExtended, sq.Safe)
if err != nil {
return nil, err
}
return data, nil
} }
func (db *Database) ListChannelsBySubscriber(ctx db.TxContext, userid models.UserID, confirmed *bool) ([]models.ChannelWithSubscription, error) { func (db *Database) ListChannelsBySubscriber(ctx db.TxContext, userid models.UserID, confirmed *bool) ([]models.ChannelWithSubscription, error) {
@@ -131,19 +92,13 @@ func (db *Database) ListChannelsBySubscriber(ctx db.TxContext, userid models.Use
order := " ORDER BY channels.timestamp_created ASC, channels.channel_id ASC " order := " ORDER BY channels.timestamp_created ASC, channels.channel_id ASC "
rows, err := tx.Query(ctx, "SELECT channels.*, sub.* FROM channels LEFT JOIN subscriptions AS sub on channels.channel_id = sub.channel_id AND sub.subscriber_user_id = :subuid WHERE sub.subscription_id IS NOT NULL "+confCond+order, sq.PP{ sql := "SELECT channels.*, sub.* FROM channels LEFT JOIN subscriptions AS sub on channels.channel_id = sub.channel_id AND sub.subscriber_user_id = :subuid WHERE sub.subscription_id IS NOT NULL " + confCond + order
pp := sq.PP{
"subuid": userid, "subuid": userid,
})
if err != nil {
return nil, err
} }
data, err := models.DecodeChannelsWithSubscription(ctx, tx, rows) return sq.QueryAll[models.ChannelWithSubscription](ctx, tx, sql, pp, sq.SModeExtended, sq.Safe)
if err != nil {
return nil, err
}
return data, nil
} }
func (db *Database) ListChannelsByAccess(ctx db.TxContext, userid models.UserID, confirmed *bool) ([]models.ChannelWithSubscription, error) { func (db *Database) ListChannelsByAccess(ctx db.TxContext, userid models.UserID, confirmed *bool) ([]models.ChannelWithSubscription, error) {
@@ -161,20 +116,14 @@ func (db *Database) ListChannelsByAccess(ctx db.TxContext, userid models.UserID,
order := " ORDER BY channels.timestamp_created ASC, channels.channel_id ASC " order := " ORDER BY channels.timestamp_created ASC, channels.channel_id ASC "
rows, err := tx.Query(ctx, "SELECT channels.*, sub.* FROM channels LEFT JOIN subscriptions AS sub on channels.channel_id = sub.channel_id AND sub.subscriber_user_id = :subuid WHERE owner_user_id = :ouid "+confCond+order, sq.PP{ sql := "SELECT channels.*, sub.* FROM channels LEFT JOIN subscriptions AS sub on channels.channel_id = sub.channel_id AND sub.subscriber_user_id = :subuid WHERE owner_user_id = :ouid " + confCond + order
pp := sq.PP{
"ouid": userid, "ouid": userid,
"subuid": userid, "subuid": userid,
})
if err != nil {
return nil, err
} }
data, err := models.DecodeChannelsWithSubscription(ctx, tx, rows) return sq.QueryAll[models.ChannelWithSubscription](ctx, tx, sql, pp, sq.SModeExtended, sq.Safe)
if err != nil {
return nil, err
}
return data, nil
} }
func (db *Database) GetChannel(ctx db.TxContext, userid models.UserID, channelid models.ChannelID, enforceOwner bool) (models.ChannelWithSubscription, error) { func (db *Database) GetChannel(ctx db.TxContext, userid models.UserID, channelid models.ChannelID, enforceOwner bool) (models.ChannelWithSubscription, error) {
@@ -198,17 +147,9 @@ func (db *Database) GetChannel(ctx db.TxContext, userid models.UserID, channelid
params["ouid"] = userid params["ouid"] = userid
} }
rows, err := tx.Query(ctx, "SELECT "+selectors+" FROM channels "+join+" WHERE "+cond+" LIMIT 1", params) sql := "SELECT " + selectors + " FROM channels " + join + " WHERE " + cond + " LIMIT 1"
if err != nil {
return models.ChannelWithSubscription{}, err
}
channel, err := models.DecodeChannelWithSubscription(ctx, tx, rows) return sq.QuerySingle[models.ChannelWithSubscription](ctx, tx, sql, params, sq.SModeExtended, sq.Safe)
if err != nil {
return models.ChannelWithSubscription{}, err
}
return channel, nil
} }
func (db *Database) IncChannelMessageCounter(ctx db.TxContext, channel *models.Channel) error { func (db *Database) IncChannelMessageCounter(ctx db.TxContext, channel *models.Channel) error {
@@ -228,7 +169,7 @@ func (db *Database) IncChannelMessageCounter(ctx db.TxContext, channel *models.C
} }
channel.MessagesSent += 1 channel.MessagesSent += 1
channel.TimestampLastSent = &now channel.TimestampLastSent = models.NewSCNTimePtr(&now)
return nil return nil
} }
+6 -27
View File
@@ -4,7 +4,6 @@ import (
"blackforestbytes.com/simplecloudnotifier/db" "blackforestbytes.com/simplecloudnotifier/db"
"blackforestbytes.com/simplecloudnotifier/models" "blackforestbytes.com/simplecloudnotifier/models"
"gogs.mikescher.com/BlackForestBytes/goext/sq" "gogs.mikescher.com/BlackForestBytes/goext/sq"
"time"
) )
func (db *Database) CreateClient(ctx db.TxContext, userid models.UserID, ctype models.ClientType, fcmToken string, agentModel string, agentVersion string, name *string) (models.Client, error) { func (db *Database) CreateClient(ctx db.TxContext, userid models.UserID, ctype models.ClientType, fcmToken string, agentModel string, agentVersion string, name *string) (models.Client, error) {
@@ -13,12 +12,12 @@ func (db *Database) CreateClient(ctx db.TxContext, userid models.UserID, ctype m
return models.Client{}, err return models.Client{}, err
} }
entity := models.ClientDB{ entity := models.Client{
ClientID: models.NewClientID(), ClientID: models.NewClientID(),
UserID: userid, UserID: userid,
Type: ctype, Type: ctype,
FCMToken: fcmToken, FCMToken: fcmToken,
TimestampCreated: time2DB(time.Now()), TimestampCreated: models.NowSCNTime(),
AgentModel: agentModel, AgentModel: agentModel,
AgentVersion: agentVersion, AgentVersion: agentVersion,
Name: name, Name: name,
@@ -29,7 +28,7 @@ func (db *Database) CreateClient(ctx db.TxContext, userid models.UserID, ctype m
return models.Client{}, err return models.Client{}, err
} }
return entity.Model(), nil return entity, nil
} }
func (db *Database) ClearFCMTokens(ctx db.TxContext, fcmtoken string) error { func (db *Database) ClearFCMTokens(ctx db.TxContext, fcmtoken string) error {
@@ -52,17 +51,7 @@ func (db *Database) ListClients(ctx db.TxContext, userid models.UserID) ([]model
return nil, err return nil, err
} }
rows, err := tx.Query(ctx, "SELECT * FROM clients WHERE user_id = :uid ORDER BY clients.timestamp_created DESC, clients.client_id ASC", sq.PP{"uid": userid}) return sq.QueryAll[models.Client](ctx, tx, "SELECT * FROM clients WHERE user_id = :uid ORDER BY clients.timestamp_created DESC, clients.client_id ASC", sq.PP{"uid": userid}, sq.SModeExtended, sq.Safe)
if err != nil {
return nil, err
}
data, err := models.DecodeClients(ctx, tx, rows)
if err != nil {
return nil, err
}
return data, nil
} }
func (db *Database) GetClient(ctx db.TxContext, userid models.UserID, clientid models.ClientID) (models.Client, error) { func (db *Database) GetClient(ctx db.TxContext, userid models.UserID, clientid models.ClientID) (models.Client, error) {
@@ -71,20 +60,10 @@ func (db *Database) GetClient(ctx db.TxContext, userid models.UserID, clientid m
return models.Client{}, err return models.Client{}, err
} }
rows, err := tx.Query(ctx, "SELECT * FROM clients WHERE user_id = :uid AND client_id = :cid LIMIT 1", sq.PP{ return sq.QuerySingle[models.Client](ctx, tx, "SELECT * FROM clients WHERE user_id = :uid AND client_id = :cid LIMIT 1", sq.PP{
"uid": userid, "uid": userid,
"cid": clientid, "cid": clientid,
}) }, sq.SModeExtended, sq.Safe)
if err != nil {
return models.Client{}, err
}
client, err := models.DecodeClient(ctx, tx, rows)
if err != nil {
return models.Client{}, err
}
return client, nil
} }
func (db *Database) DeleteClient(ctx db.TxContext, clientid models.ClientID) error { func (db *Database) DeleteClient(ctx db.TxContext, clientid models.ClientID) error {
+14 -3
View File
@@ -5,12 +5,13 @@ import (
"blackforestbytes.com/simplecloudnotifier/db/dbtools" "blackforestbytes.com/simplecloudnotifier/db/dbtools"
"blackforestbytes.com/simplecloudnotifier/db/schema" "blackforestbytes.com/simplecloudnotifier/db/schema"
"blackforestbytes.com/simplecloudnotifier/db/simplectx" "blackforestbytes.com/simplecloudnotifier/db/simplectx"
"blackforestbytes.com/simplecloudnotifier/models"
"context" "context"
"database/sql" "database/sql"
"errors" "errors"
"fmt" "fmt"
"github.com/glebarez/go-sqlite"
"github.com/jmoiron/sqlx" "github.com/jmoiron/sqlx"
_ "github.com/mattn/go-sqlite3"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"gogs.mikescher.com/BlackForestBytes/goext/langext" "gogs.mikescher.com/BlackForestBytes/goext/langext"
"gogs.mikescher.com/BlackForestBytes/goext/sq" "gogs.mikescher.com/BlackForestBytes/goext/sq"
@@ -26,7 +27,16 @@ type Database struct {
func NewPrimaryDatabase(cfg server.Config) (*Database, error) { func NewPrimaryDatabase(cfg server.Config) (*Database, error) {
conf := cfg.DBMain conf := cfg.DBMain
url := fmt.Sprintf("file:%s?_journal=%s&_timeout=%d&_fk=%s&_busy_timeout=%d", conf.File, conf.Journal, conf.Timeout.Milliseconds(), langext.FormatBool(conf.CheckForeignKeys, "true", "false"), conf.BusyTimeout.Milliseconds()) url := fmt.Sprintf("file:%s?_pragma=journal_mode(%s)&_pragma=timeout(%d)&_pragma=foreign_keys(%s)&_pragma=busy_timeout(%d)",
conf.File,
conf.Journal,
conf.Timeout.Milliseconds(),
langext.FormatBool(conf.CheckForeignKeys, "true", "false"),
conf.BusyTimeout.Milliseconds())
if !langext.InArray("sqlite3", sql.Drivers()) {
sqlite.RegisterAsSQLITE3()
}
xdb, err := sqlx.Open("sqlite3", url) xdb, err := sqlx.Open("sqlite3", url)
if err != nil { if err != nil {
@@ -42,7 +52,8 @@ func NewPrimaryDatabase(cfg server.Config) (*Database, error) {
xdb.SetConnMaxIdleTime(60 * time.Minute) xdb.SetConnMaxIdleTime(60 * time.Minute)
} }
qqdb := sq.NewDB(xdb, sq.DBOptions{}) qqdb := sq.NewDB(xdb, sq.DBOptions{RegisterDefaultConverter: langext.PTrue, RegisterCommentTrimmer: langext.PTrue})
models.RegisterConverter(qqdb)
if conf.EnableLogger { if conf.EnableLogger {
qqdb.AddListener(dbtools.DBLogger{}) qqdb.AddListener(dbtools.DBLogger{})
+10 -20
View File
@@ -18,16 +18,16 @@ func (db *Database) CreateRetryDelivery(ctx db.TxContext, client models.Client,
now := time.Now() now := time.Now()
next := scn.NextDeliveryTimestamp(now) next := scn.NextDeliveryTimestamp(now)
entity := models.DeliveryDB{ entity := models.Delivery{
DeliveryID: models.NewDeliveryID(), DeliveryID: models.NewDeliveryID(),
MessageID: msg.MessageID, MessageID: msg.MessageID,
ReceiverUserID: client.UserID, ReceiverUserID: client.UserID,
ReceiverClientID: client.ClientID, ReceiverClientID: client.ClientID,
TimestampCreated: time2DB(now), TimestampCreated: models.NewSCNTime(now),
TimestampFinalized: nil, TimestampFinalized: nil,
Status: models.DeliveryStatusRetry, Status: models.DeliveryStatusRetry,
RetryCount: 0, RetryCount: 0,
NextDelivery: langext.Ptr(time2DB(next)), NextDelivery: models.NewSCNTimePtr(&next),
FCMMessageID: nil, FCMMessageID: nil,
} }
@@ -36,7 +36,7 @@ func (db *Database) CreateRetryDelivery(ctx db.TxContext, client models.Client,
return models.Delivery{}, err return models.Delivery{}, err
} }
return entity.Model(), nil return entity, nil
} }
func (db *Database) CreateSuccessDelivery(ctx db.TxContext, client models.Client, msg models.Message, fcmDelivID string) (models.Delivery, error) { func (db *Database) CreateSuccessDelivery(ctx db.TxContext, client models.Client, msg models.Message, fcmDelivID string) (models.Delivery, error) {
@@ -47,13 +47,13 @@ func (db *Database) CreateSuccessDelivery(ctx db.TxContext, client models.Client
now := time.Now() now := time.Now()
entity := models.DeliveryDB{ entity := models.Delivery{
DeliveryID: models.NewDeliveryID(), DeliveryID: models.NewDeliveryID(),
MessageID: msg.MessageID, MessageID: msg.MessageID,
ReceiverUserID: client.UserID, ReceiverUserID: client.UserID,
ReceiverClientID: client.ClientID, ReceiverClientID: client.ClientID,
TimestampCreated: time2DB(now), TimestampCreated: models.NewSCNTime(now),
TimestampFinalized: langext.Ptr(time2DB(now)), TimestampFinalized: models.NewSCNTimePtr(&now),
Status: models.DeliveryStatusSuccess, Status: models.DeliveryStatusSuccess,
RetryCount: 0, RetryCount: 0,
NextDelivery: nil, NextDelivery: nil,
@@ -65,7 +65,7 @@ func (db *Database) CreateSuccessDelivery(ctx db.TxContext, client models.Client
return models.Delivery{}, err return models.Delivery{}, err
} }
return entity.Model(), nil return entity, nil
} }
func (db *Database) ListRetrieableDeliveries(ctx db.TxContext, pageSize int) ([]models.Delivery, error) { func (db *Database) ListRetrieableDeliveries(ctx db.TxContext, pageSize int) ([]models.Delivery, error) {
@@ -74,20 +74,10 @@ func (db *Database) ListRetrieableDeliveries(ctx db.TxContext, pageSize int) ([]
return nil, err return nil, err
} }
rows, err := tx.Query(ctx, "SELECT * FROM deliveries WHERE status = 'RETRY' AND next_delivery < :next ORDER BY next_delivery ASC LIMIT :lim", sq.PP{ return sq.QueryAll[models.Delivery](ctx, tx, "SELECT * FROM deliveries WHERE status = 'RETRY' AND next_delivery < :next ORDER BY next_delivery ASC LIMIT :lim", sq.PP{
"next": time2DB(time.Now()), "next": time2DB(time.Now()),
"lim": pageSize, "lim": pageSize,
}) }, sq.SModeExtended, sq.Safe)
if err != nil {
return nil, err
}
data, err := models.DecodeDeliveries(ctx, tx, rows)
if err != nil {
return nil, err
}
return data, nil
} }
func (db *Database) SetDeliverySuccess(ctx db.TxContext, delivery models.Delivery, fcmDelivID string) error { func (db *Database) SetDeliverySuccess(ctx db.TxContext, delivery models.Delivery, fcmDelivID string) error {
+11 -58
View File
@@ -3,8 +3,6 @@ package primary
import ( import (
"blackforestbytes.com/simplecloudnotifier/db" "blackforestbytes.com/simplecloudnotifier/db"
"blackforestbytes.com/simplecloudnotifier/models" "blackforestbytes.com/simplecloudnotifier/models"
"database/sql"
"errors"
"gogs.mikescher.com/BlackForestBytes/goext/langext" "gogs.mikescher.com/BlackForestBytes/goext/langext"
"gogs.mikescher.com/BlackForestBytes/goext/sq" "gogs.mikescher.com/BlackForestBytes/goext/sq"
"strings" "strings"
@@ -17,16 +15,16 @@ func (db *Database) CreateKeyToken(ctx db.TxContext, name string, owner models.U
return models.KeyToken{}, err return models.KeyToken{}, err
} }
entity := models.KeyTokenDB{ entity := models.KeyToken{
KeyTokenID: models.NewKeyTokenID(), KeyTokenID: models.NewKeyTokenID(),
Name: name, Name: name,
TimestampCreated: time2DB(time.Now()), TimestampCreated: models.NowSCNTime(),
TimestampLastUsed: nil, TimestampLastUsed: nil,
OwnerUserID: owner, OwnerUserID: owner,
AllChannels: allChannels, AllChannels: allChannels,
Channels: strings.Join(langext.ArrMap(channels, func(v models.ChannelID) string { return v.String() }), ";"), Channels: channels,
Token: token, Token: token,
Permissions: permissions.String(), Permissions: permissions,
MessagesSent: 0, MessagesSent: 0,
} }
@@ -35,7 +33,7 @@ func (db *Database) CreateKeyToken(ctx db.TxContext, name string, owner models.U
return models.KeyToken{}, err return models.KeyToken{}, err
} }
return entity.Model(), nil return entity, nil
} }
func (db *Database) ListKeyTokens(ctx db.TxContext, ownerID models.UserID) ([]models.KeyToken, error) { func (db *Database) ListKeyTokens(ctx db.TxContext, ownerID models.UserID) ([]models.KeyToken, error) {
@@ -44,17 +42,7 @@ func (db *Database) ListKeyTokens(ctx db.TxContext, ownerID models.UserID) ([]mo
return nil, err 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}) return sq.QueryAll[models.KeyToken](ctx, tx, "SELECT * FROM keytokens WHERE owner_user_id = :uid ORDER BY keytokens.timestamp_created DESC, keytokens.keytoken_id ASC", sq.PP{"uid": ownerID}, sq.SModeExtended, sq.Safe)
if err != nil {
return nil, err
}
data, err := models.DecodeKeyTokens(ctx, tx, rows)
if err != nil {
return nil, err
}
return data, nil
} }
func (db *Database) GetKeyToken(ctx db.TxContext, userid models.UserID, keyTokenid models.KeyTokenID) (models.KeyToken, error) { func (db *Database) GetKeyToken(ctx db.TxContext, userid models.UserID, keyTokenid models.KeyTokenID) (models.KeyToken, error) {
@@ -63,20 +51,10 @@ func (db *Database) GetKeyToken(ctx db.TxContext, userid models.UserID, keyToken
return models.KeyToken{}, err 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{ return sq.QuerySingle[models.KeyToken](ctx, tx, "SELECT * FROM keytokens WHERE owner_user_id = :uid AND keytoken_id = :cid LIMIT 1", sq.PP{
"uid": userid, "uid": userid,
"cid": keyTokenid, "cid": keyTokenid,
}) }, sq.SModeExtended, sq.Safe)
if err != nil {
return models.KeyToken{}, err
}
keyToken, err := models.DecodeKeyToken(ctx, tx, rows)
if err != nil {
return models.KeyToken{}, err
}
return keyToken, nil
} }
func (db *Database) GetKeyTokenByID(ctx db.TxContext, keyTokenid models.KeyTokenID) (models.KeyToken, error) { func (db *Database) GetKeyTokenByID(ctx db.TxContext, keyTokenid models.KeyTokenID) (models.KeyToken, error) {
@@ -85,19 +63,7 @@ func (db *Database) GetKeyTokenByID(ctx db.TxContext, keyTokenid models.KeyToken
return models.KeyToken{}, err return models.KeyToken{}, err
} }
rows, err := tx.Query(ctx, "SELECT * FROM keytokens WHERE keytoken_id = :cid LIMIT 1", sq.PP{ return sq.QuerySingle[models.KeyToken](ctx, tx, "SELECT * FROM keytokens WHERE keytoken_id = :cid LIMIT 1", sq.PP{"cid": keyTokenid}, sq.SModeExtended, sq.Safe)
"cid": keyTokenid,
})
if err != nil {
return models.KeyToken{}, err
}
keyToken, err := models.DecodeKeyToken(ctx, tx, rows)
if err != nil {
return models.KeyToken{}, err
}
return keyToken, nil
} }
func (db *Database) GetKeyTokenByToken(ctx db.TxContext, key string) (*models.KeyToken, error) { func (db *Database) GetKeyTokenByToken(ctx db.TxContext, key string) (*models.KeyToken, error) {
@@ -106,20 +72,7 @@ func (db *Database) GetKeyTokenByToken(ctx db.TxContext, key string) (*models.Ke
return nil, err return nil, err
} }
rows, err := tx.Query(ctx, "SELECT * FROM keytokens WHERE token = :key LIMIT 1", sq.PP{"key": key}) return sq.QuerySingleOpt[models.KeyToken](ctx, tx, "SELECT * FROM keytokens WHERE token = :key LIMIT 1", sq.PP{"key": key}, sq.SModeExtended, sq.Safe)
if err != nil {
return nil, err
}
user, err := models.DecodeKeyToken(ctx, tx, rows)
if errors.Is(err, sql.ErrNoRows) {
return nil, nil
}
if err != nil {
return nil, err
}
return &user, nil
} }
func (db *Database) DeleteKeyToken(ctx db.TxContext, keyTokenid models.KeyTokenID) error { func (db *Database) DeleteKeyToken(ctx db.TxContext, keyTokenid models.KeyTokenID) error {
@@ -220,7 +173,7 @@ func (db *Database) IncKeyTokenMessageCounter(ctx db.TxContext, keyToken *models
return err return err
} }
keyToken.TimestampLastUsed = &now keyToken.TimestampLastUsed = models.NewSCNTimePtr(&now)
keyToken.MessagesSent += 1 keyToken.MessagesSent += 1
return nil return nil
+9 -37
View File
@@ -4,7 +4,6 @@ import (
"blackforestbytes.com/simplecloudnotifier/db" "blackforestbytes.com/simplecloudnotifier/db"
ct "blackforestbytes.com/simplecloudnotifier/db/cursortoken" ct "blackforestbytes.com/simplecloudnotifier/db/cursortoken"
"blackforestbytes.com/simplecloudnotifier/models" "blackforestbytes.com/simplecloudnotifier/models"
"database/sql"
"errors" "errors"
"gogs.mikescher.com/BlackForestBytes/goext/sq" "gogs.mikescher.com/BlackForestBytes/goext/sq"
"time" "time"
@@ -16,20 +15,7 @@ func (db *Database) GetMessageByUserMessageID(ctx db.TxContext, usrMsgId string)
return nil, err return nil, err
} }
rows, err := tx.Query(ctx, "SELECT * FROM messages WHERE usr_message_id = :umid LIMIT 1", sq.PP{"umid": usrMsgId}) return sq.QuerySingleOpt[models.Message](ctx, tx, "SELECT * FROM messages WHERE usr_message_id = :umid LIMIT 1", sq.PP{"umid": usrMsgId}, sq.SModeExtended, sq.Safe)
if err != nil {
return nil, err
}
msg, err := models.DecodeMessage(ctx, tx, rows)
if errors.Is(err, sql.ErrNoRows) {
return nil, nil
}
if err != nil {
return nil, err
}
return &msg, nil
} }
func (db *Database) GetMessage(ctx db.TxContext, scnMessageID models.MessageID, allowDeleted bool) (models.Message, error) { func (db *Database) GetMessage(ctx db.TxContext, scnMessageID models.MessageID, allowDeleted bool) (models.Message, error) {
@@ -45,17 +31,7 @@ func (db *Database) GetMessage(ctx db.TxContext, scnMessageID models.MessageID,
sqlcmd = "SELECT * FROM messages WHERE message_id = :mid AND deleted=0 LIMIT 1" sqlcmd = "SELECT * FROM messages WHERE message_id = :mid AND deleted=0 LIMIT 1"
} }
rows, err := tx.Query(ctx, sqlcmd, sq.PP{"mid": scnMessageID}) return sq.QuerySingle[models.Message](ctx, tx, sqlcmd, sq.PP{"mid": scnMessageID}, sq.SModeExtended, sq.Safe)
if err != nil {
return models.Message{}, err
}
msg, err := models.DecodeMessage(ctx, tx, rows)
if err != nil {
return models.Message{}, err
}
return msg, nil
} }
func (db *Database) CreateMessage(ctx db.TxContext, senderUserID models.UserID, channel models.Channel, timestampSend *time.Time, title string, content *string, priority int, userMsgId *string, senderIP string, senderName *string, usedKeyID models.KeyTokenID) (models.Message, error) { func (db *Database) CreateMessage(ctx db.TxContext, senderUserID models.UserID, channel models.Channel, timestampSend *time.Time, title string, content *string, priority int, userMsgId *string, senderIP string, senderName *string, usedKeyID models.KeyTokenID) (models.Message, error) {
@@ -64,21 +40,22 @@ func (db *Database) CreateMessage(ctx db.TxContext, senderUserID models.UserID,
return models.Message{}, err return models.Message{}, err
} }
entity := models.MessageDB{ entity := models.Message{
MessageID: models.NewMessageID(), MessageID: models.NewMessageID(),
SenderUserID: senderUserID, SenderUserID: senderUserID,
ChannelInternalName: channel.InternalName, ChannelInternalName: channel.InternalName,
ChannelID: channel.ChannelID, ChannelID: channel.ChannelID,
SenderIP: senderIP, SenderIP: senderIP,
SenderName: senderName, SenderName: senderName,
TimestampReal: time2DB(time.Now()), TimestampReal: models.NowSCNTime(),
TimestampClient: time2DBOpt(timestampSend), TimestampClient: models.NewSCNTimePtr(timestampSend),
Title: title, Title: title,
Content: content, Content: content,
Priority: priority, Priority: priority,
UserMessageID: userMsgId, UserMessageID: userMsgId,
UsedKeyID: usedKeyID, UsedKeyID: usedKeyID,
Deleted: bool2DB(false), Deleted: false,
MessageExtra: models.MessageExtra{},
} }
_, err = sq.InsertSingle(ctx, tx, "messages", entity) _, err = sq.InsertSingle(ctx, tx, "messages", entity)
@@ -86,7 +63,7 @@ func (db *Database) CreateMessage(ctx db.TxContext, senderUserID models.UserID,
return models.Message{}, err return models.Message{}, err
} }
return entity.Model(), nil return entity, nil
} }
func (db *Database) DeleteMessage(ctx db.TxContext, messageID models.MessageID) error { func (db *Database) DeleteMessage(ctx db.TxContext, messageID models.MessageID) error {
@@ -133,12 +110,7 @@ func (db *Database) ListMessages(ctx db.TxContext, filter models.MessageFilter,
prepParams["tokts"] = inTok.Timestamp prepParams["tokts"] = inTok.Timestamp
prepParams["tokid"] = inTok.Id prepParams["tokid"] = inTok.Id
rows, err := tx.Query(ctx, sqlQuery, prepParams) data, err := sq.QueryAll[models.Message](ctx, tx, sqlQuery, prepParams, sq.SModeExtended, sq.Safe)
if err != nil {
return nil, ct.CursorToken{}, err
}
data, err := models.DecodeMessages(ctx, tx, rows)
if err != nil { if err != nil {
return nil, ct.CursorToken{}, err return nil, ct.CursorToken{}, err
} }
+8 -44
View File
@@ -3,10 +3,7 @@ package primary
import ( import (
"blackforestbytes.com/simplecloudnotifier/db" "blackforestbytes.com/simplecloudnotifier/db"
"blackforestbytes.com/simplecloudnotifier/models" "blackforestbytes.com/simplecloudnotifier/models"
"database/sql"
"errors"
"gogs.mikescher.com/BlackForestBytes/goext/sq" "gogs.mikescher.com/BlackForestBytes/goext/sq"
"time"
) )
func (db *Database) CreateSubscription(ctx db.TxContext, subscriberUID models.UserID, channel models.Channel, confirmed bool) (models.Subscription, error) { func (db *Database) CreateSubscription(ctx db.TxContext, subscriberUID models.UserID, channel models.Channel, confirmed bool) (models.Subscription, error) {
@@ -15,14 +12,14 @@ func (db *Database) CreateSubscription(ctx db.TxContext, subscriberUID models.Us
return models.Subscription{}, err return models.Subscription{}, err
} }
entity := models.SubscriptionDB{ entity := models.Subscription{
SubscriptionID: models.NewSubscriptionID(), SubscriptionID: models.NewSubscriptionID(),
SubscriberUserID: subscriberUID, SubscriberUserID: subscriberUID,
ChannelOwnerUserID: channel.OwnerUserID, ChannelOwnerUserID: channel.OwnerUserID,
ChannelID: channel.ChannelID, ChannelID: channel.ChannelID,
ChannelInternalName: channel.InternalName, ChannelInternalName: channel.InternalName,
TimestampCreated: time2DB(time.Now()), TimestampCreated: models.NowSCNTime(),
Confirmed: bool2DB(confirmed), Confirmed: confirmed,
} }
_, err = sq.InsertSingle(ctx, tx, "subscriptions", entity) _, err = sq.InsertSingle(ctx, tx, "subscriptions", entity)
@@ -30,7 +27,7 @@ func (db *Database) CreateSubscription(ctx db.TxContext, subscriberUID models.Us
return models.Subscription{}, err return models.Subscription{}, err
} }
return entity.Model(), nil return entity, nil
} }
func (db *Database) ListSubscriptions(ctx db.TxContext, filter models.SubscriptionFilter) ([]models.Subscription, error) { func (db *Database) ListSubscriptions(ctx db.TxContext, filter models.SubscriptionFilter) ([]models.Subscription, error) {
@@ -45,17 +42,7 @@ func (db *Database) ListSubscriptions(ctx db.TxContext, filter models.Subscripti
sqlQuery := "SELECT " + "subscriptions.*" + " FROM subscriptions " + filterJoin + " WHERE ( " + filterCond + " ) " + orderClause sqlQuery := "SELECT " + "subscriptions.*" + " FROM subscriptions " + filterJoin + " WHERE ( " + filterCond + " ) " + orderClause
rows, err := tx.Query(ctx, sqlQuery, prepParams) return sq.QueryAll[models.Subscription](ctx, tx, sqlQuery, prepParams, sq.SModeExtended, sq.Safe)
if err != nil {
return nil, err
}
data, err := models.DecodeSubscriptions(ctx, tx, rows)
if err != nil {
return nil, err
}
return data, nil
} }
func (db *Database) GetSubscription(ctx db.TxContext, subid models.SubscriptionID) (models.Subscription, error) { func (db *Database) GetSubscription(ctx db.TxContext, subid models.SubscriptionID) (models.Subscription, error) {
@@ -64,17 +51,7 @@ func (db *Database) GetSubscription(ctx db.TxContext, subid models.SubscriptionI
return models.Subscription{}, err return models.Subscription{}, err
} }
rows, err := tx.Query(ctx, "SELECT * FROM subscriptions WHERE subscription_id = :sid LIMIT 1", sq.PP{"sid": subid}) return sq.QuerySingle[models.Subscription](ctx, tx, "SELECT * FROM subscriptions WHERE subscription_id = :sid LIMIT 1", sq.PP{"sid": subid}, sq.SModeExtended, sq.Safe)
if err != nil {
return models.Subscription{}, err
}
sub, err := models.DecodeSubscription(ctx, tx, rows)
if err != nil {
return models.Subscription{}, err
}
return sub, nil
} }
func (db *Database) GetSubscriptionBySubscriber(ctx db.TxContext, subscriberId models.UserID, channelId models.ChannelID) (*models.Subscription, error) { func (db *Database) GetSubscriptionBySubscriber(ctx db.TxContext, subscriberId models.UserID, channelId models.ChannelID) (*models.Subscription, error) {
@@ -83,23 +60,10 @@ func (db *Database) GetSubscriptionBySubscriber(ctx db.TxContext, subscriberId m
return nil, err return nil, err
} }
rows, err := tx.Query(ctx, "SELECT * FROM subscriptions WHERE subscriber_user_id = :suid AND channel_id = :cid LIMIT 1", sq.PP{ return sq.QuerySingleOpt[models.Subscription](ctx, tx, "SELECT * FROM subscriptions WHERE subscriber_user_id = :suid AND channel_id = :cid LIMIT 1", sq.PP{
"suid": subscriberId, "suid": subscriberId,
"cid": channelId, "cid": channelId,
}) }, sq.SModeExtended, sq.Safe)
if err != nil {
return nil, err
}
user, err := models.DecodeSubscription(ctx, tx, rows)
if errors.Is(err, sql.ErrNoRows) {
return nil, nil
}
if err != nil {
return nil, err
}
return &user, nil
} }
func (db *Database) DeleteSubscription(ctx db.TxContext, subid models.SubscriptionID) error { func (db *Database) DeleteSubscription(ctx db.TxContext, subid models.SubscriptionID) error {
+8 -15
View File
@@ -15,10 +15,10 @@ func (db *Database) CreateUser(ctx db.TxContext, protoken *string, username *str
return models.User{}, err return models.User{}, err
} }
entity := models.UserDB{ entity := models.User{
UserID: models.NewUserID(), UserID: models.NewUserID(),
Username: username, Username: username,
TimestampCreated: time2DB(time.Now()), TimestampCreated: models.NowSCNTime(),
TimestampLastRead: nil, TimestampLastRead: nil,
TimestampLastSent: nil, TimestampLastSent: nil,
MessagesSent: 0, MessagesSent: 0,
@@ -26,14 +26,17 @@ func (db *Database) CreateUser(ctx db.TxContext, protoken *string, username *str
QuotaUsedDay: nil, QuotaUsedDay: nil,
IsPro: protoken != nil, IsPro: protoken != nil,
ProToken: protoken, ProToken: protoken,
UserExtra: models.UserExtra{},
} }
entity.PreMarshal()
_, err = sq.InsertSingle(ctx, tx, "users", entity) _, err = sq.InsertSingle(ctx, tx, "users", entity)
if err != nil { if err != nil {
return models.User{}, err return models.User{}, err
} }
return entity.Model(), nil return entity, nil
} }
func (db *Database) ClearProTokens(ctx db.TxContext, protoken string) error { func (db *Database) ClearProTokens(ctx db.TxContext, protoken string) error {
@@ -56,17 +59,7 @@ func (db *Database) GetUser(ctx db.TxContext, userid models.UserID) (models.User
return models.User{}, err return models.User{}, err
} }
rows, err := tx.Query(ctx, "SELECT * FROM users WHERE user_id = :uid LIMIT 1", sq.PP{"uid": userid}) return sq.QuerySingle[models.User](ctx, tx, "SELECT * FROM users WHERE user_id = :uid LIMIT 1", sq.PP{"uid": userid}, sq.SModeExtended, sq.Safe)
if err != nil {
return models.User{}, err
}
user, err := models.DecodeUser(ctx, tx, rows)
if err != nil {
return models.User{}, err
}
return user, nil
} }
func (db *Database) UpdateUserUsername(ctx db.TxContext, userid models.UserID, username *string) error { func (db *Database) UpdateUserUsername(ctx db.TxContext, userid models.UserID, username *string) error {
@@ -127,7 +120,7 @@ func (db *Database) IncUserMessageCounter(ctx db.TxContext, user *models.User) e
return err return err
} }
user.TimestampLastSent = &now user.TimestampLastSent = models.NewSCNTimePtr(&now)
user.MessagesSent = user.MessagesSent + 1 user.MessagesSent = user.MessagesSent + 1
return nil return nil
+15 -4
View File
@@ -5,12 +5,13 @@ import (
"blackforestbytes.com/simplecloudnotifier/db/dbtools" "blackforestbytes.com/simplecloudnotifier/db/dbtools"
"blackforestbytes.com/simplecloudnotifier/db/schema" "blackforestbytes.com/simplecloudnotifier/db/schema"
"blackforestbytes.com/simplecloudnotifier/db/simplectx" "blackforestbytes.com/simplecloudnotifier/db/simplectx"
"blackforestbytes.com/simplecloudnotifier/models"
"context" "context"
"database/sql" "database/sql"
"errors" "errors"
"fmt" "fmt"
"github.com/glebarez/go-sqlite"
"github.com/jmoiron/sqlx" "github.com/jmoiron/sqlx"
_ "github.com/mattn/go-sqlite3"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"gogs.mikescher.com/BlackForestBytes/goext/langext" "gogs.mikescher.com/BlackForestBytes/goext/langext"
"gogs.mikescher.com/BlackForestBytes/goext/sq" "gogs.mikescher.com/BlackForestBytes/goext/sq"
@@ -26,7 +27,16 @@ type Database struct {
func NewRequestsDatabase(cfg server.Config) (*Database, error) { func NewRequestsDatabase(cfg server.Config) (*Database, error) {
conf := cfg.DBRequests conf := cfg.DBRequests
url := fmt.Sprintf("file:%s?_journal=%s&_timeout=%d&_fk=%s&_busy_timeout=%d", conf.File, conf.Journal, conf.Timeout.Milliseconds(), langext.FormatBool(conf.CheckForeignKeys, "true", "false"), conf.BusyTimeout.Milliseconds()) url := fmt.Sprintf("file:%s?_pragma=journal_mode(%s)&_pragma=timeout(%d)&_pragma=foreign_keys(%s)&_pragma=busy_timeout(%d)",
conf.File,
conf.Journal,
conf.Timeout.Milliseconds(),
langext.FormatBool(conf.CheckForeignKeys, "true", "false"),
conf.BusyTimeout.Milliseconds())
if !langext.InArray("sqlite3", sql.Drivers()) {
sqlite.RegisterAsSQLITE3()
}
xdb, err := sqlx.Open("sqlite3", url) xdb, err := sqlx.Open("sqlite3", url)
if err != nil { if err != nil {
@@ -42,7 +52,8 @@ func NewRequestsDatabase(cfg server.Config) (*Database, error) {
xdb.SetConnMaxIdleTime(60 * time.Minute) xdb.SetConnMaxIdleTime(60 * time.Minute)
} }
qqdb := sq.NewDB(xdb, sq.DBOptions{}) qqdb := sq.NewDB(xdb, sq.DBOptions{RegisterDefaultConverter: langext.PTrue, RegisterCommentTrimmer: langext.PTrue})
models.RegisterConverter(qqdb)
if conf.EnableLogger { if conf.EnableLogger {
qqdb.AddListener(dbtools.DBLogger{}) qqdb.AddListener(dbtools.DBLogger{})
@@ -92,7 +103,7 @@ func (db *Database) Migrate(outerctx context.Context) error {
schemastr := schema.RequestsSchema[schema.RequestsSchemaVersion].SQL schemastr := schema.RequestsSchema[schema.RequestsSchemaVersion].SQL
schemahash := schema.RequestsSchema[schema.RequestsSchemaVersion].Hash schemahash := schema.RequestsSchema[schema.RequestsSchemaVersion].Hash
schemahash, err := sq.HashMattnSqliteSchema(tctx, schemastr) schemahash, err := sq.HashGoSqliteSchema(tctx, schemastr)
if err != nil { if err != nil {
return err return err
} }
+5 -11
View File
@@ -8,18 +8,17 @@ import (
"time" "time"
) )
func (db *Database) InsertRequestLog(ctx context.Context, requestid models.RequestID, data models.RequestLog) (models.RequestLog, error) { func (db *Database) InsertRequestLog(ctx context.Context, requestid models.RequestID, entity models.RequestLog) (models.RequestLog, error) {
entity := data.DB()
entity.RequestID = requestid entity.RequestID = requestid
entity.TimestampCreated = time2DB(time.Now()) entity.TimestampCreated = models.NowSCNTime()
_, err := sq.InsertSingle(ctx, db.db, "requests", entity) _, err := sq.InsertSingle(ctx, db.db, "requests", entity)
if err != nil { if err != nil {
return models.RequestLog{}, err return models.RequestLog{}, err
} }
return entity.Model(), nil return entity, nil
} }
func (db *Database) Cleanup(ctx context.Context, count int, duration time.Duration) (int64, error) { func (db *Database) Cleanup(ctx context.Context, count int, duration time.Duration) (int64, error) {
@@ -73,12 +72,7 @@ func (db *Database) ListRequestLogs(ctx context.Context, filter models.RequestLo
prepParams["tokts"] = inTok.Timestamp prepParams["tokts"] = inTok.Timestamp
prepParams["tokid"] = inTok.Id prepParams["tokid"] = inTok.Id
rows, err := db.db.Query(ctx, sqlQuery, prepParams) data, err := sq.QueryAll[models.RequestLog](ctx, db.db, sqlQuery, prepParams, sq.SModeExtended, sq.Safe)
if err != nil {
return nil, ct.CursorToken{}, err
}
data, err := models.DecodeRequestLogs(ctx, db.db, rows)
if err != nil { if err != nil {
return nil, ct.CursorToken{}, err return nil, ct.CursorToken{}, err
} }
@@ -86,7 +80,7 @@ func (db *Database) ListRequestLogs(ctx context.Context, filter models.RequestLo
if pageSize == nil || len(data) <= *pageSize { if pageSize == nil || len(data) <= *pageSize {
return data, ct.End(), nil return data, ct.End(), nil
} else { } else {
outToken := ct.Normal(data[*pageSize-1].TimestampCreated, data[*pageSize-1].RequestID.String(), "DESC", filter.Hash()) outToken := ct.Normal(data[*pageSize-1].TimestampCreated.Time(), data[*pageSize-1].RequestID.String(), "DESC", filter.Hash())
return data[0:*pageSize], outToken, nil return data[0:*pageSize], outToken, nil
} }
} }
+28 -20
View File
@@ -6,53 +6,61 @@ toolchain go1.22.3
require ( require (
github.com/gin-gonic/gin v1.10.0 github.com/gin-gonic/gin v1.10.0
github.com/go-playground/validator/v10 v10.20.0 github.com/glebarez/go-sqlite v1.22.0
github.com/go-playground/validator/v10 v10.22.1
github.com/go-sql-driver/mysql v1.8.1 github.com/go-sql-driver/mysql v1.8.1
github.com/jmoiron/sqlx v1.4.0 github.com/jmoiron/sqlx v1.4.0
github.com/mattn/go-sqlite3 v1.14.22 github.com/mattn/go-sqlite3 v1.14.22
github.com/rs/zerolog v1.33.0 github.com/rs/zerolog v1.33.0
gogs.mikescher.com/BlackForestBytes/goext v0.0.463 gogs.mikescher.com/BlackForestBytes/goext v0.0.513
gopkg.in/loremipsum.v1 v1.1.2 gopkg.in/loremipsum.v1 v1.1.2
) )
require ( require (
filippo.io/edwards25519 v1.1.0 // indirect filippo.io/edwards25519 v1.1.0 // indirect
github.com/bytedance/sonic v1.11.8 // indirect github.com/bytedance/sonic v1.12.2 // indirect
github.com/bytedance/sonic/loader v0.1.1 // indirect github.com/bytedance/sonic/loader v0.2.0 // indirect
github.com/cloudwego/base64x v0.1.4 // indirect github.com/cloudwego/base64x v0.1.4 // indirect
github.com/cloudwego/iasm v0.2.0 // indirect github.com/cloudwego/iasm v0.2.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.4 // indirect github.com/dustin/go-humanize v1.0.1 // indirect
github.com/gabriel-vasile/mimetype v1.4.5 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/goccy/go-json v0.10.3 // indirect github.com/goccy/go-json v0.10.3 // indirect
github.com/golang/snappy v0.0.4 // indirect github.com/golang/snappy v0.0.4 // indirect
github.com/google/go-cmp v0.5.9 // indirect github.com/google/uuid v1.5.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/compress v1.17.8 // indirect github.com/klauspost/compress v1.17.9 // indirect
github.com/klauspost/cpuid/v2 v2.2.7 // indirect github.com/klauspost/cpuid/v2 v2.2.8 // indirect
github.com/leodido/go-urn v1.4.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/montanaflynn/stats v0.7.1 // indirect github.com/montanaflynn/stats v0.7.1 // indirect
github.com/pelletier/go-toml/v2 v2.2.2 // indirect github.com/pelletier/go-toml/v2 v2.2.3 // indirect
github.com/rs/xid v1.5.0 // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/rs/xid v1.6.0 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.12 // indirect github.com/ugorji/go/codec v1.2.12 // indirect
github.com/viney-shih/go-lock v1.1.2 // indirect
github.com/xdg-go/pbkdf2 v1.0.0 // indirect github.com/xdg-go/pbkdf2 v1.0.0 // indirect
github.com/xdg-go/scram v1.1.2 // indirect github.com/xdg-go/scram v1.1.2 // indirect
github.com/xdg-go/stringprep v1.0.4 // indirect github.com/xdg-go/stringprep v1.0.4 // indirect
github.com/youmark/pkcs8 v0.0.0-20240424034433-3c2c7870ae76 // indirect github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
go.mongodb.org/mongo-driver v1.15.0 // indirect go.mongodb.org/mongo-driver v1.16.1 // indirect
golang.org/x/arch v0.8.0 // indirect golang.org/x/arch v0.10.0 // indirect
golang.org/x/crypto v0.23.0 // indirect golang.org/x/crypto v0.27.0 // indirect
golang.org/x/net v0.25.0 // indirect golang.org/x/net v0.29.0 // indirect
golang.org/x/sync v0.7.0 // indirect golang.org/x/sync v0.8.0 // indirect
golang.org/x/sys v0.20.0 // indirect golang.org/x/sys v0.25.0 // indirect
golang.org/x/term v0.20.0 // indirect golang.org/x/term v0.24.0 // indirect
golang.org/x/text v0.15.0 // indirect golang.org/x/text v0.18.0 // indirect
google.golang.org/protobuf v1.34.1 // indirect google.golang.org/protobuf v1.34.2 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
modernc.org/libc v1.37.6 // indirect
modernc.org/mathutil v1.6.0 // indirect
modernc.org/memory v1.7.2 // indirect
modernc.org/sqlite v1.28.0 // indirect
) )
+51 -42
View File
@@ -1,9 +1,10 @@
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/bytedance/sonic v1.11.8 h1:Zw/j1KfiS+OYTi9lyB3bb0CFxPJVkM17k1wyDG32LRA= github.com/bytedance/sonic v1.12.2 h1:oaMFuRTpMHYLpCntGca65YWt5ny+wAceDERTkT2L9lg=
github.com/bytedance/sonic v1.11.8/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4= github.com/bytedance/sonic v1.12.2/go.mod h1:B8Gt/XvtZ3Fqj+iSKMypzymZxw/FVwgIGKzMzT9r/rk=
github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM=
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
github.com/bytedance/sonic/loader v0.2.0 h1:zNprn+lsIP06C/IqCHs3gPQIvnvpKbbxyXQP1iU4kWM=
github.com/bytedance/sonic/loader v0.2.0/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y= github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
@@ -14,8 +15,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/gabriel-vasile/mimetype v1.4.4 h1:QjV6pZ7/XZ7ryI2KuyeEDE8wnh7fHP9YnQy+R0LnH8I= github.com/gabriel-vasile/mimetype v1.4.5 h1:J7wGKdGu33ocBOhGy0z653k/lFKLFDPJMG8Gql0kxn4=
github.com/gabriel-vasile/mimetype v1.4.4/go.mod h1:JwLei5XPtWdGiMFB5Pjle1oEeoSeEuJfJE+TtfvdB/s= github.com/gabriel-vasile/mimetype v1.4.5/go.mod h1:ibHel+/kbxn9x2407k1izTA1S81ku1z/DlgOW2QE0M4=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU= github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
@@ -28,8 +29,8 @@ github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/o
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8= github.com/go-playground/validator/v10 v10.22.1 h1:40JcKH+bBNGFczGuoBYgX4I6m/i27HYW8P9FDk5PbgA=
github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= github.com/go-playground/validator/v10 v10.22.1/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA= github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA=
@@ -37,20 +38,22 @@ github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PU
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo=
github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU= github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU=
github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o= github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o=
github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY= github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/klauspost/compress v1.17.8 h1:YcnTYrq7MikUT7k0Yb5eceMmALQPYBW/Xltxn0NAMnU= github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
github.com/klauspost/compress v1.17.8/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM= github.com/klauspost/cpuid/v2 v2.2.8 h1:+StwCXwm9PdpiEkPyzBXIy+M9KUb4ODm0Zarf1kS5BM=
github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= github.com/klauspost/cpuid/v2 v2.2.8/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
@@ -71,63 +74,69 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/montanaflynn/stats v0.7.1 h1:etflOAAHORrCC44V+aR6Ftzort912ZU+YLiSTuV8eaE= github.com/montanaflynn/stats v0.7.1 h1:etflOAAHORrCC44V+aR6Ftzort912ZU+YLiSTuV8eaE=
github.com/montanaflynn/stats v0.7.1/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow= github.com/montanaflynn/stats v0.7.1/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow=
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc=
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU=
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8= github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8=
github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
github.com/viney-shih/go-lock v1.1.2 h1:3TdGTiHZCPqBdTvFbQZQN/TRZzKF3KWw2rFEyKz3YqA=
github.com/viney-shih/go-lock v1.1.2/go.mod h1:Yijm78Ljteb3kRiJrbLAxVntkUukGu5uzSxq/xV7OO8=
github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c= github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c=
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY= github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY=
github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4= github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4=
github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8= github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8=
github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM= github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM=
github.com/youmark/pkcs8 v0.0.0-20240424034433-3c2c7870ae76 h1:tBiBTKHnIjovYoLX/TPkcf+OjqqKGQrPtGT3Foz+Pgo= github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM=
github.com/youmark/pkcs8 v0.0.0-20240424034433-3c2c7870ae76/go.mod h1:SQliXeA7Dhkt//vS29v3zpbEwoa+zb2Cn5xj5uO4K5U= github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.mongodb.org/mongo-driver v1.15.0 h1:rJCKC8eEliewXjZGf0ddURtl7tTVy1TK3bfl0gkUSLc= go.mongodb.org/mongo-driver v1.16.1 h1:rIVLL3q0IHM39dvE+z2ulZLp9ENZKThVfuvN/IiN4l8=
go.mongodb.org/mongo-driver v1.15.0/go.mod h1:Vzb0Mk/pa7e6cWw85R4F/endUC3u0U9jGcNU603k65c= go.mongodb.org/mongo-driver v1.16.1/go.mod h1:oB6AhJQvFQL4LEHyXi6aJzQJtBiTQHiAd83l0GdFaiw=
gogs.mikescher.com/BlackForestBytes/goext v0.0.463 h1:1sdU/jI7gzzucKv3CBefT1Hk5frGAYvgl/ItC9PdoqA= gogs.mikescher.com/BlackForestBytes/goext v0.0.511 h1:vAEhXdexKlLTNf/mGHzemp/4rzmv7n2jf5l4NK38tIw=
gogs.mikescher.com/BlackForestBytes/goext v0.0.463/go.mod h1:ZEaw70t0Wx044Ifkt8fcDHO/KtD3dwgxclX3OF6ElvA= gogs.mikescher.com/BlackForestBytes/goext v0.0.511/go.mod h1:9Q9EjraeE3yih7EXgBlnwLLJXWuRZNsl7s5TVTh3aOU=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= gogs.mikescher.com/BlackForestBytes/goext v0.0.512 h1:cdLUi1bSnGujtx8/K0fPql142aOvUyNPt+8aWMKKDFk=
golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc= gogs.mikescher.com/BlackForestBytes/goext v0.0.512/go.mod h1:9Q9EjraeE3yih7EXgBlnwLLJXWuRZNsl7s5TVTh3aOU=
golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= gogs.mikescher.com/BlackForestBytes/goext v0.0.513 h1:zGb5n220AYNElzQs611RYXfZlnUw6/VJJesfLftphkQ=
gogs.mikescher.com/BlackForestBytes/goext v0.0.513/go.mod h1:9Q9EjraeE3yih7EXgBlnwLLJXWuRZNsl7s5TVTh3aOU=
golang.org/x/arch v0.10.0 h1:S3huipmSclq3PJMNe76NGwkBR504WFkQ5dhzWzP8ZW8=
golang.org/x/arch v0.10.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI= golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -137,28 +146,29 @@ golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.20.0 h1:VnkxpohqXaOBYJtBmEppKUG6mXpi+4O6purfc2+sMhw= golang.org/x/term v0.24.0 h1:Mh5cbb+Zk2hqqXNO7S1iTjEphVL+jb8ZWaqh/g+JWkM=
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= golang.org/x/term v0.24.0/go.mod h1:lOBK/LVxemqiMij05LGJ0tzNr8xlmwBRJ81PX6wVLH8=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg= google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/loremipsum.v1 v1.1.2 h1:12APklfJKuGszqZsrArW5QoQh03/W+qyCCjvnDuS6Tw= gopkg.in/loremipsum.v1 v1.1.2 h1:12APklfJKuGszqZsrArW5QoQh03/W+qyCCjvnDuS6Tw=
gopkg.in/loremipsum.v1 v1.1.2/go.mod h1:TuRvzFuzuejXj+odBU6Tubp/EPUyGb9wmSvHenyP2Ts= gopkg.in/loremipsum.v1 v1.1.2/go.mod h1:TuRvzFuzuejXj+odBU6Tubp/EPUyGb9wmSvHenyP2Ts=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
@@ -171,4 +181,3 @@ modernc.org/memory v1.7.2/go.mod h1:NO4NVCQy0N7ln+T9ngWqOQfi7ley4vpwvARR+Hjw95E=
modernc.org/sqlite v1.28.0 h1:Zx+LyDDmXczNnEQdvPuEfcFVA2ZPyaD7UCZDjef3BHQ= modernc.org/sqlite v1.28.0 h1:Zx+LyDDmXczNnEQdvPuEfcFVA2ZPyaD7UCZDjef3BHQ=
modernc.org/sqlite v1.28.0/go.mod h1:Qxpazz0zH8Z1xCFyi5GSL3FzbtZ3fvbjmywNogldEW0= modernc.org/sqlite v1.28.0/go.mod h1:Qxpazz0zH8Z1xCFyi5GSL3FzbtZ3fvbjmywNogldEW0=
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
+6 -2
View File
@@ -9,6 +9,7 @@ import (
"errors" "errors"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"gogs.mikescher.com/BlackForestBytes/goext/ginext"
"gogs.mikescher.com/BlackForestBytes/goext/sq" "gogs.mikescher.com/BlackForestBytes/goext/sq"
"time" "time"
) )
@@ -70,7 +71,10 @@ func (ac *AppContext) Cancel() {
} }
ac.transaction = nil ac.transaction = nil
} }
ac.cancelFunc()
if ac.cancelFunc != nil {
ac.cancelFunc()
}
} }
func (ac *AppContext) RequestURI() string { func (ac *AppContext) RequestURI() string {
@@ -81,7 +85,7 @@ func (ac *AppContext) RequestURI() string {
} }
} }
func (ac *AppContext) FinishSuccess(res ginresp.HTTPResponse) ginresp.HTTPResponse { func (ac *AppContext) _FinishSuccess(res ginext.HTTPResponse) ginext.HTTPResponse {
if ac.cancelled { if ac.cancelled {
panic("Cannot finish a cancelled request") panic("Cannot finish a cancelled request")
} }
+36 -113
View File
@@ -2,22 +2,19 @@ package logic
import ( import (
scn "blackforestbytes.com/simplecloudnotifier" scn "blackforestbytes.com/simplecloudnotifier"
"blackforestbytes.com/simplecloudnotifier/api/apierr" "blackforestbytes.com/simplecloudnotifier/db"
"blackforestbytes.com/simplecloudnotifier/api/ginresp"
"blackforestbytes.com/simplecloudnotifier/db/simplectx" "blackforestbytes.com/simplecloudnotifier/db/simplectx"
"blackforestbytes.com/simplecloudnotifier/google" "blackforestbytes.com/simplecloudnotifier/google"
"blackforestbytes.com/simplecloudnotifier/models" "blackforestbytes.com/simplecloudnotifier/models"
"blackforestbytes.com/simplecloudnotifier/push" "blackforestbytes.com/simplecloudnotifier/push"
"context" "context"
"errors" "errors"
"github.com/gin-gonic/gin"
"github.com/gin-gonic/gin/binding"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"gogs.mikescher.com/BlackForestBytes/goext/langext" golock "github.com/viney-shih/go-lock"
"gogs.mikescher.com/BlackForestBytes/goext/ginext"
"gogs.mikescher.com/BlackForestBytes/goext/rext" "gogs.mikescher.com/BlackForestBytes/goext/rext"
"gogs.mikescher.com/BlackForestBytes/goext/syncext" "gogs.mikescher.com/BlackForestBytes/goext/syncext"
"net" "net"
"net/http"
"os" "os"
"os/signal" "os/signal"
"regexp" "regexp"
@@ -33,7 +30,7 @@ var rexCompatTitleChannel = rext.W(regexp.MustCompile("^\\[(?P<channel>[A-Za-z\\
type Application struct { type Application struct {
Config scn.Config Config scn.Config
Gin *gin.Engine Gin *ginext.GinWrapper
Database *DBPool Database *DBPool
Pusher push.NotificationClient Pusher push.NotificationClient
AndroidPublisher google.AndroidPublisherClient AndroidPublisher google.AndroidPublisherClient
@@ -42,18 +39,20 @@ type Application struct {
Port string Port string
IsRunning *syncext.AtomicBool IsRunning *syncext.AtomicBool
RequestLogQueue chan models.RequestLog RequestLogQueue chan models.RequestLog
MainDatabaseLock golock.RWMutex
} }
func NewApp(db *DBPool) *Application { func NewApp(db *DBPool) *Application {
return &Application{ return &Application{
Database: db, Database: db,
stopChan: make(chan bool), stopChan: make(chan bool),
IsRunning: syncext.NewAtomicBool(false), IsRunning: syncext.NewAtomicBool(false),
RequestLogQueue: make(chan models.RequestLog, 1024), RequestLogQueue: make(chan models.RequestLog, 1024),
MainDatabaseLock: golock.NewCASMutex(),
} }
} }
func (app *Application) Init(cfg scn.Config, g *gin.Engine, fb push.NotificationClient, apc google.AndroidPublisherClient, jobs []Job) { func (app *Application) Init(cfg scn.Config, g *ginext.GinWrapper, fb push.NotificationClient, apc google.AndroidPublisherClient, jobs []Job) {
app.Config = cfg app.Config = cfg
app.Gin = g app.Gin = g
app.Pusher = fb app.Pusher = fb
@@ -69,38 +68,17 @@ func (app *Application) Stop() {
} }
func (app *Application) Run() { func (app *Application) Run() {
httpserver := &http.Server{
Addr: net.JoinHostPort(app.Config.ServerIP, app.Config.ServerPort),
Handler: app.Gin,
}
errChan := make(chan error) // ================== START HTTP ==================
go func() { addr := net.JoinHostPort(app.Config.ServerIP, app.Config.ServerPort)
ln, err := net.Listen("tcp", httpserver.Addr)
if err != nil {
errChan <- err
return
}
_, port, err := net.SplitHostPort(ln.Addr().String())
if err != nil {
errChan <- err
return
}
log.Info().Str("address", httpserver.Addr).Msg("HTTP-Server started on http://localhost:" + port)
errChan, httpserver := app.Gin.ListenAndServeHTTP(addr, func(port string) {
app.Port = port app.Port = port
app.IsRunning.Set(true)
})
app.IsRunning.Set(true) // the net.Listener a few lines above is at this point actually already buffering requests // ================== START JOBS ==================
errChan <- httpserver.Serve(ln)
}()
sigstop := make(chan os.Signal, 1)
signal.Notify(sigstop, os.Interrupt, syscall.SIGTERM)
for _, job := range app.Jobs { for _, job := range app.Jobs {
err := job.Start() err := job.Start()
@@ -109,6 +87,11 @@ func (app *Application) Run() {
} }
} }
// ================== LISTEN FOR SIGNALS ==================
sigstop := make(chan os.Signal, 1)
signal.Notify(sigstop, os.Interrupt, syscall.SIGTERM)
select { select {
case <-sigstop: case <-sigstop:
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
@@ -127,7 +110,7 @@ func (app *Application) Run() {
case err := <-errChan: case err := <-errChan:
log.Error().Err(err).Msg("HTTP-Server failed") log.Error().Err(err).Msg("HTTP-Server failed")
case _ = <-app.stopChan: case <-app.stopChan:
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel() defer cancel()
@@ -142,20 +125,25 @@ func (app *Application) Run() {
} }
} }
// ================== STOP JOBS ==================
for _, job := range app.Jobs { for _, job := range app.Jobs {
job.Stop() job.Stop()
} }
log.Info().Msg("Manually stopped Jobs") // ================== STOP DB ==================
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) {
defer cancel() ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
err := app.Database.Stop(ctx) defer cancel()
if err != nil { err := app.Database.Stop(ctx)
log.Info().Err(err).Msg("Error while stopping the database") if err != nil {
log.Err(err).Msg("Failed to stop database")
}
} }
log.Info().Msg("Stopped Databases")
log.Info().Msg("Manually closed database connection") // ================== FINISH ==================
app.IsRunning.Set(false) app.IsRunning.Set(false)
} }
@@ -219,77 +207,12 @@ func (app *Application) Migrate() error {
return app.Database.Migrate(ctx) return app.Database.Migrate(ctx)
} }
type RequestOptions struct {
IgnoreWrongContentType bool
}
func (app *Application) StartRequest(g *gin.Context, uri any, query any, body any, form any, opts ...RequestOptions) (*AppContext, *ginresp.HTTPResponse) {
ignoreWrongContentType := langext.ArrAny(opts, func(o RequestOptions) bool { return o.IgnoreWrongContentType })
if uri != nil {
if err := g.ShouldBindUri(uri); err != nil {
return nil, langext.Ptr(ginresp.APIError(g, 400, apierr.BINDFAIL_URI_PARAM, "Failed to read uri", err))
}
}
if query != nil {
if err := g.ShouldBindQuery(query); err != nil {
return nil, langext.Ptr(ginresp.APIError(g, 400, apierr.BINDFAIL_QUERY_PARAM, "Failed to read query", err))
}
}
if body != nil {
if g.ContentType() == "application/json" {
if err := g.ShouldBindJSON(body); err != nil {
return nil, langext.Ptr(ginresp.APIError(g, 400, apierr.BINDFAIL_BODY_PARAM, "Failed to read body", err))
}
} else {
if !ignoreWrongContentType {
return nil, langext.Ptr(ginresp.APIError(g, 400, apierr.BINDFAIL_BODY_PARAM, "missing JSON body", nil))
}
}
}
if form != nil {
if g.ContentType() == "multipart/form-data" {
if err := g.ShouldBindWith(form, binding.Form); err != nil {
return nil, langext.Ptr(ginresp.APIError(g, 400, apierr.BINDFAIL_BODY_PARAM, "Failed to read multipart-form", err))
}
} else if g.ContentType() == "application/x-www-form-urlencoded" {
if err := g.ShouldBindWith(form, binding.Form); err != nil {
return nil, langext.Ptr(ginresp.APIError(g, 400, apierr.BINDFAIL_BODY_PARAM, "Failed to read urlencoded-form", err))
}
} else {
if !ignoreWrongContentType {
return nil, langext.Ptr(ginresp.APIError(g, 400, apierr.BINDFAIL_BODY_PARAM, "missing form body", nil))
}
}
}
ictx, cancel := context.WithTimeout(context.Background(), app.Config.RequestTimeout)
actx := CreateAppContext(app, g, ictx, cancel)
authheader := g.GetHeader("Authorization")
perm, err := app.getPermissions(actx, authheader)
if err != nil {
cancel()
return nil, langext.Ptr(ginresp.APIError(g, 400, apierr.PERM_QUERY_FAIL, "Failed to determine permissions", err))
}
actx.permissions = perm
g.Set("perm", perm)
return actx, nil
}
func (app *Application) NewSimpleTransactionContext(timeout time.Duration) *simplectx.SimpleContext { func (app *Application) NewSimpleTransactionContext(timeout time.Duration) *simplectx.SimpleContext {
ictx, cancel := context.WithTimeout(context.Background(), timeout) ictx, cancel := context.WithTimeout(context.Background(), timeout)
return simplectx.CreateSimpleContext(ictx, cancel) return simplectx.CreateSimpleContext(ictx, cancel)
} }
func (app *Application) getPermissions(ctx *AppContext, hdr string) (models.PermissionSet, error) { func (app *Application) getPermissions(ctx db.TxContext, hdr string) (models.PermissionSet, error) {
if hdr == "" { if hdr == "" {
return models.NewEmptyPermissions(), nil return models.NewEmptyPermissions(), nil
} }
+2 -1
View File
@@ -10,6 +10,7 @@ import (
"fmt" "fmt"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"gogs.mikescher.com/BlackForestBytes/goext/ginext"
"gogs.mikescher.com/BlackForestBytes/goext/langext" "gogs.mikescher.com/BlackForestBytes/goext/langext"
"gogs.mikescher.com/BlackForestBytes/goext/mathext" "gogs.mikescher.com/BlackForestBytes/goext/mathext"
"gogs.mikescher.com/BlackForestBytes/goext/timeext" "gogs.mikescher.com/BlackForestBytes/goext/timeext"
@@ -24,7 +25,7 @@ type SendMessageResponse struct {
CompatMessageID int64 CompatMessageID int64
} }
func (app *Application) SendMessage(g *gin.Context, ctx *AppContext, UserID *models.UserID, Key *string, Channel *string, Title *string, Content *string, Priority *int, UserMessageID *string, SendTimestamp *float64, SenderName *string) (*SendMessageResponse, *ginresp.HTTPResponse) { func (app *Application) SendMessage(g *gin.Context, ctx *AppContext, UserID *models.UserID, Key *string, Channel *string, Title *string, Content *string, Priority *int, UserMessageID *string, SendTimestamp *float64, SenderName *string) (*SendMessageResponse, *ginext.HTTPResponse) {
if Title != nil { if Title != nil {
Title = langext.Ptr(strings.TrimSpace(*Title)) Title = langext.Ptr(strings.TrimSpace(*Title))
} }
+8 -7
View File
@@ -6,10 +6,11 @@ import (
"blackforestbytes.com/simplecloudnotifier/models" "blackforestbytes.com/simplecloudnotifier/models"
"database/sql" "database/sql"
"errors" "errors"
"gogs.mikescher.com/BlackForestBytes/goext/ginext"
"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) *ginext.HTTPResponse {
p := ac.permissions p := ac.permissions
if p.Token != nil && p.Token.IsUserRead(userid) { if p.Token != nil && p.Token.IsUserRead(userid) {
return nil return nil
@@ -18,7 +19,7 @@ func (ac *AppContext) CheckPermissionUserRead(userid models.UserID) *ginresp.HTT
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) CheckPermissionSelfAllMessagesRead() *ginresp.HTTPResponse { func (ac *AppContext) CheckPermissionSelfAllMessagesRead() *ginext.HTTPResponse {
p := ac.permissions p := ac.permissions
if p.Token != nil && p.Token.IsAllMessagesRead(p.Token.OwnerUserID) { if p.Token != nil && p.Token.IsAllMessagesRead(p.Token.OwnerUserID) {
return nil return nil
@@ -27,7 +28,7 @@ func (ac *AppContext) CheckPermissionSelfAllMessagesRead() *ginresp.HTTPResponse
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) CheckPermissionAllMessagesRead(userid models.UserID) *ginresp.HTTPResponse { func (ac *AppContext) CheckPermissionAllMessagesRead(userid models.UserID) *ginext.HTTPResponse {
p := ac.permissions p := ac.permissions
if p.Token != nil && p.Token.IsAllMessagesRead(userid) { if p.Token != nil && p.Token.IsAllMessagesRead(userid) {
return nil return nil
@@ -36,7 +37,7 @@ func (ac *AppContext) CheckPermissionAllMessagesRead(userid models.UserID) *ginr
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 { func (ac *AppContext) CheckPermissionChanMessagesRead(channel models.Channel) *ginext.HTTPResponse {
p := ac.permissions p := ac.permissions
if p.Token != nil && p.Token.IsChannelMessagesRead(channel.ChannelID) { if p.Token != nil && p.Token.IsChannelMessagesRead(channel.ChannelID) {
@@ -63,7 +64,7 @@ func (ac *AppContext) CheckPermissionChanMessagesRead(channel models.Channel) *g
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) *ginext.HTTPResponse {
p := ac.permissions p := ac.permissions
if p.Token != nil && p.Token.IsAdmin(userid) { if p.Token != nil && p.Token.IsAdmin(userid) {
return nil return nil
@@ -72,7 +73,7 @@ func (ac *AppContext) CheckPermissionUserAdmin(userid models.UserID) *ginresp.HT
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(channel models.Channel, key string) (*models.KeyToken, *ginresp.HTTPResponse) { func (ac *AppContext) CheckPermissionSend(channel models.Channel, key string) (*models.KeyToken, *ginext.HTTPResponse) {
keytok, err := ac.app.Database.Primary.GetKeyTokenByToken(ac, key) keytok, err := ac.app.Database.Primary.GetKeyTokenByToken(ac, key)
if err != nil { if err != nil {
@@ -107,7 +108,7 @@ func (ac *AppContext) CheckPermissionMessageDelete(msg models.Message) bool {
return false return false
} }
func (ac *AppContext) CheckPermissionAny() *ginresp.HTTPResponse { func (ac *AppContext) CheckPermissionAny() *ginext.HTTPResponse {
p := ac.permissions p := ac.permissions
if p.Token == nil { 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 langext.Ptr(ginresp.APIError(ac.ginContext, 401, apierr.USER_AUTH_FAILED, "You are not authorized for this action", nil))
+264
View File
@@ -0,0 +1,264 @@
package logic
import (
scn "blackforestbytes.com/simplecloudnotifier"
"blackforestbytes.com/simplecloudnotifier/api/apierr"
"blackforestbytes.com/simplecloudnotifier/api/ginresp"
"blackforestbytes.com/simplecloudnotifier/models"
"context"
"errors"
"fmt"
"github.com/gin-gonic/gin"
"github.com/mattn/go-sqlite3"
"github.com/rs/zerolog/log"
"gogs.mikescher.com/BlackForestBytes/goext/dataext"
"gogs.mikescher.com/BlackForestBytes/goext/exerr"
"gogs.mikescher.com/BlackForestBytes/goext/ginext"
"gogs.mikescher.com/BlackForestBytes/goext/langext"
"math/rand"
"runtime/debug"
"time"
)
type RequestOptions struct {
IgnoreWrongContentType bool
}
func (app *Application) DoRequest(gectx *ginext.AppContext, g *gin.Context, lockmode models.TransactionLockMode, fn func(ctx *AppContext, finishSuccess func(r ginext.HTTPResponse) ginext.HTTPResponse) ginext.HTTPResponse) ginext.HTTPResponse {
maxRetry := scn.Conf.RequestMaxRetry
retrySleep := scn.Conf.RequestRetrySleep
reqctx := g.Request.Context()
t0 := time.Now()
for ctr := 1; ; ctr++ {
ictx, cancel := context.WithTimeout(gectx, app.Config.RequestTimeout)
actx := CreateAppContext(app, g, ictx, cancel)
wrap, stackTrace, panicObj := callPanicSafe(func(ctx *AppContext, finishSuccess func(r ginext.HTTPResponse) ginext.HTTPResponse) ginext.HTTPResponse {
dl, ok := ctx.Deadline()
if !ok {
dl = time.Now().Add(time.Second * 5)
}
if lockmode == models.TLockRead {
islock := app.MainDatabaseLock.RTryLockWithTimeout(dl.Sub(time.Now()))
if !islock {
return ginresp.APIError(g, 500, apierr.INTERNAL_EXCEPTION, "Failed to lock {MainDatabaseLock} [ro]", nil)
}
defer app.MainDatabaseLock.RUnlock()
} else if lockmode == models.TLockReadWrite {
islock := app.MainDatabaseLock.TryLockWithTimeout(dl.Sub(time.Now()))
if !islock {
return ginresp.APIError(g, 500, apierr.INTERNAL_EXCEPTION, "Failed to lock {MainDatabaseLock} [rw]", nil)
}
defer app.MainDatabaseLock.Unlock()
}
authheader := g.GetHeader("Authorization")
perm, err := app.getPermissions(actx, authheader)
if err != nil {
cancel()
return ginresp.APIError(g, 400, apierr.PERM_QUERY_FAIL, "Failed to determine permissions", err)
}
actx.permissions = perm
g.Set("perm", perm)
return fn(actx, finishSuccess)
}, actx, actx._FinishSuccess)
if panicObj != nil {
log.Error().Interface("panicObj", panicObj).Msg("Panic occured (in gin handler)")
log.Error().Msg(stackTrace)
wrap = ginresp.APIError(g, 500, apierr.PANIC, "A panic occured in the HTTP handler", errors.New(fmt.Sprintf("%+v\n\n@:\n%s", panicObj, stackTrace)))
}
if g.Writer.Written() {
if scn.Conf.ReqLogEnabled {
app.InsertRequestLog(createRequestLog(g, t0, ctr, nil, langext.Ptr("Writing in WrapperFunc is not supported")))
}
panic("Writing in WrapperFunc is not supported")
}
if ctr < maxRetry && isSqlite3Busy(wrap) {
log.Warn().Int("counter", ctr).Str("url", g.Request.URL.String()).Msg("Retry request (ErrBusy)")
err := resetBody(g)
if err != nil {
panic(err)
}
time.Sleep(time.Duration(int64(float64(retrySleep) * (0.5 + rand.Float64()))))
continue
}
if reqctx.Err() == nil {
if scn.Conf.ReqLogEnabled {
app.InsertRequestLog(createRequestLog(g, t0, ctr, wrap, nil))
}
if scw, ok := wrap.(ginext.InspectableHTTPResponse); ok {
statuscode := scw.Statuscode()
if statuscode/100 != 2 {
log.Warn().Str("url", g.Request.Method+"::"+g.Request.URL.String()).Msg(fmt.Sprintf("Request failed with statuscode %d", statuscode))
}
} else {
log.Warn().Str("url", g.Request.Method+"::"+g.Request.URL.String()).Msg(fmt.Sprintf("Request failed with statuscode [unknown]"))
}
}
return wrap
}
}
func callPanicSafe(fn func(ctx *AppContext, finishSuccess func(r ginext.HTTPResponse) ginext.HTTPResponse) ginext.HTTPResponse, actx *AppContext, fnFin func(r ginext.HTTPResponse) ginext.HTTPResponse) (res ginext.HTTPResponse, stackTrace string, panicObj any) {
defer func() {
if rec := recover(); rec != nil {
res = nil
stackTrace = string(debug.Stack())
panicObj = rec
}
}()
res = fn(actx, fnFin)
return res, "", nil
}
func createRequestLog(g *gin.Context, t0 time.Time, ctr int, resp ginext.HTTPResponse, panicstr *string) models.RequestLog {
t1 := time.Now()
ua := g.Request.UserAgent()
auth := g.Request.Header.Get("Authorization")
ct := g.Request.Header.Get("Content-Type")
var reqbody []byte = nil
if g.Request.Body != nil {
brcbody, err := g.Request.Body.(dataext.BufferedReadCloser).BufferedAll()
if err == nil {
reqbody = brcbody
}
}
var strreqbody *string = nil
if len(reqbody) < scn.Conf.ReqLogMaxBodySize {
strreqbody = langext.Ptr(string(reqbody))
}
var respbody *string = nil
var strrespbody *string = nil
if resp != nil {
if resp2, ok := resp.(ginext.InspectableHTTPResponse); ok {
respbody = resp2.BodyString(g)
if respbody != nil && len(*respbody) < scn.Conf.ReqLogMaxBodySize {
strrespbody = respbody
}
}
}
permObj, hasPerm := g.Get("perm")
hasTok := false
if hasPerm {
hasTok = permObj.(models.PermissionSet).Token != nil
}
var statuscode *int64 = nil
if resp != nil {
if resp2, ok := resp.(ginext.InspectableHTTPResponse); ok {
statuscode = langext.Ptr(int64(resp2.Statuscode()))
}
}
var contentType = ""
if resp != nil {
if resp2, ok := resp.(ginext.InspectableHTTPResponse); ok {
contentType = resp2.ContentType()
}
}
return models.RequestLog{
Method: g.Request.Method,
URI: g.Request.URL.String(),
UserAgent: langext.Conditional(ua == "", nil, &ua),
Authentication: langext.Conditional(auth == "", nil, &auth),
RequestBody: strreqbody,
RequestBodySize: int64(len(reqbody)),
RequestContentType: ct,
RemoteIP: g.RemoteIP(),
KeyID: langext.ConditionalFn10(hasTok, func() *models.KeyTokenID { return langext.Ptr(permObj.(models.PermissionSet).Token.KeyTokenID) }, nil),
UserID: langext.ConditionalFn10(hasTok, func() *models.UserID { return langext.Ptr(permObj.(models.PermissionSet).Token.OwnerUserID) }, nil),
Permissions: langext.ConditionalFn10(hasTok, func() *string { return langext.Ptr(permObj.(models.PermissionSet).Token.Permissions.String()) }, nil),
ResponseStatuscode: statuscode,
ResponseBodySize: langext.ConditionalFn10(strrespbody != nil, func() *int64 { return langext.Ptr(int64(len(*respbody))) }, nil),
ResponseBody: strrespbody,
ResponseContentType: contentType,
RetryCount: int64(ctr),
Panicked: panicstr != nil,
PanicStr: panicstr,
ProcessingTime: models.SCNDuration(t1.Sub(t0)),
TimestampStart: models.NewSCNTime(t0),
TimestampFinish: models.NewSCNTime(t1),
}
}
func resetBody(g *gin.Context) error {
if g.Request.Body == nil {
return nil
}
err := g.Request.Body.(dataext.BufferedReadCloser).Reset()
if err != nil {
return err
}
return nil
}
func isSqlite3Busy(r ginext.HTTPResponse) bool {
if errwrap, ok := r.(interface{ Unwrap() error }); ok && errwrap != nil {
orig := exerr.OriginalError(errwrap.Unwrap())
var sqlite3Err sqlite3.Error
if errors.As(orig, &sqlite3Err) {
if sqlite3Err.Code == 5 { // [5] == SQLITE_BUSY
return true
}
}
}
return false
}
func BuildGinRequestError(g *gin.Context, fieldtype string, err error) ginext.HTTPResponse {
switch fieldtype {
case "URI":
return ginresp.APIError(g, 400, apierr.BINDFAIL_URI_PARAM, "Failed to read uri", err)
case "QUERY":
return ginresp.APIError(g, 400, apierr.BINDFAIL_QUERY_PARAM, "Failed to read query", err)
case "JSON":
return ginresp.APIError(g, 400, apierr.BINDFAIL_BODY_PARAM, "Failed to read JSON body", err)
case "BODY":
return ginresp.APIError(g, 400, apierr.BINDFAIL_BODY_PARAM, "Failed to read query", err)
case "FORM":
return ginresp.APIError(g, 400, apierr.BINDFAIL_BODY_PARAM, "Failed to read multipart-form / urlencoded-form", err)
case "HEADER":
return ginresp.APIError(g, 400, apierr.BINDFAIL_HEADER_PARAM, "Failed to read header", err)
case "INIT":
return ginresp.APIError(g, 400, apierr.INTERNAL_EXCEPTION, "Failed to init context", err)
default:
return ginresp.APIError(g, 400, apierr.INTERNAL_EXCEPTION, "Failed to init", err)
}
}
+22 -146
View File
@@ -1,37 +1,28 @@
package models package models
import (
"context"
"github.com/jmoiron/sqlx"
"gogs.mikescher.com/BlackForestBytes/goext/langext"
"gogs.mikescher.com/BlackForestBytes/goext/sq"
"time"
)
type Channel struct { type Channel struct {
ChannelID ChannelID ChannelID ChannelID `db:"channel_id" json:"channel_id"`
OwnerUserID UserID OwnerUserID UserID `db:"owner_user_id" json:"owner_user_id"`
InternalName string InternalName string `db:"internal_name" json:"internal_name"`
DisplayName string DisplayName string `db:"display_name" json:"display_name"`
DescriptionName *string DescriptionName *string `db:"description_name" json:"description_name"`
SubscribeKey string SubscribeKey string `db:"subscribe_key" json:"subscribe_key" jsonfilter:"INCLUDE_KEY"` // can be nil, depending on endpoint
TimestampCreated time.Time TimestampCreated SCNTime `db:"timestamp_created" json:"timestamp_created"`
TimestampLastSent *time.Time TimestampLastSent *SCNTime `db:"timestamp_lastsent" json:"timestamp_lastsent"`
MessagesSent int MessagesSent int `db:"messages_sent" json:"messages_sent"`
} }
func (c Channel) JSON(includeKey bool) ChannelJSON { type ChannelWithSubscription struct {
return ChannelJSON{ Channel
ChannelID: c.ChannelID, Subscription *Subscription `db:"sub" json:"subscription"`
OwnerUserID: c.OwnerUserID, }
InternalName: c.InternalName,
DisplayName: c.DisplayName, type ChannelPreview struct {
DescriptionName: c.DescriptionName, ChannelID ChannelID `json:"channel_id"`
SubscribeKey: langext.Conditional(includeKey, langext.Ptr(c.SubscribeKey), nil), OwnerUserID UserID `json:"owner_user_id"`
TimestampCreated: c.TimestampCreated.Format(time.RFC3339Nano), InternalName string `json:"internal_name"`
TimestampLastSent: timeOptFmt(c.TimestampLastSent, time.RFC3339Nano), DisplayName string `json:"display_name"`
MessagesSent: c.MessagesSent, DescriptionName *string `json:"description_name"`
}
} }
func (c Channel) WithSubscription(sub *Subscription) ChannelWithSubscription { func (c Channel) WithSubscription(sub *Subscription) ChannelWithSubscription {
@@ -41,8 +32,8 @@ func (c Channel) WithSubscription(sub *Subscription) ChannelWithSubscription {
} }
} }
func (c Channel) JSONPreview() ChannelPreviewJSON { func (c Channel) Preview() ChannelPreview {
return ChannelPreviewJSON{ return ChannelPreview{
ChannelID: c.ChannelID, ChannelID: c.ChannelID,
OwnerUserID: c.OwnerUserID, OwnerUserID: c.OwnerUserID,
InternalName: c.InternalName, InternalName: c.InternalName,
@@ -50,118 +41,3 @@ func (c Channel) JSONPreview() ChannelPreviewJSON {
DescriptionName: c.DescriptionName, DescriptionName: c.DescriptionName,
} }
} }
type ChannelWithSubscription struct {
Channel
Subscription *Subscription
}
func (c ChannelWithSubscription) JSON(includeChannelKey bool) ChannelWithSubscriptionJSON {
var sub *SubscriptionJSON = nil
if c.Subscription != nil {
sub = langext.Ptr(c.Subscription.JSON())
}
return ChannelWithSubscriptionJSON{
ChannelJSON: c.Channel.JSON(includeChannelKey),
Subscription: sub,
}
}
type ChannelJSON struct {
ChannelID ChannelID `json:"channel_id"`
OwnerUserID UserID `json:"owner_user_id"`
InternalName string `json:"internal_name"`
DisplayName string `json:"display_name"`
DescriptionName *string `json:"description_name"`
SubscribeKey *string `json:"subscribe_key"` // can be nil, depending on endpoint
TimestampCreated string `json:"timestamp_created"`
TimestampLastSent *string `json:"timestamp_lastsent"`
MessagesSent int `json:"messages_sent"`
}
type ChannelWithSubscriptionJSON struct {
ChannelJSON
Subscription *SubscriptionJSON `json:"subscription"`
}
type ChannelPreviewJSON struct {
ChannelID ChannelID `json:"channel_id"`
OwnerUserID UserID `json:"owner_user_id"`
InternalName string `json:"internal_name"`
DisplayName string `json:"display_name"`
DescriptionName *string `json:"description_name"`
}
type ChannelDB struct {
ChannelID ChannelID `db:"channel_id"`
OwnerUserID UserID `db:"owner_user_id"`
InternalName string `db:"internal_name"`
DisplayName string `db:"display_name"`
DescriptionName *string `db:"description_name"`
SubscribeKey string `db:"subscribe_key"`
TimestampCreated int64 `db:"timestamp_created"`
TimestampLastSent *int64 `db:"timestamp_lastsent"`
MessagesSent int `db:"messages_sent"`
}
func (c ChannelDB) Model() Channel {
return Channel{
ChannelID: c.ChannelID,
OwnerUserID: c.OwnerUserID,
InternalName: c.InternalName,
DisplayName: c.DisplayName,
DescriptionName: c.DescriptionName,
SubscribeKey: c.SubscribeKey,
TimestampCreated: timeFromMilli(c.TimestampCreated),
TimestampLastSent: timeOptFromMilli(c.TimestampLastSent),
MessagesSent: c.MessagesSent,
}
}
type ChannelWithSubscriptionDB struct {
ChannelDB
Subscription *SubscriptionDB `db:"sub"`
}
func (c ChannelWithSubscriptionDB) Model() ChannelWithSubscription {
var sub *Subscription = nil
if c.Subscription != nil {
sub = langext.Ptr(c.Subscription.Model())
}
return ChannelWithSubscription{
Channel: c.ChannelDB.Model(),
Subscription: sub,
}
}
func DecodeChannel(ctx context.Context, q sq.Queryable, r *sqlx.Rows) (Channel, error) {
data, err := sq.ScanSingle[ChannelDB](ctx, q, r, sq.SModeFast, sq.Safe, true)
if err != nil {
return Channel{}, err
}
return data.Model(), nil
}
func DecodeChannels(ctx context.Context, q sq.Queryable, r *sqlx.Rows) ([]Channel, error) {
data, err := sq.ScanAll[ChannelDB](ctx, q, r, sq.SModeFast, sq.Safe, true)
if err != nil {
return nil, err
}
return langext.ArrMap(data, func(v ChannelDB) Channel { return v.Model() }), nil
}
func DecodeChannelWithSubscription(ctx context.Context, q sq.Queryable, r *sqlx.Rows) (ChannelWithSubscription, error) {
data, err := sq.ScanSingle[ChannelWithSubscriptionDB](ctx, q, r, sq.SModeExtended, sq.Safe, true)
if err != nil {
return ChannelWithSubscription{}, err
}
return data.Model(), nil
}
func DecodeChannelsWithSubscription(ctx context.Context, q sq.Queryable, r *sqlx.Rows) ([]ChannelWithSubscription, error) {
data, err := sq.ScanAll[ChannelWithSubscriptionDB](ctx, q, r, sq.SModeExtended, sq.Safe, true)
if err != nil {
return nil, err
}
return langext.ArrMap(data, func(v ChannelWithSubscriptionDB) ChannelWithSubscription { return v.Model() }), nil
}
+8 -80
View File
@@ -1,13 +1,5 @@
package models package models
import (
"context"
"github.com/jmoiron/sqlx"
"gogs.mikescher.com/BlackForestBytes/goext/langext"
"gogs.mikescher.com/BlackForestBytes/goext/sq"
"time"
)
type ClientType string //@enum:type type ClientType string //@enum:type
const ( const (
@@ -19,76 +11,12 @@ const (
) )
type Client struct { type Client struct {
ClientID ClientID ClientID ClientID `db:"client_id" json:"client_id"`
UserID UserID UserID UserID `db:"user_id" json:"user_id"`
Type ClientType Type ClientType `db:"type" json:"type"`
FCMToken string FCMToken string `db:"fcm_token" json:"fcm_token"`
TimestampCreated time.Time TimestampCreated SCNTime `db:"timestamp_created" json:"timestamp_created"`
AgentModel string AgentModel string `db:"agent_model" json:"agent_model"`
AgentVersion string AgentVersion string `db:"agent_version" json:"agent_version"`
Name *string Name *string `db:"name" json:"name"`
}
func (c Client) JSON() ClientJSON {
return ClientJSON{
ClientID: c.ClientID,
UserID: c.UserID,
Type: c.Type,
FCMToken: c.FCMToken,
TimestampCreated: c.TimestampCreated.Format(time.RFC3339Nano),
AgentModel: c.AgentModel,
AgentVersion: c.AgentVersion,
Name: c.Name,
}
}
type ClientJSON struct {
ClientID ClientID `json:"client_id"`
UserID UserID `json:"user_id"`
Type ClientType `json:"type"`
FCMToken string `json:"fcm_token"`
TimestampCreated string `json:"timestamp_created"`
AgentModel string `json:"agent_model"`
AgentVersion string `json:"agent_version"`
Name *string `json:"name"`
}
type ClientDB struct {
ClientID ClientID `db:"client_id"`
UserID UserID `db:"user_id"`
Type ClientType `db:"type"`
FCMToken string `db:"fcm_token"`
TimestampCreated int64 `db:"timestamp_created"`
AgentModel string `db:"agent_model"`
AgentVersion string `db:"agent_version"`
Name *string `db:"name"`
}
func (c ClientDB) Model() Client {
return Client{
ClientID: c.ClientID,
UserID: c.UserID,
Type: c.Type,
FCMToken: c.FCMToken,
TimestampCreated: timeFromMilli(c.TimestampCreated),
AgentModel: c.AgentModel,
AgentVersion: c.AgentVersion,
Name: c.Name,
}
}
func DecodeClient(ctx context.Context, q sq.Queryable, r *sqlx.Rows) (Client, error) {
data, err := sq.ScanSingle[ClientDB](ctx, q, r, sq.SModeFast, sq.Safe, true)
if err != nil {
return Client{}, err
}
return data.Model(), nil
}
func DecodeClients(ctx context.Context, q sq.Queryable, r *sqlx.Rows) ([]Client, error) {
data, err := sq.ScanAll[ClientDB](ctx, q, r, sq.SModeFast, sq.Safe, true)
if err != nil {
return nil, err
}
return langext.ArrMap(data, func(v ClientDB) Client { return v.Model() }), nil
} }
+10 -90
View File
@@ -1,13 +1,5 @@
package models package models
import (
"context"
"github.com/jmoiron/sqlx"
"gogs.mikescher.com/BlackForestBytes/goext/langext"
"gogs.mikescher.com/BlackForestBytes/goext/sq"
"time"
)
type DeliveryStatus string //@enum:type type DeliveryStatus string //@enum:type
const ( const (
@@ -17,90 +9,18 @@ const (
) )
type Delivery struct { type Delivery struct {
DeliveryID DeliveryID DeliveryID DeliveryID `db:"delivery_id" json:"delivery_id"`
MessageID MessageID MessageID MessageID `db:"message_id" json:"message_id"`
ReceiverUserID UserID ReceiverUserID UserID `db:"receiver_user_id" json:"receiver_user_id"`
ReceiverClientID ClientID ReceiverClientID ClientID `db:"receiver_client_id" json:"receiver_client_id"`
TimestampCreated time.Time TimestampCreated SCNTime `db:"timestamp_created" json:"timestamp_created"`
TimestampFinalized *time.Time TimestampFinalized *SCNTime `db:"timestamp_finalized" json:"timestamp_finalized"`
Status DeliveryStatus Status DeliveryStatus `db:"status" json:"status"`
RetryCount int RetryCount int `db:"retry_count" json:"retry_count"`
NextDelivery *time.Time NextDelivery *SCNTime `db:"next_delivery" json:"next_delivery"`
FCMMessageID *string FCMMessageID *string `db:"fcm_message_id" json:"fcm_message_id"`
}
func (d Delivery) JSON() DeliveryJSON {
return DeliveryJSON{
DeliveryID: d.DeliveryID,
MessageID: d.MessageID,
ReceiverUserID: d.ReceiverUserID,
ReceiverClientID: d.ReceiverClientID,
TimestampCreated: d.TimestampCreated.Format(time.RFC3339Nano),
TimestampFinalized: timeOptFmt(d.TimestampFinalized, time.RFC3339Nano),
Status: d.Status,
RetryCount: d.RetryCount,
NextDelivery: timeOptFmt(d.NextDelivery, time.RFC3339Nano),
FCMMessageID: d.FCMMessageID,
}
} }
func (d Delivery) MaxRetryCount() int { func (d Delivery) MaxRetryCount() int {
return 5 return 5
} }
type DeliveryJSON struct {
DeliveryID DeliveryID `json:"delivery_id"`
MessageID MessageID `json:"message_id"`
ReceiverUserID UserID `json:"receiver_user_id"`
ReceiverClientID ClientID `json:"receiver_client_id"`
TimestampCreated string `json:"timestamp_created"`
TimestampFinalized *string `json:"timestamp_finalized"`
Status DeliveryStatus `json:"status"`
RetryCount int `json:"retry_count"`
NextDelivery *string `json:"next_delivery"`
FCMMessageID *string `json:"fcm_message_id"`
}
type DeliveryDB struct {
DeliveryID DeliveryID `db:"delivery_id"`
MessageID MessageID `db:"message_id"`
ReceiverUserID UserID `db:"receiver_user_id"`
ReceiverClientID ClientID `db:"receiver_client_id"`
TimestampCreated int64 `db:"timestamp_created"`
TimestampFinalized *int64 `db:"timestamp_finalized"`
Status DeliveryStatus `db:"status"`
RetryCount int `db:"retry_count"`
NextDelivery *int64 `db:"next_delivery"`
FCMMessageID *string `db:"fcm_message_id"`
}
func (d DeliveryDB) Model() Delivery {
return Delivery{
DeliveryID: d.DeliveryID,
MessageID: d.MessageID,
ReceiverUserID: d.ReceiverUserID,
ReceiverClientID: d.ReceiverClientID,
TimestampCreated: timeFromMilli(d.TimestampCreated),
TimestampFinalized: timeOptFromMilli(d.TimestampFinalized),
Status: d.Status,
RetryCount: d.RetryCount,
NextDelivery: timeOptFromMilli(d.NextDelivery),
FCMMessageID: d.FCMMessageID,
}
}
func DecodeDelivery(ctx context.Context, q sq.Queryable, r *sqlx.Rows) (Delivery, error) {
data, err := sq.ScanSingle[DeliveryDB](ctx, q, r, sq.SModeFast, sq.Safe, true)
if err != nil {
return Delivery{}, err
}
return data.Model(), nil
}
func DecodeDeliveries(ctx context.Context, q sq.Queryable, r *sqlx.Rows) ([]Delivery, error) {
data, err := sq.ScanAll[DeliveryDB](ctx, q, r, sq.SModeFast, sq.Safe, true)
if err != nil {
return nil, err
}
return langext.ArrMap(data, func(v DeliveryDB) Delivery { return v.Model() }), nil
}
+35
View File
@@ -0,0 +1,35 @@
package models
import (
"encoding/json"
"gogs.mikescher.com/BlackForestBytes/goext/timeext"
"time"
)
type SCNDuration time.Duration
func (t SCNDuration) MarshalToDB(v SCNDuration) (int64, error) {
return v.Duration().Milliseconds(), nil
}
func (t SCNDuration) UnmarshalToModel(v int64) (SCNDuration, error) {
return SCNDuration(timeext.FromMilliseconds(v)), nil
}
func (t SCNDuration) Duration() time.Duration {
return time.Duration(t)
}
func (t *SCNDuration) UnmarshalJSON(data []byte) error {
flt := float64(0)
if err := json.Unmarshal(data, &flt); err != nil {
return err
}
d0 := timeext.FromSeconds(flt)
*t = SCNDuration(d0)
return nil
}
func (t SCNDuration) MarshalJSON() ([]byte, error) {
return json.Marshal(t.Duration().Seconds())
}
+82 -1
View File
@@ -5,7 +5,7 @@ package models
import "gogs.mikescher.com/BlackForestBytes/goext/langext" import "gogs.mikescher.com/BlackForestBytes/goext/langext"
import "gogs.mikescher.com/BlackForestBytes/goext/enums" import "gogs.mikescher.com/BlackForestBytes/goext/enums"
const ChecksumEnumGenerator = "e500346e3f60b3abf78558ec3df128c3be2a1cefa71c4f1feba9293d14eb85d1" // GoExtVersion: 0.0.463 const ChecksumEnumGenerator = "902919af7c6d46bd6701b33e47308bad93d50cd10cdacaac739e5242819c4d7b" // GoExtVersion: 0.0.512
// ================================ ClientType ================================ // ================================ ClientType ================================
// //
@@ -283,6 +283,86 @@ func TokenPermValuesDescriptionMeta() []enums.EnumDescriptionMetaValue {
} }
} }
// ================================ TransactionLockMode ================================
//
// File: lock.go
// StringEnum: true
// DescrEnum: false
// DataEnum: false
//
var __TransactionLockModeValues = []TransactionLockMode{
TLockNone,
TLockRead,
TLockReadWrite,
}
var __TransactionLockModeVarnames = map[TransactionLockMode]string{
TLockNone: "TLockNone",
TLockRead: "TLockRead",
TLockReadWrite: "TLockReadWrite",
}
func (e TransactionLockMode) Valid() bool {
return langext.InArray(e, __TransactionLockModeValues)
}
func (e TransactionLockMode) Values() []TransactionLockMode {
return __TransactionLockModeValues
}
func (e TransactionLockMode) ValuesAny() []any {
return langext.ArrCastToAny(__TransactionLockModeValues)
}
func (e TransactionLockMode) ValuesMeta() []enums.EnumMetaValue {
return TransactionLockModeValuesMeta()
}
func (e TransactionLockMode) String() string {
return string(e)
}
func (e TransactionLockMode) VarName() string {
if d, ok := __TransactionLockModeVarnames[e]; ok {
return d
}
return ""
}
func (e TransactionLockMode) TypeName() string {
return "TransactionLockMode"
}
func (e TransactionLockMode) PackageName() string {
return "models"
}
func (e TransactionLockMode) Meta() enums.EnumMetaValue {
return enums.EnumMetaValue{VarName: e.VarName(), Value: e, Description: nil}
}
func ParseTransactionLockMode(vv string) (TransactionLockMode, bool) {
for _, ev := range __TransactionLockModeValues {
if string(ev) == vv {
return ev, true
}
}
return "", false
}
func TransactionLockModeValues() []TransactionLockMode {
return __TransactionLockModeValues
}
func TransactionLockModeValuesMeta() []enums.EnumMetaValue {
return []enums.EnumMetaValue{
TLockNone.Meta(),
TLockRead.Meta(),
TLockReadWrite.Meta(),
}
}
// ================================ ================= ================================ // ================================ ================= ================================
func AllPackageEnums() []enums.Enum { func AllPackageEnums() []enums.Enum {
@@ -290,5 +370,6 @@ func AllPackageEnums() []enums.Enum {
ClientTypeAndroid, // ClientType ClientTypeAndroid, // ClientType
DeliveryStatusRetry, // DeliveryStatus DeliveryStatusRetry, // DeliveryStatus
PermAdmin, // TokenPerm PermAdmin, // TokenPerm
TLockNone, // TransactionLockMode
} }
} }
+1 -1
View File
@@ -15,7 +15,7 @@ import "reflect"
import "regexp" import "regexp"
import "strings" import "strings"
const ChecksumCharsetIDGenerator = "e500346e3f60b3abf78558ec3df128c3be2a1cefa71c4f1feba9293d14eb85d1" // GoExtVersion: 0.0.463 const ChecksumCharsetIDGenerator = "902919af7c6d46bd6701b33e47308bad93d50cd10cdacaac739e5242819c4d7b" // GoExtVersion: 0.0.512
const idlen = 24 const idlen = 24
+49 -113
View File
@@ -1,12 +1,9 @@
package models package models
import ( import (
"context" "encoding/json"
"github.com/jmoiron/sqlx"
"gogs.mikescher.com/BlackForestBytes/goext/langext" "gogs.mikescher.com/BlackForestBytes/goext/langext"
"gogs.mikescher.com/BlackForestBytes/goext/sq"
"strings" "strings"
"time"
) )
type TokenPerm string //@enum:type type TokenPerm string //@enum:type
@@ -45,17 +42,53 @@ func ParseTokenPermissionList(input string) TokenPermissionList {
return r return r
} }
func (e TokenPermissionList) MarshalToDB(v TokenPermissionList) (string, error) {
return v.String(), nil
}
func (e TokenPermissionList) UnmarshalToModel(v string) (TokenPermissionList, error) {
return ParseTokenPermissionList(v), nil
}
func (t TokenPermissionList) MarshalJSON() ([]byte, error) {
return json.Marshal(t.String())
}
type ChannelIDArr []ChannelID
func (t ChannelIDArr) MarshalToDB(v ChannelIDArr) (string, error) {
return strings.Join(langext.ArrMap(v, func(v ChannelID) string { return v.String() }), ";"), nil
}
func (t ChannelIDArr) UnmarshalToModel(v string) (ChannelIDArr, error) {
channels := make([]ChannelID, 0)
if strings.TrimSpace(v) != "" {
channels = langext.ArrMap(strings.Split(v, ";"), func(v string) ChannelID { return ChannelID(v) })
}
return channels, nil
}
type KeyToken struct { type KeyToken struct {
KeyTokenID KeyTokenID KeyTokenID KeyTokenID `db:"keytoken_id" json:"keytoken_id"`
Name string Name string `db:"name" json:"name"`
TimestampCreated time.Time TimestampCreated SCNTime `db:"timestamp_created" json:"timestamp_created"`
TimestampLastUsed *time.Time TimestampLastUsed *SCNTime `db:"timestamp_lastused" json:"timestamp_lastused"`
OwnerUserID UserID OwnerUserID UserID `db:"owner_user_id" json:"owner_user_id"`
AllChannels bool AllChannels bool `db:"all_channels" json:"all_channels"`
Channels []ChannelID // can also be owned by other user (needs active subscription) Channels ChannelIDArr `db:"channels" json:"channels"`
Token string Token string `db:"token" json:"token" jsonfilter:"INCLUDE_TOKEN"`
Permissions TokenPermissionList Permissions TokenPermissionList `db:"permissions" json:"permissions"`
MessagesSent int MessagesSent int `db:"messages_sent" json:"messages_sent"`
}
type KeyTokenPreview struct {
KeyTokenID KeyTokenID `json:"keytoken_id"`
Name string `json:"name"`
OwnerUserID UserID `json:"owner_user_id"`
AllChannels bool `json:"all_channels"`
Channels []ChannelID `json:"channels"`
Permissions string `json:"permissions"`
} }
func (k KeyToken) IsUserRead(uid UserID) bool { func (k KeyToken) IsUserRead(uid UserID) bool {
@@ -78,22 +111,8 @@ 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) return (k.AllChannels == true || langext.InArray(c.ChannelID, k.Channels)) && k.OwnerUserID == c.OwnerUserID && k.Permissions.Any(PermAdmin, PermChannelSend)
} }
func (k KeyToken) JSON() KeyTokenJSON { func (k KeyToken) Preview() KeyTokenPreview {
return KeyTokenJSON{ return KeyTokenPreview{
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,
}
}
func (k KeyToken) JSONPreview() KeyTokenPreviewJSON {
return KeyTokenPreviewJSON{
KeyTokenID: k.KeyTokenID, KeyTokenID: k.KeyTokenID,
Name: k.Name, Name: k.Name,
OwnerUserID: k.OwnerUserID, OwnerUserID: k.OwnerUserID,
@@ -102,86 +121,3 @@ func (k KeyToken) JSONPreview() KeyTokenPreviewJSON {
Permissions: k.Permissions.String(), Permissions: k.Permissions.String(),
} }
} }
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"`
}
type KeyTokenPreviewJSON struct {
KeyTokenID KeyTokenID `json:"keytoken_id"`
Name string `json:"name"`
OwnerUserID UserID `json:"owner_user_id"`
AllChannels bool `json:"all_channels"`
Channels []ChannelID `json:"channels"`
Permissions string `json:"permissions"`
}
func (j KeyTokenJSON) WithToken(tok string) KeyTokenWithTokenJSON {
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 {
channels := make([]ChannelID, 0)
if strings.TrimSpace(k.Channels) != "" {
channels = langext.ArrMap(strings.Split(k.Channels, ";"), func(v string) ChannelID { return ChannelID(v) })
}
return KeyToken{
KeyTokenID: k.KeyTokenID,
Name: k.Name,
TimestampCreated: timeFromMilli(k.TimestampCreated),
TimestampLastUsed: timeOptFromMilli(k.TimestampLastUsed),
OwnerUserID: k.OwnerUserID,
AllChannels: k.AllChannels,
Channels: channels,
Token: k.Token,
Permissions: ParseTokenPermissionList(k.Permissions),
MessagesSent: k.MessagesSent,
}
}
func DecodeKeyToken(ctx context.Context, q sq.Queryable, r *sqlx.Rows) (KeyToken, error) {
data, err := sq.ScanSingle[KeyTokenDB](ctx, q, r, sq.SModeFast, sq.Safe, true)
if err != nil {
return KeyToken{}, err
}
return data.Model(), nil
}
func DecodeKeyTokens(ctx context.Context, q sq.Queryable, r *sqlx.Rows) ([]KeyToken, error) {
data, err := sq.ScanAll[KeyTokenDB](ctx, q, r, sq.SModeFast, sq.Safe, true)
if err != nil {
return nil, err
}
return langext.ArrMap(data, func(v KeyTokenDB) KeyToken { return v.Model() }), nil
}
+9
View File
@@ -0,0 +1,9 @@
package models
type TransactionLockMode string //@enum:type
const (
TLockNone TransactionLockMode = "NONE"
TLockRead TransactionLockMode = "READ"
TLockReadWrite TransactionLockMode = "READ_WRITE"
)
+31 -117
View File
@@ -1,11 +1,8 @@
package models package models
import ( import (
"context"
"fmt" "fmt"
"github.com/jmoiron/sqlx"
"gogs.mikescher.com/BlackForestBytes/goext/langext" "gogs.mikescher.com/BlackForestBytes/goext/langext"
"gogs.mikescher.com/BlackForestBytes/goext/sq"
"time" "time"
) )
@@ -15,60 +12,45 @@ const (
) )
type Message struct { type Message struct {
MessageID MessageID MessageID MessageID `db:"message_id" json:"message_id"`
SenderUserID UserID // user that sent the message (this is also the owner of the channel that contains it) SenderUserID UserID `db:"sender_user_id" json:"sender_user_id"` // user that sent the message (this is also the owner of the channel that contains it)
ChannelInternalName string ChannelInternalName string `db:"channel_internal_name" json:"channel_internal_name"`
ChannelID ChannelID ChannelID ChannelID `db:"channel_id" json:"channel_id"`
SenderName *string SenderName *string `db:"sender_name" json:"sender_name"`
SenderIP string SenderIP string `db:"sender_ip" json:"sender_ip"`
TimestampReal time.Time TimestampReal SCNTime `db:"timestamp_real" json:"-"`
TimestampClient *time.Time TimestampClient *SCNTime `db:"timestamp_client" json:"-"`
Title string Title string `db:"title" json:"title"`
Content *string Content *string `db:"content" json:"content"`
Priority int Priority int `db:"priority" json:"priority"`
UserMessageID *string UserMessageID *string `db:"usr_message_id" json:"usr_message_id"`
UsedKeyID KeyTokenID UsedKeyID KeyTokenID `db:"used_key_id" json:"used_key_id"`
Deleted bool Deleted bool `db:"deleted" json:"-"`
MessageExtra `db:"-"` // fields that are not in DB and are set on PreMarshal
} }
func (m Message) FullJSON() MessageJSON { type MessageExtra struct {
return MessageJSON{ Timestamp SCNTime `db:"-" json:"timestamp"`
MessageID: m.MessageID, Trimmed bool `db:"-" json:"trimmed"`
SenderUserID: m.SenderUserID,
ChannelInternalName: m.ChannelInternalName,
ChannelID: m.ChannelID,
SenderName: m.SenderName,
SenderIP: m.SenderIP,
Timestamp: m.Timestamp().Format(time.RFC3339Nano),
Title: m.Title,
Content: m.Content,
Priority: m.Priority,
UserMessageID: m.UserMessageID,
UsedKeyID: m.UsedKeyID,
Trimmed: false,
}
} }
func (m Message) TrimmedJSON() MessageJSON { func (u *Message) PreMarshal() Message {
return MessageJSON{ u.MessageExtra.Timestamp = NewSCNTime(u.Timestamp())
MessageID: m.MessageID, return *u
SenderUserID: m.SenderUserID, }
ChannelInternalName: m.ChannelInternalName,
ChannelID: m.ChannelID, func (m Message) Trim() Message {
SenderName: m.SenderName, r := m
SenderIP: m.SenderIP, if !r.Trimmed && r.NeedsTrim() {
Timestamp: m.Timestamp().Format(time.RFC3339Nano), r.Content = r.TrimmedContent()
Title: m.Title, r.MessageExtra.Trimmed = true
Content: m.TrimmedContent(),
Priority: m.Priority,
UserMessageID: m.UserMessageID,
UsedKeyID: m.UsedKeyID,
Trimmed: m.NeedsTrim(),
} }
return r.PreMarshal()
} }
func (m Message) Timestamp() time.Time { func (m Message) Timestamp() time.Time {
return langext.Coalesce(m.TimestampClient, m.TimestampReal) return langext.Coalesce(m.TimestampClient, m.TimestampReal).Time()
} }
func (m Message) NeedsTrim() bool { func (m Message) NeedsTrim() bool {
@@ -102,71 +84,3 @@ func (m Message) FormatNotificationTitle(user User, channel Channel) string {
return fmt.Sprintf("[%s] %s", channel.DisplayName, m.Title) return fmt.Sprintf("[%s] %s", channel.DisplayName, m.Title)
} }
type MessageJSON struct {
MessageID MessageID `json:"message_id"`
SenderUserID UserID `json:"sender_user_id"`
ChannelInternalName string `json:"channel_internal_name"`
ChannelID ChannelID `json:"channel_id"`
SenderName *string `json:"sender_name"`
SenderIP string `json:"sender_ip"`
Timestamp string `json:"timestamp"`
Title string `json:"title"`
Content *string `json:"content"`
Priority int `json:"priority"`
UserMessageID *string `json:"usr_message_id"`
UsedKeyID KeyTokenID `json:"used_key_id"`
Trimmed bool `json:"trimmed"`
}
type MessageDB struct {
MessageID MessageID `db:"message_id"`
SenderUserID UserID `db:"sender_user_id"`
ChannelInternalName string `db:"channel_internal_name"`
ChannelID ChannelID `db:"channel_id"`
SenderName *string `db:"sender_name"`
SenderIP string `db:"sender_ip"`
TimestampReal int64 `db:"timestamp_real"`
TimestampClient *int64 `db:"timestamp_client"`
Title string `db:"title"`
Content *string `db:"content"`
Priority int `db:"priority"`
UserMessageID *string `db:"usr_message_id"`
UsedKeyID KeyTokenID `db:"used_key_id"`
Deleted int `db:"deleted"`
}
func (m MessageDB) Model() Message {
return Message{
MessageID: m.MessageID,
SenderUserID: m.SenderUserID,
ChannelInternalName: m.ChannelInternalName,
ChannelID: m.ChannelID,
SenderName: m.SenderName,
SenderIP: m.SenderIP,
TimestampReal: timeFromMilli(m.TimestampReal),
TimestampClient: timeOptFromMilli(m.TimestampClient),
Title: m.Title,
Content: m.Content,
Priority: m.Priority,
UserMessageID: m.UserMessageID,
UsedKeyID: m.UsedKeyID,
Deleted: m.Deleted != 0,
}
}
func DecodeMessage(ctx context.Context, q sq.Queryable, r *sqlx.Rows) (Message, error) {
data, err := sq.ScanSingle[MessageDB](ctx, q, r, sq.SModeFast, sq.Safe, true)
if err != nil {
return Message{}, err
}
return data.Model(), nil
}
func DecodeMessages(ctx context.Context, q sq.Queryable, r *sqlx.Rows) ([]Message, error) {
data, err := sq.ScanAll[MessageDB](ctx, q, r, sq.SModeFast, sq.Safe, true)
if err != nil {
return nil, err
}
return langext.ArrMap(data, func(v MessageDB) Message { return v.Model() }), nil
}
+23 -184
View File
@@ -1,188 +1,27 @@
package models package models
import (
"context"
"github.com/jmoiron/sqlx"
"gogs.mikescher.com/BlackForestBytes/goext/langext"
"gogs.mikescher.com/BlackForestBytes/goext/sq"
"gogs.mikescher.com/BlackForestBytes/goext/timeext"
"time"
)
type RequestLog struct { type RequestLog struct {
RequestID RequestID RequestID RequestID `db:"request_id" json:"requestLog_id"`
Method string Method string `db:"method" json:"method"`
URI string URI string `db:"uri" json:"uri"`
UserAgent *string UserAgent *string `db:"user_agent" json:"user_agent"`
Authentication *string Authentication *string `db:"authentication" json:"authentication"`
RequestBody *string RequestBody *string `db:"request_body" json:"request_body"`
RequestBodySize int64 RequestBodySize int64 `db:"request_body_size" json:"request_body_size"`
RequestContentType string RequestContentType string `db:"request_content_type" json:"request_content_type"`
RemoteIP string RemoteIP string `db:"remote_ip" json:"remote_ip"`
KeyID *KeyTokenID KeyID *KeyTokenID `db:"key_id" json:"key_id"`
UserID *UserID UserID *UserID `db:"userid" json:"userid"`
Permissions *string Permissions *string `db:"permissions" json:"permissions"`
ResponseStatuscode *int64 ResponseStatuscode *int64 `db:"response_statuscode" json:"response_statuscode"`
ResponseBodySize *int64 ResponseBodySize *int64 `db:"response_body_size" json:"response_body_size"`
ResponseBody *string ResponseBody *string `db:"response_body" json:"response_body"`
ResponseContentType string ResponseContentType string `db:"response_content_type" json:"response_content_type"`
RetryCount int64 RetryCount int64 `db:"retry_count" json:"retry_count"`
Panicked bool Panicked bool `db:"panicked" json:"panicked"`
PanicStr *string PanicStr *string `db:"panic_str" json:"panic_str"`
ProcessingTime time.Duration ProcessingTime SCNDuration `db:"processing_time" json:"processing_time"`
TimestampCreated time.Time TimestampCreated SCNTime `db:"timestamp_created" json:"timestamp_created"`
TimestampStart time.Time TimestampStart SCNTime `db:"timestamp_start" json:"timestamp_start"`
TimestampFinish time.Time TimestampFinish SCNTime `db:"timestamp_finish" json:"timestamp_finish"`
}
func (c RequestLog) JSON() RequestLogJSON {
return RequestLogJSON{
RequestID: c.RequestID,
Method: c.Method,
URI: c.URI,
UserAgent: c.UserAgent,
Authentication: c.Authentication,
RequestBody: c.RequestBody,
RequestBodySize: c.RequestBodySize,
RequestContentType: c.RequestContentType,
RemoteIP: c.RemoteIP,
KeyID: c.KeyID,
UserID: c.UserID,
Permissions: c.Permissions,
ResponseStatuscode: c.ResponseStatuscode,
ResponseBodySize: c.ResponseBodySize,
ResponseBody: c.ResponseBody,
ResponseContentType: c.ResponseContentType,
RetryCount: c.RetryCount,
Panicked: c.Panicked,
PanicStr: c.PanicStr,
ProcessingTime: c.ProcessingTime.Seconds(),
TimestampCreated: c.TimestampCreated.Format(time.RFC3339Nano),
TimestampStart: c.TimestampStart.Format(time.RFC3339Nano),
TimestampFinish: c.TimestampFinish.Format(time.RFC3339Nano),
}
}
func (c RequestLog) DB() RequestLogDB {
return RequestLogDB{
RequestID: c.RequestID,
Method: c.Method,
URI: c.URI,
UserAgent: c.UserAgent,
Authentication: c.Authentication,
RequestBody: c.RequestBody,
RequestBodySize: c.RequestBodySize,
RequestContentType: c.RequestContentType,
RemoteIP: c.RemoteIP,
KeyID: c.KeyID,
UserID: c.UserID,
Permissions: c.Permissions,
ResponseStatuscode: c.ResponseStatuscode,
ResponseBodySize: c.ResponseBodySize,
ResponseBody: c.ResponseBody,
ResponseContentType: c.ResponseContentType,
RetryCount: c.RetryCount,
Panicked: langext.Conditional[int64](c.Panicked, 1, 0),
PanicStr: c.PanicStr,
ProcessingTime: c.ProcessingTime.Milliseconds(),
TimestampCreated: c.TimestampCreated.UnixMilli(),
TimestampStart: c.TimestampStart.UnixMilli(),
TimestampFinish: c.TimestampFinish.UnixMilli(),
}
}
type RequestLogJSON struct {
RequestID RequestID `json:"requestLog_id"`
Method string `json:"method"`
URI string `json:"uri"`
UserAgent *string `json:"user_agent"`
Authentication *string `json:"authentication"`
RequestBody *string `json:"request_body"`
RequestBodySize int64 `json:"request_body_size"`
RequestContentType string `json:"request_content_type"`
RemoteIP string `json:"remote_ip"`
KeyID *KeyTokenID `json:"key_id"`
UserID *UserID `json:"userid"`
Permissions *string `json:"permissions"`
ResponseStatuscode *int64 `json:"response_statuscode"`
ResponseBodySize *int64 `json:"response_body_size"`
ResponseBody *string `json:"response_body"`
ResponseContentType string `json:"response_content_type"`
RetryCount int64 `json:"retry_count"`
Panicked bool `json:"panicked"`
PanicStr *string `json:"panic_str"`
ProcessingTime float64 `json:"processing_time"`
TimestampCreated string `json:"timestamp_created"`
TimestampStart string `json:"timestamp_start"`
TimestampFinish string `json:"timestamp_finish"`
}
type RequestLogDB struct {
RequestID RequestID `db:"request_id"`
Method string `db:"method"`
URI string `db:"uri"`
UserAgent *string `db:"user_agent"`
Authentication *string `db:"authentication"`
RequestBody *string `db:"request_body"`
RequestBodySize int64 `db:"request_body_size"`
RequestContentType string `db:"request_content_type"`
RemoteIP string `db:"remote_ip"`
KeyID *KeyTokenID `db:"key_id"`
UserID *UserID `db:"userid"`
Permissions *string `db:"permissions"`
ResponseStatuscode *int64 `db:"response_statuscode"`
ResponseBodySize *int64 `db:"response_body_size"`
ResponseBody *string `db:"response_body"`
ResponseContentType string `db:"response_content_type"`
RetryCount int64 `db:"retry_count"`
Panicked int64 `db:"panicked"`
PanicStr *string `db:"panic_str"`
ProcessingTime int64 `db:"processing_time"`
TimestampCreated int64 `db:"timestamp_created"`
TimestampStart int64 `db:"timestamp_start"`
TimestampFinish int64 `db:"timestamp_finish"`
}
func (c RequestLogDB) Model() RequestLog {
return RequestLog{
RequestID: c.RequestID,
Method: c.Method,
URI: c.URI,
UserAgent: c.UserAgent,
Authentication: c.Authentication,
RequestBody: c.RequestBody,
RequestBodySize: c.RequestBodySize,
RequestContentType: c.RequestContentType,
RemoteIP: c.RemoteIP,
KeyID: c.KeyID,
UserID: c.UserID,
Permissions: c.Permissions,
ResponseStatuscode: c.ResponseStatuscode,
ResponseBodySize: c.ResponseBodySize,
ResponseBody: c.ResponseBody,
ResponseContentType: c.ResponseContentType,
RetryCount: c.RetryCount,
Panicked: c.Panicked != 0,
PanicStr: c.PanicStr,
ProcessingTime: timeext.FromMilliseconds(c.ProcessingTime),
TimestampCreated: timeFromMilli(c.TimestampCreated),
TimestampStart: timeFromMilli(c.TimestampStart),
TimestampFinish: timeFromMilli(c.TimestampFinish),
}
}
func DecodeRequestLog(ctx context.Context, q sq.Queryable, r *sqlx.Rows) (RequestLog, error) {
data, err := sq.ScanSingle[RequestLogDB](ctx, q, r, sq.SModeFast, sq.Safe, true)
if err != nil {
return RequestLog{}, err
}
return data.Model(), nil
}
func DecodeRequestLogs(ctx context.Context, q sq.Queryable, r *sqlx.Rows) ([]RequestLog, error) {
data, err := sq.ScanAll[RequestLogDB](ctx, q, r, sq.SModeFast, sq.Safe, true)
if err != nil {
return nil, err
}
return langext.ArrMap(data, func(v RequestLogDB) RequestLog { return v.Model() }), nil
} }
+7 -75
View File
@@ -1,13 +1,5 @@
package models package models
import (
"context"
"github.com/jmoiron/sqlx"
"gogs.mikescher.com/BlackForestBytes/goext/langext"
"gogs.mikescher.com/BlackForestBytes/goext/sq"
"time"
)
// [!] subscriptions are read-access to channels, // [!] subscriptions are read-access to channels,
// //
// The set of subscriptions specifies which messages the ListMessages() API call returns // The set of subscriptions specifies which messages the ListMessages() API call returns
@@ -16,71 +8,11 @@ import (
// (use keytokens for write-access) // (use keytokens for write-access)
type Subscription struct { type Subscription struct {
SubscriptionID SubscriptionID SubscriptionID SubscriptionID `db:"subscription_id" json:"subscription_id"`
SubscriberUserID UserID SubscriberUserID UserID `db:"subscriber_user_id" json:"subscriber_user_id"`
ChannelOwnerUserID UserID ChannelOwnerUserID UserID `db:"channel_owner_user_id" json:"channel_owner_user_id"`
ChannelID ChannelID ChannelID ChannelID `db:"channel_id" json:"channel_id"`
ChannelInternalName string ChannelInternalName string `db:"channel_internal_name" json:"channel_internal_name"`
TimestampCreated time.Time TimestampCreated SCNTime `db:"timestamp_created" json:"timestamp_created"`
Confirmed bool Confirmed bool `db:"confirmed" json:"confirmed"`
}
func (s Subscription) JSON() SubscriptionJSON {
return SubscriptionJSON{
SubscriptionID: s.SubscriptionID,
SubscriberUserID: s.SubscriberUserID,
ChannelOwnerUserID: s.ChannelOwnerUserID,
ChannelID: s.ChannelID,
ChannelInternalName: s.ChannelInternalName,
TimestampCreated: s.TimestampCreated.Format(time.RFC3339Nano),
Confirmed: s.Confirmed,
}
}
type SubscriptionJSON struct {
SubscriptionID SubscriptionID `json:"subscription_id"`
SubscriberUserID UserID `json:"subscriber_user_id"`
ChannelOwnerUserID UserID `json:"channel_owner_user_id"`
ChannelID ChannelID `json:"channel_id"`
ChannelInternalName string `json:"channel_internal_name"`
TimestampCreated string `json:"timestamp_created"`
Confirmed bool `json:"confirmed"`
}
type SubscriptionDB struct {
SubscriptionID SubscriptionID `db:"subscription_id"`
SubscriberUserID UserID `db:"subscriber_user_id"`
ChannelOwnerUserID UserID `db:"channel_owner_user_id"`
ChannelID ChannelID `db:"channel_id"`
ChannelInternalName string `db:"channel_internal_name"`
TimestampCreated int64 `db:"timestamp_created"`
Confirmed int `db:"confirmed"`
}
func (s SubscriptionDB) Model() Subscription {
return Subscription{
SubscriptionID: s.SubscriptionID,
SubscriberUserID: s.SubscriberUserID,
ChannelOwnerUserID: s.ChannelOwnerUserID,
ChannelID: s.ChannelID,
ChannelInternalName: s.ChannelInternalName,
TimestampCreated: timeFromMilli(s.TimestampCreated),
Confirmed: s.Confirmed != 0,
}
}
func DecodeSubscription(ctx context.Context, q sq.Queryable, r *sqlx.Rows) (Subscription, error) {
data, err := sq.ScanSingle[SubscriptionDB](ctx, q, r, sq.SModeFast, sq.Safe, true)
if err != nil {
return Subscription{}, err
}
return data.Model(), nil
}
func DecodeSubscriptions(ctx context.Context, q sq.Queryable, r *sqlx.Rows) ([]Subscription, error) {
data, err := sq.ScanAll[SubscriptionDB](ctx, q, r, sq.SModeFast, sq.Safe, true)
if err != nil {
return nil, err
}
return langext.ArrMap(data, func(v SubscriptionDB) Subscription { return v.Model() }), nil
} }
+65
View File
@@ -0,0 +1,65 @@
package models
import (
"encoding/json"
"gogs.mikescher.com/BlackForestBytes/goext/langext"
"gogs.mikescher.com/BlackForestBytes/goext/rfctime"
"time"
)
type SCNTime time.Time
func (t SCNTime) MarshalToDB(v SCNTime) (int64, error) {
return v.Time().UnixMilli(), nil
}
func (t SCNTime) UnmarshalToModel(v int64) (SCNTime, error) {
return NewSCNTime(time.UnixMilli(v)), nil
}
func (t SCNTime) Time() time.Time {
return time.Time(t)
}
func (t *SCNTime) UnmarshalJSON(data []byte) error {
str := ""
if err := json.Unmarshal(data, &str); err != nil {
return err
}
t0, err := time.Parse(time.RFC3339Nano, str)
if err != nil {
return err
}
*t = SCNTime(t0)
return nil
}
func (t SCNTime) MarshalJSON() ([]byte, error) {
str := t.Time().Format(time.RFC3339Nano)
return json.Marshal(str)
}
func NewSCNTime(t time.Time) SCNTime {
return SCNTime(t)
}
func NewSCNTimePtr(t *time.Time) *SCNTime {
if t == nil {
return nil
}
return langext.Ptr(SCNTime(*t))
}
func NowSCNTime() SCNTime {
return SCNTime(time.Now())
}
func tt(v rfctime.AnyTime) time.Time {
if r, ok := v.(time.Time); ok {
return r
}
if r, ok := v.(rfctime.RFCTime); ok {
return r.Time()
}
return time.Unix(0, v.UnixNano()).In(v.Location())
}
+53 -114
View File
@@ -2,38 +2,63 @@ package models
import ( import (
scn "blackforestbytes.com/simplecloudnotifier" scn "blackforestbytes.com/simplecloudnotifier"
"context"
"github.com/jmoiron/sqlx"
"gogs.mikescher.com/BlackForestBytes/goext/langext"
"gogs.mikescher.com/BlackForestBytes/goext/sq"
"time"
) )
type User struct { type User struct {
UserID UserID UserID UserID `db:"user_id" json:"user_id"`
Username *string Username *string `db:"username" json:"username"`
TimestampCreated time.Time TimestampCreated SCNTime `db:"timestamp_created" json:"timestamp_created"`
TimestampLastRead *time.Time TimestampLastRead *SCNTime `db:"timestamp_lastread" json:"timestamp_lastread"`
TimestampLastSent *time.Time TimestampLastSent *SCNTime `db:"timestamp_lastsent" json:"timestamp_lastsent"`
MessagesSent int MessagesSent int `db:"messages_sent" json:"messages_sent"`
QuotaUsed int QuotaUsed int `db:"quota_used" json:"quota_used"`
QuotaUsedDay *string QuotaUsedDay *string `db:"quota_used_day" json:"-"`
IsPro bool IsPro bool `db:"is_pro" json:"is_pro"`
ProToken *string ProToken *string `db:"pro_token" json:"-"`
UserExtra `db:"-"` // fields that are not in DB and are set on PreMarshal
} }
func (u User) JSON() UserJSON { type UserExtra struct {
return UserJSON{ QuotaRemaining int `json:"quota_remaining"`
UserID: u.UserID, QuotaPerDay int `json:"quota_max"`
Username: u.Username, DefaultChannel string `json:"default_channel"`
TimestampCreated: u.TimestampCreated.Format(time.RFC3339Nano), MaxBodySize int `json:"max_body_size"`
TimestampLastRead: timeOptFmt(u.TimestampLastRead, time.RFC3339Nano), MaxTitleLength int `json:"max_title_length"`
TimestampLastSent: timeOptFmt(u.TimestampLastSent, time.RFC3339Nano), DefaultPriority int `json:"default_priority"`
MessagesSent: u.MessagesSent, MaxChannelNameLength int `json:"max_channel_name_length"`
QuotaUsed: u.QuotaUsedToday(), MaxChannelDescriptionLength int `json:"max_channel_description_length"`
MaxSenderNameLength int `json:"max_sender_name_length"`
MaxUserMessageIDLength int `json:"max_user_message_id_length"`
}
type UserPreview struct {
UserID UserID `json:"user_id"`
Username *string `json:"username"`
}
type UserWithClientsAndKeys struct {
User
Clients []Client `json:"clients"`
SendKey string `json:"send_key"`
ReadKey string `json:"read_key"`
AdminKey string `json:"admin_key"`
}
func (u User) WithClients(clients []Client, ak string, sk string, rk string) UserWithClientsAndKeys {
return UserWithClientsAndKeys{
User: u.PreMarshal(),
Clients: clients,
SendKey: sk,
ReadKey: rk,
AdminKey: ak,
}
}
func (u *User) PreMarshal() User {
u.UserExtra = UserExtra{
QuotaPerDay: u.QuotaPerDay(), QuotaPerDay: u.QuotaPerDay(),
QuotaRemaining: u.QuotaRemainingToday(), QuotaRemaining: u.QuotaRemainingToday(),
IsPro: u.IsPro,
DefaultChannel: u.DefaultChannel(), DefaultChannel: u.DefaultChannel(),
MaxBodySize: u.MaxContentLength(), MaxBodySize: u.MaxContentLength(),
MaxTitleLength: u.MaxTitleLength(), MaxTitleLength: u.MaxTitleLength(),
@@ -43,16 +68,7 @@ func (u User) JSON() UserJSON {
MaxSenderNameLength: u.MaxSenderNameLength(), MaxSenderNameLength: u.MaxSenderNameLength(),
MaxUserMessageIDLength: u.MaxUserMessageIDLength(), MaxUserMessageIDLength: u.MaxUserMessageIDLength(),
} }
} return *u
func (u User) JSONWithClients(clients []Client, ak string, sk string, rk string) UserJSONWithClientsAndKeys {
return UserJSONWithClientsAndKeys{
UserJSON: u.JSON(),
Clients: langext.ArrMap(clients, func(v Client) ClientJSON { return v.JSON() }),
SendKey: sk,
ReadKey: rk,
AdminKey: ak,
}
} }
func (u User) MaxContentLength() int { func (u User) MaxContentLength() int {
@@ -116,86 +132,9 @@ func (u User) MaxTimestampDiffHours() int {
return 24 return 24
} }
func (u User) JSONPreview() UserPreviewJSON { func (u User) JSONPreview() UserPreview {
return UserPreviewJSON{ return UserPreview{
UserID: u.UserID, UserID: u.UserID,
Username: u.Username, Username: u.Username,
} }
} }
type UserJSON struct {
UserID UserID `json:"user_id"`
Username *string `json:"username"`
TimestampCreated string `json:"timestamp_created"`
TimestampLastRead *string `json:"timestamp_lastread"`
TimestampLastSent *string `json:"timestamp_lastsent"`
MessagesSent int `json:"messages_sent"`
QuotaUsed int `json:"quota_used"`
QuotaRemaining int `json:"quota_remaining"`
QuotaPerDay int `json:"quota_max"`
IsPro bool `json:"is_pro"`
DefaultChannel string `json:"default_channel"`
MaxBodySize int `json:"max_body_size"`
MaxTitleLength int `json:"max_title_length"`
DefaultPriority int `json:"default_priority"`
MaxChannelNameLength int `json:"max_channel_name_length"`
MaxChannelDescriptionLength int `json:"max_channel_description_length"`
MaxSenderNameLength int `json:"max_sender_name_length"`
MaxUserMessageIDLength int `json:"max_user_message_id_length"`
}
type UserPreviewJSON struct {
UserID UserID `json:"user_id"`
Username *string `json:"username"`
}
type UserJSONWithClientsAndKeys struct {
UserJSON
Clients []ClientJSON `json:"clients"`
SendKey string `json:"send_key"`
ReadKey string `json:"read_key"`
AdminKey string `json:"admin_key"`
}
type UserDB struct {
UserID UserID `db:"user_id"`
Username *string `db:"username"`
TimestampCreated int64 `db:"timestamp_created"`
TimestampLastRead *int64 `db:"timestamp_lastread"`
TimestampLastSent *int64 `db:"timestamp_lastsent"`
MessagesSent int `db:"messages_sent"`
QuotaUsed int `db:"quota_used"`
QuotaUsedDay *string `db:"quota_used_day"`
IsPro bool `db:"is_pro"`
ProToken *string `db:"pro_token"`
}
func (u UserDB) Model() User {
return User{
UserID: u.UserID,
Username: u.Username,
TimestampCreated: timeFromMilli(u.TimestampCreated),
TimestampLastRead: timeOptFromMilli(u.TimestampLastRead),
TimestampLastSent: timeOptFromMilli(u.TimestampLastSent),
MessagesSent: u.MessagesSent,
QuotaUsed: u.QuotaUsed,
QuotaUsedDay: u.QuotaUsedDay,
IsPro: u.IsPro,
}
}
func DecodeUser(ctx context.Context, q sq.Queryable, r *sqlx.Rows) (User, error) {
data, err := sq.ScanSingle[UserDB](ctx, q, r, sq.SModeFast, sq.Safe, true)
if err != nil {
return User{}, err
}
return data.Model(), nil
}
func DecodeUsers(ctx context.Context, q sq.Queryable, r *sqlx.Rows) ([]User, error) {
data, err := sq.ScanAll[UserDB](ctx, q, r, sq.SModeFast, sq.Safe, true)
if err != nil {
return nil, err
}
return langext.ArrMap(data, func(v UserDB) User { return v.Model() }), nil
}
+8
View File
@@ -2,6 +2,7 @@ package models
import ( import (
"gogs.mikescher.com/BlackForestBytes/goext/langext" "gogs.mikescher.com/BlackForestBytes/goext/langext"
"gogs.mikescher.com/BlackForestBytes/goext/sq"
"time" "time"
) )
@@ -23,3 +24,10 @@ func timeOptFromMilli(millis *int64) *time.Time {
func timeFromMilli(millis int64) time.Time { func timeFromMilli(millis int64) time.Time {
return time.UnixMilli(millis) return time.UnixMilli(millis)
} }
func RegisterConverter(db sq.DB) {
db.RegisterConverter(sq.NewAutoDBTypeConverter(SCNTime{}))
db.RegisterConverter(sq.NewAutoDBTypeConverter(SCNDuration(0)))
db.RegisterConverter(sq.NewAutoDBTypeConverter(TokenPermissionList{}))
db.RegisterConverter(sq.NewAutoDBTypeConverter(ChannelIDArr{}))
}
+9 -7
View File
@@ -1,10 +1,10 @@
package swagger package swagger
import ( import (
"blackforestbytes.com/simplecloudnotifier/api/ginresp"
"embed" "embed"
_ "embed" _ "embed"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"gogs.mikescher.com/BlackForestBytes/goext/ginext"
"net/http" "net/http"
"strings" "strings"
) )
@@ -46,26 +46,28 @@ func getAsset(fn string) ([]byte, string, bool) {
return data, mime, true return data, mime, true
} }
func Handle(g *gin.Context) ginresp.HTTPResponse { func Handle(pctx ginext.PreContext) ginext.HTTPResponse {
type uri struct { type uri struct {
Filename string `uri:"sub"` Filename string `uri:"sub"`
} }
var u uri var u uri
if err := g.ShouldBindUri(&u); err != nil { ctx, _, errResp := pctx.URI(&u).Start()
return ginresp.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) if errResp != nil {
return *errResp
} }
defer ctx.Cancel()
u.Filename = strings.TrimLeft(u.Filename, "/") u.Filename = strings.TrimLeft(u.Filename, "/")
if u.Filename == "" { if u.Filename == "" {
index, _, _ := getAsset("index.html") index, _, _ := getAsset("index.html")
return ginresp.Data(http.StatusOK, "text/html", index) return ginext.Data(http.StatusOK, "text/html", index)
} }
if data, mime, ok := getAsset(u.Filename); ok { if data, mime, ok := getAsset(u.Filename); ok {
return ginresp.Data(http.StatusOK, mime, data) return ginext.Data(http.StatusOK, mime, data)
} }
return ginresp.JSON(http.StatusNotFound, gin.H{"error": "AssetNotFound", "filename": u.Filename}) return ginext.JSON(http.StatusNotFound, gin.H{"error": "AssetNotFound", "filename": u.Filename})
} }
+146 -318
View File
@@ -19,63 +19,39 @@
"parameters": [ "parameters": [
{ {
"type": "string", "type": "string",
"example": "test",
"name": "channel",
"in": "query"
},
{
"type": "string",
"example": "This is a message",
"name": "content", "name": "content",
"in": "query" "in": "query"
}, },
{ {
"type": "string", "type": "string",
"example": "P3TNH8mvv14fm",
"name": "key",
"in": "query"
},
{
"type": "string",
"example": "db8b0e6a-a08c-4646",
"name": "msg_id", "name": "msg_id",
"in": "query" "in": "query"
}, },
{ {
"enum": [
0,
1,
2
],
"type": "integer", "type": "integer",
"example": 1,
"name": "priority", "name": "priority",
"in": "query" "in": "query"
}, },
{
"type": "string",
"example": "example-server",
"name": "sender_name",
"in": "query"
},
{ {
"type": "number", "type": "number",
"example": 1669824037,
"name": "timestamp", "name": "timestamp",
"in": "query" "in": "query"
}, },
{ {
"type": "string", "type": "string",
"example": "Hello World",
"name": "title", "name": "title",
"in": "query" "in": "query"
}, },
{ {
"type": "string", "type": "integer",
"example": "7725",
"name": "user_id", "name": "user_id",
"in": "query" "in": "query"
}, },
{
"type": "string",
"name": "user_key",
"in": "query"
},
{ {
"description": " ", "description": " ",
"name": "post_body", "name": "post_body",
@@ -86,62 +62,38 @@
}, },
{ {
"type": "string", "type": "string",
"example": "test",
"name": "channel",
"in": "formData"
},
{
"type": "string",
"example": "This is a message",
"name": "content", "name": "content",
"in": "formData" "in": "formData"
}, },
{ {
"type": "string", "type": "string",
"example": "P3TNH8mvv14fm",
"name": "key",
"in": "formData"
},
{
"type": "string",
"example": "db8b0e6a-a08c-4646",
"name": "msg_id", "name": "msg_id",
"in": "formData" "in": "formData"
}, },
{ {
"enum": [
0,
1,
2
],
"type": "integer", "type": "integer",
"example": 1,
"name": "priority", "name": "priority",
"in": "formData" "in": "formData"
}, },
{
"type": "string",
"example": "example-server",
"name": "sender_name",
"in": "formData"
},
{ {
"type": "number", "type": "number",
"example": 1669824037,
"name": "timestamp", "name": "timestamp",
"in": "formData" "in": "formData"
}, },
{ {
"type": "string", "type": "string",
"example": "Hello World",
"name": "title", "name": "title",
"in": "formData" "in": "formData"
}, },
{ {
"type": "string", "type": "integer",
"example": "7725",
"name": "user_id", "name": "user_id",
"in": "formData" "in": "formData"
},
{
"type": "string",
"name": "user_key",
"in": "formData"
} }
], ],
"responses": { "responses": {
@@ -978,7 +930,7 @@
"200": { "200": {
"description": "OK", "description": "OK",
"schema": { "schema": {
"$ref": "#/definitions/models.MessageJSON" "$ref": "#/definitions/models.Message"
} }
}, },
"400": { "400": {
@@ -1027,7 +979,7 @@
"200": { "200": {
"description": "OK", "description": "OK",
"schema": { "schema": {
"$ref": "#/definitions/models.MessageJSON" "$ref": "#/definitions/models.Message"
} }
}, },
"400": { "400": {
@@ -1077,7 +1029,7 @@
"200": { "200": {
"description": "OK", "description": "OK",
"schema": { "schema": {
"$ref": "#/definitions/models.ChannelPreviewJSON" "$ref": "#/definitions/models.ChannelPreview"
} }
}, },
"400": { "400": {
@@ -1127,7 +1079,7 @@
"200": { "200": {
"description": "OK", "description": "OK",
"schema": { "schema": {
"$ref": "#/definitions/models.KeyTokenPreviewJSON" "$ref": "#/definitions/models.KeyTokenPreview"
} }
}, },
"400": { "400": {
@@ -1177,7 +1129,7 @@
"200": { "200": {
"description": "OK", "description": "OK",
"schema": { "schema": {
"$ref": "#/definitions/models.UserPreviewJSON" "$ref": "#/definitions/models.UserPreview"
} }
}, },
"400": { "400": {
@@ -1228,7 +1180,7 @@
"200": { "200": {
"description": "OK", "description": "OK",
"schema": { "schema": {
"$ref": "#/definitions/models.UserJSONWithClientsAndKeys" "$ref": "#/definitions/models.UserWithClientsAndKeys"
} }
}, },
"400": { "400": {
@@ -1266,7 +1218,7 @@
"200": { "200": {
"description": "OK", "description": "OK",
"schema": { "schema": {
"$ref": "#/definitions/models.UserJSON" "$ref": "#/definitions/models.User"
} }
}, },
"400": { "400": {
@@ -1331,7 +1283,7 @@
"200": { "200": {
"description": "OK", "description": "OK",
"schema": { "schema": {
"$ref": "#/definitions/models.UserJSON" "$ref": "#/definitions/models.User"
} }
}, },
"400": { "400": {
@@ -1445,7 +1397,7 @@
"200": { "200": {
"description": "OK", "description": "OK",
"schema": { "schema": {
"$ref": "#/definitions/models.ChannelWithSubscriptionJSON" "$ref": "#/definitions/models.ChannelWithSubscription"
} }
}, },
"400": { "400": {
@@ -1502,7 +1454,7 @@
"200": { "200": {
"description": "OK", "description": "OK",
"schema": { "schema": {
"$ref": "#/definitions/models.ChannelWithSubscriptionJSON" "$ref": "#/definitions/models.ChannelWithSubscription"
} }
}, },
"400": { "400": {
@@ -1581,7 +1533,7 @@
"200": { "200": {
"description": "OK", "description": "OK",
"schema": { "schema": {
"$ref": "#/definitions/models.ChannelWithSubscriptionJSON" "$ref": "#/definitions/models.ChannelWithSubscription"
} }
}, },
"400": { "400": {
@@ -1816,7 +1768,7 @@
"200": { "200": {
"description": "OK", "description": "OK",
"schema": { "schema": {
"$ref": "#/definitions/models.ClientJSON" "$ref": "#/definitions/models.Client"
} }
}, },
"400": { "400": {
@@ -1867,7 +1819,7 @@
"200": { "200": {
"description": "OK", "description": "OK",
"schema": { "schema": {
"$ref": "#/definitions/models.ClientJSON" "$ref": "#/definitions/models.Client"
} }
}, },
"400": { "400": {
@@ -1922,7 +1874,7 @@
"200": { "200": {
"description": "OK", "description": "OK",
"schema": { "schema": {
"$ref": "#/definitions/models.ClientJSON" "$ref": "#/definitions/models.Client"
} }
}, },
"400": { "400": {
@@ -1994,7 +1946,7 @@
"200": { "200": {
"description": "OK", "description": "OK",
"schema": { "schema": {
"$ref": "#/definitions/models.ClientJSON" "$ref": "#/definitions/models.Client"
} }
}, },
"400": { "400": {
@@ -2101,7 +2053,7 @@
"200": { "200": {
"description": "OK", "description": "OK",
"schema": { "schema": {
"$ref": "#/definitions/models.KeyTokenJSON" "$ref": "#/definitions/models.KeyToken"
} }
}, },
"400": { "400": {
@@ -2159,7 +2111,7 @@
"200": { "200": {
"description": "OK", "description": "OK",
"schema": { "schema": {
"$ref": "#/definitions/models.KeyTokenWithTokenJSON" "$ref": "#/definitions/models.KeyToken"
} }
}, },
"400": { "400": {
@@ -2217,7 +2169,7 @@
"200": { "200": {
"description": "OK", "description": "OK",
"schema": { "schema": {
"$ref": "#/definitions/models.KeyTokenJSON" "$ref": "#/definitions/models.KeyToken"
} }
}, },
"400": { "400": {
@@ -2273,7 +2225,7 @@
"200": { "200": {
"description": "OK", "description": "OK",
"schema": { "schema": {
"$ref": "#/definitions/models.KeyTokenJSON" "$ref": "#/definitions/models.KeyToken"
} }
}, },
"400": { "400": {
@@ -2336,7 +2288,7 @@
"200": { "200": {
"description": "OK", "description": "OK",
"schema": { "schema": {
"$ref": "#/definitions/models.KeyTokenJSON" "$ref": "#/definitions/models.KeyToken"
} }
}, },
"400": { "400": {
@@ -2458,7 +2410,7 @@
"200": { "200": {
"description": "OK", "description": "OK",
"schema": { "schema": {
"$ref": "#/definitions/models.SubscriptionJSON" "$ref": "#/definitions/models.Subscription"
} }
}, },
"400": { "400": {
@@ -2509,7 +2461,7 @@
"200": { "200": {
"description": "OK", "description": "OK",
"schema": { "schema": {
"$ref": "#/definitions/models.SubscriptionJSON" "$ref": "#/definitions/models.Subscription"
} }
}, },
"400": { "400": {
@@ -2564,7 +2516,7 @@
"200": { "200": {
"description": "OK", "description": "OK",
"schema": { "schema": {
"$ref": "#/definitions/models.SubscriptionJSON" "$ref": "#/definitions/models.Subscription"
} }
}, },
"400": { "400": {
@@ -2627,7 +2579,7 @@
"200": { "200": {
"description": "OK", "description": "OK",
"schema": { "schema": {
"$ref": "#/definitions/models.SubscriptionJSON" "$ref": "#/definitions/models.Subscription"
} }
}, },
"400": { "400": {
@@ -2765,63 +2717,39 @@
"parameters": [ "parameters": [
{ {
"type": "string", "type": "string",
"example": "test",
"name": "channel",
"in": "query"
},
{
"type": "string",
"example": "This is a message",
"name": "content", "name": "content",
"in": "query" "in": "query"
}, },
{ {
"type": "string", "type": "string",
"example": "P3TNH8mvv14fm",
"name": "key",
"in": "query"
},
{
"type": "string",
"example": "db8b0e6a-a08c-4646",
"name": "msg_id", "name": "msg_id",
"in": "query" "in": "query"
}, },
{ {
"enum": [
0,
1,
2
],
"type": "integer", "type": "integer",
"example": 1,
"name": "priority", "name": "priority",
"in": "query" "in": "query"
}, },
{
"type": "string",
"example": "example-server",
"name": "sender_name",
"in": "query"
},
{ {
"type": "number", "type": "number",
"example": 1669824037,
"name": "timestamp", "name": "timestamp",
"in": "query" "in": "query"
}, },
{ {
"type": "string", "type": "string",
"example": "Hello World",
"name": "title", "name": "title",
"in": "query" "in": "query"
}, },
{ {
"type": "string", "type": "integer",
"example": "7725",
"name": "user_id", "name": "user_id",
"in": "query" "in": "query"
}, },
{
"type": "string",
"name": "user_key",
"in": "query"
},
{ {
"description": " ", "description": " ",
"name": "post_body", "name": "post_body",
@@ -2832,62 +2760,38 @@
}, },
{ {
"type": "string", "type": "string",
"example": "test",
"name": "channel",
"in": "formData"
},
{
"type": "string",
"example": "This is a message",
"name": "content", "name": "content",
"in": "formData" "in": "formData"
}, },
{ {
"type": "string", "type": "string",
"example": "P3TNH8mvv14fm",
"name": "key",
"in": "formData"
},
{
"type": "string",
"example": "db8b0e6a-a08c-4646",
"name": "msg_id", "name": "msg_id",
"in": "formData" "in": "formData"
}, },
{ {
"enum": [
0,
1,
2
],
"type": "integer", "type": "integer",
"example": 1,
"name": "priority", "name": "priority",
"in": "formData" "in": "formData"
}, },
{
"type": "string",
"example": "example-server",
"name": "sender_name",
"in": "formData"
},
{ {
"type": "number", "type": "number",
"example": 1669824037,
"name": "timestamp", "name": "timestamp",
"in": "formData" "in": "formData"
}, },
{ {
"type": "string", "type": "string",
"example": "Hello World",
"name": "title", "name": "title",
"in": "formData" "in": "formData"
}, },
{ {
"type": "string", "type": "integer",
"example": "7725",
"name": "user_id", "name": "user_id",
"in": "formData" "in": "formData"
},
{
"type": "string",
"name": "user_key",
"in": "formData"
} }
], ],
"responses": { "responses": {
@@ -2935,121 +2839,73 @@
"parameters": [ "parameters": [
{ {
"type": "string", "type": "string",
"example": "test",
"name": "channel",
"in": "query"
},
{
"type": "string",
"example": "This is a message",
"name": "content", "name": "content",
"in": "query" "in": "query"
}, },
{ {
"type": "string", "type": "string",
"example": "P3TNH8mvv14fm",
"name": "key",
"in": "query"
},
{
"type": "string",
"example": "db8b0e6a-a08c-4646",
"name": "msg_id", "name": "msg_id",
"in": "query" "in": "query"
}, },
{ {
"enum": [
0,
1,
2
],
"type": "integer", "type": "integer",
"example": 1,
"name": "priority", "name": "priority",
"in": "query" "in": "query"
}, },
{
"type": "string",
"example": "example-server",
"name": "sender_name",
"in": "query"
},
{ {
"type": "number", "type": "number",
"example": 1669824037,
"name": "timestamp", "name": "timestamp",
"in": "query" "in": "query"
}, },
{ {
"type": "string", "type": "string",
"example": "Hello World",
"name": "title", "name": "title",
"in": "query" "in": "query"
}, },
{ {
"type": "string", "type": "integer",
"example": "7725",
"name": "user_id", "name": "user_id",
"in": "query" "in": "query"
}, },
{ {
"type": "string", "type": "string",
"example": "test", "name": "user_key",
"name": "channel", "in": "query"
"in": "formData"
}, },
{ {
"type": "string", "type": "string",
"example": "This is a message",
"name": "content", "name": "content",
"in": "formData" "in": "formData"
}, },
{ {
"type": "string", "type": "string",
"example": "P3TNH8mvv14fm",
"name": "key",
"in": "formData"
},
{
"type": "string",
"example": "db8b0e6a-a08c-4646",
"name": "msg_id", "name": "msg_id",
"in": "formData" "in": "formData"
}, },
{ {
"enum": [
0,
1,
2
],
"type": "integer", "type": "integer",
"example": 1,
"name": "priority", "name": "priority",
"in": "formData" "in": "formData"
}, },
{
"type": "string",
"example": "example-server",
"name": "sender_name",
"in": "formData"
},
{ {
"type": "number", "type": "number",
"example": 1669824037,
"name": "timestamp", "name": "timestamp",
"in": "formData" "in": "formData"
}, },
{ {
"type": "string", "type": "string",
"example": "Hello World",
"name": "title", "name": "title",
"in": "formData" "in": "formData"
}, },
{ {
"type": "string", "type": "integer",
"example": "7725",
"name": "user_id", "name": "user_id",
"in": "formData" "in": "formData"
},
{
"type": "string",
"name": "user_key",
"in": "formData"
} }
], ],
"responses": { "responses": {
@@ -3103,6 +2959,7 @@
1151, 1151,
1152, 1152,
1153, 1153,
1152,
1161, 1161,
1171, 1171,
1201, 1201,
@@ -3148,6 +3005,7 @@
"BINDFAIL_QUERY_PARAM", "BINDFAIL_QUERY_PARAM",
"BINDFAIL_BODY_PARAM", "BINDFAIL_BODY_PARAM",
"BINDFAIL_URI_PARAM", "BINDFAIL_URI_PARAM",
"BINDFAIL_HEADER_PARAM",
"INVALID_BODY_PARAM", "INVALID_BODY_PARAM",
"INVALID_ENUM_VALUE", "INVALID_ENUM_VALUE",
"NO_TITLE", "NO_TITLE",
@@ -3413,7 +3271,7 @@
"messages": { "messages": {
"type": "array", "type": "array",
"items": { "items": {
"$ref": "#/definitions/models.MessageJSON" "$ref": "#/definitions/models.Message"
} }
}, },
"next_page_token": { "next_page_token": {
@@ -3430,7 +3288,7 @@
"subscriptions": { "subscriptions": {
"type": "array", "type": "array",
"items": { "items": {
"$ref": "#/definitions/models.SubscriptionJSON" "$ref": "#/definitions/models.Subscription"
} }
} }
} }
@@ -3441,7 +3299,7 @@
"channels": { "channels": {
"type": "array", "type": "array",
"items": { "items": {
"$ref": "#/definitions/models.ChannelWithSubscriptionJSON" "$ref": "#/definitions/models.ChannelWithSubscription"
} }
} }
} }
@@ -3452,7 +3310,7 @@
"clients": { "clients": {
"type": "array", "type": "array",
"items": { "items": {
"$ref": "#/definitions/models.ClientJSON" "$ref": "#/definitions/models.Client"
} }
} }
} }
@@ -3463,7 +3321,7 @@
"messages": { "messages": {
"type": "array", "type": "array",
"items": { "items": {
"$ref": "#/definitions/models.MessageJSON" "$ref": "#/definitions/models.Message"
} }
}, },
"next_page_token": { "next_page_token": {
@@ -3480,7 +3338,7 @@
"keys": { "keys": {
"type": "array", "type": "array",
"items": { "items": {
"$ref": "#/definitions/models.KeyTokenJSON" "$ref": "#/definitions/models.KeyToken"
} }
} }
} }
@@ -3491,7 +3349,7 @@
"subscriptions": { "subscriptions": {
"type": "array", "type": "array",
"items": { "items": {
"$ref": "#/definitions/models.SubscriptionJSON" "$ref": "#/definitions/models.Subscription"
} }
} }
} }
@@ -3545,46 +3403,26 @@
"handler.SendMessage.combined": { "handler.SendMessage.combined": {
"type": "object", "type": "object",
"properties": { "properties": {
"channel": {
"type": "string",
"example": "test"
},
"content": { "content": {
"type": "string", "type": "string"
"example": "This is a message"
},
"key": {
"type": "string",
"example": "P3TNH8mvv14fm"
}, },
"msg_id": { "msg_id": {
"type": "string", "type": "string"
"example": "db8b0e6a-a08c-4646"
}, },
"priority": { "priority": {
"type": "integer", "type": "integer"
"enum": [
0,
1,
2
],
"example": 1
},
"sender_name": {
"type": "string",
"example": "example-server"
}, },
"timestamp": { "timestamp": {
"type": "number", "type": "number"
"example": 1669824037
}, },
"title": { "title": {
"type": "string", "type": "string"
"example": "Hello World"
}, },
"user_id": { "user_id": {
"type": "string", "type": "integer"
"example": "7725" },
"user_key": {
"type": "string"
} }
} }
}, },
@@ -3613,7 +3451,7 @@
"type": "integer" "type": "integer"
}, },
"scn_msg_id": { "scn_msg_id": {
"type": "string" "type": "integer"
}, },
"success": { "success": {
"type": "boolean" "type": "boolean"
@@ -3801,7 +3639,7 @@
} }
} }
}, },
"models.ChannelPreviewJSON": { "models.ChannelPreview": {
"type": "object", "type": "object",
"properties": { "properties": {
"channel_id": { "channel_id": {
@@ -3821,7 +3659,7 @@
} }
} }
}, },
"models.ChannelWithSubscriptionJSON": { "models.ChannelWithSubscription": {
"type": "object", "type": "object",
"properties": { "properties": {
"channel_id": { "channel_id": {
@@ -3847,7 +3685,7 @@
"type": "string" "type": "string"
}, },
"subscription": { "subscription": {
"$ref": "#/definitions/models.SubscriptionJSON" "$ref": "#/definitions/models.Subscription"
}, },
"timestamp_created": { "timestamp_created": {
"type": "string" "type": "string"
@@ -3857,7 +3695,7 @@
} }
} }
}, },
"models.ClientJSON": { "models.Client": {
"type": "object", "type": "object",
"properties": { "properties": {
"agent_model": { "agent_model": {
@@ -3929,7 +3767,7 @@
} }
} }
}, },
"models.KeyTokenJSON": { "models.KeyToken": {
"type": "object", "type": "object",
"properties": { "properties": {
"all_channels": { "all_channels": {
@@ -3954,69 +3792,11 @@
"type": "string" "type": "string"
}, },
"permissions": { "permissions": {
"type": "string"
},
"timestamp_created": {
"type": "string"
},
"timestamp_lastused": {
"type": "string"
}
}
},
"models.KeyTokenPreviewJSON": {
"type": "object",
"properties": {
"all_channels": {
"type": "boolean"
},
"channels": {
"type": "array", "type": "array",
"items": { "items": {
"type": "string" "$ref": "#/definitions/models.TokenPerm"
} }
}, },
"keytoken_id": {
"type": "string"
},
"name": {
"type": "string"
},
"owner_user_id": {
"type": "string"
},
"permissions": {
"type": "string"
}
}
},
"models.KeyTokenWithTokenJSON": {
"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": { "timestamp_created": {
"type": "string" "type": "string"
}, },
@@ -4028,7 +3808,33 @@
} }
} }
}, },
"models.MessageJSON": { "models.KeyTokenPreview": {
"type": "object",
"properties": {
"all_channels": {
"type": "boolean"
},
"channels": {
"type": "array",
"items": {
"type": "string"
}
},
"keytoken_id": {
"type": "string"
},
"name": {
"type": "string"
},
"owner_user_id": {
"type": "string"
},
"permissions": {
"type": "string"
}
}
},
"models.Message": {
"type": "object", "type": "object",
"properties": { "properties": {
"channel_id": { "channel_id": {
@@ -4053,6 +3859,7 @@
"type": "string" "type": "string"
}, },
"sender_user_id": { "sender_user_id": {
"description": "user that sent the message (this is also the owner of the channel that contains it)",
"type": "string" "type": "string"
}, },
"timestamp": { "timestamp": {
@@ -4072,7 +3879,7 @@
} }
} }
}, },
"models.SubscriptionJSON": { "models.Subscription": {
"type": "object", "type": "object",
"properties": { "properties": {
"channel_id": { "channel_id": {
@@ -4098,7 +3905,28 @@
} }
} }
}, },
"models.UserJSON": { "models.TokenPerm": {
"type": "string",
"enum": [
"A",
"CR",
"CS",
"UR"
],
"x-enum-comments": {
"PermAdmin": "Edit userdata (+ includes all other permissions)",
"PermChannelRead": "Read messages",
"PermChannelSend": "Send messages",
"PermUserRead": "Read userdata"
},
"x-enum-varnames": [
"PermAdmin",
"PermChannelRead",
"PermChannelSend",
"PermUserRead"
]
},
"models.User": {
"type": "object", "type": "object",
"properties": { "properties": {
"default_channel": { "default_channel": {
@@ -4157,7 +3985,18 @@
} }
} }
}, },
"models.UserJSONWithClientsAndKeys": { "models.UserPreview": {
"type": "object",
"properties": {
"user_id": {
"type": "string"
},
"username": {
"type": "string"
}
}
},
"models.UserWithClientsAndKeys": {
"type": "object", "type": "object",
"properties": { "properties": {
"admin_key": { "admin_key": {
@@ -4166,7 +4005,7 @@
"clients": { "clients": {
"type": "array", "type": "array",
"items": { "items": {
"$ref": "#/definitions/models.ClientJSON" "$ref": "#/definitions/models.Client"
} }
}, },
"default_channel": { "default_channel": {
@@ -4230,17 +4069,6 @@
"type": "string" "type": "string"
} }
} }
},
"models.UserPreviewJSON": {
"type": "object",
"properties": {
"user_id": {
"type": "string"
},
"username": {
"type": "string"
}
}
} }
}, },
"tags": [ "tags": [
+144 -275
View File
@@ -14,6 +14,7 @@ definitions:
- 1151 - 1151
- 1152 - 1152
- 1153 - 1153
- 1152
- 1161 - 1161
- 1171 - 1171
- 1201 - 1201
@@ -59,6 +60,7 @@ definitions:
- BINDFAIL_QUERY_PARAM - BINDFAIL_QUERY_PARAM
- BINDFAIL_BODY_PARAM - BINDFAIL_BODY_PARAM
- BINDFAIL_URI_PARAM - BINDFAIL_URI_PARAM
- BINDFAIL_HEADER_PARAM
- INVALID_BODY_PARAM - INVALID_BODY_PARAM
- INVALID_ENUM_VALUE - INVALID_ENUM_VALUE
- NO_TITLE - NO_TITLE
@@ -242,7 +244,7 @@ definitions:
properties: properties:
messages: messages:
items: items:
$ref: '#/definitions/models.MessageJSON' $ref: '#/definitions/models.Message'
type: array type: array
next_page_token: next_page_token:
type: string type: string
@@ -253,28 +255,28 @@ definitions:
properties: properties:
subscriptions: subscriptions:
items: items:
$ref: '#/definitions/models.SubscriptionJSON' $ref: '#/definitions/models.Subscription'
type: array type: array
type: object type: object
handler.ListChannels.response: handler.ListChannels.response:
properties: properties:
channels: channels:
items: items:
$ref: '#/definitions/models.ChannelWithSubscriptionJSON' $ref: '#/definitions/models.ChannelWithSubscription'
type: array type: array
type: object type: object
handler.ListClients.response: handler.ListClients.response:
properties: properties:
clients: clients:
items: items:
$ref: '#/definitions/models.ClientJSON' $ref: '#/definitions/models.Client'
type: array type: array
type: object type: object
handler.ListMessages.response: handler.ListMessages.response:
properties: properties:
messages: messages:
items: items:
$ref: '#/definitions/models.MessageJSON' $ref: '#/definitions/models.Message'
type: array type: array
next_page_token: next_page_token:
type: string type: string
@@ -285,14 +287,14 @@ definitions:
properties: properties:
keys: keys:
items: items:
$ref: '#/definitions/models.KeyTokenJSON' $ref: '#/definitions/models.KeyToken'
type: array type: array
type: object type: object
handler.ListUserSubscriptions.response: handler.ListUserSubscriptions.response:
properties: properties:
subscriptions: subscriptions:
items: items:
$ref: '#/definitions/models.SubscriptionJSON' $ref: '#/definitions/models.Subscription'
type: array type: array
type: object type: object
handler.Register.response: handler.Register.response:
@@ -327,36 +329,19 @@ definitions:
type: object type: object
handler.SendMessage.combined: handler.SendMessage.combined:
properties: properties:
channel:
example: test
type: string
content: content:
example: This is a message
type: string
key:
example: P3TNH8mvv14fm
type: string type: string
msg_id: msg_id:
example: db8b0e6a-a08c-4646
type: string type: string
priority: priority:
enum:
- 0
- 1
- 2
example: 1
type: integer type: integer
sender_name:
example: example-server
type: string
timestamp: timestamp:
example: 1669824037
type: number type: number
title: title:
example: Hello World
type: string type: string
user_id: user_id:
example: "7725" type: integer
user_key:
type: string type: string
type: object type: object
handler.SendMessage.response: handler.SendMessage.response:
@@ -376,7 +361,7 @@ definitions:
quota_max: quota_max:
type: integer type: integer
scn_msg_id: scn_msg_id:
type: string type: integer
success: success:
type: boolean type: boolean
suppress_send: suppress_send:
@@ -497,7 +482,7 @@ definitions:
uri: uri:
type: string type: string
type: object type: object
models.ChannelPreviewJSON: models.ChannelPreview:
properties: properties:
channel_id: channel_id:
type: string type: string
@@ -510,7 +495,7 @@ definitions:
owner_user_id: owner_user_id:
type: string type: string
type: object type: object
models.ChannelWithSubscriptionJSON: models.ChannelWithSubscription:
properties: properties:
channel_id: channel_id:
type: string type: string
@@ -528,13 +513,13 @@ definitions:
description: can be nil, depending on endpoint description: can be nil, depending on endpoint
type: string type: string
subscription: subscription:
$ref: '#/definitions/models.SubscriptionJSON' $ref: '#/definitions/models.Subscription'
timestamp_created: timestamp_created:
type: string type: string
timestamp_lastsent: timestamp_lastsent:
type: string type: string
type: object type: object
models.ClientJSON: models.Client:
properties: properties:
agent_model: agent_model:
type: string type: string
@@ -584,7 +569,7 @@ definitions:
usr_msg_id: usr_msg_id:
type: string type: string
type: object type: object
models.KeyTokenJSON: models.KeyToken:
properties: properties:
all_channels: all_channels:
type: boolean type: boolean
@@ -601,47 +586,9 @@ definitions:
owner_user_id: owner_user_id:
type: string type: string
permissions: permissions:
type: string
timestamp_created:
type: string
timestamp_lastused:
type: string
type: object
models.KeyTokenPreviewJSON:
properties:
all_channels:
type: boolean
channels:
items: items:
type: string $ref: '#/definitions/models.TokenPerm'
type: array type: array
keytoken_id:
type: string
name:
type: string
owner_user_id:
type: string
permissions:
type: string
type: object
models.KeyTokenWithTokenJSON:
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: timestamp_created:
type: string type: string
timestamp_lastused: timestamp_lastused:
@@ -649,7 +596,24 @@ definitions:
token: token:
type: string type: string
type: object type: object
models.MessageJSON: models.KeyTokenPreview:
properties:
all_channels:
type: boolean
channels:
items:
type: string
type: array
keytoken_id:
type: string
name:
type: string
owner_user_id:
type: string
permissions:
type: string
type: object
models.Message:
properties: properties:
channel_id: channel_id:
type: string type: string
@@ -666,6 +630,8 @@ definitions:
sender_name: sender_name:
type: string type: string
sender_user_id: sender_user_id:
description: user that sent the message (this is also the owner of the channel
that contains it)
type: string type: string
timestamp: timestamp:
type: string type: string
@@ -678,7 +644,7 @@ definitions:
usr_message_id: usr_message_id:
type: string type: string
type: object type: object
models.SubscriptionJSON: models.Subscription:
properties: properties:
channel_id: channel_id:
type: string type: string
@@ -695,7 +661,24 @@ definitions:
timestamp_created: timestamp_created:
type: string type: string
type: object type: object
models.UserJSON: models.TokenPerm:
enum:
- A
- CR
- CS
- UR
type: string
x-enum-comments:
PermAdmin: Edit userdata (+ includes all other permissions)
PermChannelRead: Read messages
PermChannelSend: Send messages
PermUserRead: Read userdata
x-enum-varnames:
- PermAdmin
- PermChannelRead
- PermChannelSend
- PermUserRead
models.User:
properties: properties:
default_channel: default_channel:
type: string type: string
@@ -734,13 +717,20 @@ definitions:
username: username:
type: string type: string
type: object type: object
models.UserJSONWithClientsAndKeys: models.UserPreview:
properties:
user_id:
type: string
username:
type: string
type: object
models.UserWithClientsAndKeys:
properties: properties:
admin_key: admin_key:
type: string type: string
clients: clients:
items: items:
$ref: '#/definitions/models.ClientJSON' $ref: '#/definitions/models.Client'
type: array type: array
default_channel: default_channel:
type: string type: string
@@ -783,13 +773,6 @@ definitions:
username: username:
type: string type: string
type: object type: object
models.UserPreviewJSON:
properties:
user_id:
type: string
username:
type: string
type: object
host: simplecloudnotifier.de host: simplecloudnotifier.de
info: info:
contact: {} contact: {}
@@ -802,90 +785,52 @@ 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: test - in: query
in: query
name: channel
type: string
- example: This is a message
in: query
name: content name: content
type: string type: string
- example: P3TNH8mvv14fm - in: query
in: query
name: key
type: string
- example: db8b0e6a-a08c-4646
in: query
name: msg_id name: msg_id
type: string type: string
- enum: - in: query
- 0
- 1
- 2
example: 1
in: query
name: priority name: priority
type: integer type: integer
- example: example-server - in: query
in: query
name: sender_name
type: string
- example: 1669824037
in: query
name: timestamp name: timestamp
type: number type: number
- example: Hello World - in: query
in: query
name: title name: title
type: string type: string
- example: "7725" - in: query
in: query
name: user_id name: user_id
type: integer
- in: query
name: user_key
type: string 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: test - in: formData
in: formData
name: channel
type: string
- example: This is a message
in: formData
name: content name: content
type: string type: string
- example: P3TNH8mvv14fm - in: formData
in: formData
name: key
type: string
- example: db8b0e6a-a08c-4646
in: formData
name: msg_id name: msg_id
type: string type: string
- enum: - in: formData
- 0
- 1
- 2
example: 1
in: formData
name: priority name: priority
type: integer type: integer
- example: example-server - in: formData
in: formData
name: sender_name
type: string
- example: 1669824037
in: formData
name: timestamp name: timestamp
type: number type: number
- example: Hello World - in: formData
in: formData
name: title name: title
type: string type: string
- example: "7725" - in: formData
in: formData
name: user_id name: user_id
type: integer
- in: formData
name: user_key
type: string type: string
responses: responses:
"200": "200":
@@ -1458,7 +1403,7 @@ paths:
"200": "200":
description: OK description: OK
schema: schema:
$ref: '#/definitions/models.MessageJSON' $ref: '#/definitions/models.Message'
"400": "400":
description: supplied values/parameters cannot be parsed / are invalid description: supplied values/parameters cannot be parsed / are invalid
schema: schema:
@@ -1494,7 +1439,7 @@ paths:
"200": "200":
description: OK description: OK
schema: schema:
$ref: '#/definitions/models.MessageJSON' $ref: '#/definitions/models.Message'
"400": "400":
description: supplied values/parameters cannot be parsed / are invalid description: supplied values/parameters cannot be parsed / are invalid
schema: schema:
@@ -1527,7 +1472,7 @@ paths:
"200": "200":
description: OK description: OK
schema: schema:
$ref: '#/definitions/models.ChannelPreviewJSON' $ref: '#/definitions/models.ChannelPreview'
"400": "400":
description: supplied values/parameters cannot be parsed / are invalid description: supplied values/parameters cannot be parsed / are invalid
schema: schema:
@@ -1561,7 +1506,7 @@ paths:
"200": "200":
description: OK description: OK
schema: schema:
$ref: '#/definitions/models.KeyTokenPreviewJSON' $ref: '#/definitions/models.KeyTokenPreview'
"400": "400":
description: supplied values/parameters cannot be parsed / are invalid description: supplied values/parameters cannot be parsed / are invalid
schema: schema:
@@ -1595,7 +1540,7 @@ paths:
"200": "200":
description: OK description: OK
schema: schema:
$ref: '#/definitions/models.UserPreviewJSON' $ref: '#/definitions/models.UserPreview'
"400": "400":
description: supplied values/parameters cannot be parsed / are invalid description: supplied values/parameters cannot be parsed / are invalid
schema: schema:
@@ -1629,7 +1574,7 @@ paths:
"200": "200":
description: OK description: OK
schema: schema:
$ref: '#/definitions/models.UserJSONWithClientsAndKeys' $ref: '#/definitions/models.UserWithClientsAndKeys'
"400": "400":
description: supplied values/parameters cannot be parsed / are invalid description: supplied values/parameters cannot be parsed / are invalid
schema: schema:
@@ -1654,7 +1599,7 @@ paths:
"200": "200":
description: OK description: OK
schema: schema:
$ref: '#/definitions/models.UserJSON' $ref: '#/definitions/models.User'
"400": "400":
description: supplied values/parameters cannot be parsed / are invalid description: supplied values/parameters cannot be parsed / are invalid
schema: schema:
@@ -1697,7 +1642,7 @@ paths:
"200": "200":
description: OK description: OK
schema: schema:
$ref: '#/definitions/models.UserJSON' $ref: '#/definitions/models.User'
"400": "400":
description: supplied values/parameters cannot be parsed / are invalid description: supplied values/parameters cannot be parsed / are invalid
schema: schema:
@@ -1780,7 +1725,7 @@ paths:
"200": "200":
description: OK description: OK
schema: schema:
$ref: '#/definitions/models.ChannelWithSubscriptionJSON' $ref: '#/definitions/models.ChannelWithSubscription'
"400": "400":
description: supplied values/parameters cannot be parsed / are invalid description: supplied values/parameters cannot be parsed / are invalid
schema: schema:
@@ -1818,7 +1763,7 @@ paths:
"200": "200":
description: OK description: OK
schema: schema:
$ref: '#/definitions/models.ChannelWithSubscriptionJSON' $ref: '#/definitions/models.ChannelWithSubscription'
"400": "400":
description: supplied values/parameters cannot be parsed / are invalid description: supplied values/parameters cannot be parsed / are invalid
schema: schema:
@@ -1871,7 +1816,7 @@ paths:
"200": "200":
description: OK description: OK
schema: schema:
$ref: '#/definitions/models.ChannelWithSubscriptionJSON' $ref: '#/definitions/models.ChannelWithSubscription'
"400": "400":
description: supplied values/parameters cannot be parsed / are invalid description: supplied values/parameters cannot be parsed / are invalid
schema: schema:
@@ -2030,7 +1975,7 @@ paths:
"200": "200":
description: OK description: OK
schema: schema:
$ref: '#/definitions/models.ClientJSON' $ref: '#/definitions/models.Client'
"400": "400":
description: supplied values/parameters cannot be parsed / are invalid description: supplied values/parameters cannot be parsed / are invalid
schema: schema:
@@ -2064,7 +2009,7 @@ paths:
"200": "200":
description: OK description: OK
schema: schema:
$ref: '#/definitions/models.ClientJSON' $ref: '#/definitions/models.Client'
"400": "400":
description: supplied values/parameters cannot be parsed / are invalid description: supplied values/parameters cannot be parsed / are invalid
schema: schema:
@@ -2101,7 +2046,7 @@ paths:
"200": "200":
description: OK description: OK
schema: schema:
$ref: '#/definitions/models.ClientJSON' $ref: '#/definitions/models.Client'
"400": "400":
description: supplied values/parameters cannot be parsed / are invalid description: supplied values/parameters cannot be parsed / are invalid
schema: schema:
@@ -2149,7 +2094,7 @@ paths:
"200": "200":
description: OK description: OK
schema: schema:
$ref: '#/definitions/models.ClientJSON' $ref: '#/definitions/models.Client'
"400": "400":
description: supplied values/parameters cannot be parsed / are invalid description: supplied values/parameters cannot be parsed / are invalid
schema: schema:
@@ -2221,7 +2166,7 @@ paths:
"200": "200":
description: OK description: OK
schema: schema:
$ref: '#/definitions/models.KeyTokenJSON' $ref: '#/definitions/models.KeyToken'
"400": "400":
description: supplied values/parameters cannot be parsed / are invalid description: supplied values/parameters cannot be parsed / are invalid
schema: schema:
@@ -2260,7 +2205,7 @@ paths:
"200": "200":
description: OK description: OK
schema: schema:
$ref: '#/definitions/models.KeyTokenJSON' $ref: '#/definitions/models.KeyToken'
"400": "400":
description: supplied values/parameters cannot be parsed / are invalid description: supplied values/parameters cannot be parsed / are invalid
schema: schema:
@@ -2299,7 +2244,7 @@ paths:
"200": "200":
description: OK description: OK
schema: schema:
$ref: '#/definitions/models.KeyTokenJSON' $ref: '#/definitions/models.KeyToken'
"400": "400":
description: supplied values/parameters cannot be parsed / are invalid description: supplied values/parameters cannot be parsed / are invalid
schema: schema:
@@ -2341,7 +2286,7 @@ paths:
"200": "200":
description: OK description: OK
schema: schema:
$ref: '#/definitions/models.KeyTokenJSON' $ref: '#/definitions/models.KeyToken'
"400": "400":
description: supplied values/parameters cannot be parsed / are invalid description: supplied values/parameters cannot be parsed / are invalid
schema: schema:
@@ -2381,7 +2326,7 @@ paths:
"200": "200":
description: OK description: OK
schema: schema:
$ref: '#/definitions/models.KeyTokenWithTokenJSON' $ref: '#/definitions/models.KeyToken'
"400": "400":
description: supplied values/parameters cannot be parsed / are invalid description: supplied values/parameters cannot be parsed / are invalid
schema: schema:
@@ -2483,7 +2428,7 @@ paths:
"200": "200":
description: OK description: OK
schema: schema:
$ref: '#/definitions/models.SubscriptionJSON' $ref: '#/definitions/models.Subscription'
"400": "400":
description: supplied values/parameters cannot be parsed / are invalid description: supplied values/parameters cannot be parsed / are invalid
schema: schema:
@@ -2517,7 +2462,7 @@ paths:
"200": "200":
description: OK description: OK
schema: schema:
$ref: '#/definitions/models.SubscriptionJSON' $ref: '#/definitions/models.Subscription'
"400": "400":
description: supplied values/parameters cannot be parsed / are invalid description: supplied values/parameters cannot be parsed / are invalid
schema: schema:
@@ -2554,7 +2499,7 @@ paths:
"200": "200":
description: OK description: OK
schema: schema:
$ref: '#/definitions/models.SubscriptionJSON' $ref: '#/definitions/models.Subscription'
"400": "400":
description: supplied values/parameters cannot be parsed / are invalid description: supplied values/parameters cannot be parsed / are invalid
schema: schema:
@@ -2596,7 +2541,7 @@ paths:
"200": "200":
description: OK description: OK
schema: schema:
$ref: '#/definitions/models.SubscriptionJSON' $ref: '#/definitions/models.Subscription'
"400": "400":
description: supplied values/parameters cannot be parsed / are invalid description: supplied values/parameters cannot be parsed / are invalid
schema: schema:
@@ -2685,90 +2630,52 @@ 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: test - in: query
in: query
name: channel
type: string
- example: This is a message
in: query
name: content name: content
type: string type: string
- example: P3TNH8mvv14fm - in: query
in: query
name: key
type: string
- example: db8b0e6a-a08c-4646
in: query
name: msg_id name: msg_id
type: string type: string
- enum: - in: query
- 0
- 1
- 2
example: 1
in: query
name: priority name: priority
type: integer type: integer
- example: example-server - in: query
in: query
name: sender_name
type: string
- example: 1669824037
in: query
name: timestamp name: timestamp
type: number type: number
- example: Hello World - in: query
in: query
name: title name: title
type: string type: string
- example: "7725" - in: query
in: query
name: user_id name: user_id
type: integer
- in: query
name: user_key
type: string 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: test - in: formData
in: formData
name: channel
type: string
- example: This is a message
in: formData
name: content name: content
type: string type: string
- example: P3TNH8mvv14fm - in: formData
in: formData
name: key
type: string
- example: db8b0e6a-a08c-4646
in: formData
name: msg_id name: msg_id
type: string type: string
- enum: - in: formData
- 0
- 1
- 2
example: 1
in: formData
name: priority name: priority
type: integer type: integer
- example: example-server - in: formData
in: formData
name: sender_name
type: string
- example: 1669824037
in: formData
name: timestamp name: timestamp
type: number type: number
- example: Hello World - in: formData
in: formData
name: title name: title
type: string type: string
- example: "7725" - in: formData
in: formData
name: user_id name: user_id
type: integer
- in: formData
name: user_key
type: string type: string
responses: responses:
"200": "200":
@@ -2801,85 +2708,47 @@ paths:
description: All parameter can be set via query-parameter or form-data body. description: All parameter can be set via query-parameter or form-data body.
Only UserID, UserKey and Title are required Only UserID, UserKey and Title are required
parameters: parameters:
- example: test - in: query
in: query
name: channel
type: string
- example: This is a message
in: query
name: content name: content
type: string type: string
- example: P3TNH8mvv14fm - in: query
in: query
name: key
type: string
- example: db8b0e6a-a08c-4646
in: query
name: msg_id name: msg_id
type: string type: string
- enum: - in: query
- 0
- 1
- 2
example: 1
in: query
name: priority name: priority
type: integer type: integer
- example: example-server - in: query
in: query
name: sender_name
type: string
- example: 1669824037
in: query
name: timestamp name: timestamp
type: number type: number
- example: Hello World - in: query
in: query
name: title name: title
type: string type: string
- example: "7725" - in: query
in: query
name: user_id name: user_id
type: integer
- in: query
name: user_key
type: string type: string
- example: test - in: formData
in: formData
name: channel
type: string
- example: This is a message
in: formData
name: content name: content
type: string type: string
- example: P3TNH8mvv14fm - in: formData
in: formData
name: key
type: string
- example: db8b0e6a-a08c-4646
in: formData
name: msg_id name: msg_id
type: string type: string
- enum: - in: formData
- 0
- 1
- 2
example: 1
in: formData
name: priority name: priority
type: integer type: integer
- example: example-server - in: formData
in: formData
name: sender_name
type: string
- example: 1669824037
in: formData
name: timestamp name: timestamp
type: number type: number
- example: Hello World - in: formData
in: formData
name: title name: title
type: string type: string
- example: "7725" - in: formData
in: formData
name: user_id name: user_id
type: integer
- in: formData
name: user_key
type: string type: string
responses: responses:
"200": "200":
+1 -1
View File
@@ -131,7 +131,7 @@ func TestTokenKeys(t *testing.T) {
msg1 := tt.RequestAuthGet[gin.H](t, data.AdminKey, baseUrl, fmt.Sprintf("/api/v2/messages/%s", msg1s["scn_msg_id"])) msg1 := tt.RequestAuthGet[gin.H](t, data.AdminKey, baseUrl, fmt.Sprintf("/api/v2/messages/%s", msg1s["scn_msg_id"]))
tt.AssertEqual(t, "AllChannels", key7.KeytokenId, msg1["used_key_id"]) tt.AssertEqual(t, "used_key_id", key7.KeytokenId, msg1["used_key_id"])
tt.RequestPostShouldFail(t, baseUrl, "/", gin.H{ tt.RequestPostShouldFail(t, baseUrl, "/", gin.H{
"key": key7.Token, "key": key7.Token,
+22
View File
@@ -124,3 +124,25 @@ func TestRequestLogSimple(t *testing.T) {
} }
} }
func TestRequestLogAPI(t *testing.T) {
ws, baseUrl, stop := tt.StartSimpleWebserver(t)
defer stop()
data := tt.InitDefaultData(t, ws)
time.Sleep(900 * time.Millisecond)
ctx := ws.NewSimpleTransactionContext(5 * time.Second)
defer ctx.Cancel()
rl1, _, err := ws.Database.Requests.ListRequestLogs(ctx, models.RequestLogFilter{}, nil, ct.Start())
tt.TestFailIfErr(t, err)
tt.RequestAuthGet[gin.H](t, data.User[0].ReadKey, baseUrl, "/api/v2/users/"+data.User[0].UID)
time.Sleep(900 * time.Millisecond)
rl2, _, err := ws.Database.Requests.ListRequestLogs(ctx, models.RequestLogFilter{}, nil, ct.Start())
tt.TestFailIfErr(t, err)
tt.AssertEqual(t, "requestlog.count", len(rl1)+1, len(rl2))
}
+290
View File
@@ -0,0 +1,290 @@
package test
import (
tt "blackforestbytes.com/simplecloudnotifier/test/util"
"fmt"
"github.com/gin-gonic/gin"
"testing"
)
func TestResponseChannel(t *testing.T) {
ws, baseUrl, stop := tt.StartSimpleWebserver(t)
defer stop()
data := tt.InitDefaultData(t, ws)
response := tt.RequestAuthGetRaw(t, data.User[0].AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/channels/%s", data.User[0].UID, data.User[0].Channels[0]))
tt.AssertJsonStructureMatch(t, "json[channel]", response, map[string]any{
"channel_id": "id",
"owner_user_id": "id",
"internal_name": "string",
"display_name": "string",
"description_name": "null",
"subscribe_key": "string",
"timestamp_created": "rfc3339",
"timestamp_lastsent": "rfc3339",
"messages_sent": "int",
"subscription": map[string]any{
"subscription_id": "id",
"subscriber_user_id": "id",
"channel_owner_user_id": "id",
"channel_id": "id",
"channel_internal_name": "string",
"timestamp_created": "rfc3339",
"confirmed": "bool",
},
})
}
func TestResponseClient(t *testing.T) {
ws, baseUrl, stop := tt.StartSimpleWebserver(t)
defer stop()
data := tt.InitDefaultData(t, ws)
response := tt.RequestAuthGetRaw(t, data.User[2].AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/clients/%s", data.User[2].UID, data.User[2].Clients[0]))
tt.AssertJsonStructureMatch(t, "json[client]", response, map[string]any{
"client_id": "id",
"user_id": "id",
"type": "string",
"fcm_token": "string",
"timestamp_created": "rfc3339",
"agent_model": "string",
"agent_version": "string",
"name": "string|null",
})
}
func TestResponseKeyToken1(t *testing.T) {
ws, baseUrl, stop := tt.StartSimpleWebserver(t)
defer stop()
data := tt.InitDefaultData(t, ws)
response := tt.RequestAuthGetRaw(t, data.User[0].AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/keys/%s", data.User[0].UID, data.User[0].Keys[0]))
tt.AssertJsonStructureMatch(t, "json[key]", response, map[string]any{
"keytoken_id": "id",
"name": "string",
"timestamp_created": "rfc3339",
"timestamp_lastused": "rfc3339|null",
"owner_user_id": "id",
"all_channels": "bool",
"channels": []any{"string"},
"permissions": "string",
"messages_sent": "int",
})
}
func TestResponseKeyToken2(t *testing.T) {
ws, baseUrl, stop := tt.StartSimpleWebserver(t)
defer stop()
data := tt.InitSingleData(t, ws)
chan1 := tt.RequestAuthPost[gin.H](t, data.AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/channels", data.UID), gin.H{
"name": "TestChan1asdf",
})
type keyobj struct {
KeytokenId string `json:"keytoken_id"`
}
k0 := tt.RequestAuthPost[keyobj](t, data.AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/keys", data.UID), gin.H{
"all_channels": false,
"channels": []string{chan1["channel_id"].(string)},
"name": "TKey1",
"permissions": "CS",
})
response := tt.RequestAuthGetRaw(t, data.AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/keys/%s", data.UID, k0.KeytokenId))
tt.AssertJsonStructureMatch(t, "json[key]", response, map[string]any{
"keytoken_id": "id",
"name": "string",
"timestamp_created": "rfc3339",
"timestamp_lastused": "rfc3339|null",
"owner_user_id": "id",
"all_channels": "bool",
"channels": []any{"string"},
"permissions": "string",
"messages_sent": "int",
})
}
func TestResponseKeyToken3(t *testing.T) {
ws, baseUrl, stop := tt.StartSimpleWebserver(t)
defer stop()
data := tt.InitSingleData(t, ws)
response := tt.RequestAuthGetRaw(t, data.AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/keys/current", data.UID))
tt.AssertJsonStructureMatch(t, "json[key]", response, map[string]any{
"keytoken_id": "id",
"name": "string",
"timestamp_created": "rfc3339",
"timestamp_lastused": "rfc3339|null",
"owner_user_id": "id",
"all_channels": "bool",
"channels": []any{"string"},
"permissions": "string",
"messages_sent": "int",
"token": "string",
})
}
func TestResponseKeyToken4(t *testing.T) {
ws, baseUrl, stop := tt.StartSimpleWebserver(t)
defer stop()
data := tt.InitSingleData(t, ws)
chan1 := tt.RequestAuthPost[gin.H](t, data.AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/channels", data.UID), gin.H{
"name": "TestChan1asdf",
})
response := tt.RequestAuthPostRaw(t, data.AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/keys", data.UID), gin.H{
"all_channels": false,
"channels": []string{chan1["channel_id"].(string)},
"name": "TKey1",
"permissions": "CS",
})
tt.AssertJsonStructureMatch(t, "json[key]", response, map[string]any{
"keytoken_id": "id",
"name": "string",
"timestamp_created": "rfc3339",
"timestamp_lastused": "rfc3339|null",
"owner_user_id": "id",
"all_channels": "bool",
"channels": []any{"string"},
"permissions": "string",
"messages_sent": "int",
"token": "string",
})
}
func TestResponseMessage(t *testing.T) {
ws, baseUrl, stop := tt.StartSimpleWebserver(t)
defer stop()
data := tt.InitDefaultData(t, ws)
response := tt.RequestAuthGetRaw(t, data.User[0].AdminKey, baseUrl, fmt.Sprintf("/api/v2/messages/%s", data.User[0].Messages[0]))
tt.AssertJsonStructureMatch(t, "json[message]", response, map[string]any{
"message_id": "id",
"sender_user_id": "id",
"channel_internal_name": "string",
"channel_id": "id",
"sender_name": "string",
"sender_ip": "string",
"timestamp": "rfc3339",
"title": "string",
"content": "null",
"priority": "int",
"usr_message_id": "null",
"used_key_id": "id",
"trimmed": "bool",
})
}
func TestResponseSubscription(t *testing.T) {
ws, baseUrl, stop := tt.StartSimpleWebserver(t)
defer stop()
data := tt.InitDefaultData(t, ws)
response := tt.RequestAuthGetRaw(t, data.User[0].AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/subscriptions/%s", data.User[0].UID, data.User[0].Subscriptions[0]))
tt.AssertJsonStructureMatch(t, "json[subscription]", response, map[string]any{
"subscription_id": "id",
"subscriber_user_id": "id",
"channel_owner_user_id": "id",
"channel_id": "id",
"channel_internal_name": "string",
"timestamp_created": "rfc3339",
"confirmed": "bool",
})
}
func TestResponseUser(t *testing.T) {
ws, baseUrl, stop := tt.StartSimpleWebserver(t)
defer stop()
data := tt.InitDefaultData(t, ws)
response := tt.RequestAuthGetRaw(t, data.User[0].AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s", data.User[0].UID))
tt.AssertJsonStructureMatch(t, "json[user]", response, map[string]any{
"user_id": "id",
"username": "null",
"timestamp_created": "rfc3339",
"timestamp_lastread": "null",
"timestamp_lastsent": "rfc3339",
"messages_sent": "int",
"quota_used": "int",
"quota_remaining": "int",
"quota_max": "int",
"is_pro": "bool",
"default_channel": "string",
"max_body_size": "int",
"max_title_length": "int",
"default_priority": "int",
"max_channel_name_length": "int",
"max_channel_description_length": "int",
"max_sender_name_length": "int",
"max_user_message_id_length": "int",
})
}
func TestResponseChannelPreview(t *testing.T) {
ws, baseUrl, stop := tt.StartSimpleWebserver(t)
defer stop()
data := tt.InitDefaultData(t, ws)
response := tt.RequestAuthGetRaw(t, data.User[1].AdminKey, baseUrl, fmt.Sprintf("/api/v2/preview/channels/%s", data.User[0].Channels[0]))
tt.AssertJsonStructureMatch(t, "json[channel]", response, map[string]any{
"channel_id": "id",
"owner_user_id": "id",
"internal_name": "string",
"display_name": "string",
"description_name": "string|null",
})
}
func TestResponseUserPreview(t *testing.T) {
ws, baseUrl, stop := tt.StartSimpleWebserver(t)
defer stop()
data := tt.InitDefaultData(t, ws)
response := tt.RequestAuthGetRaw(t, data.User[1].AdminKey, baseUrl, fmt.Sprintf("/api/v2/preview/users/%s", data.User[0].UID))
tt.AssertJsonStructureMatch(t, "json[user]", response, map[string]any{
"user_id": "id",
"username": "string|null",
})
}
func TestResponseKeyTokenPreview(t *testing.T) {
ws, baseUrl, stop := tt.StartSimpleWebserver(t)
defer stop()
data := tt.InitDefaultData(t, ws)
response := tt.RequestAuthGetRaw(t, data.User[1].AdminKey, baseUrl, fmt.Sprintf("/api/v2/preview/keys/%s", data.User[0].Keys[0]))
tt.AssertJsonStructureMatch(t, "json[key]", response, map[string]any{
"keytoken_id": "id",
"name": "string",
"owner_user_id": "id",
"all_channels": "bool",
"channels": []any{"id"},
"permissions": "string",
})
}
+34 -7
View File
@@ -7,6 +7,7 @@ import (
tt "blackforestbytes.com/simplecloudnotifier/test/util" tt "blackforestbytes.com/simplecloudnotifier/test/util"
"fmt" "fmt"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"math/rand/v2"
"net/url" "net/url"
"strings" "strings"
"testing" "testing"
@@ -836,7 +837,7 @@ func TestSendWithTimestamp(t *testing.T) {
tt.AssertEqual(t, "messageCount", 1, len(pusher.Data)) tt.AssertEqual(t, "messageCount", 1, len(pusher.Data))
tt.AssertStrRepEqual(t, "msg.title", "TTT", pusher.Last().Message.Title) tt.AssertStrRepEqual(t, "msg.title", "TTT", pusher.Last().Message.Title)
tt.AssertStrRepEqual(t, "msg.TimestampClient", ts, pusher.Last().Message.TimestampClient.Unix()) tt.AssertStrRepEqual(t, "msg.TimestampClient", ts, pusher.Last().Message.TimestampClient.Time().Unix())
tt.AssertStrRepEqual(t, "msg.Timestamp", ts, pusher.Last().Message.Timestamp().Unix()) tt.AssertStrRepEqual(t, "msg.Timestamp", ts, pusher.Last().Message.Timestamp().Unix())
tt.AssertNotStrRepEqual(t, "msg.ts", pusher.Last().Message.TimestampClient, pusher.Last().Message.TimestampReal) tt.AssertNotStrRepEqual(t, "msg.ts", pusher.Last().Message.TimestampClient, pusher.Last().Message.TimestampReal)
tt.AssertStrRepEqual(t, "msg.scn_msg_id", msg1["scn_msg_id"], pusher.Last().Message.MessageID) tt.AssertStrRepEqual(t, "msg.scn_msg_id", msg1["scn_msg_id"], pusher.Last().Message.MessageID)
@@ -1341,8 +1342,14 @@ func TestSendParallel(t *testing.T) {
uid := r0["user_id"].(string) uid := r0["user_id"].(string)
sendtok := r0["send_key"].(string) sendtok := r0["send_key"].(string)
admintok := r0["admin_key"].(string)
count := 128 count := 512
chanNames := make([]string, 0)
for i := 0; i < count/50; i++ {
chanNames = append(chanNames, tt.ShortLipsum0(1))
}
sem := make(chan tt.Void, count) // semaphore pattern sem := make(chan tt.Void, count) // semaphore pattern
for i := 0; i < count; i++ { for i := 0; i < count; i++ {
@@ -1350,11 +1357,31 @@ func TestSendParallel(t *testing.T) {
defer func() { defer func() {
sem <- tt.Void{} sem <- tt.Void{}
}() }()
tt.RequestPost[gin.H](t, baseUrl, "/", gin.H{
"key": sendtok, if rand.Int()%2 == 0 {
"user_id": uid, tt.RequestPost[gin.H](t, baseUrl, "/", gin.H{
"title": tt.ShortLipsum0(2), "key": sendtok,
}) "user_id": uid,
"title": tt.ShortLipsum0(2),
})
} else {
tt.RequestPost[gin.H](t, baseUrl, "/", gin.H{
"key": sendtok,
"user_id": uid,
"title": tt.ShortLipsum0(2),
"channel": chanNames[rand.IntN(len(chanNames))],
})
}
tt.RequestGet[tt.Void](t, baseUrl, fmt.Sprintf("/api/ping"))
tt.RequestAuthGet[gin.H](t, admintok, baseUrl, fmt.Sprintf("/api/v2/messages"))
tt.RequestAuthGet[gin.H](t, admintok, baseUrl, "/api/v2/users/"+uid)
tt.RequestAuthGet[gin.H](t, admintok, baseUrl, "/api/v2/users/"+uid+"/channels")
tt.RequestAuthGet[gin.H](t, admintok, baseUrl, "/api/v2/users/"+uid+"/clients")
}() }()
} }
// wait for goroutines to finish // wait for goroutines to finish
+57 -8
View File
@@ -5,6 +5,7 @@ import (
"fmt" "fmt"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"gogs.mikescher.com/BlackForestBytes/goext/langext"
"gogs.mikescher.com/BlackForestBytes/goext/timeext" "gogs.mikescher.com/BlackForestBytes/goext/timeext"
"gopkg.in/loremipsum.v1" "gopkg.in/loremipsum.v1"
"testing" "testing"
@@ -59,10 +60,15 @@ type clientex struct {
} }
type Userdat struct { type Userdat struct {
UID string UID string
SendKey string SendKey string
AdminKey string AdminKey string
ReadKey string ReadKey string
Clients []string
Channels []string
Messages []string
Keys []string
Subscriptions []string
} }
const PX = -1 const PX = -1
@@ -367,7 +373,8 @@ func InitDefaultData(t *testing.T, ws *logic.Application) DefData {
body["agent_version"] = cex.AgentVersion body["agent_version"] = cex.AgentVersion
body["client_type"] = cex.ClientType body["client_type"] = cex.ClientType
body["fcm_token"] = cex.FCMTok body["fcm_token"] = cex.FCMTok
RequestAuthPost[gin.H](t, users[cex.User].AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/clients", users[cex.User].UID), body) r0 := RequestAuthPost[gin.H](t, users[cex.User].AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/clients", users[cex.User].UID), body)
users[cex.User].Clients = append(users[cex.User].Clients, r0["client_id"].(string))
} }
// Create Messages // Create Messages
@@ -398,7 +405,8 @@ func InitDefaultData(t *testing.T, ws *logic.Application) DefData {
body["timestamp"] = (time.Now().Add(mex.TSOffset)).Unix() body["timestamp"] = (time.Now().Add(mex.TSOffset)).Unix()
} }
RequestPost[gin.H](t, baseUrl, "/", body) r0 := RequestPost[gin.H](t, baseUrl, "/", body)
users[mex.User].Messages = append(users[mex.User].Messages, r0["scn_msg_id"].(string))
} }
// create manual channels // create manual channels
@@ -407,6 +415,45 @@ func InitDefaultData(t *testing.T, ws *logic.Application) DefData {
RequestAuthPost[Void](t, users[9].AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/channels", users[9].UID), gin.H{"name": "manual@chan"}) RequestAuthPost[Void](t, users[9].AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/channels", users[9].UID), gin.H{"name": "manual@chan"})
} }
// list channels
for i, usr := range users {
type schan struct {
ID string `json:"channel_id"`
}
type chanlist struct {
Channels []schan `json:"channels"`
}
r0 := RequestAuthGet[chanlist](t, usr.AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/channels?selector=%s", usr.UID, "owned"))
users[i].Channels = langext.ArrMap(r0.Channels, func(v schan) string { return v.ID })
}
// list keys
for i, usr := range users {
type skey struct {
ID string `json:"keytoken_id"`
}
type keylist struct {
Keys []skey `json:"keys"`
}
r0 := RequestAuthGet[keylist](t, usr.AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/keys", usr.UID))
users[i].Keys = langext.ArrMap(r0.Keys, func(v skey) string { return v.ID })
}
// list subscriptions
for i, usr := range users {
type ssub struct {
ID string `json:"subscription_id"`
}
type sublist struct {
Subs []ssub `json:"subscriptions"`
}
r0 := RequestAuthGet[sublist](t, usr.AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/subscriptions?direction=%s&confirmation=%s", usr.UID, "outgoing", "confirmed"))
users[i].Subscriptions = langext.ArrMap(r0.Subs, func(v ssub) string { return v.ID })
}
// Sub/Unsub for Users 12+13 // Sub/Unsub for Users 12+13
{ {
@@ -463,18 +510,20 @@ func InitSingleData(t *testing.T, ws *logic.Application) SingleData {
success = true success = true
return SingleData{ sd := SingleData{
UID: r0.UserId, UID: r0.UserId,
AdminKey: r0.AdminKey, AdminKey: r0.AdminKey,
SendKey: r0.SendKey, SendKey: r0.SendKey,
ReadKey: r0.ReadKey, ReadKey: r0.ReadKey,
ClientID: r0.Clients[0].ClientId, ClientID: r0.Clients[0].ClientId,
} }
return sd
} }
func doSubscribe(t *testing.T, baseUrl string, user Userdat, chanOwner Userdat, chanInternalName string) { func doSubscribe(t *testing.T, baseUrl string, user Userdat, chanOwner Userdat, chanInternalName string) {
if user == chanOwner { if user.UID == chanOwner.UID {
RequestAuthPost[Void](t, user.AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/channels", user.UID), gin.H{ RequestAuthPost[Void](t, user.AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/channels", user.UID), gin.H{
"channel_owner_user_id": chanOwner.UID, "channel_owner_user_id": chanOwner.UID,
-3
View File
@@ -1,7 +1,6 @@
package util package util
import ( import (
"blackforestbytes.com/simplecloudnotifier/api/ginext"
"fmt" "fmt"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
@@ -13,7 +12,6 @@ func SetBufLogger() {
buflogger = &BufferWriter{cw: createConsoleWriter()} buflogger = &BufferWriter{cw: createConsoleWriter()}
log.Logger = createLogger(buflogger) log.Logger = createLogger(buflogger)
gin.SetMode(gin.ReleaseMode) gin.SetMode(gin.ReleaseMode)
ginext.SuppressGinLogs = true
} }
func ClearBufLogger(dump bool) { func ClearBufLogger(dump bool) {
@@ -24,7 +22,6 @@ func ClearBufLogger(dump bool) {
log.Logger = createLogger(createConsoleWriter()) log.Logger = createLogger(createConsoleWriter())
buflogger = nil buflogger = nil
gin.SetMode(gin.TestMode) gin.SetMode(gin.TestMode)
ginext.SuppressGinLogs = false
if !dump { if !dump {
log.Info().Msgf("Suppressed %d logmessages / printf-statements", size) log.Info().Msgf("Suppressed %d logmessages / printf-statements", size)
} }
+19 -3
View File
@@ -26,10 +26,18 @@ func RequestAuthGet[TResult any](t *testing.T, akey string, baseURL string, urlS
return RequestAny[TResult](t, akey, "GET", baseURL, urlSuffix, nil, true) return RequestAny[TResult](t, akey, "GET", baseURL, urlSuffix, nil, true)
} }
func RequestAuthGetRaw(t *testing.T, akey string, baseURL string, urlSuffix string) string {
return RequestAny[string](t, akey, "GET", baseURL, urlSuffix, nil, false)
}
func RequestPost[TResult any](t *testing.T, baseURL string, urlSuffix string, body any) TResult { func RequestPost[TResult any](t *testing.T, baseURL string, urlSuffix string, body any) TResult {
return RequestAny[TResult](t, "", "POST", baseURL, urlSuffix, body, true) return RequestAny[TResult](t, "", "POST", baseURL, urlSuffix, body, true)
} }
func RequestAuthPostRaw(t *testing.T, akey string, baseURL string, urlSuffix string, body any) string {
return RequestAny[string](t, akey, "POST", baseURL, urlSuffix, body, false)
}
func RequestAuthPost[TResult any](t *testing.T, akey string, baseURL string, urlSuffix string, body any) TResult { func RequestAuthPost[TResult any](t *testing.T, akey string, baseURL string, urlSuffix string, body any) TResult {
return RequestAny[TResult](t, akey, "POST", baseURL, urlSuffix, body, true) return RequestAny[TResult](t, akey, "POST", baseURL, urlSuffix, body, true)
} }
@@ -166,14 +174,22 @@ func RequestAny[TResult any](t *testing.T, akey string, method string, baseURL s
TestFailFmt(t, "Statuscode != 200 (actual = %d)", resp.StatusCode) TestFailFmt(t, "Statuscode != 200 (actual = %d)", resp.StatusCode)
} }
var data TResult
if deserialize { if deserialize {
var data TResult
if err := json.Unmarshal(respBodyBin, &data); err != nil { if err := json.Unmarshal(respBodyBin, &data); err != nil {
TestFailErr(t, err) TestFailErr(t, err)
return data
}
return data
} else {
if _, ok := (any(*new(TResult))).([]byte); ok {
return any(respBodyBin).(TResult)
} else if _, ok := (any(*new(TResult))).(string); ok {
return any(string(respBodyBin)).(TResult)
} else {
return *new(TResult)
} }
} }
return data
} }
func RequestAuthAnyShouldFail(t *testing.T, akey string, method string, baseURL string, urlSuffix string, body any, expectedStatusCode int, errcode apierr.APIError) { func RequestAuthAnyShouldFail(t *testing.T, akey string, method string, baseURL string, urlSuffix string, body any, expectedStatusCode int, errcode apierr.APIError) {
+176
View File
@@ -0,0 +1,176 @@
package util
import (
"encoding/json"
"fmt"
"gogs.mikescher.com/BlackForestBytes/goext/langext"
"reflect"
"testing"
"time"
)
func AssertJsonStructureMatch(t *testing.T, key string, jsonData string, expected map[string]any) {
realData := make(map[string]any)
err := json.Unmarshal([]byte(jsonData), &realData)
if err != nil {
t.Errorf("Failed to decode json of [%s]: %s", key, err.Error())
return
}
assertjsonStructureMatchMapObject(t, expected, realData, key)
}
func assertJsonStructureMatch(t *testing.T, schema any, realValue any, keyPath string) {
if strschema, ok := schema.(string); ok {
assertjsonStructureMatchSingleValue(t, strschema, realValue, keyPath)
} else if mapschema, ok := schema.(map[string]any); ok {
if reflect.ValueOf(realValue).Kind() != reflect.Map {
t.Errorf("Key < %s > is not a object (its actually %T: '%v')", keyPath, realValue, realValue)
return
}
if _, ok := realValue.(map[string]any); !ok {
t.Errorf("Key < %s > is not a object[recursive] (its actually %T: '%v')", keyPath, realValue, realValue)
return
}
assertjsonStructureMatchMapObject(t, mapschema, realValue.(map[string]any), keyPath)
} else if arrschema, ok := schema.([]any); ok && len(arrschema) == 1 {
if _, ok := realValue.([]any); !ok {
t.Errorf("Key < %s > is not a array[recursive] (its actually %T: '%v')", keyPath, realValue, realValue)
return
}
assertjsonStructureMatchArray(t, arrschema, realValue.([]any), keyPath)
} else {
t.Errorf("Unknown schema type '%s' for key < %s >", schema, keyPath)
}
}
func assertjsonStructureMatchSingleValue(t *testing.T, strschema string, realValue any, keyPath string) {
switch strschema {
case "id":
if _, ok := realValue.(string); !ok {
t.Errorf("Key < %s > is not a string<id> (its actually %T: '%v')", keyPath, realValue, realValue)
return
}
if len(realValue.(string)) != 24 { //TODO validate checksum?
t.Errorf("Key < %s > is not a valid entity-id date (its '%v')", keyPath, realValue)
return
}
case "string":
if _, ok := realValue.(string); !ok {
t.Errorf("Key < %s > is not a string (its actually %T: '%v')", keyPath, realValue, realValue)
return
}
case "null":
if !langext.IsNil(realValue) {
t.Errorf("Key < %s > is not a NULL (its actually %T: '%v')", keyPath, realValue, realValue)
return
}
case "string|null":
if langext.IsNil(realValue) {
return // OK
} else if _, ok := realValue.(string); !ok {
return // OK
} else {
t.Errorf("Key < %s > is not a string|null (its actually %T: '%v')", keyPath, realValue, realValue)
return
}
case "rfc3339":
if _, ok := realValue.(string); !ok {
t.Errorf("Key < %s > is not a string<rfc3339> (its actually %T: '%v')", keyPath, realValue, realValue)
return
}
if _, err := time.Parse(time.RFC3339, realValue.(string)); err != nil {
t.Errorf("Key < %s > is not a valid rfc3339 date (its '%v')", keyPath, realValue)
return
}
case "rfc3339|null":
if langext.IsNil(realValue) {
return // OK
}
if _, ok := realValue.(string); !ok {
t.Errorf("Key < %s > is not a string<rfc3339> (its actually %T: '%v')", keyPath, realValue, realValue)
return
}
if _, err := time.Parse(time.RFC3339, realValue.(string)); err != nil {
t.Errorf("Key < %s > is not a valid rfc3339 date (its '%v')", keyPath, realValue)
return
}
case "int":
if _, ok := realValue.(float64); !ok {
t.Errorf("Key < %s > is not a int (its actually %T: '%v')", keyPath, realValue, realValue)
return
}
if realValue.(float64) != float64(int(realValue.(float64))) {
t.Errorf("Key < %s > is not a int (its actually %T: '%v')", keyPath, realValue, realValue)
return
}
case "float":
if _, ok := realValue.(float64); !ok {
t.Errorf("Key < %s > is not a int (its actually %T: '%v')", keyPath, realValue, realValue)
return
}
case "bool":
if _, ok := realValue.(bool); !ok {
t.Errorf("Key < %s > is not a int (its actually %T: '%v')", keyPath, realValue, realValue)
return
}
default:
t.Errorf("Unknown schema type '%s' for key < %s >", strschema, keyPath)
return
}
}
func assertjsonStructureMatchMapObject(t *testing.T, mapschema map[string]any, realValue map[string]any, keyPath string) {
for k := range mapschema {
if _, ok := realValue[k]; !ok {
t.Errorf("Missing Key: < %s >", keyPath+"."+k)
}
}
for k := range realValue {
if _, ok := mapschema[k]; !ok {
t.Errorf("Additional key: < %s >", keyPath+"."+k)
}
}
for k, v := range realValue {
kpath := keyPath + "." + k
schema, ok := mapschema[k]
if !ok {
t.Errorf("Key < %s > is missing in response", kpath)
continue
}
assertJsonStructureMatch(t, schema, v, kpath)
}
}
func assertjsonStructureMatchArray(t *testing.T, arrschema []any, realValue []any, keyPath string) {
if len(arrschema) != 1 {
t.Errorf("Array schema must have exactly one element, but got %d", len(arrschema))
return
}
for i, realArrVal := range realValue {
assertJsonStructureMatch(t, arrschema[0], realArrVal, fmt.Sprintf("%s[%d]", keyPath, i))
}
}
+8 -2
View File
@@ -3,11 +3,11 @@ package util
import ( import (
scn "blackforestbytes.com/simplecloudnotifier" scn "blackforestbytes.com/simplecloudnotifier"
"blackforestbytes.com/simplecloudnotifier/api" "blackforestbytes.com/simplecloudnotifier/api"
"blackforestbytes.com/simplecloudnotifier/api/ginext"
"blackforestbytes.com/simplecloudnotifier/google" "blackforestbytes.com/simplecloudnotifier/google"
"blackforestbytes.com/simplecloudnotifier/jobs" "blackforestbytes.com/simplecloudnotifier/jobs"
"blackforestbytes.com/simplecloudnotifier/logic" "blackforestbytes.com/simplecloudnotifier/logic"
"blackforestbytes.com/simplecloudnotifier/push" "blackforestbytes.com/simplecloudnotifier/push"
"gogs.mikescher.com/BlackForestBytes/goext/ginext"
"gogs.mikescher.com/BlackForestBytes/goext/langext" "gogs.mikescher.com/BlackForestBytes/goext/langext"
"os" "os"
"path/filepath" "path/filepath"
@@ -88,7 +88,13 @@ func StartSimpleWebserver(t *testing.T) (*logic.Application, string, func()) {
TestFailErr(t, err) TestFailErr(t, err)
} }
ginengine := ginext.NewEngine(scn.Conf) ginengine := ginext.NewEngine(ginext.Options{
AllowCors: &scn.Conf.Cors,
GinDebug: &scn.Conf.GinDebug,
BufferBody: langext.PTrue,
Timeout: langext.Ptr(time.Duration(int64(scn.Conf.RequestTimeout) * int64(scn.Conf.RequestMaxRetry))),
BuildRequestBindError: logic.BuildGinRequestError,
})
router := api.NewRouter(app) router := api.NewRouter(app)