Moar federation
This commit is contained in:
parent
d47daa6dfc
commit
4c9bcc718f
5 changed files with 376 additions and 29 deletions
|
|
@ -77,6 +77,9 @@ public class SecurityConfig {
|
||||||
// Public endpoints - Activity track data (for public activities)
|
// Public endpoints - Activity track data (for public activities)
|
||||||
.requestMatchers(HttpMethod.GET, "/api/activities/*/track").permitAll()
|
.requestMatchers(HttpMethod.GET, "/api/activities/*/track").permitAll()
|
||||||
|
|
||||||
|
// Public endpoints - Activity images (for federation)
|
||||||
|
.requestMatchers(HttpMethod.GET, "/api/activities/*/image").permitAll()
|
||||||
|
|
||||||
// Public endpoints - User's public activities
|
// Public endpoints - User's public activities
|
||||||
.requestMatchers(HttpMethod.GET, "/api/activities/user/*").permitAll()
|
.requestMatchers(HttpMethod.GET, "/api/activities/user/*").permitAll()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import org.operaton.fitpub.model.dto.ActivityUploadRequest;
|
||||||
import org.operaton.fitpub.model.entity.Activity;
|
import org.operaton.fitpub.model.entity.Activity;
|
||||||
import org.operaton.fitpub.model.entity.User;
|
import org.operaton.fitpub.model.entity.User;
|
||||||
import org.operaton.fitpub.repository.UserRepository;
|
import org.operaton.fitpub.repository.UserRepository;
|
||||||
|
import org.operaton.fitpub.service.ActivityImageService;
|
||||||
import org.operaton.fitpub.service.FederationService;
|
import org.operaton.fitpub.service.FederationService;
|
||||||
import org.operaton.fitpub.service.FitFileService;
|
import org.operaton.fitpub.service.FitFileService;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
|
@ -40,6 +41,7 @@ public class ActivityController {
|
||||||
private final FitFileService fitFileService;
|
private final FitFileService fitFileService;
|
||||||
private final UserRepository userRepository;
|
private final UserRepository userRepository;
|
||||||
private final FederationService federationService;
|
private final FederationService federationService;
|
||||||
|
private final ActivityImageService activityImageService;
|
||||||
|
|
||||||
@Value("${fitpub.base-url}")
|
@Value("${fitpub.base-url}")
|
||||||
private String baseUrl;
|
private String baseUrl;
|
||||||
|
|
@ -106,13 +108,20 @@ public class ActivityController {
|
||||||
noteObject.put("to", List.of(actorUri + "/followers"));
|
noteObject.put("to", List.of(actorUri + "/followers"));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add summary with key metrics
|
|
||||||
String summary = formatActivitySummary(activity);
|
|
||||||
noteObject.put("summary", summary);
|
|
||||||
|
|
||||||
// Add URL to the activity page
|
// Add URL to the activity page
|
||||||
noteObject.put("url", baseUrl + "/activities/" + activity.getId());
|
noteObject.put("url", baseUrl + "/activities/" + activity.getId());
|
||||||
|
|
||||||
|
// Generate and attach activity image
|
||||||
|
String imageUrl = activityImageService.generateActivityImage(activity);
|
||||||
|
if (imageUrl != null) {
|
||||||
|
Map<String, Object> imageAttachment = new HashMap<>();
|
||||||
|
imageAttachment.put("type", "Image");
|
||||||
|
imageAttachment.put("mediaType", "image/png");
|
||||||
|
imageAttachment.put("url", imageUrl);
|
||||||
|
imageAttachment.put("name", "Activity map showing " + activity.getActivityType() + " route");
|
||||||
|
noteObject.put("attachment", List.of(imageAttachment));
|
||||||
|
}
|
||||||
|
|
||||||
federationService.sendCreateActivity(
|
federationService.sendCreateActivity(
|
||||||
activityUri,
|
activityUri,
|
||||||
noteObject,
|
noteObject,
|
||||||
|
|
@ -170,30 +179,6 @@ public class ActivityController {
|
||||||
return content.toString();
|
return content.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Format activity summary for ActivityPub.
|
|
||||||
*/
|
|
||||||
private String formatActivitySummary(Activity activity) {
|
|
||||||
StringBuilder summary = new StringBuilder();
|
|
||||||
summary.append(activity.getActivityType());
|
|
||||||
|
|
||||||
if (activity.getTotalDistance() != null) {
|
|
||||||
summary.append(" • ").append(String.format("%.2f km", activity.getTotalDistance().doubleValue() / 1000.0));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (activity.getTotalDurationSeconds() != null) {
|
|
||||||
long hours = activity.getTotalDurationSeconds() / 3600;
|
|
||||||
long minutes = (activity.getTotalDurationSeconds() % 3600) / 60;
|
|
||||||
if (hours > 0) {
|
|
||||||
summary.append(" • ").append(hours).append("h ").append(minutes).append("m");
|
|
||||||
} else {
|
|
||||||
summary.append(" • ").append(minutes).append("m");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return summary.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Simple HTML escaping.
|
* Simple HTML escaping.
|
||||||
*/
|
*/
|
||||||
|
|
@ -472,4 +457,32 @@ public class ActivityController {
|
||||||
|
|
||||||
return ResponseEntity.ok(geoJson);
|
return ResponseEntity.ok(geoJson);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Serves the generated activity image.
|
||||||
|
*
|
||||||
|
* @param id the activity ID
|
||||||
|
* @return the activity image
|
||||||
|
*/
|
||||||
|
@GetMapping("/{id}/image")
|
||||||
|
public ResponseEntity<org.springframework.core.io.Resource> getActivityImage(@PathVariable UUID id) {
|
||||||
|
try {
|
||||||
|
java.io.File imageFile = activityImageService.getActivityImageFile(id);
|
||||||
|
|
||||||
|
if (!imageFile.exists()) {
|
||||||
|
return ResponseEntity.notFound().build();
|
||||||
|
}
|
||||||
|
|
||||||
|
org.springframework.core.io.Resource resource =
|
||||||
|
new org.springframework.core.io.FileSystemResource(imageFile);
|
||||||
|
|
||||||
|
return ResponseEntity.ok()
|
||||||
|
.contentType(org.springframework.http.MediaType.IMAGE_PNG)
|
||||||
|
.header(org.springframework.http.HttpHeaders.CACHE_CONTROL, "public, max-age=31536000")
|
||||||
|
.body(resource);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Error serving activity image for {}", id, e);
|
||||||
|
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,321 @@
|
||||||
|
package org.operaton.fitpub.service;
|
||||||
|
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.operaton.fitpub.model.entity.Activity;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import javax.imageio.ImageIO;
|
||||||
|
import java.awt.*;
|
||||||
|
import java.awt.geom.Path2D;
|
||||||
|
import java.awt.image.BufferedImage;
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service for generating activity preview images for ActivityPub federation.
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@Slf4j
|
||||||
|
public class ActivityImageService {
|
||||||
|
|
||||||
|
@Value("${fitpub.storage.images.path:${java.io.tmpdir}/fitpub/images}")
|
||||||
|
private String imagesPath;
|
||||||
|
|
||||||
|
@Value("${fitpub.base-url}")
|
||||||
|
private String baseUrl;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a preview image for an activity showing the track outline and metadata.
|
||||||
|
*
|
||||||
|
* @param activity the activity to generate an image for
|
||||||
|
* @return the URL of the generated image
|
||||||
|
*/
|
||||||
|
public String generateActivityImage(Activity activity) {
|
||||||
|
try {
|
||||||
|
// Image dimensions
|
||||||
|
int width = 1200;
|
||||||
|
int height = 630; // Open Graph standard size
|
||||||
|
|
||||||
|
// Create image
|
||||||
|
BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
|
||||||
|
Graphics2D g2d = image.createGraphics();
|
||||||
|
|
||||||
|
// Enable antialiasing
|
||||||
|
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
|
||||||
|
g2d.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
|
||||||
|
g2d.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
|
||||||
|
|
||||||
|
// Background
|
||||||
|
g2d.setColor(new Color(30, 30, 30)); // Dark background
|
||||||
|
g2d.fillRect(0, 0, width, height);
|
||||||
|
|
||||||
|
// Draw track if available
|
||||||
|
if (activity.getTrackPointsJson() != null && !activity.getTrackPointsJson().isEmpty()) {
|
||||||
|
drawTrack(g2d, activity, width, height);
|
||||||
|
} else if (activity.getSimplifiedTrack() != null) {
|
||||||
|
drawSimplifiedTrack(g2d, activity, width, height);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw metadata overlay
|
||||||
|
drawMetadata(g2d, activity, width, height);
|
||||||
|
|
||||||
|
g2d.dispose();
|
||||||
|
|
||||||
|
// Save image
|
||||||
|
File imagesDir = new File(imagesPath);
|
||||||
|
if (!imagesDir.exists()) {
|
||||||
|
imagesDir.mkdirs();
|
||||||
|
}
|
||||||
|
|
||||||
|
String filename = activity.getId() + ".png";
|
||||||
|
File imageFile = new File(imagesDir, filename);
|
||||||
|
ImageIO.write(image, "png", imageFile);
|
||||||
|
|
||||||
|
log.info("Generated activity image: {}", imageFile.getAbsolutePath());
|
||||||
|
|
||||||
|
// Return URL to the image
|
||||||
|
return baseUrl + "/api/activities/" + activity.getId() + "/image";
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Failed to generate activity image for {}", activity.getId(), e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draw the track outline from high-resolution track points.
|
||||||
|
*/
|
||||||
|
private void drawTrack(Graphics2D g2d, Activity activity, int width, int height) {
|
||||||
|
List<Map<String, Object>> trackPoints = parseTrackPoints(activity.getTrackPointsJson());
|
||||||
|
if (trackPoints == null || trackPoints.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find bounds
|
||||||
|
double minLat = Double.MAX_VALUE, maxLat = -Double.MAX_VALUE;
|
||||||
|
double minLon = Double.MAX_VALUE, maxLon = -Double.MAX_VALUE;
|
||||||
|
|
||||||
|
for (Map<String, Object> point : trackPoints) {
|
||||||
|
Double lat = getDouble(point, "latitude");
|
||||||
|
Double lon = getDouble(point, "longitude");
|
||||||
|
if (lat != null && lon != null) {
|
||||||
|
minLat = Math.min(minLat, lat);
|
||||||
|
maxLat = Math.max(maxLat, lat);
|
||||||
|
minLon = Math.min(minLon, lon);
|
||||||
|
maxLon = Math.max(maxLon, lon);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add padding
|
||||||
|
double latRange = maxLat - minLat;
|
||||||
|
double lonRange = maxLon - minLon;
|
||||||
|
double padding = 0.1; // 10% padding
|
||||||
|
minLat -= latRange * padding;
|
||||||
|
maxLat += latRange * padding;
|
||||||
|
minLon -= lonRange * padding;
|
||||||
|
maxLon += lonRange * padding;
|
||||||
|
|
||||||
|
// Calculate scale (use left 60% of image for track, right 40% for metadata)
|
||||||
|
int trackWidth = (int) (width * 0.6);
|
||||||
|
int trackHeight = height;
|
||||||
|
double scaleX = trackWidth / (maxLon - minLon);
|
||||||
|
double scaleY = trackHeight / (maxLat - minLat);
|
||||||
|
double scale = Math.min(scaleX, scaleY);
|
||||||
|
|
||||||
|
// Create path
|
||||||
|
Path2D.Double path = new Path2D.Double();
|
||||||
|
boolean first = true;
|
||||||
|
|
||||||
|
for (Map<String, Object> point : trackPoints) {
|
||||||
|
Double lat = getDouble(point, "latitude");
|
||||||
|
Double lon = getDouble(point, "longitude");
|
||||||
|
if (lat != null && lon != null) {
|
||||||
|
double x = (lon - minLon) * scale;
|
||||||
|
double y = trackHeight - (lat - minLat) * scale; // Flip Y axis
|
||||||
|
|
||||||
|
if (first) {
|
||||||
|
path.moveTo(x, y);
|
||||||
|
first = false;
|
||||||
|
} else {
|
||||||
|
path.lineTo(x, y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw track
|
||||||
|
g2d.setColor(new Color(0, 180, 216)); // Bright blue
|
||||||
|
g2d.setStroke(new BasicStroke(4.0f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
|
||||||
|
g2d.draw(path);
|
||||||
|
|
||||||
|
// Draw start and end markers
|
||||||
|
if (!trackPoints.isEmpty()) {
|
||||||
|
Map<String, Object> firstPoint = trackPoints.get(0);
|
||||||
|
Map<String, Object> lastPoint = trackPoints.get(trackPoints.size() - 1);
|
||||||
|
|
||||||
|
drawMarker(g2d, firstPoint, minLat, minLon, scale, trackHeight, new Color(0, 255, 0)); // Green start
|
||||||
|
drawMarker(g2d, lastPoint, minLat, minLon, scale, trackHeight, new Color(255, 0, 0)); // Red end
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draw the track from simplified track (LineString).
|
||||||
|
*/
|
||||||
|
private void drawSimplifiedTrack(Graphics2D g2d, Activity activity, int width, int height) {
|
||||||
|
// Similar logic but using simplified track coordinates
|
||||||
|
// This is a fallback if high-res track points aren't available
|
||||||
|
log.debug("Using simplified track for activity {}", activity.getId());
|
||||||
|
// TODO: Implement if needed
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draw a circular marker at a track point.
|
||||||
|
*/
|
||||||
|
private void drawMarker(Graphics2D g2d, Map<String, Object> point, double minLat, double minLon,
|
||||||
|
double scale, int trackHeight, Color color) {
|
||||||
|
Double lat = getDouble(point, "latitude");
|
||||||
|
Double lon = getDouble(point, "longitude");
|
||||||
|
if (lat != null && lon != null) {
|
||||||
|
double x = (lon - minLon) * scale;
|
||||||
|
double y = trackHeight - (lat - minLat) * scale;
|
||||||
|
|
||||||
|
g2d.setColor(color);
|
||||||
|
int markerSize = 12;
|
||||||
|
g2d.fillOval((int) x - markerSize / 2, (int) y - markerSize / 2, markerSize, markerSize);
|
||||||
|
|
||||||
|
// White outline
|
||||||
|
g2d.setColor(Color.WHITE);
|
||||||
|
g2d.setStroke(new BasicStroke(2.0f));
|
||||||
|
g2d.drawOval((int) x - markerSize / 2, (int) y - markerSize / 2, markerSize, markerSize);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draw metadata overlay on the right side of the image.
|
||||||
|
*/
|
||||||
|
private void drawMetadata(Graphics2D g2d, Activity activity, int width, int height) {
|
||||||
|
int metadataX = (int) (width * 0.62); // Start at 62% to leave some margin
|
||||||
|
int y = 60;
|
||||||
|
int lineHeight = 50;
|
||||||
|
|
||||||
|
// Title
|
||||||
|
g2d.setColor(Color.WHITE);
|
||||||
|
g2d.setFont(new Font("Arial", Font.BOLD, 32));
|
||||||
|
String title = activity.getTitle() != null ? activity.getTitle() : "Activity";
|
||||||
|
if (title.length() > 20) {
|
||||||
|
title = title.substring(0, 20) + "...";
|
||||||
|
}
|
||||||
|
g2d.drawString(title, metadataX, y);
|
||||||
|
y += lineHeight + 10;
|
||||||
|
|
||||||
|
// Activity type
|
||||||
|
g2d.setFont(new Font("Arial", Font.PLAIN, 24));
|
||||||
|
g2d.setColor(new Color(200, 200, 200));
|
||||||
|
g2d.drawString(activity.getActivityType().toString(), metadataX, y);
|
||||||
|
y += lineHeight;
|
||||||
|
|
||||||
|
// Distance
|
||||||
|
if (activity.getTotalDistance() != null) {
|
||||||
|
g2d.setFont(new Font("Arial", Font.BOLD, 28));
|
||||||
|
g2d.setColor(Color.WHITE);
|
||||||
|
String distance = String.format("%.2f km", activity.getTotalDistance().doubleValue() / 1000.0);
|
||||||
|
g2d.drawString(distance, metadataX, y);
|
||||||
|
g2d.setFont(new Font("Arial", Font.PLAIN, 18));
|
||||||
|
g2d.setColor(new Color(150, 150, 150));
|
||||||
|
g2d.drawString("Distance", metadataX, y + 25);
|
||||||
|
y += lineHeight + 30;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Duration
|
||||||
|
if (activity.getTotalDurationSeconds() != null) {
|
||||||
|
long hours = activity.getTotalDurationSeconds() / 3600;
|
||||||
|
long minutes = (activity.getTotalDurationSeconds() % 3600) / 60;
|
||||||
|
long seconds = activity.getTotalDurationSeconds() % 60;
|
||||||
|
|
||||||
|
g2d.setFont(new Font("Arial", Font.BOLD, 28));
|
||||||
|
g2d.setColor(Color.WHITE);
|
||||||
|
String duration;
|
||||||
|
if (hours > 0) {
|
||||||
|
duration = String.format("%d:%02d:%02d", hours, minutes, seconds);
|
||||||
|
} else {
|
||||||
|
duration = String.format("%d:%02d", minutes, seconds);
|
||||||
|
}
|
||||||
|
g2d.drawString(duration, metadataX, y);
|
||||||
|
g2d.setFont(new Font("Arial", Font.PLAIN, 18));
|
||||||
|
g2d.setColor(new Color(150, 150, 150));
|
||||||
|
g2d.drawString("Duration", metadataX, y + 25);
|
||||||
|
y += lineHeight + 30;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Elevation gain
|
||||||
|
if (activity.getElevationGain() != null) {
|
||||||
|
g2d.setFont(new Font("Arial", Font.BOLD, 28));
|
||||||
|
g2d.setColor(Color.WHITE);
|
||||||
|
String elevation = String.format("%.0f m", activity.getElevationGain());
|
||||||
|
g2d.drawString(elevation, metadataX, y);
|
||||||
|
g2d.setFont(new Font("Arial", Font.PLAIN, 18));
|
||||||
|
g2d.setColor(new Color(150, 150, 150));
|
||||||
|
g2d.drawString("Elevation Gain", metadataX, y + 25);
|
||||||
|
y += lineHeight + 30;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Branding
|
||||||
|
g2d.setFont(new Font("Arial", Font.PLAIN, 20));
|
||||||
|
g2d.setColor(new Color(100, 100, 100));
|
||||||
|
g2d.drawString("FitPub", metadataX, height - 40);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper to safely extract Double from Map.
|
||||||
|
*/
|
||||||
|
private Double getDouble(Map<String, Object> map, String key) {
|
||||||
|
Object value = map.get(key);
|
||||||
|
if (value instanceof Number) {
|
||||||
|
return ((Number) value).doubleValue();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the file path for an activity image.
|
||||||
|
*/
|
||||||
|
public File getActivityImageFile(UUID activityId) {
|
||||||
|
return new File(imagesPath, activityId + ".png");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses track points from JSONB string.
|
||||||
|
*/
|
||||||
|
private List<Map<String, Object>> parseTrackPoints(String trackPointsJson) {
|
||||||
|
if (trackPointsJson == null || trackPointsJson.isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
com.fasterxml.jackson.databind.ObjectMapper mapper = new com.fasterxml.jackson.databind.ObjectMapper();
|
||||||
|
com.fasterxml.jackson.databind.JsonNode root = mapper.readTree(trackPointsJson);
|
||||||
|
|
||||||
|
if (root.isArray()) {
|
||||||
|
List<Map<String, Object>> trackPoints = new java.util.ArrayList<>();
|
||||||
|
for (com.fasterxml.jackson.databind.JsonNode node : root) {
|
||||||
|
Map<String, Object> point = new java.util.LinkedHashMap<>();
|
||||||
|
|
||||||
|
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());
|
||||||
|
|
||||||
|
trackPoints.add(point);
|
||||||
|
}
|
||||||
|
return trackPoints;
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Error parsing track points JSON: " + e.getMessage(), e);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -247,6 +247,12 @@ const FitPubAuth = {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Activity detail pages are public (for viewing public activities)
|
||||||
|
// Pattern: /activities/{uuid}
|
||||||
|
if (currentPath.startsWith('/activities/') && currentPath.split('/').length === 3) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Check if authenticated
|
// Check if authenticated
|
||||||
if (!this.isAuthenticated()) {
|
if (!this.isAuthenticated()) {
|
||||||
// Redirect to login for protected pages
|
// Redirect to login for protected pages
|
||||||
|
|
|
||||||
|
|
@ -271,7 +271,11 @@
|
||||||
|
|
||||||
async function loadActivity() {
|
async function loadActivity() {
|
||||||
try {
|
try {
|
||||||
const response = await FitPubAuth.authenticatedFetch(`/api/activities/${activityId}`);
|
// Use authenticated fetch if user is logged in, otherwise regular fetch
|
||||||
|
// This allows public activities to be viewed without authentication
|
||||||
|
const response = FitPubAuth.isAuthenticated()
|
||||||
|
? await FitPubAuth.authenticatedFetch(`/api/activities/${activityId}`)
|
||||||
|
: await fetch(`/api/activities/${activityId}`);
|
||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
const activity = await response.json();
|
const activity = await response.json();
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue