From 26483c41c6d13b3777247c810a492226c972ca4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=B4=8A=E1=B4=8F=E1=B4=87=20=E1=B4=84=CA=9C=E1=B4=87?= =?UTF-8?q?=C9=B4?= Date: Sat, 23 May 2026 23:33:41 -0400 Subject: [PATCH] feat(web): add React sign-up page with Flamego captcha (#8291) --- cmd/gogs/internal/web/web.go | 23 +- cmd/gogs/internal/web/webapi.go | 166 ++++++++++++-- conf/locale/locale_en-US.ini | 22 +- go.mod | 4 +- go.sum | 8 +- internal/database/users.go | 10 - internal/form/user.go | 18 -- internal/route/user/auth.go | 115 +--------- templates/user/auth/activate.tmpl | 4 +- templates/user/auth/signup.tmpl | 56 ----- web/scripts/extract-locales.mjs | 20 +- web/src/components/Navbar.tsx | 6 +- web/src/components/PasswordInput.tsx | 65 ++++++ web/src/lib/loader-error.ts | 34 +++ web/src/locales/bg-BG.json | 10 +- web/src/locales/cs-CZ.json | 10 +- web/src/locales/de-DE.json | 10 +- web/src/locales/en-GB.json | 10 +- web/src/locales/en-US.json | 19 +- web/src/locales/es-ES.json | 10 +- web/src/locales/fa-IR.json | 10 +- web/src/locales/fi-FI.json | 10 +- web/src/locales/fr-FR.json | 10 +- web/src/locales/gl-ES.json | 10 +- web/src/locales/hu-HU.json | 10 +- web/src/locales/id-ID.json | 10 +- web/src/locales/it-IT.json | 10 +- web/src/locales/ja-JP.json | 10 +- web/src/locales/ko-KR.json | 10 +- web/src/locales/lv-LV.json | 10 +- web/src/locales/mn-MN.json | 10 +- web/src/locales/nl-NL.json | 10 +- web/src/locales/pl-PL.json | 10 +- web/src/locales/pt-BR.json | 10 +- web/src/locales/pt-PT.json | 10 +- web/src/locales/ro-RO.json | 10 +- web/src/locales/ru-RU.json | 10 +- web/src/locales/sk-SK.json | 10 +- web/src/locales/sr-SP.json | 10 +- web/src/locales/sv-SE.json | 10 +- web/src/locales/tr-TR.json | 10 +- web/src/locales/uk-UA.json | 10 +- web/src/locales/vi-VN.json | 10 +- web/src/locales/zh-CN.json | 10 +- web/src/locales/zh-HK.json | 10 +- web/src/locales/zh-TW.json | 10 +- web/src/pages/Landing.tsx | 2 +- web/src/pages/ResetPassword.tsx | 93 +++----- web/src/pages/ServerError.tsx | 51 +++++ web/src/pages/SignIn.tsx | 2 +- web/src/pages/SignUp.tsx | 329 +++++++++++++++++++++++++++ web/src/router.tsx | 20 +- 52 files changed, 1001 insertions(+), 376 deletions(-) delete mode 100644 templates/user/auth/signup.tmpl create mode 100644 web/src/components/PasswordInput.tsx create mode 100644 web/src/lib/loader-error.ts create mode 100644 web/src/pages/ServerError.tsx create mode 100644 web/src/pages/SignUp.tsx diff --git a/cmd/gogs/internal/web/web.go b/cmd/gogs/internal/web/web.go index d508e431e..d968cf0e8 100644 --- a/cmd/gogs/internal/web/web.go +++ b/cmd/gogs/internal/web/web.go @@ -15,10 +15,10 @@ import ( "github.com/cockroachdb/errors" "github.com/flamego/cache" + "github.com/flamego/captcha" "github.com/flamego/flamego" "github.com/go-macaron/binding" macaroncache "github.com/go-macaron/cache" - "github.com/go-macaron/captcha" "github.com/go-macaron/csrf" "github.com/go-macaron/gzip" "github.com/go-macaron/i18n" @@ -66,7 +66,6 @@ func Run(configPath string, portOverride int) error { reqSignIn := context.Toggle(&context.ToggleOptions{SignInRequired: true}) ignSignIn := context.Toggle(&context.ToggleOptions{SignInRequired: conf.Auth.RequireSigninView}) - reqSignOut := context.Toggle(&context.ToggleOptions{SignOutRequired: true}) bindIgnErr := binding.BindIgnErr @@ -87,11 +86,6 @@ func Run(configPath string, portOverride int) error { m.Get("/^:type(issues|pulls)$", reqSignIn, user.Issues) // ***** START: User ***** - m.Group("/user", func() { - m.Get("/sign_up", user.SignUp) - m.Post("/sign_up", bindIgnErr(form.Register{}), user.SignUpPost) - }, reqSignOut) - m.Group("/user/settings", func() { m.Get("", user.Settings) m.Post("", bindIgnErr(form.UpdateProfile{}), user.SettingsPost) @@ -517,8 +511,9 @@ func Run(configPath string, portOverride int) error { apiv1.RegisterRoutes(m) }, ignSignIn) - m.Any("/api/web/*", bridgeToWebAPI(webHandler)) - m.Get("/redirect", bridgeToWebAPI(webHandler)) + m.Any("/api/web/*", flamegoBridger(webHandler)) + m.Get("/redirect", flamegoBridger(webHandler)) + m.Get("/captcha/*", flamegoBridger(webHandler)) m.Any("/*", func(c *context.Context) { c.ServeWeb() }) }, session.Sessioner(session.Options{ @@ -681,6 +676,8 @@ func Run(configPath string, portOverride int) error { func newRoutingHandler() (http.Handler, error) { f := flamego.New() f.Use(flamego.Recovery()) + f.Use(flamegoInjector) + f.Use(captcha.Captchaer(captcha.Options{URLPrefix: "/captcha/"})) cacherOpts, err := parseCacheOptions(conf.Cache) if err != nil { @@ -690,6 +687,11 @@ func newRoutingHandler() (http.Handler, error) { f.Get("/redirect", getRedirect) + // The captcha middleware writes the image response itself when the request path + // matches its URLPrefix. This route just needs to exist so the request reaches + // the middleware chain. + f.Get("/captcha/image.jpeg", func() {}) + mountWebAPIRoutes(f) err = mountWebAppRoutes(f) if err != nil { @@ -794,9 +796,6 @@ func newMacaron() (*macaron.Macaron, error) { AdapterConfig: conf.Cache.Host, Interval: conf.Cache.Interval, })) - m.Use(captcha.Captchaer(captcha.Options{ - SubURL: conf.Server.Subpath, - })) m.Route("/healthcheck", http.MethodHead+","+http.MethodGet, healthCheck) return m, nil } diff --git a/cmd/gogs/internal/web/webapi.go b/cmd/gogs/internal/web/webapi.go index bc61a9cba..a02fbc205 100644 --- a/cmd/gogs/internal/web/webapi.go +++ b/cmd/gogs/internal/web/webapi.go @@ -6,16 +6,19 @@ import ( "net/http" "os" "reflect" + "regexp" "strings" "time" "github.com/cockroachdb/errors" "github.com/flamego/binding" "github.com/flamego/cache" + "github.com/flamego/captcha" "github.com/flamego/flamego" + "github.com/flamego/session" "github.com/flamego/validator" "github.com/go-macaron/i18n" - "github.com/go-macaron/session" + macaronsession "github.com/go-macaron/session" "gopkg.in/macaron.v1" log "unknwon.dev/clog/v2" @@ -35,7 +38,7 @@ type ( webAPILocaleKey struct{} ) -func bridgeToWebAPI(webHandler http.Handler) func(c *context.Context, l i18n.Locale) { +func flamegoBridger(webHandler http.Handler) func(c *context.Context, l i18n.Locale) { return func(c *context.Context, l i18n.Locale) { ctx := c.Req.Context() ctx = stdctx.WithValue(ctx, webAPIUserKey{}, c.User) @@ -46,15 +49,32 @@ func bridgeToWebAPI(webHandler http.Handler) func(c *context.Context, l i18n.Loc } } -func webAPIInjector(c flamego.Context) { +func flamegoInjector(c flamego.Context) { ctx := c.Request().Context() user, _ := ctx.Value(webAPIUserKey{}).(*database.User) - sess, _ := ctx.Value(webAPISessionKey{}).(session.Store) + sess, _ := ctx.Value(webAPISessionKey{}).(macaronsession.Store) mc, _ := ctx.Value(webAPIMacaronKey{}).(*macaron.Context) l, _ := ctx.Value(webAPILocaleKey{}).(i18n.Locale) c.Map(user, sess, mc, l) + c.MapTo(flamegoSessionAdapter{sess: sess}, (*session.Session)(nil)) } +// flamegoSessionAdapter exposes the underlying Macaron session via the Flamego +// session interface so the captcha middleware (and any future Flamego-native +// session consumer) can read/write the same session store the rest of the app +// uses. +type flamegoSessionAdapter struct { + sess macaronsession.Store +} + +func (s flamegoSessionAdapter) ID() string { return s.sess.ID() } +func (s flamegoSessionAdapter) Get(key interface{}) interface{} { return s.sess.Get(key) } +func (s flamegoSessionAdapter) Set(key, val interface{}) { _ = s.sess.Set(key, val) } +func (s flamegoSessionAdapter) SetFlash(val interface{}) { _ = s.sess.Set("_flash", val) } +func (s flamegoSessionAdapter) Delete(key interface{}) { _ = s.sess.Delete(key) } +func (s flamegoSessionAdapter) Flush() { _ = s.sess.Flush() } +func (s flamegoSessionAdapter) Encode() ([]byte, error) { return nil, nil } + func webAPIBodyLimiter(c flamego.Context) { r := c.Request().Request r.Body = http.MaxBytesReader(c.ResponseWriter(), r.Body, 4*1024) // 4 KiB @@ -73,9 +93,14 @@ var webAPIValidator = func() *validator.Validate { } return name }) + _ = v.RegisterValidation("alphadashdot", func(fl validator.FieldLevel) bool { + return !alphaDashDotInvalid.MatchString(fl.Field().String()) + }) return v }() +var alphaDashDotInvalid = regexp.MustCompile(`[^\d\w\-_\.]`) + // bindJSON binds the request body to T. On binding or validation failure it // short-circuits with a 400 carrying the standard renderBindingErrors payload, // so downstream handlers can drop the `if len(bindErrs) > 0` boilerplate and @@ -116,6 +141,9 @@ func mountWebAPIRoutes(f *flamego.Flame) { f.Group("/api/web", func() { f.Group("/user", func() { f.Get("/info", getUserInfo) + f.Combo("/sign-up"). + Get(getUserSignUp). + Post(bindJSON(userSignUpRequest{}), postUserSignUp) f.Group("/reset-password", func() { f.Combo(""). Get(getUserResetPassword). @@ -133,7 +161,7 @@ func mountWebAPIRoutes(f *flamego.Flame) { }) f.Post("/sign-out", postUserSignOut) }) - }, webAPIBodyLimiter, webAPIInjector) + }, webAPIBodyLimiter) } // fieldErrors maps JSON field names to per-field localized messages. A non-nil @@ -155,12 +183,13 @@ type bindingErrorResponse struct { // (e.g. "max" -> "form.max_size_error"). Messages are composed as // + , mirroring the legacy Macaron binding behavior. var ruleSuffixKeys = map[string]string{ - "required": "form.require_error", - "max": "form.max_size_error", - "min": "form.min_size_error", - "len": "form.size_error", - "email": "form.email_error", - "url": "form.url_error", + "required": "form.require_error", + "max": "form.max_size_error", + "min": "form.min_size_error", + "len": "form.size_error", + "email": "form.email_error", + "url": "form.url_error", + "alphadashdot": "form.alpha_dash_dot_error", } // renderBindingErrors maps binding.Errors to the response shape, looking up @@ -216,6 +245,101 @@ type getUserSignInResponse struct { LoginSources []loginSource `json:"loginSources"` } +type getUserSignUpResponse struct { + RegistrationDisabled bool `json:"registrationDisabled"` + CaptchaEnabled bool `json:"captchaEnabled"` +} + +func getUserSignUp() (statusCode int, resp *getUserSignUpResponse, err error) { + return http.StatusOK, &getUserSignUpResponse{ + RegistrationDisabled: conf.Auth.DisableRegistration, + CaptchaEnabled: conf.Auth.EnableRegistrationCaptcha, + }, nil +} + +type userSignUpRequest struct { + UserName string `json:"userName" validate:"required,alphadashdot,max=35"` + Email string `json:"email" validate:"required,email,max=254"` + Password string `json:"password" validate:"required,max=255"` + Captcha string `json:"captcha"` +} + +type userSignUpResponse struct { + EmailConfirmationRequired bool `json:"emailConfirmationRequired,omitempty"` + Email string `json:"email,omitempty"` + Hours int `json:"hours,omitempty"` +} + +func postUserSignUp(r *http.Request, mc *macaron.Context, ca cache.Cache, l i18n.Locale, cpt captcha.Captcha, req userSignUpRequest) (statusCode int, resp any, err error) { + if conf.Auth.DisableRegistration { + return http.StatusForbidden, &bindingErrorResponse{Error: l.Tr("auth.disable_register_prompt")}, nil + } + if conf.Auth.EnableRegistrationCaptcha && !cpt.ValidText(req.Captcha) { + msg := l.Tr("form.captcha_incorrect") + return http.StatusUnauthorized, &bindingErrorResponse{ + Fields: fieldErrors{"captcha": &msg}, + }, nil + } + u, err := database.Handle.Users().Create( + r.Context(), + req.UserName, + req.Email, + database.CreateUserOptions{ + Password: req.Password, + Activated: !conf.Auth.RequireEmailConfirmation, + }, + ) + if err != nil { + switch { + case database.IsErrUserAlreadyExist(err): + msg := l.Tr("form.username_been_taken") + return http.StatusUnprocessableEntity, &bindingErrorResponse{Fields: fieldErrors{"userName": &msg}}, nil + case database.IsErrEmailAlreadyUsed(err): + msg := l.Tr("form.email_been_used") + return http.StatusUnprocessableEntity, &bindingErrorResponse{Fields: fieldErrors{"email": &msg}}, nil + case database.IsErrNameNotAllowed(err): + msg := l.Tr("user.form.name_not_allowed", err.(database.ErrNameNotAllowed).Value()) + return http.StatusBadRequest, &bindingErrorResponse{Fields: fieldErrors{"userName": &msg}}, nil + default: + log.Error("postUserSignUp: create user %q: %v", req.UserName, err) + return http.StatusInternalServerError, nil, errors.Wrap(err, "create user") + } + } + log.Trace("Account created: %s", u.Name) + + if database.Handle.Users().Count(r.Context()) == 1 { + v := true + err := database.Handle.Users().Update( + r.Context(), + u.ID, + database.UpdateUserOptions{ + IsActivated: &v, + IsAdmin: &v, + }, + ) + if err != nil { + log.Error("postUserSignUp: update first user %q: %v", u.Name, err) + return http.StatusInternalServerError, nil, errors.Wrap(err, "update user") + } + } + + if conf.Auth.RequireEmailConfirmation && u.ID > 1 { + if err := email.SendActivateAccountMail(mc, database.NewMailerUser(u)); err != nil { + log.Error("postUserSignUp: send activation mail to user %q: %v", u.Name, err) + } + if err := ca.Set(r.Context(), userx.MailResendCacheKey(u.ID), 1, 180*time.Second); err != nil { + log.Error("postUserSignUp: put mail resend cache for user %q: %v", u.Name, err) + } + return http.StatusOK, &userSignUpResponse{ + EmailConfirmationRequired: true, + Email: u.Email, + Hours: conf.Auth.ActivateCodeLives / 60, + }, nil + } + + return http.StatusOK, &userSignUpResponse{}, nil +} + func getUserSignIn(r *http.Request) (statusCode int, resp *getUserSignInResponse, err error) { sources, err := database.Handle.LoginSources().List(r.Context(), database.ListLoginSourceOptions{OnlyActivated: true}) if err != nil { @@ -323,7 +447,7 @@ type userSignInResponse struct { MFA bool `json:"mfa,omitempty"` } -func postUserSignIn(r *http.Request, sess session.Store, mc *macaron.Context, l i18n.Locale, req userSignInRequest) (statusCode int, resp any, err error) { +func postUserSignIn(r *http.Request, sess session.Session, mc *macaron.Context, l i18n.Locale, req userSignInRequest) (statusCode int, resp any, err error) { u, err := database.Handle.Users().Authenticate(r.Context(), req.Username, req.Password, req.LoginSource) if err != nil { switch { @@ -341,7 +465,7 @@ func postUserSignIn(r *http.Request, sess session.Store, mc *macaron.Context, l } if database.Handle.TwoFactors().IsEnabled(r.Context(), u.ID) { - _ = sess.Set("mfaUserID", u.ID) + sess.Set("mfaUserID", u.ID) return http.StatusOK, &userSignInResponse{MFA: true}, nil } @@ -353,10 +477,10 @@ func postUserSignIn(r *http.Request, sess session.Store, mc *macaron.Context, l // clears any in-flight MFA state, and sets the login-status cookie. The // caller is responsible for navigating to a post-login destination via // /redirect?to=. -func completeSignIn(sess session.Store, mc *macaron.Context, u *database.User) { - _ = sess.Set("uid", u.ID) - _ = sess.Set("uname", u.Name) - _ = sess.Delete("mfaUserID") +func completeSignIn(sess session.Session, mc *macaron.Context, u *database.User) { + sess.Set("uid", u.ID) + sess.Set("uname", u.Name) + sess.Delete("mfaUserID") mc.SetCookie(conf.Session.CSRFCookieName, "", -1, conf.Server.Subpath) if conf.Security.EnableLoginStatusCookie { @@ -364,7 +488,7 @@ func completeSignIn(sess session.Store, mc *macaron.Context, u *database.User) { } } -func getUserMFA(sess session.Store) (statusCode int, resp any, err error) { +func getUserMFA(sess session.Session) (statusCode int, resp any, err error) { if _, ok := sess.Get("mfaUserID").(int64); !ok { return http.StatusNotFound, nil, nil } @@ -377,7 +501,7 @@ type userMFARequest struct { type userMFAResponse struct{} -func postUserMFA(r *http.Request, sess session.Store, mc *macaron.Context, ca cache.Cache, l i18n.Locale, req userMFARequest) (statusCode int, resp any, err error) { +func postUserMFA(r *http.Request, sess session.Session, mc *macaron.Context, ca cache.Cache, l i18n.Locale, req userMFARequest) (statusCode int, resp any, err error) { userID, ok := sess.Get("mfaUserID").(int64) if !ok { return http.StatusUnauthorized, &bindingErrorResponse{Error: l.Tr("auth.mfa_session_expired")}, nil @@ -428,7 +552,7 @@ type userMFARecoveryRequest struct { RecoveryCode string `json:"recoveryCode" validate:"required,len=11"` } -func postUserMFARecovery(r *http.Request, sess session.Store, mc *macaron.Context, l i18n.Locale, req userMFARecoveryRequest) (statusCode int, resp any, err error) { +func postUserMFARecovery(r *http.Request, sess session.Session, mc *macaron.Context, l i18n.Locale, req userMFARecoveryRequest) (statusCode int, resp any, err error) { userID, ok := sess.Get("mfaUserID").(int64) if !ok { return http.StatusUnauthorized, &bindingErrorResponse{Error: l.Tr("auth.mfa_session_expired")}, nil @@ -476,7 +600,7 @@ func getUserInfo(user *database.User) (statusCode int, resp *userInfo, err error nil } -func postUserSignOut(sess session.Store, mc *macaron.Context) (statusCode int, resp any, err error) { +func postUserSignOut(sess macaronsession.Store, mc *macaron.Context) (statusCode int, resp any, err error) { _ = sess.Flush() _ = sess.Destory(mc) mc.SetCookie(conf.Session.CSRFCookieName, "", -1, conf.Server.Subpath) diff --git a/conf/locale/locale_en-US.ini b/conf/locale/locale_en-US.ini index c32c18d6c..a67be812f 100644 --- a/conf/locale/locale_en-US.ini +++ b/conf/locale/locale_en-US.ini @@ -6,7 +6,7 @@ explore = Explore help = Help sign_in = Sign in sign_out = Sign out -sign_up = Sign Up +sign_up = Sign up register = Create account website = Website page = Page @@ -18,11 +18,16 @@ signed_in_as = Signed in as username = Username username_placeholder = Enter your username or email +new_username_placeholder = Choose a username email = Email +email_placeholder = Enter your email password = Password password_placeholder = Enter your password -re_type = Re-Type captcha = Captcha +captcha_placeholder = Enter the characters shown above +captcha_image_alt = Captcha image +refresh_captcha = Refresh captcha +click_to_refresh_captcha = Click to refresh repository = Repository organization = Organization @@ -51,7 +56,7 @@ cancel = Cancel [status] page_not_found = Page not found -internal_server_error = Internal Server Error +internal_server_error = Internal server error [install] install = Installation @@ -122,7 +127,7 @@ admin_setting_desc = You don't need to create an admin account right now. The fi admin_title = Admin Account Settings admin_name = Username admin_password = Password -confirm_password = Confirm Password +confirm_password = Confirm password admin_email = Admin Email install_gogs = Install Gogs test_git_failed = Failed to test 'git' command: %v @@ -157,7 +162,9 @@ organizations = Organizations search = Search [auth] -create_new_account = Create New Account +create_new_account = Create new account +sign_up_submitting = Creating account... +sign_up_failed = Could not create account, please try again. sign_in_submitting = Signing in... sign_in_failed = Could not sign in, please try again. show_password = Show password @@ -186,7 +193,7 @@ local = Local forgot_password= Forgot Password forget_password = Forgot password? sign_up_now = Create a new account -confirmation_mail_sent_prompt = A new confirmation email has been sent to %s, please check your inbox within the next %d hours to complete the registration process. +confirmation_email_sent = A new confirmation email has been sent to %s, please check your inbox within the next %d hours to complete the registration process. active_your_account = Activate Your Account prohibit_login = Login Prohibited prohibit_login_desc = Your account is prohibited from logging in. Please contact the site admin. @@ -205,9 +212,10 @@ reset_password_resend_limited = You already requested a password reset email rec reset_password_failed = Could not reset password, please try again. new_password = New password new_password_placeholder = Enter your new password +confirm_password_placeholder = Re-enter your password confirm_new_password = Confirm new password confirm_new_password_placeholder = Re-enter your new password -reset_password_mismatch = The two passwords do not match. +password_mismatch = The two passwords do not match. non_local_account = Non-local accounts cannot change passwords through Gogs. [mail] diff --git a/go.mod b/go.mod index aa7ae89c0..ea77ac94c 100644 --- a/go.mod +++ b/go.mod @@ -11,14 +11,15 @@ require ( github.com/fatih/color v1.18.0 github.com/flamego/binding v1.3.0 github.com/flamego/cache v1.5.1 + github.com/flamego/captcha v1.3.0 github.com/flamego/flamego v1.12.0 + github.com/flamego/session v1.3.0 github.com/flamego/validator v1.0.0 github.com/glebarez/go-sqlite v1.21.2 github.com/glebarez/sqlite v1.11.0 github.com/go-ldap/ldap/v3 v3.4.12 github.com/go-macaron/binding v1.2.0 github.com/go-macaron/cache v0.0.0-20190810181446-10f7c57e2196 - github.com/go-macaron/captcha v0.2.0 github.com/go-macaron/csrf v0.0.0-20190812063352-946f6d303a4c github.com/go-macaron/gzip v0.0.0-20160222043647-cad1c6580a07 github.com/go-macaron/i18n v0.6.0 @@ -98,6 +99,7 @@ require ( github.com/go-redis/redis/v8 v8.11.5 // indirect github.com/go-sql-driver/mysql v1.8.1 // indirect github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect github.com/google/go-querystring v1.0.0 // indirect github.com/gorilla/css v1.0.1 // indirect github.com/itchyny/gojq v0.12.11 // indirect diff --git a/go.sum b/go.sum index f5a39a7ec..6e16fc1fc 100644 --- a/go.sum +++ b/go.sum @@ -110,8 +110,12 @@ github.com/flamego/binding v1.3.0 h1:CPbnSuP0SxT50JR7lK2khTjcQi1oOECqRK7kbOYw91U github.com/flamego/binding v1.3.0/go.mod h1:xgm6FEpEKKkF8CQilK2X3MJ5kTjOTnYdz/ooFctDTdc= github.com/flamego/cache v1.5.1 h1:2B4QhLFV7je0oUMCVKsAGAT+OyDHlXhozOoUffm+O3s= github.com/flamego/cache v1.5.1/go.mod h1:cTWYm/Ls35KKHo8vwcKgTlJUNXswEhzFWqVCTFzj24s= +github.com/flamego/captcha v1.3.0 h1:CyQivqkiO4zT0nJY2vO0ySdOi85Z7EyESGMXvNQmi5U= +github.com/flamego/captcha v1.3.0/go.mod h1:fCjE5o1cJXQkVJ2aYk7ISIBohfbNy1WxI2A3Ervzyp8= github.com/flamego/flamego v1.12.0 h1:BS0iY6RytweVvu5j40fQJ53X2ZcUVeuQ8ZSigVkDB9A= github.com/flamego/flamego v1.12.0/go.mod h1:MM4kNGS7SvJtwUZYb2oGySR+ncdtIvtJHsl8OhH1Ngo= +github.com/flamego/session v1.3.0 h1:mj+fyNnJeM9aNXx2CGKppH5VFFUVHNEkhjObJIVH9hY= +github.com/flamego/session v1.3.0/go.mod h1:x4oNtRuWDnaA2uRylTm3kShbCI3lTWM+dUHuJyeeiZE= github.com/flamego/validator v1.0.0 h1:ixuWHVgiVGp4pVGtUn/0d6HBjZJbbXfJHDNkxW+rZoY= github.com/flamego/validator v1.0.0/go.mod h1:POYn0/5iW4sdamdPAYPrzqN6DFC4YaczY0gYY+Pyx5E= github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE= @@ -144,8 +148,6 @@ github.com/go-macaron/binding v1.2.0 h1:/A8x8ZVQNTzFO43ch8czTqhc4VzOEPXYU/ELjIyh github.com/go-macaron/binding v1.2.0/go.mod h1:8pXMCyR9UPsXV02PYGLI+t2Xep/v2OgVuuLTNtCG03c= github.com/go-macaron/cache v0.0.0-20190810181446-10f7c57e2196 h1:fqWZxyMLF6RVGmjvsZ9FijiU9UlAjuE6nu9RfNBZ+iE= github.com/go-macaron/cache v0.0.0-20190810181446-10f7c57e2196/go.mod h1:O6fSdaYZbGh4clVMGMGO5k2KbMO0Cz8YdBnPrD0I8dM= -github.com/go-macaron/captcha v0.2.0 h1:d38eYDDF8tdqoM0hJbk+Jb7WQGWlwYNnQwRqLRmSk1Y= -github.com/go-macaron/captcha v0.2.0/go.mod h1:lmhlZnu9cTRGNQEkSh1qZi2IK3HJH4Z1MXkg6ARQKZA= github.com/go-macaron/csrf v0.0.0-20190812063352-946f6d303a4c h1:kFFz1OpaH3+efG7RA33z+D0piwpA/a3x/Zn2d8z9rfw= github.com/go-macaron/csrf v0.0.0-20190812063352-946f6d303a4c/go.mod h1:FX53Xq0NNlUj0E5in5J8Dq5nrbdK3ZyDIy6y5VWOiUo= github.com/go-macaron/gzip v0.0.0-20160222043647-cad1c6580a07 h1:YSIA98PevNf1NtCa/J6cz7gjzpz99WVAOa9Eg0klKps= @@ -184,6 +186,8 @@ github.com/gogs/go-libravatar v0.0.0-20191106065024-33a75213d0a0 h1:K02vod+sn3M1 github.com/gogs/go-libravatar v0.0.0-20191106065024-33a75213d0a0/go.mod h1:Zas3BtO88pk1cwUfEYlvnl/CRwh0ybDxRWSwRjG8I3w= github.com/gogs/minwinsvc v0.0.0-20170301035411-95be6356811a h1:8DZwxETOVWIinYxDK+i6L+rMb7eGATGaakD6ZucfHVk= github.com/gogs/minwinsvc v0.0.0-20170301035411-95be6356811a/go.mod h1:TUIZ+29jodWQ8Gk6Pvtg4E09aMsc3C/VLZiVYfUhWQU= +github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g= +github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= diff --git a/internal/database/users.go b/internal/database/users.go index e9728c0b7..b283a4091 100644 --- a/internal/database/users.go +++ b/internal/database/users.go @@ -5,7 +5,6 @@ import ( "database/sql" "fmt" "os" - "regexp" "strings" "time" "unicode/utf8" @@ -51,10 +50,6 @@ func (err ErrLoginSourceMismatch) Error() string { return fmt.Sprintf("login source mismatch: %v", err.args) } -// disallowedUsernameChars matches any character not allowed in a username: -// anything outside ASCII letters, digits, underscore, hyphen, or dot. -var disallowedUsernameChars = regexp.MustCompile(`[^\d\w-_\.]`) - // Authenticate validates username and password via given login source ID. It // returns ErrUserNotExist when the user was not found. // @@ -132,11 +127,6 @@ func (s *UsersStore) Authenticate(ctx context.Context, login, password string, l return user, nil } - // Validate username make sure it satisfies requirement. - if disallowedUsernameChars.MatchString(extAccount.Name) { - return nil, errors.Newf("invalid pattern for attribute 'username' [%s]: must be valid alpha or numeric or dash(-_) or dot characters", extAccount.Name) - } - return s.Create(ctx, extAccount.Name, extAccount.Email, CreateUserOptions{ FullName: extAccount.FullName, diff --git a/internal/form/user.go b/internal/form/user.go index 0c046d234..7781cbbb5 100644 --- a/internal/form/user.go +++ b/internal/form/user.go @@ -54,24 +54,6 @@ func (f *Install) Validate(ctx *macaron.Context, errs binding.Errors) binding.Er return validate(errs, ctx.Data, f, ctx.Locale) } -// _____ ____ _________________ ___ -// / _ \ | | \__ ___/ | \ -// / /_\ \| | / | | / ~ \ -// / | \ | / | | \ Y / -// \____|__ /______/ |____| \___|_ / -// \/ \/ - -type Register struct { - UserName string `binding:"Required;AlphaDashDot;MaxSize(35)"` - Email string `binding:"Required;Email;MaxSize(254)"` - Password string `binding:"Required;MaxSize(255)"` - Retype string -} - -func (f *Register) Validate(ctx *macaron.Context, errs binding.Errors) binding.Errors { - return validate(errs, ctx.Data, f, ctx.Locale) -} - // __________________________________________.___ _______ ________ _________ // / _____/\_ _____/\__ ___/\__ ___/| |\ \ / _____/ / _____/ // \_____ \ | __)_ | | | | | |/ | \/ \ ___ \_____ \ diff --git a/internal/route/user/auth.go b/internal/route/user/auth.go index c16322aec..2ecc0b208 100644 --- a/internal/route/user/auth.go +++ b/internal/route/user/auth.go @@ -3,25 +3,19 @@ package user import ( gocontext "context" "encoding/hex" - "net/http" "strconv" - "github.com/go-macaron/captcha" log "unknwon.dev/clog/v2" "gogs.io/gogs/internal/conf" "gogs.io/gogs/internal/context" "gogs.io/gogs/internal/database" "gogs.io/gogs/internal/email" - "gogs.io/gogs/internal/form" "gogs.io/gogs/internal/tool" "gogs.io/gogs/internal/userx" ) -const ( - tmplUserAuthSignup = "user/auth/signup" - TmplUserAuthActivate = "user/auth/activate" -) +const TmplUserAuthActivate = "user/auth/activate" func SignOut(c *context.Context) { _ = c.Session.Flush() @@ -34,113 +28,6 @@ func SignOut(c *context.Context) { c.RedirectSubpath("/") } -func SignUp(c *context.Context) { - c.Title("sign_up") - - c.Data["EnableCaptcha"] = conf.Auth.EnableRegistrationCaptcha - - if conf.Auth.DisableRegistration { - c.Data["DisableRegistration"] = true - c.Success(tmplUserAuthSignup) - return - } - - c.Success(tmplUserAuthSignup) -} - -func SignUpPost(c *context.Context, cpt *captcha.Captcha, f form.Register) { - c.Title("sign_up") - - c.Data["EnableCaptcha"] = conf.Auth.EnableRegistrationCaptcha - - if conf.Auth.DisableRegistration { - c.Status(http.StatusForbidden) - return - } - - if c.HasError() { - c.HTML(http.StatusBadRequest, tmplUserAuthSignup) - return - } - - if conf.Auth.EnableRegistrationCaptcha && !cpt.VerifyReq(c.Req) { - c.FormErr("Captcha") - c.RenderWithErr(c.Tr("form.captcha_incorrect"), http.StatusUnauthorized, tmplUserAuthSignup, &f) - return - } - - if f.Password != f.Retype { - c.FormErr("Password") - c.RenderWithErr(c.Tr("form.password_not_match"), http.StatusBadRequest, tmplUserAuthSignup, &f) - return - } - - user, err := database.Handle.Users().Create( - c.Req.Context(), - f.UserName, - f.Email, - database.CreateUserOptions{ - Password: f.Password, - Activated: !conf.Auth.RequireEmailConfirmation, - }, - ) - if err != nil { - switch { - case database.IsErrUserAlreadyExist(err): - c.FormErr("UserName") - c.RenderWithErr(c.Tr("form.username_been_taken"), http.StatusUnprocessableEntity, tmplUserAuthSignup, &f) - case database.IsErrEmailAlreadyUsed(err): - c.FormErr("Email") - c.RenderWithErr(c.Tr("form.email_been_used"), http.StatusUnprocessableEntity, tmplUserAuthSignup, &f) - case database.IsErrNameNotAllowed(err): - c.FormErr("UserName") - c.RenderWithErr(c.Tr("user.form.name_not_allowed", err.(database.ErrNameNotAllowed).Value()), http.StatusBadRequest, tmplUserAuthSignup, &f) - default: - c.Error(err, "create user") - } - return - } - log.Trace("Account created: %s", user.Name) - - // FIXME: Count has pretty bad performance implication in large instances, we - // should have a dedicate method to check whether the "user" table is empty. - // - // Auto-set admin for the only user. - if database.Handle.Users().Count(c.Req.Context()) == 1 { - v := true - err := database.Handle.Users().Update( - c.Req.Context(), - user.ID, - database.UpdateUserOptions{ - IsActivated: &v, - IsAdmin: &v, - }, - ) - if err != nil { - c.Error(err, "update user") - return - } - } - - // Send confirmation email. - if conf.Auth.RequireEmailConfirmation && user.ID > 1 { - if err := email.SendActivateAccountMail(c.Context, database.NewMailerUser(user)); err != nil { - log.Error("Failed to send activate account mail: %v", err) - } - c.Data["IsSendRegisterMail"] = true - c.Data["Email"] = user.Email - c.Data["Hours"] = conf.Auth.ActivateCodeLives / 60 - c.Success(TmplUserAuthActivate) - - if err := c.Cache.Put(userx.MailResendCacheKey(user.ID), 1, 180); err != nil { - log.Error("Failed to put cache key 'mail resend': %v", err) - } - return - } - - c.RedirectSubpath("/user/sign-in") -} - // parseUserFromCode returns user by username encoded in code. // It returns nil if code or username is invalid. func parseUserFromCode(code string) (user *database.User) { diff --git a/templates/user/auth/activate.tmpl b/templates/user/auth/activate.tmpl index efcce9841..e44bb4ea7 100644 --- a/templates/user/auth/activate.tmpl +++ b/templates/user/auth/activate.tmpl @@ -15,11 +15,11 @@ {{else if .ResendLimited}}

{{.i18n.Tr "auth.resent_limit_prompt"}}

{{else}} -

{{.i18n.Tr "auth.confirmation_mail_sent_prompt" .LoggedUser.Email .Hours | Str2HTML}}

+

{{.i18n.Tr "auth.confirmation_email_sent" .LoggedUser.Email .Hours | Str2HTML}}

{{end}} {{else}} {{if .IsSendRegisterMail}} -

{{.i18n.Tr "auth.confirmation_mail_sent_prompt" .Email .Hours | Str2HTML}}

+

{{.i18n.Tr "auth.confirmation_email_sent" .Email .Hours | Str2HTML}}

{{else if .IsActivateFailed}}

{{.i18n.Tr "auth.invalid_code"}}

{{else}} diff --git a/templates/user/auth/signup.tmpl b/templates/user/auth/signup.tmpl deleted file mode 100644 index d8e956a00..000000000 --- a/templates/user/auth/signup.tmpl +++ /dev/null @@ -1,56 +0,0 @@ -{{template "base/head" .}} - -{{template "base/footer" .}} diff --git a/web/scripts/extract-locales.mjs b/web/scripts/extract-locales.mjs index b88b64585..b9a84e692 100644 --- a/web/scripts/extract-locales.mjs +++ b/web/scripts/extract-locales.mjs @@ -35,27 +35,41 @@ const REUSED_KEYS = [ "settings", "language", "page_not_found", + "internal_server_error", "theme", "theme_light", "theme_dark", "theme_system", "username", "username_placeholder", + "new_username_placeholder", "email", + "email_placeholder", "password", "password_placeholder", + "captcha", + "captcha_placeholder", + "captcha_image_alt", + "refresh_captcha", + "click_to_refresh_captcha", "auth_source", "local", - "remember_me", "forget_password", "send_reset_email", "reset_password_email_submitting", "reset_password_email_failed", "reset_password_email_sent", "disable_register_mail", + "disable_register_prompt", "reset_password_resend_limited", "non_local_account", + "confirmation_email_sent", + "create_new_account", + "register_hepler_msg", + "sign_up", "sign_up_now", + "sign_up_submitting", + "sign_up_failed", "sign_in_submitting", "sign_in_failed", "show_password", @@ -68,9 +82,11 @@ const REUSED_KEYS = [ "reset_password_failed", "new_password", "new_password_placeholder", + "confirm_password", + "confirm_password_placeholder", "confirm_new_password", "confirm_new_password_placeholder", - "reset_password_mismatch", + "password_mismatch", "mfa_title", "mfa_passcode", "mfa_passcode_placeholder", diff --git a/web/src/components/Navbar.tsx b/web/src/components/Navbar.tsx index 3c777ad03..c43b61ab3 100644 --- a/web/src/components/Navbar.tsx +++ b/web/src/components/Navbar.tsx @@ -65,7 +65,9 @@ export function Navbar() { {t("sign_in")} - {t("register")} + + {t("register")} + )} @@ -153,7 +155,7 @@ export function Navbar() { setOpen(false)}> {t("sign_in")} - setOpen(false)}> + setOpen(false)}> {t("register")} diff --git a/web/src/components/PasswordInput.tsx b/web/src/components/PasswordInput.tsx new file mode 100644 index 000000000..140d38a73 --- /dev/null +++ b/web/src/components/PasswordInput.tsx @@ -0,0 +1,65 @@ +import { Eye, EyeOff } from "lucide-react"; +import { useTranslation } from "react-i18next"; + +import { Input } from "@/components/ui/input"; + +export function PasswordInput({ + inputRef, + id, + value, + tabIndex, + placeholder, + show, + onToggleShow, + disabled, + describedBy, + invalid, + autoFocus, + onChange, +}: { + inputRef: React.RefObject; + id: string; + value: string; + tabIndex: number; + placeholder: string; + show: boolean; + onToggleShow: () => void; + disabled: boolean; + describedBy?: string; + invalid: boolean; + autoFocus?: boolean; + onChange: (value: string) => void; +}) { + const { t } = useTranslation(); + return ( +
+ onChange(e.target.value)} + aria-invalid={invalid ? true : undefined} + aria-describedby={describedBy} + className="pr-10" + /> + +
+ ); +} diff --git a/web/src/lib/loader-error.ts b/web/src/lib/loader-error.ts new file mode 100644 index 000000000..abce7f635 --- /dev/null +++ b/web/src/lib/loader-error.ts @@ -0,0 +1,34 @@ +// LoaderResponseError carries enough of a failed loader fetch for the route +// error component to render a useful message: the HTTP status, the parsed +// `error` field when the body is JSON-shaped like the webapi 4xx/5xx +// responses, and the raw body as a fallback for non-JSON responses (e.g. a +// reverse proxy error page). +export class LoaderResponseError extends Error { + status: number; + body: string; + errorField: string | null; + + constructor(status: number, body: string, errorField: string | null) { + super(errorField ?? `HTTP ${status}`); + this.name = "LoaderResponseError"; + this.status = status; + this.body = body; + this.errorField = errorField; + } +} + +export async function loaderResponseError(res: Response): Promise { + const body = await res.text().catch(() => ""); + let errorField: string | null = null; + if (body) { + try { + const parsed = JSON.parse(body) as { error?: unknown }; + if (typeof parsed.error === "string" && parsed.error) { + errorField = parsed.error; + } + } catch { + // Body is not JSON; fall back to raw body in ServerError. + } + } + return new LoaderResponseError(res.status, body, errorField); +} diff --git a/web/src/locales/bg-BG.json b/web/src/locales/bg-BG.json index 4d55ae587..b92307b88 100644 --- a/web/src/locales/bg-BG.json +++ b/web/src/locales/bg-BG.json @@ -21,17 +21,23 @@ "settings": "Настройки", "language": "Език", "page_not_found": "Страницата не е намерена", + "internal_server_error": "Вътрешна грешка в сървър", "username": "Потребител", "email": "Ел. поща", "password": "Парола", + "captcha": "Captcha", "auth_source": "Източник за удостоверяване", "local": "Локален", - "remember_me": "Запомни ме", "forget_password": "Забравена парола?", "disable_register_mail": "За съжаление потвърждението на регистрации е изключено.", + "disable_register_prompt": "За съжаление създаването на нови регистрации е изключено. Обърнете се към администратора на сайта.", "non_local_account": "Нелокални потребители не могат да сменят паролата си през Gogs.", + "create_new_account": "Създай нов профил", + "register_hepler_msg": "Вече имате профил? Впишете се сега!", + "sign_up": "Регистрирайте се", "sign_up_now": "Нуждаете се от профил? Регистрирайте се сега.", "reset_password": "Нулиране на паролата", "invalid_code": "За съжаление Вашия код за потвърждение е изтекъл или е невалиден.", - "new_password": "Нова парола" + "new_password": "Нова парола", + "confirm_password": "Потвърждение на паролата" } diff --git a/web/src/locales/cs-CZ.json b/web/src/locales/cs-CZ.json index 6115aae72..9efb5aeb9 100644 --- a/web/src/locales/cs-CZ.json +++ b/web/src/locales/cs-CZ.json @@ -21,17 +21,23 @@ "settings": "Nastavení", "language": "Jazyk", "page_not_found": "Page Not Found", + "internal_server_error": "Internal Server Error", "username": "Uživatelské jméno", "email": "E-mail", "password": "Heslo", + "captcha": "CAPTCHA", "auth_source": "Zdroj ověření", "local": "Lokální", - "remember_me": "Zapamatovat si mne", "forget_password": "Zapomněli jste heslo?", "disable_register_mail": "Omlouváme se, ale e-mailové služby jsou vypnuté. Kontaktujte správce systému.", + "disable_register_prompt": "Omlouváme se, ale registrace jsou vypnuty. Kontaktujte správce systému.", "non_local_account": "Externí účty nemohou měnit hesla přes Gogs.", + "create_new_account": "Vytvořit nový účet", + "register_hepler_msg": "Již máte účet? Přihlašte se!", + "sign_up": "Registrovat se", "sign_up_now": "Potřebujete účet? Zaregistrujte se.", "reset_password": "Obnova vašeho hesla", "invalid_code": "Omlouváme se, ale kód z vašeho potvrzovacího e-mailu už vypršel nebo není správný.", - "new_password": "Nové heslo" + "new_password": "Nové heslo", + "confirm_password": "Potvrdit heslo" } diff --git a/web/src/locales/de-DE.json b/web/src/locales/de-DE.json index ba6620373..79145062b 100644 --- a/web/src/locales/de-DE.json +++ b/web/src/locales/de-DE.json @@ -21,17 +21,23 @@ "settings": "Einstellungen", "language": "Sprache", "page_not_found": "Seite nicht gefunden", + "internal_server_error": "Interner Serverfehler", "username": "Benutzername", "email": "E-Mail", "password": "Passwort", + "captcha": "Captcha", "auth_source": "Authentifizierungsquelle", "local": "Lokal", - "remember_me": "Angemeldet bleiben", "forget_password": "Passwort vergessen?", "disable_register_mail": "Es tut uns leid, die Bestätigung der Registrierungs-E-Mail wurde deaktiviert.", + "disable_register_prompt": "Es tut uns leid, die Registrierung wurde deaktiviert. Bitte wenden Sie sich an den Administrator.", "non_local_account": "Nicht-lokale Konten können Passwörter nicht via Gogs ändern.", + "create_new_account": "Neues Konto erstellen", + "register_hepler_msg": "Haben Sie bereits ein Konto? Jetzt anmelden!", + "sign_up": "Registrieren", "sign_up_now": "Benötigen Sie ein Konto? Registrieren Sie sich jetzt.", "reset_password": "Passwort zurücksetzen", "invalid_code": "Es tut uns leid, der Bestätigungscode ist abgelaufen oder ungültig.", - "new_password": "Neues Passwort" + "new_password": "Neues Passwort", + "confirm_password": "Passwort bestätigen" } diff --git a/web/src/locales/en-GB.json b/web/src/locales/en-GB.json index c8b64c55b..78ed3cca3 100644 --- a/web/src/locales/en-GB.json +++ b/web/src/locales/en-GB.json @@ -21,17 +21,23 @@ "settings": "Settings", "language": "Language", "page_not_found": "Page Not Found", + "internal_server_error": "Internal Server Error", "username": "Username", "email": "Email", "password": "Password", + "captcha": "Captcha", "auth_source": "Authentication Source", "local": "Local", - "remember_me": "Remember Me", "forget_password": "Forgot password?", "disable_register_mail": "Sorry, Register Mail Confirmation has been disabled.", + "disable_register_prompt": "Sorry, registration has been disabled. Please contact the site administrator.", "non_local_account": "Non-local accounts cannot change passwords through Gogs.", + "create_new_account": "Create New Account", + "register_hepler_msg": "Already have an account? Sign in now!", + "sign_up": "Sign Up", "sign_up_now": "Need an account? Sign up now.", "reset_password": "Reset Your Password", "invalid_code": "Sorry, your confirmation code has expired or not valid.", - "new_password": "New Password" + "new_password": "New Password", + "confirm_password": "Confirm Password" } diff --git a/web/src/locales/en-US.json b/web/src/locales/en-US.json index 5b29221d4..677952fe8 100644 --- a/web/src/locales/en-US.json +++ b/web/src/locales/en-US.json @@ -21,15 +21,23 @@ "settings": "Settings", "language": "Language", "page_not_found": "Page not found", + "internal_server_error": "Internal server error", "theme": "Theme", "theme_light": "Light", "theme_dark": "Dark", "theme_system": "System", "username": "Username", "username_placeholder": "Enter your username or email", + "new_username_placeholder": "Choose a username", "email": "Email", + "email_placeholder": "Enter your email", "password": "Password", "password_placeholder": "Enter your password", + "captcha": "Captcha", + "captcha_placeholder": "Enter the characters shown above", + "captcha_image_alt": "Captcha image", + "refresh_captcha": "Refresh captcha", + "click_to_refresh_captcha": "Click to refresh", "auth_source": "Authentication source", "local": "Local", "forget_password": "Forgot password?", @@ -38,9 +46,16 @@ "reset_password_email_failed": "Could not send password reset email, please try again.", "reset_password_email_sent": "A password reset email has been sent to {email}, please check your inbox within {hours} hours.", "disable_register_mail": "Sorry, email services are disabled. Please contact the site administrator.", + "disable_register_prompt": "Sorry, registration has been disabled. Please contact the site administrator.", "reset_password_resend_limited": "You already requested a password reset email recently. Please wait 3 minutes then try again.", "non_local_account": "Non-local accounts cannot change passwords through Gogs.", + "confirmation_email_sent": "A new confirmation email has been sent to %s, please check your inbox within the next %d hours to complete the registration process.", + "create_new_account": "Create new account", + "register_hepler_msg": "Already have an account? Sign in now!", + "sign_up": "Sign up", "sign_up_now": "Create a new account", + "sign_up_submitting": "Creating account...", + "sign_up_failed": "Could not create account, please try again.", "sign_in_submitting": "Signing in...", "sign_in_failed": "Could not sign in, please try again.", "show_password": "Show password", @@ -53,9 +68,11 @@ "reset_password_failed": "Could not reset password, please try again.", "new_password": "New password", "new_password_placeholder": "Enter your new password", + "confirm_password": "Confirm password", + "confirm_password_placeholder": "Re-enter your password", "confirm_new_password": "Confirm new password", "confirm_new_password_placeholder": "Re-enter your new password", - "reset_password_mismatch": "The two passwords do not match.", + "password_mismatch": "The two passwords do not match.", "mfa_title": "Multi-factor authentication", "mfa_passcode": "Passcode", "mfa_passcode_placeholder": "Enter the 6-digit code from your authenticator", diff --git a/web/src/locales/es-ES.json b/web/src/locales/es-ES.json index edae062b7..c0a8cc4ea 100644 --- a/web/src/locales/es-ES.json +++ b/web/src/locales/es-ES.json @@ -21,17 +21,23 @@ "settings": "Configuraciones", "language": "Idioma", "page_not_found": "Página no encontrada", + "internal_server_error": "Error Interno del Servidor", "username": "Nombre de usuario", "email": "Correo electrónico", "password": "Contraseña", + "captcha": "Captcha", "auth_source": "Authentication Source", "local": "Local", - "remember_me": "Recuérdame", "forget_password": "¿Has olvidado tu contraseña?", "disable_register_mail": "Lo sentimos. Los correos de Confirmación de Registro están deshabilitados.", + "disable_register_prompt": "Lo sentimos, el registro está deshabilitado. Por favor, contacta con el administrador del sitio.", "non_local_account": "Cuentas que no son locales no pueden cambiar las contraseñas a través de Gogs.", + "create_new_account": "Crear una nueva cuenta", + "register_hepler_msg": "¿Ya tienes una cuenta? ¡Inicia sesión!", + "sign_up": "Registro", "sign_up_now": "¿Necesitas una cuenta? Regístrate ahora.", "reset_password": "Restablecer su contraseña", "invalid_code": "Lo sentimos, su código de confirmación ha expirado o no es valido.", - "new_password": "Nueva contraseña" + "new_password": "Nueva contraseña", + "confirm_password": "Confirmar Contraseña" } diff --git a/web/src/locales/fa-IR.json b/web/src/locales/fa-IR.json index f480a5a17..5fbdec8be 100644 --- a/web/src/locales/fa-IR.json +++ b/web/src/locales/fa-IR.json @@ -21,17 +21,23 @@ "settings": "تنظيمات", "language": "زبان", "page_not_found": "صفحه مورد نظر یافت نشد.", + "internal_server_error": "خطای داخلی سرور", "username": "نام کاربری", "email": "ایمیل", "password": "رمز عبور", + "captcha": "تصویر امنیتی", "auth_source": "محل احراز هویت", "local": "محلی", - "remember_me": "مرا به خاطر بسپار", "forget_password": "رمز عبور خود را فراموش کرده‌اید؟", "disable_register_mail": "با عرض پوزش، تایید ایمیل ثبت نام غیر فعال شده است.", + "disable_register_prompt": "با عرض پوزش، ثبت نام غیرفعال شده است. لطفا با مدیر سایت تماس بگیرید.", "non_local_account": "حساب های کاربری غیر محلی قادر به تغییر رمز عبور از طریق Gogs نمی باشند.", + "create_new_account": "ایجاد حساب جدید", + "register_hepler_msg": "قبلا ثبت نام کردید؟ از اینجا وارد شوید!", + "sign_up": "ثبت‌نام کنید", "sign_up_now": "نیاز به یک حساب دارید؟ هم‌اکنون ثبت نام کنید.", "reset_password": "تنظیم مجدد رمز عبور", "invalid_code": "با عرض پوزش، کد تایید شما منقضی شده است و یا معتبر نیست.", - "new_password": "رمز عبور جدید" + "new_password": "رمز عبور جدید", + "confirm_password": "تأیید رمز عبور" } diff --git a/web/src/locales/fi-FI.json b/web/src/locales/fi-FI.json index 38390be19..6021fd956 100644 --- a/web/src/locales/fi-FI.json +++ b/web/src/locales/fi-FI.json @@ -21,17 +21,23 @@ "settings": "Asetukset", "language": "Kieli", "page_not_found": "Sivua ei löydy", + "internal_server_error": "Sisäinen palvelinvirhe", "username": "Käyttäjätunnus", "email": "Sähköposti", "password": "Salasana", + "captcha": "Captcha", "auth_source": "Todennuslähde", "local": "Paikallinen", - "remember_me": "Muista minut", "forget_password": "Unohtuiko salasana?", "disable_register_mail": "Valitettavasti sähköpostipalvelut ovat poissa käytöstä. Otathan yhteyttä sivuston ylläpitoon.", + "disable_register_prompt": "Valitettavasti rekisteröinti on poistettu käytöstä. Ole hyvä ja ota yhteyttä sivuston ylläpitoon.", "non_local_account": "Vain paikallisten käyttäjätilien salasanan vaihto onnistuu Gogsin kautta.", + "create_new_account": "Luo uusi tili", + "register_hepler_msg": "Onko sinulla jo tili? Kirjaudu sisään nyt!", + "sign_up": "Rekisteröidy", "sign_up_now": "Tarvitsetko tilin? Rekisteröidy nyt.", "reset_password": "Nollaa salasanasi", "invalid_code": "Sori, varmistuskoodisi on vanhentunut tai väärä.", - "new_password": "Uusi salasana" + "new_password": "Uusi salasana", + "confirm_password": "Varmista salasana" } diff --git a/web/src/locales/fr-FR.json b/web/src/locales/fr-FR.json index 78809fa9b..e4e2e6812 100644 --- a/web/src/locales/fr-FR.json +++ b/web/src/locales/fr-FR.json @@ -21,17 +21,23 @@ "settings": "Paramètres", "language": "Langue", "page_not_found": "Page non trouvée", + "internal_server_error": "Erreur interne du serveur", "username": "Nom d'utilisateur", "email": "E-mail", "password": "Mot de passe", + "captcha": "Captcha", "auth_source": "Sources d'authentification", "local": "Locale", - "remember_me": "Se souvenir de moi", "forget_password": "Mot de passe oublié ?", "disable_register_mail": "Désolé, la confirmation par courriel des enregistrements a été désactivée.", + "disable_register_prompt": "Désolé, les enregistrements ont été désactivés. Veuillez contacter l'administrateur du site.", "non_local_account": "Les comptes non locaux ne peuvent pas changer leur mot de passe via Gogs.", + "create_new_account": "Créer un nouveau compte", + "register_hepler_msg": "Déjà enregistré ? Connectez-vous !", + "sign_up": "Inscription", "sign_up_now": "Pas de compte ? Inscrivez-vous maintenant.", "reset_password": "Réinitialiser le mot de passe", "invalid_code": "Désolé, votre code de confirmation est invalide ou a expiré.", - "new_password": "Nouveau mot de passe" + "new_password": "Nouveau mot de passe", + "confirm_password": "Confirmez le mot de passe" } diff --git a/web/src/locales/gl-ES.json b/web/src/locales/gl-ES.json index 1829d194a..21c7939a2 100644 --- a/web/src/locales/gl-ES.json +++ b/web/src/locales/gl-ES.json @@ -21,17 +21,23 @@ "settings": "Configuracións", "language": "Idioma", "page_not_found": "Page Not Found", + "internal_server_error": "Internal Server Error", "username": "Nome da persoa usuaria", "email": "Correo electrónico", "password": "Contrasinal", + "captcha": "Captcha=Captcha", "auth_source": "Fonte de Autenticación", "local": "Configuración rexional", - "remember_me": "Recórdame", "forget_password": "Esqueciches o teu contrasinal?", "disable_register_mail": "Sentímolo. Os correos de confirmación de rexistro están deshabilitados.", + "disable_register_prompt": "Sentímolo, o rexistro está deshabilitado. Por favor, contacta co administrador do sitio.", "non_local_account": "Contas que non son locais non poden cambiar os contrasinais a través de Gogs.", + "create_new_account": "Crear unha nova conta", + "register_hepler_msg": "Xa tes unha conta? Inicia sesión!", + "sign_up": "Rexistro", "sign_up_now": "Necesitas unha conta? Rexístrate agora.", "reset_password": "Restablecer o teu contrasinal", "invalid_code": "Sentímolo, o teu código de confirmación expirou ou non é válido.", - "new_password": "Novo contrasinal" + "new_password": "Novo contrasinal", + "confirm_password": "Confirmar contrasinal" } diff --git a/web/src/locales/hu-HU.json b/web/src/locales/hu-HU.json index e112c5584..b4cb59993 100644 --- a/web/src/locales/hu-HU.json +++ b/web/src/locales/hu-HU.json @@ -21,17 +21,23 @@ "settings": "Beállítások", "language": "Nyelv", "page_not_found": "Az oldal nem található", + "internal_server_error": "Belső kiszolgálóhiba", "username": "Felhasználónév", "email": "E-mail", "password": "Jelszó", + "captcha": "Ellenőrző kód", "auth_source": "Hitelesítési forrás", "local": "Helyi", - "remember_me": "Emlékezz rám", "forget_password": "Elfelejtette a jelszavát?", "disable_register_mail": "Elnézést, az email regisztráció megerősítését kikapcsolták.", + "disable_register_prompt": "Elnézést, a regisztrációt kikapcsolták. Kérlek szólj az oldal adminisztrátorának.", "non_local_account": "Nem helyi felhasználó nem cserélhet jelszót a Gogsban.", + "create_new_account": "Új fiók létrehozása", + "register_hepler_msg": "Van már felhasználói fiókja? Jelentkezz be!", + "sign_up": "Regisztráció", "sign_up_now": "Szeretne bejelentkezni? Regisztráljon most.", "reset_password": "Jelszó visszaállítása", "invalid_code": "Elnézést, a megerősítő kód lejárt vagy hibás.", - "new_password": "Új jelszó" + "new_password": "Új jelszó", + "confirm_password": "Jelszó megerősítése" } diff --git a/web/src/locales/id-ID.json b/web/src/locales/id-ID.json index 9883e4e11..118d9d564 100644 --- a/web/src/locales/id-ID.json +++ b/web/src/locales/id-ID.json @@ -21,17 +21,23 @@ "settings": "Pengaturan", "language": "Bahasa", "page_not_found": "Halaman tidak ditemukan", + "internal_server_error": "Kesalahan Server Internal", "username": "Nama pengguna", "email": "Email", "password": "Sandi", + "captcha": "Captcha", "auth_source": "Sumber Autentikasi", "local": "Lokal", - "remember_me": "Ingat saya", "forget_password": "Lupa sandi?", "disable_register_mail": "Maaf, konfirmasi pendaftaran melalui email telah dinonaktifkan.", + "disable_register_prompt": "Maaf, pendaftaran telah dinonaktifkan. Hubungi administrator situs.", "non_local_account": "Akun non-lokal tidak dapat mengganti password lewat Gogs.", + "create_new_account": "Buat akun baru", + "register_hepler_msg": "Sudah memiliki account? Sign in sekarang!", + "sign_up": "Daftar", "sign_up_now": "Membutuhkan akun? Daftar sekarang.", "reset_password": "Atur Ulang Sandi", "invalid_code": "Maaf, kode konfirmasi Anda telah kadaluarsa atau tidak valid.", - "new_password": "Sandi baru" + "new_password": "Sandi baru", + "confirm_password": "Konfirmasi sandi" } diff --git a/web/src/locales/it-IT.json b/web/src/locales/it-IT.json index 9fecb3bac..b3ff12cff 100644 --- a/web/src/locales/it-IT.json +++ b/web/src/locales/it-IT.json @@ -21,17 +21,23 @@ "settings": "Impostazioni", "language": "Lingua", "page_not_found": "Pagina Non Trovata", + "internal_server_error": "Errore Interno del Server", "username": "Nome utente", "email": "E-mail", "password": "Password", + "captcha": "Captcha", "auth_source": "Fonte di autenticazione", "local": "Locale", - "remember_me": "Ricordami", "forget_password": "Password dimenticata?", "disable_register_mail": "Siamo spiacenti, la conferma di registrazione via Mail è stata disattivata.", + "disable_register_prompt": "Siamo spiacenti, registrazione è stata disabilitata. Si prega di contattare l'amministratore del sito.", "non_local_account": "Gli account non locali non possono modificare le password tramite Gogs.", + "create_new_account": "Crea un nuovo Account", + "register_hepler_msg": "Hai già un account? Accedi ora!", + "sign_up": "Registrati", "sign_up_now": "Bisogno di un account? Iscriviti ora.", "reset_password": "Reimposta la tua Password", "invalid_code": "Siamo spiacenti, il codice di conferma è scaduto o non valido.", - "new_password": "Nuova Password" + "new_password": "Nuova Password", + "confirm_password": "Conferma Password" } diff --git a/web/src/locales/ja-JP.json b/web/src/locales/ja-JP.json index 350ba9307..32b13f528 100644 --- a/web/src/locales/ja-JP.json +++ b/web/src/locales/ja-JP.json @@ -21,17 +21,23 @@ "settings": "設定", "language": "言語", "page_not_found": "ページが見つかりません", + "internal_server_error": "サーバ内部エラー", "username": "ユーザー名", "email": "メールアドレス", "password": "パスワード", + "captcha": "CAPTCHA", "auth_source": "認証ソース", "local": "ローカル", - "remember_me": "ログインしたままにする", "forget_password": "パスワードを忘れましたか?", "disable_register_mail": "申し訳ありませんが、登録メールの確認機能が無効になっています。", + "disable_register_prompt": "申し訳ありませんが、現在登録は受け付けておりません。サイトの管理者にお問い合わせください。", "non_local_account": "非ローカルアカウントではGogs経由でのパスワード変更はできません。", + "create_new_account": "新規アカウントを作成", + "register_hepler_msg": "既にアカウントをお持ちですか?今すぐログインしましょう!", + "sign_up": "サインアップ", "sign_up_now": "アカウントが必要ですか?今すぐ登録しましょう!", "reset_password": "パスワードリセット", "invalid_code": "申し訳ありませんが、確認用コードが期限切れまたは無効です。", - "new_password": "新しいパスワード" + "new_password": "新しいパスワード", + "confirm_password": "パスワード確認" } diff --git a/web/src/locales/ko-KR.json b/web/src/locales/ko-KR.json index 4b2119e9f..d2f995091 100644 --- a/web/src/locales/ko-KR.json +++ b/web/src/locales/ko-KR.json @@ -21,17 +21,23 @@ "settings": "설정", "language": "언어", "page_not_found": "페이지를 찾을 수 없음", + "internal_server_error": "내부 서버 오류", "username": "사용자명", "email": "이메일", "password": "비밀번호", + "captcha": "보안 문자", "auth_source": "인증 소스 편집", "local": "로컬", - "remember_me": "자동 로그인", "forget_password": "비밀번호를 잊으셨습니까?", "disable_register_mail": "죄송합니다. 메일 등록이 비활성화 되었습니다.", + "disable_register_prompt": "죄송합니다, 가입이 비활성화 되어있습니다. 사이트 관리자에게 문의 해주세요.", "non_local_account": "Gogs 계정이 아니면 암호를 변경할 수 없습니다.", + "create_new_account": "새 계정 생성", + "register_hepler_msg": "이미 계정을 가지고 계신가요? 로그인하세요!", + "sign_up": "가입하기", "sign_up_now": "계정이 필요하신가요? 지금 가입하세요.", "reset_password": "비밀번호 초기화", "invalid_code": "죄송합니다. 확인 코드가 만료되었거나 유효하지 않습니다.", - "new_password": "새 비밀번호" + "new_password": "새 비밀번호", + "confirm_password": "비밀번호 확인" } diff --git a/web/src/locales/lv-LV.json b/web/src/locales/lv-LV.json index 882fd45f7..05eb01010 100644 --- a/web/src/locales/lv-LV.json +++ b/web/src/locales/lv-LV.json @@ -21,17 +21,23 @@ "settings": "Iestatījumi", "language": "Valoda", "page_not_found": "Page Not Found", + "internal_server_error": "Internal Server Error", "username": "Lietotājvārds", "email": "E-pasts", "password": "Parole", + "captcha": "Pārbaudes kods", "auth_source": "Autentificēšanas avots", "local": "Local", - "remember_me": "Atcerēties mani", "forget_password": "Aizmirsi paroli?", "disable_register_mail": "Atvainojiet, reģistrācijas e-pasta apstiprināšana ir atspējota.", + "disable_register_prompt": "Atvainojiet, reģistrācija ir atspējota. Lūdzu, sazinieties ar vietnes administratoru.", "non_local_account": "Tikai lokālie konti var nomainīt savu paroli Gogs.", + "create_new_account": "Izveidot jaunu kontu", + "register_hepler_msg": "Jau ir konts? Pieraksties tagad!", + "sign_up": "Reģistrēties", "sign_up_now": "Nepieciešams konts? Reģistrējies tagad.", "reset_password": "Atjaunot savu paroli", "invalid_code": "Atvainojiet, Jūsu apstiprināšanas kodam ir beidzies derīguma termiņš vai arī tas ir nepareizs.", - "new_password": "Jauna parole" + "new_password": "Jauna parole", + "confirm_password": "Apstipriniet paroli" } diff --git a/web/src/locales/mn-MN.json b/web/src/locales/mn-MN.json index f16f9e65b..698c73a5a 100644 --- a/web/src/locales/mn-MN.json +++ b/web/src/locales/mn-MN.json @@ -21,17 +21,23 @@ "settings": "Тохиргоо", "language": "Хэл", "page_not_found": "Хуудас олдсонгүй", + "internal_server_error": "Сервертэй холбогдоход алдаа гарлаа.", "username": "Нэвтрэх нэр", "email": "Имэйл", "password": "Нууц үг", + "captcha": "Батлах тэмдэгт", "auth_source": "Баталгаажуулалтын эх сурвалж", "local": "Локал", - "remember_me": "Сануулах", "forget_password": "Нууц үг сэргээх?", "disable_register_mail": "Уучлаарай, имэйлийн үйлчилгээ идэвхгүй байна. Сайтын админтай холбоо барина уу.", + "disable_register_prompt": "Уучлаарай, бүртгэл идэвхгүй байна. Сайтын админтай холбоо барина уу.", "non_local_account": "Гадаад хэрэглэгчид нууц үгээ солих боломжгүй.", + "create_new_account": "Шинэ данс үүсгэх", + "register_hepler_msg": "Та хэрэглэгчийн эрхээ үүсгэсэн бол Нэвтрэх хуудас руу шилжих!", + "sign_up": "Бүртгүүлэх", "sign_up_now": "Данс үүсгэх бол? Одоо бүртгүүлнэ үү.", "reset_password": "Нууц үгээ сэргээх", "invalid_code": "Уучлаарай, таны баталгаажуулах кодын хугацаа дууссан эсвэл хүчин төгөлдөр бус байна.", - "new_password": "Шинэ нууц үг" + "new_password": "Шинэ нууц үг", + "confirm_password": "Confirm Password" } diff --git a/web/src/locales/nl-NL.json b/web/src/locales/nl-NL.json index c14e854ef..7a2352cab 100644 --- a/web/src/locales/nl-NL.json +++ b/web/src/locales/nl-NL.json @@ -21,17 +21,23 @@ "settings": "Instellingen", "language": "Taal", "page_not_found": "Pagina niet gevonden", + "internal_server_error": "Interne Server Fout", "username": "Gebruikersnaam", "email": "E-mail", "password": "Wachtwoord", + "captcha": "CAPTCHA", "auth_source": "Authenticatiebron", "local": "Lokaal", - "remember_me": "Onthoud mij", "forget_password": "Wachtwoord vergeten?", "disable_register_mail": "Sorry, bevestiging van registratie per e-mail is uitgeschakeld.", + "disable_register_prompt": "Sorry, registratie is uitgeschakeld. Neem contact op met de beheerder van deze site.", "non_local_account": "Niet lokale accounts mogen hun wachtwoord niet veranderen via Gogs.", + "create_new_account": "Maak nieuw account aan", + "register_hepler_msg": "Heeft u al een account? Meld u nu aan!", + "sign_up": "Aanmelden", "sign_up_now": "Een account nodig? Meld u nu aan.", "reset_password": "Reset uw wachtwoord", "invalid_code": "Sorry, uw bevestigingscode is verlopen of niet meer geldig.", - "new_password": "Nieuw wachtwoord" + "new_password": "Nieuw wachtwoord", + "confirm_password": "Verifieer wachtwoord" } diff --git a/web/src/locales/pl-PL.json b/web/src/locales/pl-PL.json index fe8206343..cc1422553 100644 --- a/web/src/locales/pl-PL.json +++ b/web/src/locales/pl-PL.json @@ -21,17 +21,23 @@ "settings": "Ustawienia", "language": "Język", "page_not_found": "Strona nie została znaleziona", + "internal_server_error": "Wewnętrzny błąd serwera", "username": "Nazwa użytkownika", "email": "E-mail", "password": "Hasło", + "captcha": "Captcha", "auth_source": "Źródło uwierzytelniania", "local": "Lokalne", - "remember_me": "Zapamiętaj mnie", "forget_password": "Zapomniałeś hasła?", "disable_register_mail": "Przepraszamy, potwierdzenia rejestracji zostały wyłączone przez administratora.", + "disable_register_prompt": "Przepraszamy rejestracja została wyłączona. Prosimy o kontakt z administratorem serwisu.", "non_local_account": "Nie lokalne konta nie mogą zmieniać haseł przez Gogs.", + "create_new_account": "Załóż nowe konto", + "register_hepler_msg": "Masz już konto? Zaloguj się teraz!", + "sign_up": "Zarejestruj się", "sign_up_now": "Potrzebujesz konta? Zarejestruj się teraz.", "reset_password": "Resetowanie hasła", "invalid_code": "Niestety, Twój kod potwierdzający wygasł lub jest nieprawidłowy.", - "new_password": "Nowe hasło" + "new_password": "Nowe hasło", + "confirm_password": "Potwierdź hasło" } diff --git a/web/src/locales/pt-BR.json b/web/src/locales/pt-BR.json index 7292e963c..db7214a88 100644 --- a/web/src/locales/pt-BR.json +++ b/web/src/locales/pt-BR.json @@ -21,17 +21,23 @@ "settings": "Configurações", "language": "Idioma", "page_not_found": "Página Não Encontrada", + "internal_server_error": "Erro interno do servidor", "username": "Usuário", "email": "E-mail", "password": "Senha", + "captcha": "Captcha", "auth_source": "Fonte de autenticação", "local": "Local", - "remember_me": "Lembrar de mim", "forget_password": "Esqueceu a senha?", "disable_register_mail": "Desculpe, a confirmação de registro por e-mail foi desabilitada.", + "disable_register_prompt": "Desculpe, novos registros estão desabilitados. Por favor entre em contato com o administrador do site.", "non_local_account": "Não é possível mudar a senha de contas remotas pelo Gogs.", + "create_new_account": "Criar nova conta", + "register_hepler_msg": "Já tem uma conta? Entre agora!", + "sign_up": "Cadastrar", "sign_up_now": "Precisa de uma conta? Cadastre-se agora.", "reset_password": "Redefinir sua senha", "invalid_code": "Desculpe, seu código de confirmação expirou ou não é válido.", - "new_password": "Nova senha" + "new_password": "Nova senha", + "confirm_password": "Confirmar senha" } diff --git a/web/src/locales/pt-PT.json b/web/src/locales/pt-PT.json index 6e8605801..8845661f2 100644 --- a/web/src/locales/pt-PT.json +++ b/web/src/locales/pt-PT.json @@ -21,17 +21,23 @@ "settings": "Definições", "language": "Língua", "page_not_found": "Página Não Encontrada", + "internal_server_error": "Erro do servidor interno", "username": "Nome de utilizador", "email": "Endereço de email", "password": "Palavra-chave", + "captcha": "Captcha", "auth_source": "Tipo de autenticação", "local": "Local", - "remember_me": "Manter sessão iniciada", "forget_password": "Esqueceu a sua senha?", "disable_register_mail": "Desculpe, os serviços de email estão desativados. Por favor contacte o administrador.", + "disable_register_prompt": "Desculpe, o registo de novos utilizadores está desativado. Por favor contacte o administrador.", "non_local_account": "Contas não-locais não podem mudar a palavra-passe através do Gogs.", + "create_new_account": "Criar Nova Conta", + "register_hepler_msg": "Já tem uma conta? Inicie sessão!", + "sign_up": "Criar conta", "sign_up_now": "Precisa de uma conta? Inscreva-se agora.", "reset_password": "Restaurar a sua senha", "invalid_code": "Desculpe, o seu código de confirmação expirou ou é inválido.", - "new_password": "Nova senha" + "new_password": "Nova senha", + "confirm_password": "Confirmar senha" } diff --git a/web/src/locales/ro-RO.json b/web/src/locales/ro-RO.json index 89021991d..868954b0d 100644 --- a/web/src/locales/ro-RO.json +++ b/web/src/locales/ro-RO.json @@ -21,17 +21,23 @@ "settings": "Setări", "language": "Limba", "page_not_found": "Pagina nu a fost găsită", + "internal_server_error": "Eroare internă de server", "username": "Numele de utilizator", "email": "E-mail", "password": "Parolă", + "captcha": "Captcha", "auth_source": "Sursa de autentificare", "local": "Local", - "remember_me": "Ține-mă minte", "forget_password": "Ați uitat parola?", "disable_register_mail": "Ne pare rău, serviciile de e-mail sunt dezactivate. Vă rugăm să contactați administratorul site-ului.", + "disable_register_prompt": "Ne pare rău, înregistrarea a fost dezactivată. Vă rugăm să contactați administratorul site-ului.", "non_local_account": "Conturile non-locale nu pot schimba parolele prin Gogs.", + "create_new_account": "Creați un cont nou", + "register_hepler_msg": "Aveți deja un cont? Conectați-vă acum!", + "sign_up": "Înregistrare", "sign_up_now": "Nevoie de un cont? Inscrie-te acum.", "reset_password": "Resetați-vă parola", "invalid_code": "Ne pare rău, codul dvs. de confirmare a expirat sau nu este valabil.", - "new_password": "Parolă nouă" + "new_password": "Parolă nouă", + "confirm_password": "Confirmați Parola" } diff --git a/web/src/locales/ru-RU.json b/web/src/locales/ru-RU.json index 5199dd9c4..b36393aa3 100644 --- a/web/src/locales/ru-RU.json +++ b/web/src/locales/ru-RU.json @@ -21,17 +21,23 @@ "settings": "Настройки", "language": "Язык", "page_not_found": "Страница не найдена", + "internal_server_error": "Внутренняя ошибка сервера", "username": "Имя пользователя", "email": "Эл. почта", "password": "Пароль", + "captcha": "Капча", "auth_source": "Тип аутентификации", "local": "Локальный", - "remember_me": "Запомнить меня", "forget_password": "Забыли пароль?", "disable_register_mail": "К сожалению подтверждение регистрации по почте отключено.", + "disable_register_prompt": "Извините, возможность регистрации отключена. Пожалуйста, свяжитесь с администратором сайта.", "non_local_account": "Нелокальные аккаунты не могут изменить пароль через Gogs.", + "create_new_account": "Создать новый аккаунт", + "register_hepler_msg": "Уже есть аккаунт? Авторизуйтесь!", + "sign_up": "Регистрация", "sign_up_now": "Нужен аккаунт? Зарегистрируйтесь.", "reset_password": "Сброс пароля", "invalid_code": "Извините, ваш код подтверждения истек или не является допустимым.", - "new_password": "Новый пароль" + "new_password": "Новый пароль", + "confirm_password": "Подтвердить пароль" } diff --git a/web/src/locales/sk-SK.json b/web/src/locales/sk-SK.json index 9a5b8b9cb..640578e43 100644 --- a/web/src/locales/sk-SK.json +++ b/web/src/locales/sk-SK.json @@ -21,17 +21,23 @@ "settings": "Nastavenia", "language": "Jazyk", "page_not_found": "Page Not Found", + "internal_server_error": "Internal Server Error", "username": "Používateľské meno", "email": "E-mail", "password": "Heslo", + "captcha": "Kontrolný kód", "auth_source": "Zdroj overovania", "local": "Lokálny", - "remember_me": "Zapamätať prihlásenie", "forget_password": "Zabudli ste heslo?", "disable_register_mail": "Ospravedlňujeme sa, potvrdenie registračného e-mailu bolo vypnuté.", + "disable_register_prompt": "Ospravedlňujeme sa, ale registrácia bola vypnutá. Obráťte sa na administrátora stránky.", "non_local_account": "Miestne účty nemôžu meniť heslá cez Gogs.", + "create_new_account": "Vytvoriť nový účet", + "register_hepler_msg": "Máte už účet? Prihláste sa teraz!", + "sign_up": "Zaregistrovať sa", "sign_up_now": "Potrebujete účet? Zaregistrujte sa teraz.", "reset_password": "Obnovenie hesla", "invalid_code": "Ospravedlňujeme sa, váš potvrdzovací kód vypršal alebo nie je platný.", - "new_password": "Nové heslo" + "new_password": "Nové heslo", + "confirm_password": "Potvrdiť heslo" } diff --git a/web/src/locales/sr-SP.json b/web/src/locales/sr-SP.json index 8d215e6b0..bf57667af 100644 --- a/web/src/locales/sr-SP.json +++ b/web/src/locales/sr-SP.json @@ -21,17 +21,23 @@ "settings": "Подешавања", "language": "Језик", "page_not_found": "Page Not Found", + "internal_server_error": "Internal Server Error", "username": "Корисничко име", "email": "E-пошта", "password": "Лозинка", + "captcha": "Captcha", "auth_source": "Извор аутентикације", "local": "Локално", - "remember_me": "Запамти ме", "forget_password": "Заборавили сте лозинку?", "disable_register_mail": "Извините, потврда путем поште је онемогућено.", + "disable_register_prompt": "Извините регистрација је онемогућено. Молимо вас, контактирајте администратора.", "non_local_account": "Нелокални налози не могу да промените лозинку преко Gogs.", + "create_new_account": "Креирате нови налог", + "register_hepler_msg": "Већ имате налог? Пријавите се!", + "sign_up": "Регистрација", "sign_up_now": "Немате налог? Пријавите се.", "reset_password": "Ресет лозинке", "invalid_code": "Извините, ваш код за потврду је истекао или није валидан.", - "new_password": "Нова лозинка" + "new_password": "Нова лозинка", + "confirm_password": "Потврдите лозинку" } diff --git a/web/src/locales/sv-SE.json b/web/src/locales/sv-SE.json index 50b3c24fd..0c097546a 100644 --- a/web/src/locales/sv-SE.json +++ b/web/src/locales/sv-SE.json @@ -21,17 +21,23 @@ "settings": "inställningar", "language": "Språk", "page_not_found": "Sidan hittades inte", + "internal_server_error": "Internt serverfel", "username": "Användarnamn", "email": "E-post", "password": "Lösenord", + "captcha": "Captcha", "auth_source": "Autentiseringskälla", "local": "Lokal", - "remember_me": "Kom ihåg mig", "forget_password": "Glömt lösenordet?", "disable_register_mail": "Tyvärr så är registreringsbekräftelemailutskick inaktiverat.", + "disable_register_prompt": "Tyvärr är användarregistreringen inaktiverad. Vänligen kontakta din administratör.", "non_local_account": "Icke-lokala konton får inte ändra lösenord genom Gogs.", + "create_new_account": "Skapa nytt konto", + "register_hepler_msg": "Har du redan ett konto? Logga in nu!", + "sign_up": "Registrera dig", "sign_up_now": "Behöver du ett konto? Registrera dig nu.", "reset_password": "Återställ ditt lösenord", "invalid_code": "Tyvärr, din bekräftelsekod har antingen upphört att gälla eller är ogiltig.", - "new_password": "Nytt lösenord" + "new_password": "Nytt lösenord", + "confirm_password": "Bekräfta lösenord" } diff --git a/web/src/locales/tr-TR.json b/web/src/locales/tr-TR.json index 73bc8cb0b..88d203101 100644 --- a/web/src/locales/tr-TR.json +++ b/web/src/locales/tr-TR.json @@ -21,17 +21,23 @@ "settings": "Ayarlar", "language": "Dil", "page_not_found": "Sayfa Bulunamadı", + "internal_server_error": "İç Sunucu Hatası.", "username": "Kullanıcı Adı", "email": "E-Posta", "password": "Parola", + "captcha": "Captcha", "auth_source": "Yetkilendirme Kaynağı", "local": "Yerel", - "remember_me": "Beni Hatırla", "forget_password": "Parolanızı mı unuttunuz?", "disable_register_mail": "Üzgünüz, kayıt doğrulama e-postası devre dışı bırakıldı.", + "disable_register_prompt": "Üzgünüz, kaydolma devre dışı bırakıldı. Lütfen site yöneticisiyle irtibata geçin.", "non_local_account": "Yerel olmayan hesapların şifrelerini Gogs aracılığıyla değiştiremezsiniz.", + "create_new_account": "Yeni Hesap Oluştur", + "register_hepler_msg": "Bir hesabınız var mı? Şimdi giriş yapın!", + "sign_up": "Kaydol", "sign_up_now": "Bir hesaba mı ihtiyacınız var? Şimdi kaydolun.", "reset_password": "Parolanızı Sıfırlayın", "invalid_code": "Üzgünüz, doğrulama kodunuz geçersiz veya süresi dolmuş.", - "new_password": "Yeni Parola" + "new_password": "Yeni Parola", + "confirm_password": "Parolayı Doğrula" } diff --git a/web/src/locales/uk-UA.json b/web/src/locales/uk-UA.json index 0b4ad1c08..4f1a41366 100644 --- a/web/src/locales/uk-UA.json +++ b/web/src/locales/uk-UA.json @@ -21,17 +21,23 @@ "settings": "Налаштування", "language": "Мова", "page_not_found": "Сторінку не знайдено", + "internal_server_error": "Внутрішня помилка серверу", "username": "Ім'я користувача", "email": "Електронна пошта", "password": "Пароль", + "captcha": "CAPTCHA", "auth_source": "Джерело автентифікації", "local": "Локальний", - "remember_me": "Запам'ятати мене", "forget_password": "Забули пароль?", "disable_register_mail": "На жаль, підтвердження реєстрації на електрону пошту вимкнено адміністратором.", + "disable_register_prompt": "Вибачте, реєстрація відключена. Будь ласка, зв'яжіться з адміністратором сайту.", "non_local_account": "Нелокальні облікові записи не можуть змінити пароль через Gogs.", + "create_new_account": "Створити новий обліковий запис", + "register_hepler_msg": "Вже зареєстровані? Увійдіть зараз!", + "sign_up": "Реєстрація", "sign_up_now": "Потрібен обліковий запис? Зареєструватися зараз.", "reset_password": "Скинути пароль", "invalid_code": "На жаль, код підтвердження, закінчився або помилковий.", - "new_password": "Новий пароль" + "new_password": "Новий пароль", + "confirm_password": "Підтвердження паролю" } diff --git a/web/src/locales/vi-VN.json b/web/src/locales/vi-VN.json index d34c17bcd..783d99fd0 100644 --- a/web/src/locales/vi-VN.json +++ b/web/src/locales/vi-VN.json @@ -21,17 +21,23 @@ "settings": "Cài đặt", "language": "Ngôn ngữ", "page_not_found": "Không tìm thấy trang này!", + "internal_server_error": "Lỗi nội bộ máy chủ.", "username": "Username", "email": "Email", "password": "Mật khẩu", + "captcha": "Mã xác minh", "auth_source": "Authentication Source", "local": "Local", - "remember_me": "Ghi nhớ tôi", "forget_password": "Quên mật khẩu?", "disable_register_mail": "Xin lỗi, đăng ký đã bị vô hiệu. Xin vui lòng liên hệ với người quản trị trang web.", + "disable_register_prompt": "Xin lỗi, đăng ký đã bị vô hiệu. Xin vui lòng liên hệ với người quản trị trang web.", "non_local_account": "Tài khoản Non-local không thể thay đổi mật khẩu thông qua Gogs.", + "create_new_account": "Tạo một Tài khoản mới", + "register_hepler_msg": "Đã có tài khoản? Đăng nhập bây giờ!", + "sign_up": "Đăng ký", "sign_up_now": "Cần một tài khoản? Đăng ký bây giờ.", "reset_password": "Đặt lại mật khẩu của bạn", "invalid_code": "Xin lỗi, mã số xác nhận của bạn đã hết hạn hoặc không hợp lệ.", - "new_password": "Mật khẩu mới" + "new_password": "Mật khẩu mới", + "confirm_password": "Xác nhận mật khẩu" } diff --git a/web/src/locales/zh-CN.json b/web/src/locales/zh-CN.json index 7646397aa..a2c033ddb 100644 --- a/web/src/locales/zh-CN.json +++ b/web/src/locales/zh-CN.json @@ -21,17 +21,23 @@ "settings": "帐户设置", "language": "语言选项", "page_not_found": "页面未找到", + "internal_server_error": "内部服务器错误", "username": "用户名", "email": "邮箱", "password": "密码", + "captcha": "验证码", "auth_source": "认证源", "local": "本地", - "remember_me": "记住登录", "forget_password": "忘记密码?", "disable_register_mail": "对不起,注册邮箱确认功能已被关闭。", + "disable_register_prompt": "对不起,注册功能已被关闭。请联系网站管理员。", "non_local_account": "非本地类型的帐户无法通过 Gogs 修改密码。", + "create_new_account": "创建帐户", + "register_hepler_msg": "已经注册?立即登录!", + "sign_up": "注册", "sign_up_now": "还没帐户?马上注册。", "reset_password": "重置密码", "invalid_code": "对不起,您的确认代码已过期或已失效。", - "new_password": "新的密码" + "new_password": "新的密码", + "confirm_password": "确认密码" } diff --git a/web/src/locales/zh-HK.json b/web/src/locales/zh-HK.json index 4db60a927..474765f52 100644 --- a/web/src/locales/zh-HK.json +++ b/web/src/locales/zh-HK.json @@ -21,17 +21,23 @@ "settings": "設定", "language": "語言", "page_not_found": "Page Not Found", + "internal_server_error": "Internal Server Error", "username": "用戶名稱", "email": "電子郵件", "password": "密碼", + "captcha": "驗證碼", "auth_source": "Authentication Source", "local": "Local", - "remember_me": "記住登錄", "forget_password": "忘記密碼?", "disable_register_mail": "對不起,註冊郵箱確認功能已被關閉。", + "disable_register_prompt": "對不起,註冊功能已被關閉。請聯系網站管理員。", "non_local_account": "Non-local accounts cannot change passwords through Gogs.", + "create_new_account": "創建帳戶", + "register_hepler_msg": "已經註冊?立即登錄!", + "sign_up": "註冊", "sign_up_now": "還沒帳戶?馬上註冊。", "reset_password": "重置密碼", "invalid_code": "對不起,您的確認代碼已過期或已失效。", - "new_password": "新的密碼" + "new_password": "新的密碼", + "confirm_password": "確認密碼" } diff --git a/web/src/locales/zh-TW.json b/web/src/locales/zh-TW.json index de56a1796..fd7f4667c 100644 --- a/web/src/locales/zh-TW.json +++ b/web/src/locales/zh-TW.json @@ -21,17 +21,23 @@ "settings": "設定", "language": "語言", "page_not_found": "找不到頁面", + "internal_server_error": "內部伺服器錯誤", "username": "用戶名稱", "email": "電子郵件", "password": "密碼", + "captcha": "驗證碼", "auth_source": "認證來源", "local": "本地", - "remember_me": "記住登錄", "forget_password": "忘記密碼?", "disable_register_mail": "對不起,註冊郵箱確認功能已被關閉。", + "disable_register_prompt": "對不起,註冊功能已被關閉。請聯系網站管理員。", "non_local_account": "非本地帳戶無法通過 Gogs 修改密碼。", + "create_new_account": "創建帳戶", + "register_hepler_msg": "已經註冊?立即登錄!", + "sign_up": "註冊", "sign_up_now": "還沒帳戶?馬上註冊。", "reset_password": "重置密碼", "invalid_code": "對不起,您的確認代碼已過期或已失效。", - "new_password": "新的密碼" + "new_password": "新的密碼", + "confirm_password": "確認密碼" } diff --git a/web/src/pages/Landing.tsx b/web/src/pages/Landing.tsx index 231739dae..cc07d093b 100644 --- a/web/src/pages/Landing.tsx +++ b/web/src/pages/Landing.tsx @@ -45,7 +45,7 @@ export function Landing() { {"\n"} {"\n"} - + {"\n"} {"\n"} diff --git a/web/src/pages/ResetPassword.tsx b/web/src/pages/ResetPassword.tsx index b07433f52..4c4bde705 100644 --- a/web/src/pages/ResetPassword.tsx +++ b/web/src/pages/ResetPassword.tsx @@ -1,8 +1,8 @@ import { getRouteApi, useNavigate } from "@tanstack/react-router"; -import { Eye, EyeOff } from "lucide-react"; import { useRef, useState } from "react"; import { Trans, useTranslation } from "react-i18next"; +import { PasswordInput } from "@/components/PasswordInput"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; @@ -55,7 +55,7 @@ export function ResetPassword() { if (isResetForm && password !== confirmPassword) { setFormError(null); - setFieldErrors({ password: null, confirmPassword: t("reset_password_mismatch") }); + setFieldErrors({ password: null, confirmPassword: t("password_mismatch") }); requestAnimationFrame(() => confirmPasswordRef.current?.focus()); return; } @@ -160,6 +160,7 @@ export function ResetPassword() { required autoFocus tabIndex={1} + placeholder={t("email_placeholder")} value={email} onChange={(e) => setEmail(e.target.value)} aria-invalid={"email" in fieldErrors ? true : undefined} @@ -202,35 +203,20 @@ export function ResetPassword() {
-
- setPassword(e.target.value)} - aria-invalid={"password" in fieldErrors ? true : undefined} - aria-describedby={fieldErrors.password ? "password-error" : undefined} - className="pr-10" - /> - -
+ setShowPassword((v) => !v)} + disabled={submitting} + autoFocus + describedBy={fieldErrors.password ? "password-error" : undefined} + invalid={"password" in fieldErrors} + onChange={setPassword} + /> {fieldErrors.password && (

{fieldErrors.password} @@ -239,38 +225,19 @@ export function ResetPassword() {

-
- setConfirmPassword(e.target.value)} - aria-invalid={"confirmPassword" in fieldErrors ? true : undefined} - aria-describedby={fieldErrors.confirmPassword ? "confirmPassword-error" : undefined} - className="pr-10" - /> - -
+ setShowConfirmPassword((v) => !v)} + disabled={submitting} + describedBy={fieldErrors.confirmPassword ? "confirmPassword-error" : undefined} + invalid={"confirmPassword" in fieldErrors} + onChange={setConfirmPassword} + /> {fieldErrors.confirmPassword && (

{fieldErrors.confirmPassword} diff --git a/web/src/pages/ServerError.tsx b/web/src/pages/ServerError.tsx new file mode 100644 index 000000000..65c89e932 --- /dev/null +++ b/web/src/pages/ServerError.tsx @@ -0,0 +1,51 @@ +import type { ErrorComponentProps } from "@tanstack/react-router"; +import { useTranslation } from "react-i18next"; + +import { LoaderResponseError } from "@/lib/loader-error"; +import { usePageTitle } from "@/lib/page-title"; + +export function ServerError({ error }: ErrorComponentProps) { + const { t } = useTranslation(); + usePageTitle(t("internal_server_error")); + const path = typeof window === "undefined" ? "/" : window.location.pathname; + + // Prefer the structured `error` field from the webapi JSON response; fall + // back to the raw body when the upstream returned non-JSON (e.g. a proxy + // error page); fall back again to the generic message when nothing useful + // was carried over. + let detail = t("internal_server_error"); + if (error instanceof LoaderResponseError) { + if (error.errorField) { + detail = error.errorField; + } else if (error.body) { + detail = error.body; + } + } else if (error instanceof Error && error.message) { + detail = error.message; + } + + return ( +

+
+
+
+ + + + gogs — zsh +
+
+            $ 
+            gogs show {path}
+            {"\n"}
+            fatal: {detail}
+            {"\n"}
+            {"\n"}
+            $ 
+             
+          
+
+
+
+ ); +} diff --git a/web/src/pages/SignIn.tsx b/web/src/pages/SignIn.tsx index 72ac07434..0f7bf26e9 100644 --- a/web/src/pages/SignIn.tsx +++ b/web/src/pages/SignIn.tsx @@ -235,7 +235,7 @@ export function SignIn() { +
+ ); + } + + return ( +
+
+ {formError && ( +
+ {formError} +
+ )} + +
+
+ + setUserName(e.target.value)} + aria-invalid={"userName" in fieldErrors ? true : undefined} + aria-describedby={fieldErrors.userName ? "userName-error" : undefined} + /> + {fieldErrors.userName && ( +

+ {fieldErrors.userName} +

+ )} +
+ +
+ + setEmail(e.target.value)} + aria-invalid={"email" in fieldErrors ? true : undefined} + aria-describedby={fieldErrors.email ? "email-error" : undefined} + /> + {fieldErrors.email && ( +

+ {fieldErrors.email} +

+ )} +
+ +
+ + setShowPassword((v) => !v)} + disabled={submitting} + describedBy={fieldErrors.password ? "password-error" : undefined} + invalid={"password" in fieldErrors} + onChange={setPassword} + /> + {fieldErrors.password && ( +

+ {fieldErrors.password} +

+ )} +
+ +
+ + setShowConfirmPassword((v) => !v)} + disabled={submitting} + describedBy={fieldErrors.confirmPassword ? "confirmPassword-error" : undefined} + invalid={"confirmPassword" in fieldErrors} + onChange={setConfirmPassword} + /> + {fieldErrors.confirmPassword && ( +

+ {fieldErrors.confirmPassword} +

+ )} +
+ + {captchaEnabled && ( +
+ +
+ + + {t("click_to_refresh_captcha")} + +
+ setCaptcha(e.target.value)} + aria-invalid={"captcha" in fieldErrors ? true : undefined} + aria-describedby={fieldErrors.captcha ? "captcha-error" : undefined} + /> + {fieldErrors.captcha && ( +

+ {fieldErrors.captcha} +

+ )} +
+ )} + +
+ + +
+
+
+
+ ); + } +} diff --git a/web/src/router.tsx b/web/src/router.tsx index f5a501778..5cfc15e48 100644 --- a/web/src/router.tsx +++ b/web/src/router.tsx @@ -10,13 +10,16 @@ import { import { Footer } from "@/components/Footer"; import { Navbar } from "@/components/Navbar"; import { webContext } from "@/lib/context"; +import { loaderResponseError } from "@/lib/loader-error"; import { subUrl } from "@/lib/url"; import type { UserInfo } from "@/lib/user-info"; import { Landing } from "@/pages/Landing"; import { MFA } from "@/pages/MFA"; import { NotFound } from "@/pages/NotFound"; import { ResetPassword, type ResetPasswordPage } from "@/pages/ResetPassword"; +import { ServerError } from "@/pages/ServerError"; import { SignIn, type SignInPage } from "@/pages/SignIn"; +import { SignUp, type SignUpPage } from "@/pages/SignUp"; interface RouterContext { user: UserInfo | null; @@ -68,6 +71,20 @@ const signInRoute = createRoute({ component: SignIn, }); +const signUpRoute = createRoute({ + getParentRoute: () => rootRoute, + path: "/user/sign-up", + beforeLoad: requireUnauthenticated, + loader: async (): Promise => { + const res = await fetch(subUrl("/api/web/user/sign-up"), { credentials: "same-origin" }); + if (!res.ok) { + throw await loaderResponseError(res); + } + return (await res.json()) as SignUpPage; + }, + component: SignUp, +}); + const resetPasswordRoute = createRoute({ getParentRoute: () => rootRoute, path: "/user/reset-password", @@ -104,13 +121,14 @@ const mfaRoute = createRoute({ component: MFA, }); -const routeTree = rootRoute.addChildren([landingRoute, signInRoute, resetPasswordRoute, mfaRoute]); +const routeTree = rootRoute.addChildren([landingRoute, signInRoute, signUpRoute, resetPasswordRoute, mfaRoute]); function makeRouter(context: RouterContext) { return createRouter({ routeTree, basepath: webContext.subURL || "/", defaultNotFoundComponent: NotFound, + defaultErrorComponent: ServerError, context, }); }