diff --git a/vidaia/lib/helpers/constants.dart b/vidaia/lib/helpers/constants.dart index 2e6d40b..0a2133e 100644 --- a/vidaia/lib/helpers/constants.dart +++ b/vidaia/lib/helpers/constants.dart @@ -8,3 +8,4 @@ const AUTH0_CLIENT_ID = const AUTH0_ISSUER = 'https://$AUTH0_DOMAIN'; const BUNDLE_IDENTIFIER = 'ch.saynode.vidaia'; const AUTH0_REDIRECT_URI = '$BUNDLE_IDENTIFIER://login-callback'; +const REFRESH_TOKEN_KEY = 'refresh_token'; diff --git a/vidaia/lib/models/HistoryEntry.g.dart b/vidaia/lib/models/HistoryEntry.g.dart index 904f3da..0bdd4ed 100644 --- a/vidaia/lib/models/HistoryEntry.g.dart +++ b/vidaia/lib/models/HistoryEntry.g.dart @@ -12,7 +12,9 @@ HistoryEntry _$HistoryEntryFromJson(Map json) => HistoryEntry( json['isReceived'] as bool, ); -Map _$HistoryEntryToJson(HistoryEntry instance) => { +Map _$HistoryEntryToJson(HistoryEntry instance) => + { 'product': instance.product, 'date': instance.date, + 'isReceived': instance.isReceived, }; diff --git a/vidaia/lib/models/auth0_id_token.dart b/vidaia/lib/models/auth0_id_token.dart new file mode 100644 index 0000000..e386a47 --- /dev/null +++ b/vidaia/lib/models/auth0_id_token.dart @@ -0,0 +1,47 @@ +import 'package:json_annotation/json_annotation.dart'; +part 'auth0_id_token.g.dart'; + +@JsonSerializable() +class Auth0IdToken { + Auth0IdToken({ + required this.nickname, + required this.name, + required this.email, + required this.picture, + required this.updatedAt, + required this.iss, + required this.sub, // aka subject... the userId. + required this.aud, + required this.iat, + required this.exp, + this.authTime, + }); + + final String nickname; + final String name; + final String picture; + + @JsonKey(name: 'updated_at') + final String updatedAt; + + final String iss; + + // In OIDC, "sub" means "subject identifier", + // which for our purposes is the user ID. + // This getter makes it easier to understand. + String get userId => sub; + final String sub; + + final String aud; + final String email; + final int iat; + final int exp; + + @JsonKey(name: 'auth_time') + final int? authTime; // this might be null for the first time login + + factory Auth0IdToken.fromJson(Map json) => + _$Auth0IdTokenFromJson(json); + + Map toJson() => _$Auth0IdTokenToJson(this); +} diff --git a/vidaia/lib/models/auth0_id_token.g.dart b/vidaia/lib/models/auth0_id_token.g.dart new file mode 100644 index 0000000..9d32d1a --- /dev/null +++ b/vidaia/lib/models/auth0_id_token.g.dart @@ -0,0 +1,36 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'auth0_id_token.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +Auth0IdToken _$Auth0IdTokenFromJson(Map json) => Auth0IdToken( + nickname: json['nickname'] as String, + name: json['name'] as String, + email: json['email'] as String, + picture: json['picture'] as String, + updatedAt: json['updated_at'] as String, + iss: json['iss'] as String, + sub: json['sub'] as String, + aud: json['aud'] as String, + iat: json['iat'] as int, + exp: json['exp'] as int, + authTime: json['auth_time'] as int?, + ); + +Map _$Auth0IdTokenToJson(Auth0IdToken instance) => + { + 'nickname': instance.nickname, + 'name': instance.name, + 'picture': instance.picture, + 'updated_at': instance.updatedAt, + 'iss': instance.iss, + 'sub': instance.sub, + 'aud': instance.aud, + 'email': instance.email, + 'iat': instance.iat, + 'exp': instance.exp, + 'auth_time': instance.authTime, + }; diff --git a/vidaia/lib/models/auth0_user.dart b/vidaia/lib/models/auth0_user.dart new file mode 100644 index 0000000..867122c --- /dev/null +++ b/vidaia/lib/models/auth0_user.dart @@ -0,0 +1,31 @@ +import 'package:json_annotation/json_annotation.dart'; +part 'auth0_user.g.dart'; + +@JsonSerializable() +class Auth0User { + Auth0User({ + required this.nickname, + required this.name, + required this.email, + required this.picture, + required this.updatedAt, + required this.sub, + }); + final String nickname; + final String name; + final String picture; + + @JsonKey(name: 'updated_at') + final String updatedAt; + + // userID getter to understand it easier + String get id => sub; + final String sub; + + final String email; + + factory Auth0User.fromJson(Map json) => + _$Auth0UserFromJson(json); + + Map toJson() => _$Auth0UserToJson(this); +} diff --git a/vidaia/lib/models/auth0_user.g.dart b/vidaia/lib/models/auth0_user.g.dart new file mode 100644 index 0000000..7d6c118 --- /dev/null +++ b/vidaia/lib/models/auth0_user.g.dart @@ -0,0 +1,25 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'auth0_user.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +Auth0User _$Auth0UserFromJson(Map json) => Auth0User( + nickname: json['nickname'] as String, + name: json['name'] as String, + email: json['email'] as String, + picture: json['picture'] as String, + updatedAt: json['updated_at'] as String, + sub: json['sub'] as String, + ); + +Map _$Auth0UserToJson(Auth0User instance) => { + 'nickname': instance.nickname, + 'name': instance.name, + 'picture': instance.picture, + 'updated_at': instance.updatedAt, + 'sub': instance.sub, + 'email': instance.email, + }; diff --git a/vidaia/lib/pages/login_page.dart b/vidaia/lib/pages/login_page.dart index 691e5ea..6516ed7 100644 --- a/vidaia/lib/pages/login_page.dart +++ b/vidaia/lib/pages/login_page.dart @@ -5,14 +5,31 @@ import 'package:vidaia/main.dart'; import 'package:vidaia/pages/home/home_page_loader.dart'; import 'package:vidaia/utils/globals.dart'; import 'package:vidaia/utils/wallet.dart'; +import 'package:vidaia/services/auth_service.dart'; const users = { 'a@a.com': 'password', }; -class LoginPage extends StatelessWidget { +class LoginPage extends StatefulWidget { const LoginPage({Key? key}) : super(key: key); + + @override + State createState() => _LoginPageState(); +} + +class _LoginPageState extends State { Duration get loginTime => const Duration(milliseconds: 2250); + bool isProgressing = false; + bool isLoggedIn = false; + String errorMessage = ''; + String? name; + + @override + void initState() { + initAction(); + super.initState(); + } Future _authUser(LoginData data) { debugPrint('Name: ${data.name}, Password: ${data.password}'); @@ -54,24 +71,86 @@ class LoginPage extends StatelessWidget { Widget build(BuildContext context) { return Container( color: primaryColor, - child: FlutterLogin( - userType: LoginUserType.name, - logo: const AssetImage('assets/images/vidaia-live-sustainably.png'), - messages: - LoginMessages(userHint: 'Email', passwordHint: 'password'.tr()), - onLogin: _authUser, - onSignup: _signupUser, - onSubmitAnimationCompleted: () { - Navigator.push( - context, - MaterialPageRoute(builder: (context) => HomePage2()), - ); - if (!mnemonicNoted) { - showMnemonicAlert(context); - } - }, - onRecoverPassword: _recoverPassword, + child: Column( + children: [ + // FlutterLogin( + // userType: LoginUserType.name, + // logo: const AssetImage('assets/images/vidaia-live-sustainably.png'), + // messages: + // LoginMessages(userHint: 'Email', passwordHint: 'password'.tr()), + // onLogin: _authUser, + // onSignup: _signupUser, + // onSubmitAnimationCompleted: () { + // Navigator.push( + // context, + // MaterialPageRoute(builder: (context) => HomePage2()), + // ); + // if (!mnemonicNoted) { + // showMnemonicAlert(context); + // } + // }, + // onRecoverPassword: _recoverPassword, + // ), + TextButton( + onPressed: loginAction, + child: const Text('Auth0 Login | Register'), + ), + if (isProgressing) + CircularProgressIndicator() + else if (!isLoggedIn) + TextButton( + onPressed: loginAction, + child: const Text('Auth0 Login | Register'), + ) + else + Text('Welcome $name'), + ], ), ); } + + setSuccessAuthState() { + setState(() { + isProgressing = false; + isLoggedIn = true; + name = AuthService.instance.idToken?.name; + }); + + Navigator.push( + context, + MaterialPageRoute(builder: (context) => HomePage2()), + ); + } + + setLoadingState() { + setState(() { + isProgressing = true; + errorMessage = ''; + }); + } + + Future loginAction() async { + setLoadingState(); + final message = await AuthService.instance.login(); + if (message == 'Success') { + setSuccessAuthState(); + } else { + setState(() { + isProgressing = false; + errorMessage = message; + }); + } + } + + initAction() async { + setLoadingState(); + final bool isAuth = await AuthService.instance.init(); + if (isAuth) { + setSuccessAuthState(); + } else { + setState(() { + isProgressing = false; + }); + } + } } diff --git a/vidaia/lib/services/auth_service.dart b/vidaia/lib/services/auth_service.dart new file mode 100644 index 0000000..3727d27 --- /dev/null +++ b/vidaia/lib/services/auth_service.dart @@ -0,0 +1,125 @@ +import 'dart:convert'; +import 'package:http/http.dart' as http; +import 'package:flutter/services.dart'; +import 'package:flutter_appauth/flutter_appauth.dart'; +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:vidaia/helpers/constants.dart'; +import 'package:vidaia/models/auth0_id_token.dart'; +import 'package:vidaia/models/auth0_user.dart'; + +class AuthService { + Auth0User? profile; + Auth0IdToken? idToken; + String? auth0AccessToken; + + static final AuthService instance = AuthService._internal(); + factory AuthService() => instance; + AuthService._internal(); + + final FlutterAppAuth appAuth = FlutterAppAuth(); + final FlutterSecureStorage secureStorage = const FlutterSecureStorage(); + + Future init() async { + final storedRefreshToken = await secureStorage.read(key: REFRESH_TOKEN_KEY); + + if (storedRefreshToken == null) { + return false; + } + + try { + final TokenResponse? result = await appAuth.token( + TokenRequest( + AUTH0_CLIENT_ID, + AUTH0_REDIRECT_URI, + issuer: AUTH0_ISSUER, + refreshToken: storedRefreshToken, + ), + ); + final String setResult = await _setLocalVariables(result); + return setResult == 'Success'; + } catch (e, s) { + print('error on Refresh Token: $e - stack: $s'); + // logOut() possibly + return false; + } + } + + Future login() async { + try { + final authorizationTokenRequest = AuthorizationTokenRequest( + AUTH0_CLIENT_ID, + AUTH0_REDIRECT_URI, + issuer: AUTH0_ISSUER, + scopes: ['openid', 'profile', 'offline_access', 'email'], + ); + + final AuthorizationTokenResponse? result = + await appAuth.authorizeAndExchangeCode( + authorizationTokenRequest, + ); + + return await _setLocalVariables(result); + } on PlatformException { + return 'User has cancelled or no internet!'; + } catch (e) { + return 'Unkown Error!'; + } + } + + Auth0IdToken parseIdToken(String idToken) { + final parts = idToken.split(r'.'); + assert(parts.length == 3); + + final Map json = jsonDecode( + utf8.decode( + base64Url.decode( + base64Url.normalize(parts[1]), + ), + ), + ); + + return Auth0IdToken.fromJson(json); + } + + Future getUserDetails(String accessToken) async { + final url = Uri.https( + AUTH0_DOMAIN, + '/userinfo', + ); + + final response = await http.get( + url, + headers: {'Authorization': 'Bearer $accessToken'}, + ); + + print('getUserDetails ${response.body}'); + + if (response.statusCode == 200) { + return Auth0User.fromJson(jsonDecode(response.body)); + } else { + throw Exception('Failed to get user details'); + } + } + + Future _setLocalVariables(result) async { + final bool isValidResult = + result != null && result.accessToken != null && result.idToken != null; + + if (isValidResult) { + auth0AccessToken = result.accessToken; + idToken = parseIdToken(result.idToken!); + profile = await getUserDetails(result.accessToken!); + + if (result.refreshToken != null) { + await secureStorage.write( + key: REFRESH_TOKEN_KEY, + value: result.refreshToken, + ); + } + + return 'Success'; + } else { + return 'Something is Wrong!'; + } + } +}