Skip to content

Commit

Permalink
Merge pull request #88 from fabiante/feat/user-keys
Browse files Browse the repository at this point in the history
  • Loading branch information
fabiante authored Sep 24, 2023
2 parents a3a3e39 + 0fa517c commit 6da1d7f
Show file tree
Hide file tree
Showing 11 changed files with 175 additions and 18 deletions.
54 changes: 54 additions & 0 deletions api/mw_auth.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package api

import (
"errors"
"fmt"
"net/http"

"github.com/fabiante/persurl/app"
"github.com/fabiante/persurl/app/models"
"github.com/gin-gonic/gin"
)

const (
headerToken = "Persurl-Key"
)

func authenticatedMiddleware(service app.UserServiceInterface) gin.HandlerFunc {
return func(ctx *gin.Context) {
token := ctx.GetHeader(headerToken)
if token == "" {
respondWithError(ctx, http.StatusUnauthorized, errors.New("unauthorized"))
return
}

// load user by email
user, err := service.GetUserByKey(token)
switch {
case errors.Is(err, app.ErrNotFound):
respondWithError(ctx, http.StatusUnauthorized, errors.New("user not found"))
return
case err != nil:
respondWithError(ctx, http.StatusInternalServerError, err)
return
}

ctx.Set("user", user)

ctx.Next()
}
}

func getAuthenticatedUser(ctx *gin.Context) *models.User {
value, exists := ctx.Get("user")
if !exists {
panic(fmt.Errorf("missing context value for key 'user'. ensure you use the appropriate middleware on this endpoint"))
}

typed, ok := value.(*models.User)
if !ok {
panic(fmt.Errorf("context value has unexpected type %T", value))
}

return typed
}
10 changes: 10 additions & 0 deletions app/models/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,14 @@ type User struct {
gorm.Model

Email string

Keys []*UserKey `gorm:"foreignKey:OwnerID"`
}

type UserKey struct {
gorm.Model

OwnerID uint

Value string
}
6 changes: 5 additions & 1 deletion app/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ type AdminServiceInterface interface {
}

type UserServiceInterface interface {
CreateUser(email string) error
CreateUser(email string) (*models.User, error)
GetUser(email string) (*models.User, error)
GetUserByKey(key string) (*models.User, error)

CreateUserKey(user *models.User) (*models.UserKey, error)
GetUserKey(value string) (*models.UserKey, error)
}
57 changes: 54 additions & 3 deletions app/service_user.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
package app

import (
"errors"

"github.com/fabiante/persurl/app/models"
"github.com/google/uuid"
"gorm.io/gorm"
)

Expand All @@ -13,17 +16,17 @@ func NewUserService(db *gorm.DB) *UserService {
return &UserService{db: db}
}

func (s *UserService) CreateUser(email string) error {
func (s *UserService) CreateUser(email string) (*models.User, error) {
user := &models.User{
Email: email,
}

err := s.db.Create(user).Error
switch {
case err != nil:
return mapDBError(err)
return nil, mapDBError(err)
default:
return nil
return user, nil
}
}

Expand All @@ -32,9 +35,57 @@ func (s *UserService) GetUser(email string) (*models.User, error) {

err := s.db.Take(user, "email = ?", email).Error
switch {
case errors.Is(err, gorm.ErrRecordNotFound):
return nil, ErrNotFound
case err != nil:
return nil, mapDBError(err)
default:
return user, nil
}
}

func (s *UserService) GetUserByKey(key string) (*models.User, error) {
user := &models.User{}

err := s.db.Model(user).
Joins("join user_keys on user_keys.owner_id = users.id").
Take(user).Error

switch {
case errors.Is(err, gorm.ErrRecordNotFound):
return nil, ErrNotFound
case err != nil:
return nil, mapDBError(err)
default:
return user, nil
}
}

func (s *UserService) CreateUserKey(user *models.User) (*models.UserKey, error) {
key := &models.UserKey{
OwnerID: user.ID,
Value: uuid.New().String(),
}

err := s.db.Create(key).Error
switch {
case err != nil:
return nil, mapDBError(err)
default:
return key, nil
}
}

func (s *UserService) GetUserKey(value string) (*models.UserKey, error) {
key := &models.UserKey{}

err := s.db.Take(key, "value = ?", value).Error
switch {
case errors.Is(err, gorm.ErrRecordNotFound):
return nil, ErrNotFound
case err != nil:
return nil, mapDBError(err)
default:
return key, nil
}
}
29 changes: 19 additions & 10 deletions db/migrations/migrate.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,7 @@ var migrationsPostgres = []any{
deleted_at timestamp,
name varchar(128) not null
constraint domains_pk
primary key
)`,
),
primary key)`),
newMigration("2023-09-18-00000020-CreateTablePurls", `create table purls
(
id serial
Expand All @@ -68,9 +65,7 @@ var migrationsPostgres = []any{
name varchar(128) not null,
target varchar(4096) not null,
constraint purls_pk2
unique (domain_id, name)
)`,
),
unique (domain_id, name))`),
newMigration("2023-09-22-00000030-CreateTableUsers", `create table users
(
id serial
Expand All @@ -81,7 +76,21 @@ var migrationsPostgres = []any{
deleted_at timestamp,
email varchar(256) not null,
constraint purls_email
unique (email)
)`,
),
unique (email))`),
newMigration("2023-09-24-00000040-AddUserKeys", `create table user_keys
(
id serial
constraint user_keys_pk
primary key,
created_at timestamp not null,
updated_at timestamp,
deleted_at timestamp,
owner_id int not null,
value varchar(64) not null,
constraint user_keys_value
unique (value))`),
newMigration("2023-09-24-00000050-AddUserKeysFK", `alter table user_keys
add constraint user_keys_owner_fk
foreign key (owner_id) references users
on delete restrict`),
}
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ require (
github.com/DEXPRO-Solutions-GmbH/swaggerui v1.1.1
github.com/doug-martin/goqu/v9 v9.18.0
github.com/gin-gonic/gin v1.9.1
github.com/google/uuid v1.3.1
github.com/lib/pq v1.10.9
github.com/lopezator/migrator v0.3.1
github.com/spf13/cobra v1.7.0
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,8 @@ github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLe
github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4=
github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g=
Expand Down
9 changes: 7 additions & 2 deletions tests/driver/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,9 @@ import (
)

type HTTPDriver struct {
BasePath string
Client *http.Client
BasePath string
Client *http.Client
UserToken string
}

func NewHTTPDriver(basePath string, transport http.RoundTripper) *HTTPDriver {
Expand All @@ -36,6 +37,10 @@ func (driver *HTTPDriver) newRequest(method, url string, body io.Reader) (*http.
return nil, err
}

if driver.UserToken != "" {
req.Header.Set("Persurl-Key", driver.UserToken)
}

return req, nil
}

Expand Down
11 changes: 11 additions & 0 deletions tests/dsl/given.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package dsl
import (
"testing"

"github.com/fabiante/persurl/app"
"github.com/fabiante/persurl/app/models"
"github.com/stretchr/testify/require"
)

Expand All @@ -22,3 +24,12 @@ func GivenExistingDomain(t *testing.T, service AdminAPI, domain string) {
err := service.CreateDomain(domain)
require.NoError(t, err, "creating domain failed")
}

// GivenSomeUser creates a user and returns the key for it.
func GivenSomeUser(t *testing.T, userService *app.UserService) *models.UserKey {
user, err := userService.CreateUser("[email protected]")
require.NoError(t, err)

key, err := userService.CreateUserKey(user)
return key
}
7 changes: 6 additions & 1 deletion tests/http_load_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"github.com/fabiante/persurl/config"
"github.com/fabiante/persurl/db"
"github.com/fabiante/persurl/tests/driver"
"github.com/fabiante/persurl/tests/dsl"
"github.com/fabiante/persurl/tests/specs"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/require"
Expand All @@ -28,17 +29,21 @@ func TestLoadWithHTTPDriver(t *testing.T) {
database, err := db.SetupAndMigratePostgresDB(config.DbDSN(), config.DbMaxConnections())
require.NoError(t, err, "setting up db failed")

err = db.EmptyTables(database.Goqu, "purls", "domains")
err = db.EmptyTables(database.Goqu, "purls", "domains", "user_keys", "users")
require.NoError(t, err, "truncating tables failed")

service := app.NewService(database.Gorm)
userService := app.NewUserService(database.Gorm)

key := dsl.GivenSomeUser(t, userService)

server := api.NewServer(service, service, userService)
api.SetupRouting(handler, server)

testServer := httptest.NewServer(handler)

dr := driver.NewHTTPDriver(testServer.URL, http.DefaultTransport)
dr.UserToken = key.Value

specs.TestLoad(t, dr)
}
7 changes: 6 additions & 1 deletion tests/http_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"github.com/fabiante/persurl/config"
"github.com/fabiante/persurl/db"
"github.com/fabiante/persurl/tests/driver"
"github.com/fabiante/persurl/tests/dsl"
"github.com/fabiante/persurl/tests/specs"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/require"
Expand All @@ -22,17 +23,21 @@ func TestWithHTTPDriver(t *testing.T) {
database, err := db.SetupAndMigratePostgresDB(config.DbDSN(), config.DbMaxConnections())
require.NoError(t, err, "setting up db failed")

err = db.EmptyTables(database.Goqu, "purls", "domains")
err = db.EmptyTables(database.Goqu, "purls", "domains", "user_keys", "users")
require.NoError(t, err, "truncating tables failed")

service := app.NewService(database.Gorm)
userService := app.NewUserService(database.Gorm)

key := dsl.GivenSomeUser(t, userService)

server := api.NewServer(service, service, userService)
api.SetupRouting(handler, server)

testServer := httptest.NewServer(handler)

dr := driver.NewHTTPDriver(testServer.URL, http.DefaultTransport)
dr.UserToken = key.Value

specs.TestResolver(t, dr)
specs.TestAdministration(t, dr)
Expand Down

0 comments on commit 6da1d7f

Please sign in to comment.