diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 9e37d4b43..77bf5614d 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -133,13 +133,15 @@ jobs: rsync -avz -e 'ssh -i ./ssh-key' ./.env app@${{ vars.HOST_DOMAIN }}:/home/app/dm3 rsync -avz -e 'ssh -i ./ssh-key' ./dm3-*.tar app@${{ vars.HOST_DOMAIN }}:/home/app/dm3 rsync -avz -e 'ssh -i ./ssh-key' ./nginx.conf app@${{ vars.HOST_DOMAIN }}:/home/app/dm3 - rsync -avz -e 'ssh -i ./ssh-key' ./docker/docker-compose.yml app@${{ vars.HOST_DOMAIN }}:/home/app/dm3 - name: Stop docker on server run: | ssh -i ./ssh-key app@${{ vars.HOST_DOMAIN }} "\ cd dm3 && docker compose down" ssh -i ./ssh-key root@${{ vars.HOST_DOMAIN }} "\ systemctl restart docker.service" + - name: Send docker compose to server + run: | + rsync -avz -e 'ssh -i ./ssh-key' ./docker/docker-compose.yml app@${{ vars.HOST_DOMAIN }}:/home/app/dm3 - name: Load docker images run: | ssh -i ./ssh-key app@${{ vars.HOST_DOMAIN }} "\ diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index af6ff07bb..5198fef4d 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -25,7 +25,6 @@ services: - db - dm3-storage environment: - REDIS_URL: redis://db:6379 SIGNING_PUBLIC_KEY: ${SIGNING_PUBLIC_KEY} SIGNING_PRIVATE_KEY: ${SIGNING_PRIVATE_KEY} ENCRYPTION_PUBLIC_KEY: ${ENCRYPTION_PUBLIC_KEY} @@ -36,12 +35,6 @@ services: LOG_LEVEL: 'debug' DATABASE_URL: ${DATABASE_URL} - db: - image: redis - restart: always - volumes: - - ${PERSISTENCE_DIRECTORY}/db/redis:/data - dm3-storage: image: postgres:13 restart: always diff --git a/packages/backend/docker-compose.test.yml b/packages/backend/docker-compose.test.yml index bd4d42e37..1606c248d 100644 --- a/packages/backend/docker-compose.test.yml +++ b/packages/backend/docker-compose.test.yml @@ -2,12 +2,6 @@ version: '3.6' # The containers that compose the project services: - db: - image: redis - restart: always - container_name: redis-backend - ports: - - '6380:6379' # map to host port 6380 postgres: image: postgres:13 restart: always diff --git a/packages/backend/docker-compose.yml b/packages/backend/docker-compose.yml index 695e0c083..89485ee60 100644 --- a/packages/backend/docker-compose.yml +++ b/packages/backend/docker-compose.yml @@ -2,12 +2,6 @@ version: '3.6' # The containers that compose the project services: - db: - image: redis - restart: always - container_name: redis - ports: - - '6380:6379' # map to host port 6380 dm3-storage: image: postgres:13 restart: always diff --git a/packages/backend/manual_data_migration/insertWithinDocker.sh b/packages/backend/manual_data_migration/insertWithinDocker.sh deleted file mode 100755 index c99c7cf57..000000000 --- a/packages/backend/manual_data_migration/insertWithinDocker.sh +++ /dev/null @@ -1,27 +0,0 @@ -#!/bin/bash - -# Input file -input_file="dump.txt" - -DB_NAME="dm3" -DB_USER="prisma" - - -# 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))\ - ON CONFLICT (id) \ - DO UPDATE SET \"createdAt\" = excluded.\"createdAt\";" - -done < "$input_file" - diff --git a/packages/backend/manual_data_migration/notes.md b/packages/backend/manual_data_migration/notes.md deleted file mode 100644 index f7ab24c2c..000000000 --- a/packages/backend/manual_data_migration/notes.md +++ /dev/null @@ -1,37 +0,0 @@ -Process: - -check data -`docker exec -it dm3-db-1 redis-cli --scan --pattern 'session*addr.dm3.eth'` - -`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*addr.dm3.eth'`; 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:/ - -paste script onto server -vi insertWithinDocker.sh --> paste, close - -copy the script to the postgres container -docker cp insertWithinDocker.sh dm3-storage:/ - -go into the postgres container -docker exec -it dm3-storage bash - -make script executable -chmod a+x insertWithinDocker.sh - -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";' diff --git a/packages/backend/package.json b/packages/backend/package.json index b9f1bf339..c2bdf10d5 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -1,7 +1,7 @@ { "name": "@dm3-org/dm3-backend", "license": "BSD-2-Clause", - "version": "1.6.1", + "version": "1.7.0", "main": "dist/index.js", "types": "dist/index.d.ts", "dependencies": { @@ -13,7 +13,7 @@ "@dm3-org/dm3-lib-shared": "workspace:^", "@dm3-org/dm3-lib-storage": "workspace:^", "@dm3-org/dm3-lib-test-helper": "workspace:^", - "@prisma/client": "4.16.2", + "@prisma/client": "^5.19.1", "axios": "^0.27.2", "body-parser": "^1.20.1", "cors": "^2.8.5", @@ -21,9 +21,7 @@ "ethers": "5.7.2", "express": "^4.18.1", "prisma": "^5.10.1", - "redis": "^4.1.0", "web-push": "^3.6.7", - "winston": "^3.8.1", "yaml": "^2.1.3" }, "scripts": { @@ -33,7 +31,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 prisma generate && yarn tsc && cp ./config.yml ./dist/config.yml | true", + "build": "yarn prisma generate && yarn tsc | 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" @@ -45,7 +43,6 @@ "@types/cors": "^2.8.17", "@types/express": "^4.17.13", "@types/node": "^20.3.1", - "@types/redis": "^4.0.11", "@types/supertest": "^2.0.12", "babel-cli": "^6.26.0", "babel-jest": "^29.2.2", @@ -53,6 +50,7 @@ "jest": "^29.2.2", "jest-mock-extended": "2.0.4", "prettier": "^2.6.2", + "prisma": "^5.19.1", "superagent": "^8.0.3", "supertest": "^6.3.1", "ts-json-schema-generator": "^0.98.0", diff --git a/packages/backend/src/config/getDeliveryServiceProperties.test.ts b/packages/backend/src/config/getDeliveryServiceProperties.test.ts deleted file mode 100644 index 80a9575d8..000000000 --- a/packages/backend/src/config/getDeliveryServiceProperties.test.ts +++ /dev/null @@ -1,175 +0,0 @@ -import { existsSync, unlinkSync, writeFileSync } from 'fs'; -import { resolve } from 'path'; -import { stringify } from 'yaml'; -import { getDeliveryServiceProperties } from './getDeliveryServiceProperties'; -import { NotificationChannelType } from '@dm3-org/dm3-lib-shared'; - -describe('ReadDeliveryServiceProperties', () => { - let path: string; - beforeEach(() => { - path = resolve(__dirname, './config.test.yml'); - }); - - afterEach(() => { - if (existsSync(path)) { - unlinkSync(path); - } - }); - - it('Returns default DeliveryServiceProfile if config file is undefined', () => { - const config = getDeliveryServiceProperties('/unknown-path', { - messageTTL: 12345, - sizeLimit: 456, - notificationChannel: [], - }); - - expect(config).toStrictEqual({ - messageTTL: 12345, - sizeLimit: 456, - notificationChannel: [], - }); - }); - - it('Returns Config from path', () => { - writeFileSync( - path, - stringify({ - messageTTL: 12345, - sizeLimit: 456, - notificationChannel: [], - }), - { encoding: 'utf-8' }, - ); - const config = getDeliveryServiceProperties(path); - - expect(config).toStrictEqual({ - messageTTL: 12345, - sizeLimit: 456, - notificationChannel: [], - }); - }); - it('Adds default properties if config.yml is not fully specified', () => { - writeFileSync( - path, - stringify({ - messageTTL: 12345, - notificationChannel: [ - { - type: NotificationChannelType.EMAIL, - config: { - host: 'mail.alice.com', - port: 465, - secure: true, - auth: { - user: 'foo', - pass: 'bar', - }, - senderAddress: 'mail@dm3.io', - }, - }, - ], - }), - { encoding: 'utf-8' }, - ); - const config = getDeliveryServiceProperties(path); - - expect(config).toStrictEqual({ - messageTTL: 12345, - sizeLimit: 100000, - notificationChannel: [ - { - type: NotificationChannelType.EMAIL, - config: { - host: 'mail.alice.com', - port: 465, - secure: true, - auth: { - user: 'foo', - pass: 'bar', - }, - senderAddress: 'mail@dm3.io', - }, - }, - ], - }); - }); - it('Adds email channel from config.yml file & rest from default properties', () => { - writeFileSync( - path, - stringify({ - notificationChannel: [ - { - type: NotificationChannelType.EMAIL, - config: { - host: 'mail.alice.com', - port: 465, - secure: true, - auth: { - user: 'foo', - pass: 'bar', - }, - senderAddress: 'mail@dm3.io', - }, - }, - ], - }), - { encoding: 'utf-8' }, - ); - const config = getDeliveryServiceProperties(path); - - expect(config).toStrictEqual({ - messageTTL: 0, - sizeLimit: 100000, - notificationChannel: [ - { - type: NotificationChannelType.EMAIL, - config: { - host: 'mail.alice.com', - port: 465, - secure: true, - auth: { - user: 'foo', - pass: 'bar', - }, - senderAddress: 'mail@dm3.io', - }, - }, - ], - }); - }); - - it('Adds push notification channel from config.yml file & rest from default properties', () => { - writeFileSync( - path, - stringify({ - notificationChannel: [ - { - type: NotificationChannelType.PUSH, - config: { - vapidEmailId: 'test@gmail.com', - publicVapidKey: 'dbiwqeqwewqosa', - privateVapidKey: 'wqieyiwqeqwnsd', - }, - }, - ], - }), - { encoding: 'utf-8' }, - ); - const config = getDeliveryServiceProperties(path); - - expect(config).toStrictEqual({ - messageTTL: 0, - sizeLimit: 100000, - notificationChannel: [ - { - type: NotificationChannelType.PUSH, - config: { - vapidEmailId: 'test@gmail.com', - publicVapidKey: 'dbiwqeqwewqosa', - privateVapidKey: 'wqieyiwqeqwnsd', - }, - }, - ], - }); - }); -}); diff --git a/packages/backend/src/config/getDeliveryServiceProperties.ts b/packages/backend/src/config/getDeliveryServiceProperties.ts deleted file mode 100644 index cc420c553..000000000 --- a/packages/backend/src/config/getDeliveryServiceProperties.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { parse } from 'yaml'; -import { existsSync, readFileSync } from 'fs'; -import { resolve } from 'path'; -import { logInfo, validateSchema } from '@dm3-org/dm3-lib-shared'; -import { schema, DeliveryServiceProperties } from '@dm3-org/dm3-lib-delivery'; - -const DEFAULT_CONFIG_FILE_PATH = resolve(__dirname, './../config.yml'); -const DEFAULT_DELIVERY_SERVICE_PROPERTIES: DeliveryServiceProperties = { - messageTTL: 0, - //100Kb - sizeLimit: 100000, - notificationChannel: [], -}; - -export function getDeliveryServiceProperties( - path: string = DEFAULT_CONFIG_FILE_PATH, - defaultDeliveryServiceProperties: DeliveryServiceProperties = DEFAULT_DELIVERY_SERVICE_PROPERTIES, -): DeliveryServiceProperties { - console.log('resolve dir', __dirname); - console.log('looking for config.yml in ', path); - console.log('resolve root: ', resolve(__dirname, './config.yml')); - console.log( - 'can find config ', - existsSync(resolve(__dirname, './config.yml')), - ); - if (!existsSync(path)) { - logInfo('Config file not found. Default Config is used'); - return defaultDeliveryServiceProperties; - } - - const yamlString = readFileSync(path, { encoding: 'utf-8' }); - - const deliveryServiceProfile = parse(yamlString); - - const isSchemaValid = validateSchema( - // eslint-disable-next-line max-len - //The interface DeliveryServiceProperties requires all properties to be non-null. But since we are accepting a partially filled config.yml we are overwriting the required fields so basically no property is required at all. This can be done because every missing property is replaced by a default property - { - ...schema.DeliveryServiceProperties, - definitions: { - ...schema.DeliveryServiceProperties.definitions, - DeliveryServiceProperties: { - ...schema.DeliveryServiceProperties.definitions - .DeliveryServiceProperties, - required: [], - }, - }, - }, - deliveryServiceProfile, - ); - - if (!isSchemaValid) { - throw Error('Invalid config.yml'); - } - - return { - ...defaultDeliveryServiceProperties, - ...parse(yamlString), - } as DeliveryServiceProperties; -} diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index 20fe2d05b..46ae634ef 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -1,5 +1,5 @@ import { - Auth, + Authenticate, errorHandler, getCachedWebProvider, getServerSecret, @@ -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, serverSecret, web3Provider)); + app.use('/auth', Authenticate(db, serverSecret, web3Provider)); app.use(logError); app.use(errorHandler); })(); diff --git a/packages/backend/src/persistence/account/getAccount.ts b/packages/backend/src/persistence/account/getAccount.ts deleted file mode 100644 index d83694907..000000000 --- a/packages/backend/src/persistence/account/getAccount.ts +++ /dev/null @@ -1,19 +0,0 @@ -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)), - ); - - console.debug('get account ', ensName, session); - - return session - ? (JSON.parse(session) as Session & { - spamFilterRules: spamFilter.SpamFilterRules; - }) - : null; - }; -} diff --git a/packages/backend/src/persistence/account/index.ts b/packages/backend/src/persistence/account/index.ts deleted file mode 100644 index 37cd75311..000000000 --- a/packages/backend/src/persistence/account/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { setAccount } from './setAccount'; -import { getAccount } from './getAccount'; -import { getIdEnsName } from '../getIdEnsName'; -export default { setAccount, getAccount, getIdEnsName }; diff --git a/packages/backend/src/persistence/account/setAccount.ts b/packages/backend/src/persistence/account/setAccount.ts deleted file mode 100644 index 2308bcdf9..000000000 --- a/packages/backend/src/persistence/account/setAccount.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { Redis, RedisPrefix } from '../getDatabase'; -import { Session, schema } from '@dm3-org/dm3-lib-delivery'; -import { validateSchema, stringify } from '@dm3-org/dm3-lib-shared'; -import { getIdEnsName } from '../getIdEnsName'; - -export function setAccount(redis: Redis) { - return async (ensName: string, session: Session) => { - const isValid = validateSchema(schema.Session, session); - - 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/backend/src/persistence/getDatabase.ts b/packages/backend/src/persistence/getDatabase.ts index 6c16b1db8..93e508696 100644 --- a/packages/backend/src/persistence/getDatabase.ts +++ b/packages/backend/src/persistence/getDatabase.ts @@ -1,51 +1,9 @@ import { Account, PrismaClient } from '@prisma/client'; -import { createClient } from 'redis'; import Storage from './storage'; import { ConversationRecord } from './storage/postgres/dto/ConversationRecord'; import { MessageRecord } from './storage/postgres/dto/MessageRecord'; import { IAccountDatabase } from '@dm3-org/dm3-lib-server-side'; -export enum RedisPrefix { - Conversation = 'conversation:', - IncomingConversations = 'incoming.conversations:', - Sync = 'sync:', - // Account used to be called Session. The prefix still resolves to "session:" for now. - Account = 'session:', - NotificationChannel = 'notificationChannel:', - GlobalNotification = 'globalNotification:', - Otp = 'otp:', - UserStorageMigrated = 'user.storage.migrated:', -} - -export async function getRedisClient() { - const url = process.env.REDIS_URL || 'redis://127.0.0.1:6380'; - const socketConf = { - socket: { - tls: true, - rejectUnauthorized: false, - }, - }; - const client = createClient( - process.env.NODE_ENV === 'production' - ? { - url, - ...socketConf, - } - : { url }, - ); - - client.on('error', (err) => { - console.error('Redis error: ' + (err as Error).message); - }); - - client.on('reconnecting', () => console.info('Redis reconnection')); - client.on('ready', () => console.info('Redis ready')); - - await client.connect(); - - return client; -} - export async function getPrismaClient() { return new PrismaClient(); } @@ -56,28 +14,28 @@ export async function getDatabase( const prisma = _prisma ?? (await getPrismaClient()); return { - //Session + //Account setAccount: Storage.setAccount(prisma), getAccount: Storage.getAccount(prisma), hasAccount: Storage.hasAccount(prisma), - //Storage AddConversation + //AddConversation addConversation: Storage.addConversation(prisma), getConversationList: Storage.getConversationList(prisma), - //Storage Add Messages + //Add Messages addMessageBatch: Storage.addMessageBatch(prisma), - //Storage Get Messages + //Get Messages getMessagesFromStorage: Storage.getMessages(prisma), - //Storage Edit Message Batch + //Edit Message Batch editMessageBatch: Storage.editMessageBatch(prisma), - //Storage Get Number Of Messages + //Get Number Of Messages getNumberOfMessages: Storage.getNumberOfMessages(prisma), - //Storage Get Number Of Converations + //Get Number Of Converations getNumberOfConverations: Storage.getNumberOfConversations(prisma), - //Storage Toggle Hide Conversation + //Toggle Hide Conversation toggleHideConversation: Storage.toggleHideConversation(prisma), - //Storage Get Halted Messages + //Get Halted Messages getHaltedMessages: Storage.getHaltedMessages(prisma), - //Storage Delete Halted Message + //Delete Halted Message clearHaltedMessage: Storage.clearHaltedMessage(prisma), }; } @@ -130,5 +88,3 @@ export interface IBackendDatabase extends IAccountDatabase { messageId: string, ) => Promise; } - -export type Redis = Awaited>; diff --git a/packages/backend/src/persistence/getIdEnsName.ts b/packages/backend/src/persistence/getIdEnsName.ts deleted file mode 100644 index 533df28ba..000000000 --- a/packages/backend/src/persistence/getIdEnsName.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { Redis, RedisPrefix } from './getDatabase'; -import { normalizeEnsName } from '@dm3-org/dm3-lib-profile'; - -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; -} diff --git a/packages/backend/src/persistence/account/setAccount.test.ts b/packages/backend/src/persistence/storage/postgres/setAccount.test.ts similarity index 77% rename from packages/backend/src/persistence/account/setAccount.test.ts rename to packages/backend/src/persistence/storage/postgres/setAccount.test.ts index f82ab6d57..69272b0c1 100644 --- a/packages/backend/src/persistence/account/setAccount.test.ts +++ b/packages/backend/src/persistence/storage/postgres/setAccount.test.ts @@ -1,6 +1,10 @@ import { UserProfile } from '@dm3-org/dm3-lib-profile'; import { PrismaClient } from '@prisma/client'; -import { IBackendDatabase, getDatabase, getPrismaClient } from '../getDatabase'; +import { + IBackendDatabase, + getDatabase, + getPrismaClient, +} from '../../getDatabase'; const USER_NAME = '0x25A643B6e52864d0eD816F1E43c0CF49C83B8292.dm3.eth'; @@ -14,12 +18,6 @@ describe('Set Account', () => { }); it('Creates a new Account ', async () => { - const profile: UserProfile = { - publicEncryptionKey: '', - publicSigningKey: '', - deliveryServices: [], - }; - const priorSetAccount = await db.getAccount(USER_NAME); //User has no account yet diff --git a/packages/backend/src/profile/getUserProfile.ts b/packages/backend/src/profile/getUserProfile.ts deleted file mode 100644 index 337a5a666..000000000 --- a/packages/backend/src/profile/getUserProfile.ts +++ /dev/null @@ -1,11 +0,0 @@ -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.test.ts b/packages/backend/src/profile/profile.test.ts index 673869ee0..e5bfac450 100644 --- a/packages/backend/src/profile/profile.test.ts +++ b/packages/backend/src/profile/profile.test.ts @@ -43,7 +43,6 @@ const createDbMock = async () => { const dbMock = { getAccount: async (ensName: string) => Promise.resolve(accountMocked), setAccount: async (id: string) => {}, - getIdEnsName: async (ensName: string) => ensName, }; return dbMock as any; @@ -113,7 +112,6 @@ describe('Profile', () => { setAccount: async (_: string, __: any) => { return (_: any, __: any, ___: any) => {}; }, - getIdEnsName: async (ensName: string) => ensName, }; const app = express(); diff --git a/packages/backend/src/profile/profile.ts b/packages/backend/src/profile/profile.ts index 6b64090c4..9a6f83711 100644 --- a/packages/backend/src/profile/profile.ts +++ b/packages/backend/src/profile/profile.ts @@ -54,8 +54,6 @@ export default ( method: 'POST', url: req.url, ensName, - disableSessionCheck: - process.env.DISABLE_SESSION_CHECK === 'true', }); // check if profile and signature are valid diff --git a/packages/backend/src/storage.ts b/packages/backend/src/storage.ts index 736df4555..2131d63d1 100644 --- a/packages/backend/src/storage.ts +++ b/packages/backend/src/storage.ts @@ -1,5 +1,5 @@ import { normalizeEnsName } from '@dm3-org/dm3-lib-profile'; -import { authorize } from '@dm3-org/dm3-lib-server-side'; +import { authorizationMiddleware } from '@dm3-org/dm3-lib-server-side'; import { sha256, validateSchema } from '@dm3-org/dm3-lib-shared'; import cors from 'cors'; import { ethers } from 'ethers'; @@ -32,7 +32,7 @@ export default ( next: NextFunction, ensName: string, ) => { - authorize( + authorizationMiddleware( req, res, next, diff --git a/packages/billboard-client/package.json b/packages/billboard-client/package.json index d27cbb865..d0976112a 100644 --- a/packages/billboard-client/package.json +++ b/packages/billboard-client/package.json @@ -1,7 +1,7 @@ { "name": "@dm3-org/dm3-billboard-client", "license": "BSD-2-Clause", - "version": "1.6.1", + "version": "1.7.0", "private": true, "main": "dist/index.js", "types": "dist/ined.d.ts", diff --git a/packages/billboard-widget/package.json b/packages/billboard-widget/package.json index 625597ff6..d6802cce4 100644 --- a/packages/billboard-widget/package.json +++ b/packages/billboard-widget/package.json @@ -1,7 +1,7 @@ { "name": "@dm3-org/dm3-billboard-widget", "license": "BSD-2-Clause", - "version": "1.6.1", + "version": "1.7.0", "files": [ "dist" ], diff --git a/packages/delivery-service/README.md b/packages/delivery-service/README.md index 780f1d162..b77c9f1a3 100644 --- a/packages/delivery-service/README.md +++ b/packages/delivery-service/README.md @@ -23,3 +23,19 @@ npm ``` npm start ``` + +## Configuration + +### Metrics collection + +This delivery service implementation collects these metrics: + +- number of messages received +- number of notifications sent +- total size of messages received + +These metrics are accumulated over the `metricsCollectionIntervalInSeconds`, which can be defined in the config file and defaults to 1 hour. They are retained for `metricsRetentionDurationInSeconds`, which defaults to 10 days. + +The metrics are not sent anywhere, but can be accessed by anyone via the `/metrics` endpoint. This endpoint censors the current collection interval to prevent real-time tracking, which would reduce the privacy of the users. + +In order to disable metrics collection, set `metricsRetentionDurationInSeconds` to 0. diff --git a/packages/delivery-service/package.json b/packages/delivery-service/package.json index 8d66852f2..95c8cfcc9 100644 --- a/packages/delivery-service/package.json +++ b/packages/delivery-service/package.json @@ -1,7 +1,7 @@ { "name": "@dm3-org/delivery-service", "license": "BSD-2-Clause", - "version": "1.6.1", + "version": "1.7.0", "main": "dist/index.js", "types": "dist/index.d.ts", "dependencies": { @@ -11,7 +11,6 @@ "@dm3-org/dm3-lib-profile": "workspace:^", "@dm3-org/dm3-lib-server-side": "workspace:^", "@dm3-org/dm3-lib-shared": "workspace:^", - "@prisma/client": "4.16.2", "axios": "^0.27.2", "body-parser": "^1.20.1", "dotenv": "^16.0.1", @@ -20,7 +19,6 @@ "redis": "^4.1.0", "socket.io": "^4.5.1", "web-push": "^3.6.7", - "winston": "^3.8.1", "yaml": "^2.1.3" }, "scripts": { @@ -29,6 +27,7 @@ "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*)/)' ", "build": "yarn tsc ", + "dev": "nodemon --exec 'ts-node ./src/index.ts' --watch src --ext ts", "createDeliveryServiceProfile": "node --no-warnings ./cli.js", "before:tests": "docker compose -f docker-compose.test.yml up -d", "after:tests": "docker compose -f docker-compose.test.yml down" @@ -47,6 +46,7 @@ "babel-preset-env": "^1.7.0", "jest": "^29.2.2", "jest-mock-extended": "2.0.4", + "nodemon": "^3.1.4", "prettier": "^2.6.2", "superagent": "^8.0.3", "supertest": "^6.3.1", diff --git a/packages/delivery-service/src/config/getDeliveryServiceProperties.test.ts b/packages/delivery-service/src/config/getDeliveryServiceProperties.test.ts index 80a9575d8..6de40b37f 100644 --- a/packages/delivery-service/src/config/getDeliveryServiceProperties.test.ts +++ b/packages/delivery-service/src/config/getDeliveryServiceProperties.test.ts @@ -21,12 +21,16 @@ describe('ReadDeliveryServiceProperties', () => { messageTTL: 12345, sizeLimit: 456, notificationChannel: [], + metricsCollectionIntervalInSeconds: 600, + metricsRetentionDurationInSeconds: 172800, }); expect(config).toStrictEqual({ messageTTL: 12345, sizeLimit: 456, notificationChannel: [], + metricsCollectionIntervalInSeconds: 600, + metricsRetentionDurationInSeconds: 172800, }); }); @@ -37,6 +41,8 @@ describe('ReadDeliveryServiceProperties', () => { messageTTL: 12345, sizeLimit: 456, notificationChannel: [], + metricsCollectionIntervalInSeconds: 900, + metricsRetentionDurationInSeconds: 259200, }), { encoding: 'utf-8' }, ); @@ -46,8 +52,11 @@ describe('ReadDeliveryServiceProperties', () => { messageTTL: 12345, sizeLimit: 456, notificationChannel: [], + metricsCollectionIntervalInSeconds: 900, + metricsRetentionDurationInSeconds: 259200, }); }); + it('Adds default properties if config.yml is not fully specified', () => { writeFileSync( path, @@ -68,6 +77,7 @@ describe('ReadDeliveryServiceProperties', () => { }, }, ], + metricsCollectionIntervalInSeconds: 1200, }), { encoding: 'utf-8' }, ); @@ -91,8 +101,11 @@ describe('ReadDeliveryServiceProperties', () => { }, }, ], + metricsCollectionIntervalInSeconds: 1200, + metricsRetentionDurationInSeconds: 60 * 60 * 24 * 10, }); }); + it('Adds email channel from config.yml file & rest from default properties', () => { writeFileSync( path, @@ -135,6 +148,8 @@ describe('ReadDeliveryServiceProperties', () => { }, }, ], + metricsCollectionIntervalInSeconds: 60 * 60 * 24, + metricsRetentionDurationInSeconds: 60 * 60 * 24 * 10, }); }); @@ -170,6 +185,28 @@ describe('ReadDeliveryServiceProperties', () => { }, }, ], + metricsCollectionIntervalInSeconds: 60 * 60 * 24, + metricsRetentionDurationInSeconds: 60 * 60 * 24 * 10, + }); + }); + + it('Uses default values for metrics properties if not specified', () => { + writeFileSync( + path, + stringify({ + messageTTL: 54321, + sizeLimit: 789, + }), + { encoding: 'utf-8' }, + ); + const config = getDeliveryServiceProperties(path); + + expect(config).toStrictEqual({ + messageTTL: 54321, + sizeLimit: 789, + notificationChannel: [], + metricsCollectionIntervalInSeconds: 60 * 60 * 24, + metricsRetentionDurationInSeconds: 60 * 60 * 24 * 10, }); }); }); diff --git a/packages/delivery-service/src/config/getDeliveryServiceProperties.ts b/packages/delivery-service/src/config/getDeliveryServiceProperties.ts index 110aa278f..e10393e95 100644 --- a/packages/delivery-service/src/config/getDeliveryServiceProperties.ts +++ b/packages/delivery-service/src/config/getDeliveryServiceProperties.ts @@ -10,6 +10,8 @@ const DEFAULT_DELIVERY_SERVICE_PROPERTIES: DeliveryServiceProperties = { //100Kb sizeLimit: 100000, notificationChannel: [], + metricsCollectionIntervalInSeconds: 60 * 60 * 24, // 1 day + metricsRetentionDurationInSeconds: 60 * 60 * 24 * 10, // 10 days }; export function getDeliveryServiceProperties( diff --git a/packages/delivery-service/src/delivery.test.ts b/packages/delivery-service/src/delivery.test.ts index 0839b4d88..ab6fac04d 100644 --- a/packages/delivery-service/src/delivery.test.ts +++ b/packages/delivery-service/src/delivery.test.ts @@ -2,7 +2,6 @@ import { generateAuthJWT } from '@dm3-org/dm3-lib-server-side'; import bodyParser from 'body-parser'; import express from 'express'; import request from 'supertest'; -import winston from 'winston'; import delivery from './delivery'; import { Redis, getDatabase, getRedisClient } from './persistence/getDatabase'; @@ -22,10 +21,6 @@ const keysA = { const serverSecret = 'veryImportantSecret'; -global.logger = winston.createLogger({ - transports: [new winston.transports.Console()], -}); - let redisClient: Redis; describe('Delivery', () => { beforeEach(async () => { diff --git a/packages/delivery-service/src/delivery.ts b/packages/delivery-service/src/delivery.ts index 19b07799d..15ad08db0 100644 --- a/packages/delivery-service/src/delivery.ts +++ b/packages/delivery-service/src/delivery.ts @@ -4,7 +4,7 @@ import { schema, } from '@dm3-org/dm3-lib-delivery'; import { normalizeEnsName } from '@dm3-org/dm3-lib-profile'; -import { authorize } from '@dm3-org/dm3-lib-server-side'; +import { authorizationMiddleware } from '@dm3-org/dm3-lib-server-side'; import { validateSchema } from '@dm3-org/dm3-lib-shared'; import cors from 'cors'; import { ethers } from 'ethers'; @@ -40,7 +40,7 @@ export default ( //TODO remove router.use(cors()); router.param('ensName', async (req, res, next, ensName: string) => { - authorize( + authorizationMiddleware( req, res, next, diff --git a/packages/delivery-service/src/index.ts b/packages/delivery-service/src/index.ts index 84a072f52..ea7ad42c0 100644 --- a/packages/delivery-service/src/index.ts +++ b/packages/delivery-service/src/index.ts @@ -1,6 +1,6 @@ -import { Session } from '@dm3-org/dm3-lib-delivery'; +import { Account } from '@dm3-org/dm3-lib-delivery'; import { - Auth, + Authenticate, errorHandler, getCachedWebProvider, getServerSecret, @@ -18,7 +18,6 @@ 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'; @@ -29,6 +28,7 @@ import { Profile } from './profile/profile'; import RpcProxy from './rpc/rpc-proxy'; import { WebSocketManager } from './ws/WebSocketManager'; import { socketAuth } from './socketAuth'; +import Metrics from './metrics'; const app = express(); app.use(express.json({ limit: '50mb' })); @@ -46,7 +46,7 @@ const getDbWithAddressResolvedGetAccount = ( ): IDatabase => { const getAccountForEnsName = ( web3Provider: ethers.providers.JsonRpcProvider, - getAccount: (ensName: string) => Promise, + getAccount: (ensName: string) => Promise, ) => { return async (ensName: string) => { const address = await web3Provider.resolveName(ensName); @@ -92,15 +92,6 @@ const getDbWithAddressResolvedGetAccount = ( app.use(cors()); app.use(bodyParser.json()); -declare global { - var logger: winston.Logger; -} - -global.logger = winston.createLogger({ - level: process.env.LOG_LEVEL ?? 'info', - transports: [new winston.transports.Console()], -}); - (async () => { // load environment const deliveryServiceProperties = getDeliveryServiceProperties(); @@ -149,11 +140,8 @@ global.logger = winston.createLogger({ return res.status(200).send('Hello DM3'); }); - //Auth - //socketAuth - //restAuth - - app.use('/auth', Auth(db, serverSecret, web3Provider)); + app.use('/metrics', Metrics(db, deliveryServiceProperties)); + app.use('/auth', Authenticate(db, serverSecret, web3Provider)); app.use('/profile', Profile(db, web3Provider, serverSecret)); app.use('/delivery', Delivery(web3Provider, db, serverSecret)); app.use( diff --git a/packages/delivery-service/src/message/MessageProcessor.test.ts b/packages/delivery-service/src/message/MessageProcessor.test.ts index 466022d0c..09bd7eab2 100644 --- a/packages/delivery-service/src/message/MessageProcessor.test.ts +++ b/packages/delivery-service/src/message/MessageProcessor.test.ts @@ -12,7 +12,7 @@ import { MessageProcessor } from './MessageProcessor'; import { checkSignature, decryptAsymmetric } from '@dm3-org/dm3-lib-crypto'; import { DeliveryServiceProperties, - Session, + Account, spamFilter, } from '@dm3-org/dm3-lib-delivery'; import { UserProfile, normalizeEnsName } from '@dm3-org/dm3-lib-profile'; @@ -66,7 +66,7 @@ describe('MessageProcessor', () => { ensName: string, socketId?: string, ): Promise< - (Session & { spamFilterRules: spamFilter.SpamFilterRules }) | null + (Account & { spamFilterRules: spamFilter.SpamFilterRules }) | null > => { const emptyProfile: UserProfile = { publicSigningKey: '', @@ -76,11 +76,11 @@ describe('MessageProcessor', () => { const isSender = getAddress(ensName) === sender.address; const isReceiver = getAddress(ensName) === receiver.address; - const session = ( + const account = ( account: string, token: string, profile: UserProfile, - ): Session => ({ + ): Account => ({ account, signedUserProfile: { profile, @@ -97,14 +97,14 @@ describe('MessageProcessor', () => { if (isSender) { return { - ...session(sender.address, '123', emptyProfile), + ...account(sender.address, '123', emptyProfile), spamFilterRules: {}, }; } if (isReceiver) { return { - ...session(getAddress(receiver.address), 'abc', { + ...account(getAddress(receiver.address), 'abc', { ...emptyProfile, publicEncryptionKey: receiver.profileKeys.encryptionKeyPair.publicKey, @@ -256,7 +256,7 @@ describe('MessageProcessor', () => { await expect(() => messageProcessor.processEnvelop(incomingEnvelop), - ).rejects.toEqual(Error('unknown session')); + ).rejects.toEqual(Error('unknown account')); }); // //TODO remove skip once spam-filter is implemented // //TODO remove skip once spam-filter is implemented @@ -265,7 +265,7 @@ describe('MessageProcessor', () => { ({ ...(await getAccount(address)), spamFilterRules: { minNonce: 2 }, - } as Session & { spamFilterRules: spamFilter.SpamFilterRules }); + } as Account & { spamFilterRules: spamFilter.SpamFilterRules }); const db = { createMessage: async () => {}, @@ -322,7 +322,7 @@ describe('MessageProcessor', () => { ({ ...(await getAccount(address)), spamFilterRules: { minBalance: '0xa' }, - } as Session & { spamFilterRules: spamFilter.SpamFilterRules }); + } as Account & { spamFilterRules: spamFilter.SpamFilterRules }); const db = { createMessage: async () => {}, @@ -384,7 +384,7 @@ describe('MessageProcessor', () => { amount: '0xa', }, }, - } as Session & { spamFilterRules: spamFilter.SpamFilterRules }); + } as Account & { spamFilterRules: spamFilter.SpamFilterRules }); const db = { createMessage: async () => {}, diff --git a/packages/delivery-service/src/message/MessageProcessor.ts b/packages/delivery-service/src/message/MessageProcessor.ts index 0a1820067..7fb59de58 100644 --- a/packages/delivery-service/src/message/MessageProcessor.ts +++ b/packages/delivery-service/src/message/MessageProcessor.ts @@ -50,7 +50,7 @@ export class MessageProcessor { * 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 + * 3. The receiver has to have a account at the deliveryService * 4. The message must pass every {@see SpamFilterRule} the receiver declared */ public async processEnvelop(envelop: EncryptionEnvelop): Promise { @@ -107,18 +107,18 @@ export class MessageProcessor { ); console.debug(conversationId, deliveryInformation); - //Retrieves the session of the receiver - const receiverSession = await this.db.getAccount(receiverAddress); - if (!receiverSession) { + //Retrieves the account of the receiver + const receiverAccount = await this.db.getAccount(receiverAddress); + if (!receiverAccount) { console.debug('unknown user ', deliveryInformation.to); - throw Error('unknown session'); + throw Error('unknown account'); } //Checks if the message is spam if ( await spamFilter.isSpam( this.provider, - receiverSession, + receiverAccount, deliveryInformation, ) ) { @@ -129,7 +129,7 @@ export class MessageProcessor { } const receiverEncryptionKey = - receiverSession.signedUserProfile.profile.publicEncryptionKey; + receiverAccount.signedUserProfile.profile.publicEncryptionKey; const envelopWithPostmark: EncryptionEnvelop = { ...envelop, @@ -158,11 +158,11 @@ export class MessageProcessor { //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!, + receiverAccount.socketId!, envelopWithPostmark, ); - console.debug('WS send to socketId ', receiverSession.socketId); + console.debug('WS send to socketId ', receiverAccount.socketId); //If not we're notifing the user that there is a new message waiting for them } else { try { @@ -174,6 +174,7 @@ export class MessageProcessor { deliveryInformation, this.db.getUsersNotificationChannels, ); + await this.db.countNotification(this.deliveryServiceProperties); } catch (err) { console.log( 'Unable to send Notification. There might be an error in the config.yml. Message has been received regardless', diff --git a/packages/delivery-service/src/messaging.test.ts b/packages/delivery-service/src/messaging.test.ts index cb458edba..1856c4c2e 100644 --- a/packages/delivery-service/src/messaging.test.ts +++ b/packages/delivery-service/src/messaging.test.ts @@ -1,4 +1,4 @@ -import { Session } from '@dm3-org/dm3-lib-delivery'; +import { Account } 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'; @@ -50,7 +50,7 @@ describe('Messaging', () => { const isReceiver = ethersHelper.formatAddress(address) === receiver.address; - const session = ( + const account = ( account: string, token: string, profile: UserProfile, @@ -64,11 +64,11 @@ describe('Messaging', () => { }); if (isSender) { - return session(sender.address, '123', emptyProfile); + return account(sender.address, '123', emptyProfile); } if (isReceiver) { - return session(receiver.address, 'abc', { + return account(receiver.address, 'abc', { ...emptyProfile, publicEncryptionKey: receiver.profileKeys.encryptionKeyPair.publicKey, @@ -198,14 +198,14 @@ describe('Messaging', () => { done(); }); - const session = async (addr: string) => { + const account = async (addr: string) => { return { ...(await getAccount(addr)), spamFilterRules: { minNonce: 2 }, - } as Session; + } as Account; }; const db = { - getAccount: session, + getAccount: account, createMessage: () => {}, getIdEnsName: async (ensName: string) => ensName, getUsersNotificationChannels: () => Promise.resolve([]), diff --git a/packages/delivery-service/src/messaging.ts b/packages/delivery-service/src/messaging.ts index bb6c2cf13..82f68ab2e 100644 --- a/packages/delivery-service/src/messaging.ts +++ b/packages/delivery-service/src/messaging.ts @@ -17,7 +17,7 @@ export function onConnection( ) { return (socket: Socket) => { socket.on('disconnect', () => { - global.logger.info({ + console.info({ method: 'WS DISCONNECT', socketId: socket.id, }); @@ -32,7 +32,7 @@ export function onConnection( socket.on('connection_error', (err) => { console.log(err.req); // the request object console.log(err.code); // the error code, for example 1 - console.log(err.message); // the error message, for example "Session ID unknown" + console.log(err.message); // the error message, for example "Account ID unknown" console.log(err.context); // some additional error context }); diff --git a/packages/delivery-service/src/metrics.ts b/packages/delivery-service/src/metrics.ts new file mode 100644 index 000000000..b86b0dbad --- /dev/null +++ b/packages/delivery-service/src/metrics.ts @@ -0,0 +1,45 @@ +import { DeliveryServiceProperties } from '@dm3-org/dm3-lib-delivery'; +import express from 'express'; +import { IDatabase } from './persistence/getDatabase'; +import { IntervalMetric } from './persistence/metrics/metricTypes'; + +/** + * The metrics endpoint returns the metrics of the delivery service. + * These can include the number of messages received, the cumulative size of messages received, etc., + * over a certain period of time. + * @param db The database object. + */ +export default ( + db: IDatabase, + deliveryServiceProperties: DeliveryServiceProperties, +) => { + const router = express.Router(); + router.get('', async (req, res) => { + const metrics: IntervalMetric[] = await db.getMetrics( + deliveryServiceProperties, + ); + + if (metrics.length === 0) { + return res.status(204).send('No metrics data available'); + } + + // Convert metrics to CSV format + const headers = Object.keys(metrics[0]); + const csvRows = metrics.map((metric) => + headers + .map((header) => metric[header as keyof IntervalMetric]) + .join(','), + ); + + const csvData = [headers.join(','), ...csvRows].join('\n'); + + res.setHeader('Content-Type', 'text/csv'); + // res.setHeader( + // 'Content-Disposition', + // 'attachment; filename=metrics.csv', + // ); + return res.status(200).send(csvData); + }); + + return router; +}; diff --git a/packages/delivery-service/src/notifications.ts b/packages/delivery-service/src/notifications.ts index 9a993cf01..6890d988d 100644 --- a/packages/delivery-service/src/notifications.ts +++ b/packages/delivery-service/src/notifications.ts @@ -2,7 +2,7 @@ import cors from 'cors'; import { normalizeEnsName } from '@dm3-org/dm3-lib-profile'; import express from 'express'; -import { authorize } from '@dm3-org/dm3-lib-server-side'; +import { authorizationMiddleware } from '@dm3-org/dm3-lib-server-side'; import { ethers } from 'ethers'; import { validateNewNotificationChannelData, @@ -35,7 +35,7 @@ export default ( // Adding a route parameter middleware named 'ensName' router.param('ensName', (req, res, next, ensName: string) => { - authorize( + authorizationMiddleware( req, res, next, diff --git a/packages/delivery-service/src/persistence/account/getAccount.ts b/packages/delivery-service/src/persistence/account/getAccount.ts index 35d76769c..623eda6ab 100644 --- a/packages/delivery-service/src/persistence/account/getAccount.ts +++ b/packages/delivery-service/src/persistence/account/getAccount.ts @@ -1,18 +1,18 @@ -import { Session } from '@dm3-org/dm3-lib-delivery'; +import { Account } from '@dm3-org/dm3-lib-delivery'; import { ethers } from 'ethers'; import { Redis, RedisPrefix } from '../getDatabase'; export function getAccount(redis: Redis) { return async (address: string) => { - const session = await redis.get( + const account = await redis.get( RedisPrefix.Account + ethers.utils.getAddress(address), ); - if (!session) { + if (!account) { console.debug('there is no account for this address: ', address); return null; } - return JSON.parse(session) as Session; + return JSON.parse(account) as Account; }; } diff --git a/packages/delivery-service/src/persistence/account/setAccount.test.ts b/packages/delivery-service/src/persistence/account/setAccount.test.ts index b5099c65b..974448551 100644 --- a/packages/delivery-service/src/persistence/account/setAccount.test.ts +++ b/packages/delivery-service/src/persistence/account/setAccount.test.ts @@ -1,10 +1,10 @@ import { Redis, IDatabase, getRedisClient, getDatabase } from '../getDatabase'; import { UserProfile } from '@dm3-org/dm3-lib-profile'; -import { Session } from '@dm3-org/dm3-lib-delivery'; +import { Account } from '@dm3-org/dm3-lib-delivery'; const USER_ADDRESS = '0x25A643B6e52864d0eD816F1E43c0CF49C83B8292'; -describe('Set Session', () => { +describe('Set Account', () => { let redisClient: Redis; let db: IDatabase; @@ -19,13 +19,13 @@ describe('Set Session', () => { await redisClient.disconnect(); }); - it('Creates a new Session ', async () => { + it('Creates a new Account ', async () => { const profile: UserProfile = { publicEncryptionKey: '', publicSigningKey: '', deliveryServices: [], }; - const session: Session = { + const account: Account = { account: USER_ADDRESS, signedUserProfile: { profile, signature: 'foo' }, token: '', @@ -35,25 +35,25 @@ describe('Set Session', () => { }, }; - const priorSetSession = await db.getAccount(USER_ADDRESS); - //User has no session yet - expect(priorSetSession).toBe(null); - await db.setAccount(USER_ADDRESS, session); + const priorSetAccount = await db.getAccount(USER_ADDRESS); + //User has no account yet + expect(priorSetAccount).toBe(null); + await db.setAccount(USER_ADDRESS, account); - const afterSetSession = await db.getAccount(USER_ADDRESS); - //User has no session yet - expect(afterSetSession?.signedUserProfile).toEqual({ + const afterSetAccount = await db.getAccount(USER_ADDRESS); + //User has no account yet + expect(afterSetAccount?.signedUserProfile).toEqual({ profile, signature: 'foo', }); }); - it('Creates a new Session and uses normalized address', async () => { + it('Creates a new Account and uses normalized address', async () => { const profile: UserProfile = { publicEncryptionKey: '', publicSigningKey: '', deliveryServices: [], }; - const session: Session = { + const account: Account = { // Address is not normalized account: USER_ADDRESS.toUpperCase(), signedUserProfile: { profile, signature: 'foo' }, @@ -64,26 +64,26 @@ describe('Set Session', () => { }, }; - const priorSetSession = await db.getAccount(USER_ADDRESS); - //User has no session yet - expect(priorSetSession).toBe(null); - await db.setAccount(USER_ADDRESS, session); + const priorSetAccount = await db.getAccount(USER_ADDRESS); + //User has no account yet + expect(priorSetAccount).toBe(null); + await db.setAccount(USER_ADDRESS, account); - const afterSetSession = await db.getAccount(USER_ADDRESS); - //User has no session yet - expect(afterSetSession?.signedUserProfile).toEqual({ + const afterSetAccount = await db.getAccount(USER_ADDRESS); + //User has no account yet + expect(afterSetAccount?.signedUserProfile).toEqual({ profile, signature: 'foo', }); }); - it('Rejects session with an invalid address', async () => { + it('Rejects account with an invalid address', async () => { const profile: UserProfile = { publicEncryptionKey: '', publicSigningKey: '', deliveryServices: [], }; - const session: Session = { + const account: Account = { account: USER_ADDRESS, signedUserProfile: { profile, @@ -96,20 +96,20 @@ describe('Set Session', () => { }, }; try { - await db.setAccount('foo', session); + await db.setAccount('foo', account); fail(); } catch (e) { expect(e).toStrictEqual(Error('Invalid address')); } }); - it('Rejects session with an invalid schema', async () => { - const invalidSession = {} as Session; + it('Rejects account with an invalid schema', async () => { + const invalidAccount = {} as Account; try { - await db.setAccount('foo', invalidSession); + await db.setAccount('foo', invalidAccount); fail(); } catch (e) { - expect(e).toStrictEqual(Error('Invalid session')); + expect(e).toStrictEqual(Error('Invalid account')); } }); }); diff --git a/packages/delivery-service/src/persistence/account/setAccount.ts b/packages/delivery-service/src/persistence/account/setAccount.ts index b380c6987..3aad4f3af 100644 --- a/packages/delivery-service/src/persistence/account/setAccount.ts +++ b/packages/delivery-service/src/persistence/account/setAccount.ts @@ -1,16 +1,16 @@ -import { Session, schema } from '@dm3-org/dm3-lib-delivery'; +import { Account, schema } from '@dm3-org/dm3-lib-delivery'; 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 (address: string, session: Session) => { - const isValid = validateSchema(schema.Session, session); + return async (address: string, Account: Account) => { + const isValid = validateSchema(schema.Account, Account); const isAddess = ethers.utils.isAddress(address); if (!isValid) { - console.debug('Invalid session: ', session); - throw Error('Invalid session'); + console.debug('Invalid account: ', Account); + throw Error('Invalid account'); } if (!isAddess) { @@ -18,11 +18,11 @@ export function setAccount(redis: Redis) { throw Error('Invalid address'); } - console.debug('set account ', address, session); + console.debug('set account ', address, Account); await redis.set( RedisPrefix.Account + ethers.utils.getAddress(address), - stringify(session), + stringify(Account), ); }; } diff --git a/packages/delivery-service/src/persistence/getDatabase.ts b/packages/delivery-service/src/persistence/getDatabase.ts index def35c006..2ad44acbd 100644 --- a/packages/delivery-service/src/persistence/getDatabase.ts +++ b/packages/delivery-service/src/persistence/getDatabase.ts @@ -1,4 +1,9 @@ -import { IGlobalNotification, IOtp, Session } from '@dm3-org/dm3-lib-delivery'; +import { + DeliveryServiceProperties, + IGlobalNotification, + IOtp, + type Account as AccountType, +} from '@dm3-org/dm3-lib-delivery'; import { EncryptionEnvelop } from '@dm3-org/dm3-lib-messaging'; import { IAccountDatabase } from '@dm3-org/dm3-lib-server-side'; import { @@ -10,6 +15,8 @@ import Account from './account'; import { getIdEnsName } from './getIdEnsName'; import Messages from './messages'; import { syncAcknowledge } from './messages/syncAcknowledge'; +import type { IntervalMetric } from './metrics'; +import Metrics from './metrics'; import Notification from './notification'; import Otp from './otp'; @@ -22,6 +29,9 @@ export enum RedisPrefix { NotificationChannel = 'notificationChannel:', GlobalNotification = 'globalNotification:', Otp = 'otp:', + MetricsMessageCount = 'metricsMessageCount:', + MetricsMessageSize = 'metricsMessageSize:', + MetricsNotificationCount = 'metricsNotificationCount:', } export async function getRedisClient() { @@ -53,12 +63,8 @@ export async function getRedisClient() { return client; } -export async function getDatabase( - _redis?: Redis, - // _prisma?: PrismaClient, -): Promise { +export async function getDatabase(_redis?: Redis): Promise { const redis = _redis ?? (await getRedisClient()); - // const prisma = _prisma ?? (await getPrismaClient()); return { //Messages @@ -90,12 +96,16 @@ export async function getDatabase( setOtp: Otp.setOtp(redis), getOtp: Otp.getOtp(redis), resetOtp: Otp.resetOtp(redis), + // Metrics + getMetrics: Metrics.getMetrics(redis), + countMessage: Metrics.countMessage(redis), + countNotification: Metrics.countNotification(redis), }; } export interface IDatabase extends IAccountDatabase { - setAccount: (address: string, session: Session) => Promise; - getAccount: (address: string) => Promise; + setAccount: (address: string, account: AccountType) => Promise; + getAccount: (address: string) => Promise; //TODO use address getIncomingMessages: ( address: string, @@ -156,6 +166,16 @@ export interface IDatabase extends IAccountDatabase { ensName: string, channelType: NotificationChannelType, ) => Promise; + getMetrics: ( + deliveryServiceProperties: DeliveryServiceProperties, + ) => Promise; + countMessage: ( + messageSizeBytes: number, + deliveryServiceProperties: DeliveryServiceProperties, + ) => Promise; + countNotification: ( + deliveryServiceProperties: DeliveryServiceProperties, + ) => Promise; } export type Redis = Awaited>; diff --git a/packages/delivery-service/src/persistence/messages/createMessage.test.ts b/packages/delivery-service/src/persistence/messages/createMessage.test.ts index 997fe2ee6..446fa3bed 100644 --- a/packages/delivery-service/src/persistence/messages/createMessage.test.ts +++ b/packages/delivery-service/src/persistence/messages/createMessage.test.ts @@ -1,5 +1,4 @@ import { Redis, IDatabase, getRedisClient, getDatabase } from '../getDatabase'; -import winston from 'winston'; import { DeliveryInformation, EncryptionEnvelop, @@ -8,16 +7,9 @@ import { const SENDER_ADDRESS = '0x25A643B6e52864d0eD816F1E43c0CF49C83B8292'; const RECEIVER_ADDRESS = '0xDd36ae7F9a8E34FACf1e110c6e9d37D0dc917855'; -global.logger = winston.createLogger({ - transports: [new winston.transports.Console()], -}); - describe('Create Message', () => { let redisClient: Redis; let db: IDatabase; - const logger = winston.createLogger({ - transports: [new winston.transports.Console()], - }); beforeEach(async () => { redisClient = await getRedisClient(); diff --git a/packages/delivery-service/src/persistence/messages/deleteExpiredMessages.test.ts b/packages/delivery-service/src/persistence/messages/deleteExpiredMessages.test.ts index 3b72bd2d9..0ad9e1b3f 100644 --- a/packages/delivery-service/src/persistence/messages/deleteExpiredMessages.test.ts +++ b/packages/delivery-service/src/persistence/messages/deleteExpiredMessages.test.ts @@ -1,10 +1,5 @@ -import winston from 'winston'; import { getDatabase, getRedisClient, IDatabase, Redis } from '../getDatabase'; -global.logger = winston.createLogger({ - transports: [new winston.transports.Console()], -}); - describe('Delete Expired messages', () => { let redisClient: Redis; let db: IDatabase; diff --git a/packages/delivery-service/src/persistence/messages/syncAcknowledge.test.ts b/packages/delivery-service/src/persistence/messages/syncAcknowledge.test.ts index 55699d00e..78ce24647 100644 --- a/packages/delivery-service/src/persistence/messages/syncAcknowledge.test.ts +++ b/packages/delivery-service/src/persistence/messages/syncAcknowledge.test.ts @@ -1,20 +1,12 @@ import { EncryptionEnvelop } from '@dm3-org/dm3-lib-messaging'; -import winston from 'winston'; import { IDatabase, Redis, getDatabase, getRedisClient } from '../getDatabase'; const SENDER_ADDRESS = '0x25A643B6e52864d0eD816F1E43c0CF49C83B8292'; const RECEIVER_ADDRESS = '0xDd36ae7F9a8E34FACf1e110c6e9d37D0dc917855'; -global.logger = winston.createLogger({ - transports: [new winston.transports.Console()], -}); - describe('Sync Acknowledge', () => { let redisClient: Redis; let db: IDatabase; - const logger = winston.createLogger({ - transports: [new winston.transports.Console()], - }); beforeEach(async () => { redisClient = await getRedisClient(); diff --git a/packages/delivery-service/src/persistence/metrics/getCurrentIntervalTimestamp.ts b/packages/delivery-service/src/persistence/metrics/getCurrentIntervalTimestamp.ts new file mode 100644 index 000000000..c82df004b --- /dev/null +++ b/packages/delivery-service/src/persistence/metrics/getCurrentIntervalTimestamp.ts @@ -0,0 +1,16 @@ +/** + * Get the timestamp of the current metrics collection interval. + * In order to have reproducible interval names, we use the start of the interval. This is defined + * as the timestamp of the first second of the interval, when counting full intervals from unix epoch. + * @param collectionIntervalInSeconds the duration over which all metrics are summarized + * @returns the timestamp of the current interval + */ +export function getCurrentIntervalTimestamp( + collectionIntervalInSeconds: number, +): string { + const currentDate = new Date(); + const timestamp = + Math.floor(currentDate.getTime() / 1000 / collectionIntervalInSeconds) * + collectionIntervalInSeconds; + return `${timestamp}`; +} diff --git a/packages/delivery-service/src/persistence/metrics/getMetrics.test.ts b/packages/delivery-service/src/persistence/metrics/getMetrics.test.ts new file mode 100644 index 000000000..9641fa518 --- /dev/null +++ b/packages/delivery-service/src/persistence/metrics/getMetrics.test.ts @@ -0,0 +1,119 @@ +import { getMetrics } from './getMetrics'; +import { Redis, RedisPrefix } from '../getDatabase'; + +describe('getMetrics', () => { + let mockRedis: jest.Mocked; + const mockDate = new Date('2023-04-01T12:00:00Z'); + //@ts-ignore + const mockDeliveryServiceProperties: DeliveryServiceProperties = { + metricsCollectionIntervalInSeconds: 3600, + metricsRetentionDurationInSeconds: 864000, + }; + + beforeEach(() => { + jest.useFakeTimers().setSystemTime(mockDate); + mockRedis = { + keys: jest.fn(), + get: jest.fn(), + } as unknown as jest.Mocked; + }); + + it('should return an empty array when no metrics are found', async () => { + mockRedis.keys.mockResolvedValue([]); + + const getMetricsFunc = getMetrics(mockRedis); + const result = await getMetricsFunc(mockDeliveryServiceProperties); + + expect(result).toEqual([]); + }); + + it('should return metrics for all available intervals', async () => { + const mockKeys = Array.from({ length: 24 }, (_, i) => { + const timestamp = 1680307200 - i * 3600; + return `${RedisPrefix.MetricsMessageCount}${timestamp}`; + }); + mockRedis.keys.mockResolvedValue(mockKeys); + mockRedis.get.mockImplementation((key: string) => { + if (key.includes(RedisPrefix.MetricsMessageCount)) + return Promise.resolve('10'); + if (key.includes(RedisPrefix.MetricsMessageSize)) + return Promise.resolve('1000'); + if (key.includes(RedisPrefix.MetricsNotificationCount)) + return Promise.resolve('5'); + return Promise.resolve(null); + }); + + const getMetricsFunction = getMetrics(mockRedis); + const result = await getMetricsFunction(mockDeliveryServiceProperties); + + expect(mockRedis.keys).toHaveBeenCalledWith( + `${RedisPrefix.MetricsMessageCount}*`, + ); + expect(result).toHaveLength(24); + expect(result[0]).toEqual({ + timestampStart: 1680307200, + durationSeconds: 3600, + messageCount: 10, + messageSizeBytes: 1000, + notificationCount: 5, + }); + }); + + it('should handle missing data', async () => { + mockRedis.keys.mockResolvedValue([ + `${RedisPrefix.MetricsMessageCount}1680307200`, + ]); + //@ts-ignore + mockRedis.get.mockImplementation((key) => { + console.log('key', key); + //@ts-ignore + const [prefix] = key.split(':'); + switch (prefix + ':') { + case RedisPrefix.MetricsMessageCount: + return Promise.resolve('10'); + case RedisPrefix.MetricsMessageSize: + return Promise.resolve(null); + case RedisPrefix.MetricsNotificationCount: + return Promise.resolve('5'); + default: + return Promise.resolve(null); + } + }); + + const getMetricsFunction = getMetrics(mockRedis); + const result = await getMetricsFunction(mockDeliveryServiceProperties); + console.log(result); + + expect(result).toHaveLength(1); + expect(result[0]).toEqual({ + timestampStart: 1680307200, + durationSeconds: 3600, + messageCount: 10, + messageSizeBytes: 0, + notificationCount: 5, + }); + }); + + it('should censor the current interval', async () => { + const currentTimestamp = Math.floor(mockDate.getTime() / 1000); + const mockKeys = [ + `${RedisPrefix.MetricsMessageCount}${currentTimestamp}`, + `${RedisPrefix.MetricsMessageCount}${currentTimestamp - 3600}`, + ]; + mockRedis.keys.mockResolvedValue(mockKeys); + mockRedis.get.mockResolvedValue('10'); + + const getMetricsFunction = getMetrics(mockRedis); + const result = await getMetricsFunction(mockDeliveryServiceProperties); + + expect(result).toHaveLength(1); + expect(result[0].timestampStart).toBe(currentTimestamp - 3600); // 2023-04-01T11:00:00.000Z + expect( + result.every( + (metric) => + metric.timestampStart !== + Math.floor(mockDate.getTime() / 1000), + ), + ).toBe(true); + }); +}); diff --git a/packages/delivery-service/src/persistence/metrics/getMetrics.ts b/packages/delivery-service/src/persistence/metrics/getMetrics.ts new file mode 100644 index 000000000..c54344ece --- /dev/null +++ b/packages/delivery-service/src/persistence/metrics/getMetrics.ts @@ -0,0 +1,57 @@ +import { DeliveryServiceProperties } from '@dm3-org/dm3-lib-delivery'; +import { Redis, RedisPrefix } from '../getDatabase'; +import { getCurrentIntervalTimestamp } from './getCurrentIntervalTimestamp'; +import { IntervalMetric } from './metricTypes'; + +/** + * Get the metrics from the database, excluding the current interval. + * (The current interval is excluded because monitoring it closely allows + * third parties to gain too much information about the current activity on the + * DM3 network.) + * @param redis - The Redis instance. + * @returns The metrics object. + */ +export function getMetrics( + redis: Redis, +): ( + deliveryServiceProperties: DeliveryServiceProperties, +) => Promise { + return async (deliveryServiceProperties: DeliveryServiceProperties) => { + const metrics: IntervalMetric[] = []; + const messageCountKeys = await redis.keys( + `${RedisPrefix.MetricsMessageCount}*`, + ); + + const currentTimestamp = getCurrentIntervalTimestamp( + deliveryServiceProperties.metricsCollectionIntervalInSeconds, + ); + + for (const key of messageCountKeys) { + const timestamp = parseInt(key.split(':')[1], 10); + + // Skip the current interval + if (String(timestamp) === currentTimestamp) { + continue; + } + + const messageCount = await redis.get(key); + const messageSizeBytes = await redis.get( + `${RedisPrefix.MetricsMessageSize}${timestamp}`, + ); + const notificationCount = await redis.get( + `${RedisPrefix.MetricsNotificationCount}${timestamp}`, + ); + + metrics.push({ + timestampStart: timestamp, + durationSeconds: + deliveryServiceProperties.metricsCollectionIntervalInSeconds, + messageCount: parseInt(messageCount || '0', 10), + messageSizeBytes: parseInt(messageSizeBytes || '0', 10), + notificationCount: parseInt(notificationCount || '0', 10), + }); + } + + return metrics; + }; +} diff --git a/packages/delivery-service/src/persistence/metrics/index.ts b/packages/delivery-service/src/persistence/metrics/index.ts new file mode 100644 index 000000000..769910d0a --- /dev/null +++ b/packages/delivery-service/src/persistence/metrics/index.ts @@ -0,0 +1,11 @@ +import { getMetrics } from './getMetrics'; +import type { IntervalMetric } from './metricTypes'; +import { countMessage, countNotification } from './setMetrics'; + +export default { + getMetrics, + countMessage, + countNotification, +}; + +export type { IntervalMetric }; diff --git a/packages/delivery-service/src/persistence/metrics/metricTypes.ts b/packages/delivery-service/src/persistence/metrics/metricTypes.ts new file mode 100644 index 000000000..7c11cb1df --- /dev/null +++ b/packages/delivery-service/src/persistence/metrics/metricTypes.ts @@ -0,0 +1,7 @@ +export type IntervalMetric = { + timestampStart: number; + durationSeconds: number; + messageCount: number; + messageSizeBytes: number; + notificationCount: number; +}; diff --git a/packages/delivery-service/src/persistence/metrics/setMetrics.test.ts b/packages/delivery-service/src/persistence/metrics/setMetrics.test.ts new file mode 100644 index 000000000..d0157131d --- /dev/null +++ b/packages/delivery-service/src/persistence/metrics/setMetrics.test.ts @@ -0,0 +1,112 @@ +import { Redis, RedisPrefix } from '../getDatabase'; +import { countMessage, countNotification } from './setMetrics'; +import { DeliveryServiceProperties } from '@dm3-org/dm3-lib-delivery'; + +describe('setMetrics', () => { + let mockRedis: jest.Mocked; + const mockDate = new Date('2023-04-01T12:00:00Z'); + //@ts-ignore + const mockDeliveryServiceProperties: DeliveryServiceProperties = { + metricsCollectionIntervalInSeconds: 3600, + metricsRetentionDurationInSeconds: 864000, + }; + + const expectedTimestamp = '1680350400'; + + beforeEach(() => { + jest.useFakeTimers().setSystemTime(mockDate); + mockRedis = { + incrBy: jest.fn().mockResolvedValue(1), + expire: jest.fn().mockResolvedValue(1), + } as unknown as jest.Mocked; + }); + + afterEach(() => { + jest.useRealTimers(); + }); + + describe('countMessage', () => { + it('should increment message count and size, and set expiration', async () => { + const messageSizeBytes = 100; + + const countMessageFunction = countMessage(mockRedis); + + await countMessageFunction( + messageSizeBytes, + mockDeliveryServiceProperties, + ); + + expect(mockRedis.incrBy).toHaveBeenCalledTimes(2); + expect(mockRedis.incrBy).toHaveBeenCalledWith( + `${RedisPrefix.MetricsMessageCount}${expectedTimestamp}`, + 1, + ); + expect(mockRedis.incrBy).toHaveBeenCalledWith( + `${RedisPrefix.MetricsMessageSize}${expectedTimestamp}`, + messageSizeBytes, + ); + + expect(mockRedis.expire).toHaveBeenCalledTimes(2); + expect(mockRedis.expire).toHaveBeenCalledWith( + `${RedisPrefix.MetricsMessageCount}${expectedTimestamp}`, + mockDeliveryServiceProperties.metricsRetentionDurationInSeconds, + ); + expect(mockRedis.expire).toHaveBeenCalledWith( + `${RedisPrefix.MetricsMessageSize}${expectedTimestamp}`, + mockDeliveryServiceProperties.metricsRetentionDurationInSeconds, + ); + }); + + it('should not collect metrics when retention interval is 0', async () => { + const messageSizeBytes = 100; + const countMessageFunction = countMessage(mockRedis); + + const propertiesWithZeroRetention = { + ...mockDeliveryServiceProperties, + metricsRetentionDurationInSeconds: 0, + }; + + await countMessageFunction( + messageSizeBytes, + propertiesWithZeroRetention, + ); + + expect(mockRedis.incrBy).not.toHaveBeenCalled(); + expect(mockRedis.expire).not.toHaveBeenCalled(); + }); + }); + + describe('countNotification', () => { + it('should increment notification count and set expiration', async () => { + const countNotificationFunction = countNotification(mockRedis); + + await countNotificationFunction(mockDeliveryServiceProperties); + + expect(mockRedis.incrBy).toHaveBeenCalledTimes(1); + expect(mockRedis.incrBy).toHaveBeenCalledWith( + `${RedisPrefix.MetricsNotificationCount}${expectedTimestamp}`, + 1, + ); + + expect(mockRedis.expire).toHaveBeenCalledTimes(1); + expect(mockRedis.expire).toHaveBeenCalledWith( + `${RedisPrefix.MetricsNotificationCount}${expectedTimestamp}`, + mockDeliveryServiceProperties.metricsRetentionDurationInSeconds, + ); + }); + + it('should not collect metrics when retention interval is 0', async () => { + const countNotificationFunction = countNotification(mockRedis); + + const propertiesWithZeroRetention = { + ...mockDeliveryServiceProperties, + metricsRetentionDurationInSeconds: 0, + }; + + await countNotificationFunction(propertiesWithZeroRetention); + + expect(mockRedis.incrBy).not.toHaveBeenCalled(); + expect(mockRedis.expire).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/packages/delivery-service/src/persistence/metrics/setMetrics.ts b/packages/delivery-service/src/persistence/metrics/setMetrics.ts new file mode 100644 index 000000000..256d056bc --- /dev/null +++ b/packages/delivery-service/src/persistence/metrics/setMetrics.ts @@ -0,0 +1,74 @@ +import { Redis, RedisPrefix } from '../getDatabase'; +import { DeliveryServiceProperties } from '@dm3-org/dm3-lib-delivery'; +import { getCurrentIntervalTimestamp } from './getCurrentIntervalTimestamp'; + +export function countMessage(redis: Redis) { + return async ( + messageSizeBytes: number, + deliveryServiceProperties: DeliveryServiceProperties, + ) => { + if (deliveryServiceProperties.metricsRetentionDurationInSeconds <= 0) { + // Metrics are disabled + return; + } + + const timestamp = getCurrentIntervalTimestamp( + deliveryServiceProperties.metricsCollectionIntervalInSeconds, + ); + + console.log( + 'countMessage at', + timestamp, + ' with size ', + messageSizeBytes, + ); + + // Increment the message count, starting at 0 if the key doesn't exist + await redis.incrBy(`${RedisPrefix.MetricsMessageCount}${timestamp}`, 1); + + // Increment the message size bytes, starting at 0 if the key doesn't exist + await redis.incrBy( + `${RedisPrefix.MetricsMessageSize}${timestamp}`, + messageSizeBytes, + ); + + // Set expiration. After this time the metrics are automatically deleted by redis. + await redis.expire( + `${RedisPrefix.MetricsMessageCount}${timestamp}`, + deliveryServiceProperties.metricsRetentionDurationInSeconds, + ); + await redis.expire( + `${RedisPrefix.MetricsMessageSize}${timestamp}`, + deliveryServiceProperties.metricsRetentionDurationInSeconds, + ); + + console.log('countMessage of size', messageSizeBytes, 'at', timestamp); + }; +} + +export function countNotification(redis: Redis) { + return async (deliveryServiceProperties: DeliveryServiceProperties) => { + if (deliveryServiceProperties.metricsRetentionDurationInSeconds <= 0) { + // Metrics are disabled + return; + } + + const timestamp = getCurrentIntervalTimestamp( + deliveryServiceProperties.metricsCollectionIntervalInSeconds, + ); + + // Increment the notification count, starting at 0 if the key doesn't exist + await redis.incrBy( + `${RedisPrefix.MetricsNotificationCount}${timestamp}`, + 1, + ); + + // Set expiration. After this time the metrics are automatically deleted by redis. + await redis.expire( + `${RedisPrefix.MetricsNotificationCount}${timestamp}`, + deliveryServiceProperties.metricsRetentionDurationInSeconds, + ); + + console.log('countNotification at', timestamp); + }; +} diff --git a/packages/delivery-service/src/persistence/notification/addUsersNotificationChannel.test.ts b/packages/delivery-service/src/persistence/notification/addUsersNotificationChannel.test.ts index b709d2ae2..2c1cfdde6 100644 --- a/packages/delivery-service/src/persistence/notification/addUsersNotificationChannel.test.ts +++ b/packages/delivery-service/src/persistence/notification/addUsersNotificationChannel.test.ts @@ -3,14 +3,9 @@ import { NotificationChannelType, } from '@dm3-org/dm3-lib-shared'; import { IDatabase, Redis, getDatabase, getRedisClient } from '../getDatabase'; -import winston from 'winston'; const USER_ADDRESS = '0x25A643B6e52864d0eD816F1E43c0CF49C83B8292'; -global.logger = winston.createLogger({ - transports: [new winston.transports.Console()], -}); - describe('Set Users NotificationChannel', () => { let redisClient: Redis; let db: IDatabase; @@ -37,14 +32,14 @@ describe('Set Users NotificationChannel', () => { const priorSetUsersNotificationChannel = await db.getUsersNotificationChannels(USER_ADDRESS); - //User has no session yet + //User has no account yet expect(priorSetUsersNotificationChannel).toEqual([]); await db.addUsersNotificationChannel(USER_ADDRESS, notificationChannel); - const afterSetSession = await db.getUsersNotificationChannels( + const afterSetAccount = await db.getUsersNotificationChannels( USER_ADDRESS, ); - expect(afterSetSession).toEqual([notificationChannel]); + expect(afterSetAccount).toEqual([notificationChannel]); }); it('Rejects Notification Channel with an invalid schema', async () => { @@ -121,14 +116,14 @@ describe('Set Users NotificationChannel', () => { const priorSetUsersNotificationChannel = await db.getUsersNotificationChannels(USER_ADDRESS); - //User has no session yet + //User has no account yet expect(priorSetUsersNotificationChannel).toEqual([]); await db.addUsersNotificationChannel(USER_ADDRESS, notificationChannel); - const afterSetSession = await db.getUsersNotificationChannels( + const afterSetAccount = await db.getUsersNotificationChannels( USER_ADDRESS, ); - expect(afterSetSession).toEqual([notificationChannel]); + expect(afterSetAccount).toEqual([notificationChannel]); }); it('Rejects Push Notification Channel with an invalid schema', async () => { diff --git a/packages/delivery-service/src/persistence/notification/enableOrDisableNotificationChannel.test.ts b/packages/delivery-service/src/persistence/notification/enableOrDisableNotificationChannel.test.ts index 6cba567ba..d80256020 100644 --- a/packages/delivery-service/src/persistence/notification/enableOrDisableNotificationChannel.test.ts +++ b/packages/delivery-service/src/persistence/notification/enableOrDisableNotificationChannel.test.ts @@ -3,14 +3,9 @@ import { NotificationChannel, } from '@dm3-org/dm3-lib-shared'; import { IDatabase, Redis, getDatabase, getRedisClient } from '../getDatabase'; -import winston from 'winston'; const USER_ADDRESS = '0x25A643B6e52864d0eD816F1E43c0CF49C83B8292'; -global.logger = winston.createLogger({ - transports: [new winston.transports.Console()], -}); - describe('Enables/Disables users EMAIL notification channel', () => { let redisClient: Redis; let db: IDatabase; diff --git a/packages/delivery-service/src/persistence/notification/removeNotificationChannel.test.ts b/packages/delivery-service/src/persistence/notification/removeNotificationChannel.test.ts index 3d5ba80cc..959f95c36 100644 --- a/packages/delivery-service/src/persistence/notification/removeNotificationChannel.test.ts +++ b/packages/delivery-service/src/persistence/notification/removeNotificationChannel.test.ts @@ -1,4 +1,3 @@ -import winston from 'winston'; import { IDatabase, Redis, getDatabase, getRedisClient } from '../getDatabase'; import { NotificationChannelType, @@ -7,10 +6,6 @@ import { const USER_ADDRESS = '0x25A643B6e52864d0eD816F1E43c0CF49C83B8292'; -global.logger = winston.createLogger({ - transports: [new winston.transports.Console()], -}); - describe('Removes EMAIL notification channel', () => { let redisClient: Redis; let db: IDatabase; diff --git a/packages/delivery-service/src/persistence/notification/setGlobalNotification.test.ts b/packages/delivery-service/src/persistence/notification/setGlobalNotification.test.ts index 5db27f699..43fe20807 100644 --- a/packages/delivery-service/src/persistence/notification/setGlobalNotification.test.ts +++ b/packages/delivery-service/src/persistence/notification/setGlobalNotification.test.ts @@ -1,12 +1,7 @@ import { IDatabase, Redis, getDatabase, getRedisClient } from '../getDatabase'; -import winston from 'winston'; const USER_ADDRESS = '0x25A643B6e52864d0eD816F1E43c0CF49C83B8292'; -global.logger = winston.createLogger({ - transports: [new winston.transports.Console()], -}); - describe('Set Users Global Notification', () => { let redisClient: Redis; let db: IDatabase; diff --git a/packages/delivery-service/src/persistence/notification/setNotificationChannelAsVerified.test.ts b/packages/delivery-service/src/persistence/notification/setNotificationChannelAsVerified.test.ts index 4f1a3dcd6..393b71555 100644 --- a/packages/delivery-service/src/persistence/notification/setNotificationChannelAsVerified.test.ts +++ b/packages/delivery-service/src/persistence/notification/setNotificationChannelAsVerified.test.ts @@ -1,4 +1,3 @@ -import winston from 'winston'; import { IDatabase, Redis, getDatabase, getRedisClient } from '../getDatabase'; import { NotificationChannelType, @@ -7,10 +6,6 @@ import { const USER_ADDRESS = '0x25A643B6e52864d0eD816F1E43c0CF49C83B8292'; -global.logger = winston.createLogger({ - transports: [new winston.transports.Console()], -}); - describe('Set users EMAIL notification channel as verified', () => { let redisClient: Redis; let db: IDatabase; diff --git a/packages/delivery-service/src/persistence/otp/resetOtp.test.ts b/packages/delivery-service/src/persistence/otp/resetOtp.test.ts index ae3f5abcd..93627686a 100644 --- a/packages/delivery-service/src/persistence/otp/resetOtp.test.ts +++ b/packages/delivery-service/src/persistence/otp/resetOtp.test.ts @@ -1,13 +1,8 @@ import { NotificationChannelType } from '@dm3-org/dm3-lib-shared'; import { IDatabase, Redis, getDatabase, getRedisClient } from '../getDatabase'; -import winston from 'winston'; const USER_ADDRESS = '0x25A643B6e52864d0eD816F1E43c0CF49C83B8292'; -global.logger = winston.createLogger({ - transports: [new winston.transports.Console()], -}); - describe('Reset OTP', () => { let redisClient: Redis; let db: IDatabase; diff --git a/packages/delivery-service/src/persistence/otp/setOtp.test.ts b/packages/delivery-service/src/persistence/otp/setOtp.test.ts index a3e99af3a..f79ba54a9 100644 --- a/packages/delivery-service/src/persistence/otp/setOtp.test.ts +++ b/packages/delivery-service/src/persistence/otp/setOtp.test.ts @@ -1,13 +1,8 @@ import { NotificationChannelType } from '@dm3-org/dm3-lib-shared'; import { IDatabase, Redis, getDatabase, getRedisClient } from '../getDatabase'; -import winston from 'winston'; const USER_ADDRESS = '0x25A643B6e52864d0eD816F1E43c0CF49C83B8292'; -global.logger = winston.createLogger({ - transports: [new winston.transports.Console()], -}); - describe('Email Verification OTP', () => { let redisClient: Redis; let db: IDatabase; diff --git a/packages/delivery-service/src/profile/profile.test.ts b/packages/delivery-service/src/profile/profile.test.ts index 1b679d6ca..eb15d709c 100644 --- a/packages/delivery-service/src/profile/profile.test.ts +++ b/packages/delivery-service/src/profile/profile.test.ts @@ -1,4 +1,4 @@ -import { Session } from '@dm3-org/dm3-lib-delivery'; +import { Account } from '@dm3-org/dm3-lib-delivery'; import { generateAuthJWT } from '@dm3-org/dm3-lib-server-side'; import { UserProfile, @@ -27,16 +27,16 @@ const setUpApp = async ( }; const createDbMock = async () => { - const sessionMocked = { + const accountMocked = { challenge: '123', token: 'deprecated token that is not used anymore', signedUserProfile: {}, - } as Session; + } as Account; const dbMock = { getAccount: async (ensName: string) => - Promise.resolve(sessionMocked), // returns some valid session - setAccount: async (_: string, __: Session) => {}, + Promise.resolve(accountMocked), // returns some valid account + setAccount: async (_: string, __: Account) => {}, getIdEnsName: async (ensName: string) => ensName, }; diff --git a/packages/delivery-service/src/rpc/methods/handleGetDeliveryServiceProperties.ts b/packages/delivery-service/src/rpc/methods/handleGetDeliveryServiceProperties.ts index dd2454bae..691eb06c5 100644 --- a/packages/delivery-service/src/rpc/methods/handleGetDeliveryServiceProperties.ts +++ b/packages/delivery-service/src/rpc/methods/handleGetDeliveryServiceProperties.ts @@ -7,10 +7,9 @@ export function handleGetDeliveryServiceProperties( deliveryServiceProperties: DeliveryServiceProperties, ) { if (!deliveryServiceProperties) { - global.logger.error({ - method: 'RPC GET DELIVERY SERVICE PROPERTIES', - error: 'No Delivery Service Properties Set', - }); + console.error( + 'RPC GET DELIVERY SERVICE PROPERTIES: No Delivery Service Properties Set', + ); return res.status(400).send({ jsonrpc: '2.0', result: 'No Delivery Service Properties Set', diff --git a/packages/delivery-service/src/rpc/methods/handleResolveProfileExtension.ts b/packages/delivery-service/src/rpc/methods/handleResolveProfileExtension.ts index ec5577a84..25d59f8ef 100644 --- a/packages/delivery-service/src/rpc/methods/handleResolveProfileExtension.ts +++ b/packages/delivery-service/src/rpc/methods/handleResolveProfileExtension.ts @@ -10,20 +10,20 @@ export function handleResolveProfileExtension(axios: Axios, db: IDatabase) { } = req.body; const idEnsName = await db.getIdEnsName(ensName); - //Get the Session to retrieve profileExtension - const session = await db.getAccount(idEnsName); + //Get the Account to retrieve profileExtension + const account = await db.getAccount(idEnsName); - if (!session) { + if (!account) { //The requested ens-name is not known to the delivery service const error = 'unknown user'; - global.logger.warn({ + console.warn({ method: 'RPC - RESOLVE PROFILE', error, }); return res.status(400).send({ error }); } - const { profileExtension } = session; + const { profileExtension } = account; return res.status(200).send({ jsonrpc: '2.0', diff --git a/packages/delivery-service/src/rpc/methods/handleSubmitMessage.ts b/packages/delivery-service/src/rpc/methods/handleSubmitMessage.ts index 96d7d3546..2df50d000 100644 --- a/packages/delivery-service/src/rpc/methods/handleSubmitMessage.ts +++ b/packages/delivery-service/src/rpc/methods/handleSubmitMessage.ts @@ -1,5 +1,9 @@ import { DeliveryServiceProperties } from '@dm3-org/dm3-lib-delivery'; -import { EncryptionEnvelop, schema } from '@dm3-org/dm3-lib-messaging'; +import { + EncryptionEnvelop, + getEnvelopSize, + schema, +} from '@dm3-org/dm3-lib-messaging'; import { DeliveryServiceProfileKeys } from '@dm3-org/dm3-lib-profile'; import { IWebSocketManager, @@ -50,7 +54,7 @@ export async function handleSubmitMessage( if (!isSchemaValid) { const error = 'invalid schema'; - global.logger.warn({ + console.warn({ method: 'WS SUBMIT MESSAGE', error, }); @@ -70,6 +74,10 @@ export async function handleSubmitMessage( try { await messageProcessor.processEnvelop(envelop); + await db.countMessage( + getEnvelopSize(envelop), + deliveryServiceProperties, + ); res.sendStatus(200); } catch (error) { console.error('handle submit message error'); diff --git a/packages/delivery-service/src/rpc/rpc-proxy.test.ts b/packages/delivery-service/src/rpc/rpc-proxy.test.ts index ff19d50da..4bfd5a897 100644 --- a/packages/delivery-service/src/rpc/rpc-proxy.test.ts +++ b/packages/delivery-service/src/rpc/rpc-proxy.test.ts @@ -16,14 +16,10 @@ import bodyParser from 'body-parser'; import { ethers } from 'ethers'; import express from 'express'; import request from 'supertest'; -import winston from 'winston'; + import RpcProxy from './rpc-proxy'; import { EncryptionEnvelop } from '@dm3-org/dm3-lib-messaging'; -global.logger = winston.createLogger({ - transports: [new winston.transports.Console()], -}); - const mockWsManager: IWebSocketManager = { isConnected: function (ensName: string): Promise { return Promise.resolve(false); @@ -65,7 +61,7 @@ describe('rpc-Proxy', () => { const isReceiver = ethersHelper.formatAddress(address) === receiver.address; - const session = ( + const account = ( account: string, token: string, profile: UserProfile, @@ -79,11 +75,11 @@ describe('rpc-Proxy', () => { }); if (isSender) { - return session(sender.address, '123', emptyProfile); + return account(sender.address, '123', emptyProfile); } if (isReceiver) { - return session(RECEIVER_NAME, 'abc', { + return account(RECEIVER_NAME, 'abc', { ...emptyProfile, publicEncryptionKey: receiver.profileKeys.encryptionKeyPair.publicKey, @@ -158,6 +154,7 @@ describe('rpc-Proxy', () => { getAccount, getIdEnsName: async (ensName: string) => ensName, getUsersNotificationChannels: () => Promise.resolve([]), + countMessage: () => {}, }; const io = { sockets: { @@ -231,6 +228,7 @@ describe('rpc-Proxy', () => { getAccount, getIdEnsName: async (ensName: string) => ensName, getUsersNotificationChannels: () => Promise.resolve([]), + countMessage: () => {}, }; const io = { sockets: { diff --git a/packages/delivery-service/src/socketAuth.ts b/packages/delivery-service/src/socketAuth.ts index 51d2cd8c5..e478e4ab0 100644 --- a/packages/delivery-service/src/socketAuth.ts +++ b/packages/delivery-service/src/socketAuth.ts @@ -33,15 +33,15 @@ export function socketAuth( console.log('check token has failed for WS '); return next(new Error('check token has failed for WS')); } - const session = await db.getAccount(ensName); - if (!session) { - throw Error('Could not get session'); + const account = await db.getAccount(ensName); + if (!account) { + throw Error('Could not get account'); } - //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. + //we use account.account here as a key for setAccount here. + //We can do this because the address is used as account when the Account has been created. //That saves a address lookup via ENS - await db.setAccount(session.account, { - ...session, + await db.setAccount(account.account, { + ...account, socketId: socket.id, }); } catch (e) { diff --git a/packages/delivery-service/src/ws/WebSocketManager.test.ts b/packages/delivery-service/src/ws/WebSocketManager.test.ts index bf65316f8..a075cea41 100644 --- a/packages/delivery-service/src/ws/WebSocketManager.test.ts +++ b/packages/delivery-service/src/ws/WebSocketManager.test.ts @@ -94,7 +94,7 @@ describe('WebSocketManager', () => { expect(socket0IsConnected).toBe(false); }); - it('reject socket without session', async () => { + it('reject socket without account', async () => { const mockedWeb3Provider = { resolveName: (_: string) => Promise.resolve('0x'), } as any; @@ -173,7 +173,7 @@ describe('WebSocketManager', () => { }); }); describe('isConnected', () => { - it('returns true if name has one session', async () => { + it('returns true if name has one account', async () => { const mockedWeb3Provider = { resolveName: (_: string) => Promise.resolve(receiver.address), } as any; @@ -222,7 +222,7 @@ describe('WebSocketManager', () => { const isConnected = await wsManager.isConnected(receiver.address); expect(isConnected).toBe(true); }); - it('returns true if name has at least one session', async () => { + it('returns true if name has at least one account', async () => { const mockedWeb3Provider = { resolveName: (_: string) => Promise.resolve(receiver.address), } as any; @@ -345,7 +345,7 @@ describe('WebSocketManager', () => { const isConnected = await wsManager.isConnected(rando.address); expect(isConnected).toBe(false); }); - it('keeps track of different independent sessions', async () => { + it('keeps track of different independent accounts', async () => { const mockedWeb3Provider = { resolveName: (_: string) => { if (_ === receiver.account.ensName) { diff --git a/packages/delivery-service/src/ws/WebSocketManager.ts b/packages/delivery-service/src/ws/WebSocketManager.ts index 3b86d6638..b3e1bbc36 100644 --- a/packages/delivery-service/src/ws/WebSocketManager.ts +++ b/packages/delivery-service/src/ws/WebSocketManager.ts @@ -54,38 +54,38 @@ export class WebSocketManager implements IWebSocketManager { */ private async addConnection(connection: Socket) { try { - const { account, token } = connection.handshake.auth; + const { account: accountInfo, token } = connection.handshake.auth; - const ensName = normalizeEnsName(account.ensName); + const ensName = normalizeEnsName(accountInfo.ensName); //Use the already existing function checkToken to check whether the token matches the provided ensName - const hasSession = await checkToken( + const hasAccount = await checkToken( this.web3Provider, this.db.hasAccount, ensName, token, this.serverSecret, ); - //retrieve the session from the db - const session = await this.db.getAccount(ensName); - //If the ensName has not a valid session we disconnect the socket - if (!hasSession || !session) { + //retrieve the account from the db + const account = await this.db.getAccount(ensName); + //If the ensName has not a valid account we disconnect the socket + if (!hasAccount || !account) { console.log('connection refused for ', ensName); connection.emit(UNAUTHORIZED); connection.disconnect(); return; } //Get the old connections and add the new one - const oldConnections = this.connections.get(session.account) || []; - this.connections.set(session.account, [ + const oldConnections = this.connections.get(account.account) || []; + this.connections.set(account.account, [ ...oldConnections, connection, ]); //Send the authorized event connection.emit(AUTHORIZED); - console.log('connection established for ', session.account); + console.log('connection established for ', account.account); //When the socket disconnects we want them no longer in our connections List connection.on('disconnect', () => { - console.log('connection closed for ', session.account); + console.log('connection closed for ', account.account); this.removeConnection(connection); }); } catch (e) { diff --git a/packages/integration-tests/package.json b/packages/integration-tests/package.json index ea4bd0179..1f8167fca 100644 --- a/packages/integration-tests/package.json +++ b/packages/integration-tests/package.json @@ -1,7 +1,7 @@ { "name": "@dm3-org/dm3-integration-tests", "license": "BSD-2-Clause", - "version": "1.6.1", + "version": "1.7.0", "dependencies": { "@dm3-org/dm3-lib-delivery": "workspace:^", "@dm3-org/dm3-lib-messaging": "workspace:^", diff --git a/packages/lib/billboard-api/package.json b/packages/lib/billboard-api/package.json index 0c29b4222..3ded82ba1 100644 --- a/packages/lib/billboard-api/package.json +++ b/packages/lib/billboard-api/package.json @@ -1,7 +1,7 @@ { "name": "@dm3-org/dm3-lib-billboard-client-api", "license": "BSD-2-Clause", - "version": "1.6.1", + "version": "1.7.0", "main": "dist/index.js", "types": "dist/index.d.ts", "exports": { diff --git a/packages/lib/crypto/package.json b/packages/lib/crypto/package.json index ecc4a3490..820d8400c 100644 --- a/packages/lib/crypto/package.json +++ b/packages/lib/crypto/package.json @@ -1,7 +1,7 @@ { "name": "@dm3-org/dm3-lib-crypto", "license": "BSD-2-Clause", - "version": "1.6.1", + "version": "1.7.0", "main": "dist/index.js", "module": "dist-backend/index.js", "types": "dist/index.d.ts", diff --git a/packages/lib/delivery-api/package.json b/packages/lib/delivery-api/package.json index 6109a2a11..e62691f4e 100644 --- a/packages/lib/delivery-api/package.json +++ b/packages/lib/delivery-api/package.json @@ -1,7 +1,7 @@ { "name": "@dm3-org/dm3-lib-delivery-api", "license": "BSD-2-Clause", - "version": "1.6.1", + "version": "1.7.0", "main": "dist/index.js", "types": "dist/index.d.ts", "exports": { diff --git a/packages/lib/delivery/package.json b/packages/lib/delivery/package.json index 666703064..0e484db41 100644 --- a/packages/lib/delivery/package.json +++ b/packages/lib/delivery/package.json @@ -1,7 +1,7 @@ { "name": "@dm3-org/dm3-lib-delivery", "license": "BSD-2-Clause", - "version": "1.6.1", + "version": "1.7.0", "main": "dist/index.js", "module": "dist-backend/index.js", "types": "dist/index.d.ts", diff --git a/packages/lib/delivery/schemas.sh b/packages/lib/delivery/schemas.sh index 5a7aa09d4..6e76b7c52 100644 --- a/packages/lib/delivery/schemas.sh +++ b/packages/lib/delivery/schemas.sh @@ -1,4 +1,4 @@ yarn ts-json-schema-generator -f tsconfig.json --path Delivery.ts --type DeliveryServiceProperties -o ./src/schema/DeliveryServiceProperties.schema.json --no-type-check \ && yarn ts-json-schema-generator -f tsconfig.json --path Messages.ts --type Acknowledgement -o ./src/schema/Acknowledgement.schema.json --no-type-check \ -&& yarn ts-json-schema-generator -f tsconfig.json --path Session.ts --type Session -o ./src/schema/Session.schema.json --no-type-check \ +&& yarn ts-json-schema-generator -f tsconfig.json --path Account.ts --type Account -o ./src/schema/Account.schema.json --no-type-check \ && yarn ts-json-schema-generator -f tsconfig.json --path notifications/types.ts --type NotificationChannel -o ./src/schema/NotificationChannel.schema.json --no-type-check \ diff --git a/packages/lib/delivery/src/Session.test.ts b/packages/lib/delivery/src/Account.test.ts similarity index 99% rename from packages/lib/delivery/src/Session.test.ts rename to packages/lib/delivery/src/Account.test.ts index 562cfe4db..ec5edadb3 100644 --- a/packages/lib/delivery/src/Session.test.ts +++ b/packages/lib/delivery/src/Account.test.ts @@ -5,7 +5,7 @@ const serverSecret = 'veryImportantSecretToGenerateAndValidateJSONWebTokens'; // create valid jwt const token = generateAuthJWT('alice.eth', serverSecret); -describe('Session', () => { +describe('Account', () => { describe('checkToken with state', () => { it('Should return true if the jwt is valid', async () => { const hasAccount = (_: string) => Promise.resolve(true); @@ -24,7 +24,7 @@ describe('Session', () => { expect(isValid).toBe(true); }); - it('Should return false if no session exists for the account ', async () => { + it('Should return false if no account exists for the account ', async () => { const hasAccount = (_: string) => Promise.resolve(false); const isValid = await checkToken( @@ -59,7 +59,7 @@ describe('Session', () => { expect(isValid).toBe(false); }); - it('Should return false if a session exists but the token is expired ', async () => { + it('Should return false if a account exists but the token is expired ', async () => { const hasAccount = (_: string) => Promise.resolve(true); const oneMinuteAgo = new Date().getTime() / 1000 - 60; diff --git a/packages/lib/delivery/src/Session.ts b/packages/lib/delivery/src/Account.ts similarity index 94% rename from packages/lib/delivery/src/Session.ts rename to packages/lib/delivery/src/Account.ts index c0b5fb63e..b5e42a2bb 100644 --- a/packages/lib/delivery/src/Session.ts +++ b/packages/lib/delivery/src/Account.ts @@ -4,7 +4,7 @@ import { SpamFilterRules } from './spam-filter'; //1Year const TTL = 31536000000; -export interface Session { +export interface Account { account: string; signedUserProfile: SignedUserProfile; token: string; diff --git a/packages/lib/delivery/src/Delivery.ts b/packages/lib/delivery/src/Delivery.ts index 3a528dc05..96b146b35 100644 --- a/packages/lib/delivery/src/Delivery.ts +++ b/packages/lib/delivery/src/Delivery.ts @@ -5,4 +5,6 @@ export interface DeliveryServiceProperties { //Number of bytes an envelop object should not exceed sizeLimit: number; notificationChannel: NotificationChannel[]; + metricsCollectionIntervalInSeconds: number; + metricsRetentionDurationInSeconds: number; } diff --git a/packages/lib/delivery/src/UserProfile.test.ts b/packages/lib/delivery/src/UserProfile.test.ts index 33617d077..6ea821683 100644 --- a/packages/lib/delivery/src/UserProfile.test.ts +++ b/packages/lib/delivery/src/UserProfile.test.ts @@ -4,7 +4,7 @@ import { UserProfile, } from '@dm3-org/dm3-lib-profile'; import { stringify } from '@dm3-org/dm3-lib-shared'; -import { Session } from './Session'; +import { Account } from './Account'; import { getUserProfile, submitUserProfile } from './UserProfile'; const SENDER_NAME = 'alice.eth'; @@ -62,11 +62,11 @@ describe('UserProfile', () => { it('override a userProfile that already exists but with other nonce', async () => { const setAccount = () => Promise.resolve(); const getAccount = async (address: string) => { - const session = async ( + const account = async ( account: string, token: string, profile: UserProfile, - ): Promise => { + ): Promise => { const signedUserProfile = await signProfile(profile); return { account, @@ -80,7 +80,7 @@ describe('UserProfile', () => { }; }; - return session(SENDER_ADDRESS, '123', emptyProfile); + return account(SENDER_ADDRESS, '123', emptyProfile); }; const singedUserProfile = await signProfile(emptyProfile); @@ -122,16 +122,16 @@ describe('UserProfile', () => { }); }); describe('GetUserProfile', () => { - it('Returns undefined if address has no session', async () => { + it('Returns undefined if address has no account', async () => { const getAccount = () => Promise.resolve(null); const profile = await getUserProfile(getAccount, RANDO_NAME); expect(profile).toBeUndefined(); }); - it('Returns the signedUserProfile if a session was created', async () => { + it('Returns the signedUserProfile if a account was created', async () => { const getAccount = () => - Promise.resolve({ signedUserProfile: {} } as Session); + Promise.resolve({ signedUserProfile: {} } as Account); const profile = await getUserProfile(getAccount, RANDO_NAME); diff --git a/packages/lib/delivery/src/UserProfile.ts b/packages/lib/delivery/src/UserProfile.ts index 6b4944c12..9bf7ebef6 100644 --- a/packages/lib/delivery/src/UserProfile.ts +++ b/packages/lib/delivery/src/UserProfile.ts @@ -7,11 +7,11 @@ import { import { logDebug } from '@dm3-org/dm3-lib-shared'; import { ethers } from 'ethers'; import { generateAuthJWT } from '@dm3-org/dm3-lib-server-side'; -import { Session } from './Session'; +import { Account } from './Account'; export async function submitUserProfile( - getAccount: (accountAddress: string) => Promise, - setAccount: (accountAddress: string, session: Session) => Promise, + getAccount: (accountAddress: string) => Promise, + setAccount: (accountAddress: string, account: Account) => Promise, address: string, signedUserProfile: SignedUserProfile, serverSecret: string, @@ -27,25 +27,25 @@ export async function submitUserProfile( logDebug('submitUserProfile - Signature invalid'); throw Error('Signature invalid.'); } - const session: Session = { + const account: Account = { account: _address, signedUserProfile, token: generateAuthJWT(_address, serverSecret), createdAt: new Date().getTime(), profileExtension: getDefaultProfileExtension(), }; - logDebug({ text: 'submitUserProfile', session }); - await setAccount(_address, session); + logDebug({ text: 'submitUserProfile', account }); + await setAccount(_address, account); - return session.token; + return account.token; } // todo: remove this function (profiles should be loaded from chain and possibly cached) export async function getUserProfile( - getAccount: (accountAddress: string) => Promise, + getAccount: (accountAddress: string) => Promise, ensName: string, ): Promise { - const account = normalizeEnsName(ensName); - const session = await getAccount(account); - return session?.signedUserProfile; + const accountName = normalizeEnsName(ensName); + const account = await getAccount(accountName); + return account?.signedUserProfile; } diff --git a/packages/lib/delivery/src/index.ts b/packages/lib/delivery/src/index.ts index 9826ba909..4f1168996 100644 --- a/packages/lib/delivery/src/index.ts +++ b/packages/lib/delivery/src/index.ts @@ -9,7 +9,7 @@ export { getConversationId } from './Messages'; export type {} from './PublicMessages'; export * as schema from './schema'; export * as spamFilter from './spam-filter/'; -export type { Session } from './Session'; +export type { Account } from './Account'; export type { DeliveryServiceProperties } from './Delivery'; export * from './notifications'; export { diff --git a/packages/lib/delivery/src/schema/index.ts b/packages/lib/delivery/src/schema/index.ts index 2da911ccc..d54f7c5d8 100644 --- a/packages/lib/delivery/src/schema/index.ts +++ b/packages/lib/delivery/src/schema/index.ts @@ -1,10 +1,10 @@ import AcknowledgementSchema from './Acknowledgement.schema.json'; import DeliveryServicePropertiesSchema from './DeliveryServiceProperties.schema.json'; -import SessionSchema from './Session.schema.json'; +import AccountSchema from './Account.schema.json'; import NotificationChannelSchema from './NotificationChannel.schema.json'; export const Acknowledgement = AcknowledgementSchema.definitions.Acknowledgement; export const DeliveryServiceProperties = DeliveryServicePropertiesSchema; -export const Session = SessionSchema; +export const Account = AccountSchema; export const NotificationChannel = NotificationChannelSchema; diff --git a/packages/lib/delivery/src/spam-filter/index.ts b/packages/lib/delivery/src/spam-filter/index.ts index 32f890fe1..d7999808f 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 '../Session'; +import { Account } from '../Account'; export type { SpamFilterRules }; @@ -35,11 +35,11 @@ function compileSpamFilter(filter: SpamFilter[]) { }; } /** - * Maps the {@see SpamFilterRules} a user has specified in they session to an array of filters + * Maps the {@see SpamFilterRules} a user has specified in they account to an array of filters */ function getUsersSpamFilters( provider: ethers.providers.BaseProvider, - { spamFilterRules }: Session, + { spamFilterRules }: Account, ) { //User has not defined any rules if (!spamFilterRules) { @@ -55,10 +55,10 @@ function getUsersSpamFilters( */ export async function isSpam( provider: ethers.providers.BaseProvider, - session: Session, + account: Account, deliveryInformation: DeliveryInformation, ) { - const usersSpamFilters = getUsersSpamFilters(provider, session); + const usersSpamFilters = getUsersSpamFilters(provider, account); const filter = compileSpamFilter(usersSpamFilters); //The predicate of a filter returns true if the message is valid. diff --git a/packages/lib/delivery/src/spam-filter/spamfilter.test.ts b/packages/lib/delivery/src/spam-filter/spamfilter.test.ts index bc1cb7afe..f0c8bfad6 100644 --- a/packages/lib/delivery/src/spam-filter/spamfilter.test.ts +++ b/packages/lib/delivery/src/spam-filter/spamfilter.test.ts @@ -4,7 +4,7 @@ import { isSpam } from '.'; import { testData } from '../../../../../test-data/encrypted-envelops.test'; import { SpamFilterRules } from './SpamFilterRules'; -import { Session } from '@dm3-org/dm3-lib-delivery'; +import { Account } from '@dm3-org/dm3-lib-delivery'; const keysA = { encryptionKeyPair: { @@ -48,13 +48,13 @@ const connection = { describe('SpamFilter', () => { describe('isSpam ', () => { it('Should filter correctly with one filter criteria', async () => { - const session = { + const account = { spamFilterRules: { minBalance: '1' }, - } as Session & { spamFilterRules: SpamFilterRules }; + } as Account & { spamFilterRules: SpamFilterRules }; const envelopAIsSpam = await isSpam( connection.provider, - session, + account, JSON.parse( await decryptAsymmetric( keysA.encryptionKeyPair, @@ -66,7 +66,7 @@ describe('SpamFilter', () => { ); const envelopBIsSpam = await isSpam( connection.provider, - session, + account, JSON.parse( await decryptAsymmetric( keysA.encryptionKeyPair, @@ -81,13 +81,13 @@ describe('SpamFilter', () => { await expect(envelopBIsSpam).toBe(false); }); it('Should use filter correctly with two filter criteria', async () => { - const session = { + const account = { spamFilterRules: { minBalance: '1', minNonce: 2 }, - } as Session & { spamFilterRules: SpamFilterRules }; + } as Account & { spamFilterRules: SpamFilterRules }; const envelopAIsSpam = await isSpam( connection.provider, - session, + account, JSON.parse( await decryptAsymmetric( keysA.encryptionKeyPair, @@ -99,7 +99,7 @@ describe('SpamFilter', () => { ); const envelopBIsSpam = await isSpam( connection.provider, - session, + account, JSON.parse( await decryptAsymmetric( keysA.encryptionKeyPair, @@ -114,13 +114,13 @@ describe('SpamFilter', () => { await expect(envelopBIsSpam).toBe(true); }); it('Should not consider a message as spam if no SpamFilterRules are provided', async () => { - const session = {} as Session & { + const account = {} as Account & { spamFilterRules: SpamFilterRules; }; const envelopAIsSpam = await isSpam( connection.provider, - session, + account, JSON.parse( await decryptAsymmetric( keysA.encryptionKeyPair, diff --git a/packages/lib/messaging/package.json b/packages/lib/messaging/package.json index d9c355fc9..9d8a63157 100644 --- a/packages/lib/messaging/package.json +++ b/packages/lib/messaging/package.json @@ -1,7 +1,7 @@ { "name": "@dm3-org/dm3-lib-messaging", "license": "BSD-2-Clause", - "version": "1.6.1", + "version": "1.7.0", "main": "dist/index.js", "module": "dist-backend/index.js", "types": "dist/index.d.ts", diff --git a/packages/lib/offchain-resolver-api/package.json b/packages/lib/offchain-resolver-api/package.json index 73ff5498b..d91310bbd 100644 --- a/packages/lib/offchain-resolver-api/package.json +++ b/packages/lib/offchain-resolver-api/package.json @@ -1,6 +1,6 @@ { "name": "@dm3-org/dm3-lib-offchain-resolver-api", - "version": "1.6.1", + "version": "1.7.0", "license": "MIT", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/packages/lib/profile/package.json b/packages/lib/profile/package.json index 907158b67..edffbfde9 100644 --- a/packages/lib/profile/package.json +++ b/packages/lib/profile/package.json @@ -1,7 +1,7 @@ { "name": "@dm3-org/dm3-lib-profile", "license": "BSD-2-Clause", - "version": "1.6.1", + "version": "1.7.0", "main": "dist/index.js", "module": "dist-backend/index.js", "types": "dist/index.d.ts", diff --git a/packages/lib/server-side/package.json b/packages/lib/server-side/package.json index ab2fda487..12b6dd96b 100644 --- a/packages/lib/server-side/package.json +++ b/packages/lib/server-side/package.json @@ -1,7 +1,7 @@ { "name": "@dm3-org/dm3-lib-server-side", "license": "BSD-2-Clause", - "version": "1.6.1", + "version": "1.7.0", "main": "dist/index.js", "module": "dist-backend/index.js", "types": "dist/index.d.ts", diff --git a/packages/lib/server-side/src/auth.test.ts b/packages/lib/server-side/src/authenticate.test.ts similarity index 89% rename from packages/lib/server-side/src/auth.test.ts rename to packages/lib/server-side/src/authenticate.test.ts index b0e29db8e..2f4e9ebc5 100644 --- a/packages/lib/server-side/src/auth.test.ts +++ b/packages/lib/server-side/src/authenticate.test.ts @@ -5,7 +5,7 @@ import { ethers } from 'ethers'; import express from 'express'; import { verify } from 'jsonwebtoken'; import request from 'supertest'; -import { Auth } from './auth'; +import { Authenticate } from './authenticate'; import { IAccountDatabase } from './iAccountDatabase'; import { createChallenge } from './Keys'; import { sign } from '@dm3-org/dm3-lib-crypto'; @@ -70,7 +70,11 @@ describe('Auth', () => { const app = express(); app.use(bodyParser.json()); app.use( - Auth(mockDbWithAccount, serverSecret, mockWeb3Provider), + Authenticate( + mockDbWithAccount, + serverSecret, + mockWeb3Provider, + ), ); const response = await request(app) @@ -136,14 +140,18 @@ describe('Auth', () => { }); }); - describe('createNewSessionToken', () => { + describe('createNewAccountToken', () => { describe('schema', () => { it('Returns 400 if signature is invalid', async () => { const app = express(); app.use(bodyParser.json()); app.use( - Auth(mockDbWithAccount, serverSecret, mockWeb3Provider), + Authenticate( + mockDbWithAccount, + serverSecret, + mockWeb3Provider, + ), ); // create the challenge jwt @@ -172,7 +180,11 @@ describe('Auth', () => { app.use(bodyParser.json()); app.use( - Auth(mockDbWithAccount, serverSecret, mockWeb3Provider), + Authenticate( + mockDbWithAccount, + serverSecret, + mockWeb3Provider, + ), ); // create the challenge jwt @@ -207,7 +219,11 @@ describe('Auth', () => { app.use(bodyParser.json()); app.use( - Auth(mockDbWithAccount, serverSecret, mockWeb3Provider), + Authenticate( + mockDbWithAccount, + serverSecret, + mockWeb3Provider, + ), ); const { status } = await request(app).post(`/somename`).send({ @@ -221,7 +237,11 @@ describe('Auth', () => { app.use(bodyParser.json()); app.use( - Auth(mockDbWithAccount, serverSecret, mockWeb3Provider), + Authenticate( + mockDbWithAccount, + serverSecret, + mockWeb3Provider, + ), ); // create the challenge jwt diff --git a/packages/lib/server-side/src/auth.ts b/packages/lib/server-side/src/authenticate.ts similarity index 99% rename from packages/lib/server-side/src/auth.ts rename to packages/lib/server-side/src/authenticate.ts index 0901ec384..0c4bae56b 100644 --- a/packages/lib/server-side/src/auth.ts +++ b/packages/lib/server-side/src/authenticate.ts @@ -34,7 +34,7 @@ const createNewSessionTokenBodySchema = { additionalProperties: false, }; -export const Auth = ( +export const Authenticate = ( db: IAccountDatabase, serverSecret: string, web3Provider: ethers.providers.JsonRpcProvider, diff --git a/packages/lib/server-side/src/authorize.test.ts b/packages/lib/server-side/src/authorizationMiddleware.test.ts similarity index 97% rename from packages/lib/server-side/src/authorize.test.ts rename to packages/lib/server-side/src/authorizationMiddleware.test.ts index e1e0f9d66..d8eb88968 100644 --- a/packages/lib/server-side/src/authorize.test.ts +++ b/packages/lib/server-side/src/authorizationMiddleware.test.ts @@ -2,13 +2,9 @@ import bodyParser from 'body-parser'; import express, { NextFunction, Request, Response } from 'express'; import { sign, verify } from 'jsonwebtoken'; import request from 'supertest'; -import winston from 'winston'; -import { authorize } from './authorize'; +import { authorizationMiddleware } from './authorizationMiddleware'; const serverSecret = 'testSecret'; -winston.loggers.add('default', { - transports: [new winston.transports.Console({ level: 'silly' })], -}); describe('Utils', () => { describe('Auth', () => { @@ -37,7 +33,7 @@ describe('Utils', () => { next: NextFunction, ensName: string, ) => { - authorize( + authorizationMiddleware( req, res, next, @@ -86,7 +82,7 @@ describe('Utils', () => { next: NextFunction, ensName: string, ) => { - authorize( + authorizationMiddleware( req, res, next, @@ -134,7 +130,7 @@ describe('Utils', () => { next: NextFunction, ensName: string, ) => { - authorize( + authorizationMiddleware( req, res, next, @@ -191,7 +187,7 @@ describe('Utils', () => { next: NextFunction, ensName: string, ) => { - authorize( + authorizationMiddleware( req, res, next, @@ -260,7 +256,7 @@ describe('Utils', () => { next: NextFunction, ensName: string, ) => { - authorize( + authorizationMiddleware( req, res, next, @@ -349,7 +345,7 @@ describe('Utils', () => { next: NextFunction, ensName: string, ) => { - authorize( + authorizationMiddleware( req, res, next, diff --git a/packages/lib/server-side/src/authorize.ts b/packages/lib/server-side/src/authorizationMiddleware.ts similarity index 94% rename from packages/lib/server-side/src/authorize.ts rename to packages/lib/server-side/src/authorizationMiddleware.ts index 724459bf6..74a673952 100644 --- a/packages/lib/server-side/src/authorize.ts +++ b/packages/lib/server-side/src/authorizationMiddleware.ts @@ -16,7 +16,7 @@ const authJwtPayloadSchema = { additionalProperties: false, }; -export async function authorize( +export async function authorizationMiddleware( req: Request, res: Response, next: NextFunction, @@ -57,7 +57,7 @@ export async function checkToken( return false; } - console.debug('checkToken - ensName', ensName); + //console.debug('checkToken - ensName', ensName); // check jwt for validity try { @@ -66,7 +66,7 @@ export async function checkToken( algorithms: ['HS256'], }); - console.debug('checkToken - jwtPayload', jwtPayload); + //console.debug('checkToken - jwtPayload', jwtPayload); // check if payload is well formed if ( diff --git a/packages/lib/server-side/src/index.ts b/packages/lib/server-side/src/index.ts index 2106f0f28..89eb29253 100644 --- a/packages/lib/server-side/src/index.ts +++ b/packages/lib/server-side/src/index.ts @@ -1,5 +1,5 @@ -export { Auth } from './auth'; -export * from './authorize'; +export { Authenticate } from './authenticate'; +export * from './authorizationMiddleware'; export { getCachedWebProvider } from './web3Provider/getCachedWebProvider'; export type { IAccountDatabase } from './iAccountDatabase'; export * from './utils'; diff --git a/packages/lib/server-side/src/utils.ts b/packages/lib/server-side/src/utils.ts index 5af754431..ff02ce56c 100644 --- a/packages/lib/server-side/src/utils.ts +++ b/packages/lib/server-side/src/utils.ts @@ -2,10 +2,9 @@ import { DeliveryServiceProfileKeys } from '@dm3-org/dm3-lib-profile'; import { ethers } from 'ethers'; import { NextFunction, Request, Response } from 'express'; -import winston from 'winston'; export function logRequest(req: Request, res: Response, next: NextFunction) { - winston.loggers.get('default').info({ + console.info({ method: req.method, url: req.url, timestamp: new Date().getTime(), diff --git a/packages/lib/shared/package.json b/packages/lib/shared/package.json index f040c5022..6736529c0 100644 --- a/packages/lib/shared/package.json +++ b/packages/lib/shared/package.json @@ -1,7 +1,7 @@ { "name": "@dm3-org/dm3-lib-shared", "license": "BSD-2-Clause", - "version": "1.6.1", + "version": "1.7.0", "main": "dist/index.js", "module": "dist-backend/index.js", "types": "dist/index.d.ts", @@ -17,7 +17,8 @@ }, "dependencies": { "ajv-formats": "^3.0.1", - "ethers": "5.7.2" + "ethers": "5.7.2", + "safe-stable-stringify": "2.4.3" }, "devDependencies": { "jest": "^29.7.0", diff --git a/packages/lib/storage/package.json b/packages/lib/storage/package.json index 53d86a9b8..9ea334d28 100644 --- a/packages/lib/storage/package.json +++ b/packages/lib/storage/package.json @@ -1,7 +1,7 @@ { "name": "@dm3-org/dm3-lib-storage", "license": "BSD-2-Clause", - "version": "1.6.1", + "version": "1.7.0", "main": "dist/index.js", "module": "dist-backend/index.js", "types": "dist/index.d.ts", diff --git a/packages/lib/test-helper/package.json b/packages/lib/test-helper/package.json index 210e0b073..92718324f 100644 --- a/packages/lib/test-helper/package.json +++ b/packages/lib/test-helper/package.json @@ -1,6 +1,6 @@ { "name": "@dm3-org/dm3-lib-test-helper", - "version": "1.6.1", + "version": "1.7.0", "main": "dist/index.js", "module": "dist-backend/index.js", "types": "dist/index.d.ts", diff --git a/packages/messenger-demo/package.json b/packages/messenger-demo/package.json index 7f6bacb62..6b279e745 100644 --- a/packages/messenger-demo/package.json +++ b/packages/messenger-demo/package.json @@ -1,7 +1,7 @@ { "name": "@dm3-org/dm3-messenger-demo", "license": "BSD-2-Clause", - "version": "1.6.1", + "version": "1.7.0", "dependencies": { "@dm3-org/dm3-messenger-widget": "workspace:^", "@popperjs/core": "^2.11.8", diff --git a/packages/messenger-demo/src/theme.ts b/packages/messenger-demo/src/theme.ts index 434003919..5818b3cbb 100644 --- a/packages/messenger-demo/src/theme.ts +++ b/packages/messenger-demo/src/theme.ts @@ -45,4 +45,5 @@ export const themeColors = { alternateContactBackgroundColor: 'black', menuBackgroundColor: 'blue', preferencesHighlightedColor: '#8b7ff4', + configureProfileModalBackgroundColor: '#D9D9D9', }; diff --git a/packages/messenger-web/package.json b/packages/messenger-web/package.json index 172c7887a..5cbfbf9dc 100644 --- a/packages/messenger-web/package.json +++ b/packages/messenger-web/package.json @@ -1,7 +1,7 @@ { "name": "@dm3-org/dm3-messenger-web", "license": "BSD-2-Clause", - "version": "1.6.1", + "version": "1.7.0", "dependencies": { "@dm3-org/dm3-messenger-widget": "workspace:^", "@popperjs/core": "^2.11.8", diff --git a/packages/messenger-widget/README.md b/packages/messenger-widget/README.md index 8961fb1a2..7253d2f11 100644 --- a/packages/messenger-widget/README.md +++ b/packages/messenger-widget/README.md @@ -511,6 +511,7 @@ Example : alternateContactBackgroundColor: 'black', menuBackgroundColor: 'blue', preferencesHighlightedColor: '#8b7ff4', + configureProfileModalBackgroundColor: '#D9D9D9', } ``` diff --git a/packages/messenger-widget/declaration.d.ts b/packages/messenger-widget/declaration.d.ts index fe0422b22..82612c0b8 100644 --- a/packages/messenger-widget/declaration.d.ts +++ b/packages/messenger-widget/declaration.d.ts @@ -12,3 +12,5 @@ declare module 'localforage' { let localforage: LocalForage; export = localforage; } + +declare module '@ensdomains/ensjs'; diff --git a/packages/messenger-widget/package.json b/packages/messenger-widget/package.json index 2b937f258..ebd06af38 100644 --- a/packages/messenger-widget/package.json +++ b/packages/messenger-widget/package.json @@ -1,7 +1,7 @@ { "name": "@dm3-org/dm3-messenger-widget", "license": "BSD-2-Clause", - "version": "1.6.1", + "version": "1.7.0", "main": "./lib/cjs/widget.js", "module": "./lib/esm/widget.js", "types": "./lib/esm/widget.d.ts", @@ -16,6 +16,7 @@ "@dm3-org/dm3-lib-test-helper": "workspace:^", "@emoji-mart/data": "^1.1.2", "@emoji-mart/react": "^1.1.1", + "@ensdomains/ensjs": "^2.1.0", "@popperjs/core": "^2.11.8", "@rainbow-me/rainbowkit": "^1.0.6", "@testing-library/dom": "^9.3.1", diff --git a/packages/messenger-widget/src/adapters/offchainResolverApi.ts b/packages/messenger-widget/src/adapters/offchainResolverApi.ts index e79ccea58..5048844d2 100644 --- a/packages/messenger-widget/src/adapters/offchainResolverApi.ts +++ b/packages/messenger-widget/src/adapters/offchainResolverApi.ts @@ -76,6 +76,35 @@ export async function claimAddress( } } +/** + * updates profile with new delivery service nodes added + * @param address The ethereum address + * @param offchainResolverUrl The offchain resolver endpoint url + * @param subdomain The addr subdomain of the client .iE addr.dm3.eth + * @param signedUserProfile The signed dm3 user profile + */ +export async function updateProfile( + address: string, + offchainResolverUrl: string, + addrSubdomainDomain: string, + signedUserProfile: SignedUserProfile, +) { + try { + const url = `${offchainResolverUrl}/profile/address`; + const data = { + signedUserProfile, + address, + subdomain: addrSubdomainDomain, + }; + + const { status } = await axios.post(url, data); + return status === 200; + } catch (err) { + console.log('update profile failed'); + return false; + } +} + /** * returns the linked ENS name for an eth address * @param address The ethereum address diff --git a/packages/messenger-widget/src/assets/images/update.svg b/packages/messenger-widget/src/assets/images/update.svg new file mode 100644 index 000000000..285d4deb4 --- /dev/null +++ b/packages/messenger-widget/src/assets/images/update.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/packages/messenger-widget/src/components/AddConversation/AddConversation.css b/packages/messenger-widget/src/components/AddConversation/AddConversation.css index 564834a51..4eda05dfc 100644 --- a/packages/messenger-widget/src/components/AddConversation/AddConversation.css +++ b/packages/messenger-widget/src/components/AddConversation/AddConversation.css @@ -49,6 +49,7 @@ font-style: italic; color: var(--error-text); margin-left: 1rem; + display: inline-table; } .add-name-container { diff --git a/packages/messenger-widget/src/components/ConfigureProfile/ClaimDM3Name.tsx b/packages/messenger-widget/src/components/ConfigureProfile/ClaimDM3Name.tsx new file mode 100644 index 000000000..174207e24 --- /dev/null +++ b/packages/messenger-widget/src/components/ConfigureProfile/ClaimDM3Name.tsx @@ -0,0 +1,25 @@ +import { useContext } from 'react'; +import { fetchDM3NameComponent } from './bl'; +import { ConfigureProfileContext } from './context/ConfigureProfileContext'; +import { DM3ConfigurationContext } from '../../context/DM3ConfigurationContext'; + +export function ClaimDM3Name() { + const { dm3Configuration } = useContext(DM3ConfigurationContext); + + const { dm3NameServiceSelected } = useContext(ConfigureProfileContext); + + return ( +
+
+ Add new dm3 profile - claim dm3 profile - claim name +
+ +
+ {fetchDM3NameComponent( + dm3NameServiceSelected, + dm3Configuration.chainId, + )} +
+
+ ); +} diff --git a/packages/messenger-widget/src/components/ConfigureProfile/ClaimOwnName.tsx b/packages/messenger-widget/src/components/ConfigureProfile/ClaimOwnName.tsx new file mode 100644 index 000000000..2f95b7c76 --- /dev/null +++ b/packages/messenger-widget/src/components/ConfigureProfile/ClaimOwnName.tsx @@ -0,0 +1,25 @@ +import { useContext } from 'react'; +import { fetchComponent } from './bl'; +import { ConfigureProfileContext } from './context/ConfigureProfileContext'; +import { DM3ConfigurationContext } from '../../context/DM3ConfigurationContext'; + +export function ClaimOwnName() { + const { dm3Configuration } = useContext(DM3ConfigurationContext); + + const { namingServiceSelected } = useContext(ConfigureProfileContext); + + return ( +
+
+ Add new dm3 profile - claim dm3 profile - claim name +
+ +
+ {fetchComponent( + namingServiceSelected, + dm3Configuration.chainId, + )} +
+
+ ); +} diff --git a/packages/messenger-widget/src/components/ConfigureProfile/CloudStorage.tsx b/packages/messenger-widget/src/components/ConfigureProfile/CloudStorage.tsx new file mode 100644 index 000000000..e2f5c7093 --- /dev/null +++ b/packages/messenger-widget/src/components/ConfigureProfile/CloudStorage.tsx @@ -0,0 +1,126 @@ +import { useContext, useState } from 'react'; +import { ModalContext } from '../../context/ModalContext'; +import { ProfileScreenType, ProfileType } from '../../utils/enum-type-utils'; +import { BUTTON_CLASS, DM3_NAME_SERVICES, dm3NamingServices } from './bl'; +import { ConfigureProfileContext } from './context/ConfigureProfileContext'; +import { DM3ConfigurationContext } from '../../context/DM3ConfigurationContext'; +import { ConfigureDM3NameContext } from './context/ConfigureDM3NameContext'; + +export function CloudStorage() { + const [errorMsg, setErrorMsg] = useState(null); + + const { dm3NameServiceSelected, setDm3NameServiceSelected } = useContext( + ConfigureProfileContext, + ); + + const { dm3Configuration } = useContext(DM3ConfigurationContext); + + const { existingDm3Name } = useContext(ConfigureDM3NameContext); + + const { configureProfileModal, setConfigureProfileModal } = + useContext(ModalContext); + + const isNameAlreadyConfigured = (): boolean => { + if ( + existingDm3Name?.endsWith(dm3Configuration.userEnsSubdomain) && + dm3NameServiceSelected === DM3_NAME_SERVICES.CLOUD + ) { + setErrorMsg( + 'Cloud name is already configured, only one cloud name can be configured at a time', + ); + return true; + } + if ( + existingDm3Name?.endsWith('.op.dm3.eth') && + dm3NameServiceSelected === DM3_NAME_SERVICES.OPTIMISM + ) { + setErrorMsg( + 'Optimism name is already configured, only one optimism name can be configured at a time', + ); + return true; + } + return false; + }; + + const navigateToNextTab = () => { + if (!isNameAlreadyConfigured()) { + setErrorMsg(null); + setConfigureProfileModal({ + ...configureProfileModal, + onScreen: ProfileScreenType.CLAIM_NAME, + }); + } + }; + + return ( + <> +
+ Add new dm3 profile - claim dm3 profile - select storage +
+ +
+
+ {errorMsg ?? ''} +
+ +
+ +
+ +
+
+ You can get a DM3 name for free. Please check if your + desired name is available. DM3 names are created and + managed on Layer2 (e.g. Optimism). Small transaction + costs will apply for setting the profile and + administration. +
+
+ You can receive messages sent to your full DM3 username. +
+
+
+
+ + +
+ + ); +} diff --git a/packages/messenger-widget/src/components/ConfigureProfile/ConfigureProfile.css b/packages/messenger-widget/src/components/ConfigureProfile/ConfigureProfile.css index 3086de0c7..9e6ce861c 100644 --- a/packages/messenger-widget/src/components/ConfigureProfile/ConfigureProfile.css +++ b/packages/messenger-widget/src/components/ConfigureProfile/ConfigureProfile.css @@ -52,7 +52,7 @@ } .configure-btn { - padding: 8px 16px 8px 16px; + padding: 5px 30px 5px 30px; box-shadow: 0px 0px 44px 0px var(--button-shadow); width: fit-content; } @@ -66,10 +66,11 @@ } .name-service-selector { - background: var(--preferences-highlighted-color); + background-color: var(--text-primary-color); font-size: 14px; padding: 0.3rem; - font-weight: 500; + font-weight: bold; + color: #0D0D0D; outline: none; } @@ -81,6 +82,138 @@ input::placeholder { font-size: 11px; } +.dm3-address { + word-break: break-word; +} + +.address-tooltip{ + background: var(--normal-btn-hover); + color: var(--text-primary-color); + margin-left: 0.6rem; + padding: 2px 7px 2px 7px; + border-radius: 13px; + font-size: 10px; + cursor: pointer; + position: relative; +} + +.address-tooltip-text{ + visibility: hidden; + position: absolute; + z-index: 1; + width: 300px; + border-radius: 6px; + padding: 6px 6px 6px 6px; + margin: 22px 9px 10px 0px; + background-color: var(--text-secondary-color); + font-size: 0.7rem; + font-weight: 600; + line-height: 1rem; + cursor: text; +} + +.address-tooltip:hover .address-tooltip-text { + visibility: visible; +} + +.add-prof-btn-disabled { + background: var(--normal-btn-inactive); + color: var(--disabled-btn-text); + border: none; +} + +.add-prof-btn-active { + background-color: var(--normal-btn-hover); + border: var(--normal-btn-border); + color: var(--text-primary-color); +} + +.dm3-prof-select-container{ + border: 1px solid var(--text-primary-color); + border-radius: 5px; +} + +.dm3-prof-select-type{ + background: var(--configure-profile-modal-background-color); + font-weight: 500; + font-size: 14px; + padding: 0.5rem 0.5rem 0.5rem 0.5rem; + line-height: 24px; + border-radius: 5px; + color: black; +} + +.name-option{ + color: var(--text-primary-color); + font-weight: 500; + font-size: 14px; + line-height: 24px; + margin-left: 0.5rem; + display: flex; + align-items: center; +} + +.prof-option-container{ + padding: 3rem 1rem 1rem 1rem; +} + +.update-btn { + margin-left: 1.2rem !important; + font-size: 12px; + border-radius: 6px; +} + +/* Customized CSS for radio button */ +.radio { + input[type="radio"] { + opacity: 0; + + .radio-label { + &:before { + content: ''; + background: #1F2029; + border-radius: 100%; + border: 1px solid var(--text-primary-color); + display: inline-block; + width: 1.4em; + height: 1.4em; + position: relative; + top: -0.2em; + margin-right: 1em; + vertical-align: top; + cursor: pointer; + text-align: center; + transition: all 250ms ease; + } + } + &:checked { + + .radio-label { + &:before { + background-color: var(--configure-profile-modal-background-color); + box-shadow: inset 0 0 0 4px #1F2029; + } + } + } + &:focus { + + .radio-label { + &:before { + outline: none; + border-color: var(--configure-profile-modal-background-color); + } + } + } + } + } + + .ens-components-container{ + padding: 1.25rem; + } + + .config-profile-cancel-btn { + background-color: transparent; + border: 2px solid var(--preferences-highlighted-color); + color: var(--text-primary-color); +} + /* =================== Mobile Responsive CSS =================== */ @media only screen and (min-width: 800px) and (max-width: 1150px) { @@ -90,12 +223,32 @@ input::placeholder { .conversation-error { font-size: 0.7rem !important; } + .ens-components-container{ + padding: 1rem; + } + .profile-action-btns { + margin-top: 1rem !important; + } + .dm3-name-container { + width: 100%; + } +} + +@media only screen and (max-width: 950px) { + #ens-name{ + width: 90% !important; + } } @media only screen and (max-width: 800px) { .profile-input { padding: 0.2rem 0.5rem 0.2rem 0.5rem; width: 100%; + font-size: 12px; + } + + .profile-input::placeholder{ + font-size: 10px !important; } .dm3-name-container { @@ -114,7 +267,7 @@ input::placeholder { } .configure-btn { - padding: 6px 14px 4px 14px; + padding: 4px 14px 4px 14px; } .dm3-name-content { @@ -134,5 +287,63 @@ input::placeholder { .name-service-selector{ font-size: 12px; + width: 100%; + } + + .dm3-address { + word-break: break-word; + font-size: 13px !important; + } + + .address-tooltip-text{ + margin: 22px 0px 10px -83px !important; + } + + .prof-option-container{ + padding: 1rem 1rem 1rem 0rem; + } + + .name-select-container{ + margin-left: -11px; + } + + .radio { + input[type="radio"] { + opacity: 0; + + .radio-label { + &:before { + width: 2em !important; + } + } + } + } + + .dm3-prof-select-type{ + font-size: 12px; + } + + .title-content { + font-size: 12px; + } + + .ens-components-container{ + padding: 0.5rem; } + + .update-btn { + margin-top: 1rem; + margin-left: 0rem !important; + font-size: 12px; + border-radius: 6px; + width: fit-content; + } + + .del-icon{ + height: 1rem; + } + + .profile-input{ + width: 100% !important; + } + } diff --git a/packages/messenger-widget/src/components/ConfigureProfile/MobileView.tsx b/packages/messenger-widget/src/components/ConfigureProfile/MobileView.tsx index 378d48b12..d9f9943d3 100644 --- a/packages/messenger-widget/src/components/ConfigureProfile/MobileView.tsx +++ b/packages/messenger-widget/src/components/ConfigureProfile/MobileView.tsx @@ -4,16 +4,21 @@ import { useContext, useEffect } from 'react'; import { useChainId } from 'wagmi'; import { AuthContext } from '../../context/AuthContext'; import { useMainnetProvider } from '../../hooks/mainnetprovider/useMainnetProvider'; -import { - dm3NamingServices, - fetchComponent, - fetchDM3NameComponent, - fetchServiceFromChainId, - getEnsName, - namingServices, -} from './bl'; +import { BUTTON_CLASS, fetchServiceFromChainId, getEnsName } from './bl'; import { ConfigureProfileContext } from './context/ConfigureProfileContext'; import { DM3ConfigurationContext } from '../../context/DM3ConfigurationContext'; +import { ModalContext } from '../../context/ModalContext'; +import { ProfileScreenType, ProfileType } from '../../utils/enum-type-utils'; +import { ProfileTypeSelector } from './ProfileTypeSelector'; +import { ClaimDM3Name } from './ClaimDM3Name'; +import { ConfigureDM3NameContext } from './context/ConfigureDM3NameContext'; +import DeleteDM3Name from '../DeleteDM3Name/DeleteDM3Name'; +import deleteIcon from '../../assets/images/delete.svg'; +import { CloudStorage } from './CloudStorage'; +import { OwnStorage } from './OwnStorage'; +import { ClaimOwnName } from './ClaimOwnName'; +import { DM3UserProfileContext } from '../../context/DM3UserProfileContext'; +import updateIcon from '../../assets/images/update.svg'; export function MobileView() { const connectedChainId = useChainId(); @@ -22,16 +27,38 @@ export function MobileView() { const { account, ethAddress } = useContext(AuthContext); + const { configureProfileModal, setConfigureProfileModal } = + useContext(ModalContext); + + const { setEnsName, setNamingServiceSelected, existingEnsName } = + useContext(ConfigureProfileContext); + const { - setEnsName, - dm3NameServiceSelected, - setDm3NameServiceSelected, - namingServiceSelected, - setNamingServiceSelected, - } = useContext(ConfigureProfileContext); + existingDm3Name, + setShowDeleteConfirmation, + showDeleteConfirmation, + updateDeleteConfirmation, + handleClaimOrRemoveDm3Name, + } = useContext(ConfigureDM3NameContext); const { dm3Configuration } = useContext(DM3ConfigurationContext); + const { + updateProfileForAddressName, + updateProfileWithTransaction, + isProfileUpdatedForAddrName, + isProfileUpdatedForDm3Name, + isProfileUpdatedForEnsName, + } = useContext(DM3UserProfileContext); + + const ensDomainName = + (existingEnsName && + (existingEnsName?.endsWith('.gno') || + existingEnsName?.endsWith('.gnosis.eth') + ? 'GNO' + : 'ENS')) ?? + null; + // handles ENS name and address useEffect(() => { getEnsName( @@ -50,59 +77,245 @@ export function MobileView() { }, []); return ( -
- {/* DM3 Name */} -
-
-
- + Wallet Address + + i + + You can use your wallet address as a username. A + virtual profile is created and stored at a dm3 + service. There are no transaction costs for + creation and administration. +
+ + {' '} + You can receive messages sent to your wallet + address. + +
+
+

+

+ {ethAddress && + ethAddress + dm3Configuration.addressEnsSubdomain} +

+ {!isProfileUpdatedForAddrName && ( + + )}
- {fetchDM3NameComponent( - dm3NameServiceSelected, - dm3Configuration.chainId, + {/* Existing DM3 name */} + {existingDm3Name && ( +
+

+ DM3 Name + + i + + DM3 name is used as a username and can be used + by any address to send the messages to this DM3 + name. +
+ + {' '} + You can receive messages sent to your DM3 + name. + +
+
+

+

+ {existingDm3Name} + + {/* Delete icon */} + remove setShowDeleteConfirmation(true)} + /> +

+ + {!isProfileUpdatedForDm3Name && ( + + )} +
)} - {/* ENS Name */} -
-
-
- + {existingEnsName} +

+ + {!isProfileUpdatedForEnsName && ( + + )}
+ )} + + {/* Add profile button */} +
+
- {fetchComponent(namingServiceSelected, dm3Configuration.chainId)} + {/* Screen to select profile type */} + {configureProfileModal.onScreen === + ProfileScreenType.SELECT_TYPE && } + + {/* Screen to select storage type */} + {configureProfileModal.onScreen === + ProfileScreenType.SELECT_STORAGE && ( +
+ {configureProfileModal.profileOptionSelected === + ProfileType.DM3_NAME ? ( + + ) : ( + + )} +
+ )} + + {/* Screen to claim profile name */} + {configureProfileModal.onScreen === ProfileScreenType.CLAIM_NAME && + (configureProfileModal.profileOptionSelected === + ProfileType.DM3_NAME ? ( + + ) : ( + + ))}
); } diff --git a/packages/messenger-widget/src/components/ConfigureProfile/NormalView.tsx b/packages/messenger-widget/src/components/ConfigureProfile/NormalView.tsx index 0f9fd8a29..1a40d956f 100644 --- a/packages/messenger-widget/src/components/ConfigureProfile/NormalView.tsx +++ b/packages/messenger-widget/src/components/ConfigureProfile/NormalView.tsx @@ -2,19 +2,23 @@ import '../../styles/modal.css'; import './ConfigureProfile.css'; import { useContext, useEffect } from 'react'; import { useChainId } from 'wagmi'; -import tickIcon from '../../assets/images/white-tick.svg'; import { AuthContext } from '../../context/AuthContext'; import { useMainnetProvider } from '../../hooks/mainnetprovider/useMainnetProvider'; -import { - dm3NamingServices, - fetchComponent, - fetchDM3NameComponent, - fetchServiceFromChainId, - getEnsName, - namingServices, -} from './bl'; +import { BUTTON_CLASS, fetchServiceFromChainId, getEnsName } from './bl'; import { ConfigureProfileContext } from './context/ConfigureProfileContext'; import { DM3ConfigurationContext } from '../../context/DM3ConfigurationContext'; +import { ModalContext } from '../../context/ModalContext'; +import { ProfileTypeSelector } from './ProfileTypeSelector'; +import { ProfileScreenType, ProfileType } from '../../utils/enum-type-utils'; +import { ClaimDM3Name } from './ClaimDM3Name'; +import { ConfigureDM3NameContext } from './context/ConfigureDM3NameContext'; +import deleteIcon from '../../assets/images/delete.svg'; +import DeleteDM3Name from '../DeleteDM3Name/DeleteDM3Name'; +import { CloudStorage } from './CloudStorage'; +import { OwnStorage } from './OwnStorage'; +import { ClaimOwnName } from './ClaimOwnName'; +import { DM3UserProfileContext } from '../../context/DM3UserProfileContext'; +import updateIcon from '../../assets/images/update.svg'; export function NormalView() { const connectedChainId = useChainId(); @@ -23,16 +27,38 @@ export function NormalView() { const { account, ethAddress } = useContext(AuthContext); + const { configureProfileModal, setConfigureProfileModal } = + useContext(ModalContext); + + const { setEnsName, setNamingServiceSelected, existingEnsName } = + useContext(ConfigureProfileContext); + const { - setEnsName, - dm3NameServiceSelected, - setDm3NameServiceSelected, - namingServiceSelected, - setNamingServiceSelected, - } = useContext(ConfigureProfileContext); + existingDm3Name, + showDeleteConfirmation, + setShowDeleteConfirmation, + updateDeleteConfirmation, + handleClaimOrRemoveDm3Name, + } = useContext(ConfigureDM3NameContext); const { dm3Configuration } = useContext(DM3ConfigurationContext); + const { + updateProfileForAddressName, + updateProfileWithTransaction, + isProfileUpdatedForAddrName, + isProfileUpdatedForDm3Name, + isProfileUpdatedForEnsName, + } = useContext(DM3UserProfileContext); + + const ensDomainName = + (existingEnsName && + (existingEnsName?.endsWith('.gno') || + existingEnsName?.endsWith('.gnosis.eth') + ? 'GNO' + : 'ENS')) ?? + null; + // handles ENS name and address useEffect(() => { getEnsName( @@ -51,97 +77,256 @@ export function NormalView() { }, []); return ( -
- {/* Wallet Address */} -
-
- {ethAddress && } -
+
+ {/* Delete DM3 name confirmation popup modal */} + {showDeleteConfirmation && ( + + )} +
+ {/* Wallet Address */}

Wallet Address + + i + + You can use your wallet address as a + username. A virtual profile is created and + stored at a dm3 service. There are no + transaction costs for creation and + administration. +
+ + {' '} + You can receive messages sent to your + wallet address. + +
+

{ethAddress && ethAddress + dm3Configuration.addressEnsSubdomain}

+ {!isProfileUpdatedForAddrName && ( + + )}
-
-
- You can use your wallet address as a username. A - virtual profile is created and stored at a dm3 - service. There are no transaction costs for creation - and administration. + + {/* Existing DM3 name */} + {existingDm3Name && ( +
+

+ DM3 Name + + i + + DM3 name is used as a username and can + be used by any address to send the + messages to this DM3 name. +
+ + {' '} + You can receive messages sent to + your DM3 name. + +
+
+

+

+ {existingDm3Name} +

+ {/* Delete button */} + remove setShowDeleteConfirmation(true)} + /> + + {!isProfileUpdatedForDm3Name && ( + + )}
-
- You can receive messages sent to your wallet - address. + )} + + {/* Existing ENS name */} + {ensDomainName && existingEnsName && ( +
+

+ {ensDomainName} Name + + i + + {ensDomainName} name is used as a + username and can be used by any address + to send the messages to this{' '} + {ensDomainName} name. +
+ + {' '} + You can receive messages sent to + your {ensDomainName} name. + +
+
+

+

+ {existingEnsName} +

+ {!isProfileUpdatedForEnsName && ( + + )}
-
-
-
+ )} - {/* DM3 Name */} -
-
-
- + {/* Add profile button */} +
+ +
- {fetchDM3NameComponent( - dm3NameServiceSelected, - dm3Configuration.chainId, - )} + {/* Screen to select profile type */} + {configureProfileModal.onScreen === + ProfileScreenType.SELECT_TYPE && } - {/* ENS Name */} -
-
-
- + {/* Screen to select storage type */} + {configureProfileModal.onScreen === + ProfileScreenType.SELECT_STORAGE && ( +
+ {configureProfileModal.profileOptionSelected === + ProfileType.DM3_NAME ? ( + + ) : ( + + )}
-
+ )} - {fetchComponent(namingServiceSelected, dm3Configuration.chainId)} + {/* Screen to claim profile name */} + {configureProfileModal.onScreen === ProfileScreenType.CLAIM_NAME && + (configureProfileModal.profileOptionSelected === + ProfileType.DM3_NAME ? ( + + ) : ( + + ))}
); } diff --git a/packages/messenger-widget/src/components/ConfigureProfile/OwnStorage.tsx b/packages/messenger-widget/src/components/ConfigureProfile/OwnStorage.tsx new file mode 100644 index 000000000..98701e17a --- /dev/null +++ b/packages/messenger-widget/src/components/ConfigureProfile/OwnStorage.tsx @@ -0,0 +1,120 @@ +import { useContext, useState } from 'react'; +import { ModalContext } from '../../context/ModalContext'; +import { ProfileScreenType, ProfileType } from '../../utils/enum-type-utils'; +import { BUTTON_CLASS, NAME_SERVICES, namingServices } from './bl'; +import { ConfigureProfileContext } from './context/ConfigureProfileContext'; + +export function OwnStorage() { + const [errorMsg, setErrorMsg] = useState(null); + + const { configureProfileModal, setConfigureProfileModal } = + useContext(ModalContext); + + const { existingEnsName, namingServiceSelected, setNamingServiceSelected } = + useContext(ConfigureProfileContext); + + const isNameAlreadyConfigured = (): boolean => { + if ( + (existingEnsName?.endsWith('.gno') || + existingEnsName?.endsWith('.gnosis.eth')) && + namingServiceSelected === NAME_SERVICES.GENOME + ) { + setErrorMsg( + 'GNO name is already configured, only one GNO name can be configured at a time', + ); + return true; + } + if ( + existingEnsName?.endsWith('.eth') && + namingServiceSelected === NAME_SERVICES.ENS + ) { + setErrorMsg( + 'ENS name is already configured, only one ENS name can be configured at a time', + ); + return true; + } + return false; + }; + + const navigateToNextTab = () => { + if (!isNameAlreadyConfigured()) { + setErrorMsg(null); + setConfigureProfileModal({ + ...configureProfileModal, + onScreen: ProfileScreenType.CLAIM_NAME, + }); + } + }; + + return ( + <> +
+ Add new dm3 profile - claim DM3 profile - select storage +
+ +
+
+ {errorMsg ?? ''} +
+ +
+ +
+ +
+
+ To publish your dm3 profile, a transaction is sent to + set a text record in your ENS name or GNO name. + Transaction costs will apply for setting the profile and + administration. +
+
+ You can receive dm3 messages directly sent to your ENS + name or GNO name +
+
+
+
+ + +
+ + ); +} diff --git a/packages/messenger-widget/src/components/ConfigureProfile/ProfileTypeSelector.tsx b/packages/messenger-widget/src/components/ConfigureProfile/ProfileTypeSelector.tsx new file mode 100644 index 000000000..512e3c640 --- /dev/null +++ b/packages/messenger-widget/src/components/ConfigureProfile/ProfileTypeSelector.tsx @@ -0,0 +1,86 @@ +import { useContext } from 'react'; +import { ModalContext } from '../../context/ModalContext'; +import { ProfileScreenType, ProfileType } from '../../utils/enum-type-utils'; +import { BUTTON_CLASS } from './bl'; + +export function ProfileTypeSelector() { + const { configureProfileModal, setConfigureProfileModal } = + useContext(ModalContext); + + const profileOptions = [ + { + name: 'Claim a dm3 Name (dm3 cloud, Optimism, ...)', + type: ProfileType.DM3_NAME, + }, + { + name: 'use your own Name (ENS, GENOME, ...)', + type: ProfileType.OWN_NAME, + }, + ]; + + return ( +
+
+ Add new dm3 profile - select type +
+ +
+ {profileOptions.map((option, index) => ( +
+ setConfigureProfileModal({ + ...configureProfileModal, + profileOptionSelected: option.type, + }) + } + > + + +
+ ))} +
+
+ + +
+
+ ); +} diff --git a/packages/messenger-widget/src/components/ConfigureProfile/bl.tsx b/packages/messenger-widget/src/components/ConfigureProfile/bl.tsx index b50c7dfbb..bf5d2a678 100644 --- a/packages/messenger-widget/src/components/ConfigureProfile/bl.tsx +++ b/packages/messenger-widget/src/components/ConfigureProfile/bl.tsx @@ -151,7 +151,7 @@ export const fetchExistingDM3Name = async ( } }; -const enum NAME_SERVICES { +export const enum NAME_SERVICES { ENS = 'Ethereum Network - Ethereum Name Service (ENS)', GENOME = 'Gnosis Network - Genome/SpaceID', OPTIMISM = 'Optimism Network', diff --git a/packages/messenger-widget/src/components/ConfigureProfile/chain/MobileView.tsx b/packages/messenger-widget/src/components/ConfigureProfile/chain/MobileView.tsx index bcf3e5e7e..7b0535d0a 100644 --- a/packages/messenger-widget/src/components/ConfigureProfile/chain/MobileView.tsx +++ b/packages/messenger-widget/src/components/ConfigureProfile/chain/MobileView.tsx @@ -6,6 +6,9 @@ import { PROFILE_INPUT_FIELD_CLASS, BUTTON_CLASS, } from './common'; +import { ModalContext } from '../../../context/ModalContext'; +import { ConfigureDM3NameContext } from '../context/ConfigureDM3NameContext'; +import { ProfileScreenType, ProfileType } from '../../../utils/enum-type-utils'; export const MobileView = ({ propertyName, @@ -29,8 +32,14 @@ export const MobileView = ({ errorMsg, existingEnsName, setExistingEnsName, + setEnsName, } = useContext(ConfigureProfileContext); + const { setDm3Name } = useContext(ConfigureDM3NameContext); + + const { configureProfileModal, setConfigureProfileModal } = + useContext(ModalContext); + // handles configure or remove ENS name const handlePublishOrRemoveProfile = async (type: ACTION_TYPE) => { if (type === ACTION_TYPE.CONFIGURE) { @@ -46,11 +55,13 @@ export const MobileView = ({ }; return ( <> - {/* ENS Name */} -
+
-

+

{propertyName}

{showError === NAME_TYPE.ENS_NAME && errorMsg}
- {!existingEnsName ? ( -
{ - e.preventDefault(); - handlePublishOrRemoveProfile( - existingEnsName - ? ACTION_TYPE.REMOVE - : ACTION_TYPE.CONFIGURE, - ); - }} - > - , - ) => handleNameChange(e)} - /> -
- ) : ( -

- {existingEnsName} -

- )} -
- {!existingEnsName && ( - - )} -
+ +
{ + e.preventDefault(); + handlePublishOrRemoveProfile( + ACTION_TYPE.CONFIGURE, + ); + }} + > + , + ) => handleNameChange(e)} + /> +
+
{label}
{note}
+ +
+ + +
diff --git a/packages/messenger-widget/src/components/ConfigureProfile/chain/NormalView.tsx b/packages/messenger-widget/src/components/ConfigureProfile/chain/NormalView.tsx index 0e95ebea1..2a0c9d6fe 100644 --- a/packages/messenger-widget/src/components/ConfigureProfile/chain/NormalView.tsx +++ b/packages/messenger-widget/src/components/ConfigureProfile/chain/NormalView.tsx @@ -1,5 +1,4 @@ import { useContext } from 'react'; -import tickIcon from '../../../assets/images/white-tick.svg'; import { ConfigureProfileContext } from '../context/ConfigureProfileContext'; import { ACTION_TYPE, @@ -7,6 +6,9 @@ import { PROFILE_INPUT_FIELD_CLASS, BUTTON_CLASS, } from './common'; +import { ModalContext } from '../../../context/ModalContext'; +import { ProfileScreenType, ProfileType } from '../../../utils/enum-type-utils'; +import { ConfigureDM3NameContext } from '../context/ConfigureDM3NameContext'; export const NormalView = ({ propertyName, @@ -30,8 +32,14 @@ export const NormalView = ({ errorMsg, existingEnsName, setExistingEnsName, + setEnsName, } = useContext(ConfigureProfileContext); + const { setDm3Name } = useContext(ConfigureDM3NameContext); + + const { configureProfileModal, setConfigureProfileModal } = + useContext(ModalContext); + // handles configure or remove ENS name const handlePublishOrRemoveProfile = async (type: ACTION_TYPE) => { if (type === ACTION_TYPE.CONFIGURE) { @@ -45,110 +53,103 @@ export const NormalView = ({ setExistingEnsName(null); } }; + return ( <> - {/* ENS Name */} -
-
-
- -
-

- {propertyName} -

- -
- {showError === NAME_TYPE.ENS_NAME && errorMsg} -
-
-
- -
-
- {existingEnsName && } -
+
-

+

+ {propertyName} +

+
+ {showError === NAME_TYPE.ENS_NAME && errorMsg} +
+
+
+

{propertyName}

- {!existingEnsName ? ( -
{ - e.preventDefault(); - handlePublishOrRemoveProfile( - existingEnsName - ? ACTION_TYPE.REMOVE - : ACTION_TYPE.CONFIGURE, - ); - }} - > - , - ) => handleNameChange(e)} - /> -
- ) : ( -

- {existingEnsName} -

- )} +
{ + e.preventDefault(); + handlePublishOrRemoveProfile( + ACTION_TYPE.CONFIGURE, + ); + }} + > + , + ) => handleNameChange(e)} + /> +
-
-
+ +
+
{label}
{note}
-
- {!existingEnsName && ( - +
+ +
+ +
); diff --git a/packages/messenger-widget/src/components/ConfigureProfile/chain/ens/ConfigureEnsProfile.tsx b/packages/messenger-widget/src/components/ConfigureProfile/chain/ens/ConfigureEnsProfile.tsx index 6116de421..4d6925b18 100644 --- a/packages/messenger-widget/src/components/ConfigureProfile/chain/ens/ConfigureEnsProfile.tsx +++ b/packages/messenger-widget/src/components/ConfigureProfile/chain/ens/ConfigureEnsProfile.tsx @@ -11,7 +11,6 @@ import { ModalContext } from '../../../../context/ModalContext'; import { ConfigureDM3NameContext } from '../../context/ConfigureDM3NameContext'; import { fetchChainIdFromServiceName } from '../../bl'; import { DM3ConfigurationContext } from '../../../../context/DM3ConfigurationContext'; -import { DeliveryServiceContext } from '../../../../context/DeliveryServiceContext'; export const ConfigureEnsProfile = (props: IChain) => { const connectedChainId = useChainId(); diff --git a/packages/messenger-widget/src/components/ConfigureProfile/chain/ens/bl.ts b/packages/messenger-widget/src/components/ConfigureProfile/chain/ens/bl.ts index 9ca788755..9b29442c1 100644 --- a/packages/messenger-widget/src/components/ConfigureProfile/chain/ens/bl.ts +++ b/packages/messenger-widget/src/components/ConfigureProfile/chain/ens/bl.ts @@ -3,10 +3,11 @@ import { ethersHelper, stringify } from '@dm3-org/dm3-lib-shared'; import { ethers } from 'ethers'; import { closeLoader, startLoader } from '../../../Loader/Loader'; import { NAME_TYPE } from '../common'; +import ENS from '@ensdomains/ensjs'; // method to check ENS name is valid or not const isEnsNameValid = async ( - mainnetProvider: ethers.providers.StaticJsonRpcProvider, + mainnetProvider: ethers.providers.JsonRpcProvider, ensName: string, ethAddress: string, setError: (type: NAME_TYPE | undefined, msg: string) => void, @@ -17,15 +18,15 @@ const isEnsNameValid = async ( return false; } - const address = await ethersHelper.resolveOwner(mainnetProvider!, ensName); + // Fetch owner of ENS name + const ens = getEnsUtils(mainnetProvider); + const owner = await ens.name(ensName).getAddress(); - if (address === null) { + if (owner === null) { setError(NAME_TYPE.ENS_NAME, 'Resolver not found'); return false; } - const owner = await ethersHelper.resolveName(mainnetProvider!, ensName); - if ( owner && ethersHelper.formatAddress(owner) !== @@ -77,7 +78,12 @@ export const submitEnsNameTransaction = async ( const response = await ethersHelper.executeTransaction(tx); await response.wait(); setEnsNameFromResolver(ensName); + // stop loader + closeLoader(); + return true; } else { + // stop loader + closeLoader(); throw Error('Error creating publish transaction'); } } catch (e: any) { @@ -88,10 +94,10 @@ export const submitEnsNameTransaction = async ( ? 'User rejected transaction' : 'You are not the owner/manager of this name', ); + // stop loader + closeLoader(); + return false; } - - // stop loader - closeLoader(); }; export async function getPublishProfileOnchainTransaction( @@ -109,17 +115,17 @@ export async function getPublishProfileOnchainTransaction( throw Error('No signature'); } - const ethersResolver = await ethersHelper.getResolver( - mainnetProvider, - ensName, - ); + const ens = getEnsUtils(mainnetProvider); + + // Fetch resolver of account + const ensResolverAddress = await ens.name(ensName).getResolver(); - if (!ethersResolver) { + if (!ensResolverAddress) { throw Error('No resolver found'); } const resolver = ethersHelper.getConractInstance( - ethersResolver.address, + ensResolverAddress, [ 'function setText(bytes32 node, string calldata key, string calldata value) external', ], @@ -141,3 +147,12 @@ export async function getPublishProfileOnchainTransaction( args: [node, key, value], }; } + +const getEnsUtils = ( + mainnetProvider: ethers.providers.StaticJsonRpcProvider, +) => { + return new ENS({ + provider: mainnetProvider, + ensAddress: '0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e', + }); +}; diff --git a/packages/messenger-widget/src/components/ConfigureProfile/chain/genome/bl.ts b/packages/messenger-widget/src/components/ConfigureProfile/chain/genome/bl.ts index ff72ff4ab..cd778d769 100644 --- a/packages/messenger-widget/src/components/ConfigureProfile/chain/genome/bl.ts +++ b/packages/messenger-widget/src/components/ConfigureProfile/chain/genome/bl.ts @@ -132,7 +132,12 @@ export const submitGenomeNameTransaction = async ( const response = await ethersHelper.executeTransaction(tx); await response.wait(); setEnsNameFromResolver(ensName); + // stop loader + closeLoader(); + return true; } else { + // stop loader + closeLoader(); throw Error('Error creating publish transaction'); } } catch (e: any) { @@ -143,10 +148,10 @@ export const submitGenomeNameTransaction = async ( ? 'User rejected transaction' : 'You are not the owner/manager of this name', ); + // stop loader + closeLoader(); + return false; } - - // stop loader - closeLoader(); }; export const validateGenomeName = (ensName: string) => { diff --git a/packages/messenger-widget/src/components/ConfigureProfile/context/ConfigureProfileContext.tsx b/packages/messenger-widget/src/components/ConfigureProfile/context/ConfigureProfileContext.tsx index 8bc5127a1..ede84923e 100644 --- a/packages/messenger-widget/src/components/ConfigureProfile/context/ConfigureProfileContext.tsx +++ b/packages/messenger-widget/src/components/ConfigureProfile/context/ConfigureProfileContext.tsx @@ -1,6 +1,8 @@ -import React, { useState } from 'react'; +import React, { useContext, useEffect, useState } from 'react'; import { NAME_TYPE } from '../chain/common'; import { dm3NamingServices, namingServices } from '../bl'; +import { AuthContext } from '../../../context/AuthContext'; +import { DM3ConfigurationContext } from '../../../context/DM3ConfigurationContext'; export interface ConfigureProfileContextType { existingEnsName: string | null; @@ -50,6 +52,21 @@ export const ConfigureProfileContextProvider = (props: { children?: any }) => { namingServices[0].name, ); + const { displayName } = useContext(AuthContext); + const { dm3Configuration } = useContext(DM3ConfigurationContext); + + useEffect(() => { + setExistingEnsName( + displayName + ? !displayName.endsWith(dm3Configuration.addressEnsSubdomain) && + !displayName.endsWith(dm3Configuration.userEnsSubdomain) && + !displayName.endsWith('.op.dm3.eth') + ? displayName + : null + : null, + ); + }, []); + return ( { const { setDisplayName } = useContext(AuthContext); - const { errorMsg, showError } = useContext(ConfigureProfileContext); + const { errorMsg, showError, setEnsName, onShowError } = useContext( + ConfigureProfileContext, + ); const { dm3Name, - existingDm3Name, - showDeleteConfirmation, handleNameChange, handleClaimOrRemoveDm3Name, - updateDeleteConfirmation, - setShowDeleteConfirmation, + setDm3Name, } = useContext(ConfigureDM3NameContext); + const { configureProfileModal, setConfigureProfileModal } = + useContext(ModalContext); + return (
- {/* Delete DM3 name confirmation popup modal */} - {showDeleteConfirmation && ( - - )}

{showError === NAME_TYPE.DM3_NAME && errorMsg}

- {!existingDm3Name ? ( -
{ - e.preventDefault(); - handleClaimOrRemoveDm3Name( - ACTION_TYPE.CONFIGURE, - setDisplayName, - submitDm3UsernameClaim, - ); - }} - > - , - ) => handleNameChange(e, NAME_TYPE.DM3_NAME)} - /> -

- {nameExtension} -

-
- ) : ( - <> -

- {existingDm3Name} - remove - setShowDeleteConfirmation(true) - } - /> -

