- Add 10-second periodic auto-sync to CycleEntriesNotifier - Fix husband_devotional_screen: use partnerId for isConnected check, navigate to SharingSettingsScreen instead of legacy mock dialog - Remove obsolete _showConnectDialog method and mock data import - Update husband_settings_screen: show 'Partner Settings' with linked partner name when connected - Add SharingSettingsScreen: Pad Supplies toggle (disabled when pad tracking off), Intimacy always enabled - Add CORS OPTIONS handler to backend server - Add _ensureServerRegistration for reliable partner linking - Add copy button to Invite Partner dialog - Dynamic base URL for web (uses window.location.hostname)
199 lines
6.3 KiB
Dart
199 lines
6.3 KiB
Dart
import 'dart:io';
|
|
import 'dart:convert';
|
|
import 'package:shelf/shelf.dart';
|
|
import 'package:shelf/shelf_io.dart' as io;
|
|
import 'package:shelf_router/shelf_router.dart';
|
|
import 'package:tracker_backend/database.dart';
|
|
|
|
void main(List<String> args) async {
|
|
// Use port 8090 as requested
|
|
final port = 8090;
|
|
final db = TrackerDatabase();
|
|
|
|
final app = Router();
|
|
|
|
app.get('/', (Request request) {
|
|
return Response.ok('Tracker Sync Server Running');
|
|
});
|
|
|
|
// Handle CORS Preflight (OPTIONS) for all routes
|
|
app.add('OPTIONS', r'/<ignored|.*>', (Request request) {
|
|
return Response.ok('', headers: {
|
|
'Access-Control-Allow-Origin': '*',
|
|
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
|
|
'Access-Control-Allow-Headers': 'Origin, Content-Type',
|
|
});
|
|
});
|
|
|
|
// Sync Push Endpoint
|
|
// Expects JSON: { "userId": "...", "entries": [], "teachingPlans": [], "prayerRequests": [] }
|
|
app.post('/sync/push', (Request request) async {
|
|
try {
|
|
final payload = await request.readAsString();
|
|
final data = jsonDecode(payload);
|
|
final userId = data['userId'];
|
|
|
|
// 0. Update User Record
|
|
// We expect 'userDetails' in payload now, but handle legacy
|
|
if (data.containsKey('userDetails')) {
|
|
db.upsertUser(userId, data['userDetails']);
|
|
} else {
|
|
// Fallback: create basic record if missing?
|
|
// Or just let it fail if we need linking.
|
|
// We'll trust frontend sends it now.
|
|
}
|
|
|
|
// 1. Cycle Entries
|
|
final entries = data['entries'] as List? ?? [];
|
|
for (var entry in entries) {
|
|
db.upsertCycleEntry(userId, entry);
|
|
}
|
|
|
|
// 2. Teaching Plans
|
|
final teachingPlans = data['teachingPlans'] as List? ?? [];
|
|
for (var plan in teachingPlans) {
|
|
db.upsertTeachingPlan(userId, plan);
|
|
}
|
|
|
|
// 3. Prayer Requests
|
|
final prayerRequests = data['prayerRequests'] as List? ?? [];
|
|
for (var req in prayerRequests) {
|
|
db.upsertPrayerRequest(userId, req);
|
|
}
|
|
|
|
print(
|
|
'Synced for $userId: ${entries.length} entries, ${teachingPlans.length} plans, ${prayerRequests.length} prayers');
|
|
|
|
return Response.ok(jsonEncode({'status': 'success'}));
|
|
} catch (e) {
|
|
print('Sync Error: $e');
|
|
return Response.internalServerError(body: 'Sync Failed: $e');
|
|
}
|
|
});
|
|
|
|
// Preview Link Endpoint
|
|
// POST /sync/preview
|
|
// Body: { "targetId": "..." }
|
|
// Returns: { "name": "...", "role": "..." }
|
|
app.post('/sync/preview', (Request request) async {
|
|
try {
|
|
final payload = await request.readAsString();
|
|
final data = jsonDecode(payload);
|
|
final targetId = data['targetId'];
|
|
|
|
if (targetId == null) {
|
|
return Response.badRequest(body: 'Missing targetId');
|
|
}
|
|
|
|
final targetUser = db.getUser(targetId);
|
|
if (targetUser == null) {
|
|
return Response.notFound(jsonEncode({'error': 'Partner ID not found'}));
|
|
}
|
|
|
|
return Response.ok(jsonEncode({
|
|
'status': 'success',
|
|
'partnerName': targetUser['name'],
|
|
'partnerRole': targetUser['role'],
|
|
}));
|
|
} catch (e) {
|
|
print('Preview Error: $e');
|
|
return Response.internalServerError(body: 'Preview Failed: $e');
|
|
}
|
|
});
|
|
|
|
// Link Endpoint
|
|
// POST /sync/link
|
|
// Body: { "userId": "...", "targetId": "..." }
|
|
app.post('/sync/link', (Request request) async {
|
|
try {
|
|
final payload = await request.readAsString();
|
|
final data = jsonDecode(payload);
|
|
final userId = data['userId'];
|
|
final targetId = data['targetId'];
|
|
|
|
if (userId == null || targetId == null) {
|
|
return Response.badRequest(body: 'Missing userId or targetId');
|
|
}
|
|
|
|
// Verify target exists
|
|
final targetUser = db.getUser(targetId);
|
|
if (targetUser == null) {
|
|
return Response.notFound(jsonEncode({'error': 'Partner ID not found'}));
|
|
}
|
|
|
|
// Perform Link
|
|
db.linkPartners(userId, targetId);
|
|
|
|
// Return partner name/info
|
|
return Response.ok(jsonEncode({
|
|
'status': 'success',
|
|
'partnerName': targetUser['name'],
|
|
'partnerEmail': targetUser['email']
|
|
}));
|
|
} catch (e) {
|
|
print('Link Error: $e');
|
|
return Response.internalServerError(body: 'Link Failed: $e');
|
|
}
|
|
});
|
|
|
|
// Pull Endpoint
|
|
// GET /sync/pull?userId=...&partnerId=...
|
|
app.get('/sync/pull', (Request request) {
|
|
final userId = request.url.queryParameters['userId'];
|
|
final partnerId = request.url.queryParameters['partnerId'];
|
|
|
|
if (userId == null) return Response.badRequest(body: 'Missing userId');
|
|
|
|
// 1. Get My Data
|
|
final myEntries = db.getCycleEntries(userId);
|
|
final myPlans = db.getTeachingPlans(userId); // Plans I created
|
|
final myPrayers = db.getPrayerRequests(userId); // Prayers I created
|
|
|
|
// 2. Get Partner Data (if linked)
|
|
List<Map<String, dynamic>> partnerEntries = [];
|
|
List<Map<String, dynamic>> partnerPlans = [];
|
|
List<Map<String, dynamic>> partnerPrayers = [];
|
|
|
|
if (partnerId != null && partnerId.isNotEmpty) {
|
|
// Fetch partner's cycle entries
|
|
partnerEntries = db.getCycleEntries(partnerId);
|
|
|
|
// Fetch plans created by partner (e.g. Husband created plans for Wife to see)
|
|
partnerPlans = db.getTeachingPlans(partnerId);
|
|
|
|
// Fetch partner's prayer requests
|
|
partnerPrayers = db.getPrayerRequests(partnerId);
|
|
}
|
|
|
|
// 3. Get User Profile (to sync back partner changes)
|
|
final userProfile = db.getUser(userId);
|
|
|
|
// Combine Data
|
|
final responseData = {
|
|
'entries': [...myEntries, ...partnerEntries],
|
|
// Teaching Plans: I want to see my own AND my partner's
|
|
'teachingPlans': [...myPlans, ...partnerPlans],
|
|
// Prayer Requests: I want to see my own AND my partner's
|
|
'prayerRequests': [...myPrayers, ...partnerPrayers],
|
|
if (userProfile != null) 'userProfile': userProfile,
|
|
};
|
|
|
|
return Response.ok(jsonEncode(responseData));
|
|
});
|
|
|
|
// Enable CORS
|
|
final handler = Pipeline().addMiddleware((innerHandler) {
|
|
return (request) async {
|
|
final response = await innerHandler(request);
|
|
return response.change(headers: {
|
|
'Access-Control-Allow-Origin': '*',
|
|
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
|
|
'Access-Control-Allow-Headers': 'Origin, Content-Type',
|
|
});
|
|
};
|
|
}).addHandler(app);
|
|
|
|
final server = await io.serve(handler, InternetAddress.anyIPv4, port);
|
|
print('Server running on localhost:${server.port}');
|
|
}
|