Merge branch 'master' into 2303-Allow-the-admin-to-edit-automatic-contribution-link

This commit is contained in:
Alexander Friedland 2022-11-08 14:12:56 +01:00 committed by GitHub
commit 065a01c83a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
38 changed files with 1370 additions and 190 deletions

72
.github/workflows/lint_pr.yml vendored Normal file
View File

@ -0,0 +1,72 @@
name: "gradido lint pull request CI"
on:
pull_request:
pull_request_target:
types:
- opened
- edited
- synchronize
jobs:
main:
name: Validate PR title
runs-on: ubuntu-latest
steps:
- uses: amannn/action-semantic-pull-request@v5
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
# Configure which types are allowed (newline delimited).
# Default: https://github.com/commitizen/conventional-commit-types
#types: |
# fix
# feat
# Configure which scopes are allowed (newline delimited).
scopes: |
backend
frontend
admin
database
release
other
# Configure that a scope must always be provided.
requireScope: true
# Configure which scopes (newline delimited) are disallowed in PR
# titles. For instance by setting # the value below, `chore(release):
# ...` and `ci(e2e,release): ...` will be rejected.
#disallowScopes: |
# release
# Configure additional validation for the subject based on a regex.
# This example ensures the subject doesn't start with an uppercase character.
subjectPattern: ^(?![A-Z]).+$
# If `subjectPattern` is configured, you can use this property to override
# the default error message that is shown when the pattern doesn't match.
# The variables `subject` and `title` can be used within the message.
subjectPatternError: |
The subject "{subject}" found in the pull request title "{title}"
didn't match the configured pattern. Please ensure that the subject
doesn't start with an uppercase character.
# If you use GitHub Enterprise, you can set this to the URL of your server
#githubBaseUrl: https://github.myorg.com/api/v3
# If the PR contains one of these labels (newline delimited), the
# validation is skipped.
# If you want to rerun the validation when labels change, you might want
# to use the `labeled` and `unlabeled` event triggers in your workflow.
#ignoreLabels: |
# bot
# ignore-semantic-pull-request
# If you're using a format for the PR title that differs from the traditional Conventional
# Commits spec, you can use these options to customize the parsing of the type, scope and
# subject. The `headerPattern` should contain a regex where the capturing groups in parentheses
# correspond to the parts listed in `headerPatternCorrespondence`.
# See: https://github.com/conventional-changelog/conventional-changelog/tree/master/packages/conventional-commits-parser#headerpattern
headerPattern: '^(\w*)(?:\(([\w$.\-*/ ]*)\))?: (.*)$'
headerPatternCorrespondence: type, scope, subject
# For work-in-progress PRs you can typically use draft pull requests
# from GitHub. However, private repositories on the free plan don't have
# this option and therefore this action allows you to opt-in to using the
# special "[WIP]" prefix to indicate this state. This will avoid the
# validation of the PR title and the pull request checks remain pending.
# Note that a second check will be reported if this is enabled.
wip: true

View File

@ -139,7 +139,7 @@ jobs:
build_test_nginx:
name: Docker Build Test - Nginx
runs-on: ubuntu-latest
#needs: [nothing]
needs: [build_test_backend, build_test_admin, build_test_frontend]
steps:
##########################################################################
# CHECKOUT CODE ##########################################################
@ -560,7 +560,7 @@ jobs:
end-to-end-tests:
name: End-to-End Tests
runs-on: ubuntu-latest
# needs: [build_test_mariadb, build_test_database_up, build_test_backend, build_test_admin, build_test_frontend, build_test_nginx]
needs: [build_test_mariadb, build_test_database_up, build_test_backend, build_test_admin, build_test_frontend, build_test_nginx]
steps:
##########################################################################
# CHECKOUT CODE ##########################################################
@ -570,65 +570,63 @@ jobs:
##########################################################################
# DOWNLOAD DOCKER IMAGES #################################################
##########################################################################
# - name: Download Docker Image (Mariadb)
# uses: actions/download-artifact@v3
# with:
# name: docker-mariadb-test
# path: /tmp
# - name: Load Docker Image (Mariadb)
# run: docker load < /tmp/mariadb.tar
# - name: Download Docker Image (Database Up)
# uses: actions/download-artifact@v3
# with:
# name: docker-database-test_up
# path: /tmp
# - name: Load Docker Image (Database Up)
# run: docker load < /tmp/database_up.tar
# - name: Download Docker Image (Backend)
# uses: actions/download-artifact@v3
# with:
# name: docker-backend-test
# path: /tmp
# - name: Load Docker Image (Backend)
# run: docker load < /tmp/backend.tar
# - name: Download Docker Image (Frontend)
# uses: actions/download-artifact@v3
# with:
# name: docker-frontend-test
# path: /tmp
# - name: Load Docker Image (Frontend)
# run: docker load < /tmp/frontend.tar
# - name: Download Docker Image (Admin Interface)
# uses: actions/download-artifact@v3
# with:
# name: docker-admin-test
# path: /tmp
# - name: Load Docker Image (Admin Interface)
# run: docker load < /tmp/admin.tar
# - name: Download Docker Image (Nginx)
# uses: actions/download-artifact@v3
# with:
# name: docker-nginx-test
# path: /tmp
# - name: Load Docker Image (Nginx)
# run: docker load < /tmp/nginx.tar
- name: Download Docker Image (Mariadb)
uses: actions/download-artifact@v3
with:
name: docker-mariadb-test
path: /tmp
- name: Load Docker Image (Mariadb)
run: docker load < /tmp/mariadb.tar
- name: Download Docker Image (Database Up)
uses: actions/download-artifact@v3
with:
name: docker-database-test_up
path: /tmp
- name: Load Docker Image (Database Up)
run: docker load < /tmp/database_up.tar
- name: Download Docker Image (Backend)
uses: actions/download-artifact@v3
with:
name: docker-backend-test
path: /tmp
- name: Load Docker Image (Backend)
run: docker load < /tmp/backend.tar
- name: Download Docker Image (Frontend)
uses: actions/download-artifact@v3
with:
name: docker-frontend-test
path: /tmp
- name: Load Docker Image (Frontend)
run: docker load < /tmp/frontend.tar
- name: Download Docker Image (Admin Interface)
uses: actions/download-artifact@v3
with:
name: docker-admin-test
path: /tmp
- name: Load Docker Image (Admin Interface)
run: docker load < /tmp/admin.tar
- name: Download Docker Image (Nginx)
uses: actions/download-artifact@v3
with:
name: docker-nginx-test
path: /tmp
- name: Load Docker Image (Nginx)
run: docker load < /tmp/nginx.tar
##########################################################################
# BOOT UP THE TEST SYSTEM ################################################
##########################################################################
- name: Boot up test system | docker-compose mariadb
run: docker-compose up --detach mariadb
- name: Sleep for 30 seconds
run: sleep 30s
run: docker-compose -f docker-compose.yml -f docker-compose.test.yml up --detach mariadb
- name: Boot up test system | docker-compose database
run: docker-compose up --detach --no-deps database
run: docker-compose -f docker-compose.yml -f docker-compose.test.yml up --detach --no-deps database
- name: Boot up test system | docker-compose backend
run: docker-compose up --detach --no-deps backend
run: docker-compose -f docker-compose.yml -f docker-compose.test.yml up --detach --no-deps backend
- name: Sleep for 90 seconds
run: sleep 90s
- name: Sleep for 10 seconds
run: sleep 10s
- name: Boot up test system | seed backend
run: |
@ -640,10 +638,10 @@ jobs:
cd ..
- name: Boot up test system | docker-compose frontends
run: docker-compose up --detach --no-deps frontend admin nginx
run: docker-compose -f docker-compose.yml -f docker-compose.test.yml up --detach --no-deps frontend admin nginx
- name: Sleep for 2.5 minutes
run: sleep 150s
- name: Sleep for 15 seconds
run: sleep 15s
##########################################################################
# END-TO-END TESTS #######################################################

View File

