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 4ccd60333..1cff23d5a 100644 --- a/backend/.env.template +++ b/backend/.env.template @@ -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 cdf35b725..8f70ab11e 100644 --- a/backend/package.json +++ b/backend/package.json @@ -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/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/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/resolver/UserResolver.test.ts b/backend/src/graphql/resolver/UserResolver.test.ts index d8df20585..9bb15c66e 100644 --- a/backend/src/graphql/resolver/UserResolver.test.ts +++ b/backend/src/graphql/resolver/UserResolver.test.ts @@ -177,6 +177,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 +201,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, }) }) }) diff --git a/backend/src/graphql/resolver/UserResolver.ts b/backend/src/graphql/resolver/UserResolver.ts index e3b323f8a..7be858d7a 100644 --- a/backend/src/graphql/resolver/UserResolver.ts +++ b/backend/src/graphql/resolver/UserResolver.ts @@ -71,6 +71,7 @@ import { findUserByIdentifier } from './util/findUserByIdentifier' import { findUsers } from './util/findUsers' import { getKlicktippState } from './util/getKlicktippState' 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 +362,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) } 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/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/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/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..e11842960 --- /dev/null +++ b/database/entity/0082-introduce_gms_registration/User.ts @@ -0,0 +1,163 @@ +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 { 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 }) + 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/deployment/bare_metal/.env.dist b/deployment/bare_metal/.env.dist index 88962bd4c..19d4651ec 100644 --- a/deployment/bare_metal/.env.dist +++ b/deployment/bare_metal/.env.dist @@ -117,4 +117,11 @@ NGINX_SSL_INCLUDE=/etc/letsencrypt/options-ssl-nginx.conf # 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/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/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