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" "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, cpfCnpj, cep, endereco, numero, complemento, bairro, cidade, estado 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), Ativo: false, }) 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 { var empID pgtype.UUID if empresaID != nil && *empresaID != "" { parsedEmpID, err := uuid.Parse(*empresaID) if err == nil { empID = pgtype.UUID{Bytes: parsedEmpID, Valid: true} } } _, err = s.queries.CreateCadastroCliente(ctx, generated.CreateCadastroClienteParams{ UsuarioID: pgtype.UUID{Bytes: user.ID.Bytes, Valid: true}, EmpresaID: empID, Nome: pgtype.Text{String: nome, Valid: true}, Telefone: pgtype.Text{String: telefone, Valid: telefone != ""}, CpfCnpj: toPgText(&cpfCnpj), Cep: toPgText(&cep), Endereco: toPgText(&endereco), Numero: toPgText(&numero), Complemento: toPgText(&complemento), Bairro: toPgText(&bairro), Cidade: toPgText(&cidade), Estado: toPgText(&estado), }) 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, empresaID string, telefone, cpfCnpj, cep, endereco, numero, complemento, bairro, cidade, estado 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 } // Update Regions if provided if regiao != "" { err = s.queries.UpdateUsuarioRegions(ctx, generated.UpdateUsuarioRegionsParams{ ID: pgtype.UUID{Bytes: user.ID.Bytes, Valid: true}, RegioesPermitidas: []string{regiao}, }) if err != nil { _ = s.queries.DeleteUsuario(ctx, user.ID) return nil, err } } if ativo { // Approve user immediately err = s.ApproveUser(ctx, uuid.UUID(user.ID.Bytes).String()) if err != nil { _ = s.queries.DeleteUsuario(ctx, user.ID) return nil, err } user.Ativo = true } // Link Company if EVENT_OWNER if role == RoleEventOwner && empresaID != "" { empUuid, err := uuid.Parse(empresaID) if err == nil { _, err = s.queries.CreateCadastroCliente(ctx, generated.CreateCadastroClienteParams{ UsuarioID: pgtype.UUID{Bytes: user.ID.Bytes, Valid: true}, EmpresaID: pgtype.UUID{Bytes: empUuid, Valid: true}, Nome: pgtype.Text{String: nome, Valid: true}, Telefone: toPgText(&telefone), CpfCnpj: toPgText(&cpfCnpj), Cep: toPgText(&cep), Endereco: toPgText(&endereco), Numero: toPgText(&numero), Complemento: toPgText(&complemento), Bairro: toPgText(&bairro), Cidade: toPgText(&cidade), Estado: toPgText(&estado), }) if err != nil { // Log error but maybe don't fail user creation? // Ideally rollback _ = s.queries.DeleteUsuario(ctx, user.ID) return nil, err } } } // 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, } // Create the professional _, err = s.profissionaisService.Create(ctx, uuid.UUID(user.ID.Bytes).String(), profInput, regiao) if err != nil { // 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) // Create if not exists if errors.Is(err, pgx.ErrNoRows) { // Only create if user not found fmt.Printf("Creating demo user: %s (%s)\n", u.Email, u.Role) // Pass empty strings for new client fields for demo users user, err := s.AdminCreateUser(ctx, u.Email, "123456", u.Role, u.Name, "", true, regions[0], "", "", "", "", "", "", "", "", "", "") if err != nil { fmt.Printf("Error creating demo 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 { // If it's another error, return it 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 } type UpdateClientInput struct { Nome string Telefone string CpfCnpj string Cep string Endereco string Numero string Complemento string Bairro string Cidade string Estado string } func (s *Service) UpdateClientData(ctx context.Context, userID string, input UpdateClientInput) error { uid, err := uuid.Parse(userID) if err != nil { return err } // Prepare params params := generated.UpdateCadastroClienteParams{ UsuarioID: pgtype.UUID{Bytes: uid, Valid: true}, Nome: pgtype.Text{String: input.Nome, Valid: input.Nome != ""}, Telefone: pgtype.Text{String: input.Telefone, Valid: input.Telefone != ""}, CpfCnpj: pgtype.Text{String: input.CpfCnpj, Valid: input.CpfCnpj != ""}, Cep: pgtype.Text{String: input.Cep, Valid: input.Cep != ""}, Endereco: pgtype.Text{String: input.Endereco, Valid: input.Endereco != ""}, Numero: pgtype.Text{String: input.Numero, Valid: input.Numero != ""}, Complemento: pgtype.Text{String: input.Complemento, Valid: input.Complemento != ""}, Bairro: pgtype.Text{String: input.Bairro, Valid: input.Bairro != ""}, Cidade: pgtype.Text{String: input.Cidade, Valid: input.Cidade != ""}, Estado: pgtype.Text{String: input.Estado, Valid: input.Estado != ""}, } _, err = s.queries.UpdateCadastroCliente(ctx, params) return err } func (s *Service) ListUsers(ctx context.Context, regiao string) ([]generated.ListAllUsuariosRow, error) { return s.queries.ListAllUsuarios(ctx, regiao) } 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 (s *Service) GetClientData(ctx context.Context, userID string) (*generated.CadastroCliente, error) { parsedUUID, err := uuid.Parse(userID) if err != nil { return nil, err } var pgID pgtype.UUID pgID.Bytes = parsedUUID pgID.Valid = true c, err := s.queries.GetCadastroClienteByUsuarioID(ctx, pgID) if err != nil { return nil, err } return &c, nil } func toPgText(s *string) pgtype.Text { if s == nil { return pgtype.Text{Valid: false} } return pgtype.Text{String: *s, Valid: true} }