mirror of
https://github.com/go-gitea/gitea
synced 2025-07-22 18:28:37 +00:00
Refactor routers directory (#15800)
* refactor routers directory * move func used for web and api to common * make corsHandler a function to prohibit side efects * rm unused func Co-authored-by: 6543 <6543@obermui.de>
This commit is contained in:
485
routers/web/admin/admin.go
Normal file
485
routers/web/admin/admin.go
Normal file
@@ -0,0 +1,485 @@
|
||||
// Copyright 2014 The Gogs Authors. All rights reserved.
|
||||
// Copyright 2019 The Gitea Authors. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package admin
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/gitea/models"
|
||||
"code.gitea.io/gitea/modules/base"
|
||||
"code.gitea.io/gitea/modules/context"
|
||||
"code.gitea.io/gitea/modules/cron"
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/process"
|
||||
"code.gitea.io/gitea/modules/queue"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
"code.gitea.io/gitea/modules/web"
|
||||
"code.gitea.io/gitea/services/forms"
|
||||
"code.gitea.io/gitea/services/mailer"
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
|
||||
"gitea.com/go-chi/session"
|
||||
)
|
||||
|
||||
const (
|
||||
tplDashboard base.TplName = "admin/dashboard"
|
||||
tplConfig base.TplName = "admin/config"
|
||||
tplMonitor base.TplName = "admin/monitor"
|
||||
tplQueue base.TplName = "admin/queue"
|
||||
)
|
||||
|
||||
var sysStatus struct {
|
||||
Uptime 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)
|
||||
LastGC string // last run in absolute time (ns)
|
||||
PauseTotalNs string
|
||||
PauseNs string // circular buffer of recent GC pause times, most recent at [(NumGC+255)%256]
|
||||
NumGC uint32
|
||||
}
|
||||
|
||||
func updateSystemStatus() {
|
||||
sysStatus.Uptime = timeutil.TimeSincePro(setting.AppStartTime, "en")
|
||||
|
||||
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.LastGC = fmt.Sprintf("%.1fs", float64(time.Now().UnixNano()-int64(m.LastGC))/1000/1000/1000)
|
||||
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
|
||||
}
|
||||
|
||||
// Dashboard show admin panel dashboard
|
||||
func Dashboard(ctx *context.Context) {
|
||||
ctx.Data["Title"] = ctx.Tr("admin.dashboard")
|
||||
ctx.Data["PageIsAdmin"] = true
|
||||
ctx.Data["PageIsAdminDashboard"] = true
|
||||
ctx.Data["Stats"] = models.GetStatistic()
|
||||
// FIXME: update periodically
|
||||
updateSystemStatus()
|
||||
ctx.Data["SysStatus"] = sysStatus
|
||||
ctx.Data["SSH"] = setting.SSH
|
||||
ctx.HTML(http.StatusOK, tplDashboard)
|
||||
}
|
||||
|
||||
// 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["PageIsAdmin"] = true
|
||||
ctx.Data["PageIsAdminDashboard"] = true
|
||||
ctx.Data["Stats"] = models.GetStatistic()
|
||||
updateSystemStatus()
|
||||
ctx.Data["SysStatus"] = sysStatus
|
||||
|
||||
// Run operation.
|
||||
if form.Op != "" {
|
||||
task := cron.GetTask(form.Op)
|
||||
if task != nil {
|
||||
go task.RunWithUser(ctx.User, 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")
|
||||
} else {
|
||||
ctx.Redirect(setting.AppSubURL + "/admin")
|
||||
}
|
||||
}
|
||||
|
||||
// SendTestMail send test mail to confirm mail service is OK
|
||||
func SendTestMail(ctx *context.Context) {
|
||||
email := ctx.Query("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")
|
||||
}
|
||||
|
||||
func shadowPasswordKV(cfgItem, splitter string) string {
|
||||
fields := strings.Split(cfgItem, splitter)
|
||||
for i := 0; i < len(fields); i++ {
|
||||
if strings.HasPrefix(fields[i], "password=") {
|
||||
fields[i] = "password=******"
|
||||
break
|
||||
}
|
||||
}
|
||||
return strings.Join(fields, splitter)
|
||||
}
|
||||
|
||||
func shadowURL(provider, cfgItem string) string {
|
||||
u, err := url.Parse(cfgItem)
|
||||
if err != nil {
|
||||
log.Error("Shadowing Password for %v failed: %v", provider, err)
|
||||
return cfgItem
|
||||
}
|
||||
if u.User != nil {
|
||||
atIdx := strings.Index(cfgItem, "@")
|
||||
if atIdx > 0 {
|
||||
colonIdx := strings.LastIndex(cfgItem[:atIdx], ":")
|
||||
if colonIdx > 0 {
|
||||
return cfgItem[:colonIdx+1] + "******" + cfgItem[atIdx:]
|
||||
}
|
||||
}
|
||||
}
|
||||
return cfgItem
|
||||
}
|
||||
|
||||
func shadowPassword(provider, cfgItem string) string {
|
||||
switch provider {
|
||||
case "redis":
|
||||
return shadowPasswordKV(cfgItem, ",")
|
||||
case "mysql":
|
||||
//root:@tcp(localhost:3306)/macaron?charset=utf8
|
||||
atIdx := strings.Index(cfgItem, "@")
|
||||
if atIdx > 0 {
|
||||
colonIdx := strings.Index(cfgItem[:atIdx], ":")
|
||||
if colonIdx > 0 {
|
||||
return cfgItem[:colonIdx+1] + "******" + cfgItem[atIdx:]
|
||||
}
|
||||
}
|
||||
return cfgItem
|
||||
case "postgres":
|
||||
// user=jiahuachen dbname=macaron port=5432 sslmode=disable
|
||||
if !strings.HasPrefix(cfgItem, "postgres://") {
|
||||
return shadowPasswordKV(cfgItem, " ")
|
||||
}
|
||||
fallthrough
|
||||
case "couchbase":
|
||||
return shadowURL(provider, cfgItem)
|
||||
// postgres://pqgotest:password@localhost/pqgotest?sslmode=verify-full
|
||||
// Notice: use shadowURL
|
||||
}
|
||||
return cfgItem
|
||||
}
|
||||
|
||||
// Config show admin config page
|
||||
func Config(ctx *context.Context) {
|
||||
ctx.Data["Title"] = ctx.Tr("admin.config")
|
||||
ctx.Data["PageIsAdmin"] = true
|
||||
ctx.Data["PageIsAdminConfig"] = true
|
||||
|
||||
ctx.Data["CustomConf"] = setting.CustomConf
|
||||
ctx.Data["AppUrl"] = setting.AppURL
|
||||
ctx.Data["Domain"] = setting.Domain
|
||||
ctx.Data["OfflineMode"] = setting.OfflineMode
|
||||
ctx.Data["DisableRouterLog"] = setting.DisableRouterLog
|
||||
ctx.Data["RunUser"] = setting.RunUser
|
||||
ctx.Data["RunMode"] = strings.Title(setting.RunMode)
|
||||
if version, err := git.LocalVersion(); err == nil {
|
||||
ctx.Data["GitVersion"] = version.Original()
|
||||
}
|
||||
ctx.Data["RepoRootPath"] = setting.RepoRootPath
|
||||
ctx.Data["CustomRootPath"] = setting.CustomPath
|
||||
ctx.Data["StaticRootPath"] = setting.StaticRootPath
|
||||
ctx.Data["LogRootPath"] = setting.LogRootPath
|
||||
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["CacheConn"] = shadowPassword(setting.CacheService.Adapter, setting.CacheService.Conn)
|
||||
ctx.Data["CacheItemTTL"] = setting.CacheService.TTL
|
||||
|
||||
sessionCfg := setting.SessionConfig
|
||||
if sessionCfg.Provider == "VirtualSession" {
|
||||
var realSession session.Options
|
||||
json := jsoniter.ConfigCompatibleWithStandardLibrary
|
||||
if err := json.Unmarshal([]byte(sessionCfg.ProviderConfig), &realSession); err != nil {
|
||||
log.Error("Unable to unmarshall session config for virtualed 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 = shadowPassword(sessionCfg.Provider, sessionCfg.ProviderConfig)
|
||||
ctx.Data["SessionConfig"] = sessionCfg
|
||||
|
||||
ctx.Data["DisableGravatar"] = setting.DisableGravatar
|
||||
ctx.Data["EnableFederatedAvatar"] = setting.EnableFederatedAvatar
|
||||
|
||||
ctx.Data["Git"] = setting.Git
|
||||
|
||||
type envVar struct {
|
||||
Name, Value string
|
||||
}
|
||||
|
||||
envVars := map[string]*envVar{}
|
||||
if len(os.Getenv("GITEA_WORK_DIR")) > 0 {
|
||||
envVars["GITEA_WORK_DIR"] = &envVar{"GITEA_WORK_DIR", os.Getenv("GITEA_WORK_DIR")}
|
||||
}
|
||||
if len(os.Getenv("GITEA_CUSTOM")) > 0 {
|
||||
envVars["GITEA_CUSTOM"] = &envVar{"GITEA_CUSTOM", os.Getenv("GITEA_CUSTOM")}
|
||||
}
|
||||
|
||||
ctx.Data["EnvVars"] = envVars
|
||||
ctx.Data["Loggers"] = setting.GetLogDescriptions()
|
||||
ctx.Data["EnableAccessLog"] = setting.EnableAccessLog
|
||||
ctx.Data["AccessLogTemplate"] = setting.AccessLogTemplate
|
||||
ctx.Data["DisableRouterLog"] = setting.DisableRouterLog
|
||||
ctx.Data["EnableXORMLog"] = setting.EnableXORMLog
|
||||
ctx.Data["LogSQL"] = setting.Database.LogSQL
|
||||
|
||||
ctx.HTML(http.StatusOK, tplConfig)
|
||||
}
|
||||
|
||||
// Monitor show admin monitor page
|
||||
func Monitor(ctx *context.Context) {
|
||||
ctx.Data["Title"] = ctx.Tr("admin.monitor")
|
||||
ctx.Data["PageIsAdmin"] = true
|
||||
ctx.Data["PageIsAdminMonitor"] = true
|
||||
ctx.Data["Processes"] = process.GetManager().Processes()
|
||||
ctx.Data["Entries"] = cron.ListTasks()
|
||||
ctx.Data["Queues"] = queue.GetManager().ManagedQueues()
|
||||
ctx.HTML(http.StatusOK, tplMonitor)
|
||||
}
|
||||
|
||||
// MonitorCancel cancels a process
|
||||
func MonitorCancel(ctx *context.Context) {
|
||||
pid := ctx.ParamsInt64("pid")
|
||||
process.GetManager().Cancel(pid)
|
||||
ctx.JSON(http.StatusOK, map[string]interface{}{
|
||||
"redirect": setting.AppSubURL + "/admin/monitor",
|
||||
})
|
||||
}
|
||||
|
||||
// Queue shows details for a specific queue
|
||||
func Queue(ctx *context.Context) {
|
||||
qid := ctx.ParamsInt64("qid")
|
||||
mq := queue.GetManager().GetManagedQueue(qid)
|
||||
if mq == nil {
|
||||
ctx.Status(404)
|
||||
return
|
||||
}
|
||||
ctx.Data["Title"] = ctx.Tr("admin.monitor.queue", mq.Name)
|
||||
ctx.Data["PageIsAdmin"] = true
|
||||
ctx.Data["PageIsAdminMonitor"] = true
|
||||
ctx.Data["Queue"] = mq
|
||||
ctx.HTML(http.StatusOK, tplQueue)
|
||||
}
|
||||
|
||||
// WorkerCancel cancels a worker group
|
||||
func WorkerCancel(ctx *context.Context) {
|
||||
qid := ctx.ParamsInt64("qid")
|
||||
mq := queue.GetManager().GetManagedQueue(qid)
|
||||
if mq == nil {
|
||||
ctx.Status(404)
|
||||
return
|
||||
}
|
||||
pid := ctx.ParamsInt64("pid")
|
||||
mq.CancelWorkers(pid)
|
||||
ctx.Flash.Info(ctx.Tr("admin.monitor.queue.pool.cancelling"))
|
||||
ctx.JSON(http.StatusOK, map[string]interface{}{
|
||||
"redirect": setting.AppSubURL + "/admin/monitor/queue/" + strconv.FormatInt(qid, 10),
|
||||
})
|
||||
}
|
||||
|
||||
// Flush flushes a queue
|
||||
func Flush(ctx *context.Context) {
|
||||
qid := ctx.ParamsInt64("qid")
|
||||
mq := queue.GetManager().GetManagedQueue(qid)
|
||||
if mq == nil {
|
||||
ctx.Status(404)
|
||||
return
|
||||
}
|
||||
timeout, err := time.ParseDuration(ctx.Query("timeout"))
|
||||
if err != nil {
|
||||
timeout = -1
|
||||
}
|
||||
ctx.Flash.Info(ctx.Tr("admin.monitor.queue.pool.flush.added", mq.Name))
|
||||
go func() {
|
||||
err := mq.Flush(timeout)
|
||||
if err != nil {
|
||||
log.Error("Flushing failure for %s: Error %v", mq.Name, err)
|
||||
}
|
||||
}()
|
||||
ctx.Redirect(setting.AppSubURL + "/admin/monitor/queue/" + strconv.FormatInt(qid, 10))
|
||||
}
|
||||
|
||||
// AddWorkers adds workers to a worker group
|
||||
func AddWorkers(ctx *context.Context) {
|
||||
qid := ctx.ParamsInt64("qid")
|
||||
mq := queue.GetManager().GetManagedQueue(qid)
|
||||
if mq == nil {
|
||||
ctx.Status(404)
|
||||
return
|
||||
}
|
||||
number := ctx.QueryInt("number")
|
||||
if number < 1 {
|
||||
ctx.Flash.Error(ctx.Tr("admin.monitor.queue.pool.addworkers.mustnumbergreaterzero"))
|
||||
ctx.Redirect(setting.AppSubURL + "/admin/monitor/queue/" + strconv.FormatInt(qid, 10))
|
||||
return
|
||||
}
|
||||
timeout, err := time.ParseDuration(ctx.Query("timeout"))
|
||||
if err != nil {
|
||||
ctx.Flash.Error(ctx.Tr("admin.monitor.queue.pool.addworkers.musttimeoutduration"))
|
||||
ctx.Redirect(setting.AppSubURL + "/admin/monitor/queue/" + strconv.FormatInt(qid, 10))
|
||||
return
|
||||
}
|
||||
if _, ok := mq.Managed.(queue.ManagedPool); !ok {
|
||||
ctx.Flash.Error(ctx.Tr("admin.monitor.queue.pool.none"))
|
||||
ctx.Redirect(setting.AppSubURL + "/admin/monitor/queue/" + strconv.FormatInt(qid, 10))
|
||||
return
|
||||
}
|
||||
mq.AddWorkers(number, timeout)
|
||||
ctx.Flash.Success(ctx.Tr("admin.monitor.queue.pool.added"))
|
||||
ctx.Redirect(setting.AppSubURL + "/admin/monitor/queue/" + strconv.FormatInt(qid, 10))
|
||||
}
|
||||
|
||||
// SetQueueSettings sets the maximum number of workers and other settings for this queue
|
||||
func SetQueueSettings(ctx *context.Context) {
|
||||
qid := ctx.ParamsInt64("qid")
|
||||
mq := queue.GetManager().GetManagedQueue(qid)
|
||||
if mq == nil {
|
||||
ctx.Status(404)
|
||||
return
|
||||
}
|
||||
if _, ok := mq.Managed.(queue.ManagedPool); !ok {
|
||||
ctx.Flash.Error(ctx.Tr("admin.monitor.queue.pool.none"))
|
||||
ctx.Redirect(setting.AppSubURL + "/admin/monitor/queue/" + strconv.FormatInt(qid, 10))
|
||||
return
|
||||
}
|
||||
|
||||
maxNumberStr := ctx.Query("max-number")
|
||||
numberStr := ctx.Query("number")
|
||||
timeoutStr := ctx.Query("timeout")
|
||||
|
||||
var err error
|
||||
var maxNumber, number int
|
||||
var timeout time.Duration
|
||||
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.MaxNumberOfWorkers()
|
||||
}
|
||||
|
||||
if len(numberStr) > 0 {
|
||||
number, err = strconv.Atoi(numberStr)
|
||||
if err != nil || number < 0 {
|
||||
ctx.Flash.Error(ctx.Tr("admin.monitor.queue.settings.numberworkers.error"))
|
||||
ctx.Redirect(setting.AppSubURL + "/admin/monitor/queue/" + strconv.FormatInt(qid, 10))
|
||||
return
|
||||
}
|
||||
} else {
|
||||
number = mq.BoostWorkers()
|
||||
}
|
||||
|
||||
if len(timeoutStr) > 0 {
|
||||
timeout, err = time.ParseDuration(timeoutStr)
|
||||
if err != nil {
|
||||
ctx.Flash.Error(ctx.Tr("admin.monitor.queue.settings.timeout.error"))
|
||||
ctx.Redirect(setting.AppSubURL + "/admin/monitor/queue/" + strconv.FormatInt(qid, 10))
|
||||
return
|
||||
}
|
||||
} else {
|
||||
timeout = mq.BoostTimeout()
|
||||
}
|
||||
|
||||
mq.SetPoolSettings(maxNumber, number, timeout)
|
||||
ctx.Flash.Success(ctx.Tr("admin.monitor.queue.settings.changed"))
|
||||
ctx.Redirect(setting.AppSubURL + "/admin/monitor/queue/" + strconv.FormatInt(qid, 10))
|
||||
}
|
69
routers/web/admin/admin_test.go
Normal file
69
routers/web/admin/admin_test.go
Normal file
@@ -0,0 +1,69 @@
|
||||
// Copyright 2019 The Gitea Authors. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package admin
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestShadowPassword(t *testing.T) {
|
||||
var kases = []struct {
|
||||
Provider string
|
||||
CfgItem string
|
||||
Result string
|
||||
}{
|
||||
{
|
||||
Provider: "redis",
|
||||
CfgItem: "network=tcp,addr=:6379,password=gitea,db=0,pool_size=100,idle_timeout=180",
|
||||
Result: "network=tcp,addr=:6379,password=******,db=0,pool_size=100,idle_timeout=180",
|
||||
},
|
||||
{
|
||||
Provider: "mysql",
|
||||
CfgItem: "root:@tcp(localhost:3306)/gitea?charset=utf8",
|
||||
Result: "root:******@tcp(localhost:3306)/gitea?charset=utf8",
|
||||
},
|
||||
{
|
||||
Provider: "mysql",
|
||||
CfgItem: "/gitea?charset=utf8",
|
||||
Result: "/gitea?charset=utf8",
|
||||
},
|
||||
{
|
||||
Provider: "mysql",
|
||||
CfgItem: "user:mypassword@/dbname",
|
||||
Result: "user:******@/dbname",
|
||||
},
|
||||
{
|
||||
Provider: "postgres",
|
||||
CfgItem: "user=pqgotest dbname=pqgotest sslmode=verify-full",
|
||||
Result: "user=pqgotest dbname=pqgotest sslmode=verify-full",
|
||||
},
|
||||
{
|
||||
Provider: "postgres",
|
||||
CfgItem: "user=pqgotest password= dbname=pqgotest sslmode=verify-full",
|
||||
Result: "user=pqgotest password=****** dbname=pqgotest sslmode=verify-full",
|
||||
},
|
||||
{
|
||||
Provider: "postgres",
|
||||
CfgItem: "postgres://user:pass@hostname/dbname",
|
||||
Result: "postgres://user:******@hostname/dbname",
|
||||
},
|
||||
{
|
||||
Provider: "couchbase",
|
||||
CfgItem: "http://dev-couchbase.example.com:8091/",
|
||||
Result: "http://dev-couchbase.example.com:8091/",
|
||||
},
|
||||
{
|
||||
Provider: "couchbase",
|
||||
CfgItem: "http://user:the_password@dev-couchbase.example.com:8091/",
|
||||
Result: "http://user:******@dev-couchbase.example.com:8091/",
|
||||
},
|
||||
}
|
||||
|
||||
for _, k := range kases {
|
||||
assert.EqualValues(t, k.Result, shadowPassword(k.Provider, k.CfgItem))
|
||||
}
|
||||
}
|
410
routers/web/admin/auths.go
Normal file
410
routers/web/admin/auths.go
Normal file
@@ -0,0 +1,410 @@
|
||||
// Copyright 2014 The Gogs Authors. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package admin
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"regexp"
|
||||
|
||||
"code.gitea.io/gitea/models"
|
||||
"code.gitea.io/gitea/modules/auth/ldap"
|
||||
"code.gitea.io/gitea/modules/auth/oauth2"
|
||||
"code.gitea.io/gitea/modules/auth/pam"
|
||||
"code.gitea.io/gitea/modules/base"
|
||||
"code.gitea.io/gitea/modules/context"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/modules/web"
|
||||
"code.gitea.io/gitea/services/forms"
|
||||
|
||||
"xorm.io/xorm/convert"
|
||||
)
|
||||
|
||||
const (
|
||||
tplAuths base.TplName = "admin/auth/list"
|
||||
tplAuthNew base.TplName = "admin/auth/new"
|
||||
tplAuthEdit base.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["PageIsAdmin"] = true
|
||||
ctx.Data["PageIsAdminAuthentications"] = true
|
||||
|
||||
var err error
|
||||
ctx.Data["Sources"], err = models.LoginSources()
|
||||
if err != nil {
|
||||
ctx.ServerError("LoginSources", err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Data["Total"] = models.CountLoginSources()
|
||||
ctx.HTML(http.StatusOK, tplAuths)
|
||||
}
|
||||
|
||||
type dropdownItem struct {
|
||||
Name string
|
||||
Type interface{}
|
||||
}
|
||||
|
||||
var (
|
||||
authSources = func() []dropdownItem {
|
||||
items := []dropdownItem{
|
||||
{models.LoginNames[models.LoginLDAP], models.LoginLDAP},
|
||||
{models.LoginNames[models.LoginDLDAP], models.LoginDLDAP},
|
||||
{models.LoginNames[models.LoginSMTP], models.LoginSMTP},
|
||||
{models.LoginNames[models.LoginOAuth2], models.LoginOAuth2},
|
||||
{models.LoginNames[models.LoginSSPI], models.LoginSSPI},
|
||||
}
|
||||
if pam.Supported {
|
||||
items = append(items, dropdownItem{models.LoginNames[models.LoginPAM], models.LoginPAM})
|
||||
}
|
||||
return items
|
||||
}()
|
||||
|
||||
securityProtocols = []dropdownItem{
|
||||
{models.SecurityProtocolNames[ldap.SecurityProtocolUnencrypted], ldap.SecurityProtocolUnencrypted},
|
||||
{models.SecurityProtocolNames[ldap.SecurityProtocolLDAPS], ldap.SecurityProtocolLDAPS},
|
||||
{models.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["PageIsAdmin"] = true
|
||||
ctx.Data["PageIsAdminAuthentications"] = true
|
||||
|
||||
ctx.Data["type"] = models.LoginLDAP
|
||||
ctx.Data["CurrentTypeName"] = models.LoginNames[models.LoginLDAP]
|
||||
ctx.Data["CurrentSecurityProtocol"] = models.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"] = models.SMTPAuths
|
||||
ctx.Data["OAuth2Providers"] = models.OAuth2Providers
|
||||
ctx.Data["OAuth2DefaultCustomURLMappings"] = models.OAuth2DefaultCustomURLMappings
|
||||
|
||||
ctx.Data["SSPIAutoCreateUsers"] = true
|
||||
ctx.Data["SSPIAutoActivateUsers"] = true
|
||||
ctx.Data["SSPIStripDomainNames"] = true
|
||||
ctx.Data["SSPISeparatorReplacement"] = "_"
|
||||
ctx.Data["SSPIDefaultLanguage"] = ""
|
||||
|
||||
// only the first as default
|
||||
for key := range models.OAuth2Providers {
|
||||
ctx.Data["oauth2_provider"] = key
|
||||
break
|
||||
}
|
||||
|
||||
ctx.HTML(http.StatusOK, tplAuthNew)
|
||||
}
|
||||
|
||||
func parseLDAPConfig(form forms.AuthenticationForm) *models.LDAPConfig {
|
||||
var pageSize uint32
|
||||
if form.UsePagedSearch {
|
||||
pageSize = uint32(form.SearchPageSize)
|
||||
}
|
||||
return &models.LDAPConfig{
|
||||
Source: &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,
|
||||
SearchPageSize: pageSize,
|
||||
Filter: form.Filter,
|
||||
GroupsEnabled: form.GroupsEnabled,
|
||||
GroupDN: form.GroupDN,
|
||||
GroupFilter: form.GroupFilter,
|
||||
GroupMemberUID: form.GroupMemberUID,
|
||||
UserUID: form.UserUID,
|
||||
AdminFilter: form.AdminFilter,
|
||||
RestrictedFilter: form.RestrictedFilter,
|
||||
AllowDeactivateAll: form.AllowDeactivateAll,
|
||||
Enabled: true,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func parseSMTPConfig(form forms.AuthenticationForm) *models.SMTPConfig {
|
||||
return &models.SMTPConfig{
|
||||
Auth: form.SMTPAuth,
|
||||
Host: form.SMTPHost,
|
||||
Port: form.SMTPPort,
|
||||
AllowedDomains: form.AllowedDomains,
|
||||
TLS: form.TLS,
|
||||
SkipVerify: form.SkipVerify,
|
||||
}
|
||||
}
|
||||
|
||||
func parseOAuth2Config(form forms.AuthenticationForm) *models.OAuth2Config {
|
||||
var customURLMapping *oauth2.CustomURLMapping
|
||||
if form.Oauth2UseCustomURL {
|
||||
customURLMapping = &oauth2.CustomURLMapping{
|
||||
TokenURL: form.Oauth2TokenURL,
|
||||
AuthURL: form.Oauth2AuthURL,
|
||||
ProfileURL: form.Oauth2ProfileURL,
|
||||
EmailURL: form.Oauth2EmailURL,
|
||||
}
|
||||
} else {
|
||||
customURLMapping = nil
|
||||
}
|
||||
return &models.OAuth2Config{
|
||||
Provider: form.Oauth2Provider,
|
||||
ClientID: form.Oauth2Key,
|
||||
ClientSecret: form.Oauth2Secret,
|
||||
OpenIDConnectAutoDiscoveryURL: form.OpenIDConnectAutoDiscoveryURL,
|
||||
CustomURLMapping: customURLMapping,
|
||||
IconURL: form.Oauth2IconURL,
|
||||
}
|
||||
}
|
||||
|
||||
func parseSSPIConfig(ctx *context.Context, form forms.AuthenticationForm) (*models.SSPIConfig, error) {
|
||||
if util.IsEmptyString(form.SSPISeparatorReplacement) {
|
||||
ctx.Data["Err_SSPISeparatorReplacement"] = true
|
||||
return nil, errors.New(ctx.Tr("form.SSPISeparatorReplacement") + ctx.Tr("form.require_error"))
|
||||
}
|
||||
if separatorAntiPattern.MatchString(form.SSPISeparatorReplacement) {
|
||||
ctx.Data["Err_SSPISeparatorReplacement"] = true
|
||||
return nil, errors.New(ctx.Tr("form.SSPISeparatorReplacement") + ctx.Tr("form.alpha_dash_dot_error"))
|
||||
}
|
||||
|
||||
if form.SSPIDefaultLanguage != "" && !langCodePattern.MatchString(form.SSPIDefaultLanguage) {
|
||||
ctx.Data["Err_SSPIDefaultLanguage"] = true
|
||||
return nil, errors.New(ctx.Tr("form.lang_select_error"))
|
||||
}
|
||||
|
||||
return &models.SSPIConfig{
|
||||
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["PageIsAdmin"] = true
|
||||
ctx.Data["PageIsAdminAuthentications"] = true
|
||||
|
||||
ctx.Data["CurrentTypeName"] = models.LoginNames[models.LoginType(form.Type)]
|
||||
ctx.Data["CurrentSecurityProtocol"] = models.SecurityProtocolNames[ldap.SecurityProtocol(form.SecurityProtocol)]
|
||||
ctx.Data["AuthSources"] = authSources
|
||||
ctx.Data["SecurityProtocols"] = securityProtocols
|
||||
ctx.Data["SMTPAuths"] = models.SMTPAuths
|
||||
ctx.Data["OAuth2Providers"] = models.OAuth2Providers
|
||||
ctx.Data["OAuth2DefaultCustomURLMappings"] = models.OAuth2DefaultCustomURLMappings
|
||||
|
||||
ctx.Data["SSPIAutoCreateUsers"] = true
|
||||
ctx.Data["SSPIAutoActivateUsers"] = true
|
||||
ctx.Data["SSPIStripDomainNames"] = true
|
||||
ctx.Data["SSPISeparatorReplacement"] = "_"
|
||||
ctx.Data["SSPIDefaultLanguage"] = ""
|
||||
|
||||
hasTLS := false
|
||||
var config convert.Conversion
|
||||
switch models.LoginType(form.Type) {
|
||||
case models.LoginLDAP, models.LoginDLDAP:
|
||||
config = parseLDAPConfig(form)
|
||||
hasTLS = ldap.SecurityProtocol(form.SecurityProtocol) > ldap.SecurityProtocolUnencrypted
|
||||
case models.LoginSMTP:
|
||||
config = parseSMTPConfig(form)
|
||||
hasTLS = true
|
||||
case models.LoginPAM:
|
||||
config = &models.PAMConfig{
|
||||
ServiceName: form.PAMServiceName,
|
||||
EmailDomain: form.PAMEmailDomain,
|
||||
}
|
||||
case models.LoginOAuth2:
|
||||
config = parseOAuth2Config(form)
|
||||
case models.LoginSSPI:
|
||||
var err error
|
||||
config, err = parseSSPIConfig(ctx, form)
|
||||
if err != nil {
|
||||
ctx.RenderWithErr(err.Error(), tplAuthNew, form)
|
||||
return
|
||||
}
|
||||
existing, err := models.LoginSourcesByType(models.LoginSSPI)
|
||||
if err != nil || len(existing) > 0 {
|
||||
ctx.Data["Err_Type"] = true
|
||||
ctx.RenderWithErr(ctx.Tr("admin.auths.login_source_of_type_exist"), tplAuthNew, form)
|
||||
return
|
||||
}
|
||||
default:
|
||||
ctx.Error(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
ctx.Data["HasTLS"] = hasTLS
|
||||
|
||||
if ctx.HasError() {
|
||||
ctx.HTML(http.StatusOK, tplAuthNew)
|
||||
return
|
||||
}
|
||||
|
||||
if err := models.CreateLoginSource(&models.LoginSource{
|
||||
Type: models.LoginType(form.Type),
|
||||
Name: form.Name,
|
||||
IsActived: form.IsActive,
|
||||
IsSyncEnabled: form.IsSyncEnabled,
|
||||
Cfg: config,
|
||||
}); err != nil {
|
||||
if models.IsErrLoginSourceAlreadyExist(err) {
|
||||
ctx.Data["Err_Name"] = true
|
||||
ctx.RenderWithErr(ctx.Tr("admin.auths.login_source_exist", err.(models.ErrLoginSourceAlreadyExist).Name), tplAuthNew, form)
|
||||
} else {
|
||||
ctx.ServerError("CreateSource", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
log.Trace("Authentication created by admin(%s): %s", ctx.User.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["PageIsAdmin"] = true
|
||||
ctx.Data["PageIsAdminAuthentications"] = true
|
||||
|
||||
ctx.Data["SecurityProtocols"] = securityProtocols
|
||||
ctx.Data["SMTPAuths"] = models.SMTPAuths
|
||||
ctx.Data["OAuth2Providers"] = models.OAuth2Providers
|
||||
ctx.Data["OAuth2DefaultCustomURLMappings"] = models.OAuth2DefaultCustomURLMappings
|
||||
|
||||
source, err := models.GetLoginSourceByID(ctx.ParamsInt64(":authid"))
|
||||
if err != nil {
|
||||
ctx.ServerError("GetLoginSourceByID", err)
|
||||
return
|
||||
}
|
||||
ctx.Data["Source"] = source
|
||||
ctx.Data["HasTLS"] = source.HasTLS()
|
||||
|
||||
if source.IsOAuth2() {
|
||||
ctx.Data["CurrentOAuth2Provider"] = models.OAuth2Providers[source.OAuth2().Provider]
|
||||
}
|
||||
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["PageIsAdmin"] = true
|
||||
ctx.Data["PageIsAdminAuthentications"] = true
|
||||
|
||||
ctx.Data["SMTPAuths"] = models.SMTPAuths
|
||||
ctx.Data["OAuth2Providers"] = models.OAuth2Providers
|
||||
ctx.Data["OAuth2DefaultCustomURLMappings"] = models.OAuth2DefaultCustomURLMappings
|
||||
|
||||
source, err := models.GetLoginSourceByID(ctx.ParamsInt64(":authid"))
|
||||
if err != nil {
|
||||
ctx.ServerError("GetLoginSourceByID", err)
|
||||
return
|
||||
}
|
||||
ctx.Data["Source"] = source
|
||||
ctx.Data["HasTLS"] = source.HasTLS()
|
||||
|
||||
if ctx.HasError() {
|
||||
ctx.HTML(http.StatusOK, tplAuthEdit)
|
||||
return
|
||||
}
|
||||
|
||||
var config convert.Conversion
|
||||
switch models.LoginType(form.Type) {
|
||||
case models.LoginLDAP, models.LoginDLDAP:
|
||||
config = parseLDAPConfig(form)
|
||||
case models.LoginSMTP:
|
||||
config = parseSMTPConfig(form)
|
||||
case models.LoginPAM:
|
||||
config = &models.PAMConfig{
|
||||
ServiceName: form.PAMServiceName,
|
||||
EmailDomain: form.PAMEmailDomain,
|
||||
}
|
||||
case models.LoginOAuth2:
|
||||
config = parseOAuth2Config(form)
|
||||
case models.LoginSSPI:
|
||||
config, err = parseSSPIConfig(ctx, form)
|
||||
if err != nil {
|
||||
ctx.RenderWithErr(err.Error(), tplAuthEdit, form)
|
||||
return
|
||||
}
|
||||
default:
|
||||
ctx.Error(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
source.Name = form.Name
|
||||
source.IsActived = form.IsActive
|
||||
source.IsSyncEnabled = form.IsSyncEnabled
|
||||
source.Cfg = config
|
||||
if err := models.UpdateSource(source); err != nil {
|
||||
if models.IsErrOpenIDConnectInitialize(err) {
|
||||
ctx.Flash.Error(err.Error(), true)
|
||||
ctx.HTML(http.StatusOK, tplAuthEdit)
|
||||
} else {
|
||||
ctx.ServerError("UpdateSource", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
log.Trace("Authentication changed by admin(%s): %d", ctx.User.Name, source.ID)
|
||||
|
||||
ctx.Flash.Success(ctx.Tr("admin.auths.update_success"))
|
||||
ctx.Redirect(setting.AppSubURL + "/admin/auths/" + fmt.Sprint(form.ID))
|
||||
}
|
||||
|
||||
// DeleteAuthSource response for deleting an auth source
|
||||
func DeleteAuthSource(ctx *context.Context) {
|
||||
source, err := models.GetLoginSourceByID(ctx.ParamsInt64(":authid"))
|
||||
if err != nil {
|
||||
ctx.ServerError("GetLoginSourceByID", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = models.DeleteSource(source); err != nil {
|
||||
if models.IsErrLoginSourceInUse(err) {
|
||||
ctx.Flash.Error(ctx.Tr("admin.auths.still_in_used"))
|
||||
} else {
|
||||
ctx.Flash.Error(fmt.Sprintf("DeleteSource: %v", err))
|
||||
}
|
||||
ctx.JSON(http.StatusOK, map[string]interface{}{
|
||||
"redirect": setting.AppSubURL + "/admin/auths/" + ctx.Params(":authid"),
|
||||
})
|
||||
return
|
||||
}
|
||||
log.Trace("Authentication deleted by admin(%s): %d", ctx.User.Name, source.ID)
|
||||
|
||||
ctx.Flash.Success(ctx.Tr("admin.auths.deletion_success"))
|
||||
ctx.JSON(http.StatusOK, map[string]interface{}{
|
||||
"redirect": setting.AppSubURL + "/admin/auths",
|
||||
})
|
||||
}
|
156
routers/web/admin/emails.go
Normal file
156
routers/web/admin/emails.go
Normal file
@@ -0,0 +1,156 @@
|
||||
// Copyright 2020 The Gitea Authors.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package admin
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"code.gitea.io/gitea/models"
|
||||
"code.gitea.io/gitea/modules/base"
|
||||
"code.gitea.io/gitea/modules/context"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
)
|
||||
|
||||
const (
|
||||
tplEmails base.TplName = "admin/emails/list"
|
||||
)
|
||||
|
||||
// Emails show all emails
|
||||
func Emails(ctx *context.Context) {
|
||||
ctx.Data["Title"] = ctx.Tr("admin.emails")
|
||||
ctx.Data["PageIsAdmin"] = true
|
||||
ctx.Data["PageIsAdminEmails"] = true
|
||||
|
||||
opts := &models.SearchEmailOptions{
|
||||
ListOptions: models.ListOptions{
|
||||
PageSize: setting.UI.Admin.UserPagingNum,
|
||||
Page: ctx.QueryInt("page"),
|
||||
},
|
||||
}
|
||||
|
||||
if opts.Page <= 1 {
|
||||
opts.Page = 1
|
||||
}
|
||||
|
||||
type ActiveEmail struct {
|
||||
models.SearchEmailResult
|
||||
CanChange bool
|
||||
}
|
||||
|
||||
var (
|
||||
baseEmails []*models.SearchEmailResult
|
||||
emails []ActiveEmail
|
||||
count int64
|
||||
err error
|
||||
orderBy models.SearchEmailOrderBy
|
||||
)
|
||||
|
||||
ctx.Data["SortType"] = ctx.Query("sort")
|
||||
switch ctx.Query("sort") {
|
||||
case "email":
|
||||
orderBy = models.SearchEmailOrderByEmail
|
||||
case "reverseemail":
|
||||
orderBy = models.SearchEmailOrderByEmailReverse
|
||||
case "username":
|
||||
orderBy = models.SearchEmailOrderByName
|
||||
case "reverseusername":
|
||||
orderBy = models.SearchEmailOrderByNameReverse
|
||||
default:
|
||||
ctx.Data["SortType"] = "email"
|
||||
orderBy = models.SearchEmailOrderByEmail
|
||||
}
|
||||
|
||||
opts.Keyword = ctx.QueryTrim("q")
|
||||
opts.SortType = orderBy
|
||||
if len(ctx.Query("is_activated")) != 0 {
|
||||
opts.IsActivated = util.OptionalBoolOf(ctx.QueryBool("activated"))
|
||||
}
|
||||
if len(ctx.Query("is_primary")) != 0 {
|
||||
opts.IsPrimary = util.OptionalBoolOf(ctx.QueryBool("primary"))
|
||||
}
|
||||
|
||||
if len(opts.Keyword) == 0 || isKeywordValid(opts.Keyword) {
|
||||
baseEmails, count, err = models.SearchEmails(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.User.ID != emails[i].UID || !emails[i].IsPrimary
|
||||
}
|
||||
}
|
||||
ctx.Data["Keyword"] = opts.Keyword
|
||||
ctx.Data["Total"] = count
|
||||
ctx.Data["Emails"] = emails
|
||||
|
||||
pager := context.NewPagination(int(count), opts.PageSize, opts.Page, 5)
|
||||
pager.SetDefaultParams(ctx)
|
||||
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.QueryInt64("uid")
|
||||
email := ctx.Query("email")
|
||||
primary, okp := truefalse[ctx.Query("primary")]
|
||||
activate, oka := truefalse[ctx.Query("activate")]
|
||||
|
||||
if uid == 0 || len(email) == 0 || !okp || !oka {
|
||||
ctx.Error(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
log.Info("Changing activation for User ID: %d, email: %s, primary: %v to %v", uid, email, primary, activate)
|
||||
|
||||
if err := models.ActivateUserEmail(uid, email, primary, activate); err != nil {
|
||||
log.Error("ActivateUserEmail(%v,%v,%v,%v): %v", uid, email, primary, activate, err)
|
||||
if models.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.QueryTrim("q"); len(val) > 0 {
|
||||
q.Set("q", val)
|
||||
}
|
||||
if val := ctx.QueryTrim("sort"); len(val) > 0 {
|
||||
q.Set("sort", val)
|
||||
}
|
||||
if val := ctx.QueryTrim("is_primary"); len(val) > 0 {
|
||||
q.Set("is_primary", val)
|
||||
}
|
||||
if val := ctx.QueryTrim("is_activated"); len(val) > 0 {
|
||||
q.Set("is_activated", val)
|
||||
}
|
||||
redirect.RawQuery = q.Encode()
|
||||
ctx.Redirect(redirect.String())
|
||||
}
|
72
routers/web/admin/hooks.go
Normal file
72
routers/web/admin/hooks.go
Normal file
@@ -0,0 +1,72 @@
|
||||
// Copyright 2018 The Gitea Authors. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package admin
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"code.gitea.io/gitea/models"
|
||||
"code.gitea.io/gitea/modules/base"
|
||||
"code.gitea.io/gitea/modules/context"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
)
|
||||
|
||||
const (
|
||||
// tplAdminHooks template path to render hook settings
|
||||
tplAdminHooks base.TplName = "admin/hooks"
|
||||
)
|
||||
|
||||
// DefaultOrSystemWebhooks renders both admin default and system webhook list pages
|
||||
func DefaultOrSystemWebhooks(ctx *context.Context) {
|
||||
var err error
|
||||
|
||||
ctx.Data["PageIsAdminSystemHooks"] = true
|
||||
ctx.Data["PageIsAdminDefaultHooks"] = true
|
||||
|
||||
def := make(map[string]interface{}, len(ctx.Data))
|
||||
sys := make(map[string]interface{}, 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")
|
||||
sys["Webhooks"], err = models.GetSystemWebhooks()
|
||||
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")
|
||||
def["Webhooks"], err = models.GetDefaultWebhooks()
|
||||
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 := models.DeleteDefaultSystemWebhook(ctx.QueryInt64("id")); err != nil {
|
||||
ctx.Flash.Error("DeleteDefaultWebhook: " + err.Error())
|
||||
} else {
|
||||
ctx.Flash.Success(ctx.Tr("repo.settings.webhook_deletion_success"))
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, map[string]interface{}{
|
||||
"redirect": setting.AppSubURL + "/admin/hooks",
|
||||
})
|
||||
}
|
16
routers/web/admin/main_test.go
Normal file
16
routers/web/admin/main_test.go
Normal file
@@ -0,0 +1,16 @@
|
||||
// Copyright 2018 The Gitea Authors. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package admin
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/models"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
models.MainTest(m, filepath.Join("..", "..", ".."))
|
||||
}
|
79
routers/web/admin/notice.go
Normal file
79
routers/web/admin/notice.go
Normal file
@@ -0,0 +1,79 @@
|
||||
// Copyright 2014 The Gogs Authors. All rights reserved.
|
||||
// Copyright 2019 The Gitea Authors. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package admin
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"code.gitea.io/gitea/models"
|
||||
"code.gitea.io/gitea/modules/base"
|
||||
"code.gitea.io/gitea/modules/context"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
)
|
||||
|
||||
const (
|
||||
tplNotices base.TplName = "admin/notice"
|
||||
)
|
||||
|
||||
// Notices show notices for admin
|
||||
func Notices(ctx *context.Context) {
|
||||
ctx.Data["Title"] = ctx.Tr("admin.notices")
|
||||
ctx.Data["PageIsAdmin"] = true
|
||||
ctx.Data["PageIsAdminNotices"] = true
|
||||
|
||||
total := models.CountNotices()
|
||||
page := ctx.QueryInt("page")
|
||||
if page <= 1 {
|
||||
page = 1
|
||||
}
|
||||
|
||||
notices, err := models.Notices(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(int(total), setting.UI.Admin.NoticePagingNum, page, 5)
|
||||
|
||||
ctx.HTML(http.StatusOK, tplNotices)
|
||||
}
|
||||
|
||||
// DeleteNotices delete the specific notices
|
||||
func DeleteNotices(ctx *context.Context) {
|
||||
strs := ctx.QueryStrings("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 := models.DeleteNoticesByIDs(ids); err != nil {
|
||||
ctx.Flash.Error("DeleteNoticesByIDs: " + err.Error())
|
||||
ctx.Status(500)
|
||||
} else {
|
||||
ctx.Flash.Success(ctx.Tr("admin.notices.delete_success"))
|
||||
ctx.Status(200)
|
||||
}
|
||||
}
|
||||
|
||||
// EmptyNotices delete all the notices
|
||||
func EmptyNotices(ctx *context.Context) {
|
||||
if err := models.DeleteNotices(0, 0); err != nil {
|
||||
ctx.ServerError("DeleteNotices", err)
|
||||
return
|
||||
}
|
||||
|
||||
log.Trace("System notices deleted by admin (%s): [start: %d]", ctx.User.Name, 0)
|
||||
ctx.Flash.Success(ctx.Tr("admin.notices.delete_success"))
|
||||
ctx.Redirect(setting.AppSubURL + "/admin/notices")
|
||||
}
|
34
routers/web/admin/orgs.go
Normal file
34
routers/web/admin/orgs.go
Normal file
@@ -0,0 +1,34 @@
|
||||
// Copyright 2014 The Gogs Authors. All rights reserved.
|
||||
// Copyright 2020 The Gitea Authors.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package admin
|
||||
|
||||
import (
|
||||
"code.gitea.io/gitea/models"
|
||||
"code.gitea.io/gitea/modules/base"
|
||||
"code.gitea.io/gitea/modules/context"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/routers/web/explore"
|
||||
)
|
||||
|
||||
const (
|
||||
tplOrgs base.TplName = "admin/org/list"
|
||||
)
|
||||
|
||||
// Organizations show all the organizations
|
||||
func Organizations(ctx *context.Context) {
|
||||
ctx.Data["Title"] = ctx.Tr("admin.organizations")
|
||||
ctx.Data["PageIsAdmin"] = true
|
||||
ctx.Data["PageIsAdminOrganizations"] = true
|
||||
|
||||
explore.RenderUserSearch(ctx, &models.SearchUserOptions{
|
||||
Type: models.UserTypeOrganization,
|
||||
ListOptions: models.ListOptions{
|
||||
PageSize: setting.UI.Admin.OrgPagingNum,
|
||||
},
|
||||
Visible: []structs.VisibleType{structs.VisibleTypePublic, structs.VisibleTypeLimited, structs.VisibleTypePrivate},
|
||||
}, tplOrgs)
|
||||
}
|
166
routers/web/admin/repos.go
Normal file
166
routers/web/admin/repos.go
Normal file
@@ -0,0 +1,166 @@
|
||||
// Copyright 2014 The Gogs Authors. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package admin
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/models"
|
||||
"code.gitea.io/gitea/modules/base"
|
||||
"code.gitea.io/gitea/modules/context"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/repository"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/routers/web/explore"
|
||||
repo_service "code.gitea.io/gitea/services/repository"
|
||||
)
|
||||
|
||||
const (
|
||||
tplRepos base.TplName = "admin/repo/list"
|
||||
tplUnadoptedRepos base.TplName = "admin/repo/unadopted"
|
||||
)
|
||||
|
||||
// Repos show all the repositories
|
||||
func Repos(ctx *context.Context) {
|
||||
ctx.Data["Title"] = ctx.Tr("admin.repositories")
|
||||
ctx.Data["PageIsAdmin"] = true
|
||||
ctx.Data["PageIsAdminRepositories"] = true
|
||||
|
||||
explore.RenderRepoSearch(ctx, &explore.RepoSearchOptions{
|
||||
Private: true,
|
||||
PageSize: setting.UI.Admin.RepoPagingNum,
|
||||
TplName: tplRepos,
|
||||
})
|
||||
}
|
||||
|
||||
// DeleteRepo delete one repository
|
||||
func DeleteRepo(ctx *context.Context) {
|
||||
repo, err := models.GetRepositoryByID(ctx.QueryInt64("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.User, repo); err != nil {
|
||||
ctx.ServerError("DeleteRepository", err)
|
||||
return
|
||||
}
|
||||
log.Trace("Repository deleted: %s", repo.FullName())
|
||||
|
||||
ctx.Flash.Success(ctx.Tr("repo.settings.deletion_success"))
|
||||
ctx.JSON(http.StatusOK, map[string]interface{}{
|
||||
"redirect": setting.AppSubURL + "/admin/repos?page=" + ctx.Query("page") + "&sort=" + ctx.Query("sort"),
|
||||
})
|
||||
}
|
||||
|
||||
// UnadoptedRepos lists the unadopted repositories
|
||||
func UnadoptedRepos(ctx *context.Context) {
|
||||
ctx.Data["Title"] = ctx.Tr("admin.repositories")
|
||||
ctx.Data["PageIsAdmin"] = true
|
||||
ctx.Data["PageIsAdminRepositories"] = true
|
||||
|
||||
opts := models.ListOptions{
|
||||
PageSize: setting.UI.Admin.UserPagingNum,
|
||||
Page: ctx.QueryInt("page"),
|
||||
}
|
||||
|
||||
if opts.Page <= 0 {
|
||||
opts.Page = 1
|
||||
}
|
||||
|
||||
ctx.Data["CurrentPage"] = opts.Page
|
||||
|
||||
doSearch := ctx.QueryBool("search")
|
||||
|
||||
ctx.Data["search"] = doSearch
|
||||
q := ctx.Query("q")
|
||||
|
||||
if !doSearch {
|
||||
pager := context.NewPagination(0, opts.PageSize, opts.Page, 5)
|
||||
pager.SetDefaultParams(ctx)
|
||||
pager.AddParam(ctx, "search", "search")
|
||||
ctx.Data["Page"] = pager
|
||||
ctx.HTML(http.StatusOK, tplUnadoptedRepos)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Data["Keyword"] = q
|
||||
repoNames, count, err := repository.ListUnadoptedRepositories(q, &opts)
|
||||
if err != nil {
|
||||
ctx.ServerError("ListUnadoptedRepositories", err)
|
||||
}
|
||||
ctx.Data["Dirs"] = repoNames
|
||||
pager := context.NewPagination(int(count), opts.PageSize, opts.Page, 5)
|
||||
pager.SetDefaultParams(ctx)
|
||||
pager.AddParam(ctx, "search", "search")
|
||||
ctx.Data["Page"] = pager
|
||||
ctx.HTML(http.StatusOK, tplUnadoptedRepos)
|
||||
}
|
||||
|
||||
// AdoptOrDeleteRepository adopts or deletes a repository
|
||||
func AdoptOrDeleteRepository(ctx *context.Context) {
|
||||
dir := ctx.Query("id")
|
||||
action := ctx.Query("action")
|
||||
page := ctx.QueryInt("page")
|
||||
q := ctx.Query("q")
|
||||
|
||||
dirSplit := strings.SplitN(dir, "/", 2)
|
||||
if len(dirSplit) != 2 {
|
||||
ctx.Redirect(setting.AppSubURL + "/admin/repos")
|
||||
return
|
||||
}
|
||||
|
||||
ctxUser, err := models.GetUserByName(dirSplit[0])
|
||||
if err != nil {
|
||||
if models.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 := models.IsRepositoryExist(ctxUser, repoName)
|
||||
if err != nil {
|
||||
ctx.ServerError("IsRepositoryExist", err)
|
||||
return
|
||||
}
|
||||
isDir, err := util.IsDir(models.RepoPath(ctxUser.Name, repoName))
|
||||
if err != nil {
|
||||
ctx.ServerError("IsDir", err)
|
||||
return
|
||||
}
|
||||
if has || !isDir {
|
||||
// Fallthrough to failure mode
|
||||
} else if action == "adopt" {
|
||||
if _, err := repository.AdoptRepository(ctx.User, ctxUser, models.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 := repository.DeleteUnadoptedRepository(ctx.User, 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=" + strconv.Itoa(page))
|
||||
}
|
371
routers/web/admin/users.go
Normal file
371
routers/web/admin/users.go
Normal file
@@ -0,0 +1,371 @@
|
||||
// Copyright 2014 The Gogs Authors. All rights reserved.
|
||||
// Copyright 2020 The Gitea Authors.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package admin
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/models"
|
||||
"code.gitea.io/gitea/modules/base"
|
||||
"code.gitea.io/gitea/modules/context"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/password"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/web"
|
||||
"code.gitea.io/gitea/routers/web/explore"
|
||||
router_user_setting "code.gitea.io/gitea/routers/web/user/setting"
|
||||
"code.gitea.io/gitea/services/forms"
|
||||
"code.gitea.io/gitea/services/mailer"
|
||||
)
|
||||
|
||||
const (
|
||||
tplUsers base.TplName = "admin/user/list"
|
||||
tplUserNew base.TplName = "admin/user/new"
|
||||
tplUserEdit base.TplName = "admin/user/edit"
|
||||
)
|
||||
|
||||
// Users show all the users
|
||||
func Users(ctx *context.Context) {
|
||||
ctx.Data["Title"] = ctx.Tr("admin.users")
|
||||
ctx.Data["PageIsAdmin"] = true
|
||||
ctx.Data["PageIsAdminUsers"] = true
|
||||
|
||||
explore.RenderUserSearch(ctx, &models.SearchUserOptions{
|
||||
Type: models.UserTypeIndividual,
|
||||
ListOptions: models.ListOptions{
|
||||
PageSize: setting.UI.Admin.UserPagingNum,
|
||||
},
|
||||
SearchByEmail: true,
|
||||
}, tplUsers)
|
||||
}
|
||||
|
||||
// NewUser render adding a new user page
|
||||
func NewUser(ctx *context.Context) {
|
||||
ctx.Data["Title"] = ctx.Tr("admin.users.new_account")
|
||||
ctx.Data["PageIsAdmin"] = true
|
||||
ctx.Data["PageIsAdminUsers"] = true
|
||||
|
||||
ctx.Data["login_type"] = "0-0"
|
||||
|
||||
sources, err := models.LoginSources()
|
||||
if err != nil {
|
||||
ctx.ServerError("LoginSources", 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["PageIsAdmin"] = true
|
||||
ctx.Data["PageIsAdminUsers"] = true
|
||||
|
||||
sources, err := models.LoginSources()
|
||||
if err != nil {
|
||||
ctx.ServerError("LoginSources", err)
|
||||
return
|
||||
}
|
||||
ctx.Data["Sources"] = sources
|
||||
|
||||
ctx.Data["CanSendEmail"] = setting.MailService != nil
|
||||
|
||||
if ctx.HasError() {
|
||||
ctx.HTML(http.StatusOK, tplUserNew)
|
||||
return
|
||||
}
|
||||
|
||||
u := &models.User{
|
||||
Name: form.UserName,
|
||||
Email: form.Email,
|
||||
Passwd: form.Password,
|
||||
IsActive: true,
|
||||
LoginType: models.LoginPlain,
|
||||
}
|
||||
|
||||
if len(form.LoginType) > 0 {
|
||||
fields := strings.Split(form.LoginType, "-")
|
||||
if len(fields) == 2 {
|
||||
lType, _ := strconv.ParseInt(fields[0], 10, 0)
|
||||
u.LoginType = models.LoginType(lType)
|
||||
u.LoginSource, _ = strconv.ParseInt(fields[1], 10, 64)
|
||||
u.LoginName = form.LoginName
|
||||
}
|
||||
}
|
||||
if u.LoginType == models.LoginNoType || u.LoginType == models.LoginPlain {
|
||||
if len(form.Password) < setting.MinPasswordLength {
|
||||
ctx.Data["Err_Password"] = true
|
||||
ctx.RenderWithErr(ctx.Tr("auth.password_too_short", setting.MinPasswordLength), tplUserNew, &form)
|
||||
return
|
||||
}
|
||||
if !password.IsComplexEnough(form.Password) {
|
||||
ctx.Data["Err_Password"] = true
|
||||
ctx.RenderWithErr(password.BuildComplexityError(ctx), tplUserNew, &form)
|
||||
return
|
||||
}
|
||||
pwned, err := password.IsPwned(ctx, form.Password)
|
||||
if pwned {
|
||||
ctx.Data["Err_Password"] = true
|
||||
errMsg := ctx.Tr("auth.password_pwned")
|
||||
if err != nil {
|
||||
log.Error(err.Error())
|
||||
errMsg = ctx.Tr("auth.password_pwned_err")
|
||||
}
|
||||
ctx.RenderWithErr(errMsg, tplUserNew, &form)
|
||||
return
|
||||
}
|
||||
u.MustChangePassword = form.MustChangePassword
|
||||
}
|
||||
if err := models.CreateUser(u); err != nil {
|
||||
switch {
|
||||
case models.IsErrUserAlreadyExist(err):
|
||||
ctx.Data["Err_UserName"] = true
|
||||
ctx.RenderWithErr(ctx.Tr("form.username_been_taken"), tplUserNew, &form)
|
||||
case models.IsErrEmailAlreadyUsed(err):
|
||||
ctx.Data["Err_Email"] = true
|
||||
ctx.RenderWithErr(ctx.Tr("form.email_been_used"), tplUserNew, &form)
|
||||
case models.IsErrEmailInvalid(err):
|
||||
ctx.Data["Err_Email"] = true
|
||||
ctx.RenderWithErr(ctx.Tr("form.email_invalid"), tplUserNew, &form)
|
||||
case models.IsErrNameReserved(err):
|
||||
ctx.Data["Err_UserName"] = true
|
||||
ctx.RenderWithErr(ctx.Tr("user.form.name_reserved", err.(models.ErrNameReserved).Name), tplUserNew, &form)
|
||||
case models.IsErrNamePatternNotAllowed(err):
|
||||
ctx.Data["Err_UserName"] = true
|
||||
ctx.RenderWithErr(ctx.Tr("user.form.name_pattern_not_allowed", err.(models.ErrNamePatternNotAllowed).Pattern), tplUserNew, &form)
|
||||
case models.IsErrNameCharsNotAllowed(err):
|
||||
ctx.Data["Err_UserName"] = true
|
||||
ctx.RenderWithErr(ctx.Tr("user.form.name_chars_not_allowed", err.(models.ErrNameCharsNotAllowed).Name), tplUserNew, &form)
|
||||
default:
|
||||
ctx.ServerError("CreateUser", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
log.Trace("Account created by admin (%s): %s", ctx.User.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/" + fmt.Sprint(u.ID))
|
||||
}
|
||||
|
||||
func prepareUserInfo(ctx *context.Context) *models.User {
|
||||
u, err := models.GetUserByID(ctx.ParamsInt64(":userid"))
|
||||
if err != nil {
|
||||
ctx.ServerError("GetUserByID", err)
|
||||
return nil
|
||||
}
|
||||
ctx.Data["User"] = u
|
||||
|
||||
if u.LoginSource > 0 {
|
||||
ctx.Data["LoginSource"], err = models.GetLoginSourceByID(u.LoginSource)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetLoginSourceByID", err)
|
||||
return nil
|
||||
}
|
||||
} else {
|
||||
ctx.Data["LoginSource"] = &models.LoginSource{}
|
||||
}
|
||||
|
||||
sources, err := models.LoginSources()
|
||||
if err != nil {
|
||||
ctx.ServerError("LoginSources", err)
|
||||
return nil
|
||||
}
|
||||
ctx.Data["Sources"] = sources
|
||||
|
||||
ctx.Data["TwoFactorEnabled"] = true
|
||||
_, err = models.GetTwoFactorByUID(u.ID)
|
||||
if err != nil {
|
||||
if !models.IsErrTwoFactorNotEnrolled(err) {
|
||||
ctx.ServerError("IsErrTwoFactorNotEnrolled", err)
|
||||
return nil
|
||||
}
|
||||
ctx.Data["TwoFactorEnabled"] = false
|
||||
}
|
||||
|
||||
return u
|
||||
}
|
||||
|
||||
// EditUser show editting user page
|
||||
func EditUser(ctx *context.Context) {
|
||||
ctx.Data["Title"] = ctx.Tr("admin.users.edit_account")
|
||||
ctx.Data["PageIsAdmin"] = true
|
||||
ctx.Data["PageIsAdminUsers"] = true
|
||||
ctx.Data["DisableRegularOrgCreation"] = setting.Admin.DisableRegularOrgCreation
|
||||
ctx.Data["DisableMigrations"] = setting.Repository.DisableMigrations
|
||||
|
||||
prepareUserInfo(ctx)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
|
||||
ctx.HTML(http.StatusOK, tplUserEdit)
|
||||
}
|
||||
|
||||
// EditUserPost response for editting user
|
||||
func EditUserPost(ctx *context.Context) {
|
||||
form := web.GetForm(ctx).(*forms.AdminEditUserForm)
|
||||
ctx.Data["Title"] = ctx.Tr("admin.users.edit_account")
|
||||
ctx.Data["PageIsAdmin"] = true
|
||||
ctx.Data["PageIsAdminUsers"] = true
|
||||
ctx.Data["DisableMigrations"] = setting.Repository.DisableMigrations
|
||||
|
||||
u := prepareUserInfo(ctx)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
|
||||
if ctx.HasError() {
|
||||
ctx.HTML(http.StatusOK, tplUserEdit)
|
||||
return
|
||||
}
|
||||
|
||||
fields := strings.Split(form.LoginType, "-")
|
||||
if len(fields) == 2 {
|
||||
loginType, _ := strconv.ParseInt(fields[0], 10, 0)
|
||||
loginSource, _ := strconv.ParseInt(fields[1], 10, 64)
|
||||
|
||||
if u.LoginSource != loginSource {
|
||||
u.LoginSource = loginSource
|
||||
u.LoginType = models.LoginType(loginType)
|
||||
}
|
||||
}
|
||||
|
||||
if len(form.Password) > 0 && (u.IsLocal() || u.IsOAuth2()) {
|
||||
var err error
|
||||
if len(form.Password) < setting.MinPasswordLength {
|
||||
ctx.Data["Err_Password"] = true
|
||||
ctx.RenderWithErr(ctx.Tr("auth.password_too_short", setting.MinPasswordLength), tplUserEdit, &form)
|
||||
return
|
||||
}
|
||||
if !password.IsComplexEnough(form.Password) {
|
||||
ctx.RenderWithErr(password.BuildComplexityError(ctx), tplUserEdit, &form)
|
||||
return
|
||||
}
|
||||
pwned, err := password.IsPwned(ctx, form.Password)
|
||||
if pwned {
|
||||
ctx.Data["Err_Password"] = true
|
||||
errMsg := ctx.Tr("auth.password_pwned")
|
||||
if err != nil {
|
||||
log.Error(err.Error())
|
||||
errMsg = ctx.Tr("auth.password_pwned_err")
|
||||
}
|
||||
ctx.RenderWithErr(errMsg, tplUserNew, &form)
|
||||
return
|
||||
}
|
||||
if u.Salt, err = models.GetUserSalt(); err != nil {
|
||||
ctx.ServerError("UpdateUser", err)
|
||||
return
|
||||
}
|
||||
if err = u.SetPassword(form.Password); err != nil {
|
||||
ctx.ServerError("SetPassword", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if len(form.UserName) != 0 && u.Name != form.UserName {
|
||||
if err := router_user_setting.HandleUsernameChange(ctx, u, form.UserName); err != nil {
|
||||
ctx.Redirect(setting.AppSubURL + "/admin/users")
|
||||
return
|
||||
}
|
||||
u.Name = form.UserName
|
||||
u.LowerName = strings.ToLower(form.UserName)
|
||||
}
|
||||
|
||||
if form.Reset2FA {
|
||||
tf, err := models.GetTwoFactorByUID(u.ID)
|
||||
if err != nil && !models.IsErrTwoFactorNotEnrolled(err) {
|
||||
ctx.ServerError("GetTwoFactorByUID", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = models.DeleteTwoFactorByID(tf.ID, u.ID); err != nil {
|
||||
ctx.ServerError("DeleteTwoFactorByID", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
u.LoginName = form.LoginName
|
||||
u.FullName = form.FullName
|
||||
u.Email = form.Email
|
||||
u.Website = form.Website
|
||||
u.Location = form.Location
|
||||
u.MaxRepoCreation = form.MaxRepoCreation
|
||||
u.IsActive = form.Active
|
||||
u.IsAdmin = form.Admin
|
||||
u.IsRestricted = form.Restricted
|
||||
u.AllowGitHook = form.AllowGitHook
|
||||
u.AllowImportLocal = form.AllowImportLocal
|
||||
u.AllowCreateOrganization = form.AllowCreateOrganization
|
||||
|
||||
// skip self Prohibit Login
|
||||
if ctx.User.ID == u.ID {
|
||||
u.ProhibitLogin = false
|
||||
} else {
|
||||
u.ProhibitLogin = form.ProhibitLogin
|
||||
}
|
||||
|
||||
if err := models.UpdateUser(u); err != nil {
|
||||
if models.IsErrEmailAlreadyUsed(err) {
|
||||
ctx.Data["Err_Email"] = true
|
||||
ctx.RenderWithErr(ctx.Tr("form.email_been_used"), tplUserEdit, &form)
|
||||
} else if models.IsErrEmailInvalid(err) {
|
||||
ctx.Data["Err_Email"] = true
|
||||
ctx.RenderWithErr(ctx.Tr("form.email_invalid"), tplUserEdit, &form)
|
||||
} else {
|
||||
ctx.ServerError("UpdateUser", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
log.Trace("Account profile updated by admin (%s): %s", ctx.User.Name, u.Name)
|
||||
|
||||
ctx.Flash.Success(ctx.Tr("admin.users.update_profile_success"))
|
||||
ctx.Redirect(setting.AppSubURL + "/admin/users/" + ctx.Params(":userid"))
|
||||
}
|
||||
|
||||
// DeleteUser response for deleting a user
|
||||
func DeleteUser(ctx *context.Context) {
|
||||
u, err := models.GetUserByID(ctx.ParamsInt64(":userid"))
|
||||
if err != nil {
|
||||
ctx.ServerError("GetUserByID", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = models.DeleteUser(u); err != nil {
|
||||
switch {
|
||||
case models.IsErrUserOwnRepos(err):
|
||||
ctx.Flash.Error(ctx.Tr("admin.users.still_own_repo"))
|
||||
ctx.JSON(http.StatusOK, map[string]interface{}{
|
||||
"redirect": setting.AppSubURL + "/admin/users/" + ctx.Params(":userid"),
|
||||
})
|
||||
case models.IsErrUserHasOrgs(err):
|
||||
ctx.Flash.Error(ctx.Tr("admin.users.still_has_org"))
|
||||
ctx.JSON(http.StatusOK, map[string]interface{}{
|
||||
"redirect": setting.AppSubURL + "/admin/users/" + ctx.Params(":userid"),
|
||||
})
|
||||
default:
|
||||
ctx.ServerError("DeleteUser", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
log.Trace("Account deleted by admin (%s): %s", ctx.User.Name, u.Name)
|
||||
|
||||
ctx.Flash.Success(ctx.Tr("admin.users.deletion_success"))
|
||||
ctx.JSON(http.StatusOK, map[string]interface{}{
|
||||
"redirect": setting.AppSubURL + "/admin/users",
|
||||
})
|
||||
}
|
123
routers/web/admin/users_test.go
Normal file
123
routers/web/admin/users_test.go
Normal file
@@ -0,0 +1,123 @@
|
||||
// Copyright 2017 The Gitea Authors. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package admin
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/models"
|
||||
"code.gitea.io/gitea/modules/test"
|
||||
"code.gitea.io/gitea/modules/web"
|
||||
"code.gitea.io/gitea/services/forms"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestNewUserPost_MustChangePassword(t *testing.T) {
|
||||
|
||||
models.PrepareTestEnv(t)
|
||||
ctx := test.MockContext(t, "admin/users/new")
|
||||
|
||||
u := models.AssertExistsAndLoadBean(t, &models.User{
|
||||
IsAdmin: true,
|
||||
ID: 2,
|
||||
}).(*models.User)
|
||||
|
||||
ctx.User = 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 := models.GetUserByName(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) {
|
||||
|
||||
models.PrepareTestEnv(t)
|
||||
ctx := test.MockContext(t, "admin/users/new")
|
||||
|
||||
u := models.AssertExistsAndLoadBean(t, &models.User{
|
||||
IsAdmin: true,
|
||||
ID: 2,
|
||||
}).(*models.User)
|
||||
|
||||
ctx.User = 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 := models.GetUserByName(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) {
|
||||
|
||||
models.PrepareTestEnv(t)
|
||||
ctx := test.MockContext(t, "admin/users/new")
|
||||
|
||||
u := models.AssertExistsAndLoadBean(t, &models.User{
|
||||
IsAdmin: true,
|
||||
ID: 2,
|
||||
}).(*models.User)
|
||||
|
||||
ctx.User = 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)
|
||||
}
|
Reference in New Issue
Block a user