From 4b85cdf811d5f2dec8e4a3c612454d1a7736efad Mon Sep 17 00:00:00 2001 From: optclblast Date: Mon, 27 May 2024 21:08:19 +0300 Subject: [PATCH] invite join implemented --- backend/README.md | 37 ++++++++-- backend/internal/factory/interface.go | 2 + backend/internal/factory/wire_gen.go | 2 +- .../interface/rest/controllers/auth.go | 70 +++++++++++++++++-- .../rest/controllers/participants.go | 4 +- .../interface/rest/presenters/auth.go | 2 +- .../interface/rest/presenters/request.go | 2 +- backend/internal/pkg/logger/logger.go | 13 ++-- .../usecase/interactors/chain/chain.go | 17 +++-- .../interactors/organizations/interactor.go | 54 +++++++++++++- .../usecase/interactors/users/interactor.go | 2 + .../usecase/repository/auth/repository.go | 22 +++--- 12 files changed, 191 insertions(+), 36 deletions(-) diff --git a/backend/README.md b/backend/README.md index 16c8709..405a80b 100644 --- a/backend/README.md +++ b/backend/README.md @@ -384,7 +384,7 @@ curl --request POST \ Response: ``` json { - "Ok": true + "ok": true } ``` @@ -475,15 +475,43 @@ curl --request GET \ --url http://localhost:8081/invite/YR9vO4ZXYTgtIyi4aScsi6UZr0vNS74x9b8Y8SKF84g= ``` Response: -```bash +```json { - "Ok": true + "ok": true } ``` ## POST **/invite/{hash}/join** Join with invite link -// todo +### Request body +name (string) +credentials (email, phone, telegram) (optional, string) +mnemonic (string) +### Example +Request: +```bash +curl --request POST \ + --url 'http://localhost:8081/invite/RYPJ9HZfIM5vlRdaNhiDMsaVDPvQxylGVk$ZOaVFqyM=/join' \ + --header 'content-type: application/json' \ + --data '{ + "name": "ower", + "credentals": { + "email": "ower@gmail.com", + "phone": "+79999999999", + "telegram": "@ower" + }, + "mnemonic": "short orient camp maple lend pole balance token pledge fat analyst badge art happy property" +}' +``` +Response: +```json +{ + "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3MTY5MTgzMzk5OTEsInVpZCI6IjAxOGZiYjI4LTZkODgtNzg2NC04OWMxLTYzODYxNzU3NmFhNiJ9.zobQ9AAEUEHPz2BoetdtZDm5AfgUPCyCVS0JYNEYj5c", + "token_expired_at": 1716918339991, + "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3MTczNTAzMzk5OTEsInJ0X2hhc2giOiJzQStTZW42WDFUOVNVOGl1eVFhTy9sejJLSHNBMkpHcnlURDRHR3JQcTg1QUtLTE9XSWc0VTVEcFpXcjkvR1pqaDBGWGkvdWJYdHpIRzRCcUswV09jZz09IiwidWlkIjoiMDE4ZmJiMjgtNmQ4OC03ODY0LTg5YzEtNjM4NjE3NTc2YWE2In0.D0ZAHcJGH0Ga_nXLZojLBW8cMgTf8kNhcQbFVfKrGWs", + "refresh_token_expired_at": 1717350339991 +} +``` ## POST **/organizations/{organization_id}/participants/invite** Create new invite link @@ -495,7 +523,6 @@ Request: curl --request POST \ --url http://localhost:8081/organizations/018fb246-1616-7f1b-9fe2-1a3202224695/participants/invite \ --header 'Authorization: Bearer token' \ - --header 'X-Seed: a b c 1 2 3' \ --header 'accept: application/json' \ --header 'content-type: application/json' \ --data '{}' diff --git a/backend/internal/factory/interface.go b/backend/internal/factory/interface.go index 14fe5d8..4d4d210 100644 --- a/backend/internal/factory/interface.go +++ b/backend/internal/factory/interface.go @@ -69,6 +69,7 @@ func provideAuthController( authPresenter presenters.AuthPresenter, jwtInteractor jwt.JWTInteractor, repo auth.Repository, + orgInteractor organizations.OrganizationsInteractor, ) controllers.AuthController { return controllers.NewAuthController( log.WithGroup("auth-controller"), @@ -76,6 +77,7 @@ func provideAuthController( usersInteractor, jwtInteractor, repo, + orgInteractor, ) } diff --git a/backend/internal/factory/wire_gen.go b/backend/internal/factory/wire_gen.go index e630c1b..9ed2b0d 100644 --- a/backend/internal/factory/wire_gen.go +++ b/backend/internal/factory/wire_gen.go @@ -28,10 +28,10 @@ func ProvideService(c config.Config) (service.Service, func(), error) { authRepository := provideAuthRepository(db) jwtInteractor := provideJWTInteractor(c, usersInteractor, authRepository) authPresenter := provideAuthPresenter(jwtInteractor) - authController := provideAuthController(logger, usersInteractor, authPresenter, jwtInteractor, authRepository) client, cleanup2 := provideRedisConnection(c) cache := provideRedisCache(client, logger) organizationsInteractor := provideOrganizationsInteractor(logger, organizationsRepository, cache) + authController := provideAuthController(logger, usersInteractor, authPresenter, jwtInteractor, authRepository, organizationsInteractor) organizationsPresenter := provideOrganizationsPresenter() organizationsController := provideOrganizationsController(logger, organizationsInteractor, organizationsPresenter) transactionsInteractor := provideTxInteractor(logger, transactionsRepository, organizationsInteractor) diff --git a/backend/internal/interface/rest/controllers/auth.go b/backend/internal/interface/rest/controllers/auth.go index 6574606..751965a 100644 --- a/backend/internal/interface/rest/controllers/auth.go +++ b/backend/internal/interface/rest/controllers/auth.go @@ -17,8 +17,10 @@ import ( "github.com/emochka2007/block-accounting/internal/pkg/ctxmeta" "github.com/emochka2007/block-accounting/internal/pkg/hdwallet" "github.com/emochka2007/block-accounting/internal/usecase/interactors/jwt" + "github.com/emochka2007/block-accounting/internal/usecase/interactors/organizations" "github.com/emochka2007/block-accounting/internal/usecase/interactors/users" "github.com/emochka2007/block-accounting/internal/usecase/repository/auth" + "github.com/go-chi/chi/v5" ) var ( @@ -41,6 +43,7 @@ type authController struct { usersInteractor users.UsersInteractor jwtInteractor jwt.JWTInteractor repo auth.Repository + orgInteractor organizations.OrganizationsInteractor } func NewAuthController( @@ -49,6 +52,7 @@ func NewAuthController( usersInteractor users.UsersInteractor, jwtInteractor jwt.JWTInteractor, repo auth.Repository, + orgInteractor organizations.OrganizationsInteractor, ) AuthController { return &authController{ log: log, @@ -56,6 +60,7 @@ func NewAuthController( usersInteractor: usersInteractor, jwtInteractor: jwtInteractor, repo: repo, + orgInteractor: orgInteractor, } } @@ -81,6 +86,8 @@ func (c *authController) Join(w http.ResponseWriter, req *http.Request) ([]byte, Tg: request.Credentals.Telegram, Mnemonic: request.Mnemonic, Activate: true, + Owner: true, + Admin: true, }) if err != nil { return nil, fmt.Errorf("error create new user. %w", err) @@ -185,7 +192,7 @@ func (c *authController) Invite(w http.ResponseWriter, r *http.Request) ([]byte, strings.ReplaceAll( strings.ReplaceAll( base64.StdEncoding.EncodeToString(linkHash.Sum(nil)), - "/", "%", + "/", "$", ), "?", "@", ), @@ -217,11 +224,66 @@ func (c *authController) Invite(w http.ResponseWriter, r *http.Request) ([]byte, return c.presenter.ResponseNewInvite(ctx, organizationID, linkHashString) } -func (c *authController) JoinWithInvite(w http.ResponseWriter, req *http.Request) ([]byte, error) { +func (c *authController) JoinWithInvite(w http.ResponseWriter, r *http.Request) ([]byte, error) { + c.log.Debug("join with link request") - return nil, nil // implement + request, err := presenters.CreateRequest[domain.JoinRequest](r) + if err != nil { + return nil, fmt.Errorf("error create join request. %w", err) + } + + c.log.Debug("join with invite request", slog.Any("request", request)) + + if !bip39.IsMnemonicValid(request.Mnemonic) { + return nil, fmt.Errorf("error invalid mnemonic. %w", ErrorAuthInvalidMnemonic) + } + + hash := chi.URLParam(r, "hash") + + if hash == "" { + return nil, fmt.Errorf("error fetch invite hash from request") + } + + usedAt := time.Now() + + ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second) + defer cancel() + + organizationID, err := c.repo.MarkAsUsedLink(ctx, hash, usedAt) + if err != nil { + return nil, fmt.Errorf("error mark invite link as used. %w", err) + } + + user, err := c.usersInteractor.Create(ctx, users.CreateParams{ + Name: request.Name, + Email: request.Credentals.Email, + Phone: request.Credentals.Phone, + Tg: request.Credentals.Telegram, + Mnemonic: request.Mnemonic, + Activate: true, + }) + if err != nil { + return nil, fmt.Errorf("error create new user with invire link. %w", err) + } + + if err = c.orgInteractor.AddUser(ctx, organizations.AddUserParams{ + User: user, + OrganizationID: organizationID, + SkipRights: true, + }); err != nil { + c.log.Error( + "error add user into organization", + slog.String("organization id", organizationID.String()), + slog.String("user id", user.Id().String()), + slog.String("invire hash", hash), + ) + + return nil, fmt.Errorf("error add user into organization. %w", err) + } + + return c.presenter.ResponseJoin(user) } -func (c *authController) InviteGet(w http.ResponseWriter, req *http.Request) ([]byte, error) { +func (c *authController) InviteGet(w http.ResponseWriter, r *http.Request) ([]byte, error) { return presenters.ResponseOK() } diff --git a/backend/internal/interface/rest/controllers/participants.go b/backend/internal/interface/rest/controllers/participants.go index 6b436a5..108d245 100644 --- a/backend/internal/interface/rest/controllers/participants.go +++ b/backend/internal/interface/rest/controllers/participants.go @@ -105,10 +105,10 @@ func (c *participantsController) New(w http.ResponseWriter, r *http.Request) ([] return nil, fmt.Errorf("error fetch organization id from context. %w", err) } - ctx, cancel := context.WithTimeout(r.Context(), 3000*time.Second) + ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second) defer cancel() - participant, err := c.orgInteractor.AddParticipant(ctx, organizations.AddParticipantParams{ + participant, err := c.orgInteractor.AddEmployee(ctx, organizations.AddParticipantParams{ OrganizationID: organizationID, Name: req.Name, Position: req.Position, diff --git a/backend/internal/interface/rest/presenters/auth.go b/backend/internal/interface/rest/presenters/auth.go index 53ab719..b4e2c8a 100644 --- a/backend/internal/interface/rest/presenters/auth.go +++ b/backend/internal/interface/rest/presenters/auth.go @@ -93,7 +93,7 @@ func (p *authPresenter) ResponseNewInvite( link string, ) ([]byte, error) { out, err := json.Marshal(map[string]string{ - "link": "/" + organizationID.String() + "/invite/" + link, + "link": "/invite/" + link + "/join", }) if err != nil { return nil, fmt.Errorf("error marshal refresh response. %w", err) diff --git a/backend/internal/interface/rest/presenters/request.go b/backend/internal/interface/rest/presenters/request.go index 097d65e..e9710b3 100644 --- a/backend/internal/interface/rest/presenters/request.go +++ b/backend/internal/interface/rest/presenters/request.go @@ -25,7 +25,7 @@ func CreateRequest[T any](r *http.Request) (*T, error) { } type ok struct { - Ok bool + Ok bool `json:"ok"` } func ResponseOK() ([]byte, error) { diff --git a/backend/internal/pkg/logger/logger.go b/backend/internal/pkg/logger/logger.go index 135a991..8820e38 100644 --- a/backend/internal/pkg/logger/logger.go +++ b/backend/internal/pkg/logger/logger.go @@ -38,6 +38,10 @@ func (b *LoggerBuilder) WithSource() *LoggerBuilder { } func (b *LoggerBuilder) Build() *slog.Logger { + if len(b.writers) == 0 { + b.writers = append(b.writers, os.Stdout) + } + w := io.MultiWriter(b.writers...) if b.local { @@ -53,17 +57,12 @@ func (b *LoggerBuilder) Build() *slog.Logger { return slog.New(handler) } - return slog.New( - slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{ - Level: b.lvl, - AddSource: b.addSource, - }), - ) + return newLogger(b.lvl, w) } func newLogger(lvl slog.Level, w io.Writer) *slog.Logger { return slog.New( - slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: lvl}), + slog.NewJSONHandler(w, &slog.HandlerOptions{Level: lvl}), ) } diff --git a/backend/internal/usecase/interactors/chain/chain.go b/backend/internal/usecase/interactors/chain/chain.go index b9dbeab..c7df1b4 100644 --- a/backend/internal/usecase/interactors/chain/chain.go +++ b/backend/internal/usecase/interactors/chain/chain.go @@ -124,11 +124,15 @@ func (i *chainInteractor) NewMultisig(ctx context.Context, params NewMultisigPar return fmt.Errorf("error parse chain-api response body. %w", err) } - multisigAddress := common.Hex2Bytes(respObject.Address) + if respObject.Address == "" { + return fmt.Errorf("error multisig address is empty") + } + + multisigAddress := common.Hex2Bytes(respObject.Address[2:]) createdAt := time.Now() - if err := i.txRepository.AddMultisig(ctx, models.Multisig{ + msg := models.Multisig{ ID: uuid.Must(uuid.NewV7()), Title: params.Title, Address: multisigAddress, @@ -137,15 +141,20 @@ func (i *chainInteractor) NewMultisig(ctx context.Context, params NewMultisigPar ConfirmationsRequired: params.Confirmations, CreatedAt: createdAt, UpdatedAt: createdAt, - }); err != nil { - return fmt.Errorf("error add new multisig. %w", err) } i.log.Debug( "deploy multisig response", slog.Int("code", resp.StatusCode), + slog.String("body", string(raw)), + slog.Any("parsed", respObject), + slog.Any("multisig object", msg), ) + if err := i.txRepository.AddMultisig(ctx, msg); err != nil { + return fmt.Errorf("error add new multisig. %w", err) + } + return nil } diff --git a/backend/internal/usecase/interactors/organizations/interactor.go b/backend/internal/usecase/interactors/organizations/interactor.go index f88c856..84f1bfc 100644 --- a/backend/internal/usecase/interactors/organizations/interactor.go +++ b/backend/internal/usecase/interactors/organizations/interactor.go @@ -62,7 +62,8 @@ type OrganizationsInteractor interface { 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) + AddEmployee(ctx context.Context, params AddParticipantParams) (models.OrganizationParticipant, error) + AddUser(ctx context.Context, params AddUserParams) error } type organizationsInteractor struct { @@ -310,7 +311,7 @@ type AddParticipantParams struct { WalletAddress string } -func (i *organizationsInteractor) AddParticipant( +func (i *organizationsInteractor) AddEmployee( ctx context.Context, params AddParticipantParams, ) (models.OrganizationParticipant, error) { @@ -329,7 +330,7 @@ func (i *organizationsInteractor) AddParticipant( return nil, fmt.Errorf("error fetch actor. %w", err) } - if !actor.IsOwner() { + if !actor.IsAdmin() || !actor.IsOwner() { return nil, fmt.Errorf("error actor not an owner") } @@ -355,3 +356,50 @@ func (i *organizationsInteractor) AddParticipant( return &empl, nil } + +type AddUserParams struct { + User *models.User + IsAdmin bool + IsOwner bool + OrganizationID uuid.UUID + SkipRights bool +} + +func (i *organizationsInteractor) AddUser(ctx context.Context, params AddUserParams) error { + if !params.SkipRights { + user, err := ctxmeta.User(ctx) + if err != nil { + return 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 fmt.Errorf("error fetch actor. %w", err) + } + + if !actor.IsAdmin() || !actor.IsOwner() { + return fmt.Errorf("error actor not an owner") + } + } + + i.log.Debug( + "add user", + slog.Any("params", params), + ) + + if err := i.orgRepository.AddParticipant(ctx, organizations.AddParticipantParams{ + OrganizationId: params.OrganizationID, + UserId: params.User.Id(), + IsAdmin: params.IsAdmin, + IsOwner: params.IsOwner, + }); err != nil { + return fmt.Errorf("error add user into organization. %w", err) + } + + return nil +} diff --git a/backend/internal/usecase/interactors/users/interactor.go b/backend/internal/usecase/interactors/users/interactor.go index 6748089..575bee6 100644 --- a/backend/internal/usecase/interactors/users/interactor.go +++ b/backend/internal/usecase/interactors/users/interactor.go @@ -25,6 +25,8 @@ type CreateParams struct { Tg string Mnemonic string Activate bool + Owner bool + Admin bool } type GetParams struct { diff --git a/backend/internal/usecase/repository/auth/repository.go b/backend/internal/usecase/repository/auth/repository.go index 94eeb03..abd8826 100644 --- a/backend/internal/usecase/repository/auth/repository.go +++ b/backend/internal/usecase/repository/auth/repository.go @@ -63,7 +63,7 @@ type Repository interface { RefreshToken(ctx context.Context, params RefreshTokenParams) error AddInvite(ctx context.Context, params AddInviteParams) error - MarkAsUsedLink(ctx context.Context, linkHash string, usedAt time.Time) error + MarkAsUsedLink(ctx context.Context, linkHash string, usedAt time.Time) (uuid.UUID, error) } type repositorySQL struct { @@ -223,32 +223,38 @@ func (r *repositorySQL) MarkAsUsedLink( ctx context.Context, linkHash string, usedAt time.Time, -) error { - return sqltools.Transaction(ctx, r.db, func(ctx context.Context) error { - query := sq.Select("expired_at").From("invites").Where(sq.Eq{ +) (uuid.UUID, error) { + var orgID uuid.UUID + + if err := sqltools.Transaction(ctx, r.db, func(ctx context.Context) error { + query := sq.Select("organization_id", "expired_at").From("invites").Where(sq.Eq{ "link_hash": linkHash, }).Limit(1).PlaceholderFormat(sq.Dollar) var expAt time.Time - if err := query.RunWith(r.Conn(ctx)).QueryRowContext(ctx).Scan(&expAt); err != nil { + if err := query.RunWith(r.Conn(ctx)).QueryRowContext(ctx).Scan(&orgID, &expAt); err != nil { return fmt.Errorf("error fetch expiration date from database. %w", err) } - if expAt.After(time.Now()) { + if expAt.Before(time.Now()) { return ErrorInviteLinkExpired } updateQuery := sq.Update("invites").SetMap(sq.Eq{ "used_at": usedAt, - }) + }).PlaceholderFormat(sq.Dollar) if _, err := updateQuery.RunWith(r.Conn(ctx)).ExecContext(ctx); err != nil { return fmt.Errorf("error add invite link. %w", err) } return nil - }) + }); err != nil { + return uuid.Nil, err + } + + return orgID, nil } func NewRepository(db *sql.DB) Repository {