Merge remote-tracking branch 'origin/master' into

3447-modify-humhub-card-in-overview-page
This commit is contained in:
clauspeterhuebner 2025-03-20 23:15:25 +01:00
commit 6f02396d73
149 changed files with 2515 additions and 1330 deletions

View File

@ -29,7 +29,7 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Build 'test' image
run: |
docker build --target test -t "gradido/dht-node:test" -f dht-node/Dockerfile .
@ -49,7 +49,7 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Lint
run: cd database && yarn && cd ../config && yarn install && cd ../dht-node && yarn && yarn run lint
@ -61,7 +61,7 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Download Docker Image
uses: actions/download-artifact@v4
with:
@ -70,10 +70,10 @@ jobs:
- name: Load Docker Image
run: docker load < /tmp/dht-node.tar
- name: docker-compose mariadb
run: docker compose -f docker-compose.yml -f docker-compose.test.yml up --detach --no-deps mariadb
- name: Sleep for 30 seconds
run: sleep 30s
shell: bash
@ -85,7 +85,7 @@ jobs:
run: sleep 30s
shell: bash
#- name: Unit tests
# run: cd database && yarn && yarn build && cd ../dht-node && yarn && yarn test
- name: Unit tests
run: docker run --env NODE_ENV=test --env DB_HOST=mariadb --network gradido_internal-net --rm gradido/dht-node:test yarn run test
run: cd database && yarn && yarn build && cd ../config && yarn install && cd ../dht-node && yarn && yarn test
#- name: Unit tests
# run: docker run --env NODE_ENV=test --env DB_HOST=mariadb --network gradido_internal-net --rm gradido/dht-node:test yarn run test

View File

@ -4,8 +4,15 @@ All notable changes to this project will be documented in this file. Dates are d
Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
#### [2.4.5](https://github.com/gradido/gradido/compare/2.3.1...2.4.5)
#### [2.5.1](https://github.com/gradido/gradido/compare/2.3.1...2.5.1)
- refactor(frontend): contribution form refactor [`#3442`](https://github.com/gradido/gradido/pull/3442)
- feat(backend): correct user data transfer to gms [`#3433`](https://github.com/gradido/gradido/pull/3433)
- feat(backend): switch marker colors of community location and user location [`#3445`](https://github.com/gradido/gradido/pull/3445)
- feat(backend): darker email font for content [`#3441`](https://github.com/gradido/gradido/pull/3441)
- fix(other): clear also ~/.cache/yarn [`#3362`](https://github.com/gradido/gradido/pull/3362)
- refactor(backend): test refactor better bun compatibility [`#3438`](https://github.com/gradido/gradido/pull/3438)
- chore(release): v2.4.5 beta [`#3435`](https://github.com/gradido/gradido/pull/3435)
- feat(backend): add answer button inside E-Mail [`#3431`](https://github.com/gradido/gradido/pull/3431)
- feat(other): build config in deployment [`#3430`](https://github.com/gradido/gradido/pull/3430)
- fix(dht): and federation using config in bare-metal setup [`#3434`](https://github.com/gradido/gradido/pull/3434)

View File

@ -3,4 +3,5 @@ GRAPHQL_PATH=/graphql
WALLET_URL=http://localhost
WALLET_AUTH_PATH=/authenticate?token=
WALLET_LOGIN_PATH=/login
DEBUG_DISABLE_AUTH=false
DEBUG_DISABLE_AUTH=false
HUMHUB_ACTIVE=false

View File

@ -6,3 +6,6 @@ WALLET_AUTH_PATH=$WALLET_AUTH_PATH
WALLET_LOGIN_PATH=$WALLET_LOGIN_PATH
GRAPHQL_PATH=$GRAPHQL_PATH
DEBUG_DISABLE_AUTH=false
HUMHUB_ACTIVE=$HUMHUB_ACTIVE
HUMHUB_API_URL=$HUMHUB_API_URL

View File

@ -3,7 +3,7 @@
"description": "Administration Interface for Gradido",
"main": "index.js",
"author": "Moriz Wahl",
"version": "2.4.5",
"version": "2.5.1",
"license": "Apache-2.0",
"scripts": {
"start": "node run/server.js",
@ -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"
},

View File

@ -0,0 +1,17 @@
<template>
<div class="collapse-icon">
<IBiArrowUpCircle v-if="visible" class="text-black h2" />
<IBiArrowDownCircle v-else class="text-muted h2" />
</div>
</template>
<script>
export default {
name: 'CollapseIcon',
props: {
visible: {
type: Boolean,
required: true,
},
},
}
</script>

View File

@ -222,6 +222,10 @@ const onSubmit = () => {
}
}
toastSuccess(t('message.request'))
form.value = {
text: '',
memo: props.contributionMemo,
}
loading.value = false
})
.catch((error) => {

View File

@ -93,10 +93,12 @@ describe('ContributionMessagesList', () => {
wrapper = mount(ContributionMessagesList, {
props: {
contributionId: 42,
contributionMemo: 'test memo',
contributionUserId: 108,
contributionStatus: 'PENDING',
contribution: {
id: 42,
memo: 'test memo',
userId: 108,
status: 'PENDING',
},
hideResubmission: true,
},
global: {
@ -137,7 +139,7 @@ describe('ContributionMessagesList', () => {
})
it('does not render the ContributionMessagesFormular when status is not PENDING or IN_PROGRESS', async () => {
await wrapper.setProps({ contributionStatus: 'COMPLETED' })
await wrapper.setProps({ contribution: { status: 'COMPLETED' } })
expect(wrapper.find('contribution-messages-formular-stub').exists()).toBe(false)
})

View File

@ -1,17 +1,49 @@
<template>
<div class="contribution-messages-list">
<BListGroup>
<BListGroupItem>
<routerLink :to="searchLink" :title="$t('goTo.userSearch')">
{{ contribution.firstName }} {{ contribution.lastName }}
</routerLink>
&nbsp;
<a :href="mailtoLink">{{ contribution.email }}</a>
<IBiFilter id="filter-by-email" class="ms-1 pointer" @click="searchForEmail" />
<BTooltip target="filter-by-email" triggers="hover">
{{ $t('filter.byEmail') }}
</BTooltip>
&nbsp;
{{ contribution.username }}
&nbsp;
<span>
<a
v-if="humhubProfileLink"
id="humhub-username"
:href="humhubProfileLink"
target="_blank"
>
<i-arcticons-circles class="svg-icon" />
</a>
<BTooltip target="humhub-username" triggers="hover">
{{ $t('goTo.humhubProfile') }}
</BTooltip>
</span>
</BListGroupItem>
<BListGroupItem>
{{ $t('registered') }}: {{ new Date(contribution.createdAt).toLocaleString() }}
</BListGroupItem>
</BListGroup>
<BContainer>
<div v-for="message in messages" :key="message.id">
<contribution-messages-list-item
:message="message"
:contribution-user-id="contributionUserId"
:contribution-user-id="contribution.userId"
/>
</div>
</BContainer>
<div v-if="contributionStatus === 'PENDING' || contributionStatus === 'IN_PROGRESS'">
<div v-if="contribution.status === 'PENDING' || contribution.status === 'IN_PROGRESS'">
<contribution-messages-formular
:contribution-id="contributionId"
:contribution-memo="contributionMemo"
:contribution-id="contribution.id"
:contribution-memo="contribution.memo"
:hide-resubmission="hideResubmission"
:input-resubmission-date="resubmissionAt"
@get-list-contribution-messages="refetch"
@ -24,27 +56,16 @@
</template>
<script setup>
import { ref } from 'vue'
import { ref, computed } from 'vue'
import { useQuery } from '@vue/apollo-composable'
import { adminListContributionMessages } from '../../graphql/adminListContributionMessages.js'
import { useAppToast } from '@/composables/useToast'
import { BListGroupItem } from 'bootstrap-vue-next'
import CONFIG from '@/config'
const props = defineProps({
contributionId: {
type: Number,
required: true,
},
contributionMemo: {
type: String,
required: true,
},
contributionStatus: {
type: String,
required: true,
},
contributionUserId: {
type: Number,
contribution: {
type: Object,
required: true,
},
hideResubmission: {
@ -57,15 +78,36 @@ const props = defineProps({
},
})
const emit = defineEmits(['update-status', 'reload-contribution', 'update-contributions'])
const emit = defineEmits([
'update-status',
'reload-contribution',
'update-contributions',
'search-for-email',
])
const { toastError } = useAppToast()
const mailtoLink = computed(() => {
return `mailto:${props.contribution.email}`
})
const searchLink = computed(() => {
return `/user?search=${props.contribution.email}`
})
const humhubProfileLink = computed(() => {
if (CONFIG.HUMHUB_ACTIVE !== true) {
return undefined
}
let url = CONFIG.HUMHUB_API_URL
if (url.endsWith('/')) {
url = url.slice(0, -1)
}
return `${url}/u/${props.contribution.humhubUsername}`
})
const messages = ref([])
const { onResult, onError, result, refetch } = useQuery(
adminListContributionMessages,
{
contributionId: props.contributionId,
contributionId: props.contribution.id,
},
{
fetchPolicy: 'no-cache',
@ -91,6 +133,10 @@ const reloadContribution = (id) => {
const updateContributions = () => {
emit('update-contributions')
}
const searchForEmail = () => {
emit('search-for-email', props.contribution.email)
}
</script>
<style scoped>
.temp-message {

View File

@ -56,7 +56,7 @@ export default {
? formatDistanceToNow(new Date(dateString), {
includeSecond: true,
addSuffix: true,
locale: useDateLocale,
locale: useDateLocale(),
})
: ''
},

View File

@ -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')
})
})

View File

@ -32,10 +32,24 @@
<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>
<BNavItem @click="handleWallet">{{ $t('navbar.my-account') }}</BNavItem>
<BLink
href="https://gradido.net/coin/moderators-tutorial/"
class="nav-link"
target="_blank"
>
{{ $t('help.help') }}
</BLink>
<BNavItem @click="handleLogout">{{ $t('navbar.logout') }}</BNavItem>
</BNavbarNav>
</BCollapse>
@ -58,6 +72,7 @@ import {
BNavbarToggle,
vBToggle,
vBColorMode,
BLink,
} from 'bootstrap-vue-next'
import { useRoute } from 'vue-router'

View File

@ -0,0 +1,86 @@
<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 && paginationTotal > ITEMS_PER_PAGE"
v-model="paginationPage"
:total-rows="paginationTotal"
: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 paginationTotal = computed(() => result.value?.spaces?.pagination?.total || 0)
const paginationPage = computed(() => result.value?.spaces?.pagination?.page || 1)
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>

View 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>

View 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>

View File

@ -67,6 +67,7 @@
<BButton v-if="row.item.messagesCount > 0" @click="rowToggleDetails(row, 0)">
<IBiChatDots />
</BButton>
<collapse-icon v-else :visible="row.detailsShowing" @click="rowToggleDetails(row, 0)" />
</template>
<template #cell(deny)="row">
<div v-if="!myself(row.item)">
@ -92,6 +93,12 @@
</BButton>
</div>
</template>
<template #cell(firstName)="row">
<div class="no-select">{{ row.item.firstName }}</div>
</template>
<template #cell(lastName)="row">
<div class="no-select">{{ row.item.lastName }}</div>
</template>
<template #row-details="row">
<row-details
:row="row"
@ -103,6 +110,7 @@
<template #show-creation>
<div v-if="row.item.moderatorId">
<edit-creation-formular
v-if="row.item.confirmedAt === null"
type="singleCreation"
:item="row.item"
:row="row"
@ -112,15 +120,13 @@
</div>
<div v-else>
<contribution-messages-list
:contribution-id="row.item.id"
:contribution-status="row.item.status"
:contribution-user-id="row.item.userId"
:contribution-memo="row.item.memo"
:contribution="row.item"
:resubmission-at="row.item.resubmissionAt"
:hide-resubmission="hideResubmission"
@update-status="updateStatus"
@reload-contribution="reloadContribution"
@update-contributions="updateContributions"
@search-for-email="$emit('search-for-email', $event)"
/>
</div>
</template>
@ -168,7 +174,13 @@ export default {
required: false,
},
},
emits: ['update-contributions', 'reload-contribution', 'update-status', 'show-overlay'],
emits: [
'update-contributions',
'reload-contribution',
'update-status',
'show-overlay',
'search-for-email',
],
data() {
return {
slotIndex: 0,
@ -176,6 +188,12 @@ export default {
creationUserData: {},
}
},
mounted() {
this.addClipboardListener()
},
beforeUnmount() {
this.removeClipboardListener()
},
methods: {
myself(item) {
return item.userId === this.$store.state.moderator.id
@ -201,28 +219,39 @@ export default {
this.$emit('update-contributions')
},
rowToggleDetails(row, index) {
if (this.openRow) {
if (this.openRow.index === row.index) {
if (index === this.slotIndex) {
row.toggleDetails()
this.openRow = null
} else {
this.slotIndex = index
}
} else {
this.openRow.toggleDetails()
row.toggleDetails()
this.slotIndex = index
this.openRow = row
this.creationUserData = row.item
}
const isSameRow = this.openRow && this.openRow.index === row.index
const isSameSlot = index === this.slotIndex
if (isSameRow && isSameSlot) {
row.toggleDetails()
this.openRow = null
} else {
if (this.openRow) {
this.openRow.toggleDetails()
}
row.toggleDetails()
this.slotIndex = index
this.openRow = row
this.creationUserData = row.item
}
},
addClipboardListener() {
document.addEventListener('copy', this.handleCopy)
},
removeClipboardListener() {
document.removeEventListener('copy', this.handleCopy)
},
handleCopy(event) {
// get from user selected text
const selectedText = window.getSelection().toString()
if (selectedText) {
// remove hashtags
const cleanedText = selectedText.replace(/#[a-zA-Z0-9_-]*/g, '')
event.clipboardData.setData('text/plain', cleanedText)
event.preventDefault()
}
},
},
}
</script>
@ -231,4 +260,8 @@ export default {
background-color: #e1a908;
border-color: #e1a908;
}
.no-select {
user-select: none;
}
</style>

View 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>

View 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>

View File

@ -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,
@ -50,12 +50,17 @@ const endpoints = {
const debug = {
DEBUG_DISABLE_AUTH: process.env.DEBUG_DISABLE_AUTH === 'true' ?? false,
}
const humhub = {
HUMHUB_ACTIVE: process.env.HUMHUB_ACTIVE === 'true' || false,
HUMHUB_API_URL: process.env.HUMHUB_API_URL ?? COMMUNITY_URL + '/community/',
}
const CONFIG = {
...version,
...environment,
...endpoints,
...debug,
...humhub,
ADMIN_MODULE_URL,
COMMUNITY_URL,
}

View File

@ -5,6 +5,8 @@ const {
COMMUNITY_URL,
DEBUG,
GRAPHQL_URI,
HUMHUB_ACTIVE,
HUMHUB_API_URL,
NODE_ENV,
PRODUCTION,
} = require('gradido-config/build/src/commonSchema.js')
@ -17,6 +19,8 @@ module.exports = Joi.object({
COMMUNITY_URL,
DEBUG,
GRAPHQL_URI,
HUMHUB_ACTIVE,
HUMHUB_API_URL,
NODE_ENV,
PRODUCTION,

View File

@ -26,6 +26,9 @@ export const adminListContributions = gql`
id
firstName
lastName
email
username
humhubUsername
amount
memo
createdAt

View 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
}

View 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
}
}

View File

@ -1,5 +1,7 @@
{
"GDD": "GDD",
"actions": "Aktionen",
"alias": "Alias",
"all_emails": "Alle Nutzer",
"back": "zurück",
"change_user_role": "Nutzerrolle ändern",
@ -33,7 +35,8 @@
"validTo": "Enddatum"
},
"contributionMessagesForm": {
"resubmissionDateInPast": "Wiedervorlage Datum befindet sich in der Vergangenheit!"
"resubmissionDateInPast": "Wiedervorlage Datum befindet sich in der Vergangenheit!",
"hasRegisteredAt": "hat sich am {createdAt} registriert."
},
"contributions": {
"all": "Alle",
@ -42,6 +45,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 +73,8 @@
"deleted": "gelöscht",
"deleted_user": "Alle gelöschten Nutzer",
"deny": "Ablehnen",
"description": "Beschreibung",
"details": "Details",
"e_mail": "E-Mail",
"edit": "bearbeiten",
"enabled": "aktiviert",
@ -92,6 +99,9 @@
"verified": "Verifiziert",
"verifiedAt": "Verifiziert am"
},
"filter": {
"byEmail": "Nach E-Mail filtern"
},
"firstname": "Vorname",
"footer": {
"app_version": "App version {version}",
@ -112,6 +122,10 @@
"describe": "Teilt Koordinaten im Format 'Breitengrad, Längengrad' automatisch auf. Fügen sie hier einfach z.B. ihre Koordinaten von Google Maps, zum Beispiel: 49.28187664243721, 9.740672183943639, ein."
}
},
"goTo": {
"userSearch": "Zur Nutzersuche gehen",
"humhubProfile": "Zum Humhub Profil gehen"
},
"help": {
"help": "Hilfe",
"transactionlist": {
@ -127,6 +141,8 @@
"lastname": "Nachname",
"latitude": "Breitengrad:",
"latitude-longitude-smart": "Breitengrad, Längengrad",
"link": "Link",
"logo": "Logo",
"longitude": "Längengrad:",
"math": {
"equals": "=",
@ -134,7 +150,7 @@
"plus": "+"
},
"message": {
"request": "Die Anfrage wurde gesendet."
"request": "Die Eingabe wurde gespeichert."
},
"moderator": {
"history": "Die Daten wurden geändert. Dies sind die alten Daten.",
@ -155,6 +171,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 +217,24 @@
"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",
"registered": "Registriert",
"removeNotSelf": "Als Admin/Moderator kannst du dich nicht selber löschen.",
"reset": "Zurücksetzen",
"save": "Speichern",
"statistic": {
"activeUsers": "Aktive Mitglieder",

View File

@ -1,5 +1,7 @@
{
"GDD": "GDD",
"actions": "Actions",
"alias": "Alias",
"all_emails": "All users",
"back": "back",
"change_user_role": "Change user role",
@ -33,7 +35,8 @@
"validTo": "End-Date"
},
"contributionMessagesForm": {
"resubmissionDateInPast": "Resubmission date is in the past!"
"resubmissionDateInPast": "Resubmission date is in the past!",
"hasRegisteredAt": "registered on {createdAt}."
},
"contributions": {
"all": "All",
@ -42,6 +45,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 +73,8 @@
"deleted": "deleted",
"deleted_user": "All deleted user",
"deny": "Reject",
"description": "Description",
"details": "Details",
"e_mail": "E-mail",
"edit": "edit",
"enabled": "enabled",
@ -92,6 +99,9 @@
"verified": "Verified",
"verifiedAt": "Verified at"
},
"filter": {
"byEmail": "Filter by email"
},
"firstname": "Firstname",
"footer": {
"app_version": "App version {version}",
@ -112,6 +122,10 @@
"describe": "Automatically splits coordinates in the format 'latitude, longitude'. Simply enter your coordinates from Google Maps here, for example: 49.28187664243721, 9.740672183943639."
}
},
"goTo": {
"userSearch": "Go to user search",
"humhubProfile": "Go to Humhub profile"
},
"help": {
"help": "Help",
"transactionlist": {
@ -127,6 +141,8 @@
"lastname": "Lastname",
"latitude": "Latitude:",
"latitude-longitude-smart": "Latitude, Longitude",
"link": "Link",
"logo": "Logo",
"longitude": "Longitude:",
"math": {
"equals": "=",
@ -134,7 +150,7 @@
"plus": "+"
},
"message": {
"request": "Request has been sent."
"request": "The entry has been saved."
},
"moderator": {
"history": "The data has been changed. This is the old data.",
@ -155,6 +171,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 +217,25 @@
"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",
"registered": "Registered",
"removeNotSelf": "As an admin/moderator, you cannot delete yourself.",
"save": "Speichern",
"reset": "Reset",
"save": "Save",
"statistic": {
"activeUsers": "Active members",
"count": "Count",

View File

@ -58,6 +58,7 @@
@update-status="updateStatus"
@reload-contribution="reloadContribution"
@update-contributions="refetch"
@search-for-email="query = $event"
/>
<BPagination
@ -152,7 +153,7 @@ const fields = computed(
formatter: (value) => formatDateOrDash(value),
},
{ key: 'moderatorId', label: t('moderator.moderator') },
{ key: 'editCreation', label: t('chat') },
{ key: 'editCreation', label: t('details') },
{ key: 'confirm', label: t('save') },
],
// confirmed contributions
@ -181,7 +182,7 @@ const fields = computed(
formatter: (value) => formatDateOrDash(value),
},
{ key: 'confirmedBy', label: t('moderator.moderator') },
{ key: 'chatCreation', label: t('chat') },
{ key: 'chatCreation', label: t('details') },
],
// denied contributions
[
@ -209,7 +210,7 @@ const fields = computed(
formatter: (value) => formatDateOrDash(value),
},
{ key: 'deniedBy', label: t('moderator.moderator') },
{ key: 'chatCreation', label: t('chat') },
{ key: 'chatCreation', label: t('details') },
],
// deleted contributions
[
@ -237,7 +238,7 @@ const fields = computed(
formatter: (value) => formatDateOrDash(value),
},
{ key: 'deletedBy', label: t('moderator.moderator') },
{ key: 'chatCreation', label: t('chat') },
{ key: 'chatCreation', label: t('details') },
],
// all contributions
[
@ -266,7 +267,7 @@ const fields = computed(
formatter: (value) => formatDateOrDash(value),
},
{ key: 'confirmedBy', label: t('moderator.moderator') },
{ key: 'chatCreation', label: t('chat') },
{ key: 'chatCreation', label: t('details') },
],
][tabIndex.value],
)

View 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>

View File

@ -15,6 +15,11 @@ vi.mock('@/composables/useToast', () => ({
toastSuccess: vi.fn(),
}),
}))
vi.mock('vue-router', () => ({
useRoute: () => ({
query: {},
}),
}))
// Mock icon components
const mockIconComponent = {

View File

@ -43,8 +43,9 @@
</div>
</template>
<script setup>
import { ref, reactive, computed, watch, watchEffect } from 'vue'
import { ref, reactive, computed, watch, watchEffect, onMounted } from 'vue'
import { useQuery } from '@vue/apollo-composable'
import { useRoute } from 'vue-router'
import { searchUsers } from '../graphql/searchUsers.js'
import useCreationMonths from '../composables/useCreationMonths'
import SearchUserTable from '../components/Tables/SearchUserTable'
@ -68,6 +69,7 @@ const response = ref()
const { creationLabel } = useCreationMonths()
const { toastSuccess } = useAppToast()
const route = useRoute()
const { result, refetch } = useQuery(searchUsers, {
query: criteria.value,
@ -105,6 +107,13 @@ const deletedUserSearch = () => {
refetch()
}
onMounted(() => {
const searchQuery = route.query.search
if (searchQuery) {
criteria.value = searchQuery
}
})
const fields = computed(() => [
{ key: 'email', label: t('e_mail') },
{ key: 'firstName', label: t('firstname') },

View File

@ -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',

View File

@ -17,13 +17,13 @@ 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 {
CONFIG.ADMIN_HOSTING = 'nginx'
}
// Check config
validate(schema, CONFIG)
// make sure that all urls used in browser have the same protocol to prevent mixed content errors
validate(browserUrls, [
@ -77,8 +77,11 @@ export default defineConfig(({ command }) => {
WALLET_AUTH_PATH: CONFIG.WALLET_AUTH_PATH ?? null,
WALLET_LOGIN_PATH: CONFIG.WALLET_LOGIN_URL ?? null, // null,
DEBUG_DISABLE_AUTH: CONFIG.DEBUG_DISABLE_AUTH ?? null, // null,
HUMHUB_ACTIVE: CONFIG.HUMHUB_ACTIVE ?? null, // null,
HUMHUB_API_URL: CONFIG.HUMHUB_API_URL ?? null, // null,
// CONFIG_VERSION: CONFIG.CONFIG_VERSION, // null,
}),
vitePluginGraphqlLoader(),
commonjs(),
],
build: {

View File

@ -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'],
},
}
})

View File

@ -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"

View File

@ -6,7 +6,7 @@ GRAPHIQL=false
GDT_ACTIVE=false
# Database
DB_HOST=localhost
DB_HOST=127.0.0.1
DB_PORT=3306
DB_USER=root
DB_PASSWORD=

View File

@ -6,7 +6,7 @@ GRAPHIQL=false
GDT_API_URL=https://gdt.gradido.net
# Database
DB_HOST=localhost
DB_HOST=127.0.0.1
DB_PORT=3306
DB_USER=root
DB_PASSWORD=

View File

@ -8,7 +8,7 @@ GRAPHIQL=false
GDT_API_URL=$GDT_API_URL
# Database
DB_HOST=localhost
DB_HOST=127.0.0.1
DB_PORT=3306
DB_USER=$DB_USER
DB_PASSWORD=$DB_PASSWORD

View File

@ -1 +1 @@
v18.7.0
v18.20.7

View File

@ -1,7 +1,7 @@
##################################################################################
# BASE ###########################################################################
##################################################################################
FROM node:18.7.0-alpine3.16 as base
FROM node:18.20.7-alpine3.21 as base
# ENVs (available in production aswell, can be overwritten by commandline or env file)
## DOCKER_WORKDIR would be a classical ARG, but that is not multi layer persistent - shame

View File

@ -7,7 +7,7 @@ module.exports = {
collectCoverageFrom: ['src/**/*.ts', '!**/node_modules/**', '!src/seeds/**', '!build/**'],
coverageThreshold: {
global: {
lines: 80,
lines: 79,
},
},
setupFiles: ['<rootDir>/test/testSetup.ts'],

View File

@ -1,6 +1,6 @@
{
"name": "gradido-backend",
"version": "2.4.5",
"version": "2.5.1",
"description": "Gradido unified backend providing an API-Service for Gradido Transactions",
"main": "src/index.ts",
"repository": "https://github.com/gradido/gradido/backend",
@ -69,10 +69,10 @@
"@types/nodemailer": "^6.4.4",
"@types/sodium-native": "^2.3.5",
"@types/uuid": "^8.3.4",
"@typescript-eslint/eslint-plugin": "^5.57.1",
"@typescript-eslint/parser": "^5.57.1",
"@typescript-eslint/eslint-plugin": "^5.62.0",
"@typescript-eslint/parser": "^5.62.0",
"apollo-server-testing": "^2.25.2",
"eslint": "^8.37.0",
"eslint": "^8.57.1",
"eslint-config-prettier": "^8.8.0",
"eslint-config-standard": "^17.0.0",
"eslint-import-resolver-typescript": "^3.5.4",
@ -92,9 +92,9 @@
"nodemon": "^2.0.7",
"prettier": "^2.8.7",
"ts-jest": "^27.0.5",
"ts-node": "^10.0.0",
"ts-node": "^10.9.2",
"tsconfig-paths": "^3.14.0",
"typescript": "^4.3.4"
"typescript": "^4.9.5"
},
"nodemonConfig": {
"ignore": [

View File

@ -49,14 +49,16 @@ export class GmsUser {
if (
user.gmsAllowed &&
user.alias &&
user.gmsPublishName === PublishNameType.PUBLISH_NAME_ALIAS_OR_INITALS
(user.gmsPublishName as PublishNameType) === PublishNameType.PUBLISH_NAME_ALIAS_OR_INITALS
) {
return user.alias
}
if (
user.gmsAllowed &&
((!user.alias && user.gmsPublishName === PublishNameType.PUBLISH_NAME_ALIAS_OR_INITALS) ||
user.gmsPublishName === PublishNameType.PUBLISH_NAME_INITIALS)
((!user.alias &&
(user.gmsPublishName as PublishNameType) ===
PublishNameType.PUBLISH_NAME_ALIAS_OR_INITALS) ||
(user.gmsPublishName as PublishNameType) === PublishNameType.PUBLISH_NAME_INITIALS)
) {
return (
this.firstUpperCaseSecondLowerCase(user.firstName) +
@ -68,16 +70,18 @@ export class GmsUser {
private getGmsFirstName(user: dbUser): string | null | undefined {
if (
user.gmsAllowed &&
(user.gmsPublishName === PublishNameType.PUBLISH_NAME_FIRST ||
user.gmsPublishName === PublishNameType.PUBLISH_NAME_FIRST_INITIAL ||
user.gmsPublishName === PublishNameType.PUBLISH_NAME_FULL)
((user.gmsPublishName as PublishNameType) === PublishNameType.PUBLISH_NAME_FIRST ||
(user.gmsPublishName as PublishNameType) === PublishNameType.PUBLISH_NAME_FIRST_INITIAL ||
(user.gmsPublishName as PublishNameType) === PublishNameType.PUBLISH_NAME_FULL)
) {
return user.firstName
}
if (
user.gmsAllowed &&
((!user.alias && user.gmsPublishName === PublishNameType.PUBLISH_NAME_ALIAS_OR_INITALS) ||
user.gmsPublishName === PublishNameType.PUBLISH_NAME_INITIALS)
((!user.alias &&
(user.gmsPublishName as PublishNameType) ===
PublishNameType.PUBLISH_NAME_ALIAS_OR_INITALS) ||
(user.gmsPublishName as PublishNameType) === PublishNameType.PUBLISH_NAME_INITIALS)
) {
// return this.firstUpperCaseSecondLowerCase(user.firstName)
return null // cause to delete firstname in gms
@ -85,10 +89,16 @@ export class GmsUser {
}
private getGmsLastName(user: dbUser): string | null | undefined {
if (user.gmsAllowed && user.gmsPublishName === PublishNameType.PUBLISH_NAME_FULL) {
if (
user.gmsAllowed &&
(user.gmsPublishName as PublishNameType) === PublishNameType.PUBLISH_NAME_FULL
) {
return user.lastName
}
if (user.gmsAllowed && user.gmsPublishName === PublishNameType.PUBLISH_NAME_FIRST_INITIAL) {
if (
user.gmsAllowed &&
(user.gmsPublishName as PublishNameType) === PublishNameType.PUBLISH_NAME_FIRST_INITIAL
) {
return this.firstUpperCaseSecondLowerCase(user.lastName)
}
return null // cause to delete lastname in gms
@ -114,8 +124,10 @@ export class GmsUser {
private getGmsCountryCode(user: dbUser): string | undefined {
if (
user.gmsAllowed &&
(user.emailContact?.gmsPublishPhone === GmsPublishPhoneType.GMS_PUBLISH_PHONE_COUNTRY ||
user.emailContact?.gmsPublishPhone === GmsPublishPhoneType.GMS_PUBLISH_PHONE_FULL)
((user.emailContact?.gmsPublishPhone as GmsPublishPhoneType) ===
GmsPublishPhoneType.GMS_PUBLISH_PHONE_COUNTRY ||
(user.emailContact?.gmsPublishPhone as GmsPublishPhoneType) ===
GmsPublishPhoneType.GMS_PUBLISH_PHONE_FULL)
) {
return user.emailContact?.countryCode
}
@ -124,7 +136,8 @@ export class GmsUser {
private getGmsPhone(user: dbUser): string | undefined {
if (
user.gmsAllowed &&
user.emailContact?.gmsPublishPhone === GmsPublishPhoneType.GMS_PUBLISH_PHONE_FULL
(user.emailContact?.gmsPublishPhone as GmsPublishPhoneType) ===
GmsPublishPhoneType.GMS_PUBLISH_PHONE_FULL
) {
return user.emailContact?.phone
}

View File

@ -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)
}
}
}

View File

@ -0,0 +1,7 @@
export class Space {
id: number
guid: string
name: string
description: string
url: string
}

View File

@ -0,0 +1,8 @@
import { Space } from './Space'
export interface SpacesResponse {
total: number
page: number
pages: number
results: Space[]
}

View File

@ -8,4 +8,5 @@ export const ADMIN_RIGHTS = [
RIGHTS.COMMUNITY_BY_UUID,
RIGHTS.COMMUNITY_BY_IDENTIFIER,
RIGHTS.HOME_COMMUNITY,
RIGHTS.PROJECT_BRANDING_MUTATE,
]

View File

@ -9,4 +9,5 @@ export const INALIENABLE_RIGHTS = [
RIGHTS.QUERY_TRANSACTION_LINK,
RIGHTS.QUERY_OPT_IN,
RIGHTS.CHECK_USERNAME,
RIGHTS.PROJECT_BRANDING_BANNER,
]

View File

@ -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',
}

View File

@ -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,
]

View File

@ -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',

View File

@ -17,6 +17,7 @@ import {
GMS_ACTIVE,
GRAPHIQL,
HUMHUB_ACTIVE,
HUMHUB_API_URL,
LOG4JS_CONFIG,
LOGIN_APP_SECRET,
LOGIN_SERVER_KEY,
@ -44,6 +45,7 @@ export const schema = Joi.object({
GMS_ACTIVE,
GRAPHIQL,
HUMHUB_ACTIVE,
HUMHUB_API_URL,
LOG4JS_CONFIG,
LOGIN_APP_SECRET,
LOGIN_SERVER_KEY,
@ -281,11 +283,6 @@ export const schema = Joi.object({
.when('GMS_ACTIVE', { is: true, then: Joi.required(), otherwise: Joi.optional() })
.description('The secret postfix for the GMS webhook endpoint'),
HUMHUB_API_URL: Joi.string()
.uri({ scheme: ['http', 'https'] })
.when('HUMHUB_ACTIVE', { is: true, then: Joi.required(), otherwise: Joi.optional() })
.description('The API URL for HumHub integration'),
HUMHUB_JWT_KEY: Joi.string()
.min(1)
.when('HUMHUB_ACTIVE', {

View File

@ -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,

View File

@ -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

View File

@ -30,4 +30,8 @@ export class CreateUserArgs {
@Field(() => String, { nullable: true })
@IsString()
redeemCode?: string | null
@Field(() => String, { nullable: true })
@IsString()
project?: string | null
}

View File

@ -14,4 +14,8 @@ export class UnsecureLoginArgs {
@Field(() => Int, { nullable: true })
@IsInt()
publisherId?: number | null
@Field(() => String, { nullable: true })
@IsString()
project?: string | null
}

View 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
}

View File

@ -3,12 +3,22 @@ import { User } from '@entity/User'
import { Decimal } from 'decimal.js-light'
import { ObjectType, Field, Int } from 'type-graphql'
import { PublishNameType } from '@enum/PublishNameType'
import { PublishNameLogic } from '@/data/PublishName.logic'
@ObjectType()
export class Contribution {
constructor(contribution: dbContribution, user?: User | null) {
this.id = contribution.id
this.firstName = user ? user.firstName : null
this.lastName = user ? user.lastName : null
this.firstName = user?.firstName ?? null
this.lastName = user?.lastName ?? null
this.email = user?.emailContact?.email ?? null
this.username = user?.alias ?? null
if (user) {
const publishNameLogic = new PublishNameLogic(user)
this.humhubUsername = publishNameLogic.getUsername(user.humhubPublishName as PublishNameType)
}
this.amount = contribution.amount
this.memo = contribution.memo
this.createdAt = contribution.createdAt
@ -37,6 +47,15 @@ export class Contribution {
@Field(() => String, { nullable: true })
lastName: string | null
@Field(() => String, { nullable: true })
email: string | null
@Field(() => String, { nullable: true })
username: string | null
@Field(() => String, { nullable: true })
humhubUsername: string | null
@Field(() => Decimal)
amount: Decimal

View 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
}
}

View 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
}

View 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)
}
}

View 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
}
}

View File

@ -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',

View File

@ -144,7 +144,7 @@ export class ContributionResolver {
dbContributions.map((contribution) => {
// filter out moderator messages for this call
contribution.messages = contribution.messages?.filter(
(m) => m.type !== ContributionMessageType.MODERATOR,
(m) => (m.type as ContributionMessageType) !== ContributionMessageType.MODERATOR,
)
return new Contribution(contribution, user)
}),
@ -352,7 +352,7 @@ export class ContributionResolver {
}
const moderator = getUser(context)
if (
contribution.contributionType === ContributionType.USER &&
(contribution.contributionType as ContributionType) === ContributionType.USER &&
contribution.userId === moderator.id
) {
throw new LogError('Own contribution can not be deleted as admin')
@ -532,8 +532,9 @@ export class ContributionResolver {
throw new LogError('Contribution not found', id)
}
if (
contributionToUpdate.contributionStatus !== ContributionStatus.IN_PROGRESS &&
contributionToUpdate.contributionStatus !== ContributionStatus.PENDING
(contributionToUpdate.contributionStatus as ContributionStatus) !==
ContributionStatus.IN_PROGRESS &&
(contributionToUpdate.contributionStatus as ContributionStatus) !== ContributionStatus.PENDING
) {
throw new LogError(
'Status of the contribution is not allowed',

View File

@ -0,0 +1,105 @@
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 spaces = await humhub.spaces(page, limit)
if (!spaces) {
throw new LogError('Error requesting spaces from HumHub')
}
return new SpaceList(spaces)
}
}

View File

@ -266,7 +266,7 @@ export class TransactionResolver {
// userTransactions.forEach((transaction: dbTransaction) => {
// use normal for loop because of timing problems with await in forEach-loop
for (const transaction of userTransactions) {
if (transaction.typeId === TransactionTypeId.CREATION) {
if ((transaction.typeId as TransactionTypeId) === TransactionTypeId.CREATION) {
continue
}
if (transaction.linkedUserId && !involvedUserIds.includes(transaction.linkedUserId)) {
@ -398,7 +398,7 @@ export class TransactionResolver {
: involvedUsers.find((u) => u.id === userTransaction.linkedUserId)
*/
let linkedUser: User | undefined
if (userTransaction.typeId === TransactionTypeId.CREATION) {
if ((userTransaction.typeId as TransactionTypeId) === TransactionTypeId.CREATION) {
linkedUser = communityUser
logger.debug('CREATION-linkedUser=', linkedUser)
} else if (userTransaction.linkedUserId) {

View File

@ -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,8 +186,17 @@ export class UserResolver {
getHumhubUser.account.username,
)
}
if (project) {
projectBrandingPromise = ProjectBranding.findOne({
where: { alias: project },
select: { spaceId: true },
})
}
if (dbUser.passwordEncryptionType !== PasswordEncryptionType.GRADIDO_ID) {
if (
(dbUser.passwordEncryptionType as PasswordEncryptionType) !==
PasswordEncryptionType.GRADIDO_ID
) {
dbUser.passwordEncryptionType = PasswordEncryptionType.GRADIDO_ID
dbUser.password = await encryptPassword(dbUser, password)
await dbUser.save()
@ -213,11 +224,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 +269,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 +329,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 +383,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 +401,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 +413,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 +429,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) {
@ -538,7 +574,7 @@ export class UserResolver {
// Sign into Klicktipp
// TODO do we always signUp the user? How to handle things with old users?
if (userContact.emailOptInTypeId === OptInType.EMAIL_OPT_IN_REGISTER) {
if ((userContact.emailOptInTypeId as OptInType) === OptInType.EMAIL_OPT_IN_REGISTER) {
try {
await subscribe(userContact.email, user.language, user.firstName, user.lastName)
logger.debug(
@ -771,7 +807,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 +829,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])

View File

@ -2,6 +2,7 @@ import { Point } from '@dbTools/typeorm'
import { User as DbUser } from '@entity/User'
import { UpdateUserInfosArgs } from '@/graphql/arg/UpdateUserInfosArgs'
import { GmsPublishLocationType } from '@/graphql/enum/GmsPublishLocationType'
import { PublishNameType } from '@/graphql/enum/PublishNameType'
import { LogError } from '@/server/LogError'
import { backendLogger as logger } from '@/server/logger'
@ -44,13 +45,14 @@ export function compareGmsRelevantUserSettings(
}
if (
updateUserInfosArgs.gmsPublishLocation &&
orgUser.gmsPublishLocation !== updateUserInfosArgs.gmsPublishLocation
(orgUser.gmsPublishLocation as GmsPublishLocationType) !==
updateUserInfosArgs.gmsPublishLocation
) {
return true
}
if (
updateUserInfosArgs.gmsPublishName &&
orgUser.gmsPublishName !== updateUserInfosArgs.gmsPublishName
(orgUser.gmsPublishName as PublishNameType) !== updateUserInfosArgs.gmsPublishName
) {
return true
}

View File

@ -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

View File

@ -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
}

View File

@ -38,7 +38,7 @@ export abstract class AbstractUnconfirmedContributionRole {
throw new LogError('Month of contribution can not be changed')
}
if (this.self.contributionStatus === ContributionStatus.CONFIRMED) {
if ((this.self.contributionStatus as ContributionStatus) === ContributionStatus.CONFIRMED) {
throw new LogError('the contribution is already confirmed, cannot be changed anymore')
}

View File

@ -35,9 +35,10 @@ export class UnconfirmedContributionUserRole extends AbstractUnconfirmedContribu
throw new LogError('Can not update contribution of another user', this.self, user.id)
}
// only admins and moderators can update it when status is other than progress or pending
const contributionStatus = this.self.contributionStatus as ContributionStatus
if (
this.self.contributionStatus !== ContributionStatus.IN_PROGRESS &&
this.self.contributionStatus !== ContributionStatus.PENDING
contributionStatus !== ContributionStatus.IN_PROGRESS &&
contributionStatus !== ContributionStatus.PENDING
) {
throw new LogError(
'Contribution can not be updated due to status',

View File

@ -22,7 +22,7 @@ export class UnconfirmedContributionUserAddMessageRole extends AbstractUnconfirm
protected update(): void {
if (
this.self.contributionStatus === ContributionStatus.IN_PROGRESS ||
(this.self.contributionStatus as ContributionStatus) === ContributionStatus.IN_PROGRESS ||
this.self.resubmissionAt !== null
) {
this.self.contributionStatus = ContributionStatus.PENDING
@ -38,9 +38,10 @@ export class UnconfirmedContributionUserAddMessageRole extends AbstractUnconfirm
}
// only admins and moderators can update it when status is other than progress or pending
// but we are in the user add message role.. we are currently not admin or moderator
const contributionStatus = this.self.contributionStatus as ContributionStatus
if (
this.self.contributionStatus !== ContributionStatus.IN_PROGRESS &&
this.self.contributionStatus !== ContributionStatus.PENDING
contributionStatus !== ContributionStatus.IN_PROGRESS &&
contributionStatus !== ContributionStatus.PENDING
) {
throw new LogError(
'Contribution can not be updated due to status',

View File

@ -52,7 +52,8 @@ export const userFactory = async (
if (user.deletedAt) {
dbUser.deletedAt = user.deletedAt
}
if (user.role && (user.role === RoleNames.ADMIN || user.role === RoleNames.MODERATOR)) {
const userRole = user.role as RoleNames
if (userRole && (userRole === RoleNames.ADMIN || userRole === RoleNames.MODERATOR)) {
await setUserRole(dbUser, user.role)
}
await dbUser.save()

File diff suppressed because it is too large Load Diff

View File

@ -1 +1 @@
v18.7.0
v18.20.7

View File

@ -16,7 +16,7 @@
"devDependencies": {
"@types/joi": "^17.2.3",
"@types/node": "^17.0.21",
"typescript": "^4.3.5",
"typescript": "^4.9.5",
"mkdirp": "^3.0.1",
"ncp": "^2.0.0"
},

View File

@ -117,6 +117,11 @@ export const HUMHUB_ACTIVE = Joi.boolean()
.default(false)
.required()
export const HUMHUB_API_URL = Joi.string()
.uri({ scheme: ['http', 'https'] })
.when('HUMHUB_ACTIVE', { is: true, then: Joi.required(), otherwise: Joi.optional() })
.description('The API URL for HumHub integration')
export const LOG_LEVEL = Joi.string()
.valid('all', 'mark', 'trace', 'debug', 'info', 'warn', 'error', 'fatal', 'off')
.description('set log level')

View File

@ -64,7 +64,7 @@ ncp@^2.0.0:
resolved "https://registry.yarnpkg.com/ncp/-/ncp-2.0.0.tgz#195a21d6c46e361d2fb1281ba38b91e9df7bdbb3"
integrity sha512-zIdGUrPRFTUELUvr3Gmc7KZ2Sw/h1PiVM0Af/oHB6zgnV1ikqSfRk+TOufi79aHYCW3NiOXmr1BP5nWbzojLaA==
typescript@^4.3.5:
typescript@^4.9.5:
version "4.9.5"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.9.5.tgz#095979f9bcc0d09da324d58d03ce8f8374cbe65a"
integrity sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==

View File

@ -1,4 +1,4 @@
DB_HOST=localhost
DB_HOST=127.0.0.1
DB_PORT=3306
DB_USER=root
DB_PASSWORD=

View File

@ -1,6 +1,6 @@
CONFIG_VERSION=$DATABASE_CONFIG_VERSION
DB_HOST=localhost
DB_HOST=127.0.0.1
DB_PORT=3306
DB_USER=$DB_USER
DB_PASSWORD=$DB_PASSWORD

View File

@ -1 +1 @@
v18.7.0
v18.20.7

View File

@ -1,7 +1,7 @@
##################################################################################
# BASE ###########################################################################
##################################################################################
FROM node:18.7.0-alpine3.16 as base
FROM node:18.20.7-alpine3.21 as base
# ENVs (available in production aswell, can be overwritten by commandline or env file)
## DOCKER_WORKDIR would be a classical ARG, but that is not multi layer persistent - shame

View File

@ -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
}

View File

@ -0,0 +1 @@
export { ProjectBranding } from './0088-create_project_brandings/ProjectBranding'

View File

@ -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,

View File

@ -1,4 +1,3 @@
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
/* eslint-disable @typescript-eslint/no-explicit-any */

View 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`')
}

View File

@ -1,6 +1,6 @@
{
"name": "gradido-database",
"version": "2.4.5",
"version": "2.5.1",
"description": "Gradido Database Tool to execute database migrations",
"main": "src/index.ts",
"repository": "https://github.com/gradido/gradido/database",
@ -37,8 +37,8 @@
"mkdirp": "^3.0.1",
"ncp": "^2.0.0",
"prettier": "^2.8.7",
"ts-node": "^10.2.1",
"typescript": "^4.3.5"
"ts-node": "^10.9.2",
"typescript": "^4.9.5"
},
"dependencies": {
"@types/uuid": "^8.3.4",

View File

@ -9,17 +9,12 @@
dependencies:
regenerator-runtime "^0.13.11"
"@cspotcode/source-map-consumer@0.8.0":
version "0.8.0"
resolved "https://registry.yarnpkg.com/@cspotcode/source-map-consumer/-/source-map-consumer-0.8.0.tgz#33bf4b7b39c178821606f669bbc447a6a629786b"
integrity sha512-41qniHzTU8yAGbCp04ohlmSrZf8bkf/iJsl3V0dRGsQN/5GFfx+LbCSsCpp2gqrqjTVg/K6O8ycoV35JIwAzAg==
"@cspotcode/source-map-support@0.6.1":
version "0.6.1"
resolved "https://registry.yarnpkg.com/@cspotcode/source-map-support/-/source-map-support-0.6.1.tgz#118511f316e2e87ee4294761868e254d3da47960"
integrity sha512-DX3Z+T5dt1ockmPdobJS/FAsQPW4V4SrWEhD2iYQT2Cb2tQsiMnYxrcUH9By/Z3B+v0S5LMBkQtV/XOBbpLEOg==
"@cspotcode/source-map-support@^0.8.0":
version "0.8.1"
resolved "https://registry.yarnpkg.com/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz#00629c35a688e05a88b1cda684fb9d5e73f000a1"
integrity sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==
dependencies:
"@cspotcode/source-map-consumer" "0.8.0"
"@jridgewell/trace-mapping" "0.3.9"
"@eslint-community/eslint-plugin-eslint-comments@^3.2.1":
version "3.2.1"
@ -80,6 +75,24 @@
resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz#b520529ec21d8e5945a1851dfd1c32e94e39ff45"
integrity sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==
"@jridgewell/resolve-uri@^3.0.3":
version "3.1.2"
resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz#7a0ee601f60f99a20c7c7c5ff0c80388c1189bd6"
integrity sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==
"@jridgewell/sourcemap-codec@^1.4.10":
version "1.5.0"
resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz#3188bcb273a414b0d215fd22a58540b989b9409a"
integrity sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==
"@jridgewell/trace-mapping@0.3.9":
version "0.3.9"
resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz#6534fd5933a53ba7cbf3a17615e273a0d1273ff9"
integrity sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==
dependencies:
"@jridgewell/resolve-uri" "^3.0.3"
"@jridgewell/sourcemap-codec" "^1.4.10"
"@nodelib/fs.scandir@2.1.5":
version "2.1.5"
resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5"
@ -2353,12 +2366,12 @@ ts-mysql-migrate@^1.0.2:
"@types/mysql" "^2.15.8"
mysql "^2.18.1"
ts-node@^10.2.1:
version "10.2.1"
resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.2.1.tgz#4cc93bea0a7aba2179497e65bb08ddfc198b3ab5"
integrity sha512-hCnyOyuGmD5wHleOQX6NIjJtYVIO8bPP8F2acWkB4W06wdlkgyvJtubO/I9NkI88hCFECbsEgoLc0VNkYmcSfw==
ts-node@^10.9.2:
version "10.9.2"
resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.9.2.tgz#70f021c9e185bccdca820e26dc413805c101c71f"
integrity sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==
dependencies:
"@cspotcode/source-map-support" "0.6.1"
"@cspotcode/source-map-support" "^0.8.0"
"@tsconfig/node10" "^1.0.7"
"@tsconfig/node12" "^1.0.7"
"@tsconfig/node14" "^1.0.0"
@ -2369,6 +2382,7 @@ ts-node@^10.2.1:
create-require "^1.1.0"
diff "^4.0.1"
make-error "^1.1.1"
v8-compile-cache-lib "^3.0.1"
yn "3.1.1"
tsconfig-paths@^3.14.1:
@ -2440,10 +2454,10 @@ typeorm@^0.3.16:
uuid "^9.0.0"
yargs "^17.6.2"
typescript@^4.3.5:
version "4.3.5"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.3.5.tgz#4d1c37cc16e893973c45a06886b7113234f119f4"
integrity sha512-DqQgihaQ9cUrskJo9kIyW/+g0Vxsk8cDtZ52a3NGh0YNTfpUSArXSohyUGnvbPazEPLu398C0UxmKSOrPumUzA==
typescript@^4.9.5:
version "4.9.5"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.9.5.tgz#095979f9bcc0d09da324d58d03ce8f8374cbe65a"
integrity sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==
unbox-primitive@^1.0.2:
version "1.0.2"
@ -2482,6 +2496,11 @@ uuid@^9.0.0:
resolved "https://registry.yarnpkg.com/uuid/-/uuid-9.0.0.tgz#592f550650024a38ceb0c562f2f6aa435761efb5"
integrity sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==
v8-compile-cache-lib@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz#6336e8d71965cb3d35a1bbb7868445a7c05264bf"
integrity sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==
which-boxed-primitive@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz#13757bc89b209b049fe5d86430e21cf40a89a8e6"

View File

@ -1,4 +1,12 @@
#!/bin/bash
# helper functions
log_step() {
local message="$1"
echo -e "\e[34m$message\e[0m" > /dev/tty # blue in console
echo "<p style="color:blue">$message</p>" >> "$UPDATE_HTML" # blue in html
}
# check for parameter
if [ -z "$1" ]; then
echo "Usage: Please provide a branch name as the first argument."
@ -75,18 +83,18 @@ TODAY=$(date +"%Y-%m-%d")
exec > >(tee -a $UPDATE_HTML) 2>&1
# configure nginx for the update-page
echo 'Configuring nginx to serve the update-page' >> $UPDATE_HTML
log_step 'Configuring nginx to serve the update-page'
ln -sf $SCRIPT_DIR/nginx/sites-available/update-page.conf $SCRIPT_DIR/nginx/sites-enabled/default
sudo /etc/init.d/nginx restart
# stop all services
echo 'Stop and delete all Gradido services' >> $UPDATE_HTML
log_step "Stop and delete all Gradido services"
pm2 delete all
pm2 save
# git
BRANCH=$1
echo "Starting with git pull - branch:$BRANCH" >> $UPDATE_HTML
log_step "Starting with git pull - branch:$BRANCH"
cd $PROJECT_ROOT
# TODO: this overfetches alot, but ensures we can use start.sh with tags
git fetch --all
@ -99,7 +107,7 @@ export BUILD_COMMIT="$(git rev-parse HEAD)"
# *** set FEDERATION_PORT from FEDERATION_COMMUNITY_APIS and create gradido-federation.conf file
rm -f $NGINX_CONFIG_DIR/gradido.conf.tmp
rm -f $NGINX_CONFIG_DIR/gradido-federation.conf.locations
echo "====================================================================================================" >> $UPDATE_HTML
log_step "===================================================================================================="
IFS="," read -a API_ARRAY <<< $FEDERATION_COMMUNITY_APIS
for api in "${API_ARRAY[@]}"
do
@ -109,18 +117,18 @@ do
FEDERATION_PORT=${FEDERATION_COMMUNITY_API_PORT:-5000}
FEDERATION_PORT=$(($FEDERATION_PORT + $port))
export FEDERATION_PORT
echo "create ngingx config: location /api/$FEDERATION_APIVERSION to http://127.0.0.1:$FEDERATION_PORT" >> $UPDATE_HTML
log_step "create ngingx config: location /api/$FEDERATION_APIVERSION to http://127.0.0.1:$FEDERATION_PORT"
envsubst '$FEDERATION_APIVERSION, $FEDERATION_PORT' < $NGINX_CONFIG_DIR/gradido-federation.conf.template >> $NGINX_CONFIG_DIR/gradido-federation.conf.locations
done
unset FEDERATION_APIVERSION
unset FEDERATION_PORT
echo "====================================================================================================" >> $UPDATE_HTML
log_step "===================================================================================================="
# *** 2nd read gradido-federation.conf file in env variable to be replaced in 3rd step
export FEDERATION_NGINX_CONF=$(< $NGINX_CONFIG_DIR/gradido-federation.conf.locations)
# *** 3rd generate gradido nginx config including federation modules per api-version
echo 'Generate new gradido nginx config' >> $UPDATE_HTML
log_step 'Generate new gradido nginx config'
case "$URL_PROTOCOL" in
'https') TEMPLATE_FILE="gradido.conf.ssl.template" ;;
*) TEMPLATE_FILE="gradido.conf.template" ;;
@ -132,18 +140,20 @@ rm $NGINX_CONFIG_DIR/gradido.conf.tmp
rm $NGINX_CONFIG_DIR/gradido-federation.conf.locations
# Generate update-page.conf from template
echo 'Generate new update-page nginx config' >> $UPDATE_HTML
log_step 'Generate new update-page nginx config'
case "$URL_PROTOCOL" in
'https') TEMPLATE_FILE="update-page.conf.ssl.template" ;;
*) TEMPLATE_FILE="update-page.conf.template" ;;
esac
envsubst "$(env | sed -e 's/=.*//' -e 's/^/\$/g')" < $NGINX_CONFIG_DIR/$TEMPLATE_FILE > $NGINX_CONFIG_DIR/update-page.conf
log_step 'Clean tmp and yarn cache'
# Clean tmp folder - remove yarn files
find /tmp -name "yarn--*" -exec rm -r {} \;
# Clean user cache folder
rm -Rf ~/.cache/yarn
log_step 'Remove all node_modules and build folders'
# Remove node_modules folders
# we had problems with corrupted node_modules folder
rm -Rf $PROJECT_ROOT/database/node_modules
@ -164,6 +174,7 @@ rm -Rf $PROJECT_ROOT/admin/build
rm -Rf $PROJECT_ROOT/dht-node/build
rm -Rf $PROJECT_ROOT/federation/build
log_step 'Regenerate .env files'
# Regenerate .env files
cp -f $PROJECT_ROOT/database/.env $PROJECT_ROOT/database/.env.bak
cp -f $PROJECT_ROOT/backend/.env $PROJECT_ROOT/backend/.env.bak
@ -179,7 +190,7 @@ envsubst "$(env | sed -e 's/=.*//' -e 's/^/\$/g')" < $PROJECT_ROOT/dht-node/.env
envsubst "$(env | sed -e 's/=.*//' -e 's/^/\$/g')" < $PROJECT_ROOT/federation/.env.template > $PROJECT_ROOT/federation/.env
# Install & build database
echo 'Updating database' >> $UPDATE_HTML
log_step 'Updating database'
cd $PROJECT_ROOT/database
yarn install
yarn build
@ -191,13 +202,13 @@ else
fi
# Install & build config
echo 'Updating config' >> $UPDATE_HTML
log_step 'Updating config'
cd $PROJECT_ROOT/config
yarn install
yarn build
# Install & build backend
echo 'Updating backend' >> $UPDATE_HTML
log_step 'Updating backend'
cd $PROJECT_ROOT/backend
# TODO maybe handle this differently?
unset NODE_ENV
@ -211,35 +222,27 @@ export NODE_ENV=production
# Install & build frontend
echo 'Updating frontend' >> $UPDATE_HTML
log_step 'Updating frontend'
cd $PROJECT_ROOT/frontend
# TODO maybe handle this differently?
unset NODE_ENV
nvm use
nvm install
npm i -g yarn
yarn install
yarn build
# TODO maybe handle this differently?
export NODE_ENV=production
# Install & build admin
echo 'Updating admin' >> $UPDATE_HTML
log_step 'Updating admin'
cd $PROJECT_ROOT/admin
# TODO maybe handle this differently?
unset NODE_ENV
nvm use
nvm install
npm i -g yarn
yarn install
yarn build
# TODO maybe handle this differently?
export NODE_ENV=production
nvm use default
# Install & build dht-node
echo 'Updating dht-node' >> $UPDATE_HTML
log_step 'Updating dht-node'
cd $PROJECT_ROOT/dht-node
# TODO maybe handle this differently?
unset NODE_ENV
@ -249,7 +252,7 @@ yarn build
export NODE_ENV=production
# Install & build federation
echo 'Updating federation' >> $UPDATE_HTML
log_step 'Updating federation'
cd $PROJECT_ROOT/federation
# TODO maybe handle this differently?
unset NODE_ENV
@ -268,34 +271,34 @@ if [ ! -z $FEDERATION_DHT_TOPIC ]; then
pm2 start --name gradido-dht-node "yarn --cwd $PROJECT_ROOT/dht-node start" -l $GRADIDO_LOG_PATH/pm2.dht-node.$TODAY.log --log-date-format 'YYYY-MM-DD HH:mm:ss.SSS'
pm2 save
else
echo "=====================================================================" >> $UPDATE_HTML
echo "WARNING: FEDERATION_DHT_TOPIC not configured. DHT-Node not started..." >> $UPDATE_HTML
echo "=====================================================================" >> $UPDATE_HTML
fi
log_step "====================================================================="
log_step "WARNING: FEDERATION_DHT_TOPIC not configured. DHT-Node not started..."
log_step "====================================================================="
fi
# set FEDERATION_PORT from FEDERATION_COMMUNITY_APIS
IFS="," read -a API_ARRAY <<< $FEDERATION_COMMUNITY_APIS
for api in "${API_ARRAY[@]}"
do
export FEDERATION_API=$api
echo "FEDERATION_API=$FEDERATION_API" >> $UPDATE_HTML
log_step "FEDERATION_API=$FEDERATION_API"
export MODULENAME=gradido-federation-$api
echo "MODULENAME=$MODULENAME" >> $UPDATE_HTML
log_step "MODULENAME=$MODULENAME"
# calculate port by remove '_' and add value of api to baseport
port=${api//_/}
FEDERATION_PORT=${FEDERATION_COMMUNITY_API_PORT:-5000}
FEDERATION_PORT=$(($FEDERATION_PORT + $port))
export FEDERATION_PORT
echo "====================================================" >> $UPDATE_HTML
echo " start $MODULENAME listening on port=$FEDERATION_PORT" >> $UPDATE_HTML
echo "====================================================" >> $UPDATE_HTML
log_step "===================================================="
log_step " start $MODULENAME listening on port=$FEDERATION_PORT"
log_step "===================================================="
# pm2 delete $MODULENAME
pm2 start --name $MODULENAME "yarn --cwd $PROJECT_ROOT/federation start" -l $GRADIDO_LOG_PATH/pm2.$MODULENAME.$TODAY.log --log-date-format 'YYYY-MM-DD HH:mm:ss.SSS'
pm2 save
done
# let nginx showing gradido
echo 'Configuring nginx to serve gradido again' >> $UPDATE_HTML
log_step 'Configuring nginx to serve gradido again'
ln -sf $SCRIPT_DIR/nginx/sites-available/gradido.conf $SCRIPT_DIR/nginx/sites-enabled/default
sudo /etc/init.d/nginx restart

View File

@ -124,11 +124,11 @@ sudo systemctl daemon-reload
# setup https with certbot
certbot certonly --nginx --non-interactive --agree-tos --domains $COMMUNITY_HOST --email $COMMUNITY_SUPPORT_MAIL
# Install node 16. with nvm, with nodesource is depracted
# Install node 18
sudo -u gradido bash -c 'curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash'
# Close and reopen your terminal to start using nvm or run the following to use it now:
sudo -u gradido bash -c 'export NVM_DIR="$HOME/.nvm" && [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"'
sudo -u gradido bash -c '. $HOME/.nvm/nvm.sh && nvm install 16' # first installed version will be set to default automatic
sudo -u gradido bash -c '. $HOME/.nvm/nvm.sh && nvm install v18.20.7' # first installed version will be set to default automatic
# Install yarn
sudo -u gradido bash -c '. $HOME/.nvm/nvm.sh && npm i -g yarn'

View File

@ -0,0 +1,15 @@
## Migrate from Gradido Version 2.5.1 to 2.5.2
### What
We need to upgrade the used nodejs version in deployment. Until now for pm2, backend, dht-node, federation nodejs 16 was used,
but some newer npm libs don't work with this old nodejs version so we upgrade to nodejs 18.20.7
### What you can do now
You need to only run this [upgradeNodeJs.sh](upgradeNodeJs.sh) with `release-2_5_2-beta` as parameter
```bash
cd /home/gradido/gradido/deployment/hetzner_cloud/migration/2_5_1-2_5_2
sudo ./upgradeNodeJs.sh `release-2_5_2-beta`
```
It will stop pm2, install new nodejs version + pm2 + yarn, remove nodejs 16.
Then it will call start.sh with first parameter of ./upgradeNodeJs.sh as his first parameter

View File

@ -0,0 +1,20 @@
#!/bin/bash
# check for parameter
if [ -z "$1" ]; then
echo "Usage: Please provide a branch name as the first argument."
exit 1
fi
# stop all services
pm2 delete all
pm2 save
# upgrade node js version
nvm install 18.20.7
nvm use 18.20.7
nvm alias default 18.20.7
npm install -g pm2 yarn
nvm uninstall 16
# Start gradido
sudo -u gradido $SCRIPT_PATH/start.sh $1

View File

@ -1,5 +1,5 @@
# Database
DB_HOST=localhost
DB_HOST=127.0.0.1
DB_PORT=3306
DB_USER=root
DB_PASSWORD=

View File

@ -2,7 +2,7 @@
CONFIG_VERSION=$FEDERATION_DHT_CONFIG_VERSION
# Database
DB_HOST=localhost
DB_HOST=127.0.0.1
DB_PORT=3306
DB_USER=$DB_USER
DB_PASSWORD=$DB_PASSWORD

View File

@ -1 +1 @@
v19.5.0
v18.20.7

View File

@ -1,7 +1,7 @@
##################################################################################
# BASE ###########################################################################
##################################################################################
FROM node:19.5.0-alpine3.17 as base
FROM node:18.20.7-alpine3.21 as base
#FROM ubuntu:latest as base
# ENVs (available in production aswell, can be overwritten by commandline or env file)
@ -33,10 +33,10 @@ LABEL maintainer="support@gradido.net"
# Install Additional Software
## install: sodium requirements
RUN apk add --no-cache --virtual build-deps python3 alpine-sdk autoconf libtool automake && \
mkdir -p /prebuilds && cd /prebuilds && npm init -y && npm install sodium-native@3.1.1 && \
apk del build-deps
ENV SODIUM_NATIVE_PREBUILD=/prebuilds/node_modules/sodium-native/
#RUN apk add --no-cache --virtual build-deps python3 alpine-sdk autoconf libtool automake && \
# mkdir -p /prebuilds && cd /prebuilds && npm init -y && npm install sodium-native@3.1.1 && \
# apk del build-deps
#ENV SODIUM_NATIVE_PREBUILD=/prebuilds/node_modules/sodium-native/
# Settings
## Expose Container Port

View File

@ -1,6 +1,6 @@
{
"name": "gradido-dht-node",
"version": "2.4.5",
"version": "2.5.1",
"description": "Gradido dht-node module",
"main": "src/index.ts",
"repository": "https://github.com/gradido/gradido/",
@ -49,7 +49,7 @@
"jest": "^27.2.4",
"prettier": "^2.8.7",
"ts-jest": "^27.0.5",
"ts-node": "^10.9.1",
"ts-node": "^10.9.2",
"typescript": "^4.9.4"
},
"engines": {

View File

@ -2435,7 +2435,7 @@ graceful-fs@^4.2.4:
joi "^17.13.3"
"gradido-database@file:../database":
version "2.4.4"
version "2.5.1"
dependencies:
"@types/uuid" "^8.3.4"
cross-env "^7.0.3"
@ -4667,10 +4667,10 @@ ts-mysql-migrate@^1.0.2:
"@types/mysql" "^2.15.8"
mysql "^2.18.1"
ts-node@^10.9.1:
version "10.9.1"
resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.9.1.tgz#e73de9102958af9e1f0b168a6ff320e25adcff4b"
integrity sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==
ts-node@^10.9.2:
version "10.9.2"
resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.9.2.tgz#70f021c9e185bccdca820e26dc413805c101c71f"
integrity sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==
dependencies:
"@cspotcode/source-map-support" "^0.8.0"
"@tsconfig/node10" "^1.0.7"

View File

@ -1,6 +1,6 @@
{
"name": "gradido-dlt-connector",
"version": "2.4.5",
"version": "2.5.1",
"description": "Gradido DLT-Connector",
"main": "src/index.ts",
"repository": "https://github.com/gradido/gradido/",

View File

@ -67,7 +67,7 @@ services:
## MARIADB ##############################################
#########################################################
mariadb:
image: mariadb:10.5
image: mariadb:10.11.6
environment:
- MARIADB_ALLOW_EMPTY_ROOT_PASSWORD=1
- MARIADB_USER=root

Some files were not shown because too many files have changed in this diff Show More