summaryrefslogtreecommitdiffstats
path: root/routers/api/v1/user
diff options
context:
space:
mode:
authorDaniel Baumann <daniel@debian.org>2024-10-18 20:33:49 +0200
committerDaniel Baumann <daniel@debian.org>2024-12-12 23:57:56 +0100
commite68b9d00a6e05b3a941f63ffb696f91e554ac5ec (patch)
tree97775d6c13b0f416af55314eb6a89ef792474615 /routers/api/v1/user
parentInitial commit. (diff)
downloadforgejo-e68b9d00a6e05b3a941f63ffb696f91e554ac5ec.tar.xz
forgejo-e68b9d00a6e05b3a941f63ffb696f91e554ac5ec.zip
Adding upstream version 9.0.3.
Signed-off-by: Daniel Baumann <daniel@debian.org>
Diffstat (limited to 'routers/api/v1/user')
-rw-r--r--routers/api/v1/user/action.go381
-rw-r--r--routers/api/v1/user/app.go434
-rw-r--r--routers/api/v1/user/avatar.go73
-rw-r--r--routers/api/v1/user/email.go144
-rw-r--r--routers/api/v1/user/follower.go281
-rw-r--r--routers/api/v1/user/gpg_key.go333
-rw-r--r--routers/api/v1/user/helper.go35
-rw-r--r--routers/api/v1/user/hook.go179
-rw-r--r--routers/api/v1/user/key.go317
-rw-r--r--routers/api/v1/user/quota.go128
-rw-r--r--routers/api/v1/user/repo.go190
-rw-r--r--routers/api/v1/user/runners.go30
-rw-r--r--routers/api/v1/user/settings.go75
-rw-r--r--routers/api/v1/user/star.go213
-rw-r--r--routers/api/v1/user/user.go322
-rw-r--r--routers/api/v1/user/watch.go215
16 files changed, 3350 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..ec5289f
--- /dev/null
+++ b/routers/api/v1/user/action.go
@@ -0,0 +1,381 @@
+// 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"
+ // "401":
+ // "$ref": "#/responses/unauthorized"
+ // "403":
+ // "$ref": "#/responses/forbidden"
+ // "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"
+ // "401":
+ // "$ref": "#/responses/unauthorized"
+ // "403":
+ // "$ref": "#/responses/forbidden"
+ // "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"
+ // "401":
+ // "$ref": "#/responses/unauthorized"
+ // "403":
+ // "$ref": "#/responses/forbidden"
+ // "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"
+ // "401":
+ // "$ref": "#/responses/unauthorized"
+ // "403":
+ // "$ref": "#/responses/forbidden"
+ // "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"
+ // "401":
+ // "$ref": "#/responses/unauthorized"
+ // "403":
+ // "$ref": "#/responses/forbidden"
+ // "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"
+ // "401":
+ // "$ref": "#/responses/unauthorized"
+ // "403":
+ // "$ref": "#/responses/forbidden"
+ // "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"
+ // "401":
+ // "$ref": "#/responses/unauthorized"
+ // "403":
+ // "$ref": "#/responses/forbidden"
+ // "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..c4fb2ea
--- /dev/null
+++ b/routers/api/v1/user/app.go
@@ -0,0 +1,434 @@
+// 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"
+ // "404":
+ // "$ref": "#/responses/notFound"
+
+ 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"
+ // "404":
+ // "$ref": "#/responses/notFound"
+
+ 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"
+ // "401":
+ // "$ref": "#/responses/unauthorized"
+ // "403":
+ // "$ref": "#/responses/forbidden"
+
+ 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"
+ // "401":
+ // "$ref": "#/responses/unauthorized"
+ // "403":
+ // "$ref": "#/responses/forbidden"
+
+ 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"
+ // "401":
+ // "$ref": "#/responses/unauthorized"
+ // "403":
+ // "$ref": "#/responses/forbidden"
+ // "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"
+ // "401":
+ // "$ref": "#/responses/unauthorized"
+ // "403":
+ // "$ref": "#/responses/forbidden"
+ // "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"
+ // "401":
+ // "$ref": "#/responses/unauthorized"
+ // "403":
+ // "$ref": "#/responses/forbidden"
+ // "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..d3833a3
--- /dev/null
+++ b/routers/api/v1/user/avatar.go
@@ -0,0 +1,73 @@
+// 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"
+ // "401":
+ // "$ref": "#/responses/unauthorized"
+ // "403":
+ // "$ref": "#/responses/forbidden"
+ 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"
+ // "401":
+ // "$ref": "#/responses/unauthorized"
+ // "403":
+ // "$ref": "#/responses/forbidden"
+ 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..af5d355
--- /dev/null
+++ b/routers/api/v1/user/email.go
@@ -0,0 +1,144 @@
+// 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"
+ // "401":
+ // "$ref": "#/responses/unauthorized"
+ // "403":
+ // "$ref": "#/responses/forbidden"
+
+ 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"
+ // "401":
+ // "$ref": "#/responses/unauthorized"
+ // "403":
+ // "$ref": "#/responses/forbidden"
+ // "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"
+ // "401":
+ // "$ref": "#/responses/unauthorized"
+ // "403":
+ // "$ref": "#/responses/forbidden"
+ // "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..784e232
--- /dev/null
+++ b/routers/api/v1/user/follower.go
@@ -0,0 +1,281 @@
+// 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"
+ // "401":
+ // "$ref": "#/responses/unauthorized"
+ // "403":
+ // "$ref": "#/responses/forbidden"
+
+ 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"
+ // "401":
+ // "$ref": "#/responses/unauthorized"
+ // "403":
+ // "$ref": "#/responses/forbidden"
+
+ 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"
+ // "401":
+ // "$ref": "#/responses/unauthorized"
+ // "403":
+ // "$ref": "#/responses/forbidden"
+ // "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"
+ // "401":
+ // "$ref": "#/responses/unauthorized"
+ // "403":
+ // "$ref": "#/responses/forbidden"
+ // "404":
+ // "$ref": "#/responses/notFound"
+
+ 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"
+ // "401":
+ // "$ref": "#/responses/unauthorized"
+ // "403":
+ // "$ref": "#/responses/forbidden"
+ // "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..2fe4eb8
--- /dev/null
+++ b/routers/api/v1/user/gpg_key.go
@@ -0,0 +1,333 @@
+// 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"
+ // "401":
+ // "$ref": "#/responses/unauthorized"
+ // "403":
+ // "$ref": "#/responses/forbidden"
+
+ 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"
+ // "401":
+ // "$ref": "#/responses/unauthorized"
+ // "403":
+ // "$ref": "#/responses/forbidden"
+ // "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"
+ // "401":
+ // "$ref": "#/responses/unauthorized"
+ // "403":
+ // "$ref": "#/responses/forbidden"
+ // "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"
+ // "401":
+ // "$ref": "#/responses/unauthorized"
+ // "403":
+ // "$ref": "#/responses/forbidden"
+ // "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"
+ // "401":
+ // "$ref": "#/responses/unauthorized"
+ // "403":
+ // "$ref": "#/responses/forbidden"
+ // "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"
+ // "401":
+ // "$ref": "#/responses/unauthorized"
+ // "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..47b6498
--- /dev/null
+++ b/routers/api/v1/user/hook.go
@@ -0,0 +1,179 @@
+// 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"
+ // "401":
+ // "$ref": "#/responses/unauthorized"
+ // "403":
+ // "$ref": "#/responses/forbidden"
+
+ 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"
+ // "401":
+ // "$ref": "#/responses/unauthorized"
+ // "403":
+ // "$ref": "#/responses/forbidden"
+
+ 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"
+ // "401":
+ // "$ref": "#/responses/unauthorized"
+ // "403":
+ // "$ref": "#/responses/forbidden"
+
+ 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"
+ // "401":
+ // "$ref": "#/responses/unauthorized"
+ // "403":
+ // "$ref": "#/responses/forbidden"
+
+ 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"
+ // "401":
+ // "$ref": "#/responses/unauthorized"
+ // "403":
+ // "$ref": "#/responses/forbidden"
+
+ 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..1b4ba0a
--- /dev/null
+++ b/routers/api/v1/user/key.go
@@ -0,0 +1,317 @@
+// 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"
+ // "401":
+ // "$ref": "#/responses/unauthorized"
+ // "403":
+ // "$ref": "#/responses/forbidden"
+
+ 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"
+ // "401":
+ // "$ref": "#/responses/unauthorized"
+ // "403":
+ // "$ref": "#/responses/forbidden"
+ // "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"
+ // "401":
+ // "$ref": "#/responses/unauthorized"
+ // "403":
+ // "$ref": "#/responses/forbidden"
+ // "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"
+ // "401":
+ // "$ref": "#/responses/unauthorized"
+ // "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..ab2881b
--- /dev/null
+++ b/routers/api/v1/user/quota.go
@@ -0,0 +1,128 @@
+// 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"
+ // "401":
+ // "$ref": "#/responses/unauthorized"
+ // "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"
+ // "401":
+ // "$ref": "#/responses/unauthorized"
+ // "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"
+ // "401":
+ // "$ref": "#/responses/unauthorized"
+ // "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"
+ // "401":
+ // "$ref": "#/responses/unauthorized"
+ // "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"
+ // "401":
+ // "$ref": "#/responses/unauthorized"
+ // "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..f2e11e4
--- /dev/null
+++ b/routers/api/v1/user/repo.go
@@ -0,0 +1,190 @@
+// 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"
+ // "401":
+ // "$ref": "#/responses/unauthorized"
+ // "403":
+ // "$ref": "#/responses/forbidden"
+ // "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..dc4c187
--- /dev/null
+++ b/routers/api/v1/user/runners.go
@@ -0,0 +1,30 @@
+// 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"
+ // "401":
+ // "$ref": "#/responses/unauthorized"
+ // "403":
+ // "$ref": "#/responses/forbidden"
+
+ 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..173f06e
--- /dev/null
+++ b/routers/api/v1/user/settings.go
@@ -0,0 +1,75 @@
+// 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"
+ // "401":
+ // "$ref": "#/responses/unauthorized"
+ // "403":
+ // "$ref": "#/responses/forbidden"
+ 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"
+ // "401":
+ // "$ref": "#/responses/unauthorized"
+ // "403":
+ // "$ref": "#/responses/forbidden"
+
+ 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..be84b13
--- /dev/null
+++ b/routers/api/v1/user/star.go
@@ -0,0 +1,213 @@
+// 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"
+ // "401":
+ // "$ref": "#/responses/unauthorized"
+ // "403":
+ // "$ref": "#/responses/forbidden"
+
+ 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"
+ // "401":
+ // "$ref": "#/responses/unauthorized"
+ // "403":
+ // "$ref": "#/responses/forbidden"
+ // "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"
+ // "401":
+ // "$ref": "#/responses/unauthorized"
+ // "403":
+ // "$ref": "#/responses/forbidden"
+ // "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"
+ // "401":
+ // "$ref": "#/responses/unauthorized"
+ // "403":
+ // "$ref": "#/responses/forbidden"
+ // "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..6c8cde7
--- /dev/null
+++ b/routers/api/v1/user/user.go
@@ -0,0 +1,322 @@
+// 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"
+ // "401":
+ // "$ref": "#/responses/unauthorized"
+ // "403":
+ // "$ref": "#/responses/forbidden"
+
+ 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"
+ // "401":
+ // "$ref": "#/responses/unauthorized"
+ // "403":
+ // "$ref": "#/responses/forbidden"
+
+ 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"
+ // "401":
+ // "$ref": "#/responses/unauthorized"
+ // "403":
+ // "$ref": "#/responses/forbidden"
+ // "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"
+ // "401":
+ // "$ref": "#/responses/unauthorized"
+ // "403":
+ // "$ref": "#/responses/forbidden"
+ // "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..dc27a38
--- /dev/null
+++ b/routers/api/v1/user/watch.go
@@ -0,0 +1,215 @@
+// 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"
+ // "401":
+ // "$ref": "#/responses/unauthorized"
+ // "403":
+ // "$ref": "#/responses/forbidden"
+
+ 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"
+}