mirror of
https://github.com/IT4Change/gradido.git
synced 2025-12-13 07:45:54 +00:00
Merge pull request #1125 from gradido/login_admin_interface
Login admin interface
This commit is contained in:
commit
67594ce6ee
2
.github/workflows/test.yml
vendored
2
.github/workflows/test.yml
vendored
@ -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 }}
|
||||
|
||||
##############################################################################
|
||||
|
||||
3
admin/.env.dist
Normal file
3
admin/.env.dist
Normal file
@ -0,0 +1,3 @@
|
||||
GRAPHQL_URI=http://localhost:4000/graphql
|
||||
WALLET_AUTH_URL=http://localhost/vue/authenticate?token=$1
|
||||
DEBUG_DISABLE_AUTH=false
|
||||
@ -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', () => {
|
||||
|
||||
@ -1,19 +1,15 @@
|
||||
<template>
|
||||
<div id="app">
|
||||
<nav-bar class="wrapper-nav" />
|
||||
<router-view class="wrapper p-3"></router-view>
|
||||
<content-footer />
|
||||
<default-layout v-if="$store.state.token" />
|
||||
<router-view v-else></router-view>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import NavBar from '@/components/NavBar.vue'
|
||||
import ContentFooter from '@/components/ContentFooter.vue'
|
||||
import defaultLayout from '@/layouts/defaultLayout.vue'
|
||||
|
||||
export default {
|
||||
name: 'App',
|
||||
components: {
|
||||
NavBar,
|
||||
ContentFooter,
|
||||
},
|
||||
name: 'app',
|
||||
components: { defaultLayout },
|
||||
}
|
||||
</script>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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
|
||||
|
||||
19
admin/src/layouts/defaultLayout.vue
Normal file
19
admin/src/layouts/defaultLayout.vue
Normal 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>
|
||||
@ -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()
|
||||
|
||||
64
admin/src/router/guards.test.js
Normal file
64
admin/src/router/guards.test.js
Normal 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()
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -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')
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@ -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: '*',
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import Vuex from 'vuex'
|
||||
import Vue from 'vue'
|
||||
import createPersistedState from 'vuex-persistedstate'
|
||||
import CONFIG from '../config'
|
||||
|
||||
Vue.use(Vuex)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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
|
||||
@ -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>
|
||||
|
||||
´´´
|
||||
|
||||
@ -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.**
|
||||
-->
|
||||
|
||||
|
||||
@ -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
|
||||
```
|
||||
@ -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')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -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') }}
|
||||
<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
|
||||
|
||||
@ -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,
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
{
|
||||
"admin_area": "Adminbereich",
|
||||
"back": "Zurück",
|
||||
"community": {
|
||||
"choose-another-community": "Eine andere Gemeinschaft auswählen",
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
{
|
||||
"admin_area": "Admin's area",
|
||||
"back": "Back",
|
||||
"community": {
|
||||
"choose-another-community": "Choose another community",
|
||||
|
||||
@ -51,7 +51,7 @@ Vue.config.productionTip = false
|
||||
|
||||
loadAllRules(i18n)
|
||||
|
||||
addNavigationGuards(router, store)
|
||||
addNavigationGuards(router, store, apolloProvider.defaultClient)
|
||||
|
||||
/* eslint-disable no-new */
|
||||
new Vue({
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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', () => {
|
||||
|
||||
@ -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', () => {
|
||||
|
||||
@ -1,6 +1,9 @@
|
||||
import NotFound from '@/views/NotFoundPage.vue'
|
||||
|
||||
const routes = [
|
||||
{
|
||||
path: '/authenticate',
|
||||
},
|
||||
{
|
||||
path: '/',
|
||||
redirect: (to) => {
|
||||
|
||||
@ -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: {
|
||||
|
||||
@ -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()
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user