mirror of
https://gitlab.com/veilid/veilidchat.git
synced 2025-05-07 00:35:06 -04:00
identity work
This commit is contained in:
parent
9eff8f0cb4
commit
ac58e1dea3
15 changed files with 814 additions and 147 deletions
107
lib/state/active_logins_state.dart
Normal file
107
lib/state/active_logins_state.dart
Normal file
|
@ -0,0 +1,107 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:veilid/veilid.dart';
|
||||
import '../entities/entities.dart';
|
||||
|
||||
part 'active_logins_state.g.dart';
|
||||
|
||||
@riverpod
|
||||
class ActiveLoginsState extends _$ActiveLoginsState {
|
||||
VeilidTableDB? _userLoginsTable;
|
||||
ActiveLogins _activeLogins;
|
||||
|
||||
ActiveLoginsState() : _activeLogins = ActiveLogins.empty();
|
||||
|
||||
@override
|
||||
FutureOr<ActiveLogins> build() async {
|
||||
_userLoginsTable ??= await Veilid.instance.openTableDB("login_state", 1);
|
||||
_activeLogins =
|
||||
(await _userLoginsTable!.loadStringJson(0, "active_logins") ??
|
||||
ActiveLogins.empty()) as ActiveLogins;
|
||||
_persistenceRefreshLogic();
|
||||
return _activeLogins;
|
||||
}
|
||||
|
||||
/// Log out of active user
|
||||
Future<void> logout() async {
|
||||
// If no user is active, then logout does nothing
|
||||
if (_activeLogins.activeUserLogin == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove userlogin and set the active user to logged out
|
||||
final newUserLogins = _activeLogins.userLogins.removeWhere(
|
||||
(ul) => _activeLogins.activeUserLogin == ul.accountMasterKey);
|
||||
_activeLogins = _activeLogins.copyWith(
|
||||
activeUserLogin: null, userLogins: newUserLogins);
|
||||
|
||||
// Report changed state
|
||||
state = AsyncValue.data(_activeLogins);
|
||||
}
|
||||
|
||||
/// Log all users
|
||||
Future<void> logoutAll() async {
|
||||
// If no user is active, then logout does nothing
|
||||
if (_activeLogins.activeUserLogin == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove all userlogins and set the active user to logged out
|
||||
_activeLogins = ActiveLogins.empty();
|
||||
|
||||
// Report changed state
|
||||
state = AsyncValue.data(_activeLogins);
|
||||
}
|
||||
|
||||
/// Log out specific user identified by its master public key
|
||||
Future<void> logoutUser(TypedKey user) async {
|
||||
// Remove userlogin and set the active user to logged out
|
||||
final newUserLogins = _activeLogins.userLogins
|
||||
.removeWhere((ul) => user == ul.accountMasterKey);
|
||||
final newActiveUserLogin = _activeLogins.activeUserLogin == user
|
||||
? null
|
||||
: _activeLogins.activeUserLogin;
|
||||
_activeLogins = ActiveLogins(
|
||||
userLogins: newUserLogins, activeUserLogin: newActiveUserLogin);
|
||||
|
||||
// Report changed state
|
||||
state = AsyncValue.data(_activeLogins);
|
||||
}
|
||||
|
||||
/// Attempt a login and if successful make that user active
|
||||
Future<void> login(String publicKey, String password) async {
|
||||
state = await AsyncValue.guard<User?>(() async {
|
||||
return Future.delayed(
|
||||
networkRoundTripTime,
|
||||
() => _dummyUser,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/// Internal method used to listen authentication state changes.
|
||||
/// When the auth object is in a loading state, nothing happens.
|
||||
/// When the auth object is in a error state, we choose to remove the token
|
||||
/// Otherwise, we expect the current auth value to be reflected in our persistence API
|
||||
void _persistenceRefreshLogic() {
|
||||
ref.listenSelf((_, next) {
|
||||
if (next.isLoading) return;
|
||||
if (next.hasError) {
|
||||
sharedPreferences.remove(_sharedPrefsKey);
|
||||
return;
|
||||
}
|
||||
|
||||
final val = next.requireValue;
|
||||
final isAuthenticated = val == null;
|
||||
|
||||
isAuthenticated
|
||||
? sharedPreferences.remove(_sharedPrefsKey)
|
||||
: sharedPreferences.setString(_sharedPrefsKey, val.publicKey);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class UnauthorizedException implements Exception {
|
||||
final String message;
|
||||
const UnauthorizedException(this.message);
|
||||
}
|
25
lib/state/active_logins_state.g.dart
Normal file
25
lib/state/active_logins_state.g.dart
Normal file
|
@ -0,0 +1,25 @@
|
|||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'active_logins_state.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$activeLoginsStateHash() => r'9b8795055e21f15f8fbf13534365725591311cf4';
|
||||
|
||||
/// See also [ActiveLoginsState].
|
||||
@ProviderFor(ActiveLoginsState)
|
||||
final activeLoginsStateProvider =
|
||||
AutoDisposeAsyncNotifierProvider<ActiveLoginsState, ActiveLogins>.internal(
|
||||
ActiveLoginsState.new,
|
||||
name: r'activeLoginsStateProvider',
|
||||
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
|
||||
? null
|
||||
: _$activeLoginsStateHash,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
typedef _$ActiveLoginsState = AutoDisposeAsyncNotifier<ActiveLogins>;
|
||||
// ignore_for_file: unnecessary_raw_strings, subtype_of_sealed_class, invalid_use_of_internal_member, do_not_use_environment, prefer_const_constructors, public_member_api_docs, avoid_private_typedef_functions
|
|
@ -1,110 +0,0 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
class User {
|
||||
final String publicKey;
|
||||
final String secretKey;
|
||||
const User(this.publicKey, this.secretKey);
|
||||
}
|
||||
|
||||
/// A mock of an Authenticated User
|
||||
const _dummyUser = User("", "");
|
||||
|
||||
/// XXXX THIS IS TOTALLY BOGUS FOR NOW
|
||||
/// This notifier holds and handles the authentication state of the application
|
||||
class AuthNotifier extends AutoDisposeAsyncNotifier<User?> {
|
||||
late SharedPreferences sharedPreferences;
|
||||
static const _sharedPrefsKey = 'token';
|
||||
|
||||
/// Mock of the duration of a network request
|
||||
@override
|
||||
FutureOr<User?> build() async {
|
||||
sharedPreferences = await SharedPreferences.getInstance();
|
||||
|
||||
_persistenceRefreshLogic();
|
||||
|
||||
return await _loginRecoveryAttempt();
|
||||
}
|
||||
|
||||
/// Tries to perform a login with the saved token on the persistant storage.
|
||||
/// If _anything_ goes wrong, deletes the internal token and returns a [User.signedOut].
|
||||
Future<User?> _loginRecoveryAttempt() async {
|
||||
try {
|
||||
final savedToken = sharedPreferences.getString(_sharedPrefsKey);
|
||||
if (savedToken == null) {
|
||||
throw const UnauthorizedException(
|
||||
"Couldn't find the authentication token");
|
||||
}
|
||||
|
||||
return await _loginWithToken(savedToken);
|
||||
} catch (_, __) {
|
||||
await sharedPreferences.remove(_sharedPrefsKey);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// Mock of a request performed on logout (might be common, or not, whatevs).
|
||||
Future<void> logout() async {
|
||||
await Future.delayed(networkRoundTripTime);
|
||||
state = const AsyncValue.data(null);
|
||||
}
|
||||
|
||||
/// Mock of a successful login attempt, which results come from the network.
|
||||
Future<void> login(String publicKey, String password) async {
|
||||
state = await AsyncValue.guard<User?>(() async {
|
||||
return Future.delayed(
|
||||
networkRoundTripTime,
|
||||
() => _dummyUser,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/// Mock of a login request performed with a saved token.
|
||||
/// If such request fails, this method will throw an [UnauthorizedException].
|
||||
Future<User> _loginWithToken(String token) async {
|
||||
final logInAttempt = await Future.delayed(
|
||||
networkRoundTripTime,
|
||||
() => true,
|
||||
);
|
||||
|
||||
if (logInAttempt) return _dummyUser;
|
||||
|
||||
throw const UnauthorizedException('401 Unauthorized or something');
|
||||
}
|
||||
|
||||
/// Internal method used to listen authentication state changes.
|
||||
/// When the auth object is in a loading state, nothing happens.
|
||||
/// When the auth object is in a error state, we choose to remove the token
|
||||
/// Otherwise, we expect the current auth value to be reflected in our persistence API
|
||||
void _persistenceRefreshLogic() {
|
||||
ref.listenSelf((_, next) {
|
||||
if (next.isLoading) return;
|
||||
if (next.hasError) {
|
||||
sharedPreferences.remove(_sharedPrefsKey);
|
||||
return;
|
||||
}
|
||||
|
||||
final val = next.requireValue;
|
||||
final isAuthenticated = val == null;
|
||||
|
||||
isAuthenticated
|
||||
? sharedPreferences.remove(_sharedPrefsKey)
|
||||
: sharedPreferences.setString(_sharedPrefsKey, val.publicKey);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
final authNotifierProvider =
|
||||
AutoDisposeAsyncNotifierProvider<AuthNotifier, User?>(() {
|
||||
return AuthNotifier();
|
||||
});
|
||||
|
||||
class UnauthorizedException implements Exception {
|
||||
final String message;
|
||||
const UnauthorizedException(this.message);
|
||||
}
|
||||
|
||||
/// Mock of the duration of a network request
|
||||
const networkRoundTripTime = Duration(milliseconds: 750);
|
109
lib/state/local_account_manager.dart
Normal file
109
lib/state/local_account_manager.dart
Normal file
|
@ -0,0 +1,109 @@
|
|||
import 'dart:convert';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:veilid/veilid.dart';
|
||||
|
||||
import '../entities/entities.dart';
|
||||
import '../entities/proto.dart' as proto;
|
||||
|
||||
part 'local_account_manager.g.dart';
|
||||
|
||||
// Local account manager
|
||||
class LocalAccountManager {
|
||||
final VeilidTableDB _localAccountsTable;
|
||||
final IList<LocalAccount> _localAccounts;
|
||||
|
||||
const LocalAccountManager(
|
||||
{required VeilidTableDB localAccountsTable,
|
||||
required IList<LocalAccount> localAccounts})
|
||||
: _localAccountsTable = localAccountsTable,
|
||||
_localAccounts = localAccounts;
|
||||
|
||||
/// Gets or creates a local account manager
|
||||
static Future<LocalAccountManager> open() async {
|
||||
final localAccountsTable =
|
||||
await Veilid.instance.openTableDB("local_account_manager", 1);
|
||||
final localAccounts =
|
||||
(await localAccountsTable.loadStringJson(0, "local_accounts") ??
|
||||
const IListConst([])) as IList<LocalAccount>;
|
||||
return LocalAccountManager(
|
||||
localAccountsTable: localAccountsTable, localAccounts: localAccounts);
|
||||
}
|
||||
|
||||
/// Flush things to storage
|
||||
Future<void> flush() async {}
|
||||
|
||||
/// Creates a new master identity and returns it with its secrets
|
||||
Future<IdentityMasterWithSecrets> newIdentityMaster() async {
|
||||
final dhtctx = (await Veilid.instance.routingContext())
|
||||
.withPrivacy()
|
||||
.withSequencing(Sequencing.ensureOrdered);
|
||||
final masterRec =
|
||||
await dhtctx.createDHTRecord(const DHTSchema.dflt(oCnt: 1));
|
||||
final identityRec =
|
||||
await dhtctx.createDHTRecord(const DHTSchema.dflt(oCnt: 1));
|
||||
final crypto = await Veilid.instance.bestCryptoSystem();
|
||||
assert(masterRec.key.kind == crypto.kind());
|
||||
assert(identityRec.key.kind == crypto.kind());
|
||||
|
||||
// IdentityMaster
|
||||
final masterRecordKey = masterRec.key;
|
||||
final masterPublicKey = masterRec.owner;
|
||||
final masterSecret = masterRec.ownerSecret!;
|
||||
final masterSigBuf = masterRecordKey.decode()
|
||||
..addAll(masterPublicKey.decode());
|
||||
|
||||
final identityRecordKey = identityRec.key;
|
||||
final identityPublicKey = identityRec.owner;
|
||||
final identitySecret = identityRec.ownerSecret!;
|
||||
final identitySigBuf = identityRecordKey.decode()
|
||||
..addAll(identityPublicKey.decode());
|
||||
|
||||
final identitySignature =
|
||||
await crypto.sign(masterPublicKey, masterSecret, identitySigBuf);
|
||||
final masterSignature =
|
||||
await crypto.sign(identityPublicKey, identitySecret, masterSigBuf);
|
||||
|
||||
final identityMaster = IdentityMaster(
|
||||
identityRecordKey: identityRecordKey,
|
||||
identityPublicKey: identityPublicKey,
|
||||
masterRecordKey: masterRecordKey,
|
||||
masterPublicKey: masterPublicKey,
|
||||
identitySignature: identitySignature,
|
||||
masterSignature: masterSignature);
|
||||
|
||||
// Write identity master to master dht key
|
||||
final identityMasterBytes =
|
||||
Uint8List.fromList(utf8.encode(jsonEncode(identityMaster)));
|
||||
await dhtctx.setDHTValue(masterRecordKey, 0, identityMasterBytes);
|
||||
|
||||
return IdentityMasterWithSecrets(
|
||||
identityMaster: identityMaster,
|
||||
masterSecret: masterSecret,
|
||||
identitySecret: identitySecret);
|
||||
}
|
||||
|
||||
/// Creates a new account associated with master identity
|
||||
Future<LocalAccount> newAccount(
|
||||
IdentityMaster identityMaster,
|
||||
SecretKey identitySecret,
|
||||
EncryptionKeyType encryptionKeyType,
|
||||
String encryptionKey) async {
|
||||
//
|
||||
|
||||
return LocalAccount(
|
||||
identityMaster: identityMaster,
|
||||
identitySecretKeyBytes: identitySecretBytes,
|
||||
encryptionKeyType: encryptionKeyType,
|
||||
biometricsEnabled: false,
|
||||
hiddenAccount: false,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@riverpod
|
||||
Future<LocalAccountManager> localAccountManager(LocalAccountManagerRef ref) {
|
||||
return LocalAccountManager.open();
|
||||
}
|
|
@ -1,2 +1,2 @@
|
|||
export 'connection_state.dart';
|
||||
export 'auth.dart';
|
||||
export 'active_logins_state.dart';
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue