diff --git a/cmd/main.go b/cmd/main.go index 69fc8d6..0e84fbc 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -8,6 +8,7 @@ import ( "github.com/henriquepw/imperium-tattoo/database" "github.com/henriquepw/imperium-tattoo/web/handler" + "github.com/henriquepw/imperium-tattoo/web/service" _ "github.com/joho/godotenv/autoload" _ "github.com/tursodatabase/libsql-client-go/libsql" @@ -31,7 +32,8 @@ func main() { clientHandler := handler.NewClientHandler() server.HandleFunc("GET /clients", clientHandler.ClientsPage) - employeeHandler := handler.NewEmployeeHandler(database.NewEmployeeRepo(db)) + employeeSvc := service.NewEmployeeService(database.NewEmployeeRepo(db)) + employeeHandler := handler.NewEmployeeHandler(employeeSvc) server.HandleFunc("GET /employees", employeeHandler.EmployeesPage) server.HandleFunc("GET /employees/create", employeeHandler.EmployeeCreatePage) server.HandleFunc("POST /employees/create", employeeHandler.EmployeeCreateAction) diff --git a/database/employee_repository.go b/database/employee_repository.go index ceefb36..1439b27 100644 --- a/database/employee_repository.go +++ b/database/employee_repository.go @@ -8,20 +8,21 @@ import ( "github.com/henriquepw/imperium-tattoo/web/types" ) -type EmployeeRepository interface { +type EmployeeRepo interface { Insert(ctx context.Context, payload types.EmployeeCreateDTO) (*string, error) List(ctx context.Context) ([]types.Employee, error) + CheckEmail(ctx context.Context, email string) bool } -type EmployeeRepo struct { +type repo struct { db *sql.DB } -func NewEmployeeRepo(db *sql.DB) *EmployeeRepo { - return &EmployeeRepo{db} +func NewEmployeeRepo(db *sql.DB) *repo { + return &repo{db} } -func (r EmployeeRepo) Insert(ctx context.Context, payload types.EmployeeCreateDTO) (*string, error) { +func (r repo) Insert(ctx context.Context, payload types.EmployeeCreateDTO) (*string, error) { id, err := web.NewID() if err != nil { return nil, err @@ -49,7 +50,7 @@ func (r EmployeeRepo) Insert(ctx context.Context, payload types.EmployeeCreateDT ctx, "INSERT INTO credential (id, secret) VALUES ($1, $2)", payload.Email, - payload.PasswordHash, + payload.Password, ) if err != nil { return nil, err @@ -62,7 +63,7 @@ func (r EmployeeRepo) Insert(ctx context.Context, payload types.EmployeeCreateDT return &id, nil } -func (r EmployeeRepo) List(ctx context.Context) ([]types.Employee, error) { +func (r repo) List(ctx context.Context) ([]types.Employee, error) { rows, err := r.db.QueryContext(ctx, "SELECT id, name, email, roles FROM employee") if err != nil { return nil, err @@ -81,3 +82,8 @@ func (r EmployeeRepo) List(ctx context.Context) ([]types.Employee, error) { return items, nil } + +func (r repo) CheckEmail(ctx context.Context, email string) bool { + row := r.db.QueryRowContext(ctx, "SELECT COUNT(1) FROM employee WHERE email = ?", email) + return row.Err() == nil +} diff --git a/go.mod b/go.mod index a4f4aff..7bca2b7 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/henriquepw/imperium-tattoo -go 1.22.4 +go 1.23.1 require ( github.com/a-h/templ v0.2.747 @@ -8,6 +8,7 @@ require ( github.com/joho/godotenv v1.5.1 github.com/matoous/go-nanoid/v2 v2.1.0 github.com/tursodatabase/libsql-client-go v0.0.0-20240812094001-348a4e45b535 + golang.org/x/crypto v0.22.0 ) require ( @@ -17,7 +18,6 @@ require ( github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect github.com/leodido/go-urn v1.4.0 // indirect - golang.org/x/crypto v0.22.0 // indirect golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8 // indirect golang.org/x/net v0.24.0 // indirect golang.org/x/sys v0.21.0 // indirect diff --git a/static/css/input.css b/static/css/input.css index 0c72d59..ac950b1 100644 --- a/static/css/input.css +++ b/static/css/input.css @@ -2,6 +2,22 @@ @tailwind components; @tailwind utilities; +@layer components { + .htmx-request .hx-indicator { + display: block; + } + + .htmx-request .default-indicator { + display: none; + } + + .btn-primary { + @apply flex items-center justify-center gap-1.5 h-9 px-3 rounded bg-accent-9; + @apply font-bold text-gray-12 text-center; + @apply transition-all hover:bg-accent-10 active:bg-accent-11 active:scale-95; + } +} + @layer base { @font-face { font-family: raleway; diff --git a/web/handler/employee.go b/web/handler/employee.go new file mode 100644 index 0000000..304d7ce --- /dev/null +++ b/web/handler/employee.go @@ -0,0 +1,50 @@ +package handler + +import ( + "net/http" + + "github.com/a-h/templ" + "github.com/henriquepw/imperium-tattoo/web" + "github.com/henriquepw/imperium-tattoo/web/service" + "github.com/henriquepw/imperium-tattoo/web/types" + "github.com/henriquepw/imperium-tattoo/web/view/employee" +) + +type EmployeeHandler struct { + svc service.EmployeeService +} + +func NewEmployeeHandler(svc service.EmployeeService) *EmployeeHandler { + return &EmployeeHandler{svc} +} + +func (h EmployeeHandler) EmployeesPage(w http.ResponseWriter, r *http.Request) { + web.RenderPage(w, r, employee.EmployeesPage) +} + +func (h EmployeeHandler) EmployeeCreatePage(w http.ResponseWriter, r *http.Request) { + web.RenderPage(w, r, employee.EmployeeCreatePage) +} + +func (h EmployeeHandler) EmployeeCreateAction(w http.ResponseWriter, r *http.Request) { + r.ParseForm() + payload := types.EmployeeCreateDTO{ + Name: r.Form.Get("name"), + Email: r.Form.Get("email"), + Roles: r.Form.Get("roles"), + Password: r.Form.Get("email"), + } + + _, err := h.svc.CreateEmployee(r.Context(), payload) + if err != nil { + web.RenderError(w, r, err, func(e web.ServerError) templ.Component { + return employee.EmployeeCreateForm(employee.EmployeeCreateFormProps{ + Values: payload, + Errors: e.Errors, + }) + }) + return + } + + http.Redirect(w, r, "/employees", http.StatusSeeOther) +} diff --git a/web/handler/employee_handler.go b/web/handler/employee_handler.go deleted file mode 100644 index a0dbce0..0000000 --- a/web/handler/employee_handler.go +++ /dev/null @@ -1,62 +0,0 @@ -package handler - -import ( - "net/http" - - "github.com/henriquepw/imperium-tattoo/database" - "github.com/henriquepw/imperium-tattoo/web" - "github.com/henriquepw/imperium-tattoo/web/types" - "github.com/henriquepw/imperium-tattoo/web/view/employee" -) - -type EmployeeHandler struct { - repo database.EmployeeRepository -} - -func NewEmployeeHandler(repo database.EmployeeRepository) EmployeeHandler { - return EmployeeHandler{repo} -} - -func (h EmployeeHandler) EmployeesPage(w http.ResponseWriter, r *http.Request) { - boosted := r.Header.Get("HX-Boosted") == "true" - web.Render(w, r, http.StatusOK, employee.EmployeesPage(boosted)) -} - -func (h EmployeeHandler) EmployeeCreatePage(w http.ResponseWriter, r *http.Request) { - boosted := r.Header.Get("HX-Boosted") == "true" - web.Render(w, r, http.StatusOK, employee.EmployeeCreatePage(boosted)) -} - -func (h EmployeeHandler) EmployeeCreateAction(w http.ResponseWriter, r *http.Request) { - r.ParseForm() - payload := types.EmployeeCreateDTO{ - Name: r.Form.Get("name"), - Email: r.Form.Get("email"), - Roles: r.Form.Get("roles"), - PasswordHash: r.Form.Get("email"), - } - - if err := web.CheckPayload(payload); err != nil { - web.Render(w, r, http.StatusOK, employee.EmployeeCreateForm(employee.EmployeeCreateFormProps{ - Values: payload, - Errors: err.(web.ServerError).Errors, - })) - return - } - - _, err := h.repo.Insert(r.Context(), payload) - if err != nil { - errors := err.(web.ServerError).Errors - if errors == nil { - errors = map[string]string{"password": "Email e/ou senha inválidos"} - } - - web.Render(w, r, http.StatusOK, employee.EmployeeCreateForm(employee.EmployeeCreateFormProps{ - Values: payload, - Errors: errors, - })) - return - } - - web.Render(w, r, http.StatusOK, employee.EmployeeCreateForm(employee.EmployeeCreateFormProps{})) -} diff --git a/web/hash.go b/web/hash.go new file mode 100644 index 0000000..ffc5ff4 --- /dev/null +++ b/web/hash.go @@ -0,0 +1,15 @@ +package web + +import ( + "golang.org/x/crypto/bcrypt" +) + +func HashPassword(password string) (string, error) { + bytes, err := bcrypt.GenerateFromPassword([]byte(password), 14) + return string(bytes), err +} + +func VerifyPassword(password, hash string) bool { + err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) + return err == nil +} diff --git a/web/parsers.go b/web/parsers.go index b8ea71f..6a36af0 100644 --- a/web/parsers.go +++ b/web/parsers.go @@ -1,20 +1,10 @@ package web import ( - "net/http" "net/url" "strconv" - - "github.com/a-h/templ" ) -func Render(w http.ResponseWriter, r *http.Request, statusCode int, t templ.Component) error { - w.WriteHeader(statusCode) - w.Header().Set("Content-Type", "text/html") - - return t.Render(r.Context(), w) -} - func GetQueryInt(q url.Values, name string, defaultVal int64) int64 { val, err := strconv.ParseInt(q.Get(name), 10, 64) if err != nil { diff --git a/web/renders.go b/web/renders.go new file mode 100644 index 0000000..ae186fc --- /dev/null +++ b/web/renders.go @@ -0,0 +1,37 @@ +package web + +import ( + "net/http" + + "github.com/a-h/templ" +) + +func RenderPage(w http.ResponseWriter, r *http.Request, comp func(boosted bool) templ.Component) error { + t := comp(r.Header.Get("HX-Boosted") == "true") + return Render(w, r, http.StatusOK, t) +} + +func Render(w http.ResponseWriter, r *http.Request, statusCode int, t templ.Component) error { + w.WriteHeader(statusCode) + w.Header().Set("Content-Type", "text/html") + + return t.Render(r.Context(), w) +} + +func RenderError(w http.ResponseWriter, r *http.Request, err error, t func(e ServerError) templ.Component) error { + if e, ok := err.(ServerError); ok { + if e.Errors != nil { + return Render(w, r, e.StatusCode, t(e)) + } + + w.WriteHeader(e.StatusCode) + w.Header().Set("Content-Type", "text/plain") + w.Write([]byte(e.Message)) + return nil + } + + w.WriteHeader(http.StatusInternalServerError) + w.Header().Set("Content-Type", "text/plain") + w.Write([]byte("Houve um erro inesperado")) + return nil +} diff --git a/web/service/employee.go b/web/service/employee.go new file mode 100644 index 0000000..e8fdc9b --- /dev/null +++ b/web/service/employee.go @@ -0,0 +1,48 @@ +package service + +import ( + "context" + + "github.com/henriquepw/imperium-tattoo/database" + "github.com/henriquepw/imperium-tattoo/web" + "github.com/henriquepw/imperium-tattoo/web/types" +) + +type EmployeeService interface { + CreateEmployee(ctx context.Context, payload types.EmployeeCreateDTO) (*string, error) +} + +type EmployeeSvc struct { + repo database.EmployeeRepo +} + +func NewEmployeeService(repo database.EmployeeRepo) *EmployeeSvc { + return &EmployeeSvc{repo} +} + +func (s *EmployeeSvc) CreateEmployee(ctx context.Context, payload types.EmployeeCreateDTO) (*string, error) { + if err := web.CheckPayload(payload); err != nil { + return nil, err + } + + if s.repo.CheckEmail(ctx, payload.Email) { + return nil, web.InvalidRequestDataError(map[string]string{"email": "Email já cadastrado"}) + } + + hash, err := web.HashPassword(payload.Password) + if err != nil { + return nil, err + } + + id, err := s.repo.Insert(ctx, types.EmployeeCreateDTO{ + Name: payload.Name, + Email: payload.Email, + Roles: payload.Roles, + Password: hash, + }) + if err != nil { + return nil, err + } + + return id, nil +} diff --git a/web/service/employee_service.go b/web/service/employee_service.go deleted file mode 100644 index 6d43c33..0000000 --- a/web/service/employee_service.go +++ /dev/null @@ -1 +0,0 @@ -package service diff --git a/web/types/employee.go b/web/types/employee.go index 7f44b2b..60f90fc 100644 --- a/web/types/employee.go +++ b/web/types/employee.go @@ -14,8 +14,8 @@ type Employee struct { } type EmployeeCreateDTO struct { - Name string `json:"name" validate:"required"` - Email string `json:"email" validate:"required,email"` - Roles string `json:"roles" validate:"required"` - PasswordHash string + Name string `json:"name" validate:"required"` + Email string `json:"email" validate:"required,email"` + Roles string `json:"roles" validate:"required"` + Password string `json:"password" validate:"required"` } diff --git a/web/view/client/clients.templ b/web/view/client/clients.templ index 4d41ae1..195cd32 100644 --- a/web/view/client/clients.templ +++ b/web/view/client/clients.templ @@ -1,11 +1,20 @@ package client -import "github.com/henriquepw/imperium-tattoo/web/view/layout" +import ( + "github.com/henriquepw/imperium-tattoo/web/view/layout" + "github.com/henriquepw/imperium-tattoo/web/view/ui" +) templ ClientsPage() { @layout.Dashbaord("Painel", "clients", false) { -