From 87da2a38616782c22579f94cb148e1b92721a81b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Z=C3=B6ller?= Date: Sun, 11 Jan 2026 12:21:24 +0100 Subject: [PATCH] Remove migration logic again --- INDOOR_DETECTION_IMPLEMENTATION.md | 59 +------ .../fitpub/config/SecurityConfig.java | 3 - .../fitpub/controller/AdminController.java | 45 ----- .../fitpub/controller/AuthController.java | 5 +- .../IndoorActivityMigrationService.java | 161 ------------------ .../resources/templates/auth/register.html | 27 ++- 6 files changed, 23 insertions(+), 277 deletions(-) delete mode 100644 src/main/java/org/operaton/fitpub/controller/AdminController.java delete mode 100644 src/main/java/org/operaton/fitpub/service/IndoorActivityMigrationService.java diff --git a/INDOOR_DETECTION_IMPLEMENTATION.md b/INDOOR_DETECTION_IMPLEMENTATION.md index 87c2200..7eb62b5 100644 --- a/INDOOR_DETECTION_IMPLEMENTATION.md +++ b/INDOOR_DETECTION_IMPLEMENTATION.md @@ -123,56 +123,6 @@ WHERE a.user_id = :userId --- -## Retroactive Migration - -### For Activities Uploaded BEFORE This Feature - -**Run once** to populate indoor flags for existing data: - -```bash -POST /api/admin/migrate-indoor-flags -Authorization: Bearer -``` - -This endpoint: -1. Fetches all FIT activities with stored raw files -2. Re-parses FIT files to extract SubSport -3. Updates `indoor`, `sub_sport`, and `indoor_detection_method` -4. Only saves if values changed (idempotent) - -**Response**: -```json -{ - "message": "Indoor activity flag migration complete", - "activitiesUpdated": 15 -} -``` - -### How to Run Migration - -#### Option 1: Using Browser DevTools -1. Login to FitPub -2. Open DevTools (F12) → Application → Local Storage -3. Copy `jwt_token` value -4. Use browser fetch or Postman: -```javascript -fetch('http://localhost:8080/api/admin/migrate-indoor-flags', { - method: 'POST', - headers: { - 'Authorization': 'Bearer YOUR_TOKEN_HERE' - } -}).then(r => r.json()).then(console.log); -``` - -#### Option 2: Using curl -```bash -curl -X POST \ - -H "Authorization: Bearer YOUR_TOKEN_HERE" \ - http://localhost:8080/api/admin/migrate-indoor-flags -``` - ---- - ## Code Structure ### Key Files Modified @@ -192,15 +142,9 @@ curl -X POST \ 4. **Service**: - `ActivityFileService.java` - Save SubSport & detection method to database - - `IndoorActivityMigrationService.java` - Retroactive migration logic 5. **Repository**: - `UserHeatmapGridRepository.java` - Exclude indoor activities from heatmap queries - - `ActivityRepository.java` - Added query method for migration - -6. **Controller**: - - `AdminController.java` - Migration endpoint - - `SecurityConfig.java` - Added `/api/admin/**` route (authenticated) --- @@ -241,7 +185,6 @@ Expected: - **New uploads**: SubSport extracted during normal parsing (no overhead) - **Timeline loading**: Simple column read (instant) - **Heatmap queries**: Added `AND indoor = FALSE` filter (uses index) -- **Migration**: One-time operation, only re-parses FIT files with raw data --- @@ -286,7 +229,7 @@ GROUP BY indoor_detection_method; ✅ **Fully implemented** multi-format indoor detection ✅ **Backward compatible** - existing activities default to outdoor -✅ **Retroactive migration** endpoint for old data +✅ **Automatic detection** on upload for new activities ✅ **Heatmap exclusion** automatic via SQL filters ✅ **Timeline display** includes all activities ✅ **Works for FIT and GPX** files with different detection strategies diff --git a/src/main/java/org/operaton/fitpub/config/SecurityConfig.java b/src/main/java/org/operaton/fitpub/config/SecurityConfig.java index a8f51cf..49283f4 100644 --- a/src/main/java/org/operaton/fitpub/config/SecurityConfig.java +++ b/src/main/java/org/operaton/fitpub/config/SecurityConfig.java @@ -152,9 +152,6 @@ public class SecurityConfig { .requestMatchers(HttpMethod.POST, "/api/users/*/follow").authenticated() // Follow user .requestMatchers(HttpMethod.DELETE, "/api/users/*/follow").authenticated() // Unfollow user - // Protected endpoints - Admin API (data migration, maintenance) - .requestMatchers("/api/admin/**").authenticated() - // All other requests require authentication .anyRequest().authenticated() ) diff --git a/src/main/java/org/operaton/fitpub/controller/AdminController.java b/src/main/java/org/operaton/fitpub/controller/AdminController.java deleted file mode 100644 index 47632ad..0000000 --- a/src/main/java/org/operaton/fitpub/controller/AdminController.java +++ /dev/null @@ -1,45 +0,0 @@ -package org.operaton.fitpub.controller; - -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.operaton.fitpub.service.IndoorActivityMigrationService; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -import java.util.Map; - -/** - * Admin endpoints for data migration and maintenance tasks. - */ -@RestController -@RequestMapping("/api/admin") -@RequiredArgsConstructor -@Slf4j -public class AdminController { - - private final IndoorActivityMigrationService indoorActivityMigrationService; - - /** - * Retroactively detect and update indoor activity flags for existing activities. - * Re-parses all FIT files to detect indoor activities based on SubSport field. - * - * This is a one-time migration endpoint to update existing data. - * - * @return number of activities updated - */ - @PostMapping("/migrate-indoor-flags") - public ResponseEntity> migrateIndoorFlags() { - log.info("Admin: Starting indoor activity flag migration"); - - int updatedCount = indoorActivityMigrationService.updateIndoorFlagsForExistingActivities(); - - log.info("Admin: Indoor activity flag migration complete - {} activities updated", updatedCount); - - return ResponseEntity.ok(Map.of( - "message", "Indoor activity flag migration complete", - "activitiesUpdated", updatedCount - )); - } -} diff --git a/src/main/java/org/operaton/fitpub/controller/AuthController.java b/src/main/java/org/operaton/fitpub/controller/AuthController.java index 1d310de..a8ef4c1 100644 --- a/src/main/java/org/operaton/fitpub/controller/AuthController.java +++ b/src/main/java/org/operaton/fitpub/controller/AuthController.java @@ -91,7 +91,8 @@ public class AuthController { */ @GetMapping("/registration-status") public ResponseEntity getRegistrationStatus() { - return ResponseEntity.ok(new RegistrationStatusResponse(registrationEnabled)); + boolean passwordRequired = configuredRegistrationPassword != null && !configuredRegistrationPassword.trim().isEmpty(); + return ResponseEntity.ok(new RegistrationStatusResponse(registrationEnabled, passwordRequired)); } /** @@ -183,5 +184,5 @@ public class AuthController { /** * Registration status response DTO. */ - record RegistrationStatusResponse(boolean enabled) {} + record RegistrationStatusResponse(boolean enabled, boolean passwordRequired) {} } diff --git a/src/main/java/org/operaton/fitpub/service/IndoorActivityMigrationService.java b/src/main/java/org/operaton/fitpub/service/IndoorActivityMigrationService.java deleted file mode 100644 index 9ebbc9e..0000000 --- a/src/main/java/org/operaton/fitpub/service/IndoorActivityMigrationService.java +++ /dev/null @@ -1,161 +0,0 @@ -package org.operaton.fitpub.service; - -import com.garmin.fit.Decode; -import com.garmin.fit.MesgBroadcaster; -import com.garmin.fit.SessionMesg; -import lombok.Data; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.operaton.fitpub.model.entity.Activity; -import org.operaton.fitpub.repository.ActivityRepository; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import java.io.ByteArrayInputStream; -import java.util.List; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.concurrent.atomic.AtomicReference; - -/** - * Service for retroactively detecting and updating indoor activity flags. - * This is a data migration service to update existing activities in the database. - */ -@Service -@RequiredArgsConstructor -@Slf4j -public class IndoorActivityMigrationService { - - private final ActivityRepository activityRepository; - - /** - * Retroactively update indoor flags for all existing FIT activities. - * Re-parses stored FIT files to detect indoor activities based on SubSport field. - * - * @return number of activities updated - */ - @Transactional - public int updateIndoorFlagsForExistingActivities() { - log.info("Starting retroactive indoor activity detection for all FIT activities"); - - // Find all activities with FIT files - List fitActivities = activityRepository.findBySourceFileFormatAndRawActivityFileNotNull("FIT"); - log.info("Found {} FIT activities to analyze", fitActivities.size()); - - AtomicInteger updatedCount = new AtomicInteger(0); - AtomicInteger errorCount = new AtomicInteger(0); - - fitActivities.forEach(activity -> { - try { - IndoorDetectionResult result = detectIndoorFromFitFile(activity.getRawActivityFile()); - - boolean changed = false; - - if (result.isIndoor() != activity.getIndoor()) { - activity.setIndoor(result.isIndoor()); - changed = true; - } - - if (result.getSubSport() != null && !result.getSubSport().equals(activity.getSubSport())) { - activity.setSubSport(result.getSubSport()); - changed = true; - } - - if (result.getDetectionMethod() != null && - !result.getDetectionMethod().name().equals(activity.getIndoorDetectionMethod())) { - activity.setIndoorDetectionMethod(result.getDetectionMethod().name()); - changed = true; - } - - if (changed) { - activityRepository.save(activity); - updatedCount.incrementAndGet(); - log.info("Updated activity {} - indoor: {}, subSport: {}, method: {}", - activity.getId(), result.isIndoor(), result.getSubSport(), - result.getDetectionMethod()); - } - } catch (Exception e) { - errorCount.incrementAndGet(); - log.warn("Failed to process activity {}: {}", activity.getId(), e.getMessage()); - } - }); - - log.info("Retroactive indoor detection complete: {} activities updated, {} errors", - updatedCount.get(), errorCount.get()); - - return updatedCount.get(); - } - - /** - * Detect if a FIT file represents an indoor activity. - * Checks the SubSport field from the session message. - * - * @param fitFileBytes raw FIT file bytes - * @return detection result with indoor flag, SubSport, and detection method - */ - private IndoorDetectionResult detectIndoorFromFitFile(byte[] fitFileBytes) { - IndoorDetectionResult result = new IndoorDetectionResult(); - result.setIndoor(false); - - if (fitFileBytes == null || fitFileBytes.length == 0) { - return result; - } - - AtomicBoolean isIndoor = new AtomicBoolean(false); - AtomicReference subSport = new AtomicReference<>(null); - AtomicReference method = new AtomicReference<>(null); - - try (ByteArrayInputStream inputStream = new ByteArrayInputStream(fitFileBytes)) { - Decode decode = new Decode(); - MesgBroadcaster broadcaster = new MesgBroadcaster(decode); - - // Listen for session messages to extract SubSport - broadcaster.addListener((SessionMesg session) -> { - if (session.getSubSport() != null) { - String subSportStr = session.getSubSport().toString(); - subSport.set(subSportStr); - - String subSportUpper = subSportStr.toUpperCase(); - boolean detected = subSportUpper.contains("INDOOR") || - subSportUpper.contains("TREADMILL") || - subSportUpper.contains("VIRTUAL") || - subSportUpper.contains("TRAINER"); - if (detected) { - isIndoor.set(true); - method.set(Activity.IndoorDetectionMethod.FIT_SUBSPORT); - log.debug("Detected indoor activity from SubSport: {}", subSportStr); - } - } - }); - - // Decode the FIT file - if (!decode.checkFileIntegrity(inputStream)) { - log.warn("FIT file integrity check failed"); - return result; - } - - // Reset stream and read - inputStream.reset(); - decode.read(inputStream, broadcaster); - - result.setIndoor(isIndoor.get()); - result.setSubSport(subSport.get()); - result.setDetectionMethod(method.get()); - - } catch (Exception e) { - log.warn("Failed to parse FIT file: {}", e.getMessage()); - } - - return result; - } - - /** - * Result of indoor activity detection. - */ - @Data - private static class IndoorDetectionResult { - private boolean indoor; - private String subSport; - private Activity.IndoorDetectionMethod detectionMethod; - } -} diff --git a/src/main/resources/templates/auth/register.html b/src/main/resources/templates/auth/register.html index 8df4543..3493f3b 100644 --- a/src/main/resources/templates/auth/register.html +++ b/src/main/resources/templates/auth/register.html @@ -231,15 +231,26 @@ const registrationPasswordField = document.getElementById('registrationPasswordField'); const registrationPasswordInput = document.getElementById('registrationPassword'); - // Check if registration password is required by checking URL parameters - // If ?invite=true or REGISTRATION_PASSWORD is set, show the field - const urlParams = new URLSearchParams(window.location.search); - const showRegistrationPassword = urlParams.has('invite') || urlParams.has('code'); + // Fetch registration status from API + try { + const response = await fetch('/api/auth/registration-status'); + const data = await response.json(); - // Always show the field and let backend validate - // This simplifies the logic - if not required, backend will ignore it - registrationPasswordField.style.display = 'block'; - registrationPasswordInput.required = true; + if (data.passwordRequired) { + // Show registration password field if required + registrationPasswordField.style.display = 'block'; + registrationPasswordInput.required = true; + } else { + // Hide registration password field if not required + registrationPasswordField.style.display = 'none'; + registrationPasswordInput.required = false; + } + } catch (error) { + console.error('Failed to fetch registration status:', error); + // On error, assume password is not required + registrationPasswordField.style.display = 'none'; + registrationPasswordInput.required = false; + } // Password confirmation validation confirmPassword.addEventListener('input', function() {