diff --git a/admin/.env.dist b/admin/.env.dist index 66c84dda8..d7044669a 100644 --- a/admin/.env.dist +++ b/admin/.env.dist @@ -1,3 +1,5 @@ +CONFIG_VERSION=v1.2022-03-18 + GRAPHQL_URI=http://localhost:4000/graphql WALLET_AUTH_URL=http://localhost/authenticate?token={token} WALLET_URL=http://localhost/login diff --git a/admin/.env.template b/admin/.env.template index a965b1bb1..488c9aba4 100644 --- a/admin/.env.template +++ b/admin/.env.template @@ -1,3 +1,5 @@ +CONFIG_VERSION=$ADMIN_CONFIG_VERSION + GRAPHQL_URI=$GRAPHQL_URI WALLET_AUTH_URL=$WALLET_AUTH_URL WALLET_URL=$WALLET_URL diff --git a/admin/src/config/index.js b/admin/src/config/index.js index f7d361c12..fe373386d 100644 --- a/admin/src/config/index.js +++ b/admin/src/config/index.js @@ -4,11 +4,20 @@ // Load Package Details for some default values const pkg = require('../../package') +const constants = { + CONFIG_VERSION: { + DEFAULT: 'DEFAULT', + EXPECTED: 'v1.2022-03-18', + CURRENT: '', + }, +} + const version = { APP_VERSION: pkg.version, BUILD_COMMIT: process.env.BUILD_COMMIT || null, // self reference of `version.BUILD_COMMIT` is not possible at this point, hence the duplicate code - BUILD_COMMIT_SHORT: (process.env.BUILD_COMMIT || '0000000').substr(0, 7), + BUILD_COMMIT_SHORT: (process.env.BUILD_COMMIT || '0000000').slice(0, 7), + PORT: process.env.PORT || 8080, } const environment = { @@ -27,14 +36,24 @@ const debug = { DEBUG_DISABLE_AUTH: process.env.DEBUG_DISABLE_AUTH === 'true' || false, } -const options = {} +// Check config version +constants.CONFIG_VERSION.CURRENT = process.env.CONFIG_VERSION || constants.CONFIG_VERSION.DEFAULT +if ( + ![constants.CONFIG_VERSION.EXPECTED, constants.CONFIG_VERSION.DEFAULT].includes( + constants.CONFIG_VERSION.CURRENT, + ) +) { + throw new Error( + `Fatal: Config Version incorrect - expected "${constants.CONFIG_VERSION.EXPECTED}" or "${constants.CONFIG_VERSION.DEFAULT}", but found "${constants.CONFIG_VERSION.CURRENT}"`, + ) +} const CONFIG = { + ...constants, ...version, ...environment, ...endpoints, - ...options, ...debug, } -export default CONFIG +module.exports = CONFIG diff --git a/admin/src/pages/UserSearch.vue b/admin/src/pages/UserSearch.vue index 24334e0c9..35d29ad7f 100644 --- a/admin/src/pages/UserSearch.vue +++ b/admin/src/pages/UserSearch.vue @@ -88,6 +88,7 @@ export default { notActivated: this.filterCheckedEmails, isDeleted: this.filterDeletedUser, }, + fetchPolicy: 'no-cache', }) .then((result) => { this.rows = result.data.searchUsers.userCount diff --git a/admin/vue.config.js b/admin/vue.config.js index 4492312a0..8cc1e4b89 100644 --- a/admin/vue.config.js +++ b/admin/vue.config.js @@ -2,11 +2,12 @@ const path = require('path') const webpack = require('webpack') const Dotenv = require('dotenv-webpack') const StatsPlugin = require('stats-webpack-plugin') +const CONFIG = require('./src/config') // vue.config.js module.exports = { devServer: { - port: process.env.PORT || 8080, + port: CONFIG.PORT, }, pluginOptions: { i18n: { @@ -34,7 +35,7 @@ module.exports = { // 'process.env.DOCKER_WORKDIR': JSON.stringify(process.env.DOCKER_WORKDIR), // 'process.env.BUILD_DATE': JSON.stringify(process.env.BUILD_DATE), // 'process.env.BUILD_VERSION': JSON.stringify(process.env.BUILD_VERSION), - 'process.env.BUILD_COMMIT': JSON.stringify(process.env.BUILD_COMMIT), + 'process.env.BUILD_COMMIT': JSON.stringify(CONFIG.BUILD_COMMIT), // 'process.env.PORT': JSON.stringify(process.env.PORT), }), // generate webpack stats to allow analysis of the bundlesize @@ -46,7 +47,7 @@ module.exports = { }, css: { // Enable CSS source maps. - sourceMap: process.env.NODE_ENV !== 'production', + sourceMap: CONFIG.NODE_ENV !== 'production', }, outputDir: path.resolve(__dirname, './dist'), } diff --git a/backend/.env.dist b/backend/.env.dist index ab0527836..a81f0ad7c 100644 --- a/backend/.env.dist +++ b/backend/.env.dist @@ -1,3 +1,5 @@ +CONFIG_VERSION=v2.2022-03-24 + # Server PORT=4000 JWT_SECRET=secret123 @@ -38,10 +40,11 @@ EMAIL_SENDER=info@gradido.net EMAIL_PASSWORD=xxx EMAIL_SMTP_URL=gmail.com EMAIL_SMTP_PORT=587 -EMAIL_LINK_VERIFICATION=http://localhost/checkEmail/{code} +EMAIL_LINK_VERIFICATION=http://localhost/checkEmail/{optin}{code} EMAIL_LINK_SETPASSWORD=http://localhost/reset/{code} EMAIL_LINK_FORGOTPASSWORD=http://localhost/forgot-password EMAIL_CODE_VALID_TIME=1440 +EMAIL_CODE_REQUEST_TIME=10 # Webhook WEBHOOK_ELOPAGE_SECRET=secret \ No newline at end of file diff --git a/backend/.env.template b/backend/.env.template index b3a5eb4c6..454b25d3c 100644 --- a/backend/.env.template +++ b/backend/.env.template @@ -1,3 +1,5 @@ +CONFIG_VERSION=$BACKEND_CONFIG_VERSION + # Server JWT_SECRET=$JWT_SECRET JWT_EXPIRES_IN=10m diff --git a/backend/src/config/index.ts b/backend/src/config/index.ts index 23385fe02..05be27298 100644 --- a/backend/src/config/index.ts +++ b/backend/src/config/index.ts @@ -12,6 +12,11 @@ Decimal.set({ const constants = { DB_VERSION: '0033-add_referrer_id', DECAY_START_TIME: new Date('2021-05-13 17:46:31'), // GMT+0 + CONFIG_VERSION: { + DEFAULT: 'DEFAULT', + EXPECTED: 'v2.2022-03-24', + CURRENT: '', + }, } const server = { @@ -62,14 +67,19 @@ const email = { EMAIL_SMTP_URL: process.env.EMAIL_SMTP_URL || 'gmail.com', EMAIL_SMTP_PORT: process.env.EMAIL_SMTP_PORT || '587', EMAIL_LINK_VERIFICATION: - process.env.EMAIL_LINK_VERIFICATION || 'http://localhost/checkEmail/{code}', + process.env.EMAIL_LINK_VERIFICATION || 'http://localhost/checkEmail/{optin}{code}', EMAIL_LINK_SETPASSWORD: - process.env.EMAIL_LINK_SETPASSWORD || 'http://localhost/reset-password/{code}', + process.env.EMAIL_LINK_SETPASSWORD || 'http://localhost/reset-password/{optin}', EMAIL_LINK_FORGOTPASSWORD: process.env.EMAIL_LINK_FORGOTPASSWORD || 'http://localhost/forgot-password', + // time in minutes a optin code is valid EMAIL_CODE_VALID_TIME: process.env.EMAIL_CODE_VALID_TIME ? parseInt(process.env.EMAIL_CODE_VALID_TIME) || 1440 : 1440, + // time in minutes that must pass to request a new optin code + EMAIL_CODE_REQUEST_TIME: process.env.EMAIL_CODE_REQUEST_TIME + ? parseInt(process.env.EMAIL_CODE_REQUEST_TIME) || 10 + : 10, } const webhook = { @@ -80,6 +90,18 @@ const webhook = { // This is needed by graphql-directive-auth process.env.APP_SECRET = server.JWT_SECRET +// Check config version +constants.CONFIG_VERSION.CURRENT = process.env.CONFIG_VERSION || constants.CONFIG_VERSION.DEFAULT +if ( + ![constants.CONFIG_VERSION.EXPECTED, constants.CONFIG_VERSION.DEFAULT].includes( + constants.CONFIG_VERSION.CURRENT, + ) +) { + throw new Error( + `Fatal: Config Version incorrect - expected "${constants.CONFIG_VERSION.EXPECTED}" or "${constants.CONFIG_VERSION.DEFAULT}", but found "${constants.CONFIG_VERSION.CURRENT}"`, + ) +} + const CONFIG = { ...constants, ...server, diff --git a/backend/src/graphql/arg/CreateUserArgs.ts b/backend/src/graphql/arg/CreateUserArgs.ts index 0d63e76bb..af915b91a 100644 --- a/backend/src/graphql/arg/CreateUserArgs.ts +++ b/backend/src/graphql/arg/CreateUserArgs.ts @@ -16,4 +16,7 @@ export default class CreateUserArgs { @Field(() => Int, { nullable: true }) publisherId: number + + @Field(() => String, { nullable: true }) + redeemCode?: string | null } diff --git a/backend/src/graphql/enum/OptInType.ts b/backend/src/graphql/enum/OptInType.ts new file mode 100644 index 000000000..2dd2d07b0 --- /dev/null +++ b/backend/src/graphql/enum/OptInType.ts @@ -0,0 +1,11 @@ +import { registerEnumType } from 'type-graphql' + +export enum OptInType { + EMAIL_OPT_IN_REGISTER = 1, + EMAIL_OPT_IN_RESET_PASSWORD = 2, +} + +registerEnumType(OptInType, { + name: 'OptInType', // this one is mandatory + description: 'Type of the email optin', // this one is optional +}) diff --git a/backend/src/graphql/resolver/AdminResolver.ts b/backend/src/graphql/resolver/AdminResolver.ts index d98b38b7f..7f966b275 100644 --- a/backend/src/graphql/resolver/AdminResolver.ts +++ b/backend/src/graphql/resolver/AdminResolver.ts @@ -34,6 +34,8 @@ import { Decay } from '@model/Decay' import Paginated from '@arg/Paginated' import { Order } from '@enum/Order' import { communityUser } from '@/util/communityUser' +import { checkOptInCode, activationLink } from './UserResolver' +import { sendAccountActivationEmail } from '@/mailer/sendAccountActivationEmail' // const EMAIL_OPT_IN_REGISTER = 1 // const EMAIL_OPT_UNKNOWN = 3 // elopage? @@ -369,6 +371,39 @@ export class AdminResolver { const user = await dbUser.findOneOrFail({ id: userId }) return userTransactions.map((t) => new Transaction(t, new User(user), communityUser)) } + + @Authorized([RIGHTS.SEND_ACTIVATION_EMAIL]) + @Mutation(() => Boolean) + async sendActivationEmail(@Arg('email') email: string): Promise { + email = email.trim().toLowerCase() + const user = await dbUser.findOneOrFail({ email: email }) + + // can be both types: REGISTER and RESET_PASSWORD + let optInCode = await LoginEmailOptIn.findOne({ + where: { userId: user.id }, + order: { updatedAt: 'DESC' }, + }) + + optInCode = await checkOptInCode(optInCode, user.id) + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const emailSent = await sendAccountActivationEmail({ + link: activationLink(optInCode), + firstName: user.firstName, + lastName: user.lastName, + email, + }) + + /* uncomment this, when you need the activation link on the console + // In case EMails are disabled log the activation link for the user + if (!emailSent) { + // eslint-disable-next-line no-console + console.log(`Account confirmation link: ${activationLink}`) + } + */ + + return true + } } interface CreationMap { diff --git a/backend/src/graphql/resolver/TransactionResolver.ts b/backend/src/graphql/resolver/TransactionResolver.ts index 03640817f..0ff43486c 100644 --- a/backend/src/graphql/resolver/TransactionResolver.ts +++ b/backend/src/graphql/resolver/TransactionResolver.ts @@ -34,6 +34,9 @@ import { virtualLinkTransaction, virtualDecayTransaction } from '@/util/virtualT import Decimal from 'decimal.js-light' import { calculateDecay } from '@/util/decay' +const MEMO_MAX_CHARS = 255 +const MEMO_MIN_CHARS = 5 + export const executeTransaction = async ( amount: Decimal, memo: string, @@ -45,6 +48,14 @@ export const executeTransaction = async ( throw new Error('Sender and Recipient are the same.') } + if (memo.length > MEMO_MAX_CHARS) { + throw new Error(`memo text is too long (${MEMO_MAX_CHARS} characters maximum)`) + } + + if (memo.length < MEMO_MIN_CHARS) { + throw new Error(`memo text is too short (${MEMO_MIN_CHARS} characters minimum)`) + } + // validate amount const receivedCallDate = new Date() const sendBalance = await calculateBalance(sender.id, amount.mul(-1), receivedCallDate) @@ -117,6 +128,7 @@ export const executeTransaction = async ( recipientFirstName: recipient.firstName, recipientLastName: recipient.lastName, email: recipient.email, + senderEmail: sender.email, amount, memo, }) diff --git a/backend/src/graphql/resolver/UserResolver.test.ts b/backend/src/graphql/resolver/UserResolver.test.ts index 93c8016e0..a0f81e8b5 100644 --- a/backend/src/graphql/resolver/UserResolver.test.ts +++ b/backend/src/graphql/resolver/UserResolver.test.ts @@ -11,7 +11,7 @@ import { LoginEmailOptIn } from '@entity/LoginEmailOptIn' import { User } from '@entity/User' import CONFIG from '@/config' import { sendAccountActivationEmail } from '@/mailer/sendAccountActivationEmail' -import { printEmailCodeValidTime } from './UserResolver' +import { printTimeDuration } from './UserResolver' // import { klicktippSignIn } from '@/apis/KlicktippController' @@ -125,7 +125,10 @@ describe('UserResolver', () => { describe('account activation email', () => { it('sends an account activation email', () => { - const activationLink = CONFIG.EMAIL_LINK_VERIFICATION.replace(/{code}/g, emailOptIn) + const activationLink = CONFIG.EMAIL_LINK_VERIFICATION.replace( + /{optin}/g, + emailOptIn, + ).replace(/{code}/g, '') expect(sendAccountActivationEmail).toBeCalledWith({ link: activationLink, firstName: 'Peter', @@ -415,19 +418,16 @@ describe('UserResolver', () => { }) }) -describe('printEmailCodeValidTime', () => { +describe('printTimeDuration', () => { it('works with 10 minutes', () => { - CONFIG.EMAIL_CODE_VALID_TIME = 10 - expect(printEmailCodeValidTime()).toBe('10 minutes') + expect(printTimeDuration(10)).toBe('10 minutes') }) it('works with 1440 minutes', () => { - CONFIG.EMAIL_CODE_VALID_TIME = 1440 - expect(printEmailCodeValidTime()).toBe('24 hours') + expect(printTimeDuration(1440)).toBe('24 hours') }) it('works with 1410 minutes', () => { - CONFIG.EMAIL_CODE_VALID_TIME = 1410 - expect(printEmailCodeValidTime()).toBe('23 hours and 30 minutes') + expect(printTimeDuration(1410)).toBe('23 hours and 30 minutes') }) }) diff --git a/backend/src/graphql/resolver/UserResolver.ts b/backend/src/graphql/resolver/UserResolver.ts index 2d2355f17..a3534e223 100644 --- a/backend/src/graphql/resolver/UserResolver.ts +++ b/backend/src/graphql/resolver/UserResolver.ts @@ -3,10 +3,11 @@ import fs from 'fs' import { Resolver, Query, Args, Arg, Authorized, Ctx, UseMiddleware, Mutation } from 'type-graphql' -import { getConnection, getCustomRepository, QueryRunner } from '@dbTools/typeorm' +import { getConnection, getCustomRepository } from '@dbTools/typeorm' import CONFIG from '@/config' import { User } from '@model/User' import { User as DbUser } from '@entity/User' +import { TransactionLink as dbTransactionLink } from '@entity/TransactionLink' import { encode } from '@/auth/JWT' import CreateUserArgs from '@arg/CreateUserArgs' import UnsecureLoginArgs from '@arg/UnsecureLoginArgs' @@ -14,6 +15,7 @@ import UpdateUserInfosArgs from '@arg/UpdateUserInfosArgs' import { klicktippNewsletterStateMiddleware } from '@/middleware/klicktippMiddleware' import { UserSettingRepository } from '@repository/UserSettingRepository' import { Setting } from '@enum/Setting' +import { OptInType } from '@enum/OptInType' import { LoginEmailOptIn } from '@entity/LoginEmailOptIn' import { sendResetPasswordEmail as sendResetPasswordEmailMailer } from '@/mailer/sendResetPasswordEmail' import { sendAccountActivationEmail } from '@/mailer/sendAccountActivationEmail' @@ -23,9 +25,6 @@ import { ROLE_ADMIN } from '@/auth/ROLES' import { hasElopageBuys } from '@/util/hasElopageBuys' import { ServerUser } from '@entity/ServerUser' -const EMAIL_OPT_IN_RESET_PASSWORD = 2 -const EMAIL_OPT_IN_REGISTER = 1 - // eslint-disable-next-line @typescript-eslint/no-var-requires const sodium = require('sodium-native') // eslint-disable-next-line @typescript-eslint/no-var-requires @@ -147,57 +146,47 @@ const SecretKeyCryptographyDecrypt = (encryptedMessage: Buffer, encryptionKey: B return message } -const createEmailOptIn = async ( - loginUserId: number, - queryRunner: QueryRunner, -): Promise => { - let emailOptIn = await LoginEmailOptIn.findOne({ - userId: loginUserId, - emailOptInTypeId: EMAIL_OPT_IN_REGISTER, - }) - if (emailOptIn) { - if (isOptInCodeValid(emailOptIn)) { - throw new Error(`email already sent less than ${printEmailCodeValidTime()} ago`) - } - emailOptIn.updatedAt = new Date() - emailOptIn.resendCount++ - } else { - emailOptIn = new LoginEmailOptIn() - emailOptIn.verificationCode = random(64) - emailOptIn.userId = loginUserId - emailOptIn.emailOptInTypeId = EMAIL_OPT_IN_REGISTER - } - await queryRunner.manager.save(emailOptIn).catch((error) => { - // eslint-disable-next-line no-console - console.log('Error while saving emailOptIn', error) - throw new Error('error saving email opt in') - }) + +const newEmailOptIn = (userId: number): LoginEmailOptIn => { + const emailOptIn = new LoginEmailOptIn() + emailOptIn.verificationCode = random(64) + emailOptIn.userId = userId + emailOptIn.emailOptInTypeId = OptInType.EMAIL_OPT_IN_REGISTER return emailOptIn } -const getOptInCode = async (loginUserId: number): Promise => { - let optInCode = await LoginEmailOptIn.findOne({ - userId: loginUserId, - emailOptInTypeId: EMAIL_OPT_IN_RESET_PASSWORD, - }) - - // Check for `CONFIG.EMAIL_CODE_VALID_TIME` minute delay +// needed by AdminResolver +// checks if given code exists and can be resent +// if optIn does not exits, it is created +export const checkOptInCode = async ( + optInCode: LoginEmailOptIn | undefined, + userId: number, + optInType: OptInType = OptInType.EMAIL_OPT_IN_REGISTER, +): Promise => { if (optInCode) { - if (isOptInCodeValid(optInCode)) { - throw new Error(`email already sent less than $(printEmailCodeValidTime()} minutes ago`) + if (!canResendOptIn(optInCode)) { + throw new Error( + `email already sent less than ${printTimeDuration( + CONFIG.EMAIL_CODE_REQUEST_TIME, + )} minutes ago`, + ) } optInCode.updatedAt = new Date() optInCode.resendCount++ } else { - optInCode = new LoginEmailOptIn() - optInCode.verificationCode = random(64) - optInCode.userId = loginUserId - optInCode.emailOptInTypeId = EMAIL_OPT_IN_RESET_PASSWORD + optInCode = newEmailOptIn(userId) } - await LoginEmailOptIn.save(optInCode) + optInCode.emailOptInTypeId = optInType + await LoginEmailOptIn.save(optInCode).catch(() => { + throw new Error('Unable to save optin code.') + }) return optInCode } +export const activationLink = (optInCode: LoginEmailOptIn): string => { + return CONFIG.EMAIL_LINK_SETPASSWORD.replace(/{optin}/g, optInCode.verificationCode.toString()) +} + @Resolver() export class UserResolver { @Authorized([RIGHTS.VERIFY_LOGIN]) @@ -305,7 +294,8 @@ export class UserResolver { @Authorized([RIGHTS.CREATE_USER]) @Mutation(() => User) async createUser( - @Args() { email, firstName, lastName, language, publisherId }: CreateUserArgs, + @Args() + { email, firstName, lastName, language, publisherId, redeemCode = null }: CreateUserArgs, ): Promise { // TODO: wrong default value (should be null), how does graphql work here? Is it an required field? // default int publisher_id = 0; @@ -338,6 +328,12 @@ export class UserResolver { dbUser.language = language dbUser.publisherId = publisherId dbUser.passphrase = passphrase.join(' ') + if (redeemCode) { + const transactionLink = await dbTransactionLink.findOne({ code: redeemCode }) + if (transactionLink) { + dbUser.referrerId = transactionLink.userId + } + } // TODO this field has no null allowed unlike the loginServer table // dbUser.pubKey = Buffer.from(randomBytes(32)) // Buffer.alloc(32, 0) default to 0000... // dbUser.pubkey = keyPair[0] @@ -355,14 +351,17 @@ export class UserResolver { throw new Error('error saving user') }) - // Store EmailOptIn in DB - // TODO: this has duplicate code with sendResetPasswordEmail - const emailOptIn = await createEmailOptIn(dbUser.id, queryRunner) + const emailOptIn = newEmailOptIn(dbUser.id) + await queryRunner.manager.save(emailOptIn).catch((error) => { + // eslint-disable-next-line no-console + console.log('Error while saving emailOptIn', error) + throw new Error('error saving email opt in') + }) const activationLink = CONFIG.EMAIL_LINK_VERIFICATION.replace( - /{code}/g, + /{optin}/g, emailOptIn.verificationCode.toString(), - ) + ).replace(/{code}/g, redeemCode ? '/' + redeemCode : '') // eslint-disable-next-line @typescript-eslint/no-unused-vars const emailSent = await sendAccountActivationEmail({ @@ -380,6 +379,7 @@ export class UserResolver { console.log(`Account confirmation link: ${activationLink}`) } */ + await queryRunner.commitTransaction() } catch (e) { await queryRunner.rollbackTransaction() @@ -390,68 +390,22 @@ export class UserResolver { return new User(dbUser) } - // THis is used by the admin only - should we move it to the admin resolver? - @Authorized([RIGHTS.SEND_ACTIVATION_EMAIL]) - @Mutation(() => Boolean) - async sendActivationEmail(@Arg('email') email: string): Promise { - email = email.trim().toLowerCase() - const user = await DbUser.findOneOrFail({ email: email }) - - const queryRunner = getConnection().createQueryRunner() - await queryRunner.connect() - await queryRunner.startTransaction('READ UNCOMMITTED') - - try { - const emailOptIn = await createEmailOptIn(user.id, queryRunner) - - const activationLink = CONFIG.EMAIL_LINK_VERIFICATION.replace( - /{code}/g, - emailOptIn.verificationCode.toString(), - ) - - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const emailSent = await sendAccountActivationEmail({ - link: activationLink, - firstName: user.firstName, - lastName: user.lastName, - email, - duration: printEmailCodeValidTime(), - }) - - /* uncomment this, when you need the activation link on the console - // In case EMails are disabled log the activation link for the user - if (!emailSent) { - // eslint-disable-next-line no-console - console.log(`Account confirmation link: ${activationLink}`) - } - */ - await queryRunner.commitTransaction() - } catch (e) { - await queryRunner.rollbackTransaction() - throw e - } finally { - await queryRunner.release() - } - return true - } - @Authorized([RIGHTS.SEND_RESET_PASSWORD_EMAIL]) @Query(() => Boolean) async sendResetPasswordEmail(@Arg('email') email: string): Promise { - // TODO: this has duplicate code with createUser email = email.trim().toLowerCase() const user = await DbUser.findOneOrFail({ email }) - const optInCode = await getOptInCode(user.id) + // can be both types: REGISTER and RESET_PASSWORD + let optInCode = await LoginEmailOptIn.findOne({ + userId: user.id, + }) - const link = CONFIG.EMAIL_LINK_SETPASSWORD.replace( - /{code}/g, - optInCode.verificationCode.toString(), - ) + optInCode = await checkOptInCode(optInCode, user.id, OptInType.EMAIL_OPT_IN_RESET_PASSWORD) // eslint-disable-next-line @typescript-eslint/no-unused-vars const emailSent = await sendResetPasswordEmailMailer({ - link, + link: activationLink(optInCode), firstName: user.firstName, lastName: user.lastName, email, @@ -488,8 +442,10 @@ export class UserResolver { }) // Code is only valid for `CONFIG.EMAIL_CODE_VALID_TIME` minutes - if (!isOptInCodeValid(optInCode)) { - throw new Error(`email was sent more than ${printEmailCodeValidTime()} ago`) + if (!isOptInValid(optInCode)) { + throw new Error( + `email was sent more than ${printTimeDuration(CONFIG.EMAIL_CODE_VALID_TIME)} ago`, + ) } // load user @@ -532,11 +488,6 @@ export class UserResolver { throw new Error('error saving user: ' + error) }) - // Delete Code - await queryRunner.manager.remove(optInCode).catch((error) => { - throw new Error('error deleting code: ' + error) - }) - await queryRunner.commitTransaction() } catch (e) { await queryRunner.rollbackTransaction() @@ -547,7 +498,7 @@ export class UserResolver { // Sign into Klicktipp // TODO do we always signUp the user? How to handle things with old users? - if (optInCode.emailOptInTypeId === EMAIL_OPT_IN_REGISTER) { + if (optInCode.emailOptInTypeId === OptInType.EMAIL_OPT_IN_REGISTER) { try { await klicktippSignIn(user.email, user.language, user.firstName, user.lastName) } catch { @@ -567,8 +518,10 @@ export class UserResolver { async queryOptIn(@Arg('optIn') optIn: string): Promise { const optInCode = await LoginEmailOptIn.findOneOrFail({ verificationCode: optIn }) // Code is only valid for `CONFIG.EMAIL_CODE_VALID_TIME` minutes - if (!isOptInCodeValid(optInCode)) { - throw new Error(`email was sent more than $(printEmailCodeValidTime()} ago`) + if (!isOptInValid(optInCode)) { + throw new Error( + `email was sent more than ${printTimeDuration(CONFIG.EMAIL_CODE_VALID_TIME)} ago`, + ) } return true } @@ -675,23 +628,32 @@ export class UserResolver { } } -function isOptInCodeValid(optInCode: LoginEmailOptIn) { - const timeElapsed = Date.now() - new Date(optInCode.updatedAt).getTime() - return timeElapsed <= CONFIG.EMAIL_CODE_VALID_TIME * 60 * 1000 +const isTimeExpired = (optIn: LoginEmailOptIn, duration: number): boolean => { + const timeElapsed = Date.now() - new Date(optIn.updatedAt).getTime() + // time is given in minutes + return timeElapsed <= duration * 60 * 1000 } -const emailCodeValidTime = (): { hours?: number; minutes: number } => { - if (CONFIG.EMAIL_CODE_VALID_TIME > 60) { +const isOptInValid = (optIn: LoginEmailOptIn): boolean => { + return isTimeExpired(optIn, CONFIG.EMAIL_CODE_VALID_TIME) +} + +const canResendOptIn = (optIn: LoginEmailOptIn): boolean => { + return !isTimeExpired(optIn, CONFIG.EMAIL_CODE_REQUEST_TIME) +} + +const getTimeDurationObject = (time: number): { hours?: number; minutes: number } => { + if (time > 60) { return { - hours: Math.floor(CONFIG.EMAIL_CODE_VALID_TIME / 60), - minutes: CONFIG.EMAIL_CODE_VALID_TIME % 60, + hours: Math.floor(time / 60), + minutes: time % 60, } } - return { minutes: CONFIG.EMAIL_CODE_VALID_TIME } + return { minutes: time } } -export const printEmailCodeValidTime = (): string => { - const time = emailCodeValidTime() +export const printTimeDuration = (duration: number): string => { + const time = getTimeDurationObject(duration) const result = time.minutes > 0 ? `${time.minutes} minutes` : '' if (time.hours) return `${time.hours} hours` + (result !== '' ? ` and ${result}` : '') return result diff --git a/backend/src/mailer/sendTransactionReceivedEmail.test.ts b/backend/src/mailer/sendTransactionReceivedEmail.test.ts index 5fd013650..1ebc9dae3 100644 --- a/backend/src/mailer/sendTransactionReceivedEmail.test.ts +++ b/backend/src/mailer/sendTransactionReceivedEmail.test.ts @@ -17,6 +17,7 @@ describe('sendTransactionReceivedEmail', () => { recipientFirstName: 'Peter', recipientLastName: 'Lustig', email: 'peter@lustig.de', + senderEmail: 'bibi@bloxberg.de', amount: new Decimal(42.0), memo: 'Vielen herzlichen Dank für den neuen Hexenbesen!', }) @@ -30,6 +31,7 @@ describe('sendTransactionReceivedEmail', () => { expect.stringContaining('Hallo Peter Lustig') && expect.stringContaining('42,00 GDD') && expect.stringContaining('Bibi Bloxberg') && + expect.stringContaining('(bibi@bloxberg.de)') && expect.stringContaining('Vielen herzlichen Dank für den neuen Hexenbesen!'), }) }) diff --git a/backend/src/mailer/sendTransactionReceivedEmail.ts b/backend/src/mailer/sendTransactionReceivedEmail.ts index 3b417b10a..934783449 100644 --- a/backend/src/mailer/sendTransactionReceivedEmail.ts +++ b/backend/src/mailer/sendTransactionReceivedEmail.ts @@ -8,6 +8,7 @@ export const sendTransactionReceivedEmail = (data: { recipientFirstName: string recipientLastName: string email: string + senderEmail: string amount: Decimal memo: string }): Promise => { diff --git a/backend/src/mailer/text/transactionReceived.ts b/backend/src/mailer/text/transactionReceived.ts index f685c60ae..520ee43bf 100644 --- a/backend/src/mailer/text/transactionReceived.ts +++ b/backend/src/mailer/text/transactionReceived.ts @@ -9,6 +9,7 @@ export const transactionReceived = { recipientFirstName: string recipientLastName: string email: string + senderEmail: string amount: Decimal memo: string }): string => @@ -16,7 +17,7 @@ export const transactionReceived = { Du hast soeben ${data.amount.toFixed(2).replace('.', ',')} GDD von ${data.senderFirstName} ${ data.senderLastName - } erhalten. + } (${data.senderEmail}) erhalten. ${data.senderFirstName} ${data.senderLastName} schreibt: ${data.memo} diff --git a/backend/src/seeds/graphql/mutations.ts b/backend/src/seeds/graphql/mutations.ts index f68d983c0..298d56bdb 100644 --- a/backend/src/seeds/graphql/mutations.ts +++ b/backend/src/seeds/graphql/mutations.ts @@ -45,6 +45,7 @@ export const createUser = gql` $email: String! $language: String! $publisherId: Int + $redeemCode: String ) { createUser( email: $email @@ -52,6 +53,7 @@ export const createUser = gql` lastName: $lastName language: $language publisherId: $publisherId + redeemCode: $redeemCode ) { id } diff --git a/database/.env.dist b/database/.env.dist index 689e4f509..58362a7b9 100644 --- a/database/.env.dist +++ b/database/.env.dist @@ -1,3 +1,5 @@ +CONFIG_VERSION=v1.2022-03-18 + DB_HOST=localhost DB_PORT=3306 DB_USER=root diff --git a/database/.env.template b/database/.env.template index 5b8554bcf..f2517a397 100644 --- a/database/.env.template +++ b/database/.env.template @@ -1,3 +1,5 @@ +CONFIG_VERSION=$DATABASE_CONFIG_VERSION + DB_HOST=localhost DB_PORT=3306 DB_USER=$DB_USER diff --git a/database/src/config/index.ts b/database/src/config/index.ts index 2dde06c96..ba41f11d4 100644 --- a/database/src/config/index.ts +++ b/database/src/config/index.ts @@ -3,6 +3,14 @@ import dotenv from 'dotenv' dotenv.config() +const constants = { + CONFIG_VERSION: { + DEFAULT: 'DEFAULT', + EXPECTED: 'v1.2022-03-18', + CURRENT: '', + }, +} + const database = { DB_HOST: process.env.DB_HOST || 'localhost', DB_PORT: process.env.DB_PORT ? parseInt(process.env.DB_PORT) : 3306, @@ -15,6 +23,18 @@ const migrations = { MIGRATIONS_TABLE: process.env.MIGRATIONS_TABLE || 'migrations', } -const CONFIG = { ...database, ...migrations } +// Check config version +constants.CONFIG_VERSION.CURRENT = process.env.CONFIG_VERSION || constants.CONFIG_VERSION.DEFAULT +if ( + ![constants.CONFIG_VERSION.EXPECTED, constants.CONFIG_VERSION.DEFAULT].includes( + constants.CONFIG_VERSION.CURRENT, + ) +) { + throw new Error( + `Fatal: Config Version incorrect - expected "${constants.CONFIG_VERSION.EXPECTED}" or "${constants.CONFIG_VERSION.DEFAULT}", but found "${constants.CONFIG_VERSION.CURRENT}"`, + ) +} + +const CONFIG = { ...constants, ...database, ...migrations } export default CONFIG diff --git a/deployment/bare_metal/.env.dist b/deployment/bare_metal/.env.dist index 9a9d57b4c..ee0ac3cc3 100644 --- a/deployment/bare_metal/.env.dist +++ b/deployment/bare_metal/.env.dist @@ -18,6 +18,8 @@ WEBHOOK_GITHUB_SECRET=secret WEBHOOK_GITHUB_BRANCH=master # backend +BACKEND_CONFIG_VERSION=v1.2022-03-18 + EMAIL=true EMAIL_USERNAME=peter@lustig.de EMAIL_SENDER=peter@lustig.de @@ -43,6 +45,9 @@ KLICKTIPP_PASSWORD= KLICKTIPP_APIKEY_DE= KLICKTIPP_APIKEY_EN= +# database +DATABASE_CONFIG_VERSION=v1.2022-03-18 + # frontend GRAPHQL_URI=https://stage1.gradido.net/graphql ADMIN_AUTH_URL=https://stage1.gradido.net/admin/authenticate?token={token} diff --git a/docu/Concepts/BusinessRequirements/CommunityVerwaltung.md b/docu/Concepts/BusinessRequirements/CommunityVerwaltung.md index 8e86c76ef..805e3c402 100644 --- a/docu/Concepts/BusinessRequirements/CommunityVerwaltung.md +++ b/docu/Concepts/BusinessRequirements/CommunityVerwaltung.md @@ -31,6 +31,37 @@ Andererseits soll aber, wenn eine Community sich bei der Geldschöpfung nicht an Aber grundsätzlich bleibt bei allen *Community-Gradido*-Währungen die Vergänglichkeit als Sicherungsmechanismus des Geldvolumens und der 1:1 Umtausch zwischen verschiedenen *Community-Gradidos* bestehen. +#### Community-Gradido + +Um das Thema *Community-Gradido* näher zu beleuchten, wird im Folgenden eine etwas technischer Beschreibung gewählt. Der eingangs verwendete Begriff von *Colored-Gradido* wird hiermit abgelöst durch *Community-Gradido*. Dies soll analog der verschiedenen Währungen wie Euro, US-Dollar, Yen, etc. abgebildet werden. Das heißt die Gradido-Anwendung wird intern einen Datentyp "Money" haben, der neben dem Betrag auch die Währung trägt. Somit kann immer für einen Geld-Betrag X aus dem Datentyp *Money* alle Information herausgelesen bzw. übertragen werden. Gleichzeitig wird der Datentyp alle notwendigen Hilfsmethoden unterstützen, die für eine korrekte Anzeige, Berechnungen wie Vergänglichkeit oder sonstige Additionen und Subtraktionen notwendig sind. + +Die Information über die Währung wird intern über einen eindeutigen Community-Schlüssel getragen, wodurch für jeden Betrag die Aussage möglich ist in welcher Community dieser Betrag einmal geschöpft wurde. Der Community-Schlüssel wird schon bei der Inbetriebnahme einer Community erstellt und dient in verschiedenen anderen fachlichen Prozessen - Stichwort Inter-Community-Communication - als eindeutiger Identifikator einer Community. Die weiteren Auswirkungen auf die Kontoführung und deren Berechnungen und Verwaltung von Transaktionen wird dementsprechend berücksichtigt und detailliert in der [Kontenverwaltung](./Kontenverwaltung.md) beschrieben werden. + +##### Datentyp "Money" + +Type Money { + +* Attribute + * Integer Betrag; // in Gradido-Cent und damit ohne Nachkommastellen + * String CommunityKey; // eindeutiger CommunityKey z.B. eine UUID +* Methoden + * zurAnzeige(); // liefert den Betrag als String mit 2 Nachkommastellen und dem Gradido-Währungssymbol + * plus(Money m) // prüft, ob der CommunityKey von m gleich dem internen CommunityKey ist und falls ja wird der Betrag von m zu dem internen Betrag aufaddiert, sonst wird eine Exception geworfen + * minus(Money m) // prüft, ob der CommunityKey von m gleich dem internen CommunityKey ist und falls ja wird der Betrag von m von dem internen Betrag subtrahiert, sonst wird eine Exception geworfen + * isSameCommunity(Money m) // liefert TRUE wenn der CommunityKey von m gleich dem internen CommunityKey ist, sonst FALSE + * decay(Long sec) // berechnet die Vergänglichkeit des internen Betrages mit der übergebenen Dauer sec nach der Formel: Betrag - (Betrag x 0.99999997802044727 ^ sec) + +} + +Damit erfüllt der Datentyp *Money* zum einen die interne Unterscheidung von Gradidos aus unterschiedlichen Communities und unterliegt andererseits den sonst aufgestellten Anforderungen der Gradido-Anwendung: + +* alle Gradidos unterliegen der Vergänglichkeit, egal aus welcher Community diese geschöpft wurden +* es gibt keinen Unterschied in der Wertigkeit von Gradidos, egal aus welcher Community diese stammen + +Ob ein Benutzer aus Community A mit einem anderen Benutzer aus Community B handeln möchte, sprich per Transaktion Gradidos austauschen möchte, unterliegt allein dem Benutzer selbst und wird von der Anwendung nicht unterbunden. Es sind zukünftig Ideen geplant ( siehe Abschnitt "Schutz vor Falschgeld"), dass die Anwendung den Benutzer hierbei unterstützt - Stichwort: Blacklisting, Bereinigung, etc. - aber dies bleibt dennoch völlig unter der Abwägung, Kontrolle und des Risikos alleine beim Benutzer selbst. + +Alle weiteren Auswirkungen im Zusammenspiel mit dem Datentyp Money auf die sonstigen Kontobewegungen und den möglichen unterschiedlichen CommunityKeys, wird, wie schon erwähnt, in der [Kontenverwaltung](./Kontenverwaltung.md) detailliert beschrieben. + #### Schutz vor Falschgeld - Blacklisting @@ -43,13 +74,11 @@ Aber grundsätzlich bleibt bei allen *Community-Gradido*-Währungen die Vergäng * Vergänglichkeitsbereinigung * 1. GDD anderer Communities nach Menge von wenig nach viel - | PR-Kommentare | | | ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Ulf 07.11.2021 | Wenn die geblacklisteten Coins prioritisiert vergehen kann der findige Angreifer den Verfall umgehen, indem er sich ungültige/blacklistete Coins erschafft, die dann genau seinem Verfall entsprechen. | | Claus-Peter 25.11.2021 | Das Kapitel "Schutz vor Falschgeld" ist wohl eher noch im Status "Brainstorming" zu verstehen. Hier wurden mögliche Regeln notiert, die noch nicht in ihrer Gänze durchdacht und konzipiert sind.
Der Punkt Vergänglichkeitsbereinigung sagt folgendes aus:
Kontostand am 01.01.2021:
- 100 GDD aus der eigenen Community
- 100 GDD aus Community A
- 100 GDD aus Community B
- gesamt 300 GDD

Nach einem Jahr ohne irgendwelche weiteren Transaktionen ergibt sich folgendes am 31.12.2021:
- Vergänglichkeit mit 365 Tagen bei 300 GDD = 150 GDD (nicht gerechnet, sondern 50%)
- die 150 GDD Vergänglichkeit als Tx-Buchung führt zu folgendem Kontostand:
* 100 GDD aus der eigenen Community
* 50 GDD aus der Community A
* 0 GDD aus der Community B
* gesamt 150 GDD

Soweit der Gedankengang zur Bereinigung des Kontos mit GDD aus anderen Communities. Das hat keine Auswirkung auf die Wertigkeit, sondern soll sich allein auf die Reduktion der Viefältigkeit an Community-Währungen im eigenen Konto führen. | - * Bezahl-Vorbereitung * Austausch von Blacklist zw. Teilnehmer * ggf. Übersteuern der Balcklist falls gewünscht @@ -59,20 +88,24 @@ Aber grundsätzlich bleibt bei allen *Community-Gradido*-Währungen die Vergäng | Ulf 07.11.2021 | Ich denke die vervielfachung von Coins und damit eine Ab/Auf-Wertung der jeweiligen Währen einer Community ist entgegen dem Konzept von Gradido. Gradido schafft eine stabile Zeit-Tausch-Einheit. Diese sollte Weltweit den gleichen Wert haben - warum sollte der Peruaner für seine Zeit weniger Gradido bekommen als ein Europäer? Das zementiert einfach weiterhin die bestehende Ordnung auf dem Planeten. Wollen wir das?
Daher mein Credo: Niemals einen Faktor zwischen Communities einführen. | | Claus-Peter 25.11.2021 | Den Kommentar verstehe ich nicht in Bezug auf die zitierten Dokument-Zeilen.
Wo finden sich Hinweise auf eine Ab/Auf-Wertung der Währung?
Das Blacklisting ist mit Sicherheit ein sehr sensibler Punkt und muss genauestens und tiefer durchdacht und konzipiert werden.
Ansonsten stimme ich dem Inhalt und deinem Credo voll zu. | - ### Anzeige und -Darstellung Da es also mehrere Communities geben wird, benötigt jede Community ihren eigenen Namen und gar ein Symbol oder Bild, um eine optische Unterscheidung oder gar eigenes Branding bei der Anzeige in den Systemen sicherzustellen. Für eine Aussendarstellung wäre eine Beschreibung der Community und ihre eigene Philosopie, was die Community auszeichnet hilfreich. Diese Werte müssen vom Community-Administrator gepflegt werden können. ### Mitgliederverwaltung -Für die Verwaltung von Community-Mitgliedern werden entsprechende Verwaltungsprozesse wie Registrierung, Login mit Autentifizierung, eine Benutzerverwaltung für neue, bestehende und ausscheidende Mitgleider benötigt. Die Benutzerverwaltung stellt zusätzlich die Anforderung, dass ein Community-Mitglied eindeutig identifizierbar ist und das Community übergreifend. Das bedeutet es kann eine Person immer nur einmal existieren und darf auch niemals in mehreren Communities gleichzeitig Mitglied sein. Denn es muss sichergestellt werden, dass eine Person sich keine unerlaubte Vorteile durch zum Beispiel mehrfache Geldschöpfung in mehreren Communities verschafft. +Für die Verwaltung von Community-Mitgliedern werden entsprechende Verwaltungsprozesse wie Registrierung, Login mit Autentifizierung, eine Benutzerverwaltung für neue, bestehende und ausscheidende Mitgleider benötigt. Die Benutzerverwaltung stellt zusätzlich die Anforderung, dass ein Community-Mitglied eindeutig identifizierbar ist und das Community übergreifend. Das bedeutet es kann eine Person immer nur einmal existieren und darf auch niemals in mehreren Communities gleichzeitig Mitglied sein. Denn es muss sichergestellt werden, dass eine Person sich keine unerlaubte Vorteile durch zum Beispiel mehrfache Geldschöpfung in mehreren Communities verschafft. -Die Details der Mitgliederverwaltung werden beschrieben im Dokument [BenutzerVerwaltung](.\BenutzerVerwaltung.md). +#### ToDo: + +Die oben beschriebene "global eindeutige Identifizierung" eines Mitglieds über alle Communities ist zunächst hier als grundlegend wünschenswertes Ziel aufgenommen. Da dies aber aktuell und vermutlich sogar in naher Zukunft technisch nicht möglich sein wird, gilt es Strategien zu entwerfen, die auf anderem Wege - z.B. Vertrauens- und Beziehungsgraphen auswerten und weitere SoftKriterien, die ein Mitglied identifizieren - eine quasi "übergreifende Identifzierung" per SoftSkills ermöglichen. + + +Die Details der Mitgliederverwaltung werden beschrieben im Dokument [BenutzerVerwaltung](./BenutzerVerwaltung.md). ### Community-Netzwerk -Ein grundlegender Ansatz des Gradido-Systems beinhaltet die Einstufung aller beteiligten Gradido-Communities als gleichberechtigte Einheiten. Diese bilden unterneinander ein Kommunikations-Netzwerk zum Austausch an Informationen, aber auch zum Aufbau eines gemeinsamen Verbundes weiterer Aktivitäten. +Ein grundlegender Ansatz des Gradido-Systems beinhaltet die Einstufung aller beteiligten Gradido-Communities als gleichberechtigte Einheiten. Diese bilden untereinander ein Kommunikations-Netzwerk zum Austausch an Informationen, aber auch zum Aufbau eines gemeinsamen Verbundes weiterer Aktivitäten. #### Vernetzung @@ -101,7 +134,6 @@ Durch das Community-Netzwerk erfolgt auch der sehr wichtige Prozess der Sichers | Ulf 07.11.2021 | Diese Anforderung ist technisch nicht zu erfüllen.
1. Eine Identifikation einer Person erfolgt immer mithilfe eines technischem Identifiers wie z.B. der Personalausweisnummer, EMail, Telefonnummer oder anderes. Schon durch diese Abstraktion kann ein Nutzer mehr als eine Identität aufbauen (2 Telefonnummer z.B.)
2. Die Prüfung auf Eindeutigkeit in einem dezentralen Netzwerk kann nicht sichergestellt werden, da Teile des Netzwerks zum Zeitpunkt der Prüfung nicht erreichbar oder gar unbekannt sein können.
3. Bedarf es den Austausch der Personen-Identifikation zwischen dem Communties: "kennst du email@domain.com?". Diese Daten können verschlüsselt werden z.B mit `hash(salt,email)` welche dann an jede Community geschickt werden: kennst du `hash(salt,email), salt`? Was dazu führt, dass jede Community den hash aller seiner EMails errechen muss - die Skalierung ist entsprechend schlecht.

Alternativ kann hier eine Blockchain eingesetzt werden, welche `hash(salt,email), salt` speichert und als dezentrales Nachschlagewerk für alle zugänglich ist. Hier erwarte ich ein Konzept, bevor wir das umsetzen können. Die Sicherheit der Nutzerdaten ist ebenfalls genau zu untersuchen, wenn wir das ganze ins Internet blasen. | | Claus-Peter 25.11.2021 | Ja da gebe ich dir nach heutigem Stand vollkommen Recht, dass es dafür derzeit keine 100% technische Lösung gibt.
Trotzdem ist das Thema fachlich gewünscht und wir müssen uns Gedanken machen, wie wir dafür eine technische Lösung finden können, die nahezu an die Anforderungen heranreicht. Genau deshalb endet dieser Absatz mit dem Hinweis auf die technsiche Konzeption. | - ### Hirarchische Community Um die Vision Gradido als Währung nicht nur in Communities als gemeinsame Interessensgemeinschaften zu etablieren, sondern auch für ganze Communen, Bundesländer, Nationen oder gar weltweit, bedarf es einer Strukturierung von Communities. Dazu dient das Konzept der *hierarchischen Community*, seinen Ursprung in der Abbildung des Föderalismus von Deutschland findet. Das bedeutet, dass eine baumartige Struktur von Communities aufgebaut werden kann, wie nachfolgendes Bild schemenhaft zeigt: @@ -111,7 +143,6 @@ Um die Vision Gradido als Währung nicht nur in Communities als gemeinsame Inter | Ulf 25.11.2021 | Ich denke Förderalismus wie in der Bundesrepublik ist mit Gradido nicht möglich. Es ergibt sich durch das Schwundgeld einfach keine Vorteile eines Förderalismus.
Szenario Straßenbau zwischen zwei Communties:
- Warum sollte Geld von beiden Communities auf ein drittes Konto fließen, wenn es dort doch nur vergeht?
- Ist es nicht realistischer, dass beide Communties sich auf den Straßenbau einigen und das Geld direkt an den Auftragnehmer überweisen, um dem Schwund so weit es geht zu entgehen.

Warum sollte sich eine Community einer anderen unterordnen? Was sind die Vorteile?
*Nur ein Gedanke* | | Claus-Peter 25.11.2021 | Ich kann deinen Gedanken und Bedenken folgen, doch andererseits kann ich auch dem Föderalismus etwas abgewinnen.
Klar würde sich eine Community schwer tun, sich einer anderen Community unterzuordnen. Doch genau da beginnt die Überlegung, wie man ein Community-übergreifendes Projekt organisieren könnte? Da gibt es schon Vorteile, die natürlich noch feiner konzipiert und ausformuliert werden müssen. Daher sollten wir die Hierarchie von Communities nicht im Vorhinein ausschließen. | - ![hierarchisches Community-Modell](./image/HierarchischesCommunityModell.png) Es wird somit zwischen zwei Communities aus direkt benachbarten Ebenen eine Parent-Child-Beziehung erzeugt. Dadurch gehen diese beiden Communities eine besondere Beziehung untereinander ein, die zu folgenden veränderten Eigenschaften und Verhalten der Parent- und der Child-Community führen: @@ -157,7 +188,7 @@ Für die Dreifach-Geldschöpfung verwaltet die Community drei Arten von Konten: Für jedes Mitglied der Community wird also ein eigenes AktiveGrundeinkommen-Konto verwaltet, auf das ein Drittel der monatlichen Geldschöpfung unter Einhaltung der AGE-Regeln fließt. Das Gemeinwohlkonto und das AUF-Konto existieren pro Community einmal und auf jedes der beiden Konten fließen monatlich die beiden anderen Drittel der Geldschöpfung. -Somit muss also eine Community für jede Kontoart die entsprechenden Kontoverwaltungsprozesse anbieten. Einmal in Verbindung pro Mitglied für das AGE-Konto und dann jeweils eine Verwaltung für das Gemeinwohlkonto und eine Verwaltung für das AUF-Konto. Die Berechtigungen für die Zugriffe auf die drei Kontoarten müssen ebenfalls in der Community gepflegt und kontrolliert werden. Das bedeutet die Community muss ihren Mitgliedern auf ihre eigenen AGE-Konten Zugriffsrechte erteilen und diese auch kontrollieren, so dass keine unerlaubten Zugriffe stattfinden können. Dann müssen in der Community bestimmte Mitglieder Sonderberechtigungen erhalten, um die Verwaltung des Gemeinwohlkontos und des AUF-Kontos durchführen zu können. Die Verwaltung der Berechtigungen ist wiederum alleine dem Community-Administrator erlaubt. Die Details der Kontenverwaltung ist im Dokument [KontenVerwaltung](.\KontenVerwaltung.md) beschrieben. +Somit muss also eine Community für jede Kontoart die entsprechenden Kontoverwaltungsprozesse anbieten. Einmal in Verbindung pro Mitglied für das AGE-Konto und dann jeweils eine Verwaltung für das Gemeinwohlkonto und eine Verwaltung für das AUF-Konto. Die Berechtigungen für die Zugriffe auf die drei Kontoarten müssen ebenfalls in der Community gepflegt und kontrolliert werden. Das bedeutet die Community muss ihren Mitgliedern auf ihre eigenen AGE-Konten Zugriffsrechte erteilen und diese auch kontrollieren, so dass keine unerlaubten Zugriffe stattfinden können. Dann müssen in der Community bestimmte Mitglieder Sonderberechtigungen erhalten, um die Verwaltung des Gemeinwohlkontos und des AUF-Kontos durchführen zu können. Die Verwaltung der Berechtigungen ist wiederum alleine dem Community-Administrator erlaubt. Die Details der Kontenverwaltung ist im Dokument [KontenVerwaltung](./KontenVerwaltung.md) beschrieben. ### Tätigkeitsverwaltung @@ -168,7 +199,6 @@ Hier handelt es sich um eine Verwaltung von Tätigkeitsbeschreibungen, die von d | Ulf 07.11.2021 | Was ist wenn Tätigkeit A in Community X vergütet wird und Tätigkeit B in Community Y:
- Ich übe Tätigkeiten A & B aus
- Muss ich mich entscheiden, welche Tätigkeit ich vergütet wissen will? (Stichwort eindeutige Registrierung im gesammten Netzwerk) | | Claus-Peter 25.11.2021 | Gute Frage!
Ich denke wir sollten im 1. Schritt damit beginnen, dass jede Community ihre eigene Aktivitätenliste pflegt. Es werden dann wohl gleiche Aktivitäten in mehreren Listen der verschiedenen Communities auftauchen.

Ich kann aber als Mitglied der Community A eine Tätigkeit A.X von einem Mitglied aus Community B bestätigt bekommen und natürlich auch von Mitgliedern aus meine Community A.

Dazu müssen dann natürlich die notwendigen Informationen zw. den Communities für eine Cross-Community-Bestätigung ausgetauscht werden. Das führt dann genau zu dem gewünschten Effekt, dass zw. zwei Communities ein Informationsaustausch stattfindet, der dann eine Aussage über den Cross-Handel und das Vertrauen ermöglicht. | - Zu der Liste der Tätigkeiten gibt es einen weiteren Prozess, der in dem Dokument [RegelnDerGeldschoepfung](./RegelnDerGeldschoepfung.md) näher beschrieben ist. Hier kann soviel erst einmal gesagt werden, dass die Tätigkeitenliste als Grundlage dient, damit ein Mitglied für seine erbrachten Leistungen für das Allgemeinwohl dann sein monatliches *Aktives Grundeinkommen* gutgeschrieben bekommt. Dieses Gutschreiben des AGEs unterliegt noch einer vorherigen Bestätigung von anderen Community- oder auch Community übergreifenden Mitgliedern. Somit erfolgt dadurch eine implizite Vernetzung der Mitglieder durch dieses aktive Bestätigen anderer Leistungen, was gleichzeitig wieder Vorraussetzung ist, um sein eigenes AGE zu erhalten. ### Berechtigungsverwaltung @@ -183,6 +213,18 @@ Mit der Vernetzung der Communities und dem gemeinsamen Handel zwischen Community In diesem Kapitel werden die Attribute beschrieben, die in einer Community zu speichern sind. +#### Key + +Der *Community-Key* dient zur technisch eindeutigen Identifizierung einer Gradido-Community in dem Multi-Community-Kommunikations Verbund. Der *Key* wird direkt bei der Inbetriebnahme einer neuen Community initialisiert - zum Beispiel als einfache UUID oder eine andere alphanummerische Sequenz - und während der Federation mit den schon existierenden Communities ausgetauscht. Falls dabei auffällt, dass irgendwelche Konflikte, wie ein exakt gleicher Key einer anderen Community oder gleiche URL bei unterschiedlichen Keys, etc. , exisitieren, dann wird der Key mit einem neuen Wert initialisiert bis alle Konflikte für eine Eindeutigkeit der Community im gesamten Community-Verbund beseitigt sind. + +Die Motivation dieses technischen Schlüssels liegt in der einmaligen Initialisierung bei der Community-Erstellung und in der Unveränderlichkeit danach. Alle anderen möglichen Attribute, die ebenfalls als Schlüssel für eine Community nutzbar wären, wie die URL oder evtl. der Name, können im Laufe der Existenz einer Community verändert werden. + +Der genaue Vorgang dieser *Key*-Initialisierung wird weiter unten im Kapitel "Neue Community erstellen" bzw. im technischen Konzept der Federation beschrieben. + +Die Verwendung des *Community-Key* wird auch zur Identifikation eines in der Community geschöpften Betrages - genannt *Currency-Key* - verwendet, so dass mit jedem Betrag gleichzeitig auch die Zuordnung zur Schöpfungs-Community hergestellt werden kann. Details hierzu siehe Kapitel "*Community-Gradido*" weiter oben. + +Das Attribut *Key* wird einmalig definiert und kann nicht mehr verändert werden. Es gibt keine Schreibrechte für nachträgliches Ändern, auch nicht von einem Administrator. + #### Name Das Attribut *Name* dient zur möglichst eindeutigen Benennung der Community. Er wird als Menschen lesbare Anzeige und als Unterscheidungskriterium bei mehreren Communities eingesetzt. Nur der Community Administrator kann diesen setzen und verändern. @@ -195,9 +237,9 @@ Das Attribut *Bild* wird für die Anzeige einer Community verwendet und kann nur Das Attribut *Beschreibung* ist ein Text, der die Philosophie der Community ausdrücken soll. Hier können sich die Community-Mitglieder eine gemeinsame Formulierung ausdenken, die nach ihrer Vorstellung den Kern und die Grundregeln ihrer Gemeinschaft am besten ausdrücken. Dies könnte wie eine Art Aussendarstellung für neue Mitglieder dienen. Aber nur der Community-Administrator hat die Schreib-Rechte für dieses Attribut. -#### Serverzuordnung +#### URL -Das Attribut *Serverzuordnung* ist technisch motiviert und dient zusammen mit dem Attribut Name der eindeutigen Identifikation einer Community. Bei der Gründung einer neuen Community muss festgelegt werden auf welchem Server diese Community gehostet wird - auf einem schon vorhandenen Server oder ein extra für diese Community neu aufgesetzter Server. Das Attribut Serverzuordnung muss aber für eine Virtualisierung und technische Skalierung auf mehrere Server-Instanzen vorbereitet sein, sodass keine direkte physische Hardware-Serverzuordnung hierdurch fixiert ist. Aber auch ein eventueller Umzug der Community von einem Server auf einen anderen Server muss möglich sein. Der Community-Administrator hat alleiniges Zugriffsrecht auf dieses Attribut. +Das Attribut *URL* ist eher technisch motiviert und dient zur weltweit eindeutigen Adressierung einer Community. Bei der Gründung einer neuen Community muss festgelegt werden über welche URL diese Community addressiert werden kann. Das Routing wo exakt die Community gehostet wird - auf einem schon vorhandenen Server oder ein extra für diese Community neu aufgesetztem Server - muss von der URL unabhängig in der Community-Konfiguration vom Administrator definiert werden. Das Attribut URL muss aber für eine Virtualisierung und technische Skalierung auf mehrere Server-Instanzen vorbereitet sein, sodass keine direkte physische Hardware-Serverzuordnung hierdurch fixiert ist. Aber auch ein eventueller Umzug der Community von einem Server auf einen anderen Server muss möglich sein. Der Community-Administrator hat alleiniges Zugriffsrecht auf dieses Attribut. #### Liste von Benutzer @@ -205,11 +247,11 @@ Dieses Listenattribut beinhaltet Benutzer-Elemente, die erfolgreich als Mitglied #### Gemeinwohlkonto -Das Attribut *Gemeinwohlkonto* dient als ein Konto-Element, das den Kontotyp Gemeinwohlkonto repräsentiert. Alle Kontobewegungen, wie Geldschöpfung, Geldtransfers, etc., die das Gemeinwohl dieser Community betreffen, werden über dieses Attribut abgewickelt. Details zu Kontobewegungen werden im Dokument [KontenVerwaltung](KontenVerwaltung.md) beschrieben und die Regeln und Vorgänge der Geldschöpfung sind im Dokument [RegelnDerGeldschoepfung](RegelnDerGeldschoepfung.md) zu finden. Auf dieses Attribut haben nur Mitglieder mit entsprechenden Zugriffsrechten die Erlaubnis und Möglichkeiten darauf Einsicht zu nehmen und Prozesse auszulösen. +Das Attribut *Gemeinwohlkonto* dient als ein Konto-Element, das den Kontotyp Gemeinwohlkonto repräsentiert. Alle Kontobewegungen, wie Geldschöpfung, Geldtransfers, etc., die das Gemeinwohl dieser Community betreffen, werden über dieses Attribut abgewickelt. Details zu Kontobewegungen werden im Dokument [KontenVerwaltung](./KontenVerwaltung.md) beschrieben und die Regeln und Vorgänge der Geldschöpfung sind im Dokument [RegelnDerGeldschoepfung](./RegelnDerGeldschoepfung.md) zu finden. Auf dieses Attribut haben nur Mitglieder mit entsprechenden Zugriffsrechten die Erlaubnis und Möglichkeiten darauf Einsicht zu nehmen und Prozesse auszulösen. #### Ausgleichs- und Umweltkonto AUF-Konto -Das Attribut *Ausgleichs- und Umweltkonto* dient als ein Konto-Element, das den Kontotyp AUF-Konto repräsentiert. Alle Kontobewegungen, wie Geldschöpfung, Geldtransfers, etc., die das AUF-Konto dieser Community betreffen, werden über dieses Attribut abgewickelt. Details zu Kontobewegungen werden im Dokument [KontenVerwaltung](KontenVerwaltung.md) beschrieben und die Regeln und Vorgänge der Geldschöpfung sind im Dokument [RegelnDerGeldschoepfung](RegelnDerGeldschoepfung.md) zu finden. Auf dieses Attribut haben nur Mitglieder mit entsprechenden Zugriffsrechten die Erlaubnis und Möglichkeiten darauf Einsicht zu nehmen und Prozesse auszulösen. +Das Attribut *Ausgleichs- und Umweltkonto* dient als ein Konto-Element, das den Kontotyp AUF-Konto repräsentiert. Alle Kontobewegungen, wie Geldschöpfung, Geldtransfers, etc., die das AUF-Konto dieser Community betreffen, werden über dieses Attribut abgewickelt. Details zu Kontobewegungen werden im Dokument [KontenVerwaltung](./KontenVerwaltung.md) beschrieben und die Regeln und Vorgänge der Geldschöpfung sind im Dokument [RegelnDerGeldschoepfung](./RegelnDerGeldschoepfung.md) zu finden. Auf dieses Attribut haben nur Mitglieder mit entsprechenden Zugriffsrechten die Erlaubnis und Möglichkeiten darauf Einsicht zu nehmen und Prozesse auszulösen. #### Verteilungsschlüssel der Dreifachen-Schöpfung @@ -237,7 +279,7 @@ Der Prozess *Neue Community erstellen* kann in zwei grundlegende Schritte unterg Um eine neue Community zu erstellen wird eine dafür speziell konzepierte Infrastruktur benötigt. Die technischen Details dieser Infrastruktur werden in der *technischen Infrastruktur Beschreibung* als eigenständiges Dokument dem Administrator der neuen Community zur Verfügung gestellt. Diese ist neben den Installationsskripten und Anwendungsdateien Teil des Auslieferungspaketes der Gradido-Anwendung. -Sobald der Administrator die geforderte Infrastruktur in Betrieb genommen und darauf die entsprechenden Installationsskripte ausgeführt hat erfolgt die eigentliche Erstellung und Registrierung der neue Community. Das heißt beim erstmaligen Start der Gradido-Anwendung wird automatisch der Prozess *Neue Community erstellen* gestartet. +Sobald der Administrator die geforderte Infrastruktur in Betrieb genommen und darauf die entsprechenden Installationsskripte ausgeführt hat, erfolgt die eigentliche Erstellung und Registrierung der neue Community. Das heißt beim erstmaligen Start der Gradido-Anwendung wird automatisch der Prozess *Neue Community erstellen* gestartet. #### Ablauf @@ -245,18 +287,133 @@ Der Prozess *Neue Community erstellen* wird entweder automatisiert beim erstmali ![Ablauf Neue Community erstellen](./image/Ablauf_Neue_Community_erstellen.png) +##### Einzelschritte + +Der oben grafisch dargestellte Ablauf wird in drei grobe Teile untergliedert: + +1. )den eigentlichen Community-Prozess "*neue Community erstellen*" (links in grün gehalten), in dem die Community spezifischen Attribute erfasst, geladen und/oder angelegt werden. Dazu gehören neben dem Erfassen der Community eigenen Attributen, das Laden von vordefinierten Standard-Daten wie die Tätigkeitsliste, Berechtigungen, etc. und optional als eigenständiger Prozess die Erfassung bzw das Anlegen von neuen Community-Mitgliedern. +2. das Starten der "*Federation*" als Hintergrundprozess, um die neu erstellte Community im Gradido-Community-Verbund bekannt zu machen. Dietechnischen Details der *Federation* werden im Dokument [Federation](../TechnicalRequirements/Federation.md " ") beschrieben. Dabei wird + * als erstes geprüft, ob in der eigenen Community die notwendigen Attribute wie Community-Key, URL und ggf. weitere korrekt initialisiert und gespeichert sind. Falls nicht wird der Hintergrundprozess mit einem Fehler abgebrochen + * dann werden die Attribute Community-Key und URL in eine *newCommunity*-Message gepackt und asynchron an den Public-Channel der Community-Federation des Gradido-Community-Verbundes gesendet + * Im Anschluss geht der Federation-Prozess in den "Lausch-Modus" auf eingehende Messages am *Public-Channel*. Die Verarbeitung von eingehenden Messages muss so sichergestellt werden, dass einerseits keine Message verloren geht auch bei DownTimes und andererseits, dass eine Message erst aus dem Public-Channel gelöscht wird, sobald diese vollständig abgearbeitet ist. Der Federation-Prozess lauscht auf Messages vom Typ *replyNewCommunity* und *newCommunity*, die bei Empfang entsprechend verarbeitet werden: + * *replyNewCommunity*-Messages werden auf die zuvor gesendete *newCommunity*-Message als Antwort von allen anderen schon im Verbund existierenden Communities erwartet. Je nach MessageState erfolgt eine unterschiedliche Weiterverarbeitung: + + * Ist der *MessageState = OK*, dann werden die erhaltenen Daten - Community-Key und URL - von der antwortenden Community in der Community-Datenbank als internen Liste für "*bekannte Communities*" gespeichert. Nach dem Speichern eines neue Community-Eintrags in dieser Liste wird asynchron der dritten und letzten Schritt der Federation *"Community-Communication"* als Hintergrundprozess getriggert und geht dann wieder zurück in den "Lausch-Modus" am Public-Channel. + * Ist der MessageState = requestNewKey, dann erfolgt eine Neugenerierung und Speicherung des eigenen Community-Keys, der dann erneut als *newCommunity*-Message auf den Public-Channel verschickt wird. Danach geht der Federation-Prozess wieder in den "Lauch-Modus", um auf Anworten der existierenden Communities zu warten. + * *newCommunity*-Messages werden von neu erstellten Communities im Rahmen derer Federation in den Public-Channel gesendet. Diese Messages sollten möglichst zeitnah von möglichst vielen schon existierenden Communities beantwortet werden. Dazu wird zuerst in der Community-Datenbank nach Einträgen gesucht, die den gleichen Community-Key aber eine unterschiedliche URL als zu den empfangenen Daten haben: + + * Sollte es einen solchen Eintrag geben, dann wird eine *replyNewCommunity*-Message erzeugt mit *MessageState = requestNewKey* und ohne weitere Daten in den Public-Channel zurückgesendet. Danach wird wieder in den "Lausch-Modus" am Public-Channel gewechselt. + * Sollte es keine solche Einträge geben, dann werden die eigenen Daten *Community-Ke*y und *URL* in eine *replyNewCommunity*-Message gepackt, der *MessageState = OK* gesetzt und direkt in den Public-Channel zurückgesendet. Danach wird wieder in den "Lausch-Modus" am Public-Channel gewechselt. + * | PR-Kommentar | | + | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | + | Ulf
24.03.2022 | Schitte 1 & 2 sind komplett durch die Federation an sich abgedeckt. Bereits über die Federatierungsschnittstelle wird eine Direktverbindung zum Peer aufgebaut um Daten auszutauschen (die URLs)
Mehr infos:
[https://en.wikipedia.org/wiki/Kademlia](https://en.wikipedia.org/wiki/Kademlia)[https://github.com/hyperswarm/dht](https://github.com/hyperswarm/dht) | + * +3. und die *"Community-Communication"* als Hintergrundprozess. Dieser liest zuerst die eigenen Community-Daten und geht dann per Direkt-Verbindung über die URL mit der neuen Community in Dialog, um sich zuerst gegenseitig zu authentifizieren und um dann die Community spezifischen Daten untereinander auszutauschen. Der logische Ablauf dieser Kommunikation soll wie folgt dargestellt ablaufen: + +![AuthenticateCommunityCommunication](.\image\AuthenticateCommunityCommunication.png " ") + +Die genaue Beschreibung der dazu verwendeten APIs beider Communities erfolgt in der technischen Konzeption [CommunityCommunication](../TechnicalRequirements/CommunityCommunication.md). Wie der obigen Grafik zu entnehmen ist, erfolgt bei einer neuen Community eine einmalig zu durchlaufende Aufruf-Kette zur Authentifizierung (1. Sequenz), deren Ergebnisse in den beteiligten Communities gespeichert bleiben. Der danach folgende Request (2. Sequenz) wird vor jeder neu initiierten Kommunikation zwischen zwei Communities notwendig, um ein zeitlich gültiges Authentifizierungs-Token für die nachfolgenden fachlichen Requeste (3. Sequenz) zu erhalten. + +Die aller erste fachliche Kommunikation zwischen einer neu erstellten und einer schon existierenden Community ist die Annäherung der beiden Communities, in dem sie sich gegenseitig mit dem Request "*familiarizeCommunity*" ihre eigenen Daten in Form eines *CommunityTO*-TransferObjektes austauschen. Im zweiten Schritt erfragen sie sich gegenseitig, wie sie zukünftig miteinander Handel treiben wollen. Die Details über die Aushandlung des Tradinglevels der beiden Communities sind hier im Dokument im Anwendungsfall [Communities Tradinglevel aushandeln](#Communities-Tradinglevel-aushandeln) beschrieben. + #### Ende Status 1. Community-Infrastruktur ist installiert und aktiv 2. neue Community ist erzeugt und Daten in der Community-DB gespeichert -3. der Hintergrundprozess "Community-Vernetzung" ist am Laufen +3. der Hintergrundprozess *Federatio*n ist am Laufen * die initiale "newCommunity-Msg" mit den eigenen Community-Daten ist in den Public-Channel versendet - * ein Listener lauscht am Public-Channel auf Antworten (replyNewCommunityMsg) der schon existenten Communities - * ein Listener lauscht am Public-CHannel auf initiale "newCommunity-Msg" anderer neuer Communities -4. mit dem ersten Empfangen einer Reply-Msg einer anderen Community, wird der Community-Connection Prozess gestartet, der mit jedem Empfang von neuen Community-Daten eine P2P-Verbindung zu dieser Community aufbaut, um direkt detaillierte Daten auszutauschen -5. die vordefinierte Tätigkeitsliste ist geladen -6. die vordefinierten Berechtigungen sind aktiv -7. optional sind schon Mitglieder erfasst und in der Datenbank gespeichert + * ein Listener lauscht am Public-Channel auf Antworten (*replyNewCommunity*-Msg) der schon existenten Communities + * ein Listener lauscht am Public-Channel auf initiale "*newCommunity*-Msg" anderer neuer Communities +4. mit dem ersten Empfangen einer *replyNewCommunity*-Msg einer anderen Community, wird der *Community-Communication* Prozess gestartet und ist am Laufen. +5. mit jedem Empfang einer *replyNewCommunity*-Msg haben sich die involvierten Communities direkt miteinander verbunden, sich gegenseitig authentifiziert (Austausch der public-Keys) und weitere Daten ausgetauscht +6. die vordefinierte Tätigkeitsliste ist geladen +7. die vordefinierten Berechtigungen sind aktiv +8. optional sind schon Mitglieder erfasst und in der Datenbank gespeichert + +#### Fehlerfälle + +### Communities Tradinglevel aushandeln + +Im Anwendungsfall *Communities Tradinglevel aushandeln* geht es darum, dass die beiden involvierten Communities sich gegenseitig anfragen und bestätigen auf welchem Level bzw. mit welchen Detailtiefe sie zukünftig Datenaustauschen und miteinander interagieren. + +Diese Aushandlung bedarf auf beiden Seiten administrative als auch strategische Interaktionen und Entscheidungen. Daher kann dies kein synchroner Anfrage-Bestätigungs-Loop sein, sondern es muss zwischen einer Tradinglevel-Anfrage und ihrer Bestätigung ein Zeitfenster von gegebenenfalls mehreren Tagen vorgesehen werden. + +Folgende Tradinglevels sind derzeit vorgesehen: + +* Mitglieder-Details senden : Kennung, ob die *Community-A* zukünftig weitere Details über die eigenen Mitglieder an *Community-B* sendet +* Mitglieder-Details empfangen : Kennung, ob die *Community-A* zukünftig weitere Details über die Mitglieder von *Community-B* empfängt +* Geld empfangen : Kennung, ob *Community-A* zukünftig Geld an *Community-B* sendet +* Geld senden : Kennung, ob *Community-A* zukünftig Geld von *Community-B* empfängt +* Aktivitäten senden : Kennung, ob *Community-A* zukünftig die offenen Aktivitäten seiner Mitglieder an *Community-B* zur Bestätigung sendet +* Aktivitäten empfangen : Kennung, ob *Community-A* zukünftig die offenen Aktivitäten der Mitglieder von *Community-B* zur Bestätigung empfängt +* Backup senden : Kennung, ob *Community-A* zukünftig Daten an *Community-B* als Backup sendet +* Backup empfangen : Kennung, ob *Community-A* zukünftig Daten von *Community-B* als Backup empfängt + +#### Vorraussetzungen + +Bevor *Community-A* mit dem Aushandeln des Tradinglevels mit *Community-B* beginnen kann, muss in der *Community-A* erst einmal selbst Konsens geschaffen sein, welchen Level mal haben und auch unterstützen will. Das bedeutet der Administrator und die Community-Verantwortlichen müssen in der *Community-A* mit den eigenen Mitgliedern sich darüber einig sein, wie sie zukünftig mit der *Community-B* und deren Mitgliedern im Vertrauen und im Austausch stehen wollen. + +Sobald diese Vorstellung des zukünftigen Austausches klar ist und die administrativen und organisatorischen Vorraussetzungen dazu getroffen und umgesetzt sind - z.B. bei Bereitschaft für Backup-Daten empfangen müssen auch ausreichend Resourcen für diesen Dienst vorhanden sein oder bei Mitglieder-Details senden müssen die Mitglieder damit einverstanden sein - kann die Anfrage von *Community-A* an *Community-B* gestartet werden. + +#### Ablauf + +##### PR-Kommentar: + +Ulf: + +Das Teilnehmen an der Federation startet diesen Prozess + +Claus-Peter: + +Nicht ganz, ja bei der Federation wird es einen Austausch des +Trading-Levels geben. Dazu muss dieser aber vorher in den Communities +insbesonderen der neu hinzugekommenen Community schon definiert bzw. +konfiguriert sein. Und diese Konfiguration ist entweder in der Community +schon abgeklärt oder es handelt sich lediglich um die +Default-Konfiguration und muss im Laufe der Zeit aktualisierbar sein. + + +Community-A startet den Prozess des Tradinglevel-aushandelns indem der Administrator die zuvor geschaffenen Vorraussetzungen entsprechend in der Community internen Datenbank erfasst. Ob dies per Configurationsdatei oder über ein Admin-Dialog umgesetzt wird, bleibt erst einmal offen. + +Um den eigentlichen fachlichen Request mit den vorhandenen Tradinglevel-Daten aufrufen zu können, muss zuerst die Kommunikation zwischen den Communities zur Authentifizierung mit dem Request *openCommunication* abgesetzt werden. Nach Erhalt eines gültigen JWT-Tokens wird dann die Anfrage von *Community-A* nach dem gewünschten Trading-Level mit *Community-B* über den Aufruf requestTradingLevel abgesetzt. Als direktes Ergebnis dieser Anfrage erhält Community-A nur die Information, ob Community-B die TradingLevel-Daten erfolgreich erhalten hat oder nicht. + +In *Community-A* selbst wird intern vermerkt, dass die TradingLevel-Anfrage an *Community-B* abgeschickt und offen ist. + +In *Community-B* sind die angefragten Tradinglevel-Daten von *Community-A* in der internen Community-Liste gespeichert und müssen nun die Community internen Abstimmungen und Vorbereitungen erfolgen. Diese kann durchaus über einen längeren Zeitraum von mehreren Tagen dauern. + +##### PR-Kommentar + +Ulf: + +Das ist ein kontinuierlicher Prozess, da er einseitig erfolgt und nur das zutun einer Partei benötigt. + +Claus-Peter: + +Wenn dies ein kontinuierlicher Prozess ist, dann wird es auch auf beiden +Seiten Änderungen geben können. Klar ist, dass eine Community-A den +Trading-Level von Community-B abfragt. Die Abfrage wird ohne Zutun von +Community-B Mitgliedern automatisch ablaufen. Aber die evtl. Änderungen, +die vor einer Aktualisierungsabfrage passieren, werden aber in der +Community unter den Mitgliedern oder zumindest unter den +Support/Admins/Vorständen o.ä abgestimmt. Und diese Abstimmungsaufwände +kosten bestimmt einige Zeit, bis diese in der Trading-Level-Konfig +einpflegbar und dann auch von anderen Communities abfragbar sind. + + +Sobald in *Community-B* das Abstimmungsprozedere und die Vorstellungen des zukünftigen Interagierens mit *Community-A* beendet und erfasst sind, startet *Community-B* selbst die Bestätigung der offenen Anfrage von *Community-A*. Dazu muss auch wieder erst die Kommunikation authentifiziert und eröffnet werden, um ein gültiges JWT-Token zu erhalten. Mit dem Token und den in *Community-B* abgestimmten TradingLevel-Daten erfolgt über *confirmTradingLevel* die Übertragung der Daten nach *Community-A*. Mit Erhalt und Speicherung der Daten erfolgt eine erste Auswertung für den direkten Response an *Community-B*. Dieses Ergebnis für *Community-B* kann folgende Konstellationen haben: + +* OK : bei vollständiger Übereinstimmung des zuvor angefragten TradingLevels +* ERROR : der Empfang und die Prüfung der erhaltenen TradingLevel-Daten sind technisch fehlerhaft. *Community-B* muss den *confirmTradingLevel*-Request ausführen. +* RESERVE : die Daten wurden erfolgreich empfangen, die Prüfung ergab Unterschiede, der Trading-Level wird unter Vorbehalt bis zur abschliessenden Prüfung erst einmal so gestartet. Eventuelle Änderungen erfolgen mit einem erneuten *Request/ConfirmTradingLevel*-Loop. +* REJECT : die Daten wurden erfolgreich empfangen, die Prüfung ergab gravierende Unterschiede, der Trading-Level muss erneut vollständig ausgehandelt werden. Bis dahin werden alle Trading-Interaktion zwischen den beiden Communities abgelehnt. + +#### Ende Status + +Als Ende diese Anwendungsfalles kann es folgende Konstellationen geben: + +* die Abstimmung und der Austausch waren erfolgreich, so dass nun in beiden Communities der gleiche TradingLevel zu jeder involvierten Community gespeichert ist. Die Interaktionen des ausgehandelten TradingLevels sind nicht aktiviert, es stehen lediglich die Informationen bereit welche Interaktionen zwischen den Communities A und B erwünscht und bestätigt sind. +* die Abstimmung und der Austausch wurden unter Vorbehalt angenommen. Es sind in beiden Communities der gleiche TradingLevel zu jeder involvierten Community gespeichert. Die Interaktionen des ausgehandelten TradingLevels sind nicht aktiviert, es stehen lediglich die Informationen bereit welche Interaktionen zwischen den Communities A und B ausgehandelt sind und unter Vorbehalt angewendet werden können. +* Die Abstimmung und der Austausch sind erfolgt, im Ergebnis sind die TradingLevels der beiden Communities zu unterschiedlich, so dass diese abgelehnt wurden. Die Daten bleiben aber auch in diesem Fall in der jeweiligen Community gespeichert, so dass für zukünftige Anfragen schon einmal ein Eindruck der jeweiligen Community vorhanden ist. #### Fehlerfälle @@ -331,78 +488,3 @@ Der Prozess *Neue Community erstellen* wird entweder automatisiert beim erstmali #### Ende Status #### Fehlerfälle - -# Besprechung 19.08.2021 19:00 mit Bernd - -## Kreis-Mensch-Sein-Community - -Felix Kramer - -noch keine eigene Währung, wollen gerne Gradido - -haben auch aktives Grundeinkommen - -passt aber nicht ganz zur Gradido Philosophie, weil Gemeinwohlleistung zu unterschiedlich bewertet werden. - --> Colored Gradido? - -Community-Creation - -GDD1 (gold) ist existent - -Felix baut GGD2-Infrastruktur auf - -* Frage: willst du GDD1(gold) oder eigene Währung? -* Antwort: nein ich will eigene GDD2 (rot) - * muss neue Währung erzeugen -* Antwort: ja, dann Anfrage an GDD1, dass GDD2 auch Goldene GDD1 schöpfen darf? - * Ja wird akzeptiert - * dann bekommt GDD2 die Lizenz goldene GDD1 schöpfen kann - -Kommt später heraus, dass GDD2 nicht mehr den goldenen Regeln entspricht, dann muss die Lizenz zum goldene GDD1 Schöpfen für GDD2 gesperrt werden. - -Bisher geschöpfte goldene GDD2 beleiben aber erhalten. - -Es darf keine Markierung des Bot-Mitglieds geben, da Missbrauch/Fehler möglich - -Identität für ein Mitglied muss Human/Nichthuman enthalten - -GDD2 muss mit Lizenzentzug wechseln auf eigene Währung um weiterschöpfen zu können. - -Mitgliederwechsel in andere Community muss dann auch Währungswechsel berücksichtigen. - -Bestcase: 1 Blockchain pro Währung - -GDD1(gold) existent - -GDD2(gold) soll gegründet werden - -GDD2 baut Infrasturktur auf - -Frage an GDD2, ob goldene oder andere? - -### Tätigkeiten, die von der Community aktzeptiert werden - -Nachweise für durchgeführte Tätigkeiten, bevor diese dem AGE-Konto gutgeschrieben werden? - -Liste der Tätigkeiten muss von Community erstellt, bestätigt und verwaltet werden - -Bei Tätigkeit von x Stunden für das AGE muss aus der Liste die passende Tätigkeit gewählt werden und per Nachweis (andere Mitglieder, Video, o.ä.) - -Bei Krankheit o.ä. muss es aber möglich sein, dass dennoch Geld auf das AGE-Konto kommt. - -| PR-Kommentar | | -| ---------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Ulf 07.11.2021 | Definition? Ist Faulheit eine Krankheit? | -| Claus-Peter 25.11.2021 | :-) Das kommt auf die solidarische Einstellung der Community an ;-)

da drängt sich mir die Gegenfrage auf: Bis zu welchem Alter bekommt ein Kind sein AGE-Geld geschöpft nur durch seine blos Existenz? Oder andersherum, ab und bis zu welchem Alter muss eine Gegenleistung erbracht werden, Stichworte: unbeschwerte Kindheit und wohlverdiente Altersruhe? | - - -Kontaktförderung durch gewichtete Tätigkeitsbestätigung ( bei mind. 2 Bestätigungen pro Tätigkeit muss mind. ein neues Mitglied dabei sein) - -Liste von Mitgliedern, die ich bestätigt habe: - -* Kontaktpflege -* Gewichtung -* Vernetzung - -Ricardo Leppe Podcast Lern und Memotechniken diff --git a/docu/Concepts/BusinessRequirements/CommunityVerwaltung.pdf b/docu/Concepts/BusinessRequirements/CommunityVerwaltung.pdf new file mode 100644 index 000000000..02e383716 Binary files /dev/null and b/docu/Concepts/BusinessRequirements/CommunityVerwaltung.pdf differ diff --git a/docu/Concepts/BusinessRequirements/graphics/Ablauf_Neue_Community_erstellen.drawio b/docu/Concepts/BusinessRequirements/graphics/Ablauf_Neue_Community_erstellen.drawio index 161e78dbd..e3d691565 100644 --- a/docu/Concepts/BusinessRequirements/graphics/Ablauf_Neue_Community_erstellen.drawio +++ b/docu/Concepts/BusinessRequirements/graphics/Ablauf_Neue_Community_erstellen.drawio @@ -1,20 +1,23 @@ - + - - + + - - + + + + + - + @@ -26,11 +29,11 @@ - + - + @@ -38,7 +41,7 @@ - + @@ -54,74 +57,62 @@ - + - + - - - - - - - - - + - - + + - + - - + - + - + - - + + - - + + - + - + - + - - + + - - - - + @@ -146,9 +137,9 @@ - + - + @@ -163,140 +154,238 @@ - + - + - - + + - + - - + + + - + - - + + - + - + - + - - + + - - - - - - - - - - - - - - + - + + + + + + + + + + - + - - + + - + + + + + + + - + + + - - - - + + - - + + - - - - - - - - - - + - + - + - - + + - + - - - - + - - + + - + - - - + + + + + + + + + + + + + + + + + + - - - - - + + - - - - + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docu/Concepts/BusinessRequirements/graphics/AuthenticateCommunityCommunication.drawio b/docu/Concepts/BusinessRequirements/graphics/AuthenticateCommunityCommunication.drawio new file mode 100644 index 000000000..201002cdc --- /dev/null +++ b/docu/Concepts/BusinessRequirements/graphics/AuthenticateCommunityCommunication.drawio @@ -0,0 +1,286 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docu/Concepts/BusinessRequirements/graphics/Staging_IstAnalyse.drawio b/docu/Concepts/BusinessRequirements/graphics/Staging_IstAnalyse.drawio new file mode 100644 index 000000000..85ebbed67 --- /dev/null +++ b/docu/Concepts/BusinessRequirements/graphics/Staging_IstAnalyse.drawio @@ -0,0 +1,253 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docu/Concepts/BusinessRequirements/graphics/Staging_SollPlanung.drawio b/docu/Concepts/BusinessRequirements/graphics/Staging_SollPlanung.drawio new file mode 100644 index 000000000..b5e7c1054 --- /dev/null +++ b/docu/Concepts/BusinessRequirements/graphics/Staging_SollPlanung.drawio @@ -0,0 +1,754 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docu/Concepts/BusinessRequirements/image/Ablauf_Neue_Community_erstellen.png b/docu/Concepts/BusinessRequirements/image/Ablauf_Neue_Community_erstellen.png index 82f1664e3..95aae4d5d 100644 Binary files a/docu/Concepts/BusinessRequirements/image/Ablauf_Neue_Community_erstellen.png and b/docu/Concepts/BusinessRequirements/image/Ablauf_Neue_Community_erstellen.png differ diff --git a/docu/Concepts/BusinessRequirements/image/AuthenticateCommunityCommunication.png b/docu/Concepts/BusinessRequirements/image/AuthenticateCommunityCommunication.png new file mode 100644 index 000000000..c33cc7109 Binary files /dev/null and b/docu/Concepts/BusinessRequirements/image/AuthenticateCommunityCommunication.png differ diff --git a/docu/Concepts/BusinessRequirements/image/Staging_IstAnalyse.png b/docu/Concepts/BusinessRequirements/image/Staging_IstAnalyse.png new file mode 100644 index 000000000..e435de622 Binary files /dev/null and b/docu/Concepts/BusinessRequirements/image/Staging_IstAnalyse.png differ diff --git a/docu/Concepts/BusinessRequirements/image/Staging_SollPlanung.png b/docu/Concepts/BusinessRequirements/image/Staging_SollPlanung.png new file mode 100644 index 000000000..cb76b77bc Binary files /dev/null and b/docu/Concepts/BusinessRequirements/image/Staging_SollPlanung.png differ diff --git a/docu/Concepts/Snippets/Brainstorming_Tätigkeiten.md b/docu/Concepts/Snippets/Brainstorming_Tätigkeiten.md new file mode 100644 index 000000000..32b7578e6 --- /dev/null +++ b/docu/Concepts/Snippets/Brainstorming_Tätigkeiten.md @@ -0,0 +1,24 @@ +### Tätigkeiten, die von der Community aktzeptiert werden + +Nachweise für durchgeführte Tätigkeiten, bevor diese dem AGE-Konto gutgeschrieben werden? + +Liste der Tätigkeiten muss von Community erstellt, bestätigt und verwaltet werden + +Bei Tätigkeit von x Stunden für das AGE muss aus der Liste die passende Tätigkeit gewählt werden und per Nachweis (andere Mitglieder, Video, o.ä.) + +Bei Krankheit o.ä. muss es aber möglich sein, dass dennoch Geld auf das AGE-Konto kommt. + +| PR-Kommentar | | +| ---------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Ulf 07.11.2021 | Definition? Ist Faulheit eine Krankheit? | +| Claus-Peter 25.11.2021 | :-) Das kommt auf die solidarische Einstellung der Community an ;-)

da drängt sich mir die Gegenfrage auf: Bis zu welchem Alter bekommt ein Kind sein AGE-Geld geschöpft nur durch seine blos Existenz? Oder andersherum, ab und bis zu welchem Alter muss eine Gegenleistung erbracht werden, Stichworte: unbeschwerte Kindheit und wohlverdiente Altersruhe? | + +Kontaktförderung durch gewichtete Tätigkeitsbestätigung ( bei mind. 2 Bestätigungen pro Tätigkeit muss mind. ein neues Mitglied dabei sein) + +Liste von Mitgliedern, die ich bestätigt habe: + +* Kontaktpflege +* Gewichtung +* Vernetzung + +Ricardo Leppe Podcast Lern und Memotechniken diff --git a/docu/Concepts/TechnicalRequirements/CommunityCommunication.md b/docu/Concepts/TechnicalRequirements/CommunityCommunication.md new file mode 100644 index 000000000..dced0e585 --- /dev/null +++ b/docu/Concepts/TechnicalRequirements/CommunityCommunication.md @@ -0,0 +1,469 @@ +# Community Communication + +This document contains the detailed descriptions of the public API of a community. + +## Authentication/Autorization of new Community + +Each public API of a community has to be authenticated and autorized before. + +### Variant A: + +This could be done by following the *OpenID Connect* protocoll. To fullfil these security requirements a separate security service has to be part of the Gradido-application. + +Following the link [OpenID Connect](https://www.npmjs.com/package/openid-client) there can be found a server-side OpenID relying party implementation for node.js runtime. + +The authentication of communities base on the community-attributes *key* and *URL*, which where exchanged during the *federation process* before. In concequence a community that hasn't execute his federation well will be unknown for other communities and can't be authenticated and autorized for further cross community API calls. + +### Variant B: + +A similar solution of authentication to variant A but **without autorization** can be done by using private and public key encryption. The *community creation* process will create a private and public key and store them internally. As the third step of the federation the *community communication* background process of the new *community-A* will be startet and a sequence of service invocations will exchange the necessary security data: + +![../BusinessRequirements/image/AuthenticateCommunityCommunication.png](../BusinessRequirements/image/AuthenticateCommunityCommunication.png) + +**1.Sequence** + +1. the new *community-A* encrypt the community key of the existing *community-B* with its own privat key. Then it invokes the service *authenticateCommunity* at *community-B* by sending the own community key, the encrypted community key of *community-B* and a redirect URI back to *community-A* as input data. The *community-B* will search the given community key of *community-A* in the internally stored list of communities, which are a result of the previous *federation process* collected over a different medium. +2. If in *community-B* the given community key of *community-A* is found, a generated one-time code is stored together with the given encrypted community key in the community-entry of community-A, till an invocation of the service *verifyOneTimeCode* with this one-time-code. The one-time-code is passed back to the given Redirect URI of the *community-A*. +3. *Community-A* will send with the next invocation to *community-B* the received one-time code and the own public key by requesting the service *verifyOneTimeCode* at *community-B*. +4. *Community-B* will verify the given one-time-code and if valid, decrypt the previous received and encrypted community key from step 1 of the invocation-chain by using the given public key from *community-A*. If the decrypted community-key is equals the own community key, the public key of *community-A* is stored in the entry of *community-A* of the internal community list. As response of the *verifyOneTimeCode* the *community-B* will send back his own public key to *community-A*. +5. *Community-A* will store the received public key of *community-B* in the corresponding entry of the internal community-list. + +| PR-Kommentar | zu Punkt 1 in der List oben | +| :--------------------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Ulf
24.03.2022 | ComA.org --- (pubKeyA, SaltA, privKeyA(SaltA,pubKeyB) ---> ComB.org
+``` +<--- (pubKeyB, SaltB, privKeyB(SaltA, SaltB, pubKeyA) ---- +``` + +

Das wäre mien Vorschlag, aber ich bin kein Crypto-Experte.
Tritt ein Validierungsfehler auf, wird der Call als unauthorized markiert.

Vorteil: statless
Nachteil: Rechenaufwendig, kann umgangen werden, wenn Salt ein Datum ist und ein solcher Salt eine Gültigkeitsdauer hat - e.g. 10 min. Dann wäre aber SaltB unpraktikabel - für die Validierung auf ComB Seite wäre, dann eine Prüfung des Salt-Datums notwendig um replay Attacken zu verhindern. | +| Claus-Peter
24.03.2022 | Da scheint in deinem Bild ein Henne-Ei-Problem zu sein:
Wie kommt ComA für seinen ersten Request an ComB zu dem pubKeyB?
Den hat ComA zu dem Zeitpunkt doch noch gar nicht, oder?
Daher benötigt man einen mehr-schrittigen Handshake zw. den Communities, um diese Keys auszutauschen. | +| **PR-Kommentar** | **zu Punkt 2 in der Liste oben** | +| Ulf
24.03.2022 | explain? Out key exchange was done befor, we just need to proof that our current data is correct?! | +| Claus-Peter
24.03.2022 | Das ist genau was ich oben schon meinte mit dem Henne-Ei-Problem:

Wir sollten dazu noch einmal genau den Ablauf zu den einzelnen Zeitpunkten und welche Daten in welcher Community zu den Zeitpunkten vorliegen bzw über welche Kanäle diese ausgetauscht werden.
Ich glaube ich skizziere dies noch einmal möglichst einfach auf... | + + + + + +The result of this invocation chain is the public key exchange of the involved communities, which is the foundation to authenticate a future cross community communication - see Sequnce 2 and 3. + +To reach in Variant B nearly the same security level as in Variant A each community has to integrate several components to process this invocation chain like Variant A does. + +### Variant C: + +The third Variant exchange the all necessary data directly without the step in between returning a one-time code per redirection URI: + +1. the new *community-A* encrypt the community key of the existing *community-B* with its own privat key. Then it invokes the service *authenticateCommunity* at *community-B* by sending the own community key, the encrypted community key of *community-B* and its own public key as input data. The *community-B* will search the given community key of *community-A* in the internally stored list of communities, which are a result of the previous *federation process* collected over a different medium. +2. If in *community-B* the given community key of *community-A* is found and if the decryption of the given encrypted community key with the given public key is equals the own community key, the public key of *community-A* is stored in the entry of *community-A* of the internal community list. As response of the *authenticateCommunity* the *community-B* will send back his own public key to *community-A*. +3. *Community-A* will store the received public key of *community-B* in the corresponding entry of the internal community-list. + +Variant C is quite similar to Variant B, but to exchange all security relevant data in a single request-response-roundtrip bears more security risks and should be avoided. + +## Service: "Authenticate Community" + +This service must be invoked at first - see Variant B above - to exchange the security relevant data before further cross community communication can be done. + +The third step of the *federation process* starts the background process for community communication. As result of the previous federation steps the new community has received from at least one existing community the URL and the community key. + +After receiving the input data the service answers directly with an empty response. Then it searches for the attribute "community-key-A" in the internal community list the entry of *community-A*. If the entry of *community-A* could be found with this key, a new one-time-code is generated and stored together with the attribute "community-key-B" till the invocation of service "verifyOneTimeCode". With the given redirection URI a callback at *community-A* is invoked by sending the generated One-Time-Code back to *community-A*. + +### Route: + +POST https:///authenticateCommunity + +### Input-Data: + +``` +{ + "community-key-A" : "the community-key of the new community-A" + "community-key-B" : "the community-key of the community-B, replied during the federation, encrypted by the private key of community-A" + "redirectionURI" : "the URI for the redirection callback" +} +``` + +### Output-Data: + +* none +* redirection URI: "one-time-code" : "one-time usable code with short expiration time as input for the service *verifyOneTimeCode*" + +### Exceptions: + +*MissingParameterException* if any of the parameter attributes is not initialized. + +*UnknownCommunityException* if the community search with the value of parameter "community-key-A" could not find a matching community entry with this key. + +## Service: "Verify OneTimeCode" + +This service must be invoked directly after getting the *one-time code*, because this code has a very short expiration time. Together with the public key of *community-A* the one-time code is send as input data to *community-B*. The service verifies the given *one-time code* and if valid it decrypt with the given *public key* the previous receive *community-key-B* from the request *authenticateCommunity*. If this decrypted community-key is equals the own community-key the *public key* is stored in the community-entry of *community-A* of the internal community-list. + +### Route: + +POST https:///verifyOneTimeCode + +### Input-Data: + +``` +{ + "one-time-code" : "one-time code with short expiration, received from community-B per redirect URI" + "public-key" : "the public key of the new community (community-A)" +} +``` + +### Output-Data: + +``` +{ + "public-key" : "the public key of community-B" +} +``` + +### Exceptions: + +*MissingParameterException* if one of the parameter attributes is not initialized. + +*InvalidOneTimeCodeException* if the one-time-code is expired, invalid or unknown. + +*SecurityException* if the decryption result with the given parameter *public-key* and previous receive *community-key-B* from the request *authenticateCommunity* doesn't match with the own community-key. + +## Service: "open Communication" + +This service must be used to start a new communication session between two communities to authenticate with the returned JWT-Token further requests. + +*Community-A* will communicate with *community-B*, then *community-A* has to encrypt with its own private key the community key of *community-B*, put its own community-key and the encrypted community-key of *community-B* as input data and start with *openCommunication* to get a valid JWT-Token from *community-B*. + +In *community-B* the given "community-key-A" will be used to search in the internal community-list for the community-entry with this key. If it exists the corresponding public key is used to decrypt the given parameter "community-key-B". If the decrypted result is equals the own community-key a JWT-Token is generated with a preconfigered expiration time for a community-communication-session. The token is stored internally and returned as result for *community-A.* + +### Route: + +POST https:///openCommunication + +### Input-Data: + +The requesting *community-A* will initialize these input data: + +``` +{ + "community-key-A" : "the community-key of the community-A" + "community-key-B" : "the community-key of the community-B, encrypted by the private key of community-A" +} +``` + +### Output-Data: + +``` +{ + "token" : "valid JWT-Token with a preconfigered expiration time" +} +``` + +### Exceptions: + +*MissingParameterException* if one of the parameter attributes is not initialized. + +*UnknownCommunityException* if the community search with the value of parameter "community-key-A" could not find a matching community entry with this key. + +*SecurityException* if the decrypted community-key-B will not match the own community key. + +## Service: "Familiarize communities" + +This request is used to exchange data between an existing and a new community. It will be invoked by the existing community, which received a valid *newCommunity*-Message from a new community during the federation process. + +The invocation from the federation process gives the *Community-Key* and *New_Community_URL* as input parameters, which are used to get the *Security-Token* from the SecurityService. + +The exchanged data will be transferred as a *CommunityTO* transferobject in both directions as input and output parameter. + +### Route: + +POST https:///familiarizeCommunity/`` + +### Input-Data: + +The *existing community* will collect its own data and transferre it as + +``` +{ + CommunityTO { + "key" : "community-key", + "name" : "name of community", + "description" : "description of community", + "icon" : "picture of community", + "birthday" : "day of community creation", + "members" : "amount of members", + "known_communities" : "amount of known communities", + "trading_communities" : "amount of communities the members trade with" + } +} +``` + +### Output-Data: + +The *new community* will save the received data and returns its own collected data as + +``` +{ + CommunityTO { + "key" : "community-key", + "name" : "name of community", + "description" : "description of community", + "icon" : "picture of community", + "birthday" : "day of community creation", + "members" : "amount of members", + "known_communities" : "amount of known communities", + "trading_communities" : "amount of communities the members trade with" + } +} +``` + +### Exceptions: + +A *SecurityException* will be thrown, if the security-accesstoken is not valid or the if internal autorization rules like black-listings will not allow access. + +In case the transferred community-key from the service-consumer will not match the previous authenticated community on service-provider the exception *UnknownCommunityException* will be thrown. + +In case the transferred data can't be stored on service-provider the exception *WriteAccessException* will be thrown. + +## Service: "request TradingLevel" + +With this service a community can ask for a trading level with another community. The *community-A* invokes this service at *community-B* to offer the own vision of trading with *community-B* by sending a TradingLevelTO with all the initialized Flags for future data exchanges. *Community-B* will store these data in the entry of *community-A* of its internal community list and mark it as an *open admin request* for trading level. Such an *open admin request* will inform the administrator of *community-B*, because administrative interactions and decisions are necessary. + +After the administrator of *community-B* has cleared all community internal aspects for the requested trading level with *community-A,* he will update the stored trading level flags for *community-A* and send this data by calling the service "confirm trading Level" of *community-A*. + +### Route: + +POST https:///requestTradingLevel/`` + +### Input-Data: + +``` +{ + TradingLevelTO + { + "sendMemberDetails" : "Flag if community-A will send member details to the community-B" + "receiveMemberDetails" : "Flag if community-A will receive member details from the community-B" + "sendCoins" : "Flag if members of community-A are allowed to send coins to members of the community-B" + "receiveCoins" : "Flag if members of community-A are allowed to receive coins from members of the community-B" + "sendActivities" : "Flag if community-A will send open activities for confirmation to the community-B" + "receiveActivities" : "Flag if community-A will receive open activities for confirmation from the community-B" + "sendBackup" : "Flag if community-A will send own data to community-B as backup provider" + "receiveBackup" : "Flag if community-A will receive data from community-B as backup provider" + } +} +``` + +### Output-Data: + +``` +{ + "result" : "Message if the trading level request is accepted and stored or reasons, why the request is rejected" +} +``` + +### Exceptions: + +A *SecurityException* will be thrown, if the security-accesstoken is not valid or the if internal autorization rules like black-listings will not allow access. + +In case the transferred data can't be stored on service-provider the exception *WriteAccessException* will be thrown. + +## Service: "confirm TradingLevel" + +With this service a community sends his trading level confirmation to a previous *requestTradingLevel* invocation of another community. The *community-B* invokes this service at *community-A* to confirm the previous received and optionally updated vision of trading level data with *community-A*. This service sends the TradingLevelTO with the confirmed flags for future data exchanges between *community-A* and *community-B*. *Community-A* will store this data in the entry of *community-B* of its internal community list and mark it as a *confirmed admin request* for trading level. The update of a *admin request* to state *confirmed* will inform the administrator of *community-A* to trigger administrative interactions and decisions. + +If the confirmed trading level from *community-B* will match exactly the requested once of *community-A* the confirm request will response with an OK. + +If the confirmed trading level from *community-B* can't be verified in *community-A* the confirm request will response with an ERROR. + +If the confirmed trading level from *community-B* will differ, but acceptable under reservations for *community-A* the confirm request will response with an RESERVE. If one of the involved communities will change this, it has to start the tradinglevel handshake again. + +If the confirmated trading level from *community-B* will absolutely not acceptable for *community-A* the confirm request will response with an REJECT and an additional roundtrip to deal a new tradinglevel between both communities will be necessary. + +### Route: + +POST https:///confirmTradingLevel/`` + +### Input-Data: + +The meaning of the *TradingLevelTO*-attributes in the confirmTradingLevel request must be interpreted from the confirmator point of view. For example "receiveBackup = TRUE" means *community-A* is ready to receive backup data from *community-B*, but *community-B* is as confirmator also prepared to send its data to *community-A* for backup. + +``` +{ + TradingLevelTO + { + "sendMemberDetails" : "Flag if community-A will send member details to the community-B" + "receiveMemberDetails" : "Flag if community-A will receive member details from the community-B" + "sendCoins" : "Flag if members of community-A are allowed to send coins to members of the community-B" + "receiveCoins" : "Flag if members of community-A are allowed to receive coins from members of the community-B" + "sendActivities" : "Flag if community-A will send open activities for confirmation to the community-B" + "receiveActivities" : "Flag if community-A will receive open activities for confirmation from the community-B" + "sendBackup" : "Flag if community-A will send own data to community-B as backup provider" + "receiveBackup" : "Flag if community-A will receive data from community-B as backup provider" + } +} +``` + +### Output-Data: + +``` +{ + "state" : "OK, ERROR, RESERVE, REJECT" + "result" : "optional Message to explain the state in detail" +} +``` + +### Exceptions: + +A *SecurityException* will be thrown, if the security-accesstoken is not valid or the if internal autorization rules like black-listings will not allow access. + +## Service: "Member of Community" + +Before user A can start any cross-community interactions with user B, this service api can be used to check if user B is a valid member of the other community. + +### Route: + +GET https:///memberOfCommunity/`` + +### Input-Data: + +``` +{ + userid : "user-id following the pattern /receiveCoins/`` + +### Input-Data: + +``` +{ + TransactionTO + { + "sender-community" : "the key of the sender community", + "sender-user" : "the user-id of the user, who sends the coins", + "receiver-user" : "the user-id of the user, who will receive the coins", + "money" : "the amount of coins and community-currency (community-key) as type Money", + "reason for transfer" : "the transaction description", + "timestamp of transfer" : "date and time of transaction" + } +} +``` + +### Output-Data: + +``` +{ + "result" : "result of a valid receive coins processing (perhaps gratitude of user or community), otherwise exception" +} +``` + +### Exceptions: + +*WrongCommunityException* in case the receiver community didn't know the sender community. + +*UnknownUserException* in case the receiver community has no user with the given receiver-user-id. + +*MissingTxDetailException* in case of missing one or more attributes in the TransactionTO, + +*InvalidCurrencyException* in case the currency (community-key) doesn't match with the sender-community. + +*InvalidTxTimeException* in case the timestamp of transfer is in the past. + +*DenyTxException* in case the receiver community or user will not interact with the sender community or user. + +## Service: "get Activity List" + +This service can be used to read the activity list of another community. A community supports a list of activities a member can select to create gradidos for his actions. Each activity has an attribute *topic* on which the list reading can be filtered. + +### Route: + +GET https:///getActivityList/`` + +### Input-Data: + +``` +{ + "topics" : "list of topics the reading of the activity list should be filtered by" +} +``` + +### Output-Data: + +``` +{ + "activities" : "list of found activity, which match the input-data" +} +``` + +### Exceptions: + +## Service: "get Clearing Activities" + +This service can be used to read open activities of other community members, which are open to be cleared by other users. This base on the concept to clear at least two or more activities of other users before the amount of gradidos of the own activity can be credit on the own AGE account. + +### Route: + +GET https:///getClearingActivities/`` + +### Input-Data: + +### Output-Data: + +### Exceptions: + +## Service: "Clear Activity" + +This service can be used to clear an open activity, which was read by the service getClearingActivies before. This base on the concept to clear at least two or more activities of other users before the amount of gradidos of the own activity can be credit on the own AGE account. + +### Route: + +### Input-Data: + +### Output-Data: + +### Exceptions: diff --git a/docu/Concepts/TechnicalRequirements/Federation.md b/docu/Concepts/TechnicalRequirements/Federation.md new file mode 100644 index 000000000..2f4ffc0f9 --- /dev/null +++ b/docu/Concepts/TechnicalRequirements/Federation.md @@ -0,0 +1,32 @@ +# 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. + +## ActivityPub + +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. + +At first we asume a *gradido community* as an *ActivityPub user*. A user is represented by "*actors*" via the users's accounts on servers. User's accounts on different servers corrsponds to different actors, which means community accounts on different servers corrsponds to different communities. + +Every community (actor) has an: + +* inbox: to get messages from the world +* outbox: to send messages to others + +and are simple endpoints or just URLs, which are described in the *ActivityStream* of each *ActivityPub community*. + +### Open Decision: + +It has to be decided, if the Federation will work with an internal or with external ActivityPub-Server, as shown in the picture below: + +![FederationActivityPub](./image/FederationActivityPub.png " ") + +The Variant A with an internal server contains the benefit to be as independent as possible from third party service providers and will not cause additional hosting costs. But this solution will cause the additional efforts of impementing an ActivityPub-Server in the gradido application and the responsibility for this component. + +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. + + + +## ActivityStream + +An ActivityStream includes all definitions and terms needed for community activities and content flow around the gradido community network. diff --git a/docu/Concepts/TechnicalRequirements/graphics/FederationActivityPub.drawio b/docu/Concepts/TechnicalRequirements/graphics/FederationActivityPub.drawio new file mode 100644 index 000000000..bb8eb5ab5 --- /dev/null +++ b/docu/Concepts/TechnicalRequirements/graphics/FederationActivityPub.drawio @@ -0,0 +1,224 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docu/Concepts/TechnicalRequirements/image/FederationActivityPub.png b/docu/Concepts/TechnicalRequirements/image/FederationActivityPub.png new file mode 100644 index 000000000..64fd71735 Binary files /dev/null and b/docu/Concepts/TechnicalRequirements/image/FederationActivityPub.png differ diff --git a/docu/graphics/roadmap_publicity.drawio b/docu/graphics/roadmap_publicity.drawio index 8fb4fa612..fb61449cd 100644 --- a/docu/graphics/roadmap_publicity.drawio +++ b/docu/graphics/roadmap_publicity.drawio @@ -1,105 +1,108 @@ - + - + - + - + - + - + - + - - + + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - - + + - + + + + diff --git a/docu/graphics/roadmap_publicity.png b/docu/graphics/roadmap_publicity.png index 4fe555a35..352d01a12 100644 Binary files a/docu/graphics/roadmap_publicity.png and b/docu/graphics/roadmap_publicity.png differ diff --git a/frontend/.env.dist b/frontend/.env.dist index 0de8c6252..df3c7cd7e 100644 --- a/frontend/.env.dist +++ b/frontend/.env.dist @@ -1,3 +1,5 @@ +CONFIG_VERSION=v1.2022-03-18 + META_URL=http://localhost META_TITLE_DE="Gradido – Dein Dankbarkeitskonto" META_TITLE_EN="Gradido - Your gratitude account" diff --git a/frontend/.env.template b/frontend/.env.template index 1eef43cef..4e4a86d08 100644 --- a/frontend/.env.template +++ b/frontend/.env.template @@ -1,3 +1,5 @@ +CONFIG_VERSION=$FRONTEND_CONFIG_VERSION + META_URL=$META_URL META_TITLE_DE=$META_TITLE_DE META_TITLE_EN=$META_TITLE_EN diff --git a/frontend/src/components/GddSend/TransactionForm.spec.js b/frontend/src/components/GddSend/TransactionForm.spec.js index 49b2174e0..db971a2bc 100644 --- a/frontend/src/components/GddSend/TransactionForm.spec.js +++ b/frontend/src/components/GddSend/TransactionForm.spec.js @@ -1,10 +1,12 @@ import { mount } from '@vue/test-utils' import TransactionForm from './TransactionForm' import flushPromises from 'flush-promises' +import { SEND_TYPES } from '@/pages/Send.vue' +import DashboardLayout from '@/layouts/DashboardLayout_gdd.vue' const localVue = global.localVue -describe('GddSend', () => { +describe('TransactionForm', () => { let wrapper const mocks = { @@ -25,7 +27,12 @@ describe('GddSend', () => { } const Wrapper = () => { - return mount(TransactionForm, { localVue, mocks, propsData }) + return mount(TransactionForm, { + localVue, + mocks, + propsData, + provide: DashboardLayout.provide, + }) } describe('mount', () => { @@ -34,7 +41,7 @@ describe('GddSend', () => { }) it('renders the component', () => { - expect(wrapper.find('div.transaction-form').exists()).toBeTruthy() + expect(wrapper.find('div.transaction-form').exists()).toBe(true) }) describe('transaction form disable because balance 0,0 GDD', () => { @@ -51,31 +58,35 @@ describe('GddSend', () => { expect(wrapper.find('.text-danger').text()).toBe('form.no_gdd_available') }) it('has no reset button and no submit button ', () => { - expect(wrapper.find('.test-buttons').exists()).toBeFalsy() + expect(wrapper.find('.test-buttons').exists()).toBe(false) }) }) - describe('is selected: "send"', () => { + describe('send GDD', () => { beforeEach(async () => { - // await wrapper.setData({ - // selected: 'send', - // }) await wrapper.findAll('input[type="radio"]').at(0).setChecked() }) + it('has SEND_TYPES = send', () => { + expect(wrapper.vm.selected).toBe(SEND_TYPES.send) + }) + describe('transaction form', () => { beforeEach(() => { wrapper.setProps({ balance: 100.0 }) }) + describe('transaction form show because balance 100,0 GDD', () => { it('has no warning message ', () => { - expect(wrapper.find('.errors').exists()).toBeFalsy() + expect(wrapper.find('.errors').exists()).toBe(false) }) + it('has a reset button', () => { expect(wrapper.find('.test-buttons').findAll('button').at(0).attributes('type')).toBe( 'reset', ) }) + it('has a submit button', () => { expect(wrapper.find('.test-buttons').findAll('button').at(1).attributes('type')).toBe( 'submit', @@ -110,6 +121,12 @@ describe('GddSend', () => { expect(wrapper.find('span.errors').text()).toBe('validations.messages.email') }) + it('flushes an error message when email is the email of logged in user', async () => { + await wrapper.find('#input-group-1').find('input').setValue('user@example.org') + await flushPromises() + expect(wrapper.find('span.errors').text()).toBe('form.validation.is-not') + }) + it('trims the email after blur', async () => { await wrapper.find('#input-group-1').find('input').setValue(' valid@email.com ') await wrapper.find('#input-group-1').find('input').trigger('blur') @@ -159,13 +176,13 @@ describe('GddSend', () => { it('flushes no errors when amount is valid', async () => { await wrapper.find('#input-group-2').find('input').setValue('87.34') await flushPromises() - expect(wrapper.find('span.errors').exists()).toBeFalsy() + expect(wrapper.find('span.errors').exists()).toBe(false) }) }) describe('message text box', () => { it('has an textarea field', () => { - expect(wrapper.find('#input-group-3').find('textarea').exists()).toBeTruthy() + expect(wrapper.find('#input-group-3').find('textarea').exists()).toBe(true) }) it('has an chat-right-text icon', () => { @@ -184,16 +201,51 @@ describe('GddSend', () => { expect(wrapper.find('span.errors').text()).toBe('validations.messages.min') }) + it('flushes an error message when memo is more than 255 characters', async () => { + await wrapper.find('#input-group-3').find('textarea').setValue(` +Es ist ein König in Thule, der trinkt +Champagner, es geht ihm nichts drüber; +Und wenn er seinen Champagner trinkt, +Dann gehen die Augen ihm über. + +Die Ritter sitzen um ihn her, +Die ganze Historische Schule; +Ihm aber wird die Zunge schwer, +Es lallt der König von Thule: + +„Als Alexander, der Griechenheld, +Mit seinem kleinen Haufen +Erobert hatte die ganze Welt, +Da gab er sich ans Saufen. + +Ihn hatten so durstig gemacht der Krieg +Und die Schlachten, die er geschlagen; +Er soff sich zu Tode nach dem Sieg, +Er konnte nicht viel vertragen. + +Ich aber bin ein stärkerer Mann +Und habe mich klüger besonnen: +Wie jener endete, fang ich an, +Ich hab mit dem Trinken begonnen. + +Im Rausche wird der Heldenzug +Mir später weit besser gelingen; +Dann werde ich, taumelnd von Krug zu Krug, +Die ganze Welt bezwingen.“`) + await flushPromises() + expect(wrapper.find('span.errors').text()).toBe('validations.messages.max') + }) + it('flushes no error message when memo is valid', async () => { await wrapper.find('#input-group-3').find('textarea').setValue('Long enough') await flushPromises() - expect(wrapper.find('span.errors').exists()).toBeFalsy() + expect(wrapper.find('span.errors').exists()).toBe(false) }) }) describe('cancel button', () => { it('has a cancel button', () => { - expect(wrapper.find('button[type="reset"]').exists()).toBeTruthy() + expect(wrapper.find('button[type="reset"]').exists()).toBe(true) }) it('has the text "form.cancel"', () => { @@ -242,16 +294,17 @@ describe('GddSend', () => { }) }) - describe('is selected: "link"', () => { + describe('create transaction link', () => { beforeEach(async () => { - // await wrapper.setData({ - // selected: 'link', - // }) await wrapper.findAll('input[type="radio"]').at(1).setChecked() }) + it('has SEND_TYPES = link', () => { + expect(wrapper.vm.selected).toBe(SEND_TYPES.link) + }) + it('has no input field of id input-group-1', () => { - expect(wrapper.find('#input-group-1').isVisible()).toBeFalsy() + expect(wrapper.find('#input-group-1').exists()).toBe(false) }) }) }) diff --git a/frontend/src/components/GddSend/TransactionForm.vue b/frontend/src/components/GddSend/TransactionForm.vue index ec4aff4d3..5e683d132 100644 --- a/frontend/src/components/GddSend/TransactionForm.vue +++ b/frontend/src/components/GddSend/TransactionForm.vue @@ -16,16 +16,15 @@ -
+

{{ $t('gdd_per_link.header') }}

{{ $t('gdd_per_link.choose-amount') }}
-
+
+
-
+
{{ $t('form.no_gdd_available') }}
@@ -141,7 +138,6 @@ -
@@ -160,15 +156,19 @@ export default { }, props: { balance: { type: Number, default: 0 }, + email: { type: String, default: '' }, + amount: { type: Number, default: 0 }, + memo: { type: String, default: '' }, }, + inject: ['getTunneledEmail'], data() { return { amountFocused: false, emailFocused: false, form: { - email: '', - amount: '', - memo: '', + email: this.email, + amount: this.amount ? String(this.amount) : '', + memo: this.memo, amountValue: 0.0, }, selected: SEND_TYPES.send, @@ -208,6 +208,12 @@ export default { sendTypes() { return SEND_TYPES }, + recipientEmail() { + return this.getTunneledEmail() + }, + }, + created() { + this.form.email = this.recipientEmail ? this.recipientEmail : '' }, } diff --git a/frontend/src/components/GddSend/TransactionResultSendError.vue b/frontend/src/components/GddSend/TransactionResultSendError.vue index daa593aec..6a3761092 100644 --- a/frontend/src/components/GddSend/TransactionResultSendError.vue +++ b/frontend/src/components/GddSend/TransactionResultSendError.vue @@ -31,7 +31,7 @@ diff --git a/frontend/src/pages/Transactions.vue b/frontend/src/pages/Transactions.vue index 6fc588b10..9b2ad6fdf 100644 --- a/frontend/src/pages/Transactions.vue +++ b/frontend/src/pages/Transactions.vue @@ -12,6 +12,7 @@ :show-pagination="true" :decayStartBlock="decayStartBlock" @update-transactions="updateTransactions" + v-on="$listeners" /> diff --git a/frontend/src/pages/thx.vue b/frontend/src/pages/thx.vue index 4a86212c9..736e0c70e 100644 --- a/frontend/src/pages/thx.vue +++ b/frontend/src/pages/thx.vue @@ -9,7 +9,11 @@

{{ $t(displaySetup.subtitle) }}


- + + + {{ $t(displaySetup.button) }} + + {{ $t(displaySetup.button) }} diff --git a/frontend/src/routes/router.test.js b/frontend/src/routes/router.test.js index 85f765c69..925b3ffca 100644 --- a/frontend/src/routes/router.test.js +++ b/frontend/src/routes/router.test.js @@ -112,7 +112,7 @@ describe('router', () => { }) describe('thx', () => { - const thx = routes.find((r) => r.path === '/thx/:comingFrom') + const thx = routes.find((r) => r.path === '/thx/:comingFrom/:code?') it('loads the "Thx" page', async () => { const component = await thx.component() @@ -177,7 +177,9 @@ describe('router', () => { describe('checkEmail', () => { it('loads the "CheckEmail" page', async () => { - const component = await routes.find((r) => r.path === '/checkEmail/:optin').component() + const component = await routes + .find((r) => r.path === '/checkEmail/:optin/:code?') + .component() expect(component.default.name).toBe('ResetPassword') }) }) diff --git a/frontend/src/routes/routes.js b/frontend/src/routes/routes.js index 5e0b09c5e..a6586c201 100755 --- a/frontend/src/routes/routes.js +++ b/frontend/src/routes/routes.js @@ -47,7 +47,7 @@ const routes = [ component: () => import('@/pages/Register.vue'), }, { - path: '/thx/:comingFrom', + path: '/thx/:comingFrom/:code?', component: () => import('@/pages/thx.vue'), beforeEnter: (to, from, next) => { const validFrom = ['forgot-password', 'reset-password', 'register', 'login', 'checkEmail'] @@ -79,7 +79,7 @@ const routes = [ component: () => import('@/pages/ResetPassword.vue'), }, { - path: '/checkEmail/:optin', + path: '/checkEmail/:optin/:code?', component: () => import('@/pages/ResetPassword.vue'), }, { diff --git a/frontend/vue.config.js b/frontend/vue.config.js index 4c86a7a6d..573f2a70b 100644 --- a/frontend/vue.config.js +++ b/frontend/vue.config.js @@ -3,11 +3,12 @@ const webpack = require('webpack') const Dotenv = require('dotenv-webpack') const StatsPlugin = require('stats-webpack-plugin') const HtmlWebpackPlugin = require('vue-html-webpack-plugin') +const CONFIG = require('./src/config') // vue.config.js module.exports = { devServer: { - port: process.env.PORT || 3000, + port: CONFIG.PORT, }, pluginOptions: { i18n: { @@ -35,7 +36,7 @@ module.exports = { // 'process.env.DOCKER_WORKDIR': JSON.stringify(process.env.DOCKER_WORKDIR), // 'process.env.BUILD_DATE': JSON.stringify(process.env.BUILD_DATE), // 'process.env.BUILD_VERSION': JSON.stringify(process.env.BUILD_VERSION), - 'process.env.BUILD_COMMIT': JSON.stringify(process.env.BUILD_COMMIT), + 'process.env.BUILD_COMMIT': JSON.stringify(CONFIG.BUILD_COMMIT), // 'process.env.PORT': JSON.stringify(process.env.PORT), }), // generate webpack stats to allow analysis of the bundlesize @@ -44,14 +45,14 @@ module.exports = { vue: true, template: 'public/index.html', meta: { - title_de: process.env.META_TITLE_DE, - title_en: process.env.META_TITLE_EN, - description_de: process.env.META_DESCRIPTION_DE, - description_en: process.env.META_DESCRIPTION_EN, - keywords_de: process.env.META_KEYWORDS_DE, - keywords_en: process.env.META_KEYWORDS_EN, - author: process.env.META_AUTHOR, - url: process.env.META_URL, + title_de: CONFIG.META_TITLE_DE, + title_en: CONFIG.META_TITLE_EN, + description_de: CONFIG.META_DESCRIPTION_DE, + description_en: CONFIG.META_DESCRIPTION_EN, + keywords_de: CONFIG.META_KEYWORDS_DE, + keywords_en: CONFIG.META_KEYWORDS_EN, + author: CONFIG.META_AUTHOR, + url: CONFIG.META_URL, }, }), ], @@ -61,7 +62,7 @@ module.exports = { }, css: { // Enable CSS source maps. - sourceMap: process.env.NODE_ENV !== 'production', + sourceMap: CONFIG.NODE_ENV !== 'production', }, outputDir: path.resolve(__dirname, './dist'), }