Merge remote-tracking branch 'origin/master' into 1574-Concept_to_introduce_Gradido-ID

This commit is contained in:
Claus-Peter Hübner 2022-03-24 03:17:48 +01:00
commit 59333d905e
322 changed files with 8959 additions and 3916 deletions

View File

@ -185,7 +185,7 @@ jobs:
##########################################################################
# LOCALES FRONTEND #######################################################
##########################################################################
- name: frontend | Locales
- name: Frontend | Locales
run: docker run --rm gradido/frontend:test yarn run locales
##############################################################################
@ -214,9 +214,38 @@ jobs:
##########################################################################
# LINT FRONTEND ##########################################################
##########################################################################
- name: frontend | Lint
- name: Frontend | Lint
run: docker run --rm gradido/frontend:test yarn run lint
##############################################################################
# JOB: STYLELINT FRONTEND ####################################################
##############################################################################
stylelint_frontend:
name: Stylelint - Frontend
runs-on: ubuntu-latest
needs: [build_test_frontend]
steps:
##########################################################################
# CHECKOUT CODE ##########################################################
##########################################################################
- name: Checkout code
uses: actions/checkout@v2
##########################################################################
# DOWNLOAD DOCKER IMAGE ##################################################
##########################################################################
- name: Download Docker Image (Frontend)
uses: actions/download-artifact@v2
with:
name: docker-frontend-test
path: /tmp
- name: Load Docker Image
run: docker load < /tmp/frontend.tar
##########################################################################
# STYLELINT FRONTEND #####################################################
##########################################################################
- name: Frontend | Stylelint
run: docker run --rm gradido/frontend:test yarn run stylelint
##############################################################################
# JOB: LINT ADMIN INTERFACE ##################################################
##############################################################################
@ -247,7 +276,36 @@ jobs:
run: docker run --rm gradido/admin:test yarn run lint
##############################################################################
# JOB: LOCALES ADMIN ######################################################
# JOB: STYLELINT ADMIN INTERFACE ##############################################
##############################################################################
stylelint_admin:
name: Stylelint - Admin Interface
runs-on: ubuntu-latest
needs: [build_test_admin]
steps:
##########################################################################
# CHECKOUT CODE ##########################################################
##########################################################################
- name: Checkout code
uses: actions/checkout@v2
##########################################################################
# DOWNLOAD DOCKER IMAGE ##################################################
##########################################################################
- name: Download Docker Image (Admin Interface)
uses: actions/download-artifact@v2
with:
name: docker-admin-test
path: /tmp
- name: Load Docker Image
run: docker load < /tmp/admin.tar
##########################################################################
# STYLELINT ADMIN INTERFACE ##############################################
##########################################################################
- name: Admin Interface | Stylelint
run: docker run --rm gradido/admin:test yarn run stylelint
##############################################################################
# JOB: LOCALES ADMIN #########################################################
##############################################################################
locales_admin:
name: Locales - Admin
@ -380,7 +438,7 @@ jobs:
report_name: Coverage Frontend
type: lcov
result_path: ./coverage/lcov.info
min_coverage: 94
min_coverage: 95
token: ${{ github.token }}
##############################################################################
@ -451,16 +509,13 @@ jobs:
##########################################################################
# UNIT TESTS BACKEND #####################################################
##########################################################################
- name: backend | docker-compose
- name: backend | docker-compose mariadb
run: docker-compose -f docker-compose.yml -f docker-compose.test.yml up --detach --no-deps mariadb
- name: Sleep for 30 seconds
run: sleep 30s
shell: bash
- name: backend | docker-compose database
run: docker-compose -f docker-compose.yml -f docker-compose.test.yml up --detach --no-deps database
- name: Sleep for 30 seconds
run: sleep 30s
shell: bash
- name: backend Unit tests | test
run: cd database && yarn && yarn build && cd ../backend && yarn && yarn test
# run: docker-compose -f docker-compose.yml -f docker-compose.test.yml exec -T backend yarn test
@ -473,7 +528,7 @@ jobs:
report_name: Coverage Backend
type: lcov
result_path: ./backend/coverage/lcov.info
min_coverage: 48
min_coverage: 54
token: ${{ github.token }}
##########################################################################

View File

@ -1,3 +1,4 @@
node_modules
.git
.gitignore
.gitignore
!.eslintignore

View File

@ -1,3 +1,5 @@
CONFIG_VERSION=v1.2022-03-18
GRAPHQL_URI=http://localhost:4000/graphql
WALLET_AUTH_URL=http://localhost/authenticate?token={token}
WALLET_URL=http://localhost/login

View File

@ -1,3 +1,5 @@
CONFIG_VERSION=$ADMIN_CONFIG_VERSION
GRAPHQL_URI=$GRAPHQL_URI
WALLET_AUTH_URL=$WALLET_AUTH_URL
WALLET_URL=$WALLET_URL

View File

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

View File

