diff --git a/backend/src/config/index.ts b/backend/src/config/index.ts index e7139033b..26227b90d 100644 --- a/backend/src/config/index.ts +++ b/backend/src/config/index.ts @@ -10,7 +10,7 @@ Decimal.set({ }) const constants = { - DB_VERSION: '0052-add_updated_at_to_contributions', + DB_VERSION: '0053-change_password_encryption', DECAY_START_TIME: new Date('2021-05-13 17:46:31-0000'), // GMT+0 LOG4JS_CONFIG: 'log4js-config.json', // default log level on production should be info diff --git a/backend/src/graphql/enum/PasswordEncryptionType.ts b/backend/src/graphql/enum/PasswordEncryptionType.ts new file mode 100644 index 000000000..b3a00d748 --- /dev/null +++ b/backend/src/graphql/enum/PasswordEncryptionType.ts @@ -0,0 +1,12 @@ +import { registerEnumType } from 'type-graphql' + +export enum PasswordEncryptionType { + NO_PASSWORD = 0, + EMAIL = 1, + GRADIDO_ID = 2, +} + +registerEnumType(PasswordEncryptionType, { + name: 'PasswordEncryptionType', // this one is mandatory + description: 'Type of the password encryption', // this one is optional +}) diff --git a/backend/src/graphql/resolver/UserResolver.test.ts b/backend/src/graphql/resolver/UserResolver.test.ts index 6323abfde..d8472fba9 100644 --- a/backend/src/graphql/resolver/UserResolver.test.ts +++ b/backend/src/graphql/resolver/UserResolver.test.ts @@ -36,6 +36,9 @@ import { UserContact } from '@entity/UserContact' import { OptInType } from '../enum/OptInType' import { UserContactType } from '../enum/UserContactType' import { bobBaumeister } from '@/seeds/users/bob-baumeister' +import { encryptPassword } from '@/password/PasswordEncryptor' +import { PasswordEncryptionType } from '../enum/PasswordEncryptionType' +import { SecretKeyCryptographyCreateKey } from '@/password/EncryptorUtils' // import { klicktippSignIn } from '@/apis/KlicktippController' @@ -146,6 +149,7 @@ describe('UserResolver', () => { publisherId: 1234, referrerId: null, contributionLinkId: null, + passwordEncryptionType: PasswordEncryptionType.NO_PASSWORD, }, ]) const valUUID = validateUUID(user[0].gradidoID) @@ -491,7 +495,8 @@ describe('UserResolver', () => { }) it('updates the password', () => { - expect(newUser.password).toEqual('3917921995996627700') + const encryptedPass = encryptPassword(newUser, 'Aa12345_') + expect(newUser.password.toString()).toEqual(encryptedPass.toString()) }) /* @@ -1159,6 +1164,93 @@ describe('UserResolver', () => { }) }) }) + + describe('password encryption type', () => { + describe('user just registered', () => { + let bibi: User + + it('has password type gradido id', async () => { + const users = await User.find() + bibi = users[1] + + expect(bibi).toEqual( + expect.objectContaining({ + password: SecretKeyCryptographyCreateKey(bibi.gradidoID.toString(), 'Aa12345_')[0] + .readBigUInt64LE() + .toString(), + passwordEncryptionType: PasswordEncryptionType.GRADIDO_ID, + }), + ) + }) + }) + + describe('user has encryption type email', () => { + const variables = { + email: 'bibi@bloxberg.de', + password: 'Aa12345_', + publisherId: 1234, + } + + let bibi: User + beforeAll(async () => { + const usercontact = await UserContact.findOneOrFail( + { email: 'bibi@bloxberg.de' }, + { relations: ['user'] }, + ) + bibi = usercontact.user + bibi.passwordEncryptionType = PasswordEncryptionType.EMAIL + bibi.password = SecretKeyCryptographyCreateKey( + 'bibi@bloxberg.de', + 'Aa12345_', + )[0].readBigUInt64LE() + + await bibi.save() + }) + + it('changes to gradidoID on login', async () => { + await mutate({ mutation: login, variables: variables }) + + const usercontact = await UserContact.findOneOrFail( + { email: 'bibi@bloxberg.de' }, + { relations: ['user'] }, + ) + bibi = usercontact.user + + expect(bibi).toEqual( + expect.objectContaining({ + firstName: 'Bibi', + password: SecretKeyCryptographyCreateKey(bibi.gradidoID.toString(), 'Aa12345_')[0] + .readBigUInt64LE() + .toString(), + passwordEncryptionType: PasswordEncryptionType.GRADIDO_ID, + }), + ) + }) + + it('can login after password change', async () => { + resetToken() + expect(await mutate({ mutation: login, variables: variables })).toEqual( + expect.objectContaining({ + data: { + login: { + email: 'bibi@bloxberg.de', + firstName: 'Bibi', + hasElopage: false, + id: expect.any(Number), + isAdmin: null, + klickTipp: { + newsletterState: false, + }, + language: 'de', + lastName: 'Bloxberg', + publisherId: 1234, + }, + }, + }), + ) + }) + }) + }) }) describe('printTimeDuration', () => { diff --git a/backend/src/graphql/resolver/UserResolver.ts b/backend/src/graphql/resolver/UserResolver.ts index 707b7ac49..752c585fd 100644 --- a/backend/src/graphql/resolver/UserResolver.ts +++ b/backend/src/graphql/resolver/UserResolver.ts @@ -40,17 +40,15 @@ import { SearchAdminUsersResult } from '@model/AdminUser' import Paginated from '@arg/Paginated' import { Order } from '@enum/Order' import { v4 as uuidv4 } from 'uuid' +import { isValidPassword, SecretKeyCryptographyCreateKey } from '@/password/EncryptorUtils' +import { encryptPassword, verifyPassword } from '@/password/PasswordEncryptor' +import { PasswordEncryptionType } from '../enum/PasswordEncryptionType' // eslint-disable-next-line @typescript-eslint/no-var-requires const sodium = require('sodium-native') // eslint-disable-next-line @typescript-eslint/no-var-requires const random = require('random-bigint') -// We will reuse this for changePassword -const isPassword = (password: string): boolean => { - return !!password.match(/^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[^a-zA-Z0-9 \\t\\n\\r]).{8,}$/) -} - const LANGUAGES = ['de', 'en', 'es', 'fr', 'nl'] const DEFAULT_LANGUAGE = 'de' const isLanguage = (language: string): boolean => { @@ -107,48 +105,6 @@ const KeyPairEd25519Create = (passphrase: string[]): Buffer[] => { return [pubKey, privKey] } -const SecretKeyCryptographyCreateKey = (salt: string, password: string): Buffer[] => { - logger.trace('SecretKeyCryptographyCreateKey...') - const configLoginAppSecret = Buffer.from(CONFIG.LOGIN_APP_SECRET, 'hex') - const configLoginServerKey = Buffer.from(CONFIG.LOGIN_SERVER_KEY, 'hex') - if (configLoginServerKey.length !== sodium.crypto_shorthash_KEYBYTES) { - logger.error( - `ServerKey has an invalid size. The size must be ${sodium.crypto_shorthash_KEYBYTES} bytes.`, - ) - throw new Error( - `ServerKey has an invalid size. The size must be ${sodium.crypto_shorthash_KEYBYTES} bytes.`, - ) - } - - const state = Buffer.alloc(sodium.crypto_hash_sha512_STATEBYTES) - sodium.crypto_hash_sha512_init(state) - sodium.crypto_hash_sha512_update(state, Buffer.from(salt)) - sodium.crypto_hash_sha512_update(state, configLoginAppSecret) - const hash = Buffer.alloc(sodium.crypto_hash_sha512_BYTES) - sodium.crypto_hash_sha512_final(state, hash) - - const encryptionKey = Buffer.alloc(sodium.crypto_box_SEEDBYTES) - const opsLimit = 10 - const memLimit = 33554432 - const algo = 2 - sodium.crypto_pwhash( - encryptionKey, - Buffer.from(password), - hash.slice(0, sodium.crypto_pwhash_SALTBYTES), - opsLimit, - memLimit, - algo, - ) - - const encryptionKeyHash = Buffer.alloc(sodium.crypto_shorthash_BYTES) - sodium.crypto_shorthash(encryptionKeyHash, encryptionKey, configLoginServerKey) - - logger.debug( - `SecretKeyCryptographyCreateKey...successful: encryptionKeyHash= ${encryptionKeyHash}, encryptionKey= ${encryptionKey}`, - ) - return [encryptionKeyHash, encryptionKey] -} - /* const getEmailHash = (email: string): Buffer => { logger.trace('getEmailHash...') @@ -346,12 +302,17 @@ export class UserResolver { // TODO we want to catch this on the frontend and ask the user to check his emails or resend code throw new Error('User has no private or publicKey') } - const passwordHash = SecretKeyCryptographyCreateKey(email, password) // return short and long hash - const loginUserPassword = BigInt(dbUser.password.toString()) - if (loginUserPassword !== passwordHash[0].readBigUInt64LE()) { + + if (!verifyPassword(dbUser, password)) { logger.error('The User has no valid credentials.') throw new Error('No user with this credentials') } + + if (dbUser.passwordEncryptionType !== PasswordEncryptionType.GRADIDO_ID) { + dbUser.passwordEncryptionType = PasswordEncryptionType.GRADIDO_ID + dbUser.password = encryptPassword(dbUser, password) + await dbUser.save() + } // add pubKey in logger-context for layout-pattern X{user} to print it in each logging message logger.addContext('user', dbUser.id) logger.debug('validation of login credentials successful...') @@ -481,6 +442,7 @@ export class UserResolver { dbUser.lastName = lastName dbUser.language = language dbUser.publisherId = publisherId + dbUser.passwordEncryptionType = PasswordEncryptionType.NO_PASSWORD dbUser.passphrase = passphrase.join(' ') logger.debug('new dbUser=' + dbUser) if (redeemCode) { @@ -634,7 +596,7 @@ export class UserResolver { ): Promise { logger.info(`setPassword(${code}, ***)...`) // Validate Password - if (!isPassword(password)) { + if (!isValidPassword(password)) { logger.error('Password entered is lexically invalid') throw new Error( 'Please enter a valid password with at least 8 characters, upper and lower case letters, at least one number and one special character!', @@ -692,10 +654,11 @@ export class UserResolver { userContact.emailChecked = true // Update Password + user.passwordEncryptionType = PasswordEncryptionType.GRADIDO_ID const passwordHash = SecretKeyCryptographyCreateKey(userContact.email, password) // return short and long hash const keyPair = KeyPairEd25519Create(passphrase) // return pub, priv Key const encryptedPrivkey = SecretKeyCryptographyEncrypt(keyPair[1], passwordHash[1]) - user.password = passwordHash[0].readBigUInt64LE() // using the shorthash + user.password = encryptPassword(user, password) user.pubKey = keyPair[0] user.privKey = encryptedPrivkey logger.debug('User credentials updated ...') @@ -801,7 +764,7 @@ export class UserResolver { if (password && passwordNew) { // Validate Password - if (!isPassword(passwordNew)) { + if (!isValidPassword(passwordNew)) { logger.error('newPassword does not fullfil the rules') throw new Error( 'Please enter a valid password with at least 8 characters, upper and lower case letters, at least one number and one special character!', @@ -813,7 +776,7 @@ export class UserResolver { userEntity.emailContact.email, password, ) - if (BigInt(userEntity.password.toString()) !== oldPasswordHash[0].readBigUInt64LE()) { + if (!verifyPassword(userEntity, password)) { logger.error(`Old password is invalid`) throw new Error(`Old password is invalid`) } @@ -829,7 +792,8 @@ export class UserResolver { logger.debug('PrivateKey encrypted...') // Save new password hash and newly encrypted private key - userEntity.password = newPasswordHash[0].readBigUInt64LE() + userEntity.passwordEncryptionType = PasswordEncryptionType.GRADIDO_ID + userEntity.password = encryptPassword(userEntity, passwordNew) userEntity.privKey = encryptedPrivkey } diff --git a/backend/src/password/EncryptorUtils.ts b/backend/src/password/EncryptorUtils.ts new file mode 100644 index 000000000..971b6a32e --- /dev/null +++ b/backend/src/password/EncryptorUtils.ts @@ -0,0 +1,71 @@ +import CONFIG from '@/config' +import { backendLogger as logger } from '@/server/logger' +import { User } from '@entity/User' +import { PasswordEncryptionType } from '@enum/PasswordEncryptionType' + +// eslint-disable-next-line @typescript-eslint/no-var-requires +const sodium = require('sodium-native') + +// We will reuse this for changePassword +export const isValidPassword = (password: string): boolean => { + return !!password.match(/^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[^a-zA-Z0-9 \\t\\n\\r]).{8,}$/) +} + +export const SecretKeyCryptographyCreateKey = (salt: string, password: string): Buffer[] => { + logger.trace('SecretKeyCryptographyCreateKey...') + const configLoginAppSecret = Buffer.from(CONFIG.LOGIN_APP_SECRET, 'hex') + const configLoginServerKey = Buffer.from(CONFIG.LOGIN_SERVER_KEY, 'hex') + if (configLoginServerKey.length !== sodium.crypto_shorthash_KEYBYTES) { + logger.error( + `ServerKey has an invalid size. The size must be ${sodium.crypto_shorthash_KEYBYTES} bytes.`, + ) + throw new Error( + `ServerKey has an invalid size. The size must be ${sodium.crypto_shorthash_KEYBYTES} bytes.`, + ) + } + + const state = Buffer.alloc(sodium.crypto_hash_sha512_STATEBYTES) + sodium.crypto_hash_sha512_init(state) + sodium.crypto_hash_sha512_update(state, Buffer.from(salt)) + sodium.crypto_hash_sha512_update(state, configLoginAppSecret) + const hash = Buffer.alloc(sodium.crypto_hash_sha512_BYTES) + sodium.crypto_hash_sha512_final(state, hash) + + const encryptionKey = Buffer.alloc(sodium.crypto_box_SEEDBYTES) + const opsLimit = 10 + const memLimit = 33554432 + const algo = 2 + sodium.crypto_pwhash( + encryptionKey, + Buffer.from(password), + hash.slice(0, sodium.crypto_pwhash_SALTBYTES), + opsLimit, + memLimit, + algo, + ) + + const encryptionKeyHash = Buffer.alloc(sodium.crypto_shorthash_BYTES) + sodium.crypto_shorthash(encryptionKeyHash, encryptionKey, configLoginServerKey) + + return [encryptionKeyHash, encryptionKey] +} + +export const getUserCryptographicSalt = (dbUser: User): string => { + switch (dbUser.passwordEncryptionType) { + case PasswordEncryptionType.NO_PASSWORD: { + logger.error('Password not set for user ' + dbUser.id) + throw new Error('Password not set for user ' + dbUser.id) // user has no password + } + case PasswordEncryptionType.EMAIL: { + return dbUser.emailContact.email + break + } + case PasswordEncryptionType.GRADIDO_ID: { + return dbUser.gradidoID + break + } + default: + logger.error(`Unknown password encryption type: ${dbUser.passwordEncryptionType}`) + throw new Error(`Unknown password encryption type: ${dbUser.passwordEncryptionType}`) + } +} diff --git a/backend/src/password/PasswordEncryptor.ts b/backend/src/password/PasswordEncryptor.ts new file mode 100644 index 000000000..1735106c1 --- /dev/null +++ b/backend/src/password/PasswordEncryptor.ts @@ -0,0 +1,14 @@ +import { User } from '@entity/User' +// import { logger } from '@test/testSetup' getting error "jest is not defined" +import { getUserCryptographicSalt, SecretKeyCryptographyCreateKey } from './EncryptorUtils' + +export const encryptPassword = (dbUser: User, password: string): bigint => { + const salt = getUserCryptographicSalt(dbUser) + const keyBuffer = SecretKeyCryptographyCreateKey(salt, password) // return short and long hash + const passwordHash = keyBuffer[0].readBigUInt64LE() + return passwordHash +} + +export const verifyPassword = (dbUser: User, password: string): boolean => { + return dbUser.password.toString() === encryptPassword(dbUser, password).toString() +} diff --git a/backend/src/util/communityUser.ts b/backend/src/util/communityUser.ts index e885b7043..298348f0f 100644 --- a/backend/src/util/communityUser.ts +++ b/backend/src/util/communityUser.ts @@ -1,5 +1,6 @@ /* eslint-disable @typescript-eslint/no-unused-vars */ +import { PasswordEncryptionType } from '@/graphql/enum/PasswordEncryptionType' import { SaveOptions, RemoveOptions } from '@dbTools/typeorm' import { User as dbUser } from '@entity/User' import { UserContact } from '@entity/UserContact' @@ -26,6 +27,8 @@ const communityDbUser: dbUser = { isAdmin: null, publisherId: 0, passphrase: '', + // default password encryption type + passwordEncryptionType: PasswordEncryptionType.NO_PASSWORD, hasId: function (): boolean { throw new Error('Function not implemented.') }, diff --git a/database/entity/0053-change_password_encryption/User.ts b/database/entity/0053-change_password_encryption/User.ts new file mode 100644 index 000000000..2a3332925 --- /dev/null +++ b/database/entity/0053-change_password_encryption/User.ts @@ -0,0 +1,127 @@ +import { + BaseEntity, + Entity, + PrimaryGeneratedColumn, + Column, + DeleteDateColumn, + OneToMany, + JoinColumn, + OneToOne, +} from 'typeorm' +import { Contribution } from '../Contribution' +import { ContributionMessage } from '../ContributionMessage' +import { UserContact } from '../UserContact' + +@Entity('users', { engine: 'InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci' }) +export class User extends BaseEntity { + @PrimaryGeneratedColumn('increment', { unsigned: true }) + id: number + + @Column({ + name: 'gradido_id', + length: 36, + nullable: false, + collation: 'utf8mb4_unicode_ci', + }) + gradidoID: string + + @Column({ + name: 'alias', + length: 20, + nullable: true, + default: null, + collation: 'utf8mb4_unicode_ci', + }) + alias: string + + @Column({ name: 'public_key', type: 'binary', length: 32, default: null, nullable: true }) + pubKey: Buffer + + @Column({ name: 'privkey', type: 'binary', length: 80, default: null, nullable: true }) + privKey: Buffer + + @Column({ + type: 'text', + name: 'passphrase', + collation: 'utf8mb4_unicode_ci', + nullable: true, + default: null, + }) + passphrase: string + + @OneToOne(() => UserContact, (emailContact: UserContact) => emailContact.user) + @JoinColumn({ name: 'email_id' }) + emailContact: UserContact + + @Column({ name: 'email_id', type: 'int', unsigned: true, nullable: true, default: null }) + emailId: number | null + + @Column({ + name: 'first_name', + length: 255, + nullable: true, + default: null, + collation: 'utf8mb4_unicode_ci', + }) + firstName: string + + @Column({ + name: 'last_name', + length: 255, + nullable: true, + default: null, + collation: 'utf8mb4_unicode_ci', + }) + lastName: string + + @Column({ name: 'created_at', default: () => 'CURRENT_TIMESTAMP', nullable: false }) + createdAt: Date + + @DeleteDateColumn({ name: 'deleted_at', nullable: true }) + deletedAt: Date | null + + @Column({ type: 'bigint', default: 0, unsigned: true }) + password: BigInt + + @Column({ + name: 'password_encryption_type', + type: 'int', + unsigned: true, + nullable: false, + default: 0, + }) + passwordEncryptionType: number + + @Column({ length: 4, default: 'de', collation: 'utf8mb4_unicode_ci', nullable: false }) + language: string + + @Column({ name: 'is_admin', type: 'datetime', nullable: true, default: null }) + isAdmin: Date | null + + @Column({ name: 'referrer_id', type: 'int', unsigned: true, nullable: true, default: null }) + referrerId?: number | null + + @Column({ + name: 'contribution_link_id', + type: 'int', + unsigned: true, + nullable: true, + default: null, + }) + contributionLinkId?: number | null + + @Column({ name: 'publisher_id', default: 0 }) + publisherId: number + + @OneToMany(() => Contribution, (contribution) => contribution.user) + @JoinColumn({ name: 'user_id' }) + contributions?: Contribution[] + + @OneToMany(() => ContributionMessage, (message) => message.user) + @JoinColumn({ name: 'user_id' }) + messages?: ContributionMessage[] + + @OneToMany(() => UserContact, (userContact: UserContact) => userContact.user) + @JoinColumn({ name: 'user_id' }) + userContacts?: UserContact[] +} diff --git a/database/entity/0053-change_password_encryption/UserContact.ts b/database/entity/0053-change_password_encryption/UserContact.ts new file mode 100644 index 000000000..97b12d4cd --- /dev/null +++ b/database/entity/0053-change_password_encryption/UserContact.ts @@ -0,0 +1,60 @@ +import { + BaseEntity, + Entity, + PrimaryGeneratedColumn, + Column, + DeleteDateColumn, + OneToOne, +} from 'typeorm' +import { User } from './User' + +@Entity('user_contacts', { engine: 'InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci' }) +export class UserContact extends BaseEntity { + @PrimaryGeneratedColumn('increment', { unsigned: true }) + id: number + + @Column({ + name: 'type', + length: 100, + nullable: true, + default: null, + collation: 'utf8mb4_unicode_ci', + }) + type: string + + @OneToOne(() => User, (user) => user.emailContact) + user: User + + @Column({ name: 'user_id', type: 'int', unsigned: true, nullable: false }) + userId: number + + @Column({ length: 255, unique: true, nullable: false, collation: 'utf8mb4_unicode_ci' }) + email: string + + @Column({ name: 'email_verification_code', type: 'bigint', unsigned: true, unique: true }) + emailVerificationCode: BigInt + + @Column({ name: 'email_opt_in_type_id' }) + emailOptInTypeId: number + + @Column({ name: 'email_resend_count' }) + emailResendCount: number + + // @Column({ name: 'email_hash', type: 'binary', length: 32, default: null, nullable: true }) + // emailHash: Buffer + + @Column({ name: 'email_checked', type: 'bool', nullable: false, default: false }) + emailChecked: boolean + + @Column({ length: 255, unique: false, nullable: true, collation: 'utf8mb4_unicode_ci' }) + phone: string + + @Column({ name: 'created_at', default: () => 'CURRENT_TIMESTAMP', nullable: false }) + createdAt: Date + + @Column({ name: 'updated_at', nullable: true, default: null, type: 'datetime' }) + updatedAt: Date | null + + @DeleteDateColumn({ name: 'deleted_at', nullable: true }) + deletedAt: Date | null +} diff --git a/database/entity/User.ts b/database/entity/User.ts index d073f428a..b3c00a9b4 100644 --- a/database/entity/User.ts +++ b/database/entity/User.ts @@ -1 +1 @@ -export { User } from './0049-add_user_contacts_table/User' +export { User } from './0053-change_password_encryption/User' diff --git a/database/entity/UserContact.ts b/database/entity/UserContact.ts index a368bb7ca..dd74e65c4 100644 --- a/database/entity/UserContact.ts +++ b/database/entity/UserContact.ts @@ -1 +1 @@ -export { UserContact } from './0049-add_user_contacts_table/UserContact' +export { UserContact } from './0053-change_password_encryption/UserContact' diff --git a/database/migrations/0053-change_password_encryption.ts b/database/migrations/0053-change_password_encryption.ts new file mode 100644 index 000000000..635109430 --- /dev/null +++ b/database/migrations/0053-change_password_encryption.ts @@ -0,0 +1,24 @@ +/* MIGRATION TO ADD ENCRYPTION TYPE TO PASSWORDS + * + * This migration adds and renames columns in the table `users` + */ + +/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ +/* eslint-disable @typescript-eslint/no-explicit-any */ + +export async function upgrade(queryFn: (query: string, values?: any[]) => Promise>) { + await queryFn('ALTER TABLE users RENAME COLUMN created TO created_at;') + await queryFn('ALTER TABLE users RENAME COLUMN deletedAt TO deleted_at;') + // alter table emp rename column emp_name to name + await queryFn( + 'ALTER TABLE users ADD COLUMN password_encryption_type int(10) NOT NULL DEFAULT 0 AFTER password;', + ) + await queryFn(`UPDATE users SET password_encryption_type = 1 WHERE id IN + (SELECT user_id FROM user_contacts WHERE email_checked = 1)`) +} + +export async function downgrade(queryFn: (query: string, values?: any[]) => Promise>) { + await queryFn('ALTER TABLE users RENAME COLUMN created_at TO created;') + await queryFn('ALTER TABLE users RENAME COLUMN deleted_at TO deletedAt;') + await queryFn('ALTER TABLE users DROP COLUMN password_encryption_type;') +} diff --git a/docu/RoadMap_2022-2023.md b/docu/RoadMap_2022-2023.md new file mode 100644 index 000000000..fa8573448 --- /dev/null +++ b/docu/RoadMap_2022-2023.md @@ -0,0 +1,167 @@ +# Roadmap 2022 / 2023 + +## unsortierte Sammlung von Themen + +1. backend access layer + + - Refactoring der Resolver-Klassen + - Daten-Zugriffschicht zur Kapselung der DB-Schicht + - Transfer-Datenmodel zum Austausch von Daten zwischen den Schichten + - technisches Transaktion-Handling und Lösung von Deadlocks + - Konzept in Arbeit +2. capturing alias + + - Konzept fertig + - Änderungen in Register- und Login-Prozess +3. Passwort-Verschlüsselung: Refactoring + + - Konzept aufteilen in Ausbaustufen + - Altlasten entsorgen + - Versionierung/Typisierung der verwendeten Verschlüsselungslogik notwendig + - DB-Migration auf encryptionType=EMAIL +4. Passwort-Verschlüsselung: Login mit impliziter Neuverschlüsselung + + * Logik der Passwortverschlüsselung auf GradidoID einführen + * bei Login mit encryptionType=Email oder OneTime triggern einer Neuverschlüsselung per GradidoID + * Unabhängigkeit von Email erzeugen + * Änderung der User-Email ermöglichen +5. Contribution-Categories + + - Bewertung und Kategorisierung von Schöpfungen: Was hat Wer für Wen geleistet? + - Regeln auf Categories ermöglichen + - Konzept in Arbeit +6. Statistics / Analysen +7. Contribution-Link editieren +8. User-Tagging + + - Eine UserTag dient zur einfachen Gruppierung gleichgesinnter oder örtlich gebundener User + - Motivation des User-Taggings: bilden kleinerer lokaler User-Gruppen und jeder kennt jeden + - Einführung einer UserTaggings-Tabelle und eine User-UserTaggings-Zuordnungs-Tabelle + - Ein Moderator kann im AdminInterface die Liste der UserTags pflegen + + - neues TAG anlegen + - vorhandenes TAG umbenennen + - ein TAG löschen, sofern kein User mehr diesem TAG zugeordnet ist + - Will ein User ein TAG zugeordnet werden, so kann dies nur ein Moderator im AdminInterface tun + - Ein Moderator kann im AdminInterface + + - ein TAG einem User zuordnen + - ein TAG von einem User entfernen + - wichtige UseCases: + + - Zuordnung eines Users zu einem TAG durch einen Moderator + - TAG spezifische Schöpfung + - User muss für seinen Beitrag ein TAG auswählen können, dem er zuvor zugeordnet wurde + - TAG-Moderator kann den Beitrag bestätigen, weil er den User mit dem TAG (persönlich) kennt +9. User-Beziehungen und Favoritenverwaltung + + - User-User-Zuordnung + - aus Tx-Liste die aktuellen Favoriten ermitteln + - Verwaltung von Zuordnungen + - Auswahl + - Berechtigungen + - Gruppierung + - Community-übergreifend + - User-Beziehungen +10. technische Ablösung der Email und Ersatz durch GradidoID + + * APIs / Links / etc mit Email anpassen, so dass keine Email mehr verwendet wird + * Email soll aber im Aussen für User optional noch verwendbar bleiben + * Intern erfolgt aber auf jedenfall ein Mapping auf GradidoID egal ob per Email oder Alias angefragt wird +11. Zeitzone + + - User sieht immer seine Locale-Zeit und Monate + - Admin sieht immer UTC-Zeit und Monate + - wichtiges Kriterium für Schöpfung ist das TargetDate ( heißt in DB contributionDate) + - Berechnung der möglichen Schöpfungen muss somit auf dem TargetDate der Schöpfung ermittelt werden! **(Ist-Zustand)** + - Kann es vorkommen, dass das TargetDate der Contribution vor dem CreationDate der TX liegt? Ja + - Beispiel: User in Tokyo Locale mit Offest +09:00 + + - aktiviert Contribution-Link mit Locale: 01.11.2022 07:00:00+09:00 = TargetDate = Zieldatum der Schöpfung + - die Contribution wird gespeichert mit + + - creationDate=31.10.2022 22:00:00 UTC + - contributionDate=01.11.2022 07:00:00 + - (neu) clientRequestTime=01.11.2022 07:00:00+09:00 + - durch automatische Bestätigung und sofortiger Transaktion wird die TX gespeichert mit + + - creationDate=31.10.2022 22:00:00 UTC + - **zwingende Prüfung aller Requeste: auf -12h <= ClientRequestTime <= +12h** + + - Prüfung auf Sommerzeiten und exotische Länder beachten + - + - zur Analyse und Problemverfolgung von Contributions immer original ClientRequestTime mit Offset in DB speichern + - Beispiel für täglichen Contribution-Link während des Monats: + + - 17.10.2022 22:00 +09:00 => 17.10.2022 UTC: 17.10.2022 13:00 UTC => 17.10.2022 + - 18.10.2022 02:00 +09:00 => 18.10.2022 UTC: 17.10.2022 17:00 UTC => 17.10.2022 !!!! darf nicht weil gleicher Tag !!! + - Beispiel für täglichen Contribution-Link am Monatswechsel: + + - 31.10.2022 22:00 +09:00 => 31.10.2022 UTC: 31.10.2022 15:00 UTC => 31.10.2022 + - 01.11.2022 07:00 +09:00 => 01.11.2022 UTC: 31.10.2022 22:00 UTC => 31.10.2022 !!!! darf nicht weil gleicher Tag !!! +12. Layout +13. Lastschriften-Link +14. Registrierung mit Redeem-Link: + + * bei inaktivem Konto, sprich bisher noch keine Email-Bestätigung, keine Buchung möglich + * somit speichern des Links zusammen mit OptIn-Code + * damit kann in einem Resend der ConfirmationEmail der Link auch korrekt wieder mitgeliefert werden +15. Manuelle User-Registrierung für Admin + + - soll am 10.12.2022 für den Tag bei den Galliern produktiv sein +16. Dezentralisierung / Federation + + - Hyperswarm + + - funktioniert schon im Prototyp + - alle Instanzen finden sich gegenseitig + - ToDo: + - Infos aus HyperSwarm in der Community speichern + - Prüfung ob neue mir noch unbekannte Community hinzugekommen ist? + - Triggern der Authentifizierungs- und Autorisierungs-Handshake für neue Community + - Authentifizierungs- und Autorisierungs-Handshake + - Inter-Community-Communication + - **ToDos**: + + - DB-Migration für Community-Tabelle, User-Community-Zuordnungen, UserRights-Tabelle + - Berechtigungen für Communities + - Register- und Login-Prozess für Community-Anmeldung anpassen + + - Auswahl-Box einer Community + - createUser mit Zuordnung zur ausgewählten Community + - Schöpfungsprozess auf angemeldete Community anpassen + + - "Beitrag einreichen"-Dialog auf angemeldete Community anpassen + - "meine Beiträge zum Gemeinwohl" mit Filter auf angemeldete Community anpassen + - "Gemeinschaft"-Dialog auf angemeldete Community anpassen + - "Mein Profil"-Dialog auf Communities anpassen + + - Umzug-Service in andere Community + - Löschen der Mitgliedschaft zu angemeldeter Community (Deaktivierung der Zuordnung "User-Community") + - "Senden"-Dialog mit Community-Auswahl + - "Transaktion"-Dialog mit Filter auf angemeldeter Community + - AdminInterface auf angemeldete Community anpassen + + - "Übersicht"-Dialog mit Filter auf angemeldete Community + - "Nutzersuche"-Dialog mit Filter auf angemeldete Community + - "Mehrfachschöpfung"-Dialog mit Filter auf angemeldete Comunity + - Subject/Texte/Footer/... der Email-Benachrichtigungen auf angemeldete Community anpassen + +## Priorisierung + +1. Contribution-Link editieren (vlt schon im vorherigen Bugfix-Release Ende Okt. 2022 fertig) +2. Passwort-Verschlüsselung: Refactoring **Konzeption fertig!!**! +3. Manuelle User-Registrierung für Admin (10.12.2022) **Konzeption ongoing!!**! +4. Passwort-Verschlüsselung: implizite Login-Neuverschlüsselung **Konzeption fertig!!**! +5. Layout +6. Zeitzone +7. Dezentralisierung / Federation +8. capturing alias **Konzeption fertig!!**! +9. Registrierung mit Redeem-Link: bei inaktivem Konto keine Buchung möglich +10. Subgruppierung / User-Tagging (einfacher Ansatz) +11. backend access layer +12. technische Ablösung der Email und Ersatz durch GradidoID +13. User-Beziehungen und Favoritenverwaltung +14. Lastschriften-Link +15. Contribution-Categories +16. Statistics / Analysen diff --git a/docu/graphics/RoadMap2022-2023.drawio b/docu/graphics/RoadMap2022-2023.drawio new file mode 100644 index 000000000..58b8dec94 --- /dev/null +++ b/docu/graphics/RoadMap2022-2023.drawio @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docu/graphics/RoadMap2022-2023.png b/docu/graphics/RoadMap2022-2023.png new file mode 100644 index 000000000..3ce8511a3 Binary files /dev/null and b/docu/graphics/RoadMap2022-2023.png differ