Compare commits

..

No commits in common. "7d6e7d46cdc5fb10e5cbdc0feca2a176c7f3fbd8" and "029710ad31db0ef01f9b8d618c0879e3c7ecbcb8" have entirely different histories.

15 changed files with 104 additions and 157 deletions

View File

@ -1,7 +1,5 @@
# Brings the database up to the current migration
migrate-up:
GOOSE_DRIVER=sqlite3 GOOSE_DBSTRING=./cmd/gocook.db goose -dir ./internal/migrations up
GOOSE_DRIVER=sqlite3 GOOSE_DBSTRING=./gocook.db goose -dir ./api/migrations up
# Rolls back one migration at a time
migrate-down:
GOOSE_DRIVER=sqlite3 GOOSE_DBSTRING=./cmd/gocook.db goose -dir ./internal/migrations down
GOOSE_DRIVER=sqlite3 GOOSE_DBSTRING=./gocook.db goose -dir ./api/migrations down

2
go.mod
View File

@ -7,7 +7,6 @@ require (
github.com/glebarez/go-sqlite v1.22.0
github.com/go-playground/validator v9.31.0+incompatible
github.com/golang-jwt/jwt/v5 v5.2.1
github.com/google/uuid v1.6.0
github.com/huandu/go-sqlbuilder v1.25.0
github.com/joho/godotenv v1.5.1
github.com/labstack/echo-jwt/v4 v4.2.0
@ -23,6 +22,7 @@ require (
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/huandu/xstrings v1.3.2 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/labstack/gommon v0.4.2 // indirect

View File

@ -3,7 +3,7 @@ package domain
import "time"
type UserEntity struct {
Id int64
Id int
CreatedAt time.Time
LastUpdated time.Time
Name string
@ -12,15 +12,16 @@ type UserEntity struct {
}
type RefreshTokenEntity struct {
Id int64
Id int
Username string
Token string
ExpiresAt time.Time
CreatedAt time.Time
LastUpdated time.Time
}
type RecipeEntity struct {
Id int64
Id int32
CreatedAt time.Time
LastUpdated time.Time
Title string

View File

@ -12,5 +12,5 @@ type UpdateScopesRequest struct {
type RefreshTokenRequest struct {
Username string `json:"username"`
RefreshToken string `json:"refreshToken"`
//ExpiresAt time.Time `json:"expiresAt"`
ExpiresAt string `json:"expiresAt"`
}

View File

@ -1,30 +1,19 @@
package domain
type LoginResponse struct {
BaseResponse
Success bool `json:"success"`
Token string `json:"token"`
Type string `json:"type"`
RefreshToken string `json:"refreshToken"`
}
// This /
// /auth/refreshToken
type TokenRefreshResponse struct {
ErrorResponse
}
type BaseResponse struct {
Success bool `json:"success"`
Message string `json:"message"`
}
type ErrorResponse struct {
Success bool `json:"success"`
Message string `json:"message"`
}
type HelloWhoResponse struct {
BaseResponse
Success bool `json:"success"`
Error string `json:"error"`
Message string `json:"message"`
}

View File

@ -3,7 +3,6 @@ package v1
import (
"errors"
"net/http"
"time"
"git.jamestombleson.com/jtom38/go-cook/internal/domain"
"git.jamestombleson.com/jtom38/go-cook/internal/repositories"
@ -26,12 +25,13 @@ func (h *Handler) AuthRegister(c echo.Context) error {
password := c.FormValue("password")
//username := c.QueryParam("username")
exists, err := h.users.GetUser(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, domain.ErrorResponse{
Message: err.Error(),
Success: true,
})
@ -42,7 +42,7 @@ func (h *Handler) AuthRegister(c echo.Context) error {
}
//password := c.QueryParam("password")
err = h.users.CheckPasswordForRequirements(password)
err = h.UserService.CheckPasswordForRequirements(password)
if err != nil {
return c.JSON(http.StatusInternalServerError, domain.ErrorResponse{
Success: false,
@ -50,7 +50,7 @@ func (h *Handler) AuthRegister(c echo.Context) error {
})
}
_, err = h.users.Create(username, password, domain.ScopeRecipeRead)
_, err = h.userRepo.Create(username, password, domain.ScopeRecipeRead)
if err != nil {
return c.JSON(http.StatusInternalServerError, domain.ErrorResponse{
Success: false,
@ -73,38 +73,27 @@ func (h *Handler) AuthLogin(c echo.Context) error {
}
// check if the user exists
err := h.users.DoesUserExist(username)
err := h.UserService.DoesUserExist(username)
if err != nil {
return h.InternalServerErrorResponse(c, err.Error())
}
// make sure the hash matches
err = h.users.DoesPasswordMatchHash(username, password)
err = h.UserService.DoesPasswordMatchHash(username, password)
if err != nil {
return h.InternalServerErrorResponse(c, err.Error())
}
// TODO think about moving this down some?
expiresAt := time.Now().Add(time.Hour * 48)
jwt, err := h.generateJwtWithExp(username, h.Config.ApiUri, expiresAt)
if err != nil {
return h.InternalServerErrorResponse(c, err.Error())
}
refresh, err := h.refreshTokens.Create(username)
token, err := h.generateJwt(username, h.Config.ApiUri)
if err != nil {
return h.InternalServerErrorResponse(c, err.Error())
}
return c.JSON(http.StatusOK, domain.LoginResponse{
BaseResponse: domain.BaseResponse{
Success: true,
Message: "OK",
},
Token: jwt,
Token: token,
Type: "Bearer",
RefreshToken: refresh,
RefreshToken: "",
})
}
@ -119,7 +108,7 @@ func (h *Handler) validateAdminToken(c echo.Context, password string) error {
return h.ReturnUnauthorizedResponse(c, ErrUserNotFound)
}
token, err := h.generateJwt("admin", h.Config.ApiUri)
token, err := h.generateAdminJwt("admin")
if err != nil {
return h.InternalServerErrorResponse(c, err.Error())
}
@ -127,39 +116,14 @@ func (h *Handler) validateAdminToken(c echo.Context, password string) error {
return c.JSON(http.StatusOK, token)
}
// This will take collect some information about the requested refresh, validate and then return a new jwt token if approved.
func (h *Handler) RefreshJwtToken(c echo.Context) error {
func (h *Handler) GenerateRefreshToken(c echo.Context) error {
// Check the context for the refresh token
var request domain.RefreshTokenRequest
err := (&echo.DefaultBinder{}).BindBody(c, &request)
if err != nil {
return h.InternalServerErrorResponse(c, err.Error())
return err
}
err = h.refreshTokens.IsRequestValid(request.Username, request.RefreshToken)
if err != nil {
return h.InternalServerErrorResponse(c, err.Error())
}
jwt, err := h.generateJwtWithExp(request.Username, h.Config.ApiUri, time.Now().Add(time.Hour * 48))
if err!= nil {
return h.InternalServerErrorResponse(c, err.Error())
}
newRefreshToken, err := h.refreshTokens.Create(request.Username)
if err != nil {
return h.InternalServerErrorResponse(c, err.Error())
}
return c.JSON(http.StatusOK, domain.LoginResponse{
BaseResponse: domain.BaseResponse{
Success: true,
Message: "OK",
},
Token: jwt,
Type: "Bearer",
RefreshToken: newRefreshToken,
})
h.refreshTokenRepo.Create()
}
func (h *Handler) AddScopes(c echo.Context) error {
@ -182,7 +146,7 @@ func (h *Handler) AddScopes(c echo.Context) error {
})
}
err = h.users.AddScopes(request.Username, request.Scopes)
err = h.UserService.AddScopes(request.Username, request.Scopes)
if err != nil {
return h.InternalServerErrorResponse(c, err.Error())
}
@ -212,7 +176,7 @@ func (h *Handler) RemoveScopes(c echo.Context) error {
})
}
err = h.users.RemoveScopes(request.Username, request.Scopes)
err = h.UserService.RemoveScopes(request.Username, request.Scopes)
if err != nil {
return h.InternalServerErrorResponse(c, err.Error())
}
@ -222,6 +186,10 @@ func (h *Handler) RemoveScopes(c echo.Context) error {
})
}
func (h *Handler) RefreshJwtToken(c echo.Context) error {
return nil
}
func (h *Handler) getJwtToken(c echo.Context) (JwtToken, error) {
// Make sure that the request came with a jwtToken
token, ok := c.Get("user").(*jwt.Token)

View File

@ -11,20 +11,16 @@ import (
func (h *Handler) DemoHello(c echo.Context) error {
return c.JSON(http.StatusOK, domain.HelloWhoResponse{
BaseResponse: domain.BaseResponse{
Success: true,
Message: "Hello world!",
},
})
}
func (h *Handler) HelloWho(c echo.Context) error {
name := c.Param("who")
return c.JSON(http.StatusOK, domain.HelloWhoResponse{
BaseResponse: domain.BaseResponse{
Success: true,
Message: fmt.Sprintf("Hello, %s", name),
},
})
}
@ -32,14 +28,15 @@ func (h *Handler) HelloBody(c echo.Context) error {
request := domain.HelloBodyRequest{}
err := (&echo.DefaultBinder{}).BindBody(c, &request)
if err != nil {
return h.InternalServerErrorResponse(c, err.Error())
return c.JSON(http.StatusBadRequest, domain.HelloWhoResponse{
Success: false,
Error: err.Error(),
})
}
return c.JSON(http.StatusOK, domain.HelloWhoResponse{
BaseResponse: domain.BaseResponse{
Success: true,
Message: fmt.Sprintf("Hello, %s", request.Name),
},
})
}

View File

@ -5,6 +5,7 @@ import (
"net/http"
"git.jamestombleson.com/jtom38/go-cook/internal/domain"
"git.jamestombleson.com/jtom38/go-cook/internal/repositories"
"git.jamestombleson.com/jtom38/go-cook/internal/services"
"github.com/golang-jwt/jwt/v5"
@ -15,18 +16,19 @@ import (
type Handler struct {
Config domain.EnvConfig
users services.UserService
recipes services.Recipes
refreshTokens services.RefreshToken
UserService services.UserService
userRepo repositories.IUserTable
recipeRepo repositories.IRecipeTable
refreshTokenRepo repositories.RefreshTokenRepository
}
func NewHandler(conn *sql.DB, cfg domain.EnvConfig) *Handler {
return &Handler{
Config: cfg,
users: services.NewUserService(conn),
recipes: services.NewRecipesService(conn),
refreshTokens: services.NewRefreshTokenService(conn),
UserService: services.NewUserService(conn),
userRepo: repositories.NewUserRepository(conn),
recipeRepo: repositories.NewRecipeRepository(conn),
refreshTokenRepo: repositories.NewRefreshTokenRepository(conn),
}
}
@ -44,7 +46,6 @@ func (h *Handler) Register(v1 *echo.Group) {
auth.Use(echojwt.WithConfig(jwtConfig))
auth.POST("/scopes/add", h.AddScopes)
auth.POST("/scopes/remove", h.RemoveScopes)
auth.POST("/refreshToken", h.RefreshJwtToken)
demo := v1.Group("/demo")
demo.GET("/hello", h.DemoHello)

View File

@ -57,28 +57,40 @@ func (j JwtToken) hasScope(scope string) error {
}
func (h *Handler) generateJwt(username, issuer string) (string, error) {
return h.generateJwtWithExp(username, issuer, time.Now().Add(10 * time.Minute))
}
func (h *Handler) generateJwtWithExp(username, issuer string, expiresAt time.Time) (string, error) {
secret := []byte(h.Config.JwtSecret)
// Anyone who wants to decrypt the key needs to use the same method
token := jwt.New(jwt.SigningMethodHS256)
claims := token.Claims.(jwt.MapClaims)
claims["exp"] = expiresAt
claims["exp"] = time.Now().Add(10 * time.Minute)
claims["authorized"] = true
claims["username"] = username
claims["iss"] = issuer
var scopes []string
if (username == "admin") {
scopes = append(scopes, domain.ScopeAll)
claims["scopes"] = scopes
} else {
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 {

View File

@ -5,6 +5,7 @@ CREATE TABLE RefreshTokens (
ID INTEGER PRIMARY KEY AUTOINCREMENT,
Username TEXT NOT NULL,
Token TEXT NOT NULL,
ExpiresAt DATETIME NOT NULL,
CreatedAt DATETIME NOT NULL,
LastUpdated DATETIME NOT NULL
)

View File

@ -15,7 +15,7 @@ const (
)
type RefreshTokenTable interface {
Create(username string, token string) (int64, error)
Create(username string, token string, expiresAt time.Time) (int64, error)
GetByUsername(name string) (domain.RefreshTokenEntity, error)
DeleteById(id int64) (int64, error)
}
@ -30,12 +30,12 @@ func NewRefreshTokenRepository(conn *sql.DB) RefreshTokenRepository {
}
}
func (rt RefreshTokenRepository) Create(username string, token string) (int64, error) {
func (rt RefreshTokenRepository) Create(username string, token string, expiresAt time.Time) (int64, error) {
dt := time.Now()
builder := sqlbuilder.NewInsertBuilder()
builder.InsertInto(refreshTokenTableName)
builder.Cols("Username", "Token", "CreatedAt", "LastUpdated")
builder.Values(username, token, dt, dt)
builder.Cols("Username", "Token", "ExpiresAt", "CreatedAt", "LastUpdated")
builder.Values(username, token, expiresAt, dt, dt)
query, args := builder.Build()
_, err := rt.connection.Exec(query, args...)
@ -89,10 +89,11 @@ func (rd RefreshTokenRepository) processRows(rows *sql.Rows) []domain.RefreshTok
var id int64
var username string
var token string
var expiresAt time.Time
var createdAt time.Time
var lastUpdated time.Time
err := rows.Scan(&id, &username, &token, &createdAt, &lastUpdated)
err := rows.Scan(&id, &username, &token, &expiresAt, &createdAt, &lastUpdated)
if err != nil {
fmt.Println(err)
}
@ -101,6 +102,7 @@ func (rd RefreshTokenRepository) processRows(rows *sql.Rows) []domain.RefreshTok
Id: id,
Username: username,
Token: token,
ExpiresAt: expiresAt,
CreatedAt: createdAt,
LastUpdated: lastUpdated,
})

View File

@ -3,6 +3,7 @@ package repositories_test
import (
"database/sql"
"testing"
"time"
"git.jamestombleson.com/jtom38/go-cook/internal/repositories"
_ "github.com/glebarez/go-sqlite"
@ -17,7 +18,7 @@ func TestRefreshTokenCreate(t *testing.T) {
}
client := repositories.NewRefreshTokenRepository(conn)
rows, err := client.Create("tester", "BadTokenDontUse")
rows, err := client.Create("tester", "BadTokenDontUse", time.Now().Add(time.Hour+1))
if err != nil {
t.Log(err)
t.FailNow()
@ -36,7 +37,7 @@ func TestRefreshTokenGetByUsername(t *testing.T) {
}
client := repositories.NewRefreshTokenRepository(conn)
rows, err := client.Create("tester", "BadTokenDoNotUse")
rows, err := client.Create("tester", "BadTokenDoNotUse", time.Now().Add(time.Hour+1))
if err != nil {
t.Log(err)
t.FailNow()
@ -67,7 +68,7 @@ func TestRefreshTokenDeleteById(t *testing.T) {
}
client := repositories.NewRefreshTokenRepository(conn)
_, err = client.Create("tester", "BadTokenDoNotUse")
_, err = client.Create("tester", "BadTokenDoNotUse", time.Now().Add(time.Hour+1))
if err != nil {
t.Log(err)
t.FailNow()

View File

@ -133,7 +133,7 @@ func (ur UserRepository) processRows(rows *sql.Rows) []domain.UserEntity {
items := []domain.UserEntity{}
for rows.Next() {
var id int64
var id int
var name string
var hash string
var createdAt time.Time

View File

@ -3,25 +3,20 @@ package services
import (
"database/sql"
"errors"
"time"
"git.jamestombleson.com/jtom38/go-cook/internal/domain"
"git.jamestombleson.com/jtom38/go-cook/internal/repositories"
"github.com/google/uuid"
)
const (
ErrUnexpectedAmountOfRowsUpdated = "got a unexpected of rows updated"
)
type RefreshToken interface {
Create(username string) (string, error)
Create(username string, expiresAt time.Time) (string, error)
GetByName(name string) (domain.RefreshTokenEntity, error)
Delete(id int64) (int64, error)
IsRequestValid(username, refreshToken string) error
IsRequestValid(username, refreshToken string, jwtExpiresAt time.Time) error
}
// A new jwt token can be made if the user has the correct refresh token for the user.
// It will also require the old JWT token so the expire time is pulled and part of the validation
type RefreshTokenService struct {
table repositories.RefreshTokenTable
}
@ -32,26 +27,13 @@ func NewRefreshTokenService(conn *sql.DB) RefreshTokenService {
}
}
func (rt RefreshTokenService) Create(username string) (string, error) {
//if a refresh token already exists for a user, reuse
existingToken, err := rt.GetByName(username)
if err == nil {
rowsRemoved, err := rt.Delete(existingToken.Id)
if err != nil {
return "", err
}
if rowsRemoved != 1 {
return "", errors.New(ErrUnexpectedAmountOfRowsUpdated)
}
}
func (rt RefreshTokenService) Create(username string, expiresAt time.Time) (string, error) {
token, err := uuid.NewV7()
if err != nil {
return "", err
}
rows, err := rt.table.Create(username, token.String())
rows, err := rt.table.Create(username, token.String(), expiresAt)
if err != nil {
return "", err
}
@ -72,15 +54,19 @@ func (rt RefreshTokenService) Delete(id int64) (int64, error) {
return rt.table.DeleteById(id)
}
func (rt RefreshTokenService) IsRequestValid(username, refreshToken string) error {
func (rt RefreshTokenService) IsRequestValid(username, refreshToken string, jwtExpiresAt time.Time) error {
token, err := rt.GetByName(username)
if err != nil {
return err
}
if token.Token != refreshToken {
if (token.Token != refreshToken) {
return errors.New("the refresh token given does not match")
}
if (token.ExpiresAt != jwtExpiresAt) {
return errors.New("the time when the jwt token expires does not match what was given")
}
return nil
}

View File

@ -16,15 +16,6 @@ Content-Type: application/x-www-form-urlencoded
password=lol
### Try to refresh the token
POST http://localhost:1323/api/v1/auth/refreshToken
Content-Type: application/json
Authorization: Bearer
{
"username": "test",
"refreshToken": ""
}
### Add Scope to test user
POST http://localhost:1323/api/v1/auth/scopes/add