diff --git a/server/Makefile b/server/Makefile index c2dcd2d..5effd70 100644 --- a/server/Makefile +++ b/server/Makefile @@ -28,7 +28,8 @@ build-docker: -t "$(DOCKER_REPO)/$(DOCKER_NAME):latest" \ . -build-swagger: +.PHONY: swagger +swagger: which swag || go install github.com/swaggo/swag/cmd/swag@latest swag init -generalInfo api/router.go --output ./swagger/ --outputTypes "json,yaml" diff --git a/server/api/handler/api.go b/server/api/handler/api.go index 6ea6f78..ef1fba9 100644 --- a/server/api/handler/api.go +++ b/server/api/handler/api.go @@ -96,7 +96,7 @@ func (h APIHandler) CreateUser(g *gin.Context) ginresp.HTTPResponse { // GetUser swaggerdoc // -// @Summary Get a user (only self is allowed) +// @Summary Get a user // @ID api-user-get // // @Param uid path int true "UserID" @@ -137,7 +137,7 @@ func (h APIHandler) GetUser(g *gin.Context) ginresp.HTTPResponse { // UpdateUser swaggerdoc // -// @Summary (Partially) update a user (only self allowed) +// @Summary (Partially) update a user // @Description The body-values are optional, only send the ones you want to update // @ID api-user-update // @@ -212,8 +212,47 @@ func (h APIHandler) UpdateUser(g *gin.Context) ginresp.HTTPResponse { return ctx.FinishSuccess(ginresp.JSON(http.StatusOK, user.JSON())) } +// ListClients swaggerdoc +// +// @Summary List all clients +// @ID api-clients-list +// +// @Param uid path int true "UserID" +// +// @Success 200 {object} handler.ListClients.result +// @Failure 400 {object} ginresp.apiError +// @Failure 401 {object} ginresp.apiError +// @Failure 404 {object} ginresp.apiError +// @Failure 500 {object} ginresp.apiError +// +// @Router /api-v2/user/{uid}/clients [GET] func (h APIHandler) ListClients(g *gin.Context) ginresp.HTTPResponse { - return ginresp.NotImplemented() + type uri struct { + UserID int64 `uri:"uid"` + } + type result struct { + Clients []models.ClientJSON `json:"clients"` + } + + var u uri + ctx, errResp := h.app.StartRequest(g, &u, nil, nil) + if errResp != nil { + return *errResp + } + defer ctx.Cancel() + + if permResp := ctx.CheckPermissionUserRead(u.UserID); permResp != nil { + return *permResp + } + + clients, err := h.app.Database.ListClients(ctx, u.UserID) + if err != nil { + return ginresp.InternAPIError(500, apierr.DATABASE_ERROR, "Failed to query user", err) + } + + res := langext.ArrMap(clients, func(v models.Client) models.ClientJSON { return v.JSON() }) + + return ctx.FinishSuccess(ginresp.JSON(http.StatusOK, result{Clients: res})) } func (h APIHandler) GetClient(g *gin.Context) ginresp.HTTPResponse { diff --git a/server/api/models/channel.go b/server/api/models/channel.go index 24ba15b..ce47f5b 100644 --- a/server/api/models/channel.go +++ b/server/api/models/channel.go @@ -3,6 +3,7 @@ package models import ( "database/sql" "github.com/blockloop/scan" + "gogs.mikescher.com/BlackForestBytes/goext/langext" "time" ) @@ -71,10 +72,19 @@ func (c ChannelDB) Model() Channel { } func DecodeChannel(r *sql.Rows) (Channel, error) { - var udb ChannelDB - err := scan.RowStrict(&udb, r) + var data ChannelDB + err := scan.RowStrict(&data, r) if err != nil { return Channel{}, err } - return udb.Model(), nil + return data.Model(), nil +} + +func DecodeChannels(r *sql.Rows) ([]Channel, error) { + var data []ChannelDB + err := scan.RowsStrict(&data, r) + if err != nil { + return nil, err + } + return langext.ArrMap(data, func(v ChannelDB) Channel { return v.Model() }), nil } diff --git a/server/api/models/client.go b/server/api/models/client.go index 544a745..7566fe3 100644 --- a/server/api/models/client.go +++ b/server/api/models/client.go @@ -1,6 +1,11 @@ package models -import "time" +import ( + "database/sql" + "github.com/blockloop/scan" + "gogs.mikescher.com/BlackForestBytes/goext/langext" + "time" +) type ClientType string @@ -19,5 +24,64 @@ type Client struct { AgentVersion string } -type ClientJSON struct { +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, + } +} + +type ClientJSON struct { + ClientID int64 `json:"client_id"` + UserID int64 `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"` +} + +type ClientDB struct { + ClientID int64 `db:"client_id"` + UserID int64 `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"` +} + +func (c ClientDB) Model() Client { + return Client{ + ClientID: c.ClientID, + UserID: c.UserID, + Type: c.Type, + FCMToken: c.FCMToken, + TimestampCreated: time.UnixMilli(c.TimestampCreated), + AgentModel: c.AgentModel, + AgentVersion: c.AgentVersion, + } +} + +func DecodeClient(r *sql.Rows) (Client, error) { + var data ClientDB + err := scan.RowStrict(&data, r) + if err != nil { + return Client{}, err + } + return data.Model(), nil +} + +func DecodeClients(r *sql.Rows) ([]Client, error) { + var data []ClientDB + err := scan.RowsStrict(&data, r) + if err != nil { + return nil, err + } + return langext.ArrMap(data, func(v ClientDB) Client { return v.Model() }), nil } diff --git a/server/api/models/user.go b/server/api/models/user.go index dcc4bea..60f474f 100644 --- a/server/api/models/user.go +++ b/server/api/models/user.go @@ -3,6 +3,7 @@ package models import ( "database/sql" "github.com/blockloop/scan" + "gogs.mikescher.com/BlackForestBytes/goext/langext" "time" ) @@ -88,10 +89,19 @@ func (u UserDB) Model() User { } func DecodeUser(r *sql.Rows) (User, error) { - var udb UserDB - err := scan.RowStrict(&udb, r) + var data UserDB + err := scan.RowStrict(&data, r) if err != nil { return User{}, err } - return udb.Model(), nil + return data.Model(), nil +} + +func DecodeUsers(r *sql.Rows) ([]User, error) { + var data []UserDB + err := scan.RowsStrict(&data, r) + if err != nil { + return nil, err + } + return langext.ArrMap(data, func(v UserDB) User { return v.Model() }), nil } diff --git a/server/common/ginresp/wrapper.go b/server/common/ginresp/wrapper.go index 32978f8..e4c4ad2 100644 --- a/server/common/ginresp/wrapper.go +++ b/server/common/ginresp/wrapper.go @@ -8,13 +8,17 @@ func Wrap(fn WHandlerFunc) gin.HandlerFunc { return func(g *gin.Context) { + reqctx := g.Request.Context() + wrap := fn(g) if g.Writer.Written() { panic("Writing in WrapperFunc is not supported") } - wrap.Write(g) + if reqctx.Err() == nil { + wrap.Write(g) + } } diff --git a/server/db/methods.go b/server/db/methods.go index 1dfbd86..d4f22ed 100644 --- a/server/db/methods.go +++ b/server/db/methods.go @@ -202,3 +202,22 @@ func (db *Database) UpdateUserProToken(ctx TxContext, userid int64, protoken *st return nil } + +func (db *Database) ListClients(ctx TxContext, userid int64) ([]models.Client, error) { + tx, err := ctx.GetOrCreateTransaction(db) + if err != nil { + return nil, err + } + + rows, err := tx.QueryContext(ctx, "SELECT * FROM clients WHERE user_id = ?", userid) + if err != nil { + return nil, err + } + + data, err := models.DecodeClients(rows) + if err != nil { + return nil, err + } + + return data, nil +} diff --git a/server/go.mod b/server/go.mod index 95461ab..3c1f01f 100644 --- a/server/go.mod +++ b/server/go.mod @@ -6,6 +6,7 @@ require ( github.com/gin-gonic/gin v1.8.1 github.com/rs/zerolog v1.28.0 github.com/swaggo/swag v1.8.7 + gogs.mikescher.com/BlackForestBytes/goext v0.0.18 ) require ( @@ -33,7 +34,6 @@ require ( github.com/modern-go/reflect2 v1.0.2 // indirect github.com/pelletier/go-toml/v2 v2.0.1 // indirect github.com/ugorji/go/codec v1.2.7 // indirect - gogs.mikescher.com/BlackForestBytes/goext v0.0.17 // indirect golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97 // indirect golang.org/x/net v0.0.0-20220722155237-a158d28d115b // indirect golang.org/x/sys v0.1.0 // indirect diff --git a/server/go.sum b/server/go.sum index 7446a8f..eb701c0 100644 --- a/server/go.sum +++ b/server/go.sum @@ -95,6 +95,8 @@ github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0 github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY= gogs.mikescher.com/BlackForestBytes/goext v0.0.17 h1:jsfbvII7aa0SH9qY0fnXBdtNnQe1YY3DgXDThEwLICc= gogs.mikescher.com/BlackForestBytes/goext v0.0.17/go.mod h1:TMBOjo3FRFh/GiTT0z3nwLmgcFJB87oSF2VMs4XUCTQ= +gogs.mikescher.com/BlackForestBytes/goext v0.0.18 h1:fprrLoAPGdI4ObveHR1DjiP9WhlTJppWtjqMA6ZkyS8= +gogs.mikescher.com/BlackForestBytes/goext v0.0.18/go.mod h1:TMBOjo3FRFh/GiTT0z3nwLmgcFJB87oSF2VMs4XUCTQ= golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97 h1:/UOmuWzQfxxo9UtlXMwuQU8CMgg1eZXqTRwkSQJWKOI= golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 h1:6zppjxzCulZykYSLyVDYbneBfbaBIQPYMevg0bEwv2s= diff --git a/server/logic/application.go b/server/logic/application.go index a01f3fa..089c91e 100644 --- a/server/logic/application.go +++ b/server/logic/application.go @@ -43,7 +43,7 @@ func (app *Application) Run() { errChan := make(chan error) go func() { - log.Info().Str("address", httpserver.Addr).Msg("HTTP-Server started") + log.Info().Str("address", httpserver.Addr).Msg("HTTP-Server started on http://localhost:" + app.Config.ServerPort) errChan <- httpserver.ListenAndServe() }() diff --git a/server/swagger/swagger.json b/server/swagger/swagger.json index 0a70c2f..e52c461 100644 --- a/server/swagger/swagger.json +++ b/server/swagger/swagger.json @@ -47,7 +47,7 @@ }, "/api-v2/user/{uid}": { "get": { - "summary": "Get a user (only self is allowed)", + "summary": "Get a user", "operationId": "api-user-get", "parameters": [ { @@ -93,7 +93,7 @@ }, "patch": { "description": "The body-values are optional, only send the ones you want to update", - "summary": "(Partially) update a user (only self allowed)", + "summary": "(Partially) update a user", "operationId": "api-user-update", "parameters": [ { @@ -139,6 +139,53 @@ } } }, + "/api-v2/user/{uid}/clients": { + "get": { + "summary": "List all clients", + "operationId": "api-clients-list", + "parameters": [ + { + "type": "integer", + "description": "UserID", + "name": "uid", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.ListClients.result" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/ginresp.apiError" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/ginresp.apiError" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/ginresp.apiError" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/ginresp.apiError" + } + } + } + } + }, "/api/ack.php": { "get": { "summary": "Acknowledge that a message was received", @@ -667,6 +714,17 @@ } } }, + "handler.ListClients.result": { + "type": "object", + "properties": { + "clients": { + "type": "array", + "items": { + "$ref": "#/definitions/models.ClientJSON" + } + } + } + }, "handler.Register.response": { "type": "object", "properties": { @@ -801,6 +859,32 @@ } } }, + "models.ClientJSON": { + "type": "object", + "properties": { + "agent_model": { + "type": "string" + }, + "agent_version": { + "type": "string" + }, + "client_id": { + "type": "integer" + }, + "fcm_token": { + "type": "string" + }, + "timestamp_created": { + "type": "string" + }, + "type": { + "type": "string" + }, + "user_id": { + "type": "integer" + } + } + }, "models.CompatMessage": { "type": "object", "properties": { diff --git a/server/swagger/swagger.yaml b/server/swagger/swagger.yaml index ddbdbe0..015c2f6 100644 --- a/server/swagger/swagger.yaml +++ b/server/swagger/swagger.yaml @@ -84,6 +84,13 @@ definitions: user_key: type: string type: object + handler.ListClients.result: + properties: + clients: + items: + $ref: '#/definitions/models.ClientJSON' + type: array + type: object handler.Register.response: properties: is_pro: @@ -171,6 +178,23 @@ definitions: uri: type: string type: object + models.ClientJSON: + properties: + agent_model: + type: string + agent_version: + type: string + client_id: + type: integer + fcm_token: + type: string + timestamp_created: + type: string + type: + type: string + user_id: + type: integer + type: object models.CompatMessage: properties: body: @@ -290,7 +314,7 @@ paths: description: Internal Server Error schema: $ref: '#/definitions/ginresp.apiError' - summary: Get a user (only self is allowed) + summary: Get a user patch: description: The body-values are optional, only send the ones you want to update operationId: api-user-update @@ -321,7 +345,38 @@ paths: description: Internal Server Error schema: $ref: '#/definitions/ginresp.apiError' - summary: (Partially) update a user (only self allowed) + summary: (Partially) update a user + /api-v2/user/{uid}/clients: + get: + operationId: api-clients-list + parameters: + - description: UserID + in: path + name: uid + required: true + type: integer + responses: + "200": + description: OK + schema: + $ref: '#/definitions/handler.ListClients.result' + "400": + description: Bad Request + schema: + $ref: '#/definitions/ginresp.apiError' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/ginresp.apiError' + "404": + description: Not Found + schema: + $ref: '#/definitions/ginresp.apiError' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/ginresp.apiError' + summary: List all clients /api/ack.php: get: operationId: compat-ack