// Copyright 2026 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT // Package v2 Gitea API v2 // // This is the v2 API with improved error handling, batch operations, // and AI-friendly endpoints. It uses structured error codes for // machine-readable error handling. // // Schemes: https, http // License: MIT http://opensource.org/licenses/MIT // // Consumes: // - application/json // // Produces: // - application/json // - application/x-ndjson // // swagger:meta package v2 import ( "net/http" "strings" access_model "code.gitea.io/gitea/models/perm/access" auth_model "code.gitea.io/gitea/models/auth" repo_model "code.gitea.io/gitea/models/repo" user_model "code.gitea.io/gitea/models/user" apierrors "code.gitea.io/gitea/modules/errors" "code.gitea.io/gitea/modules/graceful" "code.gitea.io/gitea/modules/idempotency" "code.gitea.io/gitea/modules/setting" api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/modules/web/middleware" "code.gitea.io/gitea/routers/common" "code.gitea.io/gitea/services/auth" "code.gitea.io/gitea/services/context" "github.com/go-chi/cors" ) // Routes registers all v2 API routes to web application. func Routes() *web.Router { m := web.NewRouter() m.Use(middleware.RequestID()) m.Use(middleware.RateLimitInfo()) m.Use(securityHeaders()) // Idempotency middleware for POST/PUT/PATCH requests idempotencyMiddleware := idempotency.NewMiddleware(idempotency.GetDefaultStore()) m.Use(idempotencyMiddleware.Handler) if setting.CORSConfig.Enabled { m.Use(cors.Handler(cors.Options{ AllowedOrigins: setting.CORSConfig.AllowDomain, AllowedMethods: setting.CORSConfig.Methods, AllowCredentials: setting.CORSConfig.AllowCredentials, AllowedHeaders: append([]string{"Authorization", "X-Gitea-OTP"}, setting.CORSConfig.Headers...), MaxAge: int(setting.CORSConfig.MaxAge.Seconds()), })) } m.Use(context.APIContexter()) // Get user from session if logged in m.Use(apiAuth(buildAuthGroup())) m.Group("", func() { // Public endpoints (no auth required) m.Get("/version", Version) // API Documentation (Scalar) m.Get("/docs", DocsScalar) m.Get("/swagger.json", SwaggerJSON) // Health check endpoints m.Group("/health", func() { m.Get("", HealthCheck) m.Get("/live", LivenessCheck) m.Get("/ready", ReadinessCheck) m.Get("/component/{component}", ComponentHealthCheck) }) // Operation progress endpoints (SSE) m.Group("/operations", func() { m.Get("/{id}/progress", OperationProgress) m.Get("/{id}", GetOperation) m.Delete("/{id}", CancelOperation) }) // Authenticated endpoints m.Group("", func() { // User info m.Get("/user", GetAuthenticatedUser) // Batch operations - efficient bulk requests m.Group("/batch", func() { m.Post("/files", BatchGetFiles) m.Post("/repos", BatchGetRepos) }) // Streaming endpoints - NDJSON responses m.Group("/stream", func() { m.Post("/files", StreamFiles) m.Post("/commits", StreamCommits) m.Post("/issues", StreamIssues) }) // AI context endpoints - rich context for AI tools m.Group("/ai", func() { m.Post("/repo/summary", GetAIRepoSummary) m.Post("/repo/navigation", GetAINavigation) m.Post("/issue/context", GetAIIssueContext) }) }, reqToken()) // Wiki v2 API - repository wiki endpoints m.Group("/repos/{owner}/{repo}/wiki", func() { // Public read endpoints (access checked in handler) m.Get("/pages", ListWikiPagesV2) m.Get("/pages/{pageName}", GetWikiPageV2) m.Get("/pages/{pageName}/revisions", GetWikiPageRevisionsV2) m.Get("/search", SearchWikiV2) m.Get("/graph", GetWikiGraphV2) m.Get("/stats", GetWikiStatsV2) // Write endpoints require authentication m.Group("", func() { m.Post("/pages", web.Bind(api.CreateWikiPageV2Option{}), CreateWikiPageV2) m.Put("/pages/{pageName}", web.Bind(api.UpdateWikiPageV2Option{}), UpdateWikiPageV2) m.Delete("/pages/{pageName}", DeleteWikiPageV2) }, reqToken()) }) // Actions v2 API - AI-friendly runner capability discovery m.Group("/repos/{owner}/{repo}/actions", func() { m.Get("/runners/capabilities", repoAssignment(), GetActionsCapabilities) m.Post("/workflows/validate", repoAssignment(), reqToken(), web.Bind(api.WorkflowValidationRequest{}), ValidateWorkflow) }) }) return m } func securityHeaders() func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { // CORS preflight if req.Method == http.MethodOptions { return } next.ServeHTTP(w, req) }) } } func buildAuthGroup() *auth.Group { group := auth.NewGroup( &auth.OAuth2{}, &auth.HTTPSign{}, &auth.Basic{}, ) if setting.Service.EnableReverseProxyAuthAPI { group.Add(&auth.ReverseProxy{}) } if setting.IsWindows && auth_model.IsSSPIEnabled(graceful.GetManager().ShutdownContext()) { group.Add(&auth.SSPI{}) } return group } func apiAuth(authMethod auth.Method) func(*context.APIContext) { return func(ctx *context.APIContext) { ar, err := common.AuthShared(ctx.Base, nil, authMethod) if err != nil { msg, ok := auth.ErrAsUserAuthMessage(err) msg = util.Iif(ok, msg, "invalid username, password or token") ctx.APIErrorWithCodeAndMessage(apierrors.AuthInvalidCredentials, msg) return } ctx.Doer = ar.Doer ctx.IsSigned = ar.Doer != nil ctx.IsBasicAuth = ar.IsBasicAuth } } // reqToken requires authentication func reqToken() func(ctx *context.APIContext) { return func(ctx *context.APIContext) { if !ctx.IsSigned { ctx.APIErrorWithCode(apierrors.AuthTokenMissing) return } } } // repoAssignment loads the repository from path parameters func repoAssignment() func(ctx *context.APIContext) { return func(ctx *context.APIContext) { ownerName := ctx.PathParam("owner") repoName := ctx.PathParam("repo") var ( owner *user_model.User err error ) // Check if the user is the same as the repository owner if ctx.IsSigned && strings.EqualFold(ctx.Doer.LowerName, ownerName) { owner = ctx.Doer } else { owner, err = user_model.GetUserByName(ctx, ownerName) if err != nil { if user_model.IsErrUserNotExist(err) { ctx.APIErrorNotFound("GetUserByName", err) } else { ctx.APIErrorInternal(err) } return } } ctx.Repo.Owner = owner ctx.ContextUser = owner // Get repository repo, err := repo_model.GetRepositoryByName(ctx, owner.ID, repoName) if err != nil { if repo_model.IsErrRepoNotExist(err) { ctx.APIErrorNotFound("GetRepositoryByName", err) } else { ctx.APIErrorInternal(err) } return } repo.Owner = owner ctx.Repo.Repository = repo // Get permissions ctx.Repo.Permission, err = access_model.GetUserRepoPermission(ctx, repo, ctx.Doer) if err != nil { ctx.APIErrorInternal(err) return } if !ctx.Repo.Permission.HasAnyUnitAccessOrPublicAccess() { ctx.APIErrorNotFound("HasAnyUnitAccessOrPublicAccess") return } } }