- - )} -
-
- {!existingDm3Name && ( - - )} + {nameExtension} +

+
+
You can get a DM3 name for free. Please check if your @@ -146,6 +101,45 @@ export const MobileView = ({ You can receive messages sent to your full DM3 username.
+ +
+ + +
); diff --git a/packages/messenger-widget/src/components/ConfigureProfile/dm3Names/NormalView.tsx b/packages/messenger-widget/src/components/ConfigureProfile/dm3Names/NormalView.tsx index 1a4456e3c..c11ab3acd 100644 --- a/packages/messenger-widget/src/components/ConfigureProfile/dm3Names/NormalView.tsx +++ b/packages/messenger-widget/src/components/ConfigureProfile/dm3Names/NormalView.tsx @@ -1,7 +1,6 @@ +import './../ConfigureProfile.css'; import { useContext } from 'react'; import { AuthContext } from '../../../context/AuthContext'; -import DeleteDM3Name from '../../DeleteDM3Name/DeleteDM3Name'; -import tickIcon from './../../../assets/images/white-tick.svg'; import { ConfigureProfileContext } from '../context/ConfigureProfileContext'; import { ConfigureDM3NameContext } from '../context/ConfigureDM3NameContext'; import { @@ -10,7 +9,8 @@ import { NAME_TYPE, PROFILE_INPUT_FIELD_CLASS, } from '../chain/common'; -import deleteIcon from '../../../assets/images/delete.svg'; +import { ProfileScreenType, ProfileType } from '../../../utils/enum-type-utils'; +import { ModalContext } from '../../../context/ModalContext'; export const NormalView = ({ nameExtension, @@ -23,55 +23,39 @@ export const NormalView = ({ }) => { const { setDisplayName } = useContext(AuthContext); - const { errorMsg, showError } = useContext(ConfigureProfileContext); + const { errorMsg, showError, onShowError, setEnsName } = useContext( + ConfigureProfileContext, + ); const { dm3Name, - existingDm3Name, - showDeleteConfirmation, handleNameChange, handleClaimOrRemoveDm3Name, - updateDeleteConfirmation, - setShowDeleteConfirmation, + setDm3Name, } = useContext(ConfigureDM3NameContext); + const { configureProfileModal, setConfigureProfileModal } = + useContext(ModalContext); + return ( <> -
- {/* Delete DM3 name confirmation popup modal */} - {showDeleteConfirmation && ( - - )} - -
-
- -
-

- DM3 Name -

- -
- {showError === NAME_TYPE.DM3_NAME && errorMsg} -
-
-
-
-
- {existingDm3Name && } -
+
+

+ DM3 Name +

+
+ {showError === NAME_TYPE.DM3_NAME && errorMsg} +
+

{ - e.preventDefault(); - handleClaimOrRemoveDm3Name( - ACTION_TYPE.CONFIGURE, - setDisplayName, - submitDm3UsernameClaim, - ); - }} - > - , - ) => - handleNameChange(e, NAME_TYPE.DM3_NAME) - } - /> -

{ + e.preventDefault(); + handleClaimOrRemoveDm3Name( + ACTION_TYPE.CONFIGURE, + setDisplayName, + submitDm3UsernameClaim, + ); + }} + > + , + ) => handleNameChange(e, NAME_TYPE.DM3_NAME)} + /> +

