MVP done
This commit is contained in:
parent
c1729a629d
commit
ac53f04e0a
27 changed files with 3019 additions and 88 deletions
|
|
@ -0,0 +1,81 @@
|
|||
package org.operaton.fitpub.config;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.operaton.fitpub.model.entity.User;
|
||||
import org.operaton.fitpub.repository.UserRepository;
|
||||
import org.springframework.boot.CommandLineRunner;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Profile;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
|
||||
import java.security.KeyPair;
|
||||
import java.security.KeyPairGenerator;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.Base64;
|
||||
|
||||
/**
|
||||
* Development data initializer that creates a demo user for testing.
|
||||
* Only active when the 'dev' profile is enabled.
|
||||
*/
|
||||
@Configuration
|
||||
@Profile("dev")
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class DevDataInitializer {
|
||||
|
||||
private final UserRepository userRepository;
|
||||
private final PasswordEncoder passwordEncoder;
|
||||
|
||||
@Bean
|
||||
public CommandLineRunner initDemoUser() {
|
||||
return args -> {
|
||||
// Check if demo user already exists
|
||||
if (userRepository.findByUsername("demo").isPresent()) {
|
||||
log.info("Demo user already exists, skipping initialization");
|
||||
return;
|
||||
}
|
||||
|
||||
log.info("Creating demo user for development...");
|
||||
|
||||
try {
|
||||
// Generate RSA key pair for ActivityPub
|
||||
KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA");
|
||||
keyGen.initialize(2048);
|
||||
KeyPair keyPair = keyGen.generateKeyPair();
|
||||
|
||||
String publicKey = "-----BEGIN PUBLIC KEY-----\n" +
|
||||
Base64.getEncoder().encodeToString(keyPair.getPublic().getEncoded()) +
|
||||
"\n-----END PUBLIC KEY-----";
|
||||
|
||||
String privateKey = "-----BEGIN PRIVATE KEY-----\n" +
|
||||
Base64.getEncoder().encodeToString(keyPair.getPrivate().getEncoded()) +
|
||||
"\n-----END PRIVATE KEY-----";
|
||||
|
||||
// Create demo user
|
||||
User demoUser = User.builder()
|
||||
.username("demo")
|
||||
.email("demo@fitpub.local")
|
||||
.passwordHash(passwordEncoder.encode("demo"))
|
||||
.displayName("Demo User")
|
||||
.bio("This is a demo account for testing FitPub features. Upload your FIT files and explore the federated fitness tracking platform!")
|
||||
.publicKey(publicKey)
|
||||
.privateKey(privateKey)
|
||||
.build();
|
||||
|
||||
userRepository.save(demoUser);
|
||||
|
||||
log.info("=".repeat(80));
|
||||
log.info("Demo user created successfully!");
|
||||
log.info("Username: demo");
|
||||
log.info("Password: demo");
|
||||
log.info("Email: demo@fitpub.local");
|
||||
log.info("=".repeat(80));
|
||||
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
log.error("Failed to generate RSA key pair for demo user", e);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -56,7 +56,8 @@ public class SecurityConfig {
|
|||
.requestMatchers("/error").permitAll()
|
||||
|
||||
// Public endpoints - Web UI pages
|
||||
.requestMatchers("/", "/login", "/register", "/timeline", "/activities", "/activities/**").permitAll()
|
||||
.requestMatchers("/", "/login", "/register", "/timeline", "/timeline/**", "/activities", "/activities/**").permitAll()
|
||||
.requestMatchers("/profile", "/profile/**", "/settings").permitAll() // Auth checked client-side
|
||||
|
||||
// Public endpoints - ActivityPub federation
|
||||
.requestMatchers("/.well-known/**").permitAll()
|
||||
|
|
@ -70,14 +71,23 @@ public class SecurityConfig {
|
|||
// Public endpoints - Timeline API (read-only)
|
||||
.requestMatchers(HttpMethod.GET, "/api/timeline/public").permitAll()
|
||||
|
||||
// Public endpoints - Activity track data (for public activities)
|
||||
.requestMatchers(HttpMethod.GET, "/api/activities/*/track").permitAll()
|
||||
|
||||
// Public endpoints - User's public activities
|
||||
.requestMatchers(HttpMethod.GET, "/api/activities/user/*").permitAll()
|
||||
|
||||
// Protected endpoints - Activities API
|
||||
.requestMatchers("/api/activities/**").authenticated()
|
||||
|
||||
// Protected endpoints - Timeline API (user-specific)
|
||||
.requestMatchers("/api/timeline/**").authenticated()
|
||||
|
||||
// Protected web pages
|
||||
.requestMatchers("/profile", "/settings").authenticated()
|
||||
// User API endpoints
|
||||
.requestMatchers(HttpMethod.GET, "/api/users/me").authenticated()
|
||||
.requestMatchers(HttpMethod.PUT, "/api/users/me").authenticated()
|
||||
.requestMatchers(HttpMethod.GET, "/api/users/{username}").permitAll()
|
||||
.requestMatchers(HttpMethod.GET, "/api/users/id/*").permitAll()
|
||||
|
||||
// All other requests require authentication
|
||||
.anyRequest().authenticated()
|
||||
|
|
|
|||
|
|
@ -102,25 +102,31 @@ public class ActivityController {
|
|||
}
|
||||
|
||||
/**
|
||||
* Lists all activities for the authenticated user.
|
||||
* Lists all activities for the authenticated user with pagination.
|
||||
*
|
||||
* @param userDetails the authenticated user
|
||||
* @return list of activities
|
||||
* @param page page number (default: 0)
|
||||
* @param size page size (default: 10)
|
||||
* @return page of activities
|
||||
*/
|
||||
@GetMapping
|
||||
public ResponseEntity<List<ActivityDTO>> getUserActivities(
|
||||
@AuthenticationPrincipal UserDetails userDetails
|
||||
public ResponseEntity<?> getUserActivities(
|
||||
@AuthenticationPrincipal UserDetails userDetails,
|
||||
@RequestParam(defaultValue = "0") int page,
|
||||
@RequestParam(defaultValue = "10") int size
|
||||
) {
|
||||
log.info("User {} retrieving activities", userDetails.getUsername());
|
||||
log.info("User {} retrieving activities (page: {}, size: {})", userDetails.getUsername(), page, size);
|
||||
|
||||
UUID userId = getUserId(userDetails);
|
||||
|
||||
List<Activity> activities = fitFileService.getUserActivities(userId);
|
||||
List<ActivityDTO> dtos = activities.stream()
|
||||
.map(ActivityDTO::fromEntity)
|
||||
.collect(Collectors.toList());
|
||||
org.springframework.data.domain.Page<Activity> activityPage =
|
||||
fitFileService.getUserActivitiesPaginated(userId, page, size);
|
||||
|
||||
return ResponseEntity.ok(dtos);
|
||||
// Convert to DTOs
|
||||
org.springframework.data.domain.Page<ActivityDTO> dtoPage = activityPage.map(ActivityDTO::fromEntity);
|
||||
|
||||
// Return Spring Page object with all pagination metadata
|
||||
return ResponseEntity.ok(dtoPage);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -180,4 +186,108 @@ public class ActivityController {
|
|||
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Lists public activities for a specific user by username.
|
||||
*
|
||||
* @param username the username
|
||||
* @param page page number (default: 0)
|
||||
* @param size page size (default: 10)
|
||||
* @return page of public activities
|
||||
*/
|
||||
@GetMapping("/user/{username}")
|
||||
public ResponseEntity<?> getUserPublicActivities(
|
||||
@PathVariable String username,
|
||||
@RequestParam(defaultValue = "0") int page,
|
||||
@RequestParam(defaultValue = "10") int size
|
||||
) {
|
||||
log.debug("Retrieving public activities for user: {}", username);
|
||||
|
||||
// Get user by username
|
||||
User user = userRepository.findByUsername(username)
|
||||
.orElseThrow(() -> new UsernameNotFoundException("User not found: " + username));
|
||||
|
||||
// Get public activities only
|
||||
org.springframework.data.domain.Pageable pageable =
|
||||
org.springframework.data.domain.PageRequest.of(page, size,
|
||||
org.springframework.data.domain.Sort.by("startedAt").descending());
|
||||
|
||||
org.springframework.data.domain.Page<Activity> activityPage =
|
||||
fitFileService.getPublicActivitiesByUserId(user.getId(), pageable);
|
||||
|
||||
// Convert to DTOs
|
||||
org.springframework.data.domain.Page<ActivityDTO> dtoPage = activityPage.map(ActivityDTO::fromEntity);
|
||||
|
||||
return ResponseEntity.ok(dtoPage);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the GPS track data for an activity in GeoJSON format.
|
||||
* Public activities can be accessed without authentication.
|
||||
* Private/followers activities require authentication and proper access.
|
||||
*
|
||||
* @param id the activity ID
|
||||
* @param userDetails the authenticated user (optional for public activities)
|
||||
* @return GeoJSON FeatureCollection with track data
|
||||
*/
|
||||
@GetMapping("/{id}/track")
|
||||
public ResponseEntity<?> getActivityTrack(
|
||||
@PathVariable UUID id,
|
||||
@AuthenticationPrincipal UserDetails userDetails
|
||||
) {
|
||||
log.debug("Retrieving track data for activity {}", id);
|
||||
|
||||
// First try to get the activity regardless of user
|
||||
Activity activity = fitFileService.getActivityById(id);
|
||||
if (activity == null) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
|
||||
// Check visibility and access permissions
|
||||
if (activity.getVisibility() != Activity.Visibility.PUBLIC) {
|
||||
// Non-public activities require authentication
|
||||
if (userDetails == null) {
|
||||
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
|
||||
}
|
||||
|
||||
UUID userId = getUserId(userDetails);
|
||||
|
||||
// Check if user owns the activity
|
||||
if (!activity.getUserId().equals(userId)) {
|
||||
// TODO: Check if user is following the activity owner (for FOLLOWERS visibility)
|
||||
return ResponseEntity.status(HttpStatus.FORBIDDEN).build();
|
||||
}
|
||||
}
|
||||
|
||||
// Build GeoJSON FeatureCollection
|
||||
ActivityDTO dto = ActivityDTO.fromEntity(activity);
|
||||
|
||||
if (dto.getSimplifiedTrack() == null) {
|
||||
// Return empty FeatureCollection if no track data
|
||||
return ResponseEntity.ok(java.util.Map.of(
|
||||
"type", "FeatureCollection",
|
||||
"features", java.util.List.of()
|
||||
));
|
||||
}
|
||||
|
||||
// Create GeoJSON Feature with the track
|
||||
java.util.Map<String, Object> feature = new java.util.LinkedHashMap<>();
|
||||
feature.put("type", "Feature");
|
||||
feature.put("geometry", dto.getSimplifiedTrack());
|
||||
|
||||
// Add properties
|
||||
java.util.Map<String, Object> properties = new java.util.LinkedHashMap<>();
|
||||
properties.put("title", activity.getTitle());
|
||||
properties.put("activityType", activity.getActivityType().name());
|
||||
properties.put("distance", activity.getTotalDistance());
|
||||
properties.put("duration", activity.getTotalDurationSeconds());
|
||||
feature.put("properties", properties);
|
||||
|
||||
// Create FeatureCollection
|
||||
java.util.Map<String, Object> geoJson = new java.util.LinkedHashMap<>();
|
||||
geoJson.put("type", "FeatureCollection");
|
||||
geoJson.put("features", java.util.List.of(feature));
|
||||
|
||||
return ResponseEntity.ok(geoJson);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,67 @@
|
|||
package org.operaton.fitpub.controller;
|
||||
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
|
||||
/**
|
||||
* Controller for user profile view pages.
|
||||
*/
|
||||
@Controller
|
||||
public class ProfileViewController {
|
||||
|
||||
/**
|
||||
* Current user's profile page.
|
||||
* Shows own profile with edit capabilities.
|
||||
*
|
||||
* @param model the model
|
||||
* @return profile template
|
||||
*/
|
||||
@GetMapping("/profile")
|
||||
public String myProfile(Model model) {
|
||||
model.addAttribute("pageTitle", "My Profile");
|
||||
return "profile/view";
|
||||
}
|
||||
|
||||
/**
|
||||
* Profile edit page.
|
||||
* Allows user to edit their profile information.
|
||||
*
|
||||
* @param model the model
|
||||
* @return profile edit template
|
||||
*/
|
||||
@GetMapping("/profile/edit")
|
||||
public String editProfile(Model model) {
|
||||
model.addAttribute("pageTitle", "Edit Profile");
|
||||
return "profile/edit";
|
||||
}
|
||||
|
||||
/**
|
||||
* Settings page.
|
||||
* Allows user to access various settings.
|
||||
*
|
||||
* @param model the model
|
||||
* @return settings template
|
||||
*/
|
||||
@GetMapping("/settings")
|
||||
public String settings(Model model) {
|
||||
model.addAttribute("pageTitle", "Settings");
|
||||
return "settings";
|
||||
}
|
||||
|
||||
/**
|
||||
* Public user profile page by username.
|
||||
* Shows public profile of any user.
|
||||
*
|
||||
* @param username the username
|
||||
* @param model the model
|
||||
* @return profile template
|
||||
*/
|
||||
@GetMapping("/users/{username}")
|
||||
public String userProfile(@PathVariable String username, Model model) {
|
||||
model.addAttribute("pageTitle", "Profile - @" + username);
|
||||
model.addAttribute("username", username);
|
||||
return "profile/public";
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
package org.operaton.fitpub.controller;
|
||||
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
|
||||
/**
|
||||
* Controller for timeline view pages.
|
||||
*/
|
||||
@Controller
|
||||
public class TimelineViewController {
|
||||
|
||||
/**
|
||||
* Public timeline page - shows all public activities.
|
||||
*
|
||||
* @param model the model
|
||||
* @return timeline template
|
||||
*/
|
||||
@GetMapping("/timeline")
|
||||
public String publicTimeline(Model model) {
|
||||
model.addAttribute("pageTitle", "Public Timeline");
|
||||
return "timeline/public";
|
||||
}
|
||||
|
||||
/**
|
||||
* Federated timeline page - shows activities from followed users.
|
||||
* Requires authentication.
|
||||
*
|
||||
* @param model the model
|
||||
* @return timeline template
|
||||
*/
|
||||
@GetMapping("/timeline/federated")
|
||||
public String federatedTimeline(Model model) {
|
||||
model.addAttribute("pageTitle", "Federated Timeline");
|
||||
return "timeline/federated";
|
||||
}
|
||||
|
||||
/**
|
||||
* User timeline page - shows current user's own activities.
|
||||
* Requires authentication.
|
||||
*
|
||||
* @param model the model
|
||||
* @return timeline template
|
||||
*/
|
||||
@GetMapping("/timeline/user")
|
||||
public String userTimeline(Model model) {
|
||||
model.addAttribute("pageTitle", "My Timeline");
|
||||
return "timeline/user";
|
||||
}
|
||||
}
|
||||
109
src/main/java/org/operaton/fitpub/controller/UserController.java
Normal file
109
src/main/java/org/operaton/fitpub/controller/UserController.java
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
package org.operaton.fitpub.controller;
|
||||
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.operaton.fitpub.model.dto.UserDTO;
|
||||
import org.operaton.fitpub.model.dto.UserUpdateRequest;
|
||||
import org.operaton.fitpub.model.entity.User;
|
||||
import org.operaton.fitpub.repository.UserRepository;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
import org.springframework.security.core.userdetails.UsernameNotFoundException;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* REST controller for user profile operations.
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/users")
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class UserController {
|
||||
|
||||
private final UserRepository userRepository;
|
||||
|
||||
/**
|
||||
* Get current user's profile.
|
||||
*
|
||||
* @param userDetails the authenticated user
|
||||
* @return user profile
|
||||
*/
|
||||
@GetMapping("/me")
|
||||
public ResponseEntity<UserDTO> getCurrentUser(@AuthenticationPrincipal UserDetails userDetails) {
|
||||
log.debug("User {} retrieving own profile", userDetails.getUsername());
|
||||
|
||||
User user = userRepository.findByUsername(userDetails.getUsername())
|
||||
.orElseThrow(() -> new UsernameNotFoundException("User not found"));
|
||||
|
||||
return ResponseEntity.ok(UserDTO.fromEntity(user));
|
||||
}
|
||||
|
||||
/**
|
||||
* Update current user's profile.
|
||||
*
|
||||
* @param request the update request
|
||||
* @param userDetails the authenticated user
|
||||
* @return updated user profile
|
||||
*/
|
||||
@PutMapping("/me")
|
||||
public ResponseEntity<UserDTO> updateCurrentUser(
|
||||
@Valid @RequestBody UserUpdateRequest request,
|
||||
@AuthenticationPrincipal UserDetails userDetails
|
||||
) {
|
||||
log.info("User {} updating profile", userDetails.getUsername());
|
||||
|
||||
User user = userRepository.findByUsername(userDetails.getUsername())
|
||||
.orElseThrow(() -> new UsernameNotFoundException("User not found"));
|
||||
|
||||
// Update allowed fields
|
||||
if (request.getDisplayName() != null) {
|
||||
user.setDisplayName(request.getDisplayName().trim());
|
||||
}
|
||||
if (request.getBio() != null) {
|
||||
user.setBio(request.getBio().trim());
|
||||
}
|
||||
if (request.getAvatarUrl() != null) {
|
||||
user.setAvatarUrl(request.getAvatarUrl().trim());
|
||||
}
|
||||
|
||||
User updated = userRepository.save(user);
|
||||
|
||||
return ResponseEntity.ok(UserDTO.fromEntity(updated));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user profile by username.
|
||||
*
|
||||
* @param username the username
|
||||
* @return user profile
|
||||
*/
|
||||
@GetMapping("/{username}")
|
||||
public ResponseEntity<UserDTO> getUserByUsername(@PathVariable String username) {
|
||||
log.debug("Retrieving profile for username: {}", username);
|
||||
|
||||
User user = userRepository.findByUsername(username)
|
||||
.orElseThrow(() -> new UsernameNotFoundException("User not found: " + username));
|
||||
|
||||
return ResponseEntity.ok(UserDTO.fromEntity(user));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user profile by ID.
|
||||
*
|
||||
* @param id the user ID
|
||||
* @return user profile
|
||||
*/
|
||||
@GetMapping("/id/{id}")
|
||||
public ResponseEntity<UserDTO> getUserById(@PathVariable UUID id) {
|
||||
log.debug("Retrieving profile for user ID: {}", id);
|
||||
|
||||
User user = userRepository.findById(id)
|
||||
.orElseThrow(() -> new UsernameNotFoundException("User not found"));
|
||||
|
||||
return ResponseEntity.ok(UserDTO.fromEntity(user));
|
||||
}
|
||||
}
|
||||
|
|
@ -1,15 +1,24 @@
|
|||
package org.operaton.fitpub.model.dto;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import org.locationtech.jts.geom.Coordinate;
|
||||
import org.locationtech.jts.geom.LineString;
|
||||
import org.operaton.fitpub.model.entity.Activity;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.Duration;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
/**
|
||||
* DTO for Activity data transfer.
|
||||
|
|
@ -36,6 +45,40 @@ public class ActivityDTO {
|
|||
private LocalDateTime createdAt;
|
||||
private LocalDateTime updatedAt;
|
||||
|
||||
// Map rendering data
|
||||
private Map<String, Object> simplifiedTrack; // GeoJSON LineString
|
||||
private List<Map<String, Object>> trackPoints; // Full track points from JSONB
|
||||
|
||||
// Convenience getters for flattened metrics (for frontend compatibility)
|
||||
public Integer getAverageHeartRate() {
|
||||
return metrics != null ? metrics.getAverageHeartRate() : null;
|
||||
}
|
||||
|
||||
public Integer getMaxHeartRate() {
|
||||
return metrics != null ? metrics.getMaxHeartRate() : null;
|
||||
}
|
||||
|
||||
public Integer getAverageCadence() {
|
||||
return metrics != null ? metrics.getAverageCadence() : null;
|
||||
}
|
||||
|
||||
public BigDecimal getAverageSpeed() {
|
||||
return metrics != null ? metrics.getAverageSpeed() : null;
|
||||
}
|
||||
|
||||
public BigDecimal getMaxSpeed() {
|
||||
return metrics != null ? metrics.getMaxSpeed() : null;
|
||||
}
|
||||
|
||||
public Integer getCalories() {
|
||||
return metrics != null ? metrics.getCalories() : null;
|
||||
}
|
||||
|
||||
// Alias for frontend compatibility
|
||||
public Long getTotalDuration() {
|
||||
return totalDurationSeconds;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a DTO from an Activity entity.
|
||||
*/
|
||||
|
|
@ -63,6 +106,65 @@ public class ActivityDTO {
|
|||
builder.metrics(ActivityMetricsDTO.fromEntity(activity.getMetrics()));
|
||||
}
|
||||
|
||||
// Convert simplified track to GeoJSON
|
||||
if (activity.getSimplifiedTrack() != null) {
|
||||
builder.simplifiedTrack(lineStringToGeoJson(activity.getSimplifiedTrack()));
|
||||
}
|
||||
|
||||
// Parse track points from JSONB
|
||||
if (activity.getTrackPointsJson() != null && !activity.getTrackPointsJson().isEmpty()) {
|
||||
builder.trackPoints(parseTrackPoints(activity.getTrackPointsJson()));
|
||||
}
|
||||
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a JTS LineString to GeoJSON format.
|
||||
*/
|
||||
private static Map<String, Object> lineStringToGeoJson(LineString lineString) {
|
||||
Map<String, Object> geoJson = new LinkedHashMap<>();
|
||||
geoJson.put("type", "LineString");
|
||||
|
||||
List<List<Double>> coordinates = Stream.of(lineString.getCoordinates())
|
||||
.map(coord -> List.of(coord.getX(), coord.getY()))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
geoJson.put("coordinates", coordinates);
|
||||
return geoJson;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses track points from JSONB string.
|
||||
*/
|
||||
private static List<Map<String, Object>> parseTrackPoints(String trackPointsJson) {
|
||||
try {
|
||||
ObjectMapper mapper = new ObjectMapper();
|
||||
JsonNode root = mapper.readTree(trackPointsJson);
|
||||
|
||||
if (root.isArray()) {
|
||||
List<Map<String, Object>> trackPoints = new java.util.ArrayList<>();
|
||||
for (JsonNode node : root) {
|
||||
Map<String, Object> point = new LinkedHashMap<>();
|
||||
|
||||
if (node.has("timestamp")) point.put("timestamp", node.get("timestamp").asText());
|
||||
if (node.has("latitude")) point.put("latitude", node.get("latitude").asDouble());
|
||||
if (node.has("longitude")) point.put("longitude", node.get("longitude").asDouble());
|
||||
if (node.has("elevation")) point.put("elevation", node.get("elevation").asDouble());
|
||||
if (node.has("heartRate")) point.put("heartRate", node.get("heartRate").asInt());
|
||||
if (node.has("cadence")) point.put("cadence", node.get("cadence").asInt());
|
||||
if (node.has("speed")) point.put("speed", node.get("speed").asDouble());
|
||||
if (node.has("power")) point.put("power", node.get("power").asInt());
|
||||
if (node.has("temperature")) point.put("temperature", node.get("temperature").asDouble());
|
||||
|
||||
trackPoints.add(point);
|
||||
}
|
||||
return trackPoints;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// Log error but don't fail the entire DTO creation
|
||||
System.err.println("Error parsing track points JSON: " + e.getMessage());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
63
src/main/java/org/operaton/fitpub/model/dto/UserDTO.java
Normal file
63
src/main/java/org/operaton/fitpub/model/dto/UserDTO.java
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
package org.operaton.fitpub.model.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import org.operaton.fitpub.model.entity.User;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* DTO for User data transfer.
|
||||
* Used for public user profiles (excludes sensitive data like password hash and private keys).
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class UserDTO {
|
||||
|
||||
private UUID id;
|
||||
private String username;
|
||||
private String email; // Only shown to the user themselves
|
||||
private String displayName;
|
||||
private String bio;
|
||||
private String avatarUrl;
|
||||
private LocalDateTime createdAt;
|
||||
private LocalDateTime updatedAt;
|
||||
|
||||
/**
|
||||
* Creates a DTO from a User entity.
|
||||
* Note: email should only be included when user is viewing their own profile.
|
||||
*/
|
||||
public static UserDTO fromEntity(User user) {
|
||||
return UserDTO.builder()
|
||||
.id(user.getId())
|
||||
.username(user.getUsername())
|
||||
.email(user.getEmail())
|
||||
.displayName(user.getDisplayName())
|
||||
.bio(user.getBio())
|
||||
.avatarUrl(user.getAvatarUrl())
|
||||
.createdAt(user.getCreatedAt())
|
||||
.updatedAt(user.getUpdatedAt())
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a public DTO from a User entity (excludes email).
|
||||
* Use this when returning user data to other users.
|
||||
*/
|
||||
public static UserDTO fromEntityPublic(User user) {
|
||||
return UserDTO.builder()
|
||||
.id(user.getId())
|
||||
.username(user.getUsername())
|
||||
.displayName(user.getDisplayName())
|
||||
.bio(user.getBio())
|
||||
.avatarUrl(user.getAvatarUrl())
|
||||
.createdAt(user.getCreatedAt())
|
||||
.updatedAt(user.getUpdatedAt())
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
package org.operaton.fitpub.model.dto;
|
||||
|
||||
import jakarta.validation.constraints.Size;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import org.hibernate.validator.constraints.URL;
|
||||
|
||||
/**
|
||||
* DTO for user profile update requests.
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class UserUpdateRequest {
|
||||
|
||||
@Size(max = 100, message = "Display name must not exceed 100 characters")
|
||||
private String displayName;
|
||||
|
||||
@Size(max = 500, message = "Bio must not exceed 500 characters")
|
||||
private String bio;
|
||||
|
||||
@URL(message = "Avatar URL must be a valid URL")
|
||||
private String avatarUrl;
|
||||
}
|
||||
|
|
@ -45,6 +45,7 @@ public interface ActivityRepository extends JpaRepository<Activity, UUID> {
|
|||
* Find all public activities for a user.
|
||||
*
|
||||
* @param userId the user ID
|
||||
* @param visibility the visibility level
|
||||
* @return list of activities
|
||||
*/
|
||||
List<Activity> findByUserIdAndVisibilityOrderByStartedAtDesc(
|
||||
|
|
@ -52,6 +53,20 @@ public interface ActivityRepository extends JpaRepository<Activity, UUID> {
|
|||
Activity.Visibility visibility
|
||||
);
|
||||
|
||||
/**
|
||||
* Find activities for a user by visibility with pagination.
|
||||
*
|
||||
* @param userId the user ID
|
||||
* @param visibility the visibility level
|
||||
* @param pageable pagination parameters
|
||||
* @return page of activities
|
||||
*/
|
||||
Page<Activity> findByUserIdAndVisibilityOrderByStartedAtDesc(
|
||||
UUID userId,
|
||||
Activity.Visibility visibility,
|
||||
Pageable pageable
|
||||
);
|
||||
|
||||
/**
|
||||
* Find activities by type for a user.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -339,6 +339,19 @@ public class FitFileService {
|
|||
return activityRepository.findByIdAndUserId(activityId, userId).orElse(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves an activity by ID without user authorization check.
|
||||
* This is used for public activity access (e.g., viewing public tracks).
|
||||
* Caller is responsible for checking visibility and access permissions.
|
||||
*
|
||||
* @param activityId the activity ID
|
||||
* @return the activity or null if not found
|
||||
*/
|
||||
@Transactional(readOnly = true)
|
||||
public Activity getActivityById(UUID activityId) {
|
||||
return activityRepository.findById(activityId).orElse(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves all activities for a user.
|
||||
*
|
||||
|
|
@ -350,6 +363,33 @@ public class FitFileService {
|
|||
return activityRepository.findByUserIdOrderByStartedAtDesc(userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves activities for a user with pagination.
|
||||
*
|
||||
* @param userId the user ID
|
||||
* @param page page number (0-indexed)
|
||||
* @param size page size
|
||||
* @return page of activities
|
||||
*/
|
||||
@Transactional(readOnly = true)
|
||||
public org.springframework.data.domain.Page<Activity> getUserActivitiesPaginated(UUID userId, int page, int size) {
|
||||
org.springframework.data.domain.Pageable pageable =
|
||||
org.springframework.data.domain.PageRequest.of(page, size, org.springframework.data.domain.Sort.by("startedAt").descending());
|
||||
return activityRepository.findByUserIdOrderByStartedAtDesc(userId, pageable);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves public activities for a user with pagination.
|
||||
*
|
||||
* @param userId the user ID
|
||||
* @param pageable pagination parameters
|
||||
* @return page of public activities
|
||||
*/
|
||||
@Transactional(readOnly = true)
|
||||
public org.springframework.data.domain.Page<Activity> getPublicActivitiesByUserId(UUID userId, org.springframework.data.domain.Pageable pageable) {
|
||||
return activityRepository.findByUserIdAndVisibilityOrderByStartedAtDesc(userId, Activity.Visibility.PUBLIC, pageable);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update an existing activity's metadata.
|
||||
*
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue