auth services, middleware, and other stuff
This commit is contained in:
@@ -5,52 +5,48 @@ import (
|
||||
"errors"
|
||||
"log/slog"
|
||||
|
||||
"gitea.michaelthomson.dev/mthomson/habits/internal/auth"
|
||||
"gitea.michaelthomson.dev/mthomson/habits/internal/user/repository"
|
||||
"github.com/gofrs/uuid/v5"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrNotFound error = errors.New("user cannot be found")
|
||||
ErrNotFound error = errors.New("user cannot be found")
|
||||
ErrUserExists error = errors.New("user already exists")
|
||||
)
|
||||
|
||||
type User struct {
|
||||
Id uuid.UUID
|
||||
Email string
|
||||
HashedPassword string
|
||||
}
|
||||
|
||||
func NewUser(id uuid.UUID, email string, hashedPassword string) User {
|
||||
return User{Id: id, Email: email, HashedPassword: hashedPassword}
|
||||
Id uuid.UUID
|
||||
Email string
|
||||
}
|
||||
|
||||
func UserFromUserRow(userRow repository.UserRow) User {
|
||||
return User{Id: userRow.Id, Email: userRow.Email, HashedPassword: userRow.HashedPassword}
|
||||
}
|
||||
|
||||
func UserRowFromUser(user User) repository.UserRow {
|
||||
return repository.UserRow{Id: user.Id, Email: user.Email, HashedPassword: user.HashedPassword}
|
||||
return User{Id: userRow.Id, Email: userRow.Email}
|
||||
}
|
||||
|
||||
func (t User) Equal(user User) bool {
|
||||
return t.Id == user.Id && t.Email == user.Email && t.HashedPassword == user.HashedPassword
|
||||
return t.Id == user.Id && t.Email == user.Email
|
||||
}
|
||||
|
||||
type UserRepository interface {
|
||||
Create(ctx context.Context, user repository.UserRow) (repository.UserRow, error)
|
||||
GetById(ctx context.Context, id uuid.UUID) (repository.UserRow, error)
|
||||
GetByEmail(ctx context.Context, email string) (repository.UserRow, error)
|
||||
Update(ctx context.Context, user repository.UserRow) error
|
||||
Delete(ctx context.Context, id uuid.UUID) error
|
||||
}
|
||||
|
||||
type UserService struct {
|
||||
logger *slog.Logger
|
||||
repo UserRepository
|
||||
logger *slog.Logger
|
||||
repo UserRepository
|
||||
argon2IdHash *auth.Argon2IdHash
|
||||
}
|
||||
|
||||
func NewUserService(logger *slog.Logger, userRepo UserRepository) *UserService {
|
||||
func NewUserService(logger *slog.Logger, userRepo UserRepository, argon2IdHash *auth.Argon2IdHash) *UserService {
|
||||
return &UserService{
|
||||
logger: logger,
|
||||
repo: userRepo,
|
||||
logger: logger,
|
||||
repo: userRepo,
|
||||
argon2IdHash: argon2IdHash,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,17 +65,37 @@ func (s *UserService) GetUser(ctx context.Context, id uuid.UUID) (User, error) {
|
||||
return UserFromUserRow(user), err
|
||||
}
|
||||
|
||||
func (s *UserService) CreateUser(ctx context.Context, user User) (User, error) {
|
||||
userRow := UserRowFromUser(user)
|
||||
|
||||
newUserRow, err := s.repo.Create(ctx, userRow)
|
||||
func (s *UserService) Register(ctx context.Context, email string, password string) (uuid.UUID, error) {
|
||||
uuid, err := uuid.NewV4()
|
||||
|
||||
if err != nil {
|
||||
s.logger.ErrorContext(ctx, err.Error())
|
||||
return User{}, err
|
||||
return uuid, err
|
||||
}
|
||||
|
||||
return UserFromUserRow(newUserRow), err
|
||||
_, err = s.repo.GetByEmail(ctx, email)
|
||||
|
||||
if err != repository.ErrNotFound {
|
||||
return uuid, ErrUserExists
|
||||
}
|
||||
|
||||
hashSalt, err := s.argon2IdHash.GenerateHash([]byte(password), nil)
|
||||
|
||||
if err != nil {
|
||||
s.logger.ErrorContext(ctx, err.Error())
|
||||
return uuid, err
|
||||
}
|
||||
|
||||
userRow := repository.UserRow{Id: uuid, Email: email, HashedPassword: hashSalt.Hash, Salt: hashSalt.Salt}
|
||||
|
||||
_, err = s.repo.Create(ctx, userRow)
|
||||
|
||||
if err != nil {
|
||||
s.logger.ErrorContext(ctx, err.Error())
|
||||
return uuid, err
|
||||
}
|
||||
|
||||
return uuid, err
|
||||
}
|
||||
|
||||
func (s *UserService) DeleteUser(ctx context.Context, id uuid.UUID) error {
|
||||
@@ -96,10 +112,66 @@ func (s *UserService) DeleteUser(ctx context.Context, id uuid.UUID) error {
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *UserService) UpdateUser(ctx context.Context, user User) error {
|
||||
userRow := UserRowFromUser(user)
|
||||
func (s *UserService) UpdateUserEmail(ctx context.Context, id uuid.UUID, email string) error {
|
||||
user, err := s.repo.GetById(ctx, id)
|
||||
|
||||
err := s.repo.Update(ctx, userRow)
|
||||
if err == repository.ErrNotFound {
|
||||
return ErrNotFound
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
s.logger.ErrorContext(ctx, err.Error())
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = s.repo.GetByEmail(ctx, email)
|
||||
|
||||
switch err {
|
||||
case repository.ErrNotFound:
|
||||
user.Email = email
|
||||
|
||||
err = s.repo.Update(ctx, user)
|
||||
|
||||
if err == repository.ErrNotFound {
|
||||
return ErrNotFound
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
s.logger.ErrorContext(ctx, err.Error())
|
||||
}
|
||||
|
||||
return err
|
||||
case nil:
|
||||
return ErrUserExists
|
||||
default:
|
||||
s.logger.ErrorContext(ctx, err.Error())
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
func (s *UserService) UpdateUserPassword(ctx context.Context, id uuid.UUID, password string) error {
|
||||
user, err := s.repo.GetById(ctx, id)
|
||||
|
||||
if err == repository.ErrNotFound {
|
||||
return ErrNotFound
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
s.logger.ErrorContext(ctx, err.Error())
|
||||
return err
|
||||
}
|
||||
|
||||
hashSalt, err := s.argon2IdHash.GenerateHash([]byte(password), nil)
|
||||
|
||||
if err != nil {
|
||||
s.logger.ErrorContext(ctx, err.Error())
|
||||
return err
|
||||
}
|
||||
|
||||
user.HashedPassword = hashSalt.Hash
|
||||
user.Salt = hashSalt.Salt
|
||||
|
||||
err = s.repo.Update(ctx, user)
|
||||
|
||||
if err == repository.ErrNotFound {
|
||||
return ErrNotFound
|
||||
|
||||
@@ -5,27 +5,26 @@ import (
|
||||
"log/slog"
|
||||
"testing"
|
||||
|
||||
"gitea.michaelthomson.dev/mthomson/habits/internal/auth"
|
||||
"gitea.michaelthomson.dev/mthomson/habits/internal/test"
|
||||
"gitea.michaelthomson.dev/mthomson/habits/internal/user/repository"
|
||||
"gitea.michaelthomson.dev/mthomson/habits/internal/user/service"
|
||||
"github.com/gofrs/uuid/v5"
|
||||
)
|
||||
|
||||
func TestCreateUser(t *testing.T) {
|
||||
func TestRegisterUser(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := context.Background()
|
||||
logger := slog.Default()
|
||||
tdb := test.NewTestDatabase(t)
|
||||
defer tdb.TearDown()
|
||||
r := repository.NewUserRepository(logger, tdb.Db)
|
||||
argon2IdHash := auth.NewArgon2IdHash(1, 32, 64*1024, 32, 256)
|
||||
|
||||
userService := service.NewUserService(logger, r)
|
||||
userService := service.NewUserService(logger, r, argon2IdHash)
|
||||
|
||||
t.Run("Create user", func(t *testing.T) {
|
||||
uuid := NewUUID(t)
|
||||
user := service.NewUser(uuid, "test@test.com", "supersecurehash")
|
||||
|
||||
_, err := userService.CreateUser(ctx, user)
|
||||
_, err := userService.Register(ctx, "test@test.com", "supersecurepassword")
|
||||
|
||||
AssertNoError(t, err)
|
||||
})
|
||||
@@ -38,13 +37,12 @@ func TestGetUser(t *testing.T) {
|
||||
tdb := test.NewTestDatabase(t)
|
||||
defer tdb.TearDown()
|
||||
r := repository.NewUserRepository(logger, tdb.Db)
|
||||
uuid := NewUUID(t)
|
||||
argon2IdHash := auth.NewArgon2IdHash(1, 32, 64*1024, 32, 256)
|
||||
|
||||
row := repository.UserRow{Id: uuid, Email: "test@test.com", HashedPassword: "supersecurehash"}
|
||||
_, err := r.Create(ctx, row)
|
||||
AssertNoError(t, err);
|
||||
userService := service.NewUserService(logger, r, argon2IdHash)
|
||||
|
||||
userService := service.NewUserService(logger, r)
|
||||
uuid, err := userService.Register(ctx, "test@test.com", "supersecurepassword")
|
||||
AssertNoError(t, err)
|
||||
|
||||
t.Run("Get exisiting user", func(t *testing.T) {
|
||||
_, err := userService.GetUser(ctx, uuid)
|
||||
@@ -66,13 +64,12 @@ func TestDeleteUser(t *testing.T) {
|
||||
tdb := test.NewTestDatabase(t)
|
||||
defer tdb.TearDown()
|
||||
r := repository.NewUserRepository(logger, tdb.Db)
|
||||
uuid := NewUUID(t)
|
||||
argon2IdHash := auth.NewArgon2IdHash(1, 32, 64*1024, 32, 256)
|
||||
|
||||
row := repository.UserRow{Id: uuid, Email: "test@test.com", HashedPassword: "supersecurehash"}
|
||||
_, err := r.Create(ctx, row)
|
||||
AssertNoError(t, err);
|
||||
userService := service.NewUserService(logger, r, argon2IdHash)
|
||||
|
||||
userService := service.NewUserService(logger, r)
|
||||
uuid, err := userService.Register(ctx, "test@test.com", "supersecurepassword")
|
||||
AssertNoError(t, err)
|
||||
|
||||
t.Run("Delete exisiting user", func(t *testing.T) {
|
||||
err := userService.DeleteUser(ctx, uuid)
|
||||
@@ -87,25 +84,22 @@ func TestDeleteUser(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestUpdateUser(t *testing.T) {
|
||||
func TestUpdateUserEmail(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := context.Background()
|
||||
logger := slog.Default()
|
||||
tdb := test.NewTestDatabase(t)
|
||||
defer tdb.TearDown()
|
||||
r := repository.NewUserRepository(logger, tdb.Db)
|
||||
uuid := NewUUID(t)
|
||||
argon2IdHash := auth.NewArgon2IdHash(1, 32, 64*1024, 32, 256)
|
||||
|
||||
row := repository.UserRow{Id: uuid, Email: "test@test.com", HashedPassword: "supersecurehash"}
|
||||
_, err := r.Create(ctx, row)
|
||||
AssertNoError(t, err);
|
||||
userService := service.NewUserService(logger, r, argon2IdHash)
|
||||
|
||||
userService := service.NewUserService(logger, r)
|
||||
uuid, err := userService.Register(ctx, "test@test.com", "supersecurepassword")
|
||||
AssertNoError(t, err)
|
||||
|
||||
t.Run("Update exisiting user", func(t *testing.T) {
|
||||
user := service.User{uuid, "new@email.com", "supersecurehash"}
|
||||
|
||||
err := userService.UpdateUser(ctx, user)
|
||||
t.Run("Update existing user email", func(t *testing.T) {
|
||||
err := userService.UpdateUserEmail(ctx, uuid, "newemail@test.com")
|
||||
|
||||
AssertNoError(t, err)
|
||||
|
||||
@@ -113,13 +107,13 @@ func TestUpdateUser(t *testing.T) {
|
||||
|
||||
AssertNoError(t, err)
|
||||
|
||||
AssertUsers(t, newUser, user)
|
||||
if newUser.Email != "newemail@test.com" {
|
||||
t.Errorf("Emails do not match wanted %q, got %q", "newemail@test.com", newUser.Email)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Update non-existant user", func(t *testing.T) {
|
||||
user := service.User{NewUUID(t), "new@email.com", "supersecurehash"}
|
||||
|
||||
err := userService.UpdateUser(ctx, user)
|
||||
err := userService.UpdateUserEmail(ctx, NewUUID(t), "newemail@test.com")
|
||||
|
||||
AssertErrors(t, err, service.ErrNotFound)
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user