Merge remote-tracking branch 'origin/master' into

1906-feature-concept-for-gdd-creation-per-linkqr-code
This commit is contained in:
Claus-Peter Hübner 2022-06-01 02:32:00 +02:00
commit 80c3958be6
124 changed files with 3820 additions and 1755 deletions

View File

@ -528,7 +528,7 @@ jobs:
report_name: Coverage Backend report_name: Coverage Backend
type: lcov type: lcov
result_path: ./backend/coverage/lcov.info result_path: ./backend/coverage/lcov.info
min_coverage: 65 min_coverage: 66
token: ${{ github.token }} token: ${{ github.token }}
########################################################################## ##########################################################################

5
.gitignore vendored
View File

@ -1,9 +1,14 @@
<<<<<<< HEAD <<<<<<< HEAD
<<<<<<< HEAD
.dbeaver/* .dbeaver/*
======= =======
.dbeaver .dbeaver
.project .project
>>>>>>> refs/remotes/origin/improve-apollo-logging >>>>>>> refs/remotes/origin/improve-apollo-logging
=======
.dbeaver
.project
>>>>>>> refs/remotes/origin/master
*.log *.log
/node_modules/* /node_modules/*
messages.pot messages.pot

View File

@ -9,21 +9,26 @@ The Corona crisis has fundamentally changed our world within a very short time.
The dominant financial system threatens to fail around the globe, followed by mass insolvencies, record unemployment and abject poverty. Only with a sustainable new monetary system can humanity master these challenges of the 21st century. The Gradido Academy for Bionic Economy has developed such a system. The dominant financial system threatens to fail around the globe, followed by mass insolvencies, record unemployment and abject poverty. Only with a sustainable new monetary system can humanity master these challenges of the 21st century. The Gradido Academy for Bionic Economy has developed such a system.
Find out more about the Project on its [Website](https://gradido.net/). It is offering vast resources about the idea. The remaining document will discuss the gradido software only. Find out more about the Project on its [Website](https://gradido.net/). It is offering vast resources about the idea. The remaining document will discuss the gradido software only.
## Software requirements ## Software requirements
Currently we only support `docker` install instructions to run all services, since many different programming languages and frameworks are used. Currently we only support `docker` install instructions to run all services, since many different programming languages and frameworks are used.
- [docker](https://www.docker.com/) - [docker](https://www.docker.com/)
- [docker-compose] - [docker-compose]
- [yarn](https://phoenixnap.com/kb/yarn-windows)
### For Arch Linux ### For Arch Linux
Install the required packages: Install the required packages:
```bash ```bash
sudo pacman -S docker sudo pacman -S docker
sudo pacman -S docker-compose sudo pacman -S docker-compose
``` ```
Add group `docker` and then your user to it in order to allow you to run docker without sudo Add group `docker` and then your user to it in order to allow you to run docker without sudo
```bash ```bash
sudo groupadd docker # may already exist `groupadd: group 'docker' already exists` sudo groupadd docker # may already exist `groupadd: group 'docker' already exists`
sudo usermod -aG docker $USER sudo usermod -aG docker $USER
@ -31,26 +36,58 @@ groups # verify you have the group (requires relog)
``` ```
Start the docker service: Start the docker service:
```bash ```bash
sudo systemctrl start docker sudo systemctrl start docker
``` ```
### For Windows
#### docker
The installation of dockers depends on your selected product package from the [dockers page](https://www.docker.com/). For windows the product *docker desktop* will be the choice. Please follow the installation instruction of your selected product.
##### known problems
* In case the docker desktop will not start correctly because of previous docker installations, then please clean the used directories of previous docker installation - `C:\Users` - before you retry starting docker desktop. For further problems executing docker desktop please take a look in this description "[logs and trouble shooting](https://docs.docker.com/desktop/windows/troubleshoot/)"
* In case your docker desktop installation causes high memory consumption per vmmem process, then please take a look at this description "[vmmen process consuming too much memory (Docker Desktop)](https://dev.to/tallesl/vmmen-process-consuming-too-much-memory-docker-desktop-273p)"
#### yarn
For the Gradido build process the yarn package manager will be used. Please download and install [yarn for windows](https://phoenixnap.com/kb/yarn-windows) by following the instructions there.
## How to run? ## How to run?
As soon as the software requirements are fulfilled and a docker installation is up and running then open a powershell on Windows or an other commandline prompt on Linux.
Create and navigate to the directory, where you want to create the Gradido runtime environment.
```
mkdir \Gradido
cd \Gradido
```
### 1. Clone Sources ### 1. Clone Sources
Clone the repo and pull all submodules Clone the repo and pull all submodules
```bash ```bash
git clone git@github.com:gradido/gradido.git git clone git@github.com:gradido/gradido.git
git submodule update --recursive --init git submodule update --recursive --init
``` ```
### 2. Run docker-compose ### 2. Run docker-compose
Run docker-compose to bring up the development environment
Run docker-compose to bring up the development environment
```bash ```bash
docker-compose up docker-compose up
``` ```
### Additional Build options ### Additional Build options
If you want to build for production you can do this aswell: If you want to build for production you can do this aswell:
```bash ```bash
docker-compose -f docker-compose.yml up docker-compose -f docker-compose.yml up
``` ```
@ -73,6 +110,7 @@ A release is tagged on Github by its version number and published as github rele
Each release is accompanied with release notes automatically generated from the git log which is available as [CHANGELOG.md](./CHANGELOG.md). Each release is accompanied with release notes automatically generated from the git log which is available as [CHANGELOG.md](./CHANGELOG.md).
To generate the Changelog and set a new Version you should use the following commands in the main folder To generate the Changelog and set a new Version you should use the following commands in the main folder
```bash ```bash
git fetch --all git fetch --all
yarn release yarn release
@ -85,10 +123,10 @@ Note: The Changelog will be regenerated with all tags on release on the external
## Troubleshooting ## Troubleshooting
| Problem | Issue | Solution | Description | | Problem | Issue | Solution | Description |
| ------- | ----- | -------- | ----------- | | ------------------------------------------------ | ---------------------------------------------------- | ----------------------------------------------------------------------------- | --------------------------------------------------------------------------- |
| docker-compose raises database connection errors | [#1062](https://github.com/gradido/gradido/issues/1062) | End `ctrl+c` and restart the `docker-compose up` after a successful build | Several Database connection related errors occur in the docker-compose log. | | docker-compose raises database connection errors | [#1062](https://github.com/gradido/gradido/issues/1062) | End `ctrl+c` and restart the `docker-compose up` after a successful build | Several Database connection related errors occur in the docker-compose log. |
| Wallet page is empty | [#1063](https://github.com/gradido/gradido/issues/1063) | Accept Cookies and Local Storage in your Browser | The page stays empty when navigating to [http://localhost/](http://localhost/) | | Wallet page is empty | [#1063](https://github.com/gradido/gradido/issues/1063) | Accept Cookies and Local Storage in your Browser | The page stays empty when navigating to[http://localhost/](http://localhost/) |
## Useful Links ## Useful Links

View File

@ -4,5 +4,6 @@ module.exports = {
singleQuote: true, singleQuote: true,
trailingComma: "all", trailingComma: "all",
tabWidth: 2, tabWidth: 2,
bracketSpacing: true bracketSpacing: true,
endOfLine: "auto",
}; };

View File

@ -4,7 +4,7 @@
"main": "index.js", "main": "index.js",
"author": "Moriz Wahl", "author": "Moriz Wahl",
"version": "1.8.3", "version": "1.8.3",
"license": "MIT", "license": "Apache-2.0",
"private": false, "private": false,
"scripts": { "scripts": {
"start": "node run/server.js", "start": "node run/server.js",

View File

@ -5,15 +5,13 @@ export const searchUsers = gql`
$searchText: String! $searchText: String!
$currentPage: Int $currentPage: Int
$pageSize: Int $pageSize: Int
$filterByActivated: Boolean $filters: SearchUsersFiltersInput
$filterByDeleted: Boolean
) { ) {
searchUsers( searchUsers(
searchText: $searchText searchText: $searchText
currentPage: $currentPage currentPage: $currentPage
pageSize: $pageSize pageSize: $pageSize
filterByActivated: $filterByActivated filters: $filters
filterByDeleted: $filterByDeleted
) { ) {
userCount userCount
userList { userList {

View File

@ -71,8 +71,10 @@ describe('Creation', () => {
searchText: '', searchText: '',
currentPage: 1, currentPage: 1,
pageSize: 25, pageSize: 25,
filterByActivated: true, filters: {
filterByDeleted: false, filterByActivated: true,
filterByDeleted: false,
},
}, },
}), }),
) )
@ -271,8 +273,10 @@ describe('Creation', () => {
searchText: 'XX', searchText: 'XX',
currentPage: 1, currentPage: 1,
pageSize: 25, pageSize: 25,
filterByActivated: true, filters: {
filterByDeleted: false, filterByActivated: true,
filterByDeleted: false,
},
}, },
}), }),
) )
@ -288,8 +292,10 @@ describe('Creation', () => {
searchText: '', searchText: '',
currentPage: 1, currentPage: 1,
pageSize: 25, pageSize: 25,
filterByActivated: true, filters: {
filterByDeleted: false, filterByActivated: true,
filterByDeleted: false,
},
}, },
}), }),
) )
@ -305,8 +311,10 @@ describe('Creation', () => {
searchText: '', searchText: '',
currentPage: 2, currentPage: 2,
pageSize: 25, pageSize: 25,
filterByActivated: true, filters: {
filterByDeleted: false, filterByActivated: true,
filterByDeleted: false,
},
}, },
}), }),
) )

View File

@ -102,8 +102,10 @@ export default {
searchText: this.criteria, searchText: this.criteria,
currentPage: this.currentPage, currentPage: this.currentPage,
pageSize: this.perPage, pageSize: this.perPage,
filterByActivated: true, filters: {
filterByDeleted: false, filterByActivated: true,
filterByDeleted: false,
},
}, },
fetchPolicy: 'network-only', fetchPolicy: 'network-only',
}) })

View File

@ -7,7 +7,7 @@ const localVue = global.localVue
const apolloQueryMock = jest.fn().mockResolvedValue({ const apolloQueryMock = jest.fn().mockResolvedValue({
data: { data: {
searchUsers: { searchUsers: {
userCount: 1, userCount: 4,
userList: [ userList: [
{ {
userId: 1, userId: 1,
@ -82,8 +82,10 @@ describe('UserSearch', () => {
searchText: '', searchText: '',
currentPage: 1, currentPage: 1,
pageSize: 25, pageSize: 25,
filterByActivated: null, filters: {
filterByDeleted: null, filterByActivated: null,
filterByDeleted: null,
},
}, },
}), }),
) )
@ -101,8 +103,10 @@ describe('UserSearch', () => {
searchText: '', searchText: '',
currentPage: 1, currentPage: 1,
pageSize: 25, pageSize: 25,
filterByActivated: false, filters: {
filterByDeleted: null, filterByActivated: false,
filterByDeleted: null,
},
}, },
}), }),
) )
@ -121,8 +125,10 @@ describe('UserSearch', () => {
searchText: '', searchText: '',
currentPage: 1, currentPage: 1,
pageSize: 25, pageSize: 25,
filterByActivated: null, filters: {
filterByDeleted: true, filterByActivated: null,
filterByDeleted: true,
},
}, },
}), }),
) )
@ -141,8 +147,10 @@ describe('UserSearch', () => {
searchText: '', searchText: '',
currentPage: 2, currentPage: 2,
pageSize: 25, pageSize: 25,
filterByActivated: null, filters: {
filterByDeleted: null, filterByActivated: null,
filterByDeleted: null,
},
}, },
}), }),
) )
@ -161,8 +169,10 @@ describe('UserSearch', () => {
searchText: 'search string', searchText: 'search string',
currentPage: 1, currentPage: 1,
pageSize: 25, pageSize: 25,
filterByActivated: null, filters: {
filterByDeleted: null, filterByActivated: null,
filterByDeleted: null,
},
}, },
}), }),
) )
@ -178,8 +188,10 @@ describe('UserSearch', () => {
searchText: '', searchText: '',
currentPage: 1, currentPage: 1,
pageSize: 25, pageSize: 25,
filterByActivated: null, filters: {
filterByDeleted: null, filterByActivated: null,
filterByDeleted: null,
},
}, },
}), }),
) )

View File

@ -97,8 +97,10 @@ export default {
searchText: this.criteria, searchText: this.criteria,
currentPage: this.currentPage, currentPage: this.currentPage,
pageSize: this.perPage, pageSize: this.perPage,
filterByActivated: this.filterByActivated, filters: {
filterByDeleted: this.filterByDeleted, filterByActivated: this.filterByActivated,
filterByDeleted: this.filterByDeleted,
},
}, },
fetchPolicy: 'no-cache', fetchPolicy: 'no-cache',
}) })

View File

@ -49,4 +49,8 @@ EMAIL_CODE_VALID_TIME=1440
EMAIL_CODE_REQUEST_TIME=10 EMAIL_CODE_REQUEST_TIME=10
# Webhook # Webhook
WEBHOOK_ELOPAGE_SECRET=secret WEBHOOK_ELOPAGE_SECRET=secret
# SET LOG LEVEL AS NEEDED IN YOUR .ENV
# POSSIBLE VALUES: all | trace | debug | info | warn | error | fatal
# LOG_LEVEL=info

View File

@ -47,4 +47,4 @@ EMAIL_CODE_VALID_TIME=$EMAIL_CODE_VALID_TIME
EMAIL_CODE_REQUEST_TIME=$EMAIL_CODE_REQUEST_TIME EMAIL_CODE_REQUEST_TIME=$EMAIL_CODE_REQUEST_TIME
# Webhook # Webhook
WEBHOOK_ELOPAGE_SECRET=$WEBHOOK_ELOPAGE_SECRET WEBHOOK_ELOPAGE_SECRET=$WEBHOOK_ELOPAGE_SECRET

View File

@ -5,4 +5,5 @@ module.exports = {
trailingComma: "all", trailingComma: "all",
tabWidth: 2, tabWidth: 2,
bracketSpacing: true, bracketSpacing: true,
endOfLine: "auto",
}; };

102
backend/log4js-config.json Normal file
View File

@ -0,0 +1,102 @@
{
"appenders":
{
"access":
{
"type": "dateFile",
"filename": "../logs/backend/access.log",
"pattern": "%d{ISO8601} %p %c %X{user} %f:%l %m",
"keepFileExt" : true,
"fileNameSep" : "_"
},
"apollo":
{
"type": "dateFile",
"filename": "../logs/backend/apollo.log",
"pattern": "%d{ISO8601} %p %c %m",
"keepFileExt" : true,
"fileNameSep" : "_"
},
"backend":
{
"type": "dateFile",
"filename": "../logs/backend/backend.log",
"pattern": "%d{ISO8601} %p %c %X{user} %f:%l %m",
"keepFileExt" : true,
"fileNameSep" : "_"
},
"errorFile":
{
"type": "dateFile",
"filename": "../logs/backend/errors.log",
"pattern": "%d{ISO8601} %p %c %X{user} %f:%l %m",
"keepFileExt" : true,
"fileNameSep" : "_"
},
"errors":
{
"type": "logLevelFilter",
"level": "error",
"appender": "errorFile"
},
"out":
{
"type": "stdout",
"layout":
{
"type": "pattern", "pattern": "%d{ISO8601} %p %c %X{user} %f:%l %m"
}
},
"apolloOut":
{
"type": "stdout",
"layout":
{
"type": "pattern", "pattern": "%d{ISO8601} %p %c %m"
}
}
},
"categories":
{
"default":
{
"appenders":
[
"out",
"errors"
],
"level": "debug",
"enableCallStack": true
},
"apollo":
{
"appenders":
[
"apollo",
"apolloOut",
"errors"
],
"level": "debug",
"enableCallStack": true
},
"backend":
{
"appenders":
[
"backend",
"out",
"errors"
],
"level": "debug",
"enableCallStack": true
},
"http":
{
"appenders":
[
"access"
],
"level": "info"
}
}
}

View File

@ -5,7 +5,7 @@
"main": "src/index.ts", "main": "src/index.ts",
"repository": "https://github.com/gradido/gradido/backend", "repository": "https://github.com/gradido/gradido/backend",
"author": "Ulf Gebhardt", "author": "Ulf Gebhardt",
"license": "MIT", "license": "Apache-2.0",
"private": false, "private": false,
"scripts": { "scripts": {
"build": "tsc --build", "build": "tsc --build",
@ -19,7 +19,6 @@
"dependencies": { "dependencies": {
"@types/jest": "^27.0.2", "@types/jest": "^27.0.2",
"@types/lodash.clonedeep": "^4.5.6", "@types/lodash.clonedeep": "^4.5.6",
"apollo-log": "^1.1.0",
"apollo-server-express": "^2.25.2", "apollo-server-express": "^2.25.2",
"apollo-server-testing": "^2.25.2", "apollo-server-testing": "^2.25.2",
"axios": "^0.21.1", "axios": "^0.21.1",
@ -33,6 +32,7 @@
"jest": "^27.2.4", "jest": "^27.2.4",
"jsonwebtoken": "^8.5.1", "jsonwebtoken": "^8.5.1",
"lodash.clonedeep": "^4.5.0", "lodash.clonedeep": "^4.5.0",
"log4js": "^6.4.6",
"mysql2": "^2.3.0", "mysql2": "^2.3.0",
"nodemailer": "^6.6.5", "nodemailer": "^6.6.5",
"random-bigint": "^0.0.1", "random-bigint": "^0.0.1",

View File

@ -1,10 +1,14 @@
import axios from 'axios' import axios from 'axios'
import { backendLogger as logger } from '@/server/logger'
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
export const apiPost = async (url: string, payload: unknown): Promise<any> => { export const apiPost = async (url: string, payload: unknown): Promise<any> => {
logger.trace('POST: url=' + url + ' payload=' + payload)
return axios return axios
.post(url, payload) .post(url, payload)
.then((result) => { .then((result) => {
logger.trace('POST-Response: result=' + result)
if (result.status !== 200) { if (result.status !== 200) {
throw new Error('HTTP Status Error ' + result.status) throw new Error('HTTP Status Error ' + result.status)
} }
@ -20,9 +24,11 @@ export const apiPost = async (url: string, payload: unknown): Promise<any> => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
export const apiGet = async (url: string): Promise<any> => { export const apiGet = async (url: string): Promise<any> => {
logger.trace('GET: url=' + url)
return axios return axios
.get(url) .get(url)
.then((result) => { .then((result) => {
logger.trace('GET-Response: result=' + result)
if (result.status !== 200) { if (result.status !== 200) {
throw new Error('HTTP Status Error ' + result.status) throw new Error('HTTP Status Error ' + result.status)
} }

View File

@ -3,7 +3,7 @@ import CONFIG from './index'
describe('config/index', () => { describe('config/index', () => {
describe('decay start block', () => { describe('decay start block', () => {
it('has the correct date set', () => { it('has the correct date set', () => {
expect(CONFIG.DECAY_START_TIME).toEqual(new Date('2021-05-13 17:46:31')) expect(CONFIG.DECAY_START_TIME).toEqual(new Date('2021-05-13 17:46:31-0000'))
}) })
}) })
}) })

View File

@ -11,7 +11,10 @@ Decimal.set({
const constants = { const constants = {
DB_VERSION: '0036-unique_previous_in_transactions', DB_VERSION: '0036-unique_previous_in_transactions',
DECAY_START_TIME: new Date('2021-05-13 17:46:31'), // GMT+0 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: { CONFIG_VERSION: {
DEFAULT: 'DEFAULT', DEFAULT: 'DEFAULT',
EXPECTED: 'v6.2022-04-21', EXPECTED: 'v6.2022-04-21',

View File

@ -1,4 +1,5 @@
import { ArgsType, Field, Int } from 'type-graphql' import { ArgsType, Field, Int } from 'type-graphql'
import SearchUsersFilters from '@arg/SearchUsersFilters'
@ArgsType() @ArgsType()
export default class SearchUsersArgs { export default class SearchUsersArgs {
@ -11,9 +12,6 @@ export default class SearchUsersArgs {
@Field(() => Int, { nullable: true }) @Field(() => Int, { nullable: true })
pageSize?: number pageSize?: number
@Field(() => Boolean, { nullable: true }) @Field(() => SearchUsersFilters, { nullable: true })
filterByActivated?: boolean | null filters: SearchUsersFilters
@Field(() => Boolean, { nullable: true })
filterByDeleted?: boolean | null
} }

View File

@ -0,0 +1,11 @@
import { Field, InputType, ObjectType } from 'type-graphql'
@ObjectType()
@InputType('SearchUsersFiltersInput')
export default class SearchUsersFilters {
@Field(() => Boolean, { nullable: true, defaultValue: null })
filterByActivated?: boolean | null
@Field(() => Boolean, { nullable: true, defaultValue: null })
filterByDeleted?: boolean | null
}

View File

@ -3,11 +3,11 @@ import { ArgsType, Field } from 'type-graphql'
@ArgsType() @ArgsType()
export default class TransactionLinkFilters { export default class TransactionLinkFilters {
@Field(() => Boolean, { nullable: true, defaultValue: true }) @Field(() => Boolean, { nullable: true, defaultValue: true })
withDeleted?: boolean filterByDeleted?: boolean
@Field(() => Boolean, { nullable: true, defaultValue: true }) @Field(() => Boolean, { nullable: true, defaultValue: true })
withExpired?: boolean filterByExpired?: boolean
@Field(() => Boolean, { nullable: true, defaultValue: true }) @Field(() => Boolean, { nullable: true, defaultValue: true })
withRedeemed?: boolean filterByRedeemed?: boolean
} }

View File

@ -19,7 +19,4 @@ export default class UpdateUserInfosArgs {
@Field({ nullable: true }) @Field({ nullable: true })
passwordNew?: string passwordNew?: string
@Field({ nullable: true })
coinanimation?: boolean
} }

View File

@ -1,5 +0,0 @@
enum Setting {
COIN_ANIMATION = 'coinanimation',
}
export { Setting }

View File

@ -15,8 +15,6 @@ export class User {
this.language = user.language this.language = user.language
this.publisherId = user.publisherId this.publisherId = user.publisherId
this.isAdmin = user.isAdmin this.isAdmin = user.isAdmin
// TODO
this.coinanimation = null
this.klickTipp = null this.klickTipp = null
this.hasElopage = null this.hasElopage = null
} }
@ -61,11 +59,6 @@ export class User {
@Field(() => Date, { nullable: true }) @Field(() => Date, { nullable: true })
isAdmin: Date | null isAdmin: Date | null
// TODO this is a bit inconsistent with what we query from the database
// therefore all those fields are now nullable with default value null
@Field(() => Boolean, { nullable: true })
coinanimation: boolean | null
@Field(() => KlickTipp, { nullable: true }) @Field(() => KlickTipp, { nullable: true })
klickTipp: KlickTipp | null klickTipp: KlickTipp | null

View File

@ -1,6 +1,7 @@
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ /* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import { convertObjValuesToArray } from '@/util/utilities'
import { testEnvironment, resetToken, cleanDB } from '@test/helpers' import { testEnvironment, resetToken, cleanDB } from '@test/helpers'
import { userFactory } from '@/seeds/factory/user' import { userFactory } from '@/seeds/factory/user'
import { creationFactory } from '@/seeds/factory/creation' import { creationFactory } from '@/seeds/factory/creation'
@ -11,6 +12,7 @@ import { garrickOllivander } from '@/seeds/users/garrick-ollivander'
import { import {
deleteUser, deleteUser,
unDeleteUser, unDeleteUser,
searchUsers,
createPendingCreation, createPendingCreation,
createPendingCreations, createPendingCreations,
updatePendingCreation, updatePendingCreation,
@ -261,6 +263,224 @@ describe('AdminResolver', () => {
}) })
}) })
describe('search users', () => {
const variablesWithoutTextAndFilters = {
searchText: '',
currentPage: 1,
pageSize: 25,
filters: null,
}
describe('unauthenticated', () => {
it('returns an error', async () => {
await expect(
query({
query: searchUsers,
variables: {
...variablesWithoutTextAndFilters,
},
}),
).resolves.toEqual(
expect.objectContaining({
errors: [new GraphQLError('401 Unauthorized')],
}),
)
})
})
describe('authenticated', () => {
describe('without admin rights', () => {
beforeAll(async () => {
user = await userFactory(testEnv, bibiBloxberg)
await query({
query: login,
variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' },
})
})
afterAll(async () => {
await cleanDB()
resetToken()
})
it('returns an error', async () => {
await expect(
query({
query: searchUsers,
variables: {
...variablesWithoutTextAndFilters,
},
}),
).resolves.toEqual(
expect.objectContaining({
errors: [new GraphQLError('401 Unauthorized')],
}),
)
})
})
describe('with admin rights', () => {
const allUsers = {
bibi: expect.objectContaining({
email: 'bibi@bloxberg.de',
}),
garrick: expect.objectContaining({
email: 'garrick@ollivander.com',
}),
peter: expect.objectContaining({
email: 'peter@lustig.de',
}),
stephen: expect.objectContaining({
email: 'stephen@hawking.uk',
}),
}
beforeAll(async () => {
admin = await userFactory(testEnv, peterLustig)
await query({
query: login,
variables: { email: 'peter@lustig.de', password: 'Aa12345_' },
})
await userFactory(testEnv, bibiBloxberg)
await userFactory(testEnv, stephenHawking)
await userFactory(testEnv, garrickOllivander)
})
afterAll(async () => {
await cleanDB()
resetToken()
})
describe('without any filters', () => {
it('finds all users', async () => {
await expect(
query({
query: searchUsers,
variables: {
...variablesWithoutTextAndFilters,
},
}),
).resolves.toEqual(
expect.objectContaining({
data: {
searchUsers: {
userCount: 4,
userList: expect.arrayContaining(convertObjValuesToArray(allUsers)),
},
},
}),
)
})
})
describe('all filters are null', () => {
it('finds all users', async () => {
await expect(
query({
query: searchUsers,
variables: {
...variablesWithoutTextAndFilters,
filters: {
filterByActivated: null,
filterByDeleted: null,
},
},
}),
).resolves.toEqual(
expect.objectContaining({
data: {
searchUsers: {
userCount: 4,
userList: expect.arrayContaining(convertObjValuesToArray(allUsers)),
},
},
}),
)
})
})
describe('filter by unchecked email', () => {
it('finds only users with unchecked email', async () => {
await expect(
query({
query: searchUsers,
variables: {
...variablesWithoutTextAndFilters,
filters: {
filterByActivated: false,
filterByDeleted: null,
},
},
}),
).resolves.toEqual(
expect.objectContaining({
data: {
searchUsers: {
userCount: 1,
userList: expect.arrayContaining([allUsers.garrick]),
},
},
}),
)
})
})
describe('filter by deleted users', () => {
it('finds only users with deleted account', async () => {
await expect(
query({
query: searchUsers,
variables: {
...variablesWithoutTextAndFilters,
filters: {
filterByActivated: null,
filterByDeleted: true,
},
},
}),
).resolves.toEqual(
expect.objectContaining({
data: {
searchUsers: {
userCount: 1,
userList: expect.arrayContaining([allUsers.stephen]),
},
},
}),
)
})
})
describe('filter by deleted account and unchecked email', () => {
it('finds no users', async () => {
await expect(
query({
query: searchUsers,
variables: {
...variablesWithoutTextAndFilters,
filters: {
filterByActivated: false,
filterByDeleted: true,
},
},
}),
).resolves.toEqual(
expect.objectContaining({
data: {
searchUsers: {
userCount: 0,
userList: [],
},
},
}),
)
})
})
})
})
})
describe('creations', () => { describe('creations', () => {
const variables = { const variables = {
email: 'bibi@bloxberg.de', email: 'bibi@bloxberg.de',

View File

@ -52,23 +52,19 @@ export class AdminResolver {
@Query(() => SearchUsersResult) @Query(() => SearchUsersResult)
async searchUsers( async searchUsers(
@Args() @Args()
{ { searchText, currentPage = 1, pageSize = 25, filters }: SearchUsersArgs,
searchText,
currentPage = 1,
pageSize = 25,
filterByActivated = null,
filterByDeleted = null,
}: SearchUsersArgs,
): Promise<SearchUsersResult> { ): Promise<SearchUsersResult> {
const userRepository = getCustomRepository(UserRepository) const userRepository = getCustomRepository(UserRepository)
const filterCriteria: ObjectLiteral[] = [] const filterCriteria: ObjectLiteral[] = []
if (filterByActivated !== null) { if (filters) {
filterCriteria.push({ emailChecked: filterByActivated }) if (filters.filterByActivated !== null) {
} filterCriteria.push({ emailChecked: filters.filterByActivated })
}
if (filterByDeleted !== null) { if (filters.filterByDeleted !== null) {
filterCriteria.push({ deletedAt: filterByDeleted ? Not(IsNull()) : IsNull() }) filterCriteria.push({ deletedAt: filters.filterByDeleted ? Not(IsNull()) : IsNull() })
}
} }
const userFields = ['id', 'firstName', 'lastName', 'email', 'emailChecked', 'deletedAt'] const userFields = ['id', 'firstName', 'lastName', 'email', 'emailChecked', 'deletedAt']
@ -442,11 +438,11 @@ export class AdminResolver {
} = { } = {
userId, userId,
} }
if (!filters.withRedeemed) where.redeemedBy = null if (!filters.filterByRedeemed) where.redeemedBy = null
if (!filters.withExpired) where.validUntil = MoreThan(new Date()) if (!filters.filterByExpired) where.validUntil = MoreThan(new Date())
const [transactionLinks, count] = await dbTransactionLink.findAndCount({ const [transactionLinks, count] = await dbTransactionLink.findAndCount({
where, where,
withDeleted: filters.withDeleted, withDeleted: filters.filterByDeleted,
order: { order: {
createdAt: order, createdAt: order,
}, },

View File

@ -1,3 +1,5 @@
import { backendLogger as logger } from '@/server/logger'
import { Context, getUser } from '@/server/context' import { Context, getUser } from '@/server/context'
import { Resolver, Query, Ctx, Authorized } from 'type-graphql' import { Resolver, Query, Ctx, Authorized } from 'type-graphql'
import { Balance } from '@model/Balance' import { Balance } from '@model/Balance'
@ -18,15 +20,22 @@ export class BalanceResolver {
const user = getUser(context) const user = getUser(context)
const now = new Date() const now = new Date()
logger.addContext('user', user.id)
logger.info(`balance(userId=${user.id})...`)
const gdtResolver = new GdtResolver() const gdtResolver = new GdtResolver()
const balanceGDT = await gdtResolver.gdtBalance(context) const balanceGDT = await gdtResolver.gdtBalance(context)
logger.debug(`balanceGDT=${balanceGDT}`)
const lastTransaction = context.lastTransaction const lastTransaction = context.lastTransaction
? context.lastTransaction ? context.lastTransaction
: await dbTransaction.findOne({ userId: user.id }, { order: { balanceDate: 'DESC' } }) : await dbTransaction.findOne({ userId: user.id }, { order: { balanceDate: 'DESC' } })
logger.debug(`lastTransaction=${lastTransaction}`)
// No balance found // No balance found
if (!lastTransaction) { if (!lastTransaction) {
logger.info(`no balance found, return Default-Balance!`)
return new Balance({ return new Balance({
balance: new Decimal(0), balance: new Decimal(0),
balanceGDT, balanceGDT,
@ -39,6 +48,8 @@ export class BalanceResolver {
context.transactionCount || context.transactionCount === 0 context.transactionCount || context.transactionCount === 0
? context.transactionCount ? context.transactionCount
: await dbTransaction.count({ where: { userId: user.id } }) : await dbTransaction.count({ where: { userId: user.id } })
logger.debug(`transactionCount=${count}`)
const linkCount = await dbTransactionLink.count({ const linkCount = await dbTransactionLink.count({
where: { where: {
userId: user.id, userId: user.id,
@ -46,6 +57,7 @@ export class BalanceResolver {
// validUntil: MoreThan(new Date()), // validUntil: MoreThan(new Date()),
}, },
}) })
logger.debug(`linkCount=${linkCount}`)
// The decay is always calculated on the last booked transaction // The decay is always calculated on the last booked transaction
const calculatedDecay = calculateDecay( const calculatedDecay = calculateDecay(
@ -53,6 +65,9 @@ export class BalanceResolver {
lastTransaction.balanceDate, lastTransaction.balanceDate,
now, now,
) )
logger.info(
`calculatedDecay(balance=${lastTransaction.balance}, balanceDate=${lastTransaction.balanceDate})=${calculatedDecay}`,
)
// The final balance is reduced by the link amount withheld // The final balance is reduced by the link amount withheld
const transactionLinkRepository = getCustomRepository(TransactionLinkRepository) const transactionLinkRepository = getCustomRepository(TransactionLinkRepository)
@ -60,13 +75,27 @@ export class BalanceResolver {
? { sumHoldAvailableAmount: context.sumHoldAvailableAmount } ? { sumHoldAvailableAmount: context.sumHoldAvailableAmount }
: await transactionLinkRepository.summary(user.id, now) : await transactionLinkRepository.summary(user.id, now)
return new Balance({ logger.debug(`context.sumHoldAvailableAmount=${context.sumHoldAvailableAmount}`)
balance: calculatedDecay.balance logger.debug(`sumHoldAvailableAmount=${sumHoldAvailableAmount}`)
.minus(sumHoldAvailableAmount.toString())
.toDecimalPlaces(2, Decimal.ROUND_DOWN), // round towards zero const balance = calculatedDecay.balance
.minus(sumHoldAvailableAmount.toString())
.toDecimalPlaces(2, Decimal.ROUND_DOWN) // round towards zero
// const newBalance = new Balance({
// balance: calculatedDecay.balance
// .minus(sumHoldAvailableAmount.toString())
// .toDecimalPlaces(2, Decimal.ROUND_DOWN),
const newBalance = new Balance({
balance,
balanceGDT, balanceGDT,
count, count,
linkCount, linkCount,
}) })
logger.info(
`new Balance(balance=${balance}, balanceGDT=${balanceGDT}, count=${count}, linkCount=${linkCount}) = ${newBalance}`,
)
return newBalance
} }
} }

View File

@ -1,6 +1,7 @@
/* eslint-disable new-cap */ /* eslint-disable new-cap */
/* eslint-disable @typescript-eslint/no-non-null-assertion */ /* eslint-disable @typescript-eslint/no-non-null-assertion */
import { backendLogger as logger } from '@/server/logger'
import CONFIG from '@/config' import CONFIG from '@/config'
import { Context, getUser } from '@/server/context' import { Context, getUser } from '@/server/context'
@ -44,15 +45,22 @@ export const executeTransaction = async (
recipient: dbUser, recipient: dbUser,
transactionLink?: dbTransactionLink | null, transactionLink?: dbTransactionLink | null,
): Promise<boolean> => { ): Promise<boolean> => {
logger.info(
`executeTransaction(amount=${amount}, memo=${memo}, sender=${sender}, recipient=${recipient})...`,
)
if (sender.id === recipient.id) { if (sender.id === recipient.id) {
logger.error(`Sender and Recipient are the same.`)
throw new Error('Sender and Recipient are the same.') throw new Error('Sender and Recipient are the same.')
} }
if (memo.length > MEMO_MAX_CHARS) { if (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)`) throw new Error(`memo text is too long (${MEMO_MAX_CHARS} characters maximum)`)
} }
if (memo.length < MEMO_MIN_CHARS) { if (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)`) throw new Error(`memo text is too short (${MEMO_MIN_CHARS} characters minimum)`)
} }
@ -64,13 +72,16 @@ export const executeTransaction = async (
receivedCallDate, receivedCallDate,
transactionLink, transactionLink,
) )
logger.debug(`calculated Balance=${sendBalance}`)
if (!sendBalance) { if (!sendBalance) {
logger.error(`user hasn't enough GDD or amount is < 0 : balance=${sendBalance}`)
throw new Error("user hasn't enough GDD or amount is < 0") throw new Error("user hasn't enough GDD or amount is < 0")
} }
const queryRunner = getConnection().createQueryRunner() const queryRunner = getConnection().createQueryRunner()
await queryRunner.connect() await queryRunner.connect()
await queryRunner.startTransaction('READ UNCOMMITTED') await queryRunner.startTransaction('READ UNCOMMITTED')
logger.debug(`open Transaction to write...`)
try { try {
// transaction // transaction
const transactionSend = new dbTransaction() const transactionSend = new dbTransaction()
@ -87,6 +98,8 @@ export const executeTransaction = async (
transactionSend.transactionLinkId = transactionLink ? transactionLink.id : null transactionSend.transactionLinkId = transactionLink ? transactionLink.id : null
await queryRunner.manager.insert(dbTransaction, transactionSend) await queryRunner.manager.insert(dbTransaction, transactionSend)
logger.debug(`sendTransaction inserted: ${dbTransaction}`)
const transactionReceive = new dbTransaction() const transactionReceive = new dbTransaction()
transactionReceive.typeId = TransactionTypeId.RECEIVE transactionReceive.typeId = TransactionTypeId.RECEIVE
transactionReceive.memo = memo transactionReceive.memo = memo
@ -102,12 +115,15 @@ export const executeTransaction = async (
transactionReceive.linkedTransactionId = transactionSend.id transactionReceive.linkedTransactionId = transactionSend.id
transactionReceive.transactionLinkId = transactionLink ? transactionLink.id : null transactionReceive.transactionLinkId = transactionLink ? transactionLink.id : null
await queryRunner.manager.insert(dbTransaction, transactionReceive) await queryRunner.manager.insert(dbTransaction, transactionReceive)
logger.debug(`receive Transaction inserted: ${dbTransaction}`)
// Save linked transaction id for send // Save linked transaction id for send
transactionSend.linkedTransactionId = transactionReceive.id transactionSend.linkedTransactionId = transactionReceive.id
await queryRunner.manager.update(dbTransaction, { id: transactionSend.id }, transactionSend) await queryRunner.manager.update(dbTransaction, { id: transactionSend.id }, transactionSend)
logger.debug(`send Transaction updated: ${transactionSend}`)
if (transactionLink) { if (transactionLink) {
logger.info(`transactionLink: ${transactionLink}`)
transactionLink.redeemedAt = receivedCallDate transactionLink.redeemedAt = receivedCallDate
transactionLink.redeemedBy = recipient.id transactionLink.redeemedBy = recipient.id
await queryRunner.manager.update( await queryRunner.manager.update(
@ -118,13 +134,15 @@ export const executeTransaction = async (
} }
await queryRunner.commitTransaction() await queryRunner.commitTransaction()
logger.info(`commit Transaction successful...`)
} catch (e) { } catch (e) {
await queryRunner.rollbackTransaction() await queryRunner.rollbackTransaction()
logger.error(`Transaction was not successful: ${e}`)
throw new Error(`Transaction was not successful: ${e}`) throw new Error(`Transaction was not successful: ${e}`)
} finally { } finally {
await queryRunner.release() await queryRunner.release()
} }
logger.debug(`prepare Email for transaction received...`)
// send notification email // send notification email
// TODO: translate // TODO: translate
await sendTransactionReceivedEmail({ await sendTransactionReceivedEmail({
@ -138,7 +156,7 @@ export const executeTransaction = async (
memo, memo,
overviewURL: CONFIG.EMAIL_LINK_OVERVIEW, overviewURL: CONFIG.EMAIL_LINK_OVERVIEW,
}) })
logger.info(`finished executeTransaction successfully`)
return true return true
} }
@ -154,16 +172,21 @@ export class TransactionResolver {
const now = new Date() const now = new Date()
const user = getUser(context) const user = getUser(context)
logger.addContext('user', user.id)
logger.info(`transactionList(user=${user.firstName}.${user.lastName}, ${user.email})`)
// find current balance // find current balance
const lastTransaction = await dbTransaction.findOne( const lastTransaction = await dbTransaction.findOne(
{ userId: user.id }, { userId: user.id },
{ order: { balanceDate: 'DESC' } }, { order: { balanceDate: 'DESC' } },
) )
logger.debug(`lastTransaction=${lastTransaction}`)
const balanceResolver = new BalanceResolver() const balanceResolver = new BalanceResolver()
context.lastTransaction = lastTransaction context.lastTransaction = lastTransaction
if (!lastTransaction) { if (!lastTransaction) {
logger.info('no lastTransaction')
return new TransactionList(await balanceResolver.balance(context), []) return new TransactionList(await balanceResolver.balance(context), [])
} }
@ -186,6 +209,8 @@ export class TransactionResolver {
involvedUserIds.push(transaction.linkedUserId) involvedUserIds.push(transaction.linkedUserId)
} }
}) })
logger.debug(`involvedUserIds=${involvedUserIds}`)
// We need to show the name for deleted users for old transactions // We need to show the name for deleted users for old transactions
const involvedDbUsers = await dbUser const involvedDbUsers = await dbUser
.createQueryBuilder() .createQueryBuilder()
@ -193,6 +218,7 @@ export class TransactionResolver {
.where('id IN (:...userIds)', { userIds: involvedUserIds }) .where('id IN (:...userIds)', { userIds: involvedUserIds })
.getMany() .getMany()
const involvedUsers = involvedDbUsers.map((u) => new User(u)) const involvedUsers = involvedDbUsers.map((u) => new User(u))
logger.debug(`involvedUsers=${involvedUsers}`)
const self = new User(user) const self = new User(user)
const transactions: Transaction[] = [] const transactions: Transaction[] = []
@ -201,10 +227,13 @@ export class TransactionResolver {
const { sumHoldAvailableAmount, sumAmount, lastDate, firstDate, transactionLinkcount } = const { sumHoldAvailableAmount, sumAmount, lastDate, firstDate, transactionLinkcount } =
await transactionLinkRepository.summary(user.id, now) await transactionLinkRepository.summary(user.id, now)
context.linkCount = transactionLinkcount context.linkCount = transactionLinkcount
logger.debug(`transactionLinkcount=${transactionLinkcount}`)
context.sumHoldAvailableAmount = sumHoldAvailableAmount context.sumHoldAvailableAmount = sumHoldAvailableAmount
logger.debug(`sumHoldAvailableAmount=${sumHoldAvailableAmount}`)
// decay & link transactions // decay & link transactions
if (currentPage === 1 && order === Order.DESC) { if (currentPage === 1 && order === Order.DESC) {
logger.debug(`currentPage == 1: transactions=${transactions}`)
// The virtual decay is always on the booked amount, not including the generated, not yet booked links, // The virtual decay is always on the booked amount, not including the generated, not yet booked links,
// since the decay is substantially different when the amount is less // since the decay is substantially different when the amount is less
transactions.push( transactions.push(
@ -216,8 +245,11 @@ export class TransactionResolver {
sumHoldAvailableAmount, sumHoldAvailableAmount,
), ),
) )
logger.debug(`transactions=${transactions}`)
// virtual transaction for pending transaction-links sum // virtual transaction for pending transaction-links sum
if (sumHoldAvailableAmount.greaterThan(0)) { if (sumHoldAvailableAmount.greaterThan(0)) {
logger.debug(`sumHoldAvailableAmount > 0: transactions=${transactions}`)
transactions.push( transactions.push(
virtualLinkTransaction( virtualLinkTransaction(
lastTransaction.balance.minus(sumHoldAvailableAmount.toString()), lastTransaction.balance.minus(sumHoldAvailableAmount.toString()),
@ -229,6 +261,7 @@ export class TransactionResolver {
self, self,
), ),
) )
logger.debug(`transactions=${transactions}`)
} }
} }
@ -240,6 +273,7 @@ export class TransactionResolver {
: involvedUsers.find((u) => u.id === userTransaction.linkedUserId) : involvedUsers.find((u) => u.id === userTransaction.linkedUserId)
transactions.push(new Transaction(userTransaction, self, linkedUser)) transactions.push(new Transaction(userTransaction, self, linkedUser))
}) })
logger.debug(`TransactionTypeId.CREATION: transactions=${transactions}`)
// Construct Result // Construct Result
return new TransactionList(await balanceResolver.balance(context), transactions) return new TransactionList(await balanceResolver.balance(context), transactions)
@ -251,29 +285,38 @@ export class TransactionResolver {
@Args() { email, amount, memo }: TransactionSendArgs, @Args() { email, amount, memo }: TransactionSendArgs,
@Ctx() context: Context, @Ctx() context: Context,
): Promise<boolean> { ): Promise<boolean> {
logger.info(`sendCoins(email=${email}, amount=${amount}, memo=${memo})`)
// TODO this is subject to replay attacks // TODO this is subject to replay attacks
const senderUser = getUser(context) const senderUser = getUser(context)
if (senderUser.pubKey.length !== 32) { if (senderUser.pubKey.length !== 32) {
logger.error(`invalid sender public key:${senderUser.pubKey}`)
throw new Error('invalid sender public key') throw new Error('invalid sender public key')
} }
// validate recipient user // validate recipient user
const recipientUser = await dbUser.findOne({ email: email }, { withDeleted: true }) const recipientUser = await dbUser.findOne({ email: email }, { withDeleted: true })
if (!recipientUser) { if (!recipientUser) {
logger.error(`recipient not known: email=${email}`)
throw new Error('recipient not known') throw new Error('recipient not known')
} }
if (recipientUser.deletedAt) { if (recipientUser.deletedAt) {
logger.error(`The recipient account was deleted: recipientUser=${recipientUser}`)
throw new Error('The recipient account was deleted') throw new Error('The recipient account was deleted')
} }
if (!recipientUser.emailChecked) { if (!recipientUser.emailChecked) {
logger.error(`The recipient account is not activated: recipientUser=${recipientUser}`)
throw new Error('The recipient account is not activated') throw new Error('The recipient account is not activated')
} }
if (!isHexPublicKey(recipientUser.pubKey.toString('hex'))) { if (!isHexPublicKey(recipientUser.pubKey.toString('hex'))) {
logger.error(`invalid recipient public key: recipientUser=${recipientUser}`)
throw new Error('invalid recipient public key') throw new Error('invalid recipient public key')
} }
await executeTransaction(amount, memo, senderUser, recipientUser) await executeTransaction(amount, memo, senderUser, recipientUser)
logger.info(
`successful executeTransaction(amount=${amount}, memo=${memo}, senderUser=${senderUser}, recipientUser=${recipientUser})`,
)
return true return true
} }
} }

View File

@ -14,6 +14,8 @@ import { sendAccountActivationEmail } from '@/mailer/sendAccountActivationEmail'
import { sendResetPasswordEmail } from '@/mailer/sendResetPasswordEmail' import { sendResetPasswordEmail } from '@/mailer/sendResetPasswordEmail'
import { printTimeDuration, activationLink } from './UserResolver' import { printTimeDuration, activationLink } from './UserResolver'
import { logger } from '@test/testSetup'
// import { klicktippSignIn } from '@/apis/KlicktippController' // import { klicktippSignIn } from '@/apis/KlicktippController'
jest.mock('@/mailer/sendAccountActivationEmail', () => { jest.mock('@/mailer/sendAccountActivationEmail', () => {
@ -43,7 +45,7 @@ let mutate: any, query: any, con: any
let testEnv: any let testEnv: any
beforeAll(async () => { beforeAll(async () => {
testEnv = await testEnvironment() testEnv = await testEnvironment(logger)
mutate = testEnv.mutate mutate = testEnv.mutate
query = testEnv.query query = testEnv.query
con = testEnv.con con = testEnv.con
@ -149,12 +151,14 @@ describe('UserResolver', () => {
}) })
describe('email already exists', () => { describe('email already exists', () => {
it('throws an error', async () => { it('throws and logs an error', async () => {
await expect(mutate({ mutation: createUser, variables })).resolves.toEqual( const mutation = await mutate({ mutation: createUser, variables })
expect(mutation).toEqual(
expect.objectContaining({ expect.objectContaining({
errors: [new GraphQLError('User already exists.')], errors: [new GraphQLError('User already exists.')],
}), }),
) )
expect(logger.error).toBeCalledWith('User already exists with this email=peter@lustig.de')
}) })
}) })
@ -340,7 +344,6 @@ describe('UserResolver', () => {
expect.objectContaining({ expect.objectContaining({
data: { data: {
login: { login: {
coinanimation: true,
email: 'bibi@bloxberg.de', email: 'bibi@bloxberg.de',
firstName: 'Bibi', firstName: 'Bibi',
hasElopage: false, hasElopage: false,
@ -475,7 +478,6 @@ describe('UserResolver', () => {
firstName: 'Bibi', firstName: 'Bibi',
lastName: 'Bloxberg', lastName: 'Bloxberg',
language: 'de', language: 'de',
coinanimation: true,
klickTipp: { klickTipp: {
newsletterState: false, newsletterState: false,
}, },

View File

@ -1,7 +1,9 @@
import fs from 'fs' import fs from 'fs'
import { backendLogger as logger } from '@/server/logger'
import { Context, getUser } from '@/server/context' import { Context, getUser } from '@/server/context'
import { Resolver, Query, Args, Arg, Authorized, Ctx, UseMiddleware, Mutation } from 'type-graphql' import { Resolver, Query, Args, Arg, Authorized, Ctx, UseMiddleware, Mutation } from 'type-graphql'
import { getConnection, getCustomRepository } from '@dbTools/typeorm' import { getConnection } from '@dbTools/typeorm'
import CONFIG from '@/config' import CONFIG from '@/config'
import { User } from '@model/User' import { User } from '@model/User'
import { User as DbUser } from '@entity/User' import { User as DbUser } from '@entity/User'
@ -11,8 +13,6 @@ import CreateUserArgs from '@arg/CreateUserArgs'
import UnsecureLoginArgs from '@arg/UnsecureLoginArgs' import UnsecureLoginArgs from '@arg/UnsecureLoginArgs'
import UpdateUserInfosArgs from '@arg/UpdateUserInfosArgs' import UpdateUserInfosArgs from '@arg/UpdateUserInfosArgs'
import { klicktippNewsletterStateMiddleware } from '@/middleware/klicktippMiddleware' import { klicktippNewsletterStateMiddleware } from '@/middleware/klicktippMiddleware'
import { UserSettingRepository } from '@repository/UserSettingRepository'
import { Setting } from '@enum/Setting'
import { OptInType } from '@enum/OptInType' import { OptInType } from '@enum/OptInType'
import { LoginEmailOptIn } from '@entity/LoginEmailOptIn' import { LoginEmailOptIn } from '@entity/LoginEmailOptIn'
import { sendResetPasswordEmail as sendResetPasswordEmailMailer } from '@/mailer/sendResetPasswordEmail' import { sendResetPasswordEmail as sendResetPasswordEmailMailer } from '@/mailer/sendResetPasswordEmail'
@ -43,6 +43,7 @@ const WORDS = fs
.toString() .toString()
.split(',') .split(',')
const PassphraseGenerate = (): string[] => { const PassphraseGenerate = (): string[] => {
logger.trace('PassphraseGenerate...')
const result = [] const result = []
for (let i = 0; i < PHRASE_WORD_COUNT; i++) { for (let i = 0; i < PHRASE_WORD_COUNT; i++) {
result.push(WORDS[sodium.randombytes_random() % 2048]) result.push(WORDS[sodium.randombytes_random() % 2048])
@ -51,7 +52,9 @@ const PassphraseGenerate = (): string[] => {
} }
const KeyPairEd25519Create = (passphrase: string[]): Buffer[] => { const KeyPairEd25519Create = (passphrase: string[]): Buffer[] => {
logger.trace('KeyPairEd25519Create...')
if (!passphrase.length || passphrase.length < PHRASE_WORD_COUNT) { if (!passphrase.length || passphrase.length < PHRASE_WORD_COUNT) {
logger.error('passphrase empty or to short')
throw new Error('passphrase empty or to short') throw new Error('passphrase empty or to short')
} }
@ -79,14 +82,19 @@ const KeyPairEd25519Create = (passphrase: string[]): Buffer[] => {
privKey, privKey,
outputHashBuffer.slice(0, sodium.crypto_sign_SEEDBYTES), outputHashBuffer.slice(0, sodium.crypto_sign_SEEDBYTES),
) )
logger.debug(`KeyPair creation ready. pubKey=${pubKey}`)
return [pubKey, privKey] return [pubKey, privKey]
} }
const SecretKeyCryptographyCreateKey = (salt: string, password: string): Buffer[] => { const SecretKeyCryptographyCreateKey = (salt: string, password: string): Buffer[] => {
logger.trace('SecretKeyCryptographyCreateKey...')
const configLoginAppSecret = Buffer.from(CONFIG.LOGIN_APP_SECRET, 'hex') const configLoginAppSecret = Buffer.from(CONFIG.LOGIN_APP_SECRET, 'hex')
const configLoginServerKey = Buffer.from(CONFIG.LOGIN_SERVER_KEY, 'hex') const configLoginServerKey = Buffer.from(CONFIG.LOGIN_SERVER_KEY, 'hex')
if (configLoginServerKey.length !== sodium.crypto_shorthash_KEYBYTES) { if (configLoginServerKey.length !== sodium.crypto_shorthash_KEYBYTES) {
logger.error(
`ServerKey has an invalid size. The size must be ${sodium.crypto_shorthash_KEYBYTES} bytes.`,
)
throw new Error( throw new Error(
`ServerKey has an invalid size. The size must be ${sodium.crypto_shorthash_KEYBYTES} bytes.`, `ServerKey has an invalid size. The size must be ${sodium.crypto_shorthash_KEYBYTES} bytes.`,
) )
@ -115,39 +123,50 @@ const SecretKeyCryptographyCreateKey = (salt: string, password: string): Buffer[
const encryptionKeyHash = Buffer.alloc(sodium.crypto_shorthash_BYTES) const encryptionKeyHash = Buffer.alloc(sodium.crypto_shorthash_BYTES)
sodium.crypto_shorthash(encryptionKeyHash, encryptionKey, configLoginServerKey) sodium.crypto_shorthash(encryptionKeyHash, encryptionKey, configLoginServerKey)
logger.debug(
`SecretKeyCryptographyCreateKey...successful: encryptionKeyHash= ${encryptionKeyHash}, encryptionKey= ${encryptionKey}`,
)
return [encryptionKeyHash, encryptionKey] return [encryptionKeyHash, encryptionKey]
} }
const getEmailHash = (email: string): Buffer => { const getEmailHash = (email: string): Buffer => {
logger.trace('getEmailHash...')
const emailHash = Buffer.alloc(sodium.crypto_generichash_BYTES) const emailHash = Buffer.alloc(sodium.crypto_generichash_BYTES)
sodium.crypto_generichash(emailHash, Buffer.from(email)) sodium.crypto_generichash(emailHash, Buffer.from(email))
logger.debug(`getEmailHash...successful: ${emailHash}`)
return emailHash return emailHash
} }
const SecretKeyCryptographyEncrypt = (message: Buffer, encryptionKey: Buffer): Buffer => { const SecretKeyCryptographyEncrypt = (message: Buffer, encryptionKey: Buffer): Buffer => {
logger.trace('SecretKeyCryptographyEncrypt...')
const encrypted = Buffer.alloc(message.length + sodium.crypto_secretbox_MACBYTES) const encrypted = Buffer.alloc(message.length + sodium.crypto_secretbox_MACBYTES)
const nonce = Buffer.alloc(sodium.crypto_secretbox_NONCEBYTES) const nonce = Buffer.alloc(sodium.crypto_secretbox_NONCEBYTES)
nonce.fill(31) // static nonce nonce.fill(31) // static nonce
sodium.crypto_secretbox_easy(encrypted, message, nonce, encryptionKey) sodium.crypto_secretbox_easy(encrypted, message, nonce, encryptionKey)
logger.debug(`SecretKeyCryptographyEncrypt...successful: ${encrypted}`)
return encrypted return encrypted
} }
const SecretKeyCryptographyDecrypt = (encryptedMessage: Buffer, encryptionKey: Buffer): Buffer => { const SecretKeyCryptographyDecrypt = (encryptedMessage: Buffer, encryptionKey: Buffer): Buffer => {
logger.trace('SecretKeyCryptographyDecrypt...')
const message = Buffer.alloc(encryptedMessage.length - sodium.crypto_secretbox_MACBYTES) const message = Buffer.alloc(encryptedMessage.length - sodium.crypto_secretbox_MACBYTES)
const nonce = Buffer.alloc(sodium.crypto_secretbox_NONCEBYTES) const nonce = Buffer.alloc(sodium.crypto_secretbox_NONCEBYTES)
nonce.fill(31) // static nonce nonce.fill(31) // static nonce
sodium.crypto_secretbox_open_easy(message, encryptedMessage, nonce, encryptionKey) sodium.crypto_secretbox_open_easy(message, encryptedMessage, nonce, encryptionKey)
logger.debug(`SecretKeyCryptographyDecrypt...successful: ${message}`)
return message return message
} }
const newEmailOptIn = (userId: number): LoginEmailOptIn => { const newEmailOptIn = (userId: number): LoginEmailOptIn => {
logger.trace('newEmailOptIn...')
const emailOptIn = new LoginEmailOptIn() const emailOptIn = new LoginEmailOptIn()
emailOptIn.verificationCode = random(64) emailOptIn.verificationCode = random(64)
emailOptIn.userId = userId emailOptIn.userId = userId
emailOptIn.emailOptInTypeId = OptInType.EMAIL_OPT_IN_REGISTER emailOptIn.emailOptInTypeId = OptInType.EMAIL_OPT_IN_REGISTER
logger.debug(`newEmailOptIn...successful: ${emailOptIn}`)
return emailOptIn return emailOptIn
} }
@ -159,8 +178,14 @@ export const checkOptInCode = async (
userId: number, userId: number,
optInType: OptInType = OptInType.EMAIL_OPT_IN_REGISTER, optInType: OptInType = OptInType.EMAIL_OPT_IN_REGISTER,
): Promise<LoginEmailOptIn> => { ): Promise<LoginEmailOptIn> => {
logger.info(`checkOptInCode... ${optInCode}`)
if (optInCode) { if (optInCode) {
if (!canResendOptIn(optInCode)) { if (!canResendOptIn(optInCode)) {
logger.error(
`email already sent less than ${printTimeDuration(
CONFIG.EMAIL_CODE_REQUEST_TIME,
)} minutes ago`,
)
throw new Error( throw new Error(
`email already sent less than ${printTimeDuration( `email already sent less than ${printTimeDuration(
CONFIG.EMAIL_CODE_REQUEST_TIME, CONFIG.EMAIL_CODE_REQUEST_TIME,
@ -170,16 +195,20 @@ export const checkOptInCode = async (
optInCode.updatedAt = new Date() optInCode.updatedAt = new Date()
optInCode.resendCount++ optInCode.resendCount++
} else { } else {
logger.trace('create new OptIn for userId=' + userId)
optInCode = newEmailOptIn(userId) optInCode = newEmailOptIn(userId)
} }
optInCode.emailOptInTypeId = optInType optInCode.emailOptInTypeId = optInType
await LoginEmailOptIn.save(optInCode).catch(() => { await LoginEmailOptIn.save(optInCode).catch(() => {
logger.error('Unable to save optin code= ' + optInCode)
throw new Error('Unable to save optin code.') throw new Error('Unable to save optin code.')
}) })
logger.debug(`checkOptInCode...successful: ${optInCode} for userid=${userId}`)
return optInCode return optInCode
} }
export const activationLink = (optInCode: LoginEmailOptIn): string => { export const activationLink = (optInCode: LoginEmailOptIn): string => {
logger.debug(`activationLink(${LoginEmailOptIn})...`)
return CONFIG.EMAIL_LINK_SETPASSWORD.replace(/{optin}/g, optInCode.verificationCode.toString()) return CONFIG.EMAIL_LINK_SETPASSWORD.replace(/{optin}/g, optInCode.verificationCode.toString())
} }
@ -189,6 +218,7 @@ export class UserResolver {
@Query(() => User) @Query(() => User)
@UseMiddleware(klicktippNewsletterStateMiddleware) @UseMiddleware(klicktippNewsletterStateMiddleware)
async verifyLogin(@Ctx() context: Context): Promise<User> { async verifyLogin(@Ctx() context: Context): Promise<User> {
logger.info('verifyLogin...')
// TODO refactor and do not have duplicate code with login(see below) // TODO refactor and do not have duplicate code with login(see below)
const userEntity = getUser(context) const userEntity = getUser(context)
const user = new User(userEntity) const user = new User(userEntity)
@ -196,15 +226,7 @@ export class UserResolver {
// Elopage Status & Stored PublisherId // Elopage Status & Stored PublisherId
user.hasElopage = await this.hasElopage(context) user.hasElopage = await this.hasElopage(context)
// coinAnimation logger.debug(`verifyLogin... successful: ${user.firstName}.${user.lastName}, ${user.email}`)
const userSettingRepository = getCustomRepository(UserSettingRepository)
const coinanimation = await userSettingRepository
.readBoolean(userEntity.id, Setting.COIN_ANIMATION)
.catch((error) => {
throw new Error(error)
})
user.coinanimation = coinanimation
return user return user
} }
@ -215,54 +237,57 @@ export class UserResolver {
@Args() { email, password, publisherId }: UnsecureLoginArgs, @Args() { email, password, publisherId }: UnsecureLoginArgs,
@Ctx() context: Context, @Ctx() context: Context,
): Promise<User> { ): Promise<User> {
logger.info(`login with ${email}, ***, ${publisherId} ...`)
email = email.trim().toLowerCase() email = email.trim().toLowerCase()
const dbUser = await DbUser.findOneOrFail({ email }, { withDeleted: true }).catch(() => { const dbUser = await DbUser.findOneOrFail({ email }, { withDeleted: true }).catch(() => {
logger.error(`User with email=${email} does not exists`)
throw new Error('No user with this credentials') throw new Error('No user with this credentials')
}) })
if (dbUser.deletedAt) { if (dbUser.deletedAt) {
logger.error('The User was permanently deleted in database.')
throw new Error('This user was permanently deleted. Contact support for questions.') throw new Error('This user was permanently deleted. Contact support for questions.')
} }
if (!dbUser.emailChecked) { if (!dbUser.emailChecked) {
logger.error('The Users email is not validate yet.')
throw new Error('User email not validated') throw new Error('User email not validated')
} }
if (dbUser.password === BigInt(0)) { if (dbUser.password === BigInt(0)) {
logger.error('The User has not set a password yet.')
// TODO we want to catch this on the frontend and ask the user to check his emails or resend code // TODO we want to catch this on the frontend and ask the user to check his emails or resend code
throw new Error('User has no password set yet') throw new Error('User has no password set yet')
} }
if (!dbUser.pubKey || !dbUser.privKey) { if (!dbUser.pubKey || !dbUser.privKey) {
logger.error('The User has no private or publicKey.')
// TODO we want to catch this on the frontend and ask the user to check his emails or resend code // TODO we want to catch this on the frontend and ask the user to check his emails or resend code
throw new Error('User has no private or publicKey') throw new Error('User has no private or publicKey')
} }
const passwordHash = SecretKeyCryptographyCreateKey(email, password) // return short and long hash const passwordHash = SecretKeyCryptographyCreateKey(email, password) // return short and long hash
const loginUserPassword = BigInt(dbUser.password.toString()) const loginUserPassword = BigInt(dbUser.password.toString())
if (loginUserPassword !== passwordHash[0].readBigUInt64LE()) { if (loginUserPassword !== passwordHash[0].readBigUInt64LE()) {
logger.error('The User has no valid credentials.')
throw new Error('No user with this credentials') throw new Error('No user with this credentials')
} }
// add pubKey in logger-context for layout-pattern X{user} to print it in each logging message
logger.addContext('user', dbUser.id)
logger.debug('login credentials valid...')
const user = new User(dbUser) const user = new User(dbUser)
logger.debug('user=' + user)
// Elopage Status & Stored PublisherId // Elopage Status & Stored PublisherId
user.hasElopage = await this.hasElopage({ ...context, user: dbUser }) user.hasElopage = await this.hasElopage({ ...context, user: dbUser })
logger.info('user.hasElopage=' + user.hasElopage)
if (!user.hasElopage && publisherId) { if (!user.hasElopage && publisherId) {
user.publisherId = publisherId user.publisherId = publisherId
dbUser.publisherId = publisherId dbUser.publisherId = publisherId
DbUser.save(dbUser) DbUser.save(dbUser)
} }
// coinAnimation
const userSettingRepository = getCustomRepository(UserSettingRepository)
const coinanimation = await userSettingRepository
.readBoolean(dbUser.id, Setting.COIN_ANIMATION)
.catch((error) => {
throw new Error(error)
})
user.coinanimation = coinanimation
context.setHeaders.push({ context.setHeaders.push({
key: 'token', key: 'token',
value: encode(dbUser.pubKey), value: encode(dbUser.pubKey),
}) })
logger.info('successful Login:' + user)
return user return user
} }
@ -274,6 +299,9 @@ export class UserResolver {
// The functionality is fully client side - the client just needs to delete his token with the current implementation. // The functionality is fully client side - the client just needs to delete his token with the current implementation.
// we could try to force this by sending `token: null` or `token: ''` with this call. But since it bares no real security // we could try to force this by sending `token: null` or `token: ''` with this call. But since it bares no real security
// we should just return true for now. // we should just return true for now.
logger.info('Logout...')
// remove user.pubKey from logger-context to ensure a correct filter on log-messages belonging to the same user
logger.addContext('user', 'unknown')
return true return true
} }
@ -283,6 +311,9 @@ export class UserResolver {
@Args() @Args()
{ email, firstName, lastName, language, publisherId, redeemCode = null }: CreateUserArgs, { email, firstName, lastName, language, publisherId, redeemCode = null }: CreateUserArgs,
): Promise<User> { ): Promise<User> {
logger.info(
`createUser(email=${email}, firstName=${firstName}, lastName=${lastName}, language=${language}, publisherId=${publisherId}, redeemCode =${redeemCode})`,
)
// TODO: wrong default value (should be null), how does graphql work here? Is it an required field? // TODO: wrong default value (should be null), how does graphql work here? Is it an required field?
// default int publisher_id = 0; // default int publisher_id = 0;
@ -295,7 +326,9 @@ export class UserResolver {
email = email.trim().toLowerCase() email = email.trim().toLowerCase()
// TODO we cannot use repository.count(), since it does not allow to specify if you want to include the soft deletes // TODO we cannot use repository.count(), since it does not allow to specify if you want to include the soft deletes
const userFound = await DbUser.findOne({ email }, { withDeleted: true }) const userFound = await DbUser.findOne({ email }, { withDeleted: true })
logger.info(`DbUser.findOne(email=${email}) = ${userFound}`)
if (userFound) { if (userFound) {
logger.error('User already exists with this email=' + email)
// TODO: this is unsecure, but the current implementation of the login server. This way it can be queried if the user with given EMail is existent. // TODO: this is unsecure, but the current implementation of the login server. This way it can be queried if the user with given EMail is existent.
throw new Error(`User already exists.`) throw new Error(`User already exists.`)
} }
@ -314,8 +347,10 @@ export class UserResolver {
dbUser.language = language dbUser.language = language
dbUser.publisherId = publisherId dbUser.publisherId = publisherId
dbUser.passphrase = passphrase.join(' ') dbUser.passphrase = passphrase.join(' ')
logger.debug('new dbUser=' + dbUser)
if (redeemCode) { if (redeemCode) {
const transactionLink = await dbTransactionLink.findOne({ code: redeemCode }) const transactionLink = await dbTransactionLink.findOne({ code: redeemCode })
logger.info('redeemCode found transactionLink=' + transactionLink)
if (transactionLink) { if (transactionLink) {
dbUser.referrerId = transactionLink.userId dbUser.referrerId = transactionLink.userId
} }
@ -332,15 +367,13 @@ export class UserResolver {
await queryRunner.startTransaction('READ UNCOMMITTED') await queryRunner.startTransaction('READ UNCOMMITTED')
try { try {
await queryRunner.manager.save(dbUser).catch((error) => { await queryRunner.manager.save(dbUser).catch((error) => {
// eslint-disable-next-line no-console logger.error('Error while saving dbUser', error)
console.log('Error while saving dbUser', error)
throw new Error('error saving user') throw new Error('error saving user')
}) })
const emailOptIn = newEmailOptIn(dbUser.id) const emailOptIn = newEmailOptIn(dbUser.id)
await queryRunner.manager.save(emailOptIn).catch((error) => { await queryRunner.manager.save(emailOptIn).catch((error) => {
// eslint-disable-next-line no-console logger.error('Error while saving emailOptIn', error)
console.log('Error while saving emailOptIn', error)
throw new Error('error saving email opt in') throw new Error('error saving email opt in')
}) })
@ -357,31 +390,35 @@ export class UserResolver {
email, email,
duration: printTimeDuration(CONFIG.EMAIL_CODE_VALID_TIME), duration: printTimeDuration(CONFIG.EMAIL_CODE_VALID_TIME),
}) })
logger.info(`sendAccountActivationEmail of ${firstName}.${lastName} to ${email}`)
/* uncomment this, when you need the activation link on the console /* uncomment this, when you need the activation link on the console */
// In case EMails are disabled log the activation link for the user // In case EMails are disabled log the activation link for the user
if (!emailSent) { if (!emailSent) {
// eslint-disable-next-line no-console logger.debug(`Account confirmation link: ${activationLink}`)
console.log(`Account confirmation link: ${activationLink}`)
} }
*/
await queryRunner.commitTransaction() await queryRunner.commitTransaction()
} catch (e) { } catch (e) {
logger.error(`error during create user with ${e}`)
await queryRunner.rollbackTransaction() await queryRunner.rollbackTransaction()
throw e throw e
} finally { } finally {
await queryRunner.release() await queryRunner.release()
} }
logger.info('createUser() successful...')
return new User(dbUser) return new User(dbUser)
} }
@Authorized([RIGHTS.SEND_RESET_PASSWORD_EMAIL]) @Authorized([RIGHTS.SEND_RESET_PASSWORD_EMAIL])
@Mutation(() => Boolean) @Mutation(() => Boolean)
async forgotPassword(@Arg('email') email: string): Promise<boolean> { async forgotPassword(@Arg('email') email: string): Promise<boolean> {
logger.info(`forgotPassword(${email})...`)
email = email.trim().toLowerCase() email = email.trim().toLowerCase()
const user = await DbUser.findOne({ email }) const user = await DbUser.findOne({ email })
if (!user) return true if (!user) {
logger.warn(`no user found with ${email}`)
return true
}
// can be both types: REGISTER and RESET_PASSWORD // can be both types: REGISTER and RESET_PASSWORD
let optInCode = await LoginEmailOptIn.findOne({ let optInCode = await LoginEmailOptIn.findOne({
@ -389,7 +426,7 @@ export class UserResolver {
}) })
optInCode = await checkOptInCode(optInCode, user.id, OptInType.EMAIL_OPT_IN_RESET_PASSWORD) optInCode = await checkOptInCode(optInCode, user.id, OptInType.EMAIL_OPT_IN_RESET_PASSWORD)
logger.info(`optInCode for ${email}=${optInCode}`)
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
const emailSent = await sendResetPasswordEmailMailer({ const emailSent = await sendResetPasswordEmailMailer({
link: activationLink(optInCode), link: activationLink(optInCode),
@ -399,13 +436,12 @@ export class UserResolver {
duration: printTimeDuration(CONFIG.EMAIL_CODE_VALID_TIME), duration: printTimeDuration(CONFIG.EMAIL_CODE_VALID_TIME),
}) })
/* uncomment this, when you need the activation link on the console /* uncomment this, when you need the activation link on the console */
// In case EMails are disabled log the activation link for the user // In case EMails are disabled log the activation link for the user
if (!emailSent) { if (!emailSent) {
// eslint-disable-next-line no-console logger.debug(`Reset password link: ${activationLink(optInCode)}`)
console.log(`Reset password link: ${link}`)
} }
*/ logger.info(`forgotPassword(${email}) successful...`)
return true return true
} }
@ -416,6 +452,7 @@ export class UserResolver {
@Arg('code') code: string, @Arg('code') code: string,
@Arg('password') password: string, @Arg('password') password: string,
): Promise<boolean> { ): Promise<boolean> {
logger.info(`setPassword(${code}, ***)...`)
// Validate Password // Validate Password
if (!isPassword(password)) { if (!isPassword(password)) {
throw new Error( throw new Error(
@ -425,34 +462,44 @@ export class UserResolver {
// Load code // Load code
const optInCode = await LoginEmailOptIn.findOneOrFail({ verificationCode: code }).catch(() => { const optInCode = await LoginEmailOptIn.findOneOrFail({ verificationCode: code }).catch(() => {
logger.error('Could not login with emailVerificationCode')
throw new Error('Could not login with emailVerificationCode') throw new Error('Could not login with emailVerificationCode')
}) })
logger.debug('optInCode loaded...')
// Code is only valid for `CONFIG.EMAIL_CODE_VALID_TIME` minutes // Code is only valid for `CONFIG.EMAIL_CODE_VALID_TIME` minutes
if (!isOptInValid(optInCode)) { if (!isOptInValid(optInCode)) {
logger.error(
`email was sent more than ${printTimeDuration(CONFIG.EMAIL_CODE_VALID_TIME)} ago`,
)
throw new Error( throw new Error(
`email was sent more than ${printTimeDuration(CONFIG.EMAIL_CODE_VALID_TIME)} ago`, `email was sent more than ${printTimeDuration(CONFIG.EMAIL_CODE_VALID_TIME)} ago`,
) )
} }
logger.debug('optInCode is valid...')
// load user // load user
const user = await DbUser.findOneOrFail({ id: optInCode.userId }).catch(() => { const user = await DbUser.findOneOrFail({ id: optInCode.userId }).catch(() => {
logger.error('Could not find corresponding Login User')
throw new Error('Could not find corresponding Login User') throw new Error('Could not find corresponding Login User')
}) })
logger.debug('user with optInCode found...')
// Generate Passphrase if needed // Generate Passphrase if needed
if (!user.passphrase) { if (!user.passphrase) {
const passphrase = PassphraseGenerate() const passphrase = PassphraseGenerate()
user.passphrase = passphrase.join(' ') user.passphrase = passphrase.join(' ')
logger.debug('new Passphrase generated...')
} }
const passphrase = user.passphrase.split(' ') const passphrase = user.passphrase.split(' ')
if (passphrase.length < PHRASE_WORD_COUNT) { if (passphrase.length < PHRASE_WORD_COUNT) {
logger.error('Could not load a correct passphrase')
// TODO if this can happen we cannot recover from that // TODO if this can happen we cannot recover from that
// this seem to be good on production data, if we dont // this seem to be good on production data, if we dont
// make a coding mistake we do not have a problem here // make a coding mistake we do not have a problem here
throw new Error('Could not load a correct passphrase') throw new Error('Could not load a correct passphrase')
} }
logger.debug('Passphrase is valid...')
// Activate EMail // Activate EMail
user.emailChecked = true user.emailChecked = true
@ -464,6 +511,7 @@ export class UserResolver {
user.password = passwordHash[0].readBigUInt64LE() // using the shorthash user.password = passwordHash[0].readBigUInt64LE() // using the shorthash
user.pubKey = keyPair[0] user.pubKey = keyPair[0]
user.privKey = encryptedPrivkey user.privKey = encryptedPrivkey
logger.debug('User credentials updated ...')
const queryRunner = getConnection().createQueryRunner() const queryRunner = getConnection().createQueryRunner()
await queryRunner.connect() await queryRunner.connect()
@ -472,12 +520,15 @@ export class UserResolver {
try { try {
// Save user // Save user
await queryRunner.manager.save(user).catch((error) => { await queryRunner.manager.save(user).catch((error) => {
logger.error('error saving user: ' + error)
throw new Error('error saving user: ' + error) throw new Error('error saving user: ' + error)
}) })
await queryRunner.commitTransaction() await queryRunner.commitTransaction()
logger.info('User data written successfully...')
} catch (e) { } catch (e) {
await queryRunner.rollbackTransaction() await queryRunner.rollbackTransaction()
logger.error('Error on writing User data:' + e)
throw e throw e
} finally { } finally {
await queryRunner.release() await queryRunner.release()
@ -488,7 +539,11 @@ export class UserResolver {
if (optInCode.emailOptInTypeId === OptInType.EMAIL_OPT_IN_REGISTER) { if (optInCode.emailOptInTypeId === OptInType.EMAIL_OPT_IN_REGISTER) {
try { try {
await klicktippSignIn(user.email, user.language, user.firstName, user.lastName) await klicktippSignIn(user.email, user.language, user.firstName, user.lastName)
} catch { logger.debug(
`klicktippSignIn(${user.email}, ${user.language}, ${user.firstName}, ${user.lastName})`,
)
} catch (e) {
logger.error('Error subscribe to klicktipp:' + e)
// TODO is this a problem? // TODO is this a problem?
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
/* uncomment this, when you need the activation link on the console /* uncomment this, when you need the activation link on the console
@ -503,13 +558,19 @@ export class UserResolver {
@Authorized([RIGHTS.QUERY_OPT_IN]) @Authorized([RIGHTS.QUERY_OPT_IN])
@Query(() => Boolean) @Query(() => Boolean)
async queryOptIn(@Arg('optIn') optIn: string): Promise<boolean> { async queryOptIn(@Arg('optIn') optIn: string): Promise<boolean> {
logger.info(`queryOptIn(${optIn})...`)
const optInCode = await LoginEmailOptIn.findOneOrFail({ verificationCode: optIn }) const optInCode = await LoginEmailOptIn.findOneOrFail({ verificationCode: optIn })
logger.debug(`found optInCode=${optInCode}`)
// Code is only valid for `CONFIG.EMAIL_CODE_VALID_TIME` minutes // Code is only valid for `CONFIG.EMAIL_CODE_VALID_TIME` minutes
if (!isOptInValid(optInCode)) { if (!isOptInValid(optInCode)) {
logger.error(
`email was sent more than ${printTimeDuration(CONFIG.EMAIL_CODE_VALID_TIME)} ago`,
)
throw new Error( throw new Error(
`email was sent more than ${printTimeDuration(CONFIG.EMAIL_CODE_VALID_TIME)} ago`, `email was sent more than ${printTimeDuration(CONFIG.EMAIL_CODE_VALID_TIME)} ago`,
) )
} }
logger.info(`queryOptIn(${optIn}) successful...`)
return true return true
} }
@ -517,9 +578,10 @@ export class UserResolver {
@Mutation(() => Boolean) @Mutation(() => Boolean)
async updateUserInfos( async updateUserInfos(
@Args() @Args()
{ firstName, lastName, language, password, passwordNew, coinanimation }: UpdateUserInfosArgs, { firstName, lastName, language, password, passwordNew }: UpdateUserInfosArgs,
@Ctx() context: Context, @Ctx() context: Context,
): Promise<boolean> { ): Promise<boolean> {
logger.info(`updateUserInfos(${firstName}, ${lastName}, ${language}, ***, ***)...`)
const userEntity = getUser(context) const userEntity = getUser(context)
if (firstName) { if (firstName) {
@ -532,6 +594,7 @@ export class UserResolver {
if (language) { if (language) {
if (!isLanguage(language)) { if (!isLanguage(language)) {
logger.error(`"${language}" isn't a valid language`)
throw new Error(`"${language}" isn't a valid language`) throw new Error(`"${language}" isn't a valid language`)
} }
userEntity.language = language userEntity.language = language
@ -540,6 +603,7 @@ export class UserResolver {
if (password && passwordNew) { if (password && passwordNew) {
// Validate Password // Validate Password
if (!isPassword(passwordNew)) { if (!isPassword(passwordNew)) {
logger.error('newPassword does not fullfil the rules')
throw new Error( throw new Error(
'Please enter a valid password with at least 8 characters, upper and lower case letters, at least one number and one special character!', 'Please enter a valid password with at least 8 characters, upper and lower case letters, at least one number and one special character!',
) )
@ -548,13 +612,16 @@ export class UserResolver {
// TODO: This had some error cases defined - like missing private key. This is no longer checked. // TODO: This had some error cases defined - like missing private key. This is no longer checked.
const oldPasswordHash = SecretKeyCryptographyCreateKey(userEntity.email, password) const oldPasswordHash = SecretKeyCryptographyCreateKey(userEntity.email, password)
if (BigInt(userEntity.password.toString()) !== oldPasswordHash[0].readBigUInt64LE()) { if (BigInt(userEntity.password.toString()) !== oldPasswordHash[0].readBigUInt64LE()) {
logger.error(`Old password is invalid`)
throw new Error(`Old password is invalid`) throw new Error(`Old password is invalid`)
} }
const privKey = SecretKeyCryptographyDecrypt(userEntity.privKey, oldPasswordHash[1]) const privKey = SecretKeyCryptographyDecrypt(userEntity.privKey, oldPasswordHash[1])
logger.debug('oldPassword decrypted...')
const newPasswordHash = SecretKeyCryptographyCreateKey(userEntity.email, passwordNew) // return short and long hash const newPasswordHash = SecretKeyCryptographyCreateKey(userEntity.email, passwordNew) // return short and long hash
logger.debug('newPasswordHash created...')
const encryptedPrivkey = SecretKeyCryptographyEncrypt(privKey, newPasswordHash[1]) const encryptedPrivkey = SecretKeyCryptographyEncrypt(privKey, newPasswordHash[1])
logger.debug('PrivateKey encrypted...')
// Save new password hash and newly encrypted private key // Save new password hash and newly encrypted private key
userEntity.password = newPasswordHash[0].readBigUInt64LE() userEntity.password = newPasswordHash[0].readBigUInt64LE()
@ -566,39 +633,35 @@ export class UserResolver {
await queryRunner.startTransaction('READ UNCOMMITTED') await queryRunner.startTransaction('READ UNCOMMITTED')
try { try {
if (coinanimation !== null && coinanimation !== undefined) {
queryRunner.manager
.getCustomRepository(UserSettingRepository)
.setOrUpdate(userEntity.id, Setting.COIN_ANIMATION, coinanimation.toString())
.catch((error) => {
throw new Error('error saving coinanimation: ' + error)
})
}
await queryRunner.manager.save(userEntity).catch((error) => { await queryRunner.manager.save(userEntity).catch((error) => {
throw new Error('error saving user: ' + error) throw new Error('error saving user: ' + error)
}) })
await queryRunner.commitTransaction() await queryRunner.commitTransaction()
logger.debug('writing User data successful...')
} catch (e) { } catch (e) {
await queryRunner.rollbackTransaction() await queryRunner.rollbackTransaction()
logger.error(`error on writing updated user data: ${e}`)
throw e throw e
} finally { } finally {
await queryRunner.release() await queryRunner.release()
} }
logger.info('updateUserInfos() successfully finished...')
return true return true
} }
@Authorized([RIGHTS.HAS_ELOPAGE]) @Authorized([RIGHTS.HAS_ELOPAGE])
@Query(() => Boolean) @Query(() => Boolean)
async hasElopage(@Ctx() context: Context): Promise<boolean> { async hasElopage(@Ctx() context: Context): Promise<boolean> {
logger.info(`hasElopage()...`)
const userEntity = context.user const userEntity = context.user
if (!userEntity) { if (!userEntity) {
logger.info('missing context.user for EloPage-check')
return false return false
} }
const elopageBuys = hasElopageBuys(userEntity.email)
return hasElopageBuys(userEntity.email) logger.debug(`has ElopageBuys = ${elopageBuys}`)
return elopageBuys
} }
} }

View File

@ -2,6 +2,8 @@ import { sendEMail } from './sendEMail'
import { createTransport } from 'nodemailer' import { createTransport } from 'nodemailer'
import CONFIG from '@/config' import CONFIG from '@/config'
import { logger } from '@test/testSetup'
CONFIG.EMAIL = false CONFIG.EMAIL = false
CONFIG.EMAIL_SMTP_URL = 'EMAIL_SMTP_URL' CONFIG.EMAIL_SMTP_URL = 'EMAIL_SMTP_URL'
CONFIG.EMAIL_SMTP_PORT = '1234' CONFIG.EMAIL_SMTP_PORT = '1234'
@ -26,11 +28,6 @@ jest.mock('nodemailer', () => {
describe('sendEMail', () => { describe('sendEMail', () => {
let result: boolean let result: boolean
describe('config email is false', () => { describe('config email is false', () => {
// eslint-disable-next-line no-console
const consoleLog = console.log
const consoleLogMock = jest.fn()
// eslint-disable-next-line no-console
console.log = consoleLogMock
beforeEach(async () => { beforeEach(async () => {
result = await sendEMail({ result = await sendEMail({
to: 'receiver@mail.org', to: 'receiver@mail.org',
@ -39,13 +36,8 @@ describe('sendEMail', () => {
}) })
}) })
afterAll(() => { it('logs warining', () => {
// eslint-disable-next-line no-console expect(logger.info).toBeCalledWith('Emails are disabled via config...')
console.log = consoleLog
})
it('logs warining to console', () => {
expect(consoleLogMock).toBeCalledWith('Emails are disabled via config')
}) })
it('returns false', () => { it('returns false', () => {

View File

@ -1,3 +1,4 @@
import { backendLogger as logger } from '@/server/logger'
import { createTransport } from 'nodemailer' import { createTransport } from 'nodemailer'
import CONFIG from '@/config' import CONFIG from '@/config'
@ -7,9 +8,10 @@ export const sendEMail = async (emailDef: {
subject: string subject: string
text: string text: string
}): Promise<boolean> => { }): Promise<boolean> => {
logger.info(`send Email: to=${emailDef.to}, subject=${emailDef.subject}, text=${emailDef.text}`)
if (!CONFIG.EMAIL) { if (!CONFIG.EMAIL) {
// eslint-disable-next-line no-console logger.info(`Emails are disabled via config...`)
console.log('Emails are disabled via config')
return false return false
} }
const transporter = createTransport({ const transporter = createTransport({
@ -27,7 +29,9 @@ export const sendEMail = async (emailDef: {
from: `Gradido (nicht antworten) <${CONFIG.EMAIL_SENDER}>`, from: `Gradido (nicht antworten) <${CONFIG.EMAIL_SENDER}>`,
}) })
if (!info.messageId) { if (!info.messageId) {
logger.error('error sending notification email, but transaction succeed')
throw new Error('error sending notification email, but transaction succeed') throw new Error('error sending notification email, but transaction succeed')
} }
logger.info('send Email successfully.')
return true return true
} }

View File

@ -1,3 +1,4 @@
import { backendLogger as logger } from '@/server/logger'
import Decimal from 'decimal.js-light' import Decimal from 'decimal.js-light'
import { sendEMail } from './sendEMail' import { sendEMail } from './sendEMail'
import { transactionReceived } from './text/transactionReceived' import { transactionReceived } from './text/transactionReceived'
@ -13,6 +14,12 @@ export const sendTransactionReceivedEmail = (data: {
memo: string memo: string
overviewURL: string overviewURL: string
}): Promise<boolean> => { }): Promise<boolean> => {
logger.info(
`sendEmail(): to=${data.recipientFirstName} ${data.recipientLastName},
<${data.email}>,
subject=${transactionReceived.de.subject},
text=${transactionReceived.de.text(data)}`,
)
return sendEMail({ return sendEMail({
to: `${data.recipientFirstName} ${data.recipientLastName} <${data.email}>`, to: `${data.recipientFirstName} ${data.recipientLastName} <${data.email}>`,
subject: transactionReceived.de.subject, subject: transactionReceived.de.subject,

View File

@ -31,7 +31,6 @@ export const updateUserInfos = gql`
$password: String $password: String
$passwordNew: String $passwordNew: String
$locale: String $locale: String
$coinanimation: Boolean
) { ) {
updateUserInfos( updateUserInfos(
firstName: $firstName firstName: $firstName
@ -39,7 +38,6 @@ export const updateUserInfos = gql`
password: $password password: $password
passwordNew: $passwordNew passwordNew: $passwordNew
language: $locale language: $locale
coinanimation: $coinanimation
) )
} }
` `
@ -107,6 +105,35 @@ export const unDeleteUser = gql`
} }
` `
export const searchUsers = gql`
query (
$searchText: String!
$currentPage: Int
$pageSize: Int
$filters: SearchUsersFiltersInput
) {
searchUsers(
searchText: $searchText
currentPage: $currentPage
pageSize: $pageSize
filters: $filters
) {
userCount
userList {
userId
firstName
lastName
email
creation
emailChecked
hasElopage
emailConfirmationSend
deletedAt
}
}
}
`
export const createPendingCreations = gql` export const createPendingCreations = gql`
mutation ($pendingCreations: [CreatePendingCreationArgs!]!) { mutation ($pendingCreations: [CreatePendingCreationArgs!]!) {
createPendingCreations(pendingCreations: $pendingCreations) { createPendingCreations(pendingCreations: $pendingCreations) {

View File

@ -8,7 +8,6 @@ export const login = gql`
firstName firstName
lastName lastName
language language
coinanimation
klickTipp { klickTipp {
newsletterState newsletterState
} }
@ -26,7 +25,6 @@ export const verifyLogin = gql`
firstName firstName
lastName lastName
language language
coinanimation
klickTipp { klickTipp {
newsletterState newsletterState
} }

View File

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

View File

@ -22,22 +22,32 @@ import schema from '@/graphql/schema'
import { elopageWebhook } from '@/webhook/elopage' import { elopageWebhook } from '@/webhook/elopage'
import { Connection } from '@dbTools/typeorm' import { Connection } from '@dbTools/typeorm'
import { apolloLogger } from './logger'
import { Logger } from 'log4js'
// TODO implement // TODO implement
// import queryComplexity, { simpleEstimator, fieldConfigEstimator } from "graphql-query-complexity"; // import queryComplexity, { simpleEstimator, fieldConfigEstimator } from "graphql-query-complexity";
type ServerDef = { apollo: ApolloServer; app: Express; con: Connection } type ServerDef = { apollo: ApolloServer; app: Express; con: Connection }
// eslint-disable-next-line @typescript-eslint/no-explicit-any const createServer = async (
const createServer = async (context: any = serverContext): Promise<ServerDef> => { // eslint-disable-next-line @typescript-eslint/no-explicit-any
context: any = serverContext,
logger: Logger = apolloLogger,
): Promise<ServerDef> => {
logger.debug('createServer...')
// open mysql connection // open mysql connection
const con = await connection() const con = await connection()
if (!con || !con.isConnected) { if (!con || !con.isConnected) {
logger.fatal(`Couldn't open connection to database!`)
throw new Error(`Fatal: Couldn't open connection to database`) throw new Error(`Fatal: Couldn't open connection to database`)
} }
// check for correct database version // check for correct database version
const dbVersion = await checkDBVersion(CONFIG.DB_VERSION) const dbVersion = await checkDBVersion(CONFIG.DB_VERSION)
if (!dbVersion) { if (!dbVersion) {
logger.fatal('Fatal: Database Version incorrect')
throw new Error('Fatal: Database Version incorrect') throw new Error('Fatal: Database Version incorrect')
} }
@ -62,8 +72,10 @@ const createServer = async (context: any = serverContext): Promise<ServerDef> =>
introspection: CONFIG.GRAPHIQL, introspection: CONFIG.GRAPHIQL,
context, context,
plugins, plugins,
logger,
}) })
apollo.applyMiddleware({ app, path: '/' }) apollo.applyMiddleware({ app, path: '/' })
logger.debug('createServer...successful')
return { apollo, app, con } return { apollo, app, con }
} }

View File

@ -0,0 +1,17 @@
import log4js from 'log4js'
import CONFIG from '@/config'
import { readFileSync } from 'fs'
const options = JSON.parse(readFileSync(CONFIG.LOG4JS_CONFIG, 'utf-8'))
options.categories.default.level = CONFIG.LOG_LEVEL
log4js.configure(options)
const apolloLogger = log4js.getLogger('apollo')
const backendLogger = log4js.getLogger('backend')
backendLogger.addContext('user', 'unknown')
export { apolloLogger, backendLogger }

View File

@ -1,8 +1,7 @@
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ /* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import { ApolloLogPlugin, LogMutateData } from 'apollo-log' import clonedeep from 'lodash.clonedeep'
import cloneDeep from 'lodash.clonedeep'
const setHeadersPlugin = { const setHeadersPlugin = {
requestDidStart() { requestDidStart() {
@ -22,24 +21,35 @@ const setHeadersPlugin = {
}, },
} }
const apolloLogPlugin = ApolloLogPlugin({ const filterVariables = (variables: any) => {
mutate: (data: LogMutateData) => { const vars = clonedeep(variables)
// We need to deep clone the object in order to not modify the actual request if (vars.password) vars.password = '***'
const dataCopy = cloneDeep(data) if (vars.passwordNew) vars.passwordNew = '***'
return vars
}
// mask password if part of the query const logPlugin = {
if (dataCopy.context.request.variables && dataCopy.context.request.variables.password) { requestDidStart(requestContext: any) {
dataCopy.context.request.variables.password = '***' const { logger } = requestContext
const { query, mutation, variables } = requestContext.request
logger.info(`Request:
${mutation || query}variables: ${JSON.stringify(filterVariables(variables), null, 2)}`)
return {
willSendResponse(requestContext: any) {
if (requestContext.context.user) logger.info(`User ID: ${requestContext.context.user.id}`)
if (requestContext.response.data)
logger.info(`Response-Data:
${JSON.stringify(requestContext.response.data, null, 2)}`)
if (requestContext.response.errors)
logger.error(`Response-Errors:
${JSON.stringify(requestContext.response.errors, null, 2)}`)
return requestContext
},
} }
// mask token at all times
dataCopy.context.context.token = '***'
return dataCopy
}, },
}) }
const plugins = const plugins =
process.env.NODE_ENV === 'development' ? [setHeadersPlugin] : [setHeadersPlugin, apolloLogPlugin] process.env.NODE_ENV === 'development' ? [setHeadersPlugin] : [setHeadersPlugin, logPlugin]
export default plugins export default plugins

View File

@ -1,12 +1,12 @@
import { Migration } from '@entity/Migration' import { Migration } from '@entity/Migration'
import { backendLogger as logger } from '@/server/logger'
const getDBVersion = async (): Promise<string | null> => { const getDBVersion = async (): Promise<string | null> => {
try { try {
const dbVersion = await Migration.findOne({ order: { version: 'DESC' } }) const dbVersion = await Migration.findOne({ order: { version: 'DESC' } })
return dbVersion ? dbVersion.fileName : null return dbVersion ? dbVersion.fileName : null
} catch (error) { } catch (error) {
// eslint-disable-next-line no-console logger.error(error)
console.log(error)
return null return null
} }
} }
@ -14,8 +14,7 @@ const getDBVersion = async (): Promise<string | null> => {
const checkDBVersion = async (DB_VERSION: string): Promise<boolean> => { const checkDBVersion = async (DB_VERSION: string): Promise<boolean> => {
const dbVersion = await getDBVersion() const dbVersion = await getDBVersion()
if (!dbVersion || dbVersion.indexOf(DB_VERSION) === -1) { if (!dbVersion || dbVersion.indexOf(DB_VERSION) === -1) {
// eslint-disable-next-line no-console logger.error(
console.log(
`Wrong database version detected - the backend requires '${DB_VERSION}' but found '${ `Wrong database version detected - the backend requires '${DB_VERSION}' but found '${
dbVersion || 'None' dbVersion || 'None'
}`, }`,

View File

@ -20,6 +20,9 @@ const connection = async (): Promise<Connection | null> => {
logger: new FileLogger('all', { logger: new FileLogger('all', {
logPath: CONFIG.TYPEORM_LOGGING_RELATIVE_PATH, logPath: CONFIG.TYPEORM_LOGGING_RELATIVE_PATH,
}), }),
extra: {
charset: 'utf8mb4_unicode_ci',
},
}) })
} catch (error) { } catch (error) {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console

View File

@ -1,33 +1,22 @@
import { EntityRepository, Repository } from '@dbTools/typeorm' import { EntityRepository, Repository } from '@dbTools/typeorm'
import { UserSetting } from '@entity/UserSetting' import { UserSetting } from '@entity/UserSetting'
import { Setting } from '@enum/Setting'
import { isStringBoolean } from '@/util/validate' import { isStringBoolean } from '@/util/validate'
@EntityRepository(UserSetting) @EntityRepository(UserSetting)
export class UserSettingRepository extends Repository<UserSetting> { export class UserSettingRepository extends Repository<UserSetting> {
async setOrUpdate(userId: number, key: Setting, value: string): Promise<UserSetting> { async setOrUpdate(userId: number, value: string): Promise<UserSetting> {
switch (key) { let entity = await this.findOne({ userId: userId })
case Setting.COIN_ANIMATION:
if (!isStringBoolean(value)) {
throw new Error("coinanimation value isn't boolean")
}
break
default:
throw new Error("key isn't defined: " + key)
}
let entity = await this.findOne({ userId: userId, key: key })
if (!entity) { if (!entity) {
entity = new UserSetting() entity = new UserSetting()
entity.userId = userId entity.userId = userId
entity.key = key
} }
entity.value = value entity.value = value
return this.save(entity) return this.save(entity)
} }
async readBoolean(userId: number, key: Setting): Promise<boolean> { async readBoolean(userId: number): Promise<boolean> {
const entity = await this.findOne({ userId: userId, key: key }) const entity = await this.findOne({ userId: userId })
if (!entity || !isStringBoolean(entity.value)) { if (!entity || !isStringBoolean(entity.value)) {
return true return true
} }

View File

@ -0,0 +1,5 @@
export const convertObjValuesToArray = (obj: { [x: string]: string }): Array<string> => {
return Object.keys(obj).map(function (key) {
return obj[key]
})
}

View File

@ -25,8 +25,8 @@ export const cleanDB = async () => {
} }
} }
export const testEnvironment = async () => { export const testEnvironment = async (logger?: any) => {
const server = await createServer(context) const server = await createServer(context, logger)
const con = server.con const con = server.con
const testClient = createTestClient(server.apollo) const testClient = createTestClient(server.apollo)
const mutate = testClient.mutate const mutate = testClient.mutate

View File

@ -1,7 +1,22 @@
/* eslint-disable no-console */ import { backendLogger as logger } from '@/server/logger'
// disable console.info for apollo log
// eslint-disable-next-line @typescript-eslint/no-empty-function
console.info = () => {}
jest.setTimeout(1000000) jest.setTimeout(1000000)
jest.mock('@/server/logger', () => {
const originalModule = jest.requireActual('@/server/logger')
return {
__esModule: true,
...originalModule,
backendLogger: {
addContext: jest.fn(),
trace: jest.fn(),
debug: jest.fn(),
warn: jest.fn(),
info: jest.fn(),
error: jest.fn(),
fatal: jest.fn(),
},
}
})
export { logger }

View File

@ -2,7 +2,7 @@
# yarn lockfile v1 # yarn lockfile v1
"@apollo/protobufjs@1.2.2", "@apollo/protobufjs@^1.0.3": "@apollo/protobufjs@1.2.2":
version "1.2.2" version "1.2.2"
resolved "https://registry.yarnpkg.com/@apollo/protobufjs/-/protobufjs-1.2.2.tgz#4bd92cd7701ccaef6d517cdb75af2755f049f87c" resolved "https://registry.yarnpkg.com/@apollo/protobufjs/-/protobufjs-1.2.2.tgz#4bd92cd7701ccaef6d517cdb75af2755f049f87c"
integrity sha512-vF+zxhPiLtkwxONs6YanSt1EpwpGilThpneExUN5K3tCymuxNnVq2yojTvnpRjv2QfsEIt/n7ozPIIzBLwGIDQ== integrity sha512-vF+zxhPiLtkwxONs6YanSt1EpwpGilThpneExUN5K3tCymuxNnVq2yojTvnpRjv2QfsEIt/n7ozPIIzBLwGIDQ==
@ -1265,24 +1265,6 @@ apollo-link@^1.2.14:
tslib "^1.9.3" tslib "^1.9.3"
zen-observable-ts "^0.8.21" zen-observable-ts "^0.8.21"
apollo-log@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/apollo-log/-/apollo-log-1.1.0.tgz#e21287c917cf735b77adc06f07034f965e9b24de"
integrity sha512-TciLu+85LSqk7t7ZGKrYN5jFiCcRMLujBjrLiOQGHGgVVkvmKlwK0oELSS9kiHQIhTq23p8qVVWb08spLpQ7Jw==
dependencies:
apollo-server-plugin-base "^0.10.4"
chalk "^4.1.0"
fast-safe-stringify "^2.0.7"
loglevelnext "^4.0.1"
nanoid "^3.1.20"
apollo-reporting-protobuf@^0.6.2:
version "0.6.2"
resolved "https://registry.yarnpkg.com/apollo-reporting-protobuf/-/apollo-reporting-protobuf-0.6.2.tgz#5572866be9b77f133916532b10e15fbaa4158304"
integrity sha512-WJTJxLM+MRHNUxt1RTl4zD0HrLdH44F2mDzMweBj1yHL0kSt8I1WwoiF/wiGVSpnG48LZrBegCaOJeuVbJTbtw==
dependencies:
"@apollo/protobufjs" "^1.0.3"
apollo-reporting-protobuf@^0.8.0: apollo-reporting-protobuf@^0.8.0:
version "0.8.0" version "0.8.0"
resolved "https://registry.yarnpkg.com/apollo-reporting-protobuf/-/apollo-reporting-protobuf-0.8.0.tgz#ae9d967934d3d8ed816fc85a0d8068ef45c371b9" resolved "https://registry.yarnpkg.com/apollo-reporting-protobuf/-/apollo-reporting-protobuf-0.8.0.tgz#ae9d967934d3d8ed816fc85a0d8068ef45c371b9"
@ -1290,13 +1272,6 @@ apollo-reporting-protobuf@^0.8.0:
dependencies: dependencies:
"@apollo/protobufjs" "1.2.2" "@apollo/protobufjs" "1.2.2"
apollo-server-caching@^0.5.3:
version "0.5.3"
resolved "https://registry.yarnpkg.com/apollo-server-caching/-/apollo-server-caching-0.5.3.tgz#cf42a77ad09a46290a246810075eaa029b5305e1"
integrity sha512-iMi3087iphDAI0U2iSBE9qtx9kQoMMEWr6w+LwXruBD95ek9DWyj7OeC2U/ngLjRsXM43DoBDXlu7R+uMjahrQ==
dependencies:
lru-cache "^6.0.0"
apollo-server-caching@^0.7.0: apollo-server-caching@^0.7.0:
version "0.7.0" version "0.7.0"
resolved "https://registry.yarnpkg.com/apollo-server-caching/-/apollo-server-caching-0.7.0.tgz#e6d1e68e3bb571cba63a61f60b434fb771c6ff39" resolved "https://registry.yarnpkg.com/apollo-server-caching/-/apollo-server-caching-0.7.0.tgz#e6d1e68e3bb571cba63a61f60b434fb771c6ff39"
@ -1335,7 +1310,7 @@ apollo-server-core@^2.25.2:
subscriptions-transport-ws "^0.9.19" subscriptions-transport-ws "^0.9.19"
uuid "^8.0.0" uuid "^8.0.0"
apollo-server-env@^3.0.0, apollo-server-env@^3.1.0: apollo-server-env@^3.1.0:
version "3.1.0" version "3.1.0"
resolved "https://registry.yarnpkg.com/apollo-server-env/-/apollo-server-env-3.1.0.tgz#0733c2ef50aea596cc90cf40a53f6ea2ad402cd0" resolved "https://registry.yarnpkg.com/apollo-server-env/-/apollo-server-env-3.1.0.tgz#0733c2ef50aea596cc90cf40a53f6ea2ad402cd0"
integrity sha512-iGdZgEOAuVop3vb0F2J3+kaBVi4caMoxefHosxmgzAbbSpvWehB8Y1QiSyyMeouYC38XNVk5wnZl+jdGSsWsIQ== integrity sha512-iGdZgEOAuVop3vb0F2J3+kaBVi4caMoxefHosxmgzAbbSpvWehB8Y1QiSyyMeouYC38XNVk5wnZl+jdGSsWsIQ==
@ -1371,13 +1346,6 @@ apollo-server-express@^2.25.2:
subscriptions-transport-ws "^0.9.19" subscriptions-transport-ws "^0.9.19"
type-is "^1.6.16" type-is "^1.6.16"
apollo-server-plugin-base@^0.10.4:
version "0.10.4"
resolved "https://registry.yarnpkg.com/apollo-server-plugin-base/-/apollo-server-plugin-base-0.10.4.tgz#fbf73f64f95537ca9f9639dd7c535eb5eeb95dcd"
integrity sha512-HRhbyHgHFTLP0ImubQObYhSgpmVH4Rk1BinnceZmwudIVLKrqayIVOELdyext/QnSmmzg5W7vF3NLGBcVGMqDg==
dependencies:
apollo-server-types "^0.6.3"
apollo-server-plugin-base@^0.13.0: apollo-server-plugin-base@^0.13.0:
version "0.13.0" version "0.13.0"
resolved "https://registry.yarnpkg.com/apollo-server-plugin-base/-/apollo-server-plugin-base-0.13.0.tgz#3f85751a420d3c4625355b6cb3fbdd2acbe71f13" resolved "https://registry.yarnpkg.com/apollo-server-plugin-base/-/apollo-server-plugin-base-0.13.0.tgz#3f85751a420d3c4625355b6cb3fbdd2acbe71f13"
@ -1392,15 +1360,6 @@ apollo-server-testing@^2.25.2:
dependencies: dependencies:
apollo-server-core "^2.25.2" apollo-server-core "^2.25.2"
apollo-server-types@^0.6.3:
version "0.6.3"
resolved "https://registry.yarnpkg.com/apollo-server-types/-/apollo-server-types-0.6.3.tgz#f7aa25ff7157863264d01a77d7934aa6e13399e8"
integrity sha512-aVR7SlSGGY41E1f11YYz5bvwA89uGmkVUtzMiklDhZ7IgRJhysT5Dflt5IuwDxp+NdQkIhVCErUXakopocFLAg==
dependencies:
apollo-reporting-protobuf "^0.6.2"
apollo-server-caching "^0.5.3"
apollo-server-env "^3.0.0"
apollo-server-types@^0.9.0: apollo-server-types@^0.9.0:
version "0.9.0" version "0.9.0"
resolved "https://registry.yarnpkg.com/apollo-server-types/-/apollo-server-types-0.9.0.tgz#ccf550b33b07c48c72f104fbe2876232b404848b" resolved "https://registry.yarnpkg.com/apollo-server-types/-/apollo-server-types-0.9.0.tgz#ccf550b33b07c48c72f104fbe2876232b404848b"
@ -1952,6 +1911,11 @@ data-urls@^2.0.0:
whatwg-mimetype "^2.3.0" whatwg-mimetype "^2.3.0"
whatwg-url "^8.0.0" whatwg-url "^8.0.0"
date-format@^4.0.9:
version "4.0.9"
resolved "https://registry.yarnpkg.com/date-format/-/date-format-4.0.9.tgz#4788015ac56dedebe83b03bc361f00c1ddcf1923"
integrity sha512-+8J+BOUpSrlKLQLeF8xJJVTxS8QfRSuJgwxSVvslzgO3E6khbI0F5mMEPf5mTYhCCm4h99knYP6H3W9n3BQFrg==
debug@2.6.9, debug@^2.2.0, debug@^2.6.9: debug@2.6.9, debug@^2.2.0, debug@^2.6.9:
version "2.6.9" version "2.6.9"
resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
@ -1973,6 +1937,13 @@ debug@^3.2.6, debug@^3.2.7:
dependencies: dependencies:
ms "^2.1.1" ms "^2.1.1"
debug@^4.3.4:
version "4.3.4"
resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865"
integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==
dependencies:
ms "2.1.2"
decimal.js-light@^2.5.1: decimal.js-light@^2.5.1:
version "2.5.1" version "2.5.1"
resolved "https://registry.yarnpkg.com/decimal.js-light/-/decimal.js-light-2.5.1.tgz#134fd32508f19e208f4fb2f8dac0d2626a867934" resolved "https://registry.yarnpkg.com/decimal.js-light/-/decimal.js-light-2.5.1.tgz#134fd32508f19e208f4fb2f8dac0d2626a867934"
@ -2558,11 +2529,6 @@ fast-levenshtein@^2.0.6, fast-levenshtein@~2.0.6:
resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917"
integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc= integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=
fast-safe-stringify@^2.0.7:
version "2.1.1"
resolved "https://registry.yarnpkg.com/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz#c406a83b6e70d9e35ce3b30a81141df30aeba884"
integrity sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==
fastq@^1.6.0: fastq@^1.6.0:
version "1.13.0" version "1.13.0"
resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.13.0.tgz#616760f88a7526bdfc596b7cab8c18938c36b98c" resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.13.0.tgz#616760f88a7526bdfc596b7cab8c18938c36b98c"
@ -2632,6 +2598,11 @@ flatted@^3.1.0:
resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.2.tgz#64bfed5cb68fe3ca78b3eb214ad97b63bedce561" resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.2.tgz#64bfed5cb68fe3ca78b3eb214ad97b63bedce561"
integrity sha512-JaTY/wtrcSyvXJl4IMFHPKyFur1sE9AUqc0QnhOaJ0CxHtAoIV8pYDzeEfAaNEtGkOfq4gr3LBFmdXW5mOQFnA== integrity sha512-JaTY/wtrcSyvXJl4IMFHPKyFur1sE9AUqc0QnhOaJ0CxHtAoIV8pYDzeEfAaNEtGkOfq4gr3LBFmdXW5mOQFnA==
flatted@^3.2.5:
version "3.2.5"
resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.5.tgz#76c8584f4fc843db64702a6bd04ab7a8bd666da3"
integrity sha512-WIWGi2L3DyTUvUrwRKgGi9TwxQMUEqPOPQBVi71R96jZXJdFskXEmf54BoZaS1kknGODoIGASGEzBUYdyMCBJg==
follow-redirects@^1.14.0: follow-redirects@^1.14.0:
version "1.14.4" version "1.14.4"
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.4.tgz#838fdf48a8bbdd79e52ee51fb1c94e3ed98b9379" resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.4.tgz#838fdf48a8bbdd79e52ee51fb1c94e3ed98b9379"
@ -2668,6 +2639,15 @@ fs-capacitor@^2.0.4:
resolved "https://registry.yarnpkg.com/fs-capacitor/-/fs-capacitor-2.0.4.tgz#5a22e72d40ae5078b4fe64fe4d08c0d3fc88ad3c" resolved "https://registry.yarnpkg.com/fs-capacitor/-/fs-capacitor-2.0.4.tgz#5a22e72d40ae5078b4fe64fe4d08c0d3fc88ad3c"
integrity sha512-8S4f4WsCryNw2mJJchi46YgB6CR5Ze+4L1h8ewl9tEpL4SJ3ZO+c/bS4BWhB8bK+O3TMqhuZarTitd0S0eh2pA== integrity sha512-8S4f4WsCryNw2mJJchi46YgB6CR5Ze+4L1h8ewl9tEpL4SJ3ZO+c/bS4BWhB8bK+O3TMqhuZarTitd0S0eh2pA==
fs-extra@^10.1.0:
version "10.1.0"
resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-10.1.0.tgz#02873cfbc4084dde127eaa5f9905eef2325d1abf"
integrity sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==
dependencies:
graceful-fs "^4.2.0"
jsonfile "^6.0.1"
universalify "^2.0.0"
fs.realpath@^1.0.0: fs.realpath@^1.0.0:
version "1.0.0" version "1.0.0"
resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
@ -2818,6 +2798,11 @@ graceful-fs@^4.1.2, graceful-fs@^4.2.4:
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.8.tgz#e412b8d33f5e006593cbd3cee6df9f2cebbe802a" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.8.tgz#e412b8d33f5e006593cbd3cee6df9f2cebbe802a"
integrity sha512-qkIilPUYcNhJpd33n0GBXTB1MMPp14TxEsEs0pTrsSVucApsYzW5V+Q8Qxhik6KU3evy+qkAAowTByymK0avdg== integrity sha512-qkIilPUYcNhJpd33n0GBXTB1MMPp14TxEsEs0pTrsSVucApsYzW5V+Q8Qxhik6KU3evy+qkAAowTByymK0avdg==
graceful-fs@^4.1.6, graceful-fs@^4.2.0:
version "4.2.10"
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.10.tgz#147d3a006da4ca3ce14728c7aefc287c367d7a6c"
integrity sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==
graphql-extensions@^0.15.0: graphql-extensions@^0.15.0:
version "0.15.0" version "0.15.0"
resolved "https://registry.yarnpkg.com/graphql-extensions/-/graphql-extensions-0.15.0.tgz#3f291f9274876b0c289fa4061909a12678bd9817" resolved "https://registry.yarnpkg.com/graphql-extensions/-/graphql-extensions-0.15.0.tgz#3f291f9274876b0c289fa4061909a12678bd9817"
@ -3810,6 +3795,15 @@ json5@^1.0.1:
dependencies: dependencies:
minimist "^1.2.0" minimist "^1.2.0"
jsonfile@^6.0.1:
version "6.1.0"
resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.1.0.tgz#bc55b2634793c679ec6403094eb13698a6ec0aae"
integrity sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==
dependencies:
universalify "^2.0.0"
optionalDependencies:
graceful-fs "^4.1.6"
jsonwebtoken@^8.5.1: jsonwebtoken@^8.5.1:
version "8.5.1" version "8.5.1"
resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz#00e71e0b8df54c2121a1f26137df2280673bcc0d" resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz#00e71e0b8df54c2121a1f26137df2280673bcc0d"
@ -3978,16 +3972,22 @@ lodash@4.x, lodash@^4.7.0:
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
log4js@^6.4.6:
version "6.4.6"
resolved "https://registry.yarnpkg.com/log4js/-/log4js-6.4.6.tgz#1878aa3f09973298ecb441345fe9dd714e355c15"
integrity sha512-1XMtRBZszmVZqPAOOWczH+Q94AI42mtNWjvjA5RduKTSWjEc56uOBbyM1CJnfN4Ym0wSd8cQ43zOojlSHgRDAw==
dependencies:
date-format "^4.0.9"
debug "^4.3.4"
flatted "^3.2.5"
rfdc "^1.3.0"
streamroller "^3.0.8"
loglevel@^1.6.7: loglevel@^1.6.7:
version "1.7.1" version "1.7.1"
resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.7.1.tgz#005fde2f5e6e47068f935ff28573e125ef72f197" resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.7.1.tgz#005fde2f5e6e47068f935ff28573e125ef72f197"
integrity sha512-Hesni4s5UkWkwCGJMQGAh71PaLUmKFM60dHvq0zi/vDhhrzuk+4GgNbTXJ12YYQJn6ZKBDNIjYcuQGKudvqrIw== integrity sha512-Hesni4s5UkWkwCGJMQGAh71PaLUmKFM60dHvq0zi/vDhhrzuk+4GgNbTXJ12YYQJn6ZKBDNIjYcuQGKudvqrIw==
loglevelnext@^4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/loglevelnext/-/loglevelnext-4.0.1.tgz#4406c6348c243a35272ac75d7d8e4e60ecbcd011"
integrity sha512-/tlMUn5wqgzg9msy0PiWc+8fpVXEuYPq49c2RGyw2NAh0hSrgq6j/Z3YPnwWsILMoFJ+ZT6ePHnWUonkjDnq2Q==
long@^4.0.0: long@^4.0.0:
version "4.0.0" version "4.0.0"
resolved "https://registry.yarnpkg.com/long/-/long-4.0.0.tgz#9a7b71cfb7d361a194ea555241c92f7468d5bf28" resolved "https://registry.yarnpkg.com/long/-/long-4.0.0.tgz#9a7b71cfb7d361a194ea555241c92f7468d5bf28"
@ -4150,11 +4150,6 @@ named-placeholders@^1.1.2:
dependencies: dependencies:
lru-cache "^4.1.3" lru-cache "^4.1.3"
nanoid@^3.1.20:
version "3.1.32"
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.32.tgz#8f96069e6239cc0a9ae8c0d3b41a3b4933a88c0a"
integrity sha512-F8mf7R3iT9bvThBoW4tGXhXFHCctyCiUUPrWF8WaTqa3h96d9QybkSeba43XVOOE3oiLfkVDe4bT8MeGmkrTxw==
natural-compare@^1.4.0: natural-compare@^1.4.0:
version "1.4.0" version "1.4.0"
resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
@ -4746,6 +4741,11 @@ reusify@^1.0.4:
resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76"
integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==
rfdc@^1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.3.0.tgz#d0b7c441ab2720d05dc4cf26e01c89631d9da08b"
integrity sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA==
rimraf@^3.0.0, rimraf@^3.0.2: rimraf@^3.0.0, rimraf@^3.0.2:
version "3.0.2" version "3.0.2"
resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a"
@ -4981,6 +4981,15 @@ stack-utils@^2.0.3:
resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c"
integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow= integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=
streamroller@^3.0.8:
version "3.0.8"
resolved "https://registry.yarnpkg.com/streamroller/-/streamroller-3.0.8.tgz#84b190e4080ee311ca1ebe0444e30ac8eedd028d"
integrity sha512-VI+ni3czbFZrd1MrlybxykWZ8sMDCMtTU7YJyhgb9M5X6d1DDxLdJr+gSnmRpXPMnIWxWKMaAE8K0WumBp3lDg==
dependencies:
date-format "^4.0.9"
debug "^4.3.4"
fs-extra "^10.1.0"
streamsearch@0.1.2: streamsearch@0.1.2:
version "0.1.2" version "0.1.2"
resolved "https://registry.yarnpkg.com/streamsearch/-/streamsearch-0.1.2.tgz#808b9d0e56fc273d809ba57338e929919a1a9f1a" resolved "https://registry.yarnpkg.com/streamsearch/-/streamsearch-0.1.2.tgz#808b9d0e56fc273d809ba57338e929919a1a9f1a"
@ -5363,6 +5372,11 @@ universalify@^0.1.2:
resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66" resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66"
integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg== integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==
universalify@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.0.tgz#75a4984efedc4b08975c5aeb73f530d02df25717"
integrity sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==
unpipe@1.0.0, unpipe@~1.0.0: unpipe@1.0.0, unpipe@~1.0.0:
version "1.0.0" version "1.0.0"
resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec"

View File

@ -5,4 +5,5 @@ module.exports = {
trailingComma: "all", trailingComma: "all",
tabWidth: 2, tabWidth: 2,
bracketSpacing: true, bracketSpacing: true,
endOfLine: "auto",
}; };

View File

@ -5,7 +5,7 @@
"main": "src/index.ts", "main": "src/index.ts",
"repository": "https://github.com/gradido/gradido/database", "repository": "https://github.com/gradido/gradido/database",
"author": "Ulf Gebhardt", "author": "Ulf Gebhardt",
"license": "MIT", "license": "Apache-2.0",
"private": false, "private": false,
"scripts": { "scripts": {
"build": "mkdir -p build/src/config/ && cp src/config/*.txt build/src/config/ && tsc --build", "build": "mkdir -p build/src/config/ && cp src/config/*.txt build/src/config/ && tsc --build",

View File

@ -0,0 +1 @@
<mxfile host="Electron" modified="2022-05-23T15:29:00.513Z" agent="5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) draw.io/16.0.2 Chrome/96.0.4664.55 Electron/16.0.5 Safari/537.36" etag="IpEbe50ld79E_2HLyFXN" version="16.0.2" type="device"><diagram id="LWCFUfVkWozhiXBGj4Ze" name="Page-1">7VxbU9s6EP41eaTju5NHEug5D7TDlHZanhhhK45AtjKyQpL++rNy5PgiJ4Rgx4c2M8xgrVe2tN/u6vNKMLAn8eofjuazLyzEdGAZ4WpgXw0syzQNC35JyXojGXqjjSDiJFRKheCO/MZKaCjpgoQ4rSgKxqgg86owYEmCA1GRIc7Zsqo2ZbT61jmKsCa4CxDVpT9JKGZqFq5RyP/FJJqJ7YTVnRjlykqQzlDIliWRfT2wJ5wxsbmKVxNMpfFyu2z6fd5xdzswjhNxSIcoffphXUeTWCx/jtyft/OXr+GFQucF0YWa8MDyKDxvPIMXeJG8+kzZMpghLkARUA5JyOBqwjEShCVp3gHeXPRRUxbr3I4Cr7KnipiCwITLVHD2jCeMMg6ShCWgOZ4SSmuidI4CkkQgcIvWdzYHwQVM2R4vZ0TgO5DLVy3BDUHGXjCf0szcMxKGOAEZZ4skxNIaxnaEoAYj22lRc4sTODhmMRZ8DSqqg+UoaJVv26q5LBzF9pVsVnKSvBtSvhltn1zABxcKwTeg6Tqa3XEI7qyajIsZi1iC6HUhrZml0LlhmY0lVE9YiLWKTbQQrAokXhHxS3b/5KrWfenO1Uo9OWusK9aXg9tve5gLW/AA75m0rbIC4hEWe/RMuxlMjik48kt1IK1DY2uBNmGJ4ORxoYKohlsVlSYXLyHQhi8bw4ovm5buzFtZ2Zm9zpzZ0IxyUmf2D/dmsDFf/yo37ot4kM2iW9bqIAqcU0VB1vWSc7QuKcwZSURaevKtFBTeZTpuLVPW1qqavm0N9+nDxWYEhXdtp/IOh7M/Svb8QP62AbKvrOuZR0AKRG0u704pXl1KCtkuzNYxOH8yHLOM9QVIzNfgzlq3mBMwJub9+UCvLuBbfUR1D1b2jXea+ajMblcpsOm+ktir6q7KuZ3mdWcv9YI7NyR57p+BudUl8n9AwNx+18NjEmUlSZ50QXRPRcDeh6n34TjOB4DU7ZXjHENbO+Y4x3w71XD2rL7pzaHwe1avEa2tbpdhTJLelzPPqzGD3tezoWap7xwlKQqaKzDpksQUbaqSwBfyeJLWC2aEhjdozRZyzKlAwXPeGs8YJ79BHxV1TsSFihnbqGjcyZ7qmRynoHOb29esib6gVUXxBqUiHw2jFM1T8piNT3aMwWVJMmZCsLgSF20SFL+h3Ombrg6oae9BVL3tGw4ESiKYQUEVvdr7hg0OZDQ4UP11iEJ6SJDAYxkCqeZHLVDMkeZaonCtB7GGXFt3L1UQP7QKjiiJEmhSPJXdJJAkQPRSiWMShpvEvqmP32RqV04h+aYM5OyojiuiD2Nzx/ADNpzIxdaFsU6gbRZt+JHqXACNhuEjkrkPBodcYumUsJAIJNDjNlwO8r23fDnurrc3OuA+/3tXRsk3qUq4L1LMH6DTGe7W4fZ6h9vU4A5KX5IPFL4jz9h3gv3oQOw721rzjtla64ZlF0XBLUO+rxDkzj+MTOtDUON8mAdvuv1ZlC8kHGcERIVsOyywvjWzDbkyLRs1peW8Y/s465ur52X4HanY3BG1u2lXM+CdrcN6RbeyDofA9c/Id4B8AwM7LfKWXu0I5DkkHD4g0THkQq7Px+NdB6lF/DvDu4F1NeJteV3h7Wl4xzhmZ6TbRto0vb6h9jWoUQxc+RzW7YPtWD2Dbep5PGYh5kiwM23rCnVv2PfqrVfge6if/I0BPzoQ+u6yu14hB+inhMdn6tYJ5PBd3jPktl4cLyB/XJ8hbx1yt2+67uh0vZLgT7AR9jfi7vfN3R2du3P8hANxjvSOKnB9E3jvmHNknW+HmP7gDRsiXZ8f8g7dJen3iLx/BJItojbo63TfwfBsstvpD1ZX91k857WT1VV9Vx3X6fRodW7DUuK/xzApQ77XMr7qlTp4KJmn0oe6P4JWP0HkmQ0EqekI2rCzXWT9C/hHivmfvhPZApZm/S8HjIZPWqcBy86OE/qN3zfn0yCt1LK8N5/8agS/s1KWr285k2m2JkQppDWOd+J+xF/A5y4QgM0kUdntBE05tbpWZxBtzgRYfkuxuT0Hl6PTdFLTbAceaBb/MmGzjBX/eMK+/g8=</diagram></mxfile>

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

View File

@ -0,0 +1,81 @@
# Business Event Protocol
With the business event protocol the gradido application will capture and persist business information for future reports and statistics. The idea is to design and implement general functionality to capture and store business events. Each business event will be defined as a separate event type with its own business attributes. Each event type extends a basic event type to ensure a type safetiness with its mandatory and optional attributes.
## EventType - Enum
The different event types will be defined as Enum. The following list is a first draft and will grow with further event types in the future.
| EventType | Value | Description |
| --------------------------- | ----- | ---------------------------------------------------------------------------------------------------- |
| BasicEvent | 0 | the basic event is the root of all further extending event types |
| VisitGradidoEvent | 10 | if a user visits a gradido page without login or register |
| RegisterEvent | 20 | the user presses the register button |
| RedeemRegisterEvent | 21 | the user presses the register button initiated by the redeem link |
| InActiveAccountEvent | 22 | the systems create an inactive account during the register process |
| SendConfirmEmailEvent | 23 | the system send a confirmation email to the user during the register process |
| ConfirmEmailEvent | 24 | the user confirms his email during the register process |
| RegisterEmailKlickTippEvent | 25 | the system registers the confirmed email at klicktipp |
| LoginEvent | 30 | the user presses the login button |
| RedeemLoginEvent | 31 | the user presses the login button initiated by the redeem link |
| ActivateAccountEvent | 32 | the system activates the users account during the first login process |
| PasswordChangeEvent | 33 | the user changes his password |
| TxSendEvent | 40 | the user creates a transaction and sends it online |
| TxSendRedeemEvent | 41 | the user creates a transaction and sends it per redeem link |
| TxRepeateRedeemEvent | 42 | the user recreates a redeem link of a still open transaction |
| TxCreationEvent | 50 | the user receives a creation transaction for his confirmed contribution |
| TxReceiveEvent | 51 | the user receives a transaction from an other user and posts the amount on his account |
| TxReceiveRedeemEvent | 52 | the user activates the redeem link and receives the transaction and posts the amount on his account |
| ContribCreateEvent | 60 | the user enters his contribution and asks for confirmation |
| ContribConfirmEvent | 61 | the user confirms a contribution of an other user (for future multi confirmation from several users) |
| | | |
## EventProtocol - Entity
The business events will be stored in database in the new table `EventProtocol`. The tabel will have the following attributes:
| Attribute | Type | Description |
| ------------- | --------- | ------------------------------------------------------------------------------------------------ |
| id | int | technical unique key (from db sequence) |
| type | enum | type of event |
| createdAt | timestamp | timestamp the event occurs (not the time of writing) |
| userID | string | the user ID, who invokes the event |
| XuserID | string | the cross user ID, who is involved in the process like a tx-sender, contrib-receiver, ... |
| XcommunityID | string | the cross community ID, which is involved in the process like a tx-sender, contrib-receiver, ... |
| transactionID | int | the technical key of the transaction, which triggers the event |
| contribID | int | the technical key of the contribution, which triggers the event |
| amount | digital | the amount of gradido transferred by transaction, creation or redeem |
## Event Types
The following table lists for each event type the mandatory attributes, which have to be initialized at event occurence and to be written in the database event protocol table:
| EventType | id | type | createdAt | userID | XuserID | XCommunityID | transactionID | contribID | amount |
| :-------------------------- | :-: | :--: | :-------: | :----: | :-----: | :----------: | :-----------: | :-------: | :----: |
| BasicEvent | x | x | x | | | | | | |
| VisitGradidoEvent | x | x | x | | | | | | |
| RegisterEvent | x | x | x | x | | | | | |
| RedeemRegisterEvent | x | x | x | x | | | | | |
| InActiveAccountEvent | x | x | x | x | | | | | |
| SendConfirmEmailEvent | x | x | x | x | | | | | |
| ConfirmEmailEvent | x | x | x | x | | | | | |
| RegisterEmailKlickTippEvent | x | x | x | x | | | | | |
| LoginEvent | x | x | x | x | | | | | |
| RedeemLoginEvent | x | x | x | x | | | | | |
| ActivateAccountEvent | x | x | x | x | | | | | |
| PasswordChangeEvent | x | x | x | x | | | | | |
| TxSendEvent | x | x | x | x | x | x | x | | x |
| TxSendRedeemEvent | x | x | x | x | x | x | x | | x |
| TxRepeateRedeemEvent | x | x | x | x | x | x | x | | x |
| TxCreationEvent | x | x | x | x | | | x | | x |
| TxReceiveEvent | x | x | x | x | x | x | x | | x |
| TxReceiveRedeemEvent | x | x | x | x | x | x | x | | x |
| ContribCreateEvent | x | x | x | x | | | | x | |
| ContribConfirmEvent | x | x | x | x | x | x | | x | |
| | | | | | | | | | |
## Event creation
The business logic needs a *general event creation* service/methode, which accepts as input one of the predefined event type objects. An event object have to be initialized with its mandatory attributes before it can be given as input parameters for event creation. The service maps the event object attributes to the database entity and writes a new entry in the `EventProtocol `table.
At each specific location of the gradido business logic an event creation invocation has to be introduced manually, which matches the corresponding event type - see [EventType-Enum](#EventType-Enum) above.

View File

@ -1,8 +1,10 @@
# Federation # Federation
This document contains the concept and technical details for the *federation* of gradido communities. It base on the [ActivityPub specification](https://www.w3.org/TR/activitypub/ " ") and is extended for the gradido requirements. This document contains the concept and technical details for the *federation* of gradido communities. The first idea of federation was to base on the [ActivityPub specification](https://www.w3.org/TR/activitypub/ " ") and extend it for the gradido requirements.
## ActivityPub But meanwhile the usage of a DHT like HyperSwarm promises more coverage of the gradido requirements out of the box. More details about HyperSwarm can be found here [@hyperswarm/dht](https://github.com/hyperswarm/dht).
## ActivityPub (deprecated)
The activity pub defines a server-to-server federation protocol to share information between decentralized instances and will be the main komponent for the gradido community federation. The activity pub defines a server-to-server federation protocol to share information between decentralized instances and will be the main komponent for the gradido community federation.
@ -25,8 +27,387 @@ The Variant A with an internal server contains the benefit to be as independent
The Varaint B with an external server contains the benefit to reduce the implementation efforts and the responsibility for an own ActivitPub-Server. But it will cause an additional dependency to a third party service provider and the growing hosting costs. The Varaint B with an external server contains the benefit to reduce the implementation efforts and the responsibility for an own ActivitPub-Server. But it will cause an additional dependency to a third party service provider and the growing hosting costs.
## HyperSwarm
The decision to switch from ActivityPub to HyperSwarm base on the arguments, that the *hyperswarm/dht* library will satify the most federation requirements out of the box. It is now to design the business requirements of the [gradido community communication](../BusinessRequirements/CommunityVerwaltung.md#UC-createCommunity) in a technical conception.
## ActivityStream The challenge for the decentralized communities of gradido will be *how to become a new community aquainted with an existing community* ?
An ActivityStream includes all definitions and terms needed for community activities and content flow around the gradido community network. To enable such a relationship between an existing community and a new community several stages has to run through:
1. Federation
* join&connect
* direct exchange
2. Authentication
3. Autorized Communication
### Overview
At first the following diagramm gives an overview of the three stages and shows the handshake between an existing community-A and a new created community-B including the data exchange for buildup such a federated, authenticated and autorized relationship.
![FederationHyperSwarm.png](./image/FederationHyperSwarm.png)
### Prerequisits
Before starting in describing the details of the federation handshake, some prerequisits have to be defined.
#### Database
With the federation additional data tables/entities have to be created.
##### Community-Entity
Create the new *Community* table to store attributes of the own community. This table is used more like a frame for own community data in the future like the list of federated foreign communities, own users, own futher accounts like AUF- and Welfare-account and the profile data of the own community:
| Attributes | Type | Description |
| ----------- | ------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
| id | int | technical unique key of this entity |
| uuid | string | unique key for a community, which will never changed as long as the community exists |
| name | string | name of the community shown on UI e.g for selection of a community |
| description | string | description of the community shown on UI to present more community details |
| ... | | for the near future additional attributes like profile-info, trading-level, set-of-rights,... will follow with the next level of Multi-Community Readyness |
##### CommunityFederation-Entity
Create the new *CommunityFederation* table to store at this point of time only the attributes used by the federation handshake:
| Attributes | Type | Description |
| ---------------- | --------- | ------------------------------------------------------------------------------------------------------ |
| id | int | technical unique key of this entity |
| uuid | string | unique key for a community, which will never changed as long as the community exists |
| foreign | boolean | flag to mark the entry as a foreign or own community entry |
| createdAt | timestamp | the timestamp the community entry was created |
| privateKey | string | the private key of the community for asynchron encryption (only set for the own community) |
| pubKey | string | the public key of the community for asynchron encryption |
| pubKeyVerifiedAt | timestamp | the timestamp the pubkey of this foreign community is verified (for the own community its always null) |
| authenticatedAt | timestamp | the timestamp of the last successfull authentication with this foreign community |
| | | for the near future additional attributes will follow with the next level of Multi-Community Readyness |
##### CommunityApiVersion-Entity
Create the new *CommunityApiVersion* table to support several urls and apiversion of one community at once. It references the table *CommunityFederation* with the foreignkey *communityFederationID* (naming pattern foreignkea = `<referred entityname>ID`) for a 1:n relationship
| Attributes | Type | Description |
| --------------------- | --------- | ------------------------------------------------------------------------------------------------------------------ |
| id | int | technical unique key of this entity |
| communityFederationID | int | the technical foreign key to the community entity |
| url | string | the URL the community will provide its services, could be changed during lifetime of the community |
| apiversion | string | the API version the community will provide its services, will increase with each release |
| validFrom | timestamp | the timestamp as of the url and api are provided by the community |
| verifiedAt | timestamp | the timestamp the url and apiversion of this foreign community is verified (for the own community its always null) |
#### Configuration
The preparation of a community infrastructure needs some application-outside configuration, which will be read during the start phase of the gradido components. The configuration will be defined in a file based manner like key-value-pairs as properties or in a structured way like xml or json files.
| Key | Value | Default-Value | Description |
| ----------------------------------------------- | :------------------------------------ | --------------------- | ------------------------------------------------------------------------------------------------- |
| stage.name | dev<br />stage1<br />stage2<br />prod | dev | defines the name of the stage this instance will serve |
| stage.host | | | the name of the host or ip this instance will run |
| stage.mode | test<br />prod | test | the running mode this instance will work |
| federation.communityname | | Gradido-Akademie | the name of this community |
| federation.apiversion | `<versionNr>` | current 1.7 | defines the current api version this instance will provide its services |
| federation.apiversion.`<versionNr>.`url | | <br />gdd.gradido.net | <br />defines the url on which this instance of a community will provide its services |
| federation.apiversion.`<versionNr>.`validFrom | | | defines the timestamp the apiversion is or will be valid |
| federation.dhtnode.topic | | dht_gradido_topic | defines the name of the federation topic, which is used to join and connect as federation-channel |
| federation.dhtnode.host | | | defines the host where the DHT-Node is hosted, if outside apollo |
| federation.dhtnode.port | | | defines the port on which the DHT-node will provide its services, if outside apollo |
#### 1st Start of a community
The first time a new community infrastructure on a server is started, the start-phase has to check and prepair the own community database for federation. That means the application has to read the configuration and check against the database, if all current configured data is propagated in the database especially in the *CommunityXXX* entities.
* check if the*Community* table is empty or if an exisiting community entry is not equals the configured values, then update as follows:
* community.id = next sequence value
* community.uuid = generated UUID (version 4)
* community.name = Configuration.federation.communityname
* community.description = null
* prepare the *CommunityFederation* table
* communityFederation.id = next sequence value
* communityFederation.uuid = community.uuid
* communityFederation.foreign = FALSE
* communityFederation.createdAt = NOW
* communityFederation.privateKey = null
* communityFederation.pubKey = null
* communityFederation.pubKeyVerifiedAt = null
* communityFederation.authenticatedAt = null
* prepare the *CommunityApiVersion* table with all configured apiversions:
* communityApiVersion.id = next sequence value
* communityApiVersion.communityFederationID = communityFederation.id
* communityApiVersion.url = Configuration.federation.apiversion.`<versionNr>`.url
* communityApiVersion.apiversion = Configuration.federation.apiversion
* communityApiVersion.validFrom = Configuration.federation.apiversion.`<versionNr>`.validFrom
* communityApiVersion.verifiedAt = null
### Stage1 - Federation
For the 1st stage the *hyperswarm dht library* will be used. It supports an easy way to connect a new community with other existing communities. As shown in the picture above the *hyperswarm dht library* will be part of the component *DHT-Node* separated from the *apollo server* component. The background for this separation is to keep off the federation activity from the business processes or to enable component specific scaling in the future. In consequence for the inter-component communication between *DHT-Node*, *apollo server* and other components like *database* the interface and security has to be defined during development on using technical standards.
For the first federation release the *DHT-Node* will be part of the *apollo server*, but internally designed and implemented as a logical saparated component.
#### Sequence join&connect
1. In the own database of community_A the entites *Community*, *CommunityFederation* and *CommunityApiVersion* are initialized
2. When starting the *DHT-Node* of community_A it search per *apollo-ORM* for the own community entry and check on existing keypair *CommunityFederation.pubKey* and *CommunityFederation.privateKey* in the database. If they not exist, the *DHT-Node* generates the keypair *pubkey* and *privatekey* and writes them per *apollo-ORM* in the database
3. For joining with the correct channel of *hyperswarm dht* a topic has to be used. The *DHT-Node* reads the configured value of the property *federation.dhtnode.topic*.
4. with the *CommunityFederation.pubKey* and the *federation.dhtnode.topic* the *DHT-Node* joins the *hyperswarm dht* and listen for other *DHT-nodes* on the topic.
5. As soon as a the *hyperswarm dht* notifies an additional node in the topic, the *DHT-node* reads the *pubKey* of this additional node and search it per *apollo-ORM* in the *CommunityFederation* table by filtering with *CommunityFederation.foreign* = TRUE
6. if an entry with the C*ommunityFederation.pubKey* of the foreign node still exists and the *CommunityFederation.pubKeyVerifiedAt* is not NULL both the *DHT-node* and the foreign node had pass through the federation process before. Nevertheless the following steps and stages have to be processed for updating e.g the api versions or other meanwhile changed date.
7. if an entry with the *CommunityFederation.pubKey* of the additional node can't be found, the *DHT-Node* starts with the next step *direct exchange* of the federation handshake anyway.
#### Sequence direct exchange
1. if the *CommunityFederation.pubKey* of the additional node does not exists in the *CommunityFederation* table the *DHT-node* starts a *direct exchange* with this foreign node to gets the data the first time, otherwise to update previous exchanged data.
2. the *DHT-node* opens a direct connection per *hyperswarm* with the additional node and exchange respectively the *url* and *apiversion* between each other.
1. to support the future feature that one community can provide several urls and apiversion at once the exchanged data should be in a format, which can represent structured information, like JSON (or simply CSV - feel free how to implement, but be aware about security aspects to avoid possible attacks during parsing the exchanged data and the used parsers for it)
```
{
"API":
{
"url" : "comB.com",
"version" : "1.0",
"validFrom" : "2022.01.01"
}
"API" :
{
"url" : "comB.com",
"version" : "1.1",
"validFrom" : "2022.04.15"
}
"API" :
{
"url" : "comB.de",
"version" : "2.0",
"validFrom" : "2022.06.01"
}
}
```
2. the *DHT-Node* writes per *apollo-ORM* the received and parsed data from the foreign node in the database
1. For the future an optimization step will be introduced here to avoid possible attacks of a foreign node by polute our database with mass of data.
1. before the *apollo-ORM* writes the data in the database, the *apollo-graphQL* invokes for all received urls and apiversions at the foreign node the request https.//`<url>/<apiversion>/getPubKey()`.
2. Normally the foreign node will response in a very short time with its publicKey, because there will be nothing to en- or decrypt or other complex processing steps.
3. if such a request runs in a timeout anyhow, the previous exchanged data with the foreign node will be almost certainly a fake and can be refused without storing in database. Break the further federation processing steps and stages and return back to stage1 join&connect.
4. if the response is in time the received publicKey must be equals with the pubKey of the foreign node the *DHT-Node* gets from *hyperswarm dht* per topic during the join&connect stage before
5. if both keys are the same, the writing of the exchanged data per *apollo-ORM* can go on.
6. if both keys will not match the exchanged data during the direct connection will be almost certainly a fake and can be refused without storing in database. Break the further federation processing steps and stages and return back to stage1 join&connect.
2. the *apollo-ORM* inserts / updates or deletes the received data as follow
* insert/update in the *CommunityFederation* table for this foreign node:
| Column | insert | update |
| ------------------------------------ | -------------------- | :------------------ |
| communityFederation.id | next sequence value | keep existing value |
| communityFederation.uuid | null | keep existing value |
| communityFederation.foreign | TRUE | keep existing value |
| communityFederation.createdAt | NOW | keep existing value |
| communityFederation.privateKey | null | keep existing value |
| communityFederation.pubKey | exchangedData.pubKey | keep existing value |
| communityFederation.pubKeyVerifiedAt | null | keep existing value |
| communityFederation.authenticatedAt | null | keep existing value |
* for each exchangedData API
if API not exists in database then insert in the *CommunityApiVersion* table:
| Column | insert |
| ----------------------------------------- | --------------------------- |
| communityApiVersion.id | next sequence value |
| communityApiVersion.communityFederationID | communityFederation.id |
| communityApiVersion.url | exchangedData.API.url |
| communityApiVersion.apiversion | exchangedData.API.version |
| communityApiVersion.validFrom | exchangedData.API.validFrom |
| communityApiVersion.verifiedAt | null |
if API exists in database but was not part of the last data exchange, then delete it from the *CommunityApiVersion* table
if API exists in database and was part of the last data exchange, then update it in the *CommunityApiVersion* table
| Column | update |
| ----------------------------------------- | --------------------------- |
| communityApiVersion.id | keep existing value |
| communityApiVersion.communityFederationID | keep existing value |
| communityApiVersion.url | keep existing value |
| communityApiVersion.apiversion | keep existing value |
| communityApiVersion.validFrom | exchangedData.API.validFrom |
| communityApiVersion.verifiedAt | keep existing value |
*
3. After all received data is stored successfully, the *DHT-Node* starts the *stage2 - Authentication* of the federation handshake
### Stage2 - Authentication
The 2nd stage of federation is called *authentication*, because during the 1st stage the *hyperswarm dht* only ensures the knowledge that one node is the owner of its keypairs *pubKey* and *privateKey*. The exchanged data between two nodes during the *direct exchange* on the *hyperswarm dht channel* must be verified, means ensure if the proclaimed *url(s)* and *apiversion(s)* of a node is the correct address to reach the same node outside the hyperswarm infrastructure.
As mentioned before the *DHT-node* invokes the *authentication* stage on *apollo server* *graphQL* with the previous stored data of the foreign node.
#### Sequence - view of existing Community
1. the authentication stage starts by reading for the *foreignNode* from the previous federation step all necessary data
1. select with the *foreignNode.pubKey* from the tables *CommunityFederation* and *CommunityApiVersion* where *CommunityApiVersion.validFrom* <= NOW and *CommunityApiVersion.verifiedAt* = null
2. the resultSet will be a list of data with the following attributes
* foreignNode.pubKey
* foreignNode.url
* foreignNode.apiVersion
2. read the own keypair and uuid by `select uuid, privateKey, pubKey from CommunityFederation cf where cf.foreign = FALSE`
3. for each entry of the resultSet from step 1 do
1. encryptedURL = encrypting the *foreignNode.url* and *foreignNode.apiVersion* with the *foreignNode.pubKey*
2. signedAndEncryptedURL = sign the result of the encryption with the own *privateKey*
3. invoke the request `https://<foreignNode.url>/<foreignNode.apiVersion>/openConnection(own.pubKey, signedAndEncryptedURL )`
4. the foreign node will response immediately with an empty response OK, otherwise break the authentication stage with an error
4. the foreign node will process the request on its side - see [description below](#Sequence - view of new Community) - and invokes a redirect request base on the previous exchanged data during stage1 - Federation. This could be more than one redirect request depending on the amount of supported urls and apiversions we propagate to the foreignNode before.
1. if the other community will not react with an `openConnectionRedirect`-request, ther will be an error like missmatching data and the further federation processing will end and go back to join&connect.
5. for each received request `https://<own.url>/<own.apiVersion>/openConnectionRedirect(onetimecode, foreignNode.url, encryptedRedirectURL )` do
1. with the given parameter the following steps will be done
1. search for the *foreignNode.pubKey* by `select cf.pubKey from CommunityApiVersion cav, CommunityFederation cf where cav.url = foreignNode.url and cav.communityFederationID = cf.id`
2. decrypt with the `own.privateKey` the received `encryptedRedirectURL` parameter, which contains a full qualified url inc. apiversion and route
3. verify signature of `encryptedRedirectURL` with the previous found *foreignNode.pubKey* from the own database
4. if the decryption and signature verification are successful then encrypt the *own.uuid* with the *own.privateKey* to *encryptedOwnUUID*
5. invoke the redirect request with https://`<redirect.URL>(onetimecode, encryptedOwnUUID)` and
6. wait for the response with the `encryptedForeignUUID`
7. decrypt the `encrpytedForeignUUID` with the *foreignNode.pubKey*
8. write the encrypted *foreignNode.UUID* in the database by updating the CommunityFederation table per `update CommunityFederation cf set values (cf.uuid = foreignNode.UUID, cf.pubKeyVerifiedAt = NOW) where cf.pubKey = foreignNode.pubkey`
After all redirect requests are process, all relevant authentication data of the new community are well know here and stored in the database.
#### Sequence - view of new Community
This chapter contains the description of the Authentication Stage on the new community side as the request `openConnection(pubKey, signedAndEncryptedURL)`
As soon the *openConnection* request is invoked:
1. decrypted the 2nd `parameter.signedAndEncryptedURL` with the own *privatKey*
2. with the 1st parameter *pubKey* search in the own database `select uuid, url, pubKey from CommunityFederation cf where cf.foreign = TRUE and cf.pubKey = parameter.pubKey`
3. check if the decrypted `parameter.signedAndEncryptedURL` is equals the selected url from the previous selected CommunityFederationEntry
1. if not then break the further processing of this request by only writing an error-log event. There will be no answer to the invoker community, because this community will only go on with a `openConnectionRedirect`-request from this community.
2. if yes then verify the signature of `parameter.signedAndEncryptedURL` with the `cf.pubKey` read in step 2 before
3.
4.
### Stage3 - Autorized Business Communication
ongoing
# Review von Ulf
## Communication concept
The communication happens in 4 stages.
- Stage1: Federation
- Stage2: Direct-Connection
- Stage3: GraphQL-Verification
- Stage4: GraphQL-Content
### Stage1 - Federation
Using the hyperswarm dht library we can find eachother easily and exchange a pubKey and data of which we know that the other side owns the private key of.
```
ComA ---- announce ----> DHT
ComB <--- listen ------- DHT
```
Each peer will know the `pubKey` of the other participants. Furthermore a direct connection is possible.
```
ComB ---- connect -----> ComA
ComB ---- data --------> ComA
```
### Stage2 - Direct-Connection
The hyperswarm dht library offers a secure channel based on the exchanged `pubKey` so we do not need to verify things.
The Idea is now to exchange the GraphQL Endpoints and their corresponding versions API versions in form of json
```
{
"API": {
"1.0": "https://comB.com/api/1.0/",
"1.1": "https://comB.com/api/1.1/",
"2.4": "https://comB.de/api/2.4/"
}
}
```
### Stage3 - GraphQL-Verification
The task of Stage3 is to verify that the collected data through the two Federation Stages are correct, since we did not verify yet that the proclaimed URL is actually the guy we talked to in the federation. Furthermore the sender must be verified to ensure the queried community does not reveal things to a third party not authorized.
```
ComA ----- verify -----> ComB
ComA <---- authorize --- ComB
```
Assuming this Dataset on ComA after a federation (leaving out multiple API endpoints to simplify things):
```
| PubKey | API-Endpoint | PubKey Verified On |
|--------|---------------|--------------------|
| PubA* | ComA.com/api/ | NULL |
| PubB | ComB.com/api/ | NULL |
| PubC | ComB.com/api/ | NULL |
* = self
```
using the GraphQL Endpoint to query things:
```
ComA ---- getPubKey ---> ComB.com
ComA <--- PubB --------- ComB.com
ComA UPDATE database SET pubKeyVerifiedOn = now WHERE API-Endpoint=queryURL AND PubKey=QueryResult
```
resulting in:
```
| PubKey | API-Endpoint | PubKey Verified On |
|--------|---------------|--------------------|
| PubA* | ComA.com/api/ | 1.1.1970 |
| PubB | ComB.com/api/ | NOW |
| PubC | ComB.com/api/ | NULL |
```
Furthermore we use the Header to transport a proof of who the caller is when calling and when answering:
```
ComA ---- getPubKey, sign({pubA, crypt(timeToken,pubB)},privA) --> ComB.com
ComB: is pubA known to me?
ComB: is the signature correct?
ComB: can I decrypt payload?
ComB: is timeToken <= 10sec?
ComA <----- PubB, sign({timeToken}, privB) ----------------------- ComB.com
ComA: is timeToken correct?
ComA: is signature correct?
```
This process we call authentication and can result in several errors:
1. Your pubKey was not known to me
2. Your signature is incorrect
3. I cannot decrypt your payload
4. Token Timeout (recoverable)
5. Result token was incorrect
6. Result signature was incorrect
```
| PubKey | API-Endpoint | PubKey Verified On | AuthenticationLastSuccess |
|--------|---------------|--------------------|----------------------------|
| PubA* | ComA.com/api/ | 1.1.1970 | 1.1.1970 |
| PubB | ComB.com/api/ | NOW | NOW |
| PubC | ComB.com/api/ | NULL | NULL |
```
The next process is the authorization. This happens on every call on the receiver site to determine which call is allowed for the other side.
```
ComA ---- getPubKey, sign({pubA, crypt(timeToken,pubB)},privA) --> ComB.com
ComB: did I verify pubA? SELECT PubKeyVerifiedOn FROm database WHERE PubKey = pubA
ComB: is pubA allowed to query this?
```

View File

@ -0,0 +1,140 @@
# Introduction of Gradido-ID
## Motivation
To introduce the Gradido-ID base on the requirement to identify an user account per technical key instead of using an email-address. Such a technical key ensures an exact identification of an user account without giving detailed information for possible missusage.
Additionally the Gradido-ID allows to administrade any user account data like changing the email address or define several email addresses without any side effects on the identification of the user account.
## Definition
The formalized definition of the Gradido-ID can be found in the document [BenutzerVerwaltung#Gradido-ID](../BusinessRequirements/BenutzerVerwaltung#Gradido-ID).
## Steps of Introduction
To Introduce the Gradido-ID there are several steps necessary. The first step is to define a proper database schema with additional columns and tables followed by data migration steps to add or initialize the new columns and tables by keeping valid data at all.
The second step is to decribe all concerning business logic processes, which have to be adapted by introducing the Gradido-ID.
### Database-Schema
#### Users-Table
The entity users has to be changed by adding the following columns.
| Column | Type | Description |
| ------------------------ | ------ | -------------------------------------------------------------------------------------- |
| gradidoID | String | technical unique key of the user as UUID (version 4) |
| alias | String | a business unique key of the user |
| passphraseEncryptionType | int | defines the type of encrypting the passphrase: 1 = email (default), 2 = gradidoID, ... |
| emailID | int | technical foreign key to the new entity Contact |
##### Email vs emailID
The existing column `email`, will now be changed to the primary email contact, which will be stored as a contact entry in the new `UserContacts` table. It is necessary to decide if the content of the `email `will be changed to the foreign key `emailID `to the contact entry with the email address or if the email itself will be kept as a denormalized and duplicate value in the `users `table.
The preferred and proper solution will be to add a new column `Users.emailId `as foreign key to the `UsersContact `entry and delete the `Users.email` column after the migration of the email address in the `UsersContact `table.
#### new UserContacts-Table
A new entity `UserContacts `is introduced to store several contacts of different types like email, telephone or other kinds of contact addresses.
| Column | Type | Description |
| --------------- | ------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| id | int | the technical key of a contact entity |
| type | int | Defines the type of contact entry as enum: Email, Phone, etc |
| usersID | int | Defines the foreign key to the `Users` table |
| email | String | defines the address of a contact entry of type Email |
| phone | String | defines the address of a contact entry of type Phone |
| contactChannels | String | define the contact channel as comma separated list for which this entry is confirmed by the user e.g. main contact (default), infomail, contracting, advertisings, ... |
### Database-Migration
After the adaption of the database schema and to keep valid consistent data, there must be several steps of data migration to initialize the new and changed columns and tables.
#### Initialize GradidoID
In a one-time migration create for each entry of the `Users `tabel an unique UUID (version4).
#### Primary Email Contact
In a one-time migration read for each entry of the `Users `table the `Users.id` and `Users.email` and create for it a new entry in the `UsersContact `table, by initializing the contact-values with:
* id = new technical key
* type = Enum-Email
* userID = `Users.id`
* email = `Users.email`
* phone = null
* usedChannel = Enum-"main contact"
and update the `Users `entry with `Users.emailId = UsersContact.Id` and `Users.passphraseEncryptionType = 1`
After this one-time migration the column `Users.email` can be deleted.
### Adaption of BusinessLogic
The following logic or business processes has to be adapted for introducing the Gradido-ID
#### Read-Write Access of Users-Table especially Email
The ORM mapping has to be adapted to the changed and new database schema.
#### Registration Process
The logic of the registration process has to be adapted by
* initializing the `Users.userID` with a unique UUID
* creating a new `UsersContact `entry with the given email address and *maincontact* as `usedChannel `
* set `emailID `in the `Users `table as foreign key to the new `UsersContact `entry
* set `Users.passphraseEncrpytionType = 2` and encrypt the passphrase with the `Users.userID` instead of the `UsersContact.email`
#### Login Process
The logic of the login process has to be adapted by
* search the users data by reading the `Users `and the `UsersContact` table with the email (or alias as soon as the user can maintain his profil with an alias) as input
* depending on the `Users.passphraseEncryptionType` decrypt the stored password
* = 1 : with the email
* = 2 : with the userID
#### Password En/Decryption
The logic of the password en/decryption has to be adapted by encapsulate the logic to be controlled with an input parameter. The input parameter can be the email or the userID.
#### Change Password Process
The logic of change password has to be adapted by
* if the `Users.passphraseEncryptionType` = 1, then
* read the users email address from the `UsersContact `table
* give the email address as input for the password decryption of the existing password
* use the `Users.userID` as input for the password encryption fo the new password
* change the `Users.passphraseEnrycptionType` to the new value =2
* if the `Users.passphraseEncryptionType` = 2, then
* give the `Users.userID` as input for the password decryption of the existing password
* use the `Users.userID` as input for the password encryption fo the new password
#### Search- and Access Logic
A new logic has to be introduced to search the user identity per different input values. That means searching the user data must be possible by
* searching per email (only with maincontact as contactchannel)
* searching per userID
* searching per alias
#### Identity-Mapping
A new mapping logic will be necessary to allow using unmigrated APIs like GDT-servers api. So it must be possible to give this identity-mapping logic the following input to get the respective output:
* email -> userID
* email -> alias
* userID -> email
* userID -> alias
* alias -> email
* alias -> userID
#### GDT-Access
To use the GDT-servers api the used identifier for GDT has to be switch from email to userID.

View File

@ -0,0 +1,650 @@
<mxfile host="65bd71144e">
<diagram id="jqy9GLoHfEna4h-l2pXZ" name="Seite-1">
<mxGraphModel dx="1302" dy="800" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="2336" pageHeight="1654" math="0" shadow="0">
<root>
<mxCell id="0"/>
<mxCell id="1" parent="0"/>
<mxCell id="57" value="&lt;div&gt;&lt;span&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;new Community-B&lt;/span&gt;&lt;/div&gt;" style="rounded=1;whiteSpace=wrap;html=1;verticalAlign=top;fontStyle=1;fontSize=14;align=left;fillColor=#dae8fc;strokeColor=#6c8ebf;gradientColor=#7ea6e0;" parent="1" vertex="1">
<mxGeometry x="1365" y="1340" width="920" height="294" as="geometry"/>
</mxCell>
<mxCell id="153" value="&amp;nbsp; Apollo-Server" style="rounded=0;whiteSpace=wrap;html=1;fontSize=14;fillColor=#dae8fc;strokeColor=#6c8ebf;align=left;gradientColor=#7ea6e0;gradientDirection=north;fontStyle=1;verticalAlign=top;" parent="1" vertex="1">
<mxGeometry x="1375" y="1370" width="900" height="240" as="geometry"/>
</mxCell>
<mxCell id="44" value="&lt;div&gt;&lt;span&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; new infrastructure Community-B&lt;/span&gt;&lt;/div&gt;" style="rounded=1;whiteSpace=wrap;html=1;verticalAlign=top;fontStyle=1;fontSize=14;align=left;fillColor=#dae8fc;strokeColor=#6c8ebf;gradientColor=#7ea6e0;" parent="1" vertex="1">
<mxGeometry x="1365" y="811" width="920" height="450" as="geometry"/>
</mxCell>
<mxCell id="148" value="&amp;nbsp; Apollo-Server" style="rounded=0;whiteSpace=wrap;html=1;fontSize=14;fillColor=#dae8fc;strokeColor=#6c8ebf;align=left;gradientColor=#7ea6e0;gradientDirection=north;fontStyle=1;verticalAlign=top;" parent="1" vertex="1">
<mxGeometry x="1390" y="856" width="853.14" height="270" as="geometry"/>
</mxCell>
<mxCell id="42" value="&lt;div style=&quot;text-align: center&quot;&gt;&lt;span&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;existing infrastructure Community-A&lt;/span&gt;&lt;/div&gt;" style="rounded=1;whiteSpace=wrap;html=1;verticalAlign=top;fontStyle=1;fontSize=14;align=left;fillColor=#d5e8d4;strokeColor=#82b366;gradientColor=#97d077;" parent="1" vertex="1">
<mxGeometry x="75" y="841" width="490" height="480" as="geometry"/>
</mxCell>
<mxCell id="147" value="&amp;nbsp; Apollo-Server" style="rounded=0;whiteSpace=wrap;html=1;fontSize=14;fillColor=#006600;fontColor=#ffffff;strokeColor=#2D7600;align=left;gradientColor=#ffffff;fontStyle=1;verticalAlign=top;" parent="1" vertex="1">
<mxGeometry x="85" y="881" width="470" height="310" as="geometry"/>
</mxCell>
<mxCell id="144" value="&lt;div&gt;&lt;span&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;new infrastructure Community-B&lt;/span&gt;&lt;/div&gt;" style="rounded=1;whiteSpace=wrap;html=1;verticalAlign=top;fontStyle=1;fontSize=14;align=left;fillColor=#dae8fc;strokeColor=#6c8ebf;gradientColor=#7ea6e0;" parent="1" vertex="1">
<mxGeometry x="1365" y="400" width="280" height="120" as="geometry"/>
</mxCell>
<mxCell id="143" value="&lt;div style=&quot;text-align: center&quot;&gt;&lt;span&gt;&amp;nbsp; &amp;nbsp; existing Infrastructure Community-A&lt;/span&gt;&lt;/div&gt;" style="rounded=1;whiteSpace=wrap;html=1;verticalAlign=top;fontStyle=1;fontSize=14;align=left;fillColor=#d5e8d4;strokeColor=#82b366;gradientColor=#97d077;" parent="1" vertex="1">
<mxGeometry x="285" y="320" width="280" height="120" as="geometry"/>
</mxCell>
<mxCell id="39" value="&lt;div&gt;&lt;span&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;new infrastrucutre Community-B&lt;/span&gt;&lt;/div&gt;" style="rounded=1;whiteSpace=wrap;html=1;verticalAlign=top;fontStyle=1;fontSize=14;align=left;fillColor=#dae8fc;strokeColor=#6c8ebf;gradientColor=#7ea6e0;" parent="1" vertex="1">
<mxGeometry x="1363.14" y="530" width="440" height="240" as="geometry"/>
</mxCell>
<mxCell id="35" value="&lt;div style=&quot;text-align: center&quot;&gt;&lt;span&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;existing Infrastructure Community-A&lt;/span&gt;&lt;/div&gt;" style="rounded=1;whiteSpace=wrap;html=1;verticalAlign=top;fontStyle=1;fontSize=14;align=left;fillColor=#d5e8d4;strokeColor=#82b366;gradientColor=#97d077;" parent="1" vertex="1">
<mxGeometry x="123.14" y="530" width="440" height="240" as="geometry"/>
</mxCell>
<mxCell id="2" value="&lt;div style=&quot;text-align: center&quot;&gt;&lt;span&gt;&amp;nbsp; &amp;nbsp; existing Infrastructure Community-A&lt;/span&gt;&lt;/div&gt;" style="rounded=1;whiteSpace=wrap;html=1;verticalAlign=top;fontStyle=1;fontSize=14;align=left;fillColor=#d5e8d4;strokeColor=#82b366;gradientColor=#97d077;" parent="1" vertex="1">
<mxGeometry x="285" y="40" width="360" height="140" as="geometry"/>
</mxCell>
<mxCell id="138" style="edgeStyle=none;html=1;fontSize=10;fontColor=#FF0000;startArrow=none;startFill=0;endArrow=classic;endFill=1;" parent="1" source="3" target="137" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="3" value="DHT-Node&lt;br&gt;- dht_gradido-topic&lt;br&gt;&lt;div style=&quot;text-align: left&quot;&gt;&lt;span&gt;&lt;font color=&quot;#ff0000&quot;&gt;&lt;b&gt;- keypair_A&lt;/b&gt;&lt;/font&gt;&lt;/span&gt;&lt;/div&gt;" style="rounded=0;whiteSpace=wrap;html=1;fontSize=14;fillColor=#008a00;fontColor=#ffffff;strokeColor=#005700;verticalAlign=top;" parent="1" vertex="1">
<mxGeometry x="485" y="80" width="160" height="80" as="geometry"/>
</mxCell>
<mxCell id="4" value="&lt;div&gt;&lt;span&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;new infrastructure Community-B&lt;/span&gt;&lt;/div&gt;" style="rounded=1;whiteSpace=wrap;html=1;verticalAlign=top;fontStyle=1;fontSize=14;align=left;fillColor=#dae8fc;strokeColor=#6c8ebf;gradientColor=#7ea6e0;" parent="1" vertex="1">
<mxGeometry x="1285" y="180" width="320" height="160" as="geometry"/>
</mxCell>
<mxCell id="141" style="edgeStyle=none;html=1;fontSize=12;fontColor=#000000;startArrow=none;startFill=0;endArrow=classic;endFill=1;" parent="1" source="5" target="140" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="5" value="dht-node&lt;br&gt;&lt;div style=&quot;text-align: left&quot;&gt;&lt;span&gt;- dht_gradido_topic&lt;/span&gt;&lt;/div&gt;&lt;div style=&quot;text-align: left&quot;&gt;&lt;span&gt;&lt;font color=&quot;#ff0000&quot;&gt;- keypair_B&lt;/font&gt;&lt;/span&gt;&lt;/div&gt;" style="rounded=0;whiteSpace=wrap;html=1;fontSize=14;fillColor=#0050ef;fontColor=#ffffff;strokeColor=#001DBC;verticalAlign=top;" parent="1" vertex="1">
<mxGeometry x="1295" y="240" width="140" height="80" as="geometry"/>
</mxCell>
<mxCell id="15" value="" style="endArrow=classic;html=1;fontSize=14;" parent="1" edge="1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="565" y="220" as="sourcePoint"/>
<mxPoint x="1005" y="220" as="targetPoint"/>
</mxGeometry>
</mxCell>
<mxCell id="17" value="&lt;b&gt;&amp;nbsp; join_AsServer&lt;/b&gt;(dht_gradido_topic, &lt;font color=&quot;#cc0000&quot;&gt;keypair_A.pubKey&lt;/font&gt;)&amp;nbsp;&amp;nbsp;" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontSize=14;" parent="15" vertex="1" connectable="0">
<mxGeometry x="0.2216" relative="1" as="geometry">
<mxPoint x="-36" y="-1" as="offset"/>
</mxGeometry>
</mxCell>
<mxCell id="19" value="" style="endArrow=classic;html=1;fontSize=14;" parent="1" edge="1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="1365" y="360" as="sourcePoint"/>
<mxPoint x="565" y="360" as="targetPoint"/>
</mxGeometry>
</mxCell>
<mxCell id="20" value="&lt;b&gt;&amp;nbsp; join_AsClient&lt;/b&gt;(dht_gradido_topic, &lt;font color=&quot;#cc0000&quot;&gt;keypair_B.pubKey&lt;/font&gt;)&amp;nbsp;&amp;nbsp;" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontSize=14;" parent="19" vertex="1" connectable="0">
<mxGeometry x="0.4162" relative="1" as="geometry">
<mxPoint x="126" as="offset"/>
</mxGeometry>
</mxCell>
<mxCell id="23" value="" style="endArrow=none;html=1;fontSize=14;entryX=0.5;entryY=1;entryDx=0;entryDy=0;" parent="1" target="3" edge="1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="565" y="1620" as="sourcePoint"/>
<mxPoint x="995" y="330" as="targetPoint"/>
</mxGeometry>
</mxCell>
<mxCell id="24" value="" style="endArrow=none;html=1;fontSize=14;entryX=0.5;entryY=1;entryDx=0;entryDy=0;" parent="1" target="5" edge="1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="1365" y="1620" as="sourcePoint"/>
<mxPoint x="1095" y="40" as="targetPoint"/>
</mxGeometry>
</mxCell>
<mxCell id="25" value="dht-node&lt;br&gt;- dht_gradido-topic&lt;br&gt;&lt;div style=&quot;text-align: left&quot;&gt;&lt;span&gt;- keypair_A&lt;/span&gt;&lt;/div&gt;&lt;div style=&quot;text-align: left&quot;&gt;&lt;span&gt;&lt;font color=&quot;#ff8000&quot;&gt;* pubKey_B&lt;/font&gt;&lt;/span&gt;&lt;/div&gt;" style="rounded=0;whiteSpace=wrap;html=1;fontSize=14;fillColor=#008a00;fontColor=#ffffff;strokeColor=#005700;verticalAlign=top;" parent="1" vertex="1">
<mxGeometry x="405" y="360" width="160" height="70" as="geometry"/>
</mxCell>
<mxCell id="26" value="dht-node&lt;br&gt;&lt;div style=&quot;text-align: left&quot;&gt;&lt;span&gt;- dht_gradido_topic&lt;/span&gt;&lt;/div&gt;&lt;div style=&quot;text-align: left&quot;&gt;&lt;span&gt;- keypair_B&lt;/span&gt;&lt;/div&gt;&lt;div style=&quot;text-align: left&quot;&gt;&lt;span&gt;&lt;font color=&quot;#ff8000&quot;&gt;* pubKey_A&lt;/font&gt;&lt;/span&gt;&lt;/div&gt;" style="rounded=0;whiteSpace=wrap;html=1;fontSize=14;fillColor=#0050ef;fontColor=#ffffff;strokeColor=#001DBC;verticalAlign=top;" parent="1" vertex="1">
<mxGeometry x="1365" y="430" width="140" height="80" as="geometry"/>
</mxCell>
<mxCell id="27" value="" style="endArrow=classic;html=1;fontSize=14;entryX=0;entryY=0;entryDx=0;entryDy=0;exitX=1;exitY=1;exitDx=0;exitDy=0;" parent="1" source="25" target="26" edge="1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="565" y="440" as="sourcePoint"/>
<mxPoint x="1365" y="460" as="targetPoint"/>
</mxGeometry>
</mxCell>
<mxCell id="28" value="&lt;b&gt;&amp;nbsp; connect&lt;/b&gt;( socket_B( &lt;font color=&quot;#cc0000&quot;&gt;keypair_A.pubKey&lt;/font&gt;) )&amp;nbsp;&amp;nbsp;" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontSize=14;" parent="27" vertex="1" connectable="0">
<mxGeometry x="0.2216" relative="1" as="geometry">
<mxPoint x="-129" as="offset"/>
</mxGeometry>
</mxCell>
<mxCell id="31" value="" style="shape=flexArrow;endArrow=classic;startArrow=classic;html=1;fontSize=14;" parent="1" edge="1">
<mxGeometry width="100" height="100" relative="1" as="geometry">
<mxPoint x="563.14" y="560" as="sourcePoint"/>
<mxPoint x="1363.14" y="560" as="targetPoint"/>
</mxGeometry>
</mxCell>
<mxCell id="32" value="&amp;nbsp; SocketStream( exchange (&lt;font color=&quot;#cc0000&quot;&gt;url_A, apiVer_A&lt;/font&gt;), exchange(&lt;font color=&quot;#cc0000&quot;&gt;url_B, apiVer_B&lt;/font&gt;) )&amp;nbsp;&amp;nbsp;" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontSize=14;" parent="31" vertex="1" connectable="0">
<mxGeometry x="-0.215" y="-1" relative="1" as="geometry">
<mxPoint x="46" y="-3" as="offset"/>
</mxGeometry>
</mxCell>
<mxCell id="33" value="dht-node&lt;br&gt;- dht_gradido-topic&lt;br&gt;&lt;div style=&quot;text-align: left&quot;&gt;&lt;span&gt;- keypair_A&lt;/span&gt;&lt;/div&gt;" style="rounded=0;whiteSpace=wrap;html=1;fontSize=14;fillColor=#008a00;fontColor=#ffffff;strokeColor=#005700;verticalAlign=top;" parent="1" vertex="1">
<mxGeometry x="403.14" y="560" width="160" height="57" as="geometry"/>
</mxCell>
<mxCell id="34" value="dht-node&lt;br&gt;&lt;div style=&quot;text-align: left&quot;&gt;&lt;span&gt;- dht_gradido_topic&lt;/span&gt;&lt;/div&gt;&lt;div style=&quot;text-align: left&quot;&gt;&lt;span&gt;- keypair_B&lt;/span&gt;&lt;/div&gt;" style="rounded=0;whiteSpace=wrap;html=1;fontSize=14;fillColor=#0050ef;fontColor=#ffffff;strokeColor=#001DBC;verticalAlign=top;" parent="1" vertex="1">
<mxGeometry x="1363.14" y="560" width="140" height="57" as="geometry"/>
</mxCell>
<mxCell id="46" value="" style="endArrow=classic;html=1;fontSize=14;entryX=0;entryY=0.25;entryDx=0;entryDy=0;exitX=1;exitY=0.25;exitDx=0;exitDy=0;" parent="1" source="48" target="51" edge="1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="545" y="931" as="sourcePoint"/>
<mxPoint x="975" y="941" as="targetPoint"/>
</mxGeometry>
</mxCell>
<mxCell id="47" value="&lt;b&gt;&amp;nbsp; request: &lt;/b&gt;http://&amp;lt;url_B&amp;gt;/&amp;lt;apiVer_B&amp;gt;/&lt;b&gt;openConnection&lt;/b&gt;( &lt;b&gt;&lt;font color=&quot;#ff0000&quot;&gt;pubkey_A&lt;/font&gt;&lt;/b&gt;, &lt;font color=&quot;#ff0000&quot;&gt;&lt;b&gt;encrypted and signed url_A&lt;/b&gt;&lt;/font&gt;)&amp;nbsp;&amp;nbsp;" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontSize=14;" parent="46" vertex="1" connectable="0">
<mxGeometry x="-0.215" y="-1" relative="1" as="geometry">
<mxPoint x="137" as="offset"/>
</mxGeometry>
</mxCell>
<mxCell id="48" value="encrypt &lt;font color=&quot;#000000&quot;&gt;url_A&lt;/font&gt;&amp;nbsp;with &lt;font color=&quot;#ff8000&quot;&gt;pubkey_B&lt;/font&gt; +&lt;br&gt;sign it with privatKey_A&amp;nbsp;" style="rounded=0;whiteSpace=wrap;html=1;fontSize=14;fillColor=#60a917;fontColor=#ffffff;strokeColor=#2D7600;align=left;" parent="1" vertex="1">
<mxGeometry x="350" y="896" width="195" height="38.5" as="geometry"/>
</mxCell>
<mxCell id="49" value="" style="endArrow=classic;html=1;fontSize=14;exitX=0;exitY=0.5;exitDx=0;exitDy=0;entryX=1;entryY=0.5;entryDx=0;entryDy=0;" parent="1" source="100" target="87" edge="1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="995" y="651" as="sourcePoint"/>
<mxPoint x="1045" y="601" as="targetPoint"/>
</mxGeometry>
</mxCell>
<mxCell id="50" value="&lt;b&gt;&amp;nbsp; request: &lt;/b&gt;http://&amp;lt;url_A&amp;gt;/&amp;lt;apiVer_A&amp;gt;//&lt;b&gt;openConnectionRedirect&lt;/b&gt;(&lt;font color=&quot;#ff00ff&quot;&gt;&lt;b&gt;onetimeCode&lt;/b&gt;&lt;/font&gt;, url_B, encrypted and signed&amp;nbsp;&lt;font color=&quot;#cc0000&quot;&gt;&lt;b&gt;redirect_URL&lt;/b&gt;&lt;/font&gt;)&amp;nbsp;&amp;nbsp;" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontSize=14;" parent="49" vertex="1" connectable="0">
<mxGeometry x="0.255" y="2" relative="1" as="geometry">
<mxPoint x="97" y="-1" as="offset"/>
</mxGeometry>
</mxCell>
<mxCell id="132" style="edgeStyle=none;html=1;fontColor=#FF0000;startArrow=none;startFill=0;endArrow=none;endFill=0;dashed=1;exitX=0;exitY=0.75;exitDx=0;exitDy=0;" parent="1" source="51" edge="1">
<mxGeometry relative="1" as="geometry">
<mxPoint x="1365" y="926" as="targetPoint"/>
</mxGeometry>
</mxCell>
<mxCell id="51" value="decrypt &lt;font color=&quot;#ff0000&quot;&gt;&lt;b&gt;encoded_url_A&lt;/b&gt;&lt;/font&gt;&amp;nbsp; &lt;br&gt;with &lt;font color=&quot;#000000&quot;&gt;privatkey_B&lt;/font&gt;" style="rounded=0;whiteSpace=wrap;html=1;fontSize=14;fillColor=#dae8fc;strokeColor=#6c8ebf;align=left;" parent="1" vertex="1">
<mxGeometry x="1406.8600000000001" y="896" width="180" height="40" as="geometry"/>
</mxCell>
<mxCell id="84" value="" style="edgeStyle=none;html=1;fontColor=#00FF00;startArrow=none;entryX=0.5;entryY=0;entryDx=0;entryDy=0;exitX=0.5;exitY=1;exitDx=0;exitDy=0;" parent="1" source="98" target="83" edge="1">
<mxGeometry relative="1" as="geometry">
<Array as="points"/>
</mxGeometry>
</mxCell>
<mxCell id="53" value="&lt;span style=&quot;color: rgb(0 , 153 , 0)&quot;&gt;url_A of&amp;nbsp;&lt;/span&gt;&lt;font color=&quot;#009900&quot;&gt;pubkey_A&lt;/font&gt;&lt;br&gt;==&amp;nbsp;&lt;font color=&quot;#ff0000&quot;&gt;url_A&lt;/font&gt;?" style="rhombus;whiteSpace=wrap;html=1;fontSize=14;fillColor=#dae8fc;strokeColor=#6c8ebf;" parent="1" vertex="1">
<mxGeometry x="1998.14" y="880.25" width="150" height="70" as="geometry"/>
</mxCell>
<mxCell id="55" value="&lt;div style=&quot;text-align: center&quot;&gt;&lt;span&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;existing infrastructure Community-A&lt;/span&gt;&lt;/div&gt;" style="rounded=1;whiteSpace=wrap;html=1;verticalAlign=top;fontStyle=1;fontSize=14;align=left;fillColor=#d5e8d4;strokeColor=#82b366;gradientColor=#97d077;" parent="1" vertex="1">
<mxGeometry x="45" y="1370" width="520" height="264" as="geometry"/>
</mxCell>
<mxCell id="74" value="" style="edgeStyle=none;html=1;fontSize=14;fontColor=#FF8000;startArrow=none;startFill=0;endArrow=classic;endFill=1;" parent="1" source="64" target="65" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="155" style="edgeStyle=none;html=1;entryX=0;entryY=0.5;entryDx=0;entryDy=0;fontSize=12;fontColor=#FFFFFF;startArrow=none;startFill=0;endArrow=classic;endFill=1;" parent="1" source="65" target="154" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="64" value="decrypt encoded parameters&amp;nbsp;with privatkey_B" style="rounded=0;whiteSpace=wrap;html=1;fontSize=12;fillColor=#dae8fc;strokeColor=#6c8ebf;align=left;" parent="1" vertex="1">
<mxGeometry x="1385" y="1411" width="280" height="28" as="geometry"/>
</mxCell>
<mxCell id="80" value="" style="endArrow=none;dashed=1;html=1;strokeWidth=4;fontSize=14;fontColor=#FF8000;" parent="1" edge="1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="5" y="803" as="sourcePoint"/>
<mxPoint x="2325" y="800" as="targetPoint"/>
</mxGeometry>
</mxCell>
<mxCell id="81" value="&lt;font style=&quot;font-size: 24px;&quot;&gt;Federation&lt;/font&gt;" style="text;html=1;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontSize=24;fontColor=#000000;fontStyle=1" parent="1" vertex="1">
<mxGeometry x="5" y="40" width="120" height="30" as="geometry"/>
</mxCell>
<mxCell id="82" value="&lt;font style=&quot;font-size: 24px;&quot;&gt;Authentication&lt;/font&gt;" style="text;html=1;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontSize=24;fontColor=#000000;fontStyle=1" parent="1" vertex="1">
<mxGeometry x="5" y="812" width="140" height="30" as="geometry"/>
</mxCell>
<mxCell id="83" value="&lt;font color=&quot;#009900&quot;&gt;url_A&lt;/font&gt;&lt;font color=&quot;#00ff00&quot;&gt;&amp;nbsp;&lt;/font&gt;==&lt;br&gt;&amp;nbsp;&amp;nbsp;&lt;font color=&quot;#ff0000&quot;&gt;unsigned url_A&lt;/font&gt;?" style="rhombus;whiteSpace=wrap;html=1;fontSize=14;fillColor=#dae8fc;strokeColor=#6c8ebf;" parent="1" vertex="1">
<mxGeometry x="1753.14" y="946" width="180" height="50" as="geometry"/>
</mxCell>
<mxCell id="85" value="" style="endArrow=classic;html=1;fontSize=14;entryX=0;entryY=0.5;entryDx=0;entryDy=0;exitX=1;exitY=0.5;exitDx=0;exitDy=0;" parent="1" source="113" target="128" edge="1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="595" y="1101" as="sourcePoint"/>
<mxPoint x="1377.1999999999998" y="913" as="targetPoint"/>
</mxGeometry>
</mxCell>
<mxCell id="86" value="&lt;b&gt;&amp;nbsp; redirect: &lt;/b&gt;http://&amp;lt;&lt;b&gt;redirect_URL&lt;/b&gt;&amp;gt;( &lt;font color=&quot;#ff00ff&quot;&gt;&lt;b&gt;onetimeCode&lt;/b&gt;&lt;/font&gt;,&amp;nbsp;&lt;font color=&quot;#ff0000&quot;&gt;&lt;b&gt;encrypted&amp;nbsp;uuid_A&lt;/b&gt;&lt;/font&gt;)&amp;nbsp;&amp;nbsp;" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontSize=14;" parent="85" vertex="1" connectable="0">
<mxGeometry x="-0.215" y="-1" relative="1" as="geometry">
<mxPoint as="offset"/>
</mxGeometry>
</mxCell>
<mxCell id="123" style="edgeStyle=none;html=1;entryX=1;entryY=0.5;entryDx=0;entryDy=0;fontColor=#FF0000;startArrow=none;startFill=0;endArrow=classic;endFill=1;" parent="1" source="87" target="122" edge="1">
<mxGeometry relative="1" as="geometry">
<Array as="points"/>
</mxGeometry>
</mxCell>
<mxCell id="87" value="decrypt &lt;font color=&quot;#ff0000&quot;&gt;&lt;b&gt;redirect_URL&lt;/b&gt;&lt;/font&gt;&amp;nbsp;with &lt;font color=&quot;#000000&quot;&gt;privatekey_A&lt;/font&gt;&amp;nbsp;" style="rounded=0;whiteSpace=wrap;html=1;fontSize=14;fillColor=#60a917;fontColor=#ffffff;strokeColor=#2D7600;align=left;" parent="1" vertex="1">
<mxGeometry x="255" y="948.5" width="290" height="22.5" as="geometry"/>
</mxCell>
<mxCell id="104" style="edgeStyle=none;html=1;entryX=0.5;entryY=0;entryDx=0;entryDy=0;fontColor=#FFFFFF;startArrow=none;startFill=0;endArrow=classic;endFill=1;exitX=0.349;exitY=1.025;exitDx=0;exitDy=0;exitPerimeter=0;" parent="1" source="88" target="130" edge="1">
<mxGeometry relative="1" as="geometry">
<Array as="points"/>
<mxPoint x="2244.4199999999996" y="1071" as="targetPoint"/>
</mxGeometry>
</mxCell>
<mxCell id="88" value="decrypt &lt;font color=&quot;#ff0000&quot;&gt;&lt;b&gt;encoded_uuid_A&lt;/b&gt;&lt;/font&gt;&amp;nbsp; &lt;br&gt;with &lt;font color=&quot;#00ff00&quot; style=&quot;font-weight: bold&quot;&gt;pubkey_A&lt;/font&gt; of &lt;font color=&quot;#ff00ff&quot; style=&quot;font-weight: bold&quot;&gt;oneTImeCode&lt;/font&gt;" style="rounded=0;whiteSpace=wrap;html=1;fontSize=14;fillColor=#dae8fc;strokeColor=#6c8ebf;align=left;" parent="1" vertex="1">
<mxGeometry x="1803.14" y="1021" width="226.28" height="40" as="geometry"/>
</mxCell>
<mxCell id="90" value="found &amp;amp; valid&lt;br&gt;&lt;font color=&quot;#ff00ff&quot;&gt;oneTimeCode&lt;/font&gt;?" style="rhombus;whiteSpace=wrap;html=1;fontSize=14;fillColor=#dae8fc;strokeColor=#6c8ebf;" parent="1" vertex="1">
<mxGeometry x="1586.8600000000001" y="1016" width="180" height="50" as="geometry"/>
</mxCell>
<mxCell id="91" value="" style="endArrow=classic;html=1;fontSize=14;fontColor=#FF8000;entryX=0;entryY=0.5;entryDx=0;entryDy=0;exitX=1;exitY=0.5;exitDx=0;exitDy=0;" parent="1" source="90" target="88" edge="1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="1366.8600000000001" y="1076" as="sourcePoint"/>
<mxPoint x="1416.8600000000001" y="1026" as="targetPoint"/>
</mxGeometry>
</mxCell>
<mxCell id="95" value="" style="endArrow=classic;html=1;fontSize=14;entryX=1;entryY=0.75;entryDx=0;entryDy=0;" parent="1" target="48" edge="1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="1365" y="925" as="sourcePoint"/>
<mxPoint x="565" y="891" as="targetPoint"/>
</mxGeometry>
</mxCell>
<mxCell id="96" value="&lt;b&gt;&amp;nbsp; response:&lt;/b&gt;&amp;nbsp;OK" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontSize=14;" parent="95" vertex="1" connectable="0">
<mxGeometry x="0.255" y="2" relative="1" as="geometry">
<mxPoint x="52" y="-3" as="offset"/>
</mxGeometry>
</mxCell>
<mxCell id="98" value="verify&amp;nbsp;&lt;font color=&quot;#ff0000&quot;&gt;signed_url_A&lt;/font&gt;&amp;nbsp; &lt;br&gt;with &lt;font color=&quot;#009900&quot;&gt;pubkey_A&lt;/font&gt;" style="rounded=0;whiteSpace=wrap;html=1;fontSize=14;fillColor=#dae8fc;strokeColor=#6c8ebf;align=left;" parent="1" vertex="1">
<mxGeometry x="1773.14" y="896" width="140" height="40" as="geometry"/>
</mxCell>
<mxCell id="99" value="" style="edgeStyle=none;html=1;fontColor=#00FF00;endArrow=classic;endFill=1;exitX=0;exitY=0.5;exitDx=0;exitDy=0;" parent="1" source="53" target="98" edge="1">
<mxGeometry relative="1" as="geometry">
<mxPoint x="1573.14" y="911" as="sourcePoint"/>
<mxPoint x="1808.14" y="911" as="targetPoint"/>
<Array as="points"/>
</mxGeometry>
</mxCell>
<mxCell id="100" value="encrypt redirect_URL (inc. apiVersion)&amp;nbsp;&amp;nbsp;&lt;br&gt;with &lt;font color=&quot;#009900&quot;&gt;publickey_A&lt;/font&gt; + sign with &lt;font color=&quot;#009900&quot;&gt;privatKey_B&lt;/font&gt;" style="rounded=0;whiteSpace=wrap;html=1;fontSize=14;fillColor=#dae8fc;strokeColor=#6c8ebf;align=left;" parent="1" vertex="1">
<mxGeometry x="1406.8600000000001" y="951" width="276.28" height="40" as="geometry"/>
</mxCell>
<mxCell id="101" value="" style="endArrow=classic;html=1;fontSize=14;fontColor=#FF8000;exitX=0;exitY=0.5;exitDx=0;exitDy=0;entryX=1;entryY=0.5;entryDx=0;entryDy=0;startArrow=none;startFill=0;endFill=1;" parent="1" source="83" target="100" edge="1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="1713.14" y="981" as="sourcePoint"/>
<mxPoint x="1364.5800000000002" y="980" as="targetPoint"/>
<Array as="points"/>
</mxGeometry>
</mxCell>
<mxCell id="107" style="edgeStyle=none;html=1;entryX=1;entryY=0.5;entryDx=0;entryDy=0;fontColor=#000000;startArrow=none;startFill=0;endArrow=classic;endFill=1;exitX=0;exitY=0.5;exitDx=0;exitDy=0;" parent="1" source="130" target="106" edge="1">
<mxGeometry relative="1" as="geometry">
<Array as="points"/>
<mxPoint x="1898.14" y="1126" as="sourcePoint"/>
</mxGeometry>
</mxCell>
<mxCell id="134" value="2." style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontColor=#FF0000;" parent="107" vertex="1" connectable="0">
<mxGeometry x="-0.9248" relative="1" as="geometry">
<mxPoint x="-14" as="offset"/>
</mxGeometry>
</mxCell>
<mxCell id="106" value="encrypt &lt;font color=&quot;#000000&quot;&gt;&lt;b&gt;uuid_B&lt;/b&gt;&lt;/font&gt;&amp;nbsp;with &lt;b&gt;privatkey_B&lt;/b&gt;" style="rounded=0;whiteSpace=wrap;html=1;fontSize=14;fillColor=#dae8fc;strokeColor=#6c8ebf;align=left;" parent="1" vertex="1">
<mxGeometry x="1406.8600000000001" y="1081" width="226.28" height="30" as="geometry"/>
</mxCell>
<mxCell id="108" value="" style="endArrow=classic;html=1;fontSize=14;exitX=0;exitY=0.5;exitDx=0;exitDy=0;entryX=1;entryY=0.5;entryDx=0;entryDy=0;" parent="1" source="106" target="110" edge="1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="1373.56" y="1012.2" as="sourcePoint"/>
<mxPoint x="575" y="1013" as="targetPoint"/>
</mxGeometry>
</mxCell>
<mxCell id="109" value="&lt;b&gt;&amp;nbsp; response:&lt;font color=&quot;#ff0000&quot;&gt; &lt;/font&gt;&lt;/b&gt;&lt;b&gt;&lt;font color=&quot;#ff0000&quot;&gt;encoded_uuid_B&lt;/font&gt;&lt;/b&gt;&amp;nbsp;&amp;nbsp;" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontSize=14;" parent="108" vertex="1" connectable="0">
<mxGeometry x="0.255" y="2" relative="1" as="geometry">
<mxPoint x="72" y="-3" as="offset"/>
</mxGeometry>
</mxCell>
<mxCell id="110" value="decrypt &lt;font color=&quot;#ff0000&quot;&gt;&lt;b&gt;encoded_uuid_B&lt;/b&gt;&lt;/font&gt;&amp;nbsp;&amp;nbsp;&lt;br&gt;with &lt;font color=&quot;#ff0000&quot;&gt;&lt;b&gt;pubkey_B&lt;/b&gt;&lt;/font&gt; &amp;nbsp;" style="rounded=0;whiteSpace=wrap;html=1;fontSize=14;fillColor=#60a917;fontColor=#ffffff;strokeColor=#2D7600;align=left;" parent="1" vertex="1">
<mxGeometry x="365" y="1066" width="180" height="50" as="geometry"/>
</mxCell>
<mxCell id="112" value="" style="endArrow=classic;html=1;fontSize=14;exitX=0.5;exitY=1;exitDx=0;exitDy=0;entryX=0.5;entryY=0;entryDx=0;entryDy=0;" parent="1" source="110" target="135" edge="1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="575" y="1150.2" as="sourcePoint"/>
<mxPoint x="530" y="1151" as="targetPoint"/>
<Array as="points"/>
</mxGeometry>
</mxCell>
<mxCell id="113" value="&lt;span style=&quot;color: rgb(255 , 255 , 255) ; font-size: 14px ; text-align: left&quot;&gt;encrypt&amp;nbsp;&lt;/span&gt;&lt;font color=&quot;#000000&quot; style=&quot;font-size: 14px ; text-align: left&quot;&gt;uuid_A&lt;/font&gt;&lt;span style=&quot;color: rgb(255 , 255 , 255) ; font-size: 14px ; text-align: left&quot;&gt;&amp;nbsp;with &lt;/span&gt;&lt;span style=&quot;font-size: 14px ; text-align: left&quot;&gt;&lt;font color=&quot;#000000&quot;&gt;privatKey_A&lt;/font&gt;&lt;/span&gt;&lt;span style=&quot;color: rgb(255 , 255 , 255) ; font-size: 14px ; text-align: left&quot;&gt;&amp;nbsp;&lt;/span&gt;" style="rounded=0;whiteSpace=wrap;html=1;fontColor=#ffffff;fillColor=#60a917;strokeColor=#2D7600;" parent="1" vertex="1">
<mxGeometry x="325" y="1023.5" width="220" height="25" as="geometry"/>
</mxCell>
<mxCell id="115" value="" style="endArrow=none;dashed=1;html=1;strokeWidth=4;fontSize=14;fontColor=#FF8000;" parent="1" edge="1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="5" y="1333" as="sourcePoint"/>
<mxPoint x="2325" y="1330" as="targetPoint"/>
</mxGeometry>
</mxCell>
<mxCell id="120" value="search &lt;font color=&quot;#ff0000&quot;&gt;&lt;b&gt;pubKey_A&lt;/b&gt;&lt;/font&gt;&amp;nbsp;in &lt;br&gt;local Community-List" style="rounded=0;whiteSpace=wrap;html=1;fontSize=14;fillColor=#dae8fc;strokeColor=#6c8ebf;align=left;" parent="1" vertex="1">
<mxGeometry x="1613.14" y="896" width="140" height="40" as="geometry"/>
</mxCell>
<mxCell id="121" value="" style="endArrow=classic;html=1;fontSize=14;fontColor=#FF8000;entryX=0;entryY=0.5;entryDx=0;entryDy=0;exitX=1;exitY=0.5;exitDx=0;exitDy=0;endFill=1;" parent="1" source="51" target="120" edge="1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="1586.8600000000004" y="921" as="sourcePoint"/>
<mxPoint x="1853.14" y="921" as="targetPoint"/>
</mxGeometry>
</mxCell>
<mxCell id="122" value="search with&amp;nbsp;&lt;font color=&quot;#000000&quot;&gt;url_B&lt;/font&gt;&amp;nbsp;for the&amp;nbsp;&lt;font color=&quot;#ff0000&quot;&gt;&lt;b&gt;pubkey_B&lt;/b&gt;&lt;/font&gt;&amp;nbsp;" style="rounded=0;whiteSpace=wrap;html=1;fontSize=14;fillColor=#60a917;fontColor=#ffffff;strokeColor=#2D7600;align=left;" parent="1" vertex="1">
<mxGeometry x="95" y="940" width="140" height="38.5" as="geometry"/>
</mxCell>
<mxCell id="127" style="edgeStyle=none;html=1;entryX=0;entryY=0.5;entryDx=0;entryDy=0;fontColor=#000000;startArrow=none;startFill=0;endArrow=classic;endFill=1;exitX=0.128;exitY=0.98;exitDx=0;exitDy=0;exitPerimeter=0;" parent="1" source="126" target="113" edge="1">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="292" y="1036"/>
</Array>
</mxGeometry>
</mxCell>
<mxCell id="126" value="verify sign of&amp;nbsp;&lt;b style=&quot;color: rgb(255 , 0 , 0)&quot;&gt;redirect_URL&lt;/b&gt;&amp;nbsp;with &lt;font color=&quot;#ff0000&quot;&gt;&lt;b&gt;pubKey_B&lt;/b&gt;&lt;/font&gt;" style="rounded=0;whiteSpace=wrap;html=1;fontSize=14;fillColor=#60a917;fontColor=#ffffff;strokeColor=#2D7600;align=left;" parent="1" vertex="1">
<mxGeometry x="255" y="978.5" width="290" height="25" as="geometry"/>
</mxCell>
<mxCell id="129" style="edgeStyle=none;html=1;entryX=0.061;entryY=0.5;entryDx=0;entryDy=0;entryPerimeter=0;fontColor=#000000;startArrow=none;startFill=0;endArrow=classic;endFill=1;" parent="1" source="128" target="90" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="128" value="search&amp;nbsp;&lt;span style=&quot;color: rgb(255 , 0 , 255) ; font-weight: 700&quot;&gt;oneTImeCode&lt;/span&gt;" style="rounded=0;whiteSpace=wrap;html=1;fontSize=14;fillColor=#dae8fc;strokeColor=#6c8ebf;align=left;" parent="1" vertex="1">
<mxGeometry x="1406.8600000000001" y="1026" width="146.28" height="30" as="geometry"/>
</mxCell>
<mxCell id="130" value="overwrite&amp;nbsp;&lt;font color=&quot;#ff00ff&quot; style=&quot;font-weight: bold&quot;&gt;oneTImeCode &lt;/font&gt;with&amp;nbsp;decrypted&amp;nbsp;&lt;font color=&quot;#ff0000&quot;&gt;&lt;b&gt;uuid_A&lt;/b&gt;&lt;/font&gt;&amp;nbsp;" style="rounded=0;whiteSpace=wrap;html=1;fontSize=14;fillColor=#dae8fc;strokeColor=#6c8ebf;align=left;" parent="1" vertex="1">
<mxGeometry x="1803.14" y="1081" width="160" height="40" as="geometry"/>
</mxCell>
<mxCell id="135" value="insert&amp;nbsp;&lt;font color=&quot;#ff0000&quot;&gt;&lt;b&gt;uuid_B&amp;nbsp;&lt;/b&gt;&lt;/font&gt;in entry with &lt;font color=&quot;#ff0000&quot;&gt;&lt;b&gt;pubkey_B&lt;/b&gt;&lt;/font&gt; &amp;nbsp;" style="rounded=0;whiteSpace=wrap;html=1;fontSize=14;fillColor=#60a917;fontColor=#ffffff;strokeColor=#2D7600;align=left;" parent="1" vertex="1">
<mxGeometry x="365" y="1131" width="180" height="50" as="geometry"/>
</mxCell>
<mxCell id="137" value="&lt;div style=&quot;color: rgb(0, 0, 0); font-size: 10px; font-weight: 700; text-align: left;&quot;&gt;- uuid_A&lt;/div&gt;&lt;div style=&quot;color: rgb(0, 0, 0); font-size: 10px; font-weight: 700; text-align: left;&quot;&gt;- url_A&lt;br style=&quot;font-size: 10px;&quot;&gt;&lt;/div&gt;&lt;div style=&quot;color: rgb(0, 0, 0); font-size: 10px; font-weight: 700; text-align: left;&quot;&gt;- apiVer_A&lt;/div&gt;&lt;div style=&quot;color: rgb(0, 0, 0); font-size: 10px; font-weight: 700; text-align: left;&quot;&gt;&lt;font color=&quot;#cc0000&quot; style=&quot;font-size: 10px;&quot;&gt;- privatkey_A&lt;br style=&quot;font-size: 10px;&quot;&gt;&lt;/font&gt;&lt;/div&gt;&lt;div style=&quot;color: rgb(0, 0, 0); font-size: 10px; font-weight: 700; text-align: left;&quot;&gt;&lt;font color=&quot;#cc0000&quot; style=&quot;font-size: 10px;&quot;&gt;- publickey_A&lt;/font&gt;&lt;/div&gt;" style="shape=cylinder3;whiteSpace=wrap;html=1;boundedLbl=1;backgroundOutline=1;size=15;rounded=1;fontColor=#FF0000;gradientColor=#006600;gradientDirection=north;fontSize=10;" parent="1" vertex="1">
<mxGeometry x="305" y="75" width="100" height="90" as="geometry"/>
</mxCell>
<mxCell id="139" value="&lt;div style=&quot;font-size: 10px; font-weight: 700;&quot;&gt;&lt;font color=&quot;#000000&quot;&gt;- uuid_A&lt;/font&gt;&lt;/div&gt;&lt;div style=&quot;font-size: 10px; font-weight: 700;&quot;&gt;&lt;font color=&quot;#000000&quot;&gt;- url_A&lt;br style=&quot;font-size: 10px&quot;&gt;&lt;/font&gt;&lt;/div&gt;&lt;div style=&quot;font-size: 10px; font-weight: 700;&quot;&gt;&lt;font color=&quot;#000000&quot;&gt;- apiVer_A&lt;/font&gt;&lt;/div&gt;&lt;div style=&quot;font-size: 10px; font-weight: 700;&quot;&gt;&lt;font style=&quot;font-size: 10px&quot; color=&quot;#000000&quot;&gt;- privatkey_A&lt;br style=&quot;font-size: 10px&quot;&gt;&lt;/font&gt;&lt;/div&gt;&lt;div style=&quot;font-size: 10px; font-weight: 700;&quot;&gt;&lt;font style=&quot;font-size: 10px&quot; color=&quot;#000000&quot;&gt;- publickey_A&lt;/font&gt;&lt;/div&gt;" style="shape=cylinder3;whiteSpace=wrap;html=1;boundedLbl=1;backgroundOutline=1;size=15;rounded=1;fontColor=#FF0000;gradientColor=#006600;gradientDirection=north;fontSize=10;align=left;verticalAlign=top;" parent="1" vertex="1">
<mxGeometry x="133.14" y="610" width="250" height="130" as="geometry"/>
</mxCell>
<mxCell id="37" value="&lt;font color=&quot;#ff8000&quot; style=&quot;font-size: 12px;&quot;&gt;&amp;nbsp;* url_B / pubKey_B / apiVer_B&lt;br style=&quot;font-size: 12px;&quot;&gt;&lt;br style=&quot;font-size: 12px;&quot;&gt;&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;fontSize=12;fillColor=#60a917;fontColor=#ffffff;strokeColor=#2D7600;align=left;arcSize=30;gradientColor=#006600;gradientDirection=north;" parent="1" vertex="1">
<mxGeometry x="213.14" y="685" width="170" height="30" as="geometry"/>
</mxCell>
<mxCell id="140" value="&lt;div style=&quot;color: rgb(0 , 0 , 0) ; font-size: 10px ; font-weight: 700 ; text-align: left&quot;&gt;- uuid_B&lt;/div&gt;&lt;div style=&quot;color: rgb(0 , 0 , 0) ; font-size: 10px ; font-weight: 700 ; text-align: left&quot;&gt;- url_B&lt;br style=&quot;font-size: 10px&quot;&gt;&lt;/div&gt;&lt;div style=&quot;color: rgb(0 , 0 , 0) ; font-size: 10px ; font-weight: 700 ; text-align: left&quot;&gt;- apiVer_B&lt;/div&gt;&lt;div style=&quot;color: rgb(0 , 0 , 0) ; font-size: 10px ; font-weight: 700 ; text-align: left&quot;&gt;&lt;font color=&quot;#cc0000&quot; style=&quot;font-size: 10px&quot;&gt;- privatkey_B&lt;br style=&quot;font-size: 10px&quot;&gt;&lt;/font&gt;&lt;/div&gt;&lt;div style=&quot;color: rgb(0 , 0 , 0) ; font-size: 10px ; font-weight: 700 ; text-align: left&quot;&gt;&lt;font color=&quot;#cc0000&quot; style=&quot;font-size: 10px&quot;&gt;- publickey_B&lt;/font&gt;&lt;/div&gt;" style="shape=cylinder3;whiteSpace=wrap;html=1;boundedLbl=1;backgroundOutline=1;size=15;rounded=1;gradientColor=#7ea6e0;gradientDirection=north;fontSize=10;fillColor=#dae8fc;strokeColor=#6c8ebf;" parent="1" vertex="1">
<mxGeometry x="1485" y="235" width="100" height="90" as="geometry"/>
</mxCell>
<mxCell id="142" value="&lt;div style=&quot;font-size: 10px ; font-weight: 700&quot;&gt;&lt;font color=&quot;#000000&quot;&gt;- uuid_B&lt;/font&gt;&lt;/div&gt;&lt;div style=&quot;font-size: 10px ; font-weight: 700&quot;&gt;&lt;font color=&quot;#000000&quot;&gt;- url_B&lt;br style=&quot;font-size: 10px&quot;&gt;&lt;/font&gt;&lt;/div&gt;&lt;div style=&quot;font-size: 10px ; font-weight: 700&quot;&gt;&lt;font color=&quot;#000000&quot;&gt;- apiVer_B&lt;/font&gt;&lt;/div&gt;&lt;div style=&quot;font-size: 10px ; font-weight: 700&quot;&gt;&lt;font style=&quot;font-size: 10px&quot; color=&quot;#000000&quot;&gt;- privatkey_B&lt;br style=&quot;font-size: 10px&quot;&gt;&lt;/font&gt;&lt;/div&gt;&lt;div style=&quot;font-size: 10px ; font-weight: 700&quot;&gt;&lt;font style=&quot;font-size: 10px&quot; color=&quot;#000000&quot;&gt;- publickey_B&lt;/font&gt;&lt;/div&gt;" style="shape=cylinder3;whiteSpace=wrap;html=1;boundedLbl=1;backgroundOutline=1;size=15;rounded=1;gradientColor=#7ea6e0;gradientDirection=north;fontSize=10;align=left;verticalAlign=top;fillColor=#dae8fc;strokeColor=#6c8ebf;" parent="1" vertex="1">
<mxGeometry x="1543.14" y="610" width="250" height="130" as="geometry"/>
</mxCell>
<mxCell id="40" value="&lt;font color=&quot;#ff8000&quot; style=&quot;font-size: 12px&quot;&gt;&amp;nbsp;* url_A / pubKey_A / apiVer_A&lt;br style=&quot;font-size: 12px&quot;&gt;&lt;br style=&quot;font-size: 12px&quot;&gt;&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;fontSize=12;fillColor=#1ba1e2;fontColor=#ffffff;strokeColor=#006EAF;align=left;arcSize=20;" parent="1" vertex="1">
<mxGeometry x="1623.14" y="693" width="170" height="30" as="geometry"/>
</mxCell>
<mxCell id="145" value="&lt;div style=&quot;font-size: 10px; font-weight: 700;&quot;&gt;&lt;font color=&quot;#000000&quot;&gt;- uuid_A&lt;/font&gt;&lt;/div&gt;&lt;div style=&quot;font-size: 10px; font-weight: 700;&quot;&gt;&lt;font color=&quot;#000000&quot;&gt;- url_A&lt;br style=&quot;font-size: 10px&quot;&gt;&lt;/font&gt;&lt;/div&gt;&lt;div style=&quot;font-size: 10px; font-weight: 700;&quot;&gt;&lt;font color=&quot;#000000&quot;&gt;- apiVer_A&lt;/font&gt;&lt;/div&gt;&lt;div style=&quot;font-size: 10px; font-weight: 700;&quot;&gt;&lt;font style=&quot;font-size: 10px&quot; color=&quot;#000000&quot;&gt;- privatkey_A&lt;br style=&quot;font-size: 10px&quot;&gt;&lt;/font&gt;&lt;/div&gt;&lt;div style=&quot;font-size: 10px; font-weight: 700;&quot;&gt;&lt;font style=&quot;font-size: 10px&quot; color=&quot;#000000&quot;&gt;- publickey_A&lt;/font&gt;&lt;/div&gt;" style="shape=cylinder3;whiteSpace=wrap;html=1;boundedLbl=1;backgroundOutline=1;size=15;rounded=1;fontColor=#FF0000;gradientColor=#006600;gradientDirection=north;fontSize=10;align=left;verticalAlign=top;" parent="1" vertex="1">
<mxGeometry x="105" y="1201" width="400" height="110" as="geometry"/>
</mxCell>
<mxCell id="146" value="&lt;div style=&quot;font-size: 10px ; font-weight: 700&quot;&gt;&lt;font color=&quot;#000000&quot;&gt;- uuid_B&lt;/font&gt;&lt;/div&gt;&lt;div style=&quot;font-size: 10px ; font-weight: 700&quot;&gt;&lt;font color=&quot;#000000&quot;&gt;- url_B&lt;br style=&quot;font-size: 10px&quot;&gt;&lt;/font&gt;&lt;/div&gt;&lt;div style=&quot;font-size: 10px ; font-weight: 700&quot;&gt;&lt;font color=&quot;#000000&quot;&gt;- apiVer_B&lt;/font&gt;&lt;/div&gt;&lt;div style=&quot;font-size: 10px ; font-weight: 700&quot;&gt;&lt;font style=&quot;font-size: 10px&quot; color=&quot;#000000&quot;&gt;- privatkey_B&lt;br style=&quot;font-size: 10px&quot;&gt;&lt;/font&gt;&lt;/div&gt;&lt;div style=&quot;font-size: 10px ; font-weight: 700&quot;&gt;&lt;font style=&quot;font-size: 10px&quot; color=&quot;#000000&quot;&gt;- publickey_B&lt;/font&gt;&lt;/div&gt;" style="shape=cylinder3;whiteSpace=wrap;html=1;boundedLbl=1;backgroundOutline=1;size=15;rounded=1;gradientColor=#7ea6e0;gradientDirection=north;fontSize=10;align=left;verticalAlign=top;fillColor=#dae8fc;strokeColor=#6c8ebf;" parent="1" vertex="1">
<mxGeometry x="1803.14" y="1136" width="440" height="110" as="geometry"/>
</mxCell>
<mxCell id="43" value="1: * url_B / &lt;font color=&quot;#ff0000&quot;&gt;&lt;b&gt;pubkey_B&lt;/b&gt;&lt;/font&gt; / apiVer_B&lt;br&gt;2: * url_B /&amp;nbsp;&lt;font color=&quot;#ff0000&quot;&gt;&lt;b&gt;pubkey_B&lt;/b&gt;&lt;/font&gt;&amp;nbsp;/ apiVer_B &lt;font color=&quot;#ff0000&quot;&gt;&lt;b&gt;/ uuid-B&lt;/b&gt;&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;fontSize=14;fillColor=#60a917;fontColor=#ffffff;strokeColor=#2D7600;align=left;arcSize=22;gradientColor=#006600;gradientDirection=north;" parent="1" vertex="1">
<mxGeometry x="185" y="1251" width="270" height="50" as="geometry"/>
</mxCell>
<mxCell id="136" style="edgeStyle=none;html=1;fontColor=#FF0000;startArrow=none;startFill=0;endArrow=classic;endFill=1;" parent="1" source="135" edge="1">
<mxGeometry relative="1" as="geometry">
<mxPoint x="320" y="1251" as="targetPoint"/>
<Array as="points">
<mxPoint x="320" y="1156"/>
</Array>
</mxGeometry>
</mxCell>
<mxCell id="125" style="edgeStyle=none;html=1;entryX=0.045;entryY=0.98;entryDx=0;entryDy=0;fontColor=#FF0000;startArrow=none;startFill=0;endArrow=classic;endFill=1;entryPerimeter=0;" parent="1" target="126" edge="1">
<mxGeometry relative="1" as="geometry">
<mxPoint x="268" y="1251" as="sourcePoint"/>
<Array as="points"/>
</mxGeometry>
</mxCell>
<mxCell id="124" style="edgeStyle=none;html=1;entryX=0.1;entryY=-0.04;entryDx=0;entryDy=0;entryPerimeter=0;fontColor=#FF0000;startArrow=none;startFill=0;endArrow=classic;endFill=1;" parent="1" target="43" edge="1">
<mxGeometry relative="1" as="geometry">
<mxPoint x="212" y="981" as="sourcePoint"/>
</mxGeometry>
</mxCell>
<mxCell id="149" value="&lt;font color=&quot;#000000&quot;&gt;1:*&lt;/font&gt;&lt;font color=&quot;#00ff00&quot;&gt; &lt;b&gt;url_A / pubkey_A&lt;/b&gt;&lt;/font&gt; / apiVer_A&lt;br&gt;&lt;font color=&quot;#000000&quot;&gt;2:*&lt;/font&gt;&lt;font color=&quot;#00ff00&quot;&gt;&amp;nbsp;&lt;b&gt;url_A / pubkey_A&lt;/b&gt;&lt;/font&gt;&amp;nbsp;/ apiVer_A /&amp;nbsp;&lt;b&gt;&lt;font color=&quot;#ff00ff&quot;&gt;oneTimeCode&lt;br&gt;&lt;/font&gt;&lt;/b&gt;&lt;font color=&quot;#000000&quot;&gt;3:*&lt;/font&gt;&lt;font color=&quot;#00ff00&quot;&gt;&amp;nbsp;&lt;b&gt;url_A / pubkey_A&lt;/b&gt;&lt;/font&gt;&amp;nbsp;/ apiVer_A &lt;b&gt;&lt;font color=&quot;#00ff00&quot;&gt;/ &lt;/font&gt;&lt;font color=&quot;#ff0000&quot;&gt;uuid_A&lt;/font&gt;&lt;/b&gt;&lt;b&gt;&lt;font color=&quot;#ff00ff&quot;&gt;&lt;br&gt;&lt;/font&gt;&lt;/b&gt;" style="rounded=1;whiteSpace=wrap;html=1;fontSize=14;fillColor=#1ba1e2;fontColor=#ffffff;strokeColor=#006EAF;align=left;arcSize=28;" parent="1" vertex="1">
<mxGeometry x="1913.14" y="1171" width="320" height="60" as="geometry"/>
</mxCell>
<mxCell id="131" style="edgeStyle=none;html=1;fontColor=#000000;startArrow=none;startFill=0;endArrow=classic;endFill=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0.25;entryY=0;entryDx=0;entryDy=0;" parent="1" source="130" target="149" edge="1">
<mxGeometry relative="1" as="geometry">
<mxPoint x="2183.14" y="1236" as="targetPoint"/>
<Array as="points">
<mxPoint x="1993.14" y="1101"/>
</Array>
</mxGeometry>
</mxCell>
<mxCell id="133" value="1." style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontColor=#FF0000;" parent="131" vertex="1" connectable="0">
<mxGeometry x="-0.4043" y="-2" relative="1" as="geometry">
<mxPoint as="offset"/>
</mxGeometry>
</mxCell>
<mxCell id="67" value="" style="endArrow=classic;html=1;fontSize=14;fontColor=#FF8000;entryX=0.75;entryY=0;entryDx=0;entryDy=0;exitX=0.5;exitY=0;exitDx=0;exitDy=0;startArrow=none;" parent="1" source="120" target="149" edge="1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="1353.14" y="886" as="sourcePoint"/>
<mxPoint x="2082.64" y="816" as="targetPoint"/>
<Array as="points">
<mxPoint x="1683.14" y="866"/>
<mxPoint x="2153.14" y="866"/>
</Array>
</mxGeometry>
</mxCell>
<mxCell id="119" style="edgeStyle=none;html=1;fontColor=#000000;startArrow=none;startFill=0;endArrow=classic;endFill=1;exitX=0.5;exitY=0;exitDx=0;exitDy=0;entryX=0.5;entryY=1;entryDx=0;entryDy=0;" parent="1" source="149" target="53" edge="1">
<mxGeometry relative="1" as="geometry">
<mxPoint x="1893.14" y="831" as="sourcePoint"/>
<mxPoint x="2073.14" y="946" as="targetPoint"/>
<Array as="points"/>
</mxGeometry>
</mxCell>
<mxCell id="150" value="&lt;div style=&quot;font-size: 10px; font-weight: 700;&quot;&gt;&lt;font color=&quot;#000000&quot;&gt;- uuid_A&lt;/font&gt;&lt;/div&gt;&lt;div style=&quot;font-size: 10px; font-weight: 700;&quot;&gt;&lt;font color=&quot;#000000&quot;&gt;- url_A&lt;br style=&quot;font-size: 10px&quot;&gt;&lt;/font&gt;&lt;/div&gt;&lt;div style=&quot;font-size: 10px; font-weight: 700;&quot;&gt;&lt;font color=&quot;#000000&quot;&gt;- apiVer_A&lt;/font&gt;&lt;/div&gt;&lt;div style=&quot;font-size: 10px; font-weight: 700;&quot;&gt;&lt;font style=&quot;font-size: 10px&quot; color=&quot;#000000&quot;&gt;- privatkey_A&lt;br style=&quot;font-size: 10px&quot;&gt;&lt;/font&gt;&lt;/div&gt;&lt;div style=&quot;font-size: 10px; font-weight: 700;&quot;&gt;&lt;font style=&quot;font-size: 10px&quot; color=&quot;#000000&quot;&gt;- publickey_A&lt;/font&gt;&lt;/div&gt;" style="shape=cylinder3;whiteSpace=wrap;html=1;boundedLbl=1;backgroundOutline=1;size=15;rounded=1;fontColor=#FF0000;gradientColor=#006600;gradientDirection=north;fontSize=10;align=left;verticalAlign=top;" parent="1" vertex="1">
<mxGeometry x="95" y="1520" width="400" height="104" as="geometry"/>
</mxCell>
<mxCell id="56" value="&amp;nbsp;* url_B / &lt;font color=&quot;#ff0000&quot;&gt;&lt;b&gt;pubkey_B&lt;/b&gt;&lt;/font&gt; / uuid_B &lt;font color=&quot;#ff8000&quot;&gt;&lt;b&gt;/ name_B, etc.&lt;/b&gt;&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;fontSize=14;fillColor=#60a917;fontColor=#ffffff;strokeColor=#2D7600;align=left;arcSize=20;gradientColor=#006600;gradientDirection=north;" parent="1" vertex="1">
<mxGeometry x="185" y="1580" width="300" height="34" as="geometry"/>
</mxCell>
<mxCell id="151" value="&amp;nbsp; Apollo-Server" style="rounded=0;whiteSpace=wrap;html=1;fontSize=14;fillColor=#006600;fontColor=#ffffff;strokeColor=#2D7600;align=left;gradientColor=#ffffff;fontStyle=1;verticalAlign=top;" parent="1" vertex="1">
<mxGeometry x="55" y="1400" width="500" height="110" as="geometry"/>
</mxCell>
<mxCell id="61" value="encrypt with &lt;b style=&quot;font-size: 12px&quot;&gt;&lt;font color=&quot;#ff0000&quot; style=&quot;font-size: 12px&quot;&gt;pubkey_B&amp;nbsp;&lt;/font&gt;&lt;/b&gt;+ sign with &lt;font style=&quot;font-size: 12px&quot; color=&quot;#000000&quot;&gt;&lt;b style=&quot;font-size: 12px&quot;&gt;privatekey_A&lt;/b&gt;&lt;/font&gt;&lt;b style=&quot;font-size: 12px&quot;&gt;:&lt;br style=&quot;font-size: 12px&quot;&gt;- &lt;font color=&quot;#000000&quot;&gt;uuid_A&lt;/font&gt;,&amp;nbsp;&lt;/b&gt;&lt;b style=&quot;font-size: 12px&quot;&gt;uuid_B,&lt;/b&gt;&amp;nbsp;payload : name_A, description_A, etc." style="rounded=0;whiteSpace=wrap;html=1;fontSize=12;fillColor=#60a917;fontColor=#ffffff;strokeColor=#2D7600;align=left;" parent="1" vertex="1">
<mxGeometry x="215" y="1410" width="330" height="28" as="geometry"/>
</mxCell>
<mxCell id="59" value="" style="endArrow=classic;html=1;fontSize=14;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;" parent="1" source="61" target="64" edge="1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="925" y="1564" as="sourcePoint"/>
<mxPoint x="975" y="1514" as="targetPoint"/>
</mxGeometry>
</mxCell>
<mxCell id="60" value="&lt;b&gt;&amp;nbsp; request: &lt;/b&gt;http://&amp;lt;url_B&amp;gt;/&amp;lt;apiVer_B&amp;gt;/familiarizeCommunity( encrypted+signed( uuid_A, uuid_B, payload) )&amp;nbsp;&amp;nbsp;" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontSize=14;" parent="59" vertex="1" connectable="0">
<mxGeometry x="-0.215" y="-1" relative="1" as="geometry">
<mxPoint x="83" y="-3" as="offset"/>
</mxGeometry>
</mxCell>
<mxCell id="152" value="&lt;div style=&quot;font-size: 10px ; font-weight: 700&quot;&gt;&lt;font color=&quot;#000000&quot;&gt;- uuid_B&lt;/font&gt;&lt;/div&gt;&lt;div style=&quot;font-size: 10px ; font-weight: 700&quot;&gt;&lt;font color=&quot;#000000&quot;&gt;- url_B&lt;br style=&quot;font-size: 10px&quot;&gt;&lt;/font&gt;&lt;/div&gt;&lt;div style=&quot;font-size: 10px ; font-weight: 700&quot;&gt;&lt;font color=&quot;#000000&quot;&gt;- apiVer_B&lt;/font&gt;&lt;/div&gt;&lt;div style=&quot;font-size: 10px ; font-weight: 700&quot;&gt;&lt;font style=&quot;font-size: 10px&quot; color=&quot;#000000&quot;&gt;- privatkey_B&lt;br style=&quot;font-size: 10px&quot;&gt;&lt;/font&gt;&lt;/div&gt;&lt;div style=&quot;font-size: 10px ; font-weight: 700&quot;&gt;&lt;font style=&quot;font-size: 10px&quot; color=&quot;#000000&quot;&gt;- publickey_B&lt;/font&gt;&lt;/div&gt;" style="shape=cylinder3;whiteSpace=wrap;html=1;boundedLbl=1;backgroundOutline=1;size=15;rounded=1;gradientColor=#7ea6e0;gradientDirection=north;fontSize=10;align=left;verticalAlign=top;fillColor=#dae8fc;strokeColor=#6c8ebf;" parent="1" vertex="1">
<mxGeometry x="1815" y="1496" width="450" height="110" as="geometry"/>
</mxCell>
<mxCell id="160" style="edgeStyle=none;html=1;entryX=1;entryY=0.5;entryDx=0;entryDy=0;fontSize=12;fontColor=#FF0000;startArrow=none;startFill=0;endArrow=classic;endFill=1;" parent="1" source="58" target="159" edge="1">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="2080" y="1462"/>
</Array>
</mxGeometry>
</mxCell>
<mxCell id="58" value="&amp;nbsp;* url_A / &lt;font color=&quot;#ff0000&quot;&gt;&lt;b&gt;pubkey_A&lt;/b&gt;&lt;/font&gt; / apiVer_A / uuid_A &lt;font color=&quot;#ff8000&quot;&gt;/ &lt;b&gt;name_A, etc.&lt;/b&gt;&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;fontSize=14;fillColor=#1ba1e2;fontColor=#ffffff;strokeColor=#006EAF;align=left;arcSize=26;" parent="1" vertex="1">
<mxGeometry x="1895" y="1564.5" width="370" height="23" as="geometry"/>
</mxCell>
<mxCell id="156" style="edgeStyle=none;html=1;entryX=0.77;entryY=0.022;entryDx=0;entryDy=0;entryPerimeter=0;fontSize=12;fontColor=#FFFFFF;startArrow=none;startFill=0;endArrow=classic;endFill=1;" parent="1" source="154" target="58" edge="1">
<mxGeometry relative="1" as="geometry">
<Array as="points"/>
</mxGeometry>
</mxCell>
<mxCell id="154" value="search entry with uuid_A" style="rounded=0;whiteSpace=wrap;html=1;fontSize=14;fillColor=#dae8fc;strokeColor=#6c8ebf;align=left;" parent="1" vertex="1">
<mxGeometry x="2101" y="1411" width="158.14" height="28" as="geometry"/>
</mxCell>
<mxCell id="65" value="matching &lt;br&gt;uui_B&amp;nbsp;?" style="rhombus;whiteSpace=wrap;html=1;fontSize=14;fillColor=#dae8fc;strokeColor=#6c8ebf;" parent="1" vertex="1">
<mxGeometry x="1963.14" y="1400" width="120" height="50" as="geometry"/>
</mxCell>
<mxCell id="157" value="" style="edgeStyle=none;html=1;entryX=0;entryY=0.5;entryDx=0;entryDy=0;fontSize=12;fontColor=#FFFFFF;startArrow=none;startFill=0;endArrow=none;endFill=1;" parent="1" source="64" target="65" edge="1">
<mxGeometry relative="1" as="geometry">
<mxPoint x="1575" y="1430" as="sourcePoint"/>
<mxPoint x="1733.1400000000003" y="1430" as="targetPoint"/>
</mxGeometry>
</mxCell>
<mxCell id="162" style="edgeStyle=none;html=1;entryX=1;entryY=0.5;entryDx=0;entryDy=0;fontSize=12;fontColor=#FF0000;startArrow=none;startFill=0;endArrow=classic;endFill=1;" parent="1" source="159" target="161" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="159" value="verify sign of parameters&lt;br&gt;with &lt;font color=&quot;#ff0000&quot;&gt;&lt;b&gt;pubkey_A&lt;/b&gt;&lt;/font&gt;" style="rounded=0;whiteSpace=wrap;html=1;fontSize=14;fillColor=#dae8fc;strokeColor=#6c8ebf;align=left;" parent="1" vertex="1">
<mxGeometry x="1835.35" y="1443" width="161.86" height="38" as="geometry"/>
</mxCell>
<mxCell id="164" style="edgeStyle=none;html=1;entryX=1;entryY=0.5;entryDx=0;entryDy=0;fontSize=12;fontColor=#FF0000;startArrow=none;startFill=0;endArrow=classic;endFill=1;exitX=0.5;exitY=1;exitDx=0;exitDy=0;" parent="1" source="161" target="166" edge="1">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="1743" y="1531"/>
</Array>
</mxGeometry>
</mxCell>
<mxCell id="161" value="matching &lt;br&gt;uui_A ?" style="rhombus;whiteSpace=wrap;html=1;fontSize=14;fillColor=#dae8fc;strokeColor=#6c8ebf;" parent="1" vertex="1">
<mxGeometry x="1683.14" y="1438" width="120" height="50" as="geometry"/>
</mxCell>
<mxCell id="163" value="encrypt with &lt;b style=&quot;font-size: 12px&quot;&gt;&lt;font color=&quot;#ff0000&quot; style=&quot;font-size: 12px&quot;&gt;pubkey_A&amp;nbsp;&lt;/font&gt;&lt;/b&gt;+ sign with &lt;font style=&quot;font-size: 12px&quot; color=&quot;#000000&quot;&gt;&lt;b style=&quot;font-size: 12px&quot;&gt;privatekey_B&lt;/b&gt;&lt;/font&gt;&lt;b style=&quot;font-size: 12px&quot;&gt;:&lt;br style=&quot;font-size: 12px&quot;&gt;&lt;/b&gt;- payload : name_B, description_B, etc." style="rounded=0;whiteSpace=wrap;html=1;fontSize=12;fillColor=#dae8fc;strokeColor=#6c8ebf;align=left;" parent="1" vertex="1">
<mxGeometry x="1385" y="1459" width="280" height="40" as="geometry"/>
</mxCell>
<mxCell id="170" style="edgeStyle=none;html=1;entryX=1;entryY=0.5;entryDx=0;entryDy=0;fontSize=12;fontColor=#FFFFFF;startArrow=none;startFill=0;endArrow=classic;endFill=1;" parent="1" source="165" target="169" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="165" value="decrypt encoded parameters&amp;nbsp;with privatkey_A&lt;br&gt;verify sign with&lt;span style=&quot;color: rgb(0 , 0 , 0)&quot;&gt; &lt;/span&gt;&lt;font color=&quot;#ff0000&quot;&gt;&lt;b&gt;pubkey_B&lt;/b&gt;&lt;/font&gt;&lt;span style=&quot;color: rgb(0 , 0 , 0)&quot;&gt;&lt;br&gt;&lt;/span&gt;" style="rounded=0;whiteSpace=wrap;html=1;fontSize=12;fillColor=#60a917;fontColor=#ffffff;strokeColor=#2D7600;align=left;" parent="1" vertex="1">
<mxGeometry x="295" y="1464" width="250" height="29" as="geometry"/>
</mxCell>
<mxCell id="62" value="" style="endArrow=classic;html=1;fontSize=14;exitX=0;exitY=0.5;exitDx=0;exitDy=0;entryX=1;entryY=0.5;entryDx=0;entryDy=0;" parent="1" source="163" target="165" edge="1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="995" y="1224" as="sourcePoint"/>
<mxPoint x="1045" y="1174" as="targetPoint"/>
</mxGeometry>
</mxCell>
<mxCell id="63" value="&lt;b&gt;&amp;nbsp; response:&lt;/b&gt;&amp;nbsp;encrypted + signed ( payload_B )&amp;nbsp;&amp;nbsp;" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontSize=14;" parent="62" vertex="1" connectable="0">
<mxGeometry x="0.255" y="2" relative="1" as="geometry">
<mxPoint x="52" y="-3" as="offset"/>
</mxGeometry>
</mxCell>
<mxCell id="167" style="edgeStyle=none;html=1;entryX=0;entryY=0;entryDx=0;entryDy=75;entryPerimeter=0;fontSize=12;fontColor=#FF0000;startArrow=none;startFill=0;endArrow=classic;endFill=1;" parent="1" source="166" target="152" edge="1">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="1605" y="1571"/>
</Array>
</mxGeometry>
</mxCell>
<mxCell id="168" style="edgeStyle=none;html=1;entryX=0.25;entryY=1;entryDx=0;entryDy=0;fontSize=12;fontColor=#FF0000;startArrow=none;startFill=0;endArrow=classic;endFill=1;" parent="1" source="166" target="163" edge="1">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="1455" y="1530"/>
</Array>
</mxGeometry>
</mxCell>
<mxCell id="166" value="write payload in entry with &lt;b style=&quot;font-size: 12px&quot;&gt;&lt;font color=&quot;#ff0000&quot; style=&quot;font-size: 12px&quot;&gt;pubkey_A&lt;/font&gt;&lt;/b&gt;&lt;b style=&quot;font-size: 12px&quot;&gt;:&lt;br style=&quot;font-size: 12px&quot;&gt;&lt;/b&gt;- payload : name_A, description_A, etc." style="rounded=0;whiteSpace=wrap;html=1;fontSize=12;fillColor=#dae8fc;strokeColor=#6c8ebf;align=left;" parent="1" vertex="1">
<mxGeometry x="1495" y="1511" width="220" height="40" as="geometry"/>
</mxCell>
<mxCell id="171" style="edgeStyle=none;html=1;entryX=0.25;entryY=0;entryDx=0;entryDy=0;fontSize=12;fontColor=#FFFFFF;startArrow=none;startFill=0;endArrow=classic;endFill=1;" parent="1" source="169" target="56" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="169" value="write payload in entry with &lt;b style=&quot;font-size: 12px&quot;&gt;&lt;font color=&quot;#ff0000&quot; style=&quot;font-size: 12px&quot;&gt;pubkey_B&lt;/font&gt;&lt;/b&gt;&lt;b style=&quot;font-size: 12px&quot;&gt;:&lt;br style=&quot;font-size: 12px&quot;&gt;&lt;/b&gt;- payload : name_B, description_B, etc." style="rounded=0;whiteSpace=wrap;html=1;fontSize=12;fillColor=#60a917;strokeColor=#2D7600;align=left;fontColor=#ffffff;" parent="1" vertex="1">
<mxGeometry x="60" y="1463" width="220" height="40" as="geometry"/>
</mxCell>
<mxCell id="180" value="&amp;nbsp; Apollo-Server" style="rounded=0;whiteSpace=wrap;html=1;fontSize=14;fillColor=#006600;fontColor=#ffffff;strokeColor=#2D7600;align=left;gradientColor=#ffffff;fontStyle=1;verticalAlign=top;" parent="1" vertex="1">
<mxGeometry x="403.14" y="640" width="140" height="120" as="geometry"/>
</mxCell>
<mxCell id="181" value="&amp;nbsp; Apollo-Server" style="rounded=0;whiteSpace=wrap;html=1;fontSize=14;fillColor=#dae8fc;strokeColor=#6c8ebf;align=left;gradientColor=#7ea6e0;gradientDirection=north;fontStyle=1;verticalAlign=top;" parent="1" vertex="1">
<mxGeometry x="1373.14" y="640" width="130" height="120" as="geometry"/>
</mxCell>
<mxCell id="183" value="ask for pub&lt;font color=&quot;#000000&quot; style=&quot;font-size: 12px&quot;&gt;key_A&lt;/font&gt;" style="rounded=0;whiteSpace=wrap;html=1;fontSize=12;fillColor=#dae8fc;strokeColor=#6c8ebf;align=left;" parent="1" vertex="1">
<mxGeometry x="1393.14" y="720" width="101.86" height="20" as="geometry"/>
</mxCell>
<mxCell id="182" style="edgeStyle=none;html=1;entryX=1;entryY=0.5;entryDx=0;entryDy=0;fontSize=12;fontColor=#FFFFFF;startArrow=none;startFill=0;endArrow=classic;endFill=1;exitX=0;exitY=0.5;exitDx=0;exitDy=0;exitPerimeter=0;" parent="1" source="142" target="191" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="184" value="read&amp;nbsp;&lt;b style=&quot;font-size: 12px;&quot;&gt;pubkey_A&lt;/b&gt;&amp;nbsp;" style="rounded=0;whiteSpace=wrap;html=1;fontSize=12;fillColor=#60a917;fontColor=#ffffff;strokeColor=#2D7600;align=left;" parent="1" vertex="1">
<mxGeometry x="430.93" y="720" width="90" height="20" as="geometry"/>
</mxCell>
<mxCell id="185" style="edgeStyle=none;html=1;entryX=0;entryY=0.5;entryDx=0;entryDy=0;fontSize=12;fontColor=#FFFFFF;startArrow=none;startFill=0;endArrow=classic;endFill=1;exitX=1;exitY=1;exitDx=0;exitDy=-15;exitPerimeter=0;" parent="1" source="139" target="184" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="186" value="&lt;font style=&quot;font-size: 24px&quot;&gt;Autorized Communication&lt;/font&gt;" style="text;html=1;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontSize=24;fontColor=#000000;fontStyle=1" parent="1" vertex="1">
<mxGeometry x="5" y="1340" width="440" height="30" as="geometry"/>
</mxCell>
<mxCell id="189" value="ask for&amp;nbsp;&lt;b style=&quot;font-size: 12px&quot;&gt;pubkey_B&lt;/b&gt;&amp;nbsp;" style="rounded=0;whiteSpace=wrap;html=1;fontSize=12;fillColor=#60a917;fontColor=#ffffff;strokeColor=#2D7600;align=left;" parent="1" vertex="1">
<mxGeometry x="430.93" y="679" width="108.14" height="20" as="geometry"/>
</mxCell>
<mxCell id="187" style="edgeStyle=none;html=1;entryX=0.5;entryY=0;entryDx=0;entryDy=0;fontSize=12;fontColor=#FFFFFF;startArrow=none;startFill=0;endArrow=classic;endFill=1;" parent="1" source="33" target="189" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="188" value="&lt;font color=&quot;#000000&quot;&gt;url_B&lt;/font&gt;" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontSize=12;fontColor=#FFFFFF;labelBackgroundColor=default;labelBorderColor=default;" parent="187" vertex="1" connectable="0">
<mxGeometry x="-0.423" relative="1" as="geometry">
<mxPoint x="1" y="-3" as="offset"/>
</mxGeometry>
</mxCell>
<mxCell id="174" value="" style="endArrow=classic;html=1;fontSize=14;exitX=0;exitY=1;exitDx=0;exitDy=0;entryX=1;entryY=1;entryDx=0;entryDy=0;" parent="1" source="191" target="189" edge="1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="1363.14" y="710" as="sourcePoint"/>
<mxPoint x="563.14" y="710" as="targetPoint"/>
</mxGeometry>
</mxCell>
<mxCell id="175" value="&lt;b&gt;&amp;nbsp; response:&lt;/b&gt;&amp;nbsp;pubkey_B" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontSize=14;" parent="174" vertex="1" connectable="0">
<mxGeometry x="0.255" y="2" relative="1" as="geometry">
<mxPoint x="52" y="-3" as="offset"/>
</mxGeometry>
</mxCell>
<mxCell id="172" value="" style="endArrow=classic;html=1;fontSize=14;exitX=1;exitY=0;exitDx=0;exitDy=0;entryX=0;entryY=0;entryDx=0;entryDy=0;" parent="1" source="189" target="191" edge="1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="563.14" y="690" as="sourcePoint"/>
<mxPoint x="1363.14" y="690" as="targetPoint"/>
</mxGeometry>
</mxCell>
<mxCell id="173" value="&lt;b&gt;&amp;nbsp; request: &lt;/b&gt;http://&amp;lt;&lt;b&gt;url_B&lt;/b&gt;&amp;gt;/&amp;lt;&lt;b&gt;apiVer_B&lt;/b&gt;&amp;gt;/&lt;b&gt;getPubKey&lt;/b&gt;()&amp;nbsp;&amp;nbsp;" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontSize=14;" parent="172" vertex="1" connectable="0">
<mxGeometry x="-0.215" y="-1" relative="1" as="geometry">
<mxPoint x="76" as="offset"/>
</mxGeometry>
</mxCell>
<mxCell id="190" style="edgeStyle=none;html=1;entryX=0.936;entryY=0;entryDx=0;entryDy=0;fontSize=12;fontColor=#000000;startArrow=none;startFill=0;endArrow=classic;endFill=1;entryPerimeter=0;exitX=0.89;exitY=1.004;exitDx=0;exitDy=0;exitPerimeter=0;" parent="1" source="34" target="183" edge="1">
<mxGeometry relative="1" as="geometry">
<mxPoint x="1488" y="620" as="sourcePoint"/>
</mxGeometry>
</mxCell>
<mxCell id="192" value="url_A" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontSize=12;fontColor=#000000;labelBorderColor=default;" parent="190" vertex="1" connectable="0">
<mxGeometry x="-0.766" relative="1" as="geometry">
<mxPoint as="offset"/>
</mxGeometry>
</mxCell>
<mxCell id="191" value="read pub&lt;font color=&quot;#000000&quot; style=&quot;font-size: 12px&quot;&gt;key_B&lt;/font&gt;" style="rounded=0;whiteSpace=wrap;html=1;fontSize=12;fillColor=#dae8fc;strokeColor=#6c8ebf;align=left;" parent="1" vertex="1">
<mxGeometry x="1379.85" y="679" width="85.15" height="20" as="geometry"/>
</mxCell>
<mxCell id="178" value="" style="endArrow=none;html=1;fontSize=14;startArrow=classic;startFill=1;endFill=0;exitX=1;exitY=0;exitDx=0;exitDy=0;entryX=0;entryY=0;entryDx=0;entryDy=0;" parent="1" source="184" target="183" edge="1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="563.14" y="730" as="sourcePoint"/>
<mxPoint x="1363.14" y="730" as="targetPoint"/>
</mxGeometry>
</mxCell>
<mxCell id="179" value="&lt;b&gt;&amp;nbsp; request: &lt;/b&gt;http://&amp;lt;&lt;b&gt;url_A&lt;/b&gt;&amp;gt;/&amp;lt;&lt;b&gt;apiVer_A&lt;/b&gt;&amp;gt;/&lt;b&gt;getPubKey&lt;/b&gt;()&amp;nbsp;&amp;nbsp;" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontSize=14;" parent="178" vertex="1" connectable="0">
<mxGeometry x="-0.215" y="-1" relative="1" as="geometry">
<mxPoint x="76" as="offset"/>
</mxGeometry>
</mxCell>
<mxCell id="176" value="" style="endArrow=none;html=1;fontSize=14;startArrow=classic;startFill=1;endFill=0;exitX=0;exitY=1;exitDx=0;exitDy=0;entryX=1;entryY=1;entryDx=0;entryDy=0;" parent="1" source="183" target="184" edge="1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="1363.14" y="750" as="sourcePoint"/>
<mxPoint x="563.14" y="750" as="targetPoint"/>
</mxGeometry>
</mxCell>
<mxCell id="177" value="&lt;b&gt;&amp;nbsp; response:&lt;/b&gt;&amp;nbsp;pubkey_A" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontSize=14;" parent="176" vertex="1" connectable="0">
<mxGeometry x="0.255" y="2" relative="1" as="geometry">
<mxPoint x="52" y="-3" as="offset"/>
</mxGeometry>
</mxCell>
<mxCell id="41" value="" style="endArrow=classic;html=1;fontSize=14;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;" parent="1" source="183" target="40" edge="1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="1773.14" y="510" as="sourcePoint"/>
<mxPoint x="1823.14" y="460" as="targetPoint"/>
</mxGeometry>
</mxCell>
<mxCell id="36" value="" style="endArrow=classic;html=1;fontSize=14;exitX=0;exitY=0.5;exitDx=0;exitDy=0;startArrow=none;entryX=1;entryY=0.5;entryDx=0;entryDy=0;" parent="1" source="189" target="37" edge="1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="623.14" y="693" as="sourcePoint"/>
<mxPoint x="243.14" y="652" as="targetPoint"/>
</mxGeometry>
</mxCell>
<mxCell id="193" value="" style="endArrow=none;dashed=1;html=1;strokeWidth=4;fontSize=14;fontColor=#FF8000;dashPattern=1 4;" parent="1" edge="1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="5" y="524" as="sourcePoint"/>
<mxPoint x="2325" y="524" as="targetPoint"/>
</mxGeometry>
</mxCell>
<mxCell id="194" value="&lt;font style=&quot;font-size: 18px&quot;&gt;direct exchange&lt;/font&gt;" style="text;html=1;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontSize=18;fontColor=#000000;fontStyle=1" parent="1" vertex="1">
<mxGeometry x="5" y="536" width="110" height="30" as="geometry"/>
</mxCell>
<mxCell id="195" value="" style="endArrow=none;dashed=1;html=1;strokeWidth=4;fontSize=14;fontColor=#FF8000;dashPattern=1 4;" parent="1" edge="1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="5" y="180" as="sourcePoint"/>
<mxPoint x="2325" y="180" as="targetPoint"/>
</mxGeometry>
</mxCell>
<mxCell id="196" value="&lt;font style=&quot;font-size: 18px&quot;&gt;join&amp;amp;connect&lt;/font&gt;" style="text;html=1;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontSize=18;fontColor=#000000;fontStyle=1" parent="1" vertex="1">
<mxGeometry x="5" y="192" width="140" height="30" as="geometry"/>
</mxCell>
</root>
</mxGraphModel>
</diagram>
</mxfile>

Binary file not shown.

After

Width:  |  Height:  |  Size: 653 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 142 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 157 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 158 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 156 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 159 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 180 KiB

View File

@ -0,0 +1,327 @@
<mxfile host="65bd71144e">
<diagram id="RL0nU6kSSy2ttf3N9WEb" name="Seite-1">
<mxGraphModel dx="2348" dy="1231" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="2336" pageHeight="1654" math="0" shadow="0">
<root>
<mxCell id="0"/>
<mxCell id="1" parent="0"/>
<mxCell id="102" style="edgeStyle=none;html=1;fontSize=34;endArrow=none;endFill=0;" edge="1" parent="1" source="2" target="98">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="2" value="Community" style="ellipse;whiteSpace=wrap;html=1;fillColor=#d5e8d4;gradientColor=#97d077;strokeColor=#82b366;fontSize=17;fontStyle=1" vertex="1" parent="1">
<mxGeometry x="720" y="320" width="120" height="80" as="geometry"/>
</mxCell>
<mxCell id="99" style="edgeStyle=none;html=1;fontSize=34;endArrow=none;endFill=0;" edge="1" parent="1" source="3" target="98">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="3" value="User" style="ellipse;whiteSpace=wrap;html=1;fillColor=#fff2cc;gradientColor=#ffd966;strokeColor=#d6b656;fontSize=17;fontStyle=1" vertex="1" parent="1">
<mxGeometry x="1360" y="800" width="120" height="80" as="geometry"/>
</mxCell>
<mxCell id="100" style="edgeStyle=none;html=1;fontSize=34;endArrow=none;endFill=0;" edge="1" parent="1" source="4" target="98">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="4" value="Rollen &amp;amp; Rechte" style="ellipse;whiteSpace=wrap;html=1;fillColor=#e6d0de;gradientColor=#d5739d;strokeColor=#996185;fontSize=17;fontStyle=1" vertex="1" parent="1">
<mxGeometry x="920" y="920" width="120" height="80" as="geometry"/>
</mxCell>
<mxCell id="28" style="edgeStyle=none;html=1;fontSize=14;endArrow=none;endFill=0;" edge="1" parent="1" source="5" target="23">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="103" style="edgeStyle=none;html=1;fontSize=34;endArrow=none;endFill=0;" edge="1" parent="1" source="5" target="98">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="5" value="Konto" style="ellipse;whiteSpace=wrap;html=1;fillColor=#dae8fc;gradientColor=#7ea6e0;strokeColor=#6c8ebf;fontSize=17;fontStyle=1" vertex="1" parent="1">
<mxGeometry x="1200" y="358.29" width="120" height="80" as="geometry"/>
</mxCell>
<mxCell id="101" style="edgeStyle=none;html=1;fontSize=34;endArrow=none;endFill=0;" edge="1" parent="1" source="6" target="98">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="6" value="Contributions" style="ellipse;whiteSpace=wrap;html=1;fillColor=#ffcd28;gradientColor=#ffa500;strokeColor=#d79b00;fontSize=17;fontStyle=1" vertex="1" parent="1">
<mxGeometry x="640" y="720" width="120" height="80" as="geometry"/>
</mxCell>
<mxCell id="8" style="edgeStyle=none;html=1;fontSize=15;endArrow=none;endFill=0;" edge="1" parent="1" source="7" target="5">
<mxGeometry relative="1" as="geometry">
<mxPoint x="1642.6305768491611" y="320.00417540776294" as="sourcePoint"/>
</mxGeometry>
</mxCell>
<mxCell id="96" style="edgeStyle=none;html=1;fontSize=14;endArrow=none;endFill=0;" edge="1" parent="1" source="7" target="92">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="7" value="Transaktionen" style="ellipse;whiteSpace=wrap;html=1;fillColor=#dae8fc;gradientColor=#7ea6e0;strokeColor=#6c8ebf;fontSize=14;fontStyle=1" vertex="1" parent="1">
<mxGeometry x="1560" y="320" width="120" height="80" as="geometry"/>
</mxCell>
<mxCell id="10" style="edgeStyle=none;html=1;fontSize=14;endArrow=none;endFill=0;" edge="1" parent="1" source="9" target="2">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="9" value="Federation" style="ellipse;whiteSpace=wrap;html=1;fillColor=#d5e8d4;gradientColor=#97d077;strokeColor=#82b366;fontSize=14;fontStyle=1" vertex="1" parent="1">
<mxGeometry x="460" y="270" width="120" height="40" as="geometry"/>
</mxCell>
<mxCell id="19" style="edgeStyle=none;html=1;fontSize=14;endArrow=none;endFill=0;" edge="1" parent="1" source="11" target="3">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="11" value="Register" style="ellipse;whiteSpace=wrap;html=1;fillColor=#fff2cc;gradientColor=#ffd966;strokeColor=#d6b656;fontSize=14;fontStyle=1" vertex="1" parent="1">
<mxGeometry x="1720" y="850" width="120" height="40" as="geometry"/>
</mxCell>
<mxCell id="12" value="Login" style="ellipse;whiteSpace=wrap;html=1;fillColor=#fff2cc;gradientColor=#ffd966;strokeColor=#d6b656;fontSize=14;fontStyle=1" vertex="1" parent="1">
<mxGeometry x="1720" y="890" width="120" height="40" as="geometry"/>
</mxCell>
<mxCell id="18" style="edgeStyle=none;html=1;entryX=0;entryY=0.5;entryDx=0;entryDy=0;fontSize=14;endArrow=none;endFill=0;" edge="1" parent="1" source="13" target="14">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="13" value="Profile" style="ellipse;whiteSpace=wrap;html=1;fillColor=#fff2cc;gradientColor=#ffd966;strokeColor=#d6b656;fontSize=14;fontStyle=1" vertex="1" parent="1">
<mxGeometry x="1720" y="930" width="120" height="40" as="geometry"/>
</mxCell>
<mxCell id="14" value="Passwort" style="ellipse;whiteSpace=wrap;html=1;fillColor=#fff2cc;gradientColor=#ffd966;strokeColor=#d6b656;fontSize=14;fontStyle=1" vertex="1" parent="1">
<mxGeometry x="1880" y="930" width="120" height="40" as="geometry"/>
</mxCell>
<mxCell id="15" value="Email" style="ellipse;whiteSpace=wrap;html=1;fillColor=#fff2cc;gradientColor=#ffd966;strokeColor=#d6b656;fontSize=14;fontStyle=1" vertex="1" parent="1">
<mxGeometry x="1880" y="970" width="120" height="40" as="geometry"/>
</mxCell>
<mxCell id="16" value="Alias" style="ellipse;whiteSpace=wrap;html=1;fillColor=#fff2cc;gradientColor=#ffd966;strokeColor=#d6b656;fontSize=14;fontStyle=1" vertex="1" parent="1">
<mxGeometry x="1880" y="1010" width="120" height="40" as="geometry"/>
</mxCell>
<mxCell id="17" value="Sonstiges" style="ellipse;whiteSpace=wrap;html=1;fillColor=#fff2cc;gradientColor=#ffd966;strokeColor=#d6b656;fontSize=14;fontStyle=1" vertex="1" parent="1">
<mxGeometry x="1880" y="1050" width="120" height="40" as="geometry"/>
</mxCell>
<mxCell id="20" value="Umzug" style="ellipse;whiteSpace=wrap;html=1;fillColor=#fff2cc;gradientColor=#ffd966;strokeColor=#d6b656;fontSize=14;fontStyle=1" vertex="1" parent="1">
<mxGeometry x="1720" y="970" width="120" height="40" as="geometry"/>
</mxCell>
<mxCell id="21" value="DSGVO" style="ellipse;whiteSpace=wrap;html=1;fillColor=#fff2cc;gradientColor=#ffd966;strokeColor=#d6b656;fontSize=14;fontStyle=1" vertex="1" parent="1">
<mxGeometry x="1720" y="1010" width="120" height="40" as="geometry"/>
</mxCell>
<mxCell id="22" value="Löschen" style="ellipse;whiteSpace=wrap;html=1;fillColor=#fff2cc;gradientColor=#ffd966;strokeColor=#d6b656;fontSize=14;fontStyle=1" vertex="1" parent="1">
<mxGeometry x="1720" y="1050" width="120" height="40" as="geometry"/>
</mxCell>
<mxCell id="30" style="edgeStyle=none;html=1;entryX=1;entryY=0.5;entryDx=0;entryDy=0;fontSize=14;endArrow=none;endFill=0;" edge="1" parent="1" source="23" target="29">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="23" value="Anlegen" style="ellipse;whiteSpace=wrap;html=1;fillColor=#dae8fc;gradientColor=#7ea6e0;strokeColor=#6c8ebf;fontSize=14;fontStyle=1" vertex="1" parent="1">
<mxGeometry x="1360" y="480" width="120" height="40" as="geometry"/>
</mxCell>
<mxCell id="24" value="Anzeigen" style="ellipse;whiteSpace=wrap;html=1;fillColor=#dae8fc;gradientColor=#7ea6e0;strokeColor=#6c8ebf;fontSize=14;fontStyle=1" vertex="1" parent="1">
<mxGeometry x="1360" y="520" width="120" height="40" as="geometry"/>
</mxCell>
<mxCell id="25" value="Kontoauszug" style="ellipse;whiteSpace=wrap;html=1;fillColor=#dae8fc;gradientColor=#7ea6e0;strokeColor=#6c8ebf;fontSize=14;fontStyle=1" vertex="1" parent="1">
<mxGeometry x="1360" y="640" width="120" height="40" as="geometry"/>
</mxCell>
<mxCell id="26" value="Umzug" style="ellipse;whiteSpace=wrap;html=1;fillColor=#dae8fc;gradientColor=#7ea6e0;strokeColor=#6c8ebf;fontSize=14;fontStyle=1" vertex="1" parent="1">
<mxGeometry x="1360" y="560" width="120" height="40" as="geometry"/>
</mxCell>
<mxCell id="27" value="Löschen" style="ellipse;whiteSpace=wrap;html=1;fillColor=#dae8fc;gradientColor=#7ea6e0;strokeColor=#6c8ebf;fontSize=14;fontStyle=1" vertex="1" parent="1">
<mxGeometry x="1360" y="600" width="120" height="40" as="geometry"/>
</mxCell>
<mxCell id="29" value="Standard" style="ellipse;whiteSpace=wrap;html=1;fillColor=#dae8fc;gradientColor=#7ea6e0;strokeColor=#6c8ebf;fontSize=14;fontStyle=1" vertex="1" parent="1">
<mxGeometry x="1520" y="480" width="120" height="40" as="geometry"/>
</mxCell>
<mxCell id="31" value="AUF" style="ellipse;whiteSpace=wrap;html=1;fillColor=#dae8fc;gradientColor=#7ea6e0;strokeColor=#6c8ebf;fontSize=14;fontStyle=1" vertex="1" parent="1">
<mxGeometry x="1520" y="520" width="120" height="40" as="geometry"/>
</mxCell>
<mxCell id="32" value="Gemeinwohl" style="ellipse;whiteSpace=wrap;html=1;fillColor=#dae8fc;gradientColor=#7ea6e0;strokeColor=#6c8ebf;fontSize=14;fontStyle=1" vertex="1" parent="1">
<mxGeometry x="1520" y="560" width="120" height="40" as="geometry"/>
</mxCell>
<mxCell id="34" style="edgeStyle=none;html=1;entryX=1;entryY=0.5;entryDx=0;entryDy=0;fontSize=14;endArrow=none;endFill=0;exitX=0;exitY=0.5;exitDx=0;exitDy=0;" edge="1" parent="1" source="36" target="7">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="33" value="senden" style="ellipse;whiteSpace=wrap;html=1;fillColor=#dae8fc;gradientColor=#7ea6e0;strokeColor=#6c8ebf;fontSize=14;fontStyle=1" vertex="1" parent="1">
<mxGeometry x="1750" y="380" width="120" height="40" as="geometry"/>
</mxCell>
<mxCell id="85" style="edgeStyle=none;html=1;entryX=0.5;entryY=0;entryDx=0;entryDy=0;fontSize=14;endArrow=none;endFill=0;" edge="1" parent="1" source="35" target="83">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="35" value="empfangen" style="ellipse;whiteSpace=wrap;html=1;fillColor=#dae8fc;gradientColor=#7ea6e0;strokeColor=#6c8ebf;fontSize=14;fontStyle=1" vertex="1" parent="1">
<mxGeometry x="1750" y="420" width="120" height="40" as="geometry"/>
</mxCell>
<mxCell id="36" value="schöpfen" style="ellipse;whiteSpace=wrap;html=1;fillColor=#dae8fc;gradientColor=#7ea6e0;strokeColor=#6c8ebf;fontSize=14;fontStyle=1" vertex="1" parent="1">
<mxGeometry x="1750" y="340" width="120" height="40" as="geometry"/>
</mxCell>
<mxCell id="44" style="edgeStyle=none;html=1;fontSize=14;endArrow=none;endFill=0;" edge="1" parent="1" source="37" target="4">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="46" style="edgeStyle=none;html=1;fontSize=14;endArrow=none;endFill=0;" edge="1" parent="1" source="37" target="39">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="37" value="Rolle" style="ellipse;whiteSpace=wrap;html=1;fillColor=#e6d0de;gradientColor=#d5739d;strokeColor=#996185;fontSize=14;fontStyle=1" vertex="1" parent="1">
<mxGeometry x="1080" y="1000" width="120" height="40" as="geometry"/>
</mxCell>
<mxCell id="45" style="edgeStyle=none;html=1;fontSize=14;endArrow=none;endFill=0;" edge="1" parent="1" source="38" target="4">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="38" value="Recht" style="ellipse;whiteSpace=wrap;html=1;fillColor=#e6d0de;gradientColor=#d5739d;strokeColor=#996185;fontSize=14;fontStyle=1" vertex="1" parent="1">
<mxGeometry x="820" y="1030" width="120" height="40" as="geometry"/>
</mxCell>
<mxCell id="39" value="User" style="ellipse;whiteSpace=wrap;html=1;fillColor=#e6d0de;gradientColor=#d5739d;strokeColor=#996185;fontSize=14;fontStyle=1" vertex="1" parent="1">
<mxGeometry x="1220" y="1060" width="120" height="40" as="geometry"/>
</mxCell>
<mxCell id="47" style="edgeStyle=none;html=1;fontSize=14;endArrow=none;endFill=0;" edge="1" parent="1" source="40" target="41">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="40" value="Admin" style="ellipse;whiteSpace=wrap;html=1;fillColor=#e6d0de;gradientColor=#d5739d;strokeColor=#996185;fontSize=14;fontStyle=1" vertex="1" parent="1">
<mxGeometry x="1220" y="1100" width="120" height="40" as="geometry"/>
</mxCell>
<mxCell id="41" value="SuperUser" style="ellipse;whiteSpace=wrap;html=1;fillColor=#e6d0de;gradientColor=#d5739d;strokeColor=#996185;fontSize=14;fontStyle=1" vertex="1" parent="1">
<mxGeometry x="1500" y="1140" width="120" height="40" as="geometry"/>
</mxCell>
<mxCell id="42" value="Support" style="ellipse;whiteSpace=wrap;html=1;fillColor=#e6d0de;gradientColor=#d5739d;strokeColor=#996185;fontSize=14;fontStyle=1" vertex="1" parent="1">
<mxGeometry x="1220" y="1140" width="120" height="40" as="geometry"/>
</mxCell>
<mxCell id="43" value="AUF-Admin" style="ellipse;whiteSpace=wrap;html=1;fillColor=#e6d0de;gradientColor=#d5739d;strokeColor=#996185;fontSize=14;fontStyle=1" vertex="1" parent="1">
<mxGeometry x="1500" y="1180" width="120" height="40" as="geometry"/>
</mxCell>
<mxCell id="48" value="Gemeinwohl-Admin" style="ellipse;whiteSpace=wrap;html=1;fillColor=#e6d0de;gradientColor=#d5739d;strokeColor=#996185;fontSize=14;fontStyle=1" vertex="1" parent="1">
<mxGeometry x="1500" y="1220" width="120" height="40" as="geometry"/>
</mxCell>
<mxCell id="49" value="Contribution-Admin" style="ellipse;whiteSpace=wrap;html=1;fillColor=#e6d0de;gradientColor=#d5739d;strokeColor=#996185;fontSize=14;fontStyle=1" vertex="1" parent="1">
<mxGeometry x="1500" y="1260" width="120" height="40" as="geometry"/>
</mxCell>
<mxCell id="50" value="Federation-Admin" style="ellipse;whiteSpace=wrap;html=1;fillColor=#e6d0de;gradientColor=#d5739d;strokeColor=#996185;fontSize=14;fontStyle=1" vertex="1" parent="1">
<mxGeometry x="1500" y="1300" width="120" height="40" as="geometry"/>
</mxCell>
<mxCell id="51" value="Backup-Admin" style="ellipse;whiteSpace=wrap;html=1;fillColor=#e6d0de;gradientColor=#d5739d;strokeColor=#996185;fontSize=14;fontStyle=1" vertex="1" parent="1">
<mxGeometry x="1500" y="1340" width="120" height="40" as="geometry"/>
</mxCell>
<mxCell id="57" style="edgeStyle=none;html=1;fontSize=14;endArrow=none;endFill=0;" edge="1" parent="1" source="52" target="42">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="52" value="Helpdesk" style="ellipse;whiteSpace=wrap;html=1;fillColor=#e6d0de;gradientColor=#d5739d;strokeColor=#996185;fontSize=14;fontStyle=1" vertex="1" parent="1">
<mxGeometry x="1360" y="1180" width="120" height="40" as="geometry"/>
</mxCell>
<mxCell id="53" value="Transaction" style="ellipse;whiteSpace=wrap;html=1;fillColor=#e6d0de;gradientColor=#d5739d;strokeColor=#996185;fontSize=14;fontStyle=1" vertex="1" parent="1">
<mxGeometry x="1360" y="1220" width="120" height="40" as="geometry"/>
</mxCell>
<mxCell id="54" value="Contribution" style="ellipse;whiteSpace=wrap;html=1;fillColor=#e6d0de;gradientColor=#d5739d;strokeColor=#996185;fontSize=14;fontStyle=1" vertex="1" parent="1">
<mxGeometry x="1360" y="1260" width="120" height="40" as="geometry"/>
</mxCell>
<mxCell id="55" value="Community" style="ellipse;whiteSpace=wrap;html=1;fillColor=#e6d0de;gradientColor=#d5739d;strokeColor=#996185;fontSize=14;fontStyle=1" vertex="1" parent="1">
<mxGeometry x="1360" y="1300" width="120" height="40" as="geometry"/>
</mxCell>
<mxCell id="56" value="User" style="ellipse;whiteSpace=wrap;html=1;fillColor=#e6d0de;gradientColor=#d5739d;strokeColor=#996185;fontSize=14;fontStyle=1" vertex="1" parent="1">
<mxGeometry x="1360" y="1340" width="120" height="40" as="geometry"/>
</mxCell>
<mxCell id="64" style="edgeStyle=none;html=1;entryX=0;entryY=0.5;entryDx=0;entryDy=0;fontSize=14;endArrow=none;endFill=0;" edge="1" parent="1" source="58" target="6">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="58" value="anlegen" style="ellipse;whiteSpace=wrap;html=1;fillColor=#ffcd28;gradientColor=#ffa500;strokeColor=#d79b00;fontSize=14;fontStyle=1" vertex="1" parent="1">
<mxGeometry x="380" y="830" width="120" height="40" as="geometry"/>
</mxCell>
<mxCell id="59" value="löschen" style="ellipse;whiteSpace=wrap;html=1;fillColor=#ffcd28;gradientColor=#ffa500;strokeColor=#d79b00;fontSize=14;fontStyle=1" vertex="1" parent="1">
<mxGeometry x="380" y="870" width="120" height="40" as="geometry"/>
</mxCell>
<mxCell id="60" value="bearbeiten" style="ellipse;whiteSpace=wrap;html=1;fillColor=#ffcd28;gradientColor=#ffa500;strokeColor=#d79b00;fontSize=14;fontStyle=1" vertex="1" parent="1">
<mxGeometry x="380" y="910" width="120" height="40" as="geometry"/>
</mxCell>
<mxCell id="61" value="kategorisieren" style="ellipse;whiteSpace=wrap;html=1;fillColor=#ffcd28;gradientColor=#ffa500;strokeColor=#d79b00;fontSize=14;fontStyle=1" vertex="1" parent="1">
<mxGeometry x="380" y="950" width="120" height="40" as="geometry"/>
</mxCell>
<mxCell id="62" value="auswerten" style="ellipse;whiteSpace=wrap;html=1;fillColor=#ffcd28;gradientColor=#ffa500;strokeColor=#d79b00;fontSize=14;fontStyle=1" vertex="1" parent="1">
<mxGeometry x="380" y="990" width="120" height="40" as="geometry"/>
</mxCell>
<mxCell id="63" value="bestätigen" style="ellipse;whiteSpace=wrap;html=1;fillColor=#ffcd28;gradientColor=#ffa500;strokeColor=#d79b00;fontSize=14;fontStyle=1" vertex="1" parent="1">
<mxGeometry x="380" y="1030" width="120" height="40" as="geometry"/>
</mxCell>
<mxCell id="72" style="edgeStyle=none;html=1;entryX=0;entryY=0.5;entryDx=0;entryDy=0;fontSize=14;endArrow=none;endFill=0;" edge="1" parent="1" source="65" target="66">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="65" value="Verwaltung" style="ellipse;whiteSpace=wrap;html=1;fillColor=#d5e8d4;gradientColor=#97d077;strokeColor=#82b366;fontSize=14;fontStyle=1" vertex="1" parent="1">
<mxGeometry x="460" y="310" width="120" height="40" as="geometry"/>
</mxCell>
<mxCell id="66" value="Community intern" style="ellipse;whiteSpace=wrap;html=1;fillColor=#d5e8d4;gradientColor=#97d077;strokeColor=#82b366;fontSize=14;fontStyle=1" vertex="1" parent="1">
<mxGeometry x="280" y="310" width="120" height="40" as="geometry"/>
</mxCell>
<mxCell id="67" value="Community extern" style="ellipse;whiteSpace=wrap;html=1;fillColor=#d5e8d4;gradientColor=#97d077;strokeColor=#82b366;fontSize=14;fontStyle=1" vertex="1" parent="1">
<mxGeometry x="280" y="350" width="120" height="40" as="geometry"/>
</mxCell>
<mxCell id="68" value="User" style="ellipse;whiteSpace=wrap;html=1;fillColor=#d5e8d4;gradientColor=#97d077;strokeColor=#82b366;fontSize=14;fontStyle=1" vertex="1" parent="1">
<mxGeometry x="280" y="390" width="120" height="40" as="geometry"/>
</mxCell>
<mxCell id="69" value="Contribution" style="ellipse;whiteSpace=wrap;html=1;fillColor=#d5e8d4;gradientColor=#97d077;strokeColor=#82b366;fontSize=14;fontStyle=1" vertex="1" parent="1">
<mxGeometry x="280" y="430" width="120" height="40" as="geometry"/>
</mxCell>
<mxCell id="70" value="Rollen&amp;amp;Rechte" style="ellipse;whiteSpace=wrap;html=1;fillColor=#d5e8d4;gradientColor=#97d077;strokeColor=#82b366;fontSize=14;fontStyle=1" vertex="1" parent="1">
<mxGeometry x="280" y="470" width="120" height="40" as="geometry"/>
</mxCell>
<mxCell id="71" value="Konten" style="ellipse;whiteSpace=wrap;html=1;fillColor=#d5e8d4;gradientColor=#97d077;strokeColor=#82b366;fontSize=14;fontStyle=1" vertex="1" parent="1">
<mxGeometry x="280" y="510" width="120" height="40" as="geometry"/>
</mxCell>
<mxCell id="73" value="Backup-Provider" style="ellipse;whiteSpace=wrap;html=1;fillColor=#d5e8d4;gradientColor=#97d077;strokeColor=#82b366;fontSize=14;fontStyle=1" vertex="1" parent="1">
<mxGeometry x="460" y="350" width="120" height="40" as="geometry"/>
</mxCell>
<mxCell id="78" style="edgeStyle=none;html=1;entryX=0.5;entryY=1;entryDx=0;entryDy=0;fontSize=14;endArrow=none;endFill=0;" edge="1" parent="1" source="74" target="73">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="74" value="Community&lt;br&gt;intern" style="ellipse;whiteSpace=wrap;html=1;fillColor=#d5e8d4;gradientColor=#97d077;strokeColor=#82b366;fontSize=14;fontStyle=1" vertex="1" parent="1">
<mxGeometry x="520" y="460" width="120" height="40" as="geometry"/>
</mxCell>
<mxCell id="75" value="User" style="ellipse;whiteSpace=wrap;html=1;fillColor=#d5e8d4;gradientColor=#97d077;strokeColor=#82b366;fontSize=14;fontStyle=1" vertex="1" parent="1">
<mxGeometry x="520" y="500" width="120" height="40" as="geometry"/>
</mxCell>
<mxCell id="76" value="Contribution" style="ellipse;whiteSpace=wrap;html=1;fillColor=#d5e8d4;gradientColor=#97d077;strokeColor=#82b366;fontSize=14;fontStyle=1" vertex="1" parent="1">
<mxGeometry x="520" y="540" width="120" height="40" as="geometry"/>
</mxCell>
<mxCell id="77" value="Konto" style="ellipse;whiteSpace=wrap;html=1;fillColor=#d5e8d4;gradientColor=#97d077;strokeColor=#82b366;fontSize=14;fontStyle=1" vertex="1" parent="1">
<mxGeometry x="520" y="580" width="120" height="40" as="geometry"/>
</mxCell>
<mxCell id="82" style="edgeStyle=none;html=1;entryX=1;entryY=0.5;entryDx=0;entryDy=0;fontSize=14;endArrow=none;endFill=0;" edge="1" parent="1" source="79" target="33">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="79" value="online" style="ellipse;whiteSpace=wrap;html=1;fillColor=#dae8fc;gradientColor=#7ea6e0;strokeColor=#6c8ebf;fontSize=14;fontStyle=1" vertex="1" parent="1">
<mxGeometry x="1920" y="380" width="120" height="40" as="geometry"/>
</mxCell>
<mxCell id="80" value="per Link" style="ellipse;whiteSpace=wrap;html=1;fillColor=#dae8fc;gradientColor=#7ea6e0;strokeColor=#6c8ebf;fontSize=14;fontStyle=1" vertex="1" parent="1">
<mxGeometry x="1920" y="420" width="120" height="40" as="geometry"/>
</mxCell>
<mxCell id="81" value="per QR-Code" style="ellipse;whiteSpace=wrap;html=1;fillColor=#dae8fc;gradientColor=#7ea6e0;strokeColor=#6c8ebf;fontSize=14;fontStyle=1" vertex="1" parent="1">
<mxGeometry x="1920" y="460" width="120" height="40" as="geometry"/>
</mxCell>
<mxCell id="83" value="online" style="ellipse;whiteSpace=wrap;html=1;fillColor=#dae8fc;gradientColor=#7ea6e0;strokeColor=#6c8ebf;fontSize=14;fontStyle=1" vertex="1" parent="1">
<mxGeometry x="1750" y="490" width="120" height="40" as="geometry"/>
</mxCell>
<mxCell id="84" value="redeem" style="ellipse;whiteSpace=wrap;html=1;fillColor=#dae8fc;gradientColor=#7ea6e0;strokeColor=#6c8ebf;fontSize=14;fontStyle=1" vertex="1" parent="1">
<mxGeometry x="1750" y="530" width="120" height="40" as="geometry"/>
</mxCell>
<mxCell id="86" value="Auswertung&lt;span style=&quot;color: rgba(0 , 0 , 0 , 0) ; font-family: monospace ; font-size: 0px ; font-weight: 400&quot;&gt;%3CmxGraphModel%3E%3Croot%3E%3CmxCell%20id%3D%220%22%2F%3E%3CmxCell%20id%3D%221%22%20parent%3D%220%22%2F%3E%3CmxCell%20id%3D%222%22%20value%3D%22Verwaltung%22%20style%3D%22ellipse%3BwhiteSpace%3Dwrap%3Bhtml%3D1%3BfillColor%3D%23d5e8d4%3BgradientColor%3D%2397d077%3BstrokeColor%3D%2382b366%3BfontSize%3D14%3BfontStyle%3D1%22%20vertex%3D%221%22%20parent%3D%221%22%3E%3CmxGeometry%20x%3D%221000%22%20y%3D%22300%22%20width%3D%22120%22%20height%3D%2240%22%20as%3D%22geometry%22%2F%3E%3C%2FmxCell%3E%3C%2Froot%3E%3C%2FmxGraphModel%3E&lt;/span&gt;" style="ellipse;whiteSpace=wrap;html=1;fillColor=#d5e8d4;gradientColor=#97d077;strokeColor=#82b366;fontSize=14;fontStyle=1" vertex="1" parent="1">
<mxGeometry x="460" y="390" width="120" height="40" as="geometry"/>
</mxCell>
<mxCell id="88" value="Community&lt;br&gt;extern" style="ellipse;whiteSpace=wrap;html=1;fillColor=#d5e8d4;gradientColor=#97d077;strokeColor=#82b366;fontSize=14;fontStyle=1" vertex="1" parent="1">
<mxGeometry x="520" y="500" width="120" height="40" as="geometry"/>
</mxCell>
<mxCell id="89" value="User" style="ellipse;whiteSpace=wrap;html=1;fillColor=#d5e8d4;gradientColor=#97d077;strokeColor=#82b366;fontSize=14;fontStyle=1" vertex="1" parent="1">
<mxGeometry x="520" y="540" width="120" height="40" as="geometry"/>
</mxCell>
<mxCell id="90" value="Contribution" style="ellipse;whiteSpace=wrap;html=1;fillColor=#d5e8d4;gradientColor=#97d077;strokeColor=#82b366;fontSize=14;fontStyle=1" vertex="1" parent="1">
<mxGeometry x="520" y="580" width="120" height="40" as="geometry"/>
</mxCell>
<mxCell id="91" value="Konto" style="ellipse;whiteSpace=wrap;html=1;fillColor=#d5e8d4;gradientColor=#97d077;strokeColor=#82b366;fontSize=14;fontStyle=1" vertex="1" parent="1">
<mxGeometry x="520" y="620" width="120" height="40" as="geometry"/>
</mxCell>
<mxCell id="92" value="Blockchain" style="ellipse;whiteSpace=wrap;html=1;fillColor=#f5f5f5;gradientColor=#b3b3b3;strokeColor=#666666;fontSize=14;fontStyle=1" vertex="1" parent="1">
<mxGeometry x="1560" y="140" width="120" height="80" as="geometry"/>
</mxCell>
<mxCell id="93" value="senden" style="ellipse;whiteSpace=wrap;html=1;fillColor=#f5f5f5;gradientColor=#b3b3b3;strokeColor=#666666;fontSize=14;fontStyle=1" vertex="1" parent="1">
<mxGeometry x="1760" y="200" width="120" height="40" as="geometry"/>
</mxCell>
<mxCell id="94" value="empfangen" style="ellipse;whiteSpace=wrap;html=1;fillColor=#f5f5f5;gradientColor=#b3b3b3;strokeColor=#666666;fontSize=14;fontStyle=1" vertex="1" parent="1">
<mxGeometry x="1760" y="240" width="120" height="40" as="geometry"/>
</mxCell>
<mxCell id="97" style="edgeStyle=none;html=1;fontSize=14;endArrow=none;endFill=0;" edge="1" parent="1" source="95" target="92">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="95" value="schöpfen" style="ellipse;whiteSpace=wrap;html=1;fillColor=#f5f5f5;gradientColor=#b3b3b3;strokeColor=#666666;fontSize=14;fontStyle=1" vertex="1" parent="1">
<mxGeometry x="1760" y="160" width="120" height="40" as="geometry"/>
</mxCell>
<mxCell id="98" value="G R A D I D O" style="ellipse;whiteSpace=wrap;html=1;fillColor=#ffcd28;gradientColor=#ffa500;strokeColor=#BD8800;fontSize=34;fontStyle=1;gradientDirection=radial;" vertex="1" parent="1">
<mxGeometry x="880" y="480" width="320" height="320" as="geometry"/>
</mxCell>
<mxCell id="105" style="edgeStyle=none;html=1;fontSize=34;endArrow=none;endFill=0;" edge="1" parent="1" source="104" target="25">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="104" value="Kassenbuch" style="ellipse;whiteSpace=wrap;html=1;fillColor=#dae8fc;gradientColor=#7ea6e0;strokeColor=#6c8ebf;fontSize=14;fontStyle=1" vertex="1" parent="1">
<mxGeometry x="1520" y="690" width="120" height="40" as="geometry"/>
</mxCell>
<mxCell id="107" style="edgeStyle=none;html=1;fontSize=34;endArrow=none;endFill=0;" edge="1" parent="1" source="106" target="92">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="106" value="Kassenbuch" style="ellipse;whiteSpace=wrap;html=1;fillColor=#f5f5f5;gradientColor=#b3b3b3;strokeColor=#666666;fontSize=14;fontStyle=1" vertex="1" parent="1">
<mxGeometry x="1420" y="230" width="120" height="40" as="geometry"/>
</mxCell>
</root>
</mxGraphModel>
</diagram>
</mxfile>

View File

@ -45,16 +45,15 @@ module.exports = {
extensions: ['.js', '.vue'], extensions: ['.js', '.vue'],
// TODO: remove ignores // TODO: remove ignores
ignores: [ ignores: [
'/site.thx./',
'/form./', '/form./',
'/time./', '/time./',
'/decay.types./', '/decay.types./',
'settings.password.resend_subtitle', 'settings.password.resend_subtitle',
'settings.password.reset',
'settings.password.reset-password.text', 'settings.password.reset-password.text',
'settings.password.set', 'settings.password.set',
'settings.password.set-password.text', 'settings.password.set-password.text',
'settings.password.subtitle', 'settings.password.subtitle',
'site.login.signin',
], ],
enableFix: false, enableFix: false,
}, },

View File

@ -4,5 +4,6 @@ module.exports = {
singleQuote: true, singleQuote: true,
trailingComma: "all", trailingComma: "all",
tabWidth: 2, tabWidth: 2,
bracketSpacing: true bracketSpacing: true,
endOfLine: "auto",
}; };

View File

@ -45,7 +45,6 @@
"jest": "^26.6.3", "jest": "^26.6.3",
"jest-canvas-mock": "^2.3.1", "jest-canvas-mock": "^2.3.1",
"jest-environment-jsdom-sixteen": "^2.0.0", "jest-environment-jsdom-sixteen": "^2.0.0",
"particles-bg-vue": "1.2.3",
"portal-vue": "^2.1.7", "portal-vue": "^2.1.7",
"prettier": "^2.2.1", "prettier": "^2.2.1",
"qrcanvas-vue": "2.1.1", "qrcanvas-vue": "2.1.1",
@ -100,5 +99,6 @@
"not ie <= 10" "not ie <= 10"
], ],
"author": "Gradido-Akademie - https://www.gradido.net/", "author": "Gradido-Akademie - https://www.gradido.net/",
"license": "Apache-2.0",
"description": "Gradido, the Natural Economy of Life, is a way to worldwide prosperity and peace in harmony with nature. - Gradido, die Natürliche Ökonomie des lebens, ist ein Weg zu weltweitem Wohlstand und Frieden in Harmonie mit der Natur." "description": "Gradido, the Natural Economy of Life, is a way to worldwide prosperity and peace in harmony with nature. - Gradido, die Natürliche Ökonomie des lebens, ist ein Weg zu weltweitem Wohlstand und Frieden in Harmonie mit der Natur."
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 157 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 156 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 180 KiB

61
frontend/src/App.spec.js Normal file
View File

@ -0,0 +1,61 @@
import { mount, RouterLinkStub } from '@vue/test-utils'
import App from './App'
const localVue = global.localVue
const mockStoreCommit = jest.fn()
const stubs = {
RouterLink: RouterLinkStub,
RouterView: true,
}
describe('App', () => {
const mocks = {
$i18n: {
locale: 'en',
},
$t: jest.fn((t) => t),
$store: {
commit: mockStoreCommit,
state: {
token: null,
},
},
$route: {
meta: {
requiresAuth: false,
},
},
}
let wrapper
const Wrapper = () => {
return mount(App, { localVue, mocks, stubs })
}
describe('mount', () => {
beforeEach(() => {
wrapper = Wrapper()
})
it('renders the App', () => {
expect(wrapper.find('#app').exists()).toBe(true)
})
it('has a component AuthLayout', () => {
expect(wrapper.findComponent({ name: 'AuthLayout' }).exists()).toBe(true)
})
describe('route requires authorization', () => {
beforeEach(() => {
mocks.$route.meta.requiresAuth = true
wrapper = Wrapper()
})
it('has a component DashboardLayout', () => {
expect(wrapper.findComponent({ name: 'DashboardLayout' }).exists()).toBe(true)
})
})
})
})

View File

@ -1,47 +1,49 @@
<template> <template>
<div id="app" class="font-sans text-gray-800"> <div id="app" class="h-100">
<div> <component :is="$route.meta.requiresAuth ? 'DashboardLayout' : 'AuthLayout'" />
<particles-bg v-if="$store.state.coinanimation" type="custom" :config="config" :bg="true" /> <div class="goldrand position-fixed w-100 fixed-bottom zindex1000"></div>
<component :is="$route.meta.requiresAuth ? 'DashboardLayout' : 'AuthLayoutGDD'" />
</div>
</div> </div>
</template> </template>
<script> <script>
import { ParticlesBg } from 'particles-bg-vue' import DashboardLayout from '@/layouts/DashboardLayout.vue'
import icon from './icon.js' import AuthLayout from '@/layouts/AuthLayout.vue'
import DashboardLayout from '@/layouts/DashboardLayout_gdd.vue'
import AuthLayoutGDD from '@/layouts/AuthLayout_gdd.vue'
export default { export default {
name: 'app', name: 'App',
components: { components: {
ParticlesBg,
DashboardLayout, DashboardLayout,
AuthLayoutGDD, AuthLayout,
},
data() {
return {
config: {
num: [1, 7],
rps: 15,
radius: [5, 50],
life: [6.5, 15],
v: [1, 1],
tha: [-40, 40],
body: icon,
alpha: [0.6, 0],
scale: [0.1, 0.4],
position: 'all',
cross: 'dead',
random: 2,
},
}
}, },
} }
</script> </script>
<style> <style>
.pointer { @font-face {
cursor: pointer; font-family: 'WorkSans', sans-serif !important;
src: url(./assets/scss/fonts/WorkSans-VariableFont_wght.ttf) format('truetype');
}
#app {
min-width: 360px;
font-size: 1rem;
font-family: 'WorkSans', sans-serif !important;
}
@media screen and (max-width: 500px) {
#app {
font-size: 0.85rem;
}
}
.goldrand {
background: linear-gradient(
90deg,
rgba(197, 141, 56, 1) 6%,
rgba(243, 205, 124, 1) 30%,
rgba(219, 176, 86, 1) 54%,
rgba(238, 192, 95, 1) 63%,
rgba(204, 157, 61, 1) 88%
);
height: 13px;
} }
</style> </style>

View File

@ -1,4 +1,4 @@
// Body // Body
$body-bg: #f8f9fe !default; $body-bg: #fff !default;
$body-color: $gray-700 !default; $body-color: $gray-700 !default;

View File

@ -16,7 +16,9 @@ $custom-control-indicator-active-bg: $component-active-bg !default;
$custom-control-indicator-active-border-color: $component-active-border-color !default; $custom-control-indicator-active-border-color: $component-active-border-color !default;
$custom-control-indicator-active-box-shadow: $custom-control-indicator-box-shadow !default; $custom-control-indicator-active-box-shadow: $custom-control-indicator-box-shadow !default;
$custom-control-indicator-checked-color: $component-active-color !default; $custom-control-indicator-checked-color: $component-active-color !default;
$custom-control-indicator-checked-bg: $component-active-bg !default;
// $custom-control-indicator-checked-bg: $component-active-bg !default;
$custom-control-indicator-checked-bg: #047006 !default;
$custom-control-indicator-checked-border-color: $component-active-border-color !default; $custom-control-indicator-checked-border-color: $component-active-border-color !default;
$custom-control-indicator-checked-box-shadow: $custom-control-indicator-box-shadow !default; $custom-control-indicator-checked-box-shadow: $custom-control-indicator-box-shadow !default;
@ -24,6 +26,8 @@ $custom-control-indicator-checked-box-shadow: $custom-control-indicator-box-shad
$custom-control-indicator-checked-disabled-bg: theme-color("primary") !default; $custom-control-indicator-checked-disabled-bg: theme-color("primary") !default;
$custom-control-indicator-disabled-bg: $gray-200 !default; $custom-control-indicator-disabled-bg: $gray-200 !default;
$custom-control-label-disabled-color: $gray-600 !default; $custom-control-label-disabled-color: $gray-600 !default;
$custom-checkbox-indicator-border-radius: $border-radius-sm !default;
// $custom-checkbox-indicator-border-radius: $border-radius-sm !default;
$custom-checkbox-indicator-border-radius: 50px !default;
// $custom-checkbox-indicator-icon-checked: str-replace(url("data:image/svg+xml !default;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%23fff' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3E%3C/svg%3E"), "#", "%23") !default; // $custom-checkbox-indicator-icon-checked: str-replace(url("data:image/svg+xml !default;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%23fff' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3E%3C/svg%3E"), "#", "%23") !default;

View File

@ -4,7 +4,7 @@ $grid-breakpoints: (
xs: 0, xs: 0,
sm: 576px, sm: 576px,
md: 768px, md: 768px,
lg: 992px, lg: 1025px,
xl: 1200px xl: 1200px
); );

View File

@ -1,4 +1,13 @@
// Body // Sections
$body-bg: #f8f9fe !default; // $section-colors: () !default;
$body-color: $gray-700 !default; // $section-colors: map-merge(
// (
// "primary": $body-bg,
// "secondary": $secondary,
// "light": $gray-400,
// "dark": $dark,
// "darker": $darker
// ),
// $section-colors
// );

View File

@ -0,0 +1,217 @@
html,
body {
height: 100%;
}
.pointer {
cursor: pointer;
}
.c-grey {
color: #383838 !important;
}
.c-blau {
color: #0e79bc !important;
}
/* Navbar */
a,
.navbar-light,
.navbar-nav,
.nav-link {
color: #047006;
}
a:hover,
.nav-link:hover {
color: #383838 !important;
}
.navbar-light .navbar-nav .nav-link.active {
color: rgb(35 121 188 / 90%);
}
.text-gradido {
color: rgb(249 205 105 / 100%);
}
.gradient-gradido {
background-image: linear-gradient(146deg, rgb(220 167 44) 50%, rgb(197 141 56 / 100%) 100%);
}
/* Button */
.btn {
border-radius: 25px;
}
.btn-gradido {
display: inline-block;
padding: 0.6em 3em;
letter-spacing: 0.05em;
color: #fff;
transition: all 0.5s ease;
background: rgb(249 205 105);
background: linear-gradient(135deg, rgb(249 205 105 / 100%) 2%, rgb(197 141 56 / 100%) 55%);
box-shadow: rgb(0 0 0 / 40%) 0 30px 90px;
border-radius: 26px;
padding-right: 50px;
padding-left: 50px;
border-style: none;
}
.btn-gradido:hover {
color: #fff;
box-shadow: 0 5px 10px rgb(0 0 0 / 20%);
}
.btn-gradido:focus {
outline: none;
}
.btn-gradido-disable {
padding: 0.6em 3em;
letter-spacing: 0.05em;
color: #fff;
transition: all 0.5s ease;
background: rgb(97 97 97);
background: linear-gradient(135deg, rgb(180 180 180 / 100%) 46%, rgb(180 180 180 / 100%) 99%);
box-shadow: rgb(0 0 0 / 40%) 0 30px 90px;
border-radius: 26px;
padding-right: 50px;
padding-left: 50px;
border-style: none;
}
.btn-gradido-disable:hover {
color: #fff;
}
.btn-outline-gradido {
color: rgb(140 121 88);
border: 1px solid #f5b805;
box-shadow: 10px 10px 50px 10px rgb(56 56 56 / 31%);
}
.btn-outline-gradido:hover {
box-shadow: 10px 10px 50px 10px rgb(56 56 56 / 0%);
}
.form-control,
.custom-select {
border-radius: 17px;
height: 50px;
}
.rounded-right {
border-top-right-radius: 17px !important;
border-bottom-right-radius: 17px !important;
}
.alert-success {
background-color: #d4edda;
border-color: #c3e6cb;
color: #155724;
}
.alert-danger {
color: #721c24;
background-color: #f8d7da;
border-color: #f5c6cb;
}
.b-toast-danger .toast .toast-header {
color: #721c24;
background-color: rgb(248 215 218 / 85%);
border-bottom-color: rgb(245 198 203 / 85%);
}
.b-toast-danger .toast .toast-body {
background-color: rgb(252 237 238 / 85%);
border-color: rgb(245 198 203 / 85%);
color: #721c24;
}
.b-toast-success .toast .toast-header {
color: #155724;
background-color: rgb(212 237 218 / 58%);
border-bottom-color: rgb(195 230 203 / 85%);
}
.b-toast-success .toast .toast-body {
color: #155724;
background-color: rgb(212 237 218 / 85%);
border-bottom-color: rgb(195 230 203 / 85%);
}
// .btn-primary pim {
.btn-primary {
background-color: #5a7b02;
border-color: #5e72e4;
}
.gradido-font-large {
font-size: large;
height: auto !important;
}
.font2em {
font-size: 1.5em;
}
.zindex10 {
z-index: 10;
}
.zindex100 {
z-index: 100;
}
.zindex1000 {
z-index: 1000;
}
.zindex10000 {
z-index: 10000;
}
.zindex100000 {
z-index: 100000;
}
.gradido-global-color-blue {
color: #0e79bc;
}
.gradido-global-color-accent {
color: #047006;
}
.gradido-global-color-gray {
color: #858383;
}
.gradido-custom-background {
background-color: #ebebeba3 !important;
border-radius: 25pt;
}
.gradido-width-300 {
width: 300px;
}
.gradido-width-96 {
width: 96%;
}
.gradido-no-border-radius {
border-radius: 0;
}
.gradido-no-border {
border: 0;
}
.gradido-font-15rem {
font-size: 1.5rem;
}

View File

@ -51,165 +51,4 @@
// Bootstrap-vue (2.21.1) scss // Bootstrap-vue (2.21.1) scss
@import "~bootstrap-vue/src/index"; @import "~bootstrap-vue/src/index";
@import "gradido-template";
.alert-success {
color: #155724;
background-color: #d4edda;
border-color: #c3e6cb;
}
.alert-danger {
color: #721c24;
background-color: #f8d7da;
border-color: #f5c6cb;
}
.b-toast-danger .toast .toast-header {
color: #721c24;
background-color: rgb(248 215 218 / 85%);
border-bottom-color: rgb(245 198 203 / 85%);
}
.b-toast-danger .toast .toast-body {
background-color: rgb(252 237 238 / 85%);
border-color: rgb(245 198 203 / 85%);
color: #721c24;
}
.b-toast-success .toast .toast-header {
color: #155724;
background-color: rgb(212 237 218 / 58%);
border-bottom-color: rgb(195 230 203 / 85%);
}
.b-toast-success .toast .toast-body {
color: #155724;
background-color: rgb(212 237 218 / 85%);
border-bottom-color: rgb(195 230 203 / 85%);
}
// .btn-primary pim {
.btn-primary {
background-color: #5a7b02;
border-color: #5e72e4;
}
.gradido-font-large {
font-size: large;
height: auto !important;
}
a,
.copyright {
color: #5a7b02;
}
.font12em {
font-size: 1.2em;
}
.font2em {
font-size: 1.5em;
}
.gradido-global-color-text {
color: #3d443b;
}
.gradido-global-color-accent {
color: #047006;
}
.gradido-global-color-6e0a9c9e {
color: #000;
}
.gradido-global-color-2d0fb154 {
color: #047006;
}
.gradido-global-color-16efe88c {
color: #7ebc55;
}
.gradido-global-color-1939326 {
color: #f6fff6;
}
.gradido-global-color-9d79fc1 {
color: #047006;
}
.gradido-global-color-6347f4d {
color: #5a7b02;
}
.gradido-global-color-4fbc19a {
color: #014034;
}
.gradido-global-color-d341874 {
color: #b6d939;
}
.gradido-global-color-619d338 {
color: #8ebfb1;
}
.gradido-global-color-44819a9 {
color: #026873;
}
.gradido-global-color-gray {
color: #858383;
}
.gradido-custom-background {
background-color: #ebebeba3 !important;
}
.gradido-shadow-inset {
box-shadow: inset 0.3em rgba(241 187 187 / 100%);
}
.gradido-max-width {
width: 100%;
}
.gradido-width-300 {
width: 300px;
}
.gradido-absolute {
position: absolute;
}
.gradido-width-95-absolute {
width: 95%;
position: absolute;
}
.gradido-width-96-absolute {
width: 96%;
position: absolute;
}
.gradido-no-border-radius {
border-radius: 0;
}
.gradido-no-border {
border: 0;
}
.gradido-background-f1 {
background-color: #f1f1f1;
}
.gradido-background-white {
background-color: #fff;
}
.gradido-font-15rem {
font-size: 1.5rem;
}

View File

@ -0,0 +1,33 @@
<template>
<div>
<b-carousel :interval="13000">
<b-carousel-slide img-src="/img/template/Foto_01_2400_small.jpg"></b-carousel-slide>
<b-carousel-slide img-src="/img/template/Foto_02_2400_small.jpg"></b-carousel-slide>
<b-carousel-slide img-src="/img/template/Foto_03_2400_small.jpg"></b-carousel-slide>
</b-carousel>
</div>
</template>
<script>
export default {
name: 'AuthCarousel',
}
</script>
<style>
.carousel {
position: relative;
height: 110%;
top: -16px;
}
.carousel-inner {
height: 100%;
border-radius: 0% 49% 49% 0% / 0% 51% 49% 0%;
-webkit-border-radius: 0% 49% 49% 0% / 0% 51% 49% 0%;
backface-visibility: hidden;
-webkit-backface-visibility: hidden;
transform: translate3d(0, 0, 0);
-webkit-transform: translate3d(0, 0, 0);
}
</style>

View File

@ -0,0 +1,61 @@
<template>
<footer class="footer">
<b-row class="mt-lg-7 mt-md-6 mt-4">
<b-col class="col-12 col-md-12 col-lg-6">
<div
class="d-flex justify-content-center justify-content-md-center justify-content-lg-start ml-3"
>
<b-nav class="nav-footer">
<b-nav-item :href="`https://gradido.net/${$i18n.locale}/impressum/`" target="_blank">
{{ $t('footer.imprint') }}
</b-nav-item>
<b-nav-item :href="`https://gradido.net/${$i18n.locale}/datenschutz/`" target="_blank">
{{ $t('footer.privacy_policy') }}
</b-nav-item>
</b-nav>
</div>
</b-col>
<b-col class="col-12 col-md-12 col-lg-6 mt-4 mb-4 mt-lg-0 mb-lg-0">
<div class="text-center ml-3 ml-lg-0 text-lg-right pt-1">
{{ $t('followUs') }}
<b-link href="https://www.facebook.com/groups/Gradido/" target="_blank">
<b-icon-facebook class="ml-3 mr-3 c-grey" font-scale="1"></b-icon-facebook>
</b-link>
<b-link href="https://twitter.com/gradido" target="_blank">
<b-icon-twitter class="mr-3 c-grey" font-scale="1"></b-icon-twitter>
</b-link>
<b-link href="https://www.youtube.com/c/GradidoNet" target="_blank">
<b-icon-youtube class="mr-3 c-grey" font-scale="1"></b-icon-youtube>
</b-link>
<b-link href="https://t.me/Gradido" target="_blank">
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
fill="currentColor"
class="bi bi-telegram c-grey"
viewBox="0 0 16 16"
>
<path
d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0zM8.287 5.906c-.778.324-2.334.994-4.666 2.01-.378.15-.577.298-.595.442-.03.243.275.339.69.47l.175.055c.408.133.958.288 1.243.294.26.006.549-.1.868-.32 2.179-1.471 3.304-2.214 3.374-2.23.05-.012.12-.026.166.016.047.041.042.12.037.141-.03.129-1.227 1.241-1.846 1.817-.193.18-.33.307-.358.336a8.154 8.154 0 0 1-.188.186c-.38.366-.664.64.015 1.088.327.216.589.393.85.571.284.194.568.387.936.629.093.06.183.125.27.187.331.236.63.448.997.414.214-.02.435-.22.547-.82.265-1.417.786-4.486.906-5.751a1.426 1.426 0 0 0-.013-.315.337.337 0 0 0-.114-.217.526.526 0 0 0-.31-.093c-.3.005-.763.166-2.984 1.09z"
/>
</svg>
</b-link>
</div>
</b-col>
</b-row>
</footer>
</template>
<script>
export default {
name: 'AuthFooter',
}
</script>
<style>
.bi-telegram {
margin-top: -5px;
}
</style>

View File

@ -0,0 +1,127 @@
<template>
<div
class="mobil-start-box position-fixed h-100 d-inline d-sm-inline d-md-inline d-lg-none zindex1000"
>
<div class="position-absolute h1 text-white zindex1000 w-100 text-center mt-8">
{{ $t('auth.left.gratitude') }}
</div>
<div class="position-absolute h2 text-white zindex1000 w-100 text-center mt-9">
{{ $t('auth.left.newCurrency') }}
</div>
<img
src="/img/template/Blaetter.png"
class="sheet-img position-absolute d-block d-lg-none zindex1000"
/>
<b-img
id="img0"
class="position-absolute zindex1000"
src="/img/template/logo-header.png"
alt="start background image"
></b-img>
<b-img
fluid
id="img1"
class="position-absolute h-100 w-100 overflow-hidden zindex100"
src="/img/template/gold_03.png"
alt="start background image"
></b-img>
<b-img
id="img2"
class="position-absolute zindex100"
src="/img/template/gradido_background_header.png"
alt="start background image"
></b-img>
<b-img
id="img3"
class="position-relative zindex10"
src="/img/template/Foto_01.jpg"
alt="start background image"
></b-img>
<div class="mobil-start-box-text position-fixed w-100 text-center zindex1000">
<b-button variant="gradido" to="/register" @click="$emit('set-mobile-start', false)">
{{ $t('signup') }}
</b-button>
<div class="mt-3 h3 text-white">
{{ $t('auth.left.hasAccount') }}
<b-link
to="/login"
class="text-gradido gradido-global-color-blue"
@click="$emit('set-mobile-start', false)"
>
{{ $t('auth.left.hereLogin') }}
</b-link>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'AuthMobileStart',
props: {
mobileStart: { type: Boolean, default: false },
},
}
</script>
<style>
.mobil-start-box-text {
bottom: 65px;
}
/* logo */
.mobil-start-box #img0 {
width: 200px;
}
/* background logo */
.mobil-start-box #img2 {
width: 230px;
}
/* background maske */
@media screen and (max-width: 1024px) {
.mobil-start-box #img3 {
width: 100%;
top: -100px;
}
}
@media screen and (max-width: 991px) {
.mobil-start-box #img3 {
width: 100%;
top: -148px;
}
}
@media screen and (max-height: 740px) {
.mobil-start-box #img3 {
width: 115%;
}
}
@media screen and (max-width: 650px) {
.mobil-start-box #img3 {
width: 115%;
top: 66px;
}
}
@media screen and (max-width: 450px) {
.mobil-start-box #img3 {
width: 160%;
left: -71px;
top: 35px;
min-width: 360px;
}
}
@media screen and (max-width: 310px) {
.mobil-start-box #img3 {
width: 145%;
left: -94px;
top: 24px;
min-width: 360px;
}
}
</style>

View File

@ -0,0 +1,73 @@
<template>
<div class="auth-header position-sticky">
<b-navbar toggleable="lg" class="pr-4">
<b-navbar-brand>
<b-img
class="imgLogo position-absolute ml--3 mt--3 p-2 zindex1000"
:src="logo"
width="200"
alt="..."
/>
<b-img
class="imgLogoBack mt--3 ml--3"
src="/img/template/gradido_background_header.png"
width="230"
alt="start background image"
></b-img>
</b-navbar-brand>
<b-img class="sheet-img position-absolute d-block d-lg-none zindex1000" :src="sheet"></b-img>
<b-navbar-toggle target="nav-collapse" class="zindex1000"></b-navbar-toggle>
<b-collapse id="nav-collapse" is-nav class="mt-5 mt-lg-0">
<b-navbar-nav class="ml-auto" right>
<b-nav-item href="https://gradido.net/de/" target="_blank">
{{ $t('auth.navbar.aboutGradido') }}
</b-nav-item>
<b-nav-item to="/register" class="authNavbar ml-lg-5">{{ $t('signup') }}</b-nav-item>
<span class="d-none d-lg-block mt-1">{{ $t('math.pipe') }}</span>
<b-nav-item to="/login" class="authNavbar">{{ $t('signin') }}</b-nav-item>
</b-navbar-nav>
</b-collapse>
</b-navbar>
</div>
</template>
<script>
export default {
name: 'AuthNavbar',
data() {
return {
logo: '/img/brand/green.png',
sheet: '/img/template/Blaetter.png',
}
},
}
</script>
<style lang="scss">
.authNavbar > .nav-link {
color: #383838 !important;
}
.authNavbar > .router-link-exact-active {
color: #0e79bc !important;
}
.auth-header {
font-family: 'Open Sans', sans-serif !important;
}
.sheet-img {
top: -11px;
right: 7%;
max-width: 64%;
}
@media screen and (max-width: 450px) {
.sheet-img {
top: -15px;
right: 0%;
max-width: 61%;
}
}
</style>

View File

@ -0,0 +1,17 @@
<template>
<div class="navbar-small">
<b-navbar>
<b-navbar-nav>
<b-nav-item to="/register" class="authNavbar">{{ $t('signup') }}</b-nav-item>
<span class="mt-1">{{ $t('math.pipe') }}</span>
<b-nav-item to="/login" class="authNavbar">{{ $t('signin') }}</b-nav-item>
</b-navbar-nav>
</b-navbar>
</div>
</template>
<script>
export default {
name: 'AuthNavbarSmall',
}
</script>

View File

@ -1,7 +1,7 @@
<template> <template>
<div class="collapse-links-list"> <div class="collapse-links-list">
<div class="d-flex"> <div class="d-flex">
<div class="gradido-max-width"> <div class="w-100">
<hr /> <hr />
<div> <div>
<transaction-link <transaction-link

View File

@ -2,7 +2,7 @@ import { mount } from '@vue/test-utils'
import TransactionForm from './TransactionForm' import TransactionForm from './TransactionForm'
import flushPromises from 'flush-promises' import flushPromises from 'flush-promises'
import { SEND_TYPES } from '@/pages/Send.vue' import { SEND_TYPES } from '@/pages/Send.vue'
import DashboardLayout from '@/layouts/DashboardLayout_gdd.vue' import DashboardLayout from '@/layouts/DashboardLayout.vue'
const localVue = global.localVue const localVue = global.localVue

View File

@ -0,0 +1,75 @@
<template>
<div class="language-switch">
<span
v-for="(lang, index) in locales"
@click.prevent="saveLocale(lang.code)"
:key="lang.code"
class="pointer pr-3"
:class="$store.state.language === lang.code ? 'c-blau' : 'c-grey'"
>
{{ lang.name }}
<span class="ml-3">{{ locales.length - 1 > index ? $t('math.pipe') : '' }}</span>
</span>
</div>
</template>
<script>
import locales from '@/locales/'
import { updateUserInfos } from '@/graphql/mutations'
export default {
name: 'LanguageSwitch',
data() {
return {
locales: locales,
currentLanguage: {},
}
},
methods: {
setLocale(locale) {
this.$store.commit('language', locale)
this.currentLanguage = this.getLocaleObject(locale)
},
async saveLocale(locale) {
if (this.$i18n.locale === locale) return
this.setLocale(locale)
if (this.$store.state.email) {
this.$apollo
.mutate({
mutation: updateUserInfos,
variables: {
locale: locale,
},
})
.then(() => {
// toast success message
})
.catch(() => {
// toast error message
})
}
},
getLocaleObject(code) {
return this.locales.find((l) => l.code === code)
},
getNavigatorLanguage() {
const lang = navigator.language
if (lang) return lang.split('-')[0]
return lang
},
setCurrentLanguage() {
let locale = this.$store.state.language || this.getNavigatorLanguage() || 'en'
let object = this.getLocaleObject(locale)
if (!object) {
locale = 'en'
object = this.getLocaleObject(locale)
}
this.setLocale(locale)
this.currentLanguage = object
},
},
created() {
this.setCurrentLanguage()
},
}
</script>

View File

@ -1,5 +1,5 @@
<template> <template>
<div class="component-navbar gradido-background-white"> <div class="component-navbar">
<b-navbar toggleable="lg" type="light" variant="faded"> <b-navbar toggleable="lg" type="light" variant="faded">
<div class="navbar-brand"> <div class="navbar-brand">
<b-navbar-nav @click="$emit('set-visible', false)"> <b-navbar-nav @click="$emit('set-visible', false)">

View File

@ -4,8 +4,8 @@ import Message from './Message'
const localVue = global.localVue const localVue = global.localVue
const propsData = { const propsData = {
headline: 'site.thx.title', headline: 'Headline text',
subtitle: 'site.thx.email', subtitle: 'Subtitle text',
buttonText: 'login', buttonText: 'login',
linkTo: '/login', linkTo: '/login',
} }
@ -32,8 +32,8 @@ describe('Message', () => {
describe('with button', () => { describe('with button', () => {
it('renders title, subtitle, and button text', () => { it('renders title, subtitle, and button text', () => {
expect(wrapper.find('.test-message-headline').text()).toBe('site.thx.title') expect(wrapper.find('.test-message-headline').text()).toBe('Headline text')
expect(wrapper.find('.test-message-subtitle').text()).toBe('site.thx.email') expect(wrapper.find('.test-message-subtitle').text()).toBe('Subtitle text')
expect(wrapper.find('.test-message-button').text()).toBe('login') expect(wrapper.find('.test-message-button').text()).toBe('login')
}) })
@ -51,8 +51,8 @@ describe('Message', () => {
}) })
it('renders title, subtitle, and button text', () => { it('renders title, subtitle, and button text', () => {
expect(wrapper.find('.test-message-headline').text()).toBe('site.thx.title') expect(wrapper.find('.test-message-headline').text()).toBe('Headline text')
expect(wrapper.find('.test-message-subtitle').text()).toBe('site.thx.email') expect(wrapper.find('.test-message-subtitle').text()).toBe('Subtitle text')
}) })
it('button is not shown', () => { it('button is not shown', () => {

View File

@ -3,12 +3,12 @@
<div class="list-group"> <div class="list-group">
<div class="list-group-item gdt-transaction-list-item" v-b-toggle="collapseId"> <div class="list-group-item gdt-transaction-list-item" v-b-toggle="collapseId">
<!-- icon --> <!-- icon -->
<div class="text-right gradido-absolute"> <div class="text-right position-absolute">
<b-icon :icon="getLinesByType.icon" :class="getLinesByType.iconclasses"></b-icon> <b-icon :icon="getLinesByType.icon" :class="getLinesByType.iconclasses"></b-icon>
</div> </div>
<!-- collaps Button --> <!-- collaps Button -->
<div class="text-right gradido-width-96-absolute"> <div class="text-right gradido-width-96 position-absolute">
<b-icon <b-icon
:icon="getCollapseState(id) ? 'caret-up-square' : 'caret-down-square'" :icon="getCollapseState(id) ? 'caret-up-square' : 'caret-down-square'"
:class="getCollapseState(id) ? 'text-black' : 'text-muted'" :class="getCollapseState(id) ? 'text-black' : 'text-muted'"

View File

@ -1,127 +0,0 @@
import { mount } from '@vue/test-utils'
import UserCoinAnimation from './UserCoinAnimation'
import { updateUserInfos } from '@/graphql/mutations'
import { toastErrorSpy, toastSuccessSpy } from '@test/testSetup'
const localVue = global.localVue
const mockAPIcall = jest.fn()
const storeCommitMock = jest.fn()
describe('UserCard_CoinAnimation', () => {
let wrapper
const mocks = {
$t: jest.fn((t) => t),
$store: {
state: {
language: 'de',
coinanimation: true,
},
commit: storeCommitMock,
},
$apollo: {
mutate: mockAPIcall,
},
}
const Wrapper = () => {
return mount(UserCoinAnimation, { localVue, mocks })
}
describe('mount', () => {
beforeEach(() => {
jest.clearAllMocks()
wrapper = Wrapper()
})
it('renders the component', () => {
expect(wrapper.find('div#formusercoinanimation').exists()).toBeTruthy()
})
it('has an edit BFormCheckbox switch', () => {
expect(wrapper.find('.Test-BFormCheckbox').exists()).toBeTruthy()
})
describe('enable with success', () => {
beforeEach(async () => {
await wrapper.setData({ CoinAnimationStatus: false })
mockAPIcall.mockResolvedValue({
data: {
updateUserInfos: {
validValues: 1,
},
},
})
await wrapper.find('input').setChecked()
})
it('calls the updateUserInfos mutation', () => {
expect(mockAPIcall).toBeCalledWith({
mutation: updateUserInfos,
variables: {
coinanimation: true,
},
})
})
it('updates the store', () => {
expect(storeCommitMock).toBeCalledWith('coinanimation', true)
})
it('toasts a success message', () => {
expect(toastSuccessSpy).toBeCalledWith('settings.coinanimation.True')
})
})
describe('disable with success', () => {
beforeEach(async () => {
await wrapper.setData({ CoinAnimationStatus: true })
mockAPIcall.mockResolvedValue({
data: {
updateUserInfos: {
validValues: 1,
},
},
})
await wrapper.find('input').setChecked(false)
})
it('calls the subscribe mutation', () => {
expect(mockAPIcall).toBeCalledWith({
mutation: updateUserInfos,
variables: {
coinanimation: false,
},
})
})
it('updates the store', () => {
expect(storeCommitMock).toBeCalledWith('coinanimation', false)
})
it('toasts a success message', () => {
expect(toastSuccessSpy).toBeCalledWith('settings.coinanimation.False')
})
})
describe('disable with server error', () => {
beforeEach(() => {
mockAPIcall.mockRejectedValue({
message: 'Ouch',
})
wrapper.find('input').trigger('change')
})
it('resets the CoinAnimationStatus', () => {
expect(wrapper.vm.CoinAnimationStatus).toBeTruthy()
})
it('toasts an error message', () => {
expect(toastErrorSpy).toBeCalledWith('Ouch')
})
})
})
})

View File

@ -1,65 +0,0 @@
<template>
<b-card
id="formusercoinanimation"
class="bg-transparent gradido-custom-background gradido-no-border-radius"
>
<div>
<b-row class="mb-3">
<b-col class="mb-2 col-12">
<small>
<b>{{ $t('settings.coinanimation.coinanimation') }}</b>
</small>
</b-col>
<b-col class="col-12">
<b-form-checkbox
class="Test-BFormCheckbox"
v-model="CoinAnimationStatus"
name="check-button"
switch
@change="onSubmit"
>
{{
CoinAnimationStatus
? $t('settings.coinanimation.True')
: $t('settings.coinanimation.False')
}}
</b-form-checkbox>
</b-col>
</b-row>
</div>
</b-card>
</template>
<script>
import { updateUserInfos } from '@/graphql/mutations'
export default {
name: 'UserCoinAnimation',
data() {
return {
CoinAnimationStatus: this.$store.state.coinanimation,
}
},
methods: {
async onSubmit() {
this.$apollo
.mutate({
mutation: updateUserInfos,
variables: {
coinanimation: this.CoinAnimationStatus,
},
})
.then(() => {
this.$store.commit('coinanimation', this.CoinAnimationStatus)
this.toastSuccess(
this.CoinAnimationStatus
? this.$t('settings.coinanimation.True')
: this.$t('settings.coinanimation.False'),
)
})
.catch((error) => {
this.CoinAnimationStatus = this.$store.state.coinanimation
this.toastError(error.message)
})
},
},
}
</script>

View File

@ -189,7 +189,7 @@ describe('UserCard_FormUserPasswort', () => {
}) })
it('toasts a success message', () => { it('toasts a success message', () => {
expect(toastSuccessSpy).toBeCalledWith('site.thx.reset') expect(toastSuccessSpy).toBeCalledWith('message.reset')
}) })
it('cancels the edit process', () => { it('cancels the edit process', () => {

View File

@ -89,7 +89,7 @@ export default {
}, },
}) })
.then(() => { .then(() => {
this.toastSuccess(this.$t('site.thx.reset')) this.toastSuccess(this.$t('message.reset'))
this.cancelEdit() this.cancelEdit()
}) })
.catch((error) => { .catch((error) => {

View File

@ -31,7 +31,6 @@ export const updateUserInfos = gql`
$password: String $password: String
$passwordNew: String $passwordNew: String
$locale: String $locale: String
$coinanimation: Boolean
) { ) {
updateUserInfos( updateUserInfos(
firstName: $firstName firstName: $firstName
@ -39,7 +38,6 @@ export const updateUserInfos = gql`
password: $password password: $password
passwordNew: $passwordNew passwordNew: $passwordNew
language: $locale language: $locale
coinanimation: $coinanimation
) )
} }
` `

View File

@ -7,7 +7,6 @@ export const login = gql`
firstName firstName
lastName lastName
language language
coinanimation
klickTipp { klickTipp {
newsletterState newsletterState
} }
@ -25,7 +24,6 @@ export const verifyLogin = gql`
firstName firstName
lastName lastName
language language
coinanimation
klickTipp { klickTipp {
newsletterState newsletterState
} }

View File

@ -0,0 +1,83 @@
import { mount, RouterLinkStub } from '@vue/test-utils'
import AuthLayout from './AuthLayout'
const localVue = global.localVue
describe('AuthLayout', () => {
let wrapper
const mocks = {
$i18n: {
locale: 'en',
},
$t: jest.fn((t) => t),
$store: {
state: {},
commit: jest.fn(),
},
$route: {
meta: {
requiresAuth: false,
},
},
}
const stubs = {
RouterLink: RouterLinkStub,
RouterView: true,
}
const Wrapper = () => {
return mount(AuthLayout, { localVue, mocks, stubs })
}
describe('mount', () => {
beforeEach(() => {
wrapper = Wrapper()
})
describe('Mobile Version Start', () => {
beforeEach(() => {
wrapper.vm.mobileStart = true
})
it('has Component AuthMobileStart', () => {
expect(wrapper.findComponent({ name: 'AuthMobileStart' }).exists()).toBe(true)
})
it('has Component AuthNavbarSmall', () => {
expect(wrapper.findComponent({ name: 'AuthNavbarSmall' }).exists()).toBe(true)
})
})
describe('Desktop Version Start', () => {
beforeEach(() => {
wrapper.vm.mobileStart = false
})
it('has Component AuthNavbar', () => {
expect(wrapper.findComponent({ name: 'AuthNavbar' }).exists()).toBe(true)
})
it('has Component AuthCarousel', () => {
expect(wrapper.findComponent({ name: 'AuthCarousel' }).exists()).toBe(true)
})
it('has Component AuthFooter', () => {
expect(wrapper.findComponent({ name: 'AuthFooter' }).exists()).toBe(true)
})
it('has no sidebar', () => {
expect(wrapper.find('nav#sidenav-main').exists()).not.toBeTruthy()
})
it('has LanguageSwitch', () => {
expect(wrapper.findComponent({ name: 'LanguageSwitch' }).exists()).toBeTruthy()
})
test('test size in setTextSize ', () => {
wrapper.vm.setTextSize('85')
expect(wrapper.vm.$refs.pageFontSize.style.fontSize).toBe('85rem')
})
})
})
})

View File

@ -0,0 +1,178 @@
<template>
<div class="auth-template">
<auth-mobile-start
v-if="mobileStart"
class="d-inline d-lg-none zindex10000"
@set-mobile-start="setMobileStart"
/>
<div class="h-100 align-middle">
<auth-navbar class="zindex10" />
<div class="left-content-box position-fixed d-none d-lg-block">
<div class="bg-img-box position-absolute w-100">
<auth-carousel class="carousel" />
</div>
<div class="bg-txt-box position-relative d-none d-lg-block text-center align-self-center">
<div class="h0 text-white">{{ $t('auth.left.gratitude') }}</div>
<div class="h1 text-white">{{ $t('auth.left.newCurrency') }}</div>
<div class="h2 text-white">{{ $t('auth.left.oneAnotherNature') }}</div>
<b-button variant="gradido">{{ $t('auth.left.learnMore') }}</b-button>
</div>
</div>
<b-row class="justify-content-md-center">
<b-col sm="12" md="8" offset-lg="6" lg="6" class="zindex1000">
<div class="right-content-box ml-3 ml-sm-4 mr-3 mr-sm-4">
<b-row class="d-none d-md-block d-lg-none">
<b-col class="mb--4 d-flex justify-content-end">
<auth-navbar-small />
</b-col>
</b-row>
<b-row class="mt-5 pl-2 pl-md-0 pl-lg-0">
<b-col cols="9">
<div class="h1 mb--2">{{ $t('welcome') }}</div>
<div class="h1 mb-0">{{ $t('WelcomeBy', { name: communityName }) }}</div>
<div class="mb-0">{{ $t('1000thanks') }}</div>
</b-col>
<b-col cols="3" class="text-right d-none d-sm-none d-md-inline">
<b-avatar src="/img/brand/gradido_coin●.png" size="6rem"></b-avatar>
</b-col>
</b-row>
<b-card no-body ref="pageFontSize" class="border-0 mt-4 gradido-custom-background">
<b-row class="p-4">
<b-col cols="10">
<language-switch class="ml-3" />
</b-col>
<b-col cols="2" class="text-right">
<div id="popover-target-1" class="pointer">
<b-img src="/img/svg/type.svg" width="19" class="svgType"></b-img>
</div>
<b-popover
target="popover-target-1"
triggers="click"
placement="top"
variant="dark"
>
<div class="text-light">
<span class="pointer" @click="setTextSize(0.85)">{{ $t('85') }}</span>
{{ $t('math.pipe') }}
<span class="pointer" @click="setTextSize(1)">{{ $t('100') }}</span>
{{ $t('math.pipe') }}
<span class="pointer" @click="setTextSize(1.25)">{{ $t('125') }}</span>
</div>
</b-popover>
</b-col>
</b-row>
<b-row class="d-inline d-sm-inline d-md-none d-lg-none mb-3">
<b-col class="text-center">
<b-avatar src="/img/brand/gradido_coin●.png" size="6rem"></b-avatar>
<b-row>
<b-col class="zindex1000 d-flex justify-content-center">
<auth-navbar-small />
</b-col>
</b-row>
</b-col>
</b-row>
<b-card-body class="">
<router-view @set-mobile-start="setMobileStart"></router-view>
</b-card-body>
</b-card>
</div>
<auth-footer v-if="!$route.meta.hideFooter" class="pr-5 mb-5"></auth-footer>
</b-col>
</b-row>
<!-- <auth-layout-gdd />-->
</div>
</div>
</template>
<script>
import AuthMobileStart from '@/components/Auth/AuthMobileStart.vue'
import AuthNavbar from '@/components/Auth/AuthNavbar.vue'
import AuthNavbarSmall from '@/components/Auth/AuthNavbarSmall.vue'
import AuthCarousel from '@/components/Auth/AuthCarousel.vue'
import LanguageSwitch from '@/components/LanguageSwitch2'
import AuthFooter from '@/components/Auth/AuthFooter.vue'
import CONFIG from '@/config'
export default {
name: 'AuthLayout',
components: {
AuthMobileStart,
AuthNavbar,
AuthNavbarSmall,
AuthCarousel,
LanguageSwitch,
AuthFooter,
},
data() {
return {
mobileStart: true,
communityName: CONFIG.COMMUNITY_NAME,
}
},
methods: {
setMobileStart(boolean) {
this.mobileStart = boolean
},
setTextSize(size) {
this.$refs.pageFontSize.style.fontSize = size + 'rem'
},
},
}
</script>
<style lang="scss">
/* left */
.left-content-box {
width: 40%;
top: 0px;
bottom: 0px;
}
.bg-img-box {
top: 0px;
bottom: 0px;
}
/* right */
.right-content-box {
max-width: 640px;
}
.page-font-size {
font-size: 1rem;
}
.auth-template {
overflow-x: hidden;
}
.bg-txt-box {
margin-top: 317px;
text-shadow: 2px 2px 8px #000000;
max-width: 733px;
}
.bg-txt-box > .h0 {
font-size: 4em;
text-shadow: -2px -2px -8px #e4a907;
}
.bg-txt-box .h1,
.bg-txt-box .h2 {
font-size: 1.5em;
text-shadow: -2px -2px -8px #e4a907;
}
.bg-img {
border-radius: 0% 50% 70% 0% / 50% 70% 70% 50%;
overflow: hidden;
}
.svgType:hover {
filter: invert(38%) sepia(18%) saturate(5307%) hue-rotate(179deg) brightness(89%) contrast(89%);
}
@media screen and (min-width: 2000px) {
.right-content-box {
max-width: 60%;
font-size: xx-large;
}
}
</style>

View File

@ -1,67 +0,0 @@
import { mount } from '@vue/test-utils'
import AuthLayoutGdd from './AuthLayout_gdd'
const localVue = global.localVue
describe('AuthLayoutGdd', () => {
let wrapper
const mocks = {
$i18n: {
locale: 'en',
},
$t: jest.fn((t) => t),
$route: {
meta: {
hideFooter: false,
},
path: '/',
},
$store: {
state: {},
commit: jest.fn(),
},
}
const stubs = {
// RouterLink: RouterLinkStub,
RouterView: true,
}
const Wrapper = () => {
return mount(AuthLayoutGdd, { localVue, mocks, stubs })
}
describe('mount', () => {
beforeEach(() => {
wrapper = Wrapper()
})
it('has no sidebar', () => {
expect(wrapper.find('nav#sidenav-main').exists()).not.toBeTruthy()
})
it('has a main content div', () => {
expect(wrapper.find('div.main-content').exists()).toBeTruthy()
})
it('has a footer inside the main content', () => {
expect(wrapper.find('div.main-content').find('footer.footer').exists()).toBeTruthy()
})
it('has LanguageSwitch', () => {
expect(wrapper.findComponent({ name: 'LanguageSwitch' }).exists()).toBeTruthy()
})
describe('check LanguageSwitch on register page', () => {
beforeEach(() => {
mocks.$route.path = '/register'
wrapper = Wrapper()
})
it('has not LanguageSwitch', () => {
expect(wrapper.findComponent({ name: 'LanguageSwitch' }).exists()).toBeFalsy()
})
})
})
})

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