Frontend first draft

This commit is contained in:
Arseni
2025-11-13 15:06:15 +03:00
parent e47f343afb
commit ddb54ddfdc
504 changed files with 25498 additions and 1 deletions

View File

@@ -0,0 +1,61 @@
import 'package:logging/logging.dart';
import 'package:share_plus/share_plus.dart';
import 'package:pshared/api/requests/signup.dart';
import 'package:pshared/api/responses/account.dart';
import 'package:pshared/api/requests/change_password.dart';
import 'package:pshared/data/mapper/account/account.dart';
import 'package:pshared/models/account/account.dart';
import 'package:pshared/service/authorization/service.dart';
import 'package:pshared/service/files.dart';
import 'package:pshared/service/services.dart';
import 'package:pshared/utils/http/requests.dart';
class AccountService {
static final _logger = Logger('service.account');
static const String _objectType = Services.account;
static Future<Account> login(String email, String password, String locale) async {
_logger.fine('Logging in');
return AuthorizationService.login(_objectType, email, password, locale);
}
static Future<Account> restore() async {
return AuthorizationService.restore();
}
static Future<void> signup(SignupRequest request) async {
await getPOSTResponse(_objectType, 'signup', request.toJson());
}
static Future<void> logout() async {
_logger.fine('Logging out');
await AuthorizationService.logout();
}
static Future<Account> _getAccount(Future<Map<String, dynamic>> future) async {
final response = await future;
return AccountResponse.fromJson(response).account.toDomain();
}
static Future<Account> update(Account account) async {
_logger.fine('Patching account ${account.id}');
return _getAccount(AuthorizationService.getPUTResponse(_objectType, '', account.toDTO().toJson()));
}
static Future<Account> changePassword(String oldPassword, String newPassword) async {
_logger.fine('Changing password');
return _getAccount(AuthorizationService.getPATCHResponse(
_objectType,
'password',
ChangePassword(oldPassword: oldPassword, newPassword: newPassword).toJson(),
));
}
static Future<String> uploadAvatar(String id, XFile avatarFile) async {
_logger.fine('Uploading avatar');
return FilesService.uploadImage(_objectType, id, avatarFile);
}
}

View File

@@ -0,0 +1,35 @@
import 'package:logging/logging.dart';
import 'package:pshared/api/errors/unauthorized.dart';
import 'package:pshared/api/responses/employees.dart';
import 'package:pshared/models/organization/employee.dart';
import 'package:pshared/data/mapper/account/account.dart';
import 'package:pshared/service/authorization/service.dart';
import 'package:pshared/service/services.dart';
class EmployeesService {
static final _logger = Logger('service.employees');
static const String _objectType = Services.account;
static Future<List<Employee>> list(String organizationRef) async {
_logger.fine('Loading organization employees');
return _getEmployees(AuthorizationService.getGETResponse(_objectType, '/list/$organizationRef'));
}
static Future<List<Employee>> _getEmployees(Future<Map<String, dynamic>> future) async {
try {
final responseJson = await future;
final response = EmployeesResponse.fromJson(responseJson);
final accounts = response.accounts.map((dto) => dto.toDomain()).toList();
if (accounts.isEmpty) throw ErrorUnauthorized();
_logger.fine('Fetched ${accounts.length} account(s)');
return accounts;
} catch (e, stackTrace) {
_logger.severe('Failed to fetch accounts', e, stackTrace);
rethrow;
}
}
}

View File

