Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 66 additions & 0 deletions lib/helpers/sanity_checks.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/*
* This file is part of wger Workout Manager <https://github.com/wger-project>.
* Copyright (c) 2025 wger Team
*
* wger Workout Manager is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*/

import 'dart:convert';
import 'package:http/http.dart' as http;

/// Checks if the server's reverse proxy is configured correctly
/// by comparing pagination URLs with the base URL domain.
Future<SanityCheckResult> checkServerPaginationUrls({
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would move this to the auth provider (while it's not strictly anything to do with authentication is related enough, plus we already have the min application version check there)

required String baseUrl,
required String token,
http.Client? client,
}) async {
final httpClient = client ?? http.Client();

try {
final baseUri = Uri.parse(baseUrl);
final expectedHost = baseUri.host;
final expectedScheme = baseUri.scheme;

final response = await httpClient.get(
Uri.parse('$baseUrl/api/v2/exercise/?limit=1'),
headers: {
'Authorization': 'Token $token',
'Accept': 'application/json',
},
);

if (response.statusCode != 200) {
return const SanityCheckResult(isValid: false);
}

final data = jsonDecode(response.body) as Map<String, dynamic>;
final nextUrl = data['next'] as String?;

if (nextUrl == null) {
return const SanityCheckResult(isValid: true);
}

final nextUri = Uri.parse(nextUrl);

if (nextUri.host != expectedHost || nextUri.scheme != expectedScheme) {
return const SanityCheckResult(isValid: false);
}

return const SanityCheckResult(isValid: true);
} catch (e) {
return const SanityCheckResult(isValid: false);
}
}

/// Result of a server sanity check
class SanityCheckResult {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

a boolean might be enough 😄 This can be useful if we added more checks, but at the moment there's nothing planed

const SanityCheckResult({
required this.isValid,
});

final bool isValid;
}
16 changes: 16 additions & 0 deletions lib/l10n/app_en.arb
Original file line number Diff line number Diff line change
Expand Up @@ -1136,5 +1136,21 @@
"addToOpenFoodFacts": "Add to Open Food Facts",
"@addToOpenFoodFacts": {
"description": "Label shown as the clickable link to go on Open Food Facts"
},
"serverConfigIssueTitle": "Server Configuration Issue",
"@serverConfigIssueTitle": {
"description": "Title of the dialog shown when server misconfiguration is detected"
},
"serverConfigIssueMessage": "Server misconfiguration detected, headers are not being passed correctly, please consult the documentation.",
"@serverConfigIssueMessage": {
"description": "Message explaining server misconfiguration and linking to documentation"
},
"understand": "I understand",
"@understand": {
"description": "Button label to acknowledge and dismiss the server configuration warning"
},
"viewDocumentation": "View documentation",
"@viewDocumentation": {
"description": "Button label to open the documentation about server configuration"
}
}
24 changes: 24 additions & 0 deletions lib/providers/auth.dart
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import 'package:package_info_plus/package_info_plus.dart';
import 'package:version/version.dart';
import 'package:wger/core/exceptions/http_exception.dart';
import 'package:wger/helpers/consts.dart';
import 'package:wger/helpers/sanity_checks.dart';
import 'package:wger/helpers/shared_preferences.dart';

import 'helpers.dart';
Expand All @@ -53,6 +54,8 @@ class AuthProvider with ChangeNotifier {
PackageInfo? applicationVersion;
Map<String, String> metadata = {};
AuthState state = AuthState.loggedOut;
bool _serverConfigWarning = false;
bool get serverConfigWarning => _serverConfigWarning;

static const MIN_APP_VERSION_URL = 'min-app-version';
static const SERVER_VERSION_URL = 'version';
Expand All @@ -66,6 +69,12 @@ class AuthProvider with ChangeNotifier {
this.client = client ?? http.Client();
}

/// Clear the server config warnings
void clearServerConfigWarning() {
_serverConfigWarning = false;
notifyListeners();
}

/// flag to indicate that the application has successfully loaded all initial data
bool dataInit = false;

Expand Down Expand Up @@ -192,6 +201,21 @@ class AuthProvider with ChangeNotifier {
return LoginActions.update;
}

// Check server configuration sanity
try {
final sanityCheck = await checkServerPaginationUrls(
baseUrl: serverUrl,
token: token!,
);

if (!sanityCheck.isValid) {
_serverConfigWarning = true;
notifyListeners();
}
} catch (e) {
_logger.info('Sanity check error: $e');
}

// Log user in
state = AuthState.loggedIn;
notifyListeners();
Expand Down
12 changes: 12 additions & 0 deletions lib/screens/auth_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import 'package:wger/widgets/auth/email_field.dart';
import 'package:wger/widgets/auth/password_field.dart';
import 'package:wger/widgets/auth/server_field.dart';
import 'package:wger/widgets/auth/username_field.dart';
import 'package:wger/widgets/core/server_config_warning_dialog.dart';

import '../providers/auth.dart';

Expand Down Expand Up @@ -206,6 +207,17 @@ class _AuthCardState extends State<AuthCard> {
);
return;
}

if (context.mounted && res == LoginActions.proceed) {
final authProvider = context.read<AuthProvider>();
if (authProvider.serverConfigWarning) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be more consistent to use something like AuthState.updateRequired here

if (context.mounted) {
showServerConfigWarning(context);
authProvider.clearServerConfigWarning();
}
}
}

if (context.mounted) {
setState(() {
_isLoading = false;
Expand Down
48 changes: 48 additions & 0 deletions lib/widgets/core/server_config_warning_dialog.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/*
* This file is part of wger Workout Manager <https://github.com/wger-project>.
* Copyright (c) 2025 wger Team
*
* wger Workout Manager is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*/

import 'package:flutter/material.dart';
import 'package:wger/helpers/misc.dart';
import 'package:wger/l10n/generated/app_localizations.dart';

/// Shows a warning dialog when server misconfiguration is detected
void showServerConfigWarning(BuildContext context) {
final i18n = AppLocalizations.of(context);

showDialog(
context: context,
builder: (context) => AlertDialog(
title: Row(
children: [
const Icon(Icons.warning, color: Colors.orange, size: 28),
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

better use something like Theme.of(context).colorScheme.error instead of hard coding the color, then this updates automatically if we change something there

const SizedBox(width: 12),
Expanded(child: Text(i18n.serverConfigIssueTitle)),
],
),
content: Text(i18n.serverConfigIssueMessage),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text(i18n.understand),
),
TextButton(
onPressed: () {
launchURL(
'https://wger.readthedocs.io/en/latest/administration/errors.html#wrong-pagination-links',
context,
);
Navigator.pop(context);
},
child: Text(i18n.viewDocumentation),
),
],
),
);
}
Loading