From f14cedc4fa48168e58b4dbb350f75bc651d2a559 Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Thu, 24 Sep 2026 07:14:31 +0800 Subject: [PATCH] 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 --- cmd/web_acme.go | 6 +- custom/conf/app.example.ini | 11 +- models/db/install/db.go | 13 +- models/unittest/testdb.go | 1 - modules/auth/password/hash/setting.go | 12 - modules/auth/webauthn/webauthn.go | 2 +- modules/auth/webauthn/webauthn_test.go | 4 +- modules/setting/api.go | 16 +- modules/setting/server.go | 66 +++--- modules/setting/service.go | 8 +- modules/setting/session.go | 2 +- modules/setting/setting.go | 11 +- modules/setting/ssh.go | 8 +- modules/templates/helper.go | 2 +- modules/templates/mail.go | 4 +- options/locale/locale_en-US.json | 37 +-- routers/install/install.go | 277 +++++++++------------- routers/install/install_test.go | 11 + routers/web/user/setting/security/2fa.go | 2 +- services/forms/user_form.go | 27 +-- services/mailer/incoming/incoming.go | 2 +- services/mailer/incoming/incoming_test.go | 2 +- services/mailer/mail.go | 4 +- services/mailer/mail_issue_common.go | 4 +- services/mailer/mail_release.go | 2 +- services/mailer/mail_release_test.go | 3 +- services/mailer/mail_test.go | 20 +- services/mailer/mail_workflow_run.go | 4 +- services/mailer/mail_workflow_run_test.go | 2 +- services/mailer/sender/message.go | 2 +- services/mailer/sender/message_test.go | 1 - templates/install.tmpl | 111 +++------ tests/integration/goget_test.go | 4 +- tools/test-e2e.sh | 1 + web_src/js/features/install.ts | 18 -- 35 files changed, 261 insertions(+), 439 deletions(-) diff --git a/cmd/web_acme.go b/cmd/web_acme.go index 10a00cc4ec5..6e40aef2924 100644 --- a/cmd/web_acme.go +++ b/cmd/web_acme.go @@ -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). // Prefer keeping the existing cert and retrying renewals asynchronously. ctx := graceful.GetManager().ShutdownContext() - err := magic.ManageSync(ctx, []string{setting.Domain}) + err := magic.ManageSync(ctx, []string{setting.AppDomain}) if err != nil { - cert, cacheErr := magic.CacheManagedCertificate(ctx, setting.Domain) + cert, cacheErr := magic.CacheManagedCertificate(ctx, setting.AppDomain) if cacheErr != nil || cert.Expired() { return errors.Join(err, cacheErr) } 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) } } diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini index 213d1a0489f..ab5ddb6beff 100644 --- a/custom/conf/app.example.ini +++ b/custom/conf/app.example.ini @@ -62,10 +62,7 @@ ;; The protocol the server listens on. One of "http", "https", "http+unix", "fcgi" or "fcgi+unix". ;PROTOCOL = http ;; -;; Set the domain for the server. -;DOMAIN = localhost -;; -;; The AppURL is used to generate public URL links, defaults to "{PROTOCOL}://{DOMAIN}:{HTTP_PORT}/". +;; The AppURL is used to generate public URL links, defaults to "{PROTOCOL}://localhost:{HTTP_PORT}/". ;; Most users should set it to the real website URL of their Gitea instance when there is a reverse proxy. ;ROOT_URL = ;; @@ -1735,12 +1732,12 @@ LEVEL = Info ;; - login.launchpad.net ;; - .livejournal.com ;; -;; Whether to allow signin in via OpenID -;ENABLE_OPENID_SIGNIN = true +;; Whether to allow sign-in via OpenID +;ENABLE_OPENID_SIGNIN = false ;; ;; Whether to allow registering via OpenID ;; Do not include to rely on rhw DISABLE_REGISTRATION setting -;;ENABLE_OPENID_SIGNUP = true +;;ENABLE_OPENID_SIGNUP = false ;; ;; Allowed URI patterns (POSIX regexp). ;; Space separated. diff --git a/models/db/install/db.go b/models/db/install/db.go index 1fee5ff9db4..a0d8614b4e1 100644 --- a/models/db/install/db.go +++ b/models/db/install/db.go @@ -7,7 +7,6 @@ import ( "context" "gitea.dev/models/db" - "gitea.dev/modules/setting" ) // CheckDatabaseConnection checks the database connection @@ -45,15 +44,5 @@ func HasPostInstallationUsers(ctx context.Context) (bool, error) { return false, nil } - // if there are 2 or more users in database, we consider there are users created after installation - 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 + return x.Table("user").Exist() } diff --git a/models/unittest/testdb.go b/models/unittest/testdb.go index 740df0e2ab9..2b00980df02 100644 --- a/models/unittest/testdb.go +++ b/models/unittest/testdb.go @@ -61,7 +61,6 @@ func mainTest(m *testing.M, testOptsArg ...*TestOptions) int { } setting.AppURL = "https://try.gitea.io/" - setting.Domain = "try.gitea.io" setting.RunUser = "runuser" setting.SSH.User = "sshuser" setting.SSH.BuiltinServerUser = "builtinuser" diff --git a/modules/auth/password/hash/setting.go b/modules/auth/password/hash/setting.go index e0caed5ba88..8b9cc594137 100644 --- a/modules/auth/password/hash/setting.go +++ b/modules/auth/password/hash/setting.go @@ -62,15 +62,3 @@ func SetDefaultPasswordHashAlgorithm(algorithmName string) (string, *PasswordHas DefaultHashAlgorithm = Parse(algoSpec) 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 -} diff --git a/modules/auth/webauthn/webauthn.go b/modules/auth/webauthn/webauthn.go index 1afaf1cedd1..d3dfe4c3607 100644 --- a/modules/auth/webauthn/webauthn.go +++ b/modules/auth/webauthn/webauthn.go @@ -29,7 +29,7 @@ func Init() { WebAuthn = &webauthn.WebAuthn{ Config: &webauthn.Config{ RPDisplayName: setting.AppName, - RPID: setting.Domain, + RPID: setting.AppDomain, RPOrigins: []string{appURL}, AttestationPreference: protocol.PreferNoAttestation, // Gitea never verifies attestation }, diff --git a/modules/auth/webauthn/webauthn_test.go b/modules/auth/webauthn/webauthn_test.go index c83afefa2ac..f24fd5fa634 100644 --- a/modules/auth/webauthn/webauthn_test.go +++ b/modules/auth/webauthn/webauthn_test.go @@ -12,14 +12,14 @@ import ( ) func TestInit(t *testing.T) { - setting.Domain = "domain" + setting.AppDomain = "domain" setting.AppName = "AppName" setting.AppURL = "https://domain/" rpOrigin := []string{"https://domain"} 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, rpOrigin, WebAuthn.Config.RPOrigins) } diff --git a/modules/setting/api.go b/modules/setting/api.go index 05f0a485efa..d888fd3a594 100644 --- a/modules/setting/api.go +++ b/modules/setting/api.go @@ -3,13 +3,6 @@ package setting -import ( - "net/url" - "path" - - "gitea.dev/modules/log" -) - // API settings var API = struct { EnableSwagger bool @@ -31,12 +24,5 @@ var API = struct { func loadAPIFrom(rootCfg ConfigProvider) { mustMapSetting(rootCfg, "api", &API) - - 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() + API.SwaggerURL = AppURL + "api/swagger" } diff --git a/modules/setting/server.go b/modules/setting/server.go index 3ce32d73e43..6d0683655a2 100644 --- a/modules/setting/server.go +++ b/modules/setting/server.go @@ -49,7 +49,8 @@ const ( var ( // AppURL is the Application ROOT_URL. It always has a '/' suffix // 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 string @@ -80,7 +81,6 @@ var ( ProxyProtocolTLSBridging bool ProxyProtocolHeaderTimeout time.Duration ProxyProtocolAcceptUnknown bool - Domain string HTTPAddr string HTTPPort string LocalUseProxyProtocol bool @@ -114,11 +114,39 @@ var ( 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) { sec := rootCfg.Section("server") 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") HTTPPort = sec.Key("HTTP_PORT").MustString("3000") @@ -217,37 +245,7 @@ func loadServerFrom(rootCfg ConfigProvider) { PerWriteTimeout = sec.Key("PER_WRITE_TIMEOUT").MustDuration(PerWriteTimeout) PerWritePerKbTimeout = sec.Key("PER_WRITE_PER_KB_TIMEOUT").MustDuration(PerWritePerKbTimeout) - defaultAppURL := string(Protocol) + "://" + Domain + ":" + 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), "/") - - // 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 - } + loadServerDomainAndURL(sec, string(Protocol)) var defaultLocalURL string switch Protocol { diff --git a/modules/setting/service.go b/modules/setting/service.go index 18db05f7159..7a09c132bfb 100644 --- a/modules/setting/service.go +++ b/modules/setting/service.go @@ -145,7 +145,7 @@ func loadServiceFrom(rootCfg ConfigProvider) { sec := rootCfg.Section("service") Service.ActiveCodeLives = sec.Key("ACTIVE_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.AllowOnlyExternalRegistration = sec.Key("ALLOW_ONLY_EXTERNAL_REGISTRATION").MustBool() if Service.AllowOnlyExternalRegistration && Service.AllowOnlyInternalRegistration { @@ -209,7 +209,7 @@ func loadServiceFrom(rootCfg ConfigProvider) { Service.DefaultEnableDependencies = sec.Key("DEFAULT_ENABLE_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.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.EnableUserHeatmap = sec.Key("ENABLE_USER_HEATMAP").MustBool(true) Service.AutoWatchNewRepos = sec.Key("AUTO_WATCH_NEW_REPOS").MustBool(true) @@ -263,8 +263,8 @@ func loadServiceFrom(rootCfg ConfigProvider) { func loadOpenIDSetting(rootCfg ConfigProvider) { sec := rootCfg.Section("openid") - Service.EnableOpenIDSignIn = sec.Key("ENABLE_OPENID_SIGNIN").MustBool(!InstallLock) - Service.EnableOpenIDSignUp = sec.Key("ENABLE_OPENID_SIGNUP").MustBool(!Service.DisableRegistration && Service.EnableOpenIDSignIn) + Service.EnableOpenIDSignIn = sec.Key("ENABLE_OPENID_SIGNIN").MustBool(false) + Service.EnableOpenIDSignUp = sec.Key("ENABLE_OPENID_SIGNUP").MustBool(false) pats := sec.Key("WHITELISTED_URIS").Strings(" ") if len(pats) != 0 { Service.OpenIDWhitelist = make([]*regexp.Regexp, len(pats)) diff --git a/modules/setting/session.go b/modules/setting/session.go index f8780e7e0b5..67f1b7ca6b5 100644 --- a/modules/setting/session.go +++ b/modules/setting/session.go @@ -42,7 +42,7 @@ var SessionConfig = struct { func loadSessionFrom(rootCfg ConfigProvider) { 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 { case "redis": diff --git a/modules/setting/setting.go b/modules/setting/setting.go index 696d945bb8b..94d3432ad2d 100644 --- a/modules/setting/setting.go +++ b/modules/setting/setting.go @@ -7,6 +7,7 @@ package setting import ( "fmt" "os" + "path/filepath" "runtime" "strings" "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. // 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. - + if !filepath.IsAbs(AppDataPath) { + return fmt.Errorf("app data path %q must be an absolute path", AppDataPath) + } st, err := os.Stat(AppDataPath) if os.IsNotExist(err) { err = os.MkdirAll(AppDataPath, os.ModePerm) 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 } 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 */ { - 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 diff --git a/modules/setting/ssh.go b/modules/setting/ssh.go index 5064522e66c..60b3bcbe60d 100644 --- a/modules/setting/ssh.go +++ b/modules/setting/ssh.go @@ -93,19 +93,15 @@ func parseAuthorizedPrincipalsAllow(values []string) ([]string, bool) { } func loadSSHFrom(rootCfg ConfigProvider) { - sec := rootCfg.Section("server") - if len(SSH.Domain) == 0 { - SSH.Domain = Domain - } - homeDir, err := util.HomeDir() if err != nil { log.Fatal("Failed to get home directory: %v", err) } homeDir = strings.ReplaceAll(homeDir, "\\", "/") - SSH.RootPath = filepath.Join(homeDir, ".ssh") + SSH.Domain = AppDomain + sec := rootCfg.Section("server") if err = sec.MapTo(&SSH); err != nil { log.Fatal("Failed to map SSH settings: %v", err) } diff --git a/modules/templates/helper.go b/modules/templates/helper.go index ae7f3636a6d..c4f0f16225a 100644 --- a/modules/templates/helper.go +++ b/modules/templates/helper.go @@ -85,7 +85,7 @@ func newFuncMapWebPage() template.FuncMap { return setting.AppVer }, "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 { return setting.Other.ShowFooterTemplateLoadTime diff --git a/modules/templates/mail.go b/modules/templates/mail.go index 4383a0a3d33..023be18f839 100644 --- a/modules/templates/mail.go +++ b/modules/templates/mail.go @@ -54,7 +54,7 @@ func mailSubjectTextFuncMap() texttmpl.FuncMap { return setting.AppName }, "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 }, "AppDomain": func() string { - return setting.Domain + return setting.AppDomain }, } } diff --git a/options/locale/locale_en-US.json b/options/locale/locale_en-US.json index d043a6986b6..28ae21e7eb5 100644 --- a/options/locale/locale_en-US.json +++ b/options/locale/locale_en-US.json @@ -244,6 +244,7 @@ "install.installing_desc": "Installing now, please wait…", "install.title": "Initial Configuration", "install.docker_helper": "If you run Gitea inside Docker, please read the documentation before changing any settings.", + "install.config_edit_helper": "To fine tune more details: edit the configuration file after installation. Reference: app.example.ini.", "install.require_db_desc": "Gitea requires MySQL, PostgreSQL, MSSQL, SQLite3 or TiDB (MySQL protocol).", "install.db_title": "Database Settings", "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.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.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_email": "The administrator email address cannot be empty.", "install.err_admin_name_is_reserved": "Administrator username is invalid. Username is reserved.", @@ -271,22 +273,18 @@ "install.general_title": "General Settings", "install.app_name": "Site Title", "install.app_name_helper": "You can enter your company name here.", - "install.repo_path": "Repository Root Path", - "install.repo_path_helper": "Remote Git repositories 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.app_data_path": "Data Path", + "install.app_data_path_helper": "Most Gitea data will be saved to this directory.", "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.domain": "Server Domain", "install.domain_helper": "Domain or host address for the server.", "install.ssh_port": "SSH Server Port", "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.app_url": "Gitea Base URL", - "install.app_url_helper": "Base address for HTTP(S) 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.app_url": "Gitea Website URL", + "install.app_url_helper": "Gitea web application's public URL for HTTP(S) access, clone URLs and email notifications.", "install.optional_title": "Optional Settings", "install.email_title": "Email Settings", "install.smtp_addr": "SMTP Host", @@ -299,17 +297,11 @@ "install.register_confirm": "Require Email Confirmation to Register", "install.mail_notify": "Enable Email Notifications", "install.server_service_title": "Server and Third-Party Service Settings", - "install.disable_registration": "Disable 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.disable_registration": "Only administrators can create user accounts (no self-registration)", "install.enable_captcha": "Enable registration CAPTCHA", "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_popup": "Limit page access to signed-in users. Visitors will only see the sign-in and registration 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. 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_title": "Administrator Account Settings", "install.admin_name": "Administrator Username", @@ -320,7 +312,6 @@ "install.test_git_failed": "Could not test 'git' command: %v", "install.invalid_db_setting": "The database settings are 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.internal_token_failed": "Failed to generate internal token: %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_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_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_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.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.enable_update_checker": "Enable update checker", "install.env_config_keys": "Environment Configuration", "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", diff --git a/routers/install/install.go b/routers/install/install.go index 9f8fa67c663..1490947b879 100644 --- a/routers/install/install.go +++ b/routers/install/install.go @@ -10,6 +10,7 @@ import ( "os" "os/exec" "path/filepath" + "regexp" "slices" "strconv" "strings" @@ -19,7 +20,6 @@ import ( "gitea.dev/models/db" db_install "gitea.dev/models/db/install" user_model "gitea.dev/models/user" - "gitea.dev/modules/auth/password/hash" "gitea.dev/modules/generate" "gitea.dev/modules/graceful" "gitea.dev/modules/log" @@ -39,7 +39,6 @@ import ( ) const ( - // tplInstall template for installation page tplInstall templates.TplName = "install" tplPostInstall templates.TplName = "post-install" ) @@ -54,10 +53,9 @@ func getSupportedDbTypeNames() (dbTypeNames []map[string]string) { func installContexter() func(next http.Handler) http.Handler { return context.ContexterInstallPage(map[string]any{ - "DbTypeNames": getSupportedDbTypeNames(), - "EnvConfigKeys": setting.CollectEnvConfigKeys(), - "CustomConfFile": setting.CustomConf, - "PasswordHashAlgorithms": hash.RecommendedHashAlgorithms, + "DbTypeNames": getSupportedDbTypeNames(), + "EnvConfigKeys": setting.CollectEnvConfigKeys(), + "CustomConfFile": setting.CustomConf, }) } @@ -87,14 +85,11 @@ func Install(ctx *context.Context) { // Application general settings form.AppName = setting.AppName - form.RepoRootPath = setting.RepoRootPath - form.LFSRootPath = setting.LFS.Storage.Path + form.AppDataPath = setting.AppDataPath form.RunUser = setting.RunUser - form.Domain = setting.Domain form.SSHPort = setting.SSH.Port form.HTTPPort = setting.HTTPPort form.AppURL = setting.AppURL - form.LogRootPath = setting.Log.RootPath // E-mail service settings if setting.MailService != nil { @@ -108,27 +103,19 @@ func Install(ctx *context.Context) { form.MailNotify = setting.Service.EnableNotifyMail 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.AllowOnlyExternalRegistration = setting.Service.AllowOnlyExternalRegistration form.EnableCaptcha = setting.Service.EnableCaptcha form.RequireSignInView = setting.Service.RequireSignInViewStrict form.DefaultKeepEmailPrivate = setting.Service.DefaultKeepEmailPrivate form.DefaultAllowCreateOrganization = setting.Service.DefaultAllowCreateOrganization - form.DefaultEnableTimetracking = setting.Service.DefaultEnableTimetracking form.NoReplyAddress = setting.Service.NoReplyAddress - form.PasswordAlgorithm = hash.ConfigHashAlgorithm(setting.PasswordHashAlgo) middleware.AssignForm(form, ctx.Data) ctx.HTML(http.StatusOK, tplInstall) } func checkDatabase(ctx *context.Context, form *forms.InstallForm) bool { - var err error - - if (setting.Database.Type == setting.DatabaseTypeSQLite3) && - len(setting.Database.Path) == 0 { + if setting.Database.Type.IsSQLite3() && setting.Database.Path == "" { ctx.Data["Err_DbPath"] = true ctx.RenderWithErrDeprecated(ctx.Tr("install.err_empty_db_path"), tplInstall, form) return false @@ -138,13 +125,13 @@ func checkDatabase(ctx *context.Context, form *forms.InstallForm) bool { 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.RenderWithErrDeprecated(ctx.Tr("install.invalid_db_setting", err), tplInstall, form) return false } - err = db_install.CheckDatabaseConnection(ctx) + err := db_install.CheckDatabaseConnection(ctx) if err != nil { ctx.Data["Err_DbSetting"] = true 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") } + 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 { 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 } - var err error - form := web.GetForm[*forms.InstallForm](ctx) // fix form values @@ -208,7 +209,7 @@ func SubmitInstall(ctx *context.Context) { 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) return } @@ -226,52 +227,21 @@ func SubmitInstall(ctx *context.Context) { setting.Database.Path = form.DbPath setting.Database.LogSQL = !setting.IsProd - if !checkDatabase(ctx, form) { - return - } - // 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) return } - // Test repository root path. - 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) + if !checkDatabase(ctx, form) { return } // Check admin user creation - if len(form.AdminName) > 0 { + if form.AdminName != "" { // Ensure AdminName is valid if err := user_model.IsUsableUsername(form.AdminName); err != nil { ctx.Data["Err_Admin"] = true @@ -309,7 +279,7 @@ func SubmitInstall(ctx *context.Context) { } // 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() ctx.Data["Err_DbSetting"] = true ctx.RenderWithErrDeprecated(ctx.Tr("install.invalid_db_setting", err), tplInstall, form) @@ -320,7 +290,15 @@ func SubmitInstall(ctx *context.Context) { if cfg == nil { 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 { @@ -332,11 +310,12 @@ func fillInstallConfig(ctx *context.Context, envs []string, form *forms.InstallF } 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("RUN_USER").SetValue(form.RunUser) 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("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("SSL_MODE").SetValue(setting.Database.SSLMode) 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("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 { 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)) } - if form.LFSRootPath != "" { - 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 form.SMTPAddr != "" { if _, err := mail.ParseAddress(form.SMTPFrom); err != nil { ctx.RenderWithErrDeprecated(ctx.Tr("install.smtp_from_invalid"), tplInstall, form) 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("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("ALLOW_ONLY_EXTERNAL_REGISTRATION").SetValue(strconv.FormatBool(form.AllowOnlyExternalRegistration)) 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("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_ENABLE_TIMETRACKING").SetValue(strconv.FormatBool(form.DefaultEnableTimetracking)) cfg.Section("service").Key("NO_REPLY_ADDRESS").SetValue(form.NoReplyAddress) + 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("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) // if there is no InternalToken, generate one and save to security.INTERNAL_TOKEN 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) } - 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) + fillInstallConfigCleanUp(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) err := os.MkdirAll(filepath.Dir(setting.CustomConf), os.ModePerm) if err != nil { ctx.RenderWithErrDeprecated(ctx.Tr("install.save_config_failed", err), tplInstall, form) - return + return false } if err := cfg.SaveTo(setting.CustomConf); err != nil { ctx.RenderWithErrDeprecated(ctx.Tr("install.save_config_failed", err), tplInstall, form) - return + return false } // unset default engine before reload database setting db.UnsetDefaultEngine() - // ---- All checks are passed - // Reload settings (and re-initialize database connection) setting.InitCfgProvider(setting.CustomConf) setting.LoadCommonSettings() @@ -486,64 +443,64 @@ func saveConfigAndRestart(ctx *context.Context, cfg setting.ConfigProvider, form log.Fatal("ORM engine initialization failed: %v", err) } - // Create admin account - if len(form.AdminName) > 0 { - u := &user_model.User{ - Name: form.AdminName, - Email: form.AdminEmail, - Passwd: form.AdminPasswd, - IsAdmin: true, - } - overwriteDefault := &user_model.CreateUserOverwriteOptions{ - IsRestricted: optional.Some(false), - IsActive: optional.Some(true), - } + setting.ClearEnvConfigKeys() + log.Info("Installation completed! You can also use 'gitea admin user ...' sub-commands to create or edit admin users.") + log.Info("----------------------------------------") + return true +} - if err := user_model.CreateUser(ctx, u, &user_model.Meta{}, overwriteDefault); err != nil { - if !user_model.IsErrUserAlreadyExist(err) { - setting.InstallLock = false - 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 - } +func initAdminUser(ctx *context.Context, form *forms.InstallForm) bool { + if form.AdminName == "" { + return true } - setting.ClearEnvConfigKeys() - log.Info("First-time run install finished!") - InstallDone(ctx) + adminUser := &user_model.User{ + Name: form.AdminName, + 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() { // 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 .... 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 { log.Error("Unable to shutdown the install server! Error: %v", err) } diff --git a/routers/install/install_test.go b/routers/install/install_test.go index 770fddc1eed..534b1156019 100644 --- a/routers/install/install_test.go +++ b/routers/install/install_test.go @@ -9,6 +9,7 @@ import ( "testing" "gitea.dev/models/unittest" + "gitea.dev/modules/setting" "gitea.dev/services/contexttest" "gitea.dev/services/forms" @@ -59,4 +60,14 @@ func TestFillInstallConfig(t *testing.T) { assert.NotEmpty(t, cfg.Section("oauth2").Key("JWT_SECRET").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()) + }) } diff --git a/routers/web/user/setting/security/2fa.go b/routers/web/user/setting/security/2fa.go index 992df455ad8..bc9d57dd865 100644 --- a/routers/web/user/setting/security/2fa.go +++ b/routers/web/user/setting/security/2fa.go @@ -114,7 +114,7 @@ func twofaGenerateSecretAndQr(ctx *context.Context) bool { } } // Filter unsafe character ':' in issuer - issuer := strings.ReplaceAll(setting.AppName+" ("+setting.Domain+")", ":", "") + issuer := strings.ReplaceAll(setting.AppName+" ("+setting.AppDomain+")", ":", "") if otpKey == nil { otpKey, err = totp.Generate(totp.GenerateOpts{ SecretSize: 40, diff --git a/services/forms/user_form.go b/services/forms/user_form.go index 3f111df9d06..5f5eb8ca1f3 100644 --- a/services/forms/user_form.go +++ b/services/forms/user_form.go @@ -28,15 +28,12 @@ type InstallForm struct { DbPath string `binding:"TrimSpace"` DbSchema string `binding:"TrimSpace"` - AppName string `binding:"TrimSpace;Required" locale:"install.app_name"` - RepoRootPath string `binding:"TrimSpace;Required"` - LFSRootPath string `binding:"TrimSpace"` - RunUser string `binding:"TrimSpace;Required"` - Domain string `binding:"TrimSpace;Required"` - SSHPort int - HTTPPort string `binding:"TrimSpace;Required"` - AppURL string `binding:"TrimSpace;Required"` - LogRootPath string `binding:"TrimSpace;Required"` + AppName string `binding:"TrimSpace;Required" locale:"install.app_name"` + AppDataPath string `binding:"TrimSpace;Required"` + RunUser string `binding:"TrimSpace;Required"` + SSHPort int + HTTPPort string `binding:"TrimSpace;Required"` + AppURL string `binding:"TrimSpace;Required"` SMTPAddr string `binding:"TrimSpace"` SMTPPort string `binding:"TrimSpace"` @@ -46,24 +43,18 @@ type InstallForm struct { RegisterConfirm bool MailNotify bool - EnableOpenIDSignIn bool - EnableOpenIDSignUp bool DisableRegistration bool - AllowOnlyExternalRegistration bool EnableCaptcha bool RequireSignInView bool DefaultKeepEmailPrivate bool DefaultAllowCreateOrganization bool - DefaultEnableTimetracking bool EnableUpdateChecker bool NoReplyAddress string `binding:"TrimSpace"` - PasswordAlgorithm string `binding:"TrimSpace"` - - AdminName string `binding:"TrimSpace;OmitEmpty;Username;MaxSize(30)" locale:"install.admin_name"` - AdminPasswd string `binding:"OmitEmpty;MaxSize(255)" locale:"install.admin_password"` + AdminName string `binding:"TrimSpace;Username;MaxSize(30)" locale:"install.admin_name"` + AdminPasswd string `binding:"MaxSize(255)" locale:"install.admin_password"` 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 bool diff --git a/services/mailer/incoming/incoming.go b/services/mailer/incoming/incoming.go index eab7ad6c91f..881126b4255 100644 --- a/services/mailer/incoming/incoming.go +++ b/services/mailer/incoming/incoming.go @@ -308,7 +308,7 @@ func searchTokenInHeaders(env *enmime.Envelope) string { if end == -1 || begin > end { break } - t := extractToken(references[begin:end], "reply-", "@"+setting.Domain) + t := extractToken(references[begin:end], "reply-", "@"+setting.AppDomain) if t != "" { return t } diff --git a/services/mailer/incoming/incoming_test.go b/services/mailer/incoming/incoming_test.go index 9588c9021ac..9700368cf74 100644 --- a/services/mailer/incoming/incoming_test.go +++ b/services/mailer/incoming/incoming_test.go @@ -71,8 +71,8 @@ func TestIsAutomaticReply(t *testing.T) { } func TestSearchTokenInHeadersCaseInsensitive(t *testing.T) { + setting.AppDomain = "DoMain.com" setting.IncomingEmail.ReplyToAddress = "InComing+%{token}@ExAmPle.com" - setting.Domain = "DoMain.com" mkEnv := func(s string) *enmime.Envelope { env, _ := enmime.ReadEnvelope(strings.NewReader(s + "\r\n\r\n")) return env diff --git a/services/mailer/mail.go b/services/mailer/mail.go index 4bbb8b15ed0..ee4ba64d653 100644 --- a/services/mailer/mail.go +++ b/services/mailer/mail.go @@ -162,7 +162,7 @@ func fromDisplayName(u *user_model.User) string { err := setting.MailService.FromDisplayNameFormatTemplate.Execute(&buf, map[string]any{ "DisplayName": u.DisplayName(), "AppName": setting.AppName, - "Domain": setting.Domain, + "Domain": setting.AppDomain, }) if err == nil { 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 { return map[string]string{ // 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 "List-Archive": fmt.Sprintf("<%s>", repo.HTMLURL()), diff --git a/services/mailer/mail_issue_common.go b/services/mailer/mail_issue_common.go index 8bd5929cc10..95e5aa18cce 100644 --- a/services/mailer/mail_issue_common.go +++ b/services/mailer/mail_issue_common.go @@ -188,7 +188,7 @@ func composeIssueCommentMessages(ctx context.Context, comment *mailComment, lang msg.ReplyTo = replyAddress msg.SetHeader("List-Post", fmt.Sprintf("", replyAddress)) - references = append(references, fmt.Sprintf("", token, setting.Domain)) + references = append(references, fmt.Sprintf("", 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 { diff --git a/services/mailer/mail_release.go b/services/mailer/mail_release.go index 92f30fb4bc5..ced05c29159 100644 --- a/services/mailer/mail_release.go +++ b/services/mailer/mail_release.go @@ -25,7 +25,7 @@ import ( const tplNewReleaseMail templates.TplName = "mail/repo/release" 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. diff --git a/services/mailer/mail_release_test.go b/services/mailer/mail_release_test.go index 9c84090e419..a2ed0072c1a 100644 --- a/services/mailer/mail_release_test.go +++ b/services/mailer/mail_release_test.go @@ -21,7 +21,7 @@ func TestMailNewReleaseFiltersUnauthorizedWatchers(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) defer test.MockVariableValue(&setting.MailService)() - defer test.MockVariableValue(&setting.Domain)() + defer test.MockVariableValue(&setting.AppDomain)() defer test.MockVariableValue(&setting.AppName)() defer test.MockVariableValue(&setting.AppURL)() @@ -29,7 +29,6 @@ func TestMailNewReleaseFiltersUnauthorizedWatchers(t *testing.T) { From: "Gitea", FromEmail: "noreply@example.com", } - setting.Domain = "example.com" setting.AppName = "Gitea" setting.AppURL = "https://example.com/" defer mockMailTemplates(string(tplNewReleaseMail), "{{.Subject}}", "

