summaryrefslogtreecommitdiffstats
path: root/routers/web/admin
diff options
context:
space:
mode:
authorLunny Xiao <xiaolunwen@gmail.com>2021-06-09 07:33:54 +0800
committerGitHub <noreply@github.com>2021-06-09 01:33:54 +0200
commit1bfb0a24d843e10d6d95c4319a84980485e584ed (patch)
treee4a736f9abee3eaad1270bf3b60ee3bb9401a9dc /routers/web/admin
parente03a91a48ef7fb716cc7c8bfb411ca8f332dcfe5 (diff)
downloadgitea-1bfb0a24d843e10d6d95c4319a84980485e584ed.tar.gz
gitea-1bfb0a24d843e10d6d95c4319a84980485e584ed.zip
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>
Diffstat (limited to 'routers/web/admin')
-rw-r--r--routers/web/admin/admin.go485
-rw-r--r--routers/web/admin/admin_test.go69
-rw-r--r--routers/web/admin/auths.go410
-rw-r--r--routers/web/admin/emails.go156
-rw-r--r--routers/web/admin/hooks.go72
-rw-r--r--routers/web/admin/main_test.go16
-rw-r--r--routers/web/admin/notice.go79
-rw-r--r--routers/web/admin/orgs.go34
-rw-r--r--routers/web/admin/repos.go166
-rw-r--r--routers/web/admin/users.go371
-rw-r--r--routers/web/admin/users_test.go123
11 files changed, 1981 insertions, 0 deletions
diff --git a/routers/web/admin/admin.go b/routers/web/admin/admin.go
new file mode 100644
index 0000000000..c2d94ab9c9
--- /dev/null
+++ b/routers/web/admin/admin.go
@@ -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))
+}
diff --git a/routers/web/admin/admin_test.go b/routers/web/admin/admin_test.go
new file mode 100644
index 0000000000..da404e50d7
--- /dev/null
+++ b/routers/web/admin/admin_test.go
@@ -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))
+ }
+}
diff --git a/routers/web/admin/auths.go b/routers/web/admin/auths.go
new file mode 100644
index 0000000000..a2f9ab0a5c
--- /dev/null
+++ b/routers/web/admin/auths.go
@@ -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",
+ })
+}
diff --git a/routers/web/admin/emails.go b/routers/web/admin/emails.go
new file mode 100644
index 0000000000..f7e8c97fb6
--- /dev/null
+++ b/routers/web/admin/emails.go
@@ -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())
+}
diff --git a/routers/web/admin/hooks.go b/routers/web/admin/hooks.go
new file mode 100644
index 0000000000..ff32260cc0
--- /dev/null
+++ b/routers/web/admin/hooks.go
@@ -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",
+ })
+}
diff --git a/routers/web/admin/main_test.go b/routers/web/admin/main_test.go
new file mode 100644
index 0000000000..352907c737
--- /dev/null
+++ b/routers/web/admin/main_test.go
@@ -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("..", "..", ".."))
+}
diff --git a/routers/web/admin/notice.go b/routers/web/admin/notice.go
new file mode 100644
index 0000000000..e2ebd0d917
--- /dev/null
+++ b/routers/web/admin/notice.go
@@ -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")
+}
diff --git a/routers/web/admin/orgs.go b/routers/web/admin/orgs.go
new file mode 100644
index 0000000000..618f945704
--- /dev/null
+++ b/routers/web/admin/orgs.go
@@ -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)
+}
diff --git a/routers/web/admin/repos.go b/routers/web/admin/repos.go
new file mode 100644
index 0000000000..6128992f5a
--- /dev/null
+++ b/routers/web/admin/repos.go
@@ -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))
+}
diff --git a/routers/web/admin/users.go b/routers/web/admin/users.go
new file mode 100644
index 0000000000..1b65795865
--- /dev/null
+++ b/routers/web/admin/users.go
@@ -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",
+ })
+}
diff --git a/routers/web/admin/users_test.go b/routers/web/admin/users_test.go
new file mode 100644
index 0000000000..b19dcb886b
--- /dev/null
+++ b/routers/web/admin/users_test.go
@@ -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)
+}