UpdateUser() works

This commit is contained in:
Mike Schwörer 2022-11-18 23:28:37 +01:00
parent 55f53deadf
commit 35ef2175bc
Signed by: Mikescher
GPG Key ID: D3C7172E0A70F8CF
8 changed files with 329 additions and 44 deletions

View File

@ -7,7 +7,9 @@ import (
"blackforestbytes.com/simplecloudnotifier/logic" "blackforestbytes.com/simplecloudnotifier/logic"
"database/sql" "database/sql"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"gogs.mikescher.com/BlackForestBytes/goext/langext"
"net/http" "net/http"
"regexp"
) )
type APIHandler struct { type APIHandler struct {
@ -19,21 +21,21 @@ type APIHandler struct {
// @Summary Create a new user // @Summary Create a new user
// @ID api-user-create // @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 // @Success 200 {object} models.UserJSON
// @Failure 400 {object} ginresp.apiError // @Failure 400 {object} ginresp.apiError
// @Failure 500 {object} ginresp.apiError // @Failure 500 {object} ginresp.apiError
// //
// @Router /api-v2/user/ [POST] // @Router /api-v2/user/ [POST]
func (h APIHandler) CreateUser(g *gin.Context) ginresp.HTTPResponse { func (h APIHandler) CreateUser(g *gin.Context) ginresp.HTTPResponse {
type body struct { type body struct {
FCMToken string `form:"fcm_token"` FCMToken string `json:"fcm_token"`
ProToken *string `form:"pro_token"` ProToken *string `json:"pro_token"`
Username *string `form:"username"` Username *string `json:"username"`
AgentModel string `form:"agent_model"` AgentModel string `json:"agent_model"`
AgentVersion string `form:"agent_version"` AgentVersion string `json:"agent_version"`
ClientType string `form:"client_type"` ClientType string `json:"client_type"`
} }
var b body var b body
@ -49,17 +51,17 @@ func (h APIHandler) CreateUser(g *gin.Context) ginresp.HTTPResponse {
} else if b.ClientType == string(models.ClientTypeIOS) { } else if b.ClientType == string(models.ClientTypeIOS) {
clientType = models.ClientTypeIOS clientType = models.ClientTypeIOS
} else { } else {
return ginresp.InternAPIError(apierr.INVALID_CLIENTTYPE, "Invalid ClientType", nil) return ginresp.InternAPIError(400, apierr.INVALID_CLIENTTYPE, "Invalid ClientType", nil)
} }
if b.ProToken != nil { if b.ProToken != nil {
ptok, err := h.app.VerifyProToken(*b.ProToken) ptok, err := h.app.VerifyProToken(*b.ProToken)
if err != nil { 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 { 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) err := h.app.Database.ClearFCMTokens(ctx, b.FCMToken)
if err != nil { 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 { if b.ProToken != nil {
err := h.app.Database.ClearProTokens(ctx, b.FCMToken) err := h.app.Database.ClearProTokens(ctx, *b.ProToken)
if err != nil { 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) userobj, err := h.app.Database.CreateUser(ctx, readKey, sendKey, adminKey, b.ProToken, b.Username)
if err != nil { 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) _, err = h.app.Database.CreateClient(ctx, userobj.UserID, clientType, b.FCMToken, b.AgentModel, b.AgentVersion)
if err != nil { 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())) return ctx.FinishSuccess(ginresp.JSON(http.StatusOK, userobj.JSON()))
@ -94,11 +96,10 @@ func (h APIHandler) CreateUser(g *gin.Context) ginresp.HTTPResponse {
// GetUser swaggerdoc // GetUser swaggerdoc
// //
// @Summary Create a new user // @Summary Get a user (only self is allowed)
// @ID api-user-create // @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 // @Success 200 {object} models.UserJSON
// @Failure 400 {object} ginresp.apiError // @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) user, err := h.app.Database.GetUser(ctx, u.UserID)
if err == sql.ErrNoRows { 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 { 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())) 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 { 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 { func (h APIHandler) ListClients(g *gin.Context) ginresp.HTTPResponse {

View File

@ -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()}} 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 { 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 { } 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}}
} }
} }

View File

@ -174,3 +174,31 @@ func (db *Database) GetUser(ctx TxContext, userid int64) (models.User, error) {
return user, nil 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
}

View File

@ -99,19 +99,19 @@ func (app *Application) StartRequest(g *gin.Context, uri any, query any, body an
if body != nil { if body != nil {
if err := g.ShouldBindJSON(&body); err != 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 query != nil {
if err := g.ShouldBindQuery(&query); err != 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 uri != nil {
if err := g.ShouldBindUri(&uri); err != 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) perm, err := app.getPermissions(actx, authheader)
if err != nil { if err != nil {
cancel() 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 actx.permissions = perm

View File

@ -63,7 +63,7 @@ func (ac *AppContext) FinishSuccess(res ginresp.HTTPResponse) ginresp.HTTPRespon
if ac.transaction != nil { if ac.transaction != nil {
err := ac.transaction.Commit() err := ac.transaction.Commit()
if err != nil { 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 ac.transaction = nil
} }

View File

@ -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 { func (ac *AppContext) CheckPermissionUserRead(userid int64) *ginresp.HTTPResponse {
p := ac.permissions p := ac.permissions
@ -42,3 +42,12 @@ func (ac *AppContext) CheckPermissionUserRead(userid int64) *ginresp.HTTPRespons
return langext.Ptr(respoNotAuthorized) 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)
}

View File

@ -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": { "/api/ack.php": {
"get": { "get": {
"summary": "Acknowledge that a message was received", "summary": "Acknowledge that a message was received",
@ -482,19 +576,19 @@
"handler.CreateUser.body": { "handler.CreateUser.body": {
"type": "object", "type": "object",
"properties": { "properties": {
"agentModel": { "agent_model": {
"type": "string" "type": "string"
}, },
"agentVersion": { "agent_version": {
"type": "string" "type": "string"
}, },
"clientType": { "client_type": {
"type": "string" "type": "string"
}, },
"fcmtoken": { "fcm_token": {
"type": "string" "type": "string"
}, },
"proToken": { "pro_token": {
"type": "string" "type": "string"
}, },
"username": { "username": {
@ -645,6 +739,17 @@
} }
} }
}, },
"handler.UpdateUser.body": {
"type": "object",
"properties": {
"pro_token": {
"type": "string"
},
"username": {
"type": "string"
}
}
},
"handler.Upgrade.response": { "handler.Upgrade.response": {
"type": "object", "type": "object",
"properties": { "properties": {

View File

@ -25,15 +25,15 @@ definitions:
type: object type: object
handler.CreateUser.body: handler.CreateUser.body:
properties: properties:
agentModel: agent_model:
type: string type: string
agentVersion: agent_version:
type: string type: string
clientType: client_type:
type: string type: string
fcmtoken: fcm_token:
type: string type: string
proToken: pro_token:
type: string type: string
username: username:
type: string type: string
@ -131,6 +131,13 @@ definitions:
user_key: user_key:
type: string type: string
type: object type: object
handler.UpdateUser.body:
properties:
pro_token:
type: string
username:
type: string
type: object
handler.Upgrade.response: handler.Upgrade.response:
properties: properties:
data: data:
@ -253,6 +260,68 @@ paths:
schema: schema:
$ref: '#/definitions/ginresp.apiError' $ref: '#/definitions/ginresp.apiError'
summary: Create a new user 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: /api/ack.php:
get: get:
operationId: compat-ack operationId: compat-ack