From 1fdbee906c395cdf7c2e8ba5c045c872c474c66d Mon Sep 17 00:00:00 2001 From: einhornimmond Date: Tue, 18 Feb 2025 16:49:50 +0100 Subject: [PATCH] forward to spaceurl after login with project query parameter --- .../ProjectBranding/ListHumhubSpaces.vue | 23 ++++++------ .../ProjectBranding/ProjectBrandingForm.vue | 13 ++++--- .../ProjectBranding/ProjectBrandingItem.vue | 16 ++++++-- admin/src/locales/de.json | 3 ++ admin/src/locales/en.json | 3 ++ admin/src/pages/ProjectBranding.vue | 1 + backend/src/apis/humhub/HumHubClient.ts | 37 ++++++++++++++++--- backend/src/graphql/arg/CreateUserArgs.ts | 4 ++ .../src/graphql/input/ProjectBrandingInput.ts | 6 +++ backend/src/graphql/model/ProjectBranding.ts | 4 +- .../resolver/ProjectBrandingResolver.ts | 2 +- backend/src/graphql/resolver/UserResolver.ts | 21 +++++++++-- .../src/graphql/resolver/util/syncHumhub.ts | 14 ++++++- .../ProjectBranding.ts | 3 ++ .../0088-create_project_brandings.ts | 3 +- frontend/src/graphql/queries.js | 5 +++ frontend/src/layouts/AuthLayout.vue | 26 +++++++++---- frontend/src/pages/Login.vue | 12 +++++- frontend/src/pages/Register.vue | 9 ++--- frontend/src/store/store.js | 5 +++ 20 files changed, 165 insertions(+), 45 deletions(-) diff --git a/admin/src/components/ProjectBranding/ListHumhubSpaces.vue b/admin/src/components/ProjectBranding/ListHumhubSpaces.vue index f1e027b5a..6412006f7 100644 --- a/admin/src/components/ProjectBranding/ListHumhubSpaces.vue +++ b/admin/src/components/ProjectBranding/ListHumhubSpaces.vue @@ -14,7 +14,7 @@ 'cursor-pointer', { active: space.id === selectedSpaceId }, ]" - @click="selectedSpaceId = space.id" + @click="chooseSpace(space)" >
@@ -37,7 +37,7 @@ diff --git a/admin/src/components/ProjectBranding/ProjectBrandingForm.vue b/admin/src/components/ProjectBranding/ProjectBrandingForm.vue index c3ac7d6b0..e412e55f3 100644 --- a/admin/src/components/ProjectBranding/ProjectBrandingForm.vue +++ b/admin/src/components/ProjectBranding/ProjectBrandingForm.vue @@ -67,10 +67,7 @@
- + @@ -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, }) diff --git a/admin/src/components/ProjectBranding/ProjectBrandingItem.vue b/admin/src/components/ProjectBranding/ProjectBrandingItem.vue index b94aa19cc..265ccf597 100644 --- a/admin/src/components/ProjectBranding/ProjectBrandingItem.vue +++ b/admin/src/components/ProjectBranding/ProjectBrandingItem.vue @@ -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` diff --git a/admin/src/locales/de.json b/admin/src/locales/de.json index 3fd75c805..5118d2acb 100644 --- a/admin/src/locales/de.json +++ b/admin/src/locales/de.json @@ -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" }, diff --git a/admin/src/locales/en.json b/admin/src/locales/en.json index 1c7f1da97..c4681084e 100644 --- a/admin/src/locales/en.json +++ b/admin/src/locales/en.json @@ -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" }, diff --git a/admin/src/pages/ProjectBranding.vue b/admin/src/pages/ProjectBranding.vue index 2718ae478..684eaa156 100644 --- a/admin/src/pages/ProjectBranding.vue +++ b/admin/src/pages/ProjectBranding.vue @@ -99,6 +99,7 @@ function createEntry() { alias: '', description: undefined, spaceId: undefined, + spaceUrl: undefined, newUserToSpace: false, logoUrl: undefined, }) diff --git a/backend/src/apis/humhub/HumHubClient.ts b/backend/src/apis/humhub/HumHubClient.ts index 4cbdae481..dbde41feb 100644 --- a/backend/src/apis/humhub/HumHubClient.ts +++ b/backend/src/apis/humhub/HumHubClient.ts @@ -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 | 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 { const options = await this.createRequestOptions() const response = await this.restClient.get(`/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 { + 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) + } + } +} diff --git a/backend/src/graphql/arg/CreateUserArgs.ts b/backend/src/graphql/arg/CreateUserArgs.ts index 28105559d..d7182d81b 100644 --- a/backend/src/graphql/arg/CreateUserArgs.ts +++ b/backend/src/graphql/arg/CreateUserArgs.ts @@ -30,4 +30,8 @@ export class CreateUserArgs { @Field(() => String, { nullable: true }) @IsString() redeemCode?: string | null + + @Field(() => String, { nullable: true }) + @IsString() + project?: string | null } diff --git a/backend/src/graphql/input/ProjectBrandingInput.ts b/backend/src/graphql/input/ProjectBrandingInput.ts index cf5459b49..16585c858 100644 --- a/backend/src/graphql/input/ProjectBrandingInput.ts +++ b/backend/src/graphql/input/ProjectBrandingInput.ts @@ -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 diff --git a/backend/src/graphql/model/ProjectBranding.ts b/backend/src/graphql/model/ProjectBranding.ts index 8db26efe5..4bbddbcf4 100644 --- a/backend/src/graphql/model/ProjectBranding.ts +++ b/backend/src/graphql/model/ProjectBranding.ts @@ -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 diff --git a/backend/src/graphql/resolver/ProjectBrandingResolver.ts b/backend/src/graphql/resolver/ProjectBrandingResolver.ts index ab1804d3b..11361ab5c 100644 --- a/backend/src/graphql/resolver/ProjectBrandingResolver.ts +++ b/backend/src/graphql/resolver/ProjectBrandingResolver.ts @@ -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' diff --git a/backend/src/graphql/resolver/UserResolver.ts b/backend/src/graphql/resolver/UserResolver.ts index 93d5777f8..a69a970c9 100644 --- a/backend/src/graphql/resolver/UserResolver.ts +++ b/backend/src/graphql/resolver/UserResolver.ts @@ -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 { 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 { + async authenticateHumhubAutoLogin( + @Ctx() context: Context, + @Arg('project', () => String, { nullable: true }) project?: string | null, + ): Promise { 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]) diff --git a/backend/src/graphql/resolver/util/syncHumhub.ts b/backend/src/graphql/resolver/util/syncHumhub.ts index c229df655..d08e6114e 100644 --- a/backend/src/graphql/resolver/util/syncHumhub.ts +++ b/backend/src/graphql/resolver/util/syncHumhub.ts @@ -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 { + spaceId?: number | null, +): Promise { // 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 } diff --git a/database/entity/0088-create_project_brandings/ProjectBranding.ts b/database/entity/0088-create_project_brandings/ProjectBranding.ts index 4757904a3..449dc62ee 100644 --- a/database/entity/0088-create_project_brandings/ProjectBranding.ts +++ b/database/entity/0088-create_project_brandings/ProjectBranding.ts @@ -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 diff --git a/database/migrations/0088-create_project_brandings.ts b/database/migrations/0088-create_project_brandings.ts index a741b4fed..9a0c56dc2 100644 --- a/database/migrations/0088-create_project_brandings.ts +++ b/database/migrations/0088-create_project_brandings.ts @@ -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; `) diff --git a/frontend/src/graphql/queries.js b/frontend/src/graphql/queries.js index 0d4858e98..b1ae7bed8 100644 --- a/frontend/src/graphql/queries.js +++ b/frontend/src/graphql/queries.js @@ -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) { diff --git a/frontend/src/layouts/AuthLayout.vue b/frontend/src/layouts/AuthLayout.vue index f3b6f1bee..01edd3522 100644 --- a/frontend/src/layouts/AuthLayout.vue +++ b/frontend/src/layouts/AuthLayout.vue @@ -26,6 +26,7 @@