Merge branch 'master' into backend_rights

# Conflicts:
#	admin/src/router/guards.js
#	admin/src/store/store.js
#	backend/src/graphql/resolver/UserResolver.ts
#	frontend/src/components/SidebarPlugin/SideBar.vue
#	frontend/src/graphql/queries.js
#	frontend/src/routes/guards.js
This commit is contained in:
Ulf Gebhardt 2021-11-25 08:49:06 +01:00
commit 77dc54155a
Signed by: ulfgebhardt
GPG Key ID: DA6B843E748679C9
14 changed files with 226 additions and 34 deletions

View File

@ -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: 47 min_coverage: 51
token: ${{ github.token }} token: ${{ github.token }}
############################################################################## ##############################################################################

View File

@ -7,11 +7,19 @@ const stubs = {
RouterView: true, RouterView: true,
} }
const mocks = {
$store: {
state: {
token: null,
},
},
}
describe('App', () => { describe('App', () => {
let wrapper let wrapper
const Wrapper = () => { const Wrapper = () => {
return shallowMount(App, { localVue, stubs }) return shallowMount(App, { localVue, stubs, mocks })
} }
describe('shallowMount', () => { describe('shallowMount', () => {

View File

@ -3,7 +3,7 @@ import CONFIG from '../config'
const addNavigationGuards = (router, store) => { const addNavigationGuards = (router, store) => {
// store token on `authenticate` // store token on `authenticate`
router.beforeEach((to, from, next) => { router.beforeEach((to, from, next) => {
if (to.path === '/authenticate' && to.query.token) { if (to.path === '/authenticate' && to.query && to.query.token) {
// TODO verify user to get user data // TODO verify user to get user data
store.commit('token', to.query.token) store.commit('token', to.query.token)
next({ path: '/' }) next({ path: '/' })

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

@ -44,19 +44,19 @@ describe('router', () => {
}) })
describe('routes', () => { describe('routes', () => {
it('has seven routes defined', () => {
expect(routes).toHaveLength(7)
})
it('has "/overview" as default', async () => { it('has "/overview" as default', async () => {
const component = await routes.find((r) => r.path === '/').component() const component = await routes.find((r) => r.path === '/').component()
expect(component.default.name).toBe('overview') expect(component.default.name).toBe('overview')
}) })
it('has fourteen routes defined', () => { describe('logout', () => {
expect(routes).toHaveLength(6) it('loads the "NotFoundPage" component', async () => {
}) const component = await routes.find((r) => r.path === '/logout').component()
expect(component.default.name).toBe('not-found')
describe('overview', () => {
it('loads the "Overview" component', async () => {
const component = await routes.find((r) => r.path === '/overview').component()
expect(component.default.name).toBe('overview')
}) })
}) })

View File

@ -23,7 +23,7 @@ export const mutations = {
export const actions = { export const actions = {
logout: ({ commit, state }) => { logout: ({ commit, state }) => {
commit('token', null) commit('token', null)
localStorage.clear() window.localStorage.clear()
}, },
} }

View File

@ -1,6 +1,11 @@
import { mutations } from './store' import store, { mutations, actions } from './store'
const { token, openCreationsPlus, openCreationsMinus, resetOpenCreations } = mutations const { token, openCreationsPlus, openCreationsMinus, resetOpenCreations } = mutations
const { logout } = actions
const CONFIG = {
DEBUG_DISABLE_AUTH: true,
}
describe('Vuex store', () => { describe('Vuex store', () => {
describe('mutations', () => { describe('mutations', () => {
@ -36,4 +41,43 @@ describe('Vuex store', () => {
}) })
}) })
}) })
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

@ -49,7 +49,7 @@ export class User {
@Field(() => number) @Field(() => number)
created: number created: number
@Field(() => Boolean) @Field(() =>>> Boolean)
emailChecked: boolean emailChecked: boolean
@Field(() => Boolean) @Field(() => Boolean)

View File

@ -195,24 +195,28 @@ const SecretKeyCryptographyDecrypt = (encryptedMessage: Buffer, encryptionKey: B
@Resolver() @Resolver()
export class UserResolver { export class UserResolver {
/*
@Authorized() @Authorized()
@Query(() => User) @Query(() => User)
@UseMiddleware(klicktippNewsletterStateMiddleware)
async verifyLogin(@Ctx() context: any): Promise<User> { 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 loginUserRepository = getCustomRepository(LoginUserRepository)
loginUser = loginUserRepository.findByPubkeyHex() const loginUser = await loginUserRepository.findByEmail(userEntity.email)
const user = new User(result.data.user) 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
this.email = json.email // Elopage Status & Stored PublisherId
this.firstName = json.first_name user.hasElopage = await this.hasElopage(context)
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
// coinAnimation
const userSettingRepository = getCustomRepository(UserSettingRepository) const userSettingRepository = getCustomRepository(UserSettingRepository)
const coinanimation = await userSettingRepository const coinanimation = await userSettingRepository
.readBoolean(userEntity.id, Setting.COIN_ANIMATION) .readBoolean(userEntity.id, Setting.COIN_ANIMATION)
@ -223,7 +227,6 @@ export class UserResolver {
user.isAdmin = true // TODO implement user.isAdmin = true // TODO implement
return user return user
} }
*/
@Authorized([RIGHTS.LOGIN]) @Authorized([RIGHTS.LOGIN])
@Query(() => User) @Query(() => User)
@ -298,6 +301,7 @@ export class UserResolver {
throw new Error(error) throw new Error(error)
}) })
user.coinanimation = coinanimation user.coinanimation = coinanimation
user.isAdmin = true // TODO implement
user.isAdmin = true // TODO implement user.isAdmin = true // TODO implement

View File

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

@ -50,7 +50,15 @@
<li class="nav-item"> <li class="nav-item">
<a :href="getElopageLink()" class="nav-link" target="_blank"> <a :href="getElopageLink()" class="nav-link" target="_blank">
{{ $t('members_area') }}&nbsp; {{ $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> </a>
</li> </li>
</ul> </ul>
@ -122,7 +130,7 @@ export default {
this.$emit('logout') this.$emit('logout')
}, },
admin() { admin() {
window.location = CONFIG.ADMIN_AUTH_URL.replace('$1', this.$store.state.token) window.location.assign(CONFIG.ADMIN_AUTH_URL.replace('$1', this.$store.state.token))
this.$store.dispatch('logout') // logout without redirect this.$store.dispatch('logout') // logout without redirect
}, },
getElopageLink() { getElopageLink() {

View File

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

View File

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

View File

@ -1,4 +1,6 @@
const addNavigationGuards = (router, store) => { import { verifyLogin } from '../graphql/queries'
const addNavigationGuards = (router, store, apollo) => {
// handle publisherId // handle publisherId
router.beforeEach((to, from, next) => { router.beforeEach((to, from, next) => {
const publisherId = to.query.pid const publisherId = to.query.pid
@ -10,10 +12,14 @@ const addNavigationGuards = (router, store) => {
}) })
// store token on authenticate // store token on authenticate
router.beforeEach((to, from, next) => { router.beforeEach(async (to, from, next) => {
if (to.path === '/authenticate' && to.query.token) { if (to.path === '/authenticate' && to.query.token) {
// TODO verify user in order to get user data
store.commit('token', to.query.token) 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' }) next({ path: '/overview' })
} else { } else {
next() next()