Merge pull request 'features/working-on-scopes' (#13) from features/working-on-scopes into main
Reviewed-on: #13
This commit is contained in:
commit
0c7c4d311e
1
.gitignore
vendored
1
.gitignore
vendored
@ -10,6 +10,7 @@
|
||||
*.dylib
|
||||
go-cook
|
||||
*.db
|
||||
*.env
|
||||
|
||||
# Test binary, built with `go test -c`
|
||||
*.test
|
||||
|
7
api/domain/dto.go
Normal file
7
api/domain/dto.go
Normal file
@ -0,0 +1,7 @@
|
||||
package domain
|
||||
|
||||
type UserDto struct {
|
||||
Id int `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Scopes string `json:"scopes"`
|
||||
}
|
21
api/domain/entities.go
Normal file
21
api/domain/entities.go
Normal file
@ -0,0 +1,21 @@
|
||||
package domain
|
||||
|
||||
import "time"
|
||||
|
||||
type UserEntity struct {
|
||||
Id int
|
||||
CreatedAt time.Time
|
||||
LastUpdated time.Time
|
||||
Name string
|
||||
Hash string
|
||||
Scopes string
|
||||
}
|
||||
|
||||
type RecipeEntity struct {
|
||||
Id int32
|
||||
CreatedAt time.Time
|
||||
LastUpdated time.Time
|
||||
Title string
|
||||
Thumbnail string
|
||||
Content string
|
||||
}
|
6
api/domain/models.go
Normal file
6
api/domain/models.go
Normal file
@ -0,0 +1,6 @@
|
||||
package domain
|
||||
|
||||
type EnvConfig struct {
|
||||
AdminToken string
|
||||
JwtSecret string
|
||||
}
|
10
api/domain/requests.go
Normal file
10
api/domain/requests.go
Normal file
@ -0,0 +1,10 @@
|
||||
package domain
|
||||
|
||||
type HelloBodyRequest struct {
|
||||
Name string `json:"name" validate:"required"`
|
||||
}
|
||||
|
||||
type UpdateScopesRequest struct {
|
||||
Username string `json:"name"`
|
||||
Scopes []string `json:"scopes" validate:"required"`
|
||||
}
|
12
api/domain/responses.go
Normal file
12
api/domain/responses.go
Normal file
@ -0,0 +1,12 @@
|
||||
package domain
|
||||
|
||||
type ErrorResponse struct {
|
||||
Success bool `json:"success"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
type HelloWhoResponse struct {
|
||||
Success bool `json:"success"`
|
||||
Error string `json:"error"`
|
||||
Message string `json:"message"`
|
||||
}
|
8
api/domain/scopes.go
Normal file
8
api/domain/scopes.go
Normal file
@ -0,0 +1,8 @@
|
||||
package domain
|
||||
|
||||
const (
|
||||
ScopeAll = "all"
|
||||
ScopeRecipeRead = "recipe:read"
|
||||
ScopeRecipeCreate = "recipe:create"
|
||||
ScopeRecipeDelete = "recipe:delete"
|
||||
)
|
@ -2,75 +2,58 @@ package v1
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"go-cook/api/models"
|
||||
"go-cook/api/domain"
|
||||
"go-cook/api/repositories"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"github.com/labstack/echo/v4"
|
||||
)
|
||||
|
||||
const (
|
||||
ErrJwtMissing = "auth token is missing"
|
||||
ErrJwtClaimsMissing = "claims missing on token"
|
||||
ErrJwtExpired = "auth token has expired"
|
||||
ErrJwtMissing = "auth token is missing"
|
||||
ErrJwtClaimsMissing = "claims missing on token"
|
||||
ErrJwtExpired = "auth token has expired"
|
||||
ErrJwtScopeMissing = "required scope is missing"
|
||||
ErrUserNotFound = "requested user does not exist"
|
||||
ErrUsernameAlreadyExists = "the requested username already exists"
|
||||
)
|
||||
|
||||
type JwtToken struct {
|
||||
Exp time.Time `json:"exp"`
|
||||
Authorized bool `json:"authorized"`
|
||||
UserName string `json:"username"`
|
||||
Token string `json:"token"`
|
||||
jwt.RegisteredClaims
|
||||
}
|
||||
|
||||
func generateJwt(username string) (string, error) {
|
||||
//TODO use env here
|
||||
secret := []byte("ThisIsABadSecretDontReallyUseThis")
|
||||
|
||||
token := jwt.New(jwt.SigningMethodHS256)
|
||||
claims := token.Claims.(jwt.MapClaims)
|
||||
claims["exp"] = time.Now().Add(10 * time.Minute)
|
||||
claims["authorized"] = true
|
||||
claims["username"] = username
|
||||
|
||||
tokenString, err := token.SignedString(secret)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return tokenString, nil
|
||||
}
|
||||
|
||||
func (h *Handler) AuthRegister(c echo.Context) error {
|
||||
username := c.QueryParam("username")
|
||||
_, err := h.userRepo.GetByName(username)
|
||||
username := c.FormValue("username")
|
||||
password := c.FormValue("password")
|
||||
|
||||
//username := c.QueryParam("username")
|
||||
exists, err := h.userRepo.GetByName(username)
|
||||
if err != nil {
|
||||
// if we have an err, validate that if its not user not found.
|
||||
// if the user is not found, we can use that name
|
||||
if err.Error() != repositories.ErrUserNotFound {
|
||||
return c.JSON(http.StatusInternalServerError, models.ErrorResponse{
|
||||
HttpCode: http.StatusInternalServerError,
|
||||
Message: err.Error(),
|
||||
return c.JSON(http.StatusInternalServerError, domain.ErrorResponse{
|
||||
|
||||
Message: err.Error(),
|
||||
Success: true,
|
||||
})
|
||||
}
|
||||
}
|
||||
if exists.Name == username {
|
||||
return h.InternalServerErrorResponse(c, ErrUsernameAlreadyExists)
|
||||
}
|
||||
|
||||
password := c.QueryParam("password")
|
||||
//password := c.QueryParam("password")
|
||||
err = h.UserService.CheckPasswordForRequirements(password)
|
||||
if err != nil {
|
||||
return c.JSON(http.StatusInternalServerError, models.ErrorResponse{
|
||||
HttpCode: http.StatusInternalServerError,
|
||||
Message: err.Error(),
|
||||
return c.JSON(http.StatusInternalServerError, domain.ErrorResponse{
|
||||
Success: false,
|
||||
Message: err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
_, err = h.userRepo.Create(username, password)
|
||||
_, err = h.userRepo.Create(username, password, domain.ScopeRecipeRead)
|
||||
if err != nil {
|
||||
return c.JSON(http.StatusInternalServerError, models.ErrorResponse{
|
||||
HttpCode: http.StatusInternalServerError,
|
||||
Message: err.Error(),
|
||||
return c.JSON(http.StatusInternalServerError, domain.ErrorResponse{
|
||||
Success: false,
|
||||
Message: err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
@ -78,29 +61,113 @@ func (h *Handler) AuthRegister(c echo.Context) error {
|
||||
}
|
||||
|
||||
func (h *Handler) AuthLogin(c echo.Context) error {
|
||||
username := c.QueryParam("username")
|
||||
password := c.QueryParam("password")
|
||||
username := c.FormValue("name")
|
||||
password := c.FormValue("password")
|
||||
|
||||
// Check to see if they are trying to login with the admin token
|
||||
if username == "" {
|
||||
return h.validateAdminToken(c, password)
|
||||
}
|
||||
|
||||
// check if the user exists
|
||||
err := h.UserService.DoesUserExist(username)
|
||||
if err != nil {
|
||||
return c.JSON(http.StatusInternalServerError, err)
|
||||
return h.InternalServerErrorResponse(c, err.Error())
|
||||
}
|
||||
|
||||
// make sure the hash matches
|
||||
err = h.UserService.DoesPasswordMatchHash(username, password)
|
||||
if err != nil {
|
||||
return c.JSON(http.StatusInternalServerError, err)
|
||||
return h.InternalServerErrorResponse(c, err.Error())
|
||||
}
|
||||
|
||||
token, err := generateJwt(username)
|
||||
token, err := h.generateJwt(username)
|
||||
if err != nil {
|
||||
return c.JSON(http.StatusInternalServerError, err)
|
||||
return h.InternalServerErrorResponse(c, err.Error())
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, token)
|
||||
}
|
||||
|
||||
func (h *Handler) validateAdminToken(c echo.Context, password string) error {
|
||||
// if the admin token is blank, then the admin wanted this disabled.
|
||||
// this will fail right away and not progress.
|
||||
if h.Config.AdminToken == "" {
|
||||
return h.InternalServerErrorResponse(c, ErrUserNotFound)
|
||||
}
|
||||
|
||||
if h.Config.AdminToken != password {
|
||||
return h.ReturnUnauthorizedResponse(c, ErrUserNotFound)
|
||||
}
|
||||
|
||||
token, err := h.generateAdminJwt("admin")
|
||||
if err != nil {
|
||||
return h.InternalServerErrorResponse(c, err.Error())
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, token)
|
||||
}
|
||||
|
||||
func (h *Handler) AddScopes(c echo.Context) error {
|
||||
token, err := h.getJwtToken(c)
|
||||
if err != nil {
|
||||
return h.ReturnUnauthorizedResponse(c, err.Error())
|
||||
}
|
||||
|
||||
err = token.IsValid(domain.ScopeAll)
|
||||
if err != nil {
|
||||
return h.ReturnUnauthorizedResponse(c, err.Error())
|
||||
}
|
||||
|
||||
request := domain.UpdateScopesRequest{}
|
||||
err = (&echo.DefaultBinder{}).BindBody(c, &request)
|
||||
if err != nil {
|
||||
return c.JSON(http.StatusBadRequest, domain.ErrorResponse{
|
||||
Success: false,
|
||||
Message: err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
err = h.UserService.AddScopes(request.Username, request.Scopes)
|
||||
if err != nil {
|
||||
return h.InternalServerErrorResponse(c, err.Error())
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, domain.ErrorResponse{
|
||||
Success: true,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *Handler) RemoveScopes(c echo.Context) error {
|
||||
token, err := h.getJwtToken(c)
|
||||
if err != nil {
|
||||
return h.ReturnUnauthorizedResponse(c, err.Error())
|
||||
}
|
||||
|
||||
err = token.IsValid(domain.ScopeAll)
|
||||
if err != nil {
|
||||
return h.ReturnUnauthorizedResponse(c, err.Error())
|
||||
}
|
||||
|
||||
request := domain.UpdateScopesRequest{}
|
||||
err = (&echo.DefaultBinder{}).BindBody(c, &request)
|
||||
if err != nil {
|
||||
return c.JSON(http.StatusBadRequest, domain.ErrorResponse{
|
||||
Success: false,
|
||||
Message: err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
err = h.UserService.RemoveScopes(request.Username, request.Scopes)
|
||||
if err != nil {
|
||||
return h.InternalServerErrorResponse(c, err.Error())
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, domain.ErrorResponse{
|
||||
Success: true,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *Handler) RefreshJwtToken(c echo.Context) error {
|
||||
return nil
|
||||
}
|
||||
@ -118,11 +185,5 @@ func (h *Handler) getJwtToken(c echo.Context) (JwtToken, error) {
|
||||
return JwtToken{}, errors.New(ErrJwtClaimsMissing)
|
||||
}
|
||||
|
||||
// Check to see if the token has expired
|
||||
hasExpired := claims.Exp.Compare(time.Now())
|
||||
if hasExpired == -1 {
|
||||
return JwtToken{}, errors.New(ErrJwtExpired)
|
||||
}
|
||||
|
||||
return *claims, nil
|
||||
}
|
||||
|
@ -2,20 +2,14 @@ package v1
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"go-cook/api/models"
|
||||
"go-cook/api/domain"
|
||||
"net/http"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
)
|
||||
|
||||
type HelloWhoResponse struct {
|
||||
Success bool `json:"success"`
|
||||
Error string `error:"error"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
func (h *Handler) DemoHello(c echo.Context) error {
|
||||
return c.JSON(http.StatusOK, HelloWhoResponse{
|
||||
return c.JSON(http.StatusOK, domain.HelloWhoResponse{
|
||||
Success: true,
|
||||
Message: "Hello world!",
|
||||
})
|
||||
@ -23,27 +17,23 @@ func (h *Handler) DemoHello(c echo.Context) error {
|
||||
|
||||
func (h *Handler) HelloWho(c echo.Context) error {
|
||||
name := c.Param("who")
|
||||
return c.JSON(http.StatusOK, HelloWhoResponse{
|
||||
return c.JSON(http.StatusOK, domain.HelloWhoResponse{
|
||||
Success: true,
|
||||
Message: fmt.Sprintf("Hello, %s", name),
|
||||
})
|
||||
}
|
||||
|
||||
type HelloBodyRequest struct {
|
||||
Name string `json:"name" validate:"required"`
|
||||
}
|
||||
|
||||
func (h *Handler) HelloBody(c echo.Context) error {
|
||||
request := HelloBodyRequest{}
|
||||
request := domain.HelloBodyRequest{}
|
||||
err := (&echo.DefaultBinder{}).BindBody(c, &request)
|
||||
if err != nil {
|
||||
return c.JSON(http.StatusBadRequest, HelloWhoResponse{
|
||||
return c.JSON(http.StatusBadRequest, domain.HelloWhoResponse{
|
||||
Success: false,
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, HelloWhoResponse{
|
||||
return c.JSON(http.StatusOK, domain.HelloWhoResponse{
|
||||
Success: true,
|
||||
Message: fmt.Sprintf("Hello, %s", request.Name),
|
||||
})
|
||||
@ -52,10 +42,12 @@ func (h *Handler) HelloBody(c echo.Context) error {
|
||||
func (h *Handler) ProtectedRoute(c echo.Context) error {
|
||||
token, err := h.getJwtToken(c)
|
||||
if err != nil {
|
||||
return c.JSON(http.StatusForbidden, models.ErrorResponse{
|
||||
HttpCode: http.StatusForbidden,
|
||||
Message: err.Error(),
|
||||
})
|
||||
h.ReturnUnauthorizedResponse(c, err.Error())
|
||||
}
|
||||
|
||||
err = token.IsValid(domain.ScopeRecipeRead)
|
||||
if err != nil {
|
||||
h.ReturnUnauthorizedResponse(c, ErrJwtScopeMissing)
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, token)
|
||||
|
@ -2,8 +2,10 @@ package v1
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"go-cook/api/domain"
|
||||
"go-cook/api/repositories"
|
||||
"go-cook/api/services"
|
||||
"net/http"
|
||||
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
echojwt "github.com/labstack/echo-jwt/v4"
|
||||
@ -11,16 +13,19 @@ import (
|
||||
)
|
||||
|
||||
type Handler struct {
|
||||
Config domain.EnvConfig
|
||||
|
||||
UserService services.UserService
|
||||
userRepo repositories.IUserTable
|
||||
recipeRepo repositories.IRecipeTable
|
||||
}
|
||||
|
||||
func NewHandler(conn *sql.DB) *Handler {
|
||||
func NewHandler(conn *sql.DB, cfg domain.EnvConfig) *Handler {
|
||||
return &Handler{
|
||||
Config: cfg,
|
||||
UserService: services.NewUserService(conn),
|
||||
userRepo: repositories.NewUserRepository(conn),
|
||||
recipeRepo: repositories.NewRecipeRepository(conn),
|
||||
userRepo: repositories.NewUserRepository(conn),
|
||||
recipeRepo: repositories.NewRecipeRepository(conn),
|
||||
}
|
||||
}
|
||||
|
||||
@ -29,13 +34,17 @@ func (h *Handler) Register(v1 *echo.Group) {
|
||||
NewClaimsFunc: func(c echo.Context) jwt.Claims {
|
||||
return new(JwtToken)
|
||||
},
|
||||
SigningKey: []byte("ThisIsABadSecretDontReallyUseThis"),
|
||||
SigningKey: []byte(h.Config.JwtSecret),
|
||||
}
|
||||
|
||||
v1.POST("/login", h.AuthLogin)
|
||||
v1.POST("/register", h.AuthRegister)
|
||||
auth := v1.Group("/auth")
|
||||
auth.POST("/login", h.AuthLogin)
|
||||
auth.POST("/register", h.AuthRegister)
|
||||
auth.Use(echojwt.WithConfig(jwtConfig))
|
||||
auth.POST("/scopes/add", h.AddScopes)
|
||||
auth.POST("/scopes/remove", h.RemoveScopes)
|
||||
|
||||
demo := v1.Group("/demo")
|
||||
|
||||
demo.GET("/hello", h.DemoHello)
|
||||
demo.GET("/hello/:who", h.HelloWho)
|
||||
|
||||
@ -53,3 +62,17 @@ func (h *Handler) Register(v1 *echo.Group) {
|
||||
//users.POST("/login", h.LoginUser)
|
||||
//users.POST("/update/password", h.UpdatePassword)
|
||||
}
|
||||
|
||||
func (h *Handler) ReturnUnauthorizedResponse(c echo.Context, message string) error {
|
||||
return c.JSON(http.StatusUnauthorized, domain.ErrorResponse{
|
||||
Success: false,
|
||||
Message: message,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *Handler) InternalServerErrorResponse(c echo.Context, message string) error {
|
||||
return c.JSON(http.StatusServiceUnavailable, domain.ErrorResponse{
|
||||
Success: false,
|
||||
Message: message,
|
||||
})
|
||||
}
|
||||
|
98
api/handlers/v1/jwt.go
Normal file
98
api/handlers/v1/jwt.go
Normal file
@ -0,0 +1,98 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"go-cook/api/domain"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
)
|
||||
|
||||
type JwtToken struct {
|
||||
Exp time.Time `json:"exp"`
|
||||
Iss string `json:"iss"`
|
||||
Authorized bool `json:"authorized"`
|
||||
UserName string `json:"username"`
|
||||
Scopes []string `json:"scopes"`
|
||||
jwt.RegisteredClaims
|
||||
}
|
||||
|
||||
func (j JwtToken) IsValid(scope string) error {
|
||||
err := j.hasExpired()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = j.hasScope(scope)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (j JwtToken) hasExpired() error {
|
||||
// Check to see if the token has expired
|
||||
hasExpired := j.Exp.Compare(time.Now())
|
||||
if hasExpired == -1 {
|
||||
return errors.New(ErrJwtExpired)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (j JwtToken) hasScope(scope string) error {
|
||||
// they have the scope to access everything, so let them pass.
|
||||
if strings.Contains(domain.ScopeAll, scope) {
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, s := range j.Scopes {
|
||||
if strings.Contains(s, scope) {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return errors.New(ErrJwtScopeMissing)
|
||||
}
|
||||
|
||||
func (h *Handler) generateJwt(username string) (string, error) {
|
||||
secret := []byte(h.Config.JwtSecret)
|
||||
|
||||
token := jwt.New(jwt.SigningMethodHS256)
|
||||
claims := token.Claims.(jwt.MapClaims)
|
||||
claims["exp"] = time.Now().Add(10 * time.Minute)
|
||||
claims["authorized"] = true
|
||||
claims["username"] = username
|
||||
|
||||
var scopes []string
|
||||
scopes = append(scopes, domain.ScopeRecipeRead)
|
||||
claims["scopes"] = scopes
|
||||
|
||||
tokenString, err := token.SignedString(secret)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return tokenString, nil
|
||||
}
|
||||
|
||||
func (h *Handler) generateAdminJwt(username string) (string, error) {
|
||||
secret := []byte(h.Config.JwtSecret)
|
||||
|
||||
token := jwt.New(jwt.SigningMethodHS256)
|
||||
claims := token.Claims.(jwt.MapClaims)
|
||||
claims["exp"] = time.Now().Add(10 * time.Minute)
|
||||
claims["authorized"] = true
|
||||
claims["username"] = username
|
||||
|
||||
var scopes []string
|
||||
scopes = append(scopes, domain.ScopeAll)
|
||||
claims["scopes"] = scopes
|
||||
|
||||
tokenString, err := token.SignedString(secret)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return tokenString, nil
|
||||
}
|
10
api/migrations/20240329211828_user_scopes.sql
Normal file
10
api/migrations/20240329211828_user_scopes.sql
Normal file
@ -0,0 +1,10 @@
|
||||
-- +goose Up
|
||||
-- +goose StatementBegin
|
||||
SELECT 'up SQL query';
|
||||
ALTER Table USERS ADD Scopes TEXT;
|
||||
-- +goose StatementEnd
|
||||
-- +goose Down
|
||||
-- +goose StatementBegin
|
||||
SELECT 'down SQL query';
|
||||
ALTER TABLE USERS DROP COLUMN Scopes;
|
||||
-- +goose StatementEnd
|
@ -1,12 +0,0 @@
|
||||
package models
|
||||
|
||||
import "time"
|
||||
|
||||
type RecipeModel struct {
|
||||
Id int32
|
||||
Title string
|
||||
Thumbnail string
|
||||
Content string
|
||||
CreatedAt time.Time
|
||||
LastUpdated time.Time
|
||||
}
|
@ -1,6 +0,0 @@
|
||||
package models
|
||||
|
||||
type ErrorResponse struct {
|
||||
HttpCode int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
}
|
@ -1,15 +0,0 @@
|
||||
package models
|
||||
|
||||
import "time"
|
||||
|
||||
type UserModel struct {
|
||||
Id int
|
||||
Name string
|
||||
Hash string
|
||||
CreatedAt time.Time
|
||||
LastUpdated time.Time
|
||||
}
|
||||
|
||||
type UserDto struct {
|
||||
|
||||
}
|
@ -3,15 +3,15 @@ package repositories
|
||||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"go-cook/api/models"
|
||||
"go-cook/api/domain"
|
||||
)
|
||||
|
||||
type IRecipeTable interface {
|
||||
Create(models.RecipeModel) error
|
||||
List() ([]models.RecipeModel, error)
|
||||
Get(id int) (models.RecipeModel, error)
|
||||
Update(id int, entity models.RecipeModel) error
|
||||
Delete(id int) error
|
||||
Create(domain.RecipeEntity) error
|
||||
List() ([]domain.RecipeEntity, error)
|
||||
Get(id int) (domain.RecipeEntity, error)
|
||||
Update(id int, entity domain.RecipeEntity) error
|
||||
Delete(id int) error
|
||||
}
|
||||
|
||||
type RecipeRepository struct {
|
||||
@ -24,22 +24,22 @@ func NewRecipeRepository(client *sql.DB) RecipeRepository {
|
||||
}
|
||||
}
|
||||
|
||||
func (rr RecipeRepository) Create(models.RecipeModel) error {
|
||||
func (rr RecipeRepository) Create(domain.RecipeEntity) error {
|
||||
return errors.New("not implemented")
|
||||
}
|
||||
|
||||
func (rr RecipeRepository) List() ([]models.RecipeModel, error) {
|
||||
return []models.RecipeModel{}, errors.New("not implemented")
|
||||
func (rr RecipeRepository) List() ([]domain.RecipeEntity, error) {
|
||||
return []domain.RecipeEntity{}, errors.New("not implemented")
|
||||
}
|
||||
|
||||
func (rr RecipeRepository) Get(id int) (models.RecipeModel, error) {
|
||||
return models.RecipeModel{}, errors.New("not implemented")
|
||||
func (rr RecipeRepository) Get(id int) (domain.RecipeEntity, error) {
|
||||
return domain.RecipeEntity{}, errors.New("not implemented")
|
||||
}
|
||||
|
||||
func (rr RecipeRepository) Update(id int, entity models.RecipeModel) error {
|
||||
func (rr RecipeRepository) Update(id int, entity domain.RecipeEntity) error {
|
||||
return errors.New("not implemented")
|
||||
}
|
||||
|
||||
func (rr RecipeRepository) Delete(id int) error {
|
||||
return errors.New("not implemented")
|
||||
}
|
||||
}
|
||||
|
@ -4,7 +4,7 @@ import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"go-cook/api/models"
|
||||
"go-cook/api/domain"
|
||||
"time"
|
||||
|
||||
"github.com/huandu/go-sqlbuilder"
|
||||
@ -12,16 +12,17 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
TableName string = "users"
|
||||
TableName string = "users"
|
||||
ErrUserNotFound string = "requested user was not found"
|
||||
)
|
||||
|
||||
type IUserTable interface {
|
||||
GetByName(name string) (models.UserModel, error)
|
||||
Create(name, password string) (int64, error)
|
||||
Update(id int, entity models.UserModel) error
|
||||
GetByName(name string) (domain.UserEntity, error)
|
||||
Create(name, password, scope string) (int64, error)
|
||||
Update(id int, entity domain.UserEntity) error
|
||||
UpdatePassword(name, password string) error
|
||||
CheckUserHash(name, password string) error
|
||||
UpdateScopes(name, scope string) error
|
||||
}
|
||||
|
||||
// Creates a new instance of UserRepository with the bound sql
|
||||
@ -35,7 +36,7 @@ type UserRepository struct {
|
||||
connection *sql.DB
|
||||
}
|
||||
|
||||
func (ur UserRepository) GetByName(name string) (models.UserModel, error) {
|
||||
func (ur UserRepository) GetByName(name string) (domain.UserEntity, error) {
|
||||
builder := sqlbuilder.NewSelectBuilder()
|
||||
builder.Select("*").From("users").Where(
|
||||
builder.E("Name", name),
|
||||
@ -44,18 +45,18 @@ func (ur UserRepository) GetByName(name string) (models.UserModel, error) {
|
||||
|
||||
rows, err := ur.connection.Query(query, args...)
|
||||
if err != nil {
|
||||
return models.UserModel{}, err
|
||||
return domain.UserEntity{}, err
|
||||
}
|
||||
|
||||
data := ur.processRows(rows)
|
||||
if (len(data) == 0) {
|
||||
return models.UserModel{}, errors.New(ErrUserNotFound)
|
||||
if len(data) == 0 {
|
||||
return domain.UserEntity{}, errors.New(ErrUserNotFound)
|
||||
}
|
||||
|
||||
return data[0], nil
|
||||
}
|
||||
|
||||
func (ur UserRepository) Create(name, password string) (int64, error) {
|
||||
func (ur UserRepository) Create(name, password, scope string) (int64, error) {
|
||||
passwordBytes := []byte(password)
|
||||
hash, err := bcrypt.GenerateFromPassword(passwordBytes, bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
@ -65,8 +66,8 @@ func (ur UserRepository) Create(name, password string) (int64, error) {
|
||||
dt := time.Now()
|
||||
queryBuilder := sqlbuilder.NewInsertBuilder()
|
||||
queryBuilder.InsertInto("users")
|
||||
queryBuilder.Cols("Name", "Hash", "LastUpdated", "CreatedAt")
|
||||
queryBuilder.Values(name, string(hash), dt, dt)
|
||||
queryBuilder.Cols("Name", "Hash", "LastUpdated", "CreatedAt", "Scopes")
|
||||
queryBuilder.Values(name, string(hash), dt, dt, scope)
|
||||
query, args := queryBuilder.Build()
|
||||
|
||||
_, err = ur.connection.Exec(query, args...)
|
||||
@ -77,7 +78,7 @@ func (ur UserRepository) Create(name, password string) (int64, error) {
|
||||
return 1, nil
|
||||
}
|
||||
|
||||
func (ur UserRepository) Update(id int, entity models.UserModel) error {
|
||||
func (ur UserRepository) Update(id int, entity domain.UserEntity) error {
|
||||
return errors.New("not implemented")
|
||||
}
|
||||
|
||||
@ -109,8 +110,26 @@ func (ur UserRepository) CheckUserHash(name, password string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ur UserRepository) processRows(rows *sql.Rows) []models.UserModel {
|
||||
items := []models.UserModel{}
|
||||
func (ur UserRepository) UpdateScopes(name, scope string) error {
|
||||
builder := sqlbuilder.NewUpdateBuilder()
|
||||
builder.Update("users")
|
||||
builder.Set (
|
||||
builder.Assign("Scopes", scope),
|
||||
)
|
||||
builder.Where(
|
||||
builder.Equal("Name", name),
|
||||
)
|
||||
query, args := builder.Build()
|
||||
|
||||
_, err := ur.connection.Exec(query, args...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ur UserRepository) processRows(rows *sql.Rows) []domain.UserEntity {
|
||||
items := []domain.UserEntity{}
|
||||
|
||||
for rows.Next() {
|
||||
var id int
|
||||
@ -118,15 +137,17 @@ func (ur UserRepository) processRows(rows *sql.Rows) []models.UserModel {
|
||||
var hash string
|
||||
var createdAt time.Time
|
||||
var lastUpdated time.Time
|
||||
err := rows.Scan(&id, &name, &hash, &createdAt, &lastUpdated)
|
||||
var scopes string
|
||||
err := rows.Scan(&id, &name, &hash, &createdAt, &lastUpdated, &scopes)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
|
||||
items = append(items, models.UserModel{
|
||||
items = append(items, domain.UserEntity{
|
||||
Id: id,
|
||||
Name: name,
|
||||
Hash: hash,
|
||||
Scopes: scopes,
|
||||
CreatedAt: createdAt,
|
||||
LastUpdated: lastUpdated,
|
||||
})
|
||||
|
@ -2,6 +2,7 @@ package repositories_test
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"go-cook/api/domain"
|
||||
"go-cook/api/repositories"
|
||||
"log"
|
||||
"testing"
|
||||
@ -20,7 +21,7 @@ func TestCanCreateNewUser(t *testing.T) {
|
||||
defer db.Close()
|
||||
|
||||
repo := repositories.NewUserRepository(db)
|
||||
updated, err := repo.Create("testing", "NotSecure")
|
||||
updated, err := repo.Create("testing", "NotSecure", domain.ScopeRecipeRead)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
t.FailNow()
|
||||
|
21
api/services/env.go
Normal file
21
api/services/env.go
Normal file
@ -0,0 +1,21 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"go-cook/api/domain"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"github.com/joho/godotenv"
|
||||
)
|
||||
|
||||
func NewEnvConfig() domain.EnvConfig {
|
||||
err := godotenv.Load()
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
|
||||
return domain.EnvConfig{
|
||||
AdminToken: os.Getenv("AdminToken"),
|
||||
JwtSecret: os.Getenv("JwtSecret"),
|
||||
}
|
||||
}
|
@ -3,7 +3,7 @@ package services
|
||||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"go-cook/api/models"
|
||||
"go-cook/api/domain"
|
||||
"go-cook/api/repositories"
|
||||
"strings"
|
||||
|
||||
@ -36,12 +36,6 @@ func (us UserService) DoesUserExist(username string) error {
|
||||
}
|
||||
|
||||
func (us UserService) DoesPasswordMatchHash(username, password string) error {
|
||||
//passwordBytes := []byte(password)
|
||||
//hash, err := bcrypt.GenerateFromPassword(passwordBytes, bcrypt.DefaultCost)
|
||||
//if err != nil {
|
||||
// return err
|
||||
//}
|
||||
|
||||
model, err := us.GetUser(username)
|
||||
if err != nil {
|
||||
return err
|
||||
@ -55,18 +49,75 @@ func (us UserService) DoesPasswordMatchHash(username, password string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (us UserService) GetUser(username string) (models.UserModel, error) {
|
||||
func (us UserService) GetUser(username string) (domain.UserEntity, error) {
|
||||
return us.repo.GetByName(username)
|
||||
}
|
||||
|
||||
func (us UserService) CreateNewUser(name, password string) (models.UserModel, error) {
|
||||
err := us.CheckPasswordForRequirements(password)
|
||||
func (us UserService) AddScopes(username string, scopes []string) error {
|
||||
usr, err := us.repo.GetByName(username)
|
||||
if err != nil {
|
||||
return models.UserModel{}, err
|
||||
return err
|
||||
}
|
||||
|
||||
us.repo.Create(name, password)
|
||||
return models.UserModel{}, nil
|
||||
if usr.Name != username {
|
||||
return errors.New(repositories.ErrUserNotFound)
|
||||
}
|
||||
|
||||
currentScopes := strings.Split(usr.Scopes, ",")
|
||||
|
||||
// check the current scopes
|
||||
for _, item := range scopes {
|
||||
if !strings.Contains(usr.Scopes, item) {
|
||||
currentScopes = append(currentScopes, item)
|
||||
}
|
||||
}
|
||||
return us.repo.UpdateScopes(username, strings.Join(currentScopes, ","))
|
||||
}
|
||||
|
||||
func (us UserService) RemoveScopes(username string, scopes []string) error {
|
||||
usr, err := us.repo.GetByName(username)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if usr.Name != username {
|
||||
return errors.New(repositories.ErrUserNotFound)
|
||||
}
|
||||
|
||||
var newScopes []string
|
||||
|
||||
// check all the scopes that are currently assigned
|
||||
for _, item := range strings.Split(usr.Scopes, ",") {
|
||||
|
||||
// check the scopes given, if one matches skip it
|
||||
if us.doesScopeExist(scopes, item) {
|
||||
continue
|
||||
}
|
||||
|
||||
// did not match, add it
|
||||
newScopes = append(newScopes, item)
|
||||
}
|
||||
|
||||
return us.repo.UpdateScopes(username, strings.Join(newScopes, ","))
|
||||
}
|
||||
|
||||
func (us UserService) doesScopeExist(scopes []string, target string) bool {
|
||||
for _, item := range scopes {
|
||||
if item == target {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (us UserService) CreateNewUser(name, password, scope string) (domain.UserEntity, error) {
|
||||
err := us.CheckPasswordForRequirements(password)
|
||||
if err != nil {
|
||||
return domain.UserEntity{}, err
|
||||
}
|
||||
|
||||
us.repo.Create(name, password, domain.ScopeRecipeRead)
|
||||
return domain.UserEntity{}, nil
|
||||
}
|
||||
|
||||
func (us UserService) CheckPasswordForRequirements(password string) error {
|
||||
|
1
go.mod
1
go.mod
@ -25,6 +25,7 @@ require (
|
||||
github.com/google/uuid v1.5.0 // indirect
|
||||
github.com/huandu/go-sqlbuilder v1.25.0 // indirect
|
||||
github.com/huandu/xstrings v1.3.2 // indirect
|
||||
github.com/joho/godotenv v1.5.1 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/labstack/echo-jwt/v4 v4.2.0 // indirect
|
||||
github.com/labstack/echo/v4 v4.11.4 // indirect
|
||||
|
2
go.sum
2
go.sum
@ -43,6 +43,8 @@ github.com/huandu/go-sqlbuilder v1.25.0 h1:h1l+6CqeCviPJCnkEZoRGNdfZ5RO9BKMvG3A+
|
||||
github.com/huandu/go-sqlbuilder v1.25.0/go.mod h1:nUVmMitjOmn/zacMLXT0d3Yd3RHoO2K+vy906JzqxMI=
|
||||
github.com/huandu/xstrings v1.3.2 h1:L18LIDzqlW6xN2rEkpdV8+oL/IXWJ1APd+vsdYy4Wdw=
|
||||
github.com/huandu/xstrings v1.3.2/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
|
||||
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||
github.com/kisielk/sqlstruct v0.0.0-20201105191214-5f3e10d3ab46/go.mod h1:yyMNCyc/Ib3bDTKd379tNMpB/7/H5TjM2Y9QJ5THLbE=
|
||||
|
5
main.go
5
main.go
@ -3,6 +3,7 @@ package main
|
||||
import (
|
||||
"database/sql"
|
||||
v1 "go-cook/api/handlers/v1"
|
||||
"go-cook/api/services"
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
@ -21,6 +22,8 @@ func main() {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
cfg := services.NewEnvConfig()
|
||||
|
||||
e := echo.New()
|
||||
e.Validator = &CustomValidator{
|
||||
validator: validator.New(),
|
||||
@ -30,7 +33,7 @@ func main() {
|
||||
e.Pre(middleware.Recover())
|
||||
|
||||
v1Group := e.Group("/api/v1")
|
||||
v1Api := v1.NewHandler(db)
|
||||
v1Api := v1.NewHandler(db, cfg)
|
||||
v1Api.Register(v1Group)
|
||||
|
||||
e.Logger.Fatal(e.Start(":1323"))
|
||||
|
47
rest.http
47
rest.http
@ -1,7 +1,48 @@
|
||||
### Create a standard User
|
||||
POST http://localhost:1323/api/v1/auth/register
|
||||
Content-Type: application/x-www-form-urlencoded
|
||||
|
||||
name=test&password=test1234!
|
||||
### Login with user
|
||||
POST http://localhost:1323/api/v1/auth/login
|
||||
Content-Type: application/x-www-form-urlencoded
|
||||
|
||||
name=test&password=test1234!
|
||||
|
||||
|
||||
### Login with the admin token
|
||||
POST http://localhost:1323/api/v1/auth/login
|
||||
Content-Type: application/x-www-form-urlencoded
|
||||
|
||||
password=lol
|
||||
|
||||
|
||||
### Add Scope to test user
|
||||
POST http://localhost:1323/api/v1/auth/scopes/add
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer
|
||||
|
||||
{
|
||||
"name": "test",
|
||||
"scopes": [
|
||||
"recipe:create"
|
||||
]
|
||||
}
|
||||
|
||||
### Remove scope from test user
|
||||
POST http://localhost:1323/api/v1/auth/scopes/remove
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer
|
||||
|
||||
{
|
||||
"name": "test",
|
||||
"scopes": [
|
||||
"recipe:create"
|
||||
]
|
||||
}
|
||||
|
||||
###
|
||||
POST http://localhost:1323/api/v1/register?username=test&password=test1234!
|
||||
###
|
||||
POST http://localhost:1323/api/v1/login?username=test&password=test1234!
|
||||
POST http://localhost:1323/api/v1/
|
||||
###
|
||||
GET http://localhost:1323/api/v1/demo/hello
|
||||
###
|
||||
|
Loading…
Reference in New Issue
Block a user