Merge branch 'master' into ADMINBEREICH-first-step

This commit is contained in:
Ulf Gebhardt 2021-11-18 00:35:06 +01:00 committed by GitHub
commit 72c17d349d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 8786 additions and 93 deletions

View File

@ -399,7 +399,7 @@ jobs:
report_name: Coverage Frontend
type: lcov
result_path: ./coverage/lcov.info
min_coverage: 85
min_coverage: 86
token: ${{ github.token }}
##############################################################################
@ -491,7 +491,7 @@ jobs:
report_name: Coverage Backend
type: lcov
result_path: ./backend/coverage/lcov.info
min_coverage: 41
min_coverage: 39
token: ${{ github.token }}
##############################################################################

View File

@ -18,6 +18,8 @@ DB_DATABASE=gradido_community
#EMAIL_SMTP_URL=
#EMAIL_SMTP_PORT=587
#EMAIL_LINK_VERIFICATION=http://localhost/vue/checkEmail/$1
#KLICKTIPP_USER=
#KLICKTIPP_PASSWORD=
#KLICKTIPP_APIKEY_DE=
@ -26,4 +28,6 @@ DB_DATABASE=gradido_community
COMMUNITY_NAME=
COMMUNITY_URL=
COMMUNITY_REGISTER_URL=
COMMUNITY_DESCRIPTION=
COMMUNITY_DESCRIPTION=
LOGIN_APP_SECRET=21ffbbc616fe
LOGIN_SERVER_KEY=a51ef8ac7ef1abf162fb7a65261acd7a

View File

