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:
``` 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 '{}'

View File

@ -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,
)
}

View File

@ -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)

View File

@ -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)
}
func (c *authController) InviteGet(w http.ResponseWriter, req *http.Request) ([]byte, error) {
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, r *http.Request) ([]byte, error) {
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)
}
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,

View File

@ -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)

View File

@ -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) {

View File

@ -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}),
)
}

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)
}
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
}

View File

@ -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
}

View File

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

View File

@ -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 {