invite join implemented

This commit is contained in:
r8zavetr8v 2024-05-27 21:08:19 +03:00
parent c3854a7606
commit 4b85cdf811
12 changed files with 191 additions and 36 deletions

View File

@ -384,7 +384,7 @@ curl --request POST \
Response: Response:
``` json ``` json
{ {
"Ok": true "ok": true
} }
``` ```
@ -475,15 +475,43 @@ curl --request GET \
--url http://localhost:8081/invite/YR9vO4ZXYTgtIyi4aScsi6UZr0vNS74x9b8Y8SKF84g= --url http://localhost:8081/invite/YR9vO4ZXYTgtIyi4aScsi6UZr0vNS74x9b8Y8SKF84g=
``` ```
Response: Response:
```bash ```json
{ {
"Ok": true "ok": true
} }
``` ```
## POST **/invite/{hash}/join** ## POST **/invite/{hash}/join**
Join with invite link 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** ## POST **/organizations/{organization_id}/participants/invite**
Create new invite link Create new invite link
@ -495,7 +523,6 @@ Request:
curl --request POST \ curl --request POST \
--url http://localhost:8081/organizations/018fb246-1616-7f1b-9fe2-1a3202224695/participants/invite \ --url http://localhost:8081/organizations/018fb246-1616-7f1b-9fe2-1a3202224695/participants/invite \
--header 'Authorization: Bearer token' \ --header 'Authorization: Bearer token' \
--header 'X-Seed: a b c 1 2 3' \
--header 'accept: application/json' \ --header 'accept: application/json' \
--header 'content-type: application/json' \ --header 'content-type: application/json' \
--data '{}' --data '{}'

View File

@ -69,6 +69,7 @@ func provideAuthController(
authPresenter presenters.AuthPresenter, authPresenter presenters.AuthPresenter,
jwtInteractor jwt.JWTInteractor, jwtInteractor jwt.JWTInteractor,
repo auth.Repository, repo auth.Repository,
orgInteractor organizations.OrganizationsInteractor,
) controllers.AuthController { ) controllers.AuthController {
return controllers.NewAuthController( return controllers.NewAuthController(
log.WithGroup("auth-controller"), log.WithGroup("auth-controller"),
@ -76,6 +77,7 @@ func provideAuthController(
usersInteractor, usersInteractor,
jwtInteractor, jwtInteractor,
repo, repo,
orgInteractor,
) )
} }

View File

@ -28,10 +28,10 @@ func ProvideService(c config.Config) (service.Service, func(), error) {
authRepository := provideAuthRepository(db) authRepository := provideAuthRepository(db)
jwtInteractor := provideJWTInteractor(c, usersInteractor, authRepository) jwtInteractor := provideJWTInteractor(c, usersInteractor, authRepository)
authPresenter := provideAuthPresenter(jwtInteractor) authPresenter := provideAuthPresenter(jwtInteractor)
authController := provideAuthController(logger, usersInteractor, authPresenter, jwtInteractor, authRepository)
client, cleanup2 := provideRedisConnection(c) client, cleanup2 := provideRedisConnection(c)
cache := provideRedisCache(client, logger) cache := provideRedisCache(client, logger)
organizationsInteractor := provideOrganizationsInteractor(logger, organizationsRepository, cache) organizationsInteractor := provideOrganizationsInteractor(logger, organizationsRepository, cache)
authController := provideAuthController(logger, usersInteractor, authPresenter, jwtInteractor, authRepository, organizationsInteractor)
organizationsPresenter := provideOrganizationsPresenter() organizationsPresenter := provideOrganizationsPresenter()
organizationsController := provideOrganizationsController(logger, organizationsInteractor, organizationsPresenter) organizationsController := provideOrganizationsController(logger, organizationsInteractor, organizationsPresenter)
transactionsInteractor := provideTxInteractor(logger, transactionsRepository, organizationsInteractor) transactionsInteractor := provideTxInteractor(logger, transactionsRepository, organizationsInteractor)

View File

@ -17,8 +17,10 @@ import (
"github.com/emochka2007/block-accounting/internal/pkg/ctxmeta" "github.com/emochka2007/block-accounting/internal/pkg/ctxmeta"
"github.com/emochka2007/block-accounting/internal/pkg/hdwallet" "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/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/interactors/users"
"github.com/emochka2007/block-accounting/internal/usecase/repository/auth" "github.com/emochka2007/block-accounting/internal/usecase/repository/auth"
"github.com/go-chi/chi/v5"
) )
var ( var (
@ -41,6 +43,7 @@ type authController struct {
usersInteractor users.UsersInteractor usersInteractor users.UsersInteractor
jwtInteractor jwt.JWTInteractor jwtInteractor jwt.JWTInteractor
repo auth.Repository repo auth.Repository
orgInteractor organizations.OrganizationsInteractor
} }
func NewAuthController( func NewAuthController(
@ -49,6 +52,7 @@ func NewAuthController(
usersInteractor users.UsersInteractor, usersInteractor users.UsersInteractor,
jwtInteractor jwt.JWTInteractor, jwtInteractor jwt.JWTInteractor,
repo auth.Repository, repo auth.Repository,
orgInteractor organizations.OrganizationsInteractor,
) AuthController { ) AuthController {
return &authController{ return &authController{
log: log, log: log,
@ -56,6 +60,7 @@ func NewAuthController(
usersInteractor: usersInteractor, usersInteractor: usersInteractor,
jwtInteractor: jwtInteractor, jwtInteractor: jwtInteractor,
repo: repo, repo: repo,
orgInteractor: orgInteractor,
} }
} }
@ -81,6 +86,8 @@ func (c *authController) Join(w http.ResponseWriter, req *http.Request) ([]byte,
Tg: request.Credentals.Telegram, Tg: request.Credentals.Telegram,
Mnemonic: request.Mnemonic, Mnemonic: request.Mnemonic,
Activate: true, Activate: true,
Owner: true,
Admin: true,
}) })
if err != nil { if err != nil {
return nil, fmt.Errorf("error create new user. %w", err) 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(
strings.ReplaceAll( strings.ReplaceAll(
base64.StdEncoding.EncodeToString(linkHash.Sum(nil)), 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) 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() return presenters.ResponseOK()
} }

View File

@ -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) 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() defer cancel()
participant, err := c.orgInteractor.AddParticipant(ctx, organizations.AddParticipantParams{ participant, err := c.orgInteractor.AddEmployee(ctx, organizations.AddParticipantParams{
OrganizationID: organizationID, OrganizationID: organizationID,
Name: req.Name, Name: req.Name,
Position: req.Position, Position: req.Position,

View File

@ -93,7 +93,7 @@ func (p *authPresenter) ResponseNewInvite(
link string, link string,
) ([]byte, error) { ) ([]byte, error) {
out, err := json.Marshal(map[string]string{ out, err := json.Marshal(map[string]string{
"link": "/" + organizationID.String() + "/invite/" + link, "link": "/invite/" + link + "/join",
}) })
if err != nil { if err != nil {
return nil, fmt.Errorf("error marshal refresh response. %w", err) return nil, fmt.Errorf("error marshal refresh response. %w", err)

View File

@ -25,7 +25,7 @@ func CreateRequest[T any](r *http.Request) (*T, error) {
} }
type ok struct { type ok struct {
Ok bool Ok bool `json:"ok"`
} }
func ResponseOK() ([]byte, error) { func ResponseOK() ([]byte, error) {

View File

@ -38,6 +38,10 @@ func (b *LoggerBuilder) WithSource() *LoggerBuilder {
} }
func (b *LoggerBuilder) Build() *slog.Logger { func (b *LoggerBuilder) Build() *slog.Logger {
if len(b.writers) == 0 {
b.writers = append(b.writers, os.Stdout)
}
w := io.MultiWriter(b.writers...) w := io.MultiWriter(b.writers...)
if b.local { if b.local {
@ -53,17 +57,12 @@ func (b *LoggerBuilder) Build() *slog.Logger {
return slog.New(handler) return slog.New(handler)
} }
return slog.New( return newLogger(b.lvl, w)
slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
Level: b.lvl,
AddSource: b.addSource,
}),
)
} }
func newLogger(lvl slog.Level, w io.Writer) *slog.Logger { func newLogger(lvl slog.Level, w io.Writer) *slog.Logger {
return slog.New( return slog.New(
slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: lvl}), slog.NewJSONHandler(w, &slog.HandlerOptions{Level: lvl}),
) )
} }

View File

@ -124,11 +124,15 @@ func (i *chainInteractor) NewMultisig(ctx context.Context, params NewMultisigPar
return fmt.Errorf("error parse chain-api response body. %w", err) 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() createdAt := time.Now()
if err := i.txRepository.AddMultisig(ctx, models.Multisig{ msg := models.Multisig{
ID: uuid.Must(uuid.NewV7()), ID: uuid.Must(uuid.NewV7()),
Title: params.Title, Title: params.Title,
Address: multisigAddress, Address: multisigAddress,
@ -137,15 +141,20 @@ func (i *chainInteractor) NewMultisig(ctx context.Context, params NewMultisigPar
ConfirmationsRequired: params.Confirmations, ConfirmationsRequired: params.Confirmations,
CreatedAt: createdAt, CreatedAt: createdAt,
UpdatedAt: createdAt, UpdatedAt: createdAt,
}); err != nil {
return fmt.Errorf("error add new multisig. %w", err)
} }
i.log.Debug( i.log.Debug(
"deploy multisig response", "deploy multisig response",
slog.Int("code", resp.StatusCode), 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 return nil
} }

View File

@ -62,7 +62,8 @@ type OrganizationsInteractor interface {
Participant(ctx context.Context, params ParticipantParams) (models.OrganizationParticipant, error) Participant(ctx context.Context, params ParticipantParams) (models.OrganizationParticipant, error)
Participants(ctx context.Context, params ParticipantsParams) ([]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 { type organizationsInteractor struct {
@ -310,7 +311,7 @@ type AddParticipantParams struct {
WalletAddress string WalletAddress string
} }
func (i *organizationsInteractor) AddParticipant( func (i *organizationsInteractor) AddEmployee(
ctx context.Context, ctx context.Context,
params AddParticipantParams, params AddParticipantParams,
) (models.OrganizationParticipant, error) { ) (models.OrganizationParticipant, error) {
@ -329,7 +330,7 @@ func (i *organizationsInteractor) AddParticipant(
return nil, fmt.Errorf("error fetch actor. %w", err) 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") return nil, fmt.Errorf("error actor not an owner")
} }
@ -355,3 +356,50 @@ func (i *organizationsInteractor) AddParticipant(
return &empl, nil 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
}

View File

@ -25,6 +25,8 @@ type CreateParams struct {
Tg string Tg string
Mnemonic string Mnemonic string
Activate bool Activate bool
Owner bool
Admin bool
} }
type GetParams struct { type GetParams struct {

View File

@ -63,7 +63,7 @@ type Repository interface {
RefreshToken(ctx context.Context, params RefreshTokenParams) error RefreshToken(ctx context.Context, params RefreshTokenParams) error
AddInvite(ctx context.Context, params AddInviteParams) 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 { type repositorySQL struct {
@ -223,32 +223,38 @@ func (r *repositorySQL) MarkAsUsedLink(
ctx context.Context, ctx context.Context,
linkHash string, linkHash string,
usedAt time.Time, usedAt time.Time,
) error { ) (uuid.UUID, error) {
return sqltools.Transaction(ctx, r.db, func(ctx context.Context) error { var orgID uuid.UUID
query := sq.Select("expired_at").From("invites").Where(sq.Eq{
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, "link_hash": linkHash,
}).Limit(1).PlaceholderFormat(sq.Dollar) }).Limit(1).PlaceholderFormat(sq.Dollar)
var expAt time.Time 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) return fmt.Errorf("error fetch expiration date from database. %w", err)
} }
if expAt.After(time.Now()) { if expAt.Before(time.Now()) {
return ErrorInviteLinkExpired return ErrorInviteLinkExpired
} }
updateQuery := sq.Update("invites").SetMap(sq.Eq{ updateQuery := sq.Update("invites").SetMap(sq.Eq{
"used_at": usedAt, "used_at": usedAt,
}) }).PlaceholderFormat(sq.Dollar)
if _, err := updateQuery.RunWith(r.Conn(ctx)).ExecContext(ctx); err != nil { if _, err := updateQuery.RunWith(r.Conn(ctx)).ExecContext(ctx); err != nil {
return fmt.Errorf("error add invite link. %w", err) return fmt.Errorf("error add invite link. %w", err)
} }
return nil return nil
}) }); err != nil {
return uuid.Nil, err
}
return orgID, nil
} }
func NewRepository(db *sql.DB) Repository { func NewRepository(db *sql.DB) Repository {