Merge branch 'master' into replace-jasonwebtoken-with-jose

This commit is contained in:
Ulf Gebhardt 2023-05-17 13:56:03 +02:00
commit 97ed8df148
Signed by: ulfgebhardt
GPG Key ID: DA6B843E748679C9
108 changed files with 1437 additions and 615 deletions

View File

@ -1,3 +1,3 @@
node_modules/ node_modules/
dist/ build/
coverage/ coverage/

2
admin/.gitignore vendored
View File

@ -1,5 +1,5 @@
node_modules/ node_modules/
dist/ build/
.cache/ .cache/
/.env /.env

View File

@ -84,7 +84,7 @@ CMD /bin/sh -c "yarn run dev"
FROM base as production FROM base as production
# Copy "binary"-files from build image # Copy "binary"-files from build image
COPY --from=build ${DOCKER_WORKDIR}/dist ./dist COPY --from=build ${DOCKER_WORKDIR}/build ./build
# We also copy the node_modules express and serve-static for the run script # We also copy the node_modules express and serve-static for the run script
COPY --from=build ${DOCKER_WORKDIR}/node_modules ./node_modules COPY --from=build ${DOCKER_WORKDIR}/node_modules ./node_modules
# Copy static files # Copy static files

View File

@ -11,7 +11,7 @@
"serve": "vue-cli-service serve --open", "serve": "vue-cli-service serve --open",
"build": "vue-cli-service build", "build": "vue-cli-service build",
"dev": "yarn run serve", "dev": "yarn run serve",
"analyse-bundle": "yarn build && webpack-bundle-analyzer dist/webpack.stats.json", "analyse-bundle": "yarn build && webpack-bundle-analyzer build/webpack.stats.json",
"lint": "eslint --max-warnings=0 --ext .js,.vue,.json .", "lint": "eslint --max-warnings=0 --ext .js,.vue,.json .",
"stylelint": "stylelint --max-warnings=0 '**/*.{scss,vue}'", "stylelint": "stylelint --max-warnings=0 '**/*.{scss,vue}'",
"test": "cross-env TZ=UTC jest", "test": "cross-env TZ=UTC jest",

View File