- {nameExtension} -

- - ) : ( - <> -

- {existingDm3Name} - remove - setShowDeleteConfirmation(true) - } - /> -

- - )} + > + {nameExtension} +

+
-
-
+ +
+
You can get a DM3 name for free. Please check if your desired name is available. DM3 names are created and managed on Layer2 (e.g. Optimism). Small @@ -148,29 +112,45 @@ export const NormalView = ({
-
- {!existingDm3Name && ( - +
+ +
+ +
); diff --git a/packages/messenger-widget/src/components/ConfigureProfile/dm3Names/cloudName/ConfigureCloudNameProfile.tsx b/packages/messenger-widget/src/components/ConfigureProfile/dm3Names/cloudName/ConfigureCloudNameProfile.tsx index 4076db81e..9251ac01b 100644 --- a/packages/messenger-widget/src/components/ConfigureProfile/dm3Names/cloudName/ConfigureCloudNameProfile.tsx +++ b/packages/messenger-widget/src/components/ConfigureProfile/dm3Names/cloudName/ConfigureCloudNameProfile.tsx @@ -1,22 +1,23 @@ import { useContext, useEffect } from 'react'; -import { DM3Name } from './../DM3Name'; +import { DM3Name } from '../DM3Name'; import { NAME_TYPE } from '../../chain/common'; import { AuthContext } from '../../../../context/AuthContext'; import { closeLoader, startLoader } from '../../../Loader/Loader'; import { claimSubdomain } from '../../../../adapters/offchainResolverApi'; import { ConfigureDM3NameContext } from '../../context/ConfigureDM3NameContext'; import { DM3ConfigurationContext } from '../../../../context/DM3ConfigurationContext'; -import { useMainnetProvider } from '../../../../hooks/mainnetprovider/useMainnetProvider'; import { ModalContext } from '../../../../context/ModalContext'; import { ConfigureProfileContext } from '../../context/ConfigureProfileContext'; +import { ProfileScreenType } from '../../../../utils/enum-type-utils'; export const ConfigureCloudNameProfile = () => { - const mainnetProvider = useMainnetProvider(); - const { setLoaderContent } = useContext(ModalContext); const { dm3Configuration } = useContext(DM3ConfigurationContext); + const { configureProfileModal, setConfigureProfileModal } = + useContext(ModalContext); + const { dm3NameServiceSelected } = useContext(ConfigureProfileContext); const { setExistingDm3Name, setError, setDm3Name } = useContext( @@ -47,6 +48,10 @@ export const ConfigureCloudNameProfile = () => { setDisplayName(ensName); setExistingDm3Name(ensName); + setConfigureProfileModal({ + ...configureProfileModal, + onScreen: ProfileScreenType.NONE, + }); } } catch (e) { setError('Name is not available', NAME_TYPE.DM3_NAME); diff --git a/packages/messenger-widget/src/components/ConfigureProfile/dm3Names/optimismName/ConfigureOptimismNameProfile.tsx b/packages/messenger-widget/src/components/ConfigureProfile/dm3Names/optimismName/ConfigureOptimismNameProfile.tsx index 03e1c0b9a..ae6aa2f88 100644 --- a/packages/messenger-widget/src/components/ConfigureProfile/dm3Names/optimismName/ConfigureOptimismNameProfile.tsx +++ b/packages/messenger-widget/src/components/ConfigureProfile/dm3Names/optimismName/ConfigureOptimismNameProfile.tsx @@ -3,22 +3,27 @@ import { useContext, useEffect, useState } from 'react'; import { useChainId, useSwitchNetwork } from 'wagmi'; import { AuthContext } from '../../../../context/AuthContext'; import { ConfigureDM3NameContext } from '../../context/ConfigureDM3NameContext'; -import { closeLoader, startLoader } from './../../../Loader/Loader'; -import { IChain, NAME_TYPE } from './../../chain/common'; -import { DM3Name } from './../DM3Name'; +import { closeLoader, startLoader } from '../../../Loader/Loader'; +import { IChain, NAME_TYPE } from '../../chain/common'; +import { DM3Name } from '../DM3Name'; import { publishProfile } from './tx/publishProfile'; import { registerOpName } from './tx/registerOpName'; import { ModalContext } from '../../../../context/ModalContext'; import { fetchChainIdFromDM3ServiceName } from '../../bl'; import { DM3ConfigurationContext } from '../../../../context/DM3ConfigurationContext'; import { ConfigureProfileContext } from '../../context/ConfigureProfileContext'; +import { ProfileScreenType } from '../../../../utils/enum-type-utils'; export const ConfigureOptimismNameProfile = (props: IChain) => { const connectedChainId = useChainId(); const { switchNetwork } = useSwitchNetwork(); - const { setLoaderContent } = useContext(ModalContext); + const { + setLoaderContent, + configureProfileModal, + setConfigureProfileModal, + } = useContext(ModalContext); const { account, setDisplayName } = useContext(AuthContext); @@ -89,6 +94,10 @@ export const ConfigureOptimismNameProfile = (props: IChain) => { setDisplayName(ensName); setExistingDm3Name(ensName); + setConfigureProfileModal({ + ...configureProfileModal, + onScreen: ProfileScreenType.NONE, + }); } catch (e) { // check user rejects setError('Name is not available', NAME_TYPE.DM3_NAME); diff --git a/packages/messenger-widget/src/components/ConfigureProfile/dm3Names/optimismName/tx/publishProfile.ts b/packages/messenger-widget/src/components/ConfigureProfile/dm3Names/optimismName/tx/publishProfile.ts index 67eac8e29..071063e45 100644 --- a/packages/messenger-widget/src/components/ConfigureProfile/dm3Names/optimismName/tx/publishProfile.ts +++ b/packages/messenger-widget/src/components/ConfigureProfile/dm3Names/optimismName/tx/publishProfile.ts @@ -37,4 +37,5 @@ export const publishProfile = async ( }); console.log('publishProfile Tx res', publishProfileTx); await publishProfileTx.wait(); + return true; }; diff --git a/packages/messenger-widget/src/components/DM3/DM3.tsx b/packages/messenger-widget/src/components/DM3/DM3.tsx index bf26b95f6..100af01c1 100644 --- a/packages/messenger-widget/src/components/DM3/DM3.tsx +++ b/packages/messenger-widget/src/components/DM3/DM3.tsx @@ -11,6 +11,7 @@ import { Loader, startLoader } from '../Loader/Loader'; import { SignIn } from '../SignIn/SignIn'; import { Siwe } from '../Siwe/Siwe'; import { NotificationContextProvider } from '../../context/NotificationContext'; +import { DM3UserProfileContextProvider } from '../../context/DM3UserProfileContext'; function DM3(props: Dm3Props) { const { @@ -61,24 +62,28 @@ function DM3(props: Dm3Props) { return (
- - - - {!isProfileReady ? ( - props.config.siwe ? ( - + + + + + {!isProfileReady ? ( + props.config.siwe ? ( + + ) : ( + + ) ) : ( - - ) - ) : ( - -
- -
-
- )} -
-
+ +
+ +
+
+ )} +
+
+
); } diff --git a/packages/messenger-widget/src/components/Preferences/DM3Profile/DM3Profile.tsx b/packages/messenger-widget/src/components/Preferences/DM3Profile/DM3Profile.tsx index a486a2b41..f9591d630 100644 --- a/packages/messenger-widget/src/components/Preferences/DM3Profile/DM3Profile.tsx +++ b/packages/messenger-widget/src/components/Preferences/DM3Profile/DM3Profile.tsx @@ -13,8 +13,8 @@ export function DM3Profile() { const description = screenWidth <= MOBILE_SCREEN_WIDTH ? '' - : 'Your dm3 profile needs to be published. You can use your own ENS name, ' + - 'get a DM3 name, or keep your wallet address.'; + : 'Your dm3 profile needs to be published. You can use your own web3 name (ENS, ...),' + + 'get a dm3 name, or keep your wallet address.'; return (
diff --git a/packages/messenger-widget/src/components/Preferences/Heading/Heading.tsx b/packages/messenger-widget/src/components/Preferences/Heading/Heading.tsx index 8603c8efc..9f1d186ee 100644 --- a/packages/messenger-widget/src/components/Preferences/Heading/Heading.tsx +++ b/packages/messenger-widget/src/components/Preferences/Heading/Heading.tsx @@ -14,8 +14,11 @@ export interface IHeading { export function Heading(props: IHeading) { const { screenWidth } = useContext(DM3ConfigurationContext); - const { setShowPreferencesModal, setShowProfileConfigurationModal } = - useContext(ModalContext); + const { + setShowPreferencesModal, + setShowProfileConfigurationModal, + resetConfigureProfileModal, + } = useContext(ModalContext); return (
@@ -33,6 +36,7 @@ export function Heading(props: IHeading) { src={closeIcon} alt="close" onClick={() => { + resetConfigureProfileModal(); setShowPreferencesModal(false); closeConfigurationModal( setShowProfileConfigurationModal, diff --git a/packages/messenger-widget/src/components/Preferences/MobileView.tsx b/packages/messenger-widget/src/components/Preferences/MobileView.tsx index 08e430182..6686d16a6 100644 --- a/packages/messenger-widget/src/components/Preferences/MobileView.tsx +++ b/packages/messenger-widget/src/components/Preferences/MobileView.tsx @@ -1,6 +1,6 @@ import './Preferences.css'; import { preferencesItems } from './bl'; -import { useContext, useEffect, useState } from 'react'; +import { useContext, useEffect } from 'react'; import closeIcon from '../../assets/images/cross.svg'; import { closeConfigurationModal } from '../ConfigureProfile/bl'; import { ModalContext } from '../../context/ModalContext'; @@ -10,9 +10,12 @@ export function MobileView() { setShowPreferencesModal, showProfileConfigurationModal, setShowProfileConfigurationModal, + resetConfigureProfileModal, + preferencesOptionSelected, + setPreferencesOptionSelected, } = useContext(ModalContext); - const [optionChoosen, setOptionChoosen] = useState(null); + // const [optionChoosen, setOptionChoosen] = useState(null); /** * Opens DM3 profile configuration by default if user clicked @@ -20,10 +23,20 @@ export function MobileView() { */ useEffect(() => { if (showProfileConfigurationModal) { - setOptionChoosen(preferencesItems[1]); + setPreferencesOptionSelected(preferencesItems[1]); } }, []); + // reset states of configure profile modal if any other component is loaded + useEffect(() => { + if ( + preferencesOptionSelected && + preferencesOptionSelected.name !== 'dm3 Profile' + ) { + resetConfigureProfileModal(); + } + }, [preferencesOptionSelected]); + return (
- setOptionChoosen(item) + setPreferencesOptionSelected( + item, + ) } > {item.icon} @@ -66,6 +81,7 @@ export function MobileView() { src={closeIcon} alt="close" onClick={() => { + resetConfigureProfileModal(); setShowPreferencesModal(false); closeConfigurationModal( setShowProfileConfigurationModal, @@ -75,9 +91,9 @@ export function MobileView() {
- {optionChoosen && - optionChoosen.isEnabled && - optionChoosen.component} + {preferencesOptionSelected && + preferencesOptionSelected.isEnabled && + preferencesOptionSelected.component}
diff --git a/packages/messenger-widget/src/components/Preferences/Network/Network.css b/packages/messenger-widget/src/components/Preferences/Network/Network.css new file mode 100644 index 000000000..6921e58c5 --- /dev/null +++ b/packages/messenger-widget/src/components/Preferences/Network/Network.css @@ -0,0 +1,155 @@ +.node-icon-active { + fill: var(--text-primary-color) !important; +} + +.network { + padding: 2rem; +} + +.node-name{ + min-width: 10rem; +} + +.network-text{ + font-size: 1rem; +} + +.node-btn { + padding: 5px 30px 5px 30px; + box-shadow: 0px 0px 44px 0px var(--button-shadow); + width: fit-content; + font-weight: 400; + font-size: 12px; + border-radius: 4px; + line-height: 24px; +} + +.add-node-btn-disabled { + background: var(--normal-btn-inactive); + color: var(--disabled-btn-text); + border: none; +} + +.add-node-btn-active { + background-color: var(--normal-btn-hover); + border: var(--normal-btn-border); + color: var(--text-primary-color); +} + +.node-name-input-field { + line-height: 24px; + border-radius: 6px; + font-size: 0.8rem; + font-weight: 400; + border: 1px solid var(--input-field-border-color); + padding: 0.3rem 1rem 0.3rem 1rem; + color: var(--input-field-text-color); + background: var(--input-field-background-color); + width: 100%; +} + +.add-node-name-container{ + border: 1px solid var(--text-primary-color); + border-radius: 5px; +} + +.node-name-inner-container{ + padding: 2rem 3rem 1rem 2rem; + align-items: center; + justify-content: space-between; +} + +.node-name-form{ + width: fit-content; + min-width: 60%; +} + +.add-name-btn-container{ + margin-bottom: 1rem; + margin-right: 3rem; +} + +.add-name-error{ + font-size: 0.8rem; + line-height: 14px; + font-style: italic; + color: var(--error-text); + margin-left: 1rem; + display: flex; + justify-content: center; +} + +.node-icon { + height: 1.1rem; +} + +.update-profile-box{ + font-size: 13px; + display: block; + text-align: center; + background-color: var(--configure-profile-modal-background-color); + color: #1F2029; + font-weight: 500; + padding: 0.7rem; + border-radius: 3px; + width: 80%; +} + +.cancel-btn { + background-color: transparent; + border: 2px solid var(--preferences-highlighted-color); + color: var(--text-primary-color); +} + +@media only screen and (min-width: 800px) and (max-width: 1150px) { + .add-name-error { + font-size: 0.7rem !important; + } +} + +@media only screen and (max-width: 800px) { + .network { + padding: 1rem; + } + + .network-text{ + font-size: 0.75rem; + } + + .node-name-input-field { + padding: 0.2rem 0.5rem 0.2rem 0.5rem; + width: 100%; + font-size: 12px; + } + + .node-name-input-field::placeholder{ + font-size: 10px !important; + } + + .add-name-error { + margin-left: 0px !important; + height: 14px; + font-size: 0.6rem !important; + } + + .node-name-inner-container{ + display: flex; + flex-direction: column; + padding: 1rem 0rem 0rem 1rem; + width: 100%; + align-items: flex-start; + } + + .node-name-form{ + width: 90%; + } + + .add-name-btn-container{ + margin-right: 2rem; + } + + .update-profile-box{ + margin-top: 1rem; + } + +} \ No newline at end of file diff --git a/packages/messenger-widget/src/components/Preferences/Network/Network.tsx b/packages/messenger-widget/src/components/Preferences/Network/Network.tsx index c71cad5c2..a8013f4f5 100644 --- a/packages/messenger-widget/src/components/Preferences/Network/Network.tsx +++ b/packages/messenger-widget/src/components/Preferences/Network/Network.tsx @@ -1,15 +1,200 @@ +import './Network.css'; import { Heading } from '../Heading/Heading'; +import deleteIcon from '../../../assets/images/delete.svg'; +import { useContext, useEffect } from 'react'; +import { DM3UserProfileContext } from '../../../context/DM3UserProfileContext'; +import { ModalContext } from '../../../context/ModalContext'; +import { preferencesItems } from '../bl'; export function Network() { const heading = 'Network'; - const description = - 'Prevent spam from being sent to you by setting rules ' + - 'that senders must fulfill in order for messages to be accepted.'; + const description = 'Define how you will be connected with the dm3 network'; + + const { + error, + nodes, + deleteNode, + isModalOpenToAddNode, + setIsModalOpenToAddNode, + nodeName, + addNode, + handleNodeNameChange, + isProfileUpdated, + setNodeName, + setError, + } = useContext(DM3UserProfileContext); + + const { setPreferencesOptionSelected } = useContext(ModalContext); + + // input field modal should remain close on initial load of network screen + useEffect(() => { + setIsModalOpenToAddNode(false); + }, []); return (
-
+
+
+ Connected to delivery service nodes: +
+ +
+
+ {nodes.dsNames.map((data, index) => { + return ( +
+
+ {index + 1}. + {data} +
+
+ {/* Delete node option is shown only when more than 1 nodes exists */} + {nodes.dsNames.length > 1 && ( + remove { + deleteNode(index); + }} + /> + )} +
+
+ ); + })} +
+ + {!isProfileUpdated && ( +
+ +
+ {' '} + Your profile configuration needs to be + updated!{' '} +
+ + setPreferencesOptionSelected( + preferencesItems[1], + ) + } + > + Update your profiles now! + +
+
+ )} +
+ + {/* Button to add new Node */} +
+ +
+ + {/* Modal to add new NODE */} + {isModalOpenToAddNode && ( +
+
+ Add dm3 delivery service node +
+ + {/* Error msg container for invalid DS name */} +
+
+

+
+ {/* Error msg */} +
+
+ {error ?? ''} +
+
+
+ +
+
+

+ DM3 delivery service node’s name: +

+
+
+
{ + e.preventDefault(); + addNode(); + }} + > + , + ) => handleNodeNameChange(e)} + /> +
+
+
+ + {/* Button to submit node name */} +
+ + +
+
+ )} +
); } diff --git a/packages/messenger-widget/src/components/Preferences/NormalView.tsx b/packages/messenger-widget/src/components/Preferences/NormalView.tsx index 82fe6bdf3..726d68f0f 100644 --- a/packages/messenger-widget/src/components/Preferences/NormalView.tsx +++ b/packages/messenger-widget/src/components/Preferences/NormalView.tsx @@ -11,9 +11,12 @@ export function NormalView() { setShowPreferencesModal, showProfileConfigurationModal, setShowProfileConfigurationModal, + resetConfigureProfileModal, + preferencesOptionSelected, + setPreferencesOptionSelected, } = useContext(ModalContext); - const [optionChoosen, setOptionChoosen] = useState(null); + // const [optionChoosen, setOptionChoosen] = useState(null); /** * Opens DM3 profile configuration by default if user clicked @@ -21,10 +24,20 @@ export function NormalView() { */ useEffect(() => { if (showProfileConfigurationModal) { - setOptionChoosen(preferencesItems[1]); + setPreferencesOptionSelected(preferencesItems[1]); } }, []); + // reset states of configure profile modal if any other component is loaded + useEffect(() => { + if ( + preferencesOptionSelected && + preferencesOptionSelected.name !== 'dm3 Profile' + ) { + resetConfigureProfileModal(); + } + }, [preferencesOptionSelected]); + return (
- setOptionChoosen(item) + setPreferencesOptionSelected( + item, + ) } > {item.icon} @@ -91,8 +106,9 @@ export function NormalView() {
- {optionChoosen && optionChoosen.isEnabled ? ( - optionChoosen.component + {preferencesOptionSelected && + preferencesOptionSelected.isEnabled ? ( + preferencesOptionSelected.component ) : (
close { + resetConfigureProfileModal(); setShowPreferencesModal(false); closeConfigurationModal( setShowProfileConfigurationModal, diff --git a/packages/messenger-widget/src/components/Preferences/Preferences.test.tsx b/packages/messenger-widget/src/components/Preferences/Preferences.test.tsx deleted file mode 100644 index 4fc46a390..000000000 --- a/packages/messenger-widget/src/components/Preferences/Preferences.test.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import { render } from '@testing-library/react'; -import { Preferences } from './Preferences'; -import '@testing-library/jest-dom'; - -describe('Preferences test cases', () => { - it('Renders Preferences component', () => { - const { container } = render(); - const element = container.getElementsByClassName( - 'preferences-modal-content', - ); - expect(element[0]).toBeInTheDocument(); - }); -}); diff --git a/packages/messenger-widget/src/components/Preferences/bl.tsx b/packages/messenger-widget/src/components/Preferences/bl.tsx index 3afde13c2..7ad12578b 100644 --- a/packages/messenger-widget/src/components/Preferences/bl.tsx +++ b/packages/messenger-widget/src/components/Preferences/bl.tsx @@ -54,7 +54,7 @@ export const preferencesItems = [ ), name: 'Network', component: , - isEnabled: false, + isEnabled: true, }, { icon: ( diff --git a/packages/messenger-widget/src/context/DM3UserProfileContext.tsx b/packages/messenger-widget/src/context/DM3UserProfileContext.tsx new file mode 100644 index 000000000..dcb9ca6e5 --- /dev/null +++ b/packages/messenger-widget/src/context/DM3UserProfileContext.tsx @@ -0,0 +1,100 @@ +import React from 'react'; +import { + INodeDetails, + useDm3UserProfile, +} from '../hooks/userProfile/useDm3UserProfile'; + +export type DM3UserProfileContextType = { + initialize: () => void; + updateProfileForAddressName: () => void; + updateProfileWithTransaction: (existingName: string) => void; + addNode: () => void; + deleteNode: (id: number) => void; + nodes: INodeDetails; + isModalOpenToAddNode: boolean; + setIsModalOpenToAddNode: (action: boolean) => void; + error: string | null; + setError: (msg: string | null) => void; + nodeName: string; + handleNodeNameChange: (e: React.ChangeEvent) => void; + setNodeName: (name: string) => void; + isProfileUpdated: boolean; + isProfileUpdatedForAddrName: boolean; + isProfileUpdatedForDm3Name: boolean; + isProfileUpdatedForEnsName: boolean; +}; + +export const DM3UserProfileContext = + React.createContext({ + initialize: () => {}, + updateProfileForAddressName: () => {}, + updateProfileWithTransaction: (existingName: string) => {}, + addNode: () => {}, + deleteNode: (id: number) => {}, + nodes: { + dsNames: [''], + }, + isModalOpenToAddNode: false, + setIsModalOpenToAddNode: (action: boolean) => {}, + error: null, + setError: (msg: string) => {}, + nodeName: '', + handleNodeNameChange: (e: React.ChangeEvent) => {}, + setNodeName: (name: string) => {}, + isProfileUpdated: true, + isProfileUpdatedForAddrName: true, + isProfileUpdatedForDm3Name: true, + isProfileUpdatedForEnsName: true, + }); + +export const DM3UserProfileContextProvider = ({ + children, +}: { + children?: any; +}) => { + const { + initialize, + addNode, + updateProfileForAddressName, + updateProfileWithTransaction, + deleteNode, + nodes, + isModalOpenToAddNode, + setIsModalOpenToAddNode, + error, + setError, + nodeName, + handleNodeNameChange, + isProfileUpdated, + isProfileUpdatedForAddrName, + isProfileUpdatedForDm3Name, + isProfileUpdatedForEnsName, + setNodeName, + } = useDm3UserProfile(); + + return ( + + {children} + + ); +}; diff --git a/packages/messenger-widget/src/context/ModalContext.tsx b/packages/messenger-widget/src/context/ModalContext.tsx index bbbb83439..57b143007 100644 --- a/packages/messenger-widget/src/context/ModalContext.tsx +++ b/packages/messenger-widget/src/context/ModalContext.tsx @@ -1,7 +1,16 @@ import React from 'react'; import { NewContact } from '../interfaces/utils'; -import { MessageActionType } from '../utils/enum-type-utils'; -import { IOpenEmojiPopup, useModal } from '../hooks/modals/useModal'; +import { + MessageActionType, + ProfileScreenType, + ProfileType, +} from '../utils/enum-type-utils'; +import { + IConfigureProfileModal, + IOpenEmojiPopup, + useModal, +} from '../hooks/modals/useModal'; +import { preferencesItems } from '../components/Preferences/bl'; export type ModalContextType = { loaderContent: string; @@ -22,7 +31,22 @@ export type ModalContextType = { setShowAboutModal: (show: boolean) => void; showAddConversationModal: boolean; setShowAddConversationModal: (show: boolean) => void; + configureProfileModal: IConfigureProfileModal; + setConfigureProfileModal: (modal: IConfigureProfileModal) => void; + resetConfigureProfileModal: () => void; resetModalStates: () => void; + preferencesOptionSelected: { + icon: JSX.Element; + name: string; + component: JSX.Element; + isEnabled: boolean; + }; + setPreferencesOptionSelected: (item: { + icon: JSX.Element; + name: string; + component: JSX.Element; + isEnabled: boolean; + }) => void; }; export const ModalContext = React.createContext({ @@ -48,7 +72,20 @@ export const ModalContext = React.createContext({ setShowAboutModal: (show: boolean) => {}, showAddConversationModal: false, setShowAddConversationModal: (show: boolean) => {}, + configureProfileModal: { + profileOptionSelected: ProfileType.DM3_NAME, + onScreen: ProfileScreenType.NONE, + }, + setConfigureProfileModal: (modal: IConfigureProfileModal) => {}, + resetConfigureProfileModal: () => {}, resetModalStates: () => {}, + preferencesOptionSelected: preferencesItems[1], + setPreferencesOptionSelected: (item: { + icon: JSX.Element; + name: string; + component: JSX.Element; + isEnabled: boolean; + }) => {}, }); export const ModalContextProvider = ({ children }: { children?: any }) => { @@ -71,6 +108,11 @@ export const ModalContextProvider = ({ children }: { children?: any }) => { setShowAboutModal, showAddConversationModal, setShowAddConversationModal, + configureProfileModal, + setConfigureProfileModal, + resetConfigureProfileModal, + preferencesOptionSelected, + setPreferencesOptionSelected, resetModalStates, } = useModal(); @@ -96,6 +138,11 @@ export const ModalContextProvider = ({ children }: { children?: any }) => { showAddConversationModal, setShowAddConversationModal, resetModalStates, + configureProfileModal, + setConfigureProfileModal, + resetConfigureProfileModal, + preferencesOptionSelected, + setPreferencesOptionSelected, }} > {children} diff --git a/packages/messenger-widget/src/context/testHelper/getMockedAuthContext.ts b/packages/messenger-widget/src/context/testHelper/getMockedAuthContext.ts index b34523d42..b99fbcaa7 100644 --- a/packages/messenger-widget/src/context/testHelper/getMockedAuthContext.ts +++ b/packages/messenger-widget/src/context/testHelper/getMockedAuthContext.ts @@ -1,6 +1,6 @@ import { AuthContextType } from '../AuthContext'; -//Provide a mocked Auth context +//Provide a mocked Authenticate context //Override the default values with the provided values export const getMockedAuthContext = (override?: Partial) => { const defaultValues = { diff --git a/packages/messenger-widget/src/context/testHelper/getMockedConversationContext.ts b/packages/messenger-widget/src/context/testHelper/getMockedConversationContext.ts index 24412867d..02e3dc3f4 100644 --- a/packages/messenger-widget/src/context/testHelper/getMockedConversationContext.ts +++ b/packages/messenger-widget/src/context/testHelper/getMockedConversationContext.ts @@ -1,7 +1,7 @@ import { ContactPreview } from '../../interfaces/utils'; import { ConversationContextType } from '../ConversationContext'; -//Provide a mocked Auth context +//Provide a mocked Authenticate context //Override the default values with the provided values export const getMockedConversationContext = ( override?: Partial, diff --git a/packages/messenger-widget/src/hooks/messages/receipt/ReceiptDispatcher.ts b/packages/messenger-widget/src/hooks/messages/receipt/ReceiptDispatcher.ts index bded908c2..dcfca44ce 100644 --- a/packages/messenger-widget/src/hooks/messages/receipt/ReceiptDispatcher.ts +++ b/packages/messenger-widget/src/hooks/messages/receipt/ReceiptDispatcher.ts @@ -74,6 +74,7 @@ export class ReceiptDispatcher { //We only want to acknowledge messages from type NEW or REPLY. Every other message type can be neglected for now const shouldAcknowledgeMessage = messageModel.envelop.message.metadata.type === 'NEW' || + messageModel.envelop.message.metadata.type === 'EDIT' || messageModel.envelop.message.metadata.type === 'REPLY'; //Check if the selected contact is the sender of the message. // If that is the case we've to acknowledge the message and send a READ_OPENED acknowledgement to the sender diff --git a/packages/messenger-widget/src/hooks/modals/useModal.ts b/packages/messenger-widget/src/hooks/modals/useModal.ts index df4f9328f..0274e715e 100644 --- a/packages/messenger-widget/src/hooks/modals/useModal.ts +++ b/packages/messenger-widget/src/hooks/modals/useModal.ts @@ -1,13 +1,23 @@ import { useState } from 'react'; import { NewContact } from '../../interfaces/utils'; import { MessageProps } from '../../interfaces/props'; -import { MessageActionType } from '../../utils/enum-type-utils'; +import { + MessageActionType, + ProfileScreenType, + ProfileType, +} from '../../utils/enum-type-utils'; +import { preferencesItems } from '../../components/Preferences/bl'; export interface IOpenEmojiPopup { action: boolean; data: MessageProps | undefined; } +export interface IConfigureProfileModal { + profileOptionSelected: ProfileType; + onScreen: ProfileScreenType; +} + export const useModal = () => { const [loaderContent, setLoaderContent] = useState(''); @@ -40,6 +50,25 @@ export const useModal = () => { const [showAboutModal, setShowAboutModal] = useState(false); + const [configureProfileModal, setConfigureProfileModal] = + useState({ + profileOptionSelected: ProfileType.DM3_NAME, + onScreen: ProfileScreenType.NONE, + }); + + const [preferencesOptionSelected, setPreferencesOptionSelected] = useState<{ + icon: JSX.Element; + name: string; + component: JSX.Element; + isEnabled: boolean; + }>(preferencesItems[1]); + + const resetConfigureProfileModal = () => { + setConfigureProfileModal({ + profileOptionSelected: ProfileType.DM3_NAME, + onScreen: ProfileScreenType.NONE, + }); + }; const resetModalStates = () => { setLoaderContent(''); setContactToHide(undefined); @@ -54,6 +83,11 @@ export const useModal = () => { setShowPreferencesModal(false); setShowAboutModal(false); setShowAddConversationModal(false); + setConfigureProfileModal({ + profileOptionSelected: ProfileType.DM3_NAME, + onScreen: ProfileScreenType.NONE, + }); + setPreferencesOptionSelected(preferencesItems[1]); }; return { @@ -76,5 +110,10 @@ export const useModal = () => { showAddConversationModal, setShowAddConversationModal, resetModalStates, + configureProfileModal, + setConfigureProfileModal, + resetConfigureProfileModal, + preferencesOptionSelected, + setPreferencesOptionSelected, }; }; diff --git a/packages/messenger-widget/src/hooks/userProfile/useDm3UserProfile.tsx b/packages/messenger-widget/src/hooks/userProfile/useDm3UserProfile.tsx new file mode 100644 index 000000000..74de4f04f --- /dev/null +++ b/packages/messenger-widget/src/hooks/userProfile/useDm3UserProfile.tsx @@ -0,0 +1,755 @@ +import { useMemo, useContext, useEffect, useState } from 'react'; +import { AuthContext } from '../../context/AuthContext'; +import { isValidName } from 'ethers/lib/utils'; +import { updateProfile } from './../../adapters/offchainResolverApi'; +import { DM3ConfigurationContext } from '../../context/DM3ConfigurationContext'; +import { ethers } from 'ethers'; +import { + Account, + getProfileCreationMessage, + getUserProfile, + SignedUserProfile, + UserProfile, +} from '@dm3-org/dm3-lib-profile'; +import { useMainnetProvider } from '../mainnetprovider/useMainnetProvider'; +import { + fetchExistingDM3Name, + fetchExistingEnsName, + fetchExistingGnosisName, + fetchExistingOpName, +} from '../../utils/names'; +import { publishProfile } from '../../components/ConfigureProfile/dm3Names/optimismName/tx/publishProfile'; +import { registerOpName } from '../../components/ConfigureProfile/dm3Names/optimismName/tx/registerOpName'; +import { ModalContext } from '../../context/ModalContext'; +import { closeLoader, startLoader } from '../../components/Loader/Loader'; +import { submitGenomeNameTransaction } from '../../components/ConfigureProfile/chain/genome/bl'; +import { submitEnsNameTransaction } from '../../components/ConfigureProfile/chain/ens/bl'; +import { useChainId, useSwitchNetwork, useWalletClient } from 'wagmi'; +import { stringify } from '@dm3-org/dm3-lib-shared'; +import { + DM3_NAME_SERVICES, + fetchChainIdFromDM3ServiceName, + fetchChainIdFromServiceName, + NAME_SERVICES, +} from '../../components/ConfigureProfile/bl'; + +export interface INodeDetails { + dsNames: string[]; +} + +export interface INameProfile { + addrName: { + profile: null | UserProfile; + isActive: boolean; + } | null; + dm3Name: { + profile: null | UserProfile; + isActive: boolean; + } | null; + opName: { + profile: null | UserProfile; + isActive: boolean; + } | null; + gnosisName: { + profile: null | UserProfile; + isActive: boolean; + } | null; + ensName: { + profile: null | UserProfile; + isActive: boolean; + } | null; +} + +export const useDm3UserProfile = () => { + const connectedChainId = useChainId(); + + const { switchNetwork } = useSwitchNetwork({ + onSuccess: async () => { + await executeTransaction(); + }, + }); + + const { data: walletClient } = useWalletClient(); + + const mainnetProvider = useMainnetProvider(); + + const { account, ethAddress } = useContext(AuthContext); + + const { dm3Configuration } = useContext(DM3ConfigurationContext); + + const { setLoaderContent } = useContext(ModalContext); + + const [nodes, setNodes] = useState({ + dsNames: account?.profile?.deliveryServices as string[], + }); + + const [isModalOpenToAddNode, setIsModalOpenToAddNode] = + useState(false); + + const [error, setError] = useState(null); + + const [nodeName, setNodeName] = useState(''); + + const [profileName, setProfileName] = useState(null); + + const [namesWithProfile, setNamesWithProfile] = useState({ + addrName: null, + dm3Name: null, + opName: null, + gnosisName: null, + ensName: null, + }); + + // adds DS nodes in local storage of browser + const setDsNodesInLocalStorage = (data: INodeDetails) => { + // fetch data from local storage + const dsLocalData = localStorage.getItem('ds_nodes'); + let parsedData; + if (dsLocalData) { + // update or add the DS node data of connected account to local storage + parsedData = JSON.parse(dsLocalData); + parsedData[ethAddress as string] = data; + } + // if local storage doesn't have data then add fresh data + const freshData = { [ethAddress as string]: data }; + localStorage.setItem( + 'ds_nodes', + JSON.stringify(parsedData ?? freshData), + ); + }; + + // retrieves DS nodes from local storage of browser + const getDsNodesFromLocalStorage = (): INodeDetails => { + const data = localStorage.getItem('ds_nodes'); + // checks if local storage has data or not + const parsedData = data ? JSON.parse(data) : null; + // return the data only if connected account's data found + return parsedData + ? parsedData[ethAddress as string] + ? parsedData[ethAddress as string] + : null + : null; + }; + + // fetches profile for the names like ADDR, DM3, OP, GNO and ENS + const fetchUserProfileForAllNames = async (dsNodes: INodeDetails) => { + if (ethAddress && dsNodes) { + // fetch ADDR name, DM3 name, OP name, GNO name & ENS name + const addressName = ethAddress.concat( + dm3Configuration.addressEnsSubdomain, + ); + + const dm3Name = await fetchExistingDM3Name( + account as Account, + mainnetProvider, + dm3Configuration, + addressName, + ); + + const opName = await fetchExistingOpName( + account as Account, + mainnetProvider, + dm3Configuration, + addressName, + ); + + const ensName = await fetchExistingEnsName( + account as Account, + mainnetProvider, + dm3Configuration, + addressName, + ); + + const gnosisName = await fetchExistingGnosisName( + account as Account, + mainnetProvider, + dm3Configuration, + addressName, + ); + + // fetch user profiles for ADDR name, DM3 name, OP name, GNO name & ENS name + const addressNameProfile = await getUserProfile( + mainnetProvider, + addressName, + ).catch((e) => null); + + const dm3NameProfile = dm3Name + ? await getUserProfile(mainnetProvider, dm3Name).catch( + (e) => null, + ) + : null; + + const opNameProfile = opName + ? await getUserProfile(mainnetProvider, opName).catch( + (e) => null, + ) + : null; + + const ensNameProfile = ensName + ? await getUserProfile(mainnetProvider, ensName).catch( + (e) => null, + ) + : null; + + const gnosisNameProfile = gnosisName + ? await getUserProfile(mainnetProvider, gnosisName).catch( + (e) => null, + ) + : null; + + // update states to store the profle for each name + setNamesWithProfile({ + addrName: { + profile: addressNameProfile?.profile ?? null, + isActive: !!addressNameProfile?.profile, + }, + dm3Name: { + profile: dm3NameProfile?.profile ?? null, + isActive: !!dm3NameProfile?.profile, + }, + opName: { + profile: opNameProfile?.profile ?? null, + isActive: !!opNameProfile?.profile, + }, + ensName: { + profile: ensNameProfile?.profile ?? null, + isActive: !!ensNameProfile?.profile, + }, + gnosisName: { + profile: gnosisNameProfile?.profile ?? null, + isActive: !!gnosisNameProfile?.profile, + }, + }); + } + }; + + // Initializes user delivery services to show on Network screen + const initialize = async () => { + if (account?.profile) { + // fetch DS nodes from profile + const profileData = account.profile; + const dsNodesFromProfile: INodeDetails = { + dsNames: profileData.deliveryServices, + }; + + // fetch DS nodes from local storage + const dsNodeFromLocalStorage = getDsNodesFromLocalStorage(); + + // filter out duplicate nodes + const profileDsNodes: INodeDetails = dsNodeFromLocalStorage + ? dsNodeFromLocalStorage + : dsNodesFromProfile; + + if (!dsNodeFromLocalStorage) { + setDsNodesInLocalStorage(profileDsNodes); + } + + // fetches all profiles of a account + await fetchUserProfileForAllNames(profileDsNodes); + + // set nodes in the list to show on Network UI + setNodes(profileDsNodes); + return; + } + }; + + const updateProfileForAddressName = async () => { + // add those nodes to new profile + const newProfile = { ...account?.profile! }; + newProfile.deliveryServices = nodes.dsNames; + + // fetch subdomain for ADDR name + const subdomain = dm3Configuration.addressEnsSubdomain.substring(1); + + // fetch profile creation message to update the profile + const profileCreationMessage = getProfileCreationMessage( + stringify(newProfile), + ethAddress as string, + ); + + // sign the message to update the profile + const signature = await walletClient + ?.signMessage({ + message: profileCreationMessage, + }) + .catch((err) => { + console.log('signature error: ', err); + return; + }); + + // create new profile object + const signedUserProfile = { + profile: newProfile, + signature: signature, + } as SignedUserProfile; + + // update profile + const success = await updateProfile( + ethAddress as string, + dm3Configuration.resolverBackendUrl, + subdomain, + signedUserProfile, + ); + + // update the profile state of ADDR name & DM3 name as both have same profile + if (success) { + setNamesWithProfile((prev) => { + return { + ...prev, + addrName: { profile: newProfile, isActive: true }, + dm3Name: prev.dm3Name + ? { profile: newProfile, isActive: true } + : prev.dm3Name, + }; + }); + } + }; + + /** + * Changes network to update the profile on particular blockchain + */ + const updateProfileWithTransaction = async ( + existingName: string | null, + ) => { + // extract array of delivery service nodes names + const newNodes = nodes.dsNames.map((n) => n); + + // add those nodes to new profile + const newProfile: UserProfile = { ...account?.profile! }; + newProfile.deliveryServices = newNodes; + + // update account with new profile + const updatedAccount: Account = { ...account } as Account; + updatedAccount.profile = newProfile; + + // if no name found, don't update + if (!existingName) { + return; + } + + setProfileName(existingName); + + // check its OPTIMISM name + if (existingName.endsWith('.op.dm3.eth')) { + const chainId = fetchChainIdFromDM3ServiceName( + DM3_NAME_SERVICES.OPTIMISM, + dm3Configuration.chainId, + ); + + chainId !== connectedChainId + ? changeNetwork(chainId) + : executeTransaction(existingName); + } + + // check its GNOSIS name + if ( + existingName?.endsWith('.gno') || + existingName?.endsWith('.gnosis.eth') + ) { + const chainId = fetchChainIdFromServiceName( + NAME_SERVICES.GENOME, + dm3Configuration.chainId, + ); + + chainId !== connectedChainId + ? changeNetwork(chainId) + : executeTransaction(existingName); + } + + // check its ENS name + if (existingName?.endsWith('.eth')) { + const chainId = fetchChainIdFromServiceName( + NAME_SERVICES.ENS, + dm3Configuration.chainId, + ); + chainId !== connectedChainId + ? changeNetwork(chainId) + : executeTransaction(existingName); + } + }; + + /** + * Executes blockchain transaction to update profile on OP, GNO and ENS name + */ + const executeTransaction = async (name?: string) => { + const profileNameToUpdate = name ? name : profileName; + + // extract array of delivery service nodes names + const newNodes = nodes.dsNames.map((n) => n); + + // add those nodes to new profile + const newProfile: UserProfile = { ...account?.profile! }; + newProfile.deliveryServices = newNodes; + + const provider = new ethers.providers.Web3Provider( + window.ethereum as ethers.providers.ExternalProvider, + ); + + // if no name found, don't update + if (!profileNameToUpdate) { + return; + } + + // fetch profile creation message to update the profile + const profileCreationMessage = getProfileCreationMessage( + stringify(newProfile), + ethAddress as string, + ); + + // sign the message to update the profile + const signature = await walletClient + ?.signMessage({ + message: profileCreationMessage, + }) + .catch((err) => { + console.log('signature error: ', err); + return; + }); + + // update account with new profile + const updatedAccount: Account = { ...account } as Account; + updatedAccount.profile = newProfile; + updatedAccount.profileSignature = signature as string; + + // check if its OPTIMISM name + if (profileNameToUpdate.endsWith('.op.dm3.eth')) { + // start loader + setLoaderContent('Updating profile...'); + startLoader(); + + try { + await registerOpName(provider, () => {}, profileNameToUpdate); + + // do transaction + const response = await publishProfile( + provider, + updatedAccount, + profileNameToUpdate, + ); + + if (response) { + setNamesWithProfile((prev) => { + return { + ...prev, + opName: { + profile: newProfile, + isActive: true, + }, + }; + }); + } + + setProfileName(null); + closeLoader(); + } catch (error) { + console.log('Failed to update profile : ', error); + closeLoader(); + } + } + + // check if its GNOSIS name + if ( + profileNameToUpdate?.endsWith('.gno') || + profileNameToUpdate?.endsWith('.gnosis.eth') + ) { + // start loader + setLoaderContent('Updating profile...'); + startLoader(); + + try { + const response = await submitGenomeNameTransaction( + provider, + updatedAccount!, + setLoaderContent, + profileNameToUpdate, + ethAddress!, + () => {}, + () => {}, + ); + + if (response) { + setNamesWithProfile((prev) => { + return { + ...prev, + gnosisName: { + profile: newProfile, + isActive: true, + }, + }; + }); + } + + setProfileName(null); + closeLoader(); + } catch (error) { + console.log('Failed to update profile : ', error); + closeLoader(); + } + } + + // check if its ENS name + if (profileNameToUpdate?.endsWith('.eth')) { + // start loader + setLoaderContent('Updating profile...'); + startLoader(); + + try { + const response = await submitEnsNameTransaction( + provider!, + updatedAccount!, + ethAddress!, + setLoaderContent, + profileNameToUpdate, + () => {}, + () => {}, + ); + + if (response) { + setNamesWithProfile((prev) => { + return { + ...prev, + ensName: { + profile: newProfile, + isActive: true, + }, + }; + }); + } + + setProfileName(null); + closeLoader(); + } catch (error) { + console.log('Failed to update profile : ', error); + closeLoader(); + } + } + }; + + /** + * Adds node to the local storage + */ + const addNode = async () => { + setLoaderContent('Adding DS node...'); + startLoader(); + + let result: boolean = true; + + // validate node name + if (!isValidName(nodeName)) { + result = false; + } + + // node name must include . and end with .eth + if (!nodeName.includes('.') || !nodeName.endsWith('.eth')) { + result = false; + } + + // set error if any validation fails + if (!result) { + setError('Invalid ENS name'); + closeLoader(); + return; + } + + // set error if node already exists + if (nodes.dsNames.filter((data) => data === nodeName).length) { + setError('ENS name already exists'); + closeLoader(); + return; + } + + // validate DS node by looking into ENS record + if (!(await checkDsNameValidity(nodeName))) { + setError('Invalid DS node name'); + closeLoader(); + return; + } + + // update the DS nodes in local storage + const updatedNodes = { + dsNames: [...nodes.dsNames, nodeName], + }; + + // update nodes in local storage + setDsNodesInLocalStorage(updatedNodes); + + // update node list + setNodes(updatedNodes); + + // clear the input field + setNodeName(''); + + // clear all errors + setError(null); + + // close the input field window + setIsModalOpenToAddNode(false); + + // close the loader + closeLoader(); + }; + + /** + * Deletes node from the local storage + */ + const deleteNode = async (id: number) => { + // filter out node to delete + const updatedNodes = nodes.dsNames.filter((d, i) => i !== id); + + // updated nodes + const newNodes = { dsNames: updatedNodes }; + + // clear the input field + setNodeName(''); + + // clear all errors + setError(null); + + // close the input field window + setIsModalOpenToAddNode(false); + + // update nodes in local storage + setDsNodesInLocalStorage(newNodes); + + // update node list + setNodes(newNodes); + }; + + /** + * Checks the address name profile is updated or not + */ + const isProfileUpdatedForAddrName = useMemo(() => { + const { addrName } = namesWithProfile; + if ( + addrName && + addrName.isActive && + addrName.profile && + JSON.stringify(addrName.profile.deliveryServices) !== + JSON.stringify(nodes.dsNames) + ) { + return false; + } + return true; + }, [nodes, namesWithProfile]); + + /** + * Checks the DM3/OP name profile is updated or not + */ + const isProfileUpdatedForDm3Name = useMemo(() => { + const { dm3Name, opName } = namesWithProfile; + if ( + dm3Name && + dm3Name.isActive && + dm3Name.profile && + JSON.stringify(dm3Name.profile.deliveryServices) !== + JSON.stringify(nodes.dsNames) + ) { + return false; + } + if ( + opName && + opName.isActive && + opName.profile && + JSON.stringify(opName.profile.deliveryServices) !== + JSON.stringify(nodes.dsNames) + ) { + return false; + } + return true; + }, [nodes, namesWithProfile]); + + /** + * Checks the ENS/GNO name profile is updated or not + */ + const isProfileUpdatedForEnsName = useMemo(() => { + const { ensName, gnosisName } = namesWithProfile; + if ( + ensName && + ensName.isActive && + ensName.profile && + JSON.stringify(ensName.profile.deliveryServices) !== + JSON.stringify(nodes.dsNames) + ) { + return false; + } + if ( + gnosisName && + gnosisName.isActive && + gnosisName.profile && + JSON.stringify(gnosisName.profile.deliveryServices) !== + JSON.stringify(nodes.dsNames) + ) { + return false; + } + return true; + }, [nodes, namesWithProfile]); + + /** + * Check profile is updated for ADDR name or not. + * If its not updated return false. + * Check profile is updated for DM3 name or not. + * If its not updated return false. + * If DM3 name profile is updated, then check ENS name profile and return true/false. + */ + const isProfileUpdated = useMemo(() => { + return !isProfileUpdatedForAddrName + ? false + : !isProfileUpdatedForDm3Name + ? false + : isProfileUpdatedForEnsName; + }, [nodes, namesWithProfile]); + + const handleNodeNameChange = (e: React.ChangeEvent) => { + setError(null); + setNodeName(e.target.value); + }; + + const changeNetwork = (chainId: number) => { + if (chainId !== connectedChainId && switchNetwork) { + switchNetwork(chainId); + } + }; + + /** + * Validates DS name is actually a delivery service or not + */ + const checkDsNameValidity = async (dsName: string): Promise => { + try { + const resolver = await mainnetProvider.getResolver(dsName); + if (resolver) { + const dsProfile = await resolver.getText( + 'network.dm3.deliveryService', + ); + return !!dsProfile; + } + return false; + } catch (error) { + console.log('Invalid DN node : ', error); + return false; + } + }; + + useEffect(() => { + initialize(); + }, [account?.ensName]); + + return { + initialize, + addNode, + updateProfileForAddressName, + updateProfileWithTransaction, + deleteNode, + nodes, + isModalOpenToAddNode, + setIsModalOpenToAddNode, + error, + setError, + nodeName, + setNodeName, + handleNodeNameChange, + isProfileUpdated, + isProfileUpdatedForAddrName, + isProfileUpdatedForDm3Name, + isProfileUpdatedForEnsName, + }; +}; diff --git a/packages/messenger-widget/src/utils/enum-type-utils.ts b/packages/messenger-widget/src/utils/enum-type-utils.ts index 90b10959f..933846cf8 100644 --- a/packages/messenger-widget/src/utils/enum-type-utils.ts +++ b/packages/messenger-widget/src/utils/enum-type-utils.ts @@ -33,3 +33,15 @@ export enum SiweValidityStatus { VALIDATED = 'VALIDATED', ERROR = 'ERROR', } + +export enum ProfileType { + DM3_NAME = 'dm3Name', + OWN_NAME = 'ownName', +} + +export enum ProfileScreenType { + SELECT_TYPE, + SELECT_STORAGE, + CLAIM_NAME, + NONE, +} diff --git a/packages/messenger-widget/src/utils/names.ts b/packages/messenger-widget/src/utils/names.ts new file mode 100644 index 000000000..3fc72fe86 --- /dev/null +++ b/packages/messenger-widget/src/utils/names.ts @@ -0,0 +1,109 @@ +import { Account } from '@dm3-org/dm3-lib-profile'; +import { Dm3Name } from '../hooks/topLevelAlias/nameService/Dm3Name'; +import { ethers } from 'ethers'; +import { DM3Configuration } from '../interfaces/config'; +import { OptimismNames } from '../hooks/topLevelAlias/nameService/OptimismNames'; +import { Genome } from '../hooks/topLevelAlias/nameService/Genome'; +import { EthereumNameService } from '../hooks/topLevelAlias/nameService/EthereumNameService'; + +export const fetchExistingDM3Name = async ( + account: Account, + mainnetProvider: ethers.providers.JsonRpcProvider, + dm3Configuration: DM3Configuration, + addressName: string, +) => { + try { + if (account) { + const dm3NameService = new Dm3Name( + mainnetProvider, + dm3Configuration.addressEnsSubdomain, + dm3Configuration.userEnsSubdomain, + dm3Configuration.resolverBackendUrl, + ); + const dm3Name = await dm3NameService.resolveAliasToTLD(addressName); + // Not a DM3 name -> 0xa966.beta-addr.dm3.eth + // Its DM3 name -> bob.beta-user.dm3.eth + // Checks user sub domain for setting DM3 name + return dm3Name.endsWith(dm3Configuration.userEnsSubdomain) + ? dm3Name + : null; + } + return null; + } catch (error) { + console.log('dm3 name : ', error); + return null; + } +}; + +export const fetchExistingOpName = async ( + account: Account, + mainnetProvider: ethers.providers.JsonRpcProvider, + dm3Configuration: DM3Configuration, + addressName: string, +) => { + try { + if (account) { + const opNameService = new OptimismNames( + mainnetProvider, + dm3Configuration.addressEnsSubdomain, + ); + const opName = await opNameService.resolveAliasToTLD(addressName); + return opName.endsWith('.op.dm3.eth') ? opName : null; + } + return null; + } catch (error) { + console.log('OP name : ', error); + return null; + } +}; + +export const fetchExistingGnosisName = async ( + account: Account, + mainnetProvider: ethers.providers.JsonRpcProvider, + dm3Configuration: DM3Configuration, + addressName: string, +) => { + try { + if (account) { + const gnosisNameService = new Genome( + mainnetProvider, + dm3Configuration.addressEnsSubdomain, + ); + const gnosisName = await gnosisNameService.resolveAliasToTLD( + addressName, + ); + return gnosisName.endsWith('.gnosis.eth') || + gnosisName.endsWith('.gno') + ? gnosisName + : null; + } + return null; + } catch (error) { + console.log('Gnosis name : ', error); + return null; + } +}; + +export const fetchExistingEnsName = async ( + account: Account, + mainnetProvider: ethers.providers.JsonRpcProvider, + dm3Configuration: DM3Configuration, + addressName: string, +) => { + try { + if (account) { + const ensNameService = new EthereumNameService( + mainnetProvider, + dm3Configuration.addressEnsSubdomain, + dm3Configuration.userEnsSubdomain, + ); + const ensName = await ensNameService.resolveAliasToTLD(addressName); + // .dm3.eth means it is not ENS name + return ensName.endsWith('.dm3.eth') ? null : ensName; + } + return null; + } catch (error) { + console.log('ENS name : ', error); + return null; + } +}; diff --git a/packages/messenger-widget/src/utils/style-utils.ts b/packages/messenger-widget/src/utils/style-utils.ts index c25488b90..ede1c74af 100644 --- a/packages/messenger-widget/src/utils/style-utils.ts +++ b/packages/messenger-widget/src/utils/style-utils.ts @@ -52,6 +52,7 @@ export const setTheme = (theme: string | undefined | null) => { --alternate-contact-background-color: ${themeDetails.alternateContactBackgroundColor}; --menu-background-color: ${themeDetails.menuBackgroundColor}; --preferences-highlighted-color: ${themeDetails.preferencesHighlightedColor}; + --configure-profile-modal-background-color: ${themeDetails.configureProfileModalBackgroundColor}; }`, }), ); diff --git a/packages/messenger-widget/src/utils/theme-utils.ts b/packages/messenger-widget/src/utils/theme-utils.ts index 93f3c1c0f..f29497399 100644 --- a/packages/messenger-widget/src/utils/theme-utils.ts +++ b/packages/messenger-widget/src/utils/theme-utils.ts @@ -45,6 +45,7 @@ export const defaultTheme: any = { alternateContactBackgroundColor: '#5443931A', menuBackgroundColor: '#2A2B38', preferencesHighlightedColor: '#8b7ff4', + configureProfileModalBackgroundColor: '#D9D9D9', }; export const getCustomizedTheme = (themeCss: any) => { @@ -165,5 +166,8 @@ export const getCustomizedTheme = (themeCss: any) => { preferencesHighlightedColor: themeCss.preferencesHighlightedColor ?? defaultTheme.preferencesHighlightedColor, + configureProfileModalBackgroundColor: + themeCss.configureProfileModalBackgroundColor ?? + defaultTheme.configureProfileModalBackgroundColor, }; }; diff --git a/packages/messenger-widget/src/version.ts b/packages/messenger-widget/src/version.ts index 906d0451a..824005cf5 100644 --- a/packages/messenger-widget/src/version.ts +++ b/packages/messenger-widget/src/version.ts @@ -1 +1 @@ -export const version = '1.6.0'; +export const version = '1.6.1'; diff --git a/packages/offchain-resolver/package.json b/packages/offchain-resolver/package.json index 1b2fac0a9..d2426de56 100644 --- a/packages/offchain-resolver/package.json +++ b/packages/offchain-resolver/package.json @@ -1,7 +1,7 @@ { "name": "@dm3-org/dm3-offchain-resolver", "license": "BSD-2-Clause", - "version": "1.6.1", + "version": "1.7.0", "main": "dist/index.js", "types": "dist/index.d.ts", "dependencies": { @@ -23,7 +23,6 @@ "ts-node": "^10.8.1", "typescript": "^4.4.2", "uuid": "^9.0.0", - "winston": "^3.8.1", "yaml": "^2.1.3" }, "scripts": { diff --git a/packages/offchain-resolver/src/http/profile.test.ts b/packages/offchain-resolver/src/http/profile.test.ts index 4e2293002..009b965d1 100644 --- a/packages/offchain-resolver/src/http/profile.test.ts +++ b/packages/offchain-resolver/src/http/profile.test.ts @@ -8,7 +8,7 @@ import { ethers } from 'ethers'; import express from 'express'; import request from 'supertest'; -import winston from 'winston'; + import { getDatabase, getDbClient } from '../persistence/getDatabase'; import { IDatabase } from '../persistence/IDatabase'; import { profile } from './profile'; @@ -260,7 +260,7 @@ describe('Profile', () => { expect(body.error).to.equal('invalid profile'); }); - it('Rejects if subdomain has already a profile', async () => { + it('Updates profile if subdomain has already a profile', async () => { app.use(profile(provider)); const offChainProfile1 = await getSignedUserProfile(); @@ -291,8 +291,7 @@ describe('Profile', () => { subdomain: 'beta-addr.dm3.eth', }); - expect(res2.status).to.equal(400); - expect(res2.body.error).to.eql('subdomain already claimed'); + expect(res2.status).to.equal(200); }); it('Rejects if subdomain is not supported', async () => { app.use(profile(provider)); diff --git a/packages/offchain-resolver/src/http/profile.ts b/packages/offchain-resolver/src/http/profile.ts index 22026a31b..301ab596c 100644 --- a/packages/offchain-resolver/src/http/profile.ts +++ b/packages/offchain-resolver/src/http/profile.ts @@ -267,16 +267,6 @@ export function profile(web3Provider: ethers.providers.BaseProvider) { return res.status(400).send({ error: 'invalid profile' }); } - const hasAddressProfile = - !!(await req.app.locals.db.getProfileContainer(address)); - - //One address can only claim one subdomain - if (hasAddressProfile) { - return res.status(400).send({ - error: 'address has already claimed a subdomain', - }); - } - const name = `${address}.${subdomain}`; //ask the subdomain manager if the names subdomain is supported @@ -286,15 +276,6 @@ export function profile(web3Provider: ethers.providers.BaseProvider) { }); } - const profileExists = - !!(await req.app.locals.db.getProfileContainer(name)); - - if (profileExists) { - return res - .status(400) - .send({ error: 'subdomain already claimed' }); - } - await req.app.locals.db.setUserProfile( name, signedUserProfile, diff --git a/packages/offchain-resolver/src/persistence/profile/createOrUpdateUserProfile.ts b/packages/offchain-resolver/src/persistence/profile/createOrUpdateUserProfile.ts new file mode 100644 index 000000000..5ff615596 --- /dev/null +++ b/packages/offchain-resolver/src/persistence/profile/createOrUpdateUserProfile.ts @@ -0,0 +1,69 @@ +import { ethers } from 'ethers'; +import { v4 as uuidv4 } from 'uuid'; +import { PrismaClient } from '@prisma/client'; +import { + formatAddress, + normalizeEnsName, + SignedUserProfile, +} from '@dm3-org/dm3-lib-profile'; + +export const createOrUpdateUserProfile = async ( + db: PrismaClient, + name: string, + profile: SignedUserProfile, + address: string, +) => { + // hash the name + const hashedName = ethers.utils.namehash(name); + + //Check if profile already exists + const profileContainer = await db.profileContainer.findUnique({ + where: { + nameHash: hashedName, + }, + }); + + if (profileContainer) { + console.debug({ + message: 'pre updateUserProfile', + hashedName, + profile: JSON.stringify(profile), + address: formatAddress(address), + }); + + //If a profile already exist. Update the profile property. + //At the moemnt this is the only updatable field + const updatedProfile = await db.profileContainer.update({ + where: { + nameHash: hashedName, + }, + data: { + profile: JSON.stringify(profile), + }, + }); + + return updatedProfile; + } + + // If profile does not exist, create it + const id = uuidv4(); + + console.debug({ + message: 'pre setUserProfile', + id, + hashedName, + profile: JSON.stringify(profile), + address: formatAddress(address), + ensName: normalizeEnsName(name), + }); + + return await db.profileContainer.create({ + data: { + id, + nameHash: hashedName, + profile: JSON.stringify(profile), + address: formatAddress(address), + ensName: normalizeEnsName(name), + }, + }); +}; diff --git a/packages/offchain-resolver/src/persistence/profile/getProfileContainer.test.ts b/packages/offchain-resolver/src/persistence/profile/getProfileContainer.test.ts index 88921042d..069aa0763 100644 --- a/packages/offchain-resolver/src/persistence/profile/getProfileContainer.test.ts +++ b/packages/offchain-resolver/src/persistence/profile/getProfileContainer.test.ts @@ -1,6 +1,6 @@ import { SignedUserProfile } from '@dm3-org/dm3-lib-profile'; import { ethers } from 'ethers'; -import winston from 'winston'; + import { getProfileContainer, setUserProfile } from '.'; import { IDatabase } from '../IDatabase'; import { getDatabase, getDbClient } from '../getDatabase'; diff --git a/packages/offchain-resolver/src/persistence/profile/getProfileContainerByAddress.test.ts b/packages/offchain-resolver/src/persistence/profile/getProfileContainerByAddress.test.ts index 789a4caed..199117bd7 100644 --- a/packages/offchain-resolver/src/persistence/profile/getProfileContainerByAddress.test.ts +++ b/packages/offchain-resolver/src/persistence/profile/getProfileContainerByAddress.test.ts @@ -1,4 +1,3 @@ -import winston from 'winston'; import { getDatabase, getDbClient } from '../getDatabase'; import { IDatabase } from '../IDatabase'; const SENDER_ADDRESS = '0x25A643B6e52864d0eD816F1E43c0CF49C83B8292'; diff --git a/packages/offchain-resolver/src/persistence/profile/getUserProfilesByAddress.test.ts b/packages/offchain-resolver/src/persistence/profile/getUserProfilesByAddress.test.ts index 789a4caed..199117bd7 100644 --- a/packages/offchain-resolver/src/persistence/profile/getUserProfilesByAddress.test.ts +++ b/packages/offchain-resolver/src/persistence/profile/getUserProfilesByAddress.test.ts @@ -1,4 +1,3 @@ -import winston from 'winston'; import { getDatabase, getDbClient } from '../getDatabase'; import { IDatabase } from '../IDatabase'; const SENDER_ADDRESS = '0x25A643B6e52864d0eD816F1E43c0CF49C83B8292'; diff --git a/packages/offchain-resolver/src/persistence/profile/index.ts b/packages/offchain-resolver/src/persistence/profile/index.ts index 13f1eff0a..4c79f39f7 100644 --- a/packages/offchain-resolver/src/persistence/profile/index.ts +++ b/packages/offchain-resolver/src/persistence/profile/index.ts @@ -1,5 +1,3 @@ -import winston from 'winston'; - export { getProfileContainer } from './getProfileContainer'; export { getProfileContainerByAddress } from './getProfileContainerByAddress'; export { removeUserProfile } from './removeUserProfile'; diff --git a/packages/offchain-resolver/src/persistence/profile/removeUserProfile.test.ts b/packages/offchain-resolver/src/persistence/profile/removeUserProfile.test.ts index 7484b14cd..07d4e52da 100644 --- a/packages/offchain-resolver/src/persistence/profile/removeUserProfile.test.ts +++ b/packages/offchain-resolver/src/persistence/profile/removeUserProfile.test.ts @@ -3,7 +3,7 @@ import { IDatabase } from '../IDatabase'; import { setUserProfile } from './setUserProfile'; import { removeUserProfile } from './removeUserProfile'; import { ethers } from 'ethers'; -import winston from 'winston'; + import { SignedUserProfile } from '@dm3-org/dm3-lib-profile'; import { PrismaClient } from '@prisma/client'; import { clearDb } from '../clearDb'; diff --git a/packages/offchain-resolver/src/persistence/profile/setUserProfile.test.ts b/packages/offchain-resolver/src/persistence/profile/setUserProfile.test.ts index 10bae39f5..bc7d20b28 100644 --- a/packages/offchain-resolver/src/persistence/profile/setUserProfile.test.ts +++ b/packages/offchain-resolver/src/persistence/profile/setUserProfile.test.ts @@ -2,12 +2,12 @@ import { getDatabase, getDbClient } from '../getDatabase'; import { IDatabase } from '../IDatabase'; import { setUserProfile } from './setUserProfile'; import { ethers } from 'ethers'; -import winston from 'winston'; import { SignedUserProfile } from '@dm3-org/dm3-lib-profile'; import { PrismaClient } from '@prisma/client'; import { clearDb } from '../clearDb'; import chai, { expect } from 'chai'; import chaiAsPromised from 'chai-as-promised'; +import { getProfileContainer } from './getProfileContainer'; chai.use(chaiAsPromised); @@ -61,7 +61,7 @@ describe('setUserProfile', () => { expect(writeResult).to.be.true; }); - it('Rejects if a name already has profile attached', async () => { + it('Updates the profile if it already exists', async () => { const { address } = ethers.Wallet.createRandom(); const profile: SignedUserProfile = { @@ -84,12 +84,24 @@ describe('setUserProfile', () => { ); expect(firstWrite).to.be.true; + // add new DS to the profile + const newProfile: SignedUserProfile = { ...profile }; + newProfile.profile.deliveryServices = ['ds.eth']; + //This should reject bc the subdomain already has a profile const secondWrite = await setUserProfile(prismaClient)( 'foo.eth', - profile, + newProfile, address, ); - expect(secondWrite).to.be.false; + + const retrievedProfile = await getProfileContainer(prismaClient)( + 'foo.eth', + ); + + expect(secondWrite).to.be.true; + expect(JSON.stringify(retrievedProfile?.profile)).to.equal( + JSON.stringify(newProfile), + ); }); }); diff --git a/packages/offchain-resolver/src/persistence/profile/setUserProfile.ts b/packages/offchain-resolver/src/persistence/profile/setUserProfile.ts index 1c1ce3da7..c50d7dda2 100644 --- a/packages/offchain-resolver/src/persistence/profile/setUserProfile.ts +++ b/packages/offchain-resolver/src/persistence/profile/setUserProfile.ts @@ -1,13 +1,7 @@ -import { ethers } from 'ethers'; -import { - SignedUserProfile, - normalizeEnsName, - schema, - formatAddress, -} from '@dm3-org/dm3-lib-profile'; +import { SignedUserProfile, schema } from '@dm3-org/dm3-lib-profile'; import { validateSchema } from '@dm3-org/dm3-lib-shared'; import { PrismaClient } from '@prisma/client'; -import { v4 as uuidv4 } from 'uuid'; +import { createOrUpdateUserProfile } from './createOrUpdateUserProfile'; /** * @@ -32,34 +26,11 @@ export function setUserProfile(db: PrismaClient) { throw Error('Invalid user profile'); } - const nameHash = ethers.utils.namehash(name); - try { - const id = uuidv4(); - console.debug({ - message: 'pre setUserProfile', - id, - nameHash, - profile: JSON.stringify(profile), - address: formatAddress(address), - ensName: normalizeEnsName(name), - }); - await db.profileContainer.create({ - data: { - id, - nameHash, - profile: JSON.stringify(profile), - address: formatAddress(address), - ensName: normalizeEnsName(name), - }, - }); - + await createOrUpdateUserProfile(db, name, profile, address); return true; } catch (e) { - console.warn({ - message: `setUserProfile error`, - error: JSON.stringify(e), - }); + console.log('setUserProfile error ', e); return false; } }; diff --git a/packages/toplevel-alias/package-lock.json b/packages/toplevel-alias/package-lock.json index b077ec56b..d1b502c71 100644 --- a/packages/toplevel-alias/package-lock.json +++ b/packages/toplevel-alias/package-lock.json @@ -103,7 +103,7 @@ } }, "node_modules/@ethereumjs/util/node_modules/@noble/hashes": { - "version": "1.6.1", + "version": "1.7.0", "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.1.tgz", "integrity": "sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA==", "dev": true, @@ -1603,7 +1603,7 @@ } }, "node_modules/@scure/bip32": { - "version": "1.6.1", + "version": "1.7.0", "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.3.1.tgz", "integrity": "sha512-osvveYtyzdEVbt3OfwwXFr4P2iVBL5u1Q3q4ONBfDY/UpOuXmOlbgwc1xECEboY8wIays8Yt6onaWMUdUbfl0A==", "dev": true, @@ -1631,7 +1631,7 @@ } }, "node_modules/@scure/bip32/node_modules/@noble/hashes": { - "version": "1.6.1", + "version": "1.7.0", "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.1.tgz", "integrity": "sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA==", "dev": true, @@ -1891,7 +1891,7 @@ } }, "node_modules/@types/concat-stream": { - "version": "1.6.1", + "version": "1.7.0", "resolved": "https://registry.npmjs.org/@types/concat-stream/-/concat-stream-1.6.1.tgz", "integrity": "sha512-eHE4cQPoj6ngxBZMvVf6Hw7Mh4jMW4U9lpGmS5GBPB9RYxlFg+CHaVN7ErNY4W9XfLIEn20b4VDYaIrbq0q4uA==", "dev": true, @@ -2400,7 +2400,7 @@ } }, "node_modules/browser-stdout": { - "version": "1.6.1", + "version": "1.7.0", "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", "dev": true @@ -2750,7 +2750,7 @@ "dev": true }, "node_modules/colors": { - "version": "1.6.1", + "version": "1.7.0", "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz", "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==", "dev": true, @@ -4347,7 +4347,7 @@ "peer": true }, "node_modules/interpret": { - "version": "1.6.1", + "version": "1.7.0", "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.4.0.tgz", "integrity": "sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==", "dev": true, @@ -4532,7 +4532,7 @@ } }, "node_modules/jsonschema": { - "version": "1.6.1", + "version": "1.7.0", "resolved": "https://registry.npmjs.org/jsonschema/-/jsonschema-1.4.1.tgz", "integrity": "sha512-S6cATIPVv1z0IlxdN+zUk5EPjkGCdnhN4wVSBlvoUO1tOLJootbo9CquNJmbIh4yikWHiUedhRYrNPn1arpEmQ==", "dev": true, @@ -4567,7 +4567,7 @@ } }, "node_modules/klaw": { - "version": "1.6.1", + "version": "1.7.0", "resolved": "https://registry.npmjs.org/klaw/-/klaw-1.3.1.tgz", "integrity": "sha512-TED5xi9gGQjGpNnvRWknrwAB1eL5GciPfVFOt3Vk1OJCVDQbzuSfrF3hkUQKlsgKrG1F+0t5W0m+Fje1jIt8rw==", "dev": true, @@ -4842,7 +4842,7 @@ } }, "node_modules/merge2": { - "version": "1.6.1", + "version": "1.7.0", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", "dev": true, @@ -5277,7 +5277,7 @@ "dev": true }, "node_modules/once": { - "version": "1.6.1", + "version": "1.7.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", "dev": true, @@ -7108,7 +7108,7 @@ } }, "node_modules/web3-utils/node_modules/@noble/hashes": { - "version": "1.6.1", + "version": "1.7.0", "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.1.tgz", "integrity": "sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA==", "dev": true, @@ -7134,7 +7134,7 @@ } }, "node_modules/which": { - "version": "1.6.1", + "version": "1.7.0", "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", "dev": true, diff --git a/yarn.lock b/yarn.lock index dd42d23fa..415f08321 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2024,7 +2024,6 @@ __metadata: "@dm3-org/dm3-lib-profile": "workspace:^" "@dm3-org/dm3-lib-server-side": "workspace:^" "@dm3-org/dm3-lib-shared": "workspace:^" - "@prisma/client": 4.16.2 "@types/express": ^4.17.13 "@types/node": ^20.3.1 "@types/redis": ^4.0.11 @@ -2040,6 +2039,7 @@ __metadata: express: ^4.18.1 jest: ^29.2.2 jest-mock-extended: 2.0.4 + nodemon: ^3.1.4 prettier: ^2.6.2 redis: ^4.1.0 socket.io: ^4.5.1 @@ -2048,7 +2048,6 @@ __metadata: ts-node: ^10.9.1 typescript: ^4.4.2 web-push: ^3.6.7 - winston: ^3.8.1 yaml: ^2.1.3 languageName: unknown linkType: soft @@ -2068,11 +2067,10 @@ __metadata: "@dm3-org/dm3-lib-shared": "workspace:^" "@dm3-org/dm3-lib-storage": "workspace:^" "@dm3-org/dm3-lib-test-helper": "workspace:^" - "@prisma/client": 4.16.2 + "@prisma/client": ^5.19.1 "@types/cors": ^2.8.17 "@types/express": ^4.17.13 "@types/node": ^20.3.1 - "@types/redis": ^4.0.11 "@types/supertest": ^2.0.12 axios: ^0.27.2 babel-cli: ^6.26.0 @@ -2086,15 +2084,13 @@ __metadata: jest: ^29.2.2 jest-mock-extended: 2.0.4 prettier: ^2.6.2 - prisma: ^5.10.1 - redis: ^4.1.0 + prisma: ^5.19.1 superagent: ^8.0.3 supertest: ^6.3.1 ts-json-schema-generator: ^0.98.0 ts-node: ^10.9.1 typescript: ^4.4.2 web-push: ^3.6.7 - winston: ^3.8.1 yaml: ^2.1.3 languageName: unknown linkType: soft @@ -2279,6 +2275,7 @@ __metadata: ajv-formats: ^3.0.1 ethers: 5.7.2 jest: ^29.7.0 + safe-stable-stringify: 2.4.3 ts-jest: ^29.1.4 typescript: ^4.4.2 languageName: unknown @@ -2409,6 +2406,7 @@ __metadata: "@dm3-org/dm3-lib-test-helper": "workspace:^" "@emoji-mart/data": ^1.1.2 "@emoji-mart/react": ^1.1.1 + "@ensdomains/ensjs": ^2.1.0 "@popperjs/core": ^2.11.8 "@rainbow-me/rainbowkit": ^1.0.6 "@testing-library/dom": ^9.3.1 @@ -2490,7 +2488,6 @@ __metadata: ts-node: ^10.8.1 typescript: ^4.4.2 uuid: ^9.0.0 - winston: ^3.8.1 yaml: ^2.1.3 languageName: unknown linkType: soft @@ -2645,7 +2642,7 @@ __metadata: languageName: node linkType: hard -"@ensdomains/ensjs@npm:^2.0.1": +"@ensdomains/ensjs@npm:^2.0.1, @ensdomains/ensjs@npm:^2.1.0": version: 2.1.0 resolution: "@ensdomains/ensjs@npm:2.1.0" dependencies: @@ -5278,7 +5275,7 @@ __metadata: languageName: node linkType: hard -"@prisma/client@npm:4.16.2, @prisma/client@npm:^4.15.0": +"@prisma/client@npm:^4.15.0": version: 4.16.2 resolution: "@prisma/client@npm:4.16.2" dependencies: @@ -5292,10 +5289,22 @@ __metadata: languageName: node linkType: hard -"@prisma/debug@npm:5.10.1": - version: 5.10.1 - resolution: "@prisma/debug@npm:5.10.1" - checksum: 137329cfff47af9fb7b124f72b1146fde2e9e2dc30f89973dbb596205847aa1b93674ada8620cdbc654ab2ab807a2b215d707220313edcb8e68a2f1ecc60f239 +"@prisma/client@npm:^5.19.1": + version: 5.19.1 + resolution: "@prisma/client@npm:5.19.1" + peerDependencies: + prisma: "*" + peerDependenciesMeta: + prisma: + optional: true + checksum: 23468c154d3e42fbaa88b904b72a893cf3211a87992988af48d2d93fb353b05a8c843f18b2fa0b50f7e78cf84461d7c6ef4e6f524398c30a62c81f5762d2f1c0 + languageName: node + linkType: hard + +"@prisma/debug@npm:5.19.1": + version: 5.19.1 + resolution: "@prisma/debug@npm:5.19.1" + checksum: 73fb1de49e09bf641501c7c5819d033b29df282af4f93c96a1347ee56888e4b946d27c6b72e285fe2d31603561e52d7d266d5fdb6b3d76ee2007df9178e28bef languageName: node linkType: hard @@ -5306,10 +5315,10 @@ __metadata: languageName: node linkType: hard -"@prisma/engines-version@npm:5.10.0-34.5a9203d0590c951969e85a7d07215503f4672eb9": - version: 5.10.0-34.5a9203d0590c951969e85a7d07215503f4672eb9 - resolution: "@prisma/engines-version@npm:5.10.0-34.5a9203d0590c951969e85a7d07215503f4672eb9" - checksum: 88d7c2a310123427b671b4ac4ca6dd35c85811a537406630ef5e03b07dd896821b5c652d4b70a8a4a68deb94851e7bde755e70b10823e265ab9a9ea1a4cc3801 +"@prisma/engines-version@npm:5.19.1-2.69d742ee20b815d88e17e54db4a2a7a3b30324e3": + version: 5.19.1-2.69d742ee20b815d88e17e54db4a2a7a3b30324e3 + resolution: "@prisma/engines-version@npm:5.19.1-2.69d742ee20b815d88e17e54db4a2a7a3b30324e3" + checksum: 2b8ca8d4a1cc2913cf5b45213ba3efc04fa2a993a4cf3f9f136b5df5f716651cfe2784b698e115e3c40bc15eee3350be6d071a88d01e7939e0dac116b95d29a8 languageName: node linkType: hard @@ -5320,35 +5329,35 @@ __metadata: languageName: node linkType: hard -"@prisma/engines@npm:5.10.1": - version: 5.10.1 - resolution: "@prisma/engines@npm:5.10.1" +"@prisma/engines@npm:5.19.1": + version: 5.19.1 + resolution: "@prisma/engines@npm:5.19.1" dependencies: - "@prisma/debug": 5.10.1 - "@prisma/engines-version": 5.10.0-34.5a9203d0590c951969e85a7d07215503f4672eb9 - "@prisma/fetch-engine": 5.10.1 - "@prisma/get-platform": 5.10.1 - checksum: dc3c9502e468ade8b3de34ceebb9a87a2e357331d2327be86f131322eb97a0389b9745b21f74d8b95ae36e0efee25af0625fe9754fdbd063286223310a7c1ddf + "@prisma/debug": 5.19.1 + "@prisma/engines-version": 5.19.1-2.69d742ee20b815d88e17e54db4a2a7a3b30324e3 + "@prisma/fetch-engine": 5.19.1 + "@prisma/get-platform": 5.19.1 + checksum: 05384c9cbbf4dc84357e8b8b293ad0c7d077c77491a7f3c74f673a9e8a13a7e98a8502127e19df62c38639a735a050298d6de893d847acd47398022d274f6ba3 languageName: node linkType: hard -"@prisma/fetch-engine@npm:5.10.1": - version: 5.10.1 - resolution: "@prisma/fetch-engine@npm:5.10.1" +"@prisma/fetch-engine@npm:5.19.1": + version: 5.19.1 + resolution: "@prisma/fetch-engine@npm:5.19.1" dependencies: - "@prisma/debug": 5.10.1 - "@prisma/engines-version": 5.10.0-34.5a9203d0590c951969e85a7d07215503f4672eb9 - "@prisma/get-platform": 5.10.1 - checksum: e102c7be4b51c310b9c6bde051d8f5f46ebe1c45b1af5092734455d096a20900ba0a1440e58164d8621d9efc29d404d60a30203693b0f7c927a7e385d8f7eb37 + "@prisma/debug": 5.19.1 + "@prisma/engines-version": 5.19.1-2.69d742ee20b815d88e17e54db4a2a7a3b30324e3 + "@prisma/get-platform": 5.19.1 + checksum: 8669cebf4cbf7248f2af4e642d1934688c24f424a5f32449d936f404168d40e2a721255f535bea7f17e20c5540fd341114df3ac462f0130af4e6c35006afa4b1 languageName: node linkType: hard -"@prisma/get-platform@npm:5.10.1": - version: 5.10.1 - resolution: "@prisma/get-platform@npm:5.10.1" +"@prisma/get-platform@npm:5.19.1": + version: 5.19.1 + resolution: "@prisma/get-platform@npm:5.19.1" dependencies: - "@prisma/debug": 5.10.1 - checksum: 76594e3c792cb8d4c23e21b80a8d2682439f9d32a99b24cca31826a3a8803eb72495d6f64d50c9b677265f1b0ef8a2a72812e1d62ed50f7fa13f5dfd072a67b9 + "@prisma/debug": 5.19.1 + checksum: dcf2afecbf881dd4a280d759e39a415901b8206a6b43a774d09e9e8d511f9726d02073a9ac26125bf2bb27cb4d37e1cdd0cd9cc3143c46968bbd3d3a01deabc6 languageName: node linkType: hard @@ -12263,6 +12272,25 @@ __metadata: languageName: node linkType: hard +"chokidar@npm:^3.5.2": + version: 3.6.0 + resolution: "chokidar@npm:3.6.0" + dependencies: + anymatch: ~3.1.2 + braces: ~3.0.2 + fsevents: ~2.3.2 + glob-parent: ~5.1.2 + is-binary-path: ~2.1.0 + is-glob: ~4.0.1 + normalize-path: ~3.0.0 + readdirp: ~3.6.0 + dependenciesMeta: + fsevents: + optional: true + checksum: d2f29f499705dcd4f6f3bbed79a9ce2388cf530460122eed3b9c48efeab7a4e28739c6551fd15bec9245c6b9eeca7a32baa64694d64d9b6faeb74ddb8c4a413d + languageName: node + linkType: hard + "chownr@npm:^1.1.4": version: 1.1.4 resolution: "chownr@npm:1.1.4" @@ -13604,6 +13632,18 @@ __metadata: languageName: node linkType: hard +"debug@npm:^4": + version: 4.3.6 + resolution: "debug@npm:4.3.6" + dependencies: + ms: 2.1.2 + peerDependenciesMeta: + supports-color: + optional: true + checksum: 1630b748dea3c581295e02137a9f5cbe2c1d85fea35c1e6597a65ca2b16a6fce68cec61b299d480787ef310ba927dc8c92d3061faba0ad06c6a724672f66be7f + languageName: node + linkType: hard + "decamelize-keys@npm:^1.1.0": version: 1.1.1 resolution: "decamelize-keys@npm:1.1.1" @@ -17132,6 +17172,16 @@ __metadata: languageName: node linkType: hard +"fsevents@npm:2.3.3, fsevents@npm:^2.3.2, fsevents@npm:~2.3.2": + version: 2.3.3 + resolution: "fsevents@npm:2.3.3" + dependencies: + node-gyp: latest + checksum: 11e6ea6fea15e42461fc55b4b0e4a0a3c654faa567f1877dbd353f39156f69def97a69936d1746619d656c4b93de2238bf731f6085a03a50cabf287c9d024317 + conditions: os=darwin + languageName: node + linkType: hard + "fsevents@npm:^1.0.0": version: 1.2.13 resolution: "fsevents@npm:1.2.13" @@ -17143,12 +17193,11 @@ __metadata: languageName: node linkType: hard -"fsevents@npm:^2.3.2, fsevents@npm:~2.3.2": +"fsevents@patch:fsevents@2.3.3#~builtin, fsevents@patch:fsevents@^2.3.2#~builtin, fsevents@patch:fsevents@~2.3.2#~builtin": version: 2.3.3 - resolution: "fsevents@npm:2.3.3" + resolution: "fsevents@patch:fsevents@npm%3A2.3.3#~builtin::version=2.3.3&hash=df0bf1" dependencies: node-gyp: latest - checksum: 11e6ea6fea15e42461fc55b4b0e4a0a3c654faa567f1877dbd353f39156f69def97a69936d1746619d656c4b93de2238bf731f6085a03a50cabf287c9d024317 conditions: os=darwin languageName: node linkType: hard @@ -17163,15 +17212,6 @@ __metadata: languageName: node linkType: hard -"fsevents@patch:fsevents@^2.3.2#~builtin, fsevents@patch:fsevents@~2.3.2#~builtin": - version: 2.3.3 - resolution: "fsevents@patch:fsevents@npm%3A2.3.3#~builtin::version=2.3.3&hash=df0bf1" - dependencies: - node-gyp: latest - conditions: os=darwin - languageName: node - linkType: hard - "function-bind@npm:^1.1.1": version: 1.1.1 resolution: "function-bind@npm:1.1.1" @@ -18707,6 +18747,13 @@ __metadata: languageName: node linkType: hard +"ignore-by-default@npm:^1.0.1": + version: 1.0.1 + resolution: "ignore-by-default@npm:1.0.1" + checksum: 441509147b3615e0365e407a3c18e189f78c07af08564176c680be1fabc94b6c789cad1342ad887175d4ecd5225de86f73d376cec8e06b42fd9b429505ffcf8a + languageName: node + linkType: hard + "ignore@npm:^5.1.1": version: 5.3.1 resolution: "ignore@npm:5.3.1" @@ -24302,6 +24349,26 @@ __metadata: languageName: node linkType: hard +"nodemon@npm:^3.1.4": + version: 3.1.4 + resolution: "nodemon@npm:3.1.4" + dependencies: + chokidar: ^3.5.2 + debug: ^4 + ignore-by-default: ^1.0.1 + minimatch: ^3.1.2 + pstree.remy: ^1.1.8 + semver: ^7.5.3 + simple-update-notifier: ^2.0.0 + supports-color: ^5.5.0 + touch: ^3.1.0 + undefsafe: ^2.0.5 + bin: + nodemon: bin/nodemon.js + checksum: 3f003fc2c7bdaba559108320f188b7cb063220455e5da218ff3bf4f7468ad7059852da6e35a52b8c690cc27f6e36a433a9ad1f1bdb8096ec1ee3d930629cbeca + languageName: node + linkType: hard + "nofilter@npm:^1.0.4": version: 1.0.4 resolution: "nofilter@npm:1.0.4" @@ -26519,14 +26586,18 @@ __metadata: languageName: node linkType: hard -"prisma@npm:^5.10.1": - version: 5.10.1 - resolution: "prisma@npm:5.10.1" +"prisma@npm:^5.19.1": + version: 5.19.1 + resolution: "prisma@npm:5.19.1" dependencies: - "@prisma/engines": 5.10.1 + "@prisma/engines": 5.19.1 + fsevents: 2.3.3 + dependenciesMeta: + fsevents: + optional: true bin: prisma: build/index.js - checksum: 5da340553f889a6b4724c19d5f9a8feff191409cd30ab8e235a8503e0612de9355ec17fa6a3e55210d023635807f100d9558f7de1d5ab212ccbac15e6c4e5de6 + checksum: b67f9f91969ab7a964b3c0e05adbc8b2695db646157cbec5df6668a8db496553c1ad4803366f11fd9b3bc2b19d4353253bda3ffb97a508f78d334c0fb5b230d2 languageName: node linkType: hard @@ -26670,6 +26741,13 @@ __metadata: languageName: node linkType: hard +"pstree.remy@npm:^1.1.8": + version: 1.1.8 + resolution: "pstree.remy@npm:1.1.8" + checksum: 5cb53698d6bb34dfb278c8a26957964aecfff3e161af5fbf7cee00bbe9d8547c7aced4bd9cb193bce15fb56e9e4220fc02a5bf9c14345ffb13a36b858701ec2d + languageName: node + linkType: hard + "public-encrypt@npm:^4.0.0": version: 4.0.3 resolution: "public-encrypt@npm:4.0.3" @@ -28273,7 +28351,7 @@ __metadata: languageName: node linkType: hard -"safe-stable-stringify@npm:^2.1.0, safe-stable-stringify@npm:^2.3.1, safe-stable-stringify@npm:^2.4.3": +"safe-stable-stringify@npm:2.4.3, safe-stable-stringify@npm:^2.1.0, safe-stable-stringify@npm:^2.3.1, safe-stable-stringify@npm:^2.4.3": version: 2.4.3 resolution: "safe-stable-stringify@npm:2.4.3" checksum: 3aeb64449706ee1f5ad2459fc99648b131d48e7a1fbb608d7c628020177512dc9d94108a5cb61bbc953985d313d0afea6566d243237743e02870490afef04b43 @@ -28825,6 +28903,15 @@ __metadata: languageName: node linkType: hard +"simple-update-notifier@npm:^2.0.0": + version: 2.0.0 + resolution: "simple-update-notifier@npm:2.0.0" + dependencies: + semver: ^7.5.3 + checksum: 9ba00d38ce6a29682f64a46213834e4eb01634c2f52c813a9a7b8873ca49cdbb703696f3290f3b27dc067de6d9418b0b84bef22c3eb074acf352529b2d6c27fd + languageName: node + linkType: hard + "sisteransi@npm:^1.0.5": version: 1.0.5 resolution: "sisteransi@npm:1.0.5" @@ -29969,7 +30056,7 @@ __metadata: languageName: node linkType: hard -"supports-color@npm:^5.3.0": +"supports-color@npm:^5.3.0, supports-color@npm:^5.5.0": version: 5.5.0 resolution: "supports-color@npm:5.5.0" dependencies: @@ -30489,6 +30576,15 @@ __metadata: languageName: node linkType: hard +"touch@npm:^3.1.0": + version: 3.1.1 + resolution: "touch@npm:3.1.1" + bin: + nodetouch: bin/nodetouch.js + checksum: fb8c54207500eb760b6b9d77b9c5626cc027c9ad44431eed4268845f00f8c6bbfc95ce7e9da8e487f020aa921982a8bc5d8e909d0606e82686bd0a08a8e0539b + languageName: node + linkType: hard + "tough-cookie@npm:^4.0.0": version: 4.1.3 resolution: "tough-cookie@npm:4.1.3" @@ -31314,6 +31410,13 @@ __metadata: languageName: node linkType: hard +"undefsafe@npm:^2.0.5": + version: 2.0.5 + resolution: "undefsafe@npm:2.0.5" + checksum: f42ab3b5770fedd4ada175fc1b2eb775b78f609156f7c389106aafd231bfc210813ee49f54483d7191d7b76e483bc7f537b5d92d19ded27156baf57592eb02cc + languageName: node + linkType: hard + "underscore@npm:1.12.1": version: 1.12.1 resolution: "underscore@npm:1.12.1" @@ -33404,25 +33507,6 @@ __metadata: languageName: node linkType: hard -"winston@npm:^3.8.1": - version: 3.10.0 - resolution: "winston@npm:3.10.0" - dependencies: - "@colors/colors": 1.5.0 - "@dabh/diagnostics": ^2.0.2 - async: ^3.2.3 - is-stream: ^2.0.0 - logform: ^2.4.0 - one-time: ^1.0.0 - readable-stream: ^3.4.0 - safe-stable-stringify: ^2.3.1 - stack-trace: 0.0.x - triple-beam: ^1.3.0 - winston-transport: ^4.5.0 - checksum: 47df0361220d12b46d1b3c98a1c380a3718321739d527a182ce7984fc20715e5b0b55db0bcd3fd076d1b1d3261903b890b053851cfd4bc028bda7951fa8ca2e0 - languageName: node - linkType: hard - "winston@npm:^3.9.0": version: 3.11.0 resolution: "winston@npm:3.11.0"