mirror of
https://github.com/IT4Change/gradido.git
synced 2026-02-06 09:56:05 +00:00
Merge pull request #3289 from gradido/refactor_community_load
refactor(backend): refactor community and add to dlt-connector
This commit is contained in:
commit
16c14a1b11
@ -16,6 +16,7 @@ module.exports = {
|
||||
moduleNameMapper: {
|
||||
'@/(.*)': '<rootDir>/src/$1',
|
||||
'@arg/(.*)': '<rootDir>/src/graphql/arg/$1',
|
||||
'@input/(.*)': '<rootDir>/src/graphql/input/$1',
|
||||
'@dltConnector/(.*)': '<rootDir>/src/apis/dltConnector/$1',
|
||||
'@enum/(.*)': '<rootDir>/src/graphql/enum/$1',
|
||||
'@model/(.*)': '<rootDir>/src/graphql/model/$1',
|
||||
|
||||
@ -6,4 +6,6 @@ export const ADMIN_RIGHTS = [
|
||||
RIGHTS.UNDELETE_USER,
|
||||
RIGHTS.COMMUNITY_UPDATE,
|
||||
RIGHTS.COMMUNITY_BY_UUID,
|
||||
RIGHTS.COMMUNITY_BY_IDENTIFIER,
|
||||
RIGHTS.HOME_COMMUNITY,
|
||||
]
|
||||
|
||||
3
backend/src/auth/DLT_CONNECTOR_RIGHTS.ts
Normal file
3
backend/src/auth/DLT_CONNECTOR_RIGHTS.ts
Normal file
@ -0,0 +1,3 @@
|
||||
import { RIGHTS } from './RIGHTS'
|
||||
|
||||
export const DLT_CONNECTOR_RIGHTS = [RIGHTS.COMMUNITY_BY_IDENTIFIER, RIGHTS.HOME_COMMUNITY]
|
||||
@ -59,5 +59,7 @@ export enum RIGHTS {
|
||||
DELETE_USER = 'DELETE_USER',
|
||||
UNDELETE_USER = 'UNDELETE_USER',
|
||||
COMMUNITY_BY_UUID = 'COMMUNITY_BY_UUID',
|
||||
COMMUNITY_BY_IDENTIFIER = 'COMMUNITY_BY_IDENTIFIER',
|
||||
HOME_COMMUNITY = 'HOME_COMMUNITY',
|
||||
COMMUNITY_UPDATE = 'COMMUNITY_UPDATE',
|
||||
}
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import { RoleNames } from '@/graphql/enum/RoleNames'
|
||||
|
||||
import { ADMIN_RIGHTS } from './ADMIN_RIGHTS'
|
||||
import { DLT_CONNECTOR_RIGHTS } from './DLT_CONNECTOR_RIGHTS'
|
||||
import { INALIENABLE_RIGHTS } from './INALIENABLE_RIGHTS'
|
||||
import { MODERATOR_RIGHTS } from './MODERATOR_RIGHTS'
|
||||
import { Role } from './Role'
|
||||
@ -20,5 +21,7 @@ export const ROLE_ADMIN = new Role(RoleNames.ADMIN, [
|
||||
...ADMIN_RIGHTS,
|
||||
])
|
||||
|
||||
export const ROLE_DLT_CONNECTOR = new Role(RoleNames.DLT_CONNECTOR, DLT_CONNECTOR_RIGHTS)
|
||||
|
||||
// TODO from database
|
||||
export const ROLES = [ROLE_UNAUTHORIZED, ROLE_USER, ROLE_MODERATOR, ROLE_ADMIN]
|
||||
|
||||
@ -1,14 +1,13 @@
|
||||
import { IsString } from 'class-validator'
|
||||
import { Field, ArgsType, InputType } from 'type-graphql'
|
||||
import { IsBoolean, IsString } from 'class-validator'
|
||||
import { ArgsType, Field } from 'type-graphql'
|
||||
|
||||
@InputType()
|
||||
@ArgsType()
|
||||
export class CommunityArgs {
|
||||
@Field(() => String)
|
||||
@Field(() => String, { nullable: true })
|
||||
@IsString()
|
||||
uuid: string
|
||||
communityIdentifier?: string | null
|
||||
|
||||
@Field(() => String)
|
||||
@IsString()
|
||||
gmsApiKey: string
|
||||
@Field(() => Boolean, { nullable: true })
|
||||
@IsBoolean()
|
||||
foreign?: boolean | null
|
||||
}
|
||||
|
||||
@ -6,7 +6,13 @@ import { RoleNames } from '@enum/RoleNames'
|
||||
import { INALIENABLE_RIGHTS } from '@/auth/INALIENABLE_RIGHTS'
|
||||
import { decode, encode } from '@/auth/JWT'
|
||||
import { RIGHTS } from '@/auth/RIGHTS'
|
||||
import { ROLE_UNAUTHORIZED, ROLE_USER, ROLE_ADMIN, ROLE_MODERATOR } from '@/auth/ROLES'
|
||||
import {
|
||||
ROLE_UNAUTHORIZED,
|
||||
ROLE_USER,
|
||||
ROLE_ADMIN,
|
||||
ROLE_MODERATOR,
|
||||
ROLE_DLT_CONNECTOR,
|
||||
} from '@/auth/ROLES'
|
||||
import { Context } from '@/server/context'
|
||||
import { LogError } from '@/server/LogError'
|
||||
|
||||
@ -30,31 +36,35 @@ export const isAuthorized: AuthChecker<Context> = async ({ context }, rights) =>
|
||||
// Set context gradidoID
|
||||
context.gradidoID = decoded.gradidoID
|
||||
|
||||
// TODO - load from database dynamically & admin - maybe encode this in the token to prevent many database requests
|
||||
// TODO this implementation is bullshit - two database queries cause our user identifiers are not aligned and vary between email, id and pubKey
|
||||
try {
|
||||
const user = await User.findOneOrFail({
|
||||
where: { gradidoID: decoded.gradidoID },
|
||||
withDeleted: true,
|
||||
relations: ['emailContact', 'userRoles'],
|
||||
})
|
||||
context.user = user
|
||||
context.role = ROLE_USER
|
||||
if (user.userRoles?.length > 0) {
|
||||
switch (user.userRoles[0].role) {
|
||||
case RoleNames.ADMIN:
|
||||
context.role = ROLE_ADMIN
|
||||
break
|
||||
case RoleNames.MODERATOR:
|
||||
context.role = ROLE_MODERATOR
|
||||
break
|
||||
default:
|
||||
context.role = ROLE_USER
|
||||
if (context.gradidoID === 'dlt-connector') {
|
||||
context.role = ROLE_DLT_CONNECTOR
|
||||
} else {
|
||||
// TODO - load from database dynamically & admin - maybe encode this in the token to prevent many database requests
|
||||
// TODO this implementation is bullshit - two database queries cause our user identifiers are not aligned and vary between email, id and pubKey
|
||||
try {
|
||||
const user = await User.findOneOrFail({
|
||||
where: { gradidoID: decoded.gradidoID },
|
||||
withDeleted: true,
|
||||
relations: ['emailContact', 'userRoles'],
|
||||
})
|
||||
context.user = user
|
||||
context.role = ROLE_USER
|
||||
if (user.userRoles?.length > 0) {
|
||||
switch (user.userRoles[0].role) {
|
||||
case RoleNames.ADMIN:
|
||||
context.role = ROLE_ADMIN
|
||||
break
|
||||
case RoleNames.MODERATOR:
|
||||
context.role = ROLE_MODERATOR
|
||||
break
|
||||
default:
|
||||
context.role = ROLE_USER
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// in case the database query fails (user deleted)
|
||||
throw new LogError('401 Unauthorized')
|
||||
}
|
||||
} catch {
|
||||
// in case the database query fails (user deleted)
|
||||
throw new LogError('401 Unauthorized')
|
||||
}
|
||||
|
||||
// check for correct rights
|
||||
|
||||
@ -5,6 +5,7 @@ export enum RoleNames {
|
||||
USER = 'USER',
|
||||
MODERATOR = 'MODERATOR',
|
||||
ADMIN = 'ADMIN',
|
||||
DLT_CONNECTOR = 'DLT_CONNECTOR_ROLE',
|
||||
}
|
||||
|
||||
registerEnumType(RoleNames, {
|
||||
|
||||
14
backend/src/graphql/input/EditCommunityInput.ts
Normal file
14
backend/src/graphql/input/EditCommunityInput.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import { IsString, IsUUID } from 'class-validator'
|
||||
import { ArgsType, Field, InputType } from 'type-graphql'
|
||||
|
||||
@ArgsType()
|
||||
@InputType()
|
||||
export class EditCommunityInput {
|
||||
@Field(() => String)
|
||||
@IsUUID('4')
|
||||
uuid: string
|
||||
|
||||
@Field(() => String)
|
||||
@IsString()
|
||||
gmsApiKey: string
|
||||
}
|
||||
@ -10,13 +10,19 @@ import { Community as DbCommunity } from '@entity/Community'
|
||||
import { FederatedCommunity as DbFederatedCommunity } from '@entity/FederatedCommunity'
|
||||
import { ApolloServerTestClient } from 'apollo-server-testing'
|
||||
import { GraphQLError } from 'graphql/error/GraphQLError'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
|
||||
import { cleanDB, testEnvironment } from '@test/helpers'
|
||||
import { logger, i18n as localization } from '@test/testSetup'
|
||||
|
||||
import { userFactory } from '@/seeds/factory/user'
|
||||
import { login, updateHomeCommunityQuery } from '@/seeds/graphql/mutations'
|
||||
import { getCommunities, communitiesQuery, getCommunityByUuidQuery } from '@/seeds/graphql/queries'
|
||||
import {
|
||||
getCommunities,
|
||||
communitiesQuery,
|
||||
getHomeCommunityQuery,
|
||||
getCommunityByIdentifierQuery,
|
||||
} from '@/seeds/graphql/queries'
|
||||
import { peterLustig } from '@/seeds/users/peter-lustig'
|
||||
|
||||
import { getCommunityByUuid } from './util/communities'
|
||||
@ -478,7 +484,7 @@ describe('CommunityResolver', () => {
|
||||
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.communityUuid = uuidv4()
|
||||
foreignCom2.authenticatedAt = new Date()
|
||||
foreignCom2.name = 'Stage-3_Community-name'
|
||||
foreignCom2.description = 'Stage-3_Community-description'
|
||||
@ -486,15 +492,36 @@ describe('CommunityResolver', () => {
|
||||
await DbCommunity.insert(foreignCom2)
|
||||
})
|
||||
|
||||
it('finds the home-community', async () => {
|
||||
it('finds the home-community by uuid', async () => {
|
||||
await expect(
|
||||
query({
|
||||
query: getCommunityByUuidQuery,
|
||||
variables: { communityUuid: homeCom?.communityUuid },
|
||||
query: getCommunityByIdentifierQuery,
|
||||
variables: { communityIdentifier: homeCom?.communityUuid },
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
data: {
|
||||
community: {
|
||||
communityByIdentifier: {
|
||||
id: homeCom?.id,
|
||||
foreign: homeCom?.foreign,
|
||||
name: homeCom?.name,
|
||||
description: homeCom?.description,
|
||||
url: homeCom?.url,
|
||||
creationDate: homeCom?.creationDate?.toISOString(),
|
||||
uuid: homeCom?.communityUuid,
|
||||
authenticatedAt: homeCom?.authenticatedAt,
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('finds the home-community', async () => {
|
||||
await expect(
|
||||
query({
|
||||
query: getHomeCommunityQuery,
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
data: {
|
||||
homeCommunity: {
|
||||
id: homeCom?.id,
|
||||
foreign: homeCom?.foreign,
|
||||
name: homeCom?.name,
|
||||
@ -563,7 +590,7 @@ describe('CommunityResolver', () => {
|
||||
expect(
|
||||
await mutate({
|
||||
mutation: updateHomeCommunityQuery,
|
||||
variables: { uuid: 'unknownUuid', gmsApiKey: 'gmsApiKey' },
|
||||
variables: { uuid: uuidv4(), gmsApiKey: 'gmsApiKey' },
|
||||
}),
|
||||
).toEqual(
|
||||
expect.objectContaining({
|
||||
|
||||
@ -1,16 +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, Mutation, Args } from 'type-graphql'
|
||||
import { Resolver, Query, Authorized, Mutation, Args, Arg } from 'type-graphql'
|
||||
|
||||
import { CommunityArgs } from '@arg//CommunityArgs'
|
||||
import { EditCommunityInput } from '@input/EditCommunityInput'
|
||||
import { Community } from '@model/Community'
|
||||
import { FederatedCommunity } from '@model/FederatedCommunity'
|
||||
|
||||
import { RIGHTS } from '@/auth/RIGHTS'
|
||||
import { LogError } from '@/server/LogError'
|
||||
|
||||
import { getCommunityByUuid } from './util/communities'
|
||||
import { getCommunityByIdentifier, getCommunityByUuid, getHomeCommunity } from './util/communities'
|
||||
|
||||
@Resolver()
|
||||
export class CommunityResolver {
|
||||
@ -41,41 +41,42 @@ export class CommunityResolver {
|
||||
return dbCommunities.map((dbCom: DbCommunity) => new Community(dbCom))
|
||||
}
|
||||
|
||||
@Authorized([RIGHTS.COMMUNITY_BY_UUID])
|
||||
@Authorized([RIGHTS.COMMUNITY_BY_IDENTIFIER])
|
||||
@Query(() => Community)
|
||||
async community(@Arg('communityUuid') communityUuid: string): Promise<Community> {
|
||||
const com: DbCommunity | null = await getCommunityByUuid(communityUuid)
|
||||
if (!com) {
|
||||
throw new LogError('community not found', communityUuid)
|
||||
async communityByIdentifier(
|
||||
@Arg('communityIdentifier') communityIdentifier: string,
|
||||
): Promise<Community> {
|
||||
const community = await getCommunityByIdentifier(communityIdentifier)
|
||||
if (!community) {
|
||||
throw new LogError('community not found', communityIdentifier)
|
||||
}
|
||||
return new Community(com)
|
||||
return new Community(community)
|
||||
}
|
||||
|
||||
@Authorized([RIGHTS.HOME_COMMUNITY])
|
||||
@Query(() => Community)
|
||||
async homeCommunity(): Promise<Community> {
|
||||
const community = await getHomeCommunity()
|
||||
if (!community) {
|
||||
throw new LogError('no home community exist')
|
||||
}
|
||||
return new Community(community)
|
||||
}
|
||||
|
||||
@Authorized([RIGHTS.COMMUNITY_UPDATE])
|
||||
@Mutation(() => Community)
|
||||
async updateHomeCommunity(@Args() { uuid, gmsApiKey }: CommunityArgs): Promise<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!`)
|
||||
async updateHomeCommunity(@Args() { uuid, gmsApiKey }: EditCommunityInput): Promise<Community> {
|
||||
const homeCom = await getCommunityByUuid(uuid)
|
||||
if (!homeCom) {
|
||||
throw new LogError('HomeCommunity with uuid not found: ', uuid)
|
||||
}
|
||||
return com
|
||||
if (homeCom.foreign) {
|
||||
throw new LogError('Error: Only the HomeCommunity could be modified!')
|
||||
}
|
||||
if (homeCom.gmsApiKey !== gmsApiKey) {
|
||||
homeCom.gmsApiKey = gmsApiKey
|
||||
await DbCommunity.save(homeCom)
|
||||
}
|
||||
return new Community(homeCom)
|
||||
}
|
||||
}
|
||||
|
||||
@ -38,7 +38,7 @@ import { calculateBalance } from '@/util/validate'
|
||||
import { virtualLinkTransaction, virtualDecayTransaction } from '@/util/virtualTransactions'
|
||||
|
||||
import { BalanceResolver } from './BalanceResolver'
|
||||
import { getCommunityByUuid, getCommunityName, isHomeCommunity } from './util/communities'
|
||||
import { getCommunityByIdentifier, getCommunityName, isHomeCommunity } from './util/communities'
|
||||
import { findUserByIdentifier } from './util/findUserByIdentifier'
|
||||
import { getLastTransaction } from './util/getLastTransaction'
|
||||
import { getTransactionList } from './util/getTransactionList'
|
||||
@ -452,7 +452,7 @@ export class TransactionResolver {
|
||||
if (!CONFIG.FEDERATION_XCOM_SENDCOINS_ENABLED) {
|
||||
throw new LogError('X-Community sendCoins disabled per configuration!')
|
||||
}
|
||||
const recipCom = await getCommunityByUuid(recipientCommunityIdentifier)
|
||||
const recipCom = await getCommunityByIdentifier(recipientCommunityIdentifier)
|
||||
logger.debug('recipient commuity: ', recipCom)
|
||||
if (recipCom === null) {
|
||||
throw new LogError(
|
||||
|
||||
@ -1,65 +1,88 @@
|
||||
import { FindOneOptions } from '@dbTools/typeorm'
|
||||
import { Community as DbCommunity } from '@entity/Community'
|
||||
|
||||
function findWithCommunityIdentifier(communityIdentifier: string): FindOneOptions<DbCommunity> {
|
||||
return {
|
||||
where: [
|
||||
{ communityUuid: communityIdentifier },
|
||||
{ name: communityIdentifier },
|
||||
{ url: communityIdentifier },
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a community with the given identifier exists and is not foreign.
|
||||
* @param communityIdentifier The identifier (URL, UUID, or name) of the community.
|
||||
* @returns A promise that resolves to true if a non-foreign community exists with the given identifier, otherwise false.
|
||||
*/
|
||||
export async function isHomeCommunity(communityIdentifier: string): Promise<boolean> {
|
||||
const homeCommunity = await DbCommunity.findOne({
|
||||
// The !! operator in JavaScript or TypeScript is a shorthand for converting a value to a boolean.
|
||||
// It essentially converts any truthy value to true and any falsy value to false.
|
||||
return !!(await DbCommunity.findOne({
|
||||
where: [
|
||||
{ foreign: false, communityUuid: communityIdentifier },
|
||||
{ foreign: false, name: communityIdentifier },
|
||||
{ foreign: false, url: communityIdentifier },
|
||||
],
|
||||
})
|
||||
if (homeCommunity) {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the home community, i.e., a community that is not foreign.
|
||||
* @returns A promise that resolves to the home community, or throw if no home community was found
|
||||
*/
|
||||
export async function getHomeCommunity(): Promise<DbCommunity> {
|
||||
return await DbCommunity.findOneOrFail({
|
||||
where: [{ foreign: false }],
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO: Check if it is needed, because currently it isn't used at all
|
||||
* Retrieves the URL of the community with the given identifier.
|
||||
* @param communityIdentifier The identifier (URL, UUID, or name) of the community.
|
||||
* @returns A promise that resolves to the URL of the community or throw if no community with this identifier was found
|
||||
*/
|
||||
export async function getCommunityUrl(communityIdentifier: string): Promise<string> {
|
||||
const community = await DbCommunity.findOneOrFail({
|
||||
where: [
|
||||
{ communityUuid: communityIdentifier },
|
||||
{ name: communityIdentifier },
|
||||
{ url: communityIdentifier },
|
||||
],
|
||||
})
|
||||
return community.url
|
||||
return (await DbCommunity.findOneOrFail(findWithCommunityIdentifier(communityIdentifier))).url
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO: Check if it is needed, because currently it isn't used at all
|
||||
* Checks if a community with the given identifier exists and has an authenticatedAt property set.
|
||||
* @param communityIdentifier The identifier (URL, UUID, or name) of the community.
|
||||
* @returns A promise that resolves to true if a community with an authenticatedAt property exists with the given identifier,
|
||||
* otherwise false.
|
||||
*/
|
||||
export async function isCommunityAuthenticated(communityIdentifier: string): Promise<boolean> {
|
||||
const community = await DbCommunity.findOne({
|
||||
where: [
|
||||
{ communityUuid: communityIdentifier },
|
||||
{ name: communityIdentifier },
|
||||
{ url: communityIdentifier },
|
||||
],
|
||||
})
|
||||
if (community?.authenticatedAt) {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
// The !! operator in JavaScript or TypeScript is a shorthand for converting a value to a boolean.
|
||||
// It essentially converts any truthy value to true and any falsy value to false.
|
||||
return !!(await DbCommunity.findOne(findWithCommunityIdentifier(communityIdentifier)))
|
||||
?.authenticatedAt
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the name of the community with the given identifier.
|
||||
* @param communityIdentifier The identifier (URL, UUID) of the community.
|
||||
* @returns A promise that resolves to the name of the community. If the community does not exist or has no name,
|
||||
* an empty string is returned.
|
||||
*/
|
||||
export async function getCommunityName(communityIdentifier: string): Promise<string> {
|
||||
const community = await DbCommunity.findOne({
|
||||
where: [{ communityUuid: communityIdentifier }, { url: communityIdentifier }],
|
||||
})
|
||||
if (community?.name) {
|
||||
return community.name
|
||||
} else {
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
return community?.name ? community.name : ''
|
||||
}
|
||||
export async function getCommunityByUuid(communityUuid: string): Promise<DbCommunity | null> {
|
||||
return await DbCommunity.findOne({
|
||||
where: [{ communityUuid }],
|
||||
})
|
||||
}
|
||||
|
||||
export async function getCommunityByIdentifier(
|
||||
communityIdentifier: string,
|
||||
): Promise<DbCommunity | null> {
|
||||
return await DbCommunity.findOne(findWithCommunityIdentifier(communityIdentifier))
|
||||
}
|
||||
|
||||
@ -2,9 +2,11 @@ import { FindOptionsWhere } from '@dbTools/typeorm'
|
||||
import { Community } from '@entity/Community'
|
||||
import { User as DbUser } from '@entity/User'
|
||||
import { UserContact as DbUserContact } from '@entity/UserContact'
|
||||
import { isURL } from 'class-validator'
|
||||
import { validate, version } from 'uuid'
|
||||
|
||||
import { LogError } from '@/server/LogError'
|
||||
import { isEMail, isUUID4 } from '@/util/validate'
|
||||
|
||||
import { VALID_ALIAS_REGEX } from './validateAlias'
|
||||
|
||||
@ -19,10 +21,11 @@ export const findUserByIdentifier = async (
|
||||
communityIdentifier: string,
|
||||
): Promise<DbUser> => {
|
||||
let user: DbUser | null
|
||||
const communityWhere: FindOptionsWhere<Community> =
|
||||
validate(communityIdentifier) && version(communityIdentifier) === 4
|
||||
? { communityUuid: communityIdentifier }
|
||||
: { name: communityIdentifier }
|
||||
const communityWhere: FindOptionsWhere<Community> = isURL(communityIdentifier)
|
||||
? { url: communityIdentifier }
|
||||
: isUUID4(communityIdentifier)
|
||||
? { communityUuid: communityIdentifier }
|
||||
: { name: communityIdentifier }
|
||||
|
||||
if (validate(identifier) && version(identifier) === 4) {
|
||||
user = await DbUser.findOne({
|
||||
@ -32,7 +35,7 @@ export const findUserByIdentifier = async (
|
||||
if (!user) {
|
||||
throw new LogError('No user found to given identifier(s)', identifier, communityIdentifier)
|
||||
}
|
||||
} else if (/^.{2,}@.{2,}\..{2,}$/.exec(identifier)) {
|
||||
} else if (isEMail(identifier)) {
|
||||
const userContact = await DbUserContact.findOne({
|
||||
where: {
|
||||
email: identifier,
|
||||
|
||||
@ -134,9 +134,25 @@ export const communitiesQuery = gql`
|
||||
}
|
||||
`
|
||||
|
||||
export const getCommunityByUuidQuery = gql`
|
||||
query ($communityUuid: String!) {
|
||||
community(communityUuid: $communityUuid) {
|
||||
export const getCommunityByIdentifierQuery = gql`
|
||||
query ($communityIdentifier: String!) {
|
||||
communityByIdentifier(communityIdentifier: $communityIdentifier) {
|
||||
id
|
||||
foreign
|
||||
name
|
||||
description
|
||||
url
|
||||
creationDate
|
||||
uuid
|
||||
authenticatedAt
|
||||
gmsApiKey
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const getHomeCommunityQuery = gql`
|
||||
query {
|
||||
homeCommunity {
|
||||
id
|
||||
foreign
|
||||
name
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { TransactionLink as dbTransactionLink } from '@entity/TransactionLink'
|
||||
import { Decimal } from 'decimal.js-light'
|
||||
import { validate, version } from 'uuid'
|
||||
|
||||
import { Decay } from '@model/Decay'
|
||||
|
||||
@ -16,6 +17,14 @@ function isStringBoolean(value: string): boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
function isUUID4(value: string): boolean {
|
||||
return validate(value) && version(value) === 4
|
||||
}
|
||||
|
||||
function isEMail(value: string): boolean {
|
||||
return /^.{2,}@.{2,}\..{2,}$/.exec(value) !== null
|
||||
}
|
||||
|
||||
async function calculateBalance(
|
||||
userId: number,
|
||||
amount: Decimal,
|
||||
@ -42,4 +51,4 @@ async function calculateBalance(
|
||||
return { balance, lastTransactionId: lastTransaction.id, decay }
|
||||
}
|
||||
|
||||
export { calculateBalance, isStringBoolean }
|
||||
export { calculateBalance, isStringBoolean, isUUID4, isEMail }
|
||||
|
||||
@ -49,6 +49,7 @@
|
||||
"paths": { /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
|
||||
"@/*": ["src/*"],
|
||||
"@arg/*": ["src/graphql/arg/*"],
|
||||
"@input/*": ["src/graphql/input/*"],
|
||||
"@dltConnector/*": ["src/apis/dltConnector/*"],
|
||||
"@enum/*": ["src/graphql/enum/*"],
|
||||
"@model/*": ["src/graphql/model/*"],
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
CONFIG_VERSION=v4.2023-09-12
|
||||
CONFIG_VERSION=v6.2024-02-20
|
||||
|
||||
# SET LOG LEVEL AS NEEDED IN YOUR .ENV
|
||||
# POSSIBLE VALUES: all | trace | debug | info | warn | error | fatal
|
||||
@ -19,4 +19,8 @@ DB_DATABASE_TEST=gradido_dlt_test
|
||||
TYPEORM_LOGGING_RELATIVE_PATH=typeorm.backend.log
|
||||
|
||||
# DLT-Connector
|
||||
DLT_CONNECTOR_PORT=6010
|
||||
DLT_CONNECTOR_PORT=6010
|
||||
|
||||
# Route to Backend
|
||||
BACKEND_SERVER_URL=http://localhost:4000
|
||||
JWT_SECRET=secret123
|
||||
@ -1,5 +1,7 @@
|
||||
CONFIG_VERSION=$DLT_CONNECTOR_CONFIG_VERSION
|
||||
|
||||
JWT_SECRET=$JWT_SECRET
|
||||
|
||||
#IOTA
|
||||
IOTA_API_URL=$IOTA_API_URL
|
||||
IOTA_COMMUNITY_ALIAS=$IOTA_COMMUNITY_ALIAS
|
||||
@ -15,4 +17,7 @@ DB_DATABASE_TEST=$DB_DATABASE_TEST
|
||||
TYPEORM_LOGGING_RELATIVE_PATH=typeorm.backend.log
|
||||
|
||||
# DLT-Connector
|
||||
DLT_CONNECTOR_PORT=$DLT_CONNECTOR_PORT
|
||||
DLT_CONNECTOR_PORT=$DLT_CONNECTOR_PORT
|
||||
|
||||
# Route to Backend
|
||||
BACKEND_SERVER_URL=http://localhost:4000
|
||||
@ -6,7 +6,7 @@ module.exports = {
|
||||
collectCoverageFrom: ['src/**/*.ts', '!**/node_modules/**', '!src/seeds/**', '!build/**'],
|
||||
coverageThreshold: {
|
||||
global: {
|
||||
lines: 75,
|
||||
lines: 72,
|
||||
},
|
||||
},
|
||||
setupFiles: ['<rootDir>/test/testSetup.ts'],
|
||||
|
||||
@ -31,8 +31,10 @@
|
||||
"express": "4.17.1",
|
||||
"express-slow-down": "^2.0.1",
|
||||
"graphql": "^16.7.1",
|
||||
"graphql-request": "^6.1.0",
|
||||
"graphql-scalars": "^1.22.2",
|
||||
"helmet": "^7.1.0",
|
||||
"jose": "^5.2.2",
|
||||
"log4js": "^6.7.1",
|
||||
"nodemon": "^2.0.20",
|
||||
"protobufjs": "^7.2.5",
|
||||
|
||||
105
dlt-connector/src/client/BackendClient.ts
Normal file
105
dlt-connector/src/client/BackendClient.ts
Normal file
@ -0,0 +1,105 @@
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||
import { gql, GraphQLClient } from 'graphql-request'
|
||||
import { SignJWT } from 'jose'
|
||||
|
||||
import { CONFIG } from '@/config'
|
||||
import { CommunityDraft } from '@/graphql/input/CommunityDraft'
|
||||
import { logger } from '@/logging/logger'
|
||||
import { LogError } from '@/server/LogError'
|
||||
|
||||
const homeCommunity = gql`
|
||||
query {
|
||||
homeCommunity {
|
||||
uuid
|
||||
foreign
|
||||
creationDate
|
||||
}
|
||||
}
|
||||
`
|
||||
interface Community {
|
||||
homeCommunity: {
|
||||
uuid: string
|
||||
foreign: boolean
|
||||
creationDate: string
|
||||
}
|
||||
}
|
||||
// Source: https://refactoring.guru/design-patterns/singleton/typescript/example
|
||||
// and ../federation/client/FederationClientFactory.ts
|
||||
/**
|
||||
* A Singleton class defines the `getInstance` method that lets clients access
|
||||
* the unique singleton instance.
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-extraneous-class
|
||||
export class BackendClient {
|
||||
// eslint-disable-next-line no-use-before-define
|
||||
private static instance: BackendClient
|
||||
client: GraphQLClient
|
||||
/**
|
||||
* The Singleton's constructor should always be private to prevent direct
|
||||
* construction calls with the `new` operator.
|
||||
*/
|
||||
// eslint-disable-next-line no-useless-constructor, @typescript-eslint/no-empty-function
|
||||
private constructor() {}
|
||||
|
||||
/**
|
||||
* The static method that controls the access to the singleton instance.
|
||||
*
|
||||
* This implementation let you subclass the Singleton class while keeping
|
||||
* just one instance of each subclass around.
|
||||
*/
|
||||
public static getInstance(): BackendClient | undefined {
|
||||
if (!BackendClient.instance) {
|
||||
BackendClient.instance = new BackendClient()
|
||||
}
|
||||
if (!BackendClient.instance.client) {
|
||||
try {
|
||||
BackendClient.instance.client = new GraphQLClient(CONFIG.BACKEND_SERVER_URL, {
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
},
|
||||
method: 'GET',
|
||||
jsonSerializer: {
|
||||
parse: JSON.parse,
|
||||
stringify: JSON.stringify,
|
||||
},
|
||||
})
|
||||
} catch (e) {
|
||||
logger.error("couldn't connect to backend: ", e)
|
||||
return
|
||||
}
|
||||
}
|
||||
return BackendClient.instance
|
||||
}
|
||||
|
||||
public async getHomeCommunityDraft(): Promise<CommunityDraft> {
|
||||
logger.info('check home community on backend')
|
||||
const { data, errors } = await this.client.rawRequest<Community>(
|
||||
homeCommunity,
|
||||
{},
|
||||
{
|
||||
authorization: 'Bearer ' + (await this.createJWTToken()),
|
||||
},
|
||||
)
|
||||
if (errors) {
|
||||
throw new LogError('error getting home community from backend', errors)
|
||||
}
|
||||
const communityDraft = new CommunityDraft()
|
||||
communityDraft.uuid = data.homeCommunity.uuid
|
||||
communityDraft.foreign = data.homeCommunity.foreign
|
||||
communityDraft.createdAt = data.homeCommunity.creationDate
|
||||
return communityDraft
|
||||
}
|
||||
|
||||
private async createJWTToken(): Promise<string> {
|
||||
const secret = new TextEncoder().encode(CONFIG.JWT_SECRET)
|
||||
const token = await new SignJWT({ gradidoID: 'dlt-connector', 'urn:gradido:claim': true })
|
||||
.setProtectedHeader({ alg: 'HS256' })
|
||||
.setIssuedAt()
|
||||
.setIssuer('urn:gradido:issuer')
|
||||
.setAudience('urn:gradido:audience')
|
||||
.setExpirationTime('1m')
|
||||
.sign(secret)
|
||||
return token
|
||||
}
|
||||
}
|
||||
@ -9,13 +9,14 @@ const constants = {
|
||||
LOG_LEVEL: process.env.LOG_LEVEL ?? 'info',
|
||||
CONFIG_VERSION: {
|
||||
DEFAULT: 'DEFAULT',
|
||||
EXPECTED: 'v4.2023-09-12',
|
||||
EXPECTED: 'v6.2024-02-20',
|
||||
CURRENT: '',
|
||||
},
|
||||
}
|
||||
|
||||
const server = {
|
||||
PRODUCTION: process.env.NODE_ENV === 'production' ?? false,
|
||||
JWT_SECRET: process.env.JWT_SECRET ?? 'secret123',
|
||||
}
|
||||
|
||||
const database = {
|
||||
@ -38,6 +39,10 @@ const dltConnector = {
|
||||
DLT_CONNECTOR_PORT: process.env.DLT_CONNECTOR_PORT ?? 6010,
|
||||
}
|
||||
|
||||
const backendServer = {
|
||||
BACKEND_SERVER_URL: process.env.BACKEND_SERVER_URL ?? 'http://backend:4000',
|
||||
}
|
||||
|
||||
// Check config version
|
||||
constants.CONFIG_VERSION.CURRENT = process.env.CONFIG_VERSION ?? constants.CONFIG_VERSION.DEFAULT
|
||||
if (
|
||||
@ -56,4 +61,5 @@ export const CONFIG = {
|
||||
...database,
|
||||
...iota,
|
||||
...dltConnector,
|
||||
...backendServer,
|
||||
}
|
||||
|
||||
@ -1,10 +1,42 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import 'reflect-metadata'
|
||||
|
||||
import { CONFIG } from '@/config'
|
||||
|
||||
import { BackendClient } from './client/BackendClient'
|
||||
import { CommunityRepository } from './data/Community.repository'
|
||||
import { Mnemonic } from './data/Mnemonic'
|
||||
import { CommunityDraft } from './graphql/input/CommunityDraft'
|
||||
import { AddCommunityContext } from './interactions/backendToDb/community/AddCommunity.context'
|
||||
import { logger } from './logging/logger'
|
||||
import createServer from './server/createServer'
|
||||
import { LogError } from './server/LogError'
|
||||
import { stopTransmitToIota, transmitToIota } from './tasks/transmitToIota'
|
||||
|
||||
async function waitForServer(
|
||||
backend: BackendClient,
|
||||
retryIntervalMs: number,
|
||||
maxRetries: number,
|
||||
): Promise<CommunityDraft> {
|
||||
let retries = 0
|
||||
while (retries < maxRetries) {
|
||||
logger.info(`Attempt ${retries + 1} for connecting to backend`)
|
||||
|
||||
try {
|
||||
// Make a HEAD request to the server
|
||||
return await backend.getHomeCommunityDraft()
|
||||
} catch (error) {
|
||||
logger.info('Server is not reachable: ', error)
|
||||
}
|
||||
|
||||
// Server is not reachable, wait and retry
|
||||
await new Promise((resolve) => setTimeout(resolve, retryIntervalMs))
|
||||
retries++
|
||||
}
|
||||
|
||||
throw new LogError('Max retries exceeded. Server did not become reachable.')
|
||||
}
|
||||
|
||||
async function main() {
|
||||
if (CONFIG.IOTA_HOME_COMMUNITY_SEED) {
|
||||
Mnemonic.validateSeed(CONFIG.IOTA_HOME_COMMUNITY_SEED)
|
||||
@ -13,6 +45,22 @@ async function main() {
|
||||
console.log(`DLT_CONNECTOR_PORT=${CONFIG.DLT_CONNECTOR_PORT}`)
|
||||
const { app } = await createServer()
|
||||
|
||||
// ask backend for home community if we haven't one
|
||||
try {
|
||||
await CommunityRepository.loadHomeCommunityKeyPair()
|
||||
} catch (e) {
|
||||
const backend = BackendClient.getInstance()
|
||||
if (!backend) {
|
||||
throw new LogError('cannot create backend client')
|
||||
}
|
||||
// wait for backend server to be ready
|
||||
await waitForServer(backend, 2500, 10)
|
||||
|
||||
const communityDraft = await backend.getHomeCommunityDraft()
|
||||
const addCommunityContext = new AddCommunityContext(communityDraft)
|
||||
await addCommunityContext.run()
|
||||
}
|
||||
|
||||
// loop run all the time, check for new transaction for sending to iota
|
||||
void transmitToIota()
|
||||
app.listen(CONFIG.DLT_CONNECTOR_PORT, () => {
|
||||
|
||||
@ -12,6 +12,7 @@ import { TransactionError } from '@/graphql/model/TransactionError'
|
||||
import { CommunityLoggingView } from '@/logging/CommunityLogging.view'
|
||||
import { logger } from '@/logging/logger'
|
||||
import { InterruptiveSleepManager } from '@/manager/InterruptiveSleepManager'
|
||||
import { LogError } from '@/server/LogError'
|
||||
import { getDataSource } from '@/typeorm/DataSource'
|
||||
|
||||
import { CreateTransactionRecipeContext } from '../transaction/CreateTransationRecipe.context'
|
||||
@ -24,7 +25,19 @@ export class HomeCommunityRole extends CommunityRole {
|
||||
public async create(communityDraft: CommunityDraft, topic: string): Promise<void> {
|
||||
super.create(communityDraft, topic)
|
||||
// generate key pair for signing transactions and deriving all keys for community
|
||||
const keyPair = new KeyPair(new Mnemonic(CONFIG.IOTA_HOME_COMMUNITY_SEED ?? undefined))
|
||||
let mnemonic: Mnemonic
|
||||
try {
|
||||
mnemonic = new Mnemonic(CONFIG.IOTA_HOME_COMMUNITY_SEED ?? undefined)
|
||||
} catch (e) {
|
||||
throw new LogError(
|
||||
'error creating mnemonic for home community, please fill IOTA_HOME_COMMUNITY_SEED in .env',
|
||||
{
|
||||
IOTA_HOME_COMMUNITY_SEED: CONFIG.IOTA_HOME_COMMUNITY_SEED,
|
||||
error: e,
|
||||
},
|
||||
)
|
||||
}
|
||||
const keyPair = new KeyPair(mnemonic)
|
||||
keyPair.fillInCommunityKeys(this.self)
|
||||
|
||||
// create auf account and gmw account
|
||||
|
||||
@ -569,7 +569,7 @@
|
||||
"@graphql-typed-document-node/core" "^3.1.1"
|
||||
tslib "^2.4.0"
|
||||
|
||||
"@graphql-typed-document-node/core@^3.1.1":
|
||||
"@graphql-typed-document-node/core@^3.1.1", "@graphql-typed-document-node/core@^3.2.0":
|
||||
version "3.2.0"
|
||||
resolved "https://registry.yarnpkg.com/@graphql-typed-document-node/core/-/core-3.2.0.tgz#5f3d96ec6b2354ad6d8a28bf216a1d97b5426861"
|
||||
integrity sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ==
|
||||
@ -2119,6 +2119,13 @@ cross-env@^7.0.3:
|
||||
dependencies:
|
||||
cross-spawn "^7.0.1"
|
||||
|
||||
cross-fetch@^3.1.5:
|
||||
version "3.1.8"
|
||||
resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.1.8.tgz#0327eba65fd68a7d119f8fb2bf9334a1a7956f82"
|
||||
integrity sha512-cvA+JwZoU0Xq+h6WkMvAUqPEYy92Obet6UdKLfW60qn99ftItKjB5T+BkyWOFWe2pUyfQ+IJHmpOTznqk1M6Kg==
|
||||
dependencies:
|
||||
node-fetch "^2.6.12"
|
||||
|
||||
cross-spawn@^7.0.0, cross-spawn@^7.0.1, cross-spawn@^7.0.2, cross-spawn@^7.0.3:
|
||||
version "7.0.3"
|
||||
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6"
|
||||
@ -3329,6 +3336,14 @@ graphql-query-complexity@^0.12.0:
|
||||
dependencies:
|
||||
lodash.get "^4.4.2"
|
||||
|
||||
graphql-request@^6.1.0:
|
||||
version "6.1.0"
|
||||
resolved "https://registry.yarnpkg.com/graphql-request/-/graphql-request-6.1.0.tgz#f4eb2107967af3c7a5907eb3131c671eac89be4f"
|
||||
integrity sha512-p+XPfS4q7aIpKVcgmnZKhMNqhltk20hfXtkaIkTfjjmiKMJ5xrt5c743cL03y/K7y1rg3WrIC49xGiEQ4mxdNw==
|
||||
dependencies:
|
||||
"@graphql-typed-document-node/core" "^3.2.0"
|
||||
cross-fetch "^3.1.5"
|
||||
|
||||
graphql-scalars@^1.22.2:
|
||||
version "1.22.2"
|
||||
resolved "https://registry.yarnpkg.com/graphql-scalars/-/graphql-scalars-1.22.2.tgz#6326e6fe2d0ad4228a9fea72a977e2bf26b86362"
|
||||
@ -4266,6 +4281,11 @@ jiti@^1.19.3:
|
||||
resolved "https://registry.yarnpkg.com/jiti/-/jiti-1.20.0.tgz#2d823b5852ee8963585c8dd8b7992ffc1ae83b42"
|
||||
integrity sha512-3TV69ZbrvV6U5DfQimop50jE9Dl6J8O1ja1dvBbMba/sZ3YBEQqJ2VZRoQPVnhlzjNtU1vaXRZVrVjU4qtm8yA==
|
||||
|
||||
jose@^5.2.2:
|
||||
version "5.2.2"
|
||||
resolved "https://registry.yarnpkg.com/jose/-/jose-5.2.2.tgz#b91170e9ba6dbe609b0c0a86568f9a1fbe4335c0"
|
||||
integrity sha512-/WByRr4jDcsKlvMd1dRJnPfS1GVO3WuKyaurJ/vvXcOaUQO8rnNObCQMlv/5uCceVQIq5Q4WLF44ohsdiTohdg==
|
||||
|
||||
js-tokens@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
|
||||
@ -4775,7 +4795,7 @@ node-abort-controller@^3.1.1:
|
||||
resolved "https://registry.yarnpkg.com/node-abort-controller/-/node-abort-controller-3.1.1.tgz#a94377e964a9a37ac3976d848cb5c765833b8548"
|
||||
integrity sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==
|
||||
|
||||
node-fetch@^2.6.7:
|
||||
node-fetch@^2.6.12, node-fetch@^2.6.7:
|
||||
version "2.7.0"
|
||||
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.7.0.tgz#d0f0fa6e3e2dc1d27efcd8ad99d550bda94d187d"
|
||||
integrity sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user