@ -9,10 +9,10 @@ const port = process.env.PORT || 8080
// Express Server // Express Server
const app = express() const app = express()
// Serve files // Serve files
app.use(express.static(path.join(__dirname, '../dist'))) app.use(express.static(path.join(__dirname, '../build')))
// Default to index.html // Default to index.html
app.get('*', (req, res) => { app.get('*', (req, res) => {
res.sendFile(path.join(__dirname, '../dist/index.html')) res.sendFile(path.join(__dirname, '../build/index.html'))
}) })
app.listen(port, hostname, () => { app.listen(port, hostname, () => {

View File

@ -13,7 +13,7 @@
</template> </template>
<script> <script>
import { formatDistanceToNow } from 'date-fns' import { formatDistanceToNow } from 'date-fns'
import { de, en, fr, es, nl } from 'date-fns/locale' import { de, enUS as en, fr, es, nl } from 'date-fns/locale'
const locales = { en, de, es, fr, nl } const locales = { en, de, es, fr, nl }

View File

@ -37,6 +37,7 @@ export const actions = {
const store = new Vuex.Store({ const store = new Vuex.Store({
plugins: [ plugins: [
createPersistedState({ createPersistedState({
key: 'gradido-admin',
storage: window.localStorage, storage: window.localStorage,
}), }),
], ],

View File

@ -49,5 +49,5 @@ module.exports = {
// Enable CSS source maps. // Enable CSS source maps.
sourceMap: CONFIG.NODE_ENV !== 'production', sourceMap: CONFIG.NODE_ENV !== 'production',
}, },
outputDir: path.resolve(__dirname, './dist'), outputDir: path.resolve(__dirname, './build'),
} }

View File

@ -1,3 +1,4 @@
node_modules node_modules
**/*.min.js **/*.min.js
build build
coverage

View File

@ -12,6 +12,8 @@ module.exports = {
'plugin:prettier/recommended', 'plugin:prettier/recommended',
'plugin:import/recommended', 'plugin:import/recommended',
'plugin:import/typescript', 'plugin:import/typescript',
'plugin:security/recommended',
'plugin:@eslint-community/eslint-comments/recommended',
], ],
settings: { settings: {
'import/parsers': { 'import/parsers': {
@ -151,6 +153,11 @@ module.exports = {
'promise/valid-params': 'warn', 'promise/valid-params': 'warn',
'promise/prefer-await-to-callbacks': 'error', 'promise/prefer-await-to-callbacks': 'error',
'promise/no-multiple-resolved': 'error', 'promise/no-multiple-resolved': 'error',
// eslint comments
'@eslint-community/eslint-comments/disable-enable-pair': ['error', { allowWholeFile: true }],
'@eslint-community/eslint-comments/no-restricted-disable': 'error',
'@eslint-community/eslint-comments/no-use': 'off',
'@eslint-community/eslint-comments/require-description': 'off',
}, },
overrides: [ overrides: [
// only for ts files // only for ts files
@ -159,6 +166,7 @@ module.exports = {
extends: [ extends: [
'plugin:@typescript-eslint/recommended', 'plugin:@typescript-eslint/recommended',
'plugin:@typescript-eslint/recommended-requiring-type-checking', 'plugin:@typescript-eslint/recommended-requiring-type-checking',
'plugin:@typescript-eslint/strict',
'plugin:type-graphql/recommended', 'plugin:type-graphql/recommended',
], ],
rules: { rules: {
@ -169,6 +177,8 @@ module.exports = {
'@typescript-eslint/prefer-regexp-exec': 'off', '@typescript-eslint/prefer-regexp-exec': 'off',
// this should not run on ts files: https://github.com/import-js/eslint-plugin-import/issues/2215#issuecomment-911245486 // this should not run on ts files: https://github.com/import-js/eslint-plugin-import/issues/2215#issuecomment-911245486
'import/unambiguous': 'off', 'import/unambiguous': 'off',
// this is not compatible with typeorm, due to joined tables can be null, but are not defined as nullable
'@typescript-eslint/no-unnecessary-condition': 'off',
}, },
parserOptions: { parserOptions: {
tsconfigRootDir: __dirname, tsconfigRootDir: __dirname,

View File

@ -7,7 +7,7 @@ module.exports = {
collectCoverageFrom: ['src/**/*.ts', '!**/node_modules/**', '!src/seeds/**', '!build/**'], collectCoverageFrom: ['src/**/*.ts', '!**/node_modules/**', '!src/seeds/**', '!build/**'],
coverageThreshold: { coverageThreshold: {
global: { global: {
lines: 85, lines: 86,
}, },
}, },
setupFiles: ['<rootDir>/test/testSetup.ts'], setupFiles: ['<rootDir>/test/testSetup.ts'],

View File

@ -46,6 +46,7 @@
"uuid": "^8.3.2" "uuid": "^8.3.2"
}, },
"devDependencies": { "devDependencies": {
"@eslint-community/eslint-plugin-eslint-comments": "^3.2.1",
"@types/email-templates": "^10.0.1", "@types/email-templates": "^10.0.1",
"@types/express": "^4.17.12", "@types/express": "^4.17.12",
"@types/faker": "^5.5.9", "@types/faker": "^5.5.9",
@ -67,6 +68,7 @@
"eslint-plugin-n": "^15.7.0", "eslint-plugin-n": "^15.7.0",
"eslint-plugin-prettier": "^4.2.1", "eslint-plugin-prettier": "^4.2.1",
"eslint-plugin-promise": "^6.1.1", "eslint-plugin-promise": "^6.1.1",
"eslint-plugin-security": "^1.7.1",
"eslint-plugin-type-graphql": "^1.0.0", "eslint-plugin-type-graphql": "^1.0.0",
"faker": "^5.5.3", "faker": "^5.5.3",
"graphql-tag": "^2.12.6", "graphql-tag": "^2.12.6",

View File

@ -7,7 +7,6 @@ import axios from 'axios'
import { LogError } from '@/server/LogError' import { LogError } from '@/server/LogError'
import { backendLogger as logger } from '@/server/logger' import { backendLogger as logger } from '@/server/logger'
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const apiPost = async (url: string, payload: unknown): Promise<any> => { export const apiPost = async (url: string, payload: unknown): Promise<any> => {
logger.trace('POST', url, payload) logger.trace('POST', url, payload)
try { try {
@ -25,7 +24,6 @@ export const apiPost = async (url: string, payload: unknown): Promise<any> => {
} }
} }
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const apiGet = async (url: string): Promise<any> => { export const apiGet = async (url: string): Promise<any> => {
logger.trace('GET: url=' + url) logger.trace('GET: url=' + url)
try { try {

View File

@ -12,7 +12,7 @@ import KlicktippConnector from 'klicktipp-api'
const klicktippConnector = new KlicktippConnector() const klicktippConnector = new KlicktippConnector()
export const klicktippSignIn = async ( export const subscribe = async (
email: string, email: string,
language: string, language: string,
firstName?: string, firstName?: string,
@ -28,13 +28,6 @@ export const klicktippSignIn = async (
return result return result
} }
export const signout = async (email: string, language: string): Promise<boolean> => {
if (!CONFIG.KLICKTIPP) return true
const apiKey = language === 'de' ? CONFIG.KLICKTIPP_APIKEY_DE : CONFIG.KLICKTIPP_APIKEY_EN
const result = await klicktippConnector.signoff(apiKey, email)
return result
}
export const unsubscribe = async (email: string): Promise<boolean> => { export const unsubscribe = async (email: string): Promise<boolean> => {
if (!CONFIG.KLICKTIPP) return true if (!CONFIG.KLICKTIPP) return true
const isLogin = await loginKlicktippUser() const isLogin = await loginKlicktippUser()
@ -60,38 +53,6 @@ export const loginKlicktippUser = async (): Promise<boolean> => {
return await klicktippConnector.login(CONFIG.KLICKTIPP_USER, CONFIG.KLICKTIPP_PASSWORD) return await klicktippConnector.login(CONFIG.KLICKTIPP_USER, CONFIG.KLICKTIPP_PASSWORD)
} }
export const logoutKlicktippUser = async (): Promise<boolean> => {
if (!CONFIG.KLICKTIPP) return true
return await klicktippConnector.logout()
}
export const untagUser = async (email: string, tagId: string): Promise<boolean> => {
if (!CONFIG.KLICKTIPP) return true
const isLogin = await loginKlicktippUser()
if (isLogin) {
return await klicktippConnector.untag(email, tagId)
}
return false
}
export const tagUser = async (email: string, tagIds: string): Promise<boolean> => {
if (!CONFIG.KLICKTIPP) return true
const isLogin = await loginKlicktippUser()
if (isLogin) {
return await klicktippConnector.tag(email, tagIds)
}
return false
}
export const getKlicktippTagMap = async () => {
if (!CONFIG.KLICKTIPP) return true
const isLogin = await loginKlicktippUser()
if (isLogin) {
return await klicktippConnector.tagIndex()
}
return ''
}
export const addFieldsToSubscriber = async ( export const addFieldsToSubscriber = async (
email: string, email: string,
fields: any = {}, fields: any = {},

View File

@ -8,4 +8,5 @@ export const INALIENABLE_RIGHTS = [
RIGHTS.SET_PASSWORD, RIGHTS.SET_PASSWORD,
RIGHTS.QUERY_TRANSACTION_LINK, RIGHTS.QUERY_TRANSACTION_LINK,
RIGHTS.QUERY_OPT_IN, RIGHTS.QUERY_OPT_IN,
RIGHTS.CHECK_USERNAME,
] ]

View File

@ -34,6 +34,7 @@ export enum RIGHTS {
LIST_ALL_CONTRIBUTION_MESSAGES = 'LIST_ALL_CONTRIBUTION_MESSAGES', LIST_ALL_CONTRIBUTION_MESSAGES = 'LIST_ALL_CONTRIBUTION_MESSAGES',
OPEN_CREATIONS = 'OPEN_CREATIONS', OPEN_CREATIONS = 'OPEN_CREATIONS',
USER = 'USER', USER = 'USER',
CHECK_USERNAME = 'CHECK_USERNAME',
// Admin // Admin
SEARCH_USERS = 'SEARCH_USERS', SEARCH_USERS = 'SEARCH_USERS',
SET_USER_ROLE = 'SET_USER_ROLE', SET_USER_ROLE = 'SET_USER_ROLE',

View File

@ -12,11 +12,11 @@ Decimal.set({
}) })
const constants = { const constants = {
DB_VERSION: '0065-refactor_communities_table', DB_VERSION: '0066-x-community-sendcoins-transactions_table',
DECAY_START_TIME: new Date('2021-05-13 17:46:31-0000'), // GMT+0 DECAY_START_TIME: new Date('2021-05-13 17:46:31-0000'), // GMT+0
LOG4JS_CONFIG: 'log4js-config.json', LOG4JS_CONFIG: 'log4js-config.json',
// default log level on production should be info // default log level on production should be info
LOG_LEVEL: process.env.LOG_LEVEL || 'info', LOG_LEVEL: process.env.LOG_LEVEL ?? 'info',
CONFIG_VERSION: { CONFIG_VERSION: {
DEFAULT: 'DEFAULT', DEFAULT: 'DEFAULT',
EXPECTED: 'v15.2023-02-07', EXPECTED: 'v15.2023-02-07',
@ -25,67 +25,67 @@ const constants = {
} }
const server = { 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',
GRAPHIQL: process.env.GRAPHIQL === 'true' || false, GRAPHIQL: process.env.GRAPHIQL === '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',
PRODUCTION: process.env.NODE_ENV === 'production' || false, PRODUCTION: process.env.NODE_ENV === 'production' || false,
} }
const database = { const database = {
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 ? 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',
TYPEORM_LOGGING_RELATIVE_PATH: process.env.TYPEORM_LOGGING_RELATIVE_PATH || 'typeorm.backend.log', TYPEORM_LOGGING_RELATIVE_PATH: process.env.TYPEORM_LOGGING_RELATIVE_PATH ?? 'typeorm.backend.log',
} }
const klicktipp = { const klicktipp = {
KLICKTIPP: process.env.KLICKTIPP === 'true' || false, KLICKTIPP: process.env.KLICKTIPP === 'true' || false,
KLICKTTIPP_API_URL: process.env.KLICKTIPP_API_URL || 'https://api.klicktipp.com', KLICKTTIPP_API_URL: process.env.KLICKTIPP_API_URL ?? 'https://api.klicktipp.com',
KLICKTIPP_USER: process.env.KLICKTIPP_USER || 'gradido_test', KLICKTIPP_USER: process.env.KLICKTIPP_USER ?? 'gradido_test',
KLICKTIPP_PASSWORD: process.env.KLICKTIPP_PASSWORD || 'secret321', KLICKTIPP_PASSWORD: process.env.KLICKTIPP_PASSWORD ?? 'secret321',
KLICKTIPP_APIKEY_DE: process.env.KLICKTIPP_APIKEY_DE || 'SomeFakeKeyDE', KLICKTIPP_APIKEY_DE: process.env.KLICKTIPP_APIKEY_DE ?? 'SomeFakeKeyDE',
KLICKTIPP_APIKEY_EN: process.env.KLICKTIPP_APIKEY_EN || 'SomeFakeKeyEN', KLICKTIPP_APIKEY_EN: process.env.KLICKTIPP_APIKEY_EN ?? 'SomeFakeKeyEN',
} }
const community = { const community = {
COMMUNITY_NAME: process.env.COMMUNITY_NAME || 'Gradido Entwicklung', COMMUNITY_NAME: process.env.COMMUNITY_NAME ?? 'Gradido Entwicklung',
COMMUNITY_URL: process.env.COMMUNITY_URL || 'http://localhost/', COMMUNITY_URL: process.env.COMMUNITY_URL ?? 'http://localhost/',
COMMUNITY_REGISTER_URL: process.env.COMMUNITY_REGISTER_URL || 'http://localhost/register', COMMUNITY_REGISTER_URL: process.env.COMMUNITY_REGISTER_URL ?? 'http://localhost/register',
COMMUNITY_REDEEM_URL: process.env.COMMUNITY_REDEEM_URL || 'http://localhost/redeem/{code}', COMMUNITY_REDEEM_URL: process.env.COMMUNITY_REDEEM_URL ?? 'http://localhost/redeem/{code}',
COMMUNITY_REDEEM_CONTRIBUTION_URL: COMMUNITY_REDEEM_CONTRIBUTION_URL:
process.env.COMMUNITY_REDEEM_CONTRIBUTION_URL || 'http://localhost/redeem/CL-{code}', process.env.COMMUNITY_REDEEM_CONTRIBUTION_URL ?? 'http://localhost/redeem/CL-{code}',
COMMUNITY_DESCRIPTION: COMMUNITY_DESCRIPTION:
process.env.COMMUNITY_DESCRIPTION || 'Die lokale Entwicklungsumgebung von Gradido.', process.env.COMMUNITY_DESCRIPTION ?? 'Die lokale Entwicklungsumgebung von Gradido.',
COMMUNITY_SUPPORT_MAIL: process.env.COMMUNITY_SUPPORT_MAIL || 'support@supportmail.com', COMMUNITY_SUPPORT_MAIL: process.env.COMMUNITY_SUPPORT_MAIL ?? 'support@supportmail.com',
} }
const loginServer = { const loginServer = {
LOGIN_APP_SECRET: process.env.LOGIN_APP_SECRET || '21ffbbc616fe', LOGIN_APP_SECRET: process.env.LOGIN_APP_SECRET ?? '21ffbbc616fe',
LOGIN_SERVER_KEY: process.env.LOGIN_SERVER_KEY || 'a51ef8ac7ef1abf162fb7a65261acd7a', LOGIN_SERVER_KEY: process.env.LOGIN_SERVER_KEY ?? 'a51ef8ac7ef1abf162fb7a65261acd7a',
} }
const email = { const email = {
EMAIL: process.env.EMAIL === 'true' || false, EMAIL: process.env.EMAIL === 'true' || false,
EMAIL_TEST_MODUS: process.env.EMAIL_TEST_MODUS === 'true' || false, EMAIL_TEST_MODUS: process.env.EMAIL_TEST_MODUS === 'true' || false,
EMAIL_TEST_RECEIVER: process.env.EMAIL_TEST_RECEIVER || 'stage1@gradido.net', EMAIL_TEST_RECEIVER: process.env.EMAIL_TEST_RECEIVER ?? 'stage1@gradido.net',
EMAIL_USERNAME: process.env.EMAIL_USERNAME || '', EMAIL_USERNAME: process.env.EMAIL_USERNAME ?? '',
EMAIL_SENDER: process.env.EMAIL_SENDER || 'info@gradido.net', EMAIL_SENDER: process.env.EMAIL_SENDER ?? 'info@gradido.net',
EMAIL_PASSWORD: process.env.EMAIL_PASSWORD || '', EMAIL_PASSWORD: process.env.EMAIL_PASSWORD ?? '',
EMAIL_SMTP_URL: process.env.EMAIL_SMTP_URL || 'mailserver', EMAIL_SMTP_URL: process.env.EMAIL_SMTP_URL ?? 'mailserver',
EMAIL_SMTP_PORT: Number(process.env.EMAIL_SMTP_PORT) || 1025, EMAIL_SMTP_PORT: Number(process.env.EMAIL_SMTP_PORT) || 1025,
// eslint-disable-next-line no-unneeded-ternary // eslint-disable-next-line no-unneeded-ternary
EMAIL_TLS: process.env.EMAIL_TLS === 'false' ? false : true, EMAIL_TLS: process.env.EMAIL_TLS === 'false' ? false : true,
EMAIL_LINK_VERIFICATION: EMAIL_LINK_VERIFICATION:
process.env.EMAIL_LINK_VERIFICATION || 'http://localhost/checkEmail/{optin}{code}', process.env.EMAIL_LINK_VERIFICATION ?? 'http://localhost/checkEmail/{optin}{code}',
EMAIL_LINK_SETPASSWORD: EMAIL_LINK_SETPASSWORD:
process.env.EMAIL_LINK_SETPASSWORD || 'http://localhost/reset-password/{optin}', process.env.EMAIL_LINK_SETPASSWORD ?? 'http://localhost/reset-password/{optin}',
EMAIL_LINK_FORGOTPASSWORD: EMAIL_LINK_FORGOTPASSWORD:
process.env.EMAIL_LINK_FORGOTPASSWORD || 'http://localhost/forgot-password', process.env.EMAIL_LINK_FORGOTPASSWORD ?? 'http://localhost/forgot-password',
EMAIL_LINK_OVERVIEW: process.env.EMAIL_LINK_OVERVIEW || 'http://localhost/overview', EMAIL_LINK_OVERVIEW: process.env.EMAIL_LINK_OVERVIEW ?? 'http://localhost/overview',
// time in minutes a optin code is valid // time in minutes a optin code is valid
EMAIL_CODE_VALID_TIME: process.env.EMAIL_CODE_VALID_TIME EMAIL_CODE_VALID_TIME: process.env.EMAIL_CODE_VALID_TIME
? parseInt(process.env.EMAIL_CODE_VALID_TIME) || 1440 ? parseInt(process.env.EMAIL_CODE_VALID_TIME) || 1440
@ -98,14 +98,14 @@ const email = {
const webhook = { const webhook = {
// Elopage // Elopage
WEBHOOK_ELOPAGE_SECRET: process.env.WEBHOOK_ELOPAGE_SECRET || 'secret', WEBHOOK_ELOPAGE_SECRET: process.env.WEBHOOK_ELOPAGE_SECRET ?? 'secret',
} }
// This is needed by graphql-directive-auth // This is needed by graphql-directive-auth
process.env.APP_SECRET = server.JWT_SECRET process.env.APP_SECRET = server.JWT_SECRET
// Check config version // Check config version
constants.CONFIG_VERSION.CURRENT = process.env.CONFIG_VERSION || constants.CONFIG_VERSION.DEFAULT constants.CONFIG_VERSION.CURRENT = process.env.CONFIG_VERSION ?? constants.CONFIG_VERSION.DEFAULT
if ( if (
![constants.CONFIG_VERSION.EXPECTED, constants.CONFIG_VERSION.DEFAULT].includes( ![constants.CONFIG_VERSION.EXPECTED, constants.CONFIG_VERSION.DEFAULT].includes(
constants.CONFIG_VERSION.CURRENT, constants.CONFIG_VERSION.CURRENT,

View File

@ -1,44 +0,0 @@
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-return */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
import { FederatedCommunity as DbFederatedCommunity } from '@entity/FederatedCommunity'
import { gql } from 'graphql-request'
import { GraphQLGetClient } from '@/federation/client/GraphQLGetClient'
import { LogError } from '@/server/LogError'
import { backendLogger as logger } from '@/server/logger'
export async function requestGetPublicKey(
dbCom: DbFederatedCommunity,
): Promise<string | undefined> {
let endpoint = dbCom.endPoint.endsWith('/') ? dbCom.endPoint : dbCom.endPoint + '/'
endpoint = `${endpoint}${dbCom.apiVersion}/`
logger.info(`requestGetPublicKey with endpoint='${endpoint}'...`)
const graphQLClient = GraphQLGetClient.getInstance(endpoint)
logger.debug(`graphQLClient=${JSON.stringify(graphQLClient)}`)
const query = gql`
query {
getPublicKey {
publicKey
}
}
`
const variables = {}
try {
const { data, errors, extensions, headers, status } = await graphQLClient.rawRequest(
query,
variables,
)
logger.debug(`Response-Data:`, data, errors, extensions, headers, status)
if (data) {
logger.debug(`Response-PublicKey:`, data.getPublicKey.publicKey)
logger.info(`requestGetPublicKey processed successfully`)
return data.getPublicKey.publicKey
}
logger.warn(`requestGetPublicKey processed without response data`)
} catch (err) {
throw new LogError(`Request-Error:`, err)
}
}

View File

@ -1,44 +0,0 @@
/* eslint-disable @typescript-eslint/no-unsafe-return */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
import { FederatedCommunity as DbFederatedCommunity } from '@entity/FederatedCommunity'
import { gql } from 'graphql-request'
import { GraphQLGetClient } from '@/federation/client/GraphQLGetClient'
import { LogError } from '@/server/LogError'
import { backendLogger as logger } from '@/server/logger'
export async function requestGetPublicKey(
dbCom: DbFederatedCommunity,
): Promise<string | undefined> {
let endpoint = dbCom.endPoint.endsWith('/') ? dbCom.endPoint : dbCom.endPoint + '/'
endpoint = `${endpoint}${dbCom.apiVersion}/`
logger.info(`requestGetPublicKey with endpoint='${endpoint}'...`)
const graphQLClient = GraphQLGetClient.getInstance(endpoint)
logger.debug(`graphQLClient=${JSON.stringify(graphQLClient)}`)
const query = gql`
query {
getPublicKey {
publicKey
}
}
`
const variables = {}
try {
const { data, errors, extensions, headers, status } = await graphQLClient.rawRequest(
query,
variables,
)
logger.debug(`Response-Data:`, data, errors, extensions, headers, status)
if (data) {
logger.debug(`Response-PublicKey:`, data.getPublicKey.publicKey)
logger.info(`requestGetPublicKey processed successfully`)
return data.getPublicKey.publicKey
}
logger.warn(`requestGetPublicKey processed without response data`)
} catch (err) {
throw new LogError(`Request-Error:`, err)
}
}

View File

@ -0,0 +1,58 @@
import { FederatedCommunity as DbFederatedCommunity } from '@entity/FederatedCommunity'
import { ApiVersionType } from '@/federation/enum/apiVersionType'
// eslint-disable-next-line camelcase
import { Client_1_0 } from './Client_1_0'
// eslint-disable-next-line camelcase
import { Client_1_1 } from './Client_1_1'
// eslint-disable-next-line camelcase
type FederationClient = Client_1_0 | Client_1_1
interface ClientInstance {
id: number
// eslint-disable-next-line no-use-before-define
client: FederationClient
}
// eslint-disable-next-line @typescript-eslint/no-extraneous-class
export class Client {
private static instanceArray: ClientInstance[] = []
/**
* The Singleton's constructor should always be private to prevent direct
* construction calls with the `new` operator.
*/
// eslint-disable-next-line no-useless-constructor, @typescript-eslint/no-empty-function
private constructor() {}
private static createFederationClient = (dbCom: DbFederatedCommunity) => {
switch (dbCom.apiVersion) {
case ApiVersionType.V1_0:
return new Client_1_0(dbCom)
case ApiVersionType.V1_1:
return new Client_1_1(dbCom)
default:
return null
}
}
/**
* The static method that controls the access to the singleton instance.
*
* This implementation let you subclass the Singleton class while keeping
* just one instance of each subclass around.
*/
public static getInstance(dbCom: DbFederatedCommunity): FederationClient | null {
const instance = Client.instanceArray.find((instance) => instance.id === dbCom.id)
if (instance) {
return instance.client
}
const client = Client.createFederationClient(dbCom)
if (client) {
Client.instanceArray.push({ id: dbCom.id, client } as ClientInstance)
}
return client
}
}

View File

@ -0,0 +1,49 @@
import { FederatedCommunity as DbFederatedCommunity } from '@entity/FederatedCommunity'
import { GraphQLClient } from 'graphql-request'
import { getPublicKey } from '@/federation/query/getPublicKey'
import { backendLogger as logger } from '@/server/logger'
// eslint-disable-next-line camelcase
export class Client_1_0 {
dbCom: DbFederatedCommunity
endpoint: string
client: GraphQLClient
constructor(dbCom: DbFederatedCommunity) {
this.dbCom = dbCom
this.endpoint = `${dbCom.endPoint.endsWith('/') ? dbCom.endPoint : dbCom.endPoint + '/'}${
dbCom.apiVersion
}/`
this.client = new GraphQLClient(this.endpoint, {
method: 'GET',
jsonSerializer: {
parse: JSON.parse,
stringify: JSON.stringify,
},
})
}
getPublicKey = async (): Promise<string | undefined> => {
logger.info('Federation: getPublicKey from endpoint', this.endpoint)
try {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const { data } = await this.client.rawRequest(getPublicKey, {})
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
if (!data?.getPublicKey?.publicKey) {
logger.warn('Federation: getPublicKey without response data from endpoint', this.endpoint)
return
}
logger.info(
'Federation: getPublicKey successful from endpoint',
this.endpoint,
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
data.getPublicKey.publicKey,
)
// eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-member-access
return data.getPublicKey.publicKey
} catch (err) {
logger.warn('Federation: getPublicKey failed for endpoint', this.endpoint)
}
}
}

View File

@ -0,0 +1,5 @@
// eslint-disable-next-line camelcase
import { Client_1_0 } from './Client_1_0'
// eslint-disable-next-line camelcase
export class Client_1_1 extends Client_1_0 {}

View File

@ -1,43 +0,0 @@
import { GraphQLClient } from 'graphql-request'
import { PatchedRequestInit } from 'graphql-request/dist/types'
type ClientInstance = {
url: string
// eslint-disable-next-line no-use-before-define
client: GraphQLGetClient
}
export class GraphQLGetClient extends GraphQLClient {
private static instanceArray: ClientInstance[] = []
/**
* The Singleton's constructor should always be private to prevent direct
* construction calls with the `new` operator.
*/
// eslint-disable-next-line no-useless-constructor
private constructor(url: string, options?: PatchedRequestInit) {
super(url, options)
}
/**
* The static method that controls the access to the singleton instance.
*
* This implementation let you subclass the Singleton class while keeping
* just one instance of each subclass around.
*/
public static getInstance(url: string): GraphQLGetClient {
const instance = GraphQLGetClient.instanceArray.find((instance) => instance.url === url)
if (instance) {
return instance.client
}
const client = new GraphQLGetClient(url, {
method: 'GET',
jsonSerializer: {
parse: JSON.parse,
stringify: JSON.stringify,
},
})
GraphQLGetClient.instanceArray.push({ url, client } as ClientInstance)
return client
}
}

View File

@ -0,0 +1,9 @@
import { gql } from 'graphql-request'
export const getPublicKey = gql`
query {
getPublicKey {
publicKey
}
}
`

View File

@ -84,7 +84,8 @@ describe('validate Communities', () => {
}) })
it('logs requestGetPublicKey for community api 1_0 ', () => { it('logs requestGetPublicKey for community api 1_0 ', () => {
expect(logger.info).toBeCalledWith( expect(logger.info).toBeCalledWith(
`requestGetPublicKey with endpoint='http//localhost:5001/api/1_0/'...`, 'Federation: getPublicKey from endpoint',
'http//localhost:5001/api/1_0/',
) )
}) })
}) })
@ -114,12 +115,14 @@ describe('validate Communities', () => {
}) })
it('logs requestGetPublicKey for community api 1_0 ', () => { it('logs requestGetPublicKey for community api 1_0 ', () => {
expect(logger.info).toBeCalledWith( expect(logger.info).toBeCalledWith(
`requestGetPublicKey with endpoint='http//localhost:5001/api/1_0/'...`, 'Federation: getPublicKey from endpoint',
'http//localhost:5001/api/1_0/',
) )
}) })
it('logs requestGetPublicKey for community api 1_1 ', () => { it('logs requestGetPublicKey for community api 1_1 ', () => {
expect(logger.info).toBeCalledWith( expect(logger.info).toBeCalledWith(
`requestGetPublicKey with endpoint='http//localhost:5001/api/1_1/'...`, 'Federation: getPublicKey from endpoint',
'http//localhost:5001/api/1_1/',
) )
}) })
}) })
@ -152,18 +155,21 @@ describe('validate Communities', () => {
}) })
it('logs requestGetPublicKey for community api 1_0 ', () => { it('logs requestGetPublicKey for community api 1_0 ', () => {
expect(logger.info).toBeCalledWith( expect(logger.info).toBeCalledWith(
`requestGetPublicKey with endpoint='http//localhost:5001/api/1_0/'...`, 'Federation: getPublicKey from endpoint',
'http//localhost:5001/api/1_0/',
) )
}) })
it('logs requestGetPublicKey for community api 1_1 ', () => { it('logs requestGetPublicKey for community api 1_1 ', () => {
expect(logger.info).toBeCalledWith( expect(logger.info).toBeCalledWith(
`requestGetPublicKey with endpoint='http//localhost:5001/api/1_1/'...`, 'Federation: getPublicKey from endpoint',
'http//localhost:5001/api/1_1/',
) )
}) })
it('logs unsupported api for community with api 2_0 ', () => { it('logs unsupported api for community with api 2_0 ', () => {
expect(logger.warn).toBeCalledWith( expect(logger.warn).toBeCalledWith(
`Federation: dbCom: ${dbCom.id} with unsupported apiVersion=2_0; supported versions`, 'Federation: dbCom with unsupported apiVersion',
['1_0', '1_1'], dbCom.endPoint,
'2_0',
) )
}) })
}) })

View File

@ -3,13 +3,9 @@
import { IsNull } from '@dbTools/typeorm' import { IsNull } from '@dbTools/typeorm'
import { FederatedCommunity as DbFederatedCommunity } from '@entity/FederatedCommunity' import { FederatedCommunity as DbFederatedCommunity } from '@entity/FederatedCommunity'
import { LogError } from '@/server/LogError'
import { backendLogger as logger } from '@/server/logger' import { backendLogger as logger } from '@/server/logger'
// eslint-disable-next-line camelcase import { Client } from './client/Client'
import { requestGetPublicKey as v1_0_requestGetPublicKey } from './client/1_0/FederationClient'
// eslint-disable-next-line camelcase
import { requestGetPublicKey as v1_1_requestGetPublicKey } from './client/1_1/FederationClient'
import { ApiVersionType } from './enum/apiVersionType' import { ApiVersionType } from './enum/apiVersionType'
export function startValidateCommunities(timerInterval: number): void { export function startValidateCommunities(timerInterval: number): void {
@ -36,56 +32,25 @@ export async function validateCommunities(): Promise<void> {
logger.debug('Federation: dbCom', dbCom) logger.debug('Federation: dbCom', dbCom)
const apiValueStrings: string[] = Object.values(ApiVersionType) const apiValueStrings: string[] = Object.values(ApiVersionType)
logger.debug(`suppported ApiVersions=`, apiValueStrings) logger.debug(`suppported ApiVersions=`, apiValueStrings)
if (apiValueStrings.includes(dbCom.apiVersion)) { if (!apiValueStrings.includes(dbCom.apiVersion)) {
logger.debug( logger.warn('Federation: dbCom with unsupported apiVersion', dbCom.endPoint, dbCom.apiVersion)
`Federation: validate publicKey for dbCom: ${dbCom.id} with apiVersion=${dbCom.apiVersion}`, continue
) }
try { try {
const pubKey = await invokeVersionedRequestGetPublicKey(dbCom) const client = Client.getInstance(dbCom)
logger.info( const pubKey = await client?.getPublicKey()
'Federation: received publicKey from endpoint', if (pubKey && pubKey === dbCom.publicKey.toString()) {
await DbFederatedCommunity.update({ id: dbCom.id }, { verifiedAt: new Date() })
logger.info('Federation: verified community', dbCom)
} else {
logger.warn(
'Federation: received not matching publicKey:',
pubKey, pubKey,
`${dbCom.endPoint}/${dbCom.apiVersion}`, dbCom.publicKey.toString(),
) )
if (pubKey && pubKey === dbCom.publicKey.toString()) {
logger.info(`Federation: matching publicKey: ${pubKey}`)
await DbFederatedCommunity.update({ id: dbCom.id }, { verifiedAt: new Date() })
logger.debug(`Federation: updated dbCom: ${JSON.stringify(dbCom)}`)
} else {
logger.warn(
`Federation: received not matching publicKey -> received: ${
pubKey || 'null'
}, expected: ${dbCom.publicKey.toString()} `,
)
// DbCommunity.delete({ id: dbCom.id })
}
} catch (err) {
if (!isLogError(err)) {
logger.error(`Error:`, err)
}
} }
} else { } catch (err) {
logger.warn( logger.error(`Error:`, err)
`Federation: dbCom: ${dbCom.id} with unsupported apiVersion=${dbCom.apiVersion}; supported versions`,
apiValueStrings,
)
} }
} }
} }
function isLogError(err: unknown) {
return err instanceof LogError
}
async function invokeVersionedRequestGetPublicKey(
dbCom: DbFederatedCommunity,
): Promise<string | undefined> {
switch (dbCom.apiVersion) {
case ApiVersionType.V1_0:
return v1_0_requestGetPublicKey(dbCom)
case ApiVersionType.V1_1:
return v1_1_requestGetPublicKey(dbCom)
default:
return undefined
}
}

View File

@ -8,6 +8,9 @@ export class UpdateUserInfosArgs {
@Field({ nullable: true }) @Field({ nullable: true })
lastName?: string lastName?: string
@Field({ nullable: true })
alias?: string
@Field({ nullable: true }) @Field({ nullable: true })
language?: string language?: string

View File

@ -12,7 +12,7 @@ export const isAuthorized: AuthChecker<Context> = async ({ context }, rights) =>
context.role = ROLE_UNAUTHORIZED // unauthorized user context.role = ROLE_UNAUTHORIZED // unauthorized user
// is rights an inalienable right? // is rights an inalienable right?
if ((<RIGHTS[]>rights).reduce((acc, right) => acc && INALIENABLE_RIGHTS.includes(right), true)) if ((rights as RIGHTS[]).reduce((acc, right) => acc && INALIENABLE_RIGHTS.includes(right), true))
return true return true
// Do we have a token? // Do we have a token?
@ -43,7 +43,7 @@ export const isAuthorized: AuthChecker<Context> = async ({ context }, rights) =>
} }
// check for correct rights // check for correct rights
const missingRights = (<RIGHTS[]>rights).filter((right) => !context.role?.hasRight(right)) const missingRights = (rights as RIGHTS[]).filter((right) => !context.role?.hasRight(right))
if (missingRights.length !== 0) { if (missingRights.length !== 0) {
throw new LogError('401 Unauthorized') throw new LogError('401 Unauthorized')
} }

View File

@ -10,7 +10,7 @@ export class Balance {
linkCount: number linkCount: number
}) { }) {
this.balance = data.balance this.balance = data.balance
this.balanceGDT = data.balanceGDT || null this.balanceGDT = data.balanceGDT ?? null
this.count = data.count this.count = data.count
this.linkCount = data.linkCount this.linkCount = data.linkCount
} }

View File

@ -43,13 +43,12 @@ export class Transaction {
this.memo = transaction.memo this.memo = transaction.memo
this.creationDate = transaction.creationDate this.creationDate = transaction.creationDate
this.linkedUser = linkedUser this.linkedUser = linkedUser
this.linkedTransactionId = transaction.linkedTransactionId || null this.linkedTransactionId = transaction.linkedTransactionId ?? null
this.linkId = transaction.contribution this.linkId = transaction.contribution
? transaction.contribution.contributionLinkId ? transaction.contribution.contributionLinkId
: transaction.transactionLinkId || null : transaction.transactionLinkId ?? null
this.previousBalance = this.previousBalance =
(transaction.previousTransaction && transaction.previousTransaction?.balance.toDecimalPlaces(2, Decimal.ROUND_DOWN) ??
transaction.previousTransaction.balance.toDecimalPlaces(2, Decimal.ROUND_DOWN)) ||
new Decimal(0) new Decimal(0)
} }

View File

@ -2,7 +2,6 @@ import { User as dbUser } from '@entity/User'
import { ObjectType, Field, Int } from 'type-graphql' import { ObjectType, Field, Int } from 'type-graphql'
import { KlickTipp } from './KlickTipp' import { KlickTipp } from './KlickTipp'
import { UserContact } from './UserContact'
@ObjectType() @ObjectType()
export class User { export class User {
@ -10,10 +9,7 @@ export class User {
this.id = user.id this.id = user.id
this.gradidoID = user.gradidoID this.gradidoID = user.gradidoID
this.alias = user.alias this.alias = user.alias
this.emailId = user.emailId
if (user.emailContact) { if (user.emailContact) {
this.email = user.emailContact.email
this.emailContact = new UserContact(user.emailContact)
this.emailChecked = user.emailContact.emailChecked this.emailChecked = user.emailContact.emailChecked
} }
this.firstName = user.firstName this.firstName = user.firstName
@ -38,16 +34,6 @@ export class User {
@Field(() => String, { nullable: true }) @Field(() => String, { nullable: true })
alias: string | null alias: string | null
@Field(() => Int, { nullable: true })
emailId: number | null
// TODO privacy issue here
@Field(() => String, { nullable: true })
email: string | null
@Field(() => UserContact)
emailContact: UserContact
@Field(() => String, { nullable: true }) @Field(() => String, { nullable: true })
firstName: string | null firstName: string | null

View File

@ -70,7 +70,10 @@ export class BalanceResolver {
now, now,
) )
logger.info( logger.info(
`calculatedDecay(balance=${lastTransaction.balance}, balanceDate=${lastTransaction.balanceDate})=${calculatedDecay}`, 'calculatedDecay',
lastTransaction.balance,
lastTransaction.balanceDate,
calculatedDecay,
) )
// The final balance is reduced by the link amount withheld // The final balance is reduced by the link amount withheld
@ -96,9 +99,7 @@ export class BalanceResolver {
count, count,
linkCount, linkCount,
}) })
logger.info( logger.info('new Balance', balance, balanceGDT, count, linkCount, newBalance)
`new Balance(balance=${balance}, balanceGDT=${balanceGDT}, count=${count}, linkCount=${linkCount}) = ${newBalance}`,
)
return newBalance return newBalance
} }

View File

@ -66,7 +66,7 @@ let testEnv: {
query: ApolloServerTestClient['query'] query: ApolloServerTestClient['query']
con: Connection con: Connection
} }
let creation: Contribution | void let creation: Contribution | null
let admin: User let admin: User
let pendingContribution: any let pendingContribution: any
let inProgressContribution: any let inProgressContribution: any
@ -2071,7 +2071,7 @@ describe('ContributionResolver', () => {
mutate({ mutate({
mutation: updateContribution, mutation: updateContribution,
variables: { variables: {
contributionId: (adminContribution && adminContribution.id) || -1, contributionId: adminContribution?.id ?? -1,
amount: 100.0, amount: 100.0,
memo: 'Test Test Test', memo: 'Test Test Test',
creationDate: new Date().toString(), creationDate: new Date().toString(),
@ -2565,8 +2565,8 @@ describe('ContributionResolver', () => {
}) })
describe('confirm two creations one after the other quickly', () => { describe('confirm two creations one after the other quickly', () => {
let c1: Contribution | void let c1: Contribution | null
let c2: Contribution | void let c2: Contribution | null
beforeAll(async () => { beforeAll(async () => {
const now = new Date() const now = new Date()

View File

@ -43,6 +43,7 @@ import { LogError } from '@/server/LogError'
import { backendLogger as logger } from '@/server/logger' import { backendLogger as logger } from '@/server/logger'
import { calculateDecay } from '@/util/decay' import { calculateDecay } from '@/util/decay'
import { TRANSACTIONS_LOCK } from '@/util/TRANSACTIONS_LOCK' import { TRANSACTIONS_LOCK } from '@/util/TRANSACTIONS_LOCK'
import { fullName } from '@/util/utilities'
import { MEMO_MAX_CHARS, MEMO_MIN_CHARS } from './const/const' import { MEMO_MAX_CHARS, MEMO_MIN_CHARS } from './const/const'
import { import {
@ -269,7 +270,7 @@ export class ContributionResolver {
withDeleted: true, withDeleted: true,
relations: ['user'], relations: ['user'],
}) })
if (!emailContact || !emailContact.user) { if (!emailContact?.user) {
throw new LogError('Could not find user', email) throw new LogError('Could not find user', email)
} }
if (emailContact.deletedAt || emailContact.user.deletedAt) { if (emailContact.deletedAt || emailContact.user.deletedAt) {
@ -500,6 +501,8 @@ export class ContributionResolver {
transaction.typeId = TransactionTypeId.CREATION transaction.typeId = TransactionTypeId.CREATION
transaction.memo = contribution.memo transaction.memo = contribution.memo
transaction.userId = contribution.userId transaction.userId = contribution.userId
transaction.userGradidoID = user.gradidoID
transaction.userName = fullName(user.firstName, user.lastName)
transaction.previous = lastTransaction ? lastTransaction.id : null transaction.previous = lastTransaction ? lastTransaction.id : null
transaction.amount = contribution.amount transaction.amount = contribution.amount
transaction.creationDate = contribution.contributionDate transaction.creationDate = contribution.contributionDate

View File

@ -1,6 +1,6 @@
import { Resolver, Authorized, Mutation, Ctx } from 'type-graphql' import { Resolver, Authorized, Mutation, Ctx } from 'type-graphql'
import { unsubscribe, klicktippSignIn } from '@/apis/KlicktippController' import { unsubscribe, subscribe } from '@/apis/KlicktippController'
import { RIGHTS } from '@/auth/RIGHTS' import { RIGHTS } from '@/auth/RIGHTS'
import { EVENT_NEWSLETTER_SUBSCRIBE, EVENT_NEWSLETTER_UNSUBSCRIBE } from '@/event/Events' import { EVENT_NEWSLETTER_SUBSCRIBE, EVENT_NEWSLETTER_UNSUBSCRIBE } from '@/event/Events'
import { Context, getUser } from '@/server/context' import { Context, getUser } from '@/server/context'
@ -20,6 +20,6 @@ export class KlicktippResolver {
async subscribeNewsletter(@Ctx() context: Context): Promise<boolean> { async subscribeNewsletter(@Ctx() context: Context): Promise<boolean> {
const user = getUser(context) const user = getUser(context)
await EVENT_NEWSLETTER_SUBSCRIBE(user) await EVENT_NEWSLETTER_SUBSCRIBE(user)
return klicktippSignIn(user.emailContact.email, user.language) return subscribe(user.emailContact.email, user.language)
} }
} }

View File

@ -817,8 +817,8 @@ describe('TransactionLinkResolver', () => {
const bibisTransaktionLinks = transactionLinks.filter( const bibisTransaktionLinks = transactionLinks.filter(
(transactionLink) => transactionLink.email === 'bibi@bloxberg.de', (transactionLink) => transactionLink.email === 'bibi@bloxberg.de',
) )
for (let i = 0; i < bibisTransaktionLinks.length; i++) { for (const bibisTransaktionLink of bibisTransaktionLinks) {
await transactionLinkFactory(testEnv, bibisTransaktionLinks[i]) await transactionLinkFactory(testEnv, bibisTransaktionLink)
} }
// admin: only now log in // admin: only now log in
@ -1040,6 +1040,7 @@ describe('TransactionLinkResolver', () => {
}) })
it('returns a string that ends with the hex value of date', () => { it('returns a string that ends with the hex value of date', () => {
// eslint-disable-next-line security/detect-non-literal-regexp
const regexp = new RegExp(date.getTime().toString(16) + '$') const regexp = new RegExp(date.getTime().toString(16) + '$')
expect(transactionLinkCode(date)).toEqual(expect.stringMatching(regexp)) expect(transactionLinkCode(date)).toEqual(expect.stringMatching(regexp))
}) })

View File

@ -34,6 +34,7 @@ import { LogError } from '@/server/LogError'
import { backendLogger as logger } from '@/server/logger' import { backendLogger as logger } from '@/server/logger'
import { calculateDecay } from '@/util/decay' import { calculateDecay } from '@/util/decay'
import { TRANSACTIONS_LOCK } from '@/util/TRANSACTIONS_LOCK' import { TRANSACTIONS_LOCK } from '@/util/TRANSACTIONS_LOCK'
import { fullName } from '@/util/utilities'
import { calculateBalance } from '@/util/validate' import { calculateBalance } from '@/util/validate'
import { executeTransaction } from './TransactionResolver' import { executeTransaction } from './TransactionResolver'
@ -146,7 +147,7 @@ export class TransactionLinkResolver {
const transactionLink = await DbTransactionLink.findOneOrFail({ code }, { withDeleted: true }) const transactionLink = await DbTransactionLink.findOneOrFail({ code }, { withDeleted: true })
const user = await DbUser.findOneOrFail({ id: transactionLink.userId }) const user = await DbUser.findOneOrFail({ id: transactionLink.userId })
let redeemedBy: User | null = null let redeemedBy: User | null = null
if (transactionLink && transactionLink.redeemedBy) { if (transactionLink?.redeemedBy) {
redeemedBy = new User(await DbUser.findOneOrFail({ id: transactionLink.redeemedBy })) redeemedBy = new User(await DbUser.findOneOrFail({ id: transactionLink.redeemedBy }))
} }
return new TransactionLink(transactionLink, new User(user), redeemedBy) return new TransactionLink(transactionLink, new User(user), redeemedBy)
@ -266,6 +267,8 @@ export class TransactionLinkResolver {
transaction.typeId = TransactionTypeId.CREATION transaction.typeId = TransactionTypeId.CREATION
transaction.memo = contribution.memo transaction.memo = contribution.memo
transaction.userId = contribution.userId transaction.userId = contribution.userId
transaction.userGradidoID = user.gradidoID
transaction.userName = fullName(user.firstName, user.lastName)
transaction.previous = lastTransaction ? lastTransaction.id : null transaction.previous = lastTransaction ? lastTransaction.id : null
transaction.amount = contribution.amount transaction.amount = contribution.amount
transaction.creationDate = contribution.contributionDate transaction.creationDate = contribution.contributionDate

View File

@ -20,12 +20,15 @@ import {
login, login,
sendCoins, sendCoins,
} from '@/seeds/graphql/mutations' } from '@/seeds/graphql/mutations'
import { transactionsQuery } from '@/seeds/graphql/queries'
import { bobBaumeister } from '@/seeds/users/bob-baumeister' import { bobBaumeister } from '@/seeds/users/bob-baumeister'
import { garrickOllivander } from '@/seeds/users/garrick-ollivander' import { garrickOllivander } from '@/seeds/users/garrick-ollivander'
import { peterLustig } from '@/seeds/users/peter-lustig' import { peterLustig } from '@/seeds/users/peter-lustig'
import { stephenHawking } from '@/seeds/users/stephen-hawking' import { stephenHawking } from '@/seeds/users/stephen-hawking'
let mutate: ApolloServerTestClient['mutate'], con: Connection let mutate: ApolloServerTestClient['mutate'], con: Connection
let query: ApolloServerTestClient['query']
let testEnv: { let testEnv: {
mutate: ApolloServerTestClient['mutate'] mutate: ApolloServerTestClient['mutate']
query: ApolloServerTestClient['query'] query: ApolloServerTestClient['query']
@ -35,6 +38,7 @@ let testEnv: {
beforeAll(async () => { beforeAll(async () => {
testEnv = await testEnvironment(logger) testEnv = await testEnvironment(logger)
mutate = testEnv.mutate mutate = testEnv.mutate
query = testEnv.query
con = testEnv.con con = testEnv.con
await cleanDB() await cleanDB()
}) })
@ -442,3 +446,42 @@ describe('send coins', () => {
}) })
}) })
}) })
describe('transactionList', () => {
describe('unauthenticated', () => {
it('throws an error', async () => {
await expect(query({ query: transactionsQuery })).resolves.toMatchObject({
errors: [new GraphQLError('401 Unauthorized')],
})
})
})
describe('authenticated', () => {
describe('no transactions', () => {
beforeAll(async () => {
await userFactory(testEnv, bobBaumeister)
await mutate({
mutation: login,
variables: {
email: 'bob@baumeister.de',
password: 'Aa12345_',
},
})
})
it('has no transactions and balance 0', async () => {
await expect(query({ query: transactionsQuery })).resolves.toMatchObject({
data: {
transactionList: {
balance: expect.objectContaining({
balance: expect.decimalEqual(0),
}),
transactions: [],
},
},
errors: undefined,
})
})
})
})
})

View File

@ -29,6 +29,7 @@ import { LogError } from '@/server/LogError'
import { backendLogger as logger } from '@/server/logger' import { backendLogger as logger } from '@/server/logger'
import { communityUser } from '@/util/communityUser' import { communityUser } from '@/util/communityUser'
import { TRANSACTIONS_LOCK } from '@/util/TRANSACTIONS_LOCK' import { TRANSACTIONS_LOCK } from '@/util/TRANSACTIONS_LOCK'
import { fullName } from '@/util/utilities'
import { calculateBalance } from '@/util/validate' import { calculateBalance } from '@/util/validate'
import { virtualLinkTransaction, virtualDecayTransaction } from '@/util/virtualTransactions' import { virtualLinkTransaction, virtualDecayTransaction } from '@/util/virtualTransactions'
@ -48,9 +49,7 @@ export const executeTransaction = async (
// acquire lock // acquire lock
const releaseLock = await TRANSACTIONS_LOCK.acquire() const releaseLock = await TRANSACTIONS_LOCK.acquire()
try { try {
logger.info( logger.info('executeTransaction', amount, memo, sender, recipient)
`executeTransaction(amount=${amount}, memo=${memo}, sender=${sender}, recipient=${recipient})...`,
)
if (sender.id === recipient.id) { if (sender.id === recipient.id) {
throw new LogError('Sender and Recipient are the same', sender.id) throw new LogError('Sender and Recipient are the same', sender.id)
@ -87,7 +86,11 @@ export const executeTransaction = async (
transactionSend.typeId = TransactionTypeId.SEND transactionSend.typeId = TransactionTypeId.SEND
transactionSend.memo = memo transactionSend.memo = memo
transactionSend.userId = sender.id transactionSend.userId = sender.id
transactionSend.userGradidoID = sender.gradidoID
transactionSend.userName = fullName(sender.firstName, sender.lastName)
transactionSend.linkedUserId = recipient.id transactionSend.linkedUserId = recipient.id
transactionSend.linkedUserGradidoID = recipient.gradidoID
transactionSend.linkedUserName = fullName(recipient.firstName, recipient.lastName)
transactionSend.amount = amount.mul(-1) transactionSend.amount = amount.mul(-1)
transactionSend.balance = sendBalance.balance transactionSend.balance = sendBalance.balance
transactionSend.balanceDate = receivedCallDate transactionSend.balanceDate = receivedCallDate
@ -103,7 +106,11 @@ export const executeTransaction = async (
transactionReceive.typeId = TransactionTypeId.RECEIVE transactionReceive.typeId = TransactionTypeId.RECEIVE
transactionReceive.memo = memo transactionReceive.memo = memo
transactionReceive.userId = recipient.id transactionReceive.userId = recipient.id
transactionReceive.userGradidoID = recipient.gradidoID
transactionReceive.userName = fullName(recipient.firstName, recipient.lastName)
transactionReceive.linkedUserId = sender.id transactionReceive.linkedUserId = sender.id
transactionReceive.linkedUserGradidoID = sender.gradidoID
transactionReceive.linkedUserName = fullName(sender.firstName, sender.lastName)
transactionReceive.amount = amount transactionReceive.amount = amount
const receiveBalance = await calculateBalance(recipient.id, amount, receivedCallDate) const receiveBalance = await calculateBalance(recipient.id, amount, receivedCallDate)
transactionReceive.balance = receiveBalance ? receiveBalance.balance : amount transactionReceive.balance = receiveBalance ? receiveBalance.balance : amount
@ -119,10 +126,10 @@ export const executeTransaction = async (
// Save linked transaction id for send // Save linked transaction id for send
transactionSend.linkedTransactionId = transactionReceive.id transactionSend.linkedTransactionId = transactionReceive.id
await queryRunner.manager.update(dbTransaction, { id: transactionSend.id }, transactionSend) await queryRunner.manager.update(dbTransaction, { id: transactionSend.id }, transactionSend)
logger.debug(`send Transaction updated: ${transactionSend}`) logger.debug('send Transaction updated', transactionSend)
if (transactionLink) { if (transactionLink) {
logger.info(`transactionLink: ${transactionLink}`) logger.info('transactionLink', transactionLink)
transactionLink.redeemedAt = receivedCallDate transactionLink.redeemedAt = receivedCallDate
transactionLink.redeemedBy = recipient.id transactionLink.redeemedBy = recipient.id
await queryRunner.manager.update( await queryRunner.manager.update(
@ -271,8 +278,8 @@ export class TransactionResolver {
sumAmount.mul(-1), sumAmount.mul(-1),
sumHoldAvailableAmount.mul(-1), sumHoldAvailableAmount.mul(-1),
sumHoldAvailableAmount.minus(sumAmount.toString()).mul(-1), sumHoldAvailableAmount.minus(sumAmount.toString()).mul(-1),
firstDate || now, firstDate ?? now,
lastDate || now, lastDate ?? now,
self, self,
(userTransactions.length && userTransactions[0].balance) || new Decimal(0), (userTransactions.length && userTransactions[0].balance) || new Decimal(0),
), ),
@ -325,9 +332,7 @@ export class TransactionResolver {
} }
await executeTransaction(amount, memo, senderUser, recipientUser) await executeTransaction(amount, memo, senderUser, recipientUser)
logger.info( logger.info('successful executeTransaction', amount, memo, senderUser, recipientUser)
`successful executeTransaction(amount=${amount}, memo=${memo}, senderUser=${senderUser}, recipientUser=${recipientUser})`,
)
return true return true
} }
} }

View File

@ -20,6 +20,7 @@ import { ContributionLink } from '@model/ContributionLink'
import { testEnvironment, headerPushMock, resetToken, cleanDB } from '@test/helpers' import { testEnvironment, headerPushMock, resetToken, cleanDB } from '@test/helpers'
import { logger, i18n as localization } from '@test/testSetup' import { logger, i18n as localization } from '@test/testSetup'
import { subscribe } from '@/apis/KlicktippController'
import { CONFIG } from '@/config' import { CONFIG } from '@/config'
import { import {
sendAccountActivationEmail, sendAccountActivationEmail,
@ -52,6 +53,7 @@ import {
searchAdminUsers, searchAdminUsers,
searchUsers, searchUsers,
user as userQuery, user as userQuery,
checkUsername,
} from '@/seeds/graphql/queries' } from '@/seeds/graphql/queries'
import { bibiBloxberg } from '@/seeds/users/bibi-bloxberg' import { bibiBloxberg } from '@/seeds/users/bibi-bloxberg'
import { bobBaumeister } from '@/seeds/users/bob-baumeister' import { bobBaumeister } from '@/seeds/users/bob-baumeister'
@ -61,8 +63,6 @@ import { stephenHawking } from '@/seeds/users/stephen-hawking'
import { printTimeDuration } from '@/util/time' import { printTimeDuration } from '@/util/time'
import { objectValuesToArray } from '@/util/utilities' import { objectValuesToArray } from '@/util/utilities'
// import { klicktippSignIn } from '@/apis/KlicktippController'
jest.mock('@/emails/sendEmailVariants', () => { jest.mock('@/emails/sendEmailVariants', () => {
const originalModule = jest.requireActual('@/emails/sendEmailVariants') const originalModule = jest.requireActual('@/emails/sendEmailVariants')
return { return {
@ -76,15 +76,13 @@ jest.mock('@/emails/sendEmailVariants', () => {
} }
}) })
/*
jest.mock('@/apis/KlicktippController', () => { jest.mock('@/apis/KlicktippController', () => {
return { return {
__esModule: true, __esModule: true,
klicktippSignIn: jest.fn(), subscribe: jest.fn(),
getKlickTippUser: jest.fn(),
} }
}) })
*/
let admin: User let admin: User
let user: User let user: User
@ -556,16 +554,14 @@ describe('UserResolver', () => {
expect(newUser.password.toString()).toEqual(encryptedPass.toString()) expect(newUser.password.toString()).toEqual(encryptedPass.toString())
}) })
/*
it('calls the klicktipp API', () => { it('calls the klicktipp API', () => {
expect(klicktippSignIn).toBeCalledWith( expect(subscribe).toBeCalledWith(
user[0].email, newUser.emailContact.email,
user[0].language, newUser.language,
user[0].firstName, newUser.firstName,
user[0].lastName, newUser.lastName,
) )
}) })
*/
it('returns true', () => { it('returns true', () => {
expect(result).toBeTruthy() expect(result).toBeTruthy()
@ -680,7 +676,6 @@ describe('UserResolver', () => {
expect.objectContaining({ expect.objectContaining({
data: { data: {
login: { login: {
email: 'bibi@bloxberg.de',
firstName: 'Bibi', firstName: 'Bibi',
hasElopage: false, hasElopage: false,
id: expect.any(Number), id: expect.any(Number),
@ -953,7 +948,6 @@ describe('UserResolver', () => {
expect.objectContaining({ expect.objectContaining({
data: { data: {
verifyLogin: { verifyLogin: {
email: 'bibi@bloxberg.de',
firstName: 'Bibi', firstName: 'Bibi',
lastName: 'Bloxberg', lastName: 'Bloxberg',
language: 'de', language: 'de',
@ -1205,6 +1199,28 @@ describe('UserResolver', () => {
}) })
}) })
describe('alias', () => {
beforeEach(() => {
jest.clearAllMocks()
})
describe('valid alias', () => {
it('updates the user in DB', async () => {
await mutate({
mutation: updateUserInfos,
variables: {
alias: 'bibi_Bloxberg',
},
})
await expect(User.findOne()).resolves.toEqual(
expect.objectContaining({
alias: 'bibi_Bloxberg',
}),
)
})
})
})
describe('language is not valid', () => { describe('language is not valid', () => {
it('throws an error', async () => { it('throws an error', async () => {
jest.clearAllMocks() jest.clearAllMocks()
@ -1310,7 +1326,7 @@ describe('UserResolver', () => {
expect.objectContaining({ expect.objectContaining({
data: { data: {
login: expect.objectContaining({ login: expect.objectContaining({
email: 'bibi@bloxberg.de', firstName: 'Benjamin',
}), }),
}, },
}), }),
@ -1457,7 +1473,6 @@ describe('UserResolver', () => {
expect.objectContaining({ expect.objectContaining({
data: { data: {
login: { login: {
email: 'bibi@bloxberg.de',
firstName: 'Bibi', firstName: 'Bibi',
hasElopage: false, hasElopage: false,
id: expect.any(Number), id: expect.any(Number),
@ -2428,6 +2443,34 @@ describe('UserResolver', () => {
}) })
}) })
}) })
describe('check username', () => {
describe('reserved alias', () => {
it('returns false', async () => {
await expect(
query({ query: checkUsername, variables: { username: 'root' } }),
).resolves.toMatchObject({
data: {
checkUsername: false,
},
errors: undefined,
})
})
})
describe('valid alias', () => {
it('returns true', async () => {
await expect(
query({ query: checkUsername, variables: { username: 'valid' } }),
).resolves.toMatchObject({
data: {
checkUsername: true,
},
errors: undefined,
})
})
})
})
}) })
describe('printTimeDuration', () => { describe('printTimeDuration', () => {

View File

@ -35,7 +35,7 @@ import { User } from '@model/User'
import { UserAdmin, SearchUsersResult } from '@model/UserAdmin' import { UserAdmin, SearchUsersResult } from '@model/UserAdmin'
import { UserRepository } from '@repository/User' import { UserRepository } from '@repository/User'
import { klicktippSignIn } from '@/apis/KlicktippController' import { subscribe } from '@/apis/KlicktippController'
import { encode } from '@/auth/JWT' import { encode } from '@/auth/JWT'
import { RIGHTS } from '@/auth/RIGHTS' import { RIGHTS } from '@/auth/RIGHTS'
import { CONFIG } from '@/config' import { CONFIG } from '@/config'
@ -73,6 +73,7 @@ import { getTimeDurationObject, printTimeDuration } from '@/util/time'
import { FULL_CREATION_AVAILABLE } from './const/const' import { FULL_CREATION_AVAILABLE } from './const/const'
import { getUserCreations } from './util/creations' import { getUserCreations } from './util/creations'
import { findUserByIdentifier } from './util/findUserByIdentifier' import { findUserByIdentifier } from './util/findUserByIdentifier'
import { validateAlias } from './util/validateAlias'
// eslint-disable-next-line @typescript-eslint/no-var-requires, import/no-commonjs // eslint-disable-next-line @typescript-eslint/no-var-requires, import/no-commonjs
const random = require('random-bigint') const random = require('random-bigint')
@ -94,7 +95,7 @@ const newEmailContact = (email: string, userId: number): DbUserContact => {
emailContact.emailChecked = false emailContact.emailChecked = false
emailContact.emailOptInTypeId = OptInType.EMAIL_OPT_IN_REGISTER emailContact.emailOptInTypeId = OptInType.EMAIL_OPT_IN_REGISTER
emailContact.emailVerificationCode = random(64) emailContact.emailVerificationCode = random(64)
logger.debug(`newEmailContact...successful: ${emailContact}`) logger.debug('newEmailContact...successful', emailContact)
return emailContact return emailContact
} }
@ -130,7 +131,7 @@ export class UserResolver {
// Elopage Status & Stored PublisherId // Elopage Status & Stored PublisherId
user.hasElopage = await this.hasElopage(context) user.hasElopage = await this.hasElopage(context)
logger.debug(`verifyLogin... successful: ${user.firstName}.${user.lastName}, ${user.email}`) logger.debug(`verifyLogin... successful: ${user.firstName}.${user.lastName}`)
return user return user
} }
@ -225,7 +226,7 @@ export class UserResolver {
email = email.trim().toLowerCase() email = email.trim().toLowerCase()
if (await checkEmailExists(email)) { if (await checkEmailExists(email)) {
const foundUser = await findUserByEmail(email) const foundUser = await findUserByEmail(email)
logger.info(`DbUser.findOne(email=${email}) = ${foundUser}`) logger.info('DbUser.findOne', email, foundUser)
if (foundUser) { if (foundUser) {
// ATTENTION: this logger-message will be exactly expected during tests, next line // ATTENTION: this logger-message will be exactly expected during tests, next line
@ -238,7 +239,6 @@ export class UserResolver {
const user = new User(communityDbUser) const user = new User(communityDbUser)
user.id = sodium.randombytes_random() % (2048 * 16) // TODO: for a better faking derive id from email so that it will be always the same id when the same email comes in? user.id = sodium.randombytes_random() % (2048 * 16) // TODO: for a better faking derive id from email so that it will be always the same id when the same email comes in?
user.gradidoID = uuidv4() user.gradidoID = uuidv4()
user.email = email
user.firstName = firstName user.firstName = firstName
user.lastName = lastName user.lastName = lastName
user.language = language user.language = language
@ -276,7 +276,7 @@ export class UserResolver {
dbUser.firstName = firstName dbUser.firstName = firstName
dbUser.lastName = lastName dbUser.lastName = lastName
dbUser.language = language dbUser.language = language
dbUser.publisherId = publisherId || 0 dbUser.publisherId = publisherId ?? 0
dbUser.passwordEncryptionType = PasswordEncryptionType.NO_PASSWORD dbUser.passwordEncryptionType = PasswordEncryptionType.NO_PASSWORD
logger.debug('new dbUser', dbUser) logger.debug('new dbUser', dbUser)
if (redeemCode) { if (redeemCode) {
@ -383,7 +383,7 @@ export class UserResolver {
throw new LogError('Unable to save email verification code', user.emailContact) throw new LogError('Unable to save email verification code', user.emailContact)
}) })
logger.info(`optInCode for ${email}=${user.emailContact}`) logger.info('optInCode for', email, user.emailContact)
void sendResetPasswordEmail({ void sendResetPasswordEmail({
firstName: user.firstName, firstName: user.firstName,
@ -469,9 +469,9 @@ export class UserResolver {
// TODO do we always signUp the user? How to handle things with old users? // TODO do we always signUp the user? How to handle things with old users?
if (userContact.emailOptInTypeId === OptInType.EMAIL_OPT_IN_REGISTER) { if (userContact.emailOptInTypeId === OptInType.EMAIL_OPT_IN_REGISTER) {
try { try {
await klicktippSignIn(userContact.email, user.language, user.firstName, user.lastName) await subscribe(userContact.email, user.language, user.firstName, user.lastName)
logger.debug( logger.debug(
`klicktippSignIn(${userContact.email}, ${user.language}, ${user.firstName}, ${user.lastName})`, `subscribe(${userContact.email}, ${user.language}, ${user.firstName}, ${user.lastName})`,
) )
} catch (e) { } catch (e) {
logger.error('Error subscribing to klicktipp', e) logger.error('Error subscribing to klicktipp', e)
@ -487,7 +487,7 @@ export class UserResolver {
async queryOptIn(@Arg('optIn') optIn: string): Promise<boolean> { async queryOptIn(@Arg('optIn') optIn: string): Promise<boolean> {
logger.info(`queryOptIn(${optIn})...`) logger.info(`queryOptIn(${optIn})...`)
const userContact = await DbUserContact.findOneOrFail({ emailVerificationCode: optIn }) const userContact = await DbUserContact.findOneOrFail({ emailVerificationCode: optIn })
logger.debug(`found optInCode=${userContact}`) logger.debug('found optInCode', userContact)
// Code is only valid for `CONFIG.EMAIL_CODE_VALID_TIME` minutes // Code is only valid for `CONFIG.EMAIL_CODE_VALID_TIME` minutes
if (!isEmailVerificationCodeValid(userContact.updatedAt || userContact.createdAt)) { if (!isEmailVerificationCodeValid(userContact.updatedAt || userContact.createdAt)) {
throw new LogError( throw new LogError(
@ -498,6 +498,17 @@ export class UserResolver {
return true return true
} }
@Authorized([RIGHTS.CHECK_USERNAME])
@Query(() => Boolean)
async checkUsername(@Arg('username') username: string): Promise<boolean> {
try {
await validateAlias(username)
return true
} catch {
return false
}
}
@Authorized([RIGHTS.UPDATE_USER_INFOS]) @Authorized([RIGHTS.UPDATE_USER_INFOS])
@Mutation(() => Boolean) @Mutation(() => Boolean)
async updateUserInfos( async updateUserInfos(
@ -505,6 +516,7 @@ export class UserResolver {
{ {
firstName, firstName,
lastName, lastName,
alias,
language, language,
password, password,
passwordNew, passwordNew,
@ -524,6 +536,10 @@ export class UserResolver {
user.lastName = lastName user.lastName = lastName
} }
if (alias && (await validateAlias(alias))) {
user.alias = alias
}
if (language) { if (language) {
if (!isLanguage(language)) { if (!isLanguage(language)) {
throw new LogError('Given language is not a valid language', language) throw new LogError('Given language is not a valid language', language)
@ -587,7 +603,7 @@ export class UserResolver {
logger.info(`hasElopage()...`) logger.info(`hasElopage()...`)
const userEntity = getUser(context) const userEntity = getUser(context)
const elopageBuys = hasElopageBuys(userEntity.emailContact.email) const elopageBuys = hasElopageBuys(userEntity.emailContact.email)
logger.debug(`has ElopageBuys = ${elopageBuys}`) logger.debug('has ElopageBuys', elopageBuys)
return elopageBuys return elopageBuys
} }
@ -644,7 +660,7 @@ export class UserResolver {
return 'user.' + fieldName return 'user.' + fieldName
}), }),
searchText, searchText,
filters || null, filters ?? null,
currentPage, currentPage,
pageSize, pageSize,
) )
@ -710,14 +726,14 @@ export class UserResolver {
// change isAdmin // change isAdmin
switch (user.isAdmin) { switch (user.isAdmin) {
case null: case null:
if (isAdmin === true) { if (isAdmin) {
user.isAdmin = new Date() user.isAdmin = new Date()
} else { } else {
throw new LogError('User is already an usual user') throw new LogError('User is already an usual user')
} }
break break
default: default:
if (isAdmin === false) { if (!isAdmin) {
user.isAdmin = null user.isAdmin = null
} else { } else {
throw new LogError('User is already admin') throw new LogError('User is already admin')

View File

@ -29,10 +29,12 @@ export const validateContribution = (
throw new LogError('No information for available creations for the given date', creationDate) throw new LogError('No information for available creations for the given date', creationDate)
} }
// eslint-disable-next-line security/detect-object-injection
if (amount.greaterThan(creations[index].toString())) { if (amount.greaterThan(creations[index].toString())) {
throw new LogError( throw new LogError(
'The amount to be created exceeds the amount still available for this month', 'The amount to be created exceeds the amount still available for this month',
amount, amount,
// eslint-disable-next-line security/detect-object-injection
creations[index], creations[index],
) )
} }
@ -151,6 +153,7 @@ export const updateCreations = (
if (index < 0) { if (index < 0) {
throw new LogError('You cannot create GDD for a month older than the last three months') throw new LogError('You cannot create GDD for a month older than the last three months')
} }
// eslint-disable-next-line security/detect-object-injection
creations[index] = creations[index].plus(contribution.amount.toString()) creations[index] = creations[index].plus(contribution.amount.toString())
return creations return creations
} }
@ -169,6 +172,7 @@ export const getOpenCreations = async (
return { return {
month: date.getMonth(), month: date.getMonth(),
year: date.getFullYear(), year: date.getFullYear(),
// eslint-disable-next-line security/detect-object-injection
amount: creations[index], amount: creations[index],
} }
}) })

View File

@ -24,7 +24,7 @@ export const findContributions = async (
} }
return DbContribution.findAndCount({ return DbContribution.findAndCount({
where: { where: {
...(statusFilter && statusFilter.length && { contributionStatus: In(statusFilter) }), ...(statusFilter?.length && { contributionStatus: In(statusFilter) }),
...(userId && { userId }), ...(userId && { userId }),
}, },
withDeleted, withDeleted,

View File

@ -14,7 +14,7 @@ export async function transactionLinkList(
filters: TransactionLinkFilters | null, filters: TransactionLinkFilters | null,
user: DbUser, user: DbUser,
): Promise<TransactionLinkResult> { ): Promise<TransactionLinkResult> {
const { withDeleted, withExpired, withRedeemed } = filters || { const { withDeleted, withExpired, withRedeemed } = filters ?? {
withDeleted: false, withDeleted: false,
withExpired: false, withExpired: false,
withRedeemed: false, withRedeemed: false,

View File

@ -0,0 +1,125 @@
import { Connection } from '@dbTools/typeorm'
import { User } from '@entity/User'
import { ApolloServerTestClient } from 'apollo-server-testing'
import { testEnvironment, cleanDB } from '@test/helpers'
import { logger, i18n as localization } from '@test/testSetup'
import { userFactory } from '@/seeds/factory/user'
import { bibiBloxberg } from '@/seeds/users/bibi-bloxberg'
import { validateAlias } from './validateAlias'
let con: Connection
let testEnv: {
mutate: ApolloServerTestClient['mutate']
query: ApolloServerTestClient['query']
con: Connection
}
beforeAll(async () => {
testEnv = await testEnvironment(logger, localization)
con = testEnv.con
await cleanDB()
})
afterAll(async () => {
await cleanDB()
await con.close()
})
describe('validate alias', () => {
beforeAll(() => {
jest.clearAllMocks()
})
describe('alias too short', () => {
it('throws and logs an error', async () => {
await expect(validateAlias('Bi')).rejects.toEqual(new Error('Given alias is too short'))
expect(logger.error).toBeCalledWith('Given alias is too short', 'Bi')
})
})
describe('alias too long', () => {
it('throws and logs an error', async () => {
await expect(validateAlias('BibiBloxbergHexHexHex')).rejects.toEqual(
new Error('Given alias is too long'),
)
expect(logger.error).toBeCalledWith('Given alias is too long', 'BibiBloxbergHexHexHex')
})
})
describe('alias contains invalid characters', () => {
it('throws and logs an error', async () => {
await expect(validateAlias('Bibi.Bloxberg')).rejects.toEqual(
new Error('Invalid characters in alias'),
)
expect(logger.error).toBeCalledWith('Invalid characters in alias', 'Bibi.Bloxberg')
})
})
describe('alias is a reserved word', () => {
it('throws and logs an error', async () => {
await expect(validateAlias('admin')).rejects.toEqual(new Error('Alias is not allowed'))
expect(logger.error).toBeCalledWith('Alias is not allowed', 'admin')
})
})
describe('alias is a reserved word with uppercase characters', () => {
it('throws and logs an error', async () => {
await expect(validateAlias('Admin')).rejects.toEqual(new Error('Alias is not allowed'))
expect(logger.error).toBeCalledWith('Alias is not allowed', 'Admin')
})
})
describe('hyphens and underscore', () => {
describe('alias starts with underscore', () => {
it('throws and logs an error', async () => {
await expect(validateAlias('_bibi')).rejects.toEqual(
new Error('Invalid characters in alias'),
)
expect(logger.error).toBeCalledWith('Invalid characters in alias', '_bibi')
})
})
describe('alias contains two following hyphens', () => {
it('throws and logs an error', async () => {
await expect(validateAlias('bi--bi')).rejects.toEqual(
new Error('Invalid characters in alias'),
)
expect(logger.error).toBeCalledWith('Invalid characters in alias', 'bi--bi')
})
})
})
describe('test against existing alias in database', () => {
beforeAll(async () => {
const bibi = await userFactory(testEnv, bibiBloxberg)
const user = await User.findOne({ id: bibi.id })
if (user) {
user.alias = 'b-b'
await user.save()
}
})
describe('alias exists in database', () => {
it('throws and logs an error', async () => {
await expect(validateAlias('b-b')).rejects.toEqual(new Error('Alias already in use'))
expect(logger.error).toBeCalledWith('Alias already in use', 'b-b')
})
})
describe('alias exists in database with in lower-case', () => {
it('throws and logs an error', async () => {
await expect(validateAlias('b-B')).rejects.toEqual(new Error('Alias already in use'))
expect(logger.error).toBeCalledWith('Alias already in use', 'b-B')
})
})
describe('valid alias', () => {
it('resolves to true', async () => {
await expect(validateAlias('bibi')).resolves.toEqual(true)
})
})
})
})

View File

@ -0,0 +1,38 @@
import { Raw } from '@dbTools/typeorm'
import { User as DbUser } from '@entity/User'
import { LogError } from '@/server/LogError'
const reservedAlias = [
'admin',
'email',
'gast',
'gdd',
'gradido',
'guest',
'home',
'root',
'support',
'temp',
'tmp',
'tmp',
'user',
'usr',
'var',
]
export const validateAlias = async (alias: string): Promise<boolean> => {
if (alias.length < 3) throw new LogError('Given alias is too short', alias)
if (alias.length > 20) throw new LogError('Given alias is too long', alias)
/* eslint-disable-next-line security/detect-unsafe-regex */
if (!alias.match(/^[0-9A-Za-z]([_-]?[A-Za-z0-9])+$/))
throw new LogError('Invalid characters in alias', alias)
if (reservedAlias.includes(alias.toLowerCase())) throw new LogError('Alias is not allowed', alias)
const aliasInUse = await DbUser.find({
where: { alias: Raw((a) => `LOWER(${a}) = "${alias.toLowerCase()}"`) },
})
if (aliasInUse.length !== 0) {
throw new LogError('Alias already in use', alias)
}
return true
}

View File

@ -13,7 +13,7 @@ async function main() {
console.log(`GraphIQL available at http://localhost:${CONFIG.PORT}`) console.log(`GraphIQL available at http://localhost:${CONFIG.PORT}`)
} }
}) })
void startValidateCommunities(Number(CONFIG.FEDERATION_VALIDATE_COMMUNITY_TIMER)) startValidateCommunities(Number(CONFIG.FEDERATION_VALIDATE_COMMUNITY_TIMER))
} }
main().catch((e) => { main().catch((e) => {

View File

@ -10,19 +10,6 @@ import { KlickTipp } from '@model/KlickTipp'
import { getKlickTippUser } from '@/apis/KlicktippController' import { getKlickTippUser } from '@/apis/KlicktippController'
import { klickTippLogger as logger } from '@/server/logger' import { klickTippLogger as logger } from '@/server/logger'
// export const klicktippRegistrationMiddleware: MiddlewareFn = async (
// // Only for demo
// /* eslint-disable-next-line @typescript-eslint/no-unused-vars */
// { root, args, context, info },
// next,
// ) => {
// // Do Something here before resolver is called
// const result = await next()
// // Do Something here after resolver is completed
// await klicktippSignIn(result.email, result.language, result.firstName, result.lastName)
// return result
// }
export const klicktippNewsletterStateMiddleware: MiddlewareFn = async ( export const klicktippNewsletterStateMiddleware: MiddlewareFn = async (
/* eslint-disable-next-line @typescript-eslint/no-unused-vars */ /* eslint-disable-next-line @typescript-eslint/no-unused-vars */
{ root, args, context, info }, { root, args, context, info },

View File

@ -28,6 +28,7 @@ export const updateUserInfos = gql`
mutation ( mutation (
$firstName: String $firstName: String
$lastName: String $lastName: String
$alias: String
$password: String $password: String
$passwordNew: String $passwordNew: String
$locale: String $locale: String
@ -37,6 +38,7 @@ export const updateUserInfos = gql`
updateUserInfos( updateUserInfos(
firstName: $firstName firstName: $firstName
lastName: $lastName lastName: $lastName
alias: $alias
password: $password password: $password
passwordNew: $passwordNew passwordNew: $passwordNew
language: $locale language: $locale
@ -305,7 +307,6 @@ export const login = gql`
mutation ($email: String!, $password: String!, $publisherId: Int) { mutation ($email: String!, $password: String!, $publisherId: Int) {
login(email: $email, password: $password, publisherId: $publisherId) { login(email: $email, password: $password, publisherId: $publisherId) {
id id
email
firstName firstName
lastName lastName
language language

View File

@ -3,7 +3,6 @@ import { gql } from 'graphql-tag'
export const verifyLogin = gql` export const verifyLogin = gql`
query { query {
verifyLogin { verifyLogin {
email
firstName firstName
lastName lastName
language language
@ -23,32 +22,33 @@ export const queryOptIn = gql`
} }
` `
export const checkUsername = gql`
query ($username: String!) {
checkUsername(username: $username)
}
`
export const transactionsQuery = gql` export const transactionsQuery = gql`
query ( query ($currentPage: Int = 1, $pageSize: Int = 25, $order: Order = DESC) {
$currentPage: Int = 1 transactionList(currentPage: $currentPage, pageSize: $pageSize, order: $order) {
$pageSize: Int = 25 balance {
$order: Order = DESC balance
$onlyCreations: Boolean = false balanceGDT
) { count
transactionList( linkCount
currentPage: $currentPage }
pageSize: $pageSize
order: $order
onlyCreations: $onlyCreations
) {
balanceGDT
count
balance
transactions { transactions {
id id
typeId typeId
amount amount
balance balance
previousBalance
balanceDate balanceDate
memo memo
linkedUser { linkedUser {
firstName firstName
lastName lastName
gradidoID
} }
decay { decay {
decay decay
@ -56,6 +56,7 @@ export const transactionsQuery = gql`
end end
duration duration
} }
linkId
} }
} }
} }

View File

@ -31,8 +31,8 @@ const context = {
export const cleanDB = async () => { export const cleanDB = async () => {
// this only works as long we do not have foreign key constraints // this only works as long we do not have foreign key constraints
for (let i = 0; i < entities.length; i++) { for (const entity of entities) {
await resetEntity(entities[i]) await resetEntity(entity)
} }
} }
@ -54,9 +54,8 @@ const run = async () => {
logger.info('##seed## clean database successful...') logger.info('##seed## clean database successful...')
// seed the standard users // seed the standard users
for (let i = 0; i < users.length; i++) { for (const user of users) {
const dbUser = await userFactory(seedClient, users[i]) await userFactory(seedClient, user)
logger.info(`##seed## seed standard users[ ${i} ]= ${JSON.stringify(dbUser, null, 2)}`)
} }
logger.info('##seed## seeding all standard users successful...') logger.info('##seed## seeding all standard users successful...')
@ -73,20 +72,20 @@ const run = async () => {
logger.info('##seed## seeding all random users successful...') logger.info('##seed## seeding all random users successful...')
// create GDD // create GDD
for (let i = 0; i < creations.length; i++) { for (const creation of creations) {
await creationFactory(seedClient, creations[i]) await creationFactory(seedClient, creation)
} }
logger.info('##seed## seeding all creations successful...') logger.info('##seed## seeding all creations successful...')
// create Transaction Links // create Transaction Links
for (let i = 0; i < transactionLinks.length; i++) { for (const transactionLink of transactionLinks) {
await transactionLinkFactory(seedClient, transactionLinks[i]) await transactionLinkFactory(seedClient, transactionLink)
} }
logger.info('##seed## seeding all transactionLinks successful...') logger.info('##seed## seeding all transactionLinks successful...')
// create Contribution Links // create Contribution Links
for (let i = 0; i < contributionLinks.length; i++) { for (const contributionLink of contributionLinks) {
await contributionLinkFactory(seedClient, contributionLinks[i]) await contributionLinkFactory(seedClient, contributionLink)
} }
logger.info('##seed## seeding all contributionLinks successful...') logger.info('##seed## seeding all contributionLinks successful...')

View File

@ -21,7 +21,11 @@ import { plugins } from './plugins'
// TODO implement // TODO implement
// import queryComplexity, { simpleEstimator, fieldConfigEstimator } from "graphql-query-complexity"; // import queryComplexity, { simpleEstimator, fieldConfigEstimator } from "graphql-query-complexity";
type ServerDef = { apollo: ApolloServer; app: Express; con: Connection } interface ServerDef {
apollo: ApolloServer
app: Express
con: Connection
}
export const createServer = async ( export const createServer = async (
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
@ -34,7 +38,7 @@ export const createServer = async (
// open mysql connection // open mysql connection
const con = await connection() const con = await connection()
if (!con || !con.isConnected) { if (!con?.isConnected) {
logger.fatal(`Couldn't open connection to database!`) logger.fatal(`Couldn't open connection to database!`)
throw new Error(`Fatal: Couldn't open connection to database`) throw new Error(`Fatal: Couldn't open connection to database`)
} }

View File

@ -7,6 +7,7 @@ import { configure, getLogger } from 'log4js'
import { CONFIG } from '@/config' import { CONFIG } from '@/config'
// eslint-disable-next-line security/detect-non-literal-fs-filename
const options = JSON.parse(readFileSync(CONFIG.LOG4JS_CONFIG, 'utf-8')) const options = JSON.parse(readFileSync(CONFIG.LOG4JS_CONFIG, 'utf-8'))
options.categories.backend.level = CONFIG.LOG_LEVEL options.categories.backend.level = CONFIG.LOG_LEVEL

View File

@ -12,7 +12,7 @@ const setHeadersPlugin = {
return { return {
willSendResponse(requestContext: any) { willSendResponse(requestContext: any) {
const { setHeaders = [] } = requestContext.context const { setHeaders = [] } = requestContext.context
setHeaders.forEach(({ key, value }: { [key: string]: string }) => { setHeaders.forEach(({ key, value }: Record<string, string>) => {
if (requestContext.response.http.headers.get(key)) { if (requestContext.response.http.headers.get(key)) {
requestContext.response.http.headers.set(key, value) requestContext.response.http.headers.set(key, value)
} else { } else {
@ -27,8 +27,8 @@ const setHeadersPlugin = {
const filterVariables = (variables: any) => { const filterVariables = (variables: any) => {
const vars = clonedeep(variables) const vars = clonedeep(variables)
if (vars && vars.password) vars.password = '***' if (vars?.password) vars.password = '***'
if (vars && vars.passwordNew) vars.passwordNew = '***' if (vars?.passwordNew) vars.passwordNew = '***'
return vars return vars
} }

View File

@ -14,10 +14,10 @@ const getDBVersion = async (): Promise<string | null> => {
const checkDBVersion = async (DB_VERSION: string): Promise<boolean> => { const checkDBVersion = async (DB_VERSION: string): Promise<boolean> => {
const dbVersion = await getDBVersion() const dbVersion = await getDBVersion()
if (!dbVersion || dbVersion.indexOf(DB_VERSION) === -1) { if (!dbVersion?.includes(DB_VERSION)) {
logger.error( logger.error(
`Wrong database version detected - the backend requires '${DB_VERSION}' but found '${ `Wrong database version detected - the backend requires '${DB_VERSION}' but found '${
dbVersion || 'None' dbVersion ?? 'None'
}`, }`,
) )
return false return false

View File

@ -11,8 +11,7 @@ export async function retrieveNotRegisteredEmails(): Promise<string[]> {
} }
const users = await User.find({ relations: ['emailContact'] }) const users = await User.find({ relations: ['emailContact'] })
const notRegisteredUser = [] const notRegisteredUser = []
for (let i = 0; i < users.length; i++) { for (const user of users) {
const user = users[i]
try { try {
await getKlickTippUser(user.emailContact.email) await getKlickTippUser(user.emailContact.email)
} catch (err) { } catch (err) {

View File

@ -1,11 +1,9 @@
import { Decimal } from 'decimal.js-light' import { Decimal } from 'decimal.js-light'
import i18n from 'i18n' import i18n from 'i18n'
export const objectValuesToArray = (obj: { [x: string]: string }): Array<string> => { export const objectValuesToArray = (obj: Record<string, string>): string[] =>
return Object.keys(obj).map(function (key) { // eslint-disable-next-line security/detect-object-injection
return obj[key] Object.keys(obj).map((key) => obj[key])
})
}
export const decimalSeparatorByLanguage = (a: Decimal, language: string): string => { export const decimalSeparatorByLanguage = (a: Decimal, language: string): string => {
const rememberLocaleToRestore = i18n.getLocale() const rememberLocaleToRestore = i18n.getLocale()
@ -14,3 +12,6 @@ export const decimalSeparatorByLanguage = (a: Decimal, language: string): string
i18n.setLocale(rememberLocaleToRestore) i18n.setLocale(rememberLocaleToRestore)
return result return result
} }
export const fullName = (firstName: string, lastName: string): string =>
[firstName, lastName].filter(Boolean).join(' ')

View File

@ -54,6 +54,10 @@ const virtualLinkTransaction = (
creationDate: null, creationDate: null,
contribution: null, contribution: null,
...defaultModelFunctions, ...defaultModelFunctions,
userGradidoID: '',
userName: null,
linkedUserGradidoID: null,
linkedUserName: null,
} }
return new Transaction(linkDbTransaction, user) return new Transaction(linkDbTransaction, user)
} }
@ -84,6 +88,10 @@ const virtualDecayTransaction = (
creationDate: null, creationDate: null,
contribution: null, contribution: null,
...defaultModelFunctions, ...defaultModelFunctions,
userGradidoID: '',
userName: null,
linkedUserGradidoID: null,
linkedUserName: null,
} }
return new Transaction(decayDbTransaction, user) return new Transaction(decayDbTransaction, user)
} }

View File

@ -115,6 +115,7 @@ export const elopageWebhook = async (req: any, res: any): Promise<void> => {
) { ) {
const email = loginElopageBuy.payerEmail const email = loginElopageBuy.payerEmail
// eslint-disable-next-line security/detect-unsafe-regex
const VALIDATE_EMAIL = /^[a-zA-Z0-9.!#$%&?*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/ const VALIDATE_EMAIL = /^[a-zA-Z0-9.!#$%&?*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/
const VALIDATE_NAME = /^<>&;]{2,}$/ const VALIDATE_NAME = /^<>&;]{2,}$/
@ -146,7 +147,7 @@ export const elopageWebhook = async (req: any, res: any): Promise<void> => {
email, email,
firstName, firstName,
lastName, lastName,
publisherId: loginElopageBuy.publisherId || 0, // This seemed to be the default value if not set publisherId: loginElopageBuy.publisherId ?? 0, // This seemed to be the default value if not set
}) })
} catch (error) { } catch (error) {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console

View File

@ -22,8 +22,8 @@ const context = {
export const cleanDB = async () => { export const cleanDB = async () => {
// this only works as lond we do not have foreign key constraints // this only works as lond we do not have foreign key constraints
for (let i = 0; i < entities.length; i++) { for (const entity of entities) {
await resetEntity(entities[i]) await resetEntity(entity)
} }
} }

View File

@ -382,6 +382,14 @@
dependencies: dependencies:
"@cspotcode/source-map-consumer" "0.8.0" "@cspotcode/source-map-consumer" "0.8.0"
"@eslint-community/eslint-plugin-eslint-comments@^3.2.1":
version "3.2.1"
resolved "https://registry.yarnpkg.com/@eslint-community/eslint-plugin-eslint-comments/-/eslint-plugin-eslint-comments-3.2.1.tgz#3c65061e27f155eae3744c3b30c5a8253a959040"
integrity sha512-/HZbjIGaVO2zLlWX3gRgiHmKRVvvqrC0zVu3eXnIj1ORxoyfGSj50l0PfDfqihyZAqrDYzSMdJesXzFjvAoiLQ==
dependencies:
escape-string-regexp "^1.0.5"
ignore "^5.2.4"
"@eslint-community/eslint-utils@^4.2.0": "@eslint-community/eslint-utils@^4.2.0":
version "4.2.0" version "4.2.0"
resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.2.0.tgz#a831e6e468b4b2b5ae42bf658bea015bf10bc518" resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.2.0.tgz#a831e6e468b4b2b5ae42bf658bea015bf10bc518"
@ -2986,6 +2994,13 @@ eslint-plugin-promise@^6.1.1:
resolved "https://registry.yarnpkg.com/eslint-plugin-promise/-/eslint-plugin-promise-6.1.1.tgz#269a3e2772f62875661220631bd4dafcb4083816" resolved "https://registry.yarnpkg.com/eslint-plugin-promise/-/eslint-plugin-promise-6.1.1.tgz#269a3e2772f62875661220631bd4dafcb4083816"
integrity sha512-tjqWDwVZQo7UIPMeDReOpUgHCmCiH+ePnVT+5zVapL0uuHnegBUs2smM13CzOs2Xb5+MHMRFTs9v24yjba4Oig== integrity sha512-tjqWDwVZQo7UIPMeDReOpUgHCmCiH+ePnVT+5zVapL0uuHnegBUs2smM13CzOs2Xb5+MHMRFTs9v24yjba4Oig==
eslint-plugin-security@^1.7.1:
version "1.7.1"
resolved "https://registry.yarnpkg.com/eslint-plugin-security/-/eslint-plugin-security-1.7.1.tgz#0e9c4a471f6e4d3ca16413c7a4a51f3966ba16e4"
integrity sha512-sMStceig8AFglhhT2LqlU5r+/fn9OwsA72O5bBuQVTssPCdQAOQzL+oMn/ZcpeUY6KcNfLJArgcrsSULNjYYdQ==
dependencies:
safe-regex "^2.1.1"
eslint-plugin-type-graphql@^1.0.0: eslint-plugin-type-graphql@^1.0.0:
version "1.0.0" version "1.0.0"
resolved "https://registry.yarnpkg.com/eslint-plugin-type-graphql/-/eslint-plugin-type-graphql-1.0.0.tgz#d348560ed628d6ca1dfcea35a02891432daafe6b" resolved "https://registry.yarnpkg.com/eslint-plugin-type-graphql/-/eslint-plugin-type-graphql-1.0.0.tgz#d348560ed628d6ca1dfcea35a02891432daafe6b"
@ -3958,7 +3973,7 @@ ignore@^5.1.1:
resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.1.8.tgz#f150a8b50a34289b33e22f5889abd4d8016f0e57" resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.1.8.tgz#f150a8b50a34289b33e22f5889abd4d8016f0e57"
integrity sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw== integrity sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw==
ignore@^5.2.0: ignore@^5.2.0, ignore@^5.2.4:
version "5.2.4" version "5.2.4"
resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.4.tgz#a291c0c6178ff1b960befe47fcdec301674a6324" resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.4.tgz#a291c0c6178ff1b960befe47fcdec301674a6324"
integrity sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ== integrity sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==
@ -6058,6 +6073,11 @@ reflect-metadata@^0.1.13:
resolved "https://registry.yarnpkg.com/reflect-metadata/-/reflect-metadata-0.1.13.tgz#67ae3ca57c972a2aa1642b10fe363fe32d49dc08" resolved "https://registry.yarnpkg.com/reflect-metadata/-/reflect-metadata-0.1.13.tgz#67ae3ca57c972a2aa1642b10fe363fe32d49dc08"
integrity sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg== integrity sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg==
regexp-tree@~0.1.1:
version "0.1.27"
resolved "https://registry.yarnpkg.com/regexp-tree/-/regexp-tree-0.1.27.tgz#2198f0ef54518ffa743fe74d983b56ffd631b6cd"
integrity sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA==
regexp.prototype.flags@^1.4.3: regexp.prototype.flags@^1.4.3:
version "1.4.3" version "1.4.3"
resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz#87cab30f80f66660181a3bb7bf5981a872b367ac" resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz#87cab30f80f66660181a3bb7bf5981a872b367ac"
@ -6197,6 +6217,13 @@ safe-regex-test@^1.0.0:
get-intrinsic "^1.1.3" get-intrinsic "^1.1.3"
is-regex "^1.1.4" is-regex "^1.1.4"
safe-regex@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/safe-regex/-/safe-regex-2.1.1.tgz#f7128f00d056e2fe5c11e81a1324dd974aadced2"
integrity sha512-rx+x8AMzKb5Q5lQ95Zoi6ZbJqwCLkqi3XuJXp5P3rT8OEc6sZCJG5AE5dU3lsgRr/F4Bs31jSlVN+j5KrsGu9A==
dependencies:
regexp-tree "~0.1.1"
"safer-buffer@>= 2.1.2 < 3", "safer-buffer@>= 2.1.2 < 3.0.0": "safer-buffer@>= 2.1.2 < 3", "safer-buffer@>= 2.1.2 < 3.0.0":
version "2.1.2" version "2.1.2"
resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"

View File

@ -0,0 +1,139 @@
import Decimal from 'decimal.js-light'
import { BaseEntity, Entity, PrimaryGeneratedColumn, Column, OneToOne, JoinColumn } from 'typeorm'
import { DecimalTransformer } from '../../src/typeorm/DecimalTransformer'
import { Contribution } from '../Contribution'
@Entity('transactions')
export class Transaction extends BaseEntity {
@PrimaryGeneratedColumn('increment', { unsigned: true })
id: number
@Column({ type: 'int', unsigned: true, unique: true, nullable: true, default: null })
previous: number | null
@Column({ name: 'type_id', unsigned: true, nullable: false })
typeId: number
@Column({
name: 'transaction_link_id',
type: 'int',
unsigned: true,
nullable: true,
default: null,
})
transactionLinkId?: number | null
@Column({
type: 'decimal',
precision: 40,
scale: 20,
nullable: false,
transformer: DecimalTransformer,
})
amount: Decimal
@Column({
type: 'decimal',
precision: 40,
scale: 20,
nullable: false,
transformer: DecimalTransformer,
})
balance: Decimal
@Column({
name: 'balance_date',
type: 'datetime',
default: () => 'CURRENT_TIMESTAMP',
nullable: false,
})
balanceDate: Date
@Column({
type: 'decimal',
precision: 40,
scale: 20,
nullable: false,
transformer: DecimalTransformer,
})
decay: Decimal
@Column({
name: 'decay_start',
type: 'datetime',
nullable: true,
default: null,
})
decayStart: Date | null
@Column({ length: 255, nullable: false, collation: 'utf8mb4_unicode_ci' })
memo: string
@Column({ name: 'creation_date', type: 'datetime', nullable: true, default: null })
creationDate: Date | null
@Column({ name: 'user_id', unsigned: true, nullable: false })
userId: number
@Column({
name: 'user_gradido_id',
type: 'varchar',
length: 36,
nullable: false,
collation: 'utf8mb4_unicode_ci',
})
userGradidoID: string
@Column({
name: 'user_name',
type: 'varchar',
length: 512,
nullable: true,
collation: 'utf8mb4_unicode_ci',
})
userName: string | null
@Column({
name: 'linked_user_id',
type: 'int',
unsigned: true,
nullable: true,
default: null,
})
linkedUserId?: number | null
@Column({
name: 'linked_user_gradido_id',
type: 'varchar',
length: 36,
nullable: true,
collation: 'utf8mb4_unicode_ci',
})
linkedUserGradidoID: string | null
@Column({
name: 'linked_user_name',
type: 'varchar',
length: 512,
nullable: true,
collation: 'utf8mb4_unicode_ci',
})
linkedUserName: string | null
@Column({
name: 'linked_transaction_id',
type: 'int',
unsigned: true,
nullable: true,
default: null,
})
linkedTransactionId?: number | null
@OneToOne(() => Contribution, (contribution) => contribution.transaction)
@JoinColumn({ name: 'id', referencedColumnName: 'transactionId' })
contribution?: Contribution | null
@OneToOne(() => Transaction)
@JoinColumn({ name: 'previous' })
previousTransaction?: Transaction | null
}

View File

@ -1 +1 @@
export { Transaction } from './0036-unique_previous_in_transactions/Transaction' export { Transaction } from './0066-x-community-sendcoins-transactions_table/Transaction'

View File

@ -0,0 +1,76 @@
/* MIGRATION TO add users that have a transaction but do not exist */
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
/* eslint-disable @typescript-eslint/no-explicit-any */
export async function upgrade(queryFn: (query: string, values?: any[]) => Promise<Array<any>>) {
await queryFn(
'ALTER TABLE `transactions` MODIFY COLUMN `previous` int(10) unsigned DEFAULT NULL NULL AFTER `id`;',
)
await queryFn(
'ALTER TABLE `transactions` MODIFY COLUMN `type_id` int(10) DEFAULT NULL NULL AFTER `previous`;',
)
await queryFn(
'ALTER TABLE `transactions` MODIFY COLUMN `transaction_link_id` int(10) unsigned DEFAULT NULL NULL AFTER `type_id`;',
)
await queryFn(
'ALTER TABLE `transactions` MODIFY COLUMN `amount` decimal(40,20) DEFAULT NULL NULL AFTER `transaction_link_id`;',
)
await queryFn(
'ALTER TABLE `transactions` MODIFY COLUMN `balance` decimal(40,20) DEFAULT NULL NULL AFTER `amount`;',
)
await queryFn(
'ALTER TABLE `transactions` MODIFY COLUMN `balance_date` datetime(3) DEFAULT current_timestamp(3) NOT NULL AFTER `balance`;',
)
await queryFn(
'ALTER TABLE `transactions` MODIFY COLUMN `decay` decimal(40,20) DEFAULT NULL NULL AFTER `balance_date`;',
)
await queryFn(
'ALTER TABLE `transactions` MODIFY COLUMN `decay_start` datetime(3) DEFAULT NULL NULL AFTER `decay`;',
)
await queryFn(
'ALTER TABLE `transactions` MODIFY COLUMN `memo` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL AFTER `decay_start`;',
)
await queryFn(
'ALTER TABLE `transactions` MODIFY COLUMN `creation_date` datetime(3) DEFAULT NULL NULL AFTER `memo`;',
)
await queryFn(
'ALTER TABLE `transactions` MODIFY COLUMN `user_id` int(10) unsigned NOT NULL AFTER `creation_date`;',
)
await queryFn(
'ALTER TABLE `transactions` ADD COLUMN `user_gradido_id` char(36) DEFAULT NULL NULL AFTER `user_id`;',
)
await queryFn(
'ALTER TABLE `transactions` ADD COLUMN `user_name` varchar(512) COLLATE utf8mb4_unicode_ci DEFAULT NULL NULL AFTER `user_gradido_id`;',
)
await queryFn(
'ALTER TABLE `transactions` MODIFY COLUMN `linked_user_id` int(10) unsigned DEFAULT NULL NULL AFTER `user_name`;',
)
await queryFn(
'ALTER TABLE `transactions` ADD COLUMN `linked_user_gradido_id` char(36) DEFAULT NULL NULL AFTER `linked_user_id`;',
)
await queryFn(
'ALTER TABLE `transactions` ADD COLUMN `linked_user_name` varchar(512) COLLATE utf8mb4_unicode_ci DEFAULT NULL NULL AFTER `linked_user_gradido_id`;',
)
await queryFn(
'ALTER TABLE `transactions` MODIFY COLUMN `linked_transaction_id` int(10) DEFAULT NULL NULL AFTER `linked_user_name`;',
)
await queryFn(
`UPDATE transactions t, users u SET t.user_gradido_id = u.gradido_id, t.user_name = concat(u.first_name, ' ', u.last_name) WHERE t.user_id = u.id and t.user_gradido_id is null;`,
)
await queryFn(
'ALTER TABLE `transactions` MODIFY COLUMN `user_gradido_id` char(36) NOT NULL AFTER `user_id`;',
)
await queryFn(
`UPDATE transactions t, users u SET t.linked_user_gradido_id = u.gradido_id, t.linked_user_name = concat(u.first_name, ' ', u.last_name) WHERE t.linked_user_id = u.id and t.linked_user_gradido_id is null;`,
)
}
/* eslint-disable @typescript-eslint/no-empty-function */
/* eslint-disable-next-line @typescript-eslint/no-unused-vars */
export async function downgrade(queryFn: (query: string, values?: any[]) => Promise<Array<any>>) {
await queryFn('ALTER TABLE `transactions` DROP COLUMN `user_gradido_id`;')
await queryFn('ALTER TABLE `transactions` DROP COLUMN `user_name`;')
await queryFn('ALTER TABLE `transactions` DROP COLUMN `linked_user_gradido_id`;')
await queryFn('ALTER TABLE `transactions` DROP COLUMN `linked_user_name`;')
}

View File

@ -117,7 +117,7 @@ server {
# TODO this could be a performance optimization # TODO this could be a performance optimization
#location /vue { #location /vue {
# alias /var/www/html/gradido/frontend/dist; # alias /var/www/html/gradido/frontend/build;
# index index.html; # index index.html;
# #
# location ~* \.(png)$ { # location ~* \.(png)$ {

View File

@ -103,7 +103,7 @@ server {
# TODO this could be a performance optimization # TODO this could be a performance optimization
#location /vue { #location /vue {
# alias /var/www/html/gradido/frontend/dist; # alias /var/www/html/gradido/frontend/build;
# index index.html; # index index.html;
# #
# location ~* \.(png)$ { # location ~* \.(png)$ {

View File

@ -15,6 +15,6 @@ export NVM_DIR="/root/.nvm"
$NPM_BIN install $NPM_BIN install
$NPM_BIN run build $NPM_BIN run build
# prezip for faster deliver throw nginx # prezip for faster deliver throw nginx
cd dist cd build
find . -type f -name "*.css" -exec gzip -9 -k {} \; find . -type f -name "*.css" -exec gzip -9 -k {} \;
find . -type f -name "*.js" -exec gzip -9 -k {} \; find . -type f -name "*.js" -exec gzip -9 -k {} \;

View File

@ -130,6 +130,15 @@ rm -Rf $PROJECT_ROOT/admin/node_modules
rm -Rf $PROJECT_ROOT/dht-node/node_modules rm -Rf $PROJECT_ROOT/dht-node/node_modules
rm -Rf $PROJECT_ROOT/federation/node_modules rm -Rf $PROJECT_ROOT/federation/node_modules
# Remove build folders
# we had problems with corrupted incremtal builds
rm -Rf $PROJECT_ROOT/database/build
rm -Rf $PROJECT_ROOT/backend/build
rm -Rf $PROJECT_ROOT/frontend/build
rm -Rf $PROJECT_ROOT/admin/build
rm -Rf $PROJECT_ROOT/dht-node/build
rm -Rf $PROJECT_ROOT/federation/build
# Regenerate .env files # Regenerate .env files
cp -f $PROJECT_ROOT/database/.env $PROJECT_ROOT/database/.env.bak cp -f $PROJECT_ROOT/database/.env $PROJECT_ROOT/database/.env.bak
cp -f $PROJECT_ROOT/backend/.env $PROJECT_ROOT/backend/.env.bak cp -f $PROJECT_ROOT/backend/.env $PROJECT_ROOT/backend/.env.bak

View File

@ -3,7 +3,7 @@ import dotenv from 'dotenv'
dotenv.config() dotenv.config()
const constants = { const constants = {
DB_VERSION: '0065-refactor_communities_table', DB_VERSION: '0066-x-community-sendcoins-transactions_table',
LOG4JS_CONFIG: 'log4js-config.json', LOG4JS_CONFIG: 'log4js-config.json',
// default log level on production should be info // default log level on production should be info
LOG_LEVEL: process.env.LOG_LEVEL || 'info', LOG_LEVEL: process.env.LOG_LEVEL || 'info',

View File

@ -58,7 +58,7 @@ export default defineConfig({
mailserverURL: 'http://localhost:1080', mailserverURL: 'http://localhost:1080',
loginQuery: `mutation ($email: String!, $password: String!, $publisherId: Int) { loginQuery: `mutation ($email: String!, $password: String!, $publisherId: Int) {
login(email: $email, password: $password, publisherId: $publisherId) { login(email: $email, password: $password, publisherId: $publisherId) {
email id
firstName firstName
lastName lastName
language language

View File

@ -35,6 +35,6 @@ Cypress.Commands.add('login', (email, password) => {
} }
cy.visit('/') cy.visit('/')
window.localStorage.setItem('vuex', JSON.stringify(vuexToken)) window.localStorage.setItem('gradido-frontend', JSON.stringify(vuexToken))
}) })
}) })

View File

@ -11,7 +11,7 @@ Decimal.set({
*/ */
const constants = { const constants = {
DB_VERSION: '0065-refactor_communities_table', DB_VERSION: '0066-x-community-sendcoins-transactions_table',
// DECAY_START_TIME: new Date('2021-05-13 17:46:31-0000'), // GMT+0 // DECAY_START_TIME: new Date('2021-05-13 17:46:31-0000'), // GMT+0
LOG4JS_CONFIG: 'log4js-config.json', LOG4JS_CONFIG: 'log4js-config.json',
// default log level on production should be info // default log level on production should be info

View File

@ -23,8 +23,8 @@ const setHeadersPlugin = {
const filterVariables = (variables: any) => { const filterVariables = (variables: any) => {
const vars = clonedeep(variables) const vars = clonedeep(variables)
if (vars.password) vars.password = '***' if (vars && vars.password) vars.password = '***'
if (vars.passwordNew) vars.passwordNew = '***' if (vars && vars.passwordNew) vars.passwordNew = '***'
return vars return vars
} }

View File

@ -1,3 +1,3 @@
node_modules/ node_modules/
dist/ build/
coverage/ coverage/

2
frontend/.gitignore vendored
View File

@ -1,6 +1,6 @@
.DS_Store .DS_Store
node_modules/ node_modules/
dist/ build/
.cache/ .cache/
npm-debug.log* npm-debug.log*
yarn-debug.log* yarn-debug.log*

View File

@ -84,7 +84,7 @@ CMD /bin/sh -c "yarn run dev"
FROM base as production FROM base as production
# Copy "binary"-files from build image # Copy "binary"-files from build image
COPY --from=build ${DOCKER_WORKDIR}/dist ./dist COPY --from=build ${DOCKER_WORKDIR}/build ./build
# We also copy the node_modules express and serve-static for the run script # We also copy the node_modules express and serve-static for the run script
COPY --from=build ${DOCKER_WORKDIR}/node_modules ./node_modules COPY --from=build ${DOCKER_WORKDIR}/node_modules ./node_modules
# Copy static files # Copy static files

View File

@ -7,7 +7,7 @@
"serve": "vue-cli-service serve --open", "serve": "vue-cli-service serve --open",
"build": "vue-cli-service build", "build": "vue-cli-service build",
"dev": "yarn run serve", "dev": "yarn run serve",
"analyse-bundle": "yarn build && webpack-bundle-analyzer dist/webpack.stats.json", "analyse-bundle": "yarn build && webpack-bundle-analyzer build/webpack.stats.json",
"lint": "eslint --max-warnings=0 --ext .js,.vue,.json .", "lint": "eslint --max-warnings=0 --ext .js,.vue,.json .",
"stylelint": "stylelint --max-warnings=0 '**/*.{scss,vue}'", "stylelint": "stylelint --max-warnings=0 '**/*.{scss,vue}'",
"test": "cross-env TZ=UTC jest", "test": "cross-env TZ=UTC jest",

View File

@ -9,10 +9,10 @@ const port = process.env.PORT || 3000
// Express Server // Express Server
const app = express() const app = express()
// Serve files // Serve files
app.use(express.static(path.join(__dirname, '../dist'))) app.use(express.static(path.join(__dirname, '../build')))
// Default to index.html // Default to index.html
app.get('*', (req, res) => { app.get('*', (req, res) => {
res.sendFile(path.join(__dirname, '../dist/index.html')) res.sendFile(path.join(__dirname, '../build/index.html'))
}) })
app.listen(port, hostname, () => { app.listen(port, hostname, () => {

View File

@ -3,15 +3,23 @@
<div class="bg-white appBoxShadow gradido-border-radius p-3"> <div class="bg-white appBoxShadow gradido-border-radius p-3">
<div class="h3 mb-4">{{ $t('form.send_check') }}</div> <div class="h3 mb-4">{{ $t('form.send_check') }}</div>
<b-row class="mt-5"> <b-row class="mt-5">
<b-col cols="2"></b-col> <b-col cols="12">
<b-col> <b-row class="mt-3">
<div class="h4">{{ userName ? userName : identifier }}</div> <b-col class="h5">{{ $t('form.recipientCommunity') }}</b-col>
<div class="mt-3 h5">{{ $t('form.memo') }}</div> <b-col>{{ communityName }}</b-col>
<div>{{ memo }}</div> </b-row>
</b-col> <b-row>
<b-col cols="3"> <b-col class="h5">{{ $t('form.recipient') }}</b-col>
<div class="small">{{ $t('send_gdd') }}</div> <b-col>{{ userName ? userName : identifier }}</b-col>
<div>{{ amount | GDD }}</div> </b-row>
<b-row>
<b-col class="h5">{{ $t('form.amount') }}</b-col>
<b-col>{{ amount | GDD }}</b-col>
</b-row>
<b-row>
<b-col class="h5">{{ $t('form.memo') }}</b-col>
<b-col>{{ memo }}</b-col>
</b-row>
</b-col> </b-col>
</b-row> </b-row>
@ -58,6 +66,8 @@
</div> </div>
</template> </template>
<script> <script>
import { COMMUNITY_NAME } from '@/config'
export default { export default {
name: 'TransactionConfirmationSend', name: 'TransactionConfirmationSend',
props: { props: {
@ -70,6 +80,7 @@ export default {
data() { data() {
return { return {
disabled: false, disabled: false,
communityName: COMMUNITY_NAME,
} }
}, },
} }

View File

@ -49,6 +49,14 @@
<b-row> <b-row>
<b-col> <b-col>
<b-row> <b-row>
<b-col class="mb-4" cols="12" v-if="radioSelected === sendTypes.send">
<b-row>
<b-col>{{ $t('form.recipientCommunity') }}</b-col>
</b-row>
<b-row>
<b-col class="font-weight-bold">{{ communityName }}</b-col>
</b-row>
</b-col>
<b-col cols="12" v-if="radioSelected === sendTypes.send"> <b-col cols="12" v-if="radioSelected === sendTypes.send">
<div v-if="!gradidoID"> <div v-if="!gradidoID">
<input-email <input-email
@ -131,6 +139,7 @@ import InputAmount from '@/components/Inputs/InputAmount'
import InputTextarea from '@/components/Inputs/InputTextarea' import InputTextarea from '@/components/Inputs/InputTextarea'
import { user as userQuery } from '@/graphql/queries' import { user as userQuery } from '@/graphql/queries'
import { isEmpty } from 'lodash' import { isEmpty } from 'lodash'
import { COMMUNITY_NAME } from '@/config'
export default { export default {
name: 'TransactionForm', name: 'TransactionForm',
@ -155,6 +164,7 @@ export default {
}, },
radioSelected: this.selected, radioSelected: this.selected,
userName: '', userName: '',
communityName: COMMUNITY_NAME,
} }
}, },
methods: { methods: {

View File

@ -8,7 +8,7 @@
containsLowercaseCharacter: true, containsLowercaseCharacter: true,
containsUppercaseCharacter: true, containsUppercaseCharacter: true,
containsNumericCharacter: true, containsNumericCharacter: true,
atLeastEightCharactera: true, atLeastEightCharacters: true,
atLeastOneSpecialCharater: true, atLeastOneSpecialCharater: true,
noWhitespaceCharacters: true, noWhitespaceCharacters: true,
}" }"

View File

@ -0,0 +1,71 @@
<template>
<validation-provider
tag="div"
:rules="rules"
:name="name"
:bails="!showAllErrors"
:immediate="immediate"
vid="username"
v-slot="{ errors, valid, validated, ariaInput, ariaMsg }"
>
<b-form-group :label-for="labelFor">
<b-form-input
v-model="currentValue"
v-bind="ariaInput"
:id="labelFor"
:name="name"
:placeholder="placeholder"
type="text"
:state="validated ? valid : false"
autocomplete="off"
></b-form-input>
<b-form-invalid-feedback v-bind="ariaMsg">
<div v-if="showAllErrors">
<span v-for="error in errors" :key="error">
{{ error }}
<br />
</span>
</div>
<div v-else>
{{ errors[0] }}
</div>
</b-form-invalid-feedback>
</b-form-group>
</validation-provider>
</template>
<script>
export default {
name: 'InputUsername',
props: {
rules: {
default: () => {
return {
required: true,
}
},
},
name: { type: String, default: 'username' },
label: { type: String, default: 'Username' },
placeholder: { type: String, default: 'Username' },
value: { required: true, type: String },
showAllErrors: { type: Boolean, default: false },
immediate: { type: Boolean, default: false },
unique: { type: Boolean, required: true },
},
data() {
return {
currentValue: this.value,
}
},
computed: {
labelFor() {
return this.name + '-input-field'
},
},
watch: {
currentValue() {
this.$emit('input', this.currentValue)
},
},
}
</script>

View File

@ -15,7 +15,7 @@ describe('LanguageSwitch', () => {
let wrapper let wrapper
const state = { const state = {
email: 'he@ho.he', gradidoID: 'current-user-id',
language: null, language: null,
} }

View File

@ -31,7 +31,7 @@ export default {
async saveLocale(locale) { async saveLocale(locale) {
// if (this.$i18n.locale === locale) return // if (this.$i18n.locale === locale) return
this.setLocale(locale) this.setLocale(locale)
if (this.$store.state.email) { if (this.$store.state.gradidoID) {
this.$apollo this.$apollo
.mutate({ .mutate({
mutation: updateUserInfos, mutation: updateUserInfos,

View File

@ -15,7 +15,7 @@ describe('LanguageSwitch', () => {
let wrapper let wrapper
const state = { const state = {
email: 'he@ho.he', gradidoID: 'current-user-id',
language: null, language: null,
} }

View File

@ -59,7 +59,7 @@ export default {
async saveLocale(locale) { async saveLocale(locale) {
if (this.$i18n.locale === locale) return if (this.$i18n.locale === locale) return
this.setLocale(locale) this.setLocale(locale)
if (this.$store.state.email) { if (this.$store.state.gradidoID) {
this.$apollo this.$apollo
.mutate({ .mutate({
mutation: updateUserInfos, mutation: updateUserInfos,

View File

@ -20,7 +20,7 @@ const mocks = {
state: { state: {
firstName: 'Testy', firstName: 'Testy',
lastName: 'User', lastName: 'User',
email: 'testy.user@example.com', gradidoID: 'current-user-id',
}, },
}, },
} }
@ -64,8 +64,8 @@ describe('AuthNavbar', () => {
) )
}) })
it('has the email address', () => { // I think this should be username
// expect(wrapper.find('div.small:nth-child(2)').text()).toBe(wrapper.vm.$store.state.email) it.skip('has the email address', () => {
expect(wrapper.find('div[data-test="navbar-item-email"]').text()).toBe( expect(wrapper.find('div[data-test="navbar-item-email"]').text()).toBe(
wrapper.vm.$store.state.email, wrapper.vm.$store.state.email,
) )

View File

@ -39,37 +39,5 @@ describe('AmountAndNameRow', () => {
expect(wrapper.find('div.gdd-transaction-list-item-name').find('a').exists()).toBe(false) expect(wrapper.find('div.gdd-transaction-list-item-name').find('a').exists()).toBe(false)
}) })
}) })
describe('with linked user', () => {
beforeEach(async () => {
await wrapper.setProps({
linkedUser: { firstName: 'Bibi', lastName: 'Bloxberg', email: 'bibi@bloxberg.de' },
})
})
it('has a link with first and last name', () => {
expect(wrapper.find('div.gdd-transaction-list-item-name').text()).toBe('Bibi Bloxberg')
})
it('has a link', () => {
expect(wrapper.find('div.gdd-transaction-list-item-name').find('a').exists()).toBe(true)
})
describe('click link', () => {
beforeEach(async () => {
await wrapper.find('div.gdd-transaction-list-item-name').find('a').trigger('click')
})
it('emits set tunneled email', () => {
expect(wrapper.emitted('set-tunneled-email')).toEqual([['bibi@bloxberg.de']])
})
it('pushes the route with query for email', () => {
expect(mocks.$router.push).toBeCalledWith({
path: '/send',
})
})
})
})
}) })
}) })

View File

@ -10,21 +10,7 @@
</b-col> </b-col>
<b-col cols="7"> <b-col cols="7">
<div class="gdd-transaction-list-item-name"> <div class="gdd-transaction-list-item-name">
<span v-if="linkedUser && linkedUser.email"> <span>{{ text }}</span>
<b-link @click.stop="tunnelEmail">
{{ itemText }}
</b-link>
</span>
<span v-else>{{ itemText }}</span>
<span v-if="linkId">
{{ $t('via_link') }}
<b-icon
icon="link45deg"
variant="muted"
class="m-mb-1"
:title="$t('gdd_per_link.redeemed-title')"
/>
</span>
</div> </div>
</b-col> </b-col>
</b-row> </b-row>
@ -38,31 +24,9 @@ export default {
type: String, type: String,
required: true, required: true,
}, },
linkedUser: {
type: Object,
required: false,
},
text: { text: {
type: String, type: String,
required: false, required: true,
},
linkId: {
type: Number,
required: false,
default: null,
},
},
methods: {
tunnelEmail() {
this.$emit('set-tunneled-email', this.linkedUser.email)
this.$router.push({ path: '/send' })
},
},
computed: {
itemText() {
return this.linkedUser
? this.linkedUser.firstName + ' ' + this.linkedUser.lastName
: this.text
}, },
}, },
} }

View File

@ -12,7 +12,7 @@
</template> </template>
<script> <script>
import { formatDistance } from 'date-fns' import { formatDistance } from 'date-fns'
import { en, de, es, fr, nl } from 'date-fns/locale' import { enUS as en, de, es, fr, nl } from 'date-fns/locale'
const locales = { en, de, es, fr, nl } const locales = { en, de, es, fr, nl }

View File

@ -0,0 +1,157 @@
import { mount } from '@vue/test-utils'
import UserName from './UserName'
import flushPromises from 'flush-promises'
import { toastErrorSpy, toastSuccessSpy } from '@test/testSetup'
const localVue = global.localVue
const mockAPIcall = jest.fn()
const storeCommitMock = jest.fn()
describe('UserName Form', () => {
let wrapper
const mocks = {
$t: jest.fn((t) => t),
$store: {
state: {
username: 'peter',
},
commit: storeCommitMock,
},
$apollo: {
mutate: mockAPIcall,
},
}
const Wrapper = () => {
return mount(UserName, { localVue, mocks })
}
describe('mount', () => {
beforeEach(() => {
wrapper = Wrapper()
})
it('renders the component', () => {
expect(wrapper.find('div#username_form').exists()).toBeTruthy()
})
it('has an edit icon', () => {
expect(wrapper.find('svg.bi-pencil').exists()).toBeTruthy()
})
it('renders the username', () => {
expect(wrapper.findAll('div.col').at(2).text()).toBe('peter')
})
describe('edit username', () => {
beforeEach(async () => {
await wrapper.find('svg.bi-pencil').trigger('click')
})
it('shows an cancel icon', () => {
expect(wrapper.find('svg.bi-x-circle').exists()).toBeTruthy()
})
it('closes the input when cancel icon is clicked', async () => {
await wrapper.find('svg.bi-x-circle').trigger('click')
expect(wrapper.find('input').exists()).toBeFalsy()
})
it('does not change the username when cancel is clicked', async () => {
await wrapper.find('input').setValue('petra')
await wrapper.find('svg.bi-x-circle').trigger('click')
expect(wrapper.findAll('div.col').at(2).text()).toBe('peter')
})
it('has a submit button', () => {
expect(wrapper.find('button[type="submit"]').exists()).toBeTruthy()
})
it('does not enable submit button when data is not changed', async () => {
await wrapper.find('form').trigger('keyup')
expect(wrapper.find('button[type="submit"]').attributes('disabled')).toBe('disabled')
})
describe('successfull submit', () => {
beforeEach(async () => {
mockAPIcall.mockResolvedValue({
data: {
updateUserInfos: {
validValues: 3,
},
},
})
jest.clearAllMocks()
await wrapper.find('input').setValue('petra')
await wrapper.find('form').trigger('keyup')
await wrapper.find('button[type="submit"]').trigger('click')
await flushPromises()
})
it('calls the API', () => {
expect(mockAPIcall).toBeCalledWith(
expect.objectContaining({
variables: {
alias: 'petra',
},
}),
)
})
it('commits username to store', () => {
expect(storeCommitMock).toBeCalledWith('username', 'petra')
})
it('toasts a success message', () => {
expect(toastSuccessSpy).toBeCalledWith('settings.username.change-success')
})
it('has an edit button again', () => {
expect(wrapper.find('svg.bi-pencil').exists()).toBeTruthy()
})
})
describe('submit results in server error', () => {
beforeEach(async () => {
mockAPIcall.mockRejectedValue({
message: 'Error',
})
jest.clearAllMocks()
await wrapper.find('input').setValue('petra')
await wrapper.find('form').trigger('keyup')
await wrapper.find('button[type="submit"]').trigger('click')
await flushPromises()
})
it('calls the API', () => {
expect(mockAPIcall).toBeCalledWith(
expect.objectContaining({
variables: {
alias: 'petra',
},
}),
)
})
it('toasts an error message', () => {
expect(toastErrorSpy).toBeCalledWith('Error')
})
})
})
describe('no username in store', () => {
beforeEach(() => {
mocks.$store.state.username = null
wrapper = Wrapper()
})
it('displays an information why to enter a username', () => {
expect(wrapper.findAll('div.col').at(2).text()).toBe('settings.username.no-username')
})
})
})
})

View File

@ -0,0 +1,130 @@
<template>
<b-card id="username_form" class="card-border-radius card-background-gray">
<div>
<b-row class="mb-4 text-right">
<b-col class="text-right">
<a
class="cursor-pointer"
@click="showUserData ? (showUserData = !showUserData) : cancelEdit()"
>
<span class="pointer mr-3">{{ $t('settings.username.change-username') }}</span>
<b-icon v-if="showUserData" class="pointer ml-3" icon="pencil"></b-icon>
<b-icon v-else icon="x-circle" class="pointer ml-3" variant="danger"></b-icon>
</a>
</b-col>
</b-row>
</div>
<div>
<validation-observer ref="usernameObserver" v-slot="{ handleSubmit, invalid }">
<b-form @submit.stop.prevent="handleSubmit(onSubmit)">
<b-row class="mb-3">
<b-col class="col-12">
<small>
<b>{{ $t('form.username') }}</b>
</small>
</b-col>
<b-col v-if="showUserData" class="col-12">
<span v-if="username">
{{ username }}
</span>
<div v-else class="alert">
{{ $t('settings.username.no-username') }}
</div>
</b-col>
<b-col v-else class="col-12">
<input-username
v-model="username"
:name="$t('form.username')"
:placeholder="$t('form.username-placeholder')"
:showAllErrors="true"
:unique="true"
:rules="rules"
/>
</b-col>
</b-row>
<b-row class="text-right" v-if="!showUserData">
<b-col>
<div class="text-right" ref="submitButton">
<b-button
:variant="disabled(invalid) ? 'light' : 'success'"
@click="onSubmit"
type="submit"
:disabled="disabled(invalid)"
>
{{ $t('form.save') }}
</b-button>
</div>
</b-col>
</b-row>
</b-form>
</validation-observer>
</div>
</b-card>
</template>
<script>
import { updateUserInfos } from '@/graphql/mutations'
import InputUsername from '@/components/Inputs/InputUsername'
export default {
name: 'UserName',
components: {
InputUsername,
},
data() {
return {
showUserData: true,
username: this.$store.state.username || '',
usernameUnique: false,
rules: {
required: true,
min: 3,
max: 20,
usernameAllowedChars: true,
usernameHyphens: true,
usernameUnique: true,
},
}
},
methods: {
cancelEdit() {
this.username = this.$store.state.username || ''
this.showUserData = true
},
async onSubmit(event) {
event.preventDefault()
this.$apollo
.mutate({
mutation: updateUserInfos,
variables: {
alias: this.username,
},
})
.then(() => {
this.$store.commit('username', this.username)
this.showUserData = true
this.toastSuccess(this.$t('settings.username.change-success'))
})
.catch((error) => {
this.toastError(error.message)
})
},
disabled(invalid) {
return !this.newUsername || invalid
},
},
computed: {
newUsername() {
return this.username !== this.$store.state.username
},
},
}
</script>
<style>
.cursor-pointer {
cursor: pointer;
}
div.alert {
color: red;
}
</style>

View File

@ -18,7 +18,6 @@ describe('UserCard_Newsletter', () => {
$store: { $store: {
state: { state: {
language: 'de', language: 'de',
email: 'peter@lustig.de',
newsletterState: true, newsletterState: true,
}, },
commit: storeCommitMock, commit: storeCommitMock,

View File

@ -26,6 +26,7 @@ export const forgotPassword = gql`
export const updateUserInfos = gql` export const updateUserInfos = gql`
mutation( mutation(
$alias: String
$firstName: String $firstName: String
$lastName: String $lastName: String
$password: String $password: String
@ -35,6 +36,7 @@ export const updateUserInfos = gql`
$hideAmountGDT: Boolean $hideAmountGDT: Boolean
) { ) {
updateUserInfos( updateUserInfos(
alias: $alias
firstName: $firstName firstName: $firstName
lastName: $lastName lastName: $lastName
password: $password password: $password
@ -145,7 +147,7 @@ export const login = gql`
mutation($email: String!, $password: String!, $publisherId: Int) { mutation($email: String!, $password: String!, $publisherId: Int) {
login(email: $email, password: $password, publisherId: $publisherId) { login(email: $email, password: $password, publisherId: $publisherId) {
gradidoID gradidoID
email alias
firstName firstName
lastName lastName
language language

View File

@ -3,7 +3,7 @@ import gql from 'graphql-tag'
export const verifyLogin = gql` export const verifyLogin = gql`
query { query {
verifyLogin { verifyLogin {
email gradidoID
firstName firstName
lastName lastName
language language
@ -40,7 +40,6 @@ export const transactionsQuery = gql`
firstName firstName
lastName lastName
gradidoID gradidoID
email
} }
decay { decay {
decay decay
@ -90,6 +89,12 @@ export const queryOptIn = gql`
} }
` `
export const checkUsername = gql`
query($username: String!) {
checkUsername(username: $username)
}
`
export const queryTransactionLink = gql` export const queryTransactionLink = gql`
query($code: String!) { query($code: String!) {
queryTransactionLink(code: $code) { queryTransactionLink(code: $code) {
@ -102,9 +107,9 @@ export const queryTransactionLink = gql`
redeemedAt redeemedAt
deletedAt deletedAt
user { user {
gradidoID
firstName firstName
publisherId publisherId
email
} }
} }
... on ContributionLink { ... on ContributionLink {

View File

@ -43,7 +43,6 @@ const mocks = {
$store: { $store: {
dispatch: storeDispatchMock, dispatch: storeDispatchMock,
state: { state: {
email: 'user@example.org',
publisherId: 123, publisherId: 123,
firstName: 'User', firstName: 'User',
lastName: 'Example', lastName: 'Example',
@ -260,34 +259,6 @@ describe('DashboardLayout', () => {
}) })
}) })
describe.skip('elopage URI', () => {
describe('user has no publisher ID and no elopage', () => {
beforeEach(() => {
mocks.$store.state.publisherId = null
mocks.$store.state.hasElopage = false
wrapper = Wrapper()
})
it('links to basic-de', () => {
expect(wrapper.vm.elopageUri).toBe(
'https://elopage.com/s/gradido/basic-de/payment?locale=en&prid=111&pid=2896&firstName=User&lastName=Example&email=user@example.org',
)
})
})
describe('user has elopage', () => {
beforeEach(() => {
mocks.$store.state.publisherId = '123'
mocks.$store.state.hasElopage = true
wrapper = Wrapper()
})
it('links to sign in for elopage', () => {
expect(wrapper.vm.elopageUri).toBe('https://elopage.com/s/gradido/sign_in?locale=en')
})
})
})
describe.skip('admin method', () => { describe.skip('admin method', () => {
const windowLocationMock = jest.fn() const windowLocationMock = jest.fn()
beforeEach(() => { beforeEach(() => {

View File

@ -153,6 +153,7 @@
"password_new_repeat": "Neues Passwort wiederholen", "password_new_repeat": "Neues Passwort wiederholen",
"password_old": "Altes Passwort", "password_old": "Altes Passwort",
"recipient": "Empfänger", "recipient": "Empfänger",
"recipientCommunity": "Gemeinschaft des Empfängers",
"reply": "Antworten", "reply": "Antworten",
"reset": "Zurücksetzen", "reset": "Zurücksetzen",
"save": "Speichern", "save": "Speichern",
@ -166,12 +167,15 @@
"thx": "Danke", "thx": "Danke",
"to": "bis", "to": "bis",
"to1": "an", "to1": "an",
"username": "Nutzername",
"username-placeholder": "Gebe einen eindeutigen Nutzernamen ein",
"validation": { "validation": {
"gddCreationTime": "Das Feld {_field_} muss eine Zahl zwischen {min} und {max} mit höchstens einer Nachkommastelle sein", "gddCreationTime": "Das Feld {_field_} muss eine Zahl zwischen {min} und {max} mit höchstens einer Nachkommastelle sein",
"gddSendAmount": "Das Feld {_field_} muss eine Zahl zwischen {min} und {max} mit höchstens zwei Nachkommastellen sein", "gddSendAmount": "Das Feld {_field_} muss eine Zahl zwischen {min} und {max} mit höchstens zwei Nachkommastellen sein",
"is-not": "Du kannst dir selbst keine Gradidos überweisen", "is-not": "Du kannst dir selbst keine Gradidos überweisen",
"usernmae-regex": "Der Username muss mit einem Buchstaben beginnen, auf den mindestens zwei alpha-numerische Zeichen folgen müssen.", "username-allowed-chars": "Der Nutzername darf nur aus Buchstaben (ohne Umlaute), Zahlen, Binde- oder Unterstrichen bestehen.",
"usernmae-unique": "Der Username ist bereits vergeben." "username-hyphens": "Binde- oder Unterstriche müssen zwischen Buchstaben oder Zahlen stehen.",
"username-unique": "Der Nutzername ist bereits vergeben."
}, },
"your_amount": "Dein Betrag" "your_amount": "Dein Betrag"
}, },
@ -319,7 +323,12 @@
"subtitle": "Wenn du dein Passwort vergessen hast, kannst du es hier zurücksetzen." "subtitle": "Wenn du dein Passwort vergessen hast, kannst du es hier zurücksetzen."
}, },
"showAmountGDD": "Dein GDD Betrag ist sichtbar.", "showAmountGDD": "Dein GDD Betrag ist sichtbar.",
"showAmountGDT": "Dein GDT Betrag ist sichtbar." "showAmountGDT": "Dein GDT Betrag ist sichtbar.",
"username": {
"change-success": "Dein Nutzername wurde erfolgreich geändert.",
"change-username": "Nutzername ändern",
"no-username": "Bitte gebe einen Nutzernamen ein. Damit hilfst du anderen Benutzern dich zu finden, ohne deine Email preisgeben zu müssen."
}
}, },
"signin": "Anmelden", "signin": "Anmelden",
"signup": "Registrieren", "signup": "Registrieren",

View File

@ -153,6 +153,7 @@
"password_new_repeat": "Repeat new password", "password_new_repeat": "Repeat new password",
"password_old": "Old password", "password_old": "Old password",
"recipient": "Recipient", "recipient": "Recipient",
"recipientCommunity": "Community of the recipient",
"reply": "Reply", "reply": "Reply",
"reset": "Reset", "reset": "Reset",
"save": "Save", "save": "Save",
@ -166,12 +167,15 @@
"thx": "Thank you", "thx": "Thank you",
"to": "to", "to": "to",
"to1": "to", "to1": "to",
"username": "Username",
"username-placeholder": "Enter a unique username",
"validation": { "validation": {
"gddCreationTime": "The field {_field_} must be a number between {min} and {max} with at most one decimal place.", "gddCreationTime": "The field {_field_} must be a number between {min} and {max} with at most one decimal place.",
"gddSendAmount": "The {_field_} field must be a number between {min} and {max} with at most two digits after the decimal point", "gddSendAmount": "The {_field_} field must be a number between {min} and {max} with at most two digits after the decimal point",
"is-not": "You cannot send Gradidos to yourself", "is-not": "You cannot send Gradidos to yourself",
"usernmae-regex": "The username must start with a letter, followed by at least two alphanumeric characters.", "username-allowed-chars": "The username may only contain letters, numbers, hyphens or underscores.",
"usernmae-unique": "This username is already taken." "username-hyphens": "Hyphens or underscores must be in between letters or numbers.",
"username-unique": "This username is already taken."
}, },
"your_amount": "Your amount" "your_amount": "Your amount"
}, },
@ -319,7 +323,12 @@
"subtitle": "If you have forgotten your password, you can reset it here." "subtitle": "If you have forgotten your password, you can reset it here."
}, },
"showAmountGDD": "Your GDD amount is visible.", "showAmountGDD": "Your GDD amount is visible.",
"showAmountGDT": "Your GDT amount is visible." "showAmountGDT": "Your GDT amount is visible.",
"username": {
"change-success": "Your username has been changed successfully.",
"change-username": "Change username",
"no-username": "Please enter a username. This helps other users to find you without exposing your email."
}
}, },
"signin": "Sign in", "signin": "Sign in",
"signup": "Sign up", "signup": "Sign up",

View File

@ -27,7 +27,7 @@ const filters = loadFilters(i18n)
Vue.filter('amount', filters.amount) Vue.filter('amount', filters.amount)
Vue.filter('GDD', filters.GDD) Vue.filter('GDD', filters.GDD)
loadAllRules(i18n) loadAllRules(i18n, apolloProvider.defaultClient)
addNavigationGuards(router, store, apolloProvider.defaultClient) addNavigationGuards(router, store, apolloProvider.defaultClient)

View File

@ -3,6 +3,8 @@
<user-card :balance="balance" :transactionCount="transactionCount"></user-card> <user-card :balance="balance" :transactionCount="transactionCount"></user-card>
<user-data /> <user-data />
<hr /> <hr />
<user-name />
<hr />
<user-password /> <user-password />
<hr /> <hr />
<user-language /> <user-language />
@ -13,6 +15,7 @@
<script> <script>
import UserCard from '@/components/UserSettings/UserCard' import UserCard from '@/components/UserSettings/UserCard'
import UserData from '@/components/UserSettings/UserData' import UserData from '@/components/UserSettings/UserData'
import UserName from '@/components/UserSettings/UserName'
import UserPassword from '@/components/UserSettings/UserPassword' import UserPassword from '@/components/UserSettings/UserPassword'
import UserLanguage from '@/components/UserSettings/UserLanguage' import UserLanguage from '@/components/UserSettings/UserLanguage'
import UserNewsletter from '@/components/UserSettings/UserNewsletter' import UserNewsletter from '@/components/UserSettings/UserNewsletter'
@ -22,6 +25,7 @@ export default {
components: { components: {
UserCard, UserCard,
UserData, UserData,
UserName,
UserPassword, UserPassword,
UserLanguage, UserLanguage,
UserNewsletter, UserNewsletter,

Some files were not shown because too many files have changed in this diff Show More