mirror of
https://github.com/gogs/gogs.git
synced 2026-05-28 21:30:36 +00:00
web: move password reset to React (#8290)
This commit is contained in:
@@ -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
|
||||||
|
}
|
||||||
@@ -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
@@ -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")
|
||||||
@@ -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]
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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=
|
||||||
|
|||||||
@@ -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:"-"`
|
||||||
|
|||||||
@@ -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
@@ -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)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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" .}}
|
|
||||||
@@ -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 |
@@ -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.
@@ -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.
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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": "Нова парола"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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": "رمز عبور جدید"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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ó"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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": "新しいパスワード"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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": "새 비밀번호"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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": "Шинэ нууц үг"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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ă"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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": "Новый пароль"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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": "Нова лозинка"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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": "Новий пароль"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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": "新的密码"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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": "新的密碼"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
@@ -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>
|
||||||
|
|||||||
@@ -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"}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
@@ -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({
|
||||||
|
|||||||
Reference in New Issue
Block a user