mirror of
https://github.com/go-gitea/gitea.git
synced 2026-04-05 14:48:07 +00:00
## Overview This PR introduces granular permission controls for Gitea Actions tokens (`GITEA_TOKEN`), aligning Gitea's security model with GitHub Actions standards while maintaining compatibility with Gitea's unique repository unit system. It addresses the need for finer access control by allowing administrators and repository owners to define default token permissions, set maximum permission ceilings, and control cross-repository access within organizations. ## Key Features ### 1. Granular Token Permissions - **Standard Keyword Support**: Implements support for the `permissions:` keyword in workflow and job YAML files (e.g., `contents: read`, `issues: write`). - **Permission Modes**: - **Permissive**: Default write access for most units (backwards compatible). - **Restricted**: Default read-only access for `contents` and `packages`, with no access to other units. - ~~**Custom**: Allows defining specific default levels for each unit type (Code, Issues, PRs, Packages, etc.).~~**EDIT removed UI was confusing** - **Clamping Logic**: Workflow-defined permissions are automatically "clamped" by repository or organization-level maximum settings. Workflows cannot escalate their own permissions beyond these limits. ### 2. Organization & Repository Settings - **Settings UI**: Added new settings pages at both Organization and Repository levels to manage Actions token defaults and maximums. - **Inheritance**: Repositories can be configured to "Follow organization-level configuration," simplifying management across large organizations. - **Cross-Repository Access**: Added a policy to control whether Actions workflows can access other repositories or packages within the same organization. This can be set to "None," "All," or restricted to a "Selected" list of repositories. ### 3. Security Hardening - **Fork Pull Request Protection**: Tokens for workflows triggered by pull requests from forks are strictly enforced as read-only, regardless of repository settings. - ~~**Package Access**: Actions tokens can now only access packages explicitly linked to a repository, with cross-repo access governed by the organization's security policy.~~ **EDIT removed https://github.com/go-gitea/gitea/pull/36173#issuecomment-3873675346** - **Git Hook Integration**: Propagates Actions Task IDs to git hooks to ensure that pushes performed by Actions tokens respect the specific permissions granted at runtime. ### 4. Technical Implementation - **Permission Persistence**: Parsed permissions are calculated at job creation and stored in the `action_run_job` table. This ensures the token's authority is deterministic throughout the job's lifecycle. - **Parsing Priority**: Implemented a priority system in the YAML parser where the broad `contents` scope is applied first, allowing granular scopes like `code` or `releases` to override it for precise control. - **Re-runs**: Permissions are re-evaluated during a job re-run to incorporate any changes made to repository settings in the interim. ### How to Test 1. **Unit Tests**: Run `go test ./services/actions/...` and `go test ./models/repo/...` to verify parsing logic and permission clamping. 2. **Integration Tests**: Comprehensive tests have been added to `tests/integration/actions_job_token_test.go` covering: - Permissive vs. Restricted mode behavior. - YAML `permissions:` keyword evaluation. - Organization cross-repo access policies. - Resource access (Git, API, and Packages) under various permission configs. 3. **Manual Verification**: - Navigate to **Site/Org/Repo Settings -> Actions -> General**. - Change "Default Token Permissions" and verify that newly triggered workflows reflect these changes in their `GITEA_TOKEN` capabilities. - Attempt a cross-repo API call from an Action and verify the Org policy is enforced. ## Documentation Added a PR in gitea's docs for this : https://gitea.com/gitea/docs/pulls/318 ## UI: <img width="1366" height="619" alt="Screenshot 2026-01-24 174112" src="https://github.com/user-attachments/assets/bfa29c9a-4ea5-4346-9410-16d491ef3d44" /> <img width="1360" height="621" alt="Screenshot 2026-01-24 174048" src="https://github.com/user-attachments/assets/d5ec46c8-9a13-4874-a6a4-fb379936cef5" /> /fixes #24635 /claim #24635 --------- Signed-off-by: Excellencedev <ademiluyisuccessandexcellence@gmail.com> Signed-off-by: ChristopherHX <christopher.homberger@web.de> Signed-off-by: silverwind <me@silverwind.io> Signed-off-by: wxiaoguang <wxiaoguang@gmail.com> Co-authored-by: ChristopherHX <christopher.homberger@web.de> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: silverwind <me@silverwind.io> Co-authored-by: Zettat123 <zettat123@gmail.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
1751 lines
75 KiB
Go
1751 lines
75 KiB
Go
// Copyright 2017 The Gitea Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package web
|
|
|
|
import (
|
|
"net/http"
|
|
"strings"
|
|
|
|
auth_model "code.gitea.io/gitea/models/auth"
|
|
"code.gitea.io/gitea/models/perm"
|
|
"code.gitea.io/gitea/models/unit"
|
|
"code.gitea.io/gitea/modules/git"
|
|
"code.gitea.io/gitea/modules/graceful"
|
|
"code.gitea.io/gitea/modules/log"
|
|
"code.gitea.io/gitea/modules/metrics"
|
|
"code.gitea.io/gitea/modules/public"
|
|
"code.gitea.io/gitea/modules/reqctx"
|
|
"code.gitea.io/gitea/modules/setting"
|
|
"code.gitea.io/gitea/modules/storage"
|
|
"code.gitea.io/gitea/modules/structs"
|
|
"code.gitea.io/gitea/modules/validation"
|
|
"code.gitea.io/gitea/modules/web"
|
|
"code.gitea.io/gitea/modules/web/middleware"
|
|
"code.gitea.io/gitea/modules/web/routing"
|
|
"code.gitea.io/gitea/routers/common"
|
|
"code.gitea.io/gitea/routers/web/admin"
|
|
"code.gitea.io/gitea/routers/web/auth"
|
|
"code.gitea.io/gitea/routers/web/devtest"
|
|
"code.gitea.io/gitea/routers/web/events"
|
|
"code.gitea.io/gitea/routers/web/explore"
|
|
"code.gitea.io/gitea/routers/web/feed"
|
|
"code.gitea.io/gitea/routers/web/healthcheck"
|
|
"code.gitea.io/gitea/routers/web/misc"
|
|
"code.gitea.io/gitea/routers/web/org"
|
|
"code.gitea.io/gitea/routers/web/repo"
|
|
"code.gitea.io/gitea/routers/web/repo/actions"
|
|
repo_setting "code.gitea.io/gitea/routers/web/repo/setting"
|
|
shared_actions "code.gitea.io/gitea/routers/web/shared/actions"
|
|
"code.gitea.io/gitea/routers/web/shared/project"
|
|
"code.gitea.io/gitea/routers/web/user"
|
|
user_setting "code.gitea.io/gitea/routers/web/user/setting"
|
|
"code.gitea.io/gitea/routers/web/user/setting/security"
|
|
auth_service "code.gitea.io/gitea/services/auth"
|
|
"code.gitea.io/gitea/services/context"
|
|
"code.gitea.io/gitea/services/forms"
|
|
|
|
_ "code.gitea.io/gitea/modules/session" // to registers all internal adapters
|
|
|
|
"gitea.com/go-chi/captcha"
|
|
chi_middleware "github.com/go-chi/chi/v5/middleware"
|
|
"github.com/go-chi/cors"
|
|
"github.com/klauspost/compress/gzhttp"
|
|
"github.com/prometheus/client_golang/prometheus"
|
|
)
|
|
|
|
var GzipMinSize = 1400 // min size to compress for the body size of response
|
|
|
|
// optionsCorsHandler return a http handler which sets CORS options if enabled by config, it blocks non-CORS OPTIONS requests.
|
|
func optionsCorsHandler() func(next http.Handler) http.Handler {
|
|
var corsHandler func(next http.Handler) http.Handler
|
|
if setting.CORSConfig.Enabled {
|
|
corsHandler = cors.Handler(cors.Options{
|
|
AllowedOrigins: setting.CORSConfig.AllowDomain,
|
|
AllowedMethods: setting.CORSConfig.Methods,
|
|
AllowCredentials: setting.CORSConfig.AllowCredentials,
|
|
AllowedHeaders: setting.CORSConfig.Headers,
|
|
MaxAge: int(setting.CORSConfig.MaxAge.Seconds()),
|
|
})
|
|
}
|
|
|
|
return func(next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method == http.MethodOptions {
|
|
if corsHandler != nil && r.Header.Get("Access-Control-Request-Method") != "" {
|
|
corsHandler(next).ServeHTTP(w, r)
|
|
} else {
|
|
// it should explicitly deny OPTIONS requests if CORS handler is not executed, to avoid the next GET/POST handler being incorrectly called by the OPTIONS request
|
|
w.WriteHeader(http.StatusMethodNotAllowed)
|
|
}
|
|
return
|
|
}
|
|
// for non-OPTIONS requests, call the CORS handler to add some related headers like "Vary"
|
|
if corsHandler != nil {
|
|
corsHandler(next).ServeHTTP(w, r)
|
|
} else {
|
|
next.ServeHTTP(w, r)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
type AuthMiddleware struct {
|
|
AllowOAuth2 web.PreMiddlewareProvider
|
|
AllowBasic web.PreMiddlewareProvider
|
|
MiddlewareHandler func(*context.Context)
|
|
}
|
|
|
|
func newWebAuthMiddleware() *AuthMiddleware {
|
|
type keyAllowOAuth2 struct{}
|
|
type keyAllowBasic struct{}
|
|
webAuth := &AuthMiddleware{}
|
|
|
|
middlewareSetContextValue := func(key, val any) web.PreMiddlewareProvider {
|
|
return func(next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
dataStore := reqctx.GetRequestDataStore(r.Context())
|
|
dataStore.SetContextValue(key, val)
|
|
next.ServeHTTP(w, r)
|
|
})
|
|
}
|
|
}
|
|
|
|
webAuth.AllowBasic = middlewareSetContextValue(keyAllowBasic{}, true)
|
|
webAuth.AllowOAuth2 = middlewareSetContextValue(keyAllowOAuth2{}, true)
|
|
|
|
enableSSPI := setting.IsWindows && auth_model.IsSSPIEnabled(graceful.GetManager().ShutdownContext())
|
|
webAuth.MiddlewareHandler = func(ctx *context.Context) {
|
|
allowBasic := ctx.GetContextValue(keyAllowBasic{}) == true
|
|
allowOAuth2 := ctx.GetContextValue(keyAllowOAuth2{}) == true
|
|
|
|
group := auth_service.NewGroup()
|
|
|
|
// Most auth methods should ignore the user id stored in the session.
|
|
// If the auth succeeds, it must use the user id from the auth method to make sure the new login succeeds.
|
|
if allowOAuth2 {
|
|
group.Add(&auth_service.OAuth2{})
|
|
}
|
|
if allowBasic {
|
|
group.Add(&auth_service.Basic{})
|
|
}
|
|
|
|
// Sessionless means the route's auth can be done without web ui, then it doesn't need to create a session
|
|
// For example: accessing git via http, access rss feeds, downloading attachments, etc
|
|
isSessionless := allowOAuth2 || allowBasic
|
|
|
|
if setting.Service.EnableReverseProxyAuth {
|
|
// reverse-proxy should before Session, otherwise the header will be ignored if user has login
|
|
group.Add(&auth_service.ReverseProxy{CreateSession: !isSessionless})
|
|
}
|
|
|
|
// The Session plugin will skip authentication for users that have already signed in.
|
|
group.Add(&auth_service.Session{})
|
|
|
|
if enableSSPI {
|
|
// it MUST be the last, see the comment of SSPI
|
|
group.Add(&auth_service.SSPI{CreateSession: !isSessionless})
|
|
}
|
|
|
|
ar, err := common.AuthShared(ctx.Base, ctx.Session, group)
|
|
if err != nil {
|
|
log.Error("Failed to verify user: %v", err)
|
|
ctx.HTTPError(http.StatusUnauthorized, "Failed to authenticate user")
|
|
return
|
|
}
|
|
ctx.Doer = ar.Doer
|
|
ctx.IsSigned = ar.Doer != nil
|
|
ctx.IsBasicAuth = ar.IsBasicAuth
|
|
if ctx.Doer == nil {
|
|
// ensure the session uid is deleted
|
|
_ = ctx.Session.Delete("uid")
|
|
}
|
|
}
|
|
return webAuth
|
|
}
|
|
|
|
// verifyAuthWithOptions checks authentication according to options
|
|
func verifyAuthWithOptions(options *common.VerifyOptions) func(ctx *context.Context) {
|
|
crossOriginProtection := http.NewCrossOriginProtection()
|
|
|
|
return func(ctx *context.Context) {
|
|
// Check prohibit login users.
|
|
if ctx.IsSigned {
|
|
if !ctx.Doer.IsActive && setting.Service.RegisterEmailConfirm {
|
|
ctx.Data["Title"] = ctx.Tr("auth.active_your_account")
|
|
ctx.HTML(http.StatusOK, "user/auth/activate")
|
|
return
|
|
}
|
|
if !ctx.Doer.IsActive || ctx.Doer.ProhibitLogin {
|
|
log.Info("Failed authentication attempt for %s from %s", ctx.Doer.Name, ctx.RemoteAddr())
|
|
ctx.Data["Title"] = ctx.Tr("auth.prohibit_login")
|
|
ctx.HTML(http.StatusOK, "user/auth/prohibit_login")
|
|
return
|
|
}
|
|
|
|
if ctx.Doer.MustChangePassword {
|
|
if ctx.Req.URL.Path != "/user/settings/change_password" {
|
|
if strings.HasPrefix(ctx.Req.UserAgent(), "git") {
|
|
ctx.HTTPError(http.StatusUnauthorized, ctx.Locale.TrString("auth.must_change_password"))
|
|
return
|
|
}
|
|
ctx.Data["Title"] = ctx.Tr("auth.must_change_password")
|
|
ctx.Data["ChangePasscodeLink"] = setting.AppSubURL + "/user/change_password"
|
|
middleware.SetRedirectToCookie(ctx.Resp, setting.AppSubURL+ctx.Req.URL.RequestURI())
|
|
ctx.Redirect(setting.AppSubURL + "/user/settings/change_password")
|
|
return
|
|
}
|
|
} else if ctx.Req.URL.Path == "/user/settings/change_password" {
|
|
// make sure that the form cannot be accessed by users who don't need this
|
|
ctx.Redirect(setting.AppSubURL + "/")
|
|
return
|
|
}
|
|
}
|
|
|
|
// When a signed-in user visits a page that requires sign-out (e.g.: "/user/login"), redirect to home (or alternate location)
|
|
if options.SignOutRequired && ctx.IsSigned && ctx.Req.URL.RequestURI() != "/" {
|
|
ctx.RedirectToCurrentSite(ctx.FormString("redirect_to"))
|
|
return
|
|
}
|
|
|
|
if !options.SignOutRequired && !options.DisableCrossOriginProtection {
|
|
if err := crossOriginProtection.Check(ctx.Req); err != nil {
|
|
http.Error(ctx.Resp, err.Error(), http.StatusForbidden)
|
|
return
|
|
}
|
|
}
|
|
|
|
if options.SignInRequired {
|
|
if !ctx.IsSigned {
|
|
ctx.Redirect(middleware.RedirectLinkUserLogin(ctx.Req))
|
|
return
|
|
} else if !ctx.Doer.IsActive && setting.Service.RegisterEmailConfirm {
|
|
ctx.Data["Title"] = ctx.Tr("auth.active_your_account")
|
|
ctx.HTML(http.StatusOK, "user/auth/activate")
|
|
return
|
|
}
|
|
}
|
|
|
|
// Redirect to log in page if auto-signin info is provided and has not signed in.
|
|
if !options.SignOutRequired && !ctx.IsSigned && ctx.GetSiteCookie(setting.CookieRememberName) != "" {
|
|
ctx.Redirect(middleware.RedirectLinkUserLogin(ctx.Req))
|
|
return
|
|
}
|
|
|
|
if options.AdminRequired {
|
|
if !ctx.Doer.IsAdmin {
|
|
ctx.HTTPError(http.StatusForbidden)
|
|
return
|
|
}
|
|
ctx.Data["PageIsAdmin"] = true
|
|
}
|
|
}
|
|
}
|
|
|
|
func ctxDataSet(args ...any) func(ctx *context.Context) {
|
|
return func(ctx *context.Context) {
|
|
for i := 0; i < len(args); i += 2 {
|
|
ctx.Data[args[i].(string)] = args[i+1]
|
|
}
|
|
}
|
|
}
|
|
|
|
const RouterMockPointBeforeWebRoutes = "before-web-routes"
|
|
|
|
// Routes returns all web routes
|
|
func Routes() *web.Router {
|
|
routes := web.NewRouter()
|
|
|
|
// GetHead allows a HEAD request redirect to GET if HEAD method is not defined for that route
|
|
routes.BeforeRouting(chi_middleware.GetHead)
|
|
|
|
routes.Head("/", misc.DummyOK) // for health check - doesn't need to be passed through gzip handler
|
|
routes.Methods("GET, HEAD, OPTIONS", "/assets/*", optionsCorsHandler(), public.FileHandlerFunc())
|
|
routes.Methods("GET, HEAD", "/avatars/*", avatarStorageHandler(setting.Avatar.Storage, "avatars", storage.Avatars))
|
|
routes.Methods("GET, HEAD", "/repo-avatars/*", avatarStorageHandler(setting.RepoAvatar.Storage, "repo-avatars", storage.RepoAvatars))
|
|
routes.Methods("GET, HEAD", "/apple-touch-icon.png", misc.StaticRedirect("/assets/img/apple-touch-icon.png"))
|
|
routes.Methods("GET, HEAD", "/apple-touch-icon-precomposed.png", misc.StaticRedirect("/assets/img/apple-touch-icon.png"))
|
|
routes.Methods("GET, HEAD", "/favicon.ico", misc.StaticRedirect("/assets/img/favicon.png"))
|
|
|
|
var mid []any
|
|
|
|
if setting.EnableGzip {
|
|
// random jitter is recommended by: https://pkg.go.dev/github.com/klauspost/compress/gzhttp#readme-breach-mitigation
|
|
// compression level 6 is the gzip default and a good general tradeoff between speed, CPU usage, and compression
|
|
wrapper, err := gzhttp.NewWrapper(gzhttp.RandomJitter(32, 0, false), gzhttp.MinSize(GzipMinSize), gzhttp.CompressionLevel(6))
|
|
if err != nil {
|
|
log.Fatal("gzhttp.NewWrapper failed: %v", err)
|
|
}
|
|
mid = append(mid, wrapper)
|
|
}
|
|
|
|
if setting.Service.EnableCaptcha {
|
|
// The captcha http.Handler should only fire on /captcha/* so we can just mount this on that url
|
|
routes.Methods("GET,HEAD", "/captcha/*", append(mid, captcha.Captchaer(context.GetImageCaptcha()))...)
|
|
}
|
|
|
|
if setting.Metrics.Enabled {
|
|
prometheus.MustRegister(metrics.NewCollector())
|
|
routes.Get("/metrics", append(mid, Metrics)...)
|
|
}
|
|
|
|
routes.Methods("GET,HEAD", "/robots.txt", append(mid, misc.RobotsTxt)...)
|
|
routes.Get("/ssh_info", misc.SSHInfo)
|
|
routes.Get("/api/healthz", healthcheck.Check)
|
|
|
|
mid = append(mid, common.MustInitSessioner(), context.Contexter())
|
|
|
|
// Get user from session if logged in.
|
|
webAuth := newWebAuthMiddleware()
|
|
mid = append(mid, webAuth.MiddlewareHandler)
|
|
|
|
if setting.API.EnableSwagger {
|
|
// Note: The route is here but no in API routes because it renders a web page
|
|
routes.Get("/api/swagger", append(mid, misc.Swagger)...) // Render V1 by default
|
|
}
|
|
|
|
mid = append(mid, goGet)
|
|
mid = append(mid, common.PageGlobalData)
|
|
mid = append(mid, common.BlockExpensive(), common.QoS(), web.RouterMockPoint(RouterMockPointBeforeWebRoutes))
|
|
|
|
webRoutes := web.NewRouter()
|
|
webRoutes.AfterRouting(mid...)
|
|
registerWebRoutes(webRoutes, webAuth)
|
|
|
|
routes.Mount("", webRoutes)
|
|
return routes
|
|
}
|
|
|
|
// optSignInFromAnyOrigin means that the user can (optionally) be signed in from any origin (no cross-origin protection)
|
|
// - With CORS middleware: CORS middleware does the preflight request handling, the requests has Sec-Fetch-Site header.
|
|
// The CORS mechanism already protects cross-origin requests, and the CrossOriginProtection has no "allowed origin" list, so disable CrossOriginProtection.
|
|
// - For non-browser client requests: git clone via http, no Sec-Fetch-Site header.
|
|
// Such requests are not cross-origin requests, so disable CrossOriginProtection.
|
|
var optSignInFromAnyOrigin = verifyAuthWithOptions(&common.VerifyOptions{DisableCrossOriginProtection: true})
|
|
|
|
// registerWebRoutes register routes
|
|
func registerWebRoutes(m *web.Router, webAuth *AuthMiddleware) {
|
|
// required to be signed in or signed out
|
|
reqSignIn := verifyAuthWithOptions(&common.VerifyOptions{SignInRequired: true})
|
|
reqSignOut := verifyAuthWithOptions(&common.VerifyOptions{SignOutRequired: true})
|
|
// optional sign in (if signed in, use the user as doer, if not, no doer)
|
|
optSignIn := verifyAuthWithOptions(&common.VerifyOptions{SignInRequired: setting.Service.RequireSignInViewStrict})
|
|
optExploreSignIn := verifyAuthWithOptions(&common.VerifyOptions{SignInRequired: setting.Service.RequireSignInViewStrict || setting.Service.Explore.RequireSigninView})
|
|
|
|
validation.AddBindingRules()
|
|
|
|
openIDSignInEnabled := func(ctx *context.Context) {
|
|
if !setting.Service.EnableOpenIDSignIn {
|
|
ctx.HTTPError(http.StatusForbidden)
|
|
return
|
|
}
|
|
}
|
|
|
|
openIDSignUpEnabled := func(ctx *context.Context) {
|
|
if !setting.Service.EnableOpenIDSignUp {
|
|
ctx.HTTPError(http.StatusForbidden)
|
|
return
|
|
}
|
|
}
|
|
|
|
oauth2Enabled := func(ctx *context.Context) {
|
|
if !setting.OAuth2.Enabled {
|
|
ctx.HTTPError(http.StatusForbidden)
|
|
return
|
|
}
|
|
}
|
|
|
|
reqMilestonesDashboardPageEnabled := func(ctx *context.Context) {
|
|
if !setting.Service.ShowMilestonesDashboardPage {
|
|
ctx.HTTPError(http.StatusForbidden)
|
|
return
|
|
}
|
|
}
|
|
|
|
// webhooksEnabled requires webhooks to be enabled by admin.
|
|
webhooksEnabled := func(ctx *context.Context) {
|
|
if setting.DisableWebhooks {
|
|
ctx.HTTPError(http.StatusForbidden)
|
|
return
|
|
}
|
|
}
|
|
|
|
starsEnabled := func(ctx *context.Context) {
|
|
if setting.Repository.DisableStars {
|
|
ctx.HTTPError(http.StatusForbidden)
|
|
return
|
|
}
|
|
}
|
|
|
|
lfsServerEnabled := func(ctx *context.Context) {
|
|
if !setting.LFS.StartServer {
|
|
ctx.HTTPError(http.StatusNotFound)
|
|
return
|
|
}
|
|
}
|
|
|
|
federationEnabled := func(ctx *context.Context) {
|
|
if !setting.Federation.Enabled {
|
|
ctx.HTTPError(http.StatusNotFound)
|
|
return
|
|
}
|
|
}
|
|
|
|
dlSourceEnabled := func(ctx *context.Context) {
|
|
if setting.Repository.DisableDownloadSourceArchives {
|
|
ctx.HTTPError(http.StatusNotFound)
|
|
return
|
|
}
|
|
}
|
|
|
|
sitemapEnabled := func(ctx *context.Context) {
|
|
if !setting.Other.EnableSitemap {
|
|
ctx.HTTPError(http.StatusNotFound)
|
|
return
|
|
}
|
|
}
|
|
|
|
packagesEnabled := func(ctx *context.Context) {
|
|
if !setting.Packages.Enabled {
|
|
ctx.HTTPError(http.StatusForbidden)
|
|
return
|
|
}
|
|
}
|
|
|
|
feedEnabled := func(ctx *context.Context) {
|
|
if !setting.Other.EnableFeed {
|
|
ctx.HTTPError(http.StatusNotFound)
|
|
return
|
|
}
|
|
}
|
|
|
|
reqUnitAccess := func(unitType unit.Type, accessMode perm.AccessMode, ignoreGlobal bool) func(ctx *context.Context) {
|
|
return func(ctx *context.Context) {
|
|
// only check global disabled units when ignoreGlobal is false
|
|
if !ignoreGlobal && unitType.UnitGlobalDisabled() {
|
|
ctx.NotFound(nil)
|
|
return
|
|
}
|
|
|
|
if ctx.ContextUser == nil {
|
|
ctx.NotFound(nil)
|
|
return
|
|
}
|
|
|
|
if ctx.ContextUser.IsOrganization() {
|
|
if ctx.Org.Organization.UnitPermission(ctx, ctx.Doer, unitType) < accessMode {
|
|
ctx.NotFound(nil)
|
|
return
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
addWebhookAddRoutes := func() {
|
|
m.Get("/{type}/new", repo_setting.WebhooksNew)
|
|
m.Post("/gitea/new", web.Bind(forms.NewWebhookForm{}), repo_setting.GiteaHooksNewPost)
|
|
m.Post("/gogs/new", web.Bind(forms.NewGogshookForm{}), repo_setting.GogsHooksNewPost)
|
|
m.Post("/slack/new", web.Bind(forms.NewSlackHookForm{}), repo_setting.SlackHooksNewPost)
|
|
m.Post("/discord/new", web.Bind(forms.NewDiscordHookForm{}), repo_setting.DiscordHooksNewPost)
|
|
m.Post("/dingtalk/new", web.Bind(forms.NewDingtalkHookForm{}), repo_setting.DingtalkHooksNewPost)
|
|
m.Post("/telegram/new", web.Bind(forms.NewTelegramHookForm{}), repo_setting.TelegramHooksNewPost)
|
|
m.Post("/matrix/new", web.Bind(forms.NewMatrixHookForm{}), repo_setting.MatrixHooksNewPost)
|
|
m.Post("/msteams/new", web.Bind(forms.NewMSTeamsHookForm{}), repo_setting.MSTeamsHooksNewPost)
|
|
m.Post("/feishu/new", web.Bind(forms.NewFeishuHookForm{}), repo_setting.FeishuHooksNewPost)
|
|
m.Post("/wechatwork/new", web.Bind(forms.NewWechatWorkHookForm{}), repo_setting.WechatworkHooksNewPost)
|
|
m.Post("/packagist/new", web.Bind(forms.NewPackagistHookForm{}), repo_setting.PackagistHooksNewPost)
|
|
}
|
|
|
|
addWebhookEditRoutes := func() {
|
|
m.Post("/gitea/{id}", web.Bind(forms.NewWebhookForm{}), repo_setting.GiteaHooksEditPost)
|
|
m.Post("/gogs/{id}", web.Bind(forms.NewGogshookForm{}), repo_setting.GogsHooksEditPost)
|
|
m.Post("/slack/{id}", web.Bind(forms.NewSlackHookForm{}), repo_setting.SlackHooksEditPost)
|
|
m.Post("/discord/{id}", web.Bind(forms.NewDiscordHookForm{}), repo_setting.DiscordHooksEditPost)
|
|
m.Post("/dingtalk/{id}", web.Bind(forms.NewDingtalkHookForm{}), repo_setting.DingtalkHooksEditPost)
|
|
m.Post("/telegram/{id}", web.Bind(forms.NewTelegramHookForm{}), repo_setting.TelegramHooksEditPost)
|
|
m.Post("/matrix/{id}", web.Bind(forms.NewMatrixHookForm{}), repo_setting.MatrixHooksEditPost)
|
|
m.Post("/msteams/{id}", web.Bind(forms.NewMSTeamsHookForm{}), repo_setting.MSTeamsHooksEditPost)
|
|
m.Post("/feishu/{id}", web.Bind(forms.NewFeishuHookForm{}), repo_setting.FeishuHooksEditPost)
|
|
m.Post("/wechatwork/{id}", web.Bind(forms.NewWechatWorkHookForm{}), repo_setting.WechatworkHooksEditPost)
|
|
m.Post("/packagist/{id}", web.Bind(forms.NewPackagistHookForm{}), repo_setting.PackagistHooksEditPost)
|
|
}
|
|
|
|
addSettingsVariablesRoutes := func() {
|
|
m.Group("/variables", func() {
|
|
m.Get("", shared_actions.Variables)
|
|
m.Post("/new", web.Bind(forms.EditVariableForm{}), shared_actions.VariableCreate)
|
|
m.Post("/{variable_id}/edit", web.Bind(forms.EditVariableForm{}), shared_actions.VariableUpdate)
|
|
m.Post("/{variable_id}/delete", shared_actions.VariableDelete)
|
|
})
|
|
}
|
|
|
|
addSettingsSecretsRoutes := func() {
|
|
m.Group("/secrets", func() {
|
|
m.Get("", repo_setting.Secrets)
|
|
m.Post("", web.Bind(forms.AddSecretForm{}), repo_setting.SecretsPost)
|
|
m.Post("/delete", repo_setting.SecretsDelete)
|
|
})
|
|
}
|
|
|
|
addSettingsRunnersRoutes := func() {
|
|
m.Group("/runners", func() {
|
|
m.Get("", shared_actions.Runners)
|
|
m.Combo("/{runnerid}").Get(shared_actions.RunnersEdit).
|
|
Post(web.Bind(forms.EditRunnerForm{}), shared_actions.RunnersEditPost)
|
|
m.Post("/{runnerid}/update-runner", shared_actions.RunnerUpdatePost)
|
|
m.Post("/{runnerid}/delete", shared_actions.RunnerDeletePost)
|
|
m.Post("/reset_registration_token", shared_actions.ResetRunnerRegistrationToken)
|
|
})
|
|
}
|
|
|
|
// FIXME: not all routes need go through same middleware.
|
|
// Especially some AJAX requests, we can reduce middleware number to improve performance.
|
|
|
|
m.Get("/", Home)
|
|
m.Get("/sitemap.xml", sitemapEnabled, optExploreSignIn, HomeSitemap)
|
|
m.Group("/.well-known", func() {
|
|
m.Get("/openid-configuration", auth.OIDCWellKnown)
|
|
m.Group("", func() {
|
|
m.Get("/nodeinfo", NodeInfoLinks)
|
|
m.Get("/webfinger", WebfingerQuery)
|
|
}, federationEnabled)
|
|
m.Get("/change-password", func(ctx *context.Context) {
|
|
ctx.Redirect(setting.AppSubURL + "/user/settings/account")
|
|
})
|
|
m.Get("/passkey-endpoints", passkeyEndpoints)
|
|
m.Methods("GET, HEAD", "/*", public.FileHandlerFunc())
|
|
}, optionsCorsHandler())
|
|
|
|
m.Post("/-/markup", reqSignIn, web.Bind(structs.MarkupOption{}), misc.Markup)
|
|
m.Post("/-/web-banner/dismiss", misc.WebBannerDismiss)
|
|
m.Get("/-/web-theme/list", misc.WebThemeList)
|
|
m.Post("/-/web-theme/apply", optSignIn, misc.WebThemeApply)
|
|
|
|
m.Group("/explore", func() {
|
|
m.Get("", func(ctx *context.Context) {
|
|
ctx.Redirect(setting.AppSubURL + "/explore/repos")
|
|
})
|
|
m.Get("/repos", explore.Repos)
|
|
m.Get("/repos/sitemap-{idx}.xml", sitemapEnabled, explore.Repos)
|
|
m.Get("/users", explore.Users)
|
|
m.Get("/users/sitemap-{idx}.xml", sitemapEnabled, explore.Users)
|
|
m.Get("/organizations", explore.Organizations)
|
|
m.Get("/code", func(ctx *context.Context) {
|
|
if unit.TypeCode.UnitGlobalDisabled() {
|
|
ctx.NotFound(nil)
|
|
return
|
|
}
|
|
}, explore.Code)
|
|
m.Get("/topics/search", explore.TopicSearch)
|
|
}, optExploreSignIn)
|
|
|
|
m.Group("/issues", func() {
|
|
m.Get("", user.Issues)
|
|
m.Get("/search", repo.SearchIssues)
|
|
}, reqSignIn)
|
|
|
|
m.Get("/pulls", reqSignIn, user.Pulls)
|
|
m.Get("/milestones", reqSignIn, reqMilestonesDashboardPageEnabled, user.Milestones)
|
|
|
|
// ***** START: User *****
|
|
// "user/login" doesn't need signOut, then logged-in users can still access this route for redirection purposes by "/user/login?redirec_to=..."
|
|
m.Get("/user/login", auth.SignIn)
|
|
m.Group("/user", func() {
|
|
m.Post("/login", web.Bind(forms.SignInForm{}), auth.SignInPost)
|
|
m.Group("", func() {
|
|
m.Combo("/login/openid").
|
|
Get(auth.SignInOpenID).
|
|
Post(web.Bind(forms.SignInOpenIDForm{}), auth.SignInOpenIDPost)
|
|
}, openIDSignInEnabled)
|
|
m.Group("/openid", func() {
|
|
m.Combo("/connect").
|
|
Get(auth.ConnectOpenID).
|
|
Post(web.Bind(forms.ConnectOpenIDForm{}), auth.ConnectOpenIDPost)
|
|
m.Group("/register", func() {
|
|
m.Combo("").
|
|
Get(auth.RegisterOpenID, openIDSignUpEnabled).
|
|
Post(web.Bind(forms.SignUpOpenIDForm{}), auth.RegisterOpenIDPost)
|
|
}, openIDSignUpEnabled)
|
|
}, openIDSignInEnabled)
|
|
m.Get("/sign_up", auth.SignUp)
|
|
m.Post("/sign_up", web.Bind(forms.RegisterForm{}), auth.SignUpPost)
|
|
m.Get("/link_account", auth.LinkAccount)
|
|
m.Post("/link_account_signin", web.Bind(forms.SignInForm{}), auth.LinkAccountPostSignIn)
|
|
m.Post("/link_account_signup", web.Bind(forms.RegisterForm{}), auth.LinkAccountPostRegister)
|
|
m.Group("/two_factor", func() {
|
|
m.Get("", auth.TwoFactor)
|
|
m.Post("", web.Bind(forms.TwoFactorAuthForm{}), auth.TwoFactorPost)
|
|
m.Get("/scratch", auth.TwoFactorScratch)
|
|
m.Post("/scratch", web.Bind(forms.TwoFactorScratchAuthForm{}), auth.TwoFactorScratchPost)
|
|
})
|
|
m.Group("/webauthn", func() {
|
|
m.Get("", auth.WebAuthn)
|
|
m.Get("/passkey/assertion", auth.WebAuthnPasskeyAssertion)
|
|
m.Post("/passkey/login", auth.WebAuthnPasskeyLogin)
|
|
m.Get("/assertion", auth.WebAuthnLoginAssertion)
|
|
m.Post("/assertion", auth.WebAuthnLoginAssertionPost)
|
|
})
|
|
}, reqSignOut)
|
|
|
|
m.Any("/user/events", routing.MarkLongPolling, events.Events)
|
|
|
|
m.Group("/login/oauth", func() {
|
|
m.Group("", func() {
|
|
m.Get("/authorize", web.Bind(forms.AuthorizationForm{}), auth.AuthorizeOAuth)
|
|
m.Post("/grant", web.Bind(forms.GrantApplicationForm{}), auth.GrantApplicationOAuth)
|
|
// TODO manage redirection
|
|
m.Post("/authorize", web.Bind(forms.AuthorizationForm{}), auth.AuthorizeOAuth)
|
|
}, reqSignIn)
|
|
|
|
m.Group("", func() {
|
|
m.Methods("GET, POST, OPTIONS", "/userinfo", auth.InfoOAuth)
|
|
m.Methods("POST, OPTIONS", "/access_token", web.Bind(forms.AccessTokenForm{}), auth.AccessTokenOAuth)
|
|
m.Methods("GET, OPTIONS", "/keys", auth.OIDCKeys)
|
|
m.Methods("POST, OPTIONS", "/introspect", web.Bind(forms.IntrospectTokenForm{}), auth.IntrospectOAuth)
|
|
}, optionsCorsHandler(), webAuth.AllowOAuth2, optSignInFromAnyOrigin)
|
|
}, oauth2Enabled)
|
|
|
|
m.Group("/user/settings", func() {
|
|
m.Get("", user_setting.Profile)
|
|
m.Post("", web.Bind(forms.UpdateProfileForm{}), user_setting.ProfilePost)
|
|
m.Post("/update_preferences", user_setting.UpdatePreferences)
|
|
m.Get("/change_password", auth.MustChangePassword)
|
|
m.Post("/change_password", web.Bind(forms.MustChangePasswordForm{}), auth.MustChangePasswordPost)
|
|
m.Post("/avatar", web.Bind(forms.AvatarForm{}), user_setting.AvatarPost)
|
|
m.Post("/avatar/delete", user_setting.DeleteAvatar)
|
|
m.Group("/account", func() {
|
|
m.Combo("").Get(user_setting.Account).Post(web.Bind(forms.ChangePasswordForm{}), user_setting.AccountPost)
|
|
m.Post("/email", web.Bind(forms.AddEmailForm{}), user_setting.EmailPost)
|
|
m.Post("/email/delete", user_setting.DeleteEmail)
|
|
m.Post("/delete", user_setting.DeleteAccount)
|
|
})
|
|
m.Group("/appearance", func() {
|
|
m.Get("", user_setting.Appearance)
|
|
m.Post("/language", web.Bind(forms.UpdateLanguageForm{}), user_setting.UpdateUserLang)
|
|
m.Post("/hidden_comments", user_setting.UpdateUserHiddenComments)
|
|
m.Post("/theme", web.Bind(forms.UpdateThemeForm{}), user_setting.UpdateUIThemePost)
|
|
})
|
|
m.Group("/notifications", func() {
|
|
m.Get("", user_setting.Notifications)
|
|
m.Post("/email", user_setting.NotificationsEmailPost)
|
|
m.Post("/actions", user_setting.NotificationsActionsEmailPost)
|
|
})
|
|
m.Group("/security", func() {
|
|
m.Get("", security.Security)
|
|
m.Group("/two_factor", func() {
|
|
m.Post("/regenerate_scratch", security.RegenerateScratchTwoFactor)
|
|
m.Post("/disable", security.DisableTwoFactor)
|
|
m.Get("/enroll", security.EnrollTwoFactor)
|
|
m.Post("/enroll", web.Bind(forms.TwoFactorAuthForm{}), security.EnrollTwoFactorPost)
|
|
})
|
|
m.Group("/webauthn", func() {
|
|
m.Post("/request_register", web.Bind(forms.WebauthnRegistrationForm{}), security.WebAuthnRegister)
|
|
m.Post("/register", security.WebauthnRegisterPost)
|
|
m.Post("/delete", web.Bind(forms.WebauthnDeleteForm{}), security.WebauthnDelete)
|
|
})
|
|
m.Group("/openid", func() {
|
|
m.Post("", web.Bind(forms.AddOpenIDForm{}), security.OpenIDPost)
|
|
m.Post("/delete", security.DeleteOpenID)
|
|
m.Post("/toggle_visibility", security.ToggleOpenIDVisibility)
|
|
}, openIDSignInEnabled)
|
|
m.Post("/account_link", security.DeleteAccountLink)
|
|
})
|
|
|
|
m.Group("/applications", func() {
|
|
// oauth2 applications
|
|
m.Group("/oauth2", func() {
|
|
m.Get("/{id}", user_setting.OAuth2ApplicationShow)
|
|
m.Post("/{id}", web.Bind(forms.EditOAuth2ApplicationForm{}), user_setting.OAuthApplicationsEdit)
|
|
m.Post("/{id}/regenerate_secret", user_setting.OAuthApplicationsRegenerateSecret)
|
|
m.Post("", web.Bind(forms.EditOAuth2ApplicationForm{}), user_setting.OAuthApplicationsPost)
|
|
m.Post("/{id}/delete", user_setting.DeleteOAuth2Application)
|
|
m.Post("/{id}/revoke/{grantId}", user_setting.RevokeOAuth2Grant)
|
|
}, oauth2Enabled)
|
|
|
|
// access token applications
|
|
m.Combo("").Get(user_setting.Applications).
|
|
Post(web.Bind(forms.NewAccessTokenForm{}), user_setting.ApplicationsPost)
|
|
m.Post("/delete", user_setting.DeleteApplication)
|
|
})
|
|
|
|
m.Combo("/keys").Get(user_setting.Keys).
|
|
Post(web.Bind(forms.AddKeyForm{}), user_setting.KeysPost)
|
|
m.Post("/keys/delete", user_setting.DeleteKey)
|
|
m.Group("/packages", func() {
|
|
m.Get("", user_setting.Packages)
|
|
m.Group("/rules", func() {
|
|
m.Group("/add", func() {
|
|
m.Get("", user_setting.PackagesRuleAdd)
|
|
m.Post("", web.Bind(forms.PackageCleanupRuleForm{}), user_setting.PackagesRuleAddPost)
|
|
})
|
|
m.Group("/{id}", func() {
|
|
m.Get("", user_setting.PackagesRuleEdit)
|
|
m.Post("", web.Bind(forms.PackageCleanupRuleForm{}), user_setting.PackagesRuleEditPost)
|
|
m.Get("/preview", user_setting.PackagesRulePreview)
|
|
})
|
|
})
|
|
m.Group("/cargo", func() {
|
|
m.Post("/initialize", user_setting.InitializeCargoIndex)
|
|
m.Post("/rebuild", user_setting.RebuildCargoIndex)
|
|
})
|
|
m.Post("/chef/regenerate_keypair", user_setting.RegenerateChefKeyPair)
|
|
}, packagesEnabled)
|
|
|
|
m.Group("/actions", func() {
|
|
m.Get("", misc.LocationRedirect("./actions/general"))
|
|
m.Group("/general", func() {
|
|
m.Get("", shared_actions.GeneralSettings)
|
|
m.Post("", shared_actions.UpdateGeneralSettings)
|
|
})
|
|
addSettingsRunnersRoutes()
|
|
addSettingsSecretsRoutes()
|
|
addSettingsVariablesRoutes()
|
|
}, actions.MustEnableActions)
|
|
|
|
m.Get("/organization", user_setting.Organization)
|
|
m.Get("/repos", user_setting.Repos)
|
|
m.Post("/repos/unadopted", user_setting.AdoptOrDeleteRepository)
|
|
|
|
m.Group("/hooks", func() {
|
|
m.Get("", user_setting.Webhooks)
|
|
m.Post("/delete", user_setting.DeleteWebhook)
|
|
addWebhookAddRoutes()
|
|
m.Group("/{id}", func() {
|
|
m.Get("", repo_setting.WebHooksEdit)
|
|
m.Post("/replay/{uuid}", repo_setting.ReplayWebhook)
|
|
})
|
|
addWebhookEditRoutes()
|
|
}, webhooksEnabled)
|
|
|
|
m.Group("/blocked_users", func() {
|
|
m.Get("", user_setting.BlockedUsers)
|
|
m.Post("", web.Bind(forms.BlockUserForm{}), user_setting.BlockedUsersPost)
|
|
})
|
|
}, reqSignIn, ctxDataSet("PageIsUserSettings", true, "EnablePackages", setting.Packages.Enabled, "EnableNotifyMail", setting.Service.EnableNotifyMail))
|
|
|
|
m.Group("/user", func() {
|
|
m.Get("/activate", auth.Activate)
|
|
m.Post("/activate", auth.ActivatePost)
|
|
m.Any("/activate_email", auth.ActivateEmail)
|
|
m.Get("/avatar/{username}/{size}", user.AvatarByUsernameSize)
|
|
m.Get("/recover_account", auth.ResetPasswd)
|
|
m.Post("/recover_account", auth.ResetPasswdPost)
|
|
m.Get("/forgot_password", auth.ForgotPasswd)
|
|
m.Post("/forgot_password", auth.ForgotPasswdPost)
|
|
m.Get("/logout", auth.SignOut)
|
|
m.Get("/stopwatches", reqSignIn, user.GetStopwatches)
|
|
m.Get("/search_candidates", optExploreSignIn, user.SearchCandidates)
|
|
m.Group("/oauth2", func() {
|
|
m.Get("/{provider}", auth.SignInOAuth)
|
|
m.Get("/{provider}/callback", auth.SignInOAuthCallback)
|
|
})
|
|
})
|
|
// ***** END: User *****
|
|
|
|
m.Get("/avatar/{hash}", user.AvatarByEmailHash)
|
|
|
|
adminReq := verifyAuthWithOptions(&common.VerifyOptions{SignInRequired: true, AdminRequired: true})
|
|
|
|
// ***** START: Admin *****
|
|
m.Group("/-/admin", func() {
|
|
m.Get("", admin.Dashboard)
|
|
m.Get("/system_status", admin.SystemStatus)
|
|
m.Post("", web.Bind(forms.AdminDashboardForm{}), admin.DashboardPost)
|
|
|
|
m.Get("/self_check", admin.SelfCheck)
|
|
m.Post("/self_check", admin.SelfCheckPost)
|
|
|
|
m.Group("/config", func() {
|
|
m.Get("", admin.Config)
|
|
m.Post("", admin.ChangeConfig)
|
|
m.Post("/test_mail", admin.SendTestMail)
|
|
m.Post("/test_cache", admin.TestCache)
|
|
m.Get("/settings", admin.ConfigSettings)
|
|
})
|
|
|
|
m.Group("/monitor", func() {
|
|
m.Get("/stats", admin.MonitorStats)
|
|
m.Get("/cron", admin.CronTasks)
|
|
m.Get("/perftrace", admin.PerfTrace)
|
|
m.Get("/stacktrace", admin.Stacktrace)
|
|
m.Post("/stacktrace/cancel/{pid}", admin.StacktraceCancel)
|
|
m.Get("/queue", admin.Queues)
|
|
m.Group("/queue/{qid}", func() {
|
|
m.Get("", admin.QueueManage)
|
|
m.Post("/set", admin.QueueSet)
|
|
m.Post("/remove-all-items", admin.QueueRemoveAllItems)
|
|
})
|
|
m.Get("/diagnosis", admin.MonitorDiagnosis)
|
|
})
|
|
|
|
m.Group("/users", func() {
|
|
m.Get("", admin.Users)
|
|
m.Combo("/new").Get(admin.NewUser).Post(web.Bind(forms.AdminCreateUserForm{}), admin.NewUserPost)
|
|
m.Get("/{userid}", admin.ViewUser)
|
|
m.Combo("/{userid}/edit").Get(admin.EditUser).Post(web.Bind(forms.AdminEditUserForm{}), admin.EditUserPost)
|
|
m.Post("/{userid}/delete", admin.DeleteUser)
|
|
m.Post("/{userid}/avatar", web.Bind(forms.AvatarForm{}), admin.AvatarPost)
|
|
m.Post("/{userid}/avatar/delete", admin.DeleteAvatar)
|
|
})
|
|
|
|
m.Group("/emails", func() {
|
|
m.Get("", admin.Emails)
|
|
m.Post("/activate", admin.ActivateEmail)
|
|
m.Post("/delete", admin.DeleteEmail)
|
|
})
|
|
|
|
m.Group("/orgs", func() {
|
|
m.Get("", admin.Organizations)
|
|
})
|
|
|
|
m.Group("/repos", func() {
|
|
m.Get("", admin.Repos)
|
|
m.Combo("/unadopted").Get(admin.UnadoptedRepos).Post(admin.AdoptOrDeleteRepository)
|
|
m.Post("/delete", admin.DeleteRepo)
|
|
})
|
|
|
|
m.Group("/packages", func() {
|
|
m.Get("", admin.Packages)
|
|
m.Post("/delete", admin.DeletePackageVersion)
|
|
m.Post("/cleanup", admin.CleanupExpiredData)
|
|
}, packagesEnabled)
|
|
|
|
m.Group("/hooks", func() {
|
|
m.Get("", admin.DefaultOrSystemWebhooks)
|
|
m.Post("/delete", admin.DeleteDefaultOrSystemWebhook)
|
|
m.Group("/{id}", func() {
|
|
m.Get("", repo_setting.WebHooksEdit)
|
|
m.Post("/replay/{uuid}", repo_setting.ReplayWebhook)
|
|
})
|
|
addWebhookEditRoutes()
|
|
}, webhooksEnabled)
|
|
|
|
m.Group("/{configType:default-hooks|system-hooks}", func() {
|
|
addWebhookAddRoutes()
|
|
})
|
|
|
|
m.Group("/auths", func() {
|
|
m.Get("", admin.Authentications)
|
|
m.Combo("/new").Get(admin.NewAuthSource).Post(web.Bind(forms.AuthenticationForm{}), admin.NewAuthSourcePost)
|
|
m.Combo("/{authid}").Get(admin.EditAuthSource).
|
|
Post(web.Bind(forms.AuthenticationForm{}), admin.EditAuthSourcePost)
|
|
m.Post("/{authid}/delete", admin.DeleteAuthSource)
|
|
})
|
|
|
|
m.Group("/notices", func() {
|
|
m.Get("", admin.Notices)
|
|
m.Post("/delete", admin.DeleteNotices)
|
|
m.Post("/empty", admin.EmptyNotices)
|
|
})
|
|
|
|
m.Group("/applications", func() {
|
|
m.Get("", admin.Applications)
|
|
m.Post("/oauth2", web.Bind(forms.EditOAuth2ApplicationForm{}), admin.ApplicationsPost)
|
|
m.Group("/oauth2/{id}", func() {
|
|
m.Combo("").Get(admin.EditApplication).Post(web.Bind(forms.EditOAuth2ApplicationForm{}), admin.EditApplicationPost)
|
|
m.Post("/regenerate_secret", admin.ApplicationsRegenerateSecret)
|
|
m.Post("/delete", admin.DeleteApplication)
|
|
})
|
|
}, oauth2Enabled)
|
|
|
|
m.Group("/actions", func() {
|
|
m.Get("", misc.LocationRedirect("./actions/runners"))
|
|
addSettingsRunnersRoutes()
|
|
addSettingsVariablesRoutes()
|
|
})
|
|
}, adminReq, ctxDataSet("EnableOAuth2", setting.OAuth2.Enabled, "EnablePackages", setting.Packages.Enabled))
|
|
// ***** END: Admin *****
|
|
|
|
m.Group("", func() {
|
|
// it handles "username.rss" in the handler, so allow basic auth as other rss/atom routes
|
|
m.Get("/{username}", webAuth.AllowBasic, user.UsernameSubRoute)
|
|
m.Methods("GET, OPTIONS", "/attachments/{uuid}", optionsCorsHandler(), webAuth.AllowBasic, webAuth.AllowOAuth2, repo.GetAttachment)
|
|
}, optSignIn)
|
|
|
|
m.Post("/{username}", reqSignIn, context.UserAssignmentWeb(), user.ActionUserFollow)
|
|
|
|
reqRepoAdmin := context.RequireRepoAdmin()
|
|
reqRepoCodeWriter := context.RequireUnitWriter(unit.TypeCode)
|
|
reqRepoReleaseWriter := context.RequireUnitWriter(unit.TypeReleases)
|
|
reqRepoReleaseReader := context.RequireUnitReader(unit.TypeReleases)
|
|
reqRepoIssuesOrPullsWriter := context.RequireUnitWriter(unit.TypeIssues, unit.TypePullRequests)
|
|
reqRepoIssuesOrPullsReader := context.RequireUnitReader(unit.TypeIssues, unit.TypePullRequests)
|
|
reqRepoProjectsReader := context.RequireUnitReader(unit.TypeProjects)
|
|
reqRepoProjectsWriter := context.RequireUnitWriter(unit.TypeProjects)
|
|
reqRepoActionsReader := context.RequireUnitReader(unit.TypeActions)
|
|
reqRepoActionsWriter := context.RequireUnitWriter(unit.TypeActions)
|
|
|
|
// the legacy names "reqRepoXxx" should be renamed to the correct name "reqUnitXxx", these permissions are for units, not repos
|
|
reqUnitsWithMarkdown := context.RequireUnitReader(unit.TypeCode, unit.TypeIssues, unit.TypePullRequests, unit.TypeReleases, unit.TypeWiki)
|
|
reqUnitsWithMentions := context.RequireUnitReader(unit.TypeIssues, unit.TypePullRequests, unit.TypeReleases, unit.TypeWiki, unit.TypeProjects)
|
|
reqUnitCodeReader := context.RequireUnitReader(unit.TypeCode)
|
|
reqUnitIssuesReader := context.RequireUnitReader(unit.TypeIssues)
|
|
reqUnitPullsReader := context.RequireUnitReader(unit.TypePullRequests)
|
|
reqUnitWikiReader := context.RequireUnitReader(unit.TypeWiki)
|
|
reqUnitWikiWriter := context.RequireUnitWriter(unit.TypeWiki)
|
|
|
|
reqPackageAccess := func(accessMode perm.AccessMode) func(ctx *context.Context) {
|
|
return func(ctx *context.Context) {
|
|
if ctx.Package.AccessMode < accessMode && !ctx.IsUserSiteAdmin() {
|
|
ctx.NotFound(nil)
|
|
}
|
|
}
|
|
}
|
|
|
|
individualPermsChecker := func(ctx *context.Context) {
|
|
// org permissions have been checked in context.OrgAssignment(), but individual permissions haven't been checked.
|
|
if ctx.ContextUser.IsIndividual() {
|
|
switch ctx.ContextUser.Visibility {
|
|
case structs.VisibleTypePrivate:
|
|
if ctx.Doer == nil || (ctx.ContextUser.ID != ctx.Doer.ID && !ctx.Doer.IsAdmin) {
|
|
ctx.NotFound(nil)
|
|
return
|
|
}
|
|
case structs.VisibleTypeLimited:
|
|
if ctx.Doer == nil {
|
|
ctx.NotFound(nil)
|
|
return
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
m.Group("/org", func() {
|
|
m.Group("/{org}", func() {
|
|
m.Get("/members", org.Members)
|
|
}, context.OrgAssignment(context.OrgAssignmentOptions{}))
|
|
}, optSignIn)
|
|
// end "/org": members
|
|
|
|
m.Group("/org", func() {
|
|
m.Group("", func() {
|
|
m.Get("/create", org.Create)
|
|
m.Post("/create", web.Bind(forms.CreateOrgForm{}), org.CreatePost)
|
|
})
|
|
|
|
m.Group("/invite/{token}", func() {
|
|
m.Get("", org.TeamInvite)
|
|
m.Post("", org.TeamInvitePost)
|
|
})
|
|
|
|
m.Group("/{org}", func() {
|
|
m.Get("/dashboard", user.Dashboard)
|
|
m.Get("/dashboard/{team}", user.Dashboard)
|
|
m.Get("/dashboard/-/heatmap", user.DashboardHeatmap)
|
|
m.Get("/dashboard/-/heatmap/{team}", user.DashboardHeatmap)
|
|
m.Get("/issues", user.Issues)
|
|
m.Get("/issues/{team}", user.Issues)
|
|
m.Get("/pulls", user.Pulls)
|
|
m.Get("/pulls/{team}", user.Pulls)
|
|
m.Get("/milestones", reqMilestonesDashboardPageEnabled, user.Milestones)
|
|
m.Get("/milestones/{team}", reqMilestonesDashboardPageEnabled, user.Milestones)
|
|
m.Post("/members/action/{action}", org.MembersAction)
|
|
m.Get("/teams", org.Teams)
|
|
}, context.OrgAssignment(context.OrgAssignmentOptions{RequireMember: true, RequireTeamMember: true}))
|
|
|
|
m.Group("/{org}", func() {
|
|
m.Get("/teams/{team}", org.TeamMembers)
|
|
m.Get("/teams/{team}/repositories", org.TeamRepositories)
|
|
m.Post("/teams/{team}/action/{action}", org.TeamsAction)
|
|
m.Post("/teams/{team}/action/repo/{action}", org.TeamsRepoAction)
|
|
}, context.OrgAssignment(context.OrgAssignmentOptions{RequireMember: true, RequireTeamMember: true}))
|
|
|
|
// require member/team-admin permission (old logic is: requireMember=true, requireTeamAdmin=true)
|
|
// but it doesn't seem right: requireTeamAdmin does nothing
|
|
m.Group("/{org}", func() {
|
|
m.Get("/teams/-/search", org.SearchTeam)
|
|
}, context.OrgAssignment(context.OrgAssignmentOptions{RequireMember: true, RequireTeamAdmin: true}))
|
|
|
|
// require owner permission
|
|
m.Group("/{org}", func() {
|
|
m.Get("/teams/new", org.NewTeam)
|
|
m.Post("/teams/new", web.Bind(forms.CreateTeamForm{}), org.NewTeamPost)
|
|
m.Get("/teams/{team}/edit", org.EditTeam)
|
|
m.Post("/teams/{team}/edit", web.Bind(forms.CreateTeamForm{}), org.EditTeamPost)
|
|
m.Post("/teams/{team}/delete", org.DeleteTeam)
|
|
|
|
m.Get("/worktime", context.OrgAssignment(context.OrgAssignmentOptions{RequireOwner: true}), org.Worktime)
|
|
|
|
m.Group("/settings", func() {
|
|
m.Combo("").Get(org.Settings).
|
|
Post(web.Bind(forms.UpdateOrgSettingForm{}), org.SettingsPost)
|
|
m.Post("/avatar", web.Bind(forms.AvatarForm{}), org.SettingsAvatar)
|
|
m.Post("/avatar/delete", org.SettingsDeleteAvatar)
|
|
m.Group("/applications", func() {
|
|
m.Get("", org.Applications)
|
|
m.Post("/oauth2", web.Bind(forms.EditOAuth2ApplicationForm{}), org.OAuthApplicationsPost)
|
|
m.Group("/oauth2/{id}", func() {
|
|
m.Combo("").Get(org.OAuth2ApplicationShow).Post(web.Bind(forms.EditOAuth2ApplicationForm{}), org.OAuth2ApplicationEdit)
|
|
m.Post("/regenerate_secret", org.OAuthApplicationsRegenerateSecret)
|
|
m.Post("/delete", org.DeleteOAuth2Application)
|
|
})
|
|
}, oauth2Enabled)
|
|
|
|
m.Group("/hooks", func() {
|
|
m.Get("", org.Webhooks)
|
|
m.Post("/delete", org.DeleteWebhook)
|
|
addWebhookAddRoutes()
|
|
m.Group("/{id}", func() {
|
|
m.Get("", repo_setting.WebHooksEdit)
|
|
m.Post("/replay/{uuid}", repo_setting.ReplayWebhook)
|
|
})
|
|
addWebhookEditRoutes()
|
|
}, webhooksEnabled)
|
|
|
|
m.Group("/labels", func() {
|
|
m.Get("", org.RetrieveLabels, org.Labels)
|
|
m.Post("/new", web.Bind(forms.CreateLabelForm{}), org.NewLabel)
|
|
m.Post("/edit", web.Bind(forms.CreateLabelForm{}), org.UpdateLabel)
|
|
m.Post("/delete", org.DeleteLabel)
|
|
m.Post("/initialize", web.Bind(forms.InitializeLabelsForm{}), org.InitializeLabels)
|
|
})
|
|
|
|
m.Group("/actions", func() {
|
|
m.Get("", misc.LocationRedirect("./actions/general"))
|
|
m.Group("/general", func() {
|
|
m.Get("", shared_actions.GeneralSettings)
|
|
m.Post("", shared_actions.UpdateGeneralSettings)
|
|
})
|
|
addSettingsRunnersRoutes()
|
|
addSettingsSecretsRoutes()
|
|
addSettingsVariablesRoutes()
|
|
}, actions.MustEnableActions)
|
|
|
|
m.Post("/rename", web.Bind(forms.RenameOrgForm{}), org.SettingsRenamePost)
|
|
m.Post("/delete", org.SettingsDeleteOrgPost)
|
|
m.Post("/visibility", org.SettingsChangeVisibilityPost)
|
|
|
|
m.Group("/packages", func() {
|
|
m.Get("", org.Packages)
|
|
m.Group("/rules", func() {
|
|
m.Group("/add", func() {
|
|
m.Get("", org.PackagesRuleAdd)
|
|
m.Post("", web.Bind(forms.PackageCleanupRuleForm{}), org.PackagesRuleAddPost)
|
|
})
|
|
m.Group("/{id}", func() {
|
|
m.Get("", org.PackagesRuleEdit)
|
|
m.Post("", web.Bind(forms.PackageCleanupRuleForm{}), org.PackagesRuleEditPost)
|
|
m.Get("/preview", org.PackagesRulePreview)
|
|
})
|
|
})
|
|
m.Group("/cargo", func() {
|
|
m.Post("/initialize", org.InitializeCargoIndex)
|
|
m.Post("/rebuild", org.RebuildCargoIndex)
|
|
})
|
|
}, packagesEnabled)
|
|
|
|
m.Group("/blocked_users", func() {
|
|
m.Get("", org.BlockedUsers)
|
|
m.Post("", web.Bind(forms.BlockUserForm{}), org.BlockedUsersPost)
|
|
})
|
|
}, ctxDataSet("EnableOAuth2", setting.OAuth2.Enabled, "EnablePackages", setting.Packages.Enabled, "PageIsOrgSettings", true))
|
|
}, context.OrgAssignment(context.OrgAssignmentOptions{RequireOwner: true}))
|
|
}, reqSignIn)
|
|
// end "/org": most org routes
|
|
|
|
m.Group("/repo", func() {
|
|
m.Get("/create", repo.Create)
|
|
m.Post("/create", web.Bind(forms.CreateRepoForm{}), repo.CreatePost)
|
|
m.Get("/migrate", repo.Migrate)
|
|
m.Post("/migrate", web.Bind(forms.MigrateRepoForm{}), repo.MigratePost)
|
|
m.Get("/search", repo.SearchRepo)
|
|
}, reqSignIn)
|
|
// end "/repo": create, migrate, search
|
|
|
|
m.Group("/{username}/-", func() {
|
|
if setting.Packages.Enabled {
|
|
m.Group("/packages", func() {
|
|
m.Get("", user.ListPackages)
|
|
m.Group("/{type}/{name}", func() {
|
|
m.Get("", user.RedirectToLastVersion)
|
|
m.Get("/versions", user.ListPackageVersions)
|
|
m.Group("/{version}", func() {
|
|
m.Get("", user.ViewPackageVersion)
|
|
m.Get("/{version_sub}", user.ViewPackageVersion)
|
|
m.Get("/files/{fileid}", user.DownloadPackageFile)
|
|
m.Group("/settings", func() {
|
|
m.Get("", user.PackageSettings)
|
|
m.Post("", web.Bind(forms.PackageSettingForm{}), user.PackageSettingsPost)
|
|
}, reqPackageAccess(perm.AccessModeWrite))
|
|
})
|
|
})
|
|
}, context.PackageAssignment(), reqPackageAccess(perm.AccessModeRead))
|
|
}
|
|
|
|
// at the moment, only editing "owner-level projects" need to "mention", maybe in the future we can relax the permission check
|
|
m.Get("/mentions-in-owner", reqUnitAccess(unit.TypeProjects, perm.AccessModeWrite, true), org.GetMentionsInOwner)
|
|
|
|
m.Get("/repositories", org.Repositories)
|
|
m.Get("/heatmap", user.DashboardHeatmap)
|
|
|
|
m.Group("/projects", func() {
|
|
m.Group("", func() {
|
|
m.Get("", org.Projects)
|
|
m.Get("/{id}", org.ViewProject)
|
|
}, reqUnitAccess(unit.TypeProjects, perm.AccessModeRead, true))
|
|
m.Group("", func() { //nolint:dupl // duplicates lines 1421-1441
|
|
m.Get("/new", org.RenderNewProject)
|
|
m.Post("/new", web.Bind(forms.CreateProjectForm{}), org.NewProjectPost)
|
|
m.Group("/{id}", func() {
|
|
m.Post("/delete", org.DeleteProject)
|
|
|
|
m.Get("/edit", org.RenderEditProject)
|
|
m.Post("/edit", web.Bind(forms.CreateProjectForm{}), org.EditProjectPost)
|
|
m.Post("/{action:open|close}", org.ChangeProjectStatus)
|
|
|
|
// TODO: improper name. Others are "delete project", "edit project", but this one is "move columns"
|
|
m.Post("/move", project.MoveColumns)
|
|
m.Post("/columns/new", web.Bind(forms.EditProjectColumnForm{}), org.AddColumnToProjectPost)
|
|
m.Group("/{columnID}", func() {
|
|
m.Put("", web.Bind(forms.EditProjectColumnForm{}), org.EditProjectColumn)
|
|
m.Delete("", org.DeleteProjectColumn)
|
|
m.Post("/default", org.SetDefaultProjectColumn)
|
|
m.Post("/move", org.MoveIssues)
|
|
})
|
|
})
|
|
}, reqSignIn, reqUnitAccess(unit.TypeProjects, perm.AccessModeWrite, true), func(ctx *context.Context) {
|
|
if ctx.ContextUser.IsIndividual() && ctx.ContextUser.ID != ctx.Doer.ID {
|
|
ctx.NotFound(nil)
|
|
return
|
|
}
|
|
})
|
|
}, reqUnitAccess(unit.TypeProjects, perm.AccessModeRead, true), individualPermsChecker)
|
|
|
|
m.Group("", func() {
|
|
m.Get("/code", user.CodeSearch)
|
|
}, reqUnitAccess(unit.TypeCode, perm.AccessModeRead, false), individualPermsChecker)
|
|
}, optSignIn, context.UserAssignmentWeb(), context.OrgAssignment(context.OrgAssignmentOptions{}))
|
|
// end "/{username}/-": packages, projects, code
|
|
|
|
m.Group("/{username}/{reponame}/-", func() {
|
|
m.Group("/migrate", func() {
|
|
m.Get("/status", repo.MigrateStatus)
|
|
})
|
|
}, optSignIn, context.RepoAssignment, reqUnitCodeReader)
|
|
// end "/{username}/{reponame}/-": migrate
|
|
|
|
m.Group("/{username}/{reponame}/-", func() {
|
|
m.Get("/mentions-in-repo", repo.GetMentionsInRepo)
|
|
}, optSignIn, context.RepoAssignment, reqUnitsWithMentions)
|
|
// end "/{username}/{reponame}/-": mentions
|
|
|
|
m.Group("/{username}/{reponame}/settings", func() {
|
|
m.Group("", func() {
|
|
m.Combo("").Get(repo_setting.Settings).
|
|
Post(web.Bind(forms.RepoSettingForm{}), repo_setting.SettingsPost)
|
|
}, repo_setting.SettingsCtxData)
|
|
m.Post("/avatar", web.Bind(forms.AvatarForm{}), repo_setting.SettingsAvatar)
|
|
m.Post("/avatar/delete", repo_setting.SettingsDeleteAvatar)
|
|
|
|
m.Combo("/public_access").Get(repo_setting.PublicAccess).Post(repo_setting.PublicAccessPost)
|
|
|
|
m.Group("/collaboration", func() {
|
|
m.Combo("").Get(repo_setting.Collaboration).Post(repo_setting.CollaborationPost)
|
|
m.Post("/access_mode", repo_setting.ChangeCollaborationAccessMode)
|
|
m.Post("/delete", repo_setting.DeleteCollaboration)
|
|
m.Group("/team", func() {
|
|
m.Post("", repo_setting.AddTeamPost)
|
|
m.Post("/delete", repo_setting.DeleteTeam)
|
|
})
|
|
})
|
|
|
|
m.Group("/branches", func() {
|
|
m.Post("/", repo_setting.SetDefaultBranchPost)
|
|
}, repo.MustBeNotEmpty)
|
|
|
|
m.Group("/branches", func() {
|
|
m.Get("/", repo_setting.ProtectedBranchRules)
|
|
m.Combo("/edit").Get(repo_setting.SettingsProtectedBranch).
|
|
Post(web.Bind(forms.ProtectBranchForm{}), context.RepoMustNotBeArchived(), repo_setting.SettingsProtectedBranchPost)
|
|
m.Post("/{id}/delete", repo_setting.DeleteProtectedBranchRulePost)
|
|
m.Post("/priority", web.Bind(forms.ProtectBranchPriorityForm{}), context.RepoMustNotBeArchived(), repo_setting.UpdateBranchProtectionPriories)
|
|
})
|
|
|
|
m.Group("/tags", func() {
|
|
m.Get("", repo_setting.ProtectedTags)
|
|
m.Post("", web.Bind(forms.ProtectTagForm{}), context.RepoMustNotBeArchived(), repo_setting.NewProtectedTagPost)
|
|
m.Post("/delete", context.RepoMustNotBeArchived(), repo_setting.DeleteProtectedTagPost)
|
|
m.Get("/{id}", repo_setting.EditProtectedTag)
|
|
m.Post("/{id}", web.Bind(forms.ProtectTagForm{}), context.RepoMustNotBeArchived(), repo_setting.EditProtectedTagPost)
|
|
})
|
|
|
|
m.Group("/hooks/git", func() {
|
|
m.Get("", repo_setting.GitHooks)
|
|
m.Combo("/{name}").Get(repo_setting.GitHooksEdit).
|
|
Post(repo_setting.GitHooksEditPost)
|
|
}, context.GitHookService())
|
|
|
|
m.Group("/hooks", func() {
|
|
m.Get("", repo_setting.Webhooks)
|
|
m.Post("/delete", repo_setting.DeleteWebhook)
|
|
addWebhookAddRoutes()
|
|
m.Group("/{id}", func() {
|
|
m.Get("", repo_setting.WebHooksEdit)
|
|
m.Post("/test", repo_setting.TestWebhook)
|
|
m.Post("/replay/{uuid}", repo_setting.ReplayWebhook)
|
|
})
|
|
addWebhookEditRoutes()
|
|
}, webhooksEnabled)
|
|
|
|
m.Group("/keys", func() {
|
|
m.Combo("").Get(repo_setting.DeployKeys).
|
|
Post(web.Bind(forms.AddKeyForm{}), repo_setting.DeployKeysPost)
|
|
m.Post("/delete", repo_setting.DeleteDeployKey)
|
|
})
|
|
|
|
m.Group("/lfs", func() {
|
|
m.Get("/", repo_setting.LFSFiles)
|
|
m.Get("/show/{oid}", repo_setting.LFSFileGet)
|
|
m.Post("/delete/{oid}", repo_setting.LFSDelete)
|
|
m.Get("/pointers", repo_setting.LFSPointerFiles)
|
|
m.Post("/pointers/associate", repo_setting.LFSAutoAssociate)
|
|
m.Get("/find", repo_setting.LFSFileFind)
|
|
m.Group("/locks", func() {
|
|
m.Get("/", repo_setting.LFSLocks)
|
|
m.Post("/", repo_setting.LFSLockFile)
|
|
m.Post("/{lid}/unlock", repo_setting.LFSUnlock)
|
|
})
|
|
})
|
|
m.Group("/actions/general", func() {
|
|
m.Get("", repo_setting.ActionsGeneralSettings)
|
|
m.Post("/actions_unit", repo_setting.ActionsUnitPost)
|
|
}) // doesn't require actions enabled
|
|
m.Group("/actions", func() {
|
|
m.Get("", misc.LocationRedirect("./actions/general"))
|
|
addSettingsRunnersRoutes()
|
|
addSettingsSecretsRoutes()
|
|
addSettingsVariablesRoutes()
|
|
m.Group("/general", func() {
|
|
m.Group("/collaborative_owner", func() {
|
|
m.Post("/add", repo_setting.AddCollaborativeOwner)
|
|
m.Post("/delete", repo_setting.DeleteCollaborativeOwner)
|
|
})
|
|
m.Post("/token_permissions", repo_setting.UpdateTokenPermissions)
|
|
})
|
|
}, actions.MustEnableActions)
|
|
// the follow handler must be under "settings", otherwise this incomplete repo can't be accessed
|
|
m.Group("/migrate", func() {
|
|
m.Post("/retry", repo.MigrateRetryPost)
|
|
m.Post("/cancel", repo.MigrateCancelPost)
|
|
})
|
|
},
|
|
reqSignIn, context.RepoAssignment, reqRepoAdmin,
|
|
ctxDataSet("PageIsRepoSettings", true, "LFSStartServer", setting.LFS.StartServer),
|
|
)
|
|
// end "/{username}/{reponame}/settings"
|
|
|
|
// user/org home, including rss feeds like "/{username}/{reponame}.rss"
|
|
m.Get("/{username}/{reponame}", optSignIn, webAuth.AllowBasic, context.RepoAssignment, context.RepoRefByType(git.RefTypeBranch), repo.SetEditorconfigIfExists, repo.Home)
|
|
|
|
m.Post("/{username}/{reponame}/markup", optSignIn, context.RepoAssignment, reqUnitsWithMarkdown, web.Bind(structs.MarkupOption{}), misc.Markup)
|
|
|
|
m.Group("/{username}/{reponame}", func() {
|
|
m.Group("/tree-list", func() {
|
|
m.Get("/branch/*", context.RepoRefByType(git.RefTypeBranch), repo.TreeList)
|
|
m.Get("/tag/*", context.RepoRefByType(git.RefTypeTag), repo.TreeList)
|
|
m.Get("/commit/*", context.RepoRefByType(git.RefTypeCommit), repo.TreeList)
|
|
})
|
|
m.Group("/tree-view", func() {
|
|
m.Get("/branch/*", context.RepoRefByType(git.RefTypeBranch), repo.TreeViewNodes)
|
|
m.Get("/tag/*", context.RepoRefByType(git.RefTypeTag), repo.TreeViewNodes)
|
|
m.Get("/commit/*", context.RepoRefByType(git.RefTypeCommit), repo.TreeViewNodes)
|
|
})
|
|
m.Get("/compare", repo.MustBeNotEmpty, repo.SetEditorconfigIfExists, repo.SetDiffViewStyle, repo.SetWhitespaceBehavior, repo.CompareDiff)
|
|
m.Combo("/compare/*", repo.MustBeNotEmpty, repo.SetEditorconfigIfExists).
|
|
Get(repo.SetDiffViewStyle, repo.SetWhitespaceBehavior, repo.CompareDiff).
|
|
Post(reqSignIn, context.RepoMustNotBeArchived(), reqUnitPullsReader, repo.MustAllowPulls, web.Bind(forms.CreateIssueForm{}), repo.SetWhitespaceBehavior, repo.CompareAndPullRequestPost)
|
|
m.Get("/pulls/new/*", repo.PullsNewRedirect)
|
|
}, optSignIn, context.RepoAssignment, reqUnitCodeReader)
|
|
// end "/{username}/{reponame}": repo code: find, compare, list
|
|
|
|
addIssuesPullsViewRoutes := func() {
|
|
// for /{username}/{reponame}/issues" or "/{username}/{reponame}/pulls"
|
|
m.Get("/posters", repo.IssuePullPosters)
|
|
m.Group("/{index}", func() {
|
|
m.Get("/info", repo.GetIssueInfo)
|
|
m.Get("/attachments", repo.GetIssueAttachments)
|
|
m.Get("/attachments/{uuid}", repo.GetAttachment)
|
|
m.Group("/content-history", func() {
|
|
m.Get("/overview", repo.GetContentHistoryOverview)
|
|
m.Get("/list", repo.GetContentHistoryList)
|
|
m.Get("/detail", repo.GetContentHistoryDetail)
|
|
})
|
|
})
|
|
}
|
|
// FIXME: many "pulls" requests are sent to "issues" endpoints correctly, so the issue endpoints have to tolerate pull request permissions at the moment
|
|
m.Group("/{username}/{reponame}/{type:issues}", addIssuesPullsViewRoutes, optSignIn, context.RepoAssignment, context.RequireUnitReader(unit.TypeIssues, unit.TypePullRequests))
|
|
m.Group("/{username}/{reponame}/{type:pulls}", addIssuesPullsViewRoutes, optSignIn, context.RepoAssignment, reqUnitPullsReader)
|
|
|
|
m.Group("/{username}/{reponame}", func() {
|
|
m.Get("/comments/{id}/attachments", repo.GetCommentAttachments)
|
|
m.Get("/labels", repo.RetrieveLabelsForList, repo.Labels)
|
|
m.Get("/milestones", repo.Milestones)
|
|
m.Get("/milestone/{id}", repo.MilestoneIssuesAndPulls)
|
|
m.Get("/issues/suggestions", repo.IssueSuggestions)
|
|
}, optSignIn, context.RepoAssignment, reqRepoIssuesOrPullsReader) // issue/pull attachments, labels, milestones
|
|
// end "/{username}/{reponame}": view milestone, label, issue, pull, etc
|
|
|
|
m.Group("/{username}/{reponame}/{type:issues}", func() {
|
|
// these handlers also check unit permissions internally
|
|
m.Get("", repo.Issues)
|
|
m.Get("/{index}", repo.ViewIssue) // also do pull-request redirection (".../issues/{PR-number}" -> ".../pulls/{PR-number}")
|
|
}, optSignIn, context.RepoAssignment, context.RequireUnitReader(unit.TypeIssues, unit.TypePullRequests, unit.TypeExternalTracker))
|
|
// end "/{username}/{reponame}": issue list, issue view (pull-request redirection), external tracker
|
|
|
|
m.Group("/{username}/{reponame}", func() { // edit issues, pulls, labels, milestones, etc
|
|
m.Group("/issues", func() {
|
|
m.Group("/new", func() {
|
|
m.Combo("").Get(repo.NewIssue).
|
|
Post(web.Bind(forms.CreateIssueForm{}), repo.NewIssuePost)
|
|
m.Get("/choose", repo.NewIssueChooseTemplate)
|
|
})
|
|
m.Get("/search", repo.SearchRepoIssuesJSON)
|
|
}, reqUnitIssuesReader)
|
|
|
|
addIssuesPullsUpdateRoutes := func() {
|
|
// for "/{username}/{reponame}/issues" or "/{username}/{reponame}/pulls"
|
|
m.Group("/{index}", func() {
|
|
m.Post("/title", repo.UpdateIssueTitle)
|
|
m.Post("/content", repo.UpdateIssueContent)
|
|
m.Post("/deadline", repo.UpdateIssueDeadline)
|
|
m.Post("/watch", repo.IssueWatch)
|
|
m.Post("/ref", repo.UpdateIssueRef)
|
|
m.Post("/pin", reqRepoAdmin, repo.IssuePinOrUnpin)
|
|
m.Post("/viewed-files", repo.UpdateViewedFiles)
|
|
m.Group("/dependency", func() {
|
|
m.Post("/add", repo.AddDependency)
|
|
m.Post("/delete", repo.RemoveDependency)
|
|
})
|
|
m.Combo("/comments").Post(repo.MustAllowUserComment, web.Bind(forms.CreateCommentForm{}), repo.NewComment)
|
|
m.Group("/times", func() {
|
|
m.Post("/add", web.Bind(forms.AddTimeManuallyForm{}), repo.AddTimeManually)
|
|
m.Post("/{timeid}/delete", repo.DeleteTime)
|
|
m.Group("/stopwatch", func() {
|
|
m.Post("/start", repo.IssueStartStopwatch)
|
|
m.Post("/stop", repo.IssueStopStopwatch)
|
|
m.Post("/cancel", repo.CancelStopwatch)
|
|
})
|
|
})
|
|
m.Post("/time_estimate", repo.UpdateIssueTimeEstimate)
|
|
m.Post("/reactions/{action}", web.Bind(forms.ReactionForm{}), repo.ChangeIssueReaction)
|
|
m.Post("/lock", reqRepoIssuesOrPullsWriter, web.Bind(forms.IssueLockForm{}), repo.LockIssue)
|
|
m.Post("/unlock", reqRepoIssuesOrPullsWriter, repo.UnlockIssue)
|
|
m.Post("/delete", reqRepoAdmin, repo.DeleteIssue)
|
|
m.Post("/content-history/soft-delete", repo.SoftDeleteContentHistory)
|
|
})
|
|
|
|
m.Post("/attachments", repo.UploadIssueAttachment)
|
|
m.Post("/attachments/remove", repo.DeleteAttachment)
|
|
|
|
m.Post("/labels", reqRepoIssuesOrPullsWriter, repo.UpdateIssueLabel)
|
|
m.Post("/milestone", reqRepoIssuesOrPullsWriter, repo.UpdateIssueMilestone)
|
|
m.Post("/projects", reqRepoIssuesOrPullsWriter, reqRepoProjectsReader, repo.UpdateIssueProject)
|
|
m.Post("/assignee", reqRepoIssuesOrPullsWriter, repo.UpdateIssueAssignee)
|
|
m.Post("/status", reqRepoIssuesOrPullsWriter, repo.UpdateIssueStatus)
|
|
m.Post("/delete", reqRepoAdmin, repo.BatchDeleteIssues)
|
|
m.Delete("/unpin/{index}", reqRepoAdmin, repo.IssueUnpin)
|
|
m.Post("/move_pin", reqRepoAdmin, repo.IssuePinMove)
|
|
}
|
|
// FIXME: many "pulls" requests are sent to "issues" endpoints incorrectly, so the issue endpoints have to tolerate pull request permissions at the moment
|
|
m.Group("/{type:issues}", addIssuesPullsUpdateRoutes, context.RequireUnitReader(unit.TypeIssues, unit.TypePullRequests), context.RepoMustNotBeArchived())
|
|
m.Group("/{type:pulls}", addIssuesPullsUpdateRoutes, reqUnitPullsReader, context.RepoMustNotBeArchived())
|
|
|
|
m.Group("/comments/{id}", func() {
|
|
m.Post("", repo.UpdateCommentContent)
|
|
m.Post("/delete", repo.DeleteComment)
|
|
m.Post("/reactions/{action}", web.Bind(forms.ReactionForm{}), repo.ChangeCommentReaction)
|
|
}, reqRepoIssuesOrPullsReader) // edit issue/pull comment
|
|
|
|
m.Group("/labels", func() {
|
|
m.Post("/new", web.Bind(forms.CreateLabelForm{}), repo.NewLabel)
|
|
m.Post("/edit", web.Bind(forms.CreateLabelForm{}), repo.UpdateLabel)
|
|
m.Post("/delete", repo.DeleteLabel)
|
|
m.Post("/initialize", web.Bind(forms.InitializeLabelsForm{}), repo.InitializeLabels)
|
|
}, reqRepoIssuesOrPullsWriter)
|
|
|
|
m.Group("/milestones", func() {
|
|
m.Combo("/new").Get(repo.NewMilestone).
|
|
Post(web.Bind(forms.CreateMilestoneForm{}), repo.NewMilestonePost)
|
|
m.Get("/{id}/edit", repo.EditMilestone)
|
|
m.Post("/{id}/edit", web.Bind(forms.CreateMilestoneForm{}), repo.EditMilestonePost)
|
|
m.Post("/{id}/{action}", repo.ChangeMilestoneStatus)
|
|
m.Post("/delete", repo.DeleteMilestone)
|
|
}, reqRepoIssuesOrPullsWriter)
|
|
|
|
// FIXME: many "pulls" requests are sent to "issues" endpoints incorrectly, need to move these routes to the proper place
|
|
m.Group("/issues", func() {
|
|
m.Post("/request_review", repo.UpdatePullReviewRequest)
|
|
m.Post("/dismiss_review", reqRepoAdmin, web.Bind(forms.DismissReviewForm{}), repo.DismissReview)
|
|
m.Post("/resolve_conversation", repo.SetShowOutdatedComments, repo.UpdateResolveConversation)
|
|
}, reqUnitPullsReader)
|
|
m.Post("/pull/{index}/target_branch", reqUnitPullsReader, repo.UpdatePullRequestTarget)
|
|
}, reqSignIn, context.RepoAssignment, context.RepoMustNotBeArchived())
|
|
// end "/{username}/{reponame}": create or edit issues, pulls, labels, milestones
|
|
|
|
m.Group("/{username}/{reponame}", func() { // repo code (at least "code reader")
|
|
m.Group("", func() {
|
|
m.Group("", func() {
|
|
// "GET" requests only need "code reader" permission, "POST" requests need "code writer" permission.
|
|
// Because reader can "fork and edit"
|
|
canWriteToBranch := context.CanWriteToBranch()
|
|
m.Post("/_preview/*", repo.DiffPreviewPost) // read-only, fine with "code reader"
|
|
m.Post("/_fork/*", repo.ForkToEditPost) // read-only, fork to own repo, fine with "code reader"
|
|
|
|
// the path params are used in PrepareCommitFormOptions to construct the correct form action URL
|
|
m.Combo("/{editor_action:_edit}/*").
|
|
Get(repo.EditFile).
|
|
Post(web.Bind(forms.EditRepoFileForm{}), canWriteToBranch, repo.EditFilePost)
|
|
m.Combo("/{editor_action:_new}/*").
|
|
Get(repo.EditFile).
|
|
Post(web.Bind(forms.EditRepoFileForm{}), canWriteToBranch, repo.EditFilePost)
|
|
m.Combo("/{editor_action:_delete}/*").
|
|
Get(repo.DeleteFile).
|
|
Post(web.Bind(forms.DeleteRepoFileForm{}), canWriteToBranch, repo.DeleteFilePost)
|
|
m.Combo("/{editor_action:_upload}/*", repo.MustBeAbleToUpload).
|
|
Get(repo.UploadFile).
|
|
Post(web.Bind(forms.UploadRepoFileForm{}), canWriteToBranch, repo.UploadFilePost)
|
|
m.Combo("/{editor_action:_diffpatch}/*").
|
|
Get(repo.NewDiffPatch).
|
|
Post(web.Bind(forms.EditRepoFileForm{}), canWriteToBranch, repo.NewDiffPatchPost)
|
|
m.Combo("/{editor_action:_cherrypick}/{sha:([a-f0-9]{7,64})}/*").
|
|
Get(repo.CherryPick).
|
|
Post(web.Bind(forms.CherryPickForm{}), canWriteToBranch, repo.CherryPickPost)
|
|
}, context.RepoRefByType(git.RefTypeBranch), repo.WebGitOperationCommonData)
|
|
m.Group("", func() {
|
|
m.Post("/upload-file", repo.UploadFileToServer)
|
|
m.Post("/upload-remove", repo.RemoveUploadFileFromServer)
|
|
}, repo.MustBeAbleToUpload, reqRepoCodeWriter)
|
|
}, repo.MustBeEditable, context.RepoMustNotBeArchived())
|
|
|
|
m.Group("/branches", func() {
|
|
m.Group("/_new", func() {
|
|
m.Post("/branch/*", context.RepoRefByType(git.RefTypeBranch), repo.CreateBranch)
|
|
m.Post("/tag/*", context.RepoRefByType(git.RefTypeTag), repo.CreateBranch)
|
|
m.Post("/commit/*", context.RepoRefByType(git.RefTypeCommit), repo.CreateBranch)
|
|
}, web.Bind(forms.NewBranchForm{}))
|
|
m.Post("/delete", repo.DeleteBranchPost)
|
|
m.Post("/restore", repo.RestoreBranchPost)
|
|
m.Post("/rename", web.Bind(forms.RenameBranchForm{}), repo_setting.RenameBranchPost)
|
|
m.Post("/merge-upstream", repo.MergeUpstream)
|
|
}, context.RepoMustNotBeArchived(), reqRepoCodeWriter, repo.MustBeNotEmpty)
|
|
|
|
m.Combo("/fork").Get(repo.Fork).Post(web.Bind(forms.CreateRepoForm{}), repo.ForkPost)
|
|
}, reqSignIn, context.RepoAssignment, reqUnitCodeReader)
|
|
// end "/{username}/{reponame}": repo code
|
|
|
|
m.Group("/{username}/{reponame}", func() { // repo tags
|
|
m.Group("/tags", func() {
|
|
m.Get("", context.RepoRefByDefaultBranch() /* for the "commits" tab */, repo.TagsList)
|
|
m.Get(".rss", webAuth.AllowBasic, feedEnabled, repo.TagsListFeedRSS)
|
|
m.Get(".atom", webAuth.AllowBasic, feedEnabled, repo.TagsListFeedAtom)
|
|
m.Get("/list", repo.GetTagList)
|
|
}, ctxDataSet("EnableFeed", setting.Other.EnableFeed))
|
|
m.Post("/tags/delete", reqSignIn, reqRepoCodeWriter, context.RepoMustNotBeArchived(), repo.DeleteTag)
|
|
}, optSignIn, context.RepoAssignment, repo.MustBeNotEmpty, reqUnitCodeReader)
|
|
// end "/{username}/{reponame}": repo tags
|
|
|
|
m.Group("/{username}/{reponame}", func() { // repo releases
|
|
m.Group("/releases", func() {
|
|
m.Get("", repo.Releases)
|
|
m.Get(".rss", webAuth.AllowBasic, feedEnabled, repo.ReleasesFeedRSS)
|
|
m.Get(".atom", webAuth.AllowBasic, feedEnabled, repo.ReleasesFeedAtom)
|
|
m.Get("/tag/*", repo.SingleRelease)
|
|
m.Get("/latest", repo.LatestRelease)
|
|
}, ctxDataSet("EnableFeed", setting.Other.EnableFeed))
|
|
m.Get("/releases/attachments/{uuid}", webAuth.AllowBasic, webAuth.AllowOAuth2, repo.GetAttachment)
|
|
m.Get("/releases/download/{vTag}/{fileName}", webAuth.AllowBasic, webAuth.AllowOAuth2, repo.RedirectDownload)
|
|
m.Group("/releases", func() {
|
|
m.Get("/new", repo.NewRelease)
|
|
m.Post("/new", web.Bind(forms.NewReleaseForm{}), repo.NewReleasePost)
|
|
m.Post("/generate-notes", web.Bind(forms.GenerateReleaseNotesForm{}), repo.GenerateReleaseNotes)
|
|
m.Post("/delete", repo.DeleteRelease)
|
|
m.Post("/attachments", repo.UploadReleaseAttachment)
|
|
m.Post("/attachments/remove", repo.DeleteAttachment)
|
|
}, reqSignIn, context.RepoMustNotBeArchived(), reqRepoReleaseWriter)
|
|
m.Group("/releases", func() {
|
|
m.Get("/edit/*", repo.EditRelease)
|
|
m.Post("/edit/*", web.Bind(forms.EditReleaseForm{}), repo.EditReleasePost)
|
|
}, reqSignIn, context.RepoMustNotBeArchived(), reqRepoReleaseWriter, repo.CommitInfoCache)
|
|
}, optSignIn, context.RepoAssignment, repo.MustBeNotEmpty, reqRepoReleaseReader)
|
|
// end "/{username}/{reponame}": repo releases
|
|
|
|
m.Group("/{username}/{reponame}", func() { // to maintain compatibility with old attachments
|
|
m.Get("/attachments/{uuid}", webAuth.AllowBasic, webAuth.AllowOAuth2, repo.GetAttachment)
|
|
}, optSignIn, context.RepoAssignment)
|
|
// end "/{username}/{reponame}": compatibility with old attachments
|
|
|
|
m.Group("/{username}/{reponame}", func() {
|
|
m.Post("/topics", repo.TopicsPost)
|
|
}, context.RepoAssignment, reqRepoAdmin, context.RepoMustNotBeArchived())
|
|
|
|
m.Group("/{username}/{reponame}", func() {
|
|
if setting.Packages.Enabled {
|
|
m.Get("/packages", repo.Packages)
|
|
}
|
|
}, optSignIn, context.RepoAssignment)
|
|
|
|
m.Group("/{username}/{reponame}/projects", func() {
|
|
m.Get("", repo.Projects)
|
|
m.Get("/{id}", repo.ViewProject)
|
|
m.Group("", func() { //nolint:dupl // duplicates lines 1034-1054
|
|
m.Get("/new", repo.RenderNewProject)
|
|
m.Post("/new", web.Bind(forms.CreateProjectForm{}), repo.NewProjectPost)
|
|
m.Group("/{id}", func() {
|
|
m.Post("/delete", repo.DeleteProject)
|
|
|
|
m.Get("/edit", repo.RenderEditProject)
|
|
m.Post("/edit", web.Bind(forms.CreateProjectForm{}), repo.EditProjectPost)
|
|
m.Post("/{action:open|close}", repo.ChangeProjectStatus)
|
|
|
|
// TODO: improper name. Others are "delete project", "edit project", but this one is "move columns"
|
|
m.Post("/move", project.MoveColumns)
|
|
m.Post("/columns/new", web.Bind(forms.EditProjectColumnForm{}), repo.AddColumnToProjectPost)
|
|
m.Group("/{columnID}", func() {
|
|
m.Put("", web.Bind(forms.EditProjectColumnForm{}), repo.EditProjectColumn)
|
|
m.Delete("", repo.DeleteProjectColumn)
|
|
m.Post("/default", repo.SetDefaultProjectColumn)
|
|
m.Post("/move", repo.MoveIssues)
|
|
})
|
|
})
|
|
}, reqRepoProjectsWriter, context.RepoMustNotBeArchived())
|
|
}, optSignIn, context.RepoAssignment, reqRepoProjectsReader, repo.MustEnableRepoProjects)
|
|
// end "/{username}/{reponame}/projects"
|
|
|
|
m.Group("/{username}/{reponame}/actions", func() {
|
|
m.Get("", actions.List)
|
|
m.Post("/disable", reqRepoAdmin, actions.DisableWorkflowFile)
|
|
m.Post("/enable", reqRepoAdmin, actions.EnableWorkflowFile)
|
|
m.Post("/run", reqRepoActionsWriter, actions.Run)
|
|
m.Get("/workflow-dispatch-inputs", reqRepoActionsWriter, actions.WorkflowDispatchInputs)
|
|
m.Post("/approve-all-checks", reqRepoActionsWriter, actions.ApproveAllChecks)
|
|
|
|
m.Group("/runs/{run}", func() {
|
|
m.Combo("").
|
|
Get(actions.View).
|
|
Post(web.Bind(actions.ViewRequest{}), actions.ViewPost)
|
|
m.Group("/jobs/{job}", func() {
|
|
m.Combo("").
|
|
Get(actions.View).
|
|
Post(web.Bind(actions.ViewRequest{}), actions.ViewPost)
|
|
m.Post("/rerun", reqRepoActionsWriter, actions.Rerun)
|
|
m.Get("/logs", actions.Logs)
|
|
})
|
|
m.Get("/workflow", actions.ViewWorkflowFile)
|
|
m.Post("/cancel", reqRepoActionsWriter, actions.Cancel)
|
|
m.Post("/approve", reqRepoActionsWriter, actions.Approve)
|
|
m.Post("/delete", reqRepoActionsWriter, actions.Delete)
|
|
m.Get("/artifacts/{artifact_name}", actions.ArtifactsDownloadView)
|
|
m.Delete("/artifacts/{artifact_name}", reqRepoActionsWriter, actions.ArtifactsDeleteView)
|
|
m.Post("/rerun", reqRepoActionsWriter, actions.Rerun)
|
|
m.Post("/rerun-failed", reqRepoActionsWriter, actions.RerunFailed)
|
|
})
|
|
m.Group("/workflows/{workflow_name}", func() {
|
|
m.Get("/badge.svg", webAuth.AllowBasic, webAuth.AllowOAuth2, actions.GetWorkflowBadge)
|
|
})
|
|
}, optSignIn, context.RepoAssignment, repo.MustBeNotEmpty, reqRepoActionsReader, actions.MustEnableActions)
|
|
// end "/{username}/{reponame}/actions"
|
|
|
|
m.Group("/{username}/{reponame}/wiki", func() {
|
|
m.Combo("").
|
|
Get(repo.Wiki).
|
|
Post(context.RepoMustNotBeArchived(), reqSignIn, reqUnitWikiWriter, web.Bind(forms.NewWikiForm{}), repo.WikiPost)
|
|
m.Combo("/*").
|
|
Get(repo.Wiki).
|
|
Post(context.RepoMustNotBeArchived(), reqSignIn, reqUnitWikiWriter, web.Bind(forms.NewWikiForm{}), repo.WikiPost)
|
|
m.Get("/blob_excerpt/{sha}", repo.SetEditorconfigIfExists, repo.SetDiffViewStyle, repo.ExcerptBlob)
|
|
m.Get("/commit/{sha:[a-f0-9]{7,64}}", repo.SetEditorconfigIfExists, repo.SetDiffViewStyle, repo.SetWhitespaceBehavior, repo.Diff)
|
|
m.Get("/commit/{sha:[a-f0-9]{7,64}}.{ext:patch|diff}", repo.RawDiff)
|
|
m.Get("/raw/*", repo.WikiRaw)
|
|
}, optSignIn, context.RepoAssignment, repo.MustEnableWiki, reqUnitWikiReader, func(ctx *context.Context) {
|
|
ctx.Data["PageIsWiki"] = true
|
|
ctx.Data["CloneButtonOriginLink"] = ctx.Repo.Repository.WikiCloneLink(ctx, ctx.Doer)
|
|
})
|
|
// end "/{username}/{reponame}/wiki"
|
|
|
|
m.Group("/{username}/{reponame}/activity", func() {
|
|
// activity has its own permission checks
|
|
m.Get("", repo.Activity)
|
|
m.Get("/{period}", repo.Activity)
|
|
|
|
m.Group("", func() {
|
|
m.Group("/contributors", func() {
|
|
m.Get("", repo.Contributors)
|
|
m.Get("/data", repo.ContributorsData)
|
|
})
|
|
m.Group("/code-frequency", func() {
|
|
m.Get("", repo.CodeFrequency)
|
|
m.Get("/data", repo.CodeFrequencyData)
|
|
})
|
|
m.Group("/recent-commits", func() {
|
|
m.Get("", repo.RecentCommits)
|
|
m.Get("/data", repo.CodeFrequencyData) // "recent-commits" also uses the same data as "code-frequency"
|
|
})
|
|
}, reqUnitCodeReader)
|
|
},
|
|
optSignIn, context.RepoAssignment, repo.MustBeNotEmpty,
|
|
context.RequireUnitReader(unit.TypeCode, unit.TypeIssues, unit.TypePullRequests, unit.TypeReleases),
|
|
)
|
|
// end "/{username}/{reponame}/activity"
|
|
|
|
m.Group("/{username}/{reponame}", func() {
|
|
m.Get("/{type:pulls}", repo.Issues)
|
|
m.Group("/{type:pulls}/{index}", func() {
|
|
m.Get("", repo.SetEditorconfigIfExists, repo.SetWhitespaceBehavior, repo.GetPullDiffStats, repo.ViewIssue)
|
|
m.Get(".diff", repo.DownloadPullDiff)
|
|
m.Get(".patch", repo.DownloadPullPatch)
|
|
m.Get("/merge_box", repo.ViewPullMergeBox)
|
|
m.Group("/commits", func() {
|
|
m.Get("", repo.SetWhitespaceBehavior, repo.GetPullDiffStats, repo.ViewPullCommits)
|
|
m.Get("/list", repo.GetPullCommits)
|
|
m.Get("/{sha:[a-f0-9]{7,64}}", repo.SetEditorconfigIfExists, repo.SetDiffViewStyle, repo.SetWhitespaceBehavior, repo.SetShowOutdatedComments, repo.ViewPullFilesForSingleCommit)
|
|
})
|
|
m.Post("/merge", context.RepoMustNotBeArchived(), web.Bind(forms.MergePullRequestForm{}), repo.MergePullRequest)
|
|
m.Post("/cancel_auto_merge", context.RepoMustNotBeArchived(), repo.CancelAutoMergePullRequest)
|
|
m.Post("/update", repo.UpdatePullRequest)
|
|
m.Post("/set_allow_maintainer_edit", web.Bind(forms.UpdateAllowEditsForm{}), repo.SetAllowEdits)
|
|
m.Post("/cleanup", context.RepoMustNotBeArchived(), repo.CleanUpPullRequest)
|
|
m.Group("/files", func() {
|
|
m.Get("", repo.SetEditorconfigIfExists, repo.SetDiffViewStyle, repo.SetWhitespaceBehavior, repo.SetShowOutdatedComments, repo.ViewPullFilesForAllCommitsOfPr)
|
|
m.Get("/{shaFrom:[a-f0-9]{7,64}}..{shaTo:[a-f0-9]{7,64}}", repo.SetEditorconfigIfExists, repo.SetDiffViewStyle, repo.SetWhitespaceBehavior, repo.SetShowOutdatedComments, repo.ViewPullFilesForRange)
|
|
m.Group("/reviews", func() {
|
|
m.Get("/new_comment", repo.RenderNewCodeCommentForm)
|
|
m.Post("/comments", web.Bind(forms.CodeCommentForm{}), repo.SetShowOutdatedComments, repo.CreateCodeComment)
|
|
m.Post("/submit", web.Bind(forms.SubmitReviewForm{}), repo.SubmitReview)
|
|
}, context.RepoMustNotBeArchived())
|
|
})
|
|
})
|
|
}, optSignIn, context.RepoAssignment, repo.MustAllowPulls, reqUnitPullsReader)
|
|
// end "/{username}/{reponame}/pulls/{index}": repo pull request
|
|
|
|
m.Group("/{username}/{reponame}", func() {
|
|
m.Group("/activity_author_data", func() {
|
|
m.Get("", repo.ActivityAuthors)
|
|
m.Get("/{period}", repo.ActivityAuthors)
|
|
}, repo.MustBeNotEmpty)
|
|
|
|
m.Group("/archive", func() {
|
|
m.Get("/*", repo.Download)
|
|
m.Post("/*", repo.InitiateDownload)
|
|
}, webAuth.AllowBasic, webAuth.AllowOAuth2, repo.MustBeNotEmpty, dlSourceEnabled)
|
|
|
|
m.Group("/branches", func() {
|
|
m.Get("/list", repo.GetBranchesList)
|
|
m.Get("", context.RepoRefByDefaultBranch() /* for the "commits" tab */, repo.Branches)
|
|
}, repo.MustBeNotEmpty)
|
|
|
|
m.Group("/media", func() {
|
|
m.Get("/blob/{sha}", repo.DownloadByIDOrLFS)
|
|
m.Get("/branch/*", context.RepoRefByType(git.RefTypeBranch), repo.SingleDownloadOrLFS)
|
|
m.Get("/tag/*", context.RepoRefByType(git.RefTypeTag), repo.SingleDownloadOrLFS)
|
|
m.Get("/commit/*", context.RepoRefByType(git.RefTypeCommit), repo.SingleDownloadOrLFS)
|
|
m.Get("/*", context.RepoRefByType(""), repo.SingleDownloadOrLFS) // "/*" route is deprecated, and kept for backward compatibility
|
|
}, webAuth.AllowBasic, webAuth.AllowOAuth2, repo.MustBeNotEmpty)
|
|
|
|
m.Group("/raw", func() {
|
|
m.Get("/blob/{sha}", repo.DownloadByID)
|
|
m.Get("/branch/*", context.RepoRefByType(git.RefTypeBranch), repo.SingleDownload)
|
|
m.Get("/tag/*", context.RepoRefByType(git.RefTypeTag), repo.SingleDownload)
|
|
m.Get("/commit/*", context.RepoRefByType(git.RefTypeCommit), repo.SingleDownload)
|
|
m.Get("/*", context.RepoRefByType(""), repo.SingleDownload) // "/*" route is deprecated, and kept for backward compatibility
|
|
}, webAuth.AllowBasic, webAuth.AllowOAuth2, repo.MustBeNotEmpty)
|
|
|
|
m.Group("/render", func() {
|
|
m.Get("/branch/*", context.RepoRefByType(git.RefTypeBranch), repo.RenderFile)
|
|
m.Get("/tag/*", context.RepoRefByType(git.RefTypeTag), repo.RenderFile)
|
|
m.Get("/commit/*", context.RepoRefByType(git.RefTypeCommit), repo.RenderFile)
|
|
m.Get("/blob/{sha}", repo.RenderFile)
|
|
}, repo.MustBeNotEmpty)
|
|
|
|
m.Group("/commits", func() {
|
|
m.Get("/branch/*", context.RepoRefByType(git.RefTypeBranch), repo.RefCommits)
|
|
m.Get("/tag/*", context.RepoRefByType(git.RefTypeTag), repo.RefCommits)
|
|
m.Get("/commit/*", context.RepoRefByType(git.RefTypeCommit), repo.RefCommits)
|
|
m.Get("/*", context.RepoRefByType(""), repo.RefCommits) // "/*" route is deprecated, and kept for backward compatibility
|
|
}, repo.MustBeNotEmpty)
|
|
|
|
m.Group("/blame", func() {
|
|
m.Get("/branch/*", context.RepoRefByType(git.RefTypeBranch), repo.RefBlame)
|
|
m.Get("/tag/*", context.RepoRefByType(git.RefTypeTag), repo.RefBlame)
|
|
m.Get("/commit/*", context.RepoRefByType(git.RefTypeCommit), repo.RefBlame)
|
|
}, repo.MustBeNotEmpty)
|
|
|
|
m.Get("/blob_excerpt/{sha}", repo.SetEditorconfigIfExists, repo.SetDiffViewStyle, repo.ExcerptBlob)
|
|
|
|
m.Group("", func() {
|
|
m.Get("/graph", repo.Graph)
|
|
m.Get("/commit/{sha:([a-f0-9]{7,64})$}", repo.SetEditorconfigIfExists, repo.SetDiffViewStyle, repo.SetWhitespaceBehavior, repo.Diff)
|
|
m.Get("/commit/{sha:([a-f0-9]{7,64})$}/load-branches-and-tags", repo.LoadBranchesAndTags)
|
|
|
|
// FIXME: this route `/cherry-pick/{sha}` doesn't seem useful or right, the new code always uses `/_cherrypick/` which could handle branch name correctly
|
|
m.Get("/cherry-pick/{sha:([a-f0-9]{7,64})$}", repo.SetEditorconfigIfExists, context.RepoRefByDefaultBranch(), repo.CherryPick)
|
|
}, repo.MustBeNotEmpty)
|
|
|
|
m.Get("/rss/branch/*", context.RepoRefByType(git.RefTypeBranch), webAuth.AllowBasic, feedEnabled, feed.RenderBranchFeedRSS)
|
|
m.Get("/atom/branch/*", context.RepoRefByType(git.RefTypeBranch), webAuth.AllowBasic, feedEnabled, feed.RenderBranchFeedAtom)
|
|
|
|
m.Group("/src", func() {
|
|
m.Get("", func(ctx *context.Context) { ctx.Redirect(ctx.Repo.RepoLink) }) // there is no "{owner}/{repo}/src" page, so redirect to "{owner}/{repo}" to avoid 404
|
|
m.Get("/branch/*", context.RepoRefByType(git.RefTypeBranch), repo.Home)
|
|
m.Get("/tag/*", context.RepoRefByType(git.RefTypeTag), repo.Home)
|
|
m.Get("/commit/*", context.RepoRefByType(git.RefTypeCommit), repo.Home)
|
|
m.Get("/*", context.RepoRefByType(""), repo.Home) // "/*" route is deprecated, and kept for backward compatibility
|
|
}, repo.SetEditorconfigIfExists)
|
|
m.Get("/tree/*", repo.RedirectRepoTreeToSrc) // redirect "/owner/repo/tree/*" requests to "/owner/repo/src/*"
|
|
m.Get("/blob/*", repo.RedirectRepoBlobToCommit) // redirect "/owner/repo/blob/*" requests to "/owner/repo/src/commit/*"
|
|
|
|
m.Get("/forks", repo.Forks)
|
|
m.Get("/commit/{sha:([a-f0-9]{7,64})}.{ext:patch|diff}", repo.MustBeNotEmpty, repo.RawDiff)
|
|
m.Post("/lastcommit/*", context.RepoRefByType(git.RefTypeCommit), repo.LastCommit)
|
|
}, optSignIn, context.RepoAssignment, reqUnitCodeReader)
|
|
// end "/{username}/{reponame}": repo code
|
|
|
|
m.Group("/{username}/{reponame}", func() {
|
|
m.Get("/stars", starsEnabled, repo.Stars)
|
|
m.Get("/watchers", repo.Watchers)
|
|
m.Get("/search", reqUnitCodeReader, repo.Search)
|
|
m.Post("/action/{action:star|unstar}", reqSignIn, starsEnabled, repo.ActionStar)
|
|
m.Post("/action/{action:watch|unwatch}", reqSignIn, repo.ActionWatch)
|
|
m.Post("/action/{action:accept_transfer|reject_transfer}", reqSignIn, repo.ActionTransfer)
|
|
}, optSignIn, context.RepoAssignment)
|
|
|
|
// git lfs uses its own jwt key, and it handles the token & auth by itself, it conflicts with the general "OAuth2" auth method
|
|
// pattern: "/{username}/{reponame}/{lfs-paths}": git-lfs support, see also addOwnerRepoGitHTTPRouters
|
|
common.AddOwnerRepoGitLFSRoutes(m, lfsServerEnabled, webAuth.AllowBasic, repo.CorsHandler(), optSignInFromAnyOrigin)
|
|
|
|
// Some users want to use "web-based git client" to access Gitea's repositories,
|
|
// so the CORS handler and OPTIONS method are used.
|
|
// pattern: "/{username}/{reponame}/{git-paths}": git http support
|
|
addOwnerRepoGitHTTPRouters(m, repo.HTTPGitEnabledHandler, webAuth.AllowBasic, webAuth.AllowOAuth2, repo.CorsHandler(), optSignInFromAnyOrigin, context.UserAssignmentWeb())
|
|
|
|
m.Group("/notifications", func() {
|
|
m.Get("", user.Notifications)
|
|
m.Get("/subscriptions", user.NotificationSubscriptions)
|
|
m.Get("/watching", user.NotificationWatching)
|
|
m.Post("/status", user.NotificationStatusPost)
|
|
m.Post("/purge", user.NotificationPurgePost)
|
|
m.Get("/new", user.NewAvailable)
|
|
}, reqSignIn)
|
|
|
|
if setting.API.EnableSwagger {
|
|
m.Get("/swagger.v1.json", SwaggerV1Json)
|
|
}
|
|
|
|
if !setting.IsProd {
|
|
m.Group("/devtest", func() {
|
|
m.Any("", devtest.List)
|
|
m.Any("/fetch-action-test", devtest.FetchActionTest)
|
|
m.Any("/mail-preview", devtest.MailPreview)
|
|
m.Any("/mail-preview/*", devtest.MailPreviewRender)
|
|
m.Any("/{sub}", devtest.TmplCommon)
|
|
m.Get("/repo-action-view/runs/{run}/jobs/{job}", devtest.MockActionsView)
|
|
m.Post("/actions-mock/runs/{run}/jobs/{job}", web.Bind(actions.ViewRequest{}), devtest.MockActionsRunsJobs)
|
|
})
|
|
}
|
|
|
|
m.NotFound(func(w http.ResponseWriter, req *http.Request) {
|
|
ctx := context.GetWebContext(req.Context())
|
|
defer routing.RecordFuncInfo(ctx, routing.GetFuncInfo(ctx.NotFound, "WebNotFound"))()
|
|
ctx.NotFound(nil)
|
|
})
|
|
}
|