mirror of
https://github.com/gogs/gogs.git
synced 2026-05-28 21:30:36 +00:00
feat(web): add React sign-up page with Flamego captcha (#8291)
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
+145
-21
@@ -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
|
||||
// <field label> + <suffix>, 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)
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
@@ -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) {
|
||||
|
||||
@@ -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}}
|
||||
|
||||
@@ -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" .}}
|
||||
@@ -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",
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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": "Потвърждение на паролата"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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": "تأیید رمز عبور"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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": "パスワード確認"
|
||||
}
|
||||
|
||||
@@ -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": "비밀번호 확인"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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": "Подтвердить пароль"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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": "Потврдите лозинку"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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": "Підтвердження паролю"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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": "确认密码"
|
||||
}
|
||||
|
||||
@@ -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": "確認密碼"
|
||||
}
|
||||
|
||||
@@ -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": "確認密碼"
|
||||
}
|
||||
|
||||
@@ -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"}
|
||||
|
||||
@@ -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}
|
||||
id="password"
|
||||
name="password"
|
||||
type={showPassword ? "text" : "password"}
|
||||
autoComplete="new-password"
|
||||
required
|
||||
autoFocus
|
||||
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}
|
||||
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>
|
||||
<PasswordInput
|
||||
inputRef={passwordRef}
|
||||
id="password"
|
||||
value={password}
|
||||
tabIndex={1}
|
||||
placeholder={t("new_password_placeholder")}
|
||||
show={showPassword}
|
||||
onToggleShow={() => setShowPassword((v) => !v)}
|
||||
disabled={submitting}
|
||||
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}
|
||||
id="confirmPassword"
|
||||
name="confirmPassword"
|
||||
type={showConfirmPassword ? "text" : "password"}
|
||||
autoComplete="new-password"
|
||||
required
|
||||
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}
|
||||
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>
|
||||
<PasswordInput
|
||||
inputRef={confirmPasswordRef}
|
||||
id="confirmPassword"
|
||||
value={confirmPassword}
|
||||
tabIndex={3}
|
||||
placeholder={t("confirm_new_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}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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}
|
||||
|
||||
@@ -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
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user