auth services, middleware, and other stuff
All checks were successful
ci/woodpecker/pr/build Pipeline was successful
ci/woodpecker/pr/lint Pipeline was successful
ci/woodpecker/pr/test Pipeline was successful

This commit is contained in:
2025-05-22 13:55:43 -04:00
parent 70bb4e66b4
commit e55d419d44
22 changed files with 985 additions and 95 deletions

View File

@@ -0,0 +1,90 @@
package service
import (
"context"
"errors"
"log/slog"
"time"
"gitea.michaelthomson.dev/mthomson/habits/internal/auth"
userrepository "gitea.michaelthomson.dev/mthomson/habits/internal/user/repository"
"github.com/golang-jwt/jwt/v5"
)
var (
ErrNotFound error = errors.New("user cannot be found")
ErrUnauthorized error = errors.New("user password incorrect")
)
type UserRepository interface {
Create(ctx context.Context, user userrepository.UserRow) (userrepository.UserRow, error)
GetByEmail(ctx context.Context, email string) (userrepository.UserRow, error)
}
type AuthService struct {
logger *slog.Logger
jwtKey []byte
userRepository UserRepository
argon2IdHash *auth.Argon2IdHash
}
func NewAuthService(logger *slog.Logger, jwtKey []byte, userRepository UserRepository, argon2IdHash *auth.Argon2IdHash) *AuthService {
return &AuthService{
logger: logger,
jwtKey: jwtKey,
userRepository: userRepository,
argon2IdHash: argon2IdHash,
}
}
func (a AuthService) Login(ctx context.Context, email string, password string) (string, error) {
// get user if exists
userRow, err := a.userRepository.GetByEmail(ctx, email)
if err != nil {
if err == userrepository.ErrNotFound {
return "", ErrNotFound
}
a.logger.ErrorContext(ctx, err.Error())
return "", err
}
// compare hashed passswords
err = a.argon2IdHash.Compare(userRow.HashedPassword, userRow.Salt, []byte(password))
if err == auth.ErrNoMatch {
return "", ErrUnauthorized
}
if err != nil {
a.logger.ErrorContext(ctx, err.Error())
return "", err
}
// create token and return it
token, err := a.CreateToken(ctx, email)
if err != nil {
a.logger.ErrorContext(ctx, err.Error())
return "", err
}
return token, nil
}
func (a AuthService) CreateToken(ctx context.Context, email string) (string, error) {
// Create a new JWT token with claims
claims := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"sub": email,
"iss": "todo-app",
"aud": "user",
"exp": time.Now().Add(time.Hour).Unix(),
"iat": time.Now().Unix(),
})
tokenString, err := claims.SignedString(a.jwtKey)
if err != nil {
a.logger.ErrorContext(ctx, err.Error())
}
return tokenString, nil
}

View File

@@ -0,0 +1,81 @@
package service_test
import (
"context"
"log/slog"
"testing"
"gitea.michaelthomson.dev/mthomson/habits/internal/auth"
authservice "gitea.michaelthomson.dev/mthomson/habits/internal/auth/service"
"gitea.michaelthomson.dev/mthomson/habits/internal/test"
"gitea.michaelthomson.dev/mthomson/habits/internal/user/repository"
userservice "gitea.michaelthomson.dev/mthomson/habits/internal/user/service"
"github.com/gofrs/uuid/v5"
)
func TestLogin(t *testing.T) {
t.Parallel()
ctx := context.Background()
logger := slog.Default()
tdb := test.NewTestDatabase(t)
defer tdb.TearDown()
userRepository := repository.NewUserRepository(logger, tdb.Db)
argon2IdHash := auth.NewArgon2IdHash(1, 32, 64*1024, 32, 256)
userService := userservice.NewUserService(logger, userRepository, argon2IdHash)
authService := authservice.NewAuthService(logger, []byte("secretkey"), userRepository, argon2IdHash)
_, err := userService.Register(ctx, "test@test.com", "supersecurepassword")
AssertNoError(t, err)
t.Run("login existing user with correct credentials", func(t *testing.T) {
want, err := authService.CreateToken(ctx, "test@test.com")
AssertNoError(t, err)
got, err := authService.Login(ctx, "test@test.com", "supersecurepassword")
AssertNoError(t, err)
AssertTokens(t, got, want)
})
t.Run("login existing user with incorrect credentials", func(t *testing.T) {
_, err := authService.Login(ctx, "test@test.com", "superwrongpassword")
AssertErrors(t, err, authservice.ErrUnauthorized)
})
t.Run("login nonexistant user", func(t *testing.T) {
_, err := authService.Login(ctx, "foo@test.com", "supersecurepassword")
AssertErrors(t, err, authservice.ErrNotFound)
})
}
func AssertErrors(t testing.TB, got, want error) {
t.Helper()
if got != want {
t.Errorf("got error: %v, want error: %v", want, got)
}
}
func AssertNoError(t testing.TB, err error) {
t.Helper()
if err != nil {
t.Errorf("expected no error, got %v", err)
}
}
func AssertTokens(t testing.TB, got, want string) {
t.Helper()
if got != want {
t.Errorf("expected matching tokens, got %q, want %q", got, want)
}
}
func NewUUID(t testing.TB) uuid.UUID {
t.Helper()
uuid, err := uuid.NewV4()
if err != nil {
t.Errorf("error generation uuid: %v", err)
}
return uuid
}