diff --git a/admin/package.json b/admin/package.json index f80e20152..b1a2bd044 100644 --- a/admin/package.json +++ b/admin/package.json @@ -10,6 +10,7 @@ "start": "node run/server.js", "serve": "vue-cli-service serve --open", "build": "vue-cli-service build", + "postbuild": "find build -type f -regex '.*\\.\\(html\\|js\\|css\\|svg\\|json\\)' -exec gzip -9 -k {} +", "dev": "yarn run serve", "analyse-bundle": "yarn build && webpack-bundle-analyzer build/webpack.stats.json", "lint": "eslint --max-warnings=0 --ext .js,.vue,.json .", diff --git a/backend/jest.config.js b/backend/jest.config.js index f7edec3dd..de649d66e 100644 --- a/backend/jest.config.js +++ b/backend/jest.config.js @@ -16,6 +16,7 @@ module.exports = { moduleNameMapper: { '@/(.*)': '/src/$1', '@arg/(.*)': '/src/graphql/arg/$1', + '@input/(.*)': '/src/graphql/input/$1', '@dltConnector/(.*)': '/src/apis/dltConnector/$1', '@enum/(.*)': '/src/graphql/enum/$1', '@model/(.*)': '/src/graphql/model/$1', diff --git a/backend/src/auth/ADMIN_RIGHTS.ts b/backend/src/auth/ADMIN_RIGHTS.ts index 79006a1de..e95935fd0 100644 --- a/backend/src/auth/ADMIN_RIGHTS.ts +++ b/backend/src/auth/ADMIN_RIGHTS.ts @@ -6,4 +6,6 @@ export const ADMIN_RIGHTS = [ RIGHTS.UNDELETE_USER, RIGHTS.COMMUNITY_UPDATE, RIGHTS.COMMUNITY_BY_UUID, + RIGHTS.COMMUNITY_BY_IDENTIFIER, + RIGHTS.HOME_COMMUNITY, ] diff --git a/backend/src/auth/DLT_CONNECTOR_RIGHTS.ts b/backend/src/auth/DLT_CONNECTOR_RIGHTS.ts new file mode 100644 index 000000000..399b7c2d4 --- /dev/null +++ b/backend/src/auth/DLT_CONNECTOR_RIGHTS.ts @@ -0,0 +1,3 @@ +import { RIGHTS } from './RIGHTS' + +export const DLT_CONNECTOR_RIGHTS = [RIGHTS.COMMUNITY_BY_IDENTIFIER, RIGHTS.HOME_COMMUNITY] diff --git a/backend/src/auth/RIGHTS.ts b/backend/src/auth/RIGHTS.ts index c95aa18fd..c8f02976b 100644 --- a/backend/src/auth/RIGHTS.ts +++ b/backend/src/auth/RIGHTS.ts @@ -59,5 +59,7 @@ export enum RIGHTS { DELETE_USER = 'DELETE_USER', UNDELETE_USER = 'UNDELETE_USER', COMMUNITY_BY_UUID = 'COMMUNITY_BY_UUID', + COMMUNITY_BY_IDENTIFIER = 'COMMUNITY_BY_IDENTIFIER', + HOME_COMMUNITY = 'HOME_COMMUNITY', COMMUNITY_UPDATE = 'COMMUNITY_UPDATE', } diff --git a/backend/src/auth/ROLES.ts b/backend/src/auth/ROLES.ts index 15ba7b263..75d31d149 100644 --- a/backend/src/auth/ROLES.ts +++ b/backend/src/auth/ROLES.ts @@ -1,6 +1,7 @@ import { RoleNames } from '@/graphql/enum/RoleNames' import { ADMIN_RIGHTS } from './ADMIN_RIGHTS' +import { DLT_CONNECTOR_RIGHTS } from './DLT_CONNECTOR_RIGHTS' import { INALIENABLE_RIGHTS } from './INALIENABLE_RIGHTS' import { MODERATOR_RIGHTS } from './MODERATOR_RIGHTS' import { Role } from './Role' @@ -20,5 +21,7 @@ export const ROLE_ADMIN = new Role(RoleNames.ADMIN, [ ...ADMIN_RIGHTS, ]) +export const ROLE_DLT_CONNECTOR = new Role(RoleNames.DLT_CONNECTOR, DLT_CONNECTOR_RIGHTS) + // TODO from database export const ROLES = [ROLE_UNAUTHORIZED, ROLE_USER, ROLE_MODERATOR, ROLE_ADMIN] diff --git a/backend/src/graphql/arg/CommunityArgs.ts b/backend/src/graphql/arg/CommunityArgs.ts index 163a6e504..074901e06 100644 --- a/backend/src/graphql/arg/CommunityArgs.ts +++ b/backend/src/graphql/arg/CommunityArgs.ts @@ -1,14 +1,13 @@ -import { IsString } from 'class-validator' -import { Field, ArgsType, InputType } from 'type-graphql' +import { IsBoolean, IsString } from 'class-validator' +import { ArgsType, Field } from 'type-graphql' -@InputType() @ArgsType() export class CommunityArgs { - @Field(() => String) + @Field(() => String, { nullable: true }) @IsString() - uuid: string + communityIdentifier?: string | null - @Field(() => String) - @IsString() - gmsApiKey: string + @Field(() => Boolean, { nullable: true }) + @IsBoolean() + foreign?: boolean | null } diff --git a/backend/src/graphql/directive/isAuthorized.ts b/backend/src/graphql/directive/isAuthorized.ts index 59309c91e..f3d03a539 100644 --- a/backend/src/graphql/directive/isAuthorized.ts +++ b/backend/src/graphql/directive/isAuthorized.ts @@ -6,7 +6,13 @@ import { RoleNames } from '@enum/RoleNames' import { INALIENABLE_RIGHTS } from '@/auth/INALIENABLE_RIGHTS' import { decode, encode } from '@/auth/JWT' import { RIGHTS } from '@/auth/RIGHTS' -import { ROLE_UNAUTHORIZED, ROLE_USER, ROLE_ADMIN, ROLE_MODERATOR } from '@/auth/ROLES' +import { + ROLE_UNAUTHORIZED, + ROLE_USER, + ROLE_ADMIN, + ROLE_MODERATOR, + ROLE_DLT_CONNECTOR, +} from '@/auth/ROLES' import { Context } from '@/server/context' import { LogError } from '@/server/LogError' @@ -30,31 +36,35 @@ export const isAuthorized: AuthChecker = async ({ context }, rights) => // Set context gradidoID context.gradidoID = decoded.gradidoID - // TODO - load from database dynamically & admin - maybe encode this in the token to prevent many database requests - // TODO this implementation is bullshit - two database queries cause our user identifiers are not aligned and vary between email, id and pubKey - try { - const user = await User.findOneOrFail({ - where: { gradidoID: decoded.gradidoID }, - withDeleted: true, - relations: ['emailContact', 'userRoles'], - }) - context.user = user - context.role = ROLE_USER - if (user.userRoles?.length > 0) { - switch (user.userRoles[0].role) { - case RoleNames.ADMIN: - context.role = ROLE_ADMIN - break - case RoleNames.MODERATOR: - context.role = ROLE_MODERATOR - break - default: - context.role = ROLE_USER + if (context.gradidoID === 'dlt-connector') { + context.role = ROLE_DLT_CONNECTOR + } else { + // TODO - load from database dynamically & admin - maybe encode this in the token to prevent many database requests + // TODO this implementation is bullshit - two database queries cause our user identifiers are not aligned and vary between email, id and pubKey + try { + const user = await User.findOneOrFail({ + where: { gradidoID: decoded.gradidoID }, + withDeleted: true, + relations: ['emailContact', 'userRoles'], + }) + context.user = user + context.role = ROLE_USER + if (user.userRoles?.length > 0) { + switch (user.userRoles[0].role) { + case RoleNames.ADMIN: + context.role = ROLE_ADMIN + break + case RoleNames.MODERATOR: + context.role = ROLE_MODERATOR + break + default: + context.role = ROLE_USER + } } + } catch { + // in case the database query fails (user deleted) + throw new LogError('401 Unauthorized') } - } catch { - // in case the database query fails (user deleted) - throw new LogError('401 Unauthorized') } // check for correct rights diff --git a/backend/src/graphql/enum/RoleNames.ts b/backend/src/graphql/enum/RoleNames.ts index c4a9b25cc..1eb8f22c3 100644 --- a/backend/src/graphql/enum/RoleNames.ts +++ b/backend/src/graphql/enum/RoleNames.ts @@ -5,6 +5,7 @@ export enum RoleNames { USER = 'USER', MODERATOR = 'MODERATOR', ADMIN = 'ADMIN', + DLT_CONNECTOR = 'DLT_CONNECTOR_ROLE', } registerEnumType(RoleNames, { diff --git a/backend/src/graphql/input/EditCommunityInput.ts b/backend/src/graphql/input/EditCommunityInput.ts new file mode 100644 index 000000000..8c74f874b --- /dev/null +++ b/backend/src/graphql/input/EditCommunityInput.ts @@ -0,0 +1,14 @@ +import { IsString, IsUUID } from 'class-validator' +import { ArgsType, Field, InputType } from 'type-graphql' + +@ArgsType() +@InputType() +export class EditCommunityInput { + @Field(() => String) + @IsUUID('4') + uuid: string + + @Field(() => String) + @IsString() + gmsApiKey: string +} diff --git a/backend/src/graphql/resolver/CommunityResolver.test.ts b/backend/src/graphql/resolver/CommunityResolver.test.ts index 0e4bd5e70..2d60f3444 100644 --- a/backend/src/graphql/resolver/CommunityResolver.test.ts +++ b/backend/src/graphql/resolver/CommunityResolver.test.ts @@ -10,6 +10,7 @@ import { Community as DbCommunity } from '@entity/Community' import { FederatedCommunity as DbFederatedCommunity } from '@entity/FederatedCommunity' import { ApolloServerTestClient } from 'apollo-server-testing' import { GraphQLError } from 'graphql/error/GraphQLError' +import { v4 as uuidv4 } from 'uuid' import { cleanDB, testEnvironment } from '@test/helpers' import { logger, i18n as localization } from '@test/testSetup' @@ -17,10 +18,11 @@ import { logger, i18n as localization } from '@test/testSetup' import { userFactory } from '@/seeds/factory/user' import { login, updateHomeCommunityQuery } from '@/seeds/graphql/mutations' import { + allCommunities, getCommunities, communitiesQuery, - getCommunityByUuidQuery, - allCommunities, + getHomeCommunityQuery, + getCommunityByIdentifierQuery, } from '@/seeds/graphql/queries' import { peterLustig } from '@/seeds/users/peter-lustig' @@ -735,15 +737,36 @@ describe('CommunityResolver', () => { await DbCommunity.insert(foreignCom2) }) - it('finds the home-community', async () => { + it('finds the home-community by uuid', async () => { await expect( query({ - query: getCommunityByUuidQuery, - variables: { communityUuid: homeCom?.communityUuid }, + query: getCommunityByIdentifierQuery, + variables: { communityIdentifier: homeCom?.communityUuid }, }), ).resolves.toMatchObject({ data: { - community: { + communityByIdentifier: { + id: homeCom?.id, + foreign: homeCom?.foreign, + name: homeCom?.name, + description: homeCom?.description, + url: homeCom?.url, + creationDate: homeCom?.creationDate?.toISOString(), + uuid: homeCom?.communityUuid, + authenticatedAt: homeCom?.authenticatedAt, + }, + }, + }) + }) + + it('finds the home-community', async () => { + await expect( + query({ + query: getHomeCommunityQuery, + }), + ).resolves.toMatchObject({ + data: { + homeCommunity: { id: homeCom?.id, foreign: homeCom?.foreign, name: homeCom?.name, @@ -812,7 +835,7 @@ describe('CommunityResolver', () => { expect( await mutate({ mutation: updateHomeCommunityQuery, - variables: { uuid: 'unknownUuid', gmsApiKey: 'gmsApiKey' }, + variables: { uuid: uuidv4(), gmsApiKey: 'gmsApiKey' }, }), ).toEqual( expect.objectContaining({ diff --git a/backend/src/graphql/resolver/CommunityResolver.ts b/backend/src/graphql/resolver/CommunityResolver.ts index 9cab50610..2d2323865 100644 --- a/backend/src/graphql/resolver/CommunityResolver.ts +++ b/backend/src/graphql/resolver/CommunityResolver.ts @@ -1,10 +1,10 @@ import { IsNull, Not } from '@dbTools/typeorm' import { Community as DbCommunity } from '@entity/Community' import { FederatedCommunity as DbFederatedCommunity } from '@entity/FederatedCommunity' -import { Resolver, Query, Authorized, Arg, Mutation, Args } from 'type-graphql' +import { Resolver, Query, Authorized, Mutation, Args, Arg } from 'type-graphql' -import { CommunityArgs } from '@arg//CommunityArgs' import { Paginated } from '@arg/Paginated' +import { EditCommunityInput } from '@input/EditCommunityInput' import { AdminCommunityView } from '@model/AdminCommunityView' import { Community } from '@model/Community' import { FederatedCommunity } from '@model/FederatedCommunity' @@ -12,7 +12,12 @@ import { FederatedCommunity } from '@model/FederatedCommunity' import { RIGHTS } from '@/auth/RIGHTS' import { LogError } from '@/server/LogError' -import { getAllCommunities, getCommunityByUuid } from './util/communities' +import { + getAllCommunities, + getCommunityByIdentifier, + getCommunityByUuid, + getHomeCommunity, +} from './util/communities' @Resolver() export class CommunityResolver { @@ -49,41 +54,42 @@ export class CommunityResolver { return dbCommunities.map((dbCom: DbCommunity) => new Community(dbCom)) } - @Authorized([RIGHTS.COMMUNITY_BY_UUID]) + @Authorized([RIGHTS.COMMUNITY_BY_IDENTIFIER]) @Query(() => Community) - async community(@Arg('communityUuid') communityUuid: string): Promise { - const com: DbCommunity | null = await getCommunityByUuid(communityUuid) - if (!com) { - throw new LogError('community not found', communityUuid) + async communityByIdentifier( + @Arg('communityIdentifier') communityIdentifier: string, + ): Promise { + const community = await getCommunityByIdentifier(communityIdentifier) + if (!community) { + throw new LogError('community not found', communityIdentifier) } - return new Community(com) + return new Community(community) + } + + @Authorized([RIGHTS.HOME_COMMUNITY]) + @Query(() => Community) + async homeCommunity(): Promise { + const community = await getHomeCommunity() + if (!community) { + throw new LogError('no home community exist') + } + return new Community(community) } @Authorized([RIGHTS.COMMUNITY_UPDATE]) @Mutation(() => Community) - async updateHomeCommunity(@Args() { uuid, gmsApiKey }: CommunityArgs): Promise { - let homeCom: DbCommunity | null - let com: Community - if (uuid) { - let toUpdate = false - homeCom = await getCommunityByUuid(uuid) - if (!homeCom) { - throw new LogError('HomeCommunity with uuid not found: ', uuid) - } - if (homeCom.foreign) { - throw new LogError('Error: Only the HomeCommunity could be modified!') - } - if (homeCom.gmsApiKey !== gmsApiKey) { - homeCom.gmsApiKey = gmsApiKey - toUpdate = true - } - if (toUpdate) { - await DbCommunity.save(homeCom) - } - com = new Community(homeCom) - } else { - throw new LogError(`HomeCommunity without an uuid can't be modified!`) + async updateHomeCommunity(@Args() { uuid, gmsApiKey }: EditCommunityInput): Promise { + const homeCom = await getCommunityByUuid(uuid) + if (!homeCom) { + throw new LogError('HomeCommunity with uuid not found: ', uuid) } - return com + if (homeCom.foreign) { + throw new LogError('Error: Only the HomeCommunity could be modified!') + } + if (homeCom.gmsApiKey !== gmsApiKey) { + homeCom.gmsApiKey = gmsApiKey + await DbCommunity.save(homeCom) + } + return new Community(homeCom) } } diff --git a/backend/src/graphql/resolver/TransactionResolver.ts b/backend/src/graphql/resolver/TransactionResolver.ts index ce1ba43da..56c928995 100644 --- a/backend/src/graphql/resolver/TransactionResolver.ts +++ b/backend/src/graphql/resolver/TransactionResolver.ts @@ -38,7 +38,7 @@ import { calculateBalance } from '@/util/validate' import { virtualLinkTransaction, virtualDecayTransaction } from '@/util/virtualTransactions' import { BalanceResolver } from './BalanceResolver' -import { getCommunityByUuid, getCommunityName, isHomeCommunity } from './util/communities' +import { getCommunityByIdentifier, getCommunityName, isHomeCommunity } from './util/communities' import { findUserByIdentifier } from './util/findUserByIdentifier' import { getLastTransaction } from './util/getLastTransaction' import { getTransactionList } from './util/getTransactionList' @@ -452,7 +452,7 @@ export class TransactionResolver { if (!CONFIG.FEDERATION_XCOM_SENDCOINS_ENABLED) { throw new LogError('X-Community sendCoins disabled per configuration!') } - const recipCom = await getCommunityByUuid(recipientCommunityIdentifier) + const recipCom = await getCommunityByIdentifier(recipientCommunityIdentifier) logger.debug('recipient commuity: ', recipCom) if (recipCom === null) { throw new LogError( diff --git a/backend/src/graphql/resolver/util/communities.ts b/backend/src/graphql/resolver/util/communities.ts index e85357991..87cf31610 100644 --- a/backend/src/graphql/resolver/util/communities.ts +++ b/backend/src/graphql/resolver/util/communities.ts @@ -1,3 +1,4 @@ +import { FindOneOptions } from '@dbTools/typeorm' import { Community as DbCommunity } from '@entity/Community' import { FederatedCommunity as DbFederatedCommunity } from '@entity/FederatedCommunity' @@ -6,70 +7,92 @@ import { Paginated } from '@arg/Paginated' import { LogError } from '@/server/LogError' import { Connection } from '@/typeorm/connection' +function findWithCommunityIdentifier(communityIdentifier: string): FindOneOptions { + return { + where: [ + { communityUuid: communityIdentifier }, + { name: communityIdentifier }, + { url: communityIdentifier }, + ], + } +} + +/** + * Checks if a community with the given identifier exists and is not foreign. + * @param communityIdentifier The identifier (URL, UUID, or name) of the community. + * @returns A promise that resolves to true if a non-foreign community exists with the given identifier, otherwise false. + */ export async function isHomeCommunity(communityIdentifier: string): Promise { - const homeCommunity = await DbCommunity.findOne({ + // The !! operator in JavaScript or TypeScript is a shorthand for converting a value to a boolean. + // It essentially converts any truthy value to true and any falsy value to false. + return !!(await DbCommunity.findOne({ where: [ { foreign: false, communityUuid: communityIdentifier }, { foreign: false, name: communityIdentifier }, { foreign: false, url: communityIdentifier }, ], - }) - if (homeCommunity) { - return true - } else { - return false - } + })) } +/** + * Retrieves the home community, i.e., a community that is not foreign. + * @returns A promise that resolves to the home community, or throw if no home community was found + */ export async function getHomeCommunity(): Promise { return await DbCommunity.findOneOrFail({ where: [{ foreign: false }], }) } +/** + * TODO: Check if it is needed, because currently it isn't used at all + * Retrieves the URL of the community with the given identifier. + * @param communityIdentifier The identifier (URL, UUID, or name) of the community. + * @returns A promise that resolves to the URL of the community or throw if no community with this identifier was found + */ export async function getCommunityUrl(communityIdentifier: string): Promise { - const community = await DbCommunity.findOneOrFail({ - where: [ - { communityUuid: communityIdentifier }, - { name: communityIdentifier }, - { url: communityIdentifier }, - ], - }) - return community.url + return (await DbCommunity.findOneOrFail(findWithCommunityIdentifier(communityIdentifier))).url } +/** + * TODO: Check if it is needed, because currently it isn't used at all + * Checks if a community with the given identifier exists and has an authenticatedAt property set. + * @param communityIdentifier The identifier (URL, UUID, or name) of the community. + * @returns A promise that resolves to true if a community with an authenticatedAt property exists with the given identifier, + * otherwise false. + */ export async function isCommunityAuthenticated(communityIdentifier: string): Promise { - const community = await DbCommunity.findOne({ - where: [ - { communityUuid: communityIdentifier }, - { name: communityIdentifier }, - { url: communityIdentifier }, - ], - }) - if (community?.authenticatedAt) { - return true - } else { - return false - } + // The !! operator in JavaScript or TypeScript is a shorthand for converting a value to a boolean. + // It essentially converts any truthy value to true and any falsy value to false. + return !!(await DbCommunity.findOne(findWithCommunityIdentifier(communityIdentifier))) + ?.authenticatedAt } +/** + * Retrieves the name of the community with the given identifier. + * @param communityIdentifier The identifier (URL, UUID) of the community. + * @returns A promise that resolves to the name of the community. If the community does not exist or has no name, + * an empty string is returned. + */ export async function getCommunityName(communityIdentifier: string): Promise { const community = await DbCommunity.findOne({ where: [{ communityUuid: communityIdentifier }, { url: communityIdentifier }], }) - if (community?.name) { - return community.name - } else { - return '' - } -} + return community?.name ? community.name : '' +} export async function getCommunityByUuid(communityUuid: string): Promise { return await DbCommunity.findOne({ where: [{ communityUuid }], }) } +export async function getCommunityByIdentifier( + communityIdentifier: string, +): Promise { + return await DbCommunity.findOne(findWithCommunityIdentifier(communityIdentifier)) +} + /** * Simulate RIGHT Join between Communities and Federated Communities * select * diff --git a/backend/src/graphql/resolver/util/findUserByIdentifier.ts b/backend/src/graphql/resolver/util/findUserByIdentifier.ts index 7e52327d3..435dc3d04 100644 --- a/backend/src/graphql/resolver/util/findUserByIdentifier.ts +++ b/backend/src/graphql/resolver/util/findUserByIdentifier.ts @@ -2,9 +2,11 @@ import { FindOptionsWhere } from '@dbTools/typeorm' import { Community } from '@entity/Community' import { User as DbUser } from '@entity/User' import { UserContact as DbUserContact } from '@entity/UserContact' +import { isURL } from 'class-validator' import { validate, version } from 'uuid' import { LogError } from '@/server/LogError' +import { isEMail, isUUID4 } from '@/util/validate' import { VALID_ALIAS_REGEX } from './validateAlias' @@ -19,10 +21,11 @@ export const findUserByIdentifier = async ( communityIdentifier: string, ): Promise => { let user: DbUser | null - const communityWhere: FindOptionsWhere = - validate(communityIdentifier) && version(communityIdentifier) === 4 - ? { communityUuid: communityIdentifier } - : { name: communityIdentifier } + const communityWhere: FindOptionsWhere = isURL(communityIdentifier) + ? { url: communityIdentifier } + : isUUID4(communityIdentifier) + ? { communityUuid: communityIdentifier } + : { name: communityIdentifier } if (validate(identifier) && version(identifier) === 4) { user = await DbUser.findOne({ @@ -32,7 +35,7 @@ export const findUserByIdentifier = async ( if (!user) { throw new LogError('No user found to given identifier(s)', identifier, communityIdentifier) } - } else if (/^.{2,}@.{2,}\..{2,}$/.exec(identifier)) { + } else if (isEMail(identifier)) { const userContact = await DbUserContact.findOne({ where: { email: identifier, diff --git a/backend/src/seeds/graphql/queries.ts b/backend/src/seeds/graphql/queries.ts index c3e7974a4..ed0fe6d26 100644 --- a/backend/src/seeds/graphql/queries.ts +++ b/backend/src/seeds/graphql/queries.ts @@ -134,9 +134,25 @@ export const communitiesQuery = gql` } ` -export const getCommunityByUuidQuery = gql` - query ($communityUuid: String!) { - community(communityUuid: $communityUuid) { +export const getCommunityByIdentifierQuery = gql` + query ($communityIdentifier: String!) { + communityByIdentifier(communityIdentifier: $communityIdentifier) { + id + foreign + name + description + url + creationDate + uuid + authenticatedAt + gmsApiKey + } + } +` + +export const getHomeCommunityQuery = gql` + query { + homeCommunity { id foreign name diff --git a/backend/src/util/validate.ts b/backend/src/util/validate.ts index 4780c94e8..ab0c8a12a 100644 --- a/backend/src/util/validate.ts +++ b/backend/src/util/validate.ts @@ -1,5 +1,6 @@ import { TransactionLink as dbTransactionLink } from '@entity/TransactionLink' import { Decimal } from 'decimal.js-light' +import { validate, version } from 'uuid' import { Decay } from '@model/Decay' @@ -16,6 +17,14 @@ function isStringBoolean(value: string): boolean { return false } +function isUUID4(value: string): boolean { + return validate(value) && version(value) === 4 +} + +function isEMail(value: string): boolean { + return /^.{2,}@.{2,}\..{2,}$/.exec(value) !== null +} + async function calculateBalance( userId: number, amount: Decimal, @@ -42,4 +51,4 @@ async function calculateBalance( return { balance, lastTransactionId: lastTransaction.id, decay } } -export { calculateBalance, isStringBoolean } +export { calculateBalance, isStringBoolean, isUUID4, isEMail } diff --git a/backend/tsconfig.json b/backend/tsconfig.json index 28ddf1c38..c61539e12 100644 --- a/backend/tsconfig.json +++ b/backend/tsconfig.json @@ -49,6 +49,7 @@ "paths": { /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ "@/*": ["src/*"], "@arg/*": ["src/graphql/arg/*"], + "@input/*": ["src/graphql/input/*"], "@dltConnector/*": ["src/apis/dltConnector/*"], "@enum/*": ["src/graphql/enum/*"], "@model/*": ["src/graphql/model/*"], diff --git a/deployment/bare_metal/nginx/sites-available/gradido.conf.ssl.template b/deployment/bare_metal/nginx/sites-available/gradido.conf.ssl.template index d8ed50ba4..9910c3366 100644 --- a/deployment/bare_metal/nginx/sites-available/gradido.conf.ssl.template +++ b/deployment/bare_metal/nginx/sites-available/gradido.conf.ssl.template @@ -14,8 +14,8 @@ server { server { server_name $COMMUNITY_HOST; - listen [::]:443 ssl ipv6only=on; - listen 443 ssl; + listen [::]:443 ssl ipv6only=on http2; + listen 443 ssl http2; ssl_certificate $NGINX_SSL_CERTIFICATE; ssl_certificate_key $NGINX_SSL_CERTIFICATE_KEY; include $NGINX_SSL_INCLUDE; @@ -33,7 +33,7 @@ server { return 444; } - #gzip_static on; + gzip_static on; gzip on; gzip_proxied any; gzip_types @@ -53,18 +53,13 @@ server { # Frontend (default) location / { + limit_req zone=frontend burst=40 nodelay; limit_conn addr 40; - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection 'upgrade'; - proxy_set_header X-Forwarded-For $remote_addr; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header Host $host; + root $PROJECT_ROOT/frontend/build/; + index index.html; + try_files $uri $uri/ /index.html = 404; - proxy_pass http://127.0.0.1:3000; - proxy_redirect off; - access_log $GRADIDO_LOG_PATH/nginx-access.frontend.log gradido_log; error_log $GRADIDO_LOG_PATH/nginx-error.frontend.log warn; } @@ -119,15 +114,10 @@ server { location /admin { limit_req zone=frontend burst=30 nodelay; limit_conn addr 40; - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection 'upgrade'; - proxy_set_header X-Forwarded-For $remote_addr; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header Host $host; - - proxy_pass http://127.0.0.1:8080/; - proxy_redirect off; + rewrite ^/admin/(.*)$ /$1 break; + root $PROJECT_ROOT/admin/build/; + index index.html; + try_files $uri $uri/ /index.html = 404; access_log $GRADIDO_LOG_PATH/nginx-access.admin.log gradido_log; error_log $GRADIDO_LOG_PATH/nginx-error.admin.log warn; diff --git a/deployment/bare_metal/nginx/sites-available/gradido.conf.template b/deployment/bare_metal/nginx/sites-available/gradido.conf.template index e0f382467..e64e7e1ce 100644 --- a/deployment/bare_metal/nginx/sites-available/gradido.conf.template +++ b/deployment/bare_metal/nginx/sites-available/gradido.conf.template @@ -18,7 +18,7 @@ server { return 444; } - #gzip_static on; + gzip_static on; gzip on; gzip_proxied any; gzip_types @@ -40,15 +40,9 @@ server { location / { limit_req zone=frontend burst=40 nodelay; limit_conn addr 40; - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection 'upgrade'; - proxy_set_header X-Forwarded-For $remote_addr; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header Host $host; - - proxy_pass http://127.0.0.1:3000; - proxy_redirect off; + root $PROJECT_ROOT/frontend/build/; + index index.html; + try_files $uri $uri/ /index.html = 404; access_log $GRADIDO_LOG_PATH/nginx-access.frontend.log gradido_log; error_log $GRADIDO_LOG_PATH/nginx-error.frontend.log warn; @@ -104,15 +98,10 @@ server { location /admin { limit_req zone=frontend burst=30 nodelay; limit_conn addr 40; - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection 'upgrade'; - proxy_set_header X-Forwarded-For $remote_addr; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header Host $host; - - proxy_pass http://127.0.0.1:8080/; - proxy_redirect off; + rewrite ^/admin/(.*)$ /$1 break; + root $PROJECT_ROOT/admin/build/; + index index.html; + try_files $uri $uri/ /index.html = 404; access_log $GRADIDO_LOG_PATH/nginx-access.admin.log gradido_log; error_log $GRADIDO_LOG_PATH/nginx-error.admin.log warn; diff --git a/deployment/bare_metal/start.sh b/deployment/bare_metal/start.sh index af8986318..634f60c97 100755 --- a/deployment/bare_metal/start.sh +++ b/deployment/bare_metal/start.sh @@ -241,8 +241,8 @@ export NODE_ENV=production # start after building all to use up less ressources pm2 start --name gradido-backend "yarn --cwd $PROJECT_ROOT/backend start" -l $GRADIDO_LOG_PATH/pm2.backend.$TODAY.log --log-date-format 'YYYY-MM-DD HH:mm:ss.SSS' -pm2 start --name gradido-frontend "yarn --cwd $PROJECT_ROOT/frontend start" -l $GRADIDO_LOG_PATH/pm2.frontend.$TODAY.log --log-date-format 'YYYY-MM-DD HH:mm:ss.SSS' -pm2 start --name gradido-admin "yarn --cwd $PROJECT_ROOT/admin start" -l $GRADIDO_LOG_PATH/pm2.admin.$TODAY.log --log-date-format 'YYYY-MM-DD HH:mm:ss.SSS' +#pm2 start --name gradido-frontend "yarn --cwd $PROJECT_ROOT/frontend start" -l $GRADIDO_LOG_PATH/pm2.frontend.$TODAY.log --log-date-format 'YYYY-MM-DD HH:mm:ss.SSS' +#pm2 start --name gradido-admin "yarn --cwd $PROJECT_ROOT/admin start" -l $GRADIDO_LOG_PATH/pm2.admin.$TODAY.log --log-date-format 'YYYY-MM-DD HH:mm:ss.SSS' pm2 save if [ ! -z $FEDERATION_DHT_TOPIC ]; then pm2 start --name gradido-dht-node "yarn --cwd $PROJECT_ROOT/dht-node start" -l $GRADIDO_LOG_PATH/pm2.dht-node.$TODAY.log --log-date-format 'YYYY-MM-DD HH:mm:ss.SSS' diff --git a/deployment/hetzner_cloud/install.sh b/deployment/hetzner_cloud/install.sh index 06b92ecaf..b51f9b454 100755 --- a/deployment/hetzner_cloud/install.sh +++ b/deployment/hetzner_cloud/install.sh @@ -104,6 +104,23 @@ ln -s $SCRIPT_PATH/nginx/common /etc/nginx/ rmdir /etc/nginx/conf.d ln -s $SCRIPT_PATH/nginx/conf.d /etc/nginx/ +# Make nginx restart automatic +mkdir /etc/systemd/system/nginx.service.d +# Define the content to be put into the override.conf file +CONFIG_CONTENT="[Unit] +StartLimitIntervalSec=500 +StartLimitBurst=5 + +[Service] +Restart=on-failure +RestartSec=5s" + +# Write the content to the override.conf file +echo "$CONFIG_CONTENT" | sudo tee /etc/systemd/system/nginx.service.d/override.conf >/dev/null + +# Reload systemd to apply the changes +sudo systemctl daemon-reload + # setup https with certbot certbot certonly --nginx --non-interactive --agree-tos --domains $COMMUNITY_HOST --email $COMMUNITY_SUPPORT_MAIL diff --git a/dlt-connector/.env.dist b/dlt-connector/.env.dist index 1247ac3ec..50e9fe8e1 100644 --- a/dlt-connector/.env.dist +++ b/dlt-connector/.env.dist @@ -1,4 +1,4 @@ -CONFIG_VERSION=v4.2023-09-12 +CONFIG_VERSION=v6.2024-02-20 # SET LOG LEVEL AS NEEDED IN YOUR .ENV # POSSIBLE VALUES: all | trace | debug | info | warn | error | fatal @@ -19,4 +19,8 @@ DB_DATABASE_TEST=gradido_dlt_test TYPEORM_LOGGING_RELATIVE_PATH=typeorm.backend.log # DLT-Connector -DLT_CONNECTOR_PORT=6010 \ No newline at end of file +DLT_CONNECTOR_PORT=6010 + +# Route to Backend +BACKEND_SERVER_URL=http://localhost:4000 +JWT_SECRET=secret123 \ No newline at end of file diff --git a/dlt-connector/.env.template b/dlt-connector/.env.template index e3793f642..2e123ca81 100644 --- a/dlt-connector/.env.template +++ b/dlt-connector/.env.template @@ -1,5 +1,7 @@ CONFIG_VERSION=$DLT_CONNECTOR_CONFIG_VERSION +JWT_SECRET=$JWT_SECRET + #IOTA IOTA_API_URL=$IOTA_API_URL IOTA_COMMUNITY_ALIAS=$IOTA_COMMUNITY_ALIAS @@ -15,4 +17,7 @@ DB_DATABASE_TEST=$DB_DATABASE_TEST TYPEORM_LOGGING_RELATIVE_PATH=typeorm.backend.log # DLT-Connector -DLT_CONNECTOR_PORT=$DLT_CONNECTOR_PORT \ No newline at end of file +DLT_CONNECTOR_PORT=$DLT_CONNECTOR_PORT + +# Route to Backend +BACKEND_SERVER_URL=http://localhost:4000 \ No newline at end of file diff --git a/dlt-connector/jest.config.js b/dlt-connector/jest.config.js index 3d731787f..9b5a01350 100644 --- a/dlt-connector/jest.config.js +++ b/dlt-connector/jest.config.js @@ -6,7 +6,7 @@ module.exports = { collectCoverageFrom: ['src/**/*.ts', '!**/node_modules/**', '!src/seeds/**', '!build/**'], coverageThreshold: { global: { - lines: 75, + lines: 72, }, }, setupFiles: ['/test/testSetup.ts'], diff --git a/dlt-connector/package.json b/dlt-connector/package.json index 3d0685d6e..7aa8aa10d 100644 --- a/dlt-connector/package.json +++ b/dlt-connector/package.json @@ -31,8 +31,10 @@ "express": "4.17.1", "express-slow-down": "^2.0.1", "graphql": "^16.7.1", + "graphql-request": "^6.1.0", "graphql-scalars": "^1.22.2", "helmet": "^7.1.0", + "jose": "^5.2.2", "log4js": "^6.7.1", "nodemon": "^2.0.20", "protobufjs": "^7.2.5", diff --git a/dlt-connector/src/client/BackendClient.ts b/dlt-connector/src/client/BackendClient.ts new file mode 100644 index 000000000..77356f5d8 --- /dev/null +++ b/dlt-connector/src/client/BackendClient.ts @@ -0,0 +1,105 @@ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +import { gql, GraphQLClient } from 'graphql-request' +import { SignJWT } from 'jose' + +import { CONFIG } from '@/config' +import { CommunityDraft } from '@/graphql/input/CommunityDraft' +import { logger } from '@/logging/logger' +import { LogError } from '@/server/LogError' + +const homeCommunity = gql` + query { + homeCommunity { + uuid + foreign + creationDate + } + } +` +interface Community { + homeCommunity: { + uuid: string + foreign: boolean + creationDate: string + } +} +// Source: https://refactoring.guru/design-patterns/singleton/typescript/example +// and ../federation/client/FederationClientFactory.ts +/** + * A Singleton class defines the `getInstance` method that lets clients access + * the unique singleton instance. + */ +// eslint-disable-next-line @typescript-eslint/no-extraneous-class +export class BackendClient { + // eslint-disable-next-line no-use-before-define + private static instance: BackendClient + client: GraphQLClient + /** + * The Singleton's constructor should always be private to prevent direct + * construction calls with the `new` operator. + */ + // eslint-disable-next-line no-useless-constructor, @typescript-eslint/no-empty-function + private constructor() {} + + /** + * The static method that controls the access to the singleton instance. + * + * This implementation let you subclass the Singleton class while keeping + * just one instance of each subclass around. + */ + public static getInstance(): BackendClient | undefined { + if (!BackendClient.instance) { + BackendClient.instance = new BackendClient() + } + if (!BackendClient.instance.client) { + try { + BackendClient.instance.client = new GraphQLClient(CONFIG.BACKEND_SERVER_URL, { + headers: { + 'content-type': 'application/json', + }, + method: 'GET', + jsonSerializer: { + parse: JSON.parse, + stringify: JSON.stringify, + }, + }) + } catch (e) { + logger.error("couldn't connect to backend: ", e) + return + } + } + return BackendClient.instance + } + + public async getHomeCommunityDraft(): Promise { + logger.info('check home community on backend') + const { data, errors } = await this.client.rawRequest( + homeCommunity, + {}, + { + authorization: 'Bearer ' + (await this.createJWTToken()), + }, + ) + if (errors) { + throw new LogError('error getting home community from backend', errors) + } + const communityDraft = new CommunityDraft() + communityDraft.uuid = data.homeCommunity.uuid + communityDraft.foreign = data.homeCommunity.foreign + communityDraft.createdAt = data.homeCommunity.creationDate + return communityDraft + } + + private async createJWTToken(): Promise { + const secret = new TextEncoder().encode(CONFIG.JWT_SECRET) + const token = await new SignJWT({ gradidoID: 'dlt-connector', 'urn:gradido:claim': true }) + .setProtectedHeader({ alg: 'HS256' }) + .setIssuedAt() + .setIssuer('urn:gradido:issuer') + .setAudience('urn:gradido:audience') + .setExpirationTime('1m') + .sign(secret) + return token + } +} diff --git a/dlt-connector/src/config/index.ts b/dlt-connector/src/config/index.ts index db26d9f37..6b4fdae9a 100644 --- a/dlt-connector/src/config/index.ts +++ b/dlt-connector/src/config/index.ts @@ -9,13 +9,14 @@ const constants = { LOG_LEVEL: process.env.LOG_LEVEL ?? 'info', CONFIG_VERSION: { DEFAULT: 'DEFAULT', - EXPECTED: 'v4.2023-09-12', + EXPECTED: 'v6.2024-02-20', CURRENT: '', }, } const server = { PRODUCTION: process.env.NODE_ENV === 'production' ?? false, + JWT_SECRET: process.env.JWT_SECRET ?? 'secret123', } const database = { @@ -38,6 +39,10 @@ const dltConnector = { DLT_CONNECTOR_PORT: process.env.DLT_CONNECTOR_PORT ?? 6010, } +const backendServer = { + BACKEND_SERVER_URL: process.env.BACKEND_SERVER_URL ?? 'http://backend:4000', +} + // Check config version constants.CONFIG_VERSION.CURRENT = process.env.CONFIG_VERSION ?? constants.CONFIG_VERSION.DEFAULT if ( @@ -56,4 +61,5 @@ export const CONFIG = { ...database, ...iota, ...dltConnector, + ...backendServer, } diff --git a/dlt-connector/src/index.ts b/dlt-connector/src/index.ts index 4e5ef9639..bfbff10c3 100644 --- a/dlt-connector/src/index.ts +++ b/dlt-connector/src/index.ts @@ -1,10 +1,42 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ +import 'reflect-metadata' + import { CONFIG } from '@/config' +import { BackendClient } from './client/BackendClient' +import { CommunityRepository } from './data/Community.repository' import { Mnemonic } from './data/Mnemonic' +import { CommunityDraft } from './graphql/input/CommunityDraft' +import { AddCommunityContext } from './interactions/backendToDb/community/AddCommunity.context' +import { logger } from './logging/logger' import createServer from './server/createServer' +import { LogError } from './server/LogError' import { stopTransmitToIota, transmitToIota } from './tasks/transmitToIota' +async function waitForServer( + backend: BackendClient, + retryIntervalMs: number, + maxRetries: number, +): Promise { + let retries = 0 + while (retries < maxRetries) { + logger.info(`Attempt ${retries + 1} for connecting to backend`) + + try { + // Make a HEAD request to the server + return await backend.getHomeCommunityDraft() + } catch (error) { + logger.info('Server is not reachable: ', error) + } + + // Server is not reachable, wait and retry + await new Promise((resolve) => setTimeout(resolve, retryIntervalMs)) + retries++ + } + + throw new LogError('Max retries exceeded. Server did not become reachable.') +} + async function main() { if (CONFIG.IOTA_HOME_COMMUNITY_SEED) { Mnemonic.validateSeed(CONFIG.IOTA_HOME_COMMUNITY_SEED) @@ -13,6 +45,22 @@ async function main() { console.log(`DLT_CONNECTOR_PORT=${CONFIG.DLT_CONNECTOR_PORT}`) const { app } = await createServer() + // ask backend for home community if we haven't one + try { + await CommunityRepository.loadHomeCommunityKeyPair() + } catch (e) { + const backend = BackendClient.getInstance() + if (!backend) { + throw new LogError('cannot create backend client') + } + // wait for backend server to be ready + await waitForServer(backend, 2500, 10) + + const communityDraft = await backend.getHomeCommunityDraft() + const addCommunityContext = new AddCommunityContext(communityDraft) + await addCommunityContext.run() + } + // loop run all the time, check for new transaction for sending to iota void transmitToIota() app.listen(CONFIG.DLT_CONNECTOR_PORT, () => { diff --git a/dlt-connector/src/interactions/backendToDb/community/HomeCommunity.role.ts b/dlt-connector/src/interactions/backendToDb/community/HomeCommunity.role.ts index 647c5d397..5d7bec94c 100644 --- a/dlt-connector/src/interactions/backendToDb/community/HomeCommunity.role.ts +++ b/dlt-connector/src/interactions/backendToDb/community/HomeCommunity.role.ts @@ -12,6 +12,7 @@ import { TransactionError } from '@/graphql/model/TransactionError' import { CommunityLoggingView } from '@/logging/CommunityLogging.view' import { logger } from '@/logging/logger' import { InterruptiveSleepManager } from '@/manager/InterruptiveSleepManager' +import { LogError } from '@/server/LogError' import { getDataSource } from '@/typeorm/DataSource' import { CreateTransactionRecipeContext } from '../transaction/CreateTransationRecipe.context' @@ -24,7 +25,19 @@ export class HomeCommunityRole extends CommunityRole { public async create(communityDraft: CommunityDraft, topic: string): Promise { super.create(communityDraft, topic) // generate key pair for signing transactions and deriving all keys for community - const keyPair = new KeyPair(new Mnemonic(CONFIG.IOTA_HOME_COMMUNITY_SEED ?? undefined)) + let mnemonic: Mnemonic + try { + mnemonic = new Mnemonic(CONFIG.IOTA_HOME_COMMUNITY_SEED ?? undefined) + } catch (e) { + throw new LogError( + 'error creating mnemonic for home community, please fill IOTA_HOME_COMMUNITY_SEED in .env', + { + IOTA_HOME_COMMUNITY_SEED: CONFIG.IOTA_HOME_COMMUNITY_SEED, + error: e, + }, + ) + } + const keyPair = new KeyPair(mnemonic) keyPair.fillInCommunityKeys(this.self) // create auf account and gmw account diff --git a/dlt-connector/yarn.lock b/dlt-connector/yarn.lock index 7f46d88bc..3188c39a0 100644 --- a/dlt-connector/yarn.lock +++ b/dlt-connector/yarn.lock @@ -569,7 +569,7 @@ "@graphql-typed-document-node/core" "^3.1.1" tslib "^2.4.0" -"@graphql-typed-document-node/core@^3.1.1": +"@graphql-typed-document-node/core@^3.1.1", "@graphql-typed-document-node/core@^3.2.0": version "3.2.0" resolved "https://registry.yarnpkg.com/@graphql-typed-document-node/core/-/core-3.2.0.tgz#5f3d96ec6b2354ad6d8a28bf216a1d97b5426861" integrity sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ== @@ -2119,6 +2119,13 @@ cross-env@^7.0.3: dependencies: cross-spawn "^7.0.1" +cross-fetch@^3.1.5: + version "3.1.8" + resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.1.8.tgz#0327eba65fd68a7d119f8fb2bf9334a1a7956f82" + integrity sha512-cvA+JwZoU0Xq+h6WkMvAUqPEYy92Obet6UdKLfW60qn99ftItKjB5T+BkyWOFWe2pUyfQ+IJHmpOTznqk1M6Kg== + dependencies: + node-fetch "^2.6.12" + cross-spawn@^7.0.0, cross-spawn@^7.0.1, cross-spawn@^7.0.2, cross-spawn@^7.0.3: version "7.0.3" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" @@ -3329,6 +3336,14 @@ graphql-query-complexity@^0.12.0: dependencies: lodash.get "^4.4.2" +graphql-request@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/graphql-request/-/graphql-request-6.1.0.tgz#f4eb2107967af3c7a5907eb3131c671eac89be4f" + integrity sha512-p+XPfS4q7aIpKVcgmnZKhMNqhltk20hfXtkaIkTfjjmiKMJ5xrt5c743cL03y/K7y1rg3WrIC49xGiEQ4mxdNw== + dependencies: + "@graphql-typed-document-node/core" "^3.2.0" + cross-fetch "^3.1.5" + graphql-scalars@^1.22.2: version "1.22.2" resolved "https://registry.yarnpkg.com/graphql-scalars/-/graphql-scalars-1.22.2.tgz#6326e6fe2d0ad4228a9fea72a977e2bf26b86362" @@ -4266,6 +4281,11 @@ jiti@^1.19.3: resolved "https://registry.yarnpkg.com/jiti/-/jiti-1.20.0.tgz#2d823b5852ee8963585c8dd8b7992ffc1ae83b42" integrity sha512-3TV69ZbrvV6U5DfQimop50jE9Dl6J8O1ja1dvBbMba/sZ3YBEQqJ2VZRoQPVnhlzjNtU1vaXRZVrVjU4qtm8yA== +jose@^5.2.2: + version "5.2.2" + resolved "https://registry.yarnpkg.com/jose/-/jose-5.2.2.tgz#b91170e9ba6dbe609b0c0a86568f9a1fbe4335c0" + integrity sha512-/WByRr4jDcsKlvMd1dRJnPfS1GVO3WuKyaurJ/vvXcOaUQO8rnNObCQMlv/5uCceVQIq5Q4WLF44ohsdiTohdg== + js-tokens@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" @@ -4775,7 +4795,7 @@ node-abort-controller@^3.1.1: resolved "https://registry.yarnpkg.com/node-abort-controller/-/node-abort-controller-3.1.1.tgz#a94377e964a9a37ac3976d848cb5c765833b8548" integrity sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ== -node-fetch@^2.6.7: +node-fetch@^2.6.12, node-fetch@^2.6.7: version "2.7.0" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.7.0.tgz#d0f0fa6e3e2dc1d27efcd8ad99d550bda94d187d" integrity sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A== diff --git a/frontend/package.json b/frontend/package.json index 2a3eeb56a..e54ffa263 100755 --- a/frontend/package.json +++ b/frontend/package.json @@ -6,6 +6,7 @@ "start": "node run/server.js", "serve": "vue-cli-service serve --open", "build": "vue-cli-service build", + "postbuild": "find build -type f -regex '.*\\.\\(html\\|js\\|css\\|svg\\|json\\)' -exec gzip -9 -k {} +", "dev": "yarn run serve", "analyse-bundle": "yarn build && webpack-bundle-analyzer build/webpack.stats.json", "lint": "eslint --max-warnings=0 --ext .js,.vue,.json .",