Skip to content

Commit 71e4ca9

Browse files
authored
Merge pull request #1389 from getfider/login_codes
Email sign-in changed to use sign-in codes.
2 parents 842f7ad + ff24be7 commit 71e4ca9

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

64 files changed

+1499
-279
lines changed

Makefile

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,15 @@ test-e2e-server: ## Run all E2E tests
5959
test-e2e-ui: ## Run all E2E tests
6060
npx cucumber-js e2e/features/ui/**/*.feature --require-module ts-node/register --require 'e2e/**/*.ts' --publish-quiet
6161

62+
test-e2e-ui-headed: ## Run all E2E tests with visible browser
63+
HEADED=true npx cucumber-js e2e/features/ui/**/*.feature --require-module ts-node/register --require 'e2e/**/*.ts' --publish-quiet
64+
65+
test-e2e-ui-scenario: ## Run specific E2E test scenario (use NAME="scenario name")
66+
npx cucumber-js e2e/features/ui/**/*.feature --require-module ts-node/register --require 'e2e/**/*.ts' --publish-quiet --name "$(NAME)"
67+
68+
test-e2e-ui-scenario-headed: ## Run specific E2E test scenario with visible browser (use NAME="scenario name")
69+
HEADED=true npx cucumber-js e2e/features/ui/**/*.feature --require-module ts-node/register --require 'e2e/**/*.ts' --publish-quiet --name "$(NAME)"
70+
6271

6372

6473
##@ Running (Watch Mode)

app/actions/signin.go

