package domain import ( "context" "errors" "fmt" "strings" "time" "net/mail" validate "backend/pkg/validate/common" "github.com/ethereum/go-ethereum/accounts" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/crypto" "github.com/google/uuid" ) var ( ErrUserNotFound = errors.New("user not found") ErrEmailInvalid = errors.New("email is invalid") ) type UserRepo interface { Create(ctx context.Context, user *User) error GetByID(ctx context.Context, id uuid.UUID) (*User, error) GetByPhone(ctx context.Context, phone string) (*User, error) GetByPubKey(ctx context.Context, pubKey string) (*User, error) Update(ctx context.Context, user *User) error Delete(ctx context.Context, id uuid.UUID) error } type User struct { ID uuid.UUID PubKey string Name string //TODO: add Kyc Embedded struct //TODO: add NFT Embedded struct LastName string PhoneNumber string Email *string NationalID string BirthDate *time.Time LastLogin *time.Time CreatedAt time.Time UpdatedAt time.Time DeletedAt *time.Time } // TODO: move to another file? type Challenge struct { Message uuid.UUID TimeStamp time.Time ExpiresAt time.Time } func (c *Challenge) IsExpired() bool { return time.Now().After(c.ExpiresAt) } func (c *Challenge) Verify(address string, signedMsg string) (bool, error) { if c.IsExpired() { return false, errors.New("challenge has expired") } signature, err := hexutil.Decode(signedMsg) if err != nil { return false, fmt.Errorf("decode signature: %w", err) } challengeBytes := []byte(c.Message.String()) return c.VerifySignedBytes(address, challengeBytes, signature) } func (c *Challenge) VerifySignedBytes(expectedAddress string, msg []byte, sig []byte) (bool, error) { msgHash := accounts.TextHash(msg) if len(sig) != 65 { return false, fmt.Errorf("invalid signature length: expected 65 bytes, got %d", len(sig)) } if sig[crypto.RecoveryIDOffset] == 27 || sig[crypto.RecoveryIDOffset] == 28 { sig[crypto.RecoveryIDOffset] -= 27 } recovered, err := crypto.SigToPub(msgHash, sig) if err != nil { return false, fmt.Errorf("failed to recover public key: %w", err) } recoveredAddr := crypto.PubkeyToAddress(*recovered) if !common.IsHexAddress(expectedAddress) { return false, fmt.Errorf("invalid Ethereum address format: %s", expectedAddress) } expectedAddr := common.HexToAddress(expectedAddress) return strings.EqualFold(expectedAddr.Hex(), recoveredAddr.Hex()), nil } func NewUser(pubKey, name, lastName, phoneNumber, email, nationalID string) (*User, error) { _, err := validate.IsValidPhone(phoneNumber) if err != nil { return nil, err } _, err = validate.IsValidNationalID(nationalID) if err != nil { return nil, err } _, err = validate.IsValidPublicKey(pubKey) if err != nil { return nil, err } return &User{ ID: uuid.New(), PubKey: pubKey, Name: name, LastName: lastName, PhoneNumber: phoneNumber, NationalID: nationalID, CreatedAt: time.Now().UTC(), UpdatedAt: time.Now().UTC(), }, nil } func (u *User) UpdateLastLogin() { now := time.Now().UTC() u.LastLogin = &now u.UpdatedAt = now } func (u *User) UpdateInfo(email string) { addr, err := mail.ParseAddress(email) if err == nil { u.Email = &addr.Address } u.UpdatedAt = time.Now().UTC() }