Compare commits
2 Commits
d33c41010d
...
ee7d325856
| Author | SHA1 | Date |
|---|---|---|
|
|
ee7d325856 | |
|
|
a7fe16954f |
|
|
@ -0,0 +1,25 @@
|
||||||
|
{
|
||||||
|
// Use IntelliSense to learn about possible attributes.
|
||||||
|
// Hover to view descriptions of existing attributes.
|
||||||
|
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"name": "chepuhagram",
|
||||||
|
"request": "launch",
|
||||||
|
"type": "dart"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "chepuhagram (profile mode)",
|
||||||
|
"request": "launch",
|
||||||
|
"type": "dart",
|
||||||
|
"flutterMode": "profile"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "chepuhagram (release mode)",
|
||||||
|
"request": "launch",
|
||||||
|
"type": "dart",
|
||||||
|
"flutterMode": "release"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
description: This file stores settings for Dart & Flutter DevTools.
|
description: This file stores settings for Dart & Flutter DevTools.
|
||||||
documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states
|
documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states
|
||||||
extensions:
|
extensions:
|
||||||
|
- provider: true
|
||||||
|
- shared_preferences: true
|
||||||
|
|
@ -45,7 +45,7 @@ class SocketService with WidgetsBindingObserver {
|
||||||
}
|
}
|
||||||
if (!allowConnect) return; // Не разрешаем подключение
|
if (!allowConnect) return; // Не разрешаем подключение
|
||||||
|
|
||||||
// В FastAPI эндпоинт обычно ожидает токен в URL или подзаголовке
|
// В FastAPI эндпоинт ожидает токен в URL-параметре
|
||||||
final uri = Uri.parse("${AppConstants.wsUrl}/ws?token=$token");
|
final uri = Uri.parse("${AppConstants.wsUrl}/ws?token=$token");
|
||||||
|
|
||||||
//_channel = WebSocketChannel.connect(uri);
|
//_channel = WebSocketChannel.connect(uri);
|
||||||
|
|
@ -81,9 +81,12 @@ class SocketService with WidgetsBindingObserver {
|
||||||
}
|
}
|
||||||
|
|
||||||
bool sendMessage(Map<String, dynamic> data, {int retryCnt = 0}) {
|
bool sendMessage(Map<String, dynamic> data, {int retryCnt = 0}) {
|
||||||
|
const maxRetries = 5;
|
||||||
if (_channel == null) {
|
if (_channel == null) {
|
||||||
//print("❌ ОШИБКА: Попытка отправить сообщение через NULL канал.");
|
if (retryCnt < maxRetries) {
|
||||||
sendMessage(data, retryCnt: retryCnt + 1);
|
// Schedule retry with exponential backoff
|
||||||
|
Future.delayed(Duration(seconds: 1 << retryCnt), () => sendMessage(data, retryCnt: retryCnt + 1));
|
||||||
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import 'dart:convert';
|
||||||
class ApiService extends ChangeNotifier {
|
class ApiService extends ChangeNotifier {
|
||||||
final _client = http.Client();
|
final _client = http.Client();
|
||||||
final _storage = const FlutterSecureStorage();
|
final _storage = const FlutterSecureStorage();
|
||||||
|
bool _isRefreshing = false;
|
||||||
|
|
||||||
Future<String?> uploadMedia(List<int> bytes) async {
|
Future<String?> uploadMedia(List<int> bytes) async {
|
||||||
try {
|
try {
|
||||||
|
|
@ -17,7 +18,6 @@ class ApiService extends ChangeNotifier {
|
||||||
Uri.parse('${AppConstants.baseUrl}/media/upload'),
|
Uri.parse('${AppConstants.baseUrl}/media/upload'),
|
||||||
);
|
);
|
||||||
request.headers.addAll({
|
request.headers.addAll({
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'Authorization': 'Bearer $token',
|
'Authorization': 'Bearer $token',
|
||||||
});
|
});
|
||||||
// Добавляем файл в запрос
|
// Добавляем файл в запрос
|
||||||
|
|
@ -32,7 +32,7 @@ class ApiService extends ChangeNotifier {
|
||||||
// Добавь заголовки авторизации, если они у тебя есть (JWT и т.д.)
|
// Добавь заголовки авторизации, если они у тебя есть (JWT и т.д.)
|
||||||
// request.headers.addAll({'Authorization': 'Bearer $token'});
|
// request.headers.addAll({'Authorization': 'Bearer $token'});
|
||||||
|
|
||||||
var streamedResponse = await request.send();
|
var streamedResponse = await request.send().timeout(Duration(seconds: 30));
|
||||||
var response = await http.Response.fromStream(streamedResponse);
|
var response = await http.Response.fromStream(streamedResponse);
|
||||||
|
|
||||||
if (response.statusCode == 200) {
|
if (response.statusCode == 200) {
|
||||||
|
|
@ -48,6 +48,11 @@ class ApiService extends ChangeNotifier {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> refreshToken() async {
|
Future<bool> refreshToken() async {
|
||||||
|
if (_isRefreshing) {
|
||||||
|
// Already refreshing, wait for completion or return true assuming it will succeed
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
_isRefreshing = true;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
@ -56,7 +61,7 @@ class ApiService extends ChangeNotifier {
|
||||||
Uri.parse('${AppConstants.baseUrl}/auth/refresh'),
|
Uri.parse('${AppConstants.baseUrl}/auth/refresh'),
|
||||||
body: jsonEncode({'refresh_token': refreshToken}),
|
body: jsonEncode({'refresh_token': refreshToken}),
|
||||||
headers: {'Content-Type': 'application/json'},
|
headers: {'Content-Type': 'application/json'},
|
||||||
);
|
).timeout(Duration(seconds: 30));
|
||||||
|
|
||||||
final decodedResponse =
|
final decodedResponse =
|
||||||
jsonDecode(utf8.decode(response.bodyBytes)) as Map;
|
jsonDecode(utf8.decode(response.bodyBytes)) as Map;
|
||||||
|
|
@ -78,7 +83,9 @@ class ApiService extends ChangeNotifier {
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
rethrow;
|
return false;
|
||||||
|
} finally {
|
||||||
|
_isRefreshing = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -54,6 +54,10 @@ class ContactProvider extends ChangeNotifier {
|
||||||
.where((contact) => contact.id != userIdCopy)
|
.where((contact) => contact.id != userIdCopy)
|
||||||
.toList();
|
.toList();
|
||||||
});
|
});
|
||||||
|
// Check if user changed during isolate execution
|
||||||
|
if (userIdCopy != _currentUserId) {
|
||||||
|
return; // Discard stale data
|
||||||
|
}
|
||||||
_allContacts = _contacts;
|
_allContacts = _contacts;
|
||||||
_isLoading = false;
|
_isLoading = false;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,9 @@ class _ContactsScreenState extends State<ContactsScreen> with RouteAware {
|
||||||
StreamSubscription<dynamic>? _socketSubscription;
|
StreamSubscription<dynamic>? _socketSubscription;
|
||||||
bool _isDownloading = false;
|
bool _isDownloading = false;
|
||||||
double _downloadProgress = 0.0;
|
double _downloadProgress = 0.0;
|
||||||
|
int _downloadedBytes = 0;
|
||||||
|
int _downloadTotalBytes = 0;
|
||||||
|
int _apkFileSizeBytes = 0;
|
||||||
CancelToken? _cancelToken = CancelToken();
|
CancelToken? _cancelToken = CancelToken();
|
||||||
String? _latestApkUrl;
|
String? _latestApkUrl;
|
||||||
bool _showUpdateBanner = false;
|
bool _showUpdateBanner = false;
|
||||||
|
|
@ -183,6 +186,14 @@ class _ContactsScreenState extends State<ContactsScreen> with RouteAware {
|
||||||
_showUpdateBanner = true;
|
_showUpdateBanner = true;
|
||||||
_latestApkUrl = data['apk_url'];
|
_latestApkUrl = data['apk_url'];
|
||||||
});
|
});
|
||||||
|
if (_latestApkUrl != null) {
|
||||||
|
final size = await _fetchApkSize(_latestApkUrl!);
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_apkFileSizeBytes = size;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
@ -492,21 +503,27 @@ class _ContactsScreenState extends State<ContactsScreen> with RouteAware {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
currentAccountPicture: CircleAvatar(
|
currentAccountPicture: CircleAvatar(
|
||||||
backgroundColor: authProvider.avatarUrl == null && authProvider.avatarPath == null
|
backgroundColor:
|
||||||
|
authProvider.avatarUrl == null &&
|
||||||
|
authProvider.avatarPath == null
|
||||||
? Theme.of(context).colorScheme.onSurface
|
? Theme.of(context).colorScheme.onSurface
|
||||||
: null,
|
: null,
|
||||||
backgroundImage: authProvider.avatarUrl != null
|
backgroundImage: authProvider.avatarUrl != null
|
||||||
? NetworkImage(authProvider.avatarUrl!)
|
? NetworkImage(authProvider.avatarUrl!)
|
||||||
: authProvider.avatarPath != null
|
: authProvider.avatarPath != null
|
||||||
? FileImage(File(authProvider.avatarPath!))
|
? FileImage(File(authProvider.avatarPath!))
|
||||||
: null,
|
: null,
|
||||||
child: (authProvider.avatarUrl == null && authProvider.avatarPath == null)
|
child:
|
||||||
|
(authProvider.avatarUrl == null &&
|
||||||
|
authProvider.avatarPath == null)
|
||||||
? Text(
|
? Text(
|
||||||
initials.isEmpty ? 'U' : initials,
|
initials.isEmpty ? 'U' : initials,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 20,
|
fontSize: 20,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
color: Theme.of(context).colorScheme.primaryContainer,
|
color: Theme.of(
|
||||||
|
context,
|
||||||
|
).colorScheme.primaryContainer,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
: null,
|
: null,
|
||||||
|
|
@ -558,16 +575,24 @@ class _ContactsScreenState extends State<ContactsScreen> with RouteAware {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
setState(() {
|
||||||
|
_downloadProgress = 0.0;
|
||||||
|
_downloadedBytes = 0;
|
||||||
|
_downloadTotalBytes = 0;
|
||||||
|
});
|
||||||
|
|
||||||
// Скачиваем файл «в лоб»
|
// Скачиваем файл «в лоб»
|
||||||
await Dio().download(
|
await Dio().download(
|
||||||
_latestApkUrl!,
|
_latestApkUrl!,
|
||||||
path,
|
path,
|
||||||
cancelToken: _cancelToken,
|
cancelToken: _cancelToken,
|
||||||
onReceiveProgress: (rec, total) {
|
onReceiveProgress: (rec, total) {
|
||||||
if (total != -1) {
|
if (mounted) {
|
||||||
if (mounted) {
|
setState(() {
|
||||||
setState(() => _downloadProgress = rec / total);
|
_downloadedBytes = rec;
|
||||||
}
|
_downloadTotalBytes = total > 0 ? total : 0;
|
||||||
|
_downloadProgress = total > 0 ? rec / total : 0.0;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
@ -585,11 +610,36 @@ class _ContactsScreenState extends State<ContactsScreen> with RouteAware {
|
||||||
print("Ошибка: $e");
|
print("Ошибка: $e");
|
||||||
} finally {
|
} finally {
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
setState(() => _isDownloading = false);
|
setState(() {
|
||||||
|
_isDownloading = false;
|
||||||
|
_downloadProgress = 0.0;
|
||||||
|
_downloadedBytes = 0;
|
||||||
|
_downloadTotalBytes = 0;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<int> _fetchApkSize(String url) async {
|
||||||
|
try {
|
||||||
|
final response = await http.head(Uri.parse(url));
|
||||||
|
final lengthHeader = response.headers['content-length'];
|
||||||
|
if (lengthHeader == null) return 0;
|
||||||
|
return int.tryParse(lengthHeader) ?? 0;
|
||||||
|
} catch (_) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String _formatBytes(int bytes) {
|
||||||
|
if (bytes <= 0) return '0 B';
|
||||||
|
const kb = 1024;
|
||||||
|
const mb = kb * 1024;
|
||||||
|
if (bytes < kb) return '$bytes B';
|
||||||
|
if (bytes < mb) return '${(bytes / kb).toStringAsFixed(1)} KB';
|
||||||
|
return '${(bytes / mb).toStringAsFixed(1)} MB';
|
||||||
|
}
|
||||||
|
|
||||||
Widget _buildUpdateBanner() {
|
Widget _buildUpdateBanner() {
|
||||||
return Container(
|
return Container(
|
||||||
margin: const EdgeInsets.fromLTRB(
|
margin: const EdgeInsets.fromLTRB(
|
||||||
|
|
@ -626,7 +676,9 @@ class _ContactsScreenState extends State<ContactsScreen> with RouteAware {
|
||||||
child: Text(
|
child: Text(
|
||||||
_isDownloading
|
_isDownloading
|
||||||
? 'Скачивание ${(_downloadProgress * 100).toStringAsFixed(0)}%'
|
? 'Скачивание ${(_downloadProgress * 100).toStringAsFixed(0)}%'
|
||||||
: "Доступно новое обновление!",
|
: _apkFileSizeBytes > 0
|
||||||
|
? 'Доступно новое обновление: ${_formatBytes(_apkFileSizeBytes)}'
|
||||||
|
: 'Доступно новое обновление!',
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
|
|
@ -643,6 +695,8 @@ class _ContactsScreenState extends State<ContactsScreen> with RouteAware {
|
||||||
_isDownloading = false;
|
_isDownloading = false;
|
||||||
_cancelToken = null; // Обязательно обнуляем токен!
|
_cancelToken = null; // Обязательно обнуляем токен!
|
||||||
_downloadProgress = 0.0;
|
_downloadProgress = 0.0;
|
||||||
|
_downloadedBytes = 0;
|
||||||
|
_downloadTotalBytes = 0;
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// Если не качаем — запускаем
|
// Если не качаем — запускаем
|
||||||
|
|
@ -679,6 +733,14 @@ class _ContactsScreenState extends State<ContactsScreen> with RouteAware {
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
backgroundColor: Colors.white24,
|
backgroundColor: Colors.white24,
|
||||||
),
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Align(
|
||||||
|
alignment: Alignment.centerLeft,
|
||||||
|
child: Text(
|
||||||
|
'${_formatBytes(_downloadedBytes)} из ${_formatBytes(_downloadTotalBytes)}',
|
||||||
|
style: const TextStyle(color: Colors.white70, fontSize: 14),
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,6 @@ import 'package:chepuhagram/presentation/screens/appearance_settings_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';
|
||||||
import '/core/theme_manager.dart';
|
|
||||||
import 'package:package_info_plus/package_info_plus.dart';
|
import 'package:package_info_plus/package_info_plus.dart';
|
||||||
import 'package:image_picker/image_picker.dart';
|
import 'package:image_picker/image_picker.dart';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
|
||||||
|
|
@ -15,9 +15,15 @@ class ContactTile extends StatefulWidget {
|
||||||
class _ContactTileState extends State<ContactTile> {
|
class _ContactTileState extends State<ContactTile> {
|
||||||
SharedPreferences? _prefs;
|
SharedPreferences? _prefs;
|
||||||
|
|
||||||
|
|
||||||
|
Duration? offset;
|
||||||
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
DateTime now = DateTime.now();
|
||||||
|
offset = now.timeZoneOffset;
|
||||||
_initPrefs();
|
_initPrefs();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -126,6 +132,7 @@ class _ContactTileState extends State<ContactTile> {
|
||||||
|
|
||||||
String _formatTime(DateTime? time) {
|
String _formatTime(DateTime? time) {
|
||||||
if (time == null) return "";
|
if (time == null) return "";
|
||||||
|
time = time.add(offset!);
|
||||||
return "${time.hour}:${time.minute.toString().padLeft(2, '0')}";
|
return "${time.hour}:${time.minute.toString().padLeft(2, '0')}";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
|
||||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||||
# In Windows, build-name is used as the major, minor, and patch parts
|
# In Windows, build-name is used as the major, minor, and patch parts
|
||||||
# of the product and file versions while build-number is used as the build suffix.
|
# of the product and file versions while build-number is used as the build suffix.
|
||||||
version: 2.0.0+1
|
version: 2.0.1+1
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ^3.10.0
|
sdk: ^3.10.0
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
from fastapi import FastAPI, Depends, HTTPException, status, APIRouter, File, UploadFile
|
from fastapi import FastAPI, Depends, HTTPException, status, APIRouter, File, UploadFile, Request
|
||||||
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
|
||||||
|
|
@ -8,7 +8,9 @@ from jose import JWTError, jwt
|
||||||
from app.core.security import get_current_user
|
from app.core.security import get_current_user
|
||||||
from fastapi.responses import FileResponse
|
from fastapi.responses import FileResponse
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
import uuid
|
import uuid
|
||||||
|
from io import BytesIO
|
||||||
# бд
|
# бд
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -30,12 +32,79 @@ if not os.path.exists(UPLOAD_FOLDER):
|
||||||
os.makedirs(UPLOAD_FOLDER)
|
os.makedirs(UPLOAD_FOLDER)
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_multipart_body(body: bytes):
|
||||||
|
try:
|
||||||
|
if not body.startswith(b'--'):
|
||||||
|
return None
|
||||||
|
|
||||||
|
boundary, rest = body.split(b'\r\n', 1)
|
||||||
|
parts = body.split(boundary)
|
||||||
|
for part in parts:
|
||||||
|
if not part or part in (b'--', b'--\r\n'):
|
||||||
|
continue
|
||||||
|
|
||||||
|
part = part.strip(b'\r\n')
|
||||||
|
if not part:
|
||||||
|
continue
|
||||||
|
|
||||||
|
headers, _, content = part.partition(b'\r\n\r\n')
|
||||||
|
if not headers or content is None:
|
||||||
|
continue
|
||||||
|
|
||||||
|
disposition_match = re.search(
|
||||||
|
br'Content-Disposition:\s*form-data;\s*name="([^"]+)"(?:;\s*filename="([^"]+)")?',
|
||||||
|
headers,
|
||||||
|
re.IGNORECASE,
|
||||||
|
)
|
||||||
|
if not disposition_match:
|
||||||
|
continue
|
||||||
|
|
||||||
|
field_name = disposition_match.group(1).decode('utf-8', errors='ignore')
|
||||||
|
filename = disposition_match.group(2)
|
||||||
|
if field_name != 'file':
|
||||||
|
continue
|
||||||
|
|
||||||
|
filename = filename.decode('utf-8', errors='ignore') if filename else 'upload.bin'
|
||||||
|
content_type_match = re.search(br'Content-Type:\s*([\w\-\/]+)', headers, re.IGNORECASE)
|
||||||
|
content_type = (
|
||||||
|
content_type_match.group(1).decode('utf-8', errors='ignore')
|
||||||
|
if content_type_match
|
||||||
|
else 'application/octet-stream'
|
||||||
|
)
|
||||||
|
return filename, content.rstrip(b'\r\n'), content_type
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
@mediaRouter.post('/upload')
|
@mediaRouter.post('/upload')
|
||||||
async def upload_file(file: UploadFile = File(...)):
|
async def upload_file(request: Request, file: UploadFile = File(None)):
|
||||||
# Проверяем, есть ли файл в запросе
|
uploaded_file = file
|
||||||
if not file.filename:
|
if uploaded_file is None:
|
||||||
|
raw_body = await request.body()
|
||||||
|
parsed = _parse_multipart_body(raw_body)
|
||||||
|
if parsed is not None:
|
||||||
|
filename, content, content_type = parsed
|
||||||
|
uploaded_file = UploadFile(
|
||||||
|
filename=filename,
|
||||||
|
file=BytesIO(content),
|
||||||
|
content_type=content_type,
|
||||||
|
)
|
||||||
|
|
||||||
|
if uploaded_file is None or not uploaded_file.filename:
|
||||||
raise HTTPException(status_code=400, detail="No selected file")
|
raise HTTPException(status_code=400, detail="No selected file")
|
||||||
|
|
||||||
|
# Валидация размера файла (макс 10MB)
|
||||||
|
MAX_FILE_SIZE = 10 * 1024 * 1024
|
||||||
|
content = await uploaded_file.read()
|
||||||
|
if len(content) > MAX_FILE_SIZE:
|
||||||
|
raise HTTPException(status_code=400, detail="File too large (max 10MB)")
|
||||||
|
|
||||||
|
# Валидация типа файла (для зашифрованных файлов пропускаем, так как content_type не image)
|
||||||
|
# ALLOWED_TYPES = {'image/jpeg', 'image/png', 'image/gif', 'image/webp'}
|
||||||
|
# if uploaded_file.content_type not in ALLOWED_TYPES:
|
||||||
|
# raise HTTPException(status_code=400, detail="Invalid file type")
|
||||||
|
|
||||||
# Генерируем уникальное имя, чтобы файлы не перезаписывались
|
# Генерируем уникальное имя, чтобы файлы не перезаписывались
|
||||||
file_id = str(uuid.uuid4())
|
file_id = str(uuid.uuid4())
|
||||||
filename = f"{file_id}.enc"
|
filename = f"{file_id}.enc"
|
||||||
|
|
@ -43,7 +112,6 @@ async def upload_file(file: UploadFile = File(...)):
|
||||||
|
|
||||||
# Сохраняем
|
# Сохраняем
|
||||||
with open(file_path, "wb") as f:
|
with open(file_path, "wb") as f:
|
||||||
content = await file.read()
|
|
||||||
f.write(content)
|
f.write(content)
|
||||||
|
|
||||||
print(f"Файл сохранен: {file_path}")
|
print(f"Файл сохранен: {file_path}")
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
import os
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
|
load_dotenv()
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
# Database
|
||||||
|
DATABASE_URL: str = os.getenv("DATABASE_URL", "sqlite:///./chepuhagram.db")
|
||||||
|
|
||||||
|
# Security
|
||||||
|
JWT_KEY: str = os.getenv("JWT_KEY", "")
|
||||||
|
if not JWT_KEY:
|
||||||
|
raise RuntimeError("JWT_KEY environment variable not set")
|
||||||
|
|
||||||
|
# Firebase
|
||||||
|
FIREBASE_CREDENTIALS_PATH: str = os.getenv("FIREBASE_CREDENTIALS_PATH", "chepuhagram-6ca5d-firebase-adminsdk-fbsvc-cf8a5ad2f3.json")
|
||||||
|
|
||||||
|
# Server
|
||||||
|
HOST: str = os.getenv("HOST", "0.0.0.0")
|
||||||
|
PORT: int = int(os.getenv("PORT", "8000"))
|
||||||
|
|
||||||
|
# CORS
|
||||||
|
ALLOWED_ORIGINS: list = os.getenv("ALLOWED_ORIGINS", "http://localhost:3000,http://127.0.0.1:3000").split(",")
|
||||||
|
|
||||||
|
config = Config()
|
||||||
|
|
@ -10,7 +10,10 @@ from jose import JWTError, jwt
|
||||||
import os
|
import os
|
||||||
import bcrypt
|
import bcrypt
|
||||||
load_dotenv()
|
load_dotenv()
|
||||||
SECRET_KEY = os.getenv("JWT_KEY").strip()
|
SECRET_KEY = os.getenv("JWT_KEY")
|
||||||
|
if not SECRET_KEY:
|
||||||
|
raise RuntimeError("JWT_KEY environment variable not set")
|
||||||
|
SECRET_KEY = SECRET_KEY.strip()
|
||||||
ALGORITHM = "HS256"
|
ALGORITHM = "HS256"
|
||||||
ACCESS_TOKEN_EXPIRE_MINUTES = 30
|
ACCESS_TOKEN_EXPIRE_MINUTES = 30
|
||||||
REFRESH_TOKEN_EXPIRE_MINUTES = 60 * 24 * 60
|
REFRESH_TOKEN_EXPIRE_MINUTES = 60 * 24 * 60
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,9 @@ from sqlalchemy.orm import sessionmaker
|
||||||
from sqlalchemy import Column, Integer, Text, ForeignKey, DateTime
|
from sqlalchemy import Column, Integer, Text, ForeignKey, DateTime
|
||||||
from sqlalchemy.sql import func
|
from sqlalchemy.sql import func
|
||||||
from sqlalchemy import text
|
from sqlalchemy import text
|
||||||
|
from app.core.config import config
|
||||||
|
|
||||||
SQLALCHEMY_DATABASE_URL = "sqlite:///./chepuhagram.db"
|
SQLALCHEMY_DATABASE_URL = config.DATABASE_URL
|
||||||
engine = create_engine(SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False})
|
engine = create_engine(SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False})
|
||||||
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
||||||
Base = declarative_base()
|
Base = declarative_base()
|
||||||
|
|
|
||||||
|
|
@ -7,9 +7,9 @@ from sqlalchemy.orm import Session
|
||||||
from app.db import models
|
from app.db import models
|
||||||
from firebase_admin import messaging, credentials, exceptions
|
from firebase_admin import messaging, credentials, exceptions
|
||||||
import firebase_admin
|
import firebase_admin
|
||||||
|
from app.core.config import config
|
||||||
|
|
||||||
cred = credentials.Certificate(
|
cred = credentials.Certificate(config.FIREBASE_CREDENTIALS_PATH)
|
||||||
"chepuhagram-6ca5d-firebase-adminsdk-fbsvc-cf8a5ad2f3.json")
|
|
||||||
firebase_admin.initialize_app(cred)
|
firebase_admin.initialize_app(cred)
|
||||||
|
|
||||||
# бд
|
# бд
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ from fastapi.middleware.cors import CORSMiddleware
|
||||||
import os
|
import os
|
||||||
import asyncio
|
import asyncio
|
||||||
from app.db import models
|
from app.db import models
|
||||||
|
from app.core.config import config
|
||||||
|
|
||||||
app = FastAPI()
|
app = FastAPI()
|
||||||
|
|
||||||
|
|
@ -17,7 +18,7 @@ app.include_router(wsRouter)
|
||||||
|
|
||||||
app.add_middleware(
|
app.add_middleware(
|
||||||
CORSMiddleware,
|
CORSMiddleware,
|
||||||
allow_origins=["*"],
|
allow_origins=config.ALLOWED_ORIGINS,
|
||||||
allow_credentials=True,
|
allow_credentials=True,
|
||||||
allow_methods=["*"],
|
allow_methods=["*"],
|
||||||
allow_headers=["*"],
|
allow_headers=["*"],
|
||||||
|
|
@ -27,7 +28,7 @@ app.add_middleware(
|
||||||
@app.get("/check-update")
|
@app.get("/check-update")
|
||||||
async def check_update():
|
async def check_update():
|
||||||
return {
|
return {
|
||||||
"latest_version": "2.0.0",
|
"latest_version": "2.0.1",
|
||||||
"apk_url": "https://api.chepuhagram.ru/get-update",
|
"apk_url": "https://api.chepuhagram.ru/get-update",
|
||||||
"force_update": False
|
"force_update": False
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue