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
2026-09-24 07:14:31 +08:00
committed by GitHub
parent 71065941a9
commit f14cedc4fa
35 changed files with 261 additions and 439 deletions
+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).
// 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)
}
}
+4 -7
View File
@@ -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
;; - <username>.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.
+1 -12
View File
@@ -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()
}
-1
View File
@@ -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"
-12
View File
@@ -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
}
+1 -1
View File
@@ -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
},
+2 -2
View File
@@ -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)
}
+1 -15
View File
@@ -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"
}
+32 -34
View File
@@ -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 {
+4 -4
View File
@@ -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))
+1 -1
View File
@@ -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":
+7 -4
View File
@@ -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
+2 -6
View File
@@ -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)
}
+1 -1
View File
@@ -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
+2 -2
View File
@@ -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
},
}
}
+11 -26
View File
@@ -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 <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.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",
+117 -160
View File
@@ -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)
}
+11
View File
@@ -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())
})
}
+1 -1
View File
@@ -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,
+9 -18
View File
@@ -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
+1 -1
View File
@@ -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
}
+1 -1
View File
@@ -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
+2 -2
View File
@@ -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()),
+2 -2
View File
@@ -188,7 +188,7 @@ func composeIssueCommentMessages(ctx context.Context, comment *mailComment, lang
msg.ReplyTo = 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 {
+1 -1
View File
@@ -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.
+1 -2
View File
@@ -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}}", "<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) {
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"}))
})
}
+2 -2
View File
@@ -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
+1 -1
View File
@@ -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})
+1 -1
View File
@@ -101,7 +101,7 @@ func (m *Message) generateAutoMessageID() string {
}
_, _ = h.Write([]byte(m.Subject))
_, _ = 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.
-1
View File
@@ -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")
+30 -81
View File
@@ -107,26 +107,16 @@
<input id="app_name" name="app_name" value="{{.app_name}}" required>
<span class="help">{{ctx.Locale.Tr "install.app_name_helper"}}</span>
</div>
<div class="inline required field {{if .Err_RepoRootPath}}error{{end}}">
<label for="repo_root_path">{{ctx.Locale.Tr "install.repo_path"}}</label>
<input id="repo_root_path" name="repo_root_path" value="{{.repo_root_path}}" required>
<span class="help">{{ctx.Locale.Tr "install.repo_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 class="inline required field {{if .Err_AppDataPath}}error{{end}}">
<label>{{ctx.Locale.Tr "install.app_data_path"}}</label>
<input name="app_data_path" value="{{.app_data_path}}" required>
<span class="help">{{ctx.Locale.Tr "install.app_data_path_helper"}}</span>
</div>
<div class="inline field">
<label for="run_user">{{ctx.Locale.Tr "install.run_user"}}</label>
<input id="run_user" name="run_user" value="{{.run_user}}" readonly>
<span class="help">{{ctx.Locale.Tr "install.run_user_helper"}}</span>
</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">
<label for="ssh_port">{{ctx.Locale.Tr "install.ssh_port"}}</label>
<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>
<span class="help">{{ctx.Locale.Tr "install.app_url_helper"}}</span>
</div>
<div class="inline required field">
<label for="log_root_path">{{ctx.Locale.Tr "install.log_root_path"}}</label>
<input id="log_root_path" name="log_root_path" value="{{.log_root_path}}" placeholder="log" required>
<span class="help">{{ctx.Locale.Tr "install.log_root_path_helper"}}</span>
<div class="inline field">
<div class="ui checkbox" id="disable-registration">
<label>{{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="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="ui checkbox">
<label>{{ctx.Locale.Tr "install.enable_update_checker"}}</label>
<input name="enable_update_checker" type="checkbox" {{if .enable_update_checker}}checked{{end}}>
</div>
<span class="help">{{ctx.Locale.Tr "install.enable_update_checker_helper"}}</span>
</div>
<!-- Optional Settings -->
<h4 class="ui dividing header">{{ctx.Locale.Tr "install.optional_title"}}</h4>
<div>
<!-- Email -->
<details class="optional field">
<summary class="right-content tw-py-2{{if .Err_SMTP}} tw-text-red{{end}}">
<details class="optional field" {{if .Err_SMTP}}open{{end}}>
<summary class="right-content tw-py-2 {{if .Err_SMTP}}tw-text-red{{end}}">
{{ctx.Locale.Tr "install.email_title"}}
</summary>
<div class="inline field">
@@ -199,46 +201,10 @@
</details>
<!-- Server and other services -->
<details class="optional field">
<summary class="right-content tw-py-2{{if .Err_Services}} tw-text-red{{end}}">
<details class="optional field" {{if .Err_Services}}open{{end}}>
<summary class="right-content tw-py-2 {{if .Err_Services}}tw-text-red{{end}}">
{{ctx.Locale.Tr "install.server_service_title"}}
</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="ui checkbox">
<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}}>
</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">
<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}}">
<span class="help">{{ctx.Locale.Tr "install.no_reply_address_helper"}}</span>
</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>
<!-- Admin -->
<details class="optional field">
<summary class="right-content tw-py-2{{if .Err_Admin}} tw-text-red{{end}}">
<details class="optional field" {{if .Err_Admin}}open{{end}}>
<summary class="right-content tw-py-2 {{if .Err_Admin}}tw-text-red{{end}}">
{{ctx.Locale.Tr "install.admin_title"}}
</summary>
<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}}
{{ctx.Locale.Tr "install.config_write_file_prompt" $filePath}}
</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">
<button type="submit" class="ui primary button">{{ctx.Locale.Tr "install.install_btn_confirm"}}</button>
</div>
+2 -2
View File
@@ -34,7 +34,7 @@ func TestGoGet(t *testing.T) {
<body>
go get --insecure %[1]s:%[2]s/blah/glah
</body>
</html>`, setting.Domain, setting.HTTPPort, setting.AppURL)
</html>`, setting.AppDomain, setting.HTTPPort, setting.AppURL)
assert.Equal(t, expected, resp.Body.String())
}
@@ -55,7 +55,7 @@ func TestGoGetForSSH(t *testing.T) {
<body>
go get --insecure %[1]s:%[2]s/blah/glah
</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())
}
+1
View File
@@ -109,6 +109,7 @@ INSTALL_LOCK = true
[service]
ENABLE_CAPTCHA = false
DISABLE_REGISTRATION = false
[ui.notification]
EVENT_SOURCE_UPDATE_TIME = 500ms
-18
View File
@@ -54,27 +54,9 @@ function initPreInstall() {
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 () {
if (this.checked) {
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 () {