From dea08c69f0c88f0a0b256a1bd764f97b899a5b19 Mon Sep 17 00:00:00 2001 From: malteish Date: Tue, 16 Jul 2024 15:32:49 +0200 Subject: [PATCH 001/148] add createdAt field to Account model --- packages/backend/schema.prisma | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/backend/schema.prisma b/packages/backend/schema.prisma index 38bb48aaa..d4badaeb7 100644 --- a/packages/backend/schema.prisma +++ b/packages/backend/schema.prisma @@ -33,6 +33,7 @@ model Conversation { model Account { id String @id + createdAt DateTime @default(now()) conversations Conversation[] EncryptedMessage EncryptedMessage[] } From b0453de2be1105c1e886041b5a281470aaa6169c Mon Sep 17 00:00:00 2001 From: malteish Date: Wed, 17 Jul 2024 10:36:39 +0200 Subject: [PATCH 002/148] create accounts in prisma --- .../backend/src/persistence/getDatabase.ts | 20 +++---- .../backend/src/persistence/storage/index.ts | 4 ++ .../storage/postgres/getAccount.ts | 18 ++++++ .../storage/postgres/setAccount.ts | 6 ++ packages/backend/src/profile.ts | 55 ++++++++++++++----- packages/backend/src/storage.ts | 4 +- 6 files changed, 78 insertions(+), 29 deletions(-) create mode 100644 packages/backend/src/persistence/storage/postgres/getAccount.ts create mode 100644 packages/backend/src/persistence/storage/postgres/setAccount.ts diff --git a/packages/backend/src/persistence/getDatabase.ts b/packages/backend/src/persistence/getDatabase.ts index 54253708f..8a7e6263e 100644 --- a/packages/backend/src/persistence/getDatabase.ts +++ b/packages/backend/src/persistence/getDatabase.ts @@ -1,9 +1,8 @@ import { Session as DSSession, spamFilter } from '@dm3-org/dm3-lib-delivery'; import { IAccountDatabase } from '@dm3-org/dm3-lib-server-side'; import { UserStorage } from '@dm3-org/dm3-lib-storage'; -import { PrismaClient } from '@prisma/client'; +import { Account, PrismaClient } from '@prisma/client'; import { createClient } from 'redis'; -import Session from './session'; import Storage from './storage'; import { ConversationRecord } from './storage/postgres/dto/ConversationRecord'; import { MessageRecord } from './storage/postgres/dto/MessageRecord'; @@ -57,14 +56,14 @@ export async function getPrismaClient() { export async function getDatabase( _redis?: Redis, _prisma?: PrismaClient, -): Promise { +): Promise { const redis = _redis ?? (await getRedisClient()); const prisma = _prisma ?? (await getPrismaClient()); return { //Session - setAccount: Session.setAccount(redis), - getAccount: Session.getAccount(redis), + setAccount: Storage.setAccount(prisma), + getAccount: Storage.getAccount(prisma), //Legacy remove after storage has been merged getUserStorage: Storage.getUserStorageOld(redis), setUserStorage: Storage.setUserStorageOld(redis), @@ -94,14 +93,9 @@ export async function getDatabase( }; } -export interface IDatabase extends IAccountDatabase { - setAccount: (ensName: string, session: DSSession) => Promise; - getAccount: (ensName: string) => Promise< - | (DSSession & { - spamFilterRules: spamFilter.SpamFilterRules; - }) - | null - >; +export interface IBackendDatabase { + setAccount: (ensName: string) => Promise; + getAccount: (ensName: string) => Promise; //Legacy remove after storage has been merged getUserStorage: (ensName: string) => Promise; setUserStorage: (ensName: string, data: string) => Promise; diff --git a/packages/backend/src/persistence/storage/index.ts b/packages/backend/src/persistence/storage/index.ts index 3563533eb..9d618b503 100644 --- a/packages/backend/src/persistence/storage/index.ts +++ b/packages/backend/src/persistence/storage/index.ts @@ -13,6 +13,8 @@ import { getUserDbMigrationStatus } from './getUserDbMigrationStatus'; import { setUserDbMigrated } from './setUserDbMigrated'; import { getHaltedMessages } from './postgres/haltedMessage/getHaltedMessages'; import { clearHaltedMessage } from './postgres/haltedMessage/clearHaltedMessage'; +import { getAccount } from './postgres/getAccount'; +import { setAccount } from './postgres/setAccount'; export default { getUserStorageOld, @@ -29,6 +31,8 @@ export default { clearHaltedMessage, getUserDbMigrationStatus, setUserDbMigrated, + getAccount, + setAccount, }; export type { MessageRecord }; diff --git a/packages/backend/src/persistence/storage/postgres/getAccount.ts b/packages/backend/src/persistence/storage/postgres/getAccount.ts new file mode 100644 index 000000000..d3499e82b --- /dev/null +++ b/packages/backend/src/persistence/storage/postgres/getAccount.ts @@ -0,0 +1,18 @@ +import { PrismaClient } from '@prisma/client'; + +export const getAccount = (db: PrismaClient) => async (ensName: string) => { + // Find the account + const account = await db.account.findFirst({ + where: { + id: ensName, + }, + }); + + // Return the account if it exists + if (account) { + return account; + } + + // If the account does not exist, return null + return null; +}; diff --git a/packages/backend/src/persistence/storage/postgres/setAccount.ts b/packages/backend/src/persistence/storage/postgres/setAccount.ts new file mode 100644 index 000000000..3c4e7b659 --- /dev/null +++ b/packages/backend/src/persistence/storage/postgres/setAccount.ts @@ -0,0 +1,6 @@ +import { PrismaClient } from '@prisma/client'; +import { getOrCreateAccount } from './utils/getOrCreateAccount'; + +export const setAccount = (db: PrismaClient) => async (ensName: string) => { + return getOrCreateAccount(db, ensName); +}; diff --git a/packages/backend/src/profile.ts b/packages/backend/src/profile.ts index b15139ea4..d7e520b95 100644 --- a/packages/backend/src/profile.ts +++ b/packages/backend/src/profile.ts @@ -1,12 +1,16 @@ -import { getUserProfile, submitUserProfile } from '@dm3-org/dm3-lib-delivery'; -import { normalizeEnsName, schema } from '@dm3-org/dm3-lib-profile'; +import { getUserProfile, generateAuthJWT } from '@dm3-org/dm3-lib-delivery'; +import { + checkUserProfile, + normalizeEnsName, + schema, +} from '@dm3-org/dm3-lib-profile'; import { validateSchema } from '@dm3-org/dm3-lib-shared'; import { ethers } from 'ethers'; import express from 'express'; -import { IDatabase } from './persistence/getDatabase'; +import { IBackendDatabase } from './persistence/getDatabase'; export default ( - db: IDatabase, + db: IBackendDatabase, web3Provider: ethers.providers.JsonRpcProvider, serverSecret: string, ) => { @@ -52,21 +56,44 @@ export default ( process.env.DISABLE_SESSION_CHECK === 'true', }); - const data = await submitUserProfile( - web3Provider, - db.getAccount, - db.setAccount, - ensName, - req.body, - serverSecret, - ); + // check if profile and signature are valid + if ( + !(await checkUserProfile( + web3Provider, + req.body, // as SignedUserProfile, + normalizeEnsName(ensName), + )) + ) { + console.debug( + 'Not creating account for ' + + ensName + + ' - Signature invalid', + ); + throw Error('Signature invalid.'); + } + + // check if an account for this name already exists + if (await db.getAccount(ensName)) { + console.debug( + 'Not creating account for ' + + ensName + + ' - account exists already', + ); + throw Error('Account exists already'); + } + + // create account + const account = await db.setAccount(ensName); console.debug({ message: 'POST profile', ensName, - data, + account, }); - res.json(data); + // generate auth jwt + const token = generateAuthJWT(ensName, serverSecret); + + res.json(token); } catch (e) { console.warn({ message: 'POST profile', diff --git a/packages/backend/src/storage.ts b/packages/backend/src/storage.ts index 2c4a122d5..915e54858 100644 --- a/packages/backend/src/storage.ts +++ b/packages/backend/src/storage.ts @@ -5,7 +5,7 @@ import cors from 'cors'; import { ethers } from 'ethers'; import express, { NextFunction, Request, Response } from 'express'; import stringify from 'safe-stable-stringify'; -import { IDatabase } from './persistence/getDatabase'; +import { IBackendDatabase } from './persistence/getDatabase'; import { AddMessageBatchRequest } from './schema/storage/AddMessageBatchRequest'; import { AddMessageRequest } from './schema/storage/AddMesssageRequest'; import { EditMessageBatchRequest } from './schema/storage/EditMessageBatchRequest'; @@ -17,7 +17,7 @@ const DEFAULT_CONVERSATION_PAGE_SIZE = 10; const DEFAULT_MESSAGE_PAGE_SIZE = 100; export default ( - db: IDatabase, + db: IBackendDatabase, web3Provider: ethers.providers.JsonRpcProvider, serverSecret: string, ) => { From 175e9d63b8a5944be787c2faeb09ba314ddcba44 Mon Sep 17 00:00:00 2001 From: malteish Date: Wed, 17 Jul 2024 11:07:36 +0200 Subject: [PATCH 003/148] add doesAccountExist as a simplified way to ensure it does during auth --- packages/backend/src/persistence/getDatabase.ts | 4 +++- .../backend/src/persistence/storage/index.ts | 2 ++ .../storage/postgres/doesAccountExist.ts | 17 +++++++++++++++++ packages/backend/src/profile.ts | 6 ++++-- packages/backend/src/storage.ts | 10 +++++++++- packages/lib/delivery/src/Session.ts | 5 ++--- packages/lib/server-side/src/utils.ts | 4 ++-- 7 files changed, 39 insertions(+), 9 deletions(-) create mode 100644 packages/backend/src/persistence/storage/postgres/doesAccountExist.ts diff --git a/packages/backend/src/persistence/getDatabase.ts b/packages/backend/src/persistence/getDatabase.ts index 8a7e6263e..bf147c916 100644 --- a/packages/backend/src/persistence/getDatabase.ts +++ b/packages/backend/src/persistence/getDatabase.ts @@ -64,6 +64,7 @@ export async function getDatabase( //Session setAccount: Storage.setAccount(prisma), getAccount: Storage.getAccount(prisma), + doesAccountExist: Storage.doesAccountExist(prisma), //Legacy remove after storage has been merged getUserStorage: Storage.getUserStorageOld(redis), setUserStorage: Storage.setUserStorageOld(redis), @@ -94,8 +95,9 @@ export async function getDatabase( } export interface IBackendDatabase { - setAccount: (ensName: string) => Promise; + setAccount: (ensName: string) => Promise; getAccount: (ensName: string) => Promise; + doesAccountExist: (ensName: string) => Promise; //Legacy remove after storage has been merged getUserStorage: (ensName: string) => Promise; setUserStorage: (ensName: string, data: string) => Promise; diff --git a/packages/backend/src/persistence/storage/index.ts b/packages/backend/src/persistence/storage/index.ts index 9d618b503..5956543fe 100644 --- a/packages/backend/src/persistence/storage/index.ts +++ b/packages/backend/src/persistence/storage/index.ts @@ -15,6 +15,7 @@ import { getHaltedMessages } from './postgres/haltedMessage/getHaltedMessages'; import { clearHaltedMessage } from './postgres/haltedMessage/clearHaltedMessage'; import { getAccount } from './postgres/getAccount'; import { setAccount } from './postgres/setAccount'; +import { doesAccountExist } from './postgres/doesAccountExist'; export default { getUserStorageOld, @@ -33,6 +34,7 @@ export default { setUserDbMigrated, getAccount, setAccount, + doesAccountExist, }; export type { MessageRecord }; diff --git a/packages/backend/src/persistence/storage/postgres/doesAccountExist.ts b/packages/backend/src/persistence/storage/postgres/doesAccountExist.ts new file mode 100644 index 000000000..4c4382588 --- /dev/null +++ b/packages/backend/src/persistence/storage/postgres/doesAccountExist.ts @@ -0,0 +1,17 @@ +import { PrismaClient } from '@prisma/client'; + +export const doesAccountExist = + (db: PrismaClient) => async (ensName: string) => { + //Check if account exists + const account = await db.account.findFirst({ + where: { + id: ensName, + }, + }); + //If account exists, return true + if (account) { + return true; + } + //If account does not exist, return false + return false; + }; diff --git a/packages/backend/src/profile.ts b/packages/backend/src/profile.ts index d7e520b95..488ac7975 100644 --- a/packages/backend/src/profile.ts +++ b/packages/backend/src/profile.ts @@ -1,4 +1,6 @@ -import { getUserProfile, generateAuthJWT } from '@dm3-org/dm3-lib-delivery'; +import { generateAuthJWT } from '@dm3-org/dm3-lib-delivery'; +import { getUserProfile } from '@dm3-org/dm3-lib-profile'; + import { checkUserProfile, normalizeEnsName, @@ -20,7 +22,7 @@ export default ( try { const ensName = normalizeEnsName(req.params.ensName); - const profile = await getUserProfile(db.getAccount, ensName); + const profile = await getUserProfile(web3Provider, ensName); if (profile) { res.json(profile); } else { diff --git a/packages/backend/src/storage.ts b/packages/backend/src/storage.ts index 915e54858..bd50442e0 100644 --- a/packages/backend/src/storage.ts +++ b/packages/backend/src/storage.ts @@ -34,7 +34,15 @@ export default ( next: NextFunction, ensName: string, ) => { - auth(req, res, next, ensName, db, web3Provider, serverSecret); + auth( + req, + res, + next, + ensName, + db.doesAccountExist, + web3Provider, + serverSecret, + ); }, ); diff --git a/packages/lib/delivery/src/Session.ts b/packages/lib/delivery/src/Session.ts index cd7f2c646..db394c7bc 100644 --- a/packages/lib/delivery/src/Session.ts +++ b/packages/lib/delivery/src/Session.ts @@ -33,13 +33,12 @@ export interface Session { export async function checkToken( provider: ethers.providers.JsonRpcProvider, - getAccount: (ensName: string) => Promise, + doesAccountExist: (ensName: string) => Promise, ensName: string, token: string, serverSecret: string, ): Promise { - const session = await getAccount(ensName.toLocaleLowerCase()); - if (!session) { + if (!(await doesAccountExist(ensName.toLocaleLowerCase()))) { console.debug('there is no account for this ens name: ', ensName); return false; } diff --git a/packages/lib/server-side/src/utils.ts b/packages/lib/server-side/src/utils.ts index ed3e1782d..be1bb3d14 100644 --- a/packages/lib/server-side/src/utils.ts +++ b/packages/lib/server-side/src/utils.ts @@ -16,7 +16,7 @@ export async function auth( res: Response, next: NextFunction, ensName: string, - db: IAccountDatabase, + doesAccountExist: (ensName: string) => Promise, web3Provider: ethers.providers.JsonRpcProvider, serverSecret: string, ) { @@ -28,7 +28,7 @@ export async function auth( token && (await checkToken( web3Provider, - db.getAccount, + doesAccountExist, normalizedEnsName, token, serverSecret, From 9d3ed7778fa0422216aa22b5a1612629849d80cb Mon Sep 17 00:00:00 2001 From: malteish Date: Wed, 17 Jul 2024 11:21:09 +0200 Subject: [PATCH 004/148] add doesAccountExist to delivery-service --- packages/delivery-service/src/delivery.ts | 10 +++++++++- packages/delivery-service/src/notifications.ts | 10 +++++++++- .../src/persistence/account/doesAccountExist.ts | 13 +++++++++++++ .../src/persistence/account/index.ts | 3 ++- .../delivery-service/src/persistence/getDatabase.ts | 1 + .../delivery-service/src/ws/WebSocketManager.ts | 2 +- packages/lib/delivery/src/UserProfile.ts | 1 + packages/lib/server-side/src/iSessionDatabase.ts | 1 + packages/lib/server-side/src/utils.ts | 2 +- 9 files changed, 38 insertions(+), 5 deletions(-) create mode 100644 packages/delivery-service/src/persistence/account/doesAccountExist.ts diff --git a/packages/delivery-service/src/delivery.ts b/packages/delivery-service/src/delivery.ts index 6105edd95..33b6e8b37 100644 --- a/packages/delivery-service/src/delivery.ts +++ b/packages/delivery-service/src/delivery.ts @@ -39,7 +39,15 @@ export default ( //TODO remove router.use(cors()); router.param('ensName', async (req, res, next, ensName: string) => { - auth(req, res, next, ensName, db, web3Provider, serverSecret); + auth( + req, + res, + next, + ensName, + db.doesAccountExist, + web3Provider, + serverSecret, + ); }); //Returns all incoming messages for a specific contact name router.get( diff --git a/packages/delivery-service/src/notifications.ts b/packages/delivery-service/src/notifications.ts index 21bb4aaa8..b16f8a576 100644 --- a/packages/delivery-service/src/notifications.ts +++ b/packages/delivery-service/src/notifications.ts @@ -35,7 +35,15 @@ export default ( // Adding a route parameter middleware named 'ensName' router.param('ensName', (req, res, next, ensName: string) => { - auth(req, res, next, ensName, db, web3Provider, serverSecret); + auth( + req, + res, + next, + ensName, + db.doesAccountExist, + web3Provider, + serverSecret, + ); }); // Defining a route to enable/disable global notifications diff --git a/packages/delivery-service/src/persistence/account/doesAccountExist.ts b/packages/delivery-service/src/persistence/account/doesAccountExist.ts new file mode 100644 index 000000000..d03d1defc --- /dev/null +++ b/packages/delivery-service/src/persistence/account/doesAccountExist.ts @@ -0,0 +1,13 @@ +import { Redis, RedisPrefix } from '../getDatabase'; +import { getIdEnsName } from '../getIdEnsName'; + +export function doesAccountExist(redis: Redis) { + return async (ensName: string) => { + let account = await redis.get( + RedisPrefix.Account + (await getIdEnsName(redis)(ensName)), + ); + + // return true if account exists, false otherwise + return account ? true : false; + }; +} diff --git a/packages/delivery-service/src/persistence/account/index.ts b/packages/delivery-service/src/persistence/account/index.ts index 37cd75311..ee0a1b6cc 100644 --- a/packages/delivery-service/src/persistence/account/index.ts +++ b/packages/delivery-service/src/persistence/account/index.ts @@ -1,4 +1,5 @@ import { setAccount } from './setAccount'; import { getAccount } from './getAccount'; import { getIdEnsName } from '../getIdEnsName'; -export default { setAccount, getAccount, getIdEnsName }; +import { doesAccountExist } from './doesAccountExist'; +export default { setAccount, getAccount, getIdEnsName, doesAccountExist }; diff --git a/packages/delivery-service/src/persistence/getDatabase.ts b/packages/delivery-service/src/persistence/getDatabase.ts index 55a69eac2..60d10b14c 100644 --- a/packages/delivery-service/src/persistence/getDatabase.ts +++ b/packages/delivery-service/src/persistence/getDatabase.ts @@ -74,6 +74,7 @@ export async function getDatabase( // Account setAccount: Account.setAccount(redis), getAccount: Account.getAccount(redis), + doesAccountExist: Account.doesAccountExist(redis), getIdEnsName: getIdEnsName(redis), syncAcknowledge: syncAcknowledge(redis), //Notification diff --git a/packages/delivery-service/src/ws/WebSocketManager.ts b/packages/delivery-service/src/ws/WebSocketManager.ts index 948e8888f..f22171091 100644 --- a/packages/delivery-service/src/ws/WebSocketManager.ts +++ b/packages/delivery-service/src/ws/WebSocketManager.ts @@ -59,7 +59,7 @@ export class WebSocketManager implements IWebSocketManager { //Use the already existing function checkToken to check whether the token matches the provided ensName const hasSession = await checkToken( this.web3Provider, - this.db.getAccount, + this.db.doesAccountExist, ensName, token, this.serverSecret, diff --git a/packages/lib/delivery/src/UserProfile.ts b/packages/lib/delivery/src/UserProfile.ts index d323a4305..f20340805 100644 --- a/packages/lib/delivery/src/UserProfile.ts +++ b/packages/lib/delivery/src/UserProfile.ts @@ -40,6 +40,7 @@ export async function submitUserProfile( return session.token; } +// todo: remove this function (profiles should be loaded from chain and possibly cached) export async function getUserProfile( getAccount: (accountAddress: string) => Promise, ensName: string, diff --git a/packages/lib/server-side/src/iSessionDatabase.ts b/packages/lib/server-side/src/iSessionDatabase.ts index 422b9872f..2948dde32 100644 --- a/packages/lib/server-side/src/iSessionDatabase.ts +++ b/packages/lib/server-side/src/iSessionDatabase.ts @@ -9,4 +9,5 @@ export interface IAccountDatabase { }) | null >; + doesAccountExist: (ensName: string) => Promise; } diff --git a/packages/lib/server-side/src/utils.ts b/packages/lib/server-side/src/utils.ts index be1bb3d14..ceeef7ddb 100644 --- a/packages/lib/server-side/src/utils.ts +++ b/packages/lib/server-side/src/utils.ts @@ -59,7 +59,7 @@ export function socketAuth( if ( !(await checkToken( web3Provider, - db.getAccount, + db.doesAccountExist, ensName, socket.handshake.auth.token as string, serverSecret, From 18194a097379d77ad77cb86f595aa07f28372979 Mon Sep 17 00:00:00 2001 From: AlexNi245 Date: Thu, 18 Jul 2024 11:55:44 +0200 Subject: [PATCH 005/148] WUP refactor incoming message --- .../src/persistence/session/getSession.ts | 1 + packages/delivery-service/src/delivery.ts | 2 +- .../src/message/MessageProcessor.ts | 168 ++++++++++++++++ .../message}/spam-filter/SpamFilterRules.ts | 0 .../message}/spam-filter/filter/SpamFilter.ts | 0 .../ethBalanceFilter/EthBalanceFilter.test.ts | 0 .../ethBalanceFilter/EthBalanceFilter.ts | 0 .../filter/nonceFilter/NonceFilter.test.ts | 0 .../filter/nonceFilter/NonceFilter.ts | 0 .../filter/tokenBalanceFilter/Erc20Abi.json | 0 .../TokenBalanceFilter.test.ts | 0 .../tokenBalanceFilter/TokenBalanceFilter.ts | 0 .../src/message}/spam-filter/index.ts | 2 +- .../message}/spam-filter/spamfilter.test.ts | 3 +- packages/lib/delivery/src/Messages.ts | 185 ++---------------- packages/lib/delivery/src/Session.ts | 1 + packages/lib/delivery/src/UserProfile.ts | 2 + packages/lib/delivery/src/index.ts | 4 +- packages/lib/server-side/src/utils.ts | 2 +- 19 files changed, 195 insertions(+), 175 deletions(-) create mode 100644 packages/delivery-service/src/message/MessageProcessor.ts rename packages/{lib/delivery/src => delivery-service/src/message}/spam-filter/SpamFilterRules.ts (100%) rename packages/{lib/delivery/src => delivery-service/src/message}/spam-filter/filter/SpamFilter.ts (100%) rename packages/{lib/delivery/src => delivery-service/src/message}/spam-filter/filter/ethBalanceFilter/EthBalanceFilter.test.ts (100%) rename packages/{lib/delivery/src => delivery-service/src/message}/spam-filter/filter/ethBalanceFilter/EthBalanceFilter.ts (100%) rename packages/{lib/delivery/src => delivery-service/src/message}/spam-filter/filter/nonceFilter/NonceFilter.test.ts (100%) rename packages/{lib/delivery/src => delivery-service/src/message}/spam-filter/filter/nonceFilter/NonceFilter.ts (100%) rename packages/{lib/delivery/src => delivery-service/src/message}/spam-filter/filter/tokenBalanceFilter/Erc20Abi.json (100%) rename packages/{lib/delivery/src => delivery-service/src/message}/spam-filter/filter/tokenBalanceFilter/TokenBalanceFilter.test.ts (100%) rename packages/{lib/delivery/src => delivery-service/src/message}/spam-filter/filter/tokenBalanceFilter/TokenBalanceFilter.ts (100%) rename packages/{lib/delivery/src => delivery-service/src/message}/spam-filter/index.ts (97%) rename packages/{lib/delivery/src => delivery-service/src/message}/spam-filter/spamfilter.test.ts (98%) diff --git a/packages/backend/src/persistence/session/getSession.ts b/packages/backend/src/persistence/session/getSession.ts index 81d786f6b..116bf1f26 100644 --- a/packages/backend/src/persistence/session/getSession.ts +++ b/packages/backend/src/persistence/session/getSession.ts @@ -4,6 +4,7 @@ import { getIdEnsName } from '../getIdEnsName'; export function getSession(redis: Redis) { return async (ensName: string) => { + //TODO use addr let session = await redis.get( RedisPrefix.Session + (await getIdEnsName(redis)(ensName)), ); diff --git a/packages/delivery-service/src/delivery.ts b/packages/delivery-service/src/delivery.ts index 6105edd95..3ebb9f8ae 100644 --- a/packages/delivery-service/src/delivery.ts +++ b/packages/delivery-service/src/delivery.ts @@ -22,7 +22,7 @@ const syncAcknoledgmentBodySchema = { properties: { acknoledgments: { type: 'array', - items: schema.Acknoledgment, + items: schema.Acknowledgment, }, }, required: ['acknoledgments'], diff --git a/packages/delivery-service/src/message/MessageProcessor.ts b/packages/delivery-service/src/message/MessageProcessor.ts new file mode 100644 index 000000000..610cc8ade --- /dev/null +++ b/packages/delivery-service/src/message/MessageProcessor.ts @@ -0,0 +1,168 @@ +import { DeliveryServiceProfileKeys } from '@dm3-org/dm3-lib-profile'; +import { IWebSocketManager, stringify } from '@dm3-org/dm3-lib-shared'; +import { ethers } from 'ethers'; + +import { decryptAsymmetric, KeyPair } from '@dm3-org/dm3-lib-crypto'; +import { + addPostmark, + DeliveryServiceProperties, + getConversationId, + NotificationBroker, + NotificationType, +} from '@dm3-org/dm3-lib-delivery'; +import { + DeliveryInformation, + EncryptionEnvelop, + getEnvelopSize, +} from '@dm3-org/dm3-lib-messaging'; +import { logDebug } from '@dm3-org/dm3-lib-shared'; +import { IDatabase } from '../persistence/getDatabase'; +import { isSpam } from './spam-filter'; + +type onSubmitMessage = (socketId: string, envelop: EncryptionEnvelop) => void; + +export class MessageProcessor { + private readonly db: IDatabase; + private readonly provider: ethers.providers.JsonRpcProvider; + private readonly webSocketManager: IWebSocketManager; + private readonly deliveryServiceProperties: DeliveryServiceProperties; + private readonly deliveryServiceProfileKeys: DeliveryServiceProfileKeys; + private readonly onSubmitMessage: onSubmitMessage; + + constructor( + db: IDatabase, + provider: ethers.providers.JsonRpcProvider, + webSocketManager: IWebSocketManager, + deliveryServiceProperties: DeliveryServiceProperties, + deliveryServiceProfileKeys: DeliveryServiceProfileKeys, + onSubmitMessage: onSubmitMessage, + ) { + this.db = db; + this.provider = provider; + this.webSocketManager = webSocketManager; + this.deliveryServiceProperties = deliveryServiceProperties; + this.deliveryServiceProfileKeys = deliveryServiceProfileKeys; + this.onSubmitMessage = onSubmitMessage; + } + + /** + * Handles an incoming message. + * Either stores the message or sends it directly to the receiver if a socketId is provided + * In order to be considered valid a incoming message has to meet the following criterias + * 1. The message size must be lower than the sizeLimit specified by the deliveryService {@see messageIsToLarge} + * 2. The DeliveryServiceToken used by the sender has to be valid + * 3. The receiver has to have a session at the deliveryService + * 4. The message must pass every {@see SpamFilterRule} the receiver declared + */ + public async processEnvelop(envelop: EncryptionEnvelop): Promise { + logDebug('incomingMessage'); + //Checks the size of the incoming message + if ( + this.messageIsTooLarge( + envelop, + this.deliveryServiceProperties.sizeLimit, + ) + ) { + throw Error('Message is too large'); + } + + //Decrypts the encryptryInformation with the KeyPair of the deliveryService + + const deliveryInformation: DeliveryInformation = + await decryptDeliveryInformation( + envelop, + this.deliveryServiceProfileKeys.encryptionKeyPair, + ); + console.debug('incomingMessage', deliveryInformation); + + const conversationId = getConversationId( + //TODO look into dbIdEnsName + await this.db.getIdEnsName(deliveryInformation.from), + await this.db.getIdEnsName(deliveryInformation.to), + ); + console.debug(conversationId, deliveryInformation); + + //Retrieves the session of the receiver + const receiverSession = await this.db.getSession( + deliveryInformation.to, + ); + if (!receiverSession) { + console.debug('unknown user ', deliveryInformation.to); + throw Error('unknown session'); + } + + console.debug( + 'incomingMessage', + conversationId, + deliveryInformation, + receiverSession, + ); + + //Checks if the message is spam + if (await isSpam(this.provider, receiverSession, deliveryInformation)) { + console.debug( + `incomingMessage fro ${deliveryInformation.to} is spam`, + ); + throw Error('Message does not match spam criteria'); + } + + const receiverEncryptionKey = + receiverSession.signedUserProfile.profile.publicEncryptionKey; + + const envelopWithPostmark: EncryptionEnvelop = { + ...envelop, + metadata: { + ...envelop.metadata, + //Alwaays store the encrypted metadata + deliveryInformation, + }, + postmark: stringify( + await addPostmark( + envelop, + receiverEncryptionKey, + this.deliveryServiceProfileKeys.signingKeyPair.privateKey, + ), + ), + }; + + if (process.env.DISABLE_MSG_BUFFER !== 'true') { + console.debug('storeNewMessage', conversationId); + await this.db.createMessage(conversationId, envelopWithPostmark); + } else { + console.debug('skip storeNewMessage', conversationId); + } + + //If there is currently a webSocket connection open to the receiver, the message will be directly send. + if (await this.webSocketManager.isConnected(deliveryInformation.to)) { + //Client is already connect to the delivery service and the message can be dispatched + //TODO MOVE send method to the WebSocketManager + this.onSubmitMessage( + receiverSession.socketId!, + envelopWithPostmark, + ); + + console.debug('WS send to socketId ', receiverSession.socketId); + //If not we're notifing the user that there is a new message waiting for them + } else { + try { + const { sendNotification } = NotificationBroker( + this.deliveryServiceProperties.notificationChannel, + NotificationType.NEW_MESSAGE, + ); + await sendNotification( + deliveryInformation, + this.db.getUsersNotificationChannels, + ); + } catch (err) { + console.log( + 'Unable to send Notification. There might be an error in the config.yml. Message has been received regardless', + ); + console.error(err); + } + } + } + + private messageIsTooLarge(envelop: EncryptionEnvelop, sizeLimit: number) { + return getEnvelopSize(envelop) > sizeLimit; + } +} diff --git a/packages/lib/delivery/src/spam-filter/SpamFilterRules.ts b/packages/delivery-service/src/message/spam-filter/SpamFilterRules.ts similarity index 100% rename from packages/lib/delivery/src/spam-filter/SpamFilterRules.ts rename to packages/delivery-service/src/message/spam-filter/SpamFilterRules.ts diff --git a/packages/lib/delivery/src/spam-filter/filter/SpamFilter.ts b/packages/delivery-service/src/message/spam-filter/filter/SpamFilter.ts similarity index 100% rename from packages/lib/delivery/src/spam-filter/filter/SpamFilter.ts rename to packages/delivery-service/src/message/spam-filter/filter/SpamFilter.ts diff --git a/packages/lib/delivery/src/spam-filter/filter/ethBalanceFilter/EthBalanceFilter.test.ts b/packages/delivery-service/src/message/spam-filter/filter/ethBalanceFilter/EthBalanceFilter.test.ts similarity index 100% rename from packages/lib/delivery/src/spam-filter/filter/ethBalanceFilter/EthBalanceFilter.test.ts rename to packages/delivery-service/src/message/spam-filter/filter/ethBalanceFilter/EthBalanceFilter.test.ts diff --git a/packages/lib/delivery/src/spam-filter/filter/ethBalanceFilter/EthBalanceFilter.ts b/packages/delivery-service/src/message/spam-filter/filter/ethBalanceFilter/EthBalanceFilter.ts similarity index 100% rename from packages/lib/delivery/src/spam-filter/filter/ethBalanceFilter/EthBalanceFilter.ts rename to packages/delivery-service/src/message/spam-filter/filter/ethBalanceFilter/EthBalanceFilter.ts diff --git a/packages/lib/delivery/src/spam-filter/filter/nonceFilter/NonceFilter.test.ts b/packages/delivery-service/src/message/spam-filter/filter/nonceFilter/NonceFilter.test.ts similarity index 100% rename from packages/lib/delivery/src/spam-filter/filter/nonceFilter/NonceFilter.test.ts rename to packages/delivery-service/src/message/spam-filter/filter/nonceFilter/NonceFilter.test.ts diff --git a/packages/lib/delivery/src/spam-filter/filter/nonceFilter/NonceFilter.ts b/packages/delivery-service/src/message/spam-filter/filter/nonceFilter/NonceFilter.ts similarity index 100% rename from packages/lib/delivery/src/spam-filter/filter/nonceFilter/NonceFilter.ts rename to packages/delivery-service/src/message/spam-filter/filter/nonceFilter/NonceFilter.ts diff --git a/packages/lib/delivery/src/spam-filter/filter/tokenBalanceFilter/Erc20Abi.json b/packages/delivery-service/src/message/spam-filter/filter/tokenBalanceFilter/Erc20Abi.json similarity index 100% rename from packages/lib/delivery/src/spam-filter/filter/tokenBalanceFilter/Erc20Abi.json rename to packages/delivery-service/src/message/spam-filter/filter/tokenBalanceFilter/Erc20Abi.json diff --git a/packages/lib/delivery/src/spam-filter/filter/tokenBalanceFilter/TokenBalanceFilter.test.ts b/packages/delivery-service/src/message/spam-filter/filter/tokenBalanceFilter/TokenBalanceFilter.test.ts similarity index 100% rename from packages/lib/delivery/src/spam-filter/filter/tokenBalanceFilter/TokenBalanceFilter.test.ts rename to packages/delivery-service/src/message/spam-filter/filter/tokenBalanceFilter/TokenBalanceFilter.test.ts diff --git a/packages/lib/delivery/src/spam-filter/filter/tokenBalanceFilter/TokenBalanceFilter.ts b/packages/delivery-service/src/message/spam-filter/filter/tokenBalanceFilter/TokenBalanceFilter.ts similarity index 100% rename from packages/lib/delivery/src/spam-filter/filter/tokenBalanceFilter/TokenBalanceFilter.ts rename to packages/delivery-service/src/message/spam-filter/filter/tokenBalanceFilter/TokenBalanceFilter.ts diff --git a/packages/lib/delivery/src/spam-filter/index.ts b/packages/delivery-service/src/message/spam-filter/index.ts similarity index 97% rename from packages/lib/delivery/src/spam-filter/index.ts rename to packages/delivery-service/src/message/spam-filter/index.ts index 768c4076c..417d23554 100644 --- a/packages/lib/delivery/src/spam-filter/index.ts +++ b/packages/delivery-service/src/message/spam-filter/index.ts @@ -5,7 +5,7 @@ import { nonceFilterFactory } from './filter/nonceFilter/NonceFilter'; import { SpamFilter, SpamFilterFactory } from './filter/SpamFilter'; import { tokenBalanceFilterFactory } from './filter/tokenBalanceFilter/TokenBalanceFilter'; import { SpamFilterRules } from './SpamFilterRules'; -import { Session } from '../Session'; +import { Session } from '@dm3-org/dm3-lib-delivery/src/Session'; export type { SpamFilterRules }; diff --git a/packages/lib/delivery/src/spam-filter/spamfilter.test.ts b/packages/delivery-service/src/message/spam-filter/spamfilter.test.ts similarity index 98% rename from packages/lib/delivery/src/spam-filter/spamfilter.test.ts rename to packages/delivery-service/src/message/spam-filter/spamfilter.test.ts index 386ea6596..bc1cb7afe 100644 --- a/packages/lib/delivery/src/spam-filter/spamfilter.test.ts +++ b/packages/delivery-service/src/message/spam-filter/spamfilter.test.ts @@ -3,9 +3,8 @@ import { ethers } from 'ethers'; import { isSpam } from '.'; import { testData } from '../../../../../test-data/encrypted-envelops.test'; -import { Session } from '../Session'; - import { SpamFilterRules } from './SpamFilterRules'; +import { Session } from '@dm3-org/dm3-lib-delivery'; const keysA = { encryptionKeyPair: { diff --git a/packages/lib/delivery/src/Messages.ts b/packages/lib/delivery/src/Messages.ts index c0e595ddd..6af92fcc8 100644 --- a/packages/lib/delivery/src/Messages.ts +++ b/packages/lib/delivery/src/Messages.ts @@ -3,11 +3,7 @@ import { normalizeEnsName, UserProfile, } from '@dm3-org/dm3-lib-profile'; -import { - IWebSocketManager, - NotificationChannel, - stringify, -} from '@dm3-org/dm3-lib-shared'; +import { stringify } from '@dm3-org/dm3-lib-shared'; import { ethers } from 'ethers'; import { @@ -20,18 +16,9 @@ import { import { DeliveryInformation, EncryptionEnvelop, - getEnvelopSize, Postmark, } from '@dm3-org/dm3-lib-messaging'; -import { logDebug, sha256 } from '@dm3-org/dm3-lib-shared'; -import { NotificationBroker } from './notifications'; -import { - GetNotificationChannels, - NotificationType, -} from './notifications/types'; -import { Session } from './Session'; -import { isSpam } from './spam-filter'; -import { SpamFilterRules } from './spam-filter/SpamFilterRules'; +import { sha256 } from '@dm3-org/dm3-lib-shared'; export interface Acknoledgment { contactAddress: string; @@ -81,142 +68,17 @@ export async function getMessages( .map((envelopContainer) => envelopContainer.envelop); } -/** - * Handles an incoming message. - * Either stores the message or sends it directly to the receiver if a socketId is provided - * In order to be considered valid a incoming message has to meet the following criterias - * 1. The message size must be lower than the sizeLimit specified by the deliveryService {@see messageIsToLarge} - * 2. The DeliveryServiceToken used by the sender has to be valid - * 3. The receiver has to have a session at the deliveryService - * 4. The message must pass every {@see SpamFilterRule} the receiver declared - */ -export async function incomingMessage( - envelop: EncryptionEnvelop, - signingKeyPair: KeyPair, +export async function decryptDeliveryInformation( + encryptedEnvelop: EncryptionEnvelop, encryptionKeyPair: KeyPair, - sizeLimit: number, - dsNotificationChannels: NotificationChannel[], - getSession: ( - accountAddress: string, - ) => Promise<(Session & { spamFilterRules: SpamFilterRules }) | null>, - storeNewMessage: ( - conversationId: string, - envelop: EncryptionEnvelop, - ) => Promise, - send: (socketId: string, envelop: EncryptionEnvelop) => void, - provider: ethers.providers.JsonRpcProvider, - getIdEnsName: (name: string) => Promise, - getUsersNotificationChannels: GetNotificationChannels, - wsManager: IWebSocketManager, -): Promise { - logDebug('incomingMessage'); - //Checks the size of the incoming message - if (messageIsTooLarge(envelop, sizeLimit)) { - throw Error('Message is too large'); - } - - //Decrypts the encrypted DeliveryInformation with the KeyPair of the deliveryService - - const deliveryInformation: DeliveryInformation = - await decryptDeliveryInformation(envelop, encryptionKeyPair); - logDebug({ text: 'incomingMessage', deliveryInformation }); - - const conversationId = getConversationId( - await getIdEnsName(deliveryInformation.from), - await getIdEnsName(deliveryInformation.to), - ); - logDebug({ text: 'incomingMessage', conversationId, deliveryInformation }); - - //Retrieves the session of the receiver - const receiverSession = await getSession(deliveryInformation.to); - if (!receiverSession) { - logDebug({ - text: 'incomingMessage unknown session', - }); - throw Error('unknown session'); - } - logDebug({ - text: 'incomingMessage', - conversationId, - deliveryInformation, - receiverSession, - }); - - //Checks if the message is spam - if (await isSpam(provider, receiverSession, deliveryInformation)) { - logDebug({ - text: 'incomingMessage is spam', - }); - throw Error('Message does not match spam criteria'); - } - - const receiverEncryptionKey = - receiverSession.signedUserProfile.profile.publicEncryptionKey; - - const envelopWithPostmark: EncryptionEnvelop = { - ...envelop, - metadata: { - ...envelop.metadata, - //Alwaays store the encrypted metadata - deliveryInformation, - }, - postmark: stringify( - await addPostmark( - envelop, - receiverEncryptionKey, - signingKeyPair.privateKey, - ), +): Promise { + return JSON.parse( + await decryptAsymmetric( + encryptionKeyPair, + JSON.parse(encryptedEnvelop.metadata.deliveryInformation as string), ), - }; - logDebug({ - text: 'incomingMessage', - conversationId, - envelopWithPostmark, - }); - - if (process.env.DISABLE_MSG_BUFFER !== 'true') { - logDebug({ text: 'storeNewMessage', conversationId }); - await storeNewMessage(conversationId, envelopWithPostmark); - } else { - logDebug({ text: 'skip storeNewMessage', conversationId }); - } - - //If there is currently a webSocket connection open to the receiver, the message will be directly send. - if (await wsManager.isConnected(deliveryInformation.to)) { - //Client is already connect to the delivery service and the message can be dispatched - //TODO MOVE send method to the WebSocketManager - send(receiverSession.socketId!, envelopWithPostmark); - logDebug({ - text: 'WS send to socketId ', - receiverSessionSocketId: receiverSession.socketId, - }); - //If not we're notifing the user that there is a new message waiting for them - } else { - try { - const { sendNotification } = NotificationBroker( - dsNotificationChannels, - NotificationType.NEW_MESSAGE, - ); - await sendNotification( - deliveryInformation, - getUsersNotificationChannels, - ); - } catch (err) { - console.log( - 'Unable to send Notification. There might be an error in the config.yml. Message has been received regardless', - ); - console.error(err); - } - } -} - -function messageIsTooLarge( - envelop: EncryptionEnvelop, - sizeLimit: number, -): boolean { - return getEnvelopSize(envelop) > sizeLimit; + ); } - export async function handleIncomingMessage( encryptedEnvelop: EncryptionEnvelop, deliveryServiceKeys: DeliveryServiceProfileKeys, @@ -247,6 +109,13 @@ export async function addPostmark( receiverEncryptionKey: string, deliveryServiceSigningKey: string, ): Promise { + function signPostmark( + p: Omit, + signingKey: string, + ): Promise { + const postmarkHash = sha256(stringify(p)); + return sign(signingKey, postmarkHash); + } const postmarkWithoutSig: Omit = { messageHash: ethers.utils.hashMessage(stringify(message)), incommingTimestamp: new Date().getTime(), @@ -269,23 +138,3 @@ export async function addPostmark( ephemPublicKey, }; } - -export async function decryptDeliveryInformation( - encryptedEnvelop: EncryptionEnvelop, - encryptionKeyPair: KeyPair, -): Promise { - return JSON.parse( - await decryptAsymmetric( - encryptionKeyPair, - JSON.parse(encryptedEnvelop.metadata.deliveryInformation as string), - ), - ); -} - -function signPostmark( - p: Omit, - signingKey: string, -): Promise { - const postmarkHash = sha256(stringify(p)); - return sign(signingKey, postmarkHash); -} diff --git a/packages/lib/delivery/src/Session.ts b/packages/lib/delivery/src/Session.ts index 291cb6b5f..e69ab6d2c 100644 --- a/packages/lib/delivery/src/Session.ts +++ b/packages/lib/delivery/src/Session.ts @@ -38,6 +38,7 @@ export async function checkToken( token: string, serverSecret: string, ): Promise { + // const session = await getSession(ensName.toLocaleLowerCase()); if (!session) { console.debug('there is no account for this ens name: ', ensName); diff --git a/packages/lib/delivery/src/UserProfile.ts b/packages/lib/delivery/src/UserProfile.ts index 7abfa0670..56d78ab73 100644 --- a/packages/lib/delivery/src/UserProfile.ts +++ b/packages/lib/delivery/src/UserProfile.ts @@ -35,6 +35,8 @@ export async function submitUserProfile( profileExtension: getDefaultProfileExtension(), }; logDebug({ text: 'submitUserProfile', session }); + + //TODO use addr await setSession(account.toLocaleLowerCase(), session); return session.token; diff --git a/packages/lib/delivery/src/index.ts b/packages/lib/delivery/src/index.ts index 624f242af..9afed2ead 100644 --- a/packages/lib/delivery/src/index.ts +++ b/packages/lib/delivery/src/index.ts @@ -7,14 +7,14 @@ export { submitUserProfile, getUserProfile } from './UserProfile'; export { addPostmark, getMessages, - incomingMessage, + decryptDeliveryInformation, handleIncomingMessage, } from './Messages'; export type { Acknoledgment } from './Messages'; export { getConversationId } from './Messages'; export type {} from './PublicMessages'; export * as schema from './schema'; -export * as spamFilter from './spam-filter'; +export * as spamFilter from '../../../delivery-service/src/message/spam-filter'; export { checkToken } from './Session'; export type { Session } from './Session'; export type { DeliveryServiceProperties } from './Delivery'; diff --git a/packages/lib/server-side/src/utils.ts b/packages/lib/server-side/src/utils.ts index 85012b323..e4e68e66f 100644 --- a/packages/lib/server-side/src/utils.ts +++ b/packages/lib/server-side/src/utils.ts @@ -23,7 +23,7 @@ export async function auth( const normalizedEnsName = normalizeEnsName(ensName); const authHeader = req.headers['authorization']; const token = authHeader && authHeader.split(' ')[1]; - + //TODO resolve addr for ens name if ( token && (await checkToken( From 85b17c80418799764c866b45c3ad29f25d276574 Mon Sep 17 00:00:00 2001 From: AlexNi245 Date: Thu, 18 Jul 2024 15:53:59 +0200 Subject: [PATCH 006/148] migrate incomingMessage to MessageProcessor --- packages/delivery-service/package.json | 2 +- packages/delivery-service/src/delivery.ts | 2 +- .../src/message/MessageProcessor.test.ts | 665 ++++++++++++++++++ .../src/message/MessageProcessor.ts | 19 +- packages/delivery-service/tsconfig.json | 2 +- packages/lib/delivery/src/Messages.test.ts | 624 +--------------- packages/lib/delivery/src/index.ts | 2 +- .../src}/spam-filter/SpamFilterRules.ts | 0 .../src}/spam-filter/filter/SpamFilter.ts | 0 .../ethBalanceFilter/EthBalanceFilter.test.ts | 0 .../ethBalanceFilter/EthBalanceFilter.ts | 0 .../filter/nonceFilter/NonceFilter.test.ts | 0 .../filter/nonceFilter/NonceFilter.ts | 0 .../filter/tokenBalanceFilter/Erc20Abi.json | 0 .../TokenBalanceFilter.test.ts | 0 .../tokenBalanceFilter/TokenBalanceFilter.ts | 0 .../delivery/src}/spam-filter/index.ts | 0 .../src}/spam-filter/spamfilter.test.ts | 0 18 files changed, 679 insertions(+), 637 deletions(-) create mode 100644 packages/delivery-service/src/message/MessageProcessor.test.ts rename packages/{delivery-service/src/message => lib/delivery/src}/spam-filter/SpamFilterRules.ts (100%) rename packages/{delivery-service/src/message => lib/delivery/src}/spam-filter/filter/SpamFilter.ts (100%) rename packages/{delivery-service/src/message => lib/delivery/src}/spam-filter/filter/ethBalanceFilter/EthBalanceFilter.test.ts (100%) rename packages/{delivery-service/src/message => lib/delivery/src}/spam-filter/filter/ethBalanceFilter/EthBalanceFilter.ts (100%) rename packages/{delivery-service/src/message => lib/delivery/src}/spam-filter/filter/nonceFilter/NonceFilter.test.ts (100%) rename packages/{delivery-service/src/message => lib/delivery/src}/spam-filter/filter/nonceFilter/NonceFilter.ts (100%) rename packages/{delivery-service/src/message => lib/delivery/src}/spam-filter/filter/tokenBalanceFilter/Erc20Abi.json (100%) rename packages/{delivery-service/src/message => lib/delivery/src}/spam-filter/filter/tokenBalanceFilter/TokenBalanceFilter.test.ts (100%) rename packages/{delivery-service/src/message => lib/delivery/src}/spam-filter/filter/tokenBalanceFilter/TokenBalanceFilter.ts (100%) rename packages/{delivery-service/src/message => lib/delivery/src}/spam-filter/index.ts (100%) rename packages/{delivery-service/src/message => lib/delivery/src}/spam-filter/spamfilter.test.ts (100%) diff --git a/packages/delivery-service/package.json b/packages/delivery-service/package.json index 2b53ef05e..6a66ef94a 100644 --- a/packages/delivery-service/package.json +++ b/packages/delivery-service/package.json @@ -27,7 +27,7 @@ "docker:up": "docker-compose up -d", "start": "node ./dist/index.js", "start-inspect": "node --inspect=0.0.0.0:9229 ./dist/index.js", - "test": "yarn run before:tests && jest --coverage --runInBand --transformIgnorePatterns 'node_modules/(?!(dm3-lib-\\w*)/)' && yarn run after:tests", + "test": "yarn run before:tests && jest --coverage --runInBand --transformIgnorePatterns 'node_modules/(?!(dm3-lib-\\w*)/)' ", "build": "yarn tsc ", "createDeliveryServiceProfile": "node --no-warnings ./cli.js", "before:tests": "docker-compose -f docker-compose.test.yml up -d", diff --git a/packages/delivery-service/src/delivery.ts b/packages/delivery-service/src/delivery.ts index 3ebb9f8ae..6105edd95 100644 --- a/packages/delivery-service/src/delivery.ts +++ b/packages/delivery-service/src/delivery.ts @@ -22,7 +22,7 @@ const syncAcknoledgmentBodySchema = { properties: { acknoledgments: { type: 'array', - items: schema.Acknowledgment, + items: schema.Acknoledgment, }, }, required: ['acknoledgments'], diff --git a/packages/delivery-service/src/message/MessageProcessor.test.ts b/packages/delivery-service/src/message/MessageProcessor.test.ts new file mode 100644 index 000000000..b677c21b6 --- /dev/null +++ b/packages/delivery-service/src/message/MessageProcessor.test.ts @@ -0,0 +1,665 @@ +import { EncryptionEnvelop, Postmark } from '@dm3-org/dm3-lib-messaging'; +import { + IWebSocketManager, + NotificationChannel, + NotificationChannelType, + sha256, + stringify, +} from '@dm3-org/dm3-lib-shared'; +import { BigNumber, ethers } from 'ethers'; +import { MessageProcessor } from './MessageProcessor'; + +import { + DeliveryServiceProperties, + Session, + getConversationId, + spamFilter, +} from '@dm3-org/dm3-lib-delivery'; +import { UserProfile, normalizeEnsName } from '@dm3-org/dm3-lib-profile'; +import { + MockDeliveryServiceProfile, + MockMessageFactory, + MockedUserProfile, + getMockDeliveryServiceProfile, + mockUserProfile, +} from '@dm3-org/dm3-lib-test-helper'; +import { IDatabase } from '../persistence/getDatabase'; +import { checkSignature, decryptAsymmetric } from '@dm3-org/dm3-lib-crypto'; +import exp from 'constants'; + +jest.mock('nodemailer'); + +describe('MessageProcessor', () => { + let sender: MockedUserProfile; + let receiver: MockedUserProfile; + let rando: MockedUserProfile; + + let ds: MockDeliveryServiceProfile; + + beforeEach(async () => { + sender = await mockUserProfile( + ethers.Wallet.createRandom(), + 'bob.eth', + ['http://localhost:3000'], + ); + receiver = await mockUserProfile( + ethers.Wallet.createRandom(), + 'alice.eth', + ['http://localhost:3000'], + ); + rando = await mockUserProfile( + ethers.Wallet.createRandom(), + 'rando.eth', + ['http://localhost:3000'], + ); + + ds = await getMockDeliveryServiceProfile( + ethers.Wallet.createRandom(), + 'http://localhost:3000', + ); + }); + const getSession = async ( + ensName: string, + socketId?: string, + ): Promise< + (Session & { spamFilterRules: spamFilter.SpamFilterRules }) | null + > => { + const emptyProfile: UserProfile = { + publicSigningKey: '', + publicEncryptionKey: '', + deliveryServices: [''], + }; + const isSender = normalizeEnsName(ensName) === sender.account.ensName; + const isReceiver = + normalizeEnsName(ensName) === receiver.account.ensName; + + const session = ( + account: string, + token: string, + profile: UserProfile, + ): Session => ({ + account, + signedUserProfile: { + profile, + signature: '', + }, + token, + createdAt: new Date().getTime(), + profileExtension: { + encryptionAlgorithm: [], + notSupportedMessageTypes: [], + }, + socketId, + }); + + if (isSender) { + return { + ...session(sender.account.ensName, '123', emptyProfile), + spamFilterRules: {}, + }; + } + + if (isReceiver) { + return { + ...session(receiver.account.ensName, 'abc', { + ...emptyProfile, + publicEncryptionKey: + receiver.profileKeys.encryptionKeyPair.publicKey, + }), + spamFilterRules: {}, + }; + } + + return null; + }; + + it('accepts an incoming message', async () => { + const db = { + createMessage: async () => {}, + getIdEnsName: () => '', + getSession, + getUsersNotificationChannels: () => Promise.resolve([]), + } as any as IDatabase; + + const web3Provider = { + resolveName: async () => + '0x25A643B6e52864d0eD816F1E43c0CF49C83B8292', + } as any; + + const mockWsManager: IWebSocketManager = { + isConnected: function (ensName: string): Promise { + return Promise.resolve(false); + }, + }; + + const deliveryServiceProperties: DeliveryServiceProperties = { + sizeLimit: 2 ** 14, + messageTTL: 1000, + notificationChannel: [], + }; + + const messageProcessor = new MessageProcessor( + db, + web3Provider, + mockWsManager, + deliveryServiceProperties, + ds.keys, + () => {}, + ); + + const incomingEnvelop: EncryptionEnvelop = await MockMessageFactory( + sender, + receiver, + ds, + ).createEncryptedEnvelop('hello dm3'); + + await expect(() => + messageProcessor.processEnvelop(incomingEnvelop), + ).not.toThrow(); + }); + + it('rejects an incoming message if it is to large', async () => { + const db = { + createMessage: async () => {}, + getIdEnsName: () => '', + getSession, + getUsersNotificationChannels: () => Promise.resolve([]), + } as any as IDatabase; + + const web3Provider = { + resolveName: async () => + '0x25A643B6e52864d0eD816F1E43c0CF49C83B8292', + } as any; + + const mockWsManager: IWebSocketManager = { + isConnected: function (ensName: string): Promise { + return Promise.resolve(false); + }, + }; + + const deliveryServiceProperties: DeliveryServiceProperties = { + sizeLimit: 1, + messageTTL: 1000, + notificationChannel: [], + }; + + const messageProcessor = new MessageProcessor( + db, + web3Provider, + mockWsManager, + deliveryServiceProperties, + ds.keys, + () => {}, + ); + + const incomingEnvelop: EncryptionEnvelop = await MockMessageFactory( + sender, + receiver, + ds, + ).createEncryptedEnvelop('hello dm3'); + + await expect(() => + messageProcessor.processEnvelop(incomingEnvelop), + ).rejects.toEqual(Error('Message is too large')); + }); + it('rejects an incoming message if the receiver is unknown ', async () => { + const db = { + createMessage: async () => {}, + getIdEnsName: () => '', + getSession, + getUsersNotificationChannels: () => Promise.resolve([]), + } as any as IDatabase; + + const web3Provider = { + resolveName: async () => + '0x25A643B6e52864d0eD816F1E43c0CF49C83B8292', + } as any; + + const mockWsManager: IWebSocketManager = { + isConnected: function (ensName: string): Promise { + return Promise.resolve(false); + }, + }; + + const deliveryServiceProperties: DeliveryServiceProperties = { + sizeLimit: 2 ** 14, + messageTTL: 1000, + notificationChannel: [], + }; + + const messageProcessor = new MessageProcessor( + db, + web3Provider, + mockWsManager, + deliveryServiceProperties, + ds.keys, + () => {}, + ); + + const incomingEnvelop: EncryptionEnvelop = await MockMessageFactory( + sender, + rando, + ds, + ).createEncryptedEnvelop('hello rando'); + + await expect(() => + messageProcessor.processEnvelop(incomingEnvelop), + ).rejects.toEqual(Error('unknown session')); + }); + // //TODO remove skip once spam-filter is implemented + // //TODO remove skip once spam-filter is implemented + it.skip('rejects message if the senders nonce is below the threshold', async () => { + const _getSession = async (address: string) => + ({ + ...(await getSession(address)), + spamFilterRules: { minNonce: 2 }, + } as Session & { spamFilterRules: spamFilter.SpamFilterRules }); + + const db = { + createMessage: async () => {}, + getIdEnsName: () => '', + getSession: _getSession, + getUsersNotificationChannels: () => Promise.resolve([]), + } as any as IDatabase; + + const web3Provider = { + getTransactionCount: async (_: string) => Promise.resolve(0), + resolveName: async () => + '0x25A643B6e52864d0eD816F1E43c0CF49C83B8292', + } as any; + + const deliveryServiceProperties: DeliveryServiceProperties = { + sizeLimit: 2 ** 14, + messageTTL: 1000, + notificationChannel: [], + }; + + const mockWsManager: IWebSocketManager = { + isConnected: function (ensName: string): Promise { + return Promise.resolve(false); + }, + }; + + const messageProcessor = new MessageProcessor( + db, + web3Provider, + mockWsManager, + deliveryServiceProperties, + ds.keys, + () => {}, + ); + + const incomingEnvelop: EncryptionEnvelop = await MockMessageFactory( + sender, + receiver, + ds, + ).createEncryptedEnvelop('hello dm3'); + + try { + await messageProcessor.processEnvelop(incomingEnvelop); + fail(); + } catch (err: any) { + expect(err.message).toBe('Message does not match spam criteria'); + } + }); + it.skip('rejects message if the senders eth balance is below the threshold', async () => { + const _getSession = async (address: string) => + ({ + ...(await getSession(address)), + spamFilterRules: { minBalance: '0xa' }, + } as Session & { spamFilterRules: spamFilter.SpamFilterRules }); + + const db = { + createMessage: async () => {}, + getIdEnsName: () => '', + getSession: _getSession, + getUsersNotificationChannels: () => Promise.resolve([]), + } as any as IDatabase; + + const web3Provider = { + getBalance: async (_: string) => Promise.resolve(BigNumber.from(5)), + resolveName: async () => + '0x25A643B6e52864d0eD816F1E43c0CF49C83B8292', + } as any; + + const deliveryServiceProperties: DeliveryServiceProperties = { + sizeLimit: 2 ** 14, + messageTTL: 1000, + notificationChannel: [], + }; + + const mockWsManager: IWebSocketManager = { + isConnected: function (ensName: string): Promise { + return Promise.resolve(false); + }, + }; + + const messageProcessor = new MessageProcessor( + db, + web3Provider, + mockWsManager, + deliveryServiceProperties, + ds.keys, + () => {}, + ); + + const incomingEnvelop: EncryptionEnvelop = await MockMessageFactory( + sender, + receiver, + ds, + ).createEncryptedEnvelop('hello dm3'); + + try { + messageProcessor.processEnvelop(incomingEnvelop), fail(); + } catch (err: any) { + expect(err.message).toBe('Message does not match spam criteria'); + } + }); + // //TODO remove skip once spam-filter is implemented + it.skip('rejects message if the senders token balance is below the threshold', async () => { + const _getSession = async (address: string) => + ({ + ...(await getSession(address)), + spamFilterRules: { + minTokenBalance: { + address: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', + amount: '0xa', + }, + }, + } as Session & { spamFilterRules: spamFilter.SpamFilterRules }); + + const db = { + createMessage: async () => {}, + getIdEnsName: () => '', + getSession: _getSession, + getUsersNotificationChannels: () => Promise.resolve([]), + } as any as IDatabase; + + const web3Provider = { + _isProvider: true, + call: () => Promise.resolve(BigNumber.from(0).toHexString()), + resolveName: async () => + '0x25A643B6e52864d0eD816F1E43c0CF49C83B8292', + } as unknown as ethers.providers.JsonRpcProvider; + + const mockWsManager: IWebSocketManager = { + isConnected: function (ensName: string): Promise { + return Promise.resolve(false); + }, + }; + + const deliveryServiceProperties: DeliveryServiceProperties = { + sizeLimit: 2 ** 14, + messageTTL: 1000, + notificationChannel: [], + }; + + const messageProcessor = new MessageProcessor( + db, + web3Provider, + mockWsManager, + deliveryServiceProperties, + ds.keys, + () => {}, + ); + + const incomingEnvelop: EncryptionEnvelop = await MockMessageFactory( + sender, + receiver, + ds, + ).createEncryptedEnvelop('hello dm3'); + + try { + messageProcessor.processEnvelop(incomingEnvelop); + fail(); + } catch (err: any) { + expect(err.message).toBe('Message does not match spam criteria'); + } + }); + + it('send mail after incoming message', async () => { + const sendMessageViaSocketMock = jest.fn(); + const sendMailMock = jest.fn(); + + //mock nodemailer + const nodemailer = require('nodemailer'); //doesn't work with import. idk why + nodemailer.createTransport.mockReturnValue({ + sendMail: sendMailMock, + close: () => {}, + }); + + const getNotificationChannels = (user: string) => { + return Promise.resolve([ + { + type: NotificationChannelType.EMAIL, + config: { + recipientEmailId: 'joe12345@gmail.com', + isVerified: true, + isEnabled: true, + }, + }, + ]); + }; + + const db = { + createMessage: async () => {}, + getIdEnsName: () => '', + getSession, + getUsersNotificationChannels: getNotificationChannels, + } as any as IDatabase; + + const dsNotificationChannels: NotificationChannel[] = [ + { + type: NotificationChannelType.EMAIL, + config: { + smtpHost: 'smtp.gmail.com', + smtpPort: 587, + smtpEmail: 'abc@gmail.com', + smtpUsername: 'abc@gmail.com', + smtpPassword: 'abcd1234', + }, + }, + ]; + + const web3Provider = { + getBalance: async (_: string) => Promise.resolve(BigNumber.from(5)), + resolveName: async () => + '0x25A643B6e52864d0eD816F1E43c0CF49C83B8292', + } as any; + + const mockWsManager: IWebSocketManager = { + isConnected: function (ensName: string): Promise { + return Promise.resolve(false); + }, + }; + + const deliveryServiceProperties: DeliveryServiceProperties = { + sizeLimit: 2 ** 14, + messageTTL: 1000, + notificationChannel: dsNotificationChannels, + }; + + const messageProcessor = new MessageProcessor( + db, + web3Provider, + mockWsManager, + deliveryServiceProperties, + ds.keys, + () => {}, + ); + const incomingEnvelop: EncryptionEnvelop = await MockMessageFactory( + sender, + receiver, + ds, + ).createEncryptedEnvelop('hello dm3'); + + await messageProcessor.processEnvelop(incomingEnvelop); + + expect(sendMailMock).toHaveBeenCalled(); + + //Check if the message was submitted to the socket + expect(sendMessageViaSocketMock).not.toBeCalled(); + }); + it('stores proper incoming message', async () => { + const sendMock = jest.fn(); + const createMessageMock = jest.fn(); + + const now = Date.now(); + + const mockWsManager: IWebSocketManager = { + isConnected: function (ensName: string): Promise { + return Promise.resolve(false); + }, + }; + + const db = { + createMessage: createMessageMock, + getIdEnsName: () => '', + getSession, + getUsersNotificationChannels: () => Promise.resolve([]), + } as any as IDatabase; + + const web3Provider = { + getBalance: async (_: string) => Promise.resolve(BigNumber.from(5)), + resolveName: async () => + '0x25A643B6e52864d0eD816F1E43c0CF49C83B8292', + } as any; + + const deliveryServiceProperties: DeliveryServiceProperties = { + sizeLimit: 2 ** 14, + messageTTL: 1000, + notificationChannel: [], + }; + + const messageProcessor = new MessageProcessor( + db, + web3Provider, + mockWsManager, + deliveryServiceProperties, + ds.keys, + () => {}, + ); + + const incomingEnvelop: EncryptionEnvelop = await MockMessageFactory( + sender, + receiver, + ds, + ).createEncryptedEnvelop('hello dm3'); + + await messageProcessor.processEnvelop(incomingEnvelop); + + //createMessageCall + const [_, actualEnvelop] = createMessageMock.mock.calls[0]; + + expect(createMessageMock).toBeCalled(); + expect(actualEnvelop['message']).toBe(incomingEnvelop.message); + + const actualPostmark = await decryptAsymmetric( + receiver.profileKeys.encryptionKeyPair, + JSON.parse(actualEnvelop['postmark']), + ); + + // check postmark + const { incommingTimestamp, messageHash, signature } = + JSON.parse(actualPostmark); + expect(incommingTimestamp).toBeGreaterThanOrEqual(now); + expect(incommingTimestamp).toBeLessThanOrEqual(Date.now()); + expect(messageHash).toBe( + ethers.utils.hashMessage(stringify(incomingEnvelop.message)), + ); + const postmarkWithoutSig: Omit = { + messageHash, + incommingTimestamp, + }; + expect( + await checkSignature( + ds.keys.signingKeyPair.publicKey, + sha256(stringify(postmarkWithoutSig)), + signature, + ), + ).toBe(true); + //Check if the message was submitted to the socket + expect(sendMock).not.toBeCalled(); + }); + + it('stores proper incoming message and submit it if receiver is connected to a socket', async () => { + const sendMock = jest.fn(); + const createMessageMock = jest.fn(); + + const now = Date.now(); + + const mockWsManager: IWebSocketManager = { + isConnected: function (ensName: string): Promise { + return Promise.resolve(true); + }, + }; + + const db = { + createMessage: createMessageMock, + getIdEnsName: () => '', + getSession, + getUsersNotificationChannels: () => Promise.resolve([]), + } as any as IDatabase; + + const web3Provider = { + getBalance: async (_: string) => Promise.resolve(BigNumber.from(5)), + resolveName: async () => + '0x25A643B6e52864d0eD816F1E43c0CF49C83B8292', + } as any; + + const deliveryServiceProperties: DeliveryServiceProperties = { + sizeLimit: 2 ** 14, + messageTTL: 1000, + notificationChannel: [], + }; + + const messageProcessor = new MessageProcessor( + db, + web3Provider, + mockWsManager, + deliveryServiceProperties, + ds.keys, + sendMock, + ); + + const incomingEnvelop: EncryptionEnvelop = await MockMessageFactory( + sender, + receiver, + ds, + ).createEncryptedEnvelop('hello dm3'); + + await messageProcessor.processEnvelop(incomingEnvelop); + + //createMessageCall + const [_, actualEnvelop] = createMessageMock.mock.calls[0]; + + expect(createMessageMock).toBeCalled(); + expect(actualEnvelop['message']).toBe(incomingEnvelop.message); + + const actualPostmark = await decryptAsymmetric( + receiver.profileKeys.encryptionKeyPair, + JSON.parse(actualEnvelop['postmark']), + ); + + // check postmark + const { incommingTimestamp, messageHash, signature } = + JSON.parse(actualPostmark); + expect(incommingTimestamp).toBeGreaterThanOrEqual(now); + expect(incommingTimestamp).toBeLessThanOrEqual(Date.now()); + expect(messageHash).toBe( + ethers.utils.hashMessage(stringify(incomingEnvelop.message)), + ); + const postmarkWithoutSig: Omit = { + messageHash, + incommingTimestamp, + }; + expect( + await checkSignature( + ds.keys.signingKeyPair.publicKey, + sha256(stringify(postmarkWithoutSig)), + signature, + ), + ).toBe(true); + expect(sendMock).toBeCalled(); + }); +}); diff --git a/packages/delivery-service/src/message/MessageProcessor.ts b/packages/delivery-service/src/message/MessageProcessor.ts index 610cc8ade..32ce0b774 100644 --- a/packages/delivery-service/src/message/MessageProcessor.ts +++ b/packages/delivery-service/src/message/MessageProcessor.ts @@ -2,14 +2,15 @@ import { DeliveryServiceProfileKeys } from '@dm3-org/dm3-lib-profile'; import { IWebSocketManager, stringify } from '@dm3-org/dm3-lib-shared'; import { ethers } from 'ethers'; -import { decryptAsymmetric, KeyPair } from '@dm3-org/dm3-lib-crypto'; import { addPostmark, + decryptDeliveryInformation, DeliveryServiceProperties, getConversationId, NotificationBroker, NotificationType, } from '@dm3-org/dm3-lib-delivery'; +import { spamFilter } from '@dm3-org/dm3-lib-delivery'; import { DeliveryInformation, EncryptionEnvelop, @@ -17,7 +18,6 @@ import { } from '@dm3-org/dm3-lib-messaging'; import { logDebug } from '@dm3-org/dm3-lib-shared'; import { IDatabase } from '../persistence/getDatabase'; -import { isSpam } from './spam-filter'; type onSubmitMessage = (socketId: string, envelop: EncryptionEnvelop) => void; @@ -91,15 +91,14 @@ export class MessageProcessor { throw Error('unknown session'); } - console.debug( - 'incomingMessage', - conversationId, - deliveryInformation, - receiverSession, - ); - //Checks if the message is spam - if (await isSpam(this.provider, receiverSession, deliveryInformation)) { + if ( + await spamFilter.isSpam( + this.provider, + receiverSession, + deliveryInformation, + ) + ) { console.debug( `incomingMessage fro ${deliveryInformation.to} is spam`, ); diff --git a/packages/delivery-service/tsconfig.json b/packages/delivery-service/tsconfig.json index b8721eeaa..866b7a98f 100644 --- a/packages/delivery-service/tsconfig.json +++ b/packages/delivery-service/tsconfig.json @@ -16,6 +16,6 @@ "outDir": "dist", "sourceMap": true }, - "include": ["src"], + "include": ["src", "../lib/delivery/src/spam-filter"], "exclude": ["src/**/*.test.ts"] } diff --git a/packages/lib/delivery/src/Messages.test.ts b/packages/lib/delivery/src/Messages.test.ts index 21224d803..76860df51 100644 --- a/packages/lib/delivery/src/Messages.test.ts +++ b/packages/lib/delivery/src/Messages.test.ts @@ -112,629 +112,7 @@ nodemailer.createTransport.mockReturnValue({ }); describe('Messages', () => { - describe('incomingMessage', () => { - it('accepts an incoming message', async () => { - const storeNewMessage = async ( - conversationId: string, - envelop: EncryptionEnvelop, - ) => {}; - - const mockWsManager: IWebSocketManager = { - isConnected: function (ensName: string): Promise { - return Promise.resolve(false); - }, - }; - - expect.assertions(1); - - await expect(() => - incomingMessage( - { - message: '', - metadata: { - version: '', - encryptionScheme: 'x25519-chacha20-poly1305', - deliveryInformation: stringify( - testData.deliveryInformation, - ), - encryptedMessageHash: '', - signature: '', - }, - }, - - keysA.signingKeyPair, - keysA.encryptionKeyPair, - 2 ** 14, - [], - getSession, - storeNewMessage, - () => {}, - { - resolveName: async () => - '0x25A643B6e52864d0eD816F1E43c0CF49C83B8292', - } as any, - async () => '', - getNotificationChannels, - mockWsManager, - ), - ).not.toThrow(); - }); - - it('rejects an incoming message if it is to large', async () => { - const storeNewMessage = async ( - conversationId: string, - envelop: EncryptionEnvelop, - ) => {}; - - const mockWsManager: IWebSocketManager = { - isConnected: function (ensName: string): Promise { - return Promise.resolve(false); - }, - }; - - expect.assertions(1); - - await expect(() => - incomingMessage( - { - metadata: { - encryptionScheme: 'x25519-chacha20-poly1305', - deliveryInformation: '', - encryptedMessageHash: '', - signature: '', - version: '', - }, - message: '', - }, - keysA.signingKeyPair, - keysA.encryptionKeyPair, - 1, - [], - getSession, - storeNewMessage, - () => {}, - {} as ethers.providers.JsonRpcProvider, - async () => '', - getNotificationChannels, - mockWsManager, - ), - ).rejects.toEqual(Error('Message is too large')); - }); - it('rejects an incoming message if the receiver is unknown ', async () => { - const storeNewMessage = async ( - conversationId: string, - envelop: EncryptionEnvelop, - ) => {}; - - const mockWsManager: IWebSocketManager = { - isConnected: function (ensName: string): Promise { - return Promise.resolve(false); - }, - }; - - expect.assertions(1); - - await expect(() => - incomingMessage( - { - message: '', - metadata: { - encryptionScheme: 'x25519-chacha20-poly1305', - version: '', - encryptedMessageHash: '', - signature: '', - deliveryInformation: stringify( - testData.deliveryInformationB, - ), - }, - }, - keysA.signingKeyPair, - keysA.encryptionKeyPair, - 2 ** 14, - [], - getSession, - storeNewMessage, - () => {}, - { - resolveName: async () => - '0x25A643B6e52864d0eD816F1E43c0CF49C83B8292', - } as any, - async () => '', - getNotificationChannels, - mockWsManager, - ), - ).rejects.toEqual(Error('unknown session')); - }); - //TODO remove skip once spam-filter is implemented - it.skip('rejects message if the senders nonce is below the threshold', async () => { - let messageContainer: { - conversationId?: string; - envelop?: EncryptionEnvelop; - } = {}; - - const session = async (address: string) => - ({ - ...(await getSession(address)), - spamFilterRules: { minNonce: 2 }, - } as Session & { spamFilterRules: SpamFilterRules }); - - const provider = { - getTransactionCount: async (_: string) => Promise.resolve(0), - resolveName: async () => - '0x25A643B6e52864d0eD816F1E43c0CF49C83B8292', - } as any; - - const storeNewMessage = async ( - conversationId: string, - envelop: EncryptionEnvelop, - ) => { - messageContainer = { conversationId, envelop }; - }; - - const mockWsManager: IWebSocketManager = { - isConnected: function (ensName: string): Promise { - return Promise.resolve(false); - }, - }; - - try { - await incomingMessage( - { - message: '', - metadata: { - encryptionScheme: 'x25519-chacha20-poly1305', - encryptedMessageHash: '', - signature: '', - version: '', - deliveryInformation: stringify( - testData.deliveryInformation, - ), - }, - }, - keysA.signingKeyPair, - keysA.encryptionKeyPair, - 2 ** 14, - [], - session, - storeNewMessage, - () => {}, - provider, - async () => '', - getNotificationChannels, - mockWsManager, - ); - fail(); - } catch (err: any) { - expect(err.message).toBe( - 'Message does not match spam criteria', - ); - } - }); - it('rejects message if the senders eth balance is below the threshold', async () => { - let messageContainer: { - conversationId?: string; - envelop?: EncryptionEnvelop; - } = {}; - - const session = async (address: string) => - ({ - ...(await getSession(address)), - spamFilterRules: { minBalance: '0xa' }, - } as Session & { spamFilterRules: SpamFilterRules }); - - const provider = { - getBalance: async (_: string) => - Promise.resolve(BigNumber.from(5)), - resolveName: async () => - '0x25A643B6e52864d0eD816F1E43c0CF49C83B8292', - } as any; - - const storeNewMessage = async ( - conversationId: string, - envelop: EncryptionEnvelop, - ) => { - messageContainer = { conversationId, envelop }; - }; - - const mockWsManager: IWebSocketManager = { - isConnected: function (ensName: string): Promise { - return Promise.resolve(false); - }, - }; - - try { - await incomingMessage( - { - message: '', - metadata: { - encryptionScheme: 'x25519-chacha20-poly1305', - version: '', - encryptedMessageHash: '', - signature: '', - deliveryInformation: stringify( - testData.deliveryInformation, - ), - }, - }, - keysA.signingKeyPair, - keysA.encryptionKeyPair, - 2 ** 14, - [], - session, - storeNewMessage, - () => {}, - provider, - async () => '', - getNotificationChannels, - mockWsManager, - ); - fail(); - } catch (err: any) { - expect(err.message).toBe( - 'Message does not match spam criteria', - ); - } - }); - //TODO remove skip once spam-filter is implemented - it.skip('rejects message if the senders token balance is below the threshold', async () => { - let messageContainer: { - conversationId?: string; - envelop?: EncryptionEnvelop; - } = {}; - - const session = async (address: string) => - ({ - ...(await getSession(address)), - spamFilterRules: { - minTokenBalance: { - address: - '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', - amount: '0xa', - }, - }, - } as Session & { spamFilterRules: SpamFilterRules }); - - const provider = { - _isProvider: true, - call: () => Promise.resolve(BigNumber.from(0).toHexString()), - resolveName: async () => - '0x25A643B6e52864d0eD816F1E43c0CF49C83B8292', - } as unknown as ethers.providers.JsonRpcProvider; - - const storeNewMessage = async ( - conversationId: string, - envelop: EncryptionEnvelop, - ) => { - messageContainer = { conversationId, envelop }; - }; - - const mockWsManager: IWebSocketManager = { - isConnected: function (ensName: string): Promise { - return Promise.resolve(false); - }, - }; - - try { - await incomingMessage( - { - message: '', - metadata: { - encryptionScheme: 'x25519-chacha20-poly1305', - deliveryInformation: stringify( - testData.deliveryInformation, - ), - - version: '', - encryptedMessageHash: '', - signature: '', - }, - }, - keysA.signingKeyPair, - keysA.encryptionKeyPair, - 2 ** 14, - [], - session, - storeNewMessage, - () => {}, - provider, - async () => '', - getNotificationChannels, - mockWsManager, - ); - fail(); - } catch (err: any) { - expect(err.message).toBe( - 'Message does not match spam criteria', - ); - } - }); - - it('send mail after incoming message', async () => { - //Value stored at config - let messageContainer: { - conversationId?: string; - envelop?: EncryptionEnvelop; - } = {}; - - const storeNewMessage = async ( - conversationId: string, - envelop: EncryptionEnvelop, - ) => { - messageContainer = { conversationId, envelop }; - }; - - const sendMessageViaSocketMock = jest.fn(); - - const getNotificationChannels = (user: string) => { - return Promise.resolve([ - { - type: NotificationChannelType.EMAIL, - config: { - recipientEmailId: 'joe12345@gmail.com', - isVerified: true, - isEnabled: true, - }, - }, - ]); - }; - - const dsNotificationChannels: NotificationChannel[] = [ - { - type: NotificationChannelType.EMAIL, - config: { - smtpHost: 'smtp.gmail.com', - smtpPort: 587, - smtpEmail: 'abc@gmail.com', - smtpUsername: 'abc@gmail.com', - smtpPassword: 'abcd1234', - }, - }, - ]; - - const mockWsManager: IWebSocketManager = { - isConnected: function (ensName: string): Promise { - return Promise.resolve(false); - }, - }; - - await incomingMessage( - { - message: '', - metadata: { - version: '', - encryptedMessageHash: '', - signature: '', - encryptionScheme: 'x25519-chacha20-poly1305', - deliveryInformation: stringify( - testData.deliveryInformation, - ), - }, - }, - keysA.signingKeyPair, - keysA.encryptionKeyPair, - 2 ** 14, - dsNotificationChannels, - getSession, - storeNewMessage, - sendMessageViaSocketMock, - { - resolveName: async () => - '0x25A643B6e52864d0eD816F1E43c0CF49C83B8292', - } as any, - async (ensName) => ensName, - getNotificationChannels, - mockWsManager, - ); - - expect(sendMailMock).toHaveBeenCalled(); - - //Check if the message was submitted to the socket - expect(sendMessageViaSocketMock).not.toBeCalled(); - }); - it('stores proper incoming message', async () => { - let messageContainer: { - conversationId?: string; - envelop?: EncryptionEnvelop; - } = {}; - - const storeNewMessage = async ( - conversationId: string, - envelop: EncryptionEnvelop, - ) => { - messageContainer = { conversationId, envelop }; - }; - - const sendMock = jest.fn(); - - const now = Date.now(); - - const mockWsManager: IWebSocketManager = { - isConnected: function (ensName: string): Promise { - return Promise.resolve(false); - }, - }; - - await incomingMessage( - { - message: '', - metadata: { - version: '', - encryptedMessageHash: '', - signature: '', - encryptionScheme: 'x25519-chacha20-poly1305', - deliveryInformation: stringify( - testData.deliveryInformation, - ), - }, - }, - keysA.signingKeyPair, - keysA.encryptionKeyPair, - 2 ** 14, - [], - getSession, - storeNewMessage, - sendMock, - { - resolveName: async () => - '0x25A643B6e52864d0eD816F1E43c0CF49C83B8292', - } as any, - async (ensName) => ensName, - getNotificationChannels, - mockWsManager, - ); - - const conversationId = getConversationId('alice.eth', 'bob.eth'); - - const actualPostmark = await decryptAsymmetric( - keysB.encryptionKeyPair, - JSON.parse(messageContainer.envelop?.postmark!), - ); - - //Check message - expect(messageContainer.conversationId).toEqual(conversationId); - - expect(messageContainer.envelop).toEqual( - expect.objectContaining({ - message: '', - metadata: { - version: '', - encryptedMessageHash: '', - signature: '', - encryptionScheme: 'x25519-chacha20-poly1305', - deliveryInformation: { - from: 'alice.eth', - to: 'bob.eth', - }, - }, - }), - ); - - // check postmark - const { incommingTimestamp, messageHash, signature } = - JSON.parse(actualPostmark); - expect(incommingTimestamp).toBeGreaterThanOrEqual(now); - expect(incommingTimestamp).toBeLessThanOrEqual(Date.now()); - expect(messageHash).toBe( - '0xd7c617eb7ffee435e7d4e7f6b13d46ccdf88d2e5463148c50659e5cd88d248b5', - ); - const postmarkWithoutSig: Omit = { - messageHash, - incommingTimestamp, - }; - expect( - await checkSignature( - keysA.signingKeyPair.publicKey, - sha256(stringify(postmarkWithoutSig)), - signature, - ), - ).toBe(true); - //Check if the message was submitted to the socket - expect(sendMock).not.toBeCalled(); - }); - - it('stores proper incoming message and submit it if receiver is connected to a socket', async () => { - let messageContainer: { - conversationId?: string; - envelop?: EncryptionEnvelop; - } = {}; - - const storeNewMessage = async ( - conversationId: string, - envelop: EncryptionEnvelop, - ) => { - messageContainer = { conversationId, envelop }; - }; - - const sendMock = jest.fn(); - - const _getSession = (address: string) => getSession(address, 'foo'); - const now = Date.now(); - - const mockWsManager: IWebSocketManager = { - isConnected: function (ensName: string): Promise { - return Promise.resolve(true); - }, - }; - - await incomingMessage( - { - message: '', - metadata: { - encryptionScheme: 'x25519-chacha20-poly1305', - deliveryInformation: stringify( - testData.deliveryInformation, - ), - version: '', - encryptedMessageHash: '', - signature: '', - }, - }, - keysA.signingKeyPair, - keysA.encryptionKeyPair, - 2 ** 14, - [], - _getSession, - storeNewMessage, - sendMock, - { - resolveName: async () => - '0x25A643B6e52864d0eD816F1E43c0CF49C83B8292', - } as any, - async (ensName) => ensName, - getNotificationChannels, - mockWsManager, - ); - - const conversationId = getConversationId('alice.eth', 'bob.eth'); - - const actualPostmark = await decryptAsymmetric( - keysB.encryptionKeyPair, - JSON.parse(messageContainer.envelop?.postmark!), - ); - - //Check message - expect(messageContainer.conversationId).toEqual(conversationId); - - expect(messageContainer.envelop).toEqual( - expect.objectContaining({ - message: '', - metadata: { - encryptionScheme: 'x25519-chacha20-poly1305', - deliveryInformation: { - from: 'alice.eth', - to: 'bob.eth', - }, - version: '', - encryptedMessageHash: '', - signature: '', - }, - }), - ); - - // check postmark - const { incommingTimestamp, messageHash, signature } = - JSON.parse(actualPostmark); - expect(incommingTimestamp).toBeGreaterThanOrEqual(now); - expect(incommingTimestamp).toBeLessThanOrEqual(Date.now()); - expect(messageHash).toBe( - '0xd7c617eb7ffee435e7d4e7f6b13d46ccdf88d2e5463148c50659e5cd88d248b5', - ); - const postmarkWithoutSig: Omit = { - messageHash, - incommingTimestamp, - }; - expect( - await checkSignature( - keysA.signingKeyPair.publicKey, - sha256(stringify(postmarkWithoutSig)), - signature, - ), - ).toBe(true); - //Check if the message was submitted to the socket - expect(sendMock).toBeCalled(); - }); - }); - + Ć’; describe('GetMessages', () => { it('returns all messages of the user', async () => { const conversationIdToUse = getConversationId( diff --git a/packages/lib/delivery/src/index.ts b/packages/lib/delivery/src/index.ts index 9afed2ead..a82588a41 100644 --- a/packages/lib/delivery/src/index.ts +++ b/packages/lib/delivery/src/index.ts @@ -14,7 +14,7 @@ export type { Acknoledgment } from './Messages'; export { getConversationId } from './Messages'; export type {} from './PublicMessages'; export * as schema from './schema'; -export * as spamFilter from '../../../delivery-service/src/message/spam-filter'; +export * as spamFilter from './spam-filter/'; export { checkToken } from './Session'; export type { Session } from './Session'; export type { DeliveryServiceProperties } from './Delivery'; diff --git a/packages/delivery-service/src/message/spam-filter/SpamFilterRules.ts b/packages/lib/delivery/src/spam-filter/SpamFilterRules.ts similarity index 100% rename from packages/delivery-service/src/message/spam-filter/SpamFilterRules.ts rename to packages/lib/delivery/src/spam-filter/SpamFilterRules.ts diff --git a/packages/delivery-service/src/message/spam-filter/filter/SpamFilter.ts b/packages/lib/delivery/src/spam-filter/filter/SpamFilter.ts similarity index 100% rename from packages/delivery-service/src/message/spam-filter/filter/SpamFilter.ts rename to packages/lib/delivery/src/spam-filter/filter/SpamFilter.ts diff --git a/packages/delivery-service/src/message/spam-filter/filter/ethBalanceFilter/EthBalanceFilter.test.ts b/packages/lib/delivery/src/spam-filter/filter/ethBalanceFilter/EthBalanceFilter.test.ts similarity index 100% rename from packages/delivery-service/src/message/spam-filter/filter/ethBalanceFilter/EthBalanceFilter.test.ts rename to packages/lib/delivery/src/spam-filter/filter/ethBalanceFilter/EthBalanceFilter.test.ts diff --git a/packages/delivery-service/src/message/spam-filter/filter/ethBalanceFilter/EthBalanceFilter.ts b/packages/lib/delivery/src/spam-filter/filter/ethBalanceFilter/EthBalanceFilter.ts similarity index 100% rename from packages/delivery-service/src/message/spam-filter/filter/ethBalanceFilter/EthBalanceFilter.ts rename to packages/lib/delivery/src/spam-filter/filter/ethBalanceFilter/EthBalanceFilter.ts diff --git a/packages/delivery-service/src/message/spam-filter/filter/nonceFilter/NonceFilter.test.ts b/packages/lib/delivery/src/spam-filter/filter/nonceFilter/NonceFilter.test.ts similarity index 100% rename from packages/delivery-service/src/message/spam-filter/filter/nonceFilter/NonceFilter.test.ts rename to packages/lib/delivery/src/spam-filter/filter/nonceFilter/NonceFilter.test.ts diff --git a/packages/delivery-service/src/message/spam-filter/filter/nonceFilter/NonceFilter.ts b/packages/lib/delivery/src/spam-filter/filter/nonceFilter/NonceFilter.ts similarity index 100% rename from packages/delivery-service/src/message/spam-filter/filter/nonceFilter/NonceFilter.ts rename to packages/lib/delivery/src/spam-filter/filter/nonceFilter/NonceFilter.ts diff --git a/packages/delivery-service/src/message/spam-filter/filter/tokenBalanceFilter/Erc20Abi.json b/packages/lib/delivery/src/spam-filter/filter/tokenBalanceFilter/Erc20Abi.json similarity index 100% rename from packages/delivery-service/src/message/spam-filter/filter/tokenBalanceFilter/Erc20Abi.json rename to packages/lib/delivery/src/spam-filter/filter/tokenBalanceFilter/Erc20Abi.json diff --git a/packages/delivery-service/src/message/spam-filter/filter/tokenBalanceFilter/TokenBalanceFilter.test.ts b/packages/lib/delivery/src/spam-filter/filter/tokenBalanceFilter/TokenBalanceFilter.test.ts similarity index 100% rename from packages/delivery-service/src/message/spam-filter/filter/tokenBalanceFilter/TokenBalanceFilter.test.ts rename to packages/lib/delivery/src/spam-filter/filter/tokenBalanceFilter/TokenBalanceFilter.test.ts diff --git a/packages/delivery-service/src/message/spam-filter/filter/tokenBalanceFilter/TokenBalanceFilter.ts b/packages/lib/delivery/src/spam-filter/filter/tokenBalanceFilter/TokenBalanceFilter.ts similarity index 100% rename from packages/delivery-service/src/message/spam-filter/filter/tokenBalanceFilter/TokenBalanceFilter.ts rename to packages/lib/delivery/src/spam-filter/filter/tokenBalanceFilter/TokenBalanceFilter.ts diff --git a/packages/delivery-service/src/message/spam-filter/index.ts b/packages/lib/delivery/src/spam-filter/index.ts similarity index 100% rename from packages/delivery-service/src/message/spam-filter/index.ts rename to packages/lib/delivery/src/spam-filter/index.ts diff --git a/packages/delivery-service/src/message/spam-filter/spamfilter.test.ts b/packages/lib/delivery/src/spam-filter/spamfilter.test.ts similarity index 100% rename from packages/delivery-service/src/message/spam-filter/spamfilter.test.ts rename to packages/lib/delivery/src/spam-filter/spamfilter.test.ts From 64ae37a9465b75b73c743087a8cfa38734ed39c4 Mon Sep 17 00:00:00 2001 From: AlexNi245 Date: Thu, 18 Jul 2024 16:33:17 +0200 Subject: [PATCH 007/148] add messageProcessor to Ds endpoint --- .../src/rpc/methods/handleSubmitMessage.ts | 55 ++++++++----------- 1 file changed, 22 insertions(+), 33 deletions(-) diff --git a/packages/delivery-service/src/rpc/methods/handleSubmitMessage.ts b/packages/delivery-service/src/rpc/methods/handleSubmitMessage.ts index 8ed933ab0..c1d9f847a 100644 --- a/packages/delivery-service/src/rpc/methods/handleSubmitMessage.ts +++ b/packages/delivery-service/src/rpc/methods/handleSubmitMessage.ts @@ -1,16 +1,17 @@ -import { - incomingMessage, - DeliveryServiceProperties, -} from '@dm3-org/dm3-lib-delivery'; +import { DeliveryServiceProperties } from '@dm3-org/dm3-lib-delivery'; import { EncryptionEnvelop, schema } from '@dm3-org/dm3-lib-messaging'; -import { validateSchema, logError } from '@dm3-org/dm3-lib-shared'; +import { DeliveryServiceProfileKeys } from '@dm3-org/dm3-lib-profile'; +import { + IWebSocketManager, + logError, + validateSchema, +} from '@dm3-org/dm3-lib-shared'; import 'dotenv/config'; +import { ethers } from 'ethers'; import express from 'express'; import { Server } from 'socket.io'; +import { MessageProcessor } from '../../message/MessageProcessor'; import { IDatabase } from '../../persistence/getDatabase'; -import { ethers } from 'ethers'; -import { DeliveryServiceProfileKeys } from '@dm3-org/dm3-lib-profile'; -import { IWebSocketManager } from '@dm3-org/dm3-lib-shared'; export async function handleSubmitMessage( req: express.Request, @@ -56,34 +57,22 @@ export async function handleSubmitMessage( return res.status(400).send({ error }); } + const messageProcessor = new MessageProcessor( + db, + web3Provider, + webSocketManager, + deliveryServiceProperties, + keys, + (socketId: string, envelop: EncryptionEnvelop) => { + io.sockets.to(socketId).emit('message', envelop); + }, + ); + try { - await incomingMessage( - envelop, - keys.signingKeyPair, - keys.encryptionKeyPair, - deliveryServiceProperties.sizeLimit, - deliveryServiceProperties.notificationChannel, - db.getSession, - db.createMessage, - (socketId: string, envelop: EncryptionEnvelop) => { - io.sockets.to(socketId).emit('message', envelop); - }, - web3Provider, - db.getIdEnsName, - db.getUsersNotificationChannels, - webSocketManager, - ); + await messageProcessor.processEnvelop(envelop); res.send(200); } catch (error) { - global.logger.warn({ - method: 'RPC SUBMIT MESSAGE', - error: JSON.stringify(error), - }); - logError({ - text: '[handleSubmitMessage]', - error, - }); - console.log('handle submit message error'); + console.error('handle submit message error'); console.error(error); return res.status(400).send(); From 757062e7c9a094446a79d97121ac8a4c2a776c3b Mon Sep 17 00:00:00 2001 From: AlexNi245 Date: Thu, 18 Jul 2024 16:50:18 +0200 Subject: [PATCH 008/148] use getAccount instead of getSession in MessageProcessor --- .../src/message/MessageProcessor.test.ts | 36 +++++++++---------- .../src/message/MessageProcessor.ts | 2 +- 2 files changed, 18 insertions(+), 20 deletions(-) diff --git a/packages/delivery-service/src/message/MessageProcessor.test.ts b/packages/delivery-service/src/message/MessageProcessor.test.ts index b677c21b6..2deb4323a 100644 --- a/packages/delivery-service/src/message/MessageProcessor.test.ts +++ b/packages/delivery-service/src/message/MessageProcessor.test.ts @@ -9,10 +9,10 @@ import { import { BigNumber, ethers } from 'ethers'; import { MessageProcessor } from './MessageProcessor'; +import { checkSignature, decryptAsymmetric } from '@dm3-org/dm3-lib-crypto'; import { DeliveryServiceProperties, Session, - getConversationId, spamFilter, } from '@dm3-org/dm3-lib-delivery'; import { UserProfile, normalizeEnsName } from '@dm3-org/dm3-lib-profile'; @@ -24,8 +24,6 @@ import { mockUserProfile, } from '@dm3-org/dm3-lib-test-helper'; import { IDatabase } from '../persistence/getDatabase'; -import { checkSignature, decryptAsymmetric } from '@dm3-org/dm3-lib-crypto'; -import exp from 'constants'; jest.mock('nodemailer'); @@ -58,7 +56,7 @@ describe('MessageProcessor', () => { 'http://localhost:3000', ); }); - const getSession = async ( + const getAccount = async ( ensName: string, socketId?: string, ): Promise< @@ -117,7 +115,7 @@ describe('MessageProcessor', () => { const db = { createMessage: async () => {}, getIdEnsName: () => '', - getSession, + getAccount, getUsersNotificationChannels: () => Promise.resolve([]), } as any as IDatabase; @@ -162,7 +160,7 @@ describe('MessageProcessor', () => { const db = { createMessage: async () => {}, getIdEnsName: () => '', - getSession, + getAccount, getUsersNotificationChannels: () => Promise.resolve([]), } as any as IDatabase; @@ -206,7 +204,7 @@ describe('MessageProcessor', () => { const db = { createMessage: async () => {}, getIdEnsName: () => '', - getSession, + getAccount, getUsersNotificationChannels: () => Promise.resolve([]), } as any as IDatabase; @@ -249,16 +247,16 @@ describe('MessageProcessor', () => { // //TODO remove skip once spam-filter is implemented // //TODO remove skip once spam-filter is implemented it.skip('rejects message if the senders nonce is below the threshold', async () => { - const _getSession = async (address: string) => + const _getAccount = async (address: string) => ({ - ...(await getSession(address)), + ...(await getAccount(address)), spamFilterRules: { minNonce: 2 }, } as Session & { spamFilterRules: spamFilter.SpamFilterRules }); const db = { createMessage: async () => {}, getIdEnsName: () => '', - getSession: _getSession, + getAccount: _getAccount, getUsersNotificationChannels: () => Promise.resolve([]), } as any as IDatabase; @@ -303,16 +301,16 @@ describe('MessageProcessor', () => { } }); it.skip('rejects message if the senders eth balance is below the threshold', async () => { - const _getSession = async (address: string) => + const _getAccount = async (address: string) => ({ - ...(await getSession(address)), + ...(await getAccount(address)), spamFilterRules: { minBalance: '0xa' }, } as Session & { spamFilterRules: spamFilter.SpamFilterRules }); const db = { createMessage: async () => {}, getIdEnsName: () => '', - getSession: _getSession, + getAccount: _getAccount, getUsersNotificationChannels: () => Promise.resolve([]), } as any as IDatabase; @@ -357,9 +355,9 @@ describe('MessageProcessor', () => { }); // //TODO remove skip once spam-filter is implemented it.skip('rejects message if the senders token balance is below the threshold', async () => { - const _getSession = async (address: string) => + const _getAccount = async (address: string) => ({ - ...(await getSession(address)), + ...(await getAccount(address)), spamFilterRules: { minTokenBalance: { address: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', @@ -371,7 +369,7 @@ describe('MessageProcessor', () => { const db = { createMessage: async () => {}, getIdEnsName: () => '', - getSession: _getSession, + getAccount: _getAccount, getUsersNotificationChannels: () => Promise.resolve([]), } as any as IDatabase; @@ -444,7 +442,7 @@ describe('MessageProcessor', () => { const db = { createMessage: async () => {}, getIdEnsName: () => '', - getSession, + getAccount, getUsersNotificationChannels: getNotificationChannels, } as any as IDatabase; @@ -515,7 +513,7 @@ describe('MessageProcessor', () => { const db = { createMessage: createMessageMock, getIdEnsName: () => '', - getSession, + getAccount, getUsersNotificationChannels: () => Promise.resolve([]), } as any as IDatabase; @@ -597,7 +595,7 @@ describe('MessageProcessor', () => { const db = { createMessage: createMessageMock, getIdEnsName: () => '', - getSession, + getAccount, getUsersNotificationChannels: () => Promise.resolve([]), } as any as IDatabase; diff --git a/packages/delivery-service/src/message/MessageProcessor.ts b/packages/delivery-service/src/message/MessageProcessor.ts index 32ce0b774..5ad8a0175 100644 --- a/packages/delivery-service/src/message/MessageProcessor.ts +++ b/packages/delivery-service/src/message/MessageProcessor.ts @@ -83,7 +83,7 @@ export class MessageProcessor { console.debug(conversationId, deliveryInformation); //Retrieves the session of the receiver - const receiverSession = await this.db.getSession( + const receiverSession = await this.db.getAccount( deliveryInformation.to, ); if (!receiverSession) { From 92d6b92dfaa9a08dd751ed4a31bc0675f5eccc8d Mon Sep 17 00:00:00 2001 From: malteish Date: Fri, 19 Jul 2024 11:03:20 +0200 Subject: [PATCH 009/148] fix merge --- packages/backend/src/persistence/getDatabase.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/backend/src/persistence/getDatabase.ts b/packages/backend/src/persistence/getDatabase.ts index 6f17b43c4..4e4c2eb64 100644 --- a/packages/backend/src/persistence/getDatabase.ts +++ b/packages/backend/src/persistence/getDatabase.ts @@ -90,9 +90,6 @@ export interface IBackendDatabase { setAccount: (ensName: string) => Promise; getAccount: (ensName: string) => Promise; doesAccountExist: (ensName: string) => Promise; - //Legacy remove after storage has been merged - getUserStorage: (ensName: string) => Promise; - setUserStorage: (ensName: string, data: string) => Promise; addConversation: ( ensName: string, From 776cd84910e58039f40d1bd4b381b468672696fe Mon Sep 17 00:00:00 2001 From: malteish Date: Fri, 19 Jul 2024 11:28:18 +0200 Subject: [PATCH 010/148] add db migration --- .../20240719092732_add_account_created_at/migration.sql | 2 ++ packages/backend/package.json | 1 + 2 files changed, 3 insertions(+) create mode 100644 packages/backend/migrations/20240719092732_add_account_created_at/migration.sql diff --git a/packages/backend/migrations/20240719092732_add_account_created_at/migration.sql b/packages/backend/migrations/20240719092732_add_account_created_at/migration.sql new file mode 100644 index 000000000..6472c4b02 --- /dev/null +++ b/packages/backend/migrations/20240719092732_add_account_created_at/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "Account" ADD COLUMN "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP; diff --git a/packages/backend/package.json b/packages/backend/package.json index 3fff07e0c..1e30b76a3 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -29,6 +29,7 @@ "scripts": { "docker:up": "docker-compose up -d", "prisma-init": "prisma generate && prisma migrate deploy ", + "prisma-create-migrations": "prisma generate && prisma migrate dev", "start": "yarn prisma-init && node ./dist/index.js", "start-inspect": "node --inspect=0.0.0.0:9229 ./dist/index.js", "test": "yarn run before:tests && DATABASE_URL='postgresql://prisma:prisma@localhost:5433/tests?schema=public' yarn jest --coverage --runInBand --transformIgnorePatterns 'node_modules/(?!(dm3-lib-\\w*)/)'", From 44a522b9b8fee2e7aa9fce1516934e4c2e13037c Mon Sep 17 00:00:00 2001 From: malteish Date: Fri, 19 Jul 2024 11:47:04 +0200 Subject: [PATCH 011/148] run prisma generate --- packages/backend/package.json | 2 +- packages/backend/src/persistence/getDatabase.ts | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/backend/package.json b/packages/backend/package.json index 1e30b76a3..bc4c3c34c 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -33,7 +33,7 @@ "start": "yarn prisma-init && node ./dist/index.js", "start-inspect": "node --inspect=0.0.0.0:9229 ./dist/index.js", "test": "yarn run before:tests && DATABASE_URL='postgresql://prisma:prisma@localhost:5433/tests?schema=public' yarn jest --coverage --runInBand --transformIgnorePatterns 'node_modules/(?!(dm3-lib-\\w*)/)'", - "build": "yarn tsc && cp ./config.yml ./dist/config.yml | true", + "build": "yarn prisma generate && yarn tsc && cp ./config.yml ./dist/config.yml | true", "build:schema": "sh ./schemas.sh", "createDeliveryServiceProfile": "node --no-warnings ./cli.js", "before:tests": "docker-compose -f docker-compose.test.yml up -d && DATABASE_URL='postgresql://prisma:prisma@localhost:5433/tests?schema=public' yarn prisma-init" diff --git a/packages/backend/src/persistence/getDatabase.ts b/packages/backend/src/persistence/getDatabase.ts index 4e4c2eb64..bf4f8fec4 100644 --- a/packages/backend/src/persistence/getDatabase.ts +++ b/packages/backend/src/persistence/getDatabase.ts @@ -1,5 +1,3 @@ -import { Session as DSSession, spamFilter } from '@dm3-org/dm3-lib-delivery'; -import { IAccountDatabase } from '@dm3-org/dm3-lib-server-side'; import { Account, PrismaClient } from '@prisma/client'; import { createClient } from 'redis'; import Storage from './storage'; From 56a6157d3d9b32420f71baaab4d85d6886799655 Mon Sep 17 00:00:00 2001 From: malteish Date: Fri, 19 Jul 2024 12:00:39 +0200 Subject: [PATCH 012/148] extend readme with prisma guide --- packages/backend/README.md | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/packages/backend/README.md b/packages/backend/README.md index 0a20dc02c..e7e80aab7 100644 --- a/packages/backend/README.md +++ b/packages/backend/README.md @@ -8,6 +8,15 @@ cd ../../ && yarn build ``` +#### Contributing + +Whenever the prisma schema is updated, run the following command to generate the types: + +1. `prisma-create-migrations`: this will add a new migration to the migrations folder, which will be committed to the repository. Our server environments do not generate the migrations on the fly, so we need to commit the migration to the repository. This requires a running database, so make sure to have the database running before running this command. +2. `prisma-generate`: this will generate the types for the prisma schema (and the client). This step is executed automatically when running on the server. + +Fogetting step #1 will result in the server not being able to start, as the types will be missing. + ### Usage yarn @@ -15,9 +24,3 @@ yarn ``` yarn start ``` - -npm - -``` -npm start -``` From db8edd29a1c1fd5b8e018fee4377e781d92f31a8 Mon Sep 17 00:00:00 2001 From: malteish Date: Fri, 19 Jul 2024 12:00:49 +0200 Subject: [PATCH 013/148] improve deploy script --- .github/workflows/deploy.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 6f130baf5..9e37d4b43 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -156,7 +156,7 @@ jobs: run: | ssh -i ./ssh-key root@${{ vars.HOST_DOMAIN }} "\ ufw allow from 172.18.0.1/16 proto tcp to ${{ vars.HOST_IP}} port 80; - ufw allow from 172.18.0.1/16 proto tcp to ${{ secrets.IP_ADDRESS }} port 443; + ufw allow from 172.18.0.1/16 proto tcp to ${{ vars.HOST_IP }} port 443; ufw enable" - name: Start docker on server run: | From 8122187114872838448cf5db4e27e3804cfce373 Mon Sep 17 00:00:00 2001 From: malteish Date: Fri, 19 Jul 2024 12:08:15 +0200 Subject: [PATCH 014/148] disable state reset for testing --- .github/workflows/deploy.yml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 9e37d4b43..54ef612aa 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -145,13 +145,13 @@ jobs: ssh -i ./ssh-key app@${{ vars.HOST_DOMAIN }} "\ cd dm3 && ls |grep -E 'dm3-.*tar' | xargs --no-run-if-empty -L 1 docker load -i; \ rm dm3-*.tar || true" - - name: Reset state of testing environment - run: | - if [ $environment_name == "testing" ]; then - ssh -i ./ssh-key root@${{ vars.HOST_DOMAIN }} "\ - cd ${{ vars.PERSISTENCE_DIRECTORY }}/db && rm -r * || true; - cd ${{ vars.PERSISTENCE_DIRECTORY }}/storage && rm -r * || true" - fi + # - name: Reset state of testing environment + # run: | + # if [ $environment_name == "testing" ]; then + # ssh -i ./ssh-key root@${{ vars.HOST_DOMAIN }} "\ + # cd ${{ vars.PERSISTENCE_DIRECTORY }}/db && rm -r * || true; + # cd ${{ vars.PERSISTENCE_DIRECTORY }}/storage && rm -r * || true" + # fi - name: Configure Firewall run: | ssh -i ./ssh-key root@${{ vars.HOST_DOMAIN }} "\ From 43cc13485174e7b89f7e41899fcb5b46fece0daf Mon Sep 17 00:00:00 2001 From: malteish Date: Fri, 19 Jul 2024 13:00:45 +0200 Subject: [PATCH 015/148] add data migration helpers and guide --- .../backend/manual_data_migration/insert.sh | 32 +++++++++++++++++++ .../insertWithinDocker.sh | 24 ++++++++++++++ .../backend/manual_data_migration/notes.md | 30 +++++++++++++++++ 3 files changed, 86 insertions(+) create mode 100755 packages/backend/manual_data_migration/insert.sh create mode 100755 packages/backend/manual_data_migration/insertWithinDocker.sh create mode 100644 packages/backend/manual_data_migration/notes.md diff --git a/packages/backend/manual_data_migration/insert.sh b/packages/backend/manual_data_migration/insert.sh new file mode 100755 index 000000000..6a871a49c --- /dev/null +++ b/packages/backend/manual_data_migration/insert.sh @@ -0,0 +1,32 @@ +#!/bin/bash + +# Input file +input_file="dump.txt" + +# PostgreSQL connection details +DB_HOST="localhost" +DB_PORT="5432" +DB_NAME="dm3-storage" +DB_USER="postgres" +DB_PASSWORD="postgres" + +# Export password to avoid password prompt +export PGPASSWORD=$DB_PASSWORD + +# Read the input file line by line +while IFS= read -r line +do + # Extract the ID (first part of the line) and timestamp (after "createdAt") + id=$(echo "$line" | cut -d ':' -f 2) + timestamp=$(echo "$line" | grep -oP '(?<="createdAt":)[0-9]+') + + # Convert the timestamp from milliseconds to seconds + timestamp_seconds=$(echo "scale=0; $timestamp / 1000" | bc) + + # Insert the extracted values into the PostgreSQL table + psql -h $DB_HOST -p $DB_PORT -U $DB_USER -d $DB_NAME -c "INSERT INTO \"Account\" (id, \"createdAt\") VALUES ('$id', to_timestamp($timestamp_seconds));" + #echo "INSERT INTO Account (id, timestamp) VALUES ('$id', $timestamp);" +done < "$input_file" + +# Unset the password variable for security reasons +unset PGPASSWORD diff --git a/packages/backend/manual_data_migration/insertWithinDocker.sh b/packages/backend/manual_data_migration/insertWithinDocker.sh new file mode 100755 index 000000000..dd42cb1d1 --- /dev/null +++ b/packages/backend/manual_data_migration/insertWithinDocker.sh @@ -0,0 +1,24 @@ +#!/bin/bash + +# Input file +input_file="dump.txt" + +DB_NAME="dm3-storage" +DB_USER="postgres" + + +# Read the input file line by line +while IFS= read -r line +do + # Extract the ID (first part of the line) and timestamp (after "createdAt") + id=$(echo "$line" | cut -d ':' -f 2) + timestamp=$(echo "$line" | grep -oP '(?<="createdAt":)[0-9]+') + + # Convert the timestamp from milliseconds to seconds + timestamp_seconds=$(echo $timestamp | sed 's/...$//') + + # Insert the extracted values into the PostgreSQL table + psql -U $DB_USER -d $DB_NAME -c "INSERT INTO \"Account\" (id, \"createdAt\") VALUES ('$id', to_timestamp($timestamp_seconds));" + +done < "$input_file" + diff --git a/packages/backend/manual_data_migration/notes.md b/packages/backend/manual_data_migration/notes.md new file mode 100644 index 000000000..a672951a8 --- /dev/null +++ b/packages/backend/manual_data_migration/notes.md @@ -0,0 +1,30 @@ +Process: + +check data +docker exec -it dm3-db-1 redis-cli --scan --pattern 'session\*' + +docker exec -it dm3-storage psql -U prisma -d dm3 -c 'SELECT \* FROM "Account";' + +go into the redis container +docker exec -it dm3-db-1 bash + +dump all relevant sessions +for key in `redis-cli --scan --pattern 'session*'`; do echo $key: `redis-cli GET $key` >> dump.txt; echo $key; done + +copy the dump to the host +docker cp dm3-db-1:/data/dump.txt . + +copy the dump to the postgres container +docker cp dump.txt dm3-storage:/ + +copy the script to the postgres container +docker cp insertWithinDocker.sh dm3-storage:/ + +go into the postgres container +docker exec -it dm3-storage bash + +run the script +./insertWithinDocker.sh + +check the data from outside the container +docker exec -it dm3-storage psql -U prisma -d dm3 -c 'SELECT \* FROM "Account";' From 60b54a05adce6e825c432a88b58b5f7a51923857 Mon Sep 17 00:00:00 2001 From: AlexNi245 Date: Fri, 19 Jul 2024 13:05:32 +0200 Subject: [PATCH 016/148] wip adding addr to DS --- .../src/message/MessageProcessor.test.ts | 103 +++++++++++++++++- .../src/message/MessageProcessor.ts | 7 ++ 2 files changed, 105 insertions(+), 5 deletions(-) diff --git a/packages/delivery-service/src/message/MessageProcessor.test.ts b/packages/delivery-service/src/message/MessageProcessor.test.ts index 2deb4323a..c9679b977 100644 --- a/packages/delivery-service/src/message/MessageProcessor.test.ts +++ b/packages/delivery-service/src/message/MessageProcessor.test.ts @@ -30,21 +30,26 @@ jest.mock('nodemailer'); describe('MessageProcessor', () => { let sender: MockedUserProfile; let receiver: MockedUserProfile; + let receiverOnGno: MockedUserProfile; let rando: MockedUserProfile; let ds: MockDeliveryServiceProfile; beforeEach(async () => { + //The receiver might use the same address for different networks. Hence we keep the wallet separate + + const receiverWallet = ethers.Wallet.createRandom(); sender = await mockUserProfile( ethers.Wallet.createRandom(), 'bob.eth', ['http://localhost:3000'], ); - receiver = await mockUserProfile( - ethers.Wallet.createRandom(), - 'alice.eth', - ['http://localhost:3000'], - ); + receiver = await mockUserProfile(receiverWallet, 'alice.eth', [ + 'http://localhost:3000', + ]); + receiverOnGno = await mockUserProfile(receiverWallet, 'alice.gno', [ + 'http://localhost:3000', + ]); rando = await mockUserProfile( ethers.Wallet.createRandom(), 'rando.eth', @@ -579,6 +584,94 @@ describe('MessageProcessor', () => { //Check if the message was submitted to the socket expect(sendMock).not.toBeCalled(); }); + it('stores proper incoming message using address', async () => { + const sendMock = jest.fn(); + const createMessageMock = jest.fn(); + + const now = Date.now(); + + const mockWsManager: IWebSocketManager = { + isConnected: function (ensName: string): Promise { + return Promise.resolve(false); + }, + }; + + const db = { + createMessage: createMessageMock, + getIdEnsName: () => '', + getAccount, + getUsersNotificationChannels: () => Promise.resolve([]), + } as any as IDatabase; + + const web3Provider = { + getBalance: async (_: string) => Promise.resolve(BigNumber.from(5)), + resolveName: async () => + '0x25A643B6e52864d0eD816F1E43c0CF49C83B8292', + } as any; + + const deliveryServiceProperties: DeliveryServiceProperties = { + sizeLimit: 2 ** 14, + messageTTL: 1000, + notificationChannel: [], + }; + + const messageProcessor = new MessageProcessor( + db, + web3Provider, + mockWsManager, + deliveryServiceProperties, + ds.keys, + () => {}, + ); + + const incomingEnvelop1: EncryptionEnvelop = await MockMessageFactory( + sender, + receiver, + ds, + ).createEncryptedEnvelop('hello dm3 from ens'); + const incomingEnvelop2: EncryptionEnvelop = await MockMessageFactory( + sender, + receiverOnGno, + ds, + ).createEncryptedEnvelop('hello dm3 from gno'); + + await messageProcessor.processEnvelop(incomingEnvelop1); + await messageProcessor.processEnvelop(incomingEnvelop2); + + //createMessageCall + const [_, actualEnvelop] = createMessageMock.mock.calls[0]; + + expect(createMessageMock).toBeCalled(); + expect(createMessageMock).toBeCalledTimes(2); + expect(actualEnvelop['message']).toBe(incomingEnvelop1.message); + + const actualPostmark = await decryptAsymmetric( + receiver.profileKeys.encryptionKeyPair, + JSON.parse(actualEnvelop['postmark']), + ); + + // check postmark + const { incommingTimestamp, messageHash, signature } = + JSON.parse(actualPostmark); + expect(incommingTimestamp).toBeGreaterThanOrEqual(now); + expect(incommingTimestamp).toBeLessThanOrEqual(Date.now()); + expect(messageHash).toBe( + ethers.utils.hashMessage(stringify(incomingEnvelop1.message)), + ); + const postmarkWithoutSig: Omit = { + messageHash, + incommingTimestamp, + }; + expect( + await checkSignature( + ds.keys.signingKeyPair.publicKey, + sha256(stringify(postmarkWithoutSig)), + signature, + ), + ).toBe(true); + //Check if the message was submitted to the socket + expect(sendMock).not.toBeCalled(); + }); it('stores proper incoming message and submit it if receiver is connected to a socket', async () => { const sendMock = jest.fn(); diff --git a/packages/delivery-service/src/message/MessageProcessor.ts b/packages/delivery-service/src/message/MessageProcessor.ts index 5ad8a0175..a185788dc 100644 --- a/packages/delivery-service/src/message/MessageProcessor.ts +++ b/packages/delivery-service/src/message/MessageProcessor.ts @@ -75,6 +75,13 @@ export class MessageProcessor { ); console.debug('incomingMessage', deliveryInformation); + //the delivery service has to accept any message to the receiver regardelss of the place they have choosen to host their profile. + //That means no matter what name the receiver has chosen to use, the delivery service has to resolve it to the correct address + //i.E if alice.eth resolves to 0x123 + //and alice.gno resolves to 0x123 aswell, the ds has to accept both + const address = await this.provider.resolveName(deliveryInformation.to); + console.log(address); + const conversationId = getConversationId( //TODO look into dbIdEnsName await this.db.getIdEnsName(deliveryInformation.from), From 8cddf944e6703f78d611cd4b6f7366ba2f15806b Mon Sep 17 00:00:00 2001 From: malteish Date: Fri, 19 Jul 2024 13:23:01 +0200 Subject: [PATCH 017/148] rename doesAccountExist to hasAccount --- .../backend/src/persistence/getDatabase.ts | 5 ++-- .../backend/src/persistence/storage/index.ts | 4 +-- .../storage/postgres/doesAccountExist.ts | 28 +++++++++---------- packages/backend/src/storage.ts | 2 +- packages/delivery-service/src/delivery.ts | 2 +- .../delivery-service/src/notifications.ts | 2 +- .../persistence/account/doesAccountExist.ts | 2 +- .../src/persistence/account/index.ts | 4 +-- .../src/persistence/getDatabase.ts | 2 +- .../src/ws/WebSocketManager.ts | 2 +- packages/lib/delivery/src/Session.ts | 4 +-- .../lib/server-side/src/iSessionDatabase.ts | 2 +- packages/lib/server-side/src/utils.ts | 6 ++-- 13 files changed, 31 insertions(+), 34 deletions(-) diff --git a/packages/backend/src/persistence/getDatabase.ts b/packages/backend/src/persistence/getDatabase.ts index bf4f8fec4..5b9c4fe61 100644 --- a/packages/backend/src/persistence/getDatabase.ts +++ b/packages/backend/src/persistence/getDatabase.ts @@ -10,7 +10,6 @@ export enum RedisPrefix { Sync = 'sync:', // Account used to be called Session. The prefix still resolves to "session:" for now. Account = 'session:', - Session = 'session:', NotificationChannel = 'notificationChannel:', GlobalNotification = 'globalNotification:', Otp = 'otp:', @@ -61,7 +60,7 @@ export async function getDatabase( //Session setAccount: Storage.setAccount(prisma), getAccount: Storage.getAccount(prisma), - doesAccountExist: Storage.doesAccountExist(prisma), + hasAccount: Storage.hasAccount(prisma), //Storage AddConversation addConversation: Storage.addConversation(prisma), getConversationList: Storage.getConversationList(prisma), @@ -87,7 +86,7 @@ export async function getDatabase( export interface IBackendDatabase { setAccount: (ensName: string) => Promise; getAccount: (ensName: string) => Promise; - doesAccountExist: (ensName: string) => Promise; + hasAccount: (ensName: string) => Promise; addConversation: ( ensName: string, diff --git a/packages/backend/src/persistence/storage/index.ts b/packages/backend/src/persistence/storage/index.ts index c0903a97a..cc2c2b6cd 100644 --- a/packages/backend/src/persistence/storage/index.ts +++ b/packages/backend/src/persistence/storage/index.ts @@ -12,7 +12,7 @@ import { getHaltedMessages } from './postgres/haltedMessage/getHaltedMessages'; import { clearHaltedMessage } from './postgres/haltedMessage/clearHaltedMessage'; import { getAccount } from './postgres/getAccount'; import { setAccount } from './postgres/setAccount'; -import { doesAccountExist } from './postgres/doesAccountExist'; +import { hasAccount } from './postgres/hasAccount'; export default { addConversation, @@ -27,7 +27,7 @@ export default { clearHaltedMessage, getAccount, setAccount, - doesAccountExist, + hasAccount: hasAccount, }; export type { MessageRecord }; diff --git a/packages/backend/src/persistence/storage/postgres/doesAccountExist.ts b/packages/backend/src/persistence/storage/postgres/doesAccountExist.ts index 4c4382588..342c27694 100644 --- a/packages/backend/src/persistence/storage/postgres/doesAccountExist.ts +++ b/packages/backend/src/persistence/storage/postgres/doesAccountExist.ts @@ -1,17 +1,15 @@ import { PrismaClient } from '@prisma/client'; -export const doesAccountExist = - (db: PrismaClient) => async (ensName: string) => { - //Check if account exists - const account = await db.account.findFirst({ - where: { - id: ensName, - }, - }); - //If account exists, return true - if (account) { - return true; - } - //If account does not exist, return false - return false; - }; +/// Check if an account exists for a given ENS name +/// @param db +/// @returns true if account exists, false otherwise +export const hasAccount = (db: PrismaClient) => async (ensName: string) => { + //Check if account exists + const account = await db.account.findFirst({ + where: { + id: ensName, + }, + }); + + return !!account; +}; diff --git a/packages/backend/src/storage.ts b/packages/backend/src/storage.ts index 338099391..090ec0c6d 100644 --- a/packages/backend/src/storage.ts +++ b/packages/backend/src/storage.ts @@ -37,7 +37,7 @@ export default ( res, next, ensName, - db.doesAccountExist, + db.hasAccount, web3Provider, serverSecret, ); diff --git a/packages/delivery-service/src/delivery.ts b/packages/delivery-service/src/delivery.ts index 468beec75..723cbff02 100644 --- a/packages/delivery-service/src/delivery.ts +++ b/packages/delivery-service/src/delivery.ts @@ -43,7 +43,7 @@ export default ( res, next, ensName, - db.doesAccountExist, + db.hasAccount, web3Provider, serverSecret, ); diff --git a/packages/delivery-service/src/notifications.ts b/packages/delivery-service/src/notifications.ts index b16f8a576..9f89020c2 100644 --- a/packages/delivery-service/src/notifications.ts +++ b/packages/delivery-service/src/notifications.ts @@ -40,7 +40,7 @@ export default ( res, next, ensName, - db.doesAccountExist, + db.hasAccount, web3Provider, serverSecret, ); diff --git a/packages/delivery-service/src/persistence/account/doesAccountExist.ts b/packages/delivery-service/src/persistence/account/doesAccountExist.ts index d03d1defc..8c961f8b4 100644 --- a/packages/delivery-service/src/persistence/account/doesAccountExist.ts +++ b/packages/delivery-service/src/persistence/account/doesAccountExist.ts @@ -1,7 +1,7 @@ import { Redis, RedisPrefix } from '../getDatabase'; import { getIdEnsName } from '../getIdEnsName'; -export function doesAccountExist(redis: Redis) { +export function hasAccount(redis: Redis) { return async (ensName: string) => { let account = await redis.get( RedisPrefix.Account + (await getIdEnsName(redis)(ensName)), diff --git a/packages/delivery-service/src/persistence/account/index.ts b/packages/delivery-service/src/persistence/account/index.ts index ee0a1b6cc..e13cb3151 100644 --- a/packages/delivery-service/src/persistence/account/index.ts +++ b/packages/delivery-service/src/persistence/account/index.ts @@ -1,5 +1,5 @@ import { setAccount } from './setAccount'; import { getAccount } from './getAccount'; import { getIdEnsName } from '../getIdEnsName'; -import { doesAccountExist } from './doesAccountExist'; -export default { setAccount, getAccount, getIdEnsName, doesAccountExist }; +import { hasAccount } from './hasAccount'; +export default { setAccount, getAccount, getIdEnsName, hasAccount }; diff --git a/packages/delivery-service/src/persistence/getDatabase.ts b/packages/delivery-service/src/persistence/getDatabase.ts index e33d7eb48..253995aac 100644 --- a/packages/delivery-service/src/persistence/getDatabase.ts +++ b/packages/delivery-service/src/persistence/getDatabase.ts @@ -74,7 +74,7 @@ export async function getDatabase( // Account setAccount: Account.setAccount(redis), getAccount: Account.getAccount(redis), - doesAccountExist: Account.doesAccountExist(redis), + hasAccount: Account.hasAccount(redis), getIdEnsName: getIdEnsName(redis), syncAcknowledge: syncAcknowledge(redis), //Notification diff --git a/packages/delivery-service/src/ws/WebSocketManager.ts b/packages/delivery-service/src/ws/WebSocketManager.ts index f22171091..59ab13ac2 100644 --- a/packages/delivery-service/src/ws/WebSocketManager.ts +++ b/packages/delivery-service/src/ws/WebSocketManager.ts @@ -59,7 +59,7 @@ export class WebSocketManager implements IWebSocketManager { //Use the already existing function checkToken to check whether the token matches the provided ensName const hasSession = await checkToken( this.web3Provider, - this.db.doesAccountExist, + this.db.hasAccount, ensName, token, this.serverSecret, diff --git a/packages/lib/delivery/src/Session.ts b/packages/lib/delivery/src/Session.ts index db394c7bc..2b098e5a7 100644 --- a/packages/lib/delivery/src/Session.ts +++ b/packages/lib/delivery/src/Session.ts @@ -33,12 +33,12 @@ export interface Session { export async function checkToken( provider: ethers.providers.JsonRpcProvider, - doesAccountExist: (ensName: string) => Promise, + hasAccount: (ensName: string) => Promise, ensName: string, token: string, serverSecret: string, ): Promise { - if (!(await doesAccountExist(ensName.toLocaleLowerCase()))) { + if (!(await hasAccount(ensName.toLocaleLowerCase()))) { console.debug('there is no account for this ens name: ', ensName); return false; } diff --git a/packages/lib/server-side/src/iSessionDatabase.ts b/packages/lib/server-side/src/iSessionDatabase.ts index 2948dde32..d88a8ddee 100644 --- a/packages/lib/server-side/src/iSessionDatabase.ts +++ b/packages/lib/server-side/src/iSessionDatabase.ts @@ -9,5 +9,5 @@ export interface IAccountDatabase { }) | null >; - doesAccountExist: (ensName: string) => Promise; + hasAccount: (ensName: string) => Promise; } diff --git a/packages/lib/server-side/src/utils.ts b/packages/lib/server-side/src/utils.ts index ceeef7ddb..b7e0baecd 100644 --- a/packages/lib/server-side/src/utils.ts +++ b/packages/lib/server-side/src/utils.ts @@ -16,7 +16,7 @@ export async function auth( res: Response, next: NextFunction, ensName: string, - doesAccountExist: (ensName: string) => Promise, + hasAccount: (ensName: string) => Promise, web3Provider: ethers.providers.JsonRpcProvider, serverSecret: string, ) { @@ -28,7 +28,7 @@ export async function auth( token && (await checkToken( web3Provider, - doesAccountExist, + hasAccount, normalizedEnsName, token, serverSecret, @@ -59,7 +59,7 @@ export function socketAuth( if ( !(await checkToken( web3Provider, - db.doesAccountExist, + db.hasAccount, ensName, socket.handshake.auth.token as string, serverSecret, From e14667ab8e6fe2f2ab55f6d9b56f7ac0eb2cc75f Mon Sep 17 00:00:00 2001 From: malteish Date: Fri, 19 Jul 2024 15:20:31 +0200 Subject: [PATCH 018/148] fix tests --- .../backend/src/persistence/getDatabase.ts | 2 - .../persistence/session/setAccount.test.ts | 59 ++++--------- .../backend/src/persistence/storage/index.ts | 2 +- .../{doesAccountExist.ts => hasAccount.ts} | 0 packages/backend/src/profile.test.ts | 30 +++---- packages/backend/src/storage.test.ts | 44 ++-------- packages/lib/delivery/src/Session.test.ts | 77 +++++++---------- packages/lib/server-side/src/utils.test.ts | 86 +++---------------- 8 files changed, 80 insertions(+), 220 deletions(-) rename packages/backend/src/persistence/storage/postgres/{doesAccountExist.ts => hasAccount.ts} (100%) diff --git a/packages/backend/src/persistence/getDatabase.ts b/packages/backend/src/persistence/getDatabase.ts index 5b9c4fe61..d7eb627e2 100644 --- a/packages/backend/src/persistence/getDatabase.ts +++ b/packages/backend/src/persistence/getDatabase.ts @@ -50,10 +50,8 @@ export async function getPrismaClient() { } export async function getDatabase( - _redis?: Redis, _prisma?: PrismaClient, ): Promise { - const redis = _redis ?? (await getRedisClient()); const prisma = _prisma ?? (await getPrismaClient()); return { diff --git a/packages/backend/src/persistence/session/setAccount.test.ts b/packages/backend/src/persistence/session/setAccount.test.ts index 2e333d730..eff29e30e 100644 --- a/packages/backend/src/persistence/session/setAccount.test.ts +++ b/packages/backend/src/persistence/session/setAccount.test.ts @@ -1,60 +1,37 @@ -import { Redis, IDatabase, getRedisClient, getDatabase } from '../getDatabase'; import { UserProfile } from '@dm3-org/dm3-lib-profile'; -import { Session } from '@dm3-org/dm3-lib-delivery'; +import { PrismaClient } from '@prisma/client'; +import { IBackendDatabase, getDatabase, getPrismaClient } from '../getDatabase'; -const USER_ADDRESS = '0x25A643B6e52864d0eD816F1E43c0CF49C83B8292'; +const USER_NAME = '0x25A643B6e52864d0eD816F1E43c0CF49C83B8292.dm3.eth'; -describe('Set Session', () => { - let redisClient: Redis; - let db: IDatabase; +describe('Set Account', () => { + let prismaClient: PrismaClient; + let db: IBackendDatabase; beforeEach(async () => { - redisClient = await getRedisClient(); - db = await getDatabase(redisClient); - await redisClient.flushDb(); + prismaClient = await getPrismaClient(); + db = await getDatabase(prismaClient); }); afterEach(async () => { - await redisClient.flushDb(); - await redisClient.disconnect(); + // todo: flush or anything else to clear the database? }); - it('Creates a new Session ', async () => { + it('Creates a new Account ', async () => { const profile: UserProfile = { publicEncryptionKey: '', publicSigningKey: '', deliveryServices: [], }; - const session: Session = { - account: USER_ADDRESS, - signedUserProfile: { profile, signature: 'foo' }, - token: '', - createdAt: 0, - profileExtension: { - notSupportedMessageTypes: [], - }, - }; - const priorSetSession = await db.getAccount(USER_ADDRESS); - //User has no session yet - expect(priorSetSession).toBe(null); - await db.setAccount(USER_ADDRESS, session); - - const afterSetSession = await db.getAccount(USER_ADDRESS); - //User has no session yet - expect(afterSetSession?.signedUserProfile).toEqual({ - profile, - signature: 'foo', - }); - }); + const priorSetAccount = await db.getAccount(USER_NAME); + + //User has no account yet + expect(priorSetAccount).toBe(null); + await db.setAccount(USER_NAME); - it('Rejcts session with an invalid schema', async () => { - const invalidSession = {} as Session; - try { - await db.setAccount('foo', invalidSession); - fail(); - } catch (e) { - expect(e).toStrictEqual(Error('Invalid session')); - } + const afterSetAccount = await db.getAccount(USER_NAME); + //User has no account yet + expect(afterSetAccount?.id).toEqual(USER_NAME); }); }); diff --git a/packages/backend/src/persistence/storage/index.ts b/packages/backend/src/persistence/storage/index.ts index cc2c2b6cd..dd7fb55f3 100644 --- a/packages/backend/src/persistence/storage/index.ts +++ b/packages/backend/src/persistence/storage/index.ts @@ -27,7 +27,7 @@ export default { clearHaltedMessage, getAccount, setAccount, - hasAccount: hasAccount, + hasAccount, }; export type { MessageRecord }; diff --git a/packages/backend/src/persistence/storage/postgres/doesAccountExist.ts b/packages/backend/src/persistence/storage/postgres/hasAccount.ts similarity index 100% rename from packages/backend/src/persistence/storage/postgres/doesAccountExist.ts rename to packages/backend/src/persistence/storage/postgres/hasAccount.ts diff --git a/packages/backend/src/profile.test.ts b/packages/backend/src/profile.test.ts index 936dc696c..456e64e24 100644 --- a/packages/backend/src/profile.test.ts +++ b/packages/backend/src/profile.test.ts @@ -1,22 +1,21 @@ -import { - Session, - generateAuthJWT, - spamFilter, -} from '@dm3-org/dm3-lib-delivery'; +import { generateAuthJWT } from '@dm3-org/dm3-lib-delivery'; import { UserProfile, getProfileCreationMessage, } from '@dm3-org/dm3-lib-profile'; import { stringify } from '@dm3-org/dm3-lib-shared'; +import { Account } from '@prisma/client'; import bodyParser from 'body-parser'; import { ethers } from 'ethers'; import express from 'express'; import http from 'http'; import request from 'supertest'; -import { IDatabase } from './persistence/getDatabase'; +import { IBackendDatabase } from './persistence/getDatabase'; import profile from './profile'; import storage from './storage'; +// todo: create a web3 provider mock that returns a resolver and that thren returns a text when the respective functions +// are called const web3ProviderMock: ethers.providers.JsonRpcProvider = new ethers.providers.JsonRpcProvider(); @@ -26,7 +25,7 @@ let token = generateAuthJWT('alice.eth', serverSecret); const setUpApp = async ( app: express.Express, - db: IDatabase, + db: IBackendDatabase, web3Provider: ethers.providers.JsonRpcProvider, serverSecret: string = 'my-secret', ) => { @@ -36,20 +35,13 @@ const setUpApp = async ( }; const createDbMock = async () => { - const sessionMocked = { - challenge: '123', - token: 'deprecated token that is not used anymore', - signedUserProfile: {}, - } as Session & { spamFilterRules: spamFilter.SpamFilterRules }; + const accountMocked = { + id: 'alice.eth', + } as Account; const dbMock = { - getAccount: async (ensName: string) => - Promise.resolve< - Session & { - spamFilterRules: spamFilter.SpamFilterRules; - } - >(sessionMocked), // returns some valid session - setAccount: async (_: string, __: Session) => {}, + getAccount: async (ensName: string) => Promise.resolve(accountMocked), + setAccount: async (id: string) => {}, getIdEnsName: async (ensName: string) => ensName, }; diff --git a/packages/backend/src/storage.test.ts b/packages/backend/src/storage.test.ts index 3635eeba9..c97c44c4b 100644 --- a/packages/backend/src/storage.test.ts +++ b/packages/backend/src/storage.test.ts @@ -1,9 +1,4 @@ -import { - Session, - generateAuthJWT, - spamFilter, -} from '@dm3-org/dm3-lib-delivery'; -import { SignedUserProfile, schema } from '@dm3-org/dm3-lib-profile'; +import { generateAuthJWT } from '@dm3-org/dm3-lib-delivery'; import { sha256 } from '@dm3-org/dm3-lib-shared'; import { MockDeliveryServiceProfile, @@ -17,17 +12,10 @@ import bodyParser from 'body-parser'; import { ethers } from 'ethers'; import express from 'express'; import request from 'supertest'; -import { - IDatabase, - Redis, - getDatabase, - getRedisClient, -} from './persistence/getDatabase'; +import { IBackendDatabase, getDatabase } from './persistence/getDatabase'; import { MessageRecord } from './persistence/storage/postgres/dto/MessageRecord'; import storage from './storage'; -import fs from 'fs'; - const keysA = { encryptionKeyPair: { publicKey: 'eHmMq29FeiPKfNPkSctPuZGXvV0sKeO/KZkX2nXvMgw=', @@ -51,13 +39,10 @@ describe('Storage', () => { let sender: MockedUserProfile; let receiver: MockedUserProfile; let deliveryService: MockDeliveryServiceProfile; - let redisClient: Redis; beforeEach(async () => { prisma = new PrismaClient(); - redisClient = await getRedisClient(); - await redisClient.flushDb(); app = express(); app.use(bodyParser.json()); @@ -78,29 +63,12 @@ describe('Storage', () => { 'http://localhost:3000', ); - const db = await getDatabase(redisClient, prisma); - - const sessionMocked = { - challenge: '123', - token, - signedUserProfile: { - profile: { - publicSigningKey: keysA.signingKeyPair.publicKey, - }, - } as SignedUserProfile, - } as Session & { spamFilterRules: spamFilter.SpamFilterRules }; + const db = await getDatabase(prisma); const dbMocked = { - getAccount: async (ensName: string) => - Promise.resolve< - Session & { - spamFilterRules: spamFilter.SpamFilterRules; - } - >(sessionMocked), - setAccount: async (_: string, __: Session) => {}, - getIdEnsName: async (ensName: string) => ensName, + hasAccount: (ensName: string) => true, }; - const dbFinal: IDatabase = { ...db, ...dbMocked }; + const dbFinal: IBackendDatabase = { ...db, ...dbMocked }; //const web3ProviderBase = getWeb3Provider(process.env); @@ -121,8 +89,6 @@ describe('Storage', () => { await prisma.encryptedMessage.deleteMany(); await prisma.conversation.deleteMany(); await prisma.account.deleteMany(); - await redisClient.flushDb(); - await redisClient.disconnect(); }); describe('addConversation', () => { diff --git a/packages/lib/delivery/src/Session.test.ts b/packages/lib/delivery/src/Session.test.ts index e757ce9f8..48f93fceb 100644 --- a/packages/lib/delivery/src/Session.test.ts +++ b/packages/lib/delivery/src/Session.test.ts @@ -1,6 +1,6 @@ -import { checkToken, Session } from './Session'; import { sign, verify } from 'jsonwebtoken'; import { generateAuthJWT } from './Keys'; +import { checkToken } from './Session'; const serverSecret = 'veryImportantSecretToGenerateAndValidateJSONWebTokens'; // create valid jwt @@ -9,18 +9,14 @@ const token = generateAuthJWT('alice.eth', serverSecret); describe('Session', () => { describe('checkToken with state', () => { it('Should return true if the jwt is valid', async () => { - const getAccount = (_: string) => - Promise.resolve({ - token: token, - createdAt: new Date().getTime(), - } as Session); + const hasAccount = (_: string) => Promise.resolve(true); const isValid = await checkToken( { resolveName: async () => '0x25A643B6e52864d0eD816F1E43c0CF49C83B8292', } as any, - getAccount, + hasAccount, 'alice.eth', token, serverSecret, @@ -30,14 +26,14 @@ describe('Session', () => { }); it('Should return false if no session exists for the account ', async () => { - const getAccount = (_: string) => Promise.resolve(null); + const hasAccount = (_: string) => Promise.resolve(false); const isValid = await checkToken( { resolveName: async () => '0x25A643B6e52864d0eD816F1E43c0CF49C83B8292', } as any, - getAccount, + hasAccount, 'alice.eth', token, serverSecret, @@ -48,15 +44,14 @@ describe('Session', () => { it('Should return false if the token is signed with a different secret ', async () => { const token = generateAuthJWT('alice.eth', 'attackersSecret'); - const getAccount = (_: string) => - Promise.resolve({ token: 'bar' } as Session); + const hasAccount = (_: string) => Promise.resolve(true); const isValid = await checkToken( { resolveName: async () => '0x25A643B6e52864d0eD816F1E43c0CF49C83B8292', } as any, - getAccount, + hasAccount, 'alice.eth', token, serverSecret, @@ -66,8 +61,7 @@ describe('Session', () => { }); it('Should return false if a session exists but the token is expired ', async () => { - const getAccount = (_: string) => - Promise.resolve({ token: 'foo', createdAt: 1 } as Session); + const hasAccount = (_: string) => Promise.resolve(true); const oneMinuteAgo = new Date().getTime() / 1000 - 60; // this token expired a minute ago @@ -86,7 +80,7 @@ describe('Session', () => { resolveName: async () => '0x25A643B6e52864d0eD816F1E43c0CF49C83B8292', } as any, - getAccount, + hasAccount, 'alice.eth', _token, serverSecret, @@ -96,8 +90,7 @@ describe('Session', () => { }); it('Should return false if token issuance date is in the future ', async () => { - const getAccount = (_: string) => - Promise.resolve({ token: 'foo', createdAt: 1 } as Session); + const hasAccount = (_: string) => Promise.resolve(true); const tokenBody = verify(token, serverSecret); if ( @@ -118,7 +111,7 @@ describe('Session', () => { resolveName: async () => '0x25A643B6e52864d0eD816F1E43c0CF49C83B8292', } as any, - getAccount, + hasAccount, 'alice.eth', token, serverSecret, @@ -142,7 +135,7 @@ describe('Session', () => { resolveName: async () => '0x25A643B6e52864d0eD816F1E43c0CF49C83B8292', } as any, - getAccount, + hasAccount, 'alice.eth', _token, serverSecret, @@ -153,8 +146,7 @@ describe('Session', () => { }); describe('checkToken is not missing information', () => { it('Should return false if iat is missing', async () => { - const getAccount = (_: string) => - Promise.resolve({ token: 'foo', createdAt: 1 } as Session); + const hasAccount = (_: string) => Promise.resolve(true); const tokenBody = verify(token, serverSecret); if ( @@ -175,7 +167,7 @@ describe('Session', () => { resolveName: async () => '0x25A643B6e52864d0eD816F1E43c0CF49C83B8292', } as any, - getAccount, + hasAccount, 'alice.eth', token, serverSecret, @@ -199,7 +191,7 @@ describe('Session', () => { resolveName: async () => '0x25A643B6e52864d0eD816F1E43c0CF49C83B8292', } as any, - getAccount, + hasAccount, 'alice.eth', _invalidToken, serverSecret, @@ -209,8 +201,7 @@ describe('Session', () => { }); it('Should return false if nbf is missing', async () => { - const getAccount = (_: string) => - Promise.resolve({ token: 'foo', createdAt: 1 } as Session); + const hasAccount = (_: string) => Promise.resolve(true); const tokenBody = verify(token, serverSecret); if ( @@ -231,7 +222,7 @@ describe('Session', () => { resolveName: async () => '0x25A643B6e52864d0eD816F1E43c0CF49C83B8292', } as any, - getAccount, + hasAccount, 'alice.eth', token, serverSecret, @@ -256,7 +247,7 @@ describe('Session', () => { resolveName: async () => '0x25A643B6e52864d0eD816F1E43c0CF49C83B8292', } as any, - getAccount, + hasAccount, 'alice.eth', _invalidToken, serverSecret, @@ -265,9 +256,8 @@ describe('Session', () => { expect(isValid).toBe(false); }); - it('Should return false if account is missing', async () => { - const getAccount = (_: string) => - Promise.resolve({ token: 'foo', createdAt: 1 } as Session); + it('Should return false if key "account" is missing', async () => { + const hasAccount = (_: string) => Promise.resolve(true); const tokenBody = verify(token, serverSecret); if ( @@ -288,7 +278,7 @@ describe('Session', () => { resolveName: async () => '0x25A643B6e52864d0eD816F1E43c0CF49C83B8292', } as any, - getAccount, + hasAccount, 'alice.eth', token, serverSecret, @@ -313,7 +303,7 @@ describe('Session', () => { resolveName: async () => '0x25A643B6e52864d0eD816F1E43c0CF49C83B8292', } as any, - getAccount, + hasAccount, 'alice.eth', _invalidToken, serverSecret, @@ -323,8 +313,7 @@ describe('Session', () => { }); it('Should return false if exp is missing', async () => { - const getAccount = (_: string) => - Promise.resolve({ token: 'foo', createdAt: 1 } as Session); + const hasAccount = (_: string) => Promise.resolve(true); const tokenBody = verify(token, serverSecret); if ( @@ -345,7 +334,7 @@ describe('Session', () => { resolveName: async () => '0x25A643B6e52864d0eD816F1E43c0CF49C83B8292', } as any, - getAccount, + hasAccount, 'alice.eth', token, serverSecret, @@ -370,7 +359,7 @@ describe('Session', () => { resolveName: async () => '0x25A643B6e52864d0eD816F1E43c0CF49C83B8292', } as any, - getAccount, + hasAccount, 'alice.eth', _invalidToken, serverSecret, @@ -379,10 +368,9 @@ describe('Session', () => { expect(isValid).toBe(false); }); }); - describe('checkToken does not contain unexpeted keys', () => { + describe('checkToken does not contain unexpected keys', () => { it('Should return false if challenge is present', async () => { - const getAccount = (_: string) => - Promise.resolve({ token: 'foo', createdAt: 1 } as Session); + const hasAccount = (_: string) => Promise.resolve(true); const tokenBody = verify(token, serverSecret); if ( @@ -403,7 +391,7 @@ describe('Session', () => { resolveName: async () => '0x25A643B6e52864d0eD816F1E43c0CF49C83B8292', } as any, - getAccount, + hasAccount, 'alice.eth', token, serverSecret, @@ -429,7 +417,7 @@ describe('Session', () => { resolveName: async () => '0x25A643B6e52864d0eD816F1E43c0CF49C83B8292', } as any, - getAccount, + hasAccount, 'alice.eth', _invalidToken, serverSecret, @@ -438,8 +426,7 @@ describe('Session', () => { expect(isValid).toBe(false); }); it('Should return false if some additional key is present', async () => { - const getAccount = (_: string) => - Promise.resolve({ token: 'foo', createdAt: 1 } as Session); + const hasAccount = (_: string) => Promise.resolve(true); const tokenBody = verify(token, serverSecret); if ( @@ -460,7 +447,7 @@ describe('Session', () => { resolveName: async () => '0x25A643B6e52864d0eD816F1E43c0CF49C83B8292', } as any, - getAccount, + hasAccount, 'alice.eth', token, serverSecret, @@ -486,7 +473,7 @@ describe('Session', () => { resolveName: async () => '0x25A643B6e52864d0eD816F1E43c0CF49C83B8292', } as any, - getAccount, + hasAccount, 'alice.eth', _invalidToken, serverSecret, diff --git a/packages/lib/server-side/src/utils.test.ts b/packages/lib/server-side/src/utils.test.ts index e8e91bfed..02e2e5eeb 100644 --- a/packages/lib/server-side/src/utils.test.ts +++ b/packages/lib/server-side/src/utils.test.ts @@ -18,20 +18,7 @@ describe('Utils', () => { notBefore: 0, }); - const getAccount = async (accountAddress: string) => - Promise.resolve({ - signedUserProfile: {}, - token: 'testToken', - createdAt: new Date().getTime(), - }); - const setAccount = async (_: string, __: any) => { - return (_: any, __: any, ___: any) => {}; - }; - - const db = { - getAccount, - setAccount, - }; + const hasAccount = (_: string) => Promise.resolve(true); const web3Provider = { resolveName: async () => @@ -55,7 +42,7 @@ describe('Utils', () => { res, next, ensName, - db as any, + hasAccount, web3Provider as any, serverSecret, ); @@ -80,20 +67,7 @@ describe('Utils', () => { notBefore: 0, }); - const getAccount = async (accountAddress: string) => - Promise.resolve({ - signedUserProfile: {}, - token: 'testToken', - createdAt: new Date().getTime(), - }); - const setAccount = async (_: string, __: any) => { - return (_: any, __: any, ___: any) => {}; - }; - - const db = { - getAccount, - setAccount, - }; + const hasAccount = (_: string) => Promise.resolve(true); const web3Provider = { resolveName: async () => @@ -117,7 +91,7 @@ describe('Utils', () => { res, next, ensName, - db as any, + hasAccount, web3Provider as any, serverSecret, ); @@ -140,13 +114,8 @@ describe('Utils', () => { const token = sign({ account: 'alice.eth' }, serverSecret, { expiresIn: '1h', }); - const db = { - getAccount: async (accountAddress: string) => - Promise.resolve(null), - setAccount: async (_: string, __: any) => { - return (_: any, __: any, ___: any) => {}; - }, - }; + + const hasAccount = (_: string) => Promise.resolve(false); const web3Provider = { resolveName: async () => @@ -170,7 +139,7 @@ describe('Utils', () => { res, next, ensName, - db as any, + hasAccount, web3Provider as any, serverSecret, ); @@ -203,16 +172,7 @@ describe('Utils', () => { const token = sign({ account: 'some.other.name' }, serverSecret, { expiresIn: '1h', }); - const db = { - getAccount: async (accountAddress: string) => - Promise.resolve({ - signedUserProfile: {}, - token: 'foo', - }), - setAccount: async (_: string, __: any) => { - return (_: any, __: any, ___: any) => {}; - }, - }; + const hasAccount = (_: string) => Promise.resolve(true); const web3Provider = { resolveName: async () => @@ -236,7 +196,7 @@ describe('Utils', () => { res, next, ensName, - db as any, + hasAccount, web3Provider as any, serverSecret, ); @@ -281,17 +241,7 @@ describe('Utils', () => { serverSecret, ); - const db = { - getAccount: async (accountAddress: string) => - Promise.resolve({ - signedUserProfile: {}, - token: 'foo', - createdAt: 1, - }), - setAccount: async (_: string, __: any) => { - return (_: any, __: any, ___: any) => {}; - }, - }; + const hasAccount = (_: string) => Promise.resolve(true); const web3Provider = { resolveName: async () => @@ -315,7 +265,7 @@ describe('Utils', () => { res, next, ensName, - db as any, + hasAccount, web3Provider as any, serverSecret, ); @@ -380,17 +330,7 @@ describe('Utils', () => { }, serverSecret, ); - const db = { - getAccount: async (accountAddress: string) => - Promise.resolve({ - signedUserProfile: {}, - token: 'foo', - createdAt: 1, - }), - setAccount: async (_: string, __: any) => { - return (_: any, __: any, ___: any) => {}; - }, - }; + const hasAccount = (_: string) => Promise.resolve(true); const web3Provider = { resolveName: async () => @@ -414,7 +354,7 @@ describe('Utils', () => { res, next, ensName, - db as any, + hasAccount, web3Provider as any, serverSecret, ); From 05753f9dd47e7e3247bf8ecf40e84313f81a371a Mon Sep 17 00:00:00 2001 From: malteish Date: Fri, 19 Jul 2024 15:25:31 +0200 Subject: [PATCH 019/148] rename session 2 account --- .../backend/src/persistence/{session => account}/getAccount.ts | 0 packages/backend/src/persistence/{session => account}/index.ts | 0 .../src/persistence/{session => account}/setAccount.test.ts | 0 .../backend/src/persistence/{session => account}/setAccount.ts | 0 4 files changed, 0 insertions(+), 0 deletions(-) rename packages/backend/src/persistence/{session => account}/getAccount.ts (100%) rename packages/backend/src/persistence/{session => account}/index.ts (100%) rename packages/backend/src/persistence/{session => account}/setAccount.test.ts (100%) rename packages/backend/src/persistence/{session => account}/setAccount.ts (100%) diff --git a/packages/backend/src/persistence/session/getAccount.ts b/packages/backend/src/persistence/account/getAccount.ts similarity index 100% rename from packages/backend/src/persistence/session/getAccount.ts rename to packages/backend/src/persistence/account/getAccount.ts diff --git a/packages/backend/src/persistence/session/index.ts b/packages/backend/src/persistence/account/index.ts similarity index 100% rename from packages/backend/src/persistence/session/index.ts rename to packages/backend/src/persistence/account/index.ts diff --git a/packages/backend/src/persistence/session/setAccount.test.ts b/packages/backend/src/persistence/account/setAccount.test.ts similarity index 100% rename from packages/backend/src/persistence/session/setAccount.test.ts rename to packages/backend/src/persistence/account/setAccount.test.ts diff --git a/packages/backend/src/persistence/session/setAccount.ts b/packages/backend/src/persistence/account/setAccount.ts similarity index 100% rename from packages/backend/src/persistence/session/setAccount.ts rename to packages/backend/src/persistence/account/setAccount.ts From 6669291c65eb4559d94e68a0460642ab63a3648c Mon Sep 17 00:00:00 2001 From: malteish Date: Fri, 19 Jul 2024 17:06:36 +0200 Subject: [PATCH 020/148] rename --- .../persistence/account/{doesAccountExist.ts => hasAccount.ts} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename packages/delivery-service/src/persistence/account/{doesAccountExist.ts => hasAccount.ts} (100%) diff --git a/packages/delivery-service/src/persistence/account/doesAccountExist.ts b/packages/delivery-service/src/persistence/account/hasAccount.ts similarity index 100% rename from packages/delivery-service/src/persistence/account/doesAccountExist.ts rename to packages/delivery-service/src/persistence/account/hasAccount.ts From 25166afec5b9de511e2f04d3060ead024fff6590 Mon Sep 17 00:00:00 2001 From: AlexNi245 Date: Mon, 22 Jul 2024 13:22:11 +0200 Subject: [PATCH 021/148] dont use old alias ens name for get and set account --- .../src/persistence/account/getAccount.ts | 22 +++---- .../persistence/account/setAccount.test.ts | 60 +++++++++++++++++-- .../src/persistence/account/setAccount.ts | 19 ++++-- packages/lib/delivery/src/Session.ts | 3 +- .../lib/server-side/src/iSessionDatabase.ts | 8 +-- 5 files changed, 83 insertions(+), 29 deletions(-) diff --git a/packages/delivery-service/src/persistence/account/getAccount.ts b/packages/delivery-service/src/persistence/account/getAccount.ts index adc54bdef..85ed788a6 100644 --- a/packages/delivery-service/src/persistence/account/getAccount.ts +++ b/packages/delivery-service/src/persistence/account/getAccount.ts @@ -1,17 +1,19 @@ +import { Session } from '@dm3-org/dm3-lib-delivery'; +import { ethers } from 'ethers'; import { Redis, RedisPrefix } from '../getDatabase'; -import { Session, spamFilter } from '@dm3-org/dm3-lib-delivery'; -import { getIdEnsName } from '../getIdEnsName'; export function getAccount(redis: Redis) { - return async (ensName: string) => { - let session = await redis.get( - RedisPrefix.Account + (await getIdEnsName(redis)(ensName)), + return async (address: string) => { + console.log(ethers); + const session = await redis.get( + RedisPrefix.Account + ethers.utils.getAddress(address), ); - return session - ? (JSON.parse(session) as Session & { - spamFilterRules: spamFilter.SpamFilterRules; - }) - : null; + if (!session) { + console.debug('there is no account for this address: ', address); + return null; + } + + return JSON.parse(session) as Session; }; } diff --git a/packages/delivery-service/src/persistence/account/setAccount.test.ts b/packages/delivery-service/src/persistence/account/setAccount.test.ts index 259a77c19..b5099c65b 100644 --- a/packages/delivery-service/src/persistence/account/setAccount.test.ts +++ b/packages/delivery-service/src/persistence/account/setAccount.test.ts @@ -1,14 +1,9 @@ import { Redis, IDatabase, getRedisClient, getDatabase } from '../getDatabase'; import { UserProfile } from '@dm3-org/dm3-lib-profile'; import { Session } from '@dm3-org/dm3-lib-delivery'; -import winston from 'winston'; const USER_ADDRESS = '0x25A643B6e52864d0eD816F1E43c0CF49C83B8292'; -global.logger = winston.createLogger({ - transports: [new winston.transports.Console()], -}); - describe('Set Session', () => { let redisClient: Redis; let db: IDatabase; @@ -52,6 +47,61 @@ describe('Set Session', () => { signature: 'foo', }); }); + it('Creates a new Session and uses normalized address', async () => { + const profile: UserProfile = { + publicEncryptionKey: '', + publicSigningKey: '', + deliveryServices: [], + }; + const session: Session = { + // Address is not normalized + account: USER_ADDRESS.toUpperCase(), + signedUserProfile: { profile, signature: 'foo' }, + token: '', + createdAt: 0, + profileExtension: { + notSupportedMessageTypes: [], + }, + }; + + const priorSetSession = await db.getAccount(USER_ADDRESS); + //User has no session yet + expect(priorSetSession).toBe(null); + await db.setAccount(USER_ADDRESS, session); + + const afterSetSession = await db.getAccount(USER_ADDRESS); + //User has no session yet + expect(afterSetSession?.signedUserProfile).toEqual({ + profile, + signature: 'foo', + }); + }); + + it('Rejects session with an invalid address', async () => { + const profile: UserProfile = { + publicEncryptionKey: '', + publicSigningKey: '', + deliveryServices: [], + }; + const session: Session = { + account: USER_ADDRESS, + signedUserProfile: { + profile, + signature: '', + }, + token: '', + createdAt: 0, + profileExtension: { + notSupportedMessageTypes: [], + }, + }; + try { + await db.setAccount('foo', session); + fail(); + } catch (e) { + expect(e).toStrictEqual(Error('Invalid address')); + } + }); it('Rejects session with an invalid schema', async () => { const invalidSession = {} as Session; diff --git a/packages/delivery-service/src/persistence/account/setAccount.ts b/packages/delivery-service/src/persistence/account/setAccount.ts index fd307296d..408e52191 100644 --- a/packages/delivery-service/src/persistence/account/setAccount.ts +++ b/packages/delivery-service/src/persistence/account/setAccount.ts @@ -1,18 +1,25 @@ -import { Redis, RedisPrefix } from '../getDatabase'; import { Session, schema } from '@dm3-org/dm3-lib-delivery'; -import { validateSchema, stringify } from '@dm3-org/dm3-lib-shared'; -import { normalizeEnsName } from '@dm3-org/dm3-lib-profile'; -import { getIdEnsName } from '../getIdEnsName'; +import { stringify, validateSchema } from '@dm3-org/dm3-lib-shared'; +import { ethers } from 'ethers'; +import { Redis, RedisPrefix } from '../getDatabase'; export function setAccount(redis: Redis) { - return async (ensName: string, session: Session) => { + return async (address: string, session: Session) => { const isValid = validateSchema(schema.Session, session); + const isAddess = ethers.utils.isAddress(address); if (!isValid) { + console.debug('Invalid session: ', session); throw Error('Invalid session'); } + + if (!isAddess) { + console.debug('Invalid address: ', address); + throw Error('Invalid address'); + } + await redis.set( - RedisPrefix.Account + (await getIdEnsName(redis)(ensName)), + RedisPrefix.Account + ethers.utils.getAddress(address), stringify(session), ); }; diff --git a/packages/lib/delivery/src/Session.ts b/packages/lib/delivery/src/Session.ts index cd7f2c646..3e68f82cd 100644 --- a/packages/lib/delivery/src/Session.ts +++ b/packages/lib/delivery/src/Session.ts @@ -2,6 +2,7 @@ import { ProfileExtension, SignedUserProfile } from '@dm3-org/dm3-lib-profile'; import { validateSchema } from '@dm3-org/dm3-lib-shared'; import { ethers } from 'ethers'; import { decode, verify } from 'jsonwebtoken'; +import { SpamFilterRules } from './spam-filter'; //1Year const TTL = 31536000000; @@ -28,7 +29,7 @@ export interface Session { challenge?: string; profileExtension: ProfileExtension; //TODO use SpamFilterRules once spam-filer is ready - spamFilterRules?: unknown; + spamFilterRules?: SpamFilterRules; } export async function checkToken( diff --git a/packages/lib/server-side/src/iSessionDatabase.ts b/packages/lib/server-side/src/iSessionDatabase.ts index 422b9872f..723dc6591 100644 --- a/packages/lib/server-side/src/iSessionDatabase.ts +++ b/packages/lib/server-side/src/iSessionDatabase.ts @@ -2,11 +2,5 @@ import { Session as DSSession, spamFilter } from '@dm3-org/dm3-lib-delivery'; export interface IAccountDatabase { setAccount: (ensName: string, session: DSSession) => Promise; - - getAccount: (ensName: string) => Promise< - | (DSSession & { - spamFilterRules: spamFilter.SpamFilterRules; - }) - | null - >; + getAccount: (ensName: string) => Promise; } From cc506eef6f5276394c7cfda94ccc0ab0ad53e058 Mon Sep 17 00:00:00 2001 From: AlexNi245 Date: Mon, 22 Jul 2024 13:22:45 +0200 Subject: [PATCH 022/148] use addr in submitUserProfile --- packages/lib/delivery/src/UserProfile.test.ts | 11 ++++----- packages/lib/delivery/src/UserProfile.ts | 24 +++++++++++-------- 2 files changed, 18 insertions(+), 17 deletions(-) diff --git a/packages/lib/delivery/src/UserProfile.test.ts b/packages/lib/delivery/src/UserProfile.test.ts index b601bf91c..2391be387 100644 --- a/packages/lib/delivery/src/UserProfile.test.ts +++ b/packages/lib/delivery/src/UserProfile.test.ts @@ -49,10 +49,9 @@ describe('UserProfile', () => { await expect(async () => { await submitUserProfile( - { resolveName: () => RANDO_ADDRESS } as any, getAccount, setAccount, - RANDO_NAME, + RANDO_ADDRESS, singedUserProfile, 'my-secret', ); @@ -81,17 +80,16 @@ describe('UserProfile', () => { }; }; - return session(SENDER_NAME, '123', emptyProfile); + return session(SENDER_ADDRESS, '123', emptyProfile); }; const singedUserProfile = await signProfile(emptyProfile); await expect(async () => { await submitUserProfile( - { resolveName: () => SENDER_ADDRESS } as any, getAccount, setAccount, - SENDER_NAME, + SENDER_ADDRESS, singedUserProfile, 'my-secret', ); @@ -105,10 +103,9 @@ describe('UserProfile', () => { const singedUserProfile = await signProfile(emptyProfile); await submitUserProfile( - { resolveName: () => SENDER_ADDRESS } as any, getAccount, setAccount, - SENDER_NAME, + SENDER_ADDRESS, singedUserProfile, 'my-secret', ); diff --git a/packages/lib/delivery/src/UserProfile.ts b/packages/lib/delivery/src/UserProfile.ts index d323a4305..38c96ebf6 100644 --- a/packages/lib/delivery/src/UserProfile.ts +++ b/packages/lib/delivery/src/UserProfile.ts @@ -1,6 +1,6 @@ import { SignedUserProfile, - checkUserProfile, + checkUserProfileWithAddress, getDefaultProfileExtension, normalizeEnsName, } from '@dm3-org/dm3-lib-profile'; @@ -10,32 +10,36 @@ import { generateAuthJWT } from './Keys'; import { Session } from './Session'; export async function submitUserProfile( - provider: ethers.providers.JsonRpcProvider, getAccount: (accountAddress: string) => Promise, setAccount: (accountAddress: string, session: Session) => Promise, - ensName: string, + address: string, signedUserProfile: SignedUserProfile, serverSecret: string, ): Promise { - const account = normalizeEnsName(ensName); - - if (!(await checkUserProfile(provider, signedUserProfile, account))) { + if (!ethers.utils.isAddress(address)) { + logDebug('submitUserProfile - Invalid address'); + throw Error('Invalid address'); + } + //normalize the address + const _address = ethers.utils.getAddress(address); + // check if the submitted profile is has been signed by the adddress that want's to submit the profile + if (!(await checkUserProfileWithAddress(signedUserProfile, _address))) { logDebug('submitUserProfile - Signature invalid'); throw Error('Signature invalid.'); } - if (await getAccount(account)) { + if (await getAccount(_address)) { logDebug('submitUserProfile - Profile exists already'); throw Error('Profile exists already'); } const session: Session = { - account, + account: _address, signedUserProfile, - token: generateAuthJWT(ensName, serverSecret), + token: generateAuthJWT(_address, serverSecret), createdAt: new Date().getTime(), profileExtension: getDefaultProfileExtension(), }; logDebug({ text: 'submitUserProfile', session }); - await setAccount(account.toLocaleLowerCase(), session); + await setAccount(_address, session); return session.token; } From 77cb0d2d84b283e7db164c34db3455878b8f1a1f Mon Sep 17 00:00:00 2001 From: AlexNi245 Date: Mon, 22 Jul 2024 13:22:57 +0200 Subject: [PATCH 023/148] get rid of getIdEnsName --- .../src/persistence/getIdEnsName.ts | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/packages/delivery-service/src/persistence/getIdEnsName.ts b/packages/delivery-service/src/persistence/getIdEnsName.ts index 533df28ba..409842429 100644 --- a/packages/delivery-service/src/persistence/getIdEnsName.ts +++ b/packages/delivery-service/src/persistence/getIdEnsName.ts @@ -1,15 +1,7 @@ -import { Redis, RedisPrefix } from './getDatabase'; import { normalizeEnsName } from '@dm3-org/dm3-lib-profile'; +import { Redis } from './getDatabase'; +//Todo replace db function get Db name export function getIdEnsName(redis: Redis) { - const resolveAlias = async (ensName: string): Promise => { - const lowerEnsName = normalizeEnsName( - (await redis.get( - RedisPrefix.Account + 'alias:' + normalizeEnsName(ensName), - )) ?? ensName, - ); - - return lowerEnsName === ensName ? ensName : resolveAlias(lowerEnsName); - }; - return resolveAlias; + return (ensName: string) => Promise.resolve(normalizeEnsName(ensName)); } From 6113c060070efc065cb4ec12d69f84fd910c7c70 Mon Sep 17 00:00:00 2001 From: AlexNi245 Date: Mon, 22 Jul 2024 16:39:13 +0200 Subject: [PATCH 024/148] move profile endpoint to server side --- packages/backend/src/index.ts | 2 +- packages/backend/src/profile.test.ts | 168 ---------- packages/delivery-service/src/index.ts | 6 +- .../src/persistence/getDatabase.ts | 13 +- packages/delivery-service/src/profile.test.ts | 144 --------- packages/delivery-service/src/profile.ts | 82 ----- packages/lib/server-side/src/index.ts | 1 + packages/lib/server-side/src/profile.test.ts | 302 ++++++++++++++++++ .../server-side}/src/profile.ts | 42 ++- 9 files changed, 340 insertions(+), 420 deletions(-) delete mode 100644 packages/backend/src/profile.test.ts delete mode 100644 packages/delivery-service/src/profile.test.ts delete mode 100644 packages/delivery-service/src/profile.ts create mode 100644 packages/lib/server-side/src/profile.test.ts rename packages/{backend => lib/server-side}/src/profile.ts (64%) diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index 3fc9ef1bc..cdaed9657 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -1,5 +1,6 @@ import { Auth, + Profile, errorHandler, getCachedWebProvider, getServerSecret, @@ -14,7 +15,6 @@ import express from 'express'; import http from 'http'; import path from 'path'; import { getDatabase } from './persistence/getDatabase'; -import Profile from './profile'; import Storage from './storage'; const app = express(); diff --git a/packages/backend/src/profile.test.ts b/packages/backend/src/profile.test.ts deleted file mode 100644 index 936dc696c..000000000 --- a/packages/backend/src/profile.test.ts +++ /dev/null @@ -1,168 +0,0 @@ -import { - Session, - generateAuthJWT, - spamFilter, -} from '@dm3-org/dm3-lib-delivery'; -import { - UserProfile, - getProfileCreationMessage, -} from '@dm3-org/dm3-lib-profile'; -import { stringify } from '@dm3-org/dm3-lib-shared'; -import bodyParser from 'body-parser'; -import { ethers } from 'ethers'; -import express from 'express'; -import http from 'http'; -import request from 'supertest'; -import { IDatabase } from './persistence/getDatabase'; -import profile from './profile'; -import storage from './storage'; - -const web3ProviderMock: ethers.providers.JsonRpcProvider = - new ethers.providers.JsonRpcProvider(); - -const serverSecret = 'veryImportantSecretToGenerateAndValidateJSONWebTokens'; - -let token = generateAuthJWT('alice.eth', serverSecret); - -const setUpApp = async ( - app: express.Express, - db: IDatabase, - web3Provider: ethers.providers.JsonRpcProvider, - serverSecret: string = 'my-secret', -) => { - app.use(bodyParser.json()); - const server = http.createServer(app); - app.use(profile(db, web3Provider, serverSecret)); -}; - -const createDbMock = async () => { - const sessionMocked = { - challenge: '123', - token: 'deprecated token that is not used anymore', - signedUserProfile: {}, - } as Session & { spamFilterRules: spamFilter.SpamFilterRules }; - - const dbMock = { - getAccount: async (ensName: string) => - Promise.resolve< - Session & { - spamFilterRules: spamFilter.SpamFilterRules; - } - >(sessionMocked), // returns some valid session - setAccount: async (_: string, __: Session) => {}, - getIdEnsName: async (ensName: string) => ensName, - }; - - return dbMock as any; -}; - -describe('Profile', () => { - describe('getProfile', () => { - it('Returns 200 if user profile exists', async () => { - const app = express(); - - const db = await createDbMock(); - - const _web3Provider = { - resolveName: async () => - '0x95222290DD7278Aa3Ddd389Cc1E1d165CC4BAfe5', - }; - // I don't know why this function is needed in this test. - // Remove it after storage migration. - db.getUserStorage = () => {}; - app.use(storage(db, _web3Provider as any, serverSecret)); - setUpApp(app, db, web3ProviderMock); - - const response = await request(app) - .get('/alice.eth') - .set({ - authorization: 'Bearer ' + token, - }) - .send(); - const status = response.status; - - expect(status).toBe(200); - }); - }); - - describe('submitUserProfile', () => { - it('Returns 200 if user profile creation was successful', async () => { - const mnemonic = - 'announce room limb pattern dry unit scale effort smooth jazz weasel alcohol'; - - const wallet = ethers.Wallet.fromMnemonic(mnemonic); - - // this provider must return the address of the wallet when resolveName is called - const _web3ProviderMock = { - resolveName: async () => wallet.address, - }; - // the db must return null when getAccount is called - const _dbMock = { - getAccount: async (ensName: string) => Promise.resolve(null), - setAccount: async (_: string, __: any) => { - return (_: any, __: any, ___: any) => {}; - }, - getIdEnsName: async (ensName: string) => ensName, - }; - - const app = express(); - setUpApp(app, _dbMock as any, _web3ProviderMock as any); - - const userProfile: UserProfile = { - publicSigningKey: '2', - publicEncryptionKey: '1', - deliveryServices: [], - }; - - const createUserProfileMessage = getProfileCreationMessage( - stringify(userProfile), - wallet.address, - ); - const signature = await wallet.signMessage( - createUserProfileMessage, - ); - - const signedUserProfile = { - profile: userProfile, - signature, - }; - - const response = await request(app) - .post(`/${wallet.address}`) - .send(signedUserProfile); - - const status = response.status; - - expect(status).toBe(200); - }); - - it('Returns 400 if schema is invalid', async () => { - const app = express(); - setUpApp(app, await createDbMock(), web3ProviderMock); - - const userProfile: UserProfile = { - publicSigningKey: '2', - publicEncryptionKey: '1', - deliveryServices: [], - }; - - const mnemonic = - 'announce room limb pattern dry unit scale effort smooth jazz weasel alcohol'; - - const wallet = ethers.Wallet.fromMnemonic(mnemonic); - - const signature = await wallet.signMessage(stringify(userProfile)); - - const signedUserProfile = { - profile: userProfile, - signature: null, - }; - - const { status } = await request(app) - .post(`/1234`) - .send(signedUserProfile); - - expect(status).toBe(400); - }); - }); -}); diff --git a/packages/delivery-service/src/index.ts b/packages/delivery-service/src/index.ts index d782743ab..10b96e57f 100644 --- a/packages/delivery-service/src/index.ts +++ b/packages/delivery-service/src/index.ts @@ -1,5 +1,6 @@ import { Auth, + Profile, errorHandler, getCachedWebProvider, getServerSecret, @@ -23,7 +24,6 @@ import Delivery from './delivery'; import { onConnection } from './messaging'; import Notifications from './notifications'; import { getDatabase } from './persistence/getDatabase'; -import Profile from './profile'; import RpcProxy from './rpc/rpc-proxy'; import { WebSocketManager } from './ws/WebSocketManager'; import webpush from 'web-push'; @@ -91,8 +91,8 @@ global.logger = winston.createLogger({ return res.send('Hello DM3'); }); - app.use('/auth', Auth(db.getAccount as any, serverSecret)); - app.use('/profile', Profile(db, web3Provider, io, serverSecret)); + app.use('/auth', Auth(db.getAccount, serverSecret)); + app.use('/profile', Profile(db, web3Provider, serverSecret)); app.use('/delivery', Delivery(web3Provider, db, keys, serverSecret)); app.use( '/notifications', diff --git a/packages/delivery-service/src/persistence/getDatabase.ts b/packages/delivery-service/src/persistence/getDatabase.ts index 573277af7..7d2bc0c7f 100644 --- a/packages/delivery-service/src/persistence/getDatabase.ts +++ b/packages/delivery-service/src/persistence/getDatabase.ts @@ -47,11 +47,11 @@ export async function getRedisClient() { ); client.on('error', (err) => { - global.logger.error('Redis error: ' + (err as Error).message); + console.error('Redis error: ' + (err as Error).message); }); - client.on('reconnecting', () => global.logger.info('Redis reconnection')); - client.on('ready', () => global.logger.info('Redis ready')); + client.on('reconnecting', () => console.info('Redis reconnection')); + client.on('ready', () => console.info('Redis ready')); await client.connect(); @@ -99,12 +99,7 @@ export async function getDatabase( export interface IDatabase extends IAccountDatabase { setAccount: (ensName: string, session: Session) => Promise; - getAccount: (ensName: string) => Promise< - | (Session & { - spamFilterRules: spamFilter.SpamFilterRules; - }) - | null - >; + getAccount: (ensName: string) => Promise; getIncomingMessages: ( ensName: string, limit: number, diff --git a/packages/delivery-service/src/profile.test.ts b/packages/delivery-service/src/profile.test.ts deleted file mode 100644 index 164a4f0d7..000000000 --- a/packages/delivery-service/src/profile.test.ts +++ /dev/null @@ -1,144 +0,0 @@ -import bodyParser from 'body-parser'; - -import { - UserProfile, - getProfileCreationMessage, -} from '@dm3-org/dm3-lib-profile'; -import { stringify } from '@dm3-org/dm3-lib-shared'; -import { ethers } from 'ethers'; -import express from 'express'; -import request from 'supertest'; -import profile from './profile'; -import winston from 'winston'; - -async function getEnsTextRecord( - provider: ethers.providers.JsonRpcProvider, - ensName: string, - recordKey: string, -) { - try { - const resolver = await provider.getResolver(ensName); - if (resolver === null) { - return; - } - - return await resolver.getText(recordKey); - } catch (e) { - return undefined; - } -} -global.logger = winston.createLogger({ - transports: [new winston.transports.Console()], -}); - -describe('Profile', () => { - describe('getProfile', () => { - it('Returns 200 if schema is valid', async () => { - const db = { - getAccount: async (ensName: string) => ({ - signedUserProfile: {}, - }), - setAccount: async (_: string, __: any) => { - return (_: any, __: any, ___: any) => {}; - }, - getIdEnsName: async (ensName: string) => ensName, - }; - - const app = express(); - app.use(bodyParser.json()); - app.use(profile(db as any, {} as any, {} as any, 'my-secret')); - - const { status } = await request(app) - .get('/0x99C19AB10b9EC8aC6fcda9586E81f6B73a298870') - .send(); - - expect(status).toBe(200); - }); - }); - - describe('submitUserProfile', () => { - it('Returns 200 if schema is valid', async () => { - const web3Provider = { resolveName: async () => wallet.address }; - const db = { - getAccount: async (ensName: string) => Promise.resolve(null), - setAccount: async (_: string, __: any) => { - return (_: any, __: any, ___: any) => {}; - }, - getIdEnsName: async (ensName: string) => ensName, - }; - const app = express(); - app.use(bodyParser.json()); - app.use( - profile(db as any, web3Provider as any, {} as any, 'my-secret'), - ); - - const mnemonic = - 'announce room limb pattern dry unit scale effort smooth jazz weasel alcohol'; - - const wallet = ethers.Wallet.fromMnemonic(mnemonic); - - const userProfile: UserProfile = { - publicSigningKey: '2', - publicEncryptionKey: '1', - deliveryServices: [], - }; - - const createUserProfileMessage = getProfileCreationMessage( - stringify(userProfile), - wallet.address, - ); - const signature = await wallet.signMessage( - createUserProfileMessage, - ); - - const signedUserProfile = { - profile: userProfile, - signature, - }; - - const { status } = await request(app) - .post(`/${wallet.address}`) - .send(signedUserProfile); - - expect(status).toBe(200); - }); - it('Returns 400 if schema is invalid', async () => { - const db = { - getAccount: async (accountAddress: string) => - Promise.resolve(null), - setAccount: async (_: string, __: any) => { - return (_: any, __: any, ___: any) => {}; - }, - getIdEnsName: async (ensName: string) => ensName, - }; - - const app = express(); - app.use(bodyParser.json()); - app.use(profile(db as any, {} as any, {} as any, 'my-secret')); - - const userProfile: UserProfile = { - publicSigningKey: '2', - publicEncryptionKey: '1', - deliveryServices: [], - }; - - const mnemonic = - 'announce room limb pattern dry unit scale effort smooth jazz weasel alcohol'; - - const wallet = ethers.Wallet.fromMnemonic(mnemonic); - - const signature = await wallet.signMessage(stringify(userProfile)); - - const signedUserProfile = { - profile: userProfile, - signature: null, - }; - - const { status } = await request(app) - .post(`/1234`) - .send(signedUserProfile); - - expect(status).toBe(400); - }); - }); -}); diff --git a/packages/delivery-service/src/profile.ts b/packages/delivery-service/src/profile.ts deleted file mode 100644 index 3ee70d6c1..000000000 --- a/packages/delivery-service/src/profile.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { getUserProfile, submitUserProfile } from '@dm3-org/dm3-lib-delivery'; -import { normalizeEnsName, schema } from '@dm3-org/dm3-lib-profile'; -import { validateSchema } from '@dm3-org/dm3-lib-shared'; -import express from 'express'; -import { ethers } from 'ethers'; -import { Server } from 'socket.io'; -import { IDatabase } from './persistence/getDatabase'; - -export default ( - db: IDatabase, - web3Provider: ethers.providers.JsonRpcProvider, - io: Server, - serverSecret: string, -) => { - const router = express.Router(); - - router.get('/:ensName', async (req: express.Request, res, next) => { - try { - const ensName = normalizeEnsName(req.params.ensName); - - const profile = await getUserProfile(db.getAccount, ensName); - if (profile) { - res.json(profile); - } else { - res.sendStatus(404); - } - } catch (e) { - next(e); - } - }); - - router.post('/:ensName', async (req: express.Request, res, next) => { - try { - const schemaIsValid = validateSchema( - schema.SignedUserProfile, - req.body, - ); - - if (!schemaIsValid) { - global.logger.error({ message: 'invalid schema' }); - return res.status(400).send({ error: 'invalid schema' }); - } - const ensName = normalizeEnsName(req.params.ensName); - global.logger.debug({ - method: 'POST', - url: req.url, - ensName, - disableSessionCheck: - process.env.DISABLE_SESSION_CHECK === 'true', - }); - - const data = await submitUserProfile( - web3Provider, - db.getAccount, - db.setAccount, - ensName, - req.body, - serverSecret, - ); - global.logger.debug({ - message: 'POST profile', - ensName, - data, - }); - - res.json(data); - } catch (e) { - global.logger.warn({ - message: 'POST profile', - error: JSON.stringify(e), - }); - // eslint-disable-next-line no-console - console.log('POST PROFILE ERROR', e); - res.status(400).send({ - message: `Couldn't store profile`, - error: JSON.stringify(e), - }); - } - }); - - return router; -}; diff --git a/packages/lib/server-side/src/index.ts b/packages/lib/server-side/src/index.ts index 1742335be..bc605e2c5 100644 --- a/packages/lib/server-side/src/index.ts +++ b/packages/lib/server-side/src/index.ts @@ -2,3 +2,4 @@ export { Auth } from './auth'; export * from './utils'; export { getCachedWebProvider } from './web3Provider/getCachedWebProvider'; export type { IAccountDatabase } from './iSessionDatabase'; +export { Profile } from './profile'; diff --git a/packages/lib/server-side/src/profile.test.ts b/packages/lib/server-side/src/profile.test.ts new file mode 100644 index 000000000..d9623355a --- /dev/null +++ b/packages/lib/server-side/src/profile.test.ts @@ -0,0 +1,302 @@ +import { Session, generateAuthJWT } from '@dm3-org/dm3-lib-delivery'; +import { + UserProfile, + getProfileCreationMessage, +} from '@dm3-org/dm3-lib-profile'; +import { stringify } from '@dm3-org/dm3-lib-shared'; +import bodyParser from 'body-parser'; +import { ethers } from 'ethers'; +import express from 'express'; +import request from 'supertest'; +import { Profile as profile } from './profile'; +import { IAccountDatabase } from '../dist'; + +const serverSecret = 'veryImportantSecretToGenerateAndValidateJSONWebTokens'; + +const token = generateAuthJWT('alice.eth', serverSecret); + +const setUpApp = async ( + app: express.Express, + db: IAccountDatabase, + web3Provider: ethers.providers.JsonRpcProvider, + serverSecret: string = 'my-secret', +) => { + app.use(bodyParser.json()); + app.use(profile(db, web3Provider, serverSecret)); +}; + +const createDbMock = async () => { + const sessionMocked = { + challenge: '123', + token: 'deprecated token that is not used anymore', + signedUserProfile: {}, + } as Session; + + const dbMock = { + getAccount: async (ensName: string) => + Promise.resolve(sessionMocked), // returns some valid session + setAccount: async (_: string, __: Session) => {}, + getIdEnsName: async (ensName: string) => ensName, + }; + + return dbMock as any; +}; + +describe('Profile', () => { + describe('getProfile', () => { + it('Returns 200 if user profile exists', async () => { + const app = express(); + + const db = await createDbMock(); + + const _web3Provider = { + resolveName: async () => + '0x95222290DD7278Aa3Ddd389Cc1E1d165CC4BAfe5', + } as unknown as ethers.providers.JsonRpcProvider; + // I don't know why this function is needed in this test. + // Remove it after storage migration. + db.getUserStorage = () => {}; + setUpApp(app, db, _web3Provider); + + const response = await request(app) + .get('/alice.eth') + .set({ + authorization: 'Bearer ' + token, + }) + .send(); + const status = response.status; + + expect(status).toBe(200); + }); + it('Returns 404 if user profile not exists', async () => { + const app = express(); + + const db = { + ...(await createDbMock()), + getAccount: async (ensName: string) => Promise.resolve(null), + }; + + const _web3Provider = { + resolveName: async () => + '0x95222290DD7278Aa3Ddd389Cc1E1d165CC4BAfe5', + } as unknown as ethers.providers.JsonRpcProvider; + // I don't know why this function is needed in this test. + // Remove it after storage migration. + setUpApp(app, db, _web3Provider); + + const response = await request(app) + .get('/rando.eth') + .set({ + authorization: 'Bearer ' + token, + }) + .send(); + const status = response.status; + + expect(status).toBe(404); + }); + it('Returns 400 if the name could not be resolved', async () => { + const app = express(); + + const db = await createDbMock(); + + const _web3Provider = { + resolveName: async () => null, + } as unknown as ethers.providers.JsonRpcProvider; + // I don't know why this function is needed in this test. + // Remove it after storage migration. + db.getUserStorage = () => {}; + setUpApp(app, db, _web3Provider); + + const response = await request(app) + .get('/alice.eth') + .set({ + authorization: 'Bearer ' + token, + }) + .send(); + const status = response.status; + + expect(status).toBe(400); + expect(response.body.message).toBe( + `could not get profile for alice.eth. Unable to resolve address`, + ); + }); + }); + + describe('submitUserProfile', () => { + it('Returns 200 if user profile creation was successful', async () => { + const mnemonic = + 'announce room limb pattern dry unit scale effort smooth jazz weasel alcohol'; + + const wallet = ethers.Wallet.fromMnemonic(mnemonic); + + // this provider must return the address of the wallet when resolveName is called + const _web3ProviderMock = { + resolveName: async () => wallet.address, + }; + // the db must return null when getAccount is called + const _dbMock = { + getAccount: async (ensName: string) => Promise.resolve(null), + setAccount: async (_: string, __: any) => { + return (_: any, __: any, ___: any) => {}; + }, + getIdEnsName: async (ensName: string) => ensName, + }; + + const app = express(); + setUpApp(app, _dbMock as any, _web3ProviderMock as any); + + const userProfile: UserProfile = { + publicSigningKey: '2', + publicEncryptionKey: '1', + deliveryServices: [], + }; + + const createUserProfileMessage = getProfileCreationMessage( + stringify(userProfile), + wallet.address, + ); + const signature = await wallet.signMessage( + createUserProfileMessage, + ); + + const signedUserProfile = { + profile: userProfile, + signature, + }; + + const response = await request(app) + .post(`/${wallet.address}`) + .send(signedUserProfile); + + const status = response.status; + + expect(status).toBe(200); + }); + it('Returns 400 if profile signature is wrong', async () => { + const mnemonic = + 'announce room limb pattern dry unit scale effort smooth jazz weasel alcohol'; + + const wallet = ethers.Wallet.fromMnemonic(mnemonic); + + // this provider must return the address of the wallet when resolveName is called + const _web3ProviderMock = { + resolveName: async () => wallet.address, + }; + // the db must return null when getAccount is called + const _dbMock = { + getAccount: async (ensName: string) => Promise.resolve(null), + setAccount: async (_: string, __: any) => { + return (_: any, __: any, ___: any) => {}; + }, + getIdEnsName: async (ensName: string) => ensName, + }; + + const app = express(); + setUpApp(app, _dbMock as any, _web3ProviderMock as any); + + const userProfile: UserProfile = { + publicSigningKey: '2', + publicEncryptionKey: '1', + deliveryServices: [], + }; + //sign something else + const signature = await wallet.signMessage('0x1234'); + + const signedUserProfile = { + profile: userProfile, + signature, + }; + + const response = await request(app) + .post(`/${wallet.address}`) + .send(signedUserProfile); + + const status = response.status; + + expect(status).toBe(400); + expect(response.body.message).toBe("Couldn't store profile"); + }); + it('Returns 400 if addr cannot be resolved', async () => { + const mnemonic = + 'announce room limb pattern dry unit scale effort smooth jazz weasel alcohol'; + + const wallet = ethers.Wallet.fromMnemonic(mnemonic); + + // this provider must return the address of the wallet when resolveName is called + const _web3ProviderMock = { + resolveName: async () => null, + }; + // the db must return null when getAccount is called + const _dbMock = { + getAccount: async (ensName: string) => Promise.resolve(null), + setAccount: async (_: string, __: any) => { + return (_: any, __: any, ___: any) => {}; + }, + getIdEnsName: async (ensName: string) => ensName, + }; + + const app = express(); + setUpApp(app, _dbMock as any, _web3ProviderMock as any); + + const userProfile: UserProfile = { + publicSigningKey: '2', + publicEncryptionKey: '1', + deliveryServices: [], + }; + + const createUserProfileMessage = getProfileCreationMessage( + stringify(userProfile), + wallet.address, + ); + const signature = await wallet.signMessage( + createUserProfileMessage, + ); + + const signedUserProfile = { + profile: userProfile, + signature, + }; + + const response = await request(app) + .post(`/rando.eth`) + .send(signedUserProfile); + + const status = response.status; + + expect(status).toBe(400); + expect(response.body.message).toBe( + `could not submit profile for rando.eth. Unable to resolve address`, + ); + }); + + it('Returns 400 if schema is invalid', async () => { + const web3Provider = {} as ethers.providers.JsonRpcProvider; + const app = express(); + setUpApp(app, await createDbMock(), web3Provider); + + const userProfile: UserProfile = { + publicSigningKey: '2', + publicEncryptionKey: '1', + deliveryServices: [], + }; + + const mnemonic = + 'announce room limb pattern dry unit scale effort smooth jazz weasel alcohol'; + + const wallet = ethers.Wallet.fromMnemonic(mnemonic); + + const signature = await wallet.signMessage(stringify(userProfile)); + + const signedUserProfile = { + profile: userProfile, + signature: null, + }; + + const { status } = await request(app) + .post(`/1234`) + .send(signedUserProfile); + + expect(status).toBe(400); + }); + }); +}); diff --git a/packages/backend/src/profile.ts b/packages/lib/server-side/src/profile.ts similarity index 64% rename from packages/backend/src/profile.ts rename to packages/lib/server-side/src/profile.ts index b15139ea4..9e073bf19 100644 --- a/packages/backend/src/profile.ts +++ b/packages/lib/server-side/src/profile.ts @@ -3,10 +3,10 @@ import { normalizeEnsName, schema } from '@dm3-org/dm3-lib-profile'; import { validateSchema } from '@dm3-org/dm3-lib-shared'; import { ethers } from 'ethers'; import express from 'express'; -import { IDatabase } from './persistence/getDatabase'; +import { IAccountDatabase } from './iSessionDatabase'; -export default ( - db: IDatabase, +export const Profile = ( + db: IAccountDatabase, web3Provider: ethers.providers.JsonRpcProvider, serverSecret: string, ) => { @@ -16,7 +16,18 @@ export default ( try { const ensName = normalizeEnsName(req.params.ensName); - const profile = await getUserProfile(db.getAccount, ensName); + //use the webProvider to resolve the address here + const address = await web3Provider.resolveName(ensName); + if (!address) { + const message = `could not get profile for ${ensName}. Unable to resolve address`; + console.error(message); + return res.status(400).send({ message }); + } + + const profile = await getUserProfile( + db.getAccount, + ethers.utils.getAddress(address), + ); if (profile) { res.json(profile); } else { @@ -43,20 +54,25 @@ export default ( console.error({ message: 'invalid schema' }); return res.status(400).send({ error: 'invalid schema' }); } + const ensName = normalizeEnsName(req.params.ensName); - console.debug({ - method: 'POST', - url: req.url, - ensName, - disableSessionCheck: - process.env.DISABLE_SESSION_CHECK === 'true', - }); + //use the webProvider to resolve the address here + const address = await web3Provider.resolveName(ensName); + if (!address) { + const message = `could not submit profile for ${ensName}. Unable to resolve address`; + console.error(message); + return res.status(400).send({ message }); + } + + console.debug( + `create profile for ${ensName} with address ${address}`, + ); const data = await submitUserProfile( - web3Provider, db.getAccount, db.setAccount, - ensName, + //use normalized address + ethers.utils.getAddress(address), req.body, serverSecret, ); From 8256fc71748787b3439fa288885492de5359494c Mon Sep 17 00:00:00 2001 From: AlexNi245 Date: Mon, 22 Jul 2024 16:39:57 +0200 Subject: [PATCH 025/148] dont sort conversations in conversationId --- packages/lib/delivery/src/Messages.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/lib/delivery/src/Messages.ts b/packages/lib/delivery/src/Messages.ts index 6bc291b13..a7ed84255 100644 --- a/packages/lib/delivery/src/Messages.ts +++ b/packages/lib/delivery/src/Messages.ts @@ -26,9 +26,7 @@ export interface Acknowledgment { } export function getConversationId(ensNameA: string, ensNameB: string): string { - return [normalizeEnsName(ensNameA), normalizeEnsName(ensNameB)] - .sort() - .join(); + return [normalizeEnsName(ensNameA), normalizeEnsName(ensNameB)].join(); } // fetch new messages export async function getMessages( From 2d5d96bb41a4860ce7ae1669a5e9247c6b757020 Mon Sep 17 00:00:00 2001 From: AlexNi245 Date: Mon, 22 Jul 2024 16:40:24 +0200 Subject: [PATCH 026/148] MEssageProcessor resolves address --- .../src/message/MessageProcessor.test.ts | 80 +++++++++++++------ .../src/message/MessageProcessor.ts | 12 ++- 2 files changed, 64 insertions(+), 28 deletions(-) diff --git a/packages/delivery-service/src/message/MessageProcessor.test.ts b/packages/delivery-service/src/message/MessageProcessor.test.ts index c9679b977..b53548d65 100644 --- a/packages/delivery-service/src/message/MessageProcessor.test.ts +++ b/packages/delivery-service/src/message/MessageProcessor.test.ts @@ -24,6 +24,7 @@ import { mockUserProfile, } from '@dm3-org/dm3-lib-test-helper'; import { IDatabase } from '../persistence/getDatabase'; +import { getAddress } from 'ethers/lib/utils'; jest.mock('nodemailer'); @@ -72,9 +73,8 @@ describe('MessageProcessor', () => { publicEncryptionKey: '', deliveryServices: [''], }; - const isSender = normalizeEnsName(ensName) === sender.account.ensName; - const isReceiver = - normalizeEnsName(ensName) === receiver.account.ensName; + const isSender = getAddress(ensName) === sender.address; + const isReceiver = getAddress(ensName) === receiver.address; const session = ( account: string, @@ -97,14 +97,14 @@ describe('MessageProcessor', () => { if (isSender) { return { - ...session(sender.account.ensName, '123', emptyProfile), + ...session(sender.address, '123', emptyProfile), spamFilterRules: {}, }; } if (isReceiver) { return { - ...session(receiver.account.ensName, 'abc', { + ...session(getAddress(receiver.address), 'abc', { ...emptyProfile, publicEncryptionKey: receiver.profileKeys.encryptionKeyPair.publicKey, @@ -125,8 +125,11 @@ describe('MessageProcessor', () => { } as any as IDatabase; const web3Provider = { - resolveName: async () => - '0x25A643B6e52864d0eD816F1E43c0CF49C83B8292', + resolveName: async (name: string) => { + if (name === 'alice.eth' || name === 'alice.gno') { + return receiver.address; + } + }, } as any; const mockWsManager: IWebSocketManager = { @@ -170,8 +173,11 @@ describe('MessageProcessor', () => { } as any as IDatabase; const web3Provider = { - resolveName: async () => - '0x25A643B6e52864d0eD816F1E43c0CF49C83B8292', + resolveName: async (name: string) => { + if (name === 'alice.eth' || name === 'alice.gno') { + return receiver.address; + } + }, } as any; const mockWsManager: IWebSocketManager = { @@ -214,8 +220,11 @@ describe('MessageProcessor', () => { } as any as IDatabase; const web3Provider = { - resolveName: async () => - '0x25A643B6e52864d0eD816F1E43c0CF49C83B8292', + resolveName: async (name: string) => { + if (name === 'rando.eth') { + return rando.address; + } + }, } as any; const mockWsManager: IWebSocketManager = { @@ -267,8 +276,11 @@ describe('MessageProcessor', () => { const web3Provider = { getTransactionCount: async (_: string) => Promise.resolve(0), - resolveName: async () => - '0x25A643B6e52864d0eD816F1E43c0CF49C83B8292', + resolveName: async (name: string) => { + if (name === 'alice.eth' || name === 'alice.gno') { + return receiver.address; + } + }, } as any; const deliveryServiceProperties: DeliveryServiceProperties = { @@ -321,8 +333,11 @@ describe('MessageProcessor', () => { const web3Provider = { getBalance: async (_: string) => Promise.resolve(BigNumber.from(5)), - resolveName: async () => - '0x25A643B6e52864d0eD816F1E43c0CF49C83B8292', + resolveName: async (name: string) => { + if (name === 'alice.eth' || name === 'alice.gno') { + return receiver.address; + } + }, } as any; const deliveryServiceProperties: DeliveryServiceProperties = { @@ -381,8 +396,11 @@ describe('MessageProcessor', () => { const web3Provider = { _isProvider: true, call: () => Promise.resolve(BigNumber.from(0).toHexString()), - resolveName: async () => - '0x25A643B6e52864d0eD816F1E43c0CF49C83B8292', + resolveName: async (name: string) => { + if (name === 'alice.eth' || name === 'alice.gno') { + return receiver.address; + } + }, } as unknown as ethers.providers.JsonRpcProvider; const mockWsManager: IWebSocketManager = { @@ -466,8 +484,11 @@ describe('MessageProcessor', () => { const web3Provider = { getBalance: async (_: string) => Promise.resolve(BigNumber.from(5)), - resolveName: async () => - '0x25A643B6e52864d0eD816F1E43c0CF49C83B8292', + resolveName: async (name: string) => { + if (name === 'alice.eth' || name === 'alice.gno') { + return receiver.address; + } + }, } as any; const mockWsManager: IWebSocketManager = { @@ -524,8 +545,11 @@ describe('MessageProcessor', () => { const web3Provider = { getBalance: async (_: string) => Promise.resolve(BigNumber.from(5)), - resolveName: async () => - '0x25A643B6e52864d0eD816F1E43c0CF49C83B8292', + resolveName: async (name: string) => { + if (name === 'alice.eth' || name === 'alice.gno') { + return receiver.address; + } + }, } as any; const deliveryServiceProperties: DeliveryServiceProperties = { @@ -605,8 +629,11 @@ describe('MessageProcessor', () => { const web3Provider = { getBalance: async (_: string) => Promise.resolve(BigNumber.from(5)), - resolveName: async () => - '0x25A643B6e52864d0eD816F1E43c0CF49C83B8292', + resolveName: async (name: string) => { + if (name === 'alice.eth' || name === 'alice.gno') { + return receiver.address; + } + }, } as any; const deliveryServiceProperties: DeliveryServiceProperties = { @@ -694,8 +721,11 @@ describe('MessageProcessor', () => { const web3Provider = { getBalance: async (_: string) => Promise.resolve(BigNumber.from(5)), - resolveName: async () => - '0x25A643B6e52864d0eD816F1E43c0CF49C83B8292', + resolveName: async (name: string) => { + if (name === 'alice.eth' || name === 'alice.gno') { + return receiver.address; + } + }, } as any; const deliveryServiceProperties: DeliveryServiceProperties = { diff --git a/packages/delivery-service/src/message/MessageProcessor.ts b/packages/delivery-service/src/message/MessageProcessor.ts index a185788dc..1165615da 100644 --- a/packages/delivery-service/src/message/MessageProcessor.ts +++ b/packages/delivery-service/src/message/MessageProcessor.ts @@ -80,6 +80,14 @@ export class MessageProcessor { //i.E if alice.eth resolves to 0x123 //and alice.gno resolves to 0x123 aswell, the ds has to accept both const address = await this.provider.resolveName(deliveryInformation.to); + + if (!address) { + console.debug( + 'unable to resolve address for ', + deliveryInformation.to, + ); + throw Error('unable to resolve receiver address'); + } console.log(address); const conversationId = getConversationId( @@ -90,9 +98,7 @@ export class MessageProcessor { console.debug(conversationId, deliveryInformation); //Retrieves the session of the receiver - const receiverSession = await this.db.getAccount( - deliveryInformation.to, - ); + const receiverSession = await this.db.getAccount(address); if (!receiverSession) { console.debug('unknown user ', deliveryInformation.to); throw Error('unknown session'); From 30b513427a0a606c666d2127ff3553860c158d8f Mon Sep 17 00:00:00 2001 From: AlexNi245 Date: Mon, 22 Jul 2024 16:40:45 +0200 Subject: [PATCH 027/148] remove custom spamFilter rules interface --- packages/lib/delivery/src/spam-filter/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/lib/delivery/src/spam-filter/index.ts b/packages/lib/delivery/src/spam-filter/index.ts index 417d23554..17e14fd9b 100644 --- a/packages/lib/delivery/src/spam-filter/index.ts +++ b/packages/lib/delivery/src/spam-filter/index.ts @@ -39,7 +39,7 @@ function compileSpamFilter(filter: SpamFilter[]) { */ function getUsersSpamFilters( provider: ethers.providers.BaseProvider, - { spamFilterRules }: Session & { spamFilterRules: SpamFilterRules }, + { spamFilterRules }: Session, ) { //User has not defined any rules if (!spamFilterRules) { @@ -55,7 +55,7 @@ function getUsersSpamFilters( */ export async function isSpam( provider: ethers.providers.BaseProvider, - session: Session & { spamFilterRules: SpamFilterRules }, + session: Session, deliveryInformation: DeliveryInformation, ) { const usersSpamFilters = getUsersSpamFilters(provider, session); From b633ddfb031b1d88d76274606892f9e1538b27c6 Mon Sep 17 00:00:00 2001 From: AlexNi245 Date: Mon, 22 Jul 2024 16:41:08 +0200 Subject: [PATCH 028/148] get rid of ts-ignore in auth.ts --- packages/lib/server-side/src/auth.ts | 5 +++-- packages/lib/test-helper/mocks/mockUserProfile.ts | 3 ++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/lib/server-side/src/auth.ts b/packages/lib/server-side/src/auth.ts index 90865f003..94806ac8c 100644 --- a/packages/lib/server-side/src/auth.ts +++ b/packages/lib/server-side/src/auth.ts @@ -35,8 +35,9 @@ const createNewSessionTokenBodySchema = { additionalProperties: false, }; -//@ts-ignore -export const Auth = (getAccount, serverSecret: string) => { +type GetAccount = (ensName: string) => Promise; + +export const Auth = (getAccount: GetAccount, serverSecret: string) => { const router = express.Router(); //TODO remove diff --git a/packages/lib/test-helper/mocks/mockUserProfile.ts b/packages/lib/test-helper/mocks/mockUserProfile.ts index 5d0773f51..9156e565e 100644 --- a/packages/lib/test-helper/mocks/mockUserProfile.ts +++ b/packages/lib/test-helper/mocks/mockUserProfile.ts @@ -13,6 +13,7 @@ import { } from '@dm3-org/dm3-lib-profile'; import { stringify } from '@dm3-org/dm3-lib-shared'; import { ethers } from 'ethers'; +import { getAddress } from 'ethers/lib/utils'; export type MockedUserProfile = { address: string; @@ -52,7 +53,7 @@ export const mockUserProfile = async ( return { wallet, - address: wallet.address, + address: getAddress(wallet.address), privateKey: wallet.privateKey, account: { ensName, From 25627c0830d196b701bda5aa80d0d27277cd5fbf Mon Sep 17 00:00:00 2001 From: Bhupesh-MS Date: Thu, 25 Jul 2024 19:26:09 +0530 Subject: [PATCH 029/148] msg indicators added --- packages/lib/messaging/src/Message.ts | 54 +++++++++ packages/lib/messaging/src/index.ts | 2 + .../src/profileExtension/ProfileExtension.ts | 2 + .../src/components/Chat/Chat.tsx | 3 + .../src/components/Contacts/Contacts.tsx | 15 ++- .../src/components/Message/Message.css | 5 + .../src/components/Message/MessageDetail.tsx | 58 +++++++--- .../renderer/messageTypes/renderReadOpened.ts | 25 ++++ .../messageTypes/renderReadReceived.ts | 27 +++++ .../hooks/messages/renderer/renderMessage.ts | 6 +- .../handleMessagesFromDeliveryService.ts | 70 ++++++++++++ .../sources/handleMessagesFromWebSocket.ts | 41 ++++++- .../src/hooks/messages/useMessage.tsx | 107 +++++++++++++----- .../messenger-widget/src/interfaces/props.ts | 2 + 14 files changed, 368 insertions(+), 49 deletions(-) create mode 100644 packages/messenger-widget/src/hooks/messages/renderer/messageTypes/renderReadOpened.ts create mode 100644 packages/messenger-widget/src/hooks/messages/renderer/messageTypes/renderReadReceived.ts diff --git a/packages/lib/messaging/src/Message.ts b/packages/lib/messaging/src/Message.ts index fc3146a4a..9cfe3081e 100644 --- a/packages/lib/messaging/src/Message.ts +++ b/packages/lib/messaging/src/Message.ts @@ -44,6 +44,8 @@ export type MessageType = | 'EDIT' | 'REPLY' | 'REACTION' + | 'READ_OPENED' + | 'READ_RECEIVED' | 'READ_RECEIPT' | 'RESEND_REQUEST'; @@ -253,6 +255,58 @@ export async function createReactionMessage( ); } +/** + * creats a read opened message and signs it + * @param to sender ENS name + * @param from receiver ENS name + * @param message the message content + * @param privateKey sender signing key + * @param referenceMessageHash reference to previous message + */ +export async function createReadOpenMessage( + to: string, + from: string, + message: string, + privateKey: string, + referenceMessageHash: string, +): Promise { + return internalCreateMessage( + to, + from, + message, + privateKey, + 'READ_OPENED', + [], + referenceMessageHash, + ); +} + +/** + * creates a read received message and signs it + * @param to sender ENS name + * @param from receiver ENS name + * @param message the message content + * @param privateKey sender signing key + * @param referenceMessageHash reference to previous message + */ +export async function createReadReceiveMessage( + to: string, + from: string, + message: string, + privateKey: string, + referenceMessageHash: string, +): Promise { + return internalCreateMessage( + to, + from, + message, + privateKey, + 'READ_RECEIVED', + [], + referenceMessageHash, + ); +} + export async function checkMessageSignature( message: Message, publicSigningKey: string, diff --git a/packages/lib/messaging/src/index.ts b/packages/lib/messaging/src/index.ts index 2d8bbeeaa..c3de845b8 100644 --- a/packages/lib/messaging/src/index.ts +++ b/packages/lib/messaging/src/index.ts @@ -11,6 +11,8 @@ export { createEditMessage, createReactionMessage, createReplyMessage, + createReadOpenMessage, + createReadReceiveMessage, createJsonRpcCallSubmitMessage, handleMessageOnDeliveryService, decryptEnvelop, diff --git a/packages/lib/profile/src/profileExtension/ProfileExtension.ts b/packages/lib/profile/src/profileExtension/ProfileExtension.ts index ce5857a39..e0136bbc3 100644 --- a/packages/lib/profile/src/profileExtension/ProfileExtension.ts +++ b/packages/lib/profile/src/profileExtension/ProfileExtension.ts @@ -5,6 +5,8 @@ export type MessageType = | 'EDIT' | 'REPLY' | 'REACTION' + | 'READ_OPEN' + | 'READ_RECEIVE' | 'READ_RECEIPT' | 'RESEND_REQUEST'; diff --git a/packages/messenger-widget/src/components/Chat/Chat.tsx b/packages/messenger-widget/src/components/Chat/Chat.tsx index 4f7a9d643..a4ffd09bc 100644 --- a/packages/messenger-widget/src/components/Chat/Chat.tsx +++ b/packages/messenger-widget/src/components/Chat/Chat.tsx @@ -225,6 +225,9 @@ export function Chat() { replyToMessageEnvelop={ storageEnvelopContainer.replyToMessageEnvelop } + indicator={ + storageEnvelopContainer.indicator + } /> ), diff --git a/packages/messenger-widget/src/components/Contacts/Contacts.tsx b/packages/messenger-widget/src/components/Contacts/Contacts.tsx index 004843c06..d8542137f 100644 --- a/packages/messenger-widget/src/components/Contacts/Contacts.tsx +++ b/packages/messenger-widget/src/components/Contacts/Contacts.tsx @@ -103,14 +103,25 @@ export function Contacts() { const _contact = normalizeEnsName(contactEnsName); const messages = getMessages(_contact); + // don't include the preview of acknowledgment msgs if (messages?.length > 0) { - return messages[0].envelop.message.message ?? ''; + return messages[0].envelop.message.metadata.type !== + 'READ_OPENED' && + messages[0].envelop.message.metadata.type !== 'READ_RECEIVED' && + messages[0].envelop.message.message + ? messages[0].envelop.message.message + : ''; } const contact = contacts.find( (c) => c.contactDetails.account.ensName === _contact, ); const previewMessage = contact?.message; - return previewMessage ?? ''; + return previewMessage + ? previewMessage !== 'READ_OPENED' && + previewMessage !== 'READ_RECEIVED' + ? previewMessage + : '' + : ''; }; const isContactSelected = (id: string) => { diff --git a/packages/messenger-widget/src/components/Message/Message.css b/packages/messenger-widget/src/components/Message/Message.css index 217957f99..40ac6f31b 100644 --- a/packages/messenger-widget/src/components/Message/Message.css +++ b/packages/messenger-widget/src/components/Message/Message.css @@ -36,6 +36,11 @@ margin-left: -5px; } +.indicator-tick-icon { + height: 15px; + width: 10px; +} + .msg-action-container { height: fit-content; width: fit-content; diff --git a/packages/messenger-widget/src/components/Message/MessageDetail.tsx b/packages/messenger-widget/src/components/Message/MessageDetail.tsx index 0dd608d20..9b6d75252 100644 --- a/packages/messenger-widget/src/components/Message/MessageDetail.tsx +++ b/packages/messenger-widget/src/components/Message/MessageDetail.tsx @@ -1,10 +1,45 @@ import './Message.css'; import { getMessageChangeText } from './bl'; -import tickIcon from '../../assets/images/tick.svg'; +import blueTickIcon from '../../assets/images/tick.svg'; +import whiteTickIcon from '../../assets/images/white-tick.svg'; import { MessageProps } from '../../interfaces/props'; -import { MessageState } from '@dm3-org/dm3-lib-messaging'; +import { MessageIndicator } from '../../hooks/messages/useMessage'; export function MessageDetail(props: MessageProps) { + const getMessageIndicatorView = ( + indicator: MessageIndicator | undefined, + ) => { + if (!indicator || indicator === MessageIndicator.SENT) { + return ( + read + ); + } + + const indicatorIcon = + indicator === MessageIndicator.RECEIVED + ? whiteTickIcon + : blueTickIcon; + + return ( + <> + read + read + + ); + }; + return (
{getMessageChangeText(props)} @@ -13,20 +48,13 @@ export function MessageDetail(props: MessageProps) { {/* readed message tick indicator */} {props.ownMessage ? ( - props.messageState === MessageState.Read ? ( - <> - read - read - - ) : ( - read - ) + getMessageIndicatorView(props.indicator) ) : ( - read + read )}
diff --git a/packages/messenger-widget/src/hooks/messages/renderer/messageTypes/renderReadOpened.ts b/packages/messenger-widget/src/hooks/messages/renderer/messageTypes/renderReadOpened.ts new file mode 100644 index 000000000..7409a2cc0 --- /dev/null +++ b/packages/messenger-widget/src/hooks/messages/renderer/messageTypes/renderReadOpened.ts @@ -0,0 +1,25 @@ +import { MessageIndicator, MessageModel } from '../../useMessage'; + +export const renderReadOpened = (messages: MessageModel[]) => { + //We filter out all messages that are of type READ_OPENED + const readOpenedMsgs = messages.filter( + (message) => message.envelop.message.metadata.type === 'READ_OPENED', + ); + + const msgsWithoutReadType = messages.filter( + (data) => data.envelop.message.metadata.type !== 'READ_OPENED', + ); + + //update indicator to the messages + return msgsWithoutReadType.map((message) => { + const openedMsg = readOpenedMsgs.find( + (m) => + m.envelop.message.metadata.referenceMessageHash === + message.envelop.metadata?.encryptedMessageHash, + ); + return { + ...message, + indicator: openedMsg ? MessageIndicator.READED : message.indicator, + }; + }); +}; diff --git a/packages/messenger-widget/src/hooks/messages/renderer/messageTypes/renderReadReceived.ts b/packages/messenger-widget/src/hooks/messages/renderer/messageTypes/renderReadReceived.ts new file mode 100644 index 000000000..1a0b4c79c --- /dev/null +++ b/packages/messenger-widget/src/hooks/messages/renderer/messageTypes/renderReadReceived.ts @@ -0,0 +1,27 @@ +import { MessageIndicator, MessageModel } from '../../useMessage'; + +export const renderReadReceived = (messages: MessageModel[]) => { + //We filter out all messages that are of type READ_RECEIVED + const readReceivedMsgs = messages.filter( + (message) => message.envelop.message.metadata.type === 'READ_RECEIVED', + ); + + const msgsWithoutReadType = messages.filter( + (data) => data.envelop.message.metadata.type !== 'READ_RECEIVED', + ); + + //update indicator to the messages + return msgsWithoutReadType.map((message) => { + const receivedMsg = readReceivedMsgs.find( + (m) => + m.envelop.message.metadata.referenceMessageHash === + message.envelop.metadata?.encryptedMessageHash, + ); + return { + ...message, + indicator: receivedMsg + ? MessageIndicator.RECEIVED + : MessageIndicator.SENT, + }; + }); +}; diff --git a/packages/messenger-widget/src/hooks/messages/renderer/renderMessage.ts b/packages/messenger-widget/src/hooks/messages/renderer/renderMessage.ts index eb571bb14..47bc1c9bf 100644 --- a/packages/messenger-widget/src/hooks/messages/renderer/renderMessage.ts +++ b/packages/messenger-widget/src/hooks/messages/renderer/renderMessage.ts @@ -3,6 +3,8 @@ import { renderDelete } from './messageTypes/renderDelete'; import { renderDuplicates } from './messageTypes/renderDuplicates'; import { renderEdit } from './messageTypes/renderEdit'; import { renderReactions } from './messageTypes/renderReactions'; +import { renderReadOpened } from './messageTypes/renderReadOpened'; +import { renderReadReceived } from './messageTypes/renderReadReceived'; import { renderReply } from './messageTypes/renderReply'; /** @@ -12,7 +14,9 @@ import { renderReply } from './messageTypes/renderReply'; * Putting them to the right place in the conversation. */ export const renderMessage = (messages: MessageModel[]) => { - const withDeletes = renderDelete(messages); + const withReadReceived = renderReadReceived(messages); + const withReadOpened = renderReadOpened(withReadReceived); + const withDeletes = renderDelete(withReadOpened); const withReactions = renderReactions(withDeletes); const withReply = renderReply(withReactions); diff --git a/packages/messenger-widget/src/hooks/messages/sources/handleMessagesFromDeliveryService.ts b/packages/messenger-widget/src/hooks/messages/sources/handleMessagesFromDeliveryService.ts index 2b8bf7477..131c1d0c2 100644 --- a/packages/messenger-widget/src/hooks/messages/sources/handleMessagesFromDeliveryService.ts +++ b/packages/messenger-widget/src/hooks/messages/sources/handleMessagesFromDeliveryService.ts @@ -1,5 +1,7 @@ import { decryptAsymmetric } from '@dm3-org/dm3-lib-crypto'; import { + createReadOpenMessage, + createReadReceiveMessage, EncryptionEnvelop, Envelop, MessageState, @@ -8,8 +10,10 @@ import { MessageModel, MessageSource } from '../useMessage'; import { Account, ProfileKeys } from '@dm3-org/dm3-lib-profile'; import { AddConversation, StoreMessageBatch } from '../../storage/useStorage'; import { Acknowledgment } from '@dm3-org/dm3-lib-delivery'; +import { ContactPreview } from '../../../interfaces/utils'; export const handleMessagesFromDeliveryService = async ( + selectedContact: ContactPreview | undefined, account: Account, profileKeys: ProfileKeys, addConversation: AddConversation, @@ -21,6 +25,7 @@ export const handleMessagesFromDeliveryService = async ( acknoledgments: Acknowledgment[], ) => void, updateConversationList: (conversation: string, updatedAt: number) => void, + addMessage: Function, ) => { //Fetch the messages from the delivery service const encryptedIncommingMessages = await fetchNewMessages( @@ -84,5 +89,70 @@ export const handleMessagesFromDeliveryService = async ( })); await syncAcknowledgment(account.ensName, acks); + + const ackToSend = messagesSortedASC.filter( + (data) => + data.envelop.message.metadata.type !== 'READ_RECEIVED' && + data.envelop.message.metadata.type !== 'READ_OPENED', + ); + + // if contact is selected send READ_OPENED acknowledgment to sender for all new messages received + if ( + ackToSend.length && + selectedContact && + selectedContact.contactDetails.account.ensName === + ackToSend[0].envelop.message.metadata.from + ) { + // send READ_OPENED acknowledgment to sender's for all messages + const openedMsgs = await Promise.all( + ackToSend.map(async (message: MessageModel) => { + return await createReadOpenMessage( + message.envelop.message.metadata.from, + account!.ensName, + 'READ_OPENED', + profileKeys?.signingKeyPair.privateKey!, + message.envelop.metadata?.encryptedMessageHash as string, + ); + }), + ); + + // add message + await Promise.all( + openedMsgs.map(async (msg, index) => { + await addMessage( + ackToSend[index].envelop.message.metadata.from, + msg, + ); + }), + ); + + return messagesSortedASC; + } + + if (ackToSend.length) { + // send READ_RECEIVED acknowledgment to sender's for all new messages received + const readedMsgs = await Promise.all( + ackToSend.map(async (message: MessageModel) => { + return await createReadReceiveMessage( + message.envelop.message.metadata.from, + account!.ensName, + 'READ_RECEIVED', + profileKeys?.signingKeyPair.privateKey!, + message.envelop.metadata?.encryptedMessageHash as string, + ); + }), + ); + + // add message + await Promise.all( + readedMsgs.map(async (msg, index) => { + await addMessage( + ackToSend[index].envelop.message.metadata.from, + msg, + ); + }), + ); + } + return messagesSortedASC; }; diff --git a/packages/messenger-widget/src/hooks/messages/sources/handleMessagesFromWebSocket.ts b/packages/messenger-widget/src/hooks/messages/sources/handleMessagesFromWebSocket.ts index 94b5779ce..3e043d62d 100644 --- a/packages/messenger-widget/src/hooks/messages/sources/handleMessagesFromWebSocket.ts +++ b/packages/messenger-widget/src/hooks/messages/sources/handleMessagesFromWebSocket.ts @@ -1,15 +1,22 @@ import { decryptAsymmetric } from '@dm3-org/dm3-lib-crypto'; import { + createReadOpenMessage, + createReadReceiveMessage, EncryptionEnvelop, Envelop, MessageState, } from '@dm3-org/dm3-lib-messaging'; -import { ProfileKeys, normalizeEnsName } from '@dm3-org/dm3-lib-profile'; +import { + Account, + ProfileKeys, + normalizeEnsName, +} from '@dm3-org/dm3-lib-profile'; import { ContactPreview } from '../../../interfaces/utils'; import { AddConversation, StoreMessageAsync } from '../../storage/useStorage'; import { MessageModel, MessageSource, MessageStorage } from '../useMessage'; export const handleMessagesFromWebSocket = async ( + account: Account, addConversation: AddConversation, setMessages: Function, storeMessage: StoreMessageAsync, @@ -18,6 +25,7 @@ export const handleMessagesFromWebSocket = async ( encryptedEnvelop: EncryptionEnvelop, resolveTLDtoAlias: Function, updateConversationList: (conversation: string, updatedAt: number) => void, + addMessage: Function, ) => { const decryptedEnvelop: Envelop = { message: JSON.parse( @@ -71,6 +79,37 @@ export const handleMessagesFromWebSocket = async ( }; }); + // if contact is selected then send READ_OPENED acknowledgment to sender for new message received + if ( + selectedContact && + selectedContact.contactDetails.account.ensName === + messageModel.envelop.message.metadata.from + ) { + const readedMsg = await createReadOpenMessage( + messageModel.envelop.message.metadata.from, + account!.ensName, + 'READ_OPENED', + profileKeys?.signingKeyPair.privateKey!, + messageModel.envelop.metadata?.encryptedMessageHash as string, + ); + + await addMessage(messageModel.envelop.message.metadata.from, readedMsg); + } else if ( + messageModel.envelop.message.metadata.type !== 'READ_RECEIVED' && + messageModel.envelop.message.metadata.type !== 'READ_OPENED' + ) { + // send READ_RECEIVED acknowledgment to sender for new message received + const readedMsg = await createReadReceiveMessage( + messageModel.envelop.message.metadata.from, + account!.ensName, + 'READ_RECEIVED', + profileKeys?.signingKeyPair.privateKey!, + messageModel.envelop.metadata?.encryptedMessageHash as string, + ); + + await addMessage(messageModel.envelop.message.metadata.from, readedMsg); + } + // Update the conversation with the latest message timestamp updateConversationList( contact, diff --git a/packages/messenger-widget/src/hooks/messages/useMessage.tsx b/packages/messenger-widget/src/hooks/messages/useMessage.tsx index 90d5c26ab..d8dc8a494 100644 --- a/packages/messenger-widget/src/hooks/messages/useMessage.tsx +++ b/packages/messenger-widget/src/hooks/messages/useMessage.tsx @@ -5,8 +5,9 @@ import { Message, MessageState, buildEnvelop, + createReadOpenMessage, } from '@dm3-org/dm3-lib-messaging'; -import { normalizeEnsName } from '@dm3-org/dm3-lib-profile'; +import { Account, normalizeEnsName } from '@dm3-org/dm3-lib-profile'; import { sha256, stringify } from '@dm3-org/dm3-lib-shared'; import { StorageEnvelopContainer as StorageEnvelopContainerNew } from '@dm3-org/dm3-lib-storage'; import { useCallback, useContext, useEffect, useState } from 'react'; @@ -25,6 +26,12 @@ import { handleMessagesFromWebSocket } from './sources/handleMessagesFromWebSock const DEFAULT_MESSAGE_PAGESIZE = 100; +export enum MessageIndicator { + SENT = 'SENT', + RECEIVED = 'RECEIVED', + READED = 'READED', +} + //Message source to identify where a message comes from. This is important to handle pagination of storage messages properly export enum MessageSource { //Messages added by the client via addMessage @@ -41,6 +48,7 @@ export type MessageModel = StorageEnvelopContainerNew & { reactions: Envelop[]; replyToMessageEnvelop?: Envelop; source: MessageSource; + indicator?: MessageIndicator; }; export type MessageStorage = { @@ -102,6 +110,7 @@ export const useMessage = () => { useEffect(() => { onNewMessage((encryptedEnvelop: EncryptionEnvelop) => { handleMessagesFromWebSocket( + account as Account, addConversation, setMessages, storeMessage, @@ -110,6 +119,7 @@ export const useMessage = () => { encryptedEnvelop, resolveTLDtoAlias, updateConversationList, + addMessage, ); }); @@ -120,39 +130,72 @@ export const useMessage = () => { //Mark messages as read when the selected contact changes useEffect(() => { - const _contact = selectedContact?.contactDetails.account.ensName; - if (!_contact) { - return; - } + const markMsgsAsRead = async () => { + const _contact = selectedContact?.contactDetails.account.ensName; + if (!_contact) { + return; + } - const contact = normalizeEnsName(_contact); + const contact = normalizeEnsName(_contact); - const unreadMessages = (messages[contact] ?? []).filter( - (message) => - message.messageState !== MessageState.Read && - message.envelop.message.metadata.from !== account?.ensName, - ); + const unreadMessages = (messages[contact] ?? []).filter( + (message) => + message.messageState !== MessageState.Read && + message.envelop.message.metadata.from !== account?.ensName, + ); - setMessages((prev) => { - //Check no new messages are added here - return { - ...prev, - [contact]: [ - ...(prev[contact] ?? []).map((message) => ({ - ...message, - messageState: MessageState.Read, - })), - ], - }; - }); + setMessages((prev) => { + //Check no new messages are added here + return { + ...prev, + [contact]: [ + ...(prev[contact] ?? []).map((message) => ({ + ...message, + messageState: MessageState.Read, + })), + ], + }; + }); - editMessageBatchAsync( - contact, - unreadMessages.map((message) => ({ - ...message, - messageState: MessageState.Read, - })), - ); + const ackToSend = unreadMessages.filter( + (data) => + data.envelop.message.metadata.type !== 'READ_RECEIVED' && + data.envelop.message.metadata.type !== 'READ_OPENED', + ); + + // send READ_OPENED acknowledgment to sender for all new messages received + const readedMsgs = await Promise.all( + ackToSend.map(async (message: MessageModel) => { + return await createReadOpenMessage( + message.envelop.message.metadata.from, + account!.ensName, + 'READ_OPENED', + profileKeys?.signingKeyPair.privateKey!, + message.envelop.metadata + ?.encryptedMessageHash as string, + ); + }), + ); + + // add messages + await Promise.all( + readedMsgs.map(async (msgs, index) => { + await addMessage( + ackToSend[index].envelop.message.metadata.from, + msgs, + ); + }), + ); + + editMessageBatchAsync( + contact, + unreadMessages.map((message) => ({ + ...message, + messageState: MessageState.Read, + })), + ); + }; + markMsgsAsRead(); }, [selectedContact]); //View function that returns wether a contact is loading @@ -184,6 +227,8 @@ export const useMessage = () => { return messages[contactName].filter( (message) => message.messageState !== MessageState.Read && + message.envelop.message.metadata.type !== 'READ_OPENED' && + message.envelop.message.metadata.type !== 'READ_RECEIVED' && message.envelop.message.metadata.from !== account?.ensName, ).length; }, @@ -350,6 +395,7 @@ export const useMessage = () => { 0, ), handleMessagesFromDeliveryService( + selectedContact, account!, profileKeys!, addConversation, @@ -358,6 +404,7 @@ export const useMessage = () => { fetchNewMessages, syncAcknowledgment, updateConversationList, + addMessage, ), ]); const flatten = initialMessages.reduce( diff --git a/packages/messenger-widget/src/interfaces/props.ts b/packages/messenger-widget/src/interfaces/props.ts index f27095c21..e8425748a 100644 --- a/packages/messenger-widget/src/interfaces/props.ts +++ b/packages/messenger-widget/src/interfaces/props.ts @@ -1,6 +1,7 @@ import { Envelop, MessageState } from '@dm3-org/dm3-lib-messaging'; import { MessageActionType } from '../utils/enum-type-utils'; import { IAttachmentPreview, ContactPreview } from './utils'; +import { MessageIndicator } from '../hooks/messages/useMessage'; export interface IEnsDetails { propertyKey: string; @@ -23,6 +24,7 @@ export interface MessageProps { reactions: Envelop[]; isLastMessage?: boolean; hideFunction?: string; + indicator?: MessageIndicator; } export interface MessageAction { From 65eea40889038189d781baff5b1a3f5f5bb8ba63 Mon Sep 17 00:00:00 2001 From: Bhupesh-MS Date: Fri, 26 Jul 2024 16:13:11 +0530 Subject: [PATCH 030/148] fixed broken tests --- .../src/hooks/messages/useMessage.test.tsx | 27 ++++++++++++++++--- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/packages/messenger-widget/src/hooks/messages/useMessage.test.tsx b/packages/messenger-widget/src/hooks/messages/useMessage.test.tsx index 49bd9b2c1..233b454c5 100644 --- a/packages/messenger-widget/src/hooks/messages/useMessage.test.tsx +++ b/packages/messenger-widget/src/hooks/messages/useMessage.test.tsx @@ -678,8 +678,14 @@ describe('useMessage hook test cases', () => { result.current.messages['alice.eth'].length > 0, ); + // Filter out the acknowledgment messages + const sentMsgs = result.current.messages['alice.eth'].filter( + (data) => + data.envelop.message.metadata.type !== 'READ_RECEIVED', + ); + expect(result.current.contactIsLoading('alice.eth')).toBe(false); - expect(result.current.messages['alice.eth'].length).toBe(3); + expect(sentMsgs.length).toBe(3); }); }); describe('message pagination', () => { @@ -885,9 +891,16 @@ describe('useMessage hook test cases', () => { result.current.messages['alice.eth'].length > 0, ); + // Filter out the acknowledgment messages + const sentMsgs = result.current.messages['alice.eth'].filter( + (data) => + data.envelop.message.metadata.type !== 'READ_RECEIVED', + ); + expect(result.current.contactIsLoading('alice.eth')).toBe(false); + //Initial message number would be storage(100) = Ds (13) == 113 - expect(result.current.messages['alice.eth'].length).toBe(113); + expect(sentMsgs.length).toBe(113); await act(async () => result.current.loadMoreMessages('alice.eth')); @@ -898,11 +911,17 @@ describe('useMessage hook test cases', () => { result.current.messages['alice.eth'].length > 133, ); + // Filter out the acknowledgment messages + const moreSentMsgs = result.current.messages['alice.eth'].filter( + (data) => + data.envelop.message.metadata.type !== 'READ_RECEIVED', + ); + expect(result.current.contactIsLoading('alice.eth')).toBe(false); - expect(result.current.messages['alice.eth'].length).toBe(213); + expect(moreSentMsgs.length).toBe(213); //991 = 99 message 100(since pageSize starts from 0) = 1 offset expect( - result.current.messages['alice.eth'][212].envelop.message + result.current.messages['alice.eth'][225].envelop.message .message, ).toBe('hello dm3 991'); }); From 7f76b69502ab6e7da1a368b487b4337ce8dac27a Mon Sep 17 00:00:00 2001 From: Bhupesh-MS Date: Fri, 26 Jul 2024 17:55:13 +0530 Subject: [PATCH 031/148] added console logs for auth issue --- packages/lib/delivery/src/Keys.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/packages/lib/delivery/src/Keys.ts b/packages/lib/delivery/src/Keys.ts index 61aa32461..5af091682 100644 --- a/packages/lib/delivery/src/Keys.ts +++ b/packages/lib/delivery/src/Keys.ts @@ -74,6 +74,11 @@ export async function createNewSessionToken( ): Promise { const session = await getAccount(ensName); + console.log('Signature is : ', signature); + console.log('Challenge is : ', challenge); + console.log('ENS name is : ', ensName); + console.log('Account session is :::::::::::::::: ', session); + if (!session) { throw Error('Session not found'); } @@ -84,6 +89,11 @@ export async function createNewSessionToken( algorithms: ['HS256'], }); + console.log( + 'Account challengePayload is :::::::::::::::: ', + challengePayload, + ); + // check if the payload of the challenge-jwt has the proper schema if ( typeof challengePayload == 'string' || From a3864c1f394411856ec2e4724ba9925acafa3416 Mon Sep 17 00:00:00 2001 From: AlexNi245 Date: Mon, 29 Jul 2024 10:39:12 +0200 Subject: [PATCH 032/148] apply new account method to messaging --- .../delivery-service/src/messaging.test.ts | 172 +++++++++++------- packages/delivery-service/src/messaging.ts | 9 +- 2 files changed, 110 insertions(+), 71 deletions(-) diff --git a/packages/delivery-service/src/messaging.test.ts b/packages/delivery-service/src/messaging.test.ts index 8259e54ba..1b8c4a9ad 100644 --- a/packages/delivery-service/src/messaging.test.ts +++ b/packages/delivery-service/src/messaging.test.ts @@ -1,18 +1,18 @@ -import { createKeyPair } from '@dm3-org/dm3-lib-crypto'; import { Session } from '@dm3-org/dm3-lib-delivery'; import { UserProfile } from '@dm3-org/dm3-lib-profile'; import { IWebSocketManager, ethersHelper } from '@dm3-org/dm3-lib-shared'; +import { + MockDeliveryServiceProfile, + MockMessageFactory, + MockedUserProfile, + getMockDeliveryServiceProfile, + mockUserProfile, +} from '@dm3-org/dm3-lib-test-helper'; import { Socket } from 'socket.io'; -import winston from 'winston'; import { testData } from '../../../test-data/encrypted-envelops.test'; import { onConnection } from './messaging'; - -const SENDER_ADDRESS = '0x25A643B6e52864d0eD816F1E43c0CF49C83B8292'; -const RECEIVER_ADDRESS = '0xDd36ae7F9a8E34FACf1e110c6e9d37D0dc917855'; - -global.logger = winston.createLogger({ - transports: [new winston.transports.Console()], -}); +import { ethers } from 'ethers'; +import { EncryptionEnvelop } from '@dm3-org/dm3-lib-messaging'; const serverSecret = 'secret'; const mockWsManager: IWebSocketManager = { @@ -20,59 +20,73 @@ const mockWsManager: IWebSocketManager = { return Promise.resolve(false); }, }; -const keysA = { - encryptionKeyPair: { - publicKey: 'eHmMq29FeiPKfNPkSctPuZGXvV0sKeO/KZkX2nXvMgw=', - privateKey: 'pMI77F2w3GK+omZCB4A61WDqISOOnWGXR2f/MTLbqbY=', - }, - signingKeyPair: { - publicKey: '+tkDQWZfv9ixBmObsf8tgTHTZajwAE9muTtFAUj2e9I=', - privateKey: - '+DpeBjCzICFoi743/466yJunsHR55Bhr3GnqcS4cuJX62QNBZl+/2LEGY5ux/y2BMdNlqPAAT2a5O0UBSPZ70g==', - }, - storageEncryptionKey: '+DpeBjCzICFoi743/466yJunsHR55Bhr3GnqcS4cuJU=', - storageEncryptionNonce: 0, -}; -const keyPair = createKeyPair(); +describe('Messaging', () => { + let sender: MockedUserProfile; + let receiver: MockedUserProfile; + let ds: MockDeliveryServiceProfile; -const getAccount = async (address: string) => { - const emptyProfile: UserProfile = { - publicSigningKey: '', - publicEncryptionKey: '', - deliveryServices: [''], - }; - const isSender = ethersHelper.formatAddress(address) === SENDER_ADDRESS; - const isReceiver = ethersHelper.formatAddress(address) === RECEIVER_ADDRESS; - - const session = (account: string, token: string, profile: UserProfile) => ({ - account, - signedUserProfile: { - profile, - signature: '', - }, - token, + beforeEach(async () => { + const receiverWallet = ethers.Wallet.createRandom(); + sender = await mockUserProfile( + ethers.Wallet.createRandom(), + 'bob.eth', + ['http://localhost:3000'], + ); + receiver = await mockUserProfile(receiverWallet, 'alice.eth', [ + 'http://localhost:3000', + ]); + ds = await getMockDeliveryServiceProfile( + ethers.Wallet.createRandom(), + 'http://localhost:3000', + ); }); + const getAccount = async (address: string) => { + const emptyProfile: UserProfile = { + publicSigningKey: '', + publicEncryptionKey: '', + deliveryServices: [''], + }; + const isSender = ethersHelper.formatAddress(address) === sender.address; + const isReceiver = + ethersHelper.formatAddress(address) === receiver.address; - if (isSender) { - return session(SENDER_ADDRESS, '123', emptyProfile); - } - - if (isReceiver) { - return session(RECEIVER_ADDRESS, 'abc', { - ...emptyProfile, - publicEncryptionKey: (await keyPair).publicKey, + const session = ( + account: string, + token: string, + profile: UserProfile, + ) => ({ + account, + signedUserProfile: { + profile, + signature: '', + }, + token, }); - } - return null; -}; + if (isSender) { + return session(sender.address, '123', emptyProfile); + } -describe('Messaging', () => { - // prepare some mocks that are used by many tests - const web3Provider = { - resolveName: async () => '0x25A643B6e52864d0eD816F1E43c0CF49C83B8292', + if (isReceiver) { + return session(receiver.address, 'abc', { + ...emptyProfile, + publicEncryptionKey: + receiver.profileKeys.encryptionKeyPair.publicKey, + }); + } + + return null; }; + + const web3Provider = { + resolveName: async (name: string) => { + if (name === 'alice.eth') { + return receiver.address; + } + }, + } as any; + const db = { getAccount, createMessage: () => {}, @@ -86,11 +100,6 @@ describe('Messaging', () => { }), }, }; - //The same data used in Messages.test - const data = { - envelop: testData.envelopA, - //token: '123', - }; describe('submitMessage', () => { it('returns success if schema is valid', (done: any) => { @@ -112,8 +121,14 @@ describe('Messaging', () => { on: async (name: string, onSubmitMessage: any) => { //We just want to test the submitMessage callback fn if (name === 'submitMessage') { + const envelop: EncryptionEnvelop = + await MockMessageFactory( + sender, + receiver, + ds, + ).createEncryptedEnvelop('hello dm3'); await onSubmitMessage( - { ...data, token: '123' }, + { envelop, token: '123' }, callback, ); } @@ -125,7 +140,7 @@ describe('Messaging', () => { io as any, web3Provider as any, db as any, - keysA, + ds.keys, serverSecret, mockWsManager, )(getSocketMock()); @@ -150,7 +165,13 @@ describe('Messaging', () => { on: async (name: string, onSubmitMessage: any) => { //We just want to test the submitMessage callback fn if (name === 'submitMessage') { - await onSubmitMessage(data, callback); + const envelop: EncryptionEnvelop = + await MockMessageFactory( + sender, + receiver, + ds, + ).createEncryptedEnvelop('hello dm3'); + await onSubmitMessage({ envelop }, callback); } }, } as unknown as Socket; @@ -160,7 +181,7 @@ describe('Messaging', () => { io as any, web3Provider as any, db as any, - keysA, + ds.keys, serverSecret, mockWsManager, )(getSocketMock()); @@ -196,7 +217,18 @@ describe('Messaging', () => { on: async (name: string, onSubmitMessage: any) => { //We just want to test the submitMessage callback fn if (name === 'submitMessage') { - await onSubmitMessage(data, callback); + const envelop: EncryptionEnvelop = + await MockMessageFactory( + sender, + receiver, + ds, + ).createEncryptedEnvelop('hello dm3'); + await onSubmitMessage( + { + envelop, + }, + callback, + ); } }, } as unknown as Socket; @@ -206,7 +238,7 @@ describe('Messaging', () => { io as any, web3Provider as any, db as any, - keysA, + ds.keys, serverSecret, mockWsManager, )(getSocketMock()); @@ -222,7 +254,13 @@ describe('Messaging', () => { expect(e.error).toBe('invalid schema'); }); - const localData = { ...data }; + const envelop: EncryptionEnvelop = await MockMessageFactory( + sender, + receiver, + ds, + ).createEncryptedEnvelop('hello dm3'); + + const localData = { envelop }; const metadata = localData.envelop.metadata as any; // Change the data so it becomes invalid in order to provoke the 'invalid schema' error delete metadata.deliveryInformation; @@ -243,7 +281,7 @@ describe('Messaging', () => { io as any, web3Provider as any, db as any, - keysA, + ds.keys, serverSecret, mockWsManager, )(getSocketMock()); diff --git a/packages/delivery-service/src/messaging.ts b/packages/delivery-service/src/messaging.ts index cb372650a..bb6c2cf13 100644 --- a/packages/delivery-service/src/messaging.ts +++ b/packages/delivery-service/src/messaging.ts @@ -51,7 +51,7 @@ export function onConnection( try { const deliveryServiceProperties = getDeliveryServiceProperties(); - global.logger.info({ + console.info({ method: 'WS INCOMING MESSAGE', }); @@ -63,14 +63,14 @@ export function onConnection( if (!isSchemaValid) { const error = 'invalid schema'; - global.logger.warn({ + console.warn({ method: 'WS SUBMIT MESSAGE', error, }); return callback({ error }); } - global.logger.info({ + console.info({ method: 'WS INCOMING MESSAGE', keys: keys.encryptionKeyPair.publicKey, }); @@ -86,10 +86,11 @@ export function onConnection( }, ); await messageProcessor.processEnvelop(data.envelop); + console.log('callback'); callback({ response: 'success' }); } catch (error: any) { - global.logger.warn({ + console.warn({ method: 'WS SUBMIT MESSAGE', error: (error as Error).toString(), }); From 50d6b55e04e6dc895e27eeba4c67369a7ea170b5 Mon Sep 17 00:00:00 2001 From: AlexNi245 Date: Mon, 29 Jul 2024 10:56:20 +0200 Subject: [PATCH 033/148] adjust rpc-proxy tests --- .../src/rpc/rpc-proxy.test.ts | 238 +++++++++--------- 1 file changed, 123 insertions(+), 115 deletions(-) diff --git a/packages/delivery-service/src/rpc/rpc-proxy.test.ts b/packages/delivery-service/src/rpc/rpc-proxy.test.ts index 2f64278c0..ff19d50da 100644 --- a/packages/delivery-service/src/rpc/rpc-proxy.test.ts +++ b/packages/delivery-service/src/rpc/rpc-proxy.test.ts @@ -1,47 +1,98 @@ -import { createKeyPair } from '@dm3-org/dm3-lib-crypto'; import { normalizeEnsName, UserProfile } from '@dm3-org/dm3-lib-profile'; -import { IWebSocketManager, stringify } from '@dm3-org/dm3-lib-shared'; +import { + ethersHelper, + IWebSocketManager, + stringify, +} from '@dm3-org/dm3-lib-shared'; +import { + getMockDeliveryServiceProfile, + MockDeliveryServiceProfile, + MockedUserProfile, + MockMessageFactory, + mockUserProfile, +} from '@dm3-org/dm3-lib-test-helper'; import { Axios } from 'axios'; import bodyParser from 'body-parser'; +import { ethers } from 'ethers'; import express from 'express'; import request from 'supertest'; import winston from 'winston'; -import { testData } from '../../../../test-data/encrypted-envelops.test'; import RpcProxy from './rpc-proxy'; +import { EncryptionEnvelop } from '@dm3-org/dm3-lib-messaging'; global.logger = winston.createLogger({ transports: [new winston.transports.Console()], }); -const SENDER_NAME = 'alice.eth'; -const RECEIVER_NAME = 'bob.eth'; - -const SENDER_ADDRESS = '0x25A643B6e52864d0eD816F1E43c0CF49C83B8292'; -const RECEIVER_ADDRESS = '0xDd36ae7F9a8E34FACf1e110c6e9d37D0dc917855'; - const mockWsManager: IWebSocketManager = { isConnected: function (ensName: string): Promise { return Promise.resolve(false); }, }; -const keyPair = createKeyPair(); - -const keysA = { - encryptionKeyPair: { - publicKey: 'eHmMq29FeiPKfNPkSctPuZGXvV0sKeO/KZkX2nXvMgw=', - privateKey: 'pMI77F2w3GK+omZCB4A61WDqISOOnWGXR2f/MTLbqbY=', - }, - signingKeyPair: { - publicKey: '+tkDQWZfv9ixBmObsf8tgTHTZajwAE9muTtFAUj2e9I=', - privateKey: - '+DpeBjCzICFoi743/466yJunsHR55Bhr3GnqcS4cuJX62QNBZl+/2LEGY5ux/y2BMdNlqPAAT2a5O0UBSPZ70g==', - }, - storageEncryptionKey: '+DpeBjCzICFoi743/466yJunsHR55Bhr3GnqcS4cuJU=', - storageEncryptionNonce: 0, -}; +const RECEIVER_NAME = 'alice.eth'; +const SENDER_NAME = 'bob.eth'; describe('rpc-Proxy', () => { + let sender: MockedUserProfile; + let receiver: MockedUserProfile; + let ds: MockDeliveryServiceProfile; + + beforeEach(async () => { + const receiverWallet = ethers.Wallet.createRandom(); + sender = await mockUserProfile( + ethers.Wallet.createRandom(), + SENDER_NAME, + ['http://localhost:3000'], + ); + receiver = await mockUserProfile(receiverWallet, RECEIVER_NAME, [ + 'http://localhost:3000', + ]); + ds = await getMockDeliveryServiceProfile( + ethers.Wallet.createRandom(), + 'http://localhost:3000', + ); + }); + + const getAccount = async (address: string) => { + const emptyProfile: UserProfile = { + publicSigningKey: '', + publicEncryptionKey: '', + deliveryServices: [''], + }; + + const isSender = ethersHelper.formatAddress(address) === sender.address; + const isReceiver = + ethersHelper.formatAddress(address) === receiver.address; + + const session = ( + account: string, + token: string, + profile: UserProfile, + ) => ({ + account, + signedUserProfile: { + profile, + signature: '', + }, + token, + }); + + if (isSender) { + return session(sender.address, '123', emptyProfile); + } + + if (isReceiver) { + return session(RECEIVER_NAME, 'abc', { + ...emptyProfile, + publicEncryptionKey: + receiver.profileKeys.encryptionKeyPair.publicKey, + }); + } + + return null; + }; + describe('routing', () => { it('Should block non-dm3 related requests', async () => { const app = express(); @@ -53,7 +104,7 @@ describe('rpc-Proxy', () => { {} as any, {} as any, {} as any, - keysA, + ds.keys, mockWsManager, ), ); @@ -83,22 +134,25 @@ describe('rpc-Proxy', () => { post: mockPost, } as Partial; - const keys = { - signing: keysA.signingKeyPair, - encryption: keysA.encryptionKeyPair, - }; - process.env.SIGNING_PUBLIC_KEY = keys.signing.publicKey; - process.env.SIGNING_PRIVATE_KEY = keys.signing.privateKey; - process.env.ENCRYPTION_PUBLIC_KEY = keys.encryption.publicKey; - process.env.ENCRYPTION_PRIVATE_KEY = keys.encryption.privateKey; + process.env.SIGNING_PUBLIC_KEY = ds.keys.signingKeyPair.publicKey; + process.env.SIGNING_PRIVATE_KEY = ds.keys.signingKeyPair.privateKey; + process.env.ENCRYPTION_PUBLIC_KEY = + ds.keys.encryptionKeyPair.publicKey; + process.env.ENCRYPTION_PRIVATE_KEY = + ds.keys.encryptionKeyPair.privateKey; const deliveryServiceProperties = { sizeLimit: 2 ** 14, notificationChannel: [], }; + const web3Provider = { - resolveName: async () => - '0x25A643B6e52864d0eD816F1E43c0CF49C83B8292', - }; + resolveName: async (name: string) => { + if (name === 'alice.eth') { + return receiver.address; + } + }, + } as any; + const db = { createMessage: () => {}, getAccount, @@ -122,31 +176,23 @@ describe('rpc-Proxy', () => { io as any, web3Provider as any, db as any, - keysA, + ds.keys, mockWsManager, ), ); + const envelop: EncryptionEnvelop = await MockMessageFactory( + sender, + receiver, + ds, + ).createEncryptedEnvelop('hello dm3'); + const { status } = await request(app) .post('/') .send({ jsonrpc: '2.0', method: 'dm3_submitMessage', - params: [ - JSON.stringify({ - message: '', - metadata: { - deliveryInformation: stringify( - testData.deliveryInformation, - ), - signature: '', - encryptedMessageHash: '', - version: '', - encryptionScheme: 'x25519-chacha20-poly1305', - }, - }), - '123', - ], + params: [JSON.stringify(envelop), '123'], }); expect(mockPost).not.toBeCalled(); @@ -161,22 +207,25 @@ describe('rpc-Proxy', () => { post: mockPost, } as Partial; - const keys = { - signing: keysA.signingKeyPair, - encryption: keysA.encryptionKeyPair, - }; - process.env.SIGNING_PUBLIC_KEY = keys.signing.publicKey; - process.env.SIGNING_PRIVATE_KEY = keys.signing.privateKey; - process.env.ENCRYPTION_PUBLIC_KEY = keys.encryption.publicKey; - process.env.ENCRYPTION_PRIVATE_KEY = keys.encryption.privateKey; + process.env.SIGNING_PUBLIC_KEY = ds.keys.signingKeyPair.publicKey; + process.env.SIGNING_PRIVATE_KEY = ds.keys.signingKeyPair.privateKey; + process.env.ENCRYPTION_PUBLIC_KEY = + ds.keys.encryptionKeyPair.publicKey; + process.env.ENCRYPTION_PRIVATE_KEY = + ds.keys.encryptionKeyPair.privateKey; + const deliveryServiceProperties = { sizeLimit: 2 ** 14, notificationChannel: [], }; + const web3Provider = { - resolveName: async () => - '0x25A643B6e52864d0eD816F1E43c0CF49C83B8292', - }; + resolveName: async (name: string) => { + if (name === 'alice.eth') { + return receiver.address; + } + }, + } as any; const db = { createMessage: () => {}, getAccount, @@ -200,30 +249,22 @@ describe('rpc-Proxy', () => { io as any, web3Provider as any, db as any, - keysA, + ds.keys, mockWsManager, ), ); + const envelop: EncryptionEnvelop = await MockMessageFactory( + sender, + receiver, + ds, + ).createEncryptedEnvelop('hello dm3'); const { status } = await request(app) .post('/') .send({ jsonrpc: '2.0', method: 'dm3_submitMessage', - params: [ - JSON.stringify({ - message: '', - metadata: { - deliveryInformation: stringify( - testData.deliveryInformation, - ), - signature: '', - encryptedMessageHash: '', - version: '', - encryptionScheme: 'x25519-chacha20-poly1305', - }, - }), - ], + params: [JSON.stringify(envelop)], }); expect(mockPost).not.toBeCalled(); @@ -253,7 +294,7 @@ describe('rpc-Proxy', () => { {} as any, {} as any, {} as any, - keysA, + ds.keys, {} as any, ), ); @@ -293,7 +334,7 @@ describe('rpc-Proxy', () => { {} as any, {} as any, {} as any, - keysA, + ds.keys, mockWsManager, ), ); @@ -334,7 +375,7 @@ describe('rpc-Proxy', () => { {} as any, web3Provider as any, db as any, - keysA, + ds.keys, mockWsManager, ), ); @@ -399,7 +440,7 @@ describe('rpc-Proxy', () => { {} as any, web3Provider as any, db as any, - keysA, + ds.keys, mockWsManager, ), ); @@ -423,36 +464,3 @@ describe('rpc-Proxy', () => { }); }); }); - -const getAccount = async (ensName: string) => { - const emptyProfile: UserProfile = { - publicSigningKey: '', - publicEncryptionKey: '', - deliveryServices: [''], - }; - - const isSender = normalizeEnsName(ensName) === SENDER_NAME; - const isReceiver = normalizeEnsName(ensName) === RECEIVER_NAME; - - const session = (account: string, token: string, profile: UserProfile) => ({ - account, - signedUserProfile: { - profile, - signature: '', - }, - token, - }); - - if (isSender) { - return session(SENDER_ADDRESS, '123', emptyProfile); - } - - if (isReceiver) { - return session(RECEIVER_NAME, 'abc', { - ...emptyProfile, - publicEncryptionKey: (await keyPair).publicKey, - }); - } - - return null; -}; From f5219395362168e532bdc5a386b947bb536f35d0 Mon Sep 17 00:00:00 2001 From: AlexNi245 Date: Mon, 29 Jul 2024 10:59:49 +0200 Subject: [PATCH 034/148] remove getMEssages from lib/delivery --- packages/delivery-service/src/delivery.ts | 46 ++++++--- packages/lib/delivery/src/Messages.test.ts | 104 --------------------- packages/lib/delivery/src/Messages.ts | 37 -------- packages/lib/delivery/src/index.ts | 1 - 4 files changed, 33 insertions(+), 155 deletions(-) delete mode 100644 packages/lib/delivery/src/Messages.test.ts diff --git a/packages/delivery-service/src/delivery.ts b/packages/delivery-service/src/delivery.ts index 298442920..79f8675ce 100644 --- a/packages/delivery-service/src/delivery.ts +++ b/packages/delivery-service/src/delivery.ts @@ -1,12 +1,15 @@ -import { Acknowledgment, getMessages, schema } from '@dm3-org/dm3-lib-delivery'; +import { + Acknowledgment, + getConversationId, + schema, +} from '@dm3-org/dm3-lib-delivery'; +import { normalizeEnsName } from '@dm3-org/dm3-lib-profile'; import { auth } from '@dm3-org/dm3-lib-server-side'; import { validateSchema } from '@dm3-org/dm3-lib-shared'; -import { getConversationId } from '@dm3-org/dm3-lib-delivery'; import cors from 'cors'; import { ethers } from 'ethers'; import express from 'express'; import { IDatabase } from './persistence/getDatabase'; -import { DeliveryServiceProfileKeys } from '@dm3-org/dm3-lib-profile'; const syncAcknowledgementParamsSchema = { type: 'object', @@ -31,7 +34,6 @@ const syncAcknowledgementBodySchema = { export default ( web3Provider: ethers.providers.JsonRpcProvider, db: IDatabase, - keys: DeliveryServiceProfileKeys, serverSecret: string, ) => { const router = express.Router(); @@ -45,20 +47,37 @@ export default ( '/messages/:ensName/contact/:contactEnsName', async (req: express.Request, res, next) => { try { - const idEnsName = await db.getIdEnsName(req.params.ensName); - - const idContactEnsName = await db.getIdEnsName( + //normalize the contact name + const contactEnsName = await normalizeEnsName( req.params.contactEnsName, ); + //retive the address for the contact name since it is used as a key in the db + const receiverAddress = await web3Provider.resolveName( + req.params.ensName, + ); + + //If the address is not found we return a 404. This should normally not happen since the receiver always is known to the delivery service + if (!receiverAddress) { + console.error( + 'receiver address not found for name ', + req.params.ensName, + ); + return res.status(404).send({ + error: + 'receiver address not found for name ' + + req.params.ensName, + }); + } - const newMessages = await getMessages( - db.getMessages, - keys.encryptionKeyPair, - idEnsName, - idContactEnsName, + //The new layout resolves conversations using a conversation id [addr(reiceiver),ensName(sender)] + const conversationId = getConversationId( + receiverAddress, + contactEnsName, ); - res.json(newMessages); + //Better 1000 than the previous fifty. This is a temporary solution until we implement pagination + const messages = db.getMessages(conversationId, 0, 1000); + res.json(messages); } catch (e) { next(e); } @@ -83,6 +102,7 @@ export default ( //@ts-ignore async (req: express.Request, res, next) => { try { + //TODO use address const incomingMessages = await db.getIncomingMessages( req.params.ensName, //Fetch the last 10 messages per conversation diff --git a/packages/lib/delivery/src/Messages.test.ts b/packages/lib/delivery/src/Messages.test.ts deleted file mode 100644 index af2d56c5c..000000000 --- a/packages/lib/delivery/src/Messages.test.ts +++ /dev/null @@ -1,104 +0,0 @@ -import { EncryptionEnvelop } from '@dm3-org/dm3-lib-messaging'; -import { testData } from '../../../../test-data/encrypted-envelops.test'; -import { getConversationId, getMessages } from './Messages'; - -const keysA = { - encryptionKeyPair: { - publicKey: 'eHmMq29FeiPKfNPkSctPuZGXvV0sKeO/KZkX2nXvMgw=', - privateKey: 'pMI77F2w3GK+omZCB4A61WDqISOOnWGXR2f/MTLbqbY=', - }, - signingKeyPair: { - publicKey: '+tkDQWZfv9ixBmObsf8tgTHTZajwAE9muTtFAUj2e9I=', - privateKey: - '+DpeBjCzICFoi743/466yJunsHR55Bhr3GnqcS4cuJX62QNBZl+/2LEGY5ux/y2BMdNlqPAAT2a5O0UBSPZ70g==', - }, - storageEncryptionKey: '+DpeBjCzICFoi743/466yJunsHR55Bhr3GnqcS4cuJU=', - storageEncryptionNonce: 0, -}; - -describe('Messages', () => { - describe('GetMessages', () => { - it('returns all messages of the user', async () => { - const conversationIdToUse = getConversationId( - 'alice.eth', - 'bob.eth', - ); - - const loadMessages = async ( - conversationId: string, - offset: number, - size: number, - ): Promise => { - return conversationId === conversationIdToUse - ? ([ - { - message: '', - metadata: { - encryptionScheme: 'x25519-chacha20-poly1305', - deliveryInformation: - testData.deliveryInformationUnecrypted, - version: '', - encryptedMessageHash: '', - signature: '', - }, - }, - { - message: '', - metadata: { - encryptionScheme: 'x25519-chacha20-poly1305', - deliveryInformation: - testData.deliveryInformationUnecrypted, - version: '', - encryptedMessageHash: '', - signature: '', - }, - }, - { - message: '', - metadata: { - encryptionScheme: 'x25519-chacha20-poly1305', - deliveryInformation: - testData.delvieryInformationBUnecrypted, - version: '', - encryptedMessageHash: '', - signature: '', - }, - }, - ] as EncryptionEnvelop[]) - : []; - }; - - expect( - await getMessages( - loadMessages, - keysA.encryptionKeyPair, - 'bob.eth', - 'alice.eth', - ), - ).toStrictEqual([ - { - message: '', - metadata: { - encryptionScheme: 'x25519-chacha20-poly1305', - deliveryInformation: - testData.deliveryInformationUnecrypted, - encryptedMessageHash: '', - signature: '', - version: '', - }, - }, - { - message: '', - metadata: { - encryptionScheme: 'x25519-chacha20-poly1305', - deliveryInformation: - testData.deliveryInformationUnecrypted, - version: '', - encryptedMessageHash: '', - signature: '', - }, - }, - ]); - }); - }); -}); diff --git a/packages/lib/delivery/src/Messages.ts b/packages/lib/delivery/src/Messages.ts index a7ed84255..5d1142cf8 100644 --- a/packages/lib/delivery/src/Messages.ts +++ b/packages/lib/delivery/src/Messages.ts @@ -28,43 +28,6 @@ export interface Acknowledgment { export function getConversationId(ensNameA: string, ensNameB: string): string { return [normalizeEnsName(ensNameA), normalizeEnsName(ensNameB)].join(); } -// fetch new messages -export async function getMessages( - loadMessages: ( - conversationId: string, - offset: number, - size: number, - ) => Promise, - encryptionKeyPair: KeyPair, - ensName: string, - contactEnsName: string, -) { - const account = normalizeEnsName(ensName); - - const contact = normalizeEnsName(contactEnsName); - - const conversationId = getConversationId(contact, account); - - const receivedMessages: EncryptionEnvelop[] = await loadMessages( - conversationId, - 0, - 50, - ); - - const envelopContainers = await Promise.all( - receivedMessages.map(async (envelop) => ({ - to: normalizeEnsName( - JSON.parse(JSON.stringify(envelop.metadata.deliveryInformation)) - .to, - ), - envelop, - })), - ); - - return envelopContainers - .filter((envelopContainer) => envelopContainer.to === account) - .map((envelopContainer) => envelopContainer.envelop); -} export async function decryptDeliveryInformation( encryptedEnvelop: EncryptionEnvelop, diff --git a/packages/lib/delivery/src/index.ts b/packages/lib/delivery/src/index.ts index c2254aa45..3477f82c7 100644 --- a/packages/lib/delivery/src/index.ts +++ b/packages/lib/delivery/src/index.ts @@ -6,7 +6,6 @@ export { export { submitUserProfile, getUserProfile } from './UserProfile'; export { addPostmark, - getMessages, decryptDeliveryInformation, handleIncomingMessage, } from './Messages'; From 5368f421ba2836cf3d508c898cfeeb774b527914 Mon Sep 17 00:00:00 2001 From: AlexNi245 Date: Mon, 29 Jul 2024 11:00:22 +0200 Subject: [PATCH 035/148] remove key arg from delivery.test --- packages/delivery-service/src/delivery.test.ts | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/packages/delivery-service/src/delivery.test.ts b/packages/delivery-service/src/delivery.test.ts index 0f7abff6d..0c876cd7e 100644 --- a/packages/delivery-service/src/delivery.test.ts +++ b/packages/delivery-service/src/delivery.test.ts @@ -66,9 +66,7 @@ describe('Delivery', () => { }; const app = express(); app.use(bodyParser.json()); - app.use( - delivery(web3Provider as any, db as any, keysA, serverSecret), - ); + app.use(delivery(web3Provider as any, db as any, serverSecret)); const { status } = await request(app) .get('/messages/alice.eth/contact/bob.eth') @@ -106,9 +104,7 @@ describe('Delivery', () => { }; const app = express(); app.use(bodyParser.json()); - app.use( - delivery(web3Provider as any, db as any, keysA, serverSecret), - ); + app.use(delivery(web3Provider as any, db as any, serverSecret)); const { status } = await request(app) .post( @@ -154,9 +150,7 @@ describe('Delivery', () => { const app = express(); app.use(bodyParser.json()); - app.use( - delivery(web3Provider as any, db as any, keysA, serverSecret), - ); + app.use(delivery(web3Provider as any, db as any, serverSecret)); const { status } = await request(app) .post( From ce58ac68f066ce9968adc52893ce85b44845ed53 Mon Sep 17 00:00:00 2001 From: AlexNi245 Date: Mon, 29 Jul 2024 11:01:14 +0200 Subject: [PATCH 036/148] add wrapper function to getDatabase to use resolved addr instead of name --- packages/delivery-service/src/index.ts | 43 ++++++++++++++++--- .../src/persistence/account/getAccount.ts | 1 - .../src/persistence/getDatabase.ts | 14 +++--- 3 files changed, 43 insertions(+), 15 deletions(-) diff --git a/packages/delivery-service/src/index.ts b/packages/delivery-service/src/index.ts index 10b96e57f..5906a0b61 100644 --- a/packages/delivery-service/src/index.ts +++ b/packages/delivery-service/src/index.ts @@ -1,3 +1,4 @@ +import { Session } from '@dm3-org/dm3-lib-delivery'; import { Auth, Profile, @@ -14,19 +15,20 @@ import { Axios } from 'axios'; import bodyParser from 'body-parser'; import cors from 'cors'; import 'dotenv/config'; +import { ethers } from 'ethers'; import express from 'express'; import http from 'http'; import { Server } from 'socket.io'; +import webpush from 'web-push'; import winston from 'winston'; import { startCleanUpPendingMessagesJob } from './cleanup/cleanUpPendingMessages'; import { getDeliveryServiceProperties } from './config/getDeliveryServiceProperties'; import Delivery from './delivery'; import { onConnection } from './messaging'; import Notifications from './notifications'; -import { getDatabase } from './persistence/getDatabase'; +import { IDatabase, getDatabase } from './persistence/getDatabase'; import RpcProxy from './rpc/rpc-proxy'; import { WebSocketManager } from './ws/WebSocketManager'; -import webpush from 'web-push'; const app = express(); app.use(express.json({ limit: '50mb' })); @@ -34,6 +36,29 @@ app.use(express.urlencoded({ limit: '50mb' })); const server = http.createServer(app); +const getDbWithAddressResolvedGetAccount = ( + db: IDatabase, + web3Provider: ethers.providers.JsonRpcProvider, +): IDatabase => { + const getAccountForEnsName = ( + web3Provider: ethers.providers.JsonRpcProvider, + getAccount: (ensName: string) => Promise, + ) => { + return async (ensName: string) => { + const address = await web3Provider.resolveName(ensName); + if (!address) { + console.info('no address found for ens name: ', ensName); + return null; + } + return getAccount(address); + }; + }; + + return { + ...db, + getAccount: getAccountForEnsName(web3Provider, db.getAccount), + }; +}; //TODO remove app.use(cors()); app.use(bodyParser.json()); @@ -50,8 +75,12 @@ global.logger = winston.createLogger({ (async () => { // load environment const deliveryServiceProperties = getDeliveryServiceProperties(); - const db = await getDatabase(); const web3Provider = await getCachedWebProvider(process.env); + + const db = getDbWithAddressResolvedGetAccount( + await getDatabase(), + web3Provider, + ); const keys = readKeysFromEnv(process.env); const serverSecret = getServerSecret(process.env); @@ -91,9 +120,13 @@ global.logger = winston.createLogger({ return res.send('Hello DM3'); }); - app.use('/auth', Auth(db.getAccount, serverSecret)); + //Auth + //socketAuth + //restAuth + + app.use('/auth', Auth(db, serverSecret)); app.use('/profile', Profile(db, web3Provider, serverSecret)); - app.use('/delivery', Delivery(web3Provider, db, keys, serverSecret)); + app.use('/delivery', Delivery(web3Provider, db, serverSecret)); app.use( '/notifications', Notifications( diff --git a/packages/delivery-service/src/persistence/account/getAccount.ts b/packages/delivery-service/src/persistence/account/getAccount.ts index 85ed788a6..35d76769c 100644 --- a/packages/delivery-service/src/persistence/account/getAccount.ts +++ b/packages/delivery-service/src/persistence/account/getAccount.ts @@ -4,7 +4,6 @@ import { Redis, RedisPrefix } from '../getDatabase'; export function getAccount(redis: Redis) { return async (address: string) => { - console.log(ethers); const session = await redis.get( RedisPrefix.Account + ethers.utils.getAddress(address), ); diff --git a/packages/delivery-service/src/persistence/getDatabase.ts b/packages/delivery-service/src/persistence/getDatabase.ts index 7d2bc0c7f..f313314e7 100644 --- a/packages/delivery-service/src/persistence/getDatabase.ts +++ b/packages/delivery-service/src/persistence/getDatabase.ts @@ -1,9 +1,4 @@ -import { - IGlobalNotification, - IOtp, - Session, - spamFilter, -} from '@dm3-org/dm3-lib-delivery'; +import { IGlobalNotification, IOtp, Session } from '@dm3-org/dm3-lib-delivery'; import { EncryptionEnvelop } from '@dm3-org/dm3-lib-messaging'; import { IAccountDatabase } from '@dm3-org/dm3-lib-server-side'; import { @@ -98,10 +93,11 @@ export async function getDatabase( } export interface IDatabase extends IAccountDatabase { - setAccount: (ensName: string, session: Session) => Promise; - getAccount: (ensName: string) => Promise; + setAccount: (address: string, session: Session) => Promise; + getAccount: (address: string) => Promise; + //TODO use address getIncomingMessages: ( - ensName: string, + address: string, limit: number, ) => Promise; getMessages: ( From 51a1d758a625fcd29f91970462247498a075f383 Mon Sep 17 00:00:00 2001 From: AlexNi245 Date: Mon, 29 Jul 2024 11:03:17 +0200 Subject: [PATCH 037/148] replace getAccount arg with IAccountDb --- packages/lib/server-side/src/auth.test.ts | 25 +++++++++++++++++++---- packages/lib/server-side/src/auth.ts | 11 +++++----- 2 files changed, 27 insertions(+), 9 deletions(-) diff --git a/packages/lib/server-side/src/auth.test.ts b/packages/lib/server-side/src/auth.test.ts index ac202d941..636d314a4 100644 --- a/packages/lib/server-side/src/auth.test.ts +++ b/packages/lib/server-side/src/auth.test.ts @@ -11,6 +11,7 @@ import express from 'express'; import { verify } from 'jsonwebtoken'; import request from 'supertest'; import { Auth } from './auth'; +import { IAccountDatabase } from './iSessionDatabase'; const serverSecret = 'testSecret'; @@ -38,9 +39,13 @@ describe('Auth', () => { describe('getChallenge', () => { describe('schema', () => { it('Returns 200 and a jwt if schema is valid', async () => { + const db = { + getAccount: getAccountMock, + } as IAccountDatabase; + const app = express(); app.use(bodyParser.json()); - app.use(Auth(getAccountMock, serverSecret)); + app.use(Auth(db, serverSecret)); const response = await request(app) .get( @@ -109,8 +114,12 @@ describe('Auth', () => { describe('schema', () => { it('Returns 400 if params is invalid', async () => { const app = express(); + const db = { + getAccount: getAccountMock, + } as IAccountDatabase; + app.use(bodyParser.json()); - app.use(Auth(getAccountMock, serverSecret)); + app.use(Auth(db, serverSecret)); const mnemonic = 'announce room limb pattern dry unit scale effort smooth jazz weasel alcohol'; @@ -148,8 +157,12 @@ describe('Auth', () => { }); it('Returns 400 if body is invalid', async () => { const app = express(); + const db = { + getAccount: getAccountMock, + } as IAccountDatabase; + app.use(bodyParser.json()); - app.use(Auth(getAccountMock, serverSecret)); + app.use(Auth(db, serverSecret)); const mnemonic = 'announce room limb pattern dry unit scale effort smooth jazz weasel alcohol'; @@ -195,8 +208,12 @@ describe('Auth', () => { // }); const app = express(); + const db = { + getAccount: getAccountMockLocal, + } as IAccountDatabase; + app.use(bodyParser.json()); - app.use(Auth(getAccountMockLocal, serverSecret)); + app.use(Auth(db, serverSecret)); // create the challenge jwt const challengeJwt = await createChallenge( diff --git a/packages/lib/server-side/src/auth.ts b/packages/lib/server-side/src/auth.ts index 94806ac8c..8e3673b66 100644 --- a/packages/lib/server-side/src/auth.ts +++ b/packages/lib/server-side/src/auth.ts @@ -1,4 +1,5 @@ import { + Session, createChallenge, createNewSessionToken, } from '@dm3-org/dm3-lib-delivery'; @@ -6,6 +7,8 @@ import { normalizeEnsName } from '@dm3-org/dm3-lib-profile'; import { validateSchema } from '@dm3-org/dm3-lib-shared'; import cors from 'cors'; import express from 'express'; +import { ethers } from 'ethers'; +import { IAccountDatabase } from './iSessionDatabase'; const getChallengeSchema = { type: 'object', @@ -35,9 +38,7 @@ const createNewSessionTokenBodySchema = { additionalProperties: false, }; -type GetAccount = (ensName: string) => Promise; - -export const Auth = (getAccount: GetAccount, serverSecret: string) => { +export const Auth = (db: IAccountDatabase, serverSecret: string) => { const router = express.Router(); //TODO remove @@ -57,7 +58,7 @@ export const Auth = (getAccount: GetAccount, serverSecret: string) => { } const challenge = await createChallenge( - getAccount, + db.getAccount, idEnsName, serverSecret, ); @@ -88,7 +89,7 @@ export const Auth = (getAccount: GetAccount, serverSecret: string) => { } const jwt = await createNewSessionToken( - getAccount, + db.getAccount, req.body.signature, req.body.challenge, idEnsName, From 9adf9746f6661c79f91a05759375c4e623d55659 Mon Sep 17 00:00:00 2001 From: AlexNi245 Date: Mon, 29 Jul 2024 11:03:55 +0200 Subject: [PATCH 038/148] add more logging to MessageProcessor --- .../src/message/MessageProcessor.ts | 36 ++++++++++++------- 1 file changed, 23 insertions(+), 13 deletions(-) diff --git a/packages/delivery-service/src/message/MessageProcessor.ts b/packages/delivery-service/src/message/MessageProcessor.ts index 1165615da..71e1172ac 100644 --- a/packages/delivery-service/src/message/MessageProcessor.ts +++ b/packages/delivery-service/src/message/MessageProcessor.ts @@ -9,14 +9,13 @@ import { getConversationId, NotificationBroker, NotificationType, + spamFilter, } from '@dm3-org/dm3-lib-delivery'; -import { spamFilter } from '@dm3-org/dm3-lib-delivery'; import { DeliveryInformation, EncryptionEnvelop, getEnvelopSize, } from '@dm3-org/dm3-lib-messaging'; -import { logDebug } from '@dm3-org/dm3-lib-shared'; import { IDatabase } from '../persistence/getDatabase'; type onSubmitMessage = (socketId: string, envelop: EncryptionEnvelop) => void; @@ -55,7 +54,6 @@ export class MessageProcessor { * 4. The message must pass every {@see SpamFilterRule} the receiver declared */ public async processEnvelop(envelop: EncryptionEnvelop): Promise { - logDebug('incomingMessage'); //Checks the size of the incoming message if ( this.messageIsTooLarge( @@ -63,42 +61,54 @@ export class MessageProcessor { this.deliveryServiceProperties.sizeLimit, ) ) { + console.error('Message is too large'); throw Error('Message is too large'); } + console.debug('process incomingMessage'); //Decrypts the encryptryInformation with the KeyPair of the deliveryService - const deliveryInformation: DeliveryInformation = await decryptDeliveryInformation( envelop, this.deliveryServiceProfileKeys.encryptionKeyPair, ); - console.debug('incomingMessage', deliveryInformation); + console.debug( + 'incomingMessage delivery Information', + deliveryInformation, + ); //the delivery service has to accept any message to the receiver regardelss of the place they have choosen to host their profile. //That means no matter what name the receiver has chosen to use, the delivery service has to resolve it to the correct address //i.E if alice.eth resolves to 0x123 //and alice.gno resolves to 0x123 aswell, the ds has to accept both - const address = await this.provider.resolveName(deliveryInformation.to); + const receiverAddress = await this.provider.resolveName( + deliveryInformation.to, + ); - if (!address) { + if (!receiverAddress) { console.debug( 'unable to resolve address for ', deliveryInformation.to, ); throw Error('unable to resolve receiver address'); } - console.log(address); + + console.debug( + `resolved address for ${deliveryInformation.to} to ${receiverAddress}`, + ); const conversationId = getConversationId( - //TODO look into dbIdEnsName - await this.db.getIdEnsName(deliveryInformation.from), - await this.db.getIdEnsName(deliveryInformation.to), + //We use the receivers address as the first part of the conversationId + receiverAddress, + //We use the senders ens name as the second part of the conversationId. + //We do not use the address because the sender might have not set an resolver or the addr record might not be set. + //Its up to the client to resolve the ens name to the address + deliveryInformation.from, ); console.debug(conversationId, deliveryInformation); //Retrieves the session of the receiver - const receiverSession = await this.db.getAccount(address); + const receiverSession = await this.db.getAccount(receiverAddress); if (!receiverSession) { console.debug('unknown user ', deliveryInformation.to); throw Error('unknown session'); @@ -113,7 +123,7 @@ export class MessageProcessor { ) ) { console.debug( - `incomingMessage fro ${deliveryInformation.to} is spam`, + `incomingMessage from ${deliveryInformation.to} is spam`, ); throw Error('Message does not match spam criteria'); } From abdde76c2889b10e73f25f09f454f3b36ca7f1bf Mon Sep 17 00:00:00 2001 From: AlexNi245 Date: Mon, 29 Jul 2024 14:15:46 +0200 Subject: [PATCH 039/148] add additional logging --- packages/backend/src/persistence/session/getAccount.ts | 2 ++ packages/backend/src/persistence/session/setAccount.ts | 1 + packages/delivery-service/src/messaging.test.ts | 5 ++--- .../delivery-service/src/persistence/account/setAccount.ts | 2 ++ packages/lib/server-side/src/auth.ts | 1 + packages/lib/server-side/src/utils.ts | 2 +- 6 files changed, 9 insertions(+), 4 deletions(-) diff --git a/packages/backend/src/persistence/session/getAccount.ts b/packages/backend/src/persistence/session/getAccount.ts index d7eff1774..d599e343a 100644 --- a/packages/backend/src/persistence/session/getAccount.ts +++ b/packages/backend/src/persistence/session/getAccount.ts @@ -9,6 +9,8 @@ export function getAccount(redis: Redis) { RedisPrefix.Account + (await getIdEnsName(redis)(ensName)), ); + console.debug('get account ', ensName, session); + return session ? (JSON.parse(session) as Session & { spamFilterRules: spamFilter.SpamFilterRules; diff --git a/packages/backend/src/persistence/session/setAccount.ts b/packages/backend/src/persistence/session/setAccount.ts index 2aacd2657..2308bcdf9 100644 --- a/packages/backend/src/persistence/session/setAccount.ts +++ b/packages/backend/src/persistence/session/setAccount.ts @@ -10,6 +10,7 @@ export function setAccount(redis: Redis) { if (!isValid) { throw Error('Invalid session'); } + console.debug('set account ', ensName, session); await redis.set( RedisPrefix.Account + (await getIdEnsName(redis)(ensName)), stringify(session), diff --git a/packages/delivery-service/src/messaging.test.ts b/packages/delivery-service/src/messaging.test.ts index 1b8c4a9ad..cb458edba 100644 --- a/packages/delivery-service/src/messaging.test.ts +++ b/packages/delivery-service/src/messaging.test.ts @@ -1,4 +1,5 @@ import { Session } from '@dm3-org/dm3-lib-delivery'; +import { EncryptionEnvelop } from '@dm3-org/dm3-lib-messaging'; import { UserProfile } from '@dm3-org/dm3-lib-profile'; import { IWebSocketManager, ethersHelper } from '@dm3-org/dm3-lib-shared'; import { @@ -8,11 +9,9 @@ import { getMockDeliveryServiceProfile, mockUserProfile, } from '@dm3-org/dm3-lib-test-helper'; +import { ethers } from 'ethers'; import { Socket } from 'socket.io'; -import { testData } from '../../../test-data/encrypted-envelops.test'; import { onConnection } from './messaging'; -import { ethers } from 'ethers'; -import { EncryptionEnvelop } from '@dm3-org/dm3-lib-messaging'; const serverSecret = 'secret'; const mockWsManager: IWebSocketManager = { diff --git a/packages/delivery-service/src/persistence/account/setAccount.ts b/packages/delivery-service/src/persistence/account/setAccount.ts index 408e52191..b380c6987 100644 --- a/packages/delivery-service/src/persistence/account/setAccount.ts +++ b/packages/delivery-service/src/persistence/account/setAccount.ts @@ -18,6 +18,8 @@ export function setAccount(redis: Redis) { throw Error('Invalid address'); } + console.debug('set account ', address, session); + await redis.set( RedisPrefix.Account + ethers.utils.getAddress(address), stringify(session), diff --git a/packages/lib/server-side/src/auth.ts b/packages/lib/server-side/src/auth.ts index 8e3673b66..c5e346773 100644 --- a/packages/lib/server-side/src/auth.ts +++ b/packages/lib/server-side/src/auth.ts @@ -40,6 +40,7 @@ const createNewSessionTokenBodySchema = { export const Auth = (db: IAccountDatabase, serverSecret: string) => { const router = express.Router(); + console.debug('Auth DB ', db); //TODO remove router.use(cors()); diff --git a/packages/lib/server-side/src/utils.ts b/packages/lib/server-side/src/utils.ts index f31c6ff5c..7d7ac41dc 100644 --- a/packages/lib/server-side/src/utils.ts +++ b/packages/lib/server-side/src/utils.ts @@ -102,7 +102,7 @@ export function logError( res: Response, next: NextFunction, ) { - winston.loggers.get('default').error({ + console.error({ method: req.method, url: req.url, error: error.toString(), From 8ae70256dec9be645507ab9147d9f44df164af63 Mon Sep 17 00:00:00 2001 From: AlexNi245 Date: Mon, 29 Jul 2024 14:33:52 +0200 Subject: [PATCH 040/148] use proper db import in BE --- packages/backend/src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index cdaed9657..a3cf8df0f 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -39,7 +39,7 @@ app.use(bodyParser.json()); }); app.use('/profile', Profile(db, web3Provider, serverSecret)); app.use('/storage', Storage(db, web3Provider, serverSecret)); - app.use('/auth', Auth(db.getAccount as any, serverSecret)); + app.use('/auth', Auth(db, serverSecret)); app.use(logError); app.use(errorHandler); })(); From fbbad3978976139cc114f6ab5d4896143c204896 Mon Sep 17 00:00:00 2001 From: AlexNi245 Date: Mon, 29 Jul 2024 14:46:58 +0200 Subject: [PATCH 041/148] more logging --- packages/delivery-service/src/index.ts | 5 +++++ packages/lib/delivery/src/Session.ts | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/packages/delivery-service/src/index.ts b/packages/delivery-service/src/index.ts index 5906a0b61..f22d4e876 100644 --- a/packages/delivery-service/src/index.ts +++ b/packages/delivery-service/src/index.ts @@ -46,6 +46,11 @@ const getDbWithAddressResolvedGetAccount = ( ) => { return async (ensName: string) => { const address = await web3Provider.resolveName(ensName); + console.debug( + 'getDbWithAddressResolvedGetAccount resolved address for ens name: ', + ensName, + address, + ); if (!address) { console.info('no address found for ens name: ', ensName); return null; diff --git a/packages/lib/delivery/src/Session.ts b/packages/lib/delivery/src/Session.ts index 3e68f82cd..196f65a88 100644 --- a/packages/lib/delivery/src/Session.ts +++ b/packages/lib/delivery/src/Session.ts @@ -45,6 +45,9 @@ export async function checkToken( return false; } + console.debug('checkToken - ensName', ensName); + console.debug('checkToken - session', session); + // check jwt for validity try { // will throw if signature is invalid or exp is in the past @@ -52,6 +55,8 @@ export async function checkToken( algorithms: ['HS256'], }); + console.debug('checkToken - jwtPayload', jwtPayload); + // check if payload is well formed if ( typeof jwtPayload === 'string' || From 4c68234a2d51978a8c005457f97282324a4a4802 Mon Sep 17 00:00:00 2001 From: AlexNi245 Date: Mon, 29 Jul 2024 16:51:46 +0200 Subject: [PATCH 042/148] suse dedicated submitUserProfile function for backend --- packages/backend/src/index.ts | 2 +- .../backend/src/profile/getUserProfile.ts | 11 +++ packages/backend/src/profile/profile.ts | 82 +++++++++++++++++++ .../backend/src/profile/submitUserProfile.ts | 40 +++++++++ 4 files changed, 134 insertions(+), 1 deletion(-) create mode 100644 packages/backend/src/profile/getUserProfile.ts create mode 100644 packages/backend/src/profile/profile.ts create mode 100644 packages/backend/src/profile/submitUserProfile.ts diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index a3cf8df0f..8ccb629a2 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -1,6 +1,5 @@ import { Auth, - Profile, errorHandler, getCachedWebProvider, getServerSecret, @@ -16,6 +15,7 @@ import http from 'http'; import path from 'path'; import { getDatabase } from './persistence/getDatabase'; import Storage from './storage'; +import Profile from './profile/profile'; const app = express(); app.use(express.json({ limit: '50mb' })); diff --git a/packages/backend/src/profile/getUserProfile.ts b/packages/backend/src/profile/getUserProfile.ts new file mode 100644 index 000000000..337a5a666 --- /dev/null +++ b/packages/backend/src/profile/getUserProfile.ts @@ -0,0 +1,11 @@ +import { Session } from '@dm3-org/dm3-lib-delivery'; +import { SignedUserProfile, normalizeEnsName } from '@dm3-org/dm3-lib-profile'; + +export async function getUserProfile( + getAccount: (accountAddress: string) => Promise, + ensName: string, +): Promise { + const account = normalizeEnsName(ensName); + const session = await getAccount(account); + return session?.signedUserProfile; +} diff --git a/packages/backend/src/profile/profile.ts b/packages/backend/src/profile/profile.ts new file mode 100644 index 000000000..2a530b292 --- /dev/null +++ b/packages/backend/src/profile/profile.ts @@ -0,0 +1,82 @@ +import { normalizeEnsName, schema } from '@dm3-org/dm3-lib-profile'; +import { validateSchema } from '@dm3-org/dm3-lib-shared'; +import express from 'express'; +import { ethers } from 'ethers'; +import { Server } from 'socket.io'; +import { IDatabase } from '../persistence/getDatabase'; +import { getUserProfile } from './getUserProfile'; +import { submitUserProfile } from './submitUserProfile'; + +export default ( + db: IDatabase, + web3Provider: ethers.providers.JsonRpcProvider, + serverSecret: string, +) => { + const router = express.Router(); + + router.get('/:ensName', async (req: express.Request, res, next) => { + try { + const ensName = normalizeEnsName(req.params.ensName); + + const profile = await getUserProfile(db.getAccount, ensName); + if (profile) { + res.json(profile); + } else { + res.sendStatus(404); + } + } catch (e) { + next(e); + } + }); + + router.post('/:ensName', async (req: express.Request, res, next) => { + try { + const schemaIsValid = validateSchema( + schema.SignedUserProfile, + req.body, + ); + + if (!schemaIsValid) { + console.error({ message: 'invalid schema' }); + return res.status(400).send({ error: 'invalid schema' }); + } + const ensName = normalizeEnsName(req.params.ensName); + console.debug({ + method: 'POST', + url: req.url, + ensName, + disableSessionCheck: + process.env.DISABLE_SESSION_CHECK === 'true', + }); + + const data = await submitUserProfile( + web3Provider, + db.getAccount, + db.setAccount, + ensName, + req.body, + serverSecret, + ); + console.debug({ + message: 'POST profile', + ensName, + data, + }); + + res.json(data); + } catch (e) { + console.warn({ + message: 'POST profile', + error: JSON.stringify(e), + }); + // eslint-disable-next-line no-console + console.log('POST PROFILE ERROR', e); + res.status(400).send({ + message: `Couldn't store profile`, + error: JSON.stringify(e), + }); + } + }); + + return router; +}; diff --git a/packages/backend/src/profile/submitUserProfile.ts b/packages/backend/src/profile/submitUserProfile.ts new file mode 100644 index 000000000..e3fbadf44 --- /dev/null +++ b/packages/backend/src/profile/submitUserProfile.ts @@ -0,0 +1,40 @@ +import { Session, generateAuthJWT } from '@dm3-org/dm3-lib-delivery'; +import { + SignedUserProfile, + normalizeEnsName, + checkUserProfile, + getDefaultProfileExtension, +} from '@dm3-org/dm3-lib-profile'; +import { logDebug } from '@dm3-org/dm3-lib-shared'; +import { ethers } from 'ethers'; + +export async function submitUserProfile( + provider: ethers.providers.JsonRpcProvider, + getAccount: (accountAddress: string) => Promise, + setAccount: (accountAddress: string, session: Session) => Promise, + ensName: string, + signedUserProfile: SignedUserProfile, + serverSecret: string, +): Promise { + const account = normalizeEnsName(ensName); + + if (!(await checkUserProfile(provider, signedUserProfile, account))) { + logDebug('submitUserProfile - Signature invalid'); + throw Error('Signature invalid.'); + } + if (await getAccount(account)) { + logDebug('submitUserProfile - Profile exists already'); + throw Error('Profile exists already'); + } + const session: Session = { + account, + signedUserProfile, + token: generateAuthJWT(ensName, serverSecret), + createdAt: new Date().getTime(), + profileExtension: getDefaultProfileExtension(), + }; + logDebug({ text: 'submitUserProfile', session }); + await setAccount(account.toLocaleLowerCase(), session); + + return session.token; +} From ce2c31959bf081220cd5d57628e4ed62932fbdfe Mon Sep 17 00:00:00 2001 From: AlexNi245 Date: Mon, 29 Jul 2024 17:10:48 +0200 Subject: [PATCH 043/148] optimize imports --- packages/backend/src/profile/profile.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/backend/src/profile/profile.ts b/packages/backend/src/profile/profile.ts index 2a530b292..f06e3bdec 100644 --- a/packages/backend/src/profile/profile.ts +++ b/packages/backend/src/profile/profile.ts @@ -1,8 +1,7 @@ import { normalizeEnsName, schema } from '@dm3-org/dm3-lib-profile'; import { validateSchema } from '@dm3-org/dm3-lib-shared'; -import express from 'express'; import { ethers } from 'ethers'; -import { Server } from 'socket.io'; +import express from 'express'; import { IDatabase } from '../persistence/getDatabase'; import { getUserProfile } from './getUserProfile'; import { submitUserProfile } from './submitUserProfile'; From ccd10773e9a2879e3040aca45ed0736aa9ff8187 Mon Sep 17 00:00:00 2001 From: AlexNi245 Date: Tue, 30 Jul 2024 11:34:54 +0200 Subject: [PATCH 044/148] add missing await --- packages/delivery-service/src/delivery.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/delivery-service/src/delivery.ts b/packages/delivery-service/src/delivery.ts index 79f8675ce..570a75dd9 100644 --- a/packages/delivery-service/src/delivery.ts +++ b/packages/delivery-service/src/delivery.ts @@ -76,7 +76,7 @@ export default ( ); //Better 1000 than the previous fifty. This is a temporary solution until we implement pagination - const messages = db.getMessages(conversationId, 0, 1000); + const messages = await db.getMessages(conversationId, 0, 1000); res.json(messages); } catch (e) { next(e); From 6f66f8f5f65cbac209b3f60f2709cddbccfd7683 Mon Sep 17 00:00:00 2001 From: AlexNi245 Date: Tue, 30 Jul 2024 12:10:07 +0200 Subject: [PATCH 045/148] fix ws auth issue --- packages/lib/server-side/src/utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/lib/server-side/src/utils.ts b/packages/lib/server-side/src/utils.ts index 7d7ac41dc..8489a4d00 100644 --- a/packages/lib/server-side/src/utils.ts +++ b/packages/lib/server-side/src/utils.ts @@ -73,7 +73,7 @@ export function socketAuth( throw Error('Could not get session'); } - await db.setAccount(ensName, { + await db.setAccount(session.account, { ...session, socketId: socket.id, }); From 501939fdae809699002d243e7a90f929e3daa749 Mon Sep 17 00:00:00 2001 From: AlexNi245 Date: Tue, 30 Jul 2024 12:38:50 +0200 Subject: [PATCH 046/148] add comments --- packages/lib/server-side/src/utils.ts | 4 +++- .../src/hooks/conversation/useConversation.tsx | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/lib/server-side/src/utils.ts b/packages/lib/server-side/src/utils.ts index 8489a4d00..904de2ef3 100644 --- a/packages/lib/server-side/src/utils.ts +++ b/packages/lib/server-side/src/utils.ts @@ -72,7 +72,9 @@ export function socketAuth( if (!session) { throw Error('Could not get session'); } - + //we use session.account here as a key for setAccount here. + //We can do this because the address is used as account when the Session has been created. + //That saves a address lookup via ENS await db.setAccount(session.account, { ...session, socketId: socket.id, diff --git a/packages/messenger-widget/src/hooks/conversation/useConversation.tsx b/packages/messenger-widget/src/hooks/conversation/useConversation.tsx index 0e04be5b4..a7ce78ec3 100644 --- a/packages/messenger-widget/src/hooks/conversation/useConversation.tsx +++ b/packages/messenger-widget/src/hooks/conversation/useConversation.tsx @@ -163,6 +163,7 @@ export const useConversation = (config: DM3Configuration) => { return ( incommingMessages .map((pendingMessage: EncryptionEnvelop) => { + //TODO might be necessary to resolve alias const contactEnsName = ( pendingMessage.metadata .deliveryInformation as DeliveryInformation From f54a23133bae19722a1310c538b228f9ea0a1675 Mon Sep 17 00:00:00 2001 From: AlexNi245 Date: Tue, 30 Jul 2024 13:14:49 +0200 Subject: [PATCH 047/148] resolve addr in syncack --- packages/delivery-service/src/delivery.ts | 29 +++++++++++++++++------ 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/packages/delivery-service/src/delivery.ts b/packages/delivery-service/src/delivery.ts index 570a75dd9..53a257390 100644 --- a/packages/delivery-service/src/delivery.ts +++ b/packages/delivery-service/src/delivery.ts @@ -47,10 +47,6 @@ export default ( '/messages/:ensName/contact/:contactEnsName', async (req: express.Request, res, next) => { try { - //normalize the contact name - const contactEnsName = await normalizeEnsName( - req.params.contactEnsName, - ); //retive the address for the contact name since it is used as a key in the db const receiverAddress = await web3Provider.resolveName( req.params.ensName, @@ -69,6 +65,11 @@ export default ( }); } + //normalize the contact name + const contactEnsName = await normalizeEnsName( + req.params.contactEnsName, + ); + //The new layout resolves conversations using a conversation id [addr(reiceiver),ensName(sender)] const conversationId = getConversationId( receiverAddress, @@ -133,8 +134,22 @@ export default ( } try { - const ensName = await db.getIdEnsName(req.params.ensName); - console.log('lets go'); + const receiverAddress = await web3Provider.resolveName( + req.params.ensName, + ); + + //If the address is not found we return a 404. This should normally not happen since the receiver always is known to the delivery service + if (!receiverAddress) { + console.error( + 'receiver address not found for name ', + req.params.ensName, + ); + return res.status(404).send({ + error: + 'receiver address not found for name ' + + req.params.ensName, + }); + } await Promise.all( req.body.acknowledgments.map( @@ -143,7 +158,7 @@ export default ( ack.contactAddress, ); const conversationId = getConversationId( - ensName, + receiverAddress, contactEnsName, ); From 7fd736e4d514c2a79bf565eb4c2e64a8431c3df2 Mon Sep 17 00:00:00 2001 From: Bhupesh-MS Date: Tue, 30 Jul 2024 17:22:50 +0530 Subject: [PATCH 048/148] fixed old accounts issue --- packages/lib/delivery/src/Keys.ts | 10 ---------- packages/lib/delivery/src/UserProfile.ts | 8 ++++---- packages/lib/server-side/src/auth.ts | 7 ++++++- .../server-side/ServerSideConnector.test.ts | 16 ++++++++++++++++ .../src/hooks/server-side/ServerSideConnector.ts | 15 ++++++++++++++- .../src/hooks/server-side/useBackend.ts | 10 ++++++---- .../src/hooks/server-side/useDeliveryService.ts | 11 +++++++---- 7 files changed, 53 insertions(+), 24 deletions(-) diff --git a/packages/lib/delivery/src/Keys.ts b/packages/lib/delivery/src/Keys.ts index 5af091682..61aa32461 100644 --- a/packages/lib/delivery/src/Keys.ts +++ b/packages/lib/delivery/src/Keys.ts @@ -74,11 +74,6 @@ export async function createNewSessionToken( ): Promise { const session = await getAccount(ensName); - console.log('Signature is : ', signature); - console.log('Challenge is : ', challenge); - console.log('ENS name is : ', ensName); - console.log('Account session is :::::::::::::::: ', session); - if (!session) { throw Error('Session not found'); } @@ -89,11 +84,6 @@ export async function createNewSessionToken( algorithms: ['HS256'], }); - console.log( - 'Account challengePayload is :::::::::::::::: ', - challengePayload, - ); - // check if the payload of the challenge-jwt has the proper schema if ( typeof challengePayload == 'string' || diff --git a/packages/lib/delivery/src/UserProfile.ts b/packages/lib/delivery/src/UserProfile.ts index d323a4305..68bd3edf3 100644 --- a/packages/lib/delivery/src/UserProfile.ts +++ b/packages/lib/delivery/src/UserProfile.ts @@ -23,10 +23,10 @@ export async function submitUserProfile( logDebug('submitUserProfile - Signature invalid'); throw Error('Signature invalid.'); } - if (await getAccount(account)) { - logDebug('submitUserProfile - Profile exists already'); - throw Error('Profile exists already'); - } + // if (await getAccount(account)) { + // logDebug('submitUserProfile - Profile exists already'); + // throw Error('Profile exists already'); + // } const session: Session = { account, signedUserProfile, diff --git a/packages/lib/server-side/src/auth.ts b/packages/lib/server-side/src/auth.ts index 90865f003..c0983d923 100644 --- a/packages/lib/server-side/src/auth.ts +++ b/packages/lib/server-side/src/auth.ts @@ -96,8 +96,13 @@ export const Auth = (getAccount, serverSecret: string) => { res.json(jwt); } catch (e) { - next(e); console.error('unable to create new session token ', e); + if (e instanceof Error && e.message === 'Signature invalid') { + return res.status(400).json({ + error: e.message, + }); + } + next(e); } }); diff --git a/packages/messenger-widget/src/hooks/server-side/ServerSideConnector.test.ts b/packages/messenger-widget/src/hooks/server-side/ServerSideConnector.test.ts index 8b5daf42e..7eb119197 100644 --- a/packages/messenger-widget/src/hooks/server-side/ServerSideConnector.test.ts +++ b/packages/messenger-widget/src/hooks/server-side/ServerSideConnector.test.ts @@ -73,6 +73,7 @@ describe('Server Side Connector', () => { '.addr.dm3.eth', userAddress, profileKeys, + signedUserProfile, ); const dsResult = await connector.login(signedUserProfile); expect(dsResult.deliveryServiceToken).toBe('token'); @@ -97,6 +98,7 @@ describe('Server Side Connector', () => { '.addr.dm3.eth', userAddress, profileKeys, + signedUserProfile, ); const newDsResult = await newConnector.login(signedUserProfile); expect(newDsResult.deliveryServiceToken).toBe('token2'); @@ -142,6 +144,7 @@ describe('Server Side Connector', () => { '.addr.dm3.eth', userAddress, profileKeys, + signedUserProfile, ); const newDsResult = await newConnector.login(signedUserProfile); expect(newDsResult.deliveryServiceToken).toBe('new-token'); @@ -154,12 +157,23 @@ describe('Server Side Connector', () => { axiosMock = new MockAdapter(axios); axiosMock.onGet('http://ds1.api/test').reply(500, {}); + const signMessage = async (message: string) => + Promise.resolve(message + ' signed'); + + //We create another connector to test relogin + const signedUserProfile = await createNewSignedUserProfile( + profileKeys, + userAddress, + signMessage, + ); + const connector = new ServerSideConnectorStub( 'http://ds1.api', 'http://resolver.api', '.addr.dm3.eth', userAddress, profileKeys, + signedUserProfile, ); const testRes = await connector.testError(); expect(testRes).toBe(true); @@ -193,6 +207,7 @@ describe('Server Side Connector', () => { '.addr.dm3.eth', userAddress, profileKeys, + signedUserProfile, ); await connector.login(signedUserProfile); const testRes = await connector.testHeader(); @@ -237,6 +252,7 @@ describe('Server Side Connector', () => { '.addr.dm3.eth', userAddress, profileKeys, + signedUserProfile, ); await connector.login(signedUserProfile); const testRes = await connector.testReAuth(); diff --git a/packages/messenger-widget/src/hooks/server-side/ServerSideConnector.ts b/packages/messenger-widget/src/hooks/server-side/ServerSideConnector.ts index 738ba1e14..9cd10761c 100644 --- a/packages/messenger-widget/src/hooks/server-side/ServerSideConnector.ts +++ b/packages/messenger-widget/src/hooks/server-side/ServerSideConnector.ts @@ -17,6 +17,7 @@ export abstract class ServerSideConnector extends JwtInterceptor { private readonly addressSubdomain: string; private readonly address: string; private readonly profileKeys: ProfileKeys; + private readonly signedUserProfile: SignedUserProfile; constructor( baseUrl: string, @@ -24,6 +25,7 @@ export abstract class ServerSideConnector extends JwtInterceptor { addrEnsSubdomain: string, address: string, profileKeys: ProfileKeys, + signedUserProfile: SignedUserProfile, //Websocket is disabled per default as not every connector needs a WS connection enableWebsocket: boolean = false, ) { @@ -38,6 +40,7 @@ export abstract class ServerSideConnector extends JwtInterceptor { this.addressSubdomain = addrEnsSubdomain; this.address = address; this.profileKeys = profileKeys; + this.signedUserProfile = signedUserProfile; } public async login(signedUserProfile: SignedUserProfile) { @@ -100,11 +103,21 @@ export abstract class ServerSideConnector extends JwtInterceptor { ); //Todo move to lib - const { data: newToken } = await axios.post(url, { + const { + data: newToken, + status, + data: error, + } = await axios.post(url, { signature, challenge, }); + if (status === 400 && error === 'Signature invalid') { + const token = await this.submitUserProfile(this.signedUserProfile); + this.setAuthToken(token); + return token; + } + return newToken; } diff --git a/packages/messenger-widget/src/hooks/server-side/useBackend.ts b/packages/messenger-widget/src/hooks/server-side/useBackend.ts index 63f71e31f..b9ac78bd4 100644 --- a/packages/messenger-widget/src/hooks/server-side/useBackend.ts +++ b/packages/messenger-widget/src/hooks/server-side/useBackend.ts @@ -30,6 +30,11 @@ export const useBackend = (): IBackendConnector & { console.log('profile is not ready'); return; } + const signedUserProfile = { + profile: account?.profile!, + signature: account?.profileSignature!, + }; + //We only need to initialize the backend connector once si const beConnector = new BackendConnector( dm3Configuration.backendUrl, @@ -37,12 +42,9 @@ export const useBackend = (): IBackendConnector & { dm3Configuration.addressEnsSubdomain, ethAddress!, profileKeys!, + signedUserProfile, ); - const signedUserProfile = { - profile: account?.profile!, - signature: account?.profileSignature!, - }; await beConnector.login(signedUserProfile); setbeConnector(beConnector); setIsInitialized(true); diff --git a/packages/messenger-widget/src/hooks/server-side/useDeliveryService.ts b/packages/messenger-widget/src/hooks/server-side/useDeliveryService.ts index 6bbfd1b9f..27deb3230 100644 --- a/packages/messenger-widget/src/hooks/server-side/useDeliveryService.ts +++ b/packages/messenger-widget/src/hooks/server-side/useDeliveryService.ts @@ -42,6 +42,12 @@ export const useDeliveryService = () => { } console.log('start getting ds'); const deliveryServices = account?.profile?.deliveryServices ?? []; + + const signedUserProfile = { + profile: account?.profile!, + signature: account?.profileSignature!, + }; + //Fetch DS profile for each DS const connectors = deliveryServices @@ -68,6 +74,7 @@ export const useDeliveryService = () => { dm3Configuration.addressEnsSubdomain, ethAddress!, profileKeys!, + signedUserProfile, true, ); }); @@ -79,10 +86,6 @@ export const useDeliveryService = () => { (p): p is DeliveryServiceConnector => p !== undefined, ); - const signedUserProfile = { - profile: account?.profile!, - signature: account?.profileSignature!, - }; //Sign in connectors await Promise.all( onlyValidConnectors.map((c) => c.login(signedUserProfile)), From fd5cfa77d9ed29409974037794643a45c42d40c5 Mon Sep 17 00:00:00 2001 From: Bhupesh-MS Date: Tue, 30 Jul 2024 18:34:57 +0530 Subject: [PATCH 049/148] fixed broken test --- packages/lib/delivery/src/UserProfile.test.ts | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/packages/lib/delivery/src/UserProfile.test.ts b/packages/lib/delivery/src/UserProfile.test.ts index b601bf91c..e4d258650 100644 --- a/packages/lib/delivery/src/UserProfile.test.ts +++ b/packages/lib/delivery/src/UserProfile.test.ts @@ -60,7 +60,7 @@ describe('UserProfile', () => { expect(setAccount).not.toBeCalled(); }); - it('rejects a userProfile that already exists', async () => { + it('override a userProfile that already exists but with other nonce', async () => { const setAccount = () => Promise.resolve(); const getAccount = async (address: string) => { const session = async ( @@ -86,6 +86,15 @@ describe('UserProfile', () => { const singedUserProfile = await signProfile(emptyProfile); + await submitUserProfile( + { resolveName: () => SENDER_ADDRESS } as any, + getAccount, + setAccount, + SENDER_NAME, + singedUserProfile, + 'my-secret', + ); + await expect(async () => { await submitUserProfile( { resolveName: () => SENDER_ADDRESS } as any, @@ -93,9 +102,9 @@ describe('UserProfile', () => { setAccount, SENDER_NAME, singedUserProfile, - 'my-secret', + 'my-new-secret', ); - }).rejects.toEqual(Error('Profile exists already')); + }).resolves; }); it('stores a newly created user profile', async () => { From e185d9dcaaca27b737c985d330a611723736539a Mon Sep 17 00:00:00 2001 From: AlexNi245 Date: Tue, 30 Jul 2024 15:11:39 +0200 Subject: [PATCH 050/148] use receiver address in WebsocketManager --- .../src/message/MessageProcessor.ts | 2 +- .../src/ws/WebSocketManager.test.ts | 163 ++++++++++++++---- .../src/ws/WebSocketManager.ts | 36 ++-- 3 files changed, 152 insertions(+), 49 deletions(-) diff --git a/packages/delivery-service/src/message/MessageProcessor.ts b/packages/delivery-service/src/message/MessageProcessor.ts index 71e1172ac..3b53383df 100644 --- a/packages/delivery-service/src/message/MessageProcessor.ts +++ b/packages/delivery-service/src/message/MessageProcessor.ts @@ -155,7 +155,7 @@ export class MessageProcessor { } //If there is currently a webSocket connection open to the receiver, the message will be directly send. - if (await this.webSocketManager.isConnected(deliveryInformation.to)) { + if (await this.webSocketManager.isConnected(receiverAddress)) { //Client is already connect to the delivery service and the message can be dispatched //TODO MOVE send method to the WebSocketManager this.onSubmitMessage( diff --git a/packages/delivery-service/src/ws/WebSocketManager.test.ts b/packages/delivery-service/src/ws/WebSocketManager.test.ts index aceed7abf..4b53a2fcc 100644 --- a/packages/delivery-service/src/ws/WebSocketManager.test.ts +++ b/packages/delivery-service/src/ws/WebSocketManager.test.ts @@ -3,6 +3,14 @@ import { Server as SocketIoServer } from 'socket.io'; import { createServer, Server as HttpServerType } from 'http'; import { AUTHORIZED, UNAUTHORIZED, WebSocketManager } from './WebSocketManager'; import { generateAuthJWT } from '@dm3-org/dm3-lib-delivery'; +import { + getMockDeliveryServiceProfile, + MockDeliveryServiceProfile, + MockedUserProfile, + mockUserProfile, +} from '@dm3-org/dm3-lib-test-helper'; +import { ethers } from 'ethers'; +import account from '../persistence/account'; const serverSecret = 'verySecretAndImportantServerSecret'; describe('WebSocketManager', () => { @@ -12,6 +20,13 @@ describe('WebSocketManager', () => { let httpServer; let socketIoServer; + let sender: MockedUserProfile; + let receiver: MockedUserProfile; + let receiverOnGno: MockedUserProfile; + let rando: MockedUserProfile; + + let ds: MockDeliveryServiceProfile; + beforeEach(async () => { httpServer = await mockHttpServer(4060); socketIoServer = new SocketIoServer(httpServer, { @@ -22,6 +37,31 @@ describe('WebSocketManager', () => { optionsSuccessStatus: 204, }, }); + + //The receiver might use the same address for different networks. Hence we keep the wallet separate + + const receiverWallet = ethers.Wallet.createRandom(); + sender = await mockUserProfile( + ethers.Wallet.createRandom(), + 'bob.eth', + ['http://localhost:3000'], + ); + receiver = await mockUserProfile(receiverWallet, 'alice.eth', [ + 'http://localhost:3000', + ]); + receiverOnGno = await mockUserProfile(receiverWallet, 'alice.gno', [ + 'http://localhost:3000', + ]); + rando = await mockUserProfile( + ethers.Wallet.createRandom(), + 'rando.eth', + ['http://localhost:3000'], + ); + + ds = await getMockDeliveryServiceProfile( + ethers.Wallet.createRandom(), + 'http://localhost:3000', + ); }); afterEach(() => { @@ -134,12 +174,13 @@ describe('WebSocketManager', () => { describe('isConnected', () => { it('returns true if name has one session', async () => { const mockedWeb3Provider = { - resolveName: (_: string) => Promise.resolve('0x123'), + resolveName: (_: string) => Promise.resolve(receiver.address), } as any; const mockedDatabase = { getAccount: () => Promise.resolve({ + account: receiver.address, token: 'old token that is not used anymore', createdAt: new Date().getTime(), }), @@ -155,9 +196,12 @@ describe('WebSocketManager', () => { client0 = await Client('http://localhost:4060', { auth: { account: { - ensName: 'bob.eth', + ensName: receiver.account.ensName, }, - token: generateAuthJWT('bob.eth', serverSecret), + token: generateAuthJWT( + receiver.account.ensName, + serverSecret, + ), }, }); @@ -173,17 +217,18 @@ describe('WebSocketManager', () => { ]); expect(socket0IsConnected).toBe(true); - const isConnected = await wsManager.isConnected('bob.eth'); + const isConnected = await wsManager.isConnected(receiver.address); expect(isConnected).toBe(true); }); it('returns true if name has at least one session', async () => { const mockedWeb3Provider = { - resolveName: (_: string) => Promise.resolve('0x123'), + resolveName: (_: string) => Promise.resolve(receiver.address), } as any; const mockedDatabase = { getAccount: () => Promise.resolve({ + account: receiver.address, token: 'old token that is not used anymore', createdAt: new Date().getTime(), }), @@ -199,17 +244,23 @@ describe('WebSocketManager', () => { client0 = await Client('http://localhost:4060', { auth: { account: { - ensName: 'bob.eth', + ensName: receiver.account.ensName, }, - token: generateAuthJWT('bob.eth', serverSecret), + token: generateAuthJWT( + receiver.account.ensName, + serverSecret, + ), }, }); client1 = await Client('http://localhost:4060', { auth: { account: { - ensName: 'bob.eth', + ensName: receiver.account.ensName, }, - token: generateAuthJWT('bob.eth', serverSecret), + token: generateAuthJWT( + receiver.account.ensName, + serverSecret, + ), }, }); @@ -234,22 +285,23 @@ describe('WebSocketManager', () => { expect(socket0IsConnected).toBe(true); expect(socket1IsConnected).toBe(true); - let isConnected = await wsManager.isConnected('bob.eth'); + let isConnected = await wsManager.isConnected(receiver.address); expect(isConnected).toBe(true); client0.close(); await wait(500); - isConnected = await wsManager.isConnected('bob.eth'); + isConnected = await wsManager.isConnected(receiver.address); expect(isConnected).toBe(true); }); it('returns false if name is unknown', async () => { const mockedWeb3Provider = { - resolveName: (_: string) => Promise.resolve('0x123'), + resolveName: (_: string) => Promise.resolve(receiver.address), } as any; const mockedDatabase = { getAccount: () => Promise.resolve({ + account: receiver.address, token: 'old token that is not used anymore', createdAt: new Date().getTime(), }), @@ -265,9 +317,12 @@ describe('WebSocketManager', () => { client0 = await Client('http://localhost:4060', { auth: { account: { - ensName: 'bob.eth', + ensName: receiver.account.ensName, }, - token: generateAuthJWT('bob.eth', serverSecret), + token: generateAuthJWT( + receiver.account.ensName, + serverSecret, + ), }, }); @@ -283,20 +338,42 @@ describe('WebSocketManager', () => { ]); expect(socket0IsConnected).toBe(true); - const isConnected = await wsManager.isConnected('alice.eth'); + const isConnected = await wsManager.isConnected(rando.address); expect(isConnected).toBe(false); }); it('keeps track of different independent sessions', async () => { const mockedWeb3Provider = { - resolveName: (_: string) => Promise.resolve('0x123'), + resolveName: (_: string) => { + if (_ === receiver.account.ensName) { + return Promise.resolve(receiver.address); + } + if (_ === sender.account.ensName) { + return Promise.resolve(sender.address); + } + if (_ === rando.account.ensName) { + return Promise.resolve(rando.address); + } + }, } as any; const mockedDatabase = { - getAccount: () => - Promise.resolve({ - token: 'old token that is not used anymore', - createdAt: new Date().getTime(), - }), + getAccount: (name: string) => { + if (name === receiver.account.ensName) { + return Promise.resolve({ + account: receiver.address, + }); + } + if (name === sender.account.ensName) { + return Promise.resolve({ + account: sender.address, + }); + } + if (name === rando.account.ensName) { + return Promise.resolve({ + account: rando.address, + }); + } + }, } as any; const wsManager = new WebSocketManager( @@ -309,27 +386,33 @@ describe('WebSocketManager', () => { client0 = await Client('http://localhost:4060', { auth: { account: { - ensName: 'bob.eth', + ensName: receiver.account.ensName, }, - token: generateAuthJWT('bob.eth', serverSecret), + token: generateAuthJWT( + receiver.account.ensName, + serverSecret, + ), }, }); client1 = await Client('http://localhost:4060', { auth: { account: { - ensName: 'alice.eth', + ensName: sender.account.ensName, }, - token: generateAuthJWT('alice.eth', serverSecret), + token: generateAuthJWT( + sender.account.ensName, + serverSecret, + ), }, }); client2 = await Client('http://localhost:4060', { auth: { account: { - ensName: 'vitalik.eth', + ensName: rando.account.ensName, }, - token: generateAuthJWT('vitalik.eth', serverSecret), + token: generateAuthJWT(rando.account.ensName, serverSecret), }, }); @@ -365,24 +448,25 @@ describe('WebSocketManager', () => { expect(socket1IsConnected).toBe(true); expect(socket2IsConnected).toBe(true); - expect(await wsManager.isConnected('bob.eth')).toBe(true); - expect(await wsManager.isConnected('alice.eth')).toBe(true); - expect(await wsManager.isConnected('vitalik.eth')).toBe(true); + expect(await wsManager.isConnected(receiver.address)).toBe(true); + expect(await wsManager.isConnected(sender.address)).toBe(true); + expect(await wsManager.isConnected(rando.address)).toBe(true); client1.close(); await wait(500); - expect(await wsManager.isConnected('bob.eth')).toBe(true); - expect(await wsManager.isConnected('alice.eth')).toBe(false); - expect(await wsManager.isConnected('vitalik.eth')).toBe(true); + expect(await wsManager.isConnected(receiver.address)).toBe(true); + expect(await wsManager.isConnected(sender.address)).toBe(false); + expect(await wsManager.isConnected(rando.address)).toBe(true); }); it('returns false after the user has closed all its connections', async () => { const mockedWeb3Provider = { - resolveName: (_: string) => Promise.resolve('0x123'), + resolveName: (_: string) => Promise.resolve(receiver.address), } as any; const mockedDatabase = { getAccount: () => Promise.resolve({ + account: receiver.address, token: 'old token that is not used anymore', createdAt: new Date().getTime(), }), @@ -398,9 +482,12 @@ describe('WebSocketManager', () => { client0 = await Client('http://localhost:4060', { auth: { account: { - ensName: 'bob.eth', + ensName: receiver.account.ensName, }, - token: generateAuthJWT('bob.eth', serverSecret), + token: generateAuthJWT( + receiver.account.ensName, + serverSecret, + ), }, }); @@ -416,12 +503,12 @@ describe('WebSocketManager', () => { ]); expect(socket0IsConnected).toBe(true); - let isConnected = await wsManager.isConnected('bob.eth'); + let isConnected = await wsManager.isConnected(receiver.address); expect(isConnected).toBe(true); client0.close(); await wait(500); - isConnected = await wsManager.isConnected('bob.eth'); + isConnected = await wsManager.isConnected(receiver.address); expect(isConnected).toBe(false); }); }); diff --git a/packages/delivery-service/src/ws/WebSocketManager.ts b/packages/delivery-service/src/ws/WebSocketManager.ts index 948e8888f..eceee4dee 100644 --- a/packages/delivery-service/src/ws/WebSocketManager.ts +++ b/packages/delivery-service/src/ws/WebSocketManager.ts @@ -39,11 +39,12 @@ export class WebSocketManager implements IWebSocketManager { /** * Checks if a user is connected. - * @param {string} ensName - The ENS name of the user. + * @param {string} address - The address of the user. * @returns {boolean} - Returns true if the user is connected with at least one socket, false otherwise. */ - public async isConnected(ensName: string) { - const connections = this.connections.get(ensName); + public async isConnected(address: string) { + const _address = ethers.utils.getAddress(address); + const connections = this.connections.get(_address); return !!(connections && connections.length > 0); } /** @@ -74,14 +75,17 @@ export class WebSocketManager implements IWebSocketManager { return; } //Get the old connections and add the new one - const oldConnections = this.connections.get(ensName) || []; - this.connections.set(ensName, [...oldConnections, connection]); + const oldConnections = this.connections.get(session.account) || []; + this.connections.set(session.account, [ + ...oldConnections, + connection, + ]); //Send the authorized event connection.emit(AUTHORIZED); - console.log('connection established for ', ensName); + console.log('connection established for ', session.account); //When the socket disconnects we want them no longer in our connections List connection.on('disconnect', () => { - console.log('connection closed for ', ensName); + console.log('connection closed for ', session.account); this.removeConnection(connection); }); } catch (e) { @@ -96,17 +100,29 @@ export class WebSocketManager implements IWebSocketManager { * @private * @param {Socket} connection - The socket connection instance. */ - private removeConnection(connection: Socket) { + private async removeConnection(connection: Socket) { const ensName = normalizeEnsName( connection.handshake.auth.account.ensName, ); - const connections = this.connections.get(ensName); + + //the resolved address for the name + const address = await this.web3Provider.resolveName(ensName); + if (!address) { + return; + } + //the connections the address has created previously + const connections = this.connections.get(address); + + //if there are no known connections we return if (!connections) { return; } + //we find the connection that has disconnected and remove it from the list const newConnections = connections.filter( (c) => c.id !== connection.id, ); - this.connections.set(ensName, newConnections); + + //we assign the list conaining all others connections an address might have to the list without the disconnected connection + this.connections.set(address, newConnections); } } From af1c6c46b46981c0830e9d41746a46d062f9b91e Mon Sep 17 00:00:00 2001 From: Bhupesh-MS Date: Tue, 30 Jul 2024 19:40:51 +0530 Subject: [PATCH 051/148] modified error handling --- packages/lib/delivery/src/UserProfile.ts | 5 +- packages/lib/server-side/src/auth.ts | 12 ++-- .../hooks/server-side/ServerSideConnector.ts | 59 +++++++++++-------- 3 files changed, 40 insertions(+), 36 deletions(-) diff --git a/packages/lib/delivery/src/UserProfile.ts b/packages/lib/delivery/src/UserProfile.ts index 68bd3edf3..ea5aea2c4 100644 --- a/packages/lib/delivery/src/UserProfile.ts +++ b/packages/lib/delivery/src/UserProfile.ts @@ -23,10 +23,7 @@ export async function submitUserProfile( logDebug('submitUserProfile - Signature invalid'); throw Error('Signature invalid.'); } - // if (await getAccount(account)) { - // logDebug('submitUserProfile - Profile exists already'); - // throw Error('Profile exists already'); - // } + const session: Session = { account, signedUserProfile, diff --git a/packages/lib/server-side/src/auth.ts b/packages/lib/server-side/src/auth.ts index c0983d923..9114f02c4 100644 --- a/packages/lib/server-side/src/auth.ts +++ b/packages/lib/server-side/src/auth.ts @@ -97,12 +97,12 @@ export const Auth = (getAccount, serverSecret: string) => { res.json(jwt); } catch (e) { console.error('unable to create new session token ', e); - if (e instanceof Error && e.message === 'Signature invalid') { - return res.status(400).json({ - error: e.message, - }); - } - next(e); + return res.status(400).json({ + error: + e instanceof Error + ? e.message + : 'Failed to create new session token', + }); } }); diff --git a/packages/messenger-widget/src/hooks/server-side/ServerSideConnector.ts b/packages/messenger-widget/src/hooks/server-side/ServerSideConnector.ts index 9cd10761c..c9744fdb0 100644 --- a/packages/messenger-widget/src/hooks/server-side/ServerSideConnector.ts +++ b/packages/messenger-widget/src/hooks/server-side/ServerSideConnector.ts @@ -7,6 +7,7 @@ import { import axios from 'axios'; import { claimAddress } from '../../adapters/offchainResolverApi'; import { JwtInterceptor } from './JwtInterceptor'; +import { AxiosError, AxiosResponse } from 'axios'; //Interface to support different kinds of signers export type SignMessageFn = (message: string) => Promise; @@ -92,33 +93,39 @@ export abstract class ServerSideConnector extends JwtInterceptor { } private async reAuth() { - //TODO check if we need alias subdomain - const url = `${this.baseUrl}/auth/${normalizeEnsName(this.ensName)}`; - - const { data: challenge } = await axios.get(url); - - const signature = await sign( - this.profileKeys.signingKeyPair.privateKey, - challenge, - ); - - //Todo move to lib - const { - data: newToken, - status, - data: error, - } = await axios.post(url, { - signature, - challenge, - }); - - if (status === 400 && error === 'Signature invalid') { - const token = await this.submitUserProfile(this.signedUserProfile); - this.setAuthToken(token); - return token; + try { + //TODO check if we need alias subdomain + const url = `${this.baseUrl}/auth/${normalizeEnsName( + this.ensName, + )}`; + + const { data: challenge } = await axios.get(url); + + const signature = await sign( + this.profileKeys.signingKeyPair.privateKey, + challenge, + ); + + //Todo move to lib + const { + data: newToken, + status, + data: error, + } = await axios.post(url, { + signature, + challenge, + }); + + return newToken; + } catch (err) { + if (err.response.data.error === 'Signature invalid') { + const token = await this.submitUserProfile( + this.signedUserProfile, + ); + this.setAuthToken(token); + return token; + } } - - return newToken; } private async profileExistsOnDeliveryService() { From e2be0fa287758fe0953316da9671cae31f551b08 Mon Sep 17 00:00:00 2001 From: AlexNi245 Date: Tue, 30 Jul 2024 16:22:43 +0200 Subject: [PATCH 052/148] add logging for incoming conversations --- packages/delivery-service/src/delivery.ts | 1 + .../src/persistence/messages/createMessage.ts | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/packages/delivery-service/src/delivery.ts b/packages/delivery-service/src/delivery.ts index 53a257390..04bfbb99d 100644 --- a/packages/delivery-service/src/delivery.ts +++ b/packages/delivery-service/src/delivery.ts @@ -103,6 +103,7 @@ export default ( //@ts-ignore async (req: express.Request, res, next) => { try { + console.debug('get incoming messages for ', req.params.ensName); //TODO use address const incomingMessages = await db.getIncomingMessages( req.params.ensName, diff --git a/packages/delivery-service/src/persistence/messages/createMessage.ts b/packages/delivery-service/src/persistence/messages/createMessage.ts index 53c60ae4a..71276e75b 100644 --- a/packages/delivery-service/src/persistence/messages/createMessage.ts +++ b/packages/delivery-service/src/persistence/messages/createMessage.ts @@ -39,6 +39,11 @@ export function createMessage(redis: Redis) { const encryptedDeliverInformation = envelop.metadata .deliveryInformation as DeliveryInformation; + console.debug( + 'store incoming conversation for', + encryptedDeliverInformation.to, + ); + await redis.zAdd( RedisPrefix.IncomingConversations + encryptedDeliverInformation.to, { From 5ae3ce8ea65e4f52d01baeb30593ad62c3535baf Mon Sep 17 00:00:00 2001 From: Bhupesh-MS Date: Tue, 30 Jul 2024 20:04:04 +0530 Subject: [PATCH 053/148] added comment --- .../src/hooks/server-side/ServerSideConnector.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/packages/messenger-widget/src/hooks/server-side/ServerSideConnector.ts b/packages/messenger-widget/src/hooks/server-side/ServerSideConnector.ts index c9744fdb0..0c30b6bfb 100644 --- a/packages/messenger-widget/src/hooks/server-side/ServerSideConnector.ts +++ b/packages/messenger-widget/src/hooks/server-side/ServerSideConnector.ts @@ -107,17 +107,16 @@ export abstract class ServerSideConnector extends JwtInterceptor { ); //Todo move to lib - const { - data: newToken, - status, - data: error, - } = await axios.post(url, { + const { data: newToken } = await axios.post(url, { signature, challenge, }); return newToken; } catch (err) { + // It handles the case when client provides new nonce value. + // The old nonce profile throws error of "Invalid Signature", so old profile + // is overiden by the new profile with new nonce provided by the client if (err.response.data.error === 'Signature invalid') { const token = await this.submitUserProfile( this.signedUserProfile, From c41bc2d8276654472a6e7d9db5fecc576b625227 Mon Sep 17 00:00:00 2001 From: AlexNi245 Date: Tue, 30 Jul 2024 17:11:09 +0200 Subject: [PATCH 054/148] only ack messages that could be decrypted --- .../handleMessagesFromDeliveryService.ts | 69 ++++++---- .../src/hooks/messages/useMessage.test.tsx | 124 ++++++++++++++++++ .../src/hooks/messages/useMessage.tsx | 5 +- 3 files changed, 169 insertions(+), 29 deletions(-) diff --git a/packages/messenger-widget/src/hooks/messages/sources/handleMessagesFromDeliveryService.ts b/packages/messenger-widget/src/hooks/messages/sources/handleMessagesFromDeliveryService.ts index 2b8bf7477..66e288f54 100644 --- a/packages/messenger-widget/src/hooks/messages/sources/handleMessagesFromDeliveryService.ts +++ b/packages/messenger-widget/src/hooks/messages/sources/handleMessagesFromDeliveryService.ts @@ -30,40 +30,53 @@ export const handleMessagesFromDeliveryService = async ( const incommingMessages: MessageModel[] = await Promise.all( encryptedIncommingMessages.map( - async (envelop: EncryptionEnvelop): Promise => { - const decryptedEnvelop: Envelop = { - message: JSON.parse( - await decryptAsymmetric( - profileKeys?.encryptionKeyPair!, - JSON.parse(envelop.message), + async ( + envelop: EncryptionEnvelop, + ): Promise => { + try { + const decryptedEnvelop: Envelop = { + message: JSON.parse( + await decryptAsymmetric( + profileKeys?.encryptionKeyPair!, + JSON.parse(envelop.message), + ), ), - ), - postmark: JSON.parse( - await decryptAsymmetric( - profileKeys?.encryptionKeyPair!, - JSON.parse(envelop.postmark!), + postmark: JSON.parse( + await decryptAsymmetric( + profileKeys?.encryptionKeyPair!, + JSON.parse(envelop.postmark!), + ), ), - ), - metadata: envelop.metadata, - }; - return { - envelop: decryptedEnvelop, - //Messages from the delivery service are already send by the sender - messageState: MessageState.Send, - reactions: [], - //The source of the message is the delivery service - source: MessageSource.DeliveryService, - }; + metadata: envelop.metadata, + }; + return { + envelop: decryptedEnvelop, + //Messages from the delivery service are already send by the sender + messageState: MessageState.Send, + reactions: [], + //The source of the message is the delivery service + source: MessageSource.DeliveryService, + }; + } catch (e) { + console.warn('unable to decrypt message ', e); + //We return null if the message could not be decrypted. + //This way we can filter out the message later an not acknowledge it, to keep it on the DS. + //Another client might be able to decrypt it. + return null; + } }, ), ); - const messagesSortedASC = incommingMessages.sort((a, b) => { - return ( - a.envelop.postmark?.incommingTimestamp! - - b.envelop.postmark?.incommingTimestamp! - ); - }); + const messagesSortedASC = incommingMessages + //Filter out messages that could not be decrypted to only process and acknowledge the ones that could be decrypted + .filter((message) => message !== null) + .sort((a, b) => { + return ( + a.envelop.postmark?.incommingTimestamp! - + b.envelop.postmark?.incommingTimestamp! + ); + }); //If the DS has received messages from that contact we store them, and add the contact to conversation list aswell if (messagesSortedASC.length > 0) { //If the contact is not already in the conversation list then add it diff --git a/packages/messenger-widget/src/hooks/messages/useMessage.test.tsx b/packages/messenger-widget/src/hooks/messages/useMessage.test.tsx index 49bd9b2c1..974b49104 100644 --- a/packages/messenger-widget/src/hooks/messages/useMessage.test.tsx +++ b/packages/messenger-widget/src/hooks/messages/useMessage.test.tsx @@ -494,6 +494,8 @@ describe('useMessage hook test cases', () => { describe('initialize message', () => { let sender: MockedUserProfile; let receiver: MockedUserProfile; + let rando: MockedUserProfile; + let ds: any; beforeEach(async () => { @@ -507,6 +509,11 @@ describe('useMessage hook test cases', () => { 'bob.eth', ['https://example.com'], ); + rando = await mockUserProfile( + ethers.Wallet.createRandom(), + 'rando.eth', + ['ds1.eth'], + ); ds = await getMockDeliveryServiceProfile( ethers.Wallet.createRandom(), 'https://example.com', @@ -681,6 +688,123 @@ describe('useMessage hook test cases', () => { expect(result.current.contactIsLoading('alice.eth')).toBe(false); expect(result.current.messages['alice.eth'].length).toBe(3); }); + it('should only ackknowledge messages the client was able to decrypt ', async () => { + const messageFactory = MockMessageFactory(sender, receiver, ds); + const message1 = await messageFactory.createEncryptedEnvelop( + 'hello dm3', + ); + const message2 = await messageFactory.createEncryptedEnvelop( + 'hello world', + ); + const message3 = await messageFactory.createEncryptedEnvelop( + 'hello bob', + ); + //use this to create a message that the client can't decrypt + const foreignMessageFactory = MockMessageFactory(sender, rando, ds); + + const foreignMessage = + await foreignMessageFactory.createEncryptedEnvelop( + 'this message is not encryptable', + ); + + const syncAcknowledgmentMock = jest.fn(); + + const storageContext = getMockedStorageContext({ + editMessageBatchAsync: jest.fn(), + storeMessageBatch: jest.fn(), + storeMessage: jest.fn(), + getMessages: jest.fn().mockResolvedValue([]), + getNumberOfMessages: jest.fn().mockResolvedValue(0), + }); + const conversationContext = getMockedConversationContext({ + selectedContact: getEmptyContact( + 'max.eth', + undefined, + false, + 0, + ), + contacts: [getEmptyContact('alice.eth', undefined, false, 0)], + }); + const deliveryServiceContext = getMockedDeliveryServiceContext({ + onNewMessage: (cb: Function) => { + console.log('on new message'); + }, + fetchNewMessages: jest + .fn() + .mockResolvedValue([ + message1, + foreignMessage, + message2, + message3, + ]), + syncAcknowledgment: syncAcknowledgmentMock, + removeOnNewMessageListener: jest.fn(), + }); + const authContext = getMockedAuthContext({ + profileKeys: receiver.profileKeys, + account: { + ensName: 'bob.eth', + profile: { + deliveryServices: ['ds.eth'], + publicEncryptionKey: '', + publicSigningKey: '', + }, + }, + }); + const tldContext = getMockedTldContext({}); + + const wrapper = ({ children }: { children: any }) => ( + <> + + + + + + {children} + + + + + + + ); + + const { result } = renderHook(() => useMessage(), { + wrapper, + }); + //Wait until bobs messages have been initialized + await waitFor( + () => + result.current.contactIsLoading('alice.eth') === false && + result.current.messages['alice.eth'].length > 0, + ); + + expect(result.current.contactIsLoading('alice.eth')).toBe(false); + expect(result.current.messages['alice.eth'].length).toBe(3); + + expect(syncAcknowledgmentMock).toBeCalledTimes(1); + expect(syncAcknowledgmentMock).toBeCalledWith( + receiver.account.ensName, + [ + { + contactAddress: sender.account.ensName, + messageHash: message1.metadata.encryptedMessageHash, + }, + { + contactAddress: sender.account.ensName, + messageHash: message2.metadata.encryptedMessageHash, + }, + { + contactAddress: sender.account.ensName, + messageHash: message3.metadata.encryptedMessageHash, + }, + ], + ); + }); }); describe('message pagination', () => { let sender: MockedUserProfile; diff --git a/packages/messenger-widget/src/hooks/messages/useMessage.tsx b/packages/messenger-widget/src/hooks/messages/useMessage.tsx index 90d5c26ab..55a689015 100644 --- a/packages/messenger-widget/src/hooks/messages/useMessage.tsx +++ b/packages/messenger-widget/src/hooks/messages/useMessage.tsx @@ -273,7 +273,10 @@ export const useMessage = () => { encryptAsymmetric(publicKey, msg), { from: account!, - to: recipient!.contactDetails.account, + to: { + ...recipient!.contactDetails.account, + ensName: recipient.name, + }, deliverServiceProfile, keys: profileKeys!, }, From 5b095af872d85c384641b93fa800db8d83540f7d Mon Sep 17 00:00:00 2001 From: AlexNi245 Date: Tue, 30 Jul 2024 18:52:19 +0200 Subject: [PATCH 055/148] adjust lib delivery test --- packages/lib/delivery/src/UserProfile.test.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/lib/delivery/src/UserProfile.test.ts b/packages/lib/delivery/src/UserProfile.test.ts index 54dd3963a..33617d077 100644 --- a/packages/lib/delivery/src/UserProfile.test.ts +++ b/packages/lib/delivery/src/UserProfile.test.ts @@ -86,10 +86,9 @@ describe('UserProfile', () => { const singedUserProfile = await signProfile(emptyProfile); await submitUserProfile( - { resolveName: () => SENDER_ADDRESS } as any, getAccount, setAccount, - SENDER_NAME, + SENDER_ADDRESS, singedUserProfile, 'my-secret', ); From 9732242f7a039c178471df0ee3db3dc4e244916c Mon Sep 17 00:00:00 2001 From: AlexNi245 Date: Wed, 31 Jul 2024 09:21:25 +0200 Subject: [PATCH 056/148] clean up --- packages/backend/src/persistence/session/getAccount.ts | 1 - packages/delivery-service/src/index.ts | 4 ++++ .../delivery-service/src/message/MessageProcessor.ts | 9 ++------- packages/delivery-service/src/ws/WebSocketManager.ts | 1 + packages/delivery-service/tsconfig.json | 2 +- packages/lib/delivery/src/spam-filter/index.ts | 2 +- packages/lib/server-side/src/auth.ts | 1 - 7 files changed, 9 insertions(+), 11 deletions(-) diff --git a/packages/backend/src/persistence/session/getAccount.ts b/packages/backend/src/persistence/session/getAccount.ts index d599e343a..d83694907 100644 --- a/packages/backend/src/persistence/session/getAccount.ts +++ b/packages/backend/src/persistence/session/getAccount.ts @@ -4,7 +4,6 @@ import { getIdEnsName } from '../getIdEnsName'; export function getAccount(redis: Redis) { return async (ensName: string) => { - //TODO use addr let session = await redis.get( RedisPrefix.Account + (await getIdEnsName(redis)(ensName)), ); diff --git a/packages/delivery-service/src/index.ts b/packages/delivery-service/src/index.ts index f22d4e876..1fd9f3b89 100644 --- a/packages/delivery-service/src/index.ts +++ b/packages/delivery-service/src/index.ts @@ -36,6 +36,10 @@ app.use(express.urlencoded({ limit: '50mb' })); const server = http.createServer(app); +//On the delivery-service side the address functions as an identifier for the account. +// The reason for that is that the DS should accept all messages directet to the address. Regardless of its ENS name. +// To use as much shared code as possible from lib/server-side the address is resolved to the account before each database call. +//using this wrapper around the IDatabase const getDbWithAddressResolvedGetAccount = ( db: IDatabase, web3Provider: ethers.providers.JsonRpcProvider, diff --git a/packages/delivery-service/src/message/MessageProcessor.ts b/packages/delivery-service/src/message/MessageProcessor.ts index 3b53383df..cfab60270 100644 --- a/packages/delivery-service/src/message/MessageProcessor.ts +++ b/packages/delivery-service/src/message/MessageProcessor.ts @@ -146,13 +146,8 @@ export class MessageProcessor { ), ), }; - - if (process.env.DISABLE_MSG_BUFFER !== 'true') { - console.debug('storeNewMessage', conversationId); - await this.db.createMessage(conversationId, envelopWithPostmark); - } else { - console.debug('skip storeNewMessage', conversationId); - } + console.debug('storeNewMessage', conversationId); + await this.db.createMessage(conversationId, envelopWithPostmark); //If there is currently a webSocket connection open to the receiver, the message will be directly send. if (await this.webSocketManager.isConnected(receiverAddress)) { diff --git a/packages/delivery-service/src/ws/WebSocketManager.ts b/packages/delivery-service/src/ws/WebSocketManager.ts index eceee4dee..d7ef1aac6 100644 --- a/packages/delivery-service/src/ws/WebSocketManager.ts +++ b/packages/delivery-service/src/ws/WebSocketManager.ts @@ -108,6 +108,7 @@ export class WebSocketManager implements IWebSocketManager { //the resolved address for the name const address = await this.web3Provider.resolveName(ensName); if (!address) { + console.debug('WS manager,could not resolve address for ', ensName); return; } //the connections the address has created previously diff --git a/packages/delivery-service/tsconfig.json b/packages/delivery-service/tsconfig.json index 866b7a98f..0c91eb258 100644 --- a/packages/delivery-service/tsconfig.json +++ b/packages/delivery-service/tsconfig.json @@ -16,6 +16,6 @@ "outDir": "dist", "sourceMap": true }, - "include": ["src", "../lib/delivery/src/spam-filter"], + "include": ["src",], "exclude": ["src/**/*.test.ts"] } diff --git a/packages/lib/delivery/src/spam-filter/index.ts b/packages/lib/delivery/src/spam-filter/index.ts index 17e14fd9b..1a5fd38e1 100644 --- a/packages/lib/delivery/src/spam-filter/index.ts +++ b/packages/lib/delivery/src/spam-filter/index.ts @@ -5,7 +5,7 @@ import { nonceFilterFactory } from './filter/nonceFilter/NonceFilter'; import { SpamFilter, SpamFilterFactory } from './filter/SpamFilter'; import { tokenBalanceFilterFactory } from './filter/tokenBalanceFilter/TokenBalanceFilter'; import { SpamFilterRules } from './SpamFilterRules'; -import { Session } from '@dm3-org/dm3-lib-delivery/src/Session'; +import { Session } from '@dm3-org/dm3-lib-delivery'; export type { SpamFilterRules }; diff --git a/packages/lib/server-side/src/auth.ts b/packages/lib/server-side/src/auth.ts index e619a4011..b796af0e7 100644 --- a/packages/lib/server-side/src/auth.ts +++ b/packages/lib/server-side/src/auth.ts @@ -40,7 +40,6 @@ const createNewSessionTokenBodySchema = { export const Auth = (db: IAccountDatabase, serverSecret: string) => { const router = express.Router(); - console.debug('Auth DB ', db); //TODO remove router.use(cors()); From fce385d82c4912c483b1f3c0f9fc2aa8e9084975 Mon Sep 17 00:00:00 2001 From: AlexNi245 Date: Wed, 31 Jul 2024 09:31:04 +0200 Subject: [PATCH 057/148] fix broken Session import --- packages/lib/delivery/src/spam-filter/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/lib/delivery/src/spam-filter/index.ts b/packages/lib/delivery/src/spam-filter/index.ts index 1a5fd38e1..32f890fe1 100644 --- a/packages/lib/delivery/src/spam-filter/index.ts +++ b/packages/lib/delivery/src/spam-filter/index.ts @@ -5,7 +5,7 @@ import { nonceFilterFactory } from './filter/nonceFilter/NonceFilter'; import { SpamFilter, SpamFilterFactory } from './filter/SpamFilter'; import { tokenBalanceFilterFactory } from './filter/tokenBalanceFilter/TokenBalanceFilter'; import { SpamFilterRules } from './SpamFilterRules'; -import { Session } from '@dm3-org/dm3-lib-delivery'; +import { Session } from '../Session'; export type { SpamFilterRules }; From 5b8992f9e031f99ae33dad8d18ba26e482f90ec4 Mon Sep 17 00:00:00 2001 From: AlexNi245 Date: Wed, 31 Jul 2024 10:01:44 +0200 Subject: [PATCH 058/148] dont show indicators on received messages --- .../src/components/Message/MessageDetail.tsx | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/packages/messenger-widget/src/components/Message/MessageDetail.tsx b/packages/messenger-widget/src/components/Message/MessageDetail.tsx index 9b6d75252..7a36598e7 100644 --- a/packages/messenger-widget/src/components/Message/MessageDetail.tsx +++ b/packages/messenger-widget/src/components/Message/MessageDetail.tsx @@ -50,11 +50,7 @@ export function MessageDetail(props: MessageProps) { {props.ownMessage ? ( getMessageIndicatorView(props.indicator) ) : ( - read + <> )} From 63660b578dd02c97f3b2098b9f0bf6b590156067 Mon Sep 17 00:00:00 2001 From: AlexNi245 Date: Wed, 31 Jul 2024 10:02:13 +0200 Subject: [PATCH 059/148] remove unused READ_RECEIVED MessageType --- packages/lib/messaging/src/Message.ts | 1 - .../lib/profile/src/profileExtension/ProfileExtension.ts | 5 ++--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/lib/messaging/src/Message.ts b/packages/lib/messaging/src/Message.ts index 9cfe3081e..a8fe7d212 100644 --- a/packages/lib/messaging/src/Message.ts +++ b/packages/lib/messaging/src/Message.ts @@ -46,7 +46,6 @@ export type MessageType = | 'REACTION' | 'READ_OPENED' | 'READ_RECEIVED' - | 'READ_RECEIPT' | 'RESEND_REQUEST'; export interface Postmark { diff --git a/packages/lib/profile/src/profileExtension/ProfileExtension.ts b/packages/lib/profile/src/profileExtension/ProfileExtension.ts index e0136bbc3..6a21b3853 100644 --- a/packages/lib/profile/src/profileExtension/ProfileExtension.ts +++ b/packages/lib/profile/src/profileExtension/ProfileExtension.ts @@ -5,9 +5,8 @@ export type MessageType = | 'EDIT' | 'REPLY' | 'REACTION' - | 'READ_OPEN' - | 'READ_RECEIVE' - | 'READ_RECEIPT' + | 'READ_OPENED' + | 'READ_RECEIVED' | 'RESEND_REQUEST'; export interface ProfileExtension { From c495b38467a30bc935dc9ab79ebc00563652738a Mon Sep 17 00:00:00 2001 From: AlexNi245 Date: Wed, 31 Jul 2024 10:42:32 +0200 Subject: [PATCH 060/148] rename storageEnvelopContainer to messageModel --- .../src/components/Chat/Chat.tsx | 25 +++++++------------ 1 file changed, 9 insertions(+), 16 deletions(-) diff --git a/packages/messenger-widget/src/components/Chat/Chat.tsx b/packages/messenger-widget/src/components/Chat/Chat.tsx index a4ffd09bc..a9552fc7d 100644 --- a/packages/messenger-widget/src/components/Chat/Chat.tsx +++ b/packages/messenger-widget/src/components/Chat/Chat.tsx @@ -192,41 +192,34 @@ export function Chat() { > {messages.length > 0 && messages.map( - ( - storageEnvelopContainer: MessageModel, - index, - ) => ( + (messageModel: MessageModel, index) => (
From 4826be627b063ed5498c5e3690aa5f395a479dac Mon Sep 17 00:00:00 2001 From: AlexNi245 Date: Wed, 31 Jul 2024 10:52:10 +0200 Subject: [PATCH 061/148] bring back preview message check --- .../messenger-widget/src/components/Contacts/Contacts.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/messenger-widget/src/components/Contacts/Contacts.tsx b/packages/messenger-widget/src/components/Contacts/Contacts.tsx index d8542137f..936d5be9e 100644 --- a/packages/messenger-widget/src/components/Contacts/Contacts.tsx +++ b/packages/messenger-widget/src/components/Contacts/Contacts.tsx @@ -105,9 +105,7 @@ export function Contacts() { // don't include the preview of acknowledgment msgs if (messages?.length > 0) { - return messages[0].envelop.message.metadata.type !== - 'READ_OPENED' && - messages[0].envelop.message.metadata.type !== 'READ_RECEIVED' && + return messages[0].envelop.message.metadata.type === 'NEW' && messages[0].envelop.message.message ? messages[0].envelop.message.message : ''; From e1e1b4f2bd0e8f7a379406b4f5dc724f9492217f Mon Sep 17 00:00:00 2001 From: AlexNi245 Date: Wed, 31 Jul 2024 11:43:01 +0200 Subject: [PATCH 062/148] add encryptedContactTLDName field to prisma schema --- .../migrations/20240731092314_/migration.sql | 8 ++++++++ packages/backend/schema.prisma | 19 ++++++++++--------- .../backend/src/persistence/getDatabase.ts | 1 + .../storage/postgres/addConversation.ts | 14 ++++++++++++-- .../postgres/dto/ConversationRecord.ts | 2 ++ .../storage/postgres/getConversationList.ts | 1 + .../postgres/utils/getOrCreateConversation.ts | 2 ++ 7 files changed, 36 insertions(+), 11 deletions(-) create mode 100644 packages/backend/migrations/20240731092314_/migration.sql diff --git a/packages/backend/migrations/20240731092314_/migration.sql b/packages/backend/migrations/20240731092314_/migration.sql new file mode 100644 index 000000000..80a699bc6 --- /dev/null +++ b/packages/backend/migrations/20240731092314_/migration.sql @@ -0,0 +1,8 @@ +/* + Warnings: + + - Added the required column `encryptedContactTLDName` to the `Conversation` table without a default value. This is not possible if the table is not empty. + +*/ +-- AlterTable +ALTER TABLE "Conversation" ADD COLUMN "encryptedContactTLDName" TEXT NOT NULL; diff --git a/packages/backend/schema.prisma b/packages/backend/schema.prisma index 38bb48aaa..7835c1e04 100644 --- a/packages/backend/schema.prisma +++ b/packages/backend/schema.prisma @@ -1,7 +1,7 @@ datasource db { //Use this URL for local development - //url = "postgresql://prisma:prisma@localhost:5433/tests" - url = env("DATABASE_URL") + url = "postgresql://prisma:prisma@localhost:5433/tests" + //url = env("DATABASE_URL") provider = "postgresql" } @@ -22,13 +22,14 @@ model EncryptedMessage { } model Conversation { - id String @id @default(uuid()) - updatedAt DateTime @default(now()) - encryptedContactName String - Message EncryptedMessage[] - Account Account @relation(fields: [accountId], references: [id]) - accountId String - isHidden Boolean @default(false) + id String @id @default(uuid()) + updatedAt DateTime @default(now()) + encryptedContactName String + encryptedContactTLDName String + Message EncryptedMessage[] + Account Account @relation(fields: [accountId], references: [id]) + accountId String + isHidden Boolean @default(false) } model Account { diff --git a/packages/backend/src/persistence/getDatabase.ts b/packages/backend/src/persistence/getDatabase.ts index 6832e7671..57120aaf8 100644 --- a/packages/backend/src/persistence/getDatabase.ts +++ b/packages/backend/src/persistence/getDatabase.ts @@ -97,6 +97,7 @@ export interface IDatabase extends IAccountDatabase { addConversation: ( ensName: string, encryptedContactName: string, + encryptedContactTLDNameL: string, ) => Promise; getConversationList: ( ensName: string, diff --git a/packages/backend/src/persistence/storage/postgres/addConversation.ts b/packages/backend/src/persistence/storage/postgres/addConversation.ts index b9fcf4612..b6b8c7559 100644 --- a/packages/backend/src/persistence/storage/postgres/addConversation.ts +++ b/packages/backend/src/persistence/storage/postgres/addConversation.ts @@ -2,10 +2,20 @@ import { PrismaClient } from '@prisma/client'; import { getOrCreateAccount } from './utils/getOrCreateAccount'; import { getOrCreateConversation } from './utils/getOrCreateConversation'; export const addConversation = - (db: PrismaClient) => async (ensName: string, contactName: string) => { + (db: PrismaClient) => + async ( + ensName: string, + contactName: string, + encryptedContactTLDName: string, + ) => { try { const account = await getOrCreateAccount(db, ensName); - await getOrCreateConversation(db, account.id, contactName); + await getOrCreateConversation( + db, + account.id, + contactName, + encryptedContactTLDName, + ); return true; } catch (e) { console.log('addConversation error ', e); diff --git a/packages/backend/src/persistence/storage/postgres/dto/ConversationRecord.ts b/packages/backend/src/persistence/storage/postgres/dto/ConversationRecord.ts index 4c358ad09..a4da36203 100644 --- a/packages/backend/src/persistence/storage/postgres/dto/ConversationRecord.ts +++ b/packages/backend/src/persistence/storage/postgres/dto/ConversationRecord.ts @@ -5,4 +5,6 @@ export type ConversationRecord = { previewMessage: string | null; //The time the conversation was last updated updatedAt: Date; + //This field can be used by the client to store information about the contacts TLD name + encryptedContactTLDName: string; }; diff --git a/packages/backend/src/persistence/storage/postgres/getConversationList.ts b/packages/backend/src/persistence/storage/postgres/getConversationList.ts index ddb965f7c..18ed8cb96 100644 --- a/packages/backend/src/persistence/storage/postgres/getConversationList.ts +++ b/packages/backend/src/persistence/storage/postgres/getConversationList.ts @@ -53,5 +53,6 @@ export const getConversationList = previewMessage: previewMessages[idx]?.encryptedEnvelopContainer ?? null, updatedAt: c.updatedAt, + encryptedContactTLDName: c.encryptedContactTLDName, })); }; diff --git a/packages/backend/src/persistence/storage/postgres/utils/getOrCreateConversation.ts b/packages/backend/src/persistence/storage/postgres/utils/getOrCreateConversation.ts index 2c73acc7c..7930347f7 100644 --- a/packages/backend/src/persistence/storage/postgres/utils/getOrCreateConversation.ts +++ b/packages/backend/src/persistence/storage/postgres/utils/getOrCreateConversation.ts @@ -4,6 +4,7 @@ export const getOrCreateConversation = async ( db: PrismaClient, accountId: string, encryptedContactName: string, + encryptedContactTLDName: string, ) => { //Check if conversation already exists const conversation = await db.conversation.findFirst({ @@ -20,6 +21,7 @@ export const getOrCreateConversation = async ( return await db.conversation.create({ data: { accountId, + encryptedContactTLDName, encryptedContactName, //Internal field to order conversations properly //Will set whenever a conversation is created or a message is added From 3e383481830286c9cd9e3626926efff5599d367c Mon Sep 17 00:00:00 2001 From: AlexNi245 Date: Wed, 31 Jul 2024 13:00:10 +0200 Subject: [PATCH 063/148] use default arg for getOrCreateConversation --- .../postgres/utils/getOrCreateConversation.ts | 2 +- packages/backend/src/storage.test.ts | 26 +++++++++++++++++++ packages/backend/src/storage.ts | 6 ++++- 3 files changed, 32 insertions(+), 2 deletions(-) diff --git a/packages/backend/src/persistence/storage/postgres/utils/getOrCreateConversation.ts b/packages/backend/src/persistence/storage/postgres/utils/getOrCreateConversation.ts index 7930347f7..b42ce635c 100644 --- a/packages/backend/src/persistence/storage/postgres/utils/getOrCreateConversation.ts +++ b/packages/backend/src/persistence/storage/postgres/utils/getOrCreateConversation.ts @@ -4,7 +4,7 @@ export const getOrCreateConversation = async ( db: PrismaClient, accountId: string, encryptedContactName: string, - encryptedContactTLDName: string, + encryptedContactTLDName: string = '', ) => { //Check if conversation already exists const conversation = await db.conversation.findFirst({ diff --git a/packages/backend/src/storage.test.ts b/packages/backend/src/storage.test.ts index 3635eeba9..f3b35d341 100644 --- a/packages/backend/src/storage.test.ts +++ b/packages/backend/src/storage.test.ts @@ -150,6 +150,32 @@ describe('Storage', () => { expect(body[0].contact).toEqual(aliceId); expect(body.length).toBe(1); }); + it('can add conversation with encryptedContactTLDName', async () => { + const aliceId = 'alice.eth'; + + const { status } = await request(app) + .post(`/new/bob.eth/addConversation`) + .set({ + authorization: 'Bearer ' + token, + }) + .send({ + encryptedContactName: aliceId, + encryptedContactTLDName: '123', + }); + expect(status).toBe(200); + + const { body } = await request(app) + .get(`/new/bob.eth/getConversations`) + .set({ + authorization: 'Bearer ' + token, + }) + .send(); + + expect(status).toBe(200); + expect(body[0].contact).toEqual(aliceId); + expect(body[0].encryptedContactTLDName).toEqual('123'); + expect(body.length).toBe(1); + }); it('handle duplicates add conversation', async () => { const aliceId = 'alice.eth'; const ronId = 'ron.eth'; diff --git a/packages/backend/src/storage.ts b/packages/backend/src/storage.ts index 5f0f34839..c656f2fea 100644 --- a/packages/backend/src/storage.ts +++ b/packages/backend/src/storage.ts @@ -196,16 +196,20 @@ export default ( ); router.post('/new/:ensName/addConversation', async (req, res, next) => { - const { encryptedContactName } = req.body; + const { encryptedContactName, encryptedContactTLDName } = req.body; if (!encryptedContactName) { res.status(400).send('invalid schema'); return; } + + //Param encryptedContactTLDName is optional, hence the default value is an empty string + const _encryptedContactTLDName = encryptedContactTLDName || ''; try { const ensName = normalizeEnsName(req.params.ensName); const success = await db.addConversation( ensName, encryptedContactName, + _encryptedContactTLDName, ); if (success) { return res.send(); From 242d0a8fa8f9f84c2b72843af06e5d7560c9ef9d Mon Sep 17 00:00:00 2001 From: AlexNi245 Date: Wed, 31 Jul 2024 13:00:33 +0200 Subject: [PATCH 064/148] get rid of some logging in Session.ts --- packages/lib/delivery/src/Session.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/packages/lib/delivery/src/Session.ts b/packages/lib/delivery/src/Session.ts index 196f65a88..3e68f82cd 100644 --- a/packages/lib/delivery/src/Session.ts +++ b/packages/lib/delivery/src/Session.ts @@ -45,9 +45,6 @@ export async function checkToken( return false; } - console.debug('checkToken - ensName', ensName); - console.debug('checkToken - session', session); - // check jwt for validity try { // will throw if signature is invalid or exp is in the past @@ -55,8 +52,6 @@ export async function checkToken( algorithms: ['HS256'], }); - console.debug('checkToken - jwtPayload', jwtPayload); - // check if payload is well formed if ( typeof jwtPayload === 'string' || From 2940412b5b0dfafdb52a019dacc8102d20d6cd5f Mon Sep 17 00:00:00 2001 From: AlexNi245 Date: Wed, 31 Jul 2024 13:10:08 +0200 Subject: [PATCH 065/148] fix broken useMessage.test --- .../messenger-widget/src/hooks/messages/useMessage.test.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/messenger-widget/src/hooks/messages/useMessage.test.tsx b/packages/messenger-widget/src/hooks/messages/useMessage.test.tsx index cf306d288..802c15f0f 100644 --- a/packages/messenger-widget/src/hooks/messages/useMessage.test.tsx +++ b/packages/messenger-widget/src/hooks/messages/useMessage.test.tsx @@ -27,6 +27,7 @@ import { getMockedTldContext } from '../../context/testHelper/getMockedTldContex import { getEmptyContact } from '../../interfaces/utils'; import { DM3Configuration } from '../../widget'; import { useMessage } from './useMessage'; +import { stringify } from '@dm3-org/dm3-lib-shared'; describe('useMessage hook test cases', () => { const CONTACT_NAME = 'user.dm3.eth'; @@ -789,8 +790,8 @@ describe('useMessage hook test cases', () => { result.current.messages['alice.eth'].length > 0, ); + console.log(stringify(result.current.messages['alice.eth'])); expect(result.current.contactIsLoading('alice.eth')).toBe(false); - expect(result.current.messages['alice.eth'].length).toBe(3); expect(syncAcknowledgmentMock).toBeCalledTimes(1); expect(syncAcknowledgmentMock).toBeCalledWith( From d682525e4e9789cca882bbbc856fba22a5c9c12b Mon Sep 17 00:00:00 2001 From: Bhupesh-MS Date: Thu, 1 Aug 2024 19:00:03 +0530 Subject: [PATCH 066/148] fixed duplicate msg issue in halt delivery --- .../src/new/cloudStorage/getCloudStorage.ts | 7 +++++-- packages/lib/storage/src/new/types.ts | 8 +++++++- .../testHelper/getMockedStorageContext.ts | 5 ++++- .../src/hooks/haltDelivery/useHaltDelivery.ts | 17 ++++++++++++++++- .../src/hooks/storage/useStorage.tsx | 3 ++- 5 files changed, 34 insertions(+), 6 deletions(-) diff --git a/packages/lib/storage/src/new/cloudStorage/getCloudStorage.ts b/packages/lib/storage/src/new/cloudStorage/getCloudStorage.ts index 580d39ac3..65c644d35 100644 --- a/packages/lib/storage/src/new/cloudStorage/getCloudStorage.ts +++ b/packages/lib/storage/src/new/cloudStorage/getCloudStorage.ts @@ -83,11 +83,14 @@ export const getCloudStorage = ( message.encryptedEnvelopContainer, ); - return JSON.parse(decryptedEnvelopContainer); + return { + ...JSON.parse(decryptedEnvelopContainer), + messageId: message.messageId, + }; }), ); - return decryptedMessages as StorageEnvelopContainer[]; + return decryptedMessages; }; const clearHaltedMessages = async ( diff --git a/packages/lib/storage/src/new/types.ts b/packages/lib/storage/src/new/types.ts index 5c7401267..f02d3cf61 100644 --- a/packages/lib/storage/src/new/types.ts +++ b/packages/lib/storage/src/new/types.ts @@ -7,7 +7,7 @@ export interface StorageAPI { pageSize: number, offset: number, ) => Promise; - getHaltedMessages: () => Promise; + getHaltedMessages: () => Promise; clearHaltedMessages: ( messageId: string, aliasName: string, @@ -39,6 +39,12 @@ export interface StorageEnvelopContainer { envelop: Envelop; } +export interface HaltedStorageEnvelopContainer { + messageState: MessageState; + envelop: Envelop; + messageId: string; +} + export interface Conversation { //the contactEnsName is the ensName of the contact contactEnsName: string; diff --git a/packages/messenger-widget/src/context/testHelper/getMockedStorageContext.ts b/packages/messenger-widget/src/context/testHelper/getMockedStorageContext.ts index 00d59b749..c7f17a700 100644 --- a/packages/messenger-widget/src/context/testHelper/getMockedStorageContext.ts +++ b/packages/messenger-widget/src/context/testHelper/getMockedStorageContext.ts @@ -3,6 +3,7 @@ import { Conversation, } from '@dm3-org/dm3-lib-storage'; import { StorageContextType } from '../StorageContext'; +import { HaltedStorageEnvelopContainer } from '@dm3-org/dm3-lib-storage/src/new/types'; //Provide a mocked storage context //Override the default values with the provided values @@ -56,7 +57,9 @@ export const getMockedStorageContext = ( ): Promise { throw new Error('Function not implemented.'); }, - getHaltedMessages: function (): Promise { + getHaltedMessages: function (): Promise< + HaltedStorageEnvelopContainer[] + > { throw new Error('Function not implemented.'); }, clearHaltedMessages: function ( diff --git a/packages/messenger-widget/src/hooks/haltDelivery/useHaltDelivery.ts b/packages/messenger-widget/src/hooks/haltDelivery/useHaltDelivery.ts index 17e8f88f4..059219ef9 100644 --- a/packages/messenger-widget/src/hooks/haltDelivery/useHaltDelivery.ts +++ b/packages/messenger-widget/src/hooks/haltDelivery/useHaltDelivery.ts @@ -31,6 +31,15 @@ export const useHaltDelivery = () => { // Fetch all messages the user has halted. Then check if they can be delivered now. const handleHaltedMessages = async () => { const haltedMessages = await getHaltedMessages(); + + const msgMap = new Map(); + + // Map to store messageIds so that it can be used to clear the messages, + // once the messages are sent to receiver's delivery service + haltedMessages.map((h) => { + msgMap.set(h.envelop.message.signature, h.messageId); + }); + //Get all recipients of the halted messages const recipients = Array.from( new Set( @@ -39,6 +48,7 @@ export const useHaltDelivery = () => { ), ), ); + //Resolve the tldNames to their aliases const resolvedAliases = await Promise.all( recipients.map(async (ensName) => ({ @@ -145,8 +155,13 @@ export const useHaltDelivery = () => { await submitEnvelopsToReceiversDs(dispatchableEnvelops); + // delete the halted messages with messageId dispatchableEnvelops.map((envelop) => { - clearHaltedMessages(envelop.haltedEnvelopId, envelop.aliasName); + // fetch the messageId from message signature + if (envelop.envelop.metadata?.signature) { + const msgId = msgMap.get(envelop.envelop.message.signature); + clearHaltedMessages(msgId as string, envelop.aliasName); + } }); }; diff --git a/packages/messenger-widget/src/hooks/storage/useStorage.tsx b/packages/messenger-widget/src/hooks/storage/useStorage.tsx index 91745bba9..2023413c8 100644 --- a/packages/messenger-widget/src/hooks/storage/useStorage.tsx +++ b/packages/messenger-widget/src/hooks/storage/useStorage.tsx @@ -15,6 +15,7 @@ import { sha256, stringify } from '@dm3-org/dm3-lib-shared'; import { Conversation, StorageAPI } from '@dm3-org/dm3-lib-storage'; import { useEffect, useState } from 'react'; import { BackendContextType } from '../../context/BackendContext'; +import { HaltedStorageEnvelopContainer } from '../../../../lib/storage/src/new/types'; //Handels storage sync and offers an interface for other hooks to interact with the storage export const useStorage = ( @@ -209,7 +210,7 @@ export type GetMessages = ( pageSize: number, offset: number, ) => Promise; -export type GetHaltedMessages = () => Promise; +export type GetHaltedMessages = () => Promise; export type ClearHaltedMessages = ( messageId: string, aliasName: string, From 82224849053a1f3d0b23d9f888ca4835e6f5a5e9 Mon Sep 17 00:00:00 2001 From: Bhupesh-MS Date: Thu, 1 Aug 2024 19:50:10 +0530 Subject: [PATCH 067/148] fixed build issue --- packages/lib/storage/src/new/types.ts | 8 +------- .../src/context/testHelper/getMockedStorageContext.ts | 5 +---- .../messenger-widget/src/hooks/storage/useStorage.tsx | 3 +-- 3 files changed, 3 insertions(+), 13 deletions(-) diff --git a/packages/lib/storage/src/new/types.ts b/packages/lib/storage/src/new/types.ts index f02d3cf61..cba9c4241 100644 --- a/packages/lib/storage/src/new/types.ts +++ b/packages/lib/storage/src/new/types.ts @@ -7,7 +7,7 @@ export interface StorageAPI { pageSize: number, offset: number, ) => Promise; - getHaltedMessages: () => Promise; + getHaltedMessages: () => Promise; clearHaltedMessages: ( messageId: string, aliasName: string, @@ -39,12 +39,6 @@ export interface StorageEnvelopContainer { envelop: Envelop; } -export interface HaltedStorageEnvelopContainer { - messageState: MessageState; - envelop: Envelop; - messageId: string; -} - export interface Conversation { //the contactEnsName is the ensName of the contact contactEnsName: string; diff --git a/packages/messenger-widget/src/context/testHelper/getMockedStorageContext.ts b/packages/messenger-widget/src/context/testHelper/getMockedStorageContext.ts index c7f17a700..3366da4e8 100644 --- a/packages/messenger-widget/src/context/testHelper/getMockedStorageContext.ts +++ b/packages/messenger-widget/src/context/testHelper/getMockedStorageContext.ts @@ -3,7 +3,6 @@ import { Conversation, } from '@dm3-org/dm3-lib-storage'; import { StorageContextType } from '../StorageContext'; -import { HaltedStorageEnvelopContainer } from '@dm3-org/dm3-lib-storage/src/new/types'; //Provide a mocked storage context //Override the default values with the provided values @@ -57,9 +56,7 @@ export const getMockedStorageContext = ( ): Promise { throw new Error('Function not implemented.'); }, - getHaltedMessages: function (): Promise< - HaltedStorageEnvelopContainer[] - > { + getHaltedMessages: function (): Promise { throw new Error('Function not implemented.'); }, clearHaltedMessages: function ( diff --git a/packages/messenger-widget/src/hooks/storage/useStorage.tsx b/packages/messenger-widget/src/hooks/storage/useStorage.tsx index 2023413c8..4030f34c4 100644 --- a/packages/messenger-widget/src/hooks/storage/useStorage.tsx +++ b/packages/messenger-widget/src/hooks/storage/useStorage.tsx @@ -15,7 +15,6 @@ import { sha256, stringify } from '@dm3-org/dm3-lib-shared'; import { Conversation, StorageAPI } from '@dm3-org/dm3-lib-storage'; import { useEffect, useState } from 'react'; import { BackendContextType } from '../../context/BackendContext'; -import { HaltedStorageEnvelopContainer } from '../../../../lib/storage/src/new/types'; //Handels storage sync and offers an interface for other hooks to interact with the storage export const useStorage = ( @@ -210,7 +209,7 @@ export type GetMessages = ( pageSize: number, offset: number, ) => Promise; -export type GetHaltedMessages = () => Promise; +export type GetHaltedMessages = () => Promise; export type ClearHaltedMessages = ( messageId: string, aliasName: string, From eaee6787ffce9e27c9e60d08f5d857e9a7582fa5 Mon Sep 17 00:00:00 2001 From: AlexNi245 Date: Thu, 1 Aug 2024 16:40:00 +0200 Subject: [PATCH 068/148] use DATABASE_URL in prisma --- packages/backend/schema.prisma | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/backend/schema.prisma b/packages/backend/schema.prisma index 7835c1e04..51144e04d 100644 --- a/packages/backend/schema.prisma +++ b/packages/backend/schema.prisma @@ -1,7 +1,7 @@ datasource db { //Use this URL for local development - url = "postgresql://prisma:prisma@localhost:5433/tests" - //url = env("DATABASE_URL") + //url = "postgresql://prisma:prisma@localhost:5433/tests" + url = env("DATABASE_URL") provider = "postgresql" } From e1de39addd6064cc7497374c61945c87d7538f7f Mon Sep 17 00:00:00 2001 From: AlexNi245 Date: Thu, 1 Aug 2024 18:07:19 +0200 Subject: [PATCH 069/148] addConversation store tldAliasNAme in storage --- packages/lib/shared/src/IBackendConnector.ts | 7 +++- .../src/new/cloudStorage/getCloudStorage.ts | 21 +++++++++++- packages/lib/storage/src/new/types.ts | 9 ++++-- .../AddConversation/AddConversation.tsx | 28 ++++++++-------- .../ContactMenu/ContactMenu.test.tsx | 1 + .../src/context/BackendContext.tsx | 7 +++- .../src/context/ConversationContext.tsx | 4 +-- .../src/context/StorageContext.tsx | 2 +- .../getMockedConversationContext.ts | 2 +- .../testHelper/getMockedStorageContext.ts | 1 + .../src/hooks/conversation/hydrateContact.ts | 2 ++ .../conversation/useConversation.test.tsx | 18 +++++++++++ .../hooks/conversation/useConversation.tsx | 20 ++++++++---- .../handleMessagesFromDeliveryService.ts | 9 +++--- .../sources/handleMessagesFromWebSocket.ts | 9 ++++-- .../src/hooks/messages/useMessage.test.tsx | 32 ++++++++++++++++--- .../src/hooks/server-side/BackendConnector.ts | 2 ++ .../src/hooks/server-side/useBackend.ts | 12 +++++-- .../src/hooks/storage/useStorage.tsx | 12 +++++-- .../messenger-widget/src/interfaces/utils.ts | 3 ++ 20 files changed, 156 insertions(+), 45 deletions(-) diff --git a/packages/lib/shared/src/IBackendConnector.ts b/packages/lib/shared/src/IBackendConnector.ts index d62627860..32d2d3f7c 100644 --- a/packages/lib/shared/src/IBackendConnector.ts +++ b/packages/lib/shared/src/IBackendConnector.ts @@ -1,5 +1,9 @@ export interface IBackendConnector { - addConversation(ensName: string, encryptedContactName: string): void; + addConversation( + ensName: string, + encryptedContactName: string, + encryptedContactTLDName: string, + ): void; getConversations( ensName: string, size: number, @@ -7,6 +11,7 @@ export interface IBackendConnector { ): Promise< { contact: string; + encryptedContactTLDName: string; previewMessage: string; updatedAt: Date; }[] diff --git a/packages/lib/storage/src/new/cloudStorage/getCloudStorage.ts b/packages/lib/storage/src/new/cloudStorage/getCloudStorage.ts index 580d39ac3..88934ccb5 100644 --- a/packages/lib/storage/src/new/cloudStorage/getCloudStorage.ts +++ b/packages/lib/storage/src/new/cloudStorage/getCloudStorage.ts @@ -8,13 +8,23 @@ export const getCloudStorage = ( ensName: string, encryption: Encryption, ): StorageAPI => { - const _addConversation = async (contactEnsName: string) => { + const _addConversation = async ( + contactEnsName: string, + contactTldNames: string[], + ) => { const encryptedContactName = await encryption.encryptSync( contactEnsName, ); + + const encryptedContactTLDName = await encryption.encryptSync( + JSON.stringify(contactTldNames), + ); + + console.log('add contact ', contactEnsName, contactTldNames); return await backendConnector.addConversation( ensName, encryptedContactName, + encryptedContactTLDName, ); }; @@ -29,14 +39,23 @@ export const getCloudStorage = ( conversations.map( async ({ contact, + encryptedContactTLDName, previewMessage, updatedAt, }: { contact: string; + encryptedContactTLDName: string; previewMessage: string | null; updatedAt: Date; }) => ({ contactEnsName: await encryption.decryptSync(contact), + contactTldNames: encryptedContactTLDName + ? JSON.parse( + await encryption.decryptSync( + encryptedContactTLDName, + ), + ) + : [], isHidden: false, messageCounter: 0, previewMessage: previewMessage diff --git a/packages/lib/storage/src/new/types.ts b/packages/lib/storage/src/new/types.ts index 5c7401267..2b769e6eb 100644 --- a/packages/lib/storage/src/new/types.ts +++ b/packages/lib/storage/src/new/types.ts @@ -22,7 +22,10 @@ export interface StorageAPI { ) => Promise; getNumberOfMessages: (contactEnsName: string) => Promise; getNumberOfConverations: () => Promise; - addConversation: (contactEnsName: string) => Promise; + addConversation: ( + contactEnsName: string, + contactTldNames: string[], + ) => Promise; addMessage: ( contactEnsName: string, envelop: StorageEnvelopContainer, @@ -40,8 +43,10 @@ export interface StorageEnvelopContainer { } export interface Conversation { - //the contactEnsName is the ensName of the contact + //the contactEnsName is the ensName of the contact used as the id of the conversation contactEnsName: string; + //The contact might have certain tld associated with it + contactTldNames: string[]; //the previewMessage is the last message of the conversation previewMessage?: StorageEnvelopContainer; //isHidden is a flag to hide the conversation from the conversation list diff --git a/packages/messenger-widget/src/components/AddConversation/AddConversation.tsx b/packages/messenger-widget/src/components/AddConversation/AddConversation.tsx index c019e844f..9a943d78e 100644 --- a/packages/messenger-widget/src/components/AddConversation/AddConversation.tsx +++ b/packages/messenger-widget/src/components/AddConversation/AddConversation.tsx @@ -31,7 +31,7 @@ export default function AddConversation() { setAddConversation, } = useContext(ModalContext); - const [name, setName] = useState(''); + const [tldName, setName] = useState(''); const [showError, setShowError] = useState(false); const [errorMsg, setErrorMsg] = useState(''); const [inputClass, setInputClass] = useState(INPUT_FIELD_CLASS); @@ -39,28 +39,30 @@ export default function AddConversation() { // handles new contact submission const submit = async (e: React.FormEvent) => { e.preventDefault(); - setName(name.trim()); - if (name.length) { + setName(tldName.trim()); + if (tldName.length) { // start loader setLoaderContent('Adding contact...'); startLoader(); const ensNameIsInvalid = ethAddress && - name.split('.')[0] && - ethAddress.toLowerCase() === name.split('.')[0].toLowerCase(); + tldName.split('.')[0] && + ethAddress.toLowerCase() === + tldName.split('.')[0].toLowerCase(); if (ensNameIsInvalid) { setErrorMsg('Please enter valid ENS name'); setShowError(true); return; } - //Checks wether the name entered, is an tld name. If yes, the TLD is substituded with the alias name - const aliasName = await resolveTLDtoAlias(name); + + //TODO resolve TLD name + const newContact = await addConversation(tldName); const addConversationData = { active: true, - ensName: aliasName, + ensName: newContact?.contactDetails.account.ensName, processed: false, }; @@ -72,8 +74,6 @@ export default function AddConversation() { // set right view to chat setSelectedRightView(RightViewSelected.Chat); - - const newContact = await addConversation(aliasName); if (!newContact) { //Maybe show a message that its not possible to add the users address as a contact setShowAddConversationModal(false); @@ -171,7 +171,7 @@ export default function AddConversation() { )} type="text" placeholder="Enter the name or address of the contact" - value={name} + value={tldName} onChange={( e: React.ChangeEvent, ) => handleNameChange(e)} @@ -188,10 +188,12 @@ export default function AddConversation() {