@ -53,6 +53,7 @@
"vuex-persistedstate": "^4.1.0"
},
"devDependencies": {
"@apollo/client": "^3.7.1",
"@babel/eslint-parser": "^7.15.8",
"@intlify/eslint-plugin-vue-i18n": "^1.4.0",
"@vue/cli-plugin-babel": "~4.5.0",
@ -71,6 +72,7 @@
"eslint-plugin-prettier": "3.3.1",
"eslint-plugin-promise": "^5.1.1",
"eslint-plugin-vue": "^7.20.0",
"mock-apollo-client": "^1.2.1",
"postcss": "^8.4.8",
"postcss-html": "^1.3.0",
"postcss-scss": "^4.0.3",

View File

@ -118,12 +118,11 @@ export default {
},
methods: {
updateCreationData(data) {
this.creationUserData = data
// this.creationUserData.amount = data.amount
// this.creationUserData.date = data.date
// this.creationUserData.memo = data.memo
// this.creationUserData.moderator = data.moderator
data.row.toggleDetails()
const row = data.row
this.$emit('update-contributions', data)
delete data.row
this.creationUserData = { ...this.creationUserData, ...data }
row.toggleDetails()
},
updateUserData(rowItem, newCreation) {
rowItem.creation = newCreation

View File

@ -1,42 +1,22 @@
import { mount } from '@vue/test-utils'
import CreationConfirm from './CreationConfirm.vue'
import { adminDeleteContribution } from '../graphql/adminDeleteContribution'
import { listUnconfirmedContributions } from '../graphql/listUnconfirmedContributions'
import { confirmContribution } from '../graphql/confirmContribution'
import { toastErrorSpy, toastSuccessSpy } from '../../test/testSetup'
import VueApollo from 'vue-apollo'
import { createMockClient } from 'mock-apollo-client'
const mockClient = createMockClient()
const apolloProvider = new VueApollo({
defaultClient: mockClient,
})
const localVue = global.localVue
const storeCommitMock = jest.fn()
const apolloQueryMock = jest.fn().mockResolvedValue({
data: {
listUnconfirmedContributions: [
{
id: 1,
firstName: 'Bibi',
lastName: 'Bloxberg',
userId: 99,
email: 'bibi@bloxberg.de',
amount: 500,
memo: 'Danke für alles',
date: new Date(),
moderator: 1,
},
{
id: 2,
firstName: 'Räuber',
lastName: 'Hotzenplotz',
userId: 100,
email: 'raeuber@hotzenplotz.de',
amount: 1000000,
memo: 'Gut Ergattert',
date: new Date(),
moderator: 1,
},
],
},
})
localVue.use(VueApollo)
const apolloMutateMock = jest.fn().mockResolvedValue({})
const storeCommitMock = jest.fn()
const mocks = {
$t: jest.fn((t) => t),
@ -53,17 +33,69 @@ const mocks = {
},
},
},
$apollo: {
query: apolloQueryMock,
mutate: apolloMutateMock,
},
}
const defaultData = () => {
return {
listUnconfirmedContributions: [
{
id: 1,
firstName: 'Bibi',
lastName: 'Bloxberg',
userId: 99,
email: 'bibi@bloxberg.de',
amount: 500,
memo: 'Danke für alles',
date: new Date(),
moderator: 1,
state: 'PENDING',
creation: [500, 500, 500],
messageCount: 0,
},
{
id: 2,
firstName: 'Räuber',
lastName: 'Hotzenplotz',
userId: 100,
email: 'raeuber@hotzenplotz.de',
amount: 1000000,
memo: 'Gut Ergattert',
date: new Date(),
moderator: 1,
state: 'PENDING',
creation: [500, 500, 500],
messageCount: 0,
},
],
}
}
describe('CreationConfirm', () => {
let wrapper
const listUnconfirmedContributionsMock = jest.fn()
const adminDeleteContributionMock = jest.fn()
const confirmContributionMock = jest.fn()
mockClient.setRequestHandler(
listUnconfirmedContributions,
listUnconfirmedContributionsMock
.mockRejectedValueOnce({ message: 'Ouch!' })
.mockResolvedValue({ data: defaultData() }),
)
mockClient.setRequestHandler(
adminDeleteContribution,
adminDeleteContributionMock.mockResolvedValue({ data: { adminDeleteContribution: true } }),
)
mockClient.setRequestHandler(
confirmContribution,
confirmContributionMock.mockResolvedValue({ data: { confirmContribution: true } }),
)
const Wrapper = () => {
return mount(CreationConfirm, { localVue, mocks })
return mount(CreationConfirm, { localVue, mocks, apolloProvider })
}
describe('mount', () => {
@ -72,12 +104,20 @@ describe('CreationConfirm', () => {
wrapper = Wrapper()
})
it('has a DIV element with the class.creation-confirm', () => {
expect(wrapper.find('div.creation-confirm').exists()).toBeTruthy()
describe('server response for get pending creations is error', () => {
it('toast an error message', () => {
expect(toastErrorSpy).toBeCalledWith('Ouch!')
})
})
it('has two pending creations', () => {
expect(wrapper.vm.pendingCreations).toHaveLength(2)
describe('server response is succes', () => {
it('has a DIV element with the class.creation-confirm', () => {
expect(wrapper.find('div.creation-confirm').exists()).toBeTruthy()
})
it('has two pending creations', () => {
expect(wrapper.vm.pendingCreations).toHaveLength(2)
})
})
describe('store', () => {
@ -105,10 +145,7 @@ describe('CreationConfirm', () => {
})
it('calls the adminDeleteContribution mutation', () => {
expect(apolloMutateMock).toBeCalledWith({
mutation: adminDeleteContribution,
variables: { id: 1 },
})
expect(adminDeleteContributionMock).toBeCalledWith({ id: 1 })
})
it('commits openCreationsMinus to store', () => {
@ -128,7 +165,7 @@ describe('CreationConfirm', () => {
})
it('does not call the adminDeleteContribution mutation', () => {
expect(apolloMutateMock).not.toBeCalled()
expect(adminDeleteContributionMock).not.toBeCalled()
})
})
})
@ -139,7 +176,7 @@ describe('CreationConfirm', () => {
beforeEach(async () => {
spy = jest.spyOn(wrapper.vm.$bvModal, 'msgBoxConfirm')
spy.mockImplementation(() => Promise.resolve('some value'))
apolloMutateMock.mockRejectedValue({ message: 'Ouchhh!' })
adminDeleteContributionMock.mockRejectedValue({ message: 'Ouchhh!' })
await wrapper.findAll('tr').at(1).findAll('button').at(0).trigger('click')
})
@ -150,7 +187,6 @@ describe('CreationConfirm', () => {
describe('confirm creation with success', () => {
beforeEach(async () => {
apolloMutateMock.mockResolvedValue({})
await wrapper.findAll('tr').at(2).findAll('button').at(2).trigger('click')
})
@ -179,10 +215,7 @@ describe('CreationConfirm', () => {
})
it('calls the confirmContribution mutation', () => {
expect(apolloMutateMock).toBeCalledWith({
mutation: confirmContribution,
variables: { id: 2 },
})
expect(confirmContributionMock).toBeCalledWith({ id: 2 })
})
it('commits openCreationsMinus to store', () => {
@ -200,7 +233,7 @@ describe('CreationConfirm', () => {
describe('confirm creation with error', () => {
beforeEach(async () => {
apolloMutateMock.mockRejectedValue({ message: 'Ouchhh!' })
confirmContributionMock.mockRejectedValue({ message: 'Ouchhh!' })
await wrapper.find('#overlay').findAll('button').at(1).trigger('click')
})
@ -210,19 +243,5 @@ describe('CreationConfirm', () => {
})
})
})
describe('server response for get pending creations is error', () => {
beforeEach(() => {
jest.clearAllMocks()
apolloQueryMock.mockRejectedValue({
message: 'Ouch!',
})
wrapper = Wrapper()
})
it('toast an error message', () => {
expect(toastErrorSpy).toBeCalledWith('Ouch!')
})
})
})
})

View File

@ -10,6 +10,7 @@
@remove-creation="removeCreation"
@show-overlay="showOverlay"
@update-state="updateState"
@update-contributions="$apollo.queries.PendingContributions.refetch()"
/>
</div>
</template>
@ -71,21 +72,6 @@ export default {
this.toastError(error.message)
})
},
getPendingCreations() {
this.$apollo
.query({
query: listUnconfirmedContributions,
fetchPolicy: 'network-only',
})
.then((result) => {
this.$store.commit('resetOpenCreations')
this.pendingCreations = result.data.listUnconfirmedContributions
this.$store.commit('setOpenCreations', result.data.listUnconfirmedContributions.length)
})
.catch((error) => {
this.toastError(error.message)
})
},
updatePendingCreations(id) {
this.pendingCreations = this.pendingCreations.filter((obj) => obj.id !== id)
this.$store.commit('openCreationsMinus', 1)
@ -127,8 +113,24 @@ export default {
]
},
},
async created() {
await this.getPendingCreations()
apollo: {
PendingContributions: {
query() {
return listUnconfirmedContributions
},
variables() {
// may be at some point we need a pagination here
return {}
},
update({ listUnconfirmedContributions }) {
this.$store.commit('resetOpenCreations')
this.pendingCreations = listUnconfirmedContributions
this.$store.commit('setOpenCreations', listUnconfirmedContributions.length)
},
error({ message }) {
this.toastError(message)
},
},
},
}
</script>

View File

@ -2,6 +2,25 @@
# yarn lockfile v1
"@apollo/client@^3.7.1":
version "3.7.1"
resolved "https://registry.yarnpkg.com/@apollo/client/-/client-3.7.1.tgz#86ce47c18a0714e229231148b0306562550c2248"
integrity sha512-xu5M/l7p9gT9Fx7nF3AQivp0XukjB7TM7tOd5wifIpI8RskYveL4I+rpTijzWrnqCPZabkbzJKH7WEAKdctt9w==
dependencies:
"@graphql-typed-document-node/core" "^3.1.1"
"@wry/context" "^0.7.0"
"@wry/equality" "^0.5.0"
"@wry/trie" "^0.3.0"
graphql-tag "^2.12.6"
hoist-non-react-statics "^3.3.2"
optimism "^0.16.1"
prop-types "^15.7.2"
response-iterator "^0.2.6"
symbol-observable "^4.0.0"
ts-invariant "^0.10.3"
tslib "^2.3.0"
zen-observable-ts "^1.2.5"
"@babel/code-frame@7.12.11":
version "7.12.11"
resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.12.11.tgz#f4ad435aa263db935b8f10f2c552d23fb716a63f"
@ -1030,6 +1049,11 @@
minimatch "^3.0.4"
strip-json-comments "^3.1.1"
"@graphql-typed-document-node/core@^3.1.1":
version "3.1.1"
resolved "https://registry.yarnpkg.com/@graphql-typed-document-node/core/-/core-3.1.1.tgz#076d78ce99822258cf813ecc1e7fa460fa74d052"
integrity sha512-NQ17ii0rK1b34VZonlmT2QMJFI70m0TRwbknO/ihlbatXyaktDhN/98vBiUU6kNBPljqGqyIrl2T4nY2RpFANg==
"@hapi/address@2.x.x":
version "2.1.4"
resolved "https://registry.yarnpkg.com/@hapi/address/-/address-2.1.4.tgz#5d67ed43f3fd41a69d4b9ff7b56e7c0d1d0a81e5"
@ -2419,6 +2443,20 @@
"@types/node" ">=6"
tslib "^1.9.3"
"@wry/context@^0.6.0":
version "0.6.1"
resolved "https://registry.yarnpkg.com/@wry/context/-/context-0.6.1.tgz#c3c29c0ad622adb00f6a53303c4f965ee06ebeb2"
integrity sha512-LOmVnY1iTU2D8tv4Xf6MVMZZ+juIJ87Kt/plMijjN20NMAXGmH4u8bS1t0uT74cZ5gwpocYueV58YwyI8y+GKw==
dependencies:
tslib "^2.3.0"
"@wry/context@^0.7.0":
version "0.7.0"
resolved "https://registry.yarnpkg.com/@wry/context/-/context-0.7.0.tgz#be88e22c0ddf62aeb0ae9f95c3d90932c619a5c8"
integrity sha512-LcDAiYWRtwAoSOArfk7cuYvFXytxfVrdX7yxoUmK7pPITLk5jYh2F8knCwS7LjgYL8u1eidPlKKV6Ikqq0ODqQ==
dependencies:
tslib "^2.3.0"
"@wry/equality@^0.1.2":
version "0.1.11"
resolved "https://registry.yarnpkg.com/@wry/equality/-/equality-0.1.11.tgz#35cb156e4a96695aa81a9ecc4d03787bc17f1790"
@ -2426,6 +2464,20 @@
dependencies:
tslib "^1.9.3"
"@wry/equality@^0.5.0":
version "0.5.3"
resolved "https://registry.yarnpkg.com/@wry/equality/-/equality-0.5.3.tgz#fafebc69561aa2d40340da89fa7dc4b1f6fb7831"
integrity sha512-avR+UXdSrsF2v8vIqIgmeTY0UR91UT+IyablCyKe/uk22uOJ8fusKZnH9JH9e1/EtLeNJBtagNmL3eJdnOV53g==
dependencies:
tslib "^2.3.0"
"@wry/trie@^0.3.0":
version "0.3.2"
resolved "https://registry.yarnpkg.com/@wry/trie/-/trie-0.3.2.tgz#a06f235dc184bd26396ba456711f69f8c35097e6"
integrity sha512-yRTyhWSls2OY/pYLfwff867r8ekooZ4UI+/gxot5Wj8EFwSf2rG+n+Mo/6LoLQm1TKA4GRj2+LCpbfS937dClQ==
dependencies:
tslib "^2.3.0"
"@xtuc/ieee754@^1.2.0":
version "1.2.0"
resolved "https://registry.yarnpkg.com/@xtuc/ieee754/-/ieee754-1.2.0.tgz#eef014a3145ae477a1cbc00cd1e552336dceb790"
@ -6704,6 +6756,13 @@ graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.6
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.8.tgz#e412b8d33f5e006593cbd3cee6df9f2cebbe802a"
integrity sha512-qkIilPUYcNhJpd33n0GBXTB1MMPp14TxEsEs0pTrsSVucApsYzW5V+Q8Qxhik6KU3evy+qkAAowTByymK0avdg==
graphql-tag@^2.12.6:
version "2.12.6"
resolved "https://registry.yarnpkg.com/graphql-tag/-/graphql-tag-2.12.6.tgz#d441a569c1d2537ef10ca3d1633b48725329b5f1"
integrity sha512-FdSNcu2QQcWnM2VNvSCCDCVS5PpPqpzgFT8+GXzqJuoDd0CBncxCY278u4mhRO7tMgo2JjgJA5aZ+nWSQ/Z+xg==
dependencies:
tslib "^2.1.0"
graphql-tag@^2.4.2:
version "2.12.5"
resolved "https://registry.yarnpkg.com/graphql-tag/-/graphql-tag-2.12.5.tgz#5cff974a67b417747d05c8d9f5f3cb4495d0db8f"
@ -6880,6 +6939,13 @@ hmac-drbg@^1.0.1:
minimalistic-assert "^1.0.0"
minimalistic-crypto-utils "^1.0.1"
hoist-non-react-statics@^3.3.2:
version "3.3.2"
resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45"
integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==
dependencies:
react-is "^16.7.0"
homedir-polyfill@^1.0.1:
version "1.0.3"
resolved "https://registry.yarnpkg.com/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz#743298cef4e5af3e194161fbadcc2151d3a058e8"
@ -9174,7 +9240,7 @@ lolex@^5.0.0:
dependencies:
"@sinonjs/commons" "^1.7.0"
loose-envify@^1.0.0:
loose-envify@^1.0.0, loose-envify@^1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf"
integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==
@ -9498,6 +9564,11 @@ mkdirp@0.x, mkdirp@^0.5.1, mkdirp@^0.5.3, mkdirp@^0.5.5, mkdirp@~0.5.1:
dependencies:
minimist "^1.2.5"
mock-apollo-client@^1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/mock-apollo-client/-/mock-apollo-client-1.2.1.tgz#e3bfdc3ff73b1fea28fa7e91ec82e43ba8cbfa39"
integrity sha512-QYQ6Hxo+t7hard1bcHHbsHxlNQYTQsaMNsm2Psh/NbwLMi2R4tGzplJKt97MUWuARHMq3GHB4PTLj/gxej4Caw==
moo-color@^1.0.2:
version "1.0.3"
resolved "https://registry.yarnpkg.com/moo-color/-/moo-color-1.0.3.tgz#d56435f8359c8284d83ac58016df7427febece74"
@ -9987,6 +10058,14 @@ optimism@^0.10.0:
dependencies:
"@wry/context" "^0.4.0"
optimism@^0.16.1:
version "0.16.1"
resolved "https://registry.yarnpkg.com/optimism/-/optimism-0.16.1.tgz#7c8efc1f3179f18307b887e18c15c5b7133f6e7d"
integrity sha512-64i+Uw3otrndfq5kaoGNoY7pvOhSsjFEN4bdEFh80MWVk/dbgJfMv7VFDeCT8LxNAlEVhQmdVEbfE7X2nWNIIg==
dependencies:
"@wry/context" "^0.6.0"
"@wry/trie" "^0.3.0"
optionator@^0.8.1:
version "0.8.3"
resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.3.tgz#84fa1d036fe9d3c7e21d99884b601167ec8fb495"
@ -10901,6 +10980,15 @@ prompts@^2.0.1:
kleur "^3.0.3"
sisteransi "^1.0.5"
prop-types@^15.7.2:
version "15.8.1"
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5"
integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==
dependencies:
loose-envify "^1.4.0"
object-assign "^4.1.1"
react-is "^16.13.1"
proto-list@~1.2.1:
version "1.2.4"
resolved "https://registry.yarnpkg.com/proto-list/-/proto-list-1.2.4.tgz#212d5bfe1318306a420f6402b8e26ff39647a849"
@ -11080,7 +11168,7 @@ raw-body@2.4.0:
iconv-lite "0.4.24"
unpipe "1.0.0"
react-is@^16.8.4:
react-is@^16.13.1, react-is@^16.7.0, react-is@^16.8.4:
version "16.13.1"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
@ -11423,6 +11511,11 @@ resolve@1.x, resolve@^1.10.0, resolve@^1.10.1, resolve@^1.12.0, resolve@^1.14.2,
is-core-module "^2.2.0"
path-parse "^1.0.6"
response-iterator@^0.2.6:
version "0.2.6"
resolved "https://registry.yarnpkg.com/response-iterator/-/response-iterator-0.2.6.tgz#249005fb14d2e4eeb478a3f735a28fd8b4c9f3da"
integrity sha512-pVzEEzrsg23Sh053rmDUvLSkGXluZio0qu8VT6ukrYuvtjVfCbDZH9d6PGXb8HZfzdNZt8feXv/jvUzlhRgLnw==
restore-cursor@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-2.0.0.tgz#9f7ee287f82fd326d4fd162923d62129eee0dfaf"
@ -12446,6 +12539,11 @@ symbol-observable@^1.0.2:
resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.2.0.tgz#c22688aed4eab3cdc2dfeacbb561660560a00804"
integrity sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==
symbol-observable@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-4.0.0.tgz#5b425f192279e87f2f9b937ac8540d1984b39205"
integrity sha512-b19dMThMV4HVFynSAM1++gBHAbk2Tc/osgLIBZMKsyqh34jb2e8Os7T6ZW/Bt3pJFdBTd2JwAnAAEQV7rSNvcQ==
symbol-tree@^3.2.2, symbol-tree@^3.2.4:
version "3.2.4"
resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2"
@ -12727,6 +12825,13 @@ tryer@^1.0.1:
resolved "https://registry.yarnpkg.com/tryer/-/tryer-1.0.1.tgz#f2c85406800b9b0f74c9f7465b81eaad241252f8"
integrity sha512-c3zayb8/kWWpycWYg87P71E1S1ZL6b6IJxfb5fvsUgsf0S2MVGaDhDXXjDMpdCpfWXqptc+4mXwmiy1ypXqRAA==
ts-invariant@^0.10.3:
version "0.10.3"
resolved "https://registry.yarnpkg.com/ts-invariant/-/ts-invariant-0.10.3.tgz#3e048ff96e91459ffca01304dbc7f61c1f642f6c"
integrity sha512-uivwYcQaxAucv1CzRp2n/QdYPo4ILf9VXgH19zEIjFx2EJufV16P0JtJVpYHy89DItG6Kwj2oIUjrcK5au+4tQ==
dependencies:
tslib "^2.1.0"
ts-invariant@^0.4.0:
version "0.4.4"
resolved "https://registry.yarnpkg.com/ts-invariant/-/ts-invariant-0.4.4.tgz#97a523518688f93aafad01b0e80eb803eb2abd86"
@ -12785,6 +12890,11 @@ tslib@^2.1.0:
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.1.tgz#e8a335add5ceae51aa261d32a490158ef042ef01"
integrity sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==
tslib@^2.3.0:
version "2.4.1"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.4.1.tgz#0d0bfbaac2880b91e22df0768e55be9753a5b17e"
integrity sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==
tsutils@^3.21.0:
version "3.21.0"
resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.21.0.tgz#b48717d394cea6c1e096983eed58e9d61715b623"
@ -13844,7 +13954,14 @@ zen-observable-ts@^0.8.21:
tslib "^1.9.3"
zen-observable "^0.8.0"
zen-observable@^0.8.0:
zen-observable-ts@^1.2.5:
version "1.2.5"
resolved "https://registry.yarnpkg.com/zen-observable-ts/-/zen-observable-ts-1.2.5.tgz#6c6d9ea3d3a842812c6e9519209365a122ba8b58"
integrity sha512-QZWQekv6iB72Naeake9hS1KxHlotfRpe+WGNbNx5/ta+R3DNjVO2bswf63gXlWDcs+EMd7XY8HfVQyP1X6T4Zg==
dependencies:
zen-observable "0.8.15"
zen-observable@0.8.15, zen-observable@^0.8.0:
version "0.8.15"
resolved "https://registry.yarnpkg.com/zen-observable/-/zen-observable-0.8.15.tgz#96415c512d8e3ffd920afd3889604e30b9eaac15"
integrity sha512-PQ2PC7R9rslx84ndNBZB/Dkv8V8fZEpk83RLgXtYd0fwUgEjseMn1Dgajh2x6S8QbZAFa9p2qVCEuYZNgve0dQ==

View File

@ -1,4 +1,4 @@
CONFIG_VERSION=v10.2022-09-20
CONFIG_VERSION=v11.2022-10-27
# Server
PORT=4000
@ -60,3 +60,8 @@ EVENT_PROTOCOL_DISABLED=false
# SET LOG LEVEL AS NEEDED IN YOUR .ENV
# POSSIBLE VALUES: all | trace | debug | info | warn | error | fatal
# LOG_LEVEL=info
# DHT
# if you set this value, the DHT hyperswarm will start to announce and listen
# on an hash created from this tpoic
# DHT_TOPIC=GRADIDO_HUB

View File

@ -55,3 +55,6 @@ WEBHOOK_ELOPAGE_SECRET=$WEBHOOK_ELOPAGE_SECRET
# EventProtocol
EVENT_PROTOCOL_DISABLED=$EVENT_PROTOCOL_DISABLED
# DHT
DHT_TOPIC=$DHT_TOPIC

View File

@ -18,6 +18,7 @@
"klicktipp": "cross-env TZ=UTC NODE_ENV=development ts-node -r tsconfig-paths/register src/util/klicktipp.ts"
},
"dependencies": {
"@hyperswarm/dht": "^6.2.0",
"@types/jest": "^27.0.2",
"@types/lodash.clonedeep": "^4.5.6",
"@types/uuid": "^8.3.4",

View File

@ -10,14 +10,14 @@ Decimal.set({
})
const constants = {
DB_VERSION: '0051-add_delete_by_to_contributions',
DB_VERSION: '0052-add_updated_at_to_contributions',
DECAY_START_TIME: new Date('2021-05-13 17:46:31-0000'), // GMT+0
LOG4JS_CONFIG: 'log4js-config.json',
// default log level on production should be info
LOG_LEVEL: process.env.LOG_LEVEL || 'info',
CONFIG_VERSION: {
DEFAULT: 'DEFAULT',
EXPECTED: 'v10.2022-09-20',
EXPECTED: 'v11.2022-10-27',
CURRENT: '',
},
}
@ -116,6 +116,10 @@ if (
)
}
const federation = {
DHT_TOPIC: process.env.DHT_TOPIC || null,
}
const CONFIG = {
...constants,
...server,
@ -126,6 +130,7 @@ const CONFIG = {
...loginServer,
...webhook,
...eventProtocol,
...federation,
}
export default CONFIG

View File

@ -0,0 +1 @@
declare module '@hyperswarm/dht'

View File

@ -0,0 +1,120 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import DHT from '@hyperswarm/dht'
// import { Connection } from '@dbTools/typeorm'
import { backendLogger as logger } from '@/server/logger'
function between(min: number, max: number) {
return Math.floor(Math.random() * (max - min + 1) + min)
}
const POLLTIME = 20000
const SUCCESSTIME = 120000
const ERRORTIME = 240000
const ANNOUNCETIME = 30000
const nodeRand = between(1, 99)
const nodeURL = `https://test${nodeRand}.org`
const nodeAPI = {
API_1_00: `${nodeURL}/api/1_00/`,
API_1_01: `${nodeURL}/api/1_01/`,
API_2_00: `${nodeURL}/graphql/2_00/`,
}
export const startDHT = async (
// connection: Connection,
topic: string,
): Promise<void> => {
try {
const TOPIC = DHT.hash(Buffer.from(topic))
const keyPair = DHT.keyPair()
const node = new DHT({ keyPair })
const server = node.createServer()
server.on('connection', function (socket: any) {
// noiseSocket is E2E between you and the other peer
// pipe it somewhere like any duplex stream
logger.info(`Remote public key: ${socket.remotePublicKey.toString('hex')}`)
// console.log("Local public key", noiseSocket.publicKey.toString("hex")); // same as keyPair.publicKey
socket.on('data', (data: Buffer) => logger.info(`data: ${data.toString('ascii')}`))
// process.stdin.pipe(noiseSocket).pipe(process.stdout);
})
await server.listen()
setInterval(async () => {
logger.info(`Announcing on topic: ${TOPIC.toString('hex')}`)
await node.announce(TOPIC, keyPair).finished()
}, ANNOUNCETIME)
let successfulRequests: string[] = []
let errorfulRequests: string[] = []
setInterval(async () => {
logger.info('Refreshing successful nodes')
successfulRequests = []
}, SUCCESSTIME)
setInterval(async () => {
logger.info('Refreshing errorful nodes')
errorfulRequests = []
}, ERRORTIME)
setInterval(async () => {
const result = await node.lookup(TOPIC)
const collectedPubKeys: string[] = []
for await (const data of result) {
data.peers.forEach((peer: any) => {
const pubKey = peer.publicKey.toString('hex')
if (
pubKey !== keyPair.publicKey.toString('hex') &&
!successfulRequests.includes(pubKey) &&
!errorfulRequests.includes(pubKey) &&
!collectedPubKeys.includes(pubKey)
) {
collectedPubKeys.push(peer.publicKey.toString('hex'))
}
})
}
logger.info(`Found new peers: ${collectedPubKeys}`)
collectedPubKeys.forEach((remotePubKey) => {
// publicKey here is keyPair.publicKey from above
const socket = node.connect(Buffer.from(remotePubKey, 'hex'))
// socket.once("connect", function () {
// console.log("client side emitted connect");
// });
// socket.once("end", function () {
// console.log("client side ended");
// });
socket.once('error', (err: any) => {
errorfulRequests.push(remotePubKey)
logger.error(`error on peer ${remotePubKey}: ${err.message}`)
})
socket.on('open', function () {
// noiseSocket fully open with the other peer
// console.log("writing to socket");
socket.write(Buffer.from(`${nodeRand}`))
socket.write(Buffer.from(JSON.stringify(nodeAPI)))
successfulRequests.push(remotePubKey)
})
// pipe it somewhere like any duplex stream
// process.stdin.pipe(noiseSocket).pipe(process.stdout)
})
}, POLLTIME)
} catch (err) {
logger.error(err)
}
}

View File

@ -139,6 +139,7 @@ describe('AdminResolver', () => {
describe('user to get a new role does not exist', () => {
it('throws an error', async () => {
jest.clearAllMocks()
await expect(
mutate({ mutation: setUserRole, variables: { userId: admin.id + 1, isAdmin: true } }),
).resolves.toEqual(
@ -195,6 +196,7 @@ describe('AdminResolver', () => {
describe('change role with error', () => {
describe('is own role', () => {
it('throws an error', async () => {
jest.clearAllMocks()
await expect(
mutate({ mutation: setUserRole, variables: { userId: admin.id, isAdmin: false } }),
).resolves.toEqual(
@ -211,6 +213,7 @@ describe('AdminResolver', () => {
describe('user has already role to be set', () => {
describe('to admin', () => {
it('throws an error', async () => {
jest.clearAllMocks()
await mutate({
mutation: setUserRole,
variables: { userId: user.id, isAdmin: true },
@ -231,6 +234,7 @@ describe('AdminResolver', () => {
describe('to usual user', () => {
it('throws an error', async () => {
jest.clearAllMocks()
await mutate({
mutation: setUserRole,
variables: { userId: user.id, isAdmin: false },
@ -307,6 +311,7 @@ describe('AdminResolver', () => {
describe('user to be deleted does not exist', () => {
it('throws an error', async () => {
jest.clearAllMocks()
await expect(
mutate({ mutation: deleteUser, variables: { userId: admin.id + 1 } }),
).resolves.toEqual(
@ -323,6 +328,7 @@ describe('AdminResolver', () => {
describe('delete self', () => {
it('throws an error', async () => {
jest.clearAllMocks()
await expect(
mutate({ mutation: deleteUser, variables: { userId: admin.id } }),
).resolves.toEqual(
@ -356,6 +362,7 @@ describe('AdminResolver', () => {
describe('delete deleted user', () => {
it('throws an error', async () => {
jest.clearAllMocks()
await expect(
mutate({ mutation: deleteUser, variables: { userId: user.id } }),
).resolves.toEqual(
@ -427,6 +434,7 @@ describe('AdminResolver', () => {
describe('user to be undelete does not exist', () => {
it('throws an error', async () => {
jest.clearAllMocks()
await expect(
mutate({ mutation: unDeleteUser, variables: { userId: admin.id + 1 } }),
).resolves.toEqual(
@ -447,6 +455,7 @@ describe('AdminResolver', () => {
})
it('throws an error', async () => {
jest.clearAllMocks()
await expect(
mutate({ mutation: unDeleteUser, variables: { userId: user.id } }),
).resolves.toEqual(
@ -939,6 +948,7 @@ describe('AdminResolver', () => {
describe('user to create for does not exist', () => {
it('throws an error', async () => {
jest.clearAllMocks()
await expect(
mutate({ mutation: adminCreateContribution, variables }),
).resolves.toEqual(
@ -962,6 +972,7 @@ describe('AdminResolver', () => {
})
it('throws an error', async () => {
jest.clearAllMocks()
await expect(
mutate({ mutation: adminCreateContribution, variables }),
).resolves.toEqual(
@ -987,6 +998,7 @@ describe('AdminResolver', () => {
})
it('throws an error', async () => {
jest.clearAllMocks()
await expect(
mutate({ mutation: adminCreateContribution, variables }),
).resolves.toEqual(
@ -1013,6 +1025,7 @@ describe('AdminResolver', () => {
describe('date of creation is not a date string', () => {
it('throws an error', async () => {
jest.clearAllMocks()
await expect(
mutate({ mutation: adminCreateContribution, variables }),
).resolves.toEqual(
@ -1034,6 +1047,7 @@ describe('AdminResolver', () => {
describe('date of creation is four months ago', () => {
it('throws an error', async () => {
jest.clearAllMocks()
const now = new Date()
variables.creationDate = new Date(
now.getFullYear(),
@ -1061,6 +1075,7 @@ describe('AdminResolver', () => {
describe('date of creation is in the future', () => {
it('throws an error', async () => {
jest.clearAllMocks()
const now = new Date()
variables.creationDate = new Date(
now.getFullYear(),
@ -1088,6 +1103,7 @@ describe('AdminResolver', () => {
describe('amount of creation is too high', () => {
it('throws an error', async () => {
jest.clearAllMocks()
variables.creationDate = new Date().toString()
await expect(
mutate({ mutation: adminCreateContribution, variables }),
@ -1213,6 +1229,7 @@ describe('AdminResolver', () => {
describe('user for creation to update does not exist', () => {
it('throws an error', async () => {
jest.clearAllMocks()
await expect(
mutate({
mutation: adminUpdateContribution,
@ -1242,6 +1259,7 @@ describe('AdminResolver', () => {
describe('user for creation to update is deleted', () => {
it('throws an error', async () => {
jest.clearAllMocks()
await expect(
mutate({
mutation: adminUpdateContribution,
@ -1267,6 +1285,7 @@ describe('AdminResolver', () => {
describe('creation does not exist', () => {
it('throws an error', async () => {
jest.clearAllMocks()
await expect(
mutate({
mutation: adminUpdateContribution,
@ -1292,6 +1311,7 @@ describe('AdminResolver', () => {
describe('user email does not match creation user', () => {
it('throws an error', async () => {
jest.clearAllMocks()
await expect(
mutate({
mutation: adminUpdateContribution,
@ -1326,6 +1346,7 @@ describe('AdminResolver', () => {
describe('creation update is not valid', () => {
// as this test has not clearly defined that date, it is a false positive
it('throws an error', async () => {
jest.clearAllMocks()
await expect(
mutate({
mutation: adminUpdateContribution,
@ -1502,6 +1523,7 @@ describe('AdminResolver', () => {
describe('adminDeleteContribution', () => {
describe('creation id does not exist', () => {
it('throws an error', async () => {
jest.clearAllMocks()
await expect(
mutate({
mutation: adminDeleteContribution,
@ -1538,6 +1560,7 @@ describe('AdminResolver', () => {
})
it('throws an error', async () => {
jest.clearAllMocks()
await expect(
mutate({
mutation: adminDeleteContribution,
@ -1583,6 +1606,7 @@ describe('AdminResolver', () => {
describe('confirmContribution', () => {
describe('creation does not exits', () => {
it('throws an error', async () => {
jest.clearAllMocks()
await expect(
mutate({
mutation: confirmContribution,

View File

@ -74,6 +74,7 @@ describe('ContributionResolver', () => {
describe('input not valid', () => {
it('throws error when memo length smaller than 5 chars', async () => {
jest.clearAllMocks()
const date = new Date()
await expect(
mutate({
@ -92,10 +93,11 @@ describe('ContributionResolver', () => {
})
it('logs the error found', () => {
expect(logger.error).toBeCalledWith(`memo text is too short: memo.length=4 < (5)`)
expect(logger.error).toBeCalledWith(`memo text is too short: memo.length=4 < 5`)
})
it('throws error when memo length greater than 255 chars', async () => {
jest.clearAllMocks()
const date = new Date()
await expect(
mutate({
@ -114,10 +116,11 @@ describe('ContributionResolver', () => {
})
it('logs the error found', () => {
expect(logger.error).toBeCalledWith(`memo text is too long: memo.length=259 > (255)`)
expect(logger.error).toBeCalledWith(`memo text is too long: memo.length=259 > 255`)
})
it('throws error when creationDate not-valid', async () => {
jest.clearAllMocks()
await expect(
mutate({
mutation: createContribution,
@ -144,6 +147,7 @@ describe('ContributionResolver', () => {
})
it('throws error when creationDate 3 month behind', async () => {
jest.clearAllMocks()
const date = new Date()
await expect(
mutate({
@ -375,6 +379,7 @@ describe('ContributionResolver', () => {
describe('wrong contribution id', () => {
it('throws an error', async () => {
jest.clearAllMocks()
await expect(
mutate({
mutation: updateContribution,
@ -399,6 +404,7 @@ describe('ContributionResolver', () => {
describe('Memo length smaller than 5 chars', () => {
it('throws error', async () => {
jest.clearAllMocks()
const date = new Date()
await expect(
mutate({
@ -418,12 +424,13 @@ describe('ContributionResolver', () => {
})
it('logs the error found', () => {
expect(logger.error).toBeCalledWith('memo text is too short: memo.length=4 < (5)')
expect(logger.error).toBeCalledWith('memo text is too short: memo.length=4 < 5')
})
})
describe('Memo length greater than 255 chars', () => {
it('throws error', async () => {
jest.clearAllMocks()
const date = new Date()
await expect(
mutate({
@ -443,7 +450,7 @@ describe('ContributionResolver', () => {
})
it('logs the error found', () => {
expect(logger.error).toBeCalledWith('memo text is too long: memo.length=259 > (255)')
expect(logger.error).toBeCalledWith('memo text is too long: memo.length=259 > 255')
})
})
@ -456,6 +463,7 @@ describe('ContributionResolver', () => {
})
it('throws an error', async () => {
jest.clearAllMocks()
await expect(
mutate({
mutation: updateContribution,
@ -486,6 +494,7 @@ describe('ContributionResolver', () => {
describe('admin tries to update a user contribution', () => {
it('throws an error', async () => {
jest.clearAllMocks()
await expect(
mutate({
mutation: adminUpdateContribution,
@ -516,6 +525,7 @@ describe('ContributionResolver', () => {
})
it('throws an error', async () => {
jest.clearAllMocks()
await expect(
mutate({
mutation: updateContribution,
@ -546,6 +556,7 @@ describe('ContributionResolver', () => {
describe('update creation to a date that is older than 3 months', () => {
it('throws an error', async () => {
jest.clearAllMocks()
const date = new Date()
await expect(
mutate({
@ -564,7 +575,7 @@ describe('ContributionResolver', () => {
)
})
it('logs the error found', () => {
it.skip('logs the error found', () => {
expect(logger.error).toBeCalledWith(
'No information for available creations with the given creationDate=',
'Invalid Date',
@ -830,6 +841,7 @@ describe('ContributionResolver', () => {
describe('User deletes already confirmed contribution', () => {
it('throws an error', async () => {
jest.clearAllMocks()
await mutate({
mutation: login,
variables: { email: 'peter@lustig.de', password: 'Aa12345_' },

View File

@ -13,6 +13,8 @@ import { Contribution, ContributionListResult } from '@model/Contribution'
import { UnconfirmedContribution } from '@model/UnconfirmedContribution'
import { validateContribution, getUserCreation, updateCreations } from './util/creations'
import { MEMO_MAX_CHARS, MEMO_MIN_CHARS } from './const/const'
import { ContributionMessage } from '@entity/ContributionMessage'
import { ContributionMessageType } from '@enum/MessageType'
import {
Event,
EventContributionCreate,
@ -30,12 +32,12 @@ export class ContributionResolver {
@Ctx() context: Context,
): Promise<UnconfirmedContribution> {
if (memo.length > MEMO_MAX_CHARS) {
logger.error(`memo text is too long: memo.length=${memo.length} > (${MEMO_MAX_CHARS})`)
logger.error(`memo text is too long: memo.length=${memo.length} > ${MEMO_MAX_CHARS}`)
throw new Error(`memo text is too long (${MEMO_MAX_CHARS} characters maximum)`)
}
if (memo.length < MEMO_MIN_CHARS) {
logger.error(`memo text is too short: memo.length=${memo.length} < (${MEMO_MIN_CHARS})`)
logger.error(`memo text is too short: memo.length=${memo.length} < ${MEMO_MIN_CHARS}`)
throw new Error(`memo text is too short (${MEMO_MIN_CHARS} characters minimum)`)
}
@ -170,12 +172,12 @@ export class ContributionResolver {
@Ctx() context: Context,
): Promise<UnconfirmedContribution> {
if (memo.length > MEMO_MAX_CHARS) {
logger.error(`memo text is too long: memo.length=${memo.length} > (${MEMO_MAX_CHARS}`)
logger.error(`memo text is too long: memo.length=${memo.length} > ${MEMO_MAX_CHARS}`)
throw new Error(`memo text is too long (${MEMO_MAX_CHARS} characters maximum)`)
}
if (memo.length < MEMO_MIN_CHARS) {
logger.error(`memo text is too short: memo.length=${memo.length} < (${MEMO_MIN_CHARS}`)
logger.error(`memo text is too short: memo.length=${memo.length} < ${MEMO_MIN_CHARS}`)
throw new Error(`memo text is too short (${MEMO_MIN_CHARS} characters minimum)`)
}
@ -192,7 +194,17 @@ export class ContributionResolver {
logger.error('user of the pending contribution and send user does not correspond')
throw new Error('user of the pending contribution and send user does not correspond')
}
if (
contributionToUpdate.contributionStatus !== ContributionStatus.IN_PROGRESS &&
contributionToUpdate.contributionStatus !== ContributionStatus.PENDING
) {
logger.error(
`Contribution can not be updated since the state is ${contributionToUpdate.contributionStatus}`,
)
throw new Error(
`Contribution can not be updated since the state is ${contributionToUpdate.contributionStatus}`,
)
}
const creationDateObj = new Date(creationDate)
let creations = await getUserCreation(user.id)
if (contributionToUpdate.contributionDate.getMonth() === creationDateObj.getMonth()) {
@ -204,10 +216,28 @@ export class ContributionResolver {
// all possible cases not to be true are thrown in this function
validateContribution(creations, amount, creationDateObj)
const contributionMessage = ContributionMessage.create()
contributionMessage.contributionId = contributionId
contributionMessage.createdAt = contributionToUpdate.updatedAt
? contributionToUpdate.updatedAt
: contributionToUpdate.createdAt
const changeMessage = `${contributionToUpdate.contributionDate}
---
${contributionToUpdate.memo}
---
${contributionToUpdate.amount}`
contributionMessage.message = changeMessage
contributionMessage.isModerator = false
contributionMessage.userId = user.id
contributionMessage.type = ContributionMessageType.HISTORY
ContributionMessage.save(contributionMessage)
contributionToUpdate.amount = amount
contributionToUpdate.memo = memo
contributionToUpdate.contributionDate = new Date(creationDate)
contributionToUpdate.contributionStatus = ContributionStatus.PENDING
contributionToUpdate.updatedAt = new Date()
dbContribution.save(contributionToUpdate)
const event = new Event()

View File

@ -67,6 +67,7 @@ describe('send coins', () => {
describe('unknown recipient', () => {
it('throws an error', async () => {
jest.clearAllMocks()
await mutate({
mutation: login,
variables: bobData,
@ -93,6 +94,7 @@ describe('send coins', () => {
describe('deleted recipient', () => {
it('throws an error', async () => {
jest.clearAllMocks()
await mutate({
mutation: login,
variables: peterData,
@ -125,6 +127,7 @@ describe('send coins', () => {
describe('recipient account not activated', () => {
it('throws an error', async () => {
jest.clearAllMocks()
await mutate({
mutation: login,
variables: peterData,
@ -166,6 +169,7 @@ describe('send coins', () => {
describe('sender and recipient are the same', () => {
it('throws an error', async () => {
jest.clearAllMocks()
expect(
await mutate({
mutation: sendCoins,
@ -189,6 +193,7 @@ describe('send coins', () => {
describe('memo text is too long', () => {
it('throws an error', async () => {
jest.clearAllMocks()
expect(
await mutate({
mutation: sendCoins,
@ -212,6 +217,7 @@ describe('send coins', () => {
describe('memo text is too short', () => {
it('throws an error', async () => {
jest.clearAllMocks()
expect(
await mutate({
mutation: sendCoins,
@ -235,6 +241,7 @@ describe('send coins', () => {
describe('user has not enough GDD', () => {
it('throws an error', async () => {
jest.clearAllMocks()
expect(
await mutate({
mutation: sendCoins,
@ -260,6 +267,7 @@ describe('send coins', () => {
describe('sending negative amount', () => {
it('throws an error', async () => {
jest.clearAllMocks()
expect(
await mutate({
mutation: sendCoins,

View File

@ -514,18 +514,20 @@ describe('UserResolver', () => {
await mutate({ mutation: createUser, variables: createUserVariables })
const emailContact = await UserContact.findOneOrFail({ email: createUserVariables.email })
emailVerificationCode = emailContact.emailVerificationCode.toString()
result = await mutate({
mutation: setPassword,
variables: { code: emailVerificationCode, password: 'not-valid' },
})
})
afterAll(async () => {
await cleanDB()
})
it('throws an error', () => {
expect(result).toEqual(
it('throws an error', async () => {
jest.clearAllMocks()
expect(
await mutate({
mutation: setPassword,
variables: { code: emailVerificationCode, password: 'not-valid' },
}),
).toEqual(
expect.objectContaining({
errors: [
new GraphQLError(
@ -544,18 +546,20 @@ describe('UserResolver', () => {
describe('no valid optin code', () => {
beforeAll(async () => {
await mutate({ mutation: createUser, variables: createUserVariables })
result = await mutate({
mutation: setPassword,
variables: { code: 'not valid', password: 'Aa12345_' },
})
})
afterAll(async () => {
await cleanDB()
})
it('throws an error', () => {
expect(result).toEqual(
it('throws an error', async () => {
jest.clearAllMocks()
expect(
await mutate({
mutation: setPassword,
variables: { code: 'not valid', password: 'Aa12345_' },
}),
).toEqual(
expect.objectContaining({
errors: [new GraphQLError('Could not login with emailVerificationCode')],
}),
@ -582,13 +586,9 @@ describe('UserResolver', () => {
})
describe('no users in database', () => {
beforeAll(async () => {
it('throws an error', async () => {
jest.clearAllMocks()
result = await mutate({ mutation: login, variables })
})
it('throws an error', () => {
expect(result).toEqual(
expect(await mutate({ mutation: login, variables })).toEqual(
expect.objectContaining({
errors: [new GraphQLError('No user with this credentials')],
}),
@ -666,6 +666,7 @@ describe('UserResolver', () => {
describe('logout', () => {
describe('unauthenticated', () => {
it('throws an error', async () => {
jest.clearAllMocks()
resetToken()
await expect(mutate({ mutation: logout })).resolves.toEqual(
expect.objectContaining({
@ -704,6 +705,7 @@ describe('UserResolver', () => {
describe('verifyLogin', () => {
describe('unauthenticated', () => {
it('throws an error', async () => {
jest.clearAllMocks()
resetToken()
await expect(query({ query: verifyLogin })).resolves.toEqual(
expect.objectContaining({
@ -723,6 +725,7 @@ describe('UserResolver', () => {
})
it('throws an error', async () => {
jest.clearAllMocks()
resetToken()
await expect(query({ query: verifyLogin })).resolves.toEqual(
expect.objectContaining({
@ -883,6 +886,7 @@ describe('UserResolver', () => {
describe('wrong optin code', () => {
it('throws an error', async () => {
jest.clearAllMocks()
await expect(
query({ query: queryOptIn, variables: { optIn: 'not-valid' } }),
).resolves.toEqual(
@ -919,6 +923,7 @@ describe('UserResolver', () => {
describe('updateUserInfos', () => {
describe('unauthenticated', () => {
it('throws an error', async () => {
jest.clearAllMocks()
resetToken()
await expect(mutate({ mutation: updateUserInfos })).resolves.toEqual(
expect.objectContaining({
@ -976,6 +981,7 @@ describe('UserResolver', () => {
describe('language is not valid', () => {
it('throws an error', async () => {
jest.clearAllMocks()
await expect(
mutate({
mutation: updateUserInfos,
@ -998,6 +1004,7 @@ describe('UserResolver', () => {
describe('password', () => {
describe('wrong old password', () => {
it('throws an error', async () => {
jest.clearAllMocks()
await expect(
mutate({
mutation: updateUserInfos,
@ -1020,6 +1027,7 @@ describe('UserResolver', () => {
describe('invalid new password', () => {
it('throws an error', async () => {
jest.clearAllMocks()
await expect(
mutate({
mutation: updateUserInfos,
@ -1108,6 +1116,7 @@ describe('UserResolver', () => {
describe('searchAdminUsers', () => {
describe('unauthenticated', () => {
it('throws an error', async () => {
jest.clearAllMocks()
resetToken()
await expect(mutate({ mutation: searchAdminUsers })).resolves.toEqual(
expect.objectContaining({

View File

@ -1,6 +1,7 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import createServer from './server/createServer'
import { startDHT } from '@/federation/index'
// config
import CONFIG from './config'
@ -16,6 +17,11 @@ async function main() {
console.log(`GraphIQL available at http://localhost:${CONFIG.PORT}`)
}
})
// start DHT hyperswarm when DHT_TOPIC is set in .env
if (CONFIG.DHT_TOPIC) {
await startDHT(CONFIG.DHT_TOPIC) // con,
}
}
main().catch((e) => {

View File

@ -58,7 +58,7 @@
"@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. */
"typeRoots": ["src/federation/@types", "node_modules/@types"], /* List of folders to include type definitions from. */
// "types": [], /* Type declaration files to be included in compilation. */
// "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
"esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */

View File

@ -394,6 +394,42 @@
resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-1.2.0.tgz#87de7af9c231826fdd68ac7258f77c429e0e5fcf"
integrity sha512-wdppn25U8z/2yiaT6YGquE6X8sSv7hNMWSXYSSU1jGv/yd6XqjXgTDJ8KP4NgjTXfJ3GbRjeeb8RTV7a/VpM+w==
"@hyperswarm/dht@^6.2.0":
version "6.2.0"
resolved "https://registry.yarnpkg.com/@hyperswarm/dht/-/dht-6.2.0.tgz#b2cb1218752b52fabb66f304e73448a108d1effd"
integrity sha512-AeyfRdAkfCz/J3vTC4rdpzEpT7xQ+tls87Zpzw9Py3VGUZD8hMT7pr43OOdkCBNvcln6K/5/Lxhnq5lBkzH3yw==
dependencies:
"@hyperswarm/secret-stream" "^6.0.0"
b4a "^1.3.1"
bogon "^1.0.0"
compact-encoding "^2.4.1"
compact-encoding-net "^1.0.1"
debugging-stream "^2.0.0"
dht-rpc "^6.0.0"
events "^3.3.0"
hypercore-crypto "^3.3.0"
noise-curve-ed "^1.0.2"
noise-handshake "^2.1.0"
record-cache "^1.1.1"
safety-catch "^1.0.1"
sodium-universal "^3.0.4"
udx-native "^1.1.0"
xache "^1.1.0"
"@hyperswarm/secret-stream@^6.0.0":
version "6.0.0"
resolved "https://registry.yarnpkg.com/@hyperswarm/secret-stream/-/secret-stream-6.0.0.tgz#67db820308cc9fed899cb8f5e9f47ae819d5a4e3"
integrity sha512-0xuyJIJDe8JYk4uWUx25qJvWqybdjKU2ZIfP1GTqd7dQxwdR0bpYrQKdLkrn5txWSK4a28ySC2AjH0G3I0gXTA==
dependencies:
b4a "^1.1.0"
hypercore-crypto "^3.3.0"
noise-curve-ed "^1.0.2"
noise-handshake "^2.1.0"
sodium-secretstream "^1.0.0"
sodium-universal "^3.0.4"
streamx "^2.10.2"
timeout-refresh "^2.0.0"
"@istanbuljs/load-nyc-config@^1.0.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz#fd3db1d59ecf7cf121e80650bb86712f9b55eced"
@ -1458,6 +1494,11 @@ axios@^0.21.1:
dependencies:
follow-redirects "^1.14.0"
b4a@^1.0.1, b4a@^1.1.0, b4a@^1.1.1, b4a@^1.3.0, b4a@^1.3.1, b4a@^1.5.0:
version "1.5.3"
resolved "https://registry.yarnpkg.com/b4a/-/b4a-1.5.3.tgz#56293b5607aeda3fd81c481e516e9f103fc88341"
integrity sha512-1aCQIzQJK7G0z1Una75tWMlwVAR8o+QHoAlnWc5XAxRVBESY9WsitfBgM5nPyDBP5HrhPU1Np4Pq2Y7CJQ+tVw==
babel-jest@^27.2.5:
version "27.2.5"
resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-27.2.5.tgz#6bbbc1bb4200fe0bfd1b1fbcbe02fc62ebed16aa"
@ -1534,6 +1575,22 @@ binary-extensions@^2.0.0:
resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d"
integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==
blake2b-wasm@^2.4.0:
version "2.4.0"
resolved "https://registry.yarnpkg.com/blake2b-wasm/-/blake2b-wasm-2.4.0.tgz#9115649111edbbd87eb24ce7c04b427e4e2be5be"
integrity sha512-S1kwmW2ZhZFFFOghcx73+ZajEfKBqhP82JMssxtLVMxlaPea1p9uoLiUZ5WYyHn0KddwbLc+0vh4wR0KBNoT5w==
dependencies:
b4a "^1.0.1"
nanoassert "^2.0.0"
blake2b@^2.1.1:
version "2.1.4"
resolved "https://registry.yarnpkg.com/blake2b/-/blake2b-2.1.4.tgz#817d278526ddb4cd673bfb1af16d1ad61e393ba3"
integrity sha512-AyBuuJNI64gIvwx13qiICz6H6hpmjvYS5DGkG6jbXMOT8Z3WUJ3V1X0FlhIoT1b/5JtHE3ki+xjtMvu1nn+t9A==
dependencies:
blake2b-wasm "^2.4.0"
nanoassert "^2.0.0"
body-parser@1.19.0, body-parser@^1.18.3:
version "1.19.0"
resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.19.0.tgz#96b2709e57c9c4e09a6fd66a8fd979844f69f08a"
@ -1550,6 +1607,11 @@ body-parser@1.19.0, body-parser@^1.18.3:
raw-body "2.4.0"
type-is "~1.6.17"
bogon@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/bogon/-/bogon-1.0.0.tgz#66b8cdd269f790e3aa988e157bb34d4ba75ee586"
integrity sha512-mXxtlBtnW8koqFWPUBtKJm97vBSKZRpOvxvMRVun33qQXwMNfQzq9eTcQzKzqEoNUhNqF9t8rDc/wakKCcHMTg==
boxen@^5.0.0:
version "5.1.2"
resolved "https://registry.yarnpkg.com/boxen/-/boxen-5.1.2.tgz#788cb686fc83c1f486dfa8a40c68fc2b831d2b50"
@ -1672,6 +1734,13 @@ caniuse-lite@^1.0.30001264:
resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001418.tgz"
integrity sha512-oIs7+JL3K9JRQ3jPZjlH6qyYDp+nBTCais7hjh0s+fuBwufc7uZ7hPYMXrDOJhV360KGMTcczMRObk0/iMqZRg==
chacha20-universal@^1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/chacha20-universal/-/chacha20-universal-1.0.4.tgz#e8a33a386500b1ce5361b811ec5e81f1797883f5"
integrity sha512-/IOxdWWNa7nRabfe7+oF+jVkGjlr2xUL4J8l/OvzZhj+c9RpMqoo3Dq+5nU1j/BflRV4BKnaQ4+4oH1yBpQG1Q==
dependencies:
nanoassert "^2.0.0"
chalk@^2.0.0:
version "2.4.2"
resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424"
@ -1800,6 +1869,20 @@ commander@^2.20.3:
resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33"
integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==
compact-encoding-net@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/compact-encoding-net/-/compact-encoding-net-1.0.1.tgz#4da743d52721f5d0cc73a6d00556a96bc9b9fa1b"
integrity sha512-N9k1Qwg9b1ENk+TZsZhthzkuMtn3rn4ZinN75gf3/LplE+uaTCKjyaau5sK0m2NEUa/MmR77VxiGfD/Qz1ar0g==
dependencies:
compact-encoding "^2.4.1"
compact-encoding@^2.1.0, compact-encoding@^2.4.1, compact-encoding@^2.5.1:
version "2.7.0"
resolved "https://registry.yarnpkg.com/compact-encoding/-/compact-encoding-2.7.0.tgz#e6a0df408c25cbcdf7d619c97527074478cafd06"
integrity sha512-2I0A+pYKXYwxewbLxj26tU4pJyKlFNjadzjZ+36xJ5HwTrnhD9KcMQk3McEQRl1at6jrwA8E7UjmBdsGhEAPMw==
dependencies:
b4a "^1.3.0"
concat-map@0.0.1:
version "0.0.1"
resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
@ -1949,6 +2032,13 @@ debug@^4.3.4:
dependencies:
ms "2.1.2"
debugging-stream@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/debugging-stream/-/debugging-stream-2.0.0.tgz#515cad5a35299cf4b4bc0afcbd69d52c809c84ce"
integrity sha512-xwfl6wB/3xc553uwtGnSa94jFxnGOc02C0WU2Nmzwr80gzeqn1FX4VcbvoKIhe8L/lPq4BTQttAbrTN94uN8rA==
dependencies:
streamx "^2.12.4"
decimal.js-light@^2.5.1:
version "2.5.1"
resolved "https://registry.yarnpkg.com/decimal.js-light/-/decimal.js-light-2.5.1.tgz#134fd32508f19e208f4fb2f8dac0d2626a867934"
@ -2028,6 +2118,23 @@ detect-newline@^3.0.0:
resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-3.1.0.tgz#576f5dfc63ae1a192ff192d8ad3af6308991b651"
integrity sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==
dht-rpc@^6.0.0:
version "6.1.1"
resolved "https://registry.yarnpkg.com/dht-rpc/-/dht-rpc-6.1.1.tgz#a292a22aa19b05136978d33528cb571d6e32502f"
integrity sha512-wo0nMXwn/rhxVz62V0d+l/0HuikxLQh6lkwlUIdoaUzGl9DobFj4epSScD3/lTMwKts+Ih0DFNqP+j0tYwdajQ==
dependencies:
b4a "^1.3.1"
compact-encoding "^2.1.0"
compact-encoding-net "^1.0.1"
events "^3.3.0"
fast-fifo "^1.0.0"
kademlia-routing-table "^1.0.0"
nat-sampler "^1.0.1"
sodium-universal "^3.0.4"
streamx "^2.10.3"
time-ordered-set "^1.0.2"
udx-native "^1.1.0"
dicer@0.3.0:
version "0.3.0"
resolved "https://registry.yarnpkg.com/dicer/-/dicer-0.3.0.tgz#eacd98b3bfbf92e8ab5c2fdb71aaac44bb06b872"
@ -2430,6 +2537,11 @@ eventemitter3@^3.1.0:
resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-3.1.2.tgz#2d3d48f9c346698fce83a85d7d664e98535df6e7"
integrity sha512-tvtQIeLVHjDkJYnzf2dgVMxfuSGJeM/7UCG17TT4EumTfNtF+0nebF/4zWOIkCreAbtNqhGEboB6BWrwqNaw4Q==
events@^3.3.0:
version "3.3.0"
resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400"
integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==
execa@^5.0.0:
version "5.1.1"
resolved "https://registry.yarnpkg.com/execa/-/execa-5.1.1.tgz#f80ad9cbf4298f7bd1d4c9555c21e93741c411dd"
@ -2513,6 +2625,11 @@ fast-diff@^1.1.2:
resolved "https://registry.yarnpkg.com/fast-diff/-/fast-diff-1.2.0.tgz#73ee11982d86caaf7959828d519cfe927fac5f03"
integrity sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==
fast-fifo@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/fast-fifo/-/fast-fifo-1.1.0.tgz#17d1a3646880b9891dfa0c54e69c5fef33cad779"
integrity sha512-Kl29QoNbNvn4nhDsLYjyIAaIqaJB6rBx5p3sL9VjaefJ+eMFBWVZiaoguaoZfzEKr5RhAti0UgM8703akGPJ6g==
fast-glob@^3.1.1:
version "3.2.7"
resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.7.tgz#fd6cb7a2d7e9aa7a7846111e85a196d6b2f766a1"
@ -2893,6 +3010,15 @@ has@^1.0.3:
dependencies:
function-bind "^1.1.1"
hmac-blake2b@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/hmac-blake2b/-/hmac-blake2b-2.0.0.tgz#09494e5d245d7afe45d157093080b159f7bacf15"
integrity sha512-JbGNtM1YRd8EQH/2vNTAP1oy5lJVPlBFYZfCJTu3k8sqOUm0rRIf/3+MCd5noVykETwTbun6jEOc+4Tu78ubHA==
dependencies:
nanoassert "^1.1.0"
sodium-native "^3.1.1"
sodium-universal "^3.0.0"
hosted-git-info@^2.1.4:
version "2.8.9"
resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.9.tgz#dffc0bf9a21c02209090f2aa69429e1414daf3f9"
@ -2970,6 +3096,15 @@ human-signals@^2.1.0:
resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0"
integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==
hypercore-crypto@^3.3.0:
version "3.3.0"
resolved "https://registry.yarnpkg.com/hypercore-crypto/-/hypercore-crypto-3.3.0.tgz#03ab5b44608a563e131f629f671c6f90a83c52e6"
integrity sha512-zAWbDqG7kWwS6rCxxTUeB/OeFAz3PoOmouKaoMubtDJYJsLHqXtA3wE2mLsw+E2+iYyom5zrFyBTFVYxmgwW6g==
dependencies:
b4a "^1.1.0"
compact-encoding "^2.5.1"
sodium-universal "^3.0.0"
iconv-lite@0.4.24:
version "0.4.24"
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b"
@ -3120,6 +3255,13 @@ is-core-module@^2.2.0, is-core-module@^2.6.0:
dependencies:
has "^1.0.3"
is-core-module@^2.9.0:
version "2.9.0"
resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.9.0.tgz#e1c34429cd51c6dd9e09e0799e396e27b19a9c69"
integrity sha512-+5FPy5PnwmO3lvfMb0AsoPaBG+5KHUI0wYFXOtYPnVVVspTFUuMZNfNaNVRt3FZadstu2c8x23vykRW/NBoU6A==
dependencies:
has "^1.0.3"
is-date-object@^1.0.1:
version "1.0.5"
resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.5.tgz#0841d5536e724c25597bf6ea62e1bd38298df31f"
@ -3842,6 +3984,11 @@ jws@^3.2.2:
jwa "^1.4.1"
safe-buffer "^5.0.1"
kademlia-routing-table@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/kademlia-routing-table/-/kademlia-routing-table-1.0.1.tgz#6f18416f612e885a8d4df128f04c490a90d772f6"
integrity sha512-dKk19sC3/+kWhBIvOKCthxVV+JH0NrswSBq4sA4eOkkPMqQM1rRuOWte1WSKXeP8r9Nx4NuiH2gny3lMddJTpw==
keyv@^3.0.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/keyv/-/keyv-3.1.0.tgz#ecc228486f69991e49e9476485a5be1e8fc5c4d9"
@ -4155,6 +4302,26 @@ named-placeholders@^1.1.2:
dependencies:
lru-cache "^4.1.3"
nanoassert@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/nanoassert/-/nanoassert-1.1.0.tgz#4f3152e09540fde28c76f44b19bbcd1d5a42478d"
integrity sha512-C40jQ3NzfkP53NsO8kEOFd79p4b9kDXQMwgiY1z8ZwrDZgUyom0AHwGegF4Dm99L+YoYhuaB0ceerUcXmqr1rQ==
nanoassert@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/nanoassert/-/nanoassert-2.0.0.tgz#a05f86de6c7a51618038a620f88878ed1e490c09"
integrity sha512-7vO7n28+aYO4J+8w96AzhmU8G+Y/xpPDJz/se19ICsqj/momRbb9mh9ZUtkoJ5X3nTnPdhEJyc0qnM6yAsHBaA==
napi-macros@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/napi-macros/-/napi-macros-2.0.0.tgz#2b6bae421e7b96eb687aa6c77a7858640670001b"
integrity sha512-A0xLykHtARfueITVDernsAWdtIMbOJgKgcluwENp3AlsKN/PloyO10HtmoqnFAQAcxPkgZN7wdfPfEd0zNGxbg==
nat-sampler@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/nat-sampler/-/nat-sampler-1.0.1.tgz#2b68338ea6d4c139450cd971fd00a4ac1b33d923"
integrity sha512-yQvyNN7xbqR8crTKk3U8gRgpcV1Az+vfCEijiHu9oHHsnIl8n3x+yXNHl42M6L3czGynAVoOT9TqBfS87gDdcw==
natural-compare@^1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
@ -4172,10 +4339,10 @@ node-fetch@^2.6.1:
dependencies:
whatwg-url "^5.0.0"
node-gyp-build@^4.3.0:
version "4.3.0"
resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.3.0.tgz#9f256b03e5826150be39c764bf51e993946d71a3"
integrity sha512-iWjXZvmboq0ja1pUGULQBexmxq8CV4xBhX7VDOTbL7ZR4FOowwY/VOtRxBN/yKxmdGoIp4j5ysNT4u3S2pDQ3Q==
node-gyp-build@^4.3.0, node-gyp-build@^4.4.0:
version "4.5.0"
resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.5.0.tgz#7a64eefa0b21112f89f58379da128ac177f20e40"
integrity sha512-2iGbaQBV+ITgCz76ZEjmhUKAKVf7xfY1sRl4UiKQspfZMH2h06SyhNsnSVy50cwkFQDGLyif6m/6uFXHkOZ6rg==
node-int64@^0.4.0:
version "0.4.0"
@ -4213,6 +4380,25 @@ nodemon@^2.0.7:
undefsafe "^2.0.3"
update-notifier "^5.1.0"
noise-curve-ed@^1.0.2:
version "1.0.4"
resolved "https://registry.yarnpkg.com/noise-curve-ed/-/noise-curve-ed-1.0.4.tgz#8ae83f5d2d2e31d0c9c069271ca6e462d31cd884"
integrity sha512-plUUSEOU66FZ9TaBKpk4+fgQeeS+OLlThS2o8a1TxVpMWV2v1izvEnjSpFV9gEPZl4/1yN+S5KqLubFjogqQOw==
dependencies:
b4a "^1.1.0"
nanoassert "^2.0.0"
sodium-universal "^3.0.4"
noise-handshake@^2.1.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/noise-handshake/-/noise-handshake-2.2.0.tgz#24c98f502d49118770e1ec2af2894b8789f0ac7c"
integrity sha512-+0mFUc5YSnOPI+4K/7nr6XDGduITaUasPVurzrH03sk6yW+udKxP/qjEwEekRwIpnvcCKYnjiZ9HJenJv9ljZg==
dependencies:
b4a "^1.1.0"
hmac-blake2b "^2.0.0"
nanoassert "^2.0.0"
sodium-universal "^3.0.4"
nopt@~1.0.10:
version "1.0.10"
resolved "https://registry.yarnpkg.com/nopt/-/nopt-1.0.10.tgz#6ddd21bd2a31417b92727dd585f8a6f37608ebee"
@ -4443,7 +4629,7 @@ path-key@^3.0.0, path-key@^3.1.0:
resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375"
integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==
path-parse@^1.0.6:
path-parse@^1.0.6, path-parse@^1.0.7:
version "1.0.7"
resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735"
integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==
@ -4611,6 +4797,11 @@ queue-microtask@^1.2.2:
resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243"
integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==
queue-tick@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/queue-tick/-/queue-tick-1.0.0.tgz#011104793a3309ae86bfeddd54e251dc94a36725"
integrity sha512-ULWhjjE8BmiICGn3G8+1L9wFpERNxkf8ysxkAer4+TFdRefDaXOCV5m92aMB9FtBVmn/8sETXLXY6BfW7hyaWQ==
random-bigint@^0.0.1:
version "0.0.1"
resolved "https://registry.yarnpkg.com/random-bigint/-/random-bigint-0.0.1.tgz#684de0a93784ab7448a441393916f0e632c95df9"
@ -4670,6 +4861,13 @@ readdirp@~3.6.0:
dependencies:
picomatch "^2.2.1"
record-cache@^1.1.1:
version "1.2.0"
resolved "https://registry.yarnpkg.com/record-cache/-/record-cache-1.2.0.tgz#e601bc4f164d58330cc00055e27aa4682291c882"
integrity sha512-kyy3HWCez2WrotaL3O4fTn0rsIdfRKOdQQcEJ9KpvmKmbffKVvwsloX063EgRUlpJIXHiDQFhJcTbZequ2uTZw==
dependencies:
b4a "^1.3.1"
reflect-metadata@^0.1.13:
version "0.1.13"
resolved "https://registry.yarnpkg.com/reflect-metadata/-/reflect-metadata-0.1.13.tgz#67ae3ca57c972a2aa1642b10fe363fe32d49dc08"
@ -4729,6 +4927,15 @@ resolve@^1.10.0, resolve@^1.10.1, resolve@^1.20.0:
is-core-module "^2.2.0"
path-parse "^1.0.6"
resolve@^1.17.0:
version "1.22.1"
resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.1.tgz#27cb2ebb53f91abb49470a928bba7558066ac177"
integrity sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==
dependencies:
is-core-module "^2.9.0"
path-parse "^1.0.7"
supports-preserve-symlinks-flag "^1.0.0"
responselike@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/responselike/-/responselike-1.0.2.tgz#918720ef3b631c5642be068f15ade5a46f4ba1e7"
@ -4780,6 +4987,11 @@ safe-buffer@^5.0.1:
resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
safety-catch@^1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/safety-catch/-/safety-catch-1.0.2.tgz#d64cbd57fd601da91c356b6ab8902f3e449a7a4b"
integrity sha512-C1UYVZ4dtbBxEtvOcpjBaaD27nP8MlvyAQEp2fOTOEe6pfUpk1cDUxij6BR1jZup6rSyUTaBBplK7LanskrULA==
saxes@^5.0.1:
version "5.0.1"
resolved "https://registry.yarnpkg.com/saxes/-/saxes-5.0.1.tgz#eebab953fa3b7608dbe94e5dadb15c888fa6696d"
@ -4863,6 +5075,38 @@ sha.js@^2.4.11:
inherits "^2.0.1"
safe-buffer "^5.0.1"
sha256-universal@^1.1.0:
version "1.2.1"
resolved "https://registry.yarnpkg.com/sha256-universal/-/sha256-universal-1.2.1.tgz#051d92decce280cd6137d42d496eac88da942c0e"
integrity sha512-ghn3muhdn1ailCQqqceNxRgkOeZSVfSE13RQWEg6njB+itsFzGVSJv+O//2hvNXZuxVIRyNzrgsZ37SPDdGJJw==
dependencies:
b4a "^1.0.1"
sha256-wasm "^2.2.1"
sha256-wasm@^2.2.1:
version "2.2.2"
resolved "https://registry.yarnpkg.com/sha256-wasm/-/sha256-wasm-2.2.2.tgz#4940b6c9ba28f3f08b700efce587ef36d4d516d4"
integrity sha512-qKSGARvao+JQlFiA+sjJZhJ/61gmW/3aNLblB2rsgIxDlDxsJPHo8a1seXj12oKtuHVgJSJJ7QEGBUYQN741lQ==
dependencies:
b4a "^1.0.1"
nanoassert "^2.0.0"
sha512-universal@^1.1.0:
version "1.2.1"
resolved "https://registry.yarnpkg.com/sha512-universal/-/sha512-universal-1.2.1.tgz#829505a7586530515cc1a10b78815c99722c4df0"
integrity sha512-kehYuigMoRkIngCv7rhgruLJNNHDnitGTBdkcYbCbooL8Cidj/bS78MDxByIjcc69M915WxcQTgZetZ1JbeQTQ==
dependencies:
b4a "^1.0.1"
sha512-wasm "^2.3.1"
sha512-wasm@^2.3.1:
version "2.3.4"
resolved "https://registry.yarnpkg.com/sha512-wasm/-/sha512-wasm-2.3.4.tgz#b86b37112ff6d1fc3740f2484a6855f17a6e1300"
integrity sha512-akWoxJPGCB3aZCrZ+fm6VIFhJ/p8idBv7AWGFng/CZIrQo51oQNsvDbTSRXWAzIiZJvpy16oIDiCCPqTe21sKg==
dependencies:
b4a "^1.0.1"
nanoassert "^2.0.0"
shebang-command@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea"
@ -4889,6 +5133,13 @@ signal-exit@^3.0.2, signal-exit@^3.0.3:
resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.5.tgz#9e3e8cc0c75a99472b44321033a7702e7738252f"
integrity sha512-KWcOiKeQj6ZyXx7zq4YxSMgHRlod4czeBQZrPb8OKcohcqAXShm7E20kEMle9WBt26hFcAf0qLOcp5zmY7kOqQ==
siphash24@^1.0.1:
version "1.3.1"
resolved "https://registry.yarnpkg.com/siphash24/-/siphash24-1.3.1.tgz#7f87fd2c5db88d8d46335a68f780f281641c8b22"
integrity sha512-moemC3ZKiTzH29nbFo3Iw8fbemWWod4vNs/WgKbQ54oEs6mE6XVlguxvinYjB+UmaE0PThgyED9fUkWvirT8hA==
dependencies:
nanoassert "^2.0.0"
sisteransi@^1.0.5:
version "1.0.5"
resolved "https://registry.yarnpkg.com/sisteransi/-/sisteransi-1.0.5.tgz#134d681297756437cc05ca01370d3a7a571075ed"
@ -4908,13 +5159,50 @@ slice-ansi@^4.0.0:
astral-regex "^2.0.0"
is-fullwidth-code-point "^3.0.0"
sodium-native@^3.3.0:
sodium-javascript@~0.8.0:
version "0.8.0"
resolved "https://registry.yarnpkg.com/sodium-javascript/-/sodium-javascript-0.8.0.tgz#0a94d7bb58ab17be82255f3949259af59778fdbc"
integrity sha512-rEBzR5mPxPES+UjyMDvKPIXy9ImF17KOJ32nJNi9uIquWpS/nfj+h6m05J5yLJaGXjgM72LmQoUbWZVxh/rmGg==
dependencies:
blake2b "^2.1.1"
chacha20-universal "^1.0.4"
nanoassert "^2.0.0"
sha256-universal "^1.1.0"
sha512-universal "^1.1.0"
siphash24 "^1.0.1"
xsalsa20 "^1.0.0"
sodium-native@^3.1.1, sodium-native@^3.2.0, sodium-native@^3.3.0:
version "3.3.0"
resolved "https://registry.yarnpkg.com/sodium-native/-/sodium-native-3.3.0.tgz#50ee52ac843315866cce3d0c08ab03eb78f22361"
integrity sha512-rg6lCDM/qa3p07YGqaVD+ciAbUqm6SoO4xmlcfkbU5r1zIGrguXztLiEtaLYTV5U6k8KSIUFmnU3yQUSKmf6DA==
dependencies:
node-gyp-build "^4.3.0"
sodium-secretstream@^1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/sodium-secretstream/-/sodium-secretstream-1.0.2.tgz#ae6fec16555f1a1d9fd2460b41256736d5044e13"
integrity sha512-AsWztbBHhHid+w5g28ftXA0mTrS52Dup7FYI0GR7ri1TQTlVsw0z//FNlhIqWsgtBctO/DxQosacbElCpmdcZw==
dependencies:
b4a "^1.1.1"
sodium-universal "^3.0.4"
sodium-universal@^3.0.0, sodium-universal@^3.0.4:
version "3.1.0"
resolved "https://registry.yarnpkg.com/sodium-universal/-/sodium-universal-3.1.0.tgz#f2fa0384d16b7cb99b1c8551a39cc05391a3ed41"
integrity sha512-N2gxk68Kg2qZLSJ4h0NffEhp4BjgWHCHXVlDi1aG1hA3y+ZeWEmHqnpml8Hy47QzfL1xLy5nwr9LcsWAg2Ep0A==
dependencies:
blake2b "^2.1.1"
chacha20-universal "^1.0.4"
nanoassert "^2.0.0"
resolve "^1.17.0"
sha256-universal "^1.1.0"
sha512-universal "^1.1.0"
siphash24 "^1.0.1"
sodium-javascript "~0.8.0"
sodium-native "^3.2.0"
xsalsa20 "^1.0.0"
source-map-support@^0.5.6:
version "0.5.20"
resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.20.tgz#12166089f8f5e5e8c56926b377633392dd2cb6c9"
@ -5000,6 +5288,14 @@ streamsearch@0.1.2:
resolved "https://registry.yarnpkg.com/streamsearch/-/streamsearch-0.1.2.tgz#808b9d0e56fc273d809ba57338e929919a1a9f1a"
integrity sha1-gIudDlb8Jz2Am6VzOOkpkZoanxo=
streamx@^2.10.2, streamx@^2.10.3, streamx@^2.12.0, streamx@^2.12.4:
version "2.12.4"
resolved "https://registry.yarnpkg.com/streamx/-/streamx-2.12.4.tgz#0369848b20b8f79c65320735372df17cafcd9aff"
integrity sha512-K3xdIp8YSkvbdI0PrCcP0JkniN8cPCyeKlcZgRFSl1o1xKINCYM93FryvTSOY57x73pz5/AjO5B8b9BYf21wWw==
dependencies:
fast-fifo "^1.0.0"
queue-tick "^1.0.0"
string-length@^4.0.1:
version "4.0.2"
resolved "https://registry.yarnpkg.com/string-length/-/string-length-4.0.2.tgz#a8a8dc7bd5c1a82b9b3c8b87e125f66871b6e57a"
@ -5105,6 +5401,11 @@ supports-hyperlinks@^2.0.0:
has-flag "^4.0.0"
supports-color "^7.0.0"
supports-preserve-symlinks-flag@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09"
integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==
symbol-observable@^1.0.4:
version "1.2.0"
resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.2.0.tgz#c22688aed4eab3cdc2dfeacbb561660560a00804"
@ -5154,6 +5455,16 @@ throat@^6.0.1:
resolved "https://registry.yarnpkg.com/throat/-/throat-6.0.1.tgz#d514fedad95740c12c2d7fc70ea863eb51ade375"
integrity sha512-8hmiGIJMDlwjg7dlJ4yKGLK8EsYqKgPWbG3b4wjJddKNwc7N7Dpn08Df4szr/sZdMVeOstrdYSsqzX6BYbcB+w==
time-ordered-set@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/time-ordered-set/-/time-ordered-set-1.0.2.tgz#3bd931fc048234147f8c2b8b1ebbebb0a3ecb96f"
integrity sha512-vGO99JkxvgX+u+LtOKQEpYf31Kj3i/GNwVstfnh4dyINakMgeZCpew1e3Aj+06hEslhtHEd52g7m5IV+o1K8Mw==
timeout-refresh@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/timeout-refresh/-/timeout-refresh-2.0.1.tgz#f8ec7cf1f9d93b2635b7d4388cb820c5f6c16f98"
integrity sha512-SVqEcMZBsZF9mA78rjzCrYrUs37LMJk3ShZ851ygZYW1cMeIjs9mL57KO6Iv5mmjSQnOe/29/VAfGXo+oRCiVw==
tmpl@1.0.x:
version "1.0.5"
resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.5.tgz#8683e0b902bb9c20c4f726e3c0b69f36518c07cc"
@ -5348,6 +5659,16 @@ typescript@^4.3.4:
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.4.3.tgz#bdc5407caa2b109efd4f82fe130656f977a29324"
integrity sha512-4xfscpisVgqqDfPaJo5vkd+Qd/ItkoagnHpufr+i2QCHBsNYp+G7UAoyFl8aPtx879u38wPV65rZ8qbGZijalA==
udx-native@^1.1.0:
version "1.2.1"
resolved "https://registry.yarnpkg.com/udx-native/-/udx-native-1.2.1.tgz#a229b8bfab8c9c9eea05c7e0d68e671ab70d562d"
integrity sha512-hLoJ3rE1PuqO/A1YENG8oYNuAGltdwXofzavYwXbg2yk/qQgGBDpUQd/qtdENxkawad5cEEdJEdwvchslDl7OA==
dependencies:
b4a "^1.5.0"
napi-macros "^2.0.0"
node-gyp-build "^4.4.0"
streamx "^2.12.0"
unbox-primitive@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.0.1.tgz#085e215625ec3162574dc8859abee78a59b14471"
@ -5603,6 +5924,11 @@ write-file-atomic@^3.0.0:
resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.5.tgz#8b4bc4af518cfabd0473ae4f99144287b33eb881"
integrity sha512-BAkMFcAzl8as1G/hArkxOxq3G7pjUqQ3gzYbLL0/5zNkph70e+lCoxBGnm6AW1+/aiNeV4fnKqZ8m4GZewmH2w==
xache@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/xache/-/xache-1.1.0.tgz#afc20dec9ff8b2260eea03f5ad9422dc0200c6e9"
integrity sha512-RQGZDHLy/uCvnIrAvaorZH/e6Dfrtxj16iVlGjkj4KD2/G/dNXNqhk5IdSucv5nSSnDK00y8Y/2csyRdHveJ+Q==
xdg-basedir@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/xdg-basedir/-/xdg-basedir-4.0.0.tgz#4bc8d9984403696225ef83a1573cbbcb4e79db13"
@ -5618,6 +5944,11 @@ xmlchars@^2.2.0:
resolved "https://registry.yarnpkg.com/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb"
integrity sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==
xsalsa20@^1.0.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/xsalsa20/-/xsalsa20-1.2.0.tgz#e5a05cb26f8cef723f94a559102ed50c1b44c25c"
integrity sha512-FIr/DEeoHfj7ftfylnoFt3rAIRoWXpx2AoDfrT2qD2wtp7Dp+COajvs/Icb7uHqRW9m60f5iXZwdsJJO3kvb7w==
xss@^1.0.8:
version "1.0.10"
resolved "https://registry.yarnpkg.com/xss/-/xss-1.0.10.tgz#5cd63a9b147a755a14cb0455c7db8866120eb4d2"

View File

@ -0,0 +1,95 @@
import Decimal from 'decimal.js-light'
import {
BaseEntity,
Column,
Entity,
PrimaryGeneratedColumn,
DeleteDateColumn,
JoinColumn,
ManyToOne,
OneToMany,
} from 'typeorm'
import { DecimalTransformer } from '../../src/typeorm/DecimalTransformer'
import { User } from '../User'
import { ContributionMessage } from '../ContributionMessage'
@Entity('contributions')
export class Contribution extends BaseEntity {
@PrimaryGeneratedColumn('increment', { unsigned: true })
id: number
@Column({ unsigned: true, nullable: false, name: 'user_id' })
userId: number
@ManyToOne(() => User, (user) => user.contributions)
@JoinColumn({ name: 'user_id' })
user: User
@Column({ type: 'datetime', default: () => 'CURRENT_TIMESTAMP', name: 'created_at' })
createdAt: Date
@Column({ type: 'datetime', nullable: false, name: 'contribution_date' })
contributionDate: Date
@Column({ length: 255, nullable: false, collation: 'utf8mb4_unicode_ci' })
memo: string
@Column({
type: 'decimal',
precision: 40,
scale: 20,
nullable: false,
transformer: DecimalTransformer,
})
amount: Decimal
@Column({ unsigned: true, nullable: true, name: 'moderator_id' })
moderatorId: number
@Column({ unsigned: true, nullable: true, name: 'contribution_link_id' })
contributionLinkId: number
@Column({ unsigned: true, nullable: true, name: 'confirmed_by' })
confirmedBy: number
@Column({ nullable: true, name: 'confirmed_at' })
confirmedAt: Date
@Column({ unsigned: true, nullable: true, name: 'denied_by' })
deniedBy: number
@Column({ nullable: true, name: 'denied_at' })
deniedAt: Date
@Column({
name: 'contribution_type',
length: 12,
nullable: false,
collation: 'utf8mb4_unicode_ci',
})
contributionType: string
@Column({
name: 'contribution_status',
length: 12,
nullable: false,
collation: 'utf8mb4_unicode_ci',
})
contributionStatus: string
@Column({ unsigned: true, nullable: true, name: 'transaction_id' })
transactionId: number
@Column({ nullable: true, name: 'updated_at' })
updatedAt: Date
@DeleteDateColumn({ name: 'deleted_at' })
deletedAt: Date | null
@DeleteDateColumn({ unsigned: true, nullable: true, name: 'deleted_by' })
deletedBy: number
@OneToMany(() => ContributionMessage, (message) => message.contribution)
@JoinColumn({ name: 'contribution_id' })
messages?: ContributionMessage[]
}

View File

@ -1 +1 @@
export { Contribution } from './0051-add_delete_by_to_contributions/Contribution'
export { Contribution } from './0052-add_updated_at_to_contributions/Contribution'

View File

@ -0,0 +1,12 @@
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
/* eslint-disable @typescript-eslint/no-explicit-any */
export async function upgrade(queryFn: (query: string, values?: any[]) => Promise<Array<any>>) {
await queryFn(
`ALTER TABLE \`contributions\` ADD COLUMN \`updated_at\` datetime DEFAULT NULL AFTER \`transaction_id\`;`,
)
}
export async function downgrade(queryFn: (query: string, values?: any[]) => Promise<Array<any>>) {
await queryFn(`ALTER TABLE \`contributions\` DROP COLUMN \`updated_at\`;`)
}

View File

@ -26,7 +26,7 @@ COMMUNITY_REDEEM_CONTRIBUTION_URL=https://stage1.gradido.net/redeem/CL-{code}
COMMUNITY_DESCRIPTION="Gradido Development Stage1 Test Community"
# backend
BACKEND_CONFIG_VERSION=v10.2022-09-20
BACKEND_CONFIG_VERSION=v11.2022-10-27
JWT_EXPIRES_IN=10m
GDT_API_URL=https://gdt.gradido.net
@ -59,6 +59,10 @@ WEBHOOK_ELOPAGE_SECRET=secret
# EventProtocol
EVENT_PROTOCOL_DISABLED=false
## DHT
## if you set this value, the DHT hyperswarm will start to announce and listen
## on an hash created from this tpoic
# DHT_TOPIC=GRADIDO_HUB
# database
DATABASE_CONFIG_VERSION=v1.2022-03-18
@ -86,4 +90,4 @@ SUPPORT_MAIL=support@supportmail.com
ADMIN_CONFIG_VERSION=v1.2022-03-18
WALLET_AUTH_URL=https://stage1.gradido.net/authenticate?token={token}
WALLET_URL=https://stage1.gradido.net/login
WALLET_URL=https://stage1.gradido.net/login

View File

@ -1,6 +1,25 @@
version: "3.4"
services:
########################################################
# FRONTEND #############################################
########################################################
frontend:
image: gradido/frontend:test
build:
target: test
environment:
- NODE_ENV="test"
########################################################
# ADMIN INTERFACE ######################################
########################################################
admin:
image: gradido/admin:test
build:
target: test
environment:
- NODE_ENV="test"
########################################################
# BACKEND ##############################################
@ -21,15 +40,19 @@ services:
# DATABASE #############################################
########################################################
database:
image: gradido/database:test_up
build:
context: ./database
target: test_up
environment:
- NODE_ENV="test"
# restart: always # this is very dangerous, but worth a test for the delayed mariadb startup at first run
#########################################################
## MARIADB ##############################################
#########################################################
mariadb:
image: gradido/mariadb:test
networks:
- internal-net
- external-net
@ -51,6 +74,12 @@ services:
- external-net
volumes:
- /sessions
#########################################################
## NGINX ################################################
#########################################################
nginx:
image: gradido/nginx:test
networks:
external-net:

View File

@ -0,0 +1,275 @@
# Manuelle User-Registrierung
## Motivation
Bei einer Veranstaltung o.ä. sollen neue Mitglieder geworben werden. Dabei ist ungewiss, ob sie ein Endgerät dabei haben bzw. dieses korrekt bedienen können (QR-Code, E-Mail-Zugang etc.). Es soll nun ohne Einsatz zusätzlicher Technologien eine schnelle und unkomplizierte Möglichkeit geschaffen werden, dass ein Moderator im Admin-Interface zusätzliche Funktionen zur Unterstützung des User-Registrierungsprozesses erhält:
1. manuelle Aktivierung eines User-Accounts ohne Email-Bestätigung und setzen eines (vorläufigen) Passworts
2. vollständige User-Registrierung mit Daten-Erfassung, Account-Aktivierung und setzen eines (vorläufigen) Passworts
## 1. Unterstützung einer User-Registrierung
Ein neuer User hat schon selbständig mit seiner Registrierung bei Gradido begonnen, aber in dem Moment keinen Zugriff auf seine Emails. Somit kann er seine erhaltene Bestätigungs-Email mit dem Link zur Konto-Aktivierung nicht abrufen und die Registrierung nicht abschließen.
Für diesen Fall wird im Admin-Interface eine neue Funktionalität zur "manuellen Aktivierung eines User-Accounts" bereitgestellt. Diese "manuelle Aktivierung" durch den Admin soll den neuen User kurzfristig ermächtigen auf sein Konto zugreifen zu können. Das heißt, dieser Admin-Prozess muss
* das Konto des neuen Users als aktiviert kennzeichnen
* für den User ein (vorläufiges) Passwort generieren
* die Kennzeichnung beibehalten, dass die Email-Adresse des Users noch nicht bestätigt ist
* dem User ein Login-Prozess ermöglichen, in dem der User das (vorläufige) Passwort verwenden kann
* den User nach dem erfolgreichen Login mit dem (vorläufigen) Passwort direkt zur Eingabe eines eigenen Passwortes bringen
### 1.1 Starten des Registrierungsunterstützungs-Prozesses
#### Vorbedingungen
Nach dem der neue User für sich schon die Erfassung seiner persönlichen Daten im Registrierungsdialog durchgeführt und gespeichert hat, schickt die Anwendung dem User eine Confirmation-Email an seine angegebene Email-Adresse. Der User kommt aber aktuell nicht an seine Emails bzw. benötigt Unterstützung, wie er jetzt weiter machen soll, um sich anzumelden. Mit diesem Bedarf nach Unterstützung wendet der User sich an einen Moderator mit entsprechenden Admin-Rechten.
#### Manuelle Aktivierung und One-Time-Passwort
Der Admin navigiert in seinem angemeldeten Gradido-Account auf das Admin-Interface. Dort öffnet er den Dialog "Nutzersuche".
Neu in diesem Dialog sind nun die neuen Checkboxen "noch nicht aktiviertes Konto" und "unbestätigte Email-Adresse". Im Normalfall sind diese beiden Checkboxen nicht selektiert, so dass mit der üblichen Nutzersuche alle User wie bisher ermittelt werden können.
![img](./image/Admin-UserSearch.png)
Um nun schneller einen neuen User mit "noch nicht aktiviertem Konto" und noch "unbestätigter Email-Adresse" für die Registrierungsunterstützung zu finden, kann der Admin die neuen Filter-Checkboxen selektieren. Diese schränken die User-Suche zusätzlich zur üblichen Namens-Eingabe ein, dh. ohne Eingabe eines einschränkenden Namens werden alle User-Accounts gelistet, die ein "noch nicht aktiviertes Konto" und noch eine "unbestätigte Email-Adresse" haben.
![img](./image/Admin-UserSearch_inaktivAccount.png)
Sobald der gewünschte User-Account in der Liste gefunden wurde, kann der Detail-Dialog zu diesem User per Klick geöffnet werden.
![img](./image/Admin-UserAccount-Details.png)
Der geöffnete Detail-Dialog zeigt einen neuen Reiter "Registrierung", in dem die Informationen über das User-Konto stehen: wann wurde es erzeugt und wie ist der Status der "Konto-Aktivierung" und der "Email-Bestätigung".
Der Admin kann nun entweder manuell ein One-Time-Passwort in das Eingabefeld eingeben oder über den "erzeugen"-Button eines kreieren lassen. Dieses wird dann über den Button "speichern & Konto aktivieren" in die Datenbank geschrieben, wobei damit gleichzeitig der Status des User-Kontos auf *aktiviert* gesetzt wird.
Der Admin kann nun das One-Time-Passwort dem User mitteilen, so dass dieser sich über den Login-Prozess in seinen Account ohne vorherige Email-Bestätigung anmelden kann. Der Login-Prozess mit einem One-Time-Passwort muss nach erfolgreicher Anmeldung den User sofort auf den Passwort-Ändern-Dialog führen, um den User direkt die Möglichkeit zu geben sein eigenes Passwort zu vergeben.
Mit Öffnen des Passwort-Ändern-Dialogs für einen User-Account mit One-Time-Passwort kann nicht mit Sicherheit davon ausgegangen werden, dass der User selbst der Datenschutzerklärung zugestimmt hat - dies könnte durch die Unterstützung der Moderators beim User untergegangen sein. Daher muss in diesem Fall in dem Dialog eine Checkbox zur Bestätigung der Datenschutzerklärung eingeblendet sein. Erst wenn der User sein neues Passwort gemäß den Passwort-Richtlinien gesetzt und den Datenschutzbestimmungen zugestimmt hat, wird der "Speichern"-Button enabled und die Daten können gespeichert werden.
#### One-Time-Passwort anzeigen oder ändern
Falls ein neuer User sein erhaltenes One-Time-Passwort noch nicht für einen Login verwendet hat und dieses erneut vom Admin erfragen möchte, dann kann der Administrator dieses erneut im Admin-Interface über die Nutzer-Suche anzeigen lassen. Dazu kann er die Filter-Checkbox "noch nicht aktiviertes Konto" deselektieren, aber die Checkbox "unbestätigte Email-Adresse" selektiert lassen. Dann bekommt er alle User deren Email-Bestätigung noch offen ist und nur User-Konten, die schon aktiviert sind.
![img](./image/Admin-UserAccount-ActivatedOneTimePasswort.png)
Beim Öffnen der Userkonto-Details im Reiter "Registrierung" ist dann zu sehen, dass das "Konto schon aktiv", aber die "Email-Bestätigung noch offen" ist. Im Eingabefeld des One-Time-Passwortes ist das zuvor schon gespeicherte Passwort zu lesen, so dass der Admin dieses dem User erneut mitteilen kann. Der Admin kann aber auch über den "erzeugen"-Button oder manuell das vorhandene Passwort ändern. Über den "speichern"-Button, der aufgrund der vorherigen Konto-Aktivierung nun nicht mehr "speichern & Konto aktivieren" heißt, kann die Passwort-Änderung in die Datenbank geschrieben werden.
### 1.2 Starten einer manuellen Admin-User-Registrierung
Im Admin-Interface wird im Menü ein neuer Reiter "Registrierung" angezeigt. Mit Auswahl dieses Reiters kann der Moderator den Dialog zur "Manuellen User-Registrierung" öffnen.
![img](./image/Admin-CreateUser.png)
Dabei kann der Moderator die Attribute Vorname, Nachname, Email-Adresse und ein One-Time-Passwort eingeben. Mit dem "speichern & Konto aktivieren"-Button wird im Backend zunächst eine Prüfung durchgeführt, ob die eingegebene Email-Adresse ggf. schon von einem anderen existierenden User verwendet wird. Sollte dies der Fall sein, dann wird eine entsprechend aussagekräftige Fehlermeldung ausgegeben und die zuvor eingegebenen Daten werden in dem "Manuelle User-Registrierung" erneut angezeigt. Sind alle Daten soweit valide, dann werden die eingegebenen Daten in der Datenbank gespeichert und der Konto-Status auf *aktiviert* gesetzt.
Es wird auch hier eine Email zur Emailadress-Bestätigung verschickt. Der Status "email_checked" bleibt auf false, weil der User seine Confirmation-Email zwar bekommen, aber noch nicht bestätigt hat oder eben nicht zeitnah bestätigen kann. Durch das One-Time-Passwort, das der Moderator dem User mitteilen kann, hat der User direkt die Möglichkeit sich über den Login-Prozess anzumelden, ohne vorher den Email-Bestätigungslink aktivieren zu müssen.
### 1.3 User-Login mit One-Time-Passwort
Sobald der User selbst oder durch den Moderator ein neues User-Konto angelegt und ein One-Time-Passwort vergeben ist, dann kann der User selbst sich über den üblichen Login-Prozess anmelden.
Die Anwendung erkennt, dass der Login über ein One-Time-Passwort erfolgte, so dass der User direkt nach dem erfolgreichen Login auf die Passwort-Ändern-Seite geführt wird.
![img](./image/One-Time-Passwort-Login.png)
Auf dieser Seite muss der User dann sein neues, nur ihm persönlich bekanntes Passwort eingeben und zur Kontrolle wiederholen. Da der User-Account über eine One-Time-Passwort Registrierung erstellt wurde, hatte der User sehr wahrscheinlich nicht die Gelegenheit der Datenschutzerklärung selbst zuzustimmen. Daher wird hier im Passwort-Ändern-Dialog dies nachgeholt, indem erst mit der Zustimmung zur Datenschutzerklärung der "Passwort ändern"-Button aktiviert wird.
## 2. Implementierung und Anpassungen
### 2.1 Datenbank
Für diese fachlichen Anforderungen müssen folgende Informationen neu in der Datenbank aufgenommen und gespeichert werden:
* Merkmal, dass für den User ein Konto vorhanden, aber dieses noch nicht aktiviert ist
* Merkmal, dass für den User ein Konto vorhanden und dieses schon aktiviert ist
* One-Time-Passwort, das von der Anwendung im Original angezeigt werden kann - unverschlüsselt oder ohne Interaktion zu entschlüsseln
* Merkmal, ob bzw. wann der User der Datenschutzerklärung zugestimmt hat
Es stellt sich die Frage, ob mit diesem UseCase gleich die schon sowieso geplante neue Tabelle `accounts `erstellt wird oder die obigen Merkmale erst einmal in die users-Tabelle einfließen?
**Empfehlung:** erstellen der `accounts`-Tabelle
In dieser `accounts`-Tabelle werden dann alle Account spezifischen Daten gespeichert und ein `accounts`-Eintrag ist über die Spalte `user_id` dem User in der `users`-Tabelle zugeordnet.
Ansonsten werden aber keine weiteren Datenbank-Migrationen, wie Zuordnung der Transaktionen oder Contributions zur `accounts`-Tabelle durchgeführt. Dies muss in einem separaten Issue migriert werden.
#### accounts-Tabelle
| Column | Type | Description |
| ----------------- | ---------------- | --------------------------------------------------------------------------------------------------------------------------- |
| id | unsigned int(10) | technical unique key |
| user_id | unsigned int(10) | foreign key to users entry |
| type | enum | account type: AGE (default), AGW, AUF |
| created_at | datetime(3) | the point of time the entry was created |
| activated | tinyint(4) | switch if account is active or inactive |
| creations_allowed | tinyint(4) | switch if account allows to create gradidos or not; necessary for type AGW and AUF |
| decay | tinyint(4) | switch if account supports decay or not; in case the GDT will be shiftet as a separate account type here in the application |
| balance | decimal(40, 20) | amount of gradidos at the updated_at point of time |
| updated_at | datetime(3) | the point of time the entry was updated, especially important for the balance |
Die letzten vier Spalten sind ersteinmal rein informativ, was ein `accounts`-Eintrag zukünftig enthalten wird und für diesen Usecase optional. Sie könnten auch auf ein zukünftiges Migrations-Issue verschoben werden.
#### users-tabelle
| Column | Type | Description |
| ------------------------ | ----------- | ------------------------------------------------------------------------------------------------------- |
| privacy_policy_at | datetime(3) | point of time the user agreed with the privacy policy - during registration or one-time-password change |
| password_encryption_type | enum | defines the type of encrypting the password: 0 = one-time, 1 = email (default), 2 = gradidoID, ... |
Um zu vermeiden, dass in Bezug auf das One-Time-Passwort und der anstehenden Migration der Passwort-Verschlüsselung ohne Email und stattdessen per GradidoID, es hier zu unnötigen Tabellen-Migrationen kommt, wird mit diesem Usecase die Spalte *password_encryption_type* eingeführt. Damit ist dann erkennbar, ob es sich bei dem gespeicherten Passwort um ein One-Time-Passwort handelt oder um ein anderweitig verschlüsseltes Passwort.
Sollte das Issue zur Migration der Passwort-Verschlüsselung schon vor diesem Usecase umgesetzt sein, dann existiert in der `users`-Tabelle schon die Spalte `passphrase_encryption_type`. Dann sollte diese in `password_encryption_type` umbenannt und dem Enum der Wert 0 für One-Time-Passwort hinzugefügt werden. Die Bezeichnung *passphrase_encryption_type* ist irreführend, da in der Tabelle eine Spalte `passphrase `existiert. Doch die Verschlüsselung wird auf die Spalte `password `und nicht auf `passphrase` angewendet.
#### Migration
Mit den zuvor beschriebenen Datenbankänderungen muss eine Datenbankmigration auf die bestehenden Daten durchgeführt werden. Nachdem die strukturellen Änderungen wie neue `accounts`-Tabelle anlegen und bestehende `users`-Tabelle ändern durchgeführt wurde, erfolgt nun die eigentliche Migration der Daten:
* erzeugen der neuen `accounts`-Tabelle wie oben beschrieben
* ändern der bestehenden `users`-Tabelle wie oben beschrieben mit folgenden Default-Initialisierungen
* privacy_policy_at = created_at
* passwort_encryption_type = Enum `PasswordEncryptionType.EMAIL` oder Wert=1
* Insert pro Eintrag aus der `users`-Tabelle jeweils einen Eintrag in die `accounts`-Tabelle mit folgenden Initialisierungen:
* `accounts.user_id` = `users.id`
* `accounts.type` = Enum `AccountType.AGE`
* `accounts.created_at` = `users.created_at`
* `accounts.activated` = `users.emailContact.email_checked`
* `accounts.creations_allowed` = TRUE (weil es ein account type = AGE ist)
* `accounts.decay` = TRUE (weil es ein account type = AGE ist)
* `accounts.balance` = null (dieses Attribut wird in separatem Issue "Update Account-Balance during writing a Transaction" bedient)
* `account.updated_at` = null (dieses Attribut wird in separatem Issue "Update Account-Balance during writing a Transaction" bedient)
### 2.2 Admin-Interface
#### searchUsers
Der Service *AdminResolver.searchUsers* muss die Filterkriterien "aktiviertes Konto" und "bestätigte Email" getrennt von einander unterstützen. Bisher gibt es in den *SearchUserFilters* das Filterkriterium "byActivated", doch dieses wird auf das Flag `email_checked` in der `user_contacts`-Tabelle angewendet. Das entspricht aber dann dem FilterKriterium "bestätigte Email".
Somit muss das schon existierende Fitlerkriterium "aktiviertes Konto" auf die Spalte "`activated`" in der `accounts`-Tabelle angewendet werden und ein zusätzliches Filterkriterium "bestätigte Email", das auf die Spalte `email_checked` in der `user_contacts`-Tabelle filtert.
Der ErgebnisTyp `SearchUsersResult `des Service *searchUsers* muss um die Informationen erweitert werden, die in dem oben aufgezeigten Detail-Dialog der *Nutzer-Suche* auf dem Reiter "Registrierung" zur Anzeige gebracht werden:
* Zeitpunkt der Konto-Erstellung (`accounts.created_at`)
* Status des Kontos (`accounts.activated`)
* Status der Email-Bestätigung (`user_contacts.email_checked`)
* falls `users.password_encryption_type` = 0, dann das One-Time-Passwort (`users.password`)
#### adminCreateUser
Im *AdminResolver* muss aus Berechtigungsgründen ein neuer Service *adminCreateUser* erstellt werden, da im *UserResolver* der Service *createUser* für jeden offen ist, ohne dass eine vorherige Authentifizierung per Login stattgefunden hat.
Dieser neue Service benötigt folgende Signatur als Eingabeparameter:
| Argument | Type | Bezeichnung |
| --------------- | ------ | ------------------------------------- |
| vorname | String | der Vorname des neuen Users |
| nachname | String | der Nachname des neuen Users |
| email | String | die Email-Adresse des neuen Users |
| oneTimePassword | String | das One-Time-Passwort des neuen Users |
Der neue Service entspricht der internen Logik weitestgehend dem exitierenden Service `UserResolver.create`.
* prüfen ob Email schon existiert und wenn ja, dann an diese Email eine Info-Nachricht und Abruch mit Fehlermeldung
* neues User-Objekt initialisieren mit
* GradidoID
* Vorname
* Nachname
* One-Time-Passwort mit gleichzeitigem Setzen von `password_encryption_type` = Enum `PasswordEncryptionType.ONETIME`
* das neue User-Objekt speichern
* neues UserContact-Objekt initialisieren mit
* Email
* vorherige userID
* das neue UserContact-Objekt speichern
* die erhaltene ID des neuen UserContact-Eintrags in den vorher erzeugten User-Eintrag als `emailId` schreiben
* einen EventProtokoll-Eintrag schreiben vom Typ *EventAdminRegister*, der neu anzulegen ist und von `EventBasicUserId `abgeleitet wird, aber zusätzlich die *UserId* des Moderators in das Attribut `xUserId `einträgt.
* die Confirmation-Email zur Bestätigung der Email-Adresse verschicken
* alle fachlich sonst notwendigen Eventprotokolle schreiben
Alle logischen Schritte bzgl. einer PublisherID oder eines Redeem-Links bleiben hier in diesem Service aussen vor.
Als Rückgabe sind erst einmal keine weiteren fachlichen Daten geplant, ausser einem Boolean=TRUE für eine evtl. Erfolgsmeldung. Im Fehlerfall wird der Service mit einer Exception beendet.
#### adminUpdateUser
Im *AdminResolver* wird der neue Service *adminUpdateUser* eingeführt, um für einen schon existierenden User das One-Time-Passwort zu aktualisieren. Über die vorher durchgeführte Nutzer-Suche sind die aktuell gespeicherten Userdaten schon ermittelt worden. Damit ergibt sich als Signatur für diesen Service folgendes:
| Argument | Typ | Beschreibung |
| -------- | ------ | -------------------------------------------------------- |
| userId | number | der technisch eindeutige Identifer des betroffenen Users |
| password | String | das geänderte One-Time-Passwort |
Dieser Service führt mit der übergebenen *userId* ein update auf dem *User* aus. Dazu wird bei der Aktualisierung das Kriterium `passwort_encryption_type` = Enum `PasswordEncryptionType.ONETIME` sichergestellt und das Attribut `password `mit dem übergebenen Parameter *password* sowie das Flag `activated `= TRUE gesetzt. Abschließend erfolgt das Schreiben eines EventProtokoll-Eintrags vom Typ *EventAdminPasswortChange*, der neu anzulegen ist und von `EventBasicUserId `abgeleitet wird, aber zusätzlich die *UserId* des Moderators in das Attribut `xUserId `einträgt.
Als Rückgabe sind erst einmal keine weiteren fachlichen Daten geplant, ausser einem Boolean=TRUE für eine evtl. Erfolgsmeldung. Im Fehlerfall wird der Service mit einer Exception beendet.
### 2.3 User-Interface
#### login
Im *UserResolver* muss der Service *login* angepasst werden, um eine Anmeldung per One-Time-Passwort zu erlauben.
Dabei wird zuerst per übergebener *email* der User aus der Datenbank ermittelt. Bevor die Prüfung auf das Flag `user.emailContact.email_checked` erfolgt, muss eine Prüfung auf das Attribut `user.password_encryption_type` durchgeführt werden. Ist die Passwort-Verschlüsselung dieses Users auf dem Wert `PasswordEncryptionType.ONETIME`, dann wird die Prüfung des Flags `user.emailContact.email_checked` übersprungen.
Durch den Wert des Attributs `user.password_encryption_type` wird die Passwort-Entschlüsselungsart und Prüfung gesteuert. Beim Wert `PasswordEncryptionType.ONETIME` ist das Passwort selbst für die Anwendung kein Geheimnis, da dieses durch einen Moderator und nicht geheim durch den User eingegeben wurde und jederzeit durch einen Moderator im Klartext wieder angezeigt werden kann.
Wenn zuvor es sich um ein Login per One-Time-Passwort handelte, dann erfolgt keine Überprüfung des EloPage-Status und Aktuallisierung der PublisherId.
Mit erfolgreicher Beendigung des Login-Service wird der User mit seinen aktuellen Attributwerten zurückgeliefert. Dabei ist nun im Frontend sicherzustellen, dass wenn im User das Attribut `user.password_encryption_type` den Wert `PasswordEncryptionType.ONETIME` hat, dass dann mit Verlassen des Login-Dialogs der Anwender direkt nur auf die Passwort-Ändern-Seite geführt wird. Dem Einstieg in den Passwort-Ändern-Dialog muss aus dem Login-Dialog die Information mitgeteilt werden, dass es sich hier um ein One-Time-Passwort Login handelte, damit der Passwort-Ändern-Dialog die entsprechenden Änderungen in Bezug auf diesen UseCase durchführen kann.
#### changePassword
Im *UserResolver* ist der neue Service *changePassword* zu erstellen. Dieser bekommt als Eingabesignatur folgende Parameter:
| Argument | Type | Bezeichnung |
| ------------- | ------- | ----------------------------------------------------------------------------------------- |
| userId | number | eindeutiger Identifier des Users, der zuvor beim Login an das Frontend übermittelt wurde |
| oldPassword | String | altes Passwort des angemeldeten Users |
| newPassword | String | neues Passwort des angemeldeten Users |
| privacyPolicy | boolean | optional: Flag zur Zustimmung der Datenschutzerklärung |
Die übergebenen Parameter müssen im ersten Schritt auf ihre Validität untersucht werden. Das heißt sind alle mandatorischen Parameter übergeben und entsprechen ihre Werte den formellen Anforderungen (z.B. sind die Passworte gemäß den Passwort-Regeln).
Mit der übergebenen *userId* wird der zugehörige User aus der Datenbank ermittelt. Bevor die eigentliche Logik zur Passwort-Änderung durchgeführt wird, erfolgt zuvor noch eine Prüfung auf den Userkonto-Status im Attribut `accounts.activated`. Nur wenn das Userkonto im Status *aktiviert* - per One-Time-Passwort durch den Moderator oder per Email-Confirmation durch den User selbst - ist, kann das Passwort geändert werden.
In Abhängigkeit des im User gespeicherten Typs der Passwort-Verschlüsselung, muss das übergebene *oldPassword* gegen die im User gespeicherten Passwort-Daten überprüft werden:
* *EncryptionType = One-Time-Passwort:* das im User gespeicherte Passwort muss dem übergebenen *oldPassword* entsprechen. Je nach technischer Implementierung könnte ggf. eine Entschlüsselungslogik auf das gespeicherte Passwort notwendig sein, da ein Passwort niemals im Klartext in der Datenbank gespeichert sein soll. Das heißt das im User gespeicherte Passwort muss nach einer evtl. Entschlüsselung mit dem übergebenen Passwort übereinstimmen. Ein One-Way-Hashing ist zwar sicherer aber reicht hier nicht, da das One-Time-Passwort im Admin-Interface im Klartext angezeigt werden können muss.
* *EncryptionType = EMAIL:* das im User gespeicherte Passwort wurde mit der im User gespeicherten Email als Salt verschlüsselt und als EmailHash gespeichert. Daher muss mit der im User gespeichert Email und dem übergebenen *oldPassword* ein EmailHash erzeugt werden, um diesen mit dem im User gespeicherten Emailhash zu vergleichen.
* *EncryptionType = GradidoID:* das im User gespeicherte Passwort wurde mit der im User gespeicherten *GradidoID* als Salt verschlüsselt und als EmailHash gespeichert. Daher muss mit der im User gespeicherten *GradidoID* und dem übergebenen *oldPassword* ein EmailHash erzeugt werden, um diesen mit dem im User gespeicherten EmailHash zu vergleichen.
Ist die Überprüfung des alten Passwortes in Abhängigkeit des Verschlüsselungstyps erfolgreich, dann kann mit der Verschlüsselung des *newPassword* weiter gemacht werden. Als Verschlüsselungstyp wird immer der höchsten Wert des Verschlüsselungs-Enums eingesetzt, auch wenn zuvor ein niedrigerer Typ verwendet wurde. Damit erfolgt immer ein implizites Upgrade auf den aktuellsten Verschlüsselungstyp. Somit wird das übergebene *newPassword* mit der *GradidoID* des Users zu einem EmailHash verschlüsselt und zusammen mit dem Attribut `user.password_encryption_type` = `PasswordEncryptionType.GRADIDO_ID` in die Datenbank geschrieben. Je nach Wert des übergebenen Flags `privacyPolicy` wird im User das Attribut `privacy_policy_at` mit dem aktuellen Zeitpunkt ebenfalls aktualisiert.
War die Passwort-Änderung erfolgreich wird ein boolean=TRUE zurückgegeben, ansonsten eine Exception mit aussagekräftiger Fehlermeldung.
## Brainstorming von Bernd
Damit wir ohne zusätzliche Technologie möglichst schnell und unkompliziert eine Lösung bekommen, dass wir neue User direkt vor Ort registrieren können, schlage ich folgende zwei Funktionen im Admin-Bereich vor:
1. Manuell bestätigen und (vorläufiges) Passwort setzen
2. Neuen User registrieren
### Usecase
Bei einer Veranstaltung o.ä. sollen neue Mitglieder geworben werden. Dabei ist ungewiss, ob sie ein Endgerät dabei haben bzw. dieses korrekt bedienen können (QR-Code, E-Mail-Zugang etc.)
#### Lösung:
Bei der Veranstaltung ist ein Moderator vor Ort, oder der Veranstalter bekommt vorübergehend Moderatoren-Rechte.
Der Moderator hat auf einem Browser sein Gradido-Konto (Admin-Interface) laufen. Auf einem anderen Browser (oder einem anderen Gerät) können sich ggf. User einloggen.
##### Variante 1:
Der Interessent registriert sich über Link/QR-Code, hat aber keinen Zugang zu seinen E-Mails. Der Moderator bestätigt ihn und gibt ihm ein vorläufiges Passwort (oder lässt den User im Backend selbst ein Passwort eintippen).
##### Variante 2:
Der Moderator registriert den Interessenten und gibt ihm ein vorläufiges Passwort (oder lässt den User im Backend selbst ein Passwort eintippen).
Das vorläufige Passwort kann so lange vom Moderator geändert werden, bis der User über die Mail sein Passwort neu gesetzt hat. Dadurch wird erreicht, dass der Moderator den User so lange unterstützen kann (z.B. wenn er sein PW vergessen hat), bis er Mail-Zugang hat und sein Passwort selbst setzen kann.
##### Weitere Anwendungsfälle:
Wenn eine (zukünftige) Community beschließt, dass neue Mitglieder nur durch persönliche Einladung aufgenommen werden. Für diesen Fall müsste dann noch die User-Registrierung abgeschaltet werden können.

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

View File

@ -36,6 +36,7 @@ export default defineConfig({
supportFile: "cypress/support/index.ts",
viewportHeight: 720,
viewportWidth: 1280,
video: false,
retries: {
runMode: 2,
openMode: 0,

View File

@ -207,7 +207,7 @@
},
"language": "Sprache",
"link-load": "den letzten Link nachladen | die letzten {n} Links nachladen | weitere {n} Links nachladen",
"login": "Anmeldung",
"login": "Anmelden",
"math": {
"aprox": "~",
"asterisk": "*",

View File

@ -207,7 +207,7 @@
},
"language": "Language",
"link-load": "Load the last link | Load the last {n} links | Load more {n} links",
"login": "Login",
"login": "Sign in",
"math": {
"aprox": "~",
"asterisk": "*",
@ -289,7 +289,7 @@
"heading": "Please enter the email address by which you're registered here."
},
"login": {
"heading": "Log in with your access data. Keep them safe!"
"heading": "Sign in with your access data. Keep them safe!"
},
"resetPassword": {
"heading": "Please enter your password and repeat it."