implement humhub export user function

This commit is contained in:
einhornimmond 2024-04-04 16:06:59 +02:00
parent a3cc076e47
commit 50aaf0ea89
18 changed files with 513 additions and 2 deletions

View File

@ -70,3 +70,8 @@ FEDERATION_XCOM_SENDCOINS_ENABLED=false
# Coordinates of Illuminz test instance
#GMS_URL=http://54.176.169.179:3071
GMS_URL=http://localhost:4044/
# HUMHUB
HUMHUB_ACTIVE=false
#HUMHUB_API_URL=https://community.gradido.net/
#HUMHUB_JWT_KEY=

View File

@ -67,3 +67,8 @@ FEDERATION_XCOM_SENDCOINS_ENABLED=$FEDERATION_XCOM_SENDCOINS_ENABLED
# GMS
GMS_ACTIVE=$GMS_ACTIVE
GMS_URL=$GMS_URL
# HUMHUB
HUMHUB_ACTIVE=$HUMHUB_ACTIVE
HUMHUB_API_URL=$HUMHUB_API_URL
HUMHUB_JWT_KEY=$HUMHUB_JWT_KEY

View File

@ -18,6 +18,7 @@
"klicktipp": "cross-env TZ=UTC NODE_ENV=development ts-node -r tsconfig-paths/register src/util/executeKlicktipp.ts",
"gmsusers": "cross-env TZ=UTC NODE_ENV=development ts-node -r tsconfig-paths/register src/seeds/gmsUsers.ts",
"gmsuserList": "cross-env TZ=UTC NODE_ENV=development ts-node -r tsconfig-paths/register src/seeds/gmsUserList.ts",
"humhubUserExport": "cross-env TZ=UTC NODE_ENV=development ts-node -r tsconfig-paths/register src/apis/humhub/ImportUsers.ts",
"locales": "scripts/sort.sh"
},
"dependencies": {
@ -47,6 +48,7 @@
"reflect-metadata": "^0.1.13",
"sodium-native": "^3.3.0",
"type-graphql": "^1.1.1",
"typed-rest-client": "^1.8.11",
"uuid": "^8.3.2"
},
"devDependencies": {

View File

@ -0,0 +1,137 @@
import { SignJWT } from 'jose'
import { IRequestOptions, RestClient } from 'typed-rest-client'
import { CONFIG } from '@/config'
import { LogError } from '@/server/LogError'
import { backendLogger as logger } from '@/server/logger'
import { GetUser } from './model/GetUser'
import { PostUser } from './model/PostUser'
import { PostUserError } from './model/PostUserError'
import { UsersResponse } from './model/UsersResponse'
/**
* HumHubClient as singleton class
*/
export class HumHubClient {
// eslint-disable-next-line no-use-before-define
private static instance: HumHubClient
private restClient: RestClient
// eslint-disable-next-line no-useless-constructor, @typescript-eslint/no-empty-function
private constructor() {
this.restClient = new RestClient('gradido-backend', CONFIG.HUMHUB_API_URL)
logger.info('create rest client for', CONFIG.HUMHUB_API_URL)
}
public static getInstance(): HumHubClient | undefined {
if (!CONFIG.HUMHUB_ACTIVE || !CONFIG.HUMHUB_API_URL) {
logger.info(`humhub are disabled via config...`)
return
}
if (!HumHubClient.instance) {
HumHubClient.instance = new HumHubClient()
}
return HumHubClient.instance
}
protected async createRequestOptions(
queryParams?: Record<string, string | number | (string | number)[]>,
): Promise<IRequestOptions> {
const requestOptions: IRequestOptions = {
additionalHeaders: { authorization: 'Bearer ' + (await this.createJWTToken()) },
}
if (queryParams) {
requestOptions.queryParameters = { params: queryParams }
}
return requestOptions
}
private async createJWTToken(): Promise<string> {
const secret = new TextEncoder().encode(CONFIG.HUMHUB_JWT_KEY)
const token = await new SignJWT({ 'urn:gradido:claim': true, uid: 1 })
.setProtectedHeader({ alg: 'HS512' })
.setIssuedAt()
.setIssuer('urn:gradido:issuer')
.setAudience('urn:gradido:audience')
.setExpirationTime('5m')
.sign(secret)
return token
}
/**
* Get all users from humhub
* https://marketplace.humhub.com/module/rest/docs/html/user.html#tag/User/paths/~1user/get
* @param page The number of page of the result set >= 0
* @param limit The numbers of items to return per page, Default: 20, [1 .. 50]
* @returns list of users
*/
public async users(page = 0, limit = 20): Promise<UsersResponse | null> {
const options = await this.createRequestOptions({ page, limit })
const response = await this.restClient.get<UsersResponse>('/api/v1/user', options)
if (response.statusCode !== 200) {
throw new LogError('error requesting users from humhub', response)
}
return response.result
}
/**
* get user by email
* https://marketplace.humhub.com/module/rest/docs/html/user.html#tag/User/paths/~1user~1get-by-email/get
* @param email for user search
* @returns user object if found
*/
public async userByEmail(email: string): Promise<GetUser | null> {
const options = await this.createRequestOptions({ email })
const response = await this.restClient.get<GetUser>('/api/v1/user/get-by-email', options)
// 404 = user not found
if (response.statusCode === 404) {
return null
}
return response.result
}
/**
* create user
* https://marketplace.humhub.com/module/rest/docs/html/user.html#tag/User/paths/~1user/post
* @param user for saving on humhub instance
*/
public async createUser(user: PostUser): Promise<void> {
const options = await this.createRequestOptions()
try {
const response = await this.restClient.create('/api/v1/user', user, options)
if (response.statusCode !== 200) {
throw new LogError('error creating user on humhub', { user, response })
}
} catch (error) {
throw new LogError('error on creating new user', {
user,
error: JSON.stringify(error, null, 2),
})
}
}
/**
* update user
* https://marketplace.humhub.com/module/rest/docs/html/user.html#tag/User/operation/updateUser
* @param user user object to update
* @param humhubUserId humhub user id
* @returns updated user object on success
*/
public async updateUser(user: PostUser, humhubUserId: number): Promise<GetUser | null> {
const options = await this.createRequestOptions()
const response = await this.restClient.update<GetUser>(
`/api/v1/user/${humhubUserId}`,
user,
options,
)
if (response.statusCode === 400) {
throw new LogError('Invalid user supplied', { user, response })
} else if (response.statusCode === 404) {
throw new LogError('User not found', { user, response })
}
return response.result
}
}
// new RestClient('gradido', 'api/v1/')

View File

@ -0,0 +1,150 @@
import { IsNull, Not } from '@dbTools/typeorm'
import { User } from '@entity/User'
import { CONFIG } from '@/config'
import { LogError } from '@/server/LogError'
import { backendLogger as logger } from '@/server/logger'
import { Connection } from '@/typeorm/connection'
import { checkDBVersion } from '@/typeorm/DBVersion'
import { checkForChanges } from './checkForChanges'
import { HumHubClient } from './HumHubClient'
import { GetUser } from './model/GetUser'
import { PostUser } from './model/PostUser'
const USER_BULK_SIZE = 20
enum ExecutedHumhubAction {
UPDATE,
CREATE,
SKIP,
}
function getUsersPage(page: number, limit: number): Promise<[User[], number]> {
return User.findAndCount({
relations: { emailContact: true },
skip: page * limit,
take: limit,
where: { emailContact: { email: Not(IsNull()) } },
})
}
async function createOrUpdateOrSkipUser(
user: User,
humHubClient: HumHubClient,
humhubUsers: Map<string, GetUser>,
): Promise<ExecutedHumhubAction> {
const postUser = new PostUser(user)
const humhubUser = humhubUsers.get(user.emailContact.email.trim())
if (humhubUser) {
if (checkForChanges(humhubUser, user)) {
return ExecutedHumhubAction.SKIP
}
await humHubClient.updateUser(postUser, humhubUser.id)
return ExecutedHumhubAction.UPDATE
} else {
await humHubClient.createUser(postUser)
return ExecutedHumhubAction.CREATE
}
}
/**
* @param client
* @returns user map indiced with email
*/
async function loadUsersFromHumHub(client: HumHubClient): Promise<Map<string, GetUser>> {
const start = new Date().getTime()
const humhubUsers = new Map<string, GetUser>()
const firstPage = await client.users(0, 50)
if (!firstPage) {
throw new LogError('not a single user found on humhub, please check config and setup')
}
firstPage.results.forEach((user) => {
humhubUsers.set(user.account.email.trim(), user)
})
let page = 1
while (humhubUsers.size < firstPage.total) {
const usersPage = await client.users(page, 50)
if (!usersPage) {
throw new LogError('error requesting next users page from humhub')
}
usersPage.results.forEach((user) => {
humhubUsers.set(user.account.email.trim(), user)
})
page++
}
const elapsed = new Date().getTime() - start
logger.info('load users from humhub', {
total: humhubUsers.size,
timeSeconds: elapsed / 1000.0,
})
return humhubUsers
}
async function main() {
const start = new Date().getTime()
// open mysql connection
const con = await Connection.getInstance()
if (!con?.isConnected) {
logger.fatal(`Couldn't open connection to database!`)
throw new Error(`Fatal: Couldn't open connection to database`)
}
// check for correct database version
const dbVersion = await checkDBVersion(CONFIG.DB_VERSION)
if (!dbVersion) {
logger.fatal('Fatal: Database Version incorrect')
throw new Error('Fatal: Database Version incorrect')
}
let userCount = 0
let page = 0
const humHubClient = HumHubClient.getInstance()
if (!humHubClient) {
throw new LogError('error creating humhub client')
}
const humhubUsers = await loadUsersFromHumHub(humHubClient)
let dbUserCount = 0
let updatedUserCount = 0
let createdUserCount = 0
let skippedUserCount = 0
do {
const [users, totalUsers] = await getUsersPage(page, USER_BULK_SIZE)
dbUserCount += users.length
userCount = users.length
page++
const promises: Promise<ExecutedHumhubAction>[] = []
users.forEach((user: User) =>
promises.push(createOrUpdateOrSkipUser(user, humHubClient, humhubUsers)),
)
const executedActions = await Promise.all(promises)
executedActions.forEach((executedAction: ExecutedHumhubAction) => {
if (executedAction === ExecutedHumhubAction.CREATE) createdUserCount++
else if (executedAction === ExecutedHumhubAction.UPDATE) updatedUserCount++
else if (executedAction === ExecutedHumhubAction.SKIP) skippedUserCount++
})
// using process.stdout.write here so that carriage-return is working analog to c
// printf("\rchecked user: %d/%d", dbUserCount, totalUsers);
process.stdout.write(`checked user: ${dbUserCount}/${totalUsers}\r`)
} while (userCount === USER_BULK_SIZE)
await con.destroy()
const elapsed = new Date().getTime() - start
logger.info('export user to humhub, statistics:', {
timeSeconds: elapsed / 1000.0,
gradidoUserCount: dbUserCount,
updatedUserCount,
createdUserCount,
skippedUserCount,
})
}
main().catch((e) => {
// eslint-disable-next-line no-console
console.error(e)
// eslint-disable-next-line n/no-process-exit
process.exit(1)
})

View File

@ -0,0 +1,33 @@
import { User } from '@entity/User'
import { Account } from './model/Account'
import { GetUser } from './model/GetUser'
import { Profile } from './model/Profile'
function profileIsTheSame(profile: Profile, user: User): boolean {
const gradidoUserProfile = new Profile(user)
if (profile.firstname !== gradidoUserProfile.firstname) return false
if (profile.lastname !== gradidoUserProfile.lastname) return false
if (profile.gradido_address !== gradidoUserProfile.gradido_address) return false
return true
}
function accountIsTheSame(account: Account, user: User): boolean {
const gradidoUserAccount = new Account(user)
if (account.username !== gradidoUserAccount.username) return false
if (account.email !== gradidoUserAccount.email) return false
if (account.language !== gradidoUserAccount.language) return false
return true
}
/**
* compare if gradido user (db entity) differ from humhub user
* @param humhubUser
* @param gradidoUse
*/
export function checkForChanges(humhubUser: GetUser, gradidoUser: User): boolean {
return (
profileIsTheSame(humhubUser.profile, gradidoUser) &&
accountIsTheSame(humhubUser.account, gradidoUser)
)
}

View File

@ -0,0 +1,18 @@
/**
* convert gradido language in valid humhub language
* humhub doesn't know en for example, only en-US and en-GB
* @param gradidoLanguage
*/
export function convertGradidoLanguageToHumhub(gradidoLanguage: string): string {
if (gradidoLanguage === 'en') {
return 'en-US'
}
return gradidoLanguage
}
export function convertHumhubLanguageToGradido(humhubLanguage: string): string {
if (humhubLanguage === 'en-US') {
return 'en'
}
return humhubLanguage
}

View File

@ -0,0 +1,24 @@
import { User } from '@entity/User'
import { convertGradidoLanguageToHumhub } from '@/apis/humhub/convertLanguage'
export class Account {
public constructor(user: User) {
if (user.alias && user.alias.length > 2) {
this.username = user.alias
} else {
// Use the replace method to remove the part after '+' and before '@'
// source: https://people.cs.rutgers.edu/~watrous/plus-signs-in-email-addresses.html
// email address with + exist but humhub doesn't allow username with +
this.username = user.emailContact.email.replace(/\+(.*)@/, '@')
}
this.email = user.emailContact.email
this.language = convertGradidoLanguageToHumhub(user.language)
}
username: string
email: string
tags: string[]
language: string
}

View File

@ -0,0 +1,11 @@
import { Account } from './Account'
import { Profile } from './Profile'
export class GetUser {
id: number
guid: string
// eslint-disable-next-line camelcase
display_name: string
account: Account
profile: Profile
}

View File

@ -0,0 +1,4 @@
export class Password {
newPassword: string
mustChangePassword: boolean
}

View File

@ -0,0 +1,16 @@
import { User } from '@entity/User'
import { Account } from './Account'
import { Password } from './Password'
import { Profile } from './Profile'
export class PostUser {
public constructor(user: User) {
this.account = new Account(user)
this.profile = new Profile(user)
}
account: Account
profile: Profile
password: Password
}

View File

@ -0,0 +1,6 @@
export class PostUserError {
code: number
message: string
profile: string[]
account: string[]
}

View File

@ -0,0 +1,21 @@
/* eslint-disable camelcase */
import { User } from '@entity/User'
import { CONFIG } from '@/config'
export class Profile {
public constructor(user: User) {
this.firstname = user.firstName
this.lastname = user.lastName
if (user.alias && user.alias.length > 2) {
this.gradido_address = CONFIG.COMMUNITY_NAME + '/' + user.alias
} else {
this.gradido_address = CONFIG.COMMUNITY_NAME + '/' + user.gradidoID
}
}
firstname: string
lastname: string
gradido_address: string
about: string
}

View File

@ -0,0 +1,7 @@
import { GetUser } from './GetUser'
export class UsersResponse {
total: number
page: number
results: GetUser[]
}

View File

@ -19,7 +19,7 @@ const constants = {
LOG_LEVEL: process.env.LOG_LEVEL ?? 'info',
CONFIG_VERSION: {
DEFAULT: 'DEFAULT',
EXPECTED: 'v21.2024-01-06',
EXPECTED: 'v22.2024-03-06',
CURRENT: '',
},
}
@ -147,6 +147,12 @@ const gms = {
GMS_URL: process.env.GMS_HOST ?? 'http://localhost:4044/',
}
const humhub = {
HUMHUB_ACTIVE: process.env.HUMHUB_ACTIVE === 'true' || false,
HUMHUB_API_URL: process.env.HUMHUB_API_URL ?? COMMUNITY_URL + '/community/',
HUMHUB_JWT_KEY: process.env.HUMHUB_JWT_KEY ?? '',
}
export const CONFIG = {
...constants,
...server,
@ -159,4 +165,5 @@ export const CONFIG = {
...webhook,
...federation,
...gms,
...humhub,
}

View File

@ -50,6 +50,7 @@ export class Connection {
} catch (error) {
// eslint-disable-next-line no-console
console.log(error)
console.log('catch from createConnection')
return null
}
}

View File

@ -1188,6 +1188,13 @@
"@types/mime" "^1"
"@types/node" "*"
"@types/simple-get@^4.0.3":
version "4.0.3"
resolved "https://registry.yarnpkg.com/@types/simple-get/-/simple-get-4.0.3.tgz#91f8f03fd4e5d3720a1a6114c6a1bb5761326092"
integrity sha512-X3HNxcz8ZjpTB+eLf3dH0m/KVt6mln3XKKAeBLca3rMtjFeB67hy3SDAkfWj0ulb++nqSp8nmCfMLAq9FpBW9g==
dependencies:
"@types/node" "*"
"@types/sodium-native@^2.3.5":
version "2.3.5"
resolved "https://registry.yarnpkg.com/@types/sodium-native/-/sodium-native-2.3.5.tgz#5d2681e7b6b67bcbdc63cfb133e303ec9e942e43"
@ -2513,6 +2520,13 @@ decompress-response@^3.3.0:
dependencies:
mimic-response "^1.0.0"
decompress-response@^6.0.0:
version "6.0.0"
resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-6.0.0.tgz#ca387612ddb7e104bd16d85aab00d5ecf09c66fc"
integrity sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==
dependencies:
mimic-response "^3.1.0"
dedent@^0.7.0:
version "0.7.0"
resolved "https://registry.yarnpkg.com/dedent/-/dedent-0.7.0.tgz#2495ddbaf6eb874abb0e1be9df22d2e5a544326c"
@ -3696,7 +3710,7 @@ graceful-fs@^4.1.6, graceful-fs@^4.2.0:
integrity sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==
"gradido-database@file:../database":
version "2.1.1"
version "2.2.0"
dependencies:
"@types/uuid" "^8.3.4"
cross-env "^7.0.3"
@ -5268,6 +5282,11 @@ mimic-response@^1.0.0, mimic-response@^1.0.1:
resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-1.0.1.tgz#4923538878eef42063cb8a3e3b0798781487ab1b"
integrity sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==
mimic-response@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-3.1.0.tgz#2d1d59af9c1b129815accc2c46a022a5ce1fa3c9"
integrity sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==
minimatch@^3.0.4:
version "3.0.4"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083"
@ -6070,6 +6089,13 @@ qs@^6.11.0:
dependencies:
side-channel "^1.0.4"
qs@^6.9.1:
version "6.11.2"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.2.tgz#64bea51f12c1f5da1bc01496f48ffcff7c69d7d9"
integrity sha512-tDNIz22aBzCDxLtVH++VnTfzxlfeK5CbqohpSqpJgj1Wg/cQbStNAz3NuqCs5vV+pjBsK4x4pN9HlVh7rcYRiA==
dependencies:
side-channel "^1.0.4"
queue-microtask@^1.2.2:
version "1.2.3"
resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243"
@ -6449,6 +6475,20 @@ signal-exit@^3.0.2, signal-exit@^3.0.3:
resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.5.tgz#9e3e8cc0c75a99472b44321033a7702e7738252f"
integrity sha512-KWcOiKeQj6ZyXx7zq4YxSMgHRlod4czeBQZrPb8OKcohcqAXShm7E20kEMle9WBt26hFcAf0qLOcp5zmY7kOqQ==
simple-concat@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/simple-concat/-/simple-concat-1.0.1.tgz#f46976082ba35c2263f1c8ab5edfe26c41c9552f"
integrity sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==
simple-get@^4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/simple-get/-/simple-get-4.0.1.tgz#4a39db549287c979d352112fa03fd99fd6bc3543"
integrity sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==
dependencies:
decompress-response "^6.0.0"
once "^1.3.1"
simple-concat "^1.0.0"
sisteransi@^1.0.5:
version "1.0.5"
resolved "https://registry.yarnpkg.com/sisteransi/-/sisteransi-1.0.5.tgz#134d681297756437cc05ca01370d3a7a571075ed"
@ -6916,6 +6956,11 @@ tsutils@^3.21.0:
dependencies:
tslib "^1.8.1"
tunnel@0.0.6:
version "0.0.6"
resolved "https://registry.yarnpkg.com/tunnel/-/tunnel-0.0.6.tgz#72f1314b34a5b192db012324df2cc587ca47f92c"
integrity sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==
type-check@^0.4.0, type-check@~0.4.0:
version "0.4.0"
resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1"
@ -6981,6 +7026,15 @@ typed-array-length@^1.0.4:
for-each "^0.3.3"
is-typed-array "^1.1.9"
typed-rest-client@^1.8.11:
version "1.8.11"
resolved "https://registry.yarnpkg.com/typed-rest-client/-/typed-rest-client-1.8.11.tgz#6906f02e3c91e8d851579f255abf0fd60800a04d"
integrity sha512-5UvfMpd1oelmUPRbbaVnq+rHP7ng2cE4qoQkQeAqxRL6PklkxsM0g32/HL0yfvruK6ojQ5x8EE+HF4YV6DtuCA==
dependencies:
qs "^6.9.1"
tunnel "0.0.6"
underscore "^1.12.1"
typedarray-to-buffer@^3.1.5:
version "3.1.5"
resolved "https://registry.yarnpkg.com/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz#a97ee7a9ff42691b9f783ff1bc5112fe3fca9080"
@ -7051,6 +7105,11 @@ underscore.deep@~0.5.1:
resolved "https://registry.yarnpkg.com/underscore.deep/-/underscore.deep-0.5.3.tgz#210969d58025339cecabd2a2ad8c3e8925e5c095"
integrity sha512-4OuSOlFNkiVFVc3khkeG112Pdu1gbitMj7t9B9ENb61uFmN70Jq7Iluhi3oflcSgexkKfDdJ5XAJET2gEq6ikA==
underscore@^1.12.1:
version "1.13.6"
resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.13.6.tgz#04786a1f589dc6c09f761fc5f45b89e935136441"
integrity sha512-+A5Sja4HP1M08MaXya7p5LvjuM7K6q/2EaC0+iovj/wOcMsTzMvDFbasi/oSapiwOlt252IqsKqPjCl7huKS0A==
underscore@~1.13.1:
version "1.13.4"
resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.13.4.tgz#7886b46bbdf07f768e0052f1828e1dcab40c0dee"

View File

@ -124,3 +124,8 @@ WEBHOOK_ELOPAGE_SECRET=secret
# Coordinates of Illuminz test instance
#GMS_URL=http://54.176.169.179:3071
#GMS_URL=http://localhost:4044/
# HUMHUB
HUMHUB_ACTIVE=false
#HUMHUB_API_URL=https://community.gradido.net
#HUMHUB_JWT_KEY=