From 4e2e87931456407161fdefad4e5f7a1c5837f1c0 Mon Sep 17 00:00:00 2001
From: Matt Roberts
Date: Fri, 7 Nov 2025 19:57:39 +0000
Subject: [PATCH 01/11] First stab at login codes.
---
app/actions/signin.go | 45 +++++-
app/actions/signin_test.go | 55 +++++++-
app/cmd/routes.go | 2 +
app/handlers/signin.go | 108 +++++++++++++-
app/handlers/signin_test.go | 157 ++++++++++++++++++++-
app/models/entity/email_verification.go | 5 +
app/models/query/tenant.go | 9 ++
app/pkg/rand/random.go | 20 +++
app/services/sqlstore/postgres/postgres.go | 1 +
app/services/sqlstore/postgres/tenant.go | 15 ++
app/tasks/signin.go | 5 +-
app/tasks/signin_test.go | 1 +
e2e/step_definitions/fns.ts | 16 +++
e2e/step_definitions/home.steps.ts | 15 +-
e2e/step_definitions/user.steps.ts | 11 +-
locale/en/client.json | 16 ++-
public/components/common/SignInControl.tsx | 111 ++++++++++++---
public/services/actions/tenant.ts | 15 +-
views/email/signin_email.html | 11 +-
19 files changed, 570 insertions(+), 48 deletions(-)
diff --git a/app/actions/signin.go b/app/actions/signin.go
index 4b5a3ebeb..8836394d3 100644
--- a/app/actions/signin.go
+++ b/app/actions/signin.go
@@ -14,13 +14,13 @@ import (
// SignInByEmail happens when user request to sign in by email
type SignInByEmail struct {
- Email string `json:"email" format:"lower"`
- VerificationKey string
+ Email string `json:"email" format:"lower"`
+ VerificationCode string
}
func NewSignInByEmail() *SignInByEmail {
return &SignInByEmail{
- VerificationKey: entity.GenerateEmailVerificationKey(),
+ VerificationCode: entity.GenerateEmailVerificationCode(),
}
}
@@ -74,6 +74,45 @@ func (action *SignInByEmail) GetKind() enum.EmailVerificationKind {
return enum.EmailVerificationKindSignIn
}
+// VerifySignInCode happens when user enters the verification code received via email
+type VerifySignInCode struct {
+ Email string `json:"email" format:"lower"`
+ Code string `json:"code"`
+}
+
+// IsAuthorized returns true if current user is authorized to perform this action
+func (action *VerifySignInCode) IsAuthorized(ctx context.Context, user *entity.User) bool {
+ return true
+}
+
+// Validate if current model is valid
+func (action *VerifySignInCode) Validate(ctx context.Context, user *entity.User) *validate.Result {
+ result := validate.Success()
+
+ if action.Email == "" {
+ result.AddFieldFailure("email", propertyIsRequired(ctx, "email"))
+ } else {
+ messages := validate.Email(ctx, action.Email)
+ result.AddFieldFailure("email", messages...)
+ }
+
+ if action.Code == "" {
+ result.AddFieldFailure("code", propertyIsRequired(ctx, "code"))
+ } else if len(action.Code) != 6 {
+ result.AddFieldFailure("code", "Verification code must be 6 digits")
+ } else {
+ // Validate that code contains only digits
+ for _, char := range action.Code {
+ if char < '0' || char > '9' {
+ result.AddFieldFailure("code", "Verification code must contain only digits")
+ break
+ }
+ }
+ }
+
+ return result
+}
+
// CompleteProfile happens when users completes their profile during first time sign in
type CompleteProfile struct {
Kind enum.EmailVerificationKind `json:"kind"`
diff --git a/app/actions/signin_test.go b/app/actions/signin_test.go
index 156ba5952..90efad559 100644
--- a/app/actions/signin_test.go
+++ b/app/actions/signin_test.go
@@ -32,7 +32,8 @@ func TestSignInByEmail_ShouldHaveVerificationKey(t *testing.T) {
result := action.Validate(context.Background(), nil)
ExpectSuccess(result)
- Expect(action.VerificationKey).IsNotEmpty()
+ Expect(action.VerificationCode).IsNotEmpty()
+ Expect(len(action.VerificationCode)).Equals(6)
}
func TestCompleteProfile_EmptyNameAndKey(t *testing.T) {
@@ -52,3 +53,55 @@ func TestCompleteProfile_LongName(t *testing.T) {
result := action.Validate(context.Background(), nil)
ExpectFailed(result, "name", "key")
}
+
+func TestVerifySignInCode_EmptyEmail(t *testing.T) {
+ RegisterT(t)
+
+ action := actions.VerifySignInCode{Email: "", Code: "123456"}
+ result := action.Validate(context.Background(), nil)
+ ExpectFailed(result, "email")
+}
+
+func TestVerifySignInCode_InvalidEmail(t *testing.T) {
+ RegisterT(t)
+
+ action := actions.VerifySignInCode{Email: "invalid", Code: "123456"}
+ result := action.Validate(context.Background(), nil)
+ ExpectFailed(result, "email")
+}
+
+func TestVerifySignInCode_EmptyCode(t *testing.T) {
+ RegisterT(t)
+
+ action := actions.VerifySignInCode{Email: "jon.snow@got.com", Code: ""}
+ result := action.Validate(context.Background(), nil)
+ ExpectFailed(result, "code")
+}
+
+func TestVerifySignInCode_InvalidCodeLength(t *testing.T) {
+ RegisterT(t)
+
+ action := actions.VerifySignInCode{Email: "jon.snow@got.com", Code: "12345"}
+ result := action.Validate(context.Background(), nil)
+ ExpectFailed(result, "code")
+
+ action2 := actions.VerifySignInCode{Email: "jon.snow@got.com", Code: "1234567"}
+ result2 := action2.Validate(context.Background(), nil)
+ ExpectFailed(result2, "code")
+}
+
+func TestVerifySignInCode_NonNumericCode(t *testing.T) {
+ RegisterT(t)
+
+ action := actions.VerifySignInCode{Email: "jon.snow@got.com", Code: "12345A"}
+ result := action.Validate(context.Background(), nil)
+ ExpectFailed(result, "code")
+}
+
+func TestVerifySignInCode_ValidCodeAndEmail(t *testing.T) {
+ RegisterT(t)
+
+ action := actions.VerifySignInCode{Email: "jon.snow@got.com", Code: "123456"}
+ result := action.Validate(context.Background(), nil)
+ ExpectSuccess(result)
+}
diff --git a/app/cmd/routes.go b/app/cmd/routes.go
index 389cb1888..0e35a4222 100644
--- a/app/cmd/routes.go
+++ b/app/cmd/routes.go
@@ -123,6 +123,8 @@ func routes(r *web.Engine) *web.Engine {
r.Get("/invite/verify", handlers.VerifySignInKey(enum.EmailVerificationKindUserInvitation))
r.Post("/_api/signin/complete", handlers.CompleteSignInProfile())
r.Post("/_api/signin", handlers.SignInByEmail())
+ r.Post("/_api/signin/verify", handlers.VerifySignInCode())
+ r.Post("/_api/signin/resend", handlers.ResendSignInCode())
// Block if it's private tenant with unauthenticated user
r.Use(middlewares.CheckTenantPrivacy())
diff --git a/app/handlers/signin.go b/app/handlers/signin.go
index 1f2812019..1c55ba916 100644
--- a/app/handlers/signin.go
+++ b/app/handlers/signin.go
@@ -57,7 +57,7 @@ func NotInvitedPage() web.HandlerFunc {
}
}
-// SignInByEmail sends a new email with verification key
+// SignInByEmail sends a new email with verification code
func SignInByEmail() web.HandlerFunc {
return func(c *web.Context) error {
action := actions.NewSignInByEmail()
@@ -66,15 +66,115 @@ func SignInByEmail() web.HandlerFunc {
}
err := bus.Dispatch(c, &cmd.SaveVerificationKey{
- Key: action.VerificationKey,
- Duration: 30 * time.Minute,
+ Key: action.VerificationCode,
+ Duration: 15 * time.Minute,
Request: action,
})
if err != nil {
return c.Failure(err)
}
- c.Enqueue(tasks.SendSignInEmail(action.Email, action.VerificationKey))
+ c.Enqueue(tasks.SendSignInEmail(action.Email, action.VerificationCode))
+
+ return c.Ok(web.Map{})
+ }
+}
+
+// VerifySignInCode verifies the code entered by the user and signs them in
+func VerifySignInCode() web.HandlerFunc {
+ return func(c *web.Context) error {
+ action := &actions.VerifySignInCode{}
+ if result := c.BindTo(action); !result.Ok {
+ return c.HandleValidation(result)
+ }
+
+ // Get verification by email and code
+ verification := &query.GetVerificationByEmailAndCode{
+ Email: action.Email,
+ Code: action.Code,
+ Kind: enum.EmailVerificationKindSignIn,
+ }
+ err := bus.Dispatch(c, verification)
+ if err != nil {
+ if errors.Cause(err) == app.ErrNotFound {
+ return c.BadRequest(web.Map{
+ "code": "Invalid or expired verification code",
+ })
+ }
+ return c.Failure(err)
+ }
+
+ result := verification.Result
+
+ // Check if already verified (with grace period)
+ if result.VerifiedAt != nil {
+ if time.Since(*result.VerifiedAt) > 5*time.Minute {
+ return c.Gone()
+ }
+ } else {
+ // Check if expired
+ if time.Now().After(result.ExpiresAt) {
+ // Mark as verified to prevent reuse
+ _ = bus.Dispatch(c, &cmd.SetKeyAsVerified{Key: action.Code})
+ return c.Gone()
+ }
+ }
+
+ // Check if user exists
+ userByEmail := &query.GetUserByEmail{Email: result.Email}
+ err = bus.Dispatch(c, userByEmail)
+ if err != nil {
+ if errors.Cause(err) == app.ErrNotFound {
+ // User doesn't exist, need to complete profile
+ if c.Tenant().IsPrivate {
+ return c.Forbidden()
+ }
+ return c.Ok(web.Map{
+ "showProfileCompletion": true,
+ })
+ }
+ return c.Failure(err)
+ }
+
+ // Mark code as verified
+ err = bus.Dispatch(c, &cmd.SetKeyAsVerified{Key: action.Code})
+ if err != nil {
+ return c.Failure(err)
+ }
+
+ // Authenticate user
+ webutil.AddAuthUserCookie(c, userByEmail.Result)
+
+ return c.Ok(web.Map{})
+ }
+}
+
+// ResendSignInCode invalidates the previous code and sends a new one
+func ResendSignInCode() web.HandlerFunc {
+ return func(c *web.Context) error {
+ // Create new sign-in action with new code
+ action := actions.NewSignInByEmail()
+ if result := c.BindTo(action); !result.Ok {
+ return c.HandleValidation(result)
+ }
+
+ // Authorization check
+ if !action.IsAuthorized(c, c.User()) {
+ return c.Forbidden()
+ }
+
+ // Save new verification code
+ err := bus.Dispatch(c, &cmd.SaveVerificationKey{
+ Key: action.VerificationCode,
+ Duration: 15 * time.Minute,
+ Request: action,
+ })
+ if err != nil {
+ return c.Failure(err)
+ }
+
+ // Send new email
+ c.Enqueue(tasks.SendSignInEmail(action.Email, action.VerificationCode))
return c.Ok(web.Map{})
}
diff --git a/app/handlers/signin_test.go b/app/handlers/signin_test.go
index 0bff24c2b..01c508a70 100644
--- a/app/handlers/signin_test.go
+++ b/app/handlers/signin_test.go
@@ -49,7 +49,7 @@ func TestSignInByEmailHandler_WithEmail(t *testing.T) {
ExecutePost(handlers.SignInByEmail(), `{ "email": "jon.snow@got.com" }`)
Expect(code).Equals(http.StatusOK)
- Expect(saveKeyCmd.Key).HasLen(64)
+ Expect(saveKeyCmd.Key).HasLen(6)
Expect(saveKeyCmd.Request.GetKind()).Equals(enum.EmailVerificationKindSignIn)
Expect(saveKeyCmd.Request.GetEmail()).Equals("jon.snow@got.com")
Expect(saveKeyCmd.Request.GetName()).Equals("")
@@ -642,6 +642,161 @@ func TestSignInPageHandler_PrivateTenant_UnauthenticatedUser(t *testing.T) {
Expect(code).Equals(http.StatusOK)
}
+func TestVerifySignInCodeHandler_InvalidCode(t *testing.T) {
+ RegisterT(t)
+
+ bus.AddHandler(func(ctx context.Context, q *query.GetVerificationByEmailAndCode) error {
+ return app.ErrNotFound
+ })
+
+ server := mock.NewServer()
+ code, _ := server.
+ OnTenant(mock.DemoTenant).
+ ExecutePost(handlers.VerifySignInCode(), `{ "email": "jon.snow@got.com", "code": "999999" }`)
+
+ Expect(code).Equals(http.StatusBadRequest)
+}
+
+func TestVerifySignInCodeHandler_ExpiredCode(t *testing.T) {
+ RegisterT(t)
+
+ bus.AddHandler(func(ctx context.Context, q *query.GetVerificationByEmailAndCode) error {
+ q.Result = &entity.EmailVerification{
+ Email: "jon.snow@got.com",
+ Key: "123456",
+ CreatedAt: time.Now().Add(-20 * time.Minute),
+ ExpiresAt: time.Now().Add(-5 * time.Minute),
+ }
+ return nil
+ })
+
+ bus.AddHandler(func(ctx context.Context, c *cmd.SetKeyAsVerified) error {
+ return nil
+ })
+
+ server := mock.NewServer()
+ code, _ := server.
+ OnTenant(mock.DemoTenant).
+ ExecutePost(handlers.VerifySignInCode(), `{ "email": "jon.snow@got.com", "code": "123456" }`)
+
+ Expect(code).Equals(http.StatusGone)
+}
+
+func TestVerifySignInCodeHandler_CorrectCode_ExistingUser(t *testing.T) {
+ RegisterT(t)
+
+ bus.AddHandler(func(ctx context.Context, q *query.GetVerificationByEmailAndCode) error {
+ q.Result = &entity.EmailVerification{
+ Email: "jon.snow@got.com",
+ Key: "123456",
+ CreatedAt: time.Now(),
+ ExpiresAt: time.Now().Add(15 * time.Minute),
+ }
+ return nil
+ })
+
+ bus.AddHandler(func(ctx context.Context, q *query.GetUserByEmail) error {
+ q.Result = mock.JonSnow
+ return nil
+ })
+
+ bus.AddHandler(func(ctx context.Context, c *cmd.SetKeyAsVerified) error {
+ return nil
+ })
+
+ server := mock.NewServer()
+ code, response := server.
+ OnTenant(mock.DemoTenant).
+ ExecutePost(handlers.VerifySignInCode(), `{ "email": "jon.snow@got.com", "code": "123456" }`)
+
+ Expect(code).Equals(http.StatusOK)
+ ExpectFiderAuthCookie(response, mock.JonSnow)
+}
+
+func TestVerifySignInCodeHandler_CorrectCode_NewUser(t *testing.T) {
+ RegisterT(t)
+
+ bus.AddHandler(func(ctx context.Context, q *query.GetVerificationByEmailAndCode) error {
+ q.Result = &entity.EmailVerification{
+ Email: "new.user@got.com",
+ Key: "123456",
+ CreatedAt: time.Now(),
+ ExpiresAt: time.Now().Add(15 * time.Minute),
+ }
+ return nil
+ })
+
+ bus.AddHandler(func(ctx context.Context, q *query.GetUserByEmail) error {
+ return app.ErrNotFound
+ })
+
+ server := mock.NewServer()
+ code, response := server.
+ OnTenant(mock.DemoTenant).
+ ExecutePost(handlers.VerifySignInCode(), `{ "email": "new.user@got.com", "code": "123456" }`)
+
+ Expect(code).Equals(http.StatusOK)
+ Expect(response.Body.String()).ContainsSubstring(`"showProfileCompletion":true`)
+}
+
+func TestVerifySignInCodeHandler_CorrectCode_NewUser_PrivateTenant(t *testing.T) {
+ RegisterT(t)
+
+ server := mock.NewServer()
+ mock.DemoTenant.IsPrivate = true
+
+ bus.AddHandler(func(ctx context.Context, q *query.GetVerificationByEmailAndCode) error {
+ q.Result = &entity.EmailVerification{
+ Email: "new.user@got.com",
+ Key: "123456",
+ CreatedAt: time.Now(),
+ ExpiresAt: time.Now().Add(15 * time.Minute),
+ }
+ return nil
+ })
+
+ bus.AddHandler(func(ctx context.Context, q *query.GetUserByEmail) error {
+ return app.ErrNotFound
+ })
+
+ code, _ := server.
+ OnTenant(mock.DemoTenant).
+ ExecutePost(handlers.VerifySignInCode(), `{ "email": "new.user@got.com", "code": "123456" }`)
+
+ Expect(code).Equals(http.StatusForbidden)
+}
+
+func TestResendSignInCodeHandler_ValidEmail(t *testing.T) {
+ RegisterT(t)
+
+ var saveKeyCmd *cmd.SaveVerificationKey
+ bus.AddHandler(func(ctx context.Context, c *cmd.SaveVerificationKey) error {
+ saveKeyCmd = c
+ return nil
+ })
+
+ server := mock.NewServer()
+ code, _ := server.
+ OnTenant(mock.DemoTenant).
+ ExecutePost(handlers.ResendSignInCode(), `{ "email": "jon.snow@got.com" }`)
+
+ Expect(code).Equals(http.StatusOK)
+ Expect(saveKeyCmd.Key).HasLen(6)
+ Expect(saveKeyCmd.Request.GetKind()).Equals(enum.EmailVerificationKindSignIn)
+ Expect(saveKeyCmd.Request.GetEmail()).Equals("jon.snow@got.com")
+}
+
+func TestResendSignInCodeHandler_InvalidEmail(t *testing.T) {
+ RegisterT(t)
+
+ server := mock.NewServer()
+ code, _ := server.
+ OnTenant(mock.DemoTenant).
+ ExecutePost(handlers.ResendSignInCode(), `{ "email": "invalid" }`)
+
+ Expect(code).Equals(http.StatusBadRequest)
+}
+
func ExpectFiderAuthCookie(response *httptest.ResponseRecorder, expected *entity.User) {
cookies := response.Header()["Set-Cookie"]
if expected == nil {
diff --git a/app/models/entity/email_verification.go b/app/models/entity/email_verification.go
index 639f6707b..06825b395 100644
--- a/app/models/entity/email_verification.go
+++ b/app/models/entity/email_verification.go
@@ -23,3 +23,8 @@ type EmailVerification struct {
func GenerateEmailVerificationKey() string {
return rand.String(64)
}
+
+// GenerateEmailVerificationCode returns a 6 digit numeric code
+func GenerateEmailVerificationCode() string {
+ return rand.StringNumeric(6)
+}
diff --git a/app/models/query/tenant.go b/app/models/query/tenant.go
index 41c9db5ff..fddc71c7a 100644
--- a/app/models/query/tenant.go
+++ b/app/models/query/tenant.go
@@ -29,6 +29,15 @@ type GetVerificationByKey struct {
Result *entity.EmailVerification
}
+type GetVerificationByEmailAndCode struct {
+ Email string
+ Code string
+ Kind enum.EmailVerificationKind
+
+ // Output
+ Result *entity.EmailVerification
+}
+
type GetFirstTenant struct {
// Output
diff --git a/app/pkg/rand/random.go b/app/pkg/rand/random.go
index 42c6e0267..fd4782a9b 100644
--- a/app/pkg/rand/random.go
+++ b/app/pkg/rand/random.go
@@ -6,6 +6,7 @@ import (
)
var chars = []byte("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
+var numericChars = []byte("0123456789")
// String returns a random string of given length
func String(n int) string {
@@ -25,3 +26,22 @@ func String(n int) string {
return string(bytes)
}
+
+// StringNumeric returns a random numeric string of given length
+func StringNumeric(n int) string {
+ if n <= 0 {
+ return ""
+ }
+
+ bytes := make([]byte, n)
+ charsetLen := big.NewInt(int64(len(numericChars)))
+ for i := 0; i < n; i++ {
+ c, err := rand.Int(rand.Reader, charsetLen)
+ if err != nil {
+ panic(err)
+ }
+ bytes[i] = numericChars[c.Int64()]
+ }
+
+ return string(bytes)
+}
diff --git a/app/services/sqlstore/postgres/postgres.go b/app/services/sqlstore/postgres/postgres.go
index e628494fb..f2247702a 100644
--- a/app/services/sqlstore/postgres/postgres.go
+++ b/app/services/sqlstore/postgres/postgres.go
@@ -114,6 +114,7 @@ func (s Service) Init() {
bus.AddHandler(updateTenantAdvancedSettings)
bus.AddHandler(getVerificationByKey)
+ bus.AddHandler(getVerificationByEmailAndCode)
bus.AddHandler(saveVerificationKey)
bus.AddHandler(setKeyAsVerified)
bus.AddHandler(getPendingSignUpVerification)
diff --git a/app/services/sqlstore/postgres/tenant.go b/app/services/sqlstore/postgres/tenant.go
index 31ae9fdf5..5473ad2c4 100644
--- a/app/services/sqlstore/postgres/tenant.go
+++ b/app/services/sqlstore/postgres/tenant.go
@@ -213,6 +213,21 @@ func getVerificationByKey(ctx context.Context, q *query.GetVerificationByKey) er
})
}
+func getVerificationByEmailAndCode(ctx context.Context, q *query.GetVerificationByEmailAndCode) error {
+ return using(ctx, func(trx *dbx.Trx, tenant *entity.Tenant, user *entity.User) error {
+ verification := dbEmailVerification{}
+
+ query := "SELECT id, email, name, key, created_at, verified_at, expires_at, kind, user_id FROM email_verifications WHERE tenant_id = $1 AND email = $2 AND key = $3 AND kind = $4 LIMIT 1"
+ err := trx.Get(&verification, query, tenant.ID, q.Email, q.Code, q.Kind)
+ if err != nil {
+ return errors.Wrap(err, "failed to get email verification by email and code")
+ }
+
+ q.Result = verification.toModel()
+ return nil
+ })
+}
+
func saveVerificationKey(ctx context.Context, c *cmd.SaveVerificationKey) error {
return using(ctx, func(trx *dbx.Trx, tenant *entity.Tenant, user *entity.User) error {
var userID any
diff --git a/app/tasks/signin.go b/app/tasks/signin.go
index 4980f4914..0b66f6776 100644
--- a/app/tasks/signin.go
+++ b/app/tasks/signin.go
@@ -9,11 +9,12 @@ import (
)
// SendSignInEmail is used to send the sign in email to requestor
-func SendSignInEmail(email, verificationKey string) worker.Task {
+func SendSignInEmail(email, verificationCode string) worker.Task {
return describe("Send sign in email", func(c *worker.Context) error {
to := dto.NewRecipient("", email, dto.Props{
"siteName": c.Tenant().Name,
- "link": link(web.BaseURL(c), "/signin/verify?k=%s", verificationKey),
+ "code": verificationCode,
+ "link": link(web.BaseURL(c), "/signin/verify?k=%s", verificationCode),
})
bus.Publish(c, &cmd.SendMail{
diff --git a/app/tasks/signin_test.go b/app/tasks/signin_test.go
index ee3ce22da..a554c487f 100644
--- a/app/tasks/signin_test.go
+++ b/app/tasks/signin_test.go
@@ -39,6 +39,7 @@ func TestSendSignInEmailTask(t *testing.T) {
Address: "jon@got.com",
Props: dto.Props{
"siteName": mock.DemoTenant.Name,
+ "code": "9876",
"link": "http://domain.com/signin/verify?k=9876 ",
},
})
diff --git a/e2e/step_definitions/fns.ts b/e2e/step_definitions/fns.ts
index 03096b022..acb118196 100644
--- a/e2e/step_definitions/fns.ts
+++ b/e2e/step_definitions/fns.ts
@@ -29,3 +29,19 @@ export async function getLatestLinkSentTo(address: string): Promise {
return result[0]
}
+
+export async function getLatestCodeSentTo(address: string): Promise {
+ await delay(1000)
+
+ const response = await fetch(`http://localhost:8025/api/v2/search?kind=to&query=${address}`)
+ const responseBody = await response.json()
+ const emailHtml = responseBody.items[0].Content.Body
+ // Look for 6-digit code in the email
+ const reg = /\b\d{6}\b/
+ const result = reg.exec(emailHtml)
+ if (!result) {
+ throw new Error("Could not find a 6-digit code in email content.")
+ }
+
+ return result[0]
+}
diff --git a/e2e/step_definitions/home.steps.ts b/e2e/step_definitions/home.steps.ts
index 8ca282a53..3b3059701 100644
--- a/e2e/step_definitions/home.steps.ts
+++ b/e2e/step_definitions/home.steps.ts
@@ -1,7 +1,7 @@
import { Given, Then } from "@cucumber/cucumber"
import { FiderWorld } from "../world"
import expect from "expect"
-import { getLatestLinkSentTo } from "./fns"
+import { getLatestCodeSentTo } from "./fns"
Given("I go to the home page", async function (this: FiderWorld) {
await this.page.goto(`https://${this.tenantName}.dev.fider.io:3000/`)
@@ -50,8 +50,14 @@ Given("I click submit your feedback", async function () {
Given("I click on the confirmation link", async function (this: FiderWorld) {
const userEmail = `$user-${this.tenantName}@fider.io`
- const activationLink = await getLatestLinkSentTo(userEmail)
- await this.page.goto(activationLink)
+ const code = await getLatestCodeSentTo(userEmail)
+
+ // Enter the code in the UI
+ await this.page.type("#input-code", code)
+ await this.page.click("button[type='submit']")
+
+ // Wait for navigation after successful code verification
+ await this.page.waitForLoadState("networkidle")
})
Then("I should be on the complete profile page", async function (this: FiderWorld) {
@@ -74,7 +80,8 @@ Given("I click submit", async function () {
Then("I should be on the confirmation link page", async function (this: FiderWorld) {
const userEmail = `$user-${this.tenantName}@fider.io`
- await expect(this.page.getByText(`We have just sent a confirmation link to ${userEmail}`)).toBeVisible()
+ // Updated to check for code entry message instead of link message
+ await expect(this.page.getByText(`Please type in the code we just sent to ${userEmail}`)).toBeVisible()
})
Then("I should see {string} as the draft post title", async function (this: FiderWorld, title: string) {
diff --git a/e2e/step_definitions/user.steps.ts b/e2e/step_definitions/user.steps.ts
index 6d61c2b58..7efe85103 100644
--- a/e2e/step_definitions/user.steps.ts
+++ b/e2e/step_definitions/user.steps.ts
@@ -1,6 +1,6 @@
import { Given } from "@cucumber/cucumber"
import { FiderWorld } from "e2e/world"
-import { getLatestLinkSentTo, isAuthenticated, isAuthenticatedAsUser } from "./fns"
+import { getLatestCodeSentTo, isAuthenticated, isAuthenticatedAsUser } from "./fns"
Given("I sign in as {string}", async function (this: FiderWorld, userName: string) {
if (await isAuthenticatedAsUser(this.page, userName)) {
@@ -17,6 +17,11 @@ Given("I sign in as {string}", async function (this: FiderWorld, userName: strin
await this.page.type(".c-signin-control #input-email", userEmail)
await this.page.click(".c-signin-control .c-button--primary")
- const activationLink = await getLatestLinkSentTo(userEmail)
- await this.page.goto(activationLink)
+ // Get the code from email and enter it
+ const code = await getLatestCodeSentTo(userEmail)
+ await this.page.type("#input-code", code)
+ await this.page.click("button[type='submit']")
+
+ // Wait for navigation after successful code verification
+ await this.page.waitForLoadState("networkidle")
})
diff --git a/locale/en/client.json b/locale/en/client.json
index 7df12f02f..ec03a924a 100644
--- a/locale/en/client.json
+++ b/locale/en/client.json
@@ -91,6 +91,10 @@
"label.voters": "Voters",
"labels.notagsavailable": "No tags available",
"labels.notagsselected": "No tags selected",
+ "legal.agreement": "I have read and agree to the <0/> and <1/>.",
+ "legal.notice": "By signing in, you agree to the <2/><0/> and <1/>.",
+ "legal.privacypolicy": "Privacy Policy",
+ "legal.termsofservice": "Terms of Service",
"linkmodal.insert": "Insert Link",
"linkmodal.text.label": "Text to display",
"linkmodal.text.placeholder": "Enter link text",
@@ -100,10 +104,6 @@
"linkmodal.url.label": "URL",
"linkmodal.url.placeholder": "https://example.com",
"linkmodal.url.required": "URL is required",
- "legal.agreement": "I have read and agree to the <0/> and <1/>.",
- "legal.notice": "By signing in, you agree to the <2/><0/> and <1/>.",
- "legal.privacypolicy": "Privacy Policy",
- "legal.termsofservice": "Terms of Service",
"menu.administration": "Administration",
"menu.mysettings": "My Settings",
"menu.signout": "Sign out",
@@ -200,6 +200,14 @@
"showpost.responseform.text.placeholder": "What's going on with this post? Let your users know what are your plans...",
"showpost.votespanel.more": "+{extraVotesCount} more",
"showpost.votespanel.seedetails": "see details",
+ "signin.code.edit": "Edit",
+ "signin.code.expired": "This code has expired. Please request a new one.",
+ "signin.code.getnew": "Get a new code",
+ "signin.code.instruction": "Please type in the code we just sent to <0>{email}0>",
+ "signin.code.invalid": "The code you entered is invalid. Please try again.",
+ "signin.code.placeholder": "Type in the code here",
+ "signin.code.sent": "A new code has been sent to your email.",
+ "signin.code.submit": "Submit",
"signin.email.placeholder": "Email address",
"signin.message.email": "Continue with Email",
"signin.message.emaildisabled": "Email authentication has been disabled by an administrator. If you have an administrator account and need to bypass this restriction, please <0>click here0>.",
diff --git a/public/components/common/SignInControl.tsx b/public/components/common/SignInControl.tsx
index 6044e8c4e..4d417320c 100644
--- a/public/components/common/SignInControl.tsx
+++ b/public/components/common/SignInControl.tsx
@@ -19,8 +19,11 @@ interface SignInControlProps {
export const SignInControl: React.FunctionComponent = (props) => {
const fider = useFider()
const [showEmailForm, setShowEmailForm] = useState(fider.session.tenant ? fider.session.tenant.isEmailAuthAllowed : true)
+ const [showCodeEntry, setShowCodeEntry] = useState(false)
const [email, setEmail] = useState("")
+ const [code, setCode] = useState("")
const [error, setError] = useState(undefined)
+ const [resendMessage, setResendMessage] = useState("")
const signInText = props.signInButtonText || i18n._({ id: "action.signin", message: "Sign in" })
@@ -39,20 +42,53 @@ export const SignInControl: React.FunctionComponent = (props
doPreSigninAction()
}
+ const editEmail = () => {
+ setShowCodeEntry(false)
+ setCode("")
+ setError(undefined)
+ setResendMessage("")
+ }
+
const signIn = async () => {
await doPreSigninAction()
const result = await actions.signIn(email)
if (result.ok) {
- setEmail("")
setError(undefined)
- if (props.onEmailSent) {
- props.onEmailSent(email)
+ setShowCodeEntry(true)
+ // Don't call onEmailSent - we're showing code entry inline now
+ } else if (result.error) {
+ setError(result.error)
+ }
+ }
+
+ const verifyCode = async () => {
+ const result = await actions.verifySignInCode(email, code)
+ if (result.ok) {
+ const data = result.data as { showProfileCompletion?: boolean } | undefined
+ if (data && data.showProfileCompletion) {
+ // User needs to complete profile - reload to show profile completion
+ location.reload()
+ } else {
+ // User is authenticated - reload to refresh the page
+ location.reload()
}
} else if (result.error) {
setError(result.error)
}
}
+ const resendCode = async () => {
+ setResendMessage("")
+ const result = await actions.resendSignInCode(email)
+ if (result.ok) {
+ setError(undefined)
+ setCode("")
+ setResendMessage(i18n._({ id: "signin.code.sent", message: "A new code has been sent to your email." }))
+ } else if (result.error) {
+ setError(result.error)
+ }
+ }
+
const providersLen = fider.settings.oauth.length
if (!isCookieEnabled()) {
@@ -82,23 +118,58 @@ export const SignInControl: React.FunctionComponent = (props
{props.useEmail &&
(showEmailForm ? (
-
- {!fider.session.tenant.isEmailAuthAllowed && (
-
- Currently only allowed to sign in to an administrator account
-
+ {!showCodeEntry ? (
+
+ ) : (
+
)}
) : (
diff --git a/public/services/actions/tenant.ts b/public/services/actions/tenant.ts
index 8e71231c6..fbbbf3578 100644
--- a/public/services/actions/tenant.ts
+++ b/public/services/actions/tenant.ts
@@ -54,11 +54,16 @@ export const checkAvailability = async (subdomain: string): Promise(`/_api/tenants/${subdomain}/availability`)
}
-export const signIn = async (email: string, code?: string): Promise => {
- return await http.post("/_api/signin", {
- email,
- code,
- })
+export const signIn = async (email: string): Promise => {
+ return await http.post("/_api/signin", { email })
+}
+
+export const verifySignInCode = async (email: string, code: string): Promise => {
+ return await http.post("/_api/signin/verify", { email, code })
+}
+
+export const resendSignInCode = async (email: string): Promise => {
+ return await http.post("/_api/signin/resend", { email })
}
export const completeProfile = async (kind: EmailVerificationKind, key: string, name: string): Promise => {
diff --git a/views/email/signin_email.html b/views/email/signin_email.html
index 63d17b932..20eb26bd3 100644
--- a/views/email/signin_email.html
+++ b/views/email/signin_email.html
@@ -6,7 +6,16 @@
{{ "email.greetings" | translate }}
{{ "email.signin_email.text" | translate }}
{{ translate "email.signin_email.confirmation" (dict "siteName" (.siteName | stripHtml)) | html }}
- {{ .link | html }}
+
+
+
{{ "email.signin_email.your_code" | translate }}
+
+
{{ "email.signin_email.code_expires" | translate }}
+
+
+ {{ "email.signin_email.alternative" | translate }} {{ .link | html }}
{{end}}
\ No newline at end of file
From 3a0aee6cf2167cf73ec9cc887c6a4053d15b1ca9 Mon Sep 17 00:00:00 2001
From: Matt Roberts
Date: Sun, 9 Nov 2025 20:43:40 +0000
Subject: [PATCH 02/11] Fixing issues with login code
---
app/cmd/routes.go | 1 +
app/handlers/signin.go | 14 +++++
locale/ar/client.json | 25 +++++----
locale/de/client.json | 24 ++++++---
locale/el/client.json | 24 ++++++---
locale/en/client.json | 2 +-
locale/en/server.json | 7 ++-
locale/es-ES/client.json | 14 +++--
locale/fa/client.json | 27 ++++++----
locale/fr/client.json | 25 +++++----
locale/it/client.json | 25 +++++----
locale/ja/client.json | 24 ++++++---
locale/nl/client.json | 25 +++++----
locale/pl/client.json | 25 +++++----
locale/pt-BR/client.json | 24 ++++++---
locale/ru/client.json | 24 ++++++---
locale/sk/client.json | 24 ++++++---
locale/sv-SE/client.json | 24 ++++++---
locale/tr/client.json | 24 ++++++---
locale/zh-CN/client.json | 25 +++++----
public/components/SignInModal.tsx | 51 ++++++-------------
public/components/common/SignInControl.tsx | 46 +++++++++++++----
.../pages/Home/components/ShareFeedback.tsx | 13 +++--
public/pages/SignIn/SignIn.page.tsx | 24 +++++----
24 files changed, 346 insertions(+), 195 deletions(-)
diff --git a/app/cmd/routes.go b/app/cmd/routes.go
index 0e35a4222..3de105b38 100644
--- a/app/cmd/routes.go
+++ b/app/cmd/routes.go
@@ -117,6 +117,7 @@ func routes(r *web.Engine) *web.Engine {
r.Use(middlewares.BlockPendingTenants())
r.Get("/signin", handlers.SignInPage())
+ r.Get("/signin/complete", handlers.CompleteSignInProfilePage())
r.Get("/loginemailsent", handlers.LoginEmailSentPage())
r.Get("/not-invited", handlers.NotInvitedPage())
r.Get("/signin/verify", handlers.VerifySignInKey(enum.EmailVerificationKindSignIn))
diff --git a/app/handlers/signin.go b/app/handlers/signin.go
index 1c55ba916..cf7a88368 100644
--- a/app/handlers/signin.go
+++ b/app/handlers/signin.go
@@ -46,6 +46,20 @@ func LoginEmailSentPage() web.HandlerFunc {
}
}
+// CompleteSignInProfilePage renders the complete profile page for code flow
+func CompleteSignInProfilePage() web.HandlerFunc {
+ return func(c *web.Context) error {
+ return c.Page(http.StatusOK, web.Props{
+ Page: "SignIn/CompleteSignInProfile.page",
+ Title: "Complete your profile",
+ Data: web.Map{
+ "kind": enum.EmailVerificationKindSignIn,
+ "k": c.QueryParam("code"),
+ },
+ })
+ }
+}
+
// NotInvitedPage renders the not invited page
func NotInvitedPage() web.HandlerFunc {
return func(c *web.Context) error {
diff --git a/locale/ar/client.json b/locale/ar/client.json
index 74f2fb2d0..e5d712839 100644
--- a/locale/ar/client.json
+++ b/locale/ar/client.json
@@ -96,6 +96,15 @@
"legal.notice": "من خلال تسجيل الدخول، أنت توافق على <2/><0/> و <1/>.",
"legal.privacypolicy": "سياسة الخصوصية",
"legal.termsofservice": "شروط الخدمة",
+ "linkmodal.insert": "إدراج الرابط",
+ "linkmodal.text.label": "النص المراد عرضه",
+ "linkmodal.text.placeholder": "أدخل نص الرابط",
+ "linkmodal.text.required": "النص مطلوب",
+ "linkmodal.title": "إدراج الرابط",
+ "linkmodal.url.invalid": "الرجاء إدخال عنوان URL صالح",
+ "linkmodal.url.label": "عنوان URL",
+ "linkmodal.url.placeholder": "",
+ "linkmodal.url.required": "مطلوب عنوان URL",
"menu.administration": "آلإدارة",
"menu.mysettings": "إعداداتي",
"menu.signout": "تسجيل الخروج",
@@ -192,6 +201,12 @@
"showpost.responseform.text.placeholder": "ما الذي يجري مع هذا المنشور؟ أخبر المستخدمين ما هي خططك...",
"showpost.votespanel.more": "+{extraVotesCount} أكثر",
"showpost.votespanel.seedetails": "عرض التفاصيل",
+ "signin.code.edit": "",
+ "signin.code.getnew": "",
+ "signin.code.instruction": "",
+ "signin.code.placeholder": "",
+ "signin.code.sent": "",
+ "signin.code.submit": "",
"signin.email.placeholder": "البريد الإلكتروني",
"signin.message.email": "متابعة بالبريد الإلكتروني",
"signin.message.emaildisabled": "تم تعطيل مصادقة البريد الإلكتروني من قبل المسؤول. إذا كان لديك حساب مسؤول وتحتاج إلى تجاوز هذا التقييد، الرجاء <0>انقر هنا 0>.",
@@ -204,13 +219,5 @@
"signin.message.socialbutton.intro": "تسجيل الدخول بواسطة",
"validation.custom.maxattachments": "يُسمح بحد أقصى {number} من المرفقات.",
"validation.custom.maximagesize": "يجب أن يكون حجم الصورة أصغر من {kilobytes}KB.",
- "{count, plural, one {# tag} other {# tags}}": "{count, plural, zero {}one {# وسم} two {# وسوم} few {# وسوم} many {# وسوم} other {# وسوم}}",
- "linkmodal.insert": "إدراج الرابط",
- "linkmodal.text.label": "النص المراد عرضه",
- "linkmodal.text.placeholder": "أدخل نص الرابط",
- "linkmodal.text.required": "النص مطلوب",
- "linkmodal.title": "إدراج الرابط",
- "linkmodal.url.invalid": "الرجاء إدخال عنوان URL صالح",
- "linkmodal.url.label": "عنوان URL",
- "linkmodal.url.required": "مطلوب عنوان URL"
+ "{count, plural, one {# tag} other {# tags}}": "{count, plural, zero {}one {# وسم} two {# وسوم} few {# وسوم} many {# وسوم} other {# وسوم}}"
}
\ No newline at end of file
diff --git a/locale/de/client.json b/locale/de/client.json
index 5c3cb12f4..2270c769a 100644
--- a/locale/de/client.json
+++ b/locale/de/client.json
@@ -96,6 +96,15 @@
"legal.notice": "Mit der Anmeldung stimmst du den <2/><0/> und <1/> zu.",
"legal.privacypolicy": "Datenschutzerklärung",
"legal.termsofservice": "Nutzungsbedingungen",
+ "linkmodal.insert": "Link einfügen",
+ "linkmodal.text.label": "Anzuzeigender Text",
+ "linkmodal.text.placeholder": "Linktext eingeben",
+ "linkmodal.text.required": "Text ist erforderlich",
+ "linkmodal.title": "Link einfügen",
+ "linkmodal.url.invalid": "Bitte geben Sie eine gültige URL ein",
+ "linkmodal.url.label": "",
+ "linkmodal.url.placeholder": "",
+ "linkmodal.url.required": "URL ist erforderlich",
"menu.administration": "Verwaltung",
"menu.mysettings": "Meine Einstellungen",
"menu.signout": "Abmelden",
@@ -192,6 +201,12 @@
"showpost.responseform.text.placeholder": "Was passiert in diesem Beitrag? Lass deine Benutzer wissen, was deine Pläne sind...",
"showpost.votespanel.more": "+{extraVotesCount} mehr",
"showpost.votespanel.seedetails": "Details anschauen",
+ "signin.code.edit": "",
+ "signin.code.getnew": "",
+ "signin.code.instruction": "",
+ "signin.code.placeholder": "",
+ "signin.code.sent": "",
+ "signin.code.submit": "",
"signin.email.placeholder": "E-Mail-Adresse",
"signin.message.email": "Mit E-Mail fortfahren",
"signin.message.emaildisabled": "Die E-Mail-Authentifizierung wurde von einem Administrator deaktiviert. Wenn du ein Administrator-Konto hast und diese Einschränkung umgehen musst, klicke bitte <0>hier0>.",
@@ -204,12 +219,5 @@
"signin.message.socialbutton.intro": "Einloggen mit",
"validation.custom.maxattachments": "Es sind maximal {number} Anhänge zulässig.",
"validation.custom.maximagesize": "Die Bildgröße muss kleiner als {kilobytes}KB sein.",
- "{count, plural, one {# tag} other {# tags}}": "{count, plural, one {# Tag} other {# Tags}}",
- "linkmodal.insert": "Link einfügen",
- "linkmodal.text.label": "Anzuzeigender Text",
- "linkmodal.text.placeholder": "Linktext eingeben",
- "linkmodal.text.required": "Text ist erforderlich",
- "linkmodal.title": "Link einfügen",
- "linkmodal.url.invalid": "Bitte geben Sie eine gültige URL ein",
- "linkmodal.url.required": "URL ist erforderlich"
+ "{count, plural, one {# tag} other {# tags}}": "{count, plural, one {# Tag} other {# Tags}}"
}
\ No newline at end of file
diff --git a/locale/el/client.json b/locale/el/client.json
index c239e8227..6cd91acc6 100644
--- a/locale/el/client.json
+++ b/locale/el/client.json
@@ -96,6 +96,15 @@
"legal.notice": "Με την είσοδο, συμφωνείτε με το <2/><0/> και <1/>.",
"legal.privacypolicy": "Πολιτική Απορρήτου",
"legal.termsofservice": "Όροι χρήσης",
+ "linkmodal.insert": "Εισαγωγή συνδέσμου",
+ "linkmodal.text.label": "Κείμενο προς εμφάνιση",
+ "linkmodal.text.placeholder": "Εισαγάγετε κείμενο συνδέσμου",
+ "linkmodal.text.required": "Απαιτείται κείμενο",
+ "linkmodal.title": "Εισαγωγή συνδέσμου",
+ "linkmodal.url.invalid": "Παρακαλώ εισάγετε μια έγκυρη διεύθυνση URL",
+ "linkmodal.url.label": "",
+ "linkmodal.url.placeholder": "",
+ "linkmodal.url.required": "Απαιτείται διεύθυνση URL",
"menu.administration": "Διαχείριση",
"menu.mysettings": "Οι Ρυθμίσεις Μου",
"menu.signout": "Αποσύνδεση",
@@ -192,6 +201,12 @@
"showpost.responseform.text.placeholder": "Τι συμβαίνει με αυτή την ανάρτηση; Αφήστε τους χρήστες σας να γνωρίζουν ποια είναι τα σχέδιά σας...",
"showpost.votespanel.more": "+{extraVotesCount} περισσότερα",
"showpost.votespanel.seedetails": "δείτε λεπτομέρειες",
+ "signin.code.edit": "",
+ "signin.code.getnew": "",
+ "signin.code.instruction": "",
+ "signin.code.placeholder": "",
+ "signin.code.sent": "",
+ "signin.code.submit": "",
"signin.email.placeholder": "Διεύθυνση ηλεκτρονικού ταχυδρομείου",
"signin.message.email": "Συνέχεια με email",
"signin.message.emaildisabled": "Ο έλεγχος ταυτότητας email έχει απενεργοποιηθεί από τον διαχειριστή. Εάν έχετε λογαριασμό διαχειριστή και πρέπει να παρακάμψετε αυτόν τον περιορισμό, παρακαλώ <0>κάντε κλικ εδώ0>.",
@@ -204,12 +219,5 @@
"signin.message.socialbutton.intro": "Συνδεθείτε με",
"validation.custom.maxattachments": "Επιτρέπονται έως {number} συνημμένα.",
"validation.custom.maximagesize": "Το μέγεθος της εικόνας πρέπει να είναι μικρότερο από {kilobytes}KB.",
- "{count, plural, one {# tag} other {# tags}}": "{count, plural, one {# ετικέτα} other {# ετικέτες}}",
- "linkmodal.insert": "Εισαγωγή συνδέσμου",
- "linkmodal.text.label": "Κείμενο προς εμφάνιση",
- "linkmodal.text.placeholder": "Εισαγάγετε κείμενο συνδέσμου",
- "linkmodal.text.required": "Απαιτείται κείμενο",
- "linkmodal.title": "Εισαγωγή συνδέσμου",
- "linkmodal.url.invalid": "Παρακαλώ εισάγετε μια έγκυρη διεύθυνση URL",
- "linkmodal.url.required": "Απαιτείται διεύθυνση URL"
+ "{count, plural, one {# tag} other {# tags}}": "{count, plural, one {# ετικέτα} other {# ετικέτες}}"
}
\ No newline at end of file
diff --git a/locale/en/client.json b/locale/en/client.json
index ec03a924a..ca81b5b7c 100644
--- a/locale/en/client.json
+++ b/locale/en/client.json
@@ -201,7 +201,7 @@
"showpost.votespanel.more": "+{extraVotesCount} more",
"showpost.votespanel.seedetails": "see details",
"signin.code.edit": "Edit",
- "signin.code.expired": "This code has expired. Please request a new one.",
+ "signin.code.expired": "This code has expired or already been used. Please request a new one.",
"signin.code.getnew": "Get a new code",
"signin.code.instruction": "Please type in the code we just sent to <0>{email}0>",
"signin.code.invalid": "The code you entered is invalid. Please try again.",
diff --git a/locale/en/server.json b/locale/en/server.json
index 885983422..7bdb0afd7 100644
--- a/locale/en/server.json
+++ b/locale/en/server.json
@@ -53,10 +53,13 @@
"email.new_comment.text": "{userName} left a comment on {title} ({postLink}) .",
"email.new_post.text": "{userName} created a new post {title} ({postLink}) .",
"email.signin_email.subject": "Sign in to {siteName}",
- "email.signin_email.text": "You asked us to send you a sign-in link and here it is.",
- "email.signin_email.confirmation": "Click the link below to sign in to {siteName} .",
+ "email.signin_email.text": "Here is your sign-in code.",
+ "email.signin_email.confirmation": "Use the one-time code below to sign in to {siteName} .",
"email.signup_email.subject": "Your new Fider site",
"email.signup_email.text": "You are one step away from activating your Fider site.",
+ "email.signin_email.your_code": "Your sign-in code is:",
+ "email.signin_email.code_expires": "This code will expire in 15 minutes.",
+ "email.signin_email.alternative": "Alternatively, you can click the link below to sign in directly:",
"email.signup_email.confirmation": "Through the link below you can verify your email address and complete the activation process.",
"email.footer.subscription_notice": "You are receiving this email because you are subscribed to this post. You can {view}, {unsubscribe} or {change}.",
"email.footer.subscription_notice2": "You are receiving this email because you are subscribed to this post. You can {change}.",
diff --git a/locale/es-ES/client.json b/locale/es-ES/client.json
index 12adde04b..f67c46996 100644
--- a/locale/es-ES/client.json
+++ b/locale/es-ES/client.json
@@ -92,6 +92,10 @@
"label.voters": "Votantes",
"labels.notagsavailable": "No hay etiquetas disponibles",
"labels.notagsselected": "No hay etiquetas seleccionadas",
+ "legal.agreement": "He leído y acepto los <0/> y <1/>.",
+ "legal.notice": "Al iniciar sesión, aceptas los <2/><0/> y <1/>.",
+ "legal.privacypolicy": "Política de Privacidad",
+ "legal.termsofservice": "Términos del servicio",
"linkmodal.insert": "Insertar un enlace",
"linkmodal.text.label": "Texto a mostrar",
"linkmodal.text.placeholder": "Ingresa el texto del enlace",
@@ -101,10 +105,6 @@
"linkmodal.url.label": "URL",
"linkmodal.url.placeholder": "https://example.com",
"linkmodal.url.required": "La URL es requerida",
- "legal.agreement": "He leído y acepto los <0/> y <1/>.",
- "legal.notice": "Al iniciar sesión, aceptas los <2/><0/> y <1/>.",
- "legal.privacypolicy": "Política de Privacidad",
- "legal.termsofservice": "Términos del servicio",
"menu.administration": "Administración",
"menu.mysettings": "Mis ajustes",
"menu.signout": "Cerrar sesión",
@@ -201,6 +201,12 @@
"showpost.responseform.text.placeholder": "¿Qué está pasando con esta publicación? Dile a tus usuarios cuáles son tus planes...",
"showpost.votespanel.more": "+{extraVotesCount} más",
"showpost.votespanel.seedetails": "ver detalles",
+ "signin.code.edit": "",
+ "signin.code.getnew": "",
+ "signin.code.instruction": "",
+ "signin.code.placeholder": "",
+ "signin.code.sent": "",
+ "signin.code.submit": "",
"signin.email.placeholder": "Dirección de correo electrónico",
"signin.message.email": "Continuar con el correo electrónico",
"signin.message.emaildisabled": "La autenticación de correo electrónico ha sido deshabilitada por un administrador. Si tiene una cuenta de administrador y necesita eludir esta restricción, por favor <0>haga clic aquí0>.",
diff --git a/locale/fa/client.json b/locale/fa/client.json
index c819e501f..cd32c55fb 100644
--- a/locale/fa/client.json
+++ b/locale/fa/client.json
@@ -96,6 +96,15 @@
"legal.notice": "با ورود، شما با <2/><0/> و <1/> موافقید.",
"legal.privacypolicy": "سیاست حفظ حریم خصوصی",
"legal.termsofservice": "شرایط خدمات",
+ "linkmodal.insert": "درج لینک",
+ "linkmodal.text.label": "متن برای نمایش",
+ "linkmodal.text.placeholder": "متن لینک را وارد کنید",
+ "linkmodal.text.required": "متن الزامی است",
+ "linkmodal.title": "درج لینک",
+ "linkmodal.url.invalid": "لطفا یک URL معتبر وارد کنید",
+ "linkmodal.url.label": "آدرس اینترنتی",
+ "linkmodal.url.placeholder": "",
+ "linkmodal.url.required": "آدرس اینترنتی (URL) الزامی است",
"menu.administration": "مدیریت",
"menu.mysettings": "تنظیمات من",
"menu.signout": "خروج",
@@ -156,6 +165,7 @@
"mysettings.notification.title": "رویدادهایی را که میخواهید اعلان دریافت کنید انتخاب کنید",
"mysettings.page.subtitle": "تنظیمات پروفایل خود را مدیریت کنید",
"mysettings.page.title": "تنظیمات",
+ "newpost.modal.addimage": "اضافه کردن تصاویر",
"newpost.modal.description.placeholder": "در موردش به ما بگو. کامل توضیح بده، دریغ نکن، هر چه اطلاعات بیشتر، بهتر.",
"newpost.modal.submit": "ایده خود را ثبت کنید",
"newpost.modal.title": "ایده خود را به اشتراک بگذارید...",
@@ -191,6 +201,12 @@
"showpost.responseform.text.placeholder": "برنامهٔ خود را دربارهٔ این پست با کاربران در میان بگذارید...",
"showpost.votespanel.more": "+{extraVotesCount} بیشتر",
"showpost.votespanel.seedetails": "مشاهدهٔ جزئیات",
+ "signin.code.edit": "",
+ "signin.code.getnew": "",
+ "signin.code.instruction": "",
+ "signin.code.placeholder": "",
+ "signin.code.sent": "",
+ "signin.code.submit": "",
"signin.email.placeholder": "آدرس ایمیل",
"signin.message.email": "ادامه با ایمیل",
"signin.message.emaildisabled": "ورود با ایمیل توسط مدیر غیرفعال شده است. اگر مدیر هستید و نیاز به دسترسی دارید <0>اینجا کلیک کنید0>.",
@@ -203,14 +219,5 @@
"signin.message.socialbutton.intro": "ورود با",
"validation.custom.maxattachments": "حداکثر تعداد {number} پیوست مجاز است.",
"validation.custom.maximagesize": "حجم تصویر باید کمتر از {kilobytes}KB باشد.",
- "{count, plural, one {# tag} other {# tags}}": "{count, plural, one {# برچسب} other {# برچسب}}",
- "linkmodal.insert": "درج لینک",
- "linkmodal.text.label": "متن برای نمایش",
- "linkmodal.text.placeholder": "متن لینک را وارد کنید",
- "linkmodal.text.required": "متن الزامی است",
- "linkmodal.title": "درج لینک",
- "linkmodal.url.invalid": "لطفا یک URL معتبر وارد کنید",
- "linkmodal.url.label": "آدرس اینترنتی",
- "linkmodal.url.required": "آدرس اینترنتی (URL) الزامی است",
- "newpost.modal.addimage": "اضافه کردن تصاویر"
+ "{count, plural, one {# tag} other {# tags}}": "{count, plural, one {# برچسب} other {# برچسب}}"
}
\ No newline at end of file
diff --git a/locale/fr/client.json b/locale/fr/client.json
index a7664d9af..4485c5d6d 100644
--- a/locale/fr/client.json
+++ b/locale/fr/client.json
@@ -96,6 +96,15 @@
"legal.notice": "En vous connectant, vous acceptez les <2/><0/> et <1/>.",
"legal.privacypolicy": "Politique de confidentialité",
"legal.termsofservice": "Conditions générales d’utilisation",
+ "linkmodal.insert": "Insérer un lien",
+ "linkmodal.text.label": "Texte à afficher",
+ "linkmodal.text.placeholder": "Entrez le texte du lien",
+ "linkmodal.text.required": "Le texte est obligatoire",
+ "linkmodal.title": "Insérer un lien",
+ "linkmodal.url.invalid": "Veuillez saisir une URL valide",
+ "linkmodal.url.label": "",
+ "linkmodal.url.placeholder": "https://exemple.com",
+ "linkmodal.url.required": "L'URL est requise",
"menu.administration": "Administration",
"menu.mysettings": "Mes paramètres",
"menu.signout": "Se déconnecter",
@@ -192,6 +201,12 @@
"showpost.responseform.text.placeholder": "Que se passe-t-il avec ce message ? Faites savoir à vos utilisateurs quels sont vos plans...",
"showpost.votespanel.more": "+{extraVotesCount} de plus",
"showpost.votespanel.seedetails": "voir les détails",
+ "signin.code.edit": "",
+ "signin.code.getnew": "",
+ "signin.code.instruction": "",
+ "signin.code.placeholder": "",
+ "signin.code.sent": "",
+ "signin.code.submit": "",
"signin.email.placeholder": "Adresse email",
"signin.message.email": "Continuer avec une addresse email",
"signin.message.emaildisabled": "L'authentification par e-mail a été désactivée par un administrateur. Si vous avez un compte administrateur, <0>cliquez ici0> pour vous connecter.",
@@ -204,13 +219,5 @@
"signin.message.socialbutton.intro": "Se connecter avec",
"validation.custom.maxattachments": "Un maximum de {number} pièces jointes est autorisé.",
"validation.custom.maximagesize": "La taille de l'image doit être inférieure à {kilobytes}KB.",
- "{count, plural, one {# tag} other {# tags}}": "{count, plural, one {# tag} other {# tags}}",
- "linkmodal.insert": "Insérer un lien",
- "linkmodal.text.label": "Texte à afficher",
- "linkmodal.text.placeholder": "Entrez le texte du lien",
- "linkmodal.text.required": "Le texte est obligatoire",
- "linkmodal.title": "Insérer un lien",
- "linkmodal.url.invalid": "Veuillez saisir une URL valide",
- "linkmodal.url.placeholder": "https://exemple.com",
- "linkmodal.url.required": "L'URL est requise"
+ "{count, plural, one {# tag} other {# tags}}": "{count, plural, one {# tag} other {# tags}}"
}
\ No newline at end of file
diff --git a/locale/it/client.json b/locale/it/client.json
index 6a5178133..a625e586b 100644
--- a/locale/it/client.json
+++ b/locale/it/client.json
@@ -96,6 +96,15 @@
"legal.notice": "Accedendo, accetti i <2/><0/> e <1/>.",
"legal.privacypolicy": "Informativa sulla privacy",
"legal.termsofservice": "Termini di servizio",
+ "linkmodal.insert": "Inserisci collegamento",
+ "linkmodal.text.label": "Testo da visualizzare",
+ "linkmodal.text.placeholder": "Inserisci il testo del collegamento",
+ "linkmodal.text.required": "Il testo è obbligatorio",
+ "linkmodal.title": "Inserisci collegamento",
+ "linkmodal.url.invalid": "Inserisci un URL valido",
+ "linkmodal.url.label": "",
+ "linkmodal.url.placeholder": "https://esempio.com",
+ "linkmodal.url.required": "L'URL è obbligatorio",
"menu.administration": "Amministrazione",
"menu.mysettings": "Le mie preferenze",
"menu.signout": "Disconnettersi",
@@ -192,6 +201,12 @@
"showpost.responseform.text.placeholder": "Cosa succede con questo post? Fate sapere ai vostri utenti quali sono i vostri piani...",
"showpost.votespanel.more": "+{extraVotesCount} di più",
"showpost.votespanel.seedetails": "vedi dettagli",
+ "signin.code.edit": "",
+ "signin.code.getnew": "",
+ "signin.code.instruction": "",
+ "signin.code.placeholder": "",
+ "signin.code.sent": "",
+ "signin.code.submit": "",
"signin.email.placeholder": "Indirizzo e-mail",
"signin.message.email": "Continua con l'email",
"signin.message.emaildisabled": "L'autenticazione email è stata disabilitata da un amministratore. Se hai un account amministratore e hai bisogno di bypassare questa restrizione, per favore <0>clicca qui0>.",
@@ -204,13 +219,5 @@
"signin.message.socialbutton.intro": "Accedi con",
"validation.custom.maxattachments": "Sono consentiti al massimo {number} allegati.",
"validation.custom.maximagesize": "La dimensione dell'immagine deve essere inferiore a {kilobytes}KB.",
- "{count, plural, one {# tag} other {# tags}}": "{count, plural, one {# tag} other {# tags}}",
- "linkmodal.insert": "Inserisci collegamento",
- "linkmodal.text.label": "Testo da visualizzare",
- "linkmodal.text.placeholder": "Inserisci il testo del collegamento",
- "linkmodal.text.required": "Il testo è obbligatorio",
- "linkmodal.title": "Inserisci collegamento",
- "linkmodal.url.invalid": "Inserisci un URL valido",
- "linkmodal.url.placeholder": "https://esempio.com",
- "linkmodal.url.required": "L'URL è obbligatorio"
+ "{count, plural, one {# tag} other {# tags}}": "{count, plural, one {# tag} other {# tags}}"
}
\ No newline at end of file
diff --git a/locale/ja/client.json b/locale/ja/client.json
index 46b3089d1..91bc31451 100644
--- a/locale/ja/client.json
+++ b/locale/ja/client.json
@@ -96,6 +96,15 @@
"legal.notice": "サインインすると、<2/><0/> および <1/> に同意したことになります。",
"legal.privacypolicy": "プライバシーポリシー",
"legal.termsofservice": "サービス利用規約",
+ "linkmodal.insert": "リンクを挿入",
+ "linkmodal.text.label": "表示するテキスト",
+ "linkmodal.text.placeholder": "リンクテキストを入力",
+ "linkmodal.text.required": "テキストは必須です",
+ "linkmodal.title": "リンクを挿入",
+ "linkmodal.url.invalid": "有効なURLを入力してください",
+ "linkmodal.url.label": "",
+ "linkmodal.url.placeholder": "",
+ "linkmodal.url.required": "URLは必須です",
"menu.administration": "管理",
"menu.mysettings": "ユーザー設定",
"menu.signout": "ログアウト",
@@ -192,6 +201,12 @@
"showpost.responseform.text.placeholder": "この記事はどうなっていますか? あなたのプランをユーザーに知らせてください...",
"showpost.votespanel.more": "+{extraVotesCount} 以上",
"showpost.votespanel.seedetails": "詳細を表示",
+ "signin.code.edit": "",
+ "signin.code.getnew": "",
+ "signin.code.instruction": "",
+ "signin.code.placeholder": "",
+ "signin.code.sent": "",
+ "signin.code.submit": "",
"signin.email.placeholder": "電子メールアドレス",
"signin.message.email": "メールで続行",
"signin.message.emaildisabled": "メール認証は管理者によって無効にされています。管理者アカウントを持っていて、この制限を回避する必要がある場合は、<0>ここをクリック0>してください。",
@@ -204,12 +219,5 @@
"signin.message.socialbutton.intro": "ログイン",
"validation.custom.maxattachments": "最大 {number} 個の添付ファイルが許可されます。",
"validation.custom.maximagesize": "画像サイズは{kilobytes}KB未満である必要があります。",
- "{count, plural, one {# tag} other {# tags}}": "{count, plural, one {# tag} other {# tags}}",
- "linkmodal.insert": "リンクを挿入",
- "linkmodal.text.label": "表示するテキスト",
- "linkmodal.text.placeholder": "リンクテキストを入力",
- "linkmodal.text.required": "テキストは必須です",
- "linkmodal.title": "リンクを挿入",
- "linkmodal.url.invalid": "有効なURLを入力してください",
- "linkmodal.url.required": "URLは必須です"
+ "{count, plural, one {# tag} other {# tags}}": "{count, plural, one {# tag} other {# tags}}"
}
\ No newline at end of file
diff --git a/locale/nl/client.json b/locale/nl/client.json
index 4d93764bc..4b0852a88 100644
--- a/locale/nl/client.json
+++ b/locale/nl/client.json
@@ -96,6 +96,15 @@
"legal.notice": "Door in te loggen, ga je akkoord met de <2/><0/> en <1/>.",
"legal.privacypolicy": "Privacybeleid",
"legal.termsofservice": "Algemene voorwaarden",
+ "linkmodal.insert": "Link invoegen",
+ "linkmodal.text.label": "Weer te geven tekst",
+ "linkmodal.text.placeholder": "Voer linktekst in",
+ "linkmodal.text.required": "Tekst is vereist",
+ "linkmodal.title": "Link invoegen",
+ "linkmodal.url.invalid": "Voer een geldige URL in",
+ "linkmodal.url.label": "",
+ "linkmodal.url.placeholder": "https://voorbeeld.com",
+ "linkmodal.url.required": "URL is vereist",
"menu.administration": "Beheer",
"menu.mysettings": "Mijn instellingen",
"menu.signout": "Uitloggen",
@@ -192,6 +201,12 @@
"showpost.responseform.text.placeholder": "Wat gebeurt er met dit bericht? Laat je gebruikers weten wat je plannen zijn...",
"showpost.votespanel.more": "+{extraVotesCount} meer",
"showpost.votespanel.seedetails": "details bekijken",
+ "signin.code.edit": "",
+ "signin.code.getnew": "",
+ "signin.code.instruction": "",
+ "signin.code.placeholder": "",
+ "signin.code.sent": "",
+ "signin.code.submit": "",
"signin.email.placeholder": "E-mailadres",
"signin.message.email": "Doorgaan met e-mail",
"signin.message.emaildisabled": "Inloggen met e-mail is uitgeschakeld door een beheerder. Als u een beheerder account heeft en deze beperking moet omzeilen, <0>klik dan hier0>.",
@@ -204,13 +219,5 @@
"signin.message.socialbutton.intro": "Inloggen met",
"validation.custom.maxattachments": "Er zijn maximaal {number} bijlagen toegestaan.",
"validation.custom.maximagesize": "De afbeeldingsgrootte moet kleiner zijn dan {kilobytes}KB.",
- "{count, plural, one {# tag} other {# tags}}": "{count, plural, one {# tag} other {# tags}}",
- "linkmodal.insert": "Link invoegen",
- "linkmodal.text.label": "Weer te geven tekst",
- "linkmodal.text.placeholder": "Voer linktekst in",
- "linkmodal.text.required": "Tekst is vereist",
- "linkmodal.title": "Link invoegen",
- "linkmodal.url.invalid": "Voer een geldige URL in",
- "linkmodal.url.placeholder": "https://voorbeeld.com",
- "linkmodal.url.required": "URL is vereist"
+ "{count, plural, one {# tag} other {# tags}}": "{count, plural, one {# tag} other {# tags}}"
}
\ No newline at end of file
diff --git a/locale/pl/client.json b/locale/pl/client.json
index 2de7df8c2..e27c791a7 100644
--- a/locale/pl/client.json
+++ b/locale/pl/client.json
@@ -96,6 +96,15 @@
"legal.notice": "Logując się, akceptujesz <2/><0/> i <1/>.",
"legal.privacypolicy": "Polityka Prywatności",
"legal.termsofservice": "Warunki Świadczenia Usług",
+ "linkmodal.insert": "Wstaw link",
+ "linkmodal.text.label": "Tekst do wyświetlenia",
+ "linkmodal.text.placeholder": "Wprowadź tekst linku",
+ "linkmodal.text.required": "Tekst jest wymagany",
+ "linkmodal.title": "Wstaw link",
+ "linkmodal.url.invalid": "Proszę podać prawidłowy adres URL",
+ "linkmodal.url.label": "Adres URL",
+ "linkmodal.url.placeholder": "",
+ "linkmodal.url.required": "Adres URL jest wymagany",
"menu.administration": "Administracja",
"menu.mysettings": "Moje Ustawienia",
"menu.signout": "Wyloguj się",
@@ -192,6 +201,12 @@
"showpost.responseform.text.placeholder": "Co się dzieje w temacie tego posta? Daj swoim użytkownikom znać o swoich planach...",
"showpost.votespanel.more": "+{extraVotesCount} więcej",
"showpost.votespanel.seedetails": "pokaż szczegóły",
+ "signin.code.edit": "",
+ "signin.code.getnew": "",
+ "signin.code.instruction": "",
+ "signin.code.placeholder": "",
+ "signin.code.sent": "",
+ "signin.code.submit": "",
"signin.email.placeholder": "Adres e-mail",
"signin.message.email": "Kontynuuj z e-mailem",
"signin.message.emaildisabled": "Autoryzacja za pomocą adresu email została wyłączona przez administratora. Jeśli posiadasz konto administratora i chcesz obejść to ograniczenie <0>kliknij tutaj0>.",
@@ -204,13 +219,5 @@
"signin.message.socialbutton.intro": "Zaloguj się za pomocą",
"validation.custom.maxattachments": "Maksymalna liczba załączników to {number}.",
"validation.custom.maximagesize": "Rozmiar obrazu musi być mniejszy niż {kilobytes}KB.",
- "{count, plural, one {# tag} other {# tags}}": "{count, plural, one {# tag} few {# tagów} many {# tagów} other {# tagi}}",
- "linkmodal.insert": "Wstaw link",
- "linkmodal.text.label": "Tekst do wyświetlenia",
- "linkmodal.text.placeholder": "Wprowadź tekst linku",
- "linkmodal.text.required": "Tekst jest wymagany",
- "linkmodal.title": "Wstaw link",
- "linkmodal.url.invalid": "Proszę podać prawidłowy adres URL",
- "linkmodal.url.label": "Adres URL",
- "linkmodal.url.required": "Adres URL jest wymagany"
+ "{count, plural, one {# tag} other {# tags}}": "{count, plural, one {# tag} few {# tagów} many {# tagów} other {# tagi}}"
}
\ No newline at end of file
diff --git a/locale/pt-BR/client.json b/locale/pt-BR/client.json
index 3b317e1d2..dd70260b9 100644
--- a/locale/pt-BR/client.json
+++ b/locale/pt-BR/client.json
@@ -96,6 +96,15 @@
"legal.notice": "Ao fazer o login, você concorda com os <2/><0/> e <1/>.",
"legal.privacypolicy": "Política de Privacidade",
"legal.termsofservice": "Termos do Serviço",
+ "linkmodal.insert": "Inserir link",
+ "linkmodal.text.label": "Texto para exibir",
+ "linkmodal.text.placeholder": "Insira o texto do link",
+ "linkmodal.text.required": "Texto é obrigatório",
+ "linkmodal.title": "Inserir link",
+ "linkmodal.url.invalid": "Por favor, insira um URL válido",
+ "linkmodal.url.label": "",
+ "linkmodal.url.placeholder": "",
+ "linkmodal.url.required": "URL é obrigatório",
"menu.administration": "Administração",
"menu.mysettings": "Minhas Configurações",
"menu.signout": "Finalizar sessão",
@@ -192,6 +201,12 @@
"showpost.responseform.text.placeholder": "O que está acontecendo com esta postagem? Informe seus usuários quais são os seus planos...",
"showpost.votespanel.more": "+{extraVotesCount} mais",
"showpost.votespanel.seedetails": "ver detalhes",
+ "signin.code.edit": "",
+ "signin.code.getnew": "",
+ "signin.code.instruction": "",
+ "signin.code.placeholder": "",
+ "signin.code.sent": "",
+ "signin.code.submit": "",
"signin.email.placeholder": "Endereço de e-mail",
"signin.message.email": "Entrar com email",
"signin.message.emaildisabled": "A autenticação por e-mail foi desativada por um administrador. Se você tem uma conta de administrador e precisa ignorar esta restrição, <0>clique aqui0>.",
@@ -204,12 +219,5 @@
"signin.message.socialbutton.intro": "Fazer login com",
"validation.custom.maxattachments": "São permitidos no máximo {number} anexos.",
"validation.custom.maximagesize": "O tamanho da imagem deve ser menor que {kilobytes}KB.",
- "{count, plural, one {# tag} other {# tags}}": "{count, plural, one {# tag} other {# tags}}",
- "linkmodal.insert": "Inserir link",
- "linkmodal.text.label": "Texto para exibir",
- "linkmodal.text.placeholder": "Insira o texto do link",
- "linkmodal.text.required": "Texto é obrigatório",
- "linkmodal.title": "Inserir link",
- "linkmodal.url.invalid": "Por favor, insira um URL válido",
- "linkmodal.url.required": "URL é obrigatório"
+ "{count, plural, one {# tag} other {# tags}}": "{count, plural, one {# tag} other {# tags}}"
}
\ No newline at end of file
diff --git a/locale/ru/client.json b/locale/ru/client.json
index 9bc4bcf3e..383718415 100644
--- a/locale/ru/client.json
+++ b/locale/ru/client.json
@@ -96,6 +96,15 @@
"legal.notice": "Войдя в систему, вы соглашаетесь с <2/><0/> и <1/>.",
"legal.privacypolicy": "Политика Конфиденциальности",
"legal.termsofservice": "Условия Использования",
+ "linkmodal.insert": "Вставить ссылку",
+ "linkmodal.text.label": "Текст для отображения",
+ "linkmodal.text.placeholder": "Введите текст ссылки",
+ "linkmodal.text.required": "Текст обязателен",
+ "linkmodal.title": "Вставить ссылку",
+ "linkmodal.url.invalid": "Пожалуйста, введите действительный URL-адрес",
+ "linkmodal.url.label": "",
+ "linkmodal.url.placeholder": "",
+ "linkmodal.url.required": "URL-адрес обязателен",
"menu.administration": "Администрирование",
"menu.mysettings": "Мой аккаунт",
"menu.signout": "Выйти",
@@ -192,6 +201,12 @@
"showpost.responseform.text.placeholder": "Что произойдёт с этим предложением? Дайте людям знать о ваших планах...",
"showpost.votespanel.more": "и ещё {extraVotesCount}",
"showpost.votespanel.seedetails": "подробнее",
+ "signin.code.edit": "",
+ "signin.code.getnew": "",
+ "signin.code.instruction": "",
+ "signin.code.placeholder": "",
+ "signin.code.sent": "",
+ "signin.code.submit": "",
"signin.email.placeholder": "Адрес электронной почты",
"signin.message.email": "Продолжить с электронной почтой",
"signin.message.emaildisabled": "Аутентификация по адресу электронной почты отключена. <0>Я администратор и мне нужно обойти это ограничение0>.",
@@ -204,12 +219,5 @@
"signin.message.socialbutton.intro": "Войти с помощью",
"validation.custom.maxattachments": "Разрешено максимум {number} вложений.",
"validation.custom.maximagesize": "Размер изображения должен быть меньше {kilobytes}КБ.",
- "{count, plural, one {# tag} other {# tags}}": "{count, plural, one {# tag} other {# tags}}",
- "linkmodal.insert": "Вставить ссылку",
- "linkmodal.text.label": "Текст для отображения",
- "linkmodal.text.placeholder": "Введите текст ссылки",
- "linkmodal.text.required": "Текст обязателен",
- "linkmodal.title": "Вставить ссылку",
- "linkmodal.url.invalid": "Пожалуйста, введите действительный URL-адрес",
- "linkmodal.url.required": "URL-адрес обязателен"
+ "{count, plural, one {# tag} other {# tags}}": "{count, plural, one {# tag} other {# tags}}"
}
\ No newline at end of file
diff --git a/locale/sk/client.json b/locale/sk/client.json
index 4af08fec9..1ad731433 100644
--- a/locale/sk/client.json
+++ b/locale/sk/client.json
@@ -96,6 +96,15 @@
"legal.notice": "Prihlásením súhlasíte s <2/><0/> a <1/>.",
"legal.privacypolicy": "Zásady ochrany osobných údajov",
"legal.termsofservice": "Podmienky služby",
+ "linkmodal.insert": "Vložiť odkaz",
+ "linkmodal.text.label": "Text na zobrazenie",
+ "linkmodal.text.placeholder": "Zadajte text odkazu",
+ "linkmodal.text.required": "Vyžaduje sa text",
+ "linkmodal.title": "Vložiť odkaz",
+ "linkmodal.url.invalid": "Zadajte platnú URL adresu",
+ "linkmodal.url.label": "",
+ "linkmodal.url.placeholder": "",
+ "linkmodal.url.required": "URL adresa je povinná",
"menu.administration": "Administrácia",
"menu.mysettings": "Moje nastavenia",
"menu.signout": "Odhlásiť sa",
@@ -192,6 +201,12 @@
"showpost.responseform.text.placeholder": "Čo sa deje s týmto príspevkom? Dajte svojim používateľom vedieť, aké máte plány...",
"showpost.votespanel.more": "+{extraVotesCount} viac",
"showpost.votespanel.seedetails": "pozri detaily",
+ "signin.code.edit": "",
+ "signin.code.getnew": "",
+ "signin.code.instruction": "",
+ "signin.code.placeholder": "",
+ "signin.code.sent": "",
+ "signin.code.submit": "",
"signin.email.placeholder": "Emailová adresa",
"signin.message.email": "Pokračovať pomocou e-mailu",
"signin.message.emaildisabled": "Správca zakázal overovanie emailu. Ak máte účet správcu a potrebujete obísť toto obmedzenie, <0>kliknite sem0>.",
@@ -204,12 +219,5 @@
"signin.message.socialbutton.intro": "Prihlásiť sa pomocou",
"validation.custom.maxattachments": "Maximálny počet príloh je {number}.",
"validation.custom.maximagesize": "Veľkosť obrázka musí byť menšia ako {kilobytes}KB.",
- "{count, plural, one {# tag} other {# tags}}": "{count, plural, one {# tag} other {# tags}}",
- "linkmodal.insert": "Vložiť odkaz",
- "linkmodal.text.label": "Text na zobrazenie",
- "linkmodal.text.placeholder": "Zadajte text odkazu",
- "linkmodal.text.required": "Vyžaduje sa text",
- "linkmodal.title": "Vložiť odkaz",
- "linkmodal.url.invalid": "Zadajte platnú URL adresu",
- "linkmodal.url.required": "URL adresa je povinná"
+ "{count, plural, one {# tag} other {# tags}}": "{count, plural, one {# tag} other {# tags}}"
}
\ No newline at end of file
diff --git a/locale/sv-SE/client.json b/locale/sv-SE/client.json
index afd19a2e7..bb30c2c8d 100644
--- a/locale/sv-SE/client.json
+++ b/locale/sv-SE/client.json
@@ -96,6 +96,15 @@
"legal.notice": "Genom att logga in godkänner du <2/><0/> och <1/>.",
"legal.privacypolicy": "Integritetspolicy",
"legal.termsofservice": "Användarvillkor",
+ "linkmodal.insert": "Infoga länk",
+ "linkmodal.text.label": "Text att visa",
+ "linkmodal.text.placeholder": "Ange länktext",
+ "linkmodal.text.required": "Text krävs",
+ "linkmodal.title": "Infoga länk",
+ "linkmodal.url.invalid": "Ange en giltig URL",
+ "linkmodal.url.label": "",
+ "linkmodal.url.placeholder": "",
+ "linkmodal.url.required": "URL krävs",
"menu.administration": "Administration",
"menu.mysettings": "Mina Inställningar",
"menu.signout": "Logga ut",
@@ -192,6 +201,12 @@
"showpost.responseform.text.placeholder": "Vad händer med det här inlägget? Låt dina användare veta vad du planerar...",
"showpost.votespanel.more": "+{extraVotesCount} ytterligare",
"showpost.votespanel.seedetails": "visa detaljer",
+ "signin.code.edit": "",
+ "signin.code.getnew": "",
+ "signin.code.instruction": "",
+ "signin.code.placeholder": "",
+ "signin.code.sent": "",
+ "signin.code.submit": "",
"signin.email.placeholder": "E-postadress",
"signin.message.email": "Fortsätt med e-post",
"signin.message.emaildisabled": "E-postautentisering har inaktiverats av en administratör. Om du har ett administratörskonto och behöver kringgå denna begränsning, <0>klicka här0>.",
@@ -204,12 +219,5 @@
"signin.message.socialbutton.intro": "Logga in med",
"validation.custom.maxattachments": "Maximalt {number} bilagor är tillåtna.",
"validation.custom.maximagesize": "Bildstorleken måste vara mindre än {kilobytes}KB.",
- "{count, plural, one {# tag} other {# tags}}": "{count, plural, =1 {# etikett} other {# etiketter}}",
- "linkmodal.insert": "Infoga länk",
- "linkmodal.text.label": "Text att visa",
- "linkmodal.text.placeholder": "Ange länktext",
- "linkmodal.text.required": "Text krävs",
- "linkmodal.title": "Infoga länk",
- "linkmodal.url.invalid": "Ange en giltig URL",
- "linkmodal.url.required": "URL krävs"
+ "{count, plural, one {# tag} other {# tags}}": "{count, plural, =1 {# etikett} other {# etiketter}}"
}
\ No newline at end of file
diff --git a/locale/tr/client.json b/locale/tr/client.json
index 25ffe8345..e7a18cbf6 100644
--- a/locale/tr/client.json
+++ b/locale/tr/client.json
@@ -96,6 +96,15 @@
"legal.notice": "Giriş yaparak <2/><0/> ve <1/> maddelerini kabul etmiş olursunuz.",
"legal.privacypolicy": "Gizlilik Politikası",
"legal.termsofservice": "Hizmet Koşulları",
+ "linkmodal.insert": "Bağlantı Ekle",
+ "linkmodal.text.label": "Görüntülenecek metin",
+ "linkmodal.text.placeholder": "Bağlantı metnini girin",
+ "linkmodal.text.required": "Metin gereklidir",
+ "linkmodal.title": "Bağlantı Ekle",
+ "linkmodal.url.invalid": "Lütfen geçerli bir URL girin",
+ "linkmodal.url.label": "",
+ "linkmodal.url.placeholder": "",
+ "linkmodal.url.required": "URL gerekli",
"menu.administration": "Yönetim",
"menu.mysettings": "Ayarlarım",
"menu.signout": "Çıkış yap",
@@ -192,6 +201,12 @@
"showpost.responseform.text.placeholder": "Bu öneriye neler oluyor? Kullanıcılara planlarınız hakkında bilgi verin...",
"showpost.votespanel.more": "+{extraVotesCount} daha",
"showpost.votespanel.seedetails": "ayrıntıları gör",
+ "signin.code.edit": "",
+ "signin.code.getnew": "",
+ "signin.code.instruction": "",
+ "signin.code.placeholder": "",
+ "signin.code.sent": "",
+ "signin.code.submit": "",
"signin.email.placeholder": "E-posta adresi",
"signin.message.email": "E-postayla devam et",
"signin.message.emaildisabled": "E-posta doğrulaması bir yönetici tarafından devre dışı bırakıldı. Eğer bir yönetici hesabınız varsa ve bu kısıtı kaldırmak istiyorsanız <0>buraya tıklayın0>.",
@@ -204,12 +219,5 @@
"signin.message.socialbutton.intro": "İle giriş yapın",
"validation.custom.maxattachments": "En fazla {number} ek dosyaya izin verilir.",
"validation.custom.maximagesize": "Resim boyutu {kilobytes}KB'den küçük olmalıdır.",
- "{count, plural, one {# tag} other {# tags}}": "{count, plural, one {# etiket} other {# etiket}}",
- "linkmodal.insert": "Bağlantı Ekle",
- "linkmodal.text.label": "Görüntülenecek metin",
- "linkmodal.text.placeholder": "Bağlantı metnini girin",
- "linkmodal.text.required": "Metin gereklidir",
- "linkmodal.title": "Bağlantı Ekle",
- "linkmodal.url.invalid": "Lütfen geçerli bir URL girin",
- "linkmodal.url.required": "URL gerekli"
+ "{count, plural, one {# tag} other {# tags}}": "{count, plural, one {# etiket} other {# etiket}}"
}
\ No newline at end of file
diff --git a/locale/zh-CN/client.json b/locale/zh-CN/client.json
index 263649983..712c9bc23 100644
--- a/locale/zh-CN/client.json
+++ b/locale/zh-CN/client.json
@@ -96,6 +96,15 @@
"legal.notice": "在签署时,您同意<2/><0/>和<1/>。",
"legal.privacypolicy": "隐私政策",
"legal.termsofservice": "服务条款",
+ "linkmodal.insert": "插入链接",
+ "linkmodal.text.label": "要显示的文本",
+ "linkmodal.text.placeholder": "输入链接文本",
+ "linkmodal.text.required": "文本为必填项",
+ "linkmodal.title": "插入链接",
+ "linkmodal.url.invalid": "请输入有效的 URL",
+ "linkmodal.url.label": "网址",
+ "linkmodal.url.placeholder": "",
+ "linkmodal.url.required": "需要 URL",
"menu.administration": "管理",
"menu.mysettings": "我的设置",
"menu.signout": "退出",
@@ -192,6 +201,12 @@
"showpost.responseform.text.placeholder": "这篇文章怎么了?让你的用户知道你的计划是什么...",
"showpost.votespanel.more": "+{extraVotesCount} 更多",
"showpost.votespanel.seedetails": "查看详细信息",
+ "signin.code.edit": "",
+ "signin.code.getnew": "",
+ "signin.code.instruction": "",
+ "signin.code.placeholder": "",
+ "signin.code.sent": "",
+ "signin.code.submit": "",
"signin.email.placeholder": "电子邮件",
"signin.message.email": "通过电子邮件继续",
"signin.message.emaildisabled": "管理员已禁用电子邮件身份验证。如果您有管理员帐户并且需要绕过此限制,请 <0>点击这里0>.",
@@ -204,13 +219,5 @@
"signin.message.socialbutton.intro": "使用以下方式登录",
"validation.custom.maxattachments": "最多允许 {number} 个附件。",
"validation.custom.maximagesize": "图像大小必须小于{kilobytes}KB。",
- "{count, plural, one {# tag} other {# tags}}": "{count, plural, one {# tag} other {# tags}}",
- "linkmodal.insert": "插入链接",
- "linkmodal.text.label": "要显示的文本",
- "linkmodal.text.placeholder": "输入链接文本",
- "linkmodal.text.required": "文本为必填项",
- "linkmodal.title": "插入链接",
- "linkmodal.url.invalid": "请输入有效的 URL",
- "linkmodal.url.label": "网址",
- "linkmodal.url.required": "需要 URL"
+ "{count, plural, one {# tag} other {# tags}}": "{count, plural, one {# tag} other {# tags}}"
}
\ No newline at end of file
diff --git a/public/components/SignInModal.tsx b/public/components/SignInModal.tsx
index 137f6c39a..31aec720c 100644
--- a/public/components/SignInModal.tsx
+++ b/public/components/SignInModal.tsx
@@ -1,6 +1,6 @@
-import React, { useState, useEffect } from "react"
+import React from "react"
import { Modal, SignInControl, LegalFooter, TenantLogo } from "@fider/components"
-import { Button, CloseIcon } from "./common"
+import { CloseIcon } from "./common"
import { Trans } from "@lingui/react/macro"
import { HStack, VStack } from "./layout"
@@ -10,54 +10,33 @@ interface SignInModalProps {
}
export const SignInModal: React.FC = (props) => {
- const [email, setEmail] = useState("")
-
- useEffect(() => {
- if (email) {
- setTimeout(() => setEmail(""), 5000)
+ const onCodeVerified = (result: { showProfileCompletion?: boolean; code?: string }): void => {
+ if (result.showProfileCompletion && result.code) {
+ // User needs to complete profile - redirect to profile completion page
+ location.href = `/signin/complete?code=${encodeURIComponent(result.code)}`
+ } else {
+ // User is authenticated - close modal and reload to refresh the page
+ props.onClose()
+ location.reload()
}
- }, [email])
-
- const onEmailSent = (value: string): void => {
- setEmail(value)
- }
-
- const closeModal = () => {
- setEmail("")
- props.onClose()
}
- const content = email ? (
- <>
-
-
- We have just sent a confirmation link to {email} . Click the link and you’ll be signed in.
-
-
-
-
- OK
-
-
- >
- ) : (
-
- )
-
return (
-
+
-
+
- {content}
+
+
+
)
diff --git a/public/components/common/SignInControl.tsx b/public/components/common/SignInControl.tsx
index 4d417320c..ead9ae109 100644
--- a/public/components/common/SignInControl.tsx
+++ b/public/components/common/SignInControl.tsx
@@ -14,6 +14,7 @@ interface SignInControlProps {
onSubmit?: () => void
onEmailSent?: (email: string) => void
signInButtonText?: string
+ onCodeVerified?: (result: { showProfileCompletion?: boolean; code?: string }) => void
}
export const SignInControl: React.FunctionComponent = (props) => {
@@ -65,15 +66,26 @@ export const SignInControl: React.FunctionComponent = (props
const result = await actions.verifySignInCode(email, code)
if (result.ok) {
const data = result.data as { showProfileCompletion?: boolean } | undefined
- if (data && data.showProfileCompletion) {
- // User needs to complete profile - reload to show profile completion
- location.reload()
+ if (props.onCodeVerified) {
+ // Let the parent component decide what to do, pass the code along
+ props.onCodeVerified({ ...data, code })
} else {
- // User is authenticated - reload to refresh the page
+ // Default behavior: reload the page
location.reload()
}
- } else if (result.error) {
- setError(result.error)
+ } else {
+ // Handle validation errors - convert data object to Failure format
+ const data = result.data as Record | undefined
+ if (data && typeof data === "object") {
+ const errors = Object.entries(data).map(([field, message]) => ({
+ field,
+ message,
+ }))
+ setError({ errors })
+ } else if (result.error) {
+ // Display the error from the server
+ setError(result.error)
+ }
}
}
@@ -143,7 +155,14 @@ export const SignInControl: React.FunctionComponent = (props
Please type in the code we just sent to {email}
{" "}
- { e.preventDefault(); editEmail(); }}>
+ {
+ e.preventDefault()
+ editEmail()
+ }}
+ >
Edit
@@ -161,11 +180,16 @@ export const SignInControl: React.FunctionComponent = (props
Submit
- {resendMessage && (
- {resendMessage}
- )}
+ {resendMessage && {resendMessage}
}
- { e.preventDefault(); resendCode(); }}>
+ {
+ e.preventDefault()
+ resendCode()
+ }}
+ >
Get a new code
diff --git a/public/pages/Home/components/ShareFeedback.tsx b/public/pages/Home/components/ShareFeedback.tsx
index 7b453e532..37e11c186 100644
--- a/public/pages/Home/components/ShareFeedback.tsx
+++ b/public/pages/Home/components/ShareFeedback.tsx
@@ -200,8 +200,15 @@ export const ShareFeedback: React.FC = (props) => {
}
}
- const onEmailSent = (email: string) => {
- window.location.href = "/loginemailsent?email=" + encodeURIComponent(email)
+ const onCodeVerified = (result: { showProfileCompletion?: boolean; code?: string }): void => {
+ if (result.showProfileCompletion && result.code) {
+ // User needs to complete profile - redirect to profile completion page
+ // The cached feedback will be preserved for after profile setup
+ location.href = `/signin/complete?code=${encodeURIComponent(result.code)}`
+ } else {
+ // User is authenticated - finalize the feedback submission
+ finaliseFeedback()
+ }
}
const handleEditorFocus = () => {
@@ -274,7 +281,7 @@ export const ShareFeedback: React.FC = (props) => {
{
export const SignInPage = () => {
const fider = useFider()
- const onEmailSent = (email: string) => {
- notify.success(
-
-
- We have just sent a confirmation link to {email} . Click the link and you’ll be signed in.
-
-
- )
+ const onCodeVerified = (result: { showProfileCompletion?: boolean; code?: string }) => {
+ if (result.showProfileCompletion && result.code) {
+ // User needs to complete profile - redirect to profile completion page
+ location.href = `/signin/complete?code=${encodeURIComponent(result.code)}`
+ } else {
+ // User is authenticated - redirect to the appropriate URL
+ const redirect = new URLSearchParams(window.location.search).get("redirect")
+ if (redirect && redirect.startsWith("/")) {
+ location.href = fider.settings.baseURL + redirect
+ } else {
+ location.href = fider.settings.baseURL
+ }
+ }
}
const getRedirectToUrl = () => {
@@ -63,7 +67,7 @@ export const SignInPage = () => {
{fider.session.tenant.isPrivate ?
:
}
-
+
)
From 9541f36cde62b8d527c0dfbb883e538506189972 Mon Sep 17 00:00:00 2001
From: Matt Roberts
Date: Tue, 11 Nov 2025 15:51:06 +0000
Subject: [PATCH 03/11] Better flow for the code based login
---
app/actions/signin.go | 60 +++++
app/actions/signin_test.go | 47 ++++
app/cmd/routes.go | 1 +
app/handlers/signin.go | 110 ++++++++-
app/handlers/signin_test.go | 143 +++++++++++-
e2e/features/ui/post.feature | 8 +-
e2e/step_definitions/home.steps.ts | 32 ++-
locale/ar/client.json | 2 +
locale/de/client.json | 2 +
locale/el/client.json | 2 +
locale/en/client.json | 3 +
locale/es-ES/client.json | 2 +
locale/fa/client.json | 2 +
locale/fr/client.json | 2 +
locale/it/client.json | 3 +
locale/ja/client.json | 2 +
locale/nl/client.json | 2 +
locale/pl/client.json | 2 +
locale/pt-BR/client.json | 2 +
locale/ru/client.json | 2 +
locale/sk/client.json | 3 +
locale/sv-SE/client.json | 2 +
locale/tr/client.json | 2 +
locale/zh-CN/client.json | 2 +
public/components/SignInModal.tsx | 13 +-
public/components/common/SignInControl.tsx | 221 ++++++++++++------
.../pages/Home/components/ShareFeedback.tsx | 12 +-
public/pages/SignIn/SignIn.page.tsx | 17 +-
public/services/actions/tenant.ts | 4 +
29 files changed, 570 insertions(+), 135 deletions(-)
diff --git a/app/actions/signin.go b/app/actions/signin.go
index 8836394d3..7b8f8ebde 100644
--- a/app/actions/signin.go
+++ b/app/actions/signin.go
@@ -113,6 +113,66 @@ func (action *VerifySignInCode) Validate(ctx context.Context, user *entity.User)
return result
}
+// SignInByEmailWithName happens when a new user (without account) requests to sign in by email
+type SignInByEmailWithName struct {
+ Email string `json:"email" format:"lower"`
+ Name string `json:"name"`
+ VerificationCode string
+}
+
+func NewSignInByEmailWithName() *SignInByEmailWithName {
+ return &SignInByEmailWithName{
+ VerificationCode: entity.GenerateEmailVerificationCode(),
+ }
+}
+
+// IsAuthorized returns true if current user is authorized to perform this action
+func (action *SignInByEmailWithName) IsAuthorized(ctx context.Context, user *entity.User) bool {
+ tenant := ctx.Value(app.TenantCtxKey).(*entity.Tenant)
+ // New users can only sign in if tenant allows email auth or is not private
+ return tenant.IsEmailAuthAllowed || !tenant.IsPrivate
+}
+
+// Validate if current model is valid
+func (action *SignInByEmailWithName) Validate(ctx context.Context, user *entity.User) *validate.Result {
+ result := validate.Success()
+
+ if action.Email == "" {
+ result.AddFieldFailure("email", propertyIsRequired(ctx, "email"))
+ } else {
+ messages := validate.Email(ctx, action.Email)
+ result.AddFieldFailure("email", messages...)
+ }
+
+ if action.Name == "" {
+ result.AddFieldFailure("name", propertyIsRequired(ctx, "name"))
+ } else if len(action.Name) > 100 {
+ result.AddFieldFailure("name", propertyMaxStringLen(ctx, "name", 100))
+ }
+
+ return result
+}
+
+// GetEmail returns the email being verified
+func (action *SignInByEmailWithName) GetEmail() string {
+ return action.Email
+}
+
+// GetName returns the name provided by the user
+func (action *SignInByEmailWithName) GetName() string {
+ return action.Name
+}
+
+// GetUser returns the current user performing this action
+func (action *SignInByEmailWithName) GetUser() *entity.User {
+ return nil
+}
+
+// GetKind returns EmailVerificationKindSignIn
+func (action *SignInByEmailWithName) GetKind() enum.EmailVerificationKind {
+ return enum.EmailVerificationKindSignIn
+}
+
// CompleteProfile happens when users completes their profile during first time sign in
type CompleteProfile struct {
Kind enum.EmailVerificationKind `json:"kind"`
diff --git a/app/actions/signin_test.go b/app/actions/signin_test.go
index 90efad559..d7a5fde39 100644
--- a/app/actions/signin_test.go
+++ b/app/actions/signin_test.go
@@ -105,3 +105,50 @@ func TestVerifySignInCode_ValidCodeAndEmail(t *testing.T) {
result := action.Validate(context.Background(), nil)
ExpectSuccess(result)
}
+
+func TestSignInByEmailWithName_EmptyEmailAndName(t *testing.T) {
+ RegisterT(t)
+
+ action := actions.SignInByEmailWithName{Email: "", Name: ""}
+ result := action.Validate(context.Background(), nil)
+ ExpectFailed(result, "email", "name")
+}
+
+func TestSignInByEmailWithName_InvalidEmail(t *testing.T) {
+ RegisterT(t)
+
+ action := actions.SignInByEmailWithName{Email: "invalid", Name: "Jon Snow"}
+ result := action.Validate(context.Background(), nil)
+ ExpectFailed(result, "email")
+}
+
+func TestSignInByEmailWithName_EmptyName(t *testing.T) {
+ RegisterT(t)
+
+ action := actions.SignInByEmailWithName{Email: "jon.snow@got.com", Name: ""}
+ result := action.Validate(context.Background(), nil)
+ ExpectFailed(result, "name")
+}
+
+func TestSignInByEmailWithName_LongName(t *testing.T) {
+ RegisterT(t)
+
+ // 101 characters
+ longName := "12345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901"
+ action := actions.SignInByEmailWithName{Email: "jon.snow@got.com", Name: longName}
+ result := action.Validate(context.Background(), nil)
+ ExpectFailed(result, "name")
+}
+
+func TestSignInByEmailWithName_ValidEmailAndName(t *testing.T) {
+ RegisterT(t)
+
+ action := actions.NewSignInByEmailWithName()
+ action.Email = "jon.snow@got.com"
+ action.Name = "Jon Snow"
+
+ result := action.Validate(context.Background(), nil)
+ ExpectSuccess(result)
+ Expect(action.VerificationCode).IsNotEmpty()
+ Expect(len(action.VerificationCode)).Equals(6)
+}
diff --git a/app/cmd/routes.go b/app/cmd/routes.go
index 3de105b38..e315776f2 100644
--- a/app/cmd/routes.go
+++ b/app/cmd/routes.go
@@ -124,6 +124,7 @@ func routes(r *web.Engine) *web.Engine {
r.Get("/invite/verify", handlers.VerifySignInKey(enum.EmailVerificationKindUserInvitation))
r.Post("/_api/signin/complete", handlers.CompleteSignInProfile())
r.Post("/_api/signin", handlers.SignInByEmail())
+ r.Post("/_api/signin/newuser", handlers.SignInByEmailWithName())
r.Post("/_api/signin/verify", handlers.VerifySignInCode())
r.Post("/_api/signin/resend", handlers.ResendSignInCode())
diff --git a/app/handlers/signin.go b/app/handlers/signin.go
index cf7a88368..4ad1bb2f6 100644
--- a/app/handlers/signin.go
+++ b/app/handlers/signin.go
@@ -71,7 +71,7 @@ func NotInvitedPage() web.HandlerFunc {
}
}
-// SignInByEmail sends a new email with verification code
+// SignInByEmail checks if user exists and sends code only for existing users
func SignInByEmail() web.HandlerFunc {
return func(c *web.Context) error {
action := actions.NewSignInByEmail()
@@ -79,7 +79,56 @@ func SignInByEmail() web.HandlerFunc {
return c.HandleValidation(result)
}
- err := bus.Dispatch(c, &cmd.SaveVerificationKey{
+ // Check if user exists
+ userByEmail := &query.GetUserByEmail{Email: action.Email}
+ err := bus.Dispatch(c, userByEmail)
+ userExists := err == nil
+
+ // Only send code if user exists
+ if userExists {
+ err := bus.Dispatch(c, &cmd.SaveVerificationKey{
+ Key: action.VerificationCode,
+ Duration: 15 * time.Minute,
+ Request: action,
+ })
+ if err != nil {
+ return c.Failure(err)
+ }
+
+ c.Enqueue(tasks.SendSignInEmail(action.Email, action.VerificationCode))
+ }
+
+ return c.Ok(web.Map{
+ "userExists": userExists,
+ })
+ }
+}
+
+// SignInByEmailWithName sends verification code for new users with their name
+func SignInByEmailWithName() web.HandlerFunc {
+ return func(c *web.Context) error {
+ action := actions.NewSignInByEmailWithName()
+ if result := c.BindTo(action); !result.Ok {
+ return c.HandleValidation(result)
+ }
+
+ // Check that user doesn't already exist
+ userByEmail := &query.GetUserByEmail{Email: action.Email}
+ err := bus.Dispatch(c, userByEmail)
+ if err == nil {
+ // User already exists, should use regular sign in
+ return c.BadRequest(web.Map{
+ "email": "An account with this email already exists. Please sign in.",
+ })
+ }
+
+ // Check if tenant is private (new users not allowed)
+ if c.Tenant().IsPrivate {
+ return c.Forbidden()
+ }
+
+ // Save verification with name
+ err = bus.Dispatch(c, &cmd.SaveVerificationKey{
Key: action.VerificationCode,
Duration: 15 * time.Minute,
Request: action,
@@ -139,10 +188,36 @@ func VerifySignInCode() web.HandlerFunc {
err = bus.Dispatch(c, userByEmail)
if err != nil {
if errors.Cause(err) == app.ErrNotFound {
- // User doesn't exist, need to complete profile
+ // User doesn't exist
if c.Tenant().IsPrivate {
return c.Forbidden()
}
+
+ // If name is provided in verification, create user account
+ if result.Name != "" {
+ user := &entity.User{
+ Name: result.Name,
+ Email: result.Email,
+ Tenant: c.Tenant(),
+ Role: enum.RoleVisitor,
+ }
+ err = bus.Dispatch(c, &cmd.RegisterUser{User: user})
+ if err != nil {
+ return c.Failure(err)
+ }
+
+ // Mark code as verified
+ err = bus.Dispatch(c, &cmd.SetKeyAsVerified{Key: action.Code})
+ if err != nil {
+ return c.Failure(err)
+ }
+
+ // Authenticate newly created user
+ webutil.AddAuthUserCookie(c, user)
+ return c.Ok(web.Map{})
+ }
+
+ // Name not provided - shouldn't happen with new flow, but handle legacy case
return c.Ok(web.Map{
"showProfileCompletion": true,
})
@@ -211,14 +286,27 @@ func VerifySignInKey(kind enum.EmailVerificationKind) web.HandlerFunc {
return NotInvitedPage()(c)
}
- return c.Page(http.StatusOK, web.Props{
- Page: "SignIn/CompleteSignInProfile.page",
- Title: "Complete Sign In Profile",
- Data: web.Map{
- "kind": kind,
- "k": key,
- },
- })
+ // User should already have entered their name so we can get them registered.
+ user := &entity.User{
+ Name: result.Name,
+ Email: result.Email,
+ Tenant: c.Tenant(),
+ Role: enum.RoleVisitor,
+ }
+ err = bus.Dispatch(c, &cmd.RegisterUser{User: user})
+ if err != nil {
+ return c.Failure(err)
+ }
+
+ err = bus.Dispatch(c, &cmd.SetKeyAsVerified{Key: key})
+ if err != nil {
+ return c.Failure(err)
+ }
+
+ webutil.AddAuthUserCookie(c, user)
+ baseURL := c.BaseURL()
+ return c.Redirect(baseURL)
+
}
return c.Failure(err)
}
diff --git a/app/handlers/signin_test.go b/app/handlers/signin_test.go
index 01c508a70..88f1fcdd9 100644
--- a/app/handlers/signin_test.go
+++ b/app/handlers/signin_test.go
@@ -34,7 +34,7 @@ func TestSignInByEmailHandler_WithoutEmail(t *testing.T) {
Expect(code).Equals(http.StatusBadRequest)
}
-func TestSignInByEmailHandler_WithEmail(t *testing.T) {
+func TestSignInByEmailHandler_ExistingUser(t *testing.T) {
RegisterT(t)
var saveKeyCmd *cmd.SaveVerificationKey
@@ -43,8 +43,16 @@ func TestSignInByEmailHandler_WithEmail(t *testing.T) {
return nil
})
+ bus.AddHandler(func(ctx context.Context, q *query.GetUserByEmail) error {
+ if q.Email == "jon.snow@got.com" {
+ q.Result = mock.JonSnow
+ return nil
+ }
+ return app.ErrNotFound
+ })
+
server := mock.NewServer()
- code, _ := server.
+ code, response := server.
OnTenant(mock.DemoTenant).
ExecutePost(handlers.SignInByEmail(), `{ "email": "jon.snow@got.com" }`)
@@ -52,7 +60,90 @@ func TestSignInByEmailHandler_WithEmail(t *testing.T) {
Expect(saveKeyCmd.Key).HasLen(6)
Expect(saveKeyCmd.Request.GetKind()).Equals(enum.EmailVerificationKindSignIn)
Expect(saveKeyCmd.Request.GetEmail()).Equals("jon.snow@got.com")
- Expect(saveKeyCmd.Request.GetName()).Equals("")
+ Expect(response.Body.String()).ContainsSubstring(`"userExists":true`)
+}
+
+func TestSignInByEmailHandler_NewUser(t *testing.T) {
+ RegisterT(t)
+
+ bus.AddHandler(func(ctx context.Context, q *query.GetUserByEmail) error {
+ return app.ErrNotFound
+ })
+
+ server := mock.NewServer()
+ code, response := server.
+ OnTenant(mock.DemoTenant).
+ ExecutePost(handlers.SignInByEmail(), `{ "email": "new.user@got.com" }`)
+
+ Expect(code).Equals(http.StatusOK)
+ Expect(response.Body.String()).ContainsSubstring(`"userExists":false`)
+}
+
+func TestSignInByEmailWithNameHandler_NewUser(t *testing.T) {
+ RegisterT(t)
+
+ var saveKeyCmd *cmd.SaveVerificationKey
+ bus.AddHandler(func(ctx context.Context, c *cmd.SaveVerificationKey) error {
+ saveKeyCmd = c
+ return nil
+ })
+
+ bus.AddHandler(func(ctx context.Context, q *query.GetUserByEmail) error {
+ return app.ErrNotFound
+ })
+
+ server := mock.NewServer()
+ code, _ := server.
+ OnTenant(mock.DemoTenant).
+ ExecutePost(handlers.SignInByEmailWithName(), `{ "email": "new.user@got.com", "name": "New User" }`)
+
+ Expect(code).Equals(http.StatusOK)
+ Expect(saveKeyCmd.Key).HasLen(6)
+ Expect(saveKeyCmd.Request.GetKind()).Equals(enum.EmailVerificationKindSignIn)
+ Expect(saveKeyCmd.Request.GetEmail()).Equals("new.user@got.com")
+ Expect(saveKeyCmd.Request.GetName()).Equals("New User")
+}
+
+func TestSignInByEmailWithNameHandler_ExistingUser(t *testing.T) {
+ RegisterT(t)
+
+ bus.AddHandler(func(ctx context.Context, q *query.GetUserByEmail) error {
+ if q.Email == "jon.snow@got.com" {
+ q.Result = mock.JonSnow
+ return nil
+ }
+ return app.ErrNotFound
+ })
+
+ server := mock.NewServer()
+ code, response := server.
+ OnTenant(mock.DemoTenant).
+ ExecutePost(handlers.SignInByEmailWithName(), `{ "email": "jon.snow@got.com", "name": "Jon Snow" }`)
+
+ Expect(code).Equals(http.StatusBadRequest)
+ Expect(response.Body.String()).ContainsSubstring("already exists")
+}
+
+func TestSignInByEmailWithNameHandler_PrivateTenant(t *testing.T) {
+ RegisterT(t)
+
+ bus.AddHandler(func(ctx context.Context, q *query.GetUserByEmail) error {
+ return app.ErrNotFound
+ })
+
+ privateTenant := &entity.Tenant{
+ ID: 1,
+ Name: "Private Tenant",
+ Subdomain: "private",
+ IsPrivate: true,
+ }
+
+ server := mock.NewServer()
+ code, _ := server.
+ OnTenant(privateTenant).
+ ExecutePost(handlers.SignInByEmailWithName(), `{ "email": "new.user@got.com", "name": "New User" }`)
+
+ Expect(code).Equals(http.StatusForbidden)
}
func TestVerifySignInKeyHandler_UnknownKey(t *testing.T) {
@@ -713,7 +804,7 @@ func TestVerifySignInCodeHandler_CorrectCode_ExistingUser(t *testing.T) {
ExpectFiderAuthCookie(response, mock.JonSnow)
}
-func TestVerifySignInCodeHandler_CorrectCode_NewUser(t *testing.T) {
+func TestVerifySignInCodeHandler_CorrectCode_NewUser_WithoutName(t *testing.T) {
RegisterT(t)
bus.AddHandler(func(ctx context.Context, q *query.GetVerificationByEmailAndCode) error {
@@ -722,6 +813,7 @@ func TestVerifySignInCodeHandler_CorrectCode_NewUser(t *testing.T) {
Key: "123456",
CreatedAt: time.Now(),
ExpiresAt: time.Now().Add(15 * time.Minute),
+ Name: "", // No name stored (legacy flow)
}
return nil
})
@@ -739,6 +831,49 @@ func TestVerifySignInCodeHandler_CorrectCode_NewUser(t *testing.T) {
Expect(response.Body.String()).ContainsSubstring(`"showProfileCompletion":true`)
}
+func TestVerifySignInCodeHandler_CorrectCode_NewUser_WithName(t *testing.T) {
+ RegisterT(t)
+
+ bus.AddHandler(func(ctx context.Context, q *query.GetVerificationByEmailAndCode) error {
+ q.Result = &entity.EmailVerification{
+ Email: "new.user@got.com",
+ Key: "123456",
+ CreatedAt: time.Now(),
+ ExpiresAt: time.Now().Add(15 * time.Minute),
+ Name: "New User", // Name stored (new flow)
+ }
+ return nil
+ })
+
+ bus.AddHandler(func(ctx context.Context, q *query.GetUserByEmail) error {
+ return app.ErrNotFound
+ })
+
+ userRegistered := false
+ var registeredUser *entity.User
+ bus.AddHandler(func(ctx context.Context, c *cmd.RegisterUser) error {
+ userRegistered = true
+ registeredUser = c.User
+ registeredUser.ID = 999
+ return nil
+ })
+
+ bus.AddHandler(func(ctx context.Context, c *cmd.SetKeyAsVerified) error {
+ return nil
+ })
+
+ server := mock.NewServer()
+ code, response := server.
+ OnTenant(mock.DemoTenant).
+ ExecutePost(handlers.VerifySignInCode(), `{ "email": "new.user@got.com", "code": "123456" }`)
+
+ Expect(code).Equals(http.StatusOK)
+ Expect(userRegistered).IsTrue()
+ Expect(registeredUser.Name).Equals("New User")
+ Expect(registeredUser.Email).Equals("new.user@got.com")
+ ExpectFiderAuthCookie(response, registeredUser)
+}
+
func TestVerifySignInCodeHandler_CorrectCode_NewUser_PrivateTenant(t *testing.T) {
RegisterT(t)
diff --git a/e2e/features/ui/post.feature b/e2e/features/ui/post.feature
index 4344fdbd4..7a69abb88 100644
--- a/e2e/features/ui/post.feature
+++ b/e2e/features/ui/post.feature
@@ -24,11 +24,11 @@ Feature: Post
And I type "This is a draft post from a new user" as the description
And I type my email address
And I click continue with email
- Then I should be on the confirmation link page
- Given I click on the confirmation link
- Then I should be on the complete profile page
+ Then I should see the name field
Given I enter my name as "Matt"
- And I click submit
+ And I click continue
+ Then I should be on the confirmation code page
+ Given I enter the confirmation code
Then I should be on the home page
And I should see the new post modal
And I should see "This is a draft post from a new user" as the draft post title
\ No newline at end of file
diff --git a/e2e/step_definitions/home.steps.ts b/e2e/step_definitions/home.steps.ts
index 3b3059701..e03eca7c0 100644
--- a/e2e/step_definitions/home.steps.ts
+++ b/e2e/step_definitions/home.steps.ts
@@ -44,11 +44,30 @@ Given("I click continue with email", async function () {
await this.page.click(".c-signin-control button[type='submit']")
})
+Then("I should see the name field", async function (this: FiderWorld) {
+ // Wait for the name field to appear
+ await this.page.waitForSelector("#input-name", { timeout: 5000 })
+ const nameField = await this.page.locator("#input-name")
+ await expect(nameField).toBeVisible()
+})
+
+Given("I click continue", async function () {
+ await this.page.click("button[type='submit']")
+})
+
Given("I click submit your feedback", async function () {
await this.page.click(".c-share-feedback__content .c-button--primary")
})
-Given("I click on the confirmation link", async function (this: FiderWorld) {
+Then("I should be on the confirmation code page", async function (this: FiderWorld) {
+ const userEmail = `$user-${this.tenantName}@fider.io`
+ // Wait for the code entry field to appear
+ await this.page.waitForSelector("#input-code", { timeout: 5000 })
+ // Check for code entry instruction message
+ await expect(this.page.getByText(`Please type in the code we just sent to ${userEmail}`)).toBeVisible()
+})
+
+Given("I enter the confirmation code", async function (this: FiderWorld) {
const userEmail = `$user-${this.tenantName}@fider.io`
const code = await getLatestCodeSentTo(userEmail)
@@ -60,11 +79,6 @@ Given("I click on the confirmation link", async function (this: FiderWorld) {
await this.page.waitForLoadState("networkidle")
})
-Then("I should be on the complete profile page", async function (this: FiderWorld) {
- const container = await this.page.$$("#p-complete-profile")
- await expect(container).toBeDefined()
-})
-
Then("I should see the new post modal", async function (this: FiderWorld) {
const container = await this.page.getByTestId("modal")
await expect(container).toBeVisible()
@@ -78,12 +92,6 @@ Given("I click submit", async function () {
await this.page.click("button[type='submit']")
})
-Then("I should be on the confirmation link page", async function (this: FiderWorld) {
- const userEmail = `$user-${this.tenantName}@fider.io`
- // Updated to check for code entry message instead of link message
- await expect(this.page.getByText(`Please type in the code we just sent to ${userEmail}`)).toBeVisible()
-})
-
Then("I should see {string} as the draft post title", async function (this: FiderWorld, title: string) {
const postTitle = await this.page.locator("#input-title").inputValue()
await expect(postTitle).toBe(title)
diff --git a/locale/ar/client.json b/locale/ar/client.json
index e5d712839..e6733320f 100644
--- a/locale/ar/client.json
+++ b/locale/ar/client.json
@@ -4,6 +4,7 @@
"action.close": "إغلاق",
"action.commentsfeed": "تغذية التعليقات",
"action.confirm": "تأكيد",
+ "action.continue": "",
"action.copylink": "نسخ الرابط",
"action.delete": "حذف",
"action.edit": "تعديل",
@@ -217,6 +218,7 @@
"signin.message.private.text": "إذا كان لديك حساب أو دعوت، فيمكنك استخدام الخيارات التالية لتسجيل الدخول.",
"signin.message.private.title": "<0>{0}0> مساحة خاصة، يجب عليك تسجيل الدخول للمشاركة والتصويت.",
"signin.message.socialbutton.intro": "تسجيل الدخول بواسطة",
+ "signin.name.placeholder": "",
"validation.custom.maxattachments": "يُسمح بحد أقصى {number} من المرفقات.",
"validation.custom.maximagesize": "يجب أن يكون حجم الصورة أصغر من {kilobytes}KB.",
"{count, plural, one {# tag} other {# tags}}": "{count, plural, zero {}one {# وسم} two {# وسوم} few {# وسوم} many {# وسوم} other {# وسوم}}"
diff --git a/locale/de/client.json b/locale/de/client.json
index 2270c769a..f03326c46 100644
--- a/locale/de/client.json
+++ b/locale/de/client.json
@@ -4,6 +4,7 @@
"action.close": "Schließen",
"action.commentsfeed": "Kommentar-Feed",
"action.confirm": "Bestätigen",
+ "action.continue": "",
"action.copylink": "Link kopieren",
"action.delete": "Löschen",
"action.edit": "Bearbeiten",
@@ -217,6 +218,7 @@
"signin.message.private.text": "Wenn du ein Konto oder eine Einladung hast, kannst du folgende Optionen nutzen, um dich anzumelden.",
"signin.message.private.title": "<0>{0}0> ist ein privater Raum, du musst dich anmelden, um teilzunehmen und abstimmen zu können.",
"signin.message.socialbutton.intro": "Einloggen mit",
+ "signin.name.placeholder": "",
"validation.custom.maxattachments": "Es sind maximal {number} Anhänge zulässig.",
"validation.custom.maximagesize": "Die Bildgröße muss kleiner als {kilobytes}KB sein.",
"{count, plural, one {# tag} other {# tags}}": "{count, plural, one {# Tag} other {# Tags}}"
diff --git a/locale/el/client.json b/locale/el/client.json
index 6cd91acc6..deeb85ea0 100644
--- a/locale/el/client.json
+++ b/locale/el/client.json
@@ -4,6 +4,7 @@
"action.close": "Κλείσιμο",
"action.commentsfeed": "Ροή σχολίων",
"action.confirm": "Επιβεβαίωση",
+ "action.continue": "",
"action.copylink": "Αντιγραφή συνδέσμου",
"action.delete": "Διαγραφή",
"action.edit": "Επεξεργασία",
@@ -217,6 +218,7 @@
"signin.message.private.text": "Αν έχετε λογαριασμό ή πρόσκληση, μπορείτε να χρησιμοποιήσετε τις παρακάτω επιλογές για να συνδεθείτε.",
"signin.message.private.title": "<0>{0}0> είναι ένας ιδιωτικός χώρος, πρέπει να συνδεθείτε για να συμμετάσχετε και να ψηφίσετε.",
"signin.message.socialbutton.intro": "Συνδεθείτε με",
+ "signin.name.placeholder": "",
"validation.custom.maxattachments": "Επιτρέπονται έως {number} συνημμένα.",
"validation.custom.maximagesize": "Το μέγεθος της εικόνας πρέπει να είναι μικρότερο από {kilobytes}KB.",
"{count, plural, one {# tag} other {# tags}}": "{count, plural, one {# ετικέτα} other {# ετικέτες}}"
diff --git a/locale/en/client.json b/locale/en/client.json
index ca81b5b7c..9fecb8f4b 100644
--- a/locale/en/client.json
+++ b/locale/en/client.json
@@ -4,6 +4,7 @@
"action.close": "Close",
"action.commentsfeed": "Comment Feed",
"action.confirm": "Confirm",
+ "action.continue": "Continue",
"action.copylink": "Copy link",
"action.delete": "Delete",
"action.edit": "Edit",
@@ -13,6 +14,7 @@
"action.respond": "Respond",
"action.save": "Save",
"action.signin": "Sign in",
+ "action.signup": "Sign up",
"action.submit": "Submit",
"action.vote": "Vote for this idea",
"action.voted": "Voted!",
@@ -218,6 +220,7 @@
"signin.message.private.text": "If you have an account or an invitation, you may use following options to sign in.",
"signin.message.private.title": "<0>{0}0> is a private space, you must sign in to participate and vote.",
"signin.message.socialbutton.intro": "Continue with",
+ "signin.name.placeholder": "Your name",
"validation.custom.maxattachments": "A maximum of {number} attachments are allowed.",
"validation.custom.maximagesize": "The image size must be smaller than {kilobytes}KB.",
"{count, plural, one {# tag} other {# tags}}": "{count, plural, one {# tag} other {# tags}}"
diff --git a/locale/es-ES/client.json b/locale/es-ES/client.json
index f67c46996..e87d9551a 100644
--- a/locale/es-ES/client.json
+++ b/locale/es-ES/client.json
@@ -4,6 +4,7 @@
"action.close": "Cerrar",
"action.commentsfeed": "Feed de comentarios",
"action.confirm": "Confirmar",
+ "action.continue": "",
"action.copylink": "Copiar enlace",
"action.delete": "Eliminar",
"action.edit": "Editar",
@@ -217,6 +218,7 @@
"signin.message.private.text": "Si tienes una cuenta o una invitación, puedes usar las siguientes opciones para iniciar sesión.",
"signin.message.private.title": "<0>{0}0> es un espacio privado, debes iniciar sesión para participar y votar.",
"signin.message.socialbutton.intro": "Iniciar sesión con",
+ "signin.name.placeholder": "",
"validation.custom.maxattachments": "Se permite un máximo de {number} archivos adjuntos.",
"validation.custom.maximagesize": "El tamaño de la imagen debe ser menor que {kilobytes}KB.",
"{count, plural, one {# tag} other {# tags}}": "{count, plural, one {# etiqueta} other {# etiquetas}}"
diff --git a/locale/fa/client.json b/locale/fa/client.json
index cd32c55fb..c00703d2d 100644
--- a/locale/fa/client.json
+++ b/locale/fa/client.json
@@ -4,6 +4,7 @@
"action.close": "بستن",
"action.commentsfeed": "فید نظرات",
"action.confirm": "تأیید",
+ "action.continue": "",
"action.copylink": "کپی لینک",
"action.delete": "حذف",
"action.edit": "ویرایش",
@@ -217,6 +218,7 @@
"signin.message.private.text": "اگر حساب یا دعوتنامه دارید، از گزینههای زیر برای ورود استفاده کنید.",
"signin.message.private.title": "<0>{0}0> یک فضای خصوصی است؛ برای مشارکت باید وارد شوید.",
"signin.message.socialbutton.intro": "ورود با",
+ "signin.name.placeholder": "",
"validation.custom.maxattachments": "حداکثر تعداد {number} پیوست مجاز است.",
"validation.custom.maximagesize": "حجم تصویر باید کمتر از {kilobytes}KB باشد.",
"{count, plural, one {# tag} other {# tags}}": "{count, plural, one {# برچسب} other {# برچسب}}"
diff --git a/locale/fr/client.json b/locale/fr/client.json
index 4485c5d6d..b971a41ab 100644
--- a/locale/fr/client.json
+++ b/locale/fr/client.json
@@ -4,6 +4,7 @@
"action.close": "Fermer",
"action.commentsfeed": "Flux de commentaires",
"action.confirm": "Confirmer",
+ "action.continue": "",
"action.copylink": "Copier le lien",
"action.delete": "Supprimer",
"action.edit": "Modifier",
@@ -217,6 +218,7 @@
"signin.message.private.text": "Si vous avez un compte ou une invitation, vous pouvez utiliser les options suivantes pour vous connecter.",
"signin.message.private.title": "<0>{0}0> est un espace privé, vous devez vous connecter pour participer et voter.",
"signin.message.socialbutton.intro": "Se connecter avec",
+ "signin.name.placeholder": "",
"validation.custom.maxattachments": "Un maximum de {number} pièces jointes est autorisé.",
"validation.custom.maximagesize": "La taille de l'image doit être inférieure à {kilobytes}KB.",
"{count, plural, one {# tag} other {# tags}}": "{count, plural, one {# tag} other {# tags}}"
diff --git a/locale/it/client.json b/locale/it/client.json
index a625e586b..2c998ad41 100644
--- a/locale/it/client.json
+++ b/locale/it/client.json
@@ -4,6 +4,7 @@
"action.close": "Chiudi",
"action.commentsfeed": "Feed dei commenti",
"action.confirm": "Conferma",
+ "action.continue": "",
"action.copylink": "Copia il collegamento",
"action.delete": "Cancella",
"action.edit": "Modifica",
@@ -217,6 +218,8 @@
"signin.message.private.text": "Se si dispone di un account o di un invito, è possibile utilizzare le seguenti opzioni per accedere.",
"signin.message.private.title": "<0>{0}0> è uno spazio privato, è necessario registrarsi per partecipare e votare.",
"signin.message.socialbutton.intro": "Accedi con",
+ "
+ "signin.name.placeholder": "",
"validation.custom.maxattachments": "Sono consentiti al massimo {number} allegati.",
"validation.custom.maximagesize": "La dimensione dell'immagine deve essere inferiore a {kilobytes}KB.",
"{count, plural, one {# tag} other {# tags}}": "{count, plural, one {# tag} other {# tags}}"
diff --git a/locale/ja/client.json b/locale/ja/client.json
index 91bc31451..b8635ab28 100644
--- a/locale/ja/client.json
+++ b/locale/ja/client.json
@@ -4,6 +4,7 @@
"action.close": "閉じる",
"action.commentsfeed": "コメントフィード",
"action.confirm": "確認",
+ "action.continue": "",
"action.copylink": "リンクをコピー",
"action.delete": "削除",
"action.edit": "編集",
@@ -217,6 +218,7 @@
"signin.message.private.text": "アカウントや招待状をお持ちの場合は、以下のオプションを使用してサインインできます。",
"signin.message.private.title": "<0>{0}0> はプライベートなスペースです。サインインして投票してください。",
"signin.message.socialbutton.intro": "ログイン",
+ "signin.name.placeholder": "",
"validation.custom.maxattachments": "最大 {number} 個の添付ファイルが許可されます。",
"validation.custom.maximagesize": "画像サイズは{kilobytes}KB未満である必要があります。",
"{count, plural, one {# tag} other {# tags}}": "{count, plural, one {# tag} other {# tags}}"
diff --git a/locale/nl/client.json b/locale/nl/client.json
index 4b0852a88..327aeb96d 100644
--- a/locale/nl/client.json
+++ b/locale/nl/client.json
@@ -4,6 +4,7 @@
"action.close": "Sluiten",
"action.commentsfeed": "Reactiefeed",
"action.confirm": "Bevestigen",
+ "action.continue": "",
"action.copylink": "Link kopiëren",
"action.delete": "Verwijderen",
"action.edit": "Bewerken",
@@ -217,6 +218,7 @@
"signin.message.private.text": "Als je een account of een uitnodiging hebt, kun je de volgende methodes gebruiken om in te loggen.",
"signin.message.private.title": "<0>{0}0> is een privéruimte. U moet ingelogd zijn om deel te nemen en te stemmen.",
"signin.message.socialbutton.intro": "Inloggen met",
+ "signin.name.placeholder": "",
"validation.custom.maxattachments": "Er zijn maximaal {number} bijlagen toegestaan.",
"validation.custom.maximagesize": "De afbeeldingsgrootte moet kleiner zijn dan {kilobytes}KB.",
"{count, plural, one {# tag} other {# tags}}": "{count, plural, one {# tag} other {# tags}}"
diff --git a/locale/pl/client.json b/locale/pl/client.json
index e27c791a7..1ea0df213 100644
--- a/locale/pl/client.json
+++ b/locale/pl/client.json
@@ -4,6 +4,7 @@
"action.close": "Zamknij",
"action.commentsfeed": "Kanał komentarzy",
"action.confirm": "Potwierdź",
+ "action.continue": "",
"action.copylink": "Kopiuj link",
"action.delete": "Usuń",
"action.edit": "Edytuj",
@@ -217,6 +218,7 @@
"signin.message.private.text": "Jeśli posiadasz konto lub zaproszenie możesz użyć poniższych opcji aby się zalogować.",
"signin.message.private.title": "<0>{0}0> to przestrzeń prywatna, musisz się zalogować, aby uczestniczyć i głosować.",
"signin.message.socialbutton.intro": "Zaloguj się za pomocą",
+ "signin.name.placeholder": "",
"validation.custom.maxattachments": "Maksymalna liczba załączników to {number}.",
"validation.custom.maximagesize": "Rozmiar obrazu musi być mniejszy niż {kilobytes}KB.",
"{count, plural, one {# tag} other {# tags}}": "{count, plural, one {# tag} few {# tagów} many {# tagów} other {# tagi}}"
diff --git a/locale/pt-BR/client.json b/locale/pt-BR/client.json
index dd70260b9..a8440a802 100644
--- a/locale/pt-BR/client.json
+++ b/locale/pt-BR/client.json
@@ -4,6 +4,7 @@
"action.close": "Fechar",
"action.commentsfeed": "Feed de comentários",
"action.confirm": "Confirmar",
+ "action.continue": "",
"action.copylink": "Copiar link",
"action.delete": "Deletar",
"action.edit": "Editar",
@@ -217,6 +218,7 @@
"signin.message.private.text": "Se você tem uma conta ou um convite, você pode usar as seguintes opções para fazer o login.",
"signin.message.private.title": "<0>{0}0> é um espaço privado, você deve se inscrever para participar e votar.",
"signin.message.socialbutton.intro": "Fazer login com",
+ "signin.name.placeholder": "",
"validation.custom.maxattachments": "São permitidos no máximo {number} anexos.",
"validation.custom.maximagesize": "O tamanho da imagem deve ser menor que {kilobytes}KB.",
"{count, plural, one {# tag} other {# tags}}": "{count, plural, one {# tag} other {# tags}}"
diff --git a/locale/ru/client.json b/locale/ru/client.json
index 383718415..6bd38280f 100644
--- a/locale/ru/client.json
+++ b/locale/ru/client.json
@@ -4,6 +4,7 @@
"action.close": "Закрыть",
"action.commentsfeed": "Лента комментариев",
"action.confirm": "Подтвердить",
+ "action.continue": "",
"action.copylink": "Копировать ссылку",
"action.delete": "Удалить",
"action.edit": "Изменить",
@@ -217,6 +218,7 @@
"signin.message.private.text": "Если у вас есть аккаунт или приглашение, вы можете использовать их для входа.",
"signin.message.private.title": "<0>{0}0> является приватным пространством, вы должны войти в систему, чтобы принять участие и проголосовать.",
"signin.message.socialbutton.intro": "Войти с помощью",
+ "signin.name.placeholder": "",
"validation.custom.maxattachments": "Разрешено максимум {number} вложений.",
"validation.custom.maximagesize": "Размер изображения должен быть меньше {kilobytes}КБ.",
"{count, plural, one {# tag} other {# tags}}": "{count, plural, one {# tag} other {# tags}}"
diff --git a/locale/sk/client.json b/locale/sk/client.json
index 1ad731433..33572a28d 100644
--- a/locale/sk/client.json
+++ b/locale/sk/client.json
@@ -4,6 +4,7 @@
"action.close": "Zavrieť",
"action.commentsfeed": "Kanál komentárov",
"action.confirm": "Potvrdiť",
+ "action.continue": "",
"action.copylink": "Kopírovať odkaz",
"action.delete": "Vymazať",
"action.edit": "Upraviť",
@@ -217,6 +218,8 @@
"signin.message.private.text": "Ak máte účet alebo pozvánku, na prihlásenie môžete použiť nasledujúce možnosti.",
"signin.message.private.title": "<0>{0}0> je súkromný priestor, ak sa chcete zúčastniť diskusie a hlasovať, musíte sa prihlásiť.",
"signin.message.socialbutton.intro": "Prihlásiť sa pomocou",
+ "
+ "signin.name.placeholder": "",
"validation.custom.maxattachments": "Maximálny počet príloh je {number}.",
"validation.custom.maximagesize": "Veľkosť obrázka musí byť menšia ako {kilobytes}KB.",
"{count, plural, one {# tag} other {# tags}}": "{count, plural, one {# tag} other {# tags}}"
diff --git a/locale/sv-SE/client.json b/locale/sv-SE/client.json
index bb30c2c8d..16c827cdc 100644
--- a/locale/sv-SE/client.json
+++ b/locale/sv-SE/client.json
@@ -4,6 +4,7 @@
"action.close": "Stäng",
"action.commentsfeed": "Kommentarflöde",
"action.confirm": "Bekräfta",
+ "action.continue": "",
"action.copylink": "Kopiera länk",
"action.delete": "Radera",
"action.edit": "Ändra",
@@ -217,6 +218,7 @@
"signin.message.private.text": "Om du har ett konto eller en inbjudan kan du använda följande alternativ för att logga in.",
"signin.message.private.title": "<0>{0}0> är ett privat utrymme, du måste logga in för att delta och rösta.",
"signin.message.socialbutton.intro": "Logga in med",
+ "signin.name.placeholder": "",
"validation.custom.maxattachments": "Maximalt {number} bilagor är tillåtna.",
"validation.custom.maximagesize": "Bildstorleken måste vara mindre än {kilobytes}KB.",
"{count, plural, one {# tag} other {# tags}}": "{count, plural, =1 {# etikett} other {# etiketter}}"
diff --git a/locale/tr/client.json b/locale/tr/client.json
index e7a18cbf6..c3e0fe467 100644
--- a/locale/tr/client.json
+++ b/locale/tr/client.json
@@ -4,6 +4,7 @@
"action.close": "Kapat",
"action.commentsfeed": "Yorum Beslemesi",
"action.confirm": "Onayla",
+ "action.continue": "",
"action.copylink": "Bağlantıyı kopyala",
"action.delete": "Sil",
"action.edit": "Düzenle",
@@ -217,6 +218,7 @@
"signin.message.private.text": "Eğer bir hesabınız ya da davetiyeniz varsa aşağıdaki seçenekleri kullanarak giriş yapabilirsiniz.",
"signin.message.private.title": "<0>{0}0> özel bir alandır ve katılabilmek için davetiye almanız gerekir.",
"signin.message.socialbutton.intro": "İle giriş yapın",
+ "signin.name.placeholder": "",
"validation.custom.maxattachments": "En fazla {number} ek dosyaya izin verilir.",
"validation.custom.maximagesize": "Resim boyutu {kilobytes}KB'den küçük olmalıdır.",
"{count, plural, one {# tag} other {# tags}}": "{count, plural, one {# etiket} other {# etiket}}"
diff --git a/locale/zh-CN/client.json b/locale/zh-CN/client.json
index 712c9bc23..30f96f080 100644
--- a/locale/zh-CN/client.json
+++ b/locale/zh-CN/client.json
@@ -4,6 +4,7 @@
"action.close": "关闭",
"action.commentsfeed": "评论提要",
"action.confirm": "确认",
+ "action.continue": "",
"action.copylink": "复制链接",
"action.delete": "删除",
"action.edit": "编辑",
@@ -217,6 +218,7 @@
"signin.message.private.text": "如果您有帐户或邀请,您可以使用以下选项登录.",
"signin.message.private.title": "<0>{0}0> 这是一个私人空间,您必须登录才能参与和投票.",
"signin.message.socialbutton.intro": "使用以下方式登录",
+ "signin.name.placeholder": "",
"validation.custom.maxattachments": "最多允许 {number} 个附件。",
"validation.custom.maximagesize": "图像大小必须小于{kilobytes}KB。",
"{count, plural, one {# tag} other {# tags}}": "{count, plural, one {# tag} other {# tags}}"
diff --git a/public/components/SignInModal.tsx b/public/components/SignInModal.tsx
index 31aec720c..c27a65718 100644
--- a/public/components/SignInModal.tsx
+++ b/public/components/SignInModal.tsx
@@ -10,15 +10,10 @@ interface SignInModalProps {
}
export const SignInModal: React.FC = (props) => {
- const onCodeVerified = (result: { showProfileCompletion?: boolean; code?: string }): void => {
- if (result.showProfileCompletion && result.code) {
- // User needs to complete profile - redirect to profile completion page
- location.href = `/signin/complete?code=${encodeURIComponent(result.code)}`
- } else {
- // User is authenticated - close modal and reload to refresh the page
- props.onClose()
- location.reload()
- }
+ const onCodeVerified = (): void => {
+ // User is authenticated - close modal and reload to refresh the page
+ props.onClose()
+ location.reload()
}
return (
diff --git a/public/components/common/SignInControl.tsx b/public/components/common/SignInControl.tsx
index ead9ae109..894e458e7 100644
--- a/public/components/common/SignInControl.tsx
+++ b/public/components/common/SignInControl.tsx
@@ -14,20 +14,25 @@ interface SignInControlProps {
onSubmit?: () => void
onEmailSent?: (email: string) => void
signInButtonText?: string
- onCodeVerified?: (result: { showProfileCompletion?: boolean; code?: string }) => void
+ onCodeVerified?: () => void
+}
+
+enum EmailSigninStep {
+ EnterEmail,
+ EnterName,
+ EnterCode,
}
export const SignInControl: React.FunctionComponent = (props) => {
const fider = useFider()
const [showEmailForm, setShowEmailForm] = useState(fider.session.tenant ? fider.session.tenant.isEmailAuthAllowed : true)
- const [showCodeEntry, setShowCodeEntry] = useState(false)
const [email, setEmail] = useState("")
+ const [emailSignInStep, setEmailSignInStep] = useState(EmailSigninStep.EnterEmail)
+ const [userName, setUserName] = useState("")
const [code, setCode] = useState("")
const [error, setError] = useState(undefined)
const [resendMessage, setResendMessage] = useState("")
- const signInText = props.signInButtonText || i18n._({ id: "action.signin", message: "Sign in" })
-
const forceShowEmailForm = (e: React.MouseEvent) => {
e.preventDefault()
setShowEmailForm(true)
@@ -44,7 +49,8 @@ export const SignInControl: React.FunctionComponent = (props
}
const editEmail = () => {
- setShowCodeEntry(false)
+ setEmailSignInStep(EmailSigninStep.EnterEmail)
+ setUserName("")
setCode("")
setError(undefined)
setResendMessage("")
@@ -55,8 +61,25 @@ export const SignInControl: React.FunctionComponent = (props
const result = await actions.signIn(email)
if (result.ok) {
setError(undefined)
- setShowCodeEntry(true)
- // Don't call onEmailSent - we're showing code entry inline now
+ const data = result.data as { userExists?: boolean } | undefined
+ if (data && data.userExists === false) {
+ // New user - show name field
+ setEmailSignInStep(EmailSigninStep.EnterName)
+ } else {
+ // Existing user - show code entry
+ setEmailSignInStep(EmailSigninStep.EnterCode)
+ }
+ } else if (result.error) {
+ setError(result.error)
+ }
+ }
+
+ const submitNewUser = async () => {
+ doPreSigninAction()
+ const result = await actions.signInNewUser(email, userName)
+ if (result.ok) {
+ setError(undefined)
+ setEmailSignInStep(EmailSigninStep.EnterCode)
} else if (result.error) {
setError(result.error)
}
@@ -65,10 +88,9 @@ export const SignInControl: React.FunctionComponent = (props
const verifyCode = async () => {
const result = await actions.verifySignInCode(email, code)
if (result.ok) {
- const data = result.data as { showProfileCompletion?: boolean } | undefined
if (props.onCodeVerified) {
- // Let the parent component decide what to do, pass the code along
- props.onCodeVerified({ ...data, code })
+ // Let the parent component decide what to do
+ props.onCodeVerified()
} else {
// Default behavior: reload the page
location.reload()
@@ -103,6 +125,30 @@ export const SignInControl: React.FunctionComponent = (props
const providersLen = fider.settings.oauth.length
+ const renderSigninEmailButton = () => {
+ if (emailSignInStep == EmailSigninStep.EnterEmail) {
+ return (
+
+ Continue with Email
+
+ )
+ }
+ if (emailSignInStep == EmailSigninStep.EnterName) {
+ return (
+
+ Sign up
+
+ )
+ }
+ if (emailSignInStep == EmailSigninStep.EnterCode) {
+ return (
+
+ Submit
+
+ )
+ }
+ }
+
if (!isCookieEnabled()) {
return (
@@ -130,71 +176,15 @@ export const SignInControl: React.FunctionComponent = (props
{props.useEmail &&
(showEmailForm ? (
- {!showCodeEntry ? (
-
- ) : (
-
- )}
+
) : (
@@ -211,4 +201,85 @@ export const SignInControl: React.FunctionComponent = (props
))}
)
+
+ function renderNameField() {
+ return (
+
+ )
+ }
+
+ function renderEmailField(): React.ReactNode {
+ return (
+ <>
+
+ {!fider.session.tenant.isEmailAuthAllowed && (
+
+ Currently only allowed to sign in to an administrator account
+
+ )}
+ >
+ )
+ }
+
+ function renderCodeField(): React.ReactNode {
+ return (
+ <>
+
+
+ Please type in the code we just sent to {email}
+ {" "}
+ {
+ e.preventDefault()
+ editEmail()
+ }}
+ >
+ Edit
+
+
+
+ {resendMessage && {resendMessage}
}
+
+ {
+ e.preventDefault()
+ resendCode()
+ }}
+ >
+ Get a new code
+
+
+ >
+ )
+ }
}
diff --git a/public/pages/Home/components/ShareFeedback.tsx b/public/pages/Home/components/ShareFeedback.tsx
index 37e11c186..c13ae6314 100644
--- a/public/pages/Home/components/ShareFeedback.tsx
+++ b/public/pages/Home/components/ShareFeedback.tsx
@@ -200,15 +200,9 @@ export const ShareFeedback: React.FC = (props) => {
}
}
- const onCodeVerified = (result: { showProfileCompletion?: boolean; code?: string }): void => {
- if (result.showProfileCompletion && result.code) {
- // User needs to complete profile - redirect to profile completion page
- // The cached feedback will be preserved for after profile setup
- location.href = `/signin/complete?code=${encodeURIComponent(result.code)}`
- } else {
- // User is authenticated - finalize the feedback submission
- finaliseFeedback()
- }
+ const onCodeVerified = (): void => {
+ // User is authenticated - finalize the feedback submission
+ finaliseFeedback()
}
const handleEditorFocus = () => {
diff --git a/public/pages/SignIn/SignIn.page.tsx b/public/pages/SignIn/SignIn.page.tsx
index e13fdc711..6b54aa784 100644
--- a/public/pages/SignIn/SignIn.page.tsx
+++ b/public/pages/SignIn/SignIn.page.tsx
@@ -34,18 +34,13 @@ const Private = (): JSX.Element => {
export const SignInPage = () => {
const fider = useFider()
- const onCodeVerified = (result: { showProfileCompletion?: boolean; code?: string }) => {
- if (result.showProfileCompletion && result.code) {
- // User needs to complete profile - redirect to profile completion page
- location.href = `/signin/complete?code=${encodeURIComponent(result.code)}`
+ const onCodeVerified = () => {
+ // User is authenticated - redirect to the appropriate URL
+ const redirect = new URLSearchParams(window.location.search).get("redirect")
+ if (redirect && redirect.startsWith("/")) {
+ location.href = fider.settings.baseURL + redirect
} else {
- // User is authenticated - redirect to the appropriate URL
- const redirect = new URLSearchParams(window.location.search).get("redirect")
- if (redirect && redirect.startsWith("/")) {
- location.href = fider.settings.baseURL + redirect
- } else {
- location.href = fider.settings.baseURL
- }
+ location.href = fider.settings.baseURL
}
}
diff --git a/public/services/actions/tenant.ts b/public/services/actions/tenant.ts
index fbbbf3578..58e5d62d9 100644
--- a/public/services/actions/tenant.ts
+++ b/public/services/actions/tenant.ts
@@ -58,6 +58,10 @@ export const signIn = async (email: string): Promise => {
return await http.post("/_api/signin", { email })
}
+export const signInNewUser = async (email: string, name: string): Promise => {
+ return await http.post("/_api/signin/newuser", { email, name })
+}
+
export const verifySignInCode = async (email: string, code: string): Promise => {
return await http.post("/_api/signin/verify", { email, code })
}
From 8f78e39fcc4d3e264cc001fa184487322e628e59 Mon Sep 17 00:00:00 2001
From: Matt Roberts
Date: Wed, 12 Nov 2025 09:08:30 +0000
Subject: [PATCH 04/11] Fixes to the sign in flow for new users.
---
app/handlers/signin.go | 46 ++++++++++++--------
app/handlers/signin_test.go | 50 ++++++++++++++++++++++
locale/ar/client.json | 1 +
locale/de/client.json | 1 +
locale/el/client.json | 1 +
locale/es-ES/client.json | 1 +
locale/fa/client.json | 1 +
locale/fr/client.json | 1 +
locale/it/client.json | 2 +-
locale/ja/client.json | 1 +
locale/nl/client.json | 1 +
locale/pl/client.json | 1 +
locale/pt-BR/client.json | 1 +
locale/ru/client.json | 1 +
locale/sk/client.json | 2 +-
locale/sv-SE/client.json | 1 +
locale/tr/client.json | 1 +
locale/zh-CN/client.json | 1 +
public/components/common/SignInControl.tsx | 4 +-
19 files changed, 97 insertions(+), 21 deletions(-)
diff --git a/app/handlers/signin.go b/app/handlers/signin.go
index 4ad1bb2f6..df03aa133 100644
--- a/app/handlers/signin.go
+++ b/app/handlers/signin.go
@@ -286,26 +286,38 @@ func VerifySignInKey(kind enum.EmailVerificationKind) web.HandlerFunc {
return NotInvitedPage()(c)
}
- // User should already have entered their name so we can get them registered.
- user := &entity.User{
- Name: result.Name,
- Email: result.Email,
- Tenant: c.Tenant(),
- Role: enum.RoleVisitor,
- }
- err = bus.Dispatch(c, &cmd.RegisterUser{User: user})
- if err != nil {
- return c.Failure(err)
- }
+ // If name is provided in verification, create user account immediately
+ if result.Name != "" {
+ user := &entity.User{
+ Name: result.Name,
+ Email: result.Email,
+ Tenant: c.Tenant(),
+ Role: enum.RoleVisitor,
+ }
+ err = bus.Dispatch(c, &cmd.RegisterUser{User: user})
+ if err != nil {
+ return c.Failure(err)
+ }
- err = bus.Dispatch(c, &cmd.SetKeyAsVerified{Key: key})
- if err != nil {
- return c.Failure(err)
+ err = bus.Dispatch(c, &cmd.SetKeyAsVerified{Key: key})
+ if err != nil {
+ return c.Failure(err)
+ }
+
+ webutil.AddAuthUserCookie(c, user)
+ baseURL := c.BaseURL()
+ return c.Redirect(baseURL)
}
- webutil.AddAuthUserCookie(c, user)
- baseURL := c.BaseURL()
- return c.Redirect(baseURL)
+ // Otherwise, show profile completion page
+ return c.Page(http.StatusOK, web.Props{
+ Page: "SignIn/CompleteSignInProfile.page",
+ Title: "Complete Sign In Profile",
+ Data: web.Map{
+ "kind": kind,
+ "k": key,
+ },
+ })
}
return c.Failure(err)
diff --git a/app/handlers/signin_test.go b/app/handlers/signin_test.go
index 88f1fcdd9..f00ea8d15 100644
--- a/app/handlers/signin_test.go
+++ b/app/handlers/signin_test.go
@@ -357,6 +357,56 @@ func TestVerifySignInKeyHandler_CorrectKey_NewUser(t *testing.T) {
Expect(code).Equals(http.StatusOK)
}
+func TestVerifySignInKeyHandler_CorrectKey_NewUser_WithName(t *testing.T) {
+ RegisterT(t)
+
+ bus.AddHandler(func(ctx context.Context, q *query.GetUserByEmail) error {
+ return app.ErrNotFound
+ })
+
+ key := "1234567890"
+ bus.AddHandler(func(ctx context.Context, q *query.GetVerificationByKey) error {
+ if q.Key == key && q.Kind == enum.EmailVerificationKindSignIn {
+ expiresAt := time.Now().Add(5 * time.Minute)
+ q.Result = &entity.EmailVerification{
+ Key: q.Key,
+ Kind: q.Kind,
+ ExpiresAt: expiresAt,
+ Email: "hot.pie@got.com",
+ Name: "Hot Pie",
+ }
+ return nil
+ }
+ return app.ErrNotFound
+ })
+
+ var registeredUser *entity.User
+ bus.AddHandler(func(ctx context.Context, c *cmd.RegisterUser) error {
+ registeredUser = c.User
+ registeredUser.ID = 1
+ Expect(c.User.Name).Equals("Hot Pie")
+ Expect(c.User.Email).Equals("hot.pie@got.com")
+ Expect(c.User.Role).Equals(enum.RoleVisitor)
+ return nil
+ })
+
+ bus.AddHandler(func(ctx context.Context, c *cmd.SetKeyAsVerified) error {
+ return nil
+ })
+
+ server := mock.NewServer()
+
+ code, response := server.
+ OnTenant(mock.DemoTenant).
+ WithURL("http://demo.test.fider.io/signin/verify?k=" + key).
+ Execute(handlers.VerifySignInKey(enum.EmailVerificationKindSignIn))
+
+ Expect(code).Equals(http.StatusTemporaryRedirect)
+ Expect(response.Header().Get("Location")).Equals("http://demo.test.fider.io")
+ Expect(registeredUser).IsNotNil()
+ ExpectFiderAuthCookie(response, registeredUser)
+}
+
func TestVerifySignInKeyHandler_PrivateTenant_SignInRequest_NonInviteNewUser(t *testing.T) {
RegisterT(t)
diff --git a/locale/ar/client.json b/locale/ar/client.json
index e6733320f..638761796 100644
--- a/locale/ar/client.json
+++ b/locale/ar/client.json
@@ -14,6 +14,7 @@
"action.respond": "رد",
"action.save": "احفظ",
"action.signin": "تسجيل الدخول",
+ "action.signup": "",
"action.submit": "إرسال",
"action.vote": "صوت لهذه الفكرة",
"action.voted": "تم التصويت!",
diff --git a/locale/de/client.json b/locale/de/client.json
index f03326c46..a2d68bbe7 100644
--- a/locale/de/client.json
+++ b/locale/de/client.json
@@ -14,6 +14,7 @@
"action.respond": "Antworten",
"action.save": "Sichern",
"action.signin": "Anmelden",
+ "action.signup": "",
"action.submit": "Absenden",
"action.vote": "Abstimmen",
"action.voted": "Abgestimmt!",
diff --git a/locale/el/client.json b/locale/el/client.json
index deeb85ea0..f6f90cdba 100644
--- a/locale/el/client.json
+++ b/locale/el/client.json
@@ -14,6 +14,7 @@
"action.respond": "Απάντηση",
"action.save": "Αποθήκευση",
"action.signin": "Είσοδος",
+ "action.signup": "",
"action.submit": "Υποβολή",
"action.vote": "Ψηφίστε αυτήν την ιδέα",
"action.voted": "Ψηφίστηκε!",
diff --git a/locale/es-ES/client.json b/locale/es-ES/client.json
index e87d9551a..2ae282314 100644
--- a/locale/es-ES/client.json
+++ b/locale/es-ES/client.json
@@ -14,6 +14,7 @@
"action.respond": "Responder",
"action.save": "Guardar",
"action.signin": "Iniciar sesión",
+ "action.signup": "",
"action.submit": "Enviar",
"action.vote": "Vota por esta idea",
"action.voted": "¡Votado!",
diff --git a/locale/fa/client.json b/locale/fa/client.json
index c00703d2d..f8e6848ca 100644
--- a/locale/fa/client.json
+++ b/locale/fa/client.json
@@ -14,6 +14,7 @@
"action.respond": "پاسخ",
"action.save": "ذخیره",
"action.signin": "ورود",
+ "action.signup": "",
"action.submit": "ارسال",
"action.vote": "به این ایده رأی دهید",
"action.voted": "رأی داده شد!",
diff --git a/locale/fr/client.json b/locale/fr/client.json
index b971a41ab..a1dbc55f7 100644
--- a/locale/fr/client.json
+++ b/locale/fr/client.json
@@ -14,6 +14,7 @@
"action.respond": "Répondre",
"action.save": "Enregistrer",
"action.signin": "Se connecter",
+ "action.signup": "",
"action.submit": "Valider",
"action.vote": "Voter pour cette idée",
"action.voted": "Votée !",
diff --git a/locale/it/client.json b/locale/it/client.json
index 2c998ad41..0c54c9b27 100644
--- a/locale/it/client.json
+++ b/locale/it/client.json
@@ -14,6 +14,7 @@
"action.respond": "Rispondi",
"action.save": "Salva",
"action.signin": "Accedi",
+ "action.signup": "",
"action.submit": "Invia",
"action.vote": "Vota questa idea",
"action.voted": "Votato!",
@@ -218,7 +219,6 @@
"signin.message.private.text": "Se si dispone di un account o di un invito, è possibile utilizzare le seguenti opzioni per accedere.",
"signin.message.private.title": "<0>{0}0> è uno spazio privato, è necessario registrarsi per partecipare e votare.",
"signin.message.socialbutton.intro": "Accedi con",
- "
"signin.name.placeholder": "",
"validation.custom.maxattachments": "Sono consentiti al massimo {number} allegati.",
"validation.custom.maximagesize": "La dimensione dell'immagine deve essere inferiore a {kilobytes}KB.",
diff --git a/locale/ja/client.json b/locale/ja/client.json
index b8635ab28..e86e3765e 100644
--- a/locale/ja/client.json
+++ b/locale/ja/client.json
@@ -14,6 +14,7 @@
"action.respond": "回答",
"action.save": "保存",
"action.signin": "ログイン",
+ "action.signup": "",
"action.submit": "送信",
"action.vote": "このアイデアに投票",
"action.voted": "投票完了!",
diff --git a/locale/nl/client.json b/locale/nl/client.json
index 327aeb96d..106b044d2 100644
--- a/locale/nl/client.json
+++ b/locale/nl/client.json
@@ -14,6 +14,7 @@
"action.respond": "Reageren",
"action.save": "Opslaan",
"action.signin": "Inloggen",
+ "action.signup": "",
"action.submit": "Verzenden",
"action.vote": "Stem op dit idee",
"action.voted": "Gestemd!",
diff --git a/locale/pl/client.json b/locale/pl/client.json
index 1ea0df213..4d8a50183 100644
--- a/locale/pl/client.json
+++ b/locale/pl/client.json
@@ -14,6 +14,7 @@
"action.respond": "Odpowiedz",
"action.save": "Zapisz",
"action.signin": "Zaloguj się",
+ "action.signup": "",
"action.submit": "Prześlij",
"action.vote": "Zagłosuj na ten pomysł",
"action.voted": "Zagłosowane!",
diff --git a/locale/pt-BR/client.json b/locale/pt-BR/client.json
index a8440a802..00954f204 100644
--- a/locale/pt-BR/client.json
+++ b/locale/pt-BR/client.json
@@ -14,6 +14,7 @@
"action.respond": "Responder",
"action.save": "Salvar",
"action.signin": "Iniciar sessão",
+ "action.signup": "",
"action.submit": "Enviar",
"action.vote": "Votar",
"action.voted": "Votado",
diff --git a/locale/ru/client.json b/locale/ru/client.json
index 6bd38280f..321256ccc 100644
--- a/locale/ru/client.json
+++ b/locale/ru/client.json
@@ -14,6 +14,7 @@
"action.respond": "Ответить",
"action.save": "Сохранить",
"action.signin": "Войти",
+ "action.signup": "",
"action.submit": "Продолжить",
"action.vote": "Проголосуйте за эту идею",
"action.voted": "Проголосовал!",
diff --git a/locale/sk/client.json b/locale/sk/client.json
index 33572a28d..6bdf3fc4f 100644
--- a/locale/sk/client.json
+++ b/locale/sk/client.json
@@ -14,6 +14,7 @@
"action.respond": "Odpovedať",
"action.save": "Uložiť",
"action.signin": "Prihlásiť sa",
+ "action.signup": "",
"action.submit": "Potvrdiť",
"action.vote": "Hlasovať za tento nápad",
"action.voted": "Zahlasované!",
@@ -218,7 +219,6 @@
"signin.message.private.text": "Ak máte účet alebo pozvánku, na prihlásenie môžete použiť nasledujúce možnosti.",
"signin.message.private.title": "<0>{0}0> je súkromný priestor, ak sa chcete zúčastniť diskusie a hlasovať, musíte sa prihlásiť.",
"signin.message.socialbutton.intro": "Prihlásiť sa pomocou",
- "
"signin.name.placeholder": "",
"validation.custom.maxattachments": "Maximálny počet príloh je {number}.",
"validation.custom.maximagesize": "Veľkosť obrázka musí byť menšia ako {kilobytes}KB.",
diff --git a/locale/sv-SE/client.json b/locale/sv-SE/client.json
index 16c827cdc..752f83f4e 100644
--- a/locale/sv-SE/client.json
+++ b/locale/sv-SE/client.json
@@ -14,6 +14,7 @@
"action.respond": "Svara",
"action.save": "Spara",
"action.signin": "Logga in",
+ "action.signup": "",
"action.submit": "Skicka",
"action.vote": "Rösta på den här idén",
"action.voted": "Röstade!",
diff --git a/locale/tr/client.json b/locale/tr/client.json
index c3e0fe467..fd6796b63 100644
--- a/locale/tr/client.json
+++ b/locale/tr/client.json
@@ -14,6 +14,7 @@
"action.respond": "Yanıtla",
"action.save": "Kaydet",
"action.signin": "Giriş Yap",
+ "action.signup": "",
"action.submit": "Gönder",
"action.vote": "Bu fikre oy verin",
"action.voted": "Oy verildi!",
diff --git a/locale/zh-CN/client.json b/locale/zh-CN/client.json
index 30f96f080..73740a6b4 100644
--- a/locale/zh-CN/client.json
+++ b/locale/zh-CN/client.json
@@ -14,6 +14,7 @@
"action.respond": "回复/标记",
"action.save": "保存",
"action.signin": "登录",
+ "action.signup": "",
"action.submit": "提交",
"action.vote": "投票支持这个想法",
"action.voted": "已投票!",
diff --git a/public/components/common/SignInControl.tsx b/public/components/common/SignInControl.tsx
index 894e458e7..b0b551ad8 100644
--- a/public/components/common/SignInControl.tsx
+++ b/public/components/common/SignInControl.tsx
@@ -240,7 +240,7 @@ export const SignInControl: React.FunctionComponent = (props
function renderCodeField(): React.ReactNode {
return (
<>
-
+
Please type in the code we just sent to {email}
{" "}
@@ -267,7 +267,7 @@ export const SignInControl: React.FunctionComponent = (props
/>
{resendMessage && {resendMessage}
}
-
+
Date: Thu, 13 Nov 2025 15:09:51 +0000
Subject: [PATCH 05/11] Fixed the login tests
---
Makefile | 9 +++++++++
e2e/setup.ts | 4 ++--
e2e/step_definitions/home.steps.ts | 10 +++++-----
e2e/step_definitions/user.steps.ts | 6 +++---
4 files changed, 19 insertions(+), 10 deletions(-)
diff --git a/Makefile b/Makefile
index 2be0e4d7a..63591484f 100644
--- a/Makefile
+++ b/Makefile
@@ -59,6 +59,15 @@ test-e2e-server: ## Run all E2E tests
test-e2e-ui: ## Run all E2E tests
npx cucumber-js e2e/features/ui/**/*.feature --require-module ts-node/register --require 'e2e/**/*.ts' --publish-quiet
+test-e2e-ui-headed: ## Run all E2E tests with visible browser
+ HEADED=true npx cucumber-js e2e/features/ui/**/*.feature --require-module ts-node/register --require 'e2e/**/*.ts' --publish-quiet
+
+test-e2e-ui-scenario: ## Run specific E2E test scenario (use NAME="scenario name")
+ npx cucumber-js e2e/features/ui/**/*.feature --require-module ts-node/register --require 'e2e/**/*.ts' --publish-quiet --name "$(NAME)"
+
+test-e2e-ui-scenario-headed: ## Run specific E2E test scenario with visible browser (use NAME="scenario name")
+ HEADED=true npx cucumber-js e2e/features/ui/**/*.feature --require-module ts-node/register --require 'e2e/**/*.ts' --publish-quiet --name "$(NAME)"
+
##@ Running (Watch Mode)
diff --git a/e2e/setup.ts b/e2e/setup.ts
index 4ff69bc9d..a14f4e767 100644
--- a/e2e/setup.ts
+++ b/e2e/setup.ts
@@ -11,8 +11,8 @@ type BrowserName = "chromium" | "firefox" | "webkit"
BeforeAll({ timeout: 30 * 1000 }, async function () {
const name = (process.env.BROWSER || "chromium") as BrowserName
browser = await playwright[name].launch({
- headless: true,
- slowMo: 10,
+ headless: process.env.HEADED !== "true",
+ slowMo: process.env.HEADED === "true" ? 100 : 10,
})
if (!tenantName) {
diff --git a/e2e/step_definitions/home.steps.ts b/e2e/step_definitions/home.steps.ts
index e03eca7c0..ac5f6dead 100644
--- a/e2e/step_definitions/home.steps.ts
+++ b/e2e/step_definitions/home.steps.ts
@@ -52,7 +52,7 @@ Then("I should see the name field", async function (this: FiderWorld) {
})
Given("I click continue", async function () {
- await this.page.click("button[type='submit']")
+ await this.page.getByRole("button", { name: "Sign up" }).click()
})
Given("I click submit your feedback", async function () {
@@ -72,8 +72,8 @@ Given("I enter the confirmation code", async function (this: FiderWorld) {
const code = await getLatestCodeSentTo(userEmail)
// Enter the code in the UI
- await this.page.type("#input-code", code)
- await this.page.click("button[type='submit']")
+ await this.page.fill("#input-code", code)
+ await this.page.getByRole("button", { name: "submit" }).click()
// Wait for navigation after successful code verification
await this.page.waitForLoadState("networkidle")
@@ -85,11 +85,11 @@ Then("I should see the new post modal", async function (this: FiderWorld) {
})
Given("I enter my name as {string}", async function (this: FiderWorld, name: string) {
- await this.page.type("#input-name", name)
+ await this.page.fill("#input-name", name)
})
Given("I click submit", async function () {
- await this.page.click("button[type='submit']")
+ await this.page.getByRole("button", { name: "submit" }).click()
})
Then("I should see {string} as the draft post title", async function (this: FiderWorld, title: string) {
diff --git a/e2e/step_definitions/user.steps.ts b/e2e/step_definitions/user.steps.ts
index 7efe85103..83469fce2 100644
--- a/e2e/step_definitions/user.steps.ts
+++ b/e2e/step_definitions/user.steps.ts
@@ -14,13 +14,13 @@ Given("I sign in as {string}", async function (this: FiderWorld, userName: strin
const userEmail = `${userName}-${this.tenantName}@fider.io`
await this.page.click(".c-menu .uppercase.text-sm")
- await this.page.type(".c-signin-control #input-email", userEmail)
+ await this.page.fill(".c-signin-control #input-email", userEmail)
await this.page.click(".c-signin-control .c-button--primary")
// Get the code from email and enter it
const code = await getLatestCodeSentTo(userEmail)
- await this.page.type("#input-code", code)
- await this.page.click("button[type='submit']")
+ await this.page.fill("#input-code", code)
+ await this.page.getByRole("button", { name: "submit" }).click()
// Wait for navigation after successful code verification
await this.page.waitForLoadState("networkidle")
From bb6e5729566a25403f58a0823a01e86402deb6d4 Mon Sep 17 00:00:00 2001
From: Matt Roberts
Date: Fri, 14 Nov 2025 11:06:21 +0000
Subject: [PATCH 06/11] Put the code in the email subject
---
locale/ar/server.json | 2 +-
locale/cs/server.json | 2 +-
locale/de/server.json | 2 +-
locale/el/server.json | 2 +-
locale/en/server.json | 2 +-
locale/es-ES/server.json | 2 +-
locale/fa/server.json | 2 +-
locale/fr/server.json | 2 +-
locale/it/server.json | 2 +-
locale/ja/server.json | 2 +-
locale/ko/server.json | 2 +-
locale/nl/server.json | 2 +-
locale/pl/server.json | 2 +-
locale/pt-BR/server.json | 2 +-
locale/ru/server.json | 2 +-
locale/si-LK/server.json | 2 +-
locale/sk/server.json | 2 +-
locale/sv-SE/server.json | 2 +-
locale/tr/server.json | 2 +-
locale/zh-CN/server.json | 2 +-
views/email/signin_email.html | 2 +-
21 files changed, 21 insertions(+), 21 deletions(-)
diff --git a/locale/ar/server.json b/locale/ar/server.json
index fb0e977bc..a43e4bd97 100644
--- a/locale/ar/server.json
+++ b/locale/ar/server.json
@@ -52,7 +52,7 @@
"email.delete_post.text": "{title} تم حذفه .",
"email.new_comment.text": "{userName} ترك تعليقًا على {title} ({postLink}) .",
"email.new_post.text": "{userName} أنشأ منشورًا جديدًا {title} ({postLink}) .",
- "email.signin_email.subject": "سجل الدخول إلى {siteName}",
+ "email.signin_email.subject": "رمز تسجيل الدخول إلى {siteName} هو {code}",
"email.signin_email.text": "لقد طلبت منا أن نرسل لك رابط تسجيل الدخول وهنا هو كذلك.",
"email.signin_email.confirmation": "انقر على الرابط أدناه لتسجيل الدخول إلى {siteName} .",
"email.signup_email.subject": "موقع فايـدر الجديد الخاص بك",
diff --git a/locale/cs/server.json b/locale/cs/server.json
index b6f22f20a..2dfb812a2 100644
--- a/locale/cs/server.json
+++ b/locale/cs/server.json
@@ -52,7 +52,7 @@
"email.delete_post.text": "Soubor {title} byl smazán .",
"email.new_comment.text": "{userName} zanechal(a) komentář k příspěvku {title} ({postLink}) .",
"email.new_post.text": "{userName} vytvořil(a) nový příspěvek {title} ({postLink}) .",
- "email.signin_email.subject": "Přihlaste se do {siteName}",
+ "email.signin_email.subject": "Váš přihlašovací kód pro {siteName} je {code}",
"email.signin_email.text": "Požádali jste nás o zaslání přihlašovacího odkazu a tady je.",
"email.signin_email.confirmation": "Klikněte na odkaz níže a přihlaste se do {siteName} .",
"email.signup_email.subject": "Vaše nové stránky Fideru",
diff --git a/locale/de/server.json b/locale/de/server.json
index b9c73d8d5..bbf520042 100644
--- a/locale/de/server.json
+++ b/locale/de/server.json
@@ -52,7 +52,7 @@
"email.delete_post.text": "{title} wurde gelöscht .",
"email.new_comment.text": "{userName} hinterliess einen Kommentar auf {title} ({postLink}) .",
"email.new_post.text": "{userName} hat einen neuen Beitrag {title} ({postLink}) erstellt.",
- "email.signin_email.subject": "Auf {siteName} registrieren",
+ "email.signin_email.subject": "Ihr Anmeldecode für {siteName} ist {code}",
"email.signin_email.text": "Du hast uns gebeten, dir einen Login-Link zu schicken, und das hier ist er.",
"email.signin_email.confirmation": "Klick auf den Link unten, um dich bei {siteName} anzumelden.",
"email.signup_email.subject": "Deine neue Fider-Seite",
diff --git a/locale/el/server.json b/locale/el/server.json
index 4fd94a43e..21d3693aa 100644
--- a/locale/el/server.json
+++ b/locale/el/server.json
@@ -52,7 +52,7 @@
"email.delete_post.text": "{title} έχει διαγραφεί .",
"email.new_comment.text": "{userName} άφησε ένα σχόλιο σχετικά με {title} ({postLink}) .",
"email.new_post.text": "{userName} δημιούργησε μια νέα ανάρτηση {title} ({postLink}) .",
- "email.signin_email.subject": "Συνδεθείτε στο {siteName}",
+ "email.signin_email.subject": "Ο κωδικός σύνδεσής σας για το {siteName} είναι {code}",
"email.signin_email.text": "Μας ζητήσατε να σας στείλουμε έναν σύνδεσμο σύνδεσης και εδώ είναι.",
"email.signin_email.confirmation": "Κάντε κλικ στον παρακάτω σύνδεσμο για να συνδεθείτε στο {siteName} .",
"email.signup_email.subject": "Το νέο σας Fider site",
diff --git a/locale/en/server.json b/locale/en/server.json
index 7bdb0afd7..e2bd144eb 100644
--- a/locale/en/server.json
+++ b/locale/en/server.json
@@ -52,7 +52,7 @@
"email.delete_post.text": "{title} has been deleted .",
"email.new_comment.text": "{userName} left a comment on {title} ({postLink}) .",
"email.new_post.text": "{userName} created a new post {title} ({postLink}) .",
- "email.signin_email.subject": "Sign in to {siteName}",
+ "email.signin_email.subject": "Your sign in code for {siteName} is {code}",
"email.signin_email.text": "Here is your sign-in code.",
"email.signin_email.confirmation": "Use the one-time code below to sign in to {siteName} .",
"email.signup_email.subject": "Your new Fider site",
diff --git a/locale/es-ES/server.json b/locale/es-ES/server.json
index 41ca3cc77..4b318486d 100644
--- a/locale/es-ES/server.json
+++ b/locale/es-ES/server.json
@@ -52,7 +52,7 @@
"email.delete_post.text": "{title} ha sido eliminado .",
"email.new_comment.text": "{userName} dejó un comentario en {title} ({postLink}) .",
"email.new_post.text": "{userName} dejó un comentario en {title} ({postLink}) .",
- "email.signin_email.subject": "Iniciar sesión en {siteName}",
+ "email.signin_email.subject": "Su código de inicio de sesión para {siteName} es {code}",
"email.signin_email.text": "Nos ha pedido que le enviemos un enlace de inicio de sesión y aquí está.",
"email.signin_email.confirmation": "Haz clic en el siguiente enlace para iniciar sesión en {siteName} .",
"email.signup_email.subject": "Tu nuevo sitio de Fider",
diff --git a/locale/fa/server.json b/locale/fa/server.json
index 5880dc086..3defc857d 100644
--- a/locale/fa/server.json
+++ b/locale/fa/server.json
@@ -52,7 +52,7 @@
"email.delete_post.text": "{title} حذف شد.",
"email.new_comment.text": "{userName} در {title} ({postLink}) نظری گذاشت.",
"email.new_post.text": "{userName} پست جدیدی ایجاد کرد {title} ({postLink}) .",
- "email.signin_email.subject": "ورود به {siteName}",
+ "email.signin_email.subject": "کد ورود شما برای {siteName} این است: {code}",
"email.signin_email.text": "شما درخواست کردید لینکی برای ورود ارسال کنیم و اینک در اختیارتان است.",
"email.signin_email.confirmation": "برای ورود به {siteName} روی لینک زیر کلیک کنید.",
"email.signup_email.subject": "سایت Fider جدید شما",
diff --git a/locale/fr/server.json b/locale/fr/server.json
index 9770ee255..6794bf316 100644
--- a/locale/fr/server.json
+++ b/locale/fr/server.json
@@ -52,7 +52,7 @@
"email.delete_post.text": "{title} a été supprimé .",
"email.new_comment.text": "{userName} a laissé un commentaire sur {title} ({postLink}) .",
"email.new_post.text": "{userName} a créé un nouveau message {title} ({postLink}) .",
- "email.signin_email.subject": "Se connecter à {siteName}",
+ "email.signin_email.subject": "Votre code de connexion pour {siteName} est {code}",
"email.signin_email.text": "Vous nous avez demandé de vous envoyer un lien de connexion et le voici.",
"email.signin_email.confirmation": "Cliquez sur le lien ci-dessous pour vous connecter à {siteName} .",
"email.signup_email.subject": "Votre nouveau site Fider",
diff --git a/locale/it/server.json b/locale/it/server.json
index 3d745ab62..8df7b5d38 100644
--- a/locale/it/server.json
+++ b/locale/it/server.json
@@ -52,7 +52,7 @@
"email.delete_post.text": "{title} è stato cancellato .",
"email.new_comment.text": "{userName} ha lasciato un commento su {title} ({postLink}) .",
"email.new_post.text": "{userName} ha creato un nuovo post {title} ({postLink}) .",
- "email.signin_email.subject": "Accedi a {siteName}",
+ "email.signin_email.subject": "Il tuo codice di accesso per {siteName} è {code}",
"email.signin_email.text": "Ci hai chiesto d'inviarti un link di accesso ed è qui.",
"email.signin_email.confirmation": "Clicca sul link qui sotto per accedere a {siteName} .",
"email.signup_email.subject": "Il tuo nuovo sito Fider",
diff --git a/locale/ja/server.json b/locale/ja/server.json
index 855ad5709..5dc35d560 100644
--- a/locale/ja/server.json
+++ b/locale/ja/server.json
@@ -52,7 +52,7 @@
"email.delete_post.text": "{title} は 削除されました 。",
"email.new_comment.text": "{userName} さんが {title} ({postLink}) にコメントを残しました。",
"email.new_post.text": "{userName} が新しい投稿 {title} ({postLink}) を作成しました。",
- "email.signin_email.subject": "{siteName} にログイン",
+ "email.signin_email.subject": "{siteName} のサインインコードは {code} です",
"email.signin_email.text": "ご要望のサインインリンクはこれです。",
"email.signin_email.confirmation": "{siteName} にサインインするには、下のリンクをクリックしてください。",
"email.signup_email.subject": "あなたの新しい Fider サイト",
diff --git a/locale/ko/server.json b/locale/ko/server.json
index d60f2a2f3..5e2403118 100644
--- a/locale/ko/server.json
+++ b/locale/ko/server.json
@@ -52,7 +52,7 @@
"email.delete_post.text": "{title} 이 삭제되었습니다 .",
"email.new_comment.text": "{userName} 님이 {title} ({postLink}) 에 댓글을 남겼습니다.",
"email.new_post.text": "{userName} 님이 새 게시물 {title} ({postLink}) 을 만들었습니다.",
- "email.signin_email.subject": "{siteName}에 로그인하세요",
+ "email.signin_email.subject": "{siteName}의 로그인 코드는 {code}입니다",
"email.signin_email.text": "귀하께서 로그인 링크를 보내달라고 요청하셨고, 여기에 링크가 있습니다.",
"email.signin_email.confirmation": "아래 링크를 클릭하여 {siteName} 에 로그인하세요.",
"email.signup_email.subject": "새로운 Fider 사이트",
diff --git a/locale/nl/server.json b/locale/nl/server.json
index 1593a9695..5fa4678de 100644
--- a/locale/nl/server.json
+++ b/locale/nl/server.json
@@ -52,7 +52,7 @@
"email.delete_post.text": "{title} is verwijderd .",
"email.new_comment.text": "{userName} heeft een reactie achtergelaten op {title} ({postLink}) .",
"email.new_post.text": "{userName} heeft een nieuw bericht gemaakt {title} ({postLink}) .",
- "email.signin_email.subject": "Log in bij {siteName}",
+ "email.signin_email.subject": "Uw inlogcode voor {siteName} is {code}",
"email.signin_email.text": "Je hebt ons gevraagd om een link om in te loggen en hier is-ie dan.",
"email.signin_email.confirmation": "Klik op de link hieronder om in te loggen op {siteName} .",
"email.signup_email.subject": "Jouw nieuwe Fider-site",
diff --git a/locale/pl/server.json b/locale/pl/server.json
index f5cbea568..f6d81bbd3 100644
--- a/locale/pl/server.json
+++ b/locale/pl/server.json
@@ -52,7 +52,7 @@
"email.delete_post.text": "{title} został usunięty .",
"email.new_comment.text": "{userName} skomentował {title} ({postLink}) .",
"email.new_post.text": "{userName} opublikował post {title} ({postLink}) .",
- "email.signin_email.subject": "Zaloguj się do {siteName}",
+ "email.signin_email.subject": "Twój kod logowania do {siteName} to {code}",
"email.signin_email.text": "Poprosiłeś o wysłanie linku do logowania i oto on.",
"email.signin_email.confirmation": "Kliknij poniższy link, aby zalogować się do {siteName} .",
"email.signup_email.subject": "Twój nowy Fider",
diff --git a/locale/pt-BR/server.json b/locale/pt-BR/server.json
index d51a7e341..226f459f7 100644
--- a/locale/pt-BR/server.json
+++ b/locale/pt-BR/server.json
@@ -52,7 +52,7 @@
"email.delete_post.text": "{title} foi removido .",
"email.new_comment.text": "{userName} deixou um comentário em {title} ({postLink}) .",
"email.new_post.text": "{userName} criou uma nova postagem {title} ({postLink}) .",
- "email.signin_email.subject": "Seu link de acesso a {siteName}",
+ "email.signin_email.subject": "Seu código de acesso para {siteName} é {code}",
"email.signin_email.text": "Você solicitou um link para login e aqui está.",
"email.signin_email.confirmation": "Clique no link abaixo para fazer login em {siteName} .",
"email.signup_email.subject": "Seu novo site no Fider",
diff --git a/locale/ru/server.json b/locale/ru/server.json
index bb34f86b3..082424a53 100644
--- a/locale/ru/server.json
+++ b/locale/ru/server.json
@@ -52,7 +52,7 @@
"email.delete_post.text": "Пост {title} был удалён .",
"email.new_comment.text": "{userName} оставил комментарий на {title} ({postLink}) .",
"email.new_post.text": "{userName} создал новый пост: {title} ({postLink}) .",
- "email.signin_email.subject": "Вход на {siteName}",
+ "email.signin_email.subject": "Ваш код входа для {siteName}: {code}",
"email.signin_email.text": "Вы попросили отправить ссылку для входа, и вот она.",
"email.signin_email.confirmation": "Перейдите по ссылке ниже, чтобы войти на {siteName} .",
"email.signup_email.subject": "Ваш новый сайт с Fider",
diff --git a/locale/si-LK/server.json b/locale/si-LK/server.json
index a2244335a..09169d9fa 100644
--- a/locale/si-LK/server.json
+++ b/locale/si-LK/server.json
@@ -52,7 +52,7 @@
"email.delete_post.text": "{title} මකා දමා ඇත.",
"email.new_comment.text": "{userName} {title} ({postLink}) හි අදහසක් දැක්වීය.",
"email.new_post.text": "{userName} නව සටහනක් නිර්මාණය කළා {title} ({postLink}) .",
- "email.signin_email.subject": "{siteName} වෙත පුරනය වන්න",
+ "email.signin_email.subject": "{siteName} සඳහා ඔබගේ පිවිසුම් කේතය {code} වේ",
"email.signin_email.text": "ඔබ අපෙන් ඔබට පුරනය වීමේ සබැඳියක් එවන ලෙස ඉල්ලා සිටියා, මෙන්න එය.",
"email.signin_email.confirmation": "{siteName} වෙත පුරනය වීමට පහත සබැඳිය ක්ලික් කරන්න.",
"email.signup_email.subject": "ඔබේ නව ෆයිඩර් අඩවිය",
diff --git a/locale/sk/server.json b/locale/sk/server.json
index 0fe43caa2..83f22f4fc 100644
--- a/locale/sk/server.json
+++ b/locale/sk/server.json
@@ -52,7 +52,7 @@
"email.delete_post.text": "Príspevok {title} bol vymazaný .",
"email.new_comment.text": "{userName} zanechal komentár na príspevku {title} ({postLink}) .",
"email.new_post.text": "{userName} vytvoril nový príspevok {title} ({postLink}) .",
- "email.signin_email.subject": "Prihláste sa do {siteName}",
+ "email.signin_email.subject": "Váš prihlasovací kód pre {siteName} je {code}",
"email.signin_email.text": "Požiadali ste nás, aby sme vám poslali odkaz na prihlásenie, a tu je.",
"email.signin_email.confirmation": "Prihláste sa kliknutím na odkaz nižšie {siteName} .",
"email.signup_email.subject": "Vaša nová stránka Fider",
diff --git a/locale/sv-SE/server.json b/locale/sv-SE/server.json
index 0eb7253f4..8febef244 100644
--- a/locale/sv-SE/server.json
+++ b/locale/sv-SE/server.json
@@ -52,7 +52,7 @@
"email.delete_post.text": "{title} har raderats .",
"email.new_comment.text": "{userName} skrev en kommentar på {title} ({postLink}) .",
"email.new_post.text": "{userName} skapade ett nytt inlägg {title} ({postLink}) .",
- "email.signin_email.subject": "Logga in på {siteName}",
+ "email.signin_email.subject": "Din inloggningskod för {siteName} är {code}",
"email.signin_email.text": "Du bad oss att skicka dig en inloggningslänk och här är den.",
"email.signin_email.confirmation": "Klicka på länken nedan för att logga in på {siteName} .",
"email.signup_email.subject": "Din nya Fider-webbplats",
diff --git a/locale/tr/server.json b/locale/tr/server.json
index 38e04e350..ba034029d 100644
--- a/locale/tr/server.json
+++ b/locale/tr/server.json
@@ -52,7 +52,7 @@
"email.delete_post.text": "{title} silinmiştir .",
"email.new_comment.text": "{userName} , {title} ({postLink}) kaydına yorum eklemiştir.",
"email.new_post.text": "{userName} , {title} ({postLink}) yeni bir kayıt oluşturmuştur.",
- "email.signin_email.subject": "{siteName} sayfasına giriş yapın",
+ "email.signin_email.subject": "{siteName} için giriş kodunuz {code}",
"email.signin_email.text": "Bizden bir giriş bağlantısı talep etmiştiniz, işte burada bulabilirsiniz.",
"email.signin_email.confirmation": "{siteName} alanına giriş yapabilmek için aşağıdaki bağlantıya tıklayın.",
"email.signup_email.subject": "Yeni Fider sayfanız",
diff --git a/locale/zh-CN/server.json b/locale/zh-CN/server.json
index a0f876c80..e4e14fe16 100644
--- a/locale/zh-CN/server.json
+++ b/locale/zh-CN/server.json
@@ -52,7 +52,7 @@
"email.delete_post.text": "{title} 已经 删除了 .",
"email.new_comment.text": "{userName} 发表评论 {title} ({postLink}) .",
"email.new_post.text": "{userName} 创建了一个新帖子 {title} ({postLink}) .",
- "email.signin_email.subject": "登录到 {siteName}",
+ "email.signin_email.subject": "您的 {siteName} 登录代码是 {code}",
"email.signin_email.text": "您要求我们向您发送登录链接,现在是.",
"email.signin_email.confirmation": "单击下面的链接登录 {siteName} .",
"email.signup_email.subject": "您的新 Fider 网站",
diff --git a/views/email/signin_email.html b/views/email/signin_email.html
index 20eb26bd3..45af3795d 100644
--- a/views/email/signin_email.html
+++ b/views/email/signin_email.html
@@ -1,4 +1,4 @@
-{{define "subject"}}{{ translate "email.signin_email.subject" (dict "siteName" .siteName) }}{{end}}
+{{define "subject"}}{{ translate "email.signin_email.subject" (dict "siteName" .siteName "code" .code) }}{{end}}
{{define "body"}}
From 3672620aa3371bb98291e0cd14a88ed413f79d8f Mon Sep 17 00:00:00 2001
From: Matt Roberts
Date: Fri, 14 Nov 2025 14:09:01 +0000
Subject: [PATCH 07/11] Minor playwright changes.
---
e2e/setup.ts | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/e2e/setup.ts b/e2e/setup.ts
index a14f4e767..75335ab7e 100644
--- a/e2e/setup.ts
+++ b/e2e/setup.ts
@@ -51,10 +51,10 @@ async function createNewSite() {
const adminEmail = `admin-${tenantName}@fider.io`
//Create site
await page.goto("https://login.dev.fider.io:3000/signup")
- await page.type("#input-name", "admin")
- await page.type("#input-email", adminEmail)
- await page.type("#input-tenantName", tenantName)
- await page.type("#input-subdomain", tenantName)
+ await page.fill("#input-name", "admin")
+ await page.fill("#input-email", adminEmail)
+ await page.fill("#input-tenantName", tenantName)
+ await page.fill("#input-subdomain", tenantName)
await page.check("#input-legalAgreement")
await page.click(".c-button--primary")
From 7bb5a9351cc894a5c9c2c2080c1c44982c84afeb Mon Sep 17 00:00:00 2001
From: Matt Roberts
Date: Fri, 14 Nov 2025 20:11:34 +0000
Subject: [PATCH 08/11] Firming up the sign in forms so they "submit" and use
the right input type.
---
public/components/common/SignInControl.tsx | 43 ++++++++++++++--------
public/components/common/form/Form.tsx | 11 +++++-
public/components/common/form/Input.tsx | 2 +
3 files changed, 40 insertions(+), 16 deletions(-)
diff --git a/public/components/common/SignInControl.tsx b/public/components/common/SignInControl.tsx
index b0b551ad8..1be99bbc0 100644
--- a/public/components/common/SignInControl.tsx
+++ b/public/components/common/SignInControl.tsx
@@ -125,24 +125,36 @@ export const SignInControl: React.FunctionComponent = (props
const providersLen = fider.settings.oauth.length
+ const handleFormSubmit = async (e: React.FormEvent) => {
+ e.preventDefault()
+ // Form submission handler that routes to the correct function based on step
+ if (emailSignInStep === EmailSigninStep.EnterEmail) {
+ await signIn()
+ } else if (emailSignInStep === EmailSigninStep.EnterName) {
+ await submitNewUser()
+ } else if (emailSignInStep === EmailSigninStep.EnterCode) {
+ await verifyCode()
+ }
+ }
+
const renderSigninEmailButton = () => {
if (emailSignInStep == EmailSigninStep.EnterEmail) {
return (
-
+
Continue with Email
)
}
if (emailSignInStep == EmailSigninStep.EnterName) {
return (
-
+
Sign up
)
}
if (emailSignInStep == EmailSigninStep.EnterCode) {
return (
-
+
Submit
)
@@ -176,7 +188,7 @@ export const SignInControl: React.FunctionComponent = (props
{props.useEmail &&
(showEmailForm ? (