forgejo/routers/api/v1/user/app.go
0ko 022ab86988 chore(api): update swagger method descripitons (#8728)
Speaking from personal experience, when exploring the API I find myself trying to parse the exact meaning of many descriptions for a while, and I also have to get used to many different kinds of inconsistencies and grammar issues.

This PR improves a few of these. Some I tried to reword to make them easier to understand, for others I just improved consistency a little, like capitalization. This area needs more work, this PR just makes some progress. Anything that is improved in this one can be improved further in later PRs, so in review please focus on regressions if you find any.

Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/8728
Reviewed-by: Earl Warren <earl-warren@noreply.codeberg.org>
Co-authored-by: 0ko <0ko@noreply.codeberg.org>
Co-committed-by: 0ko <0ko@noreply.codeberg.org>
2025-07-30 18:08:28 +02:00

434 lines
12 KiB
Go

// 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 "forgejo.org/models/auth"
"forgejo.org/models/db"
api "forgejo.org/modules/structs"
"forgejo.org/modules/web"
"forgejo.org/routers/api/v1/utils"
"forgejo.org/services/context"
"forgejo.org/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 creates an access token for doer
func CreateAccessToken(ctx *context.APIContext) {
// swagger:operation POST /users/{username}/tokens user userCreateToken
// ---
// summary: Generate an access token for the current user
// 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 deletes an access token from doer's account
func DeleteAccessToken(ctx *context.APIContext) {
// swagger:operation DELETE /users/{username}/tokens/{token} user userDeleteAccessToken
// ---
// summary: Delete an access token from current user's account
// 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 returns an 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 updates an 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))
}