Добавлен пароль расшифровки сообщений
This commit is contained in:
parent
275dd2e024
commit
8a0a237e18
|
|
@ -1,5 +1,6 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
import 'package:chepuhagram/domain/services/api_service.dart';
|
||||||
import 'package:web_socket_channel/web_socket_channel.dart';
|
import 'package:web_socket_channel/web_socket_channel.dart';
|
||||||
import 'package:web_socket_channel/status.dart' as status;
|
import 'package:web_socket_channel/status.dart' as status;
|
||||||
import 'package:chepuhagram/core/constants.dart';
|
import 'package:chepuhagram/core/constants.dart';
|
||||||
|
|
@ -12,7 +13,8 @@ class SocketService {
|
||||||
// Поток, который будут слушать провайдеры
|
// Поток, который будут слушать провайдеры
|
||||||
Stream<Map<String, dynamic>> get messages => _messageController.stream;
|
Stream<Map<String, dynamic>> get messages => _messageController.stream;
|
||||||
|
|
||||||
void connect(String token) {
|
Future<void> connect(ApiService apiService) async {
|
||||||
|
final token = await apiService.getAccessToken();
|
||||||
if (_channel != null) return; // Уже подключены
|
if (_channel != null) return; // Уже подключены
|
||||||
|
|
||||||
// В FastAPI эндпоинт обычно ожидает токен в URL или подзаголовке
|
// В FastAPI эндпоинт обычно ожидает токен в URL или подзаголовке
|
||||||
|
|
@ -25,14 +27,14 @@ class SocketService {
|
||||||
final decoded = jsonDecode(data);
|
final decoded = jsonDecode(data);
|
||||||
_messageController.add(decoded);
|
_messageController.add(decoded);
|
||||||
},
|
},
|
||||||
onError: (error) => _reconnect(token),
|
onError: (error) => _reconnect(apiService),
|
||||||
onDone: () => _reconnect(token),
|
onDone: () => _reconnect(apiService),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _reconnect(String token) {
|
Future<void> _reconnect(ApiService apiService) async {
|
||||||
_channel = null;
|
_channel = null;
|
||||||
Future.delayed(const Duration(seconds: 5), () => connect(token));
|
Future.delayed(const Duration(seconds: 5), () => connect(apiService));
|
||||||
}
|
}
|
||||||
|
|
||||||
void sendMessage(Map<String, dynamic> data) {
|
void sendMessage(Map<String, dynamic> data) {
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,6 @@ class ApiService extends ChangeNotifier {
|
||||||
final _client = http.Client();
|
final _client = http.Client();
|
||||||
final _storage = const FlutterSecureStorage();
|
final _storage = const FlutterSecureStorage();
|
||||||
|
|
||||||
|
|
||||||
Future<bool> refreshToken() async {
|
Future<bool> refreshToken() async {
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
|
|
||||||
|
|
@ -49,7 +48,8 @@ class ApiService extends ChangeNotifier {
|
||||||
String? token = await _storage.read(key: 'access_token');
|
String? token = await _storage.read(key: 'access_token');
|
||||||
|
|
||||||
if (token != null) {
|
if (token != null) {
|
||||||
bool isExpiredSoon = JwtDecoder.isExpired(token) ||
|
bool isExpiredSoon =
|
||||||
|
JwtDecoder.isExpired(token) ||
|
||||||
JwtDecoder.getRemainingTime(token).inMinutes < 2;
|
JwtDecoder.getRemainingTime(token).inMinutes < 2;
|
||||||
|
|
||||||
if (isExpiredSoon) {
|
if (isExpiredSoon) {
|
||||||
|
|
@ -63,4 +63,31 @@ class ApiService extends ChangeNotifier {
|
||||||
}
|
}
|
||||||
return token;
|
return token;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<bool> setPublicKey(String publicKey) async {
|
||||||
|
notifyListeners();
|
||||||
|
|
||||||
|
try {
|
||||||
|
final token = await getAccessToken();
|
||||||
|
final response = await _client.post(
|
||||||
|
Uri.http(AppConstants.baseUrl, 'auth/set-public-key'),
|
||||||
|
body: jsonEncode({'public_key': publicKey}),
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': 'Bearer $token',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.statusCode == 200) {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
print("Ошибка установки ключа: ${response.statusCode}");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
rethrow;
|
||||||
|
} finally {
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,137 @@
|
||||||
|
import 'package:cryptography/cryptography.dart';
|
||||||
|
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
class CryptoService {
|
||||||
|
final _storage = const FlutterSecureStorage();
|
||||||
|
final algorithm = X25519();
|
||||||
|
final aesGcm = AesGcm.with256bits();
|
||||||
|
|
||||||
|
Future<Map<String, String>> initAccountSecurity(String masterPassword) async {
|
||||||
|
// Генерируем пару X25519 ключей
|
||||||
|
final keyPair = await algorithm.newKeyPair();
|
||||||
|
final publicKey = await keyPair.extractPublicKey();
|
||||||
|
final privateKeyBytes = await keyPair.extractPrivateKeyBytes();
|
||||||
|
|
||||||
|
// Сохраняем приватный ключ в Secure Storage
|
||||||
|
await _storage.write(
|
||||||
|
key: 'private_key',
|
||||||
|
value: base64Encode(privateKeyBytes),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Шифруем приватный ключ с мастер-паролем (AES-GCM)
|
||||||
|
final masterKey = await _deriveKeyFromPassword(masterPassword);
|
||||||
|
final nonce = aesGcm.newNonce();
|
||||||
|
final encrypted = await aesGcm.encrypt(
|
||||||
|
privateKeyBytes,
|
||||||
|
secretKey: masterKey,
|
||||||
|
nonce: nonce,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Комбинируем nonce и зашифрованные данные
|
||||||
|
final encryptedData = nonce + encrypted.mac.bytes + encrypted.cipherText;
|
||||||
|
final encryptedPrivateKey = base64Encode(encryptedData);
|
||||||
|
|
||||||
|
final publicKeyBase64 = base64Encode(publicKey.bytes);
|
||||||
|
|
||||||
|
return {
|
||||||
|
'public_key': publicKeyBase64,
|
||||||
|
'encrypted_private_key': encryptedPrivateKey,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<String> decryptPrivateKey(
|
||||||
|
String encryptedPrivateKey,
|
||||||
|
String masterPassword,
|
||||||
|
) async {
|
||||||
|
try {
|
||||||
|
final encryptedData = base64Decode(encryptedPrivateKey);
|
||||||
|
|
||||||
|
// Разделяем nonce и зашифрованные данные
|
||||||
|
final nonce = encryptedData.sublist(0, 12); // GCM nonce = 12 bytes
|
||||||
|
final macBytes = encryptedData.sublist(12, 28);
|
||||||
|
final cipherText = encryptedData.sublist(28);
|
||||||
|
|
||||||
|
final masterKey = await _deriveKeyFromPassword(masterPassword);
|
||||||
|
|
||||||
|
final decrypted = await aesGcm.decrypt(
|
||||||
|
SecretBox(cipherText, nonce: nonce, mac: Mac(macBytes)),
|
||||||
|
secretKey: masterKey,
|
||||||
|
);
|
||||||
|
|
||||||
|
return base64Encode(decrypted);
|
||||||
|
} catch (e) {
|
||||||
|
throw Exception('Неверный мастер-пароль или поврежденные данные');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<SecretKey> _deriveKeyFromPassword(String password) async {
|
||||||
|
final pbkdf2 = Pbkdf2(
|
||||||
|
macAlgorithm: Hmac.sha256(),
|
||||||
|
iterations: 10000,
|
||||||
|
bits: 256,
|
||||||
|
);
|
||||||
|
|
||||||
|
final salt = utf8.encode('chepuhagram_salt');
|
||||||
|
return await pbkdf2.deriveKeyFromPassword(password: password, nonce: salt);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<SecretKey> deriveSharedSecret(
|
||||||
|
String myPrivateKeyBase64,
|
||||||
|
String theirPublicKeyBase64,
|
||||||
|
) async {
|
||||||
|
final myKeyPair = await algorithm.newKeyPairFromSeed(base64Decode(myPrivateKeyBase64));
|
||||||
|
final theirPublicKey = SimplePublicKey(
|
||||||
|
base64Decode(theirPublicKeyBase64),
|
||||||
|
type: KeyPairType.x25519,
|
||||||
|
);
|
||||||
|
|
||||||
|
return await algorithm.sharedSecretKey(
|
||||||
|
keyPair: myKeyPair,
|
||||||
|
remotePublicKey: theirPublicKey,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<String> encryptMessage(String text, SecretKey sharedKey) async {
|
||||||
|
final nonce = aesGcm.newNonce();
|
||||||
|
final encrypted = await aesGcm.encrypt(
|
||||||
|
utf8.encode(text),
|
||||||
|
secretKey: sharedKey,
|
||||||
|
nonce: nonce,
|
||||||
|
);
|
||||||
|
// Сохраняем Nonce + MAC + CipherText для передачи
|
||||||
|
return base64Encode(nonce + encrypted.mac.bytes + encrypted.cipherText);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<String> decryptMessage(String base64Data, SecretKey sharedKey) async {
|
||||||
|
final data = base64Decode(base64Data);
|
||||||
|
|
||||||
|
final nonce = data.sublist(0, 12);
|
||||||
|
final mac = data.sublist(12, 28);
|
||||||
|
final cipherText = data.sublist(28);
|
||||||
|
|
||||||
|
final decrypted = await aesGcm.decrypt(
|
||||||
|
SecretBox(cipherText, nonce: nonce, mac: Mac(mac)),
|
||||||
|
secretKey: sharedKey,
|
||||||
|
);
|
||||||
|
|
||||||
|
return utf8.decode(decrypted);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<String?> getPrivateKey() async {
|
||||||
|
return await _storage.read(key: 'private_key');
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> hasPrivateKey() async {
|
||||||
|
final key = await _storage.read(key: 'private_key');
|
||||||
|
return key != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> savePrivateKey(String privateKey) async {
|
||||||
|
await _storage.write(key: 'private_key', value: privateKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> deletePrivateKey() async {
|
||||||
|
await _storage.delete(key: 'private_key');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -5,6 +5,7 @@ import '/core/constants.dart';
|
||||||
import 'package:http/http.dart' as http;
|
import 'package:http/http.dart' as http;
|
||||||
import 'package:chepuhagram/domain/services/api_service.dart';
|
import 'package:chepuhagram/domain/services/api_service.dart';
|
||||||
import 'package:chepuhagram/data/datasources/ws_client.dart';
|
import 'package:chepuhagram/data/datasources/ws_client.dart';
|
||||||
|
import 'package:chepuhagram/domain/services/crypto_service.dart';
|
||||||
|
|
||||||
class AuthProvider extends ChangeNotifier {
|
class AuthProvider extends ChangeNotifier {
|
||||||
bool _isLoading = false;
|
bool _isLoading = false;
|
||||||
|
|
@ -13,21 +14,30 @@ class AuthProvider extends ChangeNotifier {
|
||||||
int? _currentUserId;
|
int? _currentUserId;
|
||||||
int? get currentUserId => _currentUserId;
|
int? get currentUserId => _currentUserId;
|
||||||
|
|
||||||
|
// Флаги для определения пути пользователя
|
||||||
|
bool _needsSetup = false;
|
||||||
|
bool get needsSetup => _needsSetup;
|
||||||
|
|
||||||
|
bool _needsKeyRecovery = false;
|
||||||
|
bool get needsKeyRecovery => _needsKeyRecovery;
|
||||||
|
|
||||||
|
bool _hasPublicKeyOnServer = false;
|
||||||
|
bool get hasPublicKeyOnServer => _hasPublicKeyOnServer;
|
||||||
|
|
||||||
final _storage = const FlutterSecureStorage();
|
final _storage = const FlutterSecureStorage();
|
||||||
final _client = http.Client();
|
final _client = http.Client();
|
||||||
final ApiService _apiService = ApiService();
|
final ApiService _apiService = ApiService();
|
||||||
final SocketService _socketService = SocketService();
|
final SocketService _socketService = SocketService();
|
||||||
|
final CryptoService _cryptoService = CryptoService();
|
||||||
|
|
||||||
Future<void> initRealtime() async {
|
Future<void> initRealtime() async {
|
||||||
final token = await _apiService.getAccessToken();
|
await _socketService.connect(_apiService);
|
||||||
if (token != null) {
|
|
||||||
_socketService.connect(token);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void closeRealtime() {
|
void closeRealtime() {
|
||||||
_socketService.disconnect();
|
_socketService.disconnect();
|
||||||
}
|
}
|
||||||
|
|
||||||
SocketService get socketService => _socketService;
|
SocketService get socketService => _socketService;
|
||||||
|
|
||||||
Future<bool> login(String username, String password) async {
|
Future<bool> login(String username, String password) async {
|
||||||
|
|
@ -58,6 +68,9 @@ class AuthProvider extends ChangeNotifier {
|
||||||
);
|
);
|
||||||
_currentUserId = decodedResponse['user_id'];
|
_currentUserId = decodedResponse['user_id'];
|
||||||
|
|
||||||
|
// Проверяем статус аккаунта (нужна ли настройка или восстановление)
|
||||||
|
await _checkAccountStatus();
|
||||||
|
|
||||||
_isLoading = false;
|
_isLoading = false;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
return true;
|
return true;
|
||||||
|
|
@ -91,6 +104,12 @@ class AuthProvider extends ChangeNotifier {
|
||||||
final token = await _apiService.getAccessToken();
|
final token = await _apiService.getAccessToken();
|
||||||
if (token == null) return false;
|
if (token == null) return false;
|
||||||
|
|
||||||
|
// Загружаем currentUserId из хранилища
|
||||||
|
final userIdStr = await _storage.read(key: 'user_id');
|
||||||
|
if (userIdStr != null) {
|
||||||
|
_currentUserId = int.tryParse(userIdStr);
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final response = await _client
|
final response = await _client
|
||||||
.get(
|
.get(
|
||||||
|
|
@ -100,20 +119,112 @@ class AuthProvider extends ChangeNotifier {
|
||||||
.timeout(const Duration(seconds: 5));
|
.timeout(const Duration(seconds: 5));
|
||||||
|
|
||||||
if (response.statusCode == 200) {
|
if (response.statusCode == 200) {
|
||||||
|
// Проверяем статус аккаунта для определения дальнейшего пути
|
||||||
|
await _checkAccountStatus();
|
||||||
return true;
|
return true;
|
||||||
} else if (response.statusCode == 401) {
|
} else if (response.statusCode == 401) {
|
||||||
bool isUpdated = await _apiService.refreshToken();
|
bool isUpdated = await _apiService.refreshToken();
|
||||||
|
if (isUpdated) {
|
||||||
|
// После обновления токена проверяем статус
|
||||||
|
await _checkAccountStatus();
|
||||||
|
}
|
||||||
return isUpdated;
|
return isUpdated;
|
||||||
} else {
|
} else {
|
||||||
// Если токен протух (401), чистим память
|
|
||||||
//await logout();
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Если сервер недоступен (ошибка сети),
|
// Если сервер недоступен, позволяем offline mode
|
||||||
// в мессенджерах обычно всё равно пускают в приложение (offline mode),
|
|
||||||
// но для простоты сейчас вернем true, если токен физически есть.
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<bool> updateProfileAndSecurity({
|
||||||
|
required String firstName,
|
||||||
|
String? lastName,
|
||||||
|
required String masterPassword,
|
||||||
|
}) async {
|
||||||
|
notifyListeners();
|
||||||
|
|
||||||
|
try {
|
||||||
|
final token = await _apiService.getAccessToken();
|
||||||
|
|
||||||
|
// Генерируем ключи и шифруем приватный
|
||||||
|
final keys = await _cryptoService.initAccountSecurity(masterPassword);
|
||||||
|
|
||||||
|
final response = await _client.post(
|
||||||
|
Uri.http(AppConstants.baseUrl, 'auth/setup-account'),
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': 'Bearer $token',
|
||||||
|
},
|
||||||
|
body: jsonEncode({
|
||||||
|
'first_name': firstName,
|
||||||
|
'last_name': lastName,
|
||||||
|
'public_key': keys['public_key'],
|
||||||
|
'encrypted_private_key': keys['encrypted_private_key'],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.statusCode == 200) {
|
||||||
|
_needsSetup = false;
|
||||||
|
notifyListeners();
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
print("Ошибка настройки профиля: ${response.body}");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
print("Ошибка сети: $e");
|
||||||
|
return false;
|
||||||
|
} finally {
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Приватный метод для проверки статуса аккаунта
|
||||||
|
Future<void> _checkAccountStatus() async {
|
||||||
|
try {
|
||||||
|
final token = await _apiService.getAccessToken();
|
||||||
|
final response = await _client.get(
|
||||||
|
Uri.http(AppConstants.baseUrl, 'users/me'),
|
||||||
|
headers: {'Authorization': 'Bearer $token'},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.statusCode == 200) {
|
||||||
|
final data = jsonDecode(utf8.decode(response.bodyBytes)) as Map;
|
||||||
|
|
||||||
|
// Проверяем наличие публичного ключа на сервере
|
||||||
|
_hasPublicKeyOnServer = data['public_key'] != null && data['public_key'].isNotEmpty;
|
||||||
|
|
||||||
|
// Проверяем наличие приватного ключа локально
|
||||||
|
final hasLocalPrivateKey = await _storage.read(key: 'private_key') != null;
|
||||||
|
|
||||||
|
if (!_hasPublicKeyOnServer) {
|
||||||
|
// Путь А: Первая настройка - нужно создать ключи и профиль
|
||||||
|
_needsSetup = true;
|
||||||
|
_needsKeyRecovery = false;
|
||||||
|
} else if (!hasLocalPrivateKey) {
|
||||||
|
// Путь В: Переустановка - ключ на сервере, но его нет локально
|
||||||
|
_needsKeyRecovery = true;
|
||||||
|
_needsSetup = false;
|
||||||
|
} else {
|
||||||
|
// Путь Б: Нормальный вход - все в порядке
|
||||||
|
_needsSetup = false;
|
||||||
|
_needsKeyRecovery = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
print("Ошибка проверки статуса: $e");
|
||||||
|
_needsSetup = false;
|
||||||
|
_needsKeyRecovery = false;
|
||||||
|
}
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Метод для начала с чистого листа (новые ключи)
|
||||||
|
Future<void> resetKeys() async {
|
||||||
|
await _storage.delete(key: 'private_key');
|
||||||
|
_needsKeyRecovery = false;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,11 +5,13 @@ import '/data/repositories/contact_repository.dart';
|
||||||
class ContactProvider extends ChangeNotifier {
|
class ContactProvider extends ChangeNotifier {
|
||||||
final ContactRepository _repository = ContactRepository();
|
final ContactRepository _repository = ContactRepository();
|
||||||
List<Contact> _contacts = [];
|
List<Contact> _contacts = [];
|
||||||
|
List<Contact> _allContacts = [];
|
||||||
bool _isLoading = false;
|
bool _isLoading = false;
|
||||||
String? _error;
|
String? _error;
|
||||||
int? _currentUserId;
|
int? _currentUserId;
|
||||||
|
|
||||||
List<Contact> get contacts => _contacts;
|
List<Contact> get contacts => _contacts;
|
||||||
|
List<Contact> get allContacts => _allContacts;
|
||||||
bool get isLoading => _isLoading;
|
bool get isLoading => _isLoading;
|
||||||
String? get error => _error;
|
String? get error => _error;
|
||||||
|
|
||||||
|
|
@ -25,7 +27,27 @@ class ContactProvider extends ChangeNotifier {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final allContacts = await _repository.fetchContacts();
|
final allContacts = await _repository.fetchContacts();
|
||||||
|
// Фильтруем: исключаем себя (для основного списка - только чаты)
|
||||||
_contacts = allContacts.where((contact) => contact.id != _currentUserId).toList();
|
_contacts = allContacts.where((contact) => contact.id != _currentUserId).toList();
|
||||||
|
_allContacts = _contacts;
|
||||||
|
} catch (e) {
|
||||||
|
_error = e.toString();
|
||||||
|
} finally {
|
||||||
|
_isLoading = false;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Метод для получения всех контактов (исключая себя) для нового чата
|
||||||
|
Future<void> loadAllContactsForNewChat() async {
|
||||||
|
_isLoading = true;
|
||||||
|
_error = null;
|
||||||
|
notifyListeners();
|
||||||
|
|
||||||
|
try {
|
||||||
|
final allContacts = await _repository.fetchContacts();
|
||||||
|
// Фильтруем только исключение самого себя
|
||||||
|
_allContacts = allContacts.where((contact) => contact.id != _currentUserId).toList();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
_error = e.toString();
|
_error = e.toString();
|
||||||
} finally {
|
} finally {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,234 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
import '../../logic/auth_provider.dart';
|
||||||
|
import 'contacts_screen.dart';
|
||||||
|
|
||||||
|
class AccountSetupScreen extends StatefulWidget {
|
||||||
|
const AccountSetupScreen({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<AccountSetupScreen> createState() => _AccountSetupScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AccountSetupScreenState extends State<AccountSetupScreen> {
|
||||||
|
final _formKey = GlobalKey<FormState>();
|
||||||
|
final _firstNameController = TextEditingController();
|
||||||
|
final _lastNameController = TextEditingController();
|
||||||
|
final _masterPasswordController = TextEditingController();
|
||||||
|
final _confirmPasswordController = TextEditingController();
|
||||||
|
bool _isLoading = false;
|
||||||
|
String? _errorMessage;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_firstNameController.dispose();
|
||||||
|
_lastNameController.dispose();
|
||||||
|
_masterPasswordController.dispose();
|
||||||
|
_confirmPasswordController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _setupAccount() async {
|
||||||
|
if (!_formKey.currentState!.validate()) return;
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_isLoading = true;
|
||||||
|
_errorMessage = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
final authProvider = context.read<AuthProvider>();
|
||||||
|
|
||||||
|
// Отправляем данные на сервер с мастер-паролем
|
||||||
|
final success = await authProvider.updateProfileAndSecurity(
|
||||||
|
firstName: _firstNameController.text.trim(),
|
||||||
|
lastName: _lastNameController.text.trim(),
|
||||||
|
masterPassword: _masterPasswordController.text,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (success && mounted) {
|
||||||
|
// Переходим на экран контактов
|
||||||
|
Navigator.pushReplacement(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(builder: (_) => const ContactsScreen()),
|
||||||
|
);
|
||||||
|
} else if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_errorMessage = 'Ошибка при сохранении профиля. Попробуйте еще раз.';
|
||||||
|
_isLoading = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_errorMessage = 'Ошибка: ${e.toString()}';
|
||||||
|
_isLoading = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: const Text('Завершение настройки'),
|
||||||
|
centerTitle: true,
|
||||||
|
elevation: 0,
|
||||||
|
),
|
||||||
|
body: SingleChildScrollView(
|
||||||
|
padding: const EdgeInsets.all(24.0),
|
||||||
|
child: Form(
|
||||||
|
key: _formKey,
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Text(
|
||||||
|
'Завершите настройку вашего профиля',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: Theme.of(context).textTheme.headlineSmall,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text(
|
||||||
|
'Введите ваше имя, фамилию и создайте мастер-пароль. Мастер-пароль будет использоваться для защиты ваших ключей шифрования.',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||||
|
color: Theme.of(context).textTheme.bodySmall?.color,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 32),
|
||||||
|
|
||||||
|
// Поле Имя
|
||||||
|
TextFormField(
|
||||||
|
controller: _firstNameController,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: 'Имя *',
|
||||||
|
prefixIcon: const Icon(Icons.person_outline),
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
hintText: 'Введите ваше имя',
|
||||||
|
),
|
||||||
|
validator: (value) {
|
||||||
|
if (value == null || value.trim().isEmpty) {
|
||||||
|
return 'Имя не может быть пустым';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
|
// Поле Фамилия
|
||||||
|
TextFormField(
|
||||||
|
controller: _lastNameController,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: 'Фамилия',
|
||||||
|
prefixIcon: const Icon(Icons.person_outline),
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
hintText: 'Введите вашу фамилию (опционально)',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
|
// Поле Мастер-пароль
|
||||||
|
TextFormField(
|
||||||
|
controller: _masterPasswordController,
|
||||||
|
obscureText: true,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: 'Мастер-пароль *',
|
||||||
|
prefixIcon: const Icon(Icons.lock_outline),
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
hintText: 'Создайте надежный пароль',
|
||||||
|
),
|
||||||
|
validator: (value) {
|
||||||
|
if (value == null || value.isEmpty) {
|
||||||
|
return 'Мастер-пароль не может быть пустым';
|
||||||
|
}
|
||||||
|
if (value.length < 8) {
|
||||||
|
return 'Пароль должен содержать минимум 8 символов';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
|
// Поле Подтверждение пароля
|
||||||
|
TextFormField(
|
||||||
|
controller: _confirmPasswordController,
|
||||||
|
obscureText: true,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: 'Подтвердите пароль *',
|
||||||
|
prefixIcon: const Icon(Icons.lock_outline),
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
hintText: 'Повторите пароль',
|
||||||
|
),
|
||||||
|
validator: (value) {
|
||||||
|
if (value == null || value.isEmpty) {
|
||||||
|
return 'Подтвердите пароль';
|
||||||
|
}
|
||||||
|
if (value != _masterPasswordController.text) {
|
||||||
|
return 'Пароли не совпадают';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
|
||||||
|
// Сообщение об ошибке
|
||||||
|
if (_errorMessage != null)
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.all(12),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Theme.of(context).colorScheme.error.withOpacity(0.1),
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
_errorMessage!,
|
||||||
|
style: TextStyle(
|
||||||
|
color: Theme.of(context).colorScheme.error,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
|
||||||
|
// Кнопка подтверждения
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: _isLoading ? null : _setupAccount,
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: _isLoading
|
||||||
|
? const SizedBox(
|
||||||
|
height: 20,
|
||||||
|
width: 20,
|
||||||
|
child: CircularProgressIndicator(strokeWidth: 2),
|
||||||
|
)
|
||||||
|
: const Text(
|
||||||
|
'Завершить настройку',
|
||||||
|
style: TextStyle(fontSize: 16),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
Text(
|
||||||
|
'Сохраните мастер-пароль в надежном месте. Он потребуется для восстановления ключей шифрования при переустановке приложения.',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: Theme.of(context).textTheme.labelSmall,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import '../widgets/contact_tile.dart';
|
import '../widgets/contact_tile.dart';
|
||||||
import '/data/models/contact_model.dart';
|
|
||||||
import '../screens/settings_screen.dart';
|
import '../screens/settings_screen.dart';
|
||||||
import '../screens/new_chat_screen.dart';
|
import '../screens/new_chat_screen.dart';
|
||||||
import '../screens/chat_screen.dart';
|
import '../screens/chat_screen.dart';
|
||||||
|
|
@ -22,6 +21,8 @@ class _ContactsScreenState extends State<ContactsScreen> {
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
final authProvider = context.read<AuthProvider>();
|
final authProvider = context.read<AuthProvider>();
|
||||||
final contactProvider = context.read<ContactProvider>();
|
final contactProvider = context.read<ContactProvider>();
|
||||||
|
|
||||||
|
// Установить текущего пользователя и загрузить контакты с сообщениями
|
||||||
contactProvider.setCurrentUserId(authProvider.currentUserId);
|
contactProvider.setCurrentUserId(authProvider.currentUserId);
|
||||||
contactProvider.loadContacts();
|
contactProvider.loadContacts();
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,296 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
import '../../logic/auth_provider.dart';
|
||||||
|
import '../../domain/services/crypto_service.dart';
|
||||||
|
import '../../domain/services/api_service.dart';
|
||||||
|
import 'account_setup_screen.dart';
|
||||||
|
import 'dart:convert';
|
||||||
|
import 'package:http/http.dart' as http;
|
||||||
|
import '../../core/constants.dart';
|
||||||
|
|
||||||
|
class KeyRecoveryScreen extends StatefulWidget {
|
||||||
|
const KeyRecoveryScreen({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<KeyRecoveryScreen> createState() => _KeyRecoveryScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _KeyRecoveryScreenState extends State<KeyRecoveryScreen> {
|
||||||
|
bool _isLoading = false;
|
||||||
|
String? _errorMessage;
|
||||||
|
final _passwordController = TextEditingController();
|
||||||
|
final _formKey = GlobalKey<FormState>();
|
||||||
|
|
||||||
|
Future<void> _startFresh() async {
|
||||||
|
setState(() {
|
||||||
|
_isLoading = true;
|
||||||
|
_errorMessage = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
final authProvider = context.read<AuthProvider>();
|
||||||
|
|
||||||
|
// Удаляем старые ключи и создаем новые
|
||||||
|
await authProvider.resetKeys();
|
||||||
|
|
||||||
|
// Переходим на экран настройки для создания новых ключей
|
||||||
|
if (mounted) {
|
||||||
|
Navigator.pushReplacement(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(builder: (_) => const AccountSetupScreen()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_errorMessage = 'Ошибка: ${e.toString()}';
|
||||||
|
_isLoading = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _recoverKeys() async {
|
||||||
|
if (!_formKey.currentState!.validate()) return;
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_isLoading = true;
|
||||||
|
_errorMessage = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
final authProvider = context.read<AuthProvider>();
|
||||||
|
final apiService = ApiService();
|
||||||
|
final cryptoService = CryptoService();
|
||||||
|
|
||||||
|
// Получаем токен
|
||||||
|
final token = await apiService.getAccessToken();
|
||||||
|
if (token == null) throw Exception('Не авторизован');
|
||||||
|
|
||||||
|
// Скачиваем зашифрованный приватный ключ с сервера
|
||||||
|
final response = await http.get(
|
||||||
|
Uri.http(AppConstants.baseUrl, 'users/me'),
|
||||||
|
headers: {'Authorization': 'Bearer $token'},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.statusCode != 200) {
|
||||||
|
throw Exception('Не удалось получить данные пользователя');
|
||||||
|
}
|
||||||
|
|
||||||
|
final data = jsonDecode(utf8.decode(response.bodyBytes)) as Map;
|
||||||
|
final encryptedPrivateKey = data['encrypted_private_key'];
|
||||||
|
|
||||||
|
if (encryptedPrivateKey == null || encryptedPrivateKey.isEmpty) {
|
||||||
|
throw Exception('Зашифрованный ключ не найден на сервере');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Расшифровываем приватный ключ
|
||||||
|
final decryptedPrivateKey = await cryptoService.decryptPrivateKey(
|
||||||
|
encryptedPrivateKey,
|
||||||
|
_passwordController.text,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Сохраняем расшифрованный ключ локально
|
||||||
|
await cryptoService.savePrivateKey(decryptedPrivateKey);
|
||||||
|
|
||||||
|
// Обновляем статус в AuthProvider
|
||||||
|
await authProvider.tryAutoLogin();
|
||||||
|
|
||||||
|
if (mounted) {
|
||||||
|
// Возвращаемся на главный экран
|
||||||
|
Navigator.of(context).popUntil((route) => route.isFirst);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_errorMessage = 'Ошибка восстановления: ${e.toString().replaceAll('Exception: ', '')}';
|
||||||
|
_isLoading = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: const Text('Восстановление ключей'),
|
||||||
|
centerTitle: true,
|
||||||
|
elevation: 0,
|
||||||
|
),
|
||||||
|
body: SingleChildScrollView(
|
||||||
|
padding: const EdgeInsets.all(24.0),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
const SizedBox(height: 32),
|
||||||
|
Icon(
|
||||||
|
Icons.security_outlined,
|
||||||
|
size: 80,
|
||||||
|
color: Theme.of(context).colorScheme.primary,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
Text(
|
||||||
|
'Восстановление ключей шифрования',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: Theme.of(context).textTheme.headlineSmall,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Text(
|
||||||
|
'Вы переустановили приложение или используете новый девайс. У вас есть два варианта:',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: Theme.of(context).textTheme.bodyMedium,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 32),
|
||||||
|
|
||||||
|
// Вариант 1: Начать с чистого листа
|
||||||
|
Card(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(16.0),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Icons.restart_alt_outlined,
|
||||||
|
color: Theme.of(context).colorScheme.primary,
|
||||||
|
size: 28,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
'Начать с чистого листа',
|
||||||
|
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
Text(
|
||||||
|
'Создаются новые ключи шифрования. Старые сообщения не будут расшифрованы.',
|
||||||
|
style: Theme.of(context).textTheme.bodySmall,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
child: ElevatedButton(
|
||||||
|
onPressed: _isLoading ? null : _startFresh,
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||||
|
),
|
||||||
|
child: _isLoading
|
||||||
|
? const SizedBox(
|
||||||
|
height: 20,
|
||||||
|
width: 20,
|
||||||
|
child: CircularProgressIndicator(strokeWidth: 2),
|
||||||
|
)
|
||||||
|
: const Text('Продолжить'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
|
||||||
|
// Вариант 2: Восстановить из облака
|
||||||
|
Card(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(16.0),
|
||||||
|
child: Form(
|
||||||
|
key: _formKey,
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Icons.cloud_download_outlined,
|
||||||
|
color: Theme.of(context).colorScheme.primary,
|
||||||
|
size: 28,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
'Восстановить из облака',
|
||||||
|
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
Text(
|
||||||
|
'Введите мастер-пароль для восстановления ключей шифрования',
|
||||||
|
style: Theme.of(context).textTheme.bodySmall,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
TextFormField(
|
||||||
|
controller: _passwordController,
|
||||||
|
obscureText: true,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'Мастер-пароль',
|
||||||
|
border: OutlineInputBorder(),
|
||||||
|
),
|
||||||
|
validator: (value) {
|
||||||
|
if (value == null || value.isEmpty) {
|
||||||
|
return 'Введите мастер-пароль';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
child: ElevatedButton(
|
||||||
|
onPressed: _isLoading ? null : _recoverKeys,
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||||
|
),
|
||||||
|
child: _isLoading
|
||||||
|
? const SizedBox(
|
||||||
|
height: 20,
|
||||||
|
width: 20,
|
||||||
|
child: CircularProgressIndicator(strokeWidth: 2),
|
||||||
|
)
|
||||||
|
: const Text('Восстановить'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
|
||||||
|
// Сообщение об ошибке
|
||||||
|
if (_errorMessage != null)
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.all(12),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Theme.of(context).colorScheme.error.withOpacity(0.1),
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
_errorMessage!,
|
||||||
|
style: TextStyle(
|
||||||
|
color: Theme.of(context).colorScheme.error,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_passwordController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,6 @@
|
||||||
import 'package:chepuhagram/presentation/screens/contacts_screen.dart';
|
import 'package:chepuhagram/presentation/screens/contacts_screen.dart';
|
||||||
|
import 'package:chepuhagram/presentation/screens/account_setup_screen.dart';
|
||||||
|
import 'package:chepuhagram/presentation/screens/key_recovery_screen.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import '../../logic/auth_provider.dart';
|
import '../../logic/auth_provider.dart';
|
||||||
|
|
@ -121,10 +123,27 @@ class _LoginScreenState extends State<LoginScreen> {
|
||||||
);
|
);
|
||||||
if (success && mounted) {
|
if (success && mounted) {
|
||||||
await authProvider.initRealtime();
|
await authProvider.initRealtime();
|
||||||
Navigator.pushReplacement(
|
|
||||||
context,
|
// Определяем путь пользователя после входа
|
||||||
MaterialPageRoute(builder: (_) => const ContactsScreen()),
|
if (authProvider.needsSetup) {
|
||||||
);
|
// Путь А: Первичная настройка
|
||||||
|
Navigator.pushReplacement(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(builder: (_) => const AccountSetupScreen()),
|
||||||
|
);
|
||||||
|
} else if (authProvider.needsKeyRecovery) {
|
||||||
|
// Путь В: Восстановление ключей
|
||||||
|
Navigator.pushReplacement(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(builder: (_) => const KeyRecoveryScreen()),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// Путь Б: Нормальный вход
|
||||||
|
Navigator.pushReplacement(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(builder: (_) => const ContactsScreen()),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
|
|
||||||
|
|
@ -5,17 +5,30 @@ import '/logic/contact_provider.dart';
|
||||||
import '/logic/auth_provider.dart';
|
import '/logic/auth_provider.dart';
|
||||||
import 'chat_screen.dart';
|
import 'chat_screen.dart';
|
||||||
|
|
||||||
class NewChatScreen extends StatelessWidget {
|
class NewChatScreen extends StatefulWidget {
|
||||||
const NewChatScreen({super.key});
|
const NewChatScreen({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
State<NewChatScreen> createState() => _NewChatScreenState();
|
||||||
final authProvider = context.watch<AuthProvider>();
|
}
|
||||||
final contactProvider = context.watch<ContactProvider>();
|
|
||||||
|
|
||||||
final filteredContacts = contactProvider.contacts
|
class _NewChatScreenState extends State<NewChatScreen> {
|
||||||
.where((contact) => contact.id != authProvider.currentUserId)
|
@override
|
||||||
.toList();
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
final authProvider = context.read<AuthProvider>();
|
||||||
|
final contactProvider = context.read<ContactProvider>();
|
||||||
|
|
||||||
|
// Установить текущего пользователя и загрузить все контакты
|
||||||
|
contactProvider.setCurrentUserId(authProvider.currentUserId);
|
||||||
|
contactProvider.loadAllContactsForNewChat();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final contactProvider = context.watch<ContactProvider>();
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
|
|
@ -26,9 +39,9 @@ class NewChatScreen extends StatelessWidget {
|
||||||
: contactProvider.error != null
|
: contactProvider.error != null
|
||||||
? Center(child: Text('Error: ${contactProvider.error}'))
|
? Center(child: Text('Error: ${contactProvider.error}'))
|
||||||
: ListView.builder(
|
: ListView.builder(
|
||||||
itemCount: filteredContacts.length,
|
itemCount: contactProvider.allContacts.length,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
final contact = filteredContacts[index];
|
final contact = contactProvider.allContacts[index];
|
||||||
return ListTile(
|
return ListTile(
|
||||||
leading: CircleAvatar(
|
leading: CircleAvatar(
|
||||||
child: Text(contact.name[0]),
|
child: Text(contact.name[0]),
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,8 @@ import 'package:provider/provider.dart';
|
||||||
import '../../logic/auth_provider.dart';
|
import '../../logic/auth_provider.dart';
|
||||||
import 'login_screen.dart';
|
import 'login_screen.dart';
|
||||||
import 'contacts_screen.dart';
|
import 'contacts_screen.dart';
|
||||||
|
import 'account_setup_screen.dart';
|
||||||
|
import 'key_recovery_screen.dart';
|
||||||
|
|
||||||
class SplashScreen extends StatefulWidget {
|
class SplashScreen extends StatefulWidget {
|
||||||
const SplashScreen({super.key});
|
const SplashScreen({super.key});
|
||||||
|
|
@ -19,7 +21,7 @@ class _SplashScreenState extends State<SplashScreen> {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _initializeApp() async {
|
Future<void> _initializeApp() async {
|
||||||
// 1. Искусственная задержка в 2 секунды
|
// 1. Искусственная задержка в 2 секунды для демонстрации splash
|
||||||
await Future.delayed(const Duration(seconds: 2));
|
await Future.delayed(const Duration(seconds: 2));
|
||||||
|
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
|
|
@ -30,14 +32,32 @@ class _SplashScreenState extends State<SplashScreen> {
|
||||||
|
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
|
|
||||||
// 3. Навигация в зависимости от результата
|
// 3. Навигация в зависимости от результата и статуса аккаунта
|
||||||
if (isLoggedIn) {
|
if (isLoggedIn) {
|
||||||
await authProvider.initRealtime(); // Запускаем сокет сразу
|
await authProvider.initRealtime(); // Запускаем WebSocket сразу
|
||||||
Navigator.pushReplacement(
|
|
||||||
context,
|
// Определяем путь пользователя
|
||||||
MaterialPageRoute(builder: (_) => const ContactsScreen()),
|
if (authProvider.needsSetup) {
|
||||||
);
|
// Путь А: Первичная настройка
|
||||||
|
Navigator.pushReplacement(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(builder: (_) => const AccountSetupScreen()),
|
||||||
|
);
|
||||||
|
} else if (authProvider.needsKeyRecovery) {
|
||||||
|
// Путь В: Восстановление ключей
|
||||||
|
Navigator.pushReplacement(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(builder: (_) => const KeyRecoveryScreen()),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// Путь Б: Нормальный вход в контакты
|
||||||
|
Navigator.pushReplacement(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(builder: (_) => const ContactsScreen()),
|
||||||
|
);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
|
// Нет токена - переходим на экран входа
|
||||||
Navigator.pushReplacement(
|
Navigator.pushReplacement(
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(builder: (_) => const LoginScreen()),
|
MaterialPageRoute(builder: (_) => const LoginScreen()),
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import '../../core/app_colors.dart';
|
import '../../core/app_colors.dart';
|
||||||
import '../screens/chat_screen.dart';
|
|
||||||
import '/data/models/contact_model.dart';
|
import '/data/models/contact_model.dart';
|
||||||
|
|
||||||
class ContactTile extends StatelessWidget {
|
class ContactTile extends StatelessWidget {
|
||||||
|
|
|
||||||
|
|
@ -57,6 +57,14 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.7"
|
version: "3.0.7"
|
||||||
|
cryptography:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: cryptography
|
||||||
|
sha256: "3eda3029d34ec9095a27a198ac9785630fe525c0eb6a49f3d575272f8e792ef0"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.9.0"
|
||||||
cupertino_icons:
|
cupertino_icons:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,7 @@ dependencies:
|
||||||
flutter_secure_storage: ^10.0.0
|
flutter_secure_storage: ^10.0.0
|
||||||
jwt_decoder: ^2.0.1
|
jwt_decoder: ^2.0.1
|
||||||
web_socket_channel: ^3.0.3
|
web_socket_channel: ^3.0.3
|
||||||
|
cryptography: ^2.5.0
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
from app.db.models import Base
|
||||||
from logging.config import fileConfig
|
from logging.config import fileConfig
|
||||||
|
|
||||||
from sqlalchemy import engine_from_config
|
from sqlalchemy import engine_from_config
|
||||||
|
|
@ -16,7 +17,6 @@ if config.config_file_name is not None:
|
||||||
|
|
||||||
# add your model's MetaData object here
|
# add your model's MetaData object here
|
||||||
# for 'autogenerate' support
|
# for 'autogenerate' support
|
||||||
from app.db.models import Base
|
|
||||||
target_metadata = Base.metadata
|
target_metadata = Base.metadata
|
||||||
|
|
||||||
# other values from the config, defined by the needs of env.py,
|
# other values from the config, defined by the needs of env.py,
|
||||||
|
|
@ -64,7 +64,8 @@ def run_migrations_online() -> None:
|
||||||
|
|
||||||
with connectable.connect() as connection:
|
with connectable.connect() as connection:
|
||||||
context.configure(
|
context.configure(
|
||||||
connection=connection, target_metadata=target_metadata
|
connection=connection, target_metadata=target_metadata,
|
||||||
|
render_as_batch=True
|
||||||
)
|
)
|
||||||
|
|
||||||
with context.begin_transaction():
|
with context.begin_transaction():
|
||||||
|
|
|
||||||
|
|
@ -1,42 +0,0 @@
|
||||||
"""initial_with_new_columns
|
|
||||||
|
|
||||||
Revision ID: 4228f07bd5ad
|
|
||||||
Revises:
|
|
||||||
Create Date: 2026-04-19 01:58:07.533812
|
|
||||||
|
|
||||||
"""
|
|
||||||
from typing import Sequence, Union
|
|
||||||
|
|
||||||
from alembic import op
|
|
||||||
import sqlalchemy as sa
|
|
||||||
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
|
||||||
revision: str = '4228f07bd5ad'
|
|
||||||
down_revision: Union[str, Sequence[str], None] = None
|
|
||||||
branch_labels: Union[str, Sequence[str], None] = None
|
|
||||||
depends_on: Union[str, Sequence[str], None] = None
|
|
||||||
|
|
||||||
|
|
||||||
def upgrade() -> None:
|
|
||||||
"""Upgrade schema."""
|
|
||||||
# ### commands auto generated by Alembic - please adjust! ###
|
|
||||||
op.add_column('users', sa.Column('first_name', sa.String(length=50), server_default='User', nullable=False))
|
|
||||||
op.add_column('users', sa.Column('last_name', sa.String(length=50), nullable=True))
|
|
||||||
op.add_column('users', sa.Column('about', sa.String(), nullable=True))
|
|
||||||
op.add_column('users', sa.Column('phone', sa.String(length=20), nullable=False))
|
|
||||||
op.add_column('users', sa.Column('totp_secret', sa.String(length=32), nullable=True))
|
|
||||||
op.create_unique_constraint(None, 'users', ['phone'])
|
|
||||||
# ### end Alembic commands ###
|
|
||||||
|
|
||||||
|
|
||||||
def downgrade() -> None:
|
|
||||||
"""Downgrade schema."""
|
|
||||||
# ### commands auto generated by Alembic - please adjust! ###
|
|
||||||
op.drop_constraint(None, 'users', type_='unique')
|
|
||||||
op.drop_column('users', 'totp_secret')
|
|
||||||
op.drop_column('users', 'phone')
|
|
||||||
op.drop_column('users', 'about')
|
|
||||||
op.drop_column('users', 'last_name')
|
|
||||||
op.drop_column('users', 'first_name')
|
|
||||||
# ### end Alembic commands ###
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
"""initial_with_new_columns
|
"""add encrypted private key
|
||||||
|
|
||||||
Revision ID: 8eed4a873add
|
Revision ID: 4e1aa78f81c6
|
||||||
Revises:
|
Revises:
|
||||||
Create Date: 2026-04-19 01:59:16.030461
|
Create Date: 2026-04-24 18:17:13.010993
|
||||||
|
|
||||||
"""
|
"""
|
||||||
from typing import Sequence, Union
|
from typing import Sequence, Union
|
||||||
|
|
@ -12,7 +12,7 @@ import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
# revision identifiers, used by Alembic.
|
||||||
revision: str = '8eed4a873add'
|
revision: str = '4e1aa78f81c6'
|
||||||
down_revision: Union[str, Sequence[str], None] = None
|
down_revision: Union[str, Sequence[str], None] = None
|
||||||
branch_labels: Union[str, Sequence[str], None] = None
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
depends_on: Union[str, Sequence[str], None] = None
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
|
|
@ -21,16 +21,16 @@ depends_on: Union[str, Sequence[str], None] = None
|
||||||
def upgrade() -> None:
|
def upgrade() -> None:
|
||||||
"""Upgrade schema."""
|
"""Upgrade schema."""
|
||||||
# ### commands auto generated by Alembic - please adjust! ###
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
op.add_column('users', sa.Column('phone', sa.String(length=20), nullable=True))
|
with op.batch_alter_table('users', schema=None) as batch_op:
|
||||||
op.add_column('users', sa.Column('totp_secret', sa.String(length=32), nullable=True))
|
batch_op.create_unique_constraint('uq_user_phone', ['phone'])
|
||||||
op.create_unique_constraint(None, 'users', ['phone'])
|
|
||||||
# ### end Alembic commands ###
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
def downgrade() -> None:
|
def downgrade() -> None:
|
||||||
"""Downgrade schema."""
|
"""Downgrade schema."""
|
||||||
# ### commands auto generated by Alembic - please adjust! ###
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
op.drop_constraint(None, 'users', type_='unique')
|
with op.batch_alter_table('users', schema=None) as batch_op:
|
||||||
op.drop_column('users', 'totp_secret')
|
batch_op.drop_constraint(None, type_='unique')
|
||||||
op.drop_column('users', 'phone')
|
|
||||||
# ### end Alembic commands ###
|
# ### end Alembic commands ###
|
||||||
|
|
@ -2,10 +2,14 @@ from fastapi import FastAPI, Depends, HTTPException, status, APIRouter
|
||||||
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
|
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
from app.core import security
|
from app.core import security
|
||||||
|
from app.api import schemas
|
||||||
from app.db import models
|
from app.db import models
|
||||||
from jose import JWTError, jwt
|
from jose import JWTError, jwt
|
||||||
|
from app.core.security import get_current_user
|
||||||
|
|
||||||
# бд
|
# бд
|
||||||
|
|
||||||
|
|
||||||
def get_db():
|
def get_db():
|
||||||
db = models.SessionLocal()
|
db = models.SessionLocal()
|
||||||
try:
|
try:
|
||||||
|
|
@ -20,14 +24,19 @@ authRouter = APIRouter(
|
||||||
)
|
)
|
||||||
|
|
||||||
# регистрация
|
# регистрация
|
||||||
|
|
||||||
|
|
||||||
@authRouter.post("/register")
|
@authRouter.post("/register")
|
||||||
async def register(username: str, password: str, db: Session = Depends(get_db)):
|
async def register(username: str, password: str, db: Session = Depends(get_db)):
|
||||||
if len(password.encode('utf-8')) > 72:
|
if len(password.encode('utf-8')) > 72:
|
||||||
raise HTTPException(status_code=400, detail="Пароль слишком длинный (макс. 72 байта)")
|
raise HTTPException(
|
||||||
|
status_code=400, detail="Пароль слишком длинный (макс. 72 байта)")
|
||||||
|
|
||||||
db_user = db.query(models.User).filter(models.User.username == username).first()
|
db_user = db.query(models.User).filter(
|
||||||
|
models.User.username == username).first()
|
||||||
if db_user:
|
if db_user:
|
||||||
raise HTTPException(status_code=400, detail="Пользователь уже существует")
|
raise HTTPException(
|
||||||
|
status_code=400, detail="Пользователь уже существует")
|
||||||
|
|
||||||
hashed_pwd = security.get_password_hash(password)
|
hashed_pwd = security.get_password_hash(password)
|
||||||
new_user = models.User(username=username, hashed_password=hashed_pwd)
|
new_user = models.User(username=username, hashed_password=hashed_pwd)
|
||||||
|
|
@ -35,10 +44,23 @@ async def register(username: str, password: str, db: Session = Depends(get_db)):
|
||||||
db.commit()
|
db.commit()
|
||||||
return {"status": "ok", "message": "User created"}
|
return {"status": "ok", "message": "User created"}
|
||||||
|
|
||||||
|
|
||||||
|
@authRouter.post("/hash")
|
||||||
|
async def register(password: str):
|
||||||
|
if len(password.encode('utf-8')) > 72:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=400, detail="Пароль слишком длинный (макс. 72 байта)")
|
||||||
|
|
||||||
|
hashed_pwd = security.get_password_hash(password)
|
||||||
|
return {"password": hashed_pwd}
|
||||||
|
|
||||||
# вход
|
# вход
|
||||||
|
|
||||||
|
|
||||||
@authRouter.post("/login")
|
@authRouter.post("/login")
|
||||||
async def login(form_data: OAuth2PasswordRequestForm = Depends(), db: Session = Depends(get_db)):
|
async def login(form_data: OAuth2PasswordRequestForm = Depends(), db: Session = Depends(get_db)):
|
||||||
user = db.query(models.User).filter(models.User.username == form_data.username).first()
|
user = db.query(models.User).filter(
|
||||||
|
models.User.username == form_data.username).first()
|
||||||
|
|
||||||
if not user or not security.verify_password(form_data.password, user.hashed_password):
|
if not user or not security.verify_password(form_data.password, user.hashed_password):
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
|
|
@ -56,17 +78,31 @@ async def login(form_data: OAuth2PasswordRequestForm = Depends(), db: Session =
|
||||||
"user_id": user.id
|
"user_id": user.id
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@authRouter.post("/refresh")
|
@authRouter.post("/refresh")
|
||||||
async def refresh_token(data: models.RefreshRequest, db: Session = Depends(get_db)):
|
async def refresh_token(data: schemas.RefreshRequest):
|
||||||
try:
|
try:
|
||||||
payload = jwt.decode(data.refresh_token, security.SECRET_KEY, algorithms=[security.ALGORITHM])
|
payload = jwt.decode(data.refresh_token, security.SECRET_KEY, algorithms=[
|
||||||
user_id = payload.get("sub")
|
security.ALGORITHM])
|
||||||
|
user_id = str(payload.get("sub"))
|
||||||
if user_id is None:
|
if user_id is None:
|
||||||
raise HTTPException(status_code=401)
|
raise HTTPException(status_code=401)
|
||||||
|
|
||||||
|
|
||||||
new_access_token = security.create_access_token(data={"sub": user_id})
|
new_access_token = security.create_access_token(data={"sub": user_id})
|
||||||
new_refresh_token = security.create_refresh_token(data={"sub": user_id})
|
new_refresh_token = security.create_refresh_token(
|
||||||
|
data={"sub": user_id})
|
||||||
return {"refresh_token": new_refresh_token, "access_token": new_access_token, "token_type": "bearer"}
|
return {"refresh_token": new_refresh_token, "access_token": new_access_token, "token_type": "bearer"}
|
||||||
except JWTError:
|
except JWTError:
|
||||||
raise HTTPException(status_code=401, detail="Refresh token expired")
|
raise HTTPException(status_code=401, detail="Refresh token expired")
|
||||||
|
|
||||||
|
|
||||||
|
@authRouter.post("/setup-account")
|
||||||
|
async def setup_account(data: schemas.SetupAccount, current_user: models.User = Depends(get_current_user), db: Session = Depends(get_db)):
|
||||||
|
user_to_update = db.merge(current_user)
|
||||||
|
user_to_update.first_name = data.first_name
|
||||||
|
user_to_update.last_name = data.last_name
|
||||||
|
user_to_update.public_key = data.public_key
|
||||||
|
user_to_update.encrypted_private_key = data.encrypted_private_key
|
||||||
|
db.commit()
|
||||||
|
db.refresh(user_to_update)
|
||||||
|
return {"status": "ok", "message": "Account setup completed"}
|
||||||
|
|
@ -21,9 +21,9 @@ usersRouter = APIRouter(
|
||||||
# Пример защищенного роута
|
# Пример защищенного роута
|
||||||
@usersRouter.get("/me")
|
@usersRouter.get("/me")
|
||||||
async def read_users_me(current_user: models.User = Depends(get_current_user)):
|
async def read_users_me(current_user: models.User = Depends(get_current_user)):
|
||||||
return {"id": current_user.id, "username": current_user.username}
|
return {"id": current_user.id, "username": current_user.username, "first_name": current_user.first_name, "last_name": current_user.last_name, "public_key": current_user.public_key, "encrypted_private_key": current_user.encrypted_private_key}
|
||||||
|
|
||||||
@usersRouter.get("/all")
|
@usersRouter.get("/all")
|
||||||
async def read_users_all(current_user: models.User = Depends(get_current_user), db: Session = Depends(get_db)):
|
async def read_users_all(current_user: models.User = Depends(get_current_user), db: Session = Depends(get_db)):
|
||||||
users = db.query(models.User).all()
|
users = db.query(models.User).all()
|
||||||
return [{"id": user.id, "username": user.username, "name": f"{user.first_name} {user.last_name or ''}".strip()} for user in users]
|
return [{"id": user.id, "username": user.username, "name": f"{user.first_name} {user.last_name or ''}".strip(), "public_key": user.public_key} for user in users]
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
class SetPublicKey(BaseModel):
|
||||||
|
public_key: str
|
||||||
|
|
||||||
|
class RefreshRequest(BaseModel):
|
||||||
|
refresh_token: str
|
||||||
|
|
||||||
|
class SetupAccount(BaseModel):
|
||||||
|
first_name: str
|
||||||
|
last_name: str
|
||||||
|
public_key: str
|
||||||
|
encrypted_private_key: str
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
from sqlalchemy import Column, Integer, String, create_engine
|
from sqlalchemy import Column, Integer, String, create_engine
|
||||||
from sqlalchemy.ext.declarative import declarative_base
|
from sqlalchemy.ext.declarative import declarative_base
|
||||||
from sqlalchemy.orm import sessionmaker
|
from sqlalchemy.orm import sessionmaker
|
||||||
from pydantic import BaseModel
|
|
||||||
|
|
||||||
SQLALCHEMY_DATABASE_URL = "sqlite:///./chepuhagram.db"
|
SQLALCHEMY_DATABASE_URL = "sqlite:///./chepuhagram.db"
|
||||||
engine = create_engine(SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False})
|
engine = create_engine(SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False})
|
||||||
|
|
@ -20,8 +19,6 @@ class User(Base):
|
||||||
totp_secret = Column(String(32), nullable=True)
|
totp_secret = Column(String(32), nullable=True)
|
||||||
hashed_password = Column(String)
|
hashed_password = Column(String)
|
||||||
public_key = Column(String, nullable=True)
|
public_key = Column(String, nullable=True)
|
||||||
|
encrypted_private_key = Column(String, nullable=True)
|
||||||
class RefreshRequest(BaseModel):
|
|
||||||
refresh_token: str
|
|
||||||
|
|
||||||
Base.metadata.create_all(bind=engine)
|
Base.metadata.create_all(bind=engine)
|
||||||
Loading…
Reference in New Issue