@@ -0,0 +1,89 @@
import 'package:logging/logging.dart';
import 'package:pshared/api/errors/upload_failed.dart';
import 'package:pshared/api/requests/login.dart';
import 'package:pshared/api/responses/account.dart';
import 'package:pshared/api/responses/login.dart';
import 'package:pshared/config/constants.dart';
import 'package:pshared/data/mapper/account/account.dart';
import 'package:pshared/models/account/account.dart';
import 'package:pshared/service/authorization/storage.dart';
import 'package:pshared/service/authorization/token.dart';
import 'package:pshared/service/device_id.dart';
import 'package:pshared/utils/http/requests.dart' as httpr;
class AuthorizationService {
static final _logger = Logger('service.authorization');
static Future<void> _updateAccessToken(AccountResponse response) async {
await AuthorizationStorage.updateToken(response.accessToken);
}
static Future<void> _updateTokens(LoginResponse response) async {
await _updateAccessToken(response);
return AuthorizationStorage.updateRefreshToken(response.refreshToken);
}
static Future<LoginResponse> _completeLogin(Map<String, dynamic> response) async {
final LoginResponse lr = LoginResponse.fromJson(response);
await _updateTokens(lr);
return lr;
}
static Future<Account> login(String service, String email, String password, String locale) async {
_logger.fine('Logging in $email with $locale locale');
final deviceId = await DeviceIdManager.getDeviceId();
final response = await httpr.getPOSTResponse(
service,
'/login',
LoginRequest(
login: email.toLowerCase(),
password: password,
locale: locale,
deviceId: deviceId,
clientId: Constants.clientId,
).toJson());
return (await _completeLogin(response)).account.toDomain();
}
static Future<Account> restore() async {
return (await TokenService.rotateRefreshToken()).account.toDomain();
}
static Future<void> logout() async {
return AuthorizationStorage.removeTokens();
}
static Future<Map<String, dynamic>> _authenticatedRequest(
String service,
String url,
Future<Map<String, dynamic>> Function(String, String, Map<String, dynamic>, {String? authToken}) requestType,
{Map<String, dynamic>? body}) async {
final accessToken = await TokenService.getAccessToken();
return requestType(service, url, body ?? {}, authToken: accessToken);
}
static Future<Map<String, dynamic>> getPOSTResponse(String service, String url, Map<String, dynamic> body) async => _authenticatedRequest(service, url, httpr.getPOSTResponse, body: body);
static Future<Map<String, dynamic>> getGETResponse(String service, String url) async {
final accessToken = await TokenService.getAccessToken();
return httpr.getGETResponse(service, url, authToken: accessToken);
}
static Future<Map<String, dynamic>> getPUTResponse(String service, String url, Map<String, dynamic> body) async => _authenticatedRequest(service, url, httpr.getPUTResponse, body: body);
static Future<Map<String, dynamic>> getPATCHResponse(String service, String url, Map<String, dynamic> body) async => _authenticatedRequest(service, url, httpr.getPATCHResponse, body: body);
static Future<Map<String, dynamic>> getDELETEResponse(String service, String url, Map<String, dynamic> body) async => _authenticatedRequest(service, url, httpr.getDELETEResponse, body: body);
static Future<String> getFileUploadResponseAuth(String service, String url, String fileName, String fileType, String mediaType, List<int> bytes) async {
final accessToken = await TokenService.getAccessToken();
final res = await httpr.getFileUploadResponse(service, url, fileName, fileType, mediaType, bytes, authToken: accessToken);
if (res == null) {
throw ErrorUploadFailed();
}
return res.url;
}
}

View File

@@ -0,0 +1,53 @@
import 'dart:convert';
import 'package:logging/logging.dart';
import 'package:pshared/api/errors/unauthorized.dart';
import 'package:pshared/api/responses/token.dart';
import 'package:pshared/config/constants.dart';
import 'package:pshared/service/secure_storage.dart';
class AuthorizationStorage {
static final _logger = Logger('service.authorization.storage');
static Future<TokenData> _getTokenData(String tokenKey) async {
_logger.fine('Getting token data');
final String? tokenJson = await SecureStorageService.get(tokenKey);
if (tokenJson == null || tokenJson.isEmpty) {
throw ErrorUnauthorized();
}
return TokenData.fromJson(jsonDecode(tokenJson));
}
static Future<TokenData> getAccessToken() async {
_logger.fine('Getting access token');
return _getTokenData(Constants.accessTokenStorageKey);
}
static Future<TokenData> getRefreshToken() async {
_logger.fine('Getting refresh token');
return _getTokenData(Constants.refreshTokenStorageKey);
}
static Future<void> updateToken(TokenData tokenData) async {
_logger.fine('Storing access token...');
final tokenJson = jsonEncode(tokenData.toJson());
await SecureStorageService.set(Constants.accessTokenStorageKey, tokenJson);
}
static Future<void> updateRefreshToken(TokenData tokenData) async {
_logger.fine('Storing refresh token...');
final refreshTokenJson = jsonEncode(tokenData.toJson());
await SecureStorageService.set(Constants.refreshTokenStorageKey, refreshTokenJson);
}
static Future<void> removeTokens() {
return Future.wait([
SecureStorageService.delete(Constants.refreshTokenStorageKey),
SecureStorageService.delete(Constants.accessTokenStorageKey),
]);
}
}

View File

@@ -0,0 +1,85 @@
import 'package:logging/logging.dart';
import 'package:pshared/api/errors/unauthorized.dart';
import 'package:pshared/api/requests/tokens/access_refresh.dart';
import 'package:pshared/api/requests/tokens/refresh_rotate.dart';
import 'package:pshared/api/responses/account.dart';
import 'package:pshared/api/responses/login.dart';
import 'package:pshared/api/responses/token.dart';
import 'package:pshared/config/constants.dart';
import 'package:pshared/service/authorization/storage.dart';
import 'package:pshared/service/device_id.dart';
import 'package:pshared/service/services.dart';
import 'package:pshared/utils/http/requests.dart';
class TokenService {
static final _logger = Logger('service.authorization.token');
static const String _objectType = Services.account;
static bool _isTokenExpiringSoon(TokenData token, Duration duration) {
return token.expiration.isBefore(DateTime.now().add(duration));
}
static Future<String> getAccessToken() async {
TokenData token = await AuthorizationStorage.getAccessToken();
if (_isTokenExpiringSoon(token, const Duration(hours: 4))) {
token = (await _refreshAccessToken()).accessToken;
}
return token.token;
}
static Future<void> _updateTokens(LoginResponse response) async {
await AuthorizationStorage.updateToken(response.accessToken);
await AuthorizationStorage.updateRefreshToken(response.refreshToken);
}
static Future<AccountResponse> _refreshAccessToken() async {
_logger.fine('Refreshing access token...');
final deviceId = await DeviceIdManager.getDeviceId();
final refresh = await AuthorizationStorage.getRefreshToken();
if (_isTokenExpiringSoon(refresh, const Duration(days: 7))) {
return await rotateRefreshToken();
}
final response = await getPOSTResponse(
_objectType,
'/refresh',
AccessTokenRefreshRequest(
deviceId: deviceId,
clientId: Constants.clientId,
token: refresh.token,
).toJson(),
);
final accountResp = AccountResponse.fromJson(response);
await AuthorizationStorage.updateToken(accountResp.accessToken);
return accountResp;
}
static Future<LoginResponse> rotateRefreshToken() async {
_logger.fine('Rotating refresh token...');
final refresh = await AuthorizationStorage.getRefreshToken();
if (refresh.expiration.isBefore(DateTime.now())) throw ErrorUnauthorized();
final deviceId = await DeviceIdManager.getDeviceId();
final response = await getPOSTResponse(
_objectType,
'/rotate',
RotateRefreshTokenRequest(
token: refresh.token,
clientId: Constants.clientId,
deviceId: deviceId,
).toJson(),
);
final loginResponse = LoginResponse.fromJson(response);
await _updateTokens(loginResponse);
return loginResponse;
}
}

View File

