Skip to content

Commit 7b6ce5c

Browse files
committed
implement otp
1 parent b4ce9eb commit 7b6ce5c

File tree

11 files changed

+298
-158
lines changed

11 files changed

+298
-158
lines changed

.docker/.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
mysql
2+
redis

Makefile

+8-8
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
11
DIR := $(shell pwd)
22
GO_CI := golangci/golangci-lint:v1.59.1
33

4-
.PHONY: dev-start
5-
dev-start:
6-
@docker compose -f compose.dev.yml up -d
4+
.PHONY: local-up
5+
local-up:
6+
@docker compose -f compose.local.yml up -d
77

8-
.PHONY: dev-stop
9-
dev-stop:
10-
@docker compose -f compose.dev.yml down
8+
.PHONY: local-down
9+
local-down:
10+
@docker compose -f compose.local.yml down
1111

12-
.PHONY: dev-run
13-
dev-run:
12+
.PHONY: local-run
13+
local-run:
1414
@go run main.go serve
1515

1616
.PHONY: lint

compose.dev.yml compose.local.yml

File renamed without changes.

go.mod

+3-4
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ require (
66
github.com/aws/aws-sdk-go-v2 v1.30.4
77
github.com/aws/aws-sdk-go-v2/config v1.27.28
88
github.com/aws/aws-sdk-go-v2/credentials v1.17.28
9-
github.com/aws/aws-sdk-go-v2/service/s3 v1.59.0
9+
github.com/aws/aws-sdk-go-v2/service/s3 v1.60.0
1010
github.com/aws/smithy-go v1.20.4
1111
github.com/cockroachdb/errors v1.11.3
1212
github.com/go-playground/validator/v10 v10.22.0
@@ -15,7 +15,7 @@ require (
1515
github.com/spf13/cobra v1.8.1
1616
go.uber.org/zap v1.27.0
1717
golang.org/x/crypto v0.26.0
18-
google.golang.org/api v0.192.0
18+
google.golang.org/api v0.193.0
1919
gorm.io/driver/mysql v1.5.7
2020
gorm.io/gorm v1.25.11
2121
)
@@ -52,7 +52,6 @@ require (
5252
github.com/gogo/protobuf v1.3.2 // indirect
5353
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
5454
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
55-
github.com/golang/protobuf v1.5.4 // indirect
5655
github.com/google/s2a-go v0.1.8 // indirect
5756
github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect
5857
github.com/inconshreveable/mousetrap v1.1.0 // indirect
@@ -81,7 +80,7 @@ require (
8180
golang.org/x/sys v0.24.0 // indirect
8281
golang.org/x/text v0.17.0 // indirect
8382
golang.org/x/time v0.6.0 // indirect
84-
google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142 // indirect
83+
google.golang.org/genproto/googleapis/rpc v0.0.0-20240820151423-278611b39280 // indirect
8584
google.golang.org/grpc v1.65.0 // indirect
8685
google.golang.org/protobuf v1.34.2 // indirect
8786
)

go.sum

+11-144
Large diffs are not rendered by default.

internal/http/server/handlers/v1/auth.go

+96
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,15 @@ type signInGoogleRequest struct {
3232
Token string `json:"google_token" validate:"required"`
3333
}
3434

35+
type otpEmailGetRequest struct {
36+
Email string `json:"email" validate:"required,email,max=191"`
37+
}
38+
39+
type otpEmailValidateRequest struct {
40+
Email string `json:"email" validate:"required,email,max=191"`
41+
Otp string `json:"otp" validate:"required"`
42+
}
43+
3544
func AuthSignUp(ctr *container.Container) echo.HandlerFunc {
3645
return func(ctx echo.Context) error {
3746
var r signUpRequest
@@ -104,6 +113,93 @@ func AuthSignUp(ctr *container.Container) echo.HandlerFunc {
104113
}
105114
}
106115

116+
func AuthOtpEmailGenerate(ctr *container.Container) echo.HandlerFunc {
117+
return func(ctx echo.Context) error {
118+
var r otpEmailGetRequest
119+
if err := ctx.Bind(&r); err != nil {
120+
return ctx.JSON(http.StatusBadRequest, map[string]string{
121+
"message": "Cannot parse the request body.",
122+
})
123+
}
124+
if err := ctx.Validate(r); err != nil {
125+
return ctx.JSON(http.StatusUnprocessableEntity, map[string]string{
126+
"message": err.Error(),
127+
})
128+
}
129+
130+
ttl := ctr.OtpService.Email(r.Email)
131+
132+
return ctx.JSON(http.StatusCreated, map[string]interface{}{
133+
"ttl": ttl,
134+
})
135+
}
136+
}
137+
138+
func AuthOtpEmailSubmit(ctr *container.Container) echo.HandlerFunc {
139+
return func(ctx echo.Context) error {
140+
var r otpEmailValidateRequest
141+
if err := ctx.Bind(&r); err != nil {
142+
return ctx.JSON(http.StatusBadRequest, map[string]string{
143+
"message": "Cannot parse the request body.",
144+
})
145+
}
146+
if err := ctx.Validate(r); err != nil {
147+
return ctx.JSON(http.StatusUnprocessableEntity, map[string]string{
148+
"message": err.Error(),
149+
})
150+
}
151+
152+
isValid := ctr.OtpService.Check(r.Email, r.Otp)
153+
if !isValid {
154+
return ctx.JSON(http.StatusUnauthorized, map[string]interface{}{
155+
"message": "The OTP (one-time password) is not valid.",
156+
})
157+
}
158+
159+
user, err := ctr.UserService.FindBy("email", r.Email)
160+
if err != nil {
161+
return errors.WithStack(err)
162+
}
163+
164+
if user != nil {
165+
token, err := ctr.TokenService.Create(user)
166+
if err != nil {
167+
return errors.WithStack(err)
168+
}
169+
170+
return ctx.JSON(http.StatusCreated, map[string]interface{}{
171+
"user": user,
172+
"token": token,
173+
})
174+
} else {
175+
err = ctr.UserService.Create(&models.User{
176+
Username: r.Email,
177+
Email: r.Email,
178+
IsBanned: false,
179+
Password: "",
180+
})
181+
if err != nil {
182+
return errors.WithStack(err)
183+
}
184+
185+
user, err = ctr.UserService.FindBy("email", r.Email)
186+
if err != nil {
187+
return errors.WithStack(err)
188+
}
189+
190+
token, err := ctr.TokenService.Create(user)
191+
if err != nil {
192+
return errors.WithStack(err)
193+
}
194+
195+
return ctx.JSON(http.StatusCreated, map[string]interface{}{
196+
"user": user,
197+
"token": token,
198+
})
199+
}
200+
}
201+
}
202+
107203
func AuthSignInEmail(ctr *container.Container) echo.HandlerFunc {
108204
return func(ctx echo.Context) error {
109205
var r signInEmailRequest

internal/http/server/routes.go

+2
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ func (s *Server) registerRoutes() {
2323
public.POST("auth/sign-in/email", v1.AuthSignInEmail(s.container))
2424
public.POST("auth/sign-in/username", v1.AuthSignInUsername(s.container))
2525
public.POST("auth/sign-in/google", v1.AuthSignInGoogle(s.container, s.config))
26+
public.POST("auth/otp/email/generate", v1.AuthOtpEmailGenerate(s.container))
27+
public.POST("auth/otp/email/submit", v1.AuthOtpEmailSubmit(s.container))
2628
}
2729

2830
private := v1Api.Group("/", mw.Authorize(s.container))

internal/mailer/mailer.go

+15
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,21 @@ func (m *Mailer) SendWelcome(to, username string) {
4444
m.Send(to, "Welcome to Nightell!", message)
4545
}
4646

47+
func (m *Mailer) SendOtp(to, otp string) {
48+
message := strings.Join([]string{
49+
"Dear user,",
50+
"Your One-Time Password (OTP) for accessing your account is:",
51+
otp,
52+
"Please enter this code within the next 3 minutes to complete your verification process.",
53+
"For your security, do not share this code with anyone.",
54+
"If you did not request this code, please ignore this email.",
55+
"",
56+
"Thank you for using our service!",
57+
"https://nightell.neatplex.com",
58+
}, "\r\n")
59+
m.Send(to, "Nightell OTP (one-time password)", message)
60+
}
61+
4762
func (m *Mailer) SendDeleteAccount(to, username, link string) {
4863
message := strings.Join([]string{
4964
"Dear " + username + ",",

internal/services/container/container.go

+3
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"github.com/neatplex/nightell-core/internal/services/file"
88
"github.com/neatplex/nightell-core/internal/services/followship"
99
"github.com/neatplex/nightell-core/internal/services/like"
10+
"github.com/neatplex/nightell-core/internal/services/otp"
1011
"github.com/neatplex/nightell-core/internal/services/post"
1112
"github.com/neatplex/nightell-core/internal/services/remove"
1213
"github.com/neatplex/nightell-core/internal/services/token"
@@ -21,6 +22,7 @@ type Container struct {
2122
FileService *file.Service
2223
LikeService *like.Service
2324
FollowshipService *followship.Service
25+
OtpService *otp.Service
2426
}
2527

2628
func New(d *database.Database, s3 *s3.S3, m *mailer.Mailer) *Container {
@@ -32,5 +34,6 @@ func New(d *database.Database, s3 *s3.S3, m *mailer.Mailer) *Container {
3234
FileService: file.New(d, s3),
3335
LikeService: like.New(d),
3436
FollowshipService: followship.New(d),
37+
OtpService: otp.New(m),
3538
}
3639
}

internal/services/otp/service.go

+58
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
package otp
2+
3+
import (
4+
"github.com/neatplex/nightell-core/internal/mailer"
5+
"math/rand"
6+
"strconv"
7+
"time"
8+
)
9+
10+
const min3 = time.Duration(2) * time.Minute
11+
12+
type Service struct {
13+
mailer *mailer.Mailer
14+
passwords map[string]*password
15+
}
16+
17+
type password struct {
18+
Value string
19+
ExpireAt time.Time
20+
}
21+
22+
func (s *Service) Email(email string) int {
23+
if p, found := s.passwords[email]; found {
24+
if p.ExpireAt.After(time.Now()) {
25+
return s.ttl(p.ExpireAt)
26+
}
27+
}
28+
29+
s.passwords[email] = &password{
30+
Value: strconv.Itoa(rand.Intn(899999) + 100000),
31+
ExpireAt: time.Now().Add(min3),
32+
}
33+
34+
s.mailer.SendOtp(email, s.passwords[email].Value)
35+
36+
return s.ttl(s.passwords[email].ExpireAt)
37+
}
38+
39+
func (s *Service) Check(identifier string, password string) bool {
40+
if p, found := s.passwords[identifier]; found {
41+
defer delete(s.passwords, identifier)
42+
if p.ExpireAt.Before(time.Now().Add(min3)) {
43+
return p.Value == password
44+
}
45+
}
46+
return false
47+
}
48+
49+
func (s *Service) ttl(t time.Time) int {
50+
return int(t.Sub(time.Now()).Seconds())
51+
}
52+
53+
func New(m *mailer.Mailer) *Service {
54+
return &Service{
55+
passwords: make(map[string]*password),
56+
mailer: m,
57+
}
58+
}

0 commit comments

Comments
 (0)