feat: 0.0.1
This commit is contained in:
146 files changed
+10444
No files matched your search
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
)
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
Reference in new issue
Block a user