mirror of
https://github.com/IT4Change/gradido.git
synced 2026-02-06 09:56:05 +00:00
Merge branch 'master' into split_start_script
This commit is contained in:
commit
fe1bd899ef
5
.github/workflows/test_database.yml
vendored
5
.github/workflows/test_database.yml
vendored
@ -42,6 +42,11 @@ jobs:
|
|||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Set Node.js version
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '18.20.7'
|
||||||
|
|
||||||
- name: Database | docker-compose
|
- name: Database | docker-compose
|
||||||
run: docker compose -f docker-compose.yml -f docker-compose.test.yml up --detach mariadb
|
run: docker compose -f docker-compose.yml -f docker-compose.test.yml up --detach mariadb
|
||||||
|
|
||||||
|
|||||||
5
.github/workflows/test_e2e.yml
vendored
5
.github/workflows/test_e2e.yml
vendored
@ -9,6 +9,11 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set Node.js version
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '18.20.7'
|
||||||
|
|
||||||
- name: install bun
|
- name: install bun
|
||||||
uses: oven-sh/setup-bun@v2
|
uses: oven-sh/setup-bun@v2
|
||||||
|
|||||||
2
backend/@types/random-bigint/index.d.ts
vendored
2
backend/@types/random-bigint/index.d.ts
vendored
@ -1,4 +1,4 @@
|
|||||||
/* eslint-disable @typescript-eslint/ban-types */
|
|
||||||
declare module 'random-bigint' {
|
declare module 'random-bigint' {
|
||||||
function random(bits: number, cb?: (err: Error, num: BigInt) => void): BigInt
|
function random(bits: number, cb?: (err: Error, num: BigInt) => void): BigInt
|
||||||
export = random
|
export = random
|
||||||
|
|||||||
2
backend/@types/sodium-native/index.d.ts
vendored
2
backend/@types/sodium-native/index.d.ts
vendored
@ -1,4 +1,4 @@
|
|||||||
// eslint-disable-next-line import/no-unresolved
|
|
||||||
export * from '@/node_modules/@types/sodium-native'
|
export * from '@/node_modules/@types/sodium-native'
|
||||||
|
|
||||||
declare module 'sodium-native' {
|
declare module 'sodium-native' {
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
/** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */
|
/** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */
|
||||||
// eslint-disable-next-line import/no-commonjs, import/unambiguous
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
verbose: true,
|
verbose: true,
|
||||||
preset: 'ts-jest',
|
preset: 'ts-jest',
|
||||||
@ -7,7 +6,7 @@ module.exports = {
|
|||||||
collectCoverageFrom: ['src/**/*.ts', '!**/node_modules/**', '!src/seeds/**', '!build/**'],
|
collectCoverageFrom: ['src/**/*.ts', '!**/node_modules/**', '!src/seeds/**', '!build/**'],
|
||||||
coverageThreshold: {
|
coverageThreshold: {
|
||||||
global: {
|
global: {
|
||||||
lines: 77,
|
lines: 75,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
setupFiles: ['<rootDir>/test/testSetup.ts'],
|
setupFiles: ['<rootDir>/test/testSetup.ts'],
|
||||||
@ -25,22 +24,18 @@ module.exports = {
|
|||||||
'@typeorm/(.*)': '<rootDir>/src/typeorm/$1',
|
'@typeorm/(.*)': '<rootDir>/src/typeorm/$1',
|
||||||
'@test/(.*)': '<rootDir>/test/$1',
|
'@test/(.*)': '<rootDir>/test/$1',
|
||||||
'@entity/(.*)':
|
'@entity/(.*)':
|
||||||
// eslint-disable-next-line n/no-process-env
|
|
||||||
process.env.NODE_ENV === 'development'
|
process.env.NODE_ENV === 'development'
|
||||||
? '<rootDir>/../database/entity/$1'
|
? '<rootDir>/../database/entity/$1'
|
||||||
: '<rootDir>/../database/build/entity/$1',
|
: '<rootDir>/../database/build/entity/$1',
|
||||||
'@logging/(.*)':
|
'@logging/(.*)':
|
||||||
// eslint-disable-next-line n/no-process-env
|
|
||||||
process.env.NODE_ENV === 'development'
|
process.env.NODE_ENV === 'development'
|
||||||
? '<rootDir>/../database/logging/$1'
|
? '<rootDir>/../database/logging/$1'
|
||||||
: '<rootDir>/../database/build/logging/$1',
|
: '<rootDir>/../database/build/logging/$1',
|
||||||
'@dbTools/(.*)':
|
'@dbTools/(.*)':
|
||||||
// eslint-disable-next-line n/no-process-env
|
|
||||||
process.env.NODE_ENV === 'development'
|
process.env.NODE_ENV === 'development'
|
||||||
? '<rootDir>/../database/src/$1'
|
? '<rootDir>/../database/src/$1'
|
||||||
: '<rootDir>/../database/build/src/$1',
|
: '<rootDir>/../database/build/src/$1',
|
||||||
'@config/(.*)':
|
'@config/(.*)':
|
||||||
// eslint-disable-next-line n/no-process-env
|
|
||||||
process.env.NODE_ENV === 'development'
|
process.env.NODE_ENV === 'development'
|
||||||
? '<rootDir>/../config/src/$1'
|
? '<rootDir>/../config/src/$1'
|
||||||
: '<rootDir>/../config/build/$1',
|
: '<rootDir>/../config/build/$1',
|
||||||
|
|||||||
@ -11,15 +11,16 @@
|
|||||||
"build": "ts-node ./esbuild.config.ts && mkdirp build/templates/ && ncp src/emails/templates build/templates && mkdirp locales/ && ncp src/locales locales",
|
"build": "ts-node ./esbuild.config.ts && mkdirp build/templates/ && ncp src/emails/templates build/templates && mkdirp locales/ && ncp src/locales locales",
|
||||||
"clean": "tsc --build --clean",
|
"clean": "tsc --build --clean",
|
||||||
"dev": "cross-env TZ=UTC nodemon -w src --ext ts,pug,json,css -r tsconfig-paths/register src/index.ts",
|
"dev": "cross-env TZ=UTC nodemon -w src --ext ts,pug,json,css -r tsconfig-paths/register src/index.ts",
|
||||||
|
"test": "cross-env TZ=UTC NODE_ENV=development DB_DATABASE=gradido_test_backend 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/apis/gms/ExportUsers.ts",
|
"gmsusers": "cross-env TZ=UTC NODE_ENV=development ts-node -r tsconfig-paths/register src/apis/gms/ExportUsers.ts",
|
||||||
"humhubUserExport": "cross-env TZ=UTC NODE_ENV=development ts-node -r tsconfig-paths/register src/apis/humhub/ExportUsers.ts",
|
"humhubUserExport": "cross-env TZ=UTC NODE_ENV=development ts-node -r tsconfig-paths/register src/apis/humhub/ExportUsers.ts",
|
||||||
"klicktipp": "cross-env TZ=UTC NODE_ENV=development ts-node -r tsconfig-paths/register src/util/executeKlicktipp.ts",
|
|
||||||
"lint": "biome check --error-on-warnings .",
|
"lint": "biome check --error-on-warnings .",
|
||||||
"lint:fix": "biome check --error-on-warnings . --write",
|
"lint:fix": "biome check --error-on-warnings . --write",
|
||||||
|
"lint:fix:unsafe": "biome check --fix --unsafe",
|
||||||
"locales": "scripts/sort.sh",
|
"locales": "scripts/sort.sh",
|
||||||
"seed": "cross-env TZ=UTC NODE_ENV=development ts-node -r tsconfig-paths/register src/seeds/index.ts",
|
|
||||||
"start": "cross-env TZ=UTC NODE_ENV=production node build/index.js",
|
"start": "cross-env TZ=UTC NODE_ENV=production node build/index.js",
|
||||||
"test": "cross-env TZ=UTC NODE_ENV=development DB_DATABASE=gradido_test_backend jest --runInBand --forceExit --detectOpenHandles",
|
|
||||||
"typecheck": "tsc --noEmit"
|
"typecheck": "tsc --noEmit"
|
||||||
},
|
},
|
||||||
"nodemonConfig": {
|
"nodemonConfig": {
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
/* eslint-disable camelcase */
|
|
||||||
import { User } from 'database'
|
import { User } from 'database'
|
||||||
|
|
||||||
import { convertGradidoLanguageToHumhub } from '@/apis/humhub/convertLanguage'
|
import { convertGradidoLanguageToHumhub } from '@/apis/humhub/convertLanguage'
|
||||||
|
|||||||
@ -49,7 +49,11 @@ export async function syncUser(
|
|||||||
if (!isValid(postUser, user.id)) {
|
if (!isValid(postUser, user.id)) {
|
||||||
return ExecutedHumhubAction.VALIDATION_ERROR
|
return ExecutedHumhubAction.VALIDATION_ERROR
|
||||||
}
|
}
|
||||||
const humhubUser = humhubUsers.get(postUser.account.username)
|
let humhubUser = humhubUsers.get(postUser.account.username)
|
||||||
|
if (!humhubUser) {
|
||||||
|
// fallback for legacy users
|
||||||
|
humhubUser = humhubUsers.get(user.gradidoID)
|
||||||
|
}
|
||||||
const humHubClient = HumHubClient.getInstance()
|
const humHubClient = HumHubClient.getInstance()
|
||||||
if (!humHubClient) {
|
if (!humHubClient) {
|
||||||
throw new LogError('Error creating humhub client')
|
throw new LogError('Error creating humhub client')
|
||||||
|
|||||||
@ -7,6 +7,7 @@ export const INALIENABLE_RIGHTS = [
|
|||||||
RIGHTS.SEND_RESET_PASSWORD_EMAIL,
|
RIGHTS.SEND_RESET_PASSWORD_EMAIL,
|
||||||
RIGHTS.SET_PASSWORD,
|
RIGHTS.SET_PASSWORD,
|
||||||
RIGHTS.QUERY_TRANSACTION_LINK,
|
RIGHTS.QUERY_TRANSACTION_LINK,
|
||||||
|
RIGHTS.QUERY_REDEEM_JWT,
|
||||||
RIGHTS.QUERY_OPT_IN,
|
RIGHTS.QUERY_OPT_IN,
|
||||||
RIGHTS.CHECK_USERNAME,
|
RIGHTS.CHECK_USERNAME,
|
||||||
RIGHTS.PROJECT_BRANDING_BANNER,
|
RIGHTS.PROJECT_BRANDING_BANNER,
|
||||||
|
|||||||
@ -6,6 +6,7 @@ export enum RIGHTS {
|
|||||||
SEND_RESET_PASSWORD_EMAIL = 'SEND_RESET_PASSWORD_EMAIL',
|
SEND_RESET_PASSWORD_EMAIL = 'SEND_RESET_PASSWORD_EMAIL',
|
||||||
SET_PASSWORD = 'SET_PASSWORD',
|
SET_PASSWORD = 'SET_PASSWORD',
|
||||||
QUERY_TRANSACTION_LINK = 'QUERY_TRANSACTION_LINK',
|
QUERY_TRANSACTION_LINK = 'QUERY_TRANSACTION_LINK',
|
||||||
|
QUERY_REDEEM_JWT = 'QUERY_REDEEM_JWT',
|
||||||
QUERY_OPT_IN = 'QUERY_OPT_IN',
|
QUERY_OPT_IN = 'QUERY_OPT_IN',
|
||||||
CHECK_USERNAME = 'CHECK_USERNAME',
|
CHECK_USERNAME = 'CHECK_USERNAME',
|
||||||
PROJECT_BRANDING_BANNER = 'PROJECT_BRANDING_BANNER',
|
PROJECT_BRANDING_BANNER = 'PROJECT_BRANDING_BANNER',
|
||||||
@ -24,6 +25,7 @@ export enum RIGHTS {
|
|||||||
CREATE_TRANSACTION_LINK = 'CREATE_TRANSACTION_LINK',
|
CREATE_TRANSACTION_LINK = 'CREATE_TRANSACTION_LINK',
|
||||||
DELETE_TRANSACTION_LINK = 'DELETE_TRANSACTION_LINK',
|
DELETE_TRANSACTION_LINK = 'DELETE_TRANSACTION_LINK',
|
||||||
REDEEM_TRANSACTION_LINK = 'REDEEM_TRANSACTION_LINK',
|
REDEEM_TRANSACTION_LINK = 'REDEEM_TRANSACTION_LINK',
|
||||||
|
DISBURSE_TRANSACTION_LINK = 'DISBURSE_TRANSACTION_LINK',
|
||||||
LIST_TRANSACTION_LINKS = 'LIST_TRANSACTION_LINKS',
|
LIST_TRANSACTION_LINKS = 'LIST_TRANSACTION_LINKS',
|
||||||
GDT_BALANCE = 'GDT_BALANCE',
|
GDT_BALANCE = 'GDT_BALANCE',
|
||||||
CREATE_CONTRIBUTION = 'CREATE_CONTRIBUTION',
|
CREATE_CONTRIBUTION = 'CREATE_CONTRIBUTION',
|
||||||
|
|||||||
@ -15,6 +15,7 @@ export const USER_RIGHTS = [
|
|||||||
RIGHTS.CREATE_TRANSACTION_LINK,
|
RIGHTS.CREATE_TRANSACTION_LINK,
|
||||||
RIGHTS.DELETE_TRANSACTION_LINK,
|
RIGHTS.DELETE_TRANSACTION_LINK,
|
||||||
RIGHTS.REDEEM_TRANSACTION_LINK,
|
RIGHTS.REDEEM_TRANSACTION_LINK,
|
||||||
|
RIGHTS.DISBURSE_TRANSACTION_LINK,
|
||||||
RIGHTS.LIST_TRANSACTION_LINKS,
|
RIGHTS.LIST_TRANSACTION_LINKS,
|
||||||
RIGHTS.GDT_BALANCE,
|
RIGHTS.GDT_BALANCE,
|
||||||
RIGHTS.CREATE_CONTRIBUTION,
|
RIGHTS.CREATE_CONTRIBUTION,
|
||||||
|
|||||||
70
backend/src/auth/jwt/JWT.ts
Normal file
70
backend/src/auth/jwt/JWT.ts
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
import { createPrivateKey, sign } from 'node:crypto'
|
||||||
|
|
||||||
|
import { JWTPayload, SignJWT, decodeJwt, jwtVerify } from 'jose'
|
||||||
|
|
||||||
|
import { LogError } from '@/server/LogError'
|
||||||
|
import { backendLogger as logger } from '@/server/logger'
|
||||||
|
|
||||||
|
import { JwtPayloadType } from './payloadtypes/JwtPayloadType'
|
||||||
|
|
||||||
|
export const verify = async (token: string, signkey: string): Promise<JwtPayloadType | null> => {
|
||||||
|
if (!token) {
|
||||||
|
throw new LogError('401 Unauthorized')
|
||||||
|
}
|
||||||
|
logger.info('JWT.verify... token, signkey=', token, signkey)
|
||||||
|
|
||||||
|
try {
|
||||||
|
/*
|
||||||
|
const { KeyObject } = await import('node:crypto')
|
||||||
|
const cryptoKey = await crypto.subtle.importKey('raw', signkey, { name: 'RS256' }, false, [
|
||||||
|
'sign',
|
||||||
|
])
|
||||||
|
const keyObject = KeyObject.from(cryptoKey)
|
||||||
|
logger.info('JWT.verify... keyObject=', keyObject)
|
||||||
|
logger.info('JWT.verify... keyObject.asymmetricKeyDetails=', keyObject.asymmetricKeyDetails)
|
||||||
|
logger.info('JWT.verify... keyObject.asymmetricKeyType=', keyObject.asymmetricKeyType)
|
||||||
|
logger.info('JWT.verify... keyObject.asymmetricKeySize=', keyObject.asymmetricKeySize)
|
||||||
|
*/
|
||||||
|
const secret = new TextEncoder().encode(signkey)
|
||||||
|
const { payload } = await jwtVerify(token, secret, {
|
||||||
|
issuer: 'urn:gradido:issuer',
|
||||||
|
audience: 'urn:gradido:audience',
|
||||||
|
})
|
||||||
|
logger.info('JWT.verify after jwtVerify... payload=', payload)
|
||||||
|
return payload as JwtPayloadType
|
||||||
|
} catch (err) {
|
||||||
|
logger.error('JWT.verify after jwtVerify... error=', err)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const encode = async (payload: JwtPayloadType, signkey: string): Promise<string> => {
|
||||||
|
logger.info('JWT.encode... payload=', payload)
|
||||||
|
logger.info('JWT.encode... signkey=', signkey)
|
||||||
|
try {
|
||||||
|
const secret = new TextEncoder().encode(signkey)
|
||||||
|
const token = await new SignJWT({ payload, 'urn:gradido:claim': true })
|
||||||
|
.setProtectedHeader({
|
||||||
|
alg: 'HS256',
|
||||||
|
})
|
||||||
|
.setIssuedAt()
|
||||||
|
.setIssuer('urn:gradido:issuer')
|
||||||
|
.setAudience('urn:gradido:audience')
|
||||||
|
.setExpirationTime(payload.expiration)
|
||||||
|
.sign(secret)
|
||||||
|
return token
|
||||||
|
} catch (e) {
|
||||||
|
logger.error('Failed to sign JWT:', e)
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const verifyJwtType = async (token: string, signkey: string): Promise<string> => {
|
||||||
|
const payload = await verify(token, signkey)
|
||||||
|
return payload ? payload.tokentype : 'unknown token type'
|
||||||
|
}
|
||||||
|
|
||||||
|
export const decode = (token: string): JwtPayloadType => {
|
||||||
|
const { payload } = decodeJwt(token)
|
||||||
|
return payload as JwtPayloadType
|
||||||
|
}
|
||||||
48
backend/src/auth/jwt/payloadtypes/DisburseJwtPayloadType.ts
Normal file
48
backend/src/auth/jwt/payloadtypes/DisburseJwtPayloadType.ts
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
// import { JWTPayload } from 'jose'
|
||||||
|
import { JwtPayloadType } from './JwtPayloadType'
|
||||||
|
|
||||||
|
export class DisburseJwtPayloadType extends JwtPayloadType {
|
||||||
|
static DISBURSE_ACTIVATION_TYPE = 'disburse-activation'
|
||||||
|
|
||||||
|
sendercommunityuuid: string
|
||||||
|
sendergradidoid: string
|
||||||
|
recipientcommunityuuid: string
|
||||||
|
recipientcommunityname: string
|
||||||
|
recipientgradidoid: string
|
||||||
|
recipientfirstname: string
|
||||||
|
code: string
|
||||||
|
amount: string
|
||||||
|
memo: string
|
||||||
|
validuntil: string
|
||||||
|
recipientalias: string
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
senderCommunityUuid: string,
|
||||||
|
senderGradidoId: string,
|
||||||
|
recipientCommunityUuid: string,
|
||||||
|
recipientCommunityName: string,
|
||||||
|
recipientGradidoId: string,
|
||||||
|
recipientFirstName: string,
|
||||||
|
code: string,
|
||||||
|
amount: string,
|
||||||
|
memo: string,
|
||||||
|
validUntil: string,
|
||||||
|
recipientAlias: string,
|
||||||
|
) {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
|
||||||
|
super()
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||||
|
this.tokentype = DisburseJwtPayloadType.DISBURSE_ACTIVATION_TYPE
|
||||||
|
this.sendercommunityuuid = senderCommunityUuid
|
||||||
|
this.sendergradidoid = senderGradidoId
|
||||||
|
this.recipientcommunityuuid = recipientCommunityUuid
|
||||||
|
this.recipientcommunityname = recipientCommunityName
|
||||||
|
this.recipientgradidoid = recipientGradidoId
|
||||||
|
this.recipientfirstname = recipientFirstName
|
||||||
|
this.code = code
|
||||||
|
this.amount = amount
|
||||||
|
this.memo = memo
|
||||||
|
this.validuntil = validUntil
|
||||||
|
this.recipientalias = recipientAlias
|
||||||
|
}
|
||||||
|
}
|
||||||
21
backend/src/auth/jwt/payloadtypes/JwtPayloadType.ts
Normal file
21
backend/src/auth/jwt/payloadtypes/JwtPayloadType.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import { JWTPayload } from 'jose'
|
||||||
|
|
||||||
|
import { CONFIG } from '@/config'
|
||||||
|
|
||||||
|
export class JwtPayloadType implements JWTPayload {
|
||||||
|
iat?: number | undefined
|
||||||
|
exp?: number | undefined
|
||||||
|
nbf?: number | undefined
|
||||||
|
jti?: string | undefined
|
||||||
|
aud?: string | string[] | undefined
|
||||||
|
sub?: string | undefined
|
||||||
|
iss?: string | undefined;
|
||||||
|
[propName: string]: unknown
|
||||||
|
|
||||||
|
tokentype: string
|
||||||
|
expiration: string // in minutes (format: 10m for ten minutes)
|
||||||
|
constructor() {
|
||||||
|
this.tokentype = 'unknown jwt type'
|
||||||
|
this.expiration = CONFIG.REDEEM_JWT_TOKEN_EXPIRATION || '10m'
|
||||||
|
}
|
||||||
|
}
|
||||||
36
backend/src/auth/jwt/payloadtypes/RedeemJwtPayloadType.ts
Normal file
36
backend/src/auth/jwt/payloadtypes/RedeemJwtPayloadType.ts
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
// import { JWTPayload } from 'jose'
|
||||||
|
import { JwtPayloadType } from './JwtPayloadType'
|
||||||
|
|
||||||
|
export class RedeemJwtPayloadType extends JwtPayloadType {
|
||||||
|
static REDEEM_ACTIVATION_TYPE = 'redeem-activation'
|
||||||
|
|
||||||
|
sendercommunityuuid: string
|
||||||
|
sendergradidoid: string
|
||||||
|
sendername: string // alias or firstname
|
||||||
|
redeemcode: string
|
||||||
|
amount: string
|
||||||
|
memo: string
|
||||||
|
validuntil: string
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
senderCom: string,
|
||||||
|
senderUser: string,
|
||||||
|
sendername: string,
|
||||||
|
code: string,
|
||||||
|
amount: string,
|
||||||
|
memo: string,
|
||||||
|
validUntil: string,
|
||||||
|
) {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
|
||||||
|
super()
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||||
|
this.tokentype = RedeemJwtPayloadType.REDEEM_ACTIVATION_TYPE
|
||||||
|
this.sendercommunityuuid = senderCom
|
||||||
|
this.sendergradidoid = senderUser
|
||||||
|
this.sendername = sendername
|
||||||
|
this.redeemcode = code
|
||||||
|
this.amount = amount
|
||||||
|
this.memo = memo
|
||||||
|
this.validuntil = validUntil
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,6 +1,5 @@
|
|||||||
// ATTENTION: DO NOT PUT ANY SECRETS IN HERE (or the .env)
|
// ATTENTION: DO NOT PUT ANY SECRETS IN HERE (or the .env)
|
||||||
|
|
||||||
// eslint-disable-next-line import/no-unresolved
|
|
||||||
import { validate } from 'config-schema'
|
import { validate } from 'config-schema'
|
||||||
import { latestDbVersion } from 'database'
|
import { latestDbVersion } from 'database'
|
||||||
import { Decimal } from 'decimal.js-light'
|
import { Decimal } from 'decimal.js-light'
|
||||||
@ -26,6 +25,7 @@ const server = {
|
|||||||
PORT: process.env.PORT ?? 4000,
|
PORT: process.env.PORT ?? 4000,
|
||||||
JWT_SECRET: process.env.JWT_SECRET ?? 'secret123',
|
JWT_SECRET: process.env.JWT_SECRET ?? 'secret123',
|
||||||
JWT_EXPIRES_IN: process.env.JWT_EXPIRES_IN ?? '10m',
|
JWT_EXPIRES_IN: process.env.JWT_EXPIRES_IN ?? '10m',
|
||||||
|
REDEEM_JWT_TOKEN_EXPIRATION: process.env.REDEEM_JWT_TOKEN_EXPIRATION ?? '10m',
|
||||||
GRAPHIQL: process.env.GRAPHIQL === 'true' || false,
|
GRAPHIQL: process.env.GRAPHIQL === 'true' || false,
|
||||||
GDT_ACTIVE: process.env.GDT_ACTIVE === 'true' || false,
|
GDT_ACTIVE: process.env.GDT_ACTIVE === 'true' || false,
|
||||||
GDT_API_URL: process.env.GDT_API_URL ?? 'https://gdt.gradido.net',
|
GDT_API_URL: process.env.GDT_API_URL ?? 'https://gdt.gradido.net',
|
||||||
@ -35,8 +35,14 @@ const server = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const database = {
|
const database = {
|
||||||
|
DB_CONNECT_RETRY_COUNT: process.env.DB_CONNECT_RETRY_COUNT
|
||||||
|
? Number.parseInt(process.env.DB_CONNECT_RETRY_COUNT)
|
||||||
|
: 15,
|
||||||
|
DB_CONNECT_RETRY_DELAY_MS: process.env.DB_CONNECT_RETRY_DELAY_MS
|
||||||
|
? Number.parseInt(process.env.DB_CONNECT_RETRY_DELAY_MS)
|
||||||
|
: 500,
|
||||||
DB_HOST: process.env.DB_HOST ?? 'localhost',
|
DB_HOST: process.env.DB_HOST ?? 'localhost',
|
||||||
DB_PORT: process.env.DB_PORT ? parseInt(process.env.DB_PORT) : 3306,
|
DB_PORT: process.env.DB_PORT ? Number.parseInt(process.env.DB_PORT) : 3306,
|
||||||
DB_USER: process.env.DB_USER ?? 'root',
|
DB_USER: process.env.DB_USER ?? 'root',
|
||||||
DB_PASSWORD: process.env.DB_PASSWORD ?? '',
|
DB_PASSWORD: process.env.DB_PASSWORD ?? '',
|
||||||
DB_DATABASE: process.env.DB_DATABASE ?? 'gradido_community',
|
DB_DATABASE: process.env.DB_DATABASE ?? 'gradido_community',
|
||||||
|
|||||||
@ -3,6 +3,8 @@ import {
|
|||||||
COMMUNITY_NAME,
|
COMMUNITY_NAME,
|
||||||
COMMUNITY_SUPPORT_MAIL,
|
COMMUNITY_SUPPORT_MAIL,
|
||||||
COMMUNITY_URL,
|
COMMUNITY_URL,
|
||||||
|
DB_CONNECT_RETRY_COUNT,
|
||||||
|
DB_CONNECT_RETRY_DELAY_MS,
|
||||||
DB_DATABASE,
|
DB_DATABASE,
|
||||||
DB_HOST,
|
DB_HOST,
|
||||||
DB_PASSWORD,
|
DB_PASSWORD,
|
||||||
@ -38,6 +40,8 @@ export const schema = Joi.object({
|
|||||||
DB_USER,
|
DB_USER,
|
||||||
DB_VERSION,
|
DB_VERSION,
|
||||||
DB_DATABASE,
|
DB_DATABASE,
|
||||||
|
DB_CONNECT_RETRY_COUNT,
|
||||||
|
DB_CONNECT_RETRY_DELAY_MS,
|
||||||
DECAY_START_TIME,
|
DECAY_START_TIME,
|
||||||
GDT_API_URL,
|
GDT_API_URL,
|
||||||
GDT_ACTIVE,
|
GDT_ACTIVE,
|
||||||
@ -363,5 +367,20 @@ export const schema = Joi.object({
|
|||||||
.required()
|
.required()
|
||||||
.description('Time for JWT token to expire, auto logout'),
|
.description('Time for JWT token to expire, auto logout'),
|
||||||
|
|
||||||
|
REDEEM_JWT_TOKEN_EXPIRATION: Joi.alternatives()
|
||||||
|
.try(
|
||||||
|
Joi.string()
|
||||||
|
.pattern(/^\d+[smhdw]$/)
|
||||||
|
.description(
|
||||||
|
'Expiration time for x-community redeem JWT token, in format like "10m", "1h", "1d"',
|
||||||
|
)
|
||||||
|
.default('10m'),
|
||||||
|
Joi.number()
|
||||||
|
.positive()
|
||||||
|
.description('Expiration time for x-community redeem JWT token in minutes'),
|
||||||
|
)
|
||||||
|
.required()
|
||||||
|
.description('Time for x-community redeem JWT token to expire'),
|
||||||
|
|
||||||
WEBHOOK_ELOPAGE_SECRET: Joi.string().description("isn't really used any more").optional(),
|
WEBHOOK_ELOPAGE_SECRET: Joi.string().description("isn't really used any more").optional(),
|
||||||
})
|
})
|
||||||
|
|||||||
@ -78,6 +78,7 @@ export class FederationClient {
|
|||||||
)
|
)
|
||||||
return data.getPublicCommunityInfo
|
return data.getPublicCommunityInfo
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
logger.warn(' err', err)
|
||||||
const errorString = JSON.stringify(err)
|
const errorString = JSON.stringify(err)
|
||||||
logger.warn('Federation: getPublicCommunityInfo failed for endpoint', {
|
logger.warn('Federation: getPublicCommunityInfo failed for endpoint', {
|
||||||
endpoint: this.endpoint,
|
endpoint: this.endpoint,
|
||||||
|
|||||||
55
backend/src/graphql/model/RedeemJwtLink.ts
Normal file
55
backend/src/graphql/model/RedeemJwtLink.ts
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
import { Decimal } from 'decimal.js-light'
|
||||||
|
import { Field, ObjectType } from 'type-graphql'
|
||||||
|
|
||||||
|
import { RedeemJwtPayloadType } from '@/auth/jwt/payloadtypes/RedeemJwtPayloadType'
|
||||||
|
|
||||||
|
import { Community } from './Community'
|
||||||
|
import { User } from './User'
|
||||||
|
|
||||||
|
@ObjectType()
|
||||||
|
export class RedeemJwtLink {
|
||||||
|
constructor(
|
||||||
|
redeemJwtPayload: RedeemJwtPayloadType,
|
||||||
|
senderCommunity: Community,
|
||||||
|
senderUser: User,
|
||||||
|
recipientCommunity: Community,
|
||||||
|
recipientUser?: User,
|
||||||
|
) {
|
||||||
|
this.senderCommunity = senderCommunity
|
||||||
|
this.recipientCommunity = recipientCommunity
|
||||||
|
this.senderUser = senderUser
|
||||||
|
if (recipientUser !== undefined) {
|
||||||
|
this.recipientUser = recipientUser
|
||||||
|
} else {
|
||||||
|
this.recipientUser = null
|
||||||
|
}
|
||||||
|
this.amount = new Decimal(redeemJwtPayload.amount)
|
||||||
|
this.memo = redeemJwtPayload.memo
|
||||||
|
this.code = redeemJwtPayload.redeemcode
|
||||||
|
this.validUntil = new Date(redeemJwtPayload.validuntil)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Field(() => Community)
|
||||||
|
senderCommunity: Community
|
||||||
|
|
||||||
|
@Field(() => User)
|
||||||
|
senderUser: User
|
||||||
|
|
||||||
|
@Field(() => Community)
|
||||||
|
recipientCommunity: Community
|
||||||
|
|
||||||
|
@Field(() => User, { nullable: true })
|
||||||
|
recipientUser: User | null
|
||||||
|
|
||||||
|
@Field(() => Decimal)
|
||||||
|
amount: Decimal
|
||||||
|
|
||||||
|
@Field(() => String)
|
||||||
|
memo: string
|
||||||
|
|
||||||
|
@Field(() => String)
|
||||||
|
code: string
|
||||||
|
|
||||||
|
@Field(() => Date)
|
||||||
|
validUntil: Date
|
||||||
|
}
|
||||||
@ -1,33 +1,48 @@
|
|||||||
import { TransactionLink as dbTransactionLink } from 'database'
|
import { Community as DbCommunity, TransactionLink as DbTransactionLink } from 'database'
|
||||||
import { Decimal } from 'decimal.js-light'
|
import { Decimal } from 'decimal.js-light'
|
||||||
import { Field, Int, ObjectType } from 'type-graphql'
|
import { Field, Int, ObjectType } from 'type-graphql'
|
||||||
|
|
||||||
import { CONFIG } from '@/config'
|
import { CONFIG } from '@/config'
|
||||||
|
|
||||||
|
import { Community } from './Community'
|
||||||
import { User } from './User'
|
import { User } from './User'
|
||||||
|
|
||||||
@ObjectType()
|
@ObjectType()
|
||||||
export class TransactionLink {
|
export class TransactionLink {
|
||||||
constructor(transactionLink: dbTransactionLink, user: User, redeemedBy: User | null = null) {
|
constructor(
|
||||||
this.id = transactionLink.id
|
dbTransactionLink?: DbTransactionLink,
|
||||||
this.user = user
|
user?: User,
|
||||||
this.amount = transactionLink.amount
|
redeemedBy?: User,
|
||||||
this.holdAvailableAmount = transactionLink.holdAvailableAmount
|
dbCommunities?: DbCommunity[],
|
||||||
this.memo = transactionLink.memo
|
) {
|
||||||
this.code = transactionLink.code
|
if (dbTransactionLink !== undefined) {
|
||||||
this.createdAt = transactionLink.createdAt
|
this.id = dbTransactionLink.id
|
||||||
this.validUntil = transactionLink.validUntil
|
this.amount = dbTransactionLink.amount
|
||||||
this.deletedAt = transactionLink.deletedAt
|
this.holdAvailableAmount = dbTransactionLink.holdAvailableAmount
|
||||||
this.redeemedAt = transactionLink.redeemedAt
|
this.memo = dbTransactionLink.memo
|
||||||
this.redeemedBy = redeemedBy
|
this.code = dbTransactionLink.code
|
||||||
this.link = CONFIG.COMMUNITY_REDEEM_URL + this.code
|
this.link = CONFIG.COMMUNITY_REDEEM_URL + this.code
|
||||||
|
this.createdAt = dbTransactionLink.createdAt
|
||||||
|
this.validUntil = dbTransactionLink.validUntil
|
||||||
|
this.deletedAt = dbTransactionLink.deletedAt
|
||||||
|
this.redeemedAt = dbTransactionLink.redeemedAt
|
||||||
|
}
|
||||||
|
if (user !== undefined) {
|
||||||
|
this.senderUser = user
|
||||||
|
}
|
||||||
|
if (redeemedBy !== undefined) {
|
||||||
|
this.redeemedBy = redeemedBy
|
||||||
|
}
|
||||||
|
if (dbCommunities !== undefined) {
|
||||||
|
this.communities = dbCommunities.map((dbCom: DbCommunity) => new Community(dbCom))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Field(() => Int)
|
@Field(() => Int)
|
||||||
id: number
|
id: number
|
||||||
|
|
||||||
@Field(() => User)
|
@Field(() => User)
|
||||||
user: User
|
senderUser: User
|
||||||
|
|
||||||
@Field(() => Decimal)
|
@Field(() => Decimal)
|
||||||
amount: Decimal
|
amount: Decimal
|
||||||
@ -58,6 +73,12 @@ export class TransactionLink {
|
|||||||
|
|
||||||
@Field(() => String)
|
@Field(() => String)
|
||||||
link: string
|
link: string
|
||||||
|
|
||||||
|
@Field(() => String)
|
||||||
|
communityName: string
|
||||||
|
|
||||||
|
@Field(() => [Community])
|
||||||
|
communities: Community[]
|
||||||
}
|
}
|
||||||
|
|
||||||
@ObjectType()
|
@ObjectType()
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { User as dbUser } from 'database'
|
import { User as DbUser } from 'database'
|
||||||
import { Field, Int, ObjectType } from 'type-graphql'
|
import { Field, Int, ObjectType } from 'type-graphql'
|
||||||
import { Point } from 'typeorm'
|
import { Point } from 'typeorm'
|
||||||
|
|
||||||
@ -14,43 +14,43 @@ import { UserContact } from './UserContact'
|
|||||||
|
|
||||||
@ObjectType()
|
@ObjectType()
|
||||||
export class User {
|
export class User {
|
||||||
constructor(user: dbUser | null) {
|
constructor(dbUser: DbUser | null) {
|
||||||
if (user) {
|
if (dbUser) {
|
||||||
this.id = user.id
|
this.id = dbUser.id
|
||||||
this.foreign = user.foreign
|
this.foreign = dbUser.foreign
|
||||||
this.communityUuid = user.communityUuid
|
this.communityUuid = dbUser.communityUuid
|
||||||
if (user.community) {
|
if (dbUser.community) {
|
||||||
this.communityName = user.community.name
|
this.communityName = dbUser.community.name
|
||||||
}
|
}
|
||||||
this.gradidoID = user.gradidoID
|
this.gradidoID = dbUser.gradidoID
|
||||||
this.alias = user.alias
|
this.alias = dbUser.alias
|
||||||
|
|
||||||
const publishNameLogic = new PublishNameLogic(user)
|
const publishNameLogic = new PublishNameLogic(dbUser)
|
||||||
const publishNameType = user.humhubPublishName as PublishNameType
|
const publishNameType = dbUser.humhubPublishName as PublishNameType
|
||||||
this.publicName = publishNameLogic.getPublicName(publishNameType)
|
this.publicName = publishNameLogic.getPublicName(publishNameType)
|
||||||
this.userIdentifier = publishNameLogic.getUserIdentifier(publishNameType)
|
this.userIdentifier = publishNameLogic.getUserIdentifier(publishNameType)
|
||||||
|
|
||||||
if (user.emailContact) {
|
if (dbUser.emailContact) {
|
||||||
this.emailChecked = user.emailContact.emailChecked
|
this.emailChecked = dbUser.emailContact.emailChecked
|
||||||
this.emailContact = new UserContact(user.emailContact)
|
this.emailContact = new UserContact(dbUser.emailContact)
|
||||||
}
|
}
|
||||||
this.firstName = user.firstName
|
this.firstName = dbUser.firstName
|
||||||
this.lastName = user.lastName
|
this.lastName = dbUser.lastName
|
||||||
this.deletedAt = user.deletedAt
|
this.deletedAt = dbUser.deletedAt
|
||||||
this.createdAt = user.createdAt
|
this.createdAt = dbUser.createdAt
|
||||||
this.language = user.language
|
this.language = dbUser.language
|
||||||
this.publisherId = user.publisherId
|
this.publisherId = dbUser.publisherId
|
||||||
this.roles = user.userRoles?.map((userRole) => userRole.role) ?? []
|
this.roles = dbUser.userRoles?.map((userRole) => userRole.role) ?? []
|
||||||
this.klickTipp = null
|
this.klickTipp = null
|
||||||
this.hasElopage = null
|
this.hasElopage = null
|
||||||
this.hideAmountGDD = user.hideAmountGDD
|
this.hideAmountGDD = dbUser.hideAmountGDD
|
||||||
this.hideAmountGDT = user.hideAmountGDT
|
this.hideAmountGDT = dbUser.hideAmountGDT
|
||||||
this.humhubAllowed = user.humhubAllowed
|
this.humhubAllowed = dbUser.humhubAllowed
|
||||||
this.gmsAllowed = user.gmsAllowed
|
this.gmsAllowed = dbUser.gmsAllowed
|
||||||
this.gmsPublishName = user.gmsPublishName
|
this.gmsPublishName = dbUser.gmsPublishName
|
||||||
this.humhubPublishName = user.humhubPublishName
|
this.humhubPublishName = dbUser.humhubPublishName
|
||||||
this.gmsPublishLocation = user.gmsPublishLocation
|
this.gmsPublishLocation = dbUser.gmsPublishLocation
|
||||||
this.userLocation = user.location ? Point2Location(user.location as Point) : null
|
this.userLocation = dbUser.location ? Point2Location(dbUser.location as Point) : null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,19 @@
|
|||||||
import { randomBytes } from 'crypto'
|
import { randomBytes } from 'crypto'
|
||||||
|
|
||||||
|
import { Paginated } from '@arg/Paginated'
|
||||||
|
import { TransactionLinkArgs } from '@arg/TransactionLinkArgs'
|
||||||
|
import { TransactionLinkFilters } from '@arg/TransactionLinkFilters'
|
||||||
|
import { ContributionCycleType } from '@enum/ContributionCycleType'
|
||||||
|
import { ContributionStatus } from '@enum/ContributionStatus'
|
||||||
|
import { ContributionType } from '@enum/ContributionType'
|
||||||
|
import { TransactionTypeId } from '@enum/TransactionTypeId'
|
||||||
|
import { Community } from '@model/Community'
|
||||||
|
import { ContributionLink } from '@model/ContributionLink'
|
||||||
|
import { Decay } from '@model/Decay'
|
||||||
|
import { RedeemJwtLink } from '@model/RedeemJwtLink'
|
||||||
|
import { TransactionLink, TransactionLinkResult } from '@model/TransactionLink'
|
||||||
|
import { User } from '@model/User'
|
||||||
|
import { QueryLinkResult } from '@union/QueryLinkResult'
|
||||||
import {
|
import {
|
||||||
Contribution as DbContribution,
|
Contribution as DbContribution,
|
||||||
ContributionLink as DbContributionLink,
|
ContributionLink as DbContributionLink,
|
||||||
@ -11,20 +25,9 @@ import { Decimal } from 'decimal.js-light'
|
|||||||
import { Arg, Args, Authorized, Ctx, Int, Mutation, Query, Resolver } from 'type-graphql'
|
import { Arg, Args, Authorized, Ctx, Int, Mutation, Query, Resolver } from 'type-graphql'
|
||||||
import { getConnection } from 'typeorm'
|
import { getConnection } from 'typeorm'
|
||||||
|
|
||||||
import { Paginated } from '@arg/Paginated'
|
|
||||||
import { TransactionLinkArgs } from '@arg/TransactionLinkArgs'
|
|
||||||
import { TransactionLinkFilters } from '@arg/TransactionLinkFilters'
|
|
||||||
import { ContributionCycleType } from '@enum/ContributionCycleType'
|
|
||||||
import { ContributionStatus } from '@enum/ContributionStatus'
|
|
||||||
import { ContributionType } from '@enum/ContributionType'
|
|
||||||
import { TransactionTypeId } from '@enum/TransactionTypeId'
|
|
||||||
import { ContributionLink } from '@model/ContributionLink'
|
|
||||||
import { Decay } from '@model/Decay'
|
|
||||||
import { TransactionLink, TransactionLinkResult } from '@model/TransactionLink'
|
|
||||||
import { User } from '@model/User'
|
|
||||||
import { QueryLinkResult } from '@union/QueryLinkResult'
|
|
||||||
|
|
||||||
import { RIGHTS } from '@/auth/RIGHTS'
|
import { RIGHTS } from '@/auth/RIGHTS'
|
||||||
|
import { decode, encode, verify } from '@/auth/jwt/JWT'
|
||||||
|
import { RedeemJwtPayloadType } from '@/auth/jwt/payloadtypes/RedeemJwtPayloadType'
|
||||||
import {
|
import {
|
||||||
EVENT_CONTRIBUTION_LINK_REDEEM,
|
EVENT_CONTRIBUTION_LINK_REDEEM,
|
||||||
EVENT_TRANSACTION_LINK_CREATE,
|
EVENT_TRANSACTION_LINK_CREATE,
|
||||||
@ -40,7 +43,13 @@ import { calculateDecay } from '@/util/decay'
|
|||||||
import { fullName } from '@/util/utilities'
|
import { fullName } from '@/util/utilities'
|
||||||
import { calculateBalance } from '@/util/validate'
|
import { calculateBalance } from '@/util/validate'
|
||||||
|
|
||||||
|
import { DisburseJwtPayloadType } from '@/auth/jwt/payloadtypes/DisburseJwtPayloadType'
|
||||||
import { executeTransaction } from './TransactionResolver'
|
import { executeTransaction } from './TransactionResolver'
|
||||||
|
import {
|
||||||
|
getAuthenticatedCommunities,
|
||||||
|
getCommunityByUuid,
|
||||||
|
getHomeCommunity,
|
||||||
|
} from './util/communities'
|
||||||
import { getUserCreation, validateContribution } from './util/creations'
|
import { getUserCreation, validateContribution } from './util/creations'
|
||||||
import { getLastTransaction } from './util/getLastTransaction'
|
import { getLastTransaction } from './util/getLastTransaction'
|
||||||
import { sendTransactionsToDltConnector } from './util/sendTransactionsToDltConnector'
|
import { sendTransactionsToDltConnector } from './util/sendTransactionsToDltConnector'
|
||||||
@ -138,6 +147,7 @@ export class TransactionLinkResolver {
|
|||||||
@Authorized([RIGHTS.QUERY_TRANSACTION_LINK])
|
@Authorized([RIGHTS.QUERY_TRANSACTION_LINK])
|
||||||
@Query(() => QueryLinkResult)
|
@Query(() => QueryLinkResult)
|
||||||
async queryTransactionLink(@Arg('code') code: string): Promise<typeof QueryLinkResult> {
|
async queryTransactionLink(@Arg('code') code: string): Promise<typeof QueryLinkResult> {
|
||||||
|
logger.debug('TransactionLinkResolver.queryTransactionLink... code=', code)
|
||||||
if (code.match(/^CL-/)) {
|
if (code.match(/^CL-/)) {
|
||||||
const contributionLink = await DbContributionLink.findOneOrFail({
|
const contributionLink = await DbContributionLink.findOneOrFail({
|
||||||
where: { code: code.replace('CL-', '') },
|
where: { code: code.replace('CL-', '') },
|
||||||
@ -145,18 +155,36 @@ export class TransactionLinkResolver {
|
|||||||
})
|
})
|
||||||
return new ContributionLink(contributionLink)
|
return new ContributionLink(contributionLink)
|
||||||
} else {
|
} else {
|
||||||
const transactionLink = await DbTransactionLink.findOneOrFail({
|
let txLinkFound = false
|
||||||
where: { code },
|
let dbTransactionLink!: DbTransactionLink
|
||||||
withDeleted: true,
|
try {
|
||||||
})
|
dbTransactionLink = await DbTransactionLink.findOneOrFail({
|
||||||
const user = await DbUser.findOneOrFail({ where: { id: transactionLink.userId } })
|
where: { code },
|
||||||
let redeemedBy: User | null = null
|
withDeleted: true,
|
||||||
if (transactionLink?.redeemedBy) {
|
})
|
||||||
redeemedBy = new User(
|
txLinkFound = true
|
||||||
await DbUser.findOneOrFail({ where: { id: transactionLink.redeemedBy } }),
|
} catch (_err) {
|
||||||
)
|
txLinkFound = false
|
||||||
|
}
|
||||||
|
// normal redeem code
|
||||||
|
if (txLinkFound) {
|
||||||
|
logger.debug(
|
||||||
|
'TransactionLinkResolver.queryTransactionLink... normal redeem code found=',
|
||||||
|
txLinkFound,
|
||||||
|
)
|
||||||
|
const user = await DbUser.findOneOrFail({ where: { id: dbTransactionLink.userId } })
|
||||||
|
let redeemedBy
|
||||||
|
if (dbTransactionLink.redeemedBy) {
|
||||||
|
redeemedBy = new User(
|
||||||
|
await DbUser.findOneOrFail({ where: { id: dbTransactionLink.redeemedBy } }),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
const communities = await getAuthenticatedCommunities()
|
||||||
|
return new TransactionLink(dbTransactionLink, new User(user), redeemedBy, communities)
|
||||||
|
} else {
|
||||||
|
// redeem jwt-token
|
||||||
|
return await this.queryRedeemJwtLink(code)
|
||||||
}
|
}
|
||||||
return new TransactionLink(transactionLink, new User(user), redeemedBy)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -169,7 +197,6 @@ export class TransactionLinkResolver {
|
|||||||
const clientTimezoneOffset = getClientTimezoneOffset(context)
|
const clientTimezoneOffset = getClientTimezoneOffset(context)
|
||||||
// const homeCom = await DbCommunity.findOneOrFail({ where: { foreign: false } })
|
// const homeCom = await DbCommunity.findOneOrFail({ where: { foreign: false } })
|
||||||
const user = getUser(context)
|
const user = getUser(context)
|
||||||
|
|
||||||
if (code.match(/^CL-/)) {
|
if (code.match(/^CL-/)) {
|
||||||
// acquire lock
|
// acquire lock
|
||||||
const releaseLock = await TRANSACTIONS_LOCK.acquire()
|
const releaseLock = await TRANSACTIONS_LOCK.acquire()
|
||||||
@ -366,6 +393,104 @@ export class TransactionLinkResolver {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Authorized([RIGHTS.QUERY_REDEEM_JWT])
|
||||||
|
@Mutation(() => String)
|
||||||
|
async createRedeemJwt(
|
||||||
|
@Arg('gradidoId') gradidoId: string,
|
||||||
|
@Arg('senderCommunityUuid') senderCommunityUuid: string,
|
||||||
|
@Arg('senderCommunityName') senderCommunityName: string,
|
||||||
|
@Arg('recipientCommunityUuid') recipientCommunityUuid: string,
|
||||||
|
@Arg('code') code: string,
|
||||||
|
@Arg('amount') amount: string,
|
||||||
|
@Arg('memo') memo: string,
|
||||||
|
@Arg('firstName', { nullable: true }) firstName?: string,
|
||||||
|
@Arg('alias', { nullable: true }) alias?: string,
|
||||||
|
@Arg('validUntil', { nullable: true }) validUntil?: string,
|
||||||
|
): Promise<string> {
|
||||||
|
logger.debug('TransactionLinkResolver.queryRedeemJwt... args=', {
|
||||||
|
gradidoId,
|
||||||
|
senderCommunityUuid,
|
||||||
|
senderCommunityName,
|
||||||
|
recipientCommunityUuid,
|
||||||
|
code,
|
||||||
|
amount,
|
||||||
|
memo,
|
||||||
|
firstName,
|
||||||
|
alias,
|
||||||
|
validUntil,
|
||||||
|
})
|
||||||
|
|
||||||
|
const redeemJwtPayloadType = new RedeemJwtPayloadType(
|
||||||
|
senderCommunityUuid,
|
||||||
|
gradidoId,
|
||||||
|
alias ?? firstName ?? '',
|
||||||
|
code,
|
||||||
|
amount,
|
||||||
|
memo,
|
||||||
|
validUntil ?? '',
|
||||||
|
)
|
||||||
|
// TODO:encode/sign the jwt normally with the private key of the sender/home community, but interims with uuid
|
||||||
|
const homeCom = await getHomeCommunity()
|
||||||
|
if (!homeCom.communityUuid) {
|
||||||
|
throw new LogError('Home community UUID is not set')
|
||||||
|
}
|
||||||
|
const redeemJwt = await encode(redeemJwtPayloadType, homeCom.communityUuid)
|
||||||
|
// TODO: encrypt the payload with the public key of the target community
|
||||||
|
return redeemJwt
|
||||||
|
}
|
||||||
|
|
||||||
|
@Authorized([RIGHTS.DISBURSE_TRANSACTION_LINK])
|
||||||
|
@Mutation(() => Boolean)
|
||||||
|
async disburseTransactionLink(
|
||||||
|
@Ctx() _context: Context,
|
||||||
|
@Arg('senderCommunityUuid') senderCommunityUuid: string,
|
||||||
|
@Arg('senderGradidoId') senderGradidoId: string,
|
||||||
|
@Arg('recipientCommunityUuid') recipientCommunityUuid: string,
|
||||||
|
@Arg('recipientCommunityName') recipientCommunityName: string,
|
||||||
|
@Arg('recipientGradidoId') recipientGradidoId: string,
|
||||||
|
@Arg('recipientFirstName') recipientFirstName: string,
|
||||||
|
@Arg('code') code: string,
|
||||||
|
@Arg('amount') amount: string,
|
||||||
|
@Arg('memo') memo: string,
|
||||||
|
@Arg('validUntil', { nullable: true }) validUntil?: string,
|
||||||
|
@Arg('recipientAlias', { nullable: true }) recipientAlias?: string,
|
||||||
|
): Promise<boolean> {
|
||||||
|
logger.debug('TransactionLinkResolver.disburseTransactionLink... args=', {
|
||||||
|
senderGradidoId,
|
||||||
|
senderCommunityUuid,
|
||||||
|
recipientCommunityUuid,
|
||||||
|
recipientCommunityName,
|
||||||
|
recipientGradidoId,
|
||||||
|
recipientFirstName,
|
||||||
|
code,
|
||||||
|
amount,
|
||||||
|
memo,
|
||||||
|
validUntil,
|
||||||
|
recipientAlias,
|
||||||
|
})
|
||||||
|
const disburseJwt = await this.createDisburseJwt(
|
||||||
|
senderCommunityUuid,
|
||||||
|
senderGradidoId,
|
||||||
|
recipientCommunityUuid,
|
||||||
|
recipientCommunityName,
|
||||||
|
recipientGradidoId,
|
||||||
|
recipientFirstName,
|
||||||
|
code,
|
||||||
|
amount,
|
||||||
|
memo,
|
||||||
|
validUntil ?? '',
|
||||||
|
recipientAlias ?? '',
|
||||||
|
)
|
||||||
|
try {
|
||||||
|
logger.debug('TransactionLinkResolver.disburseTransactionLink... disburseJwt=', disburseJwt)
|
||||||
|
// now send the disburseJwt to the sender community to invoke a x-community-tx to disbures the redeemLink
|
||||||
|
// await sendDisburseJwtToSenderCommunity(context, disburseJwt)
|
||||||
|
} catch (e) {
|
||||||
|
throw new LogError('Disburse JWT was not sent successfully', e)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
@Authorized([RIGHTS.LIST_TRANSACTION_LINKS])
|
@Authorized([RIGHTS.LIST_TRANSACTION_LINKS])
|
||||||
@Query(() => TransactionLinkResult)
|
@Query(() => TransactionLinkResult)
|
||||||
async listTransactionLinks(
|
async listTransactionLinks(
|
||||||
@ -400,4 +525,163 @@ export class TransactionLinkResolver {
|
|||||||
}
|
}
|
||||||
return transactionLinkList(paginated, filters, user)
|
return transactionLinkList(paginated, filters, user)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async queryRedeemJwtLink(code: string): Promise<RedeemJwtLink> {
|
||||||
|
logger.debug('TransactionLinkResolver.queryRedeemJwtLink... redeem jwt-token found')
|
||||||
|
// decode token first to get the senderCommunityUuid as input for verify token
|
||||||
|
const decodedPayload = decode(code)
|
||||||
|
logger.debug('TransactionLinkResolver.queryRedeemJwtLink... decodedPayload=', decodedPayload)
|
||||||
|
if (
|
||||||
|
decodedPayload != null &&
|
||||||
|
decodedPayload.tokentype === RedeemJwtPayloadType.REDEEM_ACTIVATION_TYPE
|
||||||
|
) {
|
||||||
|
const redeemJwtPayload = new RedeemJwtPayloadType(
|
||||||
|
decodedPayload.sendercommunityuuid as string,
|
||||||
|
decodedPayload.sendergradidoid as string,
|
||||||
|
decodedPayload.sendername as string,
|
||||||
|
decodedPayload.redeemcode as string,
|
||||||
|
decodedPayload.amount as string,
|
||||||
|
decodedPayload.memo as string,
|
||||||
|
decodedPayload.validuntil as string,
|
||||||
|
)
|
||||||
|
logger.debug(
|
||||||
|
'TransactionLinkResolver.queryRedeemJwtLink... redeemJwtPayload=',
|
||||||
|
redeemJwtPayload,
|
||||||
|
)
|
||||||
|
const senderCom = await getCommunityByUuid(redeemJwtPayload.sendercommunityuuid)
|
||||||
|
if (!senderCom) {
|
||||||
|
throw new LogError('Sender community not found:', redeemJwtPayload.sendercommunityuuid)
|
||||||
|
}
|
||||||
|
logger.debug('TransactionLinkResolver.queryRedeemJwtLink... senderCom=', senderCom)
|
||||||
|
if (!senderCom.communityUuid) {
|
||||||
|
throw new LogError('Sender community UUID is not set')
|
||||||
|
}
|
||||||
|
// now with the sender community UUID the jwt token can be verified
|
||||||
|
const verifiedJwtPayload = await verify(code, senderCom.communityUuid)
|
||||||
|
logger.debug(
|
||||||
|
'TransactionLinkResolver.queryRedeemJwtLink... nach verify verifiedJwtPayload=',
|
||||||
|
verifiedJwtPayload,
|
||||||
|
)
|
||||||
|
let verifiedRedeemJwtPayload: RedeemJwtPayloadType | null = null
|
||||||
|
if (verifiedJwtPayload !== null) {
|
||||||
|
if (verifiedJwtPayload.exp !== undefined) {
|
||||||
|
const expDate = new Date(verifiedJwtPayload.exp * 1000)
|
||||||
|
logger.debug(
|
||||||
|
'TransactionLinkResolver.queryRedeemJwtLink... expDate, exp =',
|
||||||
|
expDate,
|
||||||
|
verifiedJwtPayload.exp,
|
||||||
|
)
|
||||||
|
if (expDate < new Date()) {
|
||||||
|
throw new LogError('Redeem JWT-Token expired! jwtPayload.exp=', expDate)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (verifiedJwtPayload.tokentype === RedeemJwtPayloadType.REDEEM_ACTIVATION_TYPE) {
|
||||||
|
logger.debug(
|
||||||
|
'TransactionLinkResolver.queryRedeemJwtLink... verifiedJwtPayload.tokentype=',
|
||||||
|
verifiedJwtPayload.tokentype,
|
||||||
|
)
|
||||||
|
verifiedRedeemJwtPayload = new RedeemJwtPayloadType(
|
||||||
|
verifiedJwtPayload.sendercommunityuuid as string,
|
||||||
|
verifiedJwtPayload.sendergradidoid as string,
|
||||||
|
verifiedJwtPayload.sendername as string,
|
||||||
|
verifiedJwtPayload.redeemcode as string,
|
||||||
|
verifiedJwtPayload.amount as string,
|
||||||
|
verifiedJwtPayload.memo as string,
|
||||||
|
verifiedJwtPayload.validuntil as string,
|
||||||
|
)
|
||||||
|
logger.debug(
|
||||||
|
'TransactionLinkResolver.queryRedeemJwtLink... nach verify verifiedRedeemJwtPayload=',
|
||||||
|
verifiedRedeemJwtPayload,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (verifiedRedeemJwtPayload === null) {
|
||||||
|
logger.debug(
|
||||||
|
'TransactionLinkResolver.queryRedeemJwtLink... verifiedRedeemJwtPayload===null',
|
||||||
|
)
|
||||||
|
verifiedRedeemJwtPayload = new RedeemJwtPayloadType(
|
||||||
|
decodedPayload.sendercommunityuuid as string,
|
||||||
|
decodedPayload.sendergradidoid as string,
|
||||||
|
decodedPayload.sendername as string,
|
||||||
|
decodedPayload.redeemcode as string,
|
||||||
|
decodedPayload.amount as string,
|
||||||
|
decodedPayload.memo as string,
|
||||||
|
decodedPayload.validuntil as string,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
// TODO: as long as the verification fails, fallback to simply decoded payload
|
||||||
|
verifiedRedeemJwtPayload = redeemJwtPayload
|
||||||
|
logger.debug(
|
||||||
|
'TransactionLinkResolver.queryRedeemJwtLink... fallback to decode verifiedRedeemJwtPayload=',
|
||||||
|
verifiedRedeemJwtPayload,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
const homeCommunity = await getHomeCommunity()
|
||||||
|
const recipientCommunity = new Community(homeCommunity)
|
||||||
|
const senderCommunity = new Community(senderCom)
|
||||||
|
const senderUser = new User(null)
|
||||||
|
senderUser.gradidoID = verifiedRedeemJwtPayload.sendergradidoid
|
||||||
|
senderUser.firstName = verifiedRedeemJwtPayload.sendername
|
||||||
|
const redeemJwtLink = new RedeemJwtLink(
|
||||||
|
verifiedRedeemJwtPayload,
|
||||||
|
senderCommunity,
|
||||||
|
senderUser,
|
||||||
|
recipientCommunity,
|
||||||
|
)
|
||||||
|
logger.debug('TransactionLinkResolver.queryRedeemJwtLink... redeemJwtLink=', redeemJwtLink)
|
||||||
|
return redeemJwtLink
|
||||||
|
} else {
|
||||||
|
throw new LogError(
|
||||||
|
'Redeem with wrong type of JWT-Token or expired! decodedPayload=',
|
||||||
|
decodedPayload,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async createDisburseJwt(
|
||||||
|
senderCommunityUuid: string,
|
||||||
|
senderGradidoId: string,
|
||||||
|
recipientCommunityUuid: string,
|
||||||
|
recipientCommunityName: string,
|
||||||
|
recipientGradidoId: string,
|
||||||
|
recipientFirstName: string,
|
||||||
|
code: string,
|
||||||
|
amount: string,
|
||||||
|
memo: string,
|
||||||
|
validUntil: string,
|
||||||
|
recipientAlias: string,
|
||||||
|
): Promise<string> {
|
||||||
|
logger.debug('TransactionLinkResolver.createDisburseJwt... args=', {
|
||||||
|
senderCommunityUuid,
|
||||||
|
senderGradidoId,
|
||||||
|
recipientCommunityUuid,
|
||||||
|
recipientCommunityName,
|
||||||
|
recipientGradidoId,
|
||||||
|
recipientFirstName,
|
||||||
|
code,
|
||||||
|
amount,
|
||||||
|
memo,
|
||||||
|
validUntil,
|
||||||
|
recipientAlias,
|
||||||
|
})
|
||||||
|
|
||||||
|
const disburseJwtPayloadType = new DisburseJwtPayloadType(
|
||||||
|
senderCommunityUuid,
|
||||||
|
senderGradidoId,
|
||||||
|
recipientCommunityUuid,
|
||||||
|
recipientCommunityName,
|
||||||
|
recipientGradidoId,
|
||||||
|
recipientFirstName,
|
||||||
|
code,
|
||||||
|
amount,
|
||||||
|
memo,
|
||||||
|
validUntil,
|
||||||
|
recipientAlias,
|
||||||
|
)
|
||||||
|
// TODO:encode/sign the jwt normally with the private key of the recipient community, but interims with uuid
|
||||||
|
const disburseJwt = await encode(disburseJwtPayloadType, recipientCommunityUuid)
|
||||||
|
logger.debug('TransactionLinkResolver.createDisburseJwt... disburseJwt=', disburseJwt)
|
||||||
|
// TODO: encrypt the payload with the public key of the target/sender community
|
||||||
|
return disburseJwt
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { Community as DbCommunity, FederatedCommunity as DbFederatedCommunity } from 'database'
|
import { Community as DbCommunity, FederatedCommunity as DbFederatedCommunity } from 'database'
|
||||||
import { FindOneOptions } from 'typeorm'
|
import { FindOneOptions, IsNull, Not } from 'typeorm'
|
||||||
|
|
||||||
import { Paginated } from '@arg/Paginated'
|
import { Paginated } from '@arg/Paginated'
|
||||||
|
|
||||||
@ -86,6 +86,16 @@ export async function getCommunityByUuid(communityUuid: string): Promise<DbCommu
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getAuthenticatedCommunities(): Promise<DbCommunity[]> {
|
||||||
|
const dbCommunities: DbCommunity[] = await DbCommunity.find({
|
||||||
|
where: { communityUuid: Not(IsNull()) }, //, authenticatedAt: Not(IsNull()) },
|
||||||
|
order: {
|
||||||
|
name: 'ASC',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
return dbCommunities
|
||||||
|
}
|
||||||
|
|
||||||
export async function getCommunityByIdentifier(
|
export async function getCommunityByIdentifier(
|
||||||
communityIdentifier: string,
|
communityIdentifier: string,
|
||||||
): Promise<DbCommunity | null> {
|
): Promise<DbCommunity | null> {
|
||||||
|
|||||||
@ -4,7 +4,9 @@ import { HumHubClient } from '@/apis/humhub/HumHubClient'
|
|||||||
import { GetUser } from '@/apis/humhub/model/GetUser'
|
import { GetUser } from '@/apis/humhub/model/GetUser'
|
||||||
import { PostUser } from '@/apis/humhub/model/PostUser'
|
import { PostUser } from '@/apis/humhub/model/PostUser'
|
||||||
import { ExecutedHumhubAction, syncUser } from '@/apis/humhub/syncUser'
|
import { ExecutedHumhubAction, syncUser } from '@/apis/humhub/syncUser'
|
||||||
|
import { PublishNameLogic } from '@/data/PublishName.logic'
|
||||||
import { UpdateUserInfosArgs } from '@/graphql/arg/UpdateUserInfosArgs'
|
import { UpdateUserInfosArgs } from '@/graphql/arg/UpdateUserInfosArgs'
|
||||||
|
import { PublishNameType } from '@/graphql/enum/PublishNameType'
|
||||||
import { backendLogger as logger } from '@/server/logger'
|
import { backendLogger as logger } from '@/server/logger'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -44,7 +46,9 @@ export async function syncHumhub(
|
|||||||
}
|
}
|
||||||
const humhubUsers = new Map<string, GetUser>()
|
const humhubUsers = new Map<string, GetUser>()
|
||||||
if (humhubUser) {
|
if (humhubUser) {
|
||||||
humhubUsers.set(humhubUser.account.username, humhubUser)
|
const publishNameLogic = new PublishNameLogic(user)
|
||||||
|
const username = publishNameLogic.getUserIdentifier(user.humhubPublishName as PublishNameType)
|
||||||
|
humhubUsers.set(username, humhubUser)
|
||||||
}
|
}
|
||||||
logger.debug('update user at humhub')
|
logger.debug('update user at humhub')
|
||||||
const result = await syncUser(user, humhubUsers)
|
const result = await syncUser(user, humhubUsers)
|
||||||
|
|||||||
@ -1,9 +1,22 @@
|
|||||||
import { createUnionType } from 'type-graphql'
|
import { createUnionType } from 'type-graphql'
|
||||||
|
|
||||||
import { ContributionLink } from '@model/ContributionLink'
|
import { ContributionLink } from '@model/ContributionLink'
|
||||||
|
import { RedeemJwtLink } from '@model/RedeemJwtLink'
|
||||||
import { TransactionLink } from '@model/TransactionLink'
|
import { TransactionLink } from '@model/TransactionLink'
|
||||||
|
|
||||||
export const QueryLinkResult = createUnionType({
|
export const QueryLinkResult = createUnionType({
|
||||||
name: 'QueryLinkResult', // the name of the GraphQL union
|
name: 'QueryLinkResult', // the name of the GraphQL union
|
||||||
types: () => [TransactionLink, ContributionLink] as const, // function that returns tuple of object types classes
|
types: () => [TransactionLink, RedeemJwtLink, ContributionLink] as const, // function that returns tuple of object types classes
|
||||||
|
resolveType: (value: TransactionLink | RedeemJwtLink | ContributionLink) => {
|
||||||
|
if (value instanceof TransactionLink) {
|
||||||
|
return TransactionLink
|
||||||
|
}
|
||||||
|
if (value instanceof RedeemJwtLink) {
|
||||||
|
return RedeemJwtLink
|
||||||
|
}
|
||||||
|
if (value instanceof ContributionLink) {
|
||||||
|
return ContributionLink
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@ -4,13 +4,11 @@ import { LogError } from './LogError'
|
|||||||
|
|
||||||
describe('LogError', () => {
|
describe('LogError', () => {
|
||||||
it('logs an Error when created', () => {
|
it('logs an Error when created', () => {
|
||||||
/* eslint-disable-next-line no-new */
|
|
||||||
new LogError('new LogError')
|
new LogError('new LogError')
|
||||||
expect(logger.error).toBeCalledWith('new LogError')
|
expect(logger.error).toBeCalledWith('new LogError')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('logs an Error including additional data when created', () => {
|
it('logs an Error including additional data when created', () => {
|
||||||
/* eslint-disable-next-line no-new */
|
|
||||||
new LogError('new LogError', { some: 'data' })
|
new LogError('new LogError', { some: 'data' })
|
||||||
expect(logger.error).toBeCalledWith('new LogError', { some: 'data' })
|
expect(logger.error).toBeCalledWith('new LogError', { some: 'data' })
|
||||||
})
|
})
|
||||||
@ -18,7 +16,6 @@ describe('LogError', () => {
|
|||||||
it('does not contain additional data in Error object when thrown', () => {
|
it('does not contain additional data in Error object when thrown', () => {
|
||||||
try {
|
try {
|
||||||
throw new LogError('new LogError', { someWeirdValue123: 'arbitraryData456' })
|
throw new LogError('new LogError', { someWeirdValue123: 'arbitraryData456' })
|
||||||
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
|
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
expect(e.stack).not.toMatch(/(someWeirdValue123|arbitraryData456)/i)
|
expect(e.stack).not.toMatch(/(someWeirdValue123|arbitraryData456)/i)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -36,9 +36,11 @@ export const createServer = async (
|
|||||||
|
|
||||||
// open mariadb connection, retry connecting with mariadb
|
// open mariadb connection, retry connecting with mariadb
|
||||||
// check for correct database version
|
// check for correct database version
|
||||||
// retry max 15 times, wait 500 ms between tries
|
// retry max CONFIG.DB_CONNECT_RETRY_COUNT times, wait CONFIG.DB_CONNECT_RETRY_DELAY ms between tries
|
||||||
// TODO: move variables into config
|
const con = await checkDBVersionUntil(
|
||||||
const con = await checkDBVersionUntil(15, 500)
|
CONFIG.DB_CONNECT_RETRY_COUNT,
|
||||||
|
CONFIG.DB_CONNECT_RETRY_DELAY_MS,
|
||||||
|
)
|
||||||
|
|
||||||
// Express Server
|
// Express Server
|
||||||
const app = express()
|
const app = express()
|
||||||
|
|||||||
@ -6,7 +6,7 @@ import { CONFIG } from '@/config'
|
|||||||
import { Connection } from '@/typeorm/connection'
|
import { Connection } from '@/typeorm/connection'
|
||||||
import { Connection as DbConnection } from 'typeorm'
|
import { Connection as DbConnection } from 'typeorm'
|
||||||
|
|
||||||
async function checkDBVersionUntil(maxRetries = 15, delayMs = 500): Promise<DbConnection> {
|
async function checkDBVersionUntil(maxRetries: number, delayMs: number): Promise<DbConnection> {
|
||||||
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
||||||
try {
|
try {
|
||||||
const connection = await Connection.getInstance()
|
const connection = await Connection.getInstance()
|
||||||
|
|||||||
@ -1,9 +1,3 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
|
||||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
|
||||||
/* eslint-disable @typescript-eslint/restrict-template-expressions */
|
|
||||||
/* eslint-disable @typescript-eslint/no-empty-interface */
|
|
||||||
/* eslint-disable @typescript-eslint/no-unsafe-argument */
|
|
||||||
|
|
||||||
import { Decimal } from 'decimal.js-light'
|
import { Decimal } from 'decimal.js-light'
|
||||||
|
|
||||||
expect.extend({
|
expect.extend({
|
||||||
@ -28,7 +22,6 @@ interface CustomMatchers<R = unknown> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
|
||||||
namespace jest {
|
namespace jest {
|
||||||
interface Expect extends CustomMatchers {}
|
interface Expect extends CustomMatchers {}
|
||||||
interface Matchers<R> extends CustomMatchers<R> {}
|
interface Matchers<R> extends CustomMatchers<R> {}
|
||||||
|
|||||||
@ -1,10 +1,3 @@
|
|||||||
/* eslint-disable @typescript-eslint/unbound-method */
|
|
||||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
|
||||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
|
||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
||||||
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
|
||||||
/* eslint-disable @typescript-eslint/no-unsafe-return */
|
|
||||||
|
|
||||||
import { createTestClient } from 'apollo-server-testing'
|
import { createTestClient } from 'apollo-server-testing'
|
||||||
import { entities } from 'database'
|
import { entities } from 'database'
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
// eslint-disable-next-line import/no-unassigned-import
|
|
||||||
import 'openai/shims/node'
|
import 'openai/shims/node'
|
||||||
import { CONFIG } from '@/config'
|
import { CONFIG } from '@/config'
|
||||||
import { i18n } from '@/server/localization'
|
import { i18n } from '@/server/localization'
|
||||||
|
|||||||
@ -37,6 +37,20 @@ export const DB_VERSION = Joi.string()
|
|||||||
)
|
)
|
||||||
.required()
|
.required()
|
||||||
|
|
||||||
|
export const DB_CONNECT_RETRY_COUNT = Joi.number()
|
||||||
|
.default(15)
|
||||||
|
.min(1)
|
||||||
|
.max(1000)
|
||||||
|
.description('Number of retries to connect to the database')
|
||||||
|
.optional()
|
||||||
|
|
||||||
|
export const DB_CONNECT_RETRY_DELAY_MS = Joi.number()
|
||||||
|
.default(500)
|
||||||
|
.min(100)
|
||||||
|
.max(10000)
|
||||||
|
.description('Delay in milliseconds between retries to connect to the database')
|
||||||
|
.optional()
|
||||||
|
|
||||||
export const COMMUNITY_URL = Joi.string()
|
export const COMMUNITY_URL = Joi.string()
|
||||||
.uri({ scheme: ['http', 'https'] })
|
.uri({ scheme: ['http', 'https'] })
|
||||||
.custom((value: string, helpers: Joi.CustomHelpers<string>) => {
|
.custom((value: string, helpers: Joi.CustomHelpers<string>) => {
|
||||||
|
|||||||
@ -17,12 +17,12 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsx ./esbuild.config.ts",
|
"build": "tsx ./esbuild.config.ts",
|
||||||
"typecheck": "tsc --noEmit",
|
"typecheck": "tsc --noEmit",
|
||||||
|
"lint": "biome check --error-on-warnings .",
|
||||||
|
"lint:fix": "biome check --error-on-warnings . --write",
|
||||||
|
"clear": "cross-env TZ=UTC tsx src/index.ts clear",
|
||||||
"up": "cross-env TZ=UTC tsx src/index.ts up",
|
"up": "cross-env TZ=UTC tsx src/index.ts up",
|
||||||
"down": "cross-env TZ=UTC tsx src/index.ts down",
|
"down": "cross-env TZ=UTC tsx src/index.ts down",
|
||||||
"reset": "cross-env TZ=UTC tsx src/index.ts reset",
|
"reset": "cross-env TZ=UTC tsx src/index.ts reset",
|
||||||
"clear": "cross-env TZ=UTC tsx src/index.ts clear",
|
|
||||||
"lint": "biome check --error-on-warnings .",
|
|
||||||
"lint:fix": "biome check --error-on-warnings . --write",
|
|
||||||
"up:backend_test": "cross-env TZ=UTC DB_DATABASE=gradido_test_backend tsx src/index.ts up",
|
"up:backend_test": "cross-env TZ=UTC DB_DATABASE=gradido_test_backend tsx src/index.ts up",
|
||||||
"up:federation_test": "cross-env TZ=UTC DB_DATABASE=gradido_test_federation tsx src/index.ts up",
|
"up:federation_test": "cross-env TZ=UTC DB_DATABASE=gradido_test_federation tsx src/index.ts up",
|
||||||
"up:dht_test": "cross-env TZ=UTC DB_DATABASE=gradido_test_dht tsx src/index.ts up"
|
"up:dht_test": "cross-env TZ=UTC DB_DATABASE=gradido_test_dht tsx src/index.ts up"
|
||||||
|
|||||||
@ -27,7 +27,10 @@ export async function truncateTables(connection: Connection) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function clearDatabase() {
|
export async function clearDatabase() {
|
||||||
const connection = await connectToDatabaseServer()
|
const connection = await connectToDatabaseServer(
|
||||||
|
CONFIG.DB_CONNECT_RETRY_COUNT,
|
||||||
|
CONFIG.DB_CONNECT_RETRY_DELAY_MS,
|
||||||
|
)
|
||||||
if (!connection) {
|
if (!connection) {
|
||||||
throw new Error('Could not connect to database server')
|
throw new Error('Could not connect to database server')
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,11 +11,17 @@ const constants = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const database = {
|
const database = {
|
||||||
DB_HOST: process.env.DB_HOST || 'localhost',
|
DB_CONNECT_RETRY_COUNT: process.env.DB_CONNECT_RETRY_COUNT
|
||||||
|
? Number.parseInt(process.env.DB_CONNECT_RETRY_COUNT)
|
||||||
|
: 15,
|
||||||
|
DB_CONNECT_RETRY_DELAY_MS: process.env.DB_CONNECT_RETRY_DELAY_MS
|
||||||
|
? Number.parseInt(process.env.DB_CONNECT_RETRY_DELAY_MS)
|
||||||
|
: 500,
|
||||||
|
DB_HOST: process.env.DB_HOST ?? 'localhost',
|
||||||
DB_PORT: process.env.DB_PORT ? Number.parseInt(process.env.DB_PORT) : 3306,
|
DB_PORT: process.env.DB_PORT ? Number.parseInt(process.env.DB_PORT) : 3306,
|
||||||
DB_USER: process.env.DB_USER || 'root',
|
DB_USER: process.env.DB_USER ?? 'root',
|
||||||
DB_PASSWORD: process.env.DB_PASSWORD || '',
|
DB_PASSWORD: process.env.DB_PASSWORD ?? '',
|
||||||
DB_DATABASE: process.env.DB_DATABASE || 'gradido_community',
|
DB_DATABASE: process.env.DB_DATABASE ?? 'gradido_community',
|
||||||
}
|
}
|
||||||
|
|
||||||
const migrations = {
|
const migrations = {
|
||||||
|
|||||||
@ -11,8 +11,8 @@ export enum DatabaseState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function connectToDatabaseServer(
|
export async function connectToDatabaseServer(
|
||||||
maxRetries = 15,
|
maxRetries: number,
|
||||||
delayMs = 500,
|
delayMs: number,
|
||||||
): Promise<Connection | null> {
|
): Promise<Connection | null> {
|
||||||
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
||||||
try {
|
try {
|
||||||
@ -42,7 +42,10 @@ async function convertJsToTsInMigrations(connection: Connection): Promise<number
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const getDatabaseState = async (): Promise<DatabaseState> => {
|
export const getDatabaseState = async (): Promise<DatabaseState> => {
|
||||||
const connection = await connectToDatabaseServer()
|
const connection = await connectToDatabaseServer(
|
||||||
|
CONFIG.DB_CONNECT_RETRY_COUNT,
|
||||||
|
CONFIG.DB_CONNECT_RETRY_DELAY_MS,
|
||||||
|
)
|
||||||
if (!connection) {
|
if (!connection) {
|
||||||
return DatabaseState.NOT_CONNECTED
|
return DatabaseState.NOT_CONNECTED
|
||||||
}
|
}
|
||||||
|
|||||||
@ -14,13 +14,16 @@
|
|||||||
"cache": false
|
"cache": false
|
||||||
},
|
},
|
||||||
"up": {
|
"up": {
|
||||||
"cache": false
|
"cache": false,
|
||||||
|
"dependsOn": ["build"]
|
||||||
},
|
},
|
||||||
"down": {
|
"down": {
|
||||||
"cache": false
|
"cache": false,
|
||||||
|
"dependsOn": ["build"]
|
||||||
},
|
},
|
||||||
"reset": {
|
"reset": {
|
||||||
"cache": false
|
"cache": false,
|
||||||
|
"dependsOn": ["build"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -22,18 +22,11 @@ done
|
|||||||
|
|
||||||
# set $1, $2, ... only with position arguments
|
# set $1, $2, ... only with position arguments
|
||||||
set -- "${POSITIONAL_ARGS[@]}"
|
set -- "${POSITIONAL_ARGS[@]}"
|
||||||
|
|
||||||
# check for missing branch name
|
|
||||||
if [ -z "$1" ]; then
|
|
||||||
echo "Usage: $0 [--fast] <branchName>"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
BRANCH_NAME="$1"
|
BRANCH_NAME="$1"
|
||||||
|
|
||||||
# Debug-Ausgabe
|
# check for parameter
|
||||||
if [ -z "$1" ]; then
|
if [ -z "$BRANCH_NAME" ]; then
|
||||||
echo "Usage: Please provide a branch name as the first argument."
|
echo "Usage: $0 [--fast] <branchName> [--fast]"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
echo "Use branch: $BRANCH_NAME"
|
echo "Use branch: $BRANCH_NAME"
|
||||||
@ -51,9 +44,17 @@ PROJECT_ROOT=$SCRIPT_DIR/../..
|
|||||||
NGINX_CONFIG_DIR=$SCRIPT_DIR/nginx/sites-available
|
NGINX_CONFIG_DIR=$SCRIPT_DIR/nginx/sites-available
|
||||||
set +o allexport
|
set +o allexport
|
||||||
|
|
||||||
# enable nvm
|
# enable nvm
|
||||||
export NVM_DIR="$HOME/.nvm" && [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
|
export NVM_DIR="$HOME/.nvm" && [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
|
||||||
nvm use
|
install_nvm() {
|
||||||
|
nvm install
|
||||||
|
nvm use
|
||||||
|
nvm alias default
|
||||||
|
npm i -g yarn pm2
|
||||||
|
pm2 startup
|
||||||
|
}
|
||||||
|
# make sure correct node version is installed
|
||||||
|
nvm use || install_nvm
|
||||||
|
|
||||||
# NOTE: all config values will be in process.env when starting
|
# NOTE: all config values will be in process.env when starting
|
||||||
# the services and will therefore take precedence over the .env
|
# the services and will therefore take precedence over the .env
|
||||||
@ -170,14 +171,12 @@ else
|
|||||||
log_warn "PM2 is already empty"
|
log_warn "PM2 is already empty"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|
||||||
# git
|
# git
|
||||||
BRANCH=$1
|
log_step "Starting with git pull - branch:$BRANCH_NAME"
|
||||||
log_step "Starting with git pull - branch:$BRANCH"
|
|
||||||
cd $PROJECT_ROOT
|
cd $PROJECT_ROOT
|
||||||
# TODO: this overfetches alot, but ensures we can use start.sh with tags
|
# TODO: this overfetches alot, but ensures we can use start.sh with tags
|
||||||
git fetch --all
|
git fetch --all
|
||||||
git checkout $BRANCH
|
git checkout $BRANCH_NAME
|
||||||
git pull
|
git pull
|
||||||
export BUILD_COMMIT="$(git rev-parse HEAD)"
|
export BUILD_COMMIT="$(git rev-parse HEAD)"
|
||||||
|
|
||||||
@ -289,7 +288,6 @@ else
|
|||||||
turbo up --env-mode=loose
|
turbo up --env-mode=loose
|
||||||
fi
|
fi
|
||||||
|
|
||||||
nvm use default
|
|
||||||
# start after building all to use up less ressources
|
# start after building all to use up less ressources
|
||||||
pm2 start --name gradido-backend "turbo backend#start --env-mode=loose" -l $GRADIDO_LOG_PATH/pm2.backend.$TODAY.log --log-date-format 'YYYY-MM-DD HH:mm:ss.SSS'
|
pm2 start --name gradido-backend "turbo backend#start --env-mode=loose" -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-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'
|
||||||
@ -335,4 +333,4 @@ cat $UPDATE_HTML >> $GRADIDO_LOG_PATH/update.$TODAY.log
|
|||||||
|
|
||||||
log_success " /\\_/\\ "
|
log_success " /\\_/\\ "
|
||||||
log_success "( ^.^ ) Update finished successfully!"
|
log_success "( ^.^ ) Update finished successfully!"
|
||||||
log_success " > <"
|
log_success " > <"
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
/** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */
|
/** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */
|
||||||
// eslint-disable-next-line import/no-commonjs, import/unambiguous
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
verbose: true,
|
verbose: true,
|
||||||
preset: 'ts-jest',
|
preset: 'ts-jest',
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
/* eslint-disable n/no-process-env */
|
|
||||||
import { validate } from 'config-schema'
|
import { validate } from 'config-schema'
|
||||||
import { latestDbVersion } from 'database'
|
import { latestDbVersion } from 'database'
|
||||||
import dotenv from 'dotenv'
|
import dotenv from 'dotenv'
|
||||||
@ -19,6 +18,12 @@ const server = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const database = {
|
const database = {
|
||||||
|
DB_CONNECT_RETRY_COUNT: process.env.DB_CONNECT_RETRY_COUNT
|
||||||
|
? Number.parseInt(process.env.DB_CONNECT_RETRY_COUNT)
|
||||||
|
: 15,
|
||||||
|
DB_CONNECT_RETRY_DELAY_MS: process.env.DB_CONNECT_RETRY_DELAY_MS
|
||||||
|
? Number.parseInt(process.env.DB_CONNECT_RETRY_DELAY_MS)
|
||||||
|
: 500,
|
||||||
DB_HOST: process.env.DB_HOST ?? 'localhost',
|
DB_HOST: process.env.DB_HOST ?? 'localhost',
|
||||||
DB_PORT: process.env.DB_PORT ? Number.parseInt(process.env.DB_PORT) : 3306,
|
DB_PORT: process.env.DB_PORT ? Number.parseInt(process.env.DB_PORT) : 3306,
|
||||||
DB_USER: process.env.DB_USER ?? 'root',
|
DB_USER: process.env.DB_USER ?? 'root',
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
import {
|
import {
|
||||||
COMMUNITY_DESCRIPTION,
|
COMMUNITY_DESCRIPTION,
|
||||||
COMMUNITY_NAME,
|
COMMUNITY_NAME,
|
||||||
|
DB_CONNECT_RETRY_COUNT,
|
||||||
|
DB_CONNECT_RETRY_DELAY_MS,
|
||||||
DB_DATABASE,
|
DB_DATABASE,
|
||||||
DB_HOST,
|
DB_HOST,
|
||||||
DB_PASSWORD,
|
DB_PASSWORD,
|
||||||
@ -19,6 +21,8 @@ export const schema = Joi.object({
|
|||||||
COMMUNITY_NAME,
|
COMMUNITY_NAME,
|
||||||
COMMUNITY_DESCRIPTION,
|
COMMUNITY_DESCRIPTION,
|
||||||
DB_DATABASE,
|
DB_DATABASE,
|
||||||
|
DB_CONNECT_RETRY_COUNT,
|
||||||
|
DB_CONNECT_RETRY_DELAY_MS,
|
||||||
DB_HOST,
|
DB_HOST,
|
||||||
DB_PASSWORD,
|
DB_PASSWORD,
|
||||||
DB_PORT,
|
DB_PORT,
|
||||||
|
|||||||
@ -1,6 +1,3 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
||||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
|
||||||
|
|
||||||
import DHT from '@hyperswarm/dht'
|
import DHT from '@hyperswarm/dht'
|
||||||
import { Community as DbCommunity, FederatedCommunity as DbFederatedCommunity } from 'database'
|
import { Community as DbCommunity, FederatedCommunity as DbFederatedCommunity } from 'database'
|
||||||
import { validate as validateUUID, version as versionUUID } from 'uuid'
|
import { validate as validateUUID, version as versionUUID } from 'uuid'
|
||||||
|
|||||||
@ -1,5 +1,3 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
||||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
|
||||||
import DHT from '@hyperswarm/dht'
|
import DHT from '@hyperswarm/dht'
|
||||||
import {
|
import {
|
||||||
CommunityLoggingView,
|
CommunityLoggingView,
|
||||||
|
|||||||
@ -6,7 +6,7 @@ import { checkDBVersionUntil } from './typeorm/DBVersion'
|
|||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
// open mysql connection
|
// open mysql connection
|
||||||
await checkDBVersionUntil()
|
await checkDBVersionUntil(CONFIG.DB_CONNECT_RETRY_COUNT, CONFIG.DB_CONNECT_RETRY_DELAY_MS)
|
||||||
logger.debug(`dhtseed set by CONFIG.FEDERATION_DHT_SEED=${CONFIG.FEDERATION_DHT_SEED}`)
|
logger.debug(`dhtseed set by CONFIG.FEDERATION_DHT_SEED=${CONFIG.FEDERATION_DHT_SEED}`)
|
||||||
logger.info(
|
logger.info(
|
||||||
`starting Federation on ${CONFIG.FEDERATION_DHT_TOPIC} ${
|
`starting Federation on ${CONFIG.FEDERATION_DHT_TOPIC} ${
|
||||||
|
|||||||
@ -6,7 +6,7 @@ import { CONFIG } from '@/config'
|
|||||||
import { Connection as DbConnection } from 'typeorm'
|
import { Connection as DbConnection } from 'typeorm'
|
||||||
import { connection as connectionFunc } from './connection'
|
import { connection as connectionFunc } from './connection'
|
||||||
|
|
||||||
async function checkDBVersionUntil(maxRetries = 15, delayMs = 500): Promise<DbConnection> {
|
async function checkDBVersionUntil(maxRetries: number, delayMs: number): Promise<DbConnection> {
|
||||||
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
||||||
try {
|
try {
|
||||||
const connection = await connectionFunc()
|
const connection = await connectionFunc()
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import { entities } from 'database'
|
import { entities } from 'database'
|
||||||
|
|
||||||
import { checkDBVersionUntil } from '@/typeorm/DBVersion'
|
import { checkDBVersionUntil } from '@/typeorm/DBVersion'
|
||||||
|
import { CONFIG } from '@/config'
|
||||||
|
|
||||||
export const headerPushMock = jest.fn((t) => {
|
export const headerPushMock = jest.fn((t) => {
|
||||||
context.token = t.value
|
context.token = t.value
|
||||||
@ -23,7 +24,7 @@ export const cleanDB = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const testEnvironment = async () => {
|
export const testEnvironment = async () => {
|
||||||
return { con: await checkDBVersionUntil() }
|
return { con: await checkDBVersionUntil(CONFIG.DB_CONNECT_RETRY_COUNT, CONFIG.DB_CONNECT_RETRY_DELAY_MS) }
|
||||||
}
|
}
|
||||||
|
|
||||||
export const resetEntity = async (entity: any) => {
|
export const resetEntity = async (entity: any) => {
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 1.1 MiB |
@ -25,7 +25,6 @@ module.exports = {
|
|||||||
? '<rootDir>/../database/entity/$1'
|
? '<rootDir>/../database/entity/$1'
|
||||||
: '<rootDir>/../database/build/entity/$1',
|
: '<rootDir>/../database/build/entity/$1',
|
||||||
'@logging/(.*)':
|
'@logging/(.*)':
|
||||||
// eslint-disable-next-line n/no-process-env
|
|
||||||
process.env.NODE_ENV === 'development'
|
process.env.NODE_ENV === 'development'
|
||||||
? '<rootDir>/../database/logging/$1'
|
? '<rootDir>/../database/logging/$1'
|
||||||
: '<rootDir>/../database/build/logging/$1',
|
: '<rootDir>/../database/build/logging/$1',
|
||||||
@ -34,7 +33,6 @@ module.exports = {
|
|||||||
? '<rootDir>/../database/src/$1'
|
? '<rootDir>/../database/src/$1'
|
||||||
: '<rootDir>/../database/build/src/$1',
|
: '<rootDir>/../database/build/src/$1',
|
||||||
'@config/(.*)':
|
'@config/(.*)':
|
||||||
// eslint-disable-next-line n/no-process-env
|
|
||||||
process.env.NODE_ENV === 'development'
|
process.env.NODE_ENV === 'development'
|
||||||
? '<rootDir>/../config/src/$1'
|
? '<rootDir>/../config/src/$1'
|
||||||
: '<rootDir>/../config/build/$1',
|
: '<rootDir>/../config/build/$1',
|
||||||
|
|||||||
@ -30,6 +30,12 @@ const server = {
|
|||||||
PRODUCTION: process.env.NODE_ENV === 'production',
|
PRODUCTION: process.env.NODE_ENV === 'production',
|
||||||
}
|
}
|
||||||
const database = {
|
const database = {
|
||||||
|
DB_CONNECT_RETRY_COUNT: process.env.DB_CONNECT_RETRY_COUNT
|
||||||
|
? Number.parseInt(process.env.DB_CONNECT_RETRY_COUNT)
|
||||||
|
: 15,
|
||||||
|
DB_CONNECT_RETRY_DELAY_MS: process.env.DB_CONNECT_RETRY_DELAY_MS
|
||||||
|
? Number.parseInt(process.env.DB_CONNECT_RETRY_DELAY_MS)
|
||||||
|
: 500,
|
||||||
DB_HOST: process.env.DB_HOST ?? 'localhost',
|
DB_HOST: process.env.DB_HOST ?? 'localhost',
|
||||||
DB_PORT: process.env.DB_PORT ? Number.parseInt(process.env.DB_PORT) : 3306,
|
DB_PORT: process.env.DB_PORT ? Number.parseInt(process.env.DB_PORT) : 3306,
|
||||||
DB_USER: process.env.DB_USER ?? 'root',
|
DB_USER: process.env.DB_USER ?? 'root',
|
||||||
|
|||||||
@ -1,4 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
|
DB_CONNECT_RETRY_COUNT,
|
||||||
|
DB_CONNECT_RETRY_DELAY_MS,
|
||||||
DB_DATABASE,
|
DB_DATABASE,
|
||||||
DB_HOST,
|
DB_HOST,
|
||||||
DB_PASSWORD,
|
DB_PASSWORD,
|
||||||
@ -17,6 +19,8 @@ import Joi from 'joi'
|
|||||||
|
|
||||||
export const schema = Joi.object({
|
export const schema = Joi.object({
|
||||||
DB_DATABASE,
|
DB_DATABASE,
|
||||||
|
DB_CONNECT_RETRY_COUNT,
|
||||||
|
DB_CONNECT_RETRY_DELAY_MS,
|
||||||
DB_HOST,
|
DB_HOST,
|
||||||
DB_PASSWORD,
|
DB_PASSWORD,
|
||||||
DB_PORT,
|
DB_PORT,
|
||||||
|
|||||||
@ -17,6 +17,7 @@ import { schema } from '@/graphql/schema'
|
|||||||
// import { elopageWebhook } from '@/webhook/elopage'
|
// import { elopageWebhook } from '@/webhook/elopage'
|
||||||
import { Connection } from 'typeorm'
|
import { Connection } from 'typeorm'
|
||||||
|
|
||||||
|
import { CONFIG } from '@/config'
|
||||||
import { slowDown } from 'express-slow-down'
|
import { slowDown } from 'express-slow-down'
|
||||||
import helmet from 'helmet'
|
import helmet from 'helmet'
|
||||||
import { Logger } from 'log4js'
|
import { Logger } from 'log4js'
|
||||||
@ -39,7 +40,10 @@ export const createServer = async (
|
|||||||
logger.debug('createServer...')
|
logger.debug('createServer...')
|
||||||
|
|
||||||
// open mysql connection
|
// open mysql connection
|
||||||
const con = await checkDBVersionUntil()
|
const con = await checkDBVersionUntil(
|
||||||
|
CONFIG.DB_CONNECT_RETRY_COUNT,
|
||||||
|
CONFIG.DB_CONNECT_RETRY_DELAY_MS,
|
||||||
|
)
|
||||||
|
|
||||||
// Express Server
|
// Express Server
|
||||||
const app = express()
|
const app = express()
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import { Migration } from 'database'
|
|||||||
import { Connection as DbConnection } from 'typeorm'
|
import { Connection as DbConnection } from 'typeorm'
|
||||||
import { connection as connectionFunc } from './connection'
|
import { connection as connectionFunc } from './connection'
|
||||||
|
|
||||||
async function checkDBVersionUntil(maxRetries = 15, delayMs = 500): Promise<DbConnection> {
|
async function checkDBVersionUntil(maxRetries: number, delayMs: number): Promise<DbConnection> {
|
||||||
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
||||||
try {
|
try {
|
||||||
const connection = await connectionFunc()
|
const connection = await connectionFunc()
|
||||||
|
|||||||
@ -46,6 +46,7 @@ const validCommunityIdentifier = ref(false)
|
|||||||
const { onResult } = useQuery(selectCommunities)
|
const { onResult } = useQuery(selectCommunities)
|
||||||
|
|
||||||
onResult(({ data }) => {
|
onResult(({ data }) => {
|
||||||
|
// console.log('CommunitySwitch.onResult...data=', data)
|
||||||
if (data) {
|
if (data) {
|
||||||
communities.value = data.communities
|
communities.value = data.communities
|
||||||
setDefaultCommunity()
|
setDefaultCommunity()
|
||||||
@ -55,22 +56,42 @@ onResult(({ data }) => {
|
|||||||
const communityIdentifier = computed(() => route.params.communityIdentifier)
|
const communityIdentifier = computed(() => route.params.communityIdentifier)
|
||||||
|
|
||||||
function updateCommunity(community) {
|
function updateCommunity(community) {
|
||||||
|
// console.log('CommunitySwitch.updateCommunity...community=', community)
|
||||||
emit('update:model-value', community)
|
emit('update:model-value', community)
|
||||||
}
|
}
|
||||||
|
|
||||||
function setDefaultCommunity() {
|
function setDefaultCommunity() {
|
||||||
|
// console.log(
|
||||||
|
// 'CommunitySwitch.setDefaultCommunity... communityIdentifier= communities=',
|
||||||
|
// communityIdentifier,
|
||||||
|
// communities,
|
||||||
|
// )
|
||||||
if (communityIdentifier.value && communities.value.length >= 1) {
|
if (communityIdentifier.value && communities.value.length >= 1) {
|
||||||
|
// console.log(
|
||||||
|
// 'CommunitySwitch.setDefaultCommunity... communities.value.length=',
|
||||||
|
// communities.value.length,
|
||||||
|
// )
|
||||||
const foundCommunity = communities.value.find((community) => {
|
const foundCommunity = communities.value.find((community) => {
|
||||||
|
// console.log('CommunitySwitch.setDefaultCommunity... community=', community)
|
||||||
if (
|
if (
|
||||||
community.uuid === communityIdentifier.value ||
|
community.uuid === communityIdentifier.value ||
|
||||||
community.name === communityIdentifier.value
|
community.name === communityIdentifier.value
|
||||||
) {
|
) {
|
||||||
validCommunityIdentifier.value = true
|
validCommunityIdentifier.value = true
|
||||||
|
// console.log(
|
||||||
|
// 'CommunitySwitch.setDefaultCommunity...true validCommunityIdentifier=',
|
||||||
|
// validCommunityIdentifier,
|
||||||
|
// )
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
// console.log(
|
||||||
|
// 'CommunitySwitch.setDefaultCommunity...false validCommunityIdentifier=',
|
||||||
|
// validCommunityIdentifier,
|
||||||
|
// )
|
||||||
return false
|
return false
|
||||||
})
|
})
|
||||||
if (foundCommunity) {
|
if (foundCommunity) {
|
||||||
|
// console.log('CommunitySwitch.setDefaultCommunity...foundCommunity=', foundCommunity)
|
||||||
updateCommunity(foundCommunity)
|
updateCommunity(foundCommunity)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -79,10 +100,20 @@ function setDefaultCommunity() {
|
|||||||
|
|
||||||
if (validCommunityIdentifier.value && !communityIdentifier.value) {
|
if (validCommunityIdentifier.value && !communityIdentifier.value) {
|
||||||
validCommunityIdentifier.value = false
|
validCommunityIdentifier.value = false
|
||||||
|
// console.log(
|
||||||
|
// 'CommunitySwitch.setDefaultCommunity...validCommunityIdentifier=',
|
||||||
|
// validCommunityIdentifier,
|
||||||
|
// )
|
||||||
}
|
}
|
||||||
|
|
||||||
if (props.modelValue?.uuid === '' && communities.value.length) {
|
if (props.modelValue?.uuid === '' && communities.value.length) {
|
||||||
|
// console.log(
|
||||||
|
// 'CommunitySwitch.setDefaultCommunity...props.modelValue= communities=',
|
||||||
|
// props.modelValue,
|
||||||
|
// communities.value.length,
|
||||||
|
// )
|
||||||
const foundCommunity = communities.value.find((community) => !community.foreign)
|
const foundCommunity = communities.value.find((community) => !community.foreign)
|
||||||
|
// console.log('CommunitySwitch.setDefaultCommunity...foundCommunity=', foundCommunity)
|
||||||
if (foundCommunity) {
|
if (foundCommunity) {
|
||||||
updateCommunity(foundCommunity)
|
updateCommunity(foundCommunity)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,182 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
:link-data="linkData"
|
||||||
|
:redeem-code="redeemCode"
|
||||||
|
:is-contribution-link="isContributionLink"
|
||||||
|
:is-redeem-jwt-link="isRedeemJwtLink"
|
||||||
|
class="redeem-community-selection"
|
||||||
|
>
|
||||||
|
<BCard bg-variant="muted" text-variant="dark" border-variant="info">
|
||||||
|
<h1 v-if="linkData.amount === ''">{{ $t('gdd_per_link.redeemlink-error') }}</h1>
|
||||||
|
<h1 v-if="!isContributionLink && linkData.amount !== ''">
|
||||||
|
<BCol class="mb-4" cols="12">
|
||||||
|
<BRow>
|
||||||
|
<BCol v-if="!isRedeemJwtLink">
|
||||||
|
{{ $t('gdd_per_link.recipientCommunitySelection') }}
|
||||||
|
</BCol>
|
||||||
|
<BCol v-else>{{ $t('gdd_per_link.recipientCommunityFix') }}</BCol>
|
||||||
|
</BRow>
|
||||||
|
<h3>
|
||||||
|
<BRow>
|
||||||
|
<BCol v-if="!isRedeemJwtLink" class="fw-bold">
|
||||||
|
<community-switch
|
||||||
|
:disabled="isRedeemJwtLink"
|
||||||
|
:model-value="currentRecipientCommunity"
|
||||||
|
@update:model-value="setRecipientCommunity"
|
||||||
|
/>
|
||||||
|
</BCol>
|
||||||
|
<BCol v-else>
|
||||||
|
{{ currentRecipientCommunity.name }}
|
||||||
|
</BCol>
|
||||||
|
<BCol v-if="isForeignCommunitySelected" sm="12" md="6" class="mt-4 mt-lg-0">
|
||||||
|
<p>{{ $t('gdd_per_link.switchCommunity') }}</p>
|
||||||
|
<BButton variant="gradido" @click="onSwitch">
|
||||||
|
{{ $t('gdd_per_link.to-switch') }}
|
||||||
|
</BButton>
|
||||||
|
</BCol>
|
||||||
|
</BRow>
|
||||||
|
</h3>
|
||||||
|
</BCol>
|
||||||
|
<template v-if="linkData.senderUser">
|
||||||
|
{{ linkData.senderUser.firstName }}
|
||||||
|
{{ $t('transaction-link.send_you') }} {{ $filters.GDD(linkData.amount) }}
|
||||||
|
</template>
|
||||||
|
</h1>
|
||||||
|
<b>{{ linkData.memo }}</b>
|
||||||
|
</BCard>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script setup>
|
||||||
|
import CONFIG from '@/config'
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import { createRedeemJwtMutation } from '@/graphql/mutations'
|
||||||
|
import { useMutation } from '@vue/apollo-composable'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
linkData: { type: Object, required: true },
|
||||||
|
redeemCode: { type: String, required: true },
|
||||||
|
isContributionLink: { type: Boolean, default: false },
|
||||||
|
isRedeemJwtLink: { type: Boolean, default: false },
|
||||||
|
recipientCommunity: {
|
||||||
|
type: Object,
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const senderCommunity = computed(() => extractHomeCommunityFromLinkData(props.linkData))
|
||||||
|
const currentRecipientCommunity = computed(
|
||||||
|
() =>
|
||||||
|
props.recipientCommunity || {
|
||||||
|
uuid: senderCommunity.value.uuid,
|
||||||
|
name: senderCommunity.value.name,
|
||||||
|
url: senderCommunity.value.url,
|
||||||
|
foreign: senderCommunity.value.foreign,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
const emit = defineEmits(['update:recipientCommunity'])
|
||||||
|
|
||||||
|
const isForeignCommunitySelected = computed(() => {
|
||||||
|
// console.log(
|
||||||
|
// 'RedeemCommunitySelection.isForeignCommunitySelected...recipientCommunity=',
|
||||||
|
// currentRecipientCommunity.value,
|
||||||
|
// )
|
||||||
|
return currentRecipientCommunity.value.foreign
|
||||||
|
})
|
||||||
|
|
||||||
|
function setRecipientCommunity(community) {
|
||||||
|
// console.log('RedeemCommunitySelection.setRecipientCommunity...community=', community)
|
||||||
|
emit('update:recipientCommunity', {
|
||||||
|
uuid: community.uuid,
|
||||||
|
name: community.name,
|
||||||
|
url: community.url,
|
||||||
|
foreign: community.foreign,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractHomeCommunityFromLinkData(linkData) {
|
||||||
|
// console.log(
|
||||||
|
// 'RedeemCommunitySelection.extractHomeCommunityFromLinkData... props.linkData=',
|
||||||
|
// props.linkData,
|
||||||
|
// )
|
||||||
|
// console.log('RedeemCommunitySelection.extractHomeCommunityFromLinkData...linkData=', linkData)
|
||||||
|
// console.log(
|
||||||
|
// 'RedeemCommunitySelection.extractHomeCommunityFromLinkData...communities=',
|
||||||
|
// linkData.communities,
|
||||||
|
// )
|
||||||
|
// console.log(
|
||||||
|
// 'RedeemCommunitySelection.extractHomeCommunityFromLinkData...linkData.value=',
|
||||||
|
// linkData.value,
|
||||||
|
// )
|
||||||
|
|
||||||
|
if (linkData.communities?.length === 0) {
|
||||||
|
return {
|
||||||
|
uuid: '',
|
||||||
|
name: CONFIG.COMMUNITY_NAME,
|
||||||
|
url: CONFIG.COMMUNITY_URL,
|
||||||
|
foreign: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const communities = linkData.communities
|
||||||
|
// console.log(
|
||||||
|
// 'RedeemCommunitySelection.extractHomeCommunityFromLinkData...communities=',
|
||||||
|
// communities,
|
||||||
|
// )
|
||||||
|
const homeCommunity = communities?.find((c) => c.foreign === false)
|
||||||
|
// console.log(
|
||||||
|
// 'RedeemCommunitySelection.extractHomeCommunityFromLinkData...homeCommunity=',
|
||||||
|
// homeCommunity,
|
||||||
|
// )
|
||||||
|
return {
|
||||||
|
uuid: homeCommunity.uuid,
|
||||||
|
name: homeCommunity.name,
|
||||||
|
url: homeCommunity.url,
|
||||||
|
foreign: homeCommunity.foreign,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const { mutate: createRedeemJwt } = useMutation(createRedeemJwtMutation)
|
||||||
|
|
||||||
|
async function onSwitch(event) {
|
||||||
|
event.preventDefault() // Prevent the default navigation
|
||||||
|
// console.log('RedeemCommunitySelection.onSwitch... props=', props)
|
||||||
|
if (isForeignCommunitySelected.value) {
|
||||||
|
// console.log('RedeemCommunitySelection.onSwitch vor createRedeemJwt params:', {
|
||||||
|
// gradidoId: props.linkData.senderUser?.gradidoID,
|
||||||
|
// senderCommunityUuid: senderCommunity.value.uuid,
|
||||||
|
// senderCommunityName: senderCommunity.value.name,
|
||||||
|
// recipientCommunityUuid: currentRecipientCommunity.value.uuid,
|
||||||
|
// code: props.redeemCode,
|
||||||
|
// amount: props.linkData.amount,
|
||||||
|
// memo: props.linkData.memo,
|
||||||
|
// firstName: props.linkData.senderUser?.firstName,
|
||||||
|
// alias: props.linkData.senderUser?.alias,
|
||||||
|
// validUntil: props.linkData.validUntil,
|
||||||
|
// })
|
||||||
|
// eslint-disable-next-line no-useless-catch
|
||||||
|
try {
|
||||||
|
const { data } = await createRedeemJwt({
|
||||||
|
gradidoId: props.linkData.senderUser?.gradidoID,
|
||||||
|
senderCommunityUuid: senderCommunity.value.uuid,
|
||||||
|
senderCommunityName: senderCommunity.value.name,
|
||||||
|
recipientCommunityUuid: currentRecipientCommunity.value.uuid,
|
||||||
|
code: props.redeemCode,
|
||||||
|
amount: props.linkData.amount,
|
||||||
|
memo: props.linkData.memo,
|
||||||
|
firstName: props.linkData.senderUser?.firstName,
|
||||||
|
alias: props.linkData.senderUser?.alias,
|
||||||
|
validUntil: props.linkData.validUntil,
|
||||||
|
})
|
||||||
|
// console.log('RedeemCommunitySelection.onSwitch... response=', data)
|
||||||
|
if (!data?.createRedeemJwt) {
|
||||||
|
throw new Error('Failed to get redeem token')
|
||||||
|
}
|
||||||
|
const targetUrl = currentRecipientCommunity.value.url.replace(/\/api\/?$/, '')
|
||||||
|
window.location.href = targetUrl + '/redeem/' + data.createRedeemJwt
|
||||||
|
} catch (error) {
|
||||||
|
// console.error('RedeemCommunitySelection.onSwitch error:', error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@ -1,16 +1,20 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="redeem-information">
|
<div class="redeem-information">
|
||||||
<BCard bg-variant="muted" text-variant="dark" border-variant="info">
|
<BCard bg-variant="muted" text-variant="dark" border-variant="info">
|
||||||
<h1 v-if="amount === ''">{{ $t('gdd_per_link.redeemlink-error') }}</h1>
|
<h1 v-if="linkData.amount === ''">{{ $t('gdd_per_link.redeemlink-error') }}</h1>
|
||||||
<h1 v-if="isContributionLink && amount !== ''">
|
<h1 v-if="isContributionLink && linkData.amount !== ''">
|
||||||
{{ CONFIG.COMMUNITY_NAME }}
|
{{ CONFIG.COMMUNITY_NAME }}
|
||||||
{{ $t('contribution-link.thanksYouWith') }} {{ $filters.GDD(amount) }}
|
{{ $t('contribution-link.thanksYouWith') }} {{ $filters.GDD(linkData.amount) }}
|
||||||
</h1>
|
</h1>
|
||||||
<h1 v-if="!isContributionLink && amount !== ''">
|
<h3 v-if="isRedeemJwtLink && linkData.amount !== ''">
|
||||||
{{ user.firstName }}
|
{{ '"' + linkData.senderCommunity.name + '.' + linkData.senderUser.firstName + '"' }}
|
||||||
{{ $t('transaction-link.send_you') }} {{ $filters.GDD(amount) }}
|
{{ $t('transaction-link.send_you') }} {{ $filters.GDD(linkData.amount) }}
|
||||||
</h1>
|
</h3>
|
||||||
<b>{{ memo }}</b>
|
<h3 v-if="!isRedeemJwtLink && linkData.amount !== ''">
|
||||||
|
{{ '"' + linkData.senderUser.firstName + '"' }}
|
||||||
|
{{ $t('transaction-link.send_you') }} {{ $filters.GDD(linkData.amount) }}
|
||||||
|
</h3>
|
||||||
|
<b>{{ linkData.memo }}</b>
|
||||||
</BCard>
|
</BCard>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@ -20,10 +24,9 @@ import CONFIG from '@/config'
|
|||||||
export default {
|
export default {
|
||||||
name: 'RedeemInformation',
|
name: 'RedeemInformation',
|
||||||
props: {
|
props: {
|
||||||
user: { type: Object, required: false },
|
linkData: { type: Object, required: true },
|
||||||
amount: { type: String, required: true },
|
|
||||||
memo: { type: String, required: true, default: '' },
|
|
||||||
isContributionLink: { type: Boolean, default: false },
|
isContributionLink: { type: Boolean, default: false },
|
||||||
|
isRedeemJwtLink: { type: Boolean, default: false },
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@ -0,0 +1,59 @@
|
|||||||
|
<template>
|
||||||
|
<div class="redeem-select-community">
|
||||||
|
<redeem-community-selection
|
||||||
|
v-model:recipient-community="recipientCommunity"
|
||||||
|
:link-data="props.linkData"
|
||||||
|
:redeem-code="props.redeemCode"
|
||||||
|
:is-transaction-link-loaded="props.isTransactionLinkLoaded"
|
||||||
|
:is-contribution-link="props.isContributionLink"
|
||||||
|
:is-redeem-jwt-link="props.isRedeemJwtLink"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<BCard v-if="props.isTransactionLinkLoaded">
|
||||||
|
<div class="mb-2">
|
||||||
|
<h2>{{ $t('gdd_per_link.redeem') }}</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<BRow>
|
||||||
|
<BCol sm="12" md="6">
|
||||||
|
<p>{{ $t('gdd_per_link.no-account') }}</p>
|
||||||
|
<BButton variant="primary" :disabled="isForeignCommunitySelected" :to="register()">
|
||||||
|
{{ $t('gdd_per_link.to-register') }}
|
||||||
|
</BButton>
|
||||||
|
</BCol>
|
||||||
|
<BCol sm="12" md="6" class="mt-4 mt-lg-0">
|
||||||
|
<p>{{ $t('gdd_per_link.has-account') }}</p>
|
||||||
|
<BButton variant="gradido" :disabled="isForeignCommunitySelected" :to="login()">
|
||||||
|
{{ $t('gdd_per_link.to-login') }}
|
||||||
|
</BButton>
|
||||||
|
</BCol>
|
||||||
|
</BRow>
|
||||||
|
</BCard>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
import CONFIG from '@/config'
|
||||||
|
import { useAuthLinks } from '@/composables/useAuthLinks'
|
||||||
|
|
||||||
|
const { login, register } = useAuthLinks()
|
||||||
|
const props = defineProps({
|
||||||
|
linkData: { type: Object, required: true },
|
||||||
|
redeemCode: { type: String, required: true },
|
||||||
|
isContributionLink: { type: Boolean, default: false },
|
||||||
|
isRedeemJwtLink: { type: Boolean, default: false },
|
||||||
|
isTransactionLinkLoaded: { type: Boolean, default: false },
|
||||||
|
})
|
||||||
|
|
||||||
|
const recipientCommunity = ref({
|
||||||
|
uuid: '',
|
||||||
|
name: CONFIG.COMMUNITY_NAME,
|
||||||
|
url: CONFIG.COMMUNITY_URL,
|
||||||
|
foreign: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
const isForeignCommunitySelected = computed(() => {
|
||||||
|
return recipientCommunity.value.foreign === true
|
||||||
|
})
|
||||||
|
</script>
|
||||||
@ -1,6 +1,11 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="redeem-valid">
|
<div class="redeem-valid">
|
||||||
<redeem-information v-bind="linkData" :is-contribution-link="isContributionLink" />
|
<redeem-information
|
||||||
|
:link-data="linkData"
|
||||||
|
:is-contribution-link="isContributionLink"
|
||||||
|
:is-redeem-jwt-link="isRedeemJwtLink"
|
||||||
|
:valid-link="validLink"
|
||||||
|
/>
|
||||||
<BCard>
|
<BCard>
|
||||||
<div class="mb-3 text-center">
|
<div class="mb-3 text-center">
|
||||||
<BButton
|
<BButton
|
||||||
@ -26,6 +31,7 @@ export default {
|
|||||||
props: {
|
props: {
|
||||||
linkData: { type: Object, required: true },
|
linkData: { type: Object, required: true },
|
||||||
isContributionLink: { type: Boolean, default: false },
|
isContributionLink: { type: Boolean, default: false },
|
||||||
|
isRedeemJwtLink: { type: Boolean, default: false },
|
||||||
validLink: { type: Boolean, default: false },
|
validLink: { type: Boolean, default: false },
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@ -209,3 +209,61 @@ export const logout = gql`
|
|||||||
logout
|
logout
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
|
export const createRedeemJwtMutation = gql`
|
||||||
|
mutation (
|
||||||
|
$gradidoId: String!
|
||||||
|
$senderCommunityUuid: String!
|
||||||
|
$senderCommunityName: String!
|
||||||
|
$recipientCommunityUuid: String!
|
||||||
|
$code: String!
|
||||||
|
$amount: String!
|
||||||
|
$memo: String!
|
||||||
|
$firstName: String
|
||||||
|
$alias: String
|
||||||
|
$validUntil: String
|
||||||
|
) {
|
||||||
|
createRedeemJwt(
|
||||||
|
gradidoId: $gradidoId
|
||||||
|
senderCommunityUuid: $senderCommunityUuid
|
||||||
|
senderCommunityName: $senderCommunityName
|
||||||
|
recipientCommunityUuid: $recipientCommunityUuid
|
||||||
|
code: $code
|
||||||
|
amount: $amount
|
||||||
|
memo: $memo
|
||||||
|
firstName: $firstName
|
||||||
|
alias: $alias
|
||||||
|
validUntil: $validUntil
|
||||||
|
)
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
export const disburseTransactionLink = gql`
|
||||||
|
mutation (
|
||||||
|
$senderCommunityUuid: String!
|
||||||
|
$senderGradidoId: String!
|
||||||
|
$recipientCommunityUuid: String!
|
||||||
|
$recipientCommunityName: String!
|
||||||
|
$recipientGradidoId: String!
|
||||||
|
$recipientFirstName: String!
|
||||||
|
$code: String!
|
||||||
|
$amount: String!
|
||||||
|
$memo: String!
|
||||||
|
$validUntil: String
|
||||||
|
$recipientAlias: String
|
||||||
|
) {
|
||||||
|
disburseTransactionLink(
|
||||||
|
senderCommunityUuid: $senderCommunityUuid
|
||||||
|
senderGradidoId: $senderGradidoId
|
||||||
|
recipientCommunityUuid: $recipientCommunityUuid
|
||||||
|
recipientCommunityName: $recipientCommunityName
|
||||||
|
recipientGradidoId: $recipientGradidoId
|
||||||
|
recipientFirstName: $recipientFirstName
|
||||||
|
code: $code
|
||||||
|
amount: $amount
|
||||||
|
memo: $memo
|
||||||
|
validUntil: $validUntil
|
||||||
|
recipientAlias: $recipientAlias
|
||||||
|
)
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|||||||
@ -99,6 +99,7 @@ export const selectCommunities = gql`
|
|||||||
name
|
name
|
||||||
description
|
description
|
||||||
foreign
|
foreign
|
||||||
|
url
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
@ -126,7 +127,43 @@ export const queryTransactionLink = gql`
|
|||||||
validUntil
|
validUntil
|
||||||
redeemedAt
|
redeemedAt
|
||||||
deletedAt
|
deletedAt
|
||||||
user {
|
senderUser {
|
||||||
|
gradidoID
|
||||||
|
firstName
|
||||||
|
publisherId
|
||||||
|
}
|
||||||
|
communities {
|
||||||
|
foreign
|
||||||
|
name
|
||||||
|
description
|
||||||
|
url
|
||||||
|
uuid
|
||||||
|
}
|
||||||
|
}
|
||||||
|
... on RedeemJwtLink {
|
||||||
|
amount
|
||||||
|
memo
|
||||||
|
code
|
||||||
|
validUntil
|
||||||
|
senderCommunity {
|
||||||
|
foreign
|
||||||
|
name
|
||||||
|
description
|
||||||
|
url
|
||||||
|
uuid
|
||||||
|
}
|
||||||
|
senderUser {
|
||||||
|
gradidoID
|
||||||
|
firstName
|
||||||
|
}
|
||||||
|
recipientCommunity {
|
||||||
|
foreign
|
||||||
|
name
|
||||||
|
description
|
||||||
|
url
|
||||||
|
uuid
|
||||||
|
}
|
||||||
|
recipientUser {
|
||||||
gradidoID
|
gradidoID
|
||||||
firstName
|
firstName
|
||||||
publisherId
|
publisherId
|
||||||
|
|||||||
@ -230,6 +230,7 @@
|
|||||||
"credit-your-gradido": "Damit die Gradido gutgeschrieben werden können, klicke auf den Link!",
|
"credit-your-gradido": "Damit die Gradido gutgeschrieben werden können, klicke auf den Link!",
|
||||||
"delete-the-link": "Den Link löschen?",
|
"delete-the-link": "Den Link löschen?",
|
||||||
"deleted": "Der Link wurde gelöscht!",
|
"deleted": "Der Link wurde gelöscht!",
|
||||||
|
"disbured": "Auszahlung des Link-Guthabens erfolgreich initiiert! Die Gutschrift von {n} GDD wird zeitnah auf dein Konto gebucht",
|
||||||
"expiredOn": "Abgelaufen am",
|
"expiredOn": "Abgelaufen am",
|
||||||
"has-account": "Du besitzt bereits ein Gradido Konto?",
|
"has-account": "Du besitzt bereits ein Gradido Konto?",
|
||||||
"header": "Gradidos versenden per Link",
|
"header": "Gradidos versenden per Link",
|
||||||
@ -245,12 +246,16 @@
|
|||||||
"no-account": "Du besitzt noch kein Gradido Konto?",
|
"no-account": "Du besitzt noch kein Gradido Konto?",
|
||||||
"no-redeem": "Du darfst deinen eigenen Link nicht einlösen!",
|
"no-redeem": "Du darfst deinen eigenen Link nicht einlösen!",
|
||||||
"not-copied": "Dein Gerät lässt das Kopieren leider nicht zu! Bitte kopiere den Link von Hand!",
|
"not-copied": "Dein Gerät lässt das Kopieren leider nicht zu! Bitte kopiere den Link von Hand!",
|
||||||
|
"recipientCommunityFix": "Empfänger-Gemeinschaft des Link-Guthabens...",
|
||||||
|
"recipientCommunitySelection": "Wähle deine Gemeinschaft zum Einlösen des Link-Guthabens...",
|
||||||
"redeem": "Einlösen",
|
"redeem": "Einlösen",
|
||||||
"redeemed": "Erfolgreich eingelöst! Deinem Konto wurden {n} GDD gutgeschrieben.",
|
"redeemed": "Erfolgreich eingelöst! Deinem Konto wurden {n} GDD gutgeschrieben.",
|
||||||
"redeemed-at": "Der Link wurde bereits am {date} eingelöst.",
|
"redeemed-at": "Der Link wurde bereits am {date} eingelöst.",
|
||||||
"redeemlink-error": "Dieser Einlöse-Link ist nicht vollständig.",
|
"redeemlink-error": "Dieser Einlöse-Link ist nicht vollständig.",
|
||||||
|
"switchCommunity": "Du hast eine andere Gemeinschaft ausgewählt...",
|
||||||
"to-login": "Log dich ein",
|
"to-login": "Log dich ein",
|
||||||
"to-register": "Registriere ein neues Konto.",
|
"to-register": "Registriere ein neues Konto.",
|
||||||
|
"to-switch": "Wechsle zur Gemeinschaft",
|
||||||
"validUntil": "Gültig bis",
|
"validUntil": "Gültig bis",
|
||||||
"validUntilDate": "Der Link ist bis zum {date} gültig."
|
"validUntilDate": "Der Link ist bis zum {date} gültig."
|
||||||
},
|
},
|
||||||
@ -413,7 +418,7 @@
|
|||||||
"first": "Vorname",
|
"first": "Vorname",
|
||||||
"first-tooltip": "Nur der Vornamen",
|
"first-tooltip": "Nur der Vornamen",
|
||||||
"first-initial": "Vorname und Initial",
|
"first-initial": "Vorname und Initial",
|
||||||
"first-initial-tooltip": "Vornamen plus die ersten beiden Anfangsbuchstabe des Nachnamens",
|
"first-initial-tooltip": "Vornamen plus den ersten Anfangsbuchstaben des Nachnamens",
|
||||||
"initials": "Initialen",
|
"initials": "Initialen",
|
||||||
"initials-tooltip": "Initialen von Vor- und Nachname also jeweils die ersten zwei Buchstaben unabhängig von der Existenz des Benutzernamens",
|
"initials-tooltip": "Initialen von Vor- und Nachname also jeweils die ersten zwei Buchstaben unabhängig von der Existenz des Benutzernamens",
|
||||||
"name-full": "Vorname und Nachname",
|
"name-full": "Vorname und Nachname",
|
||||||
|
|||||||
@ -230,6 +230,7 @@
|
|||||||
"credit-your-gradido": "For the Gradido to be credited, click on the link!",
|
"credit-your-gradido": "For the Gradido to be credited, click on the link!",
|
||||||
"delete-the-link": "Delete the link?",
|
"delete-the-link": "Delete the link?",
|
||||||
"deleted": "The link was deleted!",
|
"deleted": "The link was deleted!",
|
||||||
|
"disbured": "Disbursement of the Link-Ammount initiated! The transfer of {n} GDD into your account will be completed shortly.",
|
||||||
"expiredOn": "Expired on",
|
"expiredOn": "Expired on",
|
||||||
"has-account": "You already have a Gradido account?",
|
"has-account": "You already have a Gradido account?",
|
||||||
"header": "Send Gradidos via link",
|
"header": "Send Gradidos via link",
|
||||||
@ -245,12 +246,16 @@
|
|||||||
"no-account": "You don't have a Gradido account yet?",
|
"no-account": "You don't have a Gradido account yet?",
|
||||||
"no-redeem": "You not allowed to redeem your own link!",
|
"no-redeem": "You not allowed to redeem your own link!",
|
||||||
"not-copied": "Unfortunately, your device does not allow copying! Please copy the link by hand!",
|
"not-copied": "Unfortunately, your device does not allow copying! Please copy the link by hand!",
|
||||||
|
"recipientCommunityFix": "Recipient Community of the Link-Balance...",
|
||||||
|
"recipientCommunitySelection": "Select your Community to redeem the link-deposit...",
|
||||||
"redeem": "Redeem",
|
"redeem": "Redeem",
|
||||||
"redeemed": "Successfully redeemed! Your account has been credited with {n} GDD.",
|
"redeemed": "Successfully redeemed! Your account has been credited with {n} GDD.",
|
||||||
"redeemed-at": "The link was already redeemed on {date}.",
|
"redeemed-at": "The link was already redeemed on {date}.",
|
||||||
"redeemlink-error": "This redemption link is not complete.",
|
"redeemlink-error": "This redemption link is not complete.",
|
||||||
|
"switchCommunity": "You have selected a foreign Community...",
|
||||||
"to-login": "Log in",
|
"to-login": "Log in",
|
||||||
"to-register": "Register a new account.",
|
"to-register": "Register a new account.",
|
||||||
|
"to-switch": "Switch to Community",
|
||||||
"validUntil": "Valid until",
|
"validUntil": "Valid until",
|
||||||
"validUntilDate": "The link is valid until {date}."
|
"validUntilDate": "The link is valid until {date}."
|
||||||
},
|
},
|
||||||
@ -413,7 +418,7 @@
|
|||||||
"first": "Firstname",
|
"first": "Firstname",
|
||||||
"first-tooltip": "the first name only",
|
"first-tooltip": "the first name only",
|
||||||
"first-initial": "First name and initial",
|
"first-initial": "First name and initial",
|
||||||
"first-initial-tooltip": "first name plus the first two initial letters of the last name",
|
"first-initial-tooltip": "first name plus the first letter of the last name",
|
||||||
"initials": "Initials",
|
"initials": "Initials",
|
||||||
"initials-tooltip": "Initials of first name and last name, i.e. the first two letters of each regardless of the existence of the user name",
|
"initials-tooltip": "Initials of first name and last name, i.e. the first two letters of each regardless of the existence of the user name",
|
||||||
"name-full": "first name and last name",
|
"name-full": "first name and last name",
|
||||||
|
|||||||
@ -170,7 +170,7 @@ describe('TransactionLink', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('redeem link with success', () => {
|
describe.skip('redeem link with success', () => {
|
||||||
let mockMutation
|
let mockMutation
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
|
|||||||
@ -1,9 +1,15 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="show-transaction-link-informations">
|
<div class="show-transaction-link-informations">
|
||||||
<div class="mt-4">
|
<div v-if="isTransactionLinkLoaded" class="mt-4">
|
||||||
<transaction-link-item :type="itemTypeExt">
|
<transaction-link-item :type="itemTypeExt">
|
||||||
<template #LOGGED_OUT>
|
<template #REDEEM_SELECT_COMMUNITY>
|
||||||
<redeem-logged-out :link-data="linkData" :is-contribution-link="isContributionLink" />
|
<redeem-select-community
|
||||||
|
:link-data="linkData"
|
||||||
|
:redeem-code="redeemCode"
|
||||||
|
:is-transaction-link-loaded="isTransactionLinkLoaded"
|
||||||
|
:is-contribution-link="isContributionLink"
|
||||||
|
:is-redeem-jwt-link="isRedeemJwtLink"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #SELF_CREATOR>
|
<template #SELF_CREATOR>
|
||||||
@ -14,6 +20,7 @@
|
|||||||
<redeem-valid
|
<redeem-valid
|
||||||
:link-data="linkData"
|
:link-data="linkData"
|
||||||
:is-contribution-link="isContributionLink"
|
:is-contribution-link="isContributionLink"
|
||||||
|
:is-redeem-jwt-link="isRedeemJwtLink"
|
||||||
:valid-link="validLink"
|
:valid-link="validLink"
|
||||||
@mutation-link="mutationLink"
|
@mutation-link="mutationLink"
|
||||||
/>
|
/>
|
||||||
@ -33,48 +40,69 @@ import { useRoute, useRouter } from 'vue-router'
|
|||||||
import { useStore } from 'vuex'
|
import { useStore } from 'vuex'
|
||||||
import { useQuery, useMutation } from '@vue/apollo-composable'
|
import { useQuery, useMutation } from '@vue/apollo-composable'
|
||||||
import TransactionLinkItem from '@/components/TransactionLinkItem'
|
import TransactionLinkItem from '@/components/TransactionLinkItem'
|
||||||
import RedeemLoggedOut from '@/components/LinkInformations/RedeemLoggedOut'
|
import RedeemSelectCommunity from '@/components/LinkInformations/RedeemSelectCommunity'
|
||||||
import RedeemSelfCreator from '@/components/LinkInformations/RedeemSelfCreator'
|
import RedeemSelfCreator from '@/components/LinkInformations/RedeemSelfCreator'
|
||||||
import RedeemValid from '@/components/LinkInformations/RedeemValid'
|
import RedeemValid from '@/components/LinkInformations/RedeemValid'
|
||||||
import RedeemedTextBox from '@/components/LinkInformations/RedeemedTextBox'
|
import RedeemedTextBox from '@/components/LinkInformations/RedeemedTextBox'
|
||||||
import { useAppToast } from '@/composables/useToast'
|
import { useAppToast } from '@/composables/useToast'
|
||||||
import { queryTransactionLink } from '@/graphql/queries'
|
import { queryTransactionLink } from '@/graphql/queries'
|
||||||
import { redeemTransactionLink } from '@/graphql/mutations'
|
import { disburseTransactionLink, redeemTransactionLink } from '@/graphql/mutations'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
|
|
||||||
const { toastError, toastSuccess } = useAppToast()
|
const { toastError, toastSuccess } = useAppToast()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const { params } = useRoute()
|
const { params, meta } = useRoute()
|
||||||
const store = useStore()
|
const store = useStore()
|
||||||
const { d, t } = useI18n()
|
const { d, t } = useI18n()
|
||||||
|
|
||||||
|
const isTransactionLinkLoaded = ref(false)
|
||||||
const linkData = ref({
|
const linkData = ref({
|
||||||
__typename: 'TransactionLink',
|
__typename: 'TransactionLink',
|
||||||
amount: '',
|
validUntil: null,
|
||||||
|
amount: 0,
|
||||||
memo: '',
|
memo: '',
|
||||||
user: {
|
senderCommunity: null,
|
||||||
firstName: '',
|
senderUser: null,
|
||||||
},
|
recipientCommunity: null,
|
||||||
|
recipientUser: null,
|
||||||
deletedAt: null,
|
deletedAt: null,
|
||||||
|
redeemedAt: null,
|
||||||
validLink: false,
|
validLink: false,
|
||||||
|
communities: [],
|
||||||
|
// ContributionLink fields
|
||||||
|
validTo: null,
|
||||||
|
validFrom: null,
|
||||||
|
name: '',
|
||||||
|
cycle: null,
|
||||||
|
link: '',
|
||||||
|
maxAmountPerMonth: null,
|
||||||
})
|
})
|
||||||
|
|
||||||
const redeemedBoxText = ref('')
|
const redeemedBoxText = ref('')
|
||||||
|
|
||||||
const { result, onResult, loading, error, onError } = useQuery(queryTransactionLink, {
|
const { result, onResult, error, onError } = useQuery(queryTransactionLink, {
|
||||||
code: params.code,
|
code: params.code,
|
||||||
})
|
})
|
||||||
|
|
||||||
const {
|
const { mutate: redeemMutate } = useMutation(redeemTransactionLink)
|
||||||
mutate: redeemMutate,
|
const { mutate: disburseMutate } = useMutation(disburseTransactionLink)
|
||||||
loading: redeemLoading,
|
|
||||||
error: redeemError,
|
|
||||||
} = useMutation(redeemTransactionLink)
|
|
||||||
|
|
||||||
const isContributionLink = computed(() => {
|
const isContributionLink = computed(() => {
|
||||||
return params.code?.search(/^CL-/) === 0
|
return params.code?.search(/^CL-/) === 0
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const isRedeemJwtLink = computed(() => {
|
||||||
|
if (
|
||||||
|
isTransactionLinkLoaded.value &&
|
||||||
|
result.value?.queryTransactionLink?.__typename === 'RedeemJwtLink'
|
||||||
|
) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
|
||||||
|
const redeemCode = computed(() => params.code)
|
||||||
|
|
||||||
const tokenExpiresInSeconds = computed(() => {
|
const tokenExpiresInSeconds = computed(() => {
|
||||||
const remainingSecs = Math.floor(
|
const remainingSecs = Math.floor(
|
||||||
(new Date(store.state.tokenTime * 1000).getTime() - new Date().getTime()) / 1000,
|
(new Date(store.state.tokenTime * 1000).getTime() - new Date().getTime()) / 1000,
|
||||||
@ -83,25 +111,109 @@ const tokenExpiresInSeconds = computed(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const validLink = computed(() => {
|
const validLink = computed(() => {
|
||||||
return new Date(linkData.value.validUntil) > new Date()
|
// console.log('TransactionLink.validLink... linkData.value.validUntil=', linkData.value.validUntil)
|
||||||
|
// console.log('TransactionLink.validLink... new Date()=', new Date())
|
||||||
|
if (!isTransactionLinkLoaded.value) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (!linkData.value.validUntil) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
const validUntilDate = new Date(linkData.value.validUntil)
|
||||||
|
// console.log('TransactionLink.validLink... validUntilDate=', validUntilDate)
|
||||||
|
// console.log('TransactionLink.validLink... new Date()=', new Date())
|
||||||
|
// console.log(
|
||||||
|
// 'TransactionLink.validLink... validUntilDate.getTime() >= new Date().getTime()=',
|
||||||
|
// validUntilDate.getTime() >= new Date().getTime(),
|
||||||
|
// )
|
||||||
|
return validUntilDate.getTime() >= new Date().getTime()
|
||||||
})
|
})
|
||||||
|
|
||||||
const itemType = computed(() => {
|
const itemType = computed(() => {
|
||||||
if (linkData.value.deletedAt) return 'TEXT_DELETED'
|
// console.log('TransactionLink.itemType... isTransactionLinkLoaded=', isTransactionLinkLoaded.value)
|
||||||
if (new Date(linkData.value.validUntil) < new Date()) return 'TEXT_EXPIRED'
|
if (isTransactionLinkLoaded.value) {
|
||||||
if (linkData.value.redeemedAt) return 'TEXT_REDEEMED'
|
// console.log('TransactionLink.itemType... linkData.value=', linkData.value)
|
||||||
|
if (linkData.value.deletedAt) {
|
||||||
|
// console.log('TransactionLink.itemType... TEXT_DELETED')
|
||||||
|
return 'TEXT_DELETED'
|
||||||
|
}
|
||||||
|
|
||||||
if (store.state.token && store.state.tokenTime) {
|
const validUntilDate = new Date(linkData.value.validUntil)
|
||||||
if (tokenExpiresInSeconds.value < 5) return 'LOGGED_OUT'
|
// console.log('TransactionLink.itemType... validUntilDate=', validUntilDate)
|
||||||
if (linkData.value.user && store.state.gradidoID === linkData.value.user.gradidoID)
|
// console.log('TransactionLink.itemType... new Date()=', new Date())
|
||||||
|
// console.log(
|
||||||
|
// 'TransactionLink.itemType... validUntilDate.getTime() < new Date().getTime()=',
|
||||||
|
// validUntilDate.getTime() < new Date().getTime(),
|
||||||
|
// )
|
||||||
|
if (validUntilDate.getTime() < new Date().getTime()) {
|
||||||
|
// console.log('TransactionLink.itemType... TEXT_EXPIRED')
|
||||||
|
return 'TEXT_EXPIRED'
|
||||||
|
}
|
||||||
|
if (linkData.value.redeemedAt) {
|
||||||
|
// console.log('TransactionLink.itemType... TEXT_REDEEMED')
|
||||||
|
return 'TEXT_REDEEMED'
|
||||||
|
}
|
||||||
|
if (linkData.value.deletedAt) {
|
||||||
|
// console.log('TransactionLink.itemType... TEXT_DELETED')
|
||||||
|
return 'TEXT_DELETED'
|
||||||
|
}
|
||||||
|
if (store.state.token && store.state.tokenTime) {
|
||||||
|
if (tokenExpiresInSeconds.value < 5) {
|
||||||
|
// console.log('TransactionLink.itemType... REDEEM_SELECT_COMMUNITY')
|
||||||
|
return 'REDEEM_SELECT_COMMUNITY'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// console.log(
|
||||||
|
// 'TransactionLink.itemType... linkData.value.recipientUser=',
|
||||||
|
// linkData.value.recipientUser,
|
||||||
|
// )
|
||||||
|
// console.log('TransactionLink.itemType... linkData.value=', linkData.value)
|
||||||
|
// console.log('TransactionLink.itemType... store.state.gradidoID=', store.state.gradidoID)
|
||||||
|
// console.log('TransactionLink.itemType... isRedeemJwtLink=', isRedeemJwtLink.value)
|
||||||
|
// console.log('TransactionLink.itemType... linkData.value.senderUser=', linkData.value.senderUser)
|
||||||
|
// console.log(
|
||||||
|
// 'TransactionLink.itemType... linkData.value.recipientUser.gradidoID=',
|
||||||
|
// linkData.value.recipientUser?.gradidoID,
|
||||||
|
// )
|
||||||
|
// console.log(
|
||||||
|
// 'TransactionLink.itemType... linkData.value.senderUser.gradidoID=',
|
||||||
|
// linkData.value.senderUser?.gradidoID,
|
||||||
|
// )
|
||||||
|
if (
|
||||||
|
linkData.value.senderUser &&
|
||||||
|
linkData.value.recipientUser &&
|
||||||
|
linkData.value.senderUser.gradidoID === linkData.value.recipientUser.gradidoID
|
||||||
|
) {
|
||||||
|
// console.log('TransactionLink.itemType... SELF_CREATOR')
|
||||||
return 'SELF_CREATOR'
|
return 'SELF_CREATOR'
|
||||||
if (!linkData.value.redeemedAt && !linkData.value.deletedAt) return 'VALID'
|
}
|
||||||
|
if (
|
||||||
|
linkData.value.senderUser &&
|
||||||
|
linkData.value.recipientUser &&
|
||||||
|
linkData.value.senderUser.gradidoID !== linkData.value.recipientUser.gradidoID &&
|
||||||
|
store.state.gradidoID === linkData.value.recipientUser.gradidoID
|
||||||
|
) {
|
||||||
|
// console.log('TransactionLink.itemType... VALID')
|
||||||
|
// console.log('TransactionLink.itemType... linkData.value=', linkData.value)
|
||||||
|
// console.log('TransactionLink.itemType... store.state.gradidoID=', store.state.gradidoID)
|
||||||
|
// console.log(
|
||||||
|
// 'TransactionLink.itemType... linkData.value.recipientUser.gradidoID=',
|
||||||
|
// linkData.value.recipientUser.gradidoID,
|
||||||
|
// )
|
||||||
|
// console.log(
|
||||||
|
// 'TransactionLink.itemType... linkData.value.senderUser.gradidoID=',
|
||||||
|
// linkData.value.senderUser.gradidoID,
|
||||||
|
// )
|
||||||
|
return 'VALID'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
// console.log('TransactionLink.itemType...last return= REDEEM_SELECT_COMMUNITY')
|
||||||
return 'LOGGED_OUT'
|
return 'REDEEM_SELECT_COMMUNITY'
|
||||||
})
|
})
|
||||||
|
|
||||||
const itemTypeExt = computed(() => {
|
const itemTypeExt = computed(() => {
|
||||||
|
// console.log('TransactionLink.itemTypeExt... itemType=', itemType.value)
|
||||||
|
// console.log('TransactionLink.itemTypeExt... validLink=', validLink.value)
|
||||||
if (itemType.value.startsWith('TEXT')) {
|
if (itemType.value.startsWith('TEXT')) {
|
||||||
return 'TEXT'
|
return 'TEXT'
|
||||||
}
|
}
|
||||||
@ -109,10 +221,13 @@ const itemTypeExt = computed(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
watch(itemType, (newItemType) => {
|
watch(itemType, (newItemType) => {
|
||||||
|
// console.log('TransactionLink.watch... itemType=', itemType.value)
|
||||||
|
// console.log('TransactionLink.watch... validLink=', validLink.value)
|
||||||
updateRedeemedBoxText(newItemType)
|
updateRedeemedBoxText(newItemType)
|
||||||
})
|
})
|
||||||
|
|
||||||
function updateRedeemedBoxText(type) {
|
function updateRedeemedBoxText(type) {
|
||||||
|
// console.log('TransactionLink.updateRedeemedBoxText... type=', type)
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'TEXT_DELETED':
|
case 'TEXT_DELETED':
|
||||||
redeemedBoxText.value = t('gdd_per_link.link-deleted', {
|
redeemedBoxText.value = t('gdd_per_link.link-deleted', {
|
||||||
@ -132,43 +247,124 @@ function updateRedeemedBoxText(type) {
|
|||||||
default:
|
default:
|
||||||
redeemedBoxText.value = ''
|
redeemedBoxText.value = ''
|
||||||
}
|
}
|
||||||
|
// console.log('TransactionLink.updateRedeemedBoxText... redeemedBoxText=', redeemedBoxText)
|
||||||
}
|
}
|
||||||
|
|
||||||
const emit = defineEmits(['set-mobile-start'])
|
const emit = defineEmits(['set-mobile-start'])
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
// console.log('TransactionLink.onMounted... params=', params)
|
||||||
emit('set-mobile-start', false)
|
emit('set-mobile-start', false)
|
||||||
})
|
})
|
||||||
|
|
||||||
onResult(() => {
|
onResult(() => {
|
||||||
if (!result || !result.value) return
|
// console.log('TransactionLink.onResult... result=', result.value)
|
||||||
setTransactionLinkInformation()
|
// console.log('TransactionLink.onResult... stringify result=', JSON.stringify(result.value))
|
||||||
|
if (result.value?.queryTransactionLink?.__typename === 'TransactionLink') {
|
||||||
|
// console.log('TransactionLink.onResult... TransactionLink')
|
||||||
|
isTransactionLinkLoaded.value = true
|
||||||
|
setTransactionLinkInformation()
|
||||||
|
} else if (result.value?.queryTransactionLink?.__typename === 'RedeemJwtLink') {
|
||||||
|
// console.log('TransactionLink.onResult... RedeemJwtLink')
|
||||||
|
isTransactionLinkLoaded.value = true
|
||||||
|
setRedeemJwtLinkInformation()
|
||||||
|
} else {
|
||||||
|
// console.log('TransactionLink.onResult... unknown type:', result.value)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
onError(() => {
|
onError(() => {
|
||||||
|
// console.log('TransactionLink.onError... error=', error)
|
||||||
toastError(t('gdd_per_link.redeemlink-error'))
|
toastError(t('gdd_per_link.redeemlink-error'))
|
||||||
})
|
})
|
||||||
|
|
||||||
function setTransactionLinkInformation() {
|
function setTransactionLinkInformation() {
|
||||||
const { queryTransactionLink } = result.value
|
// console.log('TransactionLink.setTransactionLinkInformation... result=', result.value)
|
||||||
if (queryTransactionLink) {
|
// const queryTransactionLink = result.value.queryTransactionLink
|
||||||
linkData.value = queryTransactionLink
|
const deepCopy = JSON.parse(JSON.stringify(result.value))
|
||||||
|
// console.log('TransactionLink.setTransactionLinkInformation... deepCopy=', deepCopy)
|
||||||
|
if (deepCopy && deepCopy.queryTransactionLink.__typename === 'TransactionLink') {
|
||||||
|
// console.log('TransactionLink.setTransactionLinkInformation... typename === TransactionLink')
|
||||||
|
// recipientUser is only set if the user is logged in
|
||||||
|
if (store.state.gradidoID !== null) {
|
||||||
|
// console.log(
|
||||||
|
// 'TransactionLink.setTransactionLinkInformation... gradidoID=',
|
||||||
|
// store.state.gradidoID,
|
||||||
|
// )
|
||||||
|
deepCopy.queryTransactionLink.recipientUser = {
|
||||||
|
__typename: 'User',
|
||||||
|
gradidoID: store.state.gradidoID,
|
||||||
|
firstName: store.state.firstName,
|
||||||
|
alias: store.state.alias,
|
||||||
|
}
|
||||||
|
// console.log(
|
||||||
|
// 'TransactionLink.setTransactionLinkInformation... deepCopy.queryTransactionLink.recipientUser=',
|
||||||
|
// deepCopy.queryTransactionLink.recipientUser,
|
||||||
|
// )
|
||||||
|
}
|
||||||
|
linkData.value = deepCopy.queryTransactionLink
|
||||||
|
// console.log('TransactionLink.setTransactionLinkInformation... linkData.value=', linkData.value)
|
||||||
if (linkData.value.__typename === 'ContributionLink' && store.state.token) {
|
if (linkData.value.__typename === 'ContributionLink' && store.state.token) {
|
||||||
|
// console.log('TransactionLink.setTransactionLinkInformation... typename === ContributionLink')
|
||||||
|
// explicit no await
|
||||||
mutationLink(linkData.value.amount)
|
mutationLink(linkData.value.amount)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setRedeemJwtLinkInformation() {
|
||||||
|
// console.log('TransactionLink.setRedeemJwtLinkInformation... result=', result.value)
|
||||||
|
const deepCopy = JSON.parse(JSON.stringify(result.value))
|
||||||
|
// console.log('TransactionLink.setRedeemJwtLinkInformation... deepCopy=', deepCopy)
|
||||||
|
if (deepCopy) {
|
||||||
|
// recipientUser is only set if the user is logged in
|
||||||
|
if (store.state.gradidoID !== null) {
|
||||||
|
// console.log(
|
||||||
|
// 'TransactionLink.setRedeemJwtLinkInformation... gradidoID=',
|
||||||
|
// store.state.gradidoID,
|
||||||
|
// )
|
||||||
|
deepCopy.queryTransactionLink.recipientUser = {
|
||||||
|
__typename: 'User',
|
||||||
|
gradidoID: store.state.gradidoID,
|
||||||
|
firstName: store.state.firstName,
|
||||||
|
alias: store.state.alias,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// console.log(
|
||||||
|
// 'TransactionLink.setRedeemJwtLinkInformation... deepCopy.queryTransactionLink.recipientUser=',
|
||||||
|
// deepCopy.queryTransactionLink.recipientUser,
|
||||||
|
// )
|
||||||
|
linkData.value = deepCopy.queryTransactionLink
|
||||||
|
// console.log('TransactionLink.setRedeemJwtLinkInformation... linkData.value=', linkData.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function mutationLink(amount) {
|
async function mutationLink(amount) {
|
||||||
try {
|
// console.log('TransactionLink.mutationLink... params=', params)
|
||||||
await redeemMutate({
|
if (isRedeemJwtLink.value) {
|
||||||
code: params.code,
|
// console.log('TransactionLink.mutationLink... trigger disbursement from recipient-community')
|
||||||
})
|
try {
|
||||||
toastSuccess(t('gdd_per_link.redeemed', { n: amount }))
|
await disburseMutate({
|
||||||
await router.push('/overview')
|
code: params.code,
|
||||||
} catch (err) {
|
})
|
||||||
toastError(err.message)
|
toastSuccess(t('gdd_per_link.disbured', { n: amount }))
|
||||||
await router.push('/overview')
|
await router.push('/overview')
|
||||||
|
} catch (err) {
|
||||||
|
toastError(err.message)
|
||||||
|
await router.push('/overview')
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// console.log('TransactionLink.mutationLink... local transaction or contribution')
|
||||||
|
try {
|
||||||
|
await redeemMutate({
|
||||||
|
code: redeemCode.value,
|
||||||
|
})
|
||||||
|
toastSuccess(t('gdd_per_link.redeemed', { n: amount }))
|
||||||
|
await router.push('/overview')
|
||||||
|
} catch (err) {
|
||||||
|
toastError(err.message)
|
||||||
|
await router.push('/overview')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
174
frontend/src/pages/TransactionLinkDisburse.vue
Normal file
174
frontend/src/pages/TransactionLinkDisburse.vue
Normal file
@ -0,0 +1,174 @@
|
|||||||
|
<template>
|
||||||
|
<div class="show-transaction-link-informations">
|
||||||
|
<div class="mt-4">
|
||||||
|
<transaction-link-item :type="itemTypeExt">
|
||||||
|
<template #LOGGED_OUT>
|
||||||
|
<redeem-logged-out :link-data="linkData" :is-contribution-link="isContributionLink" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #SELF_CREATOR>
|
||||||
|
<redeem-self-creator :link-data="linkData" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #VALID>
|
||||||
|
<redeem-valid
|
||||||
|
:link-data="linkData"
|
||||||
|
:is-contribution-link="isContributionLink"
|
||||||
|
:valid-link="validLink"
|
||||||
|
@mutation-link="mutationLink"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #TEXT>
|
||||||
|
<redeemed-text-box :text="redeemedBoxText" />
|
||||||
|
</template>
|
||||||
|
</transaction-link-item>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, computed, onMounted, watch } from 'vue'
|
||||||
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
|
import { useStore } from 'vuex'
|
||||||
|
import { useQuery, useMutation } from '@vue/apollo-composable'
|
||||||
|
import TransactionLinkItem from '@/components/TransactionLinkItem'
|
||||||
|
import RedeemLoggedOut from '@/components/LinkInformations/RedeemLoggedOut'
|
||||||
|
import RedeemSelfCreator from '@/components/LinkInformations/RedeemSelfCreator'
|
||||||
|
import RedeemValid from '@/components/LinkInformations/RedeemValid'
|
||||||
|
import RedeemedTextBox from '@/components/LinkInformations/RedeemedTextBox'
|
||||||
|
import { useAppToast } from '@/composables/useToast'
|
||||||
|
import { queryTransactionLink } from '@/graphql/queries'
|
||||||
|
import { redeemTransactionLink } from '@/graphql/mutations'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
|
||||||
|
const { toastError, toastSuccess } = useAppToast()
|
||||||
|
const router = useRouter()
|
||||||
|
const { params } = useRoute()
|
||||||
|
const store = useStore()
|
||||||
|
const { d, t } = useI18n()
|
||||||
|
|
||||||
|
const linkData = ref({
|
||||||
|
__typename: 'TransactionLink',
|
||||||
|
amount: '',
|
||||||
|
memo: '',
|
||||||
|
user: {
|
||||||
|
firstName: '',
|
||||||
|
},
|
||||||
|
deletedAt: null,
|
||||||
|
validLink: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
const redeemedBoxText = ref('')
|
||||||
|
|
||||||
|
const { result, onResult, loading, error, onError } = useQuery(queryTransactionLink, {
|
||||||
|
code: params.code,
|
||||||
|
})
|
||||||
|
|
||||||
|
const {
|
||||||
|
mutate: redeemMutate,
|
||||||
|
loading: redeemLoading,
|
||||||
|
error: redeemError,
|
||||||
|
} = useMutation(redeemTransactionLink)
|
||||||
|
|
||||||
|
const isContributionLink = computed(() => {
|
||||||
|
return params.code?.search(/^CL-/) === 0
|
||||||
|
})
|
||||||
|
|
||||||
|
const tokenExpiresInSeconds = computed(() => {
|
||||||
|
const remainingSecs = Math.floor(
|
||||||
|
(new Date(store.state.tokenTime * 1000).getTime() - new Date().getTime()) / 1000,
|
||||||
|
)
|
||||||
|
return remainingSecs <= 0 ? 0 : remainingSecs
|
||||||
|
})
|
||||||
|
|
||||||
|
const validLink = computed(() => {
|
||||||
|
return new Date(linkData.value.validUntil) > new Date()
|
||||||
|
})
|
||||||
|
|
||||||
|
const itemType = computed(() => {
|
||||||
|
if (linkData.value.deletedAt) return 'TEXT_DELETED'
|
||||||
|
if (new Date(linkData.value.validUntil) < new Date()) return 'TEXT_EXPIRED'
|
||||||
|
if (linkData.value.redeemedAt) return 'TEXT_REDEEMED'
|
||||||
|
|
||||||
|
if (store.state.token && store.state.tokenTime) {
|
||||||
|
if (tokenExpiresInSeconds.value < 5) return 'LOGGED_OUT'
|
||||||
|
if (linkData.value.user && store.state.gradidoID === linkData.value.user.gradidoID)
|
||||||
|
return 'SELF_CREATOR'
|
||||||
|
if (!linkData.value.redeemedAt && !linkData.value.deletedAt) return 'VALID'
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'LOGGED_OUT'
|
||||||
|
})
|
||||||
|
|
||||||
|
const itemTypeExt = computed(() => {
|
||||||
|
if (itemType.value.startsWith('TEXT')) {
|
||||||
|
return 'TEXT'
|
||||||
|
}
|
||||||
|
return itemType.value
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(itemType, (newItemType) => {
|
||||||
|
updateRedeemedBoxText(newItemType)
|
||||||
|
})
|
||||||
|
|
||||||
|
function updateRedeemedBoxText(type) {
|
||||||
|
switch (type) {
|
||||||
|
case 'TEXT_DELETED':
|
||||||
|
redeemedBoxText.value = t('gdd_per_link.link-deleted', {
|
||||||
|
date: d(new Date(linkData.value.deletedAt), 'long'),
|
||||||
|
})
|
||||||
|
break
|
||||||
|
case 'TEXT_EXPIRED':
|
||||||
|
redeemedBoxText.value = t('gdd_per_link.link-expired', {
|
||||||
|
date: d(new Date(linkData.value.validUntil), 'long'),
|
||||||
|
})
|
||||||
|
break
|
||||||
|
case 'TEXT_REDEEMED':
|
||||||
|
redeemedBoxText.value = t('gdd_per_link.redeemed-at', {
|
||||||
|
date: d(new Date(linkData.value.redeemedAt), 'long'),
|
||||||
|
})
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
redeemedBoxText.value = ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const emit = defineEmits(['set-mobile-start'])
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
emit('set-mobile-start', false)
|
||||||
|
})
|
||||||
|
|
||||||
|
onResult(() => {
|
||||||
|
if (!result || !result.value) return
|
||||||
|
setTransactionLinkInformation()
|
||||||
|
})
|
||||||
|
|
||||||
|
onError(() => {
|
||||||
|
toastError(t('gdd_per_link.redeemlink-error'))
|
||||||
|
})
|
||||||
|
|
||||||
|
function setTransactionLinkInformation() {
|
||||||
|
const { queryTransactionLink } = result.value
|
||||||
|
if (queryTransactionLink) {
|
||||||
|
linkData.value = queryTransactionLink
|
||||||
|
if (linkData.value.__typename === 'ContributionLink' && store.state.token) {
|
||||||
|
mutationLink(linkData.value.amount)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function mutationLink(amount) {
|
||||||
|
try {
|
||||||
|
await redeemMutate({
|
||||||
|
code: params.code,
|
||||||
|
})
|
||||||
|
toastSuccess(t('gdd_per_link.redeemed', { n: amount }))
|
||||||
|
await router.push('/overview')
|
||||||
|
} catch (err) {
|
||||||
|
toastError(err.message)
|
||||||
|
await router.push('/overview')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
Loading…
x
Reference in New Issue
Block a user