@@ -0,0 +1,24 @@
import 'package:uuid/uuid.dart';
import 'package:logging/logging.dart';
import 'package:pshared/config/web.dart';
import 'package:pshared/service/secure_storage.dart';
class DeviceIdManager {
static final _logger = Logger('service.device_id');
static final String _key = Constants.deviceIdStorageKey;
static Future<String> getDeviceId() async {
String? deviceId = await SecureStorageService.get(_key);
if (deviceId == null) {
_logger.fine('Device id is not set, generating new');
deviceId = (const Uuid()).v4();
await SecureStorageService.set(_key, deviceId);
}
return deviceId;
}
}

View File

@@ -0,0 +1,38 @@
import 'dart:math';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:share_plus/share_plus.dart';
import 'package:pshared/service/authorization/service.dart';
import 'package:pshared/utils/image/conversion.dart';
import 'package:pshared/utils/image/transformed.dart';
String generateRandomLatinString(int length) {
const String chars = 'abcdefghijklmnopqrstuvwxyz';
final Random random = Random.secure();
return List.generate(length, (index) => chars[random.nextInt(chars.length)]).join();
}
class FilesService {
static Future<String> uploadImage(
String objectType,
String? id,
XFile imageFile, {
Future<TransformedImage> Function(XFile) fileReader = defaultTransformImage,
}) async {
final objRef = id ?? generateRandomLatinString(16);
final image = await fileReader(imageFile);
final res = await AuthorizationService.getFileUploadResponseAuth(
objectType,
'image/$objRef',
'$objRef.${image.imageType.split('/').last}',
'image',
image.imageType,
image.bytes
);
CachedNetworkImage.evictFromCache(res);
return res;
}
}

View File

@@ -0,0 +1,63 @@
import 'package:logging/logging.dart';
import 'package:share_plus/share_plus.dart';
import 'package:pshared/api/errors/unauthorized.dart';
import 'package:pshared/api/responses/organization.dart';
import 'package:pshared/models/organization/organization.dart';
import 'package:pshared/data/mapper/organization.dart';
import 'package:pshared/service/authorization/service.dart';
import 'package:pshared/service/files.dart';
import 'package:pshared/service/services.dart';
import 'package:pshared/utils/http/requests.dart';
class OrganizationService {
static final _logger = Logger('service.organization');
static const String _objectType = Services.organization;
static Future<List<Organization>> list() async {
_logger.fine('Loading all organizations');
return _getOrganizations(AuthorizationService.getGETResponse(_objectType, ''));
}
static Future<Organization> load(String organizationRef) async {
_logger.fine('Loading organization $organizationRef');
final orgs = await _getOrganizations(AuthorizationService.getGETResponse(_objectType, organizationRef));
return orgs.first;
}
static Future<Organization> loadByInvitation(String invitationRef) async {
_logger.fine('Loading organization by invitation ref $invitationRef');
final orgs = await _getOrganizations(getGETResponse(_objectType, 'invitation/$invitationRef'));
return orgs.first;
}
static Future<List<Organization>> update(Organization organization) async {
_logger.fine('Patching organization ${organization.id}');
// Convert domain object to DTO, then to JSON
return _getOrganizations(
AuthorizationService.getPUTResponse(_objectType, '', organization.toDTO().toJson())
);
}
static Future<String> uploadLogo(String organizationRef, XFile logoFile) async {
_logger.fine('Uploading logo');
return FilesService.uploadImage(_objectType, organizationRef, logoFile);
}
static Future<List<Organization>> _getOrganizations(Future<Map<String, dynamic>> future) async {
try {
final responseJson = await future;
final response = OrganizationResponse.fromJson(responseJson);
final orgs = response.organizations.map((dto) => dto.toDomain()).toList();
if (orgs.isEmpty) throw ErrorUnauthorized();
_logger.fine('Fetched ${orgs.length} organization(s)');
return orgs;
} catch (e, stackTrace) {
_logger.severe('Failed to fetch organizations', e, stackTrace);
rethrow;
}
}
}

View File

