2
0
Files
logikonline 4fabef6a65
All checks were successful
Build and Release / Build Binaries (amd64, windows, windows-latest) (push) Has been skipped
Build and Release / Build Binaries (arm64, darwin, macos) (push) Has been skipped
Build and Release / Build Binary (linux/arm64) (push) Has been skipped
Build and Release / Create Release (push) Has been skipped
Build and Release / Unit Tests (push) Successful in 3m59s
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 8m32s
Build and Release / Lint (push) Successful in 9m40s
Build and Release / Build Binaries (amd64, linux, linux-latest) (push) Has been skipped
Build and Release / Build Binaries (amd64, darwin, macos) (push) Has been skipped
feat(api): add organization management API v2 and MCP tools
Adds comprehensive v2 API endpoints for organization management:
- GET /api/v2/user/orgs - list user's organizations
- GET /api/v2/orgs/{org}/overview - org overview with pinned repos, members, stats, profile, recent activity
- GET /api/v2/orgs/{org}/repos - list org repositories with grouping support
- GET /api/v2/orgs/{org}/profile/readme - get org profile README
- PATCH /api/v2/orgs/{org} - update org metadata (requires owner/admin)
- PUT /api/v2/orgs/{org}/profile/readme - update profile README
- POST /api/v2/orgs/{org}/pinned - pin repository
- DELETE /api/v2/orgs/{org}/pinned/{repo} - unpin repository

Adds 8 MCP tools for AI assistant access: list_orgs, get_org_overview, update_org, list_org_repos, get_org_profile_readme, update_org_profile_readme, pin_org_repo, unpin_org_repo.

Introduces OrgOverviewV2 and OrgRecentActivity structs. Adds org/profile service for README and pinning operations.
2026-04-19 17:32:06 -04:00

394 lines
13 KiB
Go

