From a2e740eefda99a8443627957ddb5646cc7b45d05 Mon Sep 17 00:00:00 2001 From: James Tombleson Date: Sat, 20 Apr 2024 08:09:24 -0700 Subject: [PATCH 01/15] moved away from the api directory to internal per go doc recommendations --- {api => internal}/domain/dto.go | 0 {api => internal}/domain/entities.go | 9 ++ {api => internal}/domain/models.go | 0 {api => internal}/domain/requests.go | 6 + {api => internal}/domain/responses.go | 0 {api => internal}/domain/scopes.go | 0 {api => internal}/handlers/v1/auth.go | 14 ++- {api => internal}/handlers/v1/auth_test.go | 4 +- {api => internal}/handlers/v1/demo.go | 2 +- {api => internal}/handlers/v1/handler.go | 22 ++-- {api => internal}/handlers/v1/jwt.go | 2 +- {api => internal}/handlers/v1/users.go | 0 .../migrations/20240321194227_user_table.sql | 0 .../migrations/20240329211828_user_scopes.sql | 0 .../20240416180636_refreshtoken.sql | 17 +++ {api => internal}/repositories/recipe.go | 2 +- internal/repositories/refreshTokens.go | 111 ++++++++++++++++++ internal/repositories/refreshTokens_test.go | 111 ++++++++++++++++++ {api => internal}/repositories/users.go | 2 +- {api => internal}/repositories/users_test.go | 4 +- {api => internal}/services/env.go | 2 +- {api => internal}/services/userService.go | 4 +- .../services/userService_test.go | 2 +- 23 files changed, 290 insertions(+), 24 deletions(-) rename {api => internal}/domain/dto.go (100%) rename {api => internal}/domain/entities.go (67%) rename {api => internal}/domain/models.go (100%) rename {api => internal}/domain/requests.go (58%) rename {api => internal}/domain/responses.go (100%) rename {api => internal}/domain/scopes.go (100%) rename {api => internal}/handlers/v1/auth.go (92%) rename {api => internal}/handlers/v1/auth_test.go (94%) rename {api => internal}/handlers/v1/demo.go (95%) rename {api => internal}/handlers/v1/handler.go (72%) rename {api => internal}/handlers/v1/jwt.go (97%) rename {api => internal}/handlers/v1/users.go (100%) rename {api => internal}/migrations/20240321194227_user_table.sql (100%) rename {api => internal}/migrations/20240329211828_user_scopes.sql (100%) create mode 100644 internal/migrations/20240416180636_refreshtoken.sql rename {api => internal}/repositories/recipe.go (94%) create mode 100644 internal/repositories/refreshTokens.go create mode 100644 internal/repositories/refreshTokens_test.go rename {api => internal}/repositories/users.go (98%) rename {api => internal}/repositories/users_test.go (90%) rename {api => internal}/services/env.go (90%) rename {api => internal}/services/userService.go (96%) rename {api => internal}/services/userService_test.go (93%) diff --git a/api/domain/dto.go b/internal/domain/dto.go similarity index 100% rename from api/domain/dto.go rename to internal/domain/dto.go diff --git a/api/domain/entities.go b/internal/domain/entities.go similarity index 67% rename from api/domain/entities.go rename to internal/domain/entities.go index f26454a..1a310a6 100644 --- a/api/domain/entities.go +++ b/internal/domain/entities.go @@ -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 diff --git a/api/domain/models.go b/internal/domain/models.go similarity index 100% rename from api/domain/models.go rename to internal/domain/models.go diff --git a/api/domain/requests.go b/internal/domain/requests.go similarity index 58% rename from api/domain/requests.go rename to internal/domain/requests.go index 5be5b3b..6092867 100644 --- a/api/domain/requests.go +++ b/internal/domain/requests.go @@ -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"` +} diff --git a/api/domain/responses.go b/internal/domain/responses.go similarity index 100% rename from api/domain/responses.go rename to internal/domain/responses.go diff --git a/api/domain/scopes.go b/internal/domain/scopes.go similarity index 100% rename from api/domain/scopes.go rename to internal/domain/scopes.go diff --git a/api/handlers/v1/auth.go b/internal/handlers/v1/auth.go similarity index 92% rename from api/handlers/v1/auth.go rename to internal/handlers/v1/auth.go index f7404dc..5d41a71 100644 --- a/api/handlers/v1/auth.go +++ b/internal/handlers/v1/auth.go @@ -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 { diff --git a/api/handlers/v1/auth_test.go b/internal/handlers/v1/auth_test.go similarity index 94% rename from api/handlers/v1/auth_test.go rename to internal/handlers/v1/auth_test.go index 800127d..f564533 100644 --- a/api/handlers/v1/auth_test.go +++ b/internal/handlers/v1/auth_test.go @@ -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" diff --git a/api/handlers/v1/demo.go b/internal/handlers/v1/demo.go similarity index 95% rename from api/handlers/v1/demo.go rename to internal/handlers/v1/demo.go index 9c888d3..bb6899d 100644 --- a/api/handlers/v1/demo.go +++ b/internal/handlers/v1/demo.go @@ -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" ) diff --git a/api/handlers/v1/handler.go b/internal/handlers/v1/handler.go similarity index 72% rename from api/handlers/v1/handler.go rename to internal/handlers/v1/handler.go index 4428740..d40e92a 100644 --- a/api/handlers/v1/handler.go +++ b/internal/handlers/v1/handler.go @@ -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" @@ -16,17 +16,19 @@ import ( type Handler struct { Config domain.EnvConfig - UserService services.UserService - userRepo repositories.IUserTable - recipeRepo repositories.IRecipeTable + 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, - UserService: services.NewUserService(conn), - userRepo: repositories.NewUserRepository(conn), - recipeRepo: repositories.NewRecipeRepository(conn), + Config: cfg, + UserService: services.NewUserService(conn), + userRepo: repositories.NewUserRepository(conn), + recipeRepo: repositories.NewRecipeRepository(conn), + refreshTokenRepo: repositories.NewRefreshTokenRepository(conn), } } diff --git a/api/handlers/v1/jwt.go b/internal/handlers/v1/jwt.go similarity index 97% rename from api/handlers/v1/jwt.go rename to internal/handlers/v1/jwt.go index 0fc2f7d..0af8311 100644 --- a/api/handlers/v1/jwt.go +++ b/internal/handlers/v1/jwt.go @@ -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" ) diff --git a/api/handlers/v1/users.go b/internal/handlers/v1/users.go similarity index 100% rename from api/handlers/v1/users.go rename to internal/handlers/v1/users.go diff --git a/api/migrations/20240321194227_user_table.sql b/internal/migrations/20240321194227_user_table.sql similarity index 100% rename from api/migrations/20240321194227_user_table.sql rename to internal/migrations/20240321194227_user_table.sql diff --git a/api/migrations/20240329211828_user_scopes.sql b/internal/migrations/20240329211828_user_scopes.sql similarity index 100% rename from api/migrations/20240329211828_user_scopes.sql rename to internal/migrations/20240329211828_user_scopes.sql diff --git a/internal/migrations/20240416180636_refreshtoken.sql b/internal/migrations/20240416180636_refreshtoken.sql new file mode 100644 index 0000000..7457fb4 --- /dev/null +++ b/internal/migrations/20240416180636_refreshtoken.sql @@ -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 \ No newline at end of file diff --git a/api/repositories/recipe.go b/internal/repositories/recipe.go similarity index 94% rename from api/repositories/recipe.go rename to internal/repositories/recipe.go index b510c07..ab8c0b4 100644 --- a/api/repositories/recipe.go +++ b/internal/repositories/recipe.go @@ -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 { diff --git a/internal/repositories/refreshTokens.go b/internal/repositories/refreshTokens.go new file mode 100644 index 0000000..58e5a97 --- /dev/null +++ b/internal/repositories/refreshTokens.go @@ -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() diff --git a/internal/repositories/refreshTokens_test.go b/internal/repositories/refreshTokens_test.go new file mode 100644 index 0000000..1cd14f4 --- /dev/null +++ b/internal/repositories/refreshTokens_test.go @@ -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 +} diff --git a/api/repositories/users.go b/internal/repositories/users.go similarity index 98% rename from api/repositories/users.go rename to internal/repositories/users.go index 78cd5cf..3fc1e52 100644 --- a/api/repositories/users.go +++ b/internal/repositories/users.go @@ -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" diff --git a/api/repositories/users_test.go b/internal/repositories/users_test.go similarity index 90% rename from api/repositories/users_test.go rename to internal/repositories/users_test.go index 4d62136..32c7a30 100644 --- a/api/repositories/users_test.go +++ b/internal/repositories/users_test.go @@ -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" diff --git a/api/services/env.go b/internal/services/env.go similarity index 90% rename from api/services/env.go rename to internal/services/env.go index 3ea6d90..3c1f73d 100644 --- a/api/services/env.go +++ b/internal/services/env.go @@ -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" ) diff --git a/api/services/userService.go b/internal/services/userService.go similarity index 96% rename from api/services/userService.go rename to internal/services/userService.go index 48f0948..decb30f 100644 --- a/api/services/userService.go +++ b/internal/services/userService.go @@ -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" ) diff --git a/api/services/userService_test.go b/internal/services/userService_test.go similarity index 93% rename from api/services/userService_test.go rename to internal/services/userService_test.go index 5be3811..38106c9 100644 --- a/api/services/userService_test.go +++ b/internal/services/userService_test.go @@ -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" ) -- 2.40.1 From 439c8ed733c60d787322f4726b7857322d883162 Mon Sep 17 00:00:00 2001 From: James Tombleson Date: Sat, 20 Apr 2024 16:45:57 -0700 Subject: [PATCH 02/15] moved main.go to cmd folder per best practice --- .vscode/launch.json | 2 +- .vscode/settings.json | 1 + main.go => cmd/main.go | 0 3 files changed, 2 insertions(+), 1 deletion(-) rename main.go => cmd/main.go (100%) diff --git a/.vscode/launch.json b/.vscode/launch.json index 6deb9a6..7a5ffe9 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -9,7 +9,7 @@ "type": "go", "request": "launch", "mode": "auto", - "program": "main.go" + "program": "cmd/main.go" } ] } \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index a160986..6524a35 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -3,6 +3,7 @@ "echojwt", "glebarez", "gocook", + "godotenv", "huandu", "labstack", "sqlbuilder" diff --git a/main.go b/cmd/main.go similarity index 100% rename from main.go rename to cmd/main.go -- 2.40.1 From f5eea08c1e9e4c3e855e35e39503854db2a610f6 Mon Sep 17 00:00:00 2001 From: James Tombleson Date: Sun, 21 Apr 2024 08:57:10 -0700 Subject: [PATCH 03/15] updated how refresh token service and repo work --- internal/repositories/refreshTokens.go | 11 ++-- internal/services/refreshTokenService.go | 72 ++++++++++++++++++++++++ 2 files changed, 79 insertions(+), 4 deletions(-) create mode 100644 internal/services/refreshTokenService.go diff --git a/internal/repositories/refreshTokens.go b/internal/repositories/refreshTokens.go index 58e5a97..16e3648 100644 --- a/internal/repositories/refreshTokens.go +++ b/internal/repositories/refreshTokens.go @@ -15,6 +15,9 @@ const ( ) type RefreshTokenTable interface { + Create(username string, token string, expiresAt time.Time) (int64, error) + GetByUsername(name string) (domain.RefreshTokenEntity, error) + DeleteById(id int64) (int64, error) } type RefreshTokenRepository struct { @@ -63,7 +66,7 @@ func (rt RefreshTokenRepository) GetByUsername(name string) (domain.RefreshToken return data[0], nil } -func (rt RefreshTokenRepository) DeleteById(id int) (int64, error) { +func (rt RefreshTokenRepository) DeleteById(id int64) (int64, error) { builder := sqlbuilder.NewDeleteBuilder() builder.DeleteFrom(refreshTokenTableName) builder.Where( @@ -71,19 +74,19 @@ func (rt RefreshTokenRepository) DeleteById(id int) (int64, error) { ) query, args := builder.Build() - _, err := rt.connection.Exec(query, args...) + rows, err := rt.connection.Exec(query, args...) if err != nil { return -1, err } - return 1, nil + return rows.RowsAffected() } func (rd RefreshTokenRepository) processRows(rows *sql.Rows) []domain.RefreshTokenEntity { items := []domain.RefreshTokenEntity{} for rows.Next() { - var id int + var id int64 var username string var token string var expiresAt time.Time diff --git a/internal/services/refreshTokenService.go b/internal/services/refreshTokenService.go new file mode 100644 index 0000000..a23c06d --- /dev/null +++ b/internal/services/refreshTokenService.go @@ -0,0 +1,72 @@ +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" +) + +type RefreshToken interface { + Create(username string, expiresAt time.Time) (string, error) + GetByName(name string) (domain.RefreshTokenEntity, error) + Delete(id int64) (int64, error) + IsRequestValid(username, refreshToken string, jwtExpiresAt time.Time) error +} + +type RefreshTokenService struct { + table repositories.RefreshTokenTable +} + +func NewRefreshTokenService(conn *sql.DB) RefreshTokenService { + return RefreshTokenService{ + table: repositories.NewRefreshTokenRepository(conn), + } +} + +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(), expiresAt) + if err != nil { + return "", err + } + + if rows != 1 { + return "", errors.New("expected one row but got none") + } + return token.String(), nil +} + +// Find the saved refresh token for a user and return it if it exists +func (rt RefreshTokenService) GetByName(name string) (domain.RefreshTokenEntity, error) { + return rt.table.GetByUsername(name) +} + +// This will request that a object is removed from the database +func (rt RefreshTokenService) Delete(id int64) (int64, error) { + return rt.table.DeleteById(id) +} + +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) { + 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 +} \ No newline at end of file -- 2.40.1 From 8d8781eac4720ca92a32c0e7b1f92d35224aa4e8 Mon Sep 17 00:00:00 2001 From: James Tombleson Date: Sun, 21 Apr 2024 08:57:32 -0700 Subject: [PATCH 04/15] added a interface for userService --- internal/services/userService.go | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/internal/services/userService.go b/internal/services/userService.go index decb30f..ec626e5 100644 --- a/internal/services/userService.go +++ b/internal/services/userService.go @@ -17,6 +17,16 @@ const ( ErrInvalidPassword = "invalid password" ) +type Users interface { + DoesUserExist(username string) error + DoesPasswordMatchHash(username, password string) error + GetUser(username string) (domain.UserEntity, error) + AddScopes(username string, scopes []string) error + RemoveScopes(username string, scopes []string) error + Create(name, password, scope string) (domain.UserEntity, error) + CheckPasswordForRequirements(password string) error +} + // This will handle operations that are user related, but one layer higher then the repository type UserService struct { repo repositories.IUserTable @@ -111,7 +121,7 @@ func (us UserService) doesScopeExist(scopes []string, target string) bool { return false } -func (us UserService) CreateNewUser(name, password, scope string) (domain.UserEntity, error) { +func (us UserService) Create(name, password, scope string) (domain.UserEntity, error) { err := us.CheckPasswordForRequirements(password) if err != nil { return domain.UserEntity{}, err -- 2.40.1 From 029710ad31db0ef01f9b8d618c0879e3c7ecbcb8 Mon Sep 17 00:00:00 2001 From: James Tombleson Date: Sun, 21 Apr 2024 08:58:06 -0700 Subject: [PATCH 05/15] updated pathing in main and updated how to find migrations --- cmd/main.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cmd/main.go b/cmd/main.go index 63fb65d..2c024a4 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -5,8 +5,8 @@ import ( "log" "net/http" - "git.jamestombleson.com/jtom38/go-cook/api/services" - v1 "git.jamestombleson.com/jtom38/go-cook/api/handlers/v1" + v1 "git.jamestombleson.com/jtom38/go-cook/internal/handlers/v1" + "git.jamestombleson.com/jtom38/go-cook/internal/services" _ "github.com/glebarez/go-sqlite" "github.com/go-playground/validator" @@ -34,7 +34,7 @@ func main() { panic(err) } - err = goose.Up(db, "api/migrations") + err = goose.Up(db, "../internal/migrations") if err != nil { panic(err) } -- 2.40.1 From 5b2ff607b9ccb9e7ad8f9ac297b1defa56024cba Mon Sep 17 00:00:00 2001 From: James Tombleson Date: Sun, 21 Apr 2024 08:58:32 -0700 Subject: [PATCH 06/15] Moved entity id's to int64 that the sql driver returns --- internal/domain/entities.go | 8 ++++---- internal/repositories/users.go | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/internal/domain/entities.go b/internal/domain/entities.go index 1a310a6..70091e7 100644 --- a/internal/domain/entities.go +++ b/internal/domain/entities.go @@ -3,7 +3,7 @@ package domain import "time" type UserEntity struct { - Id int + Id int64 CreatedAt time.Time LastUpdated time.Time Name string @@ -12,8 +12,8 @@ type UserEntity struct { } type RefreshTokenEntity struct { - Id int - Username string + Id int64 + Username string Token string ExpiresAt time.Time CreatedAt time.Time @@ -21,7 +21,7 @@ type RefreshTokenEntity struct { } type RecipeEntity struct { - Id int32 + Id int64 CreatedAt time.Time LastUpdated time.Time Title string diff --git a/internal/repositories/users.go b/internal/repositories/users.go index 3fc1e52..39bc13b 100644 --- a/internal/repositories/users.go +++ b/internal/repositories/users.go @@ -133,7 +133,7 @@ func (ur UserRepository) processRows(rows *sql.Rows) []domain.UserEntity { items := []domain.UserEntity{} for rows.Next() { - var id int + var id int64 var name string var hash string var createdAt time.Time -- 2.40.1 From e218da5e7d8bd9ef991a39dfa55b2302a121b89e Mon Sep 17 00:00:00 2001 From: James Tombleson Date: Sun, 21 Apr 2024 08:59:07 -0700 Subject: [PATCH 07/15] Updated some responses to return the base struct with embedding --- internal/domain/responses.go | 21 ++++++++++++++++----- internal/handlers/v1/demo.go | 23 +++++++++++++---------- 2 files changed, 29 insertions(+), 15 deletions(-) diff --git a/internal/domain/responses.go b/internal/domain/responses.go index dce4b9a..c48c917 100644 --- a/internal/domain/responses.go +++ b/internal/domain/responses.go @@ -1,19 +1,30 @@ package domain type LoginResponse struct { - Success bool `json:"success"` - Token string `json:"token"` - Type string `json:"type"` + ErrorResponse + 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 { - Success bool `json:"success"` + BaseResponse Error string `json:"error"` - Message string `json:"message"` } diff --git a/internal/handlers/v1/demo.go b/internal/handlers/v1/demo.go index bb6899d..82997bd 100644 --- a/internal/handlers/v1/demo.go +++ b/internal/handlers/v1/demo.go @@ -11,16 +11,20 @@ import ( func (h *Handler) DemoHello(c echo.Context) error { return c.JSON(http.StatusOK, domain.HelloWhoResponse{ - Success: true, - Message: "Hello world!", + 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{ - Success: true, - Message: fmt.Sprintf("Hello, %s", name), + BaseResponse: domain.BaseResponse{ + Success: true, + Message: fmt.Sprintf("Hello, %s", name), + }, }) } @@ -28,15 +32,14 @@ func (h *Handler) HelloBody(c echo.Context) error { request := domain.HelloBodyRequest{} err := (&echo.DefaultBinder{}).BindBody(c, &request) if err != nil { - return c.JSON(http.StatusBadRequest, domain.HelloWhoResponse{ - Success: false, - Error: err.Error(), - }) + return h.InternalServerErrorResponse(c, err.Error()) } return c.JSON(http.StatusOK, domain.HelloWhoResponse{ - Success: true, - Message: fmt.Sprintf("Hello, %s", request.Name), + BaseResponse: domain.BaseResponse{ + Success: true, + Message: fmt.Sprintf("Hello, %s", request.Name), + }, }) } -- 2.40.1 From 0383d554b86a683fe51898ae390ffece89177ffe Mon Sep 17 00:00:00 2001 From: James Tombleson Date: Sun, 21 Apr 2024 08:59:25 -0700 Subject: [PATCH 08/15] added RefreshTokenRequest as a body --- internal/domain/requests.go | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/internal/domain/requests.go b/internal/domain/requests.go index 6092867..cbca893 100644 --- a/internal/domain/requests.go +++ b/internal/domain/requests.go @@ -1,5 +1,7 @@ package domain +import "time" + type HelloBodyRequest struct { Name string `json:"name" validate:"required"` } @@ -10,7 +12,7 @@ type UpdateScopesRequest struct { } type RefreshTokenRequest struct { - Username string `json:"username"` - RefreshToken string `json:"refreshToken"` - ExpiresAt string `json:"expiresAt"` + Username string `json:"username"` + RefreshToken string `json:"refreshToken"` + ExpiresAt time.Time `json:"expiresAt"` } -- 2.40.1 From b02ed3f86a4e2a809ca788c79e3f462a80a95364 Mon Sep 17 00:00:00 2001 From: James Tombleson Date: Sun, 21 Apr 2024 08:59:51 -0700 Subject: [PATCH 09/15] Moved handler to use interfaces and added the refreshToken endpoint --- internal/handlers/v1/handler.go | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/internal/handlers/v1/handler.go b/internal/handlers/v1/handler.go index d40e92a..567b7ce 100644 --- a/internal/handlers/v1/handler.go +++ b/internal/handlers/v1/handler.go @@ -5,7 +5,6 @@ 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" @@ -16,19 +15,18 @@ import ( type Handler struct { Config domain.EnvConfig - UserService services.UserService - userRepo repositories.IUserTable - recipeRepo repositories.IRecipeTable - refreshTokenRepo repositories.RefreshTokenRepository + users services.UserService + recipes services.Recipes + refreshTokens services.RefreshToken } 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), - refreshTokenRepo: repositories.NewRefreshTokenRepository(conn), + Config: cfg, + + users: services.NewUserService(conn), + recipes: services.NewRecipesService(conn), + refreshTokens: services.NewRefreshTokenService(conn), } } @@ -46,6 +44,7 @@ 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) -- 2.40.1 From e7f706b6e723824f843cde014a70f25e1c3830ca Mon Sep 17 00:00:00 2001 From: James Tombleson Date: Sun, 21 Apr 2024 10:27:56 -0700 Subject: [PATCH 10/15] The refreshtoken no longer cares about when a token expired. When they want a new jtw from refresh a new refresh token is made also --- internal/domain/entities.go | 1 - .../20240416180636_refreshtoken.sql | 1 - internal/repositories/refreshTokens.go | 12 +++--- internal/repositories/refreshTokens_test.go | 7 ++-- internal/services/refreshTokenService.go | 40 +++++++++++++------ 5 files changed, 35 insertions(+), 26 deletions(-) diff --git a/internal/domain/entities.go b/internal/domain/entities.go index 70091e7..edd2557 100644 --- a/internal/domain/entities.go +++ b/internal/domain/entities.go @@ -15,7 +15,6 @@ type RefreshTokenEntity struct { Id int64 Username string Token string - ExpiresAt time.Time CreatedAt time.Time LastUpdated time.Time } diff --git a/internal/migrations/20240416180636_refreshtoken.sql b/internal/migrations/20240416180636_refreshtoken.sql index 7457fb4..ce13bff 100644 --- a/internal/migrations/20240416180636_refreshtoken.sql +++ b/internal/migrations/20240416180636_refreshtoken.sql @@ -5,7 +5,6 @@ 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 ) diff --git a/internal/repositories/refreshTokens.go b/internal/repositories/refreshTokens.go index 16e3648..9e8889b 100644 --- a/internal/repositories/refreshTokens.go +++ b/internal/repositories/refreshTokens.go @@ -15,7 +15,7 @@ const ( ) type RefreshTokenTable interface { - Create(username string, token string, expiresAt time.Time) (int64, error) + Create(username string, token string) (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, expiresAt time.Time) (int64, error) { +func (rt RefreshTokenRepository) Create(username string, token string) (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) + builder.Cols("Username", "Token", "CreatedAt", "LastUpdated") + builder.Values(username, token, dt, dt) query, args := builder.Build() _, err := rt.connection.Exec(query, args...) @@ -89,11 +89,10 @@ 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, &expiresAt, &createdAt, &lastUpdated) + err := rows.Scan(&id, &username, &token, &createdAt, &lastUpdated) if err != nil { fmt.Println(err) } @@ -102,7 +101,6 @@ func (rd RefreshTokenRepository) processRows(rows *sql.Rows) []domain.RefreshTok Id: id, Username: username, Token: token, - ExpiresAt: expiresAt, CreatedAt: createdAt, LastUpdated: lastUpdated, }) diff --git a/internal/repositories/refreshTokens_test.go b/internal/repositories/refreshTokens_test.go index 1cd14f4..bd3a98a 100644 --- a/internal/repositories/refreshTokens_test.go +++ b/internal/repositories/refreshTokens_test.go @@ -3,7 +3,6 @@ package repositories_test import ( "database/sql" "testing" - "time" "git.jamestombleson.com/jtom38/go-cook/internal/repositories" _ "github.com/glebarez/go-sqlite" @@ -18,7 +17,7 @@ func TestRefreshTokenCreate(t *testing.T) { } client := repositories.NewRefreshTokenRepository(conn) - rows, err := client.Create("tester", "BadTokenDontUse", time.Now().Add(time.Hour+1)) + rows, err := client.Create("tester", "BadTokenDontUse") if err != nil { t.Log(err) t.FailNow() @@ -37,7 +36,7 @@ func TestRefreshTokenGetByUsername(t *testing.T) { } client := repositories.NewRefreshTokenRepository(conn) - rows, err := client.Create("tester", "BadTokenDoNotUse", time.Now().Add(time.Hour+1)) + rows, err := client.Create("tester", "BadTokenDoNotUse") if err != nil { t.Log(err) t.FailNow() @@ -68,7 +67,7 @@ func TestRefreshTokenDeleteById(t *testing.T) { } client := repositories.NewRefreshTokenRepository(conn) - _, err = client.Create("tester", "BadTokenDoNotUse", time.Now().Add(time.Hour+1)) + _, err = client.Create("tester", "BadTokenDoNotUse") if err != nil { t.Log(err) t.FailNow() diff --git a/internal/services/refreshTokenService.go b/internal/services/refreshTokenService.go index a23c06d..aa1f457 100644 --- a/internal/services/refreshTokenService.go +++ b/internal/services/refreshTokenService.go @@ -3,20 +3,25 @@ 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, expiresAt time.Time) (string, error) + Create(username string) (string, error) GetByName(name string) (domain.RefreshTokenEntity, error) Delete(id int64) (int64, error) - IsRequestValid(username, refreshToken string, jwtExpiresAt time.Time) error + IsRequestValid(username, refreshToken string) 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 } @@ -27,13 +32,26 @@ func NewRefreshTokenService(conn *sql.DB) RefreshTokenService { } } -func (rt RefreshTokenService) Create(username string, expiresAt time.Time) (string, error) { +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) + } + } + token, err := uuid.NewV7() if err != nil { return "", err } - rows, err := rt.table.Create(username, token.String(), expiresAt) + rows, err := rt.table.Create(username, token.String()) if err != nil { return "", err } @@ -54,19 +72,15 @@ func (rt RefreshTokenService) Delete(id int64) (int64, error) { return rt.table.DeleteById(id) } -func (rt RefreshTokenService) IsRequestValid(username, refreshToken string, jwtExpiresAt time.Time) error { +func (rt RefreshTokenService) IsRequestValid(username, refreshToken string) 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 -} \ No newline at end of file +} -- 2.40.1 From 8482afed8de2bf718d40533c83593e061e9e302f Mon Sep 17 00:00:00 2001 From: James Tombleson Date: Sun, 21 Apr 2024 10:28:21 -0700 Subject: [PATCH 11/15] small improvement to creating a jwt logic --- internal/handlers/v1/jwt.go | 36 ++++++++++++------------------------ 1 file changed, 12 insertions(+), 24 deletions(-) diff --git a/internal/handlers/v1/jwt.go b/internal/handlers/v1/jwt.go index 0af8311..d1cd668 100644 --- a/internal/handlers/v1/jwt.go +++ b/internal/handlers/v1/jwt.go @@ -57,40 +57,28 @@ 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"] = time.Now().Add(10 * time.Minute) + claims["exp"] = expiresAt claims["authorized"] = true claims["username"] = username claims["iss"] = issuer 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 + 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 { -- 2.40.1 From be337b4bb94600cb532ee3a49946c10b657358f7 Mon Sep 17 00:00:00 2001 From: James Tombleson Date: Sun, 21 Apr 2024 10:28:50 -0700 Subject: [PATCH 12/15] auth handler now can work with refresh token request --- internal/handlers/v1/auth.go | 72 ++++++++++++++++++++++++++---------- 1 file changed, 52 insertions(+), 20 deletions(-) diff --git a/internal/handlers/v1/auth.go b/internal/handlers/v1/auth.go index 5d41a71..4b98d3e 100644 --- a/internal/handlers/v1/auth.go +++ b/internal/handlers/v1/auth.go @@ -3,6 +3,7 @@ package v1 import ( "errors" "net/http" + "time" "git.jamestombleson.com/jtom38/go-cook/internal/domain" "git.jamestombleson.com/jtom38/go-cook/internal/repositories" @@ -25,13 +26,12 @@ func (h *Handler) AuthRegister(c echo.Context) error { password := c.FormValue("password") //username := c.QueryParam("username") - exists, err := h.userRepo.GetByName(username) + exists, err := h.users.GetUser(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.UserService.CheckPasswordForRequirements(password) + err = h.users.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.userRepo.Create(username, password, domain.ScopeRecipeRead) + _, err = h.users.Create(username, password, domain.ScopeRecipeRead) if err != nil { return c.JSON(http.StatusInternalServerError, domain.ErrorResponse{ Success: false, @@ -73,27 +73,38 @@ func (h *Handler) AuthLogin(c echo.Context) error { } // check if the user exists - err := h.UserService.DoesUserExist(username) + err := h.users.DoesUserExist(username) if err != nil { return h.InternalServerErrorResponse(c, err.Error()) } // make sure the hash matches - err = h.UserService.DoesPasswordMatchHash(username, password) + err = h.users.DoesPasswordMatchHash(username, password) if err != nil { return h.InternalServerErrorResponse(c, err.Error()) } - token, err := h.generateJwt(username, h.Config.ApiUri) + // 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) if err != nil { return h.InternalServerErrorResponse(c, err.Error()) } return c.JSON(http.StatusOK, domain.LoginResponse{ - Success: true, - Token: token, + BaseResponse: domain.BaseResponse{ + Success: true, + Message: "OK", + }, + Token: jwt, Type: "Bearer", - RefreshToken: "", + RefreshToken: refresh, }) } @@ -108,7 +119,7 @@ func (h *Handler) validateAdminToken(c echo.Context, password string) error { return h.ReturnUnauthorizedResponse(c, ErrUserNotFound) } - token, err := h.generateAdminJwt("admin") + token, err := h.generateJwt("admin", h.Config.ApiUri) if err != nil { return h.InternalServerErrorResponse(c, err.Error()) } @@ -116,14 +127,39 @@ func (h *Handler) validateAdminToken(c echo.Context, password string) error { return c.JSON(http.StatusOK, token) } -func (h *Handler) GenerateRefreshToken(c echo.Context) error { +// 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 { // Check the context for the refresh token var request domain.RefreshTokenRequest err := (&echo.DefaultBinder{}).BindBody(c, &request) if err != nil { - return err + return h.InternalServerErrorResponse(c, err.Error()) } - h.refreshTokenRepo.Create() + + 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, + }) } func (h *Handler) AddScopes(c echo.Context) error { @@ -146,7 +182,7 @@ func (h *Handler) AddScopes(c echo.Context) error { }) } - err = h.UserService.AddScopes(request.Username, request.Scopes) + err = h.users.AddScopes(request.Username, request.Scopes) if err != nil { return h.InternalServerErrorResponse(c, err.Error()) } @@ -176,7 +212,7 @@ func (h *Handler) RemoveScopes(c echo.Context) error { }) } - err = h.UserService.RemoveScopes(request.Username, request.Scopes) + err = h.users.RemoveScopes(request.Username, request.Scopes) if err != nil { return h.InternalServerErrorResponse(c, err.Error()) } @@ -186,10 +222,6 @@ 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) -- 2.40.1 From 9627c51e7a0f53542937ec90938380c33d943cdf Mon Sep 17 00:00:00 2001 From: James Tombleson Date: Sun, 21 Apr 2024 10:29:17 -0700 Subject: [PATCH 13/15] minor changes to requests and responses --- internal/domain/requests.go | 8 +++----- internal/domain/responses.go | 2 +- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/internal/domain/requests.go b/internal/domain/requests.go index cbca893..6d43ce6 100644 --- a/internal/domain/requests.go +++ b/internal/domain/requests.go @@ -1,7 +1,5 @@ package domain -import "time" - type HelloBodyRequest struct { Name string `json:"name" validate:"required"` } @@ -12,7 +10,7 @@ type UpdateScopesRequest struct { } type RefreshTokenRequest struct { - Username string `json:"username"` - RefreshToken string `json:"refreshToken"` - ExpiresAt time.Time `json:"expiresAt"` + Username string `json:"username"` + RefreshToken string `json:"refreshToken"` + //ExpiresAt time.Time `json:"expiresAt"` } diff --git a/internal/domain/responses.go b/internal/domain/responses.go index c48c917..852a776 100644 --- a/internal/domain/responses.go +++ b/internal/domain/responses.go @@ -1,7 +1,7 @@ package domain type LoginResponse struct { - ErrorResponse + BaseResponse Token string `json:"token"` Type string `json:"type"` RefreshToken string `json:"refreshToken"` -- 2.40.1 From 26820d80f7c32e528d818eab4c4a7ca7710c7e8f Mon Sep 17 00:00:00 2001 From: James Tombleson Date: Sun, 21 Apr 2024 10:29:24 -0700 Subject: [PATCH 14/15] uuid was added --- go.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 950e205..660df2d 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,7 @@ 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 @@ -22,7 +23,6 @@ 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 -- 2.40.1 From 7d6e7d46cdc5fb10e5cbdc0feca2a176c7f3fbd8 Mon Sep 17 00:00:00 2001 From: James Tombleson Date: Sun, 21 Apr 2024 10:29:46 -0700 Subject: [PATCH 15/15] small helper file updates --- Justfile | 6 ++++-- rest.http | 9 +++++++++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/Justfile b/Justfile index 264ab7f..ebe4c2f 100644 --- a/Justfile +++ b/Justfile @@ -1,5 +1,7 @@ +# Brings the database up to the current migration migrate-up: - GOOSE_DRIVER=sqlite3 GOOSE_DBSTRING=./gocook.db goose -dir ./api/migrations up + GOOSE_DRIVER=sqlite3 GOOSE_DBSTRING=./cmd/gocook.db goose -dir ./internal/migrations up +# Rolls back one migration at a time migrate-down: - GOOSE_DRIVER=sqlite3 GOOSE_DBSTRING=./gocook.db goose -dir ./api/migrations down \ No newline at end of file + GOOSE_DRIVER=sqlite3 GOOSE_DBSTRING=./cmd/gocook.db goose -dir ./internal/migrations down \ No newline at end of file diff --git a/rest.http b/rest.http index bdc77a3..26416c5 100644 --- a/rest.http +++ b/rest.http @@ -16,6 +16,15 @@ 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 -- 2.40.1