photum/backend/internal/auth/service.go
2026-02-06 10:30:26 -03:00

449 lines
13 KiB
Go

package auth
import (
"context"
"crypto/sha256"
"encoding/hex"
"errors"
"fmt"
"time"
"photum-backend/internal/config"
"photum-backend/internal/db/generated"
"photum-backend/internal/profissionais"
"github.com/google/uuid"
"github.com/jackc/pgx/v5/pgtype"
"golang.org/x/crypto/bcrypt"
)
const (
RoleSuperAdmin = "SUPERADMIN"
RoleBusinessOwner = "BUSINESS_OWNER"
RolePhotographer = "PHOTOGRAPHER"
RoleEventOwner = "EVENT_OWNER"
RoleAgendaViewer = "AGENDA_VIEWER"
RoleResearcher = "RESEARCHER"
)
type Service struct {
queries *generated.Queries
profissionaisService *profissionais.Service
jwtAccessSecret string
jwtRefreshSecret string
jwtAccessTTLMinutes int
jwtRefreshTTLDays int
}
func NewService(queries *generated.Queries, profissionaisService *profissionais.Service, cfg *config.Config) *Service {
return &Service{
queries: queries,
profissionaisService: profissionaisService,
jwtAccessSecret: cfg.JwtAccessSecret,
jwtRefreshSecret: cfg.JwtRefreshSecret,
jwtAccessTTLMinutes: cfg.JwtAccessTTLMinutes,
jwtRefreshTTLDays: cfg.JwtRefreshTTLDays,
}
}
func (s *Service) Register(ctx context.Context, email, senha, role, nome, telefone, tipoProfissional string, empresaID *string, profissionalData *profissionais.CreateProfissionalInput, regiao string) (*generated.Usuario, error) {
// Hash password
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(senha), bcrypt.DefaultCost)
if err != nil {
return nil, err
}
// Create user
user, err := s.queries.CreateUsuario(ctx, generated.CreateUsuarioParams{
Email: email,
SenhaHash: string(hashedPassword),
Role: role,
TipoProfissional: toPgText(&tipoProfissional),
})
if err != nil {
return nil, err
}
// If region is provided and different from default (or just force set it)
// We want to ensure the user has access to the region they registered in.
if regiao != "" {
err = s.queries.UpdateUsuarioRegions(ctx, generated.UpdateUsuarioRegionsParams{
ID: pgtype.UUID{Bytes: user.ID.Bytes, Valid: true},
RegioesPermitidas: []string{regiao}, // Set strictly to the selected region
})
if err != nil {
_ = s.queries.DeleteUsuario(ctx, user.ID)
return nil, err
}
// Update user object in memory to reflect this (for Login call later if it uses this object?
// Login fetches fresh from DB, so it's fine)
}
// If role is 'PHOTOGRAPHER' or 'BUSINESS_OWNER', create professional profile
if (role == RolePhotographer || role == RoleBusinessOwner) && profissionalData != nil {
userID := uuid.UUID(user.ID.Bytes).String()
_, err := s.profissionaisService.Create(ctx, userID, *profissionalData, regiao)
if err != nil {
// Rollback user creation (best effort)
_ = s.queries.DeleteUsuario(ctx, user.ID)
return nil, err
}
}
// If role is 'EVENT_OWNER', create client profile
if role == RoleEventOwner {
userID := user.ID
var empID pgtype.UUID
if empresaID != nil && *empresaID != "" {
parsedEmpID, err := uuid.Parse(*empresaID)
if err == nil {
empID.Bytes = parsedEmpID
empID.Valid = true
}
}
_, err := s.queries.CreateCadastroCliente(ctx, generated.CreateCadastroClienteParams{
UsuarioID: userID,
EmpresaID: empID,
Nome: pgtype.Text{String: nome, Valid: nome != ""},
Telefone: pgtype.Text{String: telefone, Valid: telefone != ""},
})
if err != nil {
_ = s.queries.DeleteUsuario(ctx, user.ID)
return nil, err
}
}
return &user, nil
}
type TokenPair struct {
AccessToken string
RefreshToken string
}
func (s *Service) Login(ctx context.Context, email, senha string) (*TokenPair, *generated.GetUsuarioByEmailRow, *generated.GetProfissionalByUsuarioIDRow, error) {
// The query now returns a Row with joined fields, not just Usuario struct
user, err := s.queries.GetUsuarioByEmail(ctx, email)
if err != nil {
fmt.Printf("[DEBUG] Login Failed: Email %s not found. Error: %v\n", email, err)
return nil, nil, nil, errors.New("invalid credentials")
}
err = bcrypt.CompareHashAndPassword([]byte(user.SenhaHash), []byte(senha))
if err != nil {
fmt.Printf("[DEBUG] Login Failed: Password mismatch for %s. DBHash: %s... Input: %s. Error: %v\n", email, user.SenhaHash[:10], senha, err)
return nil, nil, nil, errors.New("invalid credentials")
}
userUUID := uuid.UUID(user.ID.Bytes)
accessToken, _, err := GenerateAccessToken(userUUID, user.Role, user.RegioesPermitidas, s.jwtAccessSecret, s.jwtAccessTTLMinutes)
if err != nil {
return nil, nil, nil, err
}
refreshToken, err := GenerateRefreshToken(userUUID, s.jwtRefreshSecret, s.jwtRefreshTTLDays)
if err != nil {
return nil, nil, nil, err
}
var profData *generated.GetProfissionalByUsuarioIDRow
if user.Role == RolePhotographer || user.Role == RoleBusinessOwner {
p, err := s.queries.GetProfissionalByUsuarioID(ctx, user.ID)
if err == nil {
profData = &p
}
}
return &TokenPair{
AccessToken: accessToken,
RefreshToken: refreshToken,
}, &user, profData, nil
}
func (s *Service) Refresh(ctx context.Context, refreshTokenRaw string) (string, time.Time, error) {
hash := sha256.Sum256([]byte(refreshTokenRaw))
hashString := hex.EncodeToString(hash[:])
storedToken, err := s.queries.GetRefreshToken(ctx, hashString)
if err != nil {
return "", time.Time{}, errors.New("invalid refresh token")
}
if storedToken.Revogado {
return "", time.Time{}, errors.New("token revoked")
}
if time.Now().After(storedToken.ExpiraEm.Time) {
return "", time.Time{}, errors.New("token expired")
}
user, err := s.queries.GetUsuarioByID(ctx, storedToken.UsuarioID)
if err != nil {
return "", time.Time{}, errors.New("user not found")
}
userUUID := uuid.UUID(user.ID.Bytes)
return GenerateAccessToken(userUUID, user.Role, user.RegioesPermitidas, s.jwtAccessSecret, s.jwtAccessTTLMinutes)
}
func (s *Service) Logout(ctx context.Context, refreshTokenRaw string) error {
hash := sha256.Sum256([]byte(refreshTokenRaw))
hashString := hex.EncodeToString(hash[:])
return s.queries.RevokeRefreshToken(ctx, hashString)
}
func (s *Service) ListPendingUsers(ctx context.Context, regiao string) ([]generated.ListUsuariosPendingRow, error) {
if regiao == "" {
regiao = "SP"
}
return s.queries.ListUsuariosPending(ctx, regiao)
}
func (s *Service) ApproveUser(ctx context.Context, id string) error {
parsedUUID, err := uuid.Parse(id)
if err != nil {
return err
}
var pgID pgtype.UUID
pgID.Bytes = parsedUUID
pgID.Valid = true
_, err = s.queries.UpdateUsuarioAtivo(ctx, generated.UpdateUsuarioAtivoParams{
ID: pgID,
Ativo: true,
})
return err
}
func (s *Service) AdminCreateUser(ctx context.Context, email, senha, role, nome, tipoProfissional string, ativo bool, regiao string) (*generated.Usuario, error) {
// Hash password
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(senha), bcrypt.DefaultCost)
if err != nil {
return nil, err
}
// Create user
user, err := s.queries.CreateUsuario(ctx, generated.CreateUsuarioParams{
Email: email,
SenhaHash: string(hashedPassword),
Role: role,
TipoProfissional: toPgText(&tipoProfissional),
})
if err != nil {
return nil, err
}
if ativo {
// Approve user immediately
err = s.ApproveUser(ctx, uuid.UUID(user.ID.Bytes).String())
if err != nil {
return nil, err
}
// Refresh user object to reflect changes if needed, but ID and Email are same.
user.Ativo = true
}
// Create professional profile if applicable
if role == RolePhotographer || role == RoleBusinessOwner {
var funcaoID string
// If specific professional type is provided, resolve it
if tipoProfissional != "" {
// Resolve Function ID by Name using list scanning (since GetByName doesn't exist)
funcoes, err := s.queries.ListFuncoes(ctx)
if err == nil {
for _, f := range funcoes {
if f.Nome == tipoProfissional {
funcaoID = uuid.UUID(f.ID.Bytes).String()
break
}
}
}
}
// Prepare professional input
profInput := profissionais.CreateProfissionalInput{
Nome: nome,
Email: &email,
FuncaoProfissionalID: funcaoID,
// Add default whatsapp if user has one? User struct doesn't have phone here passed in args,
// but we can pass it if we update signature or just leave empty.
}
// Create the professional
_, err = s.profissionaisService.Create(ctx, uuid.UUID(user.ID.Bytes).String(), profInput, regiao)
if err != nil {
// Log error but don't fail user creation?
// Better to log. Backend logs not setup here.
// Just continue for now, or return error?
// If we fail here, user exists but has no profile.
// Ideally we should delete user or return error.
// Let's log and ignore for now to avoid breaking legacy flows if any.
// Actually, if this fails, the bug persists. Best to return error.
// But since we already committed user, we should probably return error so client knows.
// Try to delete user to rollback
_ = s.queries.DeleteUsuario(ctx, user.ID)
return nil, err
}
}
return &user, nil
}
func (s *Service) UpdateUserRole(ctx context.Context, id, newRole string) error {
parsedUUID, err := uuid.Parse(id)
if err != nil {
return err
}
var pgID pgtype.UUID
pgID.Bytes = parsedUUID
pgID.Valid = true
_, err = s.queries.UpdateUsuarioRole(ctx, generated.UpdateUsuarioRoleParams{
ID: pgID,
Role: newRole,
})
return err
}
func (s *Service) DeleteUser(ctx context.Context, id string) error {
parsedUUID, err := uuid.Parse(id)
if err != nil {
return err
}
var pgID pgtype.UUID
pgID.Bytes = parsedUUID
pgID.Valid = true
return s.queries.DeleteUsuario(ctx, pgID)
}
func (s *Service) EnsureDemoUsers(ctx context.Context) error {
demoUsers := []struct {
Email string
Role string
Name string
Regions []string // Optional specific regions
}{
{"admin@photum.com", RoleSuperAdmin, "Dev Admin", []string{"SP", "MG"}},
{"empresa@photum.com", RoleBusinessOwner, "PHOTUM CEO", []string{"SP", "MG"}},
{"admin.sp@photum.com", RoleBusinessOwner, "ADMIN SP", []string{"SP"}},
{"admin.mg@photum.com", RoleBusinessOwner, "ADMIN MG", []string{"MG"}},
{"foto@photum.com", RolePhotographer, "COLABORADOR PHOTUM", []string{"SP"}},
{"cliente@photum.com", RoleEventOwner, "CLIENTE TESTE", []string{"SP"}},
{"pesquisa@photum.com", RoleResearcher, "PESQUISADOR", []string{"SP"}},
{"viewer@photum.com", RoleAgendaViewer, "VISUALIZADOR PHOTUM", []string{"SP", "MG"}},
}
for _, u := range demoUsers {
existingUser, err := s.queries.GetUsuarioByEmail(ctx, u.Email)
regions := u.Regions
if len(regions) == 0 {
regions = []string{"SP"} // Default fallback
}
hashedPassword, _ := bcrypt.GenerateFromPassword([]byte("123456"), bcrypt.DefaultCost)
if err != nil {
fmt.Printf("[DEBUG] Creating User %s\n", u.Email)
// User not found (or error), try to create
// Note: We use "SP" as default regiao for professional creation
user, err := s.AdminCreateUser(ctx, u.Email, "123456", u.Role, u.Name, "", true, "SP")
if err != nil {
fmt.Printf("[DEBUG] Error creating user %s: %v\n", u.Email, err)
return err
}
// Update to include specific regions
err = s.queries.UpdateUsuarioRegions(ctx, generated.UpdateUsuarioRegionsParams{
ID: pgtype.UUID{Bytes: user.ID.Bytes, Valid: true},
RegioesPermitidas: regions,
})
if err != nil {
fmt.Printf("[DEBUG] Error updating regions for new user %s: %v\n", u.Email, err)
return err
}
} else {
// User exists, FORCE update password and ensure regions
fmt.Printf("[DEBUG] Updating Existing User %s\n", u.Email)
pgID := pgtype.UUID{Bytes: existingUser.ID.Bytes, Valid: true}
// Update Password
err = s.queries.UpdateUsuarioSenha(ctx, generated.UpdateUsuarioSenhaParams{
ID: pgID,
SenhaHash: string(hashedPassword),
})
if err != nil {
fmt.Printf("[DEBUG] Error updating password for %s: %v\n", u.Email, err)
return err
}
// Update Regions
err = s.queries.UpdateUsuarioRegions(ctx, generated.UpdateUsuarioRegionsParams{
ID: pgID,
RegioesPermitidas: regions,
})
if err != nil {
fmt.Printf("[DEBUG] Error updating regions for %s: %v\n", u.Email, err)
return err
}
// Update Role if mismatch
if existingUser.Role != u.Role {
fmt.Printf("[DEBUG] Updating Role from %s to %s for %s\n", existingUser.Role, u.Role, u.Email)
err = s.UpdateUserRole(ctx, uuid.UUID(existingUser.ID.Bytes).String(), u.Role)
if err != nil {
return err
}
}
}
}
return nil
}
func (s *Service) ListUsers(ctx context.Context) ([]generated.ListAllUsuariosRow, error) {
return s.queries.ListAllUsuarios(ctx)
}
func (s *Service) GetUser(ctx context.Context, id string) (*generated.GetUsuarioByIDRow, error) {
parsedUUID, err := uuid.Parse(id)
if err != nil {
return nil, err
}
var pgID pgtype.UUID
pgID.Bytes = parsedUUID
pgID.Valid = true
user, err := s.queries.GetUsuarioByID(ctx, pgID)
if err != nil {
return nil, err
}
return &user, nil
}
func (s *Service) GetProfessionalByUserID(ctx context.Context, userID string) (*generated.GetProfissionalByUsuarioIDRow, error) {
parsedUUID, err := uuid.Parse(userID)
if err != nil {
return nil, err
}
var pgID pgtype.UUID
pgID.Bytes = parsedUUID
pgID.Valid = true
p, err := s.queries.GetProfissionalByUsuarioID(ctx, pgID)
if err != nil {
return nil, err
}
return &p, nil
}
func toPgText(s *string) pgtype.Text {
if s == nil {
return pgtype.Text{Valid: false}
}
return pgtype.Text{String: *s, Valid: true}
}