2026-05-20 23:51:22 +00:00
|
|
|
package repository
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
|
|
|
|
"errors"
|
|
|
|
|
"time"
|
|
|
|
|
|
|
|
|
|
libmongo "gateway/internal/library/mongo"
|
|
|
|
|
member "gateway/internal/model/member/domain"
|
|
|
|
|
"gateway/internal/model/member/domain/entity"
|
|
|
|
|
domrepo "gateway/internal/model/member/domain/repository"
|
|
|
|
|
|
|
|
|
|
"go.mongodb.org/mongo-driver/v2/bson"
|
|
|
|
|
mongodriver "go.mongodb.org/mongo-driver/v2/mongo"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// MongoTOTPProfileRepository stores TOTP fields on the members document.
|
|
|
|
|
type MongoTOTPProfileRepository struct {
|
|
|
|
|
db libmongo.DocumentDBUseCase
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// NewMongoTOTPProfileRepository creates a TOTP profile repo backed by members.
|
|
|
|
|
func NewMongoTOTPProfileRepository(conf *libmongo.Conf) domrepo.TOTPProfileRepository {
|
|
|
|
|
documentDB, err := libmongo.NewDocumentDB(conf, entity.Member{}.CollectionName())
|
|
|
|
|
if err != nil {
|
|
|
|
|
panic(err)
|
|
|
|
|
}
|
|
|
|
|
return &MongoTOTPProfileRepository{db: documentDB}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (r *MongoTOTPProfileRepository) Get(ctx context.Context, tenantID, uid string) (*domrepo.TOTPProfileRecord, error) {
|
|
|
|
|
var doc entity.Member
|
|
|
|
|
filter := bson.M{member.BSONFieldTenantID: tenantID, member.BSONFieldUID: uid}
|
|
|
|
|
if err := r.db.GetClient().FindOne(ctx, &doc, filter); err != nil {
|
|
|
|
|
if errors.Is(err, mongodriver.ErrNoDocuments) {
|
|
|
|
|
return &domrepo.TOTPProfileRecord{}, nil
|
|
|
|
|
}
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
return memberToTOTPRecord(&doc), nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (r *MongoTOTPProfileRepository) Save(ctx context.Context, tenantID, uid string, rec *domrepo.TOTPProfileRecord) error {
|
|
|
|
|
set := bson.M{
|
|
|
|
|
member.BSONFieldTOTPEnrolled: rec.Enrolled,
|
|
|
|
|
member.BSONFieldTOTPSecretCipher: rec.SecretCipher,
|
|
|
|
|
member.BSONFieldTOTPBackupCodesHash: rec.BackupCodesHash,
|
|
|
|
|
member.BSONFieldTOTPEnrolledAt: rec.EnrolledAt,
|
|
|
|
|
member.BSONFieldUpdateAt: time.Now().UTC().UnixMilli(),
|
|
|
|
|
}
|
|
|
|
|
filter := bson.M{member.BSONFieldTenantID: tenantID, member.BSONFieldUID: uid}
|
2026-05-21 06:45:35 +00:00
|
|
|
res, err := r.db.GetClient().UpdateOne(ctx, filter, bson.M{bsonOpSet: set})
|
2026-05-20 23:51:22 +00:00
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
if res.MatchedCount == 0 {
|
|
|
|
|
return member.ErrNotFound
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (r *MongoTOTPProfileRepository) Clear(ctx context.Context, tenantID, uid string) error {
|
|
|
|
|
set := bson.M{
|
|
|
|
|
member.BSONFieldTOTPEnrolled: false,
|
|
|
|
|
member.BSONFieldTOTPSecretCipher: []byte{},
|
|
|
|
|
member.BSONFieldTOTPBackupCodesHash: []string{},
|
|
|
|
|
member.BSONFieldTOTPEnrolledAt: int64(0),
|
|
|
|
|
member.BSONFieldUpdateAt: time.Now().UTC().UnixMilli(),
|
|
|
|
|
}
|
|
|
|
|
filter := bson.M{member.BSONFieldTenantID: tenantID, member.BSONFieldUID: uid}
|
2026-05-21 06:45:35 +00:00
|
|
|
res, err := r.db.GetClient().UpdateOne(ctx, filter, bson.M{bsonOpSet: set})
|
2026-05-20 23:51:22 +00:00
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
if res.MatchedCount == 0 {
|
|
|
|
|
return member.ErrNotFound
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (r *MongoTOTPProfileRepository) ConsumeBackupCode(ctx context.Context, tenantID, uid, hash string) (bool, error) {
|
|
|
|
|
filter := bson.M{
|
|
|
|
|
member.BSONFieldTenantID: tenantID,
|
|
|
|
|
member.BSONFieldUID: uid,
|
|
|
|
|
member.BSONFieldTOTPBackupCodesHash: hash,
|
|
|
|
|
}
|
|
|
|
|
update := bson.M{
|
2026-05-21 06:45:35 +00:00
|
|
|
"$pull": bson.M{member.BSONFieldTOTPBackupCodesHash: hash},
|
|
|
|
|
bsonOpSet: bson.M{member.BSONFieldUpdateAt: time.Now().UTC().UnixMilli()},
|
2026-05-20 23:51:22 +00:00
|
|
|
}
|
|
|
|
|
res, err := r.db.GetClient().UpdateOne(ctx, filter, update)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return false, err
|
|
|
|
|
}
|
|
|
|
|
return res.ModifiedCount > 0, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (r *MongoTOTPProfileRepository) ReplaceBackupCodes(ctx context.Context, tenantID, uid string, hashes []string) error {
|
|
|
|
|
set := bson.M{
|
|
|
|
|
member.BSONFieldTOTPBackupCodesHash: hashes,
|
|
|
|
|
member.BSONFieldUpdateAt: time.Now().UTC().UnixMilli(),
|
|
|
|
|
}
|
|
|
|
|
filter := bson.M{member.BSONFieldTenantID: tenantID, member.BSONFieldUID: uid}
|
2026-05-21 06:45:35 +00:00
|
|
|
res, err := r.db.GetClient().UpdateOne(ctx, filter, bson.M{bsonOpSet: set})
|
2026-05-20 23:51:22 +00:00
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
if res.MatchedCount == 0 {
|
|
|
|
|
return member.ErrNotFound
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func memberToTOTPRecord(doc *entity.Member) *domrepo.TOTPProfileRecord {
|
|
|
|
|
if doc == nil {
|
|
|
|
|
return &domrepo.TOTPProfileRecord{}
|
|
|
|
|
}
|
|
|
|
|
return &domrepo.TOTPProfileRecord{
|
|
|
|
|
Enrolled: doc.TOTPEnrolled,
|
|
|
|
|
SecretCipher: append([]byte(nil), doc.TOTPSecretCipher...),
|
|
|
|
|
BackupCodesHash: append([]string(nil), doc.TOTPBackupCodesHash...),
|
|
|
|
|
EnrolledAt: doc.TOTPEnrolledAt,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var _ domrepo.TOTPProfileRepository = (*MongoTOTPProfileRepository)(nil)
|