Skip to content

Commit 7a33498

Browse files
authored
Merge pull request #1 from aisa-it/add/emailCodes
add emailCodes
2 parents 8c32914 + 8fe2fdc commit 7a33498

File tree

9 files changed

+223
-3
lines changed

9 files changed

+223
-3
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,4 +32,5 @@ go.work.sum
3232
# .vscode/
3333

3434
bin/
35+
.idea
3536
aiplan_mem.db

api/api.go

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
package api
22

33
import (
4+
"bytes"
45
"encoding/base64"
6+
"encoding/json"
7+
"github.com/aisa-it/aiplan-mem/internal/dao"
8+
"io"
59
"net/http"
610
"net/url"
711
"strconv"
@@ -70,6 +74,38 @@ func (a *AIPlanMemAPI) GetUserLastSeenTime(userId uuid.UUID) (time.Time, error)
7074
return time.Unix(int64(t), 0), err
7175
}
7276

77+
// EmailCodes methods
78+
func (a *AIPlanMemAPI) SaveEmailCode(userID uuid.UUID, newEmail string) (string, error) {
79+
if a.isModule {
80+
return a.ds.EmailCodes.GenCode(userID, newEmail)
81+
}
82+
83+
h, err := a.postRequestWithResponseHeader("/emailCodes/" + userID.String() + "?email=" + url.QueryEscape(newEmail))
84+
return h.Get("code"), err
85+
}
86+
87+
func (a *AIPlanMemAPI) VerifyEmailCode(userID uuid.UUID, email, code string) (bool, error) {
88+
if a.isModule {
89+
return a.ds.EmailCodes.VerifyCode(userID, email, code)
90+
}
91+
92+
data := dao.EmailCodeData{
93+
NewEmail: email,
94+
Code: code,
95+
CreatedAt: time.Now(),
96+
}
97+
jsonData, _ := json.Marshal(data)
98+
99+
resp, err := a.postRequestWithResponse("/emailCodes/"+userID.String()+"/verify", jsonData)
100+
if err != nil {
101+
return false, err
102+
}
103+
defer resp.Body.Close()
104+
return resp.Header.Get("verify") == "true", nil
105+
}
106+
107+
//-------------------
108+
73109
func (a *AIPlanMemAPI) getRequest(path string) (http.Header, error) {
74110
resp, err := http.Get(a.addr.ResolveReference(&url.URL{Path: path}).String())
75111
if err != nil {
@@ -87,3 +123,21 @@ func (a *AIPlanMemAPI) postRequest(path string) error {
87123
defer resp.Body.Close()
88124
return nil
89125
}
126+
127+
func (a *AIPlanMemAPI) postRequestWithResponseHeader(path string) (http.Header, error) {
128+
resp, err := http.Post(a.addr.ResolveReference(&url.URL{Path: path}).String(), "", nil)
129+
if err != nil {
130+
return nil, err
131+
}
132+
defer resp.Body.Close()
133+
return resp.Header, nil
134+
}
135+
136+
func (a *AIPlanMemAPI) postRequestWithResponse(path string, body []byte) (*http.Response, error) {
137+
var reader io.Reader
138+
if body != nil {
139+
reader = bytes.NewReader(body)
140+
}
141+
142+
return http.Post(a.addr.ResolveReference(&url.URL{Path: path}).String(), "application/json", reader)
143+
}

apierror/errors.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package apierror
2+
3+
import "errors"
4+
5+
var (
6+
ErrVerification = errors.New("verification error")
7+
ErrEmailCodeTooSoon = errors.New("email code rate limit exceeded")
8+
)

internal/config/config.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,16 @@ package config
22

33
import (
44
"os"
5+
"time"
56
)
67

78
const (
89
SessionsBlaclistBucket = "sessionsBlacklist"
910
LastSeenBucket = "userLastSeen"
11+
EmailCodesBucket = "emailCodes"
12+
13+
EmailCodeLifeTime time.Duration = time.Minute * 5
14+
EmailCodeLimitReq = time.Minute
1015
)
1116

1217
type Config struct {

internal/dao/email-code.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package dao
2+
3+
import "time"
4+
5+
type EmailCodeData struct {
6+
NewEmail string `json:"new_email"`
7+
Code string `json:"code"`
8+
CreatedAt time.Time `json:"created_at"`
9+
}

internal/db/db.go

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package db
22

33
import (
4+
emailcodes "github.com/aisa-it/aiplan-mem/internal/db/email-codes"
45
"log/slog"
56
"os"
67
"time"
@@ -14,7 +15,8 @@ import (
1415
type DataStore struct {
1516
db *bolt.DB
1617

17-
Sessions *sessions.SessionsStore
18+
Sessions *sessions.SessionsStore
19+
EmailCodes *emailcodes.EmailCodesStore
1820
}
1921

2022
func OpenDB(cfg *config.Config) (*DataStore, error) {
@@ -24,7 +26,11 @@ func OpenDB(cfg *config.Config) (*DataStore, error) {
2426
}
2527

2628
if err := db.Update(func(tx *bolt.Tx) error {
27-
_, err := tx.CreateBucketIfNotExists([]byte(config.SessionsBlaclistBucket))
29+
_, err := tx.CreateBucketIfNotExists([]byte(config.EmailCodesBucket))
30+
if err != nil {
31+
return err
32+
}
33+
_, err = tx.CreateBucketIfNotExists([]byte(config.SessionsBlaclistBucket))
2834
if err != nil {
2935
return err
3036
}
@@ -35,7 +41,9 @@ func OpenDB(cfg *config.Config) (*DataStore, error) {
3541
os.Exit(1)
3642
}
3743

38-
return &DataStore{db: db, Sessions: sessions.NewSessionsStore(db, cfg)}, nil
44+
return &DataStore{db: db,
45+
Sessions: sessions.NewSessionsStore(db, cfg),
46+
EmailCodes: emailcodes.NewEmailCodesStore(db)}, nil
3947
}
4048

4149
func (ds DataStore) Close() error {
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
package emailcodes
2+
3+
import (
4+
"encoding/json"
5+
"github.com/aisa-it/aiplan-mem/apierror"
6+
"github.com/aisa-it/aiplan-mem/internal/dao"
7+
"github.com/aisa-it/aiplan-mem/internal/utils"
8+
"time"
9+
10+
"github.com/aisa-it/aiplan-mem/internal/config"
11+
"github.com/boltdb/bolt"
12+
"github.com/gofrs/uuid/v5"
13+
)
14+
15+
type EmailCodesStore struct {
16+
db *bolt.DB
17+
}
18+
19+
func NewEmailCodesStore(db *bolt.DB) *EmailCodesStore {
20+
return &EmailCodesStore{db: db}
21+
}
22+
23+
func (ecs *EmailCodesStore) GenCode(userID uuid.UUID, newEmail string) (string, error) {
24+
var codeData *dao.EmailCodeData
25+
data := dao.EmailCodeData{
26+
NewEmail: newEmail,
27+
Code: utils.GenCode(),
28+
CreatedAt: time.Now(),
29+
}
30+
return data.Code, ecs.db.Update(func(tx *bolt.Tx) error {
31+
b := tx.Bucket([]byte(config.EmailCodesBucket))
32+
33+
jsonDataOld := b.Get(userID.Bytes())
34+
if jsonDataOld != nil {
35+
if err := json.Unmarshal(jsonDataOld, &codeData); err != nil {
36+
return err
37+
}
38+
39+
if codeData.CreatedAt.Add(config.EmailCodeLimitReq).After(time.Now()) {
40+
return apierror.ErrEmailCodeTooSoon
41+
}
42+
}
43+
44+
jsonData, err := json.Marshal(data)
45+
if err != nil {
46+
return err
47+
}
48+
49+
return b.Put(userID.Bytes(), jsonData)
50+
})
51+
}
52+
53+
func (ecs *EmailCodesStore) VerifyCode(userID uuid.UUID, email, code string) (bool, error) {
54+
var verified bool
55+
var codeData *dao.EmailCodeData
56+
57+
err := ecs.db.Update(func(tx *bolt.Tx) error {
58+
b := tx.Bucket([]byte(config.EmailCodesBucket))
59+
60+
jsonData := b.Get(userID.Bytes())
61+
if jsonData == nil {
62+
return nil
63+
}
64+
65+
if err := json.Unmarshal(jsonData, &codeData); err != nil {
66+
return err
67+
}
68+
69+
if codeData.NewEmail != email ||
70+
codeData.Code != code ||
71+
codeData.CreatedAt.Add(config.EmailCodeLifeTime).Before(time.Now()) {
72+
return apierror.ErrVerification
73+
}
74+
75+
if err := b.Delete(userID.Bytes()); err != nil {
76+
return err
77+
}
78+
79+
verified = true
80+
return nil
81+
})
82+
83+
if err != nil {
84+
return false, err
85+
}
86+
87+
return verified, nil
88+
}

internal/server/server.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,13 @@ package server
22

33
import (
44
"encoding/base64"
5+
"encoding/json"
56
"fmt"
67
"log/slog"
78
"net/http"
89

910
"github.com/aisa-it/aiplan-mem/internal/config"
11+
"github.com/aisa-it/aiplan-mem/internal/dao"
1012
"github.com/aisa-it/aiplan-mem/internal/db"
1113
"github.com/gofrs/uuid/v5"
1214
"github.com/labstack/echo/v4"
@@ -34,6 +36,12 @@ func RunServer(cfg *config.Config, ds *db.DataStore) {
3436
lastSeenGroup.POST("/:userId", s.postUserLastSeen)
3537
}
3638

39+
emailCodeGroup := e.Group("/emailCodes")
40+
{
41+
emailCodeGroup.POST("/:userId/verify", s.verifyEmailCode)
42+
emailCodeGroup.POST("/:userId", s.saveEmailCode)
43+
}
44+
3745
if err := e.Start(cfg.ListenAddr); err != nil {
3846
slog.Error("Start http server", "err", err)
3947
}
@@ -90,3 +98,35 @@ func (s *Server) postUserLastSeen(c echo.Context) error {
9098
}
9199
return c.NoContent(http.StatusOK)
92100
}
101+
102+
// EmailCodes handlers
103+
104+
func (s *Server) saveEmailCode(c echo.Context) error {
105+
userId := uuid.FromStringOrNil(c.Param("userId"))
106+
email := c.QueryParam("email")
107+
108+
code, err := s.DataStore.EmailCodes.GenCode(userId, email)
109+
if err != nil {
110+
return sendError(c, err)
111+
}
112+
113+
c.Response().Header().Set("code", fmt.Sprint(code))
114+
return c.NoContent(http.StatusOK)
115+
}
116+
117+
func (s *Server) verifyEmailCode(c echo.Context) error {
118+
userId := uuid.FromStringOrNil(c.Param("userId"))
119+
120+
var req dao.EmailCodeData
121+
if err := json.NewDecoder(c.Request().Body).Decode(&req); err != nil {
122+
return sendError(c, err)
123+
}
124+
125+
verify, err := s.DataStore.EmailCodes.VerifyCode(userId, req.NewEmail, req.Code)
126+
if err != nil {
127+
return sendError(c, err)
128+
}
129+
130+
c.Response().Header().Set("verify", fmt.Sprint(verify))
131+
return c.NoContent(http.StatusOK)
132+
}

internal/utils/utils.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package utils
2+
3+
import "github.com/sethvargo/go-password/password"
4+
5+
func GenCode() string {
6+
return password.MustGenerate(6, 6, 0, false, true)
7+
}

0 commit comments

Comments
 (0)