From 35ef2175bc99c1456d27feaad17b9641e478e35d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mike=20Schw=C3=B6rer?= Date: Fri, 18 Nov 2022 23:28:37 +0100 Subject: [PATCH] UpdateUser() works --- server/api/handler/api.go | 124 +++++++++++++++++++++++++++------- server/common/ginresp/resp.go | 6 +- server/db/methods.go | 28 ++++++++ server/logic/application.go | 8 +-- server/logic/context.go | 2 +- server/logic/permissions.go | 11 ++- server/swagger/swagger.json | 115 +++++++++++++++++++++++++++++-- server/swagger/swagger.yaml | 79 ++++++++++++++++++++-- 8 files changed, 329 insertions(+), 44 deletions(-) diff --git a/server/api/handler/api.go b/server/api/handler/api.go index 5ab8bd6..6ea6f78 100644 --- a/server/api/handler/api.go +++ b/server/api/handler/api.go @@ -7,7 +7,9 @@ import ( "blackforestbytes.com/simplecloudnotifier/logic" "database/sql" "github.com/gin-gonic/gin" + "gogs.mikescher.com/BlackForestBytes/goext/langext" "net/http" + "regexp" ) type APIHandler struct { @@ -19,21 +21,21 @@ type APIHandler struct { // @Summary Create a new user // @ID api-user-create // -// @Param post_body body handler.CreateUser.body false " " +// @Param post_body body handler.CreateUser.body false " " // -// @Success 200 {object} models.UserJSON -// @Failure 400 {object} ginresp.apiError -// @Failure 500 {object} ginresp.apiError +// @Success 200 {object} models.UserJSON +// @Failure 400 {object} ginresp.apiError +// @Failure 500 {object} ginresp.apiError // // @Router /api-v2/user/ [POST] func (h APIHandler) CreateUser(g *gin.Context) ginresp.HTTPResponse { type body struct { - FCMToken string `form:"fcm_token"` - ProToken *string `form:"pro_token"` - Username *string `form:"username"` - AgentModel string `form:"agent_model"` - AgentVersion string `form:"agent_version"` - ClientType string `form:"client_type"` + FCMToken string `json:"fcm_token"` + ProToken *string `json:"pro_token"` + Username *string `json:"username"` + AgentModel string `json:"agent_model"` + AgentVersion string `json:"agent_version"` + ClientType string `json:"client_type"` } var b body @@ -49,17 +51,17 @@ func (h APIHandler) CreateUser(g *gin.Context) ginresp.HTTPResponse { } else if b.ClientType == string(models.ClientTypeIOS) { clientType = models.ClientTypeIOS } else { - return ginresp.InternAPIError(apierr.INVALID_CLIENTTYPE, "Invalid ClientType", nil) + return ginresp.InternAPIError(400, apierr.INVALID_CLIENTTYPE, "Invalid ClientType", nil) } if b.ProToken != nil { ptok, err := h.app.VerifyProToken(*b.ProToken) if err != nil { - return ginresp.InternAPIError(apierr.FAILED_VERIFY_PRO_TOKEN, "Failed to query purchase status", err) + return ginresp.InternAPIError(500, apierr.FAILED_VERIFY_PRO_TOKEN, "Failed to query purchase status", err) } if !ptok { - return ginresp.InternAPIError(apierr.INVALID_PRO_TOKEN, "Purchase token could not be verified", nil) + return ginresp.InternAPIError(400, apierr.INVALID_PRO_TOKEN, "Purchase token could not be verified", nil) } } @@ -69,24 +71,24 @@ func (h APIHandler) CreateUser(g *gin.Context) ginresp.HTTPResponse { err := h.app.Database.ClearFCMTokens(ctx, b.FCMToken) if err != nil { - return ginresp.InternAPIError(apierr.DATABASE_ERROR, "Failed to clear existing fcm tokens", err) + return ginresp.InternAPIError(500, apierr.DATABASE_ERROR, "Failed to clear existing fcm tokens", err) } if b.ProToken != nil { - err := h.app.Database.ClearProTokens(ctx, b.FCMToken) + err := h.app.Database.ClearProTokens(ctx, *b.ProToken) if err != nil { - return ginresp.InternAPIError(apierr.DATABASE_ERROR, "Failed to clear existing fcm tokens", err) + return ginresp.InternAPIError(500, apierr.DATABASE_ERROR, "Failed to clear existing fcm tokens", err) } } userobj, err := h.app.Database.CreateUser(ctx, readKey, sendKey, adminKey, b.ProToken, b.Username) if err != nil { - return ginresp.InternAPIError(apierr.DATABASE_ERROR, "Failed to create user in db", err) + return ginresp.InternAPIError(500, apierr.DATABASE_ERROR, "Failed to create user in db", err) } _, err = h.app.Database.CreateClient(ctx, userobj.UserID, clientType, b.FCMToken, b.AgentModel, b.AgentVersion) if err != nil { - return ginresp.InternAPIError(apierr.DATABASE_ERROR, "Failed to create user in db", err) + return ginresp.InternAPIError(500, apierr.DATABASE_ERROR, "Failed to create user in db", err) } return ctx.FinishSuccess(ginresp.JSON(http.StatusOK, userobj.JSON())) @@ -94,11 +96,10 @@ func (h APIHandler) CreateUser(g *gin.Context) ginresp.HTTPResponse { // GetUser swaggerdoc // -// @Summary Create a new user -// @ID api-user-create +// @Summary Get a user (only self is allowed) +// @ID api-user-get // -// @Param post_body body handler.CreateUser.body false " " -// @Param uid path int true "UserID" +// @Param uid path int true "UserID" // // @Success 200 {object} models.UserJSON // @Failure 400 {object} ginresp.apiError @@ -125,17 +126,90 @@ func (h APIHandler) GetUser(g *gin.Context) ginresp.HTTPResponse { user, err := h.app.Database.GetUser(ctx, u.UserID) if err == sql.ErrNoRows { - return ginresp.InternAPIError(apierr.USER_NOT_FOUND, "User not found", err) + return ginresp.InternAPIError(404, apierr.USER_NOT_FOUND, "User not found", err) } if err != nil { - return ginresp.InternAPIError(apierr.DATABASE_ERROR, "Failed to query user", err) + return ginresp.InternAPIError(500, apierr.DATABASE_ERROR, "Failed to query user", err) } return ctx.FinishSuccess(ginresp.JSON(http.StatusOK, user.JSON())) } +// UpdateUser swaggerdoc +// +// @Summary (Partially) update a user (only self allowed) +// @Description The body-values are optional, only send the ones you want to update +// @ID api-user-update +// +// @Param post_body body handler.UpdateUser.body false " " +// +// @Success 200 {object} models.UserJSON +// @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} [PATCH] func (h APIHandler) UpdateUser(g *gin.Context) ginresp.HTTPResponse { - return ginresp.NotImplemented() + type uri struct { + UserID int64 `uri:"uid"` + } + type body struct { + Username *string `json:"username"` + ProToken *string `json:"pro_token"` + } + + var u uri + var b body + ctx, errResp := h.app.StartRequest(g, &u, nil, &b) + if errResp != nil { + return *errResp + } + defer ctx.Cancel() + + if permResp := ctx.CheckPermissionUserAdmin(u.UserID); permResp != nil { + return *permResp + } + + if b.Username != nil { + username := langext.Ptr(regexp.MustCompile(`[[:alnum:]\-_]`).ReplaceAllString(*b.Username, "")) + if *username == "" { + username = nil + } + + err := h.app.Database.UpdateUserUsername(ctx, u.UserID, b.Username) + if err != nil { + return ginresp.InternAPIError(500, apierr.DATABASE_ERROR, "Failed to update user", err) + } + } + + if b.ProToken != nil { + ptok, err := h.app.VerifyProToken(*b.ProToken) + if err != nil { + return ginresp.InternAPIError(500, apierr.FAILED_VERIFY_PRO_TOKEN, "Failed to query purchase status", err) + } + + if !ptok { + return ginresp.InternAPIError(400, apierr.INVALID_PRO_TOKEN, "Purchase token could not be verified", nil) + } + + err = h.app.Database.ClearProTokens(ctx, *b.ProToken) + if err != nil { + return ginresp.InternAPIError(500, apierr.DATABASE_ERROR, "Failed to clear existing fcm tokens", err) + } + + err = h.app.Database.UpdateUserProToken(ctx, u.UserID, b.ProToken) + if err != nil { + return ginresp.InternAPIError(500, apierr.DATABASE_ERROR, "Failed to update user", err) + } + } + + user, err := h.app.Database.GetUser(ctx, u.UserID) + if err != nil { + return ginresp.InternAPIError(500, apierr.DATABASE_ERROR, "Failed to query (updated) user", err) + } + + return ctx.FinishSuccess(ginresp.JSON(http.StatusOK, user.JSON())) } func (h APIHandler) ListClients(g *gin.Context) ginresp.HTTPResponse { diff --git a/server/common/ginresp/resp.go b/server/common/ginresp/resp.go index f1284fb..deea489 100644 --- a/server/common/ginresp/resp.go +++ b/server/common/ginresp/resp.go @@ -77,11 +77,11 @@ func InternalError(e error) HTTPResponse { return &errHTTPResponse{statusCode: http.StatusInternalServerError, data: apiError{Success: false, Error: int(apierr.INTERNAL_EXCEPTION), Message: e.Error()}} } -func InternAPIError(errorid apierr.APIError, msg string, e error) HTTPResponse { +func InternAPIError(status int, errorid apierr.APIError, msg string, e error) HTTPResponse { if scn.Conf.ReturnRawErrors { - return &errHTTPResponse{statusCode: http.StatusInternalServerError, data: apiError{Success: false, Error: int(errorid), Message: msg, RawError: e}} + return &errHTTPResponse{statusCode: status, data: apiError{Success: false, Error: int(errorid), Message: msg, RawError: e}} } else { - return &errHTTPResponse{statusCode: http.StatusInternalServerError, data: apiError{Success: false, Error: int(errorid), Message: msg}} + return &errHTTPResponse{statusCode: status, data: apiError{Success: false, Error: int(errorid), Message: msg}} } } diff --git a/server/db/methods.go b/server/db/methods.go index 26f1185..1dfbd86 100644 --- a/server/db/methods.go +++ b/server/db/methods.go @@ -174,3 +174,31 @@ func (db *Database) GetUser(ctx TxContext, userid int64) (models.User, error) { return user, nil } + +func (db *Database) UpdateUserUsername(ctx TxContext, userid int64, username *string) error { + tx, err := ctx.GetOrCreateTransaction(db) + if err != nil { + return err + } + + _, err = tx.ExecContext(ctx, "UPDATE users SET username = ? WHERE user_id = ?", username, userid) + if err != nil { + return err + } + + return nil +} + +func (db *Database) UpdateUserProToken(ctx TxContext, userid int64, protoken *string) error { + tx, err := ctx.GetOrCreateTransaction(db) + if err != nil { + return err + } + + _, err = tx.ExecContext(ctx, "UPDATE users SET pro_token = ? AND is_pro = ? WHERE user_id = ?", protoken, bool2DB(protoken != nil), userid) + if err != nil { + return err + } + + return nil +} diff --git a/server/logic/application.go b/server/logic/application.go index 72d2c74..a01f3fa 100644 --- a/server/logic/application.go +++ b/server/logic/application.go @@ -99,19 +99,19 @@ func (app *Application) StartRequest(g *gin.Context, uri any, query any, body an if body != nil { if err := g.ShouldBindJSON(&body); err != nil { - return nil, langext.Ptr(ginresp.InternAPIError(apierr.BINDFAIL_BODY_PARAM, "Failed to read body", err)) + return nil, langext.Ptr(ginresp.InternAPIError(400, apierr.BINDFAIL_BODY_PARAM, "Failed to read body", err)) } } if query != nil { if err := g.ShouldBindQuery(&query); err != nil { - return nil, langext.Ptr(ginresp.InternAPIError(apierr.BINDFAIL_QUERY_PARAM, "Failed to read query", err)) + return nil, langext.Ptr(ginresp.InternAPIError(400, apierr.BINDFAIL_QUERY_PARAM, "Failed to read query", err)) } } if uri != nil { if err := g.ShouldBindUri(&uri); err != nil { - return nil, langext.Ptr(ginresp.InternAPIError(apierr.BINDFAIL_URI_PARAM, "Failed to read uri", err)) + return nil, langext.Ptr(ginresp.InternAPIError(400, apierr.BINDFAIL_URI_PARAM, "Failed to read uri", err)) } } @@ -123,7 +123,7 @@ func (app *Application) StartRequest(g *gin.Context, uri any, query any, body an perm, err := app.getPermissions(actx, authheader) if err != nil { cancel() - return nil, langext.Ptr(ginresp.InternAPIError(apierr.PERM_QUERY_FAIL, "Failed to determine permissions", err)) + return nil, langext.Ptr(ginresp.InternAPIError(400, apierr.PERM_QUERY_FAIL, "Failed to determine permissions", err)) } actx.permissions = perm diff --git a/server/logic/context.go b/server/logic/context.go index a733473..ae907fc 100644 --- a/server/logic/context.go +++ b/server/logic/context.go @@ -63,7 +63,7 @@ func (ac *AppContext) FinishSuccess(res ginresp.HTTPResponse) ginresp.HTTPRespon if ac.transaction != nil { err := ac.transaction.Commit() if err != nil { - return ginresp.InternAPIError(apierr.COMMIT_FAILED, "Failed to comit changes to DB", err) + return ginresp.InternAPIError(500, apierr.COMMIT_FAILED, "Failed to comit changes to DB", err) } ac.transaction = nil } diff --git a/server/logic/permissions.go b/server/logic/permissions.go index eee80df..6094937 100644 --- a/server/logic/permissions.go +++ b/server/logic/permissions.go @@ -29,7 +29,7 @@ func NewEmptyPermissions() PermissionSet { } } -var respoNotAuthorized = ginresp.InternAPIError(apierr.USER_AUTH_FAILED, "You are not authorized for this action", nil) +var respoNotAuthorized = ginresp.InternAPIError(401, apierr.USER_AUTH_FAILED, "You are not authorized for this action", nil) func (ac *AppContext) CheckPermissionUserRead(userid int64) *ginresp.HTTPResponse { p := ac.permissions @@ -42,3 +42,12 @@ func (ac *AppContext) CheckPermissionUserRead(userid int64) *ginresp.HTTPRespons return langext.Ptr(respoNotAuthorized) } + +func (ac *AppContext) CheckPermissionUserAdmin(userid int64) *ginresp.HTTPResponse { + p := ac.permissions + if p.ReferenceID != nil && *p.ReferenceID == userid && p.KeyType == PermKeyTypeUserAdmin { + return nil + } + + return langext.Ptr(respoNotAuthorized) +} diff --git a/server/swagger/swagger.json b/server/swagger/swagger.json index df83e3a..0a70c2f 100644 --- a/server/swagger/swagger.json +++ b/server/swagger/swagger.json @@ -45,6 +45,100 @@ } } }, + "/api-v2/user/{uid}": { + "get": { + "summary": "Get a user (only self is allowed)", + "operationId": "api-user-get", + "parameters": [ + { + "type": "integer", + "description": "UserID", + "name": "uid", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.UserJSON" + } + }, + "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" + } + } + } + }, + "patch": { + "description": "The body-values are optional, only send the ones you want to update", + "summary": "(Partially) update a user (only self allowed)", + "operationId": "api-user-update", + "parameters": [ + { + "description": " ", + "name": "post_body", + "in": "body", + "schema": { + "$ref": "#/definitions/handler.UpdateUser.body" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.UserJSON" + } + }, + "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", @@ -482,19 +576,19 @@ "handler.CreateUser.body": { "type": "object", "properties": { - "agentModel": { + "agent_model": { "type": "string" }, - "agentVersion": { + "agent_version": { "type": "string" }, - "clientType": { + "client_type": { "type": "string" }, - "fcmtoken": { + "fcm_token": { "type": "string" }, - "proToken": { + "pro_token": { "type": "string" }, "username": { @@ -645,6 +739,17 @@ } } }, + "handler.UpdateUser.body": { + "type": "object", + "properties": { + "pro_token": { + "type": "string" + }, + "username": { + "type": "string" + } + } + }, "handler.Upgrade.response": { "type": "object", "properties": { diff --git a/server/swagger/swagger.yaml b/server/swagger/swagger.yaml index bd20c7b..ddbdbe0 100644 --- a/server/swagger/swagger.yaml +++ b/server/swagger/swagger.yaml @@ -25,15 +25,15 @@ definitions: type: object handler.CreateUser.body: properties: - agentModel: + agent_model: type: string - agentVersion: + agent_version: type: string - clientType: + client_type: type: string - fcmtoken: + fcm_token: type: string - proToken: + pro_token: type: string username: type: string @@ -131,6 +131,13 @@ definitions: user_key: type: string type: object + handler.UpdateUser.body: + properties: + pro_token: + type: string + username: + type: string + type: object handler.Upgrade.response: properties: data: @@ -253,6 +260,68 @@ paths: schema: $ref: '#/definitions/ginresp.apiError' summary: Create a new user + /api-v2/user/{uid}: + get: + operationId: api-user-get + parameters: + - description: UserID + in: path + name: uid + required: true + type: integer + responses: + "200": + description: OK + schema: + $ref: '#/definitions/models.UserJSON' + "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: Get a user (only self is allowed) + patch: + description: The body-values are optional, only send the ones you want to update + operationId: api-user-update + parameters: + - description: ' ' + in: body + name: post_body + schema: + $ref: '#/definitions/handler.UpdateUser.body' + responses: + "200": + description: OK + schema: + $ref: '#/definitions/models.UserJSON' + "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: (Partially) update a user (only self allowed) /api/ack.php: get: operationId: compat-ack