{{.Release.TagName}}

")() diff --git a/services/mailer/mail_test.go b/services/mailer/mail_test.go index b46de7e9c36..03c7f5f8258 100644 --- a/services/mailer/mail_test.go +++ b/services/mailer/mail_test.go @@ -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) { assert.NoError(t, unittest.PrepareTestDatabase()) setting.MailService = &setting.Mailer{From: "test@gitea.com"} - setting.Domain = "localhost" setting.AppURL = "https://try.gitea.io/" doer = unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) @@ -390,7 +389,7 @@ func TestGenerateMessageIDForIssue(t *testing.T) { issue: issue, 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", @@ -398,7 +397,7 @@ func TestGenerateMessageIDForIssue(t *testing.T) { issue: pullIssue, 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", @@ -407,7 +406,7 @@ func TestGenerateMessageIDForIssue(t *testing.T) { comment: comment, 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", @@ -416,7 +415,7 @@ func TestGenerateMessageIDForIssue(t *testing.T) { comment: comment, 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", @@ -518,15 +517,8 @@ func TestFromDisplayName(t *testing.T) { tmpl, err = texttmpl.New("mailFrom").Parse("{{ .DisplayName }} (by {{ .AppName }} on [{{ .Domain }}])") assert.NoError(t, err) setting.MailService = &setting.Mailer{FromDisplayNameFormatTemplate: tmpl} - oldAppName := setting.AppName - setting.AppName = "Code IT" - oldDomain := setting.Domain - setting.Domain = "code.it" - defer func() { - setting.AppName = oldAppName - setting.Domain = oldDomain - }() - + defer test.MockVariableValue(&setting.AppName, "Code IT")() + defer test.MockVariableValue(&setting.AppDomain, "code.it")() assert.Equal(t, "Mister X (by Code IT on [code.it])", fromDisplayName(&user_model.User{FullName: "Mister X", Name: "tmp"})) }) } diff --git a/services/mailer/mail_workflow_run.go b/services/mailer/mail_workflow_run.go index 1ae8b544189..f26a3135e66 100644 --- a/services/mailer/mail_workflow_run.go +++ b/services/mailer/mail_workflow_run.go @@ -85,7 +85,7 @@ func composeAndSendActionsWorkflowRunStatusEmail(ctx context.Context, repo *repo embedded := make(container.Set[string]) for _, job := range jobs { 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{ HTMLURL: fmt.Sprintf("%s/actions/runs/%d/jobs/%d", repo.HTMLURL(ctx), run.ID, job.ID), Name: job.Name, @@ -143,7 +143,7 @@ func composeAndSendActionsWorkflowRunStatusEmail(ctx context.Context, repo *repo for key, value := range generateMetadataHeaders(repo) { 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) return nil diff --git a/services/mailer/mail_workflow_run_test.go b/services/mailer/mail_workflow_run_test.go index 54f8d3b2300..5b171c227b2 100644 --- a/services/mailer/mail_workflow_run_test.go +++ b/services/mailer/mail_workflow_run_test.go @@ -56,7 +56,7 @@ func TestWorkflowRunMail(t *testing.T) { translation.InitLocales(t.Context()) require.NoError(t, unittest.PrepareTestDatabase()) 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/")() recipient := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) diff --git a/services/mailer/sender/message.go b/services/mailer/sender/message.go index a223aee71d1..f5a32b55125 100644 --- a/services/mailer/sender/message.go +++ b/services/mailer/sender/message.go @@ -101,7 +101,7 @@ func (m *Message) generateAutoMessageID() string { } _, _ = h.Write([]byte(m.Subject)) _, _ = h.Write([]byte(m.Body)) - return fmt.Sprintf("", dateMs, h.Sum64(), setting.Domain) + return fmt.Sprintf("", dateMs, h.Sum64(), setting.AppDomain) } // NewMessageFrom creates new mail message object with custom From header. diff --git a/services/mailer/sender/message_test.go b/services/mailer/sender/message_test.go index 441fb818e88..5f18c6fde74 100644 --- a/services/mailer/sender/message_test.go +++ b/services/mailer/sender/message_test.go @@ -19,7 +19,6 @@ func TestGenerateMessageID(t *testing.T) { } setting.MailService = &mailService - setting.Domain = "localhost" date := time.Date(2000, 1, 2, 3, 4, 5, 6, time.UTC) m := NewMessageFrom("", "display-name", "from-address", "subject", "body") diff --git a/templates/install.tmpl b/templates/install.tmpl index 172fd97e2da..a9b993acaed 100644 --- a/templates/install.tmpl +++ b/templates/install.tmpl @@ -107,26 +107,16 @@ {{ctx.Locale.Tr "install.app_name_helper"}} -
- - - {{ctx.Locale.Tr "install.repo_path_helper"}} -
-
- - - {{ctx.Locale.Tr "install.lfs_path_helper"}} +
+ + + {{ctx.Locale.Tr "install.app_data_path_helper"}}
{{ctx.Locale.Tr "install.run_user_helper"}}
-
- - - {{ctx.Locale.Tr "install.domain_helper"}} -
@@ -142,25 +132,37 @@ {{ctx.Locale.Tr "install.app_url_helper"}}
-
- - - {{ctx.Locale.Tr "install.log_root_path_helper"}} +
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
- {{ctx.Locale.Tr "install.enable_update_checker_helper"}}

