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
17 changes: 8 additions & 9 deletions lib/config/routes/v2/routes_config/routes/noauth_routes.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,14 @@ final List<GoRoute> noAuthRoutes = [
GoRoute(
path: noAuthPath,
builder: (context, state) {
// Try to get secret from query parameters
final secret = state.uri.queryParameters['secret'];

if (secret == null) {
return const SwitchEndpointNoAuthView(arguments: null);
}

// Create arguments from query parameters
final args = SwitchEndpointArgs.fromJson(state.uri.queryParameters);
// A link without a secret (e.g. the app QR from the login page) is
// still a valid host switch, so parse arguments in both cases. The
// original scanned link is passed along as the `uri` parameter.
final params = {
...state.uri.queryParameters,
'uri': state.uri.queryParameters['uri'] ?? state.uri.toString(),
};
final args = SwitchEndpointArgs.fromJson(params);

return SwitchEndpointNoAuthView(arguments: args);
},
Expand Down
20 changes: 17 additions & 3 deletions lib/core/auth/login/provider/login_provider.dart
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,10 @@ part 'login_provider.g.dart';

@riverpod
class Login extends _$Login {
final _tbClient = getIt<ITbClientService>().client;
// Read the live client on every access: a QR-code endpoint switch re-creates
// the client (ITbClientService.reInit), so a reference captured at build time
// would keep pointing at the old host and fail with 401 (PROD-8200).
ThingsboardClient get _tbClient => getIt<ITbClientService>().client;
final _deviceInfoService = getIt<IDeviceInfoService>();
late final StreamSubscription<UserLoadedEvent> _listener;
final _overlayService = getIt<IOverlayService>();
Expand All @@ -32,13 +35,24 @@ class Login extends _$Login {
_listener = getIt<ICommunicationService>().on<UserLoadedEvent>().listen((
_,
) async {
await handleUserLoaded();
await _safeHandleUserLoaded();
});
ref.onDispose(() => _listener.cancel());
Future(() => handleUserLoaded());
Future(_safeHandleUserLoaded);
return const LoginState(isUserLoaded: false);
}

/// handleUserLoaded runs from fire-and-forget contexts (event bus, build):
/// a failure there (e.g. the session was invalidated while loading the
/// user) must not escape as an unhandled zone error (PROD-8200).
Future<void> _safeHandleUserLoaded() async {
try {
await handleUserLoaded();
} catch (e) {
log('handle user loaded failed: $e');
}
}

Future<void> logout() async {
if (getIt<IFirebaseService>().apps.isNotEmpty &&
state.isFullyAuthenticated()) {
Expand Down
8 changes: 8 additions & 0 deletions lib/core/auth/login/provider/oauth_provider.dart
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,19 @@ Future<LoginMobileInfo> oauth(Ref ref) async {
final tbClient = getIt<ITbClientService>().client;
final deviceInfoService = getIt<IDeviceInfoService>();
try {
// Best-effort call with a graceful fallback below: don't let the
// interceptor surface its failures as error toasts (e.g. some servers
// answer 403 for a package they don't know about) (PROD-8200).
final response = await tbClient
.getMobileAppControllerApi()
.getLoginMobileInfo(
pkgName: deviceInfoService.getApplicationId(),
platform: deviceInfoService.getPlatformType().name,
extra:
InterceptorConfig(
ignoreErrors: true,
ignoreLoading: true,
).toExtra(),
);
final loginInfo = response.data;
if (loginInfo != null) {
Expand Down
22 changes: 12 additions & 10 deletions lib/core/auth/noauth/data/model/switch_endpoint_args.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,24 @@ part 'switch_endpoint_args.g.dart';

@freezed
abstract class SwitchEndpointArgs with _$SwitchEndpointArgs {
const factory SwitchEndpointArgs(
{required String secret,
String? host,
String? ttl,
@JsonKey(fromJson: fromFluroData, toJson: uriToJson)
required Uri uri}) = _SwitchEndpointArgs;
const factory SwitchEndpointArgs({
String? secret,
String? host,
String? ttl,
@JsonKey(fromJson: fromFluroData, toJson: uriToJson) required Uri uri,
}) = _SwitchEndpointArgs;

factory SwitchEndpointArgs.fromJson(Map<String, dynamic> json) =>
_$SwitchEndpointArgsFromJson(json);
}
Uri fromFluroData(dynamic data) {
if(data is Uri) {

Uri fromFluroData(dynamic data) {
if (data is Uri) {
return data;
}
return Uri.parse(data.toString());
return Uri.parse(data.toString());
}

String uriToJson(Uri uri) {
return uri.toString();
}
}
28 changes: 14 additions & 14 deletions lib/core/auth/noauth/data/model/switch_endpoint_args.freezed.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,14 @@ import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';

import 'package:thingsboard_app/core/auth/login/provider/login_provider.dart';
import 'package:thingsboard_app/core/auth/noauth/data/model/switch_endpoint_args.dart';

import 'package:thingsboard_app/core/auth/noauth/presentation/widgets/noauth_loading_widget.dart';
import 'package:thingsboard_app/core/auth/noauth/provider/noauth_provider.dart';
import 'package:thingsboard_app/locator.dart';
import 'package:thingsboard_app/modules/main/providers/navigation_provider.dart';
import 'package:thingsboard_app/thingsboard_client.dart';
import 'package:thingsboard_app/utils/services/tb_client_service/i_tb_client_service.dart';

class SwitchEndpointNoAuthView extends HookConsumerWidget {
const SwitchEndpointNoAuthView({required this.arguments, super.key});
Expand All @@ -15,31 +19,77 @@ class SwitchEndpointNoAuthView extends HookConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
final noAuth = ref.watch(noauthProviderProvider);
useEffect(() {
ref.invalidate(noauthProviderProvider);
ref.invalidate(noauthProviderProvider);
if (arguments != null) {
ref
.read(noauthProviderProvider.notifier)
.switchEndpoint(SwitchEndpointParams(data: arguments!));
} else {
// Nothing to switch to: never leave the user on an endless spinner.
WidgetsBinding.instance.addPostFrameCallback((_) {
if (context.mounted) {
context.go('/login');
}
});
}
return null;
}, []);
ref.listen(noauthProviderProvider, (prev, next) {
if (next.error != null) {
Future.delayed(const Duration(seconds: 5), () {
if (context.mounted) {
if (!context.mounted) {
return;
}
if (context.canPop()) {
context.pop();
} else {
context.go('/login');
}
});
}
if (next.isDone) {

// A switch with a login secret ends authenticated: HomeHandler
// navigates to the home page once the user is fully loaded. A
// host-only switch (QR without a secret) ends unauthenticated: show
// the login page of the new host.
final authenticated =
getIt<ITbClientService>().client.isAuthenticated();
if (!context.mounted) {
return;
}
if (!authenticated) {
context.go('/login');
} else if (ref.read(loginProvider).isFullyAuthenticated()) {
// Already fully logged in (e.g. the same QR was scanned twice):
// HomeHandler won't see a state transition, navigate ourselves.
final navigation = ref.read(navigationProvider);
if (navigation.bottomBarPages.isNotEmpty) {
context.go(navigation.bottomBarPages.first.path);
}
} else {
// HomeHandler navigates once the user finishes loading. If that
// never happens (e.g. the new session gets rejected), don't leave
// the user on the spinner forever.
Future.delayed(const Duration(seconds: 12), () {
if (context.mounted) {
context.go('/login');
}
});
}
}
});
return Scaffold(
body: SafeArea(
child: Builder(
builder: (context) {
if (noAuth.error != null) {
final error = noAuth.error;
// Never render error.toString(): for ThingsboardError it
// includes the captured stacktrace (PROD-8200).
final message =
error is ThingsboardError
? error.message ?? noAuth.message
: noAuth.message;
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: Center(
Expand All @@ -49,8 +99,7 @@ class SwitchEndpointNoAuthView extends HookConsumerWidget {
const Icon(Icons.error, color: Colors.red, size: 50),
const SizedBox(height: 10),
Text(
noAuth.error.toString(),
//S.of(context).somethingWentWrongRollback,
message,
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
fontWeight: FontWeight.w500,
Expand Down
Loading