Add full auth endpoints and swagger updates
This commit is contained in:
parent
276b6bb923
commit
b72f8f3099
14 changed files with 3309 additions and 324 deletions
1151
backend/docs/docs.go
1151
backend/docs/docs.go
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -17,6 +17,7 @@ require (
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/KyleBanks/depth v1.2.1 // indirect
|
github.com/KyleBanks/depth v1.2.1 // indirect
|
||||||
|
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d // indirect
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/go-openapi/jsonpointer v0.19.5 // indirect
|
github.com/go-openapi/jsonpointer v0.19.5 // indirect
|
||||||
github.com/go-openapi/jsonreference v0.20.0 // indirect
|
github.com/go-openapi/jsonreference v0.20.0 // indirect
|
||||||
|
|
@ -31,7 +32,10 @@ require (
|
||||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
github.com/rogpeppe/go-internal v1.14.1 // indirect
|
github.com/rogpeppe/go-internal v1.14.1 // indirect
|
||||||
|
github.com/russross/blackfriday/v2 v2.0.1 // indirect
|
||||||
|
github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect
|
||||||
github.com/swaggo/files v0.0.0-20220610200504-28940afbdbfe // indirect
|
github.com/swaggo/files v0.0.0-20220610200504-28940afbdbfe // indirect
|
||||||
|
github.com/urfave/cli/v2 v2.3.0 // indirect
|
||||||
golang.org/x/mod v0.21.0 // indirect
|
golang.org/x/mod v0.21.0 // indirect
|
||||||
golang.org/x/net v0.34.0 // indirect
|
golang.org/x/net v0.34.0 // indirect
|
||||||
golang.org/x/sync v0.13.0 // indirect
|
golang.org/x/sync v0.13.0 // indirect
|
||||||
|
|
@ -39,4 +43,5 @@ require (
|
||||||
golang.org/x/tools v0.26.0 // indirect
|
golang.org/x/tools v0.26.0 // indirect
|
||||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
|
sigs.k8s.io/yaml v1.3.0 // indirect
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,12 @@
|
||||||
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||||
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||||
|
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||||
github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU=
|
github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU=
|
||||||
github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU=
|
github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU=
|
||||||
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
|
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
|
||||||
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
|
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
|
||||||
|
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY=
|
||||||
|
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||||
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=
|
||||||
|
|
@ -64,6 +67,10 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||||
|
github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q=
|
||||||
|
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||||
|
github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
|
||||||
|
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||||
|
|
@ -80,6 +87,8 @@ github.com/swaggo/http-swagger v1.3.4 h1:q7t/XLx0n15H1Q9/tk3Y9L4n210XzJF5WtnDX64
|
||||||
github.com/swaggo/http-swagger v1.3.4/go.mod h1:9dAh0unqMBAlbp1uE2Uc2mQTxNMU/ha4UbucIg1MFkQ=
|
github.com/swaggo/http-swagger v1.3.4/go.mod h1:9dAh0unqMBAlbp1uE2Uc2mQTxNMU/ha4UbucIg1MFkQ=
|
||||||
github.com/swaggo/swag v1.16.6 h1:qBNcx53ZaX+M5dxVyTrgQ0PJ/ACK+NzhwcbieTt+9yI=
|
github.com/swaggo/swag v1.16.6 h1:qBNcx53ZaX+M5dxVyTrgQ0PJ/ACK+NzhwcbieTt+9yI=
|
||||||
github.com/swaggo/swag v1.16.6/go.mod h1:ngP2etMK5a0P3QBizic5MEwpRmluJZPHjXcMoj4Xesg=
|
github.com/swaggo/swag v1.16.6/go.mod h1:ngP2etMK5a0P3QBizic5MEwpRmluJZPHjXcMoj4Xesg=
|
||||||
|
github.com/urfave/cli/v2 v2.3.0 h1:qph92Y649prgesehzOrQjdWyxFOp/QVM+6imKHad91M=
|
||||||
|
github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI=
|
||||||
golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
|
golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
|
||||||
golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
|
golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
|
||||||
golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0=
|
golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0=
|
||||||
|
|
@ -104,9 +113,12 @@ gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||||
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.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/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=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo=
|
||||||
|
sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8=
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,7 @@ type User struct {
|
||||||
Role string `db:"role" json:"role"`
|
Role string `db:"role" json:"role"`
|
||||||
Name string `db:"name" json:"name"`
|
Name string `db:"name" json:"name"`
|
||||||
Email string `db:"email" json:"email"`
|
Email string `db:"email" json:"email"`
|
||||||
|
EmailVerified bool `db:"email_verified" json:"email_verified"`
|
||||||
PasswordHash string `db:"password_hash" json:"-"`
|
PasswordHash string `db:"password_hash" json:"-"`
|
||||||
CreatedAt time.Time `db:"created_at" json:"created_at"`
|
CreatedAt time.Time `db:"created_at" json:"created_at"`
|
||||||
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
|
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
|
||||||
|
|
|
||||||
|
|
@ -31,3 +31,11 @@ type PaginationResponse[T any] struct {
|
||||||
CurrentPage int `json:"current_page"`
|
CurrentPage int `json:"current_page"`
|
||||||
TotalPages int `json:"total_pages"`
|
TotalPages int `json:"total_pages"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ProductPaginationResponse is a swagger-friendly pagination response for products.
|
||||||
|
type ProductPaginationResponse struct {
|
||||||
|
Items []Product `json:"items"`
|
||||||
|
TotalCount int64 `json:"total_count"`
|
||||||
|
CurrentPage int `json:"current_page"`
|
||||||
|
TotalPages int `json:"total_pages"`
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import (
|
||||||
"errors"
|
"errors"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gofrs/uuid/v5"
|
"github.com/gofrs/uuid/v5"
|
||||||
|
|
@ -48,11 +49,34 @@ type loginRequest struct {
|
||||||
Password string `json:"password"`
|
Password string `json:"password"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type forgotPasswordRequest struct {
|
||||||
|
Email string `json:"email"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type resetPasswordRequest struct {
|
||||||
|
Token string `json:"token"`
|
||||||
|
Password string `json:"password"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type verifyEmailRequest struct {
|
||||||
|
Token string `json:"token"`
|
||||||
|
}
|
||||||
|
|
||||||
type authResponse struct {
|
type authResponse struct {
|
||||||
Token string `json:"token"`
|
Token string `json:"token"`
|
||||||
ExpiresAt time.Time `json:"expires_at"`
|
ExpiresAt time.Time `json:"expires_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type messageResponse struct {
|
||||||
|
Message string `json:"message"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type resetTokenResponse struct {
|
||||||
|
Message string `json:"message"`
|
||||||
|
ResetToken string `json:"reset_token,omitempty"`
|
||||||
|
ExpiresAt *time.Time `json:"expires_at,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
type inventoryAdjustRequest struct {
|
type inventoryAdjustRequest struct {
|
||||||
ProductID uuid.UUID `json:"product_id"`
|
ProductID uuid.UUID `json:"product_id"`
|
||||||
Delta int64 `json:"delta"`
|
Delta int64 `json:"delta"`
|
||||||
|
|
@ -268,3 +292,19 @@ func getRequester(r *http.Request) (requester, error) {
|
||||||
|
|
||||||
return requester{Role: role, CompanyID: companyID}, nil
|
return requester{Role: role, CompanyID: companyID}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func parseBearerToken(r *http.Request) (string, error) {
|
||||||
|
authHeader := r.Header.Get("Authorization")
|
||||||
|
if authHeader == "" {
|
||||||
|
return "", errors.New("missing Authorization header")
|
||||||
|
}
|
||||||
|
parts := strings.SplitN(authHeader, " ", 2)
|
||||||
|
if len(parts) != 2 || !strings.EqualFold(parts[0], "bearer") {
|
||||||
|
return "", errors.New("invalid Authorization header")
|
||||||
|
}
|
||||||
|
token := strings.TrimSpace(parts[1])
|
||||||
|
if token == "" {
|
||||||
|
return "", errors.New("token is required")
|
||||||
|
}
|
||||||
|
return token, nil
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
package handler
|
package handler
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"database/sql"
|
||||||
"errors"
|
"errors"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
|
@ -112,3 +113,230 @@ func (h *Handler) Login(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
writeJSON(w, http.StatusOK, authResponse{Token: token, ExpiresAt: exp})
|
writeJSON(w, http.StatusOK, authResponse{Token: token, ExpiresAt: exp})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RegisterCustomer godoc
|
||||||
|
// @Summary Cadastro de cliente
|
||||||
|
// @Description Cria um usuário do tipo cliente e opcionalmente uma empresa, retornando token JWT.
|
||||||
|
// @Tags Autenticação
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param payload body registerAuthRequest true "Dados do usuário e empresa"
|
||||||
|
// @Success 201 {object} authResponse
|
||||||
|
// @Failure 400 {object} map[string]string
|
||||||
|
// @Failure 500 {object} map[string]string
|
||||||
|
// @Router /api/v1/auth/register/customer [post]
|
||||||
|
func (h *Handler) RegisterCustomer(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var req registerAuthRequest
|
||||||
|
if err := decodeJSON(r.Context(), r, &req); err != nil {
|
||||||
|
writeError(w, http.StatusBadRequest, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Role = "Customer"
|
||||||
|
h.registerWithPayload(w, r, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RegisterTenant godoc
|
||||||
|
// @Summary Cadastro de tenant
|
||||||
|
// @Description Cria um usuário do tipo tenant e opcionalmente uma empresa, retornando token JWT.
|
||||||
|
// @Tags Autenticação
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param payload body registerAuthRequest true "Dados do usuário e empresa"
|
||||||
|
// @Success 201 {object} authResponse
|
||||||
|
// @Failure 400 {object} map[string]string
|
||||||
|
// @Failure 500 {object} map[string]string
|
||||||
|
// @Router /api/v1/auth/register/tenant [post]
|
||||||
|
func (h *Handler) RegisterTenant(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var req registerAuthRequest
|
||||||
|
if err := decodeJSON(r.Context(), r, &req); err != nil {
|
||||||
|
writeError(w, http.StatusBadRequest, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Role = "Seller"
|
||||||
|
h.registerWithPayload(w, r, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RefreshToken godoc
|
||||||
|
// @Summary Atualizar token
|
||||||
|
// @Description Gera um novo JWT a partir de um token válido.
|
||||||
|
// @Tags Autenticação
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param Authorization header string true "Bearer token"
|
||||||
|
// @Success 200 {object} authResponse
|
||||||
|
// @Failure 401 {object} map[string]string
|
||||||
|
// @Router /api/v1/auth/refresh-token [post]
|
||||||
|
func (h *Handler) RefreshToken(w http.ResponseWriter, r *http.Request) {
|
||||||
|
tokenStr, err := parseBearerToken(r)
|
||||||
|
if err != nil {
|
||||||
|
writeError(w, http.StatusUnauthorized, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
token, exp, err := h.svc.RefreshToken(r.Context(), tokenStr)
|
||||||
|
if err != nil {
|
||||||
|
writeError(w, http.StatusUnauthorized, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
writeJSON(w, http.StatusOK, authResponse{Token: token, ExpiresAt: exp})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Logout godoc
|
||||||
|
// @Summary Logout
|
||||||
|
// @Description Endpoint para logout (invalidação client-side).
|
||||||
|
// @Tags Autenticação
|
||||||
|
// @Success 204 {string} string "No Content"
|
||||||
|
// @Router /api/v1/auth/logout [post]
|
||||||
|
func (h *Handler) Logout(w http.ResponseWriter, _ *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusNoContent)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ForgotPassword godoc
|
||||||
|
// @Summary Solicitar redefinição de senha
|
||||||
|
// @Description Gera um token de redefinição de senha.
|
||||||
|
// @Tags Autenticação
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param payload body forgotPasswordRequest true "Email do usuário"
|
||||||
|
// @Success 202 {object} resetTokenResponse
|
||||||
|
// @Failure 400 {object} map[string]string
|
||||||
|
// @Router /api/v1/auth/password/forgot [post]
|
||||||
|
func (h *Handler) ForgotPassword(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var req forgotPasswordRequest
|
||||||
|
if err := decodeJSON(r.Context(), r, &req); err != nil {
|
||||||
|
writeError(w, http.StatusBadRequest, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if req.Email == "" {
|
||||||
|
writeError(w, http.StatusBadRequest, errors.New("email is required"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
token, exp, err := h.svc.CreatePasswordResetToken(r.Context(), req.Email)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, sql.ErrNoRows) {
|
||||||
|
writeJSON(w, http.StatusAccepted, resetTokenResponse{
|
||||||
|
Message: "Se existir uma conta, enviaremos instruções de redefinição.",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeError(w, http.StatusInternalServerError, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
writeJSON(w, http.StatusAccepted, resetTokenResponse{
|
||||||
|
Message: "Token de redefinição gerado.",
|
||||||
|
ResetToken: token,
|
||||||
|
ExpiresAt: &exp,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResetPassword godoc
|
||||||
|
// @Summary Redefinir senha
|
||||||
|
// @Description Atualiza a senha usando o token de redefinição.
|
||||||
|
// @Tags Autenticação
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param payload body resetPasswordRequest true "Token e nova senha"
|
||||||
|
// @Success 200 {object} messageResponse
|
||||||
|
// @Failure 400 {object} map[string]string
|
||||||
|
// @Failure 401 {object} map[string]string
|
||||||
|
// @Router /api/v1/auth/password/reset [post]
|
||||||
|
func (h *Handler) ResetPassword(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var req resetPasswordRequest
|
||||||
|
if err := decodeJSON(r.Context(), r, &req); err != nil {
|
||||||
|
writeError(w, http.StatusBadRequest, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if req.Token == "" || req.Password == "" {
|
||||||
|
writeError(w, http.StatusBadRequest, errors.New("token and password are required"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := h.svc.ResetPassword(r.Context(), req.Token, req.Password); err != nil {
|
||||||
|
writeError(w, http.StatusUnauthorized, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
writeJSON(w, http.StatusOK, messageResponse{Message: "Senha atualizada com sucesso."})
|
||||||
|
}
|
||||||
|
|
||||||
|
// VerifyEmail godoc
|
||||||
|
// @Summary Verificar email
|
||||||
|
// @Description Marca o email como verificado usando um token JWT.
|
||||||
|
// @Tags Autenticação
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param payload body verifyEmailRequest true "Token de verificação"
|
||||||
|
// @Success 200 {object} messageResponse
|
||||||
|
// @Failure 400 {object} map[string]string
|
||||||
|
// @Failure 401 {object} map[string]string
|
||||||
|
// @Router /api/v1/auth/verify-email [post]
|
||||||
|
func (h *Handler) VerifyEmail(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var req verifyEmailRequest
|
||||||
|
if err := decodeJSON(r.Context(), r, &req); err != nil {
|
||||||
|
writeError(w, http.StatusBadRequest, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if req.Token == "" {
|
||||||
|
writeError(w, http.StatusBadRequest, errors.New("token is required"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := h.svc.VerifyEmail(r.Context(), req.Token); err != nil {
|
||||||
|
writeError(w, http.StatusUnauthorized, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
writeJSON(w, http.StatusOK, messageResponse{Message: "E-mail verificado com sucesso."})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) registerWithPayload(w http.ResponseWriter, r *http.Request, req registerAuthRequest) {
|
||||||
|
var company *domain.Company
|
||||||
|
if req.Company != nil {
|
||||||
|
company = &domain.Company{
|
||||||
|
ID: req.Company.ID,
|
||||||
|
Category: req.Company.Category,
|
||||||
|
CNPJ: req.Company.CNPJ,
|
||||||
|
CorporateName: req.Company.CorporateName,
|
||||||
|
LicenseNumber: req.Company.LicenseNumber,
|
||||||
|
Latitude: req.Company.Latitude,
|
||||||
|
Longitude: req.Company.Longitude,
|
||||||
|
City: req.Company.City,
|
||||||
|
State: req.Company.State,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var companyID uuid.UUID
|
||||||
|
if req.CompanyID != nil {
|
||||||
|
companyID = *req.CompanyID
|
||||||
|
}
|
||||||
|
|
||||||
|
user := &domain.User{
|
||||||
|
CompanyID: companyID,
|
||||||
|
Role: req.Role,
|
||||||
|
Name: req.Name,
|
||||||
|
Email: req.Email,
|
||||||
|
}
|
||||||
|
|
||||||
|
if user.CompanyID == uuid.Nil && company == nil {
|
||||||
|
writeError(w, http.StatusBadRequest, errors.New("company_id or company payload is required"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := h.svc.RegisterAccount(r.Context(), company, user, req.Password); err != nil {
|
||||||
|
writeError(w, http.StatusInternalServerError, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
token, exp, err := h.svc.Authenticate(r.Context(), user.Email, req.Password)
|
||||||
|
if err != nil {
|
||||||
|
writeError(w, http.StatusInternalServerError, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
writeJSON(w, http.StatusCreated, authResponse{Token: token, ExpiresAt: exp})
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ import (
|
||||||
// @Param created_before query string false "Data máxima (RFC3339)"
|
// @Param created_before query string false "Data máxima (RFC3339)"
|
||||||
// @Param page query integer false "Página"
|
// @Param page query integer false "Página"
|
||||||
// @Param page_size query integer false "Itens por página"
|
// @Param page_size query integer false "Itens por página"
|
||||||
// @Success 200 {object} domain.PaginationResponse[domain.Product]
|
// @Success 200 {object} domain.ProductPaginationResponse
|
||||||
// @Router /api/v1/marketplace/records [get]
|
// @Router /api/v1/marketplace/records [get]
|
||||||
func (h *Handler) ListMarketplaceRecords(w http.ResponseWriter, r *http.Request) {
|
func (h *Handler) ListMarketplaceRecords(w http.ResponseWriter, r *http.Request) {
|
||||||
page, pageSize := parsePagination(r)
|
page, pageSize := parsePagination(r)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
ALTER TABLE users
|
||||||
|
ADD COLUMN IF NOT EXISTS email_verified BOOLEAN NOT NULL DEFAULT FALSE;
|
||||||
|
|
@ -785,8 +785,8 @@ func (r *Repository) CreateUser(ctx context.Context, user *domain.User) error {
|
||||||
user.CreatedAt = now
|
user.CreatedAt = now
|
||||||
user.UpdatedAt = now
|
user.UpdatedAt = now
|
||||||
|
|
||||||
query := `INSERT INTO users (id, company_id, role, name, email, password_hash, created_at, updated_at)
|
query := `INSERT INTO users (id, company_id, role, name, email, email_verified, password_hash, created_at, updated_at)
|
||||||
VALUES (:id, :company_id, :role, :name, :email, :password_hash, :created_at, :updated_at)`
|
VALUES (:id, :company_id, :role, :name, :email, :email_verified, :password_hash, :created_at, :updated_at)`
|
||||||
|
|
||||||
_, err := r.db.NamedExecContext(ctx, query, user)
|
_, err := r.db.NamedExecContext(ctx, query, user)
|
||||||
return err
|
return err
|
||||||
|
|
@ -814,7 +814,7 @@ func (r *Repository) ListUsers(ctx context.Context, filter domain.UserFilter) ([
|
||||||
}
|
}
|
||||||
|
|
||||||
args = append(args, filter.Limit, filter.Offset)
|
args = append(args, filter.Limit, filter.Offset)
|
||||||
listQuery := fmt.Sprintf("SELECT id, company_id, role, name, email, password_hash, created_at, updated_at %s%s ORDER BY created_at DESC LIMIT $%d OFFSET $%d", baseQuery, where, len(args)-1, len(args))
|
listQuery := fmt.Sprintf("SELECT id, company_id, role, name, email, email_verified, password_hash, created_at, updated_at %s%s ORDER BY created_at DESC LIMIT $%d OFFSET $%d", baseQuery, where, len(args)-1, len(args))
|
||||||
|
|
||||||
var users []domain.User
|
var users []domain.User
|
||||||
if err := r.db.SelectContext(ctx, &users, listQuery, args...); err != nil {
|
if err := r.db.SelectContext(ctx, &users, listQuery, args...); err != nil {
|
||||||
|
|
@ -826,7 +826,7 @@ func (r *Repository) ListUsers(ctx context.Context, filter domain.UserFilter) ([
|
||||||
|
|
||||||
func (r *Repository) GetUser(ctx context.Context, id uuid.UUID) (*domain.User, error) {
|
func (r *Repository) GetUser(ctx context.Context, id uuid.UUID) (*domain.User, error) {
|
||||||
var user domain.User
|
var user domain.User
|
||||||
query := `SELECT id, company_id, role, name, email, password_hash, created_at, updated_at FROM users WHERE id = $1`
|
query := `SELECT id, company_id, role, name, email, email_verified, password_hash, created_at, updated_at FROM users WHERE id = $1`
|
||||||
if err := r.db.GetContext(ctx, &user, query, id); err != nil {
|
if err := r.db.GetContext(ctx, &user, query, id); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
@ -835,7 +835,7 @@ func (r *Repository) GetUser(ctx context.Context, id uuid.UUID) (*domain.User, e
|
||||||
|
|
||||||
func (r *Repository) GetUserByEmail(ctx context.Context, email string) (*domain.User, error) {
|
func (r *Repository) GetUserByEmail(ctx context.Context, email string) (*domain.User, error) {
|
||||||
var user domain.User
|
var user domain.User
|
||||||
query := `SELECT id, company_id, role, name, email, password_hash, created_at, updated_at FROM users WHERE email = $1`
|
query := `SELECT id, company_id, role, name, email, email_verified, password_hash, created_at, updated_at FROM users WHERE email = $1`
|
||||||
if err := r.db.GetContext(ctx, &user, query, email); err != nil {
|
if err := r.db.GetContext(ctx, &user, query, email); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
@ -846,7 +846,7 @@ func (r *Repository) UpdateUser(ctx context.Context, user *domain.User) error {
|
||||||
user.UpdatedAt = time.Now().UTC()
|
user.UpdatedAt = time.Now().UTC()
|
||||||
|
|
||||||
query := `UPDATE users
|
query := `UPDATE users
|
||||||
SET company_id = :company_id, role = :role, name = :name, email = :email, password_hash = :password_hash, updated_at = :updated_at
|
SET company_id = :company_id, role = :role, name = :name, email = :email, email_verified = :email_verified, password_hash = :password_hash, updated_at = :updated_at
|
||||||
WHERE id = :id`
|
WHERE id = :id`
|
||||||
|
|
||||||
res, err := r.db.NamedExecContext(ctx, query, user)
|
res, err := r.db.NamedExecContext(ctx, query, user)
|
||||||
|
|
|
||||||
|
|
@ -100,7 +100,14 @@ func New(cfg config.Config) (*Server, error) {
|
||||||
mux.Handle("GET /api/v1/dashboard/admin", chain(http.HandlerFunc(h.GetAdminDashboard), middleware.Logger, middleware.Gzip, adminOnly))
|
mux.Handle("GET /api/v1/dashboard/admin", chain(http.HandlerFunc(h.GetAdminDashboard), middleware.Logger, middleware.Gzip, adminOnly))
|
||||||
|
|
||||||
mux.Handle("POST /api/v1/auth/register", chain(http.HandlerFunc(h.Register), middleware.Logger, middleware.Gzip))
|
mux.Handle("POST /api/v1/auth/register", chain(http.HandlerFunc(h.Register), middleware.Logger, middleware.Gzip))
|
||||||
|
mux.Handle("POST /api/v1/auth/register/customer", chain(http.HandlerFunc(h.RegisterCustomer), middleware.Logger, middleware.Gzip))
|
||||||
|
mux.Handle("POST /api/v1/auth/register/tenant", chain(http.HandlerFunc(h.RegisterTenant), middleware.Logger, middleware.Gzip))
|
||||||
mux.Handle("POST /api/v1/auth/login", chain(http.HandlerFunc(h.Login), middleware.Logger, middleware.Gzip))
|
mux.Handle("POST /api/v1/auth/login", chain(http.HandlerFunc(h.Login), middleware.Logger, middleware.Gzip))
|
||||||
|
mux.Handle("POST /api/v1/auth/logout", chain(http.HandlerFunc(h.Logout), middleware.Logger, middleware.Gzip))
|
||||||
|
mux.Handle("POST /api/v1/auth/password/forgot", chain(http.HandlerFunc(h.ForgotPassword), middleware.Logger, middleware.Gzip))
|
||||||
|
mux.Handle("POST /api/v1/auth/password/reset", chain(http.HandlerFunc(h.ResetPassword), middleware.Logger, middleware.Gzip))
|
||||||
|
mux.Handle("POST /api/v1/auth/refresh-token", chain(http.HandlerFunc(h.RefreshToken), middleware.Logger, middleware.Gzip))
|
||||||
|
mux.Handle("POST /api/v1/auth/verify-email", chain(http.HandlerFunc(h.VerifyEmail), middleware.Logger, middleware.Gzip))
|
||||||
|
|
||||||
mux.Handle("POST /api/v1/users", chain(http.HandlerFunc(h.CreateUser), middleware.Logger, middleware.Gzip, auth))
|
mux.Handle("POST /api/v1/users", chain(http.HandlerFunc(h.CreateUser), middleware.Logger, middleware.Gzip, auth))
|
||||||
mux.Handle("GET /api/v1/users", chain(http.HandlerFunc(h.ListUsers), middleware.Logger, middleware.Gzip, auth))
|
mux.Handle("GET /api/v1/users", chain(http.HandlerFunc(h.ListUsers), middleware.Logger, middleware.Gzip, auth))
|
||||||
|
|
|
||||||
|
|
@ -75,6 +75,10 @@ type Service struct {
|
||||||
passwordPepper string
|
passwordPepper string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
passwordResetTTL = 30 * time.Minute
|
||||||
|
)
|
||||||
|
|
||||||
// NewService wires use cases together.
|
// NewService wires use cases together.
|
||||||
func NewService(repo Repository, pay PaymentGateway, commissionPct float64, jwtSecret string, tokenTTL time.Duration, passwordPepper string) *Service {
|
func NewService(repo Repository, pay PaymentGateway, commissionPct float64, jwtSecret string, tokenTTL time.Duration, passwordPepper string) *Service {
|
||||||
return &Service{
|
return &Service{
|
||||||
|
|
@ -629,21 +633,7 @@ func (s *Service) Authenticate(ctx context.Context, email, password string) (str
|
||||||
return "", time.Time{}, errors.New("invalid credentials")
|
return "", time.Time{}, errors.New("invalid credentials")
|
||||||
}
|
}
|
||||||
|
|
||||||
expiresAt := time.Now().Add(s.tokenTTL)
|
return s.issueAccessToken(user)
|
||||||
claims := jwt.MapClaims{
|
|
||||||
"sub": user.ID.String(),
|
|
||||||
"role": user.Role,
|
|
||||||
"company_id": user.CompanyID.String(),
|
|
||||||
"exp": expiresAt.Unix(),
|
|
||||||
}
|
|
||||||
|
|
||||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
|
||||||
signed, err := token.SignedString(s.jwtSecret)
|
|
||||||
if err != nil {
|
|
||||||
return "", time.Time{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return signed, expiresAt, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) pepperPassword(password string) string {
|
func (s *Service) pepperPassword(password string) string {
|
||||||
|
|
@ -653,6 +643,156 @@ func (s *Service) pepperPassword(password string) string {
|
||||||
return password + s.passwordPepper
|
return password + s.passwordPepper
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RefreshToken validates the provided JWT and issues a new access token.
|
||||||
|
func (s *Service) RefreshToken(ctx context.Context, tokenStr string) (string, time.Time, error) {
|
||||||
|
claims, err := s.parseToken(tokenStr)
|
||||||
|
if err != nil {
|
||||||
|
return "", time.Time{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if scope, ok := claims["scope"].(string); ok && scope != "" {
|
||||||
|
return "", time.Time{}, errors.New("invalid token scope")
|
||||||
|
}
|
||||||
|
|
||||||
|
sub, ok := claims["sub"].(string)
|
||||||
|
if !ok || sub == "" {
|
||||||
|
return "", time.Time{}, errors.New("invalid token subject")
|
||||||
|
}
|
||||||
|
|
||||||
|
userID, err := uuid.FromString(sub)
|
||||||
|
if err != nil {
|
||||||
|
return "", time.Time{}, errors.New("invalid token subject")
|
||||||
|
}
|
||||||
|
|
||||||
|
user, err := s.repo.GetUser(ctx, userID)
|
||||||
|
if err != nil {
|
||||||
|
return "", time.Time{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.issueAccessToken(user)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreatePasswordResetToken generates a short-lived token for password reset.
|
||||||
|
func (s *Service) CreatePasswordResetToken(ctx context.Context, email string) (string, time.Time, error) {
|
||||||
|
user, err := s.repo.GetUserByEmail(ctx, email)
|
||||||
|
if err != nil {
|
||||||
|
return "", time.Time{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
expiresAt := time.Now().Add(passwordResetTTL)
|
||||||
|
claims := jwt.MapClaims{
|
||||||
|
"sub": user.ID.String(),
|
||||||
|
"scope": "password_reset",
|
||||||
|
}
|
||||||
|
signed, err := s.signToken(claims, expiresAt)
|
||||||
|
if err != nil {
|
||||||
|
return "", time.Time{}, err
|
||||||
|
}
|
||||||
|
return signed, expiresAt, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResetPassword validates the reset token and updates the user password.
|
||||||
|
func (s *Service) ResetPassword(ctx context.Context, tokenStr, newPassword string) error {
|
||||||
|
claims, err := s.parseToken(tokenStr)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
scope, _ := claims["scope"].(string)
|
||||||
|
if scope != "password_reset" {
|
||||||
|
return errors.New("invalid token scope")
|
||||||
|
}
|
||||||
|
|
||||||
|
sub, ok := claims["sub"].(string)
|
||||||
|
if !ok || sub == "" {
|
||||||
|
return errors.New("invalid token subject")
|
||||||
|
}
|
||||||
|
|
||||||
|
userID, err := uuid.FromString(sub)
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("invalid token subject")
|
||||||
|
}
|
||||||
|
|
||||||
|
user, err := s.repo.GetUser(ctx, userID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.UpdateUser(ctx, user, newPassword)
|
||||||
|
}
|
||||||
|
|
||||||
|
// VerifyEmail marks the user email as verified based on a JWT token.
|
||||||
|
func (s *Service) VerifyEmail(ctx context.Context, tokenStr string) (*domain.User, error) {
|
||||||
|
claims, err := s.parseToken(tokenStr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if scope, ok := claims["scope"].(string); ok && scope != "" && scope != "email_verify" {
|
||||||
|
return nil, errors.New("invalid token scope")
|
||||||
|
}
|
||||||
|
|
||||||
|
sub, ok := claims["sub"].(string)
|
||||||
|
if !ok || sub == "" {
|
||||||
|
return nil, errors.New("invalid token subject")
|
||||||
|
}
|
||||||
|
|
||||||
|
userID, err := uuid.FromString(sub)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.New("invalid token subject")
|
||||||
|
}
|
||||||
|
|
||||||
|
user, err := s.repo.GetUser(ctx, userID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !user.EmailVerified {
|
||||||
|
user.EmailVerified = true
|
||||||
|
if err := s.repo.UpdateUser(ctx, user); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return user, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) issueAccessToken(user *domain.User) (string, time.Time, error) {
|
||||||
|
expiresAt := time.Now().Add(s.tokenTTL)
|
||||||
|
claims := jwt.MapClaims{
|
||||||
|
"sub": user.ID.String(),
|
||||||
|
"role": user.Role,
|
||||||
|
"company_id": user.CompanyID.String(),
|
||||||
|
}
|
||||||
|
signed, err := s.signToken(claims, expiresAt)
|
||||||
|
if err != nil {
|
||||||
|
return "", time.Time{}, err
|
||||||
|
}
|
||||||
|
return signed, expiresAt, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) signToken(claims jwt.MapClaims, expiresAt time.Time) (string, error) {
|
||||||
|
claims["exp"] = expiresAt.Unix()
|
||||||
|
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||||
|
return token.SignedString(s.jwtSecret)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) parseToken(tokenStr string) (jwt.MapClaims, error) {
|
||||||
|
if strings.TrimSpace(tokenStr) == "" {
|
||||||
|
return nil, errors.New("token is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
claims := jwt.MapClaims{}
|
||||||
|
parser := jwt.NewParser(jwt.WithValidMethods([]string{jwt.SigningMethodHS256.Alg()}))
|
||||||
|
token, err := parser.ParseWithClaims(tokenStr, claims, func(token *jwt.Token) (any, error) {
|
||||||
|
return s.jwtSecret, nil
|
||||||
|
})
|
||||||
|
if err != nil || !token.Valid {
|
||||||
|
return nil, errors.New("invalid token")
|
||||||
|
}
|
||||||
|
return claims, nil
|
||||||
|
}
|
||||||
|
|
||||||
// VerifyCompany marks a company as verified.
|
// VerifyCompany marks a company as verified.
|
||||||
func (s *Service) VerifyCompany(ctx context.Context, id uuid.UUID) (*domain.Company, error) {
|
func (s *Service) VerifyCompany(ctx context.Context, id uuid.UUID) (*domain.Company, error) {
|
||||||
company, err := s.repo.GetCompany(ctx, id)
|
company, err := s.repo.GetCompany(ctx, id)
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue