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 @@
- updateField(value, 'spaceId')"
- />
+
@@ -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 @@