// Copyright 2026 MarketAlly. 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"
auth_model "code.gitcaddy.com/server/v3/models/auth"
access_model "code.gitcaddy.com/server/v3/models/perm/access"
repo_model "code.gitcaddy.com/server/v3/models/repo"
user_model "code.gitcaddy.com/server/v3/models/user"
apierrors "code.gitcaddy.com/server/v3/modules/errors"
"code.gitcaddy.com/server/v3/modules/graceful"
"code.gitcaddy.com/server/v3/modules/idempotency"
"code.gitcaddy.com/server/v3/modules/setting"
api "code.gitcaddy.com/server/v3/modules/structs"
"code.gitcaddy.com/server/v3/modules/util"
"code.gitcaddy.com/server/v3/modules/web"
"code.gitcaddy.com/server/v3/modules/web/middleware"
"code.gitcaddy.com/server/v3/routers/common"
"code.gitcaddy.com/server/v3/services/auth"
"code.gitcaddy.com/server/v3/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)
})
// MCP Protocol endpoint for AI tool integration
m.Post("/mcp", MCPHandler)
// 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.Get("/runners/status", repoAssignment(), ListRunnersStatus)
m.Get("/runners/{runner_id}/status", repoAssignment(), GetRunnerStatus)
m.Post("/workflows/validate", repoAssignment(), reqToken(), web.Bind(api.WorkflowValidationRequest{}), ValidateWorkflow)
m.Get("/workflows/status", repoAssignment(), ListWorkflowStatuses)
m.Get("/runs/{run_id}/failure-log", repoAssignment(), GetRunFailureLog)
})
// Releases v2 API - Enhanced releases with app update support
// Supports public access for private repos with public_releases enabled
m.Group("/repos/{owner}/{repo}/releases", func() {
// App update endpoint - Electron/Squirrel compatible
// Returns 200 with update info or 204 if no update available
m.Get("/update", repoAssignmentWithPublicAccess(), CheckAppUpdate)
// List and get releases
m.Get("", repoAssignmentWithPublicAccess(), ListReleasesV2)
m.Get("/latest", repoAssignmentWithPublicAccess(), GetLatestReleaseV2)
m.Get("/{tag}", repoAssignmentWithPublicAccess(), GetReleaseV2)
// Release asset upload/management
m.Get("/{id}/assets", repoAssignmentWithPublicAccess(), ListReleaseAttachmentsV2)
m.Post("/{id}/assets", repoAssignment(), reqToken(), CreateReleaseAttachmentV2)
m.Delete("/{id}/assets/{asset_id}", repoAssignment(), reqToken(), DeleteReleaseAttachmentV2)
})
// Upload instructions endpoint
m.Get("/upload/instructions", GetUploadInstructions)
// Landing page API
m.Group("/repos/{owner}/{repo}/pages", func() {
// Public read endpoints
m.Get("/config", repoAssignmentWithPublicAccess(), GetPagesConfig)
m.Get("/content", repoAssignmentWithPublicAccess(), GetPagesContent)
// Write endpoints (require auth + repo admin)
m.Group("/config", func() {
m.Put("", web.Bind(api.UpdatePagesConfigOption{}), UpdatePagesConfig)
m.Patch("", web.Bind(api.UpdatePagesConfigOption{}), PatchPagesConfig)
m.Put("/brand", web.Bind(api.UpdatePagesBrandOption{}), UpdatePagesBrand)
m.Put("/hero", web.Bind(api.UpdatePagesHeroOption{}), UpdatePagesHero)
m.Put("/content", web.Bind(api.UpdatePagesContentOption{}), UpdatePagesContentSection)
m.Put("/comparison", web.Bind(api.UpdatePagesComparisonOption{}), UpdatePagesComparison)
m.Put("/social", web.Bind(api.UpdatePagesSocialOption{}), UpdatePagesSocial)
m.Put("/pricing", web.Bind(api.UpdatePagesPricingOption{}), UpdatePagesPricing)
m.Put("/footer", web.Bind(api.UpdatePagesFooterOption{}), UpdatePagesFooter)
m.Put("/theme", web.Bind(api.UpdatePagesThemeOption{}), UpdatePagesTheme)
}, repoAssignment(), reqToken())
})
// Blog v2 API - repository blog endpoints
m.Group("/repos/{owner}/{repo}/blog/posts", func() {
// Public read endpoints (access checked in handler)
m.Get("", repoAssignment(), ListBlogPostsV2)
m.Get("/{id}", repoAssignment(), GetBlogPostV2)
// Write endpoints require authentication
m.Group("", func() {
m.Post("", repoAssignment(), web.Bind(api.CreateBlogPostV2Option{}), CreateBlogPostV2)
m.Put("/{id}", repoAssignment(), web.Bind(api.UpdateBlogPostV2Option{}), UpdateBlogPostV2)
m.Delete("/{id}", repoAssignment(), DeleteBlogPostV2)
}, reqToken())
})
// Wishlist v2 API - repository wishlist endpoints
m.Group("/repos/{owner}/{repo}/wishlist", func() {
m.Get("/items", repoAssignment(), ListWishlistItemsV2)
m.Get("/items/{id}", repoAssignment(), GetWishlistItemV2)
m.Get("/items/{id}/comments", repoAssignment(), ListWishlistCommentsV2)
m.Get("/categories", repoAssignment(), ListWishlistCategoriesV2)
m.Group("", func() {
m.Post("/items", repoAssignment(), web.Bind(api.CreateWishlistItemV2Option{}), CreateWishlistItemV2)
m.Post("/items/{id}/vote", repoAssignment(), WishlistVoteToggleV2)
m.Post("/items/{id}/importance", repoAssignment(), web.Bind(api.SetWishlistImportanceV2Option{}), WishlistSetImportanceV2)
m.Post("/items/{id}/comments", repoAssignment(), web.Bind(api.CreateWishlistCommentV2Option{}), CreateWishlistCommentV2)
m.Post("/items/{id}/close", repoAssignment(), web.Bind(api.CloseWishlistItemV2Option{}), CloseWishlistItemV2)
m.Post("/items/{id}/reopen", repoAssignment(), ReopenWishlistItemV2)
}, reqToken())
})
// Hidden folders API - manage hidden folders for a repository
m.Group("/repos/{owner}/{repo}/hidden-folders", func() {
m.Get("", repoAssignment(), ListHiddenFoldersV2)
m.Group("", func() {
m.Put("", web.Bind(api.HiddenFolderOptionV2{}), AddHiddenFolderV2)
m.Delete("", web.Bind(api.HiddenFolderOptionV2{}), RemoveHiddenFolderV2)
}, repoAssignment(), reqToken())
})
// AI operations API - repo-scoped AI operations and settings
m.Group("/repos/{owner}/{repo}/ai", func() {
m.Get("/settings", GetRepoAISettings)
m.Get("/operations", ListAIOperations)
m.Get("/operations/{id}", GetAIOperation)
m.Group("", func() {
m.Put("/settings", web.Bind(api.UpdateAISettingsOption{}), UpdateRepoAISettings)
m.Post("/review/{pull}", TriggerAIReview)
m.Post("/respond/{issue}", TriggerAIRespond)
m.Post("/triage/{issue}", TriggerAITriage)
m.Post("/explain", web.Bind(api.AIExplainRequest{}), TriggerAIExplain)
m.Post("/fix/{issue}", TriggerAIFix)
}, reqToken())
}, repoAssignment())
// Organization v2 API - org overview, repos, profile, pinned repos
m.Group("/user/orgs", func() {
m.Get("", ListUserOrgsV2)
}, reqToken())
m.Group("/orgs/{org}", func() {
// Public read endpoints
m.Get("/overview", GetOrgOverviewV2)
m.Get("/repos", ListOrgReposV2)
m.Get("/profile/readme", GetOrgProfileReadmeV2)
// Write endpoints require authentication + org ownership (checked in handler)
m.Group("", func() {
m.Patch("", web.Bind(UpdateOrgV2Option{}), UpdateOrgV2)
m.Put("/profile/readme", web.Bind(UpdateOrgProfileReadmeOption{}), UpdateOrgProfileReadmeV2)
m.Post("/pinned", web.Bind(PinOrgRepoV2Option{}), PinOrgRepoV2)
m.Delete("/pinned/{repo}", UnpinOrgRepoV2)
}, reqToken())
})
// AI settings API - org-scoped AI settings
m.Group("/orgs/{org}/ai", func() {
m.Get("/settings", GetOrgAISettingsV2)
m.Put("/settings", web.Bind(api.UpdateOrgAISettingsOption{}), UpdateOrgAISettingsV2)
}, reqToken())
// AI admin API - site admin AI management
m.Group("/admin/ai", func() {
m.Get("/status", GetAIServiceStatus)
m.Get("/operations", ListAllAIOperations)
}, reqToken())
})
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
}
}
}