@@ -0,0 +1,79 @@
import 'package:logging/logging.dart';
import 'package:pshared/api/requests/change_role.dart';
import 'package:pshared/api/requests/permissions/change_policies.dart';
import 'package:pshared/api/responses/policies.dart';
import 'package:pshared/data/mapper/permissions/data/permissions.dart';
import 'package:pshared/data/mapper/permissions/descriptions/description.dart';
import 'package:pshared/models/permissions/access.dart';
import 'package:pshared/models/permissions/data/policy.dart';
import 'package:pshared/service/authorization/service.dart';
import 'package:pshared/service/services.dart';
class PermissionsService {
static final _logger = Logger('service.permissions');
static const String _objectType = Services.permission;
static Future<UserAccess> load(String organizationRef) async {
_logger.fine('Loading permissions...');
return _getPolicies(AuthorizationService.getGETResponse(_objectType, organizationRef));
}
static Future<UserAccess> loadAll(String organizationRef) async {
_logger.fine('Loading permissions for all the users...');
return _getPolicies(AuthorizationService.getGETResponse(_objectType, '/all/$organizationRef'));
}
static Future<void> changeRole(String organizationRef, ChangeRole request) async {
_logger.fine('Changing role for account ${request.accountRef} to role ${request.newRoleDescriptionRef}');
await AuthorizationService.getPOSTResponse(_objectType, '/change_role/$organizationRef', request.toJson());
}
static Future<void> deleteRoleDescription(String roleDescriptionRef) async {
_logger.fine('Deleting role $roleDescriptionRef...');
await AuthorizationService.getDELETEResponse(_objectType, '/role/$roleDescriptionRef', {});
}
static Future<void> createPolicies(List<Policy> policies) async {
_logger.fine('Creating ${policies.length} policies...');
await AuthorizationService.getPOSTResponse(
_objectType,
'/policies',
PoliciesChangeRequest.add(policies: policies).toJson(),
);
}
static Future<void> deletePolicies(List<Policy> policies) async {
_logger.fine('Deleting ${policies.length} policies...');
await AuthorizationService.getDELETEResponse(
_objectType,
'/policies',
PoliciesChangeRequest.remove(policies: policies).toJson(),
);
}
static Future<void> changePolicies(List<Policy> add, List<Policy> remove) async {
final common = add.toSet().intersection(remove.toSet());
if (common.isNotEmpty) {
throw ArgumentError.value(common, 'add/remove', 'These policies are in both add and remove: ${common.toString()}');
}
_logger.fine('Adding ${add.length} policies, removing ${remove.length} policies...');
await AuthorizationService.getPUTResponse(
_objectType,
'/policies',
PoliciesChangeRequest.change(add: add, remove: remove).toJson(),
);
}
static Future<UserAccess> _getPolicies(Future<Map<String, dynamic>> future) async {
final resp = PoliciesResponse.fromJson(await future);
final res = UserAccess(
descriptions: resp.descriptions.toDomain(),
permissions: resp.permissions.toDomain(),
);
_logger.fine('Loaded ${res.descriptions.roles.length} role descriptions, ${res.permissions.roles.length} role assignments, ${res.descriptions.policies.length} policy descriptions, ${res.permissions.policies.length} assigned policies, and ${res.permissions.permissions.length} assigned permissions');
return res;
}
}

View File

@@ -0,0 +1,15 @@
import 'package:json_annotation/json_annotation.dart';
part 'login.g.dart';
@JsonSerializable()
class LoginRequest {
final String login;
final String password;
const LoginRequest({required this.login, required this.password});
factory LoginRequest.fromJson(Map<String, dynamic> json) => _$LoginRequestFromJson(json);
Map<String, dynamic> toJson() => _$LoginRequestToJson(this);
}

View File

@@ -0,0 +1,29 @@
import 'dart:convert';
import 'package:logging/logging.dart';
import 'package:http/http.dart' as http;
import 'package:pshared/service/pfe/login.dart';
class PfeService {
static final _logger = Logger('service.pfe');
static Future<String> login(String email, String password) async {
_logger.fine('Logging in');
try {
final res = await http.post(
Uri.parse('http://localhost:3000/api/v1/auth/login'),
headers: {'Content-Type': 'application/json'},
body: json.encode(LoginRequest(login: email, password: password).toJson()),
);
return res.toString();
} catch (e) {
_logger.warning(e.toString());
rethrow;
}
}
}

