mirror of
https://github.com/IT4Change/gradido.git
synced 2025-12-12 23:35:50 +00:00
Merge pull request #1084 from gradido/login_call_updateUserInfos
login_call_updateUserInfos
This commit is contained in:
commit
640c5ec88d
2
.github/workflows/test.yml
vendored
2
.github/workflows/test.yml
vendored
@ -394,7 +394,7 @@ jobs:
|
||||
report_name: Coverage Backend
|
||||
type: lcov
|
||||
result_path: ./backend/coverage/lcov.info
|
||||
min_coverage: 39
|
||||
min_coverage: 38
|
||||
token: ${{ github.token }}
|
||||
|
||||
##############################################################################
|
||||
|
||||
@ -13,15 +13,9 @@ const isAuthorized: AuthChecker<any> = async (
|
||||
) => {
|
||||
if (context.token) {
|
||||
const decoded = decode(context.token)
|
||||
if (decoded.sessionId && decoded.sessionId !== 0) {
|
||||
const result = await apiGet(
|
||||
`${CONFIG.LOGIN_API_URL}checkSessionState?session_id=${decoded.sessionId}`,
|
||||
)
|
||||
context.sessionId = decoded.sessionId
|
||||
context.pubKey = decoded.pubKey
|
||||
context.setHeaders.push({ key: 'token', value: encode(decoded.sessionId, decoded.pubKey) })
|
||||
return result.success
|
||||
}
|
||||
context.pubKey = decoded.pubKey
|
||||
context.setHeaders.push({ key: 'token', value: encode(decoded.pubKey) })
|
||||
return true
|
||||
}
|
||||
throw new Error('401 Unauthorized')
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
@ -33,6 +33,7 @@ import { calculateDecay, calculateDecayWithInterval } from '../../util/decay'
|
||||
import { TransactionTypeId } from '../enum/TransactionTypeId'
|
||||
import { TransactionType } from '../enum/TransactionType'
|
||||
import { hasUserAmount, isHexPublicKey } from '../../util/validate'
|
||||
import { LoginUserRepository } from '../../typeorm/repository/LoginUser'
|
||||
|
||||
/*
|
||||
# Test
|
||||
@ -451,15 +452,15 @@ async function addUserTransaction(
|
||||
})
|
||||
}
|
||||
|
||||
async function getPublicKey(email: string, sessionId: number): Promise<string | undefined> {
|
||||
const result = await apiPost(CONFIG.LOGIN_API_URL + 'getUserInfos', {
|
||||
session_id: sessionId,
|
||||
email,
|
||||
ask: ['user.pubkeyhex'],
|
||||
})
|
||||
if (result.success) {
|
||||
return result.data.userData.pubkeyhex
|
||||
async function getPublicKey(email: string): Promise<string | null> {
|
||||
const loginUserRepository = getCustomRepository(LoginUserRepository)
|
||||
const loginUser = await loginUserRepository.findOne({ email: email })
|
||||
// User not found
|
||||
if (!loginUser) {
|
||||
return null
|
||||
}
|
||||
|
||||
return loginUser.pubKey.toString('hex')
|
||||
}
|
||||
|
||||
@Resolver()
|
||||
@ -517,7 +518,7 @@ export class TransactionResolver {
|
||||
|
||||
// validate recipient user
|
||||
// TODO: the detour over the public key is unnecessary
|
||||
const recipiantPublicKey = await getPublicKey(email, context.sessionId)
|
||||
const recipiantPublicKey = await getPublicKey(email)
|
||||
if (!recipiantPublicKey) {
|
||||
throw new Error('recipiant not known')
|
||||
}
|
||||
|
||||
@ -7,7 +7,6 @@ import { getConnection, getCustomRepository } from 'typeorm'
|
||||
import CONFIG from '../../config'
|
||||
import { LoginViaVerificationCode } from '../model/LoginViaVerificationCode'
|
||||
import { SendPasswordResetEmailResponse } from '../model/SendPasswordResetEmailResponse'
|
||||
import { UpdateUserInfosResponse } from '../model/UpdateUserInfosResponse'
|
||||
import { User } from '../model/User'
|
||||
import { User as DbUser } from '@entity/User'
|
||||
import encode from '../../jwt/encode'
|
||||
@ -30,6 +29,7 @@ import { LoginElopageBuys } from '@entity/LoginElopageBuys'
|
||||
import { LoginUserBackup } from '@entity/LoginUserBackup'
|
||||
import { LoginEmailOptIn } from '@entity/LoginEmailOptIn'
|
||||
import { sendEMail } from '../../util/sendEMail'
|
||||
import { LoginUserRepository } from '../../typeorm/repository/LoginUser'
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const sodium = require('sodium-native')
|
||||
@ -174,7 +174,7 @@ const getEmailHash = (email: string): 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)
|
||||
nonce.fill(31) // static nonce
|
||||
|
||||
@ -182,6 +182,16 @@ const SecretKeyCryptographyEncrypt = (message: Buffer, encryptionKey: Buffer): B
|
||||
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()
|
||||
export class UserResolver {
|
||||
@Query(() => User)
|
||||
@ -200,7 +210,7 @@ export class UserResolver {
|
||||
|
||||
context.setHeaders.push({
|
||||
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)
|
||||
// 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
|
||||
if (!user.hasElopage && publisherId) {
|
||||
user.publisherId = publisherId
|
||||
await this.updateUserInfos(
|
||||
{ publisherId },
|
||||
{ sessionId: result.data.session_id, pubKey: result.data.user.public_hex },
|
||||
)
|
||||
|
||||
const loginUserRepository = getCustomRepository(LoginUserRepository)
|
||||
const loginUser = await loginUserRepository.findOneOrFail({ email: userEntity.email })
|
||||
loginUser.publisherId = publisherId
|
||||
loginUserRepository.save(loginUser)
|
||||
}
|
||||
|
||||
const userSettingRepository = getCustomRepository(UserSettingRepository)
|
||||
@ -446,7 +457,7 @@ export class UserResolver {
|
||||
}
|
||||
|
||||
@Authorized()
|
||||
@Mutation(() => UpdateUserInfosResponse)
|
||||
@Mutation(() => Boolean)
|
||||
async updateUserInfos(
|
||||
@Args()
|
||||
{
|
||||
@ -461,79 +472,97 @@ export class UserResolver {
|
||||
coinanimation,
|
||||
}: UpdateUserInfosArgs,
|
||||
@Ctx() context: any,
|
||||
): Promise<UpdateUserInfosResponse> {
|
||||
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
|
||||
): Promise<boolean> {
|
||||
const userRepository = getCustomRepository(UserRepository)
|
||||
const userEntity = await userRepository.findByPubkeyHex(context.pubKey)
|
||||
const loginUserRepository = getCustomRepository(LoginUserRepository)
|
||||
const loginUser = await loginUserRepository.findOneOrFail({ email: userEntity.email })
|
||||
|
||||
if (
|
||||
firstName ||
|
||||
lastName ||
|
||||
description ||
|
||||
username ||
|
||||
language ||
|
||||
publisherId ||
|
||||
passwordNew ||
|
||||
password
|
||||
) {
|
||||
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 (username) {
|
||||
throw new Error('change username currently not supported!')
|
||||
// TODO: this error was thrown on login_server whenever you tried to change the username
|
||||
// to anything except "" which is an exception to the rules below. Those were defined
|
||||
// aswell, even tho never used.
|
||||
// ^[a-zA-Z][a-zA-Z0-9_-]*$
|
||||
// username must start with [a-z] or [A-Z] and than can contain also [0-9], - and _
|
||||
// username already used
|
||||
// userEntity.username = username
|
||||
}
|
||||
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)
|
||||
userSettingRepository
|
||||
.setOrUpdate(userEntity.id, Setting.COIN_ANIMATION, coinanimation.toString())
|
||||
.catch((error) => {
|
||||
throw new Error(error)
|
||||
})
|
||||
if (lastName) {
|
||||
loginUser.lastName = lastName
|
||||
userEntity.lastName = lastName
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
response = new UpdateUserInfosResponse({ valid_values: 1 })
|
||||
} else {
|
||||
response.validValues++
|
||||
if (description) {
|
||||
loginUser.description = description
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
@ -2,27 +2,22 @@ import jwt, { JwtPayload } from 'jsonwebtoken'
|
||||
import CONFIG from '../config/'
|
||||
|
||||
interface CustomJwtPayload extends JwtPayload {
|
||||
sessionId: number
|
||||
pubKey: Buffer
|
||||
}
|
||||
|
||||
type DecodedJwt = {
|
||||
token: string
|
||||
sessionId: number
|
||||
pubKey: Buffer
|
||||
}
|
||||
|
||||
export default (token: string): DecodedJwt => {
|
||||
if (!token) throw new Error('401 Unauthorized')
|
||||
let sessionId = null
|
||||
let pubKey = null
|
||||
try {
|
||||
const decoded = <CustomJwtPayload>jwt.verify(token, CONFIG.JWT_SECRET)
|
||||
sessionId = decoded.sessionId
|
||||
pubKey = decoded.pubKey
|
||||
return {
|
||||
token,
|
||||
sessionId,
|
||||
pubKey,
|
||||
}
|
||||
} catch (err) {
|
||||
|
||||
@ -5,10 +5,9 @@ import jwt from 'jsonwebtoken'
|
||||
import CONFIG from '../config/'
|
||||
|
||||
// Generate an Access Token
|
||||
export default function encode(sessionId: number, pubKey: Buffer): string {
|
||||
const token = jwt.sign({ sessionId, pubKey }, CONFIG.JWT_SECRET, {
|
||||
export default function encode(pubKey: Buffer): string {
|
||||
const token = jwt.sign({ pubKey }, CONFIG.JWT_SECRET, {
|
||||
expiresIn: CONFIG.JWT_EXPIRES_IN,
|
||||
subject: sessionId.toString(),
|
||||
})
|
||||
return token
|
||||
}
|
||||
|
||||
@ -2,7 +2,12 @@ import { createTransport } from 'nodemailer'
|
||||
|
||||
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) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('Emails are disabled via config')
|
||||
|
||||
@ -38,9 +38,7 @@ export const updateUserInfos = gql`
|
||||
passwordNew: $passwordNew
|
||||
language: $locale
|
||||
coinanimation: $coinanimation
|
||||
) {
|
||||
validValues
|
||||
}
|
||||
)
|
||||
}
|
||||
`
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user