Performed first authentication
This commit is contained in:
		@@ -1,4 +1,6 @@
 | 
				
			|||||||
 | 
					import 'package:moneymgr_mobile/services/api/api_client.dart';
 | 
				
			||||||
import 'package:moneymgr_mobile/services/api/api_token.dart';
 | 
					import 'package:moneymgr_mobile/services/api/api_token.dart';
 | 
				
			||||||
 | 
					import 'package:moneymgr_mobile/services/api/auth_api.dart';
 | 
				
			||||||
import 'package:moneymgr_mobile/services/router/routes_list.dart';
 | 
					import 'package:moneymgr_mobile/services/router/routes_list.dart';
 | 
				
			||||||
import 'package:moneymgr_mobile/services/storage/secure_storage.dart';
 | 
					import 'package:moneymgr_mobile/services/storage/secure_storage.dart';
 | 
				
			||||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
 | 
					import 'package:riverpod_annotation/riverpod_annotation.dart';
 | 
				
			||||||
@@ -14,25 +16,22 @@ class CurrentAuthState extends _$CurrentAuthState {
 | 
				
			|||||||
  @override
 | 
					  @override
 | 
				
			||||||
  AuthState build() {
 | 
					  AuthState build() {
 | 
				
			||||||
    final secureStorage = ref.watch(secureStorageProvider).requireValue;
 | 
					    final secureStorage = ref.watch(secureStorageProvider).requireValue;
 | 
				
			||||||
    final token = secureStorage.get('token');
 | 
					    final token = secureStorage.token();
 | 
				
			||||||
    return token != null ? AuthState.authenticated : AuthState.unauthenticated;
 | 
					    return token != null ? AuthState.authenticated : AuthState.unauthenticated;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /// Attempts to authenticate with [data] and saves the token and profile info to storage.
 | 
					  /// Attempts to authenticate with [token] and saves the token and profile info to storage.
 | 
				
			||||||
  /// Will invalidate the state if success.
 | 
					  /// Will invalidate the state if success and throw an exception in case of failure
 | 
				
			||||||
  Future<void> setAuthToken(ApiToken data) async {
 | 
					  Future<void> setAuthToken(ApiToken token) async {
 | 
				
			||||||
    // TODO : perform login
 | 
					    // Attempt to use provided token
 | 
				
			||||||
    /*final secureStorage = ref.read(secureStorageProvider).requireValue;
 | 
					    await ApiClient(token: token).authInfo();
 | 
				
			||||||
    final token = await ref.read(apiServiceProvider).login(data);
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Save the new [token] and [profile] to secure storage.
 | 
					    final secureStorage = ref.read(secureStorageProvider).requireValue;
 | 
				
			||||||
    secureStorage.set('token', token);
 | 
					    secureStorage.setToken(token);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    ref
 | 
					    ref
 | 
				
			||||||
    // Invalidate the state so the auth state will be updated to authenticated.
 | 
					        // Invalidate the state so the auth state will be updated to authenticated.
 | 
				
			||||||
      ..invalidateSelf()
 | 
					        .invalidateSelf();
 | 
				
			||||||
    // Invalidate the token provider so the API service will use the new token.
 | 
					 | 
				
			||||||
      ..invalidate(tokenProvider);*/
 | 
					 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /// Logs out, deletes the saved token and profile info from storage, and invalidates
 | 
					  /// Logs out, deletes the saved token and profile info from storage, and invalidates
 | 
				
			||||||
@@ -60,9 +59,17 @@ enum AuthState {
 | 
				
			|||||||
    redirectPath: authPage,
 | 
					    redirectPath: authPage,
 | 
				
			||||||
    allowedPaths: [authPage, manualAuthPage, settingsPage],
 | 
					    allowedPaths: [authPage, manualAuthPage, settingsPage],
 | 
				
			||||||
  ),
 | 
					  ),
 | 
				
			||||||
  authenticated(redirectPath: homePage, allowedPaths: null);
 | 
					  authenticated(
 | 
				
			||||||
 | 
					    redirectPath: homePage,
 | 
				
			||||||
 | 
					    allowedPaths: null,
 | 
				
			||||||
 | 
					    forbiddenPaths: [authPage, manualAuthPage],
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const AuthState({required this.redirectPath, required this.allowedPaths});
 | 
					  const AuthState({
 | 
				
			||||||
 | 
					    required this.redirectPath,
 | 
				
			||||||
 | 
					    required this.allowedPaths,
 | 
				
			||||||
 | 
					    this.forbiddenPaths,
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /// The target path to redirect when the current route is not allowed in this
 | 
					  /// The target path to redirect when the current route is not allowed in this
 | 
				
			||||||
  /// auth state.
 | 
					  /// auth state.
 | 
				
			||||||
@@ -71,4 +78,8 @@ enum AuthState {
 | 
				
			|||||||
  /// List of paths allowed when the app is in this auth state. May be set to null if there is no
 | 
					  /// List of paths allowed when the app is in this auth state. May be set to null if there is no
 | 
				
			||||||
  /// restriction applicable
 | 
					  /// restriction applicable
 | 
				
			||||||
  final List<String>? allowedPaths;
 | 
					  final List<String>? allowedPaths;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// List of paths not allowed when the app is in this auth state. May be set to null if there is no
 | 
				
			||||||
 | 
					  /// restriction applicable
 | 
				
			||||||
 | 
					  final List<String>? forbiddenPaths;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -5,7 +5,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
				
			|||||||
import 'package:logging/logging.dart';
 | 
					import 'package:logging/logging.dart';
 | 
				
			||||||
import 'package:moneymgr_mobile/routes/login/base_auth_page.dart';
 | 
					import 'package:moneymgr_mobile/routes/login/base_auth_page.dart';
 | 
				
			||||||
import 'package:moneymgr_mobile/services/api/api_token.dart';
 | 
					import 'package:moneymgr_mobile/services/api/api_token.dart';
 | 
				
			||||||
import 'package:moneymgr_mobile/services/auth_state.dart';
 | 
					import 'package:moneymgr_mobile/providers/auth_state.dart';
 | 
				
			||||||
import 'package:moneymgr_mobile/utils/extensions.dart';
 | 
					import 'package:moneymgr_mobile/utils/extensions.dart';
 | 
				
			||||||
import 'package:moneymgr_mobile/widgets/app_button.dart';
 | 
					import 'package:moneymgr_mobile/widgets/app_button.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -52,7 +52,7 @@ class ManualAuthScreen extends HookConsumerWidget {
 | 
				
			|||||||
          decoration: const InputDecoration(
 | 
					          decoration: const InputDecoration(
 | 
				
			||||||
            labelText: 'API URL',
 | 
					            labelText: 'API URL',
 | 
				
			||||||
            helperText:
 | 
					            helperText:
 | 
				
			||||||
                "This URL has usually this format: http://moneymgr.corp.com/api",
 | 
					                "Format: http://moneymgr.corp.com/api",
 | 
				
			||||||
          ),
 | 
					          ),
 | 
				
			||||||
          textInputAction: TextInputAction.next,
 | 
					          textInputAction: TextInputAction.next,
 | 
				
			||||||
        ),
 | 
					        ),
 | 
				
			||||||
@@ -68,7 +68,7 @@ class ManualAuthScreen extends HookConsumerWidget {
 | 
				
			|||||||
        ),
 | 
					        ),
 | 
				
			||||||
        Gutter(),
 | 
					        Gutter(),
 | 
				
			||||||
        TextField(
 | 
					        TextField(
 | 
				
			||||||
          controller: tokenIdController,
 | 
					          controller: tokenValueController,
 | 
				
			||||||
          keyboardType: TextInputType.text,
 | 
					          keyboardType: TextInputType.text,
 | 
				
			||||||
          decoration: const InputDecoration(
 | 
					          decoration: const InputDecoration(
 | 
				
			||||||
            labelText: 'Token value',
 | 
					            labelText: 'Token value',
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,4 +0,0 @@
 | 
				
			|||||||
/// Account API
 | 
					 | 
				
			||||||
class AccountAPI {
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -1,7 +1,72 @@
 | 
				
			|||||||
 | 
					import 'package:dart_jsonwebtoken/dart_jsonwebtoken.dart';
 | 
				
			||||||
 | 
					import 'package:dio/dio.dart';
 | 
				
			||||||
 | 
					import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
				
			||||||
 | 
					import 'package:moneymgr_mobile/services/api/api_token.dart';
 | 
				
			||||||
 | 
					import 'package:moneymgr_mobile/utils/string_utils.dart';
 | 
				
			||||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
 | 
					import 'package:riverpod_annotation/riverpod_annotation.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/// Client API
 | 
					part 'api_client.g.dart';
 | 
				
			||||||
@riverpod
 | 
					 | 
				
			||||||
class ClientAPI {
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
}
 | 
					/// API token header
 | 
				
			||||||
 | 
					const apiTokenHeader = "X-Auth-Token";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Client API
 | 
				
			||||||
 | 
					class ApiClient {
 | 
				
			||||||
 | 
					  final ApiToken token;
 | 
				
			||||||
 | 
					  final Dio client;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  ApiClient({required this.token})
 | 
				
			||||||
 | 
					    : client = Dio(BaseOptions(baseUrl: token.apiUrl));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// Get Dio instance
 | 
				
			||||||
 | 
					  Future<Response<T>> execute<T>(String uri, {String method = "GET"}) async {
 | 
				
			||||||
 | 
					    return client.request(
 | 
				
			||||||
 | 
					      uri,
 | 
				
			||||||
 | 
					      options: Options(
 | 
				
			||||||
 | 
					        method: method,
 | 
				
			||||||
 | 
					        headers: {apiTokenHeader: _genJWT(method, uri)},
 | 
				
			||||||
 | 
					      ),
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// Generate authentication JWT
 | 
				
			||||||
 | 
					  String _genJWT(String method, String uri) {
 | 
				
			||||||
 | 
					    final jwt = JWT(
 | 
				
			||||||
 | 
					      {"nonce": getRandomString(15), "met": method, "uri": uri},
 | 
				
			||||||
 | 
					      header: {"kid": token.tokenId.toString()},
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return jwt.sign(
 | 
				
			||||||
 | 
					      SecretKey(token.tokenValue),
 | 
				
			||||||
 | 
					      algorithm: JWTAlgorithm.HS256,
 | 
				
			||||||
 | 
					      expiresIn: Duration(minutes: 15),
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// An API service that handles authentication and exposes an [ApiClient].
 | 
				
			||||||
 | 
					///
 | 
				
			||||||
 | 
					/// Every API call coming from UI should watch/read this provider instead of
 | 
				
			||||||
 | 
					/// instantiating the [ApiClient] itself. When being watched, it will force any
 | 
				
			||||||
 | 
					/// data provider (provider that fetches data) to refetch when the
 | 
				
			||||||
 | 
					/// authentication state changes.
 | 
				
			||||||
 | 
					///
 | 
				
			||||||
 | 
					/// The API client is kept alive to follow dio's recommendation to use the same
 | 
				
			||||||
 | 
					/// client instance for the entire app.
 | 
				
			||||||
 | 
					@riverpod
 | 
				
			||||||
 | 
					ApiClient apiService(Ref ref) {
 | 
				
			||||||
 | 
					  /*final token = ref.watch(currentAuthStateProvider);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  final ApiClient client;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const mock = bool.fromEnvironment('MOCK_API', defaultValue: false);
 | 
				
			||||||
 | 
					  client = switch (mock) {
 | 
				
			||||||
 | 
					    true =>
 | 
				
			||||||
 | 
					    token != null ? MockedApiClient.withToken(token) : MockedApiClient(),
 | 
				
			||||||
 | 
					    false => token != null ? ApiClient.withToken(token) : ApiClient(),
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					  ref.keepAlive();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return client;*/
 | 
				
			||||||
 | 
					  throw Exception("TODO"); // TODO
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -8,7 +8,7 @@ abstract class ApiToken with _$ApiToken {
 | 
				
			|||||||
  const factory ApiToken({
 | 
					  const factory ApiToken({
 | 
				
			||||||
    required String apiUrl,
 | 
					    required String apiUrl,
 | 
				
			||||||
    required int tokenId,
 | 
					    required int tokenId,
 | 
				
			||||||
    required String tokenValue
 | 
					    required String tokenValue,
 | 
				
			||||||
  }) = _ApiToken;
 | 
					  }) = _ApiToken;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  factory ApiToken.fromJson(Map<String, dynamic> json) =>
 | 
					  factory ApiToken.fromJson(Map<String, dynamic> json) =>
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										30
									
								
								moneymgr_mobile/lib/services/api/auth_api.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								moneymgr_mobile/lib/services/api/auth_api.dart
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,30 @@
 | 
				
			|||||||
 | 
					// ignore_for_file: non_constant_identifier_names
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import 'package:freezed_annotation/freezed_annotation.dart';
 | 
				
			||||||
 | 
					import 'package:moneymgr_mobile/services/api/api_client.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					part 'auth_api.g.dart';
 | 
				
			||||||
 | 
					part 'auth_api.freezed.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@freezed
 | 
				
			||||||
 | 
					abstract class AuthInfo with _$AuthInfo {
 | 
				
			||||||
 | 
					  const factory AuthInfo({
 | 
				
			||||||
 | 
					    required int id,
 | 
				
			||||||
 | 
					    required String mail,
 | 
				
			||||||
 | 
					    required String name,
 | 
				
			||||||
 | 
					    required int time_create,
 | 
				
			||||||
 | 
					    required int time_update,
 | 
				
			||||||
 | 
					  }) = _AuthInfo;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  factory AuthInfo.fromJson(Map<String, dynamic> json) =>
 | 
				
			||||||
 | 
					      _$AuthInfoFromJson(json);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Auth API
 | 
				
			||||||
 | 
					extension AuthApi on ApiClient {
 | 
				
			||||||
 | 
					  /// Get authentication information
 | 
				
			||||||
 | 
					  Future<AuthInfo> authInfo() async {
 | 
				
			||||||
 | 
					    final response = await execute("/auth/info", method: "GET");
 | 
				
			||||||
 | 
					    return AuthInfo.fromJson(response.data);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -1,13 +1,14 @@
 | 
				
			|||||||
import 'package:flutter/material.dart';
 | 
					import 'package:flutter/material.dart';
 | 
				
			||||||
import 'package:go_router/go_router.dart';
 | 
					import 'package:go_router/go_router.dart';
 | 
				
			||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
					import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
				
			||||||
 | 
					import 'package:moneymgr_mobile/providers/auth_state.dart';
 | 
				
			||||||
import 'package:moneymgr_mobile/routes/login/login_screen.dart';
 | 
					import 'package:moneymgr_mobile/routes/login/login_screen.dart';
 | 
				
			||||||
import 'package:moneymgr_mobile/routes/login/manual_auth_screen.dart';
 | 
					import 'package:moneymgr_mobile/routes/login/manual_auth_screen.dart';
 | 
				
			||||||
import 'package:moneymgr_mobile/routes/settings/settings_screen.dart';
 | 
					import 'package:moneymgr_mobile/routes/settings/settings_screen.dart';
 | 
				
			||||||
import 'package:moneymgr_mobile/services/auth_state.dart';
 | 
					 | 
				
			||||||
import 'package:moneymgr_mobile/services/router/routes_list.dart';
 | 
					import 'package:moneymgr_mobile/services/router/routes_list.dart';
 | 
				
			||||||
import 'package:moneymgr_mobile/widgets/scaffold_with_navigation.dart';
 | 
					import 'package:moneymgr_mobile/widgets/scaffold_with_navigation.dart';
 | 
				
			||||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
 | 
					import 'package:riverpod_annotation/riverpod_annotation.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
part 'router.g.dart';
 | 
					part 'router.g.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/// The router config for the app.
 | 
					/// The router config for the app.
 | 
				
			||||||
@@ -94,7 +95,8 @@ GoRouter router(Ref ref) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
      // Check if the current path is allowed for the current auth state. If not,
 | 
					      // Check if the current path is allowed for the current auth state. If not,
 | 
				
			||||||
      // redirect to the redirect target of the current auth state.
 | 
					      // redirect to the redirect target of the current auth state.
 | 
				
			||||||
      if (authState.allowedPaths?.contains(state.fullPath) == false) {
 | 
					      if (authState.allowedPaths?.contains(state.fullPath) == false ||
 | 
				
			||||||
 | 
					          authState.forbiddenPaths?.contains(state.fullPath) == true) {
 | 
				
			||||||
        return authState.redirectPath;
 | 
					        return authState.redirectPath;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,5 +1,8 @@
 | 
				
			|||||||
 | 
					import 'dart:convert';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
 | 
					import 'package:flutter_secure_storage/flutter_secure_storage.dart';
 | 
				
			||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
					import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
				
			||||||
 | 
					import 'package:moneymgr_mobile/services/api/api_token.dart';
 | 
				
			||||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
 | 
					import 'package:riverpod_annotation/riverpod_annotation.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
part 'secure_storage.g.dart';
 | 
					part 'secure_storage.g.dart';
 | 
				
			||||||
@@ -19,11 +22,13 @@ class SecureStorage {
 | 
				
			|||||||
    const flutterSecureStorage = FlutterSecureStorage();
 | 
					    const flutterSecureStorage = FlutterSecureStorage();
 | 
				
			||||||
    final cache = <String, String>{};
 | 
					    final cache = <String, String>{};
 | 
				
			||||||
    await keys
 | 
					    await keys
 | 
				
			||||||
        .map((key) => flutterSecureStorage.read(key: key).then((value) {
 | 
					        .map(
 | 
				
			||||||
      if (value != null) {
 | 
					          (key) => flutterSecureStorage.read(key: key).then((value) {
 | 
				
			||||||
        cache[key] = value;
 | 
					            if (value != null) {
 | 
				
			||||||
      }
 | 
					              cache[key] = value;
 | 
				
			||||||
    }))
 | 
					            }
 | 
				
			||||||
 | 
					          }),
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
        .wait;
 | 
					        .wait;
 | 
				
			||||||
    return SecureStorage._(flutterSecureStorage, cache);
 | 
					    return SecureStorage._(flutterSecureStorage, cache);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
@@ -39,4 +44,19 @@ class SecureStorage {
 | 
				
			|||||||
    _cache.remove(key);
 | 
					    _cache.remove(key);
 | 
				
			||||||
    return _flutterSecureStorage.delete(key: key);
 | 
					    return _flutterSecureStorage.delete(key: key);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// Get auth token
 | 
				
			||||||
 | 
					  ApiToken? token() {
 | 
				
			||||||
 | 
					    final tokenStr = get("token");
 | 
				
			||||||
 | 
					    if (tokenStr != null) {
 | 
				
			||||||
 | 
					      return ApiToken.fromJson(jsonDecode(tokenStr));
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// Set auth token
 | 
				
			||||||
 | 
					  Future<void> setToken(ApiToken token) =>
 | 
				
			||||||
 | 
					      set("token", jsonEncode(token.toJson()));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// Remove auth token
 | 
				
			||||||
 | 
					  Future<void> removeToken() => remove("token");
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										12
									
								
								moneymgr_mobile/lib/utils/string_utils.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								moneymgr_mobile/lib/utils/string_utils.dart
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,12 @@
 | 
				
			|||||||
 | 
					import 'dart:math';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const _chars = 'AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz1234567890';
 | 
				
			||||||
 | 
					final _rnd = Random();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Generate random string
 | 
				
			||||||
 | 
					String getRandomString(int length) => String.fromCharCodes(
 | 
				
			||||||
 | 
					  Iterable.generate(
 | 
				
			||||||
 | 
					    length,
 | 
				
			||||||
 | 
					    (_) => _chars.codeUnitAt(_rnd.nextInt(_chars.length)),
 | 
				
			||||||
 | 
					  ),
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
							
								
								
									
										3
									
								
								moneymgr_mobile/lib/utils/time_utils.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								moneymgr_mobile/lib/utils/time_utils.dart
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,3 @@
 | 
				
			|||||||
 | 
					int secondsSinceEpoch(DateTime time) {
 | 
				
			||||||
 | 
					  return time.millisecondsSinceEpoch ~/ 1000;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -9,6 +9,14 @@ packages:
 | 
				
			|||||||
      url: "https://pub.dev"
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "82.0.0"
 | 
					    version: "82.0.0"
 | 
				
			||||||
 | 
					  adaptive_number:
 | 
				
			||||||
 | 
					    dependency: transitive
 | 
				
			||||||
 | 
					    description:
 | 
				
			||||||
 | 
					      name: adaptive_number
 | 
				
			||||||
 | 
					      sha256: "3a567544e9b5c9c803006f51140ad544aedc79604fd4f3f2c1380003f97c1d77"
 | 
				
			||||||
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
 | 
					    source: hosted
 | 
				
			||||||
 | 
					    version: "1.0.0"
 | 
				
			||||||
  analyzer:
 | 
					  analyzer:
 | 
				
			||||||
    dependency: transitive
 | 
					    dependency: transitive
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
@@ -217,6 +225,14 @@ packages:
 | 
				
			|||||||
      url: "https://pub.dev"
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "1.0.0+7.4.5"
 | 
					    version: "1.0.0+7.4.5"
 | 
				
			||||||
 | 
					  dart_jsonwebtoken:
 | 
				
			||||||
 | 
					    dependency: "direct main"
 | 
				
			||||||
 | 
					    description:
 | 
				
			||||||
 | 
					      name: dart_jsonwebtoken
 | 
				
			||||||
 | 
					      sha256: "21ce9f8a8712f741e8d6876a9c82c0f8a257fe928c4378a91d8527b92a3fd413"
 | 
				
			||||||
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
 | 
					    source: hosted
 | 
				
			||||||
 | 
					    version: "3.2.0"
 | 
				
			||||||
  dart_style:
 | 
					  dart_style:
 | 
				
			||||||
    dependency: transitive
 | 
					    dependency: transitive
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
@@ -225,6 +241,30 @@ packages:
 | 
				
			|||||||
      url: "https://pub.dev"
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "3.1.0"
 | 
					    version: "3.1.0"
 | 
				
			||||||
 | 
					  dio:
 | 
				
			||||||
 | 
					    dependency: "direct main"
 | 
				
			||||||
 | 
					    description:
 | 
				
			||||||
 | 
					      name: dio
 | 
				
			||||||
 | 
					      sha256: "253a18bbd4851fecba42f7343a1df3a9a4c1d31a2c1b37e221086b4fa8c8dbc9"
 | 
				
			||||||
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
 | 
					    source: hosted
 | 
				
			||||||
 | 
					    version: "5.8.0+1"
 | 
				
			||||||
 | 
					  dio_web_adapter:
 | 
				
			||||||
 | 
					    dependency: transitive
 | 
				
			||||||
 | 
					    description:
 | 
				
			||||||
 | 
					      name: dio_web_adapter
 | 
				
			||||||
 | 
					      sha256: "7586e476d70caecaf1686d21eee7247ea43ef5c345eab9e0cc3583ff13378d78"
 | 
				
			||||||
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
 | 
					    source: hosted
 | 
				
			||||||
 | 
					    version: "2.1.1"
 | 
				
			||||||
 | 
					  ed25519_edwards:
 | 
				
			||||||
 | 
					    dependency: transitive
 | 
				
			||||||
 | 
					    description:
 | 
				
			||||||
 | 
					      name: ed25519_edwards
 | 
				
			||||||
 | 
					      sha256: "6ce0112d131327ec6d42beede1e5dfd526069b18ad45dcf654f15074ad9276cd"
 | 
				
			||||||
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
 | 
					    source: hosted
 | 
				
			||||||
 | 
					    version: "0.3.1"
 | 
				
			||||||
  fake_async:
 | 
					  fake_async:
 | 
				
			||||||
    dependency: transitive
 | 
					    dependency: transitive
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
@@ -664,6 +704,14 @@ packages:
 | 
				
			|||||||
      url: "https://pub.dev"
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "2.1.8"
 | 
					    version: "2.1.8"
 | 
				
			||||||
 | 
					  pointycastle:
 | 
				
			||||||
 | 
					    dependency: transitive
 | 
				
			||||||
 | 
					    description:
 | 
				
			||||||
 | 
					      name: pointycastle
 | 
				
			||||||
 | 
					      sha256: "92aa3841d083cc4b0f4709b5c74fd6409a3e6ba833ffc7dc6a8fee096366acf5"
 | 
				
			||||||
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
 | 
					    source: hosted
 | 
				
			||||||
 | 
					    version: "4.0.0"
 | 
				
			||||||
  pool:
 | 
					  pool:
 | 
				
			||||||
    dependency: transitive
 | 
					    dependency: transitive
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -67,6 +67,12 @@ dependencies:
 | 
				
			|||||||
  # Logger
 | 
					  # Logger
 | 
				
			||||||
  logging: ^1.3.0
 | 
					  logging: ^1.3.0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  # API authentication
 | 
				
			||||||
 | 
					  dart_jsonwebtoken: ^3.2.0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  # API requests
 | 
				
			||||||
 | 
					  dio: ^5.8.0+1
 | 
				
			||||||
 | 
					
 | 
				
			||||||
dev_dependencies:
 | 
					dev_dependencies:
 | 
				
			||||||
  flutter_test:
 | 
					  flutter_test:
 | 
				
			||||||
    sdk: flutter
 | 
					    sdk: flutter
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user