From 34f254ce6793f0fcf20b80d42eba4967d100d282 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B3=BD=E5=8D=8E?= Date: Mon, 9 Sep 2024 07:57:05 +0000 Subject: [PATCH] Add admin api/v1/users api --- api/router/api.go | 1 + builder/store/database/user.go | 19 ++++++++++ common/types/user.go | 2 +- docs/docs.go | 66 ++++++++++++++++++++++++++++++++++ docs/swagger.json | 66 ++++++++++++++++++++++++++++++++++ docs/swagger.yaml | 39 ++++++++++++++++++++ user/component/user.go | 41 +++++++++++++++++++++ user/handler/user.go | 36 +++++++++++++++++++ user/router/api.go | 2 ++ 9 files changed, 271 insertions(+), 1 deletion(-) diff --git a/api/router/api.go b/api/router/api.go index 0579c1d7..c7802248 100644 --- a/api/router/api.go +++ b/api/router/api.go @@ -229,6 +229,7 @@ func NewRouter(config *config.Config, enableSwagger bool) (*gin.Engine, error) { // JWT token apiGroup.POST("/jwt/token", needAPIKey, userProxyHandler.Proxy) apiGroup.GET("/jwt/:token", needAPIKey, userProxyHandler.ProxyToApi("/api/v1/jwt/%s", "token")) + apiGroup.GET("/users", userProxyHandler.Proxy) // callback callbackCtrl, err := callback.NewGitCallbackHandler(config) diff --git a/builder/store/database/user.go b/builder/store/database/user.go index 4cc9d55c..9db82681 100644 --- a/builder/store/database/user.go +++ b/builder/store/database/user.go @@ -88,6 +88,25 @@ func (s *UserStore) Index(ctx context.Context) (users []User, err error) { return } +func (s *UserStore) IndexWithSearch(ctx context.Context, search string, per, page int) (users []User, count int, err error) { + search = strings.ToLower(search) + query := s.db.Operator.Core.NewSelect(). + Model(&users) + if search != "" { + query.Where("LOWER(username) like ? OR LOWER(email) like ?", fmt.Sprintf("%%%s%%", search), fmt.Sprintf("%%%s%%", search)) + } + count, err = query.Count(ctx) + if err != nil { + return + } + query.Order("id asc").Limit(per).Offset((page - 1) * per) + err = query.Scan(ctx, &users) + if err != nil { + return + } + return +} + func (s *UserStore) FindByUsername(ctx context.Context, username string) (user User, err error) { user.Username = username err = s.db.Operator.Core.NewSelect().Model(&user).Where("username = ?", username).Scan(ctx) diff --git a/common/types/user.go b/common/types/user.go index fa385d19..4482a741 100644 --- a/common/types/user.go +++ b/common/types/user.go @@ -143,7 +143,7 @@ type User struct { Roles []string `json:"roles,omitempty"` LastLoginAt string `json:"last_login_at,omitempty"` Orgs []Organization `json:"orgs,omitempty"` - CanChangeUserName bool `json:"can_change_username"` + CanChangeUserName bool `json:"can_change_username,omitempty"` } type UserLikesRequest struct { diff --git a/docs/docs.go b/docs/docs.go index 2769603e..404ff677 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -10078,6 +10078,72 @@ const docTemplate = `{ } } }, + "/users": { + "get": { + "security": [ + { + "ApiKey": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "User" + ], + "summary": "Get users info. Only Admin", + "parameters": [ + { + "type": "string", + "description": "search", + "name": "search", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/types.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/types.User" + } + }, + "total": { + "type": "integer" + } + } + } + ] + } + }, + "400": { + "description": "Bad request", + "schema": { + "$ref": "#/definitions/types.APIBadRequest" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/types.APIInternalServerError" + } + } + } + } + }, "/{repo_type}/{namespace}/{name}/blob/{file_path}": { "get": { "security": [ diff --git a/docs/swagger.json b/docs/swagger.json index 68375715..90a6d837 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -10067,6 +10067,72 @@ } } }, + "/users": { + "get": { + "security": [ + { + "ApiKey": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "User" + ], + "summary": "Get users info. Only Admin", + "parameters": [ + { + "type": "string", + "description": "search", + "name": "search", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/types.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/types.User" + } + }, + "total": { + "type": "integer" + } + } + } + ] + } + }, + "400": { + "description": "Bad request", + "schema": { + "$ref": "#/definitions/types.APIBadRequest" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/types.APIInternalServerError" + } + } + } + } + }, "/{repo_type}/{namespace}/{name}/blob/{file_path}": { "get": { "security": [ diff --git a/docs/swagger.yaml b/docs/swagger.yaml index c09ed6d4..b82936ee 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -10211,6 +10211,45 @@ paths: summary: '[Deprecated: use DELETE:/token/{app}/{token_name} instead]' tags: - Access token + /users: + get: + consumes: + - application/json + parameters: + - description: search + in: query + name: search + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/types.Response' + - properties: + data: + items: + $ref: '#/definitions/types.User' + type: array + total: + type: integer + type: object + "400": + description: Bad request + schema: + $ref: '#/definitions/types.APIBadRequest' + "500": + description: Internal server error + schema: + $ref: '#/definitions/types.APIInternalServerError' + security: + - ApiKey: [] + summary: Get users info. Only Admin + tags: + - User securityDefinitions: ApiKey: description: Bearer token diff --git a/user/component/user.go b/user/component/user.go index bba0a612..3397158d 100644 --- a/user/component/user.go +++ b/user/component/user.go @@ -404,6 +404,47 @@ func (c *UserComponent) Get(ctx context.Context, userName, visitorName string) ( return &u, nil } +func (c *UserComponent) Index(ctx context.Context, visitorName, search string, per, page int) ([]*types.User, int, error) { + var ( + respUsers []*types.User + onlyBasicInfo bool + ) + canAdmin, err := c.CanAdmin(ctx, visitorName) + if err != nil { + return nil, 0, fmt.Errorf("failed to check visitor user permission, visitor: %s, error: %w", visitorName, err) + } + if !canAdmin { + onlyBasicInfo = true + } + + dbusers, count, err := c.us.IndexWithSearch(ctx, search, per, page) + if err != nil { + newError := fmt.Errorf("failed to find user by name in db,error:%w", err) + return nil, count, newError + } + + for _, dbuser := range dbusers { + user := &types.User{ + Username: dbuser.Username, + Nickname: dbuser.NickName, + Avatar: dbuser.Avatar, + } + + if !onlyBasicInfo { + user.Email = dbuser.Email + user.UUID = dbuser.UUID + user.Bio = dbuser.Bio + user.Homepage = dbuser.Homepage + user.Phone = dbuser.Phone + user.Roles = dbuser.Roles() + } + + respUsers = append(respUsers, user) + } + + return respUsers, count, nil +} + func (c *UserComponent) Signin(ctx context.Context, code, state string) (*types.JWTClaims, string, error) { c.lazyInit() diff --git a/user/handler/user.go b/user/handler/user.go index 51af9147..54939fdb 100644 --- a/user/handler/user.go +++ b/user/handler/user.go @@ -10,6 +10,7 @@ import ( "opencsg.com/csghub-server/api/httpbase" "opencsg.com/csghub-server/common/config" "opencsg.com/csghub-server/common/types" + "opencsg.com/csghub-server/common/utils/common" apicomponent "opencsg.com/csghub-server/component" "opencsg.com/csghub-server/user/component" ) @@ -173,6 +174,41 @@ func (h *UserHandler) Get(ctx *gin.Context) { httpbase.OK(ctx, user) } +// GetUsers godoc +// @Security ApiKey +// @Summary Get users info. Only Admin +// @Tags User +// @Accept json +// @Produce json +// @Param search query string true "search" +// @Success 200 {object} types.Response{data=[]types.User,total=int} "OK" +// @Failure 400 {object} types.APIBadRequest "Bad request" +// @Failure 500 {object} types.APIInternalServerError "Internal server error" +// @Router /users [get] +func (h *UserHandler) Index(ctx *gin.Context) { + visitorName := httpbase.GetCurrentUser(ctx) + search := ctx.Query("search") + per, page, err := common.GetPerAndPageFromContext(ctx) + if err != nil { + slog.Error("Failed to get per and page", slog.Any("error", err)) + httpbase.BadRequest(ctx, err.Error()) + return + } + users, count, err := h.c.Index(ctx, visitorName, search, per, page) + if err != nil { + slog.Error("Failed to get user", slog.Any("error", err)) + httpbase.ServerError(ctx, err) + return + } + respData := gin.H{ + "data": users, + "total": count, + } + + slog.Info("Get users succeed") + httpbase.OK(ctx, respData) +} + func (h *UserHandler) Casdoor(ctx *gin.Context) { code := ctx.Query("code") state := ctx.Query("state") diff --git a/user/router/api.go b/user/router/api.go index fde60cb1..0252f53c 100644 --- a/user/router/api.go +++ b/user/router/api.go @@ -94,6 +94,8 @@ func NewRouter(config *config.Config) (*gin.Engine, error) { // userGroup.DELETE("/:username", userMatch, userHandler.Delete) // get user's all tokens userGroup.GET("/:username/tokens", userMatch, acHandler.GetUserTokens) + // get user list + apiV1Group.GET("/users", mustLogin(), userHandler.Index) } // routers for organizations