From 86bd1107ce03e1aa5f665ce2d1498e1c14f4de71 Mon Sep 17 00:00:00 2001 From: optclblast Date: Thu, 9 May 2024 12:41:57 +0300 Subject: [PATCH] login and tokens implemented --- backend/.vscode/launch.json | 2 +- backend/Makefile | 4 +- .../interface/rest/controllers/auth.go | 67 +++++++++++++------ .../rest/controllers/organization.go | 10 +++ backend/internal/interface/rest/domain/dto.go | 15 +---- backend/internal/interface/rest/errors.go | 20 ++++-- .../interface/rest/presenters/auth.go | 59 ++++++++-------- .../interface/rest/presenters/request.go | 25 +++++++ backend/internal/interface/rest/server.go | 37 +++++----- .../usecase/interactors/users/interactor.go | 20 +++++- .../usecase/repository/users/repository.go | 62 +++++++++++++++-- 11 files changed, 222 insertions(+), 99 deletions(-) create mode 100644 backend/internal/interface/rest/controllers/organization.go create mode 100644 backend/internal/interface/rest/presenters/request.go diff --git a/backend/.vscode/launch.json b/backend/.vscode/launch.json index 29f0023..073e203 100644 --- a/backend/.vscode/launch.json +++ b/backend/.vscode/launch.json @@ -16,7 +16,7 @@ "-log-add-source=true", "-jwt-secret=local_jwt_secret", - "-rest-address=localhost:8080", + "-rest-address=localhost:8081", "-db-host=localhost:8432", "-db-database=blockd", "-db-user=blockd", diff --git a/backend/Makefile b/backend/Makefile index 542ad58..da8c458 100644 --- a/backend/Makefile +++ b/backend/Makefile @@ -25,7 +25,7 @@ run.local: bin.build -log-level=debug \ -log-local=true \ -log-add-source=true \ - -rest-address=localhost:8080 \ + -rest-address=localhost:8081 \ -db-host=localhost:8432 \ -db-database=blockd \ -db-user=blockd \ @@ -39,7 +39,7 @@ run.debug: bin.build -log-level=debug \ -log-local=false \ -log-add-source=true \ - -rest-address=localhost:8080 \ + -rest-address=localhost:8081 \ -db-host=localhost:8432 \ -db-database=blockd \ -db-user=blockd \ diff --git a/backend/internal/interface/rest/controllers/auth.go b/backend/internal/interface/rest/controllers/auth.go index 3364ea2..ef9ce63 100644 --- a/backend/internal/interface/rest/controllers/auth.go +++ b/backend/internal/interface/rest/controllers/auth.go @@ -8,8 +8,10 @@ import ( "net/http" "time" + "github.com/emochka2007/block-accounting/internal/interface/rest/domain" "github.com/emochka2007/block-accounting/internal/interface/rest/presenters" "github.com/emochka2007/block-accounting/internal/pkg/bip32" + "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/users" ) @@ -45,19 +47,15 @@ func NewAuthController( } func (c *authController) Join(w http.ResponseWriter, req *http.Request) error { - request, err := c.presenter.CreateJoinRequest(req) + request, err := presenters.CreateRequest[domain.JoinRequest](req) if err != nil { - return c.presenter.ResponseJoin( - w, nil, fmt.Errorf("error create join request. %w", err), - ) + return fmt.Errorf("error create join request. %w", err) } c.log.Debug("join request", slog.String("mnemonic", request.Mnemonic)) if !bip32.IsMnemonicValid(request.Mnemonic) { - return c.presenter.ResponseJoin( - w, nil, fmt.Errorf("error invalid mnemonic. %w", ErrorAuthInvalidMnemonic), - ) + return fmt.Errorf("error invalid mnemonic. %w", ErrorAuthInvalidMnemonic) } ctx, cancel := context.WithTimeout(req.Context(), 5*time.Second) @@ -69,22 +67,53 @@ func (c *authController) Join(w http.ResponseWriter, req *http.Request) error { Activate: true, }) if err != nil { - return c.presenter.ResponseJoin(w, nil, fmt.Errorf("error create new user. %w", err)) + return fmt.Errorf("error create new user. %w", err) } - return c.presenter.ResponseJoin(w, user, nil) + c.log.Debug("join request", slog.String("user id", user.ID.String())) + + return c.presenter.ResponseJoin(w, user) +} + +// NIT: wrap with idempotent action handler +func (c *authController) Login(w http.ResponseWriter, req *http.Request) error { + request, err := presenters.CreateRequest[domain.LoginRequest](req) + if err != nil { + return fmt.Errorf("error create login request. %w", err) + } + + c.log.Debug("login request", slog.String("mnemonic", request.Mnemonic)) + + ctx, cancel := context.WithTimeout(req.Context(), 5*time.Second) + defer cancel() + + seed, err := hdwallet.NewSeedFromMnemonic(request.Mnemonic) + if err != nil { + return fmt.Errorf("error create seed from mnemonic. %w", err) + } + + users, err := c.usersInteractor.Get(ctx, users.GetParams{ + Seed: seed, + }) + if err != nil { + return fmt.Errorf("error fetch user by seed. %w", err) + } + + if len(users) == 0 { + return fmt.Errorf("error empty users set") + } + + c.log.Debug("login request", slog.String("user id", users[0].ID.String())) + + return c.presenter.ResponseLogin(w, users[0]) +} + +// const mnemonicEntropyBitSize int = 256 + +func (c *authController) Invite(w http.ResponseWriter, req *http.Request) error { + return nil } func (c *authController) JoinWithInvite(w http.ResponseWriter, req *http.Request) error { return nil // implement } - -func (c *authController) Login(w http.ResponseWriter, req *http.Request) error { - return nil // implement -} - -const mnemonicEntropyBitSize int = 256 - -func (c *authController) Invite(w http.ResponseWriter, req *http.Request) error { - return nil // implement -} diff --git a/backend/internal/interface/rest/controllers/organization.go b/backend/internal/interface/rest/controllers/organization.go new file mode 100644 index 0000000..e064db8 --- /dev/null +++ b/backend/internal/interface/rest/controllers/organization.go @@ -0,0 +1,10 @@ +package controllers + +import "log/slog" + +type OrganizationsController interface { +} + +type organizationsController struct { + log *slog.Logger +} diff --git a/backend/internal/interface/rest/domain/dto.go b/backend/internal/interface/rest/domain/dto.go index 3d246cd..48a22a5 100644 --- a/backend/internal/interface/rest/domain/dto.go +++ b/backend/internal/interface/rest/domain/dto.go @@ -10,24 +10,15 @@ type JoinRequest struct { } type JoinResponse struct { - Ok bool `json:"ok"` - Token string `json:"token,omitempty"` - Error *Error `json:"error,omitempty"` + Token string `json:"token"` } type LoginRequest struct { - Mnemonc string `json:"mnemonic"` + Mnemonic string `json:"mnemonic"` } type LoginResponse struct { - Ok bool `json:"ok"` - Token string `json:"token,omitempty"` - Error *Error `json:"error,omitempty"` -} - -type Error struct { - Code int `json:"code"` - Message string `json:"message"` + Token string `json:"token"` } func BuildRequest[T any](data []byte) (*T, error) { diff --git a/backend/internal/interface/rest/errors.go b/backend/internal/interface/rest/errors.go index 52b95a0..6cb801f 100644 --- a/backend/internal/interface/rest/errors.go +++ b/backend/internal/interface/rest/errors.go @@ -1,22 +1,28 @@ package rest -import "net/http" +import ( + "errors" + "net/http" + + "github.com/emochka2007/block-accounting/internal/interface/rest/controllers" +) type apiError struct { - Code int `json:"code"` - Error string `json:"error"` + Code int `json:"code"` + Message string `json:"message"` } func buildApiError(code int, message string) apiError { return apiError{ - Code: code, - Error: message, + Code: code, + Message: message, } } -func mapError(_ error) apiError { - // todo map typed errors +func mapError(err error) apiError { switch { + case errors.Is(err, controllers.ErrorAuthInvalidMnemonic): + return buildApiError(http.StatusBadRequest, "Invalid Mnemonic") default: return buildApiError(http.StatusInternalServerError, "Internal Server Error") } diff --git a/backend/internal/interface/rest/presenters/auth.go b/backend/internal/interface/rest/presenters/auth.go index f3a95c6..03d987c 100644 --- a/backend/internal/interface/rest/presenters/auth.go +++ b/backend/internal/interface/rest/presenters/auth.go @@ -3,7 +3,6 @@ package presenters import ( "encoding/json" "fmt" - "io" "net/http" "time" @@ -13,8 +12,8 @@ import ( ) type AuthPresenter interface { - CreateJoinRequest(r *http.Request) (*domain.JoinRequest, error) - ResponseJoin(w http.ResponseWriter, user *models.User, err error) error + ResponseJoin(w http.ResponseWriter, user *models.User) error + ResponseLogin(w http.ResponseWriter, user *models.User) error } type authPresenter struct { @@ -29,38 +28,16 @@ func NewAuthPresenter( } } -func (p *authPresenter) CreateJoinRequest(r *http.Request) (*domain.JoinRequest, error) { - defer r.Body.Close() - - data, err := io.ReadAll(r.Body) - if err != nil { - return nil, fmt.Errorf("error read request body. %w", err) - } - - var request domain.JoinRequest - - if err := json.Unmarshal(data, &request); err != nil { - return nil, fmt.Errorf("error unmarshal join request. %w", err) - } - - return &request, nil -} - -func (p *authPresenter) ResponseJoin(w http.ResponseWriter, user *models.User, err error) error { +func (p *authPresenter) ResponseJoin(w http.ResponseWriter, user *models.User) error { resp := new(domain.JoinResponse) + token, err := p.jwtInteractor.NewToken(user, 24*time.Hour) if err != nil { - // todo map error - } else { - token, err := p.jwtInteractor.NewToken(user, 24*time.Hour) - if err != nil { - return fmt.Errorf("error create access token. %w", err) - } - - resp.Ok = true - resp.Token = token + return fmt.Errorf("error create access token. %w", err) } + resp.Token = token + out, err := json.Marshal(resp) if err != nil { return fmt.Errorf("error marshal join response. %w", err) @@ -72,3 +49,25 @@ func (p *authPresenter) ResponseJoin(w http.ResponseWriter, user *models.User, e return nil } + +func (p *authPresenter) ResponseLogin(w http.ResponseWriter, user *models.User) error { + resp := new(domain.LoginResponse) + + token, err := p.jwtInteractor.NewToken(user, 24*time.Hour) + if err != nil { + return fmt.Errorf("error create access token. %w", err) + } + + resp.Token = token + + out, err := json.Marshal(resp) + if err != nil { + return fmt.Errorf("error marshal login response. %w", err) + } + + if _, err = w.Write(out); err != nil { + return fmt.Errorf("error write response. %w", err) + } + + return nil +} diff --git a/backend/internal/interface/rest/presenters/request.go b/backend/internal/interface/rest/presenters/request.go new file mode 100644 index 0000000..a3a748b --- /dev/null +++ b/backend/internal/interface/rest/presenters/request.go @@ -0,0 +1,25 @@ +package presenters + +import ( + "encoding/json" + "fmt" + "io" + "net/http" +) + +func CreateRequest[T any](r *http.Request) (*T, error) { + defer r.Body.Close() + + data, err := io.ReadAll(r.Body) + if err != nil { + return nil, fmt.Errorf("error read request body. %w", err) + } + + var request T + + if err := json.Unmarshal(data, &request); err != nil { + return nil, fmt.Errorf("error unmarshal join request. %w", err) + } + + return &request, nil +} diff --git a/backend/internal/interface/rest/server.go b/backend/internal/interface/rest/server.go index 0fbd4c0..8cba389 100644 --- a/backend/internal/interface/rest/server.go +++ b/backend/internal/interface/rest/server.go @@ -84,11 +84,11 @@ func (s *Server) buildRouter() { s.Use(s.handleMw) s.Use(render.SetContentType(render.ContentTypeJSON)) - s.Get("/ping", s.handlePing) // debug + s.Get("/ping", s.handle(s.controllers.Ping.Ping, "ping")) // auth - s.Post("/join", s.handle(s.handleJoin, "join")) // new user - s.Post("/login", nil) // login + s.Post("/join", s.handle(s.controllers.Auth.Join, "join")) + s.Post("/login", s.handle(s.controllers.Auth.Login, "login")) s.Route("/organization/{organization_id}", func(r chi.Router) { s.Route("/transactions", func(r chi.Router) { @@ -110,7 +110,10 @@ func (s *Server) buildRouter() { } -func (s *Server) handle(h http.HandlerFunc, method_name string) http.HandlerFunc { +func (s *Server) handle( + h func(w http.ResponseWriter, req *http.Request) error, + method_name string, +) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { started := time.Now() defer func() { @@ -122,11 +125,17 @@ func (s *Server) handle(h http.HandlerFunc, method_name string) http.HandlerFunc "method_name": method_name, }, ) - - metrics.RequestsAccepted.Add(1) }() - h(w, r) + if err := h(w, r); err != nil { + s.log.Error( + "http error", + slog.String("method_name", method_name), + logger.Err(err), + ) + + s.responseError(w, err) + } } } @@ -162,17 +171,3 @@ func (s *Server) handleMw(next http.Handler) http.Handler { return http.HandlerFunc(fn) } - -func (s *Server) handleJoin(w http.ResponseWriter, req *http.Request) { - if err := s.controllers.Auth.Join(w, req); err != nil { - s.responseError(w, err) - } -} - -func (s *Server) handlePing(w http.ResponseWriter, req *http.Request) { - s.log.Debug("ping request") - - if err := s.controllers.Ping.Ping(w, req); err != nil { - s.responseError(w, err) - } -} diff --git a/backend/internal/usecase/interactors/users/interactor.go b/backend/internal/usecase/interactors/users/interactor.go index 6d6fd90..96908ce 100644 --- a/backend/internal/usecase/interactors/users/interactor.go +++ b/backend/internal/usecase/interactors/users/interactor.go @@ -2,6 +2,7 @@ package users import ( "context" + "errors" "fmt" "log/slog" "time" @@ -12,6 +13,10 @@ import ( "github.com/google/uuid" ) +var ( + ErrorUsersNotFound = errors.New("users not found") +) + type CreateParams struct { Mnemonic string IsAdmin bool @@ -88,7 +93,20 @@ func (i *usersInteractor) Activate(ctx context.Context, params ActivateParams) e } func (i *usersInteractor) Get(ctx context.Context, params GetParams) ([]*models.User, error) { - return nil, nil + users, err := i.usersRepo.Get(ctx, users.GetParams{ + Ids: params.Ids, + OrganizationId: params.OrganizationId, + Seed: params.Seed, + }) + if err != nil { + return nil, fmt.Errorf("error fetch users from repository. %w", err) + } + + if len(users) == 0 { + return nil, fmt.Errorf("error empty users set. %w", ErrorUsersNotFound) + } + + return users, nil } func (i *usersInteractor) Delete(ctx context.Context, params DeleteParams) error { diff --git a/backend/internal/usecase/repository/users/repository.go b/backend/internal/usecase/repository/users/repository.go index 708ed34..eef530f 100644 --- a/backend/internal/usecase/repository/users/repository.go +++ b/backend/internal/usecase/repository/users/repository.go @@ -3,6 +3,7 @@ package users import ( "context" "database/sql" + "errors" "fmt" "time" @@ -14,13 +15,13 @@ import ( type GetParams struct { Ids uuid.UUIDs - OrganizationId uuid.UUIDs + OrganizationId uuid.UUID Seed []byte } // todo implement type Repository interface { - Get(ctx context.Context, params GetParams) (*models.User, error) + Get(ctx context.Context, params GetParams) ([]*models.User, error) Create(ctx context.Context, user *models.User) error Activate(ctx context.Context, id uuid.UUID) error Update(ctx context.Context, user *models.User) error @@ -45,16 +46,65 @@ func (s *repositorySQL) Conn(ctx context.Context) sqltools.DBTX { return s.db } -func (r *repositorySQL) Get(ctx context.Context, params GetParams) (*models.User, error) { - var user *models.User +func (r *repositorySQL) Get(ctx context.Context, params GetParams) ([]*models.User, error) { + 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("id, seed, created_at, activated_at"). + From("users"). + PlaceholderFormat(sq.Dollar) + + if len(params.Ids) > 0 { + query = query.Where("id", params.Ids) + } + + // if params.OrganizationId != uuid.Nil { + // // todo join org users + // } + + if params.Seed != nil { + query = query.Where("seed = ?", params.Seed) + } + + rows, err := query.RunWith(r.Conn(ctx)).QueryContext(ctx) + if err != nil { + return fmt.Errorf("error fetch data from database. %w", err) + } + + defer func() { + if cErr := rows.Close(); cErr != nil { + err = errors.Join(fmt.Errorf("error close database rows. %w", cErr), err) + } + }() + + for rows.Next() { + var ( + id uuid.UUID + seed []byte + //isAdmin bool + createdAt time.Time + activatedAt sql.NullTime + ) + + if err = rows.Scan(&id, &seed, &createdAt, &activatedAt); err != nil { + return fmt.Errorf("error scan row. %w", err) + } + + users = append(users, &models.User{ + ID: id, + Bip32Seed: seed, + //Admin: isAdmin, + CreatedAt: createdAt, + Activated: activatedAt.Valid, + }) + } - if err := sqltools.Transaction(ctx, r.db, func(ctx context.Context) error { return nil }); err != nil { return nil, fmt.Errorf("error execute transactional operation. %w", err) } - return user, nil + return users, nil } func (r *repositorySQL) Create(ctx context.Context, user *models.User) error {