diff --git a/CHANGELOG.md b/CHANGELOG.md index a728f21ea..bbb49eaf5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,8 +4,28 @@ All notable changes to this project will be documented in this file. Dates are d Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). +#### [2.2.0](https://github.com/gradido/gradido/compare/2.1.1...2.2.0) + +- feat(frontend): update news text [`#3279`](https://github.com/gradido/gradido/pull/3279) +- feat(frontend): use params instead of query for send/identifier route [`#3277`](https://github.com/gradido/gradido/pull/3277) +- fix(other): deployment bugfixes [`#3276`](https://github.com/gradido/gradido/pull/3276) +- refactor(other): federation optimize logging, fix bug [`#3272`](https://github.com/gradido/gradido/pull/3272) +- feat(other): request limit [`#3274`](https://github.com/gradido/gradido/pull/3274) +- feat(dlt): logging views [`#3270`](https://github.com/gradido/gradido/pull/3270) +- refactor(other): hetzner cloud deploy, refactor .env [`#3267`](https://github.com/gradido/gradido/pull/3267) +- refactor(dlt): dlt connector try out dci [`#3223`](https://github.com/gradido/gradido/pull/3223) +- feat(backend): fill linked_user_gradido_id and linked_user_name for creation transactions [`#3268`](https://github.com/gradido/gradido/pull/3268) +- feat(frontend): use day for contribution dates [`#3269`](https://github.com/gradido/gradido/pull/3269) +- feat(backend): fill linked_user_id for creation transactions [`#3266`](https://github.com/gradido/gradido/pull/3266) +- docs(other): 3258 feature create gms usecase docu [`#3260`](https://github.com/gradido/gradido/pull/3260) +- fix(frontend): update moderatorChangedMemo [`#3259`](https://github.com/gradido/gradido/pull/3259) +- chore(other): change filename date-pattern and stop all modules with db-write-access [`#3245`](https://github.com/gradido/gradido/pull/3245) + #### [2.1.1](https://github.com/gradido/gradido/compare/2.0.1...2.1.1) +> 1 December 2023 + +- chore(release): v2.1.1 [`#3257`](https://github.com/gradido/gradido/pull/3257) - feat(admin): wiedervorlage v2 [`#3255`](https://github.com/gradido/gradido/pull/3255) - feat(admin): resubmission [`#3252`](https://github.com/gradido/gradido/pull/3252) - feat(backend): grant moderator right to edit contribution memo [`#3233`](https://github.com/gradido/gradido/pull/3233) diff --git a/admin/package.json b/admin/package.json index 522fbd592..3f34c82c4 100644 --- a/admin/package.json +++ b/admin/package.json @@ -3,7 +3,7 @@ "description": "Administraion Interface for Gradido", "main": "index.js", "author": "Moriz Wahl", - "version": "2.1.1", + "version": "2.2.0", "license": "Apache-2.0", "private": false, "scripts": { diff --git a/backend/.env.dist b/backend/.env.dist index bdb3d3892..4ec60d856 100644 --- a/backend/.env.dist +++ b/backend/.env.dist @@ -63,4 +63,10 @@ WEBHOOK_ELOPAGE_SECRET=secret # Federation FEDERATION_VALIDATE_COMMUNITY_TIMER=60000 -FEDERATION_XCOM_SENDCOINS_ENABLED=false \ No newline at end of file +FEDERATION_XCOM_SENDCOINS_ENABLED=false + +# GMS +# GMS_ACTIVE=true +# Coordinates of Illuminz test instance +#GMS_URL=http://54.176.169.179:3071 +GMS_URL=http://localhost:4044/ diff --git a/backend/.env.template b/backend/.env.template index 5165dcef3..1cff23d5a 100644 --- a/backend/.env.template +++ b/backend/.env.template @@ -54,7 +54,7 @@ EMAIL_LINK_VERIFICATION_PATH=$EMAIL_LINK_VERIFICATION_PATH EMAIL_LINK_SETPASSWORD_PATH=$EMAIL_LINK_SETPASSWORD_PATH EMAIL_LINK_FORGOTPASSWORD_PATH=$EMAIL_LINK_FORGOTPASSWORD_PATH EMAIL_LINK_OVERVIEW_PATH=$EMAIL_LINK_OVERVIEW_PATH -EMAIL_CODE_VALID_TIME=$EMAIL_CODE_VALID_TIME_PATH +EMAIL_CODE_VALID_TIME=$EMAIL_CODE_VALID_TIME EMAIL_CODE_REQUEST_TIME=$EMAIL_CODE_REQUEST_TIME # Webhook @@ -62,4 +62,8 @@ WEBHOOK_ELOPAGE_SECRET=$WEBHOOK_ELOPAGE_SECRET # Federation FEDERATION_VALIDATE_COMMUNITY_TIMER=$FEDERATION_VALIDATE_COMMUNITY_TIMER -FEDERATION_XCOM_SENDCOINS_ENABLED=$FEDERATION_XCOM_SENDCOINS_ENABLED \ No newline at end of file +FEDERATION_XCOM_SENDCOINS_ENABLED=$FEDERATION_XCOM_SENDCOINS_ENABLED + +# GMS +GMS_ACTIVE=$GMS_ACTIVE +GMS_URL=$GMS_URL diff --git a/backend/package.json b/backend/package.json index 43b7fb87c..8f70ab11e 100644 --- a/backend/package.json +++ b/backend/package.json @@ -1,6 +1,6 @@ { "name": "gradido-backend", - "version": "2.1.1", + "version": "2.2.0", "description": "Gradido unified backend providing an API-Service for Gradido Transactions", "main": "src/index.ts", "repository": "https://github.com/gradido/gradido/backend", @@ -16,6 +16,8 @@ "test": "cross-env TZ=UTC NODE_ENV=development jest --runInBand --forceExit --detectOpenHandles", "seed": "cross-env TZ=UTC NODE_ENV=development ts-node -r tsconfig-paths/register src/seeds/index.ts", "klicktipp": "cross-env TZ=UTC NODE_ENV=development ts-node -r tsconfig-paths/register src/util/executeKlicktipp.ts", + "gmsusers": "cross-env TZ=UTC NODE_ENV=development ts-node -r tsconfig-paths/register src/seeds/gmsUsers.ts", + "gmsuserList": "cross-env TZ=UTC NODE_ENV=development ts-node -r tsconfig-paths/register src/seeds/gmsUserList.ts", "locales": "scripts/sort.sh" }, "dependencies": { diff --git a/backend/src/apis/gms/GmsClient.ts b/backend/src/apis/gms/GmsClient.ts new file mode 100644 index 000000000..46fa64006 --- /dev/null +++ b/backend/src/apis/gms/GmsClient.ts @@ -0,0 +1,146 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +import axios from 'axios' + +import { CONFIG } from '@/config' +import { LogError } from '@/server/LogError' +import { backendLogger as logger } from '@/server/logger' + +import { GmsUser } from './model/GmsUser' + +/* +export async function communityList(): Promise { + const baseUrl = CONFIG.GMS_URL.endsWith('/') ? CONFIG.GMS_URL : CONFIG.GMS_URL.concat('/') + const service = 'community/list?page=1&perPage=20' + const config = { + headers: { + accept: 'application/json', + language: 'en', + timezone: 'UTC', + connection: 'keep-alive', + authorization: + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJkYXRhIjoiVTJGc2RHVmtYMThuNzllbGJscThDbmxxZ0I2SGxicTZuajlpM2lmV3BTc3pHZFRtOFVTQjJZNWY2bG56elhuSUF0SEwvYVBWdE1uMjA3bnNtWDQ0M21xWVFyd0xJMklHNGtpRkZ3U2FKbVJwRk9VZXNDMXIyRGlta3VLMklwN1lYRTU0c2MzVmlScmMzaHE3djlFNkRabk4xeVMrU1QwRWVZRFI5c09pTDJCdmg4a05DNUc5NTdoZUJzeWlRbXcrNFFmMXFuUk5SNXpWdXhtZEE2WUUrT3hlcS85Y0d6NURyTmhoaHM3MTJZTFcvTmprZGNwdU55dUgxeWxhNEhJZyIsImlhdCI6MTcwMDUxMDg4OX0.WhtNGZc9A_hUfh8CcPjr44kWQWMkKJ7hlYXELOd3yy4', + }, + } + try { + const result = await axios.get(baseUrl.concat(service), config) + logger.debug('GET-Response of community/list:', result) + if (result.status !== 200) { + throw new LogError('HTTP Status Error in community/list:', result.status, result.statusText) + } + logger.debug('responseData:', result.data.responseData.data) + // eslint-disable-next-line @typescript-eslint/no-unsafe-call + // const gmsCom = JSON.parse(result.data.responseData.data) + // logger.debug('gmsCom:', gmsCom) + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return result.data.responseData.data + } catch (error: any) { + logger.error('Error in Get community/list:', error) + const errMsg: string = error.message + return errMsg + } +} + +export async function userList(): Promise { + const baseUrl = CONFIG.GMS_URL.endsWith('/') ? CONFIG.GMS_URL : CONFIG.GMS_URL.concat('/') + const service = 'community-user/list?page=1&perPage=20' + const config = { + headers: { + accept: 'application/json', + language: 'en', + timezone: 'UTC', + connection: 'keep-alive', + authorization: + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJkYXRhIjoiVTJGc2RHVmtYMThuNzllbGJscThDbmxxZ0I2SGxicTZuajlpM2lmV3BTc3pHZFRtOFVTQjJZNWY2bG56elhuSUF0SEwvYVBWdE1uMjA3bnNtWDQ0M21xWVFyd0xJMklHNGtpRkZ3U2FKbVJwRk9VZXNDMXIyRGlta3VLMklwN1lYRTU0c2MzVmlScmMzaHE3djlFNkRabk4xeVMrU1QwRWVZRFI5c09pTDJCdmg4a05DNUc5NTdoZUJzeWlRbXcrNFFmMXFuUk5SNXpWdXhtZEE2WUUrT3hlcS85Y0d6NURyTmhoaHM3MTJZTFcvTmprZGNwdU55dUgxeWxhNEhJZyIsImlhdCI6MTcwMDUxMDg4OX0.WhtNGZc9A_hUfh8CcPjr44kWQWMkKJ7hlYXELOd3yy4', + }, + } + try { + const result = await axios.get(baseUrl.concat(service), config) + logger.debug('GET-Response of community/list:', result) + if (result.status !== 200) { + throw new LogError( + 'HTTP Status Error in community-user/list:', + result.status, + result.statusText, + ) + } + logger.debug('responseData:', result.data.responseData.data) + // eslint-disable-next-line @typescript-eslint/no-unsafe-call + // const gmsUser = JSON.parse(result.data.responseData.data) + // logger.debug('gmsUser:', gmsUser) + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return result.data.responseData.data + } catch (error: any) { + logger.error('Error in Get community-user/list:', error) + const errMsg: string = error.message + return errMsg + } +} + +export async function userByUuid(uuid: string): Promise { + const baseUrl = CONFIG.GMS_URL.endsWith('/') ? CONFIG.GMS_URL : CONFIG.GMS_URL.concat('/') + const service = 'community-user/list?page=1&perPage=20' + const config = { + headers: { + accept: 'application/json', + language: 'en', + timezone: 'UTC', + connection: 'keep-alive', + authorization: + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJkYXRhIjoiVTJGc2RHVmtYMThuNzllbGJscThDbmxxZ0I2SGxicTZuajlpM2lmV3BTc3pHZFRtOFVTQjJZNWY2bG56elhuSUF0SEwvYVBWdE1uMjA3bnNtWDQ0M21xWVFyd0xJMklHNGtpRkZ3U2FKbVJwRk9VZXNDMXIyRGlta3VLMklwN1lYRTU0c2MzVmlScmMzaHE3djlFNkRabk4xeVMrU1QwRWVZRFI5c09pTDJCdmg4a05DNUc5NTdoZUJzeWlRbXcrNFFmMXFuUk5SNXpWdXhtZEE2WUUrT3hlcS85Y0d6NURyTmhoaHM3MTJZTFcvTmprZGNwdU55dUgxeWxhNEhJZyIsImlhdCI6MTcwMDUxMDg4OX0.WhtNGZc9A_hUfh8CcPjr44kWQWMkKJ7hlYXELOd3yy4', + }, + } + try { + const result = await axios.get(baseUrl.concat(service), config) + logger.debug('GET-Response of community/list:', result) + if (result.status !== 200) { + throw new LogError( + 'HTTP Status Error in community-user/list:', + result.status, + result.statusText, + ) + } + logger.debug('responseData:', result.data.responseData.data) + // eslint-disable-next-line @typescript-eslint/no-unsafe-call + // const gmsUser = JSON.parse(result.data.responseData.data) + // logger.debug('gmsUser:', gmsUser) + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return result.data.responseData.data + } catch (error: any) { + logger.error('Error in Get community-user/list:', error) + const errMsg: string = error.message + return errMsg + } +} +*/ + +export async function createGmsUser(apiKey: string, user: GmsUser): Promise { + const baseUrl = CONFIG.GMS_URL.endsWith('/') ? CONFIG.GMS_URL : CONFIG.GMS_URL.concat('/') + const service = 'community-user' + const config = { + headers: { + accept: 'application/json', + language: 'en', + timezone: 'UTC', + connection: 'keep-alive', + authorization: apiKey, + }, + } + try { + const result = await axios.post(baseUrl.concat(service), user, config) + logger.debug('POST-Response of community-user:', result) + if (result.status !== 200) { + throw new LogError('HTTP Status Error in community-user:', result.status, result.statusText) + } + logger.debug('responseData:', result.data.responseData) + // eslint-disable-next-line @typescript-eslint/no-unsafe-call + // const gmsUser = JSON.parse(result.data.responseData) + // logger.debug('gmsUser:', gmsUser) + return true + } catch (error: any) { + logger.error('Error in Get community-user:', error) + throw new LogError(error.message) + } +} diff --git a/backend/src/apis/gms/model/GmsCommunity.ts b/backend/src/apis/gms/model/GmsCommunity.ts new file mode 100644 index 000000000..76901f45f --- /dev/null +++ b/backend/src/apis/gms/model/GmsCommunity.ts @@ -0,0 +1,19 @@ +/* +import { GmsCommunityProfile } from './GmsCommunityProfile' +import { GmsRole } from './GmsRoles' + +export class GmsCommunity { + id: number + uuid: string + communityUuid: string + email: string + countryCode: string + mobile: string + status: number + createdAt: Date + updatedAt: Date + UserProfile: unknown + communityProfile: GmsCommunityProfile + roles: GmsRole[] +} +*/ diff --git a/backend/src/apis/gms/model/GmsCommunityProfile.ts b/backend/src/apis/gms/model/GmsCommunityProfile.ts new file mode 100644 index 000000000..c5e80caa8 --- /dev/null +++ b/backend/src/apis/gms/model/GmsCommunityProfile.ts @@ -0,0 +1,18 @@ +/* +export class GmsCommunityProfile { + name: string + location: { + type: string + coordinates: [number] + } + + address: string + communityId: number + radius: number + description: string + // eslint-disable-next-line camelcase + api_key: string + communityAuthUrl: unknown + profileImage: unknown +} +*/ diff --git a/backend/src/apis/gms/model/GmsRoles.ts b/backend/src/apis/gms/model/GmsRoles.ts new file mode 100644 index 000000000..a3b275f0c --- /dev/null +++ b/backend/src/apis/gms/model/GmsRoles.ts @@ -0,0 +1,8 @@ +/* +export class GmsRole { + code: string + status: number + name: string + Permissions: [unknown] +} +*/ diff --git a/backend/src/apis/gms/model/GmsUser.ts b/backend/src/apis/gms/model/GmsUser.ts new file mode 100644 index 000000000..7f7db7660 --- /dev/null +++ b/backend/src/apis/gms/model/GmsUser.ts @@ -0,0 +1,110 @@ +import { User as dbUser } from '@entity/User' + +import { GmsPublishLocationType } from '@/graphql/enum/GmsPublishLocationType' +import { GmsPublishNameType } from '@/graphql/enum/GmsPublishNameType' +import { GmsPublishPhoneType } from '@/graphql/enum/GmsPublishPhoneType' + +export class GmsUser { + constructor(user: dbUser) { + this.userUuid = user.gradidoID + // this.communityUuid = user.communityUuid + this.email = this.getGmsEmail(user) + this.countryCode = this.getGmsCountryCode(user) + this.mobile = this.getGmsPhone(user) + this.firstName = this.getGmsFirstName(user) + this.lastName = this.getGmsLastName(user) + this.alias = this.getGmsAlias(user) + this.type = GmsPublishLocationType.GMS_LOCATION_TYPE_RANDOM + this.location = null + } + + id: number + userUuid: string + communityUuid: string + email: string | undefined + countryCode: string | undefined + mobile: string | undefined + status: number + createdAt: Date + updatedAt: Date + firstName: string | undefined + lastName: string | undefined + alias: string | undefined + type: number + address: string | undefined + city: string | undefined + state: string + country: string | undefined + zipCode: string | undefined + language: string + location: unknown + + private getGmsAlias(user: dbUser): string | undefined { + if ( + user.gmsAllowed && + user.alias && + user.gmsPublishName === GmsPublishNameType.GMS_PUBLISH_NAME_ALIAS_OR_INITALS + ) { + return user.alias + } + } + + private getGmsFirstName(user: dbUser): string | undefined { + if ( + user.gmsAllowed && + (user.gmsPublishName === GmsPublishNameType.GMS_PUBLISH_NAME_FIRST || + user.gmsPublishName === GmsPublishNameType.GMS_PUBLISH_NAME_FIRST_INITIAL || + user.gmsPublishName === GmsPublishNameType.GMS_PUBLISH_NAME_FULL) + ) { + return user.firstName + } + if ( + user.gmsAllowed && + ((!user.alias && + user.gmsPublishName === GmsPublishNameType.GMS_PUBLISH_NAME_ALIAS_OR_INITALS) || + user.gmsPublishName === GmsPublishNameType.GMS_PUBLISH_NAME_INITIALS) + ) { + return user.firstName.substring(0, 1) + } + } + + private getGmsLastName(user: dbUser): string | undefined { + if (user.gmsAllowed && user.gmsPublishName === GmsPublishNameType.GMS_PUBLISH_NAME_FULL) { + return user.lastName + } + if ( + user.gmsAllowed && + ((!user.alias && + user.gmsPublishName === GmsPublishNameType.GMS_PUBLISH_NAME_ALIAS_OR_INITALS) || + user.gmsPublishName === GmsPublishNameType.GMS_PUBLISH_NAME_FIRST_INITIAL || + user.gmsPublishName === GmsPublishNameType.GMS_PUBLISH_NAME_INITIALS) + ) { + return user.lastName.substring(0, 1) + } + } + + private getGmsEmail(user: dbUser): string | undefined { + if (user.gmsAllowed && user.emailContact.gmsPublishEmail) { + return user.emailContact.email + } + } + + private getGmsCountryCode(user: dbUser): string | undefined { + if ( + user.gmsAllowed && + (user.emailContact.gmsPublishPhone === GmsPublishPhoneType.GMS_PUBLISH_PHONE_COUNTRY || + user.emailContact.gmsPublishPhone === GmsPublishPhoneType.GMS_PUBLISH_PHONE_FULL) + ) { + return user.emailContact.countryCode + } + } + + private getGmsPhone(user: dbUser): string | undefined { + if ( + user.gmsAllowed && + user.emailContact.gmsPublishPhone === GmsPublishPhoneType.GMS_PUBLISH_PHONE_FULL + ) { + return user.emailContact.phone + } + } +} diff --git a/backend/src/apis/gms/model/GmsUserAccount.ts b/backend/src/apis/gms/model/GmsUserAccount.ts new file mode 100644 index 000000000..8d6b3652c --- /dev/null +++ b/backend/src/apis/gms/model/GmsUserAccount.ts @@ -0,0 +1,18 @@ +/* +import { Decimal } from 'decimal.js-light' + +export class GmsUserAccount { + name: string + location: { + type: string + coordinates: [Decimal, Decimal] + } + + address: string + radius: number + description: string + // eslint-disable-next-line camelcase + api_key: string + profileImage: unknown +} +*/ diff --git a/backend/src/apis/gms/model/GmsUserProfile.ts b/backend/src/apis/gms/model/GmsUserProfile.ts new file mode 100644 index 000000000..ed3372610 --- /dev/null +++ b/backend/src/apis/gms/model/GmsUserProfile.ts @@ -0,0 +1,24 @@ +/* +import { Decimal } from 'decimal.js-light' + +export class GmsUserProfile { + firstName: string | undefined + lastName: string | undefined + alias: string + type: number + name: string | undefined + location: { + type: string + coordinates: [Decimal, Decimal] + } + + accuracy: unknown + address: string | undefined + city: string | undefined + state: string + country: string | undefined + zipCode: string | undefined + language: string + profileImage: unknown +} +*/ diff --git a/backend/src/auth/ADMIN_RIGHTS.ts b/backend/src/auth/ADMIN_RIGHTS.ts index b81ff51d6..79006a1de 100644 --- a/backend/src/auth/ADMIN_RIGHTS.ts +++ b/backend/src/auth/ADMIN_RIGHTS.ts @@ -1,3 +1,9 @@ import { RIGHTS } from './RIGHTS' -export const ADMIN_RIGHTS = [RIGHTS.SET_USER_ROLE, RIGHTS.DELETE_USER, RIGHTS.UNDELETE_USER] +export const ADMIN_RIGHTS = [ + RIGHTS.SET_USER_ROLE, + RIGHTS.DELETE_USER, + RIGHTS.UNDELETE_USER, + RIGHTS.COMMUNITY_UPDATE, + RIGHTS.COMMUNITY_BY_UUID, +] diff --git a/backend/src/auth/RIGHTS.ts b/backend/src/auth/RIGHTS.ts index 0f6a4c00c..c95aa18fd 100644 --- a/backend/src/auth/RIGHTS.ts +++ b/backend/src/auth/RIGHTS.ts @@ -58,4 +58,6 @@ export enum RIGHTS { SET_USER_ROLE = 'SET_USER_ROLE', DELETE_USER = 'DELETE_USER', UNDELETE_USER = 'UNDELETE_USER', + COMMUNITY_BY_UUID = 'COMMUNITY_BY_UUID', + COMMUNITY_UPDATE = 'COMMUNITY_UPDATE', } diff --git a/backend/src/config/index.ts b/backend/src/config/index.ts index ee90261f4..b94178157 100644 --- a/backend/src/config/index.ts +++ b/backend/src/config/index.ts @@ -12,7 +12,7 @@ Decimal.set({ }) const constants = { - DB_VERSION: '0081-user_join_community', + DB_VERSION: '0082-introduce_gms_registration', DECAY_START_TIME: new Date('2021-05-13 17:46:31-0000'), // GMT+0 LOG4JS_CONFIG: 'log4js-config.json', // default log level on production should be info @@ -139,6 +139,12 @@ const federation = { process.env.FEDERATION_XCOM_MAXREPEAT_REVERTSENDCOINS ?? 3, } +const gms = { + GMS_ACTIVE: process.env.GMS_ACTIVE === 'true' || false, + // koordinates of Illuminz-instance of GMS + GMS_URL: process.env.GMS_HOST ?? 'http://localhost:4044/', +} + export const CONFIG = { ...constants, ...server, @@ -150,4 +156,5 @@ export const CONFIG = { ...loginServer, ...webhook, ...federation, + ...gms, } diff --git a/backend/src/graphql/arg/CommunityArgs.ts b/backend/src/graphql/arg/CommunityArgs.ts new file mode 100644 index 000000000..163a6e504 --- /dev/null +++ b/backend/src/graphql/arg/CommunityArgs.ts @@ -0,0 +1,14 @@ +import { IsString } from 'class-validator' +import { Field, ArgsType, InputType } from 'type-graphql' + +@InputType() +@ArgsType() +export class CommunityArgs { + @Field(() => String) + @IsString() + uuid: string + + @Field(() => String) + @IsString() + gmsApiKey: string +} diff --git a/backend/src/graphql/arg/UpdateUserInfosArgs.ts b/backend/src/graphql/arg/UpdateUserInfosArgs.ts index 6b2ab1032..0920fb3bc 100644 --- a/backend/src/graphql/arg/UpdateUserInfosArgs.ts +++ b/backend/src/graphql/arg/UpdateUserInfosArgs.ts @@ -1,6 +1,11 @@ import { IsBoolean, IsInt, IsString } from 'class-validator' -import { ArgsType, Field, Int } from 'type-graphql' +import { ArgsType, Field, InputType, Int } from 'type-graphql' +import { Location } from '@model/Location' + +import { isValidLocation } from '@/graphql/validator/Location' + +@InputType() @ArgsType() export class UpdateUserInfosArgs { @Field({ nullable: true }) @@ -38,4 +43,20 @@ export class UpdateUserInfosArgs { @Field({ nullable: true }) @IsBoolean() hideAmountGDT?: boolean + + @Field({ nullable: true, defaultValue: true }) + @IsBoolean() + gmsAllowed?: boolean + + @Field(() => Int, { nullable: true, defaultValue: 0 }) + @IsInt() + gmsPublishName?: number | null + + @Field(() => Location, { nullable: true }) + @isValidLocation() + gmsLocation?: Location | null + + @Field(() => Int, { nullable: true, defaultValue: 2 }) + @IsInt() + gmsPublishLocation?: number | null } diff --git a/backend/src/graphql/enum/GmsPublishLocationType.ts b/backend/src/graphql/enum/GmsPublishLocationType.ts new file mode 100644 index 000000000..afb9c246d --- /dev/null +++ b/backend/src/graphql/enum/GmsPublishLocationType.ts @@ -0,0 +1,12 @@ +import { registerEnumType } from 'type-graphql' + +export enum GmsPublishLocationType { + GMS_LOCATION_TYPE_EXACT = 0, + GMS_LOCATION_TYPE_APPROXIMATE = 1, + GMS_LOCATION_TYPE_RANDOM = 2, +} + +registerEnumType(GmsPublishLocationType, { + name: 'GmsPublishLocationType', // this one is mandatory + description: 'Type of location treatment in GMS', // this one is optional +}) diff --git a/backend/src/graphql/enum/GmsPublishNameType.ts b/backend/src/graphql/enum/GmsPublishNameType.ts new file mode 100644 index 000000000..08aaaf8ef --- /dev/null +++ b/backend/src/graphql/enum/GmsPublishNameType.ts @@ -0,0 +1,14 @@ +import { registerEnumType } from 'type-graphql' + +export enum GmsPublishNameType { + GMS_PUBLISH_NAME_ALIAS_OR_INITALS = 0, + GMS_PUBLISH_NAME_INITIALS = 1, + GMS_PUBLISH_NAME_FIRST = 2, + GMS_PUBLISH_NAME_FIRST_INITIAL = 3, + GMS_PUBLISH_NAME_FULL = 4, +} + +registerEnumType(GmsPublishNameType, { + name: 'GmsPublishNameType', // this one is mandatory + description: 'Type of name publishing', // this one is optional +}) diff --git a/backend/src/graphql/enum/GmsPublishPhoneType.ts b/backend/src/graphql/enum/GmsPublishPhoneType.ts new file mode 100644 index 000000000..a9821d8ff --- /dev/null +++ b/backend/src/graphql/enum/GmsPublishPhoneType.ts @@ -0,0 +1,12 @@ +import { registerEnumType } from 'type-graphql' + +export enum GmsPublishPhoneType { + GMS_PUBLISH_PHONE_NOTHING = 0, + GMS_PUBLISH_PHONE_COUNTRY = 1, + GMS_PUBLISH_PHONE_FULL = 2, +} + +registerEnumType(GmsPublishPhoneType, { + name: 'GmsPublishPhoneType', // this one is mandatory + description: 'Type of Phone publishing', // this one is optional +}) diff --git a/backend/src/graphql/model/Community.ts b/backend/src/graphql/model/Community.ts index 43e0a7108..06c07875e 100644 --- a/backend/src/graphql/model/Community.ts +++ b/backend/src/graphql/model/Community.ts @@ -12,6 +12,7 @@ export class Community { this.creationDate = dbCom.creationDate this.uuid = dbCom.communityUuid this.authenticatedAt = dbCom.authenticatedAt + this.gmsApiKey = dbCom.gmsApiKey } @Field(() => Int) @@ -37,4 +38,7 @@ export class Community { @Field(() => Date, { nullable: true }) authenticatedAt: Date | null + + @Field(() => String, { nullable: true }) + gmsApiKey: string | null } diff --git a/backend/src/graphql/model/Location.ts b/backend/src/graphql/model/Location.ts new file mode 100644 index 000000000..2b4c720f4 --- /dev/null +++ b/backend/src/graphql/model/Location.ts @@ -0,0 +1,10 @@ +import { ArgsType, Field, Int } from 'type-graphql' + +@ArgsType() +export class Location { + @Field(() => Int) + longitude: number + + @Field(() => Int) + latitude: number +} diff --git a/backend/src/graphql/resolver/CommunityResolver.test.ts b/backend/src/graphql/resolver/CommunityResolver.test.ts index e0cdc06fa..e720eb716 100644 --- a/backend/src/graphql/resolver/CommunityResolver.test.ts +++ b/backend/src/graphql/resolver/CommunityResolver.test.ts @@ -9,21 +9,38 @@ import { Connection } from '@dbTools/typeorm' 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 { cleanDB, testEnvironment } from '@test/helpers' +import { logger, i18n as localization } from '@test/testSetup' -import { getCommunities, communities } from '@/seeds/graphql/queries' +import { userFactory } from '@/seeds/factory/user' +import { login, updateHomeCommunityQuery } from '@/seeds/graphql/mutations' +import { getCommunities, communitiesQuery, getCommunityByUuidQuery } from '@/seeds/graphql/queries' +import { peterLustig } from '@/seeds/users/peter-lustig' + +import { getCommunityByUuid } from './util/communities' // to do: We need a setup for the tests that closes the connection -let query: ApolloServerTestClient['query'], con: Connection +let mutate: ApolloServerTestClient['mutate'], + query: ApolloServerTestClient['query'], + con: Connection + let testEnv: { mutate: ApolloServerTestClient['mutate'] query: ApolloServerTestClient['query'] con: Connection } +const peterLoginData = { + email: 'peter@lustig.de', + password: 'Aa12345_', + publisherId: 1234, +} + beforeAll(async () => { - testEnv = await testEnvironment() + testEnv = await testEnvironment(logger, localization) + mutate = testEnv.mutate query = testEnv.query con = testEnv.con await DbFederatedCommunity.clear() @@ -302,7 +319,7 @@ describe('CommunityResolver', () => { it('returns no community entry', async () => { // const result: Community[] = await query({ query: getCommunities }) // expect(result.length).toEqual(0) - await expect(query({ query: communities })).resolves.toMatchObject({ + await expect(query({ query: communitiesQuery })).resolves.toMatchObject({ data: { communities: [], }, @@ -329,7 +346,7 @@ describe('CommunityResolver', () => { }) it('returns 1 home-community entry', async () => { - await expect(query({ query: communities })).resolves.toMatchObject({ + await expect(query({ query: communitiesQuery })).resolves.toMatchObject({ data: { communities: [ { @@ -391,7 +408,7 @@ describe('CommunityResolver', () => { }) it('returns 2 community entries', async () => { - await expect(query({ query: communities })).resolves.toMatchObject({ + await expect(query({ query: communitiesQuery })).resolves.toMatchObject({ data: { communities: [ { @@ -431,5 +448,129 @@ describe('CommunityResolver', () => { }) }) }) + + describe('search community by uuid', () => { + let homeCom: DbCommunity | null + beforeEach(async () => { + await cleanDB() + jest.clearAllMocks() + const admin = await userFactory(testEnv, peterLustig) + // login as admin + await mutate({ mutation: login, variables: peterLoginData }) + + // HomeCommunity is still created in userFactory + homeCom = await getCommunityByUuid(admin.communityUuid) + + foreignCom1 = DbCommunity.create() + foreignCom1.foreign = true + foreignCom1.url = 'http://stage-2.gradido.net/api' + foreignCom1.publicKey = Buffer.from('publicKey-stage-2_Community') + foreignCom1.privateKey = Buffer.from('privateKey-stage-2_Community') + // foreignCom1.communityUuid = 'Stage2-Com-UUID' + // foreignCom1.authenticatedAt = new Date() + foreignCom1.name = 'Stage-2_Community-name' + foreignCom1.description = 'Stage-2_Community-description' + foreignCom1.creationDate = new Date() + await DbCommunity.insert(foreignCom1) + + foreignCom2 = DbCommunity.create() + foreignCom2.foreign = true + foreignCom2.url = 'http://stage-3.gradido.net/api' + foreignCom2.publicKey = Buffer.from('publicKey-stage-3_Community') + foreignCom2.privateKey = Buffer.from('privateKey-stage-3_Community') + foreignCom2.communityUuid = 'Stage3-Com-UUID' + foreignCom2.authenticatedAt = new Date() + foreignCom2.name = 'Stage-3_Community-name' + foreignCom2.description = 'Stage-3_Community-description' + foreignCom2.creationDate = new Date() + await DbCommunity.insert(foreignCom2) + }) + + it('finds the home-community', async () => { + await expect( + query({ + query: getCommunityByUuidQuery, + variables: { communityUuid: homeCom?.communityUuid }, + }), + ).resolves.toMatchObject({ + data: { + community: { + 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('updates the home-community gmsApiKey', async () => { + await expect( + mutate({ + mutation: updateHomeCommunityQuery, + variables: { uuid: homeCom?.communityUuid, gmsApiKey: 'gmsApiKey' }, + }), + ).resolves.toMatchObject({ + data: { + updateHomeCommunity: { + id: expect.any(Number), + foreign: homeCom?.foreign, + name: homeCom?.name, + description: homeCom?.description, + url: homeCom?.url, + creationDate: homeCom?.creationDate?.toISOString(), + uuid: homeCom?.communityUuid, + authenticatedAt: homeCom?.authenticatedAt, + gmsApiKey: 'gmsApiKey', + }, + }, + }) + }) + + it('throws error on updating a foreign-community', async () => { + expect( + await mutate({ + mutation: updateHomeCommunityQuery, + variables: { uuid: foreignCom2.communityUuid, gmsApiKey: 'gmsApiKey' }, + }), + ).toEqual( + expect.objectContaining({ + errors: [new GraphQLError('Error: Only the HomeCommunity could be modified!')], + }), + ) + }) + + it('throws error on updating a community without uuid', async () => { + expect( + await mutate({ + mutation: updateHomeCommunityQuery, + variables: { uuid: null, gmsApiKey: 'gmsApiKey' }, + }), + ).toEqual( + expect.objectContaining({ + errors: [ + new GraphQLError(`Variable "$uuid" of non-null type "String!" must not be null.`), + ], + }), + ) + }) + + it('throws error on updating a community with not existing uuid', async () => { + expect( + await mutate({ + mutation: updateHomeCommunityQuery, + variables: { uuid: 'unknownUuid', gmsApiKey: 'gmsApiKey' }, + }), + ).toEqual( + expect.objectContaining({ + errors: [new GraphQLError('HomeCommunity with uuid not found: ')], + }), + ) + }) + }) }) }) diff --git a/backend/src/graphql/resolver/CommunityResolver.ts b/backend/src/graphql/resolver/CommunityResolver.ts index 989dc74bf..760b982cc 100644 --- a/backend/src/graphql/resolver/CommunityResolver.ts +++ b/backend/src/graphql/resolver/CommunityResolver.ts @@ -1,15 +1,16 @@ 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 } from 'type-graphql' +import { Resolver, Query, Authorized, Arg, Mutation, Args } from 'type-graphql' +import { CommunityArgs } from '@arg//CommunityArgs' import { Community } from '@model/Community' import { FederatedCommunity } from '@model/FederatedCommunity' import { RIGHTS } from '@/auth/RIGHTS' import { LogError } from '@/server/LogError' -import { getCommunity } from './util/communities' +import { getCommunityByUuid } from './util/communities' @Resolver() export class CommunityResolver { @@ -40,13 +41,41 @@ export class CommunityResolver { return dbCommunities.map((dbCom: DbCommunity) => new Community(dbCom)) } - @Authorized([RIGHTS.COMMUNITIES]) + @Authorized([RIGHTS.COMMUNITY_BY_UUID]) @Query(() => Community) async community(@Arg('communityUuid') communityUuid: string): Promise { - const community = await getCommunity(communityUuid) - if (!community) { + const com: DbCommunity | null = await getCommunityByUuid(communityUuid) + if (!com) { throw new LogError('community not found', communityUuid) } - return new Community(community) + return new Community(com) + } + + @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!`) + } + return com } } diff --git a/backend/src/graphql/resolver/TransactionResolver.test.ts b/backend/src/graphql/resolver/TransactionResolver.test.ts index 97e210dfa..1bb4a86f7 100644 --- a/backend/src/graphql/resolver/TransactionResolver.test.ts +++ b/backend/src/graphql/resolver/TransactionResolver.test.ts @@ -15,6 +15,8 @@ import { ApolloServerTestClient } from 'apollo-server-testing' import { GraphQLError } from 'graphql' import { v4 as uuidv4 } from 'uuid' +import { GmsPublishLocationType } from '@enum/GmsPublishLocationType' +import { GmsPublishNameType } from '@enum/GmsPublishNameType' import { cleanDB, testEnvironment } from '@test/helpers' import { logger } from '@test/testSetup' @@ -532,6 +534,9 @@ describe('send coins', () => { mutation: updateUserInfos, variables: { alias: 'bob', + gmsAllowed: true, + gmsPublishName: GmsPublishNameType.GMS_PUBLISH_NAME_ALIAS_OR_INITALS, + gmsPublishLocation: GmsPublishLocationType.GMS_LOCATION_TYPE_RANDOM, }, }) await mutate({ diff --git a/backend/src/graphql/resolver/TransactionResolver.ts b/backend/src/graphql/resolver/TransactionResolver.ts index 00894ecd3..ce1ba43da 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 { getCommunity, getCommunityName, isHomeCommunity } from './util/communities' +import { getCommunityByUuid, 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 getCommunity(recipientCommunityIdentifier) + const recipCom = await getCommunityByUuid(recipientCommunityIdentifier) logger.debug('recipient commuity: ', recipCom) if (recipCom === null) { throw new LogError( diff --git a/backend/src/graphql/resolver/UserResolver.test.ts b/backend/src/graphql/resolver/UserResolver.test.ts index d8df20585..c570dbd9f 100644 --- a/backend/src/graphql/resolver/UserResolver.test.ts +++ b/backend/src/graphql/resolver/UserResolver.test.ts @@ -16,11 +16,14 @@ import { ApolloServerTestClient } from 'apollo-server-testing' import { GraphQLError } from 'graphql' import { v4 as uuidv4, validate as validateUUID, version as versionUUID } from 'uuid' +import { GmsPublishLocationType } from '@enum/GmsPublishLocationType' +import { GmsPublishNameType } from '@enum/GmsPublishNameType' import { OptInType } from '@enum/OptInType' import { PasswordEncryptionType } from '@enum/PasswordEncryptionType' import { RoleNames } from '@enum/RoleNames' import { UserContactType } from '@enum/UserContactType' import { ContributionLink } from '@model/ContributionLink' +import { Location } from '@model/Location' import { testEnvironment, headerPushMock, resetToken, cleanDB } from '@test/helpers' import { logger, i18n as localization } from '@test/testSetup' @@ -68,6 +71,8 @@ import { stephenHawking } from '@/seeds/users/stephen-hawking' import { printTimeDuration } from '@/util/time' import { objectValuesToArray } from '@/util/utilities' +import { Location2Point } from './util/Location2Point' + jest.mock('@/emails/sendEmailVariants', () => { const originalModule = jest.requireActual('@/emails/sendEmailVariants') return { @@ -177,6 +182,12 @@ describe('UserResolver', () => { passwordEncryptionType: PasswordEncryptionType.NO_PASSWORD, communityUuid: homeCom.communityUuid, foreign: false, + gmsAllowed: true, + gmsPublishName: 0, + gmsPublishLocation: 2, + location: null, + gmsRegistered: false, + gmsRegisteredAt: null, }, ]) const valUUID = validateUUID(user[0].gradidoID) @@ -195,10 +206,13 @@ describe('UserResolver', () => { emailVerificationCode: expect.any(String), emailOptInTypeId: OptInType.EMAIL_OPT_IN_REGISTER, emailResendCount: 0, + countryCode: null, phone: null, createdAt: expect.any(Date), deletedAt: null, updatedAt: null, + gmsPublishEmail: false, + gmsPublishPhone: 0, }) }) }) @@ -1156,7 +1170,12 @@ describe('UserResolver', () => { it('throws an error', async () => { jest.clearAllMocks() resetToken() - await expect(mutate({ mutation: updateUserInfos })).resolves.toEqual( + await expect( + mutate({ + mutation: updateUserInfos, + variables: {}, + }), + ).resolves.toEqual( expect.objectContaining({ errors: [new GraphQLError('401 Unauthorized')], }), @@ -1181,7 +1200,12 @@ describe('UserResolver', () => { }) it('returns true', async () => { - await expect(mutate({ mutation: updateUserInfos })).resolves.toEqual( + await expect( + mutate({ + mutation: updateUserInfos, + variables: {}, + }), + ).resolves.toEqual( expect.objectContaining({ data: { updateUserInfos: true, @@ -1205,6 +1229,9 @@ describe('UserResolver', () => { firstName: 'Benjamin', lastName: 'Blümchen', language: 'en', + gmsAllowed: true, + gmsPublishName: GmsPublishNameType.GMS_PUBLISH_NAME_ALIAS_OR_INITALS, + gmsPublishLocation: GmsPublishLocationType.GMS_LOCATION_TYPE_RANDOM, }), ]) }) @@ -1240,6 +1267,76 @@ describe('UserResolver', () => { await expect(User.find()).resolves.toEqual([ expect.objectContaining({ alias: 'bibi_Bloxberg', + gmsAllowed: true, + gmsPublishName: GmsPublishNameType.GMS_PUBLISH_NAME_ALIAS_OR_INITALS, + gmsPublishLocation: GmsPublishLocationType.GMS_LOCATION_TYPE_RANDOM, + }), + ]) + }) + }) + }) + + describe('gms attributes', () => { + beforeEach(() => { + jest.clearAllMocks() + }) + + describe('default settings', () => { + it('updates the user in DB', async () => { + await mutate({ + mutation: updateUserInfos, + variables: {}, + }) + await expect(User.find()).resolves.toEqual([ + expect.objectContaining({ + gmsAllowed: true, + gmsPublishName: GmsPublishNameType.GMS_PUBLISH_NAME_ALIAS_OR_INITALS, + gmsPublishLocation: GmsPublishLocationType.GMS_LOCATION_TYPE_RANDOM, + }), + ]) + }) + }) + + describe('individual settings', () => { + it('updates the user in DB', async () => { + await mutate({ + mutation: updateUserInfos, + variables: { + gmsAllowed: false, + gmsPublishName: GmsPublishNameType.GMS_PUBLISH_NAME_FIRST_INITIAL, + gmsPublishLocation: GmsPublishLocationType.GMS_LOCATION_TYPE_APPROXIMATE, + }, + }) + await expect(User.find()).resolves.toEqual([ + expect.objectContaining({ + gmsAllowed: false, + gmsPublishName: GmsPublishNameType.GMS_PUBLISH_NAME_FIRST_INITIAL, + gmsPublishLocation: GmsPublishLocationType.GMS_LOCATION_TYPE_APPROXIMATE, + }), + ]) + }) + }) + + describe('with gms location', () => { + const loc = new Location() + loc.longitude = 9.573224 + loc.latitude = 49.679437 + it('updates the user in DB', async () => { + await mutate({ + mutation: updateUserInfos, + variables: { + gmsAllowed: true, + gmsPublishName: GmsPublishNameType.GMS_PUBLISH_NAME_ALIAS_OR_INITALS, + gmsLocation: loc, + gmsPublishLocation: GmsPublishLocationType.GMS_LOCATION_TYPE_RANDOM, + }, + }) + await expect(User.find()).resolves.toEqual([ + expect.objectContaining({ + gmsAllowed: true, + gmsPublishName: GmsPublishNameType.GMS_PUBLISH_NAME_ALIAS_OR_INITALS, + location: Location2Point(loc), + gmsPublishLocation: GmsPublishLocationType.GMS_LOCATION_TYPE_RANDOM, }), ]) }) @@ -2577,6 +2674,9 @@ describe('UserResolver', () => { mutation: updateUserInfos, variables: { alias: 'bibi', + gmsAllowed: true, + gmsPublishName: GmsPublishNameType.GMS_PUBLISH_NAME_ALIAS_OR_INITALS, + gmsPublishLocation: GmsPublishLocationType.GMS_LOCATION_TYPE_RANDOM, }, }) }) diff --git a/backend/src/graphql/resolver/UserResolver.ts b/backend/src/graphql/resolver/UserResolver.ts index e3b323f8a..3f70ce112 100644 --- a/backend/src/graphql/resolver/UserResolver.ts +++ b/backend/src/graphql/resolver/UserResolver.ts @@ -19,11 +19,14 @@ import { SearchUsersFilters } from '@arg/SearchUsersFilters' import { SetUserRoleArgs } from '@arg/SetUserRoleArgs' import { UnsecureLoginArgs } from '@arg/UnsecureLoginArgs' import { UpdateUserInfosArgs } from '@arg/UpdateUserInfosArgs' +import { GmsPublishLocationType } from '@enum/GmsPublishLocationType' +import { GmsPublishNameType } from '@enum/GmsPublishNameType' import { OptInType } from '@enum/OptInType' import { Order } from '@enum/Order' import { PasswordEncryptionType } from '@enum/PasswordEncryptionType' import { UserContactType } from '@enum/UserContactType' import { SearchAdminUsersResult } from '@model/AdminUser' +// import { Location } from '@model/Location' import { User } from '@model/User' import { UserAdmin, SearchUsersResult } from '@model/UserAdmin' @@ -70,7 +73,9 @@ import { getUserCreations } from './util/creations' import { findUserByIdentifier } from './util/findUserByIdentifier' import { findUsers } from './util/findUsers' import { getKlicktippState } from './util/getKlicktippState' +import { Location2Point } from './util/Location2Point' import { setUserRole, deleteUserRole } from './util/modifyUserRole' +import { sendUserToGms } from './util/sendUserToGms' import { validateAlias } from './util/validateAlias' const LANGUAGES = ['de', 'en', 'es', 'fr', 'nl'] @@ -361,6 +366,18 @@ export class UserResolver { } else { await EVENT_USER_REGISTER(dbUser) } + + if (!CONFIG.GMS_ACTIVE) { + logger.info('GMS deactivated per configuration! New user is not published to GMS.') + } else { + try { + if (dbUser.gmsAllowed && !dbUser.gmsRegistered) { + await sendUserToGms(dbUser, homeCom) + } + } catch (err) { + logger.error('Error publishing new created user to GMS:', err) + } + } return new User(dbUser) } @@ -534,12 +551,29 @@ export class UserResolver { passwordNew, hideAmountGDD, hideAmountGDT, + gmsAllowed, + gmsPublishName, + gmsLocation, + gmsPublishLocation, }: UpdateUserInfosArgs, @Ctx() context: Context, ): Promise { - logger.info(`updateUserInfos(${firstName}, ${lastName}, ${language}, ***, ***)...`) - const user = getUser(context) + logger.info( + `updateUserInfos(${firstName}, ${lastName}, ${alias}, ${language}, ***, ***, ${hideAmountGDD}, ${hideAmountGDT}, ${gmsAllowed}, ${gmsPublishName}, ${gmsLocation}, ${gmsPublishLocation})...`, + ) + // check default arg settings + if (gmsAllowed === null || gmsAllowed === undefined) { + gmsAllowed = true + } + if (!gmsPublishName) { + gmsPublishName = GmsPublishNameType.GMS_PUBLISH_NAME_ALIAS_OR_INITALS + } + if (!gmsPublishLocation) { + gmsPublishLocation = GmsPublishLocationType.GMS_LOCATION_TYPE_RANDOM + } + const user = getUser(context) + // try { if (firstName) { user.firstName = firstName } @@ -586,6 +620,15 @@ export class UserResolver { user.hideAmountGDT = hideAmountGDT } + user.gmsAllowed = gmsAllowed + user.gmsPublishName = gmsPublishName + if (gmsLocation) { + user.location = Location2Point(gmsLocation) + } + user.gmsPublishLocation = gmsPublishLocation + // } catch (err) { + // console.log('error:', err) + // } const queryRunner = getConnection().createQueryRunner() await queryRunner.connect() await queryRunner.startTransaction('REPEATABLE READ') @@ -596,7 +639,7 @@ export class UserResolver { }) await queryRunner.commitTransaction() - logger.debug('writing User data successful...') + logger.debug('writing User data successful...', user) } catch (e) { await queryRunner.rollbackTransaction() throw new LogError('Error on writing updated user data', e) diff --git a/backend/src/graphql/resolver/util/Location2Point.ts b/backend/src/graphql/resolver/util/Location2Point.ts new file mode 100644 index 000000000..df40c034a --- /dev/null +++ b/backend/src/graphql/resolver/util/Location2Point.ts @@ -0,0 +1,27 @@ +import { Point } from '@dbTools/typeorm' + +import { Location } from '@model/Location' + +export function Location2Point(location: Location): Point { + let pointStr: string + if (location.longitude && location.latitude) { + pointStr = '{ "type": "Point", "coordinates": [' + .concat(location.longitude?.toString()) + .concat(', ') + .concat(location.latitude?.toString()) + .concat('] }') + } else { + pointStr = '{ "type": "Point", "coordinates": [] }' + } + const point = JSON.parse(pointStr) as Point + return point +} + +export function Point2Location(point: Point): Location { + const location = new Location() + if (point.type === 'Point' && point.coordinates.length === 2) { + location.longitude = point.coordinates[0] + location.latitude = point.coordinates[1] + } + return location +} diff --git a/backend/src/graphql/resolver/util/communities.ts b/backend/src/graphql/resolver/util/communities.ts index 0c0023a19..e506548c5 100644 --- a/backend/src/graphql/resolver/util/communities.ts +++ b/backend/src/graphql/resolver/util/communities.ts @@ -58,7 +58,7 @@ export async function getCommunityName(communityIdentifier: string): Promise { +export async function getCommunityByUuid(communityUuid: string): Promise { return await DbCommunity.findOne({ where: [{ communityUuid }], }) diff --git a/backend/src/graphql/resolver/util/sendUserToGms.ts b/backend/src/graphql/resolver/util/sendUserToGms.ts new file mode 100644 index 000000000..335141ffe --- /dev/null +++ b/backend/src/graphql/resolver/util/sendUserToGms.ts @@ -0,0 +1,27 @@ +import { Community as DbCommunity } from '@entity/Community' +import { User as DbUser } from '@entity/User' + +import { createGmsUser } from '@/apis/gms/GmsClient' +import { GmsUser } from '@/apis/gms/model/GmsUser' +import { LogError } from '@/server/LogError' +import { backendLogger as logger } from '@/server/logger' + +export async function sendUserToGms(user: DbUser, homeCom: DbCommunity): Promise { + if (homeCom.gmsApiKey === null) { + throw new LogError('HomeCommunity needs GMS-ApiKey to publish user data to GMS.') + } + logger.debug('User send to GMS:', user) + const gmsUser = new GmsUser(user) + try { + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + if (await createGmsUser(homeCom.gmsApiKey, gmsUser)) { + logger.debug('GMS user published successfully:', gmsUser) + user.gmsRegistered = true + user.gmsRegisteredAt = new Date() + await DbUser.save(user) + logger.debug('mark user as gms published:', user) + } + } catch (err) { + logger.warn('publishing user fails with ', err) + } +} diff --git a/backend/src/graphql/scalar/Location.ts b/backend/src/graphql/scalar/Location.ts new file mode 100644 index 000000000..8b475d7f6 --- /dev/null +++ b/backend/src/graphql/scalar/Location.ts @@ -0,0 +1,43 @@ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +import { GraphQLScalarType, Kind } from 'graphql' + +import { Location } from '@model/Location' + +import { LogError } from '@/server/LogError' + +export const LocationScalar = new GraphQLScalarType({ + name: 'Location', + description: + 'The `Location` scalar type to represent longitude and latitude values of a geo location', + + serialize(value: Location) { + return value + }, + + parseValue(value): Location { + try { + const loc = new Location() + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-assignment + loc.longitude = value.longitude + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-assignment + loc.latitude = value.latitude + return loc + } catch (err) { + throw new LogError('Error:', err) + } + // return new Location() + }, + + parseLiteral(ast): Location { + if (ast.kind !== Kind.STRING) { + throw new TypeError(`${String(ast)} is not a valid Location value.`) + } + let loc = new Location() + try { + loc = JSON.parse(ast.value) as Location + } catch (err) { + throw new LogError('Error:', err) + } + return loc + }, +}) diff --git a/backend/src/graphql/scalar/Point.ts b/backend/src/graphql/scalar/Point.ts new file mode 100644 index 000000000..06af56bfc --- /dev/null +++ b/backend/src/graphql/scalar/Point.ts @@ -0,0 +1,31 @@ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +import { Point as DbPoint } from '@dbTools/typeorm' +import { GraphQLScalarType, Kind } from 'graphql' + +export const PointScalar = new GraphQLScalarType({ + name: 'Point', + description: + 'The `Point` scalar type to represent longitude and latitude values of a geo location', + + serialize(value: DbPoint) { + // Check type of value + if (value.type !== 'Point') { + throw new Error(`PointScalar can only serialize Geometry type 'Point' values`) + } + return value + }, + + parseValue(value): DbPoint { + const point = JSON.parse(value) as DbPoint + return point + }, + + parseLiteral(ast) { + if (ast.kind !== Kind.STRING) { + throw new TypeError(`${String(ast)} is not a valid Geometry value.`) + } + + const point = JSON.parse(ast.value) as DbPoint + return point + }, +}) diff --git a/backend/src/graphql/schema.ts b/backend/src/graphql/schema.ts index 18214861f..bcb8081a6 100644 --- a/backend/src/graphql/schema.ts +++ b/backend/src/graphql/schema.ts @@ -4,14 +4,20 @@ import { Decimal } from 'decimal.js-light' import { GraphQLSchema } from 'graphql' import { buildSchema } from 'type-graphql' +import { Location } from '@model/Location' + import { isAuthorized } from './directive/isAuthorized' import { DecimalScalar } from './scalar/Decimal' +import { LocationScalar } from './scalar/Location' export const schema = async (): Promise => { return buildSchema({ resolvers: [path.join(__dirname, 'resolver', `!(*.test).{js,ts}`)], authChecker: isAuthorized, - scalarsMap: [{ type: Decimal, scalar: DecimalScalar }], + scalarsMap: [ + { type: Decimal, scalar: DecimalScalar }, + { type: Location, scalar: LocationScalar }, + ], validate: { validationError: { target: false }, skipMissingProperties: true, diff --git a/backend/src/graphql/validator/Location.ts b/backend/src/graphql/validator/Location.ts new file mode 100644 index 000000000..f1f23cd81 --- /dev/null +++ b/backend/src/graphql/validator/Location.ts @@ -0,0 +1,29 @@ +import { registerDecorator, ValidationOptions, ValidationArguments } from 'class-validator' + +import { Location } from '@model/Location' + +import { Location2Point } from '@/graphql/resolver/util/Location2Point' + +export function isValidLocation(validationOptions?: ValidationOptions) { + // eslint-disable-next-line @typescript-eslint/ban-types + return function (object: Object, propertyName: string) { + registerDecorator({ + name: 'isValidLocation', + target: object.constructor, + propertyName, + options: validationOptions, + validator: { + validate(value: Location) { + // console.log('isValidLocation:', value, value.getPoint()) + if (!value || Location2Point(value).type === 'Point') { + return true + } + return false + }, + defaultMessage(args: ValidationArguments) { + return `${propertyName} must be a valid Location, ${args.property}` + }, + }, + }) + } +} diff --git a/backend/src/seeds/gmsUserList.ts b/backend/src/seeds/gmsUserList.ts new file mode 100644 index 000000000..7603ca116 --- /dev/null +++ b/backend/src/seeds/gmsUserList.ts @@ -0,0 +1,60 @@ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable @typescript-eslint/no-explicit-any */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ + +import { entities } from '@entity/index' +// import { createTestClient } from 'apollo-server-testing' + +import { CONFIG } from '@/config' +import { createServer } from '@/server/createServer' +import { backendLogger as logger } from '@/server/logger' + +CONFIG.EMAIL = false + +const context = { + token: '', + setHeaders: { + push: (value: { key: string; value: string }): void => { + context.token = value.value + }, + // eslint-disable-next-line @typescript-eslint/no-empty-function + forEach: (): void => {}, + }, + clientTimezoneOffset: 0, +} + +export const cleanDB = async () => { + // this only works as long we do not have foreign key constraints + for (const entity of entities) { + await resetEntity(entity) + } +} + +const resetEntity = async (entity: any) => { + const items = await entity.find({ withDeleted: true }) + if (items.length > 0) { + const ids = items.map((e: any) => e.id) + await entity.delete(ids) + } +} + +const run = async () => { + const server = await createServer(context) + // const seedClient = createTestClient(server.apollo) + const { con } = server + + // test GMS-Api Client + try { + // const gmsComArray = await communityList() + // logger.debug('GMS-Community-List:', gmsComArray) + // const gmsUserArray = await userList() + // logger.debug('GMS-Community-User-List:', gmsUserArray) + } catch (err) { + logger.error('Error in GMS-API:', err) + } + await con.close() +} + +void run() diff --git a/backend/src/seeds/gmsUsers.ts b/backend/src/seeds/gmsUsers.ts new file mode 100644 index 000000000..46a8998e1 --- /dev/null +++ b/backend/src/seeds/gmsUsers.ts @@ -0,0 +1,102 @@ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable @typescript-eslint/no-explicit-any */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ + +import { entities } from '@entity/index' +import { User as DbUser } from '@entity/User' +// import { createTestClient } from 'apollo-server-testing' + +// import { createGmsUser } from '@/apis/gms/GmsClient' +// import { GmsUser } from '@/apis/gms/model/GmsUser' +import { CONFIG } from '@/config' +import { getHomeCommunity } from '@/graphql/resolver/util/communities' +import { sendUserToGms } from '@/graphql/resolver/util/sendUserToGms' +import { createServer } from '@/server/createServer' +import { LogError } from '@/server/LogError' +import { backendLogger as logger } from '@/server/logger' + +CONFIG.EMAIL = false + +const context = { + token: '', + setHeaders: { + push: (value: { key: string; value: string }): void => { + context.token = value.value + }, + // eslint-disable-next-line @typescript-eslint/no-empty-function + forEach: (): void => {}, + }, + clientTimezoneOffset: 0, +} + +export const cleanDB = async () => { + // this only works as long we do not have foreign key constraints + for (const entity of entities) { + await resetEntity(entity) + } +} + +const resetEntity = async (entity: any) => { + const items = await entity.find({ withDeleted: true }) + if (items.length > 0) { + const ids = items.map((e: any) => e.id) + await entity.delete(ids) + } +} + +const run = async () => { + const server = await createServer(context) + // const seedClient = createTestClient(server.apollo) + const { con } = server + + const homeCom = await getHomeCommunity() + if (homeCom.gmsApiKey === null) { + throw new LogError('HomeCommunity needs GMS-ApiKey to publish user data to GMS.') + } + // read the ids of all local users, which are still not gms registered + const userIds = await DbUser.createQueryBuilder() + .select('id') + .where({ foreign: false }) + .andWhere('deleted_at is null') + .andWhere({ gmsRegistered: false }) + .getRawMany() + logger.debug('userIds:', userIds) + + for (const idStr of userIds) { + logger.debug('Id:', idStr.id) + const user = await DbUser.findOne({ + where: { id: idStr.id }, + relations: ['emailContact'], + }) + if (user) { + logger.debug('found local User:', user) + if (user.gmsAllowed) { + await sendUserToGms(user, homeCom) + /* + const gmsUser = new GmsUser(user) + try { + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + if (await createGmsUser(homeCom.gmsApiKey, gmsUser)) { + logger.debug('GMS user published successfully:', gmsUser) + user.gmsRegistered = true + user.gmsRegisteredAt = new Date() + await DbUser.save(user) + logger.debug('mark user as gms published:', user) + } + } catch (err) { + logger.warn('publishing user fails with ', err) + } + */ + } else { + logger.debug('GMS-Publishing not allowed by user settings:', user) + } + } + } + logger.info('##gms## publishing all local users successful...') + + await con.close() +} + +void run() diff --git a/backend/src/seeds/graphql/mutations.ts b/backend/src/seeds/graphql/mutations.ts index 981bb0da6..b10bb4b4e 100644 --- a/backend/src/seeds/graphql/mutations.ts +++ b/backend/src/seeds/graphql/mutations.ts @@ -34,6 +34,10 @@ export const updateUserInfos = gql` $locale: String $hideAmountGDD: Boolean $hideAmountGDT: Boolean + $gmsAllowed: Boolean + $gmsPublishName: Int + $gmsLocation: Location + $gmsPublishLocation: Int ) { updateUserInfos( firstName: $firstName @@ -44,6 +48,10 @@ export const updateUserInfos = gql` language: $locale hideAmountGDD: $hideAmountGDD hideAmountGDT: $hideAmountGDT + gmsAllowed: $gmsAllowed + gmsPublishName: $gmsPublishName + gmsLocation: $gmsLocation + gmsPublishLocation: $gmsPublishLocation ) } ` @@ -354,3 +362,19 @@ export const logout = gql` logout } ` + +export const updateHomeCommunityQuery = gql` + mutation ($uuid: String!, $gmsApiKey: String!) { + updateHomeCommunity(uuid: $uuid, gmsApiKey: $gmsApiKey) { + id + foreign + name + description + url + creationDate + uuid + authenticatedAt + gmsApiKey + } + } +` diff --git a/backend/src/seeds/graphql/queries.ts b/backend/src/seeds/graphql/queries.ts index 2daa5e8bd..6bd106174 100644 --- a/backend/src/seeds/graphql/queries.ts +++ b/backend/src/seeds/graphql/queries.ts @@ -118,7 +118,7 @@ export const listGDTEntriesQuery = gql` } ` -export const communities = gql` +export const communitiesQuery = gql` query { communities { id @@ -129,6 +129,23 @@ export const communities = gql` creationDate uuid authenticatedAt + gmsApiKey + } + } +` + +export const getCommunityByUuidQuery = gql` + query ($communityUuid: String!) { + community(communityUuid: $communityUuid) { + id + foreign + name + description + url + creationDate + uuid + authenticatedAt + gmsApiKey } } ` diff --git a/backend/src/server/createServer.ts b/backend/src/server/createServer.ts index 250a4b901..3f02b0afc 100644 --- a/backend/src/server/createServer.ts +++ b/backend/src/server/createServer.ts @@ -79,6 +79,8 @@ export const createServer = async ( */ }) app.use(limiter) + // because of nginx proxy, needed for limiter + app.set('trust proxy', 1) // bodyparser json app.use(json()) diff --git a/backend/src/typeorm/connection.ts b/backend/src/typeorm/connection.ts index 3c8307478..104f6449d 100644 --- a/backend/src/typeorm/connection.ts +++ b/backend/src/typeorm/connection.ts @@ -30,6 +30,7 @@ export class Connection { Connection.instance = await createConnection({ name: 'default', type: 'mysql', + legacySpatialSupport: false, host: CONFIG.DB_HOST, port: CONFIG.DB_PORT, username: CONFIG.DB_USER, diff --git a/backend/src/util/communityUser.ts b/backend/src/util/communityUser.ts index c4c420d51..732c585d0 100644 --- a/backend/src/util/communityUser.ts +++ b/backend/src/util/communityUser.ts @@ -51,6 +51,12 @@ const communityDbUser: dbUser = { foreign: false, communityUuid: '55555555-4444-4333-2222-11111111', community: null, + gmsPublishName: 0, + gmsAllowed: false, + location: null, + gmsPublishLocation: 2, + gmsRegistered: false, + gmsRegisteredAt: null, } const communityUser = new User(communityDbUser) diff --git a/backend/yarn.lock b/backend/yarn.lock index 234dc817a..91186187b 100644 --- a/backend/yarn.lock +++ b/backend/yarn.lock @@ -3483,6 +3483,11 @@ gensync@^1.0.0-beta.2: resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0" integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg== +geojson@^0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/geojson/-/geojson-0.5.0.tgz#3cd6c96399be65b56ee55596116fe9191ce701c0" + integrity sha512-/Bx5lEn+qRF4TfQ5aLu6NH+UKtvIv7Lhc487y/c8BdludrCTpiWf9wyI0RTyqg49MFefIAvFDuEi5Dfd/zgNxQ== + get-caller-file@^2.0.5: version "2.0.5" resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" @@ -3698,11 +3703,13 @@ graceful-fs@^4.1.6, graceful-fs@^4.2.0: crypto "^1.0.1" decimal.js-light "^2.5.1" dotenv "^10.0.0" + geojson "^0.5.0" mysql2 "^2.3.0" reflect-metadata "^0.1.13" ts-mysql-migrate "^1.0.2" typeorm "^0.3.16" uuid "^8.3.2" + wkx "^0.5.0" grapheme-splitter@^1.0.4: version "1.0.4" @@ -7301,6 +7308,13 @@ with@^7.0.0: assert-never "^1.2.1" babel-walk "3.0.0-canary-5" +wkx@^0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/wkx/-/wkx-0.5.0.tgz#c6c37019acf40e517cc6b94657a25a3d4aa33e8c" + integrity sha512-Xng/d4Ichh8uN4l0FToV/258EjMGU9MGcA0HV2d9B/ZpZB3lqQm7nkOdZdm5GhKtLLhAE7PiVQwN4eN+2YJJUg== + dependencies: + "@types/node" "*" + word-wrap@^1.2.3, word-wrap@~1.2.3: version "1.2.3" resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c" diff --git a/database/entity/0082-introduce_gms_registration/Community.ts b/database/entity/0082-introduce_gms_registration/Community.ts new file mode 100644 index 000000000..cc5607e2e --- /dev/null +++ b/database/entity/0082-introduce_gms_registration/Community.ts @@ -0,0 +1,73 @@ +import { + BaseEntity, + Entity, + PrimaryGeneratedColumn, + Column, + CreateDateColumn, + UpdateDateColumn, + OneToMany, + JoinColumn, +} from 'typeorm' +import { User } from '../User' + +@Entity('communities') +export class Community extends BaseEntity { + @PrimaryGeneratedColumn('increment', { unsigned: true }) + id: number + + @Column({ name: 'foreign', type: 'bool', nullable: false, default: true }) + foreign: boolean + + @Column({ name: 'url', length: 255, nullable: false }) + url: string + + @Column({ name: 'public_key', type: 'binary', length: 32, nullable: false }) + publicKey: Buffer + + @Column({ name: 'private_key', type: 'binary', length: 64, nullable: true }) + privateKey: Buffer | null + + @Column({ + name: 'community_uuid', + type: 'char', + length: 36, + nullable: true, + collation: 'utf8mb4_unicode_ci', + }) + communityUuid: string | null + + @Column({ name: 'authenticated_at', type: 'datetime', nullable: true }) + authenticatedAt: Date | null + + @Column({ name: 'name', type: 'varchar', length: 40, nullable: true }) + name: string | null + + @Column({ name: 'description', type: 'varchar', length: 255, nullable: true }) + description: string | null + + @CreateDateColumn({ name: 'creation_date', type: 'datetime', nullable: true }) + creationDate: Date | null + + @Column({ name: 'gms_api_key', type: 'varchar', length: 512, nullable: true, default: null }) + gmsApiKey: string | null + + @CreateDateColumn({ + name: 'created_at', + type: 'datetime', + default: () => 'CURRENT_TIMESTAMP(3)', + nullable: false, + }) + createdAt: Date + + @UpdateDateColumn({ + name: 'updated_at', + type: 'datetime', + onUpdate: 'CURRENT_TIMESTAMP(3)', + nullable: true, + }) + updatedAt: Date | null + + @OneToMany(() => User, (user) => user.community) + @JoinColumn({ name: 'community_uuid', referencedColumnName: 'communityUuid' }) + users: User[] +} diff --git a/database/entity/0082-introduce_gms_registration/User.ts b/database/entity/0082-introduce_gms_registration/User.ts new file mode 100644 index 000000000..3dc0dccb6 --- /dev/null +++ b/database/entity/0082-introduce_gms_registration/User.ts @@ -0,0 +1,170 @@ +import { + BaseEntity, + Entity, + PrimaryGeneratedColumn, + Column, + DeleteDateColumn, + OneToMany, + JoinColumn, + OneToOne, + Geometry, + ManyToOne, +} from 'typeorm' +import { Contribution } from '../Contribution' +import { ContributionMessage } from '../ContributionMessage' +import { UserContact } from '../UserContact' +import { UserRole } from '../UserRole' +import { GeometryTransformer } from '../../src/typeorm/GeometryTransformer' +import { Community } from '../Community' + +@Entity('users', { engine: 'InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci' }) +export class User extends BaseEntity { + @PrimaryGeneratedColumn('increment', { unsigned: true }) + id: number + + @Column({ type: 'bool', default: false }) + foreign: boolean + + @Column({ + name: 'gradido_id', + length: 36, + nullable: false, + collation: 'utf8mb4_unicode_ci', + }) + gradidoID: string + + @Column({ + name: 'community_uuid', + type: 'char', + length: 36, + nullable: true, + collation: 'utf8mb4_unicode_ci', + }) + communityUuid: string + + @ManyToOne(() => Community, (community) => community.users) + @JoinColumn({ name: 'community_uuid', referencedColumnName: 'communityUuid' }) + community: Community | null + + @Column({ + name: 'alias', + length: 20, + nullable: true, + default: null, + collation: 'utf8mb4_unicode_ci', + }) + alias: string + + @OneToOne(() => UserContact, (emailContact: UserContact) => emailContact.user) + @JoinColumn({ name: 'email_id' }) + emailContact: UserContact + + @Column({ name: 'email_id', type: 'int', unsigned: true, nullable: true, default: null }) + emailId: number | null + + @Column({ + name: 'first_name', + length: 255, + nullable: true, + default: null, + collation: 'utf8mb4_unicode_ci', + }) + firstName: string + + @Column({ + name: 'last_name', + length: 255, + nullable: true, + default: null, + collation: 'utf8mb4_unicode_ci', + }) + lastName: string + + @Column({ name: 'gms_publish_name', type: 'int', unsigned: true, nullable: false, default: 0 }) + gmsPublishName: number + + @Column({ name: 'created_at', default: () => 'CURRENT_TIMESTAMP(3)', nullable: false }) + createdAt: Date + + @DeleteDateColumn({ name: 'deleted_at', nullable: true }) + deletedAt: Date | null + + @Column({ type: 'bigint', default: 0, unsigned: true }) + password: BigInt + + @Column({ + name: 'password_encryption_type', + type: 'int', + unsigned: true, + nullable: false, + default: 0, + }) + passwordEncryptionType: number + + @Column({ length: 4, default: 'de', collation: 'utf8mb4_unicode_ci', nullable: false }) + language: string + + @Column({ type: 'bool', default: false }) + hideAmountGDD: boolean + + @Column({ type: 'bool', default: false }) + hideAmountGDT: boolean + + @OneToMany(() => UserRole, (userRole) => userRole.user) + @JoinColumn({ name: 'user_id' }) + userRoles: UserRole[] + + @Column({ name: 'referrer_id', type: 'int', unsigned: true, nullable: true, default: null }) + referrerId?: number | null + + @Column({ + name: 'contribution_link_id', + type: 'int', + unsigned: true, + nullable: true, + default: null, + }) + contributionLinkId?: number | null + + @Column({ name: 'publisher_id', default: 0 }) + publisherId: number + + @Column({ name: 'gms_allowed', type: 'bool', default: true }) + gmsAllowed: boolean + + @Column({ + name: 'location', + type: 'geometry', + default: null, + nullable: true, + transformer: GeometryTransformer, + }) + location: Geometry | null + + @Column({ + name: 'gms_publish_location', + type: 'int', + unsigned: true, + nullable: false, + default: 2, + }) + gmsPublishLocation: number + + @Column({ name: 'gms_registered', type: 'bool', default: false }) + gmsRegistered: boolean + + @Column({ name: 'gms_registered_at', type: 'datetime', default: null, nullable: true }) + gmsRegisteredAt: Date | null + + @OneToMany(() => Contribution, (contribution) => contribution.user) + @JoinColumn({ name: 'user_id' }) + contributions?: Contribution[] + + @OneToMany(() => ContributionMessage, (message) => message.user) + @JoinColumn({ name: 'user_id' }) + messages?: ContributionMessage[] + + @OneToMany(() => UserContact, (userContact: UserContact) => userContact.user) + @JoinColumn({ name: 'user_id' }) + userContacts?: UserContact[] +} diff --git a/database/entity/0082-introduce_gms_registration/UserContact.ts b/database/entity/0082-introduce_gms_registration/UserContact.ts new file mode 100644 index 000000000..82ae4c013 --- /dev/null +++ b/database/entity/0082-introduce_gms_registration/UserContact.ts @@ -0,0 +1,78 @@ +import { + BaseEntity, + Entity, + PrimaryGeneratedColumn, + Column, + DeleteDateColumn, + OneToOne, + CreateDateColumn, + UpdateDateColumn, +} from 'typeorm' +import { User } from '../User' + +@Entity('user_contacts', { engine: 'InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci' }) +export class UserContact extends BaseEntity { + @PrimaryGeneratedColumn('increment', { unsigned: true }) + id: number + + @Column({ + name: 'type', + length: 100, + nullable: true, + default: null, + collation: 'utf8mb4_unicode_ci', + }) + type: string + + @OneToOne(() => User, (user) => user.emailContact) + user: User + + @Column({ name: 'user_id', type: 'int', unsigned: true, nullable: false }) + userId: number + + @Column({ length: 255, unique: true, nullable: false, collation: 'utf8mb4_unicode_ci' }) + email: string + + @Column({ name: 'gms_publish_email', type: 'bool', nullable: false, default: false }) + gmsPublishEmail: boolean + + @Column({ name: 'email_verification_code', type: 'bigint', unsigned: true, unique: true }) + emailVerificationCode: string + + @Column({ name: 'email_opt_in_type_id' }) + emailOptInTypeId: number + + @Column({ name: 'email_resend_count' }) + emailResendCount: number + + @Column({ name: 'email_checked', type: 'bool', nullable: false, default: false }) + emailChecked: boolean + + @Column({ + name: 'country_code', + length: 255, + unique: false, + nullable: true, + collation: 'utf8mb4_unicode_ci', + }) + countryCode: string + + @Column({ length: 255, unique: false, nullable: true, collation: 'utf8mb4_unicode_ci' }) + phone: string + + @Column({ name: 'gms_publish_phone', type: 'int', unsigned: true, nullable: false, default: 0 }) + gmsPublishPhone: number + + @CreateDateColumn({ name: 'created_at', default: () => 'CURRENT_TIMESTAMP(3)', nullable: false }) + createdAt: Date + + @UpdateDateColumn({ + name: 'updated_at', + nullable: true, + onUpdate: 'CURRENT_TIMESTAMP(3)', + }) + updatedAt: Date | null + + @DeleteDateColumn({ name: 'deleted_at', nullable: true }) + deletedAt: Date | null +} diff --git a/database/entity/Community.ts b/database/entity/Community.ts index d286749eb..3b48d5c29 100644 --- a/database/entity/Community.ts +++ b/database/entity/Community.ts @@ -1 +1 @@ -export { Community } from './0081-user_join_community/Community' +export { Community } from './0082-introduce_gms_registration/Community' diff --git a/database/entity/User.ts b/database/entity/User.ts index b75693674..e3f15113d 100644 --- a/database/entity/User.ts +++ b/database/entity/User.ts @@ -1 +1 @@ -export { User } from './0081-user_join_community/User' +export { User } from './0082-introduce_gms_registration/User' diff --git a/database/entity/UserContact.ts b/database/entity/UserContact.ts index 17d4575b0..e91e9a9d3 100644 --- a/database/entity/UserContact.ts +++ b/database/entity/UserContact.ts @@ -1 +1 @@ -export { UserContact } from './0057-clear_old_password_junk/UserContact' +export { UserContact } from './0082-introduce_gms_registration/UserContact' diff --git a/database/migrations/0082-introduce_gms_registration.ts b/database/migrations/0082-introduce_gms_registration.ts new file mode 100644 index 000000000..e02801a4f --- /dev/null +++ b/database/migrations/0082-introduce_gms_registration.ts @@ -0,0 +1,54 @@ +/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ +/* eslint-disable @typescript-eslint/no-explicit-any */ + +export async function upgrade(queryFn: (query: string, values?: any[]) => Promise>) { + await queryFn( + 'ALTER TABLE `users` MODIFY COLUMN `foreign` tinyint(1) NOT NULL DEFAULT 0 AFTER `id`;', + ) + await queryFn( + 'ALTER TABLE `users` ADD COLUMN IF NOT EXISTS `gms_publish_name` int unsigned NOT NULL DEFAULT 0 AFTER `last_name`;', // COMMENT '0:alias if exists or initials only , 1:initials only, 2:firstName only, 3:firstName + Initial of LastName, 4:fullName' + ) + await queryFn( + 'ALTER TABLE `users` ADD COLUMN IF NOT EXISTS `gms_allowed` tinyint(1) NOT NULL DEFAULT 1;', + ) + await queryFn( + 'ALTER TABLE `users` ADD COLUMN IF NOT EXISTS `location` geometry DEFAULT NULL NULL AFTER `gms_allowed`;', + ) + await queryFn( + 'ALTER TABLE `users` ADD COLUMN IF NOT EXISTS `gms_publish_location` int unsigned NOT NULL DEFAULT 2 AFTER `location`;', // COMMENT '0:exact, 1:approximate, 2:random' + ) + await queryFn( + 'ALTER TABLE `users` ADD COLUMN IF NOT EXISTS `gms_registered` tinyint(1) NOT NULL DEFAULT 0;', + ) + await queryFn( + 'ALTER TABLE `users` ADD COLUMN IF NOT EXISTS `gms_registered_at` datetime(3) DEFAULT NULL NULL;', + ) + await queryFn( + 'ALTER TABLE `user_contacts` ADD COLUMN IF NOT EXISTS `gms_publish_email` tinyint(1) NOT NULL DEFAULT 0 AFTER `email_checked`;', // COMMENT '0:nothing, 1:email' + ) + await queryFn( + 'ALTER TABLE `user_contacts` ADD COLUMN IF NOT EXISTS `country_code` varchar(255) DEFAULT NULL NULL AFTER `gms_publish_email`;', + ) + await queryFn( + 'ALTER TABLE `user_contacts` ADD COLUMN IF NOT EXISTS `gms_publish_phone` int unsigned NOT NULL DEFAULT 0 AFTER `phone`;', // COMMENT '0:nothing, 1:country_code only, 2:complet phone number' + ) + await queryFn( + 'ALTER TABLE `communities` ADD COLUMN IF NOT EXISTS `gms_api_key` varchar(512) DEFAULT NULL NULL AFTER `description`;', + ) +} + +export async function downgrade(queryFn: (query: string, values?: any[]) => Promise>) { + await queryFn( + 'ALTER TABLE `users` MODIFY COLUMN `foreign` tinyint(4) NOT NULL DEFAULT 0 AFTER `id`;', + ) + await queryFn('ALTER TABLE `users` DROP COLUMN IF EXISTS `gms_publish_name`;') + await queryFn('ALTER TABLE `users` DROP COLUMN IF EXISTS `gms_allowed`;') + await queryFn('ALTER TABLE `users` DROP COLUMN IF EXISTS `location`;') + await queryFn('ALTER TABLE `users` DROP COLUMN IF EXISTS `gms_publish_location`;') + await queryFn('ALTER TABLE `users` DROP COLUMN IF EXISTS `gms_registered`;') + await queryFn('ALTER TABLE `users` DROP COLUMN IF EXISTS `gms_registered_at`;') + await queryFn('ALTER TABLE `user_contacts` DROP COLUMN IF EXISTS `gms_publish_email`;') + await queryFn('ALTER TABLE `user_contacts` DROP COLUMN IF EXISTS `country_code`;') + await queryFn('ALTER TABLE `user_contacts` DROP COLUMN IF EXISTS `gms_publish_phone`;') + await queryFn('ALTER TABLE `communities` DROP COLUMN IF EXISTS `gms_api_key`;') +} diff --git a/database/package.json b/database/package.json index 8e1a99826..caeb917c4 100644 --- a/database/package.json +++ b/database/package.json @@ -1,6 +1,6 @@ { "name": "gradido-database", - "version": "2.1.1", + "version": "2.2.0", "description": "Gradido Database Tool to execute database migrations", "main": "src/index.ts", "repository": "https://github.com/gradido/gradido/database", @@ -21,6 +21,7 @@ "devDependencies": { "@eslint-community/eslint-plugin-eslint-comments": "^3.2.1", "@types/faker": "^5.5.9", + "@types/geojson": "^7946.0.13", "@types/node": "^16.10.3", "@typescript-eslint/eslint-plugin": "^5.57.1", "@typescript-eslint/parser": "^5.57.1", @@ -45,11 +46,13 @@ "crypto": "^1.0.1", "decimal.js-light": "^2.5.1", "dotenv": "^10.0.0", + "geojson": "^0.5.0", "mysql2": "^2.3.0", "reflect-metadata": "^0.1.13", "ts-mysql-migrate": "^1.0.2", "typeorm": "^0.3.16", - "uuid": "^8.3.2" + "uuid": "^8.3.2", + "wkx": "^0.5.0" }, "engines": { "node": ">=14" diff --git a/database/src/typeorm/GeometryTransformer.ts b/database/src/typeorm/GeometryTransformer.ts new file mode 100644 index 000000000..3598c493f --- /dev/null +++ b/database/src/typeorm/GeometryTransformer.ts @@ -0,0 +1,28 @@ +/* eslint-disable camelcase */ +import { Geometry as wkx_Geometry } from 'wkx' +import { Geometry } from 'geojson' +import { ValueTransformer } from 'typeorm/decorator/options/ValueTransformer' + +/** + * TypeORM transformer to convert GeoJSON to MySQL WKT (Well Known Text) e.g. POINT(LAT, LON) and back + */ +export const GeometryTransformer: ValueTransformer = { + to: (geojson: Geometry): string | null => { + if (geojson) { + const wkxg = wkx_Geometry.parseGeoJSON(geojson) + const str = wkxg.toWkt() + return str + } + return null + }, + + from: (wkb: string): Record | null => { + // wkb ? wkx_Geometry.parse(wkb).toGeoJSON() : undefined + if (!wkb) { + return null + } + const record = wkx_Geometry.parse(wkb) + const str = record.toGeoJSON() + return str + }, +} diff --git a/database/yarn.lock b/database/yarn.lock index d8a0d6ffb..fd5598693 100644 --- a/database/yarn.lock +++ b/database/yarn.lock @@ -143,6 +143,11 @@ resolved "https://registry.yarnpkg.com/@types/faker/-/faker-5.5.9.tgz#588ede92186dc557bff8341d294335d50d255f0c" integrity sha512-uCx6mP3UY5SIO14XlspxsGjgaemrxpssJI0Ol+GfhxtcKpv9pgRZYsS4eeKeHVLje6Qtc8lGszuBI461+gVZBA== +"@types/geojson@^7946.0.13": + version "7946.0.13" + resolved "https://registry.yarnpkg.com/@types/geojson/-/geojson-7946.0.13.tgz#e6e77ea9ecf36564980a861e24e62a095988775e" + integrity sha512-bmrNrgKMOhM3WsafmbGmC+6dsF2Z308vLFsQ3a/bT8X8Sv5clVYpPars/UPq+sAaJP+5OoLAYgwbkS5QEJdLUQ== + "@types/json-schema@^7.0.9": version "7.0.12" resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.12.tgz#d70faba7039d5fca54c83c7dbab41051d2b6f6cb" @@ -1132,6 +1137,11 @@ generate-function@^2.3.1: dependencies: is-property "^1.0.2" +geojson@^0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/geojson/-/geojson-0.5.0.tgz#3cd6c96399be65b56ee55596116fe9191ce701c0" + integrity sha512-/Bx5lEn+qRF4TfQ5aLu6NH+UKtvIv7Lhc487y/c8BdludrCTpiWf9wyI0RTyqg49MFefIAvFDuEi5Dfd/zgNxQ== + get-caller-file@^2.0.5: version "2.0.5" resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" @@ -2502,6 +2512,13 @@ which@^2.0.1: dependencies: isexe "^2.0.0" +wkx@^0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/wkx/-/wkx-0.5.0.tgz#c6c37019acf40e517cc6b94657a25a3d4aa33e8c" + integrity sha512-Xng/d4Ichh8uN4l0FToV/258EjMGU9MGcA0HV2d9B/ZpZB3lqQm7nkOdZdm5GhKtLLhAE7PiVQwN4eN+2YJJUg== + dependencies: + "@types/node" "*" + word-wrap@^1.2.3: version "1.2.3" resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c" diff --git a/deployment/bare_metal/.env.dist b/deployment/bare_metal/.env.dist index deba914b1..9e6e911eb 100644 --- a/deployment/bare_metal/.env.dist +++ b/deployment/bare_metal/.env.dist @@ -30,7 +30,6 @@ FRONTEND_CONFIG_VERSION=v5.2024-01-08 ADMIN_CONFIG_VERSION=v2.2024-01-04 FEDERATION_CONFIG_VERSION=v2.2023-08-24 FEDERATION_DHT_CONFIG_VERSION=v4.2024-01-17 - FEDERATION_DHT_TOPIC=GRADIDO_HUB # Need adjustments for test system @@ -114,8 +113,14 @@ NGINX_UPDATE_PAGE_ROOT=/home/gradido/gradido/deployment/bare_metal/nginx/update- #NGINX_SSL_CERTIFICATE_KEY=/etc/letsencrypt/live/gddhost.tld/privkey.pem NGINX_SSL_DHPARAM=/etc/letsencrypt/ssl-dhparams.pem NGINX_SSL_INCLUDE=/etc/letsencrypt/options-ssl-nginx.conf +NGINX_REWRITE_LEGACY_URLS=false # LEGACY -NGINX_REWRITE_LEGACY_URLS=false DEFAULT_PUBLISHER_ID=2896 -WEBHOOK_ELOPAGE_SECRET=secret \ No newline at end of file +WEBHOOK_ELOPAGE_SECRET=secret + +# GMS +#GMS_ACTIVE=true +# Coordinates of Illuminz test instance +#GMS_URL=http://54.176.169.179:3071 +#GMS_URL=http://localhost:4044/ diff --git a/deployment/bare_metal/start.sh b/deployment/bare_metal/start.sh index 222e13f81..634f60c97 100755 --- a/deployment/bare_metal/start.sh +++ b/deployment/bare_metal/start.sh @@ -1,5 +1,9 @@ #!/bin/bash - +# check for parameter +if [ -z "$1" ]; then + echo "Usage: Please provide a branch name as the first argument." + exit 1 +fi # Find current directory & configure paths set -o allexport SCRIPT_PATH=$(realpath $0) @@ -80,7 +84,7 @@ pm2 delete all pm2 save # git -BRANCH=${1:-master} +BRANCH=$1 echo "Starting with git pull - branch:$BRANCH" >> $UPDATE_HTML cd $PROJECT_ROOT # TODO: this overfetches alot, but ensures we can use start.sh with tags diff --git a/deployment/hetzner_cloud/install.sh b/deployment/hetzner_cloud/install.sh index e9ed69e76..06b92ecaf 100755 --- a/deployment/hetzner_cloud/install.sh +++ b/deployment/hetzner_cloud/install.sh @@ -1,4 +1,9 @@ #!/bin/bash +# check for parameter +if [ -z "$1" ]; then + echo "Usage: Please provide a branch name as the first argument." + exit 1 +fi # Note: This is needed - since there is Summer-Time included in the default server Setup - UTC is REQUIRED for production data timedatectl set-timezone UTC diff --git a/dht-node/package.json b/dht-node/package.json index ec47913de..7a73791d5 100644 --- a/dht-node/package.json +++ b/dht-node/package.json @@ -1,6 +1,6 @@ { "name": "gradido-dht-node", - "version": "2.1.1", + "version": "2.2.0", "description": "Gradido dht-node module", "main": "src/index.ts", "repository": "https://github.com/gradido/gradido/", diff --git a/dht-node/src/config/index.ts b/dht-node/src/config/index.ts index 632ccdba3..949ae47ce 100644 --- a/dht-node/src/config/index.ts +++ b/dht-node/src/config/index.ts @@ -4,7 +4,7 @@ import dotenv from 'dotenv' dotenv.config() const constants = { - DB_VERSION: '0081-user_join_community', + DB_VERSION: '0082-introduce_gms_registration', LOG4JS_CONFIG: 'log4js-config.json', // default log level on production should be info LOG_LEVEL: process.env.LOG_LEVEL ?? 'info', diff --git a/dlt-connector/package.json b/dlt-connector/package.json index 8b5ae357c..5bc9673de 100644 --- a/dlt-connector/package.json +++ b/dlt-connector/package.json @@ -1,6 +1,6 @@ { "name": "gradido-dlt-connector", - "version": "2.1.1", + "version": "2.2.0", "description": "Gradido DLT-Connector", "main": "src/index.ts", "repository": "https://github.com/gradido/gradido/", diff --git a/dlt-connector/src/server/createServer.ts b/dlt-connector/src/server/createServer.ts index 50e8d96cb..25e0a12e2 100755 --- a/dlt-connector/src/server/createServer.ts +++ b/dlt-connector/src/server/createServer.ts @@ -62,6 +62,8 @@ const createServer = async ( */ }) app.use(limiter) + // because of nginx proxy, needed for limiter + app.set('trust proxy', 1) await apollo.start() app.use( diff --git a/federation/package.json b/federation/package.json index 1457b1be1..fcfaa0e46 100644 --- a/federation/package.json +++ b/federation/package.json @@ -1,6 +1,6 @@ { "name": "gradido-federation", - "version": "2.1.1", + "version": "2.2.0", "description": "Gradido federation module providing Gradido-Hub-Federation and versioned API for inter community communication", "main": "src/index.ts", "repository": "https://github.com/gradido/gradido/federation", diff --git a/federation/src/config/index.ts b/federation/src/config/index.ts index 8a8947b93..ec6fdff26 100644 --- a/federation/src/config/index.ts +++ b/federation/src/config/index.ts @@ -10,7 +10,7 @@ Decimal.set({ }) const constants = { - DB_VERSION: '0081-user_join_community', + DB_VERSION: '0082-introduce_gms_registration', DECAY_START_TIME: new Date('2021-05-13 17:46:31-0000'), // GMT+0 LOG4JS_CONFIG: 'log4js-config.json', // default log level on production should be info diff --git a/federation/src/server/createServer.ts b/federation/src/server/createServer.ts index 97729b882..3d3b80dd6 100644 --- a/federation/src/server/createServer.ts +++ b/federation/src/server/createServer.ts @@ -84,6 +84,8 @@ export const createServer = async ( */ }) app.use(limiter) + // because of nginx proxy, needed for limiter + app.set('trust proxy', 1) // bodyparser json app.use(express.json()) diff --git a/frontend/package.json b/frontend/package.json index 1a18388e2..e54ffa263 100755 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "bootstrap-vue-gradido-wallet", - "version": "2.1.1", + "version": "2.2.0", "private": true, "scripts": { "start": "node run/server.js", diff --git a/frontend/src/graphql/mutations.js b/frontend/src/graphql/mutations.js index b4f96179f..cade098da 100644 --- a/frontend/src/graphql/mutations.js +++ b/frontend/src/graphql/mutations.js @@ -26,24 +26,32 @@ export const forgotPassword = gql` export const updateUserInfos = gql` mutation( - $alias: String $firstName: String $lastName: String + $alias: String $password: String $passwordNew: String $locale: String $hideAmountGDD: Boolean $hideAmountGDT: Boolean + $gmsAllowed: Boolean + $gmsPublishName: Int + $gmsLocation: Location + $gmsPublishLocation: Int ) { updateUserInfos( - alias: $alias firstName: $firstName lastName: $lastName + alias: $alias password: $password passwordNew: $passwordNew language: $locale hideAmountGDD: $hideAmountGDD hideAmountGDT: $hideAmountGDT + gmsAllowed: $gmsAllowed + gmsPublishName: $gmsPublishName + gmsLocation: $gmsLocation + gmsPublishLocation: $gmsPublishLocation ) } ` diff --git a/package.json b/package.json index 063d7394a..b85be6f24 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "gradido", - "version": "2.1.1", + "version": "2.2.0", "description": "Gradido", "main": "index.js", "repository": "git@github.com:gradido/gradido.git", diff --git a/scripts/release.sh b/scripts/release.sh index ac7a748d0..caa8d517c 100755 --- a/scripts/release.sh +++ b/scripts/release.sh @@ -38,4 +38,4 @@ yarn version --no-git-tag-version --no-commit-hooks --no-commit --new-version ${ # generate changelog cd ${PROJECT_DIR} -auto-changelog --commit-limit 0 --latest-version ${VERSION} \ No newline at end of file +./node_modules/.bin/auto-changelog --commit-limit 0 --latest-version ${VERSION} \ No newline at end of file