mirror of
https://github.com/emo2007/block-accounting.git
synced 2025-01-18 15:36:27 +00:00
invite join implemented
This commit is contained in:
parent
c3854a7606
commit
4b85cdf811
@ -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 '{}'
|
||||
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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()
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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)
|
||||
|
@ -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) {
|
||||
|
@ -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}),
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -25,6 +25,8 @@ type CreateParams struct {
|
||||
Tg string
|
||||
Mnemonic string
|
||||
Activate bool
|
||||
Owner bool
|
||||
Admin bool
|
||||
}
|
||||
|
||||
type GetParams struct {
|
||||
|
@ -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 {
|
||||
|
Loading…
Reference in New Issue
Block a user