forward to spaceurl after login with project query parameter

This commit is contained in:
einhornimmond 2025-02-18 16:49:50 +01:00
parent 8ea8201967
commit 1fdbee906c
20 changed files with 165 additions and 45 deletions

View File

@ -14,7 +14,7 @@
'cursor-pointer',
{ active: space.id === selectedSpaceId },
]"
@click="selectedSpaceId = space.id"
@click="chooseSpace(space)"
>
<div>
<input v-model="selectedSpaceId" type="radio" :value="space.id" class="me-2" />
@ -37,7 +37,7 @@
</template>
<script setup>
import { ref, onMounted, computed, watch } from 'vue'
import { ref, onMounted, computed } from 'vue'
import { useQuery } from '@vue/apollo-composable'
import gql from 'graphql-tag'
@ -48,7 +48,11 @@ const props = defineProps({
},
})
const emit = defineEmits(['update:modelValue'])
const emit = defineEmits(['chooseSpace'])
function chooseSpace(space) {
selectedSpaceId.value = space.id
emit('chooseSpace', space)
}
const ITEMS_PER_PAGE = 20
const page = ref(1)
@ -74,16 +78,13 @@ const { result, refetch } = useQuery(
const spaces = computed(() => result.value?.spaces?.results || [])
watch(
() => selectedSpaceId.value,
(newValue) => emit('update:modelValue', newValue),
)
onMounted(() => {
if (props.modelValue) {
const targetPage = Math.ceil(props.modelValue / ITEMS_PER_PAGE)
page.value = targetPage
refetch({ page: targetPage })
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>

View File

@ -67,10 +67,7 @@
</div>
</BForm>
<BModal v-model="isModalVisible" title="Select Space" hide-footer>
<ListHumhubSpaces
:model-value="spaceId"
@update:model-value="(value) => updateField(value, 'spaceId')"
/>
<ListHumhubSpaces :model-value="spaceId" @choose-space="chooseSpace" />
</BModal>
</div>
</template>
@ -136,13 +133,18 @@ const validationSchema = object({
.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
console.log('updateField called with', { value, name })
}
const emit = defineEmits(['update:modelValue'])
function submit() {
@ -163,6 +165,7 @@ function resetForm() {
alias: '',
description: undefined,
spaceId: undefined,
spaceUrl: undefined,
newUserToSpace: false,
logoUrl: undefined,
})

View File

@ -87,7 +87,6 @@ function toggleDetails() {
}
function update(form) {
details.value = false
const { mutate } = useMutation(gql`
mutation upsertProjectBranding($input: ProjectBrandingInput!) {
upsertProjectBranding(input: $input) {
@ -96,6 +95,7 @@ function update(form) {
alias
description
spaceId
spaceUrl
newUserToSpace
logoUrl
}
@ -104,9 +104,19 @@ function update(form) {
mutate({
input: { ...form },
}).then(({ data }) => {
emit('update:item', data.upsertProjectBranding)
})
.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(gql`

View File

@ -210,11 +210,14 @@
"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"
},

View File

@ -211,11 +211,14 @@
"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"
},

View File

@ -99,6 +99,7 @@ function createEntry() {
alias: '',
description: undefined,
spaceId: undefined,
spaceUrl: undefined,
newUserToSpace: false,
logoUrl: undefined,
})

View File

@ -11,6 +11,7 @@ import { PostUser } from './model/PostUser'
import { SpacesResponse } from './model/SpacesResponse'
import { UsersResponse } from './model/UsersResponse'
import { Space } from './model/Space'
import { ProjectBranding } from '@entity/ProjectBranding'
/**
* HumHubClient as singleton class
@ -63,16 +64,31 @@ 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`)
let projectBrandingPromise: Promise<ProjectBranding | null> | undefined
if (project) {
projectBrandingPromise = ProjectBranding.findOne({
where: { alias: project },
select: { spaceUrl: true },
})
}
const token = await new SignJWT({ username })
.setProtectedHeader({ alg: 'HS256' })
.setIssuedAt()
.setExpirationTime(CONFIG.JWT_EXPIRES_IN)
.sign(secret)
return `${CONFIG.HUMHUB_API_URL}user/auth/external?authclient=jwt&jwt=${token}`
let loginUrl = `${CONFIG.HUMHUB_API_URL}user/auth/external?authclient=jwt&jwt=${token}`
if (projectBrandingPromise) {
const projectBranding = await projectBrandingPromise
if (projectBranding?.spaceUrl) {
loginUrl += `&redirect=${projectBranding.spaceUrl as string}`
}
}
return loginUrl
}
/**
@ -205,12 +221,23 @@ export class HumHubClient {
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)
console.log(response)
if (response.statusCode !== 200) {
throw new LogError('error requesting space from humhub', response)
}
return response.result
}
}
// new RestClient('gradido', 'api/v1/')
// 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

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

@ -26,6 +26,12 @@ export class ProjectBrandingInput {
@IsOptional()
spaceId: number | null | undefined
@Field(() => String, { nullable: true })
@IsOptional()
@IsString()
@IsUrl()
spaceUrl: string | null | undefined
@Field(() => Boolean)
@IsBoolean()
newUserToSpace: boolean

View File

@ -24,8 +24,8 @@ export class ProjectBranding {
@Field(() => Int, { nullable: true })
spaceId: number | null
@Field(() => Space, { nullable: true })
space: Space | null
@Field(() => String, { nullable: true })
spaceUrl: string | null
@Field(() => Boolean)
newUserToSpace: boolean

View File

@ -1,5 +1,5 @@
import { ProjectBranding as DbProjectBranding } from '@entity/ProjectBranding'
import { Resolver, Query, Mutation, Arg, Int, Authorized, ID, FieldResolver, Root } from 'type-graphql'
import { Resolver, Query, Mutation, Arg, Int, Authorized, ID } from 'type-graphql'
import { ProjectBrandingInput } from '@input/ProjectBrandingInput'
import { ProjectBranding } from '@model/ProjectBranding'

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'
@ -249,6 +250,7 @@ export class UserResolver {
language,
publisherId = null,
redeemCode = null,
project = null,
}: CreateUserArgs,
): Promise<User> {
logger.addContext('user', 'unknown')
@ -397,7 +399,17 @@ export class UserResolver {
}
logger.info('createUser() successful...')
if (CONFIG.HUMHUB_ACTIVE) {
void syncHumhub(null, dbUser)
let spaceId: number | null = null
if (project) {
const projectBranding = await ProjectBranding.findOne({
where: { alias: project, newUserToSpace: true },
select: { spaceId: true },
})
if (projectBranding) {
spaceId = projectBranding.spaceId
}
}
void syncHumhub(null, dbUser, spaceId)
}
if (redeemCode) {
@ -771,7 +783,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 +805,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

@ -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) {
logger.debug(`add user to space ${spaceId}`)
await humhubClient.addUserToSpace(humhubUser.id, spaceId)
}
return user.id
}

View File

@ -17,6 +17,9 @@ export class ProjectBranding extends BaseEntity {
@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

View File

@ -13,8 +13,9 @@ export async function upgrade(queryFn: (query: string, values?: any[]) => Promis
\`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,
\`logo_url\` VARCHAR(255) NULL DEFAULT NULL,
PRIMARY KEY(\`id\`)
) ENGINE = InnoDB;
`)

View File

@ -42,6 +42,11 @@ export const authenticateHumhubAutoLogin = gql`
authenticateHumhubAutoLogin
}
`
export const authenticateHumhubAutoLoginProject = gql`
query ($project: String!) {
authenticateHumhubAutoLogin(project: $project)
}
`
export const transactionsQuery = gql`
query ($currentPage: Int = 1, $pageSize: Int = 25, $order: Order = DESC) {

View File

@ -26,6 +26,7 @@
<BRow v-if="projectBannerResult || projectBannerLoading" class="d-none d-md-block">
<BCol>
<BImg
v-if="projectBannerResult"
:src="projectBannerResult.projectBrandingBanner"
class="img-fluid"
alt="project banner"
@ -95,7 +96,7 @@
</template>
<script setup>
import { ref, computed } from 'vue'
import { onMounted, computed } from 'vue'
import { useQuery } from '@vue/apollo-composable'
import AuthNavbar from '@/components/Auth/AuthNavbar'
import AuthNavbarSmall from '@/components/Auth/AuthNavbarSmall'
@ -103,28 +104,39 @@ import AuthCarousel from '@/components/Auth/AuthCarousel'
import LanguageSwitch2 from '@/components/LanguageSwitch2'
import AuthFooter from '@/components/Auth/AuthFooter'
import CONFIG from '@/config'
import { useStore } from 'vuex'
import gql from 'graphql-tag'
const communityName = CONFIG.COMMUNITY_NAME
const store = useStore()
const setTextSize = (size) => {
document.querySelector('.page-font-size').style.fontSize = size + 'rem'
}
const project = computed(() => {
const urlParams = new URLSearchParams(window.location.search)
return urlParams.get('project')
})
const project = computed(() => store.state.project)
const { result: projectBannerResult, loading: projectBannerLoading } = useQuery(
const {
result: projectBannerResult,
loading: projectBannerLoading,
refetch,
} = useQuery(
gql`
query ($project: String!) {
projectBrandingBanner(alias: $project)
}
`,
{ project },
{ enabled: !!project.value },
)
onMounted(async () => {
const urlParams = new URLSearchParams(window.location.search)
const projectValue = urlParams.get('project')
if (projectValue) {
store.commit('project', projectValue)
} else {
store.commit('project', '')
}
})
</script>
<style lang="scss" scoped>

View File

@ -50,12 +50,13 @@ import InputPassword from '@/components/Inputs/InputPassword'
import InputEmail from '@/components/Inputs/InputEmail'
import Message from '@/components/Message/Message'
import { login } from '@/graphql/mutations'
import { authenticateHumhubAutoLoginProject } from '@/graphql/queries'
import { ref, computed } from 'vue'
import { useStore } from 'vuex'
import { useI18n } from 'vue-i18n'
import { useRouter, useRoute } from 'vue-router'
import { useForm } from 'vee-validate'
import { useMutation } from '@vue/apollo-composable'
import { useMutation, useLazyQuery } from '@vue/apollo-composable'
import { useAppToast } from '@/composables/useToast'
// import { useLoading } from 'vue-loading-overlay'
@ -64,6 +65,7 @@ const route = useRoute()
const store = useStore()
const { t } = useI18n()
const { mutate } = useMutation(login)
const { load } = useLazyQuery(authenticateHumhubAutoLoginProject)
// const $loading = useLoading() // TODO needs to be updated but there is some sort of an issue that breaks the app.
const { toastError } = useAppToast()
@ -97,6 +99,14 @@ const onSubmit = handleSubmit(async (values) => {
store.commit('email', values.email)
// await loader.hide()
if (store.state.project) {
const result = await load(authenticateHumhubAutoLoginProject, {
project: store.state.project,
})
window.location.href = result.authenticateHumhubAutoLogin
return
}
if (route.params.code) {
await router.push(`/redeem/${route.params.code}`)
} else {

View File

@ -96,19 +96,19 @@ const { mutate } = useMutation(createUser)
const { values: formValues, meta: formMeta, defineField, handleSubmit } = useForm()
const [firstname, firstnameProps] = defineField('firstname')
const [firstname] = defineField('firstname')
const { meta: firstnameMeta, errorMessage: firstnameError } = useField('firstname', {
required: true,
min: 3,
})
const [lastname, lastnameProps] = defineField('lastname')
const [lastname] = defineField('lastname')
const { meta: lastnameMeta, errorMessage: lastnameError } = useField('lastname', {
required: true,
min: 2,
})
const [agree, agreeProps] = defineField('agree')
const [agree] = defineField('agree')
const { meta: agreeMeta } = useField('agree', 'required')
const { t } = useI18n()
@ -116,10 +116,8 @@ const store = useStore()
const { params } = useRoute()
const showPageMessage = ref(false)
const submitted = ref(false)
const publisherId = ref(store.state.publisherId)
const redeemCode = ref(params.code)
const CONFIG = window.config
const enterData = computed(() => {
return !showPageMessage.value
@ -134,6 +132,7 @@ async function onSubmit() {
language: store.state.language,
publisherId: publisherId.value,
redeemCode: redeemCode.value,
project: store.state.project,
})
showPageMessage.value = true
} catch (error) {

View File

@ -49,6 +49,9 @@ export const mutations = {
gmsPublishLocation: (state, gmsPublishLocation) => {
state.gmsPublishLocation = gmsPublishLocation
},
project: (state, project) => {
state.project = project
},
publisherId: (state, publisherId) => {
let pubId = parseInt(publisherId)
if (isNaN(pubId)) pubId = null
@ -113,6 +116,7 @@ export const actions = {
commit('humhubPublishName', null)
commit('gmsPublishLocation', null)
commit('hasElopage', false)
commit('project', null)
commit('publisherId', null)
commit('roles', null)
commit('hideAmountGDD', false)
@ -153,6 +157,7 @@ try {
humhubPublishName: null,
gmsPublishLocation: null,
hasElopage: false,
project: null,
publisherId: null,
hideAmountGDD: null,
hideAmountGDT: null,