refactor: "install" page (#39400)

1. remove useless options
2. set AppDataPath instead of repo root path
3. make "disable self-registration" default enabled
4. avoid writing corrupted ini file
5. avoid auto-sign-in the existing admin user
This commit is contained in:
wxiaoguang authored and GitHub committed 2026-09-23 23:14:31 +00:00
1 parent 71065941a9
commit f14cedc4fa
35 files changed
+261 -439

No files matched your search

+3 -3
View File
@@ -99,14 +99,14 @@ func runACME(listenAddr string, m http.Handler) error {
// takes HTTPS down on restart (https://github.com/go-gitea/gitea/issues/38519). // takes HTTPS down on restart (https://github.com/go-gitea/gitea/issues/38519).
// Prefer keeping the existing cert and retrying renewals asynchronously. // Prefer keeping the existing cert and retrying renewals asynchronously.
ctx := graceful.GetManager().ShutdownContext() ctx := graceful.GetManager().ShutdownContext()
err := magic.ManageSync(ctx, []string{setting.Domain}) err := magic.ManageSync(ctx, []string{setting.AppDomain})
if err != nil { if err != nil {
cert, cacheErr := magic.CacheManagedCertificate(ctx, setting.Domain) cert, cacheErr := magic.CacheManagedCertificate(ctx, setting.AppDomain)
if cacheErr != nil || cert.Expired() { if cacheErr != nil || cert.Expired() {
return errors.Join(err, cacheErr) return errors.Join(err, cacheErr)
} }
log.Error("ACME certificate manage failed; continuing with existing certificate: %v", err) log.Error("ACME certificate manage failed; continuing with existing certificate: %v", err)
if err := magic.ManageAsync(ctx, []string{setting.Domain}); err != nil { if err := magic.ManageAsync(ctx, []string{setting.AppDomain}); err != nil {
log.Error("Failed to start async ACME management: %v", err) log.Error("Failed to start async ACME management: %v", err)
} }
} }
+4 -7
View File
@@ -62,10 +62,7 @@
;; The protocol the server listens on. One of "http", "https", "http+unix", "fcgi" or "fcgi+unix". ;; The protocol the server listens on. One of "http", "https", "http+unix", "fcgi" or "fcgi+unix".
;PROTOCOL = http ;PROTOCOL = http
;; ;;
;; Set the domain for the server. ;; The AppURL is used to generate public URL links, defaults to "{PROTOCOL}://localhost:{HTTP_PORT}/".
;DOMAIN = localhost
;;
;; The AppURL is used to generate public URL links, defaults to "{PROTOCOL}://{DOMAIN}:{HTTP_PORT}/".
;; Most users should set it to the real website URL of their Gitea instance when there is a reverse proxy. ;; Most users should set it to the real website URL of their Gitea instance when there is a reverse proxy.
;ROOT_URL = ;ROOT_URL =
;; ;;
@@ -1735,12 +1732,12 @@ LEVEL = Info
;; - login.launchpad.net ;; - login.launchpad.net
;; - <username>.livejournal.com ;; - <username>.livejournal.com
;; ;;
;; Whether to allow signin in via OpenID ;; Whether to allow sign-in via OpenID
;ENABLE_OPENID_SIGNIN = true ;ENABLE_OPENID_SIGNIN = false
;; ;;
;; Whether to allow registering via OpenID ;; Whether to allow registering via OpenID
;; Do not include to rely on rhw DISABLE_REGISTRATION setting ;; Do not include to rely on rhw DISABLE_REGISTRATION setting
;;ENABLE_OPENID_SIGNUP = true ;;ENABLE_OPENID_SIGNUP = false
;; ;;
;; Allowed URI patterns (POSIX regexp). ;; Allowed URI patterns (POSIX regexp).
;; Space separated. ;; Space separated.
+1 -12
View File
@@ -7,7 +7,6 @@ import (
"context" "context"
"gitea.dev/models/db" "gitea.dev/models/db"
"gitea.dev/modules/setting"
) )
// CheckDatabaseConnection checks the database connection // CheckDatabaseConnection checks the database connection
@@ -45,15 +44,5 @@ func HasPostInstallationUsers(ctx context.Context) (bool, error) {
return false, nil return false, nil
} }
// if there are 2 or more users in database, we consider there are users created after installation return x.Table("user").Exist()
threshold := 2
if !setting.IsProd {
// to debug easily, with non-prod RUN_MODE, we only check the count to 1
threshold = 1
}
res, err := x.Table("user").Cols("id").Limit(threshold).Query()
if err != nil {
return false, err
}
return len(res) >= threshold, nil
} }
-1
View File
@@ -61,7 +61,6 @@ func mainTest(m *testing.M, testOptsArg ...*TestOptions) int {
} }
setting.AppURL = "https://try.gitea.io/" setting.AppURL = "https://try.gitea.io/"
setting.Domain = "try.gitea.io"
setting.RunUser = "runuser" setting.RunUser = "runuser"
setting.SSH.User = "sshuser" setting.SSH.User = "sshuser"
setting.SSH.BuiltinServerUser = "builtinuser" setting.SSH.BuiltinServerUser = "builtinuser"
-12
View File
@@ -62,15 +62,3 @@ func SetDefaultPasswordHashAlgorithm(algorithmName string) (string, *PasswordHas
DefaultHashAlgorithm = Parse(algoSpec) DefaultHashAlgorithm = Parse(algoSpec)
return algoSpec, DefaultHashAlgorithm return algoSpec, DefaultHashAlgorithm
} }
// ConfigHashAlgorithm will try to find a "recommended algorithm name" defined by RecommendedHashAlgorithms for config
// This function is not fast and is only used for the installation page
func ConfigHashAlgorithm(algorithm string) string {
algorithm = hashAlgorithmToSpec(algorithm)
for _, recommAlgo := range RecommendedHashAlgorithms {
if algorithm == hashAlgorithmToSpec(recommAlgo) {
return recommAlgo
}
}
return algorithm
}
+1 -1
View File
@@ -29,7 +29,7 @@ func Init() {
WebAuthn = &webauthn.WebAuthn{ WebAuthn = &webauthn.WebAuthn{
Config: &webauthn.Config{ Config: &webauthn.Config{
RPDisplayName: setting.AppName, RPDisplayName: setting.AppName,
RPID: setting.Domain, RPID: setting.AppDomain,
RPOrigins: []string{appURL}, RPOrigins: []string{appURL},
AttestationPreference: protocol.PreferNoAttestation, // Gitea never verifies attestation AttestationPreference: protocol.PreferNoAttestation, // Gitea never verifies attestation
}, },
+2 -2
View File
@@ -12,14 +12,14 @@ import (
) )
func TestInit(t *testing.T) { func TestInit(t *testing.T) {
setting.Domain = "domain" setting.AppDomain = "domain"
setting.AppName = "AppName" setting.AppName = "AppName"
setting.AppURL = "https://domain/" setting.AppURL = "https://domain/"
rpOrigin := []string{"https://domain"} rpOrigin := []string{"https://domain"}
Init() Init()
assert.Equal(t, setting.Domain, WebAuthn.Config.RPID) assert.Equal(t, setting.AppDomain, WebAuthn.Config.RPID)
assert.Equal(t, setting.AppName, WebAuthn.Config.RPDisplayName) assert.Equal(t, setting.AppName, WebAuthn.Config.RPDisplayName)
assert.Equal(t, rpOrigin, WebAuthn.Config.RPOrigins) assert.Equal(t, rpOrigin, WebAuthn.Config.RPOrigins)
} }
+1 -15
View File
@@ -3,13 +3,6 @@
package setting package setting
import (
"net/url"
"path"
"gitea.dev/modules/log"
)
// API settings // API settings
var API = struct { var API = struct {
EnableSwagger bool EnableSwagger bool
@@ -31,12 +24,5 @@ var API = struct {
func loadAPIFrom(rootCfg ConfigProvider) { func loadAPIFrom(rootCfg ConfigProvider) {
mustMapSetting(rootCfg, "api", &API) mustMapSetting(rootCfg, "api", &API)
API.SwaggerURL = AppURL + "api/swagger"
defaultAppURL := string(Protocol) + "://" + Domain + ":" + HTTPPort
u, err := url.Parse(rootCfg.Section("server").Key("ROOT_URL").MustString(defaultAppURL))
if err != nil {
log.Fatal("Invalid ROOT_URL '%s': %s", AppURL, err)
}
u.Path = path.Join(u.Path, "api", "swagger")
API.SwaggerURL = u.String()
} }
+32 -34
View File
@@ -49,7 +49,8 @@ const (
var ( var (
// AppURL is the Application ROOT_URL. It always has a '/' suffix // AppURL is the Application ROOT_URL. It always has a '/' suffix
// It maps to ini:"ROOT_URL" // It maps to ini:"ROOT_URL"
AppURL string AppURL string
AppDomain string
// PublicURLDetection controls how to use the HTTP request headers to detect public URL // PublicURLDetection controls how to use the HTTP request headers to detect public URL
PublicURLDetection string PublicURLDetection string
@@ -80,7 +81,6 @@ var (
ProxyProtocolTLSBridging bool ProxyProtocolTLSBridging bool
ProxyProtocolHeaderTimeout time.Duration ProxyProtocolHeaderTimeout time.Duration
ProxyProtocolAcceptUnknown bool ProxyProtocolAcceptUnknown bool
Domain string
HTTPAddr string HTTPAddr string
HTTPPort string HTTPPort string
LocalUseProxyProtocol bool LocalUseProxyProtocol bool
@@ -114,11 +114,39 @@ var (
StaticURLPrefix string // no trailing slash, defaults to AppSubURL, the URL can be relative or absolute StaticURLPrefix string // no trailing slash, defaults to AppSubURL, the URL can be relative or absolute
) )
func loadServerDomainAndURL(sec ConfigSection, protocol string) {
defaultAppURL := protocol + "://localhost:" + HTTPPort
AppURL = sec.Key("ROOT_URL").MustString(defaultAppURL)
PublicURLDetection = sec.Key("PUBLIC_URL_DETECTION").MustString(PublicURLAuto)
if PublicURLDetection != PublicURLAuto && PublicURLDetection != PublicURLLegacy && PublicURLDetection != PublicURLNever {
log.Fatal("Invalid PUBLIC_URL_DETECTION value: %s", PublicURLDetection)
}
// Check validity of AppURL
appURL, err := url.Parse(AppURL)
if err != nil {
log.Fatal("Invalid ROOT_URL %q: %s", AppURL, err)
}
// Remove default ports from AppURL.
// (scheme-based URL normalization, RFC 3986 section 6.2.3)
if (appURL.Scheme == string(HTTP) && appURL.Port() == "80") || (appURL.Scheme == string(HTTPS) && appURL.Port() == "443") {
appURL.Host = appURL.Hostname()
}
// This should be TrimRight to ensure that there is only a single '/' at the end of AppURL.
AppURL = strings.TrimRight(appURL.String(), "/") + "/"
// AppSubURL should start with '/' and end without '/', such as '/{subpath}'.
// This value is empty if site does not have sub-url.
AppSubURL = strings.TrimSuffix(appURL.Path, "/")
UseSubURLPath = sec.Key("USE_SUB_URL_PATH").MustBool(false)
StaticURLPrefix = strings.TrimSuffix(sec.Key("STATIC_URL_PREFIX").MustString(AppSubURL), "/")
AppDomain = appURL.Hostname()
}
func loadServerFrom(rootCfg ConfigProvider) { func loadServerFrom(rootCfg ConfigProvider) {
sec := rootCfg.Section("server") sec := rootCfg.Section("server")
AppName = rootCfg.Section("").Key("APP_NAME").MustString("Gitea: Git with a cup of tea") AppName = rootCfg.Section("").Key("APP_NAME").MustString("Gitea: Git with a cup of tea")
Domain = sec.Key("DOMAIN").MustString("localhost")
HTTPAddr = sec.Key("HTTP_ADDR").MustString("0.0.0.0") HTTPAddr = sec.Key("HTTP_ADDR").MustString("0.0.0.0")
HTTPPort = sec.Key("HTTP_PORT").MustString("3000") HTTPPort = sec.Key("HTTP_PORT").MustString("3000")
@@ -217,37 +245,7 @@ func loadServerFrom(rootCfg ConfigProvider) {
PerWriteTimeout = sec.Key("PER_WRITE_TIMEOUT").MustDuration(PerWriteTimeout) PerWriteTimeout = sec.Key("PER_WRITE_TIMEOUT").MustDuration(PerWriteTimeout)
PerWritePerKbTimeout = sec.Key("PER_WRITE_PER_KB_TIMEOUT").MustDuration(PerWritePerKbTimeout) PerWritePerKbTimeout = sec.Key("PER_WRITE_PER_KB_TIMEOUT").MustDuration(PerWritePerKbTimeout)
defaultAppURL := string(Protocol) + "://" + Domain + ":" + HTTPPort loadServerDomainAndURL(sec, string(Protocol))
AppURL = sec.Key("ROOT_URL").MustString(defaultAppURL)
PublicURLDetection = sec.Key("PUBLIC_URL_DETECTION").MustString(PublicURLAuto)
if PublicURLDetection != PublicURLAuto && PublicURLDetection != PublicURLLegacy && PublicURLDetection != PublicURLNever {
log.Fatal("Invalid PUBLIC_URL_DETECTION value: %s", PublicURLDetection)
}
// Check validity of AppURL
appURL, err := url.Parse(AppURL)
if err != nil {
log.Fatal("Invalid ROOT_URL %q: %s", AppURL, err)
}
// Remove default ports from AppURL.
// (scheme-based URL normalization, RFC 3986 section 6.2.3)
if (appURL.Scheme == string(HTTP) && appURL.Port() == "80") || (appURL.Scheme == string(HTTPS) && appURL.Port() == "443") {
appURL.Host = appURL.Hostname()
}
// This should be TrimRight to ensure that there is only a single '/' at the end of AppURL.
AppURL = strings.TrimRight(appURL.String(), "/") + "/"
// AppSubURL should start with '/' and end without '/', such as '/{subpath}'.
// This value is empty if site does not have sub-url.
AppSubURL = strings.TrimSuffix(appURL.Path, "/")
UseSubURLPath = sec.Key("USE_SUB_URL_PATH").MustBool(false)
StaticURLPrefix = strings.TrimSuffix(sec.Key("STATIC_URL_PREFIX").MustString(AppSubURL), "/")
// Check if Domain differs from AppURL domain than update it to AppURL's domain
urlHostname := appURL.Hostname()
if urlHostname != Domain && net.ParseIP(urlHostname) == nil && urlHostname != "" {
Domain = urlHostname
}
var defaultLocalURL string var defaultLocalURL string
switch Protocol { switch Protocol {
+4 -4
View File
@@ -145,7 +145,7 @@ func loadServiceFrom(rootCfg ConfigProvider) {
sec := rootCfg.Section("service") sec := rootCfg.Section("service")
Service.ActiveCodeLives = sec.Key("ACTIVE_CODE_LIVE_MINUTES").MustInt(180) Service.ActiveCodeLives = sec.Key("ACTIVE_CODE_LIVE_MINUTES").MustInt(180)
Service.ResetPwdCodeLives = sec.Key("RESET_PASSWD_CODE_LIVE_MINUTES").MustInt(180) Service.ResetPwdCodeLives = sec.Key("RESET_PASSWD_CODE_LIVE_MINUTES").MustInt(180)
Service.DisableRegistration = sec.Key("DISABLE_REGISTRATION").MustBool() Service.DisableRegistration = sec.Key("DISABLE_REGISTRATION").MustBool(true)
Service.AllowOnlyInternalRegistration = sec.Key("ALLOW_ONLY_INTERNAL_REGISTRATION").MustBool() Service.AllowOnlyInternalRegistration = sec.Key("ALLOW_ONLY_INTERNAL_REGISTRATION").MustBool()
Service.AllowOnlyExternalRegistration = sec.Key("ALLOW_ONLY_EXTERNAL_REGISTRATION").MustBool() Service.AllowOnlyExternalRegistration = sec.Key("ALLOW_ONLY_EXTERNAL_REGISTRATION").MustBool()
if Service.AllowOnlyExternalRegistration && Service.AllowOnlyInternalRegistration { if Service.AllowOnlyExternalRegistration && Service.AllowOnlyInternalRegistration {
@@ -209,7 +209,7 @@ func loadServiceFrom(rootCfg ConfigProvider) {
Service.DefaultEnableDependencies = sec.Key("DEFAULT_ENABLE_DEPENDENCIES").MustBool(true) Service.DefaultEnableDependencies = sec.Key("DEFAULT_ENABLE_DEPENDENCIES").MustBool(true)
Service.AllowCrossRepositoryDependencies = sec.Key("ALLOW_CROSS_REPOSITORY_DEPENDENCIES").MustBool(true) Service.AllowCrossRepositoryDependencies = sec.Key("ALLOW_CROSS_REPOSITORY_DEPENDENCIES").MustBool(true)
Service.DefaultAllowOnlyContributorsToTrackTime = sec.Key("DEFAULT_ALLOW_ONLY_CONTRIBUTORS_TO_TRACK_TIME").MustBool(true) Service.DefaultAllowOnlyContributorsToTrackTime = sec.Key("DEFAULT_ALLOW_ONLY_CONTRIBUTORS_TO_TRACK_TIME").MustBool(true)
Service.NoReplyAddress = sec.Key("NO_REPLY_ADDRESS").MustString("noreply." + Domain) Service.NoReplyAddress = sec.Key("NO_REPLY_ADDRESS").MustString("noreply." + AppDomain)
Service.UserLocationMapURL = sec.Key("USER_LOCATION_MAP_URL").String() Service.UserLocationMapURL = sec.Key("USER_LOCATION_MAP_URL").String()
Service.EnableUserHeatmap = sec.Key("ENABLE_USER_HEATMAP").MustBool(true) Service.EnableUserHeatmap = sec.Key("ENABLE_USER_HEATMAP").MustBool(true)
Service.AutoWatchNewRepos = sec.Key("AUTO_WATCH_NEW_REPOS").MustBool(true) Service.AutoWatchNewRepos = sec.Key("AUTO_WATCH_NEW_REPOS").MustBool(true)
@@ -263,8 +263,8 @@ func loadServiceFrom(rootCfg ConfigProvider) {
func loadOpenIDSetting(rootCfg ConfigProvider) { func loadOpenIDSetting(rootCfg ConfigProvider) {
sec := rootCfg.Section("openid") sec := rootCfg.Section("openid")
Service.EnableOpenIDSignIn = sec.Key("ENABLE_OPENID_SIGNIN").MustBool(!InstallLock) Service.EnableOpenIDSignIn = sec.Key("ENABLE_OPENID_SIGNIN").MustBool(false)
Service.EnableOpenIDSignUp = sec.Key("ENABLE_OPENID_SIGNUP").MustBool(!Service.DisableRegistration && Service.EnableOpenIDSignIn) Service.EnableOpenIDSignUp = sec.Key("ENABLE_OPENID_SIGNUP").MustBool(false)
pats := sec.Key("WHITELISTED_URIS").Strings(" ") pats := sec.Key("WHITELISTED_URIS").Strings(" ")
if len(pats) != 0 { if len(pats) != 0 {
Service.OpenIDWhitelist = make([]*regexp.Regexp, len(pats)) Service.OpenIDWhitelist = make([]*regexp.Regexp, len(pats))
+1 -1
View File
@@ -42,7 +42,7 @@ var SessionConfig = struct {
func loadSessionFrom(rootCfg ConfigProvider) { func loadSessionFrom(rootCfg ConfigProvider) {
sec := rootCfg.Section("session") sec := rootCfg.Section("session")
SessionConfig.Provider = sec.Key("PROVIDER").In("memory", []string{"memory", "file", "redis", "mysql", "postgres", "couchbase", "memcache", "db"}) SessionConfig.Provider = sec.Key("PROVIDER").In("file", []string{"memory", "file", "redis", "mysql", "postgres", "couchbase", "memcache", "db"})
switch SessionConfig.Provider { switch SessionConfig.Provider {
case "redis": case "redis":
+7 -4
View File
@@ -7,6 +7,7 @@ package setting
import ( import (
"fmt" "fmt"
"os" "os"
"path/filepath"
"runtime" "runtime"
"strings" "strings"
"time" "time"
@@ -71,22 +72,24 @@ func PrepareAppDataPath() error {
// The correct behavior should be: creating parent directories is end users' duty. We only create sub-directories in existing parent directories. // The correct behavior should be: creating parent directories is end users' duty. We only create sub-directories in existing parent directories.
// For quickstart, the parent directories should be created automatically for first startup (eg: a flag or a check of INSTALL_LOCK). // For quickstart, the parent directories should be created automatically for first startup (eg: a flag or a check of INSTALL_LOCK).
// Now we can take the first step to do correctly (using Mkdir) in other packages, and prepare the AppDataPath here, then make a refactor in future. // Now we can take the first step to do correctly (using Mkdir) in other packages, and prepare the AppDataPath here, then make a refactor in future.
if !filepath.IsAbs(AppDataPath) {
return fmt.Errorf("app data path %q must be an absolute path", AppDataPath)
}
st, err := os.Stat(AppDataPath) st, err := os.Stat(AppDataPath)
if os.IsNotExist(err) { if os.IsNotExist(err) {
err = os.MkdirAll(AppDataPath, os.ModePerm) err = os.MkdirAll(AppDataPath, os.ModePerm)
if err != nil { if err != nil {
return fmt.Errorf("unable to create the APP_DATA_PATH directory: %q, Error: %w", AppDataPath, err) return fmt.Errorf("unable to create the app data path directory: %q, Error: %w", AppDataPath, err)
} }
return nil return nil
} }
if err != nil { if err != nil {
return fmt.Errorf("unable to use APP_DATA_PATH %q. Error: %w", AppDataPath, err) return fmt.Errorf("unable to use app data path %q. Error: %w", AppDataPath, err)
} }
if !st.IsDir() /* also works for symlink */ { if !st.IsDir() /* also works for symlink */ {
return fmt.Errorf("the APP_DATA_PATH %q is not a directory (or symlink to a directory) and can't be used", AppDataPath) return fmt.Errorf("the app data path %q is not a directory (or symlink to a directory) and can't be used", AppDataPath)
} }
return nil return nil
+2 -6
View File
@@ -93,19 +93,15 @@ func parseAuthorizedPrincipalsAllow(values []string) ([]string, bool) {
} }
func loadSSHFrom(rootCfg ConfigProvider) { func loadSSHFrom(rootCfg ConfigProvider) {
sec := rootCfg.Section("server")
if len(SSH.Domain) == 0 {
SSH.Domain = Domain
}
homeDir, err := util.HomeDir() homeDir, err := util.HomeDir()
if err != nil { if err != nil {
log.Fatal("Failed to get home directory: %v", err) log.Fatal("Failed to get home directory: %v", err)
} }
homeDir = strings.ReplaceAll(homeDir, "\\", "/") homeDir = strings.ReplaceAll(homeDir, "\\", "/")
SSH.RootPath = filepath.Join(homeDir, ".ssh") SSH.RootPath = filepath.Join(homeDir, ".ssh")
SSH.Domain = AppDomain
sec := rootCfg.Section("server")
if err = sec.MapTo(&SSH); err != nil { if err = sec.MapTo(&SSH); err != nil {
log.Fatal("Failed to map SSH settings: %v", err) log.Fatal("Failed to map SSH settings: %v", err)
} }
+1 -1
View File
@@ -85,7 +85,7 @@ func newFuncMapWebPage() template.FuncMap {
return setting.AppVer return setting.AppVer
}, },
"AppDomain": func() string { // TODO: helm registry still uses it, need to use current request host in the future "AppDomain": func() string { // TODO: helm registry still uses it, need to use current request host in the future
return setting.Domain return setting.AppDomain
}, },
"ShowFooterTemplateLoadTime": func() bool { "ShowFooterTemplateLoadTime": func() bool {
return setting.Other.ShowFooterTemplateLoadTime return setting.Other.ShowFooterTemplateLoadTime
+2 -2
View File
@@ -54,7 +54,7 @@ func mailSubjectTextFuncMap() texttmpl.FuncMap {
return setting.AppName return setting.AppName
}, },
"AppDomain": func() string { // documented in mail-templates.md "AppDomain": func() string { // documented in mail-templates.md
return setting.Domain return setting.AppDomain
}, },
} }
} }
@@ -99,7 +99,7 @@ func mailBodyFuncMap() template.FuncMap {
return setting.AppURL return setting.AppURL
}, },
"AppDomain": func() string { "AppDomain": func() string {
return setting.Domain return setting.AppDomain
}, },
} }
} }
+11 -26
View File
@@ -244,6 +244,7 @@
"install.installing_desc": "Installing now, please wait…", "install.installing_desc": "Installing now, please wait…",
"install.title": "Initial Configuration", "install.title": "Initial Configuration",
"install.docker_helper": "If you run Gitea inside Docker, please read the <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"%s\">documentation</a> before changing any settings.", "install.docker_helper": "If you run Gitea inside Docker, please read the <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"%s\">documentation</a> before changing any settings.",
"install.config_edit_helper": "To fine tune more details: edit the configuration file after installation. Reference: <a target=\"_blank\" href=\"%s\">app.example.ini</a>.",
"install.require_db_desc": "Gitea requires MySQL, PostgreSQL, MSSQL, SQLite3 or TiDB (MySQL protocol).", "install.require_db_desc": "Gitea requires MySQL, PostgreSQL, MSSQL, SQLite3 or TiDB (MySQL protocol).",
"install.db_title": "Database Settings", "install.db_title": "Database Settings",
"install.db_type": "Database Type", "install.db_type": "Database Type",
@@ -263,6 +264,7 @@
"install.reinstall_confirm_check_3": "You confirm that you are absolutely sure that this Gitea is running with the correct app.ini location and that you are sure that you have to re-install. You confirm that you acknowledge the above risks.", "install.reinstall_confirm_check_3": "You confirm that you are absolutely sure that this Gitea is running with the correct app.ini location and that you are sure that you have to re-install. You confirm that you acknowledge the above risks.",
"install.err_empty_db_path": "The SQLite3 database path cannot be empty.", "install.err_empty_db_path": "The SQLite3 database path cannot be empty.",
"install.no_admin_and_disable_registration": "You cannot disable user self-registration without creating an administrator account.", "install.no_admin_and_disable_registration": "You cannot disable user self-registration without creating an administrator account.",
"install.admin_user_recreation_disallowed": "You cannot re-create an admin user since the database user table is not empty, you can use 'gitea admin user ...' sub-commands to manage admin users after installation.",
"install.err_empty_admin_password": "The administrator password cannot be empty.", "install.err_empty_admin_password": "The administrator password cannot be empty.",
"install.err_empty_admin_email": "The administrator email address cannot be empty.", "install.err_empty_admin_email": "The administrator email address cannot be empty.",
"install.err_admin_name_is_reserved": "Administrator username is invalid. Username is reserved.", "install.err_admin_name_is_reserved": "Administrator username is invalid. Username is reserved.",
@@ -271,22 +273,18 @@
"install.general_title": "General Settings", "install.general_title": "General Settings",
"install.app_name": "Site Title", "install.app_name": "Site Title",
"install.app_name_helper": "You can enter your company name here.", "install.app_name_helper": "You can enter your company name here.",
"install.repo_path": "Repository Root Path", "install.app_data_path": "Data Path",
"install.repo_path_helper": "Remote Git repositories will be saved to this directory.", "install.app_data_path_helper": "Most Gitea data will be saved to this directory.",
"install.lfs_path": "Git LFS Root Path",
"install.lfs_path_helper": "Files tracked by Git LFS will be stored in this directory. Leave empty to disable.",
"install.run_user": "Run As Username", "install.run_user": "Run As Username",
"install.run_user_helper": "The operating system username that Gitea runs as, it must have write access to the data paths. This value is auto-detected and cannot be changed here. To use a different user, restart Gitea under that account.", "install.run_user_helper": "The operating system username that Gitea runs as, it must have write access to the data paths. This value is auto-detected and cannot be changed here. To use a different user, restart Gitea under that account.",
"install.domain": "Server Domain", "install.domain": "Server Domain",
"install.domain_helper": "Domain or host address for the server.", "install.domain_helper": "Domain or host address for the server.",
"install.ssh_port": "SSH Server Port", "install.ssh_port": "SSH Server Port",
"install.ssh_port_helper": "Port number your SSH server listens on. Leave empty to disable.", "install.ssh_port_helper": "Port number your SSH server listens on. Leave empty to disable.",
"install.http_port": "Gitea HTTP Listen Port", "install.http_port": "HTTP Server Port",
"install.http_port_helper": "Port number the Gitea web server will listen on.", "install.http_port_helper": "Port number the Gitea web server will listen on.",
"install.app_url": "Gitea Base URL", "install.app_url": "Gitea Website URL",
"install.app_url_helper": "Base address for HTTP(S) clone URLs and email notifications.", "install.app_url_helper": "Gitea web application's public URL for HTTP(S) access, clone URLs and email notifications.",
"install.log_root_path": "Log Path",
"install.log_root_path_helper": "Log files will be written to this directory.",
"install.optional_title": "Optional Settings", "install.optional_title": "Optional Settings",
"install.email_title": "Email Settings", "install.email_title": "Email Settings",
"install.smtp_addr": "SMTP Host", "install.smtp_addr": "SMTP Host",
@@ -299,17 +297,11 @@
"install.register_confirm": "Require Email Confirmation to Register", "install.register_confirm": "Require Email Confirmation to Register",
"install.mail_notify": "Enable Email Notifications", "install.mail_notify": "Enable Email Notifications",
"install.server_service_title": "Server and Third-Party Service Settings", "install.server_service_title": "Server and Third-Party Service Settings",
"install.disable_registration": "Disable Self-Registration", "install.disable_registration": "Only administrators can create user accounts (no self-registration)",
"install.disable_registration_popup": "Disable user self-registration. Only administrators will be able to create new user accounts.",
"install.allow_only_external_registration_popup": "Allow Registration Only Through External Services",
"install.openid_signin": "Enable OpenID Sign-In",
"install.openid_signin_popup": "Enable user sign-in via OpenID.",
"install.openid_signup": "Enable OpenID Self-Registration",
"install.openid_signup_popup": "Enable OpenID-based user self-registration.",
"install.enable_captcha": "Enable registration CAPTCHA", "install.enable_captcha": "Enable registration CAPTCHA",
"install.enable_captcha_popup": "Require a CAPTCHA for user self-registration.", "install.enable_captcha_popup": "Require a CAPTCHA for user self-registration.",
"install.require_sign_in_view": "Require Sign-In to View Pages", "install.require_sign_in_view": "Require sign-in to view pages (recommended for private instances)",
"install.require_sign_in_view_popup": "Limit page access to signed-in users. Visitors will only see the sign-in and registration pages.", "install.require_sign_in_view_popup": "Limit page access to signed-in users. Anonymous visitors will only see the sign-in and registration pages.",
"install.admin_setting_desc": "Creating an administrator account is optional. The first registered user will automatically become an administrator.", "install.admin_setting_desc": "Creating an administrator account is optional. The first registered user will automatically become an administrator.",
"install.admin_title": "Administrator Account Settings", "install.admin_title": "Administrator Account Settings",
"install.admin_name": "Administrator Username", "install.admin_name": "Administrator Username",
@@ -320,7 +312,6 @@
"install.test_git_failed": "Could not test 'git' command: %v", "install.test_git_failed": "Could not test 'git' command: %v",
"install.invalid_db_setting": "The database settings are invalid: %v", "install.invalid_db_setting": "The database settings are invalid: %v",
"install.invalid_db_table": "The database table \"%s\" is invalid: %v", "install.invalid_db_table": "The database table \"%s\" is invalid: %v",
"install.invalid_repo_path": "The repository root path is invalid: %v",
"install.invalid_app_data_path": "The app data path is invalid: %v", "install.invalid_app_data_path": "The app data path is invalid: %v",
"install.internal_token_failed": "Failed to generate internal token: %v", "install.internal_token_failed": "Failed to generate internal token: %v",
"install.secret_key_failed": "Failed to generate secret key: %v", "install.secret_key_failed": "Failed to generate secret key: %v",
@@ -331,15 +322,9 @@
"install.default_keep_email_private_popup": "Hide email addresses of new user accounts by default.", "install.default_keep_email_private_popup": "Hide email addresses of new user accounts by default.",
"install.default_allow_create_organization": "Allow Creation of Organizations by Default", "install.default_allow_create_organization": "Allow Creation of Organizations by Default",
"install.default_allow_create_organization_popup": "Allow new user accounts to create organizations by default.", "install.default_allow_create_organization_popup": "Allow new user accounts to create organizations by default.",
"install.default_enable_timetracking": "Enable Time Tracking by Default",
"install.default_enable_timetracking_popup": "Enable time tracking for new repositories by default.",
"install.no_reply_address": "Hidden Email Domain", "install.no_reply_address": "Hidden Email Domain",
"install.no_reply_address_helper": "Domain name for users with a hidden email address. For example, the username 'joe' will be logged in Git as 'joe@noreply.example.org' if the hidden email domain is set to 'noreply.example.org'.", "install.no_reply_address_helper": "Domain name for users with a hidden email address. For example, the username 'joe' will be logged in Git as 'joe@noreply.example.org' if the hidden email domain is set to 'noreply.example.org'.",
"install.password_algorithm": "Password Hash Algorithm", "install.enable_update_checker": "Enable update checker",
"install.invalid_password_algorithm": "Invalid password hash algorithm",
"install.password_algorithm_helper": "Set the password hashing algorithm. Algorithms have differing requirements and strengths. The argon2 algorithm is rather secure but uses a lot of memory and may be inappropriate for small systems.",
"install.enable_update_checker": "Enable Update Checker",
"install.enable_update_checker_helper": "Checks for new version releases periodically by connecting to gitea.io.",
"install.env_config_keys": "Environment Configuration", "install.env_config_keys": "Environment Configuration",
"install.env_config_keys_prompt": "The following environment variables will also be applied to your configuration file:", "install.env_config_keys_prompt": "The following environment variables will also be applied to your configuration file:",
"install.config_write_file_prompt": "These configuration options will be written into: %s", "install.config_write_file_prompt": "These configuration options will be written into: %s",
+117 -160
View File
@@ -10,6 +10,7 @@ import (
"os" "os"
"os/exec" "os/exec"
"path/filepath" "path/filepath"
"regexp"
"slices" "slices"
"strconv" "strconv"
"strings" "strings"
@@ -19,7 +20,6 @@ import (
"gitea.dev/models/db" "gitea.dev/models/db"
db_install "gitea.dev/models/db/install" db_install "gitea.dev/models/db/install"
user_model "gitea.dev/models/user" user_model "gitea.dev/models/user"
"gitea.dev/modules/auth/password/hash"
"gitea.dev/modules/generate" "gitea.dev/modules/generate"
"gitea.dev/modules/graceful" "gitea.dev/modules/graceful"
"gitea.dev/modules/log" "gitea.dev/modules/log"
@@ -39,7 +39,6 @@ import (
) )
const ( const (
// tplInstall template for installation page
tplInstall templates.TplName = "install" tplInstall templates.TplName = "install"
tplPostInstall templates.TplName = "post-install" tplPostInstall templates.TplName = "post-install"
) )
@@ -54,10 +53,9 @@ func getSupportedDbTypeNames() (dbTypeNames []map[string]string) {
func installContexter() func(next http.Handler) http.Handler { func installContexter() func(next http.Handler) http.Handler {
return context.ContexterInstallPage(map[string]any{ return context.ContexterInstallPage(map[string]any{
"DbTypeNames": getSupportedDbTypeNames(), "DbTypeNames": getSupportedDbTypeNames(),
"EnvConfigKeys": setting.CollectEnvConfigKeys(), "EnvConfigKeys": setting.CollectEnvConfigKeys(),
"CustomConfFile": setting.CustomConf, "CustomConfFile": setting.CustomConf,
"PasswordHashAlgorithms": hash.RecommendedHashAlgorithms,
}) })
} }
@@ -87,14 +85,11 @@ func Install(ctx *context.Context) {
// Application general settings // Application general settings
form.AppName = setting.AppName form.AppName = setting.AppName
form.RepoRootPath = setting.RepoRootPath form.AppDataPath = setting.AppDataPath
form.LFSRootPath = setting.LFS.Storage.Path
form.RunUser = setting.RunUser form.RunUser = setting.RunUser
form.Domain = setting.Domain
form.SSHPort = setting.SSH.Port form.SSHPort = setting.SSH.Port
form.HTTPPort = setting.HTTPPort form.HTTPPort = setting.HTTPPort
form.AppURL = setting.AppURL form.AppURL = setting.AppURL
form.LogRootPath = setting.Log.RootPath
// E-mail service settings // E-mail service settings
if setting.MailService != nil { if setting.MailService != nil {
@@ -108,27 +103,19 @@ func Install(ctx *context.Context) {
form.MailNotify = setting.Service.EnableNotifyMail form.MailNotify = setting.Service.EnableNotifyMail
form.EnableUpdateChecker = setting.CfgProvider.Section("cron.update_checker").Key("ENABLED").MustBool(true) form.EnableUpdateChecker = setting.CfgProvider.Section("cron.update_checker").Key("ENABLED").MustBool(true)
form.EnableOpenIDSignIn = setting.Service.EnableOpenIDSignIn
form.EnableOpenIDSignUp = setting.Service.EnableOpenIDSignUp
form.DisableRegistration = setting.Service.DisableRegistration form.DisableRegistration = setting.Service.DisableRegistration
form.AllowOnlyExternalRegistration = setting.Service.AllowOnlyExternalRegistration
form.EnableCaptcha = setting.Service.EnableCaptcha form.EnableCaptcha = setting.Service.EnableCaptcha
form.RequireSignInView = setting.Service.RequireSignInViewStrict form.RequireSignInView = setting.Service.RequireSignInViewStrict
form.DefaultKeepEmailPrivate = setting.Service.DefaultKeepEmailPrivate form.DefaultKeepEmailPrivate = setting.Service.DefaultKeepEmailPrivate
form.DefaultAllowCreateOrganization = setting.Service.DefaultAllowCreateOrganization form.DefaultAllowCreateOrganization = setting.Service.DefaultAllowCreateOrganization
form.DefaultEnableTimetracking = setting.Service.DefaultEnableTimetracking
form.NoReplyAddress = setting.Service.NoReplyAddress form.NoReplyAddress = setting.Service.NoReplyAddress
form.PasswordAlgorithm = hash.ConfigHashAlgorithm(setting.PasswordHashAlgo)
middleware.AssignForm(form, ctx.Data) middleware.AssignForm(form, ctx.Data)
ctx.HTML(http.StatusOK, tplInstall) ctx.HTML(http.StatusOK, tplInstall)
} }
func checkDatabase(ctx *context.Context, form *forms.InstallForm) bool { func checkDatabase(ctx *context.Context, form *forms.InstallForm) bool {
var err error if setting.Database.Type.IsSQLite3() && setting.Database.Path == "" {
if (setting.Database.Type == setting.DatabaseTypeSQLite3) &&
len(setting.Database.Path) == 0 {
ctx.Data["Err_DbPath"] = true ctx.Data["Err_DbPath"] = true
ctx.RenderWithErrDeprecated(ctx.Tr("install.err_empty_db_path"), tplInstall, form) ctx.RenderWithErrDeprecated(ctx.Tr("install.err_empty_db_path"), tplInstall, form)
return false return false
@@ -138,13 +125,13 @@ func checkDatabase(ctx *context.Context, form *forms.InstallForm) bool {
db.UnsetDefaultEngine() db.UnsetDefaultEngine()
defer db.UnsetDefaultEngine() defer db.UnsetDefaultEngine()
if err = db.InitEngine(ctx); err != nil { if err := db.InitEngine(ctx); err != nil {
ctx.Data["Err_DbSetting"] = true ctx.Data["Err_DbSetting"] = true
ctx.RenderWithErrDeprecated(ctx.Tr("install.invalid_db_setting", err), tplInstall, form) ctx.RenderWithErrDeprecated(ctx.Tr("install.invalid_db_setting", err), tplInstall, form)
return false return false
} }
err = db_install.CheckDatabaseConnection(ctx) err := db_install.CheckDatabaseConnection(ctx)
if err != nil { if err != nil {
ctx.Data["Err_DbSetting"] = true ctx.Data["Err_DbSetting"] = true
ctx.RenderWithErrDeprecated(ctx.Tr("install.invalid_db_setting", err), tplInstall, form) ctx.RenderWithErrDeprecated(ctx.Tr("install.invalid_db_setting", err), tplInstall, form)
@@ -176,6 +163,22 @@ func checkDatabase(ctx *context.Context, form *forms.InstallForm) bool {
log.Info("User confirmed re-installation of Gitea into a pre-existing database") log.Info("User confirmed re-installation of Gitea into a pre-existing database")
} }
if hasPostInstallationUser {
// non-empty user table
if form.AdminName != "" {
ctx.Data["Err_Admin"] = true
ctx.RenderWithErrDeprecated(ctx.Tr("install.admin_user_recreation_disallowed"), tplInstall, form)
return false
}
} else {
// empty user table, check logic loophole between disable self-registration and no admin account.
if form.DisableRegistration && form.AdminName == "" {
ctx.Data["Err_Admin"] = true
ctx.RenderWithErrDeprecated(ctx.Tr("install.no_admin_and_disable_registration"), tplInstall, form)
return false
}
}
if hasPostInstallationUser || dbMigrationVersion > 0 { if hasPostInstallationUser || dbMigrationVersion > 0 {
log.Info("Gitea will be installed in a database with: hasPostInstallationUser=%v, dbMigrationVersion=%v", hasPostInstallationUser, dbMigrationVersion) log.Info("Gitea will be installed in a database with: hasPostInstallationUser=%v, dbMigrationVersion=%v", hasPostInstallationUser, dbMigrationVersion)
} }
@@ -190,8 +193,6 @@ func SubmitInstall(ctx *context.Context) {
return return
} }
var err error
form := web.GetForm[*forms.InstallForm](ctx) form := web.GetForm[*forms.InstallForm](ctx)
// fix form values // fix form values
@@ -208,7 +209,7 @@ func SubmitInstall(ctx *context.Context) {
return return
} }
if _, err = exec.LookPath("git"); err != nil { if _, err := exec.LookPath("git"); err != nil {
ctx.RenderWithErrDeprecated(ctx.Tr("install.test_git_failed", err), tplInstall, form) ctx.RenderWithErrDeprecated(ctx.Tr("install.test_git_failed", err), tplInstall, form)
return return
} }
@@ -226,52 +227,21 @@ func SubmitInstall(ctx *context.Context) {
setting.Database.Path = form.DbPath setting.Database.Path = form.DbPath
setting.Database.LogSQL = !setting.IsProd setting.Database.LogSQL = !setting.IsProd
if !checkDatabase(ctx, form) {
return
}
// Prepare AppDataPath, it is very important for Gitea // Prepare AppDataPath, it is very important for Gitea
if err = setting.PrepareAppDataPath(); err != nil { // old code replaced "\\" to "/", it's questionable whether it's worth to do so
form.AppDataPath = strings.ReplaceAll(form.AppDataPath, "\\", "/")
setting.AppDataPath = form.AppDataPath
if err := setting.PrepareAppDataPath(); err != nil {
ctx.RenderWithErrDeprecated(ctx.Tr("install.invalid_app_data_path", err), tplInstall, form) ctx.RenderWithErrDeprecated(ctx.Tr("install.invalid_app_data_path", err), tplInstall, form)
return return
} }
// Test repository root path. if !checkDatabase(ctx, form) {
form.RepoRootPath = strings.ReplaceAll(form.RepoRootPath, "\\", "/")
if err = os.MkdirAll(form.RepoRootPath, os.ModePerm); err != nil {
ctx.Data["Err_RepoRootPath"] = true
ctx.RenderWithErrDeprecated(ctx.Tr("install.invalid_repo_path", err), tplInstall, form)
return
}
// Test LFS root path if not empty, empty meaning disable LFS
if form.LFSRootPath != "" {
form.LFSRootPath = strings.ReplaceAll(form.LFSRootPath, "\\", "/")
if err := os.MkdirAll(form.LFSRootPath, os.ModePerm); err != nil {
ctx.Data["Err_LFSRootPath"] = true
ctx.RenderWithErrDeprecated(ctx.Tr("install.invalid_lfs_path", err), tplInstall, form)
return
}
}
// Test log root path.
form.LogRootPath = strings.ReplaceAll(form.LogRootPath, "\\", "/")
if err = os.MkdirAll(form.LogRootPath, os.ModePerm); err != nil {
ctx.Data["Err_LogRootPath"] = true
ctx.RenderWithErrDeprecated(ctx.Tr("install.invalid_log_root_path", err), tplInstall, form)
return
}
// Check logic loophole between disable self-registration and no admin account.
if form.DisableRegistration && len(form.AdminName) == 0 {
ctx.Data["Err_Services"] = true
ctx.Data["Err_Admin"] = true
ctx.RenderWithErrDeprecated(ctx.Tr("install.no_admin_and_disable_registration"), tplInstall, form)
return return
} }
// Check admin user creation // Check admin user creation
if len(form.AdminName) > 0 { if form.AdminName != "" {
// Ensure AdminName is valid // Ensure AdminName is valid
if err := user_model.IsUsableUsername(form.AdminName); err != nil { if err := user_model.IsUsableUsername(form.AdminName); err != nil {
ctx.Data["Err_Admin"] = true ctx.Data["Err_Admin"] = true
@@ -309,7 +279,7 @@ func SubmitInstall(ctx *context.Context) {
} }
// Init the engine with migration // Init the engine with migration
if err = db.InitEngineWithMigration(ctx, versioned_migration.Migrate); err != nil { if err := db.InitEngineWithMigration(ctx, versioned_migration.Migrate); err != nil {
db.UnsetDefaultEngine() db.UnsetDefaultEngine()
ctx.Data["Err_DbSetting"] = true ctx.Data["Err_DbSetting"] = true
ctx.RenderWithErrDeprecated(ctx.Tr("install.invalid_db_setting", err), tplInstall, form) ctx.RenderWithErrDeprecated(ctx.Tr("install.invalid_db_setting", err), tplInstall, form)
@@ -320,7 +290,15 @@ func SubmitInstall(ctx *context.Context) {
if cfg == nil { if cfg == nil {
return return
} }
saveConfigAndRestart(ctx, cfg, form) if !saveConfigReinitDB(ctx, cfg, form) {
return
}
if !initAdminUser(ctx, form) {
return
}
InstallDone(ctx) // render the "install done" page
restartServer(ctx)
} }
func fillInstallConfig(ctx *context.Context, envs []string, form *forms.InstallForm) setting.ConfigProvider { func fillInstallConfig(ctx *context.Context, envs []string, form *forms.InstallForm) setting.ConfigProvider {
@@ -332,11 +310,12 @@ func fillInstallConfig(ctx *context.Context, envs []string, form *forms.InstallF
} }
setting.EnvironmentToConfig(cfg, envs) setting.EnvironmentToConfig(cfg, envs)
cfg.Section("").Key("RUN_MODE").SetValue("prod")
cfg.Section("security").Key("INSTALL_LOCK").SetValue("true")
cfg.Section("").Key("APP_NAME").SetValue(form.AppName) cfg.Section("").Key("APP_NAME").SetValue(form.AppName)
cfg.Section("").Key("RUN_USER").SetValue(form.RunUser) cfg.Section("").Key("RUN_USER").SetValue(form.RunUser)
cfg.Section("").Key("WORK_PATH").SetValue(setting.AppWorkPath) cfg.Section("").Key("WORK_PATH").SetValue(setting.AppWorkPath)
cfg.Section("").Key("RUN_MODE").SetValue("prod")
cfg.Section("database").Key("DB_TYPE").SetValue(string(setting.Database.Type)) cfg.Section("database").Key("DB_TYPE").SetValue(string(setting.Database.Type))
cfg.Section("database").Key("HOST").SetValue(setting.Database.Host) cfg.Section("database").Key("HOST").SetValue(setting.Database.Host)
@@ -346,14 +325,15 @@ func fillInstallConfig(ctx *context.Context, envs []string, form *forms.InstallF
cfg.Section("database").Key("SCHEMA").SetValue(setting.Database.Schema) cfg.Section("database").Key("SCHEMA").SetValue(setting.Database.Schema)
cfg.Section("database").Key("SSL_MODE").SetValue(setting.Database.SSLMode) cfg.Section("database").Key("SSL_MODE").SetValue(setting.Database.SSLMode)
cfg.Section("database").Key("PATH").SetValue(setting.Database.Path) cfg.Section("database").Key("PATH").SetValue(setting.Database.Path)
cfg.Section("database").Key("LOG_SQL").SetValue("false") // LOG_SQL is rarely helpful
cfg.Section("repository").Key("ROOT").SetValue(form.RepoRootPath)
cfg.Section("server").Key("SSH_DOMAIN").SetValue(form.Domain)
cfg.Section("server").Key("DOMAIN").SetValue(form.Domain)
cfg.Section("server").Key("HTTP_PORT").SetValue(form.HTTPPort) cfg.Section("server").Key("HTTP_PORT").SetValue(form.HTTPPort)
cfg.Section("server").Key("ROOT_URL").SetValue(form.AppURL) cfg.Section("server").Key("ROOT_URL").SetValue(form.AppURL)
cfg.Section("server").Key("APP_DATA_PATH").SetValue(setting.AppDataPath) cfg.Section("server").Key("APP_DATA_PATH").SetValue(form.AppDataPath)
cfg.Section("server").Key("LFS_START_SERVER").SetValue("true")
if !cfg.Section("server").HasKey("LFS_JWT_SECRET_URI") {
_, lfsJwtSecret := generate.NewJwtSecretWithBase64()
cfg.Section("server").Key("LFS_JWT_SECRET").SetValue(lfsJwtSecret)
}
if form.SSHPort == 0 { if form.SSHPort == 0 {
cfg.Section("server").Key("DISABLE_SSH").SetValue("true") cfg.Section("server").Key("DISABLE_SSH").SetValue("true")
@@ -362,19 +342,7 @@ func fillInstallConfig(ctx *context.Context, envs []string, form *forms.InstallF
cfg.Section("server").Key("SSH_PORT").SetValue(strconv.Itoa(form.SSHPort)) cfg.Section("server").Key("SSH_PORT").SetValue(strconv.Itoa(form.SSHPort))
} }
if form.LFSRootPath != "" { if form.SMTPAddr != "" {
cfg.Section("server").Key("LFS_START_SERVER").SetValue("true")
cfg.Section("lfs").Key("PATH").SetValue(form.LFSRootPath)
if !cfg.Section("server").HasKey("LFS_JWT_SECRET_URI") {
_, lfsJwtSecret := generate.NewJwtSecretWithBase64()
cfg.Section("server").Key("LFS_JWT_SECRET").SetValue(lfsJwtSecret)
}
} else {
cfg.Section("server").Key("LFS_START_SERVER").SetValue("false")
}
if len(strings.TrimSpace(form.SMTPAddr)) > 0 {
if _, err := mail.ParseAddress(form.SMTPFrom); err != nil { if _, err := mail.ParseAddress(form.SMTPFrom); err != nil {
ctx.RenderWithErrDeprecated(ctx.Tr("install.smtp_from_invalid"), tplInstall, form) ctx.RenderWithErrDeprecated(ctx.Tr("install.smtp_from_invalid"), tplInstall, form)
return nil return nil
@@ -392,30 +360,17 @@ func fillInstallConfig(ctx *context.Context, envs []string, form *forms.InstallF
cfg.Section("service").Key("REGISTER_EMAIL_CONFIRM").SetValue(strconv.FormatBool(form.RegisterConfirm)) cfg.Section("service").Key("REGISTER_EMAIL_CONFIRM").SetValue(strconv.FormatBool(form.RegisterConfirm))
cfg.Section("service").Key("ENABLE_NOTIFY_MAIL").SetValue(strconv.FormatBool(form.MailNotify)) cfg.Section("service").Key("ENABLE_NOTIFY_MAIL").SetValue(strconv.FormatBool(form.MailNotify))
cfg.Section("openid").Key("ENABLE_OPENID_SIGNIN").SetValue(strconv.FormatBool(form.EnableOpenIDSignIn))
cfg.Section("openid").Key("ENABLE_OPENID_SIGNUP").SetValue(strconv.FormatBool(form.EnableOpenIDSignUp))
cfg.Section("service").Key("DISABLE_REGISTRATION").SetValue(strconv.FormatBool(form.DisableRegistration)) cfg.Section("service").Key("DISABLE_REGISTRATION").SetValue(strconv.FormatBool(form.DisableRegistration))
cfg.Section("service").Key("ALLOW_ONLY_EXTERNAL_REGISTRATION").SetValue(strconv.FormatBool(form.AllowOnlyExternalRegistration))
cfg.Section("service").Key("ENABLE_CAPTCHA").SetValue(strconv.FormatBool(form.EnableCaptcha)) cfg.Section("service").Key("ENABLE_CAPTCHA").SetValue(strconv.FormatBool(form.EnableCaptcha))
cfg.Section("service").Key("REQUIRE_SIGNIN_VIEW").SetValue(strconv.FormatBool(form.RequireSignInView)) cfg.Section("service").Key("REQUIRE_SIGNIN_VIEW").SetValue(strconv.FormatBool(form.RequireSignInView))
cfg.Section("service").Key("DEFAULT_KEEP_EMAIL_PRIVATE").SetValue(strconv.FormatBool(form.DefaultKeepEmailPrivate)) cfg.Section("service").Key("DEFAULT_KEEP_EMAIL_PRIVATE").SetValue(strconv.FormatBool(form.DefaultKeepEmailPrivate))
cfg.Section("service").Key("DEFAULT_ALLOW_CREATE_ORGANIZATION").SetValue(strconv.FormatBool(form.DefaultAllowCreateOrganization)) cfg.Section("service").Key("DEFAULT_ALLOW_CREATE_ORGANIZATION").SetValue(strconv.FormatBool(form.DefaultAllowCreateOrganization))
cfg.Section("service").Key("DEFAULT_ENABLE_TIMETRACKING").SetValue(strconv.FormatBool(form.DefaultEnableTimetracking))
cfg.Section("service").Key("NO_REPLY_ADDRESS").SetValue(form.NoReplyAddress) cfg.Section("service").Key("NO_REPLY_ADDRESS").SetValue(form.NoReplyAddress)
cfg.Section("cron.update_checker").Key("ENABLED").SetValue(strconv.FormatBool(form.EnableUpdateChecker)) cfg.Section("cron.update_checker").Key("ENABLED").SetValue(strconv.FormatBool(form.EnableUpdateChecker))
cfg.Section("session").Key("PROVIDER").SetValue("file")
cfg.Section("log").Key("MODE").MustString("console") // FIXME: INI-MUST-SIDE-EFFECT
cfg.Section("log").Key("LEVEL").SetValue(setting.Log.Level.String())
cfg.Section("log").Key("ROOT_PATH").SetValue(form.LogRootPath)
cfg.Section("repository.pull-request").Key("DEFAULT_MERGE_STYLE").SetValue("merge")
cfg.Section("repository.signing").Key("DEFAULT_TRUST_MODEL").SetValue("committer") cfg.Section("repository.signing").Key("DEFAULT_TRUST_MODEL").SetValue("committer")
cfg.Section("security").Key("INSTALL_LOCK").SetValue("true")
// the internal token could be read from INTERNAL_TOKEN or INTERNAL_TOKEN_URI (the file is guaranteed to be non-empty) // the internal token could be read from INTERNAL_TOKEN or INTERNAL_TOKEN_URI (the file is guaranteed to be non-empty)
// if there is no InternalToken, generate one and save to security.INTERNAL_TOKEN // if there is no InternalToken, generate one and save to security.INTERNAL_TOKEN
if setting.InternalToken == "" { if setting.InternalToken == "" {
@@ -444,39 +399,41 @@ func fillInstallConfig(ctx *context.Context, envs []string, form *forms.InstallF
cfg.Section("security").Key("SECRET_KEY").SetValue(secretKey) cfg.Section("security").Key("SECRET_KEY").SetValue(secretKey)
} }
if len(form.PasswordAlgorithm) > 0 {
var algorithm *hash.PasswordHashAlgorithm
setting.PasswordHashAlgo, algorithm = hash.SetDefaultPasswordHashAlgorithm(form.PasswordAlgorithm)
if algorithm == nil {
ctx.RenderWithErrDeprecated(ctx.Tr("install.invalid_password_algorithm"), tplInstall, form)
return nil
}
cfg.Section("security").Key("PASSWORD_HASH_ALGO").SetValue(form.PasswordAlgorithm)
}
setting.EnvironmentToConfig(cfg, envs) setting.EnvironmentToConfig(cfg, envs)
fillInstallConfigCleanUp(cfg)
return cfg return cfg
} }
func saveConfigAndRestart(ctx *context.Context, cfg setting.ConfigProvider, form *forms.InstallForm) { func fillInstallConfigCleanUp(cfg setting.ConfigProvider) {
// this is just a quick patch to avoid generating a corrupted ini file,
// if there would be no bug for ini handling (e.g.: we write our package), this patch is not needed.
re := regexp.MustCompile(`[\x00-\x1F\x7F]`)
for _, sec := range cfg.Sections() {
for _, key := range sec.Keys() {
s := key.String()
s = re.ReplaceAllString(s, " ")
key.SetValue(s)
}
}
}
func saveConfigReinitDB(ctx *context.Context, cfg setting.ConfigProvider, form *forms.InstallForm) bool {
log.Info("Save settings to custom config file %s", setting.CustomConf) log.Info("Save settings to custom config file %s", setting.CustomConf)
err := os.MkdirAll(filepath.Dir(setting.CustomConf), os.ModePerm) err := os.MkdirAll(filepath.Dir(setting.CustomConf), os.ModePerm)
if err != nil { if err != nil {
ctx.RenderWithErrDeprecated(ctx.Tr("install.save_config_failed", err), tplInstall, form) ctx.RenderWithErrDeprecated(ctx.Tr("install.save_config_failed", err), tplInstall, form)
return return false
} }
if err := cfg.SaveTo(setting.CustomConf); err != nil { if err := cfg.SaveTo(setting.CustomConf); err != nil {
ctx.RenderWithErrDeprecated(ctx.Tr("install.save_config_failed", err), tplInstall, form) ctx.RenderWithErrDeprecated(ctx.Tr("install.save_config_failed", err), tplInstall, form)
return return false
} }
// unset default engine before reload database setting // unset default engine before reload database setting
db.UnsetDefaultEngine() db.UnsetDefaultEngine()
// ---- All checks are passed
// Reload settings (and re-initialize database connection) // Reload settings (and re-initialize database connection)
setting.InitCfgProvider(setting.CustomConf) setting.InitCfgProvider(setting.CustomConf)
setting.LoadCommonSettings() setting.LoadCommonSettings()
@@ -486,64 +443,64 @@ func saveConfigAndRestart(ctx *context.Context, cfg setting.ConfigProvider, form
log.Fatal("ORM engine initialization failed: %v", err) log.Fatal("ORM engine initialization failed: %v", err)
} }
// Create admin account setting.ClearEnvConfigKeys()
if len(form.AdminName) > 0 { log.Info("Installation completed! You can also use 'gitea admin user ...' sub-commands to create or edit admin users.")
u := &user_model.User{ log.Info("----------------------------------------")
Name: form.AdminName, return true
Email: form.AdminEmail, }
Passwd: form.AdminPasswd,
IsAdmin: true,
}
overwriteDefault := &user_model.CreateUserOverwriteOptions{
IsRestricted: optional.Some(false),
IsActive: optional.Some(true),
}
if err := user_model.CreateUser(ctx, u, &user_model.Meta{}, overwriteDefault); err != nil { func initAdminUser(ctx *context.Context, form *forms.InstallForm) bool {
if !user_model.IsErrUserAlreadyExist(err) { if form.AdminName == "" {
setting.InstallLock = false return true
ctx.Data["Err_AdminName"] = true
ctx.Data["Err_AdminEmail"] = true
ctx.RenderWithErrDeprecated(ctx.Tr("install.invalid_admin_setting", err), tplInstall, form)
return
}
log.Info("Admin account already exist")
u, _ = user_model.GetUserByName(ctx, u.Name)
}
audit.RecordAs(ctx, u, audit_model.UserCreate, u)
nt, token, err := auth_service.CreateAuthTokenForUserID(ctx, u.ID)
if err != nil {
ctx.ServerError("CreateAuthTokenForUserID", err)
return
}
ctx.SetSiteCookie(setting.CookieRememberName, nt.ID+":"+token, setting.LogInRememberDays*timeutil.Day)
// Auto-login for admin
if err = ctx.Session.Set(session.KeyUID, u.ID); err != nil {
ctx.RenderWithErrDeprecated(ctx.Tr("install.save_config_failed", err), tplInstall, form)
return
}
if err = ctx.Session.Release(); err != nil {
ctx.RenderWithErrDeprecated(ctx.Tr("install.save_config_failed", err), tplInstall, form)
return
}
} }
setting.ClearEnvConfigKeys() adminUser := &user_model.User{
log.Info("First-time run install finished!") Name: form.AdminName,
InstallDone(ctx) Email: form.AdminEmail,
Passwd: form.AdminPasswd,
IsAdmin: true,
}
overwriteDefault := &user_model.CreateUserOverwriteOptions{
IsRestricted: optional.Some(false),
IsActive: optional.Some(true),
}
if err := user_model.CreateUser(ctx, adminUser, &user_model.Meta{}, overwriteDefault); err != nil {
ctx.Data["Err_AdminName"] = true
ctx.Data["Err_AdminEmail"] = true
if user_model.IsErrUserAlreadyExist(err) {
ctx.RenderWithErrDeprecated(ctx.Tr("install.admin_user_recreation_disallowed"), tplInstall, form)
} else {
ctx.RenderWithErrDeprecated(ctx.Tr("install.invalid_admin_setting", err), tplInstall, form)
}
return false
}
audit.RecordAs(ctx, adminUser, audit_model.UserCreate, adminUser)
nt, token, err := auth_service.CreateAuthTokenForUserID(ctx, adminUser.ID)
if err != nil {
ctx.ServerError("CreateAuthTokenForUserID", err)
return false
}
// Auto-login for admin, even if any "session" error happens, it should still continue
ctx.SetSiteCookie(setting.CookieRememberName, nt.ID+":"+token, setting.LogInRememberDays*timeutil.Day)
_ = ctx.Session.Set(session.KeyUID, adminUser.ID)
_ = ctx.Session.Release()
return true
}
func restartServer(ctx *context.Context) {
// Now get the http.Server from this request and shut it down
// NB: This is not our hammerable graceful shutdown this is http.Server.Shutdown
srv, _ := ctx.Value(http.ServerContextKey).(*http.Server)
go func() { go func() {
// Sleep for a while to make sure the user's browser has loaded the post-install page and its assets (images, css, js) // Sleep for a while to make sure the user's browser has loaded the post-install page and its assets (images, css, js)
// What if this duration is not long enough? That's impossible -- if the user can't load the simple page in time, how could they install or use Gitea in the future .... // What if this duration is not long enough? That's impossible -- if the user can't load the simple page in time, how could they install or use Gitea in the future ....
time.Sleep(3 * time.Second) time.Sleep(3 * time.Second)
// Now get the http.Server from this request and shut it down
// NB: This is not our hammerable graceful shutdown this is http.Server.Shutdown
srv := ctx.Value(http.ServerContextKey).(*http.Server) //nolint:forcetypeassert // must exist
if err := srv.Shutdown(graceful.GetManager().HammerContext()); err != nil { if err := srv.Shutdown(graceful.GetManager().HammerContext()); err != nil {
log.Error("Unable to shutdown the install server! Error: %v", err) log.Error("Unable to shutdown the install server! Error: %v", err)
} }
+11
View File
@@ -9,6 +9,7 @@ import (
"testing" "testing"
"gitea.dev/models/unittest" "gitea.dev/models/unittest"
"gitea.dev/modules/setting"
"gitea.dev/services/contexttest" "gitea.dev/services/contexttest"
"gitea.dev/services/forms" "gitea.dev/services/forms"
@@ -59,4 +60,14 @@ func TestFillInstallConfig(t *testing.T) {
assert.NotEmpty(t, cfg.Section("oauth2").Key("JWT_SECRET").String()) assert.NotEmpty(t, cfg.Section("oauth2").Key("JWT_SECRET").String())
assert.Empty(t, cfg.Section("oauth2").Key("JWT_SECRET_URI").String()) assert.Empty(t, cfg.Section("oauth2").Key("JWT_SECRET_URI").String())
}) })
t.Run("CleanUp", func(t *testing.T) {
tmpFile := t.TempDir() + "/test.ini"
f := &forms.InstallForm{AppName: "foo\x00\r\nbar"}
cfg := fillInstallConfig(ctx, []string{}, f)
err := cfg.SaveTo(tmpFile)
assert.NoError(t, err)
ini, err := setting.NewConfigProviderFromFile(tmpFile)
assert.NoError(t, err)
assert.Equal(t, `foo bar`, ini.Section("").Key("APP_NAME").String())
})
} }
+1 -1
View File
@@ -114,7 +114,7 @@ func twofaGenerateSecretAndQr(ctx *context.Context) bool {
} }
} }
// Filter unsafe character ':' in issuer // Filter unsafe character ':' in issuer
issuer := strings.ReplaceAll(setting.AppName+" ("+setting.Domain+")", ":", "") issuer := strings.ReplaceAll(setting.AppName+" ("+setting.AppDomain+")", ":", "")
if otpKey == nil { if otpKey == nil {
otpKey, err = totp.Generate(totp.GenerateOpts{ otpKey, err = totp.Generate(totp.GenerateOpts{
SecretSize: 40, SecretSize: 40,
+9 -18
View File
@@ -28,15 +28,12 @@ type InstallForm struct {
DbPath string `binding:"TrimSpace"` DbPath string `binding:"TrimSpace"`
DbSchema string `binding:"TrimSpace"` DbSchema string `binding:"TrimSpace"`
AppName string `binding:"TrimSpace;Required" locale:"install.app_name"` AppName string `binding:"TrimSpace;Required" locale:"install.app_name"`
RepoRootPath string `binding:"TrimSpace;Required"` AppDataPath string `binding:"TrimSpace;Required"`
LFSRootPath string `binding:"TrimSpace"` RunUser string `binding:"TrimSpace;Required"`
RunUser string `binding:"TrimSpace;Required"` SSHPort int
Domain string `binding:"TrimSpace;Required"` HTTPPort string `binding:"TrimSpace;Required"`
SSHPort int AppURL string `binding:"TrimSpace;Required"`
HTTPPort string `binding:"TrimSpace;Required"`
AppURL string `binding:"TrimSpace;Required"`
LogRootPath string `binding:"TrimSpace;Required"`
SMTPAddr string `binding:"TrimSpace"` SMTPAddr string `binding:"TrimSpace"`
SMTPPort string `binding:"TrimSpace"` SMTPPort string `binding:"TrimSpace"`
@@ -46,24 +43,18 @@ type InstallForm struct {
RegisterConfirm bool RegisterConfirm bool
MailNotify bool MailNotify bool
EnableOpenIDSignIn bool
EnableOpenIDSignUp bool
DisableRegistration bool DisableRegistration bool
AllowOnlyExternalRegistration bool
EnableCaptcha bool EnableCaptcha bool
RequireSignInView bool RequireSignInView bool
DefaultKeepEmailPrivate bool DefaultKeepEmailPrivate bool
DefaultAllowCreateOrganization bool DefaultAllowCreateOrganization bool
DefaultEnableTimetracking bool
EnableUpdateChecker bool EnableUpdateChecker bool
NoReplyAddress string `binding:"TrimSpace"` NoReplyAddress string `binding:"TrimSpace"`
PasswordAlgorithm string `binding:"TrimSpace"` AdminName string `binding:"TrimSpace;Username;MaxSize(30)" locale:"install.admin_name"`
AdminPasswd string `binding:"MaxSize(255)" locale:"install.admin_password"`
AdminName string `binding:"TrimSpace;OmitEmpty;Username;MaxSize(30)" locale:"install.admin_name"`
AdminPasswd string `binding:"OmitEmpty;MaxSize(255)" locale:"install.admin_password"`
AdminConfirmPasswd string AdminConfirmPasswd string
AdminEmail string `binding:"TrimSpace;OmitEmpty;MinSize(3);MaxSize(254);Include(@)" locale:"install.admin_email"` AdminEmail string `binding:"TrimSpace;MinSize(3);MaxSize(254);Include(@)" locale:"install.admin_email"`
// ReinstallConfirmFirst we can not use 1/2/3 or A/B/C here, there is a framework bug, can not parse "reinstall_confirm_1" or "reinstall_confirm_a" // ReinstallConfirmFirst we can not use 1/2/3 or A/B/C here, there is a framework bug, can not parse "reinstall_confirm_1" or "reinstall_confirm_a"
ReinstallConfirmFirst bool ReinstallConfirmFirst bool
+1 -1
View File
@@ -308,7 +308,7 @@ func searchTokenInHeaders(env *enmime.Envelope) string {
if end == -1 || begin > end { if end == -1 || begin > end {
break break
} }
t := extractToken(references[begin:end], "reply-", "@"+setting.Domain) t := extractToken(references[begin:end], "reply-", "@"+setting.AppDomain)
if t != "" { if t != "" {
return t return t
} }
+1 -1
View File
@@ -71,8 +71,8 @@ func TestIsAutomaticReply(t *testing.T) {
} }
func TestSearchTokenInHeadersCaseInsensitive(t *testing.T) { func TestSearchTokenInHeadersCaseInsensitive(t *testing.T) {
setting.AppDomain = "DoMain.com"
setting.IncomingEmail.ReplyToAddress = "InComing+%{token}@ExAmPle.com" setting.IncomingEmail.ReplyToAddress = "InComing+%{token}@ExAmPle.com"
setting.Domain = "DoMain.com"
mkEnv := func(s string) *enmime.Envelope { mkEnv := func(s string) *enmime.Envelope {
env, _ := enmime.ReadEnvelope(strings.NewReader(s + "\r\n\r\n")) env, _ := enmime.ReadEnvelope(strings.NewReader(s + "\r\n\r\n"))
return env return env
+2 -2
View File
@@ -162,7 +162,7 @@ func fromDisplayName(u *user_model.User) string {
err := setting.MailService.FromDisplayNameFormatTemplate.Execute(&buf, map[string]any{ err := setting.MailService.FromDisplayNameFormatTemplate.Execute(&buf, map[string]any{
"DisplayName": u.DisplayName(), "DisplayName": u.DisplayName(),
"AppName": setting.AppName, "AppName": setting.AppName,
"Domain": setting.Domain, "Domain": setting.AppDomain,
}) })
if err == nil { if err == nil {
return mime.QEncoding.Encode("utf-8", buf.String()) return mime.QEncoding.Encode("utf-8", buf.String())
@@ -180,7 +180,7 @@ func fromDisplayName(u *user_model.User) string {
func generateMetadataHeaders(repo *repo_model.Repository) map[string]string { func generateMetadataHeaders(repo *repo_model.Repository) map[string]string {
return map[string]string{ return map[string]string{
// https://datatracker.ietf.org/doc/html/rfc2919 // https://datatracker.ietf.org/doc/html/rfc2919
"List-ID": fmt.Sprintf("%s <%s.%s.%s>", repo.FullName(), repo.Name, repo.OwnerName, setting.Domain), "List-ID": fmt.Sprintf("%s <%s.%s.%s>", repo.FullName(), repo.Name, repo.OwnerName, setting.AppDomain),
// https://datatracker.ietf.org/doc/html/rfc2369 // https://datatracker.ietf.org/doc/html/rfc2369
"List-Archive": fmt.Sprintf("<%s>", repo.HTMLURL()), "List-Archive": fmt.Sprintf("<%s>", repo.HTMLURL()),
+2 -2
View File
@@ -188,7 +188,7 @@ func composeIssueCommentMessages(ctx context.Context, comment *mailComment, lang
msg.ReplyTo = replyAddress msg.ReplyTo = replyAddress
msg.SetHeader("List-Post", fmt.Sprintf("<mailto:%s>", replyAddress)) msg.SetHeader("List-Post", fmt.Sprintf("<mailto:%s>", replyAddress))
references = append(references, fmt.Sprintf("<reply-%s@%s>", token, setting.Domain)) references = append(references, fmt.Sprintf("<reply-%s@%s>", token, setting.AppDomain))
} }
} }
@@ -301,7 +301,7 @@ func generateMessageIDForIssue(issue *issues_model.Issue, comment *issues_model.
} }
} }
return fmt.Sprintf("<%s/%s/%d%s@%s>", issue.Repo.FullName(), path, issue.Index, extra, setting.Domain) return fmt.Sprintf("<%s/%s/%d%s@%s>", issue.Repo.FullName(), path, issue.Index, extra, setting.AppDomain)
} }
func generateAdditionalHeadersForIssue(ctx context.Context, comment *mailComment, reason string, recipient *user_model.User) map[string]string { func generateAdditionalHeadersForIssue(ctx context.Context, comment *mailComment, reason string, recipient *user_model.User) map[string]string {
+1 -1
View File
@@ -25,7 +25,7 @@ import (
const tplNewReleaseMail templates.TplName = "mail/repo/release" const tplNewReleaseMail templates.TplName = "mail/repo/release"
func generateMessageIDForRelease(release *repo_model.Release) string { func generateMessageIDForRelease(release *repo_model.Release) string {
return fmt.Sprintf("<%s/releases/%d@%s>", release.Repo.FullName(), release.ID, setting.Domain) return fmt.Sprintf("<%s/releases/%d@%s>", release.Repo.FullName(), release.ID, setting.AppDomain)
} }
// MailNewRelease send new release notify to all repo watchers. // MailNewRelease send new release notify to all repo watchers.
+1 -2
View File
@@ -21,7 +21,7 @@ func TestMailNewReleaseFiltersUnauthorizedWatchers(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase()) assert.NoError(t, unittest.PrepareTestDatabase())
defer test.MockVariableValue(&setting.MailService)() defer test.MockVariableValue(&setting.MailService)()
defer test.MockVariableValue(&setting.Domain)() defer test.MockVariableValue(&setting.AppDomain)()
defer test.MockVariableValue(&setting.AppName)() defer test.MockVariableValue(&setting.AppName)()
defer test.MockVariableValue(&setting.AppURL)() defer test.MockVariableValue(&setting.AppURL)()
@@ -29,7 +29,6 @@ func TestMailNewReleaseFiltersUnauthorizedWatchers(t *testing.T) {
From: "Gitea", From: "Gitea",
FromEmail: "noreply@example.com", FromEmail: "noreply@example.com",
} }
setting.Domain = "example.com"
setting.AppName = "Gitea" setting.AppName = "Gitea"
setting.AppURL = "https://example.com/" setting.AppURL = "https://example.com/"
defer mockMailTemplates(string(tplNewReleaseMail), "{{.Subject}}", "<p>{{.Release.TagName}}</p>")() defer mockMailTemplates(string(tplNewReleaseMail), "{{.Subject}}", "<p>{{.Release.TagName}}</p>")()
+6 -14
View File
@@ -61,7 +61,6 @@ const bodyTpl = `
func prepareMailerTest(t *testing.T) (doer *user_model.User, repo *repo_model.Repository, issue *issues_model.Issue, comment *issues_model.Comment) { func prepareMailerTest(t *testing.T) (doer *user_model.User, repo *repo_model.Repository, issue *issues_model.Issue, comment *issues_model.Comment) {
assert.NoError(t, unittest.PrepareTestDatabase()) assert.NoError(t, unittest.PrepareTestDatabase())
setting.MailService = &setting.Mailer{From: "test@gitea.com"} setting.MailService = &setting.Mailer{From: "test@gitea.com"}
setting.Domain = "localhost"
setting.AppURL = "https://try.gitea.io/" setting.AppURL = "https://try.gitea.io/"
doer = unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) doer = unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
@@ -390,7 +389,7 @@ func TestGenerateMessageIDForIssue(t *testing.T) {
issue: issue, issue: issue,
actionType: activities_model.ActionCreateIssue, actionType: activities_model.ActionCreateIssue,
}, },
prefix: fmt.Sprintf("<%s/issues/%d@%s>", issue.Repo.FullName(), issue.Index, setting.Domain), prefix: fmt.Sprintf("<%s/issues/%d@%s>", issue.Repo.FullName(), issue.Index, setting.AppDomain),
}, },
{ {
name: "Open Pull", name: "Open Pull",
@@ -398,7 +397,7 @@ func TestGenerateMessageIDForIssue(t *testing.T) {
issue: pullIssue, issue: pullIssue,
actionType: activities_model.ActionCreatePullRequest, actionType: activities_model.ActionCreatePullRequest,
}, },
prefix: fmt.Sprintf("<%s/pulls/%d@%s>", issue.Repo.FullName(), issue.Index, setting.Domain), prefix: fmt.Sprintf("<%s/pulls/%d@%s>", issue.Repo.FullName(), issue.Index, setting.AppDomain),
}, },
{ {
name: "Comment Issue", name: "Comment Issue",
@@ -407,7 +406,7 @@ func TestGenerateMessageIDForIssue(t *testing.T) {
comment: comment, comment: comment,
actionType: activities_model.ActionCommentIssue, actionType: activities_model.ActionCommentIssue,
}, },
prefix: fmt.Sprintf("<%s/issues/%d/comment/%d@%s>", issue.Repo.FullName(), issue.Index, comment.ID, setting.Domain), prefix: fmt.Sprintf("<%s/issues/%d/comment/%d@%s>", issue.Repo.FullName(), issue.Index, comment.ID, setting.AppDomain),
}, },
{ {
name: "Comment Pull", name: "Comment Pull",
@@ -416,7 +415,7 @@ func TestGenerateMessageIDForIssue(t *testing.T) {
comment: comment, comment: comment,
actionType: activities_model.ActionCommentPull, actionType: activities_model.ActionCommentPull,
}, },
prefix: fmt.Sprintf("<%s/pulls/%d/comment/%d@%s>", issue.Repo.FullName(), issue.Index, comment.ID, setting.Domain), prefix: fmt.Sprintf("<%s/pulls/%d/comment/%d@%s>", issue.Repo.FullName(), issue.Index, comment.ID, setting.AppDomain),
}, },
{ {
name: "Close Issue", name: "Close Issue",
@@ -518,15 +517,8 @@ func TestFromDisplayName(t *testing.T) {
tmpl, err = texttmpl.New("mailFrom").Parse("{{ .DisplayName }} (by {{ .AppName }} on [{{ .Domain }}])") tmpl, err = texttmpl.New("mailFrom").Parse("{{ .DisplayName }} (by {{ .AppName }} on [{{ .Domain }}])")
assert.NoError(t, err) assert.NoError(t, err)
setting.MailService = &setting.Mailer{FromDisplayNameFormatTemplate: tmpl} setting.MailService = &setting.Mailer{FromDisplayNameFormatTemplate: tmpl}
oldAppName := setting.AppName defer test.MockVariableValue(&setting.AppName, "Code IT")()
setting.AppName = "Code IT" defer test.MockVariableValue(&setting.AppDomain, "code.it")()
oldDomain := setting.Domain
setting.Domain = "code.it"
defer func() {
setting.AppName = oldAppName
setting.Domain = oldDomain
}()
assert.Equal(t, "Mister X (by Code IT on [code.it])", fromDisplayName(&user_model.User{FullName: "Mister X", Name: "tmp"})) assert.Equal(t, "Mister X (by Code IT on [code.it])", fromDisplayName(&user_model.User{FullName: "Mister X", Name: "tmp"}))
}) })
} }
+2 -2
View File
@@ -85,7 +85,7 @@ func composeAndSendActionsWorkflowRunStatusEmail(ctx context.Context, repo *repo
embedded := make(container.Set[string]) embedded := make(container.Set[string])
for _, job := range jobs { for _, job := range jobs {
icon, class := workflowRunJobStatusPresentation(job.Status) icon, class := workflowRunJobStatusPresentation(job.Status)
contentID := fmt.Sprintf("%s.actions-run-%d@%s", icon, run.ID, setting.Domain) contentID := fmt.Sprintf("%s.actions-run-%d@%s", icon, run.ID, setting.AppDomain)
mailJobs = append(mailJobs, workflowRunMailJob{ mailJobs = append(mailJobs, workflowRunMailJob{
HTMLURL: fmt.Sprintf("%s/actions/runs/%d/jobs/%d", repo.HTMLURL(ctx), run.ID, job.ID), HTMLURL: fmt.Sprintf("%s/actions/runs/%d/jobs/%d", repo.HTMLURL(ctx), run.ID, job.ID),
Name: job.Name, Name: job.Name,
@@ -143,7 +143,7 @@ func composeAndSendActionsWorkflowRunStatusEmail(ctx context.Context, repo *repo
for key, value := range generateMetadataHeaders(repo) { for key, value := range generateMetadataHeaders(repo) {
msg.SetHeader(key, value) msg.SetHeader(key, value)
} }
msg.SetHeader("Message-ID", fmt.Sprintf("<%s/actions/runs/%d@%s>", repo.FullName(), run.Index, setting.Domain)) msg.SetHeader("Message-ID", fmt.Sprintf("<%s/actions/runs/%d@%s>", repo.FullName(), run.Index, setting.AppDomain))
SendAsync(msg) SendAsync(msg)
return nil return nil
+1 -1
View File
@@ -56,7 +56,7 @@ func TestWorkflowRunMail(t *testing.T) {
translation.InitLocales(t.Context()) translation.InitLocales(t.Context())
require.NoError(t, unittest.PrepareTestDatabase()) require.NoError(t, unittest.PrepareTestDatabase())
defer test.MockVariableValue(&setting.MailService, &setting.Mailer{FromEmail: "gitea@localhost"})() defer test.MockVariableValue(&setting.MailService, &setting.Mailer{FromEmail: "gitea@localhost"})()
defer test.MockVariableValue(&setting.Domain, "localhost")() defer test.MockVariableValue(&setting.AppDomain, "localhost")()
defer test.MockVariableValue(&setting.AppURL, "http://localhost:3000/")() defer test.MockVariableValue(&setting.AppURL, "http://localhost:3000/")()
recipient := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) recipient := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
+1 -1
View File
@@ -101,7 +101,7 @@ func (m *Message) generateAutoMessageID() string {
} }
_, _ = h.Write([]byte(m.Subject)) _, _ = h.Write([]byte(m.Subject))
_, _ = h.Write([]byte(m.Body)) _, _ = h.Write([]byte(m.Body))
return fmt.Sprintf("<autogen-%d-%016x@%s>", dateMs, h.Sum64(), setting.Domain) return fmt.Sprintf("<autogen-%d-%016x@%s>", dateMs, h.Sum64(), setting.AppDomain)
} }
// NewMessageFrom creates new mail message object with custom From header. // NewMessageFrom creates new mail message object with custom From header.
-1
View File
@@ -19,7 +19,6 @@ func TestGenerateMessageID(t *testing.T) {
} }
setting.MailService = &mailService setting.MailService = &mailService
setting.Domain = "localhost"
date := time.Date(2000, 1, 2, 3, 4, 5, 6, time.UTC) date := time.Date(2000, 1, 2, 3, 4, 5, 6, time.UTC)
m := NewMessageFrom("", "display-name", "from-address", "subject", "body") m := NewMessageFrom("", "display-name", "from-address", "subject", "body")
+30 -81
View File
@@ -107,26 +107,16 @@
<input id="app_name" name="app_name" value="{{.app_name}}" required> <input id="app_name" name="app_name" value="{{.app_name}}" required>
<span class="help">{{ctx.Locale.Tr "install.app_name_helper"}}</span> <span class="help">{{ctx.Locale.Tr "install.app_name_helper"}}</span>
</div> </div>
<div class="inline required field {{if .Err_RepoRootPath}}error{{end}}"> <div class="inline required field {{if .Err_AppDataPath}}error{{end}}">
<label for="repo_root_path">{{ctx.Locale.Tr "install.repo_path"}}</label> <label>{{ctx.Locale.Tr "install.app_data_path"}}</label>
<input id="repo_root_path" name="repo_root_path" value="{{.repo_root_path}}" required> <input name="app_data_path" value="{{.app_data_path}}" required>
<span class="help">{{ctx.Locale.Tr "install.repo_path_helper"}}</span> <span class="help">{{ctx.Locale.Tr "install.app_data_path_helper"}}</span>
</div>
<div class="inline field {{if .Err_LFSRootPath}}error{{end}}">
<label for="lfs_root_path">{{ctx.Locale.Tr "install.lfs_path"}}</label>
<input id="lfs_root_path" name="lfs_root_path" value="{{.lfs_root_path}}">
<span class="help">{{ctx.Locale.Tr "install.lfs_path_helper"}}</span>
</div> </div>
<div class="inline field"> <div class="inline field">
<label for="run_user">{{ctx.Locale.Tr "install.run_user"}}</label> <label for="run_user">{{ctx.Locale.Tr "install.run_user"}}</label>
<input id="run_user" name="run_user" value="{{.run_user}}" readonly> <input id="run_user" name="run_user" value="{{.run_user}}" readonly>
<span class="help">{{ctx.Locale.Tr "install.run_user_helper"}}</span> <span class="help">{{ctx.Locale.Tr "install.run_user_helper"}}</span>
</div> </div>
<div class="inline required field">
<label for="domain">{{ctx.Locale.Tr "install.domain"}}</label>
<input id="domain" name="domain" value="{{.domain}}" placeholder="demo.gitea.com" required>
<span class="help">{{ctx.Locale.Tr "install.domain_helper"}}</span>
</div>
<div class="inline field"> <div class="inline field">
<label for="ssh_port">{{ctx.Locale.Tr "install.ssh_port"}}</label> <label for="ssh_port">{{ctx.Locale.Tr "install.ssh_port"}}</label>
<input id="ssh_port" name="ssh_port" value="{{.ssh_port}}"> <input id="ssh_port" name="ssh_port" value="{{.ssh_port}}">
@@ -142,25 +132,37 @@
<input id="app_url" name="app_url" value="{{.app_url}}" placeholder="https://demo.gitea.com" required> <input id="app_url" name="app_url" value="{{.app_url}}" placeholder="https://demo.gitea.com" required>
<span class="help">{{ctx.Locale.Tr "install.app_url_helper"}}</span> <span class="help">{{ctx.Locale.Tr "install.app_url_helper"}}</span>
</div> </div>
<div class="inline required field"> <div class="inline field">
<label for="log_root_path">{{ctx.Locale.Tr "install.log_root_path"}}</label> <div class="ui checkbox" id="disable-registration">
<input id="log_root_path" name="log_root_path" value="{{.log_root_path}}" placeholder="log" required> <label>{{ctx.Locale.Tr "install.disable_registration"}}</label>
<span class="help">{{ctx.Locale.Tr "install.log_root_path_helper"}}</span> <input name="disable_registration" type="checkbox" {{if .disable_registration}}checked{{end}}>
</div>
</div>
<div class="inline field">
<div class="ui checkbox" id="enable-captcha">
<label data-tooltip-content="{{ctx.Locale.Tr "install.enable_captcha_popup"}}">{{ctx.Locale.Tr "install.enable_captcha"}}</label>
<input name="enable_captcha" type="checkbox" {{if .enable_captcha}}checked{{end}}>
</div>
</div>
<div class="inline field">
<div class="ui checkbox">
<label data-tooltip-content="{{ctx.Locale.Tr "install.require_sign_in_view_popup"}}">{{ctx.Locale.Tr "install.require_sign_in_view"}}</label>
<input name="require_sign_in_view" type="checkbox" {{if .require_sign_in_view}}checked{{end}}>
</div>
</div> </div>
<div class="inline field"> <div class="inline field">
<div class="ui checkbox"> <div class="ui checkbox">
<label>{{ctx.Locale.Tr "install.enable_update_checker"}}</label> <label>{{ctx.Locale.Tr "install.enable_update_checker"}}</label>
<input name="enable_update_checker" type="checkbox" {{if .enable_update_checker}}checked{{end}}> <input name="enable_update_checker" type="checkbox" {{if .enable_update_checker}}checked{{end}}>
</div> </div>
<span class="help">{{ctx.Locale.Tr "install.enable_update_checker_helper"}}</span>
</div> </div>
<!-- Optional Settings --> <!-- Optional Settings -->
<h4 class="ui dividing header">{{ctx.Locale.Tr "install.optional_title"}}</h4> <h4 class="ui dividing header">{{ctx.Locale.Tr "install.optional_title"}}</h4>
<div> <div>
<!-- Email --> <!-- Email -->
<details class="optional field"> <details class="optional field" {{if .Err_SMTP}}open{{end}}>
<summary class="right-content tw-py-2{{if .Err_SMTP}} tw-text-red{{end}}"> <summary class="right-content tw-py-2 {{if .Err_SMTP}}tw-text-red{{end}}">
{{ctx.Locale.Tr "install.email_title"}} {{ctx.Locale.Tr "install.email_title"}}
</summary> </summary>
<div class="inline field"> <div class="inline field">
@@ -199,46 +201,10 @@
</details> </details>
<!-- Server and other services --> <!-- Server and other services -->
<details class="optional field"> <details class="optional field" {{if .Err_Services}}open{{end}}>
<summary class="right-content tw-py-2{{if .Err_Services}} tw-text-red{{end}}"> <summary class="right-content tw-py-2 {{if .Err_Services}}tw-text-red{{end}}">
{{ctx.Locale.Tr "install.server_service_title"}} {{ctx.Locale.Tr "install.server_service_title"}}
</summary> </summary>
<div class="inline field">
<div class="ui checkbox" id="enable-openid-signin">
<label data-tooltip-content="{{ctx.Locale.Tr "install.openid_signin_popup"}}">{{ctx.Locale.Tr "install.openid_signin"}}</label>
<input name="enable_open_id_sign_in" type="checkbox" {{if .enable_open_id_sign_in}}checked{{end}}>
</div>
</div>
<div class="inline field">
<div class="ui checkbox" id="disable-registration">
<label data-tooltip-content="{{ctx.Locale.Tr "install.disable_registration_popup"}}">{{ctx.Locale.Tr "install.disable_registration"}}</label>
<input name="disable_registration" type="checkbox" {{if .disable_registration}}checked{{end}}>
</div>
</div>
<div class="inline field">
<div class="ui checkbox" id="allow-only-external-registration">
<label data-tooltip-content="{{ctx.Locale.Tr "install.allow_only_external_registration_popup"}}">{{ctx.Locale.Tr "install.allow_only_external_registration_popup"}}</label>
<input name="allow_only_external_registration" type="checkbox" {{if .allow_only_external_registration}}checked{{end}}>
</div>
</div>
<div class="inline field">
<div class="ui checkbox" id="enable-openid-signup">
<label data-tooltip-content="{{ctx.Locale.Tr "install.openid_signup_popup"}}">{{ctx.Locale.Tr "install.openid_signup"}}</label>
<input name="enable_open_id_sign_up" type="checkbox" {{if .enable_open_id_sign_up}}checked{{end}}>
</div>
</div>
<div class="inline field">
<div class="ui checkbox" id="enable-captcha">
<label data-tooltip-content="{{ctx.Locale.Tr "install.enable_captcha_popup"}}">{{ctx.Locale.Tr "install.enable_captcha"}}</label>
<input name="enable_captcha" type="checkbox" {{if .enable_captcha}}checked{{end}}>
</div>
</div>
<div class="inline field">
<div class="ui checkbox">
<label data-tooltip-content="{{ctx.Locale.Tr "install.require_sign_in_view_popup"}}">{{ctx.Locale.Tr "install.require_sign_in_view"}}</label>
<input name="require_sign_in_view" type="checkbox" {{if .require_sign_in_view}}checked{{end}}>
</div>
</div>
<div class="inline field"> <div class="inline field">
<div class="ui checkbox"> <div class="ui checkbox">
<label data-tooltip-content="{{ctx.Locale.Tr "install.default_keep_email_private_popup"}}">{{ctx.Locale.Tr "install.default_keep_email_private"}}</label> <label data-tooltip-content="{{ctx.Locale.Tr "install.default_keep_email_private_popup"}}">{{ctx.Locale.Tr "install.default_keep_email_private"}}</label>
@@ -251,36 +217,16 @@
<input name="default_allow_create_organization" type="checkbox" {{if .default_allow_create_organization}}checked{{end}}> <input name="default_allow_create_organization" type="checkbox" {{if .default_allow_create_organization}}checked{{end}}>
</div> </div>
</div> </div>
<div class="inline field">
<div class="ui checkbox">
<label data-tooltip-content="{{ctx.Locale.Tr "install.default_enable_timetracking_popup"}}">{{ctx.Locale.Tr "install.default_enable_timetracking"}}</label>
<input name="default_enable_timetracking" type="checkbox" {{if .default_enable_timetracking}}checked{{end}}>
</div>
</div>
<div class="inline field"> <div class="inline field">
<label for="no_reply_address">{{ctx.Locale.Tr "install.no_reply_address"}}</label> <label for="no_reply_address">{{ctx.Locale.Tr "install.no_reply_address"}}</label>
<input id="_no_reply_address" name="no_reply_address" value="{{.no_reply_address}}"> <input id="_no_reply_address" name="no_reply_address" value="{{.no_reply_address}}">
<span class="help">{{ctx.Locale.Tr "install.no_reply_address_helper"}}</span> <span class="help">{{ctx.Locale.Tr "install.no_reply_address_helper"}}</span>
</div> </div>
<div class="inline field">
<label for="password_algorithm">{{ctx.Locale.Tr "install.password_algorithm"}}</label>
<div class="ui selection dropdown">
<input id="password_algorithm" type="hidden" name="password_algorithm" value="{{.password_algorithm}}">
<div class="text">{{.password_algorithm}}</div>
{{svg "octicon-triangle-down" 14 "dropdown icon"}}
<div class="menu">
{{range .PasswordHashAlgorithms}}
<div class="item" data-value="{{.}}">{{.}}</div>
{{end}}
</div>
</div>
<span class="help">{{ctx.Locale.Tr "install.password_algorithm_helper"}}</span>
</div>
</details> </details>
<!-- Admin --> <!-- Admin -->
<details class="optional field"> <details class="optional field" {{if .Err_Admin}}open{{end}}>
<summary class="right-content tw-py-2{{if .Err_Admin}} tw-text-red{{end}}"> <summary class="right-content tw-py-2 {{if .Err_Admin}}tw-text-red{{end}}">
{{ctx.Locale.Tr "install.admin_title"}} {{ctx.Locale.Tr "install.admin_title"}}
</summary> </summary>
<p class="center">{{ctx.Locale.Tr "install.admin_setting_desc"}}</p> <p class="center">{{ctx.Locale.Tr "install.admin_setting_desc"}}</p>
@@ -324,6 +270,9 @@
{{$filePath := HTMLFormat `<span class="ui label">%s</span> <button type="button" class="btn interact-fg" data-clipboard-text="%s">%s</button>` .CustomConfFile .CustomConfFile $copyBtn}} {{$filePath := HTMLFormat `<span class="ui label">%s</span> <button type="button" class="btn interact-fg" data-clipboard-text="%s">%s</button>` .CustomConfFile .CustomConfFile $copyBtn}}
{{ctx.Locale.Tr "install.config_write_file_prompt" $filePath}} {{ctx.Locale.Tr "install.config_write_file_prompt" $filePath}}
</div> </div>
<div class="right-content tw-mt-2">
{{ctx.Locale.Tr "install.config_edit_helper" "https://github.com/go-gitea/gitea/blob/main/custom/conf/app.example.ini"}}
</div>
<div class="tw-mt-4 tw-mb-2 tw-text-center"> <div class="tw-mt-4 tw-mb-2 tw-text-center">
<button type="submit" class="ui primary button">{{ctx.Locale.Tr "install.install_btn_confirm"}}</button> <button type="submit" class="ui primary button">{{ctx.Locale.Tr "install.install_btn_confirm"}}</button>
</div> </div>
+2 -2
View File
@@ -34,7 +34,7 @@ func TestGoGet(t *testing.T) {
<body> <body>
go get --insecure %[1]s:%[2]s/blah/glah go get --insecure %[1]s:%[2]s/blah/glah
</body> </body>
</html>`, setting.Domain, setting.HTTPPort, setting.AppURL) </html>`, setting.AppDomain, setting.HTTPPort, setting.AppURL)
assert.Equal(t, expected, resp.Body.String()) assert.Equal(t, expected, resp.Body.String())
} }
@@ -55,7 +55,7 @@ func TestGoGetForSSH(t *testing.T) {
<body> <body>
go get --insecure %[1]s:%[2]s/blah/glah go get --insecure %[1]s:%[2]s/blah/glah
</body> </body>
</html>`, setting.Domain, setting.HTTPPort, setting.AppURL, setting.SSH.Domain, setting.SSH.Port) </html>`, setting.AppDomain, setting.HTTPPort, setting.AppURL, setting.SSH.Domain, setting.SSH.Port)
assert.Equal(t, expected, resp.Body.String()) assert.Equal(t, expected, resp.Body.String())
} }
+1
View File
@@ -109,6 +109,7 @@ INSTALL_LOCK = true
[service] [service]
ENABLE_CAPTCHA = false ENABLE_CAPTCHA = false
DISABLE_REGISTRATION = false
[ui.notification] [ui.notification]
EVENT_SOURCE_UPDATE_TIME = 500ms EVENT_SOURCE_UPDATE_TIME = 500ms
-18
View File
@@ -54,27 +54,9 @@ function initPreInstall() {
appUrl.value = window.location.href; appUrl.value = window.location.href;
} }
const domain = document.querySelector<HTMLInputElement>('#domain')!;
if (domain.value.trim() === 'localhost') {
domain.value = window.location.hostname;
}
// TODO: better handling of exclusive relations.
document.querySelector<HTMLInputElement>('#enable-openid-signin input')!.addEventListener('change', function () {
if (this.checked) {
if (!document.querySelector<HTMLInputElement>('#disable-registration input')!.checked) {
document.querySelector<HTMLInputElement>('#enable-openid-signup input')!.checked = true;
}
} else {
document.querySelector<HTMLInputElement>('#enable-openid-signup input')!.checked = false;
}
});
document.querySelector<HTMLInputElement>('#disable-registration input')!.addEventListener('change', function () { document.querySelector<HTMLInputElement>('#disable-registration input')!.addEventListener('change', function () {
if (this.checked) { if (this.checked) {
document.querySelector<HTMLInputElement>('#enable-captcha input')!.checked = false; document.querySelector<HTMLInputElement>('#enable-captcha input')!.checked = false;
document.querySelector<HTMLInputElement>('#enable-openid-signup input')!.checked = false;
} else {
document.querySelector<HTMLInputElement>('#enable-openid-signup input')!.checked = true;
} }
}); });
document.querySelector<HTMLInputElement>('#enable-captcha input')!.addEventListener('change', function () { document.querySelector<HTMLInputElement>('#enable-captcha input')!.addEventListener('change', function () {