mirror of
https://github.com/IT4Change/gradido.git
synced 2026-02-06 09:56:05 +00:00
Merge branch 'master' into release-2_2_1-merge
This commit is contained in:
commit
2c8f8df919
@ -9,7 +9,7 @@ module.exports = {
|
||||
],
|
||||
coverageThreshold: {
|
||||
global: {
|
||||
lines: 96,
|
||||
lines: 95,
|
||||
},
|
||||
},
|
||||
moduleFileExtensions: [
|
||||
@ -31,6 +31,9 @@ module.exports = {
|
||||
setupFiles: ['<rootDir>/test/testSetup.js', 'jest-canvas-mock'],
|
||||
testMatch: ['**/?(*.)+(spec|test).js?(x)'],
|
||||
// snapshotSerializers: ['jest-serializer-vue'],
|
||||
transformIgnorePatterns: ['<rootDir>/node_modules/(?!vee-validate/dist/rules)'],
|
||||
transformIgnorePatterns: [
|
||||
'<rootDir>/node_modules/(?!vee-validate/dist/rules)',
|
||||
'/node_modules/(?!@babel)',
|
||||
],
|
||||
testEnvironment: 'jest-environment-jsdom-sixteen', // why this is still needed? should not be needed anymore since jest@26, see: https://www.npmjs.com/package/jest-environment-jsdom-sixteen
|
||||
}
|
||||
|
||||
@ -16,6 +16,7 @@
|
||||
"lint": "eslint --max-warnings=0 --ext .js,.vue,.json .",
|
||||
"stylelint": "stylelint --max-warnings=0 '**/*.{scss,vue}'",
|
||||
"test": "cross-env TZ=UTC jest",
|
||||
"test:debug": "node --inspect-brk node_modules/.bin/vue-cli-service test:unit --no-cache --watch --runInBand",
|
||||
"locales": "scripts/sort.sh"
|
||||
},
|
||||
"dependencies": {
|
||||
|
||||
@ -1,63 +0,0 @@
|
||||
<template>
|
||||
<div class="federation-visualize-item">
|
||||
<b-row>
|
||||
<b-col cols="1"><b-icon :icon="icon" :variant="variant" class="mr-4"></b-icon></b-col>
|
||||
<b-col>
|
||||
<div>{{ item.url }}</div>
|
||||
<small>{{ `${item.publicKey.substring(0, 26)}…` }}</small>
|
||||
</b-col>
|
||||
<b-col cols="2">{{ lastAnnouncedAt }}</b-col>
|
||||
<b-col cols="2">{{ createdAt }}</b-col>
|
||||
</b-row>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { formatDistanceToNow } from 'date-fns'
|
||||
import { de, enUS as en, fr, es, nl } from 'date-fns/locale'
|
||||
|
||||
const locales = { en, de, es, fr, nl }
|
||||
|
||||
export default {
|
||||
name: 'FederationVisualizeItem',
|
||||
props: {
|
||||
item: { type: Object },
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
formatDistanceToNow,
|
||||
locale: this.$i18n.locale,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
verified() {
|
||||
return new Date(this.item.verifiedAt) >= new Date(this.item.lastAnnouncedAt)
|
||||
},
|
||||
icon() {
|
||||
return this.verified ? 'check' : 'x-circle'
|
||||
},
|
||||
variant() {
|
||||
return this.verified ? 'success' : 'danger'
|
||||
},
|
||||
lastAnnouncedAt() {
|
||||
if (this.item.lastAnnouncedAt) {
|
||||
return formatDistanceToNow(new Date(this.item.lastAnnouncedAt), {
|
||||
includeSecond: true,
|
||||
addSuffix: true,
|
||||
locale: locales[this.locale],
|
||||
})
|
||||
}
|
||||
return ''
|
||||
},
|
||||
createdAt() {
|
||||
if (this.item.createdAt) {
|
||||
return formatDistanceToNow(new Date(this.item.createdAt), {
|
||||
includeSecond: true,
|
||||
addSuffix: true,
|
||||
locale: locales[this.locale],
|
||||
})
|
||||
}
|
||||
return ''
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@ -1,36 +1,83 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import FederationVisualizeItem from './FederationVisualizeItem.vue'
|
||||
import Vuex from 'vuex'
|
||||
import CommunityVisualizeItem from './CommunityVisualizeItem.vue'
|
||||
|
||||
const localVue = global.localVue
|
||||
localVue.use(Vuex)
|
||||
const today = new Date()
|
||||
const createdDate = new Date()
|
||||
createdDate.setDate(createdDate.getDate() - 3)
|
||||
|
||||
// Mock für den Vuex-Store
|
||||
const store = new Vuex.Store({
|
||||
state: {
|
||||
moderator: {
|
||||
roles: ['ADMIN'],
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
let propsData = {
|
||||
item: {
|
||||
id: 7590,
|
||||
id: 1,
|
||||
foreign: false,
|
||||
publicKey: 'eaf6a426b24fd54f8fbae11c17700fc595080ca25159579c63d38dbc64284ba7',
|
||||
url: 'http://localhost/api/2_0',
|
||||
lastAnnouncedAt: createdDate,
|
||||
verifiedAt: today,
|
||||
lastErrorAt: null,
|
||||
url: 'http://localhost/api/',
|
||||
publicKey: '4007170edd8d33fb009cd99ee4e87f214e7cd21b668d45540a064deb42e243c2',
|
||||
communityUuid: '5ab0befd-b150-4f31-a631-7f3637e47b21',
|
||||
authenticatedAt: null,
|
||||
name: 'Gradido Test',
|
||||
description: 'Gradido Community zum testen',
|
||||
gmsApiKey: '<api key>',
|
||||
creationDate: createdDate,
|
||||
createdAt: createdDate,
|
||||
updatedAt: null,
|
||||
updatedAt: createdDate,
|
||||
federatedCommunities: [
|
||||
{
|
||||
id: 2046,
|
||||
apiVersion: '2_0',
|
||||
endPoint: 'http://localhost/api/',
|
||||
lastAnnouncedAt: createdDate,
|
||||
verifiedAt: today,
|
||||
lastErrorAt: null,
|
||||
createdAt: createdDate,
|
||||
updatedAt: null,
|
||||
},
|
||||
{
|
||||
id: 2045,
|
||||
apiVersion: '1_1',
|
||||
endPoint: 'http://localhost/api/',
|
||||
lastAnnouncedAt: null,
|
||||
verifiedAt: null,
|
||||
lastErrorAt: null,
|
||||
createdAt: '2024-01-16T10:08:21.550Z',
|
||||
updatedAt: null,
|
||||
},
|
||||
{
|
||||
id: 2044,
|
||||
apiVersion: '1_0',
|
||||
endPoint: 'http://localhost/api/',
|
||||
lastAnnouncedAt: null,
|
||||
verifiedAt: null,
|
||||
lastErrorAt: null,
|
||||
createdAt: '2024-01-16T10:08:21.544Z',
|
||||
updatedAt: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
const mocks = {
|
||||
$t: (key) => key,
|
||||
$i18n: {
|
||||
locale: 'en',
|
||||
},
|
||||
}
|
||||
|
||||
describe('FederationVisualizeItem', () => {
|
||||
describe('CommunityVisualizeItem', () => {
|
||||
let wrapper
|
||||
|
||||
const Wrapper = () => {
|
||||
return mount(FederationVisualizeItem, { localVue, mocks, propsData })
|
||||
return mount(CommunityVisualizeItem, { localVue, mocks, propsData, store })
|
||||
}
|
||||
|
||||
describe('mount', () => {
|
||||
@ -39,19 +86,35 @@ describe('FederationVisualizeItem', () => {
|
||||
})
|
||||
|
||||
it('renders the component', () => {
|
||||
expect(wrapper.find('div.federation-visualize-item').exists()).toBe(true)
|
||||
expect(wrapper.exists()).toBe(true)
|
||||
expect(wrapper.find('div.community-visualize-item').exists()).toBe(true)
|
||||
expect(wrapper.find('.details').exists()).toBe(false)
|
||||
})
|
||||
|
||||
it('toggles details on click', async () => {
|
||||
// Click the row to toggle details
|
||||
await wrapper.find('.row').trigger('click')
|
||||
|
||||
// Assert that details are now open
|
||||
expect(wrapper.find('.details').exists()).toBe(true)
|
||||
|
||||
// Click the row again to toggle details back
|
||||
await wrapper.find('.row').trigger('click')
|
||||
|
||||
// Assert that details are now closed
|
||||
expect(wrapper.find('.details').exists()).toBe(false)
|
||||
})
|
||||
|
||||
describe('rendering item properties', () => {
|
||||
it('has the url', () => {
|
||||
expect(wrapper.find('.row > div:nth-child(2) > div').text()).toBe(
|
||||
'http://localhost/api/2_0',
|
||||
expect(wrapper.find('.row > div:nth-child(2) > div > a').text()).toBe(
|
||||
'http://localhost/api/',
|
||||
)
|
||||
})
|
||||
|
||||
it('has the public key', () => {
|
||||
expect(wrapper.find('.row > div:nth-child(2) > small').text()).toContain(
|
||||
'eaf6a426b24fd54f8fbae11c17700fc595080ca25159579c63d38dbc64284ba7'.substring(0, 26),
|
||||
'4007170edd8d33fb009cd99ee4e87f214e7cd21b668d45540a064deb42e243c2'.substring(0, 26),
|
||||
)
|
||||
})
|
||||
|
||||
@ -65,33 +128,6 @@ describe('FederationVisualizeItem', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('not verified item', () => {
|
||||
beforeEach(() => {
|
||||
propsData = {
|
||||
item: {
|
||||
id: 7590,
|
||||
foreign: false,
|
||||
publicKey: 'eaf6a426b24fd54f8fbae11c17700fc595080ca25159579c63d38dbc64284ba7',
|
||||
url: 'http://localhost/api/2_0',
|
||||
lastAnnouncedAt: createdDate,
|
||||
verifiedAt: null,
|
||||
lastErrorAt: null,
|
||||
createdAt: createdDate,
|
||||
updatedAt: null,
|
||||
},
|
||||
}
|
||||
wrapper = Wrapper()
|
||||
})
|
||||
|
||||
it('has the x-circle icon', () => {
|
||||
expect(wrapper.find('svg.bi-x-circle').exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('has the text variant "danger"', () => {
|
||||
expect(wrapper.find('.text-danger').exists()).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
// describe('with different locales (de, en, fr, es, nl)', () => {
|
||||
describe('lastAnnouncedAt', () => {
|
||||
it('computes the time string for different locales (de, en, fr, es, nl)', () => {
|
||||
@ -155,6 +191,30 @@ describe('FederationVisualizeItem', () => {
|
||||
expect(wrapper.vm.createdAt).toBe('3 dagen geleden')
|
||||
})
|
||||
|
||||
describe('not verified item', () => {
|
||||
beforeEach(() => {
|
||||
propsData = {
|
||||
item: {
|
||||
id: 7590,
|
||||
foreign: false,
|
||||
publicKey: 'eaf6a426b24fd54f8fbae11c17700fc595080ca25159579c63d38dbc64284ba7',
|
||||
url: 'http://localhost/api/',
|
||||
createdAt: createdDate,
|
||||
updatedAt: null,
|
||||
},
|
||||
}
|
||||
wrapper = Wrapper()
|
||||
})
|
||||
|
||||
it('has the x-circle icon', () => {
|
||||
expect(wrapper.find('svg.bi-x-circle').exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('has the text variant "danger"', () => {
|
||||
expect(wrapper.find('.text-danger').exists()).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('createdAt == null', () => {
|
||||
beforeEach(() => {
|
||||
propsData = {
|
||||
@ -163,9 +223,9 @@ describe('FederationVisualizeItem', () => {
|
||||
foreign: false,
|
||||
publicKey: 'eaf6a426b24fd54f8fbae11c17700fc595080ca25159579c63d38dbc64284ba7',
|
||||
url: 'http://localhost/api/2_0',
|
||||
lastAnnouncedAt: createdDate,
|
||||
verifiedAt: null,
|
||||
lastErrorAt: null,
|
||||
communityUuid: '5ab0befd-b150-4f31-a631-7f3637e47b21',
|
||||
authenticatedAt: null,
|
||||
creationDate: null,
|
||||
createdAt: null,
|
||||
updatedAt: null,
|
||||
},
|
||||
160
admin/src/components/Federation/CommunityVisualizeItem.vue
Normal file
160
admin/src/components/Federation/CommunityVisualizeItem.vue
Normal file
@ -0,0 +1,160 @@
|
||||
<template>
|
||||
<div class="community-visualize-item">
|
||||
<b-row @click="toggleDetails">
|
||||
<b-col cols="1"><b-icon :icon="icon" :variant="variant" class="mr-4"></b-icon></b-col>
|
||||
<b-col>
|
||||
<div>
|
||||
<a :href="item.url" target="_blank">{{ item.url }}</a>
|
||||
</div>
|
||||
<small>{{ `${item.publicKey.substring(0, 26)}…` }}</small>
|
||||
</b-col>
|
||||
<b-col v-b-tooltip="item.description">{{ item.name }}</b-col>
|
||||
<b-col cols="2">{{ lastAnnouncedAt }}</b-col>
|
||||
<b-col cols="2">{{ createdAt }}</b-col>
|
||||
</b-row>
|
||||
<b-row v-if="details" class="details">
|
||||
<b-col colspan="5">
|
||||
<b-list-group>
|
||||
<b-list-group-item v-if="item.uuid">
|
||||
{{ $t('federation.communityUuid') }} {{ item.uuid }}
|
||||
</b-list-group-item>
|
||||
<b-list-group-item v-if="item.authenticatedAt">
|
||||
{{ $t('federation.authenticatedAt') }} {{ item.authenticatedAt }}
|
||||
</b-list-group-item>
|
||||
<b-list-group-item>
|
||||
{{ $t('federation.publicKey') }} {{ item.publicKey }}
|
||||
</b-list-group-item>
|
||||
<b-list-group-item v-if="!item.foreign">
|
||||
{{ $t('federation.gmsApiKey') }}
|
||||
<editable-label
|
||||
:value="gmsApiKey"
|
||||
:allowEdit="$store.state.moderator.roles.includes('ADMIN')"
|
||||
@save="handleSaveGsmApiKey"
|
||||
/>
|
||||
</b-list-group-item>
|
||||
<b-list-group-item>
|
||||
<b-list-group>
|
||||
<b-row>
|
||||
<b-col class="ml-1">{{ $t('federation.verified') }}</b-col>
|
||||
<b-col>{{ $t('federation.apiVersion') }}</b-col>
|
||||
<b-col>{{ $t('federation.createdAt') }}</b-col>
|
||||
<b-col>{{ $t('federation.lastAnnouncedAt') }}</b-col>
|
||||
<b-col>{{ $t('federation.verifiedAt') }}</b-col>
|
||||
<b-col>{{ $t('federation.lastErrorAt') }}</b-col>
|
||||
</b-row>
|
||||
<b-list-group-item
|
||||
v-for="federation in item.federatedCommunities"
|
||||
:key="federation.id"
|
||||
:variant="!item.foreign ? 'primary' : 'warning'"
|
||||
>
|
||||
<federation-visualize-item :item="federation" />
|
||||
</b-list-group-item>
|
||||
</b-list-group>
|
||||
</b-list-group-item>
|
||||
</b-list-group>
|
||||
</b-col>
|
||||
</b-row>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { formatDistanceToNow } from 'date-fns'
|
||||
import { de, enUS as en, fr, es, nl } from 'date-fns/locale'
|
||||
import EditableLabel from '@/components/input/EditableLabel'
|
||||
import FederationVisualizeItem from './FederationVisualizeItem.vue'
|
||||
import { updateHomeCommunity } from '../../graphql/updateHomeCommunity'
|
||||
|
||||
const locales = { en, de, es, fr, nl }
|
||||
|
||||
export default {
|
||||
name: 'CommunityVisualizeItem',
|
||||
components: {
|
||||
EditableLabel,
|
||||
FederationVisualizeItem,
|
||||
},
|
||||
props: {
|
||||
item: { type: Object },
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
formatDistanceToNow,
|
||||
locale: this.$i18n.locale,
|
||||
details: false,
|
||||
gmsApiKey: '',
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.gmsApiKey = this.item.gmsApiKey
|
||||
},
|
||||
computed: {
|
||||
verified() {
|
||||
if (!this.item.federatedCommunities || this.item.federatedCommunities.length === 0) {
|
||||
return false
|
||||
}
|
||||
return (
|
||||
this.item.federatedCommunities.filter(
|
||||
(federatedCommunity) =>
|
||||
new Date(federatedCommunity.verifiedAt) >= new Date(federatedCommunity.lastAnnouncedAt),
|
||||
).length > 0
|
||||
)
|
||||
},
|
||||
icon() {
|
||||
return this.verified ? 'check' : 'x-circle'
|
||||
},
|
||||
variant() {
|
||||
return this.verified ? 'success' : 'danger'
|
||||
},
|
||||
lastAnnouncedAt() {
|
||||
if (!this.item.federatedCommunities || this.item.federatedCommunities.length === 0) return ''
|
||||
const minDate = new Date(0)
|
||||
const lastAnnouncedAt = this.item.federatedCommunities.reduce(
|
||||
(lastAnnouncedAt, federateCommunity) => {
|
||||
if (!federateCommunity.lastAnnouncedAt) return lastAnnouncedAt
|
||||
const date = new Date(federateCommunity.lastAnnouncedAt)
|
||||
return date > lastAnnouncedAt ? date : lastAnnouncedAt
|
||||
},
|
||||
minDate,
|
||||
)
|
||||
if (lastAnnouncedAt !== minDate) {
|
||||
return formatDistanceToNow(lastAnnouncedAt, {
|
||||
includeSecond: true,
|
||||
addSuffix: true,
|
||||
locale: locales[this.locale],
|
||||
})
|
||||
}
|
||||
return ''
|
||||
},
|
||||
createdAt() {
|
||||
if (this.item.createdAt) {
|
||||
return formatDistanceToNow(new Date(this.item.createdAt), {
|
||||
includeSecond: true,
|
||||
addSuffix: true,
|
||||
locale: locales[this.locale],
|
||||
})
|
||||
}
|
||||
return ''
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
toggleDetails() {
|
||||
this.details = !this.details
|
||||
},
|
||||
handleSaveGsmApiKey(gmsApiKey) {
|
||||
this.gmsApiKey = gmsApiKey
|
||||
this.$apollo
|
||||
.mutate({
|
||||
mutation: updateHomeCommunity,
|
||||
variables: {
|
||||
uuid: this.item.uuid,
|
||||
gmsApiKey: gmsApiKey,
|
||||
},
|
||||
})
|
||||
.then(() => {
|
||||
this.toastSuccess(this.$t('federation.toast_gmsApiKeyUpdated'))
|
||||
})
|
||||
.catch((error) => {
|
||||
this.toastError(error.message)
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
47
admin/src/components/Federation/FederationVisualizeItem.vue
Normal file
47
admin/src/components/Federation/FederationVisualizeItem.vue
Normal file
@ -0,0 +1,47 @@
|
||||
<template>
|
||||
<div class="federation-visualize-item">
|
||||
<b-row>
|
||||
<b-col><b-icon :icon="icon" :variant="variant" class="mr-4"></b-icon></b-col>
|
||||
<b-col class="ml-1">{{ item.apiVersion }}</b-col>
|
||||
<b-col v-b-tooltip="item.createdAt">{{ distanceDate(item.createdAt) }}</b-col>
|
||||
<b-col v-b-tooltip="item.lastAnnouncedAt">{{ distanceDate(item.lastAnnouncedAt) }}</b-col>
|
||||
<b-col v-b-tooltip="item.verifiedAt">{{ distanceDate(item.verifiedAt) }}</b-col>
|
||||
<b-col v-b-tooltip="item.lastErrorAt">{{ distanceDate(item.lastErrorAt) }}</b-col>
|
||||
</b-row>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { formatDistanceToNow } from 'date-fns'
|
||||
import { de, enUS as en, fr, es, nl } from 'date-fns/locale'
|
||||
|
||||
const locales = { en, de, es, fr, nl }
|
||||
|
||||
export default {
|
||||
name: 'FederationVisualizeItem',
|
||||
props: {
|
||||
item: { type: Object },
|
||||
},
|
||||
computed: {
|
||||
verified() {
|
||||
return new Date(this.item.verifiedAt) >= new Date(this.item.lastAnnouncedAt)
|
||||
},
|
||||
icon() {
|
||||
return this.verified ? 'check' : 'x-circle'
|
||||
},
|
||||
variant() {
|
||||
return this.verified ? 'success' : 'danger'
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
distanceDate(dateString) {
|
||||
return dateString
|
||||
? formatDistanceToNow(new Date(dateString), {
|
||||
includeSecond: true,
|
||||
addSuffix: true,
|
||||
locale: locales[this.$i18n.locale],
|
||||
})
|
||||
: ''
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
83
admin/src/components/input/EditableLabel.spec.js
Normal file
83
admin/src/components/input/EditableLabel.spec.js
Normal file
@ -0,0 +1,83 @@
|
||||
// Test written by ChatGPT 3.5
|
||||
import { mount } from '@vue/test-utils'
|
||||
import EditableLabel from './EditableLabel.vue'
|
||||
|
||||
const localVue = global.localVue
|
||||
const value = 'test label value'
|
||||
|
||||
describe('EditableLabel', () => {
|
||||
let wrapper
|
||||
|
||||
const createWrapper = (propsData) => {
|
||||
return mount(EditableLabel, {
|
||||
localVue,
|
||||
propsData,
|
||||
})
|
||||
}
|
||||
|
||||
it('renders the label when not editing', () => {
|
||||
wrapper = createWrapper({ value, allowEdit: true })
|
||||
|
||||
expect(wrapper.find('label').text()).toBe(value)
|
||||
})
|
||||
|
||||
it('renders the input when editing', async () => {
|
||||
wrapper = createWrapper({ value, allowEdit: true })
|
||||
|
||||
await wrapper.find('button').trigger('click')
|
||||
|
||||
expect(wrapper.find('input').exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('emits save event when clicking save button', async () => {
|
||||
wrapper = createWrapper({ value, allowEdit: true })
|
||||
|
||||
await wrapper.find('button').trigger('click')
|
||||
await wrapper.setData({ inputValue: 'New Value' })
|
||||
await wrapper.find('button').trigger('click')
|
||||
|
||||
expect(wrapper.emitted().save).toBeTruthy()
|
||||
expect(wrapper.emitted().save[0][0]).toBe('New Value')
|
||||
})
|
||||
|
||||
it('disables save button when value is not changed', async () => {
|
||||
wrapper = createWrapper({ value, allowEdit: true })
|
||||
|
||||
await wrapper.find('button').trigger('click')
|
||||
|
||||
expect(wrapper.find('button').attributes('disabled')).toBe('disabled')
|
||||
})
|
||||
|
||||
it('enables save button when value is changed', async () => {
|
||||
wrapper = createWrapper({ value, allowEdit: true })
|
||||
|
||||
await wrapper.find('button').trigger('click')
|
||||
await wrapper.setData({ inputValue: 'New Value' })
|
||||
|
||||
expect(wrapper.find('button').attributes('disabled')).toBeFalsy()
|
||||
})
|
||||
|
||||
it('updates originalValue when saving changes', async () => {
|
||||
wrapper = createWrapper({ value, allowEdit: true })
|
||||
|
||||
await wrapper.find('button').trigger('click')
|
||||
await wrapper.setData({ inputValue: 'New Value' })
|
||||
await wrapper.find('button').trigger('click')
|
||||
|
||||
expect(wrapper.vm.originalValue).toBe('New Value')
|
||||
})
|
||||
|
||||
it('changes variant to success when editing', async () => {
|
||||
wrapper = createWrapper({ value, allowEdit: true })
|
||||
|
||||
await wrapper.find('button').trigger('click')
|
||||
|
||||
expect(wrapper.vm.variant).toBe('success')
|
||||
})
|
||||
|
||||
it('changes variant to prime when not editing', async () => {
|
||||
wrapper = createWrapper({ value, allowEdit: true })
|
||||
|
||||
expect(wrapper.vm.variant).toBe('prime')
|
||||
})
|
||||
})
|
||||
64
admin/src/components/input/EditableLabel.vue
Normal file
64
admin/src/components/input/EditableLabel.vue
Normal file
@ -0,0 +1,64 @@
|
||||
<template>
|
||||
<div>
|
||||
<b-form-group>
|
||||
<label v-if="!editing">{{ value }}</label>
|
||||
<b-form-input v-else v-model="inputValue" :placeholder="placeholder" />
|
||||
</b-form-group>
|
||||
<b-button
|
||||
v-if="allowEdit"
|
||||
@click="toggleEdit"
|
||||
:disabled="!isValueChanged && editing"
|
||||
:variant="variant"
|
||||
>
|
||||
<b-icon v-if="!editing" icon="pencil-fill" tooltip="$t('edit')"></b-icon>
|
||||
<b-icon v-else icon="check" tooltip="$t('save')"></b-icon>
|
||||
</b-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
// Code written from chatGPT 3.5
|
||||
name: 'EditableLabel',
|
||||
props: {
|
||||
value: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
allowEdit: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
editing: false,
|
||||
inputValue: this.value,
|
||||
originalValue: this.value,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
variant() {
|
||||
return this.editing ? 'success' : 'prime'
|
||||
},
|
||||
isValueChanged() {
|
||||
return this.inputValue !== this.originalValue
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
toggleEdit() {
|
||||
if (this.editing) {
|
||||
this.$emit('save', this.inputValue)
|
||||
this.originalValue = this.inputValue
|
||||
}
|
||||
this.editing = !this.editing
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
29
admin/src/graphql/allCommunities.js
Normal file
29
admin/src/graphql/allCommunities.js
Normal file
@ -0,0 +1,29 @@
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
export const allCommunities = gql`
|
||||
query {
|
||||
allCommunities {
|
||||
foreign
|
||||
url
|
||||
publicKey
|
||||
uuid
|
||||
authenticatedAt
|
||||
name
|
||||
description
|
||||
gmsApiKey
|
||||
creationDate
|
||||
createdAt
|
||||
updatedAt
|
||||
federatedCommunities {
|
||||
id
|
||||
apiVersion
|
||||
endPoint
|
||||
lastAnnouncedAt
|
||||
verifiedAt
|
||||
lastErrorAt
|
||||
createdAt
|
||||
updatedAt
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
@ -1,17 +0,0 @@
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
export const getCommunities = gql`
|
||||
query {
|
||||
getCommunities {
|
||||
id
|
||||
foreign
|
||||
publicKey
|
||||
url
|
||||
lastAnnouncedAt
|
||||
verifiedAt
|
||||
lastErrorAt
|
||||
createdAt
|
||||
updatedAt
|
||||
}
|
||||
}
|
||||
`
|
||||
9
admin/src/graphql/updateHomeCommunity.js
Normal file
9
admin/src/graphql/updateHomeCommunity.js
Normal file
@ -0,0 +1,9 @@
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
export const updateHomeCommunity = gql`
|
||||
mutation ($uuid: String!, $gmsApiKey: String!) {
|
||||
updateHomeCommunity(uuid: $uuid, gmsApiKey: $gmsApiKey) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`
|
||||
@ -73,11 +73,20 @@
|
||||
"error": "Fehler",
|
||||
"expired": "abgelaufen",
|
||||
"federation": {
|
||||
"apiVersion": "API Version",
|
||||
"authenticatedAt": "Verifiziert am:",
|
||||
"communityUuid": "Community UUID:",
|
||||
"createdAt": "Erstellt am",
|
||||
"gmsApiKey": "GMS API Key:",
|
||||
"toast_gmsApiKeyUpdated": "Der GMS Api Key wurde erfolgreich aktualisiert!",
|
||||
"gradidoInstances": "Gradido Instanzen",
|
||||
"lastAnnouncedAt": "letzte Bekanntgabe",
|
||||
"lastErrorAt": "Letzer Fehler am",
|
||||
"name": "Name",
|
||||
"publicKey": "PublicKey:",
|
||||
"url": "Url",
|
||||
"verified": "Verifiziert"
|
||||
"verified": "Verifiziert",
|
||||
"verifiedAt": "Verifiziert am"
|
||||
},
|
||||
"firstname": "Vorname",
|
||||
"footer": {
|
||||
|
||||
@ -73,11 +73,20 @@
|
||||
"error": "Error",
|
||||
"expired": "expired",
|
||||
"federation": {
|
||||
"apiVersion": "API Version",
|
||||
"authenticatedAt": "verified at:",
|
||||
"communityUuid": "Community UUID:",
|
||||
"createdAt": "Created At ",
|
||||
"gmsApiKey": "GMS API Key:",
|
||||
"toast_gmsApiKeyUpdated": "The GMS Api Key has been successfully updated!",
|
||||
"gradidoInstances": "Gradido Instances",
|
||||
"lastAnnouncedAt": "Last Announced",
|
||||
"lastErrorAt": "last error at",
|
||||
"name": "Name",
|
||||
"publicKey": "PublicKey:",
|
||||
"url": "Url",
|
||||
"verified": "Verified"
|
||||
"verified": "Verified",
|
||||
"verifiedAt": "Verified at"
|
||||
},
|
||||
"firstname": "Firstname",
|
||||
"footer": {
|
||||
|
||||
@ -2,7 +2,7 @@ import { mount } from '@vue/test-utils'
|
||||
import FederationVisualize from './FederationVisualize'
|
||||
import VueApollo from 'vue-apollo'
|
||||
import { createMockClient } from 'mock-apollo-client'
|
||||
import { getCommunities } from '@/graphql/getCommunities'
|
||||
import { allCommunities } from '@/graphql/allCommunities'
|
||||
import { toastErrorSpy } from '../../test/testSetup'
|
||||
|
||||
const mockClient = createMockClient()
|
||||
@ -25,42 +25,54 @@ const mocks = {
|
||||
|
||||
const defaultData = () => {
|
||||
return {
|
||||
getCommunities: [
|
||||
allCommunities: [
|
||||
{
|
||||
id: 1776,
|
||||
foreign: true,
|
||||
publicKey: 'c7ca9e742421bb167b8666cb78f90b40c665b8f35db8f001988d44dbb3ce8527',
|
||||
url: 'http://localhost/api/2_0',
|
||||
lastAnnouncedAt: '2023-04-07T12:27:24.037Z',
|
||||
verifiedAt: null,
|
||||
lastErrorAt: null,
|
||||
createdAt: '2023-04-07T11:45:06.254Z',
|
||||
updatedAt: null,
|
||||
__typename: 'Community',
|
||||
},
|
||||
{
|
||||
id: 1775,
|
||||
foreign: true,
|
||||
publicKey: 'c7ca9e742421bb167b8666cb78f90b40c665b8f35db8f001988d44dbb3ce8527',
|
||||
url: 'http://localhost/api/1_1',
|
||||
lastAnnouncedAt: '2023-04-07T12:27:24.023Z',
|
||||
verifiedAt: null,
|
||||
lastErrorAt: null,
|
||||
createdAt: '2023-04-07T11:45:06.234Z',
|
||||
updatedAt: null,
|
||||
__typename: 'Community',
|
||||
},
|
||||
{
|
||||
id: 1774,
|
||||
foreign: true,
|
||||
publicKey: 'c7ca9e742421bb167b8666cb78f90b40c665b8f35db8f001988d44dbb3ce8527',
|
||||
url: 'http://localhost/api/1_0',
|
||||
lastAnnouncedAt: '2023-04-07T12:27:24.009Z',
|
||||
verifiedAt: null,
|
||||
lastErrorAt: null,
|
||||
createdAt: '2023-04-07T11:45:06.218Z',
|
||||
updatedAt: null,
|
||||
__typename: 'Community',
|
||||
id: 1,
|
||||
foreign: false,
|
||||
url: 'http://localhost/api/',
|
||||
publicKey: '4007170edd8d33fb009cd99ee4e87f214e7cd21b668d45540a064deb42e243c2',
|
||||
communityUuid: '5ab0befd-b150-4f31-a631-7f3637e47b21',
|
||||
authenticatedAt: null,
|
||||
name: 'Gradido Test',
|
||||
description: 'Gradido Community zum testen',
|
||||
gmsApiKey: '<api key>',
|
||||
creationDate: '2024-01-09T15:56:40.592Z',
|
||||
createdAt: '2024-01-09T15:56:40.595Z',
|
||||
updatedAt: '2024-01-16T11:17:15.000Z',
|
||||
federatedCommunities: [
|
||||
{
|
||||
id: 2046,
|
||||
apiVersion: '2_0',
|
||||
endPoint: 'http://localhost/api/',
|
||||
lastAnnouncedAt: null,
|
||||
verifiedAt: null,
|
||||
lastErrorAt: null,
|
||||
createdAt: '2024-01-16T10:08:21.544Z',
|
||||
updatedAt: null,
|
||||
},
|
||||
{
|
||||
id: 2045,
|
||||
apiVersion: '1_1',
|
||||
endPoint: 'http://localhost/api/',
|
||||
lastAnnouncedAt: null,
|
||||
verifiedAt: null,
|
||||
lastErrorAt: null,
|
||||
createdAt: '2024-01-16T10:08:21.550Z',
|
||||
updatedAt: null,
|
||||
__typename: 'FederatedCommunity',
|
||||
},
|
||||
{
|
||||
id: 2044,
|
||||
apiVersion: '1_0',
|
||||
endPoint: 'http://localhost/api/',
|
||||
lastAnnouncedAt: null,
|
||||
verifiedAt: null,
|
||||
lastErrorAt: null,
|
||||
createdAt: '2024-01-16T10:08:21.544Z',
|
||||
updatedAt: null,
|
||||
__typename: 'FederatedCommunity',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
@ -68,11 +80,11 @@ const defaultData = () => {
|
||||
|
||||
describe('FederationVisualize', () => {
|
||||
let wrapper
|
||||
const getCommunitiesMock = jest.fn()
|
||||
const allCommunitiesMock = jest.fn()
|
||||
|
||||
mockClient.setRequestHandler(
|
||||
getCommunities,
|
||||
getCommunitiesMock
|
||||
allCommunities,
|
||||
allCommunitiesMock
|
||||
.mockRejectedValueOnce({ message: 'Ouch!' })
|
||||
.mockResolvedValue({ data: defaultData() }),
|
||||
)
|
||||
@ -95,7 +107,7 @@ describe('FederationVisualize', () => {
|
||||
|
||||
describe('sever success', () => {
|
||||
it('sends query to Apollo when created', () => {
|
||||
expect(getCommunitiesMock).toBeCalled()
|
||||
expect(allCommunitiesMock).toBeCalled()
|
||||
})
|
||||
|
||||
it('has a DIV element with the class "federation-visualize"', () => {
|
||||
@ -106,8 +118,8 @@ describe('FederationVisualize', () => {
|
||||
expect(wrapper.find('[data-test="federation-communities-refresh-btn"]').exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('renders 3 community list items', () => {
|
||||
expect(wrapper.findAll('.list-group-item').length).toBe(3)
|
||||
it('renders 1 community list item', () => {
|
||||
expect(wrapper.findAll('.list-group-item').length).toBe(1)
|
||||
})
|
||||
|
||||
describe('cklicking the refresh button', () => {
|
||||
@ -117,7 +129,7 @@ describe('FederationVisualize', () => {
|
||||
})
|
||||
|
||||
it('calls the API', async () => {
|
||||
expect(getCommunitiesMock).toBeCalled()
|
||||
expect(allCommunitiesMock).toBeCalled()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -7,7 +7,7 @@
|
||||
icon="arrow-clockwise"
|
||||
font-scale="2"
|
||||
:animation="animation"
|
||||
@click="$apollo.queries.GetCommunities.refresh()"
|
||||
@click="$apollo.queries.allCommunities.refresh()"
|
||||
data-test="federation-communities-refresh-btn"
|
||||
></b-icon>
|
||||
</b-button>
|
||||
@ -16,28 +16,29 @@
|
||||
<b-row>
|
||||
<b-col cols="1" class="ml-1">{{ $t('federation.verified') }}</b-col>
|
||||
<b-col class="ml-3">{{ $t('federation.url') }}</b-col>
|
||||
<b-col class="ml-3">{{ $t('federation.name') }}</b-col>
|
||||
<b-col cols="2">{{ $t('federation.lastAnnouncedAt') }}</b-col>
|
||||
<b-col cols="2">{{ $t('federation.createdAt') }}</b-col>
|
||||
</b-row>
|
||||
<b-list-group-item
|
||||
v-for="item in communities"
|
||||
:key="item.id"
|
||||
:key="item.publicKey"
|
||||
:variant="!item.foreign ? 'primary' : 'warning'"
|
||||
>
|
||||
<federation-visualize-item :item="item" />
|
||||
<community-visualize-item :item="item" />
|
||||
</b-list-group-item>
|
||||
</b-list-group>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { getCommunities } from '@/graphql/getCommunities'
|
||||
import { allCommunities } from '@/graphql/allCommunities'
|
||||
|
||||
import FederationVisualizeItem from '../components/Fedaration/FederationVisualizeItem.vue'
|
||||
import CommunityVisualizeItem from '../components/Federation/CommunityVisualizeItem.vue'
|
||||
|
||||
export default {
|
||||
name: 'FederationVisualize',
|
||||
components: {
|
||||
FederationVisualizeItem,
|
||||
CommunityVisualizeItem,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
@ -48,17 +49,17 @@ export default {
|
||||
},
|
||||
computed: {
|
||||
animation() {
|
||||
return this.$apollo.queries.GetCommunities.loading ? 'spin' : ''
|
||||
return this.$apollo.queries.allCommunities.loading ? 'spin' : ''
|
||||
},
|
||||
},
|
||||
apollo: {
|
||||
GetCommunities: {
|
||||
allCommunities: {
|
||||
fetchPolicy: 'network-only',
|
||||
query() {
|
||||
return getCommunities
|
||||
return allCommunities
|
||||
},
|
||||
update({ getCommunities }) {
|
||||
this.communities = getCommunities
|
||||
update({ allCommunities }) {
|
||||
this.communities = allCommunities
|
||||
},
|
||||
error({ message }) {
|
||||
this.toastError(message)
|
||||
|
||||
@ -16,6 +16,7 @@ module.exports = {
|
||||
moduleNameMapper: {
|
||||
'@/(.*)': '<rootDir>/src/$1',
|
||||
'@arg/(.*)': '<rootDir>/src/graphql/arg/$1',
|
||||
'@input/(.*)': '<rootDir>/src/graphql/input/$1',
|
||||
'@dltConnector/(.*)': '<rootDir>/src/apis/dltConnector/$1',
|
||||
'@enum/(.*)': '<rootDir>/src/graphql/enum/$1',
|
||||
'@model/(.*)': '<rootDir>/src/graphql/model/$1',
|
||||
|
||||
@ -6,4 +6,6 @@ export const ADMIN_RIGHTS = [
|
||||
RIGHTS.UNDELETE_USER,
|
||||
RIGHTS.COMMUNITY_UPDATE,
|
||||
RIGHTS.COMMUNITY_BY_UUID,
|
||||
RIGHTS.COMMUNITY_BY_IDENTIFIER,
|
||||
RIGHTS.HOME_COMMUNITY,
|
||||
]
|
||||
|
||||
3
backend/src/auth/DLT_CONNECTOR_RIGHTS.ts
Normal file
3
backend/src/auth/DLT_CONNECTOR_RIGHTS.ts
Normal file
@ -0,0 +1,3 @@
|
||||
import { RIGHTS } from './RIGHTS'
|
||||
|
||||
export const DLT_CONNECTOR_RIGHTS = [RIGHTS.COMMUNITY_BY_IDENTIFIER, RIGHTS.HOME_COMMUNITY]
|
||||
@ -59,5 +59,7 @@ export enum RIGHTS {
|
||||
DELETE_USER = 'DELETE_USER',
|
||||
UNDELETE_USER = 'UNDELETE_USER',
|
||||
COMMUNITY_BY_UUID = 'COMMUNITY_BY_UUID',
|
||||
COMMUNITY_BY_IDENTIFIER = 'COMMUNITY_BY_IDENTIFIER',
|
||||
HOME_COMMUNITY = 'HOME_COMMUNITY',
|
||||
COMMUNITY_UPDATE = 'COMMUNITY_UPDATE',
|
||||
}
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import { RoleNames } from '@/graphql/enum/RoleNames'
|
||||
|
||||
import { ADMIN_RIGHTS } from './ADMIN_RIGHTS'
|
||||
import { DLT_CONNECTOR_RIGHTS } from './DLT_CONNECTOR_RIGHTS'
|
||||
import { INALIENABLE_RIGHTS } from './INALIENABLE_RIGHTS'
|
||||
import { MODERATOR_RIGHTS } from './MODERATOR_RIGHTS'
|
||||
import { Role } from './Role'
|
||||
@ -20,5 +21,7 @@ export const ROLE_ADMIN = new Role(RoleNames.ADMIN, [
|
||||
...ADMIN_RIGHTS,
|
||||
])
|
||||
|
||||
export const ROLE_DLT_CONNECTOR = new Role(RoleNames.DLT_CONNECTOR, DLT_CONNECTOR_RIGHTS)
|
||||
|
||||
// TODO from database
|
||||
export const ROLES = [ROLE_UNAUTHORIZED, ROLE_USER, ROLE_MODERATOR, ROLE_ADMIN]
|
||||
|
||||
@ -12,7 +12,7 @@ Decimal.set({
|
||||
})
|
||||
|
||||
const constants = {
|
||||
DB_VERSION: '0082-introduce_gms_registration',
|
||||
DB_VERSION: '0083-join_community_federated_communities',
|
||||
DECAY_START_TIME: new Date('2021-05-13 17:46:31-0000'), // GMT+0
|
||||
LOG4JS_CONFIG: 'log4js-config.json',
|
||||
// default log level on production should be info
|
||||
|
||||
@ -1,14 +1,13 @@
|
||||
import { IsString } from 'class-validator'
|
||||
import { Field, ArgsType, InputType } from 'type-graphql'
|
||||
import { IsBoolean, IsString } from 'class-validator'
|
||||
import { ArgsType, Field } from 'type-graphql'
|
||||
|
||||
@InputType()
|
||||
@ArgsType()
|
||||
export class CommunityArgs {
|
||||
@Field(() => String)
|
||||
@Field(() => String, { nullable: true })
|
||||
@IsString()
|
||||
uuid: string
|
||||
communityIdentifier?: string | null
|
||||
|
||||
@Field(() => String)
|
||||
@IsString()
|
||||
gmsApiKey: string
|
||||
@Field(() => Boolean, { nullable: true })
|
||||
@IsBoolean()
|
||||
foreign?: boolean | null
|
||||
}
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
import { IsBoolean, IsInt, IsString } from 'class-validator'
|
||||
import { IsBoolean, IsEnum, IsInt, IsString } from 'class-validator'
|
||||
import { ArgsType, Field, InputType, Int } from 'type-graphql'
|
||||
|
||||
import { GmsPublishLocationType } from '@enum/GmsPublishLocationType'
|
||||
import { GmsPublishNameType } from '@enum/GmsPublishNameType'
|
||||
import { Location } from '@model/Location'
|
||||
|
||||
import { isValidLocation } from '@/graphql/validator/Location'
|
||||
@ -44,19 +46,19 @@ export class UpdateUserInfosArgs {
|
||||
@IsBoolean()
|
||||
hideAmountGDT?: boolean
|
||||
|
||||
@Field({ nullable: true, defaultValue: true })
|
||||
@Field({ nullable: true })
|
||||
@IsBoolean()
|
||||
gmsAllowed?: boolean
|
||||
|
||||
@Field(() => Int, { nullable: true, defaultValue: 0 })
|
||||
@IsInt()
|
||||
gmsPublishName?: number | null
|
||||
@Field(() => GmsPublishNameType, { nullable: true })
|
||||
@IsEnum(GmsPublishNameType)
|
||||
gmsPublishName?: GmsPublishNameType | null
|
||||
|
||||
@Field(() => Location, { nullable: true })
|
||||
@isValidLocation()
|
||||
gmsLocation?: Location | null
|
||||
|
||||
@Field(() => Int, { nullable: true, defaultValue: 2 })
|
||||
@IsInt()
|
||||
gmsPublishLocation?: number | null
|
||||
@Field(() => GmsPublishLocationType, { nullable: true })
|
||||
@IsEnum(GmsPublishLocationType)
|
||||
gmsPublishLocation?: GmsPublishLocationType | null
|
||||
}
|
||||
|
||||
@ -6,7 +6,13 @@ import { RoleNames } from '@enum/RoleNames'
|
||||
import { INALIENABLE_RIGHTS } from '@/auth/INALIENABLE_RIGHTS'
|
||||
import { decode, encode } from '@/auth/JWT'
|
||||
import { RIGHTS } from '@/auth/RIGHTS'
|
||||
import { ROLE_UNAUTHORIZED, ROLE_USER, ROLE_ADMIN, ROLE_MODERATOR } from '@/auth/ROLES'
|
||||
import {
|
||||
ROLE_UNAUTHORIZED,
|
||||
ROLE_USER,
|
||||
ROLE_ADMIN,
|
||||
ROLE_MODERATOR,
|
||||
ROLE_DLT_CONNECTOR,
|
||||
} from '@/auth/ROLES'
|
||||
import { Context } from '@/server/context'
|
||||
import { LogError } from '@/server/LogError'
|
||||
|
||||
@ -30,31 +36,35 @@ export const isAuthorized: AuthChecker<Context> = async ({ context }, rights) =>
|
||||
// Set context gradidoID
|
||||
context.gradidoID = decoded.gradidoID
|
||||
|
||||
// TODO - load from database dynamically & admin - maybe encode this in the token to prevent many database requests
|
||||
// TODO this implementation is bullshit - two database queries cause our user identifiers are not aligned and vary between email, id and pubKey
|
||||
try {
|
||||
const user = await User.findOneOrFail({
|
||||
where: { gradidoID: decoded.gradidoID },
|
||||
withDeleted: true,
|
||||
relations: ['emailContact', 'userRoles'],
|
||||
})
|
||||
context.user = user
|
||||
context.role = ROLE_USER
|
||||
if (user.userRoles?.length > 0) {
|
||||
switch (user.userRoles[0].role) {
|
||||
case RoleNames.ADMIN:
|
||||
context.role = ROLE_ADMIN
|
||||
break
|
||||
case RoleNames.MODERATOR:
|
||||
context.role = ROLE_MODERATOR
|
||||
break
|
||||
default:
|
||||
context.role = ROLE_USER
|
||||
if (context.gradidoID === 'dlt-connector') {
|
||||
context.role = ROLE_DLT_CONNECTOR
|
||||
} else {
|
||||
// TODO - load from database dynamically & admin - maybe encode this in the token to prevent many database requests
|
||||
// TODO this implementation is bullshit - two database queries cause our user identifiers are not aligned and vary between email, id and pubKey
|
||||
try {
|
||||
const user = await User.findOneOrFail({
|
||||
where: { gradidoID: decoded.gradidoID },
|
||||
withDeleted: true,
|
||||
relations: ['emailContact', 'userRoles'],
|
||||
})
|
||||
context.user = user
|
||||
context.role = ROLE_USER
|
||||
if (user.userRoles?.length > 0) {
|
||||
switch (user.userRoles[0].role) {
|
||||
case RoleNames.ADMIN:
|
||||
context.role = ROLE_ADMIN
|
||||
break
|
||||
case RoleNames.MODERATOR:
|
||||
context.role = ROLE_MODERATOR
|
||||
break
|
||||
default:
|
||||
context.role = ROLE_USER
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// in case the database query fails (user deleted)
|
||||
throw new LogError('401 Unauthorized')
|
||||
}
|
||||
} catch {
|
||||
// in case the database query fails (user deleted)
|
||||
throw new LogError('401 Unauthorized')
|
||||
}
|
||||
|
||||
// check for correct rights
|
||||
|
||||
@ -5,6 +5,7 @@ export enum RoleNames {
|
||||
USER = 'USER',
|
||||
MODERATOR = 'MODERATOR',
|
||||
ADMIN = 'ADMIN',
|
||||
DLT_CONNECTOR = 'DLT_CONNECTOR_ROLE',
|
||||
}
|
||||
|
||||
registerEnumType(RoleNames, {
|
||||
|
||||
14
backend/src/graphql/input/EditCommunityInput.ts
Normal file
14
backend/src/graphql/input/EditCommunityInput.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import { IsString, IsUUID } from 'class-validator'
|
||||
import { ArgsType, Field, InputType } from 'type-graphql'
|
||||
|
||||
@ArgsType()
|
||||
@InputType()
|
||||
export class EditCommunityInput {
|
||||
@Field(() => String)
|
||||
@IsUUID('4')
|
||||
uuid: string
|
||||
|
||||
@Field(() => String)
|
||||
@IsString()
|
||||
gmsApiKey: string
|
||||
}
|
||||
76
backend/src/graphql/model/AdminCommunityView.ts
Normal file
76
backend/src/graphql/model/AdminCommunityView.ts
Normal file
@ -0,0 +1,76 @@
|
||||
import { Community as DbCommunity } from '@entity/Community'
|
||||
import { FederatedCommunity as DbFederatedCommunity } from '@entity/FederatedCommunity'
|
||||
import { ObjectType, Field } from 'type-graphql'
|
||||
|
||||
import { FederatedCommunity } from './FederatedCommunity'
|
||||
|
||||
@ObjectType()
|
||||
export class AdminCommunityView {
|
||||
constructor(dbCom: DbCommunity) {
|
||||
if (dbCom.federatedCommunities && dbCom.federatedCommunities.length > 0) {
|
||||
const federatedCommunity = dbCom.federatedCommunities[0]
|
||||
this.foreign = federatedCommunity.foreign
|
||||
const url = new URL(federatedCommunity.endPoint)
|
||||
// use only the host part
|
||||
this.url = url.protocol + '//' + url.host
|
||||
this.publicKey = federatedCommunity.publicKey.toString('hex')
|
||||
this.federatedCommunities = dbCom.federatedCommunities.map(
|
||||
(federatedCom: DbFederatedCommunity) => new FederatedCommunity(federatedCom),
|
||||
)
|
||||
}
|
||||
if (dbCom.foreign !== undefined) {
|
||||
this.foreign = dbCom.foreign
|
||||
}
|
||||
this.name = dbCom.name
|
||||
this.description = dbCom.description
|
||||
this.gmsApiKey = dbCom.gmsApiKey
|
||||
if (dbCom.url) {
|
||||
this.url = dbCom.url
|
||||
}
|
||||
if (dbCom.publicKey && dbCom.publicKey.length === 32) {
|
||||
this.publicKey = dbCom.publicKey.toString('hex')
|
||||
}
|
||||
this.creationDate = dbCom.creationDate
|
||||
this.createdAt = dbCom.createdAt
|
||||
this.updatedAt = dbCom.updatedAt
|
||||
this.uuid = dbCom.communityUuid
|
||||
this.authenticatedAt = dbCom.authenticatedAt
|
||||
this.gmsApiKey = dbCom.gmsApiKey
|
||||
}
|
||||
|
||||
@Field(() => Boolean)
|
||||
foreign: boolean
|
||||
|
||||
@Field(() => String)
|
||||
url: string
|
||||
|
||||
@Field(() => String)
|
||||
publicKey: string
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
uuid: string | null
|
||||
|
||||
@Field(() => Date, { nullable: true })
|
||||
authenticatedAt: Date | null
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
name: string | null
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
description: string | null
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
gmsApiKey: string | null
|
||||
|
||||
@Field(() => Date, { nullable: true })
|
||||
creationDate: Date | null
|
||||
|
||||
@Field(() => Date, { nullable: true })
|
||||
createdAt: Date | null
|
||||
|
||||
@Field(() => Date, { nullable: true })
|
||||
updatedAt: Date | null
|
||||
|
||||
@Field(() => [FederatedCommunity], { nullable: true })
|
||||
federatedCommunities: FederatedCommunity[] | null
|
||||
}
|
||||
@ -7,8 +7,8 @@ export class FederatedCommunity {
|
||||
this.id = dbCom.id
|
||||
this.foreign = dbCom.foreign
|
||||
this.publicKey = dbCom.publicKey.toString('hex')
|
||||
this.url =
|
||||
(dbCom.endPoint.endsWith('/') ? dbCom.endPoint : dbCom.endPoint + '/') + dbCom.apiVersion
|
||||
this.apiVersion = dbCom.apiVersion
|
||||
this.endPoint = dbCom.endPoint.endsWith('/') ? dbCom.endPoint : dbCom.endPoint + '/'
|
||||
this.lastAnnouncedAt = dbCom.lastAnnouncedAt
|
||||
this.verifiedAt = dbCom.verifiedAt
|
||||
this.lastErrorAt = dbCom.lastErrorAt
|
||||
@ -26,7 +26,10 @@ export class FederatedCommunity {
|
||||
publicKey: string
|
||||
|
||||
@Field(() => String)
|
||||
url: string
|
||||
apiVersion: string
|
||||
|
||||
@Field(() => String)
|
||||
endPoint: string
|
||||
|
||||
@Field(() => Date, { nullable: true })
|
||||
lastAnnouncedAt: Date | null
|
||||
|
||||
@ -1,6 +1,9 @@
|
||||
import { User as dbUser } from '@entity/User'
|
||||
import { ObjectType, Field, Int } from 'type-graphql'
|
||||
|
||||
import { GmsPublishLocationType } from '@enum/GmsPublishLocationType'
|
||||
import { GmsPublishNameType } from '@enum/GmsPublishNameType'
|
||||
|
||||
import { KlickTipp } from './KlickTipp'
|
||||
|
||||
@ObjectType()
|
||||
@ -29,6 +32,9 @@ export class User {
|
||||
this.hasElopage = null
|
||||
this.hideAmountGDD = user.hideAmountGDD
|
||||
this.hideAmountGDT = user.hideAmountGDT
|
||||
this.gmsAllowed = user.gmsAllowed
|
||||
this.gmsPublishName = user.gmsPublishName
|
||||
this.gmsPublishLocation = user.gmsPublishLocation
|
||||
}
|
||||
}
|
||||
|
||||
@ -74,6 +80,15 @@ export class User {
|
||||
@Field(() => Boolean)
|
||||
hideAmountGDT: boolean
|
||||
|
||||
@Field(() => Boolean)
|
||||
gmsAllowed: boolean
|
||||
|
||||
@Field(() => GmsPublishNameType, { nullable: true })
|
||||
gmsPublishName: GmsPublishNameType | null
|
||||
|
||||
@Field(() => GmsPublishLocationType, { nullable: true })
|
||||
gmsPublishLocation: GmsPublishLocationType | null
|
||||
|
||||
// This is not the users publisherId, but the one of the users who recommend him
|
||||
@Field(() => Int, { nullable: true })
|
||||
publisherId: number | null
|
||||
|
||||
@ -10,13 +10,20 @@ import { Community as DbCommunity } from '@entity/Community'
|
||||
import { FederatedCommunity as DbFederatedCommunity } from '@entity/FederatedCommunity'
|
||||
import { ApolloServerTestClient } from 'apollo-server-testing'
|
||||
import { GraphQLError } from 'graphql/error/GraphQLError'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
|
||||
import { cleanDB, testEnvironment } from '@test/helpers'
|
||||
import { logger, i18n as localization } from '@test/testSetup'
|
||||
|
||||
import { userFactory } from '@/seeds/factory/user'
|
||||
import { login, updateHomeCommunityQuery } from '@/seeds/graphql/mutations'
|
||||
import { getCommunities, communitiesQuery, getCommunityByUuidQuery } from '@/seeds/graphql/queries'
|
||||
import {
|
||||
allCommunities,
|
||||
getCommunities,
|
||||
communitiesQuery,
|
||||
getHomeCommunityQuery,
|
||||
getCommunityByIdentifierQuery,
|
||||
} from '@/seeds/graphql/queries'
|
||||
import { peterLustig } from '@/seeds/users/peter-lustig'
|
||||
|
||||
import { getCommunityByUuid } from './util/communities'
|
||||
@ -164,7 +171,8 @@ describe('CommunityResolver', () => {
|
||||
id: 3,
|
||||
foreign: homeCom3.foreign,
|
||||
publicKey: expect.stringMatching(ed25519KeyPairStaticHex[2].public),
|
||||
url: expect.stringMatching('http://localhost/api/2_0'),
|
||||
endPoint: expect.stringMatching('http://localhost/api/'),
|
||||
apiVersion: '2_0',
|
||||
lastAnnouncedAt: null,
|
||||
verifiedAt: null,
|
||||
lastErrorAt: null,
|
||||
@ -175,7 +183,8 @@ describe('CommunityResolver', () => {
|
||||
id: 2,
|
||||
foreign: homeCom2.foreign,
|
||||
publicKey: expect.stringMatching(ed25519KeyPairStaticHex[1].public),
|
||||
url: expect.stringMatching('http://localhost/api/1_1'),
|
||||
endPoint: expect.stringMatching('http://localhost/api/'),
|
||||
apiVersion: '1_1',
|
||||
lastAnnouncedAt: null,
|
||||
verifiedAt: null,
|
||||
lastErrorAt: null,
|
||||
@ -186,7 +195,8 @@ describe('CommunityResolver', () => {
|
||||
id: 1,
|
||||
foreign: homeCom1.foreign,
|
||||
publicKey: expect.stringMatching(ed25519KeyPairStaticHex[0].public),
|
||||
url: expect.stringMatching('http://localhost/api/1_0'),
|
||||
endPoint: expect.stringMatching('http://localhost/api/'),
|
||||
apiVersion: '1_0',
|
||||
lastAnnouncedAt: null,
|
||||
verifiedAt: null,
|
||||
lastErrorAt: null,
|
||||
@ -222,7 +232,7 @@ describe('CommunityResolver', () => {
|
||||
foreignCom3 = DbFederatedCommunity.create()
|
||||
foreignCom3.foreign = true
|
||||
foreignCom3.publicKey = Buffer.from(ed25519KeyPairStaticHex[5].public, 'hex')
|
||||
foreignCom3.apiVersion = '1_2'
|
||||
foreignCom3.apiVersion = '2_0'
|
||||
foreignCom3.endPoint = 'http://remotehost/api'
|
||||
foreignCom3.createdAt = new Date()
|
||||
await DbFederatedCommunity.insert(foreignCom3)
|
||||
@ -236,7 +246,8 @@ describe('CommunityResolver', () => {
|
||||
id: 3,
|
||||
foreign: homeCom3.foreign,
|
||||
publicKey: expect.stringMatching(ed25519KeyPairStaticHex[2].public),
|
||||
url: expect.stringMatching('http://localhost/api/2_0'),
|
||||
endPoint: expect.stringMatching('http://localhost/api/'),
|
||||
apiVersion: '2_0',
|
||||
lastAnnouncedAt: null,
|
||||
verifiedAt: null,
|
||||
lastErrorAt: null,
|
||||
@ -247,7 +258,8 @@ describe('CommunityResolver', () => {
|
||||
id: 2,
|
||||
foreign: homeCom2.foreign,
|
||||
publicKey: expect.stringMatching(ed25519KeyPairStaticHex[1].public),
|
||||
url: expect.stringMatching('http://localhost/api/1_1'),
|
||||
endPoint: expect.stringMatching('http://localhost/api/'),
|
||||
apiVersion: '1_1',
|
||||
lastAnnouncedAt: null,
|
||||
verifiedAt: null,
|
||||
lastErrorAt: null,
|
||||
@ -258,7 +270,8 @@ describe('CommunityResolver', () => {
|
||||
id: 1,
|
||||
foreign: homeCom1.foreign,
|
||||
publicKey: expect.stringMatching(ed25519KeyPairStaticHex[0].public),
|
||||
url: expect.stringMatching('http://localhost/api/1_0'),
|
||||
endPoint: expect.stringMatching('http://localhost/api/'),
|
||||
apiVersion: '1_0',
|
||||
lastAnnouncedAt: null,
|
||||
verifiedAt: null,
|
||||
lastErrorAt: null,
|
||||
@ -269,7 +282,8 @@ describe('CommunityResolver', () => {
|
||||
id: 6,
|
||||
foreign: foreignCom3.foreign,
|
||||
publicKey: expect.stringMatching(ed25519KeyPairStaticHex[5].public),
|
||||
url: expect.stringMatching('http://remotehost/api/1_2'),
|
||||
endPoint: expect.stringMatching('http://remotehost/api/'),
|
||||
apiVersion: '2_0',
|
||||
lastAnnouncedAt: null,
|
||||
verifiedAt: null,
|
||||
lastErrorAt: null,
|
||||
@ -280,7 +294,8 @@ describe('CommunityResolver', () => {
|
||||
id: 5,
|
||||
foreign: foreignCom2.foreign,
|
||||
publicKey: expect.stringMatching(ed25519KeyPairStaticHex[4].public),
|
||||
url: expect.stringMatching('http://remotehost/api/1_1'),
|
||||
endPoint: expect.stringMatching('http://remotehost/api/'),
|
||||
apiVersion: '1_1',
|
||||
lastAnnouncedAt: null,
|
||||
verifiedAt: null,
|
||||
lastErrorAt: null,
|
||||
@ -291,7 +306,8 @@ describe('CommunityResolver', () => {
|
||||
id: 4,
|
||||
foreign: foreignCom1.foreign,
|
||||
publicKey: expect.stringMatching(ed25519KeyPairStaticHex[3].public),
|
||||
url: expect.stringMatching('http://remotehost/api/1_0'),
|
||||
endPoint: expect.stringMatching('http://remotehost/api/'),
|
||||
apiVersion: '1_0',
|
||||
lastAnnouncedAt: null,
|
||||
verifiedAt: null,
|
||||
lastErrorAt: null,
|
||||
@ -303,6 +319,229 @@ describe('CommunityResolver', () => {
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('with 6 federated community entries', () => {
|
||||
let comHomeCom1: DbCommunity
|
||||
let comForeignCom1: DbCommunity
|
||||
let comForeignCom2: DbCommunity
|
||||
let foreignCom4: DbFederatedCommunity
|
||||
|
||||
beforeEach(async () => {
|
||||
jest.clearAllMocks()
|
||||
comHomeCom1 = DbCommunity.create()
|
||||
comHomeCom1.foreign = false
|
||||
comHomeCom1.url = 'http://localhost'
|
||||
comHomeCom1.publicKey = Buffer.from(ed25519KeyPairStaticHex[0].public, 'hex')
|
||||
comHomeCom1.privateKey = Buffer.from(ed25519KeyPairStaticHex[0].private, 'hex')
|
||||
comHomeCom1.communityUuid = 'HomeCom-UUID'
|
||||
comHomeCom1.authenticatedAt = new Date()
|
||||
comHomeCom1.name = 'HomeCommunity-name'
|
||||
comHomeCom1.description = 'HomeCommunity-description'
|
||||
comHomeCom1.creationDate = new Date()
|
||||
await DbCommunity.insert(comHomeCom1)
|
||||
|
||||
comForeignCom1 = DbCommunity.create()
|
||||
comForeignCom1.foreign = true
|
||||
comForeignCom1.url = 'http://stage-2.gradido.net'
|
||||
comForeignCom1.publicKey = Buffer.from(ed25519KeyPairStaticHex[3].public, 'hex')
|
||||
comForeignCom1.privateKey = Buffer.from(ed25519KeyPairStaticHex[3].private, 'hex')
|
||||
// foreignCom1.communityUuid = 'Stage2-Com-UUID'
|
||||
// foreignCom1.authenticatedAt = new Date()
|
||||
comForeignCom1.name = 'Stage-2_Community-name'
|
||||
comForeignCom1.description = 'Stage-2_Community-description'
|
||||
comForeignCom1.creationDate = new Date()
|
||||
await DbCommunity.insert(comForeignCom1)
|
||||
|
||||
comForeignCom2 = DbCommunity.create()
|
||||
comForeignCom2.foreign = true
|
||||
comForeignCom2.url = 'http://stage-3.gradido.net'
|
||||
comForeignCom2.publicKey = Buffer.from(ed25519KeyPairStaticHex[4].public, 'hex')
|
||||
comForeignCom2.privateKey = Buffer.from(ed25519KeyPairStaticHex[4].private, 'hex')
|
||||
comForeignCom2.communityUuid = 'Stage3-Com-UUID'
|
||||
comForeignCom2.authenticatedAt = new Date()
|
||||
comForeignCom2.name = 'Stage-3_Community-name'
|
||||
comForeignCom2.description = 'Stage-3_Community-description'
|
||||
comForeignCom2.creationDate = new Date()
|
||||
await DbCommunity.insert(comForeignCom2)
|
||||
|
||||
foreignCom4 = DbFederatedCommunity.create()
|
||||
foreignCom4.foreign = true
|
||||
foreignCom4.publicKey = Buffer.from(ed25519KeyPairStaticHex[5].public, 'hex')
|
||||
foreignCom4.apiVersion = '1_0'
|
||||
foreignCom4.endPoint = 'http://remotehost/api'
|
||||
foreignCom4.createdAt = new Date()
|
||||
await DbFederatedCommunity.insert(foreignCom4)
|
||||
})
|
||||
|
||||
it('return communities structured for admin ', async () => {
|
||||
await expect(query({ query: allCommunities })).resolves.toMatchObject({
|
||||
data: {
|
||||
allCommunities: [
|
||||
{
|
||||
foreign: false,
|
||||
url: 'http://localhost',
|
||||
publicKey: expect.stringMatching(ed25519KeyPairStaticHex[2].public),
|
||||
authenticatedAt: null,
|
||||
createdAt: null,
|
||||
creationDate: null,
|
||||
description: null,
|
||||
gmsApiKey: null,
|
||||
name: null,
|
||||
updatedAt: null,
|
||||
uuid: null,
|
||||
federatedCommunities: [
|
||||
{
|
||||
id: 3,
|
||||
apiVersion: '2_0',
|
||||
endPoint: 'http://localhost/api/',
|
||||
createdAt: homeCom3.createdAt.toISOString(),
|
||||
lastAnnouncedAt: null,
|
||||
lastErrorAt: null,
|
||||
updatedAt: null,
|
||||
verifiedAt: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
foreign: false,
|
||||
url: 'http://localhost',
|
||||
publicKey: expect.stringMatching(ed25519KeyPairStaticHex[1].public),
|
||||
authenticatedAt: null,
|
||||
createdAt: null,
|
||||
creationDate: null,
|
||||
description: null,
|
||||
gmsApiKey: null,
|
||||
name: null,
|
||||
updatedAt: null,
|
||||
uuid: null,
|
||||
federatedCommunities: [
|
||||
{
|
||||
id: 2,
|
||||
apiVersion: '1_1',
|
||||
endPoint: 'http://localhost/api/',
|
||||
createdAt: homeCom2.createdAt.toISOString(),
|
||||
lastAnnouncedAt: null,
|
||||
lastErrorAt: null,
|
||||
updatedAt: null,
|
||||
verifiedAt: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
foreign: false,
|
||||
url: 'http://localhost',
|
||||
publicKey: expect.stringMatching(ed25519KeyPairStaticHex[0].public),
|
||||
authenticatedAt: comHomeCom1.authenticatedAt?.toISOString(),
|
||||
createdAt: comHomeCom1.createdAt.toISOString(),
|
||||
creationDate: comHomeCom1.creationDate?.toISOString(),
|
||||
description: comHomeCom1.description,
|
||||
gmsApiKey: null,
|
||||
name: comHomeCom1.name,
|
||||
updatedAt: null,
|
||||
uuid: comHomeCom1.communityUuid,
|
||||
federatedCommunities: [
|
||||
{
|
||||
id: 1,
|
||||
apiVersion: '1_0',
|
||||
endPoint: 'http://localhost/api/',
|
||||
createdAt: homeCom1.createdAt.toISOString(),
|
||||
lastAnnouncedAt: null,
|
||||
lastErrorAt: null,
|
||||
updatedAt: null,
|
||||
verifiedAt: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
foreign: true,
|
||||
url: 'http://remotehost',
|
||||
publicKey: expect.stringMatching(ed25519KeyPairStaticHex[5].public),
|
||||
authenticatedAt: null,
|
||||
createdAt: null,
|
||||
creationDate: null,
|
||||
description: null,
|
||||
gmsApiKey: null,
|
||||
name: null,
|
||||
updatedAt: null,
|
||||
uuid: null,
|
||||
federatedCommunities: [
|
||||
{
|
||||
id: 7,
|
||||
apiVersion: '1_0',
|
||||
endPoint: 'http://remotehost/api/',
|
||||
createdAt: foreignCom4.createdAt.toISOString(),
|
||||
lastAnnouncedAt: null,
|
||||
lastErrorAt: null,
|
||||
updatedAt: null,
|
||||
verifiedAt: null,
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
apiVersion: '2_0',
|
||||
endPoint: 'http://remotehost/api/',
|
||||
createdAt: foreignCom3.createdAt.toISOString(),
|
||||
lastAnnouncedAt: null,
|
||||
lastErrorAt: null,
|
||||
updatedAt: null,
|
||||
verifiedAt: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
foreign: true,
|
||||
url: 'http://stage-3.gradido.net',
|
||||
publicKey: expect.stringMatching(ed25519KeyPairStaticHex[4].public),
|
||||
authenticatedAt: comForeignCom2.authenticatedAt?.toISOString(),
|
||||
createdAt: comForeignCom2.createdAt.toISOString(),
|
||||
creationDate: comForeignCom2.creationDate?.toISOString(),
|
||||
description: comForeignCom2.description,
|
||||
gmsApiKey: null,
|
||||
name: comForeignCom2.name,
|
||||
updatedAt: null,
|
||||
uuid: comForeignCom2.communityUuid,
|
||||
federatedCommunities: [
|
||||
{
|
||||
id: 5,
|
||||
apiVersion: '1_1',
|
||||
endPoint: 'http://remotehost/api/',
|
||||
createdAt: foreignCom2.createdAt.toISOString(),
|
||||
lastAnnouncedAt: null,
|
||||
lastErrorAt: null,
|
||||
updatedAt: null,
|
||||
verifiedAt: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
foreign: true,
|
||||
url: 'http://stage-2.gradido.net',
|
||||
publicKey: expect.stringMatching(ed25519KeyPairStaticHex[3].public),
|
||||
authenticatedAt: null,
|
||||
createdAt: comForeignCom1.createdAt.toISOString(),
|
||||
creationDate: comForeignCom1.creationDate?.toISOString(),
|
||||
description: comForeignCom1.description,
|
||||
gmsApiKey: null,
|
||||
name: comForeignCom1.name,
|
||||
updatedAt: null,
|
||||
uuid: null,
|
||||
federatedCommunities: [
|
||||
{
|
||||
id: 4,
|
||||
apiVersion: '1_0',
|
||||
endPoint: 'http://remotehost/api/',
|
||||
createdAt: foreignCom1.createdAt.toISOString(),
|
||||
lastAnnouncedAt: null,
|
||||
lastErrorAt: null,
|
||||
updatedAt: null,
|
||||
verifiedAt: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('communities', () => {
|
||||
@ -464,8 +703,14 @@ describe('CommunityResolver', () => {
|
||||
foreignCom1 = DbCommunity.create()
|
||||
foreignCom1.foreign = true
|
||||
foreignCom1.url = 'http://stage-2.gradido.net/api'
|
||||
foreignCom1.publicKey = Buffer.from('publicKey-stage-2_Community')
|
||||
foreignCom1.privateKey = Buffer.from('privateKey-stage-2_Community')
|
||||
foreignCom1.publicKey = Buffer.from(
|
||||
'8a1f9374b99c30d827b85dcd23f7e50328430d64ef65ef35bf375ea8eb9a2e1d',
|
||||
'hex',
|
||||
)
|
||||
foreignCom1.privateKey = Buffer.from(
|
||||
'f6c2a9d78e20a3c910f35b8ffcf824aa7b37f0d3d81bfc4c0e65e17a194b3a4a',
|
||||
'hex',
|
||||
)
|
||||
// foreignCom1.communityUuid = 'Stage2-Com-UUID'
|
||||
// foreignCom1.authenticatedAt = new Date()
|
||||
foreignCom1.name = 'Stage-2_Community-name'
|
||||
@ -476,9 +721,15 @@ describe('CommunityResolver', () => {
|
||||
foreignCom2 = DbCommunity.create()
|
||||
foreignCom2.foreign = true
|
||||
foreignCom2.url = 'http://stage-3.gradido.net/api'
|
||||
foreignCom2.publicKey = Buffer.from('publicKey-stage-3_Community')
|
||||
foreignCom2.privateKey = Buffer.from('privateKey-stage-3_Community')
|
||||
foreignCom2.communityUuid = 'Stage3-Com-UUID'
|
||||
foreignCom2.publicKey = Buffer.from(
|
||||
'e047365a54082e8a7e9273da61b55c8134a2a0c836799ba12b78b9b0c52bc85f',
|
||||
'hex',
|
||||
)
|
||||
foreignCom2.privateKey = Buffer.from(
|
||||
'e047365a54082e8a7e9273da61b55c8134a2a0c836799ba12b78b9b0c52bc85f',
|
||||
'hex',
|
||||
)
|
||||
foreignCom2.communityUuid = uuidv4()
|
||||
foreignCom2.authenticatedAt = new Date()
|
||||
foreignCom2.name = 'Stage-3_Community-name'
|
||||
foreignCom2.description = 'Stage-3_Community-description'
|
||||
@ -486,15 +737,36 @@ describe('CommunityResolver', () => {
|
||||
await DbCommunity.insert(foreignCom2)
|
||||
})
|
||||
|
||||
it('finds the home-community', async () => {
|
||||
it('finds the home-community by uuid', async () => {
|
||||
await expect(
|
||||
query({
|
||||
query: getCommunityByUuidQuery,
|
||||
variables: { communityUuid: homeCom?.communityUuid },
|
||||
query: getCommunityByIdentifierQuery,
|
||||
variables: { communityIdentifier: homeCom?.communityUuid },
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
data: {
|
||||
community: {
|
||||
communityByIdentifier: {
|
||||
id: homeCom?.id,
|
||||
foreign: homeCom?.foreign,
|
||||
name: homeCom?.name,
|
||||
description: homeCom?.description,
|
||||
url: homeCom?.url,
|
||||
creationDate: homeCom?.creationDate?.toISOString(),
|
||||
uuid: homeCom?.communityUuid,
|
||||
authenticatedAt: homeCom?.authenticatedAt,
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('finds the home-community', async () => {
|
||||
await expect(
|
||||
query({
|
||||
query: getHomeCommunityQuery,
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
data: {
|
||||
homeCommunity: {
|
||||
id: homeCom?.id,
|
||||
foreign: homeCom?.foreign,
|
||||
name: homeCom?.name,
|
||||
@ -563,7 +835,7 @@ describe('CommunityResolver', () => {
|
||||
expect(
|
||||
await mutate({
|
||||
mutation: updateHomeCommunityQuery,
|
||||
variables: { uuid: 'unknownUuid', gmsApiKey: 'gmsApiKey' },
|
||||
variables: { uuid: uuidv4(), gmsApiKey: 'gmsApiKey' },
|
||||
}),
|
||||
).toEqual(
|
||||
expect.objectContaining({
|
||||
|
||||
@ -1,16 +1,23 @@
|
||||
import { IsNull, Not } from '@dbTools/typeorm'
|
||||
import { Community as DbCommunity } from '@entity/Community'
|
||||
import { FederatedCommunity as DbFederatedCommunity } from '@entity/FederatedCommunity'
|
||||
import { Resolver, Query, Authorized, Arg, Mutation, Args } from 'type-graphql'
|
||||
import { Resolver, Query, Authorized, Mutation, Args, Arg } from 'type-graphql'
|
||||
|
||||
import { CommunityArgs } from '@arg//CommunityArgs'
|
||||
import { Paginated } from '@arg/Paginated'
|
||||
import { EditCommunityInput } from '@input/EditCommunityInput'
|
||||
import { AdminCommunityView } from '@model/AdminCommunityView'
|
||||
import { Community } from '@model/Community'
|
||||
import { FederatedCommunity } from '@model/FederatedCommunity'
|
||||
|
||||
import { RIGHTS } from '@/auth/RIGHTS'
|
||||
import { LogError } from '@/server/LogError'
|
||||
|
||||
import { getCommunityByUuid } from './util/communities'
|
||||
import {
|
||||
getAllCommunities,
|
||||
getCommunityByIdentifier,
|
||||
getCommunityByUuid,
|
||||
getHomeCommunity,
|
||||
} from './util/communities'
|
||||
|
||||
@Resolver()
|
||||
export class CommunityResolver {
|
||||
@ -29,6 +36,12 @@ export class CommunityResolver {
|
||||
)
|
||||
}
|
||||
|
||||
@Authorized([RIGHTS.COMMUNITIES])
|
||||
@Query(() => [AdminCommunityView])
|
||||
async allCommunities(@Args() paginated: Paginated): Promise<AdminCommunityView[]> {
|
||||
return (await getAllCommunities(paginated)).map((dbCom) => new AdminCommunityView(dbCom))
|
||||
}
|
||||
|
||||
@Authorized([RIGHTS.COMMUNITIES])
|
||||
@Query(() => [Community])
|
||||
async communities(): Promise<Community[]> {
|
||||
@ -41,41 +54,42 @@ export class CommunityResolver {
|
||||
return dbCommunities.map((dbCom: DbCommunity) => new Community(dbCom))
|
||||
}
|
||||
|
||||
@Authorized([RIGHTS.COMMUNITY_BY_UUID])
|
||||
@Authorized([RIGHTS.COMMUNITY_BY_IDENTIFIER])
|
||||
@Query(() => Community)
|
||||
async community(@Arg('communityUuid') communityUuid: string): Promise<Community> {
|
||||
const com: DbCommunity | null = await getCommunityByUuid(communityUuid)
|
||||
if (!com) {
|
||||
throw new LogError('community not found', communityUuid)
|
||||
async communityByIdentifier(
|
||||
@Arg('communityIdentifier') communityIdentifier: string,
|
||||
): Promise<Community> {
|
||||
const community = await getCommunityByIdentifier(communityIdentifier)
|
||||
if (!community) {
|
||||
throw new LogError('community not found', communityIdentifier)
|
||||
}
|
||||
return new Community(com)
|
||||
return new Community(community)
|
||||
}
|
||||
|
||||
@Authorized([RIGHTS.HOME_COMMUNITY])
|
||||
@Query(() => Community)
|
||||
async homeCommunity(): Promise<Community> {
|
||||
const community = await getHomeCommunity()
|
||||
if (!community) {
|
||||
throw new LogError('no home community exist')
|
||||
}
|
||||
return new Community(community)
|
||||
}
|
||||
|
||||
@Authorized([RIGHTS.COMMUNITY_UPDATE])
|
||||
@Mutation(() => Community)
|
||||
async updateHomeCommunity(@Args() { uuid, gmsApiKey }: CommunityArgs): Promise<Community> {
|
||||
let homeCom: DbCommunity | null
|
||||
let com: Community
|
||||
if (uuid) {
|
||||
let toUpdate = false
|
||||
homeCom = await getCommunityByUuid(uuid)
|
||||
if (!homeCom) {
|
||||
throw new LogError('HomeCommunity with uuid not found: ', uuid)
|
||||
}
|
||||
if (homeCom.foreign) {
|
||||
throw new LogError('Error: Only the HomeCommunity could be modified!')
|
||||
}
|
||||
if (homeCom.gmsApiKey !== gmsApiKey) {
|
||||
homeCom.gmsApiKey = gmsApiKey
|
||||
toUpdate = true
|
||||
}
|
||||
if (toUpdate) {
|
||||
await DbCommunity.save(homeCom)
|
||||
}
|
||||
com = new Community(homeCom)
|
||||
} else {
|
||||
throw new LogError(`HomeCommunity without an uuid can't be modified!`)
|
||||
async updateHomeCommunity(@Args() { uuid, gmsApiKey }: EditCommunityInput): Promise<Community> {
|
||||
const homeCom = await getCommunityByUuid(uuid)
|
||||
if (!homeCom) {
|
||||
throw new LogError('HomeCommunity with uuid not found: ', uuid)
|
||||
}
|
||||
return com
|
||||
if (homeCom.foreign) {
|
||||
throw new LogError('Error: Only the HomeCommunity could be modified!')
|
||||
}
|
||||
if (homeCom.gmsApiKey !== gmsApiKey) {
|
||||
homeCom.gmsApiKey = gmsApiKey
|
||||
await DbCommunity.save(homeCom)
|
||||
}
|
||||
return new Community(homeCom)
|
||||
}
|
||||
}
|
||||
|
||||
@ -15,8 +15,6 @@ import { ApolloServerTestClient } from 'apollo-server-testing'
|
||||
import { GraphQLError } from 'graphql'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
|
||||
import { GmsPublishLocationType } from '@enum/GmsPublishLocationType'
|
||||
import { GmsPublishNameType } from '@enum/GmsPublishNameType'
|
||||
import { cleanDB, testEnvironment } from '@test/helpers'
|
||||
import { logger } from '@test/testSetup'
|
||||
|
||||
@ -530,13 +528,12 @@ describe('send coins', () => {
|
||||
|
||||
describe('send coins via alias', () => {
|
||||
beforeAll(async () => {
|
||||
// first set alias to null, because updating alias isn't allowed
|
||||
await User.update({ alias: 'MeisterBob' }, { alias: () => 'NULL' })
|
||||
await mutate({
|
||||
mutation: updateUserInfos,
|
||||
variables: {
|
||||
alias: 'bob',
|
||||
gmsAllowed: true,
|
||||
gmsPublishName: GmsPublishNameType.GMS_PUBLISH_NAME_ALIAS_OR_INITALS,
|
||||
gmsPublishLocation: GmsPublishLocationType.GMS_LOCATION_TYPE_RANDOM,
|
||||
},
|
||||
})
|
||||
await mutate({
|
||||
|
||||
@ -38,7 +38,7 @@ import { calculateBalance } from '@/util/validate'
|
||||
import { virtualLinkTransaction, virtualDecayTransaction } from '@/util/virtualTransactions'
|
||||
|
||||
import { BalanceResolver } from './BalanceResolver'
|
||||
import { getCommunityByUuid, getCommunityName, isHomeCommunity } from './util/communities'
|
||||
import { getCommunityByIdentifier, getCommunityName, isHomeCommunity } from './util/communities'
|
||||
import { findUserByIdentifier } from './util/findUserByIdentifier'
|
||||
import { getLastTransaction } from './util/getLastTransaction'
|
||||
import { getTransactionList } from './util/getTransactionList'
|
||||
@ -452,7 +452,7 @@ export class TransactionResolver {
|
||||
if (!CONFIG.FEDERATION_XCOM_SENDCOINS_ENABLED) {
|
||||
throw new LogError('X-Community sendCoins disabled per configuration!')
|
||||
}
|
||||
const recipCom = await getCommunityByUuid(recipientCommunityIdentifier)
|
||||
const recipCom = await getCommunityByIdentifier(recipientCommunityIdentifier)
|
||||
logger.debug('recipient commuity: ', recipCom)
|
||||
if (recipCom === null) {
|
||||
throw new LogError(
|
||||
|
||||
@ -1258,6 +1258,8 @@ describe('UserResolver', () => {
|
||||
|
||||
describe('valid alias', () => {
|
||||
it('updates the user in DB', async () => {
|
||||
// first empty alias, because currently updating alias isn't allowed
|
||||
await User.update({ alias: 'BBB' }, { alias: () => 'NULL' })
|
||||
await mutate({
|
||||
mutation: updateUserInfos,
|
||||
variables: {
|
||||
@ -1303,8 +1305,10 @@ describe('UserResolver', () => {
|
||||
mutation: updateUserInfos,
|
||||
variables: {
|
||||
gmsAllowed: false,
|
||||
gmsPublishName: GmsPublishNameType.GMS_PUBLISH_NAME_FIRST_INITIAL,
|
||||
gmsPublishLocation: GmsPublishLocationType.GMS_LOCATION_TYPE_APPROXIMATE,
|
||||
gmsPublishName:
|
||||
GmsPublishNameType[GmsPublishNameType.GMS_PUBLISH_NAME_FIRST_INITIAL],
|
||||
gmsPublishLocation:
|
||||
GmsPublishLocationType[GmsPublishLocationType.GMS_LOCATION_TYPE_APPROXIMATE],
|
||||
},
|
||||
})
|
||||
await expect(User.find()).resolves.toEqual([
|
||||
@ -1326,9 +1330,11 @@ describe('UserResolver', () => {
|
||||
mutation: updateUserInfos,
|
||||
variables: {
|
||||
gmsAllowed: true,
|
||||
gmsPublishName: GmsPublishNameType.GMS_PUBLISH_NAME_ALIAS_OR_INITALS,
|
||||
gmsPublishName:
|
||||
GmsPublishNameType[GmsPublishNameType.GMS_PUBLISH_NAME_ALIAS_OR_INITALS],
|
||||
gmsLocation: loc,
|
||||
gmsPublishLocation: GmsPublishLocationType.GMS_LOCATION_TYPE_RANDOM,
|
||||
gmsPublishLocation:
|
||||
GmsPublishLocationType[GmsPublishLocationType.GMS_LOCATION_TYPE_RANDOM],
|
||||
},
|
||||
})
|
||||
await expect(User.find()).resolves.toEqual([
|
||||
@ -2670,13 +2676,12 @@ describe('UserResolver', () => {
|
||||
mutation: login,
|
||||
variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' },
|
||||
})
|
||||
// first set alias to null, because updating alias isn't currently allowed
|
||||
await User.update({ alias: 'BBB' }, { alias: () => 'NULL' })
|
||||
await mutate({
|
||||
mutation: updateUserInfos,
|
||||
variables: {
|
||||
alias: 'bibi',
|
||||
gmsAllowed: true,
|
||||
gmsPublishName: GmsPublishNameType.GMS_PUBLISH_NAME_ALIAS_OR_INITALS,
|
||||
gmsPublishLocation: GmsPublishLocationType.GMS_LOCATION_TYPE_RANDOM,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
@ -19,8 +19,6 @@ import { SearchUsersFilters } from '@arg/SearchUsersFilters'
|
||||
import { SetUserRoleArgs } from '@arg/SetUserRoleArgs'
|
||||
import { UnsecureLoginArgs } from '@arg/UnsecureLoginArgs'
|
||||
import { UpdateUserInfosArgs } from '@arg/UpdateUserInfosArgs'
|
||||
import { GmsPublishLocationType } from '@enum/GmsPublishLocationType'
|
||||
import { GmsPublishNameType } from '@enum/GmsPublishNameType'
|
||||
import { OptInType } from '@enum/OptInType'
|
||||
import { Order } from '@enum/Order'
|
||||
import { PasswordEncryptionType } from '@enum/PasswordEncryptionType'
|
||||
@ -561,17 +559,6 @@ export class UserResolver {
|
||||
logger.info(
|
||||
`updateUserInfos(${firstName}, ${lastName}, ${alias}, ${language}, ***, ***, ${hideAmountGDD}, ${hideAmountGDT}, ${gmsAllowed}, ${gmsPublishName}, ${gmsLocation}, ${gmsPublishLocation})...`,
|
||||
)
|
||||
// check default arg settings
|
||||
if (gmsAllowed === null || gmsAllowed === undefined) {
|
||||
gmsAllowed = true
|
||||
}
|
||||
if (!gmsPublishName) {
|
||||
gmsPublishName = GmsPublishNameType.GMS_PUBLISH_NAME_ALIAS_OR_INITALS
|
||||
}
|
||||
if (!gmsPublishLocation) {
|
||||
gmsPublishLocation = GmsPublishLocationType.GMS_LOCATION_TYPE_RANDOM
|
||||
}
|
||||
|
||||
const user = getUser(context)
|
||||
// try {
|
||||
if (firstName) {
|
||||
@ -582,7 +569,8 @@ export class UserResolver {
|
||||
user.lastName = lastName
|
||||
}
|
||||
|
||||
if (alias && (await validateAlias(alias))) {
|
||||
// currently alias can only be set, not updated
|
||||
if (alias && !user.alias && (await validateAlias(alias))) {
|
||||
user.alias = alias
|
||||
}
|
||||
|
||||
@ -619,13 +607,18 @@ export class UserResolver {
|
||||
if (hideAmountGDT !== undefined) {
|
||||
user.hideAmountGDT = hideAmountGDT
|
||||
}
|
||||
|
||||
user.gmsAllowed = gmsAllowed
|
||||
user.gmsPublishName = gmsPublishName
|
||||
if (gmsAllowed !== undefined) {
|
||||
user.gmsAllowed = gmsAllowed
|
||||
}
|
||||
if (gmsPublishName !== null && gmsPublishName !== undefined) {
|
||||
user.gmsPublishName = gmsPublishName
|
||||
}
|
||||
if (gmsLocation) {
|
||||
user.location = Location2Point(gmsLocation)
|
||||
}
|
||||
user.gmsPublishLocation = gmsPublishLocation
|
||||
if (gmsPublishLocation !== null && gmsPublishLocation !== undefined) {
|
||||
user.gmsPublishLocation = gmsPublishLocation
|
||||
}
|
||||
// } catch (err) {
|
||||
// console.log('error:', err)
|
||||
// }
|
||||
|
||||
@ -1,65 +1,146 @@
|
||||
import { FindOneOptions } from '@dbTools/typeorm'
|
||||
import { Community as DbCommunity } from '@entity/Community'
|
||||
import { FederatedCommunity as DbFederatedCommunity } from '@entity/FederatedCommunity'
|
||||
|
||||
import { Paginated } from '@arg/Paginated'
|
||||
|
||||
import { LogError } from '@/server/LogError'
|
||||
import { Connection } from '@/typeorm/connection'
|
||||
|
||||
function findWithCommunityIdentifier(communityIdentifier: string): FindOneOptions<DbCommunity> {
|
||||
return {
|
||||
where: [
|
||||
{ communityUuid: communityIdentifier },
|
||||
{ name: communityIdentifier },
|
||||
{ url: communityIdentifier },
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a community with the given identifier exists and is not foreign.
|
||||
* @param communityIdentifier The identifier (URL, UUID, or name) of the community.
|
||||
* @returns A promise that resolves to true if a non-foreign community exists with the given identifier, otherwise false.
|
||||
*/
|
||||
export async function isHomeCommunity(communityIdentifier: string): Promise<boolean> {
|
||||
const homeCommunity = await DbCommunity.findOne({
|
||||
// The !! operator in JavaScript or TypeScript is a shorthand for converting a value to a boolean.
|
||||
// It essentially converts any truthy value to true and any falsy value to false.
|
||||
return !!(await DbCommunity.findOne({
|
||||
where: [
|
||||
{ foreign: false, communityUuid: communityIdentifier },
|
||||
{ foreign: false, name: communityIdentifier },
|
||||
{ foreign: false, url: communityIdentifier },
|
||||
],
|
||||
})
|
||||
if (homeCommunity) {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the home community, i.e., a community that is not foreign.
|
||||
* @returns A promise that resolves to the home community, or throw if no home community was found
|
||||
*/
|
||||
export async function getHomeCommunity(): Promise<DbCommunity> {
|
||||
return await DbCommunity.findOneOrFail({
|
||||
where: [{ foreign: false }],
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO: Check if it is needed, because currently it isn't used at all
|
||||
* Retrieves the URL of the community with the given identifier.
|
||||
* @param communityIdentifier The identifier (URL, UUID, or name) of the community.
|
||||
* @returns A promise that resolves to the URL of the community or throw if no community with this identifier was found
|
||||
*/
|
||||
export async function getCommunityUrl(communityIdentifier: string): Promise<string> {
|
||||
const community = await DbCommunity.findOneOrFail({
|
||||
where: [
|
||||
{ communityUuid: communityIdentifier },
|
||||
{ name: communityIdentifier },
|
||||
{ url: communityIdentifier },
|
||||
],
|
||||
})
|
||||
return community.url
|
||||
return (await DbCommunity.findOneOrFail(findWithCommunityIdentifier(communityIdentifier))).url
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO: Check if it is needed, because currently it isn't used at all
|
||||
* Checks if a community with the given identifier exists and has an authenticatedAt property set.
|
||||
* @param communityIdentifier The identifier (URL, UUID, or name) of the community.
|
||||
* @returns A promise that resolves to true if a community with an authenticatedAt property exists with the given identifier,
|
||||
* otherwise false.
|
||||
*/
|
||||
export async function isCommunityAuthenticated(communityIdentifier: string): Promise<boolean> {
|
||||
const community = await DbCommunity.findOne({
|
||||
where: [
|
||||
{ communityUuid: communityIdentifier },
|
||||
{ name: communityIdentifier },
|
||||
{ url: communityIdentifier },
|
||||
],
|
||||
})
|
||||
if (community?.authenticatedAt) {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
// The !! operator in JavaScript or TypeScript is a shorthand for converting a value to a boolean.
|
||||
// It essentially converts any truthy value to true and any falsy value to false.
|
||||
return !!(await DbCommunity.findOne(findWithCommunityIdentifier(communityIdentifier)))
|
||||
?.authenticatedAt
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the name of the community with the given identifier.
|
||||
* @param communityIdentifier The identifier (URL, UUID) of the community.
|
||||
* @returns A promise that resolves to the name of the community. If the community does not exist or has no name,
|
||||
* an empty string is returned.
|
||||
*/
|
||||
export async function getCommunityName(communityIdentifier: string): Promise<string> {
|
||||
const community = await DbCommunity.findOne({
|
||||
where: [{ communityUuid: communityIdentifier }, { url: communityIdentifier }],
|
||||
})
|
||||
if (community?.name) {
|
||||
return community.name
|
||||
} else {
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
return community?.name ? community.name : ''
|
||||
}
|
||||
export async function getCommunityByUuid(communityUuid: string): Promise<DbCommunity | null> {
|
||||
return await DbCommunity.findOne({
|
||||
where: [{ communityUuid }],
|
||||
})
|
||||
}
|
||||
|
||||
export async function getCommunityByIdentifier(
|
||||
communityIdentifier: string,
|
||||
): Promise<DbCommunity | null> {
|
||||
return await DbCommunity.findOne(findWithCommunityIdentifier(communityIdentifier))
|
||||
}
|
||||
|
||||
/**
|
||||
* Simulate RIGHT Join between Communities and Federated Communities
|
||||
* select *
|
||||
* Community as c
|
||||
* RIGHT JOIN FederatedCommunity as f
|
||||
* ON(c.public_key = f.public_key)
|
||||
* Typeorm don't has right joins
|
||||
* @returns
|
||||
*/
|
||||
export async function getAllCommunities({
|
||||
pageSize = 25,
|
||||
currentPage = 1,
|
||||
}: Paginated): Promise<DbCommunity[]> {
|
||||
const connection = await Connection.getInstance()
|
||||
if (!connection) {
|
||||
throw new LogError('Cannot connect to db')
|
||||
}
|
||||
// foreign: 'ASC',
|
||||
// createdAt: 'DESC',
|
||||
// lastAnnouncedAt: 'DESC',
|
||||
const result = await connection
|
||||
.getRepository(DbFederatedCommunity)
|
||||
.createQueryBuilder('federatedCommunity')
|
||||
.leftJoinAndSelect('federatedCommunity.community', 'community')
|
||||
.orderBy('federatedCommunity.foreign', 'ASC')
|
||||
.addOrderBy('federatedCommunity.createdAt', 'DESC')
|
||||
.addOrderBy('federatedCommunity.lastAnnouncedAt', 'DESC')
|
||||
.skip((currentPage - 1) * pageSize * 3)
|
||||
.take(pageSize * 3)
|
||||
.getManyAndCount()
|
||||
const communityMap = new Map<string, DbCommunity>()
|
||||
result[0].forEach((value: DbFederatedCommunity) => {
|
||||
const publicKeyHex = value.publicKey.toString('hex')
|
||||
if (!communityMap.has(value.publicKey.toString('hex'))) {
|
||||
let community: DbCommunity = DbCommunity.create()
|
||||
if (value.community) {
|
||||
community = value.community
|
||||
}
|
||||
if (!community.federatedCommunities) {
|
||||
community.federatedCommunities = []
|
||||
}
|
||||
communityMap.set(publicKeyHex, community)
|
||||
}
|
||||
const community = communityMap.get(publicKeyHex)
|
||||
if (!community?.federatedCommunities) {
|
||||
throw new LogError('missing community after set it into map', publicKeyHex)
|
||||
}
|
||||
community.federatedCommunities.push(value)
|
||||
})
|
||||
return Array.from(communityMap.values())
|
||||
}
|
||||
|
||||
@ -2,9 +2,11 @@ import { FindOptionsWhere } from '@dbTools/typeorm'
|
||||
import { Community } from '@entity/Community'
|
||||
import { User as DbUser } from '@entity/User'
|
||||
import { UserContact as DbUserContact } from '@entity/UserContact'
|
||||
import { isURL } from 'class-validator'
|
||||
import { validate, version } from 'uuid'
|
||||
|
||||
import { LogError } from '@/server/LogError'
|
||||
import { isEMail, isUUID4 } from '@/util/validate'
|
||||
|
||||
import { VALID_ALIAS_REGEX } from './validateAlias'
|
||||
|
||||
@ -19,10 +21,11 @@ export const findUserByIdentifier = async (
|
||||
communityIdentifier: string,
|
||||
): Promise<DbUser> => {
|
||||
let user: DbUser | null
|
||||
const communityWhere: FindOptionsWhere<Community> =
|
||||
validate(communityIdentifier) && version(communityIdentifier) === 4
|
||||
? { communityUuid: communityIdentifier }
|
||||
: { name: communityIdentifier }
|
||||
const communityWhere: FindOptionsWhere<Community> = isURL(communityIdentifier)
|
||||
? { url: communityIdentifier }
|
||||
: isUUID4(communityIdentifier)
|
||||
? { communityUuid: communityIdentifier }
|
||||
: { name: communityIdentifier }
|
||||
|
||||
if (validate(identifier) && version(identifier) === 4) {
|
||||
user = await DbUser.findOne({
|
||||
@ -32,7 +35,7 @@ export const findUserByIdentifier = async (
|
||||
if (!user) {
|
||||
throw new LogError('No user found to given identifier(s)', identifier, communityIdentifier)
|
||||
}
|
||||
} else if (/^.{2,}@.{2,}\..{2,}$/.exec(identifier)) {
|
||||
} else if (isEMail(identifier)) {
|
||||
const userContact = await DbUserContact.findOne({
|
||||
where: {
|
||||
email: identifier,
|
||||
|
||||
@ -35,9 +35,9 @@ export const updateUserInfos = gql`
|
||||
$hideAmountGDD: Boolean
|
||||
$hideAmountGDT: Boolean
|
||||
$gmsAllowed: Boolean
|
||||
$gmsPublishName: Int
|
||||
$gmsPublishName: GmsPublishNameType
|
||||
$gmsLocation: Location
|
||||
$gmsPublishLocation: Int
|
||||
$gmsPublishLocation: GmsPublishLocationType
|
||||
) {
|
||||
updateUserInfos(
|
||||
firstName: $firstName
|
||||
|
||||
@ -134,9 +134,25 @@ export const communitiesQuery = gql`
|
||||
}
|
||||
`
|
||||
|
||||
export const getCommunityByUuidQuery = gql`
|
||||
query ($communityUuid: String!) {
|
||||
community(communityUuid: $communityUuid) {
|
||||
export const getCommunityByIdentifierQuery = gql`
|
||||
query ($communityIdentifier: String!) {
|
||||
communityByIdentifier(communityIdentifier: $communityIdentifier) {
|
||||
id
|
||||
foreign
|
||||
name
|
||||
description
|
||||
url
|
||||
creationDate
|
||||
uuid
|
||||
authenticatedAt
|
||||
gmsApiKey
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const getHomeCommunityQuery = gql`
|
||||
query {
|
||||
homeCommunity {
|
||||
id
|
||||
foreign
|
||||
name
|
||||
@ -156,7 +172,8 @@ export const getCommunities = gql`
|
||||
id
|
||||
foreign
|
||||
publicKey
|
||||
url
|
||||
endPoint
|
||||
apiVersion
|
||||
lastAnnouncedAt
|
||||
verifiedAt
|
||||
lastErrorAt
|
||||
@ -166,6 +183,34 @@ export const getCommunities = gql`
|
||||
}
|
||||
`
|
||||
|
||||
export const allCommunities = gql`
|
||||
query {
|
||||
allCommunities {
|
||||
foreign
|
||||
url
|
||||
publicKey
|
||||
uuid
|
||||
authenticatedAt
|
||||
name
|
||||
description
|
||||
gmsApiKey
|
||||
creationDate
|
||||
createdAt
|
||||
updatedAt
|
||||
federatedCommunities {
|
||||
id
|
||||
apiVersion
|
||||
endPoint
|
||||
lastAnnouncedAt
|
||||
verifiedAt
|
||||
lastErrorAt
|
||||
createdAt
|
||||
updatedAt
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const queryTransactionLink = gql`
|
||||
query ($code: String!) {
|
||||
queryTransactionLink(code: $code) {
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { TransactionLink as dbTransactionLink } from '@entity/TransactionLink'
|
||||
import { Decimal } from 'decimal.js-light'
|
||||
import { validate, version } from 'uuid'
|
||||
|
||||
import { Decay } from '@model/Decay'
|
||||
|
||||
@ -16,6 +17,14 @@ function isStringBoolean(value: string): boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
function isUUID4(value: string): boolean {
|
||||
return validate(value) && version(value) === 4
|
||||
}
|
||||
|
||||
function isEMail(value: string): boolean {
|
||||
return /^.{2,}@.{2,}\..{2,}$/.exec(value) !== null
|
||||
}
|
||||
|
||||
async function calculateBalance(
|
||||
userId: number,
|
||||
amount: Decimal,
|
||||
@ -42,4 +51,4 @@ async function calculateBalance(
|
||||
return { balance, lastTransactionId: lastTransaction.id, decay }
|
||||
}
|
||||
|
||||
export { calculateBalance, isStringBoolean }
|
||||
export { calculateBalance, isStringBoolean, isUUID4, isEMail }
|
||||
|
||||
@ -49,6 +49,7 @@
|
||||
"paths": { /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
|
||||
"@/*": ["src/*"],
|
||||
"@arg/*": ["src/graphql/arg/*"],
|
||||
"@input/*": ["src/graphql/input/*"],
|
||||
"@dltConnector/*": ["src/apis/dltConnector/*"],
|
||||
"@enum/*": ["src/graphql/enum/*"],
|
||||
"@model/*": ["src/graphql/model/*"],
|
||||
|
||||
@ -0,0 +1,78 @@
|
||||
import {
|
||||
BaseEntity,
|
||||
Entity,
|
||||
PrimaryGeneratedColumn,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
UpdateDateColumn,
|
||||
OneToMany,
|
||||
JoinColumn,
|
||||
} from 'typeorm'
|
||||
import { FederatedCommunity } from '../FederatedCommunity'
|
||||
import { User } from '../User'
|
||||
|
||||
@Entity('communities')
|
||||
export class Community extends BaseEntity {
|
||||
@PrimaryGeneratedColumn('increment', { unsigned: true })
|
||||
id: number
|
||||
|
||||
@Column({ name: 'foreign', type: 'bool', nullable: false, default: true })
|
||||
foreign: boolean
|
||||
|
||||
@Column({ name: 'url', length: 255, nullable: false })
|
||||
url: string
|
||||
|
||||
@Column({ name: 'public_key', type: 'binary', length: 32, nullable: false })
|
||||
publicKey: Buffer
|
||||
|
||||
@Column({ name: 'private_key', type: 'binary', length: 64, nullable: true })
|
||||
privateKey: Buffer | null
|
||||
|
||||
@Column({
|
||||
name: 'community_uuid',
|
||||
type: 'char',
|
||||
length: 36,
|
||||
nullable: true,
|
||||
collation: 'utf8mb4_unicode_ci',
|
||||
})
|
||||
communityUuid: string | null
|
||||
|
||||
@Column({ name: 'authenticated_at', type: 'datetime', nullable: true })
|
||||
authenticatedAt: Date | null
|
||||
|
||||
@Column({ name: 'name', type: 'varchar', length: 40, nullable: true })
|
||||
name: string | null
|
||||
|
||||
@Column({ name: 'description', type: 'varchar', length: 255, nullable: true })
|
||||
description: string | null
|
||||
|
||||
@CreateDateColumn({ name: 'creation_date', type: 'datetime', nullable: true })
|
||||
creationDate: Date | null
|
||||
|
||||
@Column({ name: 'gms_api_key', type: 'varchar', length: 512, nullable: true, default: null })
|
||||
gmsApiKey: string | null
|
||||
|
||||
@CreateDateColumn({
|
||||
name: 'created_at',
|
||||
type: 'datetime',
|
||||
default: () => 'CURRENT_TIMESTAMP(3)',
|
||||
nullable: false,
|
||||
})
|
||||
createdAt: Date
|
||||
|
||||
@UpdateDateColumn({
|
||||
name: 'updated_at',
|
||||
type: 'datetime',
|
||||
onUpdate: 'CURRENT_TIMESTAMP(3)',
|
||||
nullable: true,
|
||||
})
|
||||
updatedAt: Date | null
|
||||
|
||||
@OneToMany(() => User, (user) => user.community)
|
||||
@JoinColumn({ name: 'community_uuid', referencedColumnName: 'communityUuid' })
|
||||
users: User[]
|
||||
|
||||
@OneToMany(() => FederatedCommunity, (federatedCommunity) => federatedCommunity.community)
|
||||
@JoinColumn({ name: 'public_key', referencedColumnName: 'publicKey' })
|
||||
federatedCommunities?: FederatedCommunity[]
|
||||
}
|
||||
@ -0,0 +1,58 @@
|
||||
import {
|
||||
BaseEntity,
|
||||
Entity,
|
||||
PrimaryGeneratedColumn,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
UpdateDateColumn,
|
||||
ManyToOne,
|
||||
JoinColumn,
|
||||
} from 'typeorm'
|
||||
import { Community } from '../Community'
|
||||
|
||||
@Entity('federated_communities')
|
||||
export class FederatedCommunity extends BaseEntity {
|
||||
@PrimaryGeneratedColumn('increment', { unsigned: true })
|
||||
id: number
|
||||
|
||||
@Column({ name: 'foreign', type: 'bool', nullable: false, default: true })
|
||||
foreign: boolean
|
||||
|
||||
@Column({ name: 'public_key', type: 'binary', length: 32, default: null, nullable: true })
|
||||
publicKey: Buffer
|
||||
|
||||
@Column({ name: 'api_version', length: 10, nullable: false })
|
||||
apiVersion: string
|
||||
|
||||
@Column({ name: 'end_point', length: 255, nullable: false })
|
||||
endPoint: string
|
||||
|
||||
@Column({ name: 'last_announced_at', type: 'datetime', nullable: true })
|
||||
lastAnnouncedAt: Date | null
|
||||
|
||||
@Column({ name: 'verified_at', type: 'datetime', nullable: true })
|
||||
verifiedAt: Date | null
|
||||
|
||||
@Column({ name: 'last_error_at', type: 'datetime', nullable: true })
|
||||
lastErrorAt: Date | null
|
||||
|
||||
@CreateDateColumn({
|
||||
name: 'created_at',
|
||||
type: 'datetime',
|
||||
default: () => 'CURRENT_TIMESTAMP(3)',
|
||||
nullable: false,
|
||||
})
|
||||
createdAt: Date
|
||||
|
||||
@UpdateDateColumn({
|
||||
name: 'updated_at',
|
||||
type: 'datetime',
|
||||
onUpdate: 'CURRENT_TIMESTAMP(3)',
|
||||
nullable: true,
|
||||
})
|
||||
updatedAt: Date | null
|
||||
|
||||
@ManyToOne(() => Community, (community) => community.federatedCommunities)
|
||||
@JoinColumn({ name: 'public_key', referencedColumnName: 'publicKey' })
|
||||
community?: Community
|
||||
}
|
||||
@ -1 +1 @@
|
||||
export { Community } from './0082-introduce_gms_registration/Community'
|
||||
export { Community } from './0083-join_community_federated_communities/Community'
|
||||
|
||||
@ -1 +1 @@
|
||||
export { FederatedCommunity } from './0068-community_tables_public_key_length/FederatedCommunity'
|
||||
export { FederatedCommunity } from './0083-join_community_federated_communities/FederatedCommunity'
|
||||
|
||||
@ -0,0 +1,3 @@
|
||||
export async function upgrade(queryFn: (query: string, values?: any[]) => Promise<Array<any>>) {}
|
||||
|
||||
export async function downgrade(queryFn: (query: string, values?: any[]) => Promise<Array<any>>) {}
|
||||
@ -26,7 +26,7 @@ EMAIL_CODE_REQUEST_TIME=10
|
||||
# config versions
|
||||
DATABASE_CONFIG_VERSION=v1.2022-03-18
|
||||
BACKEND_CONFIG_VERSION=v21.2024-01-06
|
||||
FRONTEND_CONFIG_VERSION=v5.2024-01-08
|
||||
FRONTEND_CONFIG_VERSION=v6.2024-02-27
|
||||
ADMIN_CONFIG_VERSION=v2.2024-01-04
|
||||
FEDERATION_CONFIG_VERSION=v2.2023-08-24
|
||||
FEDERATION_DHT_CONFIG_VERSION=v4.2024-01-17
|
||||
@ -120,7 +120,7 @@ DEFAULT_PUBLISHER_ID=2896
|
||||
WEBHOOK_ELOPAGE_SECRET=secret
|
||||
|
||||
# GMS
|
||||
#GMS_ACTIVE=true
|
||||
GMS_ACTIVE=false
|
||||
# Coordinates of Illuminz test instance
|
||||
#GMS_URL=http://54.176.169.179:3071
|
||||
#GMS_URL=http://localhost:4044/
|
||||
GMS_URL=http://localhost:4044/
|
||||
|
||||
@ -4,7 +4,7 @@ import dotenv from 'dotenv'
|
||||
dotenv.config()
|
||||
|
||||
const constants = {
|
||||
DB_VERSION: '0082-introduce_gms_registration',
|
||||
DB_VERSION: '0083-join_community_federated_communities',
|
||||
LOG4JS_CONFIG: 'log4js-config.json',
|
||||
// default log level on production should be info
|
||||
LOG_LEVEL: process.env.LOG_LEVEL ?? 'info',
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
CONFIG_VERSION=v4.2023-09-12
|
||||
CONFIG_VERSION=v6.2024-02-20
|
||||
|
||||
# SET LOG LEVEL AS NEEDED IN YOUR .ENV
|
||||
# POSSIBLE VALUES: all | trace | debug | info | warn | error | fatal
|
||||
@ -19,4 +19,8 @@ DB_DATABASE_TEST=gradido_dlt_test
|
||||
TYPEORM_LOGGING_RELATIVE_PATH=typeorm.backend.log
|
||||
|
||||
# DLT-Connector
|
||||
DLT_CONNECTOR_PORT=6010
|
||||
DLT_CONNECTOR_PORT=6010
|
||||
|
||||
# Route to Backend
|
||||
BACKEND_SERVER_URL=http://localhost:4000
|
||||
JWT_SECRET=secret123
|
||||
@ -1,5 +1,7 @@
|
||||
CONFIG_VERSION=$DLT_CONNECTOR_CONFIG_VERSION
|
||||
|
||||
JWT_SECRET=$JWT_SECRET
|
||||
|
||||
#IOTA
|
||||
IOTA_API_URL=$IOTA_API_URL
|
||||
IOTA_COMMUNITY_ALIAS=$IOTA_COMMUNITY_ALIAS
|
||||
@ -15,4 +17,7 @@ DB_DATABASE_TEST=$DB_DATABASE_TEST
|
||||
TYPEORM_LOGGING_RELATIVE_PATH=typeorm.backend.log
|
||||
|
||||
# DLT-Connector
|
||||
DLT_CONNECTOR_PORT=$DLT_CONNECTOR_PORT
|
||||
DLT_CONNECTOR_PORT=$DLT_CONNECTOR_PORT
|
||||
|
||||
# Route to Backend
|
||||
BACKEND_SERVER_URL=http://localhost:4000
|
||||
@ -6,7 +6,7 @@ module.exports = {
|
||||
collectCoverageFrom: ['src/**/*.ts', '!**/node_modules/**', '!src/seeds/**', '!build/**'],
|
||||
coverageThreshold: {
|
||||
global: {
|
||||
lines: 75,
|
||||
lines: 72,
|
||||
},
|
||||
},
|
||||
setupFiles: ['<rootDir>/test/testSetup.ts'],
|
||||
|
||||
@ -31,8 +31,10 @@
|
||||
"express": "4.17.1",
|
||||
"express-slow-down": "^2.0.1",
|
||||
"graphql": "^16.7.1",
|
||||
"graphql-request": "^6.1.0",
|
||||
"graphql-scalars": "^1.22.2",
|
||||
"helmet": "^7.1.0",
|
||||
"jose": "^5.2.2",
|
||||
"log4js": "^6.7.1",
|
||||
"nodemon": "^2.0.20",
|
||||
"protobufjs": "^7.2.5",
|
||||
|
||||
105
dlt-connector/src/client/BackendClient.ts
Normal file
105
dlt-connector/src/client/BackendClient.ts
Normal file
@ -0,0 +1,105 @@
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||
import { gql, GraphQLClient } from 'graphql-request'
|
||||
import { SignJWT } from 'jose'
|
||||
|
||||
import { CONFIG } from '@/config'
|
||||
import { CommunityDraft } from '@/graphql/input/CommunityDraft'
|
||||
import { logger } from '@/logging/logger'
|
||||
import { LogError } from '@/server/LogError'
|
||||
|
||||
const homeCommunity = gql`
|
||||
query {
|
||||
homeCommunity {
|
||||
uuid
|
||||
foreign
|
||||
creationDate
|
||||
}
|
||||
}
|
||||
`
|
||||
interface Community {
|
||||
homeCommunity: {
|
||||
uuid: string
|
||||
foreign: boolean
|
||||
creationDate: string
|
||||
}
|
||||
}
|
||||
// Source: https://refactoring.guru/design-patterns/singleton/typescript/example
|
||||
// and ../federation/client/FederationClientFactory.ts
|
||||
/**
|
||||
* A Singleton class defines the `getInstance` method that lets clients access
|
||||
* the unique singleton instance.
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-extraneous-class
|
||||
export class BackendClient {
|
||||
// eslint-disable-next-line no-use-before-define
|
||||
private static instance: BackendClient
|
||||
client: GraphQLClient
|
||||
/**
|
||||
* The Singleton's constructor should always be private to prevent direct
|
||||
* construction calls with the `new` operator.
|
||||
*/
|
||||
// eslint-disable-next-line no-useless-constructor, @typescript-eslint/no-empty-function
|
||||
private constructor() {}
|
||||
|
||||
/**
|
||||
* The static method that controls the access to the singleton instance.
|
||||
*
|
||||
* This implementation let you subclass the Singleton class while keeping
|
||||
* just one instance of each subclass around.
|
||||
*/
|
||||
public static getInstance(): BackendClient | undefined {
|
||||
if (!BackendClient.instance) {
|
||||
BackendClient.instance = new BackendClient()
|
||||
}
|
||||
if (!BackendClient.instance.client) {
|
||||
try {
|
||||
BackendClient.instance.client = new GraphQLClient(CONFIG.BACKEND_SERVER_URL, {
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
},
|
||||
method: 'GET',
|
||||
jsonSerializer: {
|
||||
parse: JSON.parse,
|
||||
stringify: JSON.stringify,
|
||||
},
|
||||
})
|
||||
} catch (e) {
|
||||
logger.error("couldn't connect to backend: ", e)
|
||||
return
|
||||
}
|
||||
}
|
||||
return BackendClient.instance
|
||||
}
|
||||
|
||||
public async getHomeCommunityDraft(): Promise<CommunityDraft> {
|
||||
logger.info('check home community on backend')
|
||||
const { data, errors } = await this.client.rawRequest<Community>(
|
||||
homeCommunity,
|
||||
{},
|
||||
{
|
||||
authorization: 'Bearer ' + (await this.createJWTToken()),
|
||||
},
|
||||
)
|
||||
if (errors) {
|
||||
throw new LogError('error getting home community from backend', errors)
|
||||
}
|
||||
const communityDraft = new CommunityDraft()
|
||||
communityDraft.uuid = data.homeCommunity.uuid
|
||||
communityDraft.foreign = data.homeCommunity.foreign
|
||||
communityDraft.createdAt = data.homeCommunity.creationDate
|
||||
return communityDraft
|
||||
}
|
||||
|
||||
private async createJWTToken(): Promise<string> {
|
||||
const secret = new TextEncoder().encode(CONFIG.JWT_SECRET)
|
||||
const token = await new SignJWT({ gradidoID: 'dlt-connector', 'urn:gradido:claim': true })
|
||||
.setProtectedHeader({ alg: 'HS256' })
|
||||
.setIssuedAt()
|
||||
.setIssuer('urn:gradido:issuer')
|
||||
.setAudience('urn:gradido:audience')
|
||||
.setExpirationTime('1m')
|
||||
.sign(secret)
|
||||
return token
|
||||
}
|
||||
}
|
||||
@ -9,13 +9,14 @@ const constants = {
|
||||
LOG_LEVEL: process.env.LOG_LEVEL ?? 'info',
|
||||
CONFIG_VERSION: {
|
||||
DEFAULT: 'DEFAULT',
|
||||
EXPECTED: 'v4.2023-09-12',
|
||||
EXPECTED: 'v6.2024-02-20',
|
||||
CURRENT: '',
|
||||
},
|
||||
}
|
||||
|
||||
const server = {
|
||||
PRODUCTION: process.env.NODE_ENV === 'production' ?? false,
|
||||
JWT_SECRET: process.env.JWT_SECRET ?? 'secret123',
|
||||
}
|
||||
|
||||
const database = {
|
||||
@ -38,6 +39,10 @@ const dltConnector = {
|
||||
DLT_CONNECTOR_PORT: process.env.DLT_CONNECTOR_PORT ?? 6010,
|
||||
}
|
||||
|
||||
const backendServer = {
|
||||
BACKEND_SERVER_URL: process.env.BACKEND_SERVER_URL ?? 'http://backend:4000',
|
||||
}
|
||||
|
||||
// Check config version
|
||||
constants.CONFIG_VERSION.CURRENT = process.env.CONFIG_VERSION ?? constants.CONFIG_VERSION.DEFAULT
|
||||
if (
|
||||
@ -56,4 +61,5 @@ export const CONFIG = {
|
||||
...database,
|
||||
...iota,
|
||||
...dltConnector,
|
||||
...backendServer,
|
||||
}
|
||||
|
||||
@ -1,10 +1,42 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import 'reflect-metadata'
|
||||
|
||||
import { CONFIG } from '@/config'
|
||||
|
||||
import { BackendClient } from './client/BackendClient'
|
||||
import { CommunityRepository } from './data/Community.repository'
|
||||
import { Mnemonic } from './data/Mnemonic'
|
||||
import { CommunityDraft } from './graphql/input/CommunityDraft'
|
||||
import { AddCommunityContext } from './interactions/backendToDb/community/AddCommunity.context'
|
||||
import { logger } from './logging/logger'
|
||||
import createServer from './server/createServer'
|
||||
import { LogError } from './server/LogError'
|
||||
import { stopTransmitToIota, transmitToIota } from './tasks/transmitToIota'
|
||||
|
||||
async function waitForServer(
|
||||
backend: BackendClient,
|
||||
retryIntervalMs: number,
|
||||
maxRetries: number,
|
||||
): Promise<CommunityDraft> {
|
||||
let retries = 0
|
||||
while (retries < maxRetries) {
|
||||
logger.info(`Attempt ${retries + 1} for connecting to backend`)
|
||||
|
||||
try {
|
||||
// Make a HEAD request to the server
|
||||
return await backend.getHomeCommunityDraft()
|
||||
} catch (error) {
|
||||
logger.info('Server is not reachable: ', error)
|
||||
}
|
||||
|
||||
// Server is not reachable, wait and retry
|
||||
await new Promise((resolve) => setTimeout(resolve, retryIntervalMs))
|
||||
retries++
|
||||
}
|
||||
|
||||
throw new LogError('Max retries exceeded. Server did not become reachable.')
|
||||
}
|
||||
|
||||
async function main() {
|
||||
if (CONFIG.IOTA_HOME_COMMUNITY_SEED) {
|
||||
Mnemonic.validateSeed(CONFIG.IOTA_HOME_COMMUNITY_SEED)
|
||||
@ -13,6 +45,22 @@ async function main() {
|
||||
console.log(`DLT_CONNECTOR_PORT=${CONFIG.DLT_CONNECTOR_PORT}`)
|
||||
const { app } = await createServer()
|
||||
|
||||
// ask backend for home community if we haven't one
|
||||
try {
|
||||
await CommunityRepository.loadHomeCommunityKeyPair()
|
||||
} catch (e) {
|
||||
const backend = BackendClient.getInstance()
|
||||
if (!backend) {
|
||||
throw new LogError('cannot create backend client')
|
||||
}
|
||||
// wait for backend server to be ready
|
||||
await waitForServer(backend, 2500, 10)
|
||||
|
||||
const communityDraft = await backend.getHomeCommunityDraft()
|
||||
const addCommunityContext = new AddCommunityContext(communityDraft)
|
||||
await addCommunityContext.run()
|
||||
}
|
||||
|
||||
// loop run all the time, check for new transaction for sending to iota
|
||||
void transmitToIota()
|
||||
app.listen(CONFIG.DLT_CONNECTOR_PORT, () => {
|
||||
|
||||
@ -12,6 +12,7 @@ import { TransactionError } from '@/graphql/model/TransactionError'
|
||||
import { CommunityLoggingView } from '@/logging/CommunityLogging.view'
|
||||
import { logger } from '@/logging/logger'
|
||||
import { InterruptiveSleepManager } from '@/manager/InterruptiveSleepManager'
|
||||
import { LogError } from '@/server/LogError'
|
||||
import { getDataSource } from '@/typeorm/DataSource'
|
||||
|
||||
import { CreateTransactionRecipeContext } from '../transaction/CreateTransationRecipe.context'
|
||||
@ -24,7 +25,19 @@ export class HomeCommunityRole extends CommunityRole {
|
||||
public async create(communityDraft: CommunityDraft, topic: string): Promise<void> {
|
||||
super.create(communityDraft, topic)
|
||||
// generate key pair for signing transactions and deriving all keys for community
|
||||
const keyPair = new KeyPair(new Mnemonic(CONFIG.IOTA_HOME_COMMUNITY_SEED ?? undefined))
|
||||
let mnemonic: Mnemonic
|
||||
try {
|
||||
mnemonic = new Mnemonic(CONFIG.IOTA_HOME_COMMUNITY_SEED ?? undefined)
|
||||
} catch (e) {
|
||||
throw new LogError(
|
||||
'error creating mnemonic for home community, please fill IOTA_HOME_COMMUNITY_SEED in .env',
|
||||
{
|
||||
IOTA_HOME_COMMUNITY_SEED: CONFIG.IOTA_HOME_COMMUNITY_SEED,
|
||||
error: e,
|
||||
},
|
||||
)
|
||||
}
|
||||
const keyPair = new KeyPair(mnemonic)
|
||||
keyPair.fillInCommunityKeys(this.self)
|
||||
|
||||
// create auf account and gmw account
|
||||
|
||||
@ -569,7 +569,7 @@
|
||||
"@graphql-typed-document-node/core" "^3.1.1"
|
||||
tslib "^2.4.0"
|
||||
|
||||
"@graphql-typed-document-node/core@^3.1.1":
|
||||
"@graphql-typed-document-node/core@^3.1.1", "@graphql-typed-document-node/core@^3.2.0":
|
||||
version "3.2.0"
|
||||
resolved "https://registry.yarnpkg.com/@graphql-typed-document-node/core/-/core-3.2.0.tgz#5f3d96ec6b2354ad6d8a28bf216a1d97b5426861"
|
||||
integrity sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ==
|
||||
@ -2119,6 +2119,13 @@ cross-env@^7.0.3:
|
||||
dependencies:
|
||||
cross-spawn "^7.0.1"
|
||||
|
||||
cross-fetch@^3.1.5:
|
||||
version "3.1.8"
|
||||
resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.1.8.tgz#0327eba65fd68a7d119f8fb2bf9334a1a7956f82"
|
||||
integrity sha512-cvA+JwZoU0Xq+h6WkMvAUqPEYy92Obet6UdKLfW60qn99ftItKjB5T+BkyWOFWe2pUyfQ+IJHmpOTznqk1M6Kg==
|
||||
dependencies:
|
||||
node-fetch "^2.6.12"
|
||||
|
||||
cross-spawn@^7.0.0, cross-spawn@^7.0.1, cross-spawn@^7.0.2, cross-spawn@^7.0.3:
|
||||
version "7.0.3"
|
||||
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6"
|
||||
@ -3329,6 +3336,14 @@ graphql-query-complexity@^0.12.0:
|
||||
dependencies:
|
||||
lodash.get "^4.4.2"
|
||||
|
||||
graphql-request@^6.1.0:
|
||||
version "6.1.0"
|
||||
resolved "https://registry.yarnpkg.com/graphql-request/-/graphql-request-6.1.0.tgz#f4eb2107967af3c7a5907eb3131c671eac89be4f"
|
||||
integrity sha512-p+XPfS4q7aIpKVcgmnZKhMNqhltk20hfXtkaIkTfjjmiKMJ5xrt5c743cL03y/K7y1rg3WrIC49xGiEQ4mxdNw==
|
||||
dependencies:
|
||||
"@graphql-typed-document-node/core" "^3.2.0"
|
||||
cross-fetch "^3.1.5"
|
||||
|
||||
graphql-scalars@^1.22.2:
|
||||
version "1.22.2"
|
||||
resolved "https://registry.yarnpkg.com/graphql-scalars/-/graphql-scalars-1.22.2.tgz#6326e6fe2d0ad4228a9fea72a977e2bf26b86362"
|
||||
@ -4266,6 +4281,11 @@ jiti@^1.19.3:
|
||||
resolved "https://registry.yarnpkg.com/jiti/-/jiti-1.20.0.tgz#2d823b5852ee8963585c8dd8b7992ffc1ae83b42"
|
||||
integrity sha512-3TV69ZbrvV6U5DfQimop50jE9Dl6J8O1ja1dvBbMba/sZ3YBEQqJ2VZRoQPVnhlzjNtU1vaXRZVrVjU4qtm8yA==
|
||||
|
||||
jose@^5.2.2:
|
||||
version "5.2.2"
|
||||
resolved "https://registry.yarnpkg.com/jose/-/jose-5.2.2.tgz#b91170e9ba6dbe609b0c0a86568f9a1fbe4335c0"
|
||||
integrity sha512-/WByRr4jDcsKlvMd1dRJnPfS1GVO3WuKyaurJ/vvXcOaUQO8rnNObCQMlv/5uCceVQIq5Q4WLF44ohsdiTohdg==
|
||||
|
||||
js-tokens@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
|
||||
@ -4775,7 +4795,7 @@ node-abort-controller@^3.1.1:
|
||||
resolved "https://registry.yarnpkg.com/node-abort-controller/-/node-abort-controller-3.1.1.tgz#a94377e964a9a37ac3976d848cb5c765833b8548"
|
||||
integrity sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==
|
||||
|
||||
node-fetch@^2.6.7:
|
||||
node-fetch@^2.6.12, node-fetch@^2.6.7:
|
||||
version "2.7.0"
|
||||
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.7.0.tgz#d0f0fa6e3e2dc1d27efcd8ad99d550bda94d187d"
|
||||
integrity sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==
|
||||
|
||||
@ -275,28 +275,45 @@ It contains a map-component of the leaflet library and offers to capture the use
|
||||
|
||||
There is no user-specific authentication nor autorization necessary for this dialog as mentioned above.
|
||||
|
||||
### GMS user playground dialog (gms efforts)
|
||||
### GMS user playground dialog
|
||||
|
||||
As described in the chapter "User search" above, we need a dialog in GMS to display in a graphical map:
|
||||
|
||||
* the location of the user as a red needle, who opens the user search-dialog
|
||||
* the location of his community as a circle, the invoker belongs to
|
||||
* the locations of all other users as white needles, belonging to the same community
|
||||
* the locations of all other users belonging to the same community as white/gray or black needles - depending on the location-type of the user
|
||||
* circles and needles of all other communities and users, which are nearby the requesting user and community location
|
||||
|
||||
There is no user-specific authentication nor autorization necessary for this dialog as mentioned above.
|
||||
On activation of the menu-entry _user-search_ a technical flow in the background have to prepare the connection between the gradido-system and the gms-component. The following list will describe the necessary steps of all involved components:
|
||||
|
||||
Which (filter-)components this playground-dialog should have next to the graphical location map is not clear at the moment. In the first run to display the above mentioned locations of users and communities with circles and needles will be sufficient.
|
||||
* **gradido-frontend:** user press the menu entry _user search_
|
||||
* **(1.a) gradido-frontend:** invokes the gradido-backend `authUserForGmsUserSearch`
|
||||
* **(1.b) gradido-backend:** the method `authUserForGmsUserSearch` reads the context-user of the current request and the uuid of the user's home-community. With these values it prepares the parameters for invokation of the `gms.verifyAuthToken` method. The first parameter is set by the `community-uuid` and the second parameter is a JWT-token with the encrypted `user-uuid` in the payload and signed by the community's privateKey
|
||||
* **(2.a) gradido-backend:** invokes the `gms.verifyAuthToken` with community-uuid as 1st parameter and JWT-Token as 2nd parameter
|
||||
* **(2.b) gms-backend:** recieving the request `verifyAuthToken` with the community-uuid and the JWT-token. After searching and verifing the given community-uuid exists in gms, it prepares the invokation of the configured endpoint `community-Auth-Url` of this community by sending the given JWT-token as parameter back to gradido.
|
||||
* **(3.a) gms-backend:** invokes the endpoint configured in `gms.community-auth-url` with the previous received JWT-token
|
||||
* **(3.b) gradido-backend:** receives the request at the endpoint "communityAuthUrl" with the previously sent JWT-token. The JWT-token will be verified if the signature is valid and afterwards the payload is decrypted to verify the contained user-data will match with the current context-user of request (1).
|
||||
* **(4.a) gradido-backend:** in case of valid JWT-token signature and valid payload data the gradido-backend returns TRUE as result of the authentication-request otherwise FALSE.
|
||||
* **(4.b) gms-backend:** receives the response of request (3) and in case of TRUE the gms-backend prepares to return a complete URI including a _JWT-access-token_ to be used for entering the user-playground. *It will not return gms-data used for the user-playground as the current implementation of the api `verify-auth-token` do.* In case of FALSE prepare returning an authentication-error.
|
||||
* **(5.a) gms-backend:** returning the complete URI including a _JWT-access-token_ as response of request (2) or an authentication-error
|
||||
* **(5.b) gradido-backend:** receiving as response of request (2) a complete URI including a _JWT-access-token_ for entering the users-playground on gms or an authentication-error
|
||||
* **(6.a) gradido-backend:** returning the complete URI including a _JWT-access-token_ as response of request (1) or an expressive error message
|
||||
* **(6.b) gradido-frontend:** receiving the complete URI including a _JWT-access-token_ after activation of menu-entry "user-search" or an expressive error-message, which will end the _user search_-flow without requesting the gms-frontend (7).
|
||||
* **(7.a) gradido-frontend:** on opening a new browser-window the gradido-frontend uses the received URI with the _JWT-access-token_ to enter the gms user-playground
|
||||
* **(7.b) gms-frontend:** receiving the request for the user-playground with an _JWT-access-token_. After verifying the access-token the gms-frontend will read the data for the user given by the access-token and loads all necessary data to render the users playground
|
||||
|
||||
The detailed requirements will come up as soon as we get some user expiriences and feedbacks.
|
||||
The following picture shows the logical flow and interaction between the involved components:
|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
The detailed requirements for the playground-dialog will come up as soon as we get some user expiriences and feedbacks.
|
||||
|
||||
### GMS Offer Capture dialog (gms efforts)
|
||||
|
||||
will come later...
|
||||
|
||||
|
||||
|
||||
### GMS Need Capture dialog (gms efforts)
|
||||
|
||||
will come later...
|
||||
|
||||
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 42 KiB |
@ -10,7 +10,7 @@ Decimal.set({
|
||||
})
|
||||
|
||||
const constants = {
|
||||
DB_VERSION: '0082-introduce_gms_registration',
|
||||
DB_VERSION: '0083-join_community_federated_communities',
|
||||
DECAY_START_TIME: new Date('2021-05-13 17:46:31-0000'), // GMT+0
|
||||
LOG4JS_CONFIG: 'log4js-config.json',
|
||||
// default log level on production should be info
|
||||
|
||||
@ -21,3 +21,5 @@ META_DESCRIPTION_EN="Gratitude is the currency of the new age. More and more peo
|
||||
META_KEYWORDS_DE="Grundeinkommen, Währung, Dankbarkeit, Schenk-Ökonomie, Natürliche Ökonomie des Lebens, Ökonomie, Ökologie, Potenzialentfaltung, Schenken und Danken, Kreislauf des Lebens, Geldsystem"
|
||||
META_KEYWORDS_EN="Basic Income, Currency, Gratitude, Gift Economy, Natural Economy of Life, Economy, Ecology, Potential Development, Giving and Thanking, Cycle of Life, Monetary System"
|
||||
META_AUTHOR="Bernd Hückstädt - Gradido-Akademie"
|
||||
|
||||
GMS_ACTIVE=false
|
||||
|
||||
@ -24,3 +24,5 @@ META_DESCRIPTION_EN=$META_DESCRIPTION_EN
|
||||
META_KEYWORDS_DE=$META_KEYWORDS_DE
|
||||
META_KEYWORDS_EN=$META_KEYWORDS_EN
|
||||
META_AUTHOR=$META_AUTHOR
|
||||
|
||||
GMS_ACTIVE=$GMS_ACTIVE
|
||||
@ -71,4 +71,16 @@ export default {
|
||||
.text-color-gdd-yellow {
|
||||
color: rgb(197 141 56);
|
||||
}
|
||||
|
||||
.dropdown > .dropdown-toggle {
|
||||
border-radius: 17px;
|
||||
height: 50px;
|
||||
text-align: left;
|
||||
}
|
||||
.dropdown-toggle::after {
|
||||
float: right;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
position: relative;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -90,20 +90,3 @@ export default {
|
||||
},
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
.community-switch > div,
|
||||
.community-switch ul.dropdown-menu {
|
||||
width: 100%;
|
||||
}
|
||||
.community-switch > div > button {
|
||||
border-radius: 17px;
|
||||
height: 50px;
|
||||
text-align: left;
|
||||
}
|
||||
.community-switch .dropdown-toggle::after {
|
||||
float: right;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
position: relative;
|
||||
}
|
||||
</style>
|
||||
|
||||
8
frontend/src/components/UserSettings/UserGMSLocation.vue
Normal file
8
frontend/src/components/UserSettings/UserGMSLocation.vue
Normal file
@ -0,0 +1,8 @@
|
||||
<template>
|
||||
<b-button>{{ $t('settings.GMS.location.button') }}</b-button>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
name: 'UserGMSLocation',
|
||||
}
|
||||
</script>
|
||||
@ -0,0 +1,79 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import UserGMSLocationFormat from './UserGMSLocationFormat.vue'
|
||||
import { toastErrorSpy } from '@test/testSetup'
|
||||
|
||||
const mockAPIcall = jest.fn()
|
||||
|
||||
const storeCommitMock = jest.fn()
|
||||
|
||||
const localVue = global.localVue
|
||||
|
||||
describe('UserGMSLocationFormat', () => {
|
||||
let wrapper
|
||||
beforeEach(() => {
|
||||
wrapper = mount(UserGMSLocationFormat, {
|
||||
mocks: {
|
||||
$t: (key) => key, // Mocking the translation function
|
||||
$store: {
|
||||
state: {
|
||||
gmsPublishLocation: null,
|
||||
},
|
||||
commit: storeCommitMock,
|
||||
},
|
||||
$apollo: {
|
||||
mutate: mockAPIcall,
|
||||
},
|
||||
},
|
||||
localVue,
|
||||
propsData: {
|
||||
selectedOption: 'GMS_LOCATION_TYPE_RANDOM',
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy()
|
||||
})
|
||||
|
||||
it('renders the correct dropdown options', () => {
|
||||
const dropdownItems = wrapper.findAll('.dropdown-item')
|
||||
expect(dropdownItems.length).toBe(3)
|
||||
|
||||
const labels = dropdownItems.wrappers.map((item) => item.text())
|
||||
expect(labels).toEqual([
|
||||
'settings.GMS.publish-location.exact',
|
||||
'settings.GMS.publish-location.approximate',
|
||||
'settings.GMS.publish-location.random',
|
||||
])
|
||||
})
|
||||
|
||||
it('updates selected option on click', async () => {
|
||||
const dropdownItem = wrapper.findAll('.dropdown-item').at(1) // Click the second item
|
||||
await dropdownItem.trigger('click')
|
||||
|
||||
expect(wrapper.emitted().gmsPublishLocation).toBeTruthy()
|
||||
expect(wrapper.emitted().gmsPublishLocation.length).toBe(1)
|
||||
expect(wrapper.emitted().gmsPublishLocation[0]).toEqual(['GMS_LOCATION_TYPE_APPROXIMATE'])
|
||||
})
|
||||
|
||||
it('does not update when clicking on already selected option', async () => {
|
||||
const dropdownItem = wrapper.findAll('.dropdown-item').at(2) // Click the third item (which is already selected)
|
||||
await dropdownItem.trigger('click')
|
||||
|
||||
expect(wrapper.emitted().gmsPublishLocation).toBeFalsy()
|
||||
})
|
||||
|
||||
describe('update with error', () => {
|
||||
beforeEach(async () => {
|
||||
mockAPIcall.mockRejectedValue({
|
||||
message: 'Ouch',
|
||||
})
|
||||
const dropdownItem = wrapper.findAll('.dropdown-item').at(1) // Click the second item
|
||||
await dropdownItem.trigger('click')
|
||||
})
|
||||
|
||||
it('toasts an error message', () => {
|
||||
expect(toastErrorSpy).toBeCalledWith('Ouch')
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,73 @@
|
||||
<template>
|
||||
<div class="user-gms-location-format">
|
||||
<b-dropdown v-model="selectedOption">
|
||||
<template slot="button-content">{{ selectedOptionLabel }}</template>
|
||||
<b-dropdown-item
|
||||
v-for="option in dropdownOptions"
|
||||
@click.prevent="update(option)"
|
||||
:key="option.value"
|
||||
:value="option.value"
|
||||
>
|
||||
{{ option.label }}
|
||||
</b-dropdown-item>
|
||||
</b-dropdown>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { updateUserInfos } from '@/graphql/mutations'
|
||||
|
||||
export default {
|
||||
name: 'UserGMSLocationFormat',
|
||||
data() {
|
||||
return {
|
||||
selectedOption: this.$store.state.gmsPublishLocation ?? 'GMS_LOCATION_TYPE_RANDOM',
|
||||
dropdownOptions: [
|
||||
{
|
||||
label: this.$t('settings.GMS.publish-location.exact'),
|
||||
value: 'GMS_LOCATION_TYPE_EXACT',
|
||||
},
|
||||
{
|
||||
label: this.$t('settings.GMS.publish-location.approximate'),
|
||||
value: 'GMS_LOCATION_TYPE_APPROXIMATE',
|
||||
},
|
||||
{
|
||||
label: this.$t('settings.GMS.publish-location.random'),
|
||||
value: 'GMS_LOCATION_TYPE_RANDOM',
|
||||
},
|
||||
],
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
selectedOptionLabel() {
|
||||
return this.dropdownOptions.find((option) => option.value === this.selectedOption).label
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
async update(option) {
|
||||
if (option.value === this.selectedOption) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
await this.$apollo.mutate({
|
||||
mutation: updateUserInfos,
|
||||
variables: {
|
||||
gmsPublishLocation: option.value,
|
||||
},
|
||||
})
|
||||
this.toastSuccess(this.$t('settings.GMS.publish-location.updated'))
|
||||
this.selectedOption = option.value
|
||||
this.$store.commit('gmsPublishLocation', option.value)
|
||||
this.$emit('gmsPublishLocation', option.value)
|
||||
} catch (error) {
|
||||
this.toastError(error.message)
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
.user-gms-location-format > .dropdown,
|
||||
.user-gms-location-format > .dropdown > .dropdown-toggle > ul.dropdown-menu {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,81 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import UserGMSNamingFormat from './UserGMSNamingFormat.vue'
|
||||
import { toastErrorSpy } from '@test/testSetup'
|
||||
|
||||
const mockAPIcall = jest.fn()
|
||||
|
||||
const storeCommitMock = jest.fn()
|
||||
|
||||
const localVue = global.localVue
|
||||
|
||||
describe('UserGMSNamingFormat', () => {
|
||||
let wrapper
|
||||
beforeEach(() => {
|
||||
wrapper = mount(UserGMSNamingFormat, {
|
||||
mocks: {
|
||||
$t: (key) => key, // Mocking the translation function
|
||||
$store: {
|
||||
state: {
|
||||
gmsPublishName: null,
|
||||
},
|
||||
commit: storeCommitMock,
|
||||
},
|
||||
$apollo: {
|
||||
mutate: mockAPIcall,
|
||||
},
|
||||
},
|
||||
localVue,
|
||||
propsData: {
|
||||
selectedOption: 'GMS_PUBLISH_NAME_ALIAS_OR_INITALS',
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy()
|
||||
})
|
||||
|
||||
it('renders the correct dropdown options', () => {
|
||||
const dropdownItems = wrapper.findAll('.dropdown-item')
|
||||
expect(dropdownItems.length).toBe(5)
|
||||
|
||||
const labels = dropdownItems.wrappers.map((item) => item.text())
|
||||
expect(labels).toEqual([
|
||||
'settings.GMS.publish-name.alias-or-initials',
|
||||
'settings.GMS.publish-name.initials',
|
||||
'settings.GMS.publish-name.first',
|
||||
'settings.GMS.publish-name.first-initial',
|
||||
'settings.GMS.publish-name.name-full',
|
||||
])
|
||||
})
|
||||
|
||||
it('updates selected option on click', async () => {
|
||||
const dropdownItem = wrapper.findAll('.dropdown-item').at(3) // Click the fourth item
|
||||
await dropdownItem.trigger('click')
|
||||
|
||||
expect(wrapper.emitted().gmsPublishName).toBeTruthy()
|
||||
expect(wrapper.emitted().gmsPublishName.length).toBe(1)
|
||||
expect(wrapper.emitted().gmsPublishName[0]).toEqual(['GMS_PUBLISH_NAME_FIRST_INITIAL'])
|
||||
})
|
||||
|
||||
it('does not update when clicking on already selected option', async () => {
|
||||
const dropdownItem = wrapper.findAll('.dropdown-item').at(0) // Click the first item (which is already selected)
|
||||
await dropdownItem.trigger('click')
|
||||
|
||||
expect(wrapper.emitted().gmsPublishName).toBeFalsy()
|
||||
})
|
||||
|
||||
describe('update with error', () => {
|
||||
beforeEach(async () => {
|
||||
mockAPIcall.mockRejectedValue({
|
||||
message: 'Ouch',
|
||||
})
|
||||
const dropdownItem = wrapper.findAll('.dropdown-item').at(2) // Click the third item
|
||||
await dropdownItem.trigger('click')
|
||||
})
|
||||
|
||||
it('toasts an error message', () => {
|
||||
expect(toastErrorSpy).toBeCalledWith('Ouch')
|
||||
})
|
||||
})
|
||||
})
|
||||
87
frontend/src/components/UserSettings/UserGMSNamingFormat.vue
Normal file
87
frontend/src/components/UserSettings/UserGMSNamingFormat.vue
Normal file
@ -0,0 +1,87 @@
|
||||
<template>
|
||||
<div class="user-gms-naming-format">
|
||||
<b-dropdown v-model="selectedOption">
|
||||
<template slot="button-content">{{ selectedOptionLabel }}</template>
|
||||
<b-dropdown-item
|
||||
v-for="option in dropdownOptions"
|
||||
@click.prevent="update(option)"
|
||||
:key="option.value"
|
||||
:value="option.value"
|
||||
:title="option.title"
|
||||
>
|
||||
{{ option.label }}
|
||||
</b-dropdown-item>
|
||||
</b-dropdown>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { updateUserInfos } from '@/graphql/mutations'
|
||||
|
||||
export default {
|
||||
name: 'UserGMSNamingFormat',
|
||||
data() {
|
||||
return {
|
||||
selectedOption: this.$store.state.gmsPublishName ?? 'GMS_PUBLISH_NAME_ALIAS_OR_INITALS',
|
||||
dropdownOptions: [
|
||||
{
|
||||
label: this.$t('settings.GMS.publish-name.alias-or-initials'),
|
||||
title: this.$t('settings.GMS.publish-name.alias-or-initials-tooltip'),
|
||||
value: 'GMS_PUBLISH_NAME_ALIAS_OR_INITALS',
|
||||
},
|
||||
{
|
||||
label: this.$t('settings.GMS.publish-name.initials'),
|
||||
title: this.$t('settings.GMS.publish-name.initials-tooltip'),
|
||||
value: 'GMS_PUBLISH_NAME_INITIALS',
|
||||
},
|
||||
{
|
||||
label: this.$t('settings.GMS.publish-name.first'),
|
||||
title: this.$t('settings.GMS.publish-name.first-tooltip'),
|
||||
value: 'GMS_PUBLISH_NAME_FIRST',
|
||||
},
|
||||
{
|
||||
label: this.$t('settings.GMS.publish-name.first-initial'),
|
||||
title: this.$t('settings.GMS.publish-name.first-initial-tooltip'),
|
||||
value: 'GMS_PUBLISH_NAME_FIRST_INITIAL',
|
||||
},
|
||||
{
|
||||
label: this.$t('settings.GMS.publish-name.name-full'),
|
||||
title: this.$t('settings.GMS.publish-name.name-full-tooltip'),
|
||||
value: 'GMS_PUBLISH_NAME_FULL',
|
||||
},
|
||||
],
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
selectedOptionLabel() {
|
||||
return this.dropdownOptions.find((option) => option.value === this.selectedOption).label
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
async update(option) {
|
||||
if (option.value === this.selectedOption) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
await this.$apollo.mutate({
|
||||
mutation: updateUserInfos,
|
||||
variables: {
|
||||
gmsPublishName: option.value,
|
||||
},
|
||||
})
|
||||
this.toastSuccess(this.$t('settings.GMS.publish-name.updated'))
|
||||
this.selectedOption = option.value
|
||||
this.$store.commit('gmsPublishName', option.value)
|
||||
this.$emit('gmsPublishName', option.value)
|
||||
} catch (error) {
|
||||
this.toastError(error.message)
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
.user-gms-naming-format > .dropdown,
|
||||
.user-gms-naming-format > .dropdown > .dropdown-toggle > ul.dropdown-menu {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
45
frontend/src/components/UserSettings/UserGMSSwitch.vue
Normal file
45
frontend/src/components/UserSettings/UserGMSSwitch.vue
Normal file
@ -0,0 +1,45 @@
|
||||
<template>
|
||||
<div class="form-user-gms-switch">
|
||||
<b-form-checkbox
|
||||
test="BFormCheckbox"
|
||||
v-model="gmsAllowed"
|
||||
name="check-button"
|
||||
switch
|
||||
@change="onChange"
|
||||
></b-form-checkbox>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { updateUserInfos } from '@/graphql/mutations'
|
||||
|
||||
export default {
|
||||
name: 'UserGMSSwitch',
|
||||
data() {
|
||||
return {
|
||||
gmsAllowed: this.$store.state.gmsAllowed,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async onChange() {
|
||||
this.$apollo
|
||||
.mutate({
|
||||
mutation: updateUserInfos,
|
||||
variables: {
|
||||
gmsAllowed: this.gmsAllowed,
|
||||
},
|
||||
})
|
||||
.then(() => {
|
||||
this.$store.commit('gmsAllowed', this.gmsAllowed)
|
||||
this.$emit('gmsAllowed', this.gmsAllowed)
|
||||
this.toastSuccess(
|
||||
this.gmsAllowed ? this.$t('settings.GMS.enabled') : this.$t('settings.GMS.disabled'),
|
||||
)
|
||||
})
|
||||
.catch((error) => {
|
||||
this.gmsAllowed = this.$store.state.gmsAllowed
|
||||
this.toastError(error.message)
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@ -8,7 +8,7 @@ const constants = {
|
||||
DECAY_START_TIME: new Date('2021-05-13 17:46:31-0000'), // GMT+0
|
||||
CONFIG_VERSION: {
|
||||
DEFAULT: 'DEFAULT',
|
||||
EXPECTED: 'v5.2024-01-08',
|
||||
EXPECTED: 'v6.2024-02-27',
|
||||
CURRENT: '',
|
||||
},
|
||||
}
|
||||
@ -20,6 +20,10 @@ const version = {
|
||||
BUILD_COMMIT_SHORT: (process.env.BUILD_COMMIT ?? '0000000').slice(0, 7),
|
||||
}
|
||||
|
||||
const features = {
|
||||
GMS_ACTIVE: process.env.GMS_ACTIVE ?? false,
|
||||
}
|
||||
|
||||
const environment = {
|
||||
NODE_ENV: process.env.NODE_ENV,
|
||||
DEBUG: process.env.NODE_ENV !== 'production' ?? false,
|
||||
@ -81,6 +85,7 @@ if (
|
||||
const CONFIG = {
|
||||
...constants,
|
||||
...version,
|
||||
...features,
|
||||
...environment,
|
||||
...endpoints,
|
||||
...community,
|
||||
|
||||
@ -35,9 +35,9 @@ export const updateUserInfos = gql`
|
||||
$hideAmountGDD: Boolean
|
||||
$hideAmountGDT: Boolean
|
||||
$gmsAllowed: Boolean
|
||||
$gmsPublishName: Int
|
||||
$gmsPublishName: GmsPublishNameType
|
||||
$gmsLocation: Location
|
||||
$gmsPublishLocation: Int
|
||||
$gmsPublishLocation: GmsPublishLocationType
|
||||
) {
|
||||
updateUserInfos(
|
||||
firstName: $firstName
|
||||
@ -172,6 +172,9 @@ export const login = gql`
|
||||
klickTipp {
|
||||
newsletterState
|
||||
}
|
||||
gmsAllowed
|
||||
gmsPublishName
|
||||
gmsPublishLocation
|
||||
hasElopage
|
||||
publisherId
|
||||
roles
|
||||
|
||||
@ -5,8 +5,10 @@
|
||||
"1000thanks": "1000 Dank, weil du bei uns bist!",
|
||||
"125": "125%",
|
||||
"85": "85%",
|
||||
"ExternServices": "Verknüpfte Dienste",
|
||||
"GDD": "GDD",
|
||||
"GDT": "GDT",
|
||||
"GMS": "Gradido Karte",
|
||||
"PersonalDetails": "Persönliche Angaben",
|
||||
"advanced-calculation": "Vorausberechnung",
|
||||
"asterisks": "****",
|
||||
@ -290,6 +292,36 @@
|
||||
},
|
||||
"settings": {
|
||||
"emailInfo": "Kann aktuell noch nicht geändert werden.",
|
||||
"GMS": {
|
||||
"disabled": "Daten werden nicht nach GMS exportiert",
|
||||
"enabled": "Daten werden nach GMS exportiert",
|
||||
"location": {
|
||||
"label": "Positionsbestimmung",
|
||||
"button": "Klick mich!"
|
||||
},
|
||||
"location-format": "Positionstyp",
|
||||
"naming-format": "Namensformat im GMS",
|
||||
"publish-location": {
|
||||
"exact": "Genaue Position",
|
||||
"approximate": "Ungefähre Position",
|
||||
"random": "Zufallsposition",
|
||||
"updated": "Positionstyp für GMS aktualisiert"
|
||||
},
|
||||
"publish-name": {
|
||||
"alias-or-initials": "Benutzername oder Initialen",
|
||||
"alias-or-initials-tooltip": "Benutzername, falls vorhanden, oder die Initialen von Vorname und Nachname",
|
||||
"first": "Vorname",
|
||||
"first-tooltip": "Nur der Vornamen",
|
||||
"first-initial": "Vorname und Initiale",
|
||||
"first-initial-tooltip": "Vornamen plus Anfangsbuchstabe des Nachnamens",
|
||||
"initials": "Initialen",
|
||||
"initials-tooltip": "Initialen von Vor- und Nachname unabhängig von der Existenz des Benutzernamens",
|
||||
"name-full": "Ganzer Name",
|
||||
"name-full-tooltip": "Vollständiger Name: Vorname plus Nachname",
|
||||
"updated": "Namensformat für GMS aktualisiert"
|
||||
},
|
||||
"switch": "Erlaubnis Daten nach GMS zu exportieren."
|
||||
},
|
||||
"hideAmountGDD": "Dein GDD Betrag ist versteckt.",
|
||||
"hideAmountGDT": "Dein GDT Betrag ist versteckt.",
|
||||
"info": "Transaktionen können nun per Benutzername oder E-Mail-Adresse getätigt werden.",
|
||||
|
||||
@ -5,8 +5,10 @@
|
||||
"1000thanks": "1000 thanks for being with us!",
|
||||
"125": "125%",
|
||||
"85": "85%",
|
||||
"ExternServices": "Extern Services",
|
||||
"GDD": "GDD",
|
||||
"GDT": "GDT",
|
||||
"GMS": "Gradido Map",
|
||||
"PersonalDetails": "Personal details",
|
||||
"advanced-calculation": "Advanced calculation",
|
||||
"asterisks": "****",
|
||||
@ -290,6 +292,36 @@
|
||||
},
|
||||
"settings": {
|
||||
"emailInfo": "Cannot be changed at this time.",
|
||||
"GMS": {
|
||||
"disabled": "Data not exported to GMS",
|
||||
"enabled": "Data exported to GMS",
|
||||
"location": {
|
||||
"label": "pinpoint location",
|
||||
"button": "click me!"
|
||||
},
|
||||
"location-format": "location type",
|
||||
"naming-format": "Format of name in GMS",
|
||||
"publish-location": {
|
||||
"exact": "exact position",
|
||||
"approximate": "approximate position",
|
||||
"random": "random position",
|
||||
"updated": "format of location for GMS updated"
|
||||
},
|
||||
"publish-name": {
|
||||
"alias-or-initials": "Username or initials",
|
||||
"alias-or-initials-tooltip": "username if exists or Initials of firstname and lastname",
|
||||
"first": "firstname",
|
||||
"first-tooltip": "the firstname only",
|
||||
"first-initial": "firstname and initial",
|
||||
"first-initial-tooltip": "firstname plus initial of lastname",
|
||||
"initials": "Initials of firstname and lastname independent if username exists",
|
||||
"initials-tooltip": "Initials of firstname and lastname independent if username exists",
|
||||
"name-full": "fullname",
|
||||
"name-full-tooltip": "fullname: firstname plus lastname",
|
||||
"updated": "format of name for GMS updated"
|
||||
},
|
||||
"switch": "Allow data export to GMS"
|
||||
},
|
||||
"hideAmountGDD": "Your GDD amount is hidden.",
|
||||
"hideAmountGDT": "Your GDT amount is hidden.",
|
||||
"info": "Transactions can now be made by username or email address.",
|
||||
|
||||
@ -1,81 +1,129 @@
|
||||
<template>
|
||||
<div class="card bg-white gradido-border-radius appBoxShadow p-4 mt--3">
|
||||
<div class="h2">{{ $t('PersonalDetails') }}</div>
|
||||
<div class="my-4 text-small">
|
||||
{{ $t('settings.info') }}
|
||||
</div>
|
||||
|
||||
<b-row>
|
||||
<b-col cols="12" md="6" lg="6">
|
||||
<user-name />
|
||||
</b-col>
|
||||
<b-col cols="12" md="6" lg="6">
|
||||
<b-form-group :label="$t('form.email')" :description="$t('settings.emailInfo')">
|
||||
<b-form-input v-model="email" readonly></b-form-input>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
</b-row>
|
||||
|
||||
<hr />
|
||||
<b-form>
|
||||
<b-row class="mt-3">
|
||||
<b-col cols="12" md="6" lg="6">
|
||||
<label>{{ $t('form.firstname') }}</label>
|
||||
<b-form-input
|
||||
v-model="firstName"
|
||||
:placeholder="$t('settings.name.enterFirstname')"
|
||||
data-test="firstname"
|
||||
trim
|
||||
></b-form-input>
|
||||
</b-col>
|
||||
<b-col cols="12" md="6" lg="6">
|
||||
<label>{{ $t('form.lastname') }}</label>
|
||||
<b-form-input
|
||||
v-model="lastName"
|
||||
:placeholder="$t('settings.name.enterLastname')"
|
||||
data-test="lastname"
|
||||
trim
|
||||
></b-form-input>
|
||||
</b-col>
|
||||
</b-row>
|
||||
<div v-if="!isDisabled" class="mt-4 pt-4 text-center">
|
||||
<b-button
|
||||
type="submit"
|
||||
variant="primary"
|
||||
@click.prevent="onSubmit"
|
||||
data-test="submit-userdata"
|
||||
>
|
||||
{{ $t('form.save') }}
|
||||
</b-button>
|
||||
</div>
|
||||
</b-form>
|
||||
<hr />
|
||||
<b-row>
|
||||
<b-col cols="12" md="6" lg="6">{{ $t('language') }}</b-col>
|
||||
<b-col cols="12" md="6" lg="6" class="text-right">
|
||||
<user-language />
|
||||
</b-col>
|
||||
</b-row>
|
||||
|
||||
<hr />
|
||||
<div class="mt-5">{{ $t('form.password') }}</div>
|
||||
<user-password />
|
||||
<hr />
|
||||
<b-row class="mb-5">
|
||||
<b-col cols="12" md="6" lg="6">
|
||||
{{ $t('settings.newsletter.newsletter') }}
|
||||
<div class="text-small">
|
||||
{{
|
||||
newsletterState
|
||||
? $t('settings.newsletter.newsletterTrue')
|
||||
: $t('settings.newsletter.newsletterFalse')
|
||||
}}
|
||||
<b-tabs content-class="mt-3">
|
||||
<b-tab :title="$t('PersonalDetails')" active>
|
||||
<div class="h2">{{ $t('PersonalDetails') }}</div>
|
||||
<div class="my-4 text-small">
|
||||
{{ $t('settings.info') }}
|
||||
</div>
|
||||
</b-col>
|
||||
<b-col cols="12" md="6" lg="6" class="text-right">
|
||||
<user-newsletter />
|
||||
</b-col>
|
||||
</b-row>
|
||||
|
||||
<b-row>
|
||||
<b-col cols="12" md="6" lg="6">
|
||||
<user-name />
|
||||
</b-col>
|
||||
<b-col cols="12" md="6" lg="6">
|
||||
<b-form-group :label="$t('form.email')" :description="$t('settings.emailInfo')">
|
||||
<b-form-input v-model="email" readonly></b-form-input>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
</b-row>
|
||||
|
||||
<hr />
|
||||
<b-form>
|
||||
<b-row class="mt-3">
|
||||
<b-col cols="12" md="6" lg="6">
|
||||
<label>{{ $t('form.firstname') }}</label>
|
||||
<b-form-input
|
||||
v-model="firstName"
|
||||
:placeholder="$t('settings.name.enterFirstname')"
|
||||
data-test="firstname"
|
||||
trim
|
||||
></b-form-input>
|
||||
</b-col>
|
||||
<b-col cols="12" md="6" lg="6">
|
||||
<label>{{ $t('form.lastname') }}</label>
|
||||
<b-form-input
|
||||
v-model="lastName"
|
||||
:placeholder="$t('settings.name.enterLastname')"
|
||||
data-test="lastname"
|
||||
trim
|
||||
></b-form-input>
|
||||
</b-col>
|
||||
</b-row>
|
||||
<div v-if="!isDisabled" class="mt-4 pt-4 text-center">
|
||||
<b-button
|
||||
type="submit"
|
||||
variant="primary"
|
||||
@click.prevent="onSubmit"
|
||||
data-test="submit-userdata"
|
||||
>
|
||||
{{ $t('form.save') }}
|
||||
</b-button>
|
||||
</div>
|
||||
</b-form>
|
||||
<hr />
|
||||
<b-row>
|
||||
<b-col cols="12" md="6" lg="6">{{ $t('language') }}</b-col>
|
||||
<b-col cols="12" md="6" lg="6" class="text-right">
|
||||
<user-language />
|
||||
</b-col>
|
||||
</b-row>
|
||||
|
||||
<hr />
|
||||
<div class="mt-5">{{ $t('form.password') }}</div>
|
||||
<user-password />
|
||||
<hr />
|
||||
<b-row class="mb-5">
|
||||
<b-col cols="12" md="6" lg="6">
|
||||
{{ $t('settings.newsletter.newsletter') }}
|
||||
<div class="text-small">
|
||||
{{
|
||||
newsletterState
|
||||
? $t('settings.newsletter.newsletterTrue')
|
||||
: $t('settings.newsletter.newsletterFalse')
|
||||
}}
|
||||
</div>
|
||||
</b-col>
|
||||
<b-col cols="12" md="6" lg="6" class="text-right">
|
||||
<user-newsletter />
|
||||
</b-col>
|
||||
</b-row>
|
||||
</b-tab>
|
||||
<div v-if="isGMS">
|
||||
<b-tab :title="$t('ExternServices')">
|
||||
<div class="h2">{{ $t('ExternServices') }}</div>
|
||||
<div class="h3">{{ $t('GMS') }}</div>
|
||||
<b-row class="mb-3">
|
||||
<b-col cols="12" md="6" lg="6">
|
||||
{{ $t('settings.GMS.switch') }}
|
||||
<div class="text-small">
|
||||
{{ gmsAllowed ? $t('settings.GMS.enabled') : $t('settings.GMS.disabled') }}
|
||||
</div>
|
||||
</b-col>
|
||||
<b-col cols="12" md="6" lg="6" class="text-right">
|
||||
<user-g-m-s-switch @gmsAllowed="gmsStateSwitch" />
|
||||
</b-col>
|
||||
</b-row>
|
||||
<div v-if="gmsAllowed">
|
||||
<b-row class="mb-4">
|
||||
<b-col cols="12" md="6" lg="6">
|
||||
{{ $t('settings.GMS.naming-format') }}
|
||||
</b-col>
|
||||
<b-col cols="12" md="6" lg="6">
|
||||
<user-g-m-s-naming-format />
|
||||
</b-col>
|
||||
</b-row>
|
||||
<b-row class="mb-4">
|
||||
<b-col cols="12" md="6" lg="6">
|
||||
{{ $t('settings.GMS.location-format') }}
|
||||
</b-col>
|
||||
<b-col cols="12" md="6" lg="6">
|
||||
<user-g-m-s-location-format />
|
||||
</b-col>
|
||||
</b-row>
|
||||
<b-row class="mb-5">
|
||||
<b-col cols="12" md="6" lg="6">
|
||||
{{ $t('settings.GMS.location.label') }}
|
||||
</b-col>
|
||||
<b-col cols="12" md="6" lg="6">
|
||||
<user-g-m-s-location />
|
||||
</b-col>
|
||||
</b-row>
|
||||
</div>
|
||||
</b-tab>
|
||||
</div>
|
||||
</b-tabs>
|
||||
|
||||
<!-- TODO<b-row>
|
||||
<b-col cols="12" md="6" lg="6">{{ $t('settings.darkMode') }}</b-col>
|
||||
<b-col cols="12" md="6" lg="6" class="text-right">
|
||||
@ -85,15 +133,24 @@
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import UserGMSSwitch from '@/components/UserSettings/UserGMSSwitch'
|
||||
import UserGMSNamingFormat from '@/components/UserSettings/UserGMSNamingFormat'
|
||||
import UserGMSLocationFormat from '@/components/UserSettings/UserGMSLocationFormat'
|
||||
import UserGMSLocation from '@/components/UserSettings/UserGMSLocation'
|
||||
import UserName from '@/components/UserSettings/UserName.vue'
|
||||
import UserPassword from '@/components/UserSettings/UserPassword'
|
||||
import UserLanguage from '@/components/LanguageSwitch2.vue'
|
||||
import UserNewsletter from '@/components/UserSettings/UserNewsletter.vue'
|
||||
import { updateUserInfos } from '@/graphql/mutations'
|
||||
import CONFIG from '../config'
|
||||
|
||||
export default {
|
||||
name: 'Profile',
|
||||
components: {
|
||||
UserGMSSwitch,
|
||||
UserGMSNamingFormat,
|
||||
UserGMSLocationFormat,
|
||||
UserGMSLocation,
|
||||
UserName,
|
||||
UserPassword,
|
||||
UserLanguage,
|
||||
@ -106,7 +163,7 @@ export default {
|
||||
|
||||
data() {
|
||||
const { state } = this.$store
|
||||
const { darkMode, firstName, lastName, email, newsletterState } = state
|
||||
const { darkMode, firstName, lastName, email, newsletterState, gmsAllowed } = state
|
||||
|
||||
return {
|
||||
darkMode,
|
||||
@ -115,6 +172,7 @@ export default {
|
||||
lastName,
|
||||
email,
|
||||
newsletterState,
|
||||
gmsAllowed,
|
||||
mutation: '',
|
||||
variables: {},
|
||||
}
|
||||
@ -125,6 +183,9 @@ export default {
|
||||
const { firstName, lastName } = this.$store.state
|
||||
return firstName === this.firstName && lastName === this.lastName
|
||||
},
|
||||
isGMS() {
|
||||
return CONFIG.GMS_ACTIVE
|
||||
},
|
||||
},
|
||||
// TODO: watch: {
|
||||
// darkMode(val) {
|
||||
@ -150,6 +211,9 @@ export default {
|
||||
this.toastSuccess(this.$t('settings.name.change-success'))
|
||||
} catch (error) {}
|
||||
},
|
||||
gmsStateSwitch(eventData) {
|
||||
this.gmsAllowed = eventData
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
@ -35,6 +35,15 @@ export const mutations = {
|
||||
newsletterState: (state, newsletterState) => {
|
||||
state.newsletterState = newsletterState
|
||||
},
|
||||
gmsAllowed: (state, gmsAllowed) => {
|
||||
state.gmsAllowed = gmsAllowed
|
||||
},
|
||||
gmsPublishName: (state, gmsPublishName) => {
|
||||
state.gmsPublishName = gmsPublishName
|
||||
},
|
||||
gmsPublishLocation: (state, gmsPublishLocation) => {
|
||||
state.gmsPublishLocation = gmsPublishLocation
|
||||
},
|
||||
publisherId: (state, publisherId) => {
|
||||
let pubId = parseInt(publisherId)
|
||||
if (isNaN(pubId)) pubId = null
|
||||
@ -71,6 +80,9 @@ export const actions = {
|
||||
commit('firstName', data.firstName)
|
||||
commit('lastName', data.lastName)
|
||||
commit('newsletterState', data.klickTipp.newsletterState)
|
||||
commit('gmsAllowed', data.gmsAllowed)
|
||||
commit('gmsPublishName', data.gmsPublishName)
|
||||
commit('gmsPublishLocation', data.gmsPublishLocation)
|
||||
commit('hasElopage', data.hasElopage)
|
||||
commit('publisherId', data.publisherId)
|
||||
commit('roles', data.roles)
|
||||
@ -85,6 +97,9 @@ export const actions = {
|
||||
commit('firstName', '')
|
||||
commit('lastName', '')
|
||||
commit('newsletterState', null)
|
||||
commit('gmsAllowed', null)
|
||||
commit('gmsPublishName', null)
|
||||
commit('gmsPublishLocation', null)
|
||||
commit('hasElopage', false)
|
||||
commit('publisherId', null)
|
||||
commit('roles', null)
|
||||
@ -117,6 +132,9 @@ try {
|
||||
tokenTime: null,
|
||||
roles: [],
|
||||
newsletterState: null,
|
||||
gmsAllowed: null,
|
||||
gmsPublishName: null,
|
||||
gmsPublishLocation: null,
|
||||
hasElopage: false,
|
||||
publisherId: null,
|
||||
hideAmountGDD: null,
|
||||
@ -126,7 +144,7 @@ try {
|
||||
redirectPath: '/overview',
|
||||
},
|
||||
getters: {},
|
||||
// Syncronous mutation of the state
|
||||
// Synchronous mutation of the state
|
||||
mutations,
|
||||
actions,
|
||||
})
|
||||
|
||||
@ -28,6 +28,9 @@ const {
|
||||
lastName,
|
||||
username,
|
||||
newsletterState,
|
||||
gmsAllowed,
|
||||
gmsPublishName,
|
||||
gmsPublishLocation,
|
||||
publisherId,
|
||||
roles,
|
||||
hasElopage,
|
||||
@ -122,6 +125,30 @@ describe('Vuex store', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('gmsAllowed', () => {
|
||||
it('sets the state of gmsAllowed', () => {
|
||||
const state = { gmsAllowed: null }
|
||||
gmsAllowed(state, true)
|
||||
expect(state.gmsAllowed).toEqual(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('gmsPublishName', () => {
|
||||
it('sets gmsPublishName', () => {
|
||||
const state = { gmsPublishName: null }
|
||||
gmsPublishName(state, 'GMS_PUBLISH_NAME_INITIALS')
|
||||
expect(state.gmsPublishName).toEqual('GMS_PUBLISH_NAME_INITIALS')
|
||||
})
|
||||
})
|
||||
|
||||
describe('gmsPublishLocation', () => {
|
||||
it('sets gmsPublishLocation', () => {
|
||||
const state = { gmsPublishLocation: null }
|
||||
gmsPublishLocation(state, 'GMS_LOCATION_TYPE_APPROXIMATE')
|
||||
expect(state.gmsPublishLocation).toEqual('GMS_LOCATION_TYPE_APPROXIMATE')
|
||||
})
|
||||
})
|
||||
|
||||
describe('publisherId', () => {
|
||||
it('sets the state of publisherId', () => {
|
||||
const state = {}
|
||||
@ -190,6 +217,9 @@ describe('Vuex store', () => {
|
||||
klickTipp: {
|
||||
newsletterState: true,
|
||||
},
|
||||
gmsAllowed: true,
|
||||
gmsPublishName: 'GMS_PUBLISH_NAME_FULL',
|
||||
gmsPublishLocation: 'GMS_LOCATION_TYPE_EXACT',
|
||||
hasElopage: false,
|
||||
publisherId: 1234,
|
||||
roles: ['admin'],
|
||||
@ -197,9 +227,9 @@ describe('Vuex store', () => {
|
||||
hideAmountGDT: true,
|
||||
}
|
||||
|
||||
it('calls eleven commits', () => {
|
||||
it('calls fifteen commits', () => {
|
||||
login({ commit, state }, commitedData)
|
||||
expect(commit).toHaveBeenCalledTimes(12)
|
||||
expect(commit).toHaveBeenCalledTimes(15)
|
||||
})
|
||||
|
||||
it('commits gradidoID', () => {
|
||||
@ -232,29 +262,44 @@ describe('Vuex store', () => {
|
||||
expect(commit).toHaveBeenNthCalledWith(6, 'newsletterState', true)
|
||||
})
|
||||
|
||||
it('commits gmsAllowed', () => {
|
||||
login({ commit, state }, commitedData)
|
||||
expect(commit).toHaveBeenNthCalledWith(7, 'gmsAllowed', true)
|
||||
})
|
||||
|
||||
it('commits gmsPublishName', () => {
|
||||
login({ commit, state }, commitedData)
|
||||
expect(commit).toHaveBeenNthCalledWith(8, 'gmsPublishName', 'GMS_PUBLISH_NAME_FULL')
|
||||
})
|
||||
|
||||
it('commits gmsPublishLocation', () => {
|
||||
login({ commit, state }, commitedData)
|
||||
expect(commit).toHaveBeenNthCalledWith(9, 'gmsPublishLocation', 'GMS_LOCATION_TYPE_EXACT')
|
||||
})
|
||||
|
||||
it('commits hasElopage', () => {
|
||||
login({ commit, state }, commitedData)
|
||||
expect(commit).toHaveBeenNthCalledWith(7, 'hasElopage', false)
|
||||
expect(commit).toHaveBeenNthCalledWith(10, 'hasElopage', false)
|
||||
})
|
||||
|
||||
it('commits publisherId', () => {
|
||||
login({ commit, state }, commitedData)
|
||||
expect(commit).toHaveBeenNthCalledWith(8, 'publisherId', 1234)
|
||||
expect(commit).toHaveBeenNthCalledWith(11, 'publisherId', 1234)
|
||||
})
|
||||
|
||||
it('commits roles', () => {
|
||||
login({ commit, state }, commitedData)
|
||||
expect(commit).toHaveBeenNthCalledWith(9, 'roles', ['admin'])
|
||||
expect(commit).toHaveBeenNthCalledWith(12, 'roles', ['admin'])
|
||||
})
|
||||
|
||||
it('commits hideAmountGDD', () => {
|
||||
login({ commit, state }, commitedData)
|
||||
expect(commit).toHaveBeenNthCalledWith(10, 'hideAmountGDD', false)
|
||||
expect(commit).toHaveBeenNthCalledWith(13, 'hideAmountGDD', false)
|
||||
})
|
||||
|
||||
it('commits hideAmountGDT', () => {
|
||||
login({ commit, state }, commitedData)
|
||||
expect(commit).toHaveBeenNthCalledWith(11, 'hideAmountGDT', true)
|
||||
expect(commit).toHaveBeenNthCalledWith(14, 'hideAmountGDT', true)
|
||||
})
|
||||
})
|
||||
|
||||
@ -262,9 +307,9 @@ describe('Vuex store', () => {
|
||||
const commit = jest.fn()
|
||||
const state = {}
|
||||
|
||||
it('calls twelve commits', () => {
|
||||
it('calls seventeen commits', () => {
|
||||
logout({ commit, state })
|
||||
expect(commit).toHaveBeenCalledTimes(14)
|
||||
expect(commit).toHaveBeenCalledTimes(17)
|
||||
})
|
||||
|
||||
it('commits token', () => {
|
||||
@ -297,34 +342,49 @@ describe('Vuex store', () => {
|
||||
expect(commit).toHaveBeenNthCalledWith(6, 'newsletterState', null)
|
||||
})
|
||||
|
||||
it('commits gmsAllowed', () => {
|
||||
logout({ commit, state })
|
||||
expect(commit).toHaveBeenNthCalledWith(7, 'gmsAllowed', null)
|
||||
})
|
||||
|
||||
it('commits gmsPublishName', () => {
|
||||
logout({ commit, state })
|
||||
expect(commit).toHaveBeenNthCalledWith(8, 'gmsPublishName', null)
|
||||
})
|
||||
|
||||
it('commits gmsPublishLocation', () => {
|
||||
logout({ commit, state })
|
||||
expect(commit).toHaveBeenNthCalledWith(9, 'gmsPublishLocation', null)
|
||||
})
|
||||
|
||||
it('commits hasElopage', () => {
|
||||
logout({ commit, state })
|
||||
expect(commit).toHaveBeenNthCalledWith(7, 'hasElopage', false)
|
||||
expect(commit).toHaveBeenNthCalledWith(10, 'hasElopage', false)
|
||||
})
|
||||
|
||||
it('commits publisherId', () => {
|
||||
logout({ commit, state })
|
||||
expect(commit).toHaveBeenNthCalledWith(8, 'publisherId', null)
|
||||
expect(commit).toHaveBeenNthCalledWith(11, 'publisherId', null)
|
||||
})
|
||||
|
||||
it('commits roles', () => {
|
||||
logout({ commit, state })
|
||||
expect(commit).toHaveBeenNthCalledWith(9, 'roles', null)
|
||||
expect(commit).toHaveBeenNthCalledWith(12, 'roles', null)
|
||||
})
|
||||
|
||||
it('commits hideAmountGDD', () => {
|
||||
logout({ commit, state })
|
||||
expect(commit).toHaveBeenNthCalledWith(10, 'hideAmountGDD', false)
|
||||
expect(commit).toHaveBeenNthCalledWith(13, 'hideAmountGDD', false)
|
||||
})
|
||||
|
||||
it('commits hideAmountGDT', () => {
|
||||
logout({ commit, state })
|
||||
expect(commit).toHaveBeenNthCalledWith(11, 'hideAmountGDT', true)
|
||||
expect(commit).toHaveBeenNthCalledWith(14, 'hideAmountGDT', true)
|
||||
})
|
||||
|
||||
it('commits email', () => {
|
||||
logout({ commit, state })
|
||||
expect(commit).toHaveBeenNthCalledWith(12, 'email', '')
|
||||
expect(commit).toHaveBeenNthCalledWith(15, 'email', '')
|
||||
})
|
||||
|
||||
// how to get this working?
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user