diff --git a/cmd/gogs/internal/web/web.go b/cmd/gogs/internal/web/web.go
index df46393f3..d98dc2c59 100644
--- a/cmd/gogs/internal/web/web.go
+++ b/cmd/gogs/internal/web/web.go
@@ -127,7 +127,6 @@ func Run(configPath string, portOverride int) error {
})
m.Group("/user", func() {
- m.Any("/activate", user.Activate)
m.Any("/activate_email", user.ActivateEmail)
m.Get("/email2user", user.Email2User)
})
diff --git a/cmd/gogs/internal/web/webapi.go b/cmd/gogs/internal/web/webapi.go
index c1d6de082..3876369d0 100644
--- a/cmd/gogs/internal/web/webapi.go
+++ b/cmd/gogs/internal/web/webapi.go
@@ -2,11 +2,13 @@ package web
import (
stdctx "context"
+ "encoding/hex"
"encoding/json"
"net/http"
"os"
"reflect"
"regexp"
+ "strconv"
"strings"
"time"
@@ -27,7 +29,7 @@ import (
"gogs.io/gogs/internal/context"
"gogs.io/gogs/internal/database"
"gogs.io/gogs/internal/email"
- "gogs.io/gogs/internal/route/user"
+ "gogs.io/gogs/internal/tool"
"gogs.io/gogs/internal/userx"
)
@@ -80,6 +82,33 @@ func webAPIBodyLimiter(c flamego.Context) {
r.Body = http.MaxBytesReader(c.ResponseWriter(), r.Body, 4*1024) // 4 KiB
}
+func parseUserFromCode(ctx stdctx.Context, code string) (user *database.User) {
+ if len(code) <= tool.TimeLimitCodeLength {
+ return nil
+ }
+
+ hexStr := code[tool.TimeLimitCodeLength:]
+ if b, err := hex.DecodeString(hexStr); err == nil {
+ if user, err = database.Handle.Users().GetByUsername(ctx, string(b)); user != nil {
+ return user
+ } else if !database.IsErrUserNotExist(err) {
+ log.Error("parseUserFromCode: get user by name %q: %v", string(b), err)
+ }
+ }
+ return nil
+}
+
+func verifyUserActiveCode(ctx stdctx.Context, code string) (user *database.User) {
+ if user = parseUserFromCode(ctx, code); user != nil {
+ prefix := code[:tool.TimeLimitCodeLength]
+ data := strconv.FormatInt(user.ID, 10) + user.Email + user.LowerName + user.Password + user.Rands
+ if tool.VerifyTimeLimitCode(data, conf.Auth.ActivateCodeLives, prefix) {
+ return user
+ }
+ }
+ return nil
+}
+
// webAPIValidator is the shared validator instance used by every webapi
// binding. Registering the json-tag name function makes validation errors
// carry the wire field name (e.g. "recoveryCode") via ve.Field(), so the
@@ -159,6 +188,12 @@ func mountWebAPIRoutes(f *flamego.Flame) {
Post(bindJSON(userMFARequest{}), postUserMFA)
f.Post("/recovery", bindJSON(userMFARecoveryRequest{}), postUserMFARecovery)
})
+ f.Group("/activate", func() {
+ f.Combo("").
+ Get(getUserActivate).
+ Post(postUserActivate)
+ f.Post("/complete", bindJSON(userActivateCompleteRequest{}), postUserActivateComplete)
+ })
f.Post("/sign-out", postUserSignOut)
})
}, webAPIBodyLimiter)
@@ -368,7 +403,7 @@ func getUserResetPassword(r *http.Request) (statusCode int, resp *getUserResetPa
code := r.URL.Query().Get("code")
return http.StatusOK, &getUserResetPasswordResponse{
EmailEnabled: conf.Email.Enabled,
- Valid: code != "" && user.VerifyUserActiveCode(code) != nil,
+ Valid: code != "" && verifyUserActiveCode(r.Context(), code) != nil,
}, nil
}
@@ -426,7 +461,7 @@ func postUserResetPassword(r *http.Request, ca cache.Cache, l i18n.Locale, req u
}
func postUserResetPasswordComplete(r *http.Request, l i18n.Locale, req userResetPasswordCompleteRequest) (statusCode int, resp any, err error) {
- u := user.VerifyUserActiveCode(req.Code)
+ u := verifyUserActiveCode(r.Context(), req.Code)
if u == nil {
return http.StatusBadRequest, &bindingErrorResponse{Error: l.Tr("auth.invalid_code")}, nil
}
@@ -600,6 +635,88 @@ func getUserInfo(user *database.User) (statusCode int, resp *userInfo, err error
nil
}
+type getUserActivateResponse struct {
+ Email string `json:"email,omitempty"`
+ CodeLifetimeHours int `json:"codeLifetimeHours,omitempty"`
+}
+
+func getUserActivate(u *database.User) (statusCode int, resp any, err error) {
+ if u == nil {
+ return http.StatusUnauthorized, nil, nil
+ }
+ // An already-active and authenticated user has no business on the activation page.
+ if u.IsActive {
+ return http.StatusNotFound, nil, nil
+ }
+ return http.StatusOK, &getUserActivateResponse{
+ Email: u.Email,
+ CodeLifetimeHours: conf.Auth.ActivateCodeLives / 60,
+ }, nil
+}
+
+type postUserActivateResponse struct {
+ RateLimited bool `json:"rateLimited,omitempty"`
+ CodeLifetimeHours int `json:"codeLifetimeHours,omitempty"`
+}
+
+func postUserActivate(r *http.Request, u *database.User, mc *macaron.Context, ca cache.Cache, l i18n.Locale) (statusCode int, resp any, err error) {
+ if u == nil {
+ return http.StatusUnauthorized, nil, nil
+ }
+ if u.IsActive {
+ return http.StatusNotFound, nil, nil
+ }
+ if !conf.Auth.RequireEmailConfirmation {
+ return http.StatusForbidden, &bindingErrorResponse{Error: l.Tr("auth.disable_register_mail")}, nil
+ }
+
+ ctx := r.Context()
+ if _, err := ca.Get(ctx, userx.MailResendCacheKey(u.ID)); err == nil {
+ return http.StatusOK, &postUserActivateResponse{
+ RateLimited: true,
+ CodeLifetimeHours: conf.Auth.ActivateCodeLives / 60,
+ }, nil
+ } else if !errors.Is(err, os.ErrNotExist) {
+ log.Error("postUserActivate: get mail resend cache for user %q: %v", u.Name, err)
+ }
+
+ if err := email.SendActivateAccountMail(mc, database.NewMailerUser(u)); err != nil {
+ log.Error("postUserActivate: send activation 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("postUserActivate: put mail resend cache for user %q: %v", u.Name, err)
+ }
+ return http.StatusOK, &postUserActivateResponse{CodeLifetimeHours: conf.Auth.ActivateCodeLives / 60}, nil
+}
+
+type userActivateCompleteRequest struct {
+ Code string `json:"code" validate:"required"`
+}
+
+func postUserActivateComplete(r *http.Request, sess session.Session, mc *macaron.Context, l i18n.Locale, req userActivateCompleteRequest) (statusCode int, resp any, err error) {
+ target := verifyUserActiveCode(r.Context(), req.Code)
+ if target == nil {
+ return http.StatusBadRequest, &bindingErrorResponse{Error: l.Tr("auth.invalid_code")}, nil
+ }
+
+ v := true
+ if err := database.Handle.Users().Update(
+ r.Context(),
+ target.ID,
+ database.UpdateUserOptions{
+ GenerateNewRands: true,
+ IsActivated: &v,
+ },
+ ); err != nil {
+ log.Error("postUserActivateComplete: update user %q: %v", target.Name, err)
+ return http.StatusInternalServerError, nil, errors.Wrap(err, "update user")
+ }
+
+ log.Trace("User activated: %s", target.Name)
+ completeSignIn(sess, mc, target)
+ return http.StatusNoContent, nil, nil
+}
+
type postUserSignOutResponse struct {
RedirectTo string `json:"redirectTo,omitempty"`
}
diff --git a/conf/locale/locale_en-US.ini b/conf/locale/locale_en-US.ini
index a67be812f..038ff6948 100644
--- a/conf/locale/locale_en-US.ini
+++ b/conf/locale/locale_en-US.ini
@@ -194,12 +194,18 @@ forgot_password= Forgot Password
forget_password = Forgot password?
sign_up_now = Create a new account
confirmation_email_sent = A new confirmation email has been sent to %s, please check your inbox within the next %d hours to complete the registration process.
-active_your_account = Activate Your Account
+activate_your_account = Activate your account
prohibit_login = Login Prohibited
prohibit_login_desc = Your account is prohibited from logging in. Please contact the site admin.
-resent_limit_prompt = Sorry, you already requested an activation email recently. Please wait 3 minutes then try again.
+resend_rate_limited = 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_activation_email = Send activation email
+check_activation_email = Please check your email and click the activation link to finish creating your account.
+activation_email_pending = Your email address {email} is not yet confirmed. Click below to send a new activation email valid for {hours} hours.
+activation_email_sent = A new activation email has been sent to {email}. Please check your inbox within {hours} hours.
+sending_activation_email = Sending activation email...
+send_activation_email_failed = Could not send activation email, please try again.
+activating_account = Activating your account...
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.
diff --git a/internal/context/auth.go b/internal/context/auth.go
index 6094547c0..a8c26faa1 100644
--- a/internal/context/auth.go
+++ b/internal/context/auth.go
@@ -80,8 +80,9 @@ func Toggle(options *ToggleOptions) macaron.Handler {
c.RedirectSubpath("/user/sign-in")
return
} else if !c.User.IsActive && conf.Auth.RequireEmailConfirmation {
- c.Title("auth.active_your_account")
- c.Success("user/auth/activate")
+ // Inactive users get bounced to the React activation page, which
+ // is responsible for offering a resend and showing status.
+ c.RedirectSubpath("/user/activate")
return
}
}
diff --git a/internal/route/home.go b/internal/route/home.go
index 5f8a3e49b..f917e3c28 100644
--- a/internal/route/home.go
+++ b/internal/route/home.go
@@ -20,8 +20,7 @@ const (
func Home(c *context.Context) {
if c.IsLogged {
if !c.User.IsActive && conf.Auth.RequireEmailConfirmation {
- c.Data["Title"] = c.Tr("auth.active_your_account")
- c.Success(user.TmplUserAuthActivate)
+ c.RedirectSubpath("/user/activate")
} else {
user.Dashboard(c)
}
diff --git a/internal/route/user/auth.go b/internal/route/user/auth.go
deleted file mode 100644
index b292075d9..000000000
--- a/internal/route/user/auth.go
+++ /dev/null
@@ -1,149 +0,0 @@
-package user
-
-import (
- gocontext "context"
- "encoding/hex"
- "strconv"
-
- log "unknwon.dev/clog/v2"
-
- "gogs.io/gogs/internal/conf"
- "gogs.io/gogs/internal/context"
- "gogs.io/gogs/internal/database"
- "gogs.io/gogs/internal/email"
- "gogs.io/gogs/internal/tool"
- "gogs.io/gogs/internal/userx"
-)
-
-const TmplUserAuthActivate = "user/auth/activate"
-
-// parseUserFromCode returns user by username encoded in code.
-// It returns nil if code or username is invalid.
-func parseUserFromCode(code string) (user *database.User) {
- if len(code) <= tool.TimeLimitCodeLength {
- return nil
- }
-
- // Use tail hex username to query user
- hexStr := code[tool.TimeLimitCodeLength:]
- if b, err := hex.DecodeString(hexStr); err == nil {
- if user, err = database.Handle.Users().GetByUsername(gocontext.TODO(), string(b)); user != nil {
- return user
- } else if !database.IsErrUserNotExist(err) {
- log.Error("Failed to get user by name %q: %v", string(b), err)
- }
- }
-
- return nil
-}
-
-// 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 {
- // time limit code
- prefix := code[:tool.TimeLimitCodeLength]
- data := strconv.FormatInt(user.ID, 10) + user.Email + user.LowerName + user.Password + user.Rands
-
- if tool.VerifyTimeLimitCode(data, minutes, prefix) {
- return user
- }
- }
- return nil
-}
-
-// verify active code when active account
-func verifyActiveEmailCode(code, email string) *database.EmailAddress {
- minutes := conf.Auth.ActivateCodeLives
-
- if user := parseUserFromCode(code); user != nil {
- // time limit code
- prefix := code[:tool.TimeLimitCodeLength]
- data := strconv.FormatInt(user.ID, 10) + email + user.LowerName + user.Password + user.Rands
-
- if tool.VerifyTimeLimitCode(data, minutes, prefix) {
- emailAddress, err := database.Handle.Users().GetEmail(gocontext.TODO(), user.ID, email, false)
- if err == nil {
- return emailAddress
- }
- }
- }
- return nil
-}
-
-func Activate(c *context.Context) {
- code := c.Query("code")
- if code == "" {
- c.Data["IsActivatePage"] = true
- if c.User.IsActive {
- c.NotFound()
- return
- }
- // Resend confirmation email.
- if conf.Auth.RequireEmailConfirmation {
- if c.Cache.IsExist(userx.MailResendCacheKey(c.User.ID)) {
- c.Data["ResendLimited"] = true
- } else {
- c.Data["Hours"] = conf.Auth.ActivateCodeLives / 60
- if err := email.SendActivateAccountMail(c.Context, database.NewMailerUser(c.User)); err != nil {
- log.Error("Failed to send activate account mail: %v", err)
- }
-
- if err := c.Cache.Put(userx.MailResendCacheKey(c.User.ID), 1, 180); err != nil {
- log.Error("Failed to put cache key 'mail resend': %v", err)
- }
- }
- } else {
- c.Data["ServiceNotEnabled"] = true
- }
- c.Success(TmplUserAuthActivate)
- return
- }
-
- // Verify code.
- if user := VerifyUserActiveCode(code); user != nil {
- v := true
- err := database.Handle.Users().Update(
- c.Req.Context(),
- user.ID,
- database.UpdateUserOptions{
- GenerateNewRands: true,
- IsActivated: &v,
- },
- )
- if err != nil {
- c.Error(err, "update user")
- return
- }
-
- log.Trace("User activated: %s", user.Name)
-
- _ = c.Session.Set("uid", user.ID)
- _ = c.Session.Set("uname", user.Name)
- c.RedirectSubpath("/")
- return
- }
-
- c.Data["IsActivateFailed"] = true
- c.Success(TmplUserAuthActivate)
-}
-
-func ActivateEmail(c *context.Context) {
- code := c.Query("code")
- emailAddr := c.Query("email")
-
- // Verify code.
- if email := verifyActiveEmailCode(code, emailAddr); email != nil {
- err := database.Handle.Users().MarkEmailActivated(c.Req.Context(), email.UserID, email.Email)
- if err != nil {
- c.Error(err, "activate email")
- return
- }
-
- log.Trace("Email activated: %s", email.Email)
- c.Flash.Success(c.Tr("settings.add_email_success"))
- }
-
- c.RedirectSubpath("/user/settings/email")
-}
diff --git a/internal/route/user/setting.go b/internal/route/user/setting.go
index b642047ab..a5237e52e 100644
--- a/internal/route/user/setting.go
+++ b/internal/route/user/setting.go
@@ -4,11 +4,13 @@ import (
"bytes"
gocontext "context"
"encoding/base64"
+ "encoding/hex"
"fmt"
"html/template"
"image/png"
"io"
"net/http"
+ "strconv"
"github.com/cockroachdb/errors"
"github.com/pquerna/otp"
@@ -240,6 +242,54 @@ func SettingsEmails(c *context.Context) {
c.Success(tmplUserSettingsEmail)
}
+func parseUserFromCode(ctx gocontext.Context, code string) (user *database.User) {
+ if len(code) <= tool.TimeLimitCodeLength {
+ return nil
+ }
+
+ hexStr := code[tool.TimeLimitCodeLength:]
+ if b, err := hex.DecodeString(hexStr); err == nil {
+ if user, err = database.Handle.Users().GetByUsername(ctx, string(b)); user != nil {
+ return user
+ } else if !database.IsErrUserNotExist(err) {
+ log.Error("parseUserFromCode: get user by name %q: %v", string(b), err)
+ }
+ }
+ return nil
+}
+
+func verifyActiveEmailCode(ctx gocontext.Context, code, email string) *database.EmailAddress {
+ if user := parseUserFromCode(ctx, code); user != nil {
+ prefix := code[:tool.TimeLimitCodeLength]
+ data := strconv.FormatInt(user.ID, 10) + email + user.LowerName + user.Password + user.Rands
+ if tool.VerifyTimeLimitCode(data, conf.Auth.ActivateCodeLives, prefix) {
+ emailAddress, err := database.Handle.Users().GetEmail(ctx, user.ID, email, false)
+ if err == nil {
+ return emailAddress
+ }
+ }
+ }
+ return nil
+}
+
+func ActivateEmail(c *context.Context) {
+ code := c.Query("code")
+ emailAddr := c.Query("email")
+
+ if email := verifyActiveEmailCode(c.Req.Context(), code, emailAddr); email != nil {
+ err := database.Handle.Users().MarkEmailActivated(c.Req.Context(), email.UserID, email.Email)
+ if err != nil {
+ c.Error(err, "activate email")
+ return
+ }
+
+ log.Trace("Email activated: %s", email.Email)
+ c.Flash.Success(c.Tr("settings.add_email_success"))
+ }
+
+ c.RedirectSubpath("/user/settings/email")
+}
+
func SettingsEmailPost(c *context.Context, f form.AddEmail) {
c.Title("settings.emails")
c.PageIs("SettingsEmails")
diff --git a/templates/user/auth/activate.tmpl b/templates/user/auth/activate.tmpl
deleted file mode 100644
index e44bb4ea7..000000000
--- a/templates/user/auth/activate.tmpl
+++ /dev/null
@@ -1,38 +0,0 @@
-{{template "base/head" .}}
-
-{{template "base/footer" .}}
diff --git a/web/scripts/extract-locales.mjs b/web/scripts/extract-locales.mjs
index b9a84e692..acde3d3cc 100644
--- a/web/scripts/extract-locales.mjs
+++ b/web/scripts/extract-locales.mjs
@@ -63,7 +63,6 @@ const REUSED_KEYS = [
"disable_register_prompt",
"reset_password_resend_limited",
"non_local_account",
- "confirmation_email_sent",
"create_new_account",
"register_hepler_msg",
"sign_up",
@@ -98,6 +97,15 @@ const REUSED_KEYS = [
"mfa_verifying",
"mfa_session_expired",
"mfa_verify_failed",
+ "activate_your_account",
+ "resend_rate_limited",
+ "send_activation_email",
+ "check_activation_email",
+ "activation_email_pending",
+ "activation_email_sent",
+ "sending_activation_email",
+ "send_activation_email_failed",
+ "activating_account",
];
// Lightweight INI parser: handles `key = value` and `key=value`, ignores
diff --git a/web/src/locales/en-US.json b/web/src/locales/en-US.json
index 677952fe8..01866fed2 100644
--- a/web/src/locales/en-US.json
+++ b/web/src/locales/en-US.json
@@ -49,7 +49,6 @@
"disable_register_prompt": "Sorry, registration has been disabled. Please contact the site administrator.",
"reset_password_resend_limited": "You already requested a password reset email recently. Please wait 3 minutes then try again.",
"non_local_account": "Non-local accounts cannot change passwords through Gogs.",
- "confirmation_email_sent": "A new confirmation email has been sent to %s, please check your inbox within the next %d hours to complete the registration process.",
"create_new_account": "Create new account",
"register_hepler_msg": "Already have an account? Sign in now!",
"sign_up": "Sign up",
@@ -83,5 +82,14 @@
"mfa_verify": "Verify",
"mfa_verifying": "Verifying...",
"mfa_session_expired": "Your sign-in session has expired. Please sign in again.",
- "mfa_verify_failed": "Verification failed. Please try again."
+ "mfa_verify_failed": "Verification failed. Please try again.",
+ "activate_your_account": "Activate your account",
+ "resend_rate_limited": "Sorry, you already requested an activation email recently. Please wait 3 minutes then try again.",
+ "send_activation_email": "Send activation email",
+ "check_activation_email": "Please check your email and click the activation link to finish creating your account.",
+ "activation_email_pending": "Your email address {email} is not yet confirmed. Click below to send a new activation email valid for {hours} hours.",
+ "activation_email_sent": "A new activation email has been sent to {email}. Please check your inbox within {hours} hours.",
+ "sending_activation_email": "Sending activation email...",
+ "send_activation_email_failed": "Could not send activation email, please try again.",
+ "activating_account": "Activating your account..."
}
diff --git a/web/src/pages/user/Activate.tsx b/web/src/pages/user/Activate.tsx
new file mode 100644
index 000000000..a3fd7e7eb
--- /dev/null
+++ b/web/src/pages/user/Activate.tsx
@@ -0,0 +1,174 @@
+import { getRouteApi } from "@tanstack/react-router";
+import { useEffect, 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 { usePageTitle } from "@/lib/page-title";
+import { subUrl } from "@/lib/url";
+import { useUserInfo } from "@/lib/use-user-info";
+
+export interface ActivatePage {
+ code: string;
+ email: string;
+ codeLifetimeHours: number;
+}
+
+interface ActivateResponse {
+ rateLimited?: boolean;
+ codeLifetimeHours?: number;
+}
+
+interface ActivateErrorResponse {
+ error?: string;
+}
+
+const route = getRouteApi("/user/activate");
+
+export function Activate() {
+ const { t } = useTranslation();
+ const { code, email, codeLifetimeHours } = route.useLoaderData();
+ const authenticated = useUserInfo() !== null;
+ usePageTitle(t("activate_your_account"));
+
+ const isVerifying = code !== "";
+ const [verifyFailed, setVerifyFailed] = useState(false);
+ const [submitting, setSubmitting] = useState(false);
+ const [resent, setResent] = useState(null);
+ const [formError, setFormError] = useState(null);
+ const verifyOnceRef = useRef(false);
+
+ useEffect(() => {
+ if (!isVerifying || verifyOnceRef.current) return;
+ verifyOnceRef.current = true;
+ void (async () => {
+ try {
+ const res = await fetch(subUrl("/api/web/user/activate/complete"), {
+ method: "POST",
+ credentials: "same-origin",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ code }),
+ });
+ if (!res.ok) {
+ setVerifyFailed(true);
+ return;
+ }
+ window.location.assign(subUrl("/"));
+ } catch {
+ setVerifyFailed(true);
+ }
+ })();
+ }, [isVerifying, code]);
+
+ function onResend(event: React.FormEvent) {
+ event.preventDefault();
+ setFormError(null);
+ setSubmitting(true);
+ void (async () => {
+ try {
+ const res = await fetch(subUrl("/api/web/user/activate"), {
+ method: "POST",
+ credentials: "same-origin",
+ });
+ if (!res.ok) {
+ const body = (await res.json().catch(() => ({}))) as ActivateErrorResponse;
+ setFormError(body.error ?? t("send_activation_email_failed"));
+ setSubmitting(false);
+ return;
+ }
+ setResent((await res.json()) as ActivateResponse);
+ setSubmitting(false);
+ } catch {
+ setFormError(t("send_activation_email_failed"));
+ setSubmitting(false);
+ }
+ })();
+ }
+
+ return (
+
+
+
+ {t("activate_your_account")}
+
+ {renderContent()}
+
+
+ );
+
+ function renderContent() {
+ if (isVerifying) {
+ if (verifyFailed) {
+ return (
+
+ );
+ }
+ return (
+
+ {t("activating_account")}
+
+ );
+ }
+
+ if (!authenticated) {
+ return (
+
+ );
+ }
+
+ if (resent) {
+ return (
+
+ {resent.rateLimited ? (
+ t("resend_rate_limited")
+ ) : (
+ , hours: }}
+ />
+ )}
+
+ );
+ }
+
+ return (
+
+ );
+ }
+}
diff --git a/web/src/pages/MFA.tsx b/web/src/pages/user/MFA.tsx
similarity index 100%
rename from web/src/pages/MFA.tsx
rename to web/src/pages/user/MFA.tsx
diff --git a/web/src/pages/ResetPassword.tsx b/web/src/pages/user/ResetPassword.tsx
similarity index 100%
rename from web/src/pages/ResetPassword.tsx
rename to web/src/pages/user/ResetPassword.tsx
diff --git a/web/src/pages/SignIn.tsx b/web/src/pages/user/SignIn.tsx
similarity index 100%
rename from web/src/pages/SignIn.tsx
rename to web/src/pages/user/SignIn.tsx
diff --git a/web/src/pages/SignUp.tsx b/web/src/pages/user/SignUp.tsx
similarity index 98%
rename from web/src/pages/SignUp.tsx
rename to web/src/pages/user/SignUp.tsx
index f9db11ee0..17eb02049 100644
--- a/web/src/pages/SignUp.tsx
+++ b/web/src/pages/user/SignUp.tsx
@@ -1,6 +1,6 @@
import { getRouteApi, useNavigate } from "@tanstack/react-router";
import { useRef, useState } from "react";
-import { useTranslation } from "react-i18next";
+import { Trans, useTranslation } from "react-i18next";
import { PasswordInput } from "@/components/PasswordInput";
import { Button } from "@/components/ui/button";
@@ -140,10 +140,11 @@ export function SignUp() {
return (
- {t("confirmation_email_sent")
- .replace(/<[^>]+>/g, "")
- .replace("%s", sent.email!)
- .replace("%d", String(sent.hours))}
+ , hours: }}
+ />