{{ctx.Locale.Tr "install.optional_title"}}

-
- +
+ {{ctx.Locale.Tr "install.email_title"}}
@@ -199,46 +201,10 @@
-
- +
+ {{ctx.Locale.Tr "install.server_service_title"}} -
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
@@ -251,36 +217,16 @@
-
-
- - -
-
{{ctx.Locale.Tr "install.no_reply_address_helper"}}
-
- - - {{ctx.Locale.Tr "install.password_algorithm_helper"}} -
-
- +
+ {{ctx.Locale.Tr "install.admin_title"}}

{{ctx.Locale.Tr "install.admin_setting_desc"}}

@@ -324,6 +270,9 @@ {{$filePath := HTMLFormat `%s ` .CustomConfFile .CustomConfFile $copyBtn}} {{ctx.Locale.Tr "install.config_write_file_prompt" $filePath}}
+
+ {{ctx.Locale.Tr "install.config_edit_helper" "https://github.com/go-gitea/gitea/blob/main/custom/conf/app.example.ini"}} +
diff --git a/tests/integration/goget_test.go b/tests/integration/goget_test.go index 6b8e9b41451..b254c5e9b15 100644 --- a/tests/integration/goget_test.go +++ b/tests/integration/goget_test.go @@ -34,7 +34,7 @@ func TestGoGet(t *testing.T) { go get --insecure %[1]s:%[2]s/blah/glah -`, setting.Domain, setting.HTTPPort, setting.AppURL) +`, setting.AppDomain, setting.HTTPPort, setting.AppURL) assert.Equal(t, expected, resp.Body.String()) } @@ -55,7 +55,7 @@ func TestGoGetForSSH(t *testing.T) { go get --insecure %[1]s:%[2]s/blah/glah -`, setting.Domain, setting.HTTPPort, setting.AppURL, setting.SSH.Domain, setting.SSH.Port) +`, setting.AppDomain, setting.HTTPPort, setting.AppURL, setting.SSH.Domain, setting.SSH.Port) assert.Equal(t, expected, resp.Body.String()) } diff --git a/tools/test-e2e.sh b/tools/test-e2e.sh index cf7a39d2e11..87451097b14 100755 --- a/tools/test-e2e.sh +++ b/tools/test-e2e.sh @@ -109,6 +109,7 @@ INSTALL_LOCK = true [service] ENABLE_CAPTCHA = false +DISABLE_REGISTRATION = false [ui.notification] EVENT_SOURCE_UPDATE_TIME = 500ms diff --git a/web_src/js/features/install.ts b/web_src/js/features/install.ts index 1154ccc27cd..c4ed2dbb0d3 100644 --- a/web_src/js/features/install.ts +++ b/web_src/js/features/install.ts @@ -54,27 +54,9 @@ function initPreInstall() { appUrl.value = window.location.href; } - const domain = document.querySelector('#domain')!; - if (domain.value.trim() === 'localhost') { - domain.value = window.location.hostname; - } - - // TODO: better handling of exclusive relations. - document.querySelector('#enable-openid-signin input')!.addEventListener('change', function () { - if (this.checked) { - if (!document.querySelector('#disable-registration input')!.checked) { - document.querySelector('#enable-openid-signup input')!.checked = true; - } - } else { - document.querySelector('#enable-openid-signup input')!.checked = false; - } - }); document.querySelector('#disable-registration input')!.addEventListener('change', function () { if (this.checked) { document.querySelector('#enable-captcha input')!.checked = false; - document.querySelector('#enable-openid-signup input')!.checked = false; - } else { - document.querySelector('#enable-openid-signup input')!.checked = true; } }); document.querySelector('#enable-captcha input')!.addEventListener('change', function () {