Lines changed: 102 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,13 @@ import (
1414

1515
// SignInByEmail happens when user request to sign in by email
1616
type SignInByEmail struct {
17-
Email string `json:"email" format:"lower"`
18-
VerificationKey string
17+
Email string `json:"email" format:"lower"`
18+
VerificationCode string
1919
}
2020

2121
func NewSignInByEmail() *SignInByEmail {
2222
return &SignInByEmail{
23-
VerificationKey: entity.GenerateEmailVerificationKey(),
23+
VerificationCode: entity.GenerateEmailVerificationCode(),
2424
}
2525
}
2626

@@ -74,6 +74,105 @@ func (action *SignInByEmail) GetKind() enum.EmailVerificationKind {
7474
return enum.EmailVerificationKindSignIn
7575
}
7676

77+
// VerifySignInCode happens when user enters the verification code received via email
78+
type VerifySignInCode struct {
79+
Email string `json:"email" format:"lower"`
80+
Code string `json:"code"`
81+
}
82+
83+
// IsAuthorized returns true if current user is authorized to perform this action
84+
func (action *VerifySignInCode) IsAuthorized(ctx context.Context, user *entity.User) bool {
85+
return true
86+
}
87+
88+
// Validate if current model is valid
89+
func (action *VerifySignInCode) Validate(ctx context.Context, user *entity.User) *validate.Result {
90+
result := validate.Success()
91+
92+
if action.Email == "" {
93+
result.AddFieldFailure("email", propertyIsRequired(ctx, "email"))
94+
} else {
95+
messages := validate.Email(ctx, action.Email)
96+
result.AddFieldFailure("email", messages...)
97+
}
98+
99+
if action.Code == "" {
100+
result.AddFieldFailure("code", propertyIsRequired(ctx, "code"))
101+
} else if len(action.Code) != 6 {
102+
result.AddFieldFailure("code", "Verification code must be 6 digits")
103+
} else {
104+
// Validate that code contains only digits
105+
for _, char := range action.Code {
106+
if char < '0' || char > '9' {
107+
result.AddFieldFailure("code", "Verification code must contain only digits")
108+
break
109+
}
110+
}
111+
}
112+
113+
return result
114+
}
115+
116+
// SignInByEmailWithName happens when a new user (without account) requests to sign in by email
117+
type SignInByEmailWithName struct {
118+
Email string `json:"email" format:"lower"`
119+
Name string `json:"name"`
120+
VerificationCode string
121+
}
122+
123+
func NewSignInByEmailWithName() *SignInByEmailWithName {
124+
return &SignInByEmailWithName{
125+
VerificationCode: entity.GenerateEmailVerificationCode(),
126+
}
127+
}
128+
129+
// IsAuthorized returns true if current user is authorized to perform this action
130+
func (action *SignInByEmailWithName) IsAuthorized(ctx context.Context, user *entity.User) bool {
131+
tenant := ctx.Value(app.TenantCtxKey).(*entity.Tenant)
132+
// New users can only sign in if tenant allows email auth or is not private
133+
return tenant.IsEmailAuthAllowed || !tenant.IsPrivate
134+
}
135+
136+
// Validate if current model is valid
137+
func (action *SignInByEmailWithName) Validate(ctx context.Context, user *entity.User) *validate.Result {
138+
result := validate.Success()
139+
140+
if action.Email == "" {
141+
result.AddFieldFailure("email", propertyIsRequired(ctx, "email"))
142+
} else {
143+
messages := validate.Email(ctx, action.Email)
144+
result.AddFieldFailure("email", messages...)
145+
}
146+
147+
if action.Name == "" {
148+
result.AddFieldFailure("name", propertyIsRequired(ctx, "name"))
149+
} else if len(action.Name) > 100 {
150+
result.AddFieldFailure("name", propertyMaxStringLen(ctx, "name", 100))
151+
}
152+
153+
return result
154+
}
155+
156+
// GetEmail returns the email being verified
157+
func (action *SignInByEmailWithName) GetEmail() string {
158+
return action.Email
159+
}
160+
161+
// GetName returns the name provided by the user
162+
func (action *SignInByEmailWithName) GetName() string {
163+
return action.Name
164+
}
165+
166+
// GetUser returns the current user performing this action
167+
func (action *SignInByEmailWithName) GetUser() *entity.User {
168+
return nil
169+
}
170+
171+
// GetKind returns EmailVerificationKindSignIn
172+
func (action *SignInByEmailWithName) GetKind() enum.EmailVerificationKind {
173+
return enum.EmailVerificationKindSignIn
174+
}
175+
77176
// CompleteProfile happens when users completes their profile during first time sign in
78177
type CompleteProfile struct {
79178
Kind enum.EmailVerificationKind `json:"kind"`

app/actions/signin_test.go

Lines changed: 101 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,8 @@ func TestSignInByEmail_ShouldHaveVerificationKey(t *testing.T) {
3232

3333
result := action.Validate(context.Background(), nil)
3434
ExpectSuccess(result)
35-
Expect(action.VerificationKey).IsNotEmpty()
35+
Expect(action.VerificationCode).IsNotEmpty()
36+
Expect(len(action.VerificationCode)).Equals(6)
3637
}
3738

3839
func TestCompleteProfile_EmptyNameAndKey(t *testing.T) {
@@ -52,3 +53,102 @@ func TestCompleteProfile_LongName(t *testing.T) {
5253
result := action.Validate(context.Background(), nil)
5354
ExpectFailed(result, "name", "key")
5455
}
56+
57+
func TestVerifySignInCode_EmptyEmail(t *testing.T) {
58+
RegisterT(t)
59+
60+
action := actions.VerifySignInCode{Email: "", Code: "123456"}
61+
result := action.Validate(context.Background(), nil)
62+
ExpectFailed(result, "email")
63+
}
64+
65+
func TestVerifySignInCode_InvalidEmail(t *testing.T) {
66+
RegisterT(t)
67+
68+
action := actions.VerifySignInCode{Email: "invalid", Code: "123456"}
69+
result := action.Validate(context.Background(), nil)
70+
ExpectFailed(result, "email")
71+
}
72+
73+
func TestVerifySignInCode_EmptyCode(t *testing.T) {
74+
RegisterT(t)
75+
76+
action := actions.VerifySignInCode{Email: "jon.snow@got.com", Code: ""}
77+
result := action.Validate(context.Background(), nil)
78+
ExpectFailed(result, "code")
79+
}
80+
81+
func TestVerifySignInCode_InvalidCodeLength(t *testing.T) {
82+
RegisterT(t)
83+
84+
action := actions.VerifySignInCode{Email: "jon.snow@got.com", Code: "12345"}
85+
result := action.Validate(context.Background(), nil)
86+
ExpectFailed(result, "code")
87+
88+
action2 := actions.VerifySignInCode{Email: "jon.snow@got.com", Code: "1234567"}
89+
result2 := action2.Validate(context.Background(), nil)
90+
ExpectFailed(result2, "code")
91+
}
92+
93+
func TestVerifySignInCode_NonNumericCode(t *testing.T) {
94+
RegisterT(t)
95+
96+
action := actions.VerifySignInCode{Email: "jon.snow@got.com", Code: "12345A"}
97+
result := action.Validate(context.Background(), nil)
98+
ExpectFailed(result, "code")
99+
}
100+
101+
func TestVerifySignInCode_ValidCodeAndEmail(t *testing.T) {
102+
RegisterT(t)
103+
104+
action := actions.VerifySignInCode{Email: "jon.snow@got.com", Code: "123456"}
105+
result := action.Validate(context.Background(), nil)
106+
ExpectSuccess(result)
107+
}
108+
109+
func TestSignInByEmailWithName_EmptyEmailAndName(t *testing.T) {
110+
RegisterT(t)
111+
112+
action := actions.SignInByEmailWithName{Email: "", Name: ""}
113+
result := action.Validate(context.Background(), nil)
114+
ExpectFailed(result, "email", "name")
115+
}
116+
117+
func TestSignInByEmailWithName_InvalidEmail(t *testing.T) {
118+
RegisterT(t)
119+
120+
action := actions.SignInByEmailWithName{Email: "invalid", Name: "Jon Snow"}
121+
result := action.Validate(context.Background(), nil)
122+
ExpectFailed(result, "email")
123+
}
124+
125+
func TestSignInByEmailWithName_EmptyName(t *testing.T) {
126+
RegisterT(t)
127+
128+
action := actions.SignInByEmailWithName{Email: "jon.snow@got.com", Name: ""}
129+
result := action.Validate(context.Background(), nil)
130+
ExpectFailed(result, "name")
131+
}
132+
133+
func TestSignInByEmailWithName_LongName(t *testing.T) {
134+
RegisterT(t)
135+
136+
// 101 characters
137+
longName := "12345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901"
138+
action := actions.SignInByEmailWithName{Email: "jon.snow@got.com", Name: longName}
139+
result := action.Validate(context.Background(), nil)
140+
ExpectFailed(result, "name")
141+
}
142+
143+
func TestSignInByEmailWithName_ValidEmailAndName(t *testing.T) {
144+
RegisterT(t)
145+
146+
action := actions.NewSignInByEmailWithName()
147+
action.Email = "jon.snow@got.com"
148+
action.Name = "Jon Snow"
149+
150+
result := action.Validate(context.Background(), nil)
151+
ExpectSuccess(result)
152+
Expect(action.VerificationCode).IsNotEmpty()
153+
Expect(len(action.VerificationCode)).Equals(6)
154+
}

app/cmd/routes.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,12 +117,16 @@ func routes(r *web.Engine) *web.Engine {
117117
r.Use(middlewares.BlockPendingTenants())
118118

119119
r.Get("/signin", handlers.SignInPage())
120+
r.Get("/signin/complete", handlers.CompleteSignInProfilePage())
120121
r.Get("/loginemailsent", handlers.LoginEmailSentPage())
121122
r.Get("/not-invited", handlers.NotInvitedPage())
122123
r.Get("/signin/verify", handlers.VerifySignInKey(enum.EmailVerificationKindSignIn))
123124
r.Get("/invite/verify", handlers.VerifySignInKey(enum.EmailVerificationKindUserInvitation))
124125
r.Post("/_api/signin/complete", handlers.CompleteSignInProfile())
125126
r.Post("/_api/signin", handlers.SignInByEmail())
127+
r.Post("/_api/signin/newuser", handlers.SignInByEmailWithName())
128+
r.Post("/_api/signin/verify", handlers.VerifySignInCode())
129+
r.Post("/_api/signin/resend", handlers.ResendSignInCode())
126130

127131
// Block if it's private tenant with unauthenticated user
128132
r.Use(middlewares.CheckTenantPrivacy())

0 commit comments

Comments
 (0)