mirror of
https://github.com/IT4Change/gradido.git
synced 2026-04-06 01:25:28 +00:00
Merge branch 'master' into release-2_2_1-merge
This commit is contained in:
commit
f492794f35
3
.gitignore
vendored
3
.gitignore
vendored
@ -15,8 +15,5 @@ package-lock.json
|
||||
/deployment/bare_metal/log
|
||||
/deployment/bare_metal/backup
|
||||
|
||||
# Node Version Manager configuration file
|
||||
.nvmrc
|
||||
|
||||
# Apple macOS folder attribute file
|
||||
.DS_Store
|
||||
|
||||
1
admin/.nvmrc
Normal file
1
admin/.nvmrc
Normal file
@ -0,0 +1 @@
|
||||
v14.17.0
|
||||
@ -10,6 +10,7 @@
|
||||
"start": "node run/server.js",
|
||||
"serve": "vue-cli-service serve --open",
|
||||
"build": "vue-cli-service build",
|
||||
"postbuild": "find build -type f -regex '.*\\.\\(html\\|js\\|css\\|svg\\|json\\)' -exec gzip -9 -k {} +",
|
||||
"dev": "yarn run serve",
|
||||
"analyse-bundle": "yarn build && webpack-bundle-analyzer build/webpack.stats.json",
|
||||
"lint": "eslint --max-warnings=0 --ext .js,.vue,.json .",
|
||||
|
||||
@ -63,4 +63,10 @@ WEBHOOK_ELOPAGE_SECRET=secret
|
||||
|
||||
# Federation
|
||||
FEDERATION_VALIDATE_COMMUNITY_TIMER=60000
|
||||
FEDERATION_XCOM_SENDCOINS_ENABLED=false
|
||||
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/
|
||||
|
||||
@ -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
|
||||
FEDERATION_XCOM_SENDCOINS_ENABLED=$FEDERATION_XCOM_SENDCOINS_ENABLED
|
||||
|
||||
# GMS
|
||||
GMS_ACTIVE=$GMS_ACTIVE
|
||||
GMS_URL=$GMS_URL
|
||||
|
||||
1
backend/.nvmrc
Normal file
1
backend/.nvmrc
Normal file
@ -0,0 +1 @@
|
||||
v18.7.0
|
||||
@ -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": {
|
||||
|
||||
146
backend/src/apis/gms/GmsClient.ts
Normal file
146
backend/src/apis/gms/GmsClient.ts
Normal file
@ -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<GmsCommunity[] | string | undefined> {
|
||||
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<GmsUser[] | string | undefined> {
|
||||
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<GmsUser[] | string | undefined> {
|
||||
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<boolean> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
19
backend/src/apis/gms/model/GmsCommunity.ts
Normal file
19
backend/src/apis/gms/model/GmsCommunity.ts
Normal file
@ -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[]
|
||||
}
|
||||
*/
|
||||
18
backend/src/apis/gms/model/GmsCommunityProfile.ts
Normal file
18
backend/src/apis/gms/model/GmsCommunityProfile.ts
Normal file
@ -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
|
||||
}
|
||||
*/
|
||||
8
backend/src/apis/gms/model/GmsRoles.ts
Normal file
8
backend/src/apis/gms/model/GmsRoles.ts
Normal file
@ -0,0 +1,8 @@
|
||||
/*
|
||||
export class GmsRole {
|
||||
code: string
|
||||
status: number
|
||||
name: string
|
||||
Permissions: [unknown]
|
||||
}
|
||||
*/
|
||||
110
backend/src/apis/gms/model/GmsUser.ts
Normal file
110
backend/src/apis/gms/model/GmsUser.ts
Normal file
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
18
backend/src/apis/gms/model/GmsUserAccount.ts
Normal file
18
backend/src/apis/gms/model/GmsUserAccount.ts
Normal file
@ -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
|
||||
}
|
||||
*/
|
||||
24
backend/src/apis/gms/model/GmsUserProfile.ts
Normal file
24
backend/src/apis/gms/model/GmsUserProfile.ts
Normal file
@ -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
|
||||
}
|
||||
*/
|
||||
@ -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,
|
||||
]
|
||||
|
||||
@ -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',
|
||||
}
|
||||
|
||||
@ -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
|
||||
@ -128,8 +128,10 @@ if (
|
||||
|
||||
const federation = {
|
||||
FEDERATION_BACKEND_SEND_ON_API: process.env.FEDERATION_BACKEND_SEND_ON_API ?? '1_0',
|
||||
FEDERATION_VALIDATE_COMMUNITY_TIMER:
|
||||
Number(process.env.FEDERATION_VALIDATE_COMMUNITY_TIMER) ?? 60000,
|
||||
// ?? operator don't work here as expected
|
||||
FEDERATION_VALIDATE_COMMUNITY_TIMER: Number(
|
||||
process.env.FEDERATION_VALIDATE_COMMUNITY_TIMER ?? 60000,
|
||||
),
|
||||
FEDERATION_XCOM_SENDCOINS_ENABLED:
|
||||
process.env.FEDERATION_XCOM_SENDCOINS_ENABLED === 'true' ?? false,
|
||||
// default value for community-uuid is equal uuid of stage-3
|
||||
@ -139,6 +141,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 +158,5 @@ export const CONFIG = {
|
||||
...loginServer,
|
||||
...webhook,
|
||||
...federation,
|
||||
...gms,
|
||||
}
|
||||
|
||||
@ -8,6 +8,7 @@ import { FederatedCommunityLoggingView } from '@logging/FederatedCommunityLoggin
|
||||
import { FederationClient as V1_0_FederationClient } from '@/federation/client/1_0/FederationClient'
|
||||
import { PublicCommunityInfo } from '@/federation/client/1_0/model/PublicCommunityInfo'
|
||||
import { FederationClientFactory } from '@/federation/client/FederationClientFactory'
|
||||
import { LogError } from '@/server/LogError'
|
||||
import { backendLogger as logger } from '@/server/logger'
|
||||
|
||||
import { startCommunityAuthentication } from './authenticateCommunities'
|
||||
@ -15,6 +16,9 @@ import { PublicCommunityInfoLoggingView } from './client/1_0/logging/PublicCommu
|
||||
import { ApiVersionType } from './enum/apiVersionType'
|
||||
|
||||
export async function startValidateCommunities(timerInterval: number): Promise<void> {
|
||||
if (Number.isNaN(timerInterval) || timerInterval <= 0) {
|
||||
throw new LogError('FEDERATION_VALIDATE_COMMUNITY_TIMER is not a positive number')
|
||||
}
|
||||
logger.info(
|
||||
`Federation: startValidateCommunities loop with an interval of ${timerInterval} ms...`,
|
||||
)
|
||||
|
||||
14
backend/src/graphql/arg/CommunityArgs.ts
Normal file
14
backend/src/graphql/arg/CommunityArgs.ts
Normal file
@ -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
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
|
||||
12
backend/src/graphql/enum/GmsPublishLocationType.ts
Normal file
12
backend/src/graphql/enum/GmsPublishLocationType.ts
Normal file
@ -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
|
||||
})
|
||||
14
backend/src/graphql/enum/GmsPublishNameType.ts
Normal file
14
backend/src/graphql/enum/GmsPublishNameType.ts
Normal file
@ -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
|
||||
})
|
||||
12
backend/src/graphql/enum/GmsPublishPhoneType.ts
Normal file
12
backend/src/graphql/enum/GmsPublishPhoneType.ts
Normal file
@ -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
|
||||
})
|
||||
@ -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
|
||||
}
|
||||
|
||||
10
backend/src/graphql/model/Location.ts
Normal file
10
backend/src/graphql/model/Location.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import { ArgsType, Field, Int } from 'type-graphql'
|
||||
|
||||
@ArgsType()
|
||||
export class Location {
|
||||
@Field(() => Int)
|
||||
longitude: number
|
||||
|
||||
@Field(() => Int)
|
||||
latitude: number
|
||||
}
|
||||
@ -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: ')],
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -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<Community> {
|
||||
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<Community> {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@ -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({
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
@ -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<boolean> {
|
||||
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)
|
||||
|
||||
27
backend/src/graphql/resolver/util/Location2Point.ts
Normal file
27
backend/src/graphql/resolver/util/Location2Point.ts
Normal file
@ -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
|
||||
}
|
||||
@ -58,7 +58,7 @@ export async function getCommunityName(communityIdentifier: string): Promise<str
|
||||
}
|
||||
}
|
||||
|
||||
export async function getCommunity(communityUuid: string): Promise<DbCommunity | null> {
|
||||
export async function getCommunityByUuid(communityUuid: string): Promise<DbCommunity | null> {
|
||||
return await DbCommunity.findOne({
|
||||
where: [{ communityUuid }],
|
||||
})
|
||||
|
||||
27
backend/src/graphql/resolver/util/sendUserToGms.ts
Normal file
27
backend/src/graphql/resolver/util/sendUserToGms.ts
Normal file
@ -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<void> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
43
backend/src/graphql/scalar/Location.ts
Normal file
43
backend/src/graphql/scalar/Location.ts
Normal file
@ -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
|
||||
},
|
||||
})
|
||||
31
backend/src/graphql/scalar/Point.ts
Normal file
31
backend/src/graphql/scalar/Point.ts
Normal file
@ -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
|
||||
},
|
||||
})
|
||||
@ -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<GraphQLSchema> => {
|
||||
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,
|
||||
|
||||
29
backend/src/graphql/validator/Location.ts
Normal file
29
backend/src/graphql/validator/Location.ts
Normal file
@ -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}`
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
60
backend/src/seeds/gmsUserList.ts
Normal file
60
backend/src/seeds/gmsUserList.ts
Normal file
@ -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()
|
||||
102
backend/src/seeds/gmsUsers.ts
Normal file
102
backend/src/seeds/gmsUsers.ts
Normal file
@ -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()
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
@ -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"
|
||||
|
||||
1
database/.nvmrc
Normal file
1
database/.nvmrc
Normal file
@ -0,0 +1 @@
|
||||
v18.7.0
|
||||
73
database/entity/0082-introduce_gms_registration/Community.ts
Normal file
73
database/entity/0082-introduce_gms_registration/Community.ts
Normal file
@ -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[]
|
||||
}
|
||||
170
database/entity/0082-introduce_gms_registration/User.ts
Normal file
170
database/entity/0082-introduce_gms_registration/User.ts
Normal file
@ -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[]
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
@ -1 +1 @@
|
||||
export { Community } from './0081-user_join_community/Community'
|
||||
export { Community } from './0082-introduce_gms_registration/Community'
|
||||
|
||||
@ -1 +1 @@
|
||||
export { User } from './0081-user_join_community/User'
|
||||
export { User } from './0082-introduce_gms_registration/User'
|
||||
|
||||
@ -1 +1 @@
|
||||
export { UserContact } from './0057-clear_old_password_junk/UserContact'
|
||||
export { UserContact } from './0082-introduce_gms_registration/UserContact'
|
||||
|
||||
54
database/migrations/0082-introduce_gms_registration.ts
Normal file
54
database/migrations/0082-introduce_gms_registration.ts
Normal file
@ -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<Array<any>>) {
|
||||
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<Array<any>>) {
|
||||
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`;')
|
||||
}
|
||||
@ -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"
|
||||
|
||||
28
database/src/typeorm/GeometryTransformer.ts
Normal file
28
database/src/typeorm/GeometryTransformer.ts
Normal file
@ -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<string, any> | null => {
|
||||
// wkb ? wkx_Geometry.parse(wkb).toGeoJSON() : undefined
|
||||
if (!wkb) {
|
||||
return null
|
||||
}
|
||||
const record = wkx_Geometry.parse(wkb)
|
||||
const str = record.toGeoJSON()
|
||||
return str
|
||||
},
|
||||
}
|
||||
@ -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"
|
||||
|
||||
@ -113,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
|
||||
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/
|
||||
|
||||
@ -14,8 +14,8 @@ server {
|
||||
server {
|
||||
server_name $COMMUNITY_HOST;
|
||||
|
||||
listen [::]:443 ssl ipv6only=on;
|
||||
listen 443 ssl;
|
||||
listen [::]:443 ssl ipv6only=on http2;
|
||||
listen 443 ssl http2;
|
||||
ssl_certificate $NGINX_SSL_CERTIFICATE;
|
||||
ssl_certificate_key $NGINX_SSL_CERTIFICATE_KEY;
|
||||
include $NGINX_SSL_INCLUDE;
|
||||
@ -33,7 +33,7 @@ server {
|
||||
return 444;
|
||||
}
|
||||
|
||||
#gzip_static on;
|
||||
gzip_static on;
|
||||
gzip on;
|
||||
gzip_proxied any;
|
||||
gzip_types
|
||||
@ -53,18 +53,13 @@ server {
|
||||
|
||||
# Frontend (default)
|
||||
location / {
|
||||
|
||||
limit_req zone=frontend burst=40 nodelay;
|
||||
limit_conn addr 40;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection 'upgrade';
|
||||
proxy_set_header X-Forwarded-For $remote_addr;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header Host $host;
|
||||
root $PROJECT_ROOT/frontend/build/;
|
||||
index index.html;
|
||||
try_files $uri $uri/ /index.html = 404;
|
||||
|
||||
proxy_pass http://127.0.0.1:3000;
|
||||
proxy_redirect off;
|
||||
|
||||
access_log $GRADIDO_LOG_PATH/nginx-access.frontend.log gradido_log;
|
||||
error_log $GRADIDO_LOG_PATH/nginx-error.frontend.log warn;
|
||||
}
|
||||
@ -119,15 +114,10 @@ server {
|
||||
location /admin {
|
||||
limit_req zone=frontend burst=30 nodelay;
|
||||
limit_conn addr 40;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection 'upgrade';
|
||||
proxy_set_header X-Forwarded-For $remote_addr;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header Host $host;
|
||||
|
||||
proxy_pass http://127.0.0.1:8080/;
|
||||
proxy_redirect off;
|
||||
rewrite ^/admin/(.*)$ /$1 break;
|
||||
root $PROJECT_ROOT/admin/build/;
|
||||
index index.html;
|
||||
try_files $uri $uri/ /index.html = 404;
|
||||
|
||||
access_log $GRADIDO_LOG_PATH/nginx-access.admin.log gradido_log;
|
||||
error_log $GRADIDO_LOG_PATH/nginx-error.admin.log warn;
|
||||
|
||||
@ -18,7 +18,7 @@ server {
|
||||
return 444;
|
||||
}
|
||||
|
||||
#gzip_static on;
|
||||
gzip_static on;
|
||||
gzip on;
|
||||
gzip_proxied any;
|
||||
gzip_types
|
||||
@ -40,15 +40,9 @@ server {
|
||||
location / {
|
||||
limit_req zone=frontend burst=40 nodelay;
|
||||
limit_conn addr 40;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection 'upgrade';
|
||||
proxy_set_header X-Forwarded-For $remote_addr;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header Host $host;
|
||||
|
||||
proxy_pass http://127.0.0.1:3000;
|
||||
proxy_redirect off;
|
||||
root $PROJECT_ROOT/frontend/build/;
|
||||
index index.html;
|
||||
try_files $uri $uri/ /index.html = 404;
|
||||
|
||||
access_log $GRADIDO_LOG_PATH/nginx-access.frontend.log gradido_log;
|
||||
error_log $GRADIDO_LOG_PATH/nginx-error.frontend.log warn;
|
||||
@ -104,15 +98,10 @@ server {
|
||||
location /admin {
|
||||
limit_req zone=frontend burst=30 nodelay;
|
||||
limit_conn addr 40;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection 'upgrade';
|
||||
proxy_set_header X-Forwarded-For $remote_addr;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header Host $host;
|
||||
|
||||
proxy_pass http://127.0.0.1:8080/;
|
||||
proxy_redirect off;
|
||||
rewrite ^/admin/(.*)$ /$1 break;
|
||||
root $PROJECT_ROOT/admin/build/;
|
||||
index index.html;
|
||||
try_files $uri $uri/ /index.html = 404;
|
||||
|
||||
access_log $GRADIDO_LOG_PATH/nginx-access.admin.log gradido_log;
|
||||
error_log $GRADIDO_LOG_PATH/nginx-error.admin.log warn;
|
||||
|
||||
@ -241,8 +241,8 @@ export NODE_ENV=production
|
||||
|
||||
# start after building all to use up less ressources
|
||||
pm2 start --name gradido-backend "yarn --cwd $PROJECT_ROOT/backend start" -l $GRADIDO_LOG_PATH/pm2.backend.$TODAY.log --log-date-format 'YYYY-MM-DD HH:mm:ss.SSS'
|
||||
pm2 start --name gradido-frontend "yarn --cwd $PROJECT_ROOT/frontend start" -l $GRADIDO_LOG_PATH/pm2.frontend.$TODAY.log --log-date-format 'YYYY-MM-DD HH:mm:ss.SSS'
|
||||
pm2 start --name gradido-admin "yarn --cwd $PROJECT_ROOT/admin start" -l $GRADIDO_LOG_PATH/pm2.admin.$TODAY.log --log-date-format 'YYYY-MM-DD HH:mm:ss.SSS'
|
||||
#pm2 start --name gradido-frontend "yarn --cwd $PROJECT_ROOT/frontend start" -l $GRADIDO_LOG_PATH/pm2.frontend.$TODAY.log --log-date-format 'YYYY-MM-DD HH:mm:ss.SSS'
|
||||
#pm2 start --name gradido-admin "yarn --cwd $PROJECT_ROOT/admin start" -l $GRADIDO_LOG_PATH/pm2.admin.$TODAY.log --log-date-format 'YYYY-MM-DD HH:mm:ss.SSS'
|
||||
pm2 save
|
||||
if [ ! -z $FEDERATION_DHT_TOPIC ]; then
|
||||
pm2 start --name gradido-dht-node "yarn --cwd $PROJECT_ROOT/dht-node start" -l $GRADIDO_LOG_PATH/pm2.dht-node.$TODAY.log --log-date-format 'YYYY-MM-DD HH:mm:ss.SSS'
|
||||
|
||||
@ -15,6 +15,10 @@ ssh_authorized_keys:
|
||||
- ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAkLGbzbG7KIGfkssKJBkc/0EVAzQ/8vjvVHzNdxhK8J yourname
|
||||
```
|
||||
|
||||
I made a (german) video to show it to you:
|
||||
|
||||
[](https://www.youtube.com/watch?v=fORK3Bt3lPw)
|
||||
|
||||
## After Setup Cloud Server with cloudConfig.yaml
|
||||
### setup your domain pointing on server ip address
|
||||
### login to your new server as root
|
||||
@ -99,16 +103,20 @@ nano .env
|
||||
# adjust values accordingly
|
||||
```
|
||||
|
||||
### Run `install.sh`
|
||||
### Run `install.sh` with branch name
|
||||
***!!! Attention !!!***
|
||||
Don't use this script if you have custom config in /etc/nginx/conf.d, because this script
|
||||
will remove it and ln ../bare_metal/nginx/conf.d
|
||||
|
||||
```bash
|
||||
cd ~/gradido/deployment/hetzner_cloud
|
||||
sudo ./install.sh
|
||||
sudo ./install.sh release-2_2_0
|
||||
```
|
||||
|
||||
I made a (german) video to show it to you:
|
||||
|
||||
[](https://www.youtube.com/watch?v=9h-55Si6bMk)
|
||||
|
||||
### Make yourself admin
|
||||
- Create an account on your new gradido instance
|
||||
- Click the link in the activation email
|
||||
@ -122,3 +130,6 @@ sudo mysql -D gradido_community -e "insert into user_roles(user_id, role) values
|
||||
- login with you newly created user
|
||||
- if you has a link to `Admin Area` it worked and you are admin
|
||||
|
||||
I made a (german) video to show it to you:
|
||||
|
||||
[](https://www.youtube.com/watch?v=xVQ5t4MnLrE)
|
||||
|
||||
@ -104,6 +104,23 @@ ln -s $SCRIPT_PATH/nginx/common /etc/nginx/
|
||||
rmdir /etc/nginx/conf.d
|
||||
ln -s $SCRIPT_PATH/nginx/conf.d /etc/nginx/
|
||||
|
||||
# Make nginx restart automatic
|
||||
mkdir /etc/systemd/system/nginx.service.d
|
||||
# Define the content to be put into the override.conf file
|
||||
CONFIG_CONTENT="[Unit]
|
||||
StartLimitIntervalSec=500
|
||||
StartLimitBurst=5
|
||||
|
||||
[Service]
|
||||
Restart=on-failure
|
||||
RestartSec=5s"
|
||||
|
||||
# Write the content to the override.conf file
|
||||
echo "$CONFIG_CONTENT" | sudo tee /etc/systemd/system/nginx.service.d/override.conf >/dev/null
|
||||
|
||||
# Reload systemd to apply the changes
|
||||
sudo systemctl daemon-reload
|
||||
|
||||
# setup https with certbot
|
||||
certbot certonly --nginx --non-interactive --agree-tos --domains $COMMUNITY_HOST --email $COMMUNITY_SUPPORT_MAIL
|
||||
|
||||
|
||||
1
dht-node/.nvmrc
Normal file
1
dht-node/.nvmrc
Normal file
@ -0,0 +1 @@
|
||||
v19.5.0
|
||||
@ -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',
|
||||
|
||||
1
dlt-connector/.nvmrc
Normal file
1
dlt-connector/.nvmrc
Normal file
@ -0,0 +1 @@
|
||||
v19.5.0
|
||||
@ -6,7 +6,7 @@ module.exports = {
|
||||
collectCoverageFrom: ['src/**/*.ts', '!**/node_modules/**', '!src/seeds/**', '!build/**'],
|
||||
coverageThreshold: {
|
||||
global: {
|
||||
lines: 66,
|
||||
lines: 75,
|
||||
},
|
||||
},
|
||||
setupFiles: ['<rootDir>/test/testSetup.ts'],
|
||||
|
||||
@ -39,7 +39,8 @@
|
||||
"reflect-metadata": "^0.1.13",
|
||||
"sodium-native": "^4.0.4",
|
||||
"tsconfig-paths": "^4.1.2",
|
||||
"type-graphql": "^2.0.0-beta.2"
|
||||
"type-graphql": "^2.0.0-beta.2",
|
||||
"uuid": "^9.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint-community/eslint-plugin-eslint-comments": "^3.2.1",
|
||||
|
||||
@ -50,7 +50,7 @@ jest.mock('@iota/client', () => {
|
||||
|
||||
describe('Iota Tests', () => {
|
||||
it('test mocked sendDataMessage', async () => {
|
||||
const result = await sendMessage('Test Message')
|
||||
const result = await sendMessage('Test Message', 'topic')
|
||||
expect(result).toBe('5498130bc3918e1a7143969ce05805502417e3e1bd596d3c44d6a0adeea22710')
|
||||
})
|
||||
|
||||
|
||||
@ -2,17 +2,19 @@ import { ClientBuilder } from '@iota/client'
|
||||
import { MessageWrapper } from '@iota/client/lib/types'
|
||||
|
||||
import { CONFIG } from '@/config'
|
||||
|
||||
const client = new ClientBuilder().node(CONFIG.IOTA_API_URL).build()
|
||||
|
||||
/**
|
||||
* send data message onto iota tangle
|
||||
* use CONFIG.IOTA_COMMUNITY_ALIAS for index
|
||||
* @param {string | Uint8Array} message - the message as utf based string, will be converted to hex automatically from @iota/client
|
||||
* @param {string | Uint8Array} topic - the iota topic to which the message will be sended
|
||||
* @return {Promise<MessageWrapper>} the iota message typed
|
||||
*/
|
||||
function sendMessage(message: string | Uint8Array): Promise<MessageWrapper> {
|
||||
return client.message().index(CONFIG.IOTA_COMMUNITY_ALIAS).data(message).submit()
|
||||
function sendMessage(
|
||||
message: string | Uint8Array,
|
||||
topic: string | Uint8Array,
|
||||
): Promise<MessageWrapper> {
|
||||
return client.message().index(topic).data(message).submit()
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -31,7 +31,7 @@ const database = {
|
||||
const iota = {
|
||||
IOTA_API_URL: process.env.IOTA_API_URL ?? 'https://chrysalis-nodes.iota.org',
|
||||
IOTA_COMMUNITY_ALIAS: process.env.IOTA_COMMUNITY_ALIAS ?? 'GRADIDO: TestHelloWelt2',
|
||||
IOTA_HOME_COMMUNITY_SEED: process.env.IOTA_HOME_COMMUNITY_SEED ?? null,
|
||||
IOTA_HOME_COMMUNITY_SEED: process.env.IOTA_HOME_COMMUNITY_SEED?.substring(0, 32) ?? null,
|
||||
}
|
||||
|
||||
const dltConnector = {
|
||||
|
||||
35
dlt-connector/src/data/Account.logic.ts
Normal file
35
dlt-connector/src/data/Account.logic.ts
Normal file
@ -0,0 +1,35 @@
|
||||
import { Account } from '@entity/Account'
|
||||
|
||||
import { LogError } from '@/server/LogError'
|
||||
|
||||
import { KeyPair } from './KeyPair'
|
||||
import { UserLogic } from './User.logic'
|
||||
|
||||
export class AccountLogic {
|
||||
// eslint-disable-next-line no-useless-constructor
|
||||
public constructor(private self: Account) {}
|
||||
|
||||
/**
|
||||
* calculate account key pair starting from community key pair => derive user key pair => derive account key pair
|
||||
* @param communityKeyPair
|
||||
*/
|
||||
public calculateKeyPair(communityKeyPair: KeyPair): KeyPair {
|
||||
if (!this.self.user) {
|
||||
throw new LogError('missing user')
|
||||
}
|
||||
const userLogic = new UserLogic(this.self.user)
|
||||
const accountKeyPair = userLogic
|
||||
.calculateKeyPair(communityKeyPair)
|
||||
.derive([this.self.derivationIndex])
|
||||
|
||||
if (
|
||||
this.self.derive2Pubkey &&
|
||||
this.self.derive2Pubkey.compare(accountKeyPair.publicKey) !== 0
|
||||
) {
|
||||
throw new LogError(
|
||||
'The freshly derived public key does not correspond to the stored public key',
|
||||
)
|
||||
}
|
||||
return accountKeyPair
|
||||
}
|
||||
}
|
||||
@ -6,6 +6,7 @@ import { LogError } from '@/server/LogError'
|
||||
import { toPublic, derivePrivate, sign, verify, generateFromSeed } from 'bip32-ed25519'
|
||||
|
||||
import { Mnemonic } from './Mnemonic'
|
||||
import { SignaturePair } from './proto/3_3/SignaturePair'
|
||||
|
||||
/**
|
||||
* Class Managing Key Pair and also generate, sign and verify signature with it
|
||||
@ -81,7 +82,7 @@ export class KeyPair {
|
||||
return sign(message, this.getExtendPrivateKey())
|
||||
}
|
||||
|
||||
public verify(message: Buffer, signature: Buffer): boolean {
|
||||
return verify(message, signature, this.getExtendPublicKey())
|
||||
public static verify(message: Buffer, { signature, pubKey }: SignaturePair): boolean {
|
||||
return verify(message, signature, pubKey)
|
||||
}
|
||||
}
|
||||
|
||||
@ -3,10 +3,13 @@ import { entropyToMnemonic, mnemonicToSeedSync } from 'bip39'
|
||||
// eslint-disable-next-line camelcase
|
||||
import { randombytes_buf } from 'sodium-native'
|
||||
|
||||
import { LogError } from '@/server/LogError'
|
||||
|
||||
export class Mnemonic {
|
||||
private _passphrase = ''
|
||||
public constructor(seed?: Buffer | string) {
|
||||
if (seed) {
|
||||
Mnemonic.validateSeed(seed)
|
||||
this._passphrase = entropyToMnemonic(seed)
|
||||
return
|
||||
}
|
||||
@ -22,4 +25,24 @@ export class Mnemonic {
|
||||
public get seed(): Buffer {
|
||||
return mnemonicToSeedSync(this._passphrase)
|
||||
}
|
||||
|
||||
public static validateSeed(seed: Buffer | string): void {
|
||||
let seedBuffer: Buffer
|
||||
if (!Buffer.isBuffer(seed)) {
|
||||
seedBuffer = Buffer.from(seed, 'hex')
|
||||
} else {
|
||||
seedBuffer = seed
|
||||
}
|
||||
if (seedBuffer.length < 16 || seedBuffer.length > 32 || seedBuffer.length % 4 !== 0) {
|
||||
throw new LogError(
|
||||
'invalid seed, must be in binary between 16 and 32 Bytes, Power of 4, for more infos: https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki#generating-the-mnemonic',
|
||||
{
|
||||
seedBufferHex: seedBuffer.toString('hex'),
|
||||
toShort: seedBuffer.length < 16,
|
||||
toLong: seedBuffer.length > 32,
|
||||
powerOf4: seedBuffer.length % 4,
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -59,6 +59,10 @@ export class TransactionBuilder {
|
||||
return this.transaction.community
|
||||
}
|
||||
|
||||
public getOtherCommunity(): Community | undefined {
|
||||
return this.transaction.otherCommunity
|
||||
}
|
||||
|
||||
public setSigningAccount(signingAccount: Account): TransactionBuilder {
|
||||
this.transaction.signingAccount = signingAccount
|
||||
return this
|
||||
@ -103,22 +107,18 @@ export class TransactionBuilder {
|
||||
return this
|
||||
}
|
||||
|
||||
public async setSenderCommunityFromSenderUser(
|
||||
senderUser: UserIdentifier,
|
||||
): Promise<TransactionBuilder> {
|
||||
public async setCommunityFromUser(user: UserIdentifier): Promise<TransactionBuilder> {
|
||||
// get sender community
|
||||
const community = await CommunityRepository.getCommunityForUserIdentifier(senderUser)
|
||||
const community = await CommunityRepository.getCommunityForUserIdentifier(user)
|
||||
if (!community) {
|
||||
throw new LogError("couldn't find community for transaction")
|
||||
}
|
||||
return this.setCommunity(community)
|
||||
}
|
||||
|
||||
public async setOtherCommunityFromRecipientUser(
|
||||
recipientUser: UserIdentifier,
|
||||
): Promise<TransactionBuilder> {
|
||||
public async setOtherCommunityFromUser(user: UserIdentifier): Promise<TransactionBuilder> {
|
||||
// get recipient community
|
||||
const otherCommunity = await CommunityRepository.getCommunityForUserIdentifier(recipientUser)
|
||||
const otherCommunity = await CommunityRepository.getCommunityForUserIdentifier(user)
|
||||
return this.setOtherCommunity(otherCommunity)
|
||||
}
|
||||
|
||||
|
||||
323
dlt-connector/src/data/Transaction.logic.test.ts
Normal file
323
dlt-connector/src/data/Transaction.logic.test.ts
Normal file
@ -0,0 +1,323 @@
|
||||
import { Community } from '@entity/Community'
|
||||
import { Transaction } from '@entity/Transaction'
|
||||
import { Decimal } from 'decimal.js-light'
|
||||
|
||||
import { logger } from '@/logging/logger'
|
||||
|
||||
import { CommunityRoot } from './proto/3_3/CommunityRoot'
|
||||
import { CrossGroupType } from './proto/3_3/enum/CrossGroupType'
|
||||
import { GradidoCreation } from './proto/3_3/GradidoCreation'
|
||||
import { GradidoDeferredTransfer } from './proto/3_3/GradidoDeferredTransfer'
|
||||
import { GradidoTransfer } from './proto/3_3/GradidoTransfer'
|
||||
import { RegisterAddress } from './proto/3_3/RegisterAddress'
|
||||
import { TransactionBody } from './proto/3_3/TransactionBody'
|
||||
import { TransactionLogic } from './Transaction.logic'
|
||||
|
||||
let a: Transaction
|
||||
let b: Transaction
|
||||
|
||||
describe('data/transaction.logic', () => {
|
||||
describe('isBelongTogether', () => {
|
||||
beforeEach(() => {
|
||||
const now = new Date()
|
||||
let body = new TransactionBody()
|
||||
body.type = CrossGroupType.OUTBOUND
|
||||
body.transfer = new GradidoTransfer()
|
||||
body.otherGroup = 'recipient group'
|
||||
|
||||
a = new Transaction()
|
||||
a.community = new Community()
|
||||
a.communityId = 1
|
||||
a.otherCommunityId = 2
|
||||
a.id = 1
|
||||
a.signingAccountId = 1
|
||||
a.recipientAccountId = 2
|
||||
a.createdAt = now
|
||||
a.amount = new Decimal('100')
|
||||
a.signature = Buffer.alloc(64)
|
||||
a.bodyBytes = Buffer.from(TransactionBody.encode(body).finish())
|
||||
|
||||
body = new TransactionBody()
|
||||
body.type = CrossGroupType.INBOUND
|
||||
body.transfer = new GradidoTransfer()
|
||||
body.otherGroup = 'sending group'
|
||||
|
||||
b = new Transaction()
|
||||
b.community = new Community()
|
||||
b.communityId = 2
|
||||
b.otherCommunityId = 1
|
||||
b.id = 2
|
||||
b.signingAccountId = 1
|
||||
b.recipientAccountId = 2
|
||||
b.createdAt = now
|
||||
b.amount = new Decimal('100')
|
||||
b.signature = Buffer.alloc(64)
|
||||
b.bodyBytes = Buffer.from(TransactionBody.encode(body).finish())
|
||||
})
|
||||
|
||||
const spy = jest.spyOn(logger, 'info')
|
||||
|
||||
it('true', () => {
|
||||
const logic = new TransactionLogic(a)
|
||||
expect(logic.isBelongTogether(b)).toBe(true)
|
||||
})
|
||||
|
||||
it('false because of same id', () => {
|
||||
b.id = 1
|
||||
const logic = new TransactionLogic(a)
|
||||
expect(logic.isBelongTogether(b)).toBe(false)
|
||||
expect(spy).toHaveBeenLastCalledWith('id is the same, it is the same transaction!')
|
||||
})
|
||||
|
||||
it('false because of different signing accounts', () => {
|
||||
b.signingAccountId = 17
|
||||
const logic = new TransactionLogic(a)
|
||||
expect(logic.isBelongTogether(b)).toBe(false)
|
||||
expect(spy).toHaveBeenLastCalledWith(
|
||||
'transaction a and b are not pairs',
|
||||
expect.objectContaining({}),
|
||||
)
|
||||
})
|
||||
|
||||
it('false because of different recipient accounts', () => {
|
||||
b.recipientAccountId = 21
|
||||
const logic = new TransactionLogic(a)
|
||||
expect(logic.isBelongTogether(b)).toBe(false)
|
||||
expect(spy).toHaveBeenLastCalledWith(
|
||||
'transaction a and b are not pairs',
|
||||
expect.objectContaining({}),
|
||||
)
|
||||
})
|
||||
|
||||
it('false because of different community ids', () => {
|
||||
b.communityId = 6
|
||||
const logic = new TransactionLogic(a)
|
||||
expect(logic.isBelongTogether(b)).toBe(false)
|
||||
expect(spy).toHaveBeenLastCalledWith(
|
||||
'transaction a and b are not pairs',
|
||||
expect.objectContaining({}),
|
||||
)
|
||||
})
|
||||
|
||||
it('false because of different other community ids', () => {
|
||||
b.otherCommunityId = 3
|
||||
const logic = new TransactionLogic(a)
|
||||
expect(logic.isBelongTogether(b)).toBe(false)
|
||||
expect(spy).toHaveBeenLastCalledWith(
|
||||
'transaction a and b are not pairs',
|
||||
expect.objectContaining({}),
|
||||
)
|
||||
})
|
||||
|
||||
it('false because of different createdAt', () => {
|
||||
b.createdAt = new Date('2021-01-01T17:12')
|
||||
const logic = new TransactionLogic(a)
|
||||
expect(logic.isBelongTogether(b)).toBe(false)
|
||||
expect(spy).toHaveBeenLastCalledWith(
|
||||
'transaction a and b are not pairs',
|
||||
expect.objectContaining({}),
|
||||
)
|
||||
})
|
||||
|
||||
describe('false because of mismatching cross group type', () => {
|
||||
const body = new TransactionBody()
|
||||
it('a is LOCAL', () => {
|
||||
body.type = CrossGroupType.LOCAL
|
||||
a.bodyBytes = Buffer.from(TransactionBody.encode(body).finish())
|
||||
const logic = new TransactionLogic(a)
|
||||
expect(logic.isBelongTogether(b)).toBe(false)
|
||||
expect(spy).toHaveBeenNthCalledWith(7, 'no one can be LOCAL')
|
||||
expect(spy).toHaveBeenLastCalledWith(
|
||||
"cross group types don't match",
|
||||
expect.objectContaining({}),
|
||||
)
|
||||
})
|
||||
|
||||
it('b is LOCAL', () => {
|
||||
body.type = CrossGroupType.LOCAL
|
||||
b.bodyBytes = Buffer.from(TransactionBody.encode(body).finish())
|
||||
const logic = new TransactionLogic(a)
|
||||
expect(logic.isBelongTogether(b)).toBe(false)
|
||||
expect(spy).toHaveBeenNthCalledWith(9, 'no one can be LOCAL')
|
||||
expect(spy).toHaveBeenLastCalledWith(
|
||||
"cross group types don't match",
|
||||
expect.objectContaining({}),
|
||||
)
|
||||
})
|
||||
|
||||
it('both are INBOUND', () => {
|
||||
body.type = CrossGroupType.INBOUND
|
||||
a.bodyBytes = Buffer.from(TransactionBody.encode(body).finish())
|
||||
b.bodyBytes = Buffer.from(TransactionBody.encode(body).finish())
|
||||
const logic = new TransactionLogic(a)
|
||||
expect(logic.isBelongTogether(b)).toBe(false)
|
||||
expect(spy).toHaveBeenLastCalledWith(
|
||||
"cross group types don't match",
|
||||
expect.objectContaining({}),
|
||||
)
|
||||
})
|
||||
|
||||
it('both are OUTBOUND', () => {
|
||||
body.type = CrossGroupType.OUTBOUND
|
||||
a.bodyBytes = Buffer.from(TransactionBody.encode(body).finish())
|
||||
b.bodyBytes = Buffer.from(TransactionBody.encode(body).finish())
|
||||
const logic = new TransactionLogic(a)
|
||||
expect(logic.isBelongTogether(b)).toBe(false)
|
||||
expect(spy).toHaveBeenLastCalledWith(
|
||||
"cross group types don't match",
|
||||
expect.objectContaining({}),
|
||||
)
|
||||
})
|
||||
|
||||
it('a is CROSS', () => {
|
||||
body.type = CrossGroupType.CROSS
|
||||
a.bodyBytes = Buffer.from(TransactionBody.encode(body).finish())
|
||||
const logic = new TransactionLogic(a)
|
||||
expect(logic.isBelongTogether(b)).toBe(false)
|
||||
expect(spy).toHaveBeenLastCalledWith(
|
||||
"cross group types don't match",
|
||||
expect.objectContaining({}),
|
||||
)
|
||||
})
|
||||
|
||||
it('b is CROSS', () => {
|
||||
body.type = CrossGroupType.CROSS
|
||||
b.bodyBytes = Buffer.from(TransactionBody.encode(body).finish())
|
||||
const logic = new TransactionLogic(a)
|
||||
expect(logic.isBelongTogether(b)).toBe(false)
|
||||
expect(spy).toHaveBeenLastCalledWith(
|
||||
"cross group types don't match",
|
||||
expect.objectContaining({}),
|
||||
)
|
||||
})
|
||||
|
||||
it('true with a as INBOUND and b as OUTBOUND', () => {
|
||||
let body = TransactionBody.fromBodyBytes(a.bodyBytes)
|
||||
body.type = CrossGroupType.INBOUND
|
||||
a.bodyBytes = Buffer.from(TransactionBody.encode(body).finish())
|
||||
body = TransactionBody.fromBodyBytes(b.bodyBytes)
|
||||
body.type = CrossGroupType.OUTBOUND
|
||||
b.bodyBytes = Buffer.from(TransactionBody.encode(body).finish())
|
||||
const logic = new TransactionLogic(a)
|
||||
expect(logic.isBelongTogether(b)).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('false because of transaction type not suitable for cross group transactions', () => {
|
||||
const body = new TransactionBody()
|
||||
body.type = CrossGroupType.OUTBOUND
|
||||
it('without transaction type (broken TransactionBody)', () => {
|
||||
a.bodyBytes = Buffer.from(TransactionBody.encode(body).finish())
|
||||
const logic = new TransactionLogic(a)
|
||||
expect(() => logic.isBelongTogether(b)).toThrowError("couldn't determine transaction type")
|
||||
})
|
||||
|
||||
it('not the same transaction types', () => {
|
||||
const body = new TransactionBody()
|
||||
body.type = CrossGroupType.OUTBOUND
|
||||
body.registerAddress = new RegisterAddress()
|
||||
a.bodyBytes = Buffer.from(TransactionBody.encode(body).finish())
|
||||
const logic = new TransactionLogic(a)
|
||||
expect(logic.isBelongTogether(b)).toBe(false)
|
||||
expect(spy).toHaveBeenLastCalledWith(
|
||||
"transaction types don't match",
|
||||
expect.objectContaining({}),
|
||||
)
|
||||
})
|
||||
|
||||
it('community root cannot be a cross group transaction', () => {
|
||||
let body = new TransactionBody()
|
||||
body.type = CrossGroupType.OUTBOUND
|
||||
body.communityRoot = new CommunityRoot()
|
||||
a.bodyBytes = Buffer.from(TransactionBody.encode(body).finish())
|
||||
body = new TransactionBody()
|
||||
body.type = CrossGroupType.INBOUND
|
||||
body.communityRoot = new CommunityRoot()
|
||||
b.bodyBytes = Buffer.from(TransactionBody.encode(body).finish())
|
||||
const logic = new TransactionLogic(a)
|
||||
expect(logic.isBelongTogether(b)).toBe(false)
|
||||
expect(spy).toHaveBeenLastCalledWith(
|
||||
"TransactionType COMMUNITY_ROOT couldn't be a CrossGroup Transaction",
|
||||
)
|
||||
})
|
||||
|
||||
it('Gradido Creation cannot be a cross group transaction', () => {
|
||||
let body = new TransactionBody()
|
||||
body.type = CrossGroupType.OUTBOUND
|
||||
body.creation = new GradidoCreation()
|
||||
a.bodyBytes = Buffer.from(TransactionBody.encode(body).finish())
|
||||
body = new TransactionBody()
|
||||
body.type = CrossGroupType.INBOUND
|
||||
body.creation = new GradidoCreation()
|
||||
b.bodyBytes = Buffer.from(TransactionBody.encode(body).finish())
|
||||
const logic = new TransactionLogic(a)
|
||||
expect(logic.isBelongTogether(b)).toBe(false)
|
||||
expect(spy).toHaveBeenLastCalledWith(
|
||||
"TransactionType GRADIDO_CREATION couldn't be a CrossGroup Transaction",
|
||||
)
|
||||
})
|
||||
|
||||
it('Deferred Transfer cannot be a cross group transaction', () => {
|
||||
let body = new TransactionBody()
|
||||
body.type = CrossGroupType.OUTBOUND
|
||||
body.deferredTransfer = new GradidoDeferredTransfer()
|
||||
a.bodyBytes = Buffer.from(TransactionBody.encode(body).finish())
|
||||
body = new TransactionBody()
|
||||
body.type = CrossGroupType.INBOUND
|
||||
body.deferredTransfer = new GradidoDeferredTransfer()
|
||||
b.bodyBytes = Buffer.from(TransactionBody.encode(body).finish())
|
||||
const logic = new TransactionLogic(a)
|
||||
expect(logic.isBelongTogether(b)).toBe(false)
|
||||
expect(spy).toHaveBeenLastCalledWith(
|
||||
"TransactionType GRADIDO_DEFERRED_TRANSFER couldn't be a CrossGroup Transaction",
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('false because of wrong amount', () => {
|
||||
it('amount missing on a', () => {
|
||||
a.amount = undefined
|
||||
const logic = new TransactionLogic(a)
|
||||
expect(logic.isBelongTogether(b)).toBe(false)
|
||||
expect(spy).toHaveBeenLastCalledWith('missing amount')
|
||||
})
|
||||
|
||||
it('amount missing on b', () => {
|
||||
b.amount = undefined
|
||||
const logic = new TransactionLogic(a)
|
||||
expect(logic.isBelongTogether(b)).toBe(false)
|
||||
expect(spy).toHaveBeenLastCalledWith('missing amount')
|
||||
})
|
||||
|
||||
it('amount not the same', () => {
|
||||
a.amount = new Decimal('101')
|
||||
const logic = new TransactionLogic(a)
|
||||
expect(logic.isBelongTogether(b)).toBe(false)
|
||||
expect(spy).toHaveBeenLastCalledWith('amounts mismatch', expect.objectContaining({}))
|
||||
})
|
||||
})
|
||||
|
||||
it('false because otherGroup are the same', () => {
|
||||
const body = new TransactionBody()
|
||||
body.type = CrossGroupType.OUTBOUND
|
||||
body.transfer = new GradidoTransfer()
|
||||
body.otherGroup = 'sending group'
|
||||
a.bodyBytes = Buffer.from(TransactionBody.encode(body).finish())
|
||||
const logic = new TransactionLogic(a)
|
||||
expect(logic.isBelongTogether(b)).toBe(false)
|
||||
expect(spy).toHaveBeenLastCalledWith('otherGroups are the same', expect.objectContaining({}))
|
||||
})
|
||||
|
||||
it('false because of different memos', () => {
|
||||
const body = new TransactionBody()
|
||||
body.type = CrossGroupType.OUTBOUND
|
||||
body.transfer = new GradidoTransfer()
|
||||
body.otherGroup = 'recipient group'
|
||||
body.memo = 'changed memo'
|
||||
a.bodyBytes = Buffer.from(TransactionBody.encode(body).finish())
|
||||
const logic = new TransactionLogic(a)
|
||||
expect(logic.isBelongTogether(b)).toBe(false)
|
||||
expect(spy).toHaveBeenLastCalledWith('memo differ', expect.objectContaining({}))
|
||||
})
|
||||
})
|
||||
})
|
||||
200
dlt-connector/src/data/Transaction.logic.ts
Normal file
200
dlt-connector/src/data/Transaction.logic.ts
Normal file
@ -0,0 +1,200 @@
|
||||
import { Transaction } from '@entity/Transaction'
|
||||
import { Not } from 'typeorm'
|
||||
|
||||
import { logger } from '@/logging/logger'
|
||||
import { TransactionBodyLoggingView } from '@/logging/TransactionBodyLogging.view'
|
||||
import { TransactionLoggingView } from '@/logging/TransactionLogging.view'
|
||||
import { LogError } from '@/server/LogError'
|
||||
|
||||
import { CrossGroupType } from './proto/3_3/enum/CrossGroupType'
|
||||
import { TransactionType } from './proto/3_3/enum/TransactionType'
|
||||
import { TransactionBody } from './proto/3_3/TransactionBody'
|
||||
|
||||
export class TransactionLogic {
|
||||
protected transactionBody: TransactionBody | undefined
|
||||
|
||||
// eslint-disable-next-line no-useless-constructor
|
||||
public constructor(private self: Transaction) {}
|
||||
|
||||
/**
|
||||
* search for transaction pair for Cross Group Transaction
|
||||
* @returns
|
||||
*/
|
||||
public async findPairTransaction(): Promise<Transaction> {
|
||||
const type = this.getBody().type
|
||||
if (type === CrossGroupType.LOCAL) {
|
||||
throw new LogError("local transaction don't has a pairing transaction")
|
||||
}
|
||||
|
||||
// check if already was loaded from db
|
||||
if (this.self.pairingTransaction) {
|
||||
return this.self.pairingTransaction
|
||||
}
|
||||
|
||||
if (this.self.pairingTransaction) {
|
||||
const pairingTransaction = await Transaction.findOneBy({ id: this.self.pairingTransaction })
|
||||
if (pairingTransaction) {
|
||||
return pairingTransaction
|
||||
}
|
||||
}
|
||||
// check if we find some in db
|
||||
const sameCreationDateTransactions = await Transaction.findBy({
|
||||
createdAt: this.self.createdAt,
|
||||
id: Not(this.self.id),
|
||||
})
|
||||
if (
|
||||
sameCreationDateTransactions.length === 1 &&
|
||||
this.isBelongTogether(sameCreationDateTransactions[0])
|
||||
) {
|
||||
return sameCreationDateTransactions[0]
|
||||
}
|
||||
// this approach only work if all entities get ids really incremented by one
|
||||
if (type === CrossGroupType.OUTBOUND) {
|
||||
const prevTransaction = await Transaction.findOneBy({ id: this.self.id - 1 })
|
||||
if (prevTransaction && this.isBelongTogether(prevTransaction)) {
|
||||
return prevTransaction
|
||||
}
|
||||
} else if (type === CrossGroupType.INBOUND) {
|
||||
const nextTransaction = await Transaction.findOneBy({ id: this.self.id + 1 })
|
||||
if (nextTransaction && this.isBelongTogether(nextTransaction)) {
|
||||
return nextTransaction
|
||||
}
|
||||
}
|
||||
throw new LogError("couldn't find valid pairing transaction", {
|
||||
id: this.self.id,
|
||||
type: CrossGroupType[type],
|
||||
transactionCountWithSameCreatedAt: sameCreationDateTransactions.length,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* check if two transactions belong together
|
||||
* are they pairs for a cross group transaction
|
||||
* @param otherTransaction
|
||||
*/
|
||||
public isBelongTogether(otherTransaction: Transaction): boolean {
|
||||
if (this.self.id === otherTransaction.id) {
|
||||
logger.info('id is the same, it is the same transaction!')
|
||||
return false
|
||||
}
|
||||
|
||||
if (
|
||||
this.self.signingAccountId !== otherTransaction.signingAccountId ||
|
||||
this.self.recipientAccountId !== otherTransaction.recipientAccountId ||
|
||||
this.self.communityId !== otherTransaction.otherCommunityId ||
|
||||
this.self.otherCommunityId !== otherTransaction.communityId ||
|
||||
this.self.createdAt.getTime() !== otherTransaction.createdAt.getTime()
|
||||
) {
|
||||
logger.info('transaction a and b are not pairs', {
|
||||
a: new TransactionLoggingView(this.self).toJSON(),
|
||||
b: new TransactionLoggingView(otherTransaction).toJSON(),
|
||||
})
|
||||
return false
|
||||
}
|
||||
const body = this.getBody()
|
||||
const otherBody = TransactionBody.fromBodyBytes(otherTransaction.bodyBytes)
|
||||
/**
|
||||
* both must be Cross or
|
||||
* one can be OUTBOUND and one can be INBOUND
|
||||
* no one can be LOCAL
|
||||
*/
|
||||
|
||||
if (!this.validCrossGroupTypes(body.type, otherBody.type)) {
|
||||
logger.info("cross group types don't match", {
|
||||
a: new TransactionBodyLoggingView(body).toJSON(),
|
||||
b: new TransactionBodyLoggingView(otherBody).toJSON(),
|
||||
})
|
||||
return false
|
||||
}
|
||||
const type = body.getTransactionType()
|
||||
const otherType = otherBody.getTransactionType()
|
||||
if (!type || !otherType) {
|
||||
throw new LogError("couldn't determine transaction type", {
|
||||
a: new TransactionBodyLoggingView(body).toJSON(),
|
||||
b: new TransactionBodyLoggingView(otherBody).toJSON(),
|
||||
})
|
||||
}
|
||||
if (type !== otherType) {
|
||||
logger.info("transaction types don't match", {
|
||||
a: new TransactionBodyLoggingView(body).toJSON(),
|
||||
b: new TransactionBodyLoggingView(otherBody).toJSON(),
|
||||
})
|
||||
return false
|
||||
}
|
||||
if (
|
||||
[
|
||||
TransactionType.COMMUNITY_ROOT,
|
||||
TransactionType.GRADIDO_CREATION,
|
||||
TransactionType.GRADIDO_DEFERRED_TRANSFER,
|
||||
].includes(type)
|
||||
) {
|
||||
logger.info(`TransactionType ${TransactionType[type]} couldn't be a CrossGroup Transaction`)
|
||||
return false
|
||||
}
|
||||
if (
|
||||
[
|
||||
TransactionType.GRADIDO_CREATION,
|
||||
TransactionType.GRADIDO_TRANSFER,
|
||||
TransactionType.GRADIDO_DEFERRED_TRANSFER,
|
||||
].includes(type)
|
||||
) {
|
||||
if (!this.self.amount || !otherTransaction.amount) {
|
||||
logger.info('missing amount')
|
||||
return false
|
||||
}
|
||||
if (this.self.amount.cmp(otherTransaction.amount.toString())) {
|
||||
logger.info('amounts mismatch', {
|
||||
a: this.self.amount.toString(),
|
||||
b: otherTransaction.amount.toString(),
|
||||
})
|
||||
return false
|
||||
}
|
||||
}
|
||||
if (body.otherGroup === otherBody.otherGroup) {
|
||||
logger.info('otherGroups are the same', {
|
||||
a: new TransactionBodyLoggingView(body).toJSON(),
|
||||
b: new TransactionBodyLoggingView(otherBody).toJSON(),
|
||||
})
|
||||
return false
|
||||
}
|
||||
if (body.memo !== otherBody.memo) {
|
||||
logger.info('memo differ', {
|
||||
a: new TransactionBodyLoggingView(body).toJSON(),
|
||||
b: new TransactionBodyLoggingView(otherBody).toJSON(),
|
||||
})
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* both must be CROSS or
|
||||
* one can be OUTBOUND and one can be INBOUND
|
||||
* no one can be LOCAL
|
||||
* @return true if crossGroupTypes are valid
|
||||
*/
|
||||
protected validCrossGroupTypes(a: CrossGroupType, b: CrossGroupType): boolean {
|
||||
logger.debug('compare ', {
|
||||
a: CrossGroupType[a],
|
||||
b: CrossGroupType[b],
|
||||
})
|
||||
if (a === CrossGroupType.LOCAL || b === CrossGroupType.LOCAL) {
|
||||
logger.info('no one can be LOCAL')
|
||||
return false
|
||||
}
|
||||
if (
|
||||
(a === CrossGroupType.INBOUND && b === CrossGroupType.OUTBOUND) ||
|
||||
(a === CrossGroupType.OUTBOUND && b === CrossGroupType.INBOUND)
|
||||
) {
|
||||
return true // One can be INBOUND and one can be OUTBOUND
|
||||
}
|
||||
return a === CrossGroupType.CROSS && b === CrossGroupType.CROSS
|
||||
}
|
||||
|
||||
public getBody(): TransactionBody {
|
||||
if (!this.transactionBody) {
|
||||
this.transactionBody = TransactionBody.fromBodyBytes(this.self.bodyBytes)
|
||||
}
|
||||
return this.transactionBody
|
||||
}
|
||||
}
|
||||
@ -12,7 +12,7 @@ export class UserLogic {
|
||||
|
||||
/**
|
||||
*
|
||||
* @param parentKeys if undefined use home community key pair
|
||||
* @param parentKeys from home community for own user
|
||||
* @returns
|
||||
*/
|
||||
|
||||
|
||||
1
dlt-connector/src/data/const.ts
Normal file
1
dlt-connector/src/data/const.ts
Normal file
@ -0,0 +1 @@
|
||||
export const TRANSMIT_TO_IOTA_INTERRUPTIVE_SLEEP_KEY = 'transmitToIota'
|
||||
@ -1,8 +1,5 @@
|
||||
import { Field, Message } from 'protobufjs'
|
||||
|
||||
import { TransactionErrorType } from '@/graphql/enum/TransactionErrorType'
|
||||
import { TransactionError } from '@/graphql/model/TransactionError'
|
||||
import { logger } from '@/logging/logger'
|
||||
import { LogError } from '@/server/LogError'
|
||||
|
||||
import { SignatureMap } from './SignatureMap'
|
||||
@ -46,14 +43,6 @@ export class GradidoTransaction extends Message<GradidoTransaction> {
|
||||
}
|
||||
|
||||
getTransactionBody(): TransactionBody {
|
||||
try {
|
||||
return TransactionBody.decode(new Uint8Array(this.bodyBytes))
|
||||
} catch (error) {
|
||||
logger.error('error decoding body from gradido transaction: %s', error)
|
||||
throw new TransactionError(
|
||||
TransactionErrorType.PROTO_DECODE_ERROR,
|
||||
'cannot decode body from gradido transaction',
|
||||
)
|
||||
}
|
||||
return TransactionBody.fromBodyBytes(this.bodyBytes)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,8 +1,11 @@
|
||||
import { Transaction } from '@entity/Transaction'
|
||||
import { Field, Message, OneOf } from 'protobufjs'
|
||||
|
||||
import { TransactionErrorType } from '@/graphql/enum/TransactionErrorType'
|
||||
import { CommunityDraft } from '@/graphql/input/CommunityDraft'
|
||||
import { TransactionDraft } from '@/graphql/input/TransactionDraft'
|
||||
import { TransactionError } from '@/graphql/model/TransactionError'
|
||||
import { logger } from '@/logging/logger'
|
||||
import { LogError } from '@/server/LogError'
|
||||
import { timestampToDate } from '@/utils/typeConverter'
|
||||
|
||||
@ -36,6 +39,18 @@ export class TransactionBody extends Message<TransactionBody> {
|
||||
}
|
||||
}
|
||||
|
||||
public static fromBodyBytes(bodyBytes: Buffer) {
|
||||
try {
|
||||
return TransactionBody.decode(new Uint8Array(bodyBytes))
|
||||
} catch (error) {
|
||||
logger.error('error decoding body from gradido transaction: %s', error)
|
||||
throw new TransactionError(
|
||||
TransactionErrorType.PROTO_DECODE_ERROR,
|
||||
'cannot decode body from gradido transaction',
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Field.d(1, 'string')
|
||||
public memo: string
|
||||
|
||||
|
||||
@ -9,9 +9,9 @@ export class UserIdentifier {
|
||||
@IsUUID('4')
|
||||
uuid: string
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
@Field(() => String)
|
||||
@IsUUID('4')
|
||||
communityUuid?: string
|
||||
communityUuid: string
|
||||
|
||||
@Field(() => Int, { defaultValue: 1, nullable: true })
|
||||
@IsPositive()
|
||||
|
||||
@ -2,11 +2,13 @@ import { Resolver, Arg, Mutation } from 'type-graphql'
|
||||
|
||||
import { TransactionDraft } from '@input/TransactionDraft'
|
||||
|
||||
import { TRANSMIT_TO_IOTA_INTERRUPTIVE_SLEEP_KEY } from '@/data/const'
|
||||
import { TransactionRepository } from '@/data/Transaction.repository'
|
||||
import { CreateTransactionRecipeContext } from '@/interactions/backendToDb/transaction/CreateTransationRecipe.context'
|
||||
import { BackendTransactionLoggingView } from '@/logging/BackendTransactionLogging.view'
|
||||
import { logger } from '@/logging/logger'
|
||||
import { TransactionLoggingView } from '@/logging/TransactionLogging.view'
|
||||
import { InterruptiveSleepManager } from '@/manager/InterruptiveSleepManager'
|
||||
import { LogError } from '@/server/LogError'
|
||||
|
||||
import { TransactionError } from '../model/TransactionError'
|
||||
@ -48,6 +50,7 @@ export class TransactionResolver {
|
||||
// we can store the transaction and with that automatic the backend transaction
|
||||
await transactionRecipe.save()
|
||||
}
|
||||
InterruptiveSleepManager.getInstance().interrupt(TRANSMIT_TO_IOTA_INTERRUPTIVE_SLEEP_KEY)
|
||||
return new TransactionResult(new TransactionRecipe(transactionRecipe))
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} catch (error: any) {
|
||||
|
||||
@ -1,17 +1,29 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { CONFIG } from '@/config'
|
||||
|
||||
import { Mnemonic } from './data/Mnemonic'
|
||||
import createServer from './server/createServer'
|
||||
import { stopTransmitToIota, transmitToIota } from './tasks/transmitToIota'
|
||||
|
||||
async function main() {
|
||||
if (CONFIG.IOTA_HOME_COMMUNITY_SEED) {
|
||||
Mnemonic.validateSeed(CONFIG.IOTA_HOME_COMMUNITY_SEED)
|
||||
}
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`DLT_CONNECTOR_PORT=${CONFIG.DLT_CONNECTOR_PORT}`)
|
||||
const { app } = await createServer()
|
||||
|
||||
// loop run all the time, check for new transaction for sending to iota
|
||||
void transmitToIota()
|
||||
app.listen(CONFIG.DLT_CONNECTOR_PORT, () => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`Server is running at http://localhost:${CONFIG.DLT_CONNECTOR_PORT}`)
|
||||
})
|
||||
|
||||
process.on('exit', () => {
|
||||
// Add shutdown logic here.
|
||||
stopTransmitToIota()
|
||||
})
|
||||
}
|
||||
|
||||
main().catch((e) => {
|
||||
|
||||
@ -0,0 +1,65 @@
|
||||
import 'reflect-metadata'
|
||||
import { Community } from '@entity/Community'
|
||||
|
||||
import { TestDB } from '@test/TestDB'
|
||||
|
||||
import { CONFIG } from '@/config'
|
||||
import { CommunityDraft } from '@/graphql/input/CommunityDraft'
|
||||
|
||||
import { AddCommunityContext } from './AddCommunity.context'
|
||||
|
||||
CONFIG.IOTA_HOME_COMMUNITY_SEED = '034b0229a2ba4e98e1cc5e8767dca886279b484303ffa73546bd5f5bf0b71285'
|
||||
|
||||
jest.mock('@typeorm/DataSource', () => ({
|
||||
getDataSource: jest.fn(() => TestDB.instance.dbConnect),
|
||||
}))
|
||||
|
||||
describe('interactions/backendToDb/community/AddCommunity Context Test', () => {
|
||||
beforeAll(async () => {
|
||||
await TestDB.instance.setupTestDB()
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await TestDB.instance.teardownTestDB()
|
||||
})
|
||||
|
||||
const homeCommunityDraft = new CommunityDraft()
|
||||
homeCommunityDraft.uuid = 'a2fd0fee-f3ba-4bef-a62a-10a34b0e2754'
|
||||
homeCommunityDraft.foreign = false
|
||||
homeCommunityDraft.createdAt = '2024-01-25T13:09:55.339Z'
|
||||
// calculated from a2fd0fee-f3ba-4bef-a62a-10a34b0e2754 with iotaTopicFromCommunityUUID
|
||||
const iotaTopic = '7be2ad83f279a3aaf6d62371cb6be301e2e3c7a3efda9c89984e8f6a7865d9ce'
|
||||
|
||||
const foreignCommunityDraft = new CommunityDraft()
|
||||
foreignCommunityDraft.uuid = '70df8de5-0fb7-4153-a124-4ff86965be9a'
|
||||
foreignCommunityDraft.foreign = true
|
||||
foreignCommunityDraft.createdAt = '2024-01-25T13:34:28.020Z'
|
||||
|
||||
it('with home community, without iota topic', async () => {
|
||||
const context = new AddCommunityContext(homeCommunityDraft)
|
||||
await context.run()
|
||||
const homeCommunity = await Community.findOneOrFail({ where: { iotaTopic } })
|
||||
expect(homeCommunity).toMatchObject({
|
||||
id: 1,
|
||||
iotaTopic,
|
||||
foreign: 0,
|
||||
rootPubkey: Buffer.from(
|
||||
'07cbf56d4b6b7b188c5f6250c0f4a01d0e44e1d422db1935eb375319ad9f9af0',
|
||||
'hex',
|
||||
),
|
||||
createdAt: new Date('2024-01-25T13:09:55.339Z'),
|
||||
})
|
||||
})
|
||||
|
||||
it('with foreign community', async () => {
|
||||
const context = new AddCommunityContext(foreignCommunityDraft, 'randomTopic')
|
||||
await context.run()
|
||||
const foreignCommunity = await Community.findOneOrFail({ where: { foreign: true } })
|
||||
expect(foreignCommunity).toMatchObject({
|
||||
id: 2,
|
||||
iotaTopic: 'randomTopic',
|
||||
foreign: 1,
|
||||
createdAt: new Date('2024-01-25T13:34:28.020Z'),
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -3,6 +3,7 @@ import { Transaction } from '@entity/Transaction'
|
||||
|
||||
import { CONFIG } from '@/config'
|
||||
import { AccountFactory } from '@/data/Account.factory'
|
||||
import { TRANSMIT_TO_IOTA_INTERRUPTIVE_SLEEP_KEY } from '@/data/const'
|
||||
import { KeyPair } from '@/data/KeyPair'
|
||||
import { Mnemonic } from '@/data/Mnemonic'
|
||||
import { TransactionErrorType } from '@/graphql/enum/TransactionErrorType'
|
||||
@ -10,6 +11,7 @@ import { CommunityDraft } from '@/graphql/input/CommunityDraft'
|
||||
import { TransactionError } from '@/graphql/model/TransactionError'
|
||||
import { CommunityLoggingView } from '@/logging/CommunityLogging.view'
|
||||
import { logger } from '@/logging/logger'
|
||||
import { InterruptiveSleepManager } from '@/manager/InterruptiveSleepManager'
|
||||
import { getDataSource } from '@/typeorm/DataSource'
|
||||
|
||||
import { CreateTransactionRecipeContext } from '../transaction/CreateTransationRecipe.context'
|
||||
@ -36,12 +38,14 @@ export class HomeCommunityRole extends CommunityRole {
|
||||
|
||||
public async store(): Promise<Community> {
|
||||
try {
|
||||
return await getDataSource().transaction(async (transactionalEntityManager) => {
|
||||
const community = await getDataSource().transaction(async (transactionalEntityManager) => {
|
||||
const community = await transactionalEntityManager.save(this.self)
|
||||
await transactionalEntityManager.save(this.transactionRecipe)
|
||||
logger.debug('store home community', new CommunityLoggingView(community))
|
||||
return community
|
||||
})
|
||||
InterruptiveSleepManager.getInstance().interrupt(TRANSMIT_TO_IOTA_INTERRUPTIVE_SLEEP_KEY)
|
||||
return community
|
||||
} catch (error) {
|
||||
logger.error('error saving home community into db: %s', error)
|
||||
throw new TransactionError(
|
||||
|
||||
@ -3,6 +3,7 @@ import { TransactionErrorType } from '@/graphql/enum/TransactionErrorType'
|
||||
import { TransactionDraft } from '@/graphql/input/TransactionDraft'
|
||||
import { UserIdentifier } from '@/graphql/input/UserIdentifier'
|
||||
import { TransactionError } from '@/graphql/model/TransactionError'
|
||||
import { iotaTopicFromCommunityUUID } from '@/utils/typeConverter'
|
||||
|
||||
export abstract class AbstractTransactionRole {
|
||||
// eslint-disable-next-line no-useless-constructor
|
||||
@ -26,7 +27,7 @@ export abstract class AbstractTransactionRole {
|
||||
* OUTBOUND: stored on 'gdd1', otherGroup: 'gdd2'
|
||||
* INBOUND: goes to receiver, stored on receiver community blockchain
|
||||
* INBOUND: stored on 'gdd2', otherGroup: 'gdd1'
|
||||
* @returns
|
||||
* @returns iota topic
|
||||
*/
|
||||
public getOtherGroup(): string {
|
||||
let user: UserIdentifier
|
||||
@ -42,7 +43,7 @@ export abstract class AbstractTransactionRole {
|
||||
'missing sender/signing user community id for cross group transaction',
|
||||
)
|
||||
}
|
||||
return user.communityUuid
|
||||
return iotaTopicFromCommunityUUID(user.communityUuid)
|
||||
case CrossGroupType.OUTBOUND:
|
||||
user = this.getRecipientUser()
|
||||
if (!user.communityUuid) {
|
||||
@ -51,7 +52,7 @@ export abstract class AbstractTransactionRole {
|
||||
'missing recipient user community id for cross group transaction',
|
||||
)
|
||||
}
|
||||
return user.communityUuid
|
||||
return iotaTopicFromCommunityUUID(user.communityUuid)
|
||||
default:
|
||||
throw new TransactionError(
|
||||
TransactionErrorType.NOT_IMPLEMENTED_YET,
|
||||
|
||||
@ -0,0 +1,340 @@
|
||||
import 'reflect-metadata'
|
||||
import { Account } from '@entity/Account'
|
||||
import { Decimal } from 'decimal.js-light'
|
||||
import { v4 } from 'uuid'
|
||||
|
||||
import { TestDB } from '@test/TestDB'
|
||||
|
||||
import { CONFIG } from '@/config'
|
||||
import { KeyPair } from '@/data/KeyPair'
|
||||
import { Mnemonic } from '@/data/Mnemonic'
|
||||
import { CrossGroupType } from '@/data/proto/3_3/enum/CrossGroupType'
|
||||
import { TransactionType } from '@/data/proto/3_3/enum/TransactionType'
|
||||
import { TransactionBody } from '@/data/proto/3_3/TransactionBody'
|
||||
import { InputTransactionType } from '@/graphql/enum/InputTransactionType'
|
||||
import { TransactionDraft } from '@/graphql/input/TransactionDraft'
|
||||
import { iotaTopicFromCommunityUUID } from '@/utils/typeConverter'
|
||||
|
||||
import { CreateTransactionRecipeContext } from './CreateTransationRecipe.context'
|
||||
|
||||
// eslint-disable-next-line import/order
|
||||
import { communitySeed } from '@test/seeding/Community.seed'
|
||||
// eslint-disable-next-line import/order
|
||||
import { createUserSet, UserSet } from '@test/seeding/UserSet.seed'
|
||||
|
||||
jest.mock('@typeorm/DataSource', () => ({
|
||||
getDataSource: jest.fn(() => TestDB.instance.dbConnect),
|
||||
}))
|
||||
|
||||
CONFIG.IOTA_HOME_COMMUNITY_SEED = '034b0229a2ba4e98e1cc5e8767dca886279b484303ffa73546bd5f5bf0b71285'
|
||||
const homeCommunityUuid = v4()
|
||||
const foreignCommunityUuid = v4()
|
||||
|
||||
const keyPair = new KeyPair(new Mnemonic(CONFIG.IOTA_HOME_COMMUNITY_SEED))
|
||||
const foreignKeyPair = new KeyPair(
|
||||
new Mnemonic('5d4e163c078cc6b51f5c88f8422bc8f21d1d59a284515ab1ea79e1c176ebec50'),
|
||||
)
|
||||
|
||||
let moderator: UserSet
|
||||
let firstUser: UserSet
|
||||
let secondUser: UserSet
|
||||
let foreignUser: UserSet
|
||||
|
||||
const topic = iotaTopicFromCommunityUUID(homeCommunityUuid)
|
||||
const foreignTopic = iotaTopicFromCommunityUUID(foreignCommunityUuid)
|
||||
|
||||
describe('interactions/backendToDb/transaction/Create Transaction Recipe Context Test', () => {
|
||||
beforeAll(async () => {
|
||||
await TestDB.instance.setupTestDB()
|
||||
await communitySeed(homeCommunityUuid, false)
|
||||
await communitySeed(foreignCommunityUuid, true, foreignKeyPair)
|
||||
|
||||
moderator = createUserSet(homeCommunityUuid, keyPair)
|
||||
firstUser = createUserSet(homeCommunityUuid, keyPair)
|
||||
secondUser = createUserSet(homeCommunityUuid, keyPair)
|
||||
foreignUser = createUserSet(foreignCommunityUuid, foreignKeyPair)
|
||||
|
||||
await Account.save([
|
||||
moderator.account,
|
||||
firstUser.account,
|
||||
secondUser.account,
|
||||
foreignUser.account,
|
||||
])
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await TestDB.instance.teardownTestDB()
|
||||
})
|
||||
|
||||
it('creation transaction', async () => {
|
||||
const creationTransactionDraft = new TransactionDraft()
|
||||
creationTransactionDraft.amount = new Decimal('2000')
|
||||
creationTransactionDraft.backendTransactionId = 1
|
||||
creationTransactionDraft.createdAt = new Date().toISOString()
|
||||
creationTransactionDraft.linkedUser = moderator.identifier
|
||||
creationTransactionDraft.user = firstUser.identifier
|
||||
creationTransactionDraft.type = InputTransactionType.CREATION
|
||||
creationTransactionDraft.targetDate = new Date().toISOString()
|
||||
const context = new CreateTransactionRecipeContext(creationTransactionDraft)
|
||||
await context.run()
|
||||
const transaction = context.getTransactionRecipe()
|
||||
|
||||
// console.log(new TransactionLoggingView(transaction))
|
||||
expect(transaction).toMatchObject({
|
||||
type: TransactionType.GRADIDO_CREATION,
|
||||
protocolVersion: '3.3',
|
||||
community: {
|
||||
rootPubkey: keyPair.publicKey,
|
||||
foreign: 0,
|
||||
iotaTopic: topic,
|
||||
},
|
||||
signingAccount: {
|
||||
derive2Pubkey: moderator.account.derive2Pubkey,
|
||||
},
|
||||
recipientAccount: {
|
||||
derive2Pubkey: firstUser.account.derive2Pubkey,
|
||||
},
|
||||
amount: new Decimal(2000),
|
||||
backendTransactions: [
|
||||
{
|
||||
typeId: InputTransactionType.CREATION,
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
const body = TransactionBody.fromBodyBytes(transaction.bodyBytes)
|
||||
// console.log(new TransactionBodyLoggingView(body))
|
||||
expect(body.creation).toBeDefined()
|
||||
if (!body.creation) throw new Error()
|
||||
const bodyReceiverPubkey = Buffer.from(body.creation.recipient.pubkey)
|
||||
expect(bodyReceiverPubkey.compare(firstUser.account.derive2Pubkey)).toBe(0)
|
||||
|
||||
expect(body).toMatchObject({
|
||||
type: CrossGroupType.LOCAL,
|
||||
creation: {
|
||||
recipient: {
|
||||
amount: '2000',
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('local send transaction', async () => {
|
||||
const sendTransactionDraft = new TransactionDraft()
|
||||
sendTransactionDraft.amount = new Decimal('100')
|
||||
sendTransactionDraft.backendTransactionId = 2
|
||||
sendTransactionDraft.createdAt = new Date().toISOString()
|
||||
sendTransactionDraft.linkedUser = secondUser.identifier
|
||||
sendTransactionDraft.user = firstUser.identifier
|
||||
sendTransactionDraft.type = InputTransactionType.SEND
|
||||
const context = new CreateTransactionRecipeContext(sendTransactionDraft)
|
||||
await context.run()
|
||||
const transaction = context.getTransactionRecipe()
|
||||
|
||||
// console.log(new TransactionLoggingView(transaction))
|
||||
expect(transaction).toMatchObject({
|
||||
type: TransactionType.GRADIDO_TRANSFER,
|
||||
protocolVersion: '3.3',
|
||||
community: {
|
||||
rootPubkey: keyPair.publicKey,
|
||||
foreign: 0,
|
||||
iotaTopic: topic,
|
||||
},
|
||||
signingAccount: {
|
||||
derive2Pubkey: firstUser.account.derive2Pubkey,
|
||||
},
|
||||
recipientAccount: {
|
||||
derive2Pubkey: secondUser.account.derive2Pubkey,
|
||||
},
|
||||
amount: new Decimal(100),
|
||||
backendTransactions: [
|
||||
{
|
||||
typeId: InputTransactionType.SEND,
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
const body = TransactionBody.fromBodyBytes(transaction.bodyBytes)
|
||||
// console.log(new TransactionBodyLoggingView(body))
|
||||
expect(body.transfer).toBeDefined()
|
||||
if (!body.transfer) throw new Error()
|
||||
expect(Buffer.from(body.transfer.recipient).compare(secondUser.account.derive2Pubkey)).toBe(0)
|
||||
expect(Buffer.from(body.transfer.sender.pubkey).compare(firstUser.account.derive2Pubkey)).toBe(
|
||||
0,
|
||||
)
|
||||
expect(body).toMatchObject({
|
||||
type: CrossGroupType.LOCAL,
|
||||
transfer: {
|
||||
sender: {
|
||||
amount: '100',
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('local recv transaction', async () => {
|
||||
const recvTransactionDraft = new TransactionDraft()
|
||||
recvTransactionDraft.amount = new Decimal('100')
|
||||
recvTransactionDraft.backendTransactionId = 3
|
||||
recvTransactionDraft.createdAt = new Date().toISOString()
|
||||
recvTransactionDraft.linkedUser = firstUser.identifier
|
||||
recvTransactionDraft.user = secondUser.identifier
|
||||
recvTransactionDraft.type = InputTransactionType.RECEIVE
|
||||
const context = new CreateTransactionRecipeContext(recvTransactionDraft)
|
||||
await context.run()
|
||||
const transaction = context.getTransactionRecipe()
|
||||
// console.log(new TransactionLoggingView(transaction))
|
||||
expect(transaction).toMatchObject({
|
||||
type: TransactionType.GRADIDO_TRANSFER,
|
||||
protocolVersion: '3.3',
|
||||
community: {
|
||||
rootPubkey: keyPair.publicKey,
|
||||
foreign: 0,
|
||||
iotaTopic: topic,
|
||||
},
|
||||
signingAccount: {
|
||||
derive2Pubkey: firstUser.account.derive2Pubkey,
|
||||
},
|
||||
recipientAccount: {
|
||||
derive2Pubkey: secondUser.account.derive2Pubkey,
|
||||
},
|
||||
amount: new Decimal(100),
|
||||
backendTransactions: [
|
||||
{
|
||||
typeId: InputTransactionType.RECEIVE,
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
const body = TransactionBody.fromBodyBytes(transaction.bodyBytes)
|
||||
// console.log(new TransactionBodyLoggingView(body))
|
||||
expect(body.transfer).toBeDefined()
|
||||
if (!body.transfer) throw new Error()
|
||||
expect(Buffer.from(body.transfer.recipient).compare(secondUser.account.derive2Pubkey)).toBe(0)
|
||||
expect(Buffer.from(body.transfer.sender.pubkey).compare(firstUser.account.derive2Pubkey)).toBe(
|
||||
0,
|
||||
)
|
||||
expect(body).toMatchObject({
|
||||
type: CrossGroupType.LOCAL,
|
||||
transfer: {
|
||||
sender: {
|
||||
amount: '100',
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('cross group send transaction', async () => {
|
||||
const crossGroupSendTransactionDraft = new TransactionDraft()
|
||||
crossGroupSendTransactionDraft.amount = new Decimal('100')
|
||||
crossGroupSendTransactionDraft.backendTransactionId = 4
|
||||
crossGroupSendTransactionDraft.createdAt = new Date().toISOString()
|
||||
crossGroupSendTransactionDraft.linkedUser = foreignUser.identifier
|
||||
crossGroupSendTransactionDraft.user = firstUser.identifier
|
||||
crossGroupSendTransactionDraft.type = InputTransactionType.SEND
|
||||
const context = new CreateTransactionRecipeContext(crossGroupSendTransactionDraft)
|
||||
await context.run()
|
||||
const transaction = context.getTransactionRecipe()
|
||||
// console.log(new TransactionLoggingView(transaction))
|
||||
expect(transaction).toMatchObject({
|
||||
type: TransactionType.GRADIDO_TRANSFER,
|
||||
protocolVersion: '3.3',
|
||||
community: {
|
||||
rootPubkey: keyPair.publicKey,
|
||||
foreign: 0,
|
||||
iotaTopic: topic,
|
||||
},
|
||||
otherCommunity: {
|
||||
rootPubkey: foreignKeyPair.publicKey,
|
||||
foreign: 1,
|
||||
iotaTopic: foreignTopic,
|
||||
},
|
||||
signingAccount: {
|
||||
derive2Pubkey: firstUser.account.derive2Pubkey,
|
||||
},
|
||||
recipientAccount: {
|
||||
derive2Pubkey: foreignUser.account.derive2Pubkey,
|
||||
},
|
||||
amount: new Decimal(100),
|
||||
backendTransactions: [
|
||||
{
|
||||
typeId: InputTransactionType.SEND,
|
||||
},
|
||||
],
|
||||
})
|
||||
const body = TransactionBody.fromBodyBytes(transaction.bodyBytes)
|
||||
// console.log(new TransactionBodyLoggingView(body))
|
||||
expect(body.transfer).toBeDefined()
|
||||
if (!body.transfer) throw new Error()
|
||||
expect(Buffer.from(body.transfer.recipient).compare(foreignUser.account.derive2Pubkey)).toBe(0)
|
||||
expect(Buffer.from(body.transfer.sender.pubkey).compare(firstUser.account.derive2Pubkey)).toBe(
|
||||
0,
|
||||
)
|
||||
expect(body).toMatchObject({
|
||||
type: CrossGroupType.OUTBOUND,
|
||||
otherGroup: foreignTopic,
|
||||
transfer: {
|
||||
sender: {
|
||||
amount: '100',
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('cross group recv transaction', async () => {
|
||||
const crossGroupRecvTransactionDraft = new TransactionDraft()
|
||||
crossGroupRecvTransactionDraft.amount = new Decimal('100')
|
||||
crossGroupRecvTransactionDraft.backendTransactionId = 5
|
||||
crossGroupRecvTransactionDraft.createdAt = new Date().toISOString()
|
||||
crossGroupRecvTransactionDraft.linkedUser = firstUser.identifier
|
||||
crossGroupRecvTransactionDraft.user = foreignUser.identifier
|
||||
crossGroupRecvTransactionDraft.type = InputTransactionType.RECEIVE
|
||||
const context = new CreateTransactionRecipeContext(crossGroupRecvTransactionDraft)
|
||||
await context.run()
|
||||
const transaction = context.getTransactionRecipe()
|
||||
// console.log(new TransactionLoggingView(transaction))
|
||||
expect(transaction).toMatchObject({
|
||||
type: TransactionType.GRADIDO_TRANSFER,
|
||||
protocolVersion: '3.3',
|
||||
community: {
|
||||
rootPubkey: foreignKeyPair.publicKey,
|
||||
foreign: 1,
|
||||
iotaTopic: foreignTopic,
|
||||
},
|
||||
otherCommunity: {
|
||||
rootPubkey: keyPair.publicKey,
|
||||
foreign: 0,
|
||||
iotaTopic: topic,
|
||||
},
|
||||
signingAccount: {
|
||||
derive2Pubkey: firstUser.account.derive2Pubkey,
|
||||
},
|
||||
recipientAccount: {
|
||||
derive2Pubkey: foreignUser.account.derive2Pubkey,
|
||||
},
|
||||
amount: new Decimal(100),
|
||||
backendTransactions: [
|
||||
{
|
||||
typeId: InputTransactionType.RECEIVE,
|
||||
},
|
||||
],
|
||||
})
|
||||
const body = TransactionBody.fromBodyBytes(transaction.bodyBytes)
|
||||
// console.log(new TransactionBodyLoggingView(body))
|
||||
expect(body.transfer).toBeDefined()
|
||||
if (!body.transfer) throw new Error()
|
||||
expect(Buffer.from(body.transfer.recipient).compare(foreignUser.account.derive2Pubkey)).toBe(0)
|
||||
expect(Buffer.from(body.transfer.sender.pubkey).compare(firstUser.account.derive2Pubkey)).toBe(
|
||||
0,
|
||||
)
|
||||
expect(body).toMatchObject({
|
||||
type: CrossGroupType.INBOUND,
|
||||
otherGroup: topic,
|
||||
transfer: {
|
||||
sender: {
|
||||
amount: '100',
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -1,5 +1,12 @@
|
||||
import { Community } from '@entity/Community'
|
||||
|
||||
import { CommunityRepository } from '@/data/Community.repository'
|
||||
import { CrossGroupType } from '@/data/proto/3_3/enum/CrossGroupType'
|
||||
import { TransactionErrorType } from '@/graphql/enum/TransactionErrorType'
|
||||
import { UserIdentifier } from '@/graphql/input/UserIdentifier'
|
||||
import { TransactionError } from '@/graphql/model/TransactionError'
|
||||
import { logger } from '@/logging/logger'
|
||||
import { UserIdentifierLoggingView } from '@/logging/UserIdentifierLogging.view'
|
||||
|
||||
import { AbstractTransactionRole } from './AbstractTransaction.role'
|
||||
|
||||
@ -15,4 +22,26 @@ export class CreationTransactionRole extends AbstractTransactionRole {
|
||||
public getCrossGroupType(): CrossGroupType {
|
||||
return CrossGroupType.LOCAL
|
||||
}
|
||||
|
||||
public async getCommunity(): Promise<Community> {
|
||||
if (this.self.user.communityUuid !== this.self.linkedUser.communityUuid) {
|
||||
throw new TransactionError(
|
||||
TransactionErrorType.LOGIC_ERROR,
|
||||
'mismatch community uuids on creation transaction',
|
||||
)
|
||||
}
|
||||
const community = await CommunityRepository.getCommunityForUserIdentifier(this.self.user)
|
||||
if (!community) {
|
||||
logger.error(
|
||||
'missing community for user identifier',
|
||||
new UserIdentifierLoggingView(this.self.user),
|
||||
)
|
||||
throw new TransactionError(TransactionErrorType.NOT_FOUND, "couldn't find community for user")
|
||||
}
|
||||
return community
|
||||
}
|
||||
|
||||
public async getOtherCommunity(): Promise<Community | null> {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,6 +1,9 @@
|
||||
import { Community } from '@entity/Community'
|
||||
import { Transaction } from '@entity/Transaction'
|
||||
|
||||
import { AccountLogic } from '@/data/Account.logic'
|
||||
import { KeyPair } from '@/data/KeyPair'
|
||||
import { CrossGroupType } from '@/data/proto/3_3/enum/CrossGroupType'
|
||||
import { TransactionBodyBuilder } from '@/data/proto/TransactionBody.builder'
|
||||
import { TransactionBuilder } from '@/data/Transaction.builder'
|
||||
import { UserRepository } from '@/data/User.repository'
|
||||
@ -52,18 +55,34 @@ export class TransactionRecipeRole {
|
||||
this.transactionBuilder
|
||||
.fromTransactionBodyBuilder(transactionBodyBuilder)
|
||||
.addBackendTransaction(transactionDraft)
|
||||
await this.transactionBuilder.setSenderCommunityFromSenderUser(signingUser)
|
||||
|
||||
await this.transactionBuilder.setCommunityFromUser(transactionDraft.user)
|
||||
if (recipientUser.communityUuid !== signingUser.communityUuid) {
|
||||
await this.transactionBuilder.setOtherCommunityFromRecipientUser(recipientUser)
|
||||
await this.transactionBuilder.setOtherCommunityFromUser(transactionDraft.linkedUser)
|
||||
}
|
||||
const transaction = this.transactionBuilder.getTransaction()
|
||||
const communityKeyPair = new KeyPair(
|
||||
this.getSigningCommunity(transactionTypeRole.getCrossGroupType()),
|
||||
)
|
||||
const accountLogic = new AccountLogic(signingAccount)
|
||||
// sign
|
||||
this.transactionBuilder.setSignature(
|
||||
new KeyPair(this.transactionBuilder.getCommunity()).sign(transaction.bodyBytes),
|
||||
accountLogic.calculateKeyPair(communityKeyPair).sign(transaction.bodyBytes),
|
||||
)
|
||||
return this
|
||||
}
|
||||
|
||||
public getSigningCommunity(crossGroupType: CrossGroupType): Community {
|
||||
if (crossGroupType === CrossGroupType.INBOUND) {
|
||||
const otherCommunity = this.transactionBuilder.getOtherCommunity()
|
||||
if (!otherCommunity) {
|
||||
throw new TransactionError(TransactionErrorType.NOT_FOUND, 'missing other community')
|
||||
}
|
||||
return otherCommunity
|
||||
}
|
||||
return this.transactionBuilder.getCommunity()
|
||||
}
|
||||
|
||||
public getTransaction(): Transaction {
|
||||
return this.transactionBuilder.getTransaction()
|
||||
}
|
||||
|
||||
@ -0,0 +1,90 @@
|
||||
import { Transaction } from '@entity/Transaction'
|
||||
|
||||
import { sendMessage as iotaSendMessage } from '@/client/IotaClient'
|
||||
import { KeyPair } from '@/data/KeyPair'
|
||||
import { GradidoTransaction } from '@/data/proto/3_3/GradidoTransaction'
|
||||
import { SignaturePair } from '@/data/proto/3_3/SignaturePair'
|
||||
import { TransactionBody } from '@/data/proto/3_3/TransactionBody'
|
||||
import { TransactionErrorType } from '@/graphql/enum/TransactionErrorType'
|
||||
import { TransactionError } from '@/graphql/model/TransactionError'
|
||||
import { GradidoTransactionLoggingView } from '@/logging/GradidoTransactionLogging.view'
|
||||
import { logger } from '@/logging/logger'
|
||||
|
||||
export abstract class AbstractTransactionRecipeRole {
|
||||
protected transactionBody: TransactionBody
|
||||
public constructor(protected self: Transaction) {
|
||||
this.transactionBody = TransactionBody.fromBodyBytes(this.self.bodyBytes)
|
||||
}
|
||||
|
||||
public abstract transmitToIota(): Promise<Transaction>
|
||||
|
||||
protected getGradidoTransaction(): GradidoTransaction {
|
||||
const transaction = new GradidoTransaction(this.transactionBody)
|
||||
if (!this.self.signature) {
|
||||
throw new TransactionError(
|
||||
TransactionErrorType.MISSING_PARAMETER,
|
||||
'missing signature in transaction recipe',
|
||||
)
|
||||
}
|
||||
const signaturePair = new SignaturePair()
|
||||
if (this.self.signature.length !== 64) {
|
||||
throw new TransactionError(TransactionErrorType.INVALID_SIGNATURE, "signature isn't 64 bytes")
|
||||
}
|
||||
signaturePair.signature = this.self.signature
|
||||
if (this.transactionBody.communityRoot) {
|
||||
const publicKey = this.self.community.rootPubkey
|
||||
if (!publicKey) {
|
||||
throw new TransactionError(
|
||||
TransactionErrorType.MISSING_PARAMETER,
|
||||
'missing community public key for community root transaction',
|
||||
)
|
||||
}
|
||||
signaturePair.pubKey = publicKey
|
||||
} else if (this.self.signingAccount) {
|
||||
const publicKey = this.self.signingAccount.derive2Pubkey
|
||||
if (!publicKey) {
|
||||
throw new TransactionError(
|
||||
TransactionErrorType.MISSING_PARAMETER,
|
||||
'missing signing account public key for transaction',
|
||||
)
|
||||
}
|
||||
signaturePair.pubKey = publicKey
|
||||
} else {
|
||||
throw new TransactionError(
|
||||
TransactionErrorType.NOT_FOUND,
|
||||
"signingAccount not exist and it isn't a community root transaction",
|
||||
)
|
||||
}
|
||||
if (signaturePair.validate()) {
|
||||
transaction.sigMap.sigPair.push(signaturePair)
|
||||
}
|
||||
if (!KeyPair.verify(transaction.bodyBytes, signaturePair)) {
|
||||
logger.debug('invalid signature', new GradidoTransactionLoggingView(transaction))
|
||||
throw new TransactionError(TransactionErrorType.INVALID_SIGNATURE, 'signature is invalid')
|
||||
}
|
||||
return transaction
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param gradidoTransaction
|
||||
* @param topic
|
||||
* @return iota message id
|
||||
*/
|
||||
protected async sendViaIota(
|
||||
gradidoTransaction: GradidoTransaction,
|
||||
topic: string,
|
||||
): Promise<Buffer> {
|
||||
// protobuf serializing function
|
||||
const messageBuffer = GradidoTransaction.encode(gradidoTransaction).finish()
|
||||
const resultMessage = await iotaSendMessage(
|
||||
messageBuffer,
|
||||
Uint8Array.from(Buffer.from(topic, 'hex')),
|
||||
)
|
||||
logger.info('transmitted Gradido Transaction to Iota', {
|
||||
id: this.self.id,
|
||||
messageId: resultMessage.messageId,
|
||||
})
|
||||
return Buffer.from(resultMessage.messageId, 'hex')
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,40 @@
|
||||
import { Transaction } from '@entity/Transaction'
|
||||
|
||||
import { TransactionLogic } from '@/data/Transaction.logic'
|
||||
import { logger } from '@/logging/logger'
|
||||
import { TransactionLoggingView } from '@/logging/TransactionLogging.view'
|
||||
import { LogError } from '@/server/LogError'
|
||||
|
||||
import { AbstractTransactionRecipeRole } from './AbstractTransactionRecipe.role'
|
||||
|
||||
/**
|
||||
* Inbound Transaction on recipient community, mark the gradidos as received from another community
|
||||
* need to set gradido id from OUTBOUND transaction!
|
||||
*/
|
||||
export class InboundTransactionRecipeRole extends AbstractTransactionRecipeRole {
|
||||
public async transmitToIota(): Promise<Transaction> {
|
||||
logger.debug('transmit INBOUND transaction to iota', new TransactionLoggingView(this.self))
|
||||
const gradidoTransaction = this.getGradidoTransaction()
|
||||
const pairingTransaction = await new TransactionLogic(this.self).findPairTransaction()
|
||||
if (!pairingTransaction.iotaMessageId || pairingTransaction.iotaMessageId.length !== 32) {
|
||||
throw new LogError(
|
||||
'missing iota message id in pairing transaction, was it already send?',
|
||||
new TransactionLoggingView(pairingTransaction),
|
||||
)
|
||||
}
|
||||
gradidoTransaction.parentMessageId = pairingTransaction.iotaMessageId
|
||||
this.self.pairingTransactionId = pairingTransaction.id
|
||||
this.self.pairingTransaction = pairingTransaction
|
||||
pairingTransaction.pairingTransactionId = this.self.id
|
||||
|
||||
if (!this.self.otherCommunity) {
|
||||
throw new LogError('missing other community')
|
||||
}
|
||||
|
||||
this.self.iotaMessageId = await this.sendViaIota(
|
||||
gradidoTransaction,
|
||||
this.self.otherCommunity.iotaTopic,
|
||||
)
|
||||
return this.self
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,25 @@
|
||||
import { Transaction } from '@entity/Transaction'
|
||||
|
||||
import { CrossGroupType } from '@/data/proto/3_3/enum/CrossGroupType'
|
||||
import { logger } from '@/logging/logger'
|
||||
import { TransactionLoggingView } from '@/logging/TransactionLogging.view'
|
||||
|
||||
import { AbstractTransactionRecipeRole } from './AbstractTransactionRecipe.role'
|
||||
|
||||
export class LocalTransactionRecipeRole extends AbstractTransactionRecipeRole {
|
||||
public async transmitToIota(): Promise<Transaction> {
|
||||
let transactionCrossGroupTypeName = 'LOCAL'
|
||||
if (this.transactionBody) {
|
||||
transactionCrossGroupTypeName = CrossGroupType[this.transactionBody.type]
|
||||
}
|
||||
logger.debug(
|
||||
`transmit ${transactionCrossGroupTypeName} transaction to iota`,
|
||||
new TransactionLoggingView(this.self),
|
||||
)
|
||||
this.self.iotaMessageId = await this.sendViaIota(
|
||||
this.getGradidoTransaction(),
|
||||
this.self.community.iotaTopic,
|
||||
)
|
||||
return this.self
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,6 @@
|
||||
import { LocalTransactionRecipeRole } from './LocalTransactionRecipe.role'
|
||||
|
||||
/**
|
||||
* Outbound Transaction on sender community, mark the gradidos as sended out of community
|
||||
*/
|
||||
export class OutboundTransactionRecipeRole extends LocalTransactionRecipeRole {}
|
||||
@ -0,0 +1,168 @@
|
||||
import 'reflect-metadata'
|
||||
import { Account } from '@entity/Account'
|
||||
import { Decimal } from 'decimal.js-light'
|
||||
import { v4 } from 'uuid'
|
||||
|
||||
import { TestDB } from '@test/TestDB'
|
||||
|
||||
import { CONFIG } from '@/config'
|
||||
import { KeyPair } from '@/data/KeyPair'
|
||||
import { Mnemonic } from '@/data/Mnemonic'
|
||||
import { CrossGroupType } from '@/data/proto/3_3/enum/CrossGroupType'
|
||||
import { TransactionBody } from '@/data/proto/3_3/TransactionBody'
|
||||
import { InputTransactionType } from '@/graphql/enum/InputTransactionType'
|
||||
import { TransactionDraft } from '@/graphql/input/TransactionDraft'
|
||||
import { logger } from '@/logging/logger'
|
||||
|
||||
import { CreateTransactionRecipeContext } from '../backendToDb/transaction/CreateTransationRecipe.context'
|
||||
|
||||
import { TransmitToIotaContext } from './TransmitToIota.context'
|
||||
|
||||
// eslint-disable-next-line import/order
|
||||
import { communitySeed } from '@test/seeding/Community.seed'
|
||||
// eslint-disable-next-line import/order
|
||||
import { createUserSet, UserSet } from '@test/seeding/UserSet.seed'
|
||||
|
||||
jest.mock('@typeorm/DataSource', () => ({
|
||||
getDataSource: jest.fn(() => TestDB.instance.dbConnect),
|
||||
}))
|
||||
|
||||
jest.mock('@/client/IotaClient', () => {
|
||||
return {
|
||||
sendMessage: jest.fn().mockReturnValue({
|
||||
messageId: '5498130bc3918e1a7143969ce05805502417e3e1bd596d3c44d6a0adeea22710',
|
||||
}),
|
||||
}
|
||||
})
|
||||
|
||||
CONFIG.IOTA_HOME_COMMUNITY_SEED = '034b0229a2ba4e98e1cc5e8767dca886279b484303ffa73546bd5f5bf0b71285'
|
||||
const homeCommunityUuid = v4()
|
||||
const foreignCommunityUuid = v4()
|
||||
|
||||
const keyPair = new KeyPair(new Mnemonic(CONFIG.IOTA_HOME_COMMUNITY_SEED))
|
||||
const foreignKeyPair = new KeyPair(
|
||||
new Mnemonic('5d4e163c078cc6b51f5c88f8422bc8f21d1d59a284515ab1ea79e1c176ebec50'),
|
||||
)
|
||||
|
||||
let moderator: UserSet
|
||||
let firstUser: UserSet
|
||||
let secondUser: UserSet
|
||||
let foreignUser: UserSet
|
||||
|
||||
const now = new Date()
|
||||
|
||||
describe('interactions/transmitToIota/TransmitToIotaContext', () => {
|
||||
beforeAll(async () => {
|
||||
await TestDB.instance.setupTestDB()
|
||||
await communitySeed(homeCommunityUuid, false)
|
||||
await communitySeed(foreignCommunityUuid, true, foreignKeyPair)
|
||||
|
||||
moderator = createUserSet(homeCommunityUuid, keyPair)
|
||||
firstUser = createUserSet(homeCommunityUuid, keyPair)
|
||||
secondUser = createUserSet(homeCommunityUuid, keyPair)
|
||||
foreignUser = createUserSet(foreignCommunityUuid, foreignKeyPair)
|
||||
|
||||
await Account.save([
|
||||
moderator.account,
|
||||
firstUser.account,
|
||||
secondUser.account,
|
||||
foreignUser.account,
|
||||
])
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await TestDB.instance.teardownTestDB()
|
||||
})
|
||||
|
||||
it('LOCAL transaction', async () => {
|
||||
const creationTransactionDraft = new TransactionDraft()
|
||||
creationTransactionDraft.amount = new Decimal('2000')
|
||||
creationTransactionDraft.backendTransactionId = 1
|
||||
creationTransactionDraft.createdAt = new Date().toISOString()
|
||||
creationTransactionDraft.linkedUser = moderator.identifier
|
||||
creationTransactionDraft.user = firstUser.identifier
|
||||
creationTransactionDraft.type = InputTransactionType.CREATION
|
||||
creationTransactionDraft.targetDate = new Date().toISOString()
|
||||
const transactionRecipeContext = new CreateTransactionRecipeContext(creationTransactionDraft)
|
||||
await transactionRecipeContext.run()
|
||||
const transaction = transactionRecipeContext.getTransactionRecipe()
|
||||
|
||||
const context = new TransmitToIotaContext(transaction)
|
||||
const debugSpy = jest.spyOn(logger, 'debug')
|
||||
await context.run()
|
||||
expect(
|
||||
transaction.iotaMessageId?.compare(
|
||||
Buffer.from('5498130bc3918e1a7143969ce05805502417e3e1bd596d3c44d6a0adeea22710', 'hex'),
|
||||
),
|
||||
).toBe(0)
|
||||
expect(debugSpy).toHaveBeenNthCalledWith(
|
||||
3,
|
||||
expect.stringContaining('transmit LOCAL transaction to iota'),
|
||||
expect.objectContaining({}),
|
||||
)
|
||||
})
|
||||
|
||||
it('OUTBOUND transaction', async () => {
|
||||
const crossGroupSendTransactionDraft = new TransactionDraft()
|
||||
crossGroupSendTransactionDraft.amount = new Decimal('100')
|
||||
crossGroupSendTransactionDraft.backendTransactionId = 4
|
||||
crossGroupSendTransactionDraft.createdAt = now.toISOString()
|
||||
crossGroupSendTransactionDraft.linkedUser = foreignUser.identifier
|
||||
crossGroupSendTransactionDraft.user = firstUser.identifier
|
||||
crossGroupSendTransactionDraft.type = InputTransactionType.SEND
|
||||
const transactionRecipeContext = new CreateTransactionRecipeContext(
|
||||
crossGroupSendTransactionDraft,
|
||||
)
|
||||
await transactionRecipeContext.run()
|
||||
const transaction = transactionRecipeContext.getTransactionRecipe()
|
||||
await transaction.save()
|
||||
const body = TransactionBody.fromBodyBytes(transaction.bodyBytes)
|
||||
expect(body.type).toBe(CrossGroupType.OUTBOUND)
|
||||
const context = new TransmitToIotaContext(transaction)
|
||||
const debugSpy = jest.spyOn(logger, 'debug')
|
||||
await context.run()
|
||||
expect(
|
||||
transaction.iotaMessageId?.compare(
|
||||
Buffer.from('5498130bc3918e1a7143969ce05805502417e3e1bd596d3c44d6a0adeea22710', 'hex'),
|
||||
),
|
||||
).toBe(0)
|
||||
expect(debugSpy).toHaveBeenNthCalledWith(
|
||||
5,
|
||||
expect.stringContaining('transmit OUTBOUND transaction to iota'),
|
||||
expect.objectContaining({}),
|
||||
)
|
||||
})
|
||||
|
||||
it('INBOUND transaction', async () => {
|
||||
const crossGroupRecvTransactionDraft = new TransactionDraft()
|
||||
crossGroupRecvTransactionDraft.amount = new Decimal('100')
|
||||
crossGroupRecvTransactionDraft.backendTransactionId = 5
|
||||
crossGroupRecvTransactionDraft.createdAt = now.toISOString()
|
||||
crossGroupRecvTransactionDraft.linkedUser = firstUser.identifier
|
||||
crossGroupRecvTransactionDraft.user = foreignUser.identifier
|
||||
crossGroupRecvTransactionDraft.type = InputTransactionType.RECEIVE
|
||||
const transactionRecipeContext = new CreateTransactionRecipeContext(
|
||||
crossGroupRecvTransactionDraft,
|
||||
)
|
||||
await transactionRecipeContext.run()
|
||||
const transaction = transactionRecipeContext.getTransactionRecipe()
|
||||
await transaction.save()
|
||||
// console.log(new TransactionLoggingView(transaction))
|
||||
const body = TransactionBody.fromBodyBytes(transaction.bodyBytes)
|
||||
expect(body.type).toBe(CrossGroupType.INBOUND)
|
||||
|
||||
const context = new TransmitToIotaContext(transaction)
|
||||
const debugSpy = jest.spyOn(logger, 'debug')
|
||||
await context.run()
|
||||
expect(
|
||||
transaction.iotaMessageId?.compare(
|
||||
Buffer.from('5498130bc3918e1a7143969ce05805502417e3e1bd596d3c44d6a0adeea22710', 'hex'),
|
||||
),
|
||||
).toBe(0)
|
||||
expect(debugSpy).toHaveBeenNthCalledWith(
|
||||
7,
|
||||
expect.stringContaining('transmit INBOUND transaction to iota'),
|
||||
expect.objectContaining({}),
|
||||
)
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,57 @@
|
||||
import { Transaction } from '@entity/Transaction'
|
||||
|
||||
import { CrossGroupType } from '@/data/proto/3_3/enum/CrossGroupType'
|
||||
import { TransactionBody } from '@/data/proto/3_3/TransactionBody'
|
||||
import { logger } from '@/logging/logger'
|
||||
import { TransactionLoggingView } from '@/logging/TransactionLogging.view'
|
||||
import { LogError } from '@/server/LogError'
|
||||
import { getDataSource } from '@/typeorm/DataSource'
|
||||
|
||||
import { AbstractTransactionRecipeRole } from './AbstractTransactionRecipe.role'
|
||||
import { InboundTransactionRecipeRole } from './InboundTransactionRecipe.role'
|
||||
import { LocalTransactionRecipeRole } from './LocalTransactionRecipe.role'
|
||||
import { OutboundTransactionRecipeRole } from './OutboundTransactionRecipeRole'
|
||||
|
||||
/**
|
||||
* @DCI-Context
|
||||
* Context for sending transaction recipe to iota
|
||||
* send every transaction only once to iota!
|
||||
*/
|
||||
export class TransmitToIotaContext {
|
||||
private transactionRecipeRole: AbstractTransactionRecipeRole
|
||||
|
||||
public constructor(transaction: Transaction) {
|
||||
const transactionBody = TransactionBody.fromBodyBytes(transaction.bodyBytes)
|
||||
switch (transactionBody.type) {
|
||||
case CrossGroupType.LOCAL:
|
||||
this.transactionRecipeRole = new LocalTransactionRecipeRole(transaction)
|
||||
break
|
||||
case CrossGroupType.INBOUND:
|
||||
this.transactionRecipeRole = new InboundTransactionRecipeRole(transaction)
|
||||
break
|
||||
case CrossGroupType.OUTBOUND:
|
||||
this.transactionRecipeRole = new OutboundTransactionRecipeRole(transaction)
|
||||
break
|
||||
default:
|
||||
throw new LogError('unknown cross group type', transactionBody.type)
|
||||
}
|
||||
}
|
||||
|
||||
public async run(): Promise<void> {
|
||||
const transaction = await this.transactionRecipeRole.transmitToIota()
|
||||
logger.debug('transaction sended via iota', new TransactionLoggingView(transaction))
|
||||
// store changes in db
|
||||
// prevent endless loop
|
||||
const pairingTransaction = transaction.pairingTransaction
|
||||
if (pairingTransaction) {
|
||||
transaction.pairingTransaction = undefined
|
||||
await getDataSource().transaction(async (transactionalEntityManager) => {
|
||||
await transactionalEntityManager.save(transaction)
|
||||
await transactionalEntityManager.save(pairingTransaction)
|
||||
})
|
||||
} else {
|
||||
await transaction.save()
|
||||
}
|
||||
logger.info('sended transaction successfully updated in db')
|
||||
}
|
||||
}
|
||||
@ -16,7 +16,7 @@ export class AccountLoggingView extends AbstractLoggingView {
|
||||
id: this.account.id,
|
||||
user: this.account.user ? new UserLoggingView(this.account.user).toJSON() : null,
|
||||
derivationIndex: this.account.derivationIndex,
|
||||
derive2pubkey: this.account.derive2Pubkey.toString(this.bufferStringFormat),
|
||||
derive2Pubkey: this.account.derive2Pubkey.toString(this.bufferStringFormat),
|
||||
type: getEnumValue(AddressType, this.account.type),
|
||||
createdAt: this.dateToString(this.account.createdAt),
|
||||
confirmedAt: this.dateToString(this.account.confirmedAt),
|
||||
|
||||
@ -18,7 +18,7 @@ export class TransactionLoggingView extends AbstractLoggingView {
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
public toJSON(showBackendTransactions = true): any {
|
||||
public toJSON(showBackendTransactions = true, deep = 1): any {
|
||||
return {
|
||||
id: this.self.id,
|
||||
nr: this.self.nr,
|
||||
@ -31,16 +31,23 @@ export class TransactionLoggingView extends AbstractLoggingView {
|
||||
community: new CommunityLoggingView(this.self.community).toJSON(),
|
||||
otherCommunity: this.self.otherCommunity
|
||||
? new CommunityLoggingView(this.self.otherCommunity)
|
||||
: undefined,
|
||||
: { id: this.self.otherCommunityId },
|
||||
iotaMessageId: this.self.iotaMessageId
|
||||
? this.self.iotaMessageId.toString(this.bufferStringFormat)
|
||||
: undefined,
|
||||
signingAccount: this.self.signingAccount
|
||||
? new AccountLoggingView(this.self.signingAccount)
|
||||
: undefined,
|
||||
: { id: this.self.signingAccountId },
|
||||
recipientAccount: this.self.recipientAccount
|
||||
? new AccountLoggingView(this.self.recipientAccount)
|
||||
: undefined,
|
||||
: { id: this.self.recipientAccountId },
|
||||
pairingTransaction:
|
||||
this.self.pairingTransaction && deep === 1
|
||||
? new TransactionLoggingView(this.self.pairingTransaction).toJSON(
|
||||
showBackendTransactions,
|
||||
deep + 1,
|
||||
)
|
||||
: { id: this.self.pairingTransaction },
|
||||
amount: this.decimalToString(this.self.amount),
|
||||
accountBalanceOnCreation: this.decimalToString(this.self.accountBalanceOnCreation),
|
||||
accountBalanceOnConfirmation: this.decimalToString(this.self.accountBalanceOnConfirmation),
|
||||
|
||||
@ -10,7 +10,7 @@ export class TransferAmountLoggingView extends AbstractLoggingView {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
public toJSON(): any {
|
||||
return {
|
||||
publicKey: Buffer.from(this.self.pubkey).toString(this.bufferStringFormat),
|
||||
pubkey: Buffer.from(this.self.pubkey).toString(this.bufferStringFormat),
|
||||
amount: this.self.amount,
|
||||
communityId: this.self.communityId,
|
||||
}
|
||||
|
||||
63
dlt-connector/src/manager/InterruptiveSleepManager.ts
Normal file
63
dlt-connector/src/manager/InterruptiveSleepManager.ts
Normal file
@ -0,0 +1,63 @@
|
||||
import { LogError } from '@/server/LogError'
|
||||
|
||||
import { InterruptiveSleep } from '../utils/InterruptiveSleep'
|
||||
|
||||
// Source: https://refactoring.guru/design-patterns/singleton/typescript/example
|
||||
// and ../federation/client/FederationClientFactory.ts
|
||||
/**
|
||||
* A Singleton class defines the `getInstance` method that lets clients access
|
||||
* the unique singleton instance.
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-extraneous-class
|
||||
export class InterruptiveSleepManager {
|
||||
// eslint-disable-next-line no-use-before-define
|
||||
private static instance: InterruptiveSleepManager
|
||||
private interruptiveSleep: Map<string, InterruptiveSleep> = new Map<string, InterruptiveSleep>()
|
||||
private stepSizeMilliseconds = 10
|
||||
|
||||
/**
|
||||
* The Singleton's constructor should always be private to prevent direct
|
||||
* construction calls with the `new` operator.
|
||||
*/
|
||||
// eslint-disable-next-line no-useless-constructor, @typescript-eslint/no-empty-function
|
||||
private constructor() {}
|
||||
|
||||
/**
|
||||
* The static method that controls the access to the singleton instance.
|
||||
*
|
||||
* This implementation let you subclass the Singleton class while keeping
|
||||
* just one instance of each subclass around.
|
||||
*/
|
||||
public static getInstance(): InterruptiveSleepManager {
|
||||
if (!InterruptiveSleepManager.instance) {
|
||||
InterruptiveSleepManager.instance = new InterruptiveSleepManager()
|
||||
}
|
||||
return InterruptiveSleepManager.instance
|
||||
}
|
||||
|
||||
/**
|
||||
* only for new created InterruptiveSleepManager Entries!
|
||||
* @param step size in ms in which new! InterruptiveSleepManager check if they where triggered
|
||||
*/
|
||||
public setStepSize(ms: number) {
|
||||
this.stepSizeMilliseconds = ms
|
||||
}
|
||||
|
||||
public interrupt(key: string): void {
|
||||
const interruptiveSleep = this.interruptiveSleep.get(key)
|
||||
if (interruptiveSleep) {
|
||||
interruptiveSleep.interrupt()
|
||||
}
|
||||
}
|
||||
|
||||
public sleep(key: string, ms: number): Promise<void> {
|
||||
if (!this.interruptiveSleep.has(key)) {
|
||||
this.interruptiveSleep.set(key, new InterruptiveSleep(this.stepSizeMilliseconds))
|
||||
}
|
||||
const interruptiveSleep = this.interruptiveSleep.get(key)
|
||||
if (!interruptiveSleep) {
|
||||
throw new LogError('map entry not exist after setting it')
|
||||
}
|
||||
return interruptiveSleep.sleep(ms)
|
||||
}
|
||||
}
|
||||
49
dlt-connector/src/tasks/transmitToIota.ts
Normal file
49
dlt-connector/src/tasks/transmitToIota.ts
Normal file
@ -0,0 +1,49 @@
|
||||
import { TRANSMIT_TO_IOTA_INTERRUPTIVE_SLEEP_KEY } from '@/data/const'
|
||||
import { TransactionRepository } from '@/data/Transaction.repository'
|
||||
import { TransmitToIotaContext } from '@/interactions/transmitToIota/TransmitToIota.context'
|
||||
import { InterruptiveSleepManager } from '@/manager/InterruptiveSleepManager'
|
||||
|
||||
import { logger } from '../logging/logger'
|
||||
|
||||
function sleep(ms: number) {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(resolve, ms)
|
||||
})
|
||||
}
|
||||
|
||||
let running = true
|
||||
|
||||
export const stopTransmitToIota = (): void => {
|
||||
running = false
|
||||
}
|
||||
/**
|
||||
* check for pending transactions:
|
||||
* - if one found call TransmitToIotaContext
|
||||
* - if not, wait 1000 ms and try again
|
||||
* if a new transaction was added, the sleep will be interrupted
|
||||
*/
|
||||
export const transmitToIota = async (): Promise<void> => {
|
||||
logger.info('start iota message transmitter')
|
||||
// eslint-disable-next-line no-unmodified-loop-condition
|
||||
while (running) {
|
||||
try {
|
||||
while (true) {
|
||||
const recipe = await TransactionRepository.getNextPendingTransaction()
|
||||
if (!recipe) break
|
||||
const transmitToIotaContext = new TransmitToIotaContext(recipe)
|
||||
await transmitToIotaContext.run()
|
||||
}
|
||||
|
||||
await InterruptiveSleepManager.getInstance().sleep(
|
||||
TRANSMIT_TO_IOTA_INTERRUPTIVE_SLEEP_KEY,
|
||||
1000,
|
||||
)
|
||||
} catch (error) {
|
||||
logger.error('error while transmitting to iota, retry in 10 seconds ', error)
|
||||
await sleep(10000)
|
||||
}
|
||||
}
|
||||
logger.error(
|
||||
'end iota message transmitter, no further transaction will be transmitted. !!! Please restart Server !!!',
|
||||
)
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user