Two factor auth support
This commit is contained in:
@@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:thingsboard_app/config/routes/router.dart';
|
||||
import 'package:thingsboard_app/core/auth/login/reset_password_request_page.dart';
|
||||
import 'package:thingsboard_app/core/auth/login/two_factor_authentication_page.dart';
|
||||
import 'package:thingsboard_app/core/context/tb_context.dart';
|
||||
|
||||
import 'login/login_page.dart';
|
||||
@@ -18,6 +19,11 @@ class AuthRoutes extends TbRoutes {
|
||||
return ResetPasswordRequestPage(tbContext);
|
||||
});
|
||||
|
||||
late var twoFactorAuthenticationHandler = Handler(
|
||||
handlerFunc: (BuildContext? context, Map<String, dynamic> params) {
|
||||
return TwoFactorAuthenticationPage(tbContext);
|
||||
});
|
||||
|
||||
AuthRoutes(TbContext tbContext) : super(tbContext);
|
||||
|
||||
@override
|
||||
@@ -25,5 +31,6 @@ class AuthRoutes extends TbRoutes {
|
||||
router.define("/login", handler: loginHandler);
|
||||
router.define("/login/resetPasswordRequest",
|
||||
handler: resetPasswordRequestHandler);
|
||||
router.define("/login/mfa", handler: twoFactorAuthenticationHandler);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
import 'package:flutter_form_builder/flutter_form_builder.dart';
|
||||
import 'package:form_builder_validators/form_builder_validators.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
@@ -38,6 +39,11 @@ class _LoginPageState extends TbPageState<LoginPage> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
if (tbClient.isPreVerificationToken()) {
|
||||
SchedulerBinding.instance.addPostFrameCallback((_) {
|
||||
navigateTo('/login/mfa');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -107,6 +113,8 @@ class _LoginPageState extends TbPageState<LoginPage> {
|
||||
children: [
|
||||
FormBuilderTextField(
|
||||
name: 'username',
|
||||
keyboardType:
|
||||
TextInputType.emailAddress,
|
||||
validator:
|
||||
FormBuilderValidators.compose([
|
||||
FormBuilderValidators.required(
|
||||
|
||||
504
lib/core/auth/login/two_factor_authentication_page.dart
Normal file
504
lib/core/auth/login/two_factor_authentication_page.dart
Normal file
@@ -0,0 +1,504 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_form_builder/flutter_form_builder.dart';
|
||||
import 'package:form_builder_validators/form_builder_validators.dart';
|
||||
import 'package:material_design_icons_flutter/material_design_icons_flutter.dart';
|
||||
import 'package:alt_sms_autofill/alt_sms_autofill.dart';
|
||||
import 'package:thingsboard_app/core/auth/login/login_page_background.dart';
|
||||
import 'package:thingsboard_app/core/context/tb_context.dart';
|
||||
import 'package:thingsboard_app/core/context/tb_context_widget.dart';
|
||||
import 'package:thingsboard_app/widgets/tb_app_bar.dart';
|
||||
import 'package:thingsboard_app/generated/l10n.dart';
|
||||
import 'package:thingsboard_client/thingsboard_client.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
|
||||
typedef ProviderDescFunction = String Function(
|
||||
BuildContext context, String? contact);
|
||||
typedef TextFunction = String Function(BuildContext context);
|
||||
|
||||
class TwoFactorAuthProviderLoginData {
|
||||
TextFunction nameFunction;
|
||||
ProviderDescFunction descFunction;
|
||||
TextFunction placeholderFunction;
|
||||
String icon;
|
||||
TwoFactorAuthProviderLoginData(
|
||||
{required this.nameFunction,
|
||||
required this.descFunction,
|
||||
required this.placeholderFunction,
|
||||
required this.icon});
|
||||
}
|
||||
|
||||
final Map<TwoFaProviderType, TwoFactorAuthProviderLoginData>
|
||||
twoFactorAuthProvidersLoginData = {
|
||||
TwoFaProviderType.TOTP: TwoFactorAuthProviderLoginData(
|
||||
nameFunction: (context) => S.of(context).mfaProviderTopt,
|
||||
descFunction: (context, contact) => S.of(context).totpAuthDescription,
|
||||
placeholderFunction: (context) => S.of(context).toptAuthPlaceholder,
|
||||
icon: 'cellphone-key'),
|
||||
TwoFaProviderType.SMS: TwoFactorAuthProviderLoginData(
|
||||
nameFunction: (context) => S.of(context).mfaProviderSms,
|
||||
descFunction: (context, contact) =>
|
||||
S.of(context).smsAuthDescription(contact ?? ''),
|
||||
placeholderFunction: (context) => S.of(context).smsAuthPlaceholder,
|
||||
icon: 'message-reply-text-outline'),
|
||||
TwoFaProviderType.EMAIL: TwoFactorAuthProviderLoginData(
|
||||
nameFunction: (context) => S.of(context).mfaProviderEmail,
|
||||
descFunction: (context, contact) =>
|
||||
S.of(context).emailAuthDescription(contact ?? ''),
|
||||
placeholderFunction: (context) => S.of(context).emailAuthPlaceholder,
|
||||
icon: 'email-outline'),
|
||||
TwoFaProviderType.BACKUP_CODE: TwoFactorAuthProviderLoginData(
|
||||
nameFunction: (context) => S.of(context).mfaProviderBackupCode,
|
||||
descFunction: (context, contact) =>
|
||||
S.of(context).backupCodeAuthDescription,
|
||||
placeholderFunction: (context) => S.of(context).backupCodeAuthPlaceholder,
|
||||
icon: 'lock-outline')
|
||||
};
|
||||
|
||||
class TwoFactorAuthenticationPage extends TbPageWidget {
|
||||
TwoFactorAuthenticationPage(TbContext tbContext) : super(tbContext);
|
||||
|
||||
@override
|
||||
_TwoFactorAuthenticationPageState createState() =>
|
||||
_TwoFactorAuthenticationPageState();
|
||||
}
|
||||
|
||||
class _TwoFactorAuthenticationPageState
|
||||
extends TbPageState<TwoFactorAuthenticationPage> {
|
||||
static RegExp smsCodeRegExp = new RegExp(r"(\d{6})");
|
||||
|
||||
final _twoFactorAuthFormKey = GlobalKey<FormBuilderState>();
|
||||
ValueNotifier<TwoFaProviderType?> _selectedProvider =
|
||||
ValueNotifier<TwoFaProviderType?>(null);
|
||||
TwoFaProviderType? _prevProvider;
|
||||
int? _minVerificationPeriod;
|
||||
List<TwoFaProviderType> _allowProviders = [];
|
||||
ValueNotifier<bool> _disableSendButton = ValueNotifier<bool>(false);
|
||||
ValueNotifier<bool> _showResendAction = ValueNotifier<bool>(false);
|
||||
ValueNotifier<bool> _hideResendButton = ValueNotifier<bool>(true);
|
||||
Timer? _timer;
|
||||
Timer? _tooManyRequestsTimer;
|
||||
ValueNotifier<int> _countDownTime = ValueNotifier<int>(0);
|
||||
bool _listenForSms = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
var providersInfo = tbContext.twoFactorAuthProviders;
|
||||
TwoFaProviderType.values.forEach((provider) {
|
||||
var providerConfig =
|
||||
providersInfo!.firstWhereOrNull((config) => config.type == provider);
|
||||
if (providerConfig != null) {
|
||||
if (providerConfig.isDefault) {
|
||||
_minVerificationPeriod =
|
||||
providerConfig.minVerificationCodeSendPeriod ?? 30;
|
||||
_selectedProvider.value = providerConfig.type;
|
||||
}
|
||||
_allowProviders.add(providerConfig.type);
|
||||
}
|
||||
});
|
||||
if (this._selectedProvider.value != TwoFaProviderType.TOTP) {
|
||||
_sendCode();
|
||||
_showResendAction.value = true;
|
||||
if (this._selectedProvider.value == TwoFaProviderType.SMS) {
|
||||
_startListenForSmsCode();
|
||||
}
|
||||
}
|
||||
_timer = Timer.periodic(Duration(seconds: 1), (timer) {
|
||||
_updatedTime();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
if (_timer != null) {
|
||||
_timer!.cancel();
|
||||
}
|
||||
if (_tooManyRequestsTimer != null) {
|
||||
_tooManyRequestsTimer!.cancel();
|
||||
}
|
||||
_cancelSmsCodeListen();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return WillPopScope(
|
||||
onWillPop: () async {
|
||||
return await _goBack();
|
||||
},
|
||||
child: Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
resizeToAvoidBottomInset: false,
|
||||
body: Stack(children: [
|
||||
LoginPageBackground(),
|
||||
SizedBox.expand(
|
||||
child: Scaffold(
|
||||
backgroundColor: Colors.transparent,
|
||||
appBar: TbAppBar(
|
||||
tbContext,
|
||||
title: Text('${S.of(context).verifyYourIdentity}'),
|
||||
),
|
||||
body: Stack(children: [
|
||||
SizedBox.expand(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(24),
|
||||
child: ValueListenableBuilder<TwoFaProviderType?>(
|
||||
valueListenable: _selectedProvider,
|
||||
builder: (context, providerType, _widget) {
|
||||
if (providerType == null) {
|
||||
var children = <Widget>[
|
||||
Padding(
|
||||
padding: EdgeInsets.only(bottom: 16),
|
||||
child: Text(
|
||||
'${S.of(context).selectWayToVerify}',
|
||||
style: TextStyle(
|
||||
color: Colors.black87,
|
||||
fontSize: 16,
|
||||
height: 24 / 16)))
|
||||
];
|
||||
_allowProviders.forEach((type) {
|
||||
var providerData =
|
||||
twoFactorAuthProvidersLoginData[
|
||||
type]!;
|
||||
Widget? icon;
|
||||
var iconData = MdiIcons.fromString(
|
||||
providerData.icon);
|
||||
if (iconData != null) {
|
||||
icon = Icon(iconData,
|
||||
size: 24,
|
||||
color:
|
||||
Theme.of(context).primaryColor);
|
||||
} else {
|
||||
icon = Icon(Icons.login,
|
||||
size: 24,
|
||||
color:
|
||||
Theme.of(context).primaryColor);
|
||||
}
|
||||
children.add(Container(
|
||||
padding:
|
||||
EdgeInsets.symmetric(vertical: 8),
|
||||
child: OutlinedButton.icon(
|
||||
style: OutlinedButton.styleFrom(
|
||||
padding: EdgeInsets.all(16),
|
||||
alignment:
|
||||
Alignment.centerLeft),
|
||||
onPressed: () async =>
|
||||
await _selectProvider(type),
|
||||
icon: icon,
|
||||
label: Text(providerData
|
||||
.nameFunction(context)))));
|
||||
});
|
||||
return ListView(
|
||||
padding:
|
||||
EdgeInsets.symmetric(vertical: 8),
|
||||
children: children,
|
||||
);
|
||||
} else {
|
||||
var providerConfig = tbContext
|
||||
.twoFactorAuthProviders
|
||||
?.firstWhereOrNull((config) =>
|
||||
config.type == providerType);
|
||||
if (providerConfig == null) {
|
||||
return SizedBox.shrink();
|
||||
}
|
||||
var providerDescription =
|
||||
twoFactorAuthProvidersLoginData[
|
||||
providerType]!
|
||||
.descFunction;
|
||||
return FormBuilder(
|
||||
key: _twoFactorAuthFormKey,
|
||||
autovalidateMode:
|
||||
AutovalidateMode.disabled,
|
||||
child: Column(
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
SizedBox(height: 16),
|
||||
Text(
|
||||
providerDescription(context,
|
||||
providerConfig.contact),
|
||||
textAlign: TextAlign.start,
|
||||
style: TextStyle(
|
||||
color: Color(0xFF7F7F7F),
|
||||
fontSize: 14,
|
||||
height: 24 / 14),
|
||||
),
|
||||
SizedBox(height: 16),
|
||||
_buildVerificationCodeField(
|
||||
context, providerType),
|
||||
Spacer(),
|
||||
ValueListenableBuilder<bool>(
|
||||
valueListenable:
|
||||
_disableSendButton,
|
||||
builder: (context,
|
||||
disableSendButton,
|
||||
_widget) {
|
||||
return ElevatedButton(
|
||||
child: Text(
|
||||
'${S.of(context).continueText}'),
|
||||
style: ElevatedButton
|
||||
.styleFrom(
|
||||
padding: EdgeInsets
|
||||
.symmetric(
|
||||
vertical:
|
||||
16)),
|
||||
onPressed: disableSendButton
|
||||
? null
|
||||
: () =>
|
||||
_sendVerificationCode(
|
||||
context));
|
||||
}),
|
||||
SizedBox(height: 16),
|
||||
SizedBox(
|
||||
height: 48,
|
||||
child: Row(
|
||||
mainAxisSize:
|
||||
MainAxisSize.max,
|
||||
children: [
|
||||
ValueListenableBuilder<
|
||||
bool>(
|
||||
valueListenable:
|
||||
_showResendAction,
|
||||
builder: (context,
|
||||
showResendActionValue,
|
||||
_widget) {
|
||||
if (showResendActionValue) {
|
||||
return Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
ValueListenableBuilder<
|
||||
int>(
|
||||
valueListenable:
|
||||
_countDownTime,
|
||||
builder: (context,
|
||||
countDown,
|
||||
_widget) {
|
||||
if (countDown >
|
||||
0) {
|
||||
return Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 12),
|
||||
child: Text(
|
||||
S.of(context).resendCodeWait(countDown),
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(color: Color(0xFF7F7F7F), fontSize: 12, height: 24 / 12),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return SizedBox.shrink();
|
||||
}
|
||||
}),
|
||||
ValueListenableBuilder<
|
||||
bool>(
|
||||
valueListenable:
|
||||
_hideResendButton,
|
||||
builder: (context,
|
||||
hideResendButton,
|
||||
_widget) {
|
||||
if (!hideResendButton) {
|
||||
return TextButton(
|
||||
child: Text('${S.of(context).resendCode}'),
|
||||
style: ElevatedButton.styleFrom(padding: EdgeInsets.symmetric(vertical: 16)),
|
||||
onPressed: () {
|
||||
_sendCode();
|
||||
},
|
||||
);
|
||||
} else {
|
||||
return SizedBox.shrink();
|
||||
}
|
||||
})
|
||||
]));
|
||||
} else {
|
||||
return SizedBox
|
||||
.shrink();
|
||||
}
|
||||
}),
|
||||
if (_allowProviders
|
||||
.length >
|
||||
1)
|
||||
Expanded(
|
||||
child: TextButton(
|
||||
child: Text(
|
||||
'${S.of(context).tryAnotherWay}'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
padding: EdgeInsets
|
||||
.symmetric(
|
||||
vertical:
|
||||
16)),
|
||||
onPressed:
|
||||
() async {
|
||||
await _selectProvider(
|
||||
null);
|
||||
},
|
||||
))
|
||||
]))
|
||||
]));
|
||||
}
|
||||
})))
|
||||
]),
|
||||
),
|
||||
)
|
||||
])));
|
||||
}
|
||||
|
||||
FormBuilderTextField _buildVerificationCodeField(
|
||||
BuildContext context, TwoFaProviderType providerType) {
|
||||
int maxLengthInput = 6;
|
||||
TextInputType keyboardType = TextInputType.number;
|
||||
String pattern = '[0-9]*';
|
||||
|
||||
if (providerType == TwoFaProviderType.BACKUP_CODE) {
|
||||
maxLengthInput = 8;
|
||||
pattern = '[0-9abcdef]*';
|
||||
keyboardType = TextInputType.text;
|
||||
}
|
||||
|
||||
List<FormFieldValidator<String>> validators = [
|
||||
FormBuilderValidators.required(
|
||||
errorText: '${S.of(context).verificationCodeInvalid}'),
|
||||
FormBuilderValidators.equalLength(maxLengthInput,
|
||||
errorText: '${S.of(context).verificationCodeInvalid}'),
|
||||
FormBuilderValidators.match(pattern,
|
||||
errorText: '${S.of(context).verificationCodeInvalid}')
|
||||
];
|
||||
|
||||
var providerFormData = twoFactorAuthProvidersLoginData[providerType]!;
|
||||
|
||||
return FormBuilderTextField(
|
||||
name: 'verificationCode',
|
||||
autofocus: true,
|
||||
maxLength: maxLengthInput,
|
||||
keyboardType: keyboardType,
|
||||
validator: FormBuilderValidators.compose(validators),
|
||||
decoration: InputDecoration(
|
||||
border: OutlineInputBorder(),
|
||||
labelText: providerFormData.placeholderFunction(context)));
|
||||
}
|
||||
|
||||
Future<void> _startListenForSmsCode() async {
|
||||
_listenForSms = true;
|
||||
_listenForSmsCode();
|
||||
}
|
||||
|
||||
Future<void> _listenForSmsCode() async {
|
||||
String? comingSms;
|
||||
try {
|
||||
comingSms = await AltSmsAutofill().listenForSms;
|
||||
} catch (e) {
|
||||
_listenForSms = false;
|
||||
comingSms = null;
|
||||
}
|
||||
if (comingSms != null) {
|
||||
RegExpMatch? match = smsCodeRegExp.firstMatch(comingSms);
|
||||
if (match != null) {
|
||||
String? codeStr = match.group(1);
|
||||
if (codeStr != null) {
|
||||
_twoFactorAuthFormKey.currentState
|
||||
?.patchValue({'verificationCode': codeStr});
|
||||
}
|
||||
}
|
||||
}
|
||||
if (_listenForSms) {
|
||||
_listenForSmsCode();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _cancelSmsCodeListen() async {
|
||||
_listenForSms = false;
|
||||
AltSmsAutofill().unregisterListener();
|
||||
}
|
||||
|
||||
Future<void> _sendVerificationCode(BuildContext context) async {
|
||||
FocusScope.of(context).unfocus();
|
||||
if (_twoFactorAuthFormKey.currentState?.saveAndValidate() ?? false) {
|
||||
var formValue = _twoFactorAuthFormKey.currentState!.value;
|
||||
String verificationCode = formValue['verificationCode'];
|
||||
try {
|
||||
await tbClient.checkTwoFaVerificationCode(
|
||||
_selectedProvider.value!, verificationCode,
|
||||
requestConfig: RequestConfig(ignoreErrors: true));
|
||||
} catch (e) {
|
||||
if (e is ThingsboardError) {
|
||||
if (e.status == 400) {
|
||||
_twoFactorAuthFormKey.currentState!.fields['verificationCode']!
|
||||
.invalidate(S.of(context).verificationCodeIncorrect);
|
||||
} else if (e.status == 429) {
|
||||
_twoFactorAuthFormKey.currentState!.fields['verificationCode']!
|
||||
.invalidate(S.of(context).verificationCodeManyRequest);
|
||||
_disableSendButton.value = true;
|
||||
if (_tooManyRequestsTimer != null) {
|
||||
_tooManyRequestsTimer!.cancel();
|
||||
}
|
||||
_tooManyRequestsTimer = Timer(Duration(seconds: 5), () {
|
||||
_twoFactorAuthFormKey.currentState!.fields['verificationCode']!
|
||||
.validate();
|
||||
_disableSendButton.value = false;
|
||||
});
|
||||
} else {
|
||||
showErrorNotification(e.message ?? 'Code verification failed!');
|
||||
}
|
||||
} else {
|
||||
showErrorNotification('Code verification failed!');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _selectProvider(TwoFaProviderType? type) async {
|
||||
_prevProvider = type == null ? _selectedProvider.value : null;
|
||||
_selectedProvider.value = type;
|
||||
_showResendAction.value = false;
|
||||
await _cancelSmsCodeListen();
|
||||
if (type != null) {
|
||||
var providersInfo = tbContext.twoFactorAuthProviders;
|
||||
var providerConfig =
|
||||
providersInfo!.firstWhereOrNull((config) => config.type == type)!;
|
||||
if (type != TwoFaProviderType.TOTP &&
|
||||
type != TwoFaProviderType.BACKUP_CODE) {
|
||||
_sendCode();
|
||||
_showResendAction.value = true;
|
||||
_minVerificationPeriod =
|
||||
providerConfig.minVerificationCodeSendPeriod ?? 30;
|
||||
if (type == TwoFaProviderType.SMS) {
|
||||
_startListenForSmsCode();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _sendCode() async {
|
||||
_hideResendButton.value = true;
|
||||
_countDownTime.value = 0;
|
||||
try {
|
||||
await tbContext.tbClient
|
||||
.getTwoFactorAuthService()
|
||||
.requestTwoFaVerificationCode(_selectedProvider.value!,
|
||||
requestConfig: RequestConfig(ignoreErrors: true));
|
||||
} catch (e) {
|
||||
} finally {
|
||||
_countDownTime.value = _minVerificationPeriod!;
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> _goBack() async {
|
||||
if (_prevProvider != null) {
|
||||
await _selectProvider(_prevProvider);
|
||||
_prevProvider = null;
|
||||
} else {
|
||||
tbClient.logout(requestConfig: RequestConfig(ignoreErrors: true));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void _updatedTime() {
|
||||
if (_countDownTime.value > 0) {
|
||||
_countDownTime.value--;
|
||||
if (_countDownTime.value == 0) {
|
||||
_hideResendButton.value = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -104,6 +104,7 @@ class TbContext {
|
||||
final ValueNotifier<bool> _isAuthenticated = ValueNotifier(false);
|
||||
PlatformType? _oauth2PlatformType;
|
||||
List<OAuth2ClientInfo>? oauth2ClientInfos;
|
||||
List<TwoFaProviderInfo>? twoFactorAuthProviders;
|
||||
User? userDetails;
|
||||
HomeDashboardInfo? homeDashboard;
|
||||
final _isLoadingNotifier = ValueNotifier<bool>(false);
|
||||
@@ -256,7 +257,7 @@ class TbContext {
|
||||
try {
|
||||
log.debug('onUserLoaded: isAuthenticated=${tbClient.isAuthenticated()}');
|
||||
isUserLoaded = true;
|
||||
if (tbClient.isAuthenticated()) {
|
||||
if (tbClient.isAuthenticated() && !tbClient.isPreVerificationToken()) {
|
||||
log.debug('authUser: ${tbClient.getAuthUser()}');
|
||||
if (tbClient.getAuthUser()!.userId != null) {
|
||||
try {
|
||||
@@ -272,6 +273,14 @@ class TbContext {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (tbClient.isPreVerificationToken()) {
|
||||
log.debug('authUser: ${tbClient.getAuthUser()}');
|
||||
twoFactorAuthProviders = await tbClient
|
||||
.getTwoFactorAuthService()
|
||||
.getAvailableLoginTwoFaProviders();
|
||||
} else {
|
||||
twoFactorAuthProviders = null;
|
||||
}
|
||||
userDetails = null;
|
||||
homeDashboard = null;
|
||||
oauth2ClientInfos = await tbClient.getOAuth2Service().getOAuth2Clients(
|
||||
@@ -308,14 +317,15 @@ class TbContext {
|
||||
|
||||
Listenable get isAuthenticatedListenable => _isAuthenticated;
|
||||
|
||||
bool get isAuthenticated => _isAuthenticated.value;
|
||||
bool get isAuthenticated =>
|
||||
_isAuthenticated.value && !tbClient.isPreVerificationToken();
|
||||
|
||||
bool get hasOAuthClients =>
|
||||
oauth2ClientInfos != null && oauth2ClientInfos!.isNotEmpty;
|
||||
|
||||
Future<void> updateRouteState() async {
|
||||
if (currentState != null) {
|
||||
if (tbClient.isAuthenticated()) {
|
||||
if (tbClient.isAuthenticated() && !tbClient.isPreVerificationToken()) {
|
||||
var defaultDashboardId = _defaultDashboardId();
|
||||
if (defaultDashboardId != null) {
|
||||
bool fullscreen = _userForceFullscreen();
|
||||
|
||||
@@ -20,6 +20,15 @@ typedef String MessageIfAbsent(String messageStr, List<dynamic> args);
|
||||
class MessageLookup extends MessageLookupByLibrary {
|
||||
String get localeName => 'en';
|
||||
|
||||
static String m0(contact) =>
|
||||
"A security code has been sent to your email address at ${contact}.";
|
||||
|
||||
static String m1(time) =>
|
||||
"Resend code in ${Intl.plural(time, one: '1 second', other: '${time} seconds')}";
|
||||
|
||||
static String m2(contact) =>
|
||||
"A security code has been sent to your phone at ${contact}.";
|
||||
|
||||
final messages = _notInlinedMessages(_notInlinedMessages);
|
||||
static Map<String, Function> _notInlinedMessages(_) => <String, Function>{
|
||||
"No": MessageLookupByLibrary.simpleMessage("No"),
|
||||
@@ -46,9 +55,14 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"auditLogDetails":
|
||||
MessageLookupByLibrary.simpleMessage("Audit log details"),
|
||||
"auditLogs": MessageLookupByLibrary.simpleMessage("Audit Logs"),
|
||||
"backupCodeAuthDescription": MessageLookupByLibrary.simpleMessage(
|
||||
"Please enter one of your backup codes."),
|
||||
"backupCodeAuthPlaceholder":
|
||||
MessageLookupByLibrary.simpleMessage("Backup code"),
|
||||
"changePassword":
|
||||
MessageLookupByLibrary.simpleMessage("Change Password"),
|
||||
"city": MessageLookupByLibrary.simpleMessage("City"),
|
||||
"continueText": MessageLookupByLibrary.simpleMessage("Continue"),
|
||||
"country": MessageLookupByLibrary.simpleMessage("Country"),
|
||||
"currentPassword":
|
||||
MessageLookupByLibrary.simpleMessage("currentPassword"),
|
||||
@@ -60,6 +74,9 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"customers": MessageLookupByLibrary.simpleMessage("Customers"),
|
||||
"devices": MessageLookupByLibrary.simpleMessage("Devices"),
|
||||
"email": MessageLookupByLibrary.simpleMessage("Email"),
|
||||
"emailAuthDescription": m0,
|
||||
"emailAuthPlaceholder":
|
||||
MessageLookupByLibrary.simpleMessage("Email code"),
|
||||
"emailInvalidText":
|
||||
MessageLookupByLibrary.simpleMessage("Invalid email format."),
|
||||
"emailRequireText":
|
||||
@@ -83,6 +100,12 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"logoDefaultValue":
|
||||
MessageLookupByLibrary.simpleMessage("Thingsboard Logo"),
|
||||
"logout": MessageLookupByLibrary.simpleMessage("Log Out"),
|
||||
"mfaProviderBackupCode":
|
||||
MessageLookupByLibrary.simpleMessage("Backup code"),
|
||||
"mfaProviderEmail": MessageLookupByLibrary.simpleMessage("Email"),
|
||||
"mfaProviderSms": MessageLookupByLibrary.simpleMessage("SMS"),
|
||||
"mfaProviderTopt":
|
||||
MessageLookupByLibrary.simpleMessage("Authenticator app"),
|
||||
"more": MessageLookupByLibrary.simpleMessage("More"),
|
||||
"newPassword": MessageLookupByLibrary.simpleMessage("newPassword"),
|
||||
"newPassword2": MessageLookupByLibrary.simpleMessage("newPassword2"),
|
||||
@@ -117,6 +140,12 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"Profile successfully updated"),
|
||||
"requestPasswordReset":
|
||||
MessageLookupByLibrary.simpleMessage("Request password reset"),
|
||||
"resendCode": MessageLookupByLibrary.simpleMessage("Resend code"),
|
||||
"resendCodeWait": m1,
|
||||
"selectWayToVerify":
|
||||
MessageLookupByLibrary.simpleMessage("Select a way to verify"),
|
||||
"smsAuthDescription": m2,
|
||||
"smsAuthPlaceholder": MessageLookupByLibrary.simpleMessage("SMS code"),
|
||||
"stateOrProvince":
|
||||
MessageLookupByLibrary.simpleMessage("State / Province"),
|
||||
"systemAdministrator":
|
||||
@@ -124,8 +153,21 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"tenantAdministrator":
|
||||
MessageLookupByLibrary.simpleMessage("Tenant Administrator"),
|
||||
"title": MessageLookupByLibrary.simpleMessage("Title"),
|
||||
"toptAuthPlaceholder": MessageLookupByLibrary.simpleMessage("Code"),
|
||||
"totpAuthDescription": MessageLookupByLibrary.simpleMessage(
|
||||
"Please enter the security code from your authenticator app."),
|
||||
"tryAgain": MessageLookupByLibrary.simpleMessage("Try Again"),
|
||||
"tryAnotherWay":
|
||||
MessageLookupByLibrary.simpleMessage("Try another way"),
|
||||
"type": MessageLookupByLibrary.simpleMessage("Type"),
|
||||
"username": MessageLookupByLibrary.simpleMessage("username")
|
||||
"username": MessageLookupByLibrary.simpleMessage("username"),
|
||||
"verificationCodeIncorrect": MessageLookupByLibrary.simpleMessage(
|
||||
"Verification code is incorrect"),
|
||||
"verificationCodeInvalid": MessageLookupByLibrary.simpleMessage(
|
||||
"Invalid verification code format"),
|
||||
"verificationCodeManyRequest": MessageLookupByLibrary.simpleMessage(
|
||||
"Too many requests check verification code"),
|
||||
"verifyYourIdentity":
|
||||
MessageLookupByLibrary.simpleMessage("Verify your identity")
|
||||
};
|
||||
}
|
||||
|
||||
@@ -779,6 +779,216 @@ class S {
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Verify your identity`
|
||||
String get verifyYourIdentity {
|
||||
return Intl.message(
|
||||
'Verify your identity',
|
||||
name: 'verifyYourIdentity',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Continue`
|
||||
String get continueText {
|
||||
return Intl.message(
|
||||
'Continue',
|
||||
name: 'continueText',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Resend code`
|
||||
String get resendCode {
|
||||
return Intl.message(
|
||||
'Resend code',
|
||||
name: 'resendCode',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Resend code in {time,plural, =1{1 second}other{{time} seconds}}`
|
||||
String resendCodeWait(num time) {
|
||||
return Intl.message(
|
||||
'Resend code in ${Intl.plural(time, one: '1 second', other: '$time seconds')}',
|
||||
name: 'resendCodeWait',
|
||||
desc: '',
|
||||
args: [time],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Please enter the security code from your authenticator app.`
|
||||
String get totpAuthDescription {
|
||||
return Intl.message(
|
||||
'Please enter the security code from your authenticator app.',
|
||||
name: 'totpAuthDescription',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `A security code has been sent to your phone at {contact}.`
|
||||
String smsAuthDescription(Object contact) {
|
||||
return Intl.message(
|
||||
'A security code has been sent to your phone at $contact.',
|
||||
name: 'smsAuthDescription',
|
||||
desc: '',
|
||||
args: [contact],
|
||||
);
|
||||
}
|
||||
|
||||
/// `A security code has been sent to your email address at {contact}.`
|
||||
String emailAuthDescription(Object contact) {
|
||||
return Intl.message(
|
||||
'A security code has been sent to your email address at $contact.',
|
||||
name: 'emailAuthDescription',
|
||||
desc: '',
|
||||
args: [contact],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Please enter one of your backup codes.`
|
||||
String get backupCodeAuthDescription {
|
||||
return Intl.message(
|
||||
'Please enter one of your backup codes.',
|
||||
name: 'backupCodeAuthDescription',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Invalid verification code format`
|
||||
String get verificationCodeInvalid {
|
||||
return Intl.message(
|
||||
'Invalid verification code format',
|
||||
name: 'verificationCodeInvalid',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Code`
|
||||
String get toptAuthPlaceholder {
|
||||
return Intl.message(
|
||||
'Code',
|
||||
name: 'toptAuthPlaceholder',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `SMS code`
|
||||
String get smsAuthPlaceholder {
|
||||
return Intl.message(
|
||||
'SMS code',
|
||||
name: 'smsAuthPlaceholder',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Email code`
|
||||
String get emailAuthPlaceholder {
|
||||
return Intl.message(
|
||||
'Email code',
|
||||
name: 'emailAuthPlaceholder',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Backup code`
|
||||
String get backupCodeAuthPlaceholder {
|
||||
return Intl.message(
|
||||
'Backup code',
|
||||
name: 'backupCodeAuthPlaceholder',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Verification code is incorrect`
|
||||
String get verificationCodeIncorrect {
|
||||
return Intl.message(
|
||||
'Verification code is incorrect',
|
||||
name: 'verificationCodeIncorrect',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Too many requests check verification code`
|
||||
String get verificationCodeManyRequest {
|
||||
return Intl.message(
|
||||
'Too many requests check verification code',
|
||||
name: 'verificationCodeManyRequest',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Try another way`
|
||||
String get tryAnotherWay {
|
||||
return Intl.message(
|
||||
'Try another way',
|
||||
name: 'tryAnotherWay',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Select a way to verify`
|
||||
String get selectWayToVerify {
|
||||
return Intl.message(
|
||||
'Select a way to verify',
|
||||
name: 'selectWayToVerify',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Authenticator app`
|
||||
String get mfaProviderTopt {
|
||||
return Intl.message(
|
||||
'Authenticator app',
|
||||
name: 'mfaProviderTopt',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `SMS`
|
||||
String get mfaProviderSms {
|
||||
return Intl.message(
|
||||
'SMS',
|
||||
name: 'mfaProviderSms',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Email`
|
||||
String get mfaProviderEmail {
|
||||
return Intl.message(
|
||||
'Email',
|
||||
name: 'mfaProviderEmail',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Backup code`
|
||||
String get mfaProviderBackupCode {
|
||||
return Intl.message(
|
||||
'Backup code',
|
||||
name: 'mfaProviderBackupCode',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class AppLocalizationDelegate extends LocalizationsDelegate<S> {
|
||||
|
||||
@@ -86,6 +86,27 @@
|
||||
"notImplemented": "Not implemented!",
|
||||
|
||||
"listIsEmptyText": "The list is currently empty.",
|
||||
"tryAgain": "Try Again"
|
||||
"tryAgain": "Try Again",
|
||||
|
||||
"verifyYourIdentity": "Verify your identity",
|
||||
"continueText": "Continue",
|
||||
"resendCode": "Resend code",
|
||||
"resendCodeWait": "Resend code in {time,plural, =1{1 second}other{{time} seconds}}",
|
||||
"totpAuthDescription": "Please enter the security code from your authenticator app.",
|
||||
"smsAuthDescription": "A security code has been sent to your phone at {contact}.",
|
||||
"emailAuthDescription": "A security code has been sent to your email address at {contact}.",
|
||||
"backupCodeAuthDescription": "Please enter one of your backup codes.",
|
||||
"verificationCodeInvalid": "Invalid verification code format",
|
||||
"toptAuthPlaceholder": "Code",
|
||||
"smsAuthPlaceholder": "SMS code",
|
||||
"emailAuthPlaceholder": "Email code",
|
||||
"backupCodeAuthPlaceholder": "Backup code",
|
||||
"verificationCodeIncorrect": "Verification code is incorrect",
|
||||
"verificationCodeManyRequest": "Too many requests check verification code",
|
||||
"tryAnotherWay": "Try another way",
|
||||
"selectWayToVerify": "Select a way to verify",
|
||||
"mfaProviderTopt": "Authenticator app",
|
||||
"mfaProviderSms": "SMS",
|
||||
"mfaProviderEmail": "Email",
|
||||
"mfaProviderBackupCode": "Backup code"
|
||||
}
|
||||
@@ -1,6 +1,13 @@
|
||||
# Generated by pub
|
||||
# See https://dart.dev/tools/pub/glossary#lockfile
|
||||
packages:
|
||||
alt_sms_autofill:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: alt_sms_autofill
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.0.0"
|
||||
archive:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -586,7 +593,7 @@ packages:
|
||||
description:
|
||||
path: "."
|
||||
ref: master
|
||||
resolved-ref: d8dcad26dade6fca574d600ab88a6cc0ccaf1a7d
|
||||
resolved-ref: cb439261f3f54f76d1df14b8e2597b20f9c38a88
|
||||
url: "git@github.com:thingsboard/dart_thingsboard_client.git"
|
||||
source: git
|
||||
version: "1.0.3"
|
||||
|
||||
@@ -50,6 +50,7 @@ dependencies:
|
||||
preload_page_view: ^0.1.6
|
||||
flutter_localizations:
|
||||
sdk: flutter
|
||||
alt_sms_autofill: ^1.0.0
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
||||
Reference in New Issue
Block a user