feat: deploy infrastructure + disk/drive, calendar, presentation, workspaces, onboarding, demo user

This commit is contained in:
2026-05-19 16:26:26 +03:00
parent 688d043dad
commit 3529c39b22
304 changed files with 18826 additions and 1176 deletions
+150
View File
@@ -0,0 +1,150 @@
import 'package:dio/dio.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../core/constants/app_constants.dart';
import 'endpoints.dart';
import 'exceptions.dart';
class ApiClient {
late final Dio _dio;
final FlutterSecureStorage _storage = const FlutterSecureStorage();
static const _tokenKey = 'access_token';
static const _refreshKey = 'refresh_token';
ApiClient() {
_dio = Dio(BaseOptions(
baseUrl: AppConstants.apiBaseUrl,
connectTimeout: AppConstants.connectTimeout,
receiveTimeout: AppConstants.receiveTimeout,
headers: {'Content-Type': 'application/json'},
));
_dio.interceptors.add(InterceptorsWrapper(
onRequest: (options, handler) async {
final token = await _storage.read(key: _tokenKey);
if (token != null) {
options.headers['Authorization'] = 'Bearer $token';
}
handler.next(options);
},
onError: (error, handler) async {
if (error.response?.statusCode == 401) {
final refreshed = await _tryRefresh();
if (refreshed) {
final retryResponse = await _retry(error.requestOptions);
handler.resolve(retryResponse);
return;
}
}
handler.next(error);
},
));
}
Future<bool> _tryRefresh() async {
try {
final refreshToken = await _storage.read(key: _refreshKey);
if (refreshToken == null) return false;
final response = await Dio(BaseOptions(
baseUrl: AppConstants.apiBaseUrl,
)).post(Endpoints.refresh, data: {'refresh_token': refreshToken});
if (response.statusCode == 200) {
await _storage.write(
key: _tokenKey, value: response.data['access_token']);
await _storage.write(
key: _refreshKey, value: response.data['refresh_token']);
return true;
}
} catch (_) {}
return false;
}
Future<Response> _retry(RequestOptions requestOptions) async {
final token = await _storage.read(key: _tokenKey);
final options = Options(
method: requestOptions.method,
headers: {
...requestOptions.headers,
'Authorization': 'Bearer $token',
},
);
return _dio.request(requestOptions.path,
data: requestOptions.data,
queryParameters: requestOptions.queryParameters,
options: options);
}
Future<Response<T>> _request<T>(
String method,
String path, {
Map<String, dynamic>? data,
Map<String, dynamic>? queryParameters,
}) async {
try {
final response = await _dio.request<T>(
path,
data: data,
queryParameters: queryParameters,
options: Options(method: method),
);
return response;
} on DioException catch (e) {
throw _mapError(e);
}
}
Future<Response> get(String path, {Map<String, dynamic>? params}) =>
_request('GET', path, queryParameters: params);
Future<Response> post(String path, {Map<String, dynamic>? data}) =>
_request('POST', path, data: data);
Future<Response> put(String path, {Map<String, dynamic>? data}) =>
_request('PUT', path, data: data);
Future<Response> patch(String path, {Map<String, dynamic>? data}) =>
_request('PATCH', path, data: data);
Future<Response> delete(String path) => _request('DELETE', path);
Future<void> saveTokens(
String accessToken, String refreshToken) async {
await _storage.write(key: _tokenKey, value: accessToken);
await _storage.write(key: _refreshKey, value: refreshToken);
}
Future<void> clearTokens() async {
await _storage.delete(key: _tokenKey);
await _storage.delete(key: _refreshKey);
}
Future<String?> getAccessToken() => _storage.read(key: _tokenKey);
ApiException _mapError(DioException e) {
final statusCode = e.response?.statusCode;
final data = e.response?.data;
final message = data is Map ? data['detail']?.toString() ?? '' : '';
switch (statusCode) {
case 401:
return UnauthorizedException(message);
case 404:
return NotFoundException(message);
case 422:
return ValidationException(
message, data is Map ? data['errors'] : null);
case 403:
return TariffLimitException(message);
case 500:
return ServerException(message);
default:
if (e.type == DioExceptionType.connectionTimeout ||
e.type == DioExceptionType.receiveTimeout ||
e.type == DioExceptionType.connectionError) {
return NetworkException();
}
return ApiException(message, statusCode: statusCode);
}
}
}
final apiClientProvider = Provider<ApiClient>((ref) => ApiClient());
+63
View File
@@ -0,0 +1,63 @@
class Endpoints {
Endpoints._();
static const String auth = '/auth';
static const String login = '/auth/login';
static const String register = '/auth/register';
static const String refresh = '/auth/refresh';
static const String twoFactor = '/auth/2fa';
static const String forgotPassword = '/auth/forgot-password';
static const String resetPassword = '/auth/reset-password';
static const String oauth = '/auth/oauth';
static const String ideas = '/ideas';
static String idea(String id) => '/ideas/$id';
static String ideaFunnel(String id) => '/ideas/$id/funnel';
static String ideaComments(String id) => '/ideas/$id/comments';
static String ideaVote(String id) => '/ideas/$id/vote';
static String ideaActivity(String id) => '/ideas/$id/activity';
static String ideaCalendar(String id) => '/ideas/$id/calendar';
static const String voiceTranscribe = '/voice/transcribe';
static const String voiceChat = '/voice/chat';
static const String voiceSessions = '/voice/sessions';
static String voiceSession(String id) => '/voice/sessions/$id';
static String voiceCommands(String sessionId) =>
'/voice/sessions/$sessionId/commands';
static const String workspaces = '/workspaces';
static String workspace(String id) => '/workspaces/$id';
static String workspaceMembers(String id) => '/workspaces/$id/members';
static String workspaceMember(String wid, String uid) =>
'/workspaces/$wid/members/$uid';
static const String notifications = '/notifications';
static String notification(String id) => '/notifications/$id';
static const String notificationsReadAll = '/notifications/read-all';
static const String notificationsUnreadCount = '/notifications/unread-count';
static const String calendarProviders = '/calendar/providers';
static const String calendarStatus = '/calendar/status';
static const String calendarDisconnect = '/calendar/disconnect';
static const String diskProviders = '/disk/providers';
static const String diskStatus = '/disk/status';
static const String diskConnect = '/disk/connect';
static const String diskDisconnect = '/disk/disconnect';
static String diskOAuthUrl(String provider) => '/disk/oauth-url/$provider';
static String ideaPresentation(String id) => '/ideas/$id/presentation';
static const String adminUsers = '/admin/users';
static String adminUser(String id) => '/admin/users/$id';
static const adminAgents = '/admin/agents';
static String adminAgent(String name) => '/admin/agents/$name';
static String adminAgentRun(String name) => '/admin/agents/$name/run';
static const String adminAgentsRunAll = '/admin/agents/run-all';
static const String adminLogs = '/admin/logs';
static const String adminTariffs = '/admin/tariffs';
static String adminTariff(String id) => '/admin/tariffs/$id';
static const String adminBotCommands = '/admin/bot-commands';
static const String adminStats = '/admin/stats';
static const String adminHealth = '/admin/health';
}
+41
View File
@@ -0,0 +1,41 @@
class ApiException implements Exception {
final String message;
final int? statusCode;
final Map<String, dynamic>? errors;
ApiException(this.message, {this.statusCode, this.errors});
@override
String toString() => 'ApiException($statusCode): $message';
}
class UnauthorizedException extends ApiException {
UnauthorizedException([String? message])
: super(message ?? 'Требуется авторизация', statusCode: 401);
}
class NotFoundException extends ApiException {
NotFoundException([String? message])
: super(message ?? 'Ресурс не найден', statusCode: 404);
}
class ValidationException extends ApiException {
ValidationException([String? message, Map<String, dynamic>? errors])
: super(message ?? 'Ошибка валидации',
statusCode: 422, errors: errors);
}
class ServerException extends ApiException {
ServerException([String? message])
: super(message ?? 'Внутренняя ошибка сервера', statusCode: 500);
}
class NetworkException extends ApiException {
NetworkException([String? message])
: super(message ?? 'Ошибка сети', statusCode: null);
}
class TariffLimitException extends ApiException {
TariffLimitException([String? message])
: super(message ?? 'Достигнут лимит тарифа', statusCode: 403);
}