163 lines
3.7 KiB
Go

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)
GetOrCreateUserByPubKey(ctx context.Context, pubKey string) (*User, error)
Update(ctx context.Context, user *User) error
Delete(ctx context.Context, id uuid.UUID) error
}
type ChallengeRepo interface {
Create(ctx context.Context, pubKey string, challenge *Challenge) error
GetByPubKey(ctx context.Context, pubKey string) (*Challenge, error)
Delete(ctx context.Context, pubKey string) error
}
type KYCLevel int8
const (
KYCLevel0 KYCLevel = iota
KYCLevel1
KYCLevel2
)
type User struct {
ID uuid.UUID
PubKey string
Name string
KYCLevel KYCLevel
LastName string
PhoneNumber string
Email *string
NationalID string
BirthDate *time.Time
LastLogin *time.Time
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt *time.Time
}
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()
}
func (u *User) UpdateKYCLevel(level KYCLevel) {
u.KYCLevel = level
u.UpdatedAt = time.Now().UTC()
}
type Challenge struct {
Message string
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)
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
}