@ -8,14 +8,42 @@ module.exports = {
parserOptions: {
parser: 'babel-eslint',
},
extends: ['standard', 'plugin:vue/essential', 'plugin:prettier/recommended'],
extends: [
'standard',
'plugin:vue/essential',
'plugin:prettier/recommended',
'plugin:@intlify/vue-i18n/recommended',
],
// required to lint *.vue files
plugins: ['vue', 'prettier', 'jest'],
overrides: [
{
files: ['*.json'],
extends: ['plugin:@intlify/vue-i18n/recommended'],
},
],
// add your custom rules here
rules: {
'no-console': ['error'],
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',
'vue/component-name-in-template-casing': ['error', 'kebab-case'],
'vue/no-static-inline-styles': [
'error',
{
allowBinding: false,
},
],
'@intlify/vue-i18n/no-dynamic-keys': 'error',
'@intlify/vue-i18n/no-unused-keys': [
'error',
{
src: './src',
extensions: ['.js', '.vue'],
ignores: [],
enableFix: false,
},
],
'@intlify/vue-i18n/no-missing-keys-in-other-locales': 'error',
'prettier/prettier': [
'error',
{
@ -23,4 +51,12 @@ module.exports = {
},
],
},
settings: {
'vue-i18n': {
localeDir: './src/locales/*.json',
// Specify the version of `vue-i18n` you are using.
// If not specified, the message will be parsed twice.
messageSyntaxVersion: '^8.26.5',
},
},
}

18
admin/.stylelintrc.js Normal file
View File

@ -0,0 +1,18 @@
'use strict';
module.exports = {
extends: ["stylelint-config-standard-scss", "stylelint-config-recommended-vue"],
overrides: [
{
files: "**/*.{scss}",
customSyntax: "postcss-scss",
extends: ["stylelint-config-standard-scss"],
},
{
files: "**/*.vue",
customSyntax: "postcss-html",
extends: ["stylelint-config-recommended-vue"],
}
]
};

View File

@ -9,11 +9,13 @@
"scripts": {
"start": "node run/server.js",
"serve": "vue-cli-service serve --open",
"dev": "yarn run serve",
"build": "vue-cli-service build",
"lint": "eslint --max-warnings=0 --ext .js,.vue .",
"dev": "yarn run serve",
"analyse-bundle": "yarn build && webpack-bundle-analyzer dist/webpack.stats.json",
"lint": "eslint --max-warnings=0 --ext .js,.vue,.json .",
"stylelint": "stylelint --max-warnings=0 '**/*.{scss,vue}'",
"test": "TZ=UTC jest --coverage",
"locales": "scripts/missing-keys.sh && scripts/sort.sh"
"locales": "scripts/sort.sh"
},
"dependencies": {
"@babel/core": "^7.15.8",
@ -49,6 +51,7 @@
},
"devDependencies": {
"@babel/eslint-parser": "^7.15.8",
"@intlify/eslint-plugin-vue-i18n": "^1.4.0",
"@vue/cli-plugin-babel": "~4.5.0",
"@vue/cli-plugin-eslint": "~4.5.0",
"@vue/cli-service": "~4.5.0",
@ -65,6 +68,13 @@
"eslint-plugin-promise": "^5.1.1",
"eslint-plugin-vue": "^7.20.0",
"jest-environment-jsdom-sixteen": "^2.0.0",
"postcss": "^8.4.8",
"postcss-html": "^1.3.0",
"postcss-scss": "^4.0.3",
"stylelint": "^14.5.3",
"stylelint-config-recommended-vue": "^1.3.0",
"stylelint-config-standard-scss": "^3.0.0",
"vue-cli-plugin-i18n": "^2.3.1",
"vue-template-compiler": "^2.6.11"
},
"browserslist": [

View File

@ -4,24 +4,24 @@
<b-row align-v="center" class="mt-4 justify-content-lg-between">
<b-col>
<div class="copyright text-center text-lg-center text-muted">
© {{ year }}
{{ $t('footer.copyright.year', { year }) }}
<a
:href="`https://gradido.net/${$i18n.locale}`"
class="font-weight-bold ml-1"
target="_blank"
>
{{ $t('gradido_admin_footer') }}
{{ $t('footer.copyright.link') }}
</a>
|
{{ $t('math.pipe') }}
<a href="https://github.com/gradido/gradido/releases/latest" target="_blank">
App version {{ version }}
{{ $t('footer.app_version', { version }) }}
</a>
<a
v-if="hash"
:href="'https://github.com/gradido/gradido/commit/' + hash"
target="_blank"
>
({{ shortHash }})
{{ $t('footer.short_hash', { shortHash }) }}
</a>
</div>
</b-col>

View File

@ -6,32 +6,30 @@ const localVue = global.localVue
const apolloQueryMock = jest.fn().mockResolvedValue({
data: {
transactionList: {
transactions: [
{
id: 1,
amount: 100,
balanceDate: 0,
creationDate: new Date(),
memo: 'Testing',
linkedUser: {
firstName: 'Gradido',
lastName: 'Akademie',
},
creationTransactionList: [
{
id: 1,
amount: 100,
balanceDate: 0,
creationDate: new Date(),
memo: 'Testing',
linkedUser: {
firstName: 'Gradido',
lastName: 'Akademie',
},
{
id: 2,
amount: 200,
balanceDate: 0,
creationDate: new Date(),
memo: 'Testing 2',
linkedUser: {
firstName: 'Gradido',
lastName: 'Akademie',
},
},
{
id: 2,
amount: 200,
balanceDate: 0,
creationDate: new Date(),
memo: 'Testing 2',
linkedUser: {
firstName: 'Gradido',
lastName: 'Akademie',
},
],
},
},
],
},
})
@ -67,7 +65,6 @@ describe('CreationTransactionListFormular', () => {
currentPage: 1,
pageSize: 25,
order: 'DESC',
onlyCreations: true,
userId: 1,
},
}),

View File

@ -5,7 +5,7 @@
</div>
</template>
<script>
import { transactionList } from '../graphql/transactionList'
import { creationTransactionList } from '../graphql/creationTransactionList'
export default {
name: 'CreationTransactionList',
props: {
@ -51,17 +51,16 @@ export default {
getTransactions() {
this.$apollo
.query({
query: transactionList,
query: creationTransactionList,
variables: {
currentPage: 1,
pageSize: 25,
order: 'DESC',
onlyCreations: true,
userId: parseInt(this.userId),
},
})
.then((result) => {
this.items = result.data.transactionList.transactions
this.items = result.data.creationTransactionList
})
.catch((error) => {
this.toastError(error.message)

View File

@ -7,7 +7,7 @@
<b-row class="mt-4">
<b-col class="col-3">{{ $t('transactionlist.amount') }}</b-col>
<b-col class="h3">
<b>{{ item.amount }} GDD</b>
<b>{{ item.amount }} {{ $t('GDD') }}</b>
</b-col>
</b-row>
<b-row>

View File

@ -12,7 +12,7 @@
>
<b-icon icon="plus" variant="success"></b-icon>
</b-button>
<div v-else>{{ $t('e_mail') }}!</div>
<div v-else>{{ $t('e_mail') }}{{ $t('math.exclaim') }}</div>
</div>
</template>
</b-table-lite>

View File

@ -4,11 +4,20 @@
// Load Package Details for some default values
const pkg = require('../../package')
const constants = {
CONFIG_VERSION: {
DEFAULT: 'DEFAULT',
EXPECTED: 'v1.2022-03-18',
CURRENT: '',
},
}
const version = {
APP_VERSION: pkg.version,
BUILD_COMMIT: process.env.BUILD_COMMIT || null,
// self reference of `version.BUILD_COMMIT` is not possible at this point, hence the duplicate code
BUILD_COMMIT_SHORT: (process.env.BUILD_COMMIT || '0000000').substr(0, 7),
BUILD_COMMIT_SHORT: (process.env.BUILD_COMMIT || '0000000').slice(0, 7),
PORT: process.env.PORT || 8080,
}
const environment = {
@ -27,14 +36,24 @@ const debug = {
DEBUG_DISABLE_AUTH: process.env.DEBUG_DISABLE_AUTH === 'true' || false,
}
const options = {}
// Check config version
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,
)
) {
throw new Error(
`Fatal: Config Version incorrect - expected "${constants.CONFIG_VERSION.EXPECTED}" or "${constants.CONFIG_VERSION.DEFAULT}", but found "${constants.CONFIG_VERSION.CURRENT}"`,
)
}
const CONFIG = {
...constants,
...version,
...environment,
...endpoints,
...options,
...debug,
}
export default CONFIG
module.exports = CONFIG

View File

@ -1,7 +1,7 @@
import gql from 'graphql-tag'
export const confirmPendingCreation = gql`
mutation ($id: Float!) {
mutation ($id: Int!) {
confirmPendingCreation(id: $id)
}
`

View File

@ -0,0 +1,22 @@
import gql from 'graphql-tag'
export const creationTransactionList = gql`
query ($currentPage: Int = 1, $pageSize: Int = 25, $order: Order = DESC, $userId: Int!) {
creationTransactionList(
currentPage: $currentPage
pageSize: $pageSize
order: $order
userId: $userId
) {
id
amount
balanceDate
creationDate
memo
linkedUser {
firstName
lastName
}
}
}
`

View File

@ -1,7 +1,7 @@
import gql from 'graphql-tag'
export const deletePendingCreation = gql`
mutation ($id: Float!) {
mutation ($id: Int!) {
deletePendingCreation(id: $id)
}
`

View File

@ -1,7 +1,7 @@
import gql from 'graphql-tag'
export const deleteUser = gql`
mutation ($userId: Float!) {
mutation ($userId: Int!) {
deleteUser(userId: $userId)
}
`

View File

@ -1,31 +0,0 @@
import gql from 'graphql-tag'
export const transactionList = gql`
query (
$currentPage: Int = 1
$pageSize: Int = 25
$order: Order = DESC
$onlyCreations: Boolean = false
$userId: Int = null
) {
transactionList(
currentPage: $currentPage
pageSize: $pageSize
order: $order
onlyCreations: $onlyCreations
userId: $userId
) {
transactions {
id
amount
balanceDate
creationDate
memo
linkedUser {
firstName
lastName
}
}
}
}
`

View File

@ -1,7 +1,7 @@
import gql from 'graphql-tag'
export const unDeleteUser = gql`
mutation ($userId: Float!) {
mutation ($userId: Int!) {
unDeleteUser(userId: $userId)
}
`

View File

@ -1,8 +1,6 @@
{
"all_emails": "Alle Nutzer",
"back": "zurück",
"bookmark": "bookmark",
"confirmed": "bestätigt",
"creation": "Schöpfung",
"creation_form": {
"creation_failed": "Ausstehende Schöpfung für {email} konnte nicht erzeugt werden.",
@ -16,7 +14,6 @@
"submit_creation": "Schöpfung einreichen",
"toasted": "Offene Schöpfung ({value} GDD) für {email} wurde gespeichert und liegt zur Bestätigung bereit",
"toasted_created": "Schöpfung wurde erfolgreich gespeichert",
"toasted_default": "`Fall {event} wird nicht unterstützt`",
"toasted_delete": "Offene Schöpfung wurde gelöscht",
"toasted_update": "`Offene Schöpfung {value} GDD) für {email} wurde geändert und liegt zur Bestätigung bereit",
"update_creation": "Schöpfung aktualisieren"
@ -27,14 +24,26 @@
"deleted": "gelöscht",
"deleted_user": "Alle gelöschten Nutzer",
"delete_user": "Nutzer löschen",
"details": "Details",
"edit": "Bearbeiten",
"enabled": "aktiviert",
"error": "Fehler",
"e_mail": "E-Mail",
"firstname": "Vorname",
"gradido_admin_footer": "Gradido Akademie Adminkonsole",
"footer": {
"app_version": "App version {version}",
"copyright": {
"link": "Gradido Akademie Adminkonsole",
"year": "© {year}"
},
"short_hash": "({shortHash})"
},
"GDD": "GDD",
"hide_details": "Details verbergen",
"lastname": "Nachname",
"math": {
"exclaim": "!",
"pipe": "|"
},
"moderator": "Moderator",
"multiple_creation_text": "Bitte wähle ein oder mehrere Mitglieder aus für die du Schöpfen möchtest.",
"name": "Name",
@ -47,7 +56,6 @@
"user_search": "Nutzersuche"
},
"not_open_creations": "Keine offenen Schöpfungen",
"open_creation": "Offene Schöpfung",
"open_creations": "Offene Schöpfungen",
"overlay": {
"confirm": {
@ -56,13 +64,6 @@
"text": "Nach dem Speichern ist der Datensatz nicht mehr änderbar und kann auch nicht mehr gelöscht werden. Bitte überprüfe genau, dass alles stimmt.",
"title": "Schöpfung bestätigen!",
"yes": "Ja, Schöpfung bestätigen und speichern!"
},
"remove": {
"no": "Nein, nicht löschen.",
"question": "Willst du die vorgespeicherte Schöpfung wirklich löschen?",
"text": "Nach dem Löschen gibt es keine Möglichkeit mehr diesen Datensatz wiederherzustellen. Es wird aber der gesamte Vorgang in der Logdatei als Übersicht gespeichert.",
"title": "Achtung! Schöpfung löschen!",
"yes": "Ja, Schöpfung löschen!"
}
},
"remove": "Entfernen",
@ -72,13 +73,11 @@
"status": "Status",
"success": "Erfolg",
"text": "Text",
"transaction": "Transaktion",
"transactionlist": {
"amount": "Betrag",
"balanceDate": "Schöpfungsdatum",
"community": "Gemeinschaft",
"date": "Datum",
"decay": "Vergänglichkeit",
"memo": "Nachricht",
"title": "Alle geschöpften Transaktionen für den Nutzer"
},

View File

@ -1,8 +1,6 @@
{
"all_emails": "All users",
"back": "back",
"bookmark": "Remember",
"confirmed": "confirmed",
"creation": "Creation",
"creation_form": {
"creation_failed": "Could not create pending creation for {email}",
@ -16,7 +14,6 @@
"submit_creation": "Submit creation",
"toasted": "Open creation ({value} GDD) for {email} has been saved and is ready for confirmation.",
"toasted_created": "Creation has been successfully saved",
"toasted_default": "`Case {event} is not supported`",
"toasted_delete": "Open creation has been deleted",
"toasted_update": "Open creation {value} GDD) for {email} has been changed and is ready for confirmation.",
"update_creation": "Creation update"
@ -27,14 +24,26 @@
"deleted": "deleted",
"deleted_user": "All deleted user",
"delete_user": "Delete user",
"details": "Details",
"edit": "Edit",
"enabled": "enabled",
"error": "Error",
"e_mail": "E-mail",
"firstname": "Firstname",
"gradido_admin_footer": "Gradido Academy Admin Console",
"footer": {
"app_version": "App version {version}",
"copyright": {
"link": "Gradido Academy Admin Console",
"year": "© {year}"
},
"short_hash": "({shortHash})"
},
"GDD": "GDD",
"hide_details": "Hide details",
"lastname": "Lastname",
"math": {
"exclaim": "!",
"pipe": "|"
},
"moderator": "Moderator",
"multiple_creation_text": "Please select one or more members for which you would like to perform creations.",
"name": "Name",
@ -47,7 +56,6 @@
"user_search": "User search"
},
"not_open_creations": "No open creations",
"open_creation": "Open creation",
"open_creations": "Open creations",
"overlay": {
"confirm": {
@ -56,13 +64,6 @@
"text": "After saving, the record can no longer be changed or deleted. Please check carefully that everything is correct.",
"title": "Confirm creation!",
"yes": "Yes, confirm and save creation!"
},
"remove": {
"no": "No, do not delete.",
"question": "Do you really want to delete the pre-stored creation?",
"text": "After deletion, there is no possibility to restore this data record. However, the entire process is saved in the log file as an overview.",
"title": "Attention! Delete creation!",
"yes": "Yes, delete creation!"
}
},
"remove": "Remove",
@ -72,13 +73,11 @@
"status": "Status",
"success": "Success",
"text": "Text",
"transaction": "Transaction",
"transactionlist": {
"amount": "Amount",
"balanceDate": "Creation date",
"community": "Community",
"date": "Date",
"decay": "Decay",
"memo": "Message",
"title": "All creation-transactions for the user"
},

View File

@ -15,7 +15,7 @@ export const toasters = {
toast(message, options) {
// for unit tests, check that replace is present
if (message.replace) message = message.replace(/^GraphQL error: /, '')
this.$bvToast.toast(message, {
this.$root.$bvToast.toast(message, {
autoHideDelay: 5000,
appendToast: true,
solid: true,

View File

@ -2,7 +2,7 @@
<div class="creation">
<b-row>
<b-col cols="12" lg="6">
<label>Usersuche</label>
<label>{{ $t('user_search') }}</label>
<b-input-group>
<b-form-input
type="text"

View File

@ -1,6 +1,6 @@
<template>
<div class="user-search">
<div style="text-align: right">
<div class="user-search-first-div">
<b-button class="unconfirmedRegisterMails" variant="light" @click="unconfirmedRegisterMails">
<b-icon icon="envelope" variant="danger"></b-icon>
{{ filterCheckedEmails ? $t('all_emails') : $t('unregistered_emails') }}
@ -99,7 +99,7 @@ export default {
},
updateDeletedAt(userId, deletedAt) {
this.searchResult.find((obj) => obj.userId === userId).deletedAt = deletedAt
this.toastSuccess(this.$t('user_deleted'))
this.toastSuccess(deletedAt ? this.$t('user_deleted') : this.$t('user_recovered'))
},
},
watch: {
@ -136,3 +136,8 @@ export default {
},
}
</script>
<style>
.user-search-first-div {
text-align: right;
}
</style>

View File

@ -2,7 +2,6 @@ import { ApolloClient, ApolloLink, InMemoryCache, HttpLink } from 'apollo-boost'
import VueApollo from 'vue-apollo'
import CONFIG from '../config'
import store from '../store/store'
import i18n from '../i18n'
const httpLink = new HttpLink({ uri: CONFIG.GRAPHQL_URI })
@ -15,7 +14,6 @@ const authLink = new ApolloLink((operation, forward) => {
})
return forward(operation).map((response) => {
if (response.errors && response.errors[0].message === '403.13 - Client certificate revoked') {
response.errors[0].message = i18n.t('error.session-expired')
store.dispatch('logout', null)
window.location.assign(CONFIG.WALLET_URL)
return response

View File

@ -2,11 +2,12 @@ const path = require('path')
const webpack = require('webpack')
const Dotenv = require('dotenv-webpack')
const StatsPlugin = require('stats-webpack-plugin')
const CONFIG = require('./src/config')
// vue.config.js
module.exports = {
devServer: {
port: process.env.PORT || 8080,
port: CONFIG.PORT,
},
pluginOptions: {
i18n: {
@ -14,6 +15,7 @@ module.exports = {
fallbackLocale: 'de',
localeDir: 'locales',
enableInSFC: false,
enableLegacy: false,
},
},
lintOnSave: true,
@ -33,7 +35,7 @@ module.exports = {
// 'process.env.DOCKER_WORKDIR': JSON.stringify(process.env.DOCKER_WORKDIR),
// 'process.env.BUILD_DATE': JSON.stringify(process.env.BUILD_DATE),
// 'process.env.BUILD_VERSION': JSON.stringify(process.env.BUILD_VERSION),
'process.env.BUILD_COMMIT': JSON.stringify(process.env.BUILD_COMMIT),
'process.env.BUILD_COMMIT': JSON.stringify(CONFIG.BUILD_COMMIT),
// 'process.env.PORT': JSON.stringify(process.env.PORT),
}),
// generate webpack stats to allow analysis of the bundlesize
@ -45,7 +47,7 @@ module.exports = {
},
css: {
// Enable CSS source maps.
sourceMap: process.env.NODE_ENV !== 'production',
sourceMap: CONFIG.NODE_ENV !== 'production',
},
outputDir: path.resolve(__dirname, './dist'),
}

File diff suppressed because it is too large Load Diff

View File

@ -1,3 +1,5 @@
CONFIG_VERSION=v1.2022-03-18
# Server
PORT=4000
JWT_SECRET=secret123
@ -38,9 +40,9 @@ EMAIL_SENDER=info@gradido.net
EMAIL_PASSWORD=xxx
EMAIL_SMTP_URL=gmail.com
EMAIL_SMTP_PORT=587
EMAIL_LINK_VERIFICATION=http://localhost/checkEmail/{code}
EMAIL_LINK_SETPASSWORD=http://localhost/reset/{code}
RESEND_TIME=10
EMAIL_LINK_VERIFICATION=http://localhost/checkEmail/{optin}{code}
EMAIL_LINK_SETPASSWORD=http://localhost/reset/{optin}
EMAIL_CODE_VALID_TIME=10
# Webhook
WEBHOOK_ELOPAGE_SECRET=secret

View File

@ -1,3 +1,5 @@
CONFIG_VERSION=$BACKEND_CONFIG_VERSION
# Server
JWT_SECRET=$JWT_SECRET
JWT_EXPIRES_IN=10m

View File

@ -3,9 +3,16 @@ module.exports = {
verbose: true,
preset: 'ts-jest',
collectCoverage: true,
collectCoverageFrom: ['src/**/*.ts', '!**/node_modules/**'],
collectCoverageFrom: ['src/**/*.ts', '!**/node_modules/**', '!src/seeds/**', '!build/**'],
setupFiles: ['<rootDir>/test/testSetup.ts'],
modulePathIgnorePatterns: ['<rootDir>/build/'],
moduleNameMapper: {
'@/(.*)': '<rootDir>/src/$1',
'@model/(.*)': '<rootDir>/src/graphql/model/$1',
'@arg/(.*)': '<rootDir>/src/graphql/arg/$1',
'@enum/(.*)': '<rootDir>/src/graphql/enum/$1',
'@repository/(.*)': '<rootDir>/src/typeorm/repository/$1',
'@test/(.*)': '<rootDir>/test/$1',
'@entity/(.*)':
process.env.NODE_ENV === 'development'
? '<rootDir>/../database/entity/$1'

View File

@ -10,10 +10,11 @@
"scripts": {
"build": "tsc --build",
"clean": "tsc --build --clean",
"start": "node build/src/index.js",
"dev": "nodemon -w src --ext ts --exec ts-node src/index.ts",
"start": "TZ=UTC TS_NODE_BASEURL=./build node -r tsconfig-paths/register build/src/index.js",
"dev": "TZ=UTC nodemon -w src --ext ts --exec ts-node -r tsconfig-paths/register src/index.ts",
"lint": "eslint --max-warnings=0 --ext .js,.ts .",
"test": "TZ=UTC NODE_ENV=development jest --runInBand --coverage --forceExit --detectOpenHandles"
"test": "TZ=UTC NODE_ENV=development jest --runInBand --coverage --forceExit --detectOpenHandles",
"seed": "TZ=UTC ts-node -r tsconfig-paths/register src/seeds/index.ts"
},
"dependencies": {
"@types/jest": "^27.0.2",
@ -31,7 +32,6 @@
"jest": "^27.2.4",
"jsonwebtoken": "^8.5.1",
"lodash.clonedeep": "^4.5.0",
"module-alias": "^2.2.2",
"mysql2": "^2.3.0",
"nodemailer": "^6.6.5",
"random-bigint": "^0.0.1",
@ -42,6 +42,7 @@
},
"devDependencies": {
"@types/express": "^4.17.12",
"@types/faker": "^5.5.9",
"@types/jsonwebtoken": "^8.5.2",
"@types/node": "^16.10.3",
"@types/nodemailer": "^6.4.4",
@ -54,13 +55,11 @@
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-prettier": "^3.4.0",
"eslint-plugin-promise": "^5.1.0",
"faker": "^5.5.3",
"nodemon": "^2.0.7",
"prettier": "^2.3.1",
"ts-node": "^10.0.0",
"tsconfig-paths": "^3.14.0",
"typescript": "^4.3.4"
},
"_moduleAliases": {
"@entity": "../database/build/entity",
"@dbTools": "../database/build/src"
}
}

View File

@ -1,7 +1,7 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import { KlicktippConnector } from './klicktippConnector'
import CONFIG from '../config'
import CONFIG from '@/config'
const klicktippConnector = new KlicktippConnector()

View File

@ -7,4 +7,6 @@ export const INALIENABLE_RIGHTS = [
RIGHTS.CREATE_USER,
RIGHTS.SEND_RESET_PASSWORD_EMAIL,
RIGHTS.SET_PASSWORD,
RIGHTS.QUERY_TRANSACTION_LINK,
RIGHTS.QUERY_OPT_IN,
]

View File

@ -1,5 +1,5 @@
import jwt from 'jsonwebtoken'
import CONFIG from '../config/'
import CONFIG from '@/config/'
import { CustomJwtPayload } from './CustomJwtPayload'
export const decode = (token: string): CustomJwtPayload | null => {

View File

@ -16,8 +16,14 @@ export enum RIGHTS {
CREATE_USER = 'CREATE_USER',
SEND_RESET_PASSWORD_EMAIL = 'SEND_RESET_PASSWORD_EMAIL',
SET_PASSWORD = 'SET_PASSWORD',
QUERY_OPT_IN = 'QUERY_OPT_IN',
UPDATE_USER_INFOS = 'UPDATE_USER_INFOS',
HAS_ELOPAGE = 'HAS_ELOPAGE',
CREATE_TRANSACTION_LINK = 'CREATE_TRANSACTION_LINK',
DELETE_TRANSACTION_LINK = 'DELETE_TRANSACTION_LINK',
QUERY_TRANSACTION_LINK = 'QUERY_TRANSACTION_LINK',
REDEEM_TRANSACTION_LINK = 'REDEEM_TRANSACTION_LINK',
LIST_TRANSACTION_LINKS = 'LIST_TRANSACTION_LINKS',
// Admin
SEARCH_USERS = 'SEARCH_USERS',
CREATE_PENDING_CREATION = 'CREATE_PENDING_CREATION',
@ -28,4 +34,5 @@ export enum RIGHTS {
SEND_ACTIVATION_EMAIL = 'SEND_ACTIVATION_EMAIL',
DELETE_USER = 'DELETE_USER',
UNDELETE_USER = 'UNDELETE_USER',
CREATION_TRANSACTION_LIST = 'CREATION_TRANSACTION_LIST',
}

View File

@ -18,6 +18,10 @@ export const ROLE_USER = new Role('user', [
RIGHTS.LOGOUT,
RIGHTS.UPDATE_USER_INFOS,
RIGHTS.HAS_ELOPAGE,
RIGHTS.CREATE_TRANSACTION_LINK,
RIGHTS.DELETE_TRANSACTION_LINK,
RIGHTS.REDEEM_TRANSACTION_LINK,
RIGHTS.LIST_TRANSACTION_LINKS,
])
export const ROLE_ADMIN = new Role('admin', Object.values(RIGHTS)) // all rights

View File

@ -10,8 +10,13 @@ Decimal.set({
})
const constants = {
DB_VERSION: '0029-clean_transaction_table',
DB_VERSION: '0033-add_referrer_id',
DECAY_START_TIME: new Date('2021-05-13 17:46:31'), // GMT+0
CONFIG_VERSION: {
DEFAULT: 'DEFAULT',
EXPECTED: 'v1.2022-03-18',
CURRENT: '',
},
}
const server = {
@ -54,8 +59,6 @@ const loginServer = {
LOGIN_SERVER_KEY: process.env.LOGIN_SERVER_KEY || 'a51ef8ac7ef1abf162fb7a65261acd7a',
}
// TODO: Hannes if I find you... this looks like blasphemy
const resendTime = parseInt(process.env.RESEND_TIME ? process.env.RESEND_TIME : 'null')
const email = {
EMAIL: process.env.EMAIL === 'true' || false,
EMAIL_USERNAME: process.env.EMAIL_USERNAME || 'gradido_email',
@ -64,9 +67,12 @@ const email = {
EMAIL_SMTP_URL: process.env.EMAIL_SMTP_URL || 'gmail.com',
EMAIL_SMTP_PORT: process.env.EMAIL_SMTP_PORT || '587',
EMAIL_LINK_VERIFICATION:
process.env.EMAIL_LINK_VERIFICATION || 'http://localhost/checkEmail/{code}',
EMAIL_LINK_SETPASSWORD: process.env.EMAIL_LINK_SETPASSWORD || 'http://localhost/reset/{code}',
RESEND_TIME: isNaN(resendTime) ? 10 : resendTime,
process.env.EMAIL_LINK_VERIFICATION || 'http://localhost/checkEmail/{optin}{code}',
EMAIL_LINK_SETPASSWORD:
process.env.EMAIL_LINK_SETPASSWORD || 'http://localhost/reset-password/{optin}',
EMAIL_CODE_VALID_TIME: process.env.EMAIL_CODE_VALID_TIME
? parseInt(process.env.EMAIL_CODE_VALID_TIME) || 10
: 10,
}
const webhook = {
@ -77,6 +83,18 @@ const webhook = {
// 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
if (
![constants.CONFIG_VERSION.EXPECTED, constants.CONFIG_VERSION.DEFAULT].includes(
constants.CONFIG_VERSION.CURRENT,
)
) {
throw new Error(
`Fatal: Config Version incorrect - expected "${constants.CONFIG_VERSION.EXPECTED}" or "${constants.CONFIG_VERSION.DEFAULT}", but found "${constants.CONFIG_VERSION.CURRENT}"`,
)
}
const CONFIG = {
...constants,
...server,

View File

@ -16,4 +16,7 @@ export default class CreateUserArgs {
@Field(() => Int, { nullable: true })
publisherId: number
@Field(() => String, { nullable: true })
redeemCode?: string | null
}

View File

@ -1,5 +1,5 @@
import { ArgsType, Field, Int } from 'type-graphql'
import { Order } from '../enum/Order'
import { Order } from '@enum/Order'
@ArgsType()
export default class Paginated {
@ -11,10 +11,4 @@ export default class Paginated {
@Field(() => Order, { nullable: true })
order?: Order
@Field(() => Boolean, { nullable: true })
onlyCreations?: boolean
@Field(() => Int, { nullable: true })
userId?: number
}

View File

@ -0,0 +1,11 @@
import { ArgsType, Field } from 'type-graphql'
import Decimal from 'decimal.js-light'
@ArgsType()
export default class TransactionLinkArgs {
@Field(() => Decimal)
amount: Decimal
@Field(() => String)
memo: string
}

View File

@ -2,46 +2,45 @@
import { AuthChecker } from 'type-graphql'
import { decode, encode } from '../../auth/JWT'
import { ROLE_UNAUTHORIZED, ROLE_USER, ROLE_ADMIN } from '../../auth/ROLES'
import { RIGHTS } from '../../auth/RIGHTS'
import { decode, encode } from '@/auth/JWT'
import { ROLE_UNAUTHORIZED, ROLE_USER, ROLE_ADMIN } from '@/auth/ROLES'
import { RIGHTS } from '@/auth/RIGHTS'
import { getCustomRepository } from '@dbTools/typeorm'
import { UserRepository } from '../../typeorm/repository/User'
import { INALIENABLE_RIGHTS } from '../../auth/INALIENABLE_RIGHTS'
import { UserRepository } from '@repository/User'
import { INALIENABLE_RIGHTS } from '@/auth/INALIENABLE_RIGHTS'
import { ServerUser } from '@entity/ServerUser'
const isAuthorized: AuthChecker<any> = 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))
return true
// Do we have a token?
if (context.token) {
// Decode the token
const decoded = decode(context.token)
if (!decoded) {
// Are all rights requested public?
const isInalienable = (<RIGHTS[]>rights).reduce(
(acc, right) => acc && INALIENABLE_RIGHTS.includes(right),
true,
)
if (isInalienable) {
// If public dont throw and permit access
return true
} else {
// Throw on a protected route
throw new Error('403.13 - Client certificate revoked')
}
}
// Set context pubKey
context.pubKey = Buffer.from(decoded.pubKey).toString('hex')
// set new header token
// TODO - load from database dynamically & admin - maybe encode this in the token to prevent many database requests
// TODO this implementation is bullshit - two database queries cause our user identifiers are not aligned and vary between email, id and pubKey
const userRepository = await getCustomRepository(UserRepository)
if (!context.token) {
throw new Error('401 Unauthorized')
}
// Decode the token
const decoded = decode(context.token)
if (!decoded) {
throw new Error('403.13 - Client certificate revoked')
}
// Set context pubKey
context.pubKey = Buffer.from(decoded.pubKey).toString('hex')
// TODO - load from database dynamically & admin - maybe encode this in the token to prevent many database requests
// TODO this implementation is bullshit - two database queries cause our user identifiers are not aligned and vary between email, id and pubKey
const userRepository = await getCustomRepository(UserRepository)
try {
const user = await userRepository.findByPubkeyHex(context.pubKey)
context.user = user
const countServerUsers = await ServerUser.count({ email: user.email })
context.role = countServerUsers > 0 ? ROLE_ADMIN : ROLE_USER
context.setHeaders.push({ key: 'token', value: encode(decoded.pubKey) })
} catch {
// in case the database query fails (user deleted)
throw new Error('401 Unauthorized')
}
// check for correct rights
@ -50,6 +49,8 @@ const isAuthorized: AuthChecker<any> = async ({ context }, rights) => {
throw new Error('401 Unauthorized')
}
// set new header token
context.setHeaders.push({ key: 'token', value: encode(decoded.pubKey) })
return true
}

View File

@ -6,6 +6,7 @@ export enum TransactionTypeId {
RECEIVE = 3,
// This is a virtual property, never occurring on the database
DECAY = 4,
LINK_SUMMARY = 5,
}
registerEnumType(TransactionTypeId, {

View File

@ -1,7 +1,7 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import { ObjectType, Field } from 'type-graphql'
import { GdtEntryType } from '../enum/GdtEntryType'
import { GdtEntryType } from '@enum/GdtEntryType'
@ObjectType()
export class GdtEntry {

View File

@ -1,19 +0,0 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
/*
import { ObjectType, Field } from 'type-graphql'
@ObjectType()
export class GdtSumPerEmail {
constructor(email: string, summe: number) {
this.email = email
this.summe = summe
}
@Field(() => String)
email: string
@Field(() => Number)
summe: number
}
*/

View File

@ -2,7 +2,7 @@ import { ObjectType, Field } from 'type-graphql'
import { Decay } from './Decay'
import { Transaction as dbTransaction } from '@entity/Transaction'
import Decimal from 'decimal.js-light'
import { TransactionTypeId } from '../enum/TransactionTypeId'
import { TransactionTypeId } from '@enum/TransactionTypeId'
import { User } from './User'
@ObjectType()
@ -30,6 +30,7 @@ export class Transaction {
this.creationDate = transaction.creationDate
this.linkedUser = linkedUser
this.linkedTransactionId = transaction.linkedTransactionId
this.transactionLinkId = transaction.transactionLinkId
}
@Field(() => Number)
@ -67,4 +68,8 @@ export class Transaction {
@Field(() => Number, { nullable: true })
linkedTransactionId?: number | null
// Links to the TransactionLink when transaction was created by a link
@Field(() => Number, { nullable: true })
transactionLinkId?: number | null
}

View File

@ -0,0 +1,54 @@
import { ObjectType, Field } from 'type-graphql'
import Decimal from 'decimal.js-light'
import { TransactionLink as dbTransactionLink } from '@entity/TransactionLink'
import { User } from './User'
@ObjectType()
export class TransactionLink {
constructor(transactionLink: dbTransactionLink, user: User, redeemedBy: User | null = null) {
this.id = transactionLink.id
this.user = user
this.amount = transactionLink.amount
this.holdAvailableAmount = transactionLink.holdAvailableAmount
this.memo = transactionLink.memo
this.code = transactionLink.code
this.createdAt = transactionLink.createdAt
this.validUntil = transactionLink.validUntil
this.deletedAt = transactionLink.deletedAt
this.redeemedAt = transactionLink.redeemedAt
this.redeemedBy = redeemedBy
}
@Field(() => Number)
id: number
@Field(() => User)
user: User
@Field(() => Decimal)
amount: Decimal
@Field(() => Decimal)
holdAvailableAmount: Decimal
@Field(() => String)
memo: string
@Field(() => String)
code: string
@Field(() => Date)
createdAt: Date
@Field(() => Date, { nullable: true })
deletedAt: Date | null
@Field(() => Date)
validUntil: Date
@Field(() => Date, { nullable: true })
redeemedAt: Date | null
@Field(() => User, { nullable: true })
redeemedBy: User | null
}

View File

@ -1,5 +1,5 @@
import { ObjectType, Field } from 'type-graphql'
import CONFIG from '../../config'
import CONFIG from '@/config'
import Decimal from 'decimal.js-light'
import { Transaction } from './Transaction'
@ -9,12 +9,14 @@ export class TransactionList {
balance: Decimal,
transactions: Transaction[],
count: number,
linkCount: number,
balanceGDT?: number | null,
decayStartBlock: Date = CONFIG.DECAY_START_TIME,
) {
this.balance = balance
this.transactions = transactions
this.count = count
this.linkCount = linkCount
this.balanceGDT = balanceGDT || null
this.decayStartBlock = decayStartBlock
}
@ -25,6 +27,9 @@ export class TransactionList {
@Field(() => Number)
count: number
@Field(() => Number)
linkCount: number
@Field(() => Decimal)
balance: Decimal

View File

@ -1,7 +1,7 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import { Resolver, Query, Arg, Args, Authorized, Mutation, Ctx } from 'type-graphql'
import { Resolver, Query, Arg, Args, Authorized, Mutation, Ctx, Int } from 'type-graphql'
import {
getCustomRepository,
IsNull,
@ -10,25 +10,30 @@ import {
getConnection,
In,
} from '@dbTools/typeorm'
import { UserAdmin, SearchUsersResult } from '../model/UserAdmin'
import { PendingCreation } from '../model/PendingCreation'
import { CreatePendingCreations } from '../model/CreatePendingCreations'
import { UpdatePendingCreation } from '../model/UpdatePendingCreation'
import { RIGHTS } from '../../auth/RIGHTS'
import { UserRepository } from '../../typeorm/repository/User'
import CreatePendingCreationArgs from '../arg/CreatePendingCreationArgs'
import UpdatePendingCreationArgs from '../arg/UpdatePendingCreationArgs'
import SearchUsersArgs from '../arg/SearchUsersArgs'
import { Transaction } from '@entity/Transaction'
import { TransactionRepository } from '../../typeorm/repository/Transaction'
import { calculateDecay } from '../../util/decay'
import { UserAdmin, SearchUsersResult } from '@model/UserAdmin'
import { PendingCreation } from '@model/PendingCreation'
import { CreatePendingCreations } from '@model/CreatePendingCreations'
import { UpdatePendingCreation } from '@model/UpdatePendingCreation'
import { RIGHTS } from '@/auth/RIGHTS'
import { UserRepository } from '@repository/User'
import CreatePendingCreationArgs from '@arg/CreatePendingCreationArgs'
import UpdatePendingCreationArgs from '@arg/UpdatePendingCreationArgs'
import SearchUsersArgs from '@arg/SearchUsersArgs'
import { Transaction as DbTransaction } from '@entity/Transaction'
import { Transaction } from '@model/Transaction'
import { TransactionRepository } from '@repository/Transaction'
import { calculateDecay } from '@/util/decay'
import { AdminPendingCreation } from '@entity/AdminPendingCreation'
import { hasElopageBuys } from '../../util/hasElopageBuys'
import { hasElopageBuys } from '@/util/hasElopageBuys'
import { LoginEmailOptIn } from '@entity/LoginEmailOptIn'
import { User } from '@entity/User'
import { TransactionTypeId } from '../enum/TransactionTypeId'
import { User as dbUser } from '@entity/User'
import { User } from '@model/User'
import { TransactionTypeId } from '@enum/TransactionTypeId'
import Decimal from 'decimal.js-light'
import { Decay } from '../model/Decay'
import { Decay } from '@model/Decay'
import Paginated from '@arg/Paginated'
import { Order } from '@enum/Order'
import { communityUser } from '@/util/communityUser'
// const EMAIL_OPT_IN_REGISTER = 1
// const EMAIL_OPT_UNKNOWN = 3 // elopage?
@ -122,28 +127,30 @@ export class AdminResolver {
@Authorized([RIGHTS.DELETE_USER])
@Mutation(() => Date, { nullable: true })
async deleteUser(@Arg('userId') userId: number, @Ctx() context: any): Promise<Date | null> {
const user = await User.findOne({ id: userId })
async deleteUser(
@Arg('userId', () => Int) userId: number,
@Ctx() context: any,
): Promise<Date | null> {
const user = await dbUser.findOne({ id: userId })
// user exists ?
if (!user) {
throw new Error(`Could not find user with userId: ${userId}`)
}
// moderator user disabled own account?
const userRepository = getCustomRepository(UserRepository)
const moderatorUser = await userRepository.findByPubkeyHex(context.pubKey)
const moderatorUser = context.user
if (moderatorUser.id === userId) {
throw new Error('Moderator can not delete his own account!')
}
// soft-delete user
await user.softRemove()
const newUser = await User.findOne({ id: userId }, { withDeleted: true })
const newUser = await dbUser.findOne({ id: userId }, { withDeleted: true })
return newUser ? newUser.deletedAt : null
}
@Authorized([RIGHTS.UNDELETE_USER])
@Mutation(() => Date, { nullable: true })
async unDeleteUser(@Arg('userId') userId: number): Promise<Date | null> {
const user = await User.findOne({ id: userId }, { withDeleted: true })
async unDeleteUser(@Arg('userId', () => Int) userId: number): Promise<Date | null> {
const user = await dbUser.findOne({ id: userId }, { withDeleted: true })
// user exists ?
if (!user) {
throw new Error(`Could not find user with userId: ${userId}`)
@ -158,7 +165,7 @@ export class AdminResolver {
async createPendingCreation(
@Args() { email, amount, memo, creationDate, moderator }: CreatePendingCreationArgs,
): Promise<number[]> {
const user = await User.findOne({ email }, { withDeleted: true })
const user = await dbUser.findOne({ email }, { withDeleted: true })
if (!user) {
throw new Error(`Could not find user with email: ${email}`)
}
@ -215,7 +222,7 @@ export class AdminResolver {
async updatePendingCreation(
@Args() { id, email, amount, memo, creationDate, moderator }: UpdatePendingCreationArgs,
): Promise<UpdatePendingCreation> {
const user = await User.findOne({ email }, { withDeleted: true })
const user = await dbUser.findOne({ email }, { withDeleted: true })
if (!user) {
throw new Error(`Could not find user with email: ${email}`)
}
@ -265,7 +272,7 @@ export class AdminResolver {
const userIds = pendingCreations.map((p) => p.userId)
const userCreations = await getUserCreations(userIds)
const users = await User.find({ where: { id: In(userIds) }, withDeleted: true })
const users = await dbUser.find({ where: { id: In(userIds) }, withDeleted: true })
return pendingCreations.map((pendingCreation) => {
const user = users.find((u) => u.id === pendingCreation.userId)
@ -284,7 +291,7 @@ export class AdminResolver {
@Authorized([RIGHTS.DELETE_PENDING_CREATION])
@Mutation(() => Boolean)
async deletePendingCreation(@Arg('id') id: number): Promise<boolean> {
async deletePendingCreation(@Arg('id', () => Int) id: number): Promise<boolean> {
const entity = await AdminPendingCreation.findOneOrFail(id)
const res = await AdminPendingCreation.delete(entity)
return !!res
@ -292,14 +299,16 @@ export class AdminResolver {
@Authorized([RIGHTS.CONFIRM_PENDING_CREATION])
@Mutation(() => Boolean)
async confirmPendingCreation(@Arg('id') id: number, @Ctx() context: any): Promise<boolean> {
async confirmPendingCreation(
@Arg('id', () => Int) id: number,
@Ctx() context: any,
): Promise<boolean> {
const pendingCreation = await AdminPendingCreation.findOneOrFail(id)
const userRepository = getCustomRepository(UserRepository)
const moderatorUser = await userRepository.findByPubkeyHex(context.pubKey)
const moderatorUser = context.user
if (moderatorUser.id === pendingCreation.userId)
throw new Error('Moderator can not confirm own pending creation')
const user = await User.findOneOrFail({ id: pendingCreation.userId }, { withDeleted: true })
const user = await dbUser.findOneOrFail({ id: pendingCreation.userId }, { withDeleted: true })
if (user.deletedAt) throw new Error('This user was deleted. Cannot confirm a creation.')
const creations = await getUserCreation(pendingCreation.userId, false)
@ -321,7 +330,7 @@ export class AdminResolver {
// TODO pending creations decimal
newBalance = newBalance.add(new Decimal(Number(pendingCreation.amount)).toString())
const transaction = new Transaction()
const transaction = new DbTransaction()
transaction.typeId = TransactionTypeId.CREATION
transaction.memo = pendingCreation.memo
transaction.userId = pendingCreation.userId
@ -339,6 +348,27 @@ export class AdminResolver {
return true
}
@Authorized([RIGHTS.CREATION_TRANSACTION_LIST])
@Query(() => [Transaction])
async creationTransactionList(
@Args()
{ currentPage = 1, pageSize = 25, order = Order.DESC }: Paginated,
@Arg('userId', () => Int) userId: number,
): Promise<Transaction[]> {
const offset = (currentPage - 1) * pageSize
const transactionRepository = getCustomRepository(TransactionRepository)
const [userTransactions] = await transactionRepository.findByUserPaged(
userId,
pageSize,
offset,
order,
true,
)
const user = await dbUser.findOneOrFail({ id: userId })
return userTransactions.map((t) => new Transaction(t, new User(user), communityUser))
}
}
interface CreationMap {

View File

@ -2,11 +2,9 @@
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import { Resolver, Query, Ctx, Authorized } from 'type-graphql'
import { getCustomRepository } from '@dbTools/typeorm'
import { Balance } from '../model/Balance'
import { UserRepository } from '../../typeorm/repository/User'
import { calculateDecay } from '../../util/decay'
import { RIGHTS } from '../../auth/RIGHTS'
import { Balance } from '@model/Balance'
import { calculateDecay } from '@/util/decay'
import { RIGHTS } from '@/auth/RIGHTS'
import { Transaction } from '@entity/Transaction'
import Decimal from 'decimal.js-light'
@ -16,9 +14,7 @@ export class BalanceResolver {
@Query(() => Balance)
async balance(@Ctx() context: any): Promise<Balance> {
// load user and balance
const userRepository = getCustomRepository(UserRepository)
const user = await userRepository.findByPubkeyHex(context.pubKey)
const { user } = context
const now = new Date()
const lastTransaction = await Transaction.findOne(

View File

@ -2,10 +2,10 @@
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import { createTestClient } from 'apollo-server-testing'
import createServer from '../../server/createServer'
import CONFIG from '../../config'
import createServer from '@/server/createServer'
import CONFIG from '@/config'
jest.mock('../../config')
jest.mock('@/config')
let query: any

View File

@ -2,9 +2,9 @@
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import { Resolver, Query, Authorized } from 'type-graphql'
import { RIGHTS } from '../../auth/RIGHTS'
import CONFIG from '../../config'
import { Community } from '../model/Community'
import { RIGHTS } from '@/auth/RIGHTS'
import CONFIG from '@/config'
import { Community } from '@model/Community'
@Resolver()
export class CommunityResolver {

View File

@ -2,14 +2,12 @@
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import { Resolver, Query, Args, Ctx, Authorized, Arg } from 'type-graphql'
import { getCustomRepository } from '@dbTools/typeorm'
import CONFIG from '../../config'
import { GdtEntryList } from '../model/GdtEntryList'
import Paginated from '../arg/Paginated'
import { apiGet } from '../../apis/HttpRequest'
import { UserRepository } from '../../typeorm/repository/User'
import { Order } from '../enum/Order'
import { RIGHTS } from '../../auth/RIGHTS'
import CONFIG from '@/config'
import { GdtEntryList } from '@model/GdtEntryList'
import Paginated from '@arg/Paginated'
import { apiGet } from '@/apis/HttpRequest'
import { Order } from '@enum/Order'
import { RIGHTS } from '@/auth/RIGHTS'
@Resolver()
export class GdtResolver {
@ -22,8 +20,7 @@ export class GdtResolver {
@Ctx() context: any,
): Promise<GdtEntryList> {
// load user
const userRepository = getCustomRepository(UserRepository)
const userEntity = await userRepository.findByPubkeyHex(context.pubKey)
const userEntity = context.user
try {
const resultGDT = await apiGet(

View File

@ -7,9 +7,9 @@ import {
getKlicktippTagMap,
unsubscribe,
klicktippSignIn,
} from '../../apis/KlicktippController'
import { RIGHTS } from '../../auth/RIGHTS'
import SubscribeNewsletterArgs from '../arg/SubscribeNewsletterArgs'
} from '@/apis/KlicktippController'
import { RIGHTS } from '@/auth/RIGHTS'
import SubscribeNewsletterArgs from '@arg/SubscribeNewsletterArgs'
@Resolver()
export class KlicktippResolver {

View File

@ -0,0 +1,14 @@
import { transactionLinkCode } from './TransactionLinkResolver'
describe('transactionLinkCode', () => {
const date = new Date()
it('returns a string of length 24', () => {
expect(transactionLinkCode(date)).toHaveLength(24)
})
it('returns a string that ends with the hex value of date', () => {
const regexp = new RegExp(date.getTime().toString(16) + '$')
expect(transactionLinkCode(date)).toEqual(expect.stringMatching(regexp))
})
})

View File

@ -0,0 +1,169 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import { Resolver, Args, Arg, Authorized, Ctx, Mutation, Query, Int } from 'type-graphql'
import { TransactionLink } from '@model/TransactionLink'
import { TransactionLink as dbTransactionLink } from '@entity/TransactionLink'
import { User as dbUser } from '@entity/User'
import TransactionLinkArgs from '@arg/TransactionLinkArgs'
import Paginated from '@arg/Paginated'
import { calculateBalance } from '@/util/validate'
import { RIGHTS } from '@/auth/RIGHTS'
import { randomBytes } from 'crypto'
import { User } from '@model/User'
import { calculateDecay } from '@/util/decay'
import { executeTransaction } from './TransactionResolver'
import { Order } from '@enum/Order'
// TODO: do not export, test it inside the resolver
export const transactionLinkCode = (date: Date): string => {
const time = date.getTime().toString(16)
return (
randomBytes(12)
.toString('hex')
.substring(0, 24 - time.length) + time
)
}
const CODE_VALID_DAYS_DURATION = 14
export const transactionLinkExpireDate = (date: Date): Date => {
const validUntil = new Date(date)
return new Date(validUntil.setDate(date.getDate() + CODE_VALID_DAYS_DURATION))
}
@Resolver()
export class TransactionLinkResolver {
@Authorized([RIGHTS.CREATE_TRANSACTION_LINK])
@Mutation(() => TransactionLink)
async createTransactionLink(
@Args() { amount, memo }: TransactionLinkArgs,
@Ctx() context: any,
): Promise<TransactionLink> {
const { user } = context
const createdDate = new Date()
const validUntil = transactionLinkExpireDate(createdDate)
const holdAvailableAmount = amount.minus(calculateDecay(amount, createdDate, validUntil).decay)
// validate amount
const sendBalance = await calculateBalance(user.id, holdAvailableAmount.mul(-1), createdDate)
if (!sendBalance) {
throw new Error("user hasn't enough GDD or amount is < 0")
}
const transactionLink = dbTransactionLink.create()
transactionLink.userId = user.id
transactionLink.amount = amount
transactionLink.memo = memo
transactionLink.holdAvailableAmount = holdAvailableAmount
transactionLink.code = transactionLinkCode(createdDate)
transactionLink.createdAt = createdDate
transactionLink.validUntil = validUntil
await dbTransactionLink.save(transactionLink).catch(() => {
throw new Error('Unable to save transaction link')
})
return new TransactionLink(transactionLink, new User(user))
}
@Authorized([RIGHTS.DELETE_TRANSACTION_LINK])
@Mutation(() => Boolean)
async deleteTransactionLink(
@Arg('id', () => Int) id: number,
@Ctx() context: any,
): Promise<boolean> {
const { user } = context
const transactionLink = await dbTransactionLink.findOne({ id })
if (!transactionLink) {
throw new Error('Transaction Link not found!')
}
if (transactionLink.userId !== user.id) {
throw new Error('Transaction Link cannot be deleted!')
}
if (transactionLink.redeemedBy) {
throw new Error('Transaction Link already redeemed!')
}
await transactionLink.softRemove().catch(() => {
throw new Error('Transaction Link could not be deleted!')
})
return true
}
@Authorized([RIGHTS.QUERY_TRANSACTION_LINK])
@Query(() => TransactionLink)
async queryTransactionLink(@Arg('code') code: string): Promise<TransactionLink> {
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) {
redeemedBy = new User(await dbUser.findOneOrFail({ id: transactionLink.redeemedBy }))
}
return new TransactionLink(transactionLink, new User(user), redeemedBy)
}
@Authorized([RIGHTS.LIST_TRANSACTION_LINKS])
@Query(() => [TransactionLink])
async listTransactionLinks(
@Args()
{ currentPage = 1, pageSize = 5, order = Order.DESC }: Paginated,
@Ctx() context: any,
): Promise<TransactionLink[]> {
const { user } = context
// const now = new Date()
const transactionLinks = await dbTransactionLink.find({
where: {
userId: user.id,
redeemedBy: null,
// validUntil: MoreThan(now),
},
order: {
createdAt: order,
},
skip: (currentPage - 1) * pageSize,
take: pageSize,
})
return transactionLinks.map((tl) => new TransactionLink(tl, new User(user)))
}
@Authorized([RIGHTS.REDEEM_TRANSACTION_LINK])
@Mutation(() => Boolean)
async redeemTransactionLink(
@Arg('code', () => String) code: string,
@Ctx() context: any,
): Promise<boolean> {
const { user } = context
const transactionLink = await dbTransactionLink.findOneOrFail({ code })
const linkedUser = await dbUser.findOneOrFail({ id: transactionLink.userId })
const now = new Date()
if (user.id === linkedUser.id) {
throw new Error('Cannot redeem own transaction link.')
}
if (transactionLink.validUntil.getTime() < now.getTime()) {
throw new Error('Transaction Link is not valid anymore.')
}
if (transactionLink.redeemedBy) {
throw new Error('Transaction Link already redeemed.')
}
await executeTransaction(
transactionLink.amount,
transactionLink.memo,
linkedUser,
user,
transactionLink,
)
return true
}
}

View File

@ -6,32 +6,124 @@
import { Resolver, Query, Args, Authorized, Ctx, Mutation } from 'type-graphql'
import { getCustomRepository, getConnection } from '@dbTools/typeorm'
import CONFIG from '../../config'
import { sendTransactionReceivedEmail } from '../../mailer/sendTransactionReceivedEmail'
import CONFIG from '@/config'
import { sendTransactionReceivedEmail } from '@/mailer/sendTransactionReceivedEmail'
import { Transaction } from '../model/Transaction'
import { TransactionList } from '../model/TransactionList'
import { Transaction } from '@model/Transaction'
import { TransactionList } from '@model/TransactionList'
import TransactionSendArgs from '../arg/TransactionSendArgs'
import Paginated from '../arg/Paginated'
import TransactionSendArgs from '@arg/TransactionSendArgs'
import Paginated from '@arg/Paginated'
import { Order } from '../enum/Order'
import { Order } from '@enum/Order'
import { UserRepository } from '../../typeorm/repository/User'
import { TransactionRepository } from '../../typeorm/repository/Transaction'
import { TransactionRepository } from '@repository/Transaction'
import { TransactionLinkRepository } from '@repository/TransactionLink'
import { User as dbUser } from '@entity/User'
import { Transaction as dbTransaction } from '@entity/Transaction'
import { TransactionLink as dbTransactionLink } from '@entity/TransactionLink'
import { apiPost } from '../../apis/HttpRequest'
import { TransactionTypeId } from '../enum/TransactionTypeId'
import { calculateBalance, isHexPublicKey } from '../../util/validate'
import { RIGHTS } from '../../auth/RIGHTS'
import { User } from '../model/User'
import { communityUser } from '../../util/communityUser'
import { virtualDecayTransaction } from '../../util/virtualDecayTransaction'
import { apiPost } from '@/apis/HttpRequest'
import { TransactionTypeId } from '@enum/TransactionTypeId'
import { calculateBalance, isHexPublicKey } from '@/util/validate'
import { RIGHTS } from '@/auth/RIGHTS'
import { User } from '@model/User'
import { communityUser } from '@/util/communityUser'
import { virtualLinkTransaction, virtualDecayTransaction } from '@/util/virtualTransactions'
import Decimal from 'decimal.js-light'
import { calculateDecay } from '../../util/decay'
import { calculateDecay } from '@/util/decay'
export const executeTransaction = async (
amount: Decimal,
memo: string,
sender: dbUser,
recipient: dbUser,
transactionLink?: dbTransactionLink | null,
): Promise<boolean> => {
if (sender.id === recipient.id) {
throw new Error('Sender and Recipient are the same.')
}
// validate amount
const receivedCallDate = new Date()
const sendBalance = await calculateBalance(sender.id, amount.mul(-1), receivedCallDate)
if (!sendBalance) {
throw new Error("user hasn't enough GDD or amount is < 0")
}
const queryRunner = getConnection().createQueryRunner()
await queryRunner.connect()
await queryRunner.startTransaction('READ UNCOMMITTED')
try {
// transaction
const transactionSend = new dbTransaction()
transactionSend.typeId = TransactionTypeId.SEND
transactionSend.memo = memo
transactionSend.userId = sender.id
transactionSend.linkedUserId = recipient.id
transactionSend.amount = amount.mul(-1)
transactionSend.balance = sendBalance.balance
transactionSend.balanceDate = receivedCallDate
transactionSend.decay = sendBalance.decay.decay
transactionSend.decayStart = sendBalance.decay.start
transactionSend.previous = sendBalance.lastTransactionId
transactionSend.transactionLinkId = transactionLink ? transactionLink.id : null
await queryRunner.manager.insert(dbTransaction, transactionSend)
const transactionReceive = new dbTransaction()
transactionReceive.typeId = TransactionTypeId.RECEIVE
transactionReceive.memo = memo
transactionReceive.userId = recipient.id
transactionReceive.linkedUserId = sender.id
transactionReceive.amount = amount
const receiveBalance = await calculateBalance(recipient.id, amount, receivedCallDate)
transactionReceive.balance = receiveBalance ? receiveBalance.balance : amount
transactionReceive.balanceDate = receivedCallDate
transactionReceive.decay = receiveBalance ? receiveBalance.decay.decay : new Decimal(0)
transactionReceive.decayStart = receiveBalance ? receiveBalance.decay.start : null
transactionReceive.previous = receiveBalance ? receiveBalance.lastTransactionId : null
transactionReceive.linkedTransactionId = transactionSend.id
transactionReceive.transactionLinkId = transactionLink ? transactionLink.id : null
await queryRunner.manager.insert(dbTransaction, transactionReceive)
// Save linked transaction id for send
transactionSend.linkedTransactionId = transactionReceive.id
await queryRunner.manager.update(dbTransaction, { id: transactionSend.id }, transactionSend)
if (transactionLink) {
transactionLink.redeemedAt = receivedCallDate
transactionLink.redeemedBy = recipient.id
await queryRunner.manager.update(
dbTransactionLink,
{ id: transactionLink.id },
transactionLink,
)
}
await queryRunner.commitTransaction()
} catch (e) {
await queryRunner.rollbackTransaction()
throw new Error(`Transaction was not successful: ${e}`)
} finally {
await queryRunner.release()
}
// send notification email
// TODO: translate
await sendTransactionReceivedEmail({
senderFirstName: sender.firstName,
senderLastName: sender.lastName,
recipientFirstName: recipient.firstName,
recipientLastName: recipient.lastName,
email: recipient.email,
senderEmail: sender.email,
amount,
memo,
})
return true
}
@Resolver()
export class TransactionResolver {
@ -39,22 +131,11 @@ export class TransactionResolver {
@Query(() => TransactionList)
async transactionList(
@Args()
{
currentPage = 1,
pageSize = 25,
order = Order.DESC,
onlyCreations = false,
userId,
}: Paginated,
{ currentPage = 1, pageSize = 25, order = Order.DESC }: Paginated,
@Ctx() context: any,
): Promise<TransactionList> {
const now = new Date()
// find user
const userRepository = getCustomRepository(UserRepository)
// TODO: separate those usecases - this is a security issue
const user = userId
? await userRepository.findOneOrFail({ id: userId }, { withDeleted: true })
: await userRepository.findByPubkeyHex(context.pubKey)
const user = context.user
// find current balance
const lastTransaction = await dbTransaction.findOne(
@ -78,7 +159,7 @@ export class TransactionResolver {
}
if (!lastTransaction) {
return new TransactionList(new Decimal(0), [], 0, balanceGDT)
return new TransactionList(new Decimal(0), [], 0, 0, balanceGDT)
}
// find transactions
@ -90,7 +171,6 @@ export class TransactionResolver {
pageSize,
offset,
order,
onlyCreations,
)
// find involved users; I am involved
@ -111,11 +191,29 @@ export class TransactionResolver {
const self = new User(user)
const transactions: Transaction[] = []
// decay transaction
if (!onlyCreations && currentPage === 1 && order === Order.DESC) {
const transactionLinkRepository = getCustomRepository(TransactionLinkRepository)
const { sumHoldAvailableAmount, sumAmount, lastDate, firstDate, transactionLinkcount } =
await transactionLinkRepository.summary(user.id, now)
// decay & link transactions
if (currentPage === 1 && order === Order.DESC) {
transactions.push(
virtualDecayTransaction(lastTransaction.balance, lastTransaction.balanceDate, now, self),
)
// virtual transaction for pending transaction-links sum
if (sumHoldAvailableAmount.greaterThan(0)) {
transactions.push(
virtualLinkTransaction(
lastTransaction.balance.minus(sumHoldAvailableAmount.toString()),
sumAmount.mul(-1),
sumHoldAvailableAmount.mul(-1),
sumHoldAvailableAmount.minus(sumAmount.toString()).mul(-1),
firstDate || now,
lastDate || now,
self,
),
)
}
}
// transactions
@ -129,9 +227,12 @@ export class TransactionResolver {
// Construct Result
return new TransactionList(
calculateDecay(lastTransaction.balance, lastTransaction.balanceDate, now).balance,
calculateDecay(lastTransaction.balance, lastTransaction.balanceDate, now).balance.minus(
sumHoldAvailableAmount.toString(),
),
transactions,
userTransactionsCount,
transactionLinkcount,
balanceGDT,
)
}
@ -143,17 +244,10 @@ export class TransactionResolver {
@Ctx() context: any,
): Promise<boolean> {
// TODO this is subject to replay attacks
const userRepository = getCustomRepository(UserRepository)
const senderUser = await userRepository.findByPubkeyHex(context.pubKey)
const senderUser = context.user
if (senderUser.pubKey.length !== 32) {
throw new Error('invalid sender public key')
}
// validate amount
const receivedCallDate = new Date()
const sendBalance = await calculateBalance(senderUser.id, amount.mul(-1), receivedCallDate)
if (!sendBalance) {
throw new Error("user hasn't enough GDD or amount is < 0")
}
// validate recipient user
const recipientUser = await dbUser.findOne({ email: email }, { withDeleted: true })
@ -167,62 +261,7 @@ export class TransactionResolver {
throw new Error('invalid recipient public key')
}
const queryRunner = getConnection().createQueryRunner()
await queryRunner.connect()
await queryRunner.startTransaction('READ UNCOMMITTED')
try {
// transaction
const transactionSend = new dbTransaction()
transactionSend.typeId = TransactionTypeId.SEND
transactionSend.memo = memo
transactionSend.userId = senderUser.id
transactionSend.linkedUserId = recipientUser.id
transactionSend.amount = amount.mul(-1)
transactionSend.balance = sendBalance.balance
transactionSend.balanceDate = receivedCallDate
transactionSend.decay = sendBalance.decay.decay
transactionSend.decayStart = sendBalance.decay.start
transactionSend.previous = sendBalance.lastTransactionId
await queryRunner.manager.insert(dbTransaction, transactionSend)
const transactionReceive = new dbTransaction()
transactionReceive.typeId = TransactionTypeId.RECEIVE
transactionReceive.memo = memo
transactionReceive.userId = recipientUser.id
transactionReceive.linkedUserId = senderUser.id
transactionReceive.amount = amount
const receiveBalance = await calculateBalance(recipientUser.id, amount, receivedCallDate)
transactionReceive.balance = receiveBalance ? receiveBalance.balance : amount
transactionReceive.balanceDate = receivedCallDate
transactionReceive.decay = receiveBalance ? receiveBalance.decay.decay : new Decimal(0)
transactionReceive.decayStart = receiveBalance ? receiveBalance.decay.start : null
transactionReceive.previous = receiveBalance ? receiveBalance.lastTransactionId : null
transactionReceive.linkedTransactionId = transactionSend.id
await queryRunner.manager.insert(dbTransaction, transactionReceive)
// Save linked transaction id for send
transactionSend.linkedTransactionId = transactionReceive.id
await queryRunner.manager.update(dbTransaction, { id: transactionSend.id }, transactionSend)
await queryRunner.commitTransaction()
} catch (e) {
await queryRunner.rollbackTransaction()
throw new Error(`Transaction was not successful: ${e}`)
} finally {
await queryRunner.release()
}
// send notification email
// TODO: translate
await sendTransactionReceivedEmail({
senderFirstName: senderUser.firstName,
senderLastName: senderUser.lastName,
recipientFirstName: recipientUser.firstName,
recipientLastName: recipientUser.lastName,
email: recipientUser.email,
amount,
memo,
})
await executeTransaction(amount, memo, senderUser, recipientUser)
return true
}

View File

@ -1,20 +1,20 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import { createTestClient } from 'apollo-server-testing'
import gql from 'graphql-tag'
import { testEnvironment, headerPushMock, resetToken, cleanDB } from '@test/helpers'
import { userFactory } from '@/seeds/factory/user'
import { bibiBloxberg } from '@/seeds/users/bibi-bloxberg'
import { createUser, setPassword } from '@/seeds/graphql/mutations'
import { login, logout } from '@/seeds/graphql/queries'
import { GraphQLError } from 'graphql'
import createServer from '../../server/createServer'
import { resetDB, initialize } from '@dbTools/helpers'
import { LoginEmailOptIn } from '@entity/LoginEmailOptIn'
import { User } from '@entity/User'
import CONFIG from '../../config'
import { sendAccountActivationEmail } from '../../mailer/sendAccountActivationEmail'
// import { klicktippSignIn } from '../../apis/KlicktippController'
import CONFIG from '@/config'
import { sendAccountActivationEmail } from '@/mailer/sendAccountActivationEmail'
jest.setTimeout(10000)
// import { klicktippSignIn } from '@/apis/KlicktippController'
jest.mock('../../mailer/sendAccountActivationEmail', () => {
jest.mock('@/mailer/sendAccountActivationEmail', () => {
return {
__esModule: true,
sendAccountActivationEmail: jest.fn(),
@ -22,7 +22,7 @@ jest.mock('../../mailer/sendAccountActivationEmail', () => {
})
/*
jest.mock('../../apis/KlicktippController', () => {
jest.mock('@/apis/KlicktippController', () => {
return {
__esModule: true,
klicktippSignIn: jest.fn(),
@ -30,19 +30,19 @@ jest.mock('../../apis/KlicktippController', () => {
})
*/
let mutate: any
let con: any
let mutate: any, query: any, con: any
let testEnv: any
beforeAll(async () => {
const server = await createServer({})
con = server.con
mutate = createTestClient(server.apollo).mutate
await initialize()
await resetDB()
testEnv = await testEnvironment()
mutate = testEnv.mutate
query = testEnv.query
con = testEnv.con
await cleanDB()
})
afterAll(async () => {
await resetDB(true)
await cleanDB()
await con.close()
})
@ -56,37 +56,22 @@ describe('UserResolver', () => {
publisherId: 1234,
}
const mutation = gql`
mutation (
$email: String!
$firstName: String!
$lastName: String!
$language: String!
$publisherId: Int
) {
createUser(
email: $email
firstName: $firstName
lastName: $lastName
language: $language
publisherId: $publisherId
)
}
`
let result: any
let emailOptIn: string
beforeAll(async () => {
result = await mutate({ mutation, variables })
jest.clearAllMocks()
result = await mutate({ mutation: createUser, variables })
})
afterAll(async () => {
await resetDB()
await cleanDB()
})
it('returns success', () => {
expect(result).toEqual(expect.objectContaining({ data: { createUser: 'success' } }))
expect(result).toEqual(
expect.objectContaining({ data: { createUser: { id: expect.any(Number) } } }),
)
})
describe('valid input data', () => {
@ -116,6 +101,7 @@ describe('UserResolver', () => {
language: 'de',
deletedAt: null,
publisherId: 1234,
referrerId: null,
},
])
})
@ -138,7 +124,10 @@ describe('UserResolver', () => {
describe('account activation email', () => {
it('sends an account activation email', () => {
const activationLink = CONFIG.EMAIL_LINK_VERIFICATION.replace(/{code}/g, emailOptIn)
const activationLink = CONFIG.EMAIL_LINK_VERIFICATION.replace(
/{optin}/g,
emailOptIn,
).replace(/{code}/g, '')
expect(sendAccountActivationEmail).toBeCalledWith({
link: activationLink,
firstName: 'Peter',
@ -150,7 +139,7 @@ describe('UserResolver', () => {
describe('email already exists', () => {
it('throws an error', async () => {
await expect(mutate({ mutation, variables })).resolves.toEqual(
await expect(mutate({ mutation: createUser, variables })).resolves.toEqual(
expect.objectContaining({
errors: [new GraphQLError('User already exists.')],
}),
@ -161,7 +150,7 @@ describe('UserResolver', () => {
describe('unknown language', () => {
it('sets "de" as default language', async () => {
await mutate({
mutation,
mutation: createUser,
variables: { ...variables, email: 'bibi@bloxberg.de', language: 'es' },
})
await expect(User.find()).resolves.toEqual(
@ -178,7 +167,7 @@ describe('UserResolver', () => {
describe('no publisher id', () => {
it('sets publisher id to null', async () => {
await mutate({
mutation,
mutation: createUser,
variables: { ...variables, email: 'raeuber@hotzenplotz.de', publisherId: undefined },
})
await expect(User.find()).resolves.toEqual(
@ -194,24 +183,6 @@ describe('UserResolver', () => {
})
describe('setPassword', () => {
const createUserMutation = gql`
mutation (
$email: String!
$firstName: String!
$lastName: String!
$language: String!
$publisherId: Int
) {
createUser(
email: $email
firstName: $firstName
lastName: $lastName
language: $language
publisherId: $publisherId
)
}
`
const createUserVariables = {
email: 'peter@lustig.de',
firstName: 'Peter',
@ -220,11 +191,6 @@ describe('UserResolver', () => {
publisherId: 1234,
}
const setPasswordMutation = gql`
mutation ($code: String!, $password: String!) {
setPassword(code: $code, password: $password)
}
`
let result: any
let emailOptIn: string
@ -232,18 +198,18 @@ describe('UserResolver', () => {
let newUser: any
beforeAll(async () => {
await mutate({ mutation: createUserMutation, variables: createUserVariables })
await mutate({ mutation: createUser, variables: createUserVariables })
const loginEmailOptIn = await LoginEmailOptIn.find()
emailOptIn = loginEmailOptIn[0].verificationCode.toString()
result = await mutate({
mutation: setPasswordMutation,
mutation: setPassword,
variables: { code: emailOptIn, password: 'Aa12345_' },
})
newUser = await User.find()
})
afterAll(async () => {
await resetDB()
await cleanDB()
})
it('sets email checked to true', () => {
@ -276,17 +242,17 @@ describe('UserResolver', () => {
describe('no valid password', () => {
beforeAll(async () => {
await mutate({ mutation: createUserMutation, variables: createUserVariables })
await mutate({ mutation: createUser, variables: createUserVariables })
const loginEmailOptIn = await LoginEmailOptIn.find()
emailOptIn = loginEmailOptIn[0].verificationCode.toString()
result = await mutate({
mutation: setPasswordMutation,
mutation: setPassword,
variables: { code: emailOptIn, password: 'not-valid' },
})
})
afterAll(async () => {
await resetDB()
await cleanDB()
})
it('throws an error', () => {
@ -304,15 +270,15 @@ describe('UserResolver', () => {
describe('no valid optin code', () => {
beforeAll(async () => {
await mutate({ mutation: createUserMutation, variables: createUserVariables })
await mutate({ mutation: createUser, variables: createUserVariables })
result = await mutate({
mutation: setPasswordMutation,
mutation: setPassword,
variables: { code: 'not valid', password: 'Aa12345_' },
})
})
afterAll(async () => {
await resetDB()
await cleanDB()
})
it('throws an error', () => {
@ -324,4 +290,128 @@ describe('UserResolver', () => {
})
})
})
describe('login', () => {
const variables = {
email: 'bibi@bloxberg.de',
password: 'Aa12345_',
publisherId: 1234,
}
let result: User
afterAll(async () => {
await cleanDB()
})
describe('no users in database', () => {
beforeAll(async () => {
result = await query({ query: login, variables })
})
it('throws an error', () => {
expect(result).toEqual(
expect.objectContaining({
errors: [new GraphQLError('No user with this credentials')],
}),
)
})
})
describe('user is in database and correct login data', () => {
beforeAll(async () => {
await userFactory(testEnv, bibiBloxberg)
result = await query({ query: login, variables })
})
afterAll(async () => {
await cleanDB()
})
it('returns the user object', () => {
expect(result).toEqual(
expect.objectContaining({
data: {
login: {
coinanimation: true,
email: 'bibi@bloxberg.de',
firstName: 'Bibi',
hasElopage: false,
id: expect.any(Number),
isAdmin: false,
klickTipp: {
newsletterState: false,
},
language: 'de',
lastName: 'Bloxberg',
publisherId: 1234,
},
},
}),
)
})
it('sets the token in the header', () => {
expect(headerPushMock).toBeCalledWith({ key: 'token', value: expect.any(String) })
})
})
describe('user is in database and wrong password', () => {
beforeAll(async () => {
await userFactory(testEnv, bibiBloxberg)
})
afterAll(async () => {
await cleanDB()
})
it('returns an error', () => {
expect(
query({ query: login, variables: { ...variables, password: 'wrong' } }),
).resolves.toEqual(
expect.objectContaining({
errors: [new GraphQLError('No user with this credentials')],
}),
)
})
})
})
describe('logout', () => {
describe('unauthenticated', () => {
it('throws an error', async () => {
resetToken()
await expect(query({ query: logout })).resolves.toEqual(
expect.objectContaining({
errors: [new GraphQLError('401 Unauthorized')],
}),
)
})
})
describe('authenticated', () => {
const variables = {
email: 'bibi@bloxberg.de',
password: 'Aa12345_',
}
beforeAll(async () => {
await userFactory(testEnv, bibiBloxberg)
await query({ query: login, variables })
})
afterAll(async () => {
await cleanDB()
})
it('returns true', async () => {
await expect(query({ query: logout })).resolves.toEqual(
expect.objectContaining({
data: { logout: 'true' },
errors: undefined,
}),
)
})
})
})
})

View File

@ -4,24 +4,24 @@
import fs from 'fs'
import { Resolver, Query, Args, Arg, Authorized, Ctx, UseMiddleware, Mutation } from 'type-graphql'
import { getConnection, getCustomRepository, QueryRunner } from '@dbTools/typeorm'
import CONFIG from '../../config'
import { User } from '../model/User'
import CONFIG from '@/config'
import { User } from '@model/User'
import { User as DbUser } from '@entity/User'
import { encode } from '../../auth/JWT'
import CreateUserArgs from '../arg/CreateUserArgs'
import UnsecureLoginArgs from '../arg/UnsecureLoginArgs'
import UpdateUserInfosArgs from '../arg/UpdateUserInfosArgs'
import { klicktippNewsletterStateMiddleware } from '../../middleware/klicktippMiddleware'
import { UserSettingRepository } from '../../typeorm/repository/UserSettingRepository'
import { Setting } from '../enum/Setting'
import { UserRepository } from '../../typeorm/repository/User'
import { TransactionLink as dbTransactionLink } from '@entity/TransactionLink'
import { encode } from '@/auth/JWT'
import CreateUserArgs from '@arg/CreateUserArgs'
import UnsecureLoginArgs from '@arg/UnsecureLoginArgs'
import UpdateUserInfosArgs from '@arg/UpdateUserInfosArgs'
import { klicktippNewsletterStateMiddleware } from '@/middleware/klicktippMiddleware'
import { UserSettingRepository } from '@repository/UserSettingRepository'
import { Setting } from '@enum/Setting'
import { LoginEmailOptIn } from '@entity/LoginEmailOptIn'
import { sendResetPasswordEmail } from '../../mailer/sendResetPasswordEmail'
import { sendAccountActivationEmail } from '../../mailer/sendAccountActivationEmail'
import { klicktippSignIn } from '../../apis/KlicktippController'
import { RIGHTS } from '../../auth/RIGHTS'
import { ROLE_ADMIN } from '../../auth/ROLES'
import { hasElopageBuys } from '../../util/hasElopageBuys'
import { sendResetPasswordEmail } from '@/mailer/sendResetPasswordEmail'
import { sendAccountActivationEmail } from '@/mailer/sendAccountActivationEmail'
import { klicktippSignIn } from '@/apis/KlicktippController'
import { RIGHTS } from '@/auth/RIGHTS'
import { ROLE_ADMIN } from '@/auth/ROLES'
import { hasElopageBuys } from '@/util/hasElopageBuys'
import { ServerUser } from '@entity/ServerUser'
const EMAIL_OPT_IN_RESET_PASSWORD = 2
@ -157,15 +157,11 @@ const createEmailOptIn = async (
emailOptInTypeId: EMAIL_OPT_IN_REGISTER,
})
if (emailOptIn) {
const timeElapsed = Date.now() - new Date(emailOptIn.updatedAt).getTime()
if (timeElapsed <= parseInt(CONFIG.RESEND_TIME.toString()) * 60 * 1000) {
throw new Error(
'email already sent less than ' + parseInt(CONFIG.RESEND_TIME.toString()) + ' minutes ago',
)
} else {
emailOptIn.updatedAt = new Date()
emailOptIn.resendCount++
if (isOptInCodeValid(emailOptIn)) {
throw new Error(`email already sent less than $(CONFIG.EMAIL_CODE_VALID_TIME} minutes ago`)
}
emailOptIn.updatedAt = new Date()
emailOptIn.resendCount++
} else {
emailOptIn = new LoginEmailOptIn()
emailOptIn.verificationCode = random(64)
@ -186,17 +182,13 @@ const getOptInCode = async (loginUserId: number): Promise<LoginEmailOptIn> => {
emailOptInTypeId: EMAIL_OPT_IN_RESET_PASSWORD,
})
// Check for 10 minute delay
// Check for `CONFIG.EMAIL_CODE_VALID_TIME` minute delay
if (optInCode) {
const timeElapsed = Date.now() - new Date(optInCode.updatedAt).getTime()
if (timeElapsed <= parseInt(CONFIG.RESEND_TIME.toString()) * 60 * 1000) {
throw new Error(
'email already sent less than ' + parseInt(CONFIG.RESEND_TIME.toString()) + ' minutes ago',
)
} else {
optInCode.updatedAt = new Date()
optInCode.resendCount++
if (isOptInCodeValid(optInCode)) {
throw new Error(`email already sent less than $(CONFIG.EMAIL_CODE_VALID_TIME} minutes ago`)
}
optInCode.updatedAt = new Date()
optInCode.resendCount++
} else {
optInCode = new LoginEmailOptIn()
optInCode.verificationCode = random(64)
@ -214,8 +206,7 @@ export class UserResolver {
@UseMiddleware(klicktippNewsletterStateMiddleware)
async verifyLogin(@Ctx() context: any): Promise<User> {
// TODO refactor and do not have duplicate code with login(see below)
const userRepository = getCustomRepository(UserRepository)
const userEntity = await userRepository.findByPubkeyHex(context.pubKey)
const userEntity = context.user
const user = new User(userEntity)
// user.pubkey = userEntity.pubKey.toString('hex')
// Elopage Status & Stored PublisherId
@ -313,10 +304,11 @@ export class UserResolver {
}
@Authorized([RIGHTS.CREATE_USER])
@Mutation(() => String)
@Mutation(() => User)
async createUser(
@Args() { email, firstName, lastName, language, publisherId }: CreateUserArgs,
): Promise<string> {
@Args()
{ email, firstName, lastName, language, publisherId, redeemCode = null }: CreateUserArgs,
): Promise<User> {
// TODO: wrong default value (should be null), how does graphql work here? Is it an required field?
// default int publisher_id = 0;
@ -348,6 +340,12 @@ export class UserResolver {
dbUser.language = language
dbUser.publisherId = publisherId
dbUser.passphrase = passphrase.join(' ')
if (redeemCode) {
const transactionLink = await dbTransactionLink.findOne({ code: redeemCode })
if (transactionLink) {
dbUser.referrerId = transactionLink.userId
}
}
// TODO this field has no null allowed unlike the loginServer table
// dbUser.pubKey = Buffer.from(randomBytes(32)) // Buffer.alloc(32, 0) default to 0000...
// dbUser.pubkey = keyPair[0]
@ -370,9 +368,11 @@ export class UserResolver {
const emailOptIn = await createEmailOptIn(dbUser.id, queryRunner)
const activationLink = CONFIG.EMAIL_LINK_VERIFICATION.replace(
/{code}/g,
/{optin}/g,
emailOptIn.verificationCode.toString(),
)
).replace(/{code}/g, redeemCode ? '/' + redeemCode : '')
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const emailSent = await sendAccountActivationEmail({
link: activationLink,
firstName,
@ -380,11 +380,14 @@ export class UserResolver {
email,
})
/* uncomment this, when you need the activation link on the console
// In case EMails are disabled log the activation link for the user
if (!emailSent) {
// eslint-disable-next-line no-console
console.log(`Account confirmation link: ${activationLink}`)
}
*/
await queryRunner.commitTransaction()
} catch (e) {
await queryRunner.rollbackTransaction()
@ -392,7 +395,7 @@ export class UserResolver {
} finally {
await queryRunner.release()
}
return 'success'
return new User(dbUser)
}
// THis is used by the admin only - should we move it to the admin resolver?
@ -410,10 +413,11 @@ export class UserResolver {
const emailOptIn = await createEmailOptIn(user.id, queryRunner)
const activationLink = CONFIG.EMAIL_LINK_VERIFICATION.replace(
/{code}/g,
/{optin}/g,
emailOptIn.verificationCode.toString(),
)
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const emailSent = await sendAccountActivationEmail({
link: activationLink,
firstName: user.firstName,
@ -421,11 +425,13 @@ export class UserResolver {
email,
})
/* uncomment this, when you need the activation link on the console
// In case EMails are disabled log the activation link for the user
if (!emailSent) {
// eslint-disable-next-line no-console
console.log(`Account confirmation link: ${activationLink}`)
}
*/
await queryRunner.commitTransaction()
} catch (e) {
await queryRunner.rollbackTransaction()
@ -446,10 +452,11 @@ export class UserResolver {
const optInCode = await getOptInCode(user.id)
const link = CONFIG.EMAIL_LINK_SETPASSWORD.replace(
/{code}/g,
/{optin}/g,
optInCode.verificationCode.toString(),
)
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const emailSent = await sendResetPasswordEmail({
link,
firstName: user.firstName,
@ -457,11 +464,13 @@ export class UserResolver {
email,
})
/* uncomment this, when you need the activation link on the console
// In case EMails are disabled log the activation link for the user
if (!emailSent) {
// eslint-disable-next-line no-console
console.log(`Reset password link: ${link}`)
}
*/
return true
}
@ -484,10 +493,9 @@ export class UserResolver {
throw new Error('Could not login with emailVerificationCode')
})
// Code is only valid for 10minutes
const timeElapsed = Date.now() - new Date(optInCode.updatedAt).getTime()
if (timeElapsed > 10 * 60 * 1000) {
throw new Error('Code is older than 10 minutes')
// Code is only valid for `CONFIG.EMAIL_CODE_VALID_TIME` minutes
if (!isOptInCodeValid(optInCode)) {
throw new Error(`email already more than $(CONFIG.EMAIL_CODE_VALID_TIME} minutes ago`)
}
// load user
@ -551,13 +559,26 @@ export class UserResolver {
} catch {
// TODO is this a problem?
// eslint-disable-next-line no-console
/* uncomment this, when you need the activation link on the console
console.log('Could not subscribe to klicktipp')
*/
}
}
return true
}
@Authorized([RIGHTS.QUERY_OPT_IN])
@Query(() => Boolean)
async queryOptIn(@Arg('optIn') optIn: string): Promise<boolean> {
const optInCode = await LoginEmailOptIn.findOneOrFail({ verificationCode: optIn })
// Code is only valid for `CONFIG.EMAIL_CODE_VALID_TIME` minutes
if (!isOptInCodeValid(optInCode)) {
throw new Error(`email was sent more than $(CONFIG.EMAIL_CODE_VALID_TIME} minutes ago`)
}
return true
}
@Authorized([RIGHTS.UPDATE_USER_INFOS])
@Mutation(() => Boolean)
async updateUserInfos(
@ -573,8 +594,7 @@ export class UserResolver {
}: UpdateUserInfosArgs,
@Ctx() context: any,
): Promise<boolean> {
const userRepository = getCustomRepository(UserRepository)
const userEntity = await userRepository.findByPubkeyHex(context.pubKey)
const userEntity = context.user
if (firstName) {
userEntity.firstName = firstName
@ -652,8 +672,7 @@ export class UserResolver {
@Authorized([RIGHTS.HAS_ELOPAGE])
@Query(() => Boolean)
async hasElopage(@Ctx() context: any): Promise<boolean> {
const userRepository = getCustomRepository(UserRepository)
const userEntity = await userRepository.findByPubkeyHex(context.pubKey).catch()
const userEntity = context.user
if (!userEntity) {
return false
}
@ -661,3 +680,7 @@ export class UserResolver {
return hasElopageBuys(userEntity.email)
}
}
function isOptInCodeValid(optInCode: LoginEmailOptIn) {
const timeElapsed = Date.now() - new Date(optInCode.updatedAt).getTime()
return timeElapsed <= CONFIG.EMAIL_CODE_VALID_TIME * 60 * 1000
}

View File

@ -1,6 +1,6 @@
import { sendEMail } from './sendEMail'
import { createTransport } from 'nodemailer'
import CONFIG from '../config'
import CONFIG from '@/config'
CONFIG.EMAIL = false
CONFIG.EMAIL_SMTP_URL = 'EMAIL_SMTP_URL'

View File

@ -1,6 +1,6 @@
import { createTransport } from 'nodemailer'
import CONFIG from '../config'
import CONFIG from '@/config'
export const sendEMail = async (emailDef: {
to: string

View File

@ -17,6 +17,7 @@ describe('sendTransactionReceivedEmail', () => {
recipientFirstName: 'Peter',
recipientLastName: 'Lustig',
email: 'peter@lustig.de',
senderEmail: 'bibi@bloxberg.de',
amount: new Decimal(42.0),
memo: 'Vielen herzlichen Dank für den neuen Hexenbesen!',
})
@ -30,6 +31,7 @@ describe('sendTransactionReceivedEmail', () => {
expect.stringContaining('Hallo Peter Lustig') &&
expect.stringContaining('42,00 GDD') &&
expect.stringContaining('Bibi Bloxberg') &&
expect.stringContaining('(bibi@bloxberg.de)') &&
expect.stringContaining('Vielen herzlichen Dank für den neuen Hexenbesen!'),
})
})

View File

@ -8,6 +8,7 @@ export const sendTransactionReceivedEmail = (data: {
recipientFirstName: string
recipientLastName: string
email: string
senderEmail: string
amount: Decimal
memo: string
}): Promise<boolean> => {

View File

@ -9,6 +9,7 @@ export const transactionReceived = {
recipientFirstName: string
recipientLastName: string
email: string
senderEmail: string
amount: Decimal
memo: string
}): string =>
@ -16,7 +17,7 @@ export const transactionReceived = {
Du hast soeben ${data.amount.toFixed(2).replace('.', ',')} GDD von ${data.senderFirstName} ${
data.senderLastName
} erhalten.
} (${data.senderEmail}) erhalten.
${data.senderFirstName} ${data.senderLastName} schreibt:
${data.memo}

View File

@ -1,7 +1,7 @@
import { MiddlewareFn } from 'type-graphql'
import { /* klicktippSignIn, */ getKlickTippUser } from '../apis/KlicktippController'
import { KlickTipp } from '../graphql/model/KlickTipp'
import CONFIG from '../config/index'
import { /* klicktippSignIn, */ getKlickTippUser } from '@/apis/KlicktippController'
import { KlickTipp } from '@model/KlickTipp'
import CONFIG from '@/config'
// export const klicktippRegistrationMiddleware: MiddlewareFn = async (
// // Only for demo

View File

@ -0,0 +1,7 @@
export interface CreationInterface {
email: string
amount: number
memo: string
creationDate: string
confirmed?: boolean
}

View File

@ -0,0 +1,29 @@
import { CreationInterface } from './CreationInterface'
const lastMonth = (date: Date): string => {
return new Date(date.getFullYear(), date.getMonth() - 1, 1).toISOString()
}
export const creations: CreationInterface[] = [
{
email: 'bibi@bloxberg.de',
amount: 1000,
memo: 'Herzlich Willkommen bei Gradido!',
creationDate: lastMonth(new Date()),
confirmed: true,
},
{
email: 'bob@baumeister.de',
amount: 1000,
memo: 'Herzlich Willkommen bei Gradido!',
creationDate: lastMonth(new Date()),
confirmed: true,
},
{
email: 'raeuber@hotzenplotz.de',
amount: 1000,
memo: 'Herzlich Willkommen bei Gradido!',
creationDate: lastMonth(new Date()),
confirmed: true,
},
]

View File

@ -0,0 +1,38 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import { createPendingCreation, confirmPendingCreation } from '@/seeds/graphql/mutations'
import { login } from '@/seeds/graphql/queries'
import { CreationInterface } from '@/seeds/creation/CreationInterface'
import { ApolloServerTestClient } from 'apollo-server-testing'
import { User } from '@entity/User'
import { AdminPendingCreation } from '@entity/AdminPendingCreation'
// import CONFIG from '@/config/index'
export const creationFactory = async (
client: ApolloServerTestClient,
creation: CreationInterface,
): Promise<void> => {
const { mutate, query } = client
// login as Peter Lustig (admin) and get his user ID
const {
data: {
login: { id },
},
} = await query({ query: login, variables: { email: 'peter@lustig.de', password: 'Aa12345_' } })
await mutate({ mutation: createPendingCreation, variables: { ...creation, moderator: id } })
// get User
const user = await User.findOneOrFail({ where: { email: creation.email } })
if (creation.confirmed) {
const pendingCreation = await AdminPendingCreation.findOneOrFail({
where: { userId: user.id },
order: { created: 'DESC' },
})
await mutate({ mutation: confirmPendingCreation, variables: { id: pendingCreation.id } })
}
}

View File

@ -0,0 +1,43 @@
import { ApolloServerTestClient } from 'apollo-server-testing'
import { createTransactionLink } from '@/seeds/graphql/mutations'
import { login } from '@/seeds/graphql/queries'
import { TransactionLinkInterface } from '@/seeds/transactionLink/TransactionLinkInterface'
import { transactionLinkExpireDate } from '@/graphql/resolver/TransactionLinkResolver'
import { TransactionLink } from '@entity/TransactionLink'
export const transactionLinkFactory = async (
client: ApolloServerTestClient,
transactionLink: TransactionLinkInterface,
): Promise<void> => {
const { mutate, query } = client
// login
await query({ query: login, variables: { email: transactionLink.email, password: 'Aa12345_' } })
const variables = {
amount: transactionLink.amount,
memo: transactionLink.memo,
}
// get the transaction links's id
const {
data: {
createTransactionLink: { id },
},
} = await mutate({ mutation: createTransactionLink, variables })
if (transactionLink.createdAt || transactionLink.deletedAt) {
const dbTransactionLink = await TransactionLink.findOneOrFail({ id })
if (transactionLink.createdAt) {
dbTransactionLink.createdAt = transactionLink.createdAt
dbTransactionLink.validUntil = transactionLinkExpireDate(transactionLink.createdAt)
await dbTransactionLink.save()
}
if (transactionLink.deletedAt) {
dbTransactionLink.deletedAt = new Date()
await dbTransactionLink.save()
}
}
}

View File

@ -0,0 +1,51 @@
import { createUser, setPassword } from '@/seeds/graphql/mutations'
import { User } from '@entity/User'
import { LoginEmailOptIn } from '@entity/LoginEmailOptIn'
import { ServerUser } from '@entity/ServerUser'
import { UserInterface } from '@/seeds/users/UserInterface'
import { ApolloServerTestClient } from 'apollo-server-testing'
export const userFactory = async (
client: ApolloServerTestClient,
user: UserInterface,
): Promise<void> => {
const { mutate } = client
const {
data: {
createUser: { id },
},
} = await mutate({ mutation: createUser, variables: user })
if (user.emailChecked) {
const optin = await LoginEmailOptIn.findOneOrFail({ userId: id })
await mutate({
mutation: setPassword,
variables: { password: 'Aa12345_', code: optin.verificationCode },
})
}
if (user.createdAt || user.deletedAt || user.isAdmin) {
// get user from database
const dbUser = await User.findOneOrFail({ id })
if (user.createdAt || user.deletedAt) {
if (user.createdAt) dbUser.createdAt = user.createdAt
if (user.deletedAt) dbUser.deletedAt = user.deletedAt
await dbUser.save()
}
if (user.isAdmin) {
const admin = new ServerUser()
admin.username = dbUser.firstName
admin.password = 'please_refactor'
admin.email = dbUser.email
admin.role = 'admin'
admin.activated = 1
admin.lastLogin = new Date()
admin.created = dbUser.createdAt
admin.modified = dbUser.createdAt
await admin.save()
}
}
}

View File

@ -0,0 +1,9 @@
export const GdtEntryType = {
FORM: 'FORM',
CVS: 'CVS',
ELOPAGE: 'ELOPAGE',
ELOPAGE_PUBLISHER: 'ELOPAGE_PUBLISHER',
DIGISTORE: 'DIGISTORE',
CVS2: 'CVS2',
GLOBAL_MODIFICATOR: 'GLOBAL_MODIFICATOR',
}

View File

@ -0,0 +1,102 @@
import gql from 'graphql-tag'
export const subscribeNewsletter = gql`
mutation ($email: String!, $language: String!) {
subscribeNewsletter(email: $email, language: $language)
}
`
export const unsubscribeNewsletter = gql`
mutation ($email: String!) {
unsubscribeNewsletter(email: $email)
}
`
export const setPassword = gql`
mutation ($code: String!, $password: String!) {
setPassword(code: $code, password: $password)
}
`
export const updateUserInfos = gql`
mutation (
$firstName: String
$lastName: String
$password: String
$passwordNew: String
$locale: String
$coinanimation: Boolean
) {
updateUserInfos(
firstName: $firstName
lastName: $lastName
password: $password
passwordNew: $passwordNew
language: $locale
coinanimation: $coinanimation
)
}
`
export const createUser = gql`
mutation (
$firstName: String!
$lastName: String!
$email: String!
$language: String!
$publisherId: Int
$redeemCode: String
) {
createUser(
email: $email
firstName: $firstName
lastName: $lastName
language: $language
publisherId: $publisherId
redeemCode: $redeemCode
) {
id
}
}
`
export const sendCoins = gql`
mutation ($email: String!, $amount: Decimal!, $memo: String!) {
sendCoins(email: $email, amount: $amount, memo: $memo)
}
`
export const createTransactionLink = gql`
mutation ($amount: Decimal!, $memo: String!) {
createTransactionLink(amount: $amount, memo: $memo) {
id
code
}
}
`
// from admin interface
export const createPendingCreation = gql`
mutation (
$email: String!
$amount: Float!
$memo: String!
$creationDate: String!
$moderator: Int!
) {
createPendingCreation(
email: $email
amount: $amount
memo: $memo
creationDate: $creationDate
moderator: $moderator
)
}
`
export const confirmPendingCreation = gql`
mutation ($id: Int!) {
confirmPendingCreation(id: $id)
}
`

View File

@ -0,0 +1,145 @@
import gql from 'graphql-tag'
export const login = gql`
query ($email: String!, $password: String!, $publisherId: Int) {
login(email: $email, password: $password, publisherId: $publisherId) {
id
email
firstName
lastName
language
coinanimation
klickTipp {
newsletterState
}
hasElopage
publisherId
isAdmin
}
}
`
export const verifyLogin = gql`
query {
verifyLogin {
email
firstName
lastName
language
coinanimation
klickTipp {
newsletterState
}
hasElopage
publisherId
isAdmin
}
}
`
export const logout = gql`
query {
logout
}
`
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
decayStartBlock
transactions {
id
typeId
amount
balance
balanceDate
memo
linkedUser {
firstName
lastName
}
decay {
decay
start
end
duration
}
}
}
}
`
export const sendResetPasswordEmail = gql`
query ($email: String!) {
sendResetPasswordEmail(email: $email)
}
`
export const listGDTEntriesQuery = gql`
query ($currentPage: Int!, $pageSize: Int!) {
listGDTEntries(currentPage: $currentPage, pageSize: $pageSize) {
count
gdtEntries {
id
amount
date
comment
gdtEntryType
factor
gdt
}
gdtSum
}
}
`
export const communityInfo = gql`
query {
getCommunityInfo {
name
description
registerUrl
url
}
}
`
export const communities = gql`
query {
communities {
id
name
url
description
registerUrl
}
}
`
export const queryTransactionLink = gql`
query ($code: String!) {
queryTransactionLink(code: $code) {
amount
memo
createdAt
validUntil
user {
firstName
publisherId
}
}
}
`

View File

@ -0,0 +1,77 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import createServer from '../server/createServer'
import { createTestClient } from 'apollo-server-testing'
import { name, internet, random } from 'faker'
import { users } from './users/index'
import { creations } from './creation/index'
import { transactionLinks } from './transactionLink/index'
import { userFactory } from './factory/user'
import { creationFactory } from './factory/creation'
import { transactionLinkFactory } from './factory/transactionLink'
import { entities } from '@entity/index'
const context = {
token: '',
setHeaders: {
push: (value: { key: string; value: string }): void => {
context.token = value.value
},
// eslint-disable-next-line @typescript-eslint/no-empty-function
forEach: (): void => {},
},
}
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])
}
}
const resetEntity = async (entity: any) => {
const items = await entity.find({ withDeleted: true })
if (items.length > 0) {
const ids = items.map((i: any) => i.id)
await entity.delete(ids)
}
}
const run = async () => {
const server = await createServer(context)
const seedClient = createTestClient(server.apollo)
const { con } = server
await cleanDB()
// seed the standard users
for (let i = 0; i < users.length; i++) {
await userFactory(seedClient, users[i])
}
// seed 100 random users
for (let i = 0; i < 100; i++) {
await userFactory(seedClient, {
firstName: name.firstName(),
lastName: name.lastName(),
email: internet.email(),
language: random.boolean() ? 'en' : 'de',
})
}
// create GDD
for (let i = 0; i < creations.length; i++) {
await creationFactory(seedClient, creations[i])
}
// create Transaction Links
for (let i = 0; i < transactionLinks.length; i++) {
await transactionLinkFactory(seedClient, transactionLinks[i])
}
await con.close()
}
run()

View File

@ -0,0 +1,7 @@
export interface TransactionLinkInterface {
email: string
amount: number
memo: string
createdAt?: Date
deletedAt?: boolean
}

View File

@ -0,0 +1,52 @@
import { TransactionLinkInterface } from './TransactionLinkInterface'
export const transactionLinks: TransactionLinkInterface[] = [
{
email: 'bibi@bloxberg.de',
amount: 19.99,
memo: 'Leider wollte niemand meine Gradidos zum Neujahr haben :(',
createdAt: new Date(2022, 0, 1),
},
{
email: 'bibi@bloxberg.de',
amount: 19.99,
memo: `Kein Trick, keine Zauberrei,
bei Gradidio sei dabei!`,
},
{
email: 'bibi@bloxberg.de',
amount: 19.99,
memo: `Kein Trick, keine Zauberrei,
bei Gradidio sei dabei!`,
},
{
email: 'bibi@bloxberg.de',
amount: 19.99,
memo: `Kein Trick, keine Zauberrei,
bei Gradidio sei dabei!`,
},
{
email: 'bibi@bloxberg.de',
amount: 19.99,
memo: `Kein Trick, keine Zauberrei,
bei Gradidio sei dabei!`,
},
{
email: 'bibi@bloxberg.de',
amount: 19.99,
memo: `Kein Trick, keine Zauberrei,
bei Gradidio sei dabei!`,
},
{
email: 'bibi@bloxberg.de',
amount: 19.99,
memo: `Kein Trick, keine Zauberrei,
bei Gradidio sei dabei!`,
},
{
email: 'bibi@bloxberg.de',
amount: 19.99,
memo: 'Da habe ich mich wohl etwas übernommen.',
deletedAt: true,
},
]

View File

@ -0,0 +1,12 @@
export interface UserInterface {
email?: string
firstName?: string
lastName?: string
// description?: string
createdAt?: Date
emailChecked?: boolean
language?: string
deletedAt?: Date
publisherId?: number
isAdmin?: boolean
}

View File

@ -0,0 +1,11 @@
import { UserInterface } from './UserInterface'
export const bibiBloxberg: UserInterface = {
email: 'bibi@bloxberg.de',
firstName: 'Bibi',
lastName: 'Bloxberg',
// description: 'Hex Hex',
emailChecked: true,
language: 'de',
publisherId: 1234,
}

View File

@ -0,0 +1,10 @@
import { UserInterface } from './UserInterface'
export const bobBaumeister: UserInterface = {
email: 'bob@baumeister.de',
firstName: 'Bob',
lastName: 'der Baumeister',
// description: 'Können wir das schaffen? Ja, wir schaffen das!',
emailChecked: true,
language: 'de',
}

View File

@ -0,0 +1,12 @@
import { UserInterface } from './UserInterface'
export const garrickOllivander: UserInterface = {
email: 'garrick@ollivander.com',
firstName: 'Garrick',
lastName: 'Ollivander',
// description: `Curious ... curious ...
// Renowned wandmaker Mr Ollivander owns the wand shop Ollivanders: Makers of Fine Wands Since 382 BC in Diagon Alley. His shop is widely considered the best place to purchase a wand.`,
createdAt: new Date('2022-01-10T10:23:17'),
emailChecked: false,
language: 'en',
}

View File

@ -0,0 +1,15 @@
import { peterLustig } from './peter-lustig'
import { bibiBloxberg } from './bibi-bloxberg'
import { bobBaumeister } from './bob-baumeister'
import { raeuberHotzenplotz } from './raeuber-hotzenplotz'
import { stephenHawking } from './stephen-hawking'
import { garrickOllivander } from './garrick-ollivander'
export const users = [
peterLustig,
bibiBloxberg,
bobBaumeister,
raeuberHotzenplotz,
stephenHawking,
garrickOllivander,
]

View File

@ -0,0 +1,12 @@
import { UserInterface } from './UserInterface'
export const peterLustig: UserInterface = {
email: 'peter@lustig.de',
firstName: 'Peter',
lastName: 'Lustig',
// description: 'Latzhose und Nickelbrille',
createdAt: new Date('2020-11-25T10:48:43'),
emailChecked: true,
language: 'de',
isAdmin: true,
}

View File

@ -0,0 +1,10 @@
import { UserInterface } from './UserInterface'
export const raeuberHotzenplotz: UserInterface = {
email: 'raeuber@hotzenplotz.de',
firstName: 'Räuber',
lastName: 'Hotzenplotz',
// description: 'Pfefferpistole',
emailChecked: true,
language: 'de',
}

View File

@ -0,0 +1,12 @@
import { UserInterface } from './UserInterface'
export const stephenHawking: UserInterface = {
email: 'stephen@hawking.uk',
firstName: 'Stephen',
lastName: 'Hawking',
// description: 'A Brief History of Time',
emailChecked: true,
createdAt: new Date('1942-01-08T09:17:52'),
deletedAt: new Date('2018-03-14T09:17:52'),
language: 'en',
}

View File

@ -1,12 +1,11 @@
import 'reflect-metadata'
import 'module-alias/register'
import { ApolloServer } from 'apollo-server-express'
import express, { Express } from 'express'
// database
import connection from '../typeorm/connection'
import { checkDBVersion } from '../typeorm/DBVersion'
import connection from '@/typeorm/connection'
import { checkDBVersion } from '@/typeorm/DBVersion'
// server
import cors from './cors'
@ -14,13 +13,13 @@ import serverContext from './context'
import plugins from './plugins'
// config
import CONFIG from '../config'
import CONFIG from '@/config'
// graphql
import schema from '../graphql/schema'
import schema from '@/graphql/schema'
// webhooks
import { elopageWebhook } from '../webhook/elopage'
import { elopageWebhook } from '@/webhook/elopage'
import { Connection } from '@dbTools/typeorm'
// TODO implement

View File

@ -1,7 +1,7 @@
// TODO This is super weird - since the entities are defined in another project they have their own globals.
// We cannot use our connection here, but must use the external typeorm installation
import { Connection, createConnection, FileLogger } from '@dbTools/typeorm'
import CONFIG from '../config'
import CONFIG from '@/config'
import { entities } from '@entity/index'
const connection = async (): Promise<Connection | null> => {

View File

@ -1,7 +1,7 @@
import { EntityRepository, Repository } from '@dbTools/typeorm'
import { Transaction } from '@entity/Transaction'
import { Order } from '../../graphql/enum/Order'
import { TransactionTypeId } from '../../graphql/enum/TransactionTypeId'
import { Order } from '@enum/Order'
import { TransactionTypeId } from '@enum/TransactionTypeId'
@EntityRepository(Transaction)
export class TransactionRepository extends Repository<Transaction> {

View File

@ -0,0 +1,39 @@
import { Repository, EntityRepository } from '@dbTools/typeorm'
import { TransactionLink as dbTransactionLink } from '@entity/TransactionLink'
import Decimal from 'decimal.js-light'
@EntityRepository(dbTransactionLink)
export class TransactionLinkRepository extends Repository<dbTransactionLink> {
async summary(
userId: number,
date: Date,
): Promise<{
sumHoldAvailableAmount: Decimal
sumAmount: Decimal
lastDate: Date | null
firstDate: Date | null
transactionLinkcount: number
}> {
const { sumHoldAvailableAmount, sumAmount, lastDate, firstDate, count } =
await this.createQueryBuilder('transactionLinks')
.select('SUM(transactionLinks.holdAvailableAmount)', 'sumHoldAvailableAmount')
.addSelect('SUM(transactionLinks.amount)', 'sumAmount')
.addSelect('MAX(transactionLinks.validUntil)', 'lastDate')
.addSelect('MIN(transactionLinks.createdAt)', 'firstDate')
.addSelect('COUNT(*)', 'count')
.where('transactionLinks.userId = :userId', { userId })
.andWhere('transactionLinks.redeemedAt is NULL')
.andWhere('transactionLinks.validUntil > :date', { date })
.orderBy('transactionLinks.createdAt', 'DESC')
.getRawOne()
return {
sumHoldAvailableAmount: sumHoldAvailableAmount
? new Decimal(sumHoldAvailableAmount)
: new Decimal(0),
sumAmount: sumAmount ? new Decimal(sumAmount) : new Decimal(0),
lastDate: lastDate || null,
firstDate: firstDate || null,
transactionLinkcount: count || 0,
}
}
}

View File

@ -1,7 +1,7 @@
import { EntityRepository, Repository } from '@dbTools/typeorm'
import { UserSetting } from '@entity/UserSetting'
import { Setting } from '../../graphql/enum/Setting'
import { isStringBoolean } from '../../util/validate'
import { Setting } from '@enum/Setting'
import { isStringBoolean } from '@/util/validate'
@EntityRepository(UserSetting)
export class UserSettingRepository extends Repository<UserSetting> {

View File

@ -2,7 +2,7 @@
import { SaveOptions, RemoveOptions } from '@dbTools/typeorm'
import { User as dbUser } from '@entity/User'
import { User } from '../graphql/model/User'
import { User } from '@model/User'
const communityDbUser: dbUser = {
id: -1,

View File

@ -1,6 +1,6 @@
import Decimal from 'decimal.js-light'
import CONFIG from '../config'
import { Decay } from '../graphql/model/Decay'
import CONFIG from '@/config'
import { Decay } from '@model/Decay'
// TODO: externalize all those definitions and functions into an external decay library

View File

@ -1,7 +1,9 @@
import { calculateDecay } from './decay'
import Decimal from 'decimal.js-light'
import { Transaction } from '@entity/Transaction'
import { Decay } from '../graphql/model/Decay'
import { Decay } from '@model/Decay'
import { getCustomRepository } from '@dbTools/typeorm'
import { TransactionLinkRepository } from '@repository/TransactionLink'
function isStringBoolean(value: string): boolean {
const lowerValue = value.toLowerCase()
@ -24,9 +26,13 @@ async function calculateBalance(
if (!lastTransaction) return null
const decay = calculateDecay(lastTransaction.balance, lastTransaction.balanceDate, time)
// TODO why we have to use toString() here?
const balance = decay.balance.add(amount.toString())
if (balance.lessThan(0)) {
const transactionLinkRepository = getCustomRepository(TransactionLinkRepository)
const { sumHoldAvailableAmount } = await transactionLinkRepository.summary(userId, time)
if (balance.minus(sumHoldAvailableAmount.toString()).lessThan(0)) {
return null
}
return { balance, lastTransactionId: lastTransaction.id, decay }

View File

@ -1,52 +0,0 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import Decimal from 'decimal.js-light'
import { SaveOptions, RemoveOptions } from '@dbTools/typeorm'
import { Transaction as dbTransaction } from '@entity/Transaction'
import { calculateDecay } from './decay'
import { TransactionTypeId } from '../graphql/enum/TransactionTypeId'
import { Transaction } from '../graphql/model/Transaction'
import { User } from '../graphql/model/User'
const virtualDecayTransaction = (
balance: Decimal,
balanceDate: Date,
time: Date = new Date(),
user: User,
): Transaction => {
const decay = calculateDecay(balance, balanceDate, time)
// const balance = decay.balance.minus(lastTransaction.balance)
const decayDbTransaction: dbTransaction = {
id: -1,
userId: -1,
previous: -1,
typeId: TransactionTypeId.DECAY,
amount: decay.decay ? decay.decay : new Decimal(0), // new Decimal(0), // this kinda is wrong, but helps with the frontend query
balance: decay.balance,
balanceDate: time,
decay: decay.decay ? decay.decay : new Decimal(0),
decayStart: decay.start,
memo: '',
creationDate: null,
hasId: function (): boolean {
throw new Error('Function not implemented.')
},
save: function (options?: SaveOptions): Promise<dbTransaction> {
throw new Error('Function not implemented.')
},
remove: function (options?: RemoveOptions): Promise<dbTransaction> {
throw new Error('Function not implemented.')
},
softRemove: function (options?: SaveOptions): Promise<dbTransaction> {
throw new Error('Function not implemented.')
},
recover: function (options?: SaveOptions): Promise<dbTransaction> {
throw new Error('Function not implemented.')
},
reload: function (): Promise<void> {
throw new Error('Function not implemented.')
},
}
return new Transaction(decayDbTransaction, user)
}
export { virtualDecayTransaction }

View File

@ -0,0 +1,82 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import { Transaction } from '@model/Transaction'
import { SaveOptions, RemoveOptions } from '@dbTools/typeorm'
import { Transaction as dbTransaction } from '@entity/Transaction'
import { TransactionTypeId } from '@enum/TransactionTypeId'
import { calculateDecay } from './decay'
import { User } from '@model/User'
import Decimal from 'decimal.js-light'
const defaultModelFunctions = {
hasId: function (): boolean {
throw new Error('Function not implemented.')
},
save: function (options?: SaveOptions): Promise<dbTransaction> {
throw new Error('Function not implemented.')
},
remove: function (options?: RemoveOptions): Promise<dbTransaction> {
throw new Error('Function not implemented.')
},
softRemove: function (options?: SaveOptions): Promise<dbTransaction> {
throw new Error('Function not implemented.')
},
recover: function (options?: SaveOptions): Promise<dbTransaction> {
throw new Error('Function not implemented.')
},
reload: function (): Promise<void> {
throw new Error('Function not implemented.')
},
}
const virtualLinkTransaction = (
balance: Decimal,
amount: Decimal,
holdAvailableAmount: Decimal,
decay: Decimal,
createdAt: Date,
validUntil: Date,
user: User,
): Transaction => {
const linkDbTransaction: dbTransaction = {
id: -2,
userId: -1,
previous: -1,
typeId: TransactionTypeId.LINK_SUMMARY,
amount: amount,
balance: balance,
balanceDate: validUntil,
decayStart: createdAt,
decay: decay,
memo: '',
creationDate: null,
...defaultModelFunctions,
}
return new Transaction(linkDbTransaction, user)
}
const virtualDecayTransaction = (
balance: Decimal,
balanceDate: Date,
time: Date = new Date(),
user: User,
): Transaction => {
const decay = calculateDecay(balance, balanceDate, time)
// const balance = decay.balance.minus(lastTransaction.balance)
const decayDbTransaction: dbTransaction = {
id: -1,
userId: -1,
previous: -1,
typeId: TransactionTypeId.DECAY,
amount: decay.decay ? decay.decay : new Decimal(0), // new Decimal(0), // this kinda is wrong, but helps with the frontend query
balance: decay.balance,
balanceDate: time,
decay: decay.decay ? decay.decay : new Decimal(0),
decayStart: decay.start,
memo: '',
creationDate: null,
...defaultModelFunctions,
}
return new Transaction(decayDbTransaction, user)
}
export { virtualLinkTransaction, virtualDecayTransaction }

View File

@ -28,7 +28,7 @@
*/
import { LoginElopageBuys } from '@entity/LoginElopageBuys'
import { UserResolver } from '../graphql/resolver/UserResolver'
import { UserResolver } from '@/graphql/resolver/UserResolver'
import { User as dbUser } from '@entity/User'
export const elopageWebhook = async (req: any, res: any): Promise<void> => {

48
backend/test/helpers.ts Normal file
View File

@ -0,0 +1,48 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import { createTestClient } from 'apollo-server-testing'
import createServer from '../src/server/createServer'
import { initialize } from '@dbTools/helpers'
import { entities } from '@entity/index'
export const headerPushMock = jest.fn((t) => {
context.token = t.value
})
const context = {
token: '',
setHeaders: {
push: headerPushMock,
forEach: jest.fn(),
},
}
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])
}
}
export const testEnvironment = async () => {
const server = await createServer(context)
const con = server.con
const testClient = createTestClient(server.apollo)
const mutate = testClient.mutate
const query = testClient.query
await initialize()
return { mutate, query, con }
}
export const resetEntity = async (entity: any) => {
const items = await entity.find({ withDeleted: true })
if (items.length > 0) {
const ids = items.map((i: any) => i.id)
await entity.delete(ids)
}
}
export const resetToken = () => {
context.token = ''
}

View File

@ -4,3 +4,4 @@
// eslint-disable-next-line @typescript-eslint/no-empty-function
console.info = () => {}
jest.setTimeout(1000000)

View File

@ -45,10 +45,17 @@
/* Module Resolution Options */
// "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
// "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
"baseUrl": ".", /* Base directory to resolve non-absolute module names. */
"paths": { /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
"@entity/*": ["../database/entity/*"],
"@dbTools/*": ["../database/src/*"]
"@/*": ["src/*"],
"@arg/*": ["src/graphql/arg/*"],
"@enum/*": ["src/graphql/enum/*"],
"@model/*": ["src/graphql/model/*"],
"@repository/*": ["src/typeorm/repository/*"],
"@test/*": ["test/*"],
/* external */
"@dbTools/*": ["../database/src/*", "../../database/build/src/*"],
"@entity/*": ["../database/entity/*", "../../database/build/entity/*"]
},
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
// "typeRoots": [], /* List of folders to include type definitions from. */
@ -76,7 +83,7 @@
{
"path": "../database/tsconfig.json",
// add 'prepend' if you want to include the referenced project in your output file
// "prepend": true,
// "prepend": true
}
]
}

View File

@ -811,6 +811,11 @@
"@types/qs" "*"
"@types/serve-static" "*"
"@types/faker@^5.5.9":
version "5.5.9"
resolved "https://registry.yarnpkg.com/@types/faker/-/faker-5.5.9.tgz#588ede92186dc557bff8341d294335d50d255f0c"
integrity sha512-uCx6mP3UY5SIO14XlspxsGjgaemrxpssJI0Ol+GfhxtcKpv9pgRZYsS4eeKeHVLje6Qtc8lGszuBI461+gVZBA==
"@types/fs-capacitor@*":
version "2.0.0"
resolved "https://registry.yarnpkg.com/@types/fs-capacitor/-/fs-capacitor-2.0.0.tgz#17113e25817f584f58100fb7a08eed288b81956e"
@ -2510,6 +2515,11 @@ express@^4.17.1:
utils-merge "1.0.1"
vary "~1.1.2"
faker@^5.5.3:
version "5.5.3"
resolved "https://registry.yarnpkg.com/faker/-/faker-5.5.3.tgz#c57974ee484431b25205c2c8dc09fda861e51e0e"
integrity sha512-wLTv2a28wjUyWkbnX7u/ABZBkUkIF2fCd73V6P2oFqEGEktDfzWx4UxrSqtPRw0xPRAcjeAOIiJWqZm3pP4u3g==
fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3:
version "3.1.3"
resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525"
@ -4092,11 +4102,6 @@ minimist@^1.2.0, minimist@^1.2.5:
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602"
integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==
module-alias@^2.2.2:
version "2.2.2"
resolved "https://registry.yarnpkg.com/module-alias/-/module-alias-2.2.2.tgz#151cdcecc24e25739ff0aa6e51e1c5716974c0e0"
integrity sha512-A/78XjoX2EmNvppVWEhM2oGk3x4lLxnkEA4jTbaK97QKSDjkIoOsKQlfylt/d3kKKi596Qy3NP5XrXJ6fZIC9Q==
ms@2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
@ -5232,6 +5237,16 @@ tsconfig-paths@^3.11.0:
minimist "^1.2.0"
strip-bom "^3.0.0"
tsconfig-paths@^3.14.0:
version "3.14.0"
resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.14.0.tgz#4fcc48f9ccea8826c41b9ca093479de7f5018976"
integrity sha512-cg/1jAZoL57R39+wiw4u/SCC6Ic9Q5NqjBOb+9xISedOYurfog9ZNmKJSxAnb2m/5Bq4lE9lhUcau33Ml8DM0g==
dependencies:
"@types/json5" "^0.0.29"
json5 "^1.0.1"
minimist "^1.2.0"
strip-bom "^3.0.0"
tslib@^1.10.0, tslib@^1.8.1, tslib@^1.9.3:
version "1.14.1"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"

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