Rename the module, proto service, and all identifiers from simple_bank/ SimpleBank to api_tmpl/ApiTmpl, and regenerate the gRPC/gateway/swagger/ mock code accordingly. - Scrub real secrets (SMTP password, auth token secret) from api.env/ api_test.env, untrack them, and add api.env.example/api_test.env.example - Remove committed build artifacts (tmp/main, tmp/build-errors.log, dbml-error.log) and gitignore them going forward - Fix Dockerfile paths (build target, migrations copy) that referenced a stale project layout - Rewrite README to match the actual cmd/ + internal/ structure and add a "using this as a template" section
140 lines
3.6 KiB
Go
140 lines
3.6 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/FernandoJVideira/api_tmpl/internal/auth"
|
|
"github.com/FernandoJVideira/api_tmpl/internal/config"
|
|
db "github.com/FernandoJVideira/api_tmpl/internal/db/sqlc"
|
|
"github.com/FernandoJVideira/api_tmpl/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
|
|
}
|