Add Jarvis Google Calendar OAuth broker
This commit is contained in:
+115
-182
@@ -25,15 +25,7 @@ class AccountsPage extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _AccountsPageState extends State<AccountsPage> {
|
||||
static const calendarScopes = [
|
||||
'https://www.googleapis.com/auth/calendar.readonly',
|
||||
];
|
||||
|
||||
final googleSignIn = GoogleSignIn.instance;
|
||||
List<GoogleCalendarInfo> googleCalendars = [];
|
||||
GoogleSignInAccount? signedInAccount;
|
||||
String? runtimeServerClientId;
|
||||
bool initialized = false;
|
||||
bool loadingCalendars = false;
|
||||
bool syncing = false;
|
||||
String? statusMessage;
|
||||
@@ -41,209 +33,156 @@ class _AccountsPageState extends State<AccountsPage> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadGoogleAppConfigAndInitialize();
|
||||
_loadGoogleConnectionStatus();
|
||||
}
|
||||
|
||||
GoogleCalendarConnection? get connection =>
|
||||
widget.connections.isEmpty ? null : widget.connections.first;
|
||||
|
||||
Future<void> _loadGoogleAppConfigAndInitialize() async {
|
||||
setState(() {
|
||||
loadingCalendars = true;
|
||||
statusMessage = 'Google napt\u00e1rkapcsolat el\u0151k\u00e9sz\u00edt\u00e9se...';
|
||||
});
|
||||
|
||||
runtimeServerClientId = widget.serverClientId.trim().isNotEmpty
|
||||
? widget.serverClientId.trim()
|
||||
: await _loadGoogleServerClientId();
|
||||
if (!mounted) return;
|
||||
await _initializeGoogle();
|
||||
String? get _workspaceToken {
|
||||
final token = widget.jarvisSettings.workspaceToken?.trim();
|
||||
return token == null || token.isEmpty ? null : token;
|
||||
}
|
||||
|
||||
Future<String?> _loadGoogleServerClientId() async {
|
||||
if (googleCalendarServerClientId.trim().isNotEmpty) {
|
||||
return googleCalendarServerClientId.trim();
|
||||
Map<String, String> get _jarvisHeaders {
|
||||
final headers = <String, String>{
|
||||
'X-Otthon-Token': widget.jarvisSettings.apiToken.trim(),
|
||||
};
|
||||
final workspaceToken = _workspaceToken;
|
||||
if (workspaceToken != null) {
|
||||
headers['X-Workspace-Token'] = workspaceToken;
|
||||
}
|
||||
return headers;
|
||||
}
|
||||
|
||||
final apiToken = widget.jarvisSettings.apiToken.trim();
|
||||
if (apiToken.isEmpty) {
|
||||
return null;
|
||||
Uri _jarvisUri(String path, [Map<String, String?> query = const {}]) {
|
||||
final baseUri = Uri.parse(normalizeJarvisUrl(widget.jarvisSettings.apiUrl));
|
||||
return baseUri.replace(path: path, queryParameters: query.isEmpty ? null : query);
|
||||
}
|
||||
|
||||
String? _jarvisConfigError() {
|
||||
if (widget.jarvisSettings.apiToken.trim().isEmpty) {
|
||||
return 'Add meg a JARVIS API tokent a fogasker\u00e9k alatt.';
|
||||
}
|
||||
if (_workspaceToken == null) {
|
||||
return 'Add meg a k\u00f6z\u00f6s munkat\u00e9r tokent a fogasker\u00e9k alatt.';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
Future<void> _loadGoogleConnectionStatus() async {
|
||||
setState(() {
|
||||
loadingCalendars = true;
|
||||
statusMessage = 'JARVIS Google kapcsolat ellen\u0151rz\u00e9se...';
|
||||
});
|
||||
|
||||
final configError = _jarvisConfigError();
|
||||
if (configError != null) {
|
||||
setState(() {
|
||||
loadingCalendars = false;
|
||||
statusMessage = configError;
|
||||
});
|
||||
return;
|
||||
}
|
||||
try {
|
||||
final baseUri = Uri.parse(normalizeJarvisUrl(widget.jarvisSettings.apiUrl));
|
||||
final uri = baseUri.replace(path: '/api/app-config', query: '');
|
||||
final response = await http
|
||||
.get(uri, headers: {'X-Otthon-Token': apiToken})
|
||||
.get(_jarvisUri('/api/google/oauth/status'), headers: _jarvisHeaders)
|
||||
.timeout(const Duration(seconds: 10));
|
||||
if (response.statusCode != 200) {
|
||||
return null;
|
||||
throw Exception('JARVIS status ${response.statusCode}: ${response.body}');
|
||||
}
|
||||
final payload = jsonDecode(response.body);
|
||||
if (payload is! Map<String, dynamic>) {
|
||||
return null;
|
||||
throw Exception('Hib\u00e1s JARVIS v\u00e1lasz.');
|
||||
}
|
||||
return payload['googleCalendarServerClientId'] as String?;
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _initializeGoogle() async {
|
||||
setState(() {
|
||||
loadingCalendars = true;
|
||||
statusMessage = 'Google kapcsolat el\u0151k\u00e9sz\u00edt\u00e9se...';
|
||||
});
|
||||
|
||||
try {
|
||||
await _ensureGoogleInitialized();
|
||||
initialized = true;
|
||||
final lightweight = googleSignIn.attemptLightweightAuthentication();
|
||||
final account = lightweight == null ? null : await lightweight;
|
||||
if (!mounted) return;
|
||||
if (account != null) {
|
||||
signedInAccount = account;
|
||||
await _loadGoogleCalendars(promptIfNecessary: false);
|
||||
final connected = payload['connected'] == true;
|
||||
final email = payload['email'] as String?;
|
||||
if (connected) {
|
||||
final current = connection;
|
||||
widget.onChanged([
|
||||
GoogleCalendarConnection(
|
||||
id: current?.id ?? uniqueId(),
|
||||
accountEmail: email ?? current?.accountEmail ?? 'Google fi\u00f3k',
|
||||
enabledCalendarIds: current?.enabledCalendarIds ?? const [],
|
||||
calendarPersonIds: current?.calendarPersonIds ?? const {},
|
||||
lastSync: current?.lastSync,
|
||||
),
|
||||
]);
|
||||
await _loadGoogleCalendars();
|
||||
} else {
|
||||
setState(() {
|
||||
loadingCalendars = false;
|
||||
statusMessage =
|
||||
'Jelentkezz be Google fi\u00f3kkal, majd v\u00e1laszd ki, mely napt\u00e1rak szinkroniz\u00e1ljanak.';
|
||||
'Google Calendar m\u00e9g nincs \u00f6sszek\u00f6tve. Ind\u00edtsd az \u00f6sszek\u00f6t\u00e9st JARVIS-on kereszt\u00fcl.';
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
loadingCalendars = false;
|
||||
statusMessage = _friendlyGoogleError(
|
||||
'Google inicializ\u00e1l\u00e1s nem siker\u00fclt',
|
||||
error,
|
||||
);
|
||||
statusMessage = 'JARVIS Google kapcsolat nem el\u00e9rhet\u0151: $error';
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Future<Map<String, String>> _googleHeaders({
|
||||
bool promptIfNecessary = true,
|
||||
}) async {
|
||||
final account = signedInAccount;
|
||||
final headers = account == null
|
||||
? await googleSignIn.authorizationClient.authorizationHeaders(
|
||||
calendarScopes,
|
||||
promptIfNecessary: promptIfNecessary,
|
||||
)
|
||||
: await account.authorizationClient.authorizationHeaders(
|
||||
calendarScopes,
|
||||
promptIfNecessary: promptIfNecessary,
|
||||
);
|
||||
if (headers == null) {
|
||||
throw Exception('A Google Calendar jogosults\u00e1g nincs megadva.');
|
||||
}
|
||||
return headers;
|
||||
}
|
||||
|
||||
Future<void> _signInGoogle() async {
|
||||
Future<void> _startGoogleConnection() async {
|
||||
setState(() {
|
||||
loadingCalendars = true;
|
||||
statusMessage = 'Google bejelentkez\u00e9s...';
|
||||
statusMessage = 'Google \u00f6sszek\u00f6t\u00e9s ind\u00edt\u00e1sa JARVIS-on...';
|
||||
});
|
||||
|
||||
try {
|
||||
if (!initialized) {
|
||||
runtimeServerClientId = widget.serverClientId.trim().isNotEmpty
|
||||
? widget.serverClientId.trim()
|
||||
: await _loadGoogleServerClientId();
|
||||
await _ensureGoogleInitialized();
|
||||
initialized = true;
|
||||
}
|
||||
final account = await googleSignIn.authenticate();
|
||||
signedInAccount = account;
|
||||
await account.authorizationClient.authorizeScopes(calendarScopes);
|
||||
await _loadGoogleCalendars(promptIfNecessary: true);
|
||||
} catch (error) {
|
||||
if (!mounted) return;
|
||||
final friendlyMessage = _friendlyGoogleError(
|
||||
'Google bejelentkez\u00e9s nem siker\u00fclt',
|
||||
error,
|
||||
);
|
||||
final configError = _jarvisConfigError();
|
||||
if (configError != null) {
|
||||
setState(() {
|
||||
loadingCalendars = false;
|
||||
statusMessage = friendlyMessage;
|
||||
statusMessage = configError;
|
||||
});
|
||||
await _showGoogleErrorDialog(friendlyMessage);
|
||||
}
|
||||
}
|
||||
|
||||
String _friendlyGoogleError(String prefix, Object error) {
|
||||
final raw = error.toString();
|
||||
if (raw.contains('serverClientId must be provided') ||
|
||||
raw.contains('CredentialManager requires a serverClientId')) {
|
||||
return '$prefix: hi\u00e1nyzik az app Google konfigur\u00e1ci\u00f3ja. '
|
||||
'A bel\u00e9p\u00e9si ablakos m\u0171k\u00f6d\u00e9shez tedd be az android/app/google-services.json f\u00e1jlt, '
|
||||
'amely tartalmaz Web application OAuth client ID-t, majd buildeld \u00fajra az APK-t.';
|
||||
}
|
||||
if (raw.contains('Account reauth failed')) {
|
||||
return '$prefix: a Google fi\u00f3k \u00fajrahiteles\u00edt\u00e9se nem siker\u00fclt. '
|
||||
'Pr\u00f3b\u00e1ld meg a tableten a Google fi\u00f3kot friss\u00edteni/bejelentkeztetni a Be\u00e1ll\u00edt\u00e1sokban, '
|
||||
'vagy t\u00f6r\u00f6ld az app Google hozz\u00e1f\u00e9r\u00e9s\u00e9t, majd jelentkezz be \u00fajra.';
|
||||
}
|
||||
return '$prefix: $raw';
|
||||
}
|
||||
|
||||
Future<void> _showGoogleErrorDialog(String message) async {
|
||||
if (!mounted) return;
|
||||
await showDialog<void>(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('Google bel\u00e9p\u00e9s nem indult el'),
|
||||
content: Text(
|
||||
'$message\n\n'
|
||||
'A v\u00e1rt m\u0171k\u00f6d\u00e9s az, hogy a Google saj\u00e1t bel\u00e9p\u00e9si/enged\u00e9lyez\u00e9si ablaka ny\u00edlik meg. '
|
||||
'Ehhez az appnak el\u0151re regisztr\u00e1lt Google konfigur\u00e1ci\u00f3 kell, \u00e9s az eszk\u00f6z\u00f6n Google Play Services / Google fi\u00f3k is sz\u00fcks\u00e9ges.',
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text('Rendben'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _ensureGoogleInitialized() async {
|
||||
final clientId = runtimeServerClientId;
|
||||
if (clientId == null || clientId.isEmpty) {
|
||||
await googleSignIn.initialize();
|
||||
return;
|
||||
}
|
||||
|
||||
await googleSignIn.initialize(
|
||||
serverClientId: clientId,
|
||||
);
|
||||
try {
|
||||
final response = await http
|
||||
.get(_jarvisUri('/api/google/oauth/start'), headers: _jarvisHeaders)
|
||||
.timeout(const Duration(seconds: 10));
|
||||
if (response.statusCode != 200) {
|
||||
throw Exception('JARVIS OAuth ${response.statusCode}: ${response.body}');
|
||||
}
|
||||
final payload = jsonDecode(response.body);
|
||||
if (payload is! Map<String, dynamic>) throw Exception('Hib\u00e1s JARVIS v\u00e1lasz.');
|
||||
final url = payload['authorizationUrl'] as String?;
|
||||
if (url == null || url.isEmpty) throw Exception('Hi\u00e1nyzik az OAuth URL.');
|
||||
final launched = await launchUrl(
|
||||
Uri.parse(url),
|
||||
mode: LaunchMode.externalApplication,
|
||||
);
|
||||
if (!launched) throw Exception('Nem siker\u00fclt megnyitni a b\u00f6ng\u00e9sz\u0151t.');
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
loadingCalendars = false;
|
||||
statusMessage =
|
||||
'A Google enged\u00e9lyez\u00e9s b\u00f6ng\u00e9sz\u0151ben megny\u00edlt. Ha k\u00e9sz, t\u00e9rj vissza ide \u00e9s friss\u00edtsd a napt\u00e1rlist\u00e1t.';
|
||||
});
|
||||
} catch (error) {
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
loadingCalendars = false;
|
||||
statusMessage = 'Google \u00f6sszek\u00f6t\u00e9s nem indult el: $error';
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _loadGoogleCalendars({bool promptIfNecessary = true}) async {
|
||||
Future<void> _loadGoogleCalendars() async {
|
||||
setState(() {
|
||||
loadingCalendars = true;
|
||||
statusMessage = 'Google napt\u00e1rak lek\u00e9r\u00e9se...';
|
||||
});
|
||||
|
||||
try {
|
||||
final headers = await _googleHeaders(
|
||||
promptIfNecessary: promptIfNecessary,
|
||||
);
|
||||
final uri = Uri.https(
|
||||
'www.googleapis.com',
|
||||
'/calendar/v3/users/me/calendarList',
|
||||
{'minAccessRole': 'reader', 'showHidden': 'true'},
|
||||
);
|
||||
final response = await http
|
||||
.get(uri, headers: headers)
|
||||
.get(_jarvisUri('/api/google/calendars'), headers: _jarvisHeaders)
|
||||
.timeout(const Duration(seconds: 15));
|
||||
if (response.statusCode != 200) {
|
||||
throw Exception(
|
||||
'CalendarList API ${response.statusCode}: ${response.body}',
|
||||
'JARVIS CalendarList ${response.statusCode}: ${response.body}',
|
||||
);
|
||||
}
|
||||
|
||||
@@ -263,16 +202,15 @@ class _AccountsPageState extends State<AccountsPage> {
|
||||
return a.name.compareTo(b.name);
|
||||
});
|
||||
|
||||
final account = signedInAccount;
|
||||
final current = connection;
|
||||
if (account != null) {
|
||||
if (current == null) {
|
||||
widget.onChanged([
|
||||
GoogleCalendarConnection(
|
||||
id: current?.id ?? uniqueId(),
|
||||
accountEmail: account.email,
|
||||
enabledCalendarIds: current?.enabledCalendarIds ?? const [],
|
||||
calendarPersonIds: current?.calendarPersonIds ?? const {},
|
||||
lastSync: current?.lastSync,
|
||||
id: uniqueId(),
|
||||
accountEmail: 'Google Calendar',
|
||||
enabledCalendarIds: const [],
|
||||
calendarPersonIds: const {},
|
||||
lastSync: null,
|
||||
),
|
||||
]);
|
||||
}
|
||||
@@ -312,9 +250,8 @@ class _AccountsPageState extends State<AccountsPage> {
|
||||
|
||||
try {
|
||||
if (googleCalendars.isEmpty) {
|
||||
await _loadGoogleCalendars(promptIfNecessary: true);
|
||||
await _loadGoogleCalendars();
|
||||
}
|
||||
final headers = await _googleHeaders(promptIfNecessary: true);
|
||||
final selectedCalendars = googleCalendars
|
||||
.where((calendar) => current.enabledCalendarIds.contains(calendar.id))
|
||||
.toList();
|
||||
@@ -329,19 +266,16 @@ class _AccountsPageState extends State<AccountsPage> {
|
||||
final end = start.add(const Duration(days: 180));
|
||||
|
||||
for (final calendar in selectedCalendars) {
|
||||
final uri = Uri.https(
|
||||
'www.googleapis.com',
|
||||
'/calendar/v3/calendars/${Uri.encodeComponent(calendar.id)}/events',
|
||||
final uri = _jarvisUri(
|
||||
'/api/google/events',
|
||||
{
|
||||
'singleEvents': 'true',
|
||||
'orderBy': 'startTime',
|
||||
'calendarId': calendar.id,
|
||||
'timeMin': start.toUtc().toIso8601String(),
|
||||
'timeMax': end.toUtc().toIso8601String(),
|
||||
'maxResults': '2500',
|
||||
},
|
||||
);
|
||||
final response = await http
|
||||
.get(uri, headers: headers)
|
||||
.get(uri, headers: _jarvisHeaders)
|
||||
.timeout(const Duration(seconds: 20));
|
||||
if (response.statusCode != 200) {
|
||||
throw Exception('${calendar.name}: Events API ${response.statusCode}');
|
||||
@@ -396,13 +330,13 @@ class _AccountsPageState extends State<AccountsPage> {
|
||||
.length;
|
||||
|
||||
return PageFrame(
|
||||
eyebrow: 'Google Calendar API szinkron',
|
||||
eyebrow: 'JARVIS Google Calendar szinkron',
|
||||
title: 'Google napt\u00e1rak',
|
||||
action: FilledButton.icon(
|
||||
onPressed: loadingCalendars ? null : _signInGoogle,
|
||||
icon: const Icon(Icons.login),
|
||||
onPressed: loadingCalendars ? null : _startGoogleConnection,
|
||||
icon: const Icon(Icons.open_in_browser),
|
||||
label: Text(
|
||||
hasConnection ? 'Fi\u00f3k v\u00e1lt\u00e1sa' : 'Google bel\u00e9p\u00e9s',
|
||||
hasConnection ? 'Google \u00f6sszek\u00f6t\u00e9s \u00fajranyit\u00e1sa' : 'Google \u00f6sszek\u00f6t\u00e9s',
|
||||
),
|
||||
),
|
||||
child: ListView(
|
||||
@@ -491,24 +425,23 @@ class _AccountsPageState extends State<AccountsPage> {
|
||||
OutlinedButton.icon(
|
||||
onPressed: loadingCalendars
|
||||
? null
|
||||
: () => _loadGoogleCalendars(promptIfNecessary: true),
|
||||
: _loadGoogleConnectionStatus,
|
||||
icon: const Icon(Icons.refresh),
|
||||
label: const Text('Google napt\u00e1rlista friss\u00edt\u00e9se'),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
OutlinedButton.icon(
|
||||
onPressed: () async {
|
||||
await googleSignIn.disconnect();
|
||||
widget.onChanged([]);
|
||||
widget.onSyncedEventsChanged([]);
|
||||
setState(() {
|
||||
signedInAccount = null;
|
||||
googleCalendars = [];
|
||||
statusMessage = 'Google kapcsolat lev\u00e1lasztva.';
|
||||
statusMessage =
|
||||
'Helyi Google kapcsolat t\u00f6r\u00f6lve. A JARVIS token szerveren marad; \u00faj \u00f6sszek\u00f6t\u00e9ssel fel\u00fcl\u00edrhat\u00f3.';
|
||||
});
|
||||
},
|
||||
icon: const Icon(Icons.link_off),
|
||||
label: const Text('Google kapcsolat lev\u00e1laszt\u00e1sa'),
|
||||
label: const Text('Helyi Google cache t\u00f6rl\u00e9se'),
|
||||
),
|
||||
],
|
||||
if (statusMessage != null) ...[
|
||||
|
||||
+1
-1
@@ -7,11 +7,11 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:geocoding/geocoding.dart' as geocoding;
|
||||
import 'package:geolocator/geolocator.dart';
|
||||
import 'package:google_sign_in/google_sign_in.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:mobile_scanner/mobile_scanner.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:timezone/data/latest.dart' as timezone_data;
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
part 'data.dart';
|
||||
part 'app_shell.dart';
|
||||
|
||||
Reference in New Issue
Block a user