Merge pull request #1084 from gradido/login_call_updateUserInfos

login_call_updateUserInfos
This commit is contained in:
Ulf Gebhardt 2021-11-18 01:48:24 +01:00 committed by GitHub
commit 640c5ec88d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 124 additions and 116 deletions

View File

@ -394,7 +394,7 @@ jobs:
report_name: Coverage Backend report_name: Coverage Backend
type: lcov type: lcov
result_path: ./backend/coverage/lcov.info result_path: ./backend/coverage/lcov.info
min_coverage: 39 min_coverage: 38
token: ${{ github.token }} token: ${{ github.token }}
############################################################################## ##############################################################################

View File

@ -13,15 +13,9 @@ const isAuthorized: AuthChecker<any> = async (
) => { ) => {
if (context.token) { if (context.token) {
const decoded = decode(context.token) const decoded = decode(context.token)
if (decoded.sessionId && decoded.sessionId !== 0) { context.pubKey = decoded.pubKey
const result = await apiGet( context.setHeaders.push({ key: 'token', value: encode(decoded.pubKey) })
`${CONFIG.LOGIN_API_URL}checkSessionState?session_id=${decoded.sessionId}`, return true
)
context.sessionId = decoded.sessionId
context.pubKey = decoded.pubKey
context.setHeaders.push({ key: 'token', value: encode(decoded.sessionId, decoded.pubKey) })
return result.success
}
} }
throw new Error('401 Unauthorized') throw new Error('401 Unauthorized')
} }

View File

@ -1,13 +0,0 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import { ObjectType, Field } from 'type-graphql'
@ObjectType()
export class UpdateUserInfosResponse {
constructor(json: any) {
this.validValues = json.valid_values
}
@Field(() => Number)
validValues: number
}

View File

@ -33,6 +33,7 @@ import { calculateDecay, calculateDecayWithInterval } from '../../util/decay'
import { TransactionTypeId } from '../enum/TransactionTypeId' import { TransactionTypeId } from '../enum/TransactionTypeId'
import { TransactionType } from '../enum/TransactionType' import { TransactionType } from '../enum/TransactionType'
import { hasUserAmount, isHexPublicKey } from '../../util/validate' import { hasUserAmount, isHexPublicKey } from '../../util/validate'
import { LoginUserRepository } from '../../typeorm/repository/LoginUser'
/* /*
# Test # Test
@ -451,15 +452,15 @@ async function addUserTransaction(
}) })
} }
async function getPublicKey(email: string, sessionId: number): Promise<string | undefined> { async function getPublicKey(email: string): Promise<string | null> {
const result = await apiPost(CONFIG.LOGIN_API_URL + 'getUserInfos', { const loginUserRepository = getCustomRepository(LoginUserRepository)
session_id: sessionId, const loginUser = await loginUserRepository.findOne({ email: email })
email, // User not found
ask: ['user.pubkeyhex'], if (!loginUser) {
}) return null
if (result.success) {
return result.data.userData.pubkeyhex
} }
return loginUser.pubKey.toString('hex')
} }
@Resolver() @Resolver()
@ -517,7 +518,7 @@ export class TransactionResolver {
// validate recipient user // validate recipient user
// TODO: the detour over the public key is unnecessary // TODO: the detour over the public key is unnecessary
const recipiantPublicKey = await getPublicKey(email, context.sessionId) const recipiantPublicKey = await getPublicKey(email)
if (!recipiantPublicKey) { if (!recipiantPublicKey) {
throw new Error('recipiant not known') throw new Error('recipiant not known')
} }

View File

@ -7,7 +7,6 @@ import { getConnection, getCustomRepository } from 'typeorm'
import CONFIG from '../../config' import CONFIG from '../../config'
import { LoginViaVerificationCode } from '../model/LoginViaVerificationCode' import { LoginViaVerificationCode } from '../model/LoginViaVerificationCode'
import { SendPasswordResetEmailResponse } from '../model/SendPasswordResetEmailResponse' import { SendPasswordResetEmailResponse } from '../model/SendPasswordResetEmailResponse'
import { UpdateUserInfosResponse } from '../model/UpdateUserInfosResponse'
import { User } from '../model/User' import { User } from '../model/User'
import { User as DbUser } from '@entity/User' import { User as DbUser } from '@entity/User'
import encode from '../../jwt/encode' import encode from '../../jwt/encode'
@ -30,6 +29,7 @@ import { LoginElopageBuys } from '@entity/LoginElopageBuys'
import { LoginUserBackup } from '@entity/LoginUserBackup' import { LoginUserBackup } from '@entity/LoginUserBackup'
import { LoginEmailOptIn } from '@entity/LoginEmailOptIn' import { LoginEmailOptIn } from '@entity/LoginEmailOptIn'
import { sendEMail } from '../../util/sendEMail' import { sendEMail } from '../../util/sendEMail'
import { LoginUserRepository } from '../../typeorm/repository/LoginUser'
// 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')
@ -174,7 +174,7 @@ const getEmailHash = (email: string): Buffer => {
} }
const SecretKeyCryptographyEncrypt = (message: Buffer, encryptionKey: Buffer): Buffer => { const SecretKeyCryptographyEncrypt = (message: Buffer, encryptionKey: Buffer): Buffer => {
const encrypted = Buffer.alloc(sodium.crypto_secretbox_MACBYTES + message.length) const encrypted = Buffer.alloc(message.length + sodium.crypto_secretbox_MACBYTES)
const nonce = Buffer.alloc(sodium.crypto_secretbox_NONCEBYTES) const nonce = Buffer.alloc(sodium.crypto_secretbox_NONCEBYTES)
nonce.fill(31) // static nonce nonce.fill(31) // static nonce
@ -182,6 +182,16 @@ const SecretKeyCryptographyEncrypt = (message: Buffer, encryptionKey: Buffer): B
return encrypted return encrypted
} }
const SecretKeyCryptographyDecrypt = (encryptedMessage: Buffer, encryptionKey: Buffer): Buffer => {
const message = Buffer.alloc(encryptedMessage.length - sodium.crypto_secretbox_MACBYTES)
const nonce = Buffer.alloc(sodium.crypto_secretbox_NONCEBYTES)
nonce.fill(31) // static nonce
sodium.crypto_secretbox_open_easy(message, encryptedMessage, nonce, encryptionKey)
return message
}
@Resolver() @Resolver()
export class UserResolver { export class UserResolver {
@Query(() => User) @Query(() => User)
@ -200,7 +210,7 @@ export class UserResolver {
context.setHeaders.push({ context.setHeaders.push({
key: 'token', key: 'token',
value: encode(result.data.session_id, result.data.user.public_hex), value: encode(result.data.user.public_hex),
}) })
const user = new User(result.data.user) const user = new User(result.data.user)
// Hack: Database Field is not validated properly and not nullable // Hack: Database Field is not validated properly and not nullable
@ -230,10 +240,11 @@ export class UserResolver {
// Save publisherId if Elopage is not yet registered // Save publisherId if Elopage is not yet registered
if (!user.hasElopage && publisherId) { if (!user.hasElopage && publisherId) {
user.publisherId = publisherId user.publisherId = publisherId
await this.updateUserInfos(
{ publisherId }, const loginUserRepository = getCustomRepository(LoginUserRepository)
{ sessionId: result.data.session_id, pubKey: result.data.user.public_hex }, const loginUser = await loginUserRepository.findOneOrFail({ email: userEntity.email })
) loginUser.publisherId = publisherId
loginUserRepository.save(loginUser)
} }
const userSettingRepository = getCustomRepository(UserSettingRepository) const userSettingRepository = getCustomRepository(UserSettingRepository)
@ -446,7 +457,7 @@ export class UserResolver {
} }
@Authorized() @Authorized()
@Mutation(() => UpdateUserInfosResponse) @Mutation(() => Boolean)
async updateUserInfos( async updateUserInfos(
@Args() @Args()
{ {
@ -461,79 +472,97 @@ export class UserResolver {
coinanimation, coinanimation,
}: UpdateUserInfosArgs, }: UpdateUserInfosArgs,
@Ctx() context: any, @Ctx() context: any,
): Promise<UpdateUserInfosResponse> { ): Promise<boolean> {
const payload = {
session_id: context.sessionId,
update: {
'User.first_name': firstName || undefined,
'User.last_name': lastName || undefined,
'User.description': description || undefined,
'User.username': username || undefined,
'User.language': language || undefined,
'User.publisher_id': publisherId || undefined,
'User.password': passwordNew || undefined,
'User.password_old': password || undefined,
},
}
let response: UpdateUserInfosResponse | undefined
const userRepository = getCustomRepository(UserRepository) const userRepository = getCustomRepository(UserRepository)
const userEntity = await userRepository.findByPubkeyHex(context.pubKey)
const loginUserRepository = getCustomRepository(LoginUserRepository)
const loginUser = await loginUserRepository.findOneOrFail({ email: userEntity.email })
if ( if (username) {
firstName || throw new Error('change username currently not supported!')
lastName || // TODO: this error was thrown on login_server whenever you tried to change the username
description || // to anything except "" which is an exception to the rules below. Those were defined
username || // aswell, even tho never used.
language || // ^[a-zA-Z][a-zA-Z0-9_-]*$
publisherId || // username must start with [a-z] or [A-Z] and than can contain also [0-9], - and _
passwordNew || // username already used
password // userEntity.username = username
) {
const result = await apiPost(CONFIG.LOGIN_API_URL + 'updateUserInfos', payload)
if (!result.success) throw new Error(result.data)
response = new UpdateUserInfosResponse(result.data)
const userEntity = await userRepository.findByPubkeyHex(context.pubKey)
let userEntityChanged = false
if (firstName) {
userEntity.firstName = firstName
userEntityChanged = true
}
if (lastName) {
userEntity.lastName = lastName
userEntityChanged = true
}
if (username) {
userEntity.username = username
userEntityChanged = true
}
if (userEntityChanged) {
userRepository.save(userEntity).catch((error) => {
throw new Error(error)
})
}
} }
if (coinanimation !== undefined) {
// load user and balance
const userEntity = await userRepository.findByPubkeyHex(context.pubKey) if (firstName) {
loginUser.firstName = firstName
userEntity.firstName = firstName
}
const userSettingRepository = getCustomRepository(UserSettingRepository) if (lastName) {
userSettingRepository loginUser.lastName = lastName
.setOrUpdate(userEntity.id, Setting.COIN_ANIMATION, coinanimation.toString()) userEntity.lastName = lastName
.catch((error) => { }
throw new Error(error)
})
if (!response) { if (description) {
response = new UpdateUserInfosResponse({ valid_values: 1 }) loginUser.description = description
} else { }
response.validValues++
if (language) {
if (!isLanguage(language)) {
throw new Error(`"${language}" isn't a valid language`)
} }
loginUser.language = language
} }
if (!response) {
throw new Error('no valid response') if (password && passwordNew) {
// TODO: This had some error cases defined - like missing private key. This is no longer checked.
const oldPasswordHash = SecretKeyCryptographyCreateKey(loginUser.email, password)
if (loginUser.password !== oldPasswordHash[0].readBigUInt64LE()) {
throw new Error(`Old password is invalid`)
}
const privKey = SecretKeyCryptographyDecrypt(loginUser.privKey, oldPasswordHash[1])
const newPasswordHash = SecretKeyCryptographyCreateKey(loginUser.email, passwordNew) // return short and long hash
const encryptedPrivkey = SecretKeyCryptographyEncrypt(privKey, newPasswordHash[1])
// Save new password hash and newly encrypted private key
loginUser.password = newPasswordHash[0].readBigInt64LE()
loginUser.privKey = encryptedPrivkey
} }
return response
// Save publisherId only if Elopage is not yet registered
if (publisherId && !(await this.hasElopage(context))) {
loginUser.publisherId = publisherId
}
const queryRunner = getConnection().createQueryRunner()
await queryRunner.connect()
await queryRunner.startTransaction('READ UNCOMMITTED')
try {
if (coinanimation) {
queryRunner.manager
.getCustomRepository(UserSettingRepository)
.setOrUpdate(userEntity.id, Setting.COIN_ANIMATION, coinanimation.toString())
.catch((error) => {
throw new Error('error saving coinanimation: ' + error)
})
}
await queryRunner.manager.save(loginUser).catch((error) => {
throw new Error('error saving loginUser: ' + error)
})
await queryRunner.manager.save(userEntity).catch((error) => {
throw new Error('error saving user: ' + error)
})
await queryRunner.commitTransaction()
} catch (e) {
await queryRunner.rollbackTransaction()
throw e
} finally {
await queryRunner.release()
}
return true
} }
@Query(() => Boolean) @Query(() => Boolean)

View File

@ -2,27 +2,22 @@ import jwt, { JwtPayload } from 'jsonwebtoken'
import CONFIG from '../config/' import CONFIG from '../config/'
interface CustomJwtPayload extends JwtPayload { interface CustomJwtPayload extends JwtPayload {
sessionId: number
pubKey: Buffer pubKey: Buffer
} }
type DecodedJwt = { type DecodedJwt = {
token: string token: string
sessionId: number
pubKey: Buffer pubKey: Buffer
} }
export default (token: string): DecodedJwt => { export default (token: string): DecodedJwt => {
if (!token) throw new Error('401 Unauthorized') if (!token) throw new Error('401 Unauthorized')
let sessionId = null
let pubKey = null let pubKey = null
try { try {
const decoded = <CustomJwtPayload>jwt.verify(token, CONFIG.JWT_SECRET) const decoded = <CustomJwtPayload>jwt.verify(token, CONFIG.JWT_SECRET)
sessionId = decoded.sessionId
pubKey = decoded.pubKey pubKey = decoded.pubKey
return { return {
token, token,
sessionId,
pubKey, pubKey,
} }
} catch (err) { } catch (err) {

View File

@ -5,10 +5,9 @@ import jwt from 'jsonwebtoken'
import CONFIG from '../config/' import CONFIG from '../config/'
// Generate an Access Token // Generate an Access Token
export default function encode(sessionId: number, pubKey: Buffer): string { export default function encode(pubKey: Buffer): string {
const token = jwt.sign({ sessionId, pubKey }, CONFIG.JWT_SECRET, { const token = jwt.sign({ pubKey }, CONFIG.JWT_SECRET, {
expiresIn: CONFIG.JWT_EXPIRES_IN, expiresIn: CONFIG.JWT_EXPIRES_IN,
subject: sessionId.toString(),
}) })
return token return token
} }

View File

@ -2,7 +2,12 @@ import { createTransport } from 'nodemailer'
import CONFIG from '../config' import CONFIG from '../config'
export const sendEMail = async (emailDef: any): Promise<boolean> => { export const sendEMail = async (emailDef: {
from: string
to: string
subject: string
text: string
}): Promise<boolean> => {
if (!CONFIG.EMAIL) { if (!CONFIG.EMAIL) {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.log('Emails are disabled via config') console.log('Emails are disabled via config')

View File

@ -38,9 +38,7 @@ export const updateUserInfos = gql`
passwordNew: $passwordNew passwordNew: $passwordNew
language: $locale language: $locale
coinanimation: $coinanimation coinanimation: $coinanimation
) { )
validValues
}
} }
` `