merge master

This commit is contained in:
ogerly 2021-12-10 11:00:37 +01:00
commit e91314f695
87 changed files with 2238 additions and 986 deletions

View File

@ -399,7 +399,7 @@ jobs:
report_name: Coverage Frontend report_name: Coverage Frontend
type: lcov type: lcov
result_path: ./coverage/lcov.info result_path: ./coverage/lcov.info
min_coverage: 87 min_coverage: 90
token: ${{ github.token }} token: ${{ github.token }}
############################################################################## ##############################################################################
@ -441,7 +441,7 @@ jobs:
report_name: Coverage Admin Interface report_name: Coverage Admin Interface
type: lcov type: lcov
result_path: ./coverage/lcov.info result_path: ./coverage/lcov.info
min_coverage: 60 min_coverage: 69
token: ${{ github.token }} token: ${{ github.token }}
############################################################################## ##############################################################################
@ -491,7 +491,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: 37
token: ${{ github.token }} token: ${{ github.token }}
############################################################################## ##############################################################################

View File

@ -11,11 +11,8 @@ import addNavigationGuards from './router/guards'
import i18n from './i18n' import i18n from './i18n'
import { ApolloClient, ApolloLink, InMemoryCache, HttpLink } from 'apollo-boost'
import VueApollo from 'vue-apollo' import VueApollo from 'vue-apollo'
import CONFIG from './config'
import { BootstrapVue, IconsPlugin } from 'bootstrap-vue' import { BootstrapVue, IconsPlugin } from 'bootstrap-vue'
import 'bootstrap/dist/css/bootstrap.css' import 'bootstrap/dist/css/bootstrap.css'
import 'bootstrap-vue/dist/bootstrap-vue.css' import 'bootstrap-vue/dist/bootstrap-vue.css'
@ -23,37 +20,7 @@ import 'bootstrap-vue/dist/bootstrap-vue.css'
import moment from 'vue-moment' import moment from 'vue-moment'
import Toasted from 'vue-toasted' import Toasted from 'vue-toasted'
const httpLink = new HttpLink({ uri: CONFIG.GRAPHQL_URI }) import { apolloProvider } from './plugins/apolloProvider'
const authLink = new ApolloLink((operation, forward) => {
const token = store.state.token
operation.setContext({
headers: {
Authorization: token && token.length > 0 ? `Bearer ${token}` : '',
},
})
return forward(operation).map((response) => {
if (response.errors && response.errors[0].message === '403.13 - Client certificate revoked') {
response.errors[0].message = i18n.t('error.session-expired')
store.dispatch('logout', null)
if (router.currentRoute.path !== '/logout') router.push('/logout')
return response
}
const newToken = operation.getContext().response.headers.get('token')
if (newToken) store.commit('token', newToken)
return response
})
})
const apolloClient = new ApolloClient({
link: authLink.concat(httpLink),
cache: new InMemoryCache(),
})
const apolloProvider = new VueApollo({
defaultClient: apolloClient,
})
Vue.use(BootstrapVue) Vue.use(BootstrapVue)

View File

@ -101,77 +101,4 @@ describe('main', () => {
}), }),
) )
}) })
describe('ApolloLink', () => {
// mock store
const storeDispatchMock = jest.fn()
store.state = {
token: 'some-token',
}
store.dispatch = storeDispatchMock
// mock i18n.t
i18n.t = jest.fn((t) => t)
// mock apllo response
const responseMock = {
errors: [{ message: '403.13 - Client certificate revoked' }],
}
// mock router
const routerPushMock = jest.fn()
router.push = routerPushMock
router.currentRoute = {
path: '/overview',
}
// mock context
const setContextMock = jest.fn()
const getContextMock = jest.fn(() => {
return {
response: {
headers: {
get: jest.fn(),
},
},
}
})
// mock apollo link function params
const operationMock = {
setContext: setContextMock,
getContext: getContextMock,
}
const forwardMock = jest.fn(() => {
return [responseMock]
})
// get apollo link callback
const middleware = ApolloLink.mock.calls[0][0]
beforeEach(() => {
jest.clearAllMocks()
// run the callback with mocked params
middleware(operationMock, forwardMock)
})
it('sets authorization header', () => {
expect(setContextMock).toBeCalledWith({
headers: {
Authorization: 'Bearer some-token',
},
})
})
describe('apollo response is 403.13', () => {
it.skip('dispatches logout', () => {
expect(storeDispatchMock).toBeCalledWith('logout', null)
})
it.skip('redirects to logout', () => {
expect(routerPushMock).toBeCalledWith('/logout')
})
})
})
}) })

View File

@ -76,6 +76,7 @@ export default {
this.$apollo this.$apollo
.query({ .query({
query: getPendingCreations, query: getPendingCreations,
fetchPolicy: 'network-only',
}) })
.then((result) => { .then((result) => {
this.$store.commit('resetOpenCreations') this.$store.commit('resetOpenCreations')

View File

@ -62,6 +62,7 @@ export default {
this.$apollo this.$apollo
.query({ .query({
query: getPendingCreations, query: getPendingCreations,
fetchPolicy: 'network-only',
}) })
.then((result) => { .then((result) => {
this.$store.commit('setOpenCreations', result.data.getPendingCreations.length) this.$store.commit('setOpenCreations', result.data.getPendingCreations.length)

View File

@ -0,0 +1,37 @@
import { ApolloClient, ApolloLink, InMemoryCache, HttpLink } from 'apollo-boost'
import VueApollo from 'vue-apollo'
import CONFIG from '../config'
import store from '../store/store'
import router from '../router/router'
import i18n from '../i18n'
const httpLink = new HttpLink({ uri: CONFIG.GRAPHQL_URI })
const authLink = new ApolloLink((operation, forward) => {
const token = store.state.token
operation.setContext({
headers: {
Authorization: token && token.length > 0 ? `Bearer ${token}` : '',
},
})
return forward(operation).map((response) => {
if (response.errors && response.errors[0].message === '403.13 - Client certificate revoked') {
response.errors[0].message = i18n.t('error.session-expired')
store.dispatch('logout', null)
if (router.currentRoute.path !== '/logout') router.push('/logout')
return response
}
const newToken = operation.getContext().response.headers.get('token')
if (newToken) store.commit('token', newToken)
return response
})
})
const apolloClient = new ApolloClient({
link: authLink.concat(httpLink),
cache: new InMemoryCache(),
})
export const apolloProvider = new VueApollo({
defaultClient: apolloClient,
})

View File

@ -0,0 +1,178 @@
import { ApolloClient, ApolloLink, HttpLink } from 'apollo-boost'
import './apolloProvider'
import CONFIG from '../config'
import VueApollo from 'vue-apollo'
import store from '../store/store'
import router from '../router/router'
import i18n from '../i18n'
jest.mock('vue-apollo')
jest.mock('../store/store')
jest.mock('../router/router')
jest.mock('../i18n')
jest.mock('apollo-boost', () => {
return {
__esModule: true,
ApolloClient: jest.fn(),
ApolloLink: jest.fn(() => {
return { concat: jest.fn() }
}),
InMemoryCache: jest.fn(),
HttpLink: jest.fn(),
}
})
describe('apolloProvider', () => {
it('calls the HttpLink', () => {
expect(HttpLink).toBeCalledWith({ uri: CONFIG.GRAPHQL_URI })
})
it('calls the ApolloLink', () => {
expect(ApolloLink).toBeCalled()
})
it('calls the ApolloClient', () => {
expect(ApolloClient).toBeCalled()
})
it('calls the VueApollo', () => {
expect(VueApollo).toBeCalled()
})
describe('ApolloLink', () => {
// mock store
const storeDispatchMock = jest.fn()
const storeCommitMock = jest.fn()
store.state = {
token: 'some-token',
}
store.dispatch = storeDispatchMock
store.commit = storeCommitMock
// mock i18n.t
i18n.t = jest.fn((t) => t)
// mock apllo response
const responseMock = {
errors: [{ message: '403.13 - Client certificate revoked' }],
}
// mock router
const routerPushMock = jest.fn()
router.push = routerPushMock
router.currentRoute = {
path: '/overview',
}
// mock context
const setContextMock = jest.fn()
const getContextMock = jest.fn(() => {
return {
response: {
headers: {
get: jest.fn(() => 'another-token'),
},
},
}
})
// mock apollo link function params
const operationMock = {
setContext: setContextMock,
getContext: getContextMock,
}
const forwardMock = jest.fn(() => {
return [responseMock]
})
// get apollo link callback
const middleware = ApolloLink.mock.calls[0][0]
describe('with token in store', () => {
it('sets authorization header with token', () => {
// run the apollo link callback with mocked params
middleware(operationMock, forwardMock)
expect(setContextMock).toBeCalledWith({
headers: {
Authorization: 'Bearer some-token',
},
})
})
})
describe('without token in store', () => {
beforeEach(() => {
store.state.token = null
})
it('sets authorization header empty', () => {
middleware(operationMock, forwardMock)
expect(setContextMock).toBeCalledWith({
headers: {
Authorization: '',
},
})
})
})
describe('apollo response is 403.13', () => {
beforeEach(() => {
// run the apollo link callback with mocked params
middleware(operationMock, forwardMock)
})
it('dispatches logout', () => {
expect(storeDispatchMock).toBeCalledWith('logout', null)
})
describe('current route is not logout', () => {
it('redirects to logout', () => {
expect(routerPushMock).toBeCalledWith('/logout')
})
})
describe('current route is logout', () => {
beforeEach(() => {
jest.clearAllMocks()
router.currentRoute.path = '/logout'
})
it('does not redirect to logout', () => {
expect(routerPushMock).not.toBeCalled()
})
})
})
describe('apollo response is with new token', () => {
beforeEach(() => {
delete responseMock.errors
middleware(operationMock, forwardMock)
})
it('commits new token to store', () => {
expect(storeCommitMock).toBeCalledWith('token', 'another-token')
})
})
describe('apollo response is without new token', () => {
beforeEach(() => {
jest.clearAllMocks()
getContextMock.mockReturnValue({
response: {
headers: {
get: jest.fn(() => null),
},
},
})
middleware(operationMock, forwardMock)
})
it('does not commit token to store', () => {
expect(storeCommitMock).not.toBeCalled()
})
})
})
})

View File

@ -2,8 +2,6 @@ PORT=4000
JWT_SECRET=secret123 JWT_SECRET=secret123
JWT_EXPIRES_IN=10m JWT_EXPIRES_IN=10m
GRAPHIQL=false GRAPHIQL=false
LOGIN_API_URL=http://login-server:1201/
COMMUNITY_API_URL=http://nginx/api/
GDT_API_URL=https://gdt.gradido.net GDT_API_URL=https://gdt.gradido.net
DB_HOST=localhost DB_HOST=localhost
DB_PORT=3306 DB_PORT=3306

View File

@ -4,10 +4,8 @@ export const INALIENABLE_RIGHTS = [
RIGHTS.LOGIN, RIGHTS.LOGIN,
RIGHTS.GET_COMMUNITY_INFO, RIGHTS.GET_COMMUNITY_INFO,
RIGHTS.COMMUNITIES, RIGHTS.COMMUNITIES,
RIGHTS.LOGIN_VIA_EMAIL_VERIFICATION_CODE,
RIGHTS.CREATE_USER, RIGHTS.CREATE_USER,
RIGHTS.SEND_RESET_PASSWORD_EMAIL, RIGHTS.SEND_RESET_PASSWORD_EMAIL,
RIGHTS.RESET_PASSWORD, RIGHTS.SET_PASSWORD,
RIGHTS.CHECK_USERNAME, RIGHTS.CHECK_USERNAME,
RIGHTS.CHECK_EMAIL,
] ]

View File

@ -12,14 +12,12 @@ export enum RIGHTS {
SUBSCRIBE_NEWSLETTER = 'SUBSCRIBE_NEWSLETTER', SUBSCRIBE_NEWSLETTER = 'SUBSCRIBE_NEWSLETTER',
TRANSACTION_LIST = 'TRANSACTION_LIST', TRANSACTION_LIST = 'TRANSACTION_LIST',
SEND_COINS = 'SEND_COINS', SEND_COINS = 'SEND_COINS',
LOGIN_VIA_EMAIL_VERIFICATION_CODE = 'LOGIN_VIA_EMAIL_VERIFICATION_CODE',
LOGOUT = 'LOGOUT', LOGOUT = 'LOGOUT',
CREATE_USER = 'CREATE_USER', CREATE_USER = 'CREATE_USER',
SEND_RESET_PASSWORD_EMAIL = 'SEND_RESET_PASSWORD_EMAIL', SEND_RESET_PASSWORD_EMAIL = 'SEND_RESET_PASSWORD_EMAIL',
RESET_PASSWORD = 'RESET_PASSWORD', SET_PASSWORD = 'SET_PASSWORD',
UPDATE_USER_INFOS = 'UPDATE_USER_INFOS', UPDATE_USER_INFOS = 'UPDATE_USER_INFOS',
CHECK_USERNAME = 'CHECK_USERNAME', CHECK_USERNAME = 'CHECK_USERNAME',
CHECK_EMAIL = 'CHECK_EMAIL',
HAS_ELOPAGE = 'HAS_ELOPAGE', HAS_ELOPAGE = 'HAS_ELOPAGE',
// Admin // Admin
SEARCH_USERS = 'SEARCH_USERS', SEARCH_USERS = 'SEARCH_USERS',

View File

@ -8,8 +8,6 @@ const server = {
JWT_SECRET: process.env.JWT_SECRET || 'secret123', JWT_SECRET: process.env.JWT_SECRET || 'secret123',
JWT_EXPIRES_IN: process.env.JWT_EXPIRES_IN || '10m', JWT_EXPIRES_IN: process.env.JWT_EXPIRES_IN || '10m',
GRAPHIQL: process.env.GRAPHIQL === 'true' || false, GRAPHIQL: process.env.GRAPHIQL === 'true' || false,
LOGIN_API_URL: process.env.LOGIN_API_URL || 'http://login-server:1201/',
COMMUNITY_API_URL: process.env.COMMUNITY_API_URL || 'http://nginx/api/',
GDT_API_URL: process.env.GDT_API_URL || 'https://gdt.gradido.net', GDT_API_URL: process.env.GDT_API_URL || 'https://gdt.gradido.net',
PRODUCTION: process.env.NODE_ENV === 'production' || false, PRODUCTION: process.env.NODE_ENV === 'production' || false,
} }
@ -53,6 +51,7 @@ const email = {
EMAIL_SMTP_PORT: process.env.EMAIL_SMTP_PORT || '587', EMAIL_SMTP_PORT: process.env.EMAIL_SMTP_PORT || '587',
EMAIL_LINK_VERIFICATION: EMAIL_LINK_VERIFICATION:
process.env.EMAIL_LINK_VERIFICATION || 'http://localhost/vue/checkEmail/$1', process.env.EMAIL_LINK_VERIFICATION || 'http://localhost/vue/checkEmail/$1',
EMAIL_LINK_SETPASSWORD: process.env.EMAIL_LINK_SETPASSWORD || 'http://localhost/vue/reset/$1',
} }
const webhook = { const webhook = {

View File

@ -1,13 +0,0 @@
import { ArgsType, Field } from 'type-graphql'
@ArgsType()
export default class ChangePasswordArgs {
@Field(() => Number)
sessionId: number
@Field(() => String)
email: string
@Field(() => String)
password: string
}

View File

@ -1,29 +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 CheckEmailResponse {
constructor(json: any) {
this.sessionId = json.session_id
this.email = json.user.email
this.language = json.user.language
this.firstName = json.user.first_name
this.lastName = json.user.last_name
}
@Field(() => Number)
sessionId: number
@Field(() => String)
email: string
@Field(() => String)
firstName: string
@Field(() => String)
lastName: string
@Field(() => String)
language: string
}

View File

@ -1,17 +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 LoginViaVerificationCode {
constructor(json: any) {
this.sessionId = json.session_id
this.email = json.user.email
}
@Field(() => Number)
sessionId: number
@Field(() => String)
email: string
}

View File

@ -1,17 +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 SendPasswordResetEmailResponse {
constructor(json: any) {
this.state = json.state
this.msg = json.msg
}
@Field(() => String)
state: string
@Field(() => String)
msg?: string
}

View File

@ -83,7 +83,7 @@ export class AdminResolver {
await pendingCreationRepository.save(updatedCreation) await pendingCreationRepository.save(updatedCreation)
const result = new UpdatePendingCreation() const result = new UpdatePendingCreation()
result.amount = parseInt(updatedCreation.amount.toString()) result.amount = parseInt(amount.toString())
result.memo = updatedCreation.memo result.memo = updatedCreation.memo
result.date = updatedCreation.date result.date = updatedCreation.date
result.moderator = updatedCreation.moderator result.moderator = updatedCreation.moderator
@ -176,7 +176,7 @@ export class AdminResolver {
} else { } else {
newBalance = lastUserTransaction.balance newBalance = lastUserTransaction.balance
} }
newBalance = Number(newBalance) + Number(parseInt(pendingCreation.amount.toString()) / 10000) newBalance = Number(newBalance) + Number(parseInt(pendingCreation.amount.toString()))
const newUserTransaction = new UserTransaction() const newUserTransaction = new UserTransaction()
newUserTransaction.userId = pendingCreation.userId newUserTransaction.userId = pendingCreation.userId
@ -194,7 +194,7 @@ export class AdminResolver {
if (!userBalance) userBalance = balanceRepository.create() if (!userBalance) userBalance = balanceRepository.create()
userBalance.userId = pendingCreation.userId userBalance.userId = pendingCreation.userId
userBalance.amount = Number(newBalance * 10000) userBalance.amount = Number(newBalance)
userBalance.modified = new Date() userBalance.modified = new Date()
userBalance.recordDate = userBalance.recordDate ? userBalance.recordDate : new Date() userBalance.recordDate = userBalance.recordDate ? userBalance.recordDate : new Date()
await balanceRepository.save(userBalance) await balanceRepository.save(userBalance)

View File

@ -428,7 +428,7 @@ async function addUserTransaction(
if (lastUserTransaction) { if (lastUserTransaction) {
newBalance += Number( newBalance += Number(
await calculateDecay( await calculateDecay(
Number(lastUserTransaction.balance * 10000), Number(lastUserTransaction.balance),
lastUserTransaction.balanceDate, lastUserTransaction.balanceDate,
transaction.received, transaction.received,
).catch(() => { ).catch(() => {

View File

@ -3,24 +3,16 @@
import fs from 'fs' import fs from 'fs'
import { Resolver, Query, Args, Arg, Authorized, Ctx, UseMiddleware, Mutation } from 'type-graphql' import { Resolver, Query, Args, Arg, Authorized, Ctx, UseMiddleware, Mutation } from 'type-graphql'
import { getConnection, getCustomRepository } from 'typeorm' import { getConnection, getCustomRepository, getRepository } from 'typeorm'
import CONFIG from '../../config' import CONFIG from '../../config'
import { LoginViaVerificationCode } from '../model/LoginViaVerificationCode'
import { SendPasswordResetEmailResponse } from '../model/SendPasswordResetEmailResponse'
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 '../../auth/JWT' import { encode } from '../../auth/JWT'
import ChangePasswordArgs from '../arg/ChangePasswordArgs'
import CheckUsernameArgs from '../arg/CheckUsernameArgs' import CheckUsernameArgs from '../arg/CheckUsernameArgs'
import CreateUserArgs from '../arg/CreateUserArgs' import CreateUserArgs from '../arg/CreateUserArgs'
import UnsecureLoginArgs from '../arg/UnsecureLoginArgs' import UnsecureLoginArgs from '../arg/UnsecureLoginArgs'
import UpdateUserInfosArgs from '../arg/UpdateUserInfosArgs' import UpdateUserInfosArgs from '../arg/UpdateUserInfosArgs'
import { apiPost, apiGet } from '../../apis/HttpRequest' import { klicktippNewsletterStateMiddleware } from '../../middleware/klicktippMiddleware'
import {
klicktippRegistrationMiddleware,
klicktippNewsletterStateMiddleware,
} from '../../middleware/klicktippMiddleware'
import { CheckEmailResponse } from '../model/CheckEmailResponse'
import { UserSettingRepository } from '../../typeorm/repository/UserSettingRepository' import { UserSettingRepository } from '../../typeorm/repository/UserSettingRepository'
import { LoginUserRepository } from '../../typeorm/repository/LoginUser' import { LoginUserRepository } from '../../typeorm/repository/LoginUser'
import { Setting } from '../enum/Setting' import { Setting } from '../enum/Setting'
@ -30,10 +22,14 @@ 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 { LoginElopageBuysRepository } from '../../typeorm/repository/LoginElopageBuys' import { LoginElopageBuysRepository } from '../../typeorm/repository/LoginElopageBuys'
import { signIn } from '../../apis/KlicktippController'
import { RIGHTS } from '../../auth/RIGHTS' import { RIGHTS } from '../../auth/RIGHTS'
import { ServerUserRepository } from '../../typeorm/repository/ServerUser' import { ServerUserRepository } from '../../typeorm/repository/ServerUser'
import { ROLE_ADMIN } from '../../auth/ROLES' import { ROLE_ADMIN } from '../../auth/ROLES'
const EMAIL_OPT_IN_RESET_PASSWORD = 2
const EMAIL_OPT_IN_REGISTER = 1
// 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
@ -58,50 +54,8 @@ const PassphraseGenerate = (): string[] => {
result.push(WORDS[sodium.randombytes_random() % 2048]) result.push(WORDS[sodium.randombytes_random() % 2048])
} }
return result 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[] => { const KeyPairEd25519Create = (passphrase: string[]): Buffer[] => {
if (!passphrase.length || passphrase.length < PHRASE_WORD_COUNT) { if (!passphrase.length || passphrase.length < PHRASE_WORD_COUNT) {
throw new Error('passphrase empty or to short') throw new Error('passphrase empty or to short')
@ -240,13 +194,21 @@ export class UserResolver {
@Ctx() context: any, @Ctx() context: any,
): Promise<User> { ): Promise<User> {
email = email.trim().toLowerCase() email = email.trim().toLowerCase()
// const result = await apiPost(CONFIG.LOGIN_API_URL + 'unsecureLogin', { email, password })
// UnsecureLogin
const loginUserRepository = getCustomRepository(LoginUserRepository) const loginUserRepository = getCustomRepository(LoginUserRepository)
const loginUser = await loginUserRepository.findByEmail(email).catch(() => { const loginUser = await loginUserRepository.findByEmail(email).catch(() => {
throw new Error('No user with this credentials') throw new Error('No user with this credentials')
}) })
if (!loginUser.emailChecked) throw new Error('user email not validated') if (!loginUser.emailChecked) {
throw new Error('User email not validated')
}
if (loginUser.password === BigInt(0)) {
// 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 password set yet')
}
if (!loginUser.pubKey || !loginUser.privKey) {
// 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')
}
const passwordHash = SecretKeyCryptographyCreateKey(email, password) // return short and long hash const passwordHash = SecretKeyCryptographyCreateKey(email, password) // return short and long hash
const loginUserPassword = BigInt(loginUser.password.toString()) const loginUserPassword = BigInt(loginUser.password.toString())
if (loginUserPassword !== passwordHash[0].readBigUInt64LE()) { if (loginUserPassword !== passwordHash[0].readBigUInt64LE()) {
@ -320,22 +282,6 @@ export class UserResolver {
return user return user
} }
@Authorized([RIGHTS.LOGIN_VIA_EMAIL_VERIFICATION_CODE])
@Query(() => LoginViaVerificationCode)
async loginViaEmailVerificationCode(
@Arg('optin') optin: string,
): Promise<LoginViaVerificationCode> {
// I cannot use number as type here.
// The value received is not the same as sent by the query
const result = await apiGet(
CONFIG.LOGIN_API_URL + 'loginViaEmailVerificationCode?emailVerificationCode=' + optin,
)
if (!result.success) {
throw new Error(result.data)
}
return new LoginViaVerificationCode(result.data)
}
@Authorized([RIGHTS.LOGOUT]) @Authorized([RIGHTS.LOGOUT])
@Query(() => String) @Query(() => String)
async logout(): Promise<boolean> { async logout(): Promise<boolean> {
@ -350,7 +296,7 @@ export class UserResolver {
@Authorized([RIGHTS.CREATE_USER]) @Authorized([RIGHTS.CREATE_USER])
@Mutation(() => String) @Mutation(() => String)
async createUser( async createUser(
@Args() { email, firstName, lastName, password, language, publisherId }: CreateUserArgs, @Args() { email, firstName, lastName, language, publisherId }: CreateUserArgs,
): Promise<string> { ): Promise<string> {
// TODO: wrong default value (should be null), how does graphql work here? Is it an required field? // TODO: wrong default value (should be null), how does graphql work here? Is it an required field?
// default int publisher_id = 0; // default int publisher_id = 0;
@ -360,13 +306,6 @@ export class UserResolver {
language = DEFAULT_LANGUAGE language = DEFAULT_LANGUAGE
} }
// 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 // Validate username
// TODO: never true // TODO: never true
const username = '' const username = ''
@ -384,10 +323,10 @@ export class UserResolver {
} }
const passphrase = PassphraseGenerate() const passphrase = PassphraseGenerate()
const keyPair = KeyPairEd25519Create(passphrase) // return pub, priv Key // const keyPair = KeyPairEd25519Create(passphrase) // return pub, priv Key
const passwordHash = SecretKeyCryptographyCreateKey(email, password) // return short and long hash // const passwordHash = SecretKeyCryptographyCreateKey(email, password) // return short and long hash
// const encryptedPrivkey = SecretKeyCryptographyEncrypt(keyPair[1], passwordHash[1])
const emailHash = getEmailHash(email) const emailHash = getEmailHash(email)
const encryptedPrivkey = SecretKeyCryptographyEncrypt(keyPair[1], passwordHash[1])
// Table: login_users // Table: login_users
const loginUser = new LoginUser() const loginUser = new LoginUser()
@ -396,13 +335,13 @@ export class UserResolver {
loginUser.lastName = lastName loginUser.lastName = lastName
loginUser.username = username loginUser.username = username
loginUser.description = '' loginUser.description = ''
loginUser.password = passwordHash[0].readBigUInt64LE() // using the shorthash // loginUser.password = passwordHash[0].readBigUInt64LE() // using the shorthash
loginUser.emailHash = emailHash loginUser.emailHash = emailHash
loginUser.language = language loginUser.language = language
loginUser.groupId = 1 loginUser.groupId = 1
loginUser.publisherId = publisherId loginUser.publisherId = publisherId
loginUser.pubKey = keyPair[0] // loginUser.pubKey = keyPair[0]
loginUser.privKey = encryptedPrivkey // loginUser.privKey = encryptedPrivkey
const queryRunner = getConnection().createQueryRunner() const queryRunner = getConnection().createQueryRunner()
await queryRunner.connect() await queryRunner.connect()
@ -428,11 +367,13 @@ export class UserResolver {
// Table: state_users // Table: state_users
const dbUser = new DbUser() const dbUser = new DbUser()
dbUser.pubkey = keyPair[0]
dbUser.email = email dbUser.email = email
dbUser.firstName = firstName dbUser.firstName = firstName
dbUser.lastName = lastName dbUser.lastName = lastName
dbUser.username = username dbUser.username = username
// TODO this field has no null allowed unlike the loginServer table
dbUser.pubkey = Buffer.alloc(32, 0) // default to 0000...
// dbUser.pubkey = keyPair[0]
await queryRunner.manager.save(dbUser).catch((er) => { await queryRunner.manager.save(dbUser).catch((er) => {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
@ -441,10 +382,11 @@ export class UserResolver {
}) })
// Store EmailOptIn in DB // Store EmailOptIn in DB
// TODO: this has duplicate code with sendResetPasswordEmail
const emailOptIn = new LoginEmailOptIn() const emailOptIn = new LoginEmailOptIn()
emailOptIn.userId = loginUserId emailOptIn.userId = loginUserId
emailOptIn.verificationCode = random(64) emailOptIn.verificationCode = random(64)
emailOptIn.emailOptInTypeId = 2 emailOptIn.emailOptInTypeId = EMAIL_OPT_IN_REGISTER
await queryRunner.manager.save(emailOptIn).catch((error) => { await queryRunner.manager.save(emailOptIn).catch((error) => {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
@ -489,38 +431,172 @@ export class UserResolver {
} }
@Authorized([RIGHTS.SEND_RESET_PASSWORD_EMAIL]) @Authorized([RIGHTS.SEND_RESET_PASSWORD_EMAIL])
@Query(() => SendPasswordResetEmailResponse) @Query(() => Boolean)
async sendResetPasswordEmail( async sendResetPasswordEmail(@Arg('email') email: string): Promise<boolean> {
@Arg('email') email: string, // TODO: this has duplicate code with createUser
): Promise<SendPasswordResetEmailResponse> { // TODO: Moriz: I think we do not need this variable.
const payload = { let emailAlreadySend = false
email,
email_text: 7, const loginUserRepository = await getCustomRepository(LoginUserRepository)
email_verification_code_type: 'resetPassword', const loginUser = await loginUserRepository.findOneOrFail({ email })
const loginEmailOptInRepository = await getRepository(LoginEmailOptIn)
let optInCode = await loginEmailOptInRepository.findOne({
userId: loginUser.id,
emailOptInTypeId: EMAIL_OPT_IN_RESET_PASSWORD,
})
if (optInCode) {
emailAlreadySend = true
} else {
optInCode = new LoginEmailOptIn()
optInCode.verificationCode = random(64)
optInCode.userId = loginUser.id
optInCode.emailOptInTypeId = EMAIL_OPT_IN_RESET_PASSWORD
await loginEmailOptInRepository.save(optInCode)
} }
const response = await apiPost(CONFIG.LOGIN_API_URL + 'sendEmail', payload)
if (!response.success) { const link = CONFIG.EMAIL_LINK_SETPASSWORD.replace(
throw new Error(response.data) /\$1/g,
optInCode.verificationCode.toString(),
)
if (emailAlreadySend) {
const timeElapsed = Date.now() - new Date(optInCode.updatedAt).getTime()
if (timeElapsed <= 10 * 60 * 1000) {
throw new Error('email already sent less than 10 minutes before')
}
} }
return new SendPasswordResetEmailResponse(response.data)
const emailSent = await sendEMail({
from: `Gradido (nicht antworten) <${CONFIG.EMAIL_SENDER}>`,
to: `${loginUser.firstName} ${loginUser.lastName} <${email}>`,
subject: 'Gradido: Reset Password',
text: `Hallo ${loginUser.firstName} ${loginUser.lastName},
Du oder jemand anderes hat für dieses Konto ein Zurücksetzen des Passworts angefordert.
Wenn du es warst, klicke bitte auf den Link: ${link}
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(`Reset password link: ${link}`)
}
return true
} }
@Authorized([RIGHTS.RESET_PASSWORD]) @Authorized([RIGHTS.SET_PASSWORD])
@Mutation(() => String) @Mutation(() => Boolean)
async resetPassword( async setPassword(
@Args() @Arg('code') code: string,
{ sessionId, email, password }: ChangePasswordArgs, @Arg('password') password: string,
): Promise<string> { ): Promise<boolean> {
const payload = { // Validate Password
session_id: sessionId, if (!isPassword(password)) {
email, throw new Error(
password, 'Please enter a valid password with at least 8 characters, upper and lower case letters, at least one number and one special character!',
)
} }
const result = await apiPost(CONFIG.LOGIN_API_URL + 'resetPassword', payload)
if (!result.success) { // Load code
throw new Error(result.data) const loginEmailOptInRepository = await getRepository(LoginEmailOptIn)
const optInCode = await loginEmailOptInRepository
.findOneOrFail({ verificationCode: code })
.catch(() => {
throw new Error('Could not login with emailVerificationCode')
})
// Code is only valid for 10minutes
const timeElapsed = Date.now() - new Date(optInCode.updatedAt).getTime()
if (timeElapsed > 10 * 60 * 1000) {
throw new Error('Code is older than 10 minutes')
} }
return 'success'
// load loginUser
const loginUserRepository = await getCustomRepository(LoginUserRepository)
const loginUser = await loginUserRepository
.findOneOrFail({ id: optInCode.userId })
.catch(() => {
throw new Error('Could not find corresponding Login User')
})
// load user
const dbUserRepository = await getCustomRepository(UserRepository)
const dbUser = await dbUserRepository.findOneOrFail({ email: loginUser.email }).catch(() => {
throw new Error('Could not find corresponding User')
})
const loginUserBackupRepository = await getRepository(LoginUserBackup)
const loginUserBackup = await loginUserBackupRepository
.findOneOrFail({ userId: loginUser.id })
.catch(() => {
throw new Error('Could not find corresponding BackupUser')
})
const passphrase = loginUserBackup.passphrase.slice(0, -1).split(' ')
if (passphrase.length < PHRASE_WORD_COUNT) {
// TODO if this can happen we cannot recover from that
throw new Error('Could not load a correct passphrase')
}
// Activate EMail
loginUser.emailChecked = true
// Update Password
const passwordHash = SecretKeyCryptographyCreateKey(loginUser.email, password) // return short and long hash
const keyPair = KeyPairEd25519Create(passphrase) // return pub, priv Key
const encryptedPrivkey = SecretKeyCryptographyEncrypt(keyPair[1], passwordHash[1])
loginUser.password = passwordHash[0].readBigUInt64LE() // using the shorthash
loginUser.pubKey = keyPair[0]
loginUser.privKey = encryptedPrivkey
dbUser.pubkey = keyPair[0]
const queryRunner = getConnection().createQueryRunner()
await queryRunner.connect()
await queryRunner.startTransaction('READ UNCOMMITTED')
try {
// Save loginUser
await queryRunner.manager.save(loginUser).catch((error) => {
throw new Error('error saving loginUser: ' + error)
})
// Save user
await queryRunner.manager.save(dbUser).catch((error) => {
throw new Error('error saving user: ' + error)
})
// Delete Code
await queryRunner.manager.remove(optInCode).catch((error) => {
throw new Error('error deleting code: ' + error)
})
await queryRunner.commitTransaction()
} catch (e) {
await queryRunner.rollbackTransaction()
throw e
} finally {
await queryRunner.release()
}
// Sign into Klicktipp
// TODO do we always signUp the user? How to handle things with old users?
if (optInCode.emailOptInTypeId === EMAIL_OPT_IN_REGISTER) {
try {
await signIn(loginUser.email, loginUser.language, loginUser.firstName, loginUser.lastName)
} catch {
// TODO is this a problem?
// eslint-disable-next-line no-console
console.log('Could not subscribe to klicktipp')
}
}
return true
} }
@Authorized([RIGHTS.UPDATE_USER_INFOS]) @Authorized([RIGHTS.UPDATE_USER_INFOS])
@ -656,19 +732,6 @@ export class UserResolver {
return true return true
} }
@Authorized([RIGHTS.CHECK_EMAIL])
@Query(() => CheckEmailResponse)
@UseMiddleware(klicktippRegistrationMiddleware)
async checkEmail(@Arg('optin') optin: string): Promise<CheckEmailResponse> {
const result = await apiGet(
CONFIG.LOGIN_API_URL + 'loginViaEmailVerificationCode?emailVerificationCode=' + optin,
)
if (!result.success) {
throw new Error(result.data)
}
return new CheckEmailResponse(result.data)
}
@Authorized([RIGHTS.HAS_ELOPAGE]) @Authorized([RIGHTS.HAS_ELOPAGE])
@Query(() => Boolean) @Query(() => Boolean)
async hasElopage(@Ctx() context: any): Promise<boolean> { async hasElopage(@Ctx() context: any): Promise<boolean> {

View File

@ -1,20 +1,20 @@
import { MiddlewareFn } from 'type-graphql' import { MiddlewareFn } from 'type-graphql'
import { signIn, getKlickTippUser } from '../apis/KlicktippController' import { /* signIn, */ getKlickTippUser } from '../apis/KlicktippController'
import { KlickTipp } from '../graphql/model/KlickTipp' import { KlickTipp } from '../graphql/model/KlickTipp'
import CONFIG from '../config/index' import CONFIG from '../config/index'
export const klicktippRegistrationMiddleware: MiddlewareFn = async ( // export const klicktippRegistrationMiddleware: MiddlewareFn = async (
// Only for demo // // Only for demo
/* eslint-disable-next-line @typescript-eslint/no-unused-vars */ // /* eslint-disable-next-line @typescript-eslint/no-unused-vars */
{ root, args, context, info }, // { root, args, context, info },
next, // next,
) => { // ) => {
// Do Something here before resolver is called // // Do Something here before resolver is called
const result = await next() // const result = await next()
// Do Something here after resolver is completed // // Do Something here after resolver is completed
await signIn(result.email, result.language, result.firstName, result.lastName) // await signIn(result.email, result.language, result.firstName, result.lastName)
return result // return result
} // }
export const klicktippNewsletterStateMiddleware: MiddlewareFn = async ( export const klicktippNewsletterStateMiddleware: MiddlewareFn = async (
/* eslint-disable-next-line @typescript-eslint/no-unused-vars */ /* eslint-disable-next-line @typescript-eslint/no-unused-vars */

View File

@ -39,6 +39,7 @@ yarn seed
## Seeded Users ## Seeded Users
| email | password | admin | | email | password | admin |
|------------------------|------------|---------|
| peter@lustig.de | `Aa12345_` | `true` | | peter@lustig.de | `Aa12345_` | `true` |
| bibi@bloxberg.de | `Aa12345_` | `false` | | bibi@bloxberg.de | `Aa12345_` | `false` |
| raeuber@hotzenplotz.de | `Aa12345_` | `false` | | raeuber@hotzenplotz.de | `Aa12345_` | `false` |

View File

@ -1,4 +1,5 @@
import { BaseEntity, Entity, PrimaryGeneratedColumn, Column } from 'typeorm' import { BaseEntity, Entity, PrimaryGeneratedColumn, Column, JoinColumn, OneToOne } from 'typeorm'
import { User } from '../User'
@Entity('state_balances') @Entity('state_balances')
export class Balance extends BaseEntity { export class Balance extends BaseEntity {
@ -16,4 +17,8 @@ export class Balance extends BaseEntity {
@Column({ type: 'bigint' }) @Column({ type: 'bigint' })
amount: number amount: number
@OneToOne(() => User, { nullable: false })
@JoinColumn({ name: 'state_user_id' })
user: User
} }

View File

@ -0,0 +1,21 @@
import { BaseEntity, Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn } from 'typeorm'
import { Transaction } from './Transaction'
@Entity('transaction_signatures')
export class TransactionSignature extends BaseEntity {
@PrimaryGeneratedColumn()
id: number
@Column({ name: 'transaction_id' })
transactionId: number
@Column({ type: 'binary', length: 64 })
signature: Buffer
@Column({ type: 'binary', length: 32 })
pubkey: Buffer
@ManyToOne(() => Transaction)
@JoinColumn({ name: 'transaction_id' })
transaction: Transaction
}

View File

@ -1,4 +1,5 @@
import { BaseEntity, Entity, PrimaryGeneratedColumn, Column } from 'typeorm' import { BaseEntity, Entity, PrimaryGeneratedColumn, Column, OneToOne } from 'typeorm'
import { Balance } from '../Balance'
// Moriz: I do not like the idea of having two user tables // Moriz: I do not like the idea of having two user tables
@Entity('state_users') @Entity('state_users')
@ -29,4 +30,7 @@ export class User extends BaseEntity {
@Column() @Column()
disabled: boolean disabled: boolean
@OneToOne(() => Balance, (balance) => balance.user)
balance: Balance
} }

View File

@ -0,0 +1 @@
export { TransactionSignature } from './0001-init_db/TransactionSignature'

View File

@ -8,6 +8,7 @@ import { Migration } from './Migration'
import { ServerUser } from './ServerUser' import { ServerUser } from './ServerUser'
import { Transaction } from './Transaction' import { Transaction } from './Transaction'
import { TransactionCreation } from './TransactionCreation' import { TransactionCreation } from './TransactionCreation'
import { TransactionSignature } from './TransactionSignature'
import { TransactionSendCoin } from './TransactionSendCoin' import { TransactionSendCoin } from './TransactionSendCoin'
import { User } from './User' import { User } from './User'
import { UserSetting } from './UserSetting' import { UserSetting } from './UserSetting'
@ -25,6 +26,7 @@ export const entities = [
ServerUser, ServerUser,
Transaction, Transaction,
TransactionCreation, TransactionCreation,
TransactionSignature,
TransactionSendCoin, TransactionSendCoin,
User, User,
UserSetting, UserSetting,

View File

@ -25,34 +25,34 @@ export async function upgrade(queryFn: (query: string, values?: any[]) => Promis
} }
await queryFn(` await queryFn(`
INSERT INTO \`login_app_access_tokens\` SELECT * FROM ${LOGIN_SERVER_DB}.\`app_access_tokens\`; INSERT IGNORE INTO \`login_app_access_tokens\` SELECT * FROM ${LOGIN_SERVER_DB}.\`app_access_tokens\`;
`) `)
await queryFn(` await queryFn(`
INSERT INTO \`login_elopage_buys\` SELECT * FROM ${LOGIN_SERVER_DB}.\`elopage_buys\`; INSERT IGNORE INTO \`login_elopage_buys\` SELECT * FROM ${LOGIN_SERVER_DB}.\`elopage_buys\`;
`) `)
await queryFn(` await queryFn(`
INSERT INTO \`login_email_opt_in_types\` SELECT * FROM ${LOGIN_SERVER_DB}.\`email_opt_in_types\`; INSERT IGNORE INTO \`login_email_opt_in_types\` SELECT * FROM ${LOGIN_SERVER_DB}.\`email_opt_in_types\`;
`) `)
await queryFn(` await queryFn(`
INSERT INTO \`login_email_opt_in\` SELECT * FROM ${LOGIN_SERVER_DB}.\`email_opt_in\`; INSERT IGNORE INTO \`login_email_opt_in\` SELECT * FROM ${LOGIN_SERVER_DB}.\`email_opt_in\`;
`) `)
await queryFn(` await queryFn(`
INSERT INTO \`login_groups\` SELECT * FROM ${LOGIN_SERVER_DB}.\`groups\`; INSERT IGNORE INTO \`login_groups\` SELECT * FROM ${LOGIN_SERVER_DB}.\`groups\`;
`) `)
await queryFn(` await queryFn(`
INSERT INTO \`login_pending_tasks\` SELECT * FROM ${LOGIN_SERVER_DB}.\`pending_tasks\`; INSERT IGNORE INTO \`login_pending_tasks\` SELECT * FROM ${LOGIN_SERVER_DB}.\`pending_tasks\`;
`) `)
await queryFn(` await queryFn(`
INSERT INTO \`login_roles\` SELECT * FROM ${LOGIN_SERVER_DB}.\`roles\`; INSERT IGNORE INTO \`login_roles\` SELECT * FROM ${LOGIN_SERVER_DB}.\`roles\`;
`) `)
await queryFn(` await queryFn(`
INSERT INTO \`login_user_backups\` SELECT * FROM ${LOGIN_SERVER_DB}.\`user_backups\`; INSERT IGNORE INTO \`login_user_backups\` SELECT * FROM ${LOGIN_SERVER_DB}.\`user_backups\`;
`) `)
await queryFn(` await queryFn(`
INSERT INTO \`login_user_roles\` SELECT * FROM ${LOGIN_SERVER_DB}.\`user_roles\`; INSERT IGNORE INTO \`login_user_roles\` SELECT * FROM ${LOGIN_SERVER_DB}.\`user_roles\`;
`) `)
await queryFn(` await queryFn(`
INSERT INTO \`login_users\` SELECT * FROM ${LOGIN_SERVER_DB}.\`users\`; INSERT IGNORE INTO \`login_users\` SELECT * FROM ${LOGIN_SERVER_DB}.\`users\`;
`) `)
// TODO clarify if we need this on non docker environment? // TODO clarify if we need this on non docker environment?

View File

@ -0,0 +1,18 @@
import Faker from 'faker'
import { define } from 'typeorm-seeding'
import { Balance } from '../../entity/Balance'
import { BalanceContext } from '../interface/TransactionContext'
define(Balance, (faker: typeof Faker, context?: BalanceContext) => {
if (!context || !context.user) {
throw new Error('Balance: No user present!')
}
const balance = new Balance()
balance.modified = context.modified ? context.modified : faker.date.recent()
balance.recordDate = context.recordDate ? context.recordDate : faker.date.recent()
balance.amount = context.amount ? context.amount : 10000000
balance.user = context.user
return balance
})

View File

@ -0,0 +1,18 @@
import Faker from 'faker'
import { define } from 'typeorm-seeding'
import { TransactionCreation } from '../../entity/TransactionCreation'
import { TransactionCreationContext } from '../interface/TransactionContext'
define(TransactionCreation, (faker: typeof Faker, context?: TransactionCreationContext) => {
if (!context || !context.userId || !context.transaction) {
throw new Error('TransactionCreation: No userId and/or transaction present!')
}
const transactionCreation = new TransactionCreation()
transactionCreation.userId = context.userId
transactionCreation.amount = context.amount ? context.amount : 100000
transactionCreation.targetDate = context.targetDate ? context.targetDate : new Date()
transactionCreation.transaction = context.transaction
return transactionCreation
})

View File

@ -0,0 +1,18 @@
import Faker from 'faker'
import { define } from 'typeorm-seeding'
import { TransactionSignature } from '../../entity/TransactionSignature'
import { TransactionSignatureContext } from '../interface/TransactionContext'
import { randomBytes } from 'crypto'
define(TransactionSignature, (faker: typeof Faker, context?: TransactionSignatureContext) => {
if (!context || !context.transaction) {
throw new Error('TransactionSignature: No transaction present!')
}
const transactionSignature = new TransactionSignature()
transactionSignature.signature = context.signature ? context.signature : randomBytes(64)
transactionSignature.pubkey = context.pubkey ? context.pubkey : randomBytes(32)
transactionSignature.transaction = context.transaction
return transactionSignature
})

View File

@ -0,0 +1,20 @@
import Faker from 'faker'
import { define } from 'typeorm-seeding'
import { Transaction } from '../../entity/Transaction'
import { TransactionContext } from '../interface/TransactionContext'
import { randomBytes } from 'crypto'
define(Transaction, (faker: typeof Faker, context?: TransactionContext) => {
if (!context) context = {}
const transaction = new Transaction()
transaction.transactionTypeId = context.transactionTypeId ? context.transactionTypeId : 2
transaction.txHash = context.txHash ? context.txHash : randomBytes(48)
transaction.memo = context.memo || context.memo === '' ? context.memo : faker.lorem.sentence()
transaction.received = context.received ? context.received : new Date()
transaction.blockchainTypeId = context.blockchainTypeId ? context.blockchainTypeId : 1
if (context.transactionSendCoin) transaction.transactionSendCoin = context.transactionSendCoin
if (context.transactionCreation) transaction.transactionCreation = context.transactionCreation
return transaction
})

View File

@ -0,0 +1,19 @@
import Faker from 'faker'
import { define } from 'typeorm-seeding'
import { UserTransaction } from '../../entity/UserTransaction'
import { UserTransactionContext } from '../interface/TransactionContext'
define(UserTransaction, (faker: typeof Faker, context?: UserTransactionContext) => {
if (!context || !context.userId || !context.transactionId) {
throw new Error('UserTransaction: No userId and/or transactionId present!')
}
const userTransaction = new UserTransaction()
userTransaction.userId = context.userId
userTransaction.transactionId = context.transactionId
userTransaction.transactionTypeId = context.transactionTypeId ? context.transactionTypeId : 1
userTransaction.balance = context.balance ? context.balance : 100000
userTransaction.balanceDate = context.balanceDate ? context.balanceDate : new Date()
return userTransaction
})

View File

@ -9,6 +9,7 @@ import { CreatePeterLustigSeed } from './seeds/users/peter-lustig.admin.seed'
import { CreateBibiBloxbergSeed } from './seeds/users/bibi-bloxberg.seed' import { CreateBibiBloxbergSeed } from './seeds/users/bibi-bloxberg.seed'
import { CreateRaeuberHotzenplotzSeed } from './seeds/users/raeuber-hotzenplotz.seed' import { CreateRaeuberHotzenplotzSeed } from './seeds/users/raeuber-hotzenplotz.seed'
import { CreateBobBaumeisterSeed } from './seeds/users/bob-baumeister.seed' import { CreateBobBaumeisterSeed } from './seeds/users/bob-baumeister.seed'
import { DecayStartBlockSeed } from './seeds/decay-start-block.seed'
const run = async (command: string) => { const run = async (command: string) => {
// Database actions not supported by our migration library // Database actions not supported by our migration library
@ -59,6 +60,7 @@ const run = async (command: string) => {
root: process.cwd(), root: process.cwd(),
configName: 'ormconfig.js', configName: 'ormconfig.js',
}) })
await runSeeder(DecayStartBlockSeed)
await runSeeder(CreatePeterLustigSeed) await runSeeder(CreatePeterLustigSeed)
await runSeeder(CreateBibiBloxbergSeed) await runSeeder(CreateBibiBloxbergSeed)
await runSeeder(CreateRaeuberHotzenplotzSeed) await runSeeder(CreateRaeuberHotzenplotzSeed)

View File

@ -0,0 +1,52 @@
import { Transaction } from '../../entity/Transaction'
import { TransactionSendCoin } from '../../entity/TransactionSendCoin'
import { TransactionCreation } from '../../entity/TransactionCreation'
import { User } from '../../entity/User'
export interface TransactionContext {
transactionTypeId?: number
txHash?: Buffer
memo?: string
received?: Date
blockchainTypeId?: number
transactionSendCoin?: TransactionSendCoin
transactionCreation?: TransactionCreation
}
export interface BalanceContext {
modified?: Date
recordDate?: Date
amount?: number
user?: User
}
export interface TransactionSendCoinContext {
senderPublic?: Buffer
userId?: number
recipiantPublic?: Buffer
recipiantUserId?: number
amount?: number
senderFinalBalance?: number
transaction?: Transaction
}
export interface TransactionCreationContext {
userId?: number
amount?: number
targetDate?: Date
transaction?: Transaction
}
export interface UserTransactionContext {
userId?: number
transactionId?: number
transactionTypeId?: number
balance?: number
balanceDate?: Date
}
export interface TransactionSignatureContext {
signature?: Buffer
pubkey?: Buffer
transaction?: Transaction
}

View File

@ -27,4 +27,14 @@ export interface UserInterface {
modified?: Date modified?: Date
// flag for admin // flag for admin
isAdmin?: boolean isAdmin?: boolean
// flag for balance (creation of 1000 GDD)
addBalance?: boolean
// balance
balanceModified?: Date
recordDate?: Date
targetDate?: Date
amount?: number
creationTxHash?: Buffer
signature?: Buffer
signaturePubkey?: Buffer
} }

View File

@ -0,0 +1,17 @@
import { Factory, Seeder } from 'typeorm-seeding'
import { Transaction } from '../../entity/Transaction'
export class DecayStartBlockSeed implements Seeder {
public async run(factory: Factory): Promise<void> {
await factory(Transaction)({
transactionTypeId: 9,
txHash: Buffer.from(
'9c9c4152b8a4cfbac287eee18d2d262e9de756fae726fc0ca36b788564973fff00000000000000000000000000000000',
'hex',
),
memo: '',
received: new Date('2021-11-30T09:13:26'),
blockchainTypeId: 1,
}).create()
}
}

View File

@ -5,16 +5,28 @@ import {
ServerUserContext, ServerUserContext,
LoginUserRolesContext, LoginUserRolesContext,
} from '../../interface/UserContext' } from '../../interface/UserContext'
import {
BalanceContext,
TransactionContext,
TransactionCreationContext,
UserTransactionContext,
TransactionSignatureContext,
} from '../../interface/TransactionContext'
import { UserInterface } from '../../interface/UserInterface' import { UserInterface } from '../../interface/UserInterface'
import { User } from '../../../entity/User' import { User } from '../../../entity/User'
import { LoginUser } from '../../../entity/LoginUser' import { LoginUser } from '../../../entity/LoginUser'
import { LoginUserBackup } from '../../../entity/LoginUserBackup' import { LoginUserBackup } from '../../../entity/LoginUserBackup'
import { ServerUser } from '../../../entity/ServerUser' import { ServerUser } from '../../../entity/ServerUser'
import { LoginUserRoles } from '../../../entity/LoginUserRoles' import { LoginUserRoles } from '../../../entity/LoginUserRoles'
import { Balance } from '../../../entity/Balance'
import { Transaction } from '../../../entity/Transaction'
import { TransactionSignature } from '../../../entity/TransactionSignature'
import { UserTransaction } from '../../../entity/UserTransaction'
import { TransactionCreation } from '../../../entity/TransactionCreation'
import { Factory } from 'typeorm-seeding' import { Factory } from 'typeorm-seeding'
export const userSeeder = async (factory: Factory, userData: UserInterface): Promise<void> => { export const userSeeder = async (factory: Factory, userData: UserInterface): Promise<void> => {
await factory(User)(createUserContext(userData)).create() const user = await factory(User)(createUserContext(userData)).create()
const loginUser = await factory(LoginUser)(createLoginUserContext(userData)).create() const loginUser = await factory(LoginUser)(createLoginUserContext(userData)).create()
await factory(LoginUserBackup)(createLoginUserBackupContext(userData, loginUser)).create() await factory(LoginUserBackup)(createLoginUserBackupContext(userData, loginUser)).create()
@ -25,9 +37,26 @@ export const userSeeder = async (factory: Factory, userData: UserInterface): Pro
// It works with LoginRoles empty!! // It works with LoginRoles empty!!
await factory(LoginUserRoles)(createLoginUserRolesContext(loginUser)).create() await factory(LoginUserRoles)(createLoginUserRolesContext(loginUser)).create()
} }
if (userData.addBalance) {
// create some GDD for the user
await factory(Balance)(createBalanceContext(userData, user)).create()
const transaction = await factory(Transaction)(
createTransactionContext(userData, 1, 'Herzlich Willkommen bei Gradido!'),
).create()
await factory(TransactionCreation)(
createTransactionCreationContext(userData, user, transaction),
).create()
await factory(UserTransaction)(
createUserTransactionContext(userData, user, transaction),
).create()
await factory(TransactionSignature)(
createTransactionSignatureContext(userData, transaction),
).create()
}
} }
export const createUserContext = (context: UserInterface): UserContext => { const createUserContext = (context: UserInterface): UserContext => {
return { return {
pubkey: context.pubKey, pubkey: context.pubKey,
email: context.email, email: context.email,
@ -38,7 +67,7 @@ export const createUserContext = (context: UserInterface): UserContext => {
} }
} }
export const createLoginUserContext = (context: UserInterface): LoginUserContext => { const createLoginUserContext = (context: UserInterface): LoginUserContext => {
return { return {
email: context.email, email: context.email,
firstName: context.firstName, firstName: context.firstName,
@ -59,7 +88,7 @@ export const createLoginUserContext = (context: UserInterface): LoginUserContext
} }
} }
export const createLoginUserBackupContext = ( const createLoginUserBackupContext = (
context: UserInterface, context: UserInterface,
loginUser: LoginUser, loginUser: LoginUser,
): LoginUserBackupContext => { ): LoginUserBackupContext => {
@ -70,7 +99,7 @@ export const createLoginUserBackupContext = (
} }
} }
export const createServerUserContext = (context: UserInterface): ServerUserContext => { const createServerUserContext = (context: UserInterface): ServerUserContext => {
return { return {
role: context.role, role: context.role,
username: context.username, username: context.username,
@ -83,9 +112,69 @@ export const createServerUserContext = (context: UserInterface): ServerUserConte
} }
} }
export const createLoginUserRolesContext = (loginUser: LoginUser): LoginUserRolesContext => { const createLoginUserRolesContext = (loginUser: LoginUser): LoginUserRolesContext => {
return { return {
userId: loginUser.id, userId: loginUser.id,
roleId: 1, roleId: 1,
} }
} }
const createBalanceContext = (context: UserInterface, user: User): BalanceContext => {
return {
modified: context.balanceModified,
recordDate: context.recordDate,
amount: context.amount,
user,
}
}
const createTransactionContext = (
context: UserInterface,
type: number,
memo: string,
): TransactionContext => {
return {
transactionTypeId: type,
txHash: context.creationTxHash,
memo,
received: context.recordDate,
}
}
const createTransactionCreationContext = (
context: UserInterface,
user: User,
transaction: Transaction,
): TransactionCreationContext => {
return {
userId: user.id,
amount: context.amount,
targetDate: context.targetDate,
transaction,
}
}
const createUserTransactionContext = (
context: UserInterface,
user: User,
transaction: Transaction,
): UserTransactionContext => {
return {
userId: user.id,
transactionId: transaction.id,
transactionTypeId: transaction.transactionTypeId,
balance: context.amount,
balanceDate: context.recordDate,
}
}
const createTransactionSignatureContext = (
context: UserInterface,
transaction: Transaction,
): TransactionSignatureContext => {
return {
signature: context.signature,
pubkey: context.signaturePubkey,
transaction,
}
}

View File

@ -22,4 +22,21 @@ export const bibiBloxberg = {
'knife normal level all hurdle crucial color avoid warrior stadium road bachelor affair topple hawk pottery right afford immune two ceiling budget glance hour ', 'knife normal level all hurdle crucial color avoid warrior stadium road bachelor affair topple hawk pottery right afford immune two ceiling budget glance hour ',
mnemonicType: 2, mnemonicType: 2,
isAdmin: false, isAdmin: false,
addBalance: true,
balanceModified: new Date('2021-11-30T10:37:11'),
recordDate: new Date('2021-11-30T10:37:11'),
targetDate: new Date('2021-08-01 00:00:00'),
amount: 10000000,
creationTxHash: Buffer.from(
'51103dc0fc2ca5d5d75a9557a1e899304e5406cfdb1328d8df6414d527b0118100000000000000000000000000000000',
'hex',
),
signature: Buffer.from(
'2a2c71f3e41adc060bbc3086577e2d57d24eeeb0a7727339c3f85aad813808f601d7e1df56a26e0929d2e67fc054fca429ccfa283ed2782185c7f009fe008f0c',
'hex',
),
signaturePubkey: Buffer.from(
'7281e0ee3258b08801f3ec73e431b4519677f65c03b0382c63a913b5784ee770',
'hex',
),
} }

View File

@ -22,4 +22,21 @@ export const bobBaumeister = {
'detail master source effort unable waste tilt flush domain orchard art truck hint barrel response gate impose peanut secret merry three uncle wink resource ', 'detail master source effort unable waste tilt flush domain orchard art truck hint barrel response gate impose peanut secret merry three uncle wink resource ',
mnemonicType: 2, mnemonicType: 2,
isAdmin: false, isAdmin: false,
addBalance: true,
balanceModified: new Date('2021-11-30T10:37:14'),
recordDate: new Date('2021-11-30T10:37:14'),
targetDate: new Date('2021-08-01 00:00:00'),
amount: 10000000,
creationTxHash: Buffer.from(
'be095dc87acb94987e71168fee8ecbf50ecb43a180b1006e75d573b35725c69c00000000000000000000000000000000',
'hex',
),
signature: Buffer.from(
'1fbd6b9a3d359923b2501557f3bc79fa7e428127c8090fb16bc490b4d87870ab142b3817ddd902d22f0b26472a483233784a0e460c0622661752a13978903905',
'hex',
),
signaturePubkey: Buffer.from(
'7281e0ee3258b08801f3ec73e431b4519677f65c03b0382c63a913b5784ee770',
'hex',
),
} }

View File

@ -22,4 +22,21 @@ export const raeuberHotzenplotz = {
'gospel trip tenant mouse spider skill auto curious man video chief response same little over expire drum display fancy clinic keen throw urge basket ', 'gospel trip tenant mouse spider skill auto curious man video chief response same little over expire drum display fancy clinic keen throw urge basket ',
mnemonicType: 2, mnemonicType: 2,
isAdmin: false, isAdmin: false,
addBalance: true,
balanceModified: new Date('2021-11-30T10:37:13'),
recordDate: new Date('2021-11-30T10:37:13'),
targetDate: new Date('2021-08-01 00:00:00'),
amount: 10000000,
creationTxHash: Buffer.from(
'23ba44fd84deb59b9f32969ad0cb18bfa4588be1bdb99c396888506474c16c1900000000000000000000000000000000',
'hex',
),
signature: Buffer.from(
'756d3da061687c575d1dbc5073908f646aa5f498b0927b217c83b48af471450e571dfe8421fb8e1f1ebd1104526b7e7c6fa78684e2da59c8f7f5a8dc3d9e5b0b',
'hex',
),
signaturePubkey: Buffer.from(
'7281e0ee3258b08801f3ec73e431b4519677f65c03b0382c63a913b5784ee770',
'hex',
),
} }

View File

@ -1,6 +1,7 @@
module.exports = { module.exports = {
presets: ['@babel/preset-env'], presets: ['@babel/preset-env'],
plugins: [ plugins: [
'transform-require-context',
[ [
'component', 'component',
{ {

View File

@ -22,4 +22,5 @@ module.exports = {
testMatch: ['**/?(*.)+(spec|test).js?(x)'], testMatch: ['**/?(*.)+(spec|test).js?(x)'],
// snapshotSerializers: ['jest-serializer-vue'], // snapshotSerializers: ['jest-serializer-vue'],
transformIgnorePatterns: ['<rootDir>/node_modules/(?!vee-validate/dist/rules)'], transformIgnorePatterns: ['<rootDir>/node_modules/(?!vee-validate/dist/rules)'],
testEnvironment: 'jest-environment-jsdom-sixteen',
} }

View File

@ -22,8 +22,9 @@
"apollo-boost": "^0.4.9", "apollo-boost": "^0.4.9",
"axios": "^0.21.1", "axios": "^0.21.1",
"babel-core": "^7.0.0-bridge.0", "babel-core": "^7.0.0-bridge.0",
"babel-jest": "^26.6.3", "babel-jest": "^27.3.1",
"babel-plugin-require-context-hook": "^1.0.0", "babel-plugin-require-context-hook": "^1.0.0",
"babel-plugin-transform-require-context": "^0.1.1",
"babel-preset-vue": "^2.0.2", "babel-preset-vue": "^2.0.2",
"bootstrap": "4.3.1", "bootstrap": "4.3.1",
"bootstrap-vue": "^2.5.0", "bootstrap-vue": "^2.5.0",
@ -51,6 +52,7 @@
"identity-obj-proxy": "^3.0.0", "identity-obj-proxy": "^3.0.0",
"jest": "^26.6.3", "jest": "^26.6.3",
"jest-canvas-mock": "^2.3.1", "jest-canvas-mock": "^2.3.1",
"jest-environment-jsdom-sixteen": "^2.0.0",
"nouislider": "^12.1.0", "nouislider": "^12.1.0",
"particles-bg-vue": "1.2.3", "particles-bg-vue": "1.2.3",
"perfect-scrollbar": "^1.3.0", "perfect-scrollbar": "^1.3.0",

View File

@ -23,6 +23,7 @@
variant="outline-light" variant="outline-light"
@click="toggleShowPassword" @click="toggleShowPassword"
class="border-left-0 rounded-right" class="border-left-0 rounded-right"
tabindex="-1"
> >
<b-icon :icon="showPassword ? 'eye' : 'eye-slash'" /> <b-icon :icon="showPassword ? 'eye' : 'eye-slash'" />
</b-button> </b-button>

View File

@ -0,0 +1,200 @@
import { mount, RouterLinkStub } from '@vue/test-utils'
import SideBar from './SideBar'
const localVue = global.localVue
const storeDispatchMock = jest.fn()
describe('SideBar', () => {
let wrapper
const stubs = {
RouterLink: RouterLinkStub,
}
const propsData = {
balance: 1234.56,
}
const mocks = {
$store: {
state: {
email: 'test@example.org',
publisherId: 123,
firstName: 'test',
lastName: 'example',
hasElopage: false,
},
dispatch: storeDispatchMock,
},
$i18n: {
locale: 'en',
},
$t: jest.fn((t) => t),
$n: jest.fn((n) => n),
}
const Wrapper = () => {
return mount(SideBar, { localVue, mocks, stubs, propsData })
}
describe('mount', () => {
beforeEach(() => {
wrapper = Wrapper()
})
it('renders the component', () => {
expect(wrapper.find('#sidenav-main').exists()).toBeTruthy()
})
describe('navbar button', () => {
it('has a navbar button', () => {
expect(wrapper.find('button.navbar-toggler').exists()).toBeTruthy()
})
it('calls showSidebar when clicked', async () => {
const spy = jest.spyOn(wrapper.vm.$sidebar, 'displaySidebar')
wrapper.find('button.navbar-toggler').trigger('click')
await wrapper.vm.$nextTick()
expect(spy).toHaveBeenCalledWith(true)
})
})
describe('balance', () => {
it('shows em-dash as balance while loading', () => {
expect(wrapper.find('div.row.text-center').text()).toBe('— GDD')
})
it('shows the when loaded', async () => {
wrapper.setProps({
pending: false,
})
await wrapper.vm.$nextTick()
expect(wrapper.find('div.row.text-center').text()).toBe('1234.56 GDD')
})
})
describe('close siedbar', () => {
it('calls closeSidebar when clicked', async () => {
const spy = jest.spyOn(wrapper.vm.$sidebar, 'displaySidebar')
wrapper.find('#sidenav-collapse-main').find('button.navbar-toggler').trigger('click')
await wrapper.vm.$nextTick()
expect(spy).toHaveBeenCalledWith(false)
})
})
describe('static menu items', () => {
describe("member's area without publisher ID", () => {
it('has a link to the elopage', () => {
expect(wrapper.findAll('li').at(0).text()).toContain('members_area')
})
it('has a badge', () => {
expect(wrapper.findAll('li').at(0).text()).toContain('!')
})
it('links to the elopage registration', () => {
expect(wrapper.findAll('li').at(0).find('a').attributes('href')).toBe(
'https://elopage.com/s/gradido/basic-de/payment?locale=en&prid=111&pid=123&firstName=test&lastName=example&email=test@example.org',
)
})
describe('with locale="de"', () => {
beforeEach(() => {
mocks.$i18n.locale = 'de'
})
it('links to the German elopage registration when locale is set to de', () => {
expect(wrapper.findAll('li').at(0).find('a').attributes('href')).toBe(
'https://elopage.com/s/gradido/basic-de/payment?locale=de&prid=111&pid=123&firstName=test&lastName=example&email=test@example.org',
)
})
})
describe("member's area with publisher ID", () => {
beforeEach(() => {
mocks.$store.state.hasElopage = true
})
it('links to the elopage member area', () => {
expect(wrapper.findAll('li').at(0).find('a').attributes('href')).toBe(
'https://elopage.com/s/gradido/sign_in?locale=de',
)
})
it('has no badge', () => {
expect(wrapper.findAll('li').at(0).text()).not.toContain('!')
})
})
describe("member's area with default publisher ID and no elopage", () => {
beforeEach(() => {
mocks.$store.state.publisherId = null
mocks.$store.state.hasElopage = false
})
it('links to the elopage member area with default publisher ID', () => {
expect(wrapper.findAll('li').at(0).find('a').attributes('href')).toBe(
'https://elopage.com/s/gradido/basic-de/payment?locale=de&prid=111&pid=2896&firstName=test&lastName=example&email=test@example.org',
)
})
it('has a badge', () => {
expect(wrapper.findAll('li').at(0).text()).toContain('!')
})
})
})
describe('logout', () => {
it('has a logout button', () => {
expect(wrapper.findAll('li').at(1).text()).toBe('logout')
})
it('emits logout when logout is clicked', async () => {
wrapper.findAll('li').at(1).find('a').trigger('click')
await wrapper.vm.$nextTick()
expect(wrapper.emitted('logout')).toEqual([[]])
})
})
describe('admin-area', () => {
it('is not visible when not an admin', () => {
expect(wrapper.findAll('li').at(1).text()).not.toBe('admin_area')
})
describe('logged in as admin', () => {
const assignLocationSpy = jest.fn()
beforeEach(async () => {
mocks.$store.state.isAdmin = true
mocks.$store.state.token = 'valid-token'
delete window.location
window.location = {
assign: assignLocationSpy,
}
wrapper = Wrapper()
})
it('is visible', () => {
expect(wrapper.findAll('li').at(1).text()).toBe('admin_area')
})
describe('click on admin area', () => {
beforeEach(async () => {
await wrapper.findAll('li').at(1).find('a').trigger('click')
})
it('opens a new window when clicked', () => {
expect(assignLocationSpy).toHaveBeenCalledWith(
'http://localhost/admin/authenticate?token=valid-token',
)
})
it('dispatches logout to store', () => {
expect(storeDispatchMock).toHaveBeenCalledWith('logout')
})
})
})
})
})
})
})

View File

@ -12,9 +12,9 @@ export const unsubscribeNewsletter = gql`
} }
` `
export const resetPassword = gql` export const setPassword = gql`
mutation($sessionId: Float!, $email: String!, $password: String!) { mutation($code: String!, $password: String!) {
resetPassword(sessionId: $sessionId, email: $email, password: $password) setPassword(code: $code, password: $password)
} }
` `
@ -42,12 +42,11 @@ export const updateUserInfos = gql`
} }
` `
export const registerUser = gql` export const createUser = gql`
mutation( mutation(
$firstName: String! $firstName: String!
$lastName: String! $lastName: String!
$email: String! $email: String!
$password: String!
$language: String! $language: String!
$publisherId: Int $publisherId: Int
) { ) {
@ -55,7 +54,6 @@ export const registerUser = gql`
email: $email email: $email
firstName: $firstName firstName: $firstName
lastName: $lastName lastName: $lastName
password: $password
language: $language language: $language
publisherId: $publisherId publisherId: $publisherId
) )

View File

@ -46,15 +46,6 @@ export const logout = gql`
} }
` `
export const loginViaEmailVerificationCode = gql`
query($optin: String!) {
loginViaEmailVerificationCode(optin: $optin) {
sessionId
email
}
}
`
export const transactionsQuery = gql` export const transactionsQuery = gql`
query($currentPage: Int = 1, $pageSize: Int = 25, $order: Order = DESC) { query($currentPage: Int = 1, $pageSize: Int = 25, $order: Order = DESC) {
transactionList(currentPage: $currentPage, pageSize: $pageSize, order: $order) { transactionList(currentPage: $currentPage, pageSize: $pageSize, order: $order) {
@ -88,9 +79,7 @@ export const transactionsQuery = gql`
export const sendResetPasswordEmail = gql` export const sendResetPasswordEmail = gql`
query($email: String!) { query($email: String!) {
sendResetPasswordEmail(email: $email) { sendResetPasswordEmail(email: $email)
state
}
} }
` `
@ -118,15 +107,6 @@ export const listGDTEntriesQuery = gql`
} }
` `
export const checkEmailQuery = gql`
query($optin: String!) {
checkEmail(optin: $optin) {
email
sessionId
}
}
`
export const communityInfo = gql` export const communityInfo = gql`
query { query {
getCommunityInfo { getCommunityInfo {

View File

@ -48,7 +48,7 @@
"error": "Fehler", "error": "Fehler",
"no-account": "Leider konnten wir keinen Account finden mit diesen Daten!", "no-account": "Leider konnten wir keinen Account finden mit diesen Daten!",
"no-email-verify": "Die Email wurde noch nicht bestätigt, bitte überprüfe deine Emails und klicke auf den Aktivierungslink!", "no-email-verify": "Die Email wurde noch nicht bestätigt, bitte überprüfe deine Emails und klicke auf den Aktivierungslink!",
"session-expired": "Sitzung abgelaufen!" "session-expired": "Die Sitzung wurde aus Sicherheitsgründen beendet."
}, },
"form": { "form": {
"amount": "Betrag", "amount": "Betrag",
@ -145,12 +145,17 @@
"password": { "password": {
"change-password": "Passwort ändern", "change-password": "Passwort ändern",
"forgot_pwd": "Passwort vergessen?", "forgot_pwd": "Passwort vergessen?",
"not-authenticated": "Leider konnten wir dich nicht authentifizieren. Bitte wende dich an den Support.",
"resend_subtitle": "Dein Aktivierungslink ist abgelaufen, Du kannst hier ein neuen anfordern.",
"reset": "Passwort zurücksetzen", "reset": "Passwort zurücksetzen",
"reset-password": { "reset-password": {
"not-authenticated": "Leider konnten wir dich nicht authentifizieren. Bitte wende dich an den Support.",
"text": "Jetzt kannst du ein neues Passwort speichern, mit dem du dich zukünftig in der Gradido-App anmelden kannst." "text": "Jetzt kannst du ein neues Passwort speichern, mit dem du dich zukünftig in der Gradido-App anmelden kannst."
}, },
"send_now": "Jetzt senden", "send_now": "Jetzt senden",
"set": "Passwort festlegen",
"set-password": {
"text": "Jetzt kannst du ein neues Passwort speichern, mit dem du dich zukünftig in der Gradido-App anmelden kannst."
},
"subtitle": "Wenn du dein Passwort vergessen hast, kannst du es hier zurücksetzen." "subtitle": "Wenn du dein Passwort vergessen hast, kannst du es hier zurücksetzen."
} }
}, },

View File

@ -48,7 +48,7 @@
"error": "Error", "error": "Error",
"no-account": "Unfortunately we could not find an account to the given data!", "no-account": "Unfortunately we could not find an account to the given data!",
"no-email-verify": "Your email is not activated yet, please check your emails and click the activation link!", "no-email-verify": "Your email is not activated yet, please check your emails and click the activation link!",
"session-expired": "The session expired" "session-expired": "The session was closed for security reasons."
}, },
"form": { "form": {
"amount": "Amount", "amount": "Amount",
@ -145,12 +145,17 @@
"password": { "password": {
"change-password": "Change password", "change-password": "Change password",
"forgot_pwd": "Forgot password?", "forgot_pwd": "Forgot password?",
"not-authenticated": "Unfortunately we could not authenticate you. Please contact the support.",
"resend_subtitle": "Your activation link is expired, here you can order a new one.",
"reset": "Reset password", "reset": "Reset password",
"reset-password": { "reset-password": {
"not-authenticated": "Unfortunately we could not authenticate you. Please contact the support.",
"text": "Now you can save a new password to login to the Gradido-App in the future." "text": "Now you can save a new password to login to the Gradido-App in the future."
}, },
"send_now": "Send now", "send_now": "Send now",
"set": "Set password",
"set-password": {
"text": "Now you can save a new password to login to the Gradido-App in the future."
},
"subtitle": "If you have forgotten your password, you can reset it here." "subtitle": "If you have forgotten your password, you can reset it here."
} }
}, },

View File

@ -3,9 +3,6 @@ import DashboardPlugin from './plugins/dashboard-plugin'
import App from './App.vue' import App from './App.vue'
import i18n from './i18n.js' import i18n from './i18n.js'
import { loadAllRules } from './validation-rules' import { loadAllRules } from './validation-rules'
import { ApolloClient, ApolloLink, InMemoryCache, HttpLink } from 'apollo-boost'
import VueApollo from 'vue-apollo'
import CONFIG from './config'
import addNavigationGuards from './routes/guards' import addNavigationGuards from './routes/guards'
@ -13,42 +10,22 @@ import { store } from './store/store'
import router from './routes/router' import router from './routes/router'
const httpLink = new HttpLink({ uri: CONFIG.GRAPHQL_URI }) import { apolloProvider } from './plugins/apolloProvider'
const authLink = new ApolloLink((operation, forward) => {
const token = store.state.token
operation.setContext({
headers: {
Authorization: token && token.length > 0 ? `Bearer ${token}` : '',
},
})
return forward(operation).map((response) => {
if (response.errors && response.errors[0].message === '403.13 - Client certificate revoked') {
response.errors[0].message = i18n.t('error.session-expired')
store.dispatch('logout', null)
if (router.currentRoute.path !== '/login') router.push('/login')
return response
}
const newToken = operation.getContext().response.headers.get('token')
if (newToken) store.commit('token', newToken)
return response
})
})
const apolloClient = new ApolloClient({
link: authLink.concat(httpLink),
cache: new InMemoryCache(),
uri: CONFIG.GRAPHQL_URI,
})
const apolloProvider = new VueApollo({
defaultClient: apolloClient,
})
// plugin setup // plugin setup
Vue.use(DashboardPlugin) Vue.use(DashboardPlugin)
Vue.config.productionTip = false Vue.config.productionTip = false
Vue.toasted.register(
'error',
(payload) => {
return payload.replace(/^GraphQL error: /, '')
},
{
type: 'error',
},
)
loadAllRules(i18n) loadAllRules(i18n)
addNavigationGuards(router, store, apolloProvider.defaultClient) addNavigationGuards(router, store, apolloProvider.defaultClient)

View File

@ -13,7 +13,7 @@ export const getCommunityInfoMixin = {
return result.data.getCommunityInfo return result.data.getCommunityInfo
}) })
.catch((error) => { .catch((error) => {
this.$toasted.error(error.message) this.$toasted.global.error(error.message)
}) })
} }
}, },

View File

@ -0,0 +1,37 @@
import { ApolloClient, ApolloLink, InMemoryCache, HttpLink } from 'apollo-boost'
import VueApollo from 'vue-apollo'
import CONFIG from '../config'
import { store } from '../store/store'
import router from '../routes/router'
import i18n from '../i18n'
const httpLink = new HttpLink({ uri: CONFIG.GRAPHQL_URI })
const authLink = new ApolloLink((operation, forward) => {
const token = store.state.token
operation.setContext({
headers: {
Authorization: token && token.length > 0 ? `Bearer ${token}` : '',
},
})
return forward(operation).map((response) => {
if (response.errors && response.errors[0].message === '403.13 - Client certificate revoked') {
response.errors[0].message = i18n.t('error.session-expired')
store.dispatch('logout', null)
if (router.currentRoute.path !== '/login') router.push('/login')
return response
}
const newToken = operation.getContext().response.headers.get('token')
if (newToken) store.commit('token', newToken)
return response
})
})
const apolloClient = new ApolloClient({
link: authLink.concat(httpLink),
cache: new InMemoryCache(),
})
export const apolloProvider = new VueApollo({
defaultClient: apolloClient,
})

View File

@ -0,0 +1,178 @@
import { ApolloClient, ApolloLink, HttpLink } from 'apollo-boost'
import './apolloProvider'
import CONFIG from '../config'
import VueApollo from 'vue-apollo'
import { store } from '../store/store.js'
import router from '../routes/router'
import i18n from '../i18n'
jest.mock('vue-apollo')
jest.mock('../store/store')
jest.mock('../routes/router')
jest.mock('../i18n')
jest.mock('apollo-boost', () => {
return {
__esModule: true,
ApolloClient: jest.fn(),
ApolloLink: jest.fn(() => {
return { concat: jest.fn() }
}),
InMemoryCache: jest.fn(),
HttpLink: jest.fn(),
}
})
describe('apolloProvider', () => {
it('calls the HttpLink', () => {
expect(HttpLink).toBeCalledWith({ uri: CONFIG.GRAPHQL_URI })
})
it('calls the ApolloLink', () => {
expect(ApolloLink).toBeCalled()
})
it('calls the ApolloClient', () => {
expect(ApolloClient).toBeCalled()
})
it('calls the VueApollo', () => {
expect(VueApollo).toBeCalled()
})
describe('ApolloLink', () => {
// mock store
const storeDispatchMock = jest.fn()
const storeCommitMock = jest.fn()
store.state = {
token: 'some-token',
}
store.dispatch = storeDispatchMock
store.commit = storeCommitMock
// mock i18n.t
i18n.t = jest.fn((t) => t)
// mock apllo response
const responseMock = {
errors: [{ message: '403.13 - Client certificate revoked' }],
}
// mock router
const routerPushMock = jest.fn()
router.push = routerPushMock
router.currentRoute = {
path: '/overview',
}
// mock context
const setContextMock = jest.fn()
const getContextMock = jest.fn(() => {
return {
response: {
headers: {
get: jest.fn(() => 'another-token'),
},
},
}
})
// mock apollo link function params
const operationMock = {
setContext: setContextMock,
getContext: getContextMock,
}
const forwardMock = jest.fn(() => {
return [responseMock]
})
// get apollo link callback
const middleware = ApolloLink.mock.calls[0][0]
describe('with token in store', () => {
it('sets authorization header with token', () => {
// run the apollo link callback with mocked params
middleware(operationMock, forwardMock)
expect(setContextMock).toBeCalledWith({
headers: {
Authorization: 'Bearer some-token',
},
})
})
})
describe('without token in store', () => {
beforeEach(() => {
store.state.token = null
})
it('sets authorization header empty', () => {
middleware(operationMock, forwardMock)
expect(setContextMock).toBeCalledWith({
headers: {
Authorization: '',
},
})
})
})
describe('apollo response is 403.13', () => {
beforeEach(() => {
// run the apollo link callback with mocked params
middleware(operationMock, forwardMock)
})
it('dispatches logout', () => {
expect(storeDispatchMock).toBeCalledWith('logout', null)
})
describe('current route is not login', () => {
it('redirects to logout', () => {
expect(routerPushMock).toBeCalledWith('/login')
})
})
describe('current route is login', () => {
beforeEach(() => {
jest.clearAllMocks()
router.currentRoute.path = '/login'
})
it('does not redirect to login', () => {
expect(routerPushMock).not.toBeCalled()
})
})
})
describe('apollo response is with new token', () => {
beforeEach(() => {
delete responseMock.errors
middleware(operationMock, forwardMock)
})
it('commits new token to store', () => {
expect(storeCommitMock).toBeCalledWith('token', 'another-token')
})
})
describe('apollo response is without new token', () => {
beforeEach(() => {
jest.clearAllMocks()
getContextMock.mockReturnValue({
response: {
headers: {
get: jest.fn(() => null),
},
},
})
middleware(operationMock, forwardMock)
})
it('does not commit token to store', () => {
expect(storeCommitMock).not.toBeCalled()
})
})
})
})

View File

@ -4,8 +4,11 @@ import Vue from 'vue'
import GlobalComponents from './globalComponents' import GlobalComponents from './globalComponents'
import GlobalDirectives from './globalDirectives' import GlobalDirectives from './globalDirectives'
import Toasted from 'vue-toasted'
jest.mock('./globalComponents') jest.mock('./globalComponents')
jest.mock('./globalDirectives') jest.mock('./globalDirectives')
jest.mock('vue-toasted')
jest.mock('vue') jest.mock('vue')
@ -22,4 +25,21 @@ describe('dashboard plugin', () => {
it('installs the global directives', () => { it('installs the global directives', () => {
expect(vueUseMock).toBeCalledWith(GlobalDirectives) expect(vueUseMock).toBeCalledWith(GlobalDirectives)
}) })
describe('vue toasted', () => {
const toastedAction = vueUseMock.mock.calls[11][1].action.onClick
const goAwayMock = jest.fn()
const toastObject = {
goAway: goAwayMock,
}
it('installs vue toasted', () => {
expect(vueUseMock).toBeCalledWith(Toasted, expect.anything())
})
it('onClick calls goAway(0)', () => {
toastedAction({}, toastObject)
expect(goAwayMock).toBeCalledWith(0)
})
})
}) })

View File

@ -49,8 +49,8 @@ describe('router', () => {
expect(routes.find((r) => r.path === '/').redirect()).toEqual({ path: '/login' }) expect(routes.find((r) => r.path === '/').redirect()).toEqual({ path: '/login' })
}) })
it('has fifteen routes defined', () => { it('has sixteen routes defined', () => {
expect(routes).toHaveLength(15) expect(routes).toHaveLength(16)
}) })
describe('overview', () => { describe('overview', () => {
@ -143,6 +143,13 @@ describe('router', () => {
}) })
}) })
describe('password with param comingFrom', () => {
it('loads the "Password" component', async () => {
const component = await routes.find((r) => r.path === '/password/:comingFrom').component()
expect(component.default.name).toBe('password')
})
})
describe('register-community', () => { describe('register-community', () => {
it('loads the "registerCommunity" component', async () => { it('loads the "registerCommunity" component', async () => {
const component = await routes.find((r) => r.path === '/register-community').component() const component = await routes.find((r) => r.path === '/register-community').component()
@ -167,7 +174,7 @@ describe('router', () => {
describe('checkEmail', () => { describe('checkEmail', () => {
it('loads the "CheckEmail" component', async () => { it('loads the "CheckEmail" component', async () => {
const component = await routes.find((r) => r.path === '/checkEmail/:optin').component() const component = await routes.find((r) => r.path === '/checkEmail/:optin').component()
expect(component.default.name).toBe('CheckEmail') expect(component.default.name).toBe('ResetPassword')
}) })
}) })

View File

@ -50,7 +50,7 @@ const routes = [
path: '/thx/:comingFrom', path: '/thx/:comingFrom',
component: () => import('../views/Pages/thx.vue'), component: () => import('../views/Pages/thx.vue'),
beforeEnter: (to, from, next) => { beforeEnter: (to, from, next) => {
const validFrom = ['password', 'reset', 'register', 'login'] const validFrom = ['password', 'reset', 'register', 'login', 'Login']
if (!validFrom.includes(from.path.split('/')[1])) { if (!validFrom.includes(from.path.split('/')[1])) {
next({ path: '/login' }) next({ path: '/login' })
} else { } else {
@ -62,6 +62,10 @@ const routes = [
path: '/password', path: '/password',
component: () => import('../views/Pages/ForgotPassword.vue'), component: () => import('../views/Pages/ForgotPassword.vue'),
}, },
{
path: '/password/:comingFrom',
component: () => import('../views/Pages/ForgotPassword.vue'),
},
{ {
path: '/register-community', path: '/register-community',
component: () => import('../views/Pages/RegisterCommunity.vue'), component: () => import('../views/Pages/RegisterCommunity.vue'),
@ -76,7 +80,7 @@ const routes = [
}, },
{ {
path: '/checkEmail/:optin', path: '/checkEmail/:optin',
component: () => import('../views/Pages/CheckEmail.vue'), component: () => import('../views/Pages/ResetPassword.vue'),
}, },
{ path: '*', component: NotFound }, { path: '*', component: NotFound },
] ]

View File

@ -39,7 +39,9 @@ describe('DashboardLayoutGdd', () => {
}, },
}, },
$toasted: { $toasted: {
error: toasterMock, global: {
error: toasterMock,
},
}, },
$apollo: { $apollo: {
query: apolloMock, query: apolloMock,
@ -216,7 +218,7 @@ describe('DashboardLayoutGdd', () => {
expect(wrapper.vm.pending).toBeTruthy() expect(wrapper.vm.pending).toBeTruthy()
}) })
it('calls $toasted.error method', () => { it('calls $toasted.global.error method', () => {
expect(toasterMock).toBeCalledWith('Ouch!') expect(toasterMock).toBeCalledWith('Ouch!')
}) })
}) })

View File

@ -101,7 +101,7 @@ export default {
}) })
.catch((error) => { .catch((error) => {
this.pending = true this.pending = true
this.$toasted.error(error.message) this.$toasted.global.error(error.message)
// what to do when loading balance fails? // what to do when loading balance fails?
}) })
}, },

View File

@ -37,7 +37,9 @@ describe('GdtTransactionList ', () => {
$n: jest.fn((n) => n), $n: jest.fn((n) => n),
$d: jest.fn((d) => d), $d: jest.fn((d) => d),
$toasted: { $toasted: {
error: toastErrorMock, global: {
error: toastErrorMock,
},
}, },
$apollo: { $apollo: {
query: apolloMock, query: apolloMock,

View File

@ -71,7 +71,7 @@ export default {
window.scrollTo(0, 0) window.scrollTo(0, 0)
}) })
.catch((error) => { .catch((error) => {
this.$toasted.error(error.message) this.$toasted.global.error(error.message)
}) })
}, },
}, },

View File

@ -1,105 +0,0 @@
import { mount, RouterLinkStub } from '@vue/test-utils'
import CheckEmail from './CheckEmail'
const localVue = global.localVue
const apolloQueryMock = jest.fn().mockRejectedValue({ message: 'error' })
const toasterMock = jest.fn()
const routerPushMock = jest.fn()
describe('CheckEmail', () => {
let wrapper
const mocks = {
$i18n: {
locale: 'en',
},
$t: jest.fn((t) => t),
$route: {
params: {
optin: '123',
},
},
$toasted: {
error: toasterMock,
},
$router: {
push: routerPushMock,
},
$loading: {
show: jest.fn(() => {
return { hide: jest.fn() }
}),
},
$apollo: {
query: apolloQueryMock,
},
}
const stubs = {
RouterLink: RouterLinkStub,
}
const Wrapper = () => {
return mount(CheckEmail, { localVue, mocks, stubs })
}
describe('mount', () => {
beforeEach(() => {
wrapper = Wrapper()
})
it('calls the checkEmail when created', async () => {
expect(apolloQueryMock).toBeCalledWith(
expect.objectContaining({ variables: { optin: '123' } }),
)
})
describe('No valid optin', () => {
it('toasts an error when no valid optin is given', () => {
expect(toasterMock).toHaveBeenCalledWith('error')
})
it('has a message suggesting to contact the support', () => {
expect(wrapper.find('div.header').text()).toContain('checkEmail.title')
expect(wrapper.find('div.header').text()).toContain('checkEmail.errorText')
})
})
describe('is authenticated', () => {
beforeEach(() => {
apolloQueryMock.mockResolvedValue({
data: {
checkEmail: {
sessionId: 1,
email: 'user@example.org',
language: 'de',
},
},
})
})
it.skip('Has sessionId from API call', async () => {
await wrapper.vm.$nextTick()
expect(wrapper.vm.sessionId).toBe(1)
})
describe('Register header', () => {
it('has a welcome message', async () => {
expect(wrapper.find('div.header').text()).toContain('checkEmail.title')
})
})
describe('links', () => {
it('has a link "Back"', async () => {
expect(wrapper.findAllComponents(RouterLinkStub).at(0).text()).toEqual('back')
})
it('links to /login when clicking "Back"', async () => {
expect(wrapper.findAllComponents(RouterLinkStub).at(0).props().to).toBe('/Login')
})
})
})
})
})

View File

@ -1,72 +0,0 @@
<template>
<div class="checkemail-form">
<b-container>
<div class="header p-4" ref="header">
<div class="header-body text-center mb-7">
<b-row class="justify-content-center">
<b-col xl="5" lg="6" md="8" class="px-2">
<h1>{{ $t('site.checkEmail.title') }}</h1>
<div class="pb-4" v-if="!pending">
<span v-if="!authenticated">
{{ $t('site.checkEmail.errorText') }}
</span>
</div>
</b-col>
</b-row>
</div>
</div>
</b-container>
<b-container class="mt--8 p-1">
<b-row>
<b-col class="text-center py-lg-4">
<router-link to="/Login" class="mt-3">{{ $t('back') }}</router-link>
</b-col>
</b-row>
</b-container>
</div>
</template>
<script>
import { checkEmailQuery } from '../../graphql/queries'
export default {
name: 'CheckEmail',
data() {
return {
authenticated: false,
sessionId: null,
email: null,
pending: true,
}
},
methods: {
async authenticate() {
const loader = this.$loading.show({
container: this.$refs.header,
})
const optin = this.$route.params.optin
this.$apollo
.query({
query: checkEmailQuery,
variables: {
optin: optin,
},
})
.then((result) => {
this.authenticated = true
this.sessionId = result.data.checkEmail.sessionId
this.email = result.data.checkEmail.email
this.$router.push('/thx/checkEmail')
})
.catch((error) => {
this.$toasted.error(error.message)
})
loader.hide()
this.pending = false
},
},
mounted() {
this.authenticate()
},
}
</script>
<style></style>

View File

@ -8,30 +8,41 @@ const localVue = global.localVue
const mockRouterPush = jest.fn() const mockRouterPush = jest.fn()
const stubs = {
RouterLink: RouterLinkStub,
}
const createMockObject = (comingFrom) => {
return {
localVue,
mocks: {
$t: jest.fn((t) => t),
$router: {
push: mockRouterPush,
},
$apollo: {
query: mockAPIcall,
},
$route: {
params: {
comingFrom,
},
},
},
stubs,
}
}
describe('ForgotPassword', () => { describe('ForgotPassword', () => {
let wrapper let wrapper
const mocks = { const Wrapper = (functionN) => {
$t: jest.fn((t) => t), return mount(ForgotPassword, functionN)
$router: {
push: mockRouterPush,
},
$apollo: {
query: mockAPIcall,
},
}
const stubs = {
RouterLink: RouterLinkStub,
}
const Wrapper = () => {
return mount(ForgotPassword, { localVue, mocks, stubs })
} }
describe('mount', () => { describe('mount', () => {
beforeEach(() => { beforeEach(() => {
wrapper = Wrapper() wrapper = Wrapper(createMockObject())
}) })
it('renders the component', () => { it('renders the component', () => {
@ -144,5 +155,15 @@ describe('ForgotPassword', () => {
}) })
}) })
}) })
describe('comingFrom login', () => {
beforeEach(() => {
wrapper = Wrapper(createMockObject('reset'))
})
it('has another subtitle', () => {
expect(wrapper.find('p.text-lead').text()).toEqual('settings.password.resend_subtitle')
})
})
}) })
}) })

View File

@ -5,8 +5,8 @@
<div class="header-body text-center mb-7"> <div class="header-body text-center mb-7">
<b-row class="justify-content-center"> <b-row class="justify-content-center">
<b-col xl="5" lg="6" md="8" class="px-2"> <b-col xl="5" lg="6" md="8" class="px-2">
<h1>{{ $t('settings.password.reset') }}</h1> <h1>{{ $t(displaySetup.headline) }}</h1>
<p class="text-lead">{{ $t('settings.password.subtitle') }}</p> <p class="text-lead">{{ $t(displaySetup.subtitle) }}</p>
</b-col> </b-col>
</b-row> </b-row>
</div> </div>
@ -22,7 +22,7 @@
<input-email v-model="form.email"></input-email> <input-email v-model="form.email"></input-email>
<div class="text-center"> <div class="text-center">
<b-button type="submit" variant="primary"> <b-button type="submit" variant="primary">
{{ $t('settings.password.send_now') }} {{ $t(displaySetup.button) }}
</b-button> </b-button>
</div> </div>
</b-form> </b-form>
@ -41,6 +41,21 @@
import { sendResetPasswordEmail } from '../../graphql/queries' import { sendResetPasswordEmail } from '../../graphql/queries'
import InputEmail from '../../components/Inputs/InputEmail' import InputEmail from '../../components/Inputs/InputEmail'
const textFields = {
reset: {
headline: 'settings.password.reset',
subtitle: 'settings.password.resend_subtitle',
button: 'settings.password.send_now',
cancel: 'back',
},
login: {
headline: 'settings.password.reset',
subtitle: 'settings.password.subtitle',
button: 'settings.password.send_now',
cancel: 'back',
},
}
export default { export default {
name: 'password', name: 'password',
components: { components: {
@ -52,6 +67,7 @@ export default {
form: { form: {
email: '', email: '',
}, },
displaySetup: {},
} }
}, },
methods: { methods: {
@ -71,6 +87,13 @@ export default {
}) })
}, },
}, },
created() {
if (this.$route.params.comingFrom) {
this.displaySetup = textFields[this.$route.params.comingFrom]
} else {
this.displaySetup = textFields.login
}
},
} }
</script> </script>
<style></style> <style></style>

View File

@ -52,7 +52,9 @@ describe('Login', () => {
push: mockRouterPush, push: mockRouterPush,
}, },
$toasted: { $toasted: {
error: toastErrorMock, global: {
error: toastErrorMock,
},
}, },
$apollo: { $apollo: {
query: apolloQueryMock, query: apolloQueryMock,
@ -238,7 +240,7 @@ describe('Login', () => {
describe('login fails', () => { describe('login fails', () => {
beforeEach(() => { beforeEach(() => {
apolloQueryMock.mockRejectedValue({ apolloQueryMock.mockRejectedValue({
message: 'Ouch!', message: '..No user with this credentials',
}) })
}) })

View File

@ -105,11 +105,11 @@ export default {
loader.hide() loader.hide()
}) })
.catch((error) => { .catch((error) => {
if (!error.message.includes('user email not validated')) { if (error.message.includes('No user with this credentials')) {
this.$toasted.error(this.$t('error.no-account')) this.$toasted.global.error(this.$t('error.no-account'))
} else { } else {
// : this.$t('error.no-email-verify') // : this.$t('error.no-email-verify')
this.$router.push('/thx/login') this.$router.push('/reset/login')
} }
loader.hide() loader.hide()
}) })

View File

@ -49,7 +49,9 @@ describe('Register', () => {
}, },
}, },
$toasted: { $toasted: {
error: toastErrorMock, global: {
error: toastErrorMock,
},
}, },
} }
@ -151,16 +153,10 @@ describe('Register', () => {
expect(wrapper.find('#Email-input-field').exists()).toBeTruthy() expect(wrapper.find('#Email-input-field').exists()).toBeTruthy()
}) })
it('has password input fields', () => {
expect(wrapper.find('input[name="form.password"]').exists()).toBeTruthy()
})
it('has password repeat input fields', () => {
expect(wrapper.find('input[name="form.passwordRepeat"]').exists()).toBeTruthy()
})
it('has Language selected field', () => { it('has Language selected field', () => {
expect(wrapper.find('.selectedLanguage').exists()).toBeTruthy() expect(wrapper.find('.selectedLanguage').exists()).toBeTruthy()
}) })
it('selects Language value en', async () => { it('selects Language value en', async () => {
wrapper.find('.selectedLanguage').findAll('option').at(1).setSelected() wrapper.find('.selectedLanguage').findAll('option').at(1).setSelected()
expect(wrapper.find('.selectedLanguage').element.value).toBe('en') expect(wrapper.find('.selectedLanguage').element.value).toBe('en')
@ -223,8 +219,6 @@ describe('Register', () => {
wrapper.find('#registerFirstname').setValue('Max') wrapper.find('#registerFirstname').setValue('Max')
wrapper.find('#registerLastname').setValue('Mustermann') wrapper.find('#registerLastname').setValue('Mustermann')
wrapper.find('#Email-input-field').setValue('max.mustermann@gradido.net') wrapper.find('#Email-input-field').setValue('max.mustermann@gradido.net')
wrapper.find('input[name="form.password"]').setValue('Aa123456_')
wrapper.find('input[name="form.passwordRepeat"]').setValue('Aa123456_')
wrapper.find('.language-switch-select').findAll('option').at(1).setSelected() wrapper.find('.language-switch-select').findAll('option').at(1).setSelected()
wrapper.find('#publisherid').setValue('12345') wrapper.find('#publisherid').setValue('12345')
}) })
@ -280,7 +274,6 @@ describe('Register', () => {
email: 'max.mustermann@gradido.net', email: 'max.mustermann@gradido.net',
firstName: 'Max', firstName: 'Max',
lastName: 'Mustermann', lastName: 'Mustermann',
password: 'Aa123456_',
language: 'en', language: 'en',
publisherId: 12345, publisherId: 12345,
}, },

View File

@ -85,10 +85,6 @@
<input-email v-model="form.email"></input-email> <input-email v-model="form.email"></input-email>
<hr /> <hr />
<input-password-confirmation
v-model="form.password"
:register="register"
></input-password-confirmation>
<b-row> <b-row>
<b-col cols="12"> <b-col cols="12">
@ -194,14 +190,13 @@
</template> </template>
<script> <script>
import InputEmail from '../../components/Inputs/InputEmail.vue' import InputEmail from '../../components/Inputs/InputEmail.vue'
import InputPasswordConfirmation from '../../components/Inputs/InputPasswordConfirmation.vue'
import LanguageSwitchSelect from '../../components/LanguageSwitchSelect.vue' import LanguageSwitchSelect from '../../components/LanguageSwitchSelect.vue'
import { registerUser } from '../../graphql/mutations' import { createUser } from '../../graphql/mutations'
import { localeChanged } from 'vee-validate' import { localeChanged } from 'vee-validate'
import { getCommunityInfoMixin } from '../../mixins/getCommunityInfo' import { getCommunityInfoMixin } from '../../mixins/getCommunityInfo'
export default { export default {
components: { InputPasswordConfirmation, InputEmail, LanguageSwitchSelect }, components: { InputEmail, LanguageSwitchSelect },
name: 'register', name: 'register',
mixins: [getCommunityInfoMixin], mixins: [getCommunityInfoMixin],
data() { data() {
@ -211,10 +206,6 @@ export default {
lastname: '', lastname: '',
email: '', email: '',
agree: false, agree: false,
password: {
password: '',
passwordRepeat: '',
},
}, },
language: '', language: '',
submitted: false, submitted: false,
@ -240,12 +231,11 @@ export default {
async onSubmit() { async onSubmit() {
this.$apollo this.$apollo
.mutate({ .mutate({
mutation: registerUser, mutation: createUser,
variables: { variables: {
email: this.form.email, email: this.form.email,
firstName: this.form.firstname, firstName: this.form.firstname,
lastName: this.form.lastname, lastName: this.form.lastname,
password: this.form.password.password,
language: this.language, language: this.language,
publisherId: this.$store.state.publisherId, publisherId: this.$store.state.publisherId,
}, },
@ -264,8 +254,6 @@ export default {
this.form.email = '' this.form.email = ''
this.form.firstname = '' this.form.firstname = ''
this.form.lastname = '' this.form.lastname = ''
this.form.password.password = ''
this.form.password.passwordRepeat = ''
}, },
}, },
computed: { computed: {

View File

@ -37,7 +37,9 @@ describe('RegisterCommunity', () => {
}, },
}, },
$toasted: { $toasted: {
error: toastErrorMock, global: {
error: toastErrorMock,
},
}, },
} }

View File

@ -79,7 +79,9 @@ describe('RegisterSelectCommunity', () => {
show: spinnerMock, show: spinnerMock,
}, },
$toasted: { $toasted: {
error: toasterMock, global: {
error: toasterMock,
},
}, },
} }

View File

@ -76,7 +76,7 @@ export default {
) )
}) })
.catch((error) => { .catch((error) => {
this.$toasted.error(error.message) this.$toasted.global.error(error.message)
}) })
loader.hide() loader.hide()
this.pending = false this.pending = false

View File

@ -6,95 +6,78 @@ import flushPromises from 'flush-promises'
const localVue = global.localVue const localVue = global.localVue
const apolloQueryMock = jest.fn().mockRejectedValue({ message: 'error' })
const apolloMutationMock = jest.fn() const apolloMutationMock = jest.fn()
const toasterMock = jest.fn() const toasterMock = jest.fn()
const routerPushMock = jest.fn() const routerPushMock = jest.fn()
const stubs = {
RouterLink: RouterLinkStub,
}
const createMockObject = (comingFrom) => {
return {
localVue,
mocks: {
$i18n: {
locale: 'en',
},
$t: jest.fn((t) => t),
$route: {
params: {
optin: '123',
comingFrom,
},
},
$toasted: {
global: {
error: toasterMock,
},
},
$router: {
push: routerPushMock,
},
$loading: {
show: jest.fn(() => {
return { hide: jest.fn() }
}),
},
$apollo: {
mutate: apolloMutationMock,
},
},
stubs,
}
}
describe('ResetPassword', () => { describe('ResetPassword', () => {
let wrapper let wrapper
const mocks = { const Wrapper = (functionName) => {
$i18n: { return mount(ResetPassword, functionName)
locale: 'en',
},
$t: jest.fn((t) => t),
$route: {
params: {
optin: '123',
},
},
$toasted: {
error: toasterMock,
},
$router: {
push: routerPushMock,
},
$loading: {
show: jest.fn(() => {
return { hide: jest.fn() }
}),
},
$apollo: {
mutate: apolloMutationMock,
query: apolloQueryMock,
},
}
const stubs = {
RouterLink: RouterLinkStub,
}
const Wrapper = () => {
return mount(ResetPassword, { localVue, mocks, stubs })
} }
describe('mount', () => { describe('mount', () => {
beforeEach(() => { beforeEach(() => {
wrapper = Wrapper() wrapper = Wrapper(createMockObject())
})
it('calls the email verification when created', async () => {
expect(apolloQueryMock).toBeCalledWith(
expect.objectContaining({ variables: { optin: '123' } }),
)
}) })
describe('No valid optin', () => { describe('No valid optin', () => {
it('does not render the Reset Password form when not authenticated', () => { it.skip('does not render the Reset Password form when not authenticated', () => {
expect(wrapper.find('form').exists()).toBeFalsy() expect(wrapper.find('form').exists()).toBeFalsy()
}) })
it('toasts an error when no valid optin is given', () => { it.skip('toasts an error when no valid optin is given', () => {
expect(toasterMock).toHaveBeenCalledWith('error') expect(toasterMock).toHaveBeenCalledWith('error')
}) })
it('has a message suggesting to contact the support', () => { it.skip('has a message suggesting to contact the support', () => {
expect(wrapper.find('div.header').text()).toContain('settings.password.reset') expect(wrapper.find('div.header').text()).toContain('settings.password.reset')
expect(wrapper.find('div.header').text()).toContain( expect(wrapper.find('div.header').text()).toContain('settings.password.not-authenticated')
'settings.password.reset-password.not-authenticated',
)
}) })
}) })
describe('is authenticated', () => { describe('is authenticated', () => {
beforeEach(() => {
apolloQueryMock.mockResolvedValue({
data: {
loginViaEmailVerificationCode: {
sessionId: 1,
email: 'user@example.org',
},
},
})
})
it.skip('Has sessionId from API call', async () => {
await wrapper.vm.$nextTick()
expect(wrapper.vm.sessionId).toBe(1)
})
it('renders the Reset Password form when authenticated', () => { it('renders the Reset Password form when authenticated', () => {
expect(wrapper.find('div.resetpwd-form').exists()).toBeTruthy() expect(wrapper.find('div.resetpwd-form').exists()).toBeTruthy()
}) })
@ -114,7 +97,7 @@ describe('ResetPassword', () => {
}) })
it('links to /login when clicking "Back"', async () => { it('links to /login when clicking "Back"', async () => {
expect(wrapper.findAllComponents(RouterLinkStub).at(0).props().to).toBe('/Login') expect(wrapper.findAllComponents(RouterLinkStub).at(0).props().to).toBe('/login')
}) })
}) })
@ -128,7 +111,7 @@ describe('ResetPassword', () => {
}) })
it('toggles the first input field to text when eye icon is clicked', async () => { it('toggles the first input field to text when eye icon is clicked', async () => {
wrapper.findAll('button').at(0).trigger('click') await wrapper.findAll('button').at(0).trigger('click')
await wrapper.vm.$nextTick() await wrapper.vm.$nextTick()
expect(wrapper.findAll('input').at(0).attributes('type')).toBe('text') expect(wrapper.findAll('input').at(0).attributes('type')).toBe('text')
}) })
@ -142,37 +125,61 @@ describe('ResetPassword', () => {
describe('submit form', () => { describe('submit form', () => {
beforeEach(async () => { beforeEach(async () => {
await wrapper.setData({ authenticated: true, sessionId: 1 }) // wrapper = Wrapper(createMockObject())
await wrapper.vm.$nextTick()
await wrapper.findAll('input').at(0).setValue('Aa123456_') await wrapper.findAll('input').at(0).setValue('Aa123456_')
await wrapper.findAll('input').at(1).setValue('Aa123456_') await wrapper.findAll('input').at(1).setValue('Aa123456_')
await flushPromises() await flushPromises()
await wrapper.find('form').trigger('submit')
}) })
describe('server response with error', () => { describe('server response with error code > 10min', () => {
beforeEach(() => { beforeEach(async () => {
apolloMutationMock.mockRejectedValue({ message: 'error' }) jest.clearAllMocks()
apolloMutationMock.mockRejectedValue({ message: '...Code is older than 10 minutes' })
await wrapper.find('form').trigger('submit')
await flushPromises()
}) })
it('toasts an error message', () => { it('toasts an error message', () => {
expect(toasterMock).toHaveBeenCalledWith('error') expect(toasterMock).toHaveBeenCalledWith('...Code is older than 10 minutes')
})
it('router pushes to /password/reset', () => {
expect(routerPushMock).toHaveBeenCalledWith('/password/reset')
})
})
describe('server response with error code > 10min', () => {
beforeEach(async () => {
jest.clearAllMocks()
apolloMutationMock.mockRejectedValueOnce({ message: 'Error' })
await wrapper.find('form').trigger('submit')
await flushPromises()
})
it('toasts an error message', () => {
expect(toasterMock).toHaveBeenCalledWith('Error')
}) })
}) })
describe('server response with success', () => { describe('server response with success', () => {
beforeEach(() => { beforeEach(async () => {
apolloMutationMock.mockResolvedValue({ apolloMutationMock.mockResolvedValue({
data: { data: {
resetPassword: 'success', resetPassword: 'success',
}, },
}) })
wrapper = Wrapper(createMockObject('checkEmail'))
await wrapper.findAll('input').at(0).setValue('Aa123456_')
await wrapper.findAll('input').at(1).setValue('Aa123456_')
await wrapper.find('form').trigger('submit')
await flushPromises()
}) })
it('calls the API', () => { it('calls the API', () => {
expect(apolloMutationMock).toBeCalledWith( expect(apolloMutationMock).toBeCalledWith(
expect.objectContaining({ expect.objectContaining({
variables: { variables: {
sessionId: 1, code: '123',
email: 'user@example.org',
password: 'Aa123456_', password: 'Aa123456_',
}, },
}), }),

View File

@ -6,13 +6,10 @@
<b-row class="justify-content-center"> <b-row class="justify-content-center">
<b-col xl="5" lg="6" md="8" class="px-2"> <b-col xl="5" lg="6" md="8" class="px-2">
<h1>{{ $t('settings.password.reset') }}</h1> <h1>{{ $t('settings.password.reset') }}</h1>
<div class="pb-4" v-if="!pending"> <div class="pb-4">
<span v-if="authenticated"> <span>
{{ $t('settings.password.reset-password.text') }} {{ $t('settings.password.reset-password.text') }}
</span> </span>
<span v-else>
{{ $t('settings.password.reset-password.not-authenticated') }}
</span>
</div> </div>
</b-col> </b-col>
</b-row> </b-row>
@ -20,16 +17,16 @@
</div> </div>
</b-container> </b-container>
<b-container class="mt--8 p-1"> <b-container class="mt--8 p-1">
<b-row class="justify-content-center" v-if="authenticated"> <b-row class="justify-content-center">
<b-col lg="6" md="8"> <b-col lg="6" md="8">
<b-card no-body class="border-0" style="background-color: #ebebeba3 !important"> <b-card no-body class="border-0" style="background-color: #ebebeba3 !important">
<b-card-body class="p-4"> <b-card-body class="p-4">
<validation-observer ref="observer" v-slot="{ handleSubmit }"> <validation-observer ref="observer" v-slot="{ handleSubmit }">
<b-form role="form" @submit.prevent="handleSubmit(onSubmit)"> <b-form role="form" @submit.prevent="handleSubmit(onSubmit)">
<input-password-confirmation v-model="form" :register="register" /> <input-password-confirmation v-model="form" />
<div class="text-center"> <div class="text-center">
<b-button type="submit" variant="primary" class="mt-4"> <b-button type="submit" variant="primary" class="mt-4">
{{ $t('settings.password.reset') }} {{ $t(displaySetup.button) }}
</b-button> </b-button>
</div> </div>
</b-form> </b-form>
@ -38,9 +35,9 @@
</b-card> </b-card>
</b-col> </b-col>
</b-row> </b-row>
<b-row> <b-row v-if="displaySetup.linkTo">
<b-col class="text-center py-lg-4"> <b-col class="text-center py-lg-4">
<router-link to="/Login" class="mt-3">{{ $t('back') }}</router-link> <router-link :to="displaySetup.linkTo" class="mt-3">{{ $t('back') }}</router-link>
</b-col> </b-col>
</b-row> </b-row>
</b-container> </b-container>
@ -48,8 +45,26 @@
</template> </template>
<script> <script>
import InputPasswordConfirmation from '../../components/Inputs/InputPasswordConfirmation' import InputPasswordConfirmation from '../../components/Inputs/InputPasswordConfirmation'
import { loginViaEmailVerificationCode } from '../../graphql/queries' import { setPassword } from '../../graphql/mutations'
import { resetPassword } from '../../graphql/mutations'
const textFields = {
reset: {
authenticated: 'settings.password.reset-password.text',
notAuthenticated: 'settings.password.not-authenticated',
button: 'settings.password.reset',
linkTo: '/login',
},
checkEmail: {
authenticated: 'settings.password.set-password.text',
notAuthenticated: 'settings.password.not-authenticated',
button: 'settings.password.set',
linkTo: '/login',
},
login: {
headline: 'site.thx.errorTitle',
subtitle: 'site.thx.activateEmail',
},
}
export default { export default {
name: 'ResetPassword', name: 'ResetPassword',
@ -62,21 +77,16 @@ export default {
password: '', password: '',
passwordRepeat: '', passwordRepeat: '',
}, },
authenticated: false, displaySetup: {},
sessionId: null,
email: null,
pending: true,
register: false,
} }
}, },
methods: { methods: {
async onSubmit() { async onSubmit() {
this.$apollo this.$apollo
.mutate({ .mutate({
mutation: resetPassword, mutation: setPassword,
variables: { variables: {
sessionId: this.sessionId, code: this.$route.params.optin,
email: this.email,
password: this.form.password, password: this.form.password,
}, },
}) })
@ -85,35 +95,24 @@ export default {
this.$router.push('/thx/reset') this.$router.push('/thx/reset')
}) })
.catch((error) => { .catch((error) => {
this.$toasted.error(error.message) if (error.message.includes('Code is older than 10 minutes')) {
this.$toasted.global.error(error.message)
this.$router.push('/password/reset')
} else {
this.$toasted.global.error(error.message)
}
}) })
}, },
async authenticate() { setDisplaySetup() {
const loader = this.$loading.show({ if (!this.$route.params.comingFrom) {
container: this.$refs.header, this.displaySetup = textFields.reset
}) } else {
const optin = this.$route.params.optin this.displaySetup = textFields[this.$route.params.comingFrom]
this.$apollo }
.query({
query: loginViaEmailVerificationCode,
variables: {
optin: optin,
},
})
.then((result) => {
this.authenticated = true
this.sessionId = result.data.loginViaEmailVerificationCode.sessionId
this.email = result.data.loginViaEmailVerificationCode.email
})
.catch((error) => {
this.$toasted.error(error.message)
})
loader.hide()
this.pending = false
}, },
}, },
mounted() { created() {
this.authenticate() this.setDisplaySetup()
}, },
} }
</script> </script>

View File

@ -24,7 +24,9 @@ describe('UserCard_CoinAnimation', () => {
}, },
$toasted: { $toasted: {
success: toastSuccessMock, success: toastSuccessMock,
error: toastErrorMock, global: {
error: toastErrorMock,
},
}, },
$apollo: { $apollo: {
mutate: mockAPIcall, mutate: mockAPIcall,

View File

@ -58,7 +58,7 @@ export default {
}) })
.catch((error) => { .catch((error) => {
this.CoinAnimationStatus = this.$store.state.coinanimation this.CoinAnimationStatus = this.$store.state.coinanimation
this.$toasted.error(error.message) this.$toasted.global.error(error.message)
}) })
}, },
}, },

View File

@ -25,7 +25,9 @@ describe('UserCard_FormUserData', () => {
}, },
$toasted: { $toasted: {
success: toastSuccessMock, success: toastSuccessMock,
error: toastErrorMock, global: {
error: toastErrorMock,
},
}, },
$apollo: { $apollo: {
mutate: mockAPIcall, mutate: mockAPIcall,

View File

@ -124,7 +124,7 @@ export default {
this.$toasted.success(this.$t('settings.name.change-success')) this.$toasted.success(this.$t('settings.name.change-success'))
}) })
.catch((error) => { .catch((error) => {
this.$toasted.error(error.message) this.$toasted.global.error(error.message)
}) })
}, },
}, },

View File

@ -17,7 +17,9 @@ describe('UserCard_FormUserPasswort', () => {
$t: jest.fn((t) => t), $t: jest.fn((t) => t),
$toasted: { $toasted: {
success: toastSuccessMock, success: toastSuccessMock,
error: toastErrorMock, global: {
error: toastErrorMock,
},
}, },
$apollo: { $apollo: {
mutate: changePasswordProfileMock, mutate: changePasswordProfileMock,

View File

@ -85,7 +85,7 @@ export default {
this.cancelEdit() this.cancelEdit()
}) })
.catch((error) => { .catch((error) => {
this.$toasted.error(error.message) this.$toasted.global.error(error.message)
}) })
}, },
}, },

View File

@ -31,7 +31,9 @@ describe('UserCard_FormUsername', () => {
}, },
$toasted: { $toasted: {
success: toastSuccessMock, success: toastSuccessMock,
error: toastErrorMock, global: {
error: toastErrorMock,
},
}, },
$apollo: { $apollo: {
mutate: mockAPIcall, mutate: mockAPIcall,

View File

@ -100,7 +100,7 @@ export default {
this.$toasted.success(this.$t('settings.name.change-success')) this.$toasted.success(this.$t('settings.name.change-success'))
}) })
.catch((error) => { .catch((error) => {
this.$toasted.error(error.message) this.$toasted.global.error(error.message)
this.showUsername = true this.showUsername = true
this.username = this.$store.state.username this.username = this.$store.state.username
this.form.username = this.$store.state.username this.form.username = this.$store.state.username

View File

@ -28,7 +28,9 @@ describe('UserCard_Language', () => {
}, },
$toasted: { $toasted: {
success: toastSuccessMock, success: toastSuccessMock,
error: toastErrorMock, global: {
error: toastErrorMock,
},
}, },
$apollo: { $apollo: {
mutate: mockAPIcall, mutate: mockAPIcall,

View File

@ -101,7 +101,7 @@ export default {
}) })
.catch((error) => { .catch((error) => {
this.language = this.$store.state.language this.language = this.$store.state.language
this.$toasted.error(error.message) this.$toasted.global.error(error.message)
}) })
}, },
buildTagFromLanguageString() { buildTagFromLanguageString() {

View File

@ -25,7 +25,9 @@ describe('UserCard_Newsletter', () => {
}, },
$toasted: { $toasted: {
success: toastSuccessMock, success: toastSuccessMock,
error: toastErrorMock, global: {
error: toastErrorMock,
},
}, },
$apollo: { $apollo: {
mutate: mockAPIcall, mutate: mockAPIcall,

View File

@ -56,7 +56,7 @@ export default {
}) })
.catch((error) => { .catch((error) => {
this.newsletterState = this.$store.state.newsletterState this.newsletterState = this.$store.state.newsletterState
this.$toasted.error(error.message) this.$toasted.global.error(error.message)
}) })
}, },
}, },

File diff suppressed because it is too large Load Diff

View File

@ -491,7 +491,7 @@ namespace model {
break; break;
case TRANSACTION_VALID_INVALID_TARGET_DATE: case TRANSACTION_VALID_INVALID_TARGET_DATE:
error_name = t->gettext_str("Creation Error"); error_name = t->gettext_str("Creation Error");
error_description = t->gettext_str("Invalid target date! No future and only 3 month in the past."); error_description = t->gettext_str("Invalid target date! No future and only 2 month in the past.");
break; break;
case TRANSACTION_VALID_CREATION_OUT_OF_BORDER: case TRANSACTION_VALID_CREATION_OUT_OF_BORDER:
error_name = t->gettext_str("Creation Error"); error_name = t->gettext_str("Creation Error");

View File

@ -74,8 +74,8 @@ namespace model {
auto now = Poco::DateTime(); auto now = Poco::DateTime();
if (target_date.year() == now.year()) if (target_date.year() == now.year())
{ {
if (target_date.month() + 3 < now.month()) { if (target_date.month() + 2 < now.month()) {
addError(new Error(function_name, "year is the same, target date month is more than 3 month in past")); addError(new Error(function_name, "year is the same, target date month is more than 2 month in past"));
return TRANSACTION_VALID_INVALID_TARGET_DATE; return TRANSACTION_VALID_INVALID_TARGET_DATE;
} }
if (target_date.month() > now.month()) { if (target_date.month() > now.month()) {
@ -96,8 +96,8 @@ namespace model {
else else
{ {
// target_date.year +1 == now.year // target_date.year +1 == now.year
if (target_date.month() + 3 < now.month() + 12) { if (target_date.month() + 2 < now.month() + 12) {
addError(new Error(function_name, "target date is more than 3 month in past")); addError(new Error(function_name, "target date is more than 2 month in past"));
return TRANSACTION_VALID_INVALID_TARGET_DATE; return TRANSACTION_VALID_INVALID_TARGET_DATE;
} }
} }