From 4935e7a63bd8d9d103c9852a06d418b6a8b597f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=B4=8A=E1=B4=8F=E1=B4=87=20=E1=B4=84=CA=9C=E1=B4=87?= =?UTF-8?q?=C9=B4?= Date: Sat, 23 May 2026 21:55:22 -0400 Subject: [PATCH] web: move password reset to React (#8290) --- cmd/gogs/internal/web/cache.go | 89 +++++ cmd/gogs/internal/web/web.go | 37 +- cmd/gogs/internal/web/webapi.go | 126 +++++-- .../web/{web_dev.go => webapp_dev.go} | 2 +- .../web/{web_prod.go => webapp_prod.go} | 2 +- conf/locale/locale_en-US.ini | 20 +- go.mod | 5 +- go.sum | 13 +- internal/conf/static.go | 15 +- internal/route/install.go | 2 + internal/route/user/auth.go | 122 +------ internal/strx/strx.go | 10 + internal/strx/strx_test.go | 17 + moon.yml | 4 +- templates/mail/auth/reset_passwd.tmpl | 2 +- templates/user/auth/forgot_passwd.tmpl | 34 -- templates/user/auth/reset_passwd.tmpl | 31 -- web/public/img/banner-dark.png | Bin 0 -> 5422 bytes web/public/img/banner-dark.svg | 22 -- web/public/img/banner-light.png | Bin 0 -> 5751 bytes web/public/img/banner-light.svg | 22 -- web/scripts/extract-locales.mjs | 18 + web/src/assets/fonts/GeistPixel-Square.woff2 | Bin 0 -> 28636 bytes web/src/assets/fonts/LICENSE.txt | 106 ++++++ web/src/index.css | 72 ++++ web/src/locales/bg-BG.json | 8 +- web/src/locales/cs-CZ.json | 8 +- web/src/locales/de-DE.json | 8 +- web/src/locales/en-GB.json | 8 +- web/src/locales/en-US.json | 18 + web/src/locales/es-ES.json | 8 +- web/src/locales/fa-IR.json | 8 +- web/src/locales/fi-FI.json | 8 +- web/src/locales/fr-FR.json | 8 +- web/src/locales/gl-ES.json | 8 +- web/src/locales/hu-HU.json | 8 +- web/src/locales/id-ID.json | 8 +- web/src/locales/it-IT.json | 8 +- web/src/locales/ja-JP.json | 8 +- web/src/locales/ko-KR.json | 8 +- web/src/locales/lv-LV.json | 8 +- web/src/locales/mn-MN.json | 8 +- web/src/locales/nl-NL.json | 8 +- web/src/locales/pl-PL.json | 8 +- web/src/locales/pt-BR.json | 8 +- web/src/locales/pt-PT.json | 8 +- web/src/locales/ro-RO.json | 8 +- web/src/locales/ru-RU.json | 8 +- web/src/locales/sk-SK.json | 8 +- web/src/locales/sr-SP.json | 8 +- web/src/locales/sv-SE.json | 8 +- web/src/locales/tr-TR.json | 8 +- web/src/locales/uk-UA.json | 8 +- web/src/locales/vi-VN.json | 8 +- web/src/locales/zh-CN.json | 8 +- web/src/locales/zh-HK.json | 8 +- web/src/locales/zh-TW.json | 8 +- web/src/pages/Landing.tsx | 22 +- web/src/pages/NotFound.tsx | 2 +- web/src/pages/ResetPassword.tsx | 324 ++++++++++++++++++ web/src/pages/SignIn.tsx | 2 +- web/src/router.tsx | 47 ++- 62 files changed, 1094 insertions(+), 340 deletions(-) create mode 100644 cmd/gogs/internal/web/cache.go rename cmd/gogs/internal/web/{web_dev.go => webapp_dev.go} (97%) rename cmd/gogs/internal/web/{web_prod.go => webapp_prod.go} (97%) delete mode 100644 templates/user/auth/forgot_passwd.tmpl delete mode 100644 templates/user/auth/reset_passwd.tmpl create mode 100644 web/public/img/banner-dark.png delete mode 100644 web/public/img/banner-dark.svg create mode 100644 web/public/img/banner-light.png delete mode 100644 web/public/img/banner-light.svg create mode 100644 web/src/assets/fonts/GeistPixel-Square.woff2 create mode 100644 web/src/assets/fonts/LICENSE.txt create mode 100644 web/src/pages/ResetPassword.tsx diff --git a/cmd/gogs/internal/web/cache.go b/cmd/gogs/internal/web/cache.go new file mode 100644 index 000000000..eeaa20939 --- /dev/null +++ b/cmd/gogs/internal/web/cache.go @@ -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 +} diff --git a/cmd/gogs/internal/web/web.go b/cmd/gogs/internal/web/web.go index 218360fef..d508e431e 100644 --- a/cmd/gogs/internal/web/web.go +++ b/cmd/gogs/internal/web/web.go @@ -14,9 +14,10 @@ import ( "strings" "github.com/cockroachdb/errors" + "github.com/flamego/cache" "github.com/flamego/flamego" "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/csrf" "github.com/go-macaron/gzip" @@ -36,12 +37,12 @@ import ( "gogs.io/gogs/internal/route" "gogs.io/gogs/internal/route/admin" 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/org" "gogs.io/gogs/internal/route/repo" "gogs.io/gogs/internal/route/user" "gogs.io/gogs/internal/template" + "gogs.io/gogs/internal/urlx" "gogs.io/gogs/public" "gogs.io/gogs/templates" ) @@ -89,8 +90,6 @@ func Run(configPath string, portOverride int) error { m.Group("/user", func() { m.Get("/sign_up", user.SignUp) m.Post("/sign_up", bindIgnErr(form.Register{}), user.SignUpPost) - m.Get("/reset_password", user.ResetPasswd) - m.Post("/reset_password", user.ResetPasswdPost) }, reqSignOut) m.Group("/user/settings", func() { @@ -137,8 +136,6 @@ func Run(configPath string, portOverride int) error { m.Any("/activate", user.Activate) m.Any("/activate_email", user.ActivateEmail) m.Get("/email2user", user.Email2User) - m.Get("/forget_password", user.ForgotPasswd) - m.Post("/forget_password", user.ForgotPasswdPost) m.Post("/logout", user.SignOut) }) // ***** END: User ***** @@ -229,10 +226,6 @@ func Run(configPath string, portOverride int) error { m.Post("/action/:action", user.Action) }, reqSignIn, context.InjectParamsUser()) - if macaron.Env == macaron.DEV { - m.Get("/template/*", dev.TemplatePreview) - } - reqRepoAdmin := context.RequireRepoAdmin() reqRepoWriter := context.RequireRepoWriter() @@ -689,14 +682,30 @@ func newRoutingHandler() (http.Handler, error) { f := flamego.New() 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 { - return nil, errors.Wrap(err, "mount web routes") + f.Get("/redirect", getRedirect) + + mountWebAPIRoutes(f) + err = mountWebAppRoutes(f) + if err != nil { + return nil, errors.Wrap(err, "mount web app routes") } 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. func newMacaron() (*macaron.Macaron, error) { m := macaron.New() @@ -780,7 +789,7 @@ func newMacaron() (*macaron.Macaron, error) { DefaultLang: "en-US", Redirect: true, })) - m.Use(cache.Cacher(cache.Options{ + m.Use(macaroncache.Cacher(macaroncache.Options{ Adapter: conf.Cache.Adapter, AdapterConfig: conf.Cache.Host, Interval: conf.Cache.Interval, diff --git a/cmd/gogs/internal/web/webapi.go b/cmd/gogs/internal/web/webapi.go index 5a1d92dbf..bc61a9cba 100644 --- a/cmd/gogs/internal/web/webapi.go +++ b/cmd/gogs/internal/web/webapi.go @@ -4,14 +4,16 @@ import ( stdctx "context" "encoding/json" "net/http" + "os" "reflect" "strings" + "time" "github.com/cockroachdb/errors" "github.com/flamego/binding" + "github.com/flamego/cache" "github.com/flamego/flamego" "github.com/flamego/validator" - "github.com/go-macaron/cache" "github.com/go-macaron/i18n" "github.com/go-macaron/session" "gopkg.in/macaron.v1" @@ -21,7 +23,8 @@ import ( "gogs.io/gogs/internal/conf" "gogs.io/gogs/internal/context" "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" ) @@ -30,17 +33,15 @@ type ( webAPISessionKey struct{} webAPIMacaronKey struct{} webAPILocaleKey struct{} - webAPICacheKey struct{} ) -func bridgeToWebAPI(webHandler http.Handler) func(c *context.Context, l i18n.Locale, ca cache.Cache) { - return 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) { ctx := c.Req.Context() ctx = stdctx.WithValue(ctx, webAPIUserKey{}, c.User) ctx = stdctx.WithValue(ctx, webAPISessionKey{}, c.Session) ctx = stdctx.WithValue(ctx, webAPIMacaronKey{}, c.Context) ctx = stdctx.WithValue(ctx, webAPILocaleKey{}, l) - ctx = stdctx.WithValue(ctx, webAPICacheKey{}, ca) webHandler.ServeHTTP(c.Resp, c.Req.WithContext(ctx)) } } @@ -51,8 +52,7 @@ func webAPIInjector(c flamego.Context) { sess, _ := ctx.Value(webAPISessionKey{}).(session.Store) mc, _ := ctx.Value(webAPIMacaronKey{}).(*macaron.Context) l, _ := ctx.Value(webAPILocaleKey{}).(i18n.Locale) - ca, _ := ctx.Value(webAPICacheKey{}).(cache.Cache) - c.Map(user, sess, mc, l, ca) + c.Map(user, sess, mc, l) } func webAPIBodyLimiter(c flamego.Context) { @@ -116,6 +116,12 @@ func mountWebAPIRoutes(f *flamego.Flame) { f.Group("/api/web", func() { f.Group("/user", func() { 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"). Get(getUserSignIn). Post(bindJSON(userSignInRequest{}), postUserSignIn) @@ -128,16 +134,6 @@ func mountWebAPIRoutes(f *flamego.Flame) { f.Post("/sign-out", postUserSignOut) }) }, 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 @@ -216,11 +212,11 @@ type loginSource struct { IsDefault bool `json:"isDefault"` } -type userSignInPageResponse struct { +type getUserSignInResponse struct { 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}) if err != nil { 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 { 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 { @@ -239,6 +235,87 @@ type userSignInRequest struct { 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 { // MFA is true when the account has MFA enabled and the password step // 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 } - 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") return http.StatusUnauthorized, &bindingErrorResponse{ Fields: fieldErrors{"passcode": &msg}, }, 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) } diff --git a/cmd/gogs/internal/web/web_dev.go b/cmd/gogs/internal/web/webapp_dev.go similarity index 97% rename from cmd/gogs/internal/web/web_dev.go rename to cmd/gogs/internal/web/webapp_dev.go index 635e335ec..8b1cff6ff 100644 --- a/cmd/gogs/internal/web/web_dev.go +++ b/cmd/gogs/internal/web/webapp_dev.go @@ -18,7 +18,7 @@ import ( "gogs.io/gogs/internal/context" ) -func mountWebRoutes(f *flamego.Flame) error { +func mountWebAppRoutes(f *flamego.Flame) error { viteURL, err := url.Parse("http://localhost:5173") if err != nil { return errors.Wrap(err, "parse Vite URL") diff --git a/cmd/gogs/internal/web/web_prod.go b/cmd/gogs/internal/web/webapp_prod.go similarity index 97% rename from cmd/gogs/internal/web/web_prod.go rename to cmd/gogs/internal/web/webapp_prod.go index 35f711cbd..f6088bd12 100644 --- a/cmd/gogs/internal/web/web_prod.go +++ b/cmd/gogs/internal/web/webapp_prod.go @@ -15,7 +15,7 @@ import ( "gogs.io/gogs/public" ) -func mountWebRoutes(f *flamego.Flame) error { +func mountWebAppRoutes(f *flamego.Flame) error { webFS, err := fs.Sub(public.WebAssets, "dist") if err != nil { return errors.Wrap(err, "load embedded web assets") diff --git a/conf/locale/locale_en-US.ini b/conf/locale/locale_en-US.ini index 8e9890ff2..c32c18d6c 100644 --- a/conf/locale/locale_en-US.ini +++ b/conf/locale/locale_en-US.ini @@ -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. has_unconfirmed_mail = Hi %s, you have an unconfirmed email address (%s). 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 -send_reset_mail = Click here to (re)send your password reset email -reset_password = Reset Your Password -invalid_code = Sorry, your confirmation code has expired or not valid. -reset_password_helper = Click here to reset your password -password_too_short = Password length must be at least 6 characters. +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}, please check your inbox within {hours} hours. +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. [mail] diff --git a/go.mod b/go.mod index ffeadd9e0..9e33cb4a2 100644 --- a/go.mod +++ b/go.mod @@ -10,6 +10,7 @@ require ( github.com/editorconfig/editorconfig-core-go/v2 v2.6.4 github.com/fatih/color v1.18.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/validator v1.0.0 github.com/glebarez/go-sqlite v1.21.2 @@ -66,6 +67,7 @@ require ( bitbucket.org/creachadair/shell v0.0.7 // indirect charm.land/lipgloss/v2 v2.0.1 // 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/alecthomas/participle/v2 v2.1.4 // 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-macaron/inject v0.0.0-20200308113650-138e5925c53b // 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/google/go-querystring v1.0.0 // 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/common v0.66.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/rivo/uniseg v0.4.7 // indirect github.com/rogpeppe/go-internal v1.10.0 // indirect diff --git a/go.sum b/go.sum index 06c04598d..8e0852f09 100644 --- a/go.sum +++ b/go.sum @@ -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.37.4 h1:glPeL3BQJsbF6aIIYfZizMwc5LTYz250bDMjttbBGAU= 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/nodb v0.0.0-20200923032308-3238c4655727/go.mod h1:h0OwsgcpJLSYtHcM5+Xciw9OEeuxi6ty4HDiO8C7aIY= 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/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= 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.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= 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/flamego/binding v1.3.0 h1:CPbnSuP0SxT50JR7lK2khTjcQi1oOECqRK7kbOYw91U= github.com/flamego/binding v1.3.0/go.mod h1:xgm6FEpEKKkF8CQilK2X3MJ5kTjOTnYdz/ooFctDTdc= +github.com/flamego/cache v1.5.1 h1:2B4QhLFV7je0oUMCVKsAGAT+OyDHlXhozOoUffm+O3s= +github.com/flamego/cache v1.5.1/go.mod h1:cTWYm/Ls35KKHo8vwcKgTlJUNXswEhzFWqVCTFzj24s= github.com/flamego/flamego v1.12.0 h1:BS0iY6RytweVvu5j40fQJ53X2ZcUVeuQ8ZSigVkDB9A= github.com/flamego/flamego v1.12.0/go.mod h1:MM4kNGS7SvJtwUZYb2oGySR+ncdtIvtJHsl8OhH1Ngo= 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/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.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.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-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= @@ -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/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= 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/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= diff --git a/internal/conf/static.go b/internal/conf/static.go index f18aebaab..b5228b8d0 100644 --- a/internal/conf/static.go +++ b/internal/conf/static.go @@ -85,13 +85,6 @@ var ( CSRFCookieName string `ini:"CSRF_COOKIE_NAME"` } - // Cache settings - Cache struct { - Adapter string - Interval int - Host string - } - // HTTP settings HTTP struct { AccessControlAllowOrigin string @@ -227,6 +220,14 @@ var ( HasRobotsTxt bool ) +type CacheOptions struct { + Adapter string + Interval int + Host string +} + +var Cache CacheOptions + type AppOpts struct { // ⚠️ WARNING: Should only be set by the main package (i.e. "cmd/gogs/main.go"). Version string `ini:"-"` diff --git a/internal/route/install.go b/internal/route/install.go index 380f36b94..6945ce716 100644 --- a/internal/route/install.go +++ b/internal/route/install.go @@ -10,6 +10,7 @@ import ( "strings" "github.com/cockroachdb/errors" + "github.com/flamego/flamego" "github.com/gogs/git-module" "gopkg.in/ini.v1" "gopkg.in/macaron.v1" @@ -35,6 +36,7 @@ const ( func checkRunMode() { if conf.IsProdMode() { macaron.Env = macaron.PROD + flamego.SetEnv(flamego.EnvTypeProd) macaron.ColorLog = false git.SetOutput(nil) } else { diff --git a/internal/route/user/auth.go b/internal/route/user/auth.go index d198cff89..c16322aec 100644 --- a/internal/route/user/auth.go +++ b/internal/route/user/auth.go @@ -19,10 +19,8 @@ import ( ) const ( - tmplUserAuthSignup = "user/auth/signup" - TmplUserAuthActivate = "user/auth/activate" - tmplUserAuthForgotPassword = "user/auth/forgot_passwd" - tmplUserAuthResetPassword = "user/auth/reset_passwd" + tmplUserAuthSignup = "user/auth/signup" + TmplUserAuthActivate = "user/auth/activate" ) func SignOut(c *context.Context) { @@ -163,8 +161,8 @@ func parseUserFromCode(code string) (user *database.User) { return nil } -// verify active code when active account -func verifyUserActiveCode(code string) (user *database.User) { +// VerifyUserActiveCode verifies an account activation or password reset code. +func VerifyUserActiveCode(code string) (user *database.User) { minutes := conf.Auth.ActivateCodeLives if user = parseUserFromCode(code); user != nil { @@ -228,7 +226,7 @@ func Activate(c *context.Context) { } // Verify code. - if user := verifyUserActiveCode(code); user != nil { + if user := VerifyUserActiveCode(code); user != nil { v := true err := database.Handle.Users().Update( c.Req.Context(), @@ -273,113 +271,3 @@ func ActivateEmail(c *context.Context) { 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) -} diff --git a/internal/strx/strx.go b/internal/strx/strx.go index dd425278c..91b6c8252 100644 --- a/internal/strx/strx.go +++ b/internal/strx/strx.go @@ -7,6 +7,16 @@ import ( "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. func ToUpperFirst(s string) string { for i, v := range s { diff --git a/internal/strx/strx_test.go b/internal/strx/strx_test.go index ed64acdb0..e1090a91a 100644 --- a/internal/strx/strx_test.go +++ b/internal/strx/strx_test.go @@ -6,6 +6,23 @@ import ( "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) { tests := []struct { name string diff --git a/moon.yml b/moon.yml index 2354b876c..ff79e00e3 100644 --- a/moon.yml +++ b/moon.yml @@ -127,7 +127,7 @@ tasks: && mv .bin/custom/conf/app.ini.tmp .bin/custom/conf/app.ini dev: - command: ".bin/gogs web" + script: "cd .bin && ./gogs web" preset: "server" env: TTY_FORCE: "1" @@ -137,7 +137,7 @@ tasks: - "portless" prod: - command: ".bin/gogs web" + script: "cd .bin && ./gogs web" preset: "server" env: TTY_FORCE: "1" diff --git a/templates/mail/auth/reset_passwd.tmpl b/templates/mail/auth/reset_passwd.tmpl index cbca9f48b..f6ec21363 100644 --- a/templates/mail/auth/reset_passwd.tmpl +++ b/templates/mail/auth/reset_passwd.tmpl @@ -8,7 +8,7 @@

Hi {{.Username}},

Please click the following link to reset your password within {{.ResetPwdCodeLives}} hours:

-

{{AppURL}}user/reset_password?code={{.Code}}

+

{{AppURL}}user/reset-password?code={{.Code}}

Not working? Try copying and pasting it to your browser.

© {{Year}} {{AppName}}

diff --git a/templates/user/auth/forgot_passwd.tmpl b/templates/user/auth/forgot_passwd.tmpl deleted file mode 100644 index 9fada7fe3..000000000 --- a/templates/user/auth/forgot_passwd.tmpl +++ /dev/null @@ -1,34 +0,0 @@ -{{template "base/head" .}} -
-
-
-
- {{.CSRFTokenHTML}} -

- {{.i18n.Tr "auth.forgot_password"}} -

-
- {{template "base/alert" .}} - {{if .IsResetSent}} -

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

- {{else if .IsResetRequest}} -
- - -
-
-
- - -
- {{else if .IsResetDisable}} -

{{.i18n.Tr "auth.disable_register_mail"}}

- {{else if .ResendLimited}} -

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

- {{end}} -
-
-
-
-
-{{template "base/footer" .}} diff --git a/templates/user/auth/reset_passwd.tmpl b/templates/user/auth/reset_passwd.tmpl deleted file mode 100644 index 801679e8c..000000000 --- a/templates/user/auth/reset_passwd.tmpl +++ /dev/null @@ -1,31 +0,0 @@ -{{template "base/head" .}} -
-
-
-
- {{.CSRFTokenHTML}} - -

- {{.i18n.Tr "auth.reset_password"}} -

-
- {{template "base/alert" .}} - {{if .IsResetForm}} -
- - -
-
-
- - -
- {{else}} -

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

- {{end}} -
-
-
-
-
-{{template "base/footer" .}} diff --git a/web/public/img/banner-dark.png b/web/public/img/banner-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..a7e317e68c34e1ffec7341bf08e803121c44a3a9 GIT binary patch literal 5422 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 literal 0 HcmV?d00001 diff --git a/web/public/img/banner-dark.svg b/web/public/img/banner-dark.svg deleted file mode 100644 index 81c617ae2..000000000 --- a/web/public/img/banner-dark.svg +++ /dev/null @@ -1,22 +0,0 @@ - - - logo组合-反白-1 - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/web/public/img/banner-light.png b/web/public/img/banner-light.png new file mode 100644 index 0000000000000000000000000000000000000000..5a1bf651eaaff612174384e1e319609793cd4a13 GIT binary patch literal 5751 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 D2*T2m literal 0 HcmV?d00001 diff --git a/web/public/img/banner-light.svg b/web/public/img/banner-light.svg deleted file mode 100644 index 1df64b9bc..000000000 --- a/web/public/img/banner-light.svg +++ /dev/null @@ -1,22 +0,0 @@ - - - logo组合-normal - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/web/scripts/extract-locales.mjs b/web/scripts/extract-locales.mjs index f9b5d8557..b88b64585 100644 --- a/web/scripts/extract-locales.mjs +++ b/web/scripts/extract-locales.mjs @@ -41,18 +41,36 @@ const REUSED_KEYS = [ "theme_system", "username", "username_placeholder", + "email", "password", "password_placeholder", "auth_source", "local", "remember_me", "forget_password", + "send_reset_email", + "reset_password_email_submitting", + "reset_password_email_failed", + "reset_password_email_sent", + "disable_register_mail", + "reset_password_resend_limited", + "non_local_account", "sign_up_now", "sign_in_submitting", "sign_in_failed", "show_password", "hide_password", "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_passcode", "mfa_passcode_placeholder", diff --git a/web/src/assets/fonts/GeistPixel-Square.woff2 b/web/src/assets/fonts/GeistPixel-Square.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..422184d43961fb539928c0c86b86de0270fbaf2c GIT binary patch literal 28636 zcmV(_K-9l?Pew8T0RR910B_s?5&!@I0di0P0B>^u0RR9100000000000000000000 z0000QfgT&2SR9T-24Db+E(n1D37iZO2nvMJ41}~&0X7081CIa;he7}ZAU|zobYUO` ziXsP?aSVYyTNht(1l_jlmEe`$te%HE&U9RuaL0r0fUYsn%!AJ5 z@ij@X!ifln!s&u#1y1;jeMevj#&uyKe%gMb-W!b$SNzrTJgTI=RYcOgQ6NeeTbM>d z=@n$T$K#KGN(-tui+TdCm_@Zoey~dV%-Ldx+FW?yHvcy=;(%>TQiL&WYQhQh`)*fHy!I7wsUaKq`=?UjbBs zoFclcfIX26N4{bJRUoHj9ZLCW`U3185{k#A?OzrgobX?9@7tWEeS{IqAdC?~1hI&q z>%Q(Qr94B-rq+EDPN}IGv=|A>VEYqPwrEw|GkXd71Yyup#9Leov4# z;5(?~yyp%04l4J6x8Sp=z-JKs6>Gq!hz%+Y_%7xf_>W)v-7{~+zxaw_ zn$lD%Arlh*_r7K2x%UbEl}c48V7f=32HPu?0w}Y+tcdA|muB%(m>G`(?b&OtMS2b$ zmq7|{IxS;*k|-i<&7!WbjVI9a1go2a51O-or6lDI;Oqf_yAPzu;RxeO0CPq$h$4s~ zZn#)pU4C0W0jA4FIhz*@7{sDX`;b0AK@g!W-MQn{gJfb=c9IJF303)do$31C|5AOH z1O7oL1)?>)7jT1Rk-j9`(w7dT^MQ0^JLn78cOg0m6XEkzm-n%$@M(c z7nDxnLQI`Q4wXwcotxJGzpt~}d%p-@6JevwCT;#3Vo~MTZn;Gd_rK4_|AT}jkXOh! ze1T^MkG*3dC*h#Eh=`ZEj?3I4_EDm zqO8~NyTyC^QJMZ3umVhQ-)uJ>7-%M0wrtoegrFKbMS>Ai-}gRqR%J?Jr*rMm7$JlZ zMlr%Tjdyd+H`g_0RpF$ht1Tc|MM1?pw;z4am-NQ=bN4YbRTU8x74bwxoewhC*Mh$B zH@9>hND)ve0TKeFg0t;zf4}qvdfc0{DN>9q!z@kDpC5V7j4*id;_r7ngD)bF$Y%v& zC95zLum!P={fL8nj`)%<5nrnuQUxWGQc7dAKnsu-X%W&AEkRnY}cW2fa8TDQTYTrAv9oTUT!ue>k zdGfb3fTaiLr0S#8Aa_c^6e>*hVf<*z|F8y+J4ol9ix5Tn81bp)kL0FQQoVu^XZdy5 z!V9EKWtbMqCw_<q^n`RP zdZn)(`N5e>T^zB}#p%JsHB(R7Q|@v?dWgVvq(rYZ3}tL$hR@2+NN4P7I(U`R=)ndu zt~oP8!dm1_mAY7sM)!PYyKB!GlNsHpM!V}!wIsz<*uCi#ttf2*xDGzAzVEuGJx19X z%8cRE*l|X8uZCc0m^bFe_R?4EWju&yun50~AC4w_=?OW3$IzVMlzvqp|to)emk*cNjvD^T-X7U7y z=0wo#3D)oU6FHl_1y5e9UVn;B3t6d!v#gdN%y>F-B!&4Vj%r~F`p7eh{D zIYb09I|8=)Q$W0W(Cs*^PC5s|h4z@lnNx!~Pdd>P$4`w>Dlb-fqCmRk_|dpB9t;Rt z%;mC_Pt6n2&E?%`C8BL_@gYhkwpms07Txx+Jb4UC*4#xJ>&ll?)eXAN4!MbN;Z1W@+N zZ=RL2*fv(u9!b1?)&p+f3(+(OyIoapAu7HnK6ZozA|>l>6!dP3y~Rq~67=IXVotbm zr7Ee7%|A0CoKS%q(hT6S-x1YA_7S%S`RhI*J|{$l5i~~s%iXRgz9fDuI%vEx8{L=% z)SxC`CxHfnvClqY3nlc=gl3({H@oVvh}crC_Xf?wz#<7yc}@y_~!zyX7RMFGWe02H7*L5bLy z$`k_Q)`8e1WF-VhxBQJrGOG(N|WQ?IVq~g((J0WDiJdr}7)-a&~fR)_g0g6J) zob;bB2hSizobO>E6V?&P3C=5DIj(SrC%oZPzU5c`l}#QRTUiAwqWDs)S%b1`U-$Ye z+vh5c;rD+193TGgpJSV_wErW|fr9@~s0KD%EQqiz=dng%579&P#N20xzjQ(<9Pw;& zo(oQ+p5I_y1B31`uSe|E3l>zwM4x_GNnei|<45qLRssTKm0$rf|6>5D1Rc_*zQz{% zC@@6!vU~-wi8OZ_w+>FBYcA&u)dg1evJMl@~3;1x{aK{5ryzs^cU;HS8 z%m(bpoWKW}v0NK;W(P=QBe_|56-VM^%zJs30J z^bBmY%gQ+gTaj|5}RV<(?!svv*e$C$I&S;A75v78kca|tIU zlvtILs#-D&nO(??1_HblTsh*-`q^yS4g<2WCy1=2h1X|BJ-QdrtF<3aoQtj%z z0cHa2bsDqr5_fsvX)eLzC)FMuK`KC6gD`hixC1k|Y}bzRwcJ#&5dAuq!A3WI{LG)y z(M4kg6rm9X5RGM_uyS_lXy_Slbno+nMTitFMl3U0rbZWj%444JG=Gxks(?|TzRQh* z6v-%9QH&}ono)>i7*$d%BZuM`g;tqSSn-T1D2Y+)n%)r#ET#VpA7!ir(;2ErCz%zn z=H%9nxXA{{umd1E7393j^Rk9SR(xl56dBMio>!71fb@B3z z5gVTKhIoB>RZy)-73y|RDHD1TT7X57+SP)k-qp1-uf3|NEnD+z-iCEbb%troU54f= zbhR(G z`3_hC@ZH2TaA56OwA#yJA!asv;6LkP@^Qk8Dyqq=j4GE=?V8l00p+zs%XQBLnT*k8 zoHN(WTgzs-SRR(26*FnQwbwdt6^4YaofWbv$=be@C#x@owPq z`Vn1v=FOpBhkg_KL+F1aE|4d3>uI zn+)JxAwX{P$*=lu(z|x=YQ1wR`4mcWO8P*FcZpkxOYx)P=f%5=Hx(B^@uK3H#pD0% z_-7{Gc)kAZVRN`lo|-}|2KFg=_?vp8{pdMz#_+KQz!aKG0MbFX%p>!x-n`Jn6ZxPb zQ(`_=?Ox_{^Yvg{)$~$J08*0ss+FF7WYwn5jjvZrv`!shlHXQx%lgg=2!`iBWJ-Od!?S*x^ zijS_Y^-#kXyu1Twe|>!8_?^FHmy_J(AwLamj^1mt+Sa8-TBgNXuDP13N6J@vquZi> zP176A)JCyB>fk}m3&uIh&F>46>+>jI;4V`R?c=1QoOOvs0c z`6{T?T`CpC&7|(_3ZcZsMLJv5_tOL$|AeRAux5Q zih{Cf!k@AUsyWiLdkpYN5oX9=y43^DiUZrAiE`2I1JJLY>DLpR1ME@=$Hdt{I8H8V z5JE$w+Oc{(JvMNMvh7j9?YTuJNr`;bihw>xPN8_M$pq+ww3{YiBD__^gzcHEjmET3 z^Rt?4y{iTMLNT{Jhr8{FRgjzq zBH!4@)TwOEM8H(HY5{}wAz7RfFCes&CGzbV=}rR^tu;tF7+orb<3|a%?`lUtq9Q-` z4v)sUHuW6BzVBEE0=}z5IfAf8U@|*-n_$x(B6(QKMFKKx(dml$&&VveriA zQ^OC_2t6J_;w5{moAeB*$w{!rw%V~WmmCy~C%l`_)>MKu4Fqzy$4ToHY_C*6JS&uT zgS1bjb{-3lG~CJW6WcH%&orIB3G$ekVE-Z%dAFx;C{ATQfb96wuz!B5G3H9MM^%FY zneZ~*x%r50d8U0WTLHVi)OT#=BGPS;a+_W1u_I~M=Pw!ZvrWs&^CaXLI>4IMpT$Z! z98!Tc?55Fr)09tI0J3(EC?ssP2%*OZrUeVH0Z%RhGqy*clIHH5Je{G#fa_5H`4yiX1Ij|*{|++qFZ-2j=EG6O*GI#18p?WK?7Yh z&_e_3XkY^kY@&fJM84y&=D5%;jm*!`mA>dUZH{K_`0iXi$8N=LryjKAfMG9MH7Dtt z4Dh|Dw2c*D*|_FP#ZEl0+`&?(Fi4w(cUoDoQcuf3n_qSKT!H2_w}93#QN3G|Z&C_h z3;N$Pm~VGOzLnox)T(^5^c~hPtVRPNR5g1K4P5ius5vZBO-g9b<{r6%L3h#AfMEBU z$-DVWhs6{U!pH>QxozJL7m=p6xOD6Y`a$kfL%ZKPc|(cNzh6GArumlwtmPp_v0jTt zY&F?Ik|_XZp=EAz8g8VqY8Y4WR*RQ?z-{Nc$TnlO47Sa(VYjto&@(2d2cW|^5i((# zY-##znaGX-uUpo}B$z7lWRSj@HQ>5~iOc7@%C;DB6cSpsgdH2@${9CN2@fbeZHQ}8 z&J3tashp@aH*8{Im|;BBAZzhXWYZ@{mmYvi=dD?}MWdI>u=s++`OsiZ?6hm<+Az9*|2rmiL~up4Wt_OWweI5B56U`?CS zTU7nj$cN5)W=^kpM?|j~uXgA-t|S-lI<8mQ7;sV zM!k|#u9XGiKt+WF4FzD61T97!#5M^!3cxO@v^Q}>*elql!5{{3NQ2P;A{^6T5(7A; z*)!F+pUim#r~p(+pfTbQ>Lf4}fM+COnuq(S#SdV|04``^i8w+^11AR1rOogr3fb0> z!Ut4>^HDojmofz4`%rOjP*md9|C?0^DuB?hAmS*&^H*?bkJEdxFr_bM zG2%!T>R@>sg`ucwWT-bCq$o)=&T%YhPV`yrXHqpbqokyu?&h#MN^~<6_7%lo!*D~7 z_jPH$HJur?KL5rn8iI{g(-OsIkW_6`Qzji3Xvh+u9|e?`8&ibDx^eWU(C667rpG0N zz3)ve%HoI1EXg)g5B0$)!P0`GETrVxH?rf5PTc6_dRRWxHkFW}>FtXz_(_o}Ay-7) zS2abwoG=ZfcD*lyu0RVG}^ zEW#|VEt}#pfCv;hiK0z7Po9rjCzl_%)6zsS2Jj0mwNk}Xbn%#SK_?MYSV&^Nx~%*uGL7;(lK*4$eIkrXk&}GsBxTxEXIdXdc7etKmj*)Kt$D? zmZ-ZpfRbn!UH5ag-)c_=OK064nc{)TobeMP`f5s zL@gN;1)tG63!-mZ$Yr#X$4dM!yxw#rfs&es}ICm3t!@Ogvjf_=nnN&L@7kCNxI2`c%!<;kd+ml z_3)#U&fK|7rBmDnU2)iVW~sd{Wg0S10mqHR)h*m|kWibCS2u$GQiif@qs+VJ^m<+} zClQlspm`Qirk*KFzOJa2P5Y&b`24yI>~w|NKm?Isc>#|N);LIPRojX{Nf%jV!^Q}_ za?*&IsYpys?FVh5!5j-^5NTgvBd&fIiYrdFqDu*@DyXCzU^!pDlhjJ&2v`dtZd-=1 znr=a+&tL^(*Wy-u-7B)x$+<&rp4$1-X_i4C5Fxg4!{szqH8ALA z;5TnL=xN&0Jbo}^t7@79!sLW>OXt`aN(K+@#xRF$JLp)(b$uKOMy&SB8|gl_3G%9& zi|49~CkO9*^`vF6@3`evS#Ke2-&Hx**gM=IZi$yZYJ)vai!_575O?Uw=)ebnTU!fv z&X#!Xz1^ga6PjMNX!eGok?N!HPrHo6FRrf8)#&oF7Cj%L@Z4gSOM>!QC`ZYziImI_0L8S~bRX_s7%7cfaO~G0bN2;lUvv_79J7{AQ zXzrV|l#1lgDF>SssNoI2_^5KQ^1Ok68^s-=q_>6S4? z!PygY6>jEDAHM4WuYN>evqZH42|T*sDw@f%`N=wO;_%_w^FFc3^XZwnt@!piHeq3s z(xog#A(}|ZogTCFLTsPwH)yA{cdMzV7ee;N(ruzLat?CM`IMRGcl_q8qQ+!xz8=KX zt;7Yxl8e_pt*&r0OW9tvT*1^0X~59*Bu5PjldCt@|9oUg;wskjgcCO4aB6dNH%SDX7(nuDrHTX)TEN~|!qqzB^`2tK;1ItSkZdKTdy z>&E>@@1%CxUIz6L!TKO3S}b7&DJUqclL%AUS;(*ovYpS;Ih$3@fU=cR($Xhn{5myn zad))qXMiAp=O1I&-Lm_2-zt;}1v0u7nIgg1pb{JsHyq9WvicgodX<2#Nwz%`O*YI- zzss4Je>Q9tNF53NUuca100n@y-F2XlFx9{dsAZBg?U$=~;RRU61tL>N)CUkZNJSt} zFfJ%EpXf%KcJ^k%WLdDap5<=`6nqm$m3AZ@Me4;|l{2#ub=z_!?j{a-u*@`@T4`^^ z!g8`U(-j~sv|~W3F;JppTDe$)cvkYhOvlchRu|GRJ^+chgRK5I(%JnAAeJ-CyB>FR zgL6N-8@f}{-k`&Fy&$O!f)*fJkVJPXu(rG2ENX`0DN7yzQBu*6yYhY29_$%Jh2x*9 z-?>BVLL@X`Q>p0qt4ukUJDxxACRL>llGi#qxalq!>OJyc;ZVKZ!Op4H7QxeZbtyn6 zkw4{b8<=ni{-lFOh$VOATxPIx;Rf8+uiJaO0oW_2H2<&PrZ{@4;G?Vbp0U%EXTcDb|6SR2n|4-s8Nsy{BdX5F9n7-R6M+~Y8s z%hq_*=^yl$Lrs!@kiP}bFk5dtfjnl%dm4%ZAPZLndxrUPQ&&!|DsP7oI5*?q1(I`y zH7T%NNc0nG5FgZ&rC9OP53d zk$vJ#tXpX&@i3_D+{B6fCt*Xkl1BdJNJ?IWy={*G}ogzVJ z7bCG>9tG+$cB%BVa92JQ6Zk64bctoQynz-(A{_b%q!vL&jA_>g8c{(^0@`)Imbt60 za@0k}JeoV2Atr8L$VsLo(jaE}XCk1R19=+r?3juwQ1a`cDoVwTcUfx^1#t{($fG3+*EAl`^o55KMIoh>>gf*$0fgM zdCSeR?EEr87v{xv$Jn*__I}k;na=Oh&%Q$y8p;4F=Ny~}4%aBZ6#A887X*5KRsms3KGufR#)Gkta)ED^yM^CZ6Am0m z2s20aABqq)_@4_Gd~*x>bf?^12_(rEnp8?Sf`u8qulC}%HzaGD`2Gbr`gHGpagx&< zYHcQ7xl5_px;UCuVhw}%t4WzT*@aeh3ErSQl8jS(jPZPVh7iS(n=w>1xo}4TM2UrN zFtGej_JotyH0fl!DO(L`GB!W*9E#k$U~v-Wc8lcEq_ri~?k!s4N0oD>O-KWscE-#$ zcA&{00h9(05q?|8c0Si0V6tT4IBRB}LC|2$5mVqnA1kvl2>dA~CXCvn16@E9Lg$D*_k2sc{k}8b{^B2FiW2DB`U@$K62e2(A4FoG^ch>!jlCMHRkWyzTNPPe6%;tQBQJY7uaeOb<5g#tSn+j{RMRoAXZ_^3R@M5WRFtKZz=!H21?r=! z_x09OM4~;yJNn7lQ)#4J&DM0hv9oOOK$G3DT05^m)hHUQJ-lxUoqAOrRmTB2c($HG zeX+j%ATa|}k#JLi@(@+5jD!EkdVE`mIQ>WBpPjL67kTO&rPs{QKDEB4e_tyu@?&(v zDi`UWAW`$Zuadp0X}{O3kGXwlz1Xv9HD~ajS;4o42%`3F4HNn;0oV0Vt!5EPsW&~M zie$tDz-#8Tx&8u>zywwAjItV(G4!zx z(rMc1m8lJp0!Qx9PS69c#_`X_6$8*_^R^*dTVc-c4=c-XWv`-wFpXuH?3K6Xd3k<0 ze8U~j=lKr^-K{=f8uu+xVuhQrA!w*APK%DzKYpzDFK%3ZCop5X;upmtMAYx}qeCjq z!fZ?ej(FB%c9(yS?_09KF|>B<)+ci=c}f5=&hjDivgiHvjJ&W}Z587UR)X!;(l=|4 zP~BGfhcv|TT+u$5?v7g5ydsgZ+IpzG2QT)ydP3WG3$nXrr!Y`AEzOp?I?V6dA{n6Z zEnYPr0*rU9cuelK^ig{_kN-J0|M$=4v#%rTj-BkG)#N$RLSovA&!6VUNWk>Gf9~+r zczc+asTf6eUAT2#&sfu3QE4i8<*Gr#E<8&=jI#w%TY_Iu$JT;GU@s(S?^%{;%xFxn z^!^(Tn*{52oAgWCFifby#U8SWaCVxmO)gNT*C zwYHSjU6ejqYqoADVH#NZZ1740k*mJHP5GxI3|TE2Odju&^^o#(^a$0_N(-beIk?4K z%G?kG%cEtyYxgcq@LYt`+~Qlaq{O&9WqLFO`MsReFLqwfF|G)lST8x|m$vWCZs$|h z#5(3_In!-90ef(wIrx(;3op=#L|TlR@Uk&J5wH_p!f0%!u+U?g<9iG zx^?#Uo2~uT?z=QQcPaW6O$Rp~m%QaBGv3nn-d&96ZoiD<0%zN`J@Pub${ zA=j{z^8(XEhkx74A8kX0upOKcBky%kj~6O`3_P`ysmuwoHrl8WT z_@Dsd^JI+Fjlt4$gH#Qf(Mx778Cq`%oI+|`)9g$B6^icI-BHRQ0Ev(sd^k}eUN8^) zf*y=XZXbi)@7P5mek)O}Zq_-uG{YZiB__CF0k8YTq;IyZL>7 zwlYLd!Wlj{HnbZXvo4-E6g5lIA_y7$?kR^vZ%r8d{ zXfr;{4L`t<_6rYoV5k7qIhqZTkGVub_aF>@QC>DNJ1t7LvREU($-OEk@~H=i4pSWN z*d?ePJ7_Fq5$+^Eim7YCsrrtsTWhn2=3w*O=$_G%(%GH2RJHTmd5PVGqc-6sF4zLw zaV-?)CjGTZH_7gzO58DGbFUJ=hecT~?Lchvq!`5JGYI7BdKkxqujA zn04V;fhHRl%mSmOa29RY9dTR_?xt06H*K5RO_MfBZ11CB(G-JRB(gUcTJh~jFcp{V@eU>9taOI` z#&8a;KE}XOmf!3@Ll!lq)L1+MK(Sptm?ol5-Sd(r@r@8Wpgqsf17v3>Y3MD$8#pl-DtUdNWaA?@pOQwe?t!hq2Z0A6EH_B-UG zk5tfpN{=EcIMRg!o5S~Q)9PoF-Z8{9$l2KY0DGfEX`F!zbbteq=-NH8MDAKWTCnZb z#5pxrdw*+CGVR0ztReU%1epo&NI1}QQKSijMDIfm+YosX$J4(-bi&mYSs_n&G4tjlP!!1)%WAHoV0i@Oj*f0pPwoE z%)@aI)dVRAgvhN1KH!%9x}U?~D2ad*1@i%p0`&HQx8u`007?#Tg)PGK9t%Qa2_kV(z+R#v3BdI7NgBERc4CK*k_E$t?(cRF~b~u z%3$lnVDAHA-ESWz+2*m_(&fT7qk--#TqBUrSR>V@V}A>Axp=dl&rNegnOZR~9oCc4 zCvCwsG5Wf~c=j_nW~@sp@H0A4UjtFyWom#prXwShY^JqaM@+wA%}w)~6EZnEBBf&O z`T`qLrDDfAly>~qNkx_cL_UMhUgV@DFVfo@8jtTkzNc`JIRa7qxESk z^Qg%Ks6jJdnT~7fxs+DacK`8us@uT1ljiYThu|I|7>{~xATOC6pEtEwaz6oK)FB^% z#xwXj^oDPEQ<>y3e70kONq1 zOV@^wY3fRC44i;+aEJ*H8~l1kVOV|)dg5+`%}F7;Z>63%gR7b%u1l&C#ZPL@IR$Cj zHqKtK!-Jf`popLHaTwREPqdUj)bMpVRb^ebPzC*__+aq-J`3Rd-I&HS-Tvjj5wn+G z$)ZULmTxt|EZzio1)m$7$OU$H6H&NQ0^I~C&}aSMwJ-mT|LQUtDKEtXXTDbr1KH{w3?0R zNB7>gmJe%6MOHpFAt1Cr`bOOB;TNxs{^fuF%WwU1tHNbfoLIzX&+%RT^50E3YGsFA z8r>*e0aZ0<*NSeMo>^;dkeZKK0>oE+%Q3zP zE^kpfmmX9B9~IRF`>@WsO3%Nrs&yb(;N)n3^jWyr%P)@a`sx4f>ROg!7dzrN8PS@U zjc~!bY=Wko)Y-^eA=&0?o4LzDZgJGoMZch3JiT)S@<1NQHzGyD*lUvpb zF2ZBW-2*;bu3dC+rO}`!P(iy=7_Jm$b|4RMLckY#OZ|_!?QmXA3m%l^WBC z8g-=<4ffQ=3NO89u2hxmeXLZJWaBW}p=s#*;jR4&>1arZTE;w}mS70k05fWncOzUk zy6l7xy}w&)zJc+_aM6XEP6#i&%a~poRlvnK^be3A5wF+5mS5!S$RL=JgpzyEfT;LCujfT&;f_G?o!sTwM<{Mn*s+v%LgG2u*Izg#< zy21SPVz{1$jy~dMZm_cjwSI~d*HECPni1Q*el%Y-cN<5fe@ptZ!D|1ILSb8RruYs8sBd+>K#%j97j*iiDHmEzP|4g?l>@ z!>6&&jyk+40;t1rMrG@^KHgGX&t!yHxCf}!4_*|?>9N&}>&xqViq?d<>zbCu?sYsY z^R6Iim$DI+vQ^f1&#cs3BwVyD=Hwxw#>;x24+e*7UHvHzu$r)ByWo$OU3GK2To|Vr!fCNOD5U2xa zP6_cQG8K8L1Xi$u6&tvh9xHJ&rQYo&(u6?BgE)|_qRm;_W*(Q^!+Z(pt3gPvQrgt`~i`x5V6w`5}O1U9grDt=qP~)znCRr~2HG z?`k%)oO6c*(ZZDPEl(|{!;nTdPFn5DBxpRw+FsXao9z@ zhzI~@+i8Y72pdjlB~)8Me_Zf)HTgHSuY~wyHp%aENF=j~4_76voE2%G*Q%nl_5&_h zD{`FVwPf6(21sC4A&z^@LWg2IF)?&8(~XIsM|3YkfF)2%8EpB4(;}y1=+jDtRx-## z)fQCa8zjC*HCepGS9PN}mN87ln3KEdCg^$)(QeO4Tv#`Xku00DwMa9~wmDFBxS#Xm z&Pr8XpqBiB>$KSgju+LIvHj)JH)N>54+HQVI`X?TOwKK?AxlB#ME;Zbcbt0 z2i~(PPylv=zMhN`$34|mSY$`MtFa`)i@qs$}T_P zB3)a+Z?f(IpY6$uJ@FTDfF2wNoRlifeS{1m_+61zCHoq|Q>aJtq!aSPfJ&_Lpn^7) zzMEleGFgji(r&g``;v5dEK8Rt$dYtb!Ix=fW)F0yu4WXYh4Q0Xd%h8A#Icf9x@S`% z#DJD+L3N8LQBf+J_Ck*}!PsE#Dy;K1ll5_`W+nWeqQF#Qkvx^JOBQ5N#ZW*aeDQef zJ6r}~-4{I)Ra&|3zPQqO5tsAc5~qteJ>WXy+vU%{LW`g7Tl%i9_L_Q!rn9=_hJ23( zmZyOR=j-F=3jynl>uK5MpX-*so$LIr9jULj{{A*?Mcb@xwxX$s~V5X@_58Y_81<3Tpi-Q)xwkvHrC958nH#D}~z}He~n!>Dl?Q zg-Y)zLVCAql*-sL53tuw*S6QYctJkjqN$dPH5y4nU&TIXMpvh9?61Qy*ST(9wIesp zi!#%vD6K?piL|OJ>C8dhHU%{2`3fQYOf_^osdHGJZ=J2aZW)DvIv=|GRWS;+N5f6t zsXZlj9<9wRletV^&}!D%8){ut_93iS*a%F>1wNnnaO6XF_KQbBjN*6$!O!lC*`kNxvE~Ihuh%?yvzaU zsUX#l_&C?1P{6MlZ2{eB9QNq%dBgLCk*X0uH-J87%KRNuQc*>jk9j}VTmUhFizXI= z-_nd(n2zkOH6q7NbJVu~{#_>)517{(C3t2HtnL3fbi@k8hsWD$4%q(tcYT9Ayz86I9#^c3Y8H|TN(?V~V~Nx{t6JkUg1MIA7cl1Uxm-8K6zKFDX9FY+m$#CtT6 zxa4d60gd2uo;l}l8NC0P2ylA!TpLa=6_?Km`-6=I9AD^M@D}YDfq!81k@!l+Z&~Hl z68ZUskk{nu*pAR!k2(qQKW?*avd-6y8F?%q`$m)$;%(*a3_a7IW9 zJUM?|Ku{F!Hx%Nk;@yn7NprSfYO5(*F?RKsZ5aAy$dXiYXq~nGwcMIuu_t5OKEo2n z)lwHwt%zX9aO?{j2%#V)74SHCR9~5uzd_+YMKvi^w;WS|R}z8Wz_s%*UO4o-9wT_#mm>(*)>?46 z+h$JjT4O6R6Swd@=5vntJ-UGByYQiMnu-s3KEyZH(|Nr{mY+9GYS@-j{)7?Em+kz0 zfOS_E9P~`PU0&-MGNF;XYbk_vrCgUgzs5Gj3 zWT0!K^Q_($9@=`ylg9iFo7zqcldEX$^g08hU(BB$%T(aAVTmo4Y9oPdpij}ffu|6QhJ}8d4`n7cRNzquznfzdQ=fEDXcqdu zAG)Q)8Yhw2clx>#MKx1VG-t zjY0vjFHX=_83`m%)@%W-%JmMWSdaNTR!3g_OI0i9C)lr)u(A?URNjIpuR+VrNYksbCTvZ%6@Q>BNGcMkOYz_ zn8xDlk@PKr)-UYs6Vhp|wMSex=ThJ@Tn+VS-!V*MSDB9A8mVS|we2K`_oYe*}vQ z7vP<>z*>6pr>Xrdc$~j^Js_2pxdtknMRm?gV~Hn{Pg#d|5Z=Wio~=FSS6P_t6~3{x z5Ka`bUn|YaY0eX+>RXzg=({7FTVCDPmgOby@@s6RHxKZC@>9WA-A?t>{Vsst{{K@c zhwArpIz{w(h2P$opQfgS=q4%lYn^!Re)`){^dC>9mfZH7RU8|Cyx(ZiP}^h=zJc>M z@zY8Ly|nP^c+QVmfuDUmggXcB;z-KjCgL!Re((k7?!&C1d#COAC@6$^@5EG4M(bZQ z=3xMy{)6f4->P}Mra7&Dsh-;>=apE5l-jM;i{R?BrcP>R-agO9h_i#$9EDU@XAz4_= zCbFAYX{jI#C!_bk)$kG#_|B?b$lYMy6lA?MsvvpO)b>tGf2te?(_?61Af-9VS8X13 z5of}S`@7|CMw9W9ns=hpKr(?~hO7W4hsi}{X1XyW;@}J++>N(Id(FuL=w7Q+%o-mf z`x5x5V_gacN5#RU_ZE7leKYXb54#Cbhj{nHomkT~2oe5P&XEsu<`H-*W} zW*xx#YD}(F`dwn@_Bs^saUc}X6g!zlEbQqkIJLl$Yp45i(mcIp8SP-&hrdCF*3C^r zY0!|}VV&PC);dqur&*DIs%Ns!=Fs&8RBJIvfHACP!IVBQS>uAe8SSDyHf_&#ZPycd z-A)+05lPvUWmM8s*??MTCwyR*%+11+{Ec9pHGsrxBdmzk@+ zuCjBYA@xH>FT99?>?=OO%9f2S@J@b&8M=Hz2jc+E#7E2GYpWxd3znk5j8mf}cL*-nss?BMnM5K_$%`cV;A3bsu)=E-Il@~rFV(9Xm?M_k zXU6+!|Kx2w43sI5hKwXHiR?H!NpRi;A7l(}EG{D+qTkZj9g!s|*Tx?NrD5-1IG;{{ zJjo@6!Z!36HF4eSt$rV+Pr{N@a|`NJ)dduM;2a4dp~lIYHscSYoT7%P`(A|bM{*aX zOtYLCE+`|t5h$Y@Kd}%Q*3peUogvzq6N{89Y>3#>z+168U$j(GjARofxSPWY{E?|o z1#axq6`hiOxA&;7K)df_Nh?(AnJ9$J!DgyS)Vae8Rp4f-_wt@?_@@=8r`O_*8_Y@< z-1bcV*{!K6aC&4%PE&=}@IRx8qx2p8n~uqu937t7F>KaGFh2+j~VjwRzw?xUL?^ zW;$#IWTv|5yZh8s;t#`jb29a-HO47%rD*vH<`?Nla$0Gp3Y6FC%}cPDxqy}dv>PFV zm086k{FKG9=PLzc3S;&rv0PUO43!wNd8Hny&lYrT)n$V=H%QI!Pg{WYwbXLk&i_Ph ze#_Ag+grI{hxdhsf>b|d<<{jNpw@t^J@#A1#IB=RZu-+a0fB^p$a3^-+1Oknn*M2y zdHyFRxybL(yjNoh1hgnnLJB2|VII_A3LJ&Jr815T5_M_`M)3jpf-N%Dsh(1S9;4E` zb(-sI&)kq2XO+z`A(^bf`e?K2;IEW-DDQ&HBS~j@J+?pfOroQ#dvDnJBF=Y9wcOyk zd`!GzV7!$W$4?OhUv6O0)b>L@=FX^N!Pl)FIrr2f@XaA_&U6UJ2Q`bp4=&Q^(*$&* z69$b;;!M4&8Q1j!OOafo8avM>f^FW6V?`u%N8Wt8P#Dh zQs3pBF)1_M4Va~#vmT33OO~)5Q|eCiU(r$4aG+ubLbugf~tPgn|^m z!I#m;s^DD74h3^TtXz;(r!ZF8)3ERE+HPzMns#cN{enDMkj=u8f{Drm|+U&8Wsd+omp^Asd8%?yVX>no)wi>TIRqHFv7PRw{__00}_-rh>rTd+u z%;J~joaae!vzVq0lTT|1l7d0Iq1lL0+3~T0qy%cAhDK3roc|1-|M( zuJp-+IMYNs#Fn7V$@1M$Tae-dl6-au1vGA>m<2oK{HYpwz`5NC>2n=}g?&fUv`zD1 zRT4(gCv z22K_M$R??yoo_InU#c$OB=4trD&QYgUGOeq1cX1s=GpQrBpb&D?5Y$pa70ex1lsio zQ5{PXL~5#PuCi`X2|&A;Lp0J23e>L(D<-V@e{61J)~z9z-^!*Zt$wm<*TSE{ zJ3qLXEZ*K(6xUmAhXYgS9~Otpz8kF@VQl6weRnnHglhD=rKP&5>U`>pm~+b|yff^; zIQY!3MaL5#@4mJ27dR;+w{}0`qNXo!Z3PDCP{18uM8Rxmr&zDUX^I%DQB-B1ipT10 zRuh_f@1P+x4;H!3JPJAHNwXvlu?ct2r1#V3D@vB+8niavASiWvmnpdUfhUX2uE@SDUS0P83$EB zU5Z`_my>SoOoA*m)-leLMmxV#Hy{f~RAghAF9i{hP_eAeq!T0h(0H0`O9&rV1=`n4 zkYw#*_J;AgfP zk=hww@nzG3X_ZGUw@AD3H;sUbVQ6KqcS+1LsG@{=I~1R;z3hfyIO z?T^5*Jq^aUN0CB>U5p95lK|4ZdpyA(S;ih#AXPW++0dWDA1N(RJCds*gDA*O zqgjq*A{3bH>jj7HaCex5QF7O;4{^VOMzfGJWXoEzv4lpxB3FAy6i9?#ND0M};a}U$ z%n;HPjJS=y;48LfW?xl6cj>tAU$xaJSnQn~QTM6R4RgW{AXv~YO2Kae#EnIWmpI*+ zfbW#CTO0R18OaA~^e)ew3+Mspss^!3uzEwyMkF~y^snSa{gn_B6c8AB*8&S+E(WmG zBae~mM5s3;Eie+qVk%G)F(@A&`**B}W8BYzRzB76a1wFgaQ?ESDO9|kEORaRQ> zz7Ke`!nb!Fy57yxLi*xeK_T|beb$3+rukOD2adIft&OSjfnh9g`;ekv2UTEr=?b{& z2?eCYi}Ahq<2@K6yn!@RxJ~G;UK^=)qmjn$S!mj=7K&qPn}>x+gJRaBt&?z5#OplI z!jHFU`AS`9Ze!EtB#zV%v3CiEftdstFuwF}Mjvim$#QsC**phzDo&*&X8u!WGh3CHIW_4zx$2BJKspdO#Xy z%M8@Pba{Yjkcd$FQL{5;*{rS^GZiz0e&H$U*EsDBW-xF{4P}~@I#dPb51WyaK!j`m z*$4Fbn7oT{^iQiu-`uPuwW~CS z6P63I=0o#ADi;BAotBK!?Fxr}|!p;!V{!<1`OU+&As71>~ zgLc;3c}5ZGxeP>O7_UM>)&$oNB<6s<>cZf45t1{~J)mx@D(%BaV+S8L0}`-e0%E9e ze^bZCs%4F;{y}jQb#TSwQS5=NUJ;)sF>Hw^G`E8**{D|eBU zEJ)SA>im(^50HeSwL<5=_EG}P+yu!Fm&q*+W`hZVwj8rc8JY|!seY*B-v-#%tSinh z2`=cW{ilZ`mJblSy55JU1lcba+Hc1Jg8=UpAtByO@@PuKG{|AJ>UQ%5GJBPOrpFTSVxehqZO=Zmub<^&GOKiT zuf178BN2xeCnghQqlrDCjVB73>NF*popegB69tRNi!}S5!3hsY;FNa9*7gP@-GeHp^6IQ_P%h+?< zq3s?}BXHvd2H)XIpu52y-DkzOF+-}?_<@YPPNug-0f|)E{vNYom#dgtwjA z{k4g6R}V2$S6xXSM-vsK*xxK{{`_wLUAvwS+dip!O0i0LR1XxXj+yb9ez*=8uVaAs zoZiP9|7?mYvySt%-MB27v!|t(-9-kU5h)E)BtG%#5Q{K;-^u_9zm91 ziy^(hk{(QoVX#7biW@jQ$&p}EMm%gw#LW2%wWssi?xy#SuI|6ZaG-S=*6e-kmn%=5 zI9^+Nbf><{enZx0d4JISoH2n$xTNtR*+deOrT7>zg7`fM1RDu4_nhlY4FcdaaCZ{F ziQOdY#$-^p^&e=9+D*{3Afi&~M%cD@vw=LX9l0uz_$`OmQE6NGJx*29)df<>uXsJa z*u1j&MO}X3oBZZMljB*lojE0gO zW)3|#(^FS58}tR!&(J~*7i+O@JVlYsH3wa-P}MkI1qz#}lEW46U9@gC=g9gKzwSvR z$5MRVkSJ)|a0v&Ro%`lIHE@N!rD3^i%kvl1R*}+s@rn6vGrj9m-7HFHy6HLJzcvPd zp07my#J8UwYzrK}nw`x8@9whcmpWep;L;@XwVya)m8Hq(C=7gW_<*sqF4Op~$^2|@ z)uBbtutz_sbs&icvD!y%gfO6Z3hpu;iefQUnG;-cNg~1NL|pDL_nX5?NAo%tY_!gt zBFJct;ArgzRIZfDF+*OdOapRQ?{8}&U(@sP5vq-?YL4M~*(i2RiO*zjcd1p_dc6?$D5jZ`$^P`0QFk7N=L8P#Ok%& zRr{+^Sk#lZ=^ORNI_dy6SY3kSPj#tB+cyfu&?E^mx5yGl;+({-UPWqR%9G~UA4EGe zZFUJUT)7&=t#VqzY$hWH@!6C{jN=$axtz~${#V_2GqzUKsoSF5C%Y7EBhcb}DIa5& zLsidQ~ zR;%*OW-a5<)8a2bD{Et;(J%}Eay(1mc{l28=wOZYn1RmhE{y1ZR+9f`ECvsy2vT`f zC<-;|dY~CBAspXi6-sI%S(W6NW1wYPbXr%_@qLA|UYG{vvED7UshpwbV&)2EOhZt4 z>CzJw>sX0d1!Y~ao~@fJ;mM(q#z|0?IJMi}R^2c&_5Y2au%=U^s;BDR#>Uonq<;C0 z77uF-y{Q?g>P<>XQnqkxqM6mJIaLV6I^(Lc(d6{)&5S>+TqD)K;Z+OX{W_k0OcVx^ zF36ntj$l2)+rXmTXMd0=;0E$RcJ!`hha(;GO4#zDsACvA)-Id+kqzu@FA9>biN~u$ZNgPy3w%6CL(ICsJ0c+!r?QB$H1@=1eri2f_K? zHq(f0%(pw!GV##UaG&5q!Fu#Ryd6AU?2W_PPkvKXGeT}rzz-j&fJ{@#bFtrV{{M^^ z5`Lcl4W^6yBY5Y(YM$mF1G$Zw9>korL}h1Az(o9zU-(PCVwy`NMMgd3MgNQY`SF*GmEK+tZ<7cJ07|If<)S*MoN_ zbv-=*fZjB1Vv)S4Lb*&#N{8!$hN`v71KE=3q{vouTzA==xDKuW##j9XCCW9HM2fqP z`+_oxL57Ep@IWWpB!eAlj|Y>I2kbe)?0lxB4I*;P2u$BsVDlt^o((_*$(>a&n0`fa zbX-pqOiSc!vm+cABz{w-*X*z;s2@#gw)Y8-Y2vmU0IfKDm51oby`$p+v=3}c3l{8L z81qE#6US4I!kLZ~;^@Eyurwb~gK%x&r*Jy#d(m3f!TLo$4AdjjZwbI#;sGdxgJ(PfR(9t}b-48>KQ+Q0WmCtCW_FP-}J{@8v}cfhtR#(3bKf zgOUNyAwiY%F4Q%y1nS%O#%FmV%Lc1e!2(gFK?;7L=TKl~r3Lg-@>zvLOyNV+V5(@c z8M;n7TcBj6vK5V}fXO~!W^+O+#{Zl8 z!c8C`d?vi4Pe>Z~nR+Ef=|-!4mza?DLnI*X4rtZsJZmk8(#?s=Y5^ZpM-;?u=0Tb=QIO)CoA^I>J<=2nWsGHrW zvSvU+KghlVg+Ul(bt=F!&(qZoU3|@Zrulhj^#%(@wCMq^2SS0%x9ATOoh{&V!1zFC zdXx&9Ju0{-hWL@jf@nju56C!Jen8EJlPNI7h90r{54R#n!dpewB(BFdE%j7ayC>CCdgKs^S?qU?*4d*g=+F*bo1JLJzR51s)lg?Qk{DAi zFjQo9*8*vZI?Gti6&8XeWM$plb-^BJ-Rznbx4lB^wlBm8BJVSiqA$R`DDVQw*_T)~ zQ#toG;68XgUWZveF^$GuserSN+s7olz)oZ2G;Pc?4j*4zMxYsC3hhhu6m5-sQS;;S zN9>GO@QB@a8-xn|?@Tve=*EGfLo)tIe)U~NN`br*-%l7YP&EzJd4Z#&+e28h-A>=E zEwhEJ_8Vr~I6joOH0N)-i`>pj+dh&aA^0gm3A*m@C^J3=6y8pnYgAIU3Bd` zf0~~L6FN8187#AT=IL4UvNT^aoK2IPRU#uE$C$nz$gt36zjaWDA1jI*!A0aOtVA|Y z9{7dWc+Mj?lh97&g_cTR3+zNvS@}CHw0hN0TA6REBzR#X3X-wmiaM}r@vGwe)VsKw zjdp3L>p{B;XTAHRen6an9xbbO;4kS&cD5d(zn~CCTp0!mBZo}W=D{fOw4RgY)+Akk zU3kE`jiw;24{37NQD4w$)>{f1yZ>WJ>%jSAu7ZWXI2^~GyWOfuBO%5>2vR(lHf&=E=Ycg;ohb`60SPx3nJ55O*Bb~QYryMs*gv7o_z3Q4`aRLMKlnQjF-#a)y^)D(3P4Miao?1}-E)5ZEVC2vwz zWAInDY2+Ae%&HHsQ2Nkz!pL$ddRd^RlyJ?1D4;YZO};9Z3X+jd|NcZf@3b!!dFA}u zlL71Tw7*}w9kCuC3brPvt;-{mXye7^Yh!VlxVEmokTcO)CoxkC;iQnC_1ociJ#_wn z+QWJKowFPduQux3d3}+uh`ZGToZ_?=%odXbwG#DLO3}D>&PpVPT zL{gANBPl@@4WtjP^ThUKV9os+zHwuh>H91D=+`hs2N}+E$_IX?@xtdn6Dlq@6HVgs zIk=%p-Y=ah;F*u2U=~&6WXbIY48F=}^HniXic~_yME?s~B?FZu(vvHLC2geBO(yBI zSe)@eMs*MyJwbB&0uo;(mHevU$pYBoJV(WN^2K1_fe?n4LCvtyEU0u~( zRgkLQC3LtQQsP)wXPK92UMWLMA)UdElEp~mnQ~t|W&ZGeLD;AtLZ3$-!|I3oz+qh6n7yZY zxpKGkrLa(iMlzF@3wRyYu^?FoGMX5d(qBvRu<*9P+W!S5QS-Y9CuacY3t$*2eD(-p zS$?Q6eU&xdSYHBp|G&V(@O26tDUJ1Yi>YRV=OJaodj9#{$5N@K+8b+2{U)3L<$@o4 z0V#3az<4dbUq2tR#4?lE2Rsw;uHc~EA$RPNHf6WmGbdW5S#zMzvW`

O_Ake)$5i=V*30+(kd#ZVZ0rK9G&`xh;rsT{QIi8VXRI5_UKAFzUP+EXi(=dDlA<+>3l_H)`s( z$%pPhHFcY?7bFWZ!2|If9JZ6hT3K|beha&xNJ6@`tdJ!MS{;>(1~738 z`>+B9bYjR^C=rf^E3mgLI#DQnvU}wSII;!t-0$cywuMJl6P8#C&UyrOO^}`t zB(h$a?0{M>JNf2;=C3r#4h4~Wyb!xqtX#Dq;}|7q7)4`QNKaH10b&Sc=Td2U6$z^8 z=>6*rLnzpF1O0%{s_Xk{=lF(|S}^_SbN&;!W(X*8P2gY{lJVplLhi-M6>9FPms-TP z{FKo4iDnhoTQ>=n-7TfT>YJ0VERy9HMZ$^mPy0+&C%QPPJak})YD|7zh_w+F9YD+* zg}ix%{LIRwnTdi@D#|8$iFSJTVl+{{O#hHsO7)+FNRHvMXa!#Z)|1U@3M`BlS(egV zSq&ANZM|cFR~rbZgfk(B1(M;Mlb9Cz9`1{YU!=KLNn^ z&z%<|=0FXcd00hul#kneGq{N<^DLLZXHxY82THr~+wqnQ2srW`5 zkk643Y&!=^*%pnyGH5>-K6uLW@WX>nUA;peChIGSM3|Stj?27%tW6Thz$5d+G)0>dvdL0i} zwSHRg>catpO!*as`Irn2AwQkl;Y8PK&R?l=Ro$i)Mgty zoLJNWlXmSkJYGBzmw|HNt5w+JiUh{c*Dq1f=YVKa#fpS0uO)i?=+PEwJRJu%N8MyO zH(+VXmXBsLLsJ>F2`7MNY*QEjdB>?(;`;CcU&S5C-Bxzo^L4Ir*)~J5?EVwHnvFmu z%%m(mIbm4NFDwG}pX-4Qf#dMttJVL3YhowcvagPIKeN2Nu#nC5@bfSsBAlNvcu_K8 zk?DKe7CT|5V^I@Mgf)4>g_doaa3w_FPPmgk+-wh+@Ko5yn(!ihQr`(5%3Etr^ToqD z^a%n#>+FPZv+i)?KjW5P!fpQ7-0uJS-*BS5^*{BEFwz9Mh8Se9(Hta8k}REr_68fk zL1P09F)FIjx;4hfagM({*9b#7?zft+Ym9FU9^E+CC=OzX=SLeE&R18(^adFc`G<|s zSE7DK7^aG`2FTTq9FCMV9l4rMFpwL=M>3IWeX;Cr-_HBS=p&aKZMiXs;vBqD^_4iO zF<(Q+%zS;>0nz1GwF$f<6c!KR;yQfnD6}_F9|;;GG#bJ;gm`R1Bh&KK+gFt?!2o^z z(~v4QKgkoVh?lwZaQ)@_YUS2bPxSHO5b=uP7}nni(TD_dxz6j*2fFuSTw`~M`o@Qe z#_4Rmr>u(hycC{Q*NN+(1vtJ(4$z;2F%|n`E;ol5Kr(+**&MVo zGNo&VRkMjOz9)rv)39)l`BSVK`xwh&h(W@>aw6PuCgo8)e^6H&OdDbKZiRA2Fijb> zn|vZstQ{~&a?cj;x)Lq5c1M>Y96GCh)zRxnPNZ$5={}7$o;JoXeI@k`-!7|k1Ihm7 zsRW2{Q$`lo>Fc0uveVBYmJ%t-f0WWcCpi+~ppBb6*wYuK*GXR1k>;Q>BUuQ6)YmN!1(*EzCg49`KMs9xJ>E z%Q(ytQhZ0Mv?7ZtT6!@)#5)oRsaVd4A^z3k%xA7RIhT{=%VSLQOC1J6_Kky@`aj=9?l~~0}R?`U8(c3_a8L_9H&^iPJl9wrm*#&hInVx+k4m~1r}#9# zW|^{vY3;0*G={WNe^iOf(1=F%VTPEFFz}hdC4*1?i8SnI)1#r>(+V2PJTA7aGUNoY@H3A{ z5)Y_(t+tR(FNA>^)EG#$uLfyo&*M0rWGBYVSe*@g%e>cw> z;+WBVXMjhwpP}@%vDGnmxV`lKO0&gMo%oHWL!l|mSX+0=%t7bOCJf#khnm}A_P7b zw13AFO3G~ajt4AfD-<|8{LA&qQnr9_Kp+tG>=b3toS*gmr4l~udX3!A`;6CT}K;Kos8AG%MgNmjs@jHh} zJ6x}puDOlHmRC;Kbg|U(YU-K`N%)lPx`#cxdQ#>-+fwM8la%`wC-Y{`X})}$&@}4T zYxWbUi7#@-T+)n-ZC_g}be#uYai}Oq%$VBJx2IK$IdP~jj0v)MxxGcYkT|DvIk6cs zL(%|d*i}d4Q==3t(fk)~w7qmxTHNIuRZ;FipPb`d?+4OWS;T!U{B!lm5p(^VEw}Bl zYLDcp0rm%WtM`Xjp=;LULM+@BJdZ_Ig=9V9uUOFF+lc12xpXA!%g#9TCoxT@U08OY z!R4T%(d(kKF8Roouj_ZN1ON71PSqUoQ_I>duee`SI_rpS(NVK^{NCPwDh>7lIVR!M literal 0 HcmV?d00001 diff --git a/web/src/assets/fonts/LICENSE.txt b/web/src/assets/fonts/LICENSE.txt new file mode 100644 index 000000000..c925dee25 --- /dev/null +++ b/web/src/assets/fonts/LICENSE.txt @@ -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. diff --git a/web/src/index.css b/web/src/index.css index 121e953d2..660cb94b3 100644 --- a/web/src/index.css +++ b/web/src/index.css @@ -3,10 +3,19 @@ @import "@fontsource-variable/geist"; @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 { --font-sans: "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-pixel: "Geist Pixel", "Geist Mono Variable", Consolas, "Liberation Mono", Menlo, monospace; } @custom-variant dark (&:where(.dark, .dark *)); @@ -88,3 +97,66 @@ -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: ""; + inherits: true; + initial-value: #ff6b35; +} +@property --flame-halo { + syntax: ""; + 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); + } + } +} diff --git a/web/src/locales/bg-BG.json b/web/src/locales/bg-BG.json index 6b9a66f24..4d55ae587 100644 --- a/web/src/locales/bg-BG.json +++ b/web/src/locales/bg-BG.json @@ -22,10 +22,16 @@ "language": "Език", "page_not_found": "Страницата не е намерена", "username": "Потребител", + "email": "Ел. поща", "password": "Парола", "auth_source": "Източник за удостоверяване", "local": "Локален", "remember_me": "Запомни ме", "forget_password": "Забравена парола?", - "sign_up_now": "Нуждаете се от профил? Регистрирайте се сега." + "disable_register_mail": "За съжаление потвърждението на регистрации е изключено.", + "non_local_account": "Нелокални потребители не могат да сменят паролата си през Gogs.", + "sign_up_now": "Нуждаете се от профил? Регистрирайте се сега.", + "reset_password": "Нулиране на паролата", + "invalid_code": "За съжаление Вашия код за потвърждение е изтекъл или е невалиден.", + "new_password": "Нова парола" } diff --git a/web/src/locales/cs-CZ.json b/web/src/locales/cs-CZ.json index 86f0694d9..6115aae72 100644 --- a/web/src/locales/cs-CZ.json +++ b/web/src/locales/cs-CZ.json @@ -22,10 +22,16 @@ "language": "Jazyk", "page_not_found": "Page Not Found", "username": "Uživatelské jméno", + "email": "E-mail", "password": "Heslo", "auth_source": "Zdroj ověření", "local": "Lokální", "remember_me": "Zapamatovat si mne", "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" } diff --git a/web/src/locales/de-DE.json b/web/src/locales/de-DE.json index bbcf6c231..ba6620373 100644 --- a/web/src/locales/de-DE.json +++ b/web/src/locales/de-DE.json @@ -22,10 +22,16 @@ "language": "Sprache", "page_not_found": "Seite nicht gefunden", "username": "Benutzername", + "email": "E-Mail", "password": "Passwort", "auth_source": "Authentifizierungsquelle", "local": "Lokal", "remember_me": "Angemeldet bleiben", "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" } diff --git a/web/src/locales/en-GB.json b/web/src/locales/en-GB.json index 5178f17f8..c8b64c55b 100644 --- a/web/src/locales/en-GB.json +++ b/web/src/locales/en-GB.json @@ -22,10 +22,16 @@ "language": "Language", "page_not_found": "Page Not Found", "username": "Username", + "email": "Email", "password": "Password", "auth_source": "Authentication Source", "local": "Local", "remember_me": "Remember Me", "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" } diff --git a/web/src/locales/en-US.json b/web/src/locales/en-US.json index e1097bf35..5b29221d4 100644 --- a/web/src/locales/en-US.json +++ b/web/src/locales/en-US.json @@ -27,17 +27,35 @@ "theme_system": "System", "username": "Username", "username_placeholder": "Enter your username or email", + "email": "Email", "password": "Password", "password_placeholder": "Enter your password", "auth_source": "Authentication source", "local": "Local", "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}, please check your inbox within {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_in_submitting": "Signing in...", "sign_in_failed": "Could not sign in, please try again.", "show_password": "Show password", "hide_password": "Hide password", "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_passcode": "Passcode", "mfa_passcode_placeholder": "Enter the 6-digit code from your authenticator", diff --git a/web/src/locales/es-ES.json b/web/src/locales/es-ES.json index 192b8eeee..edae062b7 100644 --- a/web/src/locales/es-ES.json +++ b/web/src/locales/es-ES.json @@ -22,10 +22,16 @@ "language": "Idioma", "page_not_found": "Página no encontrada", "username": "Nombre de usuario", + "email": "Correo electrónico", "password": "Contraseña", "auth_source": "Authentication Source", "local": "Local", "remember_me": "Recuérdame", "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" } diff --git a/web/src/locales/fa-IR.json b/web/src/locales/fa-IR.json index cbf3d30ef..f480a5a17 100644 --- a/web/src/locales/fa-IR.json +++ b/web/src/locales/fa-IR.json @@ -22,10 +22,16 @@ "language": "زبان", "page_not_found": "صفحه مورد نظر یافت نشد.", "username": "نام کاربری", + "email": "ایمیل", "password": "رمز عبور", "auth_source": "محل احراز هویت", "local": "محلی", "remember_me": "مرا به خاطر بسپار", "forget_password": "رمز عبور خود را فراموش کرده‌اید؟", - "sign_up_now": "نیاز به یک حساب دارید؟ هم‌اکنون ثبت نام کنید." + "disable_register_mail": "با عرض پوزش، تایید ایمیل ثبت نام غیر فعال شده است.", + "non_local_account": "حساب های کاربری غیر محلی قادر به تغییر رمز عبور از طریق Gogs نمی باشند.", + "sign_up_now": "نیاز به یک حساب دارید؟ هم‌اکنون ثبت نام کنید.", + "reset_password": "تنظیم مجدد رمز عبور", + "invalid_code": "با عرض پوزش، کد تایید شما منقضی شده است و یا معتبر نیست.", + "new_password": "رمز عبور جدید" } diff --git a/web/src/locales/fi-FI.json b/web/src/locales/fi-FI.json index 79640f692..38390be19 100644 --- a/web/src/locales/fi-FI.json +++ b/web/src/locales/fi-FI.json @@ -22,10 +22,16 @@ "language": "Kieli", "page_not_found": "Sivua ei löydy", "username": "Käyttäjätunnus", + "email": "Sähköposti", "password": "Salasana", "auth_source": "Todennuslähde", "local": "Paikallinen", "remember_me": "Muista minut", "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" } diff --git a/web/src/locales/fr-FR.json b/web/src/locales/fr-FR.json index 2bd298334..78809fa9b 100644 --- a/web/src/locales/fr-FR.json +++ b/web/src/locales/fr-FR.json @@ -22,10 +22,16 @@ "language": "Langue", "page_not_found": "Page non trouvée", "username": "Nom d'utilisateur", + "email": "E-mail", "password": "Mot de passe", "auth_source": "Sources d'authentification", "local": "Locale", "remember_me": "Se souvenir de moi", "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" } diff --git a/web/src/locales/gl-ES.json b/web/src/locales/gl-ES.json index fe8493f1e..1829d194a 100644 --- a/web/src/locales/gl-ES.json +++ b/web/src/locales/gl-ES.json @@ -22,10 +22,16 @@ "language": "Idioma", "page_not_found": "Page Not Found", "username": "Nome da persoa usuaria", + "email": "Correo electrónico", "password": "Contrasinal", "auth_source": "Fonte de Autenticación", "local": "Configuración rexional", "remember_me": "Recórdame", "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" } diff --git a/web/src/locales/hu-HU.json b/web/src/locales/hu-HU.json index a25c58696..e112c5584 100644 --- a/web/src/locales/hu-HU.json +++ b/web/src/locales/hu-HU.json @@ -22,10 +22,16 @@ "language": "Nyelv", "page_not_found": "Az oldal nem található", "username": "Felhasználónév", + "email": "E-mail", "password": "Jelszó", "auth_source": "Hitelesítési forrás", "local": "Helyi", "remember_me": "Emlékezz rám", "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ó" } diff --git a/web/src/locales/id-ID.json b/web/src/locales/id-ID.json index 9c0ebdd1f..9883e4e11 100644 --- a/web/src/locales/id-ID.json +++ b/web/src/locales/id-ID.json @@ -22,10 +22,16 @@ "language": "Bahasa", "page_not_found": "Halaman tidak ditemukan", "username": "Nama pengguna", + "email": "Email", "password": "Sandi", "auth_source": "Sumber Autentikasi", "local": "Lokal", "remember_me": "Ingat saya", "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" } diff --git a/web/src/locales/it-IT.json b/web/src/locales/it-IT.json index 67bb1330b..9fecb3bac 100644 --- a/web/src/locales/it-IT.json +++ b/web/src/locales/it-IT.json @@ -22,10 +22,16 @@ "language": "Lingua", "page_not_found": "Pagina Non Trovata", "username": "Nome utente", + "email": "E-mail", "password": "Password", "auth_source": "Fonte di autenticazione", "local": "Locale", "remember_me": "Ricordami", "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" } diff --git a/web/src/locales/ja-JP.json b/web/src/locales/ja-JP.json index e3e47e9c4..350ba9307 100644 --- a/web/src/locales/ja-JP.json +++ b/web/src/locales/ja-JP.json @@ -22,10 +22,16 @@ "language": "言語", "page_not_found": "ページが見つかりません", "username": "ユーザー名", + "email": "メールアドレス", "password": "パスワード", "auth_source": "認証ソース", "local": "ローカル", "remember_me": "ログインしたままにする", "forget_password": "パスワードを忘れましたか?", - "sign_up_now": "アカウントが必要ですか?今すぐ登録しましょう!" + "disable_register_mail": "申し訳ありませんが、登録メールの確認機能が無効になっています。", + "non_local_account": "非ローカルアカウントではGogs経由でのパスワード変更はできません。", + "sign_up_now": "アカウントが必要ですか?今すぐ登録しましょう!", + "reset_password": "パスワードリセット", + "invalid_code": "申し訳ありませんが、確認用コードが期限切れまたは無効です。", + "new_password": "新しいパスワード" } diff --git a/web/src/locales/ko-KR.json b/web/src/locales/ko-KR.json index 9a0804f2f..4b2119e9f 100644 --- a/web/src/locales/ko-KR.json +++ b/web/src/locales/ko-KR.json @@ -22,10 +22,16 @@ "language": "언어", "page_not_found": "페이지를 찾을 수 없음", "username": "사용자명", + "email": "이메일", "password": "비밀번호", "auth_source": "인증 소스 편집", "local": "로컬", "remember_me": "자동 로그인", "forget_password": "비밀번호를 잊으셨습니까?", - "sign_up_now": "계정이 필요하신가요? 지금 가입하세요." + "disable_register_mail": "죄송합니다. 메일 등록이 비활성화 되었습니다.", + "non_local_account": "Gogs 계정이 아니면 암호를 변경할 수 없습니다.", + "sign_up_now": "계정이 필요하신가요? 지금 가입하세요.", + "reset_password": "비밀번호 초기화", + "invalid_code": "죄송합니다. 확인 코드가 만료되었거나 유효하지 않습니다.", + "new_password": "새 비밀번호" } diff --git a/web/src/locales/lv-LV.json b/web/src/locales/lv-LV.json index 6d2309270..882fd45f7 100644 --- a/web/src/locales/lv-LV.json +++ b/web/src/locales/lv-LV.json @@ -22,10 +22,16 @@ "language": "Valoda", "page_not_found": "Page Not Found", "username": "Lietotājvārds", + "email": "E-pasts", "password": "Parole", "auth_source": "Autentificēšanas avots", "local": "Local", "remember_me": "Atcerēties mani", "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" } diff --git a/web/src/locales/mn-MN.json b/web/src/locales/mn-MN.json index 0322ec8b1..f16f9e65b 100644 --- a/web/src/locales/mn-MN.json +++ b/web/src/locales/mn-MN.json @@ -22,10 +22,16 @@ "language": "Хэл", "page_not_found": "Хуудас олдсонгүй", "username": "Нэвтрэх нэр", + "email": "Имэйл", "password": "Нууц үг", "auth_source": "Баталгаажуулалтын эх сурвалж", "local": "Локал", "remember_me": "Сануулах", "forget_password": "Нууц үг сэргээх?", - "sign_up_now": "Данс үүсгэх бол? Одоо бүртгүүлнэ үү." + "disable_register_mail": "Уучлаарай, имэйлийн үйлчилгээ идэвхгүй байна. Сайтын админтай холбоо барина уу.", + "non_local_account": "Гадаад хэрэглэгчид нууц үгээ солих боломжгүй.", + "sign_up_now": "Данс үүсгэх бол? Одоо бүртгүүлнэ үү.", + "reset_password": "Нууц үгээ сэргээх", + "invalid_code": "Уучлаарай, таны баталгаажуулах кодын хугацаа дууссан эсвэл хүчин төгөлдөр бус байна.", + "new_password": "Шинэ нууц үг" } diff --git a/web/src/locales/nl-NL.json b/web/src/locales/nl-NL.json index dc057c74a..c14e854ef 100644 --- a/web/src/locales/nl-NL.json +++ b/web/src/locales/nl-NL.json @@ -22,10 +22,16 @@ "language": "Taal", "page_not_found": "Pagina niet gevonden", "username": "Gebruikersnaam", + "email": "E-mail", "password": "Wachtwoord", "auth_source": "Authenticatiebron", "local": "Lokaal", "remember_me": "Onthoud mij", "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" } diff --git a/web/src/locales/pl-PL.json b/web/src/locales/pl-PL.json index 273fd6ca6..fe8206343 100644 --- a/web/src/locales/pl-PL.json +++ b/web/src/locales/pl-PL.json @@ -22,10 +22,16 @@ "language": "Język", "page_not_found": "Strona nie została znaleziona", "username": "Nazwa użytkownika", + "email": "E-mail", "password": "Hasło", "auth_source": "Źródło uwierzytelniania", "local": "Lokalne", "remember_me": "Zapamiętaj mnie", "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" } diff --git a/web/src/locales/pt-BR.json b/web/src/locales/pt-BR.json index daf3c9919..7292e963c 100644 --- a/web/src/locales/pt-BR.json +++ b/web/src/locales/pt-BR.json @@ -22,10 +22,16 @@ "language": "Idioma", "page_not_found": "Página Não Encontrada", "username": "Usuário", + "email": "E-mail", "password": "Senha", "auth_source": "Fonte de autenticação", "local": "Local", "remember_me": "Lembrar de mim", "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" } diff --git a/web/src/locales/pt-PT.json b/web/src/locales/pt-PT.json index 740457010..6e8605801 100644 --- a/web/src/locales/pt-PT.json +++ b/web/src/locales/pt-PT.json @@ -22,10 +22,16 @@ "language": "Língua", "page_not_found": "Página Não Encontrada", "username": "Nome de utilizador", + "email": "Endereço de email", "password": "Palavra-chave", "auth_source": "Tipo de autenticação", "local": "Local", "remember_me": "Manter sessão iniciada", "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" } diff --git a/web/src/locales/ro-RO.json b/web/src/locales/ro-RO.json index f41b906e7..89021991d 100644 --- a/web/src/locales/ro-RO.json +++ b/web/src/locales/ro-RO.json @@ -22,10 +22,16 @@ "language": "Limba", "page_not_found": "Pagina nu a fost găsită", "username": "Numele de utilizator", + "email": "E-mail", "password": "Parolă", "auth_source": "Sursa de autentificare", "local": "Local", "remember_me": "Ține-mă minte", "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ă" } diff --git a/web/src/locales/ru-RU.json b/web/src/locales/ru-RU.json index 2d2a5f7ff..5199dd9c4 100644 --- a/web/src/locales/ru-RU.json +++ b/web/src/locales/ru-RU.json @@ -22,10 +22,16 @@ "language": "Язык", "page_not_found": "Страница не найдена", "username": "Имя пользователя", + "email": "Эл. почта", "password": "Пароль", "auth_source": "Тип аутентификации", "local": "Локальный", "remember_me": "Запомнить меня", "forget_password": "Забыли пароль?", - "sign_up_now": "Нужен аккаунт? Зарегистрируйтесь." + "disable_register_mail": "К сожалению подтверждение регистрации по почте отключено.", + "non_local_account": "Нелокальные аккаунты не могут изменить пароль через Gogs.", + "sign_up_now": "Нужен аккаунт? Зарегистрируйтесь.", + "reset_password": "Сброс пароля", + "invalid_code": "Извините, ваш код подтверждения истек или не является допустимым.", + "new_password": "Новый пароль" } diff --git a/web/src/locales/sk-SK.json b/web/src/locales/sk-SK.json index 51f5afa08..9a5b8b9cb 100644 --- a/web/src/locales/sk-SK.json +++ b/web/src/locales/sk-SK.json @@ -22,10 +22,16 @@ "language": "Jazyk", "page_not_found": "Page Not Found", "username": "Používateľské meno", + "email": "E-mail", "password": "Heslo", "auth_source": "Zdroj overovania", "local": "Lokálny", "remember_me": "Zapamätať prihlásenie", "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" } diff --git a/web/src/locales/sr-SP.json b/web/src/locales/sr-SP.json index b021ad725..8d215e6b0 100644 --- a/web/src/locales/sr-SP.json +++ b/web/src/locales/sr-SP.json @@ -22,10 +22,16 @@ "language": "Језик", "page_not_found": "Page Not Found", "username": "Корисничко име", + "email": "E-пошта", "password": "Лозинка", "auth_source": "Извор аутентикације", "local": "Локално", "remember_me": "Запамти ме", "forget_password": "Заборавили сте лозинку?", - "sign_up_now": "Немате налог? Пријавите се." + "disable_register_mail": "Извините, потврда путем поште је онемогућено.", + "non_local_account": "Нелокални налози не могу да промените лозинку преко Gogs.", + "sign_up_now": "Немате налог? Пријавите се.", + "reset_password": "Ресет лозинке", + "invalid_code": "Извините, ваш код за потврду је истекао или није валидан.", + "new_password": "Нова лозинка" } diff --git a/web/src/locales/sv-SE.json b/web/src/locales/sv-SE.json index fa1f6624c..50b3c24fd 100644 --- a/web/src/locales/sv-SE.json +++ b/web/src/locales/sv-SE.json @@ -22,10 +22,16 @@ "language": "Språk", "page_not_found": "Sidan hittades inte", "username": "Användarnamn", + "email": "E-post", "password": "Lösenord", "auth_source": "Autentiseringskälla", "local": "Lokal", "remember_me": "Kom ihåg mig", "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" } diff --git a/web/src/locales/tr-TR.json b/web/src/locales/tr-TR.json index 85958b5c9..73bc8cb0b 100644 --- a/web/src/locales/tr-TR.json +++ b/web/src/locales/tr-TR.json @@ -22,10 +22,16 @@ "language": "Dil", "page_not_found": "Sayfa Bulunamadı", "username": "Kullanıcı Adı", + "email": "E-Posta", "password": "Parola", "auth_source": "Yetkilendirme Kaynağı", "local": "Yerel", "remember_me": "Beni Hatırla", "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" } diff --git a/web/src/locales/uk-UA.json b/web/src/locales/uk-UA.json index ed6ce0b98..0b4ad1c08 100644 --- a/web/src/locales/uk-UA.json +++ b/web/src/locales/uk-UA.json @@ -22,10 +22,16 @@ "language": "Мова", "page_not_found": "Сторінку не знайдено", "username": "Ім'я користувача", + "email": "Електронна пошта", "password": "Пароль", "auth_source": "Джерело автентифікації", "local": "Локальний", "remember_me": "Запам'ятати мене", "forget_password": "Забули пароль?", - "sign_up_now": "Потрібен обліковий запис? Зареєструватися зараз." + "disable_register_mail": "На жаль, підтвердження реєстрації на електрону пошту вимкнено адміністратором.", + "non_local_account": "Нелокальні облікові записи не можуть змінити пароль через Gogs.", + "sign_up_now": "Потрібен обліковий запис? Зареєструватися зараз.", + "reset_password": "Скинути пароль", + "invalid_code": "На жаль, код підтвердження, закінчився або помилковий.", + "new_password": "Новий пароль" } diff --git a/web/src/locales/vi-VN.json b/web/src/locales/vi-VN.json index d31bec353..d34c17bcd 100644 --- a/web/src/locales/vi-VN.json +++ b/web/src/locales/vi-VN.json @@ -22,10 +22,16 @@ "language": "Ngôn ngữ", "page_not_found": "Không tìm thấy trang này!", "username": "Username", + "email": "Email", "password": "Mật khẩu", "auth_source": "Authentication Source", "local": "Local", "remember_me": "Ghi nhớ tôi", "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" } diff --git a/web/src/locales/zh-CN.json b/web/src/locales/zh-CN.json index c6916775d..7646397aa 100644 --- a/web/src/locales/zh-CN.json +++ b/web/src/locales/zh-CN.json @@ -22,10 +22,16 @@ "language": "语言选项", "page_not_found": "页面未找到", "username": "用户名", + "email": "邮箱", "password": "密码", "auth_source": "认证源", "local": "本地", "remember_me": "记住登录", "forget_password": "忘记密码?", - "sign_up_now": "还没帐户?马上注册。" + "disable_register_mail": "对不起,注册邮箱确认功能已被关闭。", + "non_local_account": "非本地类型的帐户无法通过 Gogs 修改密码。", + "sign_up_now": "还没帐户?马上注册。", + "reset_password": "重置密码", + "invalid_code": "对不起,您的确认代码已过期或已失效。", + "new_password": "新的密码" } diff --git a/web/src/locales/zh-HK.json b/web/src/locales/zh-HK.json index 99e86616e..4db60a927 100644 --- a/web/src/locales/zh-HK.json +++ b/web/src/locales/zh-HK.json @@ -22,10 +22,16 @@ "language": "語言", "page_not_found": "Page Not Found", "username": "用戶名稱", + "email": "電子郵件", "password": "密碼", "auth_source": "Authentication Source", "local": "Local", "remember_me": "記住登錄", "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": "新的密碼" } diff --git a/web/src/locales/zh-TW.json b/web/src/locales/zh-TW.json index 80fda9ce2..de56a1796 100644 --- a/web/src/locales/zh-TW.json +++ b/web/src/locales/zh-TW.json @@ -22,10 +22,16 @@ "language": "語言", "page_not_found": "找不到頁面", "username": "用戶名稱", + "email": "電子郵件", "password": "密碼", "auth_source": "認證來源", "local": "本地", "remember_me": "記住登錄", "forget_password": "忘記密碼?", - "sign_up_now": "還沒帳戶?馬上註冊。" + "disable_register_mail": "對不起,註冊郵箱確認功能已被關閉。", + "non_local_account": "非本地帳戶無法通過 Gogs 修改密碼。", + "sign_up_now": "還沒帳戶?馬上註冊。", + "reset_password": "重置密碼", + "invalid_code": "對不起,您的確認代碼已過期或已失效。", + "new_password": "新的密碼" } diff --git a/web/src/pages/Landing.tsx b/web/src/pages/Landing.tsx index 9c41defa7..231739dae 100644 --- a/web/src/pages/Landing.tsx +++ b/web/src/pages/Landing.tsx @@ -17,26 +17,26 @@ export function Landing() { gogs — zsh -

+          
             $ 
             cat /etc/motd
             {"\n"}
             Gogs
             Gogs
             {"\n"}
-            
+            
               {t("app_desc")}
             
             {"\n"}
@@ -75,7 +75,7 @@ function CmdLink({
   spa?: boolean;
 }) {
   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 = (
     <>
       {cmd}
diff --git a/web/src/pages/NotFound.tsx b/web/src/pages/NotFound.tsx
index 60c93ce69..4a075e08f 100644
--- a/web/src/pages/NotFound.tsx
+++ b/web/src/pages/NotFound.tsx
@@ -16,7 +16,7 @@ export function NotFound() {
             
             gogs — zsh
           
-          
+          
             $ 
             gogs show {path}
             {"\n"}
diff --git a/web/src/pages/ResetPassword.tsx b/web/src/pages/ResetPassword.tsx
new file mode 100644
index 000000000..b07433f52
--- /dev/null
+++ b/web/src/pages/ResetPassword.tsx
@@ -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;
+}
+
+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(null);
+  const [formError, setFormError] = useState(null);
+  const [fieldErrors, setFieldErrors] = useState>({});
+  const emailRef = useRef(null);
+  const passwordRef = useRef(null);
+  const confirmPasswordRef = useRef(null);
+
+  function onSubmit(event: React.FormEvent) {
+    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 (
+    
+ + + {title} + + {isResetForm ? renderResetContent() : renderRequestContent()} + +
+ ); + + function renderRequestContent() { + if (!emailEnabled) { + return ( +

+ {t("disable_register_mail")} +

+ ); + } + if (sent) { + return ( +
+

+ {sent.resendLimited ? ( + t("reset_password_resend_limited") + ) : ( + , hours: }} + /> + )} +

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

+ {fieldErrors.email} +

+ )} +
+ +
+
+
+ ); + } + + function renderResetContent() { + if (!valid) { + return ( +
+

+ {t("invalid_code")} +

+ +
+ ); + } + + return ( +
+
+ {renderFormError()} +
+
+ +
+ setPassword(e.target.value)} + aria-invalid={"password" in fieldErrors ? true : undefined} + aria-describedby={fieldErrors.password ? "password-error" : undefined} + className="pr-10" + /> + +
+ {fieldErrors.password && ( +

+ {fieldErrors.password} +

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

+ {fieldErrors.confirmPassword} +

+ )} +
+ +
+
+
+ ); + } + + function renderFormError() { + if (!formError) return null; + return ( +
+ {formError} +
+ ); + } + + function FormActions({ submitLabel, submitTabIndex }: { submitLabel: string; submitTabIndex: number }) { + return ( + + ); + } +} diff --git a/web/src/pages/SignIn.tsx b/web/src/pages/SignIn.tsx index 9ef6f48a8..72ac07434 100644 --- a/web/src/pages/SignIn.tsx +++ b/web/src/pages/SignIn.tsx @@ -155,7 +155,7 @@ export function SignIn() {