diff --git a/.github/workflows/test_e2e.yml b/.github/workflows/test_e2e.yml index 50de7090b..9a36f063f 100644 --- a/.github/workflows/test_e2e.yml +++ b/.github/workflows/test_e2e.yml @@ -24,7 +24,7 @@ jobs: - name: Prepare test system run: | sudo chown runner:docker -R * - bun install + bun install --frozen-lockfile sudo cp ./nginx/e2e-test.conf /etc/nginx/sites-available/default - name: Boot up test system | seed backend @@ -40,9 +40,8 @@ jobs: cd backend cp .env.test_e2e .env cd .. - bun turbo backend#build - bun turbo frontend#build - bun turbo backend#start frontend#start --env-mode=loose & + bun turbo backend#build frontend#build --env-mode=loose + bun turbo backend#start frontend#start --env-mode=loose & - name: End-to-end tests | prepare run: | @@ -106,3 +105,154 @@ jobs: with: name: backend-logs-pr-#${{ steps.pr.outputs.number }} path: /home/runner/work/gradido/gradido/logs/backend + + end-to-end-tests-playwright: + name: End-to-End Tests Playwright + runs-on: ubuntu-22.04 + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set Node.js version + uses: actions/setup-node@v4 + with: + node-version: '18.20.7' + + - name: install bun + uses: oven-sh/setup-bun@v2 + + - name: Boot up test system | docker-compose mariadb mailserver + run: docker compose -f docker-compose.yml -f docker-compose.test.yml up --detach mariadb mailserver + + - name: Prepare test system + run: | + sudo chown runner:docker -R * + bun install --frozen-lockfile + sudo cp ./nginx/e2e-test.conf /etc/nginx/sites-available/default + + - name: Boot up test system | seed backend + run: bun turbo seed + + - name: copy test config + run: | + cd frontend + cp .env.test_e2e .env + cd ../backend + cp .env.test_e2e .env + + - name: Moving logs after seeding + run: | + mkdir -p /home/runner/work/gradido/gradido/logs/backend/seed + mv /home/runner/work/gradido/gradido/logs/backend/*.log /home/runner/work/gradido/gradido/logs/backend/seed/ + + - name: Boot up test system | docker-compose backend, frontend + run: | + bun turbo backend#build frontend#build --env-mode=loose + bun turbo backend#start frontend#start --env-mode=loose & + + - name: End-to-end tests | prepare + run: | + cd e2e-tests/playwright/typescript + bun install + + - name: wait for frontend and backend to be ready + run: | + until nc -z 127.0.0.1 3000; do echo waiting for frontend; sleep 1; done; + until nc -z 127.0.0.1 4000; do echo waiting for backend; sleep 1; done; + + - name: Start local nginx webserver + run: | + sudo nginx -t + sudo systemctl start nginx + + - name: wait for nginx and mailserver to be ready + run: | + until nc -z 127.0.0.1 80; do echo waiting for nginx; sleep 1; done; + until nc -z 127.0.0.1 1025; do echo waiting for mailserver; sleep 1; done; + + - name: End-to-end tests | run tests + id: e2e-tests + run: | + cd e2e-tests/playwright/typescript + bun run test --project="Google Chrome" + + - name: End-to-end tests | if tests failed, upload video + id: e2e-video + if: ${{ failure() && steps.e2e-tests.conclusion == 'failure' }} + uses: actions/upload-artifact@v4 + with: + name: cypress-videos-pr-#${{ steps.pr.outputs.number }} + path: /home/runner/work/gradido/gradido/e2e-tests/playwright/typescript/test-results + + end-to-end-tests-playwright-java: + name: End-to-End Tests Playwright java + runs-on: ubuntu-22.04 + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set Node.js version + uses: actions/setup-node@v4 + with: + node-version: '18.20.7' + + - uses: actions/setup-java@v3 + with: + distribution: 'temurin' + java-version: '17' + + - name: install bun + uses: oven-sh/setup-bun@v2 + + - name: Boot up test system | docker-compose mariadb mailserver + run: docker compose -f docker-compose.yml -f docker-compose.test.yml up --detach mariadb mailserver + + - name: Prepare test system + run: | + sudo chown runner:docker -R * + bun install + sudo cp ./nginx/e2e-test.conf /etc/nginx/sites-available/default + + - name: Boot up test system | seed backend + run: bun turbo seed + + - name: Moving logs after seeding + run: | + mkdir -p /home/runner/work/gradido/gradido/logs/backend/seed + mv /home/runner/work/gradido/gradido/logs/backend/*.log /home/runner/work/gradido/gradido/logs/backend/seed/ + + - name: Boot up test system | docker-compose backend, frontend + run: | + cd backend + cp .env.test_e2e .env + cd .. + bun turbo backend#build frontend#build --env-mode=loose + bun turbo backend#start frontend#start --env-mode=loose & + + - name: End-to-end tests | prepare + run: | + cd e2e-tests/playwright/typescript + bun install + + - name: wait for frontend and backend to be ready + run: | + until nc -z 127.0.0.1 3000; do echo waiting for frontend; sleep 1; done; + until nc -z 127.0.0.1 4000; do echo waiting for backend; sleep 1; done; + + - name: Start local nginx webserver + run: | + sudo nginx -t + sudo systemctl start nginx + + - name: wait for nginx and mailserver to be ready + run: | + until nc -z 127.0.0.1 80; do echo waiting for nginx; sleep 1; done; + until nc -z 127.0.0.1 1025; do echo waiting for mailserver; sleep 1; done; + + - name: End-to-end tests | run tests + id: e2e-tests + run: | + cd e2e-tests/playwright/java + mvn -B install -D skipTests --no-transfer-progress + mvn exec:java -e -D exec.mainClass=com.microsoft.playwright.CLI -D exec.args="install --with-deps" + mvn test \ No newline at end of file diff --git a/README.md b/README.md index d39282678..94728e957 100644 --- a/README.md +++ b/README.md @@ -174,6 +174,13 @@ turbo frontend#dev backend#start admin#start --env-mode=loose Tip: for local setup use a local nginx server with similar config like docker nginx [nginx.conf](./nginx/gradido.conf) but replace docker image name with localhost +### Clear +In root folder calling `yarn clear` will clear all turbo caches, node_modules and build folders of all workspaces for a clean rebuild. + +```bash +yarn clear +``` + ## Services defined in this package diff --git a/admin/package.json b/admin/package.json index 31b69defc..98c8cc89e 100644 --- a/admin/package.json +++ b/admin/package.json @@ -19,7 +19,8 @@ "test:debug": "cross-env TZ=UTC node --inspect-brk ./node_modules/vitest/vitest.mjs", "test:watch": "cross-env TZ=UTC vitest", "locales": "scripts/sort.sh", - "locales:fix": "scripts/sort.sh --fix" + "locales:fix": "scripts/sort.sh --fix", + "clear": "rm -rf node_modules && rm -rf build && rm -rf .turbo" }, "dependencies": { "@iconify/json": "^2.2.228", diff --git a/backend/.env.test_e2e b/backend/.env.test_e2e index 62abb975d..e7f3a29c8 100644 --- a/backend/.env.test_e2e +++ b/backend/.env.test_e2e @@ -4,6 +4,7 @@ JWT_EXPIRES_IN=2m GDT_ACTIVE=false HUMHUB_ACTIVE=false GMS_ACTIVE=false +USE_CRYPTO_WORKER=true # Email EMAIL=true diff --git a/backend/package.json b/backend/package.json index cd06351aa..94abef346 100644 --- a/backend/package.json +++ b/backend/package.json @@ -22,7 +22,8 @@ "locales": "scripts/sort.sh", "locales:fix": "scripts/sort.sh --fix", "start": "cross-env TZ=UTC node build/index.js", - "typecheck": "tsc --noEmit" + "typecheck": "tsc --noEmit", + "clear": "rm -rf node_modules && rm -rf build && rm -rf .turbo" }, "nodemonConfig": { "ignore": [ diff --git a/backend/src/auth/jwt/JWT.ts b/backend/src/auth/jwt/JWT.ts deleted file mode 100644 index 07384c03b..000000000 --- a/backend/src/auth/jwt/JWT.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { SignJWT, decodeJwt, jwtVerify } from 'jose' - -import { LogError } from '@/server/LogError' - -import { JwtPayloadType } from './payloadtypes/JwtPayloadType' - -import { LOG4JS_BASE_CATEGORY_NAME } from '@/config/const' -import { getLogger } from 'log4js' - -const logger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.auth.jwt.JWT`) - -export const verify = async (token: string, signkey: string): Promise => { - if (!token) { - throw new LogError('401 Unauthorized') - } - logger.info('JWT.verify... token, signkey=', token, signkey) - - try { - /* - const { KeyObject } = await import('node:crypto') - const cryptoKey = await crypto.subtle.importKey('raw', signkey, { name: 'RS256' }, false, [ - 'sign', - ]) - const keyObject = KeyObject.from(cryptoKey) - logger.info('JWT.verify... keyObject=', keyObject) - logger.info('JWT.verify... keyObject.asymmetricKeyDetails=', keyObject.asymmetricKeyDetails) - logger.info('JWT.verify... keyObject.asymmetricKeyType=', keyObject.asymmetricKeyType) - logger.info('JWT.verify... keyObject.asymmetricKeySize=', keyObject.asymmetricKeySize) - */ - const secret = new TextEncoder().encode(signkey) - const { payload } = await jwtVerify(token, secret, { - issuer: 'urn:gradido:issuer', - audience: 'urn:gradido:audience', - }) - logger.info('JWT.verify after jwtVerify... payload=', payload) - return payload as JwtPayloadType - } catch (err) { - logger.error('JWT.verify after jwtVerify... error=', err) - return null - } -} - -export const encode = async (payload: JwtPayloadType, signkey: string): Promise => { - logger.info('JWT.encode... payload=', payload) - logger.info('JWT.encode... signkey=', signkey) - try { - const secret = new TextEncoder().encode(signkey) - const token = await new SignJWT({ payload, 'urn:gradido:claim': true }) - .setProtectedHeader({ - alg: 'HS256', - }) - .setIssuedAt() - .setIssuer('urn:gradido:issuer') - .setAudience('urn:gradido:audience') - .setExpirationTime(payload.expiration) - .sign(secret) - return token - } catch (e) { - logger.error('Failed to sign JWT:', e) - throw e - } -} - -export const verifyJwtType = async (token: string, signkey: string): Promise => { - const payload = await verify(token, signkey) - return payload ? payload.tokentype : 'unknown token type' -} - -export const decode = (token: string): JwtPayloadType => { - const { payload } = decodeJwt(token) - return payload as JwtPayloadType -} diff --git a/backend/src/config/schema.ts b/backend/src/config/schema.ts index 2fb576bdd..f305a7488 100644 --- a/backend/src/config/schema.ts +++ b/backend/src/config/schema.ts @@ -42,7 +42,7 @@ export const schema = Joi.object({ OPENAI_ACTIVE, PRODUCTION, - COMMUNITY_REDEEM_URL: Joi.string() + COMMUNITY_REDEEM_URL: Joi.string() .uri({ scheme: ['http', 'https'] }) .description('The url for redeeming link transactions, must start with frontend base url') .default('http://0.0.0.0/redeem/') diff --git a/backend/src/federation/authenticateCommunities.ts b/backend/src/federation/authenticateCommunities.ts index d2d7b691d..d8946eb47 100644 --- a/backend/src/federation/authenticateCommunities.ts +++ b/backend/src/federation/authenticateCommunities.ts @@ -1,61 +1,78 @@ -import { Community as DbCommunity, FederatedCommunity as DbFederatedCommunity } from 'database' +import { CommunityLoggingView, Community as DbCommunity, FederatedCommunity as DbFederatedCommunity, FederatedCommunityLoggingView, getHomeCommunity } from 'database' import { validate as validateUUID, version as versionUUID } from 'uuid' - +import { randombytes_random } from 'sodium-native' import { CONFIG } from '@/config' import { AuthenticationClient as V1_0_AuthenticationClient } from '@/federation/client/1_0/AuthenticationClient' import { ensureUrlEndsWithSlash } from '@/util/utilities' -import { getLogger } from 'log4js' -import { OpenConnectionArgs } from './client/1_0/model/OpenConnectionArgs' -import { AuthenticationClientFactory } from './client/AuthenticationClientFactory' import { LOG4JS_BASE_CATEGORY_NAME } from '@/config/const' +import { encryptAndSign, OpenConnectionJwtPayloadType } from 'shared' +import { getLogger } from 'log4js' +import { AuthenticationClientFactory } from './client/AuthenticationClientFactory' +import { EncryptedTransferArgs } from 'core' const logger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.federation.authenticateCommunities`) export async function startCommunityAuthentication( - foreignFedCom: DbFederatedCommunity, + fedComB: DbFederatedCommunity, ): Promise { - const homeCom = await DbCommunity.findOneByOrFail({ foreign: false }) - const homeFedCom = await DbFederatedCommunity.findOneByOrFail({ + const methodLogger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.federation.authenticateCommunities.startCommunityAuthentication`) + const handshakeID = randombytes_random().toString() + methodLogger.addContext('handshakeID', handshakeID) + methodLogger.debug(`startCommunityAuthentication()...`, { + fedComB: new FederatedCommunityLoggingView(fedComB), + }) + const homeComA = await getHomeCommunity() + methodLogger.debug('homeComA', new CommunityLoggingView(homeComA!)) + const homeFedComA = await DbFederatedCommunity.findOneByOrFail({ foreign: false, apiVersion: CONFIG.FEDERATION_BACKEND_SEND_ON_API, }) - const foreignCom = await DbCommunity.findOneByOrFail({ publicKey: foreignFedCom.publicKey }) - logger.debug( - 'Authentication: started with foreignFedCom:', - foreignFedCom.endPoint, - foreignFedCom.publicKey.toString('hex'), - ) - // check if communityUuid is a valid v4Uuid and not still a temporary onetimecode - if ( - foreignCom && - ((foreignCom.communityUuid === null && foreignCom.authenticatedAt === null) || - (foreignCom.communityUuid !== null && - !validateUUID(foreignCom.communityUuid) && - versionUUID(foreignCom.communityUuid) !== 4)) - ) { - try { - const client = AuthenticationClientFactory.getInstance(foreignFedCom) + methodLogger.debug('homeFedComA', new FederatedCommunityLoggingView(homeFedComA)) + const comB = await DbCommunity.findOneByOrFail({ publicKey: fedComB.publicKey }) + methodLogger.debug('started with comB:', new CommunityLoggingView(comB)) + // check if communityUuid is not a valid v4Uuid + try { + if ( + comB && + ((comB.communityUuid === null && comB.authenticatedAt === null) || + (comB.communityUuid !== null && + (!validateUUID(comB.communityUuid) || + versionUUID(comB.communityUuid!) !== 4))) + ) { + methodLogger.debug('comB.uuid is null or is a not valid v4Uuid...', comB.communityUuid || 'null', comB.authenticatedAt || 'null') + const client = AuthenticationClientFactory.getInstance(fedComB) if (client instanceof V1_0_AuthenticationClient) { - const args = new OpenConnectionArgs() - args.publicKey = homeCom.publicKey.toString('hex') - // TODO encrypt url with foreignCom.publicKey and sign it with homeCom.privateKey - args.url = ensureUrlEndsWithSlash(homeFedCom.endPoint).concat(homeFedCom.apiVersion) - logger.debug( - 'Authentication: before client.openConnection() args:', - homeCom.publicKey.toString('hex'), - args.url, + if (!comB.publicJwtKey) { + throw new Error('Public JWT key still not exist for comB ' + comB.name) + } + //create JWT with url in payload encrypted by foreignCom.publicJwtKey and signed with homeCom.privateJwtKey + const payload = new OpenConnectionJwtPayloadType(handshakeID, + ensureUrlEndsWithSlash(homeFedComA.endPoint).concat(homeFedComA.apiVersion), ) - if (await client.openConnection(args)) { - logger.debug(`Authentication: successful initiated at community:`, foreignFedCom.endPoint) + methodLogger.debug('payload', payload) + const jws = await encryptAndSign(payload, homeComA!.privateJwtKey!, comB.publicJwtKey!) + methodLogger.debug('jws', jws) + // prepare the args for the client invocation + const args = new EncryptedTransferArgs() + args.publicKey = homeComA!.publicKey.toString('hex') + args.jwt = jws + args.handshakeID = handshakeID + methodLogger.debug('before client.openConnection() args:', args) + const result = await client.openConnection(args) + if (result) { + methodLogger.debug(`successful initiated at community:`, fedComB.endPoint) } else { - logger.error(`Authentication: can't initiate at community:`, foreignFedCom.endPoint) + methodLogger.error(`can't initiate at community:`, fedComB.endPoint) } } - } catch (err) { - logger.error(`Error:`, err) + } else { + methodLogger.debug(`comB.communityUuid is already a valid v4Uuid ${ comB.communityUuid || 'null' } and was authenticated at ${ comB.authenticatedAt || 'null'}`) } + } catch (err) { + methodLogger.error(`Error:`, err) } + methodLogger.removeContext('handshakeID') } diff --git a/backend/src/federation/client/1_0/AuthenticationClient.ts b/backend/src/federation/client/1_0/AuthenticationClient.ts index 0d478a521..b504c0030 100644 --- a/backend/src/federation/client/1_0/AuthenticationClient.ts +++ b/backend/src/federation/client/1_0/AuthenticationClient.ts @@ -5,7 +5,7 @@ import { ensureUrlEndsWithSlash } from '@/util/utilities' import { LOG4JS_BASE_CATEGORY_NAME } from '@/config/const' import { getLogger } from 'log4js' -import { OpenConnectionArgs } from './model/OpenConnectionArgs' +import { EncryptedTransferArgs } from 'core/src/graphql/model/EncryptedTransferArgs' import { openConnection } from './query/openConnection' const logger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.federation.client.1_0.AuthenticationClient`) @@ -27,7 +27,7 @@ export class AuthenticationClient { }) } - async openConnection(args: OpenConnectionArgs): Promise { + async openConnection(args: EncryptedTransferArgs): Promise { logger.debug(`openConnection at ${this.endpoint} for args:`, args) try { const { data } = await this.client.rawRequest<{ openConnection: boolean }>(openConnection, { diff --git a/backend/src/federation/client/1_0/logging/PublicCommunityInfoLogging.view.ts b/backend/src/federation/client/1_0/logging/PublicCommunityInfoLogging.view.ts index 8333da413..99fe31376 100644 --- a/backend/src/federation/client/1_0/logging/PublicCommunityInfoLogging.view.ts +++ b/backend/src/federation/client/1_0/logging/PublicCommunityInfoLogging.view.ts @@ -13,6 +13,7 @@ export class PublicCommunityInfoLoggingView extends AbstractLoggingView { description: this.self.description, creationDate: this.dateToString(this.self.creationDate), publicKey: this.self.publicKey, + publicJwtKey: this.self.publicJwtKey, } } } diff --git a/backend/src/federation/client/1_0/model/OpenConnectionArgs.ts b/backend/src/federation/client/1_0/model/OpenConnectionArgs.ts index 9afdbca5f..7e436da05 100644 --- a/backend/src/federation/client/1_0/model/OpenConnectionArgs.ts +++ b/backend/src/federation/client/1_0/model/OpenConnectionArgs.ts @@ -6,5 +6,5 @@ export class OpenConnectionArgs { publicKey: string @Field(() => String) - url: string + jwt: string } diff --git a/backend/src/federation/client/1_0/model/PublicCommunityInfo.ts b/backend/src/federation/client/1_0/model/PublicCommunityInfo.ts index cad8176be..1abbeb9e7 100644 --- a/backend/src/federation/client/1_0/model/PublicCommunityInfo.ts +++ b/backend/src/federation/client/1_0/model/PublicCommunityInfo.ts @@ -3,4 +3,5 @@ export interface PublicCommunityInfo { description: string creationDate: Date publicKey: string + publicJwtKey: string } diff --git a/backend/src/federation/client/1_0/query/getPublicCommunityInfo.ts b/backend/src/federation/client/1_0/query/getPublicCommunityInfo.ts index f075b2aae..36350fae8 100644 --- a/backend/src/federation/client/1_0/query/getPublicCommunityInfo.ts +++ b/backend/src/federation/client/1_0/query/getPublicCommunityInfo.ts @@ -7,6 +7,7 @@ export const getPublicCommunityInfo = gql` description creationDate publicKey + publicJwtKey } } ` diff --git a/backend/src/federation/client/1_0/query/openConnection.ts b/backend/src/federation/client/1_0/query/openConnection.ts index f049df5a9..93c4ce772 100644 --- a/backend/src/federation/client/1_0/query/openConnection.ts +++ b/backend/src/federation/client/1_0/query/openConnection.ts @@ -1,7 +1,7 @@ import { gql } from 'graphql-request' export const openConnection = gql` - mutation ($args: OpenConnectionArgs!) { + mutation ($args: EncryptedTransferArgs!) { openConnection(data: $args) } ` diff --git a/backend/src/federation/validateCommunities.test.ts b/backend/src/federation/validateCommunities.test.ts index b6ef2418a..255f8f180 100644 --- a/backend/src/federation/validateCommunities.test.ts +++ b/backend/src/federation/validateCommunities.test.ts @@ -343,7 +343,7 @@ describe('validate Communities', () => { }) it('logs unsupported api for community with api 2_0 ', () => { expect(logger.debug).toBeCalledWith( - 'dbCom with unsupported apiVersion', + 'dbFedComB with unsupported apiVersion', dbCom.endPoint, '2_0', ) diff --git a/backend/src/federation/validateCommunities.ts b/backend/src/federation/validateCommunities.ts index bbdd5d4d2..478261386 100644 --- a/backend/src/federation/validateCommunities.ts +++ b/backend/src/federation/validateCommunities.ts @@ -2,18 +2,20 @@ import { Community as DbCommunity, FederatedCommunity as DbFederatedCommunity, FederatedCommunityLoggingView, + getHomeCommunity, } from 'database' import { IsNull } from 'typeorm' +import { LOG4JS_BASE_CATEGORY_NAME } from '@/config/const' import { FederationClient as V1_0_FederationClient } from '@/federation/client/1_0/FederationClient' import { PublicCommunityInfo } from '@/federation/client/1_0/model/PublicCommunityInfo' import { FederationClientFactory } from '@/federation/client/FederationClientFactory' import { LogError } from '@/server/LogError' +import { createKeyPair } from 'shared' import { getLogger } from 'log4js' import { startCommunityAuthentication } from './authenticateCommunities' import { PublicCommunityInfoLoggingView } from './client/1_0/logging/PublicCommunityInfoLogging.view' import { ApiVersionType } from './enum/apiVersionType' -import { LOG4JS_BASE_CATEGORY_NAME } from '@/config/const' const logger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.federation.validateCommunities`) @@ -34,6 +36,7 @@ export async function startValidateCommunities(timerInterval: number): Promise { + // search all foreign federated communities which are still not verified or have not been verified since last dht-announcement const dbFederatedCommunities: DbFederatedCommunity[] = await DbFederatedCommunity.createQueryBuilder() .where({ foreign: true, verifiedAt: IsNull() }) @@ -41,32 +44,36 @@ export async function validateCommunities(): Promise { .getMany() logger.debug(`found ${dbFederatedCommunities.length} dbCommunities`) - for (const dbCom of dbFederatedCommunities) { - logger.debug('dbCom', new FederatedCommunityLoggingView(dbCom)) + for (const dbFedComB of dbFederatedCommunities) { + logger.debug('dbFedComB', new FederatedCommunityLoggingView(dbFedComB)) const apiValueStrings: string[] = Object.values(ApiVersionType) logger.debug(`suppported ApiVersions=`, apiValueStrings) - if (!apiValueStrings.includes(dbCom.apiVersion)) { - logger.debug('dbCom with unsupported apiVersion', dbCom.endPoint, dbCom.apiVersion) + if (!apiValueStrings.includes(dbFedComB.apiVersion)) { + logger.debug('dbFedComB with unsupported apiVersion', dbFedComB.endPoint, dbFedComB.apiVersion) continue } try { - const client = FederationClientFactory.getInstance(dbCom) + const client = FederationClientFactory.getInstance(dbFedComB) if (client instanceof V1_0_FederationClient) { const pubKey = await client.getPublicKey() - if (pubKey && pubKey === dbCom.publicKey.toString('hex')) { - await DbFederatedCommunity.update({ id: dbCom.id }, { verifiedAt: new Date() }) - logger.debug(`verified community with:`, dbCom.endPoint) + if (pubKey && pubKey === dbFedComB.publicKey.toString('hex')) { + await DbFederatedCommunity.update({ id: dbFedComB.id }, { verifiedAt: new Date() }) + logger.debug(`verified dbFedComB with:`, dbFedComB.endPoint) const pubComInfo = await client.getPublicCommunityInfo() if (pubComInfo) { - await writeForeignCommunity(dbCom, pubComInfo) - await startCommunityAuthentication(dbCom) - logger.debug(`write publicInfo of community: name=${pubComInfo.name}`) + await writeForeignCommunity(dbFedComB, pubComInfo) + logger.debug(`wrote response of getPublicCommunityInfo in dbFedComB ${dbFedComB.endPoint}`) + try { + await startCommunityAuthentication(dbFedComB) + } catch (err) { + logger.warn(`Warning: Authentication of community ${dbFedComB.endPoint} still ongoing:`, err) + } } else { logger.debug('missing result of getPublicCommunityInfo') } } else { - logger.debug('received not matching publicKey:', pubKey, dbCom.publicKey.toString('hex')) + logger.debug('received not matching publicKey:', pubKey, dbFedComB.publicKey.toString('hex')) } } } catch (err) { @@ -75,6 +82,36 @@ export async function validateCommunities(): Promise { } } +export async function writeJwtKeyPairInHomeCommunity(): Promise { + logger.debug(`Federation: writeJwtKeyPairInHomeCommunity`) + try { + // check for existing homeCommunity entry + let homeCom = await getHomeCommunity() + if (homeCom) { + if (!homeCom.publicJwtKey && !homeCom.privateJwtKey) { + // Generate key pair using jose library + const { publicKey, privateKey } = await createKeyPair(); + logger.debug(`Federation: writeJwtKeyPairInHomeCommunity publicKey=`, publicKey); + logger.debug(`Federation: writeJwtKeyPairInHomeCommunity privateKey=`, privateKey.slice(0, 20)); + + homeCom.publicJwtKey = publicKey; + logger.debug(`Federation: writeJwtKeyPairInHomeCommunity publicJwtKey.length=`, homeCom.publicJwtKey.length); + homeCom.privateJwtKey = privateKey; + logger.debug(`Federation: writeJwtKeyPairInHomeCommunity privateJwtKey.length=`, homeCom.privateJwtKey.length); + await DbCommunity.save(homeCom) + logger.debug(`Federation: writeJwtKeyPairInHomeCommunity done`) + } else { + logger.debug(`Federation: writeJwtKeyPairInHomeCommunity: keypair already exists`) + } + } else { + throw new Error(`Error! A HomeCommunity-Entry still not exist! Please start the DHT-Modul first.`) + } + return homeCom + } catch (err) { + throw new Error(`Error writing JwtKeyPair in HomeCommunity-Entry: ${err}`) + } +} + async function writeForeignCommunity( dbCom: DbFederatedCommunity, pubInfo: PublicCommunityInfo, @@ -96,6 +133,7 @@ async function writeForeignCommunity( com.foreign = true com.name = pubInfo.name com.publicKey = dbCom.publicKey + com.publicJwtKey = pubInfo.publicJwtKey com.url = dbCom.endPoint await DbCommunity.save(com) } diff --git a/backend/src/graphql/model/RedeemJwtLink.ts b/backend/src/graphql/model/RedeemJwtLink.ts index 018a92bb0..64544ad0e 100644 --- a/backend/src/graphql/model/RedeemJwtLink.ts +++ b/backend/src/graphql/model/RedeemJwtLink.ts @@ -1,7 +1,7 @@ import { Decimal } from 'decimal.js-light' import { Field, ObjectType } from 'type-graphql' -import { RedeemJwtPayloadType } from '@/auth/jwt/payloadtypes/RedeemJwtPayloadType' +import { RedeemJwtPayloadType } from 'shared' import { Community } from './Community' import { User } from './User' diff --git a/backend/src/graphql/resolver/TransactionLinkResolver.ts b/backend/src/graphql/resolver/TransactionLinkResolver.ts index a61aaa756..d8fab512a 100644 --- a/backend/src/graphql/resolver/TransactionLinkResolver.ts +++ b/backend/src/graphql/resolver/TransactionLinkResolver.ts @@ -27,8 +27,6 @@ import { Decimal } from 'decimal.js-light' import { Arg, Args, Authorized, Ctx, Int, Mutation, Query, Resolver } from 'type-graphql' import { RIGHTS } from '@/auth/RIGHTS' -import { decode, encode, verify } from '@/auth/jwt/JWT' -import { RedeemJwtPayloadType } from '@/auth/jwt/payloadtypes/RedeemJwtPayloadType' import { EVENT_CONTRIBUTION_LINK_REDEEM, EVENT_TRANSACTION_LINK_CREATE, @@ -43,13 +41,12 @@ import { } from '@/util/InterruptiveSleepManager' import { TRANSACTIONS_LOCK } from '@/util/TRANSACTIONS_LOCK' import { TRANSACTION_LINK_LOCK } from '@/util/TRANSACTION_LINK_LOCK' -import { calculateDecay } from 'shared' import { fullName } from '@/util/utilities' import { calculateBalance } from '@/util/validate' +import { calculateDecay, decode, DisburseJwtPayloadType, encode, RedeemJwtPayloadType, verify } from 'shared' -import { DisburseJwtPayloadType } from '@/auth/jwt/payloadtypes/DisburseJwtPayloadType' -import { Logger, getLogger } from 'log4js' import { LOG4JS_BASE_CATEGORY_NAME } from '@/config/const' +import { getLogger, Logger } from 'log4js' import { executeTransaction } from './TransactionResolver' import { getAuthenticatedCommunities, @@ -579,7 +576,7 @@ export class TransactionLinkResolver { throw new LogError('Sender community UUID is not set') } // now with the sender community UUID the jwt token can be verified - const verifiedJwtPayload = await verify(code, senderCom.communityUuid) + const verifiedJwtPayload = await verify('handshakeID', code, senderCom.communityUuid) logger.debug( 'TransactionLinkResolver.queryRedeemJwtLink... nach verify verifiedJwtPayload=', verifiedJwtPayload, diff --git a/backend/src/index.ts b/backend/src/index.ts index 107d5af1e..a810b64a9 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -5,12 +5,14 @@ import { sendTransactionsToDltConnector } from './apis/dltConnector/sendTransact import { CONFIG } from './config' import { startValidateCommunities } from './federation/validateCommunities' import { createServer } from './server/createServer' +import { writeJwtKeyPairInHomeCommunity } from './federation/validateCommunities' import { initLogging } from './server/logger' async function main() { initLogging() const { app } = await createServer(getLogger('apollo')) + await writeJwtKeyPairInHomeCommunity() app.listen(CONFIG.PORT, () => { // biome-ignore lint/suspicious/noConsole: no need for logging the start message console.log(`Server is running at http://localhost:${CONFIG.PORT}`) diff --git a/backend/src/seeds/index.ts b/backend/src/seeds/index.ts index 4189f87e3..451f671ac 100644 --- a/backend/src/seeds/index.ts +++ b/backend/src/seeds/index.ts @@ -58,6 +58,7 @@ const run = async () => { const { con } = server await cleanDB() logger.info('##seed## clean database successful...') + logger.info(`crypto worker enabled: ${CONFIG.USE_CRYPTO_WORKER}`) // seed home community await writeHomeCommunityEntry() diff --git a/bun.lock b/bun.lock index 18cacf7bf..7087ef10f 100644 --- a/bun.lock +++ b/bun.lock @@ -6,6 +6,7 @@ "dependencies": { "auto-changelog": "^2.4.0", "cross-env": "^7.0.3", + "jose": "^4.14.4", "turbo": "^2.5.0", "uuid": "^8.3.2", }, diff --git a/config-schema/package.json b/config-schema/package.json index c3a6a2b1d..4ac90e45c 100644 --- a/config-schema/package.json +++ b/config-schema/package.json @@ -21,7 +21,8 @@ "test": "bun test", "test:debug": "bun test --inspect-brk", "lint": "biome check --error-on-warnings .", - "lint:fix": "biome check --error-on-warnings . --write" + "lint:fix": "biome check --error-on-warnings . --write", + "clear": "rm -rf node_modules && rm -rf build && rm -rf .turbo" }, "devDependencies": { "@biomejs/biome": "2.0.0", diff --git a/core/package.json b/core/package.json index 98863bb60..047a39d20 100644 --- a/core/package.json +++ b/core/package.json @@ -21,17 +21,21 @@ "test:debug": "bun test --inspect-brk", "typecheck": "tsc --noEmit", "lint": "biome check --error-on-warnings .", - "lint:fix": "biome check --error-on-warnings . --write" + "lint:fix": "biome check --error-on-warnings . --write", + "clear": "rm -rf node_modules && rm -rf build && rm -rf .turbo" }, "devDependencies": { "@biomejs/biome": "2.0.0", "@types/node": "^17.0.21", + "type-graphql": "^1.1.1", "typescript": "^4.9.5" }, "dependencies": { "database": "*", "esbuild": "^0.25.2", + "jose": "^4.14.4", "log4js": "^6.9.1", + "shared": "*", "zod": "^3.25.61" }, "engines": { diff --git a/core/src/config/const.ts b/core/src/config/const.ts index 1c19265d0..375eacd15 100644 --- a/core/src/config/const.ts +++ b/core/src/config/const.ts @@ -1 +1 @@ -export const LOG4JS_BASE_CATEGORY_NAME = 'core' \ No newline at end of file +export const LOG4JS_BASE_CATEGORY_NAME = 'core' diff --git a/core/src/graphql/logic/interpretEncryptedTransferArgs.ts b/core/src/graphql/logic/interpretEncryptedTransferArgs.ts new file mode 100644 index 000000000..b5b2f7f7a --- /dev/null +++ b/core/src/graphql/logic/interpretEncryptedTransferArgs.ts @@ -0,0 +1,42 @@ +import { EncryptedTransferArgs } from '../model/EncryptedTransferArgs' +import { JwtPayloadType } from 'shared' +import { Community as DbCommunity } from 'database' +import { getLogger } from 'log4js' +import { CommunityLoggingView, getHomeCommunity } from 'database' +import { verifyAndDecrypt } from 'shared' +import { LOG4JS_BASE_CATEGORY_NAME } from '../../config/const' + +const logger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.graphql.resolver.util.interpretEncryptedTransferArgs`) + +export const interpretEncryptedTransferArgs = async (args: EncryptedTransferArgs): Promise => { + const methodLogger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.graphql.resolver.util.interpretEncryptedTransferArgs-method`) + methodLogger.addContext('handshakeID', args.handshakeID) + methodLogger.debug('interpretEncryptedTransferArgs()... args:', args) + // first find with args.publicKey the community 'requestingCom', which starts the request + const requestingCom = await DbCommunity.findOneBy({ publicKey: Buffer.from(args.publicKey, 'hex') }) + if (!requestingCom) { + const errmsg = `unknown requesting community with publicKey ${args.publicKey}` + methodLogger.error(errmsg) + methodLogger.removeContext('handshakeID') + throw new Error(errmsg) + } + if (!requestingCom.publicJwtKey) { + const errmsg = `missing publicJwtKey of requesting community with publicKey ${args.publicKey}` + methodLogger.error(errmsg) + methodLogger.removeContext('handshakeID') + throw new Error(errmsg) + } + methodLogger.debug(`found requestingCom:`, new CommunityLoggingView(requestingCom)) + // verify the signing of args.jwt with homeCom.privateJwtKey and decrypt args.jwt with requestingCom.publicJwtKey + const homeCom = await getHomeCommunity() + const jwtPayload = await verifyAndDecrypt(args.handshakeID, args.jwt, homeCom!.privateJwtKey!, requestingCom.publicJwtKey) as JwtPayloadType + if (!jwtPayload) { + const errmsg = `invalid payload of community with publicKey ${args.publicKey}` + methodLogger.error(errmsg) + methodLogger.removeContext('handshakeID') + throw new Error(errmsg) + } + methodLogger.debug('jwtPayload', jwtPayload) + methodLogger.removeContext('handshakeID') + return jwtPayload +} diff --git a/federation/src/graphql/api/1_0/model/OpenConnectionArgs.ts b/core/src/graphql/model/EncryptedTransferArgs.ts similarity index 57% rename from federation/src/graphql/api/1_0/model/OpenConnectionArgs.ts rename to core/src/graphql/model/EncryptedTransferArgs.ts index 9afdbca5f..78972ac85 100644 --- a/federation/src/graphql/api/1_0/model/OpenConnectionArgs.ts +++ b/core/src/graphql/model/EncryptedTransferArgs.ts @@ -1,10 +1,13 @@ import { Field, InputType } from 'type-graphql' @InputType() -export class OpenConnectionArgs { +export class EncryptedTransferArgs { + @Field(() => String) + handshakeID: string + @Field(() => String) publicKey: string @Field(() => String) - url: string + jwt: string } diff --git a/core/src/index.ts b/core/src/index.ts index 0984ebefc..28aae758a 100644 --- a/core/src/index.ts +++ b/core/src/index.ts @@ -1 +1,3 @@ -export * from './validation/user' \ No newline at end of file +export * from './validation/user' +export * from './graphql/logic/interpretEncryptedTransferArgs' +export * from './graphql/model/EncryptedTransferArgs' diff --git a/database/migration/migrations/0091-add_jwt-keypair_in_community_table.ts b/database/migration/migrations/0091-add_jwt-keypair_in_community_table.ts new file mode 100644 index 000000000..21d5c44fc --- /dev/null +++ b/database/migration/migrations/0091-add_jwt-keypair_in_community_table.ts @@ -0,0 +1,18 @@ +/* MIGRATION TO ADD JWT-KEYPAIR IN COMMUNITY TABLE + * + * This migration adds fields for the jwt-keypair in the community.table + */ + +export async function upgrade(queryFn: (query: string, values?: any[]) => Promise>) { + await queryFn( + 'ALTER TABLE `communities` ADD COLUMN `public_jwt_key` varchar(512) DEFAULT NULL AFTER `gms_api_key`;', + ) + await queryFn( + 'ALTER TABLE `communities` ADD COLUMN `private_jwt_key` varchar(2048) DEFAULT NULL AFTER `public_jwt_key`;', + ) +} + +export async function downgrade(queryFn: (query: string, values?: any[]) => Promise>) { + await queryFn('ALTER TABLE `communities` DROP COLUMN `public_jwt_key`;') + await queryFn('ALTER TABLE `communities` DROP COLUMN `private_jwt_key`;') +} diff --git a/database/package.json b/database/package.json index 21d027427..b763cf1ca 100644 --- a/database/package.json +++ b/database/package.json @@ -19,7 +19,7 @@ "typecheck": "tsc --noEmit", "lint": "biome check --error-on-warnings .", "lint:fix": "biome check --error-on-warnings . --write", - "clear": "cross-env TZ=UTC tsx migration/index.ts clear", + "clearDB": "cross-env TZ=UTC tsx migration/index.ts clear", "test": "cross-env TZ=UTC NODE_ENV=development DB_DATABASE=gradido_test vitest --reporter verbose --no-file-parallelism run", "test:debug": "cross-env TZ=UTC NODE_ENV=development DB_DATABASE=gradido_test node --inspect-brk node_modules/.bin/jest --bail --runInBand --forceExit --detectOpenHandles", "up": "cross-env TZ=UTC tsx migration/index.ts up", @@ -28,7 +28,8 @@ "up:test": "cross-env TZ=UTC DB_DATABASE=gradido_test tsx migration/index.ts up", "up:backend_test": "cross-env TZ=UTC DB_DATABASE=gradido_test_backend tsx migration/index.ts up", "up:federation_test": "cross-env TZ=UTC DB_DATABASE=gradido_test_federation tsx migration/index.ts up", - "up:dht_test": "cross-env TZ=UTC DB_DATABASE=gradido_test_dht tsx migration/index.ts up" + "up:dht_test": "cross-env TZ=UTC DB_DATABASE=gradido_test_dht tsx migration/index.ts up", + "clear": "rm -rf node_modules && rm -rf build && rm -rf .turbo" }, "devDependencies": { "@biomejs/biome": "2.0.0", diff --git a/database/src/entity/Community.ts b/database/src/entity/Community.ts index 314e96f6a..b5e3f0ce4 100644 --- a/database/src/entity/Community.ts +++ b/database/src/entity/Community.ts @@ -54,6 +54,12 @@ export class Community extends BaseEntity { @Column({ name: 'gms_api_key', type: 'varchar', length: 512, nullable: true, default: null }) gmsApiKey: string | null + @Column({ name: 'public_jwt_key', type: 'varchar', length: 512, nullable: true }) + publicJwtKey: string | null + + @Column({ name: 'private_jwt_key', type: 'varchar', length: 2048, nullable: true }) + privateJwtKey: string | null + @Column({ name: 'location', type: 'geometry', diff --git a/database/src/logging/CommunityLogging.view.ts b/database/src/logging/CommunityLogging.view.ts index 9bd847416..c06a4db41 100644 --- a/database/src/logging/CommunityLogging.view.ts +++ b/database/src/logging/CommunityLogging.view.ts @@ -13,6 +13,7 @@ export class CommunityLoggingView extends AbstractLoggingView { foreign: this.self.foreign, url: this.self.url, publicKey: this.self.publicKey.toString(this.bufferStringFormat), + publicJwtKey: this.self.publicJwtKey, communityUuid: this.self.communityUuid, authenticatedAt: this.dateToString(this.self.authenticatedAt), name: this.self.name, diff --git a/deployment/bare_metal/start.sh b/deployment/bare_metal/start.sh index 0eda8958a..498a112b9 100755 --- a/deployment/bare_metal/start.sh +++ b/deployment/bare_metal/start.sh @@ -282,7 +282,7 @@ bun install # build all modules log_step 'build all modules' -turbo build --env-mode=loose +turbo build --env-mode=loose --concurrency=$(nproc) # database log_step 'Updating database' diff --git a/dht-node/package.json b/dht-node/package.json index 8a5cd4462..e2f64191c 100644 --- a/dht-node/package.json +++ b/dht-node/package.json @@ -17,7 +17,8 @@ "lint:fix": "biome check --error-on-warnings . --write", "test": "cross-env TZ=UTC NODE_ENV=development DB_DATABASE=gradido_test_dht jest --verbose --runInBand --forceExit --detectOpenHandles", "test:debug": "cross-env TZ=UTC NODE_ENV=development DB_DATABASE=gradido_test_dht node --inspect-brk node_modules/.bin/jest --bail --runInBand --forceExit --detectOpenHandles", - "test:coverage": "cross-env TZ=UTC NODE_ENV=development DB_DATABASE=gradido_test_dht jest --coverage --runInBand --forceExit --detectOpenHandles" + "test:coverage": "cross-env TZ=UTC NODE_ENV=development DB_DATABASE=gradido_test_dht jest --coverage --runInBand --forceExit --detectOpenHandles", + "clear": "rm -rf node_modules && rm -rf build && rm -rf .turbo" }, "dependencies": { "cross-env": "^7.0.3", diff --git a/e2e-tests/.gitignore b/e2e-tests/.gitignore index 0d29725c0..405b844c5 100644 --- a/e2e-tests/.gitignore +++ b/e2e-tests/.gitignore @@ -3,4 +3,4 @@ cypress/screenshots/ cypress/videos/ cypress/reports/ cucumber-messages.ndjson - +**/target diff --git a/e2e-tests/playwright/Dockerfile b/e2e-tests/playwright/Dockerfile deleted file mode 100644 index c0dfbde47..000000000 --- a/e2e-tests/playwright/Dockerfile +++ /dev/null @@ -1,42 +0,0 @@ -############################################################################### -# Dockerfile to create a ready-to-use Playwright Docker image for end-to-end -# testing. -# -# To avoid hardcoded versoning of Playwright, this Dockerfile is a custom -# version of the ready-to-use Dockerfile privided by Playwright developement -# (https://github.com/microsoft/playwright/blob/main/utils/docker/Dockerfile.focal) -# -# Here the latest stable versions of the browsers Chromium, Firefox, and Webkit -# (Safari) are installed, icluding all dependencies based on Ubuntu specified by -# Playwright developement. -############################################################################### - -FROM ubuntu:focal - -# set a timezone for the Playwright browser dependency installation -ARG TZ=Europe/Berlin - -ARG DOCKER_WORKDIR=/tests/ -WORKDIR $DOCKER_WORKDIR - -# package manager preparation -RUN apt-get -qq update && apt-get install -qq -y curl gpg > /dev/null -# for Node.js -RUN curl -sL https://deb.nodesource.com/setup_16.x | bash - -# for Yarn -RUN curl -sL https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - && \ - echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list - -# install node v16 and Yarn -RUN apt-get -qq update && apt-get install -qq -y nodejs yarn - -COPY tests/package.json tests/yarn.lock $DOCKER_WORKDIR - -# install Playwright with all dependencies -# for the browsers chromium, firefox, and webkit -RUN yarn install && yarn playwright install --with-deps - -# clean up -RUN rm -rf /var/lib/apt/lists/* && apt-get -qq clean - -COPY tests/ $DOCKER_WORKDIR diff --git a/e2e-tests/playwright/README.md b/e2e-tests/playwright/README.md deleted file mode 100644 index a3a2b34bb..000000000 --- a/e2e-tests/playwright/README.md +++ /dev/null @@ -1,24 +0,0 @@ -# Gradido End-to-End Testing with [Playwright](https://playwright.dev/) (CI-ready via Docker) - - -A sample setup to show-case Playwright (using Typescript) as an end-to-end testing tool for Gradido runniing in a Docker container. -Here we have a simple UI-based happy path login test running against the DEV system. - -## Precondition -Since dependencies and configurations for Github Actions integration is not set up yet, please run in root directory - -```bash -docker-compose up -``` - -to boot up the DEV system, before running the test. - -## Execute the test - -```bash -# build a Docker image from the Dockerfile -docker build -t gradido_e2e-tests-playwright . - -# run the Docker container and execute the given tests -docker run -it --network=host gradido_e2e-tests-playwright yarn playwright-e2e-tests -``` diff --git a/e2e-tests/playwright/java/Readme.md b/e2e-tests/playwright/java/Readme.md new file mode 100644 index 000000000..68aebae3d --- /dev/null +++ b/e2e-tests/playwright/java/Readme.md @@ -0,0 +1,12 @@ +# gradido-e2e-tests-playwright with java + +Experimental End-to-end tests with Playwright and Java + +## Prerequisites + +- Java 17 +- Maven +- Playwright +- Gradle +- Git + diff --git a/e2e-tests/playwright/java/pom.xml b/e2e-tests/playwright/java/pom.xml new file mode 100644 index 000000000..4e8b82d8c --- /dev/null +++ b/e2e-tests/playwright/java/pom.xml @@ -0,0 +1,52 @@ + + + 4.0.0 + net.gradido + gradido-e2e-tests-playwright + 0.0.1 + Gradido Playwright End-to-End Tests in Java + + UTF-8 + 17 + 17 + + + + com.microsoft.playwright + playwright + 1.52.0 + + + + org.junit.jupiter + junit-jupiter + 5.10.0 + test + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.10.1 + + + 17 + 17 + + + + org.apache.maven.plugins + maven-surefire-plugin + 3.0.0-M9 + + + **/*Test*.java + + + + + + \ No newline at end of file diff --git a/e2e-tests/playwright/java/src/test/java/net/gradido/e2e/base/BaseTest.java b/e2e-tests/playwright/java/src/test/java/net/gradido/e2e/base/BaseTest.java new file mode 100644 index 000000000..fbf751f6c --- /dev/null +++ b/e2e-tests/playwright/java/src/test/java/net/gradido/e2e/base/BaseTest.java @@ -0,0 +1,69 @@ +package net.gradido.e2e.base; + +import com.microsoft.playwright.Browser; +import com.microsoft.playwright.BrowserContext; +import com.microsoft.playwright.Playwright; +import com.microsoft.playwright.Page; +import com.microsoft.playwright.Tracing; +import org.junit.jupiter.api.*; +import java.nio.file.Path; +import java.nio.file.Paths; + +// Subclasses will inherit PER_CLASS behavior. +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +public class BaseTest { + + protected Playwright playwright; + protected Browser browser; + + @BeforeAll + void setUpAll() { + playwright = Playwright.create(); + browser = playwright.chromium().launch(); + } + + @AfterAll + void tearDownAll() { + playwright.close(); + } + + // New instance for each test method. + protected BrowserContext context; + protected Page page; + protected Path currentTracePath; + + @BeforeEach + void setUp(TestInfo testInfo) { + context = browser.newContext( + new Browser.NewContextOptions().setBaseURL("http://localhost:3000") + ); + context.route("**/*", route -> { + String url = route.request().url(); + + // we skip fontawesome and googleapis requests, we don't need them for functions test, but they cost time + if (url.contains("use.fontawesome.com") || url.contains("fonts.googleapis.com")) { + route.abort(); + return; + } + route.resume(); + }); + + // Start tracing before creating + String testName = testInfo.getDisplayName().replaceAll("[^a-zA-Z0-9]", ""); + currentTracePath = Paths.get("target/traces/" + testName + ".zip"); + context.tracing().start(new Tracing.StartOptions() + .setScreenshots(true) + .setSnapshots(true) + .setSources(true)); + page = context.newPage(); + } + + @AfterEach + void tearDown() { + // Stop tracing and export it into a zip archive. + context.tracing().stop(new Tracing.StopOptions().setPath(currentTracePath)); + + page.close(); + context.close(); + } +} \ No newline at end of file diff --git a/e2e-tests/playwright/java/src/test/java/net/gradido/e2e/components/Toasts.java b/e2e-tests/playwright/java/src/test/java/net/gradido/e2e/components/Toasts.java new file mode 100644 index 000000000..2e59684b3 --- /dev/null +++ b/e2e-tests/playwright/java/src/test/java/net/gradido/e2e/components/Toasts.java @@ -0,0 +1,26 @@ +package net.gradido.e2e.components; + +import com.microsoft.playwright.Locator; +import com.microsoft.playwright.Page; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class Toasts { + public final Locator toastSlot; + public final Locator toastTypeError; + public final Locator toastTitle; + public final Locator toastMessage; + + public Toasts(Page page) { + toastSlot = page.locator("#__BVID__toaster-container"); + toastTypeError = toastSlot.locator(".toast.text-bg-danger"); + toastTitle = toastTypeError.locator(".gdd-toaster-title"); + toastMessage = toastTypeError.locator(".gdd-toaster-body"); + } + + public void assertErrorToastVisible() { + toastTypeError.waitFor(); + assertTrue(toastTitle.isVisible()); + assertTrue(toastMessage.isVisible()); + } +} diff --git a/e2e-tests/playwright/java/src/test/java/net/gradido/e2e/pages/LoginPage.java b/e2e-tests/playwright/java/src/test/java/net/gradido/e2e/pages/LoginPage.java new file mode 100644 index 000000000..1de1ba9f5 --- /dev/null +++ b/e2e-tests/playwright/java/src/test/java/net/gradido/e2e/pages/LoginPage.java @@ -0,0 +1,32 @@ +package net.gradido.e2e.pages; + +import com.microsoft.playwright.Locator; +import com.microsoft.playwright.Page; +import com.microsoft.playwright.Response; + +public class LoginPage { + private final Page page; + + private final Locator emailInput; + private final Locator passwordInput; + private final Locator submitButton; + + public LoginPage(Page page) { + this.page = page; + emailInput = page.locator("input[name='email']"); + passwordInput = page.locator("input[name='password']"); + submitButton = page.locator("button[type='submit']"); + } + + public void login(String email, String password) { + emailInput.fill(email); + passwordInput.fill(password); + Response response = page.waitForResponse("**/graphql", () -> { + submitButton.click(); + }); + } + + public void navigate() { + page.navigate("http://localhost:3000/login"); + } +} \ No newline at end of file diff --git a/e2e-tests/playwright/java/src/test/java/net/gradido/e2e/tests/InvalidLoginTest.java b/e2e-tests/playwright/java/src/test/java/net/gradido/e2e/tests/InvalidLoginTest.java new file mode 100644 index 000000000..5d9a70d5b --- /dev/null +++ b/e2e-tests/playwright/java/src/test/java/net/gradido/e2e/tests/InvalidLoginTest.java @@ -0,0 +1,26 @@ +package net.gradido.e2e.tests; + +import net.gradido.e2e.base.BaseTest; +import net.gradido.e2e.pages.LoginPage; +import net.gradido.e2e.components.Toasts; +import org.junit.jupiter.api.*; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class InvalidLoginTest extends BaseTest { + private LoginPage loginPage; + + @BeforeEach + void initPageObjects() { + loginPage = new LoginPage(page); + } + + @Test + void invalidUserSeesError() { + loginPage.navigate(); + loginPage.login("peter@lustig.de", "wrongpass"); + Toasts toast = new Toasts(page); + toast.assertErrorToastVisible(); + } +} \ No newline at end of file diff --git a/e2e-tests/playwright/java/src/test/java/net/gradido/e2e/tests/ValidLoginTest.java b/e2e-tests/playwright/java/src/test/java/net/gradido/e2e/tests/ValidLoginTest.java new file mode 100644 index 000000000..e9b3d4a50 --- /dev/null +++ b/e2e-tests/playwright/java/src/test/java/net/gradido/e2e/tests/ValidLoginTest.java @@ -0,0 +1,28 @@ +package net.gradido.e2e.tests; + +import net.gradido.e2e.base.BaseTest; +import net.gradido.e2e.pages.LoginPage; +import net.gradido.e2e.components.Toasts; + +import org.junit.jupiter.api.*; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class ValidLoginTest extends BaseTest { + private LoginPage loginPage; + + @BeforeEach + void initPageObjects() { + loginPage = new LoginPage(page); + } + + @Test + void validUserCanLogin() { + loginPage.navigate(); + loginPage.login("peter@lustig.de", "Aa12345_"); + page.waitForURL("http://localhost:3000/overview"); + assertTrue(page.url().contains("/overview")); + } +} + diff --git a/e2e-tests/playwright/java/src/test/java/net/gradido/e2e/utils/TestUtil.java b/e2e-tests/playwright/java/src/test/java/net/gradido/e2e/utils/TestUtil.java new file mode 100644 index 000000000..e69de29bb diff --git a/e2e-tests/playwright/java/src/test/resources/junit-platform.properties b/e2e-tests/playwright/java/src/test/resources/junit-platform.properties new file mode 100644 index 000000000..4570caf06 --- /dev/null +++ b/e2e-tests/playwright/java/src/test/resources/junit-platform.properties @@ -0,0 +1,5 @@ +junit.jupiter.execution.parallel.enabled = true +junit.jupiter.execution.parallel.mode.default = same_thread +junit.jupiter.execution.parallel.mode.classes.default = concurrent +junit.jupiter.execution.parallel.config.strategy=dynamic +junit.jupiter.execution.parallel.config.dynamic.factor=0.5 \ No newline at end of file diff --git a/e2e-tests/playwright/tests/global-setup.ts b/e2e-tests/playwright/tests/global-setup.ts deleted file mode 100644 index 1581a9aea..000000000 --- a/e2e-tests/playwright/tests/global-setup.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { FullConfig } from '@playwright/test'; - -async function globalSetup(config: FullConfig) { - process.env.EMAIL = 'bibi@bloxberg.de'; - process.env.PASSWORD = 'Aa12345_'; - process.env.GMS_ACTIVE = false; - process.env.HUMHUB_ACTIVE = false; -} - -export default globalSetup; diff --git a/e2e-tests/playwright/tests/gradido_login.spec.ts b/e2e-tests/playwright/tests/gradido_login.spec.ts deleted file mode 100644 index 0853780d1..000000000 --- a/e2e-tests/playwright/tests/gradido_login.spec.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { test, expect } from '@playwright/test'; -import { LoginPage } from './models/login_page'; -import { WelcomePage } from './models/welcome_page'; - - -test('Gradido login test (happy path)', async ({ page }) => { - const { EMAIL, PASSWORD } = process.env; - const loginPage = new LoginPage(page); - await loginPage.goto(); - await loginPage.enterEmail(EMAIL); - await loginPage.enterPassword(PASSWORD); - await loginPage.submitLogin(); - // assertions - await expect(page).toHaveURL('./overview'); -}); diff --git a/e2e-tests/playwright/tests/models/login_page.ts b/e2e-tests/playwright/tests/models/login_page.ts deleted file mode 100644 index 50abb4933..000000000 --- a/e2e-tests/playwright/tests/models/login_page.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { expect, test, Locator, Page } from '@playwright/test'; - -export class LoginPage { - readonly page: Page; - readonly url: string; - readonly emailInput: Locator; - readonly passwordInput: Locator; - readonly submitBtn: Locator; - - constructor(page: Page) { - this.page = page; - this.url = './login'; - this.emailInput = page.locator('id=Email-input-field'); - this.passwordInput = page.locator('id=Password-input-field'); - this.submitBtn = page.locator('text=Login'); - } - - async goto() { - await this.page.goto(this.url); - } - - async enterEmail(email: string) { - await this.emailInput.fill(email); - } - - async enterPassword(password: string) { - await this.passwordInput.fill(password); - } - - async submitLogin() { - await this.submitBtn.click(); - } -} diff --git a/e2e-tests/playwright/tests/models/welcome_page.ts b/e2e-tests/playwright/tests/models/welcome_page.ts deleted file mode 100644 index 81d73a771..000000000 --- a/e2e-tests/playwright/tests/models/welcome_page.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { expect, Locator, Page } from '@playwright/test'; - -export class WelcomePage { - readonly page: Page; - readonly url: string; - readonly profileLink: Locator; - - constructor(page: Page){ - this.page = page; - this.url = './overview'; - this.profileLink = page.locator('href=/profile'); - } -} diff --git a/e2e-tests/playwright/tests/playwright.config.ts b/e2e-tests/playwright/tests/playwright.config.ts deleted file mode 100644 index 13ea41d89..000000000 --- a/e2e-tests/playwright/tests/playwright.config.ts +++ /dev/null @@ -1,21 +0,0 @@ -import type { PlaywrightTestConfig } from '@playwright/test'; - -const config: PlaywrightTestConfig = { - globalSetup: require.resolve('./global-setup'), - ignoreHTTPSErrors: true, - locale: 'de-DE', - reporter: process.env.CI ? 'github' : 'list', - retries: 1, - screenshot: 'only-on-failure', - testDir: '.', - timeout: 30000, - trace: 'on-first-retry', - video: 'never', - viewport: { width: 1280, height: 720 }, - use: { - baseURL: process.env.URL || 'http://127.0.0.1:3000', - browserName: 'webkit', - - }, -}; -export default config; diff --git a/e2e-tests/playwright/typescript/.gitignore b/e2e-tests/playwright/typescript/.gitignore new file mode 100644 index 000000000..58786aac7 --- /dev/null +++ b/e2e-tests/playwright/typescript/.gitignore @@ -0,0 +1,7 @@ + +# Playwright +node_modules/ +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ diff --git a/e2e-tests/playwright/typescript/components/Toasts.ts b/e2e-tests/playwright/typescript/components/Toasts.ts new file mode 100644 index 000000000..2cfc64f16 --- /dev/null +++ b/e2e-tests/playwright/typescript/components/Toasts.ts @@ -0,0 +1,22 @@ +import type { Locator, Page } from '@playwright/test' +import { expect } from '@playwright/test' + +export class Toasts { + public readonly toastSlot: Locator + public readonly toastTypeError: Locator + public readonly toastTitle: Locator + public readonly toastMessage: Locator + + constructor(page: Page) { + this.toastSlot = page.locator('#__BVID__toaster-container') + this.toastTypeError = this.toastSlot.locator('.toast.text-bg-danger') + this.toastTitle = this.toastTypeError.locator('.gdd-toaster-title') + this.toastMessage = this.toastTypeError.locator('.gdd-toaster-body') + } + + async assertErrorToastVisible(): Promise { + await this.toastTypeError.waitFor({ state: 'visible' }) + expect(this.toastTitle).toBeVisible() + expect(this.toastMessage).toBeVisible() + } +} diff --git a/e2e-tests/playwright/typescript/config/index.ts b/e2e-tests/playwright/typescript/config/index.ts new file mode 100644 index 000000000..0f62c6739 --- /dev/null +++ b/e2e-tests/playwright/typescript/config/index.ts @@ -0,0 +1 @@ +export const FRONTEND_URL = "http://localhost:3000" \ No newline at end of file diff --git a/e2e-tests/playwright/typescript/package.json b/e2e-tests/playwright/typescript/package.json new file mode 100644 index 000000000..66b8c8806 --- /dev/null +++ b/e2e-tests/playwright/typescript/package.json @@ -0,0 +1,13 @@ +{ + "name": "typescript", + "version": "1.0.0", + "main": "index.js", + "license": "MIT", + "devDependencies": { + "@playwright/test": "^1.53.1", + "@types/node": "^24.0.7" + }, + "scripts": { + "test": "playwright test --reporter=list" + } +} diff --git a/e2e-tests/playwright/typescript/pages/LoginPage.ts b/e2e-tests/playwright/typescript/pages/LoginPage.ts new file mode 100644 index 000000000..8a10c05c3 --- /dev/null +++ b/e2e-tests/playwright/typescript/pages/LoginPage.ts @@ -0,0 +1,28 @@ +import type { Locator, Page } from '@playwright/test' +import { FRONTEND_URL } from '../config' + +export class LoginPage { + readonly page: Page + readonly emailInput: Locator + readonly passwordInput: Locator + readonly submitButton: Locator + + constructor(page: Page) { + this.page = page + this.emailInput = page.locator('#email-input-field') + this.passwordInput = page.locator('#password-input-field') + this.submitButton = page.locator("button[type='submit']") + } + + async goto() { + await this.page.goto(`${FRONTEND_URL}/login`) + } + + async login(email: string, password: string) { + await this.emailInput.fill(email) + await this.passwordInput.fill(password) + const responsePromise = this.page.waitForResponse('**/graphql') + await this.submitButton.click() + await responsePromise + } +} \ No newline at end of file diff --git a/e2e-tests/playwright/typescript/playwright.config.ts b/e2e-tests/playwright/typescript/playwright.config.ts new file mode 100644 index 000000000..1418b5af9 --- /dev/null +++ b/e2e-tests/playwright/typescript/playwright.config.ts @@ -0,0 +1,82 @@ +import { defineConfig, devices } from '@playwright/test'; + +/** + * Read environment variables from file. + * https://github.com/motdotla/dotenv + */ +// import dotenv from 'dotenv'; +// import path from 'path'; +// dotenv.config({ path: path.resolve(__dirname, '.env') }); + +/** + * See https://playwright.dev/docs/test-configuration. + */ +export default defineConfig({ + testDir: './tests', + // Glob patterns or regular expressions that match test files. + testMatch: '*', + /* Run tests in files in parallel */ + fullyParallel: true, + /* Fail the build on CI if you accidentally left test.only in the source code. */ + forbidOnly: !!process.env.CI, + /* Retry on CI only */ + retries: 2, + /* Opt out of parallel tests on CI. */ + workers: process.env.CI ? '90%' : '75%', + /* Reporter to use. See https://playwright.dev/docs/test-reporters */ + reporter: 'html', + /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ + use: { + /* Base URL to use in actions like `await page.goto('/')`. */ + // baseURL: 'http://localhost:3000', + + /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ + trace: 'on-first-retry', + video: 'on-first-retry', + }, + + /* Configure projects for major browsers */ + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + + { + name: 'firefox', + use: { ...devices['Desktop Firefox'] }, + }, + + { + name: 'webkit', + use: { ...devices['Desktop Safari'] }, + }, + + /* Test against mobile viewports. */ + // { + // name: 'Mobile Chrome', + // use: { ...devices['Pixel 5'] }, + // }, + // { + // name: 'Mobile Safari', + // use: { ...devices['iPhone 12'] }, + // }, + + /* Test against branded browsers. */ + // { + // name: 'Microsoft Edge', + // use: { ...devices['Desktop Edge'], channel: 'msedge' }, + // }, + { + name: 'Google Chrome', + use: { ...devices['Desktop Chrome'], channel: 'chrome' }, + }, + ], + + /* Run your local dev server before starting the tests */ + // webServer: { + // command: 'npm run start', + // url: 'http://localhost:3000', + // reuseExistingServer: !process.env.CI, + // }, +}); diff --git a/e2e-tests/playwright/typescript/tests/InvalidLoginTest.ts b/e2e-tests/playwright/typescript/tests/InvalidLoginTest.ts new file mode 100644 index 000000000..23747030c --- /dev/null +++ b/e2e-tests/playwright/typescript/tests/InvalidLoginTest.ts @@ -0,0 +1,13 @@ +import { test, expect } from '@playwright/test' +import { LoginPage } from '../pages/LoginPage' +import { Toasts } from '../components/Toasts' + +test('invalid login', async ({ page }) => { + const loginPage = new LoginPage(page) + await loginPage.goto() + await loginPage.login('peter@lustig.de', 'wrongpass') + + const toast = new Toasts(page) + await toast.assertErrorToastVisible() + await page.waitForTimeout(50) +}) \ No newline at end of file diff --git a/e2e-tests/playwright/typescript/tests/ValidLoginTest.ts b/e2e-tests/playwright/typescript/tests/ValidLoginTest.ts new file mode 100644 index 000000000..3380ba3c8 --- /dev/null +++ b/e2e-tests/playwright/typescript/tests/ValidLoginTest.ts @@ -0,0 +1,11 @@ +import { test, expect } from '@playwright/test' +import { LoginPage } from '../pages/LoginPage' +import { FRONTEND_URL } from '../config' + +test('valid login', async ({ page }) => { + const loginPage = new LoginPage(page) + await loginPage.goto() + await loginPage.login('peter@lustig.de', 'Aa12345_') + await page.waitForURL(`${FRONTEND_URL}/overview`) + expect(page.url()).toContain('/overview') +}) \ No newline at end of file diff --git a/e2e-tests/playwright/typescript/yarn.lock b/e2e-tests/playwright/typescript/yarn.lock new file mode 100644 index 000000000..0cd9b4a0a --- /dev/null +++ b/e2e-tests/playwright/typescript/yarn.lock @@ -0,0 +1,41 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@playwright/test@^1.53.1": + version "1.53.1" + resolved "https://registry.yarnpkg.com/@playwright/test/-/test-1.53.1.tgz#3ad5a2ce334b4a78390fd91e0a9d8423c09bc808" + integrity sha512-Z4c23LHV0muZ8hfv4jw6HngPJkbbtZxTkxPNIg7cJcTc9C28N/p2q7g3JZS2SiKBBHJ3uM1dgDye66bB7LEk5w== + dependencies: + playwright "1.53.1" + +"@types/node@^24.0.7": + version "24.0.7" + resolved "https://registry.yarnpkg.com/@types/node/-/node-24.0.7.tgz#ee580f7850c7eabaeef61ef96b8d8c04fdf94f53" + integrity sha512-YIEUUr4yf8q8oQoXPpSlnvKNVKDQlPMWrmOcgzoduo7kvA2UF0/BwJ/eMKFTiTtkNL17I0M6Xe2tvwFU7be6iw== + dependencies: + undici-types "~7.8.0" + +fsevents@2.3.2: + version "2.3.2" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" + integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== + +playwright-core@1.53.1: + version "1.53.1" + resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.53.1.tgz#0b6f7a2006ccb6126ffcc3e3b2fa9efda23b6638" + integrity sha512-Z46Oq7tLAyT0lGoFx4DOuB1IA9D1TPj0QkYxpPVUnGDqHHvDpCftu1J2hM2PiWsNMoZh8+LQaarAWcDfPBc6zg== + +playwright@1.53.1: + version "1.53.1" + resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.53.1.tgz#86fb041b237a6868d163c87c4b9737fd1cac145e" + integrity sha512-LJ13YLr/ocweuwxyGf1XNFWIU4M2zUSo149Qbp+A4cpwDjsxRPj7k6H25LBrEHiEwxvRbD8HdwvQmRMSvquhYw== + dependencies: + playwright-core "1.53.1" + optionalDependencies: + fsevents "2.3.2" + +undici-types@~7.8.0: + version "7.8.0" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-7.8.0.tgz#de00b85b710c54122e44fbfd911f8d70174cd294" + integrity sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw== diff --git a/federation/package.json b/federation/package.json index f2bae7d73..0ca1abac9 100644 --- a/federation/package.json +++ b/federation/package.json @@ -18,7 +18,8 @@ "test:debug": "cross-env TZ=UTC NODE_ENV=development DB_DATABASE=gradido_test_federation node --inspect-brk node_modules/.bin/jest --bail --runInBand --forceExit --detectOpenHandles", "test:coverage": "cross-env TZ=UTC NODE_ENV=development DB_DATABASE=gradido_test_federation jest --coverage --runInBand --forceExit --detectOpenHandles", "lint": "biome check --error-on-warnings .", - "lint:fix": "biome check --error-on-warnings . --write" + "lint:fix": "biome check --error-on-warnings . --write", + "clear": "rm -rf node_modules && rm -rf build && rm -rf .turbo" }, "dependencies": { "cross-env": "^7.0.3", @@ -41,6 +42,7 @@ "await-semaphore": "0.1.3", "class-validator": "^0.13.2", "config-schema": "*", + "core": "*", "cors": "2.8.5", "database": "*", "decimal.js-light": "^2.5.1", diff --git a/federation/src/client/1_0/AuthenticationClient.ts b/federation/src/client/1_0/AuthenticationClient.ts index 85daa706f..de7fa24bd 100644 --- a/federation/src/client/1_0/AuthenticationClient.ts +++ b/federation/src/client/1_0/AuthenticationClient.ts @@ -1,15 +1,12 @@ import { FederatedCommunity as DbFederatedCommunity } from 'database' import { GraphQLClient } from 'graphql-request' -import { getLogger } from 'log4js' +import { getLogger, Logger } from 'log4js' import { LOG4JS_BASE_CATEGORY_NAME } from '@/config/const' -import { AuthenticationArgs } from '@/graphql/api/1_0/model/AuthenticationArgs' -import { OpenConnectionCallbackArgs } from '@/graphql/api/1_0/model/OpenConnectionCallbackArgs' +import { EncryptedTransferArgs } from 'core/src/graphql/model/EncryptedTransferArgs' import { authenticate } from './query/authenticate' import { openConnectionCallback } from './query/openConnectionCallback' -const logger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.client.1_0.AuthenticationClient`) - export class AuthenticationClient { dbCom: DbFederatedCommunity endpoint: string @@ -29,36 +26,41 @@ export class AuthenticationClient { }) } - async openConnectionCallback(args: OpenConnectionCallbackArgs): Promise { - logger.debug('openConnectionCallback with endpoint', this.endpoint, args) + async openConnectionCallback(args: EncryptedTransferArgs): Promise { + const methodLogger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.client.1_0.AuthenticationClient.openConnectionCallback`) + methodLogger.addContext('handshakeID', args.handshakeID) + methodLogger.debug('openConnectionCallback with endpoint', this.endpoint, args) try { const { data } = await this.client.rawRequest(openConnectionCallback, { args }) + methodLogger.debug('after openConnectionCallback: data:', data) - if (data && data.openConnectionCallback) { - logger.warn('openConnectionCallback without response data from endpoint', this.endpoint) + if (!data || !data.openConnectionCallback) { + methodLogger.warn('openConnectionCallback without response data from endpoint', this.endpoint) return false } - logger.debug('openConnectionCallback successfully started with endpoint', this.endpoint) + methodLogger.debug('openConnectionCallback successfully started with endpoint', this.endpoint) return true } catch (err) { - logger.error('error on openConnectionCallback', err) + methodLogger.error('error on openConnectionCallback', err) } return false } - async authenticate(args: AuthenticationArgs): Promise { - logger.debug('authenticate with endpoint=', this.endpoint) + async authenticate(args: EncryptedTransferArgs): Promise { + const methodLogger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.client.1_0.AuthenticationClient.authenticate`) + methodLogger.addContext('handshakeID', args.handshakeID) + methodLogger.debug('authenticate with endpoint=', this.endpoint) try { const { data } = await this.client.rawRequest(authenticate, { args }) - logger.debug('after authenticate: data:', data) + methodLogger.debug('after authenticate: data:', data) const authUuid: string = data?.authenticate if (authUuid) { - logger.debug('received authenticated uuid', authUuid) + methodLogger.debug('received authenticated uuid', authUuid) return authUuid } } catch (err) { - logger.error('authenticate failed', { + methodLogger.error('authenticate failed', { endpoint: this.endpoint, err, }) diff --git a/federation/src/client/1_0/query/authenticate.ts b/federation/src/client/1_0/query/authenticate.ts index 843d8b78b..fb68a0189 100644 --- a/federation/src/client/1_0/query/authenticate.ts +++ b/federation/src/client/1_0/query/authenticate.ts @@ -1,7 +1,7 @@ import { gql } from 'graphql-request' export const authenticate = gql` - mutation ($args: AuthenticationArgs!) { + mutation ($args: EncryptedTransferArgs!) { authenticate(data: $args) } ` diff --git a/federation/src/client/1_0/query/openConnectionCallback.ts b/federation/src/client/1_0/query/openConnectionCallback.ts index ba026e610..380e3daed 100644 --- a/federation/src/client/1_0/query/openConnectionCallback.ts +++ b/federation/src/client/1_0/query/openConnectionCallback.ts @@ -1,7 +1,7 @@ import { gql } from 'graphql-request' export const openConnectionCallback = gql` - mutation ($args: OpenConnectionCallbackArgs!) { + mutation ($args: EncryptedTransferArgs!) { openConnectionCallback(data: $args) } ` diff --git a/federation/src/graphql/api/1_0/logger/GetPublicCommunityInfoResultLogging.view.ts b/federation/src/graphql/api/1_0/logger/GetPublicCommunityInfoResultLogging.view.ts index 77224fabc..3eb7fd208 100644 --- a/federation/src/graphql/api/1_0/logger/GetPublicCommunityInfoResultLogging.view.ts +++ b/federation/src/graphql/api/1_0/logger/GetPublicCommunityInfoResultLogging.view.ts @@ -12,6 +12,7 @@ export class GetPublicCommunityInfoResultLoggingView extends AbstractLoggingView description: this.self.description, creationDate: this.dateToString(this.self.creationDate), publicKey: this.self.publicKey, + publicJwtKey: this.self.publicJwtKey, } } } diff --git a/federation/src/graphql/api/1_0/model/GetPublicCommunityInfoResult.ts b/federation/src/graphql/api/1_0/model/GetPublicCommunityInfoResult.ts index edcdccb91..55292cee2 100644 --- a/federation/src/graphql/api/1_0/model/GetPublicCommunityInfoResult.ts +++ b/federation/src/graphql/api/1_0/model/GetPublicCommunityInfoResult.ts @@ -6,6 +6,9 @@ import { Field, ObjectType } from 'type-graphql' export class GetPublicCommunityInfoResult { constructor(dbCom: DbCommunity) { this.publicKey = dbCom.publicKey.toString('hex') + if (dbCom.publicJwtKey) { + this.publicJwtKey = dbCom.publicJwtKey + } this.name = dbCom.name this.description = dbCom.description this.creationDate = dbCom.creationDate @@ -22,4 +25,7 @@ export class GetPublicCommunityInfoResult { @Field(() => String) publicKey: string + + @Field(() => String) + publicJwtKey: string } diff --git a/federation/src/graphql/api/1_0/model/OpenConnectionCallbackArgs.ts b/federation/src/graphql/api/1_0/model/OpenConnectionCallbackArgs.ts deleted file mode 100644 index 461f6c3d7..000000000 --- a/federation/src/graphql/api/1_0/model/OpenConnectionCallbackArgs.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { Field, InputType } from 'type-graphql' - -@InputType() -export class OpenConnectionCallbackArgs { - @Field(() => String) - oneTimeCode: string - - @Field(() => String) - url: string -} diff --git a/federation/src/graphql/api/1_0/resolver/AuthenticationResolver.ts b/federation/src/graphql/api/1_0/resolver/AuthenticationResolver.ts index 73f0f2aec..bfec30bc5 100644 --- a/federation/src/graphql/api/1_0/resolver/AuthenticationResolver.ts +++ b/federation/src/graphql/api/1_0/resolver/AuthenticationResolver.ts @@ -1,17 +1,16 @@ import { CONFIG } from '@/config' -import { LogError } from '@/server/LogError' +import { LOG4JS_BASE_CATEGORY_NAME } from '@/config/const' +import { EncryptedTransferArgs, interpretEncryptedTransferArgs } from 'core' import { CommunityLoggingView, Community as DbCommunity, FederatedCommunity as DbFedCommunity, FederatedCommunityLoggingView, + getHomeCommunity, } from 'database' import { getLogger } from 'log4js' +import { AuthenticationJwtPayloadType, AuthenticationResponseJwtPayloadType, encryptAndSign, OpenConnectionCallbackJwtPayloadType, OpenConnectionJwtPayloadType } from 'shared' import { Arg, Mutation, Resolver } from 'type-graphql' -import { LOG4JS_BASE_CATEGORY_NAME } from '@/config/const' -import { AuthenticationArgs } from '../model/AuthenticationArgs' -import { OpenConnectionArgs } from '../model/OpenConnectionArgs' -import { OpenConnectionCallbackArgs } from '../model/OpenConnectionCallbackArgs' import { startAuthentication, startOpenConnectionCallback } from '../util/authenticateCommunity' const logger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.graphql.api.1_0.resolver.AuthenticationResolver`) @@ -21,67 +20,118 @@ export class AuthenticationResolver { @Mutation(() => Boolean) async openConnection( @Arg('data') - args: OpenConnectionArgs, + args: EncryptedTransferArgs, ): Promise { - const pubKeyBuf = Buffer.from(args.publicKey, 'hex') - logger.debug(`openConnection() via apiVersion=1_0:`, args) - - // first find with args.publicKey the community 'comA', which starts openConnection request - const comA = await DbCommunity.findOneBy({ - publicKey: pubKeyBuf, // Buffer.from(args.publicKey), - }) - if (!comA) { - throw new LogError(`unknown requesting community with publicKey`, pubKeyBuf.toString('hex')) + const methodLogger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.graphql.api.1_0.resolver.AuthenticationResolver.openConnection`) + methodLogger.addContext('handshakeID', args.handshakeID) + methodLogger.debug(`openConnection() via apiVersion=1_0:`, args) + const openConnectionJwtPayload = await interpretEncryptedTransferArgs(args) as OpenConnectionJwtPayloadType + methodLogger.debug('openConnectionJwtPayload', openConnectionJwtPayload) + if (!openConnectionJwtPayload) { + const errmsg = `invalid OpenConnection payload of requesting community with publicKey` + args.publicKey + methodLogger.error(errmsg) + methodLogger.removeContext('handshakeID') + throw new Error(errmsg) } - logger.debug(`found requestedCom:`, new CommunityLoggingView(comA)) - // biome-ignore lint/complexity/noVoid: no await to respond immediately and invoke callback-request asynchronously - void startOpenConnectionCallback(args, comA, CONFIG.FEDERATION_API) + if (openConnectionJwtPayload.tokentype !== OpenConnectionJwtPayloadType.OPEN_CONNECTION_TYPE) { + const errmsg = `invalid tokentype of community with publicKey` + args.publicKey + methodLogger.error(errmsg) + methodLogger.removeContext('handshakeID') + throw new Error(errmsg) + } + if (!openConnectionJwtPayload.url) { + const errmsg = `invalid url of community with publicKey` + args.publicKey + methodLogger.error(errmsg) + methodLogger.removeContext('handshakeID') + throw new Error(errmsg) + } + methodLogger.debug(`vor DbFedCommunity.findOneByOrFail()...`, { publicKey: args.publicKey }) + const fedComA = await DbFedCommunity.findOneByOrFail({ publicKey: Buffer.from(args.publicKey, 'hex') }) + methodLogger.debug(`nach DbFedCommunity.findOneByOrFail()...`, fedComA) + methodLogger.debug('fedComA', new FederatedCommunityLoggingView(fedComA)) + if (!openConnectionJwtPayload.url.startsWith(fedComA.endPoint)) { + const errmsg = `invalid url of community with publicKey` + args.publicKey + methodLogger.error(errmsg) + methodLogger.removeContext('handshakeID') + throw new Error(errmsg) + } + + // no await to respond immediately and invoke callback-request asynchronously + void startOpenConnectionCallback(args.handshakeID, args.publicKey, CONFIG.FEDERATION_API) + methodLogger.debug('openConnection() successfully initiated callback and returns true immediately...') + methodLogger.removeContext('handshakeID') return true } @Mutation(() => Boolean) async openConnectionCallback( @Arg('data') - args: OpenConnectionCallbackArgs, + args: EncryptedTransferArgs, ): Promise { - logger.debug(`openConnectionCallback() via apiVersion=1_0 ...`, args) - // TODO decrypt args.url with homeCom.privateKey and verify signing with callbackFedCom.publicKey - const endPoint = args.url.slice(0, args.url.lastIndexOf('/') + 1) - const apiVersion = args.url.slice(args.url.lastIndexOf('/') + 1, args.url.length) - logger.debug(`search fedComB per:`, endPoint, apiVersion) + const methodLogger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.graphql.api.1_0.resolver.AuthenticationResolver.openConnectionCallback`) + methodLogger.addContext('handshakeID', args.handshakeID) + methodLogger.debug(`openConnectionCallback() via apiVersion=1_0 ...`, args) + // decrypt args.url with homeCom.privateJwtKey and verify signing with callbackFedCom.publicKey + const openConnectionCallbackJwtPayload = await interpretEncryptedTransferArgs(args) as OpenConnectionCallbackJwtPayloadType + if (!openConnectionCallbackJwtPayload) { + const errmsg = `invalid OpenConnectionCallback payload of requesting community with publicKey` + args.publicKey + methodLogger.error(errmsg) + methodLogger.removeContext('handshakeID') + throw new Error(errmsg) + } + + const endPoint = openConnectionCallbackJwtPayload.url.slice(0, openConnectionCallbackJwtPayload.url.lastIndexOf('/') + 1) + const apiVersion = openConnectionCallbackJwtPayload.url.slice(openConnectionCallbackJwtPayload.url.lastIndexOf('/') + 1, openConnectionCallbackJwtPayload.url.length) + methodLogger.debug(`search fedComB per:`, endPoint, apiVersion) const fedComB = await DbFedCommunity.findOneBy({ endPoint, apiVersion }) if (!fedComB) { - throw new LogError(`unknown callback community with url`, args.url) + const errmsg = `unknown callback community with url` + openConnectionCallbackJwtPayload.url + methodLogger.error(errmsg) + methodLogger.removeContext('handshakeID') + throw new Error(errmsg) } - logger.debug( + methodLogger.debug( `found fedComB and start authentication:`, new FederatedCommunityLoggingView(fedComB), ) - // biome-ignore lint/complexity/noVoid: no await to respond immediately and invoke authenticate-request asynchronously - void startAuthentication(args.oneTimeCode, fedComB) + // no await to respond immediately and invoke authenticate-request asynchronously + void startAuthentication(args.handshakeID, openConnectionCallbackJwtPayload.oneTimeCode, fedComB) + methodLogger.debug('openConnectionCallback() successfully initiated authentication and returns true immediately...') + methodLogger.removeContext('handshakeID') return true } @Mutation(() => String) async authenticate( @Arg('data') - args: AuthenticationArgs, + args: EncryptedTransferArgs, ): Promise { - logger.debug(`authenticate() via apiVersion=1_0 ...`, args) - const authCom = await DbCommunity.findOneByOrFail({ communityUuid: args.oneTimeCode }) - logger.debug('found authCom:', new CommunityLoggingView(authCom)) + const methodLogger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.graphql.api.1_0.resolver.AuthenticationResolver.authenticate`) + methodLogger.addContext('handshakeID', args.handshakeID) + methodLogger.debug(`authenticate() via apiVersion=1_0 ...`, args) + const authArgs = await interpretEncryptedTransferArgs(args) as AuthenticationJwtPayloadType + if (!authArgs) { + const errmsg = `invalid authentication payload of requesting community with publicKey` + args.publicKey + methodLogger.error(errmsg) + methodLogger.removeContext('handshakeID') + throw new Error(errmsg) + } + const authCom = await DbCommunity.findOneByOrFail({ communityUuid: authArgs.oneTimeCode }) + methodLogger.debug('found authCom:', new CommunityLoggingView(authCom)) if (authCom) { - // TODO decrypt args.uuid with authCom.publicKey - authCom.communityUuid = args.uuid + authCom.communityUuid = authArgs.uuid authCom.authenticatedAt = new Date() await DbCommunity.save(authCom) - logger.debug('store authCom.uuid successfully:', new CommunityLoggingView(authCom)) - const homeCom = await DbCommunity.findOneByOrFail({ foreign: false }) - // TODO encrypt homeCom.uuid with homeCom.privateKey - if (homeCom.communityUuid) { - return homeCom.communityUuid + methodLogger.debug('store authCom.uuid successfully:', new CommunityLoggingView(authCom)) + const homeComB = await getHomeCommunity() + if (homeComB?.communityUuid) { + const responseArgs = new AuthenticationResponseJwtPayloadType(args.handshakeID,homeComB.communityUuid) + const responseJwt = await encryptAndSign(responseArgs, homeComB.privateJwtKey!, authCom.publicJwtKey!) + methodLogger.removeContext('handshakeID') + return responseJwt } } + methodLogger.removeContext('handshakeID') return null } } diff --git a/federation/src/graphql/api/1_0/util/authenticateCommunity.ts b/federation/src/graphql/api/1_0/util/authenticateCommunity.ts index 3f89dd5c4..c51546c15 100644 --- a/federation/src/graphql/api/1_0/util/authenticateCommunity.ts +++ b/federation/src/graphql/api/1_0/util/authenticateCommunity.ts @@ -1,46 +1,50 @@ +import { EncryptedTransferArgs } from 'core' import { CommunityLoggingView, Community as DbCommunity, FederatedCommunity as DbFedCommunity, FederatedCommunityLoggingView, + getHomeCommunity, } from 'database' import { getLogger } from 'log4js' -import { OpenConnectionArgs } from '../model/OpenConnectionArgs' -import { OpenConnectionCallbackArgs } from '../model/OpenConnectionCallbackArgs' +import { validate as validateUUID, version as versionUUID } from 'uuid' import { AuthenticationClientFactory } from '@/client/AuthenticationClientFactory' import { randombytes_random } from 'sodium-native' import { AuthenticationClient as V1_0_AuthenticationClient } from '@/client/1_0/AuthenticationClient' import { LOG4JS_BASE_CATEGORY_NAME } from '@/config/const' -import { AuthenticationArgs } from '../model/AuthenticationArgs' +import { AuthenticationJwtPayloadType, AuthenticationResponseJwtPayloadType, encryptAndSign, OpenConnectionCallbackJwtPayloadType, verifyAndDecrypt } from 'shared' const logger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.graphql.api.1_0.util.authenticateCommunity`) export async function startOpenConnectionCallback( - args: OpenConnectionArgs, - comA: DbCommunity, + handshakeID: string, + publicKey: string, api: string, ): Promise { - logger.debug(`startOpenConnectionCallback() with:`, { - args, - comA: new CommunityLoggingView(comA), + const methodLogger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.graphql.api.1_0.util.authenticateCommunity.startOpenConnectionCallback`) + methodLogger.addContext('handshakeID', handshakeID) + methodLogger.debug(`Authentication: startOpenConnectionCallback() with:`, { + publicKey, }) try { - const homeFedCom = await DbFedCommunity.findOneByOrFail({ + const homeComB = await getHomeCommunity() + const homeFedComB = await DbFedCommunity.findOneByOrFail({ foreign: false, apiVersion: api, }) + const comA = await DbCommunity.findOneByOrFail({ publicKey: Buffer.from(publicKey, 'hex') }) const fedComA = await DbFedCommunity.findOneByOrFail({ foreign: true, apiVersion: api, publicKey: comA.publicKey, }) - const oneTimeCode = randombytes_random() + const oneTimeCode = randombytes_random().toString() // store oneTimeCode in requestedCom.community_uuid as authenticate-request-identifier - comA.communityUuid = oneTimeCode.toString() + comA.communityUuid = oneTimeCode await DbCommunity.save(comA) - logger.debug( + methodLogger.debug( `Authentication: stored oneTimeCode in requestedCom:`, new CommunityLoggingView(comA), ) @@ -48,68 +52,94 @@ export async function startOpenConnectionCallback( const client = AuthenticationClientFactory.getInstance(fedComA) if (client instanceof V1_0_AuthenticationClient) { - const callbackArgs = new OpenConnectionCallbackArgs() - callbackArgs.oneTimeCode = oneTimeCode.toString() - // TODO encrypt callbackArgs.url with requestedCom.publicKey and sign it with homeCom.privateKey - callbackArgs.url = homeFedCom.endPoint.endsWith('/') - ? homeFedCom.endPoint + homeFedCom.apiVersion - : homeFedCom.endPoint + '/' + homeFedCom.apiVersion - logger.debug(`Authentication: start openConnectionCallback with args:`, callbackArgs) - if (await client.openConnectionCallback(callbackArgs)) { - logger.debug('startOpenConnectionCallback() successful:', callbackArgs) + const url = homeFedComB.endPoint.endsWith('/') + ? homeFedComB.endPoint + homeFedComB.apiVersion + : homeFedComB.endPoint + '/' + homeFedComB.apiVersion + + const callbackArgs = new OpenConnectionCallbackJwtPayloadType(handshakeID, oneTimeCode, url) + methodLogger.debug(`Authentication: start openConnectionCallback with args:`, callbackArgs) + // encrypt callbackArgs with requestedCom.publicJwtKey and sign it with homeCom.privateJwtKey + const jwt = await encryptAndSign(callbackArgs, homeComB!.privateJwtKey!, comA.publicJwtKey!) + const args = new EncryptedTransferArgs() + args.publicKey = homeComB!.publicKey.toString('hex') + args.jwt = jwt + args.handshakeID = handshakeID + const result = await client.openConnectionCallback(args) + if (result) { + methodLogger.debug('startOpenConnectionCallback() successful:', jwt) } else { - logger.error('startOpenConnectionCallback() failed:', callbackArgs) + methodLogger.error('startOpenConnectionCallback() failed:', jwt) } } } catch (err) { - logger.error('error in startOpenConnectionCallback:', err) + methodLogger.error('error in startOpenConnectionCallback:', err) } + methodLogger.removeContext('handshakeID') } export async function startAuthentication( + handshakeID: string, oneTimeCode: string, fedComB: DbFedCommunity, ): Promise { - logger.debug(`startAuthentication()...`, { + const methodLogger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.graphql.api.1_0.util.authenticateCommunity.startAuthentication`) + methodLogger.addContext('handshakeID', handshakeID) + methodLogger.debug(`startAuthentication()...`, { oneTimeCode, fedComB: new FederatedCommunityLoggingView(fedComB), }) try { - const homeCom = await DbCommunity.findOneByOrFail({ foreign: false }) + const homeComA = await getHomeCommunity() + const comB = await DbCommunity.findOneByOrFail({ + foreign: true, + publicKey: fedComB.publicKey, + }) + if (!comB.publicJwtKey) { + throw new Error('Public JWT key still not exist for foreign community') + } - // TODO encrypt homeCom.uuid with homeCom.privateKey and sign it with callbackFedCom.publicKey const client = AuthenticationClientFactory.getInstance(fedComB) if (client instanceof V1_0_AuthenticationClient) { - const authenticationArgs = new AuthenticationArgs() - authenticationArgs.oneTimeCode = oneTimeCode - // TODO encrypt callbackArgs.url with requestedCom.publicKey and sign it with homeCom.privateKey - if (homeCom.communityUuid) { - authenticationArgs.uuid = homeCom.communityUuid - } - logger.debug(`invoke authenticate() with:`, authenticationArgs) - const fedComUuid = await client.authenticate(authenticationArgs) - logger.debug(`response of authenticate():`, fedComUuid) - if (fedComUuid !== null) { - logger.debug( - `received communityUUid for callbackFedCom:`, - fedComUuid, + const authenticationArgs = new AuthenticationJwtPayloadType(handshakeID, oneTimeCode, homeComA!.communityUuid!) + // encrypt authenticationArgs.uuid with fedComB.publicJwtKey and sign it with homeCom.privateJwtKey + const jwt = await encryptAndSign(authenticationArgs, homeComA!.privateJwtKey!, comB.publicJwtKey!) + const args = new EncryptedTransferArgs() + args.publicKey = homeComA!.publicKey.toString('hex') + args.jwt = jwt + args.handshakeID = handshakeID + methodLogger.debug(`invoke authenticate() with:`, args) + const responseJwt = await client.authenticate(args) + methodLogger.debug(`response of authenticate():`, responseJwt) + if (responseJwt !== null) { + const payload = await verifyAndDecrypt(handshakeID, responseJwt, homeComA!.privateJwtKey!, comB.publicJwtKey!) as AuthenticationResponseJwtPayloadType + methodLogger.debug( + `received payload from authenticate ComB:`, + payload, new FederatedCommunityLoggingView(fedComB), ) - const callbackCom = await DbCommunity.findOneByOrFail({ - foreign: true, - publicKey: fedComB.publicKey, - }) - // TODO decrypt fedComUuid with callbackFedCom.publicKey - callbackCom.communityUuid = fedComUuid - callbackCom.authenticatedAt = new Date() - await DbCommunity.save(callbackCom) - logger.debug('Community Authentication successful:', new CommunityLoggingView(callbackCom)) + if (payload.tokentype !== AuthenticationResponseJwtPayloadType.AUTHENTICATION_RESPONSE_TYPE) { + const errmsg = `Invalid tokentype in authenticate-response of community with publicKey` + comB.publicKey + methodLogger.error(errmsg) + methodLogger.removeContext('handshakeID') + throw new Error(errmsg) + } + if (!payload.uuid || !validateUUID(payload.uuid) || versionUUID(payload.uuid) !== 4) { + const errmsg = `Invalid uuid in authenticate-response of community with publicKey` + comB.publicKey + methodLogger.error(errmsg) + methodLogger.removeContext('handshakeID') + throw new Error(errmsg) + } + comB.communityUuid = payload.uuid + comB.authenticatedAt = new Date() + await DbCommunity.save(comB) + methodLogger.debug('Community Authentication successful:', new CommunityLoggingView(comB)) } else { - logger.error('Community Authentication failed:', authenticationArgs) + methodLogger.error('Community Authentication failed:', authenticationArgs) } } } catch (err) { - logger.error('error in startAuthentication:', err) + methodLogger.error('error in startAuthentication:', err) } + methodLogger.removeContext('handshakeID') } diff --git a/frontend/.env.test_e2e b/frontend/.env.test_e2e new file mode 100644 index 000000000..fb85151cb --- /dev/null +++ b/frontend/.env.test_e2e @@ -0,0 +1 @@ +GRAPHQL_URI=http://127.0.0.1:4000/graphql \ No newline at end of file diff --git a/frontend/package.json b/frontend/package.json index 4dbace5c1..3dd82ff8c 100755 --- a/frontend/package.json +++ b/frontend/package.json @@ -21,7 +21,8 @@ "compile-scss": "node ./scripts/scss.mjs compile", "watch-scss": "node ./scripts/scss.mjs watch", "compile-scss-sass": "node ./scripts/scss.mjs compile sass", - "watch-scss-sass": "node ./scripts/scss.mjs watch sass" + "watch-scss-sass": "node ./scripts/scss.mjs watch sass", + "clear": "rm -rf node_modules && rm -rf build && rm -rf .turbo" }, "dependencies": { "@morev/vue-transitions": "^3.0.2", diff --git a/frontend/src/components/Auth/AuthNavbar.vue b/frontend/src/components/Auth/AuthNavbar.vue index 710d15b30..be605bc86 100644 --- a/frontend/src/components/Auth/AuthNavbar.vue +++ b/frontend/src/components/Auth/AuthNavbar.vue @@ -8,11 +8,11 @@ - + {{ $t('signup') }} {{ $t('|') }} - + {{ $t('signin') }} @@ -25,7 +25,7 @@ import { useAuthLinks } from '@/composables/useAuthLinks' import NavItem from '../Menu/NavItem.vue' -const { login, register } = useAuthLinks() +const { routeWithParamsAndQuery } = useAuthLinks() const backgroundHeader = '/img/template/gradido_background_header.png' const logo = '/img/brand/gradido-logo_200x59.png' diff --git a/frontend/src/components/Auth/AuthNavbarSmall.vue b/frontend/src/components/Auth/AuthNavbarSmall.vue index e7917ffe8..32986c7ab 100644 --- a/frontend/src/components/Auth/AuthNavbarSmall.vue +++ b/frontend/src/components/Auth/AuthNavbarSmall.vue @@ -2,9 +2,13 @@ @@ -14,7 +18,7 @@ import { useAuthLinks } from '@/composables/useAuthLinks' import NavItem from '../Menu/NavItem.vue' -const { login, register } = useAuthLinks() +const { routeWithParamsAndQuery } = useAuthLinks()