diff --git a/backend/internal/interface/rest/controllers/participants.go b/backend/internal/interface/rest/controllers/participants.go index 702c08f..0a44eb8 100644 --- a/backend/internal/interface/rest/controllers/participants.go +++ b/backend/internal/interface/rest/controllers/participants.go @@ -17,6 +17,7 @@ import ( type ParticipantsController interface { List(w http.ResponseWriter, r *http.Request) ([]byte, error) + New(w http.ResponseWriter, r *http.Request) ([]byte, error) } type participantsController struct { @@ -92,3 +93,30 @@ func (c *participantsController) List(w http.ResponseWriter, r *http.Request) ([ return c.presenter.ResponseListParticipants(ctx, participants) } + +func (c *participantsController) New(w http.ResponseWriter, r *http.Request) ([]byte, error) { + req, err := presenters.CreateRequest[domain.AddEmployeeRequest](r) + if err != nil { + return nil, fmt.Errorf("error build list participants request. %w", err) + } + + organizationID, err := ctxmeta.OrganizationId(r.Context()) + if err != nil { + return nil, fmt.Errorf("error fetch organization id from context. %w", err) + } + + ctx, cancel := context.WithTimeout(r.Context(), 3*time.Second) + defer cancel() + + participant, err := c.orgInteractor.AddParticipant(ctx, organizations.AddParticipantParams{ + OrganizationID: organizationID, + Name: req.Name, + Position: req.Position, + WalletAddress: req.WalletAddress, + }) + if err != nil { + return nil, fmt.Errorf("error create new participant. %w", err) + } + + return c.presenter.ResponseParticipant(ctx, participant) +} diff --git a/backend/internal/interface/rest/presenters/participants.go b/backend/internal/interface/rest/presenters/participants.go index aae3416..30056ca 100644 --- a/backend/internal/interface/rest/presenters/participants.go +++ b/backend/internal/interface/rest/presenters/participants.go @@ -16,6 +16,10 @@ type ParticipantsPresenter interface { ctx context.Context, participants []models.OrganizationParticipant, ) ([]byte, error) + ResponseParticipant( + ctx context.Context, + participant models.OrganizationParticipant, + ) ([]byte, error) } type participantsPresenter struct{} @@ -116,3 +120,20 @@ func (p *participantsPresenter) ResponseListParticipants( return out, nil } + +func (p *participantsPresenter) ResponseParticipant( + ctx context.Context, + participant models.OrganizationParticipant, +) ([]byte, error) { + r, err := p.responseParticipant(ctx, participant) + if err != nil { + return nil, fmt.Errorf("error map participant to hal resource. %w", err) + } + + out, err := json.Marshal(r) + if err != nil { + return nil, fmt.Errorf("error marshal organization create response. %w", err) + } + + return out, nil +} diff --git a/backend/internal/interface/rest/server.go b/backend/internal/interface/rest/server.go index 544ffd4..159f409 100644 --- a/backend/internal/interface/rest/server.go +++ b/backend/internal/interface/rest/server.go @@ -103,9 +103,7 @@ func (s *Server) buildRouter() { r.Post("/", s.handle(s.controllers.Organizations.NewOrganization, "new_organization")) r.Route("/{organization_id}", func(r chi.Router) { - // r.Put("/", s.handle(s.controllers.Organizations.NewOrganization, "update_organization")) - // r.Delete("/", s.handle(s.controllers.Organizations.NewOrganization, "delete_organization")) - + // Deprecated?? r.Route("/transactions", func(r chi.Router) { r.Get("/", s.handle(s.controllers.Transactions.List, "tx_list")) r.Post("/", s.handle(s.controllers.Transactions.New, "new_tx")) @@ -115,8 +113,19 @@ func (s *Server) buildRouter() { ) }) - r.Route("/payout", func(r chi.Router) { - r.Post("/", nil) + r.Route("/payrolls", func(r chi.Router) { + r.Get("/", nil) // list payrolls + r.Post("/", nil) // deploy contract + }) + + r.Route("/multisig", func(r chi.Router) { + r.Post("/", nil) // new multisig (deploy) + r.Get("/", nil) // list + }) + + r.Route("/license", func(r chi.Router) { + r.Get("/", nil) // list license + r.Post("/", nil) // deploy contract }) // join via invite link @@ -124,19 +133,21 @@ func (s *Server) buildRouter() { r.Route("/participants", func(r chi.Router) { r.Get("/", s.handle(s.controllers.Participants.List, "participants_list")) + r.Post("/", nil) - r.Put("/{participant_id}", nil) // update user / employee - r.Delete("/{participant_id}", nil) // remove user / employee + // generate new invite link + r.Post("/invite", s.handle(s.controllers.Auth.Invite, "invite")) - r.Post("/", nil) // add {employee} - - r.Post("/invite", s.handle(s.controllers.Auth.Invite, "invite")) // generate new invite link - }) - - r.Route("/multisig", func(r chi.Router) { - r.Post("/", nil) // new multisig - r.Get("/", nil) // list + r.Route("/{participant_id}", func(r chi.Router) { + r.Put("/", nil) // update user / employee + r.Delete("/", nil) // remove user / employee + r.Route("/payroll", func(r chi.Router) { + r.Post("/", nil) // set salary + r.Put("/", nil) // edit + r.Get("/", nil) + }) + }) }) }) }) diff --git a/backend/internal/pkg/config/config.go b/backend/internal/pkg/config/config.go index 056f75f..c72e35a 100644 --- a/backend/internal/pkg/config/config.go +++ b/backend/internal/pkg/config/config.go @@ -1,10 +1,10 @@ package config type Config struct { - Common CommonConfig - Rest RestConfig - DB DBConfig - Eth EthConfig + Common CommonConfig + Rest RestConfig + DB DBConfig + ChainAPI ChainAPIConfig } type CommonConfig struct { @@ -35,6 +35,6 @@ type DBConfig struct { CacheSecret string } -type EthConfig struct { - // todo +type ChainAPIConfig struct { + Host string } diff --git a/backend/internal/usecase/interactors/chain/chain.go b/backend/internal/usecase/interactors/chain/chain.go index d5a4798..f7bcc6e 100644 --- a/backend/internal/usecase/interactors/chain/chain.go +++ b/backend/internal/usecase/interactors/chain/chain.go @@ -1,24 +1,128 @@ package chain import ( + "bytes" "context" + "crypto/ecdsa" + "encoding/json" + "fmt" + "log/slog" + "net/http" + "github.com/emochka2007/block-accounting/internal/pkg/config" + "github.com/emochka2007/block-accounting/internal/pkg/models" "github.com/emochka2007/block-accounting/internal/usecase/interactors/users" "github.com/emochka2007/block-accounting/internal/usecase/repository/transactions" + "github.com/ethereum/go-ethereum/common" ) type ChainInteractor interface { + NewMultisig(ctx context.Context, params NewMultisigParams) error } type chainInteractor struct { + log *slog.Logger + config config.Config txRepository transactions.Repository usersInteractor users.UsersInteractor } +func NewChainInteractor( + log *slog.Logger, + config config.Config, + txRepository transactions.Repository, + usersInteractor users.UsersInteractor, +) ChainInteractor { + return &chainInteractor{ + log: log, + config: config, + txRepository: txRepository, + usersInteractor: usersInteractor, + } +} + type NewMultisigParams struct { - OwnersPKs []string + OwnersPKs []string + Confirmations int } -func (i *chainInteractor) NewMultisig(ctx context.Context) { +func (i *chainInteractor) NewMultisig(ctx context.Context, params NewMultisigParams) error { + deployAddr := i.config.ChainAPI.Host + "/multi-sig/deploy" + i.log.Debug( + "deploy multisig", + slog.String("endpoint", deployAddr), + slog.Any("params", params), + ) + + requestBody, err := json.Marshal(map[string]any{ + "owners": params.OwnersPKs, + "confirmations": params.Confirmations, + }) + if err != nil { + return fmt.Errorf("error marshal request body. %w", err) + } + + body := bytes.NewBuffer(requestBody) + + doneCh := make(chan struct{}) + + errCh := make(chan error) + + go func() { + resp, err := http.Post(http.MethodPost, deployAddr, body) + if err != nil { + i.log.Error( + "error send deploy multisig request", + slog.String("endpoint", deployAddr), + slog.Any("params", params), + ) + + errCh <- fmt.Errorf("error build new multisig request. %w", err) + return + } + + defer resp.Body.Close() + + i.log.Debug( + "deploy multisig response", + slog.Int("code", resp.StatusCode), + ) + + if _, ok := <-doneCh; ok { + doneCh <- struct{}{} + } + }() + + select { + case err := <-errCh: + return err + case <-doneCh: + return nil + case <-ctx.Done(): + return ctx.Err() + } +} + +func (i *chainInteractor) PubKey(ctx context.Context, user *models.User) (*ecdsa.PublicKey, error) { + hex := common.Bytes2Hex(user.Seed()) + + pubAddr := i.config.ChainAPI.Host + "/address/" + hex + + doneCh := make(chan struct{}) + + errCh := make(chan error) + + go func() { + resp, err := http.Get(pubAddr) + }() + + select { + case err := <-errCh: + return err + case <-doneCh: + return nil + case <-ctx.Done(): + return ctx.Err() + } } diff --git a/backend/internal/usecase/interactors/organizations/interactor.go b/backend/internal/usecase/interactors/organizations/interactor.go index 75267e6..947d123 100644 --- a/backend/internal/usecase/interactors/organizations/interactor.go +++ b/backend/internal/usecase/interactors/organizations/interactor.go @@ -15,6 +15,7 @@ import ( "github.com/emochka2007/block-accounting/internal/pkg/models" "github.com/emochka2007/block-accounting/internal/usecase/repository/cache" "github.com/emochka2007/block-accounting/internal/usecase/repository/organizations" + "github.com/ethereum/go-ethereum/common" "github.com/google/uuid" ) @@ -55,22 +56,12 @@ type ParticipantsParams struct { } type OrganizationsInteractor interface { - Create( - ctx context.Context, - params CreateParams, - ) (*models.Organization, error) - List( - ctx context.Context, - params ListParams, - ) (*ListResponse, error) - Participant( - ctx context.Context, - params ParticipantParams, - ) (models.OrganizationParticipant, error) - Participants( - ctx context.Context, - params ParticipantsParams, - ) ([]models.OrganizationParticipant, error) + Create(ctx context.Context, params CreateParams) (*models.Organization, error) + List(ctx context.Context, params ListParams) (*ListResponse, error) + + Participant(ctx context.Context, params ParticipantParams) (models.OrganizationParticipant, error) + Participants(ctx context.Context, params ParticipantsParams) ([]models.OrganizationParticipant, error) + AddParticipant(ctx context.Context, params AddParticipantParams) (models.OrganizationParticipant, error) } type organizationsInteractor struct { @@ -308,3 +299,57 @@ func (i *organizationsInteractor) Participants( return participants, nil } + +type AddParticipantParams struct { + OrganizationID uuid.UUID + EmployeeUserID uuid.UUID + Name string + Position string + WalletAddress string +} + +func (i *organizationsInteractor) AddParticipant( + ctx context.Context, + params AddParticipantParams, +) (models.OrganizationParticipant, error) { + user, err := ctxmeta.User(ctx) + if err != nil { + return nil, fmt.Errorf("error fetch user from context. %w", err) + } + + actor, err := i.Participant(ctx, ParticipantParams{ + ID: user.Id(), + OrganizationID: params.OrganizationID, + ActiveOnly: true, + UsersOnly: true, + }) + if err != nil { + return nil, fmt.Errorf("error fetch actor. %w", err) + } + + if !actor.IsOwner() { + return nil, fmt.Errorf("error actor not an owner") + } + + if !common.IsHexAddress(params.WalletAddress) { + return nil, fmt.Errorf("error invalid address") + } + + participantID := uuid.Must(uuid.NewV7()) + + empl := models.Employee{ + ID: participantID, + EmployeeName: params.Name, + UserID: params.EmployeeUserID, + OrganizationId: params.OrganizationID, + WalletAddress: common.FromHex(params.WalletAddress), + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + } + + if err = i.orgRepository.AddEmployee(ctx, empl); err != nil { + return nil, fmt.Errorf("error add new employee. %w", err) + } + + return &empl, nil +} diff --git a/backend/internal/usecase/interactors/users/interactor.go b/backend/internal/usecase/interactors/users/interactor.go index 5c3e1bc..ccac58a 100644 --- a/backend/internal/usecase/interactors/users/interactor.go +++ b/backend/internal/usecase/interactors/users/interactor.go @@ -87,6 +87,10 @@ func (i *usersInteractor) Create(ctx context.Context, params CreateParams) (*mod Telegram: params.Tg, } + // TODO fetch user PK from chain-api + + user.PK = []byte{0x01} + if err = i.usersRepo.Create(ctx, user); err != nil { return nil, fmt.Errorf("error create new user. %w", err) } diff --git a/backend/internal/usecase/repository/organizations/repository.go b/backend/internal/usecase/repository/organizations/repository.go index 70f53ea..460a3e8 100644 --- a/backend/internal/usecase/repository/organizations/repository.go +++ b/backend/internal/usecase/repository/organizations/repository.go @@ -61,6 +61,7 @@ type Repository interface { Participants(ctx context.Context, params ParticipantsParams) ([]models.OrganizationParticipant, error) CreateAndAdd(ctx context.Context, org models.Organization, user *models.User) error DeleteParticipant(ctx context.Context, params DeleteParticipantParams) error + AddEmployee(ctx context.Context, employee models.Employee) error } type repositorySQL struct { @@ -650,3 +651,41 @@ func (r *repositorySQL) fetchEmployees( return employees, nil } + +func (r *repositorySQL) AddEmployee(ctx context.Context, employee models.Employee) error { + if err := sqltools.Transaction(ctx, r.db, func(ctx context.Context) error { + query := sq.Insert("employees").Columns( + "id", + "user_id", + "organization_id", + "wallet_address", + "created_at", + "updated_at", + ).Values( + employee.ID, + employee.UserID, + employee.OrganizationId, + employee.WalletAddress, + employee.CreatedAt, + employee.UpdatedAt, + ) + + if _, err := query.RunWith(r.Conn(ctx)).ExecContext(ctx); err != nil { + return fmt.Errorf("error add employee. %w", err) + } + + if err := r.AddParticipant(ctx, AddParticipantParams{ + OrganizationId: employee.OrganizationId, + UserId: employee.UserID, + EmployeeId: employee.ID, + }); err != nil { + return fmt.Errorf("error add employee to organization. %w", err) + } + + return nil + }); err != nil { + return err + } + + return nil +} diff --git a/backend/internal/usecase/repository/users/repository.go b/backend/internal/usecase/repository/users/repository.go index d20bc04..0fb05c8 100644 --- a/backend/internal/usecase/repository/users/repository.go +++ b/backend/internal/usecase/repository/users/repository.go @@ -50,7 +50,7 @@ func (r *repositorySQL) Get(ctx context.Context, params GetParams) ([]*models.Us var users []*models.User = make([]*models.User, 0, len(params.Ids)) if err := sqltools.Transaction(ctx, r.db, func(ctx context.Context) (err error) { - query := sq.Select("u.id, u.name, u.email, u.phone, u.tg, u.seed, u.created_at, u.activated_at"). + query := sq.Select("u.id, u.name, u.email, u.phone, u.tg, u.seed, u.created_at, u.activated_at, u.public_key"). From("users as u"). PlaceholderFormat(sq.Dollar) @@ -95,6 +95,7 @@ func (r *repositorySQL) Get(ctx context.Context, params GetParams) ([]*models.Us tg string seed []byte + pk []byte //isAdmin bool createdAt time.Time activatedAt sql.NullTime @@ -109,6 +110,7 @@ func (r *repositorySQL) Get(ctx context.Context, params GetParams) ([]*models.Us &seed, &createdAt, &activatedAt, + &pk, ); err != nil { return fmt.Errorf("error scan row. %w", err) } @@ -122,6 +124,7 @@ func (r *repositorySQL) Get(ctx context.Context, params GetParams) ([]*models.Us Telegram: tg, }, Bip39Seed: seed, + PK: pk, //Admin: isAdmin, CreatedAt: createdAt, Activated: activatedAt.Valid, @@ -138,7 +141,7 @@ func (r *repositorySQL) Get(ctx context.Context, params GetParams) ([]*models.Us func (r *repositorySQL) Create(ctx context.Context, user *models.User) error { if err := sqltools.Transaction(ctx, r.db, func(ctx context.Context) error { - columns := []string{"id", "name", "email", "phone", "tg", "seed", "created_at"} + columns := []string{"id", "name", "email", "phone", "tg", "seed", "public_key", "created_at"} values := []any{ user.ID, @@ -147,6 +150,7 @@ func (r *repositorySQL) Create(ctx context.Context, user *models.User) error { user.Credentails.Phone, user.Credentails.Telegram, user.Bip39Seed, + user.PK, user.CreatedAt, } diff --git a/backend/migrations/blockd.sql b/backend/migrations/blockd.sql index 74a97a8..f9135d2 100644 --- a/backend/migrations/blockd.sql +++ b/backend/migrations/blockd.sql @@ -158,7 +158,7 @@ create table multisigs ( create table multisig_owners ( multisig_id uuid references multisigs(id), - owner_id uuid references organizations_users(user_id), + owner_id uuid references users(id), created_at timestamp default current_timestamp, updated_at timestamp default current_timestamp, primary key (multisig_id, owner_id) @@ -166,7 +166,7 @@ create table multisig_owners ( create table multisig_confirmations ( multisig_id uuid references multisigs(id), - owner_id uuid references organizations_users(user_id), + owner_id uuid references users(id), created_at timestamp default current_timestamp, primary key (multisig_id, owner_id) ); \ No newline at end of file