package accountapiimp import ( "context" "fmt" "os" "strings" "time" chaingatewayclient "github.com/tech/sendico/gateway/chain/client" api "github.com/tech/sendico/pkg/api/http" "github.com/tech/sendico/pkg/auth" "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/refreshtokens" "github.com/tech/sendico/pkg/db/transaction" "github.com/tech/sendico/pkg/domainprovider" "github.com/tech/sendico/pkg/merrors" "github.com/tech/sendico/pkg/messaging" "github.com/tech/sendico/pkg/mlogger" "github.com/tech/sendico/pkg/mservice" chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1" "github.com/tech/sendico/server/interface/accountservice" eapi "github.com/tech/sendico/server/interface/api" "github.com/tech/sendico/server/interface/services/fileservice" mutil "github.com/tech/sendico/server/internal/mutil/param" "go.mongodb.org/mongo-driver/bson/primitive" "go.uber.org/zap" ) type AccountAPI struct { logger mlogger.Logger db account.DB odb organization.DB tf transaction.Factory rtdb refreshtokens.DB plcdb policy.DB domain domainprovider.DomainProvider avatars mservice.MicroService producer messaging.Producer pmanager auth.Manager enf auth.Enforcer oph mutil.ParamHelper aph mutil.ParamHelper tph mutil.ParamHelper accountsPermissionRef primitive.ObjectID accService accountservice.AccountService chainGateway chainWalletClient chainAsset *chainv1.Asset } type chainWalletClient interface { CreateManagedWallet(ctx context.Context, req *chainv1.CreateManagedWalletRequest) (*chainv1.CreateManagedWalletResponse, error) Close() error } func (a *AccountAPI) Name() mservice.Type { return mservice.Accounts } func (a *AccountAPI) Finish(ctx context.Context) error { if err := a.avatars.Finish(ctx); err != nil { return err } if a.chainGateway != nil { if err := a.chainGateway.Close(); err != nil { a.logger.Warn("Failed to close chain gateway client", zap.Error(err)) } } return nil } func CreateAPI(a eapi.API) (*AccountAPI, error) { p := new(AccountAPI) p.logger = a.Logger().Named(p.Name()) var err error if p.db, err = a.DBFactory().NewAccountDB(); err != nil { p.logger.Error("Failed to create accounts database", zap.Error(err)) return nil, err } if p.rtdb, err = a.DBFactory().NewRefreshTokensDB(); err != nil { p.logger.Error("Failed to create refresh tokens database", zap.Error(err)) return nil, err } if p.odb, err = a.DBFactory().NewOrganizationDB(); err != nil { p.logger.Error("Failed to create organizations database", zap.Error(err)) return nil, err } if p.plcdb, err = a.DBFactory().NewPoliciesDB(); err != nil { p.logger.Error("Failed to create policies database", zap.Error(err)) return nil, err } p.domain = a.DomainProvider() p.producer = a.Register().Messaging().Producer() p.tf = a.DBFactory().TransactionFactory() p.pmanager = a.Permissions().Manager() p.enf = a.Permissions().Enforcer() p.oph = mutil.CreatePH(mservice.Organizations) p.aph = mutil.CreatePH(mservice.Accounts) p.tph = mutil.CreatePH("token") if p.accService, err = accountservice.NewAccountService(p.logger, a.DBFactory(), p.enf, p.pmanager.Role(), &a.Config().Mw.Password); err != nil { p.logger.Error("Failed to create account manager", zap.Error(err)) return nil, err } // Account related api endpoints a.Register().Handler(mservice.Accounts, "/signup", api.Post, p.signup) a.Register().Handler(mservice.Accounts, "/signup/availability", api.Get, p.signupAvailability) a.Register().AccountHandler(mservice.Accounts, "", api.Put, p.updateProfile) a.Register().AccountHandler(mservice.Accounts, "", api.Get, p.getProfile) a.Register().AccountHandler(mservice.Accounts, p.oph.AddRef("/employee"), api.Put, p.updateEmployee) a.Register().AccountHandler(mservice.Accounts, "/dzone", api.Get, p.dzone) a.Register().AccountHandler(mservice.Accounts, p.oph.AddRef("/profile"), api.Delete, p.deleteProfile) a.Register().AccountHandler(mservice.Accounts, p.oph.AddRef("/organization"), api.Delete, p.deleteOrganization) a.Register().AccountHandler(mservice.Accounts, p.oph.AddRef("/all"), api.Delete, p.deleteAll) a.Register().AccountHandler(mservice.Accounts, p.oph.AddRef("/list"), api.Get, p.getEmployees) a.Register().AccountHandler(mservice.Accounts, "/password", api.Post, p.checkPassword) a.Register().AccountHandler(mservice.Accounts, "/password", api.Patch, p.changePassword) a.Register().Handler(mservice.Accounts, "/password", api.Put, p.forgotPassword) a.Register().Handler(mservice.Accounts, p.tph.AddRef(p.aph.AddRef("/password/reset")), api.Post, p.resetPassword) a.Register().Handler(mservice.Accounts, mutil.AddToken("/verify"), api.Get, p.verify) a.Register().Handler(mservice.Accounts, "/email", api.Post, p.resendVerificationMail) a.Register().Handler(mservice.Accounts, "/email", api.Put, p.resendVerification) if p.avatars, err = fileservice.CreateAPI(a, p.Name()); err != nil { p.logger.Error("Failed to create image server", zap.Error(err)) return nil, err } accountsPolicy, err := a.Permissions().GetPolicyDescription(context.Background(), mservice.Accounts) if err != nil { p.logger.Warn("Failed to fetch account permission policy description", zap.Error(err)) return nil, err } p.accountsPermissionRef = accountsPolicy.ID cfg := a.Config() if cfg == nil { p.logger.Error("Failed to fetch service configuration") return nil, merrors.InvalidArgument("No configuration provided") } if err := p.initChainGateway(cfg.ChainGateway); err != nil { p.logger.Error("Failed to initialize chain gateway client", zap.Error(err)) return nil, err } return p, nil } func (a *AccountAPI) initChainGateway(cfg *eapi.ChainGatewayConfig) error { if cfg == nil { return merrors.InvalidArgument("chain gateway configuration is not provided") } cfg.Address = strings.TrimSpace(cfg.Address) if cfg.Address == "" { cfg.Address = strings.TrimSpace(os.Getenv(cfg.AddressEnv)) } if cfg.Address == "" { return merrors.InvalidArgument(fmt.Sprintf("chain gateway address is not specified and address env %s is empty", cfg.AddressEnv)) } clientCfg := chaingatewayclient.Config{ Address: cfg.Address, DialTimeout: time.Duration(cfg.DialTimeoutSeconds) * time.Second, CallTimeout: time.Duration(cfg.CallTimeoutSeconds) * time.Second, Insecure: cfg.Insecure, } client, err := chaingatewayclient.New(context.Background(), clientCfg) if err != nil { return err } asset, err := buildGatewayAsset(cfg.DefaultAsset) if err != nil { _ = client.Close() return err } a.chainGateway = client a.chainAsset = asset return nil } func buildGatewayAsset(cfg eapi.ChainGatewayAssetConfig) (*chainv1.Asset, error) { chain, err := parseChainNetwork(cfg.Chain) if err != nil { return nil, err } tokenSymbol := strings.TrimSpace(cfg.TokenSymbol) if tokenSymbol == "" { return nil, merrors.InvalidArgument("chain gateway token symbol is required") } return &chainv1.Asset{ Chain: chain, TokenSymbol: strings.ToUpper(tokenSymbol), ContractAddress: strings.ToLower(strings.TrimSpace(cfg.ContractAddress)), }, nil } func parseChainNetwork(value string) (chainv1.ChainNetwork, error) { switch strings.ToUpper(strings.TrimSpace(value)) { case "ETHEREUM_MAINNET", "CHAIN_NETWORK_ETHEREUM_MAINNET": return chainv1.ChainNetwork_CHAIN_NETWORK_ETHEREUM_MAINNET, nil case "ARBITRUM_ONE", "CHAIN_NETWORK_ARBITRUM_ONE": return chainv1.ChainNetwork_CHAIN_NETWORK_ARBITRUM_ONE, nil case "OTHER_EVM", "CHAIN_NETWORK_OTHER_EVM": return chainv1.ChainNetwork_CHAIN_NETWORK_OTHER_EVM, nil case "", "CHAIN_NETWORK_UNSPECIFIED": return chainv1.ChainNetwork_CHAIN_NETWORK_UNSPECIFIED, merrors.InvalidArgument("chain network must be specified") default: return chainv1.ChainNetwork_CHAIN_NETWORK_UNSPECIFIED, merrors.InvalidArgument(fmt.Sprintf("unsupported chain network %s", value)) } }