login and tokens implemented

This commit is contained in:
r8zavetr8v 2024-05-09 12:41:57 +03:00
parent eeb2ad30f4
commit 86bd1107ce
11 changed files with 222 additions and 99 deletions

View File

@ -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",

View File

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

View File

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

View File

@ -0,0 +1,10 @@
package controllers
import "log/slog"
type OrganizationsController interface {
}
type organizationsController struct {
log *slog.Logger
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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