初始提交: Gitea 项目代码
This commit is contained in:
@@ -0,0 +1,281 @@
|
||||
// Copyright 2014 The Gogs Authors. All rights reserved.
|
||||
// Copyright 2019 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package admin
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"runtime"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
activities_model "gitea.dev/models/activities"
|
||||
"gitea.dev/models/db"
|
||||
"gitea.dev/modules/base"
|
||||
"gitea.dev/modules/cache"
|
||||
"gitea.dev/modules/graceful"
|
||||
"gitea.dev/modules/httplib"
|
||||
"gitea.dev/modules/json"
|
||||
"gitea.dev/modules/log"
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/modules/templates"
|
||||
"gitea.dev/modules/updatechecker"
|
||||
"gitea.dev/modules/web"
|
||||
"gitea.dev/services/context"
|
||||
"gitea.dev/services/cron"
|
||||
"gitea.dev/services/forms"
|
||||
release_service "gitea.dev/services/release"
|
||||
repo_service "gitea.dev/services/repository"
|
||||
)
|
||||
|
||||
const (
|
||||
tplDashboard templates.TplName = "admin/dashboard"
|
||||
tplSystemStatus templates.TplName = "admin/system_status"
|
||||
tplSelfCheck templates.TplName = "admin/self_check"
|
||||
tplCron templates.TplName = "admin/cron"
|
||||
tplQueue templates.TplName = "admin/queue"
|
||||
tplPerfTrace templates.TplName = "admin/perftrace"
|
||||
tplStacktrace templates.TplName = "admin/stacktrace"
|
||||
tplQueueManage templates.TplName = "admin/queue_manage"
|
||||
tplStats templates.TplName = "admin/stats"
|
||||
)
|
||||
|
||||
var sysStatus struct {
|
||||
StartTime string
|
||||
NumGoroutine int
|
||||
|
||||
// General statistics.
|
||||
MemAllocated string // bytes allocated and still in use
|
||||
MemTotal string // bytes allocated (even if freed)
|
||||
MemSys string // bytes obtained from system (sum of XxxSys below)
|
||||
Lookups uint64 // number of pointer lookups
|
||||
MemMallocs uint64 // number of mallocs
|
||||
MemFrees uint64 // number of frees
|
||||
|
||||
// Main allocation heap statistics.
|
||||
HeapAlloc string // bytes allocated and still in use
|
||||
HeapSys string // bytes obtained from system
|
||||
HeapIdle string // bytes in idle spans
|
||||
HeapInuse string // bytes in non-idle span
|
||||
HeapReleased string // bytes released to the OS
|
||||
HeapObjects uint64 // total number of allocated objects
|
||||
|
||||
// Low-level fixed-size structure allocator statistics.
|
||||
// Inuse is bytes used now.
|
||||
// Sys is bytes obtained from system.
|
||||
StackInuse string // bootstrap stacks
|
||||
StackSys string
|
||||
MSpanInuse string // mspan structures
|
||||
MSpanSys string
|
||||
MCacheInuse string // mcache structures
|
||||
MCacheSys string
|
||||
BuckHashSys string // profiling bucket hash table
|
||||
GCSys string // GC metadata
|
||||
OtherSys string // other system allocations
|
||||
|
||||
// Garbage collector statistics.
|
||||
NextGC string // next run in HeapAlloc time (bytes)
|
||||
LastGCTime string // last run time
|
||||
PauseTotalNs string
|
||||
PauseNs string // circular buffer of recent GC pause times, most recent at [(NumGC+255)%256]
|
||||
NumGC uint32
|
||||
}
|
||||
|
||||
func updateSystemStatus() {
|
||||
sysStatus.StartTime = setting.AppStartTime.Format(time.RFC3339)
|
||||
|
||||
m := new(runtime.MemStats)
|
||||
runtime.ReadMemStats(m)
|
||||
sysStatus.NumGoroutine = runtime.NumGoroutine()
|
||||
|
||||
sysStatus.MemAllocated = base.FileSize(int64(m.Alloc))
|
||||
sysStatus.MemTotal = base.FileSize(int64(m.TotalAlloc))
|
||||
sysStatus.MemSys = base.FileSize(int64(m.Sys))
|
||||
sysStatus.Lookups = m.Lookups
|
||||
sysStatus.MemMallocs = m.Mallocs
|
||||
sysStatus.MemFrees = m.Frees
|
||||
|
||||
sysStatus.HeapAlloc = base.FileSize(int64(m.HeapAlloc))
|
||||
sysStatus.HeapSys = base.FileSize(int64(m.HeapSys))
|
||||
sysStatus.HeapIdle = base.FileSize(int64(m.HeapIdle))
|
||||
sysStatus.HeapInuse = base.FileSize(int64(m.HeapInuse))
|
||||
sysStatus.HeapReleased = base.FileSize(int64(m.HeapReleased))
|
||||
sysStatus.HeapObjects = m.HeapObjects
|
||||
|
||||
sysStatus.StackInuse = base.FileSize(int64(m.StackInuse))
|
||||
sysStatus.StackSys = base.FileSize(int64(m.StackSys))
|
||||
sysStatus.MSpanInuse = base.FileSize(int64(m.MSpanInuse))
|
||||
sysStatus.MSpanSys = base.FileSize(int64(m.MSpanSys))
|
||||
sysStatus.MCacheInuse = base.FileSize(int64(m.MCacheInuse))
|
||||
sysStatus.MCacheSys = base.FileSize(int64(m.MCacheSys))
|
||||
sysStatus.BuckHashSys = base.FileSize(int64(m.BuckHashSys))
|
||||
sysStatus.GCSys = base.FileSize(int64(m.GCSys))
|
||||
sysStatus.OtherSys = base.FileSize(int64(m.OtherSys))
|
||||
|
||||
sysStatus.NextGC = base.FileSize(int64(m.NextGC))
|
||||
sysStatus.LastGCTime = time.Unix(0, int64(m.LastGC)).Format(time.RFC3339)
|
||||
sysStatus.PauseTotalNs = fmt.Sprintf("%.1fs", float64(m.PauseTotalNs)/1000/1000/1000)
|
||||
sysStatus.PauseNs = fmt.Sprintf("%.3fs", float64(m.PauseNs[(m.NumGC+255)%256])/1000/1000/1000)
|
||||
sysStatus.NumGC = m.NumGC
|
||||
}
|
||||
|
||||
func prepareStartupProblemsAlert(ctx *context.Context) {
|
||||
if len(setting.StartupProblems) > 0 {
|
||||
content := setting.StartupProblems[0]
|
||||
if len(setting.StartupProblems) > 1 {
|
||||
content += fmt.Sprintf(" (and %d more)", len(setting.StartupProblems)-1)
|
||||
}
|
||||
ctx.Flash.Error(content, true)
|
||||
}
|
||||
}
|
||||
|
||||
// Dashboard show admin panel dashboard
|
||||
func Dashboard(ctx *context.Context) {
|
||||
ctx.Data["Title"] = ctx.Tr("admin.dashboard")
|
||||
ctx.Data["PageIsAdminDashboard"] = true
|
||||
ctx.Data["NeedUpdate"] = updatechecker.GetNeedUpdate(ctx)
|
||||
ctx.Data["RemoteVersion"] = updatechecker.GetRemoteVersion(ctx)
|
||||
updateSystemStatus()
|
||||
ctx.Data["SysStatus"] = sysStatus
|
||||
ctx.Data["SSH"] = setting.SSH
|
||||
prepareStartupProblemsAlert(ctx)
|
||||
ctx.HTML(http.StatusOK, tplDashboard)
|
||||
}
|
||||
|
||||
func SystemStatus(ctx *context.Context) {
|
||||
updateSystemStatus()
|
||||
ctx.Data["SysStatus"] = sysStatus
|
||||
ctx.HTML(http.StatusOK, tplSystemStatus)
|
||||
}
|
||||
|
||||
// DashboardPost run an admin operation
|
||||
func DashboardPost(ctx *context.Context) {
|
||||
form := web.GetForm(ctx).(*forms.AdminDashboardForm)
|
||||
ctx.Data["Title"] = ctx.Tr("admin.dashboard")
|
||||
ctx.Data["PageIsAdminDashboard"] = true
|
||||
updateSystemStatus()
|
||||
ctx.Data["SysStatus"] = sysStatus
|
||||
|
||||
// Run operation.
|
||||
if form.Op != "" {
|
||||
switch form.Op {
|
||||
case "sync_repo_branches":
|
||||
go func() {
|
||||
if err := repo_service.AddAllRepoBranchesToSyncQueue(graceful.GetManager().ShutdownContext()); err != nil {
|
||||
log.Error("AddAllRepoBranchesToSyncQueue: %v: %v", ctx.Doer.ID, err)
|
||||
}
|
||||
}()
|
||||
ctx.Flash.Success(ctx.Tr("admin.dashboard.sync_branch.started"))
|
||||
case "sync_repo_tags":
|
||||
go func() {
|
||||
if err := release_service.AddAllRepoTagsToSyncQueue(graceful.GetManager().ShutdownContext()); err != nil {
|
||||
log.Error("AddAllRepoTagsToSyncQueue: %v: %v", ctx.Doer.ID, err)
|
||||
}
|
||||
}()
|
||||
ctx.Flash.Success(ctx.Tr("admin.dashboard.sync_tag.started"))
|
||||
default:
|
||||
task := cron.GetTask(form.Op)
|
||||
if task != nil {
|
||||
go task.RunWithUser(ctx.Doer, nil)
|
||||
ctx.Flash.Success(ctx.Tr("admin.dashboard.task.started", ctx.Tr("admin.dashboard."+form.Op)))
|
||||
} else {
|
||||
ctx.Flash.Error(ctx.Tr("admin.dashboard.task.unknown", form.Op))
|
||||
}
|
||||
}
|
||||
}
|
||||
if form.From == "monitor" {
|
||||
ctx.Redirect(setting.AppSubURL + "/-/admin/monitor/cron")
|
||||
} else {
|
||||
ctx.Redirect(setting.AppSubURL + "/-/admin")
|
||||
}
|
||||
}
|
||||
|
||||
func SelfCheck(ctx *context.Context) {
|
||||
ctx.Data["PageIsAdminSelfCheck"] = true
|
||||
|
||||
ctx.Data["StartupProblems"] = setting.StartupProblems
|
||||
if len(setting.StartupProblems) == 0 && !setting.IsProd {
|
||||
if time.Now().Unix()%2 == 0 {
|
||||
ctx.Data["StartupProblems"] = []string{"This is a test warning message in dev mode"}
|
||||
}
|
||||
}
|
||||
|
||||
r, err := db.CheckCollationsDefaultEngine()
|
||||
if err != nil {
|
||||
ctx.Flash.Error(fmt.Sprintf("CheckCollationsDefaultEngine: %v", err), true)
|
||||
}
|
||||
|
||||
if r != nil {
|
||||
ctx.Data["DatabaseType"] = setting.Database.Type
|
||||
ctx.Data["DatabaseCheckResult"] = r
|
||||
hasProblem := false
|
||||
if !r.CollationEquals(r.DatabaseCollation, r.ExpectedCollation) {
|
||||
ctx.Data["DatabaseCheckCollationMismatch"] = true
|
||||
hasProblem = true
|
||||
}
|
||||
if !r.IsCollationCaseSensitive(r.DatabaseCollation) {
|
||||
ctx.Data["DatabaseCheckCollationCaseInsensitive"] = true
|
||||
hasProblem = true
|
||||
}
|
||||
ctx.Data["DatabaseCheckInconsistentCollationColumns"] = r.InconsistentCollationColumns
|
||||
hasProblem = hasProblem || len(r.InconsistentCollationColumns) > 0
|
||||
|
||||
ctx.Data["DatabaseCheckHasProblems"] = hasProblem
|
||||
}
|
||||
|
||||
elapsed, err := cache.Test()
|
||||
if err != nil {
|
||||
ctx.Data["CacheError"] = err
|
||||
} else if elapsed > cache.SlowCacheThreshold {
|
||||
ctx.Data["CacheSlow"] = fmt.Sprint(elapsed)
|
||||
}
|
||||
|
||||
ctx.HTML(http.StatusOK, tplSelfCheck)
|
||||
}
|
||||
|
||||
func SelfCheckPost(ctx *context.Context) {
|
||||
var problems []string
|
||||
frontendAppURL := ctx.FormString("location_origin") + setting.AppSubURL + "/"
|
||||
ctxAppURL := httplib.GuessCurrentAppURL(ctx)
|
||||
if !strings.HasPrefix(ctxAppURL, frontendAppURL) {
|
||||
problems = append(problems, ctx.Locale.TrString("admin.self_check.location_origin_mismatch", frontendAppURL, ctxAppURL))
|
||||
}
|
||||
ctx.JSON(http.StatusOK, map[string]any{"problems": problems})
|
||||
}
|
||||
|
||||
func CronTasks(ctx *context.Context) {
|
||||
ctx.Data["Title"] = ctx.Tr("admin.monitor.cron")
|
||||
ctx.Data["PageIsAdminMonitorCron"] = true
|
||||
ctx.Data["Entries"] = cron.ListTasks()
|
||||
ctx.HTML(http.StatusOK, tplCron)
|
||||
}
|
||||
|
||||
func MonitorStats(ctx *context.Context) {
|
||||
ctx.Data["Title"] = ctx.Tr("admin.monitor.stats")
|
||||
ctx.Data["PageIsAdminMonitorStats"] = true
|
||||
bs, err := json.Marshal(activities_model.GetStatistic(ctx).Counter)
|
||||
if err != nil {
|
||||
ctx.ServerError("MonitorStats", err)
|
||||
return
|
||||
}
|
||||
statsCounter := map[string]any{}
|
||||
err = json.Unmarshal(bs, &statsCounter)
|
||||
if err != nil {
|
||||
ctx.ServerError("MonitorStats", err)
|
||||
return
|
||||
}
|
||||
statsKeys := make([]string, 0, len(statsCounter))
|
||||
for k := range statsCounter {
|
||||
if statsCounter[k] == nil {
|
||||
continue
|
||||
}
|
||||
statsKeys = append(statsKeys, k)
|
||||
}
|
||||
sort.Strings(statsKeys)
|
||||
ctx.Data["StatsKeys"] = statsKeys
|
||||
ctx.Data["StatsCounter"] = statsCounter
|
||||
ctx.HTML(http.StatusOK, tplStats)
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
// Copyright 2019 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package admin
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"gitea.dev/modules/json"
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/modules/test"
|
||||
"gitea.dev/services/contexttest"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestSelfCheckPost(t *testing.T) {
|
||||
defer test.MockVariableValue(&setting.PublicURLDetection)()
|
||||
defer test.MockVariableValue(&setting.AppURL, "http://config/sub/")()
|
||||
defer test.MockVariableValue(&setting.AppSubURL, "/sub")()
|
||||
|
||||
data := struct {
|
||||
Problems []string `json:"problems"`
|
||||
}{}
|
||||
|
||||
setting.PublicURLDetection = setting.PublicURLLegacy
|
||||
ctx, resp := contexttest.MockContext(t, "GET http://host/sub/admin/self_check?location_origin=http://frontend")
|
||||
SelfCheckPost(ctx)
|
||||
assert.Equal(t, http.StatusOK, resp.Code)
|
||||
require.NoError(t, json.Unmarshal(resp.Body.Bytes(), &data))
|
||||
assert.Equal(t, []string{
|
||||
ctx.Locale.TrString("admin.self_check.location_origin_mismatch", "http://frontend/sub/", "http://config/sub/"),
|
||||
}, data.Problems)
|
||||
|
||||
setting.PublicURLDetection = setting.PublicURLAuto
|
||||
ctx, resp = contexttest.MockContext(t, "GET http://host/sub/admin/self_check?location_origin=http://frontend")
|
||||
SelfCheckPost(ctx)
|
||||
assert.Equal(t, http.StatusOK, resp.Code)
|
||||
require.NoError(t, json.Unmarshal(resp.Body.Bytes(), &data))
|
||||
assert.Equal(t, []string{
|
||||
ctx.Locale.TrString("admin.self_check.location_origin_mismatch", "http://frontend/sub/", "http://host/sub/"),
|
||||
}, data.Problems)
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
// Copyright 2022 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package admin
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"gitea.dev/models/auth"
|
||||
"gitea.dev/models/db"
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/modules/templates"
|
||||
user_setting "gitea.dev/routers/web/user/setting"
|
||||
"gitea.dev/services/context"
|
||||
)
|
||||
|
||||
var (
|
||||
tplSettingsApplications templates.TplName = "admin/applications/list"
|
||||
tplSettingsOauth2ApplicationEdit templates.TplName = "admin/applications/oauth2_edit"
|
||||
)
|
||||
|
||||
func newOAuth2CommonHandlers() *user_setting.OAuth2CommonHandlers {
|
||||
return &user_setting.OAuth2CommonHandlers{
|
||||
OwnerID: 0,
|
||||
BasePathList: setting.AppSubURL + "/-/admin/applications",
|
||||
BasePathEditPrefix: setting.AppSubURL + "/-/admin/applications/oauth2",
|
||||
TplAppEdit: tplSettingsOauth2ApplicationEdit,
|
||||
}
|
||||
}
|
||||
|
||||
// Applications render org applications page (for org, at the moment, there are only OAuth2 applications)
|
||||
func Applications(ctx *context.Context) {
|
||||
ctx.Data["Title"] = ctx.Tr("settings.applications")
|
||||
ctx.Data["PageIsAdminApplications"] = true
|
||||
|
||||
apps, err := db.Find[auth.OAuth2Application](ctx, auth.FindOAuth2ApplicationsOptions{
|
||||
IsGlobal: true,
|
||||
})
|
||||
if err != nil {
|
||||
ctx.ServerError("GetOAuth2ApplicationsByUserID", err)
|
||||
return
|
||||
}
|
||||
ctx.Data["Applications"] = apps
|
||||
ctx.Data["BuiltinApplications"] = auth.BuiltinApplications()
|
||||
ctx.HTML(http.StatusOK, tplSettingsApplications)
|
||||
}
|
||||
|
||||
// ApplicationsPost response for adding an oauth2 application
|
||||
func ApplicationsPost(ctx *context.Context) {
|
||||
ctx.Data["Title"] = ctx.Tr("settings.applications")
|
||||
ctx.Data["PageIsAdminApplications"] = true
|
||||
|
||||
oa := newOAuth2CommonHandlers()
|
||||
oa.AddApp(ctx)
|
||||
}
|
||||
|
||||
// EditApplication displays the given application
|
||||
func EditApplication(ctx *context.Context) {
|
||||
ctx.Data["PageIsAdminApplications"] = true
|
||||
|
||||
oa := newOAuth2CommonHandlers()
|
||||
oa.EditShow(ctx)
|
||||
}
|
||||
|
||||
// EditApplicationPost response for editing oauth2 application
|
||||
func EditApplicationPost(ctx *context.Context) {
|
||||
ctx.Data["Title"] = ctx.Tr("settings.applications")
|
||||
ctx.Data["PageIsAdminApplications"] = true
|
||||
|
||||
oa := newOAuth2CommonHandlers()
|
||||
oa.EditSave(ctx)
|
||||
}
|
||||
|
||||
// ApplicationsRegenerateSecret handles the post request for regenerating the secret
|
||||
func ApplicationsRegenerateSecret(ctx *context.Context) {
|
||||
ctx.Data["Title"] = ctx.Tr("settings_title")
|
||||
ctx.Data["PageIsAdminApplications"] = true
|
||||
|
||||
oa := newOAuth2CommonHandlers()
|
||||
oa.RegenerateSecret(ctx)
|
||||
}
|
||||
|
||||
// DeleteApplication deletes the given oauth2 application
|
||||
func DeleteApplication(ctx *context.Context) {
|
||||
oa := newOAuth2CommonHandlers()
|
||||
oa.DeleteApp(ctx)
|
||||
}
|
||||
|
||||
// TODO: revokes the grant with the given id
|
||||
@@ -0,0 +1,467 @@
|
||||
// Copyright 2014 The Gogs Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package admin
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"gitea.dev/models/auth"
|
||||
"gitea.dev/models/db"
|
||||
"gitea.dev/modules/auth/pam"
|
||||
"gitea.dev/modules/log"
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/modules/templates"
|
||||
"gitea.dev/modules/util"
|
||||
"gitea.dev/modules/web"
|
||||
auth_service "gitea.dev/services/auth"
|
||||
"gitea.dev/services/auth/source/ldap"
|
||||
"gitea.dev/services/auth/source/oauth2"
|
||||
pam_service "gitea.dev/services/auth/source/pam"
|
||||
"gitea.dev/services/auth/source/smtp"
|
||||
"gitea.dev/services/auth/source/sspi"
|
||||
"gitea.dev/services/context"
|
||||
"gitea.dev/services/forms"
|
||||
)
|
||||
|
||||
const (
|
||||
tplAuths templates.TplName = "admin/auth/list"
|
||||
tplAuthNew templates.TplName = "admin/auth/new"
|
||||
tplAuthEdit templates.TplName = "admin/auth/edit"
|
||||
)
|
||||
|
||||
var (
|
||||
separatorAntiPattern = regexp.MustCompile(`[^\w-\.]`)
|
||||
langCodePattern = regexp.MustCompile(`^[a-z]{2}-[A-Z]{2}$`)
|
||||
)
|
||||
|
||||
// Authentications show authentication config page
|
||||
func Authentications(ctx *context.Context) {
|
||||
ctx.Data["Title"] = ctx.Tr("admin.authentication")
|
||||
ctx.Data["PageIsAdminAuthentications"] = true
|
||||
|
||||
var err error
|
||||
ctx.Data["Sources"], ctx.Data["Total"], err = db.FindAndCount[auth.Source](ctx, auth.FindSourcesOptions{})
|
||||
if err != nil {
|
||||
ctx.ServerError("auth.Sources", err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.HTML(http.StatusOK, tplAuths)
|
||||
}
|
||||
|
||||
type dropdownItem struct {
|
||||
Name string
|
||||
Type any
|
||||
}
|
||||
|
||||
var (
|
||||
authSources = func() []dropdownItem {
|
||||
items := []dropdownItem{
|
||||
{auth.LDAP.String(), auth.LDAP},
|
||||
{auth.DLDAP.String(), auth.DLDAP},
|
||||
{auth.SMTP.String(), auth.SMTP},
|
||||
{auth.OAuth2.String(), auth.OAuth2},
|
||||
{auth.SSPI.String(), auth.SSPI},
|
||||
}
|
||||
if pam.Supported {
|
||||
items = append(items, dropdownItem{auth.Names[auth.PAM], auth.PAM})
|
||||
}
|
||||
return items
|
||||
}()
|
||||
|
||||
securityProtocols = []dropdownItem{
|
||||
{ldap.SecurityProtocolNames[ldap.SecurityProtocolUnencrypted], ldap.SecurityProtocolUnencrypted},
|
||||
{ldap.SecurityProtocolNames[ldap.SecurityProtocolLDAPS], ldap.SecurityProtocolLDAPS},
|
||||
{ldap.SecurityProtocolNames[ldap.SecurityProtocolStartTLS], ldap.SecurityProtocolStartTLS},
|
||||
}
|
||||
)
|
||||
|
||||
// NewAuthSource render adding a new auth source page
|
||||
func NewAuthSource(ctx *context.Context) {
|
||||
ctx.Data["Title"] = ctx.Tr("admin.auths.new")
|
||||
ctx.Data["PageIsAdminAuthentications"] = true
|
||||
|
||||
ctx.Data["type"] = auth.LDAP.Int()
|
||||
ctx.Data["CurrentTypeName"] = auth.Names[auth.LDAP]
|
||||
ctx.Data["CurrentSecurityProtocol"] = ldap.SecurityProtocolNames[ldap.SecurityProtocolUnencrypted]
|
||||
ctx.Data["smtp_auth"] = "PLAIN"
|
||||
ctx.Data["is_active"] = true
|
||||
ctx.Data["is_sync_enabled"] = true
|
||||
ctx.Data["AuthSources"] = authSources
|
||||
ctx.Data["SecurityProtocols"] = securityProtocols
|
||||
ctx.Data["SMTPAuths"] = smtp.Authenticators
|
||||
oauth2providers := oauth2.GetSupportedOAuth2Providers(ctx)
|
||||
ctx.Data["OAuth2Providers"] = oauth2providers
|
||||
|
||||
ctx.Data["SSPIAutoCreateUsers"] = true
|
||||
ctx.Data["SSPIAutoActivateUsers"] = true
|
||||
ctx.Data["SSPIStripDomainNames"] = true
|
||||
ctx.Data["SSPISeparatorReplacement"] = "_"
|
||||
ctx.Data["SSPIDefaultLanguage"] = ""
|
||||
|
||||
// only the first as default
|
||||
if len(oauth2providers) > 0 {
|
||||
ctx.Data["oauth2_provider"] = oauth2providers[0].Name()
|
||||
}
|
||||
|
||||
ctx.HTML(http.StatusOK, tplAuthNew)
|
||||
}
|
||||
|
||||
func parseLDAPConfig(form forms.AuthenticationForm) *ldap.Source {
|
||||
var pageSize uint32
|
||||
if form.UsePagedSearch {
|
||||
pageSize = uint32(form.SearchPageSize)
|
||||
}
|
||||
return &ldap.Source{
|
||||
Name: form.Name,
|
||||
Host: form.Host,
|
||||
Port: form.Port,
|
||||
SecurityProtocol: ldap.SecurityProtocol(form.SecurityProtocol),
|
||||
SkipVerify: form.SkipVerify,
|
||||
BindDN: form.BindDN,
|
||||
UserDN: form.UserDN,
|
||||
BindPassword: form.BindPassword,
|
||||
UserBase: form.UserBase,
|
||||
AttributeUsername: form.AttributeUsername,
|
||||
AttributeName: form.AttributeName,
|
||||
AttributeSurname: form.AttributeSurname,
|
||||
AttributeMail: form.AttributeMail,
|
||||
AttributesInBind: form.AttributesInBind,
|
||||
AttributeSSHPublicKey: form.AttributeSSHPublicKey,
|
||||
AttributeAvatar: form.AttributeAvatar,
|
||||
SSHKeysAreVerified: form.SSHKeysAreVerified,
|
||||
SearchPageSize: pageSize,
|
||||
Filter: form.Filter,
|
||||
GroupsEnabled: form.GroupsEnabled,
|
||||
GroupDN: form.GroupDN,
|
||||
GroupFilter: form.GroupFilter,
|
||||
GroupMemberUID: form.GroupMemberUID,
|
||||
GroupTeamMap: form.GroupTeamMap,
|
||||
GroupTeamMapRemoval: form.GroupTeamMapRemoval,
|
||||
UserUID: form.UserUID,
|
||||
AdminFilter: form.AdminFilter,
|
||||
RestrictedFilter: form.RestrictedFilter,
|
||||
AllowDeactivateAll: form.AllowDeactivateAll,
|
||||
Enabled: true,
|
||||
}
|
||||
}
|
||||
|
||||
func parseSMTPConfig(form forms.AuthenticationForm) *smtp.Source {
|
||||
return &smtp.Source{
|
||||
Auth: form.SMTPAuth,
|
||||
Host: form.SMTPHost,
|
||||
Port: form.SMTPPort,
|
||||
AllowedDomains: form.AllowedDomains,
|
||||
ForceSMTPS: form.ForceSMTPS,
|
||||
SkipVerify: form.SkipVerify,
|
||||
HeloHostname: form.HeloHostname,
|
||||
DisableHelo: form.DisableHelo,
|
||||
}
|
||||
}
|
||||
|
||||
func parseOAuth2Config(form forms.AuthenticationForm) *oauth2.Source {
|
||||
var customURLMapping *oauth2.CustomURLMapping
|
||||
if form.Oauth2UseCustomURL {
|
||||
customURLMapping = &oauth2.CustomURLMapping{
|
||||
TokenURL: form.Oauth2TokenURL,
|
||||
AuthURL: form.Oauth2AuthURL,
|
||||
ProfileURL: form.Oauth2ProfileURL,
|
||||
EmailURL: form.Oauth2EmailURL,
|
||||
Tenant: form.Oauth2Tenant,
|
||||
}
|
||||
} else {
|
||||
customURLMapping = nil
|
||||
}
|
||||
var scopes []string
|
||||
for s := range strings.SplitSeq(form.Oauth2Scopes, ",") {
|
||||
s = strings.TrimSpace(s)
|
||||
if s != "" {
|
||||
scopes = append(scopes, s)
|
||||
}
|
||||
}
|
||||
|
||||
return &oauth2.Source{
|
||||
Provider: form.Oauth2Provider,
|
||||
ClientID: form.Oauth2Key,
|
||||
ClientSecret: form.Oauth2Secret,
|
||||
OpenIDConnectAutoDiscoveryURL: form.OpenIDConnectAutoDiscoveryURL,
|
||||
CustomURLMapping: customURLMapping,
|
||||
IconURL: form.Oauth2IconURL,
|
||||
Scopes: scopes,
|
||||
RequiredClaimName: form.Oauth2RequiredClaimName,
|
||||
RequiredClaimValue: form.Oauth2RequiredClaimValue,
|
||||
GroupClaimName: form.Oauth2GroupClaimName,
|
||||
RestrictedGroup: form.Oauth2RestrictedGroup,
|
||||
AdminGroup: form.Oauth2AdminGroup,
|
||||
GroupTeamMap: form.Oauth2GroupTeamMap,
|
||||
GroupTeamMapRemoval: form.Oauth2GroupTeamMapRemoval,
|
||||
|
||||
SSHPublicKeyClaimName: form.Oauth2SSHPublicKeyClaimName,
|
||||
FullNameClaimName: form.Oauth2FullNameClaimName,
|
||||
ExternalIDClaim: form.OpenIDConnectExternalIDClaim,
|
||||
}
|
||||
}
|
||||
|
||||
func parseSSPIConfig(ctx *context.Context, form forms.AuthenticationForm) (*sspi.Source, error) {
|
||||
if util.IsEmptyString(form.SSPISeparatorReplacement) {
|
||||
ctx.Data["Err_SSPISeparatorReplacement"] = true
|
||||
return nil, errors.New(ctx.Locale.TrString("form.SSPISeparatorReplacement") + ctx.Locale.TrString("form.require_error"))
|
||||
}
|
||||
if separatorAntiPattern.MatchString(form.SSPISeparatorReplacement) {
|
||||
ctx.Data["Err_SSPISeparatorReplacement"] = true
|
||||
return nil, errors.New(ctx.Locale.TrString("form.SSPISeparatorReplacement") + ctx.Locale.TrString("form.alpha_dash_dot_error"))
|
||||
}
|
||||
|
||||
if form.SSPIDefaultLanguage != "" && !langCodePattern.MatchString(form.SSPIDefaultLanguage) {
|
||||
ctx.Data["Err_SSPIDefaultLanguage"] = true
|
||||
return nil, errors.New(ctx.Locale.TrString("form.lang_select_error"))
|
||||
}
|
||||
|
||||
return &sspi.Source{
|
||||
AutoCreateUsers: form.SSPIAutoCreateUsers,
|
||||
AutoActivateUsers: form.SSPIAutoActivateUsers,
|
||||
StripDomainNames: form.SSPIStripDomainNames,
|
||||
SeparatorReplacement: form.SSPISeparatorReplacement,
|
||||
DefaultLanguage: form.SSPIDefaultLanguage,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// NewAuthSourcePost response for adding an auth source
|
||||
func NewAuthSourcePost(ctx *context.Context) {
|
||||
form := *web.GetForm(ctx).(*forms.AuthenticationForm)
|
||||
ctx.Data["Title"] = ctx.Tr("admin.auths.new")
|
||||
ctx.Data["PageIsAdminAuthentications"] = true
|
||||
|
||||
ctx.Data["CurrentTypeName"] = auth.Type(form.Type).String()
|
||||
ctx.Data["CurrentSecurityProtocol"] = ldap.SecurityProtocolNames[ldap.SecurityProtocol(form.SecurityProtocol)]
|
||||
ctx.Data["AuthSources"] = authSources
|
||||
ctx.Data["SecurityProtocols"] = securityProtocols
|
||||
ctx.Data["SMTPAuths"] = smtp.Authenticators
|
||||
oauth2providers := oauth2.GetSupportedOAuth2Providers(ctx)
|
||||
ctx.Data["OAuth2Providers"] = oauth2providers
|
||||
|
||||
ctx.Data["SSPIAutoCreateUsers"] = true
|
||||
ctx.Data["SSPIAutoActivateUsers"] = true
|
||||
ctx.Data["SSPIStripDomainNames"] = true
|
||||
ctx.Data["SSPISeparatorReplacement"] = "_"
|
||||
ctx.Data["SSPIDefaultLanguage"] = ""
|
||||
|
||||
hasTLS := false
|
||||
var config auth.Config
|
||||
switch auth.Type(form.Type) {
|
||||
case auth.LDAP, auth.DLDAP:
|
||||
config = parseLDAPConfig(form)
|
||||
hasTLS = ldap.SecurityProtocol(form.SecurityProtocol) > ldap.SecurityProtocolUnencrypted
|
||||
case auth.SMTP:
|
||||
config = parseSMTPConfig(form)
|
||||
hasTLS = true
|
||||
case auth.PAM:
|
||||
config = &pam_service.Source{
|
||||
ServiceName: form.PAMServiceName,
|
||||
EmailDomain: form.PAMEmailDomain,
|
||||
}
|
||||
case auth.OAuth2:
|
||||
config = parseOAuth2Config(form)
|
||||
oauth2Config := config.(*oauth2.Source)
|
||||
if oauth2Config.Provider == "openidConnect" {
|
||||
discoveryURL, err := url.Parse(oauth2Config.OpenIDConnectAutoDiscoveryURL)
|
||||
if err != nil || (discoveryURL.Scheme != "http" && discoveryURL.Scheme != "https") {
|
||||
ctx.Data["Err_DiscoveryURL"] = true
|
||||
ctx.RenderWithErrDeprecated(ctx.Tr("admin.auths.invalid_openIdConnectAutoDiscoveryURL"), tplAuthNew, form)
|
||||
return
|
||||
}
|
||||
}
|
||||
case auth.SSPI:
|
||||
var err error
|
||||
config, err = parseSSPIConfig(ctx, form)
|
||||
if err != nil {
|
||||
ctx.RenderWithErrDeprecated(err.Error(), tplAuthNew, form)
|
||||
return
|
||||
}
|
||||
existing, err := db.Find[auth.Source](ctx, auth.FindSourcesOptions{LoginType: auth.SSPI})
|
||||
if err != nil || len(existing) > 0 {
|
||||
ctx.Data["Err_Type"] = true
|
||||
ctx.RenderWithErrDeprecated(ctx.Tr("admin.auths.login_source_of_type_exist"), tplAuthNew, form)
|
||||
return
|
||||
}
|
||||
default:
|
||||
ctx.HTTPError(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
ctx.Data["HasTLS"] = hasTLS
|
||||
|
||||
if ctx.HasError() {
|
||||
ctx.HTML(http.StatusOK, tplAuthNew)
|
||||
return
|
||||
}
|
||||
|
||||
if err := auth.CreateSource(ctx, &auth.Source{
|
||||
Type: auth.Type(form.Type),
|
||||
Name: form.Name,
|
||||
IsActive: form.IsActive,
|
||||
IsSyncEnabled: form.IsSyncEnabled,
|
||||
TwoFactorPolicy: form.TwoFactorPolicy,
|
||||
Cfg: config,
|
||||
}); err != nil {
|
||||
if auth.IsErrSourceAlreadyExist(err) {
|
||||
ctx.Data["Err_Name"] = true
|
||||
ctx.RenderWithErrDeprecated(ctx.Tr("admin.auths.login_source_exist", err.(auth.ErrSourceAlreadyExist).Name), tplAuthNew, form)
|
||||
} else if oauth2.IsErrOpenIDConnectInitialize(err) {
|
||||
ctx.Data["Err_DiscoveryURL"] = true
|
||||
unwrapped := err.(oauth2.ErrOpenIDConnectInitialize).Unwrap()
|
||||
ctx.RenderWithErrDeprecated(ctx.Tr("admin.auths.unable_to_initialize_openid", unwrapped), tplAuthNew, form)
|
||||
} else {
|
||||
ctx.ServerError("auth.CreateSource", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
log.Trace("Authentication created by admin(%s): %s", ctx.Doer.Name, form.Name)
|
||||
|
||||
ctx.Flash.Success(ctx.Tr("admin.auths.new_success", form.Name))
|
||||
ctx.Redirect(setting.AppSubURL + "/-/admin/auths")
|
||||
}
|
||||
|
||||
// EditAuthSource render editing auth source page
|
||||
func EditAuthSource(ctx *context.Context) {
|
||||
ctx.Data["Title"] = ctx.Tr("admin.auths.edit")
|
||||
ctx.Data["PageIsAdminAuthentications"] = true
|
||||
|
||||
ctx.Data["SecurityProtocols"] = securityProtocols
|
||||
ctx.Data["SMTPAuths"] = smtp.Authenticators
|
||||
oauth2providers := oauth2.GetSupportedOAuth2Providers(ctx)
|
||||
ctx.Data["OAuth2Providers"] = oauth2providers
|
||||
|
||||
source, err := auth.GetSourceByID(ctx, ctx.PathParamInt64("authid"))
|
||||
if err != nil {
|
||||
ctx.ServerError("auth.GetSourceByID", err)
|
||||
return
|
||||
}
|
||||
ctx.Data["Source"] = source
|
||||
ctx.Data["HasTLS"] = source.HasTLS()
|
||||
|
||||
if source.IsOAuth2() {
|
||||
type Named interface {
|
||||
Name() string
|
||||
}
|
||||
|
||||
for _, provider := range oauth2providers {
|
||||
if provider.Name() == source.Cfg.(Named).Name() {
|
||||
ctx.Data["CurrentOAuth2Provider"] = provider
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ctx.HTML(http.StatusOK, tplAuthEdit)
|
||||
}
|
||||
|
||||
// EditAuthSourcePost response for editing auth source
|
||||
func EditAuthSourcePost(ctx *context.Context) {
|
||||
form := *web.GetForm(ctx).(*forms.AuthenticationForm)
|
||||
ctx.Data["Title"] = ctx.Tr("admin.auths.edit")
|
||||
ctx.Data["PageIsAdminAuthentications"] = true
|
||||
|
||||
ctx.Data["SMTPAuths"] = smtp.Authenticators
|
||||
oauth2providers := oauth2.GetSupportedOAuth2Providers(ctx)
|
||||
ctx.Data["OAuth2Providers"] = oauth2providers
|
||||
|
||||
source, err := auth.GetSourceByID(ctx, ctx.PathParamInt64("authid"))
|
||||
if err != nil {
|
||||
ctx.ServerError("auth.GetSourceByID", err)
|
||||
return
|
||||
}
|
||||
ctx.Data["Source"] = source
|
||||
ctx.Data["HasTLS"] = source.HasTLS()
|
||||
|
||||
if ctx.HasError() {
|
||||
ctx.HTML(http.StatusOK, tplAuthEdit)
|
||||
return
|
||||
}
|
||||
|
||||
var config auth.Config
|
||||
switch auth.Type(form.Type) {
|
||||
case auth.LDAP, auth.DLDAP:
|
||||
config = parseLDAPConfig(form)
|
||||
case auth.SMTP:
|
||||
config = parseSMTPConfig(form)
|
||||
case auth.PAM:
|
||||
config = &pam_service.Source{
|
||||
ServiceName: form.PAMServiceName,
|
||||
EmailDomain: form.PAMEmailDomain,
|
||||
}
|
||||
case auth.OAuth2:
|
||||
config = parseOAuth2Config(form)
|
||||
oauth2Config := config.(*oauth2.Source)
|
||||
if oauth2Config.Provider == "openidConnect" {
|
||||
discoveryURL, err := url.Parse(oauth2Config.OpenIDConnectAutoDiscoveryURL)
|
||||
if err != nil || (discoveryURL.Scheme != "http" && discoveryURL.Scheme != "https") {
|
||||
ctx.Data["Err_DiscoveryURL"] = true
|
||||
ctx.RenderWithErrDeprecated(ctx.Tr("admin.auths.invalid_openIdConnectAutoDiscoveryURL"), tplAuthEdit, form)
|
||||
return
|
||||
}
|
||||
}
|
||||
case auth.SSPI:
|
||||
config, err = parseSSPIConfig(ctx, form)
|
||||
if err != nil {
|
||||
ctx.RenderWithErrDeprecated(err.Error(), tplAuthEdit, form)
|
||||
return
|
||||
}
|
||||
default:
|
||||
ctx.HTTPError(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
source.Name = form.Name
|
||||
source.IsActive = form.IsActive
|
||||
source.IsSyncEnabled = form.IsSyncEnabled
|
||||
source.Cfg = config
|
||||
source.TwoFactorPolicy = form.TwoFactorPolicy
|
||||
if err := auth.UpdateSource(ctx, source); err != nil {
|
||||
if auth.IsErrSourceAlreadyExist(err) {
|
||||
ctx.Data["Err_Name"] = true
|
||||
ctx.RenderWithErrDeprecated(ctx.Tr("admin.auths.login_source_exist", err.(auth.ErrSourceAlreadyExist).Name), tplAuthEdit, form)
|
||||
} else if oauth2.IsErrOpenIDConnectInitialize(err) {
|
||||
ctx.Flash.Error(err.Error(), true)
|
||||
ctx.Data["Err_DiscoveryURL"] = true
|
||||
ctx.HTML(http.StatusOK, tplAuthEdit)
|
||||
} else {
|
||||
ctx.ServerError("UpdateSource", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
log.Trace("Authentication changed by admin(%s): %d", ctx.Doer.Name, source.ID)
|
||||
|
||||
ctx.Flash.Success(ctx.Tr("admin.auths.update_success"))
|
||||
ctx.Redirect(setting.AppSubURL + "/-/admin/auths/" + strconv.FormatInt(source.ID, 10))
|
||||
}
|
||||
|
||||
// DeleteAuthSource response for deleting an auth source
|
||||
func DeleteAuthSource(ctx *context.Context) {
|
||||
source, err := auth.GetSourceByID(ctx, ctx.PathParamInt64("authid"))
|
||||
if err != nil {
|
||||
ctx.ServerError("auth.GetSourceByID", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = auth_service.DeleteSource(ctx, source); err != nil {
|
||||
if auth.IsErrSourceInUse(err) {
|
||||
ctx.Flash.Error(ctx.Tr("admin.auths.still_in_used"))
|
||||
} else {
|
||||
ctx.Flash.Error(fmt.Sprintf("auth_service.DeleteSource: %v", err))
|
||||
}
|
||||
ctx.JSONRedirect(setting.AppSubURL + "/-/admin/auths/" + url.PathEscape(ctx.PathParam("authid")))
|
||||
return
|
||||
}
|
||||
log.Trace("Authentication deleted by admin(%s): %d", ctx.Doer.Name, source.ID)
|
||||
|
||||
ctx.Flash.Success(ctx.Tr("admin.auths.deletion_success"))
|
||||
ctx.JSONRedirect(setting.AppSubURL + "/-/admin/auths")
|
||||
}
|
||||
@@ -0,0 +1,317 @@
|
||||
// Copyright 2026 The Gitea Authors.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package admin
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"gitea.dev/models/db"
|
||||
user_model "gitea.dev/models/user"
|
||||
"gitea.dev/modules/log"
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/modules/templates"
|
||||
"gitea.dev/modules/util"
|
||||
"gitea.dev/modules/web"
|
||||
"gitea.dev/services/context"
|
||||
"gitea.dev/services/forms"
|
||||
)
|
||||
|
||||
const (
|
||||
tplBadges templates.TplName = "admin/badge/list"
|
||||
tplBadgeNew templates.TplName = "admin/badge/new"
|
||||
tplBadgeView templates.TplName = "admin/badge/view"
|
||||
tplBadgeEdit templates.TplName = "admin/badge/edit"
|
||||
tplBadgeUsers templates.TplName = "admin/badge/users"
|
||||
)
|
||||
|
||||
// BadgeSearchDefaultAdminSort is the default sort type for admin view
|
||||
const BadgeSearchDefaultAdminSort = "oldest"
|
||||
|
||||
// Badges show all the badges
|
||||
func Badges(ctx *context.Context) {
|
||||
ctx.Data["Title"] = ctx.Tr("admin.badges")
|
||||
ctx.Data["PageIsAdminBadges"] = true
|
||||
|
||||
RenderBadgeSearch(ctx, &user_model.SearchBadgeOptions{
|
||||
ListOptions: db.ListOptions{
|
||||
Page: max(ctx.FormInt("page"), 1),
|
||||
PageSize: setting.UI.Admin.UserPagingNum,
|
||||
},
|
||||
}, tplBadges)
|
||||
}
|
||||
|
||||
// NewBadge render adding a new badge
|
||||
func NewBadge(ctx *context.Context) {
|
||||
ctx.Data["Title"] = ctx.Tr("admin.badges.new_badge")
|
||||
ctx.Data["PageIsAdminBadges"] = true
|
||||
|
||||
ctx.HTML(http.StatusOK, tplBadgeNew)
|
||||
}
|
||||
|
||||
// NewBadgePost response for adding a new badge
|
||||
func NewBadgePost(ctx *context.Context) {
|
||||
form := web.GetForm(ctx).(*forms.AdminCreateBadgeForm)
|
||||
|
||||
if ctx.HasError() {
|
||||
ctx.JSONError(ctx.GetErrMsg())
|
||||
return
|
||||
}
|
||||
|
||||
b := &user_model.Badge{
|
||||
Slug: form.Slug,
|
||||
Description: form.Description,
|
||||
ImageURL: form.ImageURL,
|
||||
}
|
||||
|
||||
if err := user_model.CreateBadge(ctx, b); err != nil {
|
||||
if errors.Is(err, util.ErrAlreadyExist) {
|
||||
ctx.JSONError(ctx.Tr("admin.badges.slug_been_taken"))
|
||||
} else {
|
||||
ctx.ServerError("CreateBadge", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
log.Trace("Badge created by admin (%s): %s", ctx.Doer.Name, b.Slug)
|
||||
|
||||
ctx.Flash.Success(ctx.Tr("admin.badges.new_success", b.Slug))
|
||||
ctx.JSONRedirect(setting.AppSubURL + "/-/admin/badges/slug/" + url.PathEscape(b.Slug))
|
||||
}
|
||||
|
||||
func prepareBadgeInfo(ctx *context.Context) *user_model.Badge {
|
||||
b, err := user_model.GetBadge(ctx, ctx.PathParam("badge_slug"))
|
||||
if err != nil {
|
||||
if errors.Is(err, util.ErrNotExist) {
|
||||
ctx.Redirect(setting.AppSubURL + "/-/admin/badges")
|
||||
} else {
|
||||
ctx.ServerError("GetBadge", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
ctx.Data["Badge"] = b
|
||||
return b
|
||||
}
|
||||
|
||||
func ViewBadge(ctx *context.Context) {
|
||||
ctx.Data["Title"] = ctx.Tr("admin.badges.details")
|
||||
ctx.Data["PageIsAdminBadges"] = true
|
||||
|
||||
prepareBadgeInfo(ctx)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
|
||||
badge := ctx.Data["Badge"].(*user_model.Badge)
|
||||
opts := &user_model.GetBadgeUsersOptions{
|
||||
ListOptions: db.ListOptions{
|
||||
Page: 1,
|
||||
PageSize: setting.UI.Admin.UserPagingNum,
|
||||
},
|
||||
BadgeSlug: badge.Slug,
|
||||
}
|
||||
users, count, err := user_model.GetBadgeUsers(ctx, opts)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetBadgeUsers", err)
|
||||
return
|
||||
}
|
||||
ctx.Data["Users"] = users
|
||||
ctx.Data["UsersTotal"] = int(count)
|
||||
|
||||
ctx.HTML(http.StatusOK, tplBadgeView)
|
||||
}
|
||||
|
||||
// EditBadge show editing badge page
|
||||
func EditBadge(ctx *context.Context) {
|
||||
ctx.Data["Title"] = ctx.Tr("admin.badges.edit_badge")
|
||||
ctx.Data["PageIsAdminBadges"] = true
|
||||
prepareBadgeInfo(ctx)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
|
||||
ctx.HTML(http.StatusOK, tplBadgeEdit)
|
||||
}
|
||||
|
||||
// EditBadgePost response for editing badge
|
||||
func EditBadgePost(ctx *context.Context) {
|
||||
b := prepareBadgeInfo(ctx)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
|
||||
form := web.GetForm(ctx).(*forms.AdminEditBadgeForm)
|
||||
if ctx.HasError() {
|
||||
ctx.JSONError(ctx.GetErrMsg())
|
||||
return
|
||||
}
|
||||
|
||||
b.ImageURL = form.ImageURL
|
||||
b.Description = form.Description
|
||||
|
||||
if err := user_model.UpdateBadge(ctx, b); err != nil {
|
||||
ctx.ServerError("UpdateBadge", err)
|
||||
return
|
||||
}
|
||||
|
||||
log.Trace("Badge updated by admin (%s): %s", ctx.Doer.Name, b.Slug)
|
||||
|
||||
ctx.Flash.Success(ctx.Tr("admin.badges.update_success"))
|
||||
ctx.JSONRedirect(setting.AppSubURL + "/-/admin/badges/slug/" + url.PathEscape(ctx.PathParam("badge_slug")))
|
||||
}
|
||||
|
||||
// DeleteBadge response for deleting a badge
|
||||
func DeleteBadge(ctx *context.Context) {
|
||||
b, err := user_model.GetBadge(ctx, ctx.PathParam("badge_slug"))
|
||||
if err != nil {
|
||||
ctx.ServerError("GetBadge", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = user_model.DeleteBadge(ctx, b); err != nil {
|
||||
ctx.ServerError("DeleteBadge", err)
|
||||
return
|
||||
}
|
||||
|
||||
log.Trace("Badge deleted by admin (%s): %s", ctx.Doer.Name, b.Slug)
|
||||
|
||||
ctx.Flash.Success(ctx.Tr("admin.badges.deletion_success"))
|
||||
ctx.Redirect(setting.AppSubURL + "/-/admin/badges")
|
||||
}
|
||||
|
||||
func BadgeUsers(ctx *context.Context) {
|
||||
ctx.Data["Title"] = ctx.Tr("admin.badges.users_with_badge", ctx.PathParam("badge_slug"))
|
||||
ctx.Data["PageIsAdminBadges"] = true
|
||||
|
||||
page := max(ctx.FormInt("page"), 1)
|
||||
|
||||
badge := &user_model.Badge{Slug: ctx.PathParam("badge_slug")}
|
||||
opts := &user_model.GetBadgeUsersOptions{
|
||||
ListOptions: db.ListOptions{
|
||||
Page: page,
|
||||
PageSize: setting.UI.Admin.UserPagingNum,
|
||||
},
|
||||
BadgeSlug: badge.Slug,
|
||||
}
|
||||
users, count, err := user_model.GetBadgeUsers(ctx, opts)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetBadgeUsers", err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Data["Users"] = users
|
||||
ctx.Data["Total"] = count
|
||||
ctx.Data["Page"] = context.NewPagination(count, setting.UI.Admin.UserPagingNum, page, 5)
|
||||
|
||||
ctx.HTML(http.StatusOK, tplBadgeUsers)
|
||||
}
|
||||
|
||||
// BadgeUsersPost response for actions for user badges
|
||||
func BadgeUsersPost(ctx *context.Context) {
|
||||
name := strings.ToLower(ctx.FormString("user"))
|
||||
|
||||
u, err := user_model.GetUserByName(ctx, name)
|
||||
if err != nil {
|
||||
if user_model.IsErrUserNotExist(err) {
|
||||
ctx.Flash.Error(ctx.Tr("form.user_not_exist"))
|
||||
ctx.Redirect(setting.AppSubURL + ctx.Req.URL.EscapedPath())
|
||||
} else {
|
||||
ctx.ServerError("GetUserByName", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if err = user_model.AddUserBadge(ctx, u, &user_model.Badge{Slug: ctx.PathParam("badge_slug")}); err != nil {
|
||||
if errors.Is(err, util.ErrNotExist) {
|
||||
ctx.Flash.Error(ctx.Tr("admin.badges.not_found"))
|
||||
ctx.Redirect(setting.AppSubURL + ctx.Req.URL.EscapedPath())
|
||||
} else if errors.Is(err, util.ErrAlreadyExist) {
|
||||
ctx.Flash.Error(ctx.Tr("admin.badges.user_already_has"))
|
||||
ctx.Redirect(setting.AppSubURL + ctx.Req.URL.EscapedPath())
|
||||
} else {
|
||||
ctx.ServerError("AddUserBadge", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Flash.Success(ctx.Tr("admin.badges.user_add_success"))
|
||||
ctx.Redirect(setting.AppSubURL + ctx.Req.URL.EscapedPath())
|
||||
}
|
||||
|
||||
// DeleteBadgeUser delete a badge from a user
|
||||
func DeleteBadgeUser(ctx *context.Context) {
|
||||
badgeUsersURL := setting.AppSubURL + "/-/admin/badges/slug/" + url.PathEscape(ctx.PathParam("badge_slug")) + "/users"
|
||||
|
||||
user, err := user_model.GetUserByID(ctx, ctx.FormInt64("id"))
|
||||
if err != nil {
|
||||
if user_model.IsErrUserNotExist(err) {
|
||||
ctx.Flash.Error(ctx.Tr("form.user_not_exist"))
|
||||
ctx.JSONRedirect(badgeUsersURL)
|
||||
return
|
||||
} else {
|
||||
ctx.ServerError("GetUserByID", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
if err := user_model.RemoveUserBadge(ctx, user, &user_model.Badge{Slug: ctx.PathParam("badge_slug")}); err == nil {
|
||||
ctx.Flash.Success(ctx.Tr("admin.badges.user_remove_success"))
|
||||
} else {
|
||||
ctx.ServerError("RemoveUserBadge", err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSONRedirect(badgeUsersURL)
|
||||
}
|
||||
|
||||
func RenderBadgeSearch(ctx *context.Context, opts *user_model.SearchBadgeOptions, tplName templates.TplName) {
|
||||
var (
|
||||
badges []*user_model.Badge
|
||||
count int64
|
||||
err error
|
||||
orderBy db.SearchOrderBy
|
||||
)
|
||||
|
||||
sortOrder := ctx.FormString("sort")
|
||||
if sortOrder == "" {
|
||||
sortOrder = BadgeSearchDefaultAdminSort
|
||||
}
|
||||
ctx.Data["SortType"] = sortOrder
|
||||
|
||||
switch sortOrder {
|
||||
case "newest":
|
||||
orderBy = "`badge`.id DESC"
|
||||
case "oldest":
|
||||
orderBy = "`badge`.id ASC"
|
||||
case "reversealphabetically":
|
||||
orderBy = "`badge`.slug DESC"
|
||||
case "alphabetically":
|
||||
orderBy = "`badge`.slug ASC"
|
||||
default:
|
||||
// In case the sort type is invalid, keep admin default sorting.
|
||||
ctx.Data["SortType"] = "oldest"
|
||||
orderBy = "`badge`.id ASC"
|
||||
}
|
||||
|
||||
opts.Keyword = ctx.FormTrim("q")
|
||||
opts.OrderBy = orderBy
|
||||
if len(opts.Keyword) == 0 || isKeywordValid(opts.Keyword) {
|
||||
badges, count, err = user_model.SearchBadges(ctx, opts)
|
||||
if err != nil {
|
||||
ctx.ServerError("SearchBadges", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
ctx.Data["Keyword"] = opts.Keyword
|
||||
ctx.Data["Total"] = count
|
||||
ctx.Data["Badges"] = badges
|
||||
|
||||
pager := context.NewPagination(count, opts.PageSize, opts.Page, 5)
|
||||
pager.AddParamFromRequest(ctx.Req)
|
||||
ctx.Data["Page"] = pager
|
||||
|
||||
ctx.HTML(http.StatusOK, tplName)
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
// Copyright 2014 The Gogs Authors. All rights reserved.
|
||||
// Copyright 2019 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package admin
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
system_model "gitea.dev/models/system"
|
||||
"gitea.dev/modules/cache"
|
||||
"gitea.dev/modules/git"
|
||||
"gitea.dev/modules/json"
|
||||
"gitea.dev/modules/log"
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/modules/setting/config"
|
||||
"gitea.dev/modules/templates"
|
||||
"gitea.dev/modules/util"
|
||||
"gitea.dev/services/context"
|
||||
"gitea.dev/services/mailer"
|
||||
|
||||
"gitea.com/go-chi/session"
|
||||
)
|
||||
|
||||
const (
|
||||
tplConfig templates.TplName = "admin/config"
|
||||
tplConfigSettings templates.TplName = "admin/config_settings/config_settings"
|
||||
)
|
||||
|
||||
// SendTestMail send test mail to confirm mail service is OK
|
||||
func SendTestMail(ctx *context.Context) {
|
||||
email := ctx.FormString("email")
|
||||
// Send a test email to the user's email address and redirect back to Config
|
||||
if err := mailer.SendTestMail(email); err != nil {
|
||||
ctx.Flash.Error(ctx.Tr("admin.config.test_mail_failed", email, err))
|
||||
} else {
|
||||
ctx.Flash.Info(ctx.Tr("admin.config.test_mail_sent", email))
|
||||
}
|
||||
|
||||
ctx.Redirect(setting.AppSubURL + "/-/admin/config")
|
||||
}
|
||||
|
||||
// TestCache test the cache settings
|
||||
func TestCache(ctx *context.Context) {
|
||||
elapsed, err := cache.Test()
|
||||
if err != nil {
|
||||
ctx.Flash.Error(ctx.Tr("admin.config.cache_test_failed", err))
|
||||
} else {
|
||||
if elapsed > cache.SlowCacheThreshold {
|
||||
ctx.Flash.Warning(ctx.Tr("admin.config.cache_test_slow", elapsed))
|
||||
} else {
|
||||
ctx.Flash.Info(ctx.Tr("admin.config.cache_test_succeeded", elapsed))
|
||||
}
|
||||
}
|
||||
|
||||
ctx.Redirect(setting.AppSubURL + "/-/admin/config")
|
||||
}
|
||||
|
||||
// Config show admin config page
|
||||
func Config(ctx *context.Context) {
|
||||
ctx.Data["Title"] = ctx.Tr("admin.config_summary")
|
||||
ctx.Data["PageIsAdminConfig"] = true
|
||||
ctx.Data["PageIsAdminConfigSummary"] = true
|
||||
|
||||
ctx.Data["CustomConf"] = setting.CustomConf
|
||||
ctx.Data["AppBuiltWith"] = setting.AppBuiltWith
|
||||
ctx.Data["RunUser"] = setting.RunUser
|
||||
ctx.Data["RunMode"] = util.ToTitleCase(setting.RunMode)
|
||||
ctx.Data["GitVersion"] = git.DefaultFeatures().VersionInfo()
|
||||
|
||||
ctx.Data["AppDataPath"] = setting.AppDataPath
|
||||
ctx.Data["RepoRootPath"] = setting.RepoRootPath
|
||||
ctx.Data["CustomRootPath"] = setting.CustomPath
|
||||
ctx.Data["LogRootPath"] = setting.Log.RootPath
|
||||
ctx.Data["ScriptType"] = setting.ScriptType
|
||||
ctx.Data["ReverseProxyAuthUser"] = setting.ReverseProxyAuthUser
|
||||
ctx.Data["ReverseProxyAuthEmail"] = setting.ReverseProxyAuthEmail
|
||||
|
||||
ctx.Data["SSH"] = setting.SSH
|
||||
ctx.Data["LFS"] = setting.LFS
|
||||
|
||||
ctx.Data["Service"] = setting.Service
|
||||
ctx.Data["DbCfg"] = setting.Database
|
||||
ctx.Data["Webhook"] = setting.Webhook
|
||||
ctx.Data["MailerEnabled"] = false
|
||||
if setting.MailService != nil {
|
||||
ctx.Data["MailerEnabled"] = true
|
||||
ctx.Data["Mailer"] = setting.MailService
|
||||
}
|
||||
|
||||
ctx.Data["CacheAdapter"] = setting.CacheService.Adapter
|
||||
ctx.Data["CacheInterval"] = setting.CacheService.Interval
|
||||
ctx.Data["CacheItemTTL"] = setting.CacheService.TTL
|
||||
|
||||
sessionCfg := setting.SessionConfig
|
||||
if sessionCfg.Provider == "VirtualSession" {
|
||||
var realSession session.Options
|
||||
if err := json.Unmarshal([]byte(sessionCfg.ProviderConfig), &realSession); err != nil {
|
||||
log.Error("Unable to unmarshall session config for virtual provider config: %s\nError: %v", sessionCfg.ProviderConfig, err)
|
||||
}
|
||||
sessionCfg.Provider = realSession.Provider
|
||||
sessionCfg.ProviderConfig = realSession.ProviderConfig
|
||||
sessionCfg.CookieName = realSession.CookieName
|
||||
sessionCfg.CookiePath = realSession.CookiePath
|
||||
sessionCfg.Gclifetime = realSession.Gclifetime
|
||||
sessionCfg.Maxlifetime = realSession.Maxlifetime
|
||||
sessionCfg.Secure = realSession.Secure
|
||||
sessionCfg.Domain = realSession.Domain
|
||||
}
|
||||
sessionCfg.ProviderConfig = ""
|
||||
ctx.Data["SessionConfig"] = sessionCfg
|
||||
|
||||
ctx.Data["Git"] = setting.Git
|
||||
ctx.Data["AccessLogTemplate"] = setting.Log.AccessLogTemplate
|
||||
ctx.Data["LogSQL"] = setting.Database.LogSQL
|
||||
|
||||
ctx.Data["Loggers"] = log.GetManager().DumpLoggers()
|
||||
config.GetDynGetter().InvalidateCache()
|
||||
prepareStartupProblemsAlert(ctx)
|
||||
|
||||
ctx.HTML(http.StatusOK, tplConfig)
|
||||
}
|
||||
|
||||
func ConfigSettings(ctx *context.Context) {
|
||||
ctx.Data["Title"] = ctx.Tr("admin.config_settings")
|
||||
ctx.Data["PageIsAdminConfig"] = true
|
||||
ctx.Data["PageIsAdminConfigSettings"] = true
|
||||
ctx.HTML(http.StatusOK, tplConfigSettings)
|
||||
}
|
||||
|
||||
func validateConfigKeyValue(dynKey, input string) error {
|
||||
opt := config.GetConfigOption(dynKey)
|
||||
if opt == nil {
|
||||
return util.NewInvalidArgumentErrorf("unknown config key: %s", dynKey)
|
||||
}
|
||||
|
||||
const limit = 64 * 1024
|
||||
if len(input) > limit {
|
||||
return util.NewInvalidArgumentErrorf("value length exceeds limit of %d", limit)
|
||||
}
|
||||
|
||||
if !json.Valid([]byte(input)) {
|
||||
return util.NewInvalidArgumentErrorf("invalid json value for key: %s", dynKey)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func ChangeConfig(ctx *context.Context) {
|
||||
_ = ctx.Req.ParseForm()
|
||||
configKeys := ctx.Req.Form["key"]
|
||||
configValues := ctx.Req.Form["value"]
|
||||
configSettings := map[string]string{}
|
||||
loop:
|
||||
for i, key := range configKeys {
|
||||
if i >= len(configValues) {
|
||||
ctx.JSONError(ctx.Tr("admin.config.set_setting_failed", key))
|
||||
break loop
|
||||
}
|
||||
value := configValues[i]
|
||||
|
||||
err := validateConfigKeyValue(key, value)
|
||||
if err != nil {
|
||||
if errors.Is(err, util.ErrInvalidArgument) {
|
||||
ctx.JSONError(err.Error())
|
||||
} else {
|
||||
ctx.JSONError(ctx.Tr("admin.config.set_setting_failed", key))
|
||||
}
|
||||
break loop
|
||||
}
|
||||
configSettings[key] = value
|
||||
}
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
if err := system_model.SetSettings(ctx, configSettings); err != nil {
|
||||
ctx.ServerError("SetSettings", err)
|
||||
return
|
||||
}
|
||||
config.GetDynGetter().InvalidateCache()
|
||||
ctx.JSONOK()
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
// Copyright 2023 The Gitea Authors.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package admin
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"fmt"
|
||||
"runtime/pprof"
|
||||
"time"
|
||||
|
||||
"gitea.dev/modules/httplib"
|
||||
"gitea.dev/modules/tailmsg"
|
||||
"gitea.dev/modules/util"
|
||||
"gitea.dev/services/context"
|
||||
)
|
||||
|
||||
func MonitorDiagnosis(ctx *context.Context) {
|
||||
seconds := min(max(ctx.FormInt64("seconds"), 1), 300)
|
||||
|
||||
httplib.ServeSetHeaders(ctx.Resp, httplib.ServeHeaderOptions{
|
||||
ContentType: "application/zip",
|
||||
Filename: fmt.Sprintf("gitea-diagnosis-%s.zip", time.Now().Format("20060102-150405")),
|
||||
ContentDisposition: httplib.ContentDispositionAttachment,
|
||||
})
|
||||
|
||||
zipWriter := zip.NewWriter(ctx.Resp)
|
||||
defer zipWriter.Close()
|
||||
|
||||
f, err := zipWriter.CreateHeader(&zip.FileHeader{Name: "goroutine-before.txt", Method: zip.Deflate, Modified: time.Now()})
|
||||
if err != nil {
|
||||
ctx.ServerError("Failed to create zip file", err)
|
||||
return
|
||||
}
|
||||
_ = pprof.Lookup("goroutine").WriteTo(f, 1)
|
||||
|
||||
f, err = zipWriter.CreateHeader(&zip.FileHeader{Name: "cpu-profile.dat", Method: zip.Deflate, Modified: time.Now()})
|
||||
if err != nil {
|
||||
ctx.ServerError("Failed to create zip file", err)
|
||||
return
|
||||
}
|
||||
|
||||
err = pprof.StartCPUProfile(f)
|
||||
if err == nil {
|
||||
time.Sleep(time.Duration(seconds) * time.Second)
|
||||
pprof.StopCPUProfile()
|
||||
} else {
|
||||
_, _ = f.Write([]byte(err.Error()))
|
||||
}
|
||||
|
||||
f, err = zipWriter.CreateHeader(&zip.FileHeader{Name: "goroutine-after.txt", Method: zip.Deflate, Modified: time.Now()})
|
||||
if err != nil {
|
||||
ctx.ServerError("Failed to create zip file", err)
|
||||
return
|
||||
}
|
||||
_ = pprof.Lookup("goroutine").WriteTo(f, 1)
|
||||
|
||||
f, err = zipWriter.CreateHeader(&zip.FileHeader{Name: "heap.dat", Method: zip.Deflate, Modified: time.Now()})
|
||||
if err != nil {
|
||||
ctx.ServerError("Failed to create zip file", err)
|
||||
return
|
||||
}
|
||||
_ = pprof.Lookup("heap").WriteTo(f, 0)
|
||||
|
||||
f, err = zipWriter.CreateHeader(&zip.FileHeader{Name: "perftrace.txt", Method: zip.Deflate, Modified: time.Now()})
|
||||
if err != nil {
|
||||
ctx.ServerError("Failed to create zip file", err)
|
||||
return
|
||||
}
|
||||
for _, record := range tailmsg.GetManager().GetTraceRecorder().GetRecords() {
|
||||
_, _ = f.Write(util.UnsafeStringToBytes(record.Time.Format(time.RFC3339)))
|
||||
_, _ = f.Write([]byte(" "))
|
||||
_, _ = f.Write(util.UnsafeStringToBytes((record.Content)))
|
||||
_, _ = f.Write([]byte("\n\n"))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
// Copyright 2020 The Gitea Authors.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package admin
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"gitea.dev/models/db"
|
||||
user_model "gitea.dev/models/user"
|
||||
"gitea.dev/modules/log"
|
||||
"gitea.dev/modules/optional"
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/modules/templates"
|
||||
"gitea.dev/services/context"
|
||||
"gitea.dev/services/user"
|
||||
)
|
||||
|
||||
const (
|
||||
tplEmails templates.TplName = "admin/emails/list"
|
||||
)
|
||||
|
||||
// Emails show all emails
|
||||
func Emails(ctx *context.Context) {
|
||||
ctx.Data["Title"] = ctx.Tr("admin.emails")
|
||||
ctx.Data["PageIsAdminEmails"] = true
|
||||
|
||||
opts := &user_model.SearchEmailOptions{
|
||||
ListOptions: db.ListOptions{
|
||||
PageSize: setting.UI.Admin.UserPagingNum,
|
||||
Page: ctx.FormInt("page"),
|
||||
},
|
||||
}
|
||||
|
||||
if opts.Page <= 1 {
|
||||
opts.Page = 1
|
||||
}
|
||||
|
||||
type ActiveEmail struct {
|
||||
user_model.SearchEmailResult
|
||||
CanChange bool
|
||||
}
|
||||
|
||||
var (
|
||||
baseEmails []*user_model.SearchEmailResult
|
||||
emails []ActiveEmail
|
||||
count int64
|
||||
err error
|
||||
orderBy user_model.SearchEmailOrderBy
|
||||
)
|
||||
|
||||
ctx.Data["SortType"] = ctx.FormString("sort")
|
||||
switch ctx.FormString("sort") {
|
||||
case "email":
|
||||
orderBy = user_model.SearchEmailOrderByEmail
|
||||
case "reverseemail":
|
||||
orderBy = user_model.SearchEmailOrderByEmailReverse
|
||||
case "username":
|
||||
orderBy = user_model.SearchEmailOrderByName
|
||||
case "reverseusername":
|
||||
orderBy = user_model.SearchEmailOrderByNameReverse
|
||||
default:
|
||||
ctx.Data["SortType"] = "email"
|
||||
orderBy = user_model.SearchEmailOrderByEmail
|
||||
}
|
||||
|
||||
opts.Keyword = ctx.FormTrim("q")
|
||||
opts.SortType = orderBy
|
||||
if len(ctx.FormString("is_activated")) != 0 {
|
||||
opts.IsActivated = optional.Some(ctx.FormBool("activated"))
|
||||
}
|
||||
if len(ctx.FormString("is_primary")) != 0 {
|
||||
opts.IsPrimary = optional.Some(ctx.FormBool("primary"))
|
||||
}
|
||||
|
||||
if len(opts.Keyword) == 0 || isKeywordValid(opts.Keyword) {
|
||||
baseEmails, count, err = user_model.SearchEmails(ctx, opts)
|
||||
if err != nil {
|
||||
ctx.ServerError("SearchEmails", err)
|
||||
return
|
||||
}
|
||||
emails = make([]ActiveEmail, len(baseEmails))
|
||||
for i := range baseEmails {
|
||||
emails[i].SearchEmailResult = *baseEmails[i]
|
||||
// Don't let the admin deactivate its own primary email address
|
||||
// We already know the user is admin
|
||||
emails[i].CanChange = ctx.Doer.ID != emails[i].UID || !emails[i].IsPrimary
|
||||
}
|
||||
}
|
||||
ctx.Data["Keyword"] = opts.Keyword
|
||||
ctx.Data["Total"] = count
|
||||
ctx.Data["Emails"] = emails
|
||||
|
||||
pager := context.NewPagination(count, opts.PageSize, opts.Page, 5)
|
||||
pager.AddParamFromRequest(ctx.Req)
|
||||
ctx.Data["Page"] = pager
|
||||
|
||||
ctx.HTML(http.StatusOK, tplEmails)
|
||||
}
|
||||
|
||||
var nullByte = []byte{0x00}
|
||||
|
||||
func isKeywordValid(keyword string) bool {
|
||||
return !bytes.Contains([]byte(keyword), nullByte)
|
||||
}
|
||||
|
||||
// ActivateEmail serves a POST request for activating/deactivating a user's email
|
||||
func ActivateEmail(ctx *context.Context) {
|
||||
truefalse := map[string]bool{"1": true, "0": false}
|
||||
|
||||
uid := ctx.FormInt64("uid")
|
||||
email := ctx.FormString("email")
|
||||
primary, okp := truefalse[ctx.FormString("primary")]
|
||||
activate, oka := truefalse[ctx.FormString("activate")]
|
||||
|
||||
if uid == 0 || len(email) == 0 || !okp || !oka {
|
||||
ctx.HTTPError(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
log.Info("Changing activation for User ID: %d, email: %s, primary: %v to %v", uid, email, primary, activate)
|
||||
|
||||
if err := user_model.ActivateUserEmail(ctx, uid, email, activate); err != nil {
|
||||
log.Error("ActivateUserEmail(%v,%v,%v): %v", uid, email, activate, err)
|
||||
if user_model.IsErrEmailAlreadyUsed(err) {
|
||||
ctx.Flash.Error(ctx.Tr("admin.emails.duplicate_active"))
|
||||
} else {
|
||||
ctx.Flash.Error(ctx.Tr("admin.emails.not_updated", err))
|
||||
}
|
||||
} else {
|
||||
log.Info("Activation for User ID: %d, email: %s, primary: %v changed to %v", uid, email, primary, activate)
|
||||
ctx.Flash.Info(ctx.Tr("admin.emails.updated"))
|
||||
}
|
||||
|
||||
redirect, _ := url.Parse(setting.AppSubURL + "/-/admin/emails")
|
||||
q := url.Values{}
|
||||
if val := ctx.FormTrim("q"); len(val) > 0 {
|
||||
q.Set("q", val)
|
||||
}
|
||||
if val := ctx.FormTrim("sort"); len(val) > 0 {
|
||||
q.Set("sort", val)
|
||||
}
|
||||
if val := ctx.FormTrim("is_primary"); len(val) > 0 {
|
||||
q.Set("is_primary", val)
|
||||
}
|
||||
if val := ctx.FormTrim("is_activated"); len(val) > 0 {
|
||||
q.Set("is_activated", val)
|
||||
}
|
||||
redirect.RawQuery = q.Encode()
|
||||
ctx.Redirect(redirect.String())
|
||||
}
|
||||
|
||||
// DeleteEmail serves a POST request for delete a user's email
|
||||
func DeleteEmail(ctx *context.Context) {
|
||||
u, err := user_model.GetUserByID(ctx, ctx.FormInt64("uid"))
|
||||
if err != nil || u == nil {
|
||||
ctx.ServerError("GetUserByID", err)
|
||||
return
|
||||
}
|
||||
|
||||
email, err := user_model.GetEmailAddressByID(ctx, u.ID, ctx.FormInt64("id"))
|
||||
if err != nil || email == nil {
|
||||
ctx.ServerError("GetEmailAddressByID", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := user.DeleteEmailAddresses(ctx, u, []string{email.Email}); err != nil {
|
||||
if user_model.IsErrPrimaryEmailCannotDelete(err) {
|
||||
ctx.Flash.Error(ctx.Tr("admin.emails.delete_primary_email_error"))
|
||||
ctx.JSONRedirect("")
|
||||
return
|
||||
}
|
||||
ctx.ServerError("DeleteEmailAddresses", err)
|
||||
return
|
||||
}
|
||||
log.Trace("Email address deleted: %s %s", u.Name, email.Email)
|
||||
|
||||
ctx.Flash.Success(ctx.Tr("admin.emails.deletion_success"))
|
||||
ctx.JSONRedirect("")
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
// Copyright 2018 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package admin
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"gitea.dev/models/webhook"
|
||||
"gitea.dev/modules/optional"
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/modules/templates"
|
||||
"gitea.dev/services/context"
|
||||
)
|
||||
|
||||
const (
|
||||
// tplAdminHooks template path to render hook settings
|
||||
tplAdminHooks templates.TplName = "admin/hooks"
|
||||
)
|
||||
|
||||
// DefaultOrSystemWebhooks renders both admin default and system webhook list pages
|
||||
func DefaultOrSystemWebhooks(ctx *context.Context) {
|
||||
var err error
|
||||
|
||||
ctx.Data["Title"] = ctx.Tr("admin.hooks")
|
||||
ctx.Data["PageIsAdminSystemHooks"] = true
|
||||
ctx.Data["PageIsAdminDefaultHooks"] = true
|
||||
|
||||
def := make(map[string]any, len(ctx.Data))
|
||||
sys := make(map[string]any, len(ctx.Data))
|
||||
for k, v := range ctx.Data {
|
||||
def[k] = v
|
||||
sys[k] = v
|
||||
}
|
||||
|
||||
sys["Title"] = ctx.Tr("admin.systemhooks")
|
||||
sys["Description"] = ctx.Tr("admin.systemhooks.desc", "https://docs.gitea.com/usage/webhooks")
|
||||
sys["Webhooks"], err = webhook.GetSystemWebhooks(ctx, optional.None[bool]())
|
||||
sys["BaseLink"] = setting.AppSubURL + "/-/admin/hooks"
|
||||
sys["BaseLinkNew"] = setting.AppSubURL + "/-/admin/system-hooks"
|
||||
if err != nil {
|
||||
ctx.ServerError("GetWebhooksAdmin", err)
|
||||
return
|
||||
}
|
||||
|
||||
def["Title"] = ctx.Tr("admin.defaulthooks")
|
||||
def["Description"] = ctx.Tr("admin.defaulthooks.desc", "https://docs.gitea.com/usage/webhooks")
|
||||
def["Webhooks"], err = webhook.GetDefaultWebhooks(ctx)
|
||||
def["BaseLink"] = setting.AppSubURL + "/-/admin/hooks"
|
||||
def["BaseLinkNew"] = setting.AppSubURL + "/-/admin/default-hooks"
|
||||
if err != nil {
|
||||
ctx.ServerError("GetWebhooksAdmin", err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Data["DefaultWebhooks"] = def
|
||||
ctx.Data["SystemWebhooks"] = sys
|
||||
|
||||
ctx.HTML(http.StatusOK, tplAdminHooks)
|
||||
}
|
||||
|
||||
// DeleteDefaultOrSystemWebhook handler to delete an admin-defined system or default webhook
|
||||
func DeleteDefaultOrSystemWebhook(ctx *context.Context) {
|
||||
if err := webhook.DeleteDefaultSystemWebhook(ctx, ctx.FormInt64("id")); err != nil {
|
||||
ctx.Flash.Error("DeleteDefaultWebhook: " + err.Error())
|
||||
} else {
|
||||
ctx.Flash.Success(ctx.Tr("repo.settings.webhook_deletion_success"))
|
||||
}
|
||||
|
||||
ctx.JSONRedirect(setting.AppSubURL + "/-/admin/hooks")
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
// Copyright 2018 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package admin
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"gitea.dev/models/unittest"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
unittest.MainTest(m)
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
// Copyright 2014 The Gogs Authors. All rights reserved.
|
||||
// Copyright 2019 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package admin
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"gitea.dev/models/db"
|
||||
system_model "gitea.dev/models/system"
|
||||
"gitea.dev/modules/log"
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/modules/templates"
|
||||
"gitea.dev/services/context"
|
||||
)
|
||||
|
||||
const (
|
||||
tplNotices templates.TplName = "admin/notice"
|
||||
)
|
||||
|
||||
// Notices show notices for admin
|
||||
func Notices(ctx *context.Context) {
|
||||
ctx.Data["Title"] = ctx.Tr("admin.notices")
|
||||
ctx.Data["PageIsAdminNotices"] = true
|
||||
|
||||
total := system_model.CountNotices(ctx)
|
||||
page := max(ctx.FormInt("page"), 1)
|
||||
|
||||
notices, err := system_model.Notices(ctx, page, setting.UI.Admin.NoticePagingNum)
|
||||
if err != nil {
|
||||
ctx.ServerError("Notices", err)
|
||||
return
|
||||
}
|
||||
ctx.Data["Notices"] = notices
|
||||
|
||||
ctx.Data["Total"] = total
|
||||
|
||||
ctx.Data["Page"] = context.NewPagination(total, setting.UI.Admin.NoticePagingNum, page, 5)
|
||||
|
||||
ctx.HTML(http.StatusOK, tplNotices)
|
||||
}
|
||||
|
||||
// DeleteNotices delete the specific notices
|
||||
func DeleteNotices(ctx *context.Context) {
|
||||
strs := ctx.FormStrings("ids[]")
|
||||
ids := make([]int64, 0, len(strs))
|
||||
for i := range strs {
|
||||
id, _ := strconv.ParseInt(strs[i], 10, 64)
|
||||
if id > 0 {
|
||||
ids = append(ids, id)
|
||||
}
|
||||
}
|
||||
|
||||
if err := db.DeleteByIDs[system_model.Notice](ctx, ids...); err != nil {
|
||||
ctx.Flash.Error("DeleteNoticesByIDs: " + err.Error())
|
||||
ctx.Status(http.StatusInternalServerError)
|
||||
} else {
|
||||
ctx.Flash.Success(ctx.Tr("admin.notices.delete_success"))
|
||||
ctx.Status(http.StatusOK)
|
||||
}
|
||||
}
|
||||
|
||||
// EmptyNotices delete all the notices
|
||||
func EmptyNotices(ctx *context.Context) {
|
||||
if err := system_model.DeleteNotices(ctx, 0, 0); err != nil {
|
||||
ctx.ServerError("DeleteNotices", err)
|
||||
return
|
||||
}
|
||||
|
||||
log.Trace("System notices deleted by admin (%s): [start: %d]", ctx.Doer.Name, 0)
|
||||
ctx.Flash.Success(ctx.Tr("admin.notices.delete_success"))
|
||||
ctx.Redirect(setting.AppSubURL + "/-/admin/notices")
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
// Copyright 2014 The Gogs Authors. All rights reserved.
|
||||
// Copyright 2020 The Gitea Authors.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package admin
|
||||
|
||||
import (
|
||||
"gitea.dev/models/db"
|
||||
user_model "gitea.dev/models/user"
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/modules/structs"
|
||||
"gitea.dev/modules/templates"
|
||||
"gitea.dev/routers/web/explore"
|
||||
"gitea.dev/services/context"
|
||||
)
|
||||
|
||||
const (
|
||||
tplOrgs templates.TplName = "admin/org/list"
|
||||
)
|
||||
|
||||
// Organizations show all the organizations
|
||||
func Organizations(ctx *context.Context) {
|
||||
ctx.Data["Title"] = ctx.Tr("admin.organizations")
|
||||
ctx.Data["PageIsAdminOrganizations"] = true
|
||||
|
||||
if ctx.FormString("sort") == "" {
|
||||
ctx.SetFormString("sort", UserSearchDefaultAdminSort)
|
||||
}
|
||||
|
||||
explore.RenderUserSearch(ctx, user_model.SearchUserOptions{
|
||||
Actor: ctx.Doer,
|
||||
Types: []user_model.UserType{user_model.UserTypeOrganization},
|
||||
IncludeReserved: true, // administrator needs to list all accounts include reserved
|
||||
ListOptions: db.ListOptions{
|
||||
PageSize: setting.UI.Admin.OrgPagingNum,
|
||||
},
|
||||
Visible: []structs.VisibleType{structs.VisibleTypePublic, structs.VisibleTypeLimited, structs.VisibleTypePrivate},
|
||||
}, tplOrgs)
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
// Copyright 2014 The Gogs Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package admin
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"gitea.dev/models/db"
|
||||
packages_model "gitea.dev/models/packages"
|
||||
"gitea.dev/modules/optional"
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/modules/templates"
|
||||
"gitea.dev/services/context"
|
||||
packages_service "gitea.dev/services/packages"
|
||||
packages_cleanup_service "gitea.dev/services/packages/cleanup"
|
||||
)
|
||||
|
||||
const (
|
||||
tplPackagesList templates.TplName = "admin/packages/list"
|
||||
)
|
||||
|
||||
// Packages shows all packages
|
||||
func Packages(ctx *context.Context) {
|
||||
page := max(ctx.FormInt("page"), 1)
|
||||
query := ctx.FormTrim("q")
|
||||
packageType := ctx.FormTrim("type")
|
||||
sort := ctx.FormTrim("sort")
|
||||
|
||||
pvs, total, err := packages_model.SearchVersions(ctx, &packages_model.PackageSearchOptions{
|
||||
Type: packages_model.Type(packageType),
|
||||
Name: packages_model.SearchValue{Value: query},
|
||||
Sort: sort,
|
||||
IsInternal: optional.Some(false),
|
||||
Paginator: &db.ListOptions{
|
||||
PageSize: setting.UI.PackagesPagingNum,
|
||||
Page: page,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
ctx.ServerError("SearchVersions", err)
|
||||
return
|
||||
}
|
||||
|
||||
pds, err := packages_model.GetPackageDescriptors(ctx, pvs)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetPackageDescriptors", err)
|
||||
return
|
||||
}
|
||||
|
||||
totalBlobSize, err := packages_model.GetTotalBlobSize(ctx)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetTotalBlobSize", err)
|
||||
return
|
||||
}
|
||||
|
||||
totalUnreferencedBlobSize, err := packages_model.GetTotalUnreferencedBlobSize(ctx)
|
||||
if err != nil {
|
||||
ctx.ServerError("CalculateBlobSize", err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Data["Title"] = ctx.Tr("packages.title")
|
||||
ctx.Data["PageIsAdminPackages"] = true
|
||||
ctx.Data["Query"] = query
|
||||
ctx.Data["PackageType"] = packageType
|
||||
ctx.Data["AvailableTypes"] = packages_model.TypeList
|
||||
ctx.Data["SortType"] = sort
|
||||
ctx.Data["PackageDescriptors"] = pds
|
||||
ctx.Data["TotalCount"] = total
|
||||
ctx.Data["TotalBlobSize"] = totalBlobSize - totalUnreferencedBlobSize
|
||||
ctx.Data["TotalUnreferencedBlobSize"] = totalUnreferencedBlobSize
|
||||
|
||||
pager := context.NewPagination(total, setting.UI.PackagesPagingNum, page, 5)
|
||||
pager.AddParamFromRequest(ctx.Req)
|
||||
ctx.Data["Page"] = pager
|
||||
|
||||
ctx.HTML(http.StatusOK, tplPackagesList)
|
||||
}
|
||||
|
||||
// DeletePackageVersion deletes a package version
|
||||
func DeletePackageVersion(ctx *context.Context) {
|
||||
pv, err := packages_model.GetVersionByID(ctx, ctx.FormInt64("id"))
|
||||
if err != nil {
|
||||
ctx.ServerError("GetRepositoryByID", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := packages_service.RemovePackageVersion(ctx, ctx.Doer, pv); err != nil {
|
||||
ctx.ServerError("RemovePackageVersion", err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Flash.Success(ctx.Tr("packages.settings.delete.version.success"))
|
||||
ctx.JSONRedirect(setting.AppSubURL + "/-/admin/packages?page=" + url.QueryEscape(ctx.FormString("page")) + "&q=" + url.QueryEscape(ctx.FormString("q")) + "&type=" + url.QueryEscape(ctx.FormString("type")))
|
||||
}
|
||||
|
||||
func CleanupExpiredData(ctx *context.Context) {
|
||||
if err := packages_cleanup_service.CleanupExpiredData(ctx, time.Duration(0)); err != nil {
|
||||
ctx.ServerError("CleanupExpiredData", err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Flash.Success(ctx.Tr("admin.packages.cleanup.success"))
|
||||
ctx.Redirect(setting.AppSubURL + "/-/admin/packages")
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package admin
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"gitea.dev/modules/tailmsg"
|
||||
"gitea.dev/services/context"
|
||||
)
|
||||
|
||||
func PerfTrace(ctx *context.Context) {
|
||||
monitorTraceCommon(ctx)
|
||||
ctx.Data["PageIsAdminMonitorPerfTrace"] = true
|
||||
ctx.Data["PerfTraceRecords"] = tailmsg.GetManager().GetTraceRecorder().GetRecords()
|
||||
ctx.HTML(http.StatusOK, tplPerfTrace)
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package admin
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"gitea.dev/modules/queue"
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/services/context"
|
||||
)
|
||||
|
||||
func Queues(ctx *context.Context) {
|
||||
if !setting.IsProd {
|
||||
initTestQueueOnce()
|
||||
}
|
||||
ctx.Data["Title"] = ctx.Tr("admin.monitor.queues")
|
||||
ctx.Data["PageIsAdminMonitorQueue"] = true
|
||||
ctx.Data["Queues"] = queue.GetManager().ManagedQueues()
|
||||
ctx.HTML(http.StatusOK, tplQueue)
|
||||
}
|
||||
|
||||
// QueueManage shows details for a specific queue
|
||||
func QueueManage(ctx *context.Context) {
|
||||
qid := ctx.PathParamInt64("qid")
|
||||
mq := queue.GetManager().GetManagedQueue(qid)
|
||||
if mq == nil {
|
||||
ctx.Status(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
ctx.Data["Title"] = ctx.Tr("admin.monitor.queue", mq.GetName())
|
||||
ctx.Data["PageIsAdminMonitor"] = true
|
||||
ctx.Data["Queue"] = mq
|
||||
ctx.HTML(http.StatusOK, tplQueueManage)
|
||||
}
|
||||
|
||||
// QueueSet sets the maximum number of workers and other settings for this queue
|
||||
func QueueSet(ctx *context.Context) {
|
||||
qid := ctx.PathParamInt64("qid")
|
||||
mq := queue.GetManager().GetManagedQueue(qid)
|
||||
if mq == nil {
|
||||
ctx.Status(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
maxNumberStr := ctx.FormString("max-number")
|
||||
|
||||
var err error
|
||||
var maxNumber int
|
||||
if len(maxNumberStr) > 0 {
|
||||
maxNumber, err = strconv.Atoi(maxNumberStr)
|
||||
if err != nil {
|
||||
ctx.Flash.Error(ctx.Tr("admin.monitor.queue.settings.maxnumberworkers.error"))
|
||||
ctx.Redirect(setting.AppSubURL + "/-/admin/monitor/queue/" + strconv.FormatInt(qid, 10))
|
||||
return
|
||||
}
|
||||
if maxNumber < -1 {
|
||||
maxNumber = -1
|
||||
}
|
||||
} else {
|
||||
maxNumber = mq.GetWorkerMaxNumber()
|
||||
}
|
||||
|
||||
mq.SetWorkerMaxNumber(maxNumber)
|
||||
ctx.Flash.Success(ctx.Tr("admin.monitor.queue.settings.changed"))
|
||||
ctx.Redirect(setting.AppSubURL + "/-/admin/monitor/queue/" + strconv.FormatInt(qid, 10))
|
||||
}
|
||||
|
||||
func QueueRemoveAllItems(ctx *context.Context) {
|
||||
// Gitea's queue doesn't have transaction support
|
||||
// So in rare cases, the queue could be corrupted/out-of-sync
|
||||
// Site admin could remove all items from the queue to make it work again
|
||||
qid := ctx.PathParamInt64("qid")
|
||||
mq := queue.GetManager().GetManagedQueue(qid)
|
||||
if mq == nil {
|
||||
ctx.Status(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
if err := mq.RemoveAllItems(ctx); err != nil {
|
||||
ctx.ServerError("RemoveAllItems", err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Flash.Success(ctx.Tr("admin.monitor.queue.settings.remove_all_items_done"))
|
||||
ctx.Redirect(setting.AppSubURL + "/-/admin/monitor/queue/" + strconv.FormatInt(qid, 10))
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package admin
|
||||
|
||||
import (
|
||||
"runtime/pprof"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"gitea.dev/modules/graceful"
|
||||
"gitea.dev/modules/log"
|
||||
"gitea.dev/modules/process"
|
||||
"gitea.dev/modules/queue"
|
||||
"gitea.dev/modules/setting"
|
||||
)
|
||||
|
||||
var testQueueOnce sync.Once
|
||||
|
||||
// initTestQueueOnce initializes the test queue for dev mode
|
||||
// the test queue will also be shown in the queue list
|
||||
// developers could see the queue length / worker number / items number on the admin page and try to remove the items
|
||||
func initTestQueueOnce() {
|
||||
testQueueOnce.Do(func() {
|
||||
ctx, _, finished := process.GetManager().AddTypedContext(graceful.GetManager().ShutdownContext(), "TestQueue", process.SystemProcessType, false)
|
||||
qs := setting.QueueSettings{
|
||||
Name: "test-queue",
|
||||
Type: "channel",
|
||||
Length: 20,
|
||||
BatchLength: 2,
|
||||
MaxWorkers: 3,
|
||||
}
|
||||
testQueue, err := queue.NewWorkerPoolQueueWithContext(ctx, "test-queue", qs, func(t ...int64) (unhandled []int64) {
|
||||
for range t {
|
||||
select {
|
||||
case <-graceful.GetManager().ShutdownContext().Done():
|
||||
case <-time.After(5 * time.Second):
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}, true)
|
||||
if err != nil {
|
||||
log.Error("unable to create test queue: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
queue.GetManager().AddManagedQueue(testQueue)
|
||||
testQueue.SetWorkerMaxNumber(5)
|
||||
go graceful.GetManager().RunWithCancel(testQueue)
|
||||
go func() {
|
||||
pprof.SetGoroutineLabels(ctx)
|
||||
defer finished()
|
||||
|
||||
cnt := int64(0)
|
||||
adding := true
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
case <-time.After(500 * time.Millisecond):
|
||||
if adding {
|
||||
if testQueue.GetQueueItemNumber() == qs.Length {
|
||||
adding = false
|
||||
}
|
||||
} else {
|
||||
if testQueue.GetQueueItemNumber() == 0 {
|
||||
adding = true
|
||||
}
|
||||
}
|
||||
if adding {
|
||||
_ = testQueue.Push(cnt)
|
||||
cnt++
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
// Copyright 2014 The Gogs Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package admin
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"gitea.dev/models/db"
|
||||
repo_model "gitea.dev/models/repo"
|
||||
user_model "gitea.dev/models/user"
|
||||
"gitea.dev/modules/gitrepo"
|
||||
"gitea.dev/modules/log"
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/modules/templates"
|
||||
"gitea.dev/routers/web/explore"
|
||||
"gitea.dev/services/context"
|
||||
repo_service "gitea.dev/services/repository"
|
||||
)
|
||||
|
||||
const (
|
||||
tplRepos templates.TplName = "admin/repo/list"
|
||||
tplUnadoptedRepos templates.TplName = "admin/repo/unadopted"
|
||||
)
|
||||
|
||||
// Repos show all the repositories
|
||||
func Repos(ctx *context.Context) {
|
||||
ctx.Data["Title"] = ctx.Tr("admin.repositories")
|
||||
ctx.Data["PageIsAdminRepositories"] = true
|
||||
|
||||
explore.RenderRepoSearch(ctx, &explore.RepoSearchOptions{
|
||||
Private: true,
|
||||
PageSize: setting.UI.Admin.RepoPagingNum,
|
||||
TplName: tplRepos,
|
||||
OnlyShowRelevant: false,
|
||||
})
|
||||
}
|
||||
|
||||
// DeleteRepo delete one repository
|
||||
func DeleteRepo(ctx *context.Context) {
|
||||
repo, err := repo_model.GetRepositoryByID(ctx, ctx.FormInt64("id"))
|
||||
if err != nil {
|
||||
ctx.ServerError("GetRepositoryByID", err)
|
||||
return
|
||||
}
|
||||
|
||||
if ctx.Repo != nil && ctx.Repo.GitRepo != nil && ctx.Repo.Repository != nil && ctx.Repo.Repository.ID == repo.ID {
|
||||
ctx.Repo.GitRepo.Close()
|
||||
}
|
||||
|
||||
if err := repo_service.DeleteRepository(ctx, ctx.Doer, repo, true); err != nil {
|
||||
ctx.ServerError("DeleteRepository", err)
|
||||
return
|
||||
}
|
||||
log.Trace("Repository deleted: %s", repo.FullName())
|
||||
|
||||
ctx.Flash.Success(ctx.Tr("repo.settings.deletion_success"))
|
||||
ctx.JSONRedirect(setting.AppSubURL + "/-/admin/repos?page=" + url.QueryEscape(ctx.FormString("page")) + "&sort=" + url.QueryEscape(ctx.FormString("sort")))
|
||||
}
|
||||
|
||||
// UnadoptedRepos lists the unadopted repositories
|
||||
func UnadoptedRepos(ctx *context.Context) {
|
||||
ctx.Data["Title"] = ctx.Tr("admin.repositories")
|
||||
ctx.Data["PageIsAdminRepositories"] = true
|
||||
|
||||
opts := db.ListOptions{
|
||||
PageSize: setting.UI.Admin.UserPagingNum,
|
||||
Page: ctx.FormInt("page"),
|
||||
}
|
||||
|
||||
if opts.Page <= 0 {
|
||||
opts.Page = 1
|
||||
}
|
||||
|
||||
ctx.Data["CurrentPage"] = opts.Page
|
||||
|
||||
doSearch := ctx.FormBool("search")
|
||||
|
||||
ctx.Data["search"] = doSearch
|
||||
q := ctx.FormString("q")
|
||||
|
||||
if !doSearch {
|
||||
pager := context.NewPagination(0, opts.PageSize, opts.Page, 5)
|
||||
pager.AddParamFromRequest(ctx.Req)
|
||||
ctx.Data["Page"] = pager
|
||||
ctx.HTML(http.StatusOK, tplUnadoptedRepos)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Data["Keyword"] = q
|
||||
repoNames, count, err := repo_service.ListUnadoptedRepositories(ctx, q, &opts)
|
||||
if err != nil {
|
||||
ctx.ServerError("ListUnadoptedRepositories", err)
|
||||
return
|
||||
}
|
||||
ctx.Data["Dirs"] = repoNames
|
||||
pager := context.NewPagination(count, opts.PageSize, opts.Page, 5)
|
||||
pager.AddParamFromRequest(ctx.Req)
|
||||
ctx.Data["Page"] = pager
|
||||
ctx.HTML(http.StatusOK, tplUnadoptedRepos)
|
||||
}
|
||||
|
||||
// AdoptOrDeleteRepository adopts or deletes a repository
|
||||
func AdoptOrDeleteRepository(ctx *context.Context) {
|
||||
dir := ctx.FormString("id")
|
||||
action := ctx.FormString("action")
|
||||
page := ctx.FormString("page")
|
||||
q := ctx.FormString("q")
|
||||
|
||||
dirSplit := strings.SplitN(dir, "/", 2)
|
||||
if len(dirSplit) != 2 {
|
||||
ctx.Redirect(setting.AppSubURL + "/-/admin/repos")
|
||||
return
|
||||
}
|
||||
|
||||
ctxUser, err := user_model.GetUserByName(ctx, dirSplit[0])
|
||||
if err != nil {
|
||||
if user_model.IsErrUserNotExist(err) {
|
||||
log.Debug("User does not exist: %s", dirSplit[0])
|
||||
ctx.Redirect(setting.AppSubURL + "/-/admin/repos")
|
||||
return
|
||||
}
|
||||
ctx.ServerError("GetUserByName", err)
|
||||
return
|
||||
}
|
||||
|
||||
repoName := dirSplit[1]
|
||||
|
||||
// check not a repo
|
||||
has, err := repo_model.IsRepositoryModelExist(ctx, ctxUser, repoName)
|
||||
if err != nil {
|
||||
ctx.ServerError("IsRepositoryExist", err)
|
||||
return
|
||||
}
|
||||
exist, err := gitrepo.IsRepositoryExist(ctx, repo_model.StorageRepo(repo_model.RelativePath(ctxUser.Name, repoName)))
|
||||
if err != nil {
|
||||
ctx.ServerError("IsDir", err)
|
||||
return
|
||||
}
|
||||
if has || !exist {
|
||||
// Fallthrough to failure mode
|
||||
} else if action == "adopt" {
|
||||
if _, err := repo_service.AdoptRepository(ctx, ctx.Doer, ctxUser, repo_service.CreateRepoOptions{
|
||||
Name: dirSplit[1],
|
||||
IsPrivate: true,
|
||||
}); err != nil {
|
||||
ctx.ServerError("repository.AdoptRepository", err)
|
||||
return
|
||||
}
|
||||
ctx.Flash.Success(ctx.Tr("repo.adopt_preexisting_success", dir))
|
||||
} else if action == "delete" {
|
||||
if err := repo_service.DeleteUnadoptedRepository(ctx, ctx.Doer, ctxUser, dirSplit[1]); err != nil {
|
||||
ctx.ServerError("repository.AdoptRepository", err)
|
||||
return
|
||||
}
|
||||
ctx.Flash.Success(ctx.Tr("repo.delete_preexisting_success", dir))
|
||||
}
|
||||
ctx.Redirect(setting.AppSubURL + "/-/admin/repos/unadopted?search=true&q=" + url.QueryEscape(q) + "&page=" + url.QueryEscape(page))
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package admin
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"runtime"
|
||||
|
||||
"gitea.dev/modules/process"
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/services/context"
|
||||
)
|
||||
|
||||
func monitorTraceCommon(ctx *context.Context) {
|
||||
ctx.Data["Title"] = ctx.Tr("admin.monitor")
|
||||
ctx.Data["PageIsAdminMonitorTrace"] = true
|
||||
// Hide the performance trace tab in production, because it shows a lot of SQLs and is not that useful for end users.
|
||||
// To avoid confusing end users, do not let them know this tab. End users should "download diagnosis report" instead.
|
||||
ctx.Data["ShowAdminPerformanceTraceTab"] = !setting.IsProd
|
||||
}
|
||||
|
||||
// Stacktrace show admin monitor goroutines page
|
||||
func Stacktrace(ctx *context.Context) {
|
||||
monitorTraceCommon(ctx)
|
||||
|
||||
ctx.Data["GoroutineCount"] = runtime.NumGoroutine()
|
||||
|
||||
show := ctx.FormString("show")
|
||||
ctx.Data["ShowGoroutineList"] = show
|
||||
// by default, do not do anything which might cause server errors, to avoid unnecessary 500 pages.
|
||||
// this page is the entrance of the chance to collect diagnosis report.
|
||||
if show != "" {
|
||||
showNoSystem := show == "process"
|
||||
processStacks, processCount, _, err := process.GetManager().ProcessStacktraces(false, showNoSystem)
|
||||
if err != nil {
|
||||
ctx.ServerError("GoroutineStacktrace", err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Data["ProcessStacks"] = processStacks
|
||||
ctx.Data["ProcessCount"] = processCount
|
||||
}
|
||||
|
||||
ctx.HTML(http.StatusOK, tplStacktrace)
|
||||
}
|
||||
|
||||
// StacktraceCancel cancels a process
|
||||
func StacktraceCancel(ctx *context.Context) {
|
||||
pid := ctx.PathParam("pid")
|
||||
process.GetManager().Cancel(process.IDType(pid))
|
||||
ctx.JSONRedirect(setting.AppSubURL + "/-/admin/monitor/stacktrace")
|
||||
}
|
||||
@@ -0,0 +1,553 @@
|
||||
// Copyright 2014 The Gogs Authors. All rights reserved.
|
||||
// Copyright 2020 The Gitea Authors.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package admin
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"gitea.dev/models/auth"
|
||||
"gitea.dev/models/db"
|
||||
org_model "gitea.dev/models/organization"
|
||||
packages_model "gitea.dev/models/packages"
|
||||
repo_model "gitea.dev/models/repo"
|
||||
user_model "gitea.dev/models/user"
|
||||
"gitea.dev/modules/auth/password"
|
||||
"gitea.dev/modules/log"
|
||||
"gitea.dev/modules/optional"
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/modules/structs"
|
||||
"gitea.dev/modules/templates"
|
||||
"gitea.dev/modules/web"
|
||||
"gitea.dev/routers/web/explore"
|
||||
user_setting "gitea.dev/routers/web/user/setting"
|
||||
"gitea.dev/services/context"
|
||||
"gitea.dev/services/forms"
|
||||
"gitea.dev/services/mailer"
|
||||
user_service "gitea.dev/services/user"
|
||||
)
|
||||
|
||||
const (
|
||||
tplUsers templates.TplName = "admin/user/list"
|
||||
tplUserNew templates.TplName = "admin/user/new"
|
||||
tplUserView templates.TplName = "admin/user/view"
|
||||
tplUserEdit templates.TplName = "admin/user/edit"
|
||||
)
|
||||
|
||||
// UserSearchDefaultAdminSort is the default sort type for admin view
|
||||
const UserSearchDefaultAdminSort = "alphabetically"
|
||||
|
||||
// Users show all the users
|
||||
func Users(ctx *context.Context) {
|
||||
ctx.Data["Title"] = ctx.Tr("admin.users")
|
||||
ctx.Data["PageIsAdminUsers"] = true
|
||||
|
||||
statusFilterKeys := []string{"is_active", "is_admin", "is_restricted", "is_2fa_enabled", "is_prohibit_login"}
|
||||
statusFilterMap := map[string]string{}
|
||||
for _, filterKey := range statusFilterKeys {
|
||||
paramKey := "status_filter[" + filterKey + "]"
|
||||
paramVal := ctx.FormString(paramKey)
|
||||
statusFilterMap[filterKey] = paramVal
|
||||
}
|
||||
|
||||
sortType := ctx.FormString("sort")
|
||||
if sortType == "" {
|
||||
sortType = UserSearchDefaultAdminSort
|
||||
ctx.SetFormString("sort", sortType)
|
||||
}
|
||||
ctx.PageData["adminUserListSearchForm"] = map[string]any{
|
||||
"StatusFilterMap": statusFilterMap,
|
||||
"SortType": sortType,
|
||||
}
|
||||
|
||||
explore.RenderUserSearch(ctx, user_model.SearchUserOptions{
|
||||
Actor: ctx.Doer,
|
||||
Types: []user_model.UserType{user_model.UserTypeIndividual},
|
||||
ListOptions: db.ListOptions{
|
||||
PageSize: setting.UI.Admin.UserPagingNum,
|
||||
},
|
||||
SearchByEmail: true,
|
||||
IsActive: optional.ParseBool(statusFilterMap["is_active"]),
|
||||
IsAdmin: optional.ParseBool(statusFilterMap["is_admin"]),
|
||||
IsRestricted: optional.ParseBool(statusFilterMap["is_restricted"]),
|
||||
IsTwoFactorEnabled: optional.ParseBool(statusFilterMap["is_2fa_enabled"]),
|
||||
IsProhibitLogin: optional.ParseBool(statusFilterMap["is_prohibit_login"]),
|
||||
IncludeReserved: true, // administrator needs to list all accounts include reserved, bot, remote ones
|
||||
}, tplUsers)
|
||||
}
|
||||
|
||||
// NewUser render adding a new user page
|
||||
func NewUser(ctx *context.Context) {
|
||||
ctx.Data["Title"] = ctx.Tr("admin.users.new_account")
|
||||
ctx.Data["PageIsAdminUsers"] = true
|
||||
ctx.Data["DefaultUserVisibilityMode"] = setting.Service.DefaultUserVisibilityMode
|
||||
ctx.Data["AllowedUserVisibilityModes"] = setting.Service.AllowedUserVisibilityModesSlice.ToVisibleTypeSlice()
|
||||
|
||||
ctx.Data["login_type"] = "0-0"
|
||||
|
||||
sources, err := db.Find[auth.Source](ctx, auth.FindSourcesOptions{
|
||||
IsActive: optional.Some(true),
|
||||
})
|
||||
if err != nil {
|
||||
ctx.ServerError("auth.Sources", err)
|
||||
return
|
||||
}
|
||||
ctx.Data["Sources"] = sources
|
||||
|
||||
ctx.Data["CanSendEmail"] = setting.MailService != nil
|
||||
ctx.HTML(http.StatusOK, tplUserNew)
|
||||
}
|
||||
|
||||
// NewUserPost response for adding a new user
|
||||
func NewUserPost(ctx *context.Context) {
|
||||
form := web.GetForm(ctx).(*forms.AdminCreateUserForm)
|
||||
ctx.Data["Title"] = ctx.Tr("admin.users.new_account")
|
||||
ctx.Data["PageIsAdminUsers"] = true
|
||||
ctx.Data["DefaultUserVisibilityMode"] = setting.Service.DefaultUserVisibilityMode
|
||||
ctx.Data["AllowedUserVisibilityModes"] = setting.Service.AllowedUserVisibilityModesSlice.ToVisibleTypeSlice()
|
||||
|
||||
sources, err := db.Find[auth.Source](ctx, auth.FindSourcesOptions{
|
||||
IsActive: optional.Some(true),
|
||||
})
|
||||
if err != nil {
|
||||
ctx.ServerError("auth.Sources", err)
|
||||
return
|
||||
}
|
||||
ctx.Data["Sources"] = sources
|
||||
|
||||
ctx.Data["CanSendEmail"] = setting.MailService != nil
|
||||
|
||||
if ctx.HasError() {
|
||||
ctx.HTML(http.StatusOK, tplUserNew)
|
||||
return
|
||||
}
|
||||
|
||||
u := &user_model.User{
|
||||
Name: form.UserName,
|
||||
Email: form.Email,
|
||||
Passwd: form.Password,
|
||||
LoginType: auth.Plain,
|
||||
}
|
||||
|
||||
overwriteDefault := &user_model.CreateUserOverwriteOptions{
|
||||
IsActive: optional.Some(true),
|
||||
Visibility: &form.Visibility,
|
||||
}
|
||||
|
||||
if len(form.LoginType) > 0 {
|
||||
fields := strings.Split(form.LoginType, "-")
|
||||
if len(fields) == 2 {
|
||||
lType, _ := strconv.ParseInt(fields[0], 10, 0)
|
||||
u.LoginType = auth.Type(lType)
|
||||
u.LoginSource, _ = strconv.ParseInt(fields[1], 10, 64)
|
||||
u.LoginName = form.LoginName
|
||||
}
|
||||
}
|
||||
if u.LoginType == auth.NoType || u.LoginType == auth.Plain {
|
||||
if len(form.Password) < setting.MinPasswordLength {
|
||||
ctx.Data["Err_Password"] = true
|
||||
ctx.RenderWithErrDeprecated(ctx.Tr("auth.password_too_short", setting.MinPasswordLength), tplUserNew, &form)
|
||||
return
|
||||
}
|
||||
if !password.IsComplexEnough(form.Password) {
|
||||
ctx.Data["Err_Password"] = true
|
||||
ctx.RenderWithErrDeprecated(password.BuildComplexityError(ctx.Locale), tplUserNew, &form)
|
||||
return
|
||||
}
|
||||
if err := password.IsPwned(ctx, form.Password); err != nil {
|
||||
ctx.Data["Err_Password"] = true
|
||||
errMsg := ctx.Tr("auth.password_pwned", "https://haveibeenpwned.com/Passwords")
|
||||
if password.IsErrIsPwnedRequest(err) {
|
||||
log.Error(err.Error())
|
||||
errMsg = ctx.Tr("auth.password_pwned_err")
|
||||
}
|
||||
ctx.RenderWithErrDeprecated(errMsg, tplUserNew, &form)
|
||||
return
|
||||
}
|
||||
u.MustChangePassword = form.MustChangePassword
|
||||
}
|
||||
|
||||
if err := user_model.AdminCreateUser(ctx, u, &user_model.Meta{}, overwriteDefault); err != nil {
|
||||
switch {
|
||||
case user_model.IsErrUserAlreadyExist(err):
|
||||
ctx.Data["Err_UserName"] = true
|
||||
ctx.RenderWithErrDeprecated(ctx.Tr("form.username_been_taken"), tplUserNew, &form)
|
||||
case user_model.IsErrEmailAlreadyUsed(err):
|
||||
ctx.Data["Err_Email"] = true
|
||||
ctx.RenderWithErrDeprecated(ctx.Tr("form.email_been_used"), tplUserNew, &form)
|
||||
case user_model.IsErrEmailInvalid(err), user_model.IsErrEmailCharIsNotSupported(err):
|
||||
ctx.Data["Err_Email"] = true
|
||||
ctx.RenderWithErrDeprecated(ctx.Tr("form.email_invalid"), tplUserNew, &form)
|
||||
case db.IsErrNameReserved(err):
|
||||
ctx.Data["Err_UserName"] = true
|
||||
ctx.RenderWithErrDeprecated(ctx.Tr("user.form.name_reserved", err.(db.ErrNameReserved).Name), tplUserNew, &form)
|
||||
case db.IsErrNamePatternNotAllowed(err):
|
||||
ctx.Data["Err_UserName"] = true
|
||||
ctx.RenderWithErrDeprecated(ctx.Tr("user.form.name_pattern_not_allowed", err.(db.ErrNamePatternNotAllowed).Pattern), tplUserNew, &form)
|
||||
case db.IsErrNameCharsNotAllowed(err):
|
||||
ctx.Data["Err_UserName"] = true
|
||||
ctx.RenderWithErrDeprecated(ctx.Tr("user.form.name_chars_not_allowed", err.(db.ErrNameCharsNotAllowed).Name), tplUserNew, &form)
|
||||
default:
|
||||
ctx.ServerError("CreateUser", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if !user_model.IsEmailDomainAllowed(u.Email) {
|
||||
ctx.Flash.Warning(ctx.Tr("form.email_domain_is_not_allowed", u.Email))
|
||||
}
|
||||
|
||||
log.Trace("Account created by admin (%s): %s", ctx.Doer.Name, u.Name)
|
||||
|
||||
// Send email notification.
|
||||
if form.SendNotify {
|
||||
mailer.SendRegisterNotifyMail(u)
|
||||
}
|
||||
|
||||
ctx.Flash.Success(ctx.Tr("admin.users.new_success", u.Name))
|
||||
ctx.Redirect(setting.AppSubURL + "/-/admin/users/" + strconv.FormatInt(u.ID, 10))
|
||||
}
|
||||
|
||||
func prepareUserInfo(ctx *context.Context) *user_model.User {
|
||||
u, err := user_model.GetUserByID(ctx, ctx.PathParamInt64("userid"))
|
||||
if err != nil {
|
||||
if user_model.IsErrUserNotExist(err) {
|
||||
ctx.Redirect(setting.AppSubURL + "/-/admin/users")
|
||||
} else {
|
||||
ctx.ServerError("GetUserByID", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
ctx.Data["User"] = u
|
||||
|
||||
if u.LoginSource > 0 {
|
||||
ctx.Data["LoginSource"], err = auth.GetSourceByID(ctx, u.LoginSource)
|
||||
if err != nil {
|
||||
ctx.ServerError("auth.GetSourceByID", err)
|
||||
return nil
|
||||
}
|
||||
} else {
|
||||
ctx.Data["LoginSource"] = &auth.Source{}
|
||||
}
|
||||
|
||||
sources, err := db.Find[auth.Source](ctx, auth.FindSourcesOptions{})
|
||||
if err != nil {
|
||||
ctx.ServerError("auth.Sources", err)
|
||||
return nil
|
||||
}
|
||||
ctx.Data["Sources"] = sources
|
||||
|
||||
hasTOTP, err := auth.HasTwoFactorByUID(ctx, u.ID)
|
||||
if err != nil {
|
||||
ctx.ServerError("auth.HasTwoFactorByUID", err)
|
||||
return nil
|
||||
}
|
||||
hasWebAuthn, err := auth.HasWebAuthnRegistrationsByUID(ctx, u.ID)
|
||||
if err != nil {
|
||||
ctx.ServerError("auth.HasWebAuthnRegistrationsByUID", err)
|
||||
return nil
|
||||
}
|
||||
ctx.Data["TwoFactorEnabled"] = hasTOTP || hasWebAuthn
|
||||
|
||||
return u
|
||||
}
|
||||
|
||||
func ViewUser(ctx *context.Context) {
|
||||
ctx.Data["Title"] = ctx.Tr("admin.users.details")
|
||||
ctx.Data["PageIsAdminUsers"] = true
|
||||
ctx.Data["DisableRegularOrgCreation"] = setting.Admin.DisableRegularOrgCreation
|
||||
ctx.Data["DisableMigrations"] = setting.Repository.DisableMigrations
|
||||
ctx.Data["AllowedUserVisibilityModes"] = setting.Service.AllowedUserVisibilityModesSlice.ToVisibleTypeSlice()
|
||||
|
||||
u := prepareUserInfo(ctx)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
|
||||
repos, count, err := repo_model.SearchRepository(ctx, repo_model.SearchRepoOptions{
|
||||
ListOptions: db.ListOptionsAll,
|
||||
OwnerID: u.ID,
|
||||
OrderBy: db.SearchOrderByAlphabetically,
|
||||
Private: true,
|
||||
Collaborate: optional.Some(false),
|
||||
})
|
||||
if err != nil {
|
||||
ctx.ServerError("SearchRepository", err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Data["Repos"] = repos
|
||||
ctx.Data["ReposTotal"] = int(count)
|
||||
|
||||
emails, err := user_model.GetEmailAddresses(ctx, u.ID)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetEmailAddresses", err)
|
||||
return
|
||||
}
|
||||
ctx.Data["Emails"] = emails
|
||||
ctx.Data["EmailsTotal"] = len(emails)
|
||||
|
||||
orgs, err := db.Find[org_model.Organization](ctx, org_model.FindOrgOptions{
|
||||
ListOptions: db.ListOptionsAll,
|
||||
UserID: u.ID,
|
||||
IncludeVisibility: structs.VisibleTypePrivate,
|
||||
})
|
||||
if err != nil {
|
||||
ctx.ServerError("FindOrgs", err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Data["Users"] = orgs // needed to be able to use explore/user_list template
|
||||
ctx.Data["OrgsTotal"] = len(orgs)
|
||||
|
||||
ctx.HTML(http.StatusOK, tplUserView)
|
||||
}
|
||||
|
||||
func editUserCommon(ctx *context.Context) {
|
||||
ctx.Data["Title"] = ctx.Tr("admin.users.edit_account")
|
||||
ctx.Data["PageIsAdminUsers"] = true
|
||||
ctx.Data["DisableRegularOrgCreation"] = setting.Admin.DisableRegularOrgCreation
|
||||
ctx.Data["DisableMigrations"] = setting.Repository.DisableMigrations
|
||||
ctx.Data["DisableGitHooks"] = setting.DisableGitHooks
|
||||
ctx.Data["DisableImportLocal"] = !setting.ImportLocalPaths
|
||||
ctx.Data["AllowedUserVisibilityModes"] = setting.Service.AllowedUserVisibilityModesSlice.ToVisibleTypeSlice()
|
||||
ctx.Data["DisableGravatar"] = setting.Config().Picture.DisableGravatar.Value(ctx)
|
||||
}
|
||||
|
||||
// EditUser show editing user page
|
||||
func EditUser(ctx *context.Context) {
|
||||
editUserCommon(ctx)
|
||||
prepareUserInfo(ctx)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
|
||||
ctx.HTML(http.StatusOK, tplUserEdit)
|
||||
}
|
||||
|
||||
// EditUserPost response for editing user
|
||||
func EditUserPost(ctx *context.Context) {
|
||||
editUserCommon(ctx)
|
||||
u := prepareUserInfo(ctx)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
|
||||
form := web.GetForm(ctx).(*forms.AdminEditUserForm)
|
||||
if ctx.HasError() {
|
||||
ctx.HTML(http.StatusOK, tplUserEdit)
|
||||
return
|
||||
}
|
||||
|
||||
if form.UserName != "" {
|
||||
if err := user_service.RenameUser(ctx, u, form.UserName, ctx.Doer); err != nil {
|
||||
switch {
|
||||
case user_model.IsErrUserIsNotLocal(err):
|
||||
ctx.Data["Err_UserName"] = true
|
||||
ctx.RenderWithErrDeprecated(ctx.Tr("form.username_change_not_local_user"), tplUserEdit, &form)
|
||||
case user_model.IsErrUserAlreadyExist(err):
|
||||
ctx.Data["Err_UserName"] = true
|
||||
ctx.RenderWithErrDeprecated(ctx.Tr("form.username_been_taken"), tplUserEdit, &form)
|
||||
case db.IsErrNameReserved(err):
|
||||
ctx.Data["Err_UserName"] = true
|
||||
ctx.RenderWithErrDeprecated(ctx.Tr("user.form.name_reserved", form.UserName), tplUserEdit, &form)
|
||||
case db.IsErrNamePatternNotAllowed(err):
|
||||
ctx.Data["Err_UserName"] = true
|
||||
ctx.RenderWithErrDeprecated(ctx.Tr("user.form.name_pattern_not_allowed", form.UserName), tplUserEdit, &form)
|
||||
case db.IsErrNameCharsNotAllowed(err):
|
||||
ctx.Data["Err_UserName"] = true
|
||||
ctx.RenderWithErrDeprecated(ctx.Tr("user.form.name_chars_not_allowed", form.UserName), tplUserEdit, &form)
|
||||
default:
|
||||
ctx.ServerError("RenameUser", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
authOpts := &user_service.UpdateAuthOptions{
|
||||
Password: optional.FromNonDefault(form.Password),
|
||||
LoginName: optional.Some(form.LoginName),
|
||||
}
|
||||
|
||||
// skip self Prohibit Login
|
||||
if ctx.Doer.ID == u.ID {
|
||||
authOpts.ProhibitLogin = optional.Some(false)
|
||||
} else {
|
||||
authOpts.ProhibitLogin = optional.Some(form.ProhibitLogin)
|
||||
}
|
||||
|
||||
fields := strings.Split(form.LoginType, "-")
|
||||
if len(fields) == 2 {
|
||||
authSource, _ := strconv.ParseInt(fields[1], 10, 64)
|
||||
|
||||
authOpts.LoginSource = optional.Some(authSource)
|
||||
}
|
||||
|
||||
if err := user_service.UpdateAuth(ctx, u, authOpts); err != nil {
|
||||
switch {
|
||||
case errors.Is(err, password.ErrMinLength):
|
||||
ctx.Data["Err_Password"] = true
|
||||
ctx.RenderWithErrDeprecated(ctx.Tr("auth.password_too_short", setting.MinPasswordLength), tplUserEdit, &form)
|
||||
case errors.Is(err, password.ErrComplexity):
|
||||
ctx.Data["Err_Password"] = true
|
||||
ctx.RenderWithErrDeprecated(password.BuildComplexityError(ctx.Locale), tplUserEdit, &form)
|
||||
case errors.Is(err, password.ErrIsPwned):
|
||||
ctx.Data["Err_Password"] = true
|
||||
ctx.RenderWithErrDeprecated(ctx.Tr("auth.password_pwned", "https://haveibeenpwned.com/Passwords"), tplUserEdit, &form)
|
||||
case password.IsErrIsPwnedRequest(err):
|
||||
ctx.Data["Err_Password"] = true
|
||||
ctx.RenderWithErrDeprecated(ctx.Tr("auth.password_pwned_err"), tplUserEdit, &form)
|
||||
default:
|
||||
ctx.ServerError("UpdateUser", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if form.Email != "" {
|
||||
if err := user_service.ReplacePrimaryEmailAddress(ctx, u, form.Email); err != nil {
|
||||
switch {
|
||||
case user_model.IsErrEmailCharIsNotSupported(err), user_model.IsErrEmailInvalid(err):
|
||||
ctx.Data["Err_Email"] = true
|
||||
ctx.RenderWithErrDeprecated(ctx.Tr("form.email_invalid"), tplUserEdit, &form)
|
||||
case user_model.IsErrEmailAlreadyUsed(err):
|
||||
ctx.Data["Err_Email"] = true
|
||||
ctx.RenderWithErrDeprecated(ctx.Tr("form.email_been_used"), tplUserEdit, &form)
|
||||
default:
|
||||
ctx.ServerError("AddOrSetPrimaryEmailAddress", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
if !user_model.IsEmailDomainAllowed(form.Email) {
|
||||
ctx.Flash.Warning(ctx.Tr("form.email_domain_is_not_allowed", form.Email))
|
||||
}
|
||||
}
|
||||
|
||||
opts := &user_service.UpdateOptions{
|
||||
FullName: optional.Some(form.FullName),
|
||||
Website: optional.Some(form.Website),
|
||||
Location: optional.Some(form.Location),
|
||||
IsActive: optional.Some(form.Active),
|
||||
IsAdmin: user_service.UpdateOptionFieldFromValue(form.Admin),
|
||||
AllowGitHook: optional.Some(form.AllowGitHook),
|
||||
AllowImportLocal: optional.Some(form.AllowImportLocal),
|
||||
MaxRepoCreation: optional.Some(form.MaxRepoCreation),
|
||||
AllowCreateOrganization: optional.Some(form.AllowCreateOrganization),
|
||||
IsRestricted: optional.Some(form.Restricted),
|
||||
Visibility: optional.Some(form.Visibility),
|
||||
Language: optional.Some(form.Language),
|
||||
}
|
||||
|
||||
if err := user_service.UpdateUser(ctx, u, opts); err != nil {
|
||||
if user_model.IsErrDeleteLastAdminUser(err) {
|
||||
ctx.RenderWithErrDeprecated(ctx.Tr("auth.last_admin"), tplUserEdit, &form)
|
||||
} else {
|
||||
ctx.ServerError("UpdateUser", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
log.Trace("Account profile updated by admin (%s): %s", ctx.Doer.Name, u.Name)
|
||||
|
||||
if form.Reset2FA {
|
||||
tf, err := auth.GetTwoFactorByUID(ctx, u.ID)
|
||||
if err != nil && !auth.IsErrTwoFactorNotEnrolled(err) {
|
||||
ctx.ServerError("auth.GetTwoFactorByUID", err)
|
||||
return
|
||||
} else if tf != nil {
|
||||
if err := auth.DeleteTwoFactorByID(ctx, tf.ID, u.ID); err != nil {
|
||||
ctx.ServerError("auth.DeleteTwoFactorByID", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
wn, err := auth.GetWebAuthnCredentialsByUID(ctx, u.ID)
|
||||
if err != nil {
|
||||
ctx.ServerError("auth.GetTwoFactorByUID", err)
|
||||
return
|
||||
}
|
||||
for _, cred := range wn {
|
||||
if _, err := auth.DeleteCredential(ctx, cred.ID, u.ID); err != nil {
|
||||
ctx.ServerError("auth.DeleteCredential", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ctx.Flash.Success(ctx.Tr("admin.users.update_profile_success"))
|
||||
ctx.Redirect(setting.AppSubURL + "/-/admin/users/" + url.PathEscape(ctx.PathParam("userid")))
|
||||
}
|
||||
|
||||
// DeleteUser response for deleting a user
|
||||
func DeleteUser(ctx *context.Context) {
|
||||
u, err := user_model.GetUserByID(ctx, ctx.PathParamInt64("userid"))
|
||||
if err != nil {
|
||||
ctx.ServerError("GetUserByID", err)
|
||||
return
|
||||
}
|
||||
|
||||
// admin should not delete themself
|
||||
if u.ID == ctx.Doer.ID {
|
||||
ctx.Flash.Error(ctx.Tr("admin.users.cannot_delete_self"))
|
||||
ctx.Redirect(setting.AppSubURL + "/-/admin/users/" + url.PathEscape(ctx.PathParam("userid")))
|
||||
return
|
||||
}
|
||||
|
||||
if err = user_service.DeleteUser(ctx, u, ctx.FormBool("purge")); err != nil {
|
||||
switch {
|
||||
case repo_model.IsErrUserOwnRepos(err):
|
||||
ctx.Flash.Error(ctx.Tr("admin.users.still_own_repo"))
|
||||
ctx.Redirect(setting.AppSubURL + "/-/admin/users/" + url.PathEscape(ctx.PathParam("userid")))
|
||||
case org_model.IsErrUserHasOrgs(err):
|
||||
ctx.Flash.Error(ctx.Tr("admin.users.still_has_org"))
|
||||
ctx.Redirect(setting.AppSubURL + "/-/admin/users/" + url.PathEscape(ctx.PathParam("userid")))
|
||||
case packages_model.IsErrUserOwnPackages(err):
|
||||
ctx.Flash.Error(ctx.Tr("admin.users.still_own_packages"))
|
||||
ctx.Redirect(setting.AppSubURL + "/-/admin/users/" + url.PathEscape(ctx.PathParam("userid")))
|
||||
case user_model.IsErrDeleteLastAdminUser(err):
|
||||
ctx.Flash.Error(ctx.Tr("auth.last_admin"))
|
||||
ctx.Redirect(setting.AppSubURL + "/-/admin/users/" + url.PathEscape(ctx.PathParam("userid")))
|
||||
default:
|
||||
ctx.ServerError("DeleteUser", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
log.Trace("Account deleted by admin (%s): %s", ctx.Doer.Name, u.Name)
|
||||
|
||||
ctx.Flash.Success(ctx.Tr("admin.users.deletion_success"))
|
||||
ctx.Redirect(setting.AppSubURL + "/-/admin/users")
|
||||
}
|
||||
|
||||
// AvatarPost response for change user's avatar request
|
||||
func AvatarPost(ctx *context.Context) {
|
||||
u := prepareUserInfo(ctx)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
|
||||
form := web.GetForm(ctx).(*forms.AvatarForm)
|
||||
if err := user_setting.UpdateAvatarSetting(ctx, form, u); err != nil {
|
||||
ctx.Flash.Error(err.Error())
|
||||
} else {
|
||||
ctx.Flash.Success(ctx.Tr("settings.update_user_avatar_success"))
|
||||
}
|
||||
|
||||
ctx.Redirect(setting.AppSubURL + "/-/admin/users/" + strconv.FormatInt(u.ID, 10))
|
||||
}
|
||||
|
||||
// DeleteAvatar render delete avatar page
|
||||
func DeleteAvatar(ctx *context.Context) {
|
||||
u := prepareUserInfo(ctx)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
|
||||
if err := user_service.DeleteAvatar(ctx, u); err != nil {
|
||||
ctx.Flash.Error(err.Error())
|
||||
}
|
||||
|
||||
ctx.JSONRedirect(setting.AppSubURL + "/-/admin/users/" + strconv.FormatInt(u.ID, 10))
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
// Copyright 2017 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package admin
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"gitea.dev/models/unittest"
|
||||
user_model "gitea.dev/models/user"
|
||||
"gitea.dev/modules/setting"
|
||||
api "gitea.dev/modules/structs"
|
||||
"gitea.dev/modules/web"
|
||||
"gitea.dev/services/contexttest"
|
||||
"gitea.dev/services/forms"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestNewUserPost_MustChangePassword(t *testing.T) {
|
||||
unittest.PrepareTestEnv(t)
|
||||
ctx, _ := contexttest.MockContext(t, "admin/users/new")
|
||||
|
||||
u := unittest.AssertExistsAndLoadBean(t, &user_model.User{
|
||||
IsAdmin: true,
|
||||
ID: 2,
|
||||
})
|
||||
|
||||
ctx.Doer = u
|
||||
|
||||
username := "gitea"
|
||||
email := "gitea@gitea.io"
|
||||
|
||||
form := forms.AdminCreateUserForm{
|
||||
LoginType: "local",
|
||||
LoginName: "local",
|
||||
UserName: username,
|
||||
Email: email,
|
||||
Password: "abc123ABC!=$",
|
||||
SendNotify: false,
|
||||
MustChangePassword: true,
|
||||
}
|
||||
|
||||
web.SetForm(ctx, &form)
|
||||
NewUserPost(ctx)
|
||||
|
||||
assert.NotEmpty(t, ctx.Flash.SuccessMsg)
|
||||
|
||||
u, err := user_model.GetUserByName(ctx, username)
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, username, u.Name)
|
||||
assert.Equal(t, email, u.Email)
|
||||
assert.True(t, u.MustChangePassword)
|
||||
}
|
||||
|
||||
func TestNewUserPost_MustChangePasswordFalse(t *testing.T) {
|
||||
unittest.PrepareTestEnv(t)
|
||||
ctx, _ := contexttest.MockContext(t, "admin/users/new")
|
||||
|
||||
u := unittest.AssertExistsAndLoadBean(t, &user_model.User{
|
||||
IsAdmin: true,
|
||||
ID: 2,
|
||||
})
|
||||
|
||||
ctx.Doer = u
|
||||
|
||||
username := "gitea"
|
||||
email := "gitea@gitea.io"
|
||||
|
||||
form := forms.AdminCreateUserForm{
|
||||
LoginType: "local",
|
||||
LoginName: "local",
|
||||
UserName: username,
|
||||
Email: email,
|
||||
Password: "abc123ABC!=$",
|
||||
SendNotify: false,
|
||||
MustChangePassword: false,
|
||||
}
|
||||
|
||||
web.SetForm(ctx, &form)
|
||||
NewUserPost(ctx)
|
||||
|
||||
assert.NotEmpty(t, ctx.Flash.SuccessMsg)
|
||||
|
||||
u, err := user_model.GetUserByName(ctx, username)
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, username, u.Name)
|
||||
assert.Equal(t, email, u.Email)
|
||||
assert.False(t, u.MustChangePassword)
|
||||
}
|
||||
|
||||
func TestNewUserPost_InvalidEmail(t *testing.T) {
|
||||
unittest.PrepareTestEnv(t)
|
||||
ctx, _ := contexttest.MockContext(t, "admin/users/new")
|
||||
|
||||
u := unittest.AssertExistsAndLoadBean(t, &user_model.User{
|
||||
IsAdmin: true,
|
||||
ID: 2,
|
||||
})
|
||||
|
||||
ctx.Doer = u
|
||||
|
||||
username := "gitea"
|
||||
email := "gitea@gitea.io\r\n"
|
||||
|
||||
form := forms.AdminCreateUserForm{
|
||||
LoginType: "local",
|
||||
LoginName: "local",
|
||||
UserName: username,
|
||||
Email: email,
|
||||
Password: "abc123ABC!=$",
|
||||
SendNotify: false,
|
||||
MustChangePassword: false,
|
||||
}
|
||||
|
||||
web.SetForm(ctx, &form)
|
||||
NewUserPost(ctx)
|
||||
|
||||
assert.NotEmpty(t, ctx.Flash.ErrorMsg)
|
||||
}
|
||||
|
||||
func TestNewUserPost_VisibilityDefaultPublic(t *testing.T) {
|
||||
unittest.PrepareTestEnv(t)
|
||||
ctx, _ := contexttest.MockContext(t, "admin/users/new")
|
||||
|
||||
u := unittest.AssertExistsAndLoadBean(t, &user_model.User{
|
||||
IsAdmin: true,
|
||||
ID: 2,
|
||||
})
|
||||
|
||||
ctx.Doer = u
|
||||
|
||||
username := "gitea"
|
||||
email := "gitea@gitea.io"
|
||||
|
||||
form := forms.AdminCreateUserForm{
|
||||
LoginType: "local",
|
||||
LoginName: "local",
|
||||
UserName: username,
|
||||
Email: email,
|
||||
Password: "abc123ABC!=$",
|
||||
SendNotify: false,
|
||||
MustChangePassword: false,
|
||||
}
|
||||
|
||||
web.SetForm(ctx, &form)
|
||||
NewUserPost(ctx)
|
||||
|
||||
assert.NotEmpty(t, ctx.Flash.SuccessMsg)
|
||||
|
||||
u, err := user_model.GetUserByName(ctx, username)
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, username, u.Name)
|
||||
assert.Equal(t, email, u.Email)
|
||||
// As default user visibility
|
||||
assert.Equal(t, setting.Service.DefaultUserVisibilityMode, u.Visibility)
|
||||
}
|
||||
|
||||
func TestNewUserPost_VisibilityPrivate(t *testing.T) {
|
||||
unittest.PrepareTestEnv(t)
|
||||
ctx, _ := contexttest.MockContext(t, "admin/users/new")
|
||||
|
||||
u := unittest.AssertExistsAndLoadBean(t, &user_model.User{
|
||||
IsAdmin: true,
|
||||
ID: 2,
|
||||
})
|
||||
|
||||
ctx.Doer = u
|
||||
|
||||
username := "gitea"
|
||||
email := "gitea@gitea.io"
|
||||
|
||||
form := forms.AdminCreateUserForm{
|
||||
LoginType: "local",
|
||||
LoginName: "local",
|
||||
UserName: username,
|
||||
Email: email,
|
||||
Password: "abc123ABC!=$",
|
||||
SendNotify: false,
|
||||
MustChangePassword: false,
|
||||
Visibility: api.VisibleTypePrivate,
|
||||
}
|
||||
|
||||
web.SetForm(ctx, &form)
|
||||
NewUserPost(ctx)
|
||||
|
||||
assert.NotEmpty(t, ctx.Flash.SuccessMsg)
|
||||
|
||||
u, err := user_model.GetUserByName(ctx, username)
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, username, u.Name)
|
||||
assert.Equal(t, email, u.Email)
|
||||
// As default user visibility
|
||||
assert.True(t, u.Visibility.IsPrivate())
|
||||
}
|
||||
Reference in New Issue
Block a user