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/
dist/
build/
coverage/

2
admin/.gitignore vendored
View File

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

View File

@ -84,7 +84,7 @@ CMD /bin/sh -c "yarn run dev"
FROM base as production
# 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
COPY --from=build ${DOCKER_WORKDIR}/node_modules ./node_modules
# Copy static files

View File

@ -11,7 +11,7 @@
"serve": "vue-cli-service serve --open",
"build": "vue-cli-service build",
"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 .",
"stylelint": "stylelint --max-warnings=0 '**/*.{scss,vue}'",
"test": "cross-env TZ=UTC jest",

View File

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

View File

@ -13,7 +13,7 @@
</template>
<script>
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 }

View File

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

View File

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

View File

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

View File

@ -12,6 +12,8 @@ module.exports = {
'plugin:prettier/recommended',
'plugin:import/recommended',
'plugin:import/typescript',
'plugin:security/recommended',
'plugin:@eslint-community/eslint-comments/recommended',
],
settings: {
'import/parsers': {
@ -151,6 +153,11 @@ module.exports = {
'promise/valid-params': 'warn',
'promise/prefer-await-to-callbacks': '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: [
// only for ts files
@ -159,6 +166,7 @@ module.exports = {
extends: [
'plugin:@typescript-eslint/recommended',
'plugin:@typescript-eslint/recommended-requiring-type-checking',
'plugin:@typescript-eslint/strict',
'plugin:type-graphql/recommended',
],
rules: {
@ -169,6 +177,8 @@ module.exports = {
'@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
'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: {
tsconfigRootDir: __dirname,

View File

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

View File

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

View File

@ -7,7 +7,6 @@ import axios from 'axios'
import { LogError } from '@/server/LogError'
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> => {
logger.trace('POST', url, payload)
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> => {
logger.trace('GET: url=' + url)
try {

View File

@ -12,7 +12,7 @@ import KlicktippConnector from 'klicktipp-api'
const klicktippConnector = new KlicktippConnector()
export const klicktippSignIn = async (
export const subscribe = async (
email: string,
language: string,
firstName?: string,
@ -28,13 +28,6 @@ export const klicktippSignIn = async (
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> => {
if (!CONFIG.KLICKTIPP) return true
const isLogin = await loginKlicktippUser()
@ -60,38 +53,6 @@ export const loginKlicktippUser = async (): Promise<boolean> => {
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 (
email: string,
fields: any = {},

View File

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

View File

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

View File

@ -12,11 +12,11 @@ Decimal.set({
})
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
LOG4JS_CONFIG: 'log4js-config.json',
// default log level on production should be info
LOG_LEVEL: process.env.LOG_LEVEL || 'info',
LOG_LEVEL: process.env.LOG_LEVEL ?? 'info',
CONFIG_VERSION: {
DEFAULT: 'DEFAULT',
EXPECTED: 'v15.2023-02-07',
@ -25,67 +25,67 @@ const constants = {
}
const server = {
PORT: process.env.PORT || 4000,
JWT_SECRET: process.env.JWT_SECRET || 'secret123',
JWT_EXPIRES_IN: process.env.JWT_EXPIRES_IN || '10m',
PORT: process.env.PORT ?? 4000,
JWT_SECRET: process.env.JWT_SECRET ?? 'secret123',
JWT_EXPIRES_IN: process.env.JWT_EXPIRES_IN ?? '10m',
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,
}
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_USER: process.env.DB_USER || 'root',
DB_PASSWORD: process.env.DB_PASSWORD || '',
DB_DATABASE: process.env.DB_DATABASE || 'gradido_community',
TYPEORM_LOGGING_RELATIVE_PATH: process.env.TYPEORM_LOGGING_RELATIVE_PATH || 'typeorm.backend.log',
DB_USER: process.env.DB_USER ?? 'root',
DB_PASSWORD: process.env.DB_PASSWORD ?? '',
DB_DATABASE: process.env.DB_DATABASE ?? 'gradido_community',
TYPEORM_LOGGING_RELATIVE_PATH: process.env.TYPEORM_LOGGING_RELATIVE_PATH ?? 'typeorm.backend.log',
}
const klicktipp = {
KLICKTIPP: process.env.KLICKTIPP === 'true' || false,
KLICKTTIPP_API_URL: process.env.KLICKTIPP_API_URL || 'https://api.klicktipp.com',
KLICKTIPP_USER: process.env.KLICKTIPP_USER || 'gradido_test',
KLICKTIPP_PASSWORD: process.env.KLICKTIPP_PASSWORD || 'secret321',
KLICKTIPP_APIKEY_DE: process.env.KLICKTIPP_APIKEY_DE || 'SomeFakeKeyDE',
KLICKTIPP_APIKEY_EN: process.env.KLICKTIPP_APIKEY_EN || 'SomeFakeKeyEN',
KLICKTTIPP_API_URL: process.env.KLICKTIPP_API_URL ?? 'https://api.klicktipp.com',
KLICKTIPP_USER: process.env.KLICKTIPP_USER ?? 'gradido_test',
KLICKTIPP_PASSWORD: process.env.KLICKTIPP_PASSWORD ?? 'secret321',
KLICKTIPP_APIKEY_DE: process.env.KLICKTIPP_APIKEY_DE ?? 'SomeFakeKeyDE',
KLICKTIPP_APIKEY_EN: process.env.KLICKTIPP_APIKEY_EN ?? 'SomeFakeKeyEN',
}
const community = {
COMMUNITY_NAME: process.env.COMMUNITY_NAME || 'Gradido Entwicklung',
COMMUNITY_URL: process.env.COMMUNITY_URL || 'http://localhost/',
COMMUNITY_REGISTER_URL: process.env.COMMUNITY_REGISTER_URL || 'http://localhost/register',
COMMUNITY_REDEEM_URL: process.env.COMMUNITY_REDEEM_URL || 'http://localhost/redeem/{code}',
COMMUNITY_NAME: process.env.COMMUNITY_NAME ?? 'Gradido Entwicklung',
COMMUNITY_URL: process.env.COMMUNITY_URL ?? 'http://localhost/',
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_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:
process.env.COMMUNITY_DESCRIPTION || 'Die lokale Entwicklungsumgebung von Gradido.',
COMMUNITY_SUPPORT_MAIL: process.env.COMMUNITY_SUPPORT_MAIL || 'support@supportmail.com',
process.env.COMMUNITY_DESCRIPTION ?? 'Die lokale Entwicklungsumgebung von Gradido.',
COMMUNITY_SUPPORT_MAIL: process.env.COMMUNITY_SUPPORT_MAIL ?? 'support@supportmail.com',
}
const loginServer = {
LOGIN_APP_SECRET: process.env.LOGIN_APP_SECRET || '21ffbbc616fe',
LOGIN_SERVER_KEY: process.env.LOGIN_SERVER_KEY || 'a51ef8ac7ef1abf162fb7a65261acd7a',
LOGIN_APP_SECRET: process.env.LOGIN_APP_SECRET ?? '21ffbbc616fe',
LOGIN_SERVER_KEY: process.env.LOGIN_SERVER_KEY ?? 'a51ef8ac7ef1abf162fb7a65261acd7a',
}
const email = {
EMAIL: process.env.EMAIL === 'true' || false,
EMAIL_TEST_MODUS: process.env.EMAIL_TEST_MODUS === 'true' || false,
EMAIL_TEST_RECEIVER: process.env.EMAIL_TEST_RECEIVER || 'stage1@gradido.net',
EMAIL_USERNAME: process.env.EMAIL_USERNAME || '',
EMAIL_SENDER: process.env.EMAIL_SENDER || 'info@gradido.net',
EMAIL_PASSWORD: process.env.EMAIL_PASSWORD || '',
EMAIL_SMTP_URL: process.env.EMAIL_SMTP_URL || 'mailserver',
EMAIL_TEST_RECEIVER: process.env.EMAIL_TEST_RECEIVER ?? 'stage1@gradido.net',
EMAIL_USERNAME: process.env.EMAIL_USERNAME ?? '',
EMAIL_SENDER: process.env.EMAIL_SENDER ?? 'info@gradido.net',
EMAIL_PASSWORD: process.env.EMAIL_PASSWORD ?? '',
EMAIL_SMTP_URL: process.env.EMAIL_SMTP_URL ?? 'mailserver',
EMAIL_SMTP_PORT: Number(process.env.EMAIL_SMTP_PORT) || 1025,
// eslint-disable-next-line no-unneeded-ternary
EMAIL_TLS: process.env.EMAIL_TLS === 'false' ? false : true,
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:
process.env.EMAIL_LINK_SETPASSWORD || 'http://localhost/reset-password/{optin}',
process.env.EMAIL_LINK_SETPASSWORD ?? 'http://localhost/reset-password/{optin}',
EMAIL_LINK_FORGOTPASSWORD:
process.env.EMAIL_LINK_FORGOTPASSWORD || 'http://localhost/forgot-password',
EMAIL_LINK_OVERVIEW: process.env.EMAIL_LINK_OVERVIEW || 'http://localhost/overview',
process.env.EMAIL_LINK_FORGOTPASSWORD ?? 'http://localhost/forgot-password',
EMAIL_LINK_OVERVIEW: process.env.EMAIL_LINK_OVERVIEW ?? 'http://localhost/overview',
// time in minutes a optin code is valid
EMAIL_CODE_VALID_TIME: process.env.EMAIL_CODE_VALID_TIME
? parseInt(process.env.EMAIL_CODE_VALID_TIME) || 1440
@ -98,14 +98,14 @@ const email = {
const webhook = {
// 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
process.env.APP_SECRET = server.JWT_SECRET
// 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 (
![constants.CONFIG_VERSION.EXPECTED, constants.CONFIG_VERSION.DEFAULT].includes(
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 ', () => {
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 ', () => {
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 ', () => {
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 ', () => {
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 ', () => {
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 ', () => {
expect(logger.warn).toBeCalledWith(
`Federation: dbCom: ${dbCom.id} with unsupported apiVersion=2_0; supported versions`,
['1_0', '1_1'],
'Federation: dbCom with unsupported apiVersion',
dbCom.endPoint,
'2_0',
)
})
})

View File

@ -3,13 +3,9 @@
import { IsNull } from '@dbTools/typeorm'
import { FederatedCommunity as DbFederatedCommunity } from '@entity/FederatedCommunity'
import { LogError } from '@/server/LogError'
import { backendLogger as logger } from '@/server/logger'
// eslint-disable-next-line camelcase
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 { Client } from './client/Client'
import { ApiVersionType } from './enum/apiVersionType'
export function startValidateCommunities(timerInterval: number): void {
@ -36,56 +32,25 @@ export async function validateCommunities(): Promise<void> {
logger.debug('Federation: dbCom', dbCom)
const apiValueStrings: string[] = Object.values(ApiVersionType)
logger.debug(`suppported ApiVersions=`, apiValueStrings)
if (apiValueStrings.includes(dbCom.apiVersion)) {
logger.debug(
`Federation: validate publicKey for dbCom: ${dbCom.id} with apiVersion=${dbCom.apiVersion}`,
)
try {
const pubKey = await invokeVersionedRequestGetPublicKey(dbCom)
logger.info(
'Federation: received publicKey from endpoint',
if (!apiValueStrings.includes(dbCom.apiVersion)) {
logger.warn('Federation: dbCom with unsupported apiVersion', dbCom.endPoint, dbCom.apiVersion)
continue
}
try {
const client = Client.getInstance(dbCom)
const pubKey = await client?.getPublicKey()
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,
`${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 {
logger.warn(
`Federation: dbCom: ${dbCom.id} with unsupported apiVersion=${dbCom.apiVersion}; supported versions`,
apiValueStrings,
)
} catch (err) {
logger.error(`Error:`, err)
}
}
}
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 })
lastName?: string
@Field({ nullable: true })
alias?: string
@Field({ nullable: true })
language?: string

View File

@ -12,7 +12,7 @@ export const isAuthorized: AuthChecker<Context> = async ({ context }, rights) =>
context.role = ROLE_UNAUTHORIZED // unauthorized user
// 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
// Do we have a token?
@ -43,7 +43,7 @@ export const isAuthorized: AuthChecker<Context> = async ({ context }, 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) {
throw new LogError('401 Unauthorized')
}

View File

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

View File

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

View File

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

View File

@ -70,7 +70,10 @@ export class BalanceResolver {
now,
)
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
@ -96,9 +99,7 @@ export class BalanceResolver {
count,
linkCount,
})
logger.info(
`new Balance(balance=${balance}, balanceGDT=${balanceGDT}, count=${count}, linkCount=${linkCount}) = ${newBalance}`,
)
logger.info('new Balance', balance, balanceGDT, count, linkCount, newBalance)
return newBalance
}

View File

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

View File

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

View File

@ -1,6 +1,6 @@
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 { EVENT_NEWSLETTER_SUBSCRIBE, EVENT_NEWSLETTER_UNSUBSCRIBE } from '@/event/Events'
import { Context, getUser } from '@/server/context'
@ -20,6 +20,6 @@ export class KlicktippResolver {
async subscribeNewsletter(@Ctx() context: Context): Promise<boolean> {
const user = getUser(context)
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(
(transactionLink) => transactionLink.email === 'bibi@bloxberg.de',
)
for (let i = 0; i < bibisTransaktionLinks.length; i++) {
await transactionLinkFactory(testEnv, bibisTransaktionLinks[i])
for (const bibisTransaktionLink of bibisTransaktionLinks) {
await transactionLinkFactory(testEnv, bibisTransaktionLink)
}
// admin: only now log in
@ -1040,6 +1040,7 @@ describe('TransactionLinkResolver', () => {
})
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) + '$')
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 { calculateDecay } from '@/util/decay'
import { TRANSACTIONS_LOCK } from '@/util/TRANSACTIONS_LOCK'
import { fullName } from '@/util/utilities'
import { calculateBalance } from '@/util/validate'
import { executeTransaction } from './TransactionResolver'
@ -146,7 +147,7 @@ export class TransactionLinkResolver {
const transactionLink = await DbTransactionLink.findOneOrFail({ code }, { withDeleted: true })
const user = await DbUser.findOneOrFail({ id: transactionLink.userId })
let redeemedBy: User | null = null
if (transactionLink && transactionLink.redeemedBy) {
if (transactionLink?.redeemedBy) {
redeemedBy = new User(await DbUser.findOneOrFail({ id: transactionLink.redeemedBy }))
}
return new TransactionLink(transactionLink, new User(user), redeemedBy)
@ -266,6 +267,8 @@ export class TransactionLinkResolver {
transaction.typeId = TransactionTypeId.CREATION
transaction.memo = contribution.memo
transaction.userId = contribution.userId
transaction.userGradidoID = user.gradidoID
transaction.userName = fullName(user.firstName, user.lastName)
transaction.previous = lastTransaction ? lastTransaction.id : null
transaction.amount = contribution.amount
transaction.creationDate = contribution.contributionDate

View File

@ -20,12 +20,15 @@ import {
login,
sendCoins,
} from '@/seeds/graphql/mutations'
import { transactionsQuery } from '@/seeds/graphql/queries'
import { bobBaumeister } from '@/seeds/users/bob-baumeister'
import { garrickOllivander } from '@/seeds/users/garrick-ollivander'
import { peterLustig } from '@/seeds/users/peter-lustig'
import { stephenHawking } from '@/seeds/users/stephen-hawking'
let mutate: ApolloServerTestClient['mutate'], con: Connection
let query: ApolloServerTestClient['query']
let testEnv: {
mutate: ApolloServerTestClient['mutate']
query: ApolloServerTestClient['query']
@ -35,6 +38,7 @@ let testEnv: {
beforeAll(async () => {
testEnv = await testEnvironment(logger)
mutate = testEnv.mutate
query = testEnv.query
con = testEnv.con
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 { communityUser } from '@/util/communityUser'
import { TRANSACTIONS_LOCK } from '@/util/TRANSACTIONS_LOCK'
import { fullName } from '@/util/utilities'
import { calculateBalance } from '@/util/validate'
import { virtualLinkTransaction, virtualDecayTransaction } from '@/util/virtualTransactions'
@ -48,9 +49,7 @@ export const executeTransaction = async (
// acquire lock
const releaseLock = await TRANSACTIONS_LOCK.acquire()
try {
logger.info(
`executeTransaction(amount=${amount}, memo=${memo}, sender=${sender}, recipient=${recipient})...`,
)
logger.info('executeTransaction', amount, memo, sender, recipient)
if (sender.id === recipient.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.memo = memo
transactionSend.userId = sender.id
transactionSend.userGradidoID = sender.gradidoID
transactionSend.userName = fullName(sender.firstName, sender.lastName)
transactionSend.linkedUserId = recipient.id
transactionSend.linkedUserGradidoID = recipient.gradidoID
transactionSend.linkedUserName = fullName(recipient.firstName, recipient.lastName)
transactionSend.amount = amount.mul(-1)
transactionSend.balance = sendBalance.balance
transactionSend.balanceDate = receivedCallDate
@ -103,7 +106,11 @@ export const executeTransaction = async (
transactionReceive.typeId = TransactionTypeId.RECEIVE
transactionReceive.memo = memo
transactionReceive.userId = recipient.id
transactionReceive.userGradidoID = recipient.gradidoID
transactionReceive.userName = fullName(recipient.firstName, recipient.lastName)
transactionReceive.linkedUserId = sender.id
transactionReceive.linkedUserGradidoID = sender.gradidoID
transactionReceive.linkedUserName = fullName(sender.firstName, sender.lastName)
transactionReceive.amount = amount
const receiveBalance = await calculateBalance(recipient.id, amount, receivedCallDate)
transactionReceive.balance = receiveBalance ? receiveBalance.balance : amount
@ -119,10 +126,10 @@ export const executeTransaction = async (
// Save linked transaction id for send
transactionSend.linkedTransactionId = transactionReceive.id
await queryRunner.manager.update(dbTransaction, { id: transactionSend.id }, transactionSend)
logger.debug(`send Transaction updated: ${transactionSend}`)
logger.debug('send Transaction updated', transactionSend)
if (transactionLink) {
logger.info(`transactionLink: ${transactionLink}`)
logger.info('transactionLink', transactionLink)
transactionLink.redeemedAt = receivedCallDate
transactionLink.redeemedBy = recipient.id
await queryRunner.manager.update(
@ -271,8 +278,8 @@ export class TransactionResolver {
sumAmount.mul(-1),
sumHoldAvailableAmount.mul(-1),
sumHoldAvailableAmount.minus(sumAmount.toString()).mul(-1),
firstDate || now,
lastDate || now,
firstDate ?? now,
lastDate ?? now,
self,
(userTransactions.length && userTransactions[0].balance) || new Decimal(0),
),
@ -325,9 +332,7 @@ export class TransactionResolver {
}
await executeTransaction(amount, memo, senderUser, recipientUser)
logger.info(
`successful executeTransaction(amount=${amount}, memo=${memo}, senderUser=${senderUser}, recipientUser=${recipientUser})`,
)
logger.info('successful executeTransaction', amount, memo, senderUser, recipientUser)
return true
}
}

View File

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

View File

@ -35,7 +35,7 @@ import { User } from '@model/User'
import { UserAdmin, SearchUsersResult } from '@model/UserAdmin'
import { UserRepository } from '@repository/User'
import { klicktippSignIn } from '@/apis/KlicktippController'
import { subscribe } from '@/apis/KlicktippController'
import { encode } from '@/auth/JWT'
import { RIGHTS } from '@/auth/RIGHTS'
import { CONFIG } from '@/config'
@ -73,6 +73,7 @@ import { getTimeDurationObject, printTimeDuration } from '@/util/time'
import { FULL_CREATION_AVAILABLE } from './const/const'
import { getUserCreations } from './util/creations'
import { findUserByIdentifier } from './util/findUserByIdentifier'
import { validateAlias } from './util/validateAlias'
// eslint-disable-next-line @typescript-eslint/no-var-requires, import/no-commonjs
const random = require('random-bigint')
@ -94,7 +95,7 @@ const newEmailContact = (email: string, userId: number): DbUserContact => {
emailContact.emailChecked = false
emailContact.emailOptInTypeId = OptInType.EMAIL_OPT_IN_REGISTER
emailContact.emailVerificationCode = random(64)
logger.debug(`newEmailContact...successful: ${emailContact}`)
logger.debug('newEmailContact...successful', emailContact)
return emailContact
}
@ -130,7 +131,7 @@ export class UserResolver {
// Elopage Status & Stored PublisherId
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
}
@ -225,7 +226,7 @@ export class UserResolver {
email = email.trim().toLowerCase()
if (await checkEmailExists(email)) {
const foundUser = await findUserByEmail(email)
logger.info(`DbUser.findOne(email=${email}) = ${foundUser}`)
logger.info('DbUser.findOne', email, foundUser)
if (foundUser) {
// ATTENTION: this logger-message will be exactly expected during tests, next line
@ -238,7 +239,6 @@ export class UserResolver {
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.gradidoID = uuidv4()
user.email = email
user.firstName = firstName
user.lastName = lastName
user.language = language
@ -276,7 +276,7 @@ export class UserResolver {
dbUser.firstName = firstName
dbUser.lastName = lastName
dbUser.language = language
dbUser.publisherId = publisherId || 0
dbUser.publisherId = publisherId ?? 0
dbUser.passwordEncryptionType = PasswordEncryptionType.NO_PASSWORD
logger.debug('new dbUser', dbUser)
if (redeemCode) {
@ -383,7 +383,7 @@ export class UserResolver {
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({
firstName: user.firstName,
@ -469,9 +469,9 @@ export class UserResolver {
// TODO do we always signUp the user? How to handle things with old users?
if (userContact.emailOptInTypeId === OptInType.EMAIL_OPT_IN_REGISTER) {
try {
await klicktippSignIn(userContact.email, user.language, user.firstName, user.lastName)
await subscribe(userContact.email, user.language, user.firstName, user.lastName)
logger.debug(
`klicktippSignIn(${userContact.email}, ${user.language}, ${user.firstName}, ${user.lastName})`,
`subscribe(${userContact.email}, ${user.language}, ${user.firstName}, ${user.lastName})`,
)
} catch (e) {
logger.error('Error subscribing to klicktipp', e)
@ -487,7 +487,7 @@ export class UserResolver {
async queryOptIn(@Arg('optIn') optIn: string): Promise<boolean> {
logger.info(`queryOptIn(${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
if (!isEmailVerificationCodeValid(userContact.updatedAt || userContact.createdAt)) {
throw new LogError(
@ -498,6 +498,17 @@ export class UserResolver {
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])
@Mutation(() => Boolean)
async updateUserInfos(
@ -505,6 +516,7 @@ export class UserResolver {
{
firstName,
lastName,
alias,
language,
password,
passwordNew,
@ -524,6 +536,10 @@ export class UserResolver {
user.lastName = lastName
}
if (alias && (await validateAlias(alias))) {
user.alias = alias
}
if (language) {
if (!isLanguage(language)) {
throw new LogError('Given language is not a valid language', language)
@ -587,7 +603,7 @@ export class UserResolver {
logger.info(`hasElopage()...`)
const userEntity = getUser(context)
const elopageBuys = hasElopageBuys(userEntity.emailContact.email)
logger.debug(`has ElopageBuys = ${elopageBuys}`)
logger.debug('has ElopageBuys', elopageBuys)
return elopageBuys
}
@ -644,7 +660,7 @@ export class UserResolver {
return 'user.' + fieldName
}),
searchText,
filters || null,
filters ?? null,
currentPage,
pageSize,
)
@ -710,14 +726,14 @@ export class UserResolver {
// change isAdmin
switch (user.isAdmin) {
case null:
if (isAdmin === true) {
if (isAdmin) {
user.isAdmin = new Date()
} else {
throw new LogError('User is already an usual user')
}
break
default:
if (isAdmin === false) {
if (!isAdmin) {
user.isAdmin = null
} else {
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)
}
// eslint-disable-next-line security/detect-object-injection
if (amount.greaterThan(creations[index].toString())) {
throw new LogError(
'The amount to be created exceeds the amount still available for this month',
amount,
// eslint-disable-next-line security/detect-object-injection
creations[index],
)
}
@ -151,6 +153,7 @@ export const updateCreations = (
if (index < 0) {
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())
return creations
}
@ -169,6 +172,7 @@ export const getOpenCreations = async (
return {
month: date.getMonth(),
year: date.getFullYear(),
// eslint-disable-next-line security/detect-object-injection
amount: creations[index],
}
})

View File

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

View File

@ -14,7 +14,7 @@ export async function transactionLinkList(
filters: TransactionLinkFilters | null,
user: DbUser,
): Promise<TransactionLinkResult> {
const { withDeleted, withExpired, withRedeemed } = filters || {
const { withDeleted, withExpired, withRedeemed } = filters ?? {
withDeleted: false,
withExpired: 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}`)
}
})
void startValidateCommunities(Number(CONFIG.FEDERATION_VALIDATE_COMMUNITY_TIMER))
startValidateCommunities(Number(CONFIG.FEDERATION_VALIDATE_COMMUNITY_TIMER))
}
main().catch((e) => {

View File

@ -10,19 +10,6 @@ import { KlickTipp } from '@model/KlickTipp'
import { getKlickTippUser } from '@/apis/KlicktippController'
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 (
/* eslint-disable-next-line @typescript-eslint/no-unused-vars */
{ root, args, context, info },

View File

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

View File

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

View File

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

View File

@ -21,7 +21,11 @@ import { plugins } from './plugins'
// TODO implement
// 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 (
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@ -34,7 +38,7 @@ export const createServer = async (
// open mysql connection
const con = await connection()
if (!con || !con.isConnected) {
if (!con?.isConnected) {
logger.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'
// eslint-disable-next-line security/detect-non-literal-fs-filename
const options = JSON.parse(readFileSync(CONFIG.LOG4JS_CONFIG, 'utf-8'))
options.categories.backend.level = CONFIG.LOG_LEVEL

View File

@ -12,7 +12,7 @@ const setHeadersPlugin = {
return {
willSendResponse(requestContext: any) {
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)) {
requestContext.response.http.headers.set(key, value)
} else {
@ -27,8 +27,8 @@ const setHeadersPlugin = {
const filterVariables = (variables: any) => {
const vars = clonedeep(variables)
if (vars && vars.password) vars.password = '***'
if (vars && vars.passwordNew) vars.passwordNew = '***'
if (vars?.password) vars.password = '***'
if (vars?.passwordNew) vars.passwordNew = '***'
return vars
}

View File

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

View File

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

View File

@ -1,11 +1,9 @@
import { Decimal } from 'decimal.js-light'
import i18n from 'i18n'
export const objectValuesToArray = (obj: { [x: string]: string }): Array<string> => {
return Object.keys(obj).map(function (key) {
return obj[key]
})
}
export const objectValuesToArray = (obj: Record<string, string>): string[] =>
// eslint-disable-next-line security/detect-object-injection
Object.keys(obj).map((key) => obj[key])
export const decimalSeparatorByLanguage = (a: Decimal, language: string): string => {
const rememberLocaleToRestore = i18n.getLocale()
@ -14,3 +12,6 @@ export const decimalSeparatorByLanguage = (a: Decimal, language: string): string
i18n.setLocale(rememberLocaleToRestore)
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,
contribution: null,
...defaultModelFunctions,
userGradidoID: '',
userName: null,
linkedUserGradidoID: null,
linkedUserName: null,
}
return new Transaction(linkDbTransaction, user)
}
@ -84,6 +88,10 @@ const virtualDecayTransaction = (
creationDate: null,
contribution: null,
...defaultModelFunctions,
userGradidoID: '',
userName: null,
linkedUserGradidoID: null,
linkedUserName: null,
}
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
// 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_NAME = /^<>&;]{2,}$/
@ -146,7 +147,7 @@ export const elopageWebhook = async (req: any, res: any): Promise<void> => {
email,
firstName,
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) {
// eslint-disable-next-line no-console

View File

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

View File

@ -382,6 +382,14 @@
dependencies:
"@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":
version "4.2.0"
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"
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:
version "1.0.0"
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"
integrity sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw==
ignore@^5.2.0:
ignore@^5.2.0, ignore@^5.2.4:
version "5.2.4"
resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.4.tgz#a291c0c6178ff1b960befe47fcdec301674a6324"
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"
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:
version "1.4.3"
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"
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":
version "2.1.2"
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
#location /vue {
# alias /var/www/html/gradido/frontend/dist;
# alias /var/www/html/gradido/frontend/build;
# index index.html;
#
# location ~* \.(png)$ {

View File

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

View File

@ -15,6 +15,6 @@ export NVM_DIR="/root/.nvm"
$NPM_BIN install
$NPM_BIN run build
# prezip for faster deliver throw nginx
cd dist
cd build
find . -type f -name "*.css" -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/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
cp -f $PROJECT_ROOT/database/.env $PROJECT_ROOT/database/.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()
const constants = {
DB_VERSION: '0065-refactor_communities_table',
DB_VERSION: '0066-x-community-sendcoins-transactions_table',
LOG4JS_CONFIG: 'log4js-config.json',
// default log level on production should be info
LOG_LEVEL: process.env.LOG_LEVEL || 'info',

View File

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

View File

@ -35,6 +35,6 @@ Cypress.Commands.add('login', (email, password) => {
}
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 = {
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
LOG4JS_CONFIG: 'log4js-config.json',
// default log level on production should be info

View File

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

View File

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

2
frontend/.gitignore vendored
View File

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

View File

@ -84,7 +84,7 @@ CMD /bin/sh -c "yarn run dev"
FROM base as production
# 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
COPY --from=build ${DOCKER_WORKDIR}/node_modules ./node_modules
# Copy static files

View File

@ -7,7 +7,7 @@
"serve": "vue-cli-service serve --open",
"build": "vue-cli-service build",
"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 .",
"stylelint": "stylelint --max-warnings=0 '**/*.{scss,vue}'",
"test": "cross-env TZ=UTC jest",

View File

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

View File

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

View File

@ -49,6 +49,14 @@
<b-row>
<b-col>
<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">
<div v-if="!gradidoID">
<input-email
@ -131,6 +139,7 @@ import InputAmount from '@/components/Inputs/InputAmount'
import InputTextarea from '@/components/Inputs/InputTextarea'
import { user as userQuery } from '@/graphql/queries'
import { isEmpty } from 'lodash'
import { COMMUNITY_NAME } from '@/config'
export default {
name: 'TransactionForm',
@ -155,6 +164,7 @@ export default {
},
radioSelected: this.selected,
userName: '',
communityName: COMMUNITY_NAME,
}
},
methods: {

View File

@ -8,7 +8,7 @@
containsLowercaseCharacter: true,
containsUppercaseCharacter: true,
containsNumericCharacter: true,
atLeastEightCharactera: true,
atLeastEightCharacters: true,
atLeastOneSpecialCharater: 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
const state = {
email: 'he@ho.he',
gradidoID: 'current-user-id',
language: null,
}

View File

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

View File

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

View File

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

View File

@ -20,7 +20,7 @@ const mocks = {
state: {
firstName: 'Testy',
lastName: 'User',
email: 'testy.user@example.com',
gradidoID: 'current-user-id',
},
},
}
@ -64,8 +64,8 @@ describe('AuthNavbar', () => {
)
})
it('has the email address', () => {
// expect(wrapper.find('div.small:nth-child(2)').text()).toBe(wrapper.vm.$store.state.email)
// I think this should be username
it.skip('has the email address', () => {
expect(wrapper.find('div[data-test="navbar-item-email"]').text()).toBe(
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)
})
})
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 cols="7">
<div class="gdd-transaction-list-item-name">
<span v-if="linkedUser && linkedUser.email">
<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>
<span>{{ text }}</span>
</div>
</b-col>
</b-row>
@ -38,31 +24,9 @@ export default {
type: String,
required: true,
},
linkedUser: {
type: Object,
required: false,
},
text: {
type: String,
required: false,
},
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
required: true,
},
},
}

View File

@ -12,7 +12,7 @@
</template>
<script>
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 }

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: {
state: {
language: 'de',
email: 'peter@lustig.de',
newsletterState: true,
},
commit: storeCommitMock,

View File

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

View File

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

View File

@ -43,7 +43,6 @@ const mocks = {
$store: {
dispatch: storeDispatchMock,
state: {
email: 'user@example.org',
publisherId: 123,
firstName: 'User',
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', () => {
const windowLocationMock = jest.fn()
beforeEach(() => {

View File

@ -153,6 +153,7 @@
"password_new_repeat": "Neues Passwort wiederholen",
"password_old": "Altes Passwort",
"recipient": "Empfänger",
"recipientCommunity": "Gemeinschaft des Empfängers",
"reply": "Antworten",
"reset": "Zurücksetzen",
"save": "Speichern",
@ -166,12 +167,15 @@
"thx": "Danke",
"to": "bis",
"to1": "an",
"username": "Nutzername",
"username-placeholder": "Gebe einen eindeutigen Nutzernamen ein",
"validation": {
"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",
"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.",
"usernmae-unique": "Der Username ist bereits vergeben."
"username-allowed-chars": "Der Nutzername darf nur aus Buchstaben (ohne Umlaute), Zahlen, Binde- oder Unterstrichen bestehen.",
"username-hyphens": "Binde- oder Unterstriche müssen zwischen Buchstaben oder Zahlen stehen.",
"username-unique": "Der Nutzername ist bereits vergeben."
},
"your_amount": "Dein Betrag"
},
@ -319,7 +323,12 @@
"subtitle": "Wenn du dein Passwort vergessen hast, kannst du es hier zurücksetzen."
},
"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",
"signup": "Registrieren",

View File

@ -153,6 +153,7 @@
"password_new_repeat": "Repeat new password",
"password_old": "Old password",
"recipient": "Recipient",
"recipientCommunity": "Community of the recipient",
"reply": "Reply",
"reset": "Reset",
"save": "Save",
@ -166,12 +167,15 @@
"thx": "Thank you",
"to": "to",
"to1": "to",
"username": "Username",
"username-placeholder": "Enter a unique username",
"validation": {
"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",
"is-not": "You cannot send Gradidos to yourself",
"usernmae-regex": "The username must start with a letter, followed by at least two alphanumeric characters.",
"usernmae-unique": "This username is already taken."
"username-allowed-chars": "The username may only contain letters, numbers, hyphens or underscores.",
"username-hyphens": "Hyphens or underscores must be in between letters or numbers.",
"username-unique": "This username is already taken."
},
"your_amount": "Your amount"
},
@ -319,7 +323,12 @@
"subtitle": "If you have forgotten your password, you can reset it here."
},
"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",
"signup": "Sign up",

View File

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

View File

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

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