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
type: lcov
result_path: ./coverage/lcov.info
min_coverage: 47
min_coverage: 51
token: ${{ github.token }}
##############################################################################

View File

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

View File

@ -3,7 +3,7 @@ import CONFIG from '../config'
const addNavigationGuards = (router, store) => {
// store token on `authenticate`
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
store.commit('token', to.query.token)
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', () => {
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')
})
it('has fourteen routes defined', () => {
expect(routes).toHaveLength(6)
})
describe('overview', () => {
it('loads the "Overview" component', async () => {
const component = await routes.find((r) => r.path === '/overview').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')
})
})

View File

@ -23,7 +23,7 @@ export const mutations = {
export const actions = {
logout: ({ commit, state }) => {
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 { logout } = actions
const CONFIG = {
DEBUG_DISABLE_AUTH: true,
}
describe('Vuex store', () => {
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)
created: number
@Field(() => Boolean)
@Field(() =>>> Boolean)
emailChecked: boolean
@Field(() => Boolean)

View File

@ -195,24 +195,28 @@ const SecretKeyCryptographyDecrypt = (encryptedMessage: Buffer, encryptionKey: B
@Resolver()
export class UserResolver {
/*
@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)
loginUser = loginUserRepository.findByPubkeyHex()
const user = new User(result.data.user)
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
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
// 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)
@ -223,7 +227,6 @@ export class UserResolver {
user.isAdmin = true // TODO implement
return user
}
*/
@Authorized([RIGHTS.LOGIN])
@Query(() => User)
@ -298,6 +301,7 @@ export class UserResolver {
throw new Error(error)
})
user.coinanimation = coinanimation
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 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

@ -50,7 +50,15 @@
<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>
@ -122,7 +130,7 @@ export default {
this.$emit('logout')
},
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
},
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`
query {
logout

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,4 +1,6 @@
const addNavigationGuards = (router, store) => {
import { verifyLogin } from '../graphql/queries'
const addNavigationGuards = (router, store, apollo) => {
// handle publisherId
router.beforeEach((to, from, next) => {
const publisherId = to.query.pid
@ -10,10 +12,14 @@ const addNavigationGuards = (router, store) => {
})
// store token on authenticate
router.beforeEach((to, from, next) => {
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()