Compare commits

...

29 Commits

Author SHA1 Message Date
0c7c4d311e Merge pull request 'features/working-on-scopes' (#13) from features/working-on-scopes into main
Reviewed-on: #13
2024-04-04 15:31:53 -07:00
e0a517a765 Added the example on how to remove a scope from someone 2024-04-04 15:31:23 -07:00
69fb7a683b updated how it looks to see what scopes to add 2024-04-04 15:30:59 -07:00
8f0e8e4d85 added remove scopes to the handler 2024-04-04 15:30:38 -07:00
9bc36bae7f if the admin token is null then it will fail an admin login. Also added the remove scopes logic and it worked for me 2024-04-04 15:30:22 -07:00
8a43c166a8 Renamed struct to update a users scopes 2024-04-04 15:29:39 -07:00
2e0596c924 rest test file has been updated 2024-04-03 16:12:34 -07:00
7dc072e849 scopes/add now requires jwt and minor ez response methods 2024-04-03 16:12:18 -07:00
02c6f4aae7 admin jwt now follows the same schema 2024-04-03 16:11:46 -07:00
f159582d34 user repo now adds a default scope on create and scopes can be replaced for a user 2024-04-03 16:11:00 -07:00
f67ed03c9d add and remove scopes methods to user service 2024-04-03 16:10:25 -07:00
29f6dc0bb0 minor domain updates 2024-04-03 16:09:31 -07:00
d9625e0b7d updated how the err response should look to follow a pattern 2024-04-02 16:30:14 -07:00
373f7da678 auth now uses UrlFormEncoded like it should 2024-04-01 17:50:07 -07:00
c071212df5 when a user is made, the default scope is now defined 2024-04-01 17:48:54 -07:00
b3ee4e420b found a bug that would let the same username get used over and over 2024-04-01 17:48:38 -07:00
f591dadd3b adminToken validation now has its own method 2024-03-31 18:10:53 -07:00
593ce11c6b moved jwt things to its own file 2024-03-31 18:09:24 -07:00
615f1184ab if a user provides the env admin token, a token will generate with god permissions 2024-03-31 18:05:33 -07:00
8f10fbfba1 mostly reworking how the JWT is processed 2024-03-31 17:49:09 -07:00
ec24600269 mostly pushing things into domain 2024-03-31 17:48:44 -07:00
0d90db89e5 added config to the Handler and trying out a standard payload return for unauthorized 2024-03-31 17:48:23 -07:00
538ff69c00 ignoring all .env files 2024-03-31 17:47:52 -07:00
7360d429ff domain update 2024-03-31 17:47:39 -07:00
e0253f04e7 Added Scopes to the user table 2024-03-31 17:47:24 -07:00
0d253270b7 added config service on startup 2024-03-31 17:47:09 -07:00
6af8a2a0cd refactored for domain 2024-03-31 17:46:40 -07:00
09235760d2 Added a config service 2024-03-31 17:46:20 -07:00
3487feed5c moved models into domain 2024-03-31 17:46:04 -07:00
24 changed files with 521 additions and 164 deletions

1
.gitignore vendored
View File

@ -10,6 +10,7 @@
*.dylib
go-cook
*.db
*.env
# Test binary, built with `go test -c`
*.test

7
api/domain/dto.go Normal file
View 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
View 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
View File

@ -0,0 +1,6 @@
package domain
type EnvConfig struct {
AdminToken string
JwtSecret string
}

10
api/domain/requests.go Normal file
View 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
View 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
View File

@ -0,0 +1,8 @@
package domain
const (
ScopeAll = "all"
ScopeRecipeRead = "recipe:read"
ScopeRecipeCreate = "recipe:create"
ScopeRecipeDelete = "recipe:delete"
)

View File

@ -2,10 +2,9 @@ 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"
@ -15,61 +14,45 @@ const (
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,
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,
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,
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
}

View File

@ -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)

View File

@ -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,13 +13,16 @@ 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),
@ -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)
demo := v1.Group("/demo")
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
View 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
}

View 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

View File

@ -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
}

View File

@ -1,6 +0,0 @@
package models
type ErrorResponse struct {
HttpCode int `json:"code"`
Message string `json:"message"`
}

View File

@ -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 {
}

View File

@ -3,14 +3,14 @@ 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
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
}
@ -24,19 +24,19 @@ 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")
}

View File

@ -4,7 +4,7 @@ import (
"database/sql"
"errors"
"fmt"
"go-cook/api/models"
"go-cook/api/domain"
"time"
"github.com/huandu/go-sqlbuilder"
@ -17,11 +17,12 @@ const (
)
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,
})

View File

@ -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
View 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"),
}
}

View File

@ -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
View File

@ -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
View File

@ -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=

View File

@ -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"))

View File

@ -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
###