implemented verifiaction db
This commit is contained in:
@@ -14,6 +14,7 @@ import (
|
|||||||
"github.com/tech/sendico/pkg/db/refreshtokens"
|
"github.com/tech/sendico/pkg/db/refreshtokens"
|
||||||
"github.com/tech/sendico/pkg/db/role"
|
"github.com/tech/sendico/pkg/db/role"
|
||||||
"github.com/tech/sendico/pkg/db/transaction"
|
"github.com/tech/sendico/pkg/db/transaction"
|
||||||
|
"github.com/tech/sendico/pkg/db/verification"
|
||||||
"github.com/tech/sendico/pkg/merrors"
|
"github.com/tech/sendico/pkg/merrors"
|
||||||
"github.com/tech/sendico/pkg/mlogger"
|
"github.com/tech/sendico/pkg/mlogger"
|
||||||
)
|
)
|
||||||
@@ -30,6 +31,7 @@ type Factory interface {
|
|||||||
NewInvitationsDB() (invitation.DB, error)
|
NewInvitationsDB() (invitation.DB, error)
|
||||||
NewRecipientsDB() (recipient.DB, error)
|
NewRecipientsDB() (recipient.DB, error)
|
||||||
NewPaymentMethodsDB() (paymethod.DB, error)
|
NewPaymentMethodsDB() (paymethod.DB, error)
|
||||||
|
NewVerificationsDB() (verification.DB, error)
|
||||||
|
|
||||||
NewRolesDB() (role.DB, error)
|
NewRolesDB() (role.DB, error)
|
||||||
NewPoliciesDB() (policy.DB, error)
|
NewPoliciesDB() (policy.DB, error)
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import (
|
|||||||
"github.com/tech/sendico/pkg/db/internal/mongo/refreshtokensdb"
|
"github.com/tech/sendico/pkg/db/internal/mongo/refreshtokensdb"
|
||||||
"github.com/tech/sendico/pkg/db/internal/mongo/rolesdb"
|
"github.com/tech/sendico/pkg/db/internal/mongo/rolesdb"
|
||||||
"github.com/tech/sendico/pkg/db/internal/mongo/transactionimp"
|
"github.com/tech/sendico/pkg/db/internal/mongo/transactionimp"
|
||||||
|
"github.com/tech/sendico/pkg/db/internal/mongo/verificationimp"
|
||||||
"github.com/tech/sendico/pkg/db/invitation"
|
"github.com/tech/sendico/pkg/db/invitation"
|
||||||
"github.com/tech/sendico/pkg/db/organization"
|
"github.com/tech/sendico/pkg/db/organization"
|
||||||
"github.com/tech/sendico/pkg/db/paymethod"
|
"github.com/tech/sendico/pkg/db/paymethod"
|
||||||
@@ -32,6 +33,7 @@ import (
|
|||||||
"github.com/tech/sendico/pkg/db/repository"
|
"github.com/tech/sendico/pkg/db/repository"
|
||||||
"github.com/tech/sendico/pkg/db/role"
|
"github.com/tech/sendico/pkg/db/role"
|
||||||
"github.com/tech/sendico/pkg/db/transaction"
|
"github.com/tech/sendico/pkg/db/transaction"
|
||||||
|
"github.com/tech/sendico/pkg/db/verification"
|
||||||
"github.com/tech/sendico/pkg/mlogger"
|
"github.com/tech/sendico/pkg/mlogger"
|
||||||
"github.com/tech/sendico/pkg/model"
|
"github.com/tech/sendico/pkg/model"
|
||||||
"github.com/tech/sendico/pkg/mservice"
|
"github.com/tech/sendico/pkg/mservice"
|
||||||
@@ -246,6 +248,10 @@ func (db *DB) NewRolesDB() (role.DB, error) {
|
|||||||
return rolesdb.Create(db.logger, db.db())
|
return rolesdb.Create(db.logger, db.db())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (db *DB) NewVerificationsDB() (verification.DB, error) {
|
||||||
|
return verificationimp.Create(db.logger, db.db(), db.TransactionFactory())
|
||||||
|
}
|
||||||
|
|
||||||
func (db *DB) TransactionFactory() transaction.Factory {
|
func (db *DB) TransactionFactory() transaction.Factory {
|
||||||
return transactionimp.CreateFactory(db.client)
|
return transactionimp.CreateFactory(db.client)
|
||||||
}
|
}
|
||||||
|
|||||||
72
api/pkg/db/internal/mongo/verificationimp/consume.go
Normal file
72
api/pkg/db/internal/mongo/verificationimp/consume.go
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
package verificationimp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/tech/sendico/pkg/db/repository"
|
||||||
|
"github.com/tech/sendico/pkg/db/repository/builder"
|
||||||
|
"github.com/tech/sendico/pkg/db/verification"
|
||||||
|
"github.com/tech/sendico/pkg/merrors"
|
||||||
|
"github.com/tech/sendico/pkg/model"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (db *verificationDB) Consume(
|
||||||
|
ct context.Context,
|
||||||
|
rawToken string,
|
||||||
|
) (*model.VerificationToken, error) {
|
||||||
|
|
||||||
|
hash := tokenHash(rawToken)
|
||||||
|
now := time.Now().UTC()
|
||||||
|
|
||||||
|
// 1) Пытаемся атомарно использовать токен
|
||||||
|
filter := repository.Query().And(
|
||||||
|
repository.Filter("verifyTokenHash", hash),
|
||||||
|
repository.Filter("usedAt", nil),
|
||||||
|
repository.Query().Comparison(repository.Field("expiresAt"), builder.Gt, now),
|
||||||
|
)
|
||||||
|
|
||||||
|
t, err := db.tf.CreateTransaction().Execute(
|
||||||
|
ct,
|
||||||
|
func(ctx context.Context) (any, error) {
|
||||||
|
var existing model.VerificationToken
|
||||||
|
if err := db.DBImp.FindOne(ctx, filter, &existing); err != nil {
|
||||||
|
if errors.Is(err, merrors.ErrNoData) {
|
||||||
|
// normal behaviour
|
||||||
|
db.Logger.Debug("Token hash not found", zap.Error(err), zap.String("hash", hash))
|
||||||
|
return nil, wrap(verification.ErrTokenNotFound, err.Error())
|
||||||
|
}
|
||||||
|
db.Logger.Warn("Failed to check token", zap.Error(err), zap.String("hash", hash))
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if existing.UsedAt != nil {
|
||||||
|
db.Logger.Debug("Token has already been used", zap.String("hash", hash), zap.Time("used_at", *existing.UsedAt))
|
||||||
|
return nil, wrap(verification.ErrTokenAlreadyUsed, "db: token already used")
|
||||||
|
}
|
||||||
|
|
||||||
|
if existing.ExpiresAt.Before(now) {
|
||||||
|
db.Logger.Debug("Token has already expired", zap.String("hash", hash), zap.Time("expired_at", existing.ExpiresAt))
|
||||||
|
return nil, wrap(verification.ErrTokenExpired, "db: token expired")
|
||||||
|
}
|
||||||
|
|
||||||
|
existing.UsedAt = &now
|
||||||
|
if err := db.DBImp.Update(ctx, &existing); err != nil {
|
||||||
|
db.Logger.Warn("Failed to consume token", zap.Error(err), zap.String("hash", hash))
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &existing, nil
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
res, ok := t.(*model.VerificationToken)
|
||||||
|
if !ok {
|
||||||
|
return nil, merrors.Internal("unexpexted token type")
|
||||||
|
}
|
||||||
|
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
74
api/pkg/db/internal/mongo/verificationimp/create.go
Normal file
74
api/pkg/db/internal/mongo/verificationimp/create.go
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
package verificationimp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/base64"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/tech/sendico/pkg/model"
|
||||||
|
"github.com/tech/sendico/pkg/mutil/mzap"
|
||||||
|
"go.mongodb.org/mongo-driver/v2/bson"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
const verificationTokenBytes = 32
|
||||||
|
|
||||||
|
func newVerificationToken(
|
||||||
|
accountRef bson.ObjectID,
|
||||||
|
purpose model.VerificationPurpose,
|
||||||
|
target string,
|
||||||
|
ttl time.Duration,
|
||||||
|
) (*model.VerificationToken, string, error) {
|
||||||
|
|
||||||
|
raw := make([]byte, verificationTokenBytes)
|
||||||
|
if _, err := rand.Read(raw); err != nil {
|
||||||
|
return nil, "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
rawToken := base64.RawURLEncoding.EncodeToString(raw)
|
||||||
|
|
||||||
|
hashStr := tokenHash(rawToken)
|
||||||
|
|
||||||
|
now := time.Now().UTC()
|
||||||
|
|
||||||
|
token := &model.VerificationToken{
|
||||||
|
AccountRef: accountRef,
|
||||||
|
Purpose: purpose,
|
||||||
|
Target: target,
|
||||||
|
VerifyTokenHash: hashStr,
|
||||||
|
UsedAt: nil,
|
||||||
|
ExpiresAt: now.Add(ttl),
|
||||||
|
}
|
||||||
|
|
||||||
|
return token, rawToken, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *verificationDB) Create(
|
||||||
|
ctx context.Context,
|
||||||
|
accountRef bson.ObjectID,
|
||||||
|
purpose model.VerificationPurpose,
|
||||||
|
target string,
|
||||||
|
ttl time.Duration,
|
||||||
|
) (string, error) {
|
||||||
|
|
||||||
|
logFields := []zap.Field{
|
||||||
|
zap.String("purpose", string(purpose)), zap.Duration("ttl", ttl),
|
||||||
|
mzap.AccRef(accountRef), zap.String("target", target),
|
||||||
|
}
|
||||||
|
|
||||||
|
token, raw, err := newVerificationToken(accountRef, purpose, target, ttl)
|
||||||
|
if err != nil {
|
||||||
|
db.Logger.Warn("Failed to generate verification token", append(logFields, zap.Error(err))...)
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := db.DBImp.Create(ctx, token); err != nil {
|
||||||
|
db.Logger.Warn("Failed to persist verification token", append(logFields, zap.Error(err))...)
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
db.Logger.Debug("Verification token created", append(logFields, zap.String("hash", token.VerifyTokenHash))...)
|
||||||
|
|
||||||
|
return raw, nil
|
||||||
|
}
|
||||||
49
api/pkg/db/internal/mongo/verificationimp/db.go
Normal file
49
api/pkg/db/internal/mongo/verificationimp/db.go
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
package verificationimp
|
||||||
|
|
||||||
|
import (
|
||||||
|
ri "github.com/tech/sendico/pkg/db/repository/index"
|
||||||
|
"github.com/tech/sendico/pkg/db/template"
|
||||||
|
"github.com/tech/sendico/pkg/db/transaction"
|
||||||
|
"github.com/tech/sendico/pkg/mlogger"
|
||||||
|
"github.com/tech/sendico/pkg/model"
|
||||||
|
"github.com/tech/sendico/pkg/mservice"
|
||||||
|
"go.mongodb.org/mongo-driver/v2/mongo"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
type verificationDB struct {
|
||||||
|
template.DBImp[*model.VerificationToken]
|
||||||
|
tf transaction.Factory
|
||||||
|
}
|
||||||
|
|
||||||
|
func Create(
|
||||||
|
logger mlogger.Logger,
|
||||||
|
db *mongo.Database,
|
||||||
|
tf transaction.Factory,
|
||||||
|
) (*verificationDB, error) {
|
||||||
|
p := &verificationDB{
|
||||||
|
DBImp: *template.Create[*model.VerificationToken](logger, mservice.VerificationTokens, db),
|
||||||
|
tf: tf,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := p.Repository.CreateIndex(&ri.Definition{
|
||||||
|
Keys: []ri.Key{{Field: "verifyTokenHash", Sort: ri.Asc}},
|
||||||
|
Unique: true,
|
||||||
|
Name: "unique_token_hash",
|
||||||
|
}); err != nil {
|
||||||
|
p.Logger.Error("Failed to create unique verifyTokenHash index", zap.Error(err))
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
ttl := int32(2678400) // 30 days
|
||||||
|
if err := p.Repository.CreateIndex(&ri.Definition{
|
||||||
|
Keys: []ri.Key{{Field: "expiresAt", Sort: ri.Asc}},
|
||||||
|
TTL: &ttl,
|
||||||
|
Name: "ttl_expires_at",
|
||||||
|
}); err != nil {
|
||||||
|
p.Logger.Error("Failed to create TTL index on expiresAt", zap.Error(err))
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return p, nil
|
||||||
|
}
|
||||||
7
api/pkg/db/internal/mongo/verificationimp/error.go
Normal file
7
api/pkg/db/internal/mongo/verificationimp/error.go
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
package verificationimp
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
func wrap(err error, msg string) error {
|
||||||
|
return fmt.Errorf("%s: %w", msg, err)
|
||||||
|
}
|
||||||
11
api/pkg/db/internal/mongo/verificationimp/hash.go
Normal file
11
api/pkg/db/internal/mongo/verificationimp/hash.go
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
package verificationimp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/base64"
|
||||||
|
)
|
||||||
|
|
||||||
|
func tokenHash(rawToken string) string {
|
||||||
|
hash := sha256.Sum256([]byte(rawToken))
|
||||||
|
return base64.RawURLEncoding.EncodeToString(hash[:])
|
||||||
|
}
|
||||||
28
api/pkg/db/verification/verification.go
Normal file
28
api/pkg/db/verification/verification.go
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
package verification
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/tech/sendico/pkg/model"
|
||||||
|
"go.mongodb.org/mongo-driver/v2/bson"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrTokenNotFound = errors.New("verification token not found")
|
||||||
|
ErrTokenAlreadyUsed = errors.New("verification token already used")
|
||||||
|
ErrTokenExpired = errors.New("verification token expired")
|
||||||
|
)
|
||||||
|
|
||||||
|
type DB interface {
|
||||||
|
// template.DB[*model.VerificationToken]
|
||||||
|
Create(
|
||||||
|
ctx context.Context,
|
||||||
|
accountRef bson.ObjectID,
|
||||||
|
purpose model.VerificationPurpose,
|
||||||
|
target string,
|
||||||
|
ttl time.Duration,
|
||||||
|
) (rawToken string, err error)
|
||||||
|
Consume(ctx context.Context, rawToken string) (*model.VerificationToken, error)
|
||||||
|
}
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
package model
|
package model
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/tech/sendico/pkg/db/storable"
|
"github.com/tech/sendico/pkg/db/storable"
|
||||||
"github.com/tech/sendico/pkg/mservice"
|
"github.com/tech/sendico/pkg/mservice"
|
||||||
"go.mongodb.org/mongo-driver/v2/bson"
|
"go.mongodb.org/mongo-driver/v2/bson"
|
||||||
@@ -28,10 +30,9 @@ type AccountPublic struct {
|
|||||||
|
|
||||||
type Account struct {
|
type Account struct {
|
||||||
AccountPublic `bson:",inline" json:",inline"`
|
AccountPublic `bson:",inline" json:",inline"`
|
||||||
EmailBackup string `bson:"emailBackup" json:"emailBackup"`
|
EmailBackup string `bson:"emailBackup" json:"-"`
|
||||||
Password string `bson:"password" json:"password"`
|
Password string `bson:"password" json:"-"` // password hash
|
||||||
ResetPasswordToken string `bson:"resetPasswordToken" json:"resetPasswordToken"`
|
EmailVerifiedAt *time.Time `bson:"emailVerifiedAt,omitempty" json:"-"`
|
||||||
VerifyToken string `bson:"verifyToken" json:"verifyToken"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *Account) HashPassword() error {
|
func (a *Account) HashPassword() error {
|
||||||
|
|||||||
30
api/pkg/model/verificaton.go
Normal file
30
api/pkg/model/verificaton.go
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/tech/sendico/pkg/db/storable"
|
||||||
|
"go.mongodb.org/mongo-driver/v2/bson"
|
||||||
|
)
|
||||||
|
|
||||||
|
type VerificationPurpose string
|
||||||
|
|
||||||
|
const (
|
||||||
|
PurposeAccountActivation VerificationPurpose = "account_activation"
|
||||||
|
PurposeEmailChange VerificationPurpose = "email_change"
|
||||||
|
PurposePasswordReset VerificationPurpose = "password_reset"
|
||||||
|
PurposeSensitiveAction VerificationPurpose = "sensitive_action"
|
||||||
|
PurposeMagicLogin VerificationPurpose = "magic_login"
|
||||||
|
)
|
||||||
|
|
||||||
|
type VerificationToken struct {
|
||||||
|
storable.Base `bson:",inline" json:",inline"`
|
||||||
|
ArchivableBase `bson:",inline" json:",inline"`
|
||||||
|
|
||||||
|
Target string `bson:"target,omitempty" json:"target"`
|
||||||
|
AccountRef bson.ObjectID `bson:"accountRef" json:"accountRef"`
|
||||||
|
Purpose VerificationPurpose `bson:"purpose" json:"purpose"`
|
||||||
|
VerifyTokenHash string `bson:"verifyTokenHash" json:"-"`
|
||||||
|
UsedAt *time.Time `bson:"usedAt,omitempty" json:"-"`
|
||||||
|
ExpiresAt time.Time `bson:"expiresAt" json:"-"`
|
||||||
|
}
|
||||||
@@ -50,6 +50,7 @@ const (
|
|||||||
Storage Type = "storage" // Represents statuses of tasks or projects
|
Storage Type = "storage" // Represents statuses of tasks or projects
|
||||||
TgSettle Type = "tgsettle_gateway" // Represents tg settlement gateway
|
TgSettle Type = "tgsettle_gateway" // Represents tg settlement gateway
|
||||||
Tenants Type = "tenants" // Represents tenants managed in the system
|
Tenants Type = "tenants" // Represents tenants managed in the system
|
||||||
|
VerificationTokens Type = "verification_tokens" //Represents verification tokens managed in the system
|
||||||
Wallets Type = "wallets" // Represents workflows for tasks or projects
|
Wallets Type = "wallets" // Represents workflows for tasks or projects
|
||||||
Workflows Type = "workflows" // Represents workflows for tasks or projects
|
Workflows Type = "workflows" // Represents workflows for tasks or projects
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -13,3 +13,7 @@ func ObjRef(name string, objRef bson.ObjectID) zap.Field {
|
|||||||
func StorableRef(obj storable.Storable) zap.Field {
|
func StorableRef(obj storable.Storable) zap.Field {
|
||||||
return ObjRef(obj.Collection()+"_ref", *obj.GetID())
|
return ObjRef(obj.Collection()+"_ref", *obj.GetID())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func AccRef(accountRef bson.ObjectID) zap.Field {
|
||||||
|
return ObjRef("account_ref", accountRef)
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user