mirror of
https://github.com/IT4Change/gradido.git
synced 2026-03-01 12:44:43 +00:00
Merge remote-tracking branch 'origin/master' into 1574-Concept_to_introduce_Gradido-ID
This commit is contained in:
commit
59333d905e
73
.github/workflows/test.yml
vendored
73
.github/workflows/test.yml
vendored
@ -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 }}
|
||||
|
||||
##########################################################################
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
node_modules
|
||||
.git
|
||||
.gitignore
|
||||
.gitignore
|
||||
!.eslintignore
|
||||
@ -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
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
CONFIG_VERSION=$ADMIN_CONFIG_VERSION
|
||||
|
||||
GRAPHQL_URI=$GRAPHQL_URI
|
||||
WALLET_AUTH_URL=$WALLET_AUTH_URL
|
||||
WALLET_URL=$WALLET_URL
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
node_modules
|
||||
coverage
|
||||
**/*.min.js
|
||||
dist
|
||||
node_modules/
|
||||
dist/
|
||||
coverage/
|
||||
@ -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
18
admin/.stylelintrc.js
Normal 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"],
|
||||
}
|
||||
]
|
||||
|
||||
};
|
||||
@ -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": [
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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,
|
||||
},
|
||||
}),
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
export const confirmPendingCreation = gql`
|
||||
mutation ($id: Float!) {
|
||||
mutation ($id: Int!) {
|
||||
confirmPendingCreation(id: $id)
|
||||
}
|
||||
`
|
||||
|
||||
22
admin/src/graphql/creationTransactionList.js
Normal file
22
admin/src/graphql/creationTransactionList.js
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
@ -1,7 +1,7 @@
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
export const deletePendingCreation = gql`
|
||||
mutation ($id: Float!) {
|
||||
mutation ($id: Int!) {
|
||||
deletePendingCreation(id: $id)
|
||||
}
|
||||
`
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
export const deleteUser = gql`
|
||||
mutation ($userId: Float!) {
|
||||
mutation ($userId: Int!) {
|
||||
deleteUser(userId: $userId)
|
||||
}
|
||||
`
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
@ -1,7 +1,7 @@
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
export const unDeleteUser = gql`
|
||||
mutation ($userId: Float!) {
|
||||
mutation ($userId: Int!) {
|
||||
unDeleteUser(userId: $userId)
|
||||
}
|
||||
`
|
||||
|
||||
@ -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"
|
||||
},
|
||||
|
||||
@ -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"
|
||||
},
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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'),
|
||||
}
|
||||
|
||||
785
admin/yarn.lock
785
admin/yarn.lock
File diff suppressed because it is too large
Load Diff
@ -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
|
||||
@ -1,3 +1,5 @@
|
||||
CONFIG_VERSION=$BACKEND_CONFIG_VERSION
|
||||
|
||||
# Server
|
||||
JWT_SECRET=$JWT_SECRET
|
||||
JWT_EXPIRES_IN=10m
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@ -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()
|
||||
|
||||
|
||||
@ -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,
|
||||
]
|
||||
|
||||
@ -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 => {
|
||||
|
||||
@ -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',
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -16,4 +16,7 @@ export default class CreateUserArgs {
|
||||
|
||||
@Field(() => Int, { nullable: true })
|
||||
publisherId: number
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
redeemCode?: string | null
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
11
backend/src/graphql/arg/TransactionLinkArgs.ts
Normal file
11
backend/src/graphql/arg/TransactionLinkArgs.ts
Normal 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
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
|
||||
|
||||
@ -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, {
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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
|
||||
}
|
||||
*/
|
||||
@ -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
|
||||
}
|
||||
|
||||
54
backend/src/graphql/model/TransactionLink.ts
Normal file
54
backend/src/graphql/model/TransactionLink.ts
Normal 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
|
||||
}
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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 {
|
||||
|
||||
14
backend/src/graphql/resolver/TransactionLinkResolver.test.ts
Normal file
14
backend/src/graphql/resolver/TransactionLinkResolver.test.ts
Normal 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))
|
||||
})
|
||||
})
|
||||
169
backend/src/graphql/resolver/TransactionLinkResolver.ts
Normal file
169
backend/src/graphql/resolver/TransactionLinkResolver.ts
Normal 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
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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,
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { createTransport } from 'nodemailer'
|
||||
|
||||
import CONFIG from '../config'
|
||||
import CONFIG from '@/config'
|
||||
|
||||
export const sendEMail = async (emailDef: {
|
||||
to: string
|
||||
|
||||
@ -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!'),
|
||||
})
|
||||
})
|
||||
|
||||
@ -8,6 +8,7 @@ export const sendTransactionReceivedEmail = (data: {
|
||||
recipientFirstName: string
|
||||
recipientLastName: string
|
||||
email: string
|
||||
senderEmail: string
|
||||
amount: Decimal
|
||||
memo: string
|
||||
}): Promise<boolean> => {
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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
|
||||
|
||||
7
backend/src/seeds/creation/CreationInterface.ts
Normal file
7
backend/src/seeds/creation/CreationInterface.ts
Normal file
@ -0,0 +1,7 @@
|
||||
export interface CreationInterface {
|
||||
email: string
|
||||
amount: number
|
||||
memo: string
|
||||
creationDate: string
|
||||
confirmed?: boolean
|
||||
}
|
||||
29
backend/src/seeds/creation/index.ts
Normal file
29
backend/src/seeds/creation/index.ts
Normal 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,
|
||||
},
|
||||
]
|
||||
38
backend/src/seeds/factory/creation.ts
Normal file
38
backend/src/seeds/factory/creation.ts
Normal 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 } })
|
||||
}
|
||||
}
|
||||
43
backend/src/seeds/factory/transactionLink.ts
Normal file
43
backend/src/seeds/factory/transactionLink.ts
Normal 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
51
backend/src/seeds/factory/user.ts
Normal file
51
backend/src/seeds/factory/user.ts
Normal 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
9
backend/src/seeds/graphql/enums.ts
Normal file
9
backend/src/seeds/graphql/enums.ts
Normal 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',
|
||||
}
|
||||
102
backend/src/seeds/graphql/mutations.ts
Normal file
102
backend/src/seeds/graphql/mutations.ts
Normal 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)
|
||||
}
|
||||
`
|
||||
145
backend/src/seeds/graphql/queries.ts
Normal file
145
backend/src/seeds/graphql/queries.ts
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
77
backend/src/seeds/index.ts
Normal file
77
backend/src/seeds/index.ts
Normal 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()
|
||||
@ -0,0 +1,7 @@
|
||||
export interface TransactionLinkInterface {
|
||||
email: string
|
||||
amount: number
|
||||
memo: string
|
||||
createdAt?: Date
|
||||
deletedAt?: boolean
|
||||
}
|
||||
52
backend/src/seeds/transactionLink/index.ts
Normal file
52
backend/src/seeds/transactionLink/index.ts
Normal 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,
|
||||
},
|
||||
]
|
||||
12
backend/src/seeds/users/UserInterface.ts
Normal file
12
backend/src/seeds/users/UserInterface.ts
Normal 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
|
||||
}
|
||||
11
backend/src/seeds/users/bibi-bloxberg.ts
Normal file
11
backend/src/seeds/users/bibi-bloxberg.ts
Normal 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,
|
||||
}
|
||||
10
backend/src/seeds/users/bob-baumeister.ts
Normal file
10
backend/src/seeds/users/bob-baumeister.ts
Normal 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',
|
||||
}
|
||||
12
backend/src/seeds/users/garrick-ollivander.ts
Normal file
12
backend/src/seeds/users/garrick-ollivander.ts
Normal 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',
|
||||
}
|
||||
15
backend/src/seeds/users/index.ts
Normal file
15
backend/src/seeds/users/index.ts
Normal 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,
|
||||
]
|
||||
12
backend/src/seeds/users/peter-lustig.ts
Normal file
12
backend/src/seeds/users/peter-lustig.ts
Normal 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,
|
||||
}
|
||||
10
backend/src/seeds/users/raeuber-hotzenplotz.ts
Normal file
10
backend/src/seeds/users/raeuber-hotzenplotz.ts
Normal 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',
|
||||
}
|
||||
12
backend/src/seeds/users/stephen-hawking.ts
Normal file
12
backend/src/seeds/users/stephen-hawking.ts
Normal 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',
|
||||
}
|
||||
@ -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
|
||||
|
||||
@ -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> => {
|
||||
|
||||
@ -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> {
|
||||
|
||||
39
backend/src/typeorm/repository/TransactionLink.ts
Normal file
39
backend/src/typeorm/repository/TransactionLink.ts
Normal 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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> {
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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 }
|
||||
|
||||
@ -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 }
|
||||
82
backend/src/util/virtualTransactions.ts
Normal file
82
backend/src/util/virtualTransactions.ts
Normal 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 }
|
||||
@ -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
48
backend/test/helpers.ts
Normal 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 = ''
|
||||
}
|
||||
@ -4,3 +4,4 @@
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
console.info = () => {}
|
||||
jest.setTimeout(1000000)
|
||||
|
||||
@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@ -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
Loading…
x
Reference in New Issue
Block a user