@ -27,11 +27,12 @@
"graphql": "^15.5.1",
"jest": "^27.2.4",
"jsonwebtoken": "^8.5.1",
"libsodium-wrappers": "^0.7.9",
"module-alias": "^2.2.2",
"mysql2": "^2.3.0",
"nodemailer": "^6.6.5",
"random-bigint": "^0.0.1",
"reflect-metadata": "^0.1.13",
"sodium-native": "^3.3.0",
"ts-jest": "^27.0.5",
"type-graphql": "^1.1.1",
"typeorm": "^0.2.38"
@ -39,7 +40,6 @@
"devDependencies": {
"@types/express": "^4.17.12",
"@types/jsonwebtoken": "^8.5.2",
"@types/libsodium-wrappers": "^0.7.9",
"@types/node": "^16.10.3",
"@types/nodemailer": "^6.4.4",
"@typescript-eslint/eslint-plugin": "^4.28.0",

View File

@ -39,6 +39,11 @@ const community = {
process.env.COMMUNITY_DESCRIPTION || 'Die lokale Entwicklungsumgebung von Gradido.',
}
const loginServer = {
LOGIN_APP_SECRET: process.env.LOGIN_APP_SECRET || '21ffbbc616fe',
LOGIN_SERVER_KEY: process.env.LOGIN_SERVER_KEY || 'a51ef8ac7ef1abf162fb7a65261acd7a',
}
const email = {
EMAIL: process.env.EMAIL === 'true' || false,
EMAIL_USERNAME: process.env.EMAIL_USERNAME || 'gradido_email',
@ -46,11 +51,14 @@ const email = {
EMAIL_PASSWORD: process.env.EMAIL_PASSWORD || 'xxx',
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/vue/checkEmail/$1',
}
// This is needed by graphql-directive-auth
process.env.APP_SECRET = server.JWT_SECRET
const CONFIG = { ...server, ...database, ...klicktipp, ...community, ...email }
const CONFIG = { ...server, ...database, ...klicktipp, ...community, ...email, ...loginServer }
export default CONFIG

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -48,7 +48,7 @@ describe('CommunityResolver', () => {
describe('getCommunityInfo', () => {
it('returns the default values', async () => {
expect(query({ query: getCommunityInfoQuery })).resolves.toMatchObject({
await expect(query({ query: getCommunityInfoQuery })).resolves.toMatchObject({
data: {
getCommunityInfo: {
name: 'Gradido Entwicklung',
@ -68,7 +68,7 @@ describe('CommunityResolver', () => {
})
it('returns three communities', async () => {
expect(query({ query: communities })).resolves.toMatchObject({
await expect(query({ query: communities })).resolves.toMatchObject({
data: {
communities: [
{
@ -104,7 +104,7 @@ describe('CommunityResolver', () => {
})
it('returns one community', async () => {
expect(query({ query: communities })).resolves.toMatchObject({
await expect(query({ query: communities })).resolves.toMatchObject({
data: {
communities: [
{

View File

@ -4,9 +4,9 @@
import { Resolver, Query, Args, Authorized, Ctx, Mutation } from 'type-graphql'
import { getCustomRepository, getConnection, QueryRunner } from 'typeorm'
import { createTransport } from 'nodemailer'
import CONFIG from '../../config'
import { sendEMail } from '../../util/sendEMail'
import { Transaction } from '../model/Transaction'
import { TransactionList } from '../model/TransactionList'
@ -33,7 +33,6 @@ import { calculateDecay, calculateDecayWithInterval } from '../../util/decay'
import { TransactionTypeId } from '../enum/TransactionTypeId'
import { TransactionType } from '../enum/TransactionType'
import { hasUserAmount, isHexPublicKey } from '../../util/validate'
import { from_hex as fromHex } from 'libsodium-wrappers'
/*
# Test
@ -201,29 +200,6 @@ INSERT INTO `transaction_signatures` (`id`, `transaction_id`, `signature`, `pubk
(1, 1, 0x60d632479707e5d01cdc32c3326b5a5bae11173a0c06b719ee7b552f9fd644de1a0cd4afc207253329081d39dac1a63421f51571d836995c649fc39afac7480a, 0x48c45cb4fea925e83850f68f2fa8f27a1a4ed1bcba68cdb59fcd86adef3f52ee);
*/
const sendEMail = async (emailDef: any): Promise<boolean> => {
if (!CONFIG.EMAIL) {
// eslint-disable-next-line no-console
console.log('Emails are disabled via config')
return false
}
const transporter = createTransport({
host: CONFIG.EMAIL_SMTP_URL,
port: Number(CONFIG.EMAIL_SMTP_PORT),
secure: false, // true for 465, false for other ports
requireTLS: true,
auth: {
user: CONFIG.EMAIL_USERNAME,
pass: CONFIG.EMAIL_PASSWORD,
},
})
const info = await transporter.sendMail(emailDef)
if (!info.messageId) {
throw new Error('error sending notification email, but transaction succeed')
}
return true
}
// Helper function
async function calculateAndAddDecayTransactions(
userTransactions: dbUserTransaction[],
@ -622,7 +598,7 @@ export class TransactionResolver {
transactionSendCoin.userId = senderUser.id
transactionSendCoin.senderPublic = senderUser.pubkey
transactionSendCoin.recipiantUserId = recipiantUser.id
transactionSendCoin.recipiantPublic = Buffer.from(fromHex(recipiantPublicKey))
transactionSendCoin.recipiantPublic = Buffer.from(recipiantPublicKey, 'hex')
transactionSendCoin.amount = centAmount
transactionSendCoin.senderFinalBalance = senderStateBalance.amount
await queryRunner.manager.save(transactionSendCoin).catch((error) => {
@ -654,8 +630,8 @@ export class TransactionResolver {
// send notification email
// TODO: translate
await sendEMail({
from: 'Gradido (nicht antworten) <' + CONFIG.EMAIL_SENDER + '>',
to: recipiantUser.firstName + ' ' + recipiantUser.lastName + ' <' + recipiantUser.email + '>',
from: `Gradido (nicht antworten) <${CONFIG.EMAIL_SENDER}>`,
to: `${recipiantUser.firstName} ${recipiantUser.lastName} <${recipiantUser.email}>`,
subject: 'Gradido Überweisung',
text: `Hallo ${recipiantUser.firstName} ${recipiantUser.lastName}
@ -666,7 +642,8 @@ export class TransactionResolver {
Bitte antworte nicht auf diese E-Mail!
Mit freundlichen Grüßen Gradido Community Server`,
Mit freundlichen Grüßen,
dein Gradido-Team`,
})
return 'success'

View File

@ -1,9 +1,9 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import fs from 'fs'
import { Resolver, Query, Args, Arg, Authorized, Ctx, UseMiddleware, Mutation } from 'type-graphql'
import { from_hex as fromHex } from 'libsodium-wrappers'
import { getCustomRepository } from 'typeorm'
import { getConnection, getCustomRepository } from 'typeorm'
import CONFIG from '../../config'
import { LoginViaVerificationCode } from '../model/LoginViaVerificationCode'
import { SendPasswordResetEmailResponse } from '../model/SendPasswordResetEmailResponse'
@ -26,6 +26,161 @@ import { UserSettingRepository } from '../../typeorm/repository/UserSettingRepos
import { Setting } from '../enum/Setting'
import { UserRepository } from '../../typeorm/repository/User'
import { LoginUser } from '@entity/LoginUser'
import { LoginElopageBuys } from '@entity/LoginElopageBuys'
import { LoginUserBackup } from '@entity/LoginUserBackup'
import { LoginEmailOptIn } from '@entity/LoginEmailOptIn'
import { sendEMail } from '../../util/sendEMail'
// 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']
const DEFAULT_LANGUAGE = 'de'
const isLanguage = (language: string): boolean => {
return LANGUAGES.includes(language)
}
const PHRASE_WORD_COUNT = 24
const WORDS = fs.readFileSync('src/config/mnemonic.english.txt').toString().split('\n')
const PassphraseGenerate = (): string[] => {
const result = []
for (let i = 0; i < PHRASE_WORD_COUNT; i++) {
result.push(WORDS[sodium.randombytes_random() % 2048])
}
return result
/*
return [
'behind',
'salmon',
'fluid',
'orphan',
'frost',
'elder',
'amateur',
'always',
'panel',
'palm',
'leopard',
'essay',
'punch',
'title',
'fun',
'annual',
'page',
'hundred',
'journey',
'select',
'figure',
'tunnel',
'casual',
'bar',
]
*/
}
/*
Test results:
INSERT INTO `login_users` (`id`, `email`, `first_name`, `last_name`, `username`, `description`, `password`, `pubkey`, `privkey`, `email_hash`, `created`, `email_checked`, `passphrase_shown`, `language`, `disabled`, `group_id`, `publisher_id`) VALUES
// old
(1, 'peter@lustig.de', 'peter', 'lustig', '', '', 4747956395458240931, 0x8c75edd507f470e5378f927489374694d68f3d155523f1c4402c36affd35a7ed, 0xb0e310655726b088631ccfd31ad6470ee50115c161dde8559572fa90657270ff13dc1200b2d3ea90dfbe92f3a4475ee4d9cee4989e39736a0870c33284bc73a8ae690e6da89f241a121eb3b500c22885, 0x9f700e6f6ec351a140b674c0edd4479509697b023bd8bee8826915ef6c2af036, '2021-11-03 20:05:04', 0, 0, 'de', 0, 1, 0);
// new
(2, 'peter@lustig.de', 'peter', 'lustig', '', '', 4747956395458240931, 0x8c75edd507f470e5378f927489374694d68f3d155523f1c4402c36affd35a7ed, 0xb0e310655726b088631ccfd31ad6470ee50115c161dde8559572fa90657270ff13dc1200b2d3ea90dfbe92f3a4475ee4d9cee4989e39736a0870c33284bc73a8ae690e6da89f241a121eb3b500c22885, 0x9f700e6f6ec351a140b674c0edd4479509697b023bd8bee8826915ef6c2af036, '2021-11-03 20:22:15', 0, 0, 'de', 0, 1, 0);
INSERT INTO `login_user_backups` (`id`, `user_id`, `passphrase`, `mnemonic_type`) VALUES
// old
(1, 1, 'behind salmon fluid orphan frost elder amateur always panel palm leopard essay punch title fun annual page hundred journey select figure tunnel casual bar ', 2);
// new
(2, 2, 'behind salmon fluid orphan frost elder amateur always panel palm leopard essay punch title fun annual page hundred journey select figure tunnel casual bar ', 2);
*/
const KeyPairEd25519Create = (passphrase: string[]): Buffer[] => {
if (!passphrase.length || passphrase.length < PHRASE_WORD_COUNT) {
throw new Error('passphrase empty or to short')
}
const state = Buffer.alloc(sodium.crypto_hash_sha512_STATEBYTES)
sodium.crypto_hash_sha512_init(state)
// To prevent breaking existing passphrase-hash combinations word indices will be put into 64 Bit Variable to mimic first implementation of algorithms
for (let i = 0; i < PHRASE_WORD_COUNT; i++) {
const value = Buffer.alloc(8)
const wordIndex = WORDS.indexOf(passphrase[i])
value.writeBigInt64LE(BigInt(wordIndex))
sodium.crypto_hash_sha512_update(state, value)
}
// trailing space is part of the login_server implementation
const clearPassphrase = passphrase.join(' ') + ' '
sodium.crypto_hash_sha512_update(state, Buffer.from(clearPassphrase))
const outputHashBuffer = Buffer.alloc(sodium.crypto_hash_sha512_BYTES)
sodium.crypto_hash_sha512_final(state, outputHashBuffer)
const pubKey = Buffer.alloc(sodium.crypto_sign_PUBLICKEYBYTES)
const privKey = Buffer.alloc(sodium.crypto_sign_SECRETKEYBYTES)
sodium.crypto_sign_seed_keypair(
pubKey,
privKey,
outputHashBuffer.slice(0, sodium.crypto_sign_SEEDBYTES),
)
return [pubKey, privKey]
}
const SecretKeyCryptographyCreateKey = (salt: string, password: string): Buffer[] => {
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) {
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]
}
const getEmailHash = (email: string): Buffer => {
const emailHash = Buffer.alloc(sodium.crypto_generichash_BYTES)
sodium.crypto_generichash(emailHash, Buffer.from(email))
return emailHash
}
const SecretKeyCryptographyEncrypt = (message: Buffer, encryptionKey: Buffer): Buffer => {
const encrypted = Buffer.alloc(sodium.crypto_secretbox_MACBYTES + message.length)
const nonce = Buffer.alloc(sodium.crypto_secretbox_NONCEBYTES)
nonce.fill(31) // static nonce
sodium.crypto_secretbox_easy(encrypted, message, nonce, encryptionKey)
return encrypted
}
@Resolver()
export class UserResolver {
@ -62,7 +217,7 @@ export class UserResolver {
userEntity.lastName = user.lastName
userEntity.username = user.username
userEntity.email = user.email
userEntity.pubkey = Buffer.from(fromHex(user.pubkey))
userEntity.pubkey = Buffer.from(user.pubkey, 'hex')
userRepository.save(userEntity).catch(() => {
throw new Error('error by save userEntity')
@ -108,47 +263,152 @@ export class UserResolver {
@Authorized()
@Query(() => String)
async logout(@Ctx() context: any): Promise<string> {
const payload = { session_id: context.sessionId }
const result = await apiPost(CONFIG.LOGIN_API_URL + 'logout', payload)
if (!result.success) {
throw new Error(result.data)
}
return 'success'
async logout(): Promise<boolean> {
// TODO: We dont need this anymore, but might need this in the future in oder to invalidate a valid JWT-Token.
// Furthermore this hook can be useful for tracking user behaviour (did he logout or not? Warn him if he didn't on next login)
// The functionality is fully client side - the client just needs to delete his token with the current implementation.
// we could try to force this by sending `token: null` or `token: ''` with this call. But since it bares no real security
// we should just return true for now.
return true
}
@Mutation(() => String)
async createUser(
@Args() { email, firstName, lastName, password, language, publisherId }: CreateUserArgs,
): Promise<string> {
const payload = {
email,
first_name: firstName,
last_name: lastName,
password,
emailType: 2,
login_after_register: true,
language: language,
publisher_id: publisherId,
}
const result = await apiPost(CONFIG.LOGIN_API_URL + 'createUser', payload)
if (!result.success) {
throw new Error(result.data)
// TODO: wrong default value (should be null), how does graphql work here? Is it an required field?
// default int publisher_id = 0;
// Validate Language (no throw)
if (!isLanguage(language)) {
language = DEFAULT_LANGUAGE
}
const user = new User(result.data.user)
const dbuser = new DbUser()
dbuser.pubkey = Buffer.from(fromHex(user.pubkey))
dbuser.email = user.email
dbuser.firstName = user.firstName
dbuser.lastName = user.lastName
dbuser.username = user.username
// Validate Password
if (!isPassword(password)) {
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!',
)
}
// Validate username
// TODO: never true
const username = ''
if (username.length > 3 && !this.checkUsername({ username })) {
throw new Error('Username already in use')
}
// Validate email unique
// TODO: i can register an email in upper/lower case twice
const userRepository = getCustomRepository(UserRepository)
userRepository.save(dbuser).catch(() => {
throw new Error('error saving user')
})
const usersFound = await userRepository.count({ email })
if (usersFound !== 0) {
// TODO: this is unsecure, but the current implementation of the login server. This way it can be queried if the user with given EMail is existent.
throw new Error(`User already exists.`)
}
const passphrase = PassphraseGenerate()
const keyPair = KeyPairEd25519Create(passphrase) // return pub, priv Key
const passwordHash = SecretKeyCryptographyCreateKey(email, password) // return short and long hash
const emailHash = getEmailHash(email)
const encryptedPrivkey = SecretKeyCryptographyEncrypt(keyPair[1], passwordHash[1])
// Table: login_users
const loginUser = new LoginUser()
loginUser.email = email
loginUser.firstName = firstName
loginUser.lastName = lastName
loginUser.username = username
loginUser.description = ''
loginUser.password = passwordHash[0].readBigUInt64LE() // using the shorthash
loginUser.emailHash = emailHash
loginUser.language = language
loginUser.groupId = 1
loginUser.publisherId = publisherId
loginUser.pubKey = keyPair[0]
loginUser.privKey = encryptedPrivkey
const queryRunner = getConnection().createQueryRunner()
await queryRunner.connect()
await queryRunner.startTransaction('READ UNCOMMITTED')
try {
const { id: loginUserId } = await queryRunner.manager.save(loginUser).catch((error) => {
// eslint-disable-next-line no-console
console.log('insert LoginUser failed', error)
throw new Error('insert user failed')
})
// Table: login_user_backups
const loginUserBackup = new LoginUserBackup()
loginUserBackup.userId = loginUserId
loginUserBackup.passphrase = passphrase.join(' ') + ' ' // login server saves trailing space
loginUserBackup.mnemonicType = 2 // ServerConfig::MNEMONIC_BIP0039_SORTED_ORDER;
await queryRunner.manager.save(loginUserBackup).catch((error) => {
// eslint-disable-next-line no-console
console.log('insert LoginUserBackup failed', error)
throw new Error('insert user backup failed')
})
// Table: state_users
const dbUser = new DbUser()
dbUser.pubkey = keyPair[0]
dbUser.email = email
dbUser.firstName = firstName
dbUser.lastName = lastName
dbUser.username = username
await queryRunner.manager.save(dbUser).catch((er) => {
// eslint-disable-next-line no-console
console.log('Error while saving dbUser', er)
throw new Error('error saving user')
})
// Store EmailOptIn in DB
const emailOptIn = new LoginEmailOptIn()
emailOptIn.userId = loginUserId
emailOptIn.verificationCode = random(64)
emailOptIn.emailOptInTypeId = 2
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')
})
// Send EMail to user
const activationLink = CONFIG.EMAIL_LINK_VERIFICATION.replace(
/\$1/g,
emailOptIn.verificationCode.toString(),
)
const emailSent = await sendEMail({
from: `Gradido (nicht antworten) <${CONFIG.EMAIL_SENDER}>`,
to: `${firstName} ${lastName} <${email}>`,
subject: 'Gradido: E-Mail Überprüfung',
text: `Hallo ${firstName} ${lastName},
Deine EMail wurde soeben bei Gradido registriert.
Klicke bitte auf diesen Link, um die Registrierung abzuschließen und dein Gradido-Konto zu aktivieren:
${activationLink}
oder kopiere den obigen Link in dein Browserfenster.
Mit freundlichen Grüßen,
dein Gradido-Team`,
})
// 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 'success'
}
@ -311,12 +571,16 @@ export class UserResolver {
return new CheckEmailResponse(result.data)
}
@Authorized()
@Query(() => Boolean)
async hasElopage(@Ctx() context: any): Promise<boolean> {
const result = await apiGet(CONFIG.LOGIN_API_URL + 'hasElopage?session_id=' + context.sessionId)
if (!result.success) {
throw new Error(result.data)
const userRepository = getCustomRepository(UserRepository)
const userEntity = await userRepository.findByPubkeyHex(context.pubKey).catch()
if (!userEntity) {
return false
}
return result.data.hasElopage
const elopageBuyCount = await LoginElopageBuys.count({ payerEmail: userEntity.email })
return elopageBuyCount > 0
}
}

View File

@ -0,0 +1,5 @@
import { EntityRepository, Repository } from 'typeorm'
import { LoginEmailOptIn } from '@entity/LoginEmailOptIn'
@EntityRepository(LoginEmailOptIn)
export class LoginEmailOptInRepository extends Repository<LoginEmailOptIn> {}

View File

@ -0,0 +1,5 @@
import { EntityRepository, Repository } from 'typeorm'
import { LoginUser } from '@entity/LoginUser'
@EntityRepository(LoginUser)
export class LoginUserRepository extends Repository<LoginUser> {}

View File

@ -0,0 +1,5 @@
import { EntityRepository, Repository } from 'typeorm'
import { LoginUserBackup } from '@entity/LoginUserBackup'
@EntityRepository(LoginUserBackup)
export class LoginUserBackupRepository extends Repository<LoginUserBackup> {}

View File

@ -0,0 +1,26 @@
import { createTransport } from 'nodemailer'
import CONFIG from '../config'
export const sendEMail = async (emailDef: any): Promise<boolean> => {
if (!CONFIG.EMAIL) {
// eslint-disable-next-line no-console
console.log('Emails are disabled via config')
return false
}
const transporter = createTransport({
host: CONFIG.EMAIL_SMTP_URL,
port: Number(CONFIG.EMAIL_SMTP_PORT),
secure: false, // true for 465, false for other ports
requireTLS: true,
auth: {
user: CONFIG.EMAIL_USERNAME,
pass: CONFIG.EMAIL_PASSWORD,
},
})
const info = await transporter.sendMail(emailDef)
if (!info.messageId) {
throw new Error('error sending notification email, but transaction succeed')
}
return true
}

View File

@ -918,11 +918,6 @@
"@types/koa-compose" "*"
"@types/node" "*"
"@types/libsodium-wrappers@^0.7.9":
version "0.7.9"
resolved "https://registry.yarnpkg.com/@types/libsodium-wrappers/-/libsodium-wrappers-0.7.9.tgz#89c3ad2156d5143e64bce86cfeb0045a983aeccc"
integrity sha512-LisgKLlYQk19baQwjkBZZXdJL0KbeTpdEnrAfz5hQACbklCY0gVFnsKUyjfNWF1UQsCSjw93Sj5jSbiO8RPfdw==
"@types/long@^4.0.0":
version "4.0.1"
resolved "https://registry.yarnpkg.com/@types/long/-/long-4.0.1.tgz#459c65fa1867dafe6a8f322c4c51695663cc55e9"
@ -3918,18 +3913,6 @@ libphonenumber-js@^1.9.7:
resolved "https://registry.yarnpkg.com/libphonenumber-js/-/libphonenumber-js-1.9.37.tgz#944f59a3618a8f85d9b619767a0b6fb87523f285"
integrity sha512-RnUR4XwiVhMLnT7uFSdnmLeprspquuDtaShAgKTA+g/ms9/S4hQU3/QpFdh3iXPHtxD52QscXLm2W2+QBmvYAg==
libsodium-wrappers@^0.7.9:
version "0.7.9"
resolved "https://registry.yarnpkg.com/libsodium-wrappers/-/libsodium-wrappers-0.7.9.tgz#4ffc2b69b8f7c7c7c5594a93a4803f80f6d0f346"
integrity sha512-9HaAeBGk1nKTRFRHkt7nzxqCvnkWTjn1pdjKgcUnZxj0FyOP4CnhgFhMdrFfgNsukijBGyBLpP2m2uKT1vuWhQ==
dependencies:
libsodium "^0.7.0"
libsodium@^0.7.0:
version "0.7.9"
resolved "https://registry.yarnpkg.com/libsodium/-/libsodium-0.7.9.tgz#4bb7bcbf662ddd920d8795c227ae25bbbfa3821b"
integrity sha512-gfeADtR4D/CM0oRUviKBViMGXZDgnFdMKMzHsvBdqLBHd9ySi6EtYnmuhHVDDYgYpAO8eU8hEY+F8vIUAPh08A==
load-json-file@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-4.0.0.tgz#2f5f45ab91e33216234fd53adab668eb4ec0993b"
@ -4223,6 +4206,11 @@ node-fetch@^2.6.1:
dependencies:
whatwg-url "^5.0.0"
node-gyp-build@^4.3.0:
version "4.3.0"
resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.3.0.tgz#9f256b03e5826150be39c764bf51e993946d71a3"
integrity sha512-iWjXZvmboq0ja1pUGULQBexmxq8CV4xBhX7VDOTbL7ZR4FOowwY/VOtRxBN/yKxmdGoIp4j5ysNT4u3S2pDQ3Q==
node-int64@^0.4.0:
version "0.4.0"
resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b"
@ -4674,6 +4662,11 @@ queue-microtask@^1.2.2:
resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243"
integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==
random-bigint@^0.0.1:
version "0.0.1"
resolved "https://registry.yarnpkg.com/random-bigint/-/random-bigint-0.0.1.tgz#684de0a93784ab7448a441393916f0e632c95df9"
integrity sha512-X+NTsf5Hzl/tRNLiNTD3N1LRU0eKdIE0+plNlV1CmXLTlnAxj6HipcTnOhWvFRoSytCz6l1f4KYFf/iH8NNSLw==
range-parser@~1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031"
@ -4966,6 +4959,13 @@ slice-ansi@^4.0.0:
astral-regex "^2.0.0"
is-fullwidth-code-point "^3.0.0"
sodium-native@^3.3.0:
version "3.3.0"
resolved "https://registry.yarnpkg.com/sodium-native/-/sodium-native-3.3.0.tgz#50ee52ac843315866cce3d0c08ab03eb78f22361"
integrity sha512-rg6lCDM/qa3p07YGqaVD+ciAbUqm6SoO4xmlcfkbU5r1zIGrguXztLiEtaLYTV5U6k8KSIUFmnU3yQUSKmf6DA==
dependencies:
node-gyp-build "^4.3.0"
source-map-support@^0.5.6:
version "0.5.20"
resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.20.tgz#12166089f8f5e5e8c56926b377633392dd2cb6c9"

View File

@ -0,0 +1,52 @@
import { BaseEntity, Entity, PrimaryGeneratedColumn, Column } from 'typeorm'
@Entity('login_elopage_buys')
export class LoginElopageBuys extends BaseEntity {
@PrimaryGeneratedColumn('increment', { unsigned: true })
id: number
@Column({ name: 'elopage_user_id', nullable: false })
elopageUserId: number
@Column({ name: 'affiliate_program_id', nullable: false })
affiliateProgramId: number
@Column({ name: 'publisher_id', nullable: false })
publisherId: number
@Column({ name: 'order_id', nullable: false })
orderId: number
@Column({ name: 'product_id', nullable: false })
productId: number
@Column({ name: 'product_price', nullable: false })
productPrice: number
@Column({
name: 'payer_email',
length: 255,
nullable: false,
charset: 'utf8',
collation: 'utf8_bin',
})
payerEmail: string
@Column({
name: 'publisher_email',
length: 255,
nullable: false,
charset: 'utf8',
collation: 'utf8_bin',
})
publisherEmail: string
@Column({ nullable: false })
payed: boolean
@Column({ name: 'success_date', nullable: false })
successDate: Date
@Column({ length: 255, nullable: false })
event: string
}

View File

@ -0,0 +1,26 @@
import { BaseEntity, Entity, PrimaryGeneratedColumn, Column } from 'typeorm'
// Moriz: I do not like the idea of having two user tables
@Entity('login_email_opt_in')
export class LoginEmailOptIn extends BaseEntity {
@PrimaryGeneratedColumn('increment', { unsigned: true })
id: number
@Column({ name: 'user_id' })
userId: number
@Column({ name: 'verification_code', type: 'bigint', unsigned: true, unique: true })
verificationCode: BigInt
@Column({ name: 'email_opt_in_type_id' })
emailOptInTypeId: number
@Column({ name: 'created', default: () => 'CURRENT_TIMESTAMP' })
createdAt: Date
@Column({ name: 'resend_count', default: 0 })
resendCount: number
@Column({ name: 'updated', default: () => 'CURRENT_TIMESTAMP' })
updatedAt: Date
}

View File

@ -22,7 +22,7 @@ export class LoginUser extends BaseEntity {
description: string
@Column({ type: 'bigint', default: 0, unsigned: true })
password: string
password: BigInt
@Column({ name: 'pubkey', type: 'binary', length: 32, default: null, nullable: true })
pubKey: Buffer

View File

@ -0,0 +1,16 @@
import { BaseEntity, Entity, PrimaryGeneratedColumn, Column } from 'typeorm'
@Entity('login_user_backups')
export class LoginUserBackup extends BaseEntity {
@PrimaryGeneratedColumn('increment', { unsigned: true })
id: number
@Column({ name: 'user_id', nullable: false })
userId: number
@Column({ type: 'text', name: 'passphrase', nullable: false })
passphrase: string
@Column({ name: 'mnemonic_type', default: -1 })
mnemonicType: number
}

View File

@ -0,0 +1 @@
export { LoginElopageBuys } from './0003-login_server_tables/LoginElopageBuys'

View File

@ -0,0 +1 @@
export { LoginEmailOptIn } from './0003-login_server_tables/LoginEmailOptIn'

View File

@ -0,0 +1 @@
export { LoginUserBackup } from './0003-login_server_tables/LoginUserBackup'

View File

@ -1,5 +1,8 @@
import { Balance } from './Balance'
import { LoginElopageBuys } from './LoginElopageBuys'
import { LoginEmailOptIn } from './LoginEmailOptIn'
import { LoginUser } from './LoginUser'
import { LoginUserBackup } from './LoginUserBackup'
import { Migration } from './Migration'
import { Transaction } from './Transaction'
import { TransactionCreation } from './TransactionCreation'
@ -10,7 +13,10 @@ import { UserTransaction } from './UserTransaction'
export const entities = [
Balance,
LoginElopageBuys,
LoginEmailOptIn,
LoginUser,
LoginUserBackup,
Migration,
Transaction,
TransactionCreation,

View File

@ -0,0 +1,29 @@
import GlobalComponents from './globalComponents.js'
import Vue from 'vue'
import 'vee-validate'
jest.mock('vue')
jest.mock('vee-validate', () => {
const originalModule = jest.requireActual('vee-validate')
return {
__esModule: true,
...originalModule,
ValidationProvider: 'mocked validation provider',
ValidationObserver: 'mocked validation observer',
}
})
const vueComponentMock = jest.fn()
Vue.component = vueComponentMock
describe('global Components', () => {
GlobalComponents.install(Vue)
it('installs the validation provider', () => {
expect(vueComponentMock).toBeCalledWith('validation-provider', 'mocked validation provider')
})
it('installs the validation observer', () => {
expect(vueComponentMock).toBeCalledWith('validation-observer', 'mocked validation observer')
})
})

View File

@ -0,0 +1,70 @@
import { mount } from '@vue/test-utils'
import QrCode from './QrCode'
const localVue = global.localVue
describe('QrCode', () => {
let wrapper
const mocks = {
$t: jest.fn((t) => t),
}
const stubs = {
QrcodeStream: true,
QrcodeCapture: true,
}
const Wrapper = () => {
return mount(QrCode, { localVue, mocks, stubs })
}
describe('mount', () => {
beforeEach(() => {
wrapper = Wrapper()
})
it('renders the component', () => {
expect(wrapper.find('div.alert').exists()).toBeTruthy()
})
describe('scanning', () => {
beforeEach(async () => {
wrapper.find('a').trigger('click')
})
it('has a scanning stream', () => {
expect(wrapper.findComponent({ name: 'QrcodeStream' }).exists()).toBeTruthy()
})
describe('decode', () => {
beforeEach(async () => {
await wrapper
.findComponent({ name: 'QrcodeStream' })
.vm.$emit('decode', '[{"email": "user@example.org", "amount": 10.0}]')
})
it('emits set transaction', () => {
expect(wrapper.emitted()['set-transaction']).toEqual([
[
{
email: 'user@example.org',
amount: 10,
},
],
])
})
})
describe('detect', () => {
beforeEach(async () => {
await wrapper.find('div.row > *').vm.$emit('detect')
})
it('calls onDetect', () => {
expect(wrapper.vm.detect).toBeTruthy()
})
})
})
})
})

View File

@ -44,6 +44,7 @@ export default {
data() {
return {
scan: false,
detect: false,
}
},
methods: {
@ -55,6 +56,10 @@ export default {
this.$emit('set-transaction', { email: arr[0].email, amount: arr[0].amount })
this.scan = false
},
async onDetect() {
// TODO: what is this for? I added the detect data to test that the method is called
this.detect = !this.detect
},
},
}
</script>