Merge branch 'master' into admin_pending_creation

This commit is contained in:
Moriz Wahl 2021-11-24 21:27:44 +01:00
commit 2f92aec460
55 changed files with 1281 additions and 353 deletions

View File

@ -441,7 +441,7 @@ jobs:
report_name: Coverage Admin Interface
type: lcov
result_path: ./coverage/lcov.info
min_coverage: 65
min_coverage: 51
token: ${{ github.token }}
##############################################################################
@ -491,7 +491,7 @@ jobs:
report_name: Coverage Backend
type: lcov
result_path: ./backend/coverage/lcov.info
min_coverage: 38
min_coverage: 37
token: ${{ github.token }}
##############################################################################

3
admin/.env.dist Normal file
View File

@ -0,0 +1,3 @@
GRAPHQL_URI=http://localhost:4000/graphql
WALLET_AUTH_URL=http://localhost/vue/authenticate?token=$1
DEBUG_DISABLE_AUTH=false

View File

@ -1,6 +1,11 @@
module.exports = {
verbose: true,
collectCoverageFrom: ['src/**/*.{js,vue}', '!**/node_modules/**', '!**/?(*.)+(spec|test).js?(x)'],
collectCoverageFrom: [
'src/**/*.{js,vue}',
'!**/node_modules/**',
'!src/assets/**',
'!**/?(*.)+(spec|test).js?(x)',
],
moduleFileExtensions: [
'js',
// 'jsx',

View File

@ -1,43 +1,28 @@
import { mount } from '@vue/test-utils'
import { shallowMount } from '@vue/test-utils'
import App from './App'
const localVue = global.localVue
const storeCommitMock = jest.fn()
const stubs = {
RouterView: true,
}
const mocks = {
$store: {
commit: storeCommitMock,
state: {
token: null,
},
},
}
const localStorageMock = (() => {
let store = {}
return {
getItem: (key) => {
return store[key] || null
},
setItem: (key, value) => {
store[key] = value.toString()
},
removeItem: (key) => {
delete store[key]
},
clear: () => {
store = {}
},
}
})()
describe('App', () => {
let wrapper
const Wrapper = () => {
return mount(App, { localVue, mocks })
return shallowMount(App, { localVue, stubs, mocks })
}
describe('mount', () => {
describe('shallowMount', () => {
beforeEach(() => {
wrapper = Wrapper()
})
@ -46,23 +31,4 @@ describe('App', () => {
expect(wrapper.find('div#app').exists()).toBeTruthy()
})
})
describe('window localStorage is undefined', () => {
it('does not commit a token to the store', () => {
expect(storeCommitMock).not.toBeCalled()
})
})
describe('with token in local storage', () => {
beforeEach(() => {
Object.defineProperty(window, 'localStorage', {
value: localStorageMock,
})
window.localStorage.setItem('vuex', JSON.stringify({ token: 1234 }))
})
it.skip('commits the token to the store', () => {
expect(storeCommitMock).toBeCalledWith('token', 1234)
})
})
})

View File

@ -1,19 +1,15 @@
<template>
<div id="app">
<nav-bar class="wrapper-nav" />
<router-view class="wrapper p-3"></router-view>
<foo-ter />
<default-layout v-if="$store.state.token" />
<router-view v-else></router-view>
</div>
</template>
<script>
import NavBar from '@/components/NavBar.vue'
import FooTer from '@/components/Footer.vue'
import defaultLayout from '@/layouts/defaultLayout.vue'
export default {
name: 'App',
components: {
NavBar,
FooTer,
},
name: 'app',
components: { defaultLayout },
}
</script>

View File

@ -8,3 +8,8 @@
</div>
</div>
</template>
<script>
export default {
name: 'ContentFooter',
}
</script>

View File

@ -19,7 +19,7 @@ const mocks = {
const propsData = {
type: '',
item: {},
creation: {},
creation: [],
itemsMassCreation: {},
}
@ -38,5 +38,104 @@ describe('CreationFormular', () => {
it('has a DIV element with the class.component-creation-formular', () => {
expect(wrapper.find('.component-creation-formular').exists()).toBeTruthy()
})
describe('radio buttons to selcet month', () => {
it('has three radio buttons', () => {
expect(wrapper.findAll('input[type="radio"]').length).toBe(3)
})
describe('with mass creation', () => {
beforeEach(async () => {
jest.clearAllMocks()
await wrapper.setProps({ type: 'massCreation' })
})
describe('first radio button', () => {
beforeEach(async () => {
await wrapper.findAll('input[type="radio"]').at(0).setChecked()
})
it('emits update-radio-selected with index 0', () => {
expect(wrapper.emitted()['update-radio-selected']).toEqual([
[expect.arrayContaining([0])],
])
})
})
describe('second radio button', () => {
beforeEach(async () => {
await wrapper.findAll('input[type="radio"]').at(1).setChecked()
})
it('emits update-radio-selected with index 1', () => {
expect(wrapper.emitted()['update-radio-selected']).toEqual([
[expect.arrayContaining([1])],
])
})
})
describe('third radio button', () => {
beforeEach(async () => {
await wrapper.findAll('input[type="radio"]').at(2).setChecked()
})
it('emits update-radio-selected with index 2', () => {
expect(wrapper.emitted()['update-radio-selected']).toEqual([
[expect.arrayContaining([2])],
])
})
})
})
describe('with single creation', () => {
beforeEach(async () => {
jest.clearAllMocks()
await wrapper.setProps({ type: 'singleCreation', creation: [200, 400, 600] })
await wrapper.setData({ rangeMin: 180 })
})
describe('first radio button', () => {
beforeEach(async () => {
await wrapper.findAll('input[type="radio"]').at(0).setChecked()
})
it('sets rangeMin to 0', () => {
expect(wrapper.vm.rangeMin).toBe(0)
})
it('sets rangeMax to 200', () => {
expect(wrapper.vm.rangeMax).toBe(200)
})
})
describe('second radio button', () => {
beforeEach(async () => {
await wrapper.findAll('input[type="radio"]').at(1).setChecked()
})
it('sets rangeMin to 0', () => {
expect(wrapper.vm.rangeMin).toBe(0)
})
it('sets rangeMax to 400', () => {
expect(wrapper.vm.rangeMax).toBe(400)
})
})
describe('third radio button', () => {
beforeEach(async () => {
await wrapper.findAll('input[type="radio"]').at(2).setChecked()
})
it('sets rangeMin to 0', () => {
expect(wrapper.vm.rangeMin).toBe(0)
})
it('sets rangeMax to 400', () => {
expect(wrapper.vm.rangeMax).toBe(600)
})
})
})
})
})
})

View File

@ -146,7 +146,7 @@ export default {
required: false,
},
creation: {
type: Object,
type: Array,
required: true,
},
itemsMassCreation: {
@ -198,9 +198,10 @@ export default {
this.text = this.creationUserData.text
break
case 'range':
this.value = this.creationUserData.creation_gdd
this.value = this.creationUserData.creationGdd
break
default:
// TODO: Toast
alert("I don't know such values")
}
},
@ -262,9 +263,11 @@ export default {
// hinweis das eine ein einzelne Schöpfung abgesendet wird an (email)
alert('UPDATE EINZEL SCHÖPFUNG ABSENDEN FÜR >> ')
// umschreiben, update eine bestehende Schöpfung eine
this.creationUserData.datum = this.radioSelected.long
this.creationUserData.creation_gdd = this.value
this.creationUserData.text = this.text
this.$emit('update-creation-data', {
datum: this.radioSelected.long,
creationGdd: this.value,
text: this.text,
})
} else {
// hinweis das eine ein einzelne Schöpfung abgesendet wird an (email)
alert('EINZEL SCHÖPFUNG ABSENDEN FÜR >> ' + this.item.firstName + '')

View File

@ -16,15 +16,43 @@
>
| {{ $store.state.openCreations }} offene Schöpfungen
</b-nav-item>
<b-nav-item @click="wallet">Wallet</b-nav-item>
<b-nav-item @click="logout">Logout</b-nav-item>
<!-- <b-nav-item v-show="open < 1" to="/creation-confirm">| keine offene Schöpfungen</b-nav-item> -->
</b-navbar-nav>
</b-collapse>
<b-navbar-brand href="http://localhost:3000/vue/login">Profilbereich</b-navbar-brand>
</b-navbar>
</div>
</template>
<script>
import CONFIG from '../config'
export default {
name: 'navbar',
methods: {
logout() {
// TODO
// this.$emit('logout')
/* this.$apollo
.query({
query: logout,
})
.then(() => {
this.$store.dispatch('logout')
this.$router.push('/logout')
})
.catch(() => {
this.$store.dispatch('logout')
if (this.$router.currentRoute.path !== '/logout') this.$router.push('/logout')
})
*/
this.$store.dispatch('logout')
this.$router.push('/logout')
},
wallet() {
window.location = CONFIG.WALLET_AUTH_URL.replace('$1', this.$store.state.token)
this.$store.dispatch('logout') // logout without redirect
},
},
}
</script>

View File

@ -10,7 +10,7 @@ describe('UserTable', () => {
type: 'Type',
itemsUser: [],
fieldsTable: [],
creation: {},
creation: [],
}
const Wrapper = () => {

View File

@ -67,6 +67,7 @@
:creation="row.item.creation"
:item="row.item"
:creationUserData="creationData"
@update-creation-data="updateCreationData"
/>
<b-button size="sm" @click="row.toggleDetails">
@ -139,7 +140,7 @@ export default {
default: '',
},
creation: {
type: Object,
type: Array,
required: false,
},
},
@ -226,6 +227,11 @@ export default {
}
row.toggleDetails()
},
updateCreationData(data) {
this.creationData = {
...data,
}
},
},
}
</script>

View File

@ -17,8 +17,13 @@ const environment = {
PRODUCTION: process.env.NODE_ENV === 'production' || false,
}
const server = {
const endpoints = {
GRAPHQL_URI: process.env.GRAPHQL_URI || 'http://localhost:4000/graphql',
WALLET_AUTH_URL: process.env.WALLET_AUTH_URL || 'http://localhost/vue/authenticate?token=$1',
}
const debug = {
DEBUG_DISABLE_AUTH: process.env.DEBUG_DISABLE_AUTH === 'true' || false,
}
const options = {}
@ -26,8 +31,9 @@ const options = {}
const CONFIG = {
...version,
...environment,
...server,
...endpoints,
...options,
...debug,
}
export default CONFIG

30
admin/src/i18n.test.js Normal file
View File

@ -0,0 +1,30 @@
import i18n from './i18n'
import VueI18n from 'vue-i18n'
jest.mock('vue-i18n')
describe('i18n', () => {
it('calls i18n with locale en', () => {
expect(VueI18n).toBeCalledWith(
expect.objectContaining({
locale: 'en',
}),
)
})
it('calls i18n with fallback locale en', () => {
expect(VueI18n).toBeCalledWith(
expect.objectContaining({
fallbackLocale: 'en',
}),
)
})
it('has a _t function', () => {
expect(i18n).toEqual(
expect.objectContaining({
_t: expect.anything(),
}),
)
})
})

View File

@ -0,0 +1,19 @@
<template>
<div>
<nav-bar class="wrapper-nav" />
<router-view class="wrapper p-3"></router-view>
<content-footer />
</div>
</template>
<script>
import NavBar from '@/components/NavBar.vue'
import ContentFooter from '@/components/ContentFooter.vue'
export default {
name: 'defaultLayout',
components: {
NavBar,
ContentFooter,
},
}
</script>

View File

@ -0,0 +1,59 @@
import { mount } from '@vue/test-utils'
import Creation from './Creation.vue'
const localVue = global.localVue
const apolloQueryMock = jest.fn().mockResolvedValue({
data: {
searchUsers: [
{
firstName: 'Bibi',
lastName: 'Bloxberg',
email: 'bibi@bloxberg.de',
creation: [200, 400, 600],
},
],
},
})
const toastErrorMock = jest.fn()
const mocks = {
$apollo: {
query: apolloQueryMock,
},
$toasted: {
error: toastErrorMock,
},
}
describe('Creation', () => {
let wrapper
const Wrapper = () => {
return mount(Creation, { localVue, mocks })
}
describe('mount', () => {
beforeEach(() => {
wrapper = Wrapper()
})
it('has a DIV element with the class.creation', () => {
expect(wrapper.find('div.creation').exists()).toBeTruthy()
})
describe('apollo returns error', () => {
beforeEach(() => {
apolloQueryMock.mockRejectedValue({
message: 'Ouch',
})
wrapper = Wrapper()
})
it('toasts an error message', () => {
expect(toastErrorMock).toBeCalledWith('Ouch')
})
})
})
})

View File

@ -1,5 +1,5 @@
<template>
<div>
<div class="creation">
<b-row>
<b-col cols="12" lg="5">
<label>Usersuche</label>
@ -10,6 +10,7 @@
placeholder="User suche"
></b-input>
<user-table
v-if="itemsList.length > 0"
type="UserListSearch"
:itemsUser="itemsList"
:fieldsTable="Searchfields"
@ -20,7 +21,7 @@
</b-col>
<b-col cols="12" lg="7" class="shadow p-3 mb-5 rounded bg-info">
<user-table
v-show="Object.keys(this.massCreation).length > 0"
v-if="massCreation.length > 0"
class="shadow p-3 mb-5 bg-white rounded"
type="UserListMassCreation"
:itemsUser="massCreation"
@ -31,6 +32,7 @@
/>
<creation-formular
v-if="massCreation.length > 0"
type="massCreation"
:creation="creation"
:itemsMassCreation="massCreation"
@ -47,7 +49,7 @@ import UserTable from '../components/UserTable.vue'
import { searchUsers } from '../graphql/searchUsers'
export default {
name: 'overview',
name: 'Creation',
components: {
CreationFormular,
UserTable,
@ -57,7 +59,6 @@ export default {
showArrays: false,
Searchfields: [
{ key: 'bookmark', label: 'merken' },
{ key: 'firstName', label: 'Firstname' },
{ key: 'lastName', label: 'Lastname' },
{ key: 'creation', label: 'Creation' },
@ -77,11 +78,11 @@ export default {
creation: [null, null, null],
}
},
created() {
this.getUsers()
async created() {
await this.getUsers()
},
methods: {
getUsers() {
async getUsers() {
this.$apollo
.query({
query: searchUsers,
@ -93,7 +94,7 @@ export default {
this.itemsList = result.data.searchUsers.map((user) => {
return {
...user,
// showDetails: true,
showDetails: false,
}
})
})

View File

@ -0,0 +1,53 @@
import { mount } from '@vue/test-utils'
import CreationConfirm from './CreationConfirm.vue'
const localVue = global.localVue
const storeCommitMock = jest.fn()
const mocks = {
$store: {
commit: storeCommitMock,
},
}
describe('CreationConfirm', () => {
let wrapper
const Wrapper = () => {
return mount(CreationConfirm, { localVue, mocks })
}
describe('mount', () => {
beforeEach(() => {
jest.clearAllMocks()
wrapper = Wrapper()
})
it('has a DIV element with the class.creation-confirm', () => {
expect(wrapper.find('div.creation-confirm').exists()).toBeTruthy()
})
describe('store', () => {
it('commits resetOpenCreations to store', () => {
expect(storeCommitMock).toBeCalledWith('resetOpenCreations')
})
it('commits openCreationsPlus to store', () => {
expect(storeCommitMock).toBeCalledWith('openCreationsPlus', 5)
})
})
describe('confirm creation', () => {
beforeEach(async () => {
await wrapper
.findComponent({ name: 'UserTable' })
.vm.$emit('remove-confirm-result', 1, 'remove')
})
it('commits openCreationsMinus to store', () => {
expect(storeCommitMock).toBeCalledWith('openCreationsMinus', 1)
})
})
})
})

View File

@ -1,5 +1,5 @@
<template>
<div>
<div class="creation-confirm">
<small class="bg-danger text-light p-1">
Die anzahl der offene Schöpfungen stimmen nicht! Diese wird bei absenden im $store
hochgezählt. Die Liste die hier angezeigt wird ist SIMULIERT!
@ -17,7 +17,7 @@
import UserTable from '../components/UserTable.vue'
export default {
name: 'creation_confirm',
name: 'CreationConfirm',
components: {
UserTable,
},

View File

@ -0,0 +1,59 @@
import { mount } from '@vue/test-utils'
import UserSearch from './UserSearch.vue'
const localVue = global.localVue
const apolloQueryMock = jest.fn().mockResolvedValue({
data: {
searchUsers: [
{
firstName: 'Bibi',
lastName: 'Bloxberg',
email: 'bibi@bloxberg.de',
creation: [200, 400, 600],
},
],
},
})
const toastErrorMock = jest.fn()
const mocks = {
$apollo: {
query: apolloQueryMock,
},
$toasted: {
error: toastErrorMock,
},
}
describe('UserSearch', () => {
let wrapper
const Wrapper = () => {
return mount(UserSearch, { localVue, mocks })
}
describe('mount', () => {
beforeEach(() => {
wrapper = Wrapper()
})
it('has a DIV element with the class.user-search', () => {
expect(wrapper.find('div.user-search').exists()).toBeTruthy()
})
describe('apollo returns error', () => {
beforeEach(() => {
apolloQueryMock.mockRejectedValue({
message: 'Ouch',
})
wrapper = Wrapper()
})
it('toasts an error message', () => {
expect(toastErrorMock).toBeCalledWith('Ouch')
})
})
})
})

View File

@ -1,5 +1,5 @@
<template>
<div>
<div class="user-search">
<label>Usersuche</label>
<b-input
type="text"
@ -21,7 +21,7 @@ import UserTable from '../components/UserTable.vue'
import { searchUsers } from '../graphql/searchUsers'
export default {
name: 'overview',
name: 'UserSearch',
components: {
UserTable,
},

View File

@ -1,7 +1,25 @@
import CONFIG from '../config'
const addNavigationGuards = (router, store) => {
// store token on `authenticate`
router.beforeEach((to, from, next) => {
// handle authentication
if (to.meta.requiresAuth && !store.state.token) {
if (to.path === '/authenticate' && to.query && to.query.token) {
// TODO verify user to get user data
store.commit('token', to.query.token)
next({ path: '/' })
} else {
next()
}
})
// protect all routes but `not-found`
router.beforeEach((to, from, next) => {
if (
!CONFIG.DEBUG_DISABLE_AUTH && // we did not disabled the auth module for debug purposes
!store.state.token && // we do not have a token
to.path !== '/not-found' && // we are not on `not-found`
to.path !== '/logout' // we are not on `logout`
) {
next({ path: '/not-found' })
} else {
next()

View File

@ -0,0 +1,64 @@
import addNavigationGuards from './guards'
import router from './router'
const storeCommitMock = jest.fn()
const store = {
commit: storeCommitMock,
state: {
token: null,
},
}
addNavigationGuards(router, store)
describe('navigation guards', () => {
beforeEach(() => {
jest.clearAllMocks()
})
describe('authenticate', () => {
const navGuard = router.beforeHooks[0]
const next = jest.fn()
describe('with valid token', () => {
it('commits the token to the store', async () => {
navGuard({ path: '/authenticate', query: { token: 'valid-token' } }, {}, next)
expect(storeCommitMock).toBeCalledWith('token', 'valid-token')
})
it('redirects to /', async () => {
navGuard({ path: '/authenticate', query: { token: 'valid-token' } }, {}, next)
expect(next).toBeCalledWith({ path: '/' })
})
})
describe('without valid token', () => {
it('does not commit the token to the store', async () => {
navGuard({ path: '/authenticate' }, {}, next)
expect(storeCommitMock).not.toBeCalledWith()
})
it('calls next withou arguments', async () => {
navGuard({ path: '/authenticate' }, {}, next)
expect(next).toBeCalledWith()
})
})
})
describe('protect all routes', () => {
const navGuard = router.beforeHooks[1]
const next = jest.fn()
it('redirects no not found with no token in store ', () => {
navGuard({ path: '/' }, {}, next)
expect(next).toBeCalledWith({ path: '/not-found' })
})
it('does not redirect when token in store', () => {
store.state.token = 'valid token'
navGuard({ path: '/' }, {}, next)
expect(next).toBeCalledWith()
})
})
})

View File

@ -0,0 +1,92 @@
import router from './router'
describe('router', () => {
describe('options', () => {
const { options } = router
const { scrollBehavior, routes } = options
it('has "/admin" as base', () => {
expect(options).toEqual(
expect.objectContaining({
base: '/admin',
}),
)
})
it('has "active" as linkActiveClass', () => {
expect(options).toEqual(
expect.objectContaining({
linkActiveClass: 'active',
}),
)
})
it('has "history" as mode', () => {
expect(options).toEqual(
expect.objectContaining({
mode: 'history',
}),
)
})
describe('scroll behavior', () => {
it('returns save position when given', () => {
expect(scrollBehavior({}, {}, 'given')).toBe('given')
})
it('returns selector when hash is given', () => {
expect(scrollBehavior({ hash: '#to' }, {})).toEqual({ selector: '#to' })
})
it('returns top left coordinates as default', () => {
expect(scrollBehavior({}, {})).toEqual({ x: 0, y: 0 })
})
})
describe('routes', () => {
it('has seven routes defined', () => {
expect(routes).toHaveLength(7)
})
it('has "/overview" as default', async () => {
const component = await routes.find((r) => r.path === '/').component()
expect(component.default.name).toBe('overview')
})
describe('logout', () => {
it('loads the "NotFoundPage" component', async () => {
const component = await routes.find((r) => r.path === '/logout').component()
expect(component.default.name).toBe('not-found')
})
})
describe('user', () => {
it('loads the "UserSearch" component', async () => {
const component = await routes.find((r) => r.path === '/user').component()
expect(component.default.name).toBe('UserSearch')
})
})
describe('creation', () => {
it('loads the "Creation" component', async () => {
const component = await routes.find((r) => r.path === '/creation').component()
expect(component.default.name).toBe('Creation')
})
})
describe('creation-confirm', () => {
it('loads the "CreationConfirm" component', async () => {
const component = await routes.find((r) => r.path === '/creation-confirm').component()
expect(component.default.name).toBe('CreationConfirm')
})
})
describe('not found page', () => {
it('renders the "NotFound" component', async () => {
const component = await routes.find((r) => r.path === '*').component()
expect(component.default.name).toEqual('not-found')
})
})
})
})
})

View File

@ -1,38 +1,27 @@
const routes = [
{
path: '/',
component: () => import('@/views/Overview.vue'),
meta: {
requiresAuth: true,
},
path: '/authenticate',
},
{
path: '/overview',
component: () => import('@/views/Overview.vue'),
meta: {
requiresAuth: true,
},
path: '/',
component: () => import('@/pages/Overview.vue'),
},
{
// TODO: Implement a "You are logged out"-Page
path: '/logout',
component: () => import('@/components/NotFoundPage.vue'),
},
{
path: '/user',
component: () => import('@/views/UserSearch.vue'),
meta: {
requiresAuth: true,
},
component: () => import('@/pages/UserSearch.vue'),
},
{
path: '/creation',
component: () => import('@/views/Creation.vue'),
meta: {
requiresAuth: true,
},
component: () => import('@/pages/Creation.vue'),
},
{
path: '/creation-confirm',
component: () => import('@/views/CreationConfirm.vue'),
meta: {
requiresAuth: true,
},
component: () => import('@/pages/CreationConfirm.vue'),
},
{
path: '*',

View File

@ -1,15 +1,16 @@
import Vuex from 'vuex'
import Vue from 'vue'
import createPersistedState from 'vuex-persistedstate'
import CONFIG from '../config'
Vue.use(Vuex)
export const mutations = {
openCreationsPlus: (state, i) => {
state.openCreations = state.openCreations + i
state.openCreations += i
},
openCreationsMinus: (state, i) => {
state.openCreations = state.openCreations - i
state.openCreations -= i
},
resetOpenCreations: (state) => {
state.openCreations = 0
@ -19,6 +20,13 @@ export const mutations = {
},
}
export const actions = {
logout: ({ commit, state }) => {
commit('token', null)
window.localStorage.clear()
},
}
const store = new Vuex.Store({
plugins: [
createPersistedState({
@ -26,12 +34,13 @@ const store = new Vuex.Store({
}),
],
state: {
token: 'some-valid-token',
token: CONFIG.DEBUG_DISABLE_AUTH ? 'validToken' : null,
moderator: 'Dertest Moderator',
openCreations: 0,
},
// Syncronous mutation of the state
mutations,
actions,
})
export default store

View File

@ -1,6 +1,11 @@
import { mutations } from './store'
import store, { mutations, actions } from './store'
const { token } = mutations
const { token, openCreationsPlus, openCreationsMinus, resetOpenCreations } = mutations
const { logout } = actions
const CONFIG = {
DEBUG_DISABLE_AUTH: true,
}
describe('Vuex store', () => {
describe('mutations', () => {
@ -11,5 +16,68 @@ describe('Vuex store', () => {
expect(state.token).toEqual('1234')
})
})
describe('openCreationsPlus', () => {
it('increases the open creations by a given number', () => {
const state = { openCreations: 0 }
openCreationsPlus(state, 12)
expect(state.openCreations).toEqual(12)
})
})
describe('openCreationsMinus', () => {
it('decreases the open creations by a given number', () => {
const state = { openCreations: 12 }
openCreationsMinus(state, 2)
expect(state.openCreations).toEqual(10)
})
})
describe('resetOpenCreations', () => {
it('sets the open creations to 0', () => {
const state = { openCreations: 24 }
resetOpenCreations(state)
expect(state.openCreations).toEqual(0)
})
})
})
describe('actions', () => {
describe('logout', () => {
const windowStorageMock = jest.fn()
const commit = jest.fn()
const state = {}
beforeEach(() => {
jest.clearAllMocks()
window.localStorage.clear = windowStorageMock
})
it('deletes the token in store', () => {
logout({ commit, state })
expect(commit).toBeCalledWith('token', null)
})
it.skip('clears the window local storage', () => {
expect(windowStorageMock).toBeCalled()
})
})
})
describe('state', () => {
describe('authentication enabled', () => {
it('has no token', () => {
expect(store.state.token).toBe(null)
})
})
describe('authentication enabled', () => {
beforeEach(() => {
CONFIG.DEBUG_DISABLE_AUTH = false
})
it.skip('has a token', () => {
expect(store.state.token).toBe('validToken')
})
})
})
})

View File

@ -1,6 +1,6 @@
import { createLocalVue } from '@vue/test-utils'
import Vue from 'vue'
import { BootstrapVue } from 'bootstrap-vue'
import { BootstrapVue, IconsPlugin } from 'bootstrap-vue'
// without this async calls are not working
import 'regenerator-runtime'
@ -8,6 +8,7 @@ import 'regenerator-runtime'
global.localVue = createLocalVue()
global.localVue.use(BootstrapVue)
global.localVue.use(IconsPlugin)
// throw errors for vue warnings to force the programmers to take care about warnings
Vue.config.warnHandler = (w) => {

View File

@ -30,4 +30,6 @@ COMMUNITY_URL=
COMMUNITY_REGISTER_URL=
COMMUNITY_DESCRIPTION=
LOGIN_APP_SECRET=21ffbbc616fe
LOGIN_SERVER_KEY=a51ef8ac7ef1abf162fb7a65261acd7a
LOGIN_SERVER_KEY=a51ef8ac7ef1abf162fb7a65261acd7a
WEBHOOK_ELOPAGE_SECRET=secret

View File

@ -20,6 +20,7 @@
"apollo-server-express": "^2.25.2",
"apollo-server-testing": "^2.25.2",
"axios": "^0.21.1",
"body-parser": "^1.19.0",
"class-validator": "^0.13.1",
"cors": "^2.8.5",
"dotenv": "^10.0.0",

View File

@ -55,9 +55,21 @@ const email = {
process.env.EMAIL_LINK_VERIFICATION || 'http://localhost/vue/checkEmail/$1',
}
const webhook = {
WEBHOOK_ELOPAGE_SECRET: process.env.WEBHOOK_ELOPAGE_SECRET || 'secret',
}
// This is needed by graphql-directive-auth
process.env.APP_SECRET = server.JWT_SECRET
const CONFIG = { ...server, ...database, ...klicktipp, ...community, ...email, ...loginServer }
const CONFIG = {
...server,
...database,
...klicktipp,
...community,
...email,
...loginServer,
...webhook,
}
export default CONFIG

View File

@ -12,10 +12,7 @@ export default class CreateUserArgs {
lastName: string
@Field(() => String)
password: string
@Field(() => String)
language: string
language?: string // Will default to DEFAULT_LANGUAGE
@Field(() => Int, { nullable: true })
publisherId: number

View File

@ -20,6 +20,7 @@ export class User {
this.pubkey = json.public_hex
this.language = json.language
this.publisherId = json.publisher_id
this.isAdmin = json.isAdmin
}
}
@ -48,7 +49,7 @@ export class User {
@Field(() => number)
created: number
@Field(() => Boolean)
@Field(() =>>> Boolean)
emailChecked: boolean
@Field(() => Boolean)
@ -71,6 +72,9 @@ export class User {
@Field(() => Int, { nullable: true })
publisherId?: number
@Field(() => Boolean)
isAdmin: boolean
@Field(() => Boolean)
coinanimation: boolean

View File

@ -194,6 +194,69 @@ const SecretKeyCryptographyDecrypt = (encryptedMessage: Buffer, encryptionKey: B
@Resolver()
export class UserResolver {
/*
@Authorized()
@Query(() => User)
async verifyLogin(@Ctx() context: any): Promise<User> {
const loginUserRepository = getCustomRepository(LoginUserRepository)
loginUser = loginUserRepository.findByPubkeyHex()
const user = new User(result.data.user)
this.email = json.email
this.firstName = json.first_name
this.lastName = json.last_name
this.username = json.username
this.description = json.description
this.pubkey = json.public_hex
this.language = json.language
this.publisherId = json.publisher_id
this.isAdmin = json.isAdmin
const userSettingRepository = getCustomRepository(UserSettingRepository)
const coinanimation = await userSettingRepository
.readBoolean(userEntity.id, Setting.COIN_ANIMATION)
.catch((error) => {
throw new Error(error)
})
user.coinanimation = coinanimation
user.isAdmin = true // TODO implement
return user
}
*/
@Authorized()
@Query(() => User)
@UseMiddleware(klicktippNewsletterStateMiddleware)
async verifyLogin(@Ctx() context: any): Promise<User> {
// TODO refactor and do not have duplicate code with login(see below)
const userRepository = getCustomRepository(UserRepository)
const userEntity = await userRepository.findByPubkeyHex(context.pubKey)
const loginUserRepository = getCustomRepository(LoginUserRepository)
const loginUser = await loginUserRepository.findByEmail(userEntity.email)
const user = new User()
user.email = userEntity.email
user.firstName = userEntity.firstName
user.lastName = userEntity.lastName
user.username = userEntity.username
user.description = loginUser.description
user.pubkey = userEntity.pubkey.toString('hex')
user.language = loginUser.language
// Elopage Status & Stored PublisherId
user.hasElopage = await this.hasElopage(context)
// coinAnimation
const userSettingRepository = getCustomRepository(UserSettingRepository)
const coinanimation = await userSettingRepository
.readBoolean(userEntity.id, Setting.COIN_ANIMATION)
.catch((error) => {
throw new Error(error)
})
user.coinanimation = coinanimation
user.isAdmin = true // TODO implement
return user
}
@Query(() => User)
@UseMiddleware(klicktippNewsletterStateMiddleware)
async login(
@ -266,6 +329,7 @@ export class UserResolver {
throw new Error(error)
})
user.coinanimation = coinanimation
user.isAdmin = true // TODO implement
context.setHeaders.push({
key: 'token',
@ -303,22 +367,23 @@ export class UserResolver {
@Mutation(() => String)
async createUser(
@Args() { email, firstName, lastName, password, language, publisherId }: CreateUserArgs,
@Args() { email, firstName, lastName, language, publisherId }: CreateUserArgs,
): Promise<string> {
// TODO: wrong default value (should be null), how does graphql work here? Is it an required field?
// default int publisher_id = 0;
// Validate Language (no throw)
if (!isLanguage(language)) {
if (!language || !isLanguage(language)) {
language = DEFAULT_LANGUAGE
}
// TODO: Register process
// 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!',
)
}
// if (!isPassword(password)) {
// throw new Error(
// 'Please enter a valid password with at least 8 characters, upper and lower case letters, at least one number and one special character!',
// )
// }
// Validate username
// TODO: never true
@ -336,11 +401,13 @@ export class UserResolver {
throw new Error(`User already exists.`)
}
const passphrase = PassphraseGenerate()
const keyPair = KeyPairEd25519Create(passphrase) // return pub, priv Key
const passwordHash = SecretKeyCryptographyCreateKey(email, password) // return short and long hash
// TODO: Register process
// const passphrase = PassphraseGenerate()
// const keyPair = KeyPairEd25519Create(passphrase) // return pub, priv Key
// const passwordHash = SecretKeyCryptographyCreateKey(email, password) // return short and long hash
// const encryptedPrivkey = SecretKeyCryptographyEncrypt(keyPair[1], passwordHash[1])
const emailHash = getEmailHash(email)
const encryptedPrivkey = SecretKeyCryptographyEncrypt(keyPair[1], passwordHash[1])
// Table: login_users
const loginUser = new LoginUser()
@ -349,13 +416,15 @@ export class UserResolver {
loginUser.lastName = lastName
loginUser.username = username
loginUser.description = ''
loginUser.password = passwordHash[0].readBigUInt64LE() // using the shorthash
// TODO: Register process
// loginUser.password = passwordHash[0].readBigUInt64LE() // using the shorthash
loginUser.emailHash = emailHash
loginUser.language = language
loginUser.groupId = 1
loginUser.publisherId = publisherId
loginUser.pubKey = keyPair[0]
loginUser.privKey = encryptedPrivkey
// TODO: Register process
// loginUser.pubKey = keyPair[0]
// loginUser.privKey = encryptedPrivkey
const queryRunner = getConnection().createQueryRunner()
await queryRunner.connect()
@ -367,21 +436,24 @@ export class UserResolver {
throw new Error('insert user failed')
})
// TODO: Register process
// Table: login_user_backups
const loginUserBackup = new LoginUserBackup()
loginUserBackup.userId = loginUserId
loginUserBackup.passphrase = passphrase.join(' ') + ' ' // login server saves trailing space
loginUserBackup.mnemonicType = 2 // ServerConfig::MNEMONIC_BIP0039_SORTED_ORDER;
// const loginUserBackup = new LoginUserBackup()
// loginUserBackup.userId = loginUserId
// loginUserBackup.passphrase = passphrase.join(' ') + ' ' // login server saves trailing space
// loginUserBackup.mnemonicType = 2 // ServerConfig::MNEMONIC_BIP0039_SORTED_ORDER;
await queryRunner.manager.save(loginUserBackup).catch((error) => {
// eslint-disable-next-line no-console
console.log('insert LoginUserBackup failed', error)
throw new Error('insert user backup failed')
})
// TODO: Register process
// await queryRunner.manager.save(loginUserBackup).catch((error) => {
// // eslint-disable-next-line no-console
// console.log('insert LoginUserBackup failed', error)
// throw new Error('insert user backup failed')
// })
// Table: state_users
const dbUser = new DbUser()
dbUser.pubkey = keyPair[0]
// TODO: Register process
// dbUser.pubkey = keyPair[0]
dbUser.email = email
dbUser.firstName = firstName
dbUser.lastName = lastName

View File

@ -6,6 +6,7 @@ import 'module-alias/register'
import { ApolloServer } from 'apollo-server-express'
import express from 'express'
import bodyParser from 'body-parser'
// database
import connection from '../typeorm/connection'
@ -22,6 +23,9 @@ import CONFIG from '../config'
// graphql
import schema from '../graphql/schema'
// webhooks
import { elopageWebhook } from '../webhook/elopage'
// TODO implement
// import queryComplexity, { simpleEstimator, fieldConfigEstimator } from "graphql-query-complexity";
@ -50,6 +54,12 @@ const createServer = async (context: any = serverContext): Promise<any> => {
// cors
app.use(cors)
// bodyparser
app.use(bodyParser.json())
// Elopage Webhook
app.post('/hook/elopage/' + CONFIG.WEBHOOK_ELOPAGE_SECRET, elopageWebhook)
// Apollo Server
const apollo = new ApolloServer({
schema: await schema(),

File diff suppressed because one or more lines are too long

View File

@ -1552,7 +1552,7 @@ binary-extensions@^2.0.0:
resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d"
integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==
body-parser@1.19.0, body-parser@^1.18.3:
body-parser@1.19.0, body-parser@^1.18.3, body-parser@^1.19.0:
version "1.19.0"
resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.19.0.tgz#96b2709e57c9c4e09a6fd66a8fd979844f69f08a"
integrity sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==

View File

@ -1,166 +1,166 @@
version: "3.4"
services:
########################################################
# FRONTEND #############################################
########################################################
frontend:
image: gradido/frontend:development
build:
target: development
environment:
- NODE_ENV="development"
# - DEBUG=true
volumes:
# This makes sure the docker container has its own node modules.
# Therefore it is possible to have a different node version on the host machine
- frontend_node_modules:/app/node_modules
# bind the local folder to the docker to allow live reload
- ./frontend:/app
########################################################
# ADMIN INTERFACE ######################################
########################################################
admin:
image: gradido/admin:development
build:
target: development
environment:
- NODE_ENV="development"
# - DEBUG=true
volumes:
# This makes sure the docker container has its own node modules.
# Therefore it is possible to have a different node version on the host machine
- admin_node_modules:/app/node_modules
# bind the local folder to the docker to allow live reload
- ./admin:/app
########################################################
# BACKEND ##############################################
########################################################
backend:
image: gradido/backend:development
build:
target: development
networks:
- external-net
- internal-net
environment:
- NODE_ENV="development"
volumes:
# This makes sure the docker container has its own node modules.
# Therefore it is possible to have a different node version on the host machine
- backend_node_modules:/app/node_modules
- backend_database_node_modules:/database/node_modules
- backend_database_build:/database/build
# bind the local folder to the docker to allow live reload
- ./backend:/app
- ./database:/database
########################################################
# DATABASE ##############################################
########################################################
database:
# we always run on production here since else the service lingers
# feel free to change this behaviour if it seems useful
# Due to problems with the volume caching the built files
# we changed this to test build. This keeps the service running.
image: gradido/database:test_up
build:
target: test_up
#networks:
# - external-net
# - internal-net
environment:
- NODE_ENV="development"
volumes:
# This makes sure the docker container has its own node modules.
# Therefore it is possible to have a different node version on the host machine
- database_node_modules:/app/node_modules
- database_build:/app/build
# bind the local folder to the docker to allow live reload
- ./database:/app
#########################################################
## LOGIN SERVER #########################################
#########################################################
login-server:
build:
dockerfile: Dockerfiles/ubuntu/Dockerfile.debug
networks:
- external-net
- internal-net
security_opt:
- seccomp:unconfined
cap_add:
- SYS_PTRACE
volumes:
- ./logs:/var/log/grd_login
- ./login_server/src:/code/src
- ./login_server/dependencies:/code/dependencies
- ./login_server/scripts:/code/scripts
- ./configs/login_server:/etc/grd_login
- login_build_ubuntu_3.1:/code/build
#########################################################
## COMMUNITY SERVER (cakephp with php-fpm) ##############
#########################################################
community-server:
build:
context: .
target: community_server
dockerfile: ./community_server/Dockerfile
depends_on:
- mariadb
networks:
- internal-net
- external-net
volumes:
- ./community_server/config/php-fpm/php-ini-overrides.ini:/etc/php/7.4/fpm/conf.d/99-overrides.ini
- ./community_server/src:/var/www/cakephp/src
#########################################################
## MARIADB ##############################################
#########################################################
mariadb:
networks:
- internal-net
- external-net
#########################################################
## NGINX ################################################
#########################################################
nginx:
depends_on:
- frontend
- community-server
- login-server
volumes:
- ./logs/nginx:/var/log/nginx
#########################################################
## PHPMYADMIN ###########################################
#########################################################
phpmyadmin:
image: phpmyadmin
environment:
- PMA_ARBITRARY=1
#restart: always
ports:
- 8074:80
networks:
- internal-net
- external-net
volumes:
- /sessions
volumes:
frontend_node_modules:
admin_node_modules:
backend_node_modules:
backend_database_node_modules:
backend_database_build:
database_node_modules:
database_build:
version: "3.4"
services:
########################################################
# FRONTEND #############################################
########################################################
frontend:
image: gradido/frontend:development
build:
target: development
environment:
- NODE_ENV="development"
# - DEBUG=true
volumes:
# This makes sure the docker container has its own node modules.
# Therefore it is possible to have a different node version on the host machine
- frontend_node_modules:/app/node_modules
# bind the local folder to the docker to allow live reload
- ./frontend:/app
########################################################
# ADMIN INTERFACE ######################################
########################################################
admin:
image: gradido/admin:development
build:
target: development
environment:
- NODE_ENV="development"
# - DEBUG=true
volumes:
# This makes sure the docker container has its own node modules.
# Therefore it is possible to have a different node version on the host machine
- admin_node_modules:/app/node_modules
# bind the local folder to the docker to allow live reload
- ./admin:/app
########################################################
# BACKEND ##############################################
########################################################
backend:
image: gradido/backend:development
build:
target: development
networks:
- external-net
- internal-net
environment:
- NODE_ENV="development"
volumes:
# This makes sure the docker container has its own node modules.
# Therefore it is possible to have a different node version on the host machine
- backend_node_modules:/app/node_modules
- backend_database_node_modules:/database/node_modules
- backend_database_build:/database/build
# bind the local folder to the docker to allow live reload
- ./backend:/app
- ./database:/database
########################################################
# DATABASE ##############################################
########################################################
database:
# we always run on production here since else the service lingers
# feel free to change this behaviour if it seems useful
# Due to problems with the volume caching the built files
# we changed this to test build. This keeps the service running.
image: gradido/database:test_up
build:
target: test_up
#networks:
# - external-net
# - internal-net
environment:
- NODE_ENV="development"
volumes:
# This makes sure the docker container has its own node modules.
# Therefore it is possible to have a different node version on the host machine
- database_node_modules:/app/node_modules
- database_build:/app/build
# bind the local folder to the docker to allow live reload
- ./database:/app
#########################################################
## LOGIN SERVER #########################################
#########################################################
login-server:
build:
dockerfile: Dockerfiles/ubuntu/Dockerfile.debug
networks:
- external-net
- internal-net
security_opt:
- seccomp:unconfined
cap_add:
- SYS_PTRACE
volumes:
- ./logs:/var/log/grd_login
- ./login_server/src:/code/src
- ./login_server/dependencies:/code/dependencies
- ./login_server/scripts:/code/scripts
- ./configs/login_server:/etc/grd_login
- login_build_ubuntu_3.1:/code/build
#########################################################
## COMMUNITY SERVER (cakephp with php-fpm) ##############
#########################################################
community-server:
build:
context: .
target: community_server
dockerfile: ./community_server/Dockerfile
depends_on:
- mariadb
networks:
- internal-net
- external-net
volumes:
- ./community_server/config/php-fpm/php-ini-overrides.ini:/etc/php/7.4/fpm/conf.d/99-overrides.ini
- ./community_server/src:/var/www/cakephp/src
#########################################################
## MARIADB ##############################################
#########################################################
mariadb:
networks:
- internal-net
- external-net
#########################################################
## NGINX ################################################
#########################################################
nginx:
depends_on:
- frontend
- community-server
- login-server
volumes:
- ./logs/nginx:/var/log/nginx
#########################################################
## PHPMYADMIN ###########################################
#########################################################
phpmyadmin:
image: phpmyadmin
environment:
- PMA_ARBITRARY=1
#restart: always
ports:
- 8074:80
networks:
- internal-net
- external-net
volumes:
- /sessions
volumes:
frontend_node_modules:
admin_node_modules:
backend_node_modules:
backend_database_node_modules:
backend_database_build:
database_node_modules:
database_build:
login_build_ubuntu_3.1:

View File

@ -1,3 +1,4 @@
GRAPHQL_URI=http://localhost:4000/graphql
DEFAULT_PUBLISHER_ID=2896
//BUILD_COMMIT=0000000
#BUILD_COMMIT=0000000
ADMIN_AUTH_URL=http://localhost/admin/authenticate?token=$1

View File

@ -1,21 +0,0 @@
DEV README von Alex
default Page:
´´´
<template>
<div>default</div>
</template>
<script>
export default {
name: 'default',
data() {
return {}
},
methods: {},
watch: {},
}
</script>
´´´

View File

@ -1,9 +0,0 @@
<!--
IMPORTANT: Please use the following link to create a new issue:
https://www.gradido.net/new-issue/bootstrap-vue-gradido-wallet
**If your issue was not created using the app above, it will be closed immediately.**
-->

View File

@ -385,4 +385,13 @@ TODO: Update GDT-Server um paging und Zugriff auf alle Einträge zu erhalten, op
GET https://staging.gradido.net/state-balances/ajaxGdtTransactions
Liefert wenn alles in Ordnung ist:
wenn nicht type 7 dann "amount" in euro ansonsten in GDT
wenn nicht type 7 dann "amount" in euro ansonsten in GDT
## Additional Software
For `yarn locales` you will need `jq` to use it.
You can install it (on arch) via
```
sudo pacman -S jq
```

View File

@ -3,6 +3,8 @@ import SideBar from './SideBar'
const localVue = global.localVue
const storeDispatchMock = jest.fn()
describe('SideBar', () => {
let wrapper
@ -23,7 +25,7 @@ describe('SideBar', () => {
lastName: 'example',
hasElopage: false,
},
commit: jest.fn(),
dispatch: storeDispatchMock,
},
$i18n: {
locale: 'en',
@ -154,6 +156,42 @@ describe('SideBar', () => {
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'
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

@ -45,11 +45,20 @@
<slot name="links"></slot>
</ul>
<hr class="my-2" />
<ul class="navbar-nav ml-3">
<li class="nav-item">
<a :href="getElopageLink()" class="nav-link" target="_blank">
{{ $t('members_area') }}&nbsp;
<b-badge v-if="!this.$store.state.hasElopage" pill variant="danger">!</b-badge>
<b-badge v-if="!$store.state.hasElopage" pill variant="danger">!</b-badge>
</a>
</li>
</ul>
<ul class="navbar-nav ml-3" v-if="$store.state.isAdmin">
<li class="nav-item">
<a class="nav-link pointer" @click="admin">
{{ $t('admin_area') }}
</a>
</li>
</ul>
@ -112,6 +121,10 @@ export default {
logout() {
this.$emit('logout')
},
admin() {
window.location.assign(CONFIG.ADMIN_AUTH_URL.replace('$1', this.$store.state.token))
this.$store.dispatch('logout') // logout without redirect
},
getElopageLink() {
const pId = this.$store.state.publisherId
? this.$store.state.publisherId

View File

@ -18,8 +18,9 @@ const environment = {
DEFAULT_PUBLISHER_ID: process.env.DEFAULT_PUBLISHER_ID || 2896,
}
const server = {
const endpoints = {
GRAPHQL_URI: process.env.GRAPHQL_URI || 'http://localhost:4000/graphql',
ADMIN_AUTH_URL: process.env.ADMIN_AUTH_URL || 'http://localhost/admin/authenticate?token=$1',
}
const options = {}
@ -27,7 +28,7 @@ const options = {}
const CONFIG = {
...version,
...environment,
...server,
...endpoints,
...options,
}

View File

@ -15,6 +15,27 @@ export const login = gql`
}
hasElopage
publisherId
isAdmin
}
}
`
export const verifyLogin = gql`
query {
verifyLogin {
email
username
firstName
lastName
language
description
coinanimation
klickTipp {
newsletterState
}
hasElopage
publisherId
isAdmin
}
}
`

View File

@ -1,4 +1,5 @@
{
"admin_area": "Adminbereich",
"back": "Zurück",
"community": {
"choose-another-community": "Eine andere Gemeinschaft auswählen",

View File

@ -1,4 +1,5 @@
{
"admin_area": "Admin's area",
"back": "Back",
"community": {
"choose-another-community": "Choose another community",

View File

@ -51,7 +51,7 @@ Vue.config.productionTip = false
loadAllRules(i18n)
addNavigationGuards(router, store)
addNavigationGuards(router, store, apolloProvider.defaultClient)
/* eslint-disable no-new */
new Vue({

View File

@ -1,12 +1,34 @@
const addNavigationGuards = (router, store) => {
import { verifyLogin } from '../graphql/queries'
const addNavigationGuards = (router, store, apollo) => {
// handle publisherId
router.beforeEach((to, from, next) => {
// handle publisherId
const publisherId = to.query.pid
if (publisherId) {
store.commit('publisherId', publisherId)
delete to.query.pid
}
// handle authentication
next()
})
// store token on authenticate
router.beforeEach(async (to, from, next) => {
if (to.path === '/authenticate' && to.query.token) {
// TODO verify user in order to get user data
store.commit('token', to.query.token)
const result = await apollo.query({
query: verifyLogin,
fetchPolicy: 'network-only',
})
store.dispatch('login', result.data.verifyLogin)
next({ path: '/overview' })
} else {
next()
}
})
// handle authentication
router.beforeEach((to, from, next) => {
if (to.meta.requiresAuth && !store.state.token) {
next({ path: '/login' })
} else {

View File

@ -30,7 +30,7 @@ describe('navigation guards', () => {
})
describe('authorization', () => {
const navGuard = router.beforeHooks[0]
const navGuard = router.beforeHooks[2]
const next = jest.fn()
it('redirects to login when not authorized', () => {

View File

@ -49,8 +49,8 @@ describe('router', () => {
expect(routes.find((r) => r.path === '/').redirect()).toEqual({ path: '/login' })
})
it('has fourteen routes defined', () => {
expect(routes).toHaveLength(14)
it('has fifteen routes defined', () => {
expect(routes).toHaveLength(15)
})
describe('overview', () => {

View File

@ -1,6 +1,9 @@
import NotFound from '@/views/NotFoundPage.vue'
const routes = [
{
path: '/authenticate',
},
{
path: '/',
redirect: (to) => {

View File

@ -34,6 +34,9 @@ export const mutations = {
if (isNaN(pubId)) pubId = null
state.publisherId = pubId
},
isAdmin: (state, isAdmin) => {
state.isAdmin = !!isAdmin
},
community: (state, community) => {
state.community = community
},
@ -57,6 +60,7 @@ export const actions = {
commit('newsletterState', data.klickTipp.newsletterState)
commit('hasElopage', data.hasElopage)
commit('publisherId', data.publisherId)
commit('isAdmin', data.isAdmin)
},
logout: ({ commit, state }) => {
commit('token', null)
@ -69,6 +73,7 @@ export const actions = {
commit('newsletterState', null)
commit('hasElopage', false)
commit('publisherId', null)
commit('isAdmin', false)
localStorage.clear()
},
}
@ -87,6 +92,7 @@ export const store = new Vuex.Store({
username: '',
description: '',
token: null,
isAdmin: false,
coinanimation: true,
newsletterState: null,
community: {

View File

@ -148,11 +148,12 @@ describe('Vuex store', () => {
},
hasElopage: false,
publisherId: 1234,
isAdmin: true,
}
it('calls ten commits', () => {
it('calls eleven commits', () => {
login({ commit, state }, commitedData)
expect(commit).toHaveBeenCalledTimes(10)
expect(commit).toHaveBeenCalledTimes(11)
})
it('commits email', () => {
@ -204,15 +205,20 @@ describe('Vuex store', () => {
login({ commit, state }, commitedData)
expect(commit).toHaveBeenNthCalledWith(10, 'publisherId', 1234)
})
it('commits isAdmin', () => {
login({ commit, state }, commitedData)
expect(commit).toHaveBeenNthCalledWith(11, 'isAdmin', true)
})
})
describe('logout', () => {
const commit = jest.fn()
const state = {}
it('calls ten commits', () => {
it('calls eleven commits', () => {
logout({ commit, state })
expect(commit).toHaveBeenCalledTimes(10)
expect(commit).toHaveBeenCalledTimes(11)
})
it('commits token', () => {
@ -265,6 +271,11 @@ describe('Vuex store', () => {
expect(commit).toHaveBeenNthCalledWith(10, 'publisherId', null)
})
it('commits isAdmin', () => {
logout({ commit, state })
expect(commit).toHaveBeenNthCalledWith(11, 'isAdmin', false)
})
// how to get this working?
it.skip('calls localStorage.clear()', () => {
const clearStorageMock = jest.fn()