feat(web): add React sign-up page with Flamego captcha (#8291)

This commit is contained in:
ᴊᴏᴇ ᴄʜᴇɴ
2026-05-23 23:33:41 -04:00
committed by GitHub
parent 403db931cf
commit 26483c41c6
52 changed files with 1001 additions and 376 deletions
+11 -12
View File
@@ -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
}
+139 -15
View File
@@ -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
@@ -161,6 +189,7 @@ var ruleSuffixKeys = map[string]string{
"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)
+15 -7
View File
@@ -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 <b>%s</b>, 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 <b>%s</b>, 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]
+3 -1
View File
@@ -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
+6 -2
View File
@@ -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=
-10
View File
@@ -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,
-18
View File
@@ -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)
}
// __________________________________________.___ _______ ________ _________
// / _____/\_ _____/\__ ___/\__ ___/| |\ \ / _____/ / _____/
// \_____ \ | __)_ | | | | | |/ | \/ \ ___ \_____ \
+1 -114
View File
@@ -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) {
+2 -2
View File
@@ -15,11 +15,11 @@
{{else if .ResendLimited}}
<p class="center">{{.i18n.Tr "auth.resent_limit_prompt"}}</p>
{{else}}
<p>{{.i18n.Tr "auth.confirmation_mail_sent_prompt" .LoggedUser.Email .Hours | Str2HTML}}</p>
<p>{{.i18n.Tr "auth.confirmation_email_sent" .LoggedUser.Email .Hours | Str2HTML}}</p>
{{end}}
{{else}}
{{if .IsSendRegisterMail}}
<p>{{.i18n.Tr "auth.confirmation_mail_sent_prompt" .Email .Hours | Str2HTML}}</p>
<p>{{.i18n.Tr "auth.confirmation_email_sent" .Email .Hours | Str2HTML}}</p>
{{else if .IsActivateFailed}}
<p>{{.i18n.Tr "auth.invalid_code"}}</p>
{{else}}
-56
View File
@@ -1,56 +0,0 @@
{{template "base/head" .}}
<div class="user signup">
<div class="ui middle very relaxed page grid">
<div class="column">
<form class="ui form" action="{{.Link}}" method="post">
{{.CSRFTokenHTML}}
<h3 class="ui top attached header">
{{.i18n.Tr "sign_up"}}
</h3>
<div class="ui attached segment">
{{template "base/alert" .}}
{{if .DisableRegistration}}
<p>{{.i18n.Tr "auth.disable_register_prompt"}}</p>
{{else}}
<div class="required inline field {{if .Err_UserName}}error{{end}}">
<label for="user_name">{{.i18n.Tr "username"}}</label>
<input id="user_name" name="user_name" value="{{.user_name}}" autofocus required>
</div>
<div class="required inline field {{if .Err_Email}}error{{end}}">
<label for="email">{{.i18n.Tr "email"}}</label>
<input id="email" name="email" type="email" value="{{.email}}" required>
</div>
<div class="required inline field {{if .Err_Password}}error{{end}}">
<label for="password">{{.i18n.Tr "password"}}</label>
<input id="password" name="password" type="password" value="{{.password}}" required>
</div>
<div class="required inline field {{if .Err_Password}}error{{end}}">
<label for="retype">{{.i18n.Tr "re_type"}}</label>
<input id="retype" name="retype" type="password" value="{{.retype}}" required>
</div>
{{if .EnableCaptcha}}
<div class="inline field">
<label></label>
{{.Captcha.CreateHtml}}
</div>
<div class="required inline field {{if .Err_Captcha}}error{{end}}">
<label for="captcha">{{.i18n.Tr "captcha"}}</label>
<input id="captcha" name="captcha" value="{{.captcha}}" autocomplete="off">
</div>
{{end}}
<div class="inline field">
<label></label>
<button class="ui green button">{{.i18n.Tr "auth.create_new_account"}}</button>
</div>
<div class="inline field">
<label></label>
<a href="{{AppSubURL}}/user/sign-in">{{.i18n.Tr "auth.register_hepler_msg"}}</a>
</div>
{{end}}
</div>
</form>
</div>
</div>
</div>
{{template "base/footer" .}}
+18 -2
View File
@@ -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",
+4 -2
View File
@@ -65,7 +65,9 @@ export function Navbar() {
<NavLink href="/user/sign-in" spa>
{t("sign_in")}
</NavLink>
<NavLink href="/user/sign_up">{t("register")}</NavLink>
<NavLink href="/user/sign-up" spa>
{t("register")}
</NavLink>
</>
)}
</div>
@@ -153,7 +155,7 @@ export function Navbar() {
<MobileLink href="/user/sign-in" spa onClick={() => setOpen(false)}>
{t("sign_in")}
</MobileLink>
<MobileLink href="/user/sign_up" onClick={() => setOpen(false)}>
<MobileLink href="/user/sign-up" spa onClick={() => setOpen(false)}>
{t("register")}
</MobileLink>
</>
+65
View File
@@ -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<HTMLInputElement | null>;
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 (
<div className="relative">
<Input
ref={inputRef}
id={id}
name={id}
type={show ? "text" : "password"}
autoComplete="new-password"
required
autoFocus={autoFocus}
tabIndex={tabIndex}
placeholder={placeholder}
value={value}
onChange={(e) => onChange(e.target.value)}
aria-invalid={invalid ? true : undefined}
aria-describedby={describedBy}
className="pr-10"
/>
<button
type="button"
tabIndex={tabIndex + 1}
disabled={disabled}
onClick={onToggleShow}
aria-label={show ? t("hide_password") : t("show_password")}
aria-pressed={show}
className="absolute inset-y-0 right-0 flex w-10 cursor-pointer items-center justify-center rounded-r-md text-(--color-muted-foreground) outline-none hover:text-(--color-foreground) focus-visible:text-(--color-foreground) focus-visible:ring-1 focus-visible:ring-(--color-ring) disabled:cursor-not-allowed disabled:opacity-50"
>
{show ? <EyeOff className="size-4" aria-hidden /> : <Eye className="size-4" aria-hidden />}
</button>
</div>
);
}
+34
View File
@@ -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<LoaderResponseError> {
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);
}
+8 -2
View File
@@ -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": "Потвърждение на паролата"
}
+8 -2
View File
@@ -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"
}
+8 -2
View File
@@ -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"
}
+8 -2
View File
@@ -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"
}
+18 -1
View File
@@ -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>{email}</email>, please check your inbox within <hours>{hours} 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 <b>%s</b>, 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",
+8 -2
View File
@@ -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"
}
+8 -2
View File
@@ -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": "تأیید رمز عبور"
}
+8 -2
View File
@@ -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"
}
+8 -2
View File
@@ -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"
}
+8 -2
View File
@@ -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"
}
+8 -2
View File
@@ -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"
}
+8 -2
View File
@@ -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"
}
+8 -2
View File
@@ -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"
}
+8 -2
View File
@@ -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": "パスワード確認"
}
+8 -2
View File
@@ -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": "비밀번호 확인"
}
+8 -2
View File
@@ -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"
}
+8 -2
View File
@@ -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"
}
+8 -2
View File
@@ -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"
}
+8 -2
View File
@@ -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"
}
+8 -2
View File
@@ -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"
}
+8 -2
View File
@@ -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"
}
+8 -2
View File
@@ -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"
}
+8 -2
View File
@@ -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": "Подтвердить пароль"
}
+8 -2
View File
@@ -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"
}
+8 -2
View File
@@ -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": "Потврдите лозинку"
}
+8 -2
View File
@@ -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"
}
+8 -2
View File
@@ -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"
}
+8 -2
View File
@@ -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": "Підтвердження паролю"
}
+8 -2
View File
@@ -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"
}
+8 -2
View File
@@ -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": "确认密码"
}
+8 -2
View File
@@ -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": "確認密碼"
}
+8 -2
View File
@@ -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": "確認密碼"
}
+1 -1
View File
@@ -45,7 +45,7 @@ export function Landing() {
{"\n"}
<CmdLink href="/user/sign-in" cmd="sign-in" desc={t("sign_in")} spa />
{"\n"}
<CmdLink href="/user/sign_up" cmd="sign-up" desc={t("register")} />
<CmdLink href="/user/sign-up" cmd="sign-up" desc={t("register")} spa />
{"\n"}
<CmdLink href="/explore/repos" cmd="explore" desc={t("explore")} />
{"\n"}
+22 -55
View File
@@ -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() {
<div className="flex flex-col gap-4">
<div className="flex flex-col gap-1.5">
<Label htmlFor="password">{t("new_password")}</Label>
<div className="relative">
<Input
ref={passwordRef}
<PasswordInput
inputRef={passwordRef}
id="password"
name="password"
type={showPassword ? "text" : "password"}
autoComplete="new-password"
required
autoFocus
value={password}
tabIndex={1}
placeholder={t("new_password_placeholder")}
value={password}
onChange={(e) => setPassword(e.target.value)}
aria-invalid={"password" in fieldErrors ? true : undefined}
aria-describedby={fieldErrors.password ? "password-error" : undefined}
className="pr-10"
/>
<button
type="button"
tabIndex={2}
show={showPassword}
onToggleShow={() => setShowPassword((v) => !v)}
disabled={submitting}
onClick={() => setShowPassword((v) => !v)}
aria-label={showPassword ? t("hide_password") : t("show_password")}
aria-pressed={showPassword}
className="absolute inset-y-0 right-0 flex w-10 cursor-pointer items-center justify-center rounded-r-md text-(--color-muted-foreground) outline-none hover:text-(--color-foreground) focus-visible:text-(--color-foreground) focus-visible:ring-1 focus-visible:ring-(--color-ring) disabled:cursor-not-allowed disabled:opacity-50"
>
{showPassword ? <EyeOff className="size-4" aria-hidden /> : <Eye className="size-4" aria-hidden />}
</button>
</div>
autoFocus
describedBy={fieldErrors.password ? "password-error" : undefined}
invalid={"password" in fieldErrors}
onChange={setPassword}
/>
{fieldErrors.password && (
<p id="password-error" className="text-sm text-(--color-destructive)">
{fieldErrors.password}
@@ -239,38 +225,19 @@ export function ResetPassword() {
</div>
<div className="flex flex-col gap-1.5">
<Label htmlFor="confirmPassword">{t("confirm_new_password")}</Label>
<div className="relative">
<Input
ref={confirmPasswordRef}
<PasswordInput
inputRef={confirmPasswordRef}
id="confirmPassword"
name="confirmPassword"
type={showConfirmPassword ? "text" : "password"}
autoComplete="new-password"
required
value={confirmPassword}
tabIndex={3}
placeholder={t("confirm_new_password_placeholder")}
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
aria-invalid={"confirmPassword" in fieldErrors ? true : undefined}
aria-describedby={fieldErrors.confirmPassword ? "confirmPassword-error" : undefined}
className="pr-10"
/>
<button
type="button"
tabIndex={4}
show={showConfirmPassword}
onToggleShow={() => setShowConfirmPassword((v) => !v)}
disabled={submitting}
onClick={() => setShowConfirmPassword((v) => !v)}
aria-label={showConfirmPassword ? t("hide_password") : t("show_password")}
aria-pressed={showConfirmPassword}
className="absolute inset-y-0 right-0 flex w-10 cursor-pointer items-center justify-center rounded-r-md text-(--color-muted-foreground) outline-none hover:text-(--color-foreground) focus-visible:text-(--color-foreground) focus-visible:ring-1 focus-visible:ring-(--color-ring) disabled:cursor-not-allowed disabled:opacity-50"
>
{showConfirmPassword ? (
<EyeOff className="size-4" aria-hidden />
) : (
<Eye className="size-4" aria-hidden />
)}
</button>
</div>
describedBy={fieldErrors.confirmPassword ? "confirmPassword-error" : undefined}
invalid={"confirmPassword" in fieldErrors}
onChange={setConfirmPassword}
/>
{fieldErrors.confirmPassword && (
<p id="confirmPassword-error" className="text-sm text-(--color-destructive)">
{fieldErrors.confirmPassword}
+51
View File
@@ -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 (
<main className="flex flex-1 items-center justify-center px-4 py-10 sm:px-6 sm:py-16">
<div className="w-full max-w-2xl">
<div className="rounded-lg border border-(--color-foreground)/80 bg-(--color-surface)/40 font-mono shadow-xs dark:border-(--color-border)">
<div className="flex items-center gap-1.5 border-b border-(--color-foreground)/80 px-3 py-2 sm:px-4 sm:py-2.5 dark:border-(--color-border)">
<span className="size-2.5 rounded-full bg-(--color-destructive)/70" />
<span className="size-2.5 rounded-full bg-(--color-warning,oklch(0.795_0.184_86.047))/70" />
<span className="size-2.5 rounded-full bg-(--color-foreground)/20" />
<span className="ml-2 text-xs text-(--color-muted-foreground) sm:ml-3">gogs zsh</span>
</div>
<pre className="px-4 py-4 font-pixel text-sm leading-relaxed break-all whitespace-pre-wrap text-(--color-foreground) sm:px-5 sm:py-5 sm:text-base">
<span className="text-(--color-muted-foreground)">$ </span>
<span>gogs show {path}</span>
{"\n"}
<span className="text-(--color-destructive)">fatal:</span> {detail}
{"\n"}
{"\n"}
<span className="text-(--color-muted-foreground)">$ </span>
<span className="inline-block w-2 animate-pulse bg-(--color-foreground) align-baseline"> </span>
</pre>
</div>
</div>
</main>
);
}
+1 -1
View File
@@ -235,7 +235,7 @@ export function SignIn() {
</Button>
<Button variant="link" size="inline" asChild className="self-center">
<a
href={subUrl("/user/sign_up")}
href={subUrl("/user/sign-up")}
tabIndex={submitting ? -1 : 7}
aria-disabled={submitting || undefined}
className={submitting ? "pointer-events-none opacity-50" : undefined}
+329
View File
@@ -0,0 +1,329 @@
import { getRouteApi, useNavigate } from "@tanstack/react-router";
import { useRef, useState } from "react";
import { 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";
import { Label } from "@/components/ui/label";
import { usePageTitle } from "@/lib/page-title";
import { subUrl } from "@/lib/url";
export interface SignUpPage {
registrationDisabled: boolean;
captchaEnabled: boolean;
}
interface SignUpResponse {
emailConfirmationRequired?: boolean;
email?: string;
hours?: number;
}
interface SignUpErrorResponse {
error?: string;
fields?: Record<string, string | null>;
}
const FIELD_ORDER = ["userName", "email", "password", "confirmPassword", "captcha"] as const;
const route = getRouteApi("/user/sign-up");
export function SignUp() {
const { t } = useTranslation();
usePageTitle(t("register"));
const navigate = useNavigate();
const { registrationDisabled, captchaEnabled } = route.useLoaderData();
const [userName, setUserName] = useState("");
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [confirmPassword, setConfirmPassword] = useState("");
const [captcha, setCaptcha] = useState("");
const [captchaRefresh, setCaptchaRefresh] = useState(0);
const [showPassword, setShowPassword] = useState(false);
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
const [submitting, setSubmitting] = useState(false);
const [sent, setSent] = useState<SignUpResponse | null>(null);
const [formError, setFormError] = useState<string | null>(null);
const [fieldErrors, setFieldErrors] = useState<Record<string, string | null>>({});
const userNameRef = useRef<HTMLInputElement>(null);
const emailRef = useRef<HTMLInputElement>(null);
const passwordRef = useRef<HTMLInputElement>(null);
const confirmPasswordRef = useRef<HTMLInputElement>(null);
const captchaRef = useRef<HTMLInputElement>(null);
function refreshCaptcha() {
setCaptcha("");
setCaptchaRefresh((value) => value + 1);
}
function onSubmit(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault();
if (registrationDisabled) return;
setFormError(null);
if (password !== confirmPassword) {
setFieldErrors({ password: null, confirmPassword: t("password_mismatch") });
requestAnimationFrame(() => confirmPasswordRef.current?.focus());
return;
}
setFieldErrors({});
setSubmitting(true);
void (async () => {
try {
const res = await fetch(subUrl("/api/web/user/sign-up"), {
method: "POST",
credentials: "same-origin",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ userName, email, password, captcha }),
});
if (!res.ok) {
const body = (await res.json().catch(() => ({}))) as SignUpErrorResponse;
if (body.error) setFormError(body.error);
let focusField: (typeof FIELD_ORDER)[number] | undefined;
if (body.fields) {
setFieldErrors(body.fields);
focusField = FIELD_ORDER.find((f) => f in (body.fields ?? {}));
}
if (!body.error && !body.fields) {
setFormError(t("sign_up_failed"));
}
setSubmitting(false);
if (captchaEnabled) refreshCaptcha();
requestAnimationFrame(() => {
if (focusField === "userName") userNameRef.current?.focus();
else if (focusField === "email") emailRef.current?.focus();
else if (focusField === "password") passwordRef.current?.focus();
else if (focusField === "confirmPassword") confirmPasswordRef.current?.focus();
else if (focusField === "captcha") captchaRef.current?.focus();
});
return;
}
const data = (await res.json()) as SignUpResponse;
if (data.emailConfirmationRequired) {
setSent(data);
setSubmitting(false);
return;
}
await navigate({ to: "/user/sign-in" });
} catch {
setFormError(t("sign_up_failed"));
setSubmitting(false);
if (captchaEnabled) refreshCaptcha();
}
})();
}
return (
<main className="flex flex-1 items-center justify-center px-4 py-10 sm:px-6 sm:py-16">
<Card className="w-full max-w-md">
<CardHeader className="items-center text-center">
<CardTitle>{t("register")}</CardTitle>
</CardHeader>
<CardContent className="pt-2">{renderContent()}</CardContent>
</Card>
</main>
);
function renderContent() {
if (registrationDisabled) {
return (
<p role="alert" className="text-center text-sm text-(--color-destructive)">
{t("disable_register_prompt")}
</p>
);
}
if (sent) {
return (
<div className="flex flex-col gap-4 text-center">
<p role="status" className="text-sm text-(--color-foreground)">
{t("confirmation_email_sent")
.replace(/<[^>]+>/g, "")
.replace("%s", sent.email!)
.replace("%d", String(sent.hours))}
</p>
<Button variant="link" size="inline" asChild className="self-center">
<a href={subUrl("/user/sign-in")}>{t("back_to_sign_in")}</a>
</Button>
</div>
);
}
return (
<form onSubmit={onSubmit} noValidate>
<fieldset disabled={submitting} className="contents">
{formError && (
<div
role="alert"
className="mb-4 rounded-md border border-(--color-destructive) bg-(--color-destructive)/10 px-3 py-2 text-sm text-(--color-destructive)"
>
{formError}
</div>
)}
<div className="flex flex-col gap-4">
<div className="flex flex-col gap-1.5">
<Label htmlFor="userName">{t("username")}</Label>
<Input
ref={userNameRef}
id="userName"
name="userName"
type="text"
autoComplete="username"
required
autoFocus
tabIndex={1}
placeholder={t("new_username_placeholder")}
value={userName}
onChange={(e) => setUserName(e.target.value)}
aria-invalid={"userName" in fieldErrors ? true : undefined}
aria-describedby={fieldErrors.userName ? "userName-error" : undefined}
/>
{fieldErrors.userName && (
<p id="userName-error" className="text-sm text-(--color-destructive)">
{fieldErrors.userName}
</p>
)}
</div>
<div className="flex flex-col gap-1.5">
<Label htmlFor="email">{t("email")}</Label>
<Input
ref={emailRef}
id="email"
name="email"
type="email"
autoComplete="email"
required
tabIndex={2}
placeholder={t("email_placeholder")}
value={email}
onChange={(e) => setEmail(e.target.value)}
aria-invalid={"email" in fieldErrors ? true : undefined}
aria-describedby={fieldErrors.email ? "email-error" : undefined}
/>
{fieldErrors.email && (
<p id="email-error" className="text-sm text-(--color-destructive)">
{fieldErrors.email}
</p>
)}
</div>
<div className="flex flex-col gap-1.5">
<Label htmlFor="password">{t("password")}</Label>
<PasswordInput
inputRef={passwordRef}
id="password"
value={password}
tabIndex={3}
placeholder={t("password_placeholder")}
show={showPassword}
onToggleShow={() => setShowPassword((v) => !v)}
disabled={submitting}
describedBy={fieldErrors.password ? "password-error" : undefined}
invalid={"password" in fieldErrors}
onChange={setPassword}
/>
{fieldErrors.password && (
<p id="password-error" className="text-sm text-(--color-destructive)">
{fieldErrors.password}
</p>
)}
</div>
<div className="flex flex-col gap-1.5">
<Label htmlFor="confirmPassword">{t("confirm_password")}</Label>
<PasswordInput
inputRef={confirmPasswordRef}
id="confirmPassword"
value={confirmPassword}
tabIndex={5}
placeholder={t("confirm_password_placeholder")}
show={showConfirmPassword}
onToggleShow={() => setShowConfirmPassword((v) => !v)}
disabled={submitting}
describedBy={fieldErrors.confirmPassword ? "confirmPassword-error" : undefined}
invalid={"confirmPassword" in fieldErrors}
onChange={setConfirmPassword}
/>
{fieldErrors.confirmPassword && (
<p id="confirmPassword-error" className="text-sm text-(--color-destructive)">
{fieldErrors.confirmPassword}
</p>
)}
</div>
{captchaEnabled && (
<div className="flex flex-col gap-2">
<Label htmlFor="captcha">{t("captcha")}</Label>
<div className="group relative">
<button
type="button"
tabIndex={7}
disabled={submitting}
onClick={refreshCaptcha}
aria-label={t("refresh_captcha")}
className="block w-full cursor-pointer overflow-hidden rounded-md border border-(--color-border) bg-(--color-surface) outline-none focus-visible:ring-1 focus-visible:ring-(--color-ring) disabled:cursor-not-allowed disabled:opacity-60"
>
<img
src={subUrl("/captcha/image.jpeg") + "?refresh=true&v=" + captchaRefresh}
alt={t("captcha_image_alt")}
className="block h-20 w-full object-fill"
/>
</button>
<span
role="tooltip"
className="pointer-events-none absolute top-1/2 left-1/2 z-10 -translate-x-1/2 -translate-y-1/2 rounded-md bg-(--color-foreground) px-2 py-1 text-xs font-medium text-(--color-background) opacity-0 shadow transition-opacity duration-150 group-hover:opacity-90 group-focus-within:opacity-90"
>
{t("click_to_refresh_captcha")}
</span>
</div>
<Input
ref={captchaRef}
id="captcha"
name="captcha"
type="text"
autoComplete="off"
required
tabIndex={8}
placeholder={t("captcha_placeholder")}
value={captcha}
onChange={(e) => setCaptcha(e.target.value)}
aria-invalid={"captcha" in fieldErrors ? true : undefined}
aria-describedby={fieldErrors.captcha ? "captcha-error" : undefined}
/>
{fieldErrors.captcha && (
<p id="captcha-error" className="text-sm text-(--color-destructive)">
{fieldErrors.captcha}
</p>
)}
</div>
)}
<div className="mt-2 flex flex-col gap-3">
<Button type="submit" disabled={submitting} tabIndex={9} className="w-full">
{submitting ? t("sign_up_submitting") : t("create_new_account")}
</Button>
<Button variant="link" size="inline" asChild className="self-center">
<a
href={subUrl("/user/sign-in")}
tabIndex={submitting ? -1 : 10}
aria-disabled={submitting || undefined}
className={submitting ? "pointer-events-none opacity-50" : undefined}
onClick={(e) => {
if (submitting) e.preventDefault();
}}
>
{t("register_hepler_msg")}
</a>
</Button>
</div>
</div>
</fieldset>
</form>
);
}
}
+19 -1
View File
@@ -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<SignUpPage> => {
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,
});
}