3. Auth domain
The authn domain has a single responsibility: validate credentials and issue a JWT. It has no entity or repository of its own — it depends on the users repository.
Service
go
// internal/authn/auth.service.go
package authn
import (
"errors"
"fmt"
"log/slog"
"time"
"github.com/go-minstack/auth"
"golang.org/x/crypto/bcrypt"
"task-api/internal/users/dto"
user_repos "task-api/internal/users/repositories"
)
type AuthService struct {
users *user_repos.UserRepository
jwt *auth.JwtService
log *slog.Logger
}
func NewAuthService(users *user_repos.UserRepository, jwt *auth.JwtService, log *slog.Logger) *AuthService {
return &AuthService{users: users, jwt: jwt, log: log}
}
func (s *AuthService) Login(input user_dto.LoginDto) (string, error) {
user, err := s.users.FindByEmail(input.Email)
if err != nil {
s.log.Warn("login failed: user not found")
return "", errors.New("invalid credentials")
}
if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(input.Password)); err != nil {
s.log.Warn("login failed: wrong password", "user_id", user.ID)
return "", errors.New("invalid credentials")
}
token, err := s.jwt.Sign(auth.Claims{
Subject: fmt.Sprintf("%d", user.ID),
Name: user.Name,
}, 24*time.Hour)
if err != nil {
return "", err
}
s.log.Info("user authenticated", "user_id", user.ID)
return token, nil
}Both a wrong email and a wrong password return the same "invalid credentials" error — this prevents user enumeration. Internally, the logger records the exact failure reason so operators can distinguish the two cases without exposing that information to the caller.
auth.Claims.Subject stores the user ID as a string. Protected endpoints parse it back to uint via strconv.ParseUint.
DTO
go
// internal/authn/dto/token.dto.go
package authn_dto
type TokenDto struct {
Token string `json:"token"`
}Controller
go
// internal/authn/auth.controller.go
package authn
import (
"net/http"
"github.com/gin-gonic/gin"
"github.com/go-minstack/web"
"task-api/internal/authn/dto"
"task-api/internal/users/dto"
)
type AuthController struct {
service *AuthService
}
func NewAuthController(service *AuthService) *AuthController {
return &AuthController{service: service}
}
func (c *AuthController) login(ctx *gin.Context) {
var input user_dto.LoginDto
if err := ctx.ShouldBindJSON(&input); err != nil {
ctx.JSON(http.StatusBadRequest, web.NewErrorDto(err))
return
}
token, err := c.service.Login(input)
if err != nil {
ctx.JSON(http.StatusUnauthorized, web.NewErrorDto(err))
return
}
ctx.JSON(http.StatusOK, authn_dto.TokenDto{Token: token})
}Routes
go
// internal/authn/auth.routes.go
package authn
import "github.com/gin-gonic/gin"
func RegisterRoutes(r *gin.Engine, c *AuthController) {
g := r.Group("/api/auth")
g.POST("/login", c.login)
}Module
go
// internal/authn/module.go
package authn
import "github.com/go-minstack/core"
func Register(app *core.App) {
app.Provide(NewAuthService)
app.Provide(NewAuthController)
app.Invoke(RegisterRoutes)
}The UserRepository that NewAuthService needs is already in the FX container from users.Register.
Next: Tasks domain