From 41afaa0e014cae295f458aa217fb1eae3f92c2e4 Mon Sep 17 00:00:00 2001 From: einhorn_b Date: Wed, 29 Sep 2021 18:47:10 +0200 Subject: [PATCH 1/2] first steps for send coins --- .gitmodules | 3 ++ backend/package.json | 3 ++ .../graphql/resolvers/TransactionResolver.ts | 17 ++++++ .../graphql/resolvers/createTransaction.ts | 6 +++ backend/src/graphql/resolvers/getPublicKey.ts | 32 +++++++++++ backend/src/graphql/resolvers/sendCoins.ts | 53 +++++++++++++++++++ backend/src/proto | 1 + backend/src/util/validate.ts | 21 ++++++++ 8 files changed, 136 insertions(+) create mode 100644 backend/src/graphql/resolvers/createTransaction.ts create mode 100644 backend/src/graphql/resolvers/getPublicKey.ts create mode 100644 backend/src/graphql/resolvers/sendCoins.ts create mode 160000 backend/src/proto create mode 100644 backend/src/util/validate.ts diff --git a/.gitmodules b/.gitmodules index 22790ccc7..5026a5b8a 100644 --- a/.gitmodules +++ b/.gitmodules @@ -34,3 +34,6 @@ [submodule "login_server/dependencies/protobuf"] path = login_server/dependencies/protobuf url = https://github.com/protocolbuffers/protobuf.git +[submodule "backend/src/proto"] + path = backend/src/proto + url = git@github.com:gradido/gradido_protocol.git diff --git a/backend/package.json b/backend/package.json index bb7848013..9809b8be6 100644 --- a/backend/package.json +++ b/backend/package.json @@ -15,6 +15,7 @@ "lint": "eslint . --ext .js,.ts" }, "dependencies": { + "@apollo/protobufjs": "^1.2.2", "apollo-server-express": "^2.25.2", "axios": "^0.21.1", "class-validator": "^0.13.1", @@ -23,6 +24,7 @@ "express": "^4.17.1", "graphql": "^15.5.1", "jsonwebtoken": "^8.5.1", + "libsodium-wrappers": "^0.7.9", "mysql2": "^2.3.0", "reflect-metadata": "^0.1.13", "type-graphql": "^1.1.1", @@ -31,6 +33,7 @@ "devDependencies": { "@types/express": "^4.17.12", "@types/jsonwebtoken": "^8.5.2", + "@types/libsodium-wrappers": "^0.7.9", "@typescript-eslint/eslint-plugin": "^4.28.0", "@typescript-eslint/parser": "^4.28.0", "eslint": "^7.29.0", diff --git a/backend/src/graphql/resolvers/TransactionResolver.ts b/backend/src/graphql/resolvers/TransactionResolver.ts index 3762cccee..bacdc4628 100644 --- a/backend/src/graphql/resolvers/TransactionResolver.ts +++ b/backend/src/graphql/resolvers/TransactionResolver.ts @@ -11,6 +11,8 @@ import { Balance as dbBalance } from '../../typeorm/entity/Balance' import listTransactions from './listTransactions' import { roundFloorFrom4 } from '../../util/round' import { calculateDecay } from '../../util/decay' +import sendCoins from './sendCoins' +import getPublicKey from './getPublicKey' @Resolver() export class TransactionResolver { @@ -69,6 +71,21 @@ export class TransactionResolver { if (!result.success) { throw new Error(result.data) } + + const recipiantPublicKey = await getPublicKey(email, context.sessionId) + if(!recipiantPublicKey) { + throw new Error('recipiant not known') + } + + // get public key for current logged in user + const loginResult = await apiGet(CONFIG.LOGIN_API_URL + 'login?session_id=' + context.sessionId) + if (!loginResult.success) throw new Error(result.data) + + // load user and balance + const userEntity = await dbUser.findByPubkeyHex(result.data.user.public_hex) + + const transaction = sendCoins(userEntity, recipiantPublicKey, amount, memo) + return 'success' } } diff --git a/backend/src/graphql/resolvers/createTransaction.ts b/backend/src/graphql/resolvers/createTransaction.ts new file mode 100644 index 000000000..66256cff6 --- /dev/null +++ b/backend/src/graphql/resolvers/createTransaction.ts @@ -0,0 +1,6 @@ + + +export default function createTransaction() +{ + +} \ No newline at end of file diff --git a/backend/src/graphql/resolvers/getPublicKey.ts b/backend/src/graphql/resolvers/getPublicKey.ts new file mode 100644 index 000000000..f60616317 --- /dev/null +++ b/backend/src/graphql/resolvers/getPublicKey.ts @@ -0,0 +1,32 @@ +import { apiPost } from '../../apis/HttpRequest' +import CONFIG from '../../config' +import { isHexPublicKey } from '../../util/validate' + +// target can be email, username or public_key +// groupId if not null and another community, try to get public key from there +export default async function getPublicKey(target: string, sessionId:number, groupId: number = 0): Promise +{ + // if it is already a public key, return it + if(isHexPublicKey(target)) { + return target + } + + // assume it is a email address if it's contain a @ + if(/@/i.test(target)) { + const result = await apiPost(CONFIG.LOGIN_API_URL + 'getUserInfos', { + session_id: sessionId, + email: target, + ask: ['user.pubkeyhex'] + }) + if (result.success) { + return result.data.userData.pubkeyhex + } + } + + // if username is used add code here + + // if we have multiple communities add code here + + return undefined + +} \ No newline at end of file diff --git a/backend/src/graphql/resolvers/sendCoins.ts b/backend/src/graphql/resolvers/sendCoins.ts new file mode 100644 index 000000000..ccb221005 --- /dev/null +++ b/backend/src/graphql/resolvers/sendCoins.ts @@ -0,0 +1,53 @@ +import protobuf from '@apollo/protobufjs' +import { from_hex } from 'libsodium-wrappers' +import { isHexPublicKey, hasUserAmount } from '../../util/validate' +import { User as dbUser } from '../../typeorm/entity/User' + + +/** + * + * @param senderPublicKey as hex string + * @param recipiantPublicKey as hex string + * @param amount as float + * @param memo + * @param groupId + */ +export default async function sendCoins( + senderUser: dbUser, + recipiantPublicKey:string, + amount:number, + memo:string, + groupId:number = 0) +{ + if(senderUser.pubkey.length != 32) { + throw new Error('invalid sender public key') + } + if(!isHexPublicKey(recipiantPublicKey)) { + throw new Error('invalid recipiant public key') + } + if(amount <= 0) { + throw new Error('invalid amount') + } + if(!hasUserAmount(senderUser, amount)) { + throw new Error('user hasn\'t enough GDD') + } + const protoRoot = await protobuf.load('../../proto/gradido/GradidoTransfer.proto') + + const GradidoTransfer = protoRoot.lookupType('proto.gradido.GradidoTransfer') + const TransferAmount = protoRoot.lookupType('proto.gradido.TransferAmount') + + const transferAmount = TransferAmount.create({ + pubkey: senderUser.pubkey, + amount: amount / 10000 + }) + + // no group id is given so we assume it is a local transfer + if(!groupId) { + const LocalTransfer = protoRoot.lookupType('proto.gradido.LocalTransfer') + const localTransfer = LocalTransfer.create({ + sender: transferAmount, + recipiant: from_hex(recipiantPublicKey) + }) + return GradidoTransfer.create({local: localTransfer}) + } +} \ No newline at end of file diff --git a/backend/src/proto b/backend/src/proto new file mode 160000 index 000000000..cc9acbb21 --- /dev/null +++ b/backend/src/proto @@ -0,0 +1 @@ +Subproject commit cc9acbb212201a560d86fc87a47665497f11f27d diff --git a/backend/src/util/validate.ts b/backend/src/util/validate.ts new file mode 100644 index 000000000..d95f64328 --- /dev/null +++ b/backend/src/util/validate.ts @@ -0,0 +1,21 @@ +import { User as dbUser } from '../typeorm/entity/User' +import { Balance as dbBalance } from '../typeorm/entity/Balance' +import { getRepository } from 'typeorm' +import { calculateDecay } from './decay' +import { UserResolver } from '../graphql/resolvers' + +function isHexPublicKey(publicKey:string): boolean { + return /^[0-9A-Fa-f]{64}$/i.test(publicKey) +} + +async function hasUserAmount(user:dbUser, amount:number): Promise { + if(amount < 0) return false + const balanceRepository = getRepository(dbBalance) + const balance = await balanceRepository.findOne({ userId: user.id }) + if(!balance) return false + + const decay = await calculateDecay(balance.amount, balance.recordDate, new Date()) + return decay > amount +} + +export { isHexPublicKey, hasUserAmount } \ No newline at end of file From e49230e1ea20a2142f278d5acd6c5581eda9c487 Mon Sep 17 00:00:00 2001 From: Einhornimmond Date: Tue, 5 Oct 2021 12:05:23 +0200 Subject: [PATCH 2/2] changes --- .../graphql/resolvers/TransactionResolver.ts | 2 +- backend/src/graphql/resolvers/UserResolver.ts | 5 +- .../graphql/resolvers/createTransaction.ts | 7 +- backend/src/graphql/resolvers/getPublicKey.ts | 46 ++++++----- backend/src/graphql/resolvers/sendCoins.ts | 81 +++++++++---------- backend/src/typeorm/entity/User.ts | 4 + backend/src/util/validate.ts | 20 ++--- backend/yarn.lock | 19 ++++- database/migrations/0002-add_settings.ts | 28 +++++++ frontend/src/graphql/mutations.js | 2 + 10 files changed, 132 insertions(+), 82 deletions(-) create mode 100644 database/migrations/0002-add_settings.ts diff --git a/backend/src/graphql/resolvers/TransactionResolver.ts b/backend/src/graphql/resolvers/TransactionResolver.ts index bacdc4628..fb6579380 100644 --- a/backend/src/graphql/resolvers/TransactionResolver.ts +++ b/backend/src/graphql/resolvers/TransactionResolver.ts @@ -73,7 +73,7 @@ export class TransactionResolver { } const recipiantPublicKey = await getPublicKey(email, context.sessionId) - if(!recipiantPublicKey) { + if (!recipiantPublicKey) { throw new Error('recipiant not known') } diff --git a/backend/src/graphql/resolvers/UserResolver.ts b/backend/src/graphql/resolvers/UserResolver.ts index 06b10daec..57d0adc8d 100644 --- a/backend/src/graphql/resolvers/UserResolver.ts +++ b/backend/src/graphql/resolvers/UserResolver.ts @@ -8,7 +8,9 @@ import { LoginViaVerificationCode } from '../models/LoginViaVerificationCode' import { SendPasswordResetEmailResponse } from '../models/SendPasswordResetEmailResponse' import { UpdateUserInfosResponse } from '../models/UpdateUserInfosResponse' import { User } from '../models/User' +import { UserSettingRepository } from '../../typeorm/repository/UserSettingRepository' import encode from '../../jwt/encode' +import { Setting } from '../../types' import { ChangePasswordArgs, CheckUsernameArgs, @@ -22,6 +24,7 @@ import { klicktippNewsletterStateMiddleware, } from '../../middleware/klicktippMiddleware' import { CheckEmailResponse } from '../models/CheckEmailResponse' +import { getCustomRepository } from 'typeorm' @Resolver() export class UserResolver { @Query(() => User) @@ -154,7 +157,7 @@ export class UserResolver { } const result = await apiPost(CONFIG.LOGIN_API_URL + 'updateUserInfos', payload) if (!result.success) throw new Error(result.data) - return new UpdateUserInfosResponse(result.data) + return new UpdateUserInfosResponse(result.data) } @Query(() => CheckUsernameResponse) diff --git a/backend/src/graphql/resolvers/createTransaction.ts b/backend/src/graphql/resolvers/createTransaction.ts index 66256cff6..0aa684a94 100644 --- a/backend/src/graphql/resolvers/createTransaction.ts +++ b/backend/src/graphql/resolvers/createTransaction.ts @@ -1,6 +1 @@ - - -export default function createTransaction() -{ - -} \ No newline at end of file +export default function createTransaction() {} diff --git a/backend/src/graphql/resolvers/getPublicKey.ts b/backend/src/graphql/resolvers/getPublicKey.ts index f60616317..4862ddc34 100644 --- a/backend/src/graphql/resolvers/getPublicKey.ts +++ b/backend/src/graphql/resolvers/getPublicKey.ts @@ -4,29 +4,31 @@ import { isHexPublicKey } from '../../util/validate' // target can be email, username or public_key // groupId if not null and another community, try to get public key from there -export default async function getPublicKey(target: string, sessionId:number, groupId: number = 0): Promise -{ - // if it is already a public key, return it - if(isHexPublicKey(target)) { - return target +export default async function getPublicKey( + target: string, + sessionId: number, + groupId = 0, +): Promise { + // if it is already a public key, return it + if (isHexPublicKey(target)) { + return target + } + + // assume it is a email address if it's contain a @ + if (/@/i.test(target)) { + const result = await apiPost(CONFIG.LOGIN_API_URL + 'getUserInfos', { + session_id: sessionId, + email: target, + ask: ['user.pubkeyhex'], + }) + if (result.success) { + return result.data.userData.pubkeyhex } + } - // assume it is a email address if it's contain a @ - if(/@/i.test(target)) { - const result = await apiPost(CONFIG.LOGIN_API_URL + 'getUserInfos', { - session_id: sessionId, - email: target, - ask: ['user.pubkeyhex'] - }) - if (result.success) { - return result.data.userData.pubkeyhex - } - } + // if username is used add code here - // if username is used add code here + // if we have multiple communities add code here - // if we have multiple communities add code here - - return undefined - -} \ No newline at end of file + return undefined +} diff --git a/backend/src/graphql/resolvers/sendCoins.ts b/backend/src/graphql/resolvers/sendCoins.ts index ccb221005..9f0a8f25f 100644 --- a/backend/src/graphql/resolvers/sendCoins.ts +++ b/backend/src/graphql/resolvers/sendCoins.ts @@ -3,51 +3,50 @@ import { from_hex } from 'libsodium-wrappers' import { isHexPublicKey, hasUserAmount } from '../../util/validate' import { User as dbUser } from '../../typeorm/entity/User' - /** - * + * * @param senderPublicKey as hex string * @param recipiantPublicKey as hex string - * @param amount as float - * @param memo - * @param groupId + * @param amount as float + * @param memo + * @param groupId */ export default async function sendCoins( - senderUser: dbUser, - recipiantPublicKey:string, - amount:number, - memo:string, - groupId:number = 0) -{ - if(senderUser.pubkey.length != 32) { - throw new Error('invalid sender public key') - } - if(!isHexPublicKey(recipiantPublicKey)) { - throw new Error('invalid recipiant public key') - } - if(amount <= 0) { - throw new Error('invalid amount') - } - if(!hasUserAmount(senderUser, amount)) { - throw new Error('user hasn\'t enough GDD') - } - const protoRoot = await protobuf.load('../../proto/gradido/GradidoTransfer.proto') - - const GradidoTransfer = protoRoot.lookupType('proto.gradido.GradidoTransfer') - const TransferAmount = protoRoot.lookupType('proto.gradido.TransferAmount') + senderUser: dbUser, + recipiantPublicKey: string, + amount: number, + memo: string, + groupId = 0, +) { + if (senderUser.pubkey.length != 32) { + throw new Error('invalid sender public key') + } + if (!isHexPublicKey(recipiantPublicKey)) { + throw new Error('invalid recipiant public key') + } + if (amount <= 0) { + throw new Error('invalid amount') + } + if (!hasUserAmount(senderUser, amount)) { + throw new Error("user hasn't enough GDD") + } + const protoRoot = await protobuf.load('../../proto/gradido/GradidoTransfer.proto') - const transferAmount = TransferAmount.create({ - pubkey: senderUser.pubkey, - amount: amount / 10000 + const GradidoTransfer = protoRoot.lookupType('proto.gradido.GradidoTransfer') + const TransferAmount = protoRoot.lookupType('proto.gradido.TransferAmount') + + const transferAmount = TransferAmount.create({ + pubkey: senderUser.pubkey, + amount: amount / 10000, + }) + + // no group id is given so we assume it is a local transfer + if (!groupId) { + const LocalTransfer = protoRoot.lookupType('proto.gradido.LocalTransfer') + const localTransfer = LocalTransfer.create({ + sender: transferAmount, + recipiant: from_hex(recipiantPublicKey), }) - - // no group id is given so we assume it is a local transfer - if(!groupId) { - const LocalTransfer = protoRoot.lookupType('proto.gradido.LocalTransfer') - const localTransfer = LocalTransfer.create({ - sender: transferAmount, - recipiant: from_hex(recipiantPublicKey) - }) - return GradidoTransfer.create({local: localTransfer}) - } -} \ No newline at end of file + return GradidoTransfer.create({ local: localTransfer }) + } +} diff --git a/backend/src/typeorm/entity/User.ts b/backend/src/typeorm/entity/User.ts index 1cd5b1c4c..21a734ec0 100644 --- a/backend/src/typeorm/entity/User.ts +++ b/backend/src/typeorm/entity/User.ts @@ -1,4 +1,5 @@ import { BaseEntity, Entity, PrimaryGeneratedColumn, Column } from 'typeorm' +import { UserSetting } from './UserSetting' // import { Group } from "./Group" @Entity('state_users') @@ -27,6 +28,9 @@ export class User extends BaseEntity { @Column() disabled: boolean + @OneToMany(() => UserSetting, (userSetting) => userSetting.user) + settings: UserSetting[] + static findByPubkeyHex(pubkeyHex: string): Promise { return this.createQueryBuilder('user') .where('hex(user.pubkey) = :pubkeyHex', { pubkeyHex }) diff --git a/backend/src/util/validate.ts b/backend/src/util/validate.ts index d95f64328..1617f5c47 100644 --- a/backend/src/util/validate.ts +++ b/backend/src/util/validate.ts @@ -4,18 +4,18 @@ import { getRepository } from 'typeorm' import { calculateDecay } from './decay' import { UserResolver } from '../graphql/resolvers' -function isHexPublicKey(publicKey:string): boolean { - return /^[0-9A-Fa-f]{64}$/i.test(publicKey) +function isHexPublicKey(publicKey: string): boolean { + return /^[0-9A-Fa-f]{64}$/i.test(publicKey) } -async function hasUserAmount(user:dbUser, amount:number): Promise { - if(amount < 0) return false - const balanceRepository = getRepository(dbBalance) - const balance = await balanceRepository.findOne({ userId: user.id }) - if(!balance) return false +async function hasUserAmount(user: dbUser, amount: number): Promise { + if (amount < 0) return false + const balanceRepository = getRepository(dbBalance) + const balance = await balanceRepository.findOne({ userId: user.id }) + if (!balance) return false - const decay = await calculateDecay(balance.amount, balance.recordDate, new Date()) - return decay > amount + const decay = await calculateDecay(balance.amount, balance.recordDate, new Date()) + return decay > amount } -export { isHexPublicKey, hasUserAmount } \ No newline at end of file +export { isHexPublicKey, hasUserAmount } diff --git a/backend/yarn.lock b/backend/yarn.lock index afaf4433c..6858c4897 100644 --- a/backend/yarn.lock +++ b/backend/yarn.lock @@ -2,7 +2,7 @@ # yarn lockfile v1 -"@apollo/protobufjs@1.2.2": +"@apollo/protobufjs@1.2.2", "@apollo/protobufjs@^1.2.2": version "1.2.2" resolved "https://registry.yarnpkg.com/@apollo/protobufjs/-/protobufjs-1.2.2.tgz#4bd92cd7701ccaef6d517cdb75af2755f049f87c" integrity sha512-vF+zxhPiLtkwxONs6YanSt1EpwpGilThpneExUN5K3tCymuxNnVq2yojTvnpRjv2QfsEIt/n7ozPIIzBLwGIDQ== @@ -336,6 +336,11 @@ "@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" @@ -2339,6 +2344,18 @@ libphonenumber-js@^1.9.7: resolved "https://registry.yarnpkg.com/libphonenumber-js/-/libphonenumber-js-1.9.22.tgz#b6b460603dedbd58f2d71f15500f216d70850fad" integrity sha512-nE0aF0wrNq09ewF36s9FVqRW73hmpw6cobVDlbexmsu1432LEfuN24BCudNuRx4t2rElSeK/N0JbedzRW/TC4A== +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" diff --git a/database/migrations/0002-add_settings.ts b/database/migrations/0002-add_settings.ts new file mode 100644 index 000000000..d4c9b0c9a --- /dev/null +++ b/database/migrations/0002-add_settings.ts @@ -0,0 +1,28 @@ +/* FIRST MIGRATION + * + * This migration is special since it takes into account that + * the database can be setup already but also may not be. + * Therefore you will find all `CREATE TABLE` statements with + * a `IF NOT EXISTS`, all `INSERT` with an `IGNORE` and in the + * downgrade function all `DROP TABLE` with a `IF EXISTS`. + * This ensures compatibility for existing or non-existing + * databases. + */ + +export async function upgrade(queryFn: (query: string, values?: any[]) => Promise>) { + + await queryFn(` + CREATE TABLE IF NOT EXISTS userSetting ( + id int(10) unsigned NOT NULL AUTO_INCREMENT, + userId int(11) NOT NULL, + key varchar(255) NOT NULL, + value varchar(255) NOT NULL + PRIMARY KEY (id) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;`) + } + + export async function downgrade(queryFn: (query: string, values?: any[]) => Promise>) { + // write downgrade logic as parameter of queryFn + await queryFn(`DROP TABLE IF EXISTS userSettings;`) + } + \ No newline at end of file diff --git a/frontend/src/graphql/mutations.js b/frontend/src/graphql/mutations.js index 939d5babe..b4c4bf840 100644 --- a/frontend/src/graphql/mutations.js +++ b/frontend/src/graphql/mutations.js @@ -28,6 +28,7 @@ export const updateUserInfos = gql` $password: String $passwordNew: String $locale: String + $coinanimation: Boolean ) { updateUserInfos( email: $email @@ -38,6 +39,7 @@ export const updateUserInfos = gql` password: $password passwordNew: $passwordNew language: $locale + coinanimation: $coinanimation ) { validValues }