diff options
Diffstat (limited to 'routers/api/v1/user')
-rw-r--r-- | routers/api/v1/user/action.go | 353 | ||||
-rw-r--r-- | routers/api/v1/user/app.go | 410 | ||||
-rw-r--r-- | routers/api/v1/user/avatar.go | 65 | ||||
-rw-r--r-- | routers/api/v1/user/email.go | 132 | ||||
-rw-r--r-- | routers/api/v1/user/follower.go | 263 | ||||
-rw-r--r-- | routers/api/v1/user/gpg_key.go | 311 | ||||
-rw-r--r-- | routers/api/v1/user/helper.go | 35 | ||||
-rw-r--r-- | routers/api/v1/user/hook.go | 159 | ||||
-rw-r--r-- | routers/api/v1/user/key.go | 303 | ||||
-rw-r--r-- | routers/api/v1/user/quota.go | 118 | ||||
-rw-r--r-- | routers/api/v1/user/repo.go | 186 | ||||
-rw-r--r-- | routers/api/v1/user/runners.go | 26 | ||||
-rw-r--r-- | routers/api/v1/user/settings.go | 67 | ||||
-rw-r--r-- | routers/api/v1/user/star.go | 197 | ||||
-rw-r--r-- | routers/api/v1/user/user.go | 306 | ||||
-rw-r--r-- | routers/api/v1/user/watch.go | 211 |
16 files changed, 3142 insertions, 0 deletions
diff --git a/routers/api/v1/user/action.go b/routers/api/v1/user/action.go new file mode 100644 index 0000000..bf78c2c --- /dev/null +++ b/routers/api/v1/user/action.go @@ -0,0 +1,353 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package user + +import ( + "errors" + "net/http" + + actions_model "code.gitea.io/gitea/models/actions" + "code.gitea.io/gitea/models/db" + api "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/modules/util" + "code.gitea.io/gitea/modules/web" + "code.gitea.io/gitea/routers/api/v1/utils" + actions_service "code.gitea.io/gitea/services/actions" + "code.gitea.io/gitea/services/context" + secret_service "code.gitea.io/gitea/services/secrets" +) + +// create or update one secret of the user scope +func CreateOrUpdateSecret(ctx *context.APIContext) { + // swagger:operation PUT /user/actions/secrets/{secretname} user updateUserSecret + // --- + // summary: Create or Update a secret value in a user scope + // consumes: + // - application/json + // produces: + // - application/json + // parameters: + // - name: secretname + // in: path + // description: name of the secret + // type: string + // required: true + // - name: body + // in: body + // schema: + // "$ref": "#/definitions/CreateOrUpdateSecretOption" + // responses: + // "201": + // description: response when creating a secret + // "204": + // description: response when updating a secret + // "400": + // "$ref": "#/responses/error" + // "404": + // "$ref": "#/responses/notFound" + + opt := web.GetForm(ctx).(*api.CreateOrUpdateSecretOption) + + _, created, err := secret_service.CreateOrUpdateSecret(ctx, ctx.Doer.ID, 0, ctx.Params("secretname"), opt.Data) + if err != nil { + if errors.Is(err, util.ErrInvalidArgument) { + ctx.Error(http.StatusBadRequest, "CreateOrUpdateSecret", err) + } else if errors.Is(err, util.ErrNotExist) { + ctx.Error(http.StatusNotFound, "CreateOrUpdateSecret", err) + } else { + ctx.Error(http.StatusInternalServerError, "CreateOrUpdateSecret", err) + } + return + } + + if created { + ctx.Status(http.StatusCreated) + } else { + ctx.Status(http.StatusNoContent) + } +} + +// DeleteSecret delete one secret of the user scope +func DeleteSecret(ctx *context.APIContext) { + // swagger:operation DELETE /user/actions/secrets/{secretname} user deleteUserSecret + // --- + // summary: Delete a secret in a user scope + // consumes: + // - application/json + // produces: + // - application/json + // parameters: + // - name: secretname + // in: path + // description: name of the secret + // type: string + // required: true + // responses: + // "204": + // description: delete one secret of the user + // "400": + // "$ref": "#/responses/error" + // "404": + // "$ref": "#/responses/notFound" + + err := secret_service.DeleteSecretByName(ctx, ctx.Doer.ID, 0, ctx.Params("secretname")) + if err != nil { + if errors.Is(err, util.ErrInvalidArgument) { + ctx.Error(http.StatusBadRequest, "DeleteSecret", err) + } else if errors.Is(err, util.ErrNotExist) { + ctx.Error(http.StatusNotFound, "DeleteSecret", err) + } else { + ctx.Error(http.StatusInternalServerError, "DeleteSecret", err) + } + return + } + + ctx.Status(http.StatusNoContent) +} + +// CreateVariable create a user-level variable +func CreateVariable(ctx *context.APIContext) { + // swagger:operation POST /user/actions/variables/{variablename} user createUserVariable + // --- + // summary: Create a user-level variable + // consumes: + // - application/json + // produces: + // - application/json + // parameters: + // - name: variablename + // in: path + // description: name of the variable + // type: string + // required: true + // - name: body + // in: body + // schema: + // "$ref": "#/definitions/CreateVariableOption" + // responses: + // "201": + // description: response when creating a variable + // "204": + // description: response when creating a variable + // "400": + // "$ref": "#/responses/error" + // "404": + // "$ref": "#/responses/notFound" + + opt := web.GetForm(ctx).(*api.CreateVariableOption) + + ownerID := ctx.Doer.ID + variableName := ctx.Params("variablename") + + v, err := actions_service.GetVariable(ctx, actions_model.FindVariablesOpts{ + OwnerID: ownerID, + Name: variableName, + }) + if err != nil && !errors.Is(err, util.ErrNotExist) { + ctx.Error(http.StatusInternalServerError, "GetVariable", err) + return + } + if v != nil && v.ID > 0 { + ctx.Error(http.StatusConflict, "VariableNameAlreadyExists", util.NewAlreadyExistErrorf("variable name %s already exists", variableName)) + return + } + + if _, err := actions_service.CreateVariable(ctx, ownerID, 0, variableName, opt.Value); err != nil { + if errors.Is(err, util.ErrInvalidArgument) { + ctx.Error(http.StatusBadRequest, "CreateVariable", err) + } else { + ctx.Error(http.StatusInternalServerError, "CreateVariable", err) + } + return + } + + ctx.Status(http.StatusNoContent) +} + +// UpdateVariable update a user-level variable which is created by current doer +func UpdateVariable(ctx *context.APIContext) { + // swagger:operation PUT /user/actions/variables/{variablename} user updateUserVariable + // --- + // summary: Update a user-level variable which is created by current doer + // consumes: + // - application/json + // produces: + // - application/json + // parameters: + // - name: variablename + // in: path + // description: name of the variable + // type: string + // required: true + // - name: body + // in: body + // schema: + // "$ref": "#/definitions/UpdateVariableOption" + // responses: + // "201": + // description: response when updating a variable + // "204": + // description: response when updating a variable + // "400": + // "$ref": "#/responses/error" + // "404": + // "$ref": "#/responses/notFound" + + opt := web.GetForm(ctx).(*api.UpdateVariableOption) + + v, err := actions_service.GetVariable(ctx, actions_model.FindVariablesOpts{ + OwnerID: ctx.Doer.ID, + Name: ctx.Params("variablename"), + }) + if err != nil { + if errors.Is(err, util.ErrNotExist) { + ctx.Error(http.StatusNotFound, "GetVariable", err) + } else { + ctx.Error(http.StatusInternalServerError, "GetVariable", err) + } + return + } + + if opt.Name == "" { + opt.Name = ctx.Params("variablename") + } + if _, err := actions_service.UpdateVariable(ctx, v.ID, opt.Name, opt.Value); err != nil { + if errors.Is(err, util.ErrInvalidArgument) { + ctx.Error(http.StatusBadRequest, "UpdateVariable", err) + } else { + ctx.Error(http.StatusInternalServerError, "UpdateVariable", err) + } + return + } + + ctx.Status(http.StatusNoContent) +} + +// DeleteVariable delete a user-level variable which is created by current doer +func DeleteVariable(ctx *context.APIContext) { + // swagger:operation DELETE /user/actions/variables/{variablename} user deleteUserVariable + // --- + // summary: Delete a user-level variable which is created by current doer + // produces: + // - application/json + // parameters: + // - name: variablename + // in: path + // description: name of the variable + // type: string + // required: true + // responses: + // "201": + // description: response when deleting a variable + // "204": + // description: response when deleting a variable + // "400": + // "$ref": "#/responses/error" + // "404": + // "$ref": "#/responses/notFound" + + if err := actions_service.DeleteVariableByName(ctx, ctx.Doer.ID, 0, ctx.Params("variablename")); err != nil { + if errors.Is(err, util.ErrInvalidArgument) { + ctx.Error(http.StatusBadRequest, "DeleteVariableByName", err) + } else if errors.Is(err, util.ErrNotExist) { + ctx.Error(http.StatusNotFound, "DeleteVariableByName", err) + } else { + ctx.Error(http.StatusInternalServerError, "DeleteVariableByName", err) + } + return + } + + ctx.Status(http.StatusNoContent) +} + +// GetVariable get a user-level variable which is created by current doer +func GetVariable(ctx *context.APIContext) { + // swagger:operation GET /user/actions/variables/{variablename} user getUserVariable + // --- + // summary: Get a user-level variable which is created by current doer + // produces: + // - application/json + // parameters: + // - name: variablename + // in: path + // description: name of the variable + // type: string + // required: true + // responses: + // "200": + // "$ref": "#/responses/ActionVariable" + // "400": + // "$ref": "#/responses/error" + // "404": + // "$ref": "#/responses/notFound" + + v, err := actions_service.GetVariable(ctx, actions_model.FindVariablesOpts{ + OwnerID: ctx.Doer.ID, + Name: ctx.Params("variablename"), + }) + if err != nil { + if errors.Is(err, util.ErrNotExist) { + ctx.Error(http.StatusNotFound, "GetVariable", err) + } else { + ctx.Error(http.StatusInternalServerError, "GetVariable", err) + } + return + } + + variable := &api.ActionVariable{ + OwnerID: v.OwnerID, + RepoID: v.RepoID, + Name: v.Name, + Data: v.Data, + } + + ctx.JSON(http.StatusOK, variable) +} + +// ListVariables list user-level variables +func ListVariables(ctx *context.APIContext) { + // swagger:operation GET /user/actions/variables user getUserVariablesList + // --- + // summary: Get the user-level list of variables which is created by current doer + // produces: + // - application/json + // parameters: + // - name: page + // in: query + // description: page number of results to return (1-based) + // type: integer + // - name: limit + // in: query + // description: page size of results + // type: integer + // responses: + // "200": + // "$ref": "#/responses/VariableList" + // "400": + // "$ref": "#/responses/error" + // "404": + // "$ref": "#/responses/notFound" + + vars, count, err := db.FindAndCount[actions_model.ActionVariable](ctx, &actions_model.FindVariablesOpts{ + OwnerID: ctx.Doer.ID, + ListOptions: utils.GetListOptions(ctx), + }) + if err != nil { + ctx.Error(http.StatusInternalServerError, "FindVariables", err) + return + } + + variables := make([]*api.ActionVariable, len(vars)) + for i, v := range vars { + variables[i] = &api.ActionVariable{ + OwnerID: v.OwnerID, + RepoID: v.RepoID, + Name: v.Name, + Data: v.Data, + } + } + + ctx.SetTotalCountHeader(count) + ctx.JSON(http.StatusOK, variables) +} diff --git a/routers/api/v1/user/app.go b/routers/api/v1/user/app.go new file mode 100644 index 0000000..d5b20f7 --- /dev/null +++ b/routers/api/v1/user/app.go @@ -0,0 +1,410 @@ +// Copyright 2014 The Gogs Authors. All rights reserved. +// Copyright 2018 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package user + +import ( + "errors" + "fmt" + "net/http" + "strconv" + "strings" + + auth_model "code.gitea.io/gitea/models/auth" + "code.gitea.io/gitea/models/db" + api "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/modules/web" + "code.gitea.io/gitea/routers/api/v1/utils" + "code.gitea.io/gitea/services/context" + "code.gitea.io/gitea/services/convert" +) + +// ListAccessTokens list all the access tokens +func ListAccessTokens(ctx *context.APIContext) { + // swagger:operation GET /users/{username}/tokens user userGetTokens + // --- + // summary: List the authenticated user's access tokens + // produces: + // - application/json + // parameters: + // - name: username + // in: path + // description: username of user + // type: string + // required: true + // - name: page + // in: query + // description: page number of results to return (1-based) + // type: integer + // - name: limit + // in: query + // description: page size of results + // type: integer + // responses: + // "200": + // "$ref": "#/responses/AccessTokenList" + // "403": + // "$ref": "#/responses/forbidden" + + opts := auth_model.ListAccessTokensOptions{UserID: ctx.ContextUser.ID, ListOptions: utils.GetListOptions(ctx)} + + tokens, count, err := db.FindAndCount[auth_model.AccessToken](ctx, opts) + if err != nil { + ctx.InternalServerError(err) + return + } + + apiTokens := make([]*api.AccessToken, len(tokens)) + for i := range tokens { + apiTokens[i] = &api.AccessToken{ + ID: tokens[i].ID, + Name: tokens[i].Name, + TokenLastEight: tokens[i].TokenLastEight, + Scopes: tokens[i].Scope.StringSlice(), + } + } + + ctx.SetTotalCountHeader(count) + ctx.JSON(http.StatusOK, &apiTokens) +} + +// CreateAccessToken create access tokens +func CreateAccessToken(ctx *context.APIContext) { + // swagger:operation POST /users/{username}/tokens user userCreateToken + // --- + // summary: Create an access token + // consumes: + // - application/json + // produces: + // - application/json + // parameters: + // - name: username + // in: path + // description: username of user + // required: true + // type: string + // - name: body + // in: body + // schema: + // "$ref": "#/definitions/CreateAccessTokenOption" + // responses: + // "201": + // "$ref": "#/responses/AccessToken" + // "400": + // "$ref": "#/responses/error" + // "403": + // "$ref": "#/responses/forbidden" + + form := web.GetForm(ctx).(*api.CreateAccessTokenOption) + + t := &auth_model.AccessToken{ + UID: ctx.ContextUser.ID, + Name: form.Name, + } + + exist, err := auth_model.AccessTokenByNameExists(ctx, t) + if err != nil { + ctx.InternalServerError(err) + return + } + if exist { + ctx.Error(http.StatusBadRequest, "AccessTokenByNameExists", errors.New("access token name has been used already")) + return + } + + scope, err := auth_model.AccessTokenScope(strings.Join(form.Scopes, ",")).Normalize() + if err != nil { + ctx.Error(http.StatusBadRequest, "AccessTokenScope.Normalize", fmt.Errorf("invalid access token scope provided: %w", err)) + return + } + if scope == "" { + ctx.Error(http.StatusBadRequest, "AccessTokenScope", "access token must have a scope") + return + } + t.Scope = scope + + if err := auth_model.NewAccessToken(ctx, t); err != nil { + ctx.Error(http.StatusInternalServerError, "NewAccessToken", err) + return + } + ctx.JSON(http.StatusCreated, &api.AccessToken{ + Name: t.Name, + Token: t.Token, + ID: t.ID, + TokenLastEight: t.TokenLastEight, + Scopes: t.Scope.StringSlice(), + }) +} + +// DeleteAccessToken delete access tokens +func DeleteAccessToken(ctx *context.APIContext) { + // swagger:operation DELETE /users/{username}/tokens/{token} user userDeleteAccessToken + // --- + // summary: delete an access token + // produces: + // - application/json + // parameters: + // - name: username + // in: path + // description: username of user + // type: string + // required: true + // - name: token + // in: path + // description: token to be deleted, identified by ID and if not available by name + // type: string + // required: true + // responses: + // "204": + // "$ref": "#/responses/empty" + // "403": + // "$ref": "#/responses/forbidden" + // "404": + // "$ref": "#/responses/notFound" + // "422": + // "$ref": "#/responses/error" + + token := ctx.Params(":id") + tokenID, _ := strconv.ParseInt(token, 0, 64) + + if tokenID == 0 { + tokens, err := db.Find[auth_model.AccessToken](ctx, auth_model.ListAccessTokensOptions{ + Name: token, + UserID: ctx.ContextUser.ID, + }) + if err != nil { + ctx.Error(http.StatusInternalServerError, "ListAccessTokens", err) + return + } + + switch len(tokens) { + case 0: + ctx.NotFound() + return + case 1: + tokenID = tokens[0].ID + default: + ctx.Error(http.StatusUnprocessableEntity, "DeleteAccessTokenByID", fmt.Errorf("multiple matches for token name '%s'", token)) + return + } + } + if tokenID == 0 { + ctx.Error(http.StatusInternalServerError, "Invalid TokenID", nil) + return + } + + if err := auth_model.DeleteAccessTokenByID(ctx, tokenID, ctx.ContextUser.ID); err != nil { + if auth_model.IsErrAccessTokenNotExist(err) { + ctx.NotFound() + } else { + ctx.Error(http.StatusInternalServerError, "DeleteAccessTokenByID", err) + } + return + } + + ctx.Status(http.StatusNoContent) +} + +// CreateOauth2Application is the handler to create a new OAuth2 Application for the authenticated user +func CreateOauth2Application(ctx *context.APIContext) { + // swagger:operation POST /user/applications/oauth2 user userCreateOAuth2Application + // --- + // summary: creates a new OAuth2 application + // produces: + // - application/json + // parameters: + // - name: body + // in: body + // required: true + // schema: + // "$ref": "#/definitions/CreateOAuth2ApplicationOptions" + // responses: + // "201": + // "$ref": "#/responses/OAuth2Application" + // "400": + // "$ref": "#/responses/error" + + data := web.GetForm(ctx).(*api.CreateOAuth2ApplicationOptions) + + app, err := auth_model.CreateOAuth2Application(ctx, auth_model.CreateOAuth2ApplicationOptions{ + Name: data.Name, + UserID: ctx.Doer.ID, + RedirectURIs: data.RedirectURIs, + ConfidentialClient: data.ConfidentialClient, + }) + if err != nil { + ctx.Error(http.StatusBadRequest, "", "error creating oauth2 application") + return + } + secret, err := app.GenerateClientSecret(ctx) + if err != nil { + ctx.Error(http.StatusBadRequest, "", "error creating application secret") + return + } + app.ClientSecret = secret + + ctx.JSON(http.StatusCreated, convert.ToOAuth2Application(app)) +} + +// ListOauth2Applications list all the Oauth2 application +func ListOauth2Applications(ctx *context.APIContext) { + // swagger:operation GET /user/applications/oauth2 user userGetOAuth2Applications + // --- + // summary: List the authenticated user's oauth2 applications + // produces: + // - application/json + // parameters: + // - name: page + // in: query + // description: page number of results to return (1-based) + // type: integer + // - name: limit + // in: query + // description: page size of results + // type: integer + // responses: + // "200": + // "$ref": "#/responses/OAuth2ApplicationList" + + apps, total, err := db.FindAndCount[auth_model.OAuth2Application](ctx, auth_model.FindOAuth2ApplicationsOptions{ + ListOptions: utils.GetListOptions(ctx), + OwnerID: ctx.Doer.ID, + }) + if err != nil { + ctx.Error(http.StatusInternalServerError, "ListOAuth2Applications", err) + return + } + + apiApps := make([]*api.OAuth2Application, len(apps)) + for i := range apps { + apiApps[i] = convert.ToOAuth2Application(apps[i]) + apiApps[i].ClientSecret = "" // Hide secret on application list + } + + ctx.SetTotalCountHeader(total) + ctx.JSON(http.StatusOK, &apiApps) +} + +// DeleteOauth2Application delete OAuth2 Application +func DeleteOauth2Application(ctx *context.APIContext) { + // swagger:operation DELETE /user/applications/oauth2/{id} user userDeleteOAuth2Application + // --- + // summary: delete an OAuth2 Application + // produces: + // - application/json + // parameters: + // - name: id + // in: path + // description: token to be deleted + // type: integer + // format: int64 + // required: true + // responses: + // "204": + // "$ref": "#/responses/empty" + // "404": + // "$ref": "#/responses/notFound" + appID := ctx.ParamsInt64(":id") + if err := auth_model.DeleteOAuth2Application(ctx, appID, ctx.Doer.ID); err != nil { + if auth_model.IsErrOAuthApplicationNotFound(err) { + ctx.NotFound() + } else { + ctx.Error(http.StatusInternalServerError, "DeleteOauth2ApplicationByID", err) + } + return + } + + ctx.Status(http.StatusNoContent) +} + +// GetOauth2Application get OAuth2 Application +func GetOauth2Application(ctx *context.APIContext) { + // swagger:operation GET /user/applications/oauth2/{id} user userGetOAuth2Application + // --- + // summary: get an OAuth2 Application + // produces: + // - application/json + // parameters: + // - name: id + // in: path + // description: Application ID to be found + // type: integer + // format: int64 + // required: true + // responses: + // "200": + // "$ref": "#/responses/OAuth2Application" + // "404": + // "$ref": "#/responses/notFound" + appID := ctx.ParamsInt64(":id") + app, err := auth_model.GetOAuth2ApplicationByID(ctx, appID) + if err != nil { + if auth_model.IsErrOauthClientIDInvalid(err) || auth_model.IsErrOAuthApplicationNotFound(err) { + ctx.NotFound() + } else { + ctx.Error(http.StatusInternalServerError, "GetOauth2ApplicationByID", err) + } + return + } + if app.UID != ctx.Doer.ID { + ctx.NotFound() + return + } + + app.ClientSecret = "" + + ctx.JSON(http.StatusOK, convert.ToOAuth2Application(app)) +} + +// UpdateOauth2Application update OAuth2 Application +func UpdateOauth2Application(ctx *context.APIContext) { + // swagger:operation PATCH /user/applications/oauth2/{id} user userUpdateOAuth2Application + // --- + // summary: update an OAuth2 Application, this includes regenerating the client secret + // produces: + // - application/json + // parameters: + // - name: id + // in: path + // description: application to be updated + // type: integer + // format: int64 + // required: true + // - name: body + // in: body + // required: true + // schema: + // "$ref": "#/definitions/CreateOAuth2ApplicationOptions" + // responses: + // "200": + // "$ref": "#/responses/OAuth2Application" + // "404": + // "$ref": "#/responses/notFound" + appID := ctx.ParamsInt64(":id") + + data := web.GetForm(ctx).(*api.CreateOAuth2ApplicationOptions) + + app, err := auth_model.UpdateOAuth2Application(ctx, auth_model.UpdateOAuth2ApplicationOptions{ + Name: data.Name, + UserID: ctx.Doer.ID, + ID: appID, + RedirectURIs: data.RedirectURIs, + ConfidentialClient: data.ConfidentialClient, + }) + if err != nil { + if auth_model.IsErrOauthClientIDInvalid(err) || auth_model.IsErrOAuthApplicationNotFound(err) { + ctx.NotFound() + } else { + ctx.Error(http.StatusInternalServerError, "UpdateOauth2ApplicationByID", err) + } + return + } + app.ClientSecret, err = app.GenerateClientSecret(ctx) + if err != nil { + ctx.Error(http.StatusBadRequest, "", "error updating application secret") + return + } + + ctx.JSON(http.StatusOK, convert.ToOAuth2Application(app)) +} diff --git a/routers/api/v1/user/avatar.go b/routers/api/v1/user/avatar.go new file mode 100644 index 0000000..30ccb63 --- /dev/null +++ b/routers/api/v1/user/avatar.go @@ -0,0 +1,65 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package user + +import ( + "encoding/base64" + "net/http" + + api "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/modules/web" + "code.gitea.io/gitea/services/context" + user_service "code.gitea.io/gitea/services/user" +) + +// UpdateAvatar updates the Avatar of an User +func UpdateAvatar(ctx *context.APIContext) { + // swagger:operation POST /user/avatar user userUpdateAvatar + // --- + // summary: Update Avatar + // produces: + // - application/json + // parameters: + // - name: body + // in: body + // schema: + // "$ref": "#/definitions/UpdateUserAvatarOption" + // responses: + // "204": + // "$ref": "#/responses/empty" + form := web.GetForm(ctx).(*api.UpdateUserAvatarOption) + + content, err := base64.StdEncoding.DecodeString(form.Image) + if err != nil { + ctx.Error(http.StatusBadRequest, "DecodeImage", err) + return + } + + err = user_service.UploadAvatar(ctx, ctx.Doer, content) + if err != nil { + ctx.Error(http.StatusInternalServerError, "UploadAvatar", err) + return + } + + ctx.Status(http.StatusNoContent) +} + +// DeleteAvatar deletes the Avatar of an User +func DeleteAvatar(ctx *context.APIContext) { + // swagger:operation DELETE /user/avatar user userDeleteAvatar + // --- + // summary: Delete Avatar + // produces: + // - application/json + // responses: + // "204": + // "$ref": "#/responses/empty" + err := user_service.DeleteAvatar(ctx, ctx.Doer) + if err != nil { + ctx.Error(http.StatusInternalServerError, "DeleteAvatar", err) + return + } + + ctx.Status(http.StatusNoContent) +} diff --git a/routers/api/v1/user/email.go b/routers/api/v1/user/email.go new file mode 100644 index 0000000..33aa851 --- /dev/null +++ b/routers/api/v1/user/email.go @@ -0,0 +1,132 @@ +// Copyright 2015 The Gogs Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package user + +import ( + "fmt" + "net/http" + + user_model "code.gitea.io/gitea/models/user" + api "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/modules/web" + "code.gitea.io/gitea/services/context" + "code.gitea.io/gitea/services/convert" + user_service "code.gitea.io/gitea/services/user" +) + +// ListEmails list all of the authenticated user's email addresses +// see https://github.com/gogits/go-gogs-client/wiki/Users-Emails#list-email-addresses-for-a-user +func ListEmails(ctx *context.APIContext) { + // swagger:operation GET /user/emails user userListEmails + // --- + // summary: List the authenticated user's email addresses + // produces: + // - application/json + // responses: + // "200": + // "$ref": "#/responses/EmailList" + + emails, err := user_model.GetEmailAddresses(ctx, ctx.Doer.ID) + if err != nil { + ctx.Error(http.StatusInternalServerError, "GetEmailAddresses", err) + return + } + apiEmails := make([]*api.Email, len(emails)) + for i := range emails { + apiEmails[i] = convert.ToEmail(emails[i]) + } + ctx.JSON(http.StatusOK, &apiEmails) +} + +// AddEmail add an email address +func AddEmail(ctx *context.APIContext) { + // swagger:operation POST /user/emails user userAddEmail + // --- + // summary: Add email addresses + // produces: + // - application/json + // parameters: + // - name: body + // in: body + // schema: + // "$ref": "#/definitions/CreateEmailOption" + // responses: + // '201': + // "$ref": "#/responses/EmailList" + // "422": + // "$ref": "#/responses/validationError" + + form := web.GetForm(ctx).(*api.CreateEmailOption) + if len(form.Emails) == 0 { + ctx.Error(http.StatusUnprocessableEntity, "", "Email list empty") + return + } + + if err := user_service.AddEmailAddresses(ctx, ctx.Doer, form.Emails); err != nil { + if user_model.IsErrEmailAlreadyUsed(err) { + ctx.Error(http.StatusUnprocessableEntity, "", "Email address has been used: "+err.(user_model.ErrEmailAlreadyUsed).Email) + } else if user_model.IsErrEmailCharIsNotSupported(err) || user_model.IsErrEmailInvalid(err) { + email := "" + if typedError, ok := err.(user_model.ErrEmailInvalid); ok { + email = typedError.Email + } + if typedError, ok := err.(user_model.ErrEmailCharIsNotSupported); ok { + email = typedError.Email + } + + errMsg := fmt.Sprintf("Email address %q invalid", email) + ctx.Error(http.StatusUnprocessableEntity, "", errMsg) + } else { + ctx.Error(http.StatusInternalServerError, "AddEmailAddresses", err) + } + return + } + + emails, err := user_model.GetEmailAddresses(ctx, ctx.Doer.ID) + if err != nil { + ctx.Error(http.StatusInternalServerError, "GetEmailAddresses", err) + return + } + + apiEmails := make([]*api.Email, 0, len(emails)) + for _, email := range emails { + apiEmails = append(apiEmails, convert.ToEmail(email)) + } + ctx.JSON(http.StatusCreated, apiEmails) +} + +// DeleteEmail delete email +func DeleteEmail(ctx *context.APIContext) { + // swagger:operation DELETE /user/emails user userDeleteEmail + // --- + // summary: Delete email addresses + // produces: + // - application/json + // parameters: + // - name: body + // in: body + // schema: + // "$ref": "#/definitions/DeleteEmailOption" + // responses: + // "204": + // "$ref": "#/responses/empty" + // "404": + // "$ref": "#/responses/notFound" + + form := web.GetForm(ctx).(*api.DeleteEmailOption) + if len(form.Emails) == 0 { + ctx.Status(http.StatusNoContent) + return + } + + if err := user_service.DeleteEmailAddresses(ctx, ctx.Doer, form.Emails); err != nil { + if user_model.IsErrEmailAddressNotExist(err) { + ctx.Error(http.StatusNotFound, "DeleteEmailAddresses", err) + } else { + ctx.Error(http.StatusInternalServerError, "DeleteEmailAddresses", err) + } + return + } + ctx.Status(http.StatusNoContent) +} diff --git a/routers/api/v1/user/follower.go b/routers/api/v1/user/follower.go new file mode 100644 index 0000000..1ba7346 --- /dev/null +++ b/routers/api/v1/user/follower.go @@ -0,0 +1,263 @@ +// Copyright 2015 The Gogs Authors. All rights reserved. +// Copyright 2020 The Gitea Authors. +// SPDX-License-Identifier: MIT + +package user + +import ( + "errors" + "net/http" + + user_model "code.gitea.io/gitea/models/user" + api "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/routers/api/v1/utils" + "code.gitea.io/gitea/services/context" + "code.gitea.io/gitea/services/convert" +) + +func responseAPIUsers(ctx *context.APIContext, users []*user_model.User) { + apiUsers := make([]*api.User, len(users)) + for i := range users { + apiUsers[i] = convert.ToUser(ctx, users[i], ctx.Doer) + } + ctx.JSON(http.StatusOK, &apiUsers) +} + +func listUserFollowers(ctx *context.APIContext, u *user_model.User) { + users, count, err := user_model.GetUserFollowers(ctx, u, ctx.Doer, utils.GetListOptions(ctx)) + if err != nil { + ctx.Error(http.StatusInternalServerError, "GetUserFollowers", err) + return + } + + ctx.SetTotalCountHeader(count) + responseAPIUsers(ctx, users) +} + +// ListMyFollowers list the authenticated user's followers +func ListMyFollowers(ctx *context.APIContext) { + // swagger:operation GET /user/followers user userCurrentListFollowers + // --- + // summary: List the authenticated user's followers + // parameters: + // - name: page + // in: query + // description: page number of results to return (1-based) + // type: integer + // - name: limit + // in: query + // description: page size of results + // type: integer + // produces: + // - application/json + // responses: + // "200": + // "$ref": "#/responses/UserList" + + listUserFollowers(ctx, ctx.Doer) +} + +// ListFollowers list the given user's followers +func ListFollowers(ctx *context.APIContext) { + // swagger:operation GET /users/{username}/followers user userListFollowers + // --- + // summary: List the given user's followers + // produces: + // - application/json + // parameters: + // - name: username + // in: path + // description: username of user + // type: string + // required: true + // - name: page + // in: query + // description: page number of results to return (1-based) + // type: integer + // - name: limit + // in: query + // description: page size of results + // type: integer + // responses: + // "200": + // "$ref": "#/responses/UserList" + // "404": + // "$ref": "#/responses/notFound" + + listUserFollowers(ctx, ctx.ContextUser) +} + +func listUserFollowing(ctx *context.APIContext, u *user_model.User) { + users, count, err := user_model.GetUserFollowing(ctx, u, ctx.Doer, utils.GetListOptions(ctx)) + if err != nil { + ctx.Error(http.StatusInternalServerError, "GetUserFollowing", err) + return + } + + ctx.SetTotalCountHeader(count) + responseAPIUsers(ctx, users) +} + +// ListMyFollowing list the users that the authenticated user is following +func ListMyFollowing(ctx *context.APIContext) { + // swagger:operation GET /user/following user userCurrentListFollowing + // --- + // summary: List the users that the authenticated user is following + // parameters: + // - name: page + // in: query + // description: page number of results to return (1-based) + // type: integer + // - name: limit + // in: query + // description: page size of results + // type: integer + // produces: + // - application/json + // responses: + // "200": + // "$ref": "#/responses/UserList" + + listUserFollowing(ctx, ctx.Doer) +} + +// ListFollowing list the users that the given user is following +func ListFollowing(ctx *context.APIContext) { + // swagger:operation GET /users/{username}/following user userListFollowing + // --- + // summary: List the users that the given user is following + // produces: + // - application/json + // parameters: + // - name: username + // in: path + // description: username of user + // type: string + // required: true + // - name: page + // in: query + // description: page number of results to return (1-based) + // type: integer + // - name: limit + // in: query + // description: page size of results + // type: integer + // responses: + // "200": + // "$ref": "#/responses/UserList" + // "404": + // "$ref": "#/responses/notFound" + + listUserFollowing(ctx, ctx.ContextUser) +} + +func checkUserFollowing(ctx *context.APIContext, u *user_model.User, followID int64) { + if user_model.IsFollowing(ctx, u.ID, followID) { + ctx.Status(http.StatusNoContent) + } else { + ctx.NotFound() + } +} + +// CheckMyFollowing whether the given user is followed by the authenticated user +func CheckMyFollowing(ctx *context.APIContext) { + // swagger:operation GET /user/following/{username} user userCurrentCheckFollowing + // --- + // summary: Check whether a user is followed by the authenticated user + // parameters: + // - name: username + // in: path + // description: username of followed user + // type: string + // required: true + // responses: + // "204": + // "$ref": "#/responses/empty" + // "404": + // "$ref": "#/responses/notFound" + + checkUserFollowing(ctx, ctx.Doer, ctx.ContextUser.ID) +} + +// CheckFollowing check if one user is following another user +func CheckFollowing(ctx *context.APIContext) { + // swagger:operation GET /users/{username}/following/{target} user userCheckFollowing + // --- + // summary: Check if one user is following another user + // parameters: + // - name: username + // in: path + // description: username of following user + // type: string + // required: true + // - name: target + // in: path + // description: username of followed user + // type: string + // required: true + // responses: + // "204": + // "$ref": "#/responses/empty" + // "404": + // "$ref": "#/responses/notFound" + + target := GetUserByParamsName(ctx, ":target") + if ctx.Written() { + return + } + checkUserFollowing(ctx, ctx.ContextUser, target.ID) +} + +// Follow follow a user +func Follow(ctx *context.APIContext) { + // swagger:operation PUT /user/following/{username} user userCurrentPutFollow + // --- + // summary: Follow a user + // parameters: + // - name: username + // in: path + // description: username of user to follow + // type: string + // required: true + // responses: + // "204": + // "$ref": "#/responses/empty" + // "404": + // "$ref": "#/responses/notFound" + // "403": + // "$ref": "#/responses/forbidden" + + if err := user_model.FollowUser(ctx, ctx.Doer.ID, ctx.ContextUser.ID); err != nil { + if errors.Is(err, user_model.ErrBlockedByUser) { + ctx.Error(http.StatusForbidden, "BlockedByUser", err) + return + } + ctx.Error(http.StatusInternalServerError, "FollowUser", err) + return + } + ctx.Status(http.StatusNoContent) +} + +// Unfollow unfollow a user +func Unfollow(ctx *context.APIContext) { + // swagger:operation DELETE /user/following/{username} user userCurrentDeleteFollow + // --- + // summary: Unfollow a user + // parameters: + // - name: username + // in: path + // description: username of user to unfollow + // type: string + // required: true + // responses: + // "204": + // "$ref": "#/responses/empty" + // "404": + // "$ref": "#/responses/notFound" + + if err := user_model.UnfollowUser(ctx, ctx.Doer.ID, ctx.ContextUser.ID); err != nil { + ctx.Error(http.StatusInternalServerError, "UnfollowUser", err) + return + } + ctx.Status(http.StatusNoContent) +} diff --git a/routers/api/v1/user/gpg_key.go b/routers/api/v1/user/gpg_key.go new file mode 100644 index 0000000..5a2f995 --- /dev/null +++ b/routers/api/v1/user/gpg_key.go @@ -0,0 +1,311 @@ +// Copyright 2017 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package user + +import ( + "fmt" + "net/http" + "strings" + + asymkey_model "code.gitea.io/gitea/models/asymkey" + "code.gitea.io/gitea/models/db" + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/setting" + api "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/modules/web" + "code.gitea.io/gitea/routers/api/v1/utils" + "code.gitea.io/gitea/services/context" + "code.gitea.io/gitea/services/convert" +) + +func listGPGKeys(ctx *context.APIContext, uid int64, listOptions db.ListOptions) { + keys, total, err := db.FindAndCount[asymkey_model.GPGKey](ctx, asymkey_model.FindGPGKeyOptions{ + ListOptions: listOptions, + OwnerID: uid, + }) + if err != nil { + ctx.Error(http.StatusInternalServerError, "ListGPGKeys", err) + return + } + + if err := asymkey_model.GPGKeyList(keys).LoadSubKeys(ctx); err != nil { + ctx.Error(http.StatusInternalServerError, "ListGPGKeys", err) + return + } + + apiKeys := make([]*api.GPGKey, len(keys)) + for i := range keys { + apiKeys[i] = convert.ToGPGKey(keys[i]) + } + + ctx.SetTotalCountHeader(total) + ctx.JSON(http.StatusOK, &apiKeys) +} + +// ListGPGKeys get the GPG key list of a user +func ListGPGKeys(ctx *context.APIContext) { + // swagger:operation GET /users/{username}/gpg_keys user userListGPGKeys + // --- + // summary: List the given user's GPG keys + // produces: + // - application/json + // parameters: + // - name: username + // in: path + // description: username of user + // type: string + // required: true + // - name: page + // in: query + // description: page number of results to return (1-based) + // type: integer + // - name: limit + // in: query + // description: page size of results + // type: integer + // responses: + // "200": + // "$ref": "#/responses/GPGKeyList" + // "404": + // "$ref": "#/responses/notFound" + + listGPGKeys(ctx, ctx.ContextUser.ID, utils.GetListOptions(ctx)) +} + +// ListMyGPGKeys get the GPG key list of the authenticated user +func ListMyGPGKeys(ctx *context.APIContext) { + // swagger:operation GET /user/gpg_keys user userCurrentListGPGKeys + // --- + // summary: List the authenticated user's GPG keys + // parameters: + // - name: page + // in: query + // description: page number of results to return (1-based) + // type: integer + // - name: limit + // in: query + // description: page size of results + // type: integer + // produces: + // - application/json + // responses: + // "200": + // "$ref": "#/responses/GPGKeyList" + + listGPGKeys(ctx, ctx.Doer.ID, utils.GetListOptions(ctx)) +} + +// GetGPGKey get the GPG key based on a id +func GetGPGKey(ctx *context.APIContext) { + // swagger:operation GET /user/gpg_keys/{id} user userCurrentGetGPGKey + // --- + // summary: Get a GPG key + // produces: + // - application/json + // parameters: + // - name: id + // in: path + // description: id of key to get + // type: integer + // format: int64 + // required: true + // responses: + // "200": + // "$ref": "#/responses/GPGKey" + // "404": + // "$ref": "#/responses/notFound" + + key, err := asymkey_model.GetGPGKeyForUserByID(ctx, ctx.Doer.ID, ctx.ParamsInt64(":id")) + if err != nil { + if asymkey_model.IsErrGPGKeyNotExist(err) { + ctx.NotFound() + } else { + ctx.Error(http.StatusInternalServerError, "GetGPGKeyByID", err) + } + return + } + if err := key.LoadSubKeys(ctx); err != nil { + ctx.Error(http.StatusInternalServerError, "LoadSubKeys", err) + return + } + ctx.JSON(http.StatusOK, convert.ToGPGKey(key)) +} + +// CreateUserGPGKey creates new GPG key to given user by ID. +func CreateUserGPGKey(ctx *context.APIContext, form api.CreateGPGKeyOption, uid int64) { + if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureManageGPGKeys) { + ctx.NotFound("Not Found", fmt.Errorf("gpg keys setting is not allowed to be visited")) + return + } + + token := asymkey_model.VerificationToken(ctx.Doer, 1) + lastToken := asymkey_model.VerificationToken(ctx.Doer, 0) + + keys, err := asymkey_model.AddGPGKey(ctx, uid, form.ArmoredKey, token, form.Signature) + if err != nil && asymkey_model.IsErrGPGInvalidTokenSignature(err) { + keys, err = asymkey_model.AddGPGKey(ctx, uid, form.ArmoredKey, lastToken, form.Signature) + } + if err != nil { + HandleAddGPGKeyError(ctx, err, token) + return + } + ctx.JSON(http.StatusCreated, convert.ToGPGKey(keys[0])) +} + +// GetVerificationToken returns the current token to be signed for this user +func GetVerificationToken(ctx *context.APIContext) { + // swagger:operation GET /user/gpg_key_token user getVerificationToken + // --- + // summary: Get a Token to verify + // produces: + // - text/plain + // parameters: + // responses: + // "200": + // "$ref": "#/responses/string" + // "404": + // "$ref": "#/responses/notFound" + + token := asymkey_model.VerificationToken(ctx.Doer, 1) + ctx.PlainText(http.StatusOK, token) +} + +// VerifyUserGPGKey creates new GPG key to given user by ID. +func VerifyUserGPGKey(ctx *context.APIContext) { + // swagger:operation POST /user/gpg_key_verify user userVerifyGPGKey + // --- + // summary: Verify a GPG key + // consumes: + // - application/json + // produces: + // - application/json + // responses: + // "201": + // "$ref": "#/responses/GPGKey" + // "404": + // "$ref": "#/responses/notFound" + // "422": + // "$ref": "#/responses/validationError" + + form := web.GetForm(ctx).(*api.VerifyGPGKeyOption) + token := asymkey_model.VerificationToken(ctx.Doer, 1) + lastToken := asymkey_model.VerificationToken(ctx.Doer, 0) + + form.KeyID = strings.TrimLeft(form.KeyID, "0") + if form.KeyID == "" { + ctx.NotFound() + return + } + + _, err := asymkey_model.VerifyGPGKey(ctx, ctx.Doer.ID, form.KeyID, token, form.Signature) + if err != nil && asymkey_model.IsErrGPGInvalidTokenSignature(err) { + _, err = asymkey_model.VerifyGPGKey(ctx, ctx.Doer.ID, form.KeyID, lastToken, form.Signature) + } + + if err != nil { + if asymkey_model.IsErrGPGInvalidTokenSignature(err) { + ctx.Error(http.StatusUnprocessableEntity, "GPGInvalidSignature", fmt.Sprintf("The provided GPG key, signature and token do not match or token is out of date. Provide a valid signature for the token: %s", token)) + return + } + ctx.Error(http.StatusInternalServerError, "VerifyUserGPGKey", err) + } + + keys, err := db.Find[asymkey_model.GPGKey](ctx, asymkey_model.FindGPGKeyOptions{ + KeyID: form.KeyID, + IncludeSubKeys: true, + }) + if err != nil { + if asymkey_model.IsErrGPGKeyNotExist(err) { + ctx.NotFound() + } else { + ctx.Error(http.StatusInternalServerError, "GetGPGKeysByKeyID", err) + } + return + } + ctx.JSON(http.StatusOK, convert.ToGPGKey(keys[0])) +} + +// swagger:parameters userCurrentPostGPGKey +type swaggerUserCurrentPostGPGKey struct { + // in:body + Form api.CreateGPGKeyOption +} + +// CreateGPGKey create a GPG key belonging to the authenticated user +func CreateGPGKey(ctx *context.APIContext) { + // swagger:operation POST /user/gpg_keys user userCurrentPostGPGKey + // --- + // summary: Create a GPG key + // consumes: + // - application/json + // produces: + // - application/json + // responses: + // "201": + // "$ref": "#/responses/GPGKey" + // "404": + // "$ref": "#/responses/notFound" + // "422": + // "$ref": "#/responses/validationError" + + form := web.GetForm(ctx).(*api.CreateGPGKeyOption) + CreateUserGPGKey(ctx, *form, ctx.Doer.ID) +} + +// DeleteGPGKey remove a GPG key belonging to the authenticated user +func DeleteGPGKey(ctx *context.APIContext) { + // swagger:operation DELETE /user/gpg_keys/{id} user userCurrentDeleteGPGKey + // --- + // summary: Remove a GPG key + // produces: + // - application/json + // parameters: + // - name: id + // in: path + // description: id of key to delete + // type: integer + // format: int64 + // required: true + // responses: + // "204": + // "$ref": "#/responses/empty" + // "403": + // "$ref": "#/responses/forbidden" + // "404": + // "$ref": "#/responses/notFound" + + if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureManageGPGKeys) { + ctx.NotFound("Not Found", fmt.Errorf("gpg keys setting is not allowed to be visited")) + return + } + + if err := asymkey_model.DeleteGPGKey(ctx, ctx.Doer, ctx.ParamsInt64(":id")); err != nil { + if asymkey_model.IsErrGPGKeyAccessDenied(err) { + ctx.Error(http.StatusForbidden, "", "You do not have access to this key") + } else { + ctx.Error(http.StatusInternalServerError, "DeleteGPGKey", err) + } + return + } + + ctx.Status(http.StatusNoContent) +} + +// HandleAddGPGKeyError handle add GPGKey error +func HandleAddGPGKeyError(ctx *context.APIContext, err error, token string) { + switch { + case asymkey_model.IsErrGPGKeyAccessDenied(err): + ctx.Error(http.StatusUnprocessableEntity, "GPGKeyAccessDenied", "You do not have access to this GPG key") + case asymkey_model.IsErrGPGKeyIDAlreadyUsed(err): + ctx.Error(http.StatusUnprocessableEntity, "GPGKeyIDAlreadyUsed", "A key with the same id already exists") + case asymkey_model.IsErrGPGKeyParsing(err): + ctx.Error(http.StatusUnprocessableEntity, "GPGKeyParsing", err) + case asymkey_model.IsErrGPGNoEmailFound(err): + ctx.Error(http.StatusNotFound, "GPGNoEmailFound", fmt.Sprintf("None of the emails attached to the GPG key could be found. It may still be added if you provide a valid signature for the token: %s", token)) + case asymkey_model.IsErrGPGInvalidTokenSignature(err): + ctx.Error(http.StatusUnprocessableEntity, "GPGInvalidSignature", fmt.Sprintf("The provided GPG key, signature and token do not match or token is out of date. Provide a valid signature for the token: %s", token)) + default: + ctx.Error(http.StatusInternalServerError, "AddGPGKey", err) + } +} diff --git a/routers/api/v1/user/helper.go b/routers/api/v1/user/helper.go new file mode 100644 index 0000000..8b5c64e --- /dev/null +++ b/routers/api/v1/user/helper.go @@ -0,0 +1,35 @@ +// Copyright 2021 The Gitea Authors. +// SPDX-License-Identifier: MIT + +package user + +import ( + "net/http" + + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/services/context" +) + +// GetUserByParamsName get user by name +func GetUserByParamsName(ctx *context.APIContext, name string) *user_model.User { + username := ctx.Params(name) + user, err := user_model.GetUserByName(ctx, username) + if err != nil { + if user_model.IsErrUserNotExist(err) { + if redirectUserID, err2 := user_model.LookupUserRedirect(ctx, username); err2 == nil { + context.RedirectToUser(ctx.Base, username, redirectUserID) + } else { + ctx.NotFound("GetUserByName", err) + } + } else { + ctx.Error(http.StatusInternalServerError, "GetUserByName", err) + } + return nil + } + return user +} + +// GetUserByParams returns user whose name is presented in URL (":username"). +func GetUserByParams(ctx *context.APIContext) *user_model.User { + return GetUserByParamsName(ctx, ":username") +} diff --git a/routers/api/v1/user/hook.go b/routers/api/v1/user/hook.go new file mode 100644 index 0000000..9d9ca5b --- /dev/null +++ b/routers/api/v1/user/hook.go @@ -0,0 +1,159 @@ +// Copyright 2022 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package user + +import ( + "net/http" + + api "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/modules/web" + "code.gitea.io/gitea/routers/api/v1/utils" + "code.gitea.io/gitea/services/context" + webhook_service "code.gitea.io/gitea/services/webhook" +) + +// ListHooks list the authenticated user's webhooks +func ListHooks(ctx *context.APIContext) { + // swagger:operation GET /user/hooks user userListHooks + // --- + // summary: List the authenticated user's webhooks + // produces: + // - application/json + // parameters: + // - name: page + // in: query + // description: page number of results to return (1-based) + // type: integer + // - name: limit + // in: query + // description: page size of results + // type: integer + // responses: + // "200": + // "$ref": "#/responses/HookList" + + utils.ListOwnerHooks( + ctx, + ctx.Doer, + ) +} + +// GetHook get the authenticated user's hook by id +func GetHook(ctx *context.APIContext) { + // swagger:operation GET /user/hooks/{id} user userGetHook + // --- + // summary: Get a hook + // produces: + // - application/json + // parameters: + // - name: id + // in: path + // description: id of the hook to get + // type: integer + // format: int64 + // required: true + // responses: + // "200": + // "$ref": "#/responses/Hook" + + hook, err := utils.GetOwnerHook(ctx, ctx.Doer.ID, ctx.ParamsInt64("id")) + if err != nil { + return + } + + if !ctx.Doer.IsAdmin && hook.OwnerID != ctx.Doer.ID { + ctx.NotFound() + return + } + + apiHook, err := webhook_service.ToHook(ctx.Doer.HomeLink(), hook) + if err != nil { + ctx.InternalServerError(err) + return + } + ctx.JSON(http.StatusOK, apiHook) +} + +// CreateHook create a hook for the authenticated user +func CreateHook(ctx *context.APIContext) { + // swagger:operation POST /user/hooks user userCreateHook + // --- + // summary: Create a hook + // consumes: + // - application/json + // produces: + // - application/json + // parameters: + // - name: body + // in: body + // required: true + // schema: + // "$ref": "#/definitions/CreateHookOption" + // responses: + // "201": + // "$ref": "#/responses/Hook" + + utils.AddOwnerHook( + ctx, + ctx.Doer, + web.GetForm(ctx).(*api.CreateHookOption), + ) +} + +// EditHook modify a hook of the authenticated user +func EditHook(ctx *context.APIContext) { + // swagger:operation PATCH /user/hooks/{id} user userEditHook + // --- + // summary: Update a hook + // consumes: + // - application/json + // produces: + // - application/json + // parameters: + // - name: id + // in: path + // description: id of the hook to update + // type: integer + // format: int64 + // required: true + // - name: body + // in: body + // schema: + // "$ref": "#/definitions/EditHookOption" + // responses: + // "200": + // "$ref": "#/responses/Hook" + + utils.EditOwnerHook( + ctx, + ctx.Doer, + web.GetForm(ctx).(*api.EditHookOption), + ctx.ParamsInt64("id"), + ) +} + +// DeleteHook delete a hook of the authenticated user +func DeleteHook(ctx *context.APIContext) { + // swagger:operation DELETE /user/hooks/{id} user userDeleteHook + // --- + // summary: Delete a hook + // produces: + // - application/json + // parameters: + // - name: id + // in: path + // description: id of the hook to delete + // type: integer + // format: int64 + // required: true + // responses: + // "204": + // "$ref": "#/responses/empty" + + utils.DeleteOwnerHook( + ctx, + ctx.Doer, + ctx.ParamsInt64("id"), + ) +} diff --git a/routers/api/v1/user/key.go b/routers/api/v1/user/key.go new file mode 100644 index 0000000..d9456e7 --- /dev/null +++ b/routers/api/v1/user/key.go @@ -0,0 +1,303 @@ +// Copyright 2015 The Gogs Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package user + +import ( + std_ctx "context" + "fmt" + "net/http" + + asymkey_model "code.gitea.io/gitea/models/asymkey" + "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/models/perm" + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/setting" + api "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/modules/web" + "code.gitea.io/gitea/routers/api/v1/repo" + "code.gitea.io/gitea/routers/api/v1/utils" + asymkey_service "code.gitea.io/gitea/services/asymkey" + "code.gitea.io/gitea/services/context" + "code.gitea.io/gitea/services/convert" +) + +// appendPrivateInformation appends the owner and key type information to api.PublicKey +func appendPrivateInformation(ctx std_ctx.Context, apiKey *api.PublicKey, key *asymkey_model.PublicKey, defaultUser *user_model.User) (*api.PublicKey, error) { + if key.Type == asymkey_model.KeyTypeDeploy { + apiKey.KeyType = "deploy" + } else if key.Type == asymkey_model.KeyTypeUser { + apiKey.KeyType = "user" + + if defaultUser.ID == key.OwnerID { + apiKey.Owner = convert.ToUser(ctx, defaultUser, defaultUser) + } else { + user, err := user_model.GetUserByID(ctx, key.OwnerID) + if err != nil { + return apiKey, err + } + apiKey.Owner = convert.ToUser(ctx, user, user) + } + } else { + apiKey.KeyType = "unknown" + } + apiKey.ReadOnly = key.Mode == perm.AccessModeRead + return apiKey, nil +} + +func composePublicKeysAPILink() string { + return setting.AppURL + "api/v1/user/keys/" +} + +func listPublicKeys(ctx *context.APIContext, user *user_model.User) { + var keys []*asymkey_model.PublicKey + var err error + var count int + + fingerprint := ctx.FormString("fingerprint") + username := ctx.Params("username") + + if fingerprint != "" { + var userID int64 // Unrestricted + // Querying not just listing + if username != "" { + // Restrict to provided uid + userID = user.ID + } + keys, err = db.Find[asymkey_model.PublicKey](ctx, asymkey_model.FindPublicKeyOptions{ + OwnerID: userID, + Fingerprint: fingerprint, + }) + count = len(keys) + } else { + var total int64 + // Use ListPublicKeys + keys, total, err = db.FindAndCount[asymkey_model.PublicKey](ctx, asymkey_model.FindPublicKeyOptions{ + ListOptions: utils.GetListOptions(ctx), + OwnerID: user.ID, + NotKeytype: asymkey_model.KeyTypePrincipal, + }) + count = int(total) + } + + if err != nil { + ctx.Error(http.StatusInternalServerError, "ListPublicKeys", err) + return + } + + apiLink := composePublicKeysAPILink() + apiKeys := make([]*api.PublicKey, len(keys)) + for i := range keys { + apiKeys[i] = convert.ToPublicKey(apiLink, keys[i]) + if ctx.Doer.IsAdmin || ctx.Doer.ID == keys[i].OwnerID { + apiKeys[i], _ = appendPrivateInformation(ctx, apiKeys[i], keys[i], user) + } + } + + ctx.SetTotalCountHeader(int64(count)) + ctx.JSON(http.StatusOK, &apiKeys) +} + +// ListMyPublicKeys list all of the authenticated user's public keys +func ListMyPublicKeys(ctx *context.APIContext) { + // swagger:operation GET /user/keys user userCurrentListKeys + // --- + // summary: List the authenticated user's public keys + // parameters: + // - name: fingerprint + // in: query + // description: fingerprint of the key + // type: string + // - name: page + // in: query + // description: page number of results to return (1-based) + // type: integer + // - name: limit + // in: query + // description: page size of results + // type: integer + // produces: + // - application/json + // responses: + // "200": + // "$ref": "#/responses/PublicKeyList" + + listPublicKeys(ctx, ctx.Doer) +} + +// ListPublicKeys list the given user's public keys +func ListPublicKeys(ctx *context.APIContext) { + // swagger:operation GET /users/{username}/keys user userListKeys + // --- + // summary: List the given user's public keys + // produces: + // - application/json + // parameters: + // - name: username + // in: path + // description: username of user + // type: string + // required: true + // - name: fingerprint + // in: query + // description: fingerprint of the key + // type: string + // - name: page + // in: query + // description: page number of results to return (1-based) + // type: integer + // - name: limit + // in: query + // description: page size of results + // type: integer + // responses: + // "200": + // "$ref": "#/responses/PublicKeyList" + // "404": + // "$ref": "#/responses/notFound" + + listPublicKeys(ctx, ctx.ContextUser) +} + +// GetPublicKey get a public key +func GetPublicKey(ctx *context.APIContext) { + // swagger:operation GET /user/keys/{id} user userCurrentGetKey + // --- + // summary: Get a public key + // produces: + // - application/json + // parameters: + // - name: id + // in: path + // description: id of key to get + // type: integer + // format: int64 + // required: true + // responses: + // "200": + // "$ref": "#/responses/PublicKey" + // "404": + // "$ref": "#/responses/notFound" + + key, err := asymkey_model.GetPublicKeyByID(ctx, ctx.ParamsInt64(":id")) + if err != nil { + if asymkey_model.IsErrKeyNotExist(err) { + ctx.NotFound() + } else { + ctx.Error(http.StatusInternalServerError, "GetPublicKeyByID", err) + } + return + } + + apiLink := composePublicKeysAPILink() + apiKey := convert.ToPublicKey(apiLink, key) + if ctx.Doer.IsAdmin || ctx.Doer.ID == key.OwnerID { + apiKey, _ = appendPrivateInformation(ctx, apiKey, key, ctx.Doer) + } + ctx.JSON(http.StatusOK, apiKey) +} + +// CreateUserPublicKey creates new public key to given user by ID. +func CreateUserPublicKey(ctx *context.APIContext, form api.CreateKeyOption, uid int64) { + if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureManageSSHKeys) { + ctx.NotFound("Not Found", fmt.Errorf("ssh keys setting is not allowed to be visited")) + return + } + + content, err := asymkey_model.CheckPublicKeyString(form.Key) + if err != nil { + repo.HandleCheckKeyStringError(ctx, err) + return + } + + key, err := asymkey_model.AddPublicKey(ctx, uid, form.Title, content, 0) + if err != nil { + repo.HandleAddKeyError(ctx, err) + return + } + apiLink := composePublicKeysAPILink() + apiKey := convert.ToPublicKey(apiLink, key) + if ctx.Doer.IsAdmin || ctx.Doer.ID == key.OwnerID { + apiKey, _ = appendPrivateInformation(ctx, apiKey, key, ctx.Doer) + } + ctx.JSON(http.StatusCreated, apiKey) +} + +// CreatePublicKey create one public key for me +func CreatePublicKey(ctx *context.APIContext) { + // swagger:operation POST /user/keys user userCurrentPostKey + // --- + // summary: Create a public key + // consumes: + // - application/json + // produces: + // - application/json + // parameters: + // - name: body + // in: body + // schema: + // "$ref": "#/definitions/CreateKeyOption" + // responses: + // "201": + // "$ref": "#/responses/PublicKey" + // "422": + // "$ref": "#/responses/validationError" + + form := web.GetForm(ctx).(*api.CreateKeyOption) + CreateUserPublicKey(ctx, *form, ctx.Doer.ID) +} + +// DeletePublicKey delete one public key +func DeletePublicKey(ctx *context.APIContext) { + // swagger:operation DELETE /user/keys/{id} user userCurrentDeleteKey + // --- + // summary: Delete a public key + // produces: + // - application/json + // parameters: + // - name: id + // in: path + // description: id of key to delete + // type: integer + // format: int64 + // required: true + // responses: + // "204": + // "$ref": "#/responses/empty" + // "403": + // "$ref": "#/responses/forbidden" + // "404": + // "$ref": "#/responses/notFound" + + if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureManageSSHKeys) { + ctx.NotFound("Not Found", fmt.Errorf("ssh keys setting is not allowed to be visited")) + return + } + + id := ctx.ParamsInt64(":id") + externallyManaged, err := asymkey_model.PublicKeyIsExternallyManaged(ctx, id) + if err != nil { + if asymkey_model.IsErrKeyNotExist(err) { + ctx.NotFound() + } else { + ctx.Error(http.StatusInternalServerError, "PublicKeyIsExternallyManaged", err) + } + return + } + + if externallyManaged { + ctx.Error(http.StatusForbidden, "", "SSH Key is externally managed for this user") + return + } + + if err := asymkey_service.DeletePublicKey(ctx, ctx.Doer, id); err != nil { + if asymkey_model.IsErrKeyAccessDenied(err) { + ctx.Error(http.StatusForbidden, "", "You do not have access to this key") + } else { + ctx.Error(http.StatusInternalServerError, "DeletePublicKey", err) + } + return + } + + ctx.Status(http.StatusNoContent) +} diff --git a/routers/api/v1/user/quota.go b/routers/api/v1/user/quota.go new file mode 100644 index 0000000..573d7b7 --- /dev/null +++ b/routers/api/v1/user/quota.go @@ -0,0 +1,118 @@ +// Copyright 2024 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package user + +import ( + "code.gitea.io/gitea/routers/api/v1/shared" + "code.gitea.io/gitea/services/context" +) + +// GetQuota returns the quota information for the authenticated user +func GetQuota(ctx *context.APIContext) { + // swagger:operation GET /user/quota user userGetQuota + // --- + // summary: Get quota information for the authenticated user + // produces: + // - application/json + // responses: + // "200": + // "$ref": "#/responses/QuotaInfo" + // "403": + // "$ref": "#/responses/forbidden" + + shared.GetQuota(ctx, ctx.Doer.ID) +} + +// CheckQuota returns whether the authenticated user is over the subject quota +func CheckQuota(ctx *context.APIContext) { + // swagger:operation GET /user/quota/check user userCheckQuota + // --- + // summary: Check if the authenticated user is over quota for a given subject + // produces: + // - application/json + // responses: + // "200": + // "$ref": "#/responses/boolean" + // "403": + // "$ref": "#/responses/forbidden" + // "422": + // "$ref": "#/responses/validationError" + + shared.CheckQuota(ctx, ctx.Doer.ID) +} + +// ListQuotaAttachments lists attachments affecting the authenticated user's quota +func ListQuotaAttachments(ctx *context.APIContext) { + // swagger:operation GET /user/quota/attachments user userListQuotaAttachments + // --- + // summary: List the attachments affecting the authenticated user's quota + // produces: + // - application/json + // parameters: + // - name: page + // in: query + // description: page number of results to return (1-based) + // type: integer + // - name: limit + // in: query + // description: page size of results + // type: integer + // responses: + // "200": + // "$ref": "#/responses/QuotaUsedAttachmentList" + // "403": + // "$ref": "#/responses/forbidden" + + shared.ListQuotaAttachments(ctx, ctx.Doer.ID) +} + +// ListQuotaPackages lists packages affecting the authenticated user's quota +func ListQuotaPackages(ctx *context.APIContext) { + // swagger:operation GET /user/quota/packages user userListQuotaPackages + // --- + // summary: List the packages affecting the authenticated user's quota + // produces: + // - application/json + // parameters: + // - name: page + // in: query + // description: page number of results to return (1-based) + // type: integer + // - name: limit + // in: query + // description: page size of results + // type: integer + // responses: + // "200": + // "$ref": "#/responses/QuotaUsedPackageList" + // "403": + // "$ref": "#/responses/forbidden" + + shared.ListQuotaPackages(ctx, ctx.Doer.ID) +} + +// ListQuotaArtifacts lists artifacts affecting the authenticated user's quota +func ListQuotaArtifacts(ctx *context.APIContext) { + // swagger:operation GET /user/quota/artifacts user userListQuotaArtifacts + // --- + // summary: List the artifacts affecting the authenticated user's quota + // produces: + // - application/json + // parameters: + // - name: page + // in: query + // description: page number of results to return (1-based) + // type: integer + // - name: limit + // in: query + // description: page size of results + // type: integer + // responses: + // "200": + // "$ref": "#/responses/QuotaUsedArtifactList" + // "403": + // "$ref": "#/responses/forbidden" + + shared.ListQuotaArtifacts(ctx, ctx.Doer.ID) +} diff --git a/routers/api/v1/user/repo.go b/routers/api/v1/user/repo.go new file mode 100644 index 0000000..86716ff --- /dev/null +++ b/routers/api/v1/user/repo.go @@ -0,0 +1,186 @@ +// Copyright 2017 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package user + +import ( + "net/http" + + access_model "code.gitea.io/gitea/models/perm/access" + repo_model "code.gitea.io/gitea/models/repo" + user_model "code.gitea.io/gitea/models/user" + api "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/routers/api/v1/utils" + "code.gitea.io/gitea/services/context" + "code.gitea.io/gitea/services/convert" +) + +// listUserRepos - List the repositories owned by the given user. +func listUserRepos(ctx *context.APIContext, u *user_model.User, private bool) { + opts := utils.GetListOptions(ctx) + + repos, count, err := repo_model.GetUserRepositories(ctx, &repo_model.SearchRepoOptions{ + Actor: u, + Private: private, + ListOptions: opts, + OrderBy: "id ASC", + }) + if err != nil { + ctx.Error(http.StatusInternalServerError, "GetUserRepositories", err) + return + } + + if err := repos.LoadAttributes(ctx); err != nil { + ctx.Error(http.StatusInternalServerError, "RepositoryList.LoadAttributes", err) + return + } + + apiRepos := make([]*api.Repository, 0, len(repos)) + for i := range repos { + permission, err := access_model.GetUserRepoPermission(ctx, repos[i], ctx.Doer) + if err != nil { + ctx.Error(http.StatusInternalServerError, "GetUserRepoPermission", err) + return + } + if ctx.IsSigned && ctx.Doer.IsAdmin || permission.HasAccess() { + apiRepos = append(apiRepos, convert.ToRepo(ctx, repos[i], permission)) + } + } + + ctx.SetLinkHeader(int(count), opts.PageSize) + ctx.SetTotalCountHeader(count) + ctx.JSON(http.StatusOK, &apiRepos) +} + +// ListUserRepos - list the repos owned by the given user. +func ListUserRepos(ctx *context.APIContext) { + // swagger:operation GET /users/{username}/repos user userListRepos + // --- + // summary: List the repos owned by the given user + // produces: + // - application/json + // parameters: + // - name: username + // in: path + // description: username of user + // type: string + // required: true + // - name: page + // in: query + // description: page number of results to return (1-based) + // type: integer + // - name: limit + // in: query + // description: page size of results + // type: integer + // responses: + // "200": + // "$ref": "#/responses/RepositoryList" + // "404": + // "$ref": "#/responses/notFound" + + private := ctx.IsSigned + listUserRepos(ctx, ctx.ContextUser, private) +} + +// ListMyRepos - list the repositories you own or have access to. +func ListMyRepos(ctx *context.APIContext) { + // swagger:operation GET /user/repos user userCurrentListRepos + // --- + // summary: List the repos that the authenticated user owns + // produces: + // - application/json + // parameters: + // - name: page + // in: query + // description: page number of results to return (1-based) + // type: integer + // - name: limit + // in: query + // description: page size of results + // type: integer + // - name: order_by + // in: query + // description: order the repositories by name (default), id, or size + // type: string + // responses: + // "200": + // "$ref": "#/responses/RepositoryList" + // "422": + // "$ref": "#/responses/validationError" + + opts := &repo_model.SearchRepoOptions{ + ListOptions: utils.GetListOptions(ctx), + Actor: ctx.Doer, + OwnerID: ctx.Doer.ID, + Private: ctx.IsSigned, + IncludeDescription: true, + } + orderBy := ctx.FormTrim("order_by") + switch orderBy { + case "name": + opts.OrderBy = "name ASC" + case "size": + opts.OrderBy = "size DESC" + case "id": + opts.OrderBy = "id ASC" + case "": + default: + ctx.Error(http.StatusUnprocessableEntity, "", "invalid order_by") + return + } + + var err error + repos, count, err := repo_model.SearchRepository(ctx, opts) + if err != nil { + ctx.Error(http.StatusInternalServerError, "SearchRepository", err) + return + } + + results := make([]*api.Repository, len(repos)) + for i, repo := range repos { + if err = repo.LoadOwner(ctx); err != nil { + ctx.Error(http.StatusInternalServerError, "LoadOwner", err) + return + } + permission, err := access_model.GetUserRepoPermission(ctx, repo, ctx.Doer) + if err != nil { + ctx.Error(http.StatusInternalServerError, "GetUserRepoPermission", err) + } + results[i] = convert.ToRepo(ctx, repo, permission) + } + + ctx.SetLinkHeader(int(count), opts.ListOptions.PageSize) + ctx.SetTotalCountHeader(count) + ctx.JSON(http.StatusOK, &results) +} + +// ListOrgRepos - list the repositories of an organization. +func ListOrgRepos(ctx *context.APIContext) { + // swagger:operation GET /orgs/{org}/repos organization orgListRepos + // --- + // summary: List an organization's repos + // produces: + // - application/json + // parameters: + // - name: org + // in: path + // description: name of the organization + // type: string + // required: true + // - name: page + // in: query + // description: page number of results to return (1-based) + // type: integer + // - name: limit + // in: query + // description: page size of results + // type: integer + // responses: + // "200": + // "$ref": "#/responses/RepositoryList" + // "404": + // "$ref": "#/responses/notFound" + + listUserRepos(ctx, ctx.Org.Organization.AsUser(), ctx.IsSigned) +} diff --git a/routers/api/v1/user/runners.go b/routers/api/v1/user/runners.go new file mode 100644 index 0000000..8992184 --- /dev/null +++ b/routers/api/v1/user/runners.go @@ -0,0 +1,26 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package user + +import ( + "code.gitea.io/gitea/routers/api/v1/shared" + "code.gitea.io/gitea/services/context" +) + +// https://docs.github.com/en/rest/actions/self-hosted-runners?apiVersion=2022-11-28#create-a-registration-token-for-an-organization + +// GetRegistrationToken returns the token to register user runners +func GetRegistrationToken(ctx *context.APIContext) { + // swagger:operation GET /user/actions/runners/registration-token user userGetRunnerRegistrationToken + // --- + // summary: Get an user's actions runner registration token + // produces: + // - application/json + // parameters: + // responses: + // "200": + // "$ref": "#/responses/RegistrationToken" + + shared.GetRegistrationToken(ctx, ctx.Doer.ID, 0) +} diff --git a/routers/api/v1/user/settings.go b/routers/api/v1/user/settings.go new file mode 100644 index 0000000..bfd2401 --- /dev/null +++ b/routers/api/v1/user/settings.go @@ -0,0 +1,67 @@ +// Copyright 2021 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package user + +import ( + "net/http" + + "code.gitea.io/gitea/modules/optional" + api "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/modules/web" + "code.gitea.io/gitea/services/context" + "code.gitea.io/gitea/services/convert" + user_service "code.gitea.io/gitea/services/user" +) + +// GetUserSettings returns user settings +func GetUserSettings(ctx *context.APIContext) { + // swagger:operation GET /user/settings user getUserSettings + // --- + // summary: Get user settings + // produces: + // - application/json + // responses: + // "200": + // "$ref": "#/responses/UserSettings" + ctx.JSON(http.StatusOK, convert.User2UserSettings(ctx.Doer)) +} + +// UpdateUserSettings returns user settings +func UpdateUserSettings(ctx *context.APIContext) { + // swagger:operation PATCH /user/settings user updateUserSettings + // --- + // summary: Update user settings + // parameters: + // - name: body + // in: body + // schema: + // "$ref": "#/definitions/UserSettingsOptions" + // produces: + // - application/json + // responses: + // "200": + // "$ref": "#/responses/UserSettings" + + form := web.GetForm(ctx).(*api.UserSettingsOptions) + + opts := &user_service.UpdateOptions{ + FullName: optional.FromPtr(form.FullName), + Description: optional.FromPtr(form.Description), + Pronouns: optional.FromPtr(form.Pronouns), + Website: optional.FromPtr(form.Website), + Location: optional.FromPtr(form.Location), + Language: optional.FromPtr(form.Language), + Theme: optional.FromPtr(form.Theme), + DiffViewStyle: optional.FromPtr(form.DiffViewStyle), + KeepEmailPrivate: optional.FromPtr(form.HideEmail), + KeepActivityPrivate: optional.FromPtr(form.HideActivity), + EnableRepoUnitHints: optional.FromPtr(form.EnableRepoUnitHints), + } + if err := user_service.UpdateUser(ctx, ctx.Doer, opts); err != nil { + ctx.InternalServerError(err) + return + } + + ctx.JSON(http.StatusOK, convert.User2UserSettings(ctx.Doer)) +} diff --git a/routers/api/v1/user/star.go b/routers/api/v1/user/star.go new file mode 100644 index 0000000..cb9e05f --- /dev/null +++ b/routers/api/v1/user/star.go @@ -0,0 +1,197 @@ +// Copyright 2016 The Gogs Authors. All rights reserved. +// Copyright 2020 The Gitea Authors. +// Copyright 2024 The Forgejo Authors. +// SPDX-License-Identifier: MIT + +package user + +import ( + std_context "context" + "net/http" + + "code.gitea.io/gitea/models/db" + access_model "code.gitea.io/gitea/models/perm/access" + repo_model "code.gitea.io/gitea/models/repo" + user_model "code.gitea.io/gitea/models/user" + api "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/routers/api/v1/utils" + "code.gitea.io/gitea/services/context" + "code.gitea.io/gitea/services/convert" + "code.gitea.io/gitea/services/repository" +) + +// getStarredRepos returns the repos that the user with the specified userID has +// starred +func getStarredRepos(ctx std_context.Context, user *user_model.User, private bool, listOptions db.ListOptions) ([]*api.Repository, error) { + starredRepos, err := repo_model.GetStarredRepos(ctx, user.ID, private, listOptions) + if err != nil { + return nil, err + } + + repos := make([]*api.Repository, len(starredRepos)) + for i, starred := range starredRepos { + permission, err := access_model.GetUserRepoPermission(ctx, starred, user) + if err != nil { + return nil, err + } + repos[i] = convert.ToRepo(ctx, starred, permission) + } + return repos, nil +} + +// GetStarredRepos returns the repos that the given user has starred +func GetStarredRepos(ctx *context.APIContext) { + // swagger:operation GET /users/{username}/starred user userListStarred + // --- + // summary: The repos that the given user has starred + // produces: + // - application/json + // parameters: + // - name: username + // in: path + // description: username of user + // type: string + // required: true + // - name: page + // in: query + // description: page number of results to return (1-based) + // type: integer + // - name: limit + // in: query + // description: page size of results + // type: integer + // responses: + // "200": + // "$ref": "#/responses/RepositoryList" + // "404": + // "$ref": "#/responses/notFound" + + private := ctx.ContextUser.ID == ctx.Doer.ID + repos, err := getStarredRepos(ctx, ctx.ContextUser, private, utils.GetListOptions(ctx)) + if err != nil { + ctx.Error(http.StatusInternalServerError, "getStarredRepos", err) + return + } + + ctx.SetTotalCountHeader(int64(ctx.ContextUser.NumStars)) + ctx.JSON(http.StatusOK, &repos) +} + +// GetMyStarredRepos returns the repos that the authenticated user has starred +func GetMyStarredRepos(ctx *context.APIContext) { + // swagger:operation GET /user/starred user userCurrentListStarred + // --- + // summary: The repos that the authenticated user has starred + // parameters: + // - name: page + // in: query + // description: page number of results to return (1-based) + // type: integer + // - name: limit + // in: query + // description: page size of results + // type: integer + // produces: + // - application/json + // responses: + // "200": + // "$ref": "#/responses/RepositoryList" + + repos, err := getStarredRepos(ctx, ctx.Doer, true, utils.GetListOptions(ctx)) + if err != nil { + ctx.Error(http.StatusInternalServerError, "getStarredRepos", err) + } + + ctx.SetTotalCountHeader(int64(ctx.Doer.NumStars)) + ctx.JSON(http.StatusOK, &repos) +} + +// IsStarring returns whether the authenticated is starring the repo +func IsStarring(ctx *context.APIContext) { + // swagger:operation GET /user/starred/{owner}/{repo} user userCurrentCheckStarring + // --- + // summary: Whether the authenticated is starring the repo + // parameters: + // - name: owner + // in: path + // description: owner of the repo + // type: string + // required: true + // - name: repo + // in: path + // description: name of the repo + // type: string + // required: true + // responses: + // "204": + // "$ref": "#/responses/empty" + // "404": + // "$ref": "#/responses/notFound" + + if repo_model.IsStaring(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID) { + ctx.Status(http.StatusNoContent) + } else { + ctx.NotFound() + } +} + +// Star the repo specified in the APIContext, as the authenticated user +func Star(ctx *context.APIContext) { + // swagger:operation PUT /user/starred/{owner}/{repo} user userCurrentPutStar + // --- + // summary: Star the given repo + // parameters: + // - name: owner + // in: path + // description: owner of the repo to star + // type: string + // required: true + // - name: repo + // in: path + // description: name of the repo to star + // type: string + // required: true + // responses: + // "204": + // "$ref": "#/responses/empty" + // "404": + // "$ref": "#/responses/notFound" + + err := repository.StarRepoAndSendLikeActivities(ctx, *ctx.Doer, ctx.Repo.Repository.ID, true) + if err != nil { + ctx.Error(http.StatusInternalServerError, "StarRepo", err) + return + } + + ctx.Status(http.StatusNoContent) +} + +// Unstar the repo specified in the APIContext, as the authenticated user +func Unstar(ctx *context.APIContext) { + // swagger:operation DELETE /user/starred/{owner}/{repo} user userCurrentDeleteStar + // --- + // summary: Unstar the given repo + // parameters: + // - name: owner + // in: path + // description: owner of the repo to unstar + // type: string + // required: true + // - name: repo + // in: path + // description: name of the repo to unstar + // type: string + // required: true + // responses: + // "204": + // "$ref": "#/responses/empty" + // "404": + // "$ref": "#/responses/notFound" + + err := repository.StarRepoAndSendLikeActivities(ctx, *ctx.Doer, ctx.Repo.Repository.ID, false) + if err != nil { + ctx.Error(http.StatusInternalServerError, "StarRepo", err) + return + } + ctx.Status(http.StatusNoContent) +} diff --git a/routers/api/v1/user/user.go b/routers/api/v1/user/user.go new file mode 100644 index 0000000..4efcb7c --- /dev/null +++ b/routers/api/v1/user/user.go @@ -0,0 +1,306 @@ +// Copyright 2014 The Gogs Authors. All rights reserved. +// Copyright 2020 The Gitea Authors. +// SPDX-License-Identifier: MIT + +package user + +import ( + "fmt" + "net/http" + + activities_model "code.gitea.io/gitea/models/activities" + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/routers/api/v1/utils" + "code.gitea.io/gitea/services/context" + "code.gitea.io/gitea/services/convert" +) + +// Search search users +func Search(ctx *context.APIContext) { + // swagger:operation GET /users/search user userSearch + // --- + // summary: Search for users + // produces: + // - application/json + // parameters: + // - name: q + // in: query + // description: keyword + // type: string + // - name: uid + // in: query + // description: ID of the user to search for + // type: integer + // format: int64 + // - name: page + // in: query + // description: page number of results to return (1-based) + // type: integer + // - name: limit + // in: query + // description: page size of results + // type: integer + // responses: + // "200": + // description: "SearchResults of a successful search" + // schema: + // type: object + // properties: + // ok: + // type: boolean + // data: + // type: array + // items: + // "$ref": "#/definitions/User" + + listOptions := utils.GetListOptions(ctx) + + uid := ctx.FormInt64("uid") + var users []*user_model.User + var maxResults int64 + var err error + + switch uid { + case user_model.GhostUserID: + maxResults = 1 + users = []*user_model.User{user_model.NewGhostUser()} + case user_model.ActionsUserID: + maxResults = 1 + users = []*user_model.User{user_model.NewActionsUser()} + default: + var visible []structs.VisibleType + if ctx.PublicOnly { + visible = []structs.VisibleType{structs.VisibleTypePublic} + } + users, maxResults, err = user_model.SearchUsers(ctx, &user_model.SearchUserOptions{ + Actor: ctx.Doer, + Keyword: ctx.FormTrim("q"), + UID: uid, + Type: user_model.UserTypeIndividual, + Visible: visible, + ListOptions: listOptions, + }) + if err != nil { + ctx.JSON(http.StatusInternalServerError, map[string]any{ + "ok": false, + "error": err.Error(), + }) + return + } + } + + ctx.SetLinkHeader(int(maxResults), listOptions.PageSize) + ctx.SetTotalCountHeader(maxResults) + + ctx.JSON(http.StatusOK, map[string]any{ + "ok": true, + "data": convert.ToUsers(ctx, ctx.Doer, users), + }) +} + +// GetInfo get user's information +func GetInfo(ctx *context.APIContext) { + // swagger:operation GET /users/{username} user userGet + // --- + // summary: Get a user + // produces: + // - application/json + // parameters: + // - name: username + // in: path + // description: username of user to get + // type: string + // required: true + // responses: + // "200": + // "$ref": "#/responses/User" + // "404": + // "$ref": "#/responses/notFound" + + if !user_model.IsUserVisibleToViewer(ctx, ctx.ContextUser, ctx.Doer) { + // fake ErrUserNotExist error message to not leak information about existence + ctx.NotFound("GetUserByName", user_model.ErrUserNotExist{Name: ctx.Params(":username")}) + return + } + ctx.JSON(http.StatusOK, convert.ToUser(ctx, ctx.ContextUser, ctx.Doer)) +} + +// GetAuthenticatedUser get current user's information +func GetAuthenticatedUser(ctx *context.APIContext) { + // swagger:operation GET /user user userGetCurrent + // --- + // summary: Get the authenticated user + // produces: + // - application/json + // responses: + // "200": + // "$ref": "#/responses/User" + + ctx.JSON(http.StatusOK, convert.ToUser(ctx, ctx.Doer, ctx.Doer)) +} + +// GetUserHeatmapData is the handler to get a users heatmap +func GetUserHeatmapData(ctx *context.APIContext) { + // swagger:operation GET /users/{username}/heatmap user userGetHeatmapData + // --- + // summary: Get a user's heatmap + // produces: + // - application/json + // parameters: + // - name: username + // in: path + // description: username of user to get + // type: string + // required: true + // responses: + // "200": + // "$ref": "#/responses/UserHeatmapData" + // "404": + // "$ref": "#/responses/notFound" + + heatmap, err := activities_model.GetUserHeatmapDataByUser(ctx, ctx.ContextUser, ctx.Doer) + if err != nil { + ctx.Error(http.StatusInternalServerError, "GetUserHeatmapDataByUser", err) + return + } + ctx.JSON(http.StatusOK, heatmap) +} + +func ListUserActivityFeeds(ctx *context.APIContext) { + // swagger:operation GET /users/{username}/activities/feeds user userListActivityFeeds + // --- + // summary: List a user's activity feeds + // produces: + // - application/json + // parameters: + // - name: username + // in: path + // description: username of user + // type: string + // required: true + // - name: only-performed-by + // in: query + // description: if true, only show actions performed by the requested user + // type: boolean + // - name: date + // in: query + // description: the date of the activities to be found + // type: string + // format: date + // - name: page + // in: query + // description: page number of results to return (1-based) + // type: integer + // - name: limit + // in: query + // description: page size of results + // type: integer + // responses: + // "200": + // "$ref": "#/responses/ActivityFeedsList" + // "404": + // "$ref": "#/responses/notFound" + + includePrivate := ctx.IsSigned && (ctx.Doer.IsAdmin || ctx.Doer.ID == ctx.ContextUser.ID) + listOptions := utils.GetListOptions(ctx) + + opts := activities_model.GetFeedsOptions{ + RequestedUser: ctx.ContextUser, + Actor: ctx.Doer, + IncludePrivate: includePrivate, + OnlyPerformedBy: ctx.FormBool("only-performed-by"), + Date: ctx.FormString("date"), + ListOptions: listOptions, + } + + feeds, count, err := activities_model.GetFeeds(ctx, opts) + if err != nil { + ctx.Error(http.StatusInternalServerError, "GetFeeds", err) + return + } + ctx.SetTotalCountHeader(count) + + ctx.JSON(http.StatusOK, convert.ToActivities(ctx, feeds, ctx.Doer)) +} + +// ListBlockedUsers list the authenticated user's blocked users. +func ListBlockedUsers(ctx *context.APIContext) { + // swagger:operation GET /user/list_blocked user userListBlockedUsers + // --- + // summary: List the authenticated user's blocked users + // produces: + // - application/json + // parameters: + // - name: page + // in: query + // description: page number of results to return (1-based) + // type: integer + // - name: limit + // in: query + // description: page size of results + // type: integer + // responses: + // "200": + // "$ref": "#/responses/BlockedUserList" + + utils.ListUserBlockedUsers(ctx, ctx.Doer) +} + +// BlockUser blocks a user from the doer. +func BlockUser(ctx *context.APIContext) { + // swagger:operation PUT /user/block/{username} user userBlockUser + // --- + // summary: Blocks a user from the doer. + // produces: + // - application/json + // parameters: + // - name: username + // in: path + // description: username of the user + // type: string + // required: true + // responses: + // "204": + // "$ref": "#/responses/empty" + // "404": + // "$ref": "#/responses/notFound" + // "422": + // "$ref": "#/responses/validationError" + + if ctx.ContextUser.IsOrganization() { + ctx.Error(http.StatusUnprocessableEntity, "", fmt.Errorf("%s is an organization not a user", ctx.ContextUser.Name)) + return + } + + utils.BlockUser(ctx, ctx.Doer, ctx.ContextUser) +} + +// UnblockUser unblocks a user from the doer. +func UnblockUser(ctx *context.APIContext) { + // swagger:operation PUT /user/unblock/{username} user userUnblockUser + // --- + // summary: Unblocks a user from the doer. + // produces: + // - application/json + // parameters: + // - name: username + // in: path + // description: username of the user + // type: string + // required: true + // responses: + // "204": + // "$ref": "#/responses/empty" + // "404": + // "$ref": "#/responses/notFound" + // "422": + // "$ref": "#/responses/validationError" + + if ctx.ContextUser.IsOrganization() { + ctx.Error(http.StatusUnprocessableEntity, "", fmt.Errorf("%s is an organization not a user", ctx.ContextUser.Name)) + return + } + + utils.UnblockUser(ctx, ctx.Doer, ctx.ContextUser) +} diff --git a/routers/api/v1/user/watch.go b/routers/api/v1/user/watch.go new file mode 100644 index 0000000..706f4cc --- /dev/null +++ b/routers/api/v1/user/watch.go @@ -0,0 +1,211 @@ +// Copyright 2016 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package user + +import ( + std_context "context" + "net/http" + + "code.gitea.io/gitea/models/db" + access_model "code.gitea.io/gitea/models/perm/access" + repo_model "code.gitea.io/gitea/models/repo" + user_model "code.gitea.io/gitea/models/user" + api "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/routers/api/v1/utils" + "code.gitea.io/gitea/services/context" + "code.gitea.io/gitea/services/convert" +) + +// getWatchedRepos returns the repos that the user with the specified userID is watching +func getWatchedRepos(ctx std_context.Context, user *user_model.User, private bool, listOptions db.ListOptions) ([]*api.Repository, int64, error) { + watchedRepos, total, err := repo_model.GetWatchedRepos(ctx, user.ID, private, listOptions) + if err != nil { + return nil, 0, err + } + + repos := make([]*api.Repository, len(watchedRepos)) + for i, watched := range watchedRepos { + permission, err := access_model.GetUserRepoPermission(ctx, watched, user) + if err != nil { + return nil, 0, err + } + repos[i] = convert.ToRepo(ctx, watched, permission) + } + return repos, total, nil +} + +// GetWatchedRepos returns the repos that the user specified in ctx is watching +func GetWatchedRepos(ctx *context.APIContext) { + // swagger:operation GET /users/{username}/subscriptions user userListSubscriptions + // --- + // summary: List the repositories watched by a user + // produces: + // - application/json + // parameters: + // - name: username + // type: string + // in: path + // description: username of the user + // required: true + // - name: page + // in: query + // description: page number of results to return (1-based) + // type: integer + // - name: limit + // in: query + // description: page size of results + // type: integer + // responses: + // "200": + // "$ref": "#/responses/RepositoryList" + // "404": + // "$ref": "#/responses/notFound" + + private := ctx.ContextUser.ID == ctx.Doer.ID + repos, total, err := getWatchedRepos(ctx, ctx.ContextUser, private, utils.GetListOptions(ctx)) + if err != nil { + ctx.Error(http.StatusInternalServerError, "getWatchedRepos", err) + } + + ctx.SetTotalCountHeader(total) + ctx.JSON(http.StatusOK, &repos) +} + +// GetMyWatchedRepos returns the repos that the authenticated user is watching +func GetMyWatchedRepos(ctx *context.APIContext) { + // swagger:operation GET /user/subscriptions user userCurrentListSubscriptions + // --- + // summary: List repositories watched by the authenticated user + // produces: + // - application/json + // parameters: + // - name: page + // in: query + // description: page number of results to return (1-based) + // type: integer + // - name: limit + // in: query + // description: page size of results + // type: integer + // responses: + // "200": + // "$ref": "#/responses/RepositoryList" + + repos, total, err := getWatchedRepos(ctx, ctx.Doer, true, utils.GetListOptions(ctx)) + if err != nil { + ctx.Error(http.StatusInternalServerError, "getWatchedRepos", err) + } + + ctx.SetTotalCountHeader(total) + ctx.JSON(http.StatusOK, &repos) +} + +// IsWatching returns whether the authenticated user is watching the repo +// specified in ctx +func IsWatching(ctx *context.APIContext) { + // swagger:operation GET /repos/{owner}/{repo}/subscription repository userCurrentCheckSubscription + // --- + // summary: Check if the current user is watching a repo + // parameters: + // - name: owner + // in: path + // description: owner of the repo + // type: string + // required: true + // - name: repo + // in: path + // description: name of the repo + // type: string + // required: true + // responses: + // "200": + // "$ref": "#/responses/WatchInfo" + // "404": + // description: User is not watching this repo or repo do not exist + + if repo_model.IsWatching(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID) { + ctx.JSON(http.StatusOK, api.WatchInfo{ + Subscribed: true, + Ignored: false, + Reason: nil, + CreatedAt: ctx.Repo.Repository.CreatedUnix.AsTime(), + URL: subscriptionURL(ctx.Repo.Repository), + RepositoryURL: ctx.Repo.Repository.APIURL(), + }) + } else { + ctx.NotFound() + } +} + +// Watch the repo specified in ctx, as the authenticated user +func Watch(ctx *context.APIContext) { + // swagger:operation PUT /repos/{owner}/{repo}/subscription repository userCurrentPutSubscription + // --- + // summary: Watch a repo + // parameters: + // - name: owner + // in: path + // description: owner of the repo + // type: string + // required: true + // - name: repo + // in: path + // description: name of the repo + // type: string + // required: true + // responses: + // "200": + // "$ref": "#/responses/WatchInfo" + // "404": + // "$ref": "#/responses/notFound" + + err := repo_model.WatchRepo(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID, true) + if err != nil { + ctx.Error(http.StatusInternalServerError, "WatchRepo", err) + return + } + ctx.JSON(http.StatusOK, api.WatchInfo{ + Subscribed: true, + Ignored: false, + Reason: nil, + CreatedAt: ctx.Repo.Repository.CreatedUnix.AsTime(), + URL: subscriptionURL(ctx.Repo.Repository), + RepositoryURL: ctx.Repo.Repository.APIURL(), + }) +} + +// Unwatch the repo specified in ctx, as the authenticated user +func Unwatch(ctx *context.APIContext) { + // swagger:operation DELETE /repos/{owner}/{repo}/subscription repository userCurrentDeleteSubscription + // --- + // summary: Unwatch a repo + // parameters: + // - name: owner + // in: path + // description: owner of the repo + // type: string + // required: true + // - name: repo + // in: path + // description: name of the repo + // type: string + // required: true + // responses: + // "204": + // "$ref": "#/responses/empty" + // "404": + // "$ref": "#/responses/notFound" + + err := repo_model.WatchRepo(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID, false) + if err != nil { + ctx.Error(http.StatusInternalServerError, "UnwatchRepo", err) + return + } + ctx.Status(http.StatusNoContent) +} + +// subscriptionURL returns the URL of the subscription API endpoint of a repo +func subscriptionURL(repo *repo_model.Repository) string { + return repo.APIURL() + "/subscription" +} |