feat: add phone auth with 2FA, multi-account storage
This commit is contained in:
@@ -0,0 +1,261 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:path/path.dart';
|
||||
import 'package:sqflite/sqflite.dart';
|
||||
import 'package:sqflite_common_ffi/sqflite_ffi.dart';
|
||||
|
||||
class ProfileData {
|
||||
final int id;
|
||||
final String firstName;
|
||||
final String? lastName;
|
||||
final int phone;
|
||||
final int? photoId;
|
||||
final String? baseUrl;
|
||||
final String? baseRawUrl;
|
||||
final String country;
|
||||
final int accountStatus;
|
||||
final int updateTime;
|
||||
|
||||
ProfileData({
|
||||
required this.id,
|
||||
required this.firstName,
|
||||
this.lastName,
|
||||
required this.phone,
|
||||
this.photoId,
|
||||
this.baseUrl,
|
||||
this.baseRawUrl,
|
||||
required this.country,
|
||||
required this.accountStatus,
|
||||
required this.updateTime,
|
||||
});
|
||||
|
||||
factory ProfileData.fromServerMap(Map<dynamic, dynamic> contact) {
|
||||
final names = contact['names'];
|
||||
String firstName = '';
|
||||
String? lastName;
|
||||
|
||||
if (names is List && names.isNotEmpty) {
|
||||
final name = names.firstWhere(
|
||||
(n) => n is Map && n['type'] == 'ONEME',
|
||||
orElse: () => names.first,
|
||||
) as Map;
|
||||
firstName = (name['firstName'] as String?) ?? '';
|
||||
lastName = name['lastName'] as String?;
|
||||
}
|
||||
|
||||
return ProfileData(
|
||||
id: contact['id'] as int,
|
||||
firstName: firstName,
|
||||
lastName: lastName,
|
||||
phone: contact['phone'] as int,
|
||||
photoId: contact['photoId'] as int?,
|
||||
baseUrl: contact['baseUrl'] as String?,
|
||||
baseRawUrl: contact['baseRawUrl'] as String?,
|
||||
country: (contact['country'] as String?) ?? '',
|
||||
accountStatus: (contact['accountStatus'] as int?) ?? 0,
|
||||
updateTime: (contact['updateTime'] as int?) ?? 0,
|
||||
);
|
||||
}
|
||||
|
||||
factory ProfileData.fromDbRow(Map<String, dynamic> row) {
|
||||
return ProfileData(
|
||||
id: row['id'] as int,
|
||||
firstName: row['first_name'] as String,
|
||||
lastName: row['last_name'] as String?,
|
||||
phone: row['phone'] as int,
|
||||
photoId: row['photo_id'] as int?,
|
||||
baseUrl: row['base_url'] as String?,
|
||||
baseRawUrl: row['base_raw_url'] as String?,
|
||||
country: row['country'] as String,
|
||||
accountStatus: row['account_status'] as int,
|
||||
updateTime: row['update_time'] as int,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toDbRow() => {
|
||||
'id': id,
|
||||
'first_name': firstName,
|
||||
'last_name': lastName,
|
||||
'phone': phone,
|
||||
'photo_id': photoId,
|
||||
'base_url': baseUrl,
|
||||
'base_raw_url': baseRawUrl,
|
||||
'country': country,
|
||||
'account_status': accountStatus,
|
||||
'update_time': updateTime,
|
||||
};
|
||||
}
|
||||
|
||||
abstract class SyncKey {
|
||||
static const chatsSync = 'chats_sync';
|
||||
static const contactsSync = 'contacts_sync';
|
||||
static const callsSync = 'calls_sync';
|
||||
static const draftsSync = 'drafts_sync';
|
||||
static const bannersSync = 'banners_sync';
|
||||
static const presenceSync = 'presence_sync';
|
||||
static const lastLogin = 'last_login';
|
||||
static const configHash = 'config_hash';
|
||||
static const chatCacheFingerprint = 'chat_cache_fingerprint';
|
||||
static const serverTime = 'server_time';
|
||||
}
|
||||
|
||||
class AppDatabase {
|
||||
static Database? _db;
|
||||
|
||||
static Future<void> init() async {
|
||||
if (Platform.isLinux || Platform.isWindows || Platform.isMacOS) {
|
||||
sqfliteFfiInit();
|
||||
databaseFactory = databaseFactoryFfi;
|
||||
}
|
||||
}
|
||||
|
||||
static Future<Database> get _instance async {
|
||||
_db ??= await _open();
|
||||
return _db!;
|
||||
}
|
||||
|
||||
static Future<Database> _open() async {
|
||||
final dbPath = await getDatabasesPath();
|
||||
return openDatabase(
|
||||
join(dbPath, 'komet.db'),
|
||||
version: 2,
|
||||
onOpen: (db) => db.execute('PRAGMA foreign_keys = ON'),
|
||||
onCreate: (db, _) => _createTables(db),
|
||||
onUpgrade: (db, oldVersion, newVersion) async {
|
||||
if (oldVersion < 2) {
|
||||
await db.execute(
|
||||
'ALTER TABLE profile ADD COLUMN is_active INTEGER NOT NULL DEFAULT 0',
|
||||
);
|
||||
await db.execute('DROP TABLE IF EXISTS sync_state');
|
||||
await db.execute(_syncStateSchema);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
static Future<void> _createTables(Database db) async {
|
||||
await db.execute('''
|
||||
CREATE TABLE profile (
|
||||
id INTEGER PRIMARY KEY,
|
||||
first_name TEXT NOT NULL,
|
||||
last_name TEXT,
|
||||
phone INTEGER NOT NULL,
|
||||
photo_id INTEGER,
|
||||
base_url TEXT,
|
||||
base_raw_url TEXT,
|
||||
country TEXT NOT NULL DEFAULT '',
|
||||
account_status INTEGER NOT NULL DEFAULT 0,
|
||||
update_time INTEGER NOT NULL DEFAULT 0,
|
||||
is_active INTEGER NOT NULL DEFAULT 0
|
||||
)
|
||||
''');
|
||||
await db.execute(_syncStateSchema);
|
||||
}
|
||||
|
||||
static const _syncStateSchema = '''
|
||||
CREATE TABLE sync_state (
|
||||
account_id INTEGER NOT NULL REFERENCES profile(id) ON DELETE CASCADE,
|
||||
key TEXT NOT NULL,
|
||||
value TEXT NOT NULL,
|
||||
PRIMARY KEY (account_id, key)
|
||||
)
|
||||
''';
|
||||
|
||||
static Future<void> saveProfile(ProfileData profile) async {
|
||||
final db = await _instance;
|
||||
await db.insert(
|
||||
'profile',
|
||||
profile.toDbRow(),
|
||||
conflictAlgorithm: ConflictAlgorithm.replace,
|
||||
);
|
||||
}
|
||||
|
||||
static Future<ProfileData?> loadProfile(int accountId) async {
|
||||
final db = await _instance;
|
||||
final rows = await db.query(
|
||||
'profile',
|
||||
where: 'id = ?',
|
||||
whereArgs: [accountId],
|
||||
limit: 1,
|
||||
);
|
||||
if (rows.isEmpty) return null;
|
||||
return ProfileData.fromDbRow(rows.first);
|
||||
}
|
||||
|
||||
static Future<List<ProfileData>> loadAllProfiles() async {
|
||||
final db = await _instance;
|
||||
final rows = await db.query('profile', orderBy: 'is_active DESC, id ASC');
|
||||
return rows.map(ProfileData.fromDbRow).toList();
|
||||
}
|
||||
|
||||
static Future<ProfileData?> loadActiveProfile() async {
|
||||
final db = await _instance;
|
||||
final rows = await db.query(
|
||||
'profile',
|
||||
where: 'is_active = 1',
|
||||
limit: 1,
|
||||
);
|
||||
if (rows.isEmpty) return null;
|
||||
return ProfileData.fromDbRow(rows.first);
|
||||
}
|
||||
|
||||
static Future<void> setActiveAccount(int accountId) async {
|
||||
final db = await _instance;
|
||||
await db.transaction((txn) async {
|
||||
await txn.update('profile', {'is_active': 0});
|
||||
await txn.update(
|
||||
'profile',
|
||||
{'is_active': 1},
|
||||
where: 'id = ?',
|
||||
whereArgs: [accountId],
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
static Future<void> deleteAccount(int accountId) async {
|
||||
final db = await _instance;
|
||||
await db.delete('profile', where: 'id = ?', whereArgs: [accountId]);
|
||||
}
|
||||
|
||||
static Future<void> setSyncValue(
|
||||
int accountId,
|
||||
String key,
|
||||
String value,
|
||||
) async {
|
||||
final db = await _instance;
|
||||
await db.insert(
|
||||
'sync_state',
|
||||
{'account_id': accountId, 'key': key, 'value': value},
|
||||
conflictAlgorithm: ConflictAlgorithm.replace,
|
||||
);
|
||||
}
|
||||
|
||||
static Future<String?> getSyncValue(int accountId, String key) async {
|
||||
final db = await _instance;
|
||||
final rows = await db.query(
|
||||
'sync_state',
|
||||
where: 'account_id = ? AND key = ?',
|
||||
whereArgs: [accountId, key],
|
||||
limit: 1,
|
||||
);
|
||||
if (rows.isEmpty) return null;
|
||||
return rows.first['value'] as String;
|
||||
}
|
||||
|
||||
static Future<Map<String, String>> getAllSyncValues(int accountId) async {
|
||||
final db = await _instance;
|
||||
final rows = await db.query(
|
||||
'sync_state',
|
||||
where: 'account_id = ?',
|
||||
whereArgs: [accountId],
|
||||
);
|
||||
return {
|
||||
for (final row in rows) row['key'] as String: row['value'] as String,
|
||||
};
|
||||
}
|
||||
|
||||
static Future<void> close() async {
|
||||
await _db?.close();
|
||||
_db = null;
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,42 @@
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
|
||||
class TokenStorage {
|
||||
static const _key = 'auth_token';
|
||||
static const _tokenPrefix = 'auth_token_';
|
||||
static const _activeAccountKey = 'active_account_id';
|
||||
|
||||
static const _storage = FlutterSecureStorage(
|
||||
aOptions: AndroidOptions(encryptedSharedPreferences: true),
|
||||
);
|
||||
|
||||
static Future<void> save(String token) =>
|
||||
_storage.write(key: _key, value: token);
|
||||
static Future<void> saveToken(String token, int accountId) =>
|
||||
_storage.write(key: '$_tokenPrefix$accountId', value: token);
|
||||
|
||||
static Future<String?> read() => _storage.read(key: _key);
|
||||
static Future<String?> readToken(int accountId) =>
|
||||
_storage.read(key: '$_tokenPrefix$accountId');
|
||||
|
||||
static Future<void> delete() => _storage.delete(key: _key);
|
||||
static Future<void> deleteToken(int accountId) =>
|
||||
_storage.delete(key: '$_tokenPrefix$accountId');
|
||||
|
||||
static Future<void> setActiveAccount(int accountId) =>
|
||||
_storage.write(key: _activeAccountKey, value: accountId.toString());
|
||||
|
||||
static Future<int?> getActiveAccountId() async {
|
||||
final val = await _storage.read(key: _activeAccountKey);
|
||||
return val != null ? int.tryParse(val) : null;
|
||||
}
|
||||
|
||||
static Future<String?> readActiveToken() async {
|
||||
final id = await getActiveAccountId();
|
||||
if (id == null) return null;
|
||||
return readToken(id);
|
||||
}
|
||||
|
||||
/// Удаляет токен аккаунта и, если он был активным, сбрасывает активный аккаунт.
|
||||
static Future<void> deleteAccount(int accountId) async {
|
||||
await deleteToken(accountId);
|
||||
final activeId = await getActiveAccountId();
|
||||
if (activeId == accountId) {
|
||||
await _storage.delete(key: _activeAccountKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user