mirror of
https://github.com/IT4Change/gradido.git
synced 2026-02-06 09:56:05 +00:00
Merge pull request #3443 from gradido/project_brandings_add_db_and_graphql_resolver
feat(frontend): project branding and login forwarding to circle
This commit is contained in:
commit
2353b93e57
@ -55,7 +55,8 @@
|
||||
"vue-router": "4.4.0",
|
||||
"vue3-datepicker": "^0.4.0",
|
||||
"vuex": "4.1.0",
|
||||
"vuex-persistedstate": "4.1.0"
|
||||
"vuex-persistedstate": "4.1.0",
|
||||
"yup": "^1.6.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@apollo/client": "^3.10.8",
|
||||
@ -88,6 +89,7 @@
|
||||
"unplugin-icons": "^0.19.0",
|
||||
"unplugin-vue-components": "^0.27.3",
|
||||
"vite-plugin-environment": "^1.1.3",
|
||||
"vite-plugin-graphql-loader": "^4.0.4",
|
||||
"vitest": "^2.0.5",
|
||||
"vitest-canvas-mock": "^0.3.3"
|
||||
},
|
||||
|
||||
@ -56,7 +56,7 @@ export default {
|
||||
? formatDistanceToNow(new Date(dateString), {
|
||||
includeSecond: true,
|
||||
addSuffix: true,
|
||||
locale: useDateLocale,
|
||||
locale: useDateLocale(),
|
||||
})
|
||||
: ''
|
||||
},
|
||||
|
||||
@ -101,12 +101,13 @@ describe('NavBar', () => {
|
||||
describe('Navbar Menu', () => {
|
||||
it('has correct menu items', () => {
|
||||
const navItems = wrapper.findAll('.nav-item a')
|
||||
expect(navItems).toHaveLength(7)
|
||||
expect(navItems).toHaveLength(8)
|
||||
expect(navItems[0].attributes('href')).toBe('/user')
|
||||
expect(navItems[1].attributes('href')).toBe('/creation-confirm')
|
||||
expect(navItems[2].attributes('href')).toBe('/contribution-links')
|
||||
expect(navItems[3].attributes('href')).toBe('/federation')
|
||||
expect(navItems[4].attributes('href')).toBe('/statistic')
|
||||
expect(navItems[4].attributes('href')).toBe('/projectBranding')
|
||||
expect(navItems[5].attributes('href')).toBe('/statistic')
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@ -32,6 +32,13 @@
|
||||
<BNavItem to="/federation" :active="isActive('federation')">
|
||||
{{ $t('navbar.instances') }}
|
||||
</BNavItem>
|
||||
<BNavItem
|
||||
to="/projectBranding"
|
||||
:active="isActive('projectBranding')"
|
||||
:title="$t('navbar.projectBrandingTooltip')"
|
||||
>
|
||||
{{ $t('navbar.projectBranding') }}
|
||||
</BNavItem>
|
||||
<BNavItem to="/statistic" :active="isActive('statistic')">
|
||||
{{ $t('navbar.statistic') }}
|
||||
</BNavItem>
|
||||
|
||||
85
admin/src/components/ProjectBranding/ListHumhubSpaces.vue
Normal file
85
admin/src/components/ProjectBranding/ListHumhubSpaces.vue
Normal file
@ -0,0 +1,85 @@
|
||||
<template>
|
||||
<div>
|
||||
<ul class="list-unstyled list-group mb-3">
|
||||
<li
|
||||
v-for="space in spaces"
|
||||
:key="space.id"
|
||||
:title="space.description"
|
||||
:class="[
|
||||
'list-group-item',
|
||||
'list-group-item-action',
|
||||
'd-flex',
|
||||
'justify-content-between',
|
||||
'align-items-center',
|
||||
'cursor-pointer',
|
||||
{ active: space.id === selectedSpaceId },
|
||||
]"
|
||||
@click="chooseSpace(space)"
|
||||
>
|
||||
<div>
|
||||
<input v-model="selectedSpaceId" type="radio" :value="space.id" class="me-2" />
|
||||
{{ space.name }}
|
||||
</div>
|
||||
<a :href="space.url" target="_blank" @click.stop>
|
||||
{{ $t('projectBranding.openSpaceInHumhub') }}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<b-pagination
|
||||
v-if="result && pagination.total > ITEMS_PER_PAGE"
|
||||
v-model="pagination.page"
|
||||
:total-rows="pagination.total"
|
||||
:per-page="ITEMS_PER_PAGE"
|
||||
aria-controls="list-humhub-spaces"
|
||||
@update:model-value="refetch({ page: $event })"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, computed } from 'vue'
|
||||
import { spaces as spacesQuery } from '@/graphql/projectBranding.graphql'
|
||||
import { useQuery } from '@vue/apollo-composable'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Number,
|
||||
default: null,
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits(['chooseSpace'])
|
||||
function chooseSpace(space) {
|
||||
selectedSpaceId.value = space.id
|
||||
emit('chooseSpace', space)
|
||||
}
|
||||
|
||||
const ITEMS_PER_PAGE = 20
|
||||
const page = ref(1)
|
||||
const selectedSpaceId = ref(props.modelValue)
|
||||
const { result, refetch } = useQuery(spacesQuery, { page: page.value, limit: ITEMS_PER_PAGE })
|
||||
|
||||
const spaces = computed(() => result.value?.spaces?.results || [])
|
||||
const pagination = computed(() => result.value?.spaces?.pagination || {})
|
||||
|
||||
onMounted(() => {
|
||||
if (props.modelValue) {
|
||||
if (!spaces.value.some((space) => space.id === props.modelValue)) {
|
||||
const targetPage = Math.ceil(props.modelValue / ITEMS_PER_PAGE)
|
||||
page.value = targetPage
|
||||
refetch({ page: targetPage })
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
<style scoped>
|
||||
.list-group-item-action:hover:not(.active) {
|
||||
background-color: #ececec;
|
||||
color: #0056b3;
|
||||
transition: background-color 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.list-group-item-action.active > a {
|
||||
color: white;
|
||||
}
|
||||
</style>
|
||||
171
admin/src/components/ProjectBranding/ProjectBrandingForm.vue
Normal file
171
admin/src/components/ProjectBranding/ProjectBrandingForm.vue
Normal file
@ -0,0 +1,171 @@
|
||||
<template>
|
||||
<div class="project-branding-form">
|
||||
<BForm @submit.prevent="submit">
|
||||
<ValidatedInput
|
||||
:model-value="name"
|
||||
name="name"
|
||||
:label="$t('name')"
|
||||
:rules="validationSchema.fields.name"
|
||||
class="mb-3"
|
||||
@update:model-value="updateField"
|
||||
/>
|
||||
<ValidatedInput
|
||||
:model-value="alias"
|
||||
name="alias"
|
||||
:label="$t('alias')"
|
||||
:rules="validationSchema.fields.alias"
|
||||
class="mb-3"
|
||||
@update:model-value="updateField"
|
||||
/>
|
||||
<ValidatedInput
|
||||
:model-value="description"
|
||||
name="description"
|
||||
:label="$t('description')"
|
||||
:rules="validationSchema.fields.description"
|
||||
textarea="true"
|
||||
class="mb-3"
|
||||
@update:model-value="updateField"
|
||||
/>
|
||||
<BButton
|
||||
variant="outline-secondary"
|
||||
class="mb-3"
|
||||
:title="result?.space.description"
|
||||
@click="isModalVisible = true"
|
||||
>
|
||||
{{ selectedSpaceText }}
|
||||
</BButton>
|
||||
<BFormGroup
|
||||
:label="$t('projectBranding.newUserToSpace')"
|
||||
label-for="newUserToSpace-input-field"
|
||||
class="mb-3"
|
||||
>
|
||||
<BFormCheckbox
|
||||
id="newUserToSpace-input-field"
|
||||
:model-value="newUserToSpace"
|
||||
name="newUserToSpace"
|
||||
value="true"
|
||||
unchecked-value="false"
|
||||
@update:model-value="(value) => updateField(value, 'newUserToSpace')"
|
||||
>
|
||||
{{ $t('projectBranding.newUserToSpaceTooltip') }}
|
||||
</BFormCheckbox>
|
||||
</BFormGroup>
|
||||
<ValidatedInput
|
||||
:model-value="logoUrl"
|
||||
name="logoUrl"
|
||||
:label="$t('logo')"
|
||||
:rules="validationSchema.fields.logoUrl"
|
||||
class="mb-3"
|
||||
@update:model-value="updateField"
|
||||
/>
|
||||
<BFormInvalidFeedback v-if="errorMessage" class="d-block mb-3">
|
||||
{{ errorMessage }}
|
||||
</BFormInvalidFeedback>
|
||||
<div class="d-flex gap-2">
|
||||
<BButton type="submit" variant="primary">{{ $t('save') }}</BButton>
|
||||
<BButton type="reset" variant="secondary" @click="resetForm">{{ $t('reset') }}</BButton>
|
||||
</div>
|
||||
</BForm>
|
||||
<BModal v-model="isModalVisible" title="Select Space" hide-footer>
|
||||
<ListHumhubSpaces :model-value="spaceId" @choose-space="chooseSpace" />
|
||||
</BModal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import ValidatedInput from '@/components/input/ValidatedInput'
|
||||
import ListHumhubSpaces from '@/components/ProjectBranding/ListHumhubSpaces.vue'
|
||||
import { spaceWithNameAndDescription } from '@/graphql/projectBranding.graphql'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { reactive, computed, watch, ref } from 'vue'
|
||||
import { object, string, boolean, number } from 'yup'
|
||||
import { useQuery } from '@vue/apollo-composable'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: { type: Object, required: true },
|
||||
})
|
||||
|
||||
const form = reactive({ ...props.modelValue })
|
||||
const isModalVisible = ref(false)
|
||||
const errorMessage = ref('')
|
||||
const { t } = useI18n()
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(newValue) => Object.assign(form, newValue),
|
||||
)
|
||||
const name = computed(() => form.name)
|
||||
const alias = computed(() => form.alias)
|
||||
const description = computed(() => form.description)
|
||||
const spaceId = computed(() => form.spaceId)
|
||||
const newUserToSpace = computed(() => form.newUserToSpace)
|
||||
const logoUrl = computed(() => form.logoUrl)
|
||||
|
||||
// show space
|
||||
const { result } = useQuery(spaceWithNameAndDescription, () => ({ id: spaceId.value }), {
|
||||
enabled: !!spaceId.value,
|
||||
})
|
||||
|
||||
const selectedSpaceText = computed(() => {
|
||||
if (!spaceId.value) {
|
||||
return t('projectBranding.selectSpace')
|
||||
}
|
||||
if (!result.value?.space) {
|
||||
return t('projectBranding.noAccessRightSpace', { spaceId: spaceId.value })
|
||||
}
|
||||
return t('projectBranding.chosenSpace', { space: result.value.space.name })
|
||||
})
|
||||
|
||||
const validationSchema = object({
|
||||
name: string().min(3).max(255).required(),
|
||||
alias: string()
|
||||
.matches(/^[a-z0-9-_]+$/, {
|
||||
message: 'Alias can only contain lowercase letters, numbers, hyphens, and underscores.',
|
||||
})
|
||||
.min(3)
|
||||
.max(32)
|
||||
.required(),
|
||||
description: string().nullable().optional(),
|
||||
spaceId: number().nullable().optional(),
|
||||
spaceUrl: string().url('Space URL must be a valid URL.').max(255).nullable().optional(),
|
||||
newUserToSpace: boolean().optional(),
|
||||
logoUrl: string().url('Logo URL must be a valid URL.').max(255).nullable().optional(),
|
||||
})
|
||||
|
||||
function chooseSpace(value) {
|
||||
updateField(value.id, 'spaceId')
|
||||
updateField(value.url, 'spaceUrl')
|
||||
}
|
||||
|
||||
function updateField(value, name) {
|
||||
form[name] = value
|
||||
}
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
function submit() {
|
||||
validationSchema
|
||||
.validate(form, { stripUnknown: true })
|
||||
.then((cleanedForm) => {
|
||||
emit('update:modelValue', { ...cleanedForm, id: props.modelValue.id })
|
||||
})
|
||||
.catch((err) => {
|
||||
errorMessage.value = err.message
|
||||
})
|
||||
}
|
||||
|
||||
function resetForm() {
|
||||
if (props.modelValue.id === undefined) {
|
||||
Object.assign(form, {
|
||||
name: '',
|
||||
alias: '',
|
||||
description: undefined,
|
||||
spaceId: undefined,
|
||||
spaceUrl: undefined,
|
||||
newUserToSpace: false,
|
||||
logoUrl: undefined,
|
||||
})
|
||||
return
|
||||
} else {
|
||||
Object.assign(form, props.modelValue)
|
||||
}
|
||||
errorMessage.value = ''
|
||||
}
|
||||
</script>
|
||||
117
admin/src/components/ProjectBranding/ProjectBrandingItem.vue
Normal file
117
admin/src/components/ProjectBranding/ProjectBrandingItem.vue
Normal file
@ -0,0 +1,117 @@
|
||||
<template>
|
||||
<div class="project-branding-item">
|
||||
<BRow :title="item.description" @click="toggleDetails">
|
||||
<BCol cols="3">
|
||||
{{ item.name }}
|
||||
<br />
|
||||
{{ frontendLoginUrl }}
|
||||
<BButton
|
||||
v-if="frontendLoginUrl"
|
||||
v-b-tooltip.hover
|
||||
variant="secondary"
|
||||
:title="$t('copy-to-clipboard')"
|
||||
@click.stop="copyToClipboard(frontendLoginUrl)"
|
||||
>
|
||||
<i class="fas fa-copy"></i>
|
||||
</BButton>
|
||||
</BCol>
|
||||
<BCol cols="2">{{ item.alias }}</BCol>
|
||||
<BCol cols="2">
|
||||
<span v-if="item.newUserToSpace" class="text-success">
|
||||
<i class="fas fa-check"></i>
|
||||
</span>
|
||||
<span v-else class="text-danger">
|
||||
<i class="fas fa-times"></i>
|
||||
</span>
|
||||
</BCol>
|
||||
<BCol cols="3" class="me-2">
|
||||
<img class="img-fluid" :src="item.logoUrl" :alt="item.logoUrl" />
|
||||
</BCol>
|
||||
<BCol v-if="store.state.moderator.roles.includes('ADMIN')" cols="1">
|
||||
<BButton v-b-tooltip.hover variant="danger" :title="$t('delete')" @click.stop="deleteItem">
|
||||
<i class="fas fa-trash-alt"></i>
|
||||
</BButton>
|
||||
</BCol>
|
||||
</BRow>
|
||||
<BRow v-if="details || item.id === undefined" class="details">
|
||||
<BCol colspan="5">
|
||||
<BCard>
|
||||
<ProjectBrandingForm :model-value="item" @update:model-value="update" />
|
||||
</BCard>
|
||||
</BCol>
|
||||
</BRow>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, ref, toRefs } from 'vue'
|
||||
import ProjectBrandingForm from './ProjectBrandingForm.vue'
|
||||
import { deleteProjectBranding, upsertProjectBranding } from '@/graphql/projectBranding.graphql'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useStore } from 'vuex'
|
||||
import { useMutation } from '@vue/apollo-composable'
|
||||
import CONFIG from '@/config'
|
||||
import { useAppToast } from '@/composables/useToast'
|
||||
|
||||
const { t } = useI18n()
|
||||
const store = useStore()
|
||||
const { toastSuccess, toastError } = useAppToast()
|
||||
|
||||
const props = defineProps({
|
||||
item: { type: Object, required: true },
|
||||
})
|
||||
const { item } = toRefs(props)
|
||||
const details = ref(false)
|
||||
|
||||
const emit = defineEmits(['update:item', 'deleted:item'])
|
||||
const frontendLoginUrl = computed(() => {
|
||||
if (item.value.alias && item.value.alias.length > 0) {
|
||||
return `${CONFIG.WALLET_LOGIN_URL}?project=${item.value.alias}`
|
||||
}
|
||||
return undefined
|
||||
})
|
||||
|
||||
async function copyToClipboard(text) {
|
||||
try {
|
||||
await navigator.clipboard.writeText(text)
|
||||
toastSuccess(t('copied-to-clipboard'))
|
||||
} catch (err) {
|
||||
toastError(err.message)
|
||||
}
|
||||
}
|
||||
|
||||
function toggleDetails() {
|
||||
if (store.state.moderator.roles.includes('ADMIN')) {
|
||||
details.value = !details.value
|
||||
}
|
||||
}
|
||||
|
||||
function update(form) {
|
||||
const { mutate } = useMutation(upsertProjectBranding)
|
||||
|
||||
mutate({
|
||||
input: { ...form },
|
||||
})
|
||||
.then(({ data }) => {
|
||||
emit('update:item', data.upsertProjectBranding)
|
||||
if (form.id) {
|
||||
toastSuccess(t('projectBranding.updated'))
|
||||
} else {
|
||||
toastSuccess(t('projectBranding.created'))
|
||||
}
|
||||
details.value = false
|
||||
})
|
||||
.catch((error) => {
|
||||
toastError(t('projectBranding.error', { message: error.message }))
|
||||
})
|
||||
}
|
||||
function deleteItem() {
|
||||
const { mutate } = useMutation(deleteProjectBranding)
|
||||
|
||||
mutate({
|
||||
id: item.value.id,
|
||||
}).then(() => {
|
||||
emit('deleted:item', item.value.id)
|
||||
})
|
||||
}
|
||||
</script>
|
||||
45
admin/src/components/input/LabeledInput.vue
Normal file
45
admin/src/components/input/LabeledInput.vue
Normal file
@ -0,0 +1,45 @@
|
||||
<template>
|
||||
<div :class="wrapperClassName">
|
||||
<BFormGroup :label="label" :label-for="labelFor">
|
||||
<BFormTextarea
|
||||
v-if="textarea"
|
||||
v-bind="{ ...$attrs, id: labelFor, name }"
|
||||
v-model="model"
|
||||
trim
|
||||
:rows="4"
|
||||
:max-rows="4"
|
||||
no-resize
|
||||
/>
|
||||
<BFormInput v-else v-bind="{ ...$attrs, id: labelFor, name }" v-model="model" />
|
||||
<slot></slot>
|
||||
</BFormGroup>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, defineOptions, defineModel } from 'vue'
|
||||
defineOptions({
|
||||
inheritAttrs: false,
|
||||
})
|
||||
|
||||
const props = defineProps({
|
||||
label: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
name: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
textarea: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
})
|
||||
|
||||
const model = defineModel()
|
||||
|
||||
const wrapperClassName = computed(() => (props.name ? `input-${props.name}` : 'input'))
|
||||
const labelFor = computed(() => `${props.name}-input-field`)
|
||||
</script>
|
||||
86
admin/src/components/input/ValidatedInput.vue
Normal file
86
admin/src/components/input/ValidatedInput.vue
Normal file
@ -0,0 +1,86 @@
|
||||
<template>
|
||||
<LabeledInput
|
||||
v-bind="$attrs"
|
||||
:min="minValue"
|
||||
:max="maxValue"
|
||||
:model-value="model"
|
||||
:reset-value="resetValue"
|
||||
:locale="$i18n.locale"
|
||||
:required="!isOptional"
|
||||
:label="label"
|
||||
:name="name"
|
||||
:state="valid"
|
||||
@update:model-value="updateValue"
|
||||
>
|
||||
<BFormInvalidFeedback v-if="errorMessage">
|
||||
{{ errorMessage }}
|
||||
</BFormInvalidFeedback>
|
||||
</LabeledInput>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import LabeledInput from './LabeledInput'
|
||||
|
||||
const props = defineProps({
|
||||
label: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
modelValue: [String, Number, Date],
|
||||
name: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
rules: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
|
||||
const model = ref(props.modelValue)
|
||||
|
||||
const valid = computed(() => {
|
||||
if (
|
||||
(props.modelValue === undefined || props.modelValue === '' || props.modelValue === null) &&
|
||||
isOptional.value
|
||||
) {
|
||||
return null
|
||||
}
|
||||
return props.rules.isValidSync(props.modelValue)
|
||||
})
|
||||
const errorMessage = computed(() => {
|
||||
if (props.modelValue === undefined || props.modelValue === '' || props.modelValue === null) {
|
||||
return undefined
|
||||
}
|
||||
try {
|
||||
props.rules.validateSync(props.modelValue)
|
||||
return undefined
|
||||
} catch (e) {
|
||||
return e.message
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
const updateValue = (newValue) => {
|
||||
emit('update:modelValue', newValue, props.name, valid.value)
|
||||
}
|
||||
|
||||
// update model and if value changed and model isn't null, check validation,
|
||||
// for loading Input with existing value and show correct validation state
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
() => {
|
||||
model.value = props.modelValue
|
||||
},
|
||||
)
|
||||
|
||||
// extract additional parameter like min and max from schema
|
||||
const schemaDescription = computed(() => props.rules.describe())
|
||||
const getTestParameter = (name) =>
|
||||
schemaDescription.value?.tests?.find((t) => t.name === name)?.params[name]
|
||||
const minValue = computed(() => getTestParameter('min'))
|
||||
const maxValue = computed(() => getTestParameter('max'))
|
||||
const resetValue = computed(() => schemaDescription.value.default)
|
||||
const isOptional = computed(() => schemaDescription.value.optional)
|
||||
</script>
|
||||
@ -6,7 +6,7 @@ const pkg = require('../../package')
|
||||
|
||||
const version = {
|
||||
ADMIN_MODULE_PROTOCOL: process.env.ADMIN_MODULE_PROTOCOL ?? 'http',
|
||||
ADMIN_MODULE_HOST: process.env.ADMIN_MODULE_HOST ?? 'localhost',
|
||||
ADMIN_MODULE_HOST: process.env.ADMIN_MODULE_HOST ?? '0.0.0.0',
|
||||
ADMIN_MODULE_PORT: process.env.ADMIN_MODULE_PORT ?? '8080',
|
||||
APP_VERSION: pkg.version,
|
||||
BUILD_COMMIT: process.env.BUILD_COMMIT ?? undefined,
|
||||
|
||||
23
admin/src/graphql/fragments.graphql
Normal file
23
admin/src/graphql/fragments.graphql
Normal file
@ -0,0 +1,23 @@
|
||||
|
||||
fragment SpaceFields on Space {
|
||||
id
|
||||
name
|
||||
description
|
||||
url
|
||||
}
|
||||
|
||||
fragment PaginationFields on Pagination {
|
||||
total
|
||||
page
|
||||
pages
|
||||
}
|
||||
|
||||
fragment ProjectBrandingCommonFields on ProjectBranding {
|
||||
id
|
||||
name
|
||||
alias
|
||||
description
|
||||
spaceId
|
||||
newUserToSpace
|
||||
logoUrl
|
||||
}
|
||||
35
admin/src/graphql/projectBranding.graphql
Normal file
35
admin/src/graphql/projectBranding.graphql
Normal file
@ -0,0 +1,35 @@
|
||||
#import './fragments.graphql'
|
||||
|
||||
query projectBrandings {
|
||||
projectBrandings {
|
||||
...ProjectBrandingCommonFields
|
||||
}
|
||||
}
|
||||
|
||||
mutation upsertProjectBranding($input: ProjectBrandingInput!) {
|
||||
upsertProjectBranding(input: $input) {
|
||||
...ProjectBrandingCommonFields
|
||||
}
|
||||
}
|
||||
|
||||
mutation deleteProjectBranding($id: ID!) {
|
||||
deleteProjectBranding(id: $id)
|
||||
}
|
||||
|
||||
query spaces($page: Int!, $limit: Int!) {
|
||||
spaces(page: $page, limit: $limit) {
|
||||
pagination {
|
||||
...PaginationFields
|
||||
}
|
||||
results {
|
||||
...SpaceFields
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
query spaceWithNameAndDescription($id: ID!) {
|
||||
space(id: $id) {
|
||||
name
|
||||
description
|
||||
}
|
||||
}
|
||||
@ -1,5 +1,7 @@
|
||||
{
|
||||
"GDD": "GDD",
|
||||
"actions": "Aktionen",
|
||||
"alias": "Alias",
|
||||
"all_emails": "Alle Nutzer",
|
||||
"back": "zurück",
|
||||
"change_user_role": "Nutzerrolle ändern",
|
||||
@ -42,6 +44,8 @@
|
||||
"denied": "Abgelehnt",
|
||||
"open": "Offen"
|
||||
},
|
||||
"copied-to-clipboard": "In die Zwischenablage kopiert",
|
||||
"copy-to-clipboard": "In die Zwischenablage kopieren",
|
||||
"created": "Geschöpft",
|
||||
"createdAt": "Angelegt",
|
||||
"creation": "Schöpfung",
|
||||
@ -68,6 +72,7 @@
|
||||
"deleted": "gelöscht",
|
||||
"deleted_user": "Alle gelöschten Nutzer",
|
||||
"deny": "Ablehnen",
|
||||
"description": "Beschreibung",
|
||||
"e_mail": "E-Mail",
|
||||
"edit": "bearbeiten",
|
||||
"enabled": "aktiviert",
|
||||
@ -127,6 +132,8 @@
|
||||
"lastname": "Nachname",
|
||||
"latitude": "Breitengrad:",
|
||||
"latitude-longitude-smart": "Breitengrad, Längengrad",
|
||||
"link": "Link",
|
||||
"logo": "Logo",
|
||||
"longitude": "Längengrad:",
|
||||
"math": {
|
||||
"equals": "=",
|
||||
@ -155,6 +162,8 @@
|
||||
"instances": "Instanzen",
|
||||
"logout": "Abmelden",
|
||||
"my-account": "Mein Konto",
|
||||
"projectBranding": "Projekt Branding",
|
||||
"projectBrandingTooltip": "Nutze ein eigenes Logo im Gradido Login und füge neue Benutzer einem Humhub-Space hinzu",
|
||||
"statistic": "Statistik",
|
||||
"user_search": "Nutzersuche"
|
||||
},
|
||||
@ -199,8 +208,23 @@
|
||||
"yes": "Ja, Nutzer wiederherstellen"
|
||||
}
|
||||
},
|
||||
"projectBranding": {
|
||||
"addTooltip": "Neuen Projekt Branding Eintrag hinzufügen",
|
||||
"chosenSpace": "Gewählter Space: {space}",
|
||||
"created": "Neuer Projekt Branding Eintrag wurde erstellt.",
|
||||
"error": "Fehler beim Erstellen des Projekt Branding Eintrags: {message}",
|
||||
"noAccessRightSpace": "Gewählter Space: {spaceId} (Keine Zugriffsrechte)",
|
||||
"openSpaceInHumhub": "In Humhub öffnen",
|
||||
"spaceId": "Humhub Space ID",
|
||||
"selectSpace": "Humhub Space auswählen",
|
||||
"title": "Projekt Brandings",
|
||||
"updated": "Projekt Branding Eintrag wurde aktualisiert.",
|
||||
"newUserToSpace": "Benutzer hinzufügen?",
|
||||
"newUserToSpaceTooltip": "Neue Benutzer automatisch zum Space hinzufügen, falls Space vorhanden"
|
||||
},
|
||||
"redeemed": "eingelöst",
|
||||
"removeNotSelf": "Als Admin/Moderator kannst du dich nicht selber löschen.",
|
||||
"reset": "Zurücksetzen",
|
||||
"save": "Speichern",
|
||||
"statistic": {
|
||||
"activeUsers": "Aktive Mitglieder",
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
{
|
||||
"GDD": "GDD",
|
||||
"actions": "Actions",
|
||||
"alias": "Alias",
|
||||
"all_emails": "All users",
|
||||
"back": "back",
|
||||
"change_user_role": "Change user role",
|
||||
@ -42,6 +44,8 @@
|
||||
"denied": "Rejected",
|
||||
"open": "Open"
|
||||
},
|
||||
"copied-to-clipboard": "Copied to clipboard",
|
||||
"copy-to-clipboard": "Copy to clipboard",
|
||||
"created": "Created for",
|
||||
"createdAt": "Created at",
|
||||
"creation": "Creation",
|
||||
@ -68,6 +72,7 @@
|
||||
"deleted": "deleted",
|
||||
"deleted_user": "All deleted user",
|
||||
"deny": "Reject",
|
||||
"description": "Description",
|
||||
"e_mail": "E-mail",
|
||||
"edit": "edit",
|
||||
"enabled": "enabled",
|
||||
@ -127,6 +132,8 @@
|
||||
"lastname": "Lastname",
|
||||
"latitude": "Latitude:",
|
||||
"latitude-longitude-smart": "Latitude, Longitude",
|
||||
"link": "Link",
|
||||
"logo": "Logo",
|
||||
"longitude": "Longitude:",
|
||||
"math": {
|
||||
"equals": "=",
|
||||
@ -155,6 +162,8 @@
|
||||
"instances": "Instances",
|
||||
"logout": "Logout",
|
||||
"my-account": "My Account",
|
||||
"projectBranding": "Project Branding",
|
||||
"projectBrandingTooltip": "Use your own logo in the Gradido login and add new users to a Humhub space",
|
||||
"statistic": "Statistic",
|
||||
"user_search": "User search"
|
||||
},
|
||||
@ -199,9 +208,24 @@
|
||||
"yes": "Yes,undelete user"
|
||||
}
|
||||
},
|
||||
"projectBranding": {
|
||||
"addTooltip": "Add new project branding entry",
|
||||
"chosenSpace": "Choosen Humhub Space: {space}",
|
||||
"created": "New project branding entry has been created.",
|
||||
"error": "Error when creating the project branding entry: {message}",
|
||||
"noAccessRightSpace": "Selected space: {spaceId} (No access rights)",
|
||||
"openSpaceInHumhub": "Open in Humhub",
|
||||
"spaceId": "Humhub Space ID",
|
||||
"selectSpace": "Select Humhub Space",
|
||||
"title": "Project Branding",
|
||||
"updated": "Project branding entry has been updated.",
|
||||
"newUserToSpace": "Add user?",
|
||||
"newUserToSpaceTooltip": "The hours should contain a maximum of two decimal places"
|
||||
},
|
||||
"redeemed": "redeemed",
|
||||
"removeNotSelf": "As an admin/moderator, you cannot delete yourself.",
|
||||
"save": "Speichern",
|
||||
"reset": "Reset",
|
||||
"save": "Save",
|
||||
"statistic": {
|
||||
"activeUsers": "Active members",
|
||||
"count": "Count",
|
||||
|
||||
109
admin/src/pages/ProjectBranding.vue
Normal file
109
admin/src/pages/ProjectBranding.vue
Normal file
@ -0,0 +1,109 @@
|
||||
<template>
|
||||
<div class="project-branding">
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<span class="h2">{{ $t('projectBranding.title') }}</span>
|
||||
<div>
|
||||
<BButton
|
||||
v-if="store.state.moderator.roles.includes('ADMIN')"
|
||||
variant="primary"
|
||||
data-test="project-branding-add-btn"
|
||||
font-scale="2"
|
||||
class="me-3"
|
||||
:title="$t('projectBranding.addTooltip')"
|
||||
:disabled="isAddButtonDisabled"
|
||||
@click="createEntry"
|
||||
>
|
||||
<IBiPlus />
|
||||
</BButton>
|
||||
<BButton
|
||||
:animation="animation"
|
||||
data-test="project-branding-refresh-btn"
|
||||
font-scale="2"
|
||||
@click="refetch"
|
||||
>
|
||||
<IBiArrowClockwise />
|
||||
</BButton>
|
||||
</div>
|
||||
</div>
|
||||
<BListGroup>
|
||||
<BRow>
|
||||
<BCol cols="3" class="ms-1">{{ $t('name') }} + {{ $t('link') }}</BCol>
|
||||
<BCol cols="2">{{ $t('alias') }}</BCol>
|
||||
<BCol cols="2" :title="$t('projectBranding.newUserToSpaceTooltip')">
|
||||
{{ $t('projectBranding.newUserToSpace') }}
|
||||
</BCol>
|
||||
<BCol cols="3">{{ $t('logo') }}</BCol>
|
||||
<BCol v-if="store.state.moderator.roles.includes('ADMIN')" cols="1">
|
||||
{{ $t('actions') }}
|
||||
</BCol>
|
||||
</BRow>
|
||||
<BListGroupItem v-for="item in projectBrandings" :key="item.id">
|
||||
<project-branding-item
|
||||
:item="item"
|
||||
@update:item="handleUpdateItem"
|
||||
@deleted:item="handleDeleteItem"
|
||||
/>
|
||||
</BListGroupItem>
|
||||
</BListGroup>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, watch, ref } from 'vue'
|
||||
import { useQuery } from '@vue/apollo-composable'
|
||||
import { useStore } from 'vuex'
|
||||
import { useAppToast } from '@/composables/useToast'
|
||||
import { projectBrandings as projectBrandingsQuery } from '@/graphql/projectBranding.graphql'
|
||||
|
||||
const { toastError } = useAppToast()
|
||||
const store = useStore()
|
||||
|
||||
const { result, loading, refetch, error } = useQuery(projectBrandingsQuery, null, {
|
||||
fetchPolicy: 'network-only',
|
||||
})
|
||||
|
||||
const projectBrandings = ref([])
|
||||
|
||||
const isAddButtonDisabled = computed(() => {
|
||||
return projectBrandings.value.some((item) => item.id === undefined)
|
||||
})
|
||||
|
||||
watch(
|
||||
result,
|
||||
() => {
|
||||
projectBrandings.value = result.value?.projectBrandings || []
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
function createEntry() {
|
||||
projectBrandings.value.push({
|
||||
id: undefined,
|
||||
name: '',
|
||||
alias: '',
|
||||
description: undefined,
|
||||
spaceId: undefined,
|
||||
spaceUrl: undefined,
|
||||
newUserToSpace: false,
|
||||
logoUrl: undefined,
|
||||
})
|
||||
}
|
||||
|
||||
function handleUpdateItem(updatedItem) {
|
||||
const index = projectBrandings.value.findIndex(
|
||||
(item) => item.id === updatedItem.id || item.id === undefined,
|
||||
)
|
||||
projectBrandings.value.splice(index, 1, updatedItem)
|
||||
}
|
||||
|
||||
function handleDeleteItem(id) {
|
||||
const index = projectBrandings.value.findIndex((item) => item.id === id)
|
||||
projectBrandings.value.splice(index, 1)
|
||||
}
|
||||
|
||||
watch(error, () => {
|
||||
if (error.value) toastError(error.value.message)
|
||||
})
|
||||
|
||||
const animation = computed(() => (loading.value ? 'spin' : ''))
|
||||
</script>
|
||||
@ -36,6 +36,11 @@ const routes = [
|
||||
name: 'federation',
|
||||
component: () => import('@/pages/FederationVisualize.vue'),
|
||||
},
|
||||
{
|
||||
path: '/projectBranding',
|
||||
name: 'projectBranding',
|
||||
component: () => import('@/pages/ProjectBranding.vue'),
|
||||
},
|
||||
{
|
||||
path: '/:catchAll(.*)',
|
||||
name: 'NotFound',
|
||||
|
||||
@ -17,7 +17,8 @@ const CONFIG = require('./src/config')
|
||||
|
||||
const path = require('path')
|
||||
|
||||
export default defineConfig(({ command }) => {
|
||||
export default defineConfig(async ({ command }) => {
|
||||
const { vitePluginGraphqlLoader } = await import('vite-plugin-graphql-loader')
|
||||
if (command === 'serve') {
|
||||
CONFIG.ADMIN_HOSTING = 'nodejs'
|
||||
} else {
|
||||
@ -79,6 +80,7 @@ export default defineConfig(({ command }) => {
|
||||
DEBUG_DISABLE_AUTH: CONFIG.DEBUG_DISABLE_AUTH ?? null, // null,
|
||||
// CONFIG_VERSION: CONFIG.CONFIG_VERSION, // null,
|
||||
}),
|
||||
vitePluginGraphqlLoader(),
|
||||
commonjs(),
|
||||
],
|
||||
build: {
|
||||
|
||||
@ -10,34 +10,37 @@ import path from 'path'
|
||||
// },
|
||||
// })
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [Vue()],
|
||||
test: {
|
||||
globals: true,
|
||||
environment: 'jsdom',
|
||||
setupFiles: ['./test/vitest.setup.js'],
|
||||
coverage: {
|
||||
provider: 'v8',
|
||||
reporter: ['text', 'json', 'html'],
|
||||
exclude: ['node_modules/**', 'src/assets/**', '**/*.{spec,test}.js'],
|
||||
lines: 95,
|
||||
export default defineConfig(async () => {
|
||||
const { vitePluginGraphqlLoader } = await import('vite-plugin-graphql-loader')
|
||||
return {
|
||||
plugins: [Vue(), vitePluginGraphqlLoader()],
|
||||
test: {
|
||||
globals: true,
|
||||
environment: 'jsdom',
|
||||
setupFiles: ['./test/vitest.setup.js'],
|
||||
coverage: {
|
||||
provider: 'v8',
|
||||
reporter: ['text', 'json', 'html'],
|
||||
exclude: ['node_modules/**', 'src/assets/**', '**/*.{spec,test}.js'],
|
||||
lines: 95,
|
||||
},
|
||||
include: ['**/?(*.)+(spec|test).js?(x)'],
|
||||
moduleNameMapper: {
|
||||
'^@/(.*)$': path.resolve(__dirname, './src/$1'),
|
||||
'\\.(css|less)$': 'identity-obj-proxy',
|
||||
},
|
||||
transformMode: {
|
||||
web: [/\.[jt]sx$/],
|
||||
},
|
||||
deps: {
|
||||
inline: [/vee-validate/, 'vitest-canvas-mock'],
|
||||
},
|
||||
},
|
||||
include: ['**/?(*.)+(spec|test).js?(x)'],
|
||||
moduleNameMapper: {
|
||||
'^@/(.*)$': path.resolve(__dirname, './src/$1'),
|
||||
'\\.(css|less)$': 'identity-obj-proxy',
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': path.resolve(__dirname, './src'),
|
||||
},
|
||||
extensions: ['.mjs', '.js', '.ts', '.jsx', '.tsx', '.json', '.vue'],
|
||||
},
|
||||
transformMode: {
|
||||
web: [/\.[jt]sx$/],
|
||||
},
|
||||
deps: {
|
||||
inline: [/vee-validate/, 'vitest-canvas-mock'],
|
||||
},
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': path.resolve(__dirname, './src'),
|
||||
},
|
||||
extensions: ['.mjs', '.js', '.ts', '.jsx', '.tsx', '.json', '.vue'],
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
@ -4241,7 +4241,7 @@ graphql-tag@^2.12.6, graphql-tag@^2.4.2:
|
||||
dependencies:
|
||||
tslib "^2.1.0"
|
||||
|
||||
graphql@^16.9.0:
|
||||
graphql@^16.8.1, graphql@^16.9.0:
|
||||
version "16.10.0"
|
||||
resolved "https://registry.yarnpkg.com/graphql/-/graphql-16.10.0.tgz#24c01ae0af6b11ea87bf55694429198aaa8e220c"
|
||||
integrity sha512-AjqGKbDGUFRKIRCP9tCKiIGHyriz2oHEbPIbEtcSLSs4YjReZOIPQQWek4+6hjw62H9QShXHyaGivGiYVLeYFQ==
|
||||
@ -5014,7 +5014,7 @@ lru-cache@^5.1.1:
|
||||
dependencies:
|
||||
yallist "^3.0.2"
|
||||
|
||||
magic-string@^0.30.11, magic-string@^0.30.12, magic-string@^0.30.14:
|
||||
magic-string@^0.30.10, magic-string@^0.30.11, magic-string@^0.30.12, magic-string@^0.30.14:
|
||||
version "0.30.17"
|
||||
resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.17.tgz#450a449673d2460e5bbcfba9a61916a1714c7453"
|
||||
integrity sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==
|
||||
@ -5657,6 +5657,11 @@ prop-types@^15.7.2:
|
||||
object-assign "^4.1.1"
|
||||
react-is "^16.13.1"
|
||||
|
||||
property-expr@^2.0.5:
|
||||
version "2.0.6"
|
||||
resolved "https://registry.yarnpkg.com/property-expr/-/property-expr-2.0.6.tgz#f77bc00d5928a6c748414ad12882e83f24aec1e8"
|
||||
integrity sha512-SVtmxhRE/CGkn3eZY1T6pC8Nln6Fr/lu1mKSgRud0eC73whjGfoAogbn78LkD8aFL0zz3bAFerKSnOl7NlErBA==
|
||||
|
||||
proto-list@~1.2.1:
|
||||
version "1.2.4"
|
||||
resolved "https://registry.yarnpkg.com/proto-list/-/proto-list-1.2.4.tgz#212d5bfe1318306a420f6402b8e26ff39647a849"
|
||||
@ -6484,6 +6489,11 @@ throttle-debounce@^5.0.0:
|
||||
resolved "https://registry.yarnpkg.com/throttle-debounce/-/throttle-debounce-5.0.2.tgz#ec5549d84e053f043c9fd0f2a6dd892ff84456b1"
|
||||
integrity sha512-B71/4oyj61iNH0KeCamLuE2rmKuTO5byTOSVwECM5FA7TiAiAW+UqTKZ9ERueC4qvgSttUhdmq1mXC3kJqGX7A==
|
||||
|
||||
tiny-case@^1.0.3:
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/tiny-case/-/tiny-case-1.0.3.tgz#d980d66bc72b5d5a9ca86fb7c9ffdb9c898ddd03"
|
||||
integrity sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q==
|
||||
|
||||
tinybench@^2.9.0:
|
||||
version "2.9.0"
|
||||
resolved "https://registry.yarnpkg.com/tinybench/-/tinybench-2.9.0.tgz#103c9f8ba6d7237a47ab6dd1dcff77251863426b"
|
||||
@ -6543,6 +6553,11 @@ toidentifier@1.0.1:
|
||||
resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35"
|
||||
integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==
|
||||
|
||||
toposort@^2.0.2:
|
||||
version "2.0.2"
|
||||
resolved "https://registry.yarnpkg.com/toposort/-/toposort-2.0.2.tgz#ae21768175d1559d48bef35420b2f4962f09c330"
|
||||
integrity sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg==
|
||||
|
||||
tough-cookie@^5.0.0:
|
||||
version "5.0.0"
|
||||
resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-5.0.0.tgz#6b6518e2b5c070cf742d872ee0f4f92d69eac1af"
|
||||
@ -6608,6 +6623,11 @@ type-fest@^0.20.2:
|
||||
resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4"
|
||||
integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==
|
||||
|
||||
type-fest@^2.19.0:
|
||||
version "2.19.0"
|
||||
resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-2.19.0.tgz#88068015bb33036a598b952e55e9311a60fd3a9b"
|
||||
integrity sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==
|
||||
|
||||
type-is@~1.6.18:
|
||||
version "1.6.18"
|
||||
resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131"
|
||||
@ -6823,6 +6843,15 @@ vite-plugin-environment@^1.1.3:
|
||||
resolved "https://registry.yarnpkg.com/vite-plugin-environment/-/vite-plugin-environment-1.1.3.tgz#d01a04abb2f69730a4866c9c9db51d3dab74645b"
|
||||
integrity sha512-9LBhB0lx+2lXVBEWxFZC+WO7PKEyE/ykJ7EPWCq95NEcCpblxamTbs5Dm3DLBGzwODpJMEnzQywJU8fw6XGGGA==
|
||||
|
||||
vite-plugin-graphql-loader@^4.0.4:
|
||||
version "4.0.4"
|
||||
resolved "https://registry.yarnpkg.com/vite-plugin-graphql-loader/-/vite-plugin-graphql-loader-4.0.4.tgz#cf6c599b3e5fa32bf2b768983da68f7beccc8486"
|
||||
integrity sha512-lYnpQ2luV2fcuXmOJADljuktfMbDW00Y+6QS+Ek8Jz1Vdzlj/51LSGJwZqyjJ24a5YQ+o29Hr6el/5+nlZetvg==
|
||||
dependencies:
|
||||
graphql "^16.8.1"
|
||||
graphql-tag "^2.12.6"
|
||||
magic-string "^0.30.10"
|
||||
|
||||
vite@3.2.10:
|
||||
version "3.2.10"
|
||||
resolved "https://registry.yarnpkg.com/vite/-/vite-3.2.10.tgz#7ac79fead82cfb6b5bf65613cd82fba6dcc81340"
|
||||
@ -7145,6 +7174,16 @@ yocto-queue@^0.1.0:
|
||||
resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b"
|
||||
integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==
|
||||
|
||||
yup@^1.6.1:
|
||||
version "1.6.1"
|
||||
resolved "https://registry.yarnpkg.com/yup/-/yup-1.6.1.tgz#8defcff9daaf9feac178029c0e13b616563ada4b"
|
||||
integrity sha512-JED8pB50qbA4FOkDol0bYF/p60qSEDQqBD0/qeIrUCG1KbPBIQ776fCUNb9ldbPcSTxA69g/47XTo4TqWiuXOA==
|
||||
dependencies:
|
||||
property-expr "^2.0.5"
|
||||
tiny-case "^1.0.3"
|
||||
toposort "^2.0.2"
|
||||
type-fest "^2.19.0"
|
||||
|
||||
zen-observable-ts@^0.8.21:
|
||||
version "0.8.21"
|
||||
resolved "https://registry.yarnpkg.com/zen-observable-ts/-/zen-observable-ts-0.8.21.tgz#85d0031fbbde1eba3cd07d3ba90da241215f421d"
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { ProjectBranding } from '@entity/ProjectBranding'
|
||||
import { SignJWT } from 'jose'
|
||||
import { IRequestOptions, IRestResponse, RestClient } from 'typed-rest-client'
|
||||
|
||||
@ -8,6 +9,8 @@ import { backendLogger as logger } from '@/server/logger'
|
||||
import { PostUserLoggingView } from './logging/PostUserLogging.view'
|
||||
import { GetUser } from './model/GetUser'
|
||||
import { PostUser } from './model/PostUser'
|
||||
import { Space } from './model/Space'
|
||||
import { SpacesResponse } from './model/SpacesResponse'
|
||||
import { UsersResponse } from './model/UsersResponse'
|
||||
|
||||
/**
|
||||
@ -61,16 +64,28 @@ export class HumHubClient {
|
||||
return token
|
||||
}
|
||||
|
||||
public async createAutoLoginUrl(username: string) {
|
||||
public async createAutoLoginUrl(username: string, project?: string | null) {
|
||||
const secret = new TextEncoder().encode(CONFIG.HUMHUB_JWT_KEY)
|
||||
logger.info(`user ${username} as username for humhub auto-login`)
|
||||
const token = await new SignJWT({ username })
|
||||
let redirectLink: string | undefined
|
||||
if (project) {
|
||||
const projectBranding = await ProjectBranding.findOne({
|
||||
where: { alias: project },
|
||||
select: { spaceUrl: true },
|
||||
})
|
||||
if (projectBranding?.spaceUrl) {
|
||||
redirectLink = projectBranding.spaceUrl
|
||||
}
|
||||
}
|
||||
const token = await new SignJWT({ username, redirectLink })
|
||||
.setProtectedHeader({ alg: 'HS256' })
|
||||
.setIssuedAt()
|
||||
.setExpirationTime(CONFIG.JWT_EXPIRES_IN)
|
||||
.sign(secret)
|
||||
|
||||
return `${CONFIG.HUMHUB_API_URL}user/auth/external?authclient=jwt&jwt=${token}`
|
||||
return `${CONFIG.HUMHUB_API_URL}${
|
||||
CONFIG.HUMHUB_API_URL.endsWith('/') ? '' : '/'
|
||||
}user/auth/external?authclient=jwt&jwt=${token}`
|
||||
}
|
||||
|
||||
/**
|
||||
@ -186,6 +201,40 @@ export class HumHubClient {
|
||||
throw new LogError('error deleting user', { userId: humhubUserId, response })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// new RestClient('gradido', 'api/v1/')
|
||||
// get spaces from humhub
|
||||
// https://marketplace.humhub.com/module/rest/docs/html/space.html#tag/Space/paths/~1space/get
|
||||
public async spaces(page = 0, limit = 20): Promise<SpacesResponse | null> {
|
||||
const options = await this.createRequestOptions({ page, limit })
|
||||
const response = await this.restClient.get<SpacesResponse>('/api/v1/space', options)
|
||||
if (response.statusCode !== 200) {
|
||||
throw new LogError('error requesting spaces from humhub', response)
|
||||
}
|
||||
return response.result
|
||||
}
|
||||
|
||||
// get space by id from humhub instance
|
||||
// https://marketplace.humhub.com/module/rest/docs/html/space.html#tag/Space/paths/~1space~1{id}/get
|
||||
public async space(spaceId: number): Promise<Space | null> {
|
||||
const options = await this.createRequestOptions()
|
||||
const response = await this.restClient.get<Space>(`/api/v1/space/${spaceId}`, options)
|
||||
if (response.statusCode !== 200) {
|
||||
throw new LogError('error requesting space from humhub', response)
|
||||
}
|
||||
return response.result
|
||||
}
|
||||
|
||||
// add user to space
|
||||
// https://marketplace.humhub.com/module/rest/docs/html/space.html#tag/Membership/paths/~1space~1%7Bid%7D~1membership~1%7BuserId%7D/post
|
||||
public async addUserToSpace(userId: number, spaceId: number): Promise<void> {
|
||||
const options = await this.createRequestOptions()
|
||||
const response = await this.restClient.create(
|
||||
`/api/v1/space/${spaceId}/membership/${userId}`,
|
||||
{ userId },
|
||||
options,
|
||||
)
|
||||
if (response.statusCode !== 200) {
|
||||
throw new LogError('error adding user to space', response)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
7
backend/src/apis/humhub/model/Space.ts
Normal file
7
backend/src/apis/humhub/model/Space.ts
Normal file
@ -0,0 +1,7 @@
|
||||
export class Space {
|
||||
id: number
|
||||
guid: string
|
||||
name: string
|
||||
description: string
|
||||
url: string
|
||||
}
|
||||
8
backend/src/apis/humhub/model/SpacesResponse.ts
Normal file
8
backend/src/apis/humhub/model/SpacesResponse.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import { Space } from './Space'
|
||||
|
||||
export interface SpacesResponse {
|
||||
total: number
|
||||
page: number
|
||||
pages: number
|
||||
results: Space[]
|
||||
}
|
||||
@ -8,4 +8,5 @@ export const ADMIN_RIGHTS = [
|
||||
RIGHTS.COMMUNITY_BY_UUID,
|
||||
RIGHTS.COMMUNITY_BY_IDENTIFIER,
|
||||
RIGHTS.HOME_COMMUNITY,
|
||||
RIGHTS.PROJECT_BRANDING_MUTATE,
|
||||
]
|
||||
|
||||
@ -9,4 +9,5 @@ export const INALIENABLE_RIGHTS = [
|
||||
RIGHTS.QUERY_TRANSACTION_LINK,
|
||||
RIGHTS.QUERY_OPT_IN,
|
||||
RIGHTS.CHECK_USERNAME,
|
||||
RIGHTS.PROJECT_BRANDING_BANNER,
|
||||
]
|
||||
|
||||
@ -8,6 +8,7 @@ export enum RIGHTS {
|
||||
QUERY_TRANSACTION_LINK = 'QUERY_TRANSACTION_LINK',
|
||||
QUERY_OPT_IN = 'QUERY_OPT_IN',
|
||||
CHECK_USERNAME = 'CHECK_USERNAME',
|
||||
PROJECT_BRANDING_BANNER = 'PROJECT_BRANDING_BANNER',
|
||||
// User
|
||||
VERIFY_LOGIN = 'VERIFY_LOGIN',
|
||||
BALANCE = 'BALANCE',
|
||||
@ -39,6 +40,8 @@ export enum RIGHTS {
|
||||
USER = 'USER',
|
||||
GMS_USER_PLAYGROUND = 'GMS_USER_PLAYGROUND',
|
||||
HUMHUB_AUTO_LOGIN = 'HUMHUB_AUTO_LOGIN',
|
||||
PROJECT_BRANDING_VIEW = 'PROJECT_BRANDING_VIEW',
|
||||
LIST_HUMHUB_SPACES = 'LIST_HUMHUB_SPACES',
|
||||
// Moderator
|
||||
SEARCH_USERS = 'SEARCH_USERS',
|
||||
ADMIN_CREATE_CONTRIBUTION = 'ADMIN_CREATE_CONTRIBUTION',
|
||||
@ -64,4 +67,5 @@ export enum RIGHTS {
|
||||
COMMUNITY_BY_IDENTIFIER = 'COMMUNITY_BY_IDENTIFIER',
|
||||
HOME_COMMUNITY = 'HOME_COMMUNITY',
|
||||
COMMUNITY_UPDATE = 'COMMUNITY_UPDATE',
|
||||
PROJECT_BRANDING_MUTATE = 'PROJECT_BRANDING_MUTATE',
|
||||
}
|
||||
|
||||
@ -31,4 +31,6 @@ export const USER_RIGHTS = [
|
||||
RIGHTS.USER,
|
||||
RIGHTS.GMS_USER_PLAYGROUND,
|
||||
RIGHTS.HUMHUB_AUTO_LOGIN,
|
||||
RIGHTS.PROJECT_BRANDING_VIEW,
|
||||
RIGHTS.LIST_HUMHUB_SPACES,
|
||||
]
|
||||
|
||||
@ -81,8 +81,8 @@ const loginServer = {
|
||||
}
|
||||
|
||||
const email = {
|
||||
EMAIL: process.env.EMAIL === 'true' ?? false,
|
||||
EMAIL_TEST_MODUS: process.env.EMAIL_TEST_MODUS === 'true' ?? false,
|
||||
EMAIL: process.env.EMAIL === 'true' || false,
|
||||
EMAIL_TEST_MODUS: process.env.EMAIL_TEST_MODUS === 'true' || false,
|
||||
EMAIL_TEST_RECEIVER: process.env.EMAIL_TEST_RECEIVER ?? 'stage1@gradido.net',
|
||||
EMAIL_USERNAME: process.env.EMAIL_USERNAME ?? '',
|
||||
EMAIL_SENDER: process.env.EMAIL_SENDER ?? 'info@gradido.net',
|
||||
@ -123,7 +123,7 @@ const federation = {
|
||||
process.env.FEDERATION_VALIDATE_COMMUNITY_TIMER ?? 60000,
|
||||
),
|
||||
FEDERATION_XCOM_SENDCOINS_ENABLED:
|
||||
process.env.FEDERATION_XCOM_SENDCOINS_ENABLED === 'true' ?? false,
|
||||
process.env.FEDERATION_XCOM_SENDCOINS_ENABLED === 'true' || false,
|
||||
// default value for community-uuid is equal uuid of stage-3
|
||||
FEDERATION_XCOM_RECEIVER_COMMUNITY_UUID:
|
||||
process.env.FEDERATION_XCOM_RECEIVER_COMMUNITY_UUID ?? '56a55482-909e-46a4-bfa2-cd025e894ebc',
|
||||
|
||||
@ -40,6 +40,7 @@ export const sendAccountActivationEmail = (data: {
|
||||
language: string
|
||||
activationLink: string
|
||||
timeDurationObject: Record<string, unknown>
|
||||
logoUrl?: string | null
|
||||
}): Promise<Record<string, unknown> | boolean | null> => {
|
||||
return sendEmailTranslated({
|
||||
receiver: { to: `${data.firstName} ${data.lastName} <${data.email}>` },
|
||||
@ -50,6 +51,7 @@ export const sendAccountActivationEmail = (data: {
|
||||
locale: data.language,
|
||||
activationLink: data.activationLink,
|
||||
timeDurationObject: data.timeDurationObject,
|
||||
logoUrl: data.logoUrl,
|
||||
resendLink: CONFIG.EMAIL_LINK_FORGOTPASSWORD,
|
||||
supportEmail: CONFIG.COMMUNITY_SUPPORT_MAIL,
|
||||
communityURL: CONFIG.COMMUNITY_URL,
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
extend ../layout.pug
|
||||
|
||||
block content
|
||||
if logoUrl
|
||||
img(src=logoUrl, alt="Banner", style="max-width: 680px; max-height: 250px;border-radius:20px")
|
||||
h2= t('emails.accountActivation.title')
|
||||
.text-block
|
||||
include ../includes/salutation.pug
|
||||
|
||||
@ -30,4 +30,8 @@ export class CreateUserArgs {
|
||||
@Field(() => String, { nullable: true })
|
||||
@IsString()
|
||||
redeemCode?: string | null
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
@IsString()
|
||||
project?: string | null
|
||||
}
|
||||
|
||||
@ -14,4 +14,8 @@ export class UnsecureLoginArgs {
|
||||
@Field(() => Int, { nullable: true })
|
||||
@IsInt()
|
||||
publisherId?: number | null
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
@IsString()
|
||||
project?: string | null
|
||||
}
|
||||
|
||||
44
backend/src/graphql/input/ProjectBrandingInput.ts
Normal file
44
backend/src/graphql/input/ProjectBrandingInput.ts
Normal file
@ -0,0 +1,44 @@
|
||||
import { IsString, IsOptional, MaxLength, IsNumber, IsBoolean, IsUrl } from 'class-validator'
|
||||
import { InputType, Field, Int } from 'type-graphql'
|
||||
|
||||
@InputType()
|
||||
export class ProjectBrandingInput {
|
||||
@Field(() => Int, { nullable: true })
|
||||
@IsOptional()
|
||||
id: number | null | undefined
|
||||
|
||||
@Field(() => String)
|
||||
@IsString()
|
||||
name: string
|
||||
|
||||
@Field(() => String)
|
||||
@IsString()
|
||||
@MaxLength(32)
|
||||
alias: string
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
description: string | null | undefined
|
||||
|
||||
@Field(() => Int, { nullable: true })
|
||||
@IsNumber()
|
||||
@IsOptional()
|
||||
spaceId: number | null | undefined
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@IsUrl()
|
||||
spaceUrl: string | null | undefined
|
||||
|
||||
@Field(() => Boolean)
|
||||
@IsBoolean()
|
||||
newUserToSpace: boolean
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@IsUrl()
|
||||
logoUrl: string | null | undefined
|
||||
}
|
||||
19
backend/src/graphql/model/Pagination.ts
Normal file
19
backend/src/graphql/model/Pagination.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import { ObjectType, Field, Int } from 'type-graphql'
|
||||
|
||||
@ObjectType()
|
||||
export class Pagination {
|
||||
@Field(() => Int)
|
||||
total: number
|
||||
|
||||
@Field(() => Int)
|
||||
page: number
|
||||
|
||||
@Field(() => Int)
|
||||
pages: number
|
||||
|
||||
constructor(total: number, page: number, pages: number) {
|
||||
this.total = total
|
||||
this.page = page
|
||||
this.pages = pages
|
||||
}
|
||||
}
|
||||
33
backend/src/graphql/model/ProjectBranding.ts
Normal file
33
backend/src/graphql/model/ProjectBranding.ts
Normal file
@ -0,0 +1,33 @@
|
||||
import { ProjectBranding as dbProjectBranding } from '@entity/ProjectBranding'
|
||||
import { ObjectType, Field, Int } from 'type-graphql'
|
||||
|
||||
@ObjectType()
|
||||
export class ProjectBranding {
|
||||
constructor(projectBranding: dbProjectBranding) {
|
||||
Object.assign(this, projectBranding)
|
||||
}
|
||||
|
||||
@Field(() => Int)
|
||||
id: number
|
||||
|
||||
@Field(() => String)
|
||||
name: string
|
||||
|
||||
@Field(() => String)
|
||||
alias: string
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
description: string | null
|
||||
|
||||
@Field(() => Int, { nullable: true })
|
||||
spaceId: number | null
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
spaceUrl: string | null
|
||||
|
||||
@Field(() => Boolean)
|
||||
newUserToSpace: boolean
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
logoUrl: string | null
|
||||
}
|
||||
25
backend/src/graphql/model/Space.ts
Normal file
25
backend/src/graphql/model/Space.ts
Normal file
@ -0,0 +1,25 @@
|
||||
import { ObjectType, Field, Int } from 'type-graphql'
|
||||
|
||||
import { Space as HumhubSpace } from '@/apis/humhub/model/Space'
|
||||
|
||||
@ObjectType()
|
||||
export class Space {
|
||||
@Field(() => Int)
|
||||
id: number
|
||||
|
||||
@Field(() => String)
|
||||
guid: string
|
||||
|
||||
@Field(() => String)
|
||||
name: string
|
||||
|
||||
@Field(() => String)
|
||||
description: string
|
||||
|
||||
@Field(() => String)
|
||||
url: string
|
||||
|
||||
constructor(data: HumhubSpace) {
|
||||
Object.assign(this, data)
|
||||
}
|
||||
}
|
||||
20
backend/src/graphql/model/SpaceList.ts
Normal file
20
backend/src/graphql/model/SpaceList.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import { ObjectType, Field } from 'type-graphql'
|
||||
|
||||
import { SpacesResponse } from '@/apis/humhub/model/SpacesResponse'
|
||||
|
||||
import { Pagination } from './Pagination'
|
||||
import { Space } from './Space'
|
||||
|
||||
@ObjectType()
|
||||
export class SpaceList {
|
||||
@Field(() => Pagination)
|
||||
pagination: Pagination
|
||||
|
||||
@Field(() => [Space])
|
||||
results: Space[]
|
||||
|
||||
constructor(data: SpacesResponse) {
|
||||
this.pagination = new Pagination(data.total, data.page, data.pages)
|
||||
this.results = data.results
|
||||
}
|
||||
}
|
||||
@ -153,9 +153,9 @@ describe('ContributionMessageResolver', () => {
|
||||
message: 'Test',
|
||||
},
|
||||
})
|
||||
expect(logger.debug).toBeCalledTimes(4)
|
||||
expect(logger.debug).toBeCalledTimes(5)
|
||||
expect(logger.debug).toHaveBeenNthCalledWith(
|
||||
4,
|
||||
5,
|
||||
'use UnconfirmedContributionUserAddMessageRole',
|
||||
)
|
||||
expect(mutationResult).toEqual(
|
||||
@ -351,9 +351,9 @@ describe('ContributionMessageResolver', () => {
|
||||
},
|
||||
})
|
||||
|
||||
expect(logger.debug).toBeCalledTimes(4)
|
||||
expect(logger.debug).toBeCalledTimes(5)
|
||||
expect(logger.debug).toHaveBeenNthCalledWith(
|
||||
4,
|
||||
5,
|
||||
'use UnconfirmedContributionAdminAddMessageRole',
|
||||
)
|
||||
|
||||
@ -386,9 +386,9 @@ describe('ContributionMessageResolver', () => {
|
||||
},
|
||||
})
|
||||
|
||||
expect(logger.debug).toBeCalledTimes(4)
|
||||
expect(logger.debug).toBeCalledTimes(5)
|
||||
expect(logger.debug).toHaveBeenNthCalledWith(
|
||||
4,
|
||||
5,
|
||||
'use UnconfirmedContributionAdminAddMessageRole',
|
||||
)
|
||||
|
||||
@ -404,7 +404,7 @@ describe('ContributionMessageResolver', () => {
|
||||
})
|
||||
|
||||
it('logs the error "ContributionMessage was not sent successfully: Error: missing right ADMIN_CREATE_CONTRIBUTION_MESSAGE for user"', () => {
|
||||
expect(logger.debug).toBeCalledTimes(4)
|
||||
expect(logger.debug).toBeCalledTimes(5)
|
||||
expect(logger.error).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
'missing right ADMIN_CREATE_CONTRIBUTION_MESSAGE for user',
|
||||
|
||||
106
backend/src/graphql/resolver/ProjectBrandingResolver.ts
Normal file
106
backend/src/graphql/resolver/ProjectBrandingResolver.ts
Normal file
@ -0,0 +1,106 @@
|
||||
import { ProjectBranding as DbProjectBranding } from '@entity/ProjectBranding'
|
||||
import { Resolver, Query, Mutation, Arg, Int, Authorized, ID } from 'type-graphql'
|
||||
|
||||
import { ProjectBrandingInput } from '@input/ProjectBrandingInput'
|
||||
import { ProjectBranding } from '@model/ProjectBranding'
|
||||
import { Space } from '@model/Space'
|
||||
import { SpaceList } from '@model/SpaceList'
|
||||
|
||||
import { HumHubClient } from '@/apis/humhub/HumHubClient'
|
||||
import { RIGHTS } from '@/auth/RIGHTS'
|
||||
import { LogError } from '@/server/LogError'
|
||||
import { backendLogger as logger } from '@/server/logger'
|
||||
|
||||
@Resolver(() => ProjectBranding)
|
||||
export class ProjectBrandingResolver {
|
||||
@Query(() => [ProjectBranding])
|
||||
@Authorized([RIGHTS.PROJECT_BRANDING_VIEW])
|
||||
async projectBrandings(): Promise<ProjectBranding[]> {
|
||||
return (await DbProjectBranding.find()).map(
|
||||
(entity: DbProjectBranding) => new ProjectBranding(entity),
|
||||
)
|
||||
}
|
||||
|
||||
@Query(() => ProjectBranding)
|
||||
@Authorized([RIGHTS.PROJECT_BRANDING_VIEW])
|
||||
async projectBranding(@Arg('id', () => Int) id: number): Promise<ProjectBranding> {
|
||||
const projectBrandingEntity = await DbProjectBranding.findOneBy({ id })
|
||||
if (!projectBrandingEntity) {
|
||||
throw new LogError(`Project Branding with id: ${id} not found`)
|
||||
}
|
||||
return new ProjectBranding(projectBrandingEntity)
|
||||
}
|
||||
|
||||
@Query(() => String, { nullable: true })
|
||||
@Authorized([RIGHTS.PROJECT_BRANDING_BANNER])
|
||||
async projectBrandingBanner(@Arg('alias', () => String) alias: string): Promise<string | null> {
|
||||
const projectBrandingEntity = await DbProjectBranding.findOne({
|
||||
where: { alias },
|
||||
select: { id: true, logoUrl: true },
|
||||
})
|
||||
if (!projectBrandingEntity) {
|
||||
throw new LogError(`Project Branding with alias: ${alias} not found`)
|
||||
}
|
||||
return projectBrandingEntity.logoUrl
|
||||
}
|
||||
|
||||
@Mutation(() => ProjectBranding, { nullable: true })
|
||||
@Authorized([RIGHTS.PROJECT_BRANDING_MUTATE])
|
||||
async upsertProjectBranding(
|
||||
@Arg('input') input: ProjectBrandingInput,
|
||||
): Promise<ProjectBranding | null> {
|
||||
const projectBranding = input.id
|
||||
? await DbProjectBranding.findOneOrFail({ where: { id: input.id } })
|
||||
: new DbProjectBranding()
|
||||
|
||||
Object.assign(projectBranding, input)
|
||||
await projectBranding.save()
|
||||
|
||||
return new ProjectBranding(projectBranding)
|
||||
}
|
||||
|
||||
@Mutation(() => Boolean)
|
||||
@Authorized([RIGHTS.PROJECT_BRANDING_MUTATE])
|
||||
async deleteProjectBranding(@Arg('id', () => ID) id: number): Promise<boolean> {
|
||||
try {
|
||||
await DbProjectBranding.delete({ id })
|
||||
return true
|
||||
} catch (err) {
|
||||
logger.error(err)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
@Query(() => Space)
|
||||
@Authorized([RIGHTS.LIST_HUMHUB_SPACES])
|
||||
async space(@Arg('id', () => ID) id: number): Promise<Space> {
|
||||
const humhub = HumHubClient.getInstance()
|
||||
if (!humhub) {
|
||||
throw new LogError('HumHub client not initialized')
|
||||
}
|
||||
|
||||
const space = await humhub.space(id)
|
||||
if (!space) {
|
||||
throw new LogError(`Error requesting space with id: ${id} from HumHub`)
|
||||
}
|
||||
return new Space(space)
|
||||
}
|
||||
|
||||
@Query(() => SpaceList)
|
||||
@Authorized([RIGHTS.LIST_HUMHUB_SPACES])
|
||||
async spaces(
|
||||
@Arg('page', () => Int, { defaultValue: 1 }) page: number,
|
||||
@Arg('limit', () => Int, { defaultValue: 20 }) limit: number,
|
||||
): Promise<SpaceList> {
|
||||
const humhub = HumHubClient.getInstance()
|
||||
if (!humhub) {
|
||||
throw new LogError('HumHub client not initialized')
|
||||
}
|
||||
const offset = (page - 1) * limit
|
||||
const spaces = await humhub.spaces(offset, limit)
|
||||
if (!spaces) {
|
||||
throw new LogError('Error requesting spaces from HumHub')
|
||||
}
|
||||
return new SpaceList(spaces)
|
||||
}
|
||||
}
|
||||
@ -4,6 +4,7 @@
|
||||
/* eslint-disable @typescript-eslint/restrict-template-expressions */
|
||||
import { getConnection, In, Point } from '@dbTools/typeorm'
|
||||
import { ContributionLink as DbContributionLink } from '@entity/ContributionLink'
|
||||
import { ProjectBranding } from '@entity/ProjectBranding'
|
||||
import { TransactionLink as DbTransactionLink } from '@entity/TransactionLink'
|
||||
import { User as DbUser } from '@entity/User'
|
||||
import { UserContact as DbUserContact } from '@entity/UserContact'
|
||||
@ -145,10 +146,10 @@ export class UserResolver {
|
||||
@Authorized([RIGHTS.LOGIN])
|
||||
@Mutation(() => User)
|
||||
async login(
|
||||
@Args() { email, password, publisherId }: UnsecureLoginArgs,
|
||||
@Args() { email, password, publisherId, project }: UnsecureLoginArgs,
|
||||
@Ctx() context: Context,
|
||||
): Promise<User> {
|
||||
logger.info(`login with ${email}, ***, ${publisherId} ...`)
|
||||
logger.info(`login with ${email}, ***, ${publisherId}, project=${project} ...`)
|
||||
email = email.trim().toLowerCase()
|
||||
let dbUser: DbUser
|
||||
|
||||
@ -177,6 +178,7 @@ export class UserResolver {
|
||||
|
||||
// request to humhub and klicktipp run in parallel
|
||||
let humhubUserPromise: Promise<IRestResponse<GetUser>> | undefined
|
||||
let projectBrandingPromise: Promise<ProjectBranding | null> | undefined
|
||||
const klicktippStatePromise = getKlicktippState(dbUser.emailContact.email)
|
||||
if (CONFIG.HUMHUB_ACTIVE && dbUser.humhubAllowed) {
|
||||
const getHumhubUser = new PostUser(dbUser)
|
||||
@ -184,6 +186,12 @@ export class UserResolver {
|
||||
getHumhubUser.account.username,
|
||||
)
|
||||
}
|
||||
if (project) {
|
||||
projectBrandingPromise = ProjectBranding.findOne({
|
||||
where: { alias: project },
|
||||
select: { spaceId: true },
|
||||
})
|
||||
}
|
||||
|
||||
if (dbUser.passwordEncryptionType !== PasswordEncryptionType.GRADIDO_ID) {
|
||||
dbUser.passwordEncryptionType = PasswordEncryptionType.GRADIDO_ID
|
||||
@ -213,11 +221,20 @@ export class UserResolver {
|
||||
})
|
||||
|
||||
await EVENT_USER_LOGIN(dbUser)
|
||||
const projectBranding = await projectBrandingPromise
|
||||
logger.debug('project branding: ', projectBranding)
|
||||
// load humhub state
|
||||
if (humhubUserPromise) {
|
||||
try {
|
||||
const result = await humhubUserPromise
|
||||
user.humhubAllowed = result?.result?.account.status === 1
|
||||
if (user.humhubAllowed) {
|
||||
let spaceId = null
|
||||
if (projectBranding) {
|
||||
spaceId = projectBranding.spaceId
|
||||
}
|
||||
void syncHumhub(null, dbUser, spaceId)
|
||||
}
|
||||
} catch (e) {
|
||||
logger.error("couldn't reach out to humhub, disable for now", e)
|
||||
user.humhubAllowed = false
|
||||
@ -249,11 +266,12 @@ export class UserResolver {
|
||||
language,
|
||||
publisherId = null,
|
||||
redeemCode = null,
|
||||
project = null,
|
||||
}: CreateUserArgs,
|
||||
): Promise<User> {
|
||||
logger.addContext('user', 'unknown')
|
||||
logger.info(
|
||||
`createUser(email=${email}, firstName=${firstName}, lastName=${lastName}, language=${language}, publisherId=${publisherId}, redeemCode =${redeemCode})`,
|
||||
`createUser(email=${email}, firstName=${firstName}, lastName=${lastName}, language=${language}, publisherId=${publisherId}, redeemCode=${redeemCode}, project=${project})`,
|
||||
)
|
||||
// TODO: wrong default value (should be null), how does graphql work here? Is it an required field?
|
||||
// default int publisher_id = 0;
|
||||
@ -308,7 +326,13 @@ export class UserResolver {
|
||||
return user
|
||||
}
|
||||
}
|
||||
|
||||
let projectBrandingPromise: Promise<ProjectBranding | null> | undefined
|
||||
if (project) {
|
||||
projectBrandingPromise = ProjectBranding.findOne({
|
||||
where: { alias: project },
|
||||
select: { logoUrl: true, spaceId: true },
|
||||
})
|
||||
}
|
||||
const gradidoID = await newGradidoID()
|
||||
|
||||
const eventRegisterRedeem = Event(
|
||||
@ -356,6 +380,7 @@ export class UserResolver {
|
||||
const queryRunner = getConnection().createQueryRunner()
|
||||
await queryRunner.connect()
|
||||
await queryRunner.startTransaction('REPEATABLE READ')
|
||||
let projectBranding: ProjectBranding | null | undefined
|
||||
try {
|
||||
dbUser = await queryRunner.manager.save(dbUser).catch((error) => {
|
||||
throw new LogError('Error while saving dbUser', error)
|
||||
@ -373,8 +398,11 @@ export class UserResolver {
|
||||
|
||||
const activationLink = `${
|
||||
CONFIG.EMAIL_LINK_VERIFICATION
|
||||
}${emailContact.emailVerificationCode.toString()}${redeemCode ? `/${redeemCode}` : ''}`
|
||||
}${emailContact.emailVerificationCode.toString()}${redeemCode ? `/${redeemCode}` : ''}${
|
||||
project ? `?project=` + project : ''
|
||||
}`
|
||||
|
||||
projectBranding = projectBrandingPromise ? await projectBrandingPromise : undefined
|
||||
void sendAccountActivationEmail({
|
||||
firstName,
|
||||
lastName,
|
||||
@ -382,6 +410,7 @@ export class UserResolver {
|
||||
language,
|
||||
activationLink,
|
||||
timeDurationObject: getTimeDurationObject(CONFIG.EMAIL_CODE_VALID_TIME),
|
||||
logoUrl: projectBranding?.logoUrl,
|
||||
})
|
||||
logger.info(`sendAccountActivationEmail of ${firstName}.${lastName} to ${email}`)
|
||||
|
||||
@ -397,7 +426,11 @@ export class UserResolver {
|
||||
}
|
||||
logger.info('createUser() successful...')
|
||||
if (CONFIG.HUMHUB_ACTIVE) {
|
||||
void syncHumhub(null, dbUser)
|
||||
let spaceId: number | null = null
|
||||
if (projectBranding) {
|
||||
spaceId = projectBranding.spaceId
|
||||
}
|
||||
void syncHumhub(null, dbUser, spaceId)
|
||||
}
|
||||
|
||||
if (redeemCode) {
|
||||
@ -771,7 +804,10 @@ export class UserResolver {
|
||||
|
||||
@Authorized([RIGHTS.HUMHUB_AUTO_LOGIN])
|
||||
@Query(() => String)
|
||||
async authenticateHumhubAutoLogin(@Ctx() context: Context): Promise<string> {
|
||||
async authenticateHumhubAutoLogin(
|
||||
@Ctx() context: Context,
|
||||
@Arg('project', () => String, { nullable: true }) project?: string | null,
|
||||
): Promise<string> {
|
||||
logger.info(`authenticateHumhubAutoLogin()...`)
|
||||
const dbUser = getUser(context)
|
||||
const humhubClient = HumHubClient.getInstance()
|
||||
@ -790,7 +826,7 @@ export class UserResolver {
|
||||
if (humhubUser.account.status !== 1) {
|
||||
throw new LogError('user status is not 1', humhubUser.account.status)
|
||||
}
|
||||
return await humhubClient.createAutoLoginUrl(humhubUser.account.username)
|
||||
return await humhubClient.createAutoLoginUrl(humhubUser.account.username, project)
|
||||
}
|
||||
|
||||
@Authorized([RIGHTS.SEARCH_ADMIN_USERS])
|
||||
|
||||
@ -5,6 +5,7 @@ import { ApolloServerTestClient } from 'apollo-server-testing'
|
||||
|
||||
import { testEnvironment, cleanDB, contributionDateFormatter } from '@test/helpers'
|
||||
|
||||
import { CONFIG } from '@/config'
|
||||
import { userFactory } from '@/seeds/factory/user'
|
||||
import { login, createContribution, adminCreateContribution } from '@/seeds/graphql/mutations'
|
||||
import { bibiBloxberg } from '@/seeds/users/bibi-bloxberg'
|
||||
@ -14,6 +15,8 @@ import { getOpenCreations, getUserCreation } from './creations'
|
||||
|
||||
jest.mock('@/password/EncryptorUtils')
|
||||
|
||||
CONFIG.HUMHUB_ACTIVE = false
|
||||
|
||||
let mutate: ApolloServerTestClient['mutate'], con: Connection
|
||||
let testEnv: {
|
||||
mutate: ApolloServerTestClient['mutate']
|
||||
@ -272,6 +275,7 @@ describe('util/creation', () => {
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('getOpenCreations', () => {
|
||||
beforeAll(() => {
|
||||
// enable Fake timers
|
||||
|
||||
@ -6,10 +6,17 @@ import { ExecutedHumhubAction, syncUser } from '@/apis/humhub/syncUser'
|
||||
import { UpdateUserInfosArgs } from '@/graphql/arg/UpdateUserInfosArgs'
|
||||
import { backendLogger as logger } from '@/server/logger'
|
||||
|
||||
/**
|
||||
* Syncs the user with humhub
|
||||
* @param updateUserInfosArg
|
||||
* @param user
|
||||
* @returns humhub user id or undefined
|
||||
*/
|
||||
export async function syncHumhub(
|
||||
updateUserInfosArg: UpdateUserInfosArgs | null,
|
||||
user: User,
|
||||
): Promise<void> {
|
||||
spaceId?: number | null,
|
||||
): Promise<number | undefined> {
|
||||
// check for humhub relevant changes
|
||||
if (
|
||||
updateUserInfosArg &&
|
||||
@ -47,4 +54,9 @@ export async function syncHumhub(
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
|
||||
result: ExecutedHumhubAction[result as ExecutedHumhubAction],
|
||||
})
|
||||
if (spaceId && humhubUser) {
|
||||
await humhubClient.addUserToSpace(humhubUser.id, spaceId)
|
||||
logger.debug(`user added to space ${spaceId}`)
|
||||
}
|
||||
return user.id
|
||||
}
|
||||
|
||||
@ -0,0 +1,28 @@
|
||||
import { Entity, PrimaryGeneratedColumn, Column, BaseEntity } from 'typeorm'
|
||||
|
||||
@Entity('project_brandings')
|
||||
export class ProjectBranding extends BaseEntity {
|
||||
@PrimaryGeneratedColumn('increment', { unsigned: true })
|
||||
id: number
|
||||
|
||||
@Column({ name: 'name', type: 'varchar', length: 255 })
|
||||
name: string
|
||||
|
||||
@Column({ name: 'alias', type: 'varchar', length: 32 })
|
||||
alias: string
|
||||
|
||||
@Column({ name: 'description', type: 'text', nullable: true, default: null })
|
||||
description: string | null
|
||||
|
||||
@Column({ name: 'space_id', type: 'int', unsigned: true, nullable: true, default: null })
|
||||
spaceId: number | null
|
||||
|
||||
@Column({ name: 'space_url', type: 'varchar', length: 255, nullable: true, default: null })
|
||||
spaceUrl: string | null
|
||||
|
||||
@Column({ name: 'new_user_to_space', type: 'tinyint', width: 1, default: 0 })
|
||||
newUserToSpace: boolean
|
||||
|
||||
@Column({ name: 'logo_url', type: 'varchar', length: 255, nullable: true, default: null })
|
||||
logoUrl: string | null
|
||||
}
|
||||
1
database/entity/ProjectBranding.ts
Normal file
1
database/entity/ProjectBranding.ts
Normal file
@ -0,0 +1 @@
|
||||
export { ProjectBranding } from './0088-create_project_brandings/ProjectBranding'
|
||||
@ -2,6 +2,7 @@ import { ContributionLink } from './ContributionLink'
|
||||
import { LoginElopageBuys } from './LoginElopageBuys'
|
||||
import { LoginEmailOptIn } from './LoginEmailOptIn'
|
||||
import { Migration } from './Migration'
|
||||
import { ProjectBranding } from './ProjectBranding'
|
||||
import { Transaction } from './Transaction'
|
||||
import { TransactionLink } from './TransactionLink'
|
||||
import { User } from './User'
|
||||
@ -26,6 +27,7 @@ export const entities = [
|
||||
LoginElopageBuys,
|
||||
LoginEmailOptIn,
|
||||
Migration,
|
||||
ProjectBranding,
|
||||
PendingTransaction,
|
||||
Transaction,
|
||||
TransactionLink,
|
||||
|
||||
26
database/migrations/0088-create_project_brandings.ts
Normal file
26
database/migrations/0088-create_project_brandings.ts
Normal file
@ -0,0 +1,26 @@
|
||||
/* MIGRATION TO CREATE THE project_brandings table
|
||||
*
|
||||
* This migration creates the `community` and 'communityfederation' tables in the `apollo` database (`gradido_community`).
|
||||
*/
|
||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
|
||||
export async function upgrade(queryFn: (query: string, values?: any[]) => Promise<Array<any>>) {
|
||||
await queryFn(`
|
||||
CREATE TABLE \`project_brandings\`(
|
||||
\`id\` INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
\`name\` VARCHAR(255) NOT NULL,
|
||||
\`alias\` VARCHAR(32) NOT NULL,
|
||||
\`description\` TEXT NULL DEFAULT NULL,
|
||||
\`space_id\` INT UNSIGNED NULL DEFAULT NULL,
|
||||
\`space_url\` VARCHAR(255) NULL DEFAULT NULL,
|
||||
\`new_user_to_space\` TINYINT(1) NOT NULL DEFAULT FALSE,
|
||||
\`logo_url\` VARCHAR(255) NULL DEFAULT NULL,
|
||||
PRIMARY KEY(\`id\`)
|
||||
) ENGINE = InnoDB;
|
||||
`)
|
||||
}
|
||||
|
||||
export async function downgrade(queryFn: (query: string, values?: any[]) => Promise<Array<any>>) {
|
||||
await queryFn('DROP TABLE `project_brandings`')
|
||||
}
|
||||
1
frontend/.gitignore
vendored
1
frontend/.gitignore
vendored
@ -2,6 +2,7 @@
|
||||
node_modules/
|
||||
build/
|
||||
src/assets/css/gradido.css
|
||||
src/assets/css/gradido.css.map
|
||||
.cache/
|
||||
.yarn/install-state.gz
|
||||
|
||||
|
||||
@ -106,6 +106,7 @@
|
||||
"unplugin-icons": "^0.19.1",
|
||||
"unplugin-vue-components": "^0.27.3",
|
||||
"vite-plugin-environment": "^1.1.3",
|
||||
"vite-plugin-graphql-loader": "^4.0.4",
|
||||
"vite-plugin-html": "^3.2.2",
|
||||
"vitest": "^2.0.5",
|
||||
"vitest-canvas-mock": "^0.3.3"
|
||||
|
||||
File diff suppressed because one or more lines are too long
@ -134,6 +134,14 @@ a:hover,
|
||||
border-radius: 26px;
|
||||
}
|
||||
|
||||
.rounded-20 {
|
||||
border-radius: 20px;
|
||||
}
|
||||
|
||||
.rounded-like-card {
|
||||
border-radius: 23pt;
|
||||
}
|
||||
|
||||
.alert {
|
||||
border-radius: 26px;
|
||||
box-shadow: rgb(0 0 0 / 14%) 0 24px 80px;
|
||||
|
||||
@ -22,6 +22,12 @@ vi.mock('@vue/apollo-composable', () => ({
|
||||
})),
|
||||
}))
|
||||
|
||||
vi.mock('@/composables/useToast', () => ({
|
||||
useAppToast: vi.fn(() => ({
|
||||
addToast: vi.fn(),
|
||||
})),
|
||||
}))
|
||||
|
||||
describe('ContributionListItem', () => {
|
||||
let wrapper
|
||||
|
||||
|
||||
@ -68,6 +68,7 @@ export const createUser = gql`
|
||||
$language: String!
|
||||
$publisherId: Int
|
||||
$redeemCode: String
|
||||
$project: String
|
||||
) {
|
||||
createUser(
|
||||
email: $email
|
||||
@ -76,6 +77,7 @@ export const createUser = gql`
|
||||
language: $language
|
||||
publisherId: $publisherId
|
||||
redeemCode: $redeemCode
|
||||
project: $project
|
||||
) {
|
||||
id
|
||||
}
|
||||
@ -166,8 +168,8 @@ export const createContributionMessage = gql`
|
||||
`
|
||||
|
||||
export const login = gql`
|
||||
mutation ($email: String!, $password: String!, $publisherId: Int) {
|
||||
login(email: $email, password: $password, publisherId: $publisherId) {
|
||||
mutation ($email: String!, $password: String!, $publisherId: Int, $project: String) {
|
||||
login(email: $email, password: $password, publisherId: $publisherId, project: $project) {
|
||||
gradidoID
|
||||
alias
|
||||
firstName
|
||||
|
||||
3
frontend/src/graphql/projectBranding.graphql
Normal file
3
frontend/src/graphql/projectBranding.graphql
Normal file
@ -0,0 +1,3 @@
|
||||
query projectBrandingBanner($project: String!) {
|
||||
projectBrandingBanner(alias: $project)
|
||||
}
|
||||
@ -42,6 +42,11 @@ export const authenticateHumhubAutoLogin = gql`
|
||||
authenticateHumhubAutoLogin
|
||||
}
|
||||
`
|
||||
export const authenticateHumhubAutoLoginProject = gql`
|
||||
query ($project: String!) {
|
||||
authenticateHumhubAutoLogin(project: $project)
|
||||
}
|
||||
`
|
||||
|
||||
export const transactionsQuery = gql`
|
||||
query ($currentPage: Int = 1, $pageSize: Int = 25, $order: Order = DESC) {
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { createStore } from 'vuex'
|
||||
import AuthLayout from './AuthLayout'
|
||||
import {
|
||||
BAvatar,
|
||||
@ -39,6 +40,19 @@ vi.mock('@/config', () => ({
|
||||
|
||||
describe('AuthLayout', () => {
|
||||
let wrapper
|
||||
const createVuexStore = () => {
|
||||
return createStore({
|
||||
state: {
|
||||
project: '',
|
||||
},
|
||||
actions: {
|
||||
project: vi.fn(),
|
||||
},
|
||||
mutations: {
|
||||
project: vi.fn(),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const createWrapper = () => {
|
||||
return mount(AuthLayout, {
|
||||
@ -54,6 +68,7 @@ describe('AuthLayout', () => {
|
||||
BImg,
|
||||
BPopover,
|
||||
},
|
||||
plugins: [createVuexStore()],
|
||||
mocks: {
|
||||
$i18n: {
|
||||
locale: 'en',
|
||||
@ -97,17 +112,16 @@ describe('AuthLayout', () => {
|
||||
expect(wrapper.find('nav#sidenav-main').exists()).toBe(false)
|
||||
})
|
||||
|
||||
it('has LanguageSwitch2', () => {
|
||||
expect(wrapper.findComponent({ name: 'LanguageSwitch2' }).exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('displays the community name', () => {
|
||||
expect(wrapper.find('.h1').text()).toBe('Test Community')
|
||||
})
|
||||
|
||||
it('test size in setTextSize', async () => {
|
||||
const mockEl = { style: {} }
|
||||
vi.spyOn(document, 'querySelector').mockReturnValue(mockEl)
|
||||
|
||||
await wrapper.vm.setTextSize(0.85)
|
||||
expect(wrapper.vm.$refs.pageFontSize.$el.style.fontSize).toBe('0.85rem')
|
||||
expect(mockEl.style.fontSize).toBe('0.85rem')
|
||||
})
|
||||
})
|
||||
|
||||
@ -115,6 +129,7 @@ describe('AuthLayout', () => {
|
||||
beforeEach(() => {
|
||||
wrapper = mount(AuthLayout, {
|
||||
global: {
|
||||
plugins: [createVuexStore()],
|
||||
mocks: {
|
||||
$i18n: {
|
||||
locale: 'en',
|
||||
|
||||
@ -19,11 +19,21 @@
|
||||
<BCol sm="12" md="8" lg="6" class="zindex1000">
|
||||
<div class="ms-3 ms-sm-4 me-3 me-sm-4">
|
||||
<BRow class="d-none d-md-block d-lg-none">
|
||||
<BCol class="mb--4">
|
||||
<BCol>
|
||||
<auth-navbar-small />
|
||||
</BCol>
|
||||
</BRow>
|
||||
<BRow class="mt-0 mt-md-5 ps-2 ps-md-0 ps-lg-0">
|
||||
<BRow v-if="projectBannerResult || projectBannerLoading" class="d-none d-md-block">
|
||||
<BCol>
|
||||
<BImg
|
||||
v-if="projectBannerResult"
|
||||
:src="projectBannerResult.projectBrandingBanner"
|
||||
class="img-fluid rounded-like-card"
|
||||
alt="project banner"
|
||||
/>
|
||||
</BCol>
|
||||
</BRow>
|
||||
<BRow v-else class="mt-0 mt-md-5 ps-2 ps-md-0 ps-lg-0">
|
||||
<BCol lg="9" md="9" sm="12">
|
||||
<div class="mb--2">{{ $t('welcome') }}</div>
|
||||
<div class="h1 mb-0">{{ communityName }}</div>
|
||||
@ -33,7 +43,7 @@
|
||||
<BAvatar src="/img/brand/gradido_coin_128x128.png" size="6rem" />
|
||||
</BCol>
|
||||
</BRow>
|
||||
<BCard ref="pageFontSize" no-body class="border-0 mt-4 gradido-custom-background">
|
||||
<BCard no-body class="border-0 mt-4 gradido-custom-background page-font-size">
|
||||
<BRow class="p-4">
|
||||
<BCol cols="10">
|
||||
<language-switch-2 class="ms-3" />
|
||||
@ -61,7 +71,14 @@
|
||||
</BRow>
|
||||
<BRow class="d-inline d-sm-inline d-md-none d-lg-none mb-3">
|
||||
<BCol class="text-center">
|
||||
<BImg
|
||||
v-if="projectBannerResult"
|
||||
:src="projectBannerResult.projectBrandingBanner"
|
||||
class="img-fluid ms-1 me-1 col-10 col-sm-10 rounded-20"
|
||||
alt="project banner"
|
||||
/>
|
||||
<BAvatar
|
||||
v-else
|
||||
src="/img/brand/gradido_coin_128x128.png"
|
||||
size="6rem"
|
||||
bg-variant="transparent"
|
||||
@ -85,34 +102,45 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
<script setup>
|
||||
import { onBeforeMount, computed, watchEffect } from 'vue'
|
||||
import { useQuery } from '@vue/apollo-composable'
|
||||
import { projectBrandingBanner } from '@/graphql/projectBranding.graphql'
|
||||
import AuthNavbar from '@/components/Auth/AuthNavbar'
|
||||
import AuthNavbarSmall from '@/components/Auth/AuthNavbarSmall'
|
||||
import AuthCarousel from '@/components/Auth/AuthCarousel'
|
||||
import LanguageSwitch2 from '@/components/LanguageSwitch2'
|
||||
import AuthFooter from '@/components/Auth/AuthFooter'
|
||||
import CONFIG from '@/config'
|
||||
import { useStore } from 'vuex'
|
||||
|
||||
export default {
|
||||
name: 'AuthLayout',
|
||||
components: {
|
||||
AuthNavbar,
|
||||
AuthNavbarSmall,
|
||||
AuthCarousel,
|
||||
LanguageSwitch2,
|
||||
AuthFooter,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
communityName: CONFIG.COMMUNITY_NAME,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
setTextSize(size) {
|
||||
this.$refs.pageFontSize.$el.style.fontSize = size + 'rem'
|
||||
},
|
||||
},
|
||||
const communityName = CONFIG.COMMUNITY_NAME
|
||||
const store = useStore()
|
||||
const project = computed(() => {
|
||||
const urlParams = new URLSearchParams(window.location.search)
|
||||
return urlParams.get('project')
|
||||
})
|
||||
|
||||
const setTextSize = (size) => {
|
||||
document.querySelector('.page-font-size').style.fontSize = size + 'rem'
|
||||
}
|
||||
|
||||
const { result: projectBannerResult, loading: projectBannerLoading } = useQuery(
|
||||
projectBrandingBanner,
|
||||
{ project: project.value },
|
||||
{ enabled: !!project.value },
|
||||
)
|
||||
|
||||
onBeforeMount(() => {
|
||||
// clear state
|
||||
store.commit('project', null)
|
||||
})
|
||||
|
||||
// put project value into store, if projectBrandingBanner query don't throw an error, so project exists
|
||||
watchEffect(() => {
|
||||
if (projectBannerResult.value) {
|
||||
store.commit('project', project.value)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@ -126,6 +126,7 @@
|
||||
"session-expired": "Die Sitzung wurde aus Sicherheitsgründen beendet.",
|
||||
"unknown-error": "Unbekannter Fehler: "
|
||||
},
|
||||
"existingGradidoAccount": "Du hast schon ein {communityName} Konto?",
|
||||
"followUs": "folge uns:",
|
||||
"footer": {
|
||||
"app_version": "App version {version}",
|
||||
@ -275,6 +276,7 @@
|
||||
"title": "Danke!",
|
||||
"unsetPassword": "Dein Passwort wurde noch nicht gesetzt. Bitte setze es neu."
|
||||
},
|
||||
"missingGradidoAccount": "Noch kein {communityName} Konto?",
|
||||
"moderatorChangedMemo": "Text vom Moderator bearbeitet",
|
||||
"moderatorChat": "Moderator Chat",
|
||||
"navigation": {
|
||||
|
||||
@ -126,6 +126,7 @@
|
||||
"session-expired": "The session was closed for security reasons.",
|
||||
"unknown-error": "Unknown error: "
|
||||
},
|
||||
"existingGradidoAccount": "Already have a {communityName} account?",
|
||||
"followUs": "follow us:",
|
||||
"footer": {
|
||||
"app_version": "App version {version}",
|
||||
@ -275,6 +276,7 @@
|
||||
"title": "Thank you!",
|
||||
"unsetPassword": "Your password has not been set yet. Please set it again."
|
||||
},
|
||||
"missingGradidoAccount": "Don't have a {communityName} account yet?",
|
||||
"moderatorChangedMemo": "Text edited by moderator",
|
||||
"moderatorChat": "Moderator Chat",
|
||||
"navigation": {
|
||||
|
||||
@ -34,6 +34,9 @@ vi.mock('@vue/apollo-composable', () => ({
|
||||
useMutation: () => ({
|
||||
mutate: mockMutate,
|
||||
}),
|
||||
useLazyQuery: () => ({
|
||||
load: vi.fn(),
|
||||
}),
|
||||
}))
|
||||
|
||||
describe('Login', () => {
|
||||
|
||||
@ -32,6 +32,18 @@
|
||||
</BButton>
|
||||
</BCol>
|
||||
</BRow>
|
||||
<BRow>
|
||||
<BCol class="mt-3">
|
||||
{{ $t('missingGradidoAccount', { communityName: CONFIG.COMMUNITY_NAME }) }}
|
||||
</BCol>
|
||||
</BRow>
|
||||
<BRow>
|
||||
<BCol class="mt-1">
|
||||
<BLink :to="register()" class="register-nav-item">
|
||||
{{ $t('signup') }}
|
||||
</BLink>
|
||||
</BCol>
|
||||
</BRow>
|
||||
</form>
|
||||
</BContainer>
|
||||
<BContainer v-else>
|
||||
@ -50,13 +62,16 @@ import InputPassword from '@/components/Inputs/InputPassword'
|
||||
import InputEmail from '@/components/Inputs/InputEmail'
|
||||
import Message from '@/components/Message/Message'
|
||||
import { login } from '@/graphql/mutations'
|
||||
import { authenticateHumhubAutoLoginProject } from '@/graphql/queries'
|
||||
import { ref, computed } from 'vue'
|
||||
import { useStore } from 'vuex'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import { useForm } from 'vee-validate'
|
||||
import { useMutation } from '@vue/apollo-composable'
|
||||
import { useMutation, useLazyQuery } from '@vue/apollo-composable'
|
||||
import { useAppToast } from '@/composables/useToast'
|
||||
import { useAuthLinks } from '@/composables/useAuthLinks'
|
||||
import CONFIG from '@/config'
|
||||
// import { useLoading } from 'vue-loading-overlay'
|
||||
|
||||
const router = useRouter()
|
||||
@ -64,15 +79,17 @@ const route = useRoute()
|
||||
const store = useStore()
|
||||
const { t } = useI18n()
|
||||
const { mutate } = useMutation(login)
|
||||
const { load } = useLazyQuery(authenticateHumhubAutoLoginProject)
|
||||
// const $loading = useLoading() // TODO needs to be updated but there is some sort of an issue that breaks the app.
|
||||
const { toastError } = useAppToast()
|
||||
const { register } = useAuthLinks()
|
||||
|
||||
const form = ref({
|
||||
email: '',
|
||||
password: '',
|
||||
})
|
||||
|
||||
const { handleSubmit, meta, values } = useForm({
|
||||
const { handleSubmit, meta } = useForm({
|
||||
initialValues: form.value,
|
||||
})
|
||||
|
||||
@ -91,12 +108,21 @@ const onSubmit = handleSubmit(async (values) => {
|
||||
email: values.email,
|
||||
password: values.password,
|
||||
publisherId: store.state.publisherId,
|
||||
project: store.state.project,
|
||||
})
|
||||
const { login: loginResponse } = result.data
|
||||
await store.dispatch('login', loginResponse)
|
||||
store.commit('email', values.email)
|
||||
// await loader.hide()
|
||||
|
||||
if (store.state.project) {
|
||||
const result = await load(authenticateHumhubAutoLoginProject, {
|
||||
project: store.state.project,
|
||||
})
|
||||
window.location.href = result.authenticateHumhubAutoLogin
|
||||
return
|
||||
}
|
||||
|
||||
if (route.params.code) {
|
||||
await router.push(`/redeem/${route.params.code}`)
|
||||
} else {
|
||||
@ -135,4 +161,8 @@ const enterData = computed(() => !showPageMessage.value)
|
||||
padding-right: 0;
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
a.register-nav-item {
|
||||
color: #0e79bc !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -70,6 +70,18 @@
|
||||
</BButton>
|
||||
</BCol>
|
||||
</BRow>
|
||||
<BRow>
|
||||
<BCol class="mt-3">
|
||||
{{ $t('existingGradidoAccount', { communityName: CONFIG.COMMUNITY_NAME }) }}
|
||||
</BCol>
|
||||
</BRow>
|
||||
<BRow>
|
||||
<BCol class="mt-1">
|
||||
<BLink :to="login()" class="login-nav-item">
|
||||
{{ $t('signin') }}
|
||||
</BLink>
|
||||
</BCol>
|
||||
</BRow>
|
||||
</BForm>
|
||||
</BContainer>
|
||||
<BContainer v-else>
|
||||
@ -89,26 +101,29 @@ import { createUser } from '@/graphql/mutations'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useStore } from 'vuex'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { useAuthLinks } from '@/composables/useAuthLinks'
|
||||
import CONFIG from '@/config'
|
||||
|
||||
const { toastError } = useAppToast()
|
||||
const { login } = useAuthLinks()
|
||||
|
||||
const { mutate } = useMutation(createUser)
|
||||
|
||||
const { values: formValues, meta: formMeta, defineField, handleSubmit } = useForm()
|
||||
|
||||
const [firstname, firstnameProps] = defineField('firstname')
|
||||
const [firstname] = defineField('firstname')
|
||||
const { meta: firstnameMeta, errorMessage: firstnameError } = useField('firstname', {
|
||||
required: true,
|
||||
min: 3,
|
||||
})
|
||||
|
||||
const [lastname, lastnameProps] = defineField('lastname')
|
||||
const [lastname] = defineField('lastname')
|
||||
const { meta: lastnameMeta, errorMessage: lastnameError } = useField('lastname', {
|
||||
required: true,
|
||||
min: 2,
|
||||
})
|
||||
|
||||
const [agree, agreeProps] = defineField('agree')
|
||||
const [agree] = defineField('agree')
|
||||
const { meta: agreeMeta } = useField('agree', 'required')
|
||||
|
||||
const { t } = useI18n()
|
||||
@ -116,10 +131,8 @@ const store = useStore()
|
||||
const { params } = useRoute()
|
||||
|
||||
const showPageMessage = ref(false)
|
||||
const submitted = ref(false)
|
||||
const publisherId = ref(store.state.publisherId)
|
||||
const redeemCode = ref(params.code)
|
||||
const CONFIG = window.config
|
||||
|
||||
const enterData = computed(() => {
|
||||
return !showPageMessage.value
|
||||
@ -134,6 +147,7 @@ async function onSubmit() {
|
||||
language: store.state.language,
|
||||
publisherId: publisherId.value,
|
||||
redeemCode: redeemCode.value,
|
||||
project: store.state.project,
|
||||
})
|
||||
showPageMessage.value = true
|
||||
} catch (error) {
|
||||
@ -152,4 +166,8 @@ async function onSubmit() {
|
||||
padding-right: 0;
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
a.login-nav-item {
|
||||
color: #0e79bc !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -49,6 +49,9 @@ export const mutations = {
|
||||
gmsPublishLocation: (state, gmsPublishLocation) => {
|
||||
state.gmsPublishLocation = gmsPublishLocation
|
||||
},
|
||||
project: (state, project) => {
|
||||
state.project = project
|
||||
},
|
||||
publisherId: (state, publisherId) => {
|
||||
let pubId = parseInt(publisherId)
|
||||
if (isNaN(pubId)) pubId = null
|
||||
@ -113,6 +116,7 @@ export const actions = {
|
||||
commit('humhubPublishName', null)
|
||||
commit('gmsPublishLocation', null)
|
||||
commit('hasElopage', false)
|
||||
commit('project', null)
|
||||
commit('publisherId', null)
|
||||
commit('roles', null)
|
||||
commit('hideAmountGDD', false)
|
||||
@ -153,6 +157,7 @@ try {
|
||||
humhubPublishName: null,
|
||||
gmsPublishLocation: null,
|
||||
hasElopage: false,
|
||||
project: null,
|
||||
publisherId: null,
|
||||
hideAmountGDD: null,
|
||||
hideAmountGDT: null,
|
||||
|
||||
@ -161,9 +161,9 @@ describe('Vuex store', () => {
|
||||
const commit = vi.fn()
|
||||
const state = {}
|
||||
|
||||
it('calls nineteen commits', () => {
|
||||
it('calls twenty commits', () => {
|
||||
logout({ commit, state })
|
||||
expect(commit).toHaveBeenCalledTimes(19)
|
||||
expect(commit).toHaveBeenCalledTimes(20)
|
||||
})
|
||||
|
||||
// ... (other logout action tests remain largely the same)
|
||||
|
||||
@ -18,7 +18,8 @@ dotenv.config() // load env vars from .env
|
||||
const CONFIG = require('./src/config')
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig(({ command }) => {
|
||||
export default defineConfig(async ({ command }) => {
|
||||
const { vitePluginGraphqlLoader } = await import('vite-plugin-graphql-loader')
|
||||
if (command === 'serve') {
|
||||
CONFIG.FRONTEND_HOSTING = 'nodejs'
|
||||
} else {
|
||||
@ -117,6 +118,7 @@ export default defineConfig(({ command }) => {
|
||||
META_KEYWORDS_EN: null,
|
||||
META_AUTHOR: null,
|
||||
}),
|
||||
vitePluginGraphqlLoader(),
|
||||
commonjs(),
|
||||
],
|
||||
css: {
|
||||
|
||||
@ -2,41 +2,44 @@ import { defineConfig } from 'vite'
|
||||
import Vue from '@vitejs/plugin-vue'
|
||||
import path from 'path'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [Vue()],
|
||||
test: {
|
||||
globals: true,
|
||||
environment: 'jsdom',
|
||||
// setupFiles: ['./test/vitest.setup.js'],
|
||||
coverage: {
|
||||
provider: 'v8',
|
||||
reporter: ['text', 'json', 'html'],
|
||||
exclude: ['node_modules/**', 'src/assets/**', '**/*.{spec,test}.js'],
|
||||
lines: 95,
|
||||
},
|
||||
include: ['**/?(*.)+(spec|test).js?(x)'],
|
||||
moduleNameMapper: {
|
||||
'^@/(.*)$': path.resolve(__dirname, './src/$1'),
|
||||
'\\.(css|less)$': 'identity-obj-proxy',
|
||||
},
|
||||
transformMode: {
|
||||
web: [/\.[jt]sx$/],
|
||||
},
|
||||
server: {
|
||||
deps: {
|
||||
inline: [/vee-validate/, 'vitest-canvas-mock'],
|
||||
export default defineConfig(async () => {
|
||||
const { vitePluginGraphqlLoader } = await import('vite-plugin-graphql-loader')
|
||||
return {
|
||||
plugins: [Vue(), vitePluginGraphqlLoader()],
|
||||
test: {
|
||||
globals: true,
|
||||
environment: 'jsdom',
|
||||
// setupFiles: ['./test/vitest.setup.js'],
|
||||
coverage: {
|
||||
provider: 'v8',
|
||||
reporter: ['text', 'json', 'html'],
|
||||
exclude: ['node_modules/**', 'src/assets/**', '**/*.{spec,test}.js'],
|
||||
lines: 95,
|
||||
},
|
||||
include: ['**/?(*.)+(spec|test).js?(x)'],
|
||||
moduleNameMapper: {
|
||||
'^@/(.*)$': path.resolve(__dirname, './src/$1'),
|
||||
'\\.(css|less)$': 'identity-obj-proxy',
|
||||
},
|
||||
transformMode: {
|
||||
web: [/\.[jt]sx$/],
|
||||
},
|
||||
server: {
|
||||
deps: {
|
||||
inline: [/vee-validate/, 'vitest-canvas-mock'],
|
||||
},
|
||||
},
|
||||
alias: {
|
||||
'@': path.resolve(__dirname, './src'),
|
||||
'@test': path.resolve(__dirname, './test'),
|
||||
},
|
||||
},
|
||||
alias: {
|
||||
'@': path.resolve(__dirname, './src'),
|
||||
'@test': path.resolve(__dirname, './test'),
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': path.resolve(__dirname, './src'),
|
||||
'@test': path.resolve(__dirname, './test'),
|
||||
},
|
||||
extensions: ['.mjs', '.js', '.ts', '.jsx', '.tsx', '.json', '.vue'],
|
||||
},
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': path.resolve(__dirname, './src'),
|
||||
'@test': path.resolve(__dirname, './test'),
|
||||
},
|
||||
extensions: ['.mjs', '.js', '.ts', '.jsx', '.tsx', '.json', '.vue'],
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
@ -4218,7 +4218,7 @@ graphql-tag@^2.12.6, graphql-tag@^2.4.2:
|
||||
dependencies:
|
||||
tslib "^2.1.0"
|
||||
|
||||
graphql@^16.9.0:
|
||||
graphql@^16.8.1, graphql@^16.9.0:
|
||||
version "16.10.0"
|
||||
resolved "https://registry.yarnpkg.com/graphql/-/graphql-16.10.0.tgz#24c01ae0af6b11ea87bf55694429198aaa8e220c"
|
||||
integrity sha512-AjqGKbDGUFRKIRCP9tCKiIGHyriz2oHEbPIbEtcSLSs4YjReZOIPQQWek4+6hjw62H9QShXHyaGivGiYVLeYFQ==
|
||||
@ -5074,7 +5074,7 @@ lru-cache@^5.1.1:
|
||||
dependencies:
|
||||
yallist "^3.0.2"
|
||||
|
||||
magic-string@^0.30.11, magic-string@^0.30.12, magic-string@^0.30.14, magic-string@^0.30.17:
|
||||
magic-string@^0.30.10, magic-string@^0.30.11, magic-string@^0.30.12, magic-string@^0.30.14, magic-string@^0.30.17:
|
||||
version "0.30.17"
|
||||
resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.17.tgz#450a449673d2460e5bbcfba9a61916a1714c7453"
|
||||
integrity sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==
|
||||
@ -6466,7 +6466,14 @@ string.prototype.trimstart@^1.0.8:
|
||||
define-properties "^1.2.1"
|
||||
es-object-atoms "^1.0.0"
|
||||
|
||||
"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@6.0.1, strip-ansi@^6.0.0, strip-ansi@^6.0.1, strip-ansi@^7.0.1, strip-ansi@^7.1.0:
|
||||
"strip-ansi-cjs@npm:strip-ansi@^6.0.1":
|
||||
version "6.0.1"
|
||||
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
|
||||
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
|
||||
dependencies:
|
||||
ansi-regex "^5.0.1"
|
||||
|
||||
strip-ansi@6.0.1, strip-ansi@^6.0.0, strip-ansi@^6.0.1, strip-ansi@^7.0.1, strip-ansi@^7.1.0:
|
||||
version "6.0.1"
|
||||
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
|
||||
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
|
||||
@ -7168,6 +7175,15 @@ vite-plugin-environment@^1.1.3:
|
||||
resolved "https://registry.yarnpkg.com/vite-plugin-environment/-/vite-plugin-environment-1.1.3.tgz#d01a04abb2f69730a4866c9c9db51d3dab74645b"
|
||||
integrity sha512-9LBhB0lx+2lXVBEWxFZC+WO7PKEyE/ykJ7EPWCq95NEcCpblxamTbs5Dm3DLBGzwODpJMEnzQywJU8fw6XGGGA==
|
||||
|
||||
vite-plugin-graphql-loader@^4.0.4:
|
||||
version "4.0.4"
|
||||
resolved "https://registry.yarnpkg.com/vite-plugin-graphql-loader/-/vite-plugin-graphql-loader-4.0.4.tgz#cf6c599b3e5fa32bf2b768983da68f7beccc8486"
|
||||
integrity sha512-lYnpQ2luV2fcuXmOJADljuktfMbDW00Y+6QS+Ek8Jz1Vdzlj/51LSGJwZqyjJ24a5YQ+o29Hr6el/5+nlZetvg==
|
||||
dependencies:
|
||||
graphql "^16.8.1"
|
||||
graphql-tag "^2.12.6"
|
||||
magic-string "^0.30.10"
|
||||
|
||||
vite-plugin-html@^3.2.2:
|
||||
version "3.2.2"
|
||||
resolved "https://registry.yarnpkg.com/vite-plugin-html/-/vite-plugin-html-3.2.2.tgz#661834fa09015d3fda48ba694dbaa809396f5f7a"
|
||||
@ -7443,7 +7459,16 @@ word-wrap@^1.2.5:
|
||||
resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34"
|
||||
integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==
|
||||
|
||||
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@7.0.0, wrap-ansi@^8.1.0:
|
||||
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0":
|
||||
version "7.0.0"
|
||||
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
|
||||
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
|
||||
dependencies:
|
||||
ansi-styles "^4.0.0"
|
||||
string-width "^4.1.0"
|
||||
strip-ansi "^6.0.0"
|
||||
|
||||
wrap-ansi@7.0.0, wrap-ansi@^8.1.0:
|
||||
version "7.0.0"
|
||||
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
|
||||
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user