Refresh Token Support and package refactor based on best practice docs #18

Merged
jtom38 merged 15 commits from features/restructure-go-recommendations into main 2024-04-21 10:30:52 -07:00
23 changed files with 290 additions and 24 deletions
Showing only changes of commit a2e740eefd - Show all commits

View File

@ -11,6 +11,15 @@ type UserEntity struct {
Scopes string
}
type RefreshTokenEntity struct {
Id int
Username string
Token string
ExpiresAt time.Time
CreatedAt time.Time
LastUpdated time.Time
}
type RecipeEntity struct {
Id int32
CreatedAt time.Time

View File

@ -8,3 +8,9 @@ type UpdateScopesRequest struct {
Username string `json:"username"`
Scopes []string `json:"scopes" validate:"required"`
}
type RefreshTokenRequest struct {
Username string `json:"username"`
RefreshToken string `json:"refreshToken"`
ExpiresAt string `json:"expiresAt"`
}

View File

@ -4,8 +4,8 @@ import (
"errors"
"net/http"
"git.jamestombleson.com/jtom38/go-cook/api/domain"
"git.jamestombleson.com/jtom38/go-cook/api/repositories"
"git.jamestombleson.com/jtom38/go-cook/internal/domain"
"git.jamestombleson.com/jtom38/go-cook/internal/repositories"
"github.com/golang-jwt/jwt/v5"
"github.com/labstack/echo/v4"
@ -116,6 +116,16 @@ func (h *Handler) validateAdminToken(c echo.Context, password string) error {
return c.JSON(http.StatusOK, token)
}
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 err
}
h.refreshTokenRepo.Create()
}
func (h *Handler) AddScopes(c echo.Context) error {
token, err := h.getJwtToken(c)
if err != nil {

View File

@ -8,8 +8,8 @@ import (
"strings"
"testing"
v1 "git.jamestombleson.com/jtom38/go-cook/api/handlers/v1"
"git.jamestombleson.com/jtom38/go-cook/api/domain"
v1 "git.jamestombleson.com/jtom38/go-cook/internal/handlers/v1"
"git.jamestombleson.com/jtom38/go-cook/internal/domain"
_ "github.com/glebarez/go-sqlite"
"github.com/labstack/echo/v4"

View File

@ -4,7 +4,7 @@ import (
"fmt"
"net/http"
"git.jamestombleson.com/jtom38/go-cook/api/domain"
"git.jamestombleson.com/jtom38/go-cook/internal/domain"
"github.com/labstack/echo/v4"
)

View File

@ -4,9 +4,9 @@ import (
"database/sql"
"net/http"
"git.jamestombleson.com/jtom38/go-cook/api/repositories"
"git.jamestombleson.com/jtom38/go-cook/api/services"
"git.jamestombleson.com/jtom38/go-cook/api/domain"
"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"
echojwt "github.com/labstack/echo-jwt/v4"
@ -19,6 +19,7 @@ type Handler struct {
UserService services.UserService
userRepo repositories.IUserTable
recipeRepo repositories.IRecipeTable
refreshTokenRepo repositories.RefreshTokenRepository
}
func NewHandler(conn *sql.DB, cfg domain.EnvConfig) *Handler {
@ -27,6 +28,7 @@ func NewHandler(conn *sql.DB, cfg domain.EnvConfig) *Handler {
UserService: services.NewUserService(conn),
userRepo: repositories.NewUserRepository(conn),
recipeRepo: repositories.NewRecipeRepository(conn),
refreshTokenRepo: repositories.NewRefreshTokenRepository(conn),
}
}

View File

@ -5,7 +5,7 @@ import (
"strings"
"time"
"git.jamestombleson.com/jtom38/go-cook/api/domain"
"git.jamestombleson.com/jtom38/go-cook/internal/domain"
"github.com/golang-jwt/jwt/v5"
)

View File

@ -0,0 +1,17 @@
-- +goose Up
-- +goose StatementBegin
SELECT 'up SQL query';
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
)
-- +goose StatementEnd
-- +goose Down
-- +goose StatementBegin
SELECT 'down SQL query';
DROP TABLE IF EXISTS RefreshTokens;
-- +goose StatementEnd

View File

@ -4,7 +4,7 @@ import (
"database/sql"
"errors"
"git.jamestombleson.com/jtom38/go-cook/api/domain"
"git.jamestombleson.com/jtom38/go-cook/internal/domain"
)
type IRecipeTable interface {

View File

@ -0,0 +1,111 @@
package repositories
import (
"database/sql"
"errors"
"fmt"
"time"
"git.jamestombleson.com/jtom38/go-cook/internal/domain"
"github.com/huandu/go-sqlbuilder"
)
const (
refreshTokenTableName = "RefreshTokens"
)
type RefreshTokenTable interface {
}
type RefreshTokenRepository struct {
connection *sql.DB
}
func NewRefreshTokenRepository(conn *sql.DB) RefreshTokenRepository {
return RefreshTokenRepository{
connection: conn,
}
}
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", "ExpiresAt", "CreatedAt", "LastUpdated")
builder.Values(username, token, expiresAt, dt, dt)
query, args := builder.Build()
_, err := rt.connection.Exec(query, args...)
if err != nil {
return 0, err
}
return 1, nil
}
func (rt RefreshTokenRepository) GetByUsername(name string) (domain.RefreshTokenEntity, error) {
builder := sqlbuilder.NewSelectBuilder()
builder.Select("*").From(refreshTokenTableName).Where(
builder.E("Username", name),
)
query, args := builder.Build()
rows, err := rt.connection.Query(query, args...)
if err != nil {
return domain.RefreshTokenEntity{}, err
}
data := rt.processRows(rows)
if len(data) == 0 {
return domain.RefreshTokenEntity{}, errors.New("no token found for user")
}
return data[0], nil
}
func (rt RefreshTokenRepository) DeleteById(id int) (int64, error) {
builder := sqlbuilder.NewDeleteBuilder()
builder.DeleteFrom(refreshTokenTableName)
builder.Where(
builder.EQ("Id", id),
)
query, args := builder.Build()
_, err := rt.connection.Exec(query, args...)
if err != nil {
return -1, err
}
return 1, nil
}
func (rd RefreshTokenRepository) processRows(rows *sql.Rows) []domain.RefreshTokenEntity {
items := []domain.RefreshTokenEntity{}
for rows.Next() {
var id int
var username string
var token string
var expiresAt time.Time
var createdAt time.Time
var lastUpdated time.Time
err := rows.Scan(&id, &username, &token, &expiresAt, &createdAt, &lastUpdated)
if err != nil {
fmt.Println(err)
}
items = append(items, domain.RefreshTokenEntity{
Id: id,
Username: username,
Token: token,
ExpiresAt: expiresAt,
CreatedAt: createdAt,
LastUpdated: lastUpdated,
})
}
return items
}
//func (rt RefreshTokenRepository) Delete()

View File

@ -0,0 +1,111 @@
package repositories_test
import (
"database/sql"
"testing"
"time"
"git.jamestombleson.com/jtom38/go-cook/internal/repositories"
_ "github.com/glebarez/go-sqlite"
"github.com/pressly/goose/v3"
)
func TestRefreshTokenCreate(t *testing.T) {
conn, err := setupInMemoryDb()
if err != nil {
t.Log(err)
t.FailNow()
}
client := repositories.NewRefreshTokenRepository(conn)
rows, err := client.Create("tester", "BadTokenDontUse", time.Now().Add(time.Hour+1))
if err != nil {
t.Log(err)
t.FailNow()
}
if rows == 0 {
t.Log("expected one row to come back but got 0")
}
}
func TestRefreshTokenGetByUsername(t *testing.T) {
conn, err := setupInMemoryDb()
if err != nil {
t.Log(err)
t.FailNow()
}
client := repositories.NewRefreshTokenRepository(conn)
rows, err := client.Create("tester", "BadTokenDoNotUse", time.Now().Add(time.Hour+1))
if err != nil {
t.Log(err)
t.FailNow()
}
if rows != 1 {
t.Log("expected a row to be added but not the wrong value back")
t.FailNow()
}
model, err := client.GetByUsername("tester")
if err != nil {
t.Log(err)
t.FailNow()
}
if model.Username != "tester" {
t.Log("got the wrong user back")
t.FailNow()
}
}
func TestRefreshTokenDeleteById(t *testing.T) {
conn, err := setupInMemoryDb()
if err != nil {
t.Log(err)
t.FailNow()
}
client := repositories.NewRefreshTokenRepository(conn)
_, err = client.Create("tester", "BadTokenDoNotUse", time.Now().Add(time.Hour+1))
if err != nil {
t.Log(err)
t.FailNow()
}
model, err := client.GetByUsername("tester")
if err != nil {
t.Log(err)
t.FailNow()
}
updated, err := client.DeleteById(model.Id)
if err != nil {
t.Log(err)
t.FailNow()
}
if updated != 1 {
t.Log("deleted the wrong number of records")
t.FailNow()
}
}
func setupInMemoryDb() (*sql.DB, error) {
db, err := sql.Open("sqlite", ":memory:")
if err != nil {
return nil, err
}
err = goose.SetDialect("sqlite3")
if err != nil {
return nil, err
}
err = goose.Up(db, "../migrations")
if err != nil {
return nil, err
}
return db, nil
}

View File

@ -6,7 +6,7 @@ import (
"fmt"
"time"
"git.jamestombleson.com/jtom38/go-cook/api/domain"
"git.jamestombleson.com/jtom38/go-cook/internal/domain"
"github.com/huandu/go-sqlbuilder"
"golang.org/x/crypto/bcrypt"

View File

@ -5,8 +5,8 @@ import (
"log"
"testing"
"git.jamestombleson.com/jtom38/go-cook/api/repositories"
"git.jamestombleson.com/jtom38/go-cook/api/domain"
"git.jamestombleson.com/jtom38/go-cook/internal/repositories"
"git.jamestombleson.com/jtom38/go-cook/internal/domain"
"github.com/DATA-DOG/go-sqlmock"
_ "github.com/glebarez/go-sqlite"

View File

@ -5,7 +5,7 @@ import (
"os"
"strconv"
"git.jamestombleson.com/jtom38/go-cook/api/domain"
"git.jamestombleson.com/jtom38/go-cook/internal/domain"
"github.com/joho/godotenv"
)

View File

@ -5,8 +5,8 @@ import (
"errors"
"strings"
"git.jamestombleson.com/jtom38/go-cook/api/domain"
"git.jamestombleson.com/jtom38/go-cook/api/repositories"
"git.jamestombleson.com/jtom38/go-cook/internal/domain"
"git.jamestombleson.com/jtom38/go-cook/internal/repositories"
"golang.org/x/crypto/bcrypt"
)

View File

@ -3,7 +3,7 @@ package services_test
import (
"testing"
"git.jamestombleson.com/jtom38/go-cook/api/services"
"git.jamestombleson.com/jtom38/go-cook/internal/services"
"github.com/DATA-DOG/go-sqlmock"
)