Merge branch 'master' into fix-wrong-balance

This commit is contained in:
Moriz Wahl 2022-11-25 14:44:39 +01:00 committed by GitHub
commit aa1a88bc11
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 653 additions and 59 deletions

View File

@ -10,7 +10,7 @@ Decimal.set({
}) })
const constants = { 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 DECAY_START_TIME: new Date('2021-05-13 17:46:31-0000'), // GMT+0
LOG4JS_CONFIG: 'log4js-config.json', LOG4JS_CONFIG: 'log4js-config.json',
// default log level on production should be info // default log level on production should be info

View File

@ -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
})

View File

@ -36,6 +36,9 @@ import { UserContact } from '@entity/UserContact'
import { OptInType } from '../enum/OptInType' import { OptInType } from '../enum/OptInType'
import { UserContactType } from '../enum/UserContactType' import { UserContactType } from '../enum/UserContactType'
import { bobBaumeister } from '@/seeds/users/bob-baumeister' 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' // import { klicktippSignIn } from '@/apis/KlicktippController'
@ -146,6 +149,7 @@ describe('UserResolver', () => {
publisherId: 1234, publisherId: 1234,
referrerId: null, referrerId: null,
contributionLinkId: null, contributionLinkId: null,
passwordEncryptionType: PasswordEncryptionType.NO_PASSWORD,
}, },
]) ])
const valUUID = validateUUID(user[0].gradidoID) const valUUID = validateUUID(user[0].gradidoID)
@ -491,7 +495,8 @@ describe('UserResolver', () => {
}) })
it('updates the password', () => { 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', () => { describe('printTimeDuration', () => {

View File

@ -40,17 +40,15 @@ import { SearchAdminUsersResult } from '@model/AdminUser'
import Paginated from '@arg/Paginated' import Paginated from '@arg/Paginated'
import { Order } from '@enum/Order' import { Order } from '@enum/Order'
import { v4 as uuidv4 } from 'uuid' 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 // eslint-disable-next-line @typescript-eslint/no-var-requires
const sodium = require('sodium-native') const sodium = require('sodium-native')
// eslint-disable-next-line @typescript-eslint/no-var-requires // eslint-disable-next-line @typescript-eslint/no-var-requires
const random = require('random-bigint') 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 LANGUAGES = ['de', 'en', 'es', 'fr', 'nl']
const DEFAULT_LANGUAGE = 'de' const DEFAULT_LANGUAGE = 'de'
const isLanguage = (language: string): boolean => { const isLanguage = (language: string): boolean => {
@ -107,48 +105,6 @@ const KeyPairEd25519Create = (passphrase: string[]): Buffer[] => {
return [pubKey, privKey] 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 => { const getEmailHash = (email: string): Buffer => {
logger.trace('getEmailHash...') 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 // TODO we want to catch this on the frontend and ask the user to check his emails or resend code
throw new Error('User has no private or publicKey') throw new Error('User has no private or publicKey')
} }
const passwordHash = SecretKeyCryptographyCreateKey(email, password) // return short and long hash
const loginUserPassword = BigInt(dbUser.password.toString()) if (!verifyPassword(dbUser, password)) {
if (loginUserPassword !== passwordHash[0].readBigUInt64LE()) {
logger.error('The User has no valid credentials.') logger.error('The User has no valid credentials.')
throw new Error('No user with this 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 // add pubKey in logger-context for layout-pattern X{user} to print it in each logging message
logger.addContext('user', dbUser.id) logger.addContext('user', dbUser.id)
logger.debug('validation of login credentials successful...') logger.debug('validation of login credentials successful...')
@ -481,6 +442,7 @@ export class UserResolver {
dbUser.lastName = lastName dbUser.lastName = lastName
dbUser.language = language dbUser.language = language
dbUser.publisherId = publisherId dbUser.publisherId = publisherId
dbUser.passwordEncryptionType = PasswordEncryptionType.NO_PASSWORD
dbUser.passphrase = passphrase.join(' ') dbUser.passphrase = passphrase.join(' ')
logger.debug('new dbUser=' + dbUser) logger.debug('new dbUser=' + dbUser)
if (redeemCode) { if (redeemCode) {
@ -634,7 +596,7 @@ export class UserResolver {
): Promise<boolean> { ): Promise<boolean> {
logger.info(`setPassword(${code}, ***)...`) logger.info(`setPassword(${code}, ***)...`)
// Validate Password // Validate Password
if (!isPassword(password)) { if (!isValidPassword(password)) {
logger.error('Password entered is lexically invalid') logger.error('Password entered is lexically invalid')
throw new Error( throw new Error(
'Please enter a valid password with at least 8 characters, upper and lower case letters, at least one number and one special character!', 'Please enter a valid password with at least 8 characters, upper and lower case letters, at least one number and one special character!',
@ -692,10 +654,11 @@ export class UserResolver {
userContact.emailChecked = true userContact.emailChecked = true
// Update Password // Update Password
user.passwordEncryptionType = PasswordEncryptionType.GRADIDO_ID
const passwordHash = SecretKeyCryptographyCreateKey(userContact.email, password) // return short and long hash const passwordHash = SecretKeyCryptographyCreateKey(userContact.email, password) // return short and long hash
const keyPair = KeyPairEd25519Create(passphrase) // return pub, priv Key const keyPair = KeyPairEd25519Create(passphrase) // return pub, priv Key
const encryptedPrivkey = SecretKeyCryptographyEncrypt(keyPair[1], passwordHash[1]) 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.pubKey = keyPair[0]
user.privKey = encryptedPrivkey user.privKey = encryptedPrivkey
logger.debug('User credentials updated ...') logger.debug('User credentials updated ...')
@ -801,7 +764,7 @@ export class UserResolver {
if (password && passwordNew) { if (password && passwordNew) {
// Validate Password // Validate Password
if (!isPassword(passwordNew)) { if (!isValidPassword(passwordNew)) {
logger.error('newPassword does not fullfil the rules') logger.error('newPassword does not fullfil the rules')
throw new Error( throw new Error(
'Please enter a valid password with at least 8 characters, upper and lower case letters, at least one number and one special character!', 'Please enter a valid password with at least 8 characters, upper and lower case letters, at least one number and one special character!',
@ -813,7 +776,7 @@ export class UserResolver {
userEntity.emailContact.email, userEntity.emailContact.email,
password, password,
) )
if (BigInt(userEntity.password.toString()) !== oldPasswordHash[0].readBigUInt64LE()) { if (!verifyPassword(userEntity, password)) {
logger.error(`Old password is invalid`) logger.error(`Old password is invalid`)
throw new Error(`Old password is invalid`) throw new Error(`Old password is invalid`)
} }
@ -829,7 +792,8 @@ export class UserResolver {
logger.debug('PrivateKey encrypted...') logger.debug('PrivateKey encrypted...')
// Save new password hash and newly encrypted private key // Save new password hash and newly encrypted private key
userEntity.password = newPasswordHash[0].readBigUInt64LE() userEntity.passwordEncryptionType = PasswordEncryptionType.GRADIDO_ID
userEntity.password = encryptPassword(userEntity, passwordNew)
userEntity.privKey = encryptedPrivkey userEntity.privKey = encryptedPrivkey
} }

View File

@ -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}`)
}
}

View File

@ -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()
}

View File

@ -1,5 +1,6 @@
/* eslint-disable @typescript-eslint/no-unused-vars */ /* eslint-disable @typescript-eslint/no-unused-vars */
import { PasswordEncryptionType } from '@/graphql/enum/PasswordEncryptionType'
import { SaveOptions, RemoveOptions } from '@dbTools/typeorm' import { SaveOptions, RemoveOptions } from '@dbTools/typeorm'
import { User as dbUser } from '@entity/User' import { User as dbUser } from '@entity/User'
import { UserContact } from '@entity/UserContact' import { UserContact } from '@entity/UserContact'
@ -26,6 +27,8 @@ const communityDbUser: dbUser = {
isAdmin: null, isAdmin: null,
publisherId: 0, publisherId: 0,
passphrase: '', passphrase: '',
// default password encryption type
passwordEncryptionType: PasswordEncryptionType.NO_PASSWORD,
hasId: function (): boolean { hasId: function (): boolean {
throw new Error('Function not implemented.') throw new Error('Function not implemented.')
}, },

View File

@ -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[]
}

View File

@ -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
}

View File

@ -1 +1 @@
export { User } from './0049-add_user_contacts_table/User' export { User } from './0053-change_password_encryption/User'

View File

@ -1 +1 @@
export { UserContact } from './0049-add_user_contacts_table/UserContact' export { UserContact } from './0053-change_password_encryption/UserContact'

View File

@ -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<Array<any>>) {
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<Array<any>>) {
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;')
}

167
docu/RoadMap_2022-2023.md Normal file
View File

@ -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

View File

@ -0,0 +1,60 @@
<mxfile host="65bd71144e">
<diagram id="CdUoMVivL2xThNJutTjM" name="Seite-1">
<mxGraphModel dx="1022" dy="800" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="2336" pageHeight="1654" math="0" shadow="0">
<root>
<mxCell id="0"/>
<mxCell id="1" parent="0"/>
<mxCell id="14" style="edgeStyle=none;html=1;entryX=0;entryY=0.5;entryDx=0;entryDy=0;fontSize=16;" edge="1" parent="1" source="2" target="7">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="160" y="100"/>
</Array>
</mxGeometry>
</mxCell>
<mxCell id="2" value="capturing alias" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;gradientColor=#b3b3b3;strokeColor=#666666;fontSize=16;" vertex="1" parent="1">
<mxGeometry x="40" y="40" width="240" height="40" as="geometry"/>
</mxCell>
<mxCell id="3" value="Manuelle User-Registrierung für Admin (10.12.2022)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;gradientColor=#b3b3b3;strokeColor=#666666;fontSize=16;" vertex="1" parent="1">
<mxGeometry x="40" y="200" width="440" height="40" as="geometry"/>
</mxCell>
<mxCell id="4" value="Zeitzone" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;gradientColor=#b3b3b3;strokeColor=#666666;fontSize=16;" vertex="1" parent="1">
<mxGeometry x="40" y="280" width="440" height="40" as="geometry"/>
</mxCell>
<mxCell id="5" value="User-Beziehungen und Favoritenverwaltung" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;gradientColor=#b3b3b3;strokeColor=#666666;fontSize=16;" vertex="1" parent="1">
<mxGeometry x="40" y="360" width="440" height="40" as="geometry"/>
</mxCell>
<mxCell id="6" value="Layout" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;gradientColor=#b3b3b3;strokeColor=#666666;fontSize=16;" vertex="1" parent="1">
<mxGeometry x="40" y="440" width="440" height="40" as="geometry"/>
</mxCell>
<mxCell id="15" style="edgeStyle=none;html=1;entryX=0;entryY=0.5;entryDx=0;entryDy=0;fontSize=16;" edge="1" parent="1" source="7" target="12">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="440" y="140"/>
</Array>
</mxGeometry>
</mxCell>
<mxCell id="7" value="Passwort-Verschlüsselung" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;gradientColor=#b3b3b3;strokeColor=#666666;fontSize=16;" vertex="1" parent="1">
<mxGeometry x="320" y="80" width="240" height="40" as="geometry"/>
</mxCell>
<mxCell id="8" value="Subgruppierung / Subcommunities" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;gradientColor=#b3b3b3;strokeColor=#666666;fontSize=16;" vertex="1" parent="1">
<mxGeometry x="40" y="520" width="440" height="40" as="geometry"/>
</mxCell>
<mxCell id="9" value="Contribution-Categories" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;gradientColor=#b3b3b3;strokeColor=#666666;fontSize=16;" vertex="1" parent="1">
<mxGeometry x="40" y="600" width="440" height="40" as="geometry"/>
</mxCell>
<mxCell id="10" value="backend access layer" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;gradientColor=#b3b3b3;strokeColor=#666666;fontSize=16;" vertex="1" parent="1">
<mxGeometry x="40" y="680" width="440" height="40" as="geometry"/>
</mxCell>
<mxCell id="11" value="Statistics / Analysen" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;gradientColor=#b3b3b3;strokeColor=#666666;fontSize=16;" vertex="1" parent="1">
<mxGeometry x="40" y="760" width="440" height="40" as="geometry"/>
</mxCell>
<mxCell id="12" value="Ablösung der Email und Ersatz durch GradidoID" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;gradientColor=#b3b3b3;strokeColor=#666666;fontSize=16;" vertex="1" parent="1">
<mxGeometry x="600" y="120" width="360" height="40" as="geometry"/>
</mxCell>
<mxCell id="13" value="Dezentralisierung / Federation" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;gradientColor=#b3b3b3;strokeColor=#666666;fontSize=16;" vertex="1" parent="1">
<mxGeometry x="40" y="840" width="440" height="40" as="geometry"/>
</mxCell>
</root>
</mxGraphModel>
</diagram>
</mxfile>

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB