From d67d53d914e90afa8acf3c19fc6340fc5326f38c Mon Sep 17 00:00:00 2001 From: George Date: Wed, 3 Jun 2026 02:17:07 +0300 Subject: [PATCH] feat: +register --- flake.nix | 4 +- package/backend/cmd/main.go | 27 ++++-- package/backend/go.mod | 8 +- package/backend/go.sum | 16 ++-- package/backend/internal/user/errors.go | 6 ++ package/backend/internal/user/handler.go | 86 ++++++++++++++++++++ package/backend/internal/user/repo.go | 46 +++++++++++ package/backend/internal/user/service.go | 41 ++++++++++ package/db/src/default.sql | 4 + package/db/src/entrypoint.sql | 1 - package/db/src/tables/default.sql | 2 + package/db/src/tables/provider_schedules.sql | 8 ++ package/db/src/tables/users.sql | 12 +++ 13 files changed, 242 insertions(+), 19 deletions(-) create mode 100644 package/backend/internal/user/errors.go create mode 100644 package/backend/internal/user/handler.go create mode 100644 package/backend/internal/user/repo.go create mode 100644 package/backend/internal/user/service.go create mode 100644 package/db/src/default.sql delete mode 100644 package/db/src/entrypoint.sql create mode 100644 package/db/src/tables/default.sql create mode 100644 package/db/src/tables/provider_schedules.sql create mode 100644 package/db/src/tables/users.sql diff --git a/flake.nix b/flake.nix index d5a53f7..8f8be8a 100644 --- a/flake.nix +++ b/flake.nix @@ -21,6 +21,8 @@ export PGHOST="$PROJECT_ROOT/package/db/.pgsocket" export PGPORT=11011 + export DATABASE_URL="host=$PGHOST port=$PGPORT dbname=booking" + mkdir -p "$PGDATA" "$PGHOST" if [ ! -f "$PGDATA/PG_VERSION" ]; then @@ -31,7 +33,7 @@ pg_ctl start -D "$PGDATA" -l "$PROJECT_ROOT/package/db/log.txt" -o "-k $PGHOST -p $PGPORT" echo "postgres started at $PGHOST:$PGPORT" - psql -h $PGHOST -p $PGPORT -d postgres -f $PROJECT_ROOT/package/db/src/entrypoint.sql + psql -h $PGHOST -p $PGPORT -d postgres -f $PROJECT_ROOT/package/db/src/default.sql ''; }; }; diff --git a/package/backend/cmd/main.go b/package/backend/cmd/main.go index 538dbef..0c8e14c 100644 --- a/package/backend/cmd/main.go +++ b/package/backend/cmd/main.go @@ -1,25 +1,34 @@ package main import ( - // "context" - "fmt" + "context" + // "fmt" "log" "net/http" - // "os" + "os" - "github.com/gorilla/mux" // "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgxpool" + "github.com/go-chi/chi/v5" + "github.com/go-chi/chi/middleware" + + "github.com/ISNUFFI/booking/internal/user" ) const serverAddr = ":8080" func main() { - r := mux.NewRouter() + r := chi.NewRouter() + r.Use(middleware.Logger) - r.HandleFunc("/hello_world", func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusOK) - fmt.Fprintln(w, "Hello world!") - }).Methods("GET") + pool, err := pgxpool.New(context.Background(), os.Getenv("DATABASE_URL")) + if err != nil { + log.Fatal("could not acquire a postgres connection pool: ", err) + } + defer pool.Close() + + userHandler := user.NewHandler(pool) + userHandler.AttachHandlers(r) log.Println("Server listening on ", serverAddr) log.Fatal(http.ListenAndServe(serverAddr, r)) diff --git a/package/backend/go.mod b/package/backend/go.mod index 258542c..8059338 100644 --- a/package/backend/go.mod +++ b/package/backend/go.mod @@ -3,12 +3,16 @@ module github.com/ISNUFFI/booking go 1.25.7 require ( - github.com/gorilla/mux v1.8.1 + github.com/go-chi/chi v1.5.5 + github.com/go-chi/chi/v5 v5.3.0 github.com/jackc/pgx/v5 v5.9.2 + golang.org/x/crypto v0.52.0 ) require ( github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect - golang.org/x/text v0.29.0 // indirect + github.com/jackc/puddle/v2 v2.2.2 // indirect + golang.org/x/sync v0.20.0 // indirect + golang.org/x/text v0.37.0 // indirect ) diff --git a/package/backend/go.sum b/package/backend/go.sum index 41a70dc..0f0e3be 100644 --- a/package/backend/go.sum +++ b/package/backend/go.sum @@ -1,8 +1,10 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= -github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= +github.com/go-chi/chi v1.5.5 h1:vOB/HbEMt9QqBqErz07QehcOKHaWFtuj87tTDVz2qXE= +github.com/go-chi/chi v1.5.5/go.mod h1:C9JqLr3tIYjDOZpzn+BCuxY8z8vmca43EeMgyZt7irw= +github.com/go-chi/chi/v5 v5.3.0 h1:halUjDxhshgXHMrao5bB8eNBXo/rnzwr8m5m36glehM= +github.com/go-chi/chi/v5 v5.3.0/go.mod h1:R+tYY2hNuVUUjxoPtqUdgBqevM9s9njzkTLutVsOCto= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= @@ -18,10 +20,12 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= -golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= -golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= -golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk= -golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4= +golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988= +golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc= +golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= +golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= +golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/package/backend/internal/user/errors.go b/package/backend/internal/user/errors.go new file mode 100644 index 0000000..8026b59 --- /dev/null +++ b/package/backend/internal/user/errors.go @@ -0,0 +1,6 @@ +package user + +import "errors" + +var ErrDuplicateKey = errors.New("duplicate key") +var ErrEmailAlreadyExists = errors.New("email already exists") diff --git a/package/backend/internal/user/handler.go b/package/backend/internal/user/handler.go new file mode 100644 index 0000000..579e7c6 --- /dev/null +++ b/package/backend/internal/user/handler.go @@ -0,0 +1,86 @@ +package user + +import ( + "errors" + "encoding/json" + "fmt" + "log" + "net/http" + "net/mail" + + "github.com/go-chi/chi/v5" + "github.com/jackc/pgx/v5/pgxpool" +) + +type Handler struct { + service Service +} + +func NewHandler(pool *pgxpool.Pool) Handler { + return Handler { + service: NewService(NewRepo(pool)), + } +} + +type RegisterRequest struct { + Email string `json:"email"` + Password string `json:"password"` +} + +type RegisterValidationError struct { + message string +} + +func (e RegisterValidationError) Error() string { + return e.message +} + +func (req RegisterRequest) Validate() error { + if _, err := mail.ParseAddress(req.Email); err != nil { + return RegisterValidationError { + message: "invalid email", + } + } + + if len(req.Password) < 10 { + return RegisterValidationError { + message: "password is too short", + } + } + + return nil +} + +func (h Handler) RegisterHandler(w http.ResponseWriter, r *http.Request) { + var req RegisterRequest + + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, "invalid json", http.StatusBadRequest) + return + } + + if err := req.Validate(); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + err := h.service.Register(r.Context(), req.Email, req.Password) + if err != nil { + log.Println("could not register: ", err) + switch { + case errors.Is(err, ErrEmailAlreadyExists): + http.Error(w, "email already exists", http.StatusConflict) + default: + http.Error(w, "internal error", http.StatusInternalServerError) + } + return + } + + w.WriteHeader(http.StatusCreated) + fmt.Fprintln(w, "success") +} + +func (h Handler) AttachHandlers(router chi.Router) { + router.Post("/register", h.RegisterHandler) + // router.Post("/login", LoginHandler) +} diff --git a/package/backend/internal/user/repo.go b/package/backend/internal/user/repo.go new file mode 100644 index 0000000..12cffdf --- /dev/null +++ b/package/backend/internal/user/repo.go @@ -0,0 +1,46 @@ +package user + +import ( + "context" + "errors" + + // "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgconn" + "github.com/jackc/pgx/v5/pgxpool" +) + +type Repo struct { + pool *pgxpool.Pool +} + +func NewRepo(pool *pgxpool.Pool) Repo { + return Repo { + pool: pool, + } +} + +func (r *Repo) Exec(ctx context.Context, query string, args ...any) (pgconn.CommandTag, error) { + conn, err := r.pool.Acquire(context.Background()) + if err != nil { + return pgconn.CommandTag{}, err + } + + return conn.Exec(ctx, query, args...) +} + +func (r *Repo) CreateUser(ctx context.Context, email, hash string) error { + _, err := r.Exec( + ctx, + "INSERT INTO users(email, password_hash) VALUES ($1, $2)", + email, hash, + ) + + if err != nil { + var pgErr *pgconn.PgError + if errors.As(err, &pgErr) { + return ErrDuplicateKey + } + } + + return err +} diff --git a/package/backend/internal/user/service.go b/package/backend/internal/user/service.go new file mode 100644 index 0000000..38f3986 --- /dev/null +++ b/package/backend/internal/user/service.go @@ -0,0 +1,41 @@ +package user + +import ( + "context" + "errors" + + "golang.org/x/crypto/bcrypt" +) + +type Service struct { + repo Repo +} + +func NewService(repo Repo) Service { + return Service { + repo: repo, + } +} + +func (s Service) Register(ctx context.Context, email, password string) error { + bytes, err := bcrypt.GenerateFromPassword([]byte(password), 14) + if err != nil { + return err + } + + if err = s.repo.CreateUser(ctx, email, string(bytes)); err != nil { + switch { + case errors.Is(err, ErrDuplicateKey): + return ErrEmailAlreadyExists + default: + return err + } + } + + return nil +} + +// func (s Service) Login(password string) (string, error) { +// hash, err := s.repo. +// err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) +// } diff --git a/package/db/src/default.sql b/package/db/src/default.sql new file mode 100644 index 0000000..d9ab721 --- /dev/null +++ b/package/db/src/default.sql @@ -0,0 +1,4 @@ +CREATE DATABASE booking; +\c booking + +\ir ./tables/default.sql diff --git a/package/db/src/entrypoint.sql b/package/db/src/entrypoint.sql deleted file mode 100644 index 10ea743..0000000 --- a/package/db/src/entrypoint.sql +++ /dev/null @@ -1 +0,0 @@ -CREATE DATABASE booking; diff --git a/package/db/src/tables/default.sql b/package/db/src/tables/default.sql new file mode 100644 index 0000000..a31b0ec --- /dev/null +++ b/package/db/src/tables/default.sql @@ -0,0 +1,2 @@ +\ir users.sql +\ir provider_schedules.sql diff --git a/package/db/src/tables/provider_schedules.sql b/package/db/src/tables/provider_schedules.sql new file mode 100644 index 0000000..3b3c62d --- /dev/null +++ b/package/db/src/tables/provider_schedules.sql @@ -0,0 +1,8 @@ +CREATE TABLE provider_schedules ( + user_id BIGINT NOT NULL REFERENCES users(id), + weekday SMALLINT NOT NULL, + start_time TIME NOT NULL, + end_time TIME NOT NULL, + + PRIMARY KEY (user_id, weekday) +); diff --git a/package/db/src/tables/users.sql b/package/db/src/tables/users.sql new file mode 100644 index 0000000..e81a501 --- /dev/null +++ b/package/db/src/tables/users.sql @@ -0,0 +1,12 @@ +CREATE TYPE user_role AS ENUM ( + 'user', + 'provider' +); + +CREATE TABLE users ( + id BIGSERIAL PRIMARY KEY, + email TEXT NOT NULL UNIQUE, + role user_role NOT NULL DEFAULT 'user', + password_hash TEXT NOT NULL, + created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now() +);