fixed verification code
All checks were successful
ci/woodpecker/push/billing_documents Pipeline was successful
ci/woodpecker/push/bff Pipeline was successful
ci/woodpecker/push/billing_fees Pipeline was successful
ci/woodpecker/push/discovery Pipeline was successful
ci/woodpecker/push/ledger Pipeline was successful
ci/woodpecker/push/notification Pipeline was successful
ci/woodpecker/push/fx_ingestor Pipeline was successful
ci/woodpecker/push/fx_oracle Pipeline was successful
ci/woodpecker/push/gateway_chain Pipeline was successful
ci/woodpecker/push/frontend Pipeline was successful
ci/woodpecker/push/gateway_mntx Pipeline was successful
ci/woodpecker/push/gateway_tgsettle Pipeline was successful
ci/woodpecker/push/gateway_tron Pipeline was successful
ci/woodpecker/push/payments_orchestrator Pipeline was successful

This commit is contained in:
Stephan D
2026-02-09 16:43:25 +01:00
83 changed files with 1331 additions and 415 deletions

View File

@@ -5,6 +5,7 @@ import (
"errors"
"fmt"
"slices"
"time"
"unicode"
"github.com/tech/sendico/pkg/auth"
@@ -13,13 +14,12 @@ import (
"github.com/tech/sendico/pkg/db/account"
"github.com/tech/sendico/pkg/db/organization"
"github.com/tech/sendico/pkg/db/policy"
"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/mlogger"
"github.com/tech/sendico/pkg/model"
"github.com/tech/sendico/pkg/mutil/mzap"
"github.com/tech/sendico/server/interface/middleware"
"github.com/tech/sendico/server/internal/mutil/flrstring"
"go.mongodb.org/mongo-driver/v2/bson"
"go.uber.org/zap"
)
@@ -31,9 +31,9 @@ type service struct {
enforcer auth.Enforcer
roleManager management.Role
config *middleware.PasswordConfig
tf transaction.Factory
policyDB policy.DB
vdb verification.DB
}
func validateUserRequest(u *model.Account) error {
@@ -112,7 +112,6 @@ func (s *service) ValidateAccount(acct *model.Account) error {
return err
}
acct.VerifyToken = flrstring.CreateRandString(s.config.TokenLength)
return nil
}
@@ -121,32 +120,50 @@ func (s *service) CreateAccount(
org *model.Organization,
acct *model.Account,
roleDescID bson.ObjectID,
) error {
) (string, error) {
if org == nil {
return merrors.InvalidArgument("Organization must not be nil")
return "", merrors.InvalidArgument("Organization must not be nil")
}
if acct == nil || len(acct.Login) == 0 {
return merrors.InvalidArgument("Account must have a non-empty login")
return "", merrors.InvalidArgument("Account must have a non-empty login")
}
if roleDescID == bson.NilObjectID {
return merrors.InvalidArgument("Role description must be provided")
return "", merrors.InvalidArgument("Role description must be provided")
}
// 1) Create the account
acct.Status = model.AccountPendingVerification
if err := s.accountDB.Create(ctx, acct); err != nil {
if errors.Is(err, merrors.ErrDataConflict) {
s.logger.Info("Username is already taken", zap.String("login", acct.Login))
} else {
s.logger.Warn("Failed to signup a user", zap.Error(err), zap.String("login", acct.Login))
}
return err
return "", err
}
// 2) Add to organization
if err := s.JoinOrganization(ctx, org, acct, roleDescID); err != nil {
s.logger.Warn("Failed to register new organization member", zap.Error(err), mzap.StorableRef(acct))
return err
return "", err
}
return nil
// 3) Issue verification token
return s.VerifyAccount(ctx, acct)
}
func (s *service) VerifyAccount(
ctx context.Context,
acct *model.Account,
) (verificationToken string, err error) {
verificationToken, err = s.vdb.Create(ctx, *acct.GetID(), model.PurposeAccountActivation, "", time.Duration(time.Hour*24))
if err != nil {
s.logger.Warn("Failed to create verification token for new account", zap.Error(err), mzap.StorableRef(acct))
return "", err
}
return verificationToken, nil
}
func (s *service) DeleteAccount(
@@ -213,20 +230,16 @@ func (s *service) RemoveAccountFromOrganization(
func (s *service) ResetPassword(
ctx context.Context,
acct *model.Account,
) error {
acct.ResetPasswordToken = flrstring.CreateRandString(s.config.TokenLength)
return s.accountDB.Update(ctx, acct)
) (string, error) {
return s.vdb.Create(ctx, *acct.GetID(), model.PurposePasswordReset, "", time.Duration(time.Hour*1))
}
func (s *service) UpdateLogin(
ctx context.Context,
acct *model.Account,
newLogin string,
) error {
acct.EmailBackup = acct.Login
acct.Login = newLogin
acct.VerifyToken = flrstring.CreateRandString(s.config.TokenLength)
return s.accountDB.Update(ctx, acct)
) (string, error) {
return s.vdb.Create(ctx, *acct.GetID(), model.PurposeEmailChange, newLogin, time.Duration(time.Hour*1))
}
func (s *service) JoinOrganization(
@@ -311,27 +324,19 @@ func (s *service) DeleteOrganization(
s.logger.Info("Starting organization deletion", mzap.StorableRef(org))
// Use transaction to ensure atomicity
_, err := s.tf.CreateTransaction().Execute(ctx, func(ctx context.Context) (any, error) {
// 8. Delete all roles and role descriptions in the organization
if err := s.deleteOrganizationRoles(ctx, org.ID); err != nil {
return nil, err
}
// 8. Delete all roles and role descriptions in the organization
if err := s.deleteOrganizationRoles(ctx, org.ID); err != nil {
return err
}
// 9. Delete all policies in the organization
if err := s.deleteOrganizationPolicies(ctx, org.ID); err != nil {
return nil, err
}
// 9. Delete all policies in the organization
if err := s.deleteOrganizationPolicies(ctx, org.ID); err != nil {
return err
}
// 10. Finally, delete the organization itself
if err := s.orgDB.Delete(ctx, bson.NilObjectID, org.ID); err != nil {
s.logger.Warn("Failed to delete organization", zap.Error(err), mzap.StorableRef(org))
return nil, err
}
return nil, nil
})
if err != nil {
s.logger.Error("Failed to delete organization", zap.Error(err), mzap.StorableRef(org))
// 10. Finally, delete the organization itself
if err := s.orgDB.Delete(ctx, bson.NilObjectID, org.ID); err != nil {
s.logger.Warn("Failed to delete organization", zap.Error(err), mzap.StorableRef(org))
return err
}
@@ -347,23 +352,14 @@ func (s *service) DeleteAll(
s.logger.Info("Starting complete deletion (organization + account)",
mzap.StorableRef(org), mzap.ObjRef("account_ref", accountRef))
// Use transaction to ensure atomicity
_, err := s.tf.CreateTransaction().Execute(ctx, func(ctx context.Context) (any, error) {
// 1. First delete the organization and all its data
if err := s.DeleteOrganization(ctx, org); err != nil {
return nil, err
}
// 1. First delete the organization and all its data
if err := s.DeleteOrganization(ctx, org); err != nil {
return err
}
// 2. Then delete the account
if err := s.accountDB.Delete(ctx, accountRef); err != nil {
s.logger.Warn("Failed to delete account", zap.Error(err), mzap.ObjRef("account_ref", accountRef))
return nil, err
}
return nil, nil
})
if err != nil {
s.logger.Error("Failed to delete all data", zap.Error(err), mzap.StorableRef(org), mzap.ObjRef("account_ref", accountRef))
// 2. Then delete the account
if err := s.accountDB.Delete(ctx, accountRef); err != nil {
s.logger.Warn("Failed to delete account", zap.Error(err), mzap.ObjRef("account_ref", accountRef))
return err
}
@@ -390,7 +386,6 @@ func NewAccountService(
enforcer: enforcer,
roleManager: ra,
config: config,
tf: dbf.TransactionFactory(),
}
var err error
if res.accountDB, err = dbf.NewAccountDB(); err != nil {
@@ -407,6 +402,9 @@ func NewAccountService(
logger.Warn("Failed to create policies database", zap.Error(err))
return nil, err
}
if res.vdb, err = dbf.NewVerificationsDB(); err != nil {
logger.Warn("Failed to create verification database", zap.Error(err))
return nil, err
}
return res, nil
}

View File

@@ -113,8 +113,6 @@ func TestValidateAccount(t *testing.T) {
// Password should be hashed after validation
assert.NotEqual(t, originalPassword, account.Password)
assert.NotEmpty(t, account.VerifyToken)
assert.Equal(t, config.TokenLength, len(account.VerifyToken))
})
t.Run("AccountMissingName", func(t *testing.T) {
@@ -245,54 +243,3 @@ func TestPasswordConfiguration(t *testing.T) {
})
}
// TestTokenGeneration verifies that verification tokens are generated with correct length
func TestTokenGeneration(t *testing.T) {
testCases := []struct {
name string
tokenLength int
}{
{"Short", 8},
{"Medium", 32},
{"Long", 64},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
config := &apiconfig.PasswordConfig{
Check: apiconfig.PasswordChecks{
MinLength: 8,
Digit: true,
Upper: true,
Lower: true,
Special: true,
},
TokenLength: tc.tokenLength,
}
logger := zap.NewNop() // Use no-op logger for tests
service := &service{
config: config,
logger: logger,
}
account := &model.Account{
AccountPublic: model.AccountPublic{
AccountBase: model.AccountBase{
Describable: model.Describable{
Name: "Test User",
},
},
UserDataBase: model.UserDataBase{
Login: "test@example.com",
},
},
Password: "TestPassword123!",
}
err := service.ValidateAccount(account)
require.NoError(t, err)
assert.Equal(t, tc.tokenLength, len(account.VerifyToken))
})
}
}

View File

@@ -35,7 +35,7 @@ type AccountService interface {
ResetPassword(
ctx context.Context,
acct *model.Account,
) error
) (verificationToken string, err error)
// CreateAccount will:
// 1) create the account
@@ -46,7 +46,12 @@ type AccountService interface {
org *model.Organization,
acct *model.Account,
roleDescID bson.ObjectID,
) error
) (verificationToken string, err error)
VerifyAccount(
ctx context.Context,
acct *model.Account,
) (verificationToken string, err error)
JoinOrganization(
ctx context.Context,
@@ -59,7 +64,7 @@ type AccountService interface {
ctx context.Context,
acct *model.Account,
newLogin string,
) error
) (verificationToken string, err error)
// DeleteAccount deletes the account and removes it from the org.
DeleteAccount(