This commit is contained in:
2024-07-19 17:04:42 +02:00
commit 5e0d0ec69f
71 changed files with 3316 additions and 0 deletions

View File

@@ -0,0 +1,123 @@
package users
import (
"errors"
"github.com/go-playground/validator/v10"
"mangezmieux-backend/internal/logger"
"mangezmieux-backend/internal/middleware"
"mangezmieux-backend/internal/responses"
"mangezmieux-backend/internal/users/model"
"mangezmieux-backend/internal/users/service"
"net/http"
"strings"
"github.com/gin-gonic/gin"
coreValidator "mangezmieux-backend/internal/validator"
)
type Handler struct {
Service *service.Service
Validator *validator.Validate
}
func NewHandler(service *service.Service, validator *validator.Validate) *Handler {
return &Handler{
Service: service,
Validator: validator,
}
}
func (h Handler) CreateUser(context *gin.Context) {
userEditable := model.UserEditable{}
if err := context.BindJSON(&userEditable); err != nil {
responses.JSONError(context.Writer, coreValidator.NewDataValidationAPIError(err))
return
}
user, err := h.Service.CreateUser(&userEditable)
if err != nil {
logger.GetLogger().Error(err)
var apiError *responses.APIError
if errors.As(err, &apiError) {
responses.JSONError(context.Writer, *apiError)
return
}
responses.JSONErrorWithMessage(context.Writer, responses.ErrInternalServer, err.Error())
return
}
responses.JSON(context.Writer, http.StatusCreated, user)
}
func (h Handler) Login(context *gin.Context) {
userLoginRequest := model.UserLoginRequest{}
if err := context.BindJSON(&userLoginRequest); err != nil {
responses.JSONError(context.Writer, coreValidator.NewDataValidationAPIError(err))
return
}
token, err := h.Service.Login(userLoginRequest)
if err != nil {
logger.GetLogger().Error(err)
var apiError *responses.APIError
if errors.As(err, &apiError) {
responses.JSONError(context.Writer, *apiError)
return
}
responses.JSONErrorWithMessage(context.Writer, responses.ErrInternalServer, err.Error())
return
}
context.SetCookie("token", token, 10, "/", "localhost", true, false)
userLoginResponse := model.UserLoginResponse{
AccessToken: token,
TokenType: "Bearer",
}
responses.JSON(context.Writer, http.StatusOK, userLoginResponse)
}
func (h Handler) IntrospectToken(context *gin.Context) {
authorization := context.Request.Header.Get("Authorization")
splitToken := strings.Split(authorization, "Bearer ")
reqToken := splitToken[1]
user, err := h.Service.Introspect(reqToken)
if err != nil {
logger.GetLogger().Error(err)
var apiError *responses.APIError
if errors.As(err, &apiError) {
responses.JSONError(context.Writer, *apiError)
return
}
responses.JSONErrorWithMessage(context.Writer, responses.ErrInternalServer, err.Error())
return
}
responses.JSON(context.Writer, http.StatusOK, user)
}
func (h Handler) RefreshToken(context *gin.Context) {
authorization := context.Request.Header.Get("Authorization")
splitToken := strings.Split(authorization, "Bearer ")
reqToken := splitToken[1]
refreshedToken, err := h.Service.Refresh(reqToken)
if err != nil {
logger.GetLogger().Error(err)
var apiError *responses.APIError
if errors.As(err, &apiError) {
responses.JSONError(context.Writer, *apiError)
return
}
responses.JSONErrorWithMessage(context.Writer, responses.ErrInternalServer, err.Error())
return
}
context.SetCookie("token", refreshedToken, 10, "/", "localhost", true, false)
}
func (h Handler) GetMe(context *gin.Context) {
usr, exists := context.Get(middleware.CtxUser)
if !exists {
responses.JSONErrorWithMessage(context.Writer, responses.ErrInternalServer, "User not found in context")
return
}
responses.JSON(context.Writer, http.StatusOK, usr)
}

View File

@@ -0,0 +1,44 @@
package users
import (
"context"
"github.com/gin-gonic/gin"
model2 "mangezmieux-backend/internal/acl/model"
"mangezmieux-backend/internal/acl/service"
"mangezmieux-backend/internal/jwt"
"mangezmieux-backend/internal/middleware"
"mangezmieux-backend/internal/users/model"
service2 "mangezmieux-backend/internal/users/service"
)
var AuthMiddleware = newMiddleware()
type internalAuthMiddleware struct {
Service *service2.Service
RoleService service.Service
UserService service.Service
}
func newMiddleware() *internalAuthMiddleware {
return &internalAuthMiddleware{}
}
func (m *internalAuthMiddleware) GinMiddleware(jwtService *jwt.Service) gin.HandlerFunc {
return middleware.GetAuthenticationMiddleware(m, jwtService)
}
// delegate useful for deferred binding (when the middleware is installed, GinMiddleware() is called, the service m.Service is not yet created :-( )
// see cmd/app.go for deferred binding at the end.
func (m *internalAuthMiddleware) Introspect(token string) (*model.User, error) {
return m.Service.Introspect(token)
}
// delegate useful for deferred binding (when the middleware is installed, GinMiddleware() is called, the service m.Service is not yet created :-( )
// see cmd/app.go for deferred binding at the end.
func (m *internalAuthMiddleware) GetRole(ctx context.Context, user *model.User) (*model2.UserRight, error) {
return m.RoleService.GetRoleForCurrentUser(user)
}
func (m *internalAuthMiddleware) GetAllRole(ctx context.Context) ([]*model2.Role, error) {
return m.RoleService.GetAllRole()
}

View File

@@ -0,0 +1,31 @@
package model
import (
"github.com/gofrs/uuid"
"time"
)
type User struct {
ID *uuid.UUID `json:"ID"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt *time.Time `json:"updatedAt"`
UserEditable
}
type UserEditable struct {
Firstname string `json:"first_name" binding:"required"`
Lastname string `json:"last_name" binding:"required"`
Email string `json:"email" binding:"required"`
Password string `json:"password" binding:"required"`
}
type UserLoginRequest struct {
Email string `json:"email" binding:"required"`
Password string `json:"password" binding:"required"`
}
type UserLoginResponse struct {
AccessToken string `json:"accessToken"`
TokenType string `json:"tokenType"`
ExpiresIn string `json:"expiresIn"`
}

View File

@@ -0,0 +1,98 @@
package service
import (
"mangezmieux-backend/internal/jwt"
"mangezmieux-backend/internal/responses"
"mangezmieux-backend/internal/users/model"
"mangezmieux-backend/internal/users/sql"
"time"
"golang.org/x/crypto/bcrypt"
)
type Service struct {
dao sql.Dao
jwt *jwt.Service
}
func NewService(dao sql.Dao, jwt *jwt.Service) *Service {
return &Service{dao: dao, jwt: jwt}
}
func (s *Service) CreateUser(userEditable *model.UserEditable) (*model.User, error) {
now := time.Now()
user := &model.User{
ID: nil,
CreatedAt: now,
UpdatedAt: &now,
UserEditable: model.UserEditable{
Firstname: userEditable.Firstname,
Lastname: userEditable.Lastname,
Email: userEditable.Email,
Password: userEditable.Password,
},
}
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(userEditable.Password), 8)
if err != nil {
return nil, &responses.ErrInternalServer
}
user.Password = string(hashedPassword)
err = s.dao.Create(user)
if err != nil {
return nil, err
}
user.Password = ""
return user, nil
}
func (s *Service) Login(request model.UserLoginRequest) (string, error) {
user, err := s.dao.FindByMail(request.Email)
if err != nil {
return "", &responses.ErrUnauthorized
}
err = bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(request.Password))
if err != nil {
return "", &responses.ErrUnauthorized
}
return s.jwt.GenerateJWTToken(user.ID.String())
}
func (s *Service) Introspect(token string) (*model.User, error) {
claims, err := s.jwt.ValidateToken(token)
if err != nil {
return nil, err
}
usr, err := s.dao.FindByID(claims.ID)
if err != nil {
return nil, err
}
userModel := s.transformEntityToResponse(usr, false)
return userModel, nil
}
func (s *Service) Refresh(oldToken string) (string, error) {
return s.jwt.Refresh(oldToken)
}
func (s *Service) transformEntityToResponse(user *model.User, withPassword bool) *model.User {
password := ""
if withPassword {
password = user.Password
}
return &model.User{
ID: user.ID,
CreatedAt: user.CreatedAt,
UpdatedAt: user.UpdatedAt,
UserEditable: model.UserEditable{
Firstname: user.Firstname,
Lastname: user.Lastname,
Email: user.Email,
Password: password,
},
}
}

View File

@@ -0,0 +1,42 @@
package users
import (
"database/sql"
"mangezmieux-backend/internal/ginserver"
"mangezmieux-backend/internal/injector"
"mangezmieux-backend/internal/jwt"
"mangezmieux-backend/internal/postgres"
service2 "mangezmieux-backend/internal/users/service"
sql2 "mangezmieux-backend/internal/users/sql"
"mangezmieux-backend/internal/validator"
"net/http"
"github.com/gin-gonic/gin"
validatorv10 "github.com/go-playground/validator/v10"
)
const ServiceKey = "UsersService"
func Setup(inj *injector.Injector) {
publicRoute := injector.Get[*gin.RouterGroup](inj, ginserver.UnsecuredRouterInjectorKey)
validatorCli := injector.Get[*validatorv10.Validate](inj, validator.ValidatorInjectorKey)
jwtService := injector.Get[*jwt.Service](inj, jwt.JWTKey)
client := injector.Get[*sql.DB](inj, postgres.DatabaseKey)
dao := sql2.NewDao(client)
service := service2.NewService(dao, jwtService)
handler := NewHandler(service, validatorCli)
inj.Set(ServiceKey, service)
publicRoute.Handle(http.MethodPost, "/api/v1/users", handler.CreateUser)
publicRoute.Handle(http.MethodPost, "/oauth2/token", handler.Login)
publicRoute.Handle(http.MethodPost, "/oauth2/introspect", handler.IntrospectToken)
publicRoute.Handle(http.MethodPost, "/oauth2/refresh", handler.RefreshToken)
securedRoute := injector.Get[*gin.RouterGroup](inj, ginserver.SecuredRouterInjectorKey)
securedRoute.Handle(http.MethodGet, "/users/me", handler.GetMe)
}

View File

@@ -0,0 +1,11 @@
package sql
import "mangezmieux-backend/internal/users/model"
type Dao interface {
FindByMail(mail string) (*model.User, error)
Create(user *model.User) error
Delete(mail string) error
FindByMailAndPassword(mail string, password string) (*model.User, error)
FindByID(id string) (*model.User, error)
}

View File

@@ -0,0 +1,111 @@
package sql
import (
"database/sql"
"errors"
"mangezmieux-backend/internal/postgres"
"mangezmieux-backend/internal/users/model"
"github.com/lib/pq"
)
type SQLDao struct {
client *sql.DB
}
func NewDao(client *sql.DB) Dao {
return &SQLDao{client: client}
}
func (sqlDAO *SQLDao) FindByMailAndPassword(mail string, password string) (*model.User, error) {
q := `
SELECT u.ID, u.first_name, u.last_name, u.creation_date, u.last_update_date
FROM mangezmieux.user u
WHERE u.email = $1 AND u.password = $2
`
row := sqlDAO.client.QueryRow(q, mail, password)
u := model.User{}
err := row.Scan(&u.Email, &u.Firstname, &u.Lastname, &u.CreatedAt, &u.UpdatedAt)
var errPq *pq.Error
if errors.As(err, &errPq) {
return nil, postgres.HandlePgError(errPq)
}
if errors.Is(err, sql.ErrNoRows) {
return nil, postgres.NewDAOError(postgres.ErrTypeNotFound, err)
}
return &u, err
}
func (sqlDAO *SQLDao) FindByMail(mail string) (*model.User, error) {
q := `
SELECT u.ID, u.email, u.first_name, u.last_name, u.creation_date, u.last_update_date, u.password
FROM mangezmieux.user u
WHERE u.email = $1
`
row := sqlDAO.client.QueryRow(q, mail)
u := model.User{}
err := row.Scan(&u.ID, &u.Email, &u.Firstname, &u.Lastname, &u.CreatedAt, &u.UpdatedAt, &u.Password)
var errPq *pq.Error
if errors.As(err, &errPq) {
return nil, postgres.HandlePgError(errPq)
}
if errors.Is(err, sql.ErrNoRows) {
return nil, postgres.NewDAOError(postgres.ErrTypeNotFound, err)
}
return &u, err
}
func (sqlDAO *SQLDao) FindByID(id string) (*model.User, error) {
q := `
SELECT u.ID, u.email, u.first_name, u.last_name, u.creation_date, u.last_update_date
FROM mangezmieux.user u
WHERE u.id = $1
`
row := sqlDAO.client.QueryRow(q, id)
u := model.User{}
err := row.Scan(&u.ID, &u.Email, &u.Firstname, &u.Lastname, &u.CreatedAt, &u.UpdatedAt)
var errPq *pq.Error
if errors.As(err, &errPq) {
return nil, postgres.HandlePgError(errPq)
}
if errors.Is(err, sql.ErrNoRows) {
return nil, postgres.NewDAOError(postgres.ErrTypeNotFound, err)
}
return &u, err
}
func (sqlDAO *SQLDao) Create(user *model.User) error {
q := `
INSERT INTO mangezmieux.user
(email, password, first_name, last_name, creation_date, last_update_date)
VALUES
($1, $2, $3, $4, $5, $6)
RETURNING id, creation_date
`
err := sqlDAO.client.
QueryRow(q, user.Email, user.Password, user.Firstname, user.Lastname, user.CreatedAt, user.UpdatedAt).
Scan(&user.ID, &user.CreatedAt)
var errPq *pq.Error
if errors.As(err, &errPq) {
return postgres.HandlePgError(errPq)
}
return err
}
func (sqlDAO *SQLDao) Delete(id string) error {
q := `
DELETE FROM mangezmieux.user
WHERE id = $1
`
_, err := sqlDAO.client.Exec(q, id)
var errPq *pq.Error
if errors.As(err, &errPq) {
return postgres.HandlePgError(errPq)
}
return err
}