feat: +register
This commit is contained in:
@@ -21,6 +21,8 @@
|
|||||||
export PGHOST="$PROJECT_ROOT/package/db/.pgsocket"
|
export PGHOST="$PROJECT_ROOT/package/db/.pgsocket"
|
||||||
export PGPORT=11011
|
export PGPORT=11011
|
||||||
|
|
||||||
|
export DATABASE_URL="host=$PGHOST port=$PGPORT dbname=booking"
|
||||||
|
|
||||||
mkdir -p "$PGDATA" "$PGHOST"
|
mkdir -p "$PGDATA" "$PGHOST"
|
||||||
|
|
||||||
if [ ! -f "$PGDATA/PG_VERSION" ]; then
|
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"
|
pg_ctl start -D "$PGDATA" -l "$PROJECT_ROOT/package/db/log.txt" -o "-k $PGHOST -p $PGPORT"
|
||||||
echo "postgres started at $PGHOST:$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
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,25 +1,34 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
// "context"
|
"context"
|
||||||
"fmt"
|
// "fmt"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
// "os"
|
"os"
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
|
||||||
// "github.com/jackc/pgx/v5"
|
// "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"
|
const serverAddr = ":8080"
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
r := mux.NewRouter()
|
r := chi.NewRouter()
|
||||||
|
r.Use(middleware.Logger)
|
||||||
|
|
||||||
r.HandleFunc("/hello_world", func(w http.ResponseWriter, r *http.Request) {
|
pool, err := pgxpool.New(context.Background(), os.Getenv("DATABASE_URL"))
|
||||||
w.WriteHeader(http.StatusOK)
|
if err != nil {
|
||||||
fmt.Fprintln(w, "Hello world!")
|
log.Fatal("could not acquire a postgres connection pool: ", err)
|
||||||
}).Methods("GET")
|
}
|
||||||
|
defer pool.Close()
|
||||||
|
|
||||||
|
userHandler := user.NewHandler(pool)
|
||||||
|
userHandler.AttachHandlers(r)
|
||||||
|
|
||||||
log.Println("Server listening on ", serverAddr)
|
log.Println("Server listening on ", serverAddr)
|
||||||
log.Fatal(http.ListenAndServe(serverAddr, r))
|
log.Fatal(http.ListenAndServe(serverAddr, r))
|
||||||
|
|||||||
@@ -3,12 +3,16 @@ module github.com/ISNUFFI/booking
|
|||||||
go 1.25.7
|
go 1.25.7
|
||||||
|
|
||||||
require (
|
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
|
github.com/jackc/pgx/v5 v5.9.2
|
||||||
|
golang.org/x/crypto v0.52.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // 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
|
||||||
)
|
)
|
||||||
|
|||||||
+10
-6
@@ -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.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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
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/go-chi/chi v1.5.5 h1:vOB/HbEMt9QqBqErz07QehcOKHaWFtuj87tTDVz2qXE=
|
||||||
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
|
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 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||||
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
|
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.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 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||||
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
|
golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988=
|
||||||
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc=
|
||||||
golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
|
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
||||||
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
|
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/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.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
package user
|
||||||
|
|
||||||
|
import "errors"
|
||||||
|
|
||||||
|
var ErrDuplicateKey = errors.New("duplicate key")
|
||||||
|
var ErrEmailAlreadyExists = errors.New("email already exists")
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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))
|
||||||
|
// }
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
CREATE DATABASE booking;
|
||||||
|
\c booking
|
||||||
|
|
||||||
|
\ir ./tables/default.sql
|
||||||
@@ -1 +0,0 @@
|
|||||||
CREATE DATABASE booking;
|
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
\ir users.sql
|
||||||
|
\ir 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)
|
||||||
|
);
|
||||||
@@ -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()
|
||||||
|
);
|
||||||
Reference in New Issue
Block a user