database schema updated, user hash indexes added, jwt interactor User method implemented, org user model implemented

This commit is contained in:
r8zavetr8v 2024-05-10 23:32:06 +03:00
parent 86bd1107ce
commit 9f3b54ab51
21 changed files with 418 additions and 47 deletions

View File

@ -50,4 +50,119 @@ make run.local
Just run Just run
``` sh ``` sh
make up make up
```
# API
Request content type: application/json
Response content type: application/json
## POST **/join**
### Request body:
name (string, optional)
credentals (object, optional)
credentals.email (string, optional)
credentals.phone (string, optional)
credentals.telegram (string, optional)
mnemonic (string, **required**)
### Example
Request:
``` bash
curl --location 'http://localhost:8081/login' \
--header 'Content-Type: application/json' \
--data-raw '{
"name": "Bladee The Grand Drainer",
"credentals": {
"email": "bladeee@gmail.com",
"phone": "+79999999999",
"telegram": "@thebladee"
},
"mnemonic":"airport donate language disagree dumb access insect tribe ozone humor foot jealous much digital confirm"
}'
```
Response:
``` json
{
"token": "token-here"
}
```
## POST **/login**
### Request body:
mnemonic (string, **required**)
### Example
Request:
``` bash
curl --location 'http://localhost:8081/login' \
--header 'Content-Type: application/json' \
--data '{
"mnemonic":"airport donate language disagree dumb access insect tribe ozone humor foot jealous much digital confirm"
}'
```
Response:
``` json
{
"token": "token-here"
}
```
## POST **/join**
### Request body:
name (string, optional)
credentals (object, optional)
credentals.email (string, optional)
credentals.phone (string, optional)
credentals.telegram (string, optional)
mnemonic (string, **required**)
### Example
Request:
``` bash
curl --location 'http://localhost:8081/login' \
--header 'Content-Type: application/json' \
--data-raw '{
"name": "Bladee The Grand Drainer",
"credentals": {
"email": "bladeee@gmail.com",
"phone": "+79999999999",
"telegram": "@thebladee"
},
"mnemonic":"airport donate language disagree dumb access insect tribe ozone humor foot jealous much digital confirm"
}'
```
Response:
``` json
{
"token": "token-here"
}
```
## POST **/organization**
### Request body:
name (string, **required**)
address (string, optional)
// org wallet address maybe??
### Example
Request:
``` bash
curl --location 'http://localhost:8081/organization' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3MTU0NTY4Mzg4NTAsInVpZCI6ImI2NmU1Mjk4LTU1ZTctNGIxNy1hYzliLTA0MzU3YjBlN2Q0ZSJ9.K1I0QoZEdDYK_HEsJ0PdWOfZ8ugTcPfLqy7fHhvK9nk' \
--data '{
"name": "The Drain Gang Inc",
"address": "Backsippestigen 22, 432 36 Varberg, Sweden"
}'
```
Response:
``` json
{
"id": "dfac7846-0f0a-11ef-9262-0242ac120002"
}
``` ```

View File

@ -9,7 +9,7 @@ import (
"time" "time"
"github.com/emochka2007/block-accounting/internal/interface/rest/domain" "github.com/emochka2007/block-accounting/internal/interface/rest/domain"
"github.com/emochka2007/block-accounting/internal/pkg/bip32" "github.com/emochka2007/block-accounting/internal/pkg/bip39"
) )
func main() { func main() {
@ -28,13 +28,13 @@ func main() {
var reqc int var reqc int
for { for {
e, err := bip32.NewEntropy(256) e, err := bip39.NewEntropy(256)
if err != nil { if err != nil {
log.Println("ERROR: ", err) log.Println("ERROR: ", err)
break break
} }
m, err := bip32.NewMnemonic(e) m, err := bip39.NewMnemonic(e)
if err != nil { if err != nil {
log.Println("ERROR: ", err) log.Println("ERROR: ", err)
break break

View File

@ -19,6 +19,7 @@ services:
depends_on: depends_on:
blockd-db: blockd-db:
condition: service_healthy condition: service_healthy
profiles: [blockd]
blockd-db: blockd-db:
container_name: blockd-db container_name: blockd-db
@ -41,6 +42,7 @@ services:
timeout: 5s timeout: 5s
retries: 10 retries: 10
start_period: 5s start_period: 5s
profiles: [blockd, database]
prometheus: prometheus:
image: prom/prometheus image: prom/prometheus
@ -55,6 +57,7 @@ services:
volumes: volumes:
- ./prometheus:/etc/prometheus - ./prometheus:/etc/prometheus
- prometheus_data:/prometheus - prometheus_data:/prometheus
profiles: [blockd, metrics]
grafana: grafana:
image: grafana/grafana image: grafana/grafana
@ -68,4 +71,5 @@ services:
- GF_SECURITY_ADMIN_USER=admin - GF_SECURITY_ADMIN_USER=admin
- GF_SECURITY_ADMIN_PASSWORD=grafana - GF_SECURITY_ADMIN_PASSWORD=grafana
volumes: volumes:
- ./grafana:/etc/grafana/provisioning/datasources - ./grafana:/etc/grafana/provisioning/datasources
profiles: [blockd, metrics]

View File

@ -16,6 +16,6 @@ func provideUsersInteractor(
return users.NewUsersInteractor(log.WithGroup("users-interactor"), usersRepo) return users.NewUsersInteractor(log.WithGroup("users-interactor"), usersRepo)
} }
func provideJWTInteractor(c config.Config) jwt.JWTInteractor { func provideJWTInteractor(c config.Config, usersInteractor users.UsersInteractor) jwt.JWTInteractor {
return jwt.NewWardenJWT(c.Common.JWTSecret) return jwt.NewWardenJWT(c.Common.JWTSecret, usersInteractor)
} }

View File

@ -55,11 +55,13 @@ func provideAuthController(
log *slog.Logger, log *slog.Logger,
usersInteractor users.UsersInteractor, usersInteractor users.UsersInteractor,
authPresenter presenters.AuthPresenter, authPresenter presenters.AuthPresenter,
jwtInteractor jwt.JWTInteractor,
) controllers.AuthController { ) controllers.AuthController {
return controllers.NewAuthController( return controllers.NewAuthController(
log.WithGroup("auth-controller"), log.WithGroup("auth-controller"),
authPresenter, authPresenter,
usersInteractor, usersInteractor,
jwtInteractor,
) )
} }

View File

@ -20,9 +20,9 @@ func ProvideService(c config.Config) (service.Service, func(), error) {
return nil, nil, err return nil, nil, err
} }
usersInteractor := provideUsersInteractor(logger, repository) usersInteractor := provideUsersInteractor(logger, repository)
jwtInteractor := provideJWTInteractor(c) jwtInteractor := provideJWTInteractor(c, usersInteractor)
authPresenter := provideAuthPresenter(jwtInteractor) authPresenter := provideAuthPresenter(jwtInteractor)
authController := provideAuthController(logger, usersInteractor, authPresenter) authController := provideAuthController(logger, usersInteractor, authPresenter, jwtInteractor)
rootController := provideControllers(logger, authController) rootController := provideControllers(logger, authController)
server := provideRestServer(logger, rootController, c) server := provideRestServer(logger, rootController, c)
serviceService := service.NewService(logger, server) serviceService := service.NewService(logger, server)

View File

@ -6,18 +6,20 @@ import (
"fmt" "fmt"
"log/slog" "log/slog"
"net/http" "net/http"
"strings"
"time" "time"
"github.com/emochka2007/block-accounting/internal/interface/rest/domain" "github.com/emochka2007/block-accounting/internal/interface/rest/domain"
"github.com/emochka2007/block-accounting/internal/interface/rest/presenters" "github.com/emochka2007/block-accounting/internal/interface/rest/presenters"
"github.com/emochka2007/block-accounting/internal/pkg/bip32" "github.com/emochka2007/block-accounting/internal/pkg/bip39"
"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/users" "github.com/emochka2007/block-accounting/internal/usecase/interactors/users"
) )
var ( var (
ErrorAuthInvalidMnemonic = errors.New("Invalid Mnemonic") ErrorAuthInvalidMnemonic = errors.New("invalid mnemonic")
ErrorTokenRequired = errors.New("token required")
) )
type AuthController interface { type AuthController interface {
@ -38,11 +40,13 @@ func NewAuthController(
log *slog.Logger, log *slog.Logger,
presenter presenters.AuthPresenter, presenter presenters.AuthPresenter,
usersInteractor users.UsersInteractor, usersInteractor users.UsersInteractor,
jwtInteractor jwt.JWTInteractor,
) AuthController { ) AuthController {
return &authController{ return &authController{
log: log, log: log,
presenter: presenter, presenter: presenter,
usersInteractor: usersInteractor, usersInteractor: usersInteractor,
jwtInteractor: jwtInteractor,
} }
} }
@ -54,7 +58,7 @@ func (c *authController) Join(w http.ResponseWriter, req *http.Request) error {
c.log.Debug("join request", slog.String("mnemonic", request.Mnemonic)) c.log.Debug("join request", slog.String("mnemonic", request.Mnemonic))
if !bip32.IsMnemonicValid(request.Mnemonic) { if !bip39.IsMnemonicValid(request.Mnemonic) {
return fmt.Errorf("error invalid mnemonic. %w", ErrorAuthInvalidMnemonic) return fmt.Errorf("error invalid mnemonic. %w", ErrorAuthInvalidMnemonic)
} }
@ -111,6 +115,20 @@ func (c *authController) Login(w http.ResponseWriter, req *http.Request) error {
// const mnemonicEntropyBitSize int = 256 // const mnemonicEntropyBitSize int = 256
func (c *authController) Invite(w http.ResponseWriter, req *http.Request) error { func (c *authController) Invite(w http.ResponseWriter, req *http.Request) error {
tokenStringRaw := req.Header.Get("Authorization")
if tokenStringRaw == "" {
return fmt.Errorf("error token requeired. %w", ErrorTokenRequired)
}
tokenString := strings.Split(tokenStringRaw, " ")[1]
user, err := c.jwtInteractor.User(tokenString)
if err != nil {
return fmt.Errorf("error fetch user from token. %w", err)
}
c.log.Debug("auth token", slog.Any("user", user))
return nil return nil
} }

View File

@ -6,6 +6,13 @@ import (
) )
type JoinRequest struct { type JoinRequest struct {
Name string `json:"name,omitempty"`
Credentals struct {
Email string `json:"email,omitempty"`
Phone string `json:"phone,omitempty"`
Telegram string `json:"telegram,omitempty"`
} `json:"credentals,omitempty"`
Mnemonic string `json:"mnemonic"` Mnemonic string `json:"mnemonic"`
} }

View File

@ -5,6 +5,7 @@ import (
"net/http" "net/http"
"github.com/emochka2007/block-accounting/internal/interface/rest/controllers" "github.com/emochka2007/block-accounting/internal/interface/rest/controllers"
"github.com/emochka2007/block-accounting/internal/usecase/interactors/jwt"
) )
type apiError struct { type apiError struct {
@ -21,8 +22,17 @@ func buildApiError(code int, message string) apiError {
func mapError(err error) apiError { func mapError(err error) apiError {
switch { switch {
// auth controller errors
case errors.Is(err, controllers.ErrorAuthInvalidMnemonic): case errors.Is(err, controllers.ErrorAuthInvalidMnemonic):
return buildApiError(http.StatusBadRequest, "Invalid Mnemonic") return buildApiError(http.StatusBadRequest, "Invalid Mnemonic")
case errors.Is(err, controllers.ErrorTokenRequired):
return buildApiError(http.StatusUnauthorized, "Token Required")
// jwt-related errors
case errors.Is(err, jwt.ErrorTokenExpired):
return buildApiError(http.StatusUnauthorized, "Token Expired")
case errors.Is(err, jwt.ErrorInvalidTokenClaims):
return buildApiError(http.StatusUnauthorized, "Invalid Token")
default: default:
return buildApiError(http.StatusInternalServerError, "Internal Server Error") return buildApiError(http.StatusInternalServerError, "Internal Server Error")
} }

View File

@ -77,30 +77,31 @@ func (s *Server) Close() {
} }
func (s *Server) buildRouter() { func (s *Server) buildRouter() {
s.Mux = chi.NewRouter() router := chi.NewRouter()
s.Use(mw.Recoverer) router.Use(mw.Recoverer)
s.Use(mw.RequestID) router.Use(mw.RequestID)
s.Use(s.handleMw) router.Use(s.handleMw)
s.Use(render.SetContentType(render.ContentTypeJSON)) router.Use(render.SetContentType(render.ContentTypeJSON))
s.Get("/ping", s.handle(s.controllers.Ping.Ping, "ping")) router.Get("/ping", s.handle(s.controllers.Ping.Ping, "ping"))
// auth router.Post("/join", s.handle(s.controllers.Auth.Join, "join"))
s.Post("/join", s.handle(s.controllers.Auth.Join, "join")) router.Post("/login", s.handle(s.controllers.Auth.Login, "login"))
s.Post("/login", s.handle(s.controllers.Auth.Login, "login"))
s.Route("/organization/{organization_id}", func(r chi.Router) { router.Post("/organization", s.handle(s.controllers.Auth.Invite, "organization"))
s.Route("/transactions", func(r chi.Router) {
router.Route("/organization/{organization_id}", func(r chi.Router) {
router.Route("/transactions", func(r chi.Router) {
r.Get("/", nil) // list r.Get("/", nil) // list
r.Post("/", nil) // add r.Post("/", nil) // add
r.Put("/{tx_id}", nil) // update / approve (or maybe body?) r.Put("/{tx_id}", nil) // update / approve (or maybe body?)
r.Delete("/{tx_id}", nil) // remove r.Delete("/{tx_id}", nil) // remove
}) })
s.Post("/invite/{hash}", nil) // create a new invite link r.Post("/invite/{hash}", s.handle(s.controllers.Auth.Invite, "invite")) // create a new invite link
s.Route("/employees", func(r chi.Router) { r.Route("/employees", func(r chi.Router) {
r.Get("/", nil) // list r.Get("/", nil) // list
r.Post("/", nil) // add r.Post("/", nil) // add
r.Put("/{employee_id}", nil) // update (or maybe body?) r.Put("/{employee_id}", nil) // update (or maybe body?)
@ -108,6 +109,7 @@ func (s *Server) buildRouter() {
}) })
}) })
s.Mux = router
} }
func (s *Server) handle( func (s *Server) handle(

View File

@ -1,4 +1,4 @@
package bip32 package bip39
import ( import (
"crypto/rand" "crypto/rand"

View File

@ -1,4 +1,4 @@
package bip32 package bip39
import ( import (
"fmt" "fmt"

View File

@ -0,0 +1,14 @@
package models
import (
"time"
"github.com/google/uuid"
)
type Organization struct {
ID uuid.UUID
Name string
CreatedAt time.Time
UpdatedAt time.Time
}

View File

@ -9,28 +9,35 @@ import (
type UserIdentity interface { type UserIdentity interface {
Id() uuid.UUID Id() uuid.UUID
Seed() []byte Seed() []byte
IsAdmin() bool
} }
type User struct { type User struct {
ID uuid.UUID ID uuid.UUID
Bip32Seed []byte
Admin bool Name string
Credentails UserCredentials
Bip39Seed []byte
Activated bool Activated bool
CreatedAt time.Time CreatedAt time.Time
} }
type UserCredentials struct {
Email string
Phone string
Telegram string
}
func NewUser( func NewUser(
id uuid.UUID, id uuid.UUID,
seed []byte, seed []byte,
isAdmin bool,
activated bool, activated bool,
createdAt time.Time, createdAt time.Time,
) *User { ) *User {
return &User{ return &User{
ID: id, ID: id,
Bip32Seed: seed, Bip39Seed: seed,
Admin: isAdmin,
Activated: activated, Activated: activated,
CreatedAt: createdAt, CreatedAt: createdAt,
} }
@ -41,14 +48,27 @@ func (u *User) Id() uuid.UUID {
} }
func (u *User) Seed() []byte { func (u *User) Seed() []byte {
return u.Bip32Seed return u.Bip39Seed
} }
func (u *User) IsAdmin() bool { type OrganizationParticipant interface {
return u.Admin UserIdentity
IsAdmin() bool
Position() string
} }
type OrganizationUser struct { type OrganizationUser struct {
User User
// add org info
OrgPosition string
Admin bool
}
func (u *OrganizationUser) IsAdmin() bool {
return u.Admin
}
func (u *OrganizationUser) Position() string {
return u.OrgPosition
} }

View File

@ -1,23 +1,37 @@
package jwt package jwt
import ( import (
"context"
"errors"
"fmt" "fmt"
"time" "time"
"github.com/emochka2007/block-accounting/internal/pkg/models" "github.com/emochka2007/block-accounting/internal/pkg/models"
"github.com/emochka2007/block-accounting/internal/usecase/interactors/users"
"github.com/golang-jwt/jwt/v5" "github.com/golang-jwt/jwt/v5"
"github.com/google/uuid"
)
var (
ErrorInvalidTokenClaims = errors.New("invalid token claims")
ErrorTokenExpired = errors.New("token expired")
) )
type JWTInteractor interface { type JWTInteractor interface {
NewToken(user models.UserIdentity, duration time.Duration) (string, error) NewToken(user models.UserIdentity, duration time.Duration) (string, error)
User(token string) (*models.User, error)
} }
type jwtInteractor struct { type jwtInteractor struct {
secret []byte secret []byte
usersInteractor users.UsersInteractor
} }
func NewWardenJWT(secret []byte) JWTInteractor { func NewWardenJWT(secret []byte, usersInteractor users.UsersInteractor) JWTInteractor {
return &jwtInteractor{secret} return &jwtInteractor{
secret: secret,
usersInteractor: usersInteractor,
}
} }
// NewToken creates new JWT token for given user // NewToken creates new JWT token for given user
@ -25,8 +39,8 @@ func (w *jwtInteractor) NewToken(user models.UserIdentity, duration time.Duratio
token := jwt.New(jwt.SigningMethodHS256) token := jwt.New(jwt.SigningMethodHS256)
claims := token.Claims.(jwt.MapClaims) claims := token.Claims.(jwt.MapClaims)
claims["uid"] = user.Id() claims["uid"] = user.Id().String()
claims["exp"] = time.Now().Add(duration).Unix() claims["exp"] = time.Now().Add(duration).UnixMilli()
secret := w.secret secret := w.secret
@ -37,3 +51,46 @@ func (w *jwtInteractor) NewToken(user models.UserIdentity, duration time.Duratio
return tokenString, nil return tokenString, nil
} }
func (w *jwtInteractor) User(tokenStr string) (*models.User, error) {
claims := make(jwt.MapClaims)
_, err := jwt.ParseWithClaims(tokenStr, claims, func(t *jwt.Token) (interface{}, error) {
return w.secret, nil
})
if err != nil {
return nil, errors.Join(fmt.Errorf("error parse jwt token. %w", err), ErrorInvalidTokenClaims)
}
if expDate, ok := claims["exp"].(float64); ok {
if time.UnixMilli(int64(expDate)).Before(time.Now()) {
return nil, fmt.Errorf("error token expired. %w", ErrorTokenExpired)
}
} else {
return nil, errors.Join(fmt.Errorf("error parse exp date. %w", err), ErrorInvalidTokenClaims)
}
var userIdString string
var ok bool
if userIdString, ok = claims["uid"].(string); !ok {
return nil, ErrorInvalidTokenClaims
}
userId, err := uuid.Parse(userIdString)
if err != nil {
return nil, errors.Join(fmt.Errorf("error parse user id. %w", err), ErrorInvalidTokenClaims)
}
ctx, cancel := context.WithTimeout(context.TODO(), 2*time.Second)
defer cancel()
users, err := w.usersInteractor.Get(ctx, users.GetParams{
Ids: uuid.UUIDs{userId},
})
if err != nil || len(users) == 0 {
return nil, fmt.Errorf("error fetch user from repository. %w", err)
}
return users[0], nil
}

View File

@ -0,0 +1,7 @@
package organizations
type OrganizationsInteractor interface {
}
type organizationsInteractor struct {
}

View File

@ -72,7 +72,6 @@ func (i *usersInteractor) Create(ctx context.Context, params CreateParams) (*mod
user := models.NewUser( user := models.NewUser(
uuid.New(), uuid.New(),
seed, seed,
params.IsAdmin,
params.Activate, params.Activate,
time.Now(), time.Now(),
) )

View File

@ -0,0 +1,80 @@
package organizations
import (
"context"
"database/sql"
"fmt"
sq "github.com/Masterminds/squirrel"
"github.com/emochka2007/block-accounting/internal/pkg/models"
sqltools "github.com/emochka2007/block-accounting/internal/pkg/sqlutils"
"github.com/google/uuid"
)
type GetParams struct {
Ids uuid.UUIDs
}
type Repository interface {
Create(ctx context.Context, org models.Organization) error
Get(ctx context.Context, params GetParams) ([]*models.Organization, error)
Update(ctx context.Context, org models.Organization) error
Delete(ctx context.Context, id uuid.UUID) error
}
type repositorySQL struct {
db *sql.DB
}
func NewRepository(db *sql.DB) Repository {
return &repositorySQL{
db: db,
}
}
func (s *repositorySQL) Conn(ctx context.Context) sqltools.DBTX {
if tx, ok := ctx.Value(sqltools.TxCtxKey).(*sql.Tx); ok {
return tx
}
return s.db
}
func (r *repositorySQL) Create(ctx context.Context, org models.Organization) error {
if err := sqltools.Transaction(ctx, r.db, func(ctx context.Context) (err error) {
query := sq.Insert("organizations").Columns(
"id, name",
).Values(
org.ID,
org.Name,
)
if _, err := query.RunWith(r.Conn(ctx)).ExecContext(ctx); err != nil {
return fmt.Errorf("error insert new organization. %w", err)
}
return nil
}); err != nil {
return fmt.Errorf("error execute transactional operation. %w", err)
}
return nil
}
func (r *repositorySQL) Get(ctx context.Context, params GetParams) ([]*models.Organization, error) {
panic("implement me!")
return nil, nil
}
func (r *repositorySQL) Update(ctx context.Context, org models.Organization) error {
panic("implement me!")
return nil
}
func (r *repositorySQL) Delete(ctx context.Context, id uuid.UUID) error {
panic("implement me!")
return nil
}

View File

@ -58,6 +58,12 @@ type repositorySQL struct {
db *sql.DB db *sql.DB
} }
func NewRepository(db *sql.DB) Repository {
return &repositorySQL{
db: db,
}
}
func (s *repositorySQL) Conn(ctx context.Context) sqltools.DBTX { func (s *repositorySQL) Conn(ctx context.Context) sqltools.DBTX {
if tx, ok := ctx.Value(sqltools.TxCtxKey).(*sql.Tx); ok { if tx, ok := ctx.Value(sqltools.TxCtxKey).(*sql.Tx); ok {
return tx return tx
@ -139,7 +145,7 @@ func (r *repositorySQL) GetTransactions(
MaxFeeAllowed: maxFeeAllowed, MaxFeeAllowed: maxFeeAllowed,
CreatedBy: &models.User{ CreatedBy: &models.User{
ID: createdById, ID: createdById,
Bip32Seed: createdBySeed, Bip39Seed: createdBySeed,
}, },
CreatedAt: createdAt, CreatedAt: createdAt,
UpdatedAt: updatedAt, UpdatedAt: updatedAt,

View File

@ -55,7 +55,9 @@ func (r *repositorySQL) Get(ctx context.Context, params GetParams) ([]*models.Us
PlaceholderFormat(sq.Dollar) PlaceholderFormat(sq.Dollar)
if len(params.Ids) > 0 { if len(params.Ids) > 0 {
query = query.Where("id", params.Ids) query = query.Where(sq.Eq{
"id": params.Ids,
})
} }
// if params.OrganizationId != uuid.Nil { // if params.OrganizationId != uuid.Nil {
@ -92,7 +94,7 @@ func (r *repositorySQL) Get(ctx context.Context, params GetParams) ([]*models.Us
users = append(users, &models.User{ users = append(users, &models.User{
ID: id, ID: id,
Bip32Seed: seed, Bip39Seed: seed,
//Admin: isAdmin, //Admin: isAdmin,
CreatedAt: createdAt, CreatedAt: createdAt,
Activated: activatedAt.Valid, Activated: activatedAt.Valid,
@ -113,7 +115,7 @@ func (r *repositorySQL) Create(ctx context.Context, user *models.User) error {
values := []any{ values := []any{
user.ID, user.ID,
user.Bip32Seed, user.Bip39Seed,
user.CreatedAt, user.CreatedAt,
} }

View File

@ -1,14 +1,31 @@
create table if not exists users ( create table if not exists users (
id uuid not null, id uuid not null,
name varchar(250),
email varchar(200),
phone varchar(16),
tg varchar(200),
seed bytea not null unique, seed bytea not null unique,
created_at timestamp default current_timestamp, created_at timestamp default current_timestamp,
activated_at timestamp default null, activated_at timestamp default null,
primary key (id, seed) primary key (id, seed)
); );
create index if not exists index_users_name
on users using hash (name);
create index if not exists index_users_email
on users using hash (email);
create index if not exists index_users_phone
on users using hash (phone);
create index if not exists index_users_seed
on users using hash (seed);
create table if not exists organizations ( create table if not exists organizations (
id uuid primary key, id uuid primary key,
name varchar(300) default 'My Organization' not null, name varchar(300) default 'My Organization' not null,
address varchar(750) default "",
created_at timestamp default current_timestamp, created_at timestamp default current_timestamp,
updated_at timestamp default current_timestamp updated_at timestamp default current_timestamp
); );
@ -31,6 +48,17 @@ create index if not exists index_organizations_users_organization_id_user_id_is_
create index if not exists index_organizations_users_organization_id_user_id create index if not exists index_organizations_users_organization_id_user_id
on organizations_users (organization_id, user_id); on organizations_users (organization_id, user_id);
create table employees (
id uuid primary key,
organization_id uuid not null,
wallet_address text not null,
created_at timestamp default current_timestamp,
updated_at timestamp default current_timestamp
);
create index if not exists index_employees_id_organization_id
on employees (id, organization_id);
create table if not exists transactions ( create table if not exists transactions (
id uuid primary key, id uuid primary key,
description text default 'New Transaction', description text default 'New Transaction',