View File

@@ -0,0 +1,25 @@
import 'package:shared_preferences/shared_preferences.dart';
class SecureStorageService {
static Future<String?> get(String key) async {
final prefs = await SharedPreferences.getInstance();
return prefs.getString(key);
}
static Future<void> _setImp(SharedPreferences prefs, String key, String value) async {
await prefs.setString(key, value);
}
static Future<void> set(String key, String? value) async {
final prefs = await SharedPreferences.getInstance();
if (value == null) {
return delete(key);
}
return _setImp(prefs, key, value);
}
static Future<void> delete(String key) async {
final prefs = await SharedPreferences.getInstance();
await prefs.remove(key);
}
}

View File

@@ -0,0 +1,32 @@
class Services {
static const String account = 'accounts';
static const String authorization = 'authorization';
static const String comments = 'comments';
static const String device = 'device';
static const String invitations = 'invitations';
static const String organization = 'organizations';
static const String permission = 'permissions';
static const String project = 'projects';
static const String pgroup = 'priority_groups';
static const String priorities = 'priorities';
static const String reactions = 'reactions';
static const String storage = 'storage';
static const String taskStatus = 'statuses';
static const String tasks = 'tasks';
static const String amplitude = 'amplitude';
static const String automations = 'automation';
static const String changes = 'changes';
static const String clients = 'clients';
static const String invoices = 'invoices';
static const String logo = 'logo';
static const String notifications = 'notifications';
static const String policies = 'policies';
static const String properties = 'properties';
static const String refreshTokens = 'refresh_tokens';
static const String roles = 'roles';
static const String steps = 'steps';
static const String teams = 'teams';
static const String workflows = 'workflows';
static const String workspaces = 'workspaces';
}

View File

@@ -0,0 +1,66 @@
import 'package:logging/logging.dart';
import 'package:pshared/service/authorization/service.dart';
class BasicService<T> {
final String _objectType;
final Logger _logger;
final List<T> Function(Map<String, dynamic> json) fromJson;
Logger get logger => _logger;
BasicService({
required String objectType,
required this.fromJson,
}) : _objectType = objectType, _logger = Logger('service.$objectType');
Future<List<T>> list(String organizationRef, String parentRef) async {
_logger.fine('Loading all objects');
return _getObjects(
AuthorizationService.getGETResponse(_objectType, '/list/$organizationRef/$parentRef'),
);
}
Future<T> get(String objectRef) async {
_logger.fine('Loading object $objectRef');
final objects = await _getObjects(
AuthorizationService.getGETResponse(_objectType, '/$objectRef'),
);
return objects.first;
}
Future<List<T>> create(String organizationRef, Map<String, dynamic> request) async {
_logger.fine('Creating new object...');
return _getObjects(
AuthorizationService.getPOSTResponse(_objectType, '/$organizationRef', request),
);
}
Future<List<T>> update(Map<String, dynamic> request) async {
_logger.fine('Patching object...');
return _getObjects(
AuthorizationService.getPUTResponse(_objectType, '/', request,
),
);
}
Future<List<T>> delete(String objecRef) async {
_logger.fine('Deleting object $objecRef');
return _getObjects(
AuthorizationService.getDELETEResponse(_objectType, '/$objecRef', {}),
);
}
Future<List<T>> _getObjects(Future<Map<String, dynamic>> future) async {
try {
final responseJson = await future;
final objects = fromJson(responseJson);
_logger.fine('Fetched ${objects.length} object(s)');
return objects;
} catch (e, stackTrace) {
_logger.severe('Failed to fetch objects', e, stackTrace);
rethrow;
}
}
}