Compare commits
9 Commits
964a06732e
...
dbe621ca05
Author | SHA1 | Date | |
---|---|---|---|
dbe621ca05 | |||
bbdbb38711 | |||
d2d524ac4e | |||
872db5b9e9 | |||
f78d78d061 | |||
e08dbea213 | |||
063e677869 | |||
13f5dba498 | |||
21c9067183 |
9
.vscode/settings.json
vendored
9
.vscode/settings.json
vendored
@ -1,9 +1,18 @@
|
||||
{
|
||||
"cSpell.words": [
|
||||
"echojwt",
|
||||
"glebarez",
|
||||
"gocook",
|
||||
"huandu",
|
||||
"labstack",
|
||||
"sqlbuilder"
|
||||
],
|
||||
"sqltools.connections": [
|
||||
{
|
||||
"previewLimit": 50,
|
||||
"driver": "SQLite",
|
||||
"database": "${workspaceFolder:go-cook}/gocook.db",
|
||||
"name": "gocook"
|
||||
}
|
||||
]
|
||||
}
|
99
api/handlers/v1/auth.go
Normal file
99
api/handlers/v1/auth.go
Normal file
@ -0,0 +1,99 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"go-cook/api/models"
|
||||
"go-cook/api/repositories"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"github.com/labstack/echo/v4"
|
||||
)
|
||||
|
||||
type JwtToken struct {
|
||||
Exp time.Time `json:"exp"`
|
||||
Authorized bool `json:"authorized"`
|
||||
UserName string `json:"username"`
|
||||
Token string `json:"token"`
|
||||
jwt.RegisteredClaims
|
||||
}
|
||||
|
||||
func generateJwt() (string, error) {
|
||||
//TODO use env here
|
||||
secret := []byte("ThisIsABadSecretDontReallyUseThis")
|
||||
|
||||
token := jwt.New(jwt.SigningMethodEdDSA)
|
||||
claims := token.Claims.(jwt.MapClaims)
|
||||
claims["exp"] = time.Now().Add(10 * time.Minute)
|
||||
claims["authorized"] = true
|
||||
claims["username"] = "someone"
|
||||
|
||||
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)
|
||||
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(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
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(),
|
||||
})
|
||||
}
|
||||
|
||||
_, err = h.userRepo.Create(username, password)
|
||||
if err != nil {
|
||||
return c.JSON(http.StatusInternalServerError, models.ErrorResponse{
|
||||
HttpCode: http.StatusInternalServerError,
|
||||
Message: err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *Handler) AuthLogin(c echo.Context) error {
|
||||
username := c.QueryParam("username")
|
||||
password := c.QueryParam("password")
|
||||
|
||||
// check if the user exists
|
||||
err := h.UserService.DoesUserExist(username)
|
||||
if err != nil {
|
||||
return c.JSON(http.StatusInternalServerError, err)
|
||||
}
|
||||
|
||||
// make sure the hash matches
|
||||
err = h.UserService.DoesPasswordMatchHash(username, password)
|
||||
if err != nil {
|
||||
return c.JSON(http.StatusInternalServerError, err)
|
||||
}
|
||||
|
||||
token, err := generateJwt()
|
||||
if err != nil {
|
||||
return c.JSON(http.StatusInternalServerError, err)
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, token)
|
||||
}
|
||||
|
||||
func (h *Handler) RefreshJwtToken(c echo.Context) error {
|
||||
return nil
|
||||
}
|
@ -47,3 +47,8 @@ func (h *Handler) HelloBody(c echo.Context) error {
|
||||
Message: fmt.Sprintf("Hello, %s", request.Name),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
func (h *Handler) ProtectedRoute(c echo.Context)error {
|
||||
return nil
|
||||
}
|
@ -3,26 +3,52 @@ package v1
|
||||
import (
|
||||
"database/sql"
|
||||
"go-cook/api/repositories"
|
||||
"go-cook/api/services"
|
||||
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
echojwt "github.com/labstack/echo-jwt/v4"
|
||||
"github.com/labstack/echo/v4"
|
||||
)
|
||||
|
||||
type Handler struct {
|
||||
userRepo repositories.UserRepository
|
||||
UserService services.UserService
|
||||
userRepo repositories.IUserTable
|
||||
recipeRepo repositories.IRecipeTable
|
||||
}
|
||||
|
||||
func NewHandler(conn *sql.DB) *Handler {
|
||||
return &Handler{
|
||||
UserService: services.NewUserService(conn),
|
||||
userRepo: repositories.NewUserRepository(conn),
|
||||
recipeRepo: repositories.NewRecipeRepository(conn),
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handler) Register(v1 *echo.Group) {
|
||||
jwtConfig := echojwt.Config{
|
||||
NewClaimsFunc: func(c echo.Context) jwt.Claims {
|
||||
return new(JwtToken)
|
||||
},
|
||||
SigningKey: []byte("ThisIsABadSecretDontReallyUseThis"),
|
||||
}
|
||||
|
||||
v1.POST("/login", h.AuthLogin)
|
||||
v1.POST("/register", h.AuthRegister)
|
||||
demo := v1.Group("/demo")
|
||||
|
||||
demo.GET("/hello", h.DemoHello)
|
||||
demo.GET("/hello/:who", h.HelloWho)
|
||||
|
||||
demo.Use(echojwt.WithConfig(jwtConfig))
|
||||
demo.GET("/hello/body", h.HelloBody)
|
||||
|
||||
users := v1.Group("/users")
|
||||
users.POST("/new", h.NewUser)
|
||||
protected := v1.Group("/demo/protected")
|
||||
protected.GET("/", h.ProtectedRoute)
|
||||
|
||||
//recipes := v1.Group("/recipe")
|
||||
|
||||
//users := v1.Group("/users")
|
||||
//users.POST("/register", h.RegisterUser)
|
||||
//users.POST("/login", h.LoginUser)
|
||||
//users.POST("/update/password", h.UpdatePassword)
|
||||
}
|
||||
|
@ -6,6 +6,10 @@ import (
|
||||
"github.com/labstack/echo/v4"
|
||||
)
|
||||
|
||||
func (h *Handler) NewUser(c echo.Context) error {
|
||||
return c.JSON(http.StatusOK, "not implemented yet")
|
||||
type newUserResponse struct {
|
||||
|
||||
}
|
||||
|
||||
func (h *Handler) RegisterUser(c echo.Context) error {
|
||||
return c.JSON(http.StatusOK, newUserResponse{})
|
||||
}
|
||||
|
12
api/models/recipe.go
Normal file
12
api/models/recipe.go
Normal file
@ -0,0 +1,12 @@
|
||||
package models
|
||||
|
||||
import "time"
|
||||
|
||||
type RecipeModel struct {
|
||||
Id int32
|
||||
Title string
|
||||
Thumbnail string
|
||||
Content string
|
||||
CreatedAt time.Time
|
||||
LastUpdated time.Time
|
||||
}
|
6
api/models/std.go
Normal file
6
api/models/std.go
Normal file
@ -0,0 +1,6 @@
|
||||
package models
|
||||
|
||||
type ErrorResponse struct {
|
||||
HttpCode int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
}
|
@ -9,3 +9,7 @@ type UserModel struct {
|
||||
CreatedAt time.Time
|
||||
LastUpdated time.Time
|
||||
}
|
||||
|
||||
type UserDto struct {
|
||||
|
||||
}
|
||||
|
45
api/repositories/recipe.go
Normal file
45
api/repositories/recipe.go
Normal file
@ -0,0 +1,45 @@
|
||||
package repositories
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"go-cook/api/models"
|
||||
)
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
type RecipeRepository struct {
|
||||
client *sql.DB
|
||||
}
|
||||
|
||||
func NewRecipeRepository(client *sql.DB) RecipeRepository {
|
||||
return RecipeRepository{
|
||||
client: client,
|
||||
}
|
||||
}
|
||||
|
||||
func (rr RecipeRepository) Create(models.RecipeModel) error {
|
||||
return errors.New("not implemented")
|
||||
}
|
||||
|
||||
func (rr RecipeRepository) List() ([]models.RecipeModel, error) {
|
||||
return []models.RecipeModel{}, errors.New("not implemented")
|
||||
}
|
||||
|
||||
func (rr RecipeRepository) Get(id int) (models.RecipeModel, error) {
|
||||
return models.RecipeModel{}, errors.New("not implemented")
|
||||
}
|
||||
|
||||
func (rr RecipeRepository) Update(id int, entity models.RecipeModel) error {
|
||||
return errors.New("not implemented")
|
||||
}
|
||||
|
||||
func (rr RecipeRepository) Delete(id int) error {
|
||||
return errors.New("not implemented")
|
||||
}
|
@ -2,6 +2,7 @@ package repositories
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"go-cook/api/models"
|
||||
"time"
|
||||
@ -12,8 +13,17 @@ import (
|
||||
|
||||
const (
|
||||
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
|
||||
UpdatePassword(name, password string) error
|
||||
CheckUserHash(name, password string) error
|
||||
}
|
||||
|
||||
// Creates a new instance of UserRepository with the bound sql
|
||||
func NewUserRepository(conn *sql.DB) UserRepository {
|
||||
return UserRepository{
|
||||
@ -37,10 +47,15 @@ func (ur UserRepository) GetByName(name string) (models.UserModel, error) {
|
||||
return models.UserModel{}, err
|
||||
}
|
||||
|
||||
return ur.processRows(rows)[0], nil
|
||||
data := ur.processRows(rows)
|
||||
if (len(data) == 0) {
|
||||
return models.UserModel{}, errors.New(ErrUserNotFound)
|
||||
}
|
||||
|
||||
func (ur UserRepository) NewUser(name, password string) (int64, error) {
|
||||
return data[0], nil
|
||||
}
|
||||
|
||||
func (ur UserRepository) Create(name, password string) (int64, error) {
|
||||
passwordBytes := []byte(password)
|
||||
hash, err := bcrypt.GenerateFromPassword(passwordBytes, bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
@ -62,6 +77,10 @@ func (ur UserRepository) NewUser(name, password string) (int64, error) {
|
||||
return 1, nil
|
||||
}
|
||||
|
||||
func (ur UserRepository) Update(id int, entity models.UserModel) error {
|
||||
return errors.New("not implemented")
|
||||
}
|
||||
|
||||
func (ur UserRepository) UpdatePassword(name, password string) error {
|
||||
_, err := ur.GetByName(name)
|
||||
if err != nil {
|
||||
|
@ -20,7 +20,7 @@ func TestCanCreateNewUser(t *testing.T) {
|
||||
defer db.Close()
|
||||
|
||||
repo := repositories.NewUserRepository(db)
|
||||
updated, err := repo.NewUser("testing", "NotSecure")
|
||||
updated, err := repo.Create("testing", "NotSecure")
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
t.FailNow()
|
||||
|
105
api/services/userService.go
Normal file
105
api/services/userService.go
Normal file
@ -0,0 +1,105 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"go-cook/api/models"
|
||||
"go-cook/api/repositories"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
const (
|
||||
ErrPasswordNotLongEnough = "password needs to be 8 character or longer"
|
||||
ErrPasswordMissingSpecialCharacter = "password needs to contain one of the following: !, @, #"
|
||||
ErrInvalidPassword = "invalid password"
|
||||
)
|
||||
|
||||
// This will handle operations that are user related, but one layer higher then the repository
|
||||
type UserService struct {
|
||||
repo repositories.IUserTable
|
||||
}
|
||||
|
||||
func NewUserService(conn *sql.DB) UserService {
|
||||
return UserService{
|
||||
repo: repositories.NewUserRepository(conn),
|
||||
}
|
||||
}
|
||||
|
||||
func (us UserService) DoesUserExist(username string) error {
|
||||
_, err := us.repo.GetByName(username)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
if model.Hash != string(hash) {
|
||||
return errors.New(ErrInvalidPassword)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (us UserService) GetUser(username string) (models.UserModel, error) {
|
||||
return us.repo.GetByName(username)
|
||||
}
|
||||
|
||||
func (us UserService) CreateNewUser(name, password string) (models.UserModel, error) {
|
||||
err := us.CheckPasswordForRequirements(password)
|
||||
if err != nil {
|
||||
return models.UserModel{}, err
|
||||
}
|
||||
|
||||
us.repo.Create(name, password)
|
||||
return models.UserModel{}, nil
|
||||
}
|
||||
|
||||
func (us UserService) CheckPasswordForRequirements(password string) error {
|
||||
err := us.checkPasswordLength(password)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = us.checkPasswordForSpecialCharacters(password)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (us UserService) checkPasswordLength(password string) error {
|
||||
if len(password) <= 8 {
|
||||
return errors.New(ErrPasswordNotLongEnough)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (us UserService) checkPasswordForSpecialCharacters(password string) error {
|
||||
var chars []string
|
||||
chars = append(chars, "!")
|
||||
chars = append(chars, "@")
|
||||
chars = append(chars, "#")
|
||||
|
||||
for _, char := range chars {
|
||||
if strings.Contains(password, char) {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return errors.New(ErrPasswordMissingSpecialCharacter)
|
||||
}
|
@ -1,43 +0,0 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"go-cook/api/models"
|
||||
"go-cook/api/repositories"
|
||||
)
|
||||
|
||||
const (
|
||||
ErrPasswordNotLongEnough = "password needs to be 8 character or longer"
|
||||
)
|
||||
|
||||
// This will handle operations that are user related, but one layer higher then the repository
|
||||
type UserService struct {
|
||||
repo repositories.UserRepository
|
||||
}
|
||||
|
||||
func NewUserService(conn *sql.DB) UserService {
|
||||
return UserService{
|
||||
repo: repositories.NewUserRepository(conn),
|
||||
}
|
||||
}
|
||||
|
||||
func (us UserService) DoesUserExist(username string) error {
|
||||
_, err := us.repo.GetByName(username)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (us UserService) CreateNewUser(name, password string) (models.UserModel, error) {
|
||||
us.repo.NewUser(name, password)
|
||||
return models.UserModel{}, nil
|
||||
}
|
||||
|
||||
func (us UserService) CheckPasswordForRequirements(password string) error {
|
||||
if len(password) <= 8 {
|
||||
return errors.New(ErrPasswordNotLongEnough)
|
||||
}
|
||||
return nil
|
||||
}
|
2
go.mod
2
go.mod
@ -21,10 +21,12 @@ require (
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/go-playground/validator v9.31.0+incompatible // indirect
|
||||
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
|
||||
github.com/golang-jwt/jwt/v5 v5.2.1 // indirect
|
||||
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/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
|
||||
github.com/labstack/gommon v0.4.2 // indirect
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
|
4
go.sum
4
go.sum
@ -34,6 +34,8 @@ github.com/go-playground/validator v9.31.0+incompatible h1:UA72EPEogEnq76ehGdEDp
|
||||
github.com/go-playground/validator v9.31.0+incompatible/go.mod h1:yrEkQXlcI+PugkyDjY2bRrL/UBU4f3rvrgkN3V8JEig=
|
||||
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
|
||||
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||
github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU=
|
||||
github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/huandu/go-assert v1.1.5/go.mod h1:yOLvuqZwmcHIC5rIzrBhT7D3Q9c3GFnd0JrPVhn/06U=
|
||||
@ -50,6 +52,8 @@ github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/labstack/echo v3.3.10+incompatible h1:pGRcYk231ExFAyoAjAfD85kQzRJCRI8bbnE7CX5OEgg=
|
||||
github.com/labstack/echo v3.3.10+incompatible/go.mod h1:0INS7j/VjnFxD4E2wkz67b8cVwCLbBmJyDaka6Cmk1s=
|
||||
github.com/labstack/echo-jwt/v4 v4.2.0 h1:odSISV9JgcSCuhgQSV/6Io3i7nUmfM/QkBeR5GVJj5c=
|
||||
github.com/labstack/echo-jwt/v4 v4.2.0/go.mod h1:MA2RqdXdEn4/uEglx0HcUOgQSyBaTh5JcaHIan3biwU=
|
||||
github.com/labstack/echo/v4 v4.11.4 h1:vDZmA+qNeh1pd/cCkEicDMrjtrnMGQ1QFI9gWN1zGq8=
|
||||
github.com/labstack/echo/v4 v4.11.4/go.mod h1:noh7EvLwqDsmh/X/HWKPUl1AjzJrhyptRyEbQJfxen8=
|
||||
github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0=
|
||||
|
1
main.go
1
main.go
@ -27,6 +27,7 @@ func main() {
|
||||
}
|
||||
e.Pre(middleware.RemoveTrailingSlash())
|
||||
e.Pre(middleware.Logger())
|
||||
e.Pre(middleware.Recover())
|
||||
|
||||
v1Group := e.Group("/api/v1")
|
||||
v1Api := v1.NewHandler(db)
|
||||
|
14
rest.http
14
rest.http
@ -1,3 +1,8 @@
|
||||
###
|
||||
POST http://localhost:1323/api/v1/register?username=test&password=test
|
||||
###
|
||||
POST http://localhost:1323/api/v1/login?username=test&password=test
|
||||
###
|
||||
GET http://localhost:1323/api/v1/demo/hello
|
||||
###
|
||||
GET http://localhost:1323/api/v1/demo/hello/james
|
||||
@ -5,5 +10,12 @@ GET http://localhost:1323/api/v1/demo/hello/james
|
||||
GET http://localhost:1323/api/v1/demo/hello/body
|
||||
Content-Type: application/json
|
||||
|
||||
name = "body"
|
||||
{
|
||||
"name": "body"
|
||||
}
|
||||
###
|
||||
|
||||
POST http://localhost:1323/api/v1/login?username=test
|
||||
|
||||
###
|
||||
GET http://localhost:1323/api/v1/demo/protected
|
Loading…
Reference in New Issue
Block a user