From 5c064ca555dfd5a153bc7da8cd5e2b62c5f6e180 Mon Sep 17 00:00:00 2001 From: Jerzy Czarkowski Date: Fri, 5 Jun 2026 14:03:46 +0200 Subject: [PATCH] refactor: error descriptions --- src/api/ApiResolver.ts | 2 +- src/api/AppException.ts | 2 +- src/api/BaseApi.ts | 2 +- src/api/JsonRpcServer.ts | 4 +- src/api/ManagementBaseApi.ts | 8 +-- src/api/main/stream/StreamApi.ts | 24 +++---- src/api/main/user/UserApi.ts | 12 ++-- src/api/plain/context/ManagementContextApi.ts | 10 +-- src/api/plain/manager/ManagerApi.ts | 10 +-- .../plain/solution/ManagementSolutionApi.ts | 4 +- src/api/session/SessionService.ts | 2 +- src/service/auth/ApiKeyService.ts | 18 ++--- src/service/auth/AuthService.ts | 22 +++--- src/service/auth/AuthorizationDetector.ts | 4 +- src/service/cloud/CloudAccessValidator.ts | 12 ++-- src/service/cloud/CloudAclChecker.ts | 4 +- src/service/cloud/CloudKeyService.ts | 2 +- src/service/cloud/ContextService.ts | 42 ++++++------ src/service/cloud/ContextUserRepository.ts | 2 +- src/service/cloud/InboxService.ts | 58 ++++++++-------- src/service/cloud/JanusContextFactory.ts | 2 +- src/service/cloud/JanusRoomsWatcher.ts | 2 +- src/service/cloud/KvdbService.ts | 36 +++++----- src/service/cloud/ResourceService.ts | 14 ++-- src/service/cloud/SolutionService.ts | 2 +- src/service/cloud/StoreService.ts | 68 +++++++++---------- src/service/cloud/StreamService.ts | 38 +++++------ src/service/cloud/ThreadService.ts | 48 ++++++------- src/service/login/EcdheLoginService.ts | 4 +- src/service/login/KeyLoginService.ts | 16 ++--- src/service/login/SessionLoginService.ts | 8 +-- src/service/login/SrpLoginService.ts | 18 ++--- src/service/misc/NonceService.ts | 8 +-- src/service/request/RequestRepository.ts | 8 +-- src/service/ws/WebSocketConnectionManager.ts | 8 +-- src/service/ws/WebSocketInnerManager.ts | 6 +- 36 files changed, 265 insertions(+), 265 deletions(-) diff --git a/src/api/ApiResolver.ts b/src/api/ApiResolver.ts index 7ab1331..a014e04 100644 --- a/src/api/ApiResolver.ts +++ b/src/api/ApiResolver.ts @@ -37,7 +37,7 @@ export class ApiResolver { async execute(ctx: Context, method: string, params: unknown): Promise { const methodEntry = this.methods.get(method); if (!methodEntry) { - throw new AppException("METHOD_NOT_FOUND"); + throw new AppException("METHOD_NOT_FOUND", `Method '${method}' not found`); } const api = methodEntry.factory(ctx); return await api.execute(methodEntry.method, params); diff --git a/src/api/AppException.ts b/src/api/AppException.ts index 4665d41..d4a02b9 100644 --- a/src/api/AppException.ts +++ b/src/api/AppException.ts @@ -183,7 +183,7 @@ export class AppException extends Error { if (ex instanceof AppException) { throw ex; } - throw new AppException("INTERNAL_ERROR"); + throw new AppException("INTERNAL_ERROR", "An unexpected internal error occurred"); } static is(e: any, errorName: ErrorCode): e is AppException { diff --git a/src/api/BaseApi.ts b/src/api/BaseApi.ts index f9b1f18..4a87af1 100644 --- a/src/api/BaseApi.ts +++ b/src/api/BaseApi.ts @@ -23,7 +23,7 @@ export class BaseApi { async execute(method: string, params: any): Promise { const m = (this as any)[method]; if (!ApiMethod.getExportedMethod(this.constructor, method) || typeof(m) != "function") { - throw new AppException("METHOD_NOT_FOUND"); + throw new AppException("METHOD_NOT_FOUND", `Method '${method}' not found`); } await this.validateAccess(method, params); this.validateParams(method, params); diff --git a/src/api/JsonRpcServer.ts b/src/api/JsonRpcServer.ts index 0c0ee44..bab03e8 100644 --- a/src/api/JsonRpcServer.ts +++ b/src/api/JsonRpcServer.ts @@ -129,13 +129,13 @@ export class JsonRpcServer { return JSON.parse(typeof(body) === "string" ? body : body.toString("utf8")); } catch { - throw new AppException("PARSE_ERROR"); + throw new AppException("PARSE_ERROR", "Failed to parse JSON request body"); } } private async process(jRpc: any) { if (!this.isJsonRpcRequest(jRpc)) { - throw new AppException("PARSE_ERROR"); + throw new AppException("PARSE_ERROR", "Request is not a valid JSON-RPC object"); } this.id = jRpc.id; this.reportData(jRpc); diff --git a/src/api/ManagementBaseApi.ts b/src/api/ManagementBaseApi.ts index 19cc062..faa977c 100644 --- a/src/api/ManagementBaseApi.ts +++ b/src/api/ManagementBaseApi.ts @@ -30,7 +30,7 @@ export class ManagementBaseApi extends BaseApi { async validateAccess() { await this.authorizationDetector.authorizeByRequest(); if (!this.authorizationHolder.isAuthorized()) { - throw new AppException("UNAUTHORIZED"); + throw new AppException("UNAUTHORIZED", "No valid API key or access token was provided"); } } @@ -42,7 +42,7 @@ export class ManagementBaseApi extends BaseApi { const solutions: types.cloud.SolutionId[] = []; const auth = this.authorizationHolder.getAuth(); if (!auth) { - throw new AppException("INSUFFICIENT_SCOPE"); + throw new AppException("INSUFFICIENT_SCOPE", "Authorization token is missing or invalid"); } const scopes = auth.session ? auth.session.scopes : auth.apiKey.scopes; for (const scope of scopes) { @@ -51,7 +51,7 @@ export class ManagementBaseApi extends BaseApi { } } if (solutions.length === 0) { - throw new AppException("ACCESS_DENIED"); + throw new AppException("ACCESS_DENIED", "No solutions found in authorization scope"); } return solutions; } @@ -59,7 +59,7 @@ export class ManagementBaseApi extends BaseApi { protected validateScope(scope: string) { const auth = this.authorizationHolder.getAuth(); if (!auth) { - throw new AppException("UNAUTHORIZED"); + throw new AppException("UNAUTHORIZED", "No valid API key or access token was provided"); } const scopes = auth.session ? auth.session.scopes : auth.apiKey.scopes; if (!scopes.includes(scope as types.auth.Scope)) { diff --git a/src/api/main/stream/StreamApi.ts b/src/api/main/stream/StreamApi.ts index 04454aa..5830b82 100644 --- a/src/api/main/stream/StreamApi.ts +++ b/src/api/main/stream/StreamApi.ts @@ -104,7 +104,7 @@ export class StreamApi extends BaseApi implements streamApi.IStreamApi { @ApiMethod({}) async streamPublish(model: streamApi.StreamPublishModel): Promise { if (!this.websocket) { - throw new AppException("METHOD_CALLABLE_WITH_WEBSOCKET_ONLY"); + throw new AppException("METHOD_CALLABLE_WITH_WEBSOCKET_ONLY", "This method requires usage of Websocket only request"); } const cloudUser = this.sessionService.validateContextSessionAndGetCloudUser(); const wsId = this.sessionService.getSessionUser().getWsId(); @@ -115,7 +115,7 @@ export class StreamApi extends BaseApi implements streamApi.IStreamApi { @ApiMethod({}) async streamUpdate(model: streamApi.StreamUpdateModel): Promise { if (!this.websocket) { - throw new AppException("METHOD_CALLABLE_WITH_WEBSOCKET_ONLY"); + throw new AppException("METHOD_CALLABLE_WITH_WEBSOCKET_ONLY", "This method requires usage of Websocket only request"); } const cloudUser = this.sessionService.validateContextSessionAndGetCloudUser(); const wsId = this.sessionService.getSessionUser().getWsId(); @@ -126,7 +126,7 @@ export class StreamApi extends BaseApi implements streamApi.IStreamApi { @ApiMethod({}) async streamsSubscribeToRemote(model: streamApi.StreamsSubscribeModel): Promise { if (!this.websocket) { - throw new AppException("METHOD_CALLABLE_WITH_WEBSOCKET_ONLY"); + throw new AppException("METHOD_CALLABLE_WITH_WEBSOCKET_ONLY", "This method requires usage of Websocket only request"); } const cloudUser = this.sessionService.validateContextSessionAndGetCloudUser(); const wsId = this.sessionService.getSessionUser().getWsId(); @@ -137,7 +137,7 @@ export class StreamApi extends BaseApi implements streamApi.IStreamApi { @ApiMethod({}) async streamsModifyRemoteSubscriptions(model: streamApi.StreamModifySubscriptionModel): Promise { if (!this.websocket) { - throw new AppException("METHOD_CALLABLE_WITH_WEBSOCKET_ONLY"); + throw new AppException("METHOD_CALLABLE_WITH_WEBSOCKET_ONLY", "This method requires usage of Websocket only request"); } const cloudUser = this.sessionService.validateContextSessionAndGetCloudUser(); const wsId = this.sessionService.getSessionUser().getWsId(); @@ -148,7 +148,7 @@ export class StreamApi extends BaseApi implements streamApi.IStreamApi { @ApiMethod({}) async streamsUnsubscribeFromRemote(model: streamApi.StreamsUnsubscribeModel): Promise { if (!this.websocket) { - throw new AppException("METHOD_CALLABLE_WITH_WEBSOCKET_ONLY"); + throw new AppException("METHOD_CALLABLE_WITH_WEBSOCKET_ONLY", "This method requires usage of Websocket only request"); } const cloudUser = this.sessionService.validateContextSessionAndGetCloudUser(); const wsId = this.sessionService.getSessionUser().getWsId(); @@ -159,7 +159,7 @@ export class StreamApi extends BaseApi implements streamApi.IStreamApi { @ApiMethod({}) async streamTrickle(model: streamApi.StreamTrickleModel): Promise { if (!this.websocket) { - throw new AppException("METHOD_CALLABLE_WITH_WEBSOCKET_ONLY"); + throw new AppException("METHOD_CALLABLE_WITH_WEBSOCKET_ONLY", "This method requires usage of Websocket only request"); } const cloudUser = this.sessionService.validateContextSessionAndGetCloudUser(); const wsId = this.sessionService.getSessionUser().getWsId(); @@ -170,7 +170,7 @@ export class StreamApi extends BaseApi implements streamApi.IStreamApi { @ApiMethod({}) async streamAcceptOffer(model: streamApi.StreamAcceptOfferModel): Promise { if (!this.websocket) { - throw new AppException("METHOD_CALLABLE_WITH_WEBSOCKET_ONLY"); + throw new AppException("METHOD_CALLABLE_WITH_WEBSOCKET_ONLY", "This method requires usage of Websocket only request"); } const cloudUser = this.sessionService.validateContextSessionAndGetCloudUser(); const wsId = this.sessionService.getSessionUser().getWsId(); @@ -181,7 +181,7 @@ export class StreamApi extends BaseApi implements streamApi.IStreamApi { @ApiMethod({}) async streamSetNewOffer(model: streamApi.StreamSetNewOfferModel): Promise { if (!this.websocket) { - throw new AppException("METHOD_CALLABLE_WITH_WEBSOCKET_ONLY"); + throw new AppException("METHOD_CALLABLE_WITH_WEBSOCKET_ONLY", "This method requires usage of Websocket only request"); } const cloudUser = this.sessionService.validateContextSessionAndGetCloudUser(); const wsId = this.sessionService.getSessionUser().getWsId(); @@ -198,7 +198,7 @@ export class StreamApi extends BaseApi implements streamApi.IStreamApi { @ApiMethod({}) async streamUnpublish(model: streamApi.StreamUnpublishModel): Promise { if (!this.websocket) { - throw new AppException("METHOD_CALLABLE_WITH_WEBSOCKET_ONLY"); + throw new AppException("METHOD_CALLABLE_WITH_WEBSOCKET_ONLY", "This method requires usage of Websocket only request"); } const cloudUser = this.sessionService.validateContextSessionAndGetCloudUser(); const wsId = this.sessionService.getSessionUser().getWsId(); @@ -217,7 +217,7 @@ export class StreamApi extends BaseApi implements streamApi.IStreamApi { @ApiMethod({}) async streamRoomJoin(model: streamApi.StreamRoomJoinModel): Promise { if (!this.websocket) { - throw new AppException("METHOD_CALLABLE_WITH_WEBSOCKET_ONLY"); + throw new AppException("METHOD_CALLABLE_WITH_WEBSOCKET_ONLY", "This method requires usage of Websocket only request"); } const cloudUser = this.sessionService.validateContextSessionAndGetCloudUser(); const wsId = this.sessionService.getSessionUser().getWsId(); @@ -228,7 +228,7 @@ export class StreamApi extends BaseApi implements streamApi.IStreamApi { @ApiMethod({}) async streamRoomLeave(model: streamApi.StreamRoomJoinModel): Promise { if (!this.websocket) { - throw new AppException("METHOD_CALLABLE_WITH_WEBSOCKET_ONLY"); + throw new AppException("METHOD_CALLABLE_WITH_WEBSOCKET_ONLY", "This method requires usage of Websocket only request"); } const cloudUser = this.sessionService.validateContextSessionAndGetCloudUser(); const wsId = this.sessionService.getSessionUser().getWsId(); @@ -239,7 +239,7 @@ export class StreamApi extends BaseApi implements streamApi.IStreamApi { @ApiMethod({}) async streamRoomEnableRecording(model: streamApi.StreamRoomRecordingModel): Promise { if (!this.websocket) { - throw new AppException("METHOD_CALLABLE_WITH_WEBSOCKET_ONLY"); + throw new AppException("METHOD_CALLABLE_WITH_WEBSOCKET_ONLY", "This method requires usage of Websocket only request"); } const cloudUser = this.sessionService.validateContextSessionAndGetCloudUser(); const wsId = this.sessionService.getSessionUser().getWsId(); diff --git a/src/api/main/user/UserApi.ts b/src/api/main/user/UserApi.ts index 69919ba..74fe4d4 100644 --- a/src/api/main/user/UserApi.ts +++ b/src/api/main/user/UserApi.ts @@ -49,7 +49,7 @@ export class UserApi extends BaseApi implements userApi.IUserApi { async authorizeWebSocket(model: {key: types.core.Base64, addWsChannelId: boolean}): Promise<{wsChannelId: types.core.WsChannelId}> { this.sessionService.assertMethod(Permission.HAS_ANY_SESSION); if (!this.webSocket) { - throw new AppException("WEBSOCKET_REQUIRED"); + throw new AppException("WEBSOCKET_REQUIRED", "This method requires a WebSocket connection"); } const wsChannelId = await this.webSocketConnectionManager.authorizeWebSocket( this.getSession(), this.webSocket as WebSocketEx, !!model.addWsChannelId, model.key); @@ -60,7 +60,7 @@ export class UserApi extends BaseApi implements userApi.IUserApi { async unauthorizeWebSocket(): Promise { this.sessionService.assertMethod(Permission.HAS_ANY_SESSION); if (!this.webSocket) { - throw new AppException("WEBSOCKET_REQUIRED"); + throw new AppException("WEBSOCKET_REQUIRED", "This method requires a WebSocket connection"); } await this.webSocketConnectionManager.unauthorizeWebSocket(this.getSession(), this.webSocket as WebSocketEx); return "OK"; @@ -70,7 +70,7 @@ export class UserApi extends BaseApi implements userApi.IUserApi { async subscribeToChannel(model: {channel: types.core.WsChannelName}): Promise { this.sessionService.assertMethod(Permission.HAS_ANY_SESSION); if (!this.webSocket) { - throw new AppException("WEBSOCKET_REQUIRED"); + throw new AppException("WEBSOCKET_REQUIRED", "This method requires a WebSocket connection"); } const subscriptionId = await this.webSocketConnectionManager.subscribeToChannelOld(this.getSession(), this.webSocket as WebSocketEx, model.channel); return {subscriptionId}; @@ -80,7 +80,7 @@ export class UserApi extends BaseApi implements userApi.IUserApi { async unsubscribeFromChannel(model: {channel: types.core.WsChannelName}): Promise { this.sessionService.assertMethod(Permission.HAS_ANY_SESSION); if (!this.webSocket) { - throw new AppException("WEBSOCKET_REQUIRED"); + throw new AppException("WEBSOCKET_REQUIRED", "This method requires a WebSocket connection"); } await this.webSocketConnectionManager.unsubscribeFromChannelOld(this.getSession(), this.webSocket as WebSocketEx, model.channel); return "OK"; @@ -90,7 +90,7 @@ export class UserApi extends BaseApi implements userApi.IUserApi { async subscribeToChannels(model: userApi.SubscribeToChannelsModel): Promise { this.sessionService.assertMethod(Permission.HAS_ANY_SESSION); if (!this.webSocket) { - throw new AppException("WEBSOCKET_REQUIRED"); + throw new AppException("WEBSOCKET_REQUIRED", "This method requires a WebSocket connection"); } const subscriptionsIds = await this.webSocketConnectionManager.subscribeToChannels(this.getSession(), this.webSocket as WebSocketEx, model.channels); return {subscriptions: subscriptionsIds}; @@ -100,7 +100,7 @@ export class UserApi extends BaseApi implements userApi.IUserApi { async unsubscribeFromChannels(model: userApi.UnsubscribeFromChannelsModel): Promise { this.sessionService.assertMethod(Permission.HAS_ANY_SESSION); if (!this.webSocket) { - throw new AppException("WEBSOCKET_REQUIRED"); + throw new AppException("WEBSOCKET_REQUIRED", "This method requires a WebSocket connection"); } await this.webSocketConnectionManager.unsubscribeFromChannels(this.getSession(), this.webSocket as WebSocketEx, model.subscriptionsIds); return "OK"; diff --git a/src/api/plain/context/ManagementContextApi.ts b/src/api/plain/context/ManagementContextApi.ts index 51b9848..259333a 100644 --- a/src/api/plain/context/ManagementContextApi.ts +++ b/src/api/plain/context/ManagementContextApi.ts @@ -34,7 +34,7 @@ export class ManagementContextApi extends BaseApi implements managementContextAp async validateAccess() { await this.authorizationDetector.authorize(); if (!this.authorizationHolder.isAuthorized()) { - throw new AppException("UNAUTHORIZED"); + throw new AppException("UNAUTHORIZED", "No valid API key or access token was provided"); } } @@ -179,7 +179,7 @@ export class ManagementContextApi extends BaseApi implements managementContextAp return; } if (!solList.every(x => solutions.includes(x))) { - throw new AppException("ACCESS_DENIED"); + throw new AppException("ACCESS_DENIED", "User does not have access to one or more of the requested solutions"); } } @@ -191,7 +191,7 @@ export class ManagementContextApi extends BaseApi implements managementContextAp } const context = await this.contextService.getContextWithCheckingExistance(contextId); if (!solutions.includes(context.solution) && !context.shares.find(x => solutions.includes(x))) { - throw new AppException("ACCESS_DENIED"); + throw new AppException("ACCESS_DENIED", "User does not have access to this context's solution"); } } @@ -199,7 +199,7 @@ export class ManagementContextApi extends BaseApi implements managementContextAp const solutions: types.cloud.SolutionId[] = []; const auth = this.authorizationHolder.getAuth(); if (!auth) { - throw new AppException("INSUFFICIENT_SCOPE"); + throw new AppException("INSUFFICIENT_SCOPE", "Authorization token is missing or invalid"); } const scopes = auth.session ? auth.session.scopes : auth.apiKey.scopes; for (const scope of scopes) { @@ -213,7 +213,7 @@ export class ManagementContextApi extends BaseApi implements managementContextAp private validateScope(scope: string) { const auth = this.authorizationHolder.getAuth(); if (!auth) { - throw new AppException("UNAUTHORIZED"); + throw new AppException("UNAUTHORIZED", "No valid API key or access token was provided"); } const scopes = auth.session ? auth.session.scopes : auth.apiKey.scopes; if (!scopes.includes(scope as types.auth.Scope)) { diff --git a/src/api/plain/manager/ManagerApi.ts b/src/api/plain/manager/ManagerApi.ts index 357d6fb..9bfdc8a 100644 --- a/src/api/plain/manager/ManagerApi.ts +++ b/src/api/plain/manager/ManagerApi.ts @@ -38,7 +38,7 @@ export class ManagerApi extends BaseApi implements managerApi.IManagerApi { async validateAccess(method: string) { await this.authorizationDetector.authorize(); if (method !== "auth" && method !== "bindAccessToken" && method !== "createFirstApiKey" && !this.authorizationHolder.isAuthorized()) { - throw new AppException("UNAUTHORIZED"); + throw new AppException("UNAUTHORIZED", "No valid API key or access token was provided"); } } @@ -110,7 +110,7 @@ export class ManagerApi extends BaseApi implements managerApi.IManagerApi { @ApiMethod({errorCodes: ["METHOD_CALLABLE_WITH_WEBSOCKET_ONLY"]}) async subscribeToChannel(model: managerApi.SubscribeToChannelModel): Promise { if (!this.webSocketEx || !this.webSocketEx.ex.plainUserInfo) { - throw new AppException("METHOD_CALLABLE_WITH_WEBSOCKET_ONLY"); + throw new AppException("METHOD_CALLABLE_WITH_WEBSOCKET_ONLY", "This method requires an active WebSocket connection with a plain user"); } for (const channel of model.channels) { this.validateScope(channel); @@ -131,7 +131,7 @@ export class ManagerApi extends BaseApi implements managerApi.IManagerApi { @ApiMethod({errorCodes: ["METHOD_CALLABLE_WITH_WEBSOCKET_ONLY"]}) async unsubscribeFromChannel(model: managerApi.UnsubscribeFromChannelModel): Promise { if (!this.webSocketEx || !this.webSocketEx.ex.plainUserInfo) { - throw new AppException("METHOD_CALLABLE_WITH_WEBSOCKET_ONLY"); + throw new AppException("METHOD_CALLABLE_WITH_WEBSOCKET_ONLY", "This method requires an active WebSocket connection with a plain user"); } for (const channel of model.channels) { this.validateScope(channel); @@ -153,7 +153,7 @@ export class ManagerApi extends BaseApi implements managerApi.IManagerApi { private validateScope(scope: string) { const auth = this.authorizationHolder.getAuth(); if (!auth) { - throw new AppException("UNAUTHORIZED"); + throw new AppException("UNAUTHORIZED", "Authorization required"); } const scopes = auth.session ? auth.session.scopes : auth.apiKey.scopes; if (!scopes.includes(scope as types.auth.Scope)) { @@ -165,7 +165,7 @@ export class ManagerApi extends BaseApi implements managerApi.IManagerApi { const solutions: types.cloud.SolutionId[] = []; const auth = this.authorizationHolder.getAuth(); if (!auth) { - throw new AppException("INSUFFICIENT_SCOPE"); + throw new AppException("INSUFFICIENT_SCOPE", "Authorization token is missing or invalid"); } const scopes = auth.session ? auth.session.scopes : auth.apiKey.scopes; for (const scope of scopes) { diff --git a/src/api/plain/solution/ManagementSolutionApi.ts b/src/api/plain/solution/ManagementSolutionApi.ts index 63048d4..8d4eaaa 100644 --- a/src/api/plain/solution/ManagementSolutionApi.ts +++ b/src/api/plain/solution/ManagementSolutionApi.ts @@ -34,7 +34,7 @@ export class ManagementSolutionApi extends BaseApi implements managementSolution async validateAccess() { await this.authorizationDetector.authorize(); if (!this.authorizationHolder.isAuthorized()) { - throw new AppException("UNAUTHORIZED"); + throw new AppException("UNAUTHORIZED", "No valid API key or access token was provided"); } } @@ -76,7 +76,7 @@ export class ManagementSolutionApi extends BaseApi implements managementSolution private validateScope(scope: string) { const auth = this.authorizationHolder.getAuth(); if (!auth) { - throw new AppException("UNAUTHORIZED"); + throw new AppException("UNAUTHORIZED", "No valid API key or access token was provided"); } const scopes = auth.session ? auth.session.scopes : auth.apiKey.scopes; if (!scopes.includes(scope as types.auth.Scope)) { diff --git a/src/api/session/SessionService.ts b/src/api/session/SessionService.ts index 4f4fc04..be5b2e5 100644 --- a/src/api/session/SessionService.ts +++ b/src/api/session/SessionService.ts @@ -171,7 +171,7 @@ export class SessionService { const session = this.getSession(); const ecdhe = session ? session.get("ecdhe") : null; if (!session || session.get("type") !== "ecdhe" || !ecdhe || !ecdhe.contextUser) { - throw new AppException("ACCESS_DENIED"); + throw new AppException("ACCESS_DENIED", "A valid ECDHE context session is required"); } return new CloudUser(ecdhe.pub, session.get("solution")); } diff --git a/src/service/auth/ApiKeyService.ts b/src/service/auth/ApiKeyService.ts index d7dac96..de0ab56 100644 --- a/src/service/auth/ApiKeyService.ts +++ b/src/service/auth/ApiKeyService.ts @@ -30,10 +30,10 @@ export class ApiKeyService { return this.lockHelper.withLock("first-api-key-creation", async () => { const apiKeyCount = await this.repositoryFactory.createApiKeyRepository().getApiKeyCount(); if (apiKeyCount !== 0) { - throw new AppException("FIRST_API_KEY_ALREADY_EXISTS"); + throw new AppException("FIRST_API_KEY_ALREADY_EXISTS", "An API key already exists; initialization can only create the first key"); } if (!this.config.server.initializationToken || initializationToken !== this.config.server.initializationToken) { - throw new AppException("INITIALIZATION_TOKEN_MISSMATCH"); + throw new AppException("INITIALIZATION_TOKEN_MISSMATCH", "Initialization token does not match"); } const user = await this.repositoryFactory.createApiUserRepository().create(); return await this.repositoryFactory.createApiKeyRepository().create(user.id, name as types.auth.ApiKeyName, [ @@ -45,7 +45,7 @@ export class ApiKeyService { async createApiKey(userId: types.auth.ApiUserId, name: types.auth.ApiKeyName, scope: types.auth.Scope[], publicKey: types.core.EccPubKeyPEM|undefined) { const apiKeys = await this.repositoryFactory.createApiKeyRepository().listForUser(userId); if (apiKeys.length > 10) { - throw new AppException("API_KEYS_LIMIT_EXCEEDED"); + throw new AppException("API_KEYS_LIMIT_EXCEEDED", "Maximum number of API keys exceeded"); } AuthoriationUtils.parseScope(scope, "disabled"); return this.repositoryFactory.createApiKeyRepository().create(userId, name, scope, false, publicKey); @@ -54,10 +54,10 @@ export class ApiKeyService { async updateApiKey(userId: types.auth.ApiUserId, model: managerApi.UpdateApiKeyModel) { const apiKey = await this.repositoryFactory.createApiKeyRepository().get(model.id); if (!apiKey) { - throw new AppException("API_KEY_DOES_NOT_EXIST"); + throw new AppException("API_KEY_DOES_NOT_EXIST", "API key does not exist"); } if (apiKey.user !== userId) { - throw new AppException("ACCESS_DENIED"); + throw new AppException("ACCESS_DENIED", "API key belongs to a different user"); } if (model.scope) { AuthoriationUtils.parseScope(model.scope, "disabled"); @@ -68,10 +68,10 @@ export class ApiKeyService { async deleteApiKey(userId: types.auth.ApiUserId, apiKeyId: types.auth.ApiKeyId) { const apiKey = await this.repositoryFactory.createApiKeyRepository().get(apiKeyId); if (!apiKey) { - throw new AppException("API_KEY_DOES_NOT_EXIST"); + throw new AppException("API_KEY_DOES_NOT_EXIST", "API key does not exist"); } if (apiKey.user !== userId) { - throw new AppException("ACCESS_DENIED"); + throw new AppException("ACCESS_DENIED", "API key belongs to a different user"); } await this.repositoryFactory.createApiKeyRepository().delete(apiKeyId); } @@ -83,10 +83,10 @@ export class ApiKeyService { async getApiKey(userId: types.auth.ApiUserId, apiKeyId: types.auth.ApiKeyId) { const apiKey = await this.repositoryFactory.createApiKeyRepository().get(apiKeyId); if (!apiKey) { - throw new AppException("API_KEY_DOES_NOT_EXIST"); + throw new AppException("API_KEY_DOES_NOT_EXIST", "API key does not exist"); } if (apiKey.user !== userId) { - throw new AppException("ACCESS_DENIED"); + throw new AppException("ACCESS_DENIED", "API key belongs to a different user"); } return apiKey; } diff --git a/src/service/auth/AuthService.ts b/src/service/auth/AuthService.ts index 936ed4c..8fc9c7c 100644 --- a/src/service/auth/AuthService.ts +++ b/src/service/auth/AuthService.ts @@ -37,14 +37,14 @@ export class AuthService { async authByApiKeyCredentials(apiKeyId: types.auth.ApiKeyId, apiKeySecret: types.auth.ApiKeySecret, scope: types.auth.Scope[]|undefined): Promise { const apiKey = await this.repositoryFactory.createApiKeyRepository().get(apiKeyId); if (!apiKey || !apiKey.enabled) { - throw new AppException("API_KEY_DOES_NOT_EXIST"); + throw new AppException("API_KEY_DOES_NOT_EXIST", "API key not found"); } const user = await this.repositoryFactory.createApiUserRepository().get(apiKey.user); if (!user || !user.enabled) { - throw new AppException("API_KEY_DOES_NOT_EXIST"); + throw new AppException("API_KEY_DOES_NOT_EXIST", "API key owner account not found"); } if (apiKey.secret !== apiKeySecret) { - throw new AppException("INVALID_CREDENTIALS"); + throw new AppException("INVALID_CREDENTIALS", "Invalid API key secret"); } const key = await this.tokenEncryptionKeyProvider.getCurrentKey(); const parsedScope = this.prepareScope(apiKey, scope, key.refreshTokenTTL); @@ -55,7 +55,7 @@ export class AuthService { async authByRefreshToken(refreshToken: types.auth.ApiRefreshToken): Promise { const tokenData = await this.tokenEncryptionService.decryptToken(refreshToken); if (!tokenData || tokenData.type !== "refreshToken") { - throw new AppException("INVALID_TOKEN"); + throw new AppException("INVALID_TOKEN", "Token is invalid or not a refresh token"); } const session = await (async () => { if (tokenData.connectionId) { @@ -64,18 +64,18 @@ export class AuthService { return this.repositoryFactory.createTokenSessionRepository().get(tokenData.sessionId); })(); if (!session || session.expiry < DateUtils.now()) { - throw new AppException("INVALID_TOKEN"); + throw new AppException("INVALID_TOKEN", "Session not found or expired"); } if (session.seq !== tokenData.seq) { - throw new AppException("INVALID_TOKEN"); + throw new AppException("INVALID_TOKEN", "Token sequence number does not match"); } const apiKey = await this.repositoryFactory.createApiKeyRepository().get(session.apiKey); if (!apiKey || !apiKey.enabled || apiKey.user !== session.user) { - throw new AppException("INVALID_TOKEN"); + throw new AppException("INVALID_TOKEN", "API key not found, or session mismatch"); } const user = await this.repositoryFactory.createApiUserRepository().get(session.user); if (!user || !user.enabled) { - throw new AppException("INVALID_TOKEN"); + throw new AppException("INVALID_TOKEN", "API key owner account not found or disabled"); } const key = await this.tokenEncryptionKeyProvider.getCurrentKey(); if (session.id === "websocket") { @@ -90,14 +90,14 @@ export class AuthService { async authByApiKeySignature(apiKeyId: types.auth.ApiKeyId, scope: types.auth.Scope[]|undefined, timestamp: types.core.Timestamp, nonce: string, signature: types.core.Base64, data: string) { const apiKey = await this.repositoryFactory.createApiKeyRepository().get(apiKeyId); if (!apiKey || !apiKey.enabled) { - throw new AppException("API_KEY_DOES_NOT_EXIST"); + throw new AppException("API_KEY_DOES_NOT_EXIST", "API key not found"); } const user = await this.repositoryFactory.createApiUserRepository().get(apiKey.user); if (!user || !user.enabled) { - throw new AppException("API_KEY_DOES_NOT_EXIST"); + throw new AppException("API_KEY_DOES_NOT_EXIST", "API key owner account not found"); } if (!await this.isValidClientSignature(apiKey, timestamp, nonce, signature, data)) { - throw new AppException("INVALID_SIGNATURE"); + throw new AppException("INVALID_SIGNATURE", "Signature verification failed"); } const key = await this.tokenEncryptionKeyProvider.getCurrentKey(); const parsedScope = this.prepareScope(apiKey, scope, key.refreshTokenTTL); diff --git a/src/service/auth/AuthorizationDetector.ts b/src/service/auth/AuthorizationDetector.ts index 6382551..0c895ca 100644 --- a/src/service/auth/AuthorizationDetector.ts +++ b/src/service/auth/AuthorizationDetector.ts @@ -45,11 +45,11 @@ export class AuthorizationDetector { async bindAccessTokenToWebsocket(token: types.auth.ApiAccessToken) { if (!this.webSocket || !this.webSocket.ex.plainUserInfo) { - throw new AppException("METHOD_CALLABLE_WITH_WEBSOCKET_ONLY"); + throw new AppException("METHOD_CALLABLE_WITH_WEBSOCKET_ONLY", "This method requires an active WebSocket connection with a plain user"); } const res = await this.isApiAccessTokenValid(token); if (res === false) { - throw new AppException("INVALID_TOKEN"); + throw new AppException("INVALID_TOKEN", "Access token is invalid or expired"); } this.webSocket.ex.plainUserInfo.token = token; } diff --git a/src/service/cloud/CloudAccessValidator.ts b/src/service/cloud/CloudAccessValidator.ts index 510a52c..cac04e8 100644 --- a/src/service/cloud/CloudAccessValidator.ts +++ b/src/service/cloud/CloudAccessValidator.ts @@ -26,22 +26,22 @@ export class CloudAccessValidator { const context = await this.getContext(contextInfo); if (executor.type === "context") { if (executor.contextId !== context.id) { - throw new AppException("ACCESS_DENIED"); + throw new AppException("ACCESS_DENIED", "Executor context does not match"); } } else if (executor.type === "plain") { if (!executor.solutions.includes(context.solution) && !executor.solutions.includes("*" as types.cloud.SolutionId)) { - throw new AppException("ACCESS_DENIED"); + throw new AppException("ACCESS_DENIED", "Executor is not authorized for this context's solution"); } } else if (executor.type === "cloud") { const user = await this.repositoryFactory.createContextUserRepository().getUserFromContext(executor.pub, context.id); if (!user) { - throw new AppException("ACCESS_DENIED"); + throw new AppException("ACCESS_DENIED", "User is not a member of this context"); } if (executor.solutionId) { if (context.solution !== executor.solutionId && !context.shares.includes(executor.solutionId)) { - throw new AppException("ACCESS_DENIED"); + throw new AppException("ACCESS_DENIED", "Executor solution is not authorized for this context"); } } await onCloudUser(user, context); @@ -54,11 +54,11 @@ export class CloudAccessValidator { const context = await this.repositoryFactory.createContextRepository().get(contextId); const user = await this.repositoryFactory.createContextUserRepository().getUserFromContext(cloudUser.pub, contextId); if (!user || !context) { - throw new AppException("ACCESS_DENIED"); + throw new AppException("ACCESS_DENIED", "User is not a member of this context"); } if (cloudUser.solutionId) { if (context.solution !== cloudUser.solutionId && !context.shares.includes(cloudUser.solutionId)) { - throw new AppException("ACCESS_DENIED"); + throw new AppException("ACCESS_DENIED", "User's solution is not authorized for this context"); } } return {user, context}; diff --git a/src/service/cloud/CloudAclChecker.ts b/src/service/cloud/CloudAclChecker.ts index 8182abf..bc4affa 100644 --- a/src/service/cloud/CloudAclChecker.ts +++ b/src/service/cloud/CloudAclChecker.ts @@ -317,7 +317,7 @@ export class CloudAclChecker { this.validateAcl(acl, 100); } catch { - throw new AppException("INVALID_ACL"); + throw new AppException("INVALID_ACL", "ACL string failed validation"); } } @@ -362,7 +362,7 @@ export class CloudAclChecker { verifyAccess(acl: types.cloud.ContextAcl, fnName: AclFunctionNameX, args: string[]) { if (!this.hasAccess(acl, fnName, args)) { - throw new AppException("ACCESS_DENIED"); + throw new AppException("ACCESS_DENIED", `ACL denied access to '${fnName}'`); } } diff --git a/src/service/cloud/CloudKeyService.ts b/src/service/cloud/CloudKeyService.ts index 004db1c..a2143ad 100644 --- a/src/service/cloud/CloudKeyService.ts +++ b/src/service/cloud/CloudKeyService.ts @@ -64,7 +64,7 @@ export class CloudKeyService { }); for (const insert of inserts) { if (!availableKeyIds.includes(insert.keyId)) { - throw new AppException("INVALID_KEY_ID"); + throw new AppException("INVALID_KEY_ID", "Key ID is not available in this context"); } let userEntry = newKeys.find(x => x.user === insert.user); if (!userEntry) { diff --git a/src/service/cloud/ContextService.ts b/src/service/cloud/ContextService.ts index 08c31e9..c4d7068 100644 --- a/src/service/cloud/ContextService.ts +++ b/src/service/cloud/ContextService.ts @@ -80,7 +80,7 @@ export class ContextService { this.policyService.validateContextPolicy(rest.policy); } if (rest.scope && context.shares.length > 0 && rest.scope === "private") { - throw new AppException("CANNOT_SWITCH_CONNECTED_CONTEXT_TO_PRIVATE"); + throw new AppException("CANNOT_SWITCH_CONNECTED_CONTEXT_TO_PRIVATE", "Context with connected solutions cannot be switched to private scope"); } await this.repositoryFactory.createContextRepository().updateContext(contextId, rest); } @@ -109,7 +109,7 @@ export class ContextService { throw new AppException("SOLUTION_DOES_NOT_EXIST"); } if (context.scope === "private") { - throw new AppException("CANNOT_ASSIGN_PRIVATE_CONTEXT"); + throw new AppException("CANNOT_ASSIGN_PRIVATE_CONTEXT", "Private contexts cannot be shared with other solutions"); } if (context.solution === solutionId) { return; @@ -127,7 +127,7 @@ export class ContextService { throw new AppException("SOLUTION_DOES_NOT_EXIST"); } if (context.solution === solutionId) { - throw new AppException("CANNOT_UNASSIGN_CONTEXT_FROM_ITS_PARENT"); + throw new AppException("CANNOT_UNASSIGN_CONTEXT_FROM_ITS_PARENT", "Cannot remove the context's own parent solution"); } await this.repositoryFactory.createContextRepository().removeSolutionFromContext(contextId, solutionId); } @@ -152,7 +152,7 @@ export class ContextService { } const user = await this.repositoryFactory.createContextUserRepository().get(contextId, userId); if (!user) { - throw new AppException("USER_DOESNT_EXIST"); + throw new AppException("USER_DOESNT_EXIST", "User does not exist in this context"); } await this.repositoryFactory.createContextUserRepository().remove(contextId, userId); void this.contextNotificationService.sendUserRemoved(userId, context.id, user.userPubKey); @@ -168,7 +168,7 @@ export class ContextService { } const users = await this.repositoryFactory.createContextUserRepository().getAllByContextAndUserPubKey(contextId, userPubKey); if (users.length === 0) { - throw new AppException("USER_DOESNT_EXIST"); + throw new AppException("USER_DOESNT_EXIST", "User does not exist in this context"); } await this.repositoryFactory.createContextUserRepository().removeAllByUserPub(contextId, userPubKey); } @@ -177,7 +177,7 @@ export class ContextService { if (listParams.lastId) { const context = await this.repositoryFactory.createContextRepository().get(listParams.lastId as types.context.ContextId); if (!context) { - throw new AppException("NO_MATCH_FOR_LAST_ID"); + throw new AppException("NO_MATCH_FOR_LAST_ID", "No context found for the given pagination cursor"); } } return cloudUser.solutionId ? @@ -196,14 +196,14 @@ export class ContextService { async getContext(cloudUser: CloudUser, contextId: types.context.ContextId) { const user = await this.repositoryFactory.createContextUserRepository().getUserFromContext(cloudUser.pub, contextId); if (!user) { - throw new AppException("ACCESS_DENIED"); + throw new AppException("ACCESS_DENIED", "User is not a member of this context"); } const context = await this.repositoryFactory.createContextRepository().get(contextId); if (!context) { throw new DbInconsistencyError(`Context=${contextId} does not exist, contextUser=${user.id}`); } if (cloudUser.solutionId && context.solution !== cloudUser.solutionId && !context.shares.includes(cloudUser.solutionId)) { - throw new AppException("ACCESS_DENIED"); + throw new AppException("ACCESS_DENIED", "User's solution is not authorized for this context"); } return {context, user}; } @@ -217,7 +217,7 @@ export class ContextService { async getUser(contextId: types.context.ContextId, userId: types.cloud.UserId) { const user = await this.repositoryFactory.createContextUserRepository().get(contextId, userId); if (!user) { - throw new AppException("USER_DOESNT_EXIST"); + throw new AppException("USER_DOESNT_EXIST", "User does not exist in this context"); } return user; } @@ -225,7 +225,7 @@ export class ContextService { async getUserByPub(contextId: types.context.ContextId, userPubKey: types.cloud.UserPubKey) { const user = await this.repositoryFactory.createContextUserRepository().getUserFromContext(userPubKey, contextId); if (!user) { - throw new AppException("USER_DOESNT_EXIST"); + throw new AppException("USER_DOESNT_EXIST", "User does not exist in this context"); } return user; } @@ -238,7 +238,7 @@ export class ContextService { this.cloudAclChecker.checkAcl(acl); const user = await this.repositoryFactory.createContextUserRepository().get(contextId, userId); if (!user) { - throw new AppException("USER_DOESNT_EXIST"); + throw new AppException("USER_DOESNT_EXIST", "User does not exist in this context"); } await this.repositoryFactory.createContextUserRepository().updateAcl(contextId, userId, acl); } @@ -246,7 +246,7 @@ export class ContextService { async getAllContextUsers(cloudUser: CloudUser, contextId: types.context.ContextId) { const user = await this.repositoryFactory.createContextUserRepository().getUserFromContext(cloudUser.pub, contextId); if (!user) { - throw new AppException("ACCESS_DENIED"); + throw new AppException("ACCESS_DENIED", "User is not a member of this context"); } this.cloudAclChecker.verifyAccess(user.acl, "context/contextGetUsers", ["contextId=" + contextId]); const context = await this.repositoryFactory.createContextRepository().get(contextId); @@ -254,10 +254,10 @@ export class ContextService { throw new AppException("CONTEXT_DOES_NOT_EXIST"); } if (!this.policy.canListUsers(context)) { - throw new AppException("ACCESS_DENIED"); + throw new AppException("ACCESS_DENIED", "Policy denied listing context users"); } if (cloudUser.solutionId && context.solution !== cloudUser.solutionId && !context.shares.includes(cloudUser.solutionId)) { - throw new AppException("ACCESS_DENIED"); + throw new AppException("ACCESS_DENIED", "User's solution is not authorized for this context"); } const users = await this.repositoryFactory.createContextUserRepository().getAllContextUsers(contextId); const usersState = await this.activeUsersMap.getUsersState({host: this.host, userPubkeys: users.map(u => u.userPubKey), solutionIds: [context.solution, ...context.shares]}); @@ -267,7 +267,7 @@ export class ContextService { async getPageOfContextUsersWithStatus(cloudUser: CloudUser, contextId: types.context.ContextId, listParams: types.core.ListModel) { const user = await this.repositoryFactory.createContextUserRepository().getUserFromContext(cloudUser.pub, contextId); if (!user) { - throw new AppException("ACCESS_DENIED"); + throw new AppException("ACCESS_DENIED", "User is not a member of this context"); } this.cloudAclChecker.verifyAccess(user.acl, "context/contextListUsers", ["contextId=" + contextId]); const context = await this.repositoryFactory.createContextRepository().get(contextId); @@ -275,10 +275,10 @@ export class ContextService { throw new AppException("CONTEXT_DOES_NOT_EXIST"); } if (!this.policy.canListUsers(context)) { - throw new AppException("ACCESS_DENIED"); + throw new AppException("ACCESS_DENIED", "Policy denied listing context users"); } if (cloudUser.solutionId && context.solution !== cloudUser.solutionId && !context.shares.includes(cloudUser.solutionId)) { - throw new AppException("ACCESS_DENIED"); + throw new AppException("ACCESS_DENIED", "User's solution is not authorized for this context"); } const users = await this.repositoryFactory.createContextUserRepository().getUsersPageWithActivityFromContext(contextId, context.solution, listParams); const usersState = await this.activeUsersMap.getUsersState({host: this.host, userPubkeys: users.list.map(u => u.userPubKey), solutionIds: [context.solution, ...context.shares]}); @@ -288,7 +288,7 @@ export class ContextService { async sendCustomNotification(cloudUser: CloudUser, contextId: types.context.ContextId, data: unknown, customChannelName: types.core.WsChannelName, usersWithEncryptionKey: {id: types.cloud.UserId, key: types.core.UserKeyData}[]) { const user = await this.repositoryFactory.createContextUserRepository().getUserFromContext(cloudUser.pub, contextId); if (!user) { - throw new AppException("ACCESS_DENIED"); + throw new AppException("ACCESS_DENIED", "User is not a member of this context"); } this.cloudAclChecker.verifyAccess(user.acl, "context/contextSendCustomNotification", ["contextId=" + contextId]); const context = await this.repositoryFactory.createContextRepository().get(contextId); @@ -296,10 +296,10 @@ export class ContextService { throw new DbInconsistencyError(`Context=${contextId} does not exist, contextUser=${user.id}`); } if (!this.policy.canSendContextCustomNotification(context)) { - throw new AppException("ACCESS_DENIED"); + throw new AppException("ACCESS_DENIED", "Policy denied context custom notification"); } if (cloudUser.solutionId && context.solution !== cloudUser.solutionId && !context.shares.includes(cloudUser.solutionId)) { - throw new AppException("ACCESS_DENIED"); + throw new AppException("ACCESS_DENIED", "User's solution is not authorized for this context"); } const usersWithPubKey = await this.repositoryFactory.createContextUserRepository().getUsers(contextId, usersWithEncryptionKey.map(e => e.id)); const users = this.mergeUsersArrays(usersWithPubKey, usersWithEncryptionKey); @@ -310,7 +310,7 @@ export class ContextService { return usersWithEncryptionKey.map(user => { const userWithPubKey = usersWithPubKey.find(x => x.userId === user.id); if (!userWithPubKey) { - throw new AppException("USER_DOESNT_EXIST"); + throw new AppException("USER_DOESNT_EXIST", "User does not exist in this context"); } return { id: user.id, diff --git a/src/service/cloud/ContextUserRepository.ts b/src/service/cloud/ContextUserRepository.ts index 61b9e71..e5fb2d7 100644 --- a/src/service/cloud/ContextUserRepository.ts +++ b/src/service/cloud/ContextUserRepository.ts @@ -42,7 +42,7 @@ export class ContextUserRepository { if (oldUser.userId === userId) { return oldUser; } - throw new AppException("PUB_KEY_ALREADY_IN_USE"); + throw new AppException("PUB_KEY_ALREADY_IN_USE", "Public key is already associated with a different user"); } const user: db.context.ContextUser = { id: this.getUserId(contextId, userId), diff --git a/src/service/cloud/InboxService.ts b/src/service/cloud/InboxService.ts index b7f1bc2..ad19556 100644 --- a/src/service/cloud/InboxService.ts +++ b/src/service/cloud/InboxService.ts @@ -74,7 +74,7 @@ export class InboxService extends BaseContainerService { } catch (err) { if (err instanceof DbDuplicateError) { - throw new AppException("DUPLICATE_RESOURCE_ID"); + throw new AppException("DUPLICATE_RESOURCE_ID", "A resource with this ID already exists"); } throw err; } @@ -88,7 +88,7 @@ export class InboxService extends BaseContainerService { const inboxRepository = this.repositoryFactory.createInboxRepository(session); const oldInbox = await inboxRepository.get(model.id); if (!oldInbox) { - throw new AppException("STORE_DOES_NOT_EXIST"); + throw new AppException("STORE_DOES_NOT_EXIST", "Inbox not found"); } const {user, context} = await this.cloudAccessValidator.getUserFromContext(cloudUser, oldInbox.contextId); this.cloudAclChecker.verifyAccess(user.acl, "inbox/inboxUpdate", ["inboxId=" + model.id]); @@ -106,7 +106,7 @@ export class InboxService extends BaseContainerService { } const newKeys = await this.cloudKeyService.checkKeysAndClients(oldInbox.contextId, [...oldInbox.history.map(x => x.keyId), model.keyId], oldInbox.keys, model.keys, model.keyId, model.users, model.managers); if (oldInbox.clientResourceId && model.resourceId && oldInbox.clientResourceId !== model.resourceId) { - throw new AppException("RESOURCE_ID_MISSMATCH"); + throw new AppException("RESOURCE_ID_MISSMATCH", "Resource ID does not match the original"); } try { const inbox = await inboxRepository.updateInbox(oldInbox, user.userId, model.managers, model.users, model.data, model.keyId, newKeys, model.policy, model.resourceId || null); @@ -114,7 +114,7 @@ export class InboxService extends BaseContainerService { } catch (err) { if (err instanceof DbDuplicateError) { - throw new AppException("DUPLICATE_RESOURCE_ID"); + throw new AppException("DUPLICATE_RESOURCE_ID", "A resource with this ID already exists"); } throw err; } @@ -135,7 +135,7 @@ export class InboxService extends BaseContainerService { } const userContext = await this.cloudAccessValidator.checkIfCanExecuteInContext(executor, inbox.contextId, (user, context) => { if (!this.policy.canDeleteContainer(user, context, inbox)) { - throw new AppException("ACCESS_DENIED"); + throw new AppException("ACCESS_DENIED", "Policy denied container deletion"); } this.cloudAclChecker.verifyAccess(user.acl, "inbox/inboxDelete", ["inboxId=" + id]); }); @@ -168,7 +168,7 @@ export class InboxService extends BaseContainerService { const toNotify: db.inbox.Inbox[] = []; for (const inbox of inboxes) { if (inbox.contextId !== contextId) { - throw new AppException("RESOURCES_HAVE_DIFFERENT_CONTEXTS"); + throw new AppException("RESOURCES_HAVE_DIFFERENT_CONTEXTS", "All resources must belong to the same context"); } if (!additionalAccessCheck(inbox)) { resultMap.set(inbox.id, "ACCESS_DENIED"); @@ -211,7 +211,7 @@ export class InboxService extends BaseContainerService { } await this.cloudAccessValidator.checkIfCanExecuteInContext(executor, inbox.contextId, (user, context) => { if (!this.policy.canReadContainer(user, context, inbox)) { - throw new AppException("ACCESS_DENIED"); + throw new AppException("ACCESS_DENIED", "Policy denied read access to this container"); } this.cloudAclChecker.verifyAccess(user.acl, "inbox/inboxGet", ["inboxId=" + inboxId]); }); @@ -240,7 +240,7 @@ export class InboxService extends BaseContainerService { const {user, context} = await this.cloudAccessValidator.getUserFromContext(cloudUser, contextId); this.cloudAclChecker.verifyAccess(user.acl, "inbox/inboxListAll", []); if (!this.policy.canListAllContainers(user, context)) { - throw new AppException("ACCESS_DENIED"); + throw new AppException("ACCESS_DENIED", "Policy denied listing all containers"); } const inboxes = await this.repositoryFactory.createInboxRepository().getAllInboxes(contextId, type, listParams, sortBy); return {user, inboxes}; @@ -251,12 +251,12 @@ export class InboxService extends BaseContainerService { this.cloudAclChecker.verifyAccess(user.acl, "inbox/inboxList", []); if (scope === "ALL") { if (!this.policy.canListAllContainers(user, context)) { - throw new AppException("ACCESS_DENIED"); + throw new AppException("ACCESS_DENIED", "Policy denied listing all containers"); } } else { if (!this.policy.canListMyContainers(user, context)) { - throw new AppException("ACCESS_DENIED"); + throw new AppException("ACCESS_DENIED", "Policy denied listing own containers"); } } const inboxes = await this.repositoryFactory.createInboxRepository().getPageByContextAndUser(contextId, type, user.userId, cloudUser.solutionId, listParams, sortBy, scope); @@ -270,7 +270,7 @@ export class InboxService extends BaseContainerService { } await this.cloudAccessValidator.checkIfCanExecuteInContext(executor, ctx, (user, context) => { if (!this.policy.canListAllContainers(user, context)) { - throw new AppException("ACCESS_DENIED"); + throw new AppException("ACCESS_DENIED", "Policy denied listing all containers"); } this.cloudAclChecker.verifyAccess(user.acl, "inbox/inboxList", []); }); @@ -291,12 +291,12 @@ export class InboxService extends BaseContainerService { throw new AppException("INBOX_DOES_NOT_EXIST"); } if (model.version > inbox.history.length) { - throw new AppException("INVALID_VERSION"); + throw new AppException("INVALID_VERSION", "Inbox version is higher than current"); } const last = inbox.history[inbox.history.length - 1]; const store = await this.repositoryFactory.createStoreRepository().get(last.data.storeId); if (!store) { - throw new AppException("STORE_DOES_NOT_EXIST"); + throw new AppException("STORE_DOES_NOT_EXIST", "Inbox not found"); } const thread = await this.repositoryFactory.createThreadRepository().get(last.data.threadId); if (!thread) { @@ -304,7 +304,7 @@ export class InboxService extends BaseContainerService { } const requestRepository = this.repositoryFactory.createRequestRepository(); if (model.files.length > 0 && !model.requestId) { - throw new AppException("REQUEST_DOES_NOT_EXIST"); + throw new AppException("REQUEST_DOES_NOT_EXIST", "No request ID provided for file uploads"); } const request = model.requestId && model.files.length > 0 ? await requestRepository.getReadyForUser(username, model.requestId) : null; const files = this.checkFilesIndexesAndCountAndSize(last.data.fileConfig, request, model); @@ -332,7 +332,7 @@ export class InboxService extends BaseContainerService { } catch (err) { if (err instanceof DbDuplicateError) { - throw new AppException("DUPLICATE_RESOURCE_ID"); + throw new AppException("DUPLICATE_RESOURCE_ID", "A resource with this ID already exists"); } throw err; } @@ -356,10 +356,10 @@ export class InboxService extends BaseContainerService { const {user, context} = await this.cloudAccessValidator.getUserFromContext(cloudUser, inbox.contextId); this.cloudAclChecker.verifyAccess(user.acl, "inbox/inboxSendCustomNotification", ["inboxId=" + inboxId]); if (!this.policy.canSendCustomNotification(user, context, inbox)) { - throw new AppException("ACCESS_DENIED"); + throw new AppException("ACCESS_DENIED", "Policy denied custom notification"); } if (users && users.some(element => !inbox.users.includes(element))) { - throw new AppException("USER_DOES_NOT_HAVE_ACCESS_TO_CONTAINER"); + throw new AppException("USER_DOES_NOT_HAVE_ACCESS_TO_CONTAINER", "One or more specified users are not members of this inbox"); } this.inboxNotificationService.sendInboxCustomEvent(inbox, keyId, data, {id: user.userId, pub: user.userPubKey}, customChannelName, users); return inbox; @@ -373,26 +373,26 @@ export class InboxService extends BaseContainerService { throw new AppException("THREAD_DOES_NOT_EXIST"); } if (!thread.managers.includes(user.userId)) { - throw new AppException("ACCESS_DENIED"); + throw new AppException("ACCESS_DENIED", "User is not a manager of this thread"); } } private async validateAccessToStore(storeId: types.store.StoreId, user: db.context.ContextUser) { const store = await this.repositoryFactory.createStoreRepository().get(storeId); if (!store) { - throw new AppException("STORE_DOES_NOT_EXIST"); + throw new AppException("STORE_DOES_NOT_EXIST", "Inbox not found"); } if (!store.managers.includes(user.userId)) { - throw new AppException("ACCESS_DENIED"); + throw new AppException("ACCESS_DENIED", "User is not a manager of this store"); } } private checkFilesIndexesAndCountAndSize(fileConfig: types.inbox.InboxFileConfig, request: db.request.Request|null, model: inboxApi.InboxSendModel) { if (model.files.length > fileConfig.maxCount) { - throw new AppException("TOO_MANY_FILES_IN_REQUEST"); + throw new AppException("TOO_MANY_FILES_IN_REQUEST", "Number of files exceeds the maximum allowed"); } if (model.files.length < fileConfig.minCount) { - throw new AppException("NOT_ENOUGH_FILES_IN_REQUEST"); + throw new AppException("NOT_ENOUGH_FILES_IN_REQUEST", "Number of files is below the minimum required"); } const usedIndexes: number[] = []; const result: FileDefinition[] = []; @@ -402,25 +402,25 @@ export class InboxService extends BaseContainerService { } const reqFile = request.files[file.fileIndex]; if (!reqFile) { - throw new AppException("INVALID_FILE_INDEX"); + throw new AppException("INVALID_FILE_INDEX", "File index not found in the request"); } if (usedIndexes.includes(file.fileIndex)) { - throw new AppException("FILE_ALREADY_USED"); + throw new AppException("FILE_ALREADY_USED", "File index is already assigned to another file"); } if (reqFile.size > fileConfig.maxFileSize) { - throw new AppException("REQUEST_FILE_SIZE_EXCEEDED"); + throw new AppException("REQUEST_FILE_SIZE_EXCEEDED", "File size exceeds the allowed limit"); } usedIndexes.push(file.fileIndex); const reqThumb = typeof(file.thumbIndex) === "number" ? request.files[file.thumbIndex] : undefined; if (typeof(file.thumbIndex) === "number") { if (!reqThumb) { - throw new AppException("INVALID_FILE_INDEX"); + throw new AppException("INVALID_FILE_INDEX", "File index not found in the request"); } if (usedIndexes.includes(file.thumbIndex)) { - throw new AppException("FILE_ALREADY_USED"); + throw new AppException("FILE_ALREADY_USED", "File index is already assigned to another file"); } if (reqThumb.size > fileConfig.maxFileSize) { - throw new AppException("REQUEST_FILE_SIZE_EXCEEDED"); + throw new AppException("REQUEST_FILE_SIZE_EXCEEDED", "File size exceeds the allowed limit"); } usedIndexes.push(file.thumbIndex); } @@ -428,7 +428,7 @@ export class InboxService extends BaseContainerService { } const wholeSize = result.reduce((sum, x) => sum + x.file.sent + (x.thumb ? x.thumb.size : 0), 0); if (wholeSize > fileConfig.maxWholeUploadSize) { - throw new AppException("REQUEST_FILE_SIZE_EXCEEDED"); + throw new AppException("REQUEST_FILE_SIZE_EXCEEDED", "File size exceeds the allowed limit"); } return result; } diff --git a/src/service/cloud/JanusContextFactory.ts b/src/service/cloud/JanusContextFactory.ts index 53ac606..f289a7f 100644 --- a/src/service/cloud/JanusContextFactory.ts +++ b/src/service/cloud/JanusContextFactory.ts @@ -73,7 +73,7 @@ export class JanusContextFactory { catch (e) { this.logger.error(e, "Error during request to media server (Admin Task)"); this.destroyAdminContext(); - throw new AppException("ERROR_DURING_REQUEST_TO_MEDIA_SERVER"); + throw new AppException("ERROR_DURING_REQUEST_TO_MEDIA_SERVER", "An error occurred during a request to the media server"); } } diff --git a/src/service/cloud/JanusRoomsWatcher.ts b/src/service/cloud/JanusRoomsWatcher.ts index f685340..adc9b9b 100644 --- a/src/service/cloud/JanusRoomsWatcher.ts +++ b/src/service/cloud/JanusRoomsWatcher.ts @@ -215,7 +215,7 @@ export class JanusRoomsWatcher { catch (e) { this.logger.debug(e, "JanusRoomsWatcher: Failed to connect."); this.cleanup(); - throw new AppException("MEDIA_SERVER_ERROR"); + throw new AppException("MEDIA_SERVER_ERROR", "Failed to connect to media server"); } } diff --git a/src/service/cloud/KvdbService.ts b/src/service/cloud/KvdbService.ts index f7c73d1..e534dda 100644 --- a/src/service/cloud/KvdbService.ts +++ b/src/service/cloud/KvdbService.ts @@ -53,7 +53,7 @@ export class KvdbService extends BaseContainerService { await this.cloudAccessValidator.checkIfCanExecuteInContext(executor, kvdb.contextId, (user, context) => { this.cloudAclChecker.verifyAccess(user.acl, "kvdb/kvdbGet", ["kvdbId=" + kvdbId]); if (!this.policy.canReadContainer(user, context, kvdb)) { - throw new AppException("ACCESS_DENIED"); + throw new AppException("ACCESS_DENIED", "Policy denied read access to this container"); } }); return kvdb; @@ -64,12 +64,12 @@ export class KvdbService extends BaseContainerService { this.cloudAclChecker.verifyAccess(user.acl, "kvdb/kvdbList", []); if (scope === "ALL") { if (!this.policy.canListAllContainers(user, context)) { - throw new AppException("ACCESS_DENIED"); + throw new AppException("ACCESS_DENIED", "Policy denied listing all containers"); } } else { if (!this.policy.canListMyContainers(user, context)) { - throw new AppException("ACCESS_DENIED"); + throw new AppException("ACCESS_DENIED", "Policy denied listing own containers"); } } const kvdbs = await this.repositoryFactory.createKvdbRepository().getPageByContextAndUser(contextId, type, user.userId, cloudUser.solutionId, listParams, sortBy, scope); @@ -80,7 +80,7 @@ export class KvdbService extends BaseContainerService { const {user, context} = await this.cloudAccessValidator.getUserFromContext(cloudUser, contextId); this.cloudAclChecker.verifyAccess(user.acl, "kvdb/kvdbListAll", []); if (!this.policy.canListAllContainers(user, context)) { - throw new AppException("ACCESS_DENIED"); + throw new AppException("ACCESS_DENIED", "Policy denied listing all containers"); } const kvdbs = await this.repositoryFactory.createKvdbRepository().getAllKvdbs(contextId, type, listParams, sortBy); return {user, kvdbs}; @@ -99,7 +99,7 @@ export class KvdbService extends BaseContainerService { } catch (err) { if (err instanceof DbDuplicateError) { - throw new AppException("DUPLICATE_RESOURCE_ID"); + throw new AppException("DUPLICATE_RESOURCE_ID", "A resource with this ID already exists"); } throw err; } @@ -124,7 +124,7 @@ export class KvdbService extends BaseContainerService { } const newKeys = await this.cloudKeyService.checkKeysAndClients(oldKvdb.contextId, [...oldKvdb.history.map(x => x.keyId), keyId], oldKvdb.keys, keys, keyId, users, managers); if (oldKvdb.clientResourceId !== resourceId) { - throw new AppException("RESOURCE_ID_MISSMATCH"); + throw new AppException("RESOURCE_ID_MISSMATCH", "Resource ID does not match the original"); } const kvdb = await kvdbRepository.updateKvdb(oldKvdb, user.userId, managers, users, data, keyId, newKeys, policy); return {kvdb, context, oldKvdb}; @@ -147,7 +147,7 @@ export class KvdbService extends BaseContainerService { const usedContext = await this.cloudAccessValidator.checkIfCanExecuteInContext(executor, oldKvdb.contextId, (user, context) => { this.cloudAclChecker.verifyAccess(user.acl, "kvdb/kvdbDelete", ["kvdbId=" + id]); if (!this.policy.canDeleteContainer(user, context, oldKvdb)) { - throw new AppException("ACCESS_DENIED"); + throw new AppException("ACCESS_DENIED", "Policy denied container deletion"); } }); await kvdbRepository.deleteKvdb(oldKvdb.id); @@ -181,7 +181,7 @@ export class KvdbService extends BaseContainerService { const toNotify: db.kvdb.Kvdb[] = []; for (const kvdb of kvdbs) { if (kvdb.contextId !== contextId) { - throw new AppException("RESOURCES_HAVE_DIFFERENT_CONTEXTS"); + throw new AppException("RESOURCES_HAVE_DIFFERENT_CONTEXTS", "All resources must belong to the same context"); } if (!additionalAccessCheck(kvdb)) { resultMap.set(kvdb.id, "ACCESS_DENIED"); @@ -217,7 +217,7 @@ export class KvdbService extends BaseContainerService { } await this.cloudAccessValidator.checkIfCanExecuteInContext(executor, ctx, (user, context) => { if (!this.policy.canListAllContainers(user, context)) { - throw new AppException("ACCESS_DENIED"); + throw new AppException("ACCESS_DENIED", "Policy denied listing all containers"); } this.cloudAclChecker.verifyAccess(user.acl, "kvdb/kvdbList", []); }); @@ -233,7 +233,7 @@ export class KvdbService extends BaseContainerService { const {user, context} = await this.cloudAccessValidator.getUserFromContext(cloudUser, kvdb.contextId); this.cloudAclChecker.verifyAccess(user.acl, "kvdb/kvdbEntrySet", ["kvdbId=" + kvdbId, "entryKey=" + kvdbEntryKey]); if (kvdb.keyId !== keyId) { - throw new AppException("INVALID_KEY_ID"); + throw new AppException("INVALID_KEY_ID", "Key ID does not match the kvdb key"); } const item = await (async () => { const entryRepository = this.repositoryFactory.createKvdbEntryRepository(); @@ -241,7 +241,7 @@ export class KvdbService extends BaseContainerService { if (!entry && (version === 0 || force)) { if (!this.policy.canCreateItem(user, context, kvdb)) { - throw new AppException("ACCESS_DENIED"); + throw new AppException("ACCESS_DENIED", "Policy denied item creation in this container"); } return await entryRepository.createEntry(kvdbEntryKey, user.userId, kvdbId, kvdbEntryValue, keyId); } @@ -252,7 +252,7 @@ export class KvdbService extends BaseContainerService { throw new AppException("INVALID_VERSION", "Version missmatch"); } if (!this.policy.canUpdateItem(user, context, kvdb, entry)) { - throw new AppException("ACCESS_DENIED"); + throw new AppException("ACCESS_DENIED", "Policy denied update of this item"); } return await entryRepository.updateEntry(entry, user.userId, kvdbEntryValue, keyId); })(); @@ -282,7 +282,7 @@ export class KvdbService extends BaseContainerService { } await this.cloudAccessValidator.checkIfCanExecuteInContext(executor, kvdb.contextId, (user, context) => { if (!this.policy.canReadItem(user, context, kvdb, item)) { - throw new AppException("ACCESS_DENIED"); + throw new AppException("ACCESS_DENIED", "Policy denied read access to this item"); } this.cloudAclChecker.verifyAccess(user.acl, "kvdb/kvdbEntryGet", ["kvdbId=" + kvdb.id, "entryKey=" + entryKey]); }); @@ -301,7 +301,7 @@ export class KvdbService extends BaseContainerService { const usedContext = await this.cloudAccessValidator.checkIfCanExecuteInContext(executor, kvdb.contextId, (user, context) => { this.cloudAclChecker.verifyAccess(user.acl, "kvdb/kvdbEntryDelete", ["itemId=" + entryKey, "kvdbId=" + kvdb.id]); if (!this.policy.canDeleteItem(user, context, kvdb, item)) { - throw new AppException("ACCESS_DENIED"); + throw new AppException("ACCESS_DENIED", "Policy denied deletion of this item"); } }); await this.repositoryFactory.createKvdbEntryRepository().deleteEntry(kvdbId, entryKey); @@ -322,7 +322,7 @@ export class KvdbService extends BaseContainerService { } await this.cloudAccessValidator.checkIfCanExecuteInContext(executor, kvdb.contextId, (user, context) => { if (!this.policy.canListAllItems(user, context, kvdb)) { - throw new AppException("ACCESS_DENIED"); + throw new AppException("ACCESS_DENIED", "Policy denied listing all items"); } this.cloudAclChecker.verifyAccess(user.acl, "kvdb/kvdbListKeys", ["kvdbId=" + kvdbId]); }); @@ -338,7 +338,7 @@ export class KvdbService extends BaseContainerService { } await this.cloudAccessValidator.checkIfCanExecuteInContext(executor, kvdb.contextId, (user, context) => { if (!this.policy.canListAllItems(user, context, kvdb)) { - throw new AppException("ACCESS_DENIED"); + throw new AppException("ACCESS_DENIED", "Policy denied listing all items"); } this.cloudAclChecker.verifyAccess(user.acl, "kvdb/kvdbListKeys", ["kvdbId=" + kvdbId]); }); @@ -358,7 +358,7 @@ export class KvdbService extends BaseContainerService { } await this.cloudAccessValidator.checkIfCanExecuteInContext(executor, kvdb.contextId, (user, context) => { if (!this.policy.canListAllItems(user, context, kvdb)) { - throw new AppException("ACCESS_DENIED"); + throw new AppException("ACCESS_DENIED", "Policy denied listing all items"); } this.cloudAclChecker.verifyAccess(user.acl, "kvdb/getKvdbEntries", ["kvdbId=" + kvdbId]); }); @@ -374,7 +374,7 @@ export class KvdbService extends BaseContainerService { } await this.cloudAccessValidator.checkIfCanExecuteInContext(executor, kvdb.contextId, (user, context) => { if (!this.policy.canListAllItems(user, context, kvdb)) { - throw new AppException("ACCESS_DENIED"); + throw new AppException("ACCESS_DENIED", "Policy denied listing all items"); } this.cloudAclChecker.verifyAccess(user.acl, "kvdb/getKvdbEntries", ["kvdbId=" + kvdbId]); }); diff --git a/src/service/cloud/ResourceService.ts b/src/service/cloud/ResourceService.ts index b5eb934..3f00fde 100644 --- a/src/service/cloud/ResourceService.ts +++ b/src/service/cloud/ResourceService.ts @@ -174,7 +174,7 @@ export class ResourceService { private async getUserFromContext(userPubKey: types.core.EccPubKey, contextId: types.context.ContextId) { const user = await this.repositoryFactory.createContextUserRepository().getUserFromContext(userPubKey, contextId); if (!user) { - throw new AppException("ACCESS_DENIED"); + throw new AppException("ACCESS_DENIED", "User is not a member of this context"); } return user; } @@ -236,7 +236,7 @@ export class ResourceService { throw new AppException("RESOURCE_DOES_NOT_EXIST", {type: type.ref, id: acl.ref}); } if (resource.last.keyId != keyId) { - throw new AppException("INVALID_KEY"); + throw new AppException("INVALID_KEY", "Key ID does not match the resource key"); } this.validateAccessToResource(resource, user); // TODO should check rights to create sub objects not normal access const result: types.resource.RefResourceAcl = { @@ -250,7 +250,7 @@ export class ResourceService { private validateAccessToResource(resource: db.resource.Resource, user: db.context.ContextUser) { if (resource.acl.type !== "embedded" || !resource.acl.users.includes(user.userId)) { - throw new AppException("ACCESS_DENIED"); + throw new AppException("ACCESS_DENIED", "User does not have access to this resource"); } } @@ -346,10 +346,10 @@ export class ResourceService { throw new AppException("INVALID_PARAMS", "props"); } if (!request) { - throw new AppException("REQUEST_DOES_NOT_EXIST"); + throw new AppException("REQUEST_DOES_NOT_EXIST", "Upload request does not exist"); } if (!request.files[value]) { - throw new AppException("INVALID_FILE_INDEX"); + throw new AppException("INVALID_FILE_INDEX", "File index not found in the request"); } } } @@ -395,11 +395,11 @@ export class ResourceService { throw new AppException("INVALID_PARAMS", "props"); } if (!request) { - throw new AppException("REQUEST_DOES_NOT_EXIST"); + throw new AppException("REQUEST_DOES_NOT_EXIST", "Upload request does not exist"); } const file = request.files[value]; if (!file) { - throw new AppException("INVALID_FILE_INDEX"); + throw new AppException("INVALID_FILE_INDEX", "File index not found in the request"); } await this.storageService.commit(file.id); return {fileId: file.id, size: file.size}; diff --git a/src/service/cloud/SolutionService.ts b/src/service/cloud/SolutionService.ts index 0e66af8..087da75 100644 --- a/src/service/cloud/SolutionService.ts +++ b/src/service/cloud/SolutionService.ts @@ -53,7 +53,7 @@ export class SolutionService { } const contexts = await this.repositoryFactory.createContextRepository().getAllBySolution(solutionId); if (contexts.length > 0) { - throw new AppException("SOLUTION_HAS_CONTEXTS"); + throw new AppException("SOLUTION_HAS_CONTEXTS", "Solution cannot be deleted because it has associated contexts"); } await this.repositoryFactory.createSolutionRepository().remove(solutionId); } diff --git a/src/service/cloud/StoreService.ts b/src/service/cloud/StoreService.ts index 23f18d2..5eba6f1 100644 --- a/src/service/cloud/StoreService.ts +++ b/src/service/cloud/StoreService.ts @@ -67,7 +67,7 @@ export class StoreService extends BaseContainerService { } catch (err) { if (err instanceof DbDuplicateError) { - throw new AppException("DUPLICATE_RESOURCE_ID"); + throw new AppException("DUPLICATE_RESOURCE_ID", "A resource with this ID already exists"); } throw err; } @@ -92,7 +92,7 @@ export class StoreService extends BaseContainerService { } const newKeys = await this.cloudKeyService.checkKeysAndClients(oldStore.contextId, [...oldStore.history.map(x => x.keyId), keyId], oldStore.keys, keys, keyId, users, managers); if (oldStore.clientResourceId && resourceId && oldStore.clientResourceId !== resourceId) { - throw new AppException("RESOURCE_ID_MISSMATCH"); + throw new AppException("RESOURCE_ID_MISSMATCH", "Resource ID does not match the original"); } try { const store = await storeRepository.updateStore(oldStore, user.userId, managers, users, data, keyId, newKeys, policy, resourceId); @@ -100,7 +100,7 @@ export class StoreService extends BaseContainerService { } catch (err) { if (err instanceof DbDuplicateError) { - throw new AppException("DUPLICATE_RESOURCE_ID"); + throw new AppException("DUPLICATE_RESOURCE_ID", "A resource with this ID already exists"); } throw err; } @@ -162,7 +162,7 @@ export class StoreService extends BaseContainerService { const toNotify: db.store.Store[] = []; for (const store of stores) { if (store.contextId !== contextId) { - throw new AppException("RESOURCES_HAVE_DIFFERENT_CONTEXTS"); + throw new AppException("RESOURCES_HAVE_DIFFERENT_CONTEXTS", "All resources must belong to the same context"); } if (!additionalAccessCheck(store)) { resultMap.set(store.id, "ACCESS_DENIED"); @@ -223,7 +223,7 @@ export class StoreService extends BaseContainerService { const {user, context} = await this.cloudAccessValidator.getUserFromContext(cloudUser, contextId); this.cloudAclChecker.verifyAccess(user.acl, "store/storeListAll", []); if (!this.policy.canListAllContainers(user, context)) { - throw new AppException("ACCESS_DENIED"); + throw new AppException("ACCESS_DENIED", "Policy denied listing all containers"); } const stores = await this.repositoryFactory.createStoreRepository().getAllStores(contextId, type, listParams, sortBy); return {user, stores}; @@ -234,12 +234,12 @@ export class StoreService extends BaseContainerService { this.cloudAclChecker.verifyAccess(user.acl, "store/storeList", []); if (scope === "ALL") { if (!this.policy.canListAllContainers(user, context)) { - throw new AppException("ACCESS_DENIED"); + throw new AppException("ACCESS_DENIED", "Policy denied listing all containers"); } } else { if (!this.policy.canListMyContainers(user, context)) { - throw new AppException("ACCESS_DENIED"); + throw new AppException("ACCESS_DENIED", "Policy denied listing own containers"); } } const stores = await this.repositoryFactory.createStoreRepository().getPageByContextAndUser(contextId, type, user.userId, cloudUser.solutionId, listParams, sortBy, scope); @@ -253,7 +253,7 @@ export class StoreService extends BaseContainerService { } await this.cloudAccessValidator.checkIfCanExecuteInContext(executor, ctx, (user, context) => { if (!this.policy.canListAllContainers(user, context)) { - throw new AppException("ACCESS_DENIED"); + throw new AppException("ACCESS_DENIED", "Policy denied listing all containers"); } this.cloudAclChecker.verifyAccess(user.acl, "store/storeList", []); }); @@ -273,7 +273,7 @@ export class StoreService extends BaseContainerService { await this.cloudAccessValidator.checkIfCanExecuteInContext(executor, store.contextId, (user, context) => { this.cloudAclChecker.verifyAccess(user.acl, "store/storeFileGet", ["storeId=" + store.id, "fileId=" + fileId]); if (!this.policy.canReadItem(user, context, store, file)) { - throw new AppException("ACCESS_DENIED"); + throw new AppException("ACCESS_DENIED", "Policy denied read access to this item"); } }); return {file, store}; @@ -287,7 +287,7 @@ export class StoreService extends BaseContainerService { await this.cloudAccessValidator.checkIfCanExecuteInContext(executor, store.contextId, (user, context) => { this.cloudAclChecker.verifyAccess(user.acl, "store/storeFileGetMany", ["storeId=" + storeId]); if (!this.policy.canListAllItems(user, context, store)) { - throw new AppException("ACCESS_DENIED"); + throw new AppException("ACCESS_DENIED", "Policy denied listing all items"); } }); const files: (db.store.StoreFile|types.store.StoreFileFetchError)[] = []; @@ -331,7 +331,7 @@ export class StoreService extends BaseContainerService { } await this.cloudAccessValidator.checkIfCanExecuteInContext(executor, store.contextId, (user, context) => { if (!this.policy.canListAllItems(user, context, store)) { - throw new AppException("ACCESS_DENIED"); + throw new AppException("ACCESS_DENIED", "Policy denied listing all items"); } this.cloudAclChecker.verifyAccess(user.acl, "store/storeFileList", ["storeId=" + storeId]); }); @@ -346,7 +346,7 @@ export class StoreService extends BaseContainerService { } await this.cloudAccessValidator.checkIfCanExecuteInContext(executor, store.contextId, (user, context) => { if (!this.policy.canListMyItems(user, context, store)) { - throw new AppException("ACCESS_DENIED"); + throw new AppException("ACCESS_DENIED", "Policy denied listing own items"); } this.cloudAclChecker.verifyAccess(user.acl, "store/storeFileListMy", ["storeId=" + storeId]); }); @@ -361,7 +361,7 @@ export class StoreService extends BaseContainerService { } await this.cloudAccessValidator.checkIfCanExecuteInContext(executor, store.contextId, (user, context) => { if (!this.policy.canListAllItems(user, context, store)) { - throw new AppException("ACCESS_DENIED"); + throw new AppException("ACCESS_DENIED", "Policy denied listing all items"); } this.cloudAclChecker.verifyAccess(user.acl, "store/storeFileList", ["storeId=" + storeId]); }); @@ -372,23 +372,23 @@ export class StoreService extends BaseContainerService { async createStoreFile(cloudUser: CloudUser, model: storeApi.StoreFileCreateModel) { const {user, context, store} = await this.getStoreAndUser(cloudUser, model.storeId); if (!this.policy.canCreateItem(user, context, store)) { - throw new AppException("ACCESS_DENIED"); + throw new AppException("ACCESS_DENIED", "Policy denied item creation in this container"); } this.cloudAclChecker.verifyAccess(user.acl, "store/storeFileCreate", ["storeId=" + model.storeId]); if (model.keyId !== store.keyId) { - throw new AppException("INVALID_KEY"); + throw new AppException("INVALID_KEY", "Key ID does not match the store key"); } const requestRepository = this.repositoryFactory.createRequestRepository(); const request = await requestRepository.getReadyForUser(cloudUser.pub, model.requestId); if (!request.files[model.fileIndex]) { - throw new AppException("INVALID_FILE_INDEX"); + throw new AppException("INVALID_FILE_INDEX", "File index not found in the request"); } if (typeof(model.thumbIndex) === "number") { if (!request.files[model.thumbIndex]) { - throw new AppException("INVALID_FILE_INDEX"); + throw new AppException("INVALID_FILE_INDEX", "File index not found in the request"); } if (model.thumbIndex === model.fileIndex) { - throw new AppException("FILE_ALREADY_USED"); + throw new AppException("FILE_ALREADY_USED", "File index is already assigned to another file"); } } const uploadedFile = request.files[model.fileIndex]; @@ -410,7 +410,7 @@ export class StoreService extends BaseContainerService { } catch (err) { if (err instanceof DbDuplicateError) { - throw new AppException("DUPLICATE_RESOURCE_ID"); + throw new AppException("DUPLICATE_RESOURCE_ID", "A resource with this ID already exists"); } throw err; } @@ -424,10 +424,10 @@ export class StoreService extends BaseContainerService { const {user, context, store} = await this.getStoreAndUser(cloudUser, oldFile.storeId); this.cloudAclChecker.verifyAccess(user.acl, "store/storeFileWrite", ["storeId=" + store.id, "fileId=" + model.fileId]); if (!this.policy.canUpdateItem(user, context, store, oldFile)) { - throw new AppException("ACCESS_DENIED"); + throw new AppException("ACCESS_DENIED", "Policy denied update of this item"); } if (model.keyId !== store.keyId) { - throw new AppException("INVALID_KEY"); + throw new AppException("INVALID_KEY", "Key ID does not match the store key"); } const currentVersion = ((oldFile.updates || []).length + 1) as types.store.StoreFileVersion; if (typeof(model.version) === "number" && currentVersion !== model.version && model.force !== true) { @@ -436,14 +436,14 @@ export class StoreService extends BaseContainerService { const requestRepository = this.repositoryFactory.createRequestRepository(); const request = await requestRepository.getReadyForUser(cloudUser.pub, model.requestId); if (!request.files[model.fileIndex]) { - throw new AppException("INVALID_FILE_INDEX"); + throw new AppException("INVALID_FILE_INDEX", "File index not found in the request"); } if (typeof(model.thumbIndex) === "number") { if (!request.files[model.thumbIndex]) { - throw new AppException("INVALID_FILE_INDEX"); + throw new AppException("INVALID_FILE_INDEX", "File index not found in the request"); } if (model.thumbIndex === model.fileIndex) { - throw new AppException("FILE_ALREADY_USED"); + throw new AppException("FILE_ALREADY_USED", "File index is already assigned to another file"); } } const uploadedFile = request.files[model.fileIndex]; @@ -471,10 +471,10 @@ export class StoreService extends BaseContainerService { const {user, context, store} = await this.getStoreAndUser(cloudUser, oldFile.storeId); this.cloudAclChecker.verifyAccess(user.acl, "store/storeFileWrite", ["storeId=" + store.id, "fileId=" + model.fileId]); if (!this.policy.canUpdateItem(user, context, store, oldFile)) { - throw new AppException("ACCESS_DENIED"); + throw new AppException("ACCESS_DENIED", "Policy denied update of this item"); } if (model.keyId !== store.keyId) { - throw new AppException("INVALID_KEY"); + throw new AppException("INVALID_KEY", "Key ID does not match the store key"); } if (!oldFile.supportsRandomWrite) { throw new AppException("UNSUPPORTED_OPERATION", "Random write can be only executed on files supporting this operation"); @@ -502,17 +502,17 @@ export class StoreService extends BaseContainerService { const {user, context, store} = await this.getStoreAndUser(cloudUser, oldFile.storeId); this.cloudAclChecker.verifyAccess(user.acl, "store/storeFileUpdate", ["storeId=" + store.id, "fileId=" + model.fileId]); if (!this.policy.canUpdateItem(user, context, store, oldFile)) { - throw new AppException("ACCESS_DENIED"); + throw new AppException("ACCESS_DENIED", "Policy denied update of this item"); } if (model.keyId !== store.keyId) { - throw new AppException("INVALID_KEY"); + throw new AppException("INVALID_KEY", "Key ID does not match the store key"); } const currentVersion = ((oldFile.updates || []).length + 1) as types.store.StoreFileVersion; if (typeof(model.version) === "number" && currentVersion !== model.version && model.force !== true) { throw new AppException("INVALID_VERSION", `version does not match, get: ${model.version}, expected: ${currentVersion}`); } if (oldFile.clientResourceId && model.resourceId && oldFile.clientResourceId !== model.resourceId) { - throw new AppException("RESOURCE_ID_MISSMATCH"); + throw new AppException("RESOURCE_ID_MISSMATCH", "Resource ID does not match the original"); } try { const file = await this.repositoryFactory.createStoreFileRepository().updateMeta(oldFile, user.userId, model.meta, model.keyId, model.resourceId || null); @@ -521,7 +521,7 @@ export class StoreService extends BaseContainerService { } catch (err) { if (err instanceof DbDuplicateError) { - throw new AppException("DUPLICATE_RESOURCE_ID"); + throw new AppException("DUPLICATE_RESOURCE_ID", "A resource with this ID already exists"); } throw err; } @@ -539,7 +539,7 @@ export class StoreService extends BaseContainerService { const usedContext = await this.cloudAccessValidator.checkIfCanExecuteInContext(executor, store.contextId, (user, context) => { this.cloudAclChecker.verifyAccess(user.acl, "store/storeFileDelete", ["storeId=" + store.id, "fileId=" + fileId]); if (!this.policy.canDeleteItem(user, context, store, file)) { - throw new AppException("ACCESS_DENIED"); + throw new AppException("ACCESS_DENIED", "Policy denied deletion of this item"); } }); const deletedAt = DateUtils.now(); @@ -634,7 +634,7 @@ export class StoreService extends BaseContainerService { } const {user, context, store} = await this.getStoreAndUser(cloudUser, file.storeId); if (!this.policy.canReadItem(user, context, store, file)) { - throw new AppException("ACCESS_DENIED"); + throw new AppException("ACCESS_DENIED", "Policy denied read access to this item"); } this.cloudAclChecker.verifyAccess(user.acl, "store/storeFileRead", ["storeId=" + store.id, "fileId=" + fileId]); if (typeof(version) === "number" && this.getFileVersion(file) !== version) { @@ -661,10 +661,10 @@ export class StoreService extends BaseContainerService { const {user, context} = await this.cloudAccessValidator.getUserFromContext(cloudUser, store.contextId); this.cloudAclChecker.verifyAccess(user.acl, "store/storeSendCustomNotification", ["storeId=" + storeId]); if (!this.policy.canSendCustomNotification(user, context, store)) { - throw new AppException("ACCESS_DENIED"); + throw new AppException("ACCESS_DENIED", "Policy denied custom notification"); } if (users && users.some(element => !store.users.includes(element))) { - throw new AppException("USER_DOES_NOT_HAVE_ACCESS_TO_CONTAINER"); + throw new AppException("USER_DOES_NOT_HAVE_ACCESS_TO_CONTAINER", "One or more users do not have access to this container"); } this.storeNotificationService.sendStoreCustomEvent(store, keyId, data, {id: user.userId, pub: user.userPubKey}, customChannelName, users); return store; diff --git a/src/service/cloud/StreamService.ts b/src/service/cloud/StreamService.ts index 7220262..d4b71b2 100644 --- a/src/service/cloud/StreamService.ts +++ b/src/service/cloud/StreamService.ts @@ -125,7 +125,7 @@ export class StreamService extends BaseContainerService { } catch (err) { if (err instanceof DbDuplicateError) { - throw new AppException("DUPLICATE_RESOURCE_ID"); + throw new AppException("DUPLICATE_RESOURCE_ID", "A resource with this ID already exists"); } throw err; } @@ -155,7 +155,7 @@ export class StreamService extends BaseContainerService { const newKeys = await this.cloudKeyService.checkKeysAndClients(oldStreamRoom.contextId, [...oldStreamRoom.history.map(x => x.keyId), keyId], oldStreamRoom.keys, keys, keyId, users, managers); if (oldStreamRoom.clientResourceId && resourceId && oldStreamRoom.clientResourceId !== resourceId) { - throw new AppException("RESOURCE_ID_MISSMATCH"); + throw new AppException("RESOURCE_ID_MISSMATCH", "Resource ID does not match the original"); } try { const streamRoom = await streamRoomRepository.updateStreamRoom(oldStreamRoom, user.userId, managers, users, data, keyId, newKeys, policy, resourceId); @@ -163,7 +163,7 @@ export class StreamService extends BaseContainerService { } catch (err) { if (err instanceof DbDuplicateError) { - throw new AppException("DUPLICATE_RESOURCE_ID"); + throw new AppException("DUPLICATE_RESOURCE_ID", "A resource with this ID already exists"); } throw err; } @@ -188,7 +188,7 @@ export class StreamService extends BaseContainerService { const usedContext = await this.cloudAccessValidator.checkIfCanExecuteInContext(executor, streamRoom.contextId, (user, context) => { this.cloudAclChecker.verifyAccess(user.acl, "stream/streamRoomDelete", ["streamRoomId=" + id]); if (!this.policy.canDeleteContainer(user, context, streamRoom)) { - throw new AppException("ACCESS_DENIED"); + throw new AppException("ACCESS_DENIED", "Policy denied container deletion"); } }); await streamRoomRepository.deleteStreamRoom(streamRoom.id); @@ -223,7 +223,7 @@ export class StreamService extends BaseContainerService { const toNotify: db.stream.StreamRoom[] = []; for (const streamRoom of streamRooms) { if (streamRoom.contextId !== contextId) { - throw new AppException("RESOURCES_HAVE_DIFFERENT_CONTEXTS"); + throw new AppException("RESOURCES_HAVE_DIFFERENT_CONTEXTS", "All resources must belong to the same context"); } if (!additionalAccessCheck(streamRoom)) { resultMap.set(streamRoom.id, "ACCESS_DENIED"); @@ -352,7 +352,7 @@ export class StreamService extends BaseContainerService { const existingJanusSession = this.findJanusSession(ctx, JanusConstants.SESSION_TYPE.SUBSCRIBER, streamRoom.janusRoomId); if (!existingJanusSession) { - throw new AppException("NO_SUBSCRIPTIONS_TO_MODIFY"); + throw new AppException("NO_SUBSCRIPTIONS_TO_MODIFY", "No active subscriptions to modify"); } const mappedStreamsToAdd = subscriptionsToAdd.map(x => ({ feed: x.streamId, mid: x.streamTrackId })); @@ -383,7 +383,7 @@ export class StreamService extends BaseContainerService { res = await ctx.ws.janusVideoRoomPluginApi.unsubscribeOnExisting(baseRequest as UnsubscribeOnExistingRequest); } else { - throw new AppException("NO_SUBSCRIPTIONS_TO_MODIFY"); + throw new AppException("NO_SUBSCRIPTIONS_TO_MODIFY", "No active subscriptions to modify"); } } catch { @@ -608,7 +608,7 @@ export class StreamService extends BaseContainerService { this.cloudAclChecker.verifyAccess(user.acl, "stream/streamUnpublish", ["streamRoomId=" + streamRoom.id]); if (!this.policy.canUpdateContainer(user, context, streamRoom)) { - throw new AppException("ACCESS_DENIED"); + throw new AppException("ACCESS_DENIED", "Policy denied container update"); } await ctx.ws.janusVideoRoomPluginApi.unpublish({ @@ -714,7 +714,7 @@ export class StreamService extends BaseContainerService { const { user, context } = await this.cloudAccessValidator.getUserFromContext(cloudUser, contextId); this.cloudAclChecker.verifyAccess(user.acl, "stream/streamRoomListAll", []); if (!this.policy.canListAllContainers(user, context)) { - throw new AppException("ACCESS_DENIED"); + throw new AppException("ACCESS_DENIED", "Policy denied listing all containers"); } const streamRooms = await this.repositoryFactory.createStreamRoomRepository().getAllStreams(contextId, type, listParams, sortBy); return { user, streamRooms }; @@ -729,7 +729,7 @@ export class StreamService extends BaseContainerService { : this.policy.canListMyContainers(user, context); if (!canList) { - throw new AppException("ACCESS_DENIED"); + throw new AppException("ACCESS_DENIED", "Policy denied listing stream rooms"); } const streamRooms = await this.repositoryFactory.createStreamRoomRepository().getPageByContextAndUser(contextId, type, user.userId, cloudUser.solutionId, listParams, sortBy, scope); @@ -744,7 +744,7 @@ export class StreamService extends BaseContainerService { await this.cloudAccessValidator.checkIfCanExecuteInContext(executor, ctx, (user, context) => { if (!this.policy.canListAllContainers(user, context)) { - throw new AppException("ACCESS_DENIED"); + throw new AppException("ACCESS_DENIED", "Policy denied listing all containers"); } this.cloudAclChecker.verifyAccess(user.acl, "stream/streamRoomList", []); }); @@ -754,7 +754,7 @@ export class StreamService extends BaseContainerService { case "all": return { streamRooms: await repo.getPageByContext(contextId, listParams) }; case "active": return { streamRooms: await repo.getPageOfActiveStreamsByContext(contextId, listParams) }; case "closed": return { streamRooms: await repo.getPageOfClosedStreamsByContext(contextId, listParams) }; - default: throw new AppException("INVALID_PARAMS"); + default: throw new AppException("INVALID_PARAMS", "Invalid state parameter, expected: all, active, or closed"); } } @@ -764,10 +764,10 @@ export class StreamService extends BaseContainerService { this.cloudAclChecker.verifyAccess(user.acl, "stream/streamSendCustomNotification", ["streamRoomId=" + streamRoomId]); if (!this.policy.canSendCustomNotification(user, context, streamRoom)) { - throw new AppException("ACCESS_DENIED"); + throw new AppException("ACCESS_DENIED", "Policy denied custom notification"); } if (users && users.some(element => !streamRoom.users.includes(element))) { - throw new AppException("USER_DOES_NOT_HAVE_ACCESS_TO_CONTAINER"); + throw new AppException("USER_DOES_NOT_HAVE_ACCESS_TO_CONTAINER", "One or more users do not have access to this container"); } this.streamNotificationService.sendStreamCustomEvent(streamRoom, keyId, data, { id: user.userId, pub: user.userPubKey }, customChannelName, users); @@ -778,12 +778,12 @@ export class StreamService extends BaseContainerService { const { streamRoom, ctx, user, context } = await this.ensureActiveStreamRoomWithAcl(cloudUser, streamRoomId, websocket, wsId, "stream/streamRoomEnableRecording"); if (!this.policy.canUpdateContainer(user, context, streamRoom)) { - throw new AppException("ACCESS_DENIED"); + throw new AppException("ACCESS_DENIED", "Policy denied container update"); } const existingSignalingSession = this.findJanusSession(ctx, JanusConstants.SESSION_TYPE.MAIN, streamRoom.janusRoomId); if (!existingSignalingSession) { - throw new AppException("MAIN_MEDIA_SESSION_FOR_USER_MISSING"); + throw new AppException("MAIN_MEDIA_SESSION_FOR_USER_MISSING", "No main media session found for this user"); } const janusSession = existingSignalingSession.session; @@ -817,7 +817,7 @@ export class StreamService extends BaseContainerService { this.cloudAclChecker.verifyAccess(user.acl, "stream/streamRoomClose", ["streamRoomId=" + id]); if (!this.policy.canUpdateContainer(user, context, streamRoom)) { - throw new AppException("ACCESS_DENIED"); + throw new AppException("ACCESS_DENIED", "Policy denied container update"); } await this.repositoryFactory.createStreamRoomRepository().closeStreamRoom(id); @@ -896,7 +896,7 @@ export class StreamService extends BaseContainerService { return await this.cloudAccessValidator.checkIfCanExecuteInContext(executor, streamRoom.contextId, (user, context) => { this.cloudAclChecker.verifyAccess(user.acl, requiredAcl, ["streamRoomId=" + streamRoom.id]); if (!this.policy.canReadContainer(user, context, streamRoom)) { - throw new AppException("ACCESS_DENIED"); + throw new AppException("ACCESS_DENIED", "Policy denied read access to this container"); } }); } @@ -910,7 +910,7 @@ export class StreamService extends BaseContainerService { const { user, context } = await this.cloudAccessValidator.getUserFromContext(cloudUser, streamRoom.contextId); this.cloudAclChecker.verifyAccess(user.acl, requiredAcl, ["streamRoomId=" + streamRoom.id]); if (!this.policy.canReadContainer(user, context, streamRoom)) { - throw new AppException("ACCESS_DENIED"); + throw new AppException("ACCESS_DENIED", "Policy denied read access to this container"); } const ctx = await this.janusContextFactory.prepareJanusContext(websocket, wsId); diff --git a/src/service/cloud/ThreadService.ts b/src/service/cloud/ThreadService.ts index 6129bea..221584b 100644 --- a/src/service/cloud/ThreadService.ts +++ b/src/service/cloud/ThreadService.ts @@ -52,7 +52,7 @@ export class ThreadService extends BaseContainerService { await this.cloudAccessValidator.checkIfCanExecuteInContext(executor, thread.contextId, (user, context) => { this.cloudAclChecker.verifyAccess(user.acl, "thread/threadGet", ["threadId=" + threadId]); if (!this.policy.canReadContainer(user, context, thread)) { - throw new AppException("ACCESS_DENIED"); + throw new AppException("ACCESS_DENIED", "Policy denied read access to this container"); } }); return thread; @@ -63,12 +63,12 @@ export class ThreadService extends BaseContainerService { this.cloudAclChecker.verifyAccess(user.acl, "thread/threadList", []); if (scope === "ALL") { if (!this.policy.canListAllContainers(user, context)) { - throw new AppException("ACCESS_DENIED"); + throw new AppException("ACCESS_DENIED", "Policy denied listing all containers"); } } else { if (!this.policy.canListMyContainers(user, context)) { - throw new AppException("ACCESS_DENIED"); + throw new AppException("ACCESS_DENIED", "Policy denied listing own containers"); } } const threads = await this.repositoryFactory.createThreadRepository().getPageByContextAndUser(contextId, type, user.userId, cloudUser.solutionId, listParams, sortBy, scope); @@ -79,7 +79,7 @@ export class ThreadService extends BaseContainerService { const {user, context} = await this.cloudAccessValidator.getUserFromContext(cloudUser, contextId); this.cloudAclChecker.verifyAccess(user.acl, "thread/threadListAll", []); if (!this.policy.canListAllContainers(user, context)) { - throw new AppException("ACCESS_DENIED"); + throw new AppException("ACCESS_DENIED", "Policy denied listing all containers"); } const threads = await this.repositoryFactory.createThreadRepository().getAllThreads(contextId, type, listParams, sortBy); return {user, threads}; @@ -92,7 +92,7 @@ export class ThreadService extends BaseContainerService { } await this.cloudAccessValidator.checkIfCanExecuteInContext(executor, ctx, (user, context) => { if (!this.policy.canListAllContainers(user, context)) { - throw new AppException("ACCESS_DENIED"); + throw new AppException("ACCESS_DENIED", "Policy denied listing all containers"); } this.cloudAclChecker.verifyAccess(user.acl, "thread/threadList", []); }); @@ -111,7 +111,7 @@ export class ThreadService extends BaseContainerService { } await this.cloudAccessValidator.checkIfCanExecuteInContext(executor, thread.contextId, (user, context) => { if (!this.policy.canReadItem(user, context, thread, message)) { - throw new AppException("ACCESS_DENIED"); + throw new AppException("ACCESS_DENIED", "Policy denied read access to this item"); } this.cloudAclChecker.verifyAccess(user.acl, "thread/threadMessageGet", ["threadId=" + thread.id, "messageId=" + messageId]); }); @@ -125,7 +125,7 @@ export class ThreadService extends BaseContainerService { } await this.cloudAccessValidator.checkIfCanExecuteInContext(executor, thread.contextId, (user, context) => { if (!this.policy.canListAllItems(user, context, thread)) { - throw new AppException("ACCESS_DENIED"); + throw new AppException("ACCESS_DENIED", "Policy denied listing all items"); } this.cloudAclChecker.verifyAccess(user.acl, "thread/threadMessagesGet", ["threadId=" + thread.id]); }); @@ -140,7 +140,7 @@ export class ThreadService extends BaseContainerService { } await this.cloudAccessValidator.checkIfCanExecuteInContext(executor, thread.contextId, (user, context) => { if (!this.policy.canListMyItems(user, context, thread)) { - throw new AppException("ACCESS_DENIED"); + throw new AppException("ACCESS_DENIED", "Policy denied listing own items"); } this.cloudAclChecker.verifyAccess(user.acl, "thread/threadMessagesGetMy", ["threadId=" + thread.id]); }); @@ -155,7 +155,7 @@ export class ThreadService extends BaseContainerService { } await this.cloudAccessValidator.checkIfCanExecuteInContext(executor, thread.contextId, (user, context) => { if (!this.policy.canListAllItems(user, context, thread)) { - throw new AppException("ACCESS_DENIED"); + throw new AppException("ACCESS_DENIED", "Policy denied listing all items"); } this.cloudAclChecker.verifyAccess(user.acl, "thread/threadMessagesGet", ["threadId=" + thread.id]); }); @@ -176,7 +176,7 @@ export class ThreadService extends BaseContainerService { } catch (err) { if (err instanceof DbDuplicateError) { - throw new AppException("DUPLICATE_RESOURCE_ID"); + throw new AppException("DUPLICATE_RESOURCE_ID", "A resource with this ID already exists"); } throw err; } @@ -201,7 +201,7 @@ export class ThreadService extends BaseContainerService { } const newKeys = await this.cloudKeyService.checkKeysAndClients(oldThread.contextId, [...oldThread.history.map(x => x.keyId), keyId], oldThread.keys, keys, keyId, users, managers); if (oldThread.clientResourceId && resourceId && oldThread.clientResourceId !== resourceId) { - throw new AppException("RESOURCE_ID_MISSMATCH"); + throw new AppException("RESOURCE_ID_MISSMATCH", "Resource ID does not match the original"); } try { const thread = await threadRepository.updateThread(oldThread, user.userId, managers, users, data, keyId, newKeys, policy, resourceId); @@ -209,7 +209,7 @@ export class ThreadService extends BaseContainerService { } catch (err) { if (err instanceof DbDuplicateError) { - throw new AppException("DUPLICATE_RESOURCE_ID"); + throw new AppException("DUPLICATE_RESOURCE_ID", "A resource with this ID already exists"); } throw err; } @@ -232,7 +232,7 @@ export class ThreadService extends BaseContainerService { const usedContext = await this.cloudAccessValidator.checkIfCanExecuteInContext(executor, oldThread.contextId, (user, context) => { this.cloudAclChecker.verifyAccess(user.acl, "thread/threadDelete", ["threadId=" + id]); if (!this.policy.canDeleteContainer(user, context, oldThread)) { - throw new AppException("ACCESS_DENIED"); + throw new AppException("ACCESS_DENIED", "Policy denied container deletion"); } }); const inboxes = await this.repositoryFactory.createInboxRepository(session).getInboxesWithThread(id); @@ -271,7 +271,7 @@ export class ThreadService extends BaseContainerService { const toNotify: db.thread.Thread[] = []; for (const thread of threads) { if (thread.contextId !== contextId) { - throw new AppException("RESOURCES_HAVE_DIFFERENT_CONTEXTS"); + throw new AppException("RESOURCES_HAVE_DIFFERENT_CONTEXTS", "All resources must belong to the same context"); } if (!additionalAccessCheck(thread)) { resultMap.set(thread.id, "ACCESS_DENIED"); @@ -323,10 +323,10 @@ export class ThreadService extends BaseContainerService { const {user, context} = await this.cloudAccessValidator.getUserFromContext(cloudUser, thread.contextId); this.cloudAclChecker.verifyAccess(user.acl, "thread/threadMessageSend", ["threadId=" + threadId]); if (!this.policy.canCreateItem(user, context, thread)) { - throw new AppException("ACCESS_DENIED"); + throw new AppException("ACCESS_DENIED", "Policy denied item creation in this container"); } if (thread.keyId !== keyId) { - throw new AppException("INVALID_THREAD_KEY"); + throw new AppException("INVALID_THREAD_KEY", "Key ID does not match the thread key"); } try { const message = await this.repositoryFactory.createThreadMessageRepository().tryCreateMessage(null, user.userId, threadId, data, keyId, resourceId); @@ -340,7 +340,7 @@ export class ThreadService extends BaseContainerService { } catch (err) { if (err instanceof DbDuplicateError) { - throw new AppException("DUPLICATE_RESOURCE_ID"); + throw new AppException("DUPLICATE_RESOURCE_ID", "A resource with this ID already exists"); } throw err; } @@ -358,17 +358,17 @@ export class ThreadService extends BaseContainerService { const {user, context} = await this.cloudAccessValidator.getUserFromContext(cloudUser, thread.contextId); this.cloudAclChecker.verifyAccess(user.acl, "thread/threadMessageUpdate", ["messageId=" + messageId, "threadId=" + thread.id]); if (!this.policy.canUpdateItem(user, context, thread, message)) { - throw new AppException("ACCESS_DENIED"); + throw new AppException("ACCESS_DENIED", "Policy denied update of this item"); } if (thread.keyId !== keyId) { - throw new AppException("INVALID_THREAD_KEY"); + throw new AppException("INVALID_THREAD_KEY", "Key ID does not match the thread key"); } const currentVersion = ((message.updates || []).length + 1) as types.thread.ThreadMessageVersion; if (typeof(version) === "number" && currentVersion !== version && force !== true) { throw new AppException("INVALID_VERSION", `version does not match, get: ${version}, expected: ${currentVersion}`); } if (message.clientResourceId && resourceId && message.clientResourceId !== resourceId) { - throw new AppException("RESOURCE_ID_MISSMATCH"); + throw new AppException("RESOURCE_ID_MISSMATCH", "Resource ID does not match the original"); } try { const newMessage = await this.repositoryFactory.createThreadMessageRepository().updateMessage(message, user.userId, data, keyId, resourceId); @@ -377,7 +377,7 @@ export class ThreadService extends BaseContainerService { } catch (err) { if (err instanceof DbDuplicateError) { - throw new AppException("DUPLICATE_RESOURCE_ID"); + throw new AppException("DUPLICATE_RESOURCE_ID", "A resource with this ID already exists"); } throw err; } @@ -395,7 +395,7 @@ export class ThreadService extends BaseContainerService { const usedContext = await this.cloudAccessValidator.checkIfCanExecuteInContext(executor, thread.contextId, (user, context) => { this.cloudAclChecker.verifyAccess(user.acl, "thread/threadMessageDelete", ["messageId=" + messageId, "threadId=" + thread.id]); if (!this.policy.canDeleteItem(user, context, thread, message)) { - throw new AppException("ACCESS_DENIED"); + throw new AppException("ACCESS_DENIED", "Policy denied deletion of this item"); } }); await this.repositoryFactory.createThreadMessageRepository().deleteMessage(messageId); @@ -492,10 +492,10 @@ export class ThreadService extends BaseContainerService { const {user, context} = await this.cloudAccessValidator.getUserFromContext(cloudUser, thread.contextId); this.cloudAclChecker.verifyAccess(user.acl, "thread/threadSendCustomNotification", ["threadId=" + threadId]); if (!this.policy.canSendCustomNotification(user, context, thread)) { - throw new AppException("ACCESS_DENIED"); + throw new AppException("ACCESS_DENIED", "Policy denied custom notification"); } if (users && users.some(element => !thread.users.includes(element))) { - throw new AppException("USER_DOES_NOT_HAVE_ACCESS_TO_CONTAINER"); + throw new AppException("USER_DOES_NOT_HAVE_ACCESS_TO_CONTAINER", "One or more users do not have access to this container"); } this.threadNotificationService.sendThreadCustomEvent(thread, keyId, data, {id: user.userId, pub: user.userPubKey}, customChannelName, users); return thread; diff --git a/src/service/login/EcdheLoginService.ts b/src/service/login/EcdheLoginService.ts index 764ada0..8d407c8 100644 --- a/src/service/login/EcdheLoginService.ts +++ b/src/service/login/EcdheLoginService.ts @@ -87,7 +87,7 @@ export class EcdheLoginService { } const pub = ECUtils.publicFromBase58DER(key); if (!pub) { - throw new AppException("INVALID_SIGNATURE"); + throw new AppException("INVALID_SIGNATURE", "Provided key is not a valid ECC public key"); } await this.nonceService.nonceCheck2P(Buffer.from("ecdhexlogin", "utf8"), pub, nonce, timestamp, signature); return this.loginUsingKey(key, solutionId, encoderType, plain); @@ -96,7 +96,7 @@ export class EcdheLoginService { async loginUsingKey(key: types.core.EccPubKey, solutionId: types.cloud.SolutionId|undefined, encoderType: EncoderType, plain?: boolean) { const keyExists = await this.repositoryFactory.createContextUserRepository().userPubKeyExists(key); if (!keyExists) { - throw new AppException("USER_DOESNT_EXIST"); + throw new AppException("USER_DOESNT_EXIST", "No user found for the given public key"); } const session = await this.sessionHolder.closeCurrentSessionAndCreateNewOne(undefined); diff --git a/src/service/login/KeyLoginService.ts b/src/service/login/KeyLoginService.ts index ce36671..5b72c5a 100644 --- a/src/service/login/KeyLoginService.ts +++ b/src/service/login/KeyLoginService.ts @@ -48,17 +48,17 @@ export class KeyLoginService { async init(out: {user?: string}, pub: types.core.EccPubKey, properties: types.user.LoginProperties): Promise { const user = await this.userLoginService.getKeyUser(pub); if (user == null) { - throw new AppException("USER_DOESNT_EXIST"); + throw new AppException("USER_DOESNT_EXIST", "User not found for the given public key"); } out.user = user.I; if (user.subidentity && user.subidentity.deviceIdRequired) { if (!user.subidentity.deviceId || properties.deviceId == null || user.subidentity.deviceId != properties.deviceId) { - throw new AppException("INVALID_DEVICE_ID"); + throw new AppException("INVALID_DEVICE_ID", "Device ID is required or does not match"); } } const proxy: types.core.Host|null = null; if (user.loginByProxy != null && (this.requestInfoHolder.serverSession == null || user.loginByProxy != this.requestInfoHolder.serverSession.host)) { - throw new AppException("INVALID_PROXY_SESSION"); + throw new AppException("INVALID_PROXY_SESSION", "Request does not originate from the required proxy host"); } const priv = ECUtils.generateRandom(); @@ -94,21 +94,21 @@ export class KeyLoginService { } catch (e) { this.logger.error(e); - throw new AppException("UNKNOWN_SESSION"); + throw new AppException("UNKNOWN_SESSION", "Session ID not found or could not be restored"); } if (session.get("state") != "keyInit") { - throw new AppException("INVALID_SESSION_STATE"); + throw new AppException("INVALID_SESSION_STATE", "Session is not in the expected 'keyInit' state"); } out.user = session.get("username"); const proxy = session.get("proxy"); if (proxy != null && (this.requestInfoHolder.serverSession == null || proxy != this.requestInfoHolder.serverSession.host)) { await this.sessionHolder.destroy(undefined, session); - throw new AppException("INVALID_PROXY_SESSION"); + throw new AppException("INVALID_PROXY_SESSION", "Request does not originate from the required proxy host"); } const keyLoginData = session.get("keyLogin"); const pub = ECUtils.publicFromBase58DER(keyLoginData.pub); if (!pub) { - throw new AppException("INVALID_SESSION_STATE"); + throw new AppException("INVALID_SESSION_STATE", "Session public key data is invalid or missing"); } try { await this.nonceService.nonceCheck2P(Buffer.from("login" + K, "utf8"), pub, nonce, timestamp, signature); @@ -120,7 +120,7 @@ export class KeyLoginService { const priv = ECUtils.fromWIF(keyLoginData.priv); if (!priv) { - throw new AppException("INVALID_SESSION_STATE"); + throw new AppException("INVALID_SESSION_STATE", "Session private key data is missing"); } const ecies = new ECIES(priv, pub); const newK = ecies.decrypt(Base64.toBuf(K)); diff --git a/src/service/login/SessionLoginService.ts b/src/service/login/SessionLoginService.ts index 9ddf542..4facc5c 100644 --- a/src/service/login/SessionLoginService.ts +++ b/src/service/login/SessionLoginService.ts @@ -36,25 +36,25 @@ export class SessionLoginService { private checkRestoreKeyInSession(session: Session, clientKey: elliptic.ec.KeyPair) { if (session.get("state") != "exchange") { - throw new AppException("UNKNOWN_SESSION"); + throw new AppException("UNKNOWN_SESSION", "Session state is not 'exchange'"); } const restoreKey = session.get("restoreKey"); const pub = ECUtils.publicToBase58DER(clientKey); if (pub != restoreKey) { - throw new AppException("UNKNOWN_SESSION"); + throw new AppException("UNKNOWN_SESSION", "Session restore key does not match"); } } private async getSession(sessionId: types.core.SessionId) { try { if (!sessionId) { - throw new AppException("UNKNOWN_SESSION"); + throw new AppException("UNKNOWN_SESSION", "No session ID provided"); } return await this.sessionHolder.closeCurrentSessionAndRestoreGiven(undefined, sessionId); } catch (e) { this.logger.error(e); - throw new AppException("UNKNOWN_SESSION"); + throw new AppException("UNKNOWN_SESSION", "Session ID not found or could not be restored"); } } } diff --git a/src/service/login/SrpLoginService.ts b/src/service/login/SrpLoginService.ts index 71cde1f..5baeaaf 100644 --- a/src/service/login/SrpLoginService.ts +++ b/src/service/login/SrpLoginService.ts @@ -70,21 +70,21 @@ export class SrpLoginService { async init(I: types.user.UserLogin, host: types.core.Host, properties: types.user.LoginProperties): Promise { this.logger.debug({I: I, host: host, properties: properties}, "init"); if (this.maintenanceService.isMaintenanceModeEnabled()) { - throw new AppException("MAINTENANCE_MODE"); + throw new AppException("MAINTENANCE_MODE", "Server is in maintenance mode"); } if (await this.loginLogService.detectAttack(this.requestInfoHolder.ip)) { await this.loginLogService.saveSrpLoginAttempt(null, I, false, "LOGIN_REJECTED", this.requestInfoHolder.ip, properties); await Utils.sleep(2000); - throw new AppException("LOGIN_REJECTED"); + throw new AppException("LOGIN_REJECTED", "Login attempt rejected due to too many failures"); } const user = await this.userLoginService.getSrpUser(I, host); if (user == null) { await this.loginLogService.saveSrpLoginAttempt(null, I, false, "USER_DOESNT_EXIST", this.requestInfoHolder.ip, properties); - throw new AppException("USER_DOESNT_EXIST"); + throw new AppException("USER_DOESNT_EXIST", "User not found"); } const proxy: types.core.Host|null = null; if (user.loginByProxy != null && (this.requestInfoHolder.serverSession == null || user.loginByProxy != this.requestInfoHolder.serverSession.host)) { - throw new AppException("INVALID_PROXY_SESSION"); + throw new AppException("INVALID_PROXY_SESSION", "Request does not originate from the required proxy host"); } const N = this.srpConfigService.config.N; @@ -135,10 +135,10 @@ export class SrpLoginService { } catch (e) { this.logger.error(e); - throw new AppException("UNKNOWN_SESSION"); + throw new AppException("UNKNOWN_SESSION", "Session ID not found or could not be restored"); } if (session.get("state") != "init") { - throw new AppException("INVALID_SESSION_STATE"); + throw new AppException("INVALID_SESSION_STATE", "Session is not in the expected 'init' state"); } const username = out.user = session.get("username"); const srpData = session.get("srp"); @@ -147,12 +147,12 @@ export class SrpLoginService { await this.sessionHolder.destroy(undefined, session); await this.loginLogService.saveSrpLoginAttempt(username, srpData.I, false, "LOGIN_REJECTED", this.requestInfoHolder.ip, properties); await Utils.sleep(2000); - throw new AppException("LOGIN_REJECTED"); + throw new AppException("LOGIN_REJECTED", "Login attempt rejected due to too many failures"); } const proxy = session.get("proxy"); if (proxy != null && (this.requestInfoHolder.serverSession == null || proxy != this.requestInfoHolder.serverSession.host)) { await this.sessionHolder.destroy(undefined, session); - throw new AppException("INVALID_PROXY_SESSION"); + throw new AppException("INVALID_PROXY_SESSION", "Request does not originate from the required proxy host"); } const N = this.deserializeBigInteger(srpData.N); // let g = this.deserializeBigInteger(srpData.g); @@ -162,7 +162,7 @@ export class SrpLoginService { const bigB = this.deserializeBigInteger(srpData.B); if (SrpLogic.valid_A(A, N) == false) { await this.sessionHolder.destroy(undefined, session); - throw new AppException("INVALID_A"); + throw new AppException("INVALID_A", "SRP parameter A is invalid"); } const u = SrpLogic.get_u(A, bigB, N); const S = SrpLogic.getServer_S(A, v, u, b, N); diff --git a/src/service/misc/NonceService.ts b/src/service/misc/NonceService.ts index c2e6b1f..04581d3 100644 --- a/src/service/misc/NonceService.ts +++ b/src/service/misc/NonceService.ts @@ -53,10 +53,10 @@ export class NonceService { async simpleNonceCheck(nonce: types.core.Nonce, timestamp: types.core.Timestamp) { if (!this.validateTimestamp(timestamp)) { - throw new AppException("INVALID_TIMESTAMP"); + throw new AppException("INVALID_TIMESTAMP", "Timestamp is outside the allowed window"); } if (nonce == null || nonce.length < 32 || nonce.length > 64 || !(await this.nonceIsUnique(nonce, timestamp))) { - throw new AppException("INVALID_NONCE"); + throw new AppException("INVALID_NONCE", "Nonce is missing, invalid, or already used"); } } @@ -64,14 +64,14 @@ export class NonceService { await this.simpleNonceCheck(nonce, timestamp); const message = this.getMessage(data, nonce, timestamp); if (!ECUtils.verifySignature(key, signature, message)) { - throw new AppException("INVALID_SIGNATURE"); + throw new AppException("INVALID_SIGNATURE", "Signature verification failed"); } } async nonceCheck2(data: Buffer, key: types.core.EccPubKey, nonce: types.core.Nonce, timestamp: types.core.Timestamp, signature: types.core.EccSignature) { const eccKey = ECUtils.publicFromBase58DER(key); if (!eccKey) { - throw new AppException("INVALID_SIGNATURE"); + throw new AppException("INVALID_SIGNATURE", "Provided key is not a valid ECC public key"); } await this.nonceCheck(data, eccKey, nonce, timestamp, Base64.toBuf(signature)); } diff --git a/src/service/request/RequestRepository.ts b/src/service/request/RequestRepository.ts index 6078d11..c4e6d3b 100644 --- a/src/service/request/RequestRepository.ts +++ b/src/service/request/RequestRepository.ts @@ -28,10 +28,10 @@ export class RequestRepository { async getWithAccessCheck(user: types.core.Username|types.core.EccPubKey, requestId: types.request.RequestId) { const request = await this.repository.get(requestId); if (!request) { - throw new AppException("REQUEST_DOES_NOT_EXIST"); + throw new AppException("REQUEST_DOES_NOT_EXIST", "Upload request does not exist"); } if (request.author !== user) { - throw new AppException("ACCESS_DENIED"); + throw new AppException("ACCESS_DENIED", "Upload request belongs to a different user"); } return request; } @@ -65,7 +65,7 @@ export class RequestRepository { const request = await this.getWithAccessCheck(user, requestId); for (const f of request.files) { if (!f.closed) { - throw new AppException("REQUEST_NOT_READY_YET"); + throw new AppException("REQUEST_NOT_READY_YET", "Upload request is not ready: some files are still pending"); } } return request; @@ -74,7 +74,7 @@ export class RequestRepository { async markRequestAsProcessing(requestId: types.request.RequestId) { const oldReq = await this.repository.get(requestId); if (!oldReq) { - throw new AppException("REQUEST_DOES_NOT_EXIST"); + throw new AppException("REQUEST_DOES_NOT_EXIST", "Upload request does not exist"); } const newReq: db.request.Request = { ...oldReq, diff --git a/src/service/ws/WebSocketConnectionManager.ts b/src/service/ws/WebSocketConnectionManager.ts index a22b97d..4609210 100644 --- a/src/service/ws/WebSocketConnectionManager.ts +++ b/src/service/ws/WebSocketConnectionManager.ts @@ -71,16 +71,16 @@ export class SimpleWebSocketConnectionManager implements WebSocketConnectionMana const wsId = session.getWsId(); const properties = session.get("properties"); if (wsEx.ex.sessions.some(x => x.wsId == wsId)) { - throw new AppException("WEBSOCKET_ALREADY_AUTHORIZED"); + throw new AppException("WEBSOCKET_ALREADY_AUTHORIZED", "This WebSocket session ID is already authorized"); } if (wsEx.ex.sessions.length > 1024) { - throw new AppException("EXCEEDED_LIMIT_OF_WEBSOCKET_CHANNELS"); + throw new AppException("EXCEEDED_LIMIT_OF_WEBSOCKET_CHANNELS", "Maximum number of WebSocket channels exceeded"); } if (wsEx.ex.sessions.length > 0 && !addWsChannelId) { - throw new AppException("ADD_WS_CHANNEL_ID_REQUIRED_ON_MULTI_CHANNEL_WEBSOCKET"); + throw new AppException("ADD_WS_CHANNEL_ID_REQUIRED_ON_MULTI_CHANNEL_WEBSOCKET", "A channel ID is required when adding to a multi-channel WebSocket"); } if (wsEx.ex.sessions.some(x => !x.addWsChannelId)) { - throw new AppException("CANNOT_ADD_CHANNEL_TO_SINGLE_CHANNEL_WEBSOCKET"); + throw new AppException("CANNOT_ADD_CHANNEL_TO_SINGLE_CHANNEL_WEBSOCKET", "Cannot add a channel to a single-channel WebSocket"); } const wsChannelId = this.generateWsChannelId(wsEx.ex.sessions); const username = session.get("username"); diff --git a/src/service/ws/WebSocketInnerManager.ts b/src/service/ws/WebSocketInnerManager.ts index 15b3ac3..48218cd 100644 --- a/src/service/ws/WebSocketInnerManager.ts +++ b/src/service/ws/WebSocketInnerManager.ts @@ -251,10 +251,10 @@ export class WebSocketInnerManager { subscribeToChannel(wsEx: WebSocketEx, wsId: types.core.WsId, channel: types.cloud.ChannelScheme) { const wsSession = wsEx.ex.sessions.find(x => x.wsId == wsId); if (!wsSession) { - throw new AppException("WS_SESSION_DOES_NOT_EXISTS"); + throw new AppException("WS_SESSION_DOES_NOT_EXISTS", "WebSocket session not found for the given ID"); } if (wsSession.channels.length >= this.config.maximumChannelsPerSession) { - throw new AppException("TOO_MANY_CHANNELS_IN_SESSION"); + throw new AppException("TOO_MANY_CHANNELS_IN_SESSION", "Maximum number of channels per session exceeded"); } wsSession.channels.push(channel); } @@ -262,7 +262,7 @@ export class WebSocketInnerManager { unsubscribeFromChannels(wsEx: WebSocketEx, wsId: types.core.WsId, subscriptionIds: types.core.SubscriptionId[]) { const wsSession = wsEx.ex.sessions.find(x => x.wsId == wsId); if (!wsSession) { - throw new AppException("WS_SESSION_DOES_NOT_EXISTS"); + throw new AppException("WS_SESSION_DOES_NOT_EXISTS", "WebSocket session not found for the given ID"); } const removeSet = new Set(subscriptionIds); wsSession.channels = wsSession.channels.filter(channel => !removeSet.has(channel.subscriptionId));