web: move password reset to React (#8290)

This commit is contained in:
ᴊᴏᴇ ᴄʜᴇɴ
2026-05-23 21:55:22 -04:00
committed by GitHub
parent 71dfd3c7ac
commit 4935e7a63b
62 changed files with 1094 additions and 340 deletions
+89
View File
@@ -0,0 +1,89 @@
package web
import (
"crypto/tls"
"strconv"
"strings"
"time"
"github.com/cockroachdb/errors"
"github.com/flamego/cache"
"github.com/flamego/cache/redis"
"gopkg.in/ini.v1"
"gogs.io/gogs/internal/conf"
"gogs.io/gogs/internal/strx"
)
func parseCacheOptions(confOpts conf.CacheOptions) (cache.Options, error) {
opts := cache.Options{
GCInterval: time.Duration(confOpts.Interval) * time.Second,
}
switch strx.Coalesce(strings.ToLower(confOpts.Adapter), "memory") {
case "memory":
opts.Initer = cache.MemoryIniter()
case "file":
opts.Initer = cache.FileIniter()
opts.Config = cache.FileConfig{RootDir: confOpts.Host}
case "redis":
cfg, err := parseRedisConfig(confOpts.Host)
if err != nil {
return cache.Options{}, errors.Wrap(err, "parse redis config")
}
opts.Initer = redis.Initer()
opts.Config = cfg
default:
return cache.Options{}, errors.Errorf("unsupported adapter %q", confOpts.Adapter)
}
return opts, nil
}
func parseRedisConfig(host string) (redis.Config, error) {
cfg, err := ini.Load([]byte(strings.ReplaceAll(host, ",", "\n")))
if err != nil {
return redis.Config{}, errors.Wrap(err, "load HOST")
}
var config redis.Config
for k, v := range cfg.Section("").KeysHash() {
switch k {
case "network":
config.Options.Network = v
case "addr":
config.Options.Addr = v
case "password":
config.Options.Password = v
case "db":
n, err := strconv.Atoi(v)
if err != nil {
return redis.Config{}, errors.Wrapf(err, "parse db %q", v)
}
config.Options.DB = n
case "pool_size":
n, err := strconv.Atoi(v)
if err != nil {
return redis.Config{}, errors.Wrapf(err, "parse pool_size %q", v)
}
config.Options.PoolSize = n
case "idle_timeout":
d, err := time.ParseDuration(v + "s")
if err != nil {
return redis.Config{}, errors.Wrapf(err, "parse idle_timeout %q", v)
}
config.Options.ConnMaxIdleTime = d
case "prefix":
config.KeyPrefix = v
case "tls":
// Matches go-macaron/session/redis: any non-empty `tls=` value enables
// TLS with InsecureSkipVerify.
config.Options.TLSConfig = &tls.Config{InsecureSkipVerify: true}
case "hset_name":
// Macaron stored values in a single Redis hash named by this key,
// whereas Flamego stores per-key with KeyPrefix, so this knob has no equivalent.
default:
return redis.Config{}, errors.Errorf("unsupported redis HOST key %q", k)
}
}
return config, nil
}
+23 -14
View File
@@ -14,9 +14,10 @@ import (
"strings" "strings"
"github.com/cockroachdb/errors" "github.com/cockroachdb/errors"
"github.com/flamego/cache"
"github.com/flamego/flamego" "github.com/flamego/flamego"
"github.com/go-macaron/binding" "github.com/go-macaron/binding"
"github.com/go-macaron/cache" macaroncache "github.com/go-macaron/cache"
"github.com/go-macaron/captcha" "github.com/go-macaron/captcha"
"github.com/go-macaron/csrf" "github.com/go-macaron/csrf"
"github.com/go-macaron/gzip" "github.com/go-macaron/gzip"
@@ -36,12 +37,12 @@ import (
"gogs.io/gogs/internal/route" "gogs.io/gogs/internal/route"
"gogs.io/gogs/internal/route/admin" "gogs.io/gogs/internal/route/admin"
apiv1 "gogs.io/gogs/internal/route/api/v1" apiv1 "gogs.io/gogs/internal/route/api/v1"
"gogs.io/gogs/internal/route/dev"
"gogs.io/gogs/internal/route/lfs" "gogs.io/gogs/internal/route/lfs"
"gogs.io/gogs/internal/route/org" "gogs.io/gogs/internal/route/org"
"gogs.io/gogs/internal/route/repo" "gogs.io/gogs/internal/route/repo"
"gogs.io/gogs/internal/route/user" "gogs.io/gogs/internal/route/user"
"gogs.io/gogs/internal/template" "gogs.io/gogs/internal/template"
"gogs.io/gogs/internal/urlx"
"gogs.io/gogs/public" "gogs.io/gogs/public"
"gogs.io/gogs/templates" "gogs.io/gogs/templates"
) )
@@ -89,8 +90,6 @@ func Run(configPath string, portOverride int) error {
m.Group("/user", func() { m.Group("/user", func() {
m.Get("/sign_up", user.SignUp) m.Get("/sign_up", user.SignUp)
m.Post("/sign_up", bindIgnErr(form.Register{}), user.SignUpPost) m.Post("/sign_up", bindIgnErr(form.Register{}), user.SignUpPost)
m.Get("/reset_password", user.ResetPasswd)
m.Post("/reset_password", user.ResetPasswdPost)
}, reqSignOut) }, reqSignOut)
m.Group("/user/settings", func() { m.Group("/user/settings", func() {
@@ -137,8 +136,6 @@ func Run(configPath string, portOverride int) error {
m.Any("/activate", user.Activate) m.Any("/activate", user.Activate)
m.Any("/activate_email", user.ActivateEmail) m.Any("/activate_email", user.ActivateEmail)
m.Get("/email2user", user.Email2User) m.Get("/email2user", user.Email2User)
m.Get("/forget_password", user.ForgotPasswd)
m.Post("/forget_password", user.ForgotPasswdPost)
m.Post("/logout", user.SignOut) m.Post("/logout", user.SignOut)
}) })
// ***** END: User ***** // ***** END: User *****
@@ -229,10 +226,6 @@ func Run(configPath string, portOverride int) error {
m.Post("/action/:action", user.Action) m.Post("/action/:action", user.Action)
}, reqSignIn, context.InjectParamsUser()) }, reqSignIn, context.InjectParamsUser())
if macaron.Env == macaron.DEV {
m.Get("/template/*", dev.TemplatePreview)
}
reqRepoAdmin := context.RequireRepoAdmin() reqRepoAdmin := context.RequireRepoAdmin()
reqRepoWriter := context.RequireRepoWriter() reqRepoWriter := context.RequireRepoWriter()
@@ -689,14 +682,30 @@ func newRoutingHandler() (http.Handler, error) {
f := flamego.New() f := flamego.New()
f.Use(flamego.Recovery()) f.Use(flamego.Recovery())
mountWebAPIRoutes(f) cacherOpts, err := parseCacheOptions(conf.Cache)
if err != nil {
return nil, errors.Wrap(err, "parse cache options")
}
f.Use(cache.Cacher(cacherOpts))
if err := mountWebRoutes(f); err != nil { f.Get("/redirect", getRedirect)
return nil, errors.Wrap(err, "mount web routes")
mountWebAPIRoutes(f)
err = mountWebAppRoutes(f)
if err != nil {
return nil, errors.Wrap(err, "mount web app routes")
} }
return f, nil return f, nil
} }
func getRedirect(c flamego.Context) {
to := c.Request().URL.Query().Get("to")
if !urlx.IsSameSite(to) {
to = conf.Server.Subpath + "/"
}
c.Redirect(to, http.StatusSeeOther)
}
// newMacaron initializes Macaron instance. // newMacaron initializes Macaron instance.
func newMacaron() (*macaron.Macaron, error) { func newMacaron() (*macaron.Macaron, error) {
m := macaron.New() m := macaron.New()
@@ -780,7 +789,7 @@ func newMacaron() (*macaron.Macaron, error) {
DefaultLang: "en-US", DefaultLang: "en-US",
Redirect: true, Redirect: true,
})) }))
m.Use(cache.Cacher(cache.Options{ m.Use(macaroncache.Cacher(macaroncache.Options{
Adapter: conf.Cache.Adapter, Adapter: conf.Cache.Adapter,
AdapterConfig: conf.Cache.Host, AdapterConfig: conf.Cache.Host,
Interval: conf.Cache.Interval, Interval: conf.Cache.Interval,
+103 -23
View File
@@ -4,14 +4,16 @@ import (
stdctx "context" stdctx "context"
"encoding/json" "encoding/json"
"net/http" "net/http"
"os"
"reflect" "reflect"
"strings" "strings"
"time"
"github.com/cockroachdb/errors" "github.com/cockroachdb/errors"
"github.com/flamego/binding" "github.com/flamego/binding"
"github.com/flamego/cache"
"github.com/flamego/flamego" "github.com/flamego/flamego"
"github.com/flamego/validator" "github.com/flamego/validator"
"github.com/go-macaron/cache"
"github.com/go-macaron/i18n" "github.com/go-macaron/i18n"
"github.com/go-macaron/session" "github.com/go-macaron/session"
"gopkg.in/macaron.v1" "gopkg.in/macaron.v1"
@@ -21,7 +23,8 @@ import (
"gogs.io/gogs/internal/conf" "gogs.io/gogs/internal/conf"
"gogs.io/gogs/internal/context" "gogs.io/gogs/internal/context"
"gogs.io/gogs/internal/database" "gogs.io/gogs/internal/database"
"gogs.io/gogs/internal/urlx" "gogs.io/gogs/internal/email"
"gogs.io/gogs/internal/route/user"
"gogs.io/gogs/internal/userx" "gogs.io/gogs/internal/userx"
) )
@@ -30,17 +33,15 @@ type (
webAPISessionKey struct{} webAPISessionKey struct{}
webAPIMacaronKey struct{} webAPIMacaronKey struct{}
webAPILocaleKey struct{} webAPILocaleKey struct{}
webAPICacheKey struct{}
) )
func bridgeToWebAPI(webHandler http.Handler) func(c *context.Context, l i18n.Locale, ca cache.Cache) { func bridgeToWebAPI(webHandler http.Handler) func(c *context.Context, l i18n.Locale) {
return func(c *context.Context, l i18n.Locale, ca cache.Cache) { return func(c *context.Context, l i18n.Locale) {
ctx := c.Req.Context() ctx := c.Req.Context()
ctx = stdctx.WithValue(ctx, webAPIUserKey{}, c.User) ctx = stdctx.WithValue(ctx, webAPIUserKey{}, c.User)
ctx = stdctx.WithValue(ctx, webAPISessionKey{}, c.Session) ctx = stdctx.WithValue(ctx, webAPISessionKey{}, c.Session)
ctx = stdctx.WithValue(ctx, webAPIMacaronKey{}, c.Context) ctx = stdctx.WithValue(ctx, webAPIMacaronKey{}, c.Context)
ctx = stdctx.WithValue(ctx, webAPILocaleKey{}, l) ctx = stdctx.WithValue(ctx, webAPILocaleKey{}, l)
ctx = stdctx.WithValue(ctx, webAPICacheKey{}, ca)
webHandler.ServeHTTP(c.Resp, c.Req.WithContext(ctx)) webHandler.ServeHTTP(c.Resp, c.Req.WithContext(ctx))
} }
} }
@@ -51,8 +52,7 @@ func webAPIInjector(c flamego.Context) {
sess, _ := ctx.Value(webAPISessionKey{}).(session.Store) sess, _ := ctx.Value(webAPISessionKey{}).(session.Store)
mc, _ := ctx.Value(webAPIMacaronKey{}).(*macaron.Context) mc, _ := ctx.Value(webAPIMacaronKey{}).(*macaron.Context)
l, _ := ctx.Value(webAPILocaleKey{}).(i18n.Locale) l, _ := ctx.Value(webAPILocaleKey{}).(i18n.Locale)
ca, _ := ctx.Value(webAPICacheKey{}).(cache.Cache) c.Map(user, sess, mc, l)
c.Map(user, sess, mc, l, ca)
} }
func webAPIBodyLimiter(c flamego.Context) { func webAPIBodyLimiter(c flamego.Context) {
@@ -116,6 +116,12 @@ func mountWebAPIRoutes(f *flamego.Flame) {
f.Group("/api/web", func() { f.Group("/api/web", func() {
f.Group("/user", func() { f.Group("/user", func() {
f.Get("/info", getUserInfo) f.Get("/info", getUserInfo)
f.Group("/reset-password", func() {
f.Combo("").
Get(getUserResetPassword).
Post(bindJSON(userResetPasswordEmailRequest{}), postUserResetPassword)
f.Post("/complete", bindJSON(userResetPasswordCompleteRequest{}), postUserResetPasswordComplete)
})
f.Combo("/sign-in"). f.Combo("/sign-in").
Get(getUserSignIn). Get(getUserSignIn).
Post(bindJSON(userSignInRequest{}), postUserSignIn) Post(bindJSON(userSignInRequest{}), postUserSignIn)
@@ -128,16 +134,6 @@ func mountWebAPIRoutes(f *flamego.Flame) {
f.Post("/sign-out", postUserSignOut) f.Post("/sign-out", postUserSignOut)
}) })
}, webAPIBodyLimiter, webAPIInjector) }, webAPIBodyLimiter, webAPIInjector)
f.Get("/redirect", getRedirect)
}
func getRedirect(c flamego.Context) {
to := c.Request().URL.Query().Get("to")
if !urlx.IsSameSite(to) {
to = conf.Server.Subpath + "/"
}
c.Redirect(to, http.StatusSeeOther)
} }
// fieldErrors maps JSON field names to per-field localized messages. A non-nil // fieldErrors maps JSON field names to per-field localized messages. A non-nil
@@ -216,11 +212,11 @@ type loginSource struct {
IsDefault bool `json:"isDefault"` IsDefault bool `json:"isDefault"`
} }
type userSignInPageResponse struct { type getUserSignInResponse struct {
LoginSources []loginSource `json:"loginSources"` LoginSources []loginSource `json:"loginSources"`
} }
func getUserSignIn(r *http.Request) (statusCode int, resp *userSignInPageResponse, err error) { func getUserSignIn(r *http.Request) (statusCode int, resp *getUserSignInResponse, err error) {
sources, err := database.Handle.LoginSources().List(r.Context(), database.ListLoginSourceOptions{OnlyActivated: true}) sources, err := database.Handle.LoginSources().List(r.Context(), database.ListLoginSourceOptions{OnlyActivated: true})
if err != nil { if err != nil {
log.Error("getUserSignIn: list activated login sources: %v", err) log.Error("getUserSignIn: list activated login sources: %v", err)
@@ -230,7 +226,7 @@ func getUserSignIn(r *http.Request) (statusCode int, resp *userSignInPageRespons
for _, s := range sources { for _, s := range sources {
loginSources = append(loginSources, loginSource{ID: s.ID, Name: s.Name, IsDefault: s.IsDefault}) loginSources = append(loginSources, loginSource{ID: s.ID, Name: s.Name, IsDefault: s.IsDefault})
} }
return http.StatusOK, &userSignInPageResponse{LoginSources: loginSources}, nil return http.StatusOK, &getUserSignInResponse{LoginSources: loginSources}, nil
} }
type userSignInRequest struct { type userSignInRequest struct {
@@ -239,6 +235,87 @@ type userSignInRequest struct {
LoginSource int64 `json:"loginSource"` LoginSource int64 `json:"loginSource"`
} }
type getUserResetPasswordResponse struct {
EmailEnabled bool `json:"emailEnabled"`
Valid bool `json:"valid"`
}
func getUserResetPassword(r *http.Request) (statusCode int, resp *getUserResetPasswordResponse, err error) {
code := r.URL.Query().Get("code")
return http.StatusOK, &getUserResetPasswordResponse{
EmailEnabled: conf.Email.Enabled,
Valid: code != "" && user.VerifyUserActiveCode(code) != nil,
}, nil
}
type userResetPasswordEmailRequest struct {
Email string `json:"email" validate:"required,email,max=254"`
}
type userResetPasswordCompleteRequest struct {
Code string `json:"code" validate:"required"`
Password string `json:"password" validate:"required,min=6,max=255"`
}
type userResetPasswordResponse struct {
Hours int `json:"hours,omitempty"`
ResendLimited bool `json:"resendLimited,omitempty"`
}
func postUserResetPassword(r *http.Request, ca cache.Cache, l i18n.Locale, req userResetPasswordEmailRequest) (statusCode int, resp any, err error) {
if !conf.Email.Enabled {
return http.StatusForbidden, &bindingErrorResponse{Error: l.Tr("auth.disable_register_mail")}, nil
}
ctx := r.Context()
u, err := database.Handle.Users().GetByEmail(ctx, req.Email)
if err != nil {
if database.IsErrUserNotExist(err) {
return http.StatusOK, &userResetPasswordResponse{Hours: conf.Auth.ActivateCodeLives / 60}, nil
}
log.Error("postUserResetPassword: get user by email %q: %v", req.Email, err)
return http.StatusInternalServerError, nil, errors.Wrap(err, "get user by email")
}
if !u.IsLocal() {
msg := l.Tr("auth.non_local_account")
return http.StatusForbidden, &bindingErrorResponse{Fields: fieldErrors{"email": &msg}}, nil
}
if _, err := ca.Get(ctx, userx.MailResendCacheKey(u.ID)); err == nil {
return http.StatusOK, &userResetPasswordResponse{
Hours: conf.Auth.ActivateCodeLives / 60,
ResendLimited: true,
}, nil
} else if !errors.Is(err, os.ErrNotExist) {
log.Error("postUserResetPassword: get mail resend cache for user %q: %v", u.Name, err)
}
if err = email.SendResetPasswordMail(l, database.NewMailerUser(u)); err != nil {
log.Error("postUserResetPassword: send reset password mail to user %q: %v", u.Name, err)
}
if err = ca.Set(ctx, userx.MailResendCacheKey(u.ID), 1, 180*time.Second); err != nil {
log.Error("postUserResetPassword: put mail resend cache for user %q: %v", u.Name, err)
}
return http.StatusOK, &userResetPasswordResponse{Hours: conf.Auth.ActivateCodeLives / 60}, nil
}
func postUserResetPasswordComplete(r *http.Request, l i18n.Locale, req userResetPasswordCompleteRequest) (statusCode int, resp any, err error) {
u := user.VerifyUserActiveCode(req.Code)
if u == nil {
return http.StatusBadRequest, &bindingErrorResponse{Error: l.Tr("auth.invalid_code")}, nil
}
if err := database.Handle.Users().Update(r.Context(), u.ID, database.UpdateUserOptions{Password: &req.Password}); err != nil {
log.Error("postUserResetPasswordComplete: update password for user %q: %v", u.Name, err)
return http.StatusInternalServerError, nil, errors.Wrap(err, "update user")
}
log.Trace("User password reset: %s", u.Name)
return http.StatusNoContent, nil, nil
}
type userSignInResponse struct { type userSignInResponse struct {
// MFA is true when the account has MFA enabled and the password step // MFA is true when the account has MFA enabled and the password step
// succeeded but a second factor is still required. The client should // succeeded but a second factor is still required. The client should
@@ -324,13 +401,16 @@ func postUserMFA(r *http.Request, sess session.Store, mc *macaron.Context, ca ca
}, nil }, nil
} }
if ca.IsExist(userx.TwoFactorCacheKey(userID, req.Passcode)) { cacheKey := userx.TwoFactorCacheKey(userID, req.Passcode)
if _, err := ca.Get(r.Context(), cacheKey); err == nil {
msg := l.Tr("auth.mfa_reused_passcode") msg := l.Tr("auth.mfa_reused_passcode")
return http.StatusUnauthorized, &bindingErrorResponse{ return http.StatusUnauthorized, &bindingErrorResponse{
Fields: fieldErrors{"passcode": &msg}, Fields: fieldErrors{"passcode": &msg},
}, nil }, nil
} else if !errors.Is(err, os.ErrNotExist) {
log.Error("postUserMFA: get two factor passcode cache for user %d: %v", userID, err)
} }
if err = ca.Put(userx.TwoFactorCacheKey(userID, req.Passcode), 1, 60); err != nil { if err = ca.Set(r.Context(), cacheKey, 1, 60*time.Second); err != nil {
log.Error("postUserMFA: cache two factor passcode for user %d: %v", userID, err) log.Error("postUserMFA: cache two factor passcode for user %d: %v", userID, err)
} }
@@ -18,7 +18,7 @@ import (
"gogs.io/gogs/internal/context" "gogs.io/gogs/internal/context"
) )
func mountWebRoutes(f *flamego.Flame) error { func mountWebAppRoutes(f *flamego.Flame) error {
viteURL, err := url.Parse("http://localhost:5173") viteURL, err := url.Parse("http://localhost:5173")
if err != nil { if err != nil {
return errors.Wrap(err, "parse Vite URL") return errors.Wrap(err, "parse Vite URL")
@@ -15,7 +15,7 @@ import (
"gogs.io/gogs/public" "gogs.io/gogs/public"
) )
func mountWebRoutes(f *flamego.Flame) error { func mountWebAppRoutes(f *flamego.Flame) error {
webFS, err := fs.Sub(public.WebAssets, "dist") webFS, err := fs.Sub(public.WebAssets, "dist")
if err != nil { if err != nil {
return errors.Wrap(err, "load embedded web assets") return errors.Wrap(err, "load embedded web assets")
+15 -5
View File
@@ -193,11 +193,21 @@ prohibit_login_desc = Your account is prohibited from logging in. Please contact
resent_limit_prompt = Sorry, you already requested an activation email recently. Please wait 3 minutes then try again. resent_limit_prompt = Sorry, you already requested an activation email recently. Please wait 3 minutes then try again.
has_unconfirmed_mail = Hi %s, you have an unconfirmed email address (<b>%s</b>). If you haven't received a confirmation email or need to receive a new one, please click the button below. has_unconfirmed_mail = Hi %s, you have an unconfirmed email address (<b>%s</b>). If you haven't received a confirmation email or need to receive a new one, please click the button below.
resend_mail = Click here to resend your activation email resend_mail = Click here to resend your activation email
send_reset_mail = Click here to (re)send your password reset email send_reset_email = Send password reset email
reset_password = Reset Your Password reset_password_email_submitting = Sending password reset email...
invalid_code = Sorry, your confirmation code has expired or not valid. reset_password_email_failed = Could not send password reset email, please try again.
reset_password_helper = Click here to reset your password reset_password_email_sent = A password reset email has been sent to <email>{email}</email>, please check your inbox within <hours>{hours} hours</hours>.
password_too_short = Password length must be at least 6 characters. reset_password = Reset your password
invalid_code = The confirmation code has expired or not valid.
reset_password_submit = Reset password
reset_password_submitting = Resetting password...
reset_password_resend_limited = You already requested a password reset email recently. Please wait 3 minutes then try again.
reset_password_failed = Could not reset password, please try again.
new_password = New password
new_password_placeholder = Enter your new 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.
non_local_account = Non-local accounts cannot change passwords through Gogs. non_local_account = Non-local accounts cannot change passwords through Gogs.
[mail] [mail]
+4 -1
View File
@@ -10,6 +10,7 @@ require (
github.com/editorconfig/editorconfig-core-go/v2 v2.6.4 github.com/editorconfig/editorconfig-core-go/v2 v2.6.4
github.com/fatih/color v1.18.0 github.com/fatih/color v1.18.0
github.com/flamego/binding v1.3.0 github.com/flamego/binding v1.3.0
github.com/flamego/cache v1.5.1
github.com/flamego/flamego v1.12.0 github.com/flamego/flamego v1.12.0
github.com/flamego/validator v1.0.0 github.com/flamego/validator v1.0.0
github.com/glebarez/go-sqlite v1.21.2 github.com/glebarez/go-sqlite v1.21.2
@@ -66,6 +67,7 @@ require (
bitbucket.org/creachadair/shell v0.0.7 // indirect bitbucket.org/creachadair/shell v0.0.7 // indirect
charm.land/lipgloss/v2 v2.0.1 // indirect charm.land/lipgloss/v2 v2.0.1 // indirect
charm.land/log/v2 v2.0.0 // indirect charm.land/log/v2 v2.0.0 // indirect
filippo.io/edwards25519 v1.1.0 // indirect
github.com/Azure/go-ntlmssp v0.1.1 // indirect github.com/Azure/go-ntlmssp v0.1.1 // indirect
github.com/alecthomas/participle/v2 v2.1.4 // indirect github.com/alecthomas/participle/v2 v2.1.4 // indirect
github.com/aymerick/douceur v0.2.0 // indirect github.com/aymerick/douceur v0.2.0 // indirect
@@ -94,7 +96,7 @@ require (
github.com/go-logr/stdr v1.2.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-macaron/inject v0.0.0-20200308113650-138e5925c53b // indirect github.com/go-macaron/inject v0.0.0-20200308113650-138e5925c53b // indirect
github.com/go-redis/redis/v8 v8.11.5 // indirect github.com/go-redis/redis/v8 v8.11.5 // indirect
github.com/go-sql-driver/mysql v1.7.0 // indirect github.com/go-sql-driver/mysql v1.8.1 // indirect
github.com/gogo/protobuf v1.3.2 // indirect github.com/gogo/protobuf v1.3.2 // indirect
github.com/google/go-querystring v1.0.0 // indirect github.com/google/go-querystring v1.0.0 // indirect
github.com/gorilla/css v1.0.1 // indirect github.com/gorilla/css v1.0.1 // indirect
@@ -128,6 +130,7 @@ require (
github.com/prometheus/client_model v0.6.2 // indirect github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.66.1 // indirect github.com/prometheus/common v0.66.1 // indirect
github.com/prometheus/procfs v0.16.1 // indirect github.com/prometheus/procfs v0.16.1 // indirect
github.com/redis/go-redis/v9 v9.5.1 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/rivo/uniseg v0.4.7 // indirect github.com/rivo/uniseg v0.4.7 // indirect
github.com/rogpeppe/go-internal v1.10.0 // indirect github.com/rogpeppe/go-internal v1.10.0 // indirect
+12 -1
View File
@@ -8,6 +8,8 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMT
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.37.4 h1:glPeL3BQJsbF6aIIYfZizMwc5LTYz250bDMjttbBGAU= cloud.google.com/go v0.37.4 h1:glPeL3BQJsbF6aIIYfZizMwc5LTYz250bDMjttbBGAU=
cloud.google.com/go v0.37.4/go.mod h1:NHPJ89PdicEuT9hdPXMROBD91xc5uRDxsMtSB16k7hw= cloud.google.com/go v0.37.4/go.mod h1:NHPJ89PdicEuT9hdPXMROBD91xc5uRDxsMtSB16k7hw=
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
gitea.com/lunny/log v0.0.0-20190322053110-01b5df579c4e/go.mod h1:uJEsN4LQpeGYRCjuPXPZBClU7N5pWzGuyF4uqLpE/e0= gitea.com/lunny/log v0.0.0-20190322053110-01b5df579c4e/go.mod h1:uJEsN4LQpeGYRCjuPXPZBClU7N5pWzGuyF4uqLpE/e0=
gitea.com/lunny/nodb v0.0.0-20200923032308-3238c4655727/go.mod h1:h0OwsgcpJLSYtHcM5+Xciw9OEeuxi6ty4HDiO8C7aIY= gitea.com/lunny/nodb v0.0.0-20200923032308-3238c4655727/go.mod h1:h0OwsgcpJLSYtHcM5+Xciw9OEeuxi6ty4HDiO8C7aIY=
github.com/Azure/go-ntlmssp v0.1.1 h1:l+FM/EEMb0U9QZE7mKNEDw5Mu3mFiaa2GKOoTSsNDPw= github.com/Azure/go-ntlmssp v0.1.1 h1:l+FM/EEMb0U9QZE7mKNEDw5Mu3mFiaa2GKOoTSsNDPw=
@@ -38,6 +40,10 @@ github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6r
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc h1:biVzkmvwrH8WK8raXaxBx6fRVTlJILwEwQGL1I/ByEI= github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc h1:biVzkmvwrH8WK8raXaxBx6fRVTlJILwEwQGL1I/ByEI=
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
github.com/bradfitz/gomemcache v0.0.0-20190329173943-551aad21a668/go.mod h1:H0wQNHz2YrLsuXOZozoeDmnHXkNCRmMW0gwFWDfEZDA= github.com/bradfitz/gomemcache v0.0.0-20190329173943-551aad21a668/go.mod h1:H0wQNHz2YrLsuXOZozoeDmnHXkNCRmMW0gwFWDfEZDA=
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
@@ -102,6 +108,8 @@ github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
github.com/flamego/binding v1.3.0 h1:CPbnSuP0SxT50JR7lK2khTjcQi1oOECqRK7kbOYw91U= github.com/flamego/binding v1.3.0 h1:CPbnSuP0SxT50JR7lK2khTjcQi1oOECqRK7kbOYw91U=
github.com/flamego/binding v1.3.0/go.mod h1:xgm6FEpEKKkF8CQilK2X3MJ5kTjOTnYdz/ooFctDTdc= 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/flamego v1.12.0 h1:BS0iY6RytweVvu5j40fQJ53X2ZcUVeuQ8ZSigVkDB9A= github.com/flamego/flamego v1.12.0 h1:BS0iY6RytweVvu5j40fQJ53X2ZcUVeuQ8ZSigVkDB9A=
github.com/flamego/flamego v1.12.0/go.mod h1:MM4kNGS7SvJtwUZYb2oGySR+ncdtIvtJHsl8OhH1Ngo= github.com/flamego/flamego v1.12.0/go.mod h1:MM4kNGS7SvJtwUZYb2oGySR+ncdtIvtJHsl8OhH1Ngo=
github.com/flamego/validator v1.0.0 h1:ixuWHVgiVGp4pVGtUn/0d6HBjZJbbXfJHDNkxW+rZoY= github.com/flamego/validator v1.0.0 h1:ixuWHVgiVGp4pVGtUn/0d6HBjZJbbXfJHDNkxW+rZoY=
@@ -155,8 +163,9 @@ github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvSc
github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI= github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI=
github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo= github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo=
github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc=
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
github.com/go-xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a h1:9wScpmSP5A3Bk8V3XHWUcJmYTh+ZnlHVyc+A4oZYS3Y= github.com/go-xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a h1:9wScpmSP5A3Bk8V3XHWUcJmYTh+ZnlHVyc+A4oZYS3Y=
@@ -386,6 +395,8 @@ github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R
github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg=
github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is=
github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
github.com/redis/go-redis/v9 v9.5.1 h1:H1X4D3yHPaYrkL5X06Wh6xNVM/pX0Ft4RV0vMGvLBh8=
github.com/redis/go-redis/v9 v9.5.1/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
+8 -7
View File
@@ -85,13 +85,6 @@ var (
CSRFCookieName string `ini:"CSRF_COOKIE_NAME"` CSRFCookieName string `ini:"CSRF_COOKIE_NAME"`
} }
// Cache settings
Cache struct {
Adapter string
Interval int
Host string
}
// HTTP settings // HTTP settings
HTTP struct { HTTP struct {
AccessControlAllowOrigin string AccessControlAllowOrigin string
@@ -227,6 +220,14 @@ var (
HasRobotsTxt bool HasRobotsTxt bool
) )
type CacheOptions struct {
Adapter string
Interval int
Host string
}
var Cache CacheOptions
type AppOpts struct { type AppOpts struct {
// ⚠️ WARNING: Should only be set by the main package (i.e. "cmd/gogs/main.go"). // ⚠️ WARNING: Should only be set by the main package (i.e. "cmd/gogs/main.go").
Version string `ini:"-"` Version string `ini:"-"`
+2
View File
@@ -10,6 +10,7 @@ import (
"strings" "strings"
"github.com/cockroachdb/errors" "github.com/cockroachdb/errors"
"github.com/flamego/flamego"
"github.com/gogs/git-module" "github.com/gogs/git-module"
"gopkg.in/ini.v1" "gopkg.in/ini.v1"
"gopkg.in/macaron.v1" "gopkg.in/macaron.v1"
@@ -35,6 +36,7 @@ const (
func checkRunMode() { func checkRunMode() {
if conf.IsProdMode() { if conf.IsProdMode() {
macaron.Env = macaron.PROD macaron.Env = macaron.PROD
flamego.SetEnv(flamego.EnvTypeProd)
macaron.ColorLog = false macaron.ColorLog = false
git.SetOutput(nil) git.SetOutput(nil)
} else { } else {
+5 -117
View File
@@ -19,10 +19,8 @@ import (
) )
const ( const (
tmplUserAuthSignup = "user/auth/signup" tmplUserAuthSignup = "user/auth/signup"
TmplUserAuthActivate = "user/auth/activate" TmplUserAuthActivate = "user/auth/activate"
tmplUserAuthForgotPassword = "user/auth/forgot_passwd"
tmplUserAuthResetPassword = "user/auth/reset_passwd"
) )
func SignOut(c *context.Context) { func SignOut(c *context.Context) {
@@ -163,8 +161,8 @@ func parseUserFromCode(code string) (user *database.User) {
return nil return nil
} }
// verify active code when active account // VerifyUserActiveCode verifies an account activation or password reset code.
func verifyUserActiveCode(code string) (user *database.User) { func VerifyUserActiveCode(code string) (user *database.User) {
minutes := conf.Auth.ActivateCodeLives minutes := conf.Auth.ActivateCodeLives
if user = parseUserFromCode(code); user != nil { if user = parseUserFromCode(code); user != nil {
@@ -228,7 +226,7 @@ func Activate(c *context.Context) {
} }
// Verify code. // Verify code.
if user := verifyUserActiveCode(code); user != nil { if user := VerifyUserActiveCode(code); user != nil {
v := true v := true
err := database.Handle.Users().Update( err := database.Handle.Users().Update(
c.Req.Context(), c.Req.Context(),
@@ -273,113 +271,3 @@ func ActivateEmail(c *context.Context) {
c.RedirectSubpath("/user/settings/email") c.RedirectSubpath("/user/settings/email")
} }
func ForgotPasswd(c *context.Context) {
c.Title("auth.forgot_password")
if !conf.Email.Enabled {
c.Data["IsResetDisable"] = true
c.Success(tmplUserAuthForgotPassword)
return
}
c.Data["IsResetRequest"] = true
c.Success(tmplUserAuthForgotPassword)
}
func ForgotPasswdPost(c *context.Context) {
c.Title("auth.forgot_password")
if !conf.Email.Enabled {
c.Status(403)
return
}
c.Data["IsResetRequest"] = true
emailAddr := c.Query("email")
c.Data["Email"] = emailAddr
u, err := database.Handle.Users().GetByEmail(c.Req.Context(), emailAddr)
if err != nil {
if database.IsErrUserNotExist(err) {
c.Data["Hours"] = conf.Auth.ActivateCodeLives / 60
c.Data["IsResetSent"] = true
c.Success(tmplUserAuthForgotPassword)
return
}
c.Error(err, "get user by email")
return
}
if !u.IsLocal() {
c.FormErr("Email")
c.RenderWithErr(c.Tr("auth.non_local_account"), http.StatusForbidden, tmplUserAuthForgotPassword, nil)
return
}
if c.Cache.IsExist(userx.MailResendCacheKey(u.ID)) {
c.Data["ResendLimited"] = true
c.Success(tmplUserAuthForgotPassword)
return
}
if err = email.SendResetPasswordMail(c.Context, database.NewMailerUser(u)); err != nil {
log.Error("Failed to send reset password mail: %v", err)
}
if err = c.Cache.Put(userx.MailResendCacheKey(u.ID), 1, 180); err != nil {
log.Error("Failed to put cache key 'mail resend': %v", err)
}
c.Data["Hours"] = conf.Auth.ActivateCodeLives / 60
c.Data["IsResetSent"] = true
c.Success(tmplUserAuthForgotPassword)
}
func ResetPasswd(c *context.Context) {
c.Title("auth.reset_password")
code := c.Query("code")
if code == "" {
c.NotFound()
return
}
c.Data["Code"] = code
c.Data["IsResetForm"] = true
c.Success(tmplUserAuthResetPassword)
}
func ResetPasswdPost(c *context.Context) {
c.Title("auth.reset_password")
code := c.Query("code")
if code == "" {
c.NotFound()
return
}
c.Data["Code"] = code
if u := verifyUserActiveCode(code); u != nil {
// Validate password length.
password := c.Query("password")
if len(password) < 6 {
c.Data["IsResetForm"] = true
c.Data["Err_Password"] = true
c.RenderWithErr(c.Tr("auth.password_too_short"), http.StatusBadRequest, tmplUserAuthResetPassword, nil)
return
}
err := database.Handle.Users().Update(c.Req.Context(), u.ID, database.UpdateUserOptions{Password: &password})
if err != nil {
c.Error(err, "update user")
return
}
log.Trace("User password reset: %s", u.Name)
c.RedirectSubpath("/user/sign-in")
return
}
c.Data["IsResetFailed"] = true
c.Success(tmplUserAuthResetPassword)
}
+10
View File
@@ -7,6 +7,16 @@ import (
"unicode" "unicode"
) )
// Coalesce returns the value of the first string that is not empty.
func Coalesce(ss ...string) string {
for _, s := range ss {
if s != "" {
return s
}
}
return ""
}
// ToUpperFirst returns s with only the first Unicode letter mapped to its upper case. // ToUpperFirst returns s with only the first Unicode letter mapped to its upper case.
func ToUpperFirst(s string) string { func ToUpperFirst(s string) string {
for i, v := range s { for i, v := range s {
+17
View File
@@ -6,6 +6,23 @@ import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
func TestCoalesce(t *testing.T) {
tests := []struct {
in []string
want string
}{
{[]string{"a", "b"}, "a"},
{[]string{"", "b", "c"}, "b"},
{[]string{"", "", ""}, ""},
}
for _, test := range tests {
t.Run(test.want, func(t *testing.T) {
got := Coalesce(test.in...)
assert.Equal(t, test.want, got)
})
}
}
func TestToUpperFirst(t *testing.T) { func TestToUpperFirst(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
+2 -2
View File
@@ -127,7 +127,7 @@ tasks:
&& mv .bin/custom/conf/app.ini.tmp .bin/custom/conf/app.ini && mv .bin/custom/conf/app.ini.tmp .bin/custom/conf/app.ini
dev: dev:
command: ".bin/gogs web" script: "cd .bin && ./gogs web"
preset: "server" preset: "server"
env: env:
TTY_FORCE: "1" TTY_FORCE: "1"
@@ -137,7 +137,7 @@ tasks:
- "portless" - "portless"
prod: prod:
command: ".bin/gogs web" script: "cd .bin && ./gogs web"
preset: "server" preset: "server"
env: env:
TTY_FORCE: "1" TTY_FORCE: "1"
+1 -1
View File
@@ -8,7 +8,7 @@
<body> <body>
<p>Hi <b>{{.Username}}</b>,</p> <p>Hi <b>{{.Username}}</b>,</p>
<p>Please click the following link to reset your password within <b>{{.ResetPwdCodeLives}} hours</b>:</p> <p>Please click the following link to reset your password within <b>{{.ResetPwdCodeLives}} hours</b>:</p>
<p><a href="{{AppURL}}user/reset_password?code={{.Code}}">{{AppURL}}user/reset_password?code={{.Code}}</a></p> <p><a href="{{AppURL}}user/reset-password?code={{.Code}}">{{AppURL}}user/reset-password?code={{.Code}}</a></p>
<p>Not working? Try copying and pasting it to your browser.</p> <p>Not working? Try copying and pasting it to your browser.</p>
<p>© {{Year}} <a target="_blank" rel="noopener noreferrer" href="{{AppURL}}">{{AppName}}</a></p> <p>© {{Year}} <a target="_blank" rel="noopener noreferrer" href="{{AppURL}}">{{AppName}}</a></p>
</body> </body>
-34
View File
@@ -1,34 +0,0 @@
{{template "base/head" .}}
<div class="user forgot password">
<div class="ui middle very relaxed page grid">
<div class="column">
<form class="ui form" action="{{.Link}}" method="post">
{{.CSRFTokenHTML}}
<h2 class="ui top attached header">
{{.i18n.Tr "auth.forgot_password"}}
</h2>
<div class="ui attached segment">
{{template "base/alert" .}}
{{if .IsResetSent}}
<p>{{.i18n.Tr "auth.confirmation_mail_sent_prompt" .Email .Hours | Str2HTML}}</p>
{{else if .IsResetRequest}}
<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}}" autofocus required>
</div>
<div class="ui divider"></div>
<div class="inline field">
<label></label>
<button class="ui blue button">{{.i18n.Tr "auth.send_reset_mail"}}</button>
</div>
{{else if .IsResetDisable}}
<p class="center">{{.i18n.Tr "auth.disable_register_mail"}}</p>
{{else if .ResendLimited}}
<p class="center">{{.i18n.Tr "auth.resent_limit_prompt"}}</p>
{{end}}
</div>
</form>
</div>
</div>
</div>
{{template "base/footer" .}}
-31
View File
@@ -1,31 +0,0 @@
{{template "base/head" .}}
<div class="user reset password">
<div class="ui middle very relaxed page grid">
<div class="column">
<form class="ui form" action="{{.Link}}" method="post">
{{.CSRFTokenHTML}}
<input name="code" type="hidden" value="{{.Code}}">
<h2 class="ui top attached header">
{{.i18n.Tr "auth.reset_password"}}
</h2>
<div class="ui attached segment">
{{template "base/alert" .}}
{{if .IsResetForm}}
<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}}" autofocus required>
</div>
<div class="ui divider"></div>
<div class="inline field">
<label></label>
<button class="ui blue button">{{.i18n.Tr "auth.reset_password_helper"}}</button>
</div>
{{else}}
<p class="center">{{.i18n.Tr "auth.invalid_code"}}</p>
{{end}}
</div>
</form>
</div>
</div>
</div>
{{template "base/footer" .}}
Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 14 KiB

+18
View File
@@ -41,18 +41,36 @@ const REUSED_KEYS = [
"theme_system", "theme_system",
"username", "username",
"username_placeholder", "username_placeholder",
"email",
"password", "password",
"password_placeholder", "password_placeholder",
"auth_source", "auth_source",
"local", "local",
"remember_me", "remember_me",
"forget_password", "forget_password",
"send_reset_email",
"reset_password_email_submitting",
"reset_password_email_failed",
"reset_password_email_sent",
"disable_register_mail",
"reset_password_resend_limited",
"non_local_account",
"sign_up_now", "sign_up_now",
"sign_in_submitting", "sign_in_submitting",
"sign_in_failed", "sign_in_failed",
"show_password", "show_password",
"hide_password", "hide_password",
"back_to_sign_in", "back_to_sign_in",
"reset_password",
"invalid_code",
"reset_password_submit",
"reset_password_submitting",
"reset_password_failed",
"new_password",
"new_password_placeholder",
"confirm_new_password",
"confirm_new_password_placeholder",
"reset_password_mismatch",
"mfa_title", "mfa_title",
"mfa_passcode", "mfa_passcode",
"mfa_passcode_placeholder", "mfa_passcode_placeholder",
Binary file not shown.
+106
View File
@@ -0,0 +1,106 @@
The fonts in this directory and the fontsource packages bundled into the
built web assets are licensed under the SIL Open Font License, Version 1.1.
Copyright holders:
Geist (Sans)
Copyright 2024 The Geist Project Authors
https://github.com/vercel/geist-font
Geist Mono
Copyright 2024 The Geist Project Authors
https://github.com/vercel/geist-font
Geist Pixel
Copyright (c) 2023 Vercel, in collaboration with basement.studio
https://github.com/vercel/geist-font
The full text of the license is also available with a FAQ at:
http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION AND CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.
+72
View File
@@ -3,10 +3,19 @@
@import "@fontsource-variable/geist"; @import "@fontsource-variable/geist";
@import "@fontsource-variable/geist-mono"; @import "@fontsource-variable/geist-mono";
@font-face {
font-family: "Geist Pixel";
font-style: normal;
font-weight: 100 900;
font-display: swap;
src: url("./assets/fonts/GeistPixel-Square.woff2") format("woff2");
}
@theme inline { @theme inline {
--font-sans: --font-sans:
"Geist Variable", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", system-ui, -apple-system, sans-serif; "Geist Variable", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", system-ui, -apple-system, sans-serif;
--font-mono: "Geist Mono Variable", "PingFang SC", "Microsoft YaHei", Consolas, "Liberation Mono", Menlo, monospace; --font-mono: "Geist Mono Variable", "PingFang SC", "Microsoft YaHei", Consolas, "Liberation Mono", Menlo, monospace;
--font-pixel: "Geist Pixel", "Geist Mono Variable", Consolas, "Liberation Mono", Menlo, monospace;
} }
@custom-variant dark (&:where(.dark, .dark *)); @custom-variant dark (&:where(.dark, .dark *));
@@ -88,3 +97,66 @@
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
} }
} }
/* Flame flicker for terminal-style hover affordances: a Hermès-orange glow
that irregularly pulses, like an ember catching from underneath the text.
Pauses for users who prefer reduced motion. */
@property --flame-core {
syntax: "<color>";
inherits: true;
initial-value: #ff6b35;
}
@property --flame-halo {
syntax: "<color>";
inherits: true;
initial-value: #ff8a4c;
}
@keyframes flame-flicker {
0% {
text-shadow:
0 0 4px color-mix(in srgb, var(--flame-core) 50%, transparent),
0 0 8px color-mix(in srgb, var(--flame-halo) 25%, transparent);
}
18% {
text-shadow:
0 0 6px color-mix(in srgb, var(--flame-core) 70%, transparent),
0 0 14px color-mix(in srgb, var(--flame-halo) 40%, transparent);
}
31% {
text-shadow:
0 0 3px color-mix(in srgb, var(--flame-core) 35%, transparent),
0 0 6px color-mix(in srgb, var(--flame-halo) 18%, transparent);
}
47% {
text-shadow:
0 0 8px color-mix(in srgb, var(--flame-core) 75%, transparent),
0 0 18px color-mix(in srgb, var(--flame-halo) 45%, transparent);
}
62% {
text-shadow:
0 0 4px color-mix(in srgb, var(--flame-core) 50%, transparent),
0 0 8px color-mix(in srgb, var(--flame-halo) 25%, transparent);
}
78% {
text-shadow:
0 0 5px color-mix(in srgb, var(--flame-core) 60%, transparent),
0 0 11px color-mix(in srgb, var(--flame-halo) 32%, transparent);
}
100% {
text-shadow:
0 0 4px color-mix(in srgb, var(--flame-core) 50%, transparent),
0 0 8px color-mix(in srgb, var(--flame-halo) 25%, transparent);
}
}
@media (prefers-reduced-motion: reduce) {
@keyframes flame-flicker {
0%,
100% {
text-shadow:
0 0 4px color-mix(in srgb, var(--flame-core) 50%, transparent),
0 0 8px color-mix(in srgb, var(--flame-halo) 25%, transparent);
}
}
}
+7 -1
View File
@@ -22,10 +22,16 @@
"language": "Език", "language": "Език",
"page_not_found": "Страницата не е намерена", "page_not_found": "Страницата не е намерена",
"username": "Потребител", "username": "Потребител",
"email": "Ел. поща",
"password": "Парола", "password": "Парола",
"auth_source": "Източник за удостоверяване", "auth_source": "Източник за удостоверяване",
"local": "Локален", "local": "Локален",
"remember_me": "Запомни ме", "remember_me": "Запомни ме",
"forget_password": "Забравена парола?", "forget_password": "Забравена парола?",
"sign_up_now": "Нуждаете се от профил? Регистрирайте се сега." "disable_register_mail": "За съжаление потвърждението на регистрации е изключено.",
"non_local_account": "Нелокални потребители не могат да сменят паролата си през Gogs.",
"sign_up_now": "Нуждаете се от профил? Регистрирайте се сега.",
"reset_password": "Нулиране на паролата",
"invalid_code": "За съжаление Вашия код за потвърждение е изтекъл или е невалиден.",
"new_password": "Нова парола"
} }
+7 -1
View File
@@ -22,10 +22,16 @@
"language": "Jazyk", "language": "Jazyk",
"page_not_found": "Page Not Found", "page_not_found": "Page Not Found",
"username": "Uživatelské jméno", "username": "Uživatelské jméno",
"email": "E-mail",
"password": "Heslo", "password": "Heslo",
"auth_source": "Zdroj ověření", "auth_source": "Zdroj ověření",
"local": "Lokální", "local": "Lokální",
"remember_me": "Zapamatovat si mne", "remember_me": "Zapamatovat si mne",
"forget_password": "Zapomněli jste heslo?", "forget_password": "Zapomněli jste heslo?",
"sign_up_now": "Potřebujete účet? Zaregistrujte se." "disable_register_mail": "Omlouváme se, ale e-mailové služby jsou vypnuté. Kontaktujte správce systému.",
"non_local_account": "Externí účty nemohou měnit hesla přes Gogs.",
"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"
} }
+7 -1
View File
@@ -22,10 +22,16 @@
"language": "Sprache", "language": "Sprache",
"page_not_found": "Seite nicht gefunden", "page_not_found": "Seite nicht gefunden",
"username": "Benutzername", "username": "Benutzername",
"email": "E-Mail",
"password": "Passwort", "password": "Passwort",
"auth_source": "Authentifizierungsquelle", "auth_source": "Authentifizierungsquelle",
"local": "Lokal", "local": "Lokal",
"remember_me": "Angemeldet bleiben", "remember_me": "Angemeldet bleiben",
"forget_password": "Passwort vergessen?", "forget_password": "Passwort vergessen?",
"sign_up_now": "Benötigen Sie ein Konto? Registrieren Sie sich jetzt." "disable_register_mail": "Es tut uns leid, die Bestätigung der Registrierungs-E-Mail wurde deaktiviert.",
"non_local_account": "Nicht-lokale Konten können Passwörter nicht via Gogs ändern.",
"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"
} }
+7 -1
View File
@@ -22,10 +22,16 @@
"language": "Language", "language": "Language",
"page_not_found": "Page Not Found", "page_not_found": "Page Not Found",
"username": "Username", "username": "Username",
"email": "Email",
"password": "Password", "password": "Password",
"auth_source": "Authentication Source", "auth_source": "Authentication Source",
"local": "Local", "local": "Local",
"remember_me": "Remember Me", "remember_me": "Remember Me",
"forget_password": "Forgot password?", "forget_password": "Forgot password?",
"sign_up_now": "Need an account? Sign up now." "disable_register_mail": "Sorry, Register Mail Confirmation has been disabled.",
"non_local_account": "Non-local accounts cannot change passwords through Gogs.",
"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"
} }
+18
View File
@@ -27,17 +27,35 @@
"theme_system": "System", "theme_system": "System",
"username": "Username", "username": "Username",
"username_placeholder": "Enter your username or email", "username_placeholder": "Enter your username or email",
"email": "Email",
"password": "Password", "password": "Password",
"password_placeholder": "Enter your password", "password_placeholder": "Enter your password",
"auth_source": "Authentication source", "auth_source": "Authentication source",
"local": "Local", "local": "Local",
"forget_password": "Forgot password?", "forget_password": "Forgot password?",
"send_reset_email": "Send password reset email",
"reset_password_email_submitting": "Sending password reset email...",
"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.",
"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.",
"sign_up_now": "Create a new account", "sign_up_now": "Create a new account",
"sign_in_submitting": "Signing in...", "sign_in_submitting": "Signing in...",
"sign_in_failed": "Could not sign in, please try again.", "sign_in_failed": "Could not sign in, please try again.",
"show_password": "Show password", "show_password": "Show password",
"hide_password": "Hide password", "hide_password": "Hide password",
"back_to_sign_in": "Back to sign in", "back_to_sign_in": "Back to sign in",
"reset_password": "Reset your password",
"invalid_code": "The confirmation code has expired or not valid.",
"reset_password_submit": "Reset password",
"reset_password_submitting": "Resetting password...",
"reset_password_failed": "Could not reset password, please try again.",
"new_password": "New password",
"new_password_placeholder": "Enter your new 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.",
"mfa_title": "Multi-factor authentication", "mfa_title": "Multi-factor authentication",
"mfa_passcode": "Passcode", "mfa_passcode": "Passcode",
"mfa_passcode_placeholder": "Enter the 6-digit code from your authenticator", "mfa_passcode_placeholder": "Enter the 6-digit code from your authenticator",
+7 -1
View File
@@ -22,10 +22,16 @@
"language": "Idioma", "language": "Idioma",
"page_not_found": "Página no encontrada", "page_not_found": "Página no encontrada",
"username": "Nombre de usuario", "username": "Nombre de usuario",
"email": "Correo electrónico",
"password": "Contraseña", "password": "Contraseña",
"auth_source": "Authentication Source", "auth_source": "Authentication Source",
"local": "Local", "local": "Local",
"remember_me": "Recuérdame", "remember_me": "Recuérdame",
"forget_password": "¿Has olvidado tu contraseña?", "forget_password": "¿Has olvidado tu contraseña?",
"sign_up_now": "¿Necesitas una cuenta? Regístrate ahora." "disable_register_mail": "Lo sentimos. Los correos de Confirmación de Registro están deshabilitados.",
"non_local_account": "Cuentas que no son locales no pueden cambiar las contraseñas a través de Gogs.",
"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"
} }
+7 -1
View File
@@ -22,10 +22,16 @@
"language": "زبان", "language": "زبان",
"page_not_found": "صفحه مورد نظر یافت نشد.", "page_not_found": "صفحه مورد نظر یافت نشد.",
"username": "نام کاربری", "username": "نام کاربری",
"email": "ایمیل",
"password": "رمز عبور", "password": "رمز عبور",
"auth_source": "محل احراز هویت", "auth_source": "محل احراز هویت",
"local": "محلی", "local": "محلی",
"remember_me": "مرا به خاطر بسپار", "remember_me": "مرا به خاطر بسپار",
"forget_password": "رمز عبور خود را فراموش کرده‌اید؟", "forget_password": "رمز عبور خود را فراموش کرده‌اید؟",
"sign_up_now": "نیاز به یک حساب دارید؟ هم‌اکنون ثبت نام کنید." "disable_register_mail": "با عرض پوزش، تایید ایمیل ثبت نام غیر فعال شده است.",
"non_local_account": "حساب های کاربری غیر محلی قادر به تغییر رمز عبور از طریق Gogs نمی باشند.",
"sign_up_now": "نیاز به یک حساب دارید؟ هم‌اکنون ثبت نام کنید.",
"reset_password": "تنظیم مجدد رمز عبور",
"invalid_code": "با عرض پوزش، کد تایید شما منقضی شده است و یا معتبر نیست.",
"new_password": "رمز عبور جدید"
} }
+7 -1
View File
@@ -22,10 +22,16 @@
"language": "Kieli", "language": "Kieli",
"page_not_found": "Sivua ei löydy", "page_not_found": "Sivua ei löydy",
"username": "Käyttäjätunnus", "username": "Käyttäjätunnus",
"email": "Sähköposti",
"password": "Salasana", "password": "Salasana",
"auth_source": "Todennuslähde", "auth_source": "Todennuslähde",
"local": "Paikallinen", "local": "Paikallinen",
"remember_me": "Muista minut", "remember_me": "Muista minut",
"forget_password": "Unohtuiko salasana?", "forget_password": "Unohtuiko salasana?",
"sign_up_now": "Tarvitsetko tilin? Rekisteröidy nyt." "disable_register_mail": "Valitettavasti sähköpostipalvelut ovat poissa käytöstä. Otathan yhteyttä sivuston ylläpitoon.",
"non_local_account": "Vain paikallisten käyttäjätilien salasanan vaihto onnistuu Gogsin kautta.",
"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"
} }
+7 -1
View File
@@ -22,10 +22,16 @@
"language": "Langue", "language": "Langue",
"page_not_found": "Page non trouvée", "page_not_found": "Page non trouvée",
"username": "Nom d'utilisateur", "username": "Nom d'utilisateur",
"email": "E-mail",
"password": "Mot de passe", "password": "Mot de passe",
"auth_source": "Sources d'authentification", "auth_source": "Sources d'authentification",
"local": "Locale", "local": "Locale",
"remember_me": "Se souvenir de moi", "remember_me": "Se souvenir de moi",
"forget_password": "Mot de passe oublié ?", "forget_password": "Mot de passe oublié ?",
"sign_up_now": "Pas de compte ? Inscrivez-vous maintenant." "disable_register_mail": "Désolé, la confirmation par courriel des enregistrements a été désactivée.",
"non_local_account": "Les comptes non locaux ne peuvent pas changer leur mot de passe via Gogs.",
"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"
} }
+7 -1
View File
@@ -22,10 +22,16 @@
"language": "Idioma", "language": "Idioma",
"page_not_found": "Page Not Found", "page_not_found": "Page Not Found",
"username": "Nome da persoa usuaria", "username": "Nome da persoa usuaria",
"email": "Correo electrónico",
"password": "Contrasinal", "password": "Contrasinal",
"auth_source": "Fonte de Autenticación", "auth_source": "Fonte de Autenticación",
"local": "Configuración rexional", "local": "Configuración rexional",
"remember_me": "Recórdame", "remember_me": "Recórdame",
"forget_password": "Esqueciches o teu contrasinal?", "forget_password": "Esqueciches o teu contrasinal?",
"sign_up_now": "Necesitas unha conta? Rexístrate agora." "disable_register_mail": "Sentímolo. Os correos de confirmación de rexistro están deshabilitados.",
"non_local_account": "Contas que non son locais non poden cambiar os contrasinais a través de Gogs.",
"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"
} }
+7 -1
View File
@@ -22,10 +22,16 @@
"language": "Nyelv", "language": "Nyelv",
"page_not_found": "Az oldal nem található", "page_not_found": "Az oldal nem található",
"username": "Felhasználónév", "username": "Felhasználónév",
"email": "E-mail",
"password": "Jelszó", "password": "Jelszó",
"auth_source": "Hitelesítési forrás", "auth_source": "Hitelesítési forrás",
"local": "Helyi", "local": "Helyi",
"remember_me": "Emlékezz rám", "remember_me": "Emlékezz rám",
"forget_password": "Elfelejtette a jelszavát?", "forget_password": "Elfelejtette a jelszavát?",
"sign_up_now": "Szeretne bejelentkezni? Regisztráljon most." "disable_register_mail": "Elnézést, az email regisztráció megerősítését kikapcsolták.",
"non_local_account": "Nem helyi felhasználó nem cserélhet jelszót a Gogsban.",
"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ó"
} }
+7 -1
View File
@@ -22,10 +22,16 @@
"language": "Bahasa", "language": "Bahasa",
"page_not_found": "Halaman tidak ditemukan", "page_not_found": "Halaman tidak ditemukan",
"username": "Nama pengguna", "username": "Nama pengguna",
"email": "Email",
"password": "Sandi", "password": "Sandi",
"auth_source": "Sumber Autentikasi", "auth_source": "Sumber Autentikasi",
"local": "Lokal", "local": "Lokal",
"remember_me": "Ingat saya", "remember_me": "Ingat saya",
"forget_password": "Lupa sandi?", "forget_password": "Lupa sandi?",
"sign_up_now": "Membutuhkan akun? Daftar sekarang." "disable_register_mail": "Maaf, konfirmasi pendaftaran melalui email telah dinonaktifkan.",
"non_local_account": "Akun non-lokal tidak dapat mengganti password lewat Gogs.",
"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"
} }
+7 -1
View File
@@ -22,10 +22,16 @@
"language": "Lingua", "language": "Lingua",
"page_not_found": "Pagina Non Trovata", "page_not_found": "Pagina Non Trovata",
"username": "Nome utente", "username": "Nome utente",
"email": "E-mail",
"password": "Password", "password": "Password",
"auth_source": "Fonte di autenticazione", "auth_source": "Fonte di autenticazione",
"local": "Locale", "local": "Locale",
"remember_me": "Ricordami", "remember_me": "Ricordami",
"forget_password": "Password dimenticata?", "forget_password": "Password dimenticata?",
"sign_up_now": "Bisogno di un account? Iscriviti ora." "disable_register_mail": "Siamo spiacenti, la conferma di registrazione via Mail è stata disattivata.",
"non_local_account": "Gli account non locali non possono modificare le password tramite Gogs.",
"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"
} }
+7 -1
View File
@@ -22,10 +22,16 @@
"language": "言語", "language": "言語",
"page_not_found": "ページが見つかりません", "page_not_found": "ページが見つかりません",
"username": "ユーザー名", "username": "ユーザー名",
"email": "メールアドレス",
"password": "パスワード", "password": "パスワード",
"auth_source": "認証ソース", "auth_source": "認証ソース",
"local": "ローカル", "local": "ローカル",
"remember_me": "ログインしたままにする", "remember_me": "ログインしたままにする",
"forget_password": "パスワードを忘れましたか?", "forget_password": "パスワードを忘れましたか?",
"sign_up_now": "アカウントが必要ですか?今すぐ登録しましょう!" "disable_register_mail": "申し訳ありませんが、登録メールの確認機能が無効になっています。",
"non_local_account": "非ローカルアカウントではGogs経由でのパスワード変更はできません。",
"sign_up_now": "アカウントが必要ですか?今すぐ登録しましょう!",
"reset_password": "パスワードリセット",
"invalid_code": "申し訳ありませんが、確認用コードが期限切れまたは無効です。",
"new_password": "新しいパスワード"
} }
+7 -1
View File
@@ -22,10 +22,16 @@
"language": "언어", "language": "언어",
"page_not_found": "페이지를 찾을 수 없음", "page_not_found": "페이지를 찾을 수 없음",
"username": "사용자명", "username": "사용자명",
"email": "이메일",
"password": "비밀번호", "password": "비밀번호",
"auth_source": "인증 소스 편집", "auth_source": "인증 소스 편집",
"local": "로컬", "local": "로컬",
"remember_me": "자동 로그인", "remember_me": "자동 로그인",
"forget_password": "비밀번호를 잊으셨습니까?", "forget_password": "비밀번호를 잊으셨습니까?",
"sign_up_now": "계정이 필요하신가요? 지금 가입하세요." "disable_register_mail": "죄송합니다. 메일 등록이 비활성화 되었습니다.",
"non_local_account": "Gogs 계정이 아니면 암호를 변경할 수 없습니다.",
"sign_up_now": "계정이 필요하신가요? 지금 가입하세요.",
"reset_password": "비밀번호 초기화",
"invalid_code": "죄송합니다. 확인 코드가 만료되었거나 유효하지 않습니다.",
"new_password": "새 비밀번호"
} }
+7 -1
View File
@@ -22,10 +22,16 @@
"language": "Valoda", "language": "Valoda",
"page_not_found": "Page Not Found", "page_not_found": "Page Not Found",
"username": "Lietotājvārds", "username": "Lietotājvārds",
"email": "E-pasts",
"password": "Parole", "password": "Parole",
"auth_source": "Autentificēšanas avots", "auth_source": "Autentificēšanas avots",
"local": "Local", "local": "Local",
"remember_me": "Atcerēties mani", "remember_me": "Atcerēties mani",
"forget_password": "Aizmirsi paroli?", "forget_password": "Aizmirsi paroli?",
"sign_up_now": "Nepieciešams konts? Reģistrējies tagad." "disable_register_mail": "Atvainojiet, reģistrācijas e-pasta apstiprināšana ir atspējota.",
"non_local_account": "Tikai lokālie konti var nomainīt savu paroli Gogs.",
"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"
} }
+7 -1
View File
@@ -22,10 +22,16 @@
"language": "Хэл", "language": "Хэл",
"page_not_found": "Хуудас олдсонгүй", "page_not_found": "Хуудас олдсонгүй",
"username": "Нэвтрэх нэр", "username": "Нэвтрэх нэр",
"email": "Имэйл",
"password": "Нууц үг", "password": "Нууц үг",
"auth_source": "Баталгаажуулалтын эх сурвалж", "auth_source": "Баталгаажуулалтын эх сурвалж",
"local": "Локал", "local": "Локал",
"remember_me": "Сануулах", "remember_me": "Сануулах",
"forget_password": "Нууц үг сэргээх?", "forget_password": "Нууц үг сэргээх?",
"sign_up_now": "Данс үүсгэх бол? Одоо бүртгүүлнэ үү." "disable_register_mail": "Уучлаарай, имэйлийн үйлчилгээ идэвхгүй байна. Сайтын админтай холбоо барина уу.",
"non_local_account": "Гадаад хэрэглэгчид нууц үгээ солих боломжгүй.",
"sign_up_now": "Данс үүсгэх бол? Одоо бүртгүүлнэ үү.",
"reset_password": "Нууц үгээ сэргээх",
"invalid_code": "Уучлаарай, таны баталгаажуулах кодын хугацаа дууссан эсвэл хүчин төгөлдөр бус байна.",
"new_password": "Шинэ нууц үг"
} }
+7 -1
View File
@@ -22,10 +22,16 @@
"language": "Taal", "language": "Taal",
"page_not_found": "Pagina niet gevonden", "page_not_found": "Pagina niet gevonden",
"username": "Gebruikersnaam", "username": "Gebruikersnaam",
"email": "E-mail",
"password": "Wachtwoord", "password": "Wachtwoord",
"auth_source": "Authenticatiebron", "auth_source": "Authenticatiebron",
"local": "Lokaal", "local": "Lokaal",
"remember_me": "Onthoud mij", "remember_me": "Onthoud mij",
"forget_password": "Wachtwoord vergeten?", "forget_password": "Wachtwoord vergeten?",
"sign_up_now": "Een account nodig? Meld u nu aan." "disable_register_mail": "Sorry, bevestiging van registratie per e-mail is uitgeschakeld.",
"non_local_account": "Niet lokale accounts mogen hun wachtwoord niet veranderen via Gogs.",
"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"
} }
+7 -1
View File
@@ -22,10 +22,16 @@
"language": "Język", "language": "Język",
"page_not_found": "Strona nie została znaleziona", "page_not_found": "Strona nie została znaleziona",
"username": "Nazwa użytkownika", "username": "Nazwa użytkownika",
"email": "E-mail",
"password": "Hasło", "password": "Hasło",
"auth_source": "Źródło uwierzytelniania", "auth_source": "Źródło uwierzytelniania",
"local": "Lokalne", "local": "Lokalne",
"remember_me": "Zapamiętaj mnie", "remember_me": "Zapamiętaj mnie",
"forget_password": "Zapomniałeś hasła?", "forget_password": "Zapomniałeś hasła?",
"sign_up_now": "Potrzebujesz konta? Zarejestruj się teraz." "disable_register_mail": "Przepraszamy, potwierdzenia rejestracji zostały wyłączone przez administratora.",
"non_local_account": "Nie lokalne konta nie mogą zmieniać haseł przez Gogs.",
"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"
} }
+7 -1
View File
@@ -22,10 +22,16 @@
"language": "Idioma", "language": "Idioma",
"page_not_found": "Página Não Encontrada", "page_not_found": "Página Não Encontrada",
"username": "Usuário", "username": "Usuário",
"email": "E-mail",
"password": "Senha", "password": "Senha",
"auth_source": "Fonte de autenticação", "auth_source": "Fonte de autenticação",
"local": "Local", "local": "Local",
"remember_me": "Lembrar de mim", "remember_me": "Lembrar de mim",
"forget_password": "Esqueceu a senha?", "forget_password": "Esqueceu a senha?",
"sign_up_now": "Precisa de uma conta? Cadastre-se agora." "disable_register_mail": "Desculpe, a confirmação de registro por e-mail foi desabilitada.",
"non_local_account": "Não é possível mudar a senha de contas remotas pelo Gogs.",
"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"
} }
+7 -1
View File
@@ -22,10 +22,16 @@
"language": "Língua", "language": "Língua",
"page_not_found": "Página Não Encontrada", "page_not_found": "Página Não Encontrada",
"username": "Nome de utilizador", "username": "Nome de utilizador",
"email": "Endereço de email",
"password": "Palavra-chave", "password": "Palavra-chave",
"auth_source": "Tipo de autenticação", "auth_source": "Tipo de autenticação",
"local": "Local", "local": "Local",
"remember_me": "Manter sessão iniciada", "remember_me": "Manter sessão iniciada",
"forget_password": "Esqueceu a sua senha?", "forget_password": "Esqueceu a sua senha?",
"sign_up_now": "Precisa de uma conta? Inscreva-se agora." "disable_register_mail": "Desculpe, os serviços de email estão desativados. Por favor contacte o administrador.",
"non_local_account": "Contas não-locais não podem mudar a palavra-passe através do Gogs.",
"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"
} }
+7 -1
View File
@@ -22,10 +22,16 @@
"language": "Limba", "language": "Limba",
"page_not_found": "Pagina nu a fost găsită", "page_not_found": "Pagina nu a fost găsită",
"username": "Numele de utilizator", "username": "Numele de utilizator",
"email": "E-mail",
"password": "Parolă", "password": "Parolă",
"auth_source": "Sursa de autentificare", "auth_source": "Sursa de autentificare",
"local": "Local", "local": "Local",
"remember_me": "Ține-mă minte", "remember_me": "Ține-mă minte",
"forget_password": "Ați uitat parola?", "forget_password": "Ați uitat parola?",
"sign_up_now": "Nevoie de un cont? Inscrie-te acum." "disable_register_mail": "Ne pare rău, serviciile de e-mail sunt dezactivate. Vă rugăm să contactați administratorul site-ului.",
"non_local_account": "Conturile non-locale nu pot schimba parolele prin Gogs.",
"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ă"
} }
+7 -1
View File
@@ -22,10 +22,16 @@
"language": "Язык", "language": "Язык",
"page_not_found": "Страница не найдена", "page_not_found": "Страница не найдена",
"username": "Имя пользователя", "username": "Имя пользователя",
"email": "Эл. почта",
"password": "Пароль", "password": "Пароль",
"auth_source": "Тип аутентификации", "auth_source": "Тип аутентификации",
"local": "Локальный", "local": "Локальный",
"remember_me": "Запомнить меня", "remember_me": "Запомнить меня",
"forget_password": "Забыли пароль?", "forget_password": "Забыли пароль?",
"sign_up_now": "Нужен аккаунт? Зарегистрируйтесь." "disable_register_mail": "К сожалению подтверждение регистрации по почте отключено.",
"non_local_account": "Нелокальные аккаунты не могут изменить пароль через Gogs.",
"sign_up_now": "Нужен аккаунт? Зарегистрируйтесь.",
"reset_password": "Сброс пароля",
"invalid_code": "Извините, ваш код подтверждения истек или не является допустимым.",
"new_password": "Новый пароль"
} }
+7 -1
View File
@@ -22,10 +22,16 @@
"language": "Jazyk", "language": "Jazyk",
"page_not_found": "Page Not Found", "page_not_found": "Page Not Found",
"username": "Používateľské meno", "username": "Používateľské meno",
"email": "E-mail",
"password": "Heslo", "password": "Heslo",
"auth_source": "Zdroj overovania", "auth_source": "Zdroj overovania",
"local": "Lokálny", "local": "Lokálny",
"remember_me": "Zapamätať prihlásenie", "remember_me": "Zapamätať prihlásenie",
"forget_password": "Zabudli ste heslo?", "forget_password": "Zabudli ste heslo?",
"sign_up_now": "Potrebujete účet? Zaregistrujte sa teraz." "disable_register_mail": "Ospravedlňujeme sa, potvrdenie registračného e-mailu bolo vypnuté.",
"non_local_account": "Miestne účty nemôžu meniť heslá cez Gogs.",
"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"
} }
+7 -1
View File
@@ -22,10 +22,16 @@
"language": "Језик", "language": "Језик",
"page_not_found": "Page Not Found", "page_not_found": "Page Not Found",
"username": "Корисничко име", "username": "Корисничко име",
"email": "E-пошта",
"password": "Лозинка", "password": "Лозинка",
"auth_source": "Извор аутентикације", "auth_source": "Извор аутентикације",
"local": "Локално", "local": "Локално",
"remember_me": "Запамти ме", "remember_me": "Запамти ме",
"forget_password": "Заборавили сте лозинку?", "forget_password": "Заборавили сте лозинку?",
"sign_up_now": "Немате налог? Пријавите се." "disable_register_mail": "Извините, потврда путем поште је онемогућено.",
"non_local_account": "Нелокални налози не могу да промените лозинку преко Gogs.",
"sign_up_now": "Немате налог? Пријавите се.",
"reset_password": "Ресет лозинке",
"invalid_code": "Извините, ваш код за потврду је истекао или није валидан.",
"new_password": "Нова лозинка"
} }
+7 -1
View File
@@ -22,10 +22,16 @@
"language": "Språk", "language": "Språk",
"page_not_found": "Sidan hittades inte", "page_not_found": "Sidan hittades inte",
"username": "Användarnamn", "username": "Användarnamn",
"email": "E-post",
"password": "Lösenord", "password": "Lösenord",
"auth_source": "Autentiseringskälla", "auth_source": "Autentiseringskälla",
"local": "Lokal", "local": "Lokal",
"remember_me": "Kom ihåg mig", "remember_me": "Kom ihåg mig",
"forget_password": "Glömt lösenordet?", "forget_password": "Glömt lösenordet?",
"sign_up_now": "Behöver du ett konto? Registrera dig nu." "disable_register_mail": "Tyvärr så är registreringsbekräftelemailutskick inaktiverat.",
"non_local_account": "Icke-lokala konton får inte ändra lösenord genom Gogs.",
"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"
} }
+7 -1
View File
@@ -22,10 +22,16 @@
"language": "Dil", "language": "Dil",
"page_not_found": "Sayfa Bulunamadı", "page_not_found": "Sayfa Bulunamadı",
"username": "Kullanıcı Adı", "username": "Kullanıcı Adı",
"email": "E-Posta",
"password": "Parola", "password": "Parola",
"auth_source": "Yetkilendirme Kaynağı", "auth_source": "Yetkilendirme Kaynağı",
"local": "Yerel", "local": "Yerel",
"remember_me": "Beni Hatırla", "remember_me": "Beni Hatırla",
"forget_password": "Parolanızı mı unuttunuz?", "forget_password": "Parolanızı mı unuttunuz?",
"sign_up_now": "Bir hesaba mı ihtiyacınız var? Şimdi kaydolun." "disable_register_mail": "Üzgünüz, kayıt doğrulama e-postası devre dışı bırakıldı.",
"non_local_account": "Yerel olmayan hesapların şifrelerini Gogs aracılığıyla değiştiremezsiniz.",
"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"
} }
+7 -1
View File
@@ -22,10 +22,16 @@
"language": "Мова", "language": "Мова",
"page_not_found": "Сторінку не знайдено", "page_not_found": "Сторінку не знайдено",
"username": "Ім'я користувача", "username": "Ім'я користувача",
"email": "Електронна пошта",
"password": "Пароль", "password": "Пароль",
"auth_source": "Джерело автентифікації", "auth_source": "Джерело автентифікації",
"local": "Локальний", "local": "Локальний",
"remember_me": "Запам'ятати мене", "remember_me": "Запам'ятати мене",
"forget_password": "Забули пароль?", "forget_password": "Забули пароль?",
"sign_up_now": "Потрібен обліковий запис? Зареєструватися зараз." "disable_register_mail": "На жаль, підтвердження реєстрації на електрону пошту вимкнено адміністратором.",
"non_local_account": "Нелокальні облікові записи не можуть змінити пароль через Gogs.",
"sign_up_now": "Потрібен обліковий запис? Зареєструватися зараз.",
"reset_password": "Скинути пароль",
"invalid_code": "На жаль, код підтвердження, закінчився або помилковий.",
"new_password": "Новий пароль"
} }
+7 -1
View File
@@ -22,10 +22,16 @@
"language": "Ngôn ngữ", "language": "Ngôn ngữ",
"page_not_found": "Không tìm thấy trang này!", "page_not_found": "Không tìm thấy trang này!",
"username": "Username", "username": "Username",
"email": "Email",
"password": "Mật khẩu", "password": "Mật khẩu",
"auth_source": "Authentication Source", "auth_source": "Authentication Source",
"local": "Local", "local": "Local",
"remember_me": "Ghi nhớ tôi", "remember_me": "Ghi nhớ tôi",
"forget_password": "Quên mật khẩu?", "forget_password": "Quên mật khẩu?",
"sign_up_now": "Cần một tài khoản? Đăng ký bây giờ." "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.",
"non_local_account": "Tài khoản Non-local không thể thay đổi mật khẩu thông qua Gogs.",
"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"
} }
+7 -1
View File
@@ -22,10 +22,16 @@
"language": "语言选项", "language": "语言选项",
"page_not_found": "页面未找到", "page_not_found": "页面未找到",
"username": "用户名", "username": "用户名",
"email": "邮箱",
"password": "密码", "password": "密码",
"auth_source": "认证源", "auth_source": "认证源",
"local": "本地", "local": "本地",
"remember_me": "记住登录", "remember_me": "记住登录",
"forget_password": "忘记密码?", "forget_password": "忘记密码?",
"sign_up_now": "还没帐户?马上注册。" "disable_register_mail": "对不起,注册邮箱确认功能已被关闭。",
"non_local_account": "非本地类型的帐户无法通过 Gogs 修改密码。",
"sign_up_now": "还没帐户?马上注册。",
"reset_password": "重置密码",
"invalid_code": "对不起,您的确认代码已过期或已失效。",
"new_password": "新的密码"
} }
+7 -1
View File
@@ -22,10 +22,16 @@
"language": "語言", "language": "語言",
"page_not_found": "Page Not Found", "page_not_found": "Page Not Found",
"username": "用戶名稱", "username": "用戶名稱",
"email": "電子郵件",
"password": "密碼", "password": "密碼",
"auth_source": "Authentication Source", "auth_source": "Authentication Source",
"local": "Local", "local": "Local",
"remember_me": "記住登錄", "remember_me": "記住登錄",
"forget_password": "忘記密碼?", "forget_password": "忘記密碼?",
"sign_up_now": "還沒帳戶?馬上註冊。" "disable_register_mail": "對不起,註冊郵箱確認功能已被關閉。",
"non_local_account": "Non-local accounts cannot change passwords through Gogs.",
"sign_up_now": "還沒帳戶?馬上註冊。",
"reset_password": "重置密碼",
"invalid_code": "對不起,您的確認代碼已過期或已失效。",
"new_password": "新的密碼"
} }
+7 -1
View File
@@ -22,10 +22,16 @@
"language": "語言", "language": "語言",
"page_not_found": "找不到頁面", "page_not_found": "找不到頁面",
"username": "用戶名稱", "username": "用戶名稱",
"email": "電子郵件",
"password": "密碼", "password": "密碼",
"auth_source": "認證來源", "auth_source": "認證來源",
"local": "本地", "local": "本地",
"remember_me": "記住登錄", "remember_me": "記住登錄",
"forget_password": "忘記密碼?", "forget_password": "忘記密碼?",
"sign_up_now": "還沒帳戶?馬上註冊。" "disable_register_mail": "對不起,註冊郵箱確認功能已被關閉。",
"non_local_account": "非本地帳戶無法通過 Gogs 修改密碼。",
"sign_up_now": "還沒帳戶?馬上註冊。",
"reset_password": "重置密碼",
"invalid_code": "對不起,您的確認代碼已過期或已失效。",
"new_password": "新的密碼"
} }
+11 -11
View File
@@ -17,26 +17,26 @@ export function Landing() {
<span className="size-2.5 rounded-full bg-(--color-foreground)/20" /> <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> <span className="ml-2 text-xs text-(--color-muted-foreground) sm:ml-3">gogs zsh</span>
</div> </div>
<pre className="px-4 py-4 text-xs leading-relaxed break-all whitespace-pre-wrap text-(--color-foreground) sm:px-5 sm:py-5 sm:text-sm"> <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 className="text-(--color-muted-foreground)">$ </span>
<span>cat /etc/motd</span> <span>cat /etc/motd</span>
{"\n"} {"\n"}
<img <img
src={subUrl("/img/banner-light.svg")} src={subUrl("/img/banner-light.png")}
alt="Gogs" alt="Gogs"
width="775" width="200"
height="294" height="76"
className="mx-auto block max-w-[280px] dark:hidden sm:max-w-sm" className="mx-auto block h-auto w-[280px] [image-rendering:pixelated] dark:hidden sm:w-96"
/> />
<img <img
src={subUrl("/img/banner-dark.svg")} src={subUrl("/img/banner-dark.png")}
alt="Gogs" alt="Gogs"
width="775" width="200"
height="294" height="76"
className="mx-auto hidden max-w-[280px] dark:block sm:max-w-sm" className="mx-auto hidden h-auto w-[280px] [image-rendering:pixelated] dark:block sm:w-96"
/> />
{"\n"} {"\n"}
<span className="block text-center font-sans text-base text-(--color-muted-foreground) sm:text-lg"> <span className="block text-center text-base text-(--color-muted-foreground) sm:text-lg">
{t("app_desc")} {t("app_desc")}
</span> </span>
{"\n"} {"\n"}
@@ -75,7 +75,7 @@ function CmdLink({
spa?: boolean; spa?: boolean;
}) { }) {
const className = const className =
"group inline-flex items-baseline gap-2 rounded-sm hover:bg-(--color-surface) hover:text-(--color-foreground)"; "group inline-flex items-baseline gap-2 rounded-sm hover:text-(--color-foreground) hover:[animation:flame-flicker_2.4s_ease-in-out_infinite]";
const inner = ( const inner = (
<> <>
<span className="inline-block w-16 text-(--color-foreground) sm:w-20">{cmd}</span> <span className="inline-block w-16 text-(--color-foreground) sm:w-20">{cmd}</span>
+1 -1
View File
@@ -16,7 +16,7 @@ export function NotFound() {
<span className="size-2.5 rounded-full bg-(--color-foreground)/20" /> <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> <span className="ml-2 text-xs text-(--color-muted-foreground) sm:ml-3">gogs zsh</span>
</div> </div>
<pre className="px-4 py-4 text-xs leading-relaxed break-all whitespace-pre-wrap text-(--color-foreground) sm:px-5 sm:py-5 sm:text-sm"> <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 className="text-(--color-muted-foreground)">$ </span>
<span>gogs show {path}</span> <span>gogs show {path}</span>
{"\n"} {"\n"}
+324
View File
@@ -0,0 +1,324 @@
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 { 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 ResetPasswordPage {
code: string;
emailEnabled: boolean;
valid: boolean;
}
interface ResetPasswordResponse {
hours?: number;
resendLimited?: boolean;
}
interface ResetPasswordErrorResponse {
error?: string;
fields?: Record<string, string | null>;
}
const route = getRouteApi("/user/reset-password");
export function ResetPassword() {
const { t } = useTranslation();
const navigate = useNavigate();
const { code, emailEnabled, valid } = route.useLoaderData();
const isResetForm = code !== "";
usePageTitle(t("reset_password"));
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [confirmPassword, setConfirmPassword] = useState("");
const [showPassword, setShowPassword] = useState(false);
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
const [submitting, setSubmitting] = useState(false);
const [sent, setSent] = useState<ResetPasswordResponse | null>(null);
const [formError, setFormError] = useState<string | null>(null);
const [fieldErrors, setFieldErrors] = useState<Record<string, string | null>>({});
const emailRef = useRef<HTMLInputElement>(null);
const passwordRef = useRef<HTMLInputElement>(null);
const confirmPasswordRef = useRef<HTMLInputElement>(null);
function onSubmit(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault();
if (isResetForm && !valid) return;
if (!isResetForm && !emailEnabled) return;
if (isResetForm && password !== confirmPassword) {
setFormError(null);
setFieldErrors({ password: null, confirmPassword: t("reset_password_mismatch") });
requestAnimationFrame(() => confirmPasswordRef.current?.focus());
return;
}
setFormError(null);
setFieldErrors({});
setSubmitting(true);
void (async () => {
try {
const res = await fetch(
subUrl(isResetForm ? "/api/web/user/reset-password/complete" : "/api/web/user/reset-password"),
{
method: "POST",
credentials: "same-origin",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(isResetForm ? { code, password } : { email }),
},
);
if (!res.ok) {
const body = (await res.json().catch(() => ({}))) as ResetPasswordErrorResponse;
if (body.error) setFormError(body.error);
if (body.fields) setFieldErrors(body.fields);
if (!body.error && !body.fields) {
setFormError(t(isResetForm ? "reset_password_failed" : "reset_password_email_failed"));
}
setSubmitting(false);
requestAnimationFrame(() => {
if (isResetForm) passwordRef.current?.focus();
else emailRef.current?.focus();
});
return;
}
if (isResetForm) {
await navigate({ to: "/user/sign-in" });
return;
}
setSent((await res.json()) as ResetPasswordResponse);
setSubmitting(false);
} catch {
setFormError(t(isResetForm ? "reset_password_failed" : "reset_password_email_failed"));
setSubmitting(false);
}
})();
}
const title = t("reset_password");
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>{title}</CardTitle>
</CardHeader>
<CardContent className="pt-2">{isResetForm ? renderResetContent() : renderRequestContent()}</CardContent>
</Card>
</main>
);
function renderRequestContent() {
if (!emailEnabled) {
return (
<p role="alert" className="text-center text-sm text-(--color-destructive)">
{t("disable_register_mail")}
</p>
);
}
if (sent) {
return (
<div className="flex flex-col gap-4 text-center">
<p role="status" className="text-sm text-(--color-foreground)">
{sent.resendLimited ? (
t("reset_password_resend_limited")
) : (
<Trans
i18nKey="reset_password_email_sent"
values={{ email, hours: sent.hours }}
components={{ email: <b />, hours: <b /> }}
/>
)}
</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">
{renderFormError()}
<div className="flex flex-col gap-4">
<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
autoFocus
tabIndex={1}
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>
<FormActions
submitLabel={submitting ? t("reset_password_email_submitting") : t("send_reset_email")}
submitTabIndex={3}
/>
</div>
</fieldset>
</form>
);
}
function renderResetContent() {
if (!valid) {
return (
<div className="flex flex-col gap-4 text-center">
<p role="alert" className="text-sm text-(--color-destructive)">
{t("invalid_code")}
</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">
{renderFormError()}
<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>
{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_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>
{fieldErrors.confirmPassword && (
<p id="confirmPassword-error" className="text-sm text-(--color-destructive)">
{fieldErrors.confirmPassword}
</p>
)}
</div>
<FormActions
submitLabel={submitting ? t("reset_password_submitting") : t("reset_password_submit")}
submitTabIndex={5}
/>
</div>
</fieldset>
</form>
);
}
function renderFormError() {
if (!formError) return null;
return (
<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>
);
}
function FormActions({ submitLabel, submitTabIndex }: { submitLabel: string; submitTabIndex: number }) {
return (
<div className="mt-2 flex flex-col gap-3">
<Button type="submit" disabled={submitting} tabIndex={submitTabIndex} className="w-full">
{submitLabel}
</Button>
<Button variant="link" size="inline" asChild className="self-center">
<a
href={subUrl("/user/sign-in")}
tabIndex={submitting ? -1 : submitTabIndex + 1}
aria-disabled={submitting || undefined}
className={submitting ? "pointer-events-none opacity-50" : undefined}
onClick={(e) => {
if (submitting) e.preventDefault();
}}
>
{t("back_to_sign_in")}
</a>
</Button>
</div>
);
}
}
+1 -1
View File
@@ -155,7 +155,7 @@ export function SignIn() {
<Label htmlFor="password">{t("password")}</Label> <Label htmlFor="password">{t("password")}</Label>
<Button variant="link" size="inline" asChild> <Button variant="link" size="inline" asChild>
<a <a
href={subUrl("/user/forget_password")} href={subUrl("/user/reset-password")}
tabIndex={submitting ? -1 : 6} tabIndex={submitting ? -1 : 6}
aria-disabled={submitting || undefined} aria-disabled={submitting || undefined}
className={submitting ? "pointer-events-none opacity-50" : undefined} className={submitting ? "pointer-events-none opacity-50" : undefined}
+33 -14
View File
@@ -15,6 +15,7 @@ import type { UserInfo } from "@/lib/user-info";
import { Landing } from "@/pages/Landing"; import { Landing } from "@/pages/Landing";
import { MFA } from "@/pages/MFA"; import { MFA } from "@/pages/MFA";
import { NotFound } from "@/pages/NotFound"; import { NotFound } from "@/pages/NotFound";
import { ResetPassword, type ResetPasswordPage } from "@/pages/ResetPassword";
import { SignIn, type SignInPage } from "@/pages/SignIn"; import { SignIn, type SignInPage } from "@/pages/SignIn";
interface RouterContext { interface RouterContext {
@@ -42,22 +43,21 @@ const landingRoute = createRoute({
component: Landing, component: Landing,
}); });
function requireUnauthenticated({ context }: { context: RouterContext }) {
if (!context.user) return;
// Bounce authenticated visits to "/" via full navigation so the server-rendered
// dashboard handler runs.
window.location.assign(subUrl("/"));
// The thrown redirect is a sentinel to halt loader execution;
// the document-level navigation above is what actually moves the user.
// eslint-disable-next-line @typescript-eslint/only-throw-error -- TanStack's redirect() returns a sentinel that must be thrown.
throw redirect({ to: "/", replace: true });
}
const signInRoute = createRoute({ const signInRoute = createRoute({
getParentRoute: () => rootRoute, getParentRoute: () => rootRoute,
path: "/user/sign-in", path: "/user/sign-in",
beforeLoad: ({ context }) => { beforeLoad: requireUnauthenticated,
if (context.user) {
// Full navigation to "/" so the server-rendered dashboard handler runs.
// A client-side TanStack redirect would render the SPA's "/" route
// (Landing, the anon page) and make an authed user look signed out.
window.location.assign(subUrl("/"));
// Throw to halt loader execution. TanStack treats the thrown redirect
// as a sentinel; we never reach a SPA navigation because the line
// above already started a document-level one.
// eslint-disable-next-line @typescript-eslint/only-throw-error -- TanStack's redirect() returns a sentinel that must be thrown.
throw redirect({ to: "/", replace: true });
}
},
loader: async (): Promise<SignInPage> => { loader: async (): Promise<SignInPage> => {
const res = await fetch(subUrl("/api/web/user/sign-in"), { credentials: "same-origin" }); const res = await fetch(subUrl("/api/web/user/sign-in"), { credentials: "same-origin" });
if (!res.ok) { if (!res.ok) {
@@ -68,6 +68,25 @@ const signInRoute = createRoute({
component: SignIn, component: SignIn,
}); });
const resetPasswordRoute = createRoute({
getParentRoute: () => rootRoute,
path: "/user/reset-password",
beforeLoad: requireUnauthenticated,
loader: async (): Promise<ResetPasswordPage> => {
const code = new URLSearchParams(window.location.search).get("code") ?? "";
const url = code
? subUrl("/api/web/user/reset-password") + "?code=" + encodeURIComponent(code)
: subUrl("/api/web/user/reset-password");
const res = await fetch(url, { credentials: "same-origin" });
if (!res.ok) {
return { code, emailEnabled: false, valid: false };
}
const data = (await res.json()) as { emailEnabled: boolean; valid: boolean };
return { code, emailEnabled: data.emailEnabled, valid: data.valid };
},
component: ResetPassword,
});
const mfaRoute = createRoute({ const mfaRoute = createRoute({
getParentRoute: () => rootRoute, getParentRoute: () => rootRoute,
path: "/user/mfa", path: "/user/mfa",
@@ -85,7 +104,7 @@ const mfaRoute = createRoute({
component: MFA, component: MFA,
}); });
const routeTree = rootRoute.addChildren([landingRoute, signInRoute, mfaRoute]); const routeTree = rootRoute.addChildren([landingRoute, signInRoute, resetPasswordRoute, mfaRoute]);
function makeRouter(context: RouterContext) { function makeRouter(context: RouterContext) {
return createRouter({ return createRouter({