Skip to main content

Integração de Usuários

Este guia fornece exemplos práticos para integrar sua aplicação com o sistema de usuários do FitLocus.

Visão Geral

A integração com o sistema de usuários do FitLocus inclui:

  • Gerenciamento de perfis de usuário
  • Consulta e atualização de informações pessoais
  • Gerenciamento de preferências e configurações
  • Consulta de métricas e estatísticas
  • Gerenciamento de relacionamentos entre usuários

Requisitos Técnicos

Para integrar com o sistema de usuários do FitLocus, você precisará:
  • Backend:
    • Java 21
    • Spring Boot
    • Spring Data JPA
    • ModelMapper ou similar para mapeamento DTO
  • Frontend:
    • Axios ou fetch para requisições HTTP
    • Biblioteca de gerenciamento de formulários (Formik, React Hook Form, etc.)
    • Biblioteca de validação (Yup, Zod, etc.)

Exemplos de Integração

Backend (Java 21 + Spring Boot)

Modelo de Usuário

@Entity
@Table(name = "users")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class User implements UserDetails {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false)
    private String name;

    @Column(nullable = false, unique = true)
    private String email;

    @Column(nullable = false)
    private String password;

    @Enumerated(EnumType.STRING)
    @Column(name = "user_type", nullable = false)
    private EnumUserType userType;

    @Column(name = "profile_picture")
    private String profilePicture;

    private String phone;

    @Column(name = "birth_date")
    private LocalDate birthDate;

    @Enumerated(EnumType.STRING)
    private Gender gender;

    private Integer height;

    private Double weight;

    @Enumerated(EnumType.STRING)
    @Column(name = "subscription_type")
    private SubscriptionType subscriptionType;

    @Column(name = "subscription_expiration_date")
    private LocalDateTime subscriptionExpirationDate;

    @Column(name = "created_at")
    private LocalDateTime createdAt;

    @Column(name = "updated_at")
    private LocalDateTime updatedAt;

    @PrePersist
    protected void onCreate() {
        createdAt = LocalDateTime.now();
        updatedAt = LocalDateTime.now();
    }

    @PreUpdate
    protected void onUpdate() {
        updatedAt = LocalDateTime.now();
    }

    // Implementação de UserDetails para Spring Security
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return List.of(new SimpleGrantedAuthority("ROLE_" + userType.name()));
    }

    @Override
    public String getUsername() {
        return email;
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }
}

DTO de Usuário

@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class UserDTO {
    private Long id;
    private String name;
    private String email;
    private EnumUserType userType;
    private String profilePicture;
    private String phone;
    private LocalDate birthDate;
    private Gender gender;
    private Integer height;
    private Double weight;
    private SubscriptionType subscriptionType;
    private LocalDateTime subscriptionExpirationDate;
    private LocalDateTime createdAt;
    private LocalDateTime updatedAt;

    public static UserDTO fromEntity(User user) {
        return UserDTO.builder()
                .id(user.getId())
                .name(user.getName())
                .email(user.getEmail())
                .userType(user.getUserType())
                .profilePicture(user.getProfilePicture())
                .phone(user.getPhone())
                .birthDate(user.getBirthDate())
                .gender(user.getGender())
                .height(user.getHeight())
                .weight(user.getWeight())
                .subscriptionType(user.getSubscriptionType())
                .subscriptionExpirationDate(user.getSubscriptionExpirationDate())
                .createdAt(user.getCreatedAt())
                .updatedAt(user.getUpdatedAt())
                .build();
    }
}

Repositório de Usuário

@Repository
public interface UserRepository extends JpaRepository<User, Long> {
    Optional<User> findByEmail(String email);
    
    List<User> findByUserType(EnumUserType userType);
    
    @Query("SELECT u FROM User u JOIN PersonalStudent ps ON u.id = ps.studentId WHERE ps.personalId = :personalId")
    List<User> findStudentsByPersonalId(@Param("personalId") Long personalId);
    
    @Query("SELECT u FROM User u JOIN PersonalStudent ps ON u.id = ps.personalId WHERE ps.studentId = :studentId")
    List<User> findPersonalsByStudentId(@Param("studentId") Long studentId);
}

