feat: 0.0.1

This commit is contained in:
FernandoJVideira committed 2026-01-08 01:15:07 +00:00
commit 961bdd81d4
146 files changed
+10444

No files matched your search

+139
View File
@@ -0,0 +1,139 @@
package service
import (
"context"
"fmt"
"time"
"github.com/FernandoJVideira/simple_bank/internal/auth"
"github.com/FernandoJVideira/simple_bank/internal/config"
db "github.com/FernandoJVideira/simple_bank/internal/db/sqlc"
"github.com/FernandoJVideira/simple_bank/internal/security"
)
type authService struct {
store db.Store
tokenMaker auth.Maker
config config.Config
}
// NewAuthService creates a new auth service
func NewAuthService(store db.Store, tokenMaker auth.Maker, config config.Config) AuthService {
return &authService{
store: store,
tokenMaker: tokenMaker,
config: config,
}
}
func (s *authService) Login(ctx context.Context, params LoginCredentials) (*LoginResponse, error) {
// Get user from database
user, err := s.store.GetUser(ctx, params.Username)
if err != nil {
return nil, fmt.Errorf("user not found: %w", err)
}
// Verify password
err = security.CheckPassword(params.Password, user.HashedPassword)
if err != nil {
return nil, fmt.Errorf("incorrect username or password: %w", err)
}
// Create access token
accessToken, accessPayload, err := s.tokenMaker.CreateToken(
user.Username,
user.Role,
s.config.AuthTokenExp,
)
if err != nil {
return nil, fmt.Errorf("failed to create access token: %w", err)
}
// Create refresh token
refreshToken, refreshPayload, err := s.tokenMaker.CreateToken(
user.Username,
user.Role,
s.config.RefreshTokenExp,
)
if err != nil {
return nil, fmt.Errorf("failed to create refresh token: %w", err)
}
// Create session
session, err := s.store.CreateSession(ctx, db.CreateSessionParams{
ID: refreshPayload.ID,
Username: user.Username,
RefreshToken: refreshToken,
UserAgent: params.UserAgent,
ClientIp: params.ClientIP,
IsBlocked: false,
ExpiresAt: refreshPayload.ExpiredAt,
})
if err != nil {
return nil, fmt.Errorf("failed to create session: %w", err)
}
return &LoginResponse{
SessionID: session.ID,
AccessToken: accessToken,
AccessTokenExpiresAt: accessPayload.ExpiredAt,
RefreshToken: refreshToken,
RefreshTokenExpiresAt: refreshPayload.ExpiredAt,
User: user,
}, nil
}
func (s *authService) RenewAccessToken(ctx context.Context, params TokenRenewalRequest) (*TokenRenewalResponse, error) {
// Verify refresh token
refreshPayload, err := s.tokenMaker.VerifyToken(params.RefreshToken)
if err != nil {
return nil, fmt.Errorf("invalid refresh token: %w", err)
}
// Get session
session, err := s.store.GetSession(ctx, refreshPayload.ID)
if err != nil {
return nil, fmt.Errorf("session not found: %w", err)
}
// Validate session
if err := s.validateSession(session, refreshPayload.Username, params.RefreshToken); err != nil {
return nil, err
}
// Create new access token
accessToken, accessPayload, err := s.tokenMaker.CreateToken(
refreshPayload.Username,
refreshPayload.Role,
s.config.AuthTokenExp,
)
if err != nil {
return nil, fmt.Errorf("failed to create access token: %w", err)
}
return &TokenRenewalResponse{
AccessToken: accessToken,
AccessTokenExpiresAt: accessPayload.ExpiredAt,
}, nil
}
// validateSession contains business rules for session validation
func (s *authService) validateSession(session db.Session, username, refreshToken string) error {
if session.IsBlocked {
return fmt.Errorf("session is blocked")
}
if session.Username != username {
return fmt.Errorf("incorrect session user")
}
if session.RefreshToken != refreshToken {
return fmt.Errorf("mismatched session token")
}
if time.Now().After(session.ExpiresAt) {
return fmt.Errorf("session has expired")
}
return nil
}
+172
View File
@@ -0,0 +1,172 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: github.com/FernandoJVideira/simple_bank/internal/service (interfaces: UserService,AuthService)
//
// Generated by this command:
//
// mockgen --package mocksvc --destination internal/service/mock/service.go github.com/FernandoJVideira/simple_bank/internal/service UserService,AuthService
//
// Package mocksvc is a generated GoMock package.
package mocksvc
import (
context "context"
reflect "reflect"
db "github.com/FernandoJVideira/simple_bank/internal/db/sqlc"
service "github.com/FernandoJVideira/simple_bank/internal/service"
gomock "go.uber.org/mock/gomock"
)
// MockUserService is a mock of UserService interface.
type MockUserService struct {
ctrl *gomock.Controller
recorder *MockUserServiceMockRecorder
isgomock struct{}
}
// MockUserServiceMockRecorder is the mock recorder for MockUserService.
type MockUserServiceMockRecorder struct {
mock *MockUserService
}
// NewMockUserService creates a new mock instance.
func NewMockUserService(ctrl *gomock.Controller) *MockUserService {
mock := &MockUserService{ctrl: ctrl}
mock.recorder = &MockUserServiceMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockUserService) EXPECT() *MockUserServiceMockRecorder {
return m.recorder
}
// CreateUser mocks base method.
func (m *MockUserService) CreateUser(ctx context.Context, params service.RegisterUserRequest) (*service.RegisterUserResponse, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CreateUser", ctx, params)
ret0, _ := ret[0].(*service.RegisterUserResponse)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// CreateUser indicates an expected call of CreateUser.
func (mr *MockUserServiceMockRecorder) CreateUser(ctx, params any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateUser", reflect.TypeOf((*MockUserService)(nil).CreateUser), ctx, params)
}
// GetUser mocks base method.
func (m *MockUserService) GetUser(ctx context.Context, username string) (*db.User, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetUser", ctx, username)
ret0, _ := ret[0].(*db.User)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetUser indicates an expected call of GetUser.
func (mr *MockUserServiceMockRecorder) GetUser(ctx, username any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUser", reflect.TypeOf((*MockUserService)(nil).GetUser), ctx, username)
}
// UpdatePassword mocks base method.
func (m *MockUserService) UpdatePassword(ctx context.Context, params service.PasswordUpdateRequest) (*db.User, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UpdatePassword", ctx, params)
ret0, _ := ret[0].(*db.User)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// UpdatePassword indicates an expected call of UpdatePassword.
func (mr *MockUserServiceMockRecorder) UpdatePassword(ctx, params any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdatePassword", reflect.TypeOf((*MockUserService)(nil).UpdatePassword), ctx, params)
}
// UpdateUser mocks base method.
func (m *MockUserService) UpdateUser(ctx context.Context, params service.UserUpdateRequest) (*db.User, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UpdateUser", ctx, params)
ret0, _ := ret[0].(*db.User)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// UpdateUser indicates an expected call of UpdateUser.
func (mr *MockUserServiceMockRecorder) UpdateUser(ctx, params any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateUser", reflect.TypeOf((*MockUserService)(nil).UpdateUser), ctx, params)
}
// VerifyEmail mocks base method.
func (m *MockUserService) VerifyEmail(ctx context.Context, params service.EmailVerificationRequest) (*service.EmailVerificationResponse, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "VerifyEmail", ctx, params)
ret0, _ := ret[0].(*service.EmailVerificationResponse)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// VerifyEmail indicates an expected call of VerifyEmail.
func (mr *MockUserServiceMockRecorder) VerifyEmail(ctx, params any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "VerifyEmail", reflect.TypeOf((*MockUserService)(nil).VerifyEmail), ctx, params)
}
// MockAuthService is a mock of AuthService interface.
type MockAuthService struct {
ctrl *gomock.Controller
recorder *MockAuthServiceMockRecorder
isgomock struct{}
}
// MockAuthServiceMockRecorder is the mock recorder for MockAuthService.
type MockAuthServiceMockRecorder struct {
mock *MockAuthService
}
// NewMockAuthService creates a new mock instance.
func NewMockAuthService(ctrl *gomock.Controller) *MockAuthService {
mock := &MockAuthService{ctrl: ctrl}
mock.recorder = &MockAuthServiceMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockAuthService) EXPECT() *MockAuthServiceMockRecorder {
return m.recorder
}
// Login mocks base method.
func (m *MockAuthService) Login(ctx context.Context, params service.LoginCredentials) (*service.LoginResponse, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Login", ctx, params)
ret0, _ := ret[0].(*service.LoginResponse)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Login indicates an expected call of Login.
func (mr *MockAuthServiceMockRecorder) Login(ctx, params any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Login", reflect.TypeOf((*MockAuthService)(nil).Login), ctx, params)
}
// RenewAccessToken mocks base method.
func (m *MockAuthService) RenewAccessToken(ctx context.Context, params service.TokenRenewalRequest) (*service.TokenRenewalResponse, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "RenewAccessToken", ctx, params)
ret0, _ := ret[0].(*service.TokenRenewalResponse)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// RenewAccessToken indicates an expected call of RenewAccessToken.
func (mr *MockAuthServiceMockRecorder) RenewAccessToken(ctx, params any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RenewAccessToken", reflect.TypeOf((*MockAuthService)(nil).RenewAccessToken), ctx, params)
}
+27
View File
@@ -0,0 +1,27 @@
package service
import (
"context"
db "github.com/FernandoJVideira/simple_bank/internal/db/sqlc"
)
type UserService interface {
CreateUser(ctx context.Context, params RegisterUserRequest) (*RegisterUserResponse, error)
GetUser(ctx context.Context, username string) (*db.User, error)
UpdateUser(ctx context.Context, params UserUpdateRequest) (*db.User, error)
UpdatePassword(ctx context.Context, params PasswordUpdateRequest) (*db.User, error)
VerifyEmail(ctx context.Context, params EmailVerificationRequest) (*EmailVerificationResponse, error)
}
// AuthService defines operations related to authentication
type AuthService interface {
Login(ctx context.Context, params LoginCredentials) (*LoginResponse, error)
RenewAccessToken(ctx context.Context, params TokenRenewalRequest) (*TokenRenewalResponse, error)
}
// Ensure implementations satisfy interfaces at compile time
var (
_ UserService = (*userService)(nil)
_ AuthService = (*authService)(nil)
)
+69
View File
@@ -0,0 +1,69 @@
package service
import (
"time"
db "github.com/FernandoJVideira/simple_bank/internal/db/sqlc"
"github.com/google/uuid"
)
// User Service Types - Use business operation names
type RegisterUserRequest struct {
Username string
Password string
FullName string
Email string
}
type RegisterUserResponse struct {
User db.User
}
type UserUpdateRequest struct {
Username string
FullName *string
Email *string
}
type PasswordUpdateRequest struct {
Username string
OldPassword string
NewPassword string
}
type EmailVerificationRequest struct {
EmailID int64
SecretCode string
}
type EmailVerificationResponse struct {
User db.User
}
// Auth Service Types - Clear, semantic names
type LoginCredentials struct {
Username string
Password string
UserAgent string
ClientIP string
}
type LoginResponse struct {
SessionID uuid.UUID
AccessToken string
AccessTokenExpiresAt time.Time
RefreshToken string
RefreshTokenExpiresAt time.Time
User db.User
}
type TokenRenewalRequest struct {
RefreshToken string
}
type TokenRenewalResponse struct {
AccessToken string
AccessTokenExpiresAt time.Time
}
+138
View File
@@ -0,0 +1,138 @@
package service
import (
"context"
"fmt"
"time"
db "github.com/FernandoJVideira/simple_bank/internal/db/sqlc"
"github.com/FernandoJVideira/simple_bank/internal/security"
"github.com/FernandoJVideira/simple_bank/internal/worker"
"github.com/hibiken/asynq"
"github.com/jackc/pgx/v5/pgtype"
)
type userService struct {
store db.Store
taskDistributor worker.TaskDistributor
}
// NewUserService creates a new user service
func NewUserService(store db.Store, taskDistributor worker.TaskDistributor) UserService {
return &userService{
store: store,
taskDistributor: taskDistributor,
}
}
func (s *userService) CreateUser(ctx context.Context, params RegisterUserRequest) (*RegisterUserResponse, error) {
// Hash the password
hashedPassword, err := security.HashPassword(params.Password)
if err != nil {
return nil, fmt.Errorf("failed to hash password: %w", err)
}
// Prepare transaction parameters
arg := db.CreateUserTxParams{
CreateUserParams: db.CreateUserParams{
Username: params.Username,
FullName: params.FullName,
Email: params.Email,
HashedPassword: hashedPassword,
},
AfterCreate: func(user db.User) error {
opts := []asynq.Option{
asynq.MaxRetry(10),
asynq.ProcessIn(10 * time.Second),
asynq.Queue(worker.CriticalQueue),
}
taskPayload := &worker.SendVerifyEmailPayload{
Username: user.Username,
}
return s.taskDistributor.DestributeTaskSendVerifyEmail(ctx, taskPayload, opts...)
},
}
// Execute transaction
txResult, err := s.store.CreateUserTx(ctx, arg)
if err != nil {
return nil, fmt.Errorf("failed to create user: %w", err)
}
return &RegisterUserResponse{User: txResult.User}, nil
}
func (s *userService) GetUser(ctx context.Context, username string) (*db.User, error) {
user, err := s.store.GetUser(ctx, username)
if err != nil {
return nil, err
}
return &user, nil
}
func (s *userService) UpdateUser(ctx context.Context, params UserUpdateRequest) (*db.User, error) {
arg := db.UpdateUserParams{
Username: params.Username,
FullName: pgtype.Text{
String: *params.FullName,
Valid: params.FullName != nil,
},
Email: pgtype.Text{
String: *params.Email,
Valid: params.Email != nil,
},
}
user, err := s.store.UpdateUser(ctx, arg)
if err != nil {
return nil, err
}
return &user, nil
}
func (s *userService) UpdatePassword(ctx context.Context, params PasswordUpdateRequest) (*db.User, error) {
user, err := s.store.GetUser(ctx, params.Username)
if err != nil {
return nil, err
}
err = security.CheckPassword(params.OldPassword, user.HashedPassword)
if err != nil {
return nil, fmt.Errorf("old password does not match: %w", err)
}
newHashedPassword, err := security.HashPassword(params.NewPassword)
if err != nil {
return nil, fmt.Errorf("failed to hash new password: %w", err)
}
updUser, err := s.store.UpdateUser(ctx, db.UpdateUserParams{
Username: params.Username,
HashedPassword: pgtype.Text{
String: newHashedPassword,
Valid: true,
},
PasswordChangedAt: pgtype.Timestamptz{
Time: time.Now(),
Valid: true,
},
})
if err != nil {
return nil, err
}
return &updUser, nil
}
func (s *userService) VerifyEmail(ctx context.Context, params EmailVerificationRequest) (*EmailVerificationResponse, error) {
txRes, err := s.store.VerifyEmailTx(ctx, db.VerifyEmailTxParams{
EmailID: params.EmailID,
SecretCode: params.SecretCode,
})
if err != nil {
return nil, err
}
return &EmailVerificationResponse{User: txRes.User}, nil
}