Serviço de Usuário

@Service
@Transactional
public class UserService {

    private final UserRepository userRepository;
    private final PasswordEncoder passwordEncoder;

    public UserService(UserRepository userRepository, PasswordEncoder passwordEncoder) {
        this.userRepository = userRepository;
        this.passwordEncoder = passwordEncoder;
    }

    public UserDTO getUserById(Long id) {
        User user = userRepository.findById(id)
                .orElseThrow(() -> new ResourceNotFoundException("Usuário não encontrado"));
        return UserDTO.fromEntity(user);
    }

    public UserDTO getUserByEmail(String email) {
        User user = userRepository.findByEmail(email)
                .orElseThrow(() -> new ResourceNotFoundException("Usuário não encontrado"));
        return UserDTO.fromEntity(user);
    }

    public List<UserDTO> getAllUsers() {
        return userRepository.findAll().stream()
                .map(UserDTO::fromEntity)
                .collect(Collectors.toList());
    }

    public List<UserDTO> getUsersByType(EnumUserType userType) {
        return userRepository.findByUserType(userType).stream()
                .map(UserDTO::fromEntity)
                .collect(Collectors.toList());
    }

    public List<UserDTO> getStudentsByPersonalId(Long personalId) {
        return userRepository.findStudentsByPersonalId(personalId).stream()
                .map(UserDTO::fromEntity)
                .collect(Collectors.toList());
    }

    public List<UserDTO> getPersonalsByStudentId(Long studentId) {
        return userRepository.findPersonalsByStudentId(studentId).stream()
                .map(UserDTO::fromEntity)
                .collect(Collectors.toList());
    }

    public UserDTO updateUser(Long id, UserUpdateRequest request) {
        User user = userRepository.findById(id)
                .orElseThrow(() -> new ResourceNotFoundException("Usuário não encontrado"));
        
        if (request.getName() != null) {
            user.setName(request.getName());
        }
        
        if (request.getPhone() != null) {
            user.setPhone(request.getPhone());
        }
        
        if (request.getBirthDate() != null) {
            user.setBirthDate(request.getBirthDate());
        }
        
        if (request.getGender() != null) {
            user.setGender(request.getGender());
        }
        
        if (request.getHeight() != null) {
            user.setHeight(request.getHeight());
        }
        
        if (request.getWeight() != null) {
            user.setWeight(request.getWeight());
        }
        
        User updatedUser = userRepository.save(user);
        return UserDTO.fromEntity(updatedUser);
    }

    public UserDTO updatePassword(Long id, PasswordUpdateRequest request) {
        User user = userRepository.findById(id)
                .orElseThrow(() -> new ResourceNotFoundException("Usuário não encontrado"));
        
        if (!passwordEncoder.matches(request.getCurrentPassword(), user.getPassword())) {
            throw new BadCredentialsException("Senha atual incorreta");
        }
        
        user.setPassword(passwordEncoder.encode(request.getNewPassword()));
        User updatedUser = userRepository.save(user);
        return UserDTO.fromEntity(updatedUser);
    }

    public UserDTO updateProfilePicture(Long id, String profilePictureUrl) {
        User user = userRepository.findById(id)
                .orElseThrow(() -> new ResourceNotFoundException("Usuário não encontrado"));
        
        user.setProfilePicture(profilePictureUrl);
        User updatedUser = userRepository.save(user);
        return UserDTO.fromEntity(updatedUser);
    }

    public void deleteUser(Long id) {
        if (!userRepository.existsById(id)) {
            throw new ResourceNotFoundException("Usuário não encontrado");
        }
        userRepository.deleteById(id);
    }
}

Controlador de Usuário

@RestController
@RequestMapping("/api/users")
public class UserController {

    private final UserService userService;
    private final FileStorageService fileStorageService;

    public UserController(UserService userService, FileStorageService fileStorageService) {
        this.userService = userService;
        this.fileStorageService = fileStorageService;
    }

    @GetMapping("/profile")
    public ResponseEntity<UserDTO> getCurrentUser(Authentication authentication) {
        String email = authentication.getName();
        UserDTO user = userService.getUserByEmail(email);
        return ResponseEntity.ok(user);
    }

    @GetMapping("/{id}")
    public ResponseEntity<UserDTO> getUserById(@PathVariable Long id) {
        UserDTO user = userService.getUserById(id);
        return ResponseEntity.ok(user);
    }

    @PreAuthorize("hasRole('ADMIN')")
    @GetMapping
    public ResponseEntity<List<UserDTO>> getAllUsers() {
        List<UserDTO> users = userService.getAllUsers();
        return ResponseEntity.ok(users);
    }

    @GetMapping("/type/{userType}")
    public ResponseEntity<List<UserDTO>> getUsersByType(@PathVariable EnumUserType userType) {
        List<UserDTO> users = userService.getUsersByType(userType);
        return ResponseEntity.ok(users);
    }

    @PreAuthorize("hasRole('PERSONAL')")
    @GetMapping("/students")
    public ResponseEntity<List<UserDTO>> getStudents(Authentication authentication) {
        User personal = (User) authentication.getPrincipal();
        List<UserDTO> students = userService.getStudentsByPersonalId(personal.getId());
        return ResponseEntity.ok(students);
    }

    @PreAuthorize("hasRole('ALUNO')")
    @GetMapping("/personals")
    public ResponseEntity<List<UserDTO>> getPersonals(Authentication authentication) {
        User student = (User) authentication.getPrincipal();
        List<UserDTO> personals = userService.getPersonalsByStudentId(student.getId());
        return ResponseEntity.ok(personals);
    }

    @PutMapping("/profile")
    public ResponseEntity<UserDTO> updateProfile(
            Authentication authentication,
            @Valid @RequestBody UserUpdateRequest request) {
        User user = (User) authentication.getPrincipal();
        UserDTO updatedUser = userService.updateUser(user.getId(), request);
        return ResponseEntity.ok(updatedUser);
    }

    @PutMapping("/password")
    public ResponseEntity<UserDTO> updatePassword(
            Authentication authentication,
            @Valid @RequestBody PasswordUpdateRequest request) {
        User user = (User) authentication.getPrincipal();
        UserDTO updatedUser = userService.updatePassword(user.getId(), request);
        return ResponseEntity.ok(updatedUser);
    }

    @PostMapping("/profile-picture")
    public ResponseEntity<UserDTO> updateProfilePicture(
            Authentication authentication,
            @RequestParam("file") MultipartFile file) {
        User user = (User) authentication.getPrincipal();
        String fileUrl = fileStorageService.storeFile(file, "profile", user.getId().toString());
        UserDTO updatedUser = userService.updateProfilePicture(user.getId(), fileUrl);
        return ResponseEntity.ok(updatedUser);
    }

    @PreAuthorize("hasRole('ADMIN') or #id == authentication.principal.id")
    @DeleteMapping("/{id}")
    public ResponseEntity<Void> deleteUser(@PathVariable Long id) {
        userService.deleteUser(id);
        return ResponseEntity.noContent().build();
    }
}

Frontend (React + TypeScript)

Serviço de Usuário

// src/services/user.service.ts
import api from './api';

export interface UserUpdateRequest {
  name?: string;
  phone?: string;
  birthDate?: string;
  gender?: 'MASCULINO' | 'FEMININO' | 'OUTRO';
  height?: number;
  weight?: number;
}

export interface PasswordUpdateRequest {
  currentPassword: string;
  newPassword: string;
  confirmPassword: string;
}

export interface User {
  id: number;
  name: string;
  email: string;
  userType: 'ALUNO' | 'PERSONAL';
  profilePicture?: string;
  phone?: string;
  birthDate?: string;
  gender?: 'MASCULINO' | 'FEMININO' | 'OUTRO';
  height?: number;
  weight?: number;
  subscriptionType?: string;
  subscriptionExpirationDate?: string;
  createdAt: string;
  updatedAt: string;
}

class UserService {
  async getCurrentUser(): Promise<User> {
    const response = await api.get<User>('/users/profile');
    return response.data;
  }

  async getUserById(id: number): Promise<User> {
    const response = await api.get<User>(`/users/${id}`);
    return response.data;
  }

  async getStudents(): Promise<User[]> {
    const response = await api.get<User[]>('/users/students');
    return response.data;
  }

  async getPersonals(): Promise<User[]> {
    const response = await api.get<User[]>('/users/personals');
    return response.data;
  }

  async updateProfile(data: UserUpdateRequest): Promise<User> {
    const response = await api.put<User>('/users/profile', data);
    return response.data;
  }

  async updatePassword(data: PasswordUpdateRequest): Promise<User> {
    const response = await api.put<User>('/users/password', data);
    return response.data;
  }

  async updateProfilePicture(file: File): Promise<User> {
    const formData = new FormData();
    formData.append('file', file);
    
    const response = await api.post<User>('/users/profile-picture', formData, {
      headers: {
        'Content-Type': 'multipart/form-data',
      },
    });
    
    return response.data;
  }

  async deleteUser(id: number): Promise<void> {
    await api.delete(`/users/${id}`);
  }
}

export default new UserService();

Hook de Usuário

// src/hooks/useUser.ts
import { useState, useEffect, useCallback } from 'react';
import userService, { User, UserUpdateRequest, PasswordUpdateRequest } from '../services/user.service';
import { useAuth } from '../contexts/AuthContext';

export const useUser = () => {
  const [user, setUser] = useState<User | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);
  const { isAuthenticated } = useAuth();

  const fetchUser = useCallback(async () => {
    if (!isAuthenticated) {
      setUser(null);
      setLoading(false);
      return;
    }

    setLoading(true);
    setError(null);

    try {
      const userData = await userService.getCurrentUser();
      setUser(userData);
    } catch (err: any) {
      setError(err.response?.data?.message || 'Erro ao carregar usuário');
      console.error(err);
    } finally {
      setLoading(false);
    }
  }, [isAuthenticated]);

  useEffect(() => {
    fetchUser();
  }, [fetchUser]);

  const updateProfile = async (data: UserUpdateRequest) => {
    setLoading(true);
    setError(null);

    try {
      const updatedUser = await userService.updateProfile(data);
      setUser(updatedUser);
      return updatedUser;
    } catch (err: any) {
      setError(err.response?.data?.message || 'Erro ao atualizar perfil');
      throw err;
    } finally {
      setLoading(false);
    }
  };

  const updatePassword = async (data: PasswordUpdateRequest) => {
    setLoading(true);
    setError(null);

    try {
      const updatedUser = await userService.updatePassword(data);
      setUser(updatedUser);
      return updatedUser;
    } catch (err: any) {
      setError(err.response?.data?.message || 'Erro ao atualizar senha');
      throw err;
    } finally {
      setLoading(false);
    }
  };

  const updateProfilePicture = async (file: File) => {
    setLoading(true);
    setError(null);

    try {
      const updatedUser = await userService.updateProfilePicture(file);
      setUser(updatedUser);
      return updatedUser;
    } catch (err: any) {
      setError(err.response?.data?.message || 'Erro ao atualizar foto de perfil');
      throw err;
    } finally {
      setLoading(false);
    }
  };

  return {
    user,
    loading,
    error,
    fetchUser,
    updateProfile,
    updatePassword,
    updateProfilePicture,
  };
};

Componente de Perfil

// src/components/ProfileForm.tsx
import React, { useState } from 'react';
import { useUser } from '../hooks/useUser';
import { UserUpdateRequest } from '../services/user.service';

const ProfileForm: React.FC = () => {
  const { user, loading, error, updateProfile } = useUser();
  const [formData, setFormData] = useState<UserUpdateRequest>({
    name: user?.name || '',
    phone: user?.phone || '',
    birthDate: user?.birthDate || '',
    gender: user?.gender || undefined,
    height: user?.height || undefined,
    weight: user?.weight || undefined,
  });
  const [success, setSuccess] = useState(false);

  const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
    const { name, value, type } = e.target as HTMLInputElement;
    
    setFormData({
      ...formData,
      [name]: type === 'number' ? (value ? Number(value) : undefined) : value,
    });
  };

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    setSuccess(false);
    
    try {
      await updateProfile(formData);
      setSuccess(true);
    } catch (err) {
      console.error(err);
    }
  };

  if (loading && !user) {
    return <div>Carregando...</div>;
  }

  return (
    <div className="profile-form">
      <h2>Meu Perfil</h2>
      
      {error && <div className="error-message">{error}</div>}
      {success && <div className="success-message">Perfil atualizado com sucesso!</div>}
      
      <form onSubmit={handleSubmit}>
        <div className="form-group">
          <label htmlFor="name">Nome</label>
          <input
            type="text"
            id="name"
            name="name"
            value={formData.name}
            onChange={handleChange}
            required
          />
        </div>
        
        <div className="form-group">
          <label htmlFor="phone">Telefone</label>
          <input
            type="tel"
            id="phone"
            name="phone"
            value={formData.phone || ''}
            onChange={handleChange}
          />
        </div>
        
        <div className="form-group">
          <label htmlFor="birthDate">Data de Nascimento</label>
          <input
            type="date"
            id="birthDate"
            name="birthDate"
            value={formData.birthDate || ''}
            onChange={handleChange}
          />
        </div>
        
        <div className="form-group">
          <label htmlFor="gender">Gênero</label>
          <select
            id="gender"
            name="gender"
            value={formData.gender || ''}
            onChange={handleChange}
          >
            <option value="">Selecione</option>
            <option value="MASCULINO">Masculino</option>
            <option value="FEMININO">Feminino</option>
            <option value="OUTRO">Outro</option>
          </select>
        </div>
        
        <div className="form-group">
          <label htmlFor="height">Altura (cm)</label>
          <input
            type="number"
            id="height"
            name="height"
            value={formData.height || ''}
            onChange={handleChange}
          />
        </div>
        
        <div className="form-group">
          <label htmlFor="weight">Peso (kg)</label>
          <input
            type="number"
            id="weight"
            name="weight"
            step="0.1"
            value={formData.weight || ''}
            onChange={handleChange}
          />
        </div>
        
        <button type="submit" disabled={loading}>
          {loading ? 'Salvando...' : 'Salvar'}
        </button>
      </form>
    </div>
  );
};

export default ProfileForm;

Componente de Atualização de Senha

// src/components/PasswordForm.tsx
import React, { useState } from 'react';
import { useUser } from '../hooks/useUser';
import { PasswordUpdateRequest } from '../services/user.service';

const PasswordForm: React.FC = () => {
  const { loading, error, updatePassword } = useUser();
  const [formData, setFormData] = useState<PasswordUpdateRequest>({
    currentPassword: '',
    newPassword: '',
    confirmPassword: '',
  });
  const [success, setSuccess] = useState(false);
  const [validationError, setValidationError] = useState<string | null>(null);

  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    const { name, value } = e.target;
    
    setFormData({
      ...formData,
      [name]: value,
    });
  };

  const validateForm = () => {
    if (formData.newPassword.length < 8) {
      setValidationError('A nova senha deve ter pelo menos 8 caracteres');
      return false;
    }
    
    if (formData.newPassword !== formData.confirmPassword) {
      setValidationError('As senhas não coincidem');
      return false;
    }
    
    setValidationError(null);
    return true;
  };

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    setSuccess(false);
    
    if (!validateForm()) {
      return;
    }
    
    try {
      await updatePassword(formData);
      setSuccess(true);
      setFormData({
        currentPassword: '',
        newPassword: '',
        confirmPassword: '',
      });
    } catch (err) {
      console.error(err);
    }
  };

  return (
    <div className="password-form">
      <h2>Alterar Senha</h2>
      
      {error && <div className="error-message">{error}</div>}
      {validationError && <div className="error-message">{validationError}</div>}
      {success && <div className="success-message">Senha atualizada com sucesso!</div>}
      
      <form onSubmit={handleSubmit}>
        <div className="form-group">
          <label htmlFor="currentPassword">Senha Atual</label>
          <input
            type="password"
            id="currentPassword"
            name="currentPassword"
            value={formData.currentPassword}
            onChange={handleChange}
            required
          />
        </div>
        
        <div className="form-group">
          <label htmlFor="newPassword">Nova Senha</label>
          <input
            type="password"
            id="newPassword"
            name="newPassword"
            value={formData.newPassword}
            onChange={handleChange}
            required
          />
        </div>
        
        <div className="form-group">
          <label htmlFor="confirmPassword">Confirmar Nova Senha</label>
          <input
            type="password"
            id="confirmPassword"
            name="confirmPassword"
            value={formData.confirmPassword}
            onChange={handleChange}
            required
          />
        </div>
        
        <button type="submit" disabled={loading}>
          {loading ? 'Salvando...' : 'Alterar Senha'}
        </button>
      </form>
    </div>
  );
};

export default PasswordForm;

Componente de Upload de Foto de Perfil

// src/components/ProfilePictureUpload.tsx
import React, { useState, useRef } from 'react';
import { useUser } from '../hooks/useUser';

const ProfilePictureUpload: React.FC = () => {
  const { user, loading, error, updateProfilePicture } = useUser();
  const [preview, setPreview] = useState<string | null>(null);
  const [success, setSuccess] = useState(false);
  const fileInputRef = useRef<HTMLInputElement>(null);

  const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    const file = e.target.files?.[0];
    if (!file) return;
    
    // Validar tipo de arquivo
    if (!file.type.match('image.*')) {
      alert('Por favor, selecione uma imagem válida');
      return;
    }
    
    // Criar preview
    const reader = new FileReader();
    reader.onload = () => {
      setPreview(reader.result as string);
    };
    reader.readAsDataURL(file);
  };

  const handleUpload = async () => {
    if (!fileInputRef.current?.files?.[0]) return;
    
    setSuccess(false);
    
    try {
      await updateProfilePicture(fileInputRef.current.files[0]);
      setSuccess(true);
      setPreview(null);
      
      // Limpar input
      if (fileInputRef.current) {
        fileInputRef.current.value = '';
      }
    } catch (err) {
      console.error(err);
    }
  };

  const handleSelectClick = () => {
    fileInputRef.current?.click();
  };

  return (
    <div className="profile-picture-upload">
      <h2>Foto de Perfil</h2>
      
      {error && <div className="error-message">{error}</div>}
      {success && <div className="success-message">Foto atualizada com sucesso!</div>}
      
      <div className="profile-picture-container">
        <div className="profile-picture">
          {preview ? (
            <img src={preview} alt="Preview" />
          ) : user?.profilePicture ? (
            <img src={user.profilePicture} alt={user.name} />
          ) : (
            <div className="profile-picture-placeholder">
              {user?.name?.charAt(0) || '?'}
            </div>
          )}
        </div>
        
        <div className="profile-picture-actions">
          <input
            type="file"
            ref={fileInputRef}
            onChange={handleFileChange}
            accept="image/*"
            style={{ display: 'none' }}
          />
          
          <button type="button" onClick={handleSelectClick}>
            Selecionar Foto
          </button>
          
          {preview && (
            <button 
              type="button" 
              onClick={handleUpload} 
              disabled={loading}
              className="upload-button"
            >
              {loading ? 'Enviando...' : 'Enviar Foto'}
            </button>
          )}
        </div>
      </div>
    </div>
  );
};

export default ProfilePictureUpload;

Página de Perfil Completa

// src/pages/ProfilePage.tsx
import React from 'react';
import { useUser } from '../hooks/useUser';
import ProfilePictureUpload from '../components/ProfilePictureUpload';
import ProfileForm from '../components/ProfileForm';
import PasswordForm from '../components/PasswordForm';

const ProfilePage: React.FC = () => {
  const { user, loading } = useUser();

  if (loading && !user) {
    return <div className="loading-container">Carregando...</div>;
  }

  return (
    <div className="profile-page">
      <h1>Meu Perfil</h1>
      
      <div className="profile-sections">
        <div className="profile-section">
          <ProfilePictureUpload />
        </div>
        
        <div className="profile-section">
          <ProfileForm />
        </div>
        
        <div className="profile-section">
          <PasswordForm />
        </div>
        
        {user?.subscriptionType && (
          <div className="profile-section">
            <h2>Assinatura</h2>
            <div className="subscription-info">
              <p><strong>Plano:</strong> {user.subscriptionType}</p>
              <p>
                <strong>Validade:</strong> {' '}
                {user.subscriptionExpirationDate 
                  ? new Date(user.subscriptionExpirationDate).toLocaleDateString('pt-BR')
                  : 'N/A'}
              </p>
            </div>
          </div>
        )}
      </div>
    </div>
  );
};

export default ProfilePage;

Listagem de Usuários (Personal Trainers e Alunos)

Componente de Lista de Alunos (para Personal Trainers)

// src/components/StudentsList.tsx
import React, { useState, useEffect } from 'react';
import { Link } from 'react-router-dom';
import userService, { User } from '../services/user.service';

const StudentsList: React.FC = () => {
  const [students, setStudents] = useState<User[]>([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    const fetchStudents = async () => {
      setLoading(true);
      setError(null);
      
      try {
        const data = await userService.getStudents();
        setStudents(data);
      } catch (err: any) {
        setError(err.response?.data?.message || 'Erro ao carregar alunos');
        console.error(err);
      } finally {
        setLoading(false);
      }
    };
    
    fetchStudents();
  }, []);

  if (loading) {
    return <div>Carregando...</div>;
  }

  if (error) {
    return <div className="error-message">{error}</div>;
  }

  if (students.length === 0) {
    return (
      <div className="empty-state">
        <p>Você ainda não tem alunos.</p>
        <Link to="/invite-student" className="button">Convidar Aluno</Link>
      </div>
    );
  }

  return (
    <div className="students-list">
      <h2>Meus Alunos</h2>
      
      <div className="students-grid">
        {students.map(student => (
          <div key={student.id} className="student-card">
            <div className="student-avatar">
              {student.profilePicture ? (
                <img src={student.profilePicture} alt={student.name} />
              ) : (
                <div className="avatar-placeholder">{student.name.charAt(0)}</div>
              )}
            </div>
            
            <div className="student-info">
              <h3>{student.name}</h3>
              <p>{student.email}</p>
              
              {student.subscriptionType && (
                <p className="subscription-badge">
                  {student.subscriptionType}
                </p>
              )}
            </div>
            
            <div className="student-actions">
              <Link to={`/students/${student.id}`} className="button">
                Ver Detalhes
              </Link>
              <Link to={`/students/${student.id}/trainings`} className="button">
                Gerenciar Treinos
              </Link>
            </div>
          </div>
        ))}
      </div>
    </div>
  );
};

export default StudentsList;

Componente de Detalhes do Aluno

// src/components/StudentDetails.tsx
import React, { useState, useEffect } from 'react';
import { useParams, Link } from 'react-router-dom';
import userService, { User } from '../services/user.service';
import trainingService from '../services/training.service';

interface StudentDetailsParams {
  id: string;
}

const StudentDetails: React.FC = () => {
  const { id } = useParams<StudentDetailsParams>();
  const [student, setStudent] = useState<User | null>(null);
  const [trainingCount, setTrainingCount] = useState(0);
  const [executionCount, setExecutionCount] = useState(0);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    const fetchStudentData = async () => {
      setLoading(true);
      setError(null);
      
      try {
        const studentId = parseInt(id || '0', 10);
        
        // Carregar dados do aluno
        const studentData = await userService.getUserById(studentId);
        setStudent(studentData);
        
        // Carregar contagem de treinos
        const trainings = await trainingService.getTrainingsByStudentId(studentId);
        setTrainingCount(trainings.length);
        
        // Carregar contagem de execuções
        const executions = await trainingService.getExecutionsByStudentId(studentId);
        setExecutionCount(executions.length);
      } catch (err: any) {
        setError(err.response?.data?.message || 'Erro ao carregar dados do aluno');
        console.error(err);
      } finally {
        setLoading(false);
      }
    };
    
    fetchStudentData();
  }, [id]);

  if (loading) {
    return <div>Carregando...</div>;
  }

  if (error || !student) {
    return <div className="error-message">{error || 'Aluno não encontrado'}</div>;
  }

  return (
    <div className="student-details">
      <div className="student-header">
        <div className="student-avatar">
          {student.profilePicture ? (
            <img src={student.profilePicture} alt={student.name} />
          ) : (
            <div className="avatar-placeholder">{student.name.charAt(0)}</div>
          )}
        </div>
        
        <div className="student-header-info">
          <h1>{student.name}</h1>
          <p>{student.email}</p>
          
          {student.subscriptionType && (
            <p className="subscription-badge">
              {student.subscriptionType}
            </p>
          )}
        </div>
      </div>
      
      <div className="student-stats">
        <div className="stat-card">
          <h3>Treinos</h3>
          <p className="stat-value">{trainingCount}</p>
        </div>
        
        <div className="stat-card">
          <h3>Execuções</h3>
          <p className="stat-value">{executionCount}</p>
        </div>
        
        <div className="stat-card">
          <h3>Desde</h3>
          <p className="stat-value">
            {new Date(student.createdAt).toLocaleDateString('pt-BR')}
          </p>
        </div>
      </div>
      
      <div className="student-details-section">
        <h2>Informações Pessoais</h2>
        
        <div className="details-grid">
          <div className="detail-item">
            <h4>Telefone</h4>
            <p>{student.phone || 'Não informado'}</p>
          </div>
          
          <div className="detail-item">
            <h4>Data de Nascimento</h4>
            <p>
              {student.birthDate 
                ? new Date(student.birthDate).toLocaleDateString('pt-BR')
                : 'Não informada'}
            </p>
          </div>
          
          <div className="detail-item">
            <h4>Gênero</h4>
            <p>{student.gender || 'Não informado'}</p>
          </div>
          
          <div className="detail-item">
            <h4>Altura</h4>
            <p>{student.height ? `${student.height} cm` : 'Não informada'}</p>
          </div>
          
          <div className="detail-item">
            <h4>Peso</h4>
            <p>{student.weight ? `${student.weight} kg` : 'Não informado'}</p>
          </div>
        </div>
      </div>
      
      <div className="student-actions">
        <Link to={`/students/${student.id}/trainings`} className="button primary">
          Gerenciar Treinos
        </Link>
        <Link to={`/students/${student.id}/plans`} className="button">
          Planos de Treino
        </Link>
        <Link to={`/students/${student.id}/metrics`} className="button">
          Métricas
        </Link>
      </div>
    </div>
  );
};

export default StudentDetails;

Melhores Práticas

Segurança

  1. Validação de Dados:
    • Valide todos os dados de entrada no backend e frontend
    • Utilize bibliotecas como Yup, Zod ou Bean Validation
  2. Controle de Acesso:
    • Implemente verificações de autorização em todos os endpoints
    • Utilize anotações como @PreAuthorize no Spring Security
  3. Proteção de Dados Sensíveis:
    • Nunca retorne senhas ou dados sensíveis nas respostas da API
    • Utilize DTOs para controlar quais campos são expostos

Performance

  1. Paginação:
    • Implemente paginação para listas grandes de usuários
    • Utilize Page<T> do Spring Data para paginação no backend
  2. Caching:
    • Implemente cache para dados que não mudam frequentemente
    • Utilize Redis ou cache em memória para melhorar a performance

Experiência do Usuário

  1. Feedback de Carregamento:
    • Exiba indicadores de carregamento durante operações assíncronas
    • Forneça mensagens de erro claras e acionáveis
  2. Validação em Tempo Real:
    • Valide formulários em tempo real para melhorar a experiência do usuário
    • Forneça feedback imediato sobre erros de validação

Solução de Problemas

Erros Comuns

ErroCausa ProvávelSolução
404 Not FoundUsuário não encontradoVerificar ID do usuário e existência no banco de dados
403 ForbiddenPermissões insuficientesVerificar tipo de usuário e permissões
400 Bad RequestDados inválidosVerificar validação de dados no frontend e backend
409 ConflictEmail já registradoVerificar unicidade de email antes de registrar

Depuração

Para depurar problemas com a API de usuários:
  1. Verifique logs do servidor para erros específicos
  2. Inspecione requisições de rede no DevTools
  3. Verifique validação de dados no frontend e backend
  4. Teste endpoints com Postman ou Insomnia

Recursos Adicionais