mirror of
https://github.com/IT4Change/gradido.git
synced 2026-02-06 09:56:05 +00:00
Merge pull request #3308 from gradido/humhub_export_user
feat(backend): humhub export user function
This commit is contained in:
commit
8b0099b24d
@ -72,3 +72,7 @@ FEDERATION_XCOM_SENDCOINS_ENABLED=false
|
||||
GMS_API_URL=http://localhost:4044/
|
||||
GMS_DASHBOARD_URL=http://localhost:8080/
|
||||
|
||||
# HUMHUB
|
||||
HUMHUB_ACTIVE=false
|
||||
#HUMHUB_API_URL=https://community.gradido.net/
|
||||
#HUMHUB_JWT_KEY=
|
||||
|
||||
@ -70,3 +70,9 @@ GMS_API_URL=$GMS_API_URL
|
||||
GMS_DASHBOARD_URL=$GMS_DASHBOARD_URL
|
||||
GMS_WEBHOOK_SECRET=$GMS_WEBHOOK_SECRET
|
||||
GMS_CREATE_USER_THROW_ERRORS=$GMS_CREATE_USER_THROW_ERRORS
|
||||
|
||||
# HUMHUB
|
||||
HUMHUB_ACTIVE=$HUMHUB_ACTIVE
|
||||
HUMHUB_API_URL=$HUMHUB_API_URL
|
||||
HUMHUB_JWT_KEY=$HUMHUB_JWT_KEY
|
||||
|
||||
|
||||
@ -7,7 +7,7 @@ module.exports = {
|
||||
collectCoverageFrom: ['src/**/*.ts', '!**/node_modules/**', '!src/seeds/**', '!build/**'],
|
||||
coverageThreshold: {
|
||||
global: {
|
||||
lines: 83,
|
||||
lines: 82,
|
||||
},
|
||||
},
|
||||
setupFiles: ['<rootDir>/test/testSetup.ts'],
|
||||
|
||||
@ -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/ExportUsers.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": {
|
||||
|
||||
119
backend/src/apis/humhub/ExportUsers.ts
Normal file
119
backend/src/apis/humhub/ExportUsers.ts
Normal file
@ -0,0 +1,119 @@
|
||||
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 { HumHubClient } from './HumHubClient'
|
||||
import { GetUser } from './model/GetUser'
|
||||
import { ExecutedHumhubAction, syncUser } from './syncUser'
|
||||
|
||||
const USER_BULK_SIZE = 20
|
||||
|
||||
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()) } },
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* @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
|
||||
const executedHumhubActionsCount = [0, 0, 0, 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(syncUser(user, humhubUsers)))
|
||||
const executedActions = await Promise.all(promises)
|
||||
executedActions.forEach((executedAction: ExecutedHumhubAction) => {
|
||||
executedHumhubActionsCount[executedAction as number]++
|
||||
})
|
||||
// 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,
|
||||
createdCount: executedHumhubActionsCount[ExecutedHumhubAction.CREATE],
|
||||
updatedCount: executedHumhubActionsCount[ExecutedHumhubAction.UPDATE],
|
||||
skippedCount: executedHumhubActionsCount[ExecutedHumhubAction.SKIP],
|
||||
deletedCount: executedHumhubActionsCount[ExecutedHumhubAction.DELETE],
|
||||
})
|
||||
}
|
||||
|
||||
main().catch((e) => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(e)
|
||||
// eslint-disable-next-line n/no-process-exit
|
||||
process.exit(1)
|
||||
})
|
||||
148
backend/src/apis/humhub/HumHubClient.ts
Normal file
148
backend/src/apis/humhub/HumHubClient.ts
Normal file
@ -0,0 +1,148 @@
|
||||
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 { 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
|
||||
}
|
||||
|
||||
public async deleteUser(humhubUserId: number): Promise<void> {
|
||||
const options = await this.createRequestOptions()
|
||||
const response = await this.restClient.del(`/api/v1/user/${humhubUserId}`, options)
|
||||
if (response.statusCode === 400) {
|
||||
throw new LogError('invalid user supplied', { userId: humhubUserId, response })
|
||||
} else if (response.statusCode === 404) {
|
||||
throw new LogError('User not found', { userId: humhubUserId, response })
|
||||
} else if (response.statusCode !== 200) {
|
||||
throw new LogError('error deleting user', { userId: humhubUserId, response })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// new RestClient('gradido', 'api/v1/')
|
||||
64
backend/src/apis/humhub/compareHumhubUserDbUser.test.ts
Normal file
64
backend/src/apis/humhub/compareHumhubUserDbUser.test.ts
Normal file
@ -0,0 +1,64 @@
|
||||
/* eslint-disable prettier/prettier */
|
||||
import { communityDbUser } from '@/util/communityUser'
|
||||
|
||||
import { isHumhubUserIdenticalToDbUser } from './compareHumhubUserDbUser'
|
||||
import { GetUser } from './model/GetUser'
|
||||
|
||||
const defaultUser = communityDbUser
|
||||
|
||||
describe('isHumhubUserIdenticalToDbUser', () => {
|
||||
beforeEach(() => {
|
||||
defaultUser.firstName = 'first name'
|
||||
defaultUser.lastName = 'last name'
|
||||
defaultUser.alias = 'alias'
|
||||
defaultUser.emailContact.email = 'email@gmail.com'
|
||||
defaultUser.language = 'en'
|
||||
})
|
||||
|
||||
it('Should return true because humhubUser was created from entity user', () => {
|
||||
const humhubUser = new GetUser(defaultUser, 1)
|
||||
const result = isHumhubUserIdenticalToDbUser(humhubUser, defaultUser)
|
||||
expect(result).toBe(true)
|
||||
})
|
||||
|
||||
it('Should return false because first name differ', () => {
|
||||
const humhubUser = new GetUser(defaultUser, 1)
|
||||
humhubUser.profile.firstname = 'changed first name'
|
||||
const result = isHumhubUserIdenticalToDbUser(humhubUser, defaultUser)
|
||||
expect(result).toBe(false)
|
||||
})
|
||||
it('Should return false because last name differ', () => {
|
||||
const humhubUser = new GetUser(defaultUser, 1)
|
||||
humhubUser.profile.lastname = 'changed last name'
|
||||
const result = isHumhubUserIdenticalToDbUser(humhubUser, defaultUser)
|
||||
expect(result).toBe(false)
|
||||
})
|
||||
it('Should return false because username differ', () => {
|
||||
const humhubUser = new GetUser(defaultUser, 1)
|
||||
humhubUser.account.username = 'changed username'
|
||||
const result = isHumhubUserIdenticalToDbUser(humhubUser, defaultUser)
|
||||
expect(result).toBe(false)
|
||||
})
|
||||
|
||||
it('Should return false because email differ', () => {
|
||||
const humhubUser = new GetUser(defaultUser, 1)
|
||||
humhubUser.account.email = 'new@gmail.com'
|
||||
const result = isHumhubUserIdenticalToDbUser(humhubUser, defaultUser)
|
||||
expect(result).toBe(false)
|
||||
})
|
||||
|
||||
it('Should return false because language differ', () => {
|
||||
const humhubUser = new GetUser(defaultUser, 1)
|
||||
humhubUser.account.language = 'de'
|
||||
const result = isHumhubUserIdenticalToDbUser(humhubUser, defaultUser)
|
||||
expect(result).toBe(false)
|
||||
})
|
||||
|
||||
it('Should return false because gradido_address differ', () => {
|
||||
const humhubUser = new GetUser(defaultUser, 1)
|
||||
// eslint-disable-next-line camelcase
|
||||
humhubUser.profile.gradido_address = 'changed gradido address'
|
||||
const result = isHumhubUserIdenticalToDbUser(humhubUser, defaultUser)
|
||||
expect(result).toBe(false)
|
||||
})
|
||||
})
|
||||
34
backend/src/apis/humhub/compareHumhubUserDbUser.ts
Normal file
34
backend/src/apis/humhub/compareHumhubUserDbUser.ts
Normal file
@ -0,0 +1,34 @@
|
||||
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
|
||||
* @return true if no differences
|
||||
*/
|
||||
export function isHumhubUserIdenticalToDbUser(humhubUser: GetUser, gradidoUser: User): boolean {
|
||||
return (
|
||||
profileIsTheSame(humhubUser.profile, gradidoUser) &&
|
||||
accountIsTheSame(humhubUser.account, gradidoUser)
|
||||
)
|
||||
}
|
||||
31
backend/src/apis/humhub/convertLanguage.test.ts
Normal file
31
backend/src/apis/humhub/convertLanguage.test.ts
Normal file
@ -0,0 +1,31 @@
|
||||
import { convertGradidoLanguageToHumhub, convertHumhubLanguageToGradido } from './convertLanguage'
|
||||
|
||||
describe('convertGradidoLanguageToHumhub', () => {
|
||||
it('Should convert "en" to "en-US"', () => {
|
||||
const result = convertGradidoLanguageToHumhub('en')
|
||||
expect(result).toBe('en-US')
|
||||
})
|
||||
|
||||
it('Should return the same language for other values', () => {
|
||||
const languages = ['de', 'fr', 'es', 'pt']
|
||||
languages.forEach((lang) => {
|
||||
const result = convertGradidoLanguageToHumhub(lang)
|
||||
expect(result).toBe(lang)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('convertHumhubLanguageToGradido', () => {
|
||||
it('Should convert "en-US" to "en"', () => {
|
||||
const result = convertHumhubLanguageToGradido('en-US')
|
||||
expect(result).toBe('en')
|
||||
})
|
||||
|
||||
it('Should return the same language for other values', () => {
|
||||
const languages = ['de', 'fr', 'es', 'pt']
|
||||
languages.forEach((lang) => {
|
||||
const result = convertHumhubLanguageToGradido(lang)
|
||||
expect(result).toBe(lang)
|
||||
})
|
||||
})
|
||||
})
|
||||
18
backend/src/apis/humhub/convertLanguage.ts
Normal file
18
backend/src/apis/humhub/convertLanguage.ts
Normal 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
|
||||
}
|
||||
20
backend/src/apis/humhub/model/Account.ts
Normal file
20
backend/src/apis/humhub/model/Account.ts
Normal file
@ -0,0 +1,20 @@
|
||||
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 {
|
||||
this.username = user.gradidoID
|
||||
}
|
||||
|
||||
this.email = user.emailContact.email
|
||||
this.language = convertGradidoLanguageToHumhub(user.language)
|
||||
}
|
||||
|
||||
username: string
|
||||
email: string
|
||||
language: string
|
||||
}
|
||||
19
backend/src/apis/humhub/model/GetUser.ts
Normal file
19
backend/src/apis/humhub/model/GetUser.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import { User } from '@entity/User'
|
||||
|
||||
import { Account } from './Account'
|
||||
import { Profile } from './Profile'
|
||||
|
||||
export class GetUser {
|
||||
public constructor(user: User, id: number) {
|
||||
this.id = id
|
||||
this.account = new Account(user)
|
||||
this.profile = new Profile(user)
|
||||
}
|
||||
|
||||
id: number
|
||||
guid: string
|
||||
// eslint-disable-next-line camelcase
|
||||
display_name: string
|
||||
account: Account
|
||||
profile: Profile
|
||||
}
|
||||
4
backend/src/apis/humhub/model/Password.ts
Normal file
4
backend/src/apis/humhub/model/Password.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export class Password {
|
||||
newPassword: string
|
||||
mustChangePassword: boolean
|
||||
}
|
||||
16
backend/src/apis/humhub/model/PostUser.ts
Normal file
16
backend/src/apis/humhub/model/PostUser.ts
Normal 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
|
||||
}
|
||||
6
backend/src/apis/humhub/model/PostUserError.ts
Normal file
6
backend/src/apis/humhub/model/PostUserError.ts
Normal file
@ -0,0 +1,6 @@
|
||||
export class PostUserError {
|
||||
code: number
|
||||
message: string
|
||||
profile: string[]
|
||||
account: string[]
|
||||
}
|
||||
23
backend/src/apis/humhub/model/Profile.ts
Normal file
23
backend/src/apis/humhub/model/Profile.ts
Normal file
@ -0,0 +1,23 @@
|
||||
/* eslint-disable camelcase */
|
||||
import { User } from '@entity/User'
|
||||
|
||||
import { CONFIG } from '@/config'
|
||||
import { PublishNameLogic } from '@/data/PublishName.logic'
|
||||
import { PublishNameType } from '@/graphql/enum/PublishNameType'
|
||||
|
||||
export class Profile {
|
||||
public constructor(user: User) {
|
||||
const publishNameLogic = new PublishNameLogic(user)
|
||||
this.firstname = publishNameLogic.getFirstName(user.humhubPublishName as PublishNameType)
|
||||
this.lastname = publishNameLogic.getLastName(user.humhubPublishName as PublishNameType)
|
||||
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
|
||||
}
|
||||
7
backend/src/apis/humhub/model/UsersResponse.ts
Normal file
7
backend/src/apis/humhub/model/UsersResponse.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import { GetUser } from './GetUser'
|
||||
|
||||
export class UsersResponse {
|
||||
total: number
|
||||
page: number
|
||||
results: GetUser[]
|
||||
}
|
||||
110
backend/src/apis/humhub/syncUser.test.ts
Normal file
110
backend/src/apis/humhub/syncUser.test.ts
Normal file
@ -0,0 +1,110 @@
|
||||
/* eslint-disable @typescript-eslint/no-empty-function */
|
||||
import { User } from '@entity/User'
|
||||
import { UserContact } from '@entity/UserContact'
|
||||
|
||||
import { CONFIG } from '@/config'
|
||||
|
||||
import { HumHubClient } from './HumHubClient'
|
||||
import { GetUser } from './model/GetUser'
|
||||
import { syncUser, ExecutedHumhubAction } from './syncUser'
|
||||
|
||||
const defaultUser = new User()
|
||||
defaultUser.emailContact = new UserContact()
|
||||
defaultUser.emailContact.email = 'email@gmail.com'
|
||||
|
||||
CONFIG.HUMHUB_ACTIVE = true
|
||||
CONFIG.HUMHUB_API_URL = 'http://localhost'
|
||||
|
||||
let humhubClient: HumHubClient | undefined
|
||||
let humhubClientSpy: {
|
||||
createUser: jest.SpyInstance
|
||||
updateUser: jest.SpyInstance
|
||||
deleteUser: jest.SpyInstance
|
||||
}
|
||||
|
||||
describe('syncUser function', () => {
|
||||
beforeAll(() => {
|
||||
humhubClient = HumHubClient.getInstance()
|
||||
if (!humhubClient) {
|
||||
throw new Error('error creating humhub client')
|
||||
}
|
||||
humhubClientSpy = {
|
||||
createUser: jest.spyOn(humhubClient, 'createUser'),
|
||||
updateUser: jest.spyOn(humhubClient, 'updateUser'),
|
||||
deleteUser: jest.spyOn(humhubClient, 'deleteUser'),
|
||||
}
|
||||
humhubClientSpy.createUser.mockImplementation(() => Promise.resolve())
|
||||
humhubClientSpy.updateUser.mockImplementation(() => Promise.resolve())
|
||||
humhubClientSpy.deleteUser.mockImplementation(() => Promise.resolve())
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
humhubClientSpy.createUser.mockClear()
|
||||
humhubClientSpy.updateUser.mockClear()
|
||||
humhubClientSpy.deleteUser.mockClear()
|
||||
})
|
||||
afterAll(() => {
|
||||
jest.resetAllMocks()
|
||||
})
|
||||
|
||||
/*
|
||||
* Trigger action according to conditions
|
||||
* | User exist on humhub | export to humhub allowed | changes in user data | ACTION
|
||||
* | true | false | ignored | DELETE
|
||||
* | true | true | true | UPDATE
|
||||
* | true | true | false | SKIP
|
||||
* | false | false | ignored | SKIP
|
||||
* | false | true | ignored | CREATE
|
||||
* */
|
||||
|
||||
it('When humhubUser exists and user.humhubAllowed is false, should return DELETE action', async () => {
|
||||
const humhubUsers = new Map<string, GetUser>()
|
||||
humhubUsers.set(defaultUser.emailContact.email, new GetUser(defaultUser, 1))
|
||||
|
||||
defaultUser.humhubAllowed = false
|
||||
const result = await syncUser(defaultUser, humhubUsers)
|
||||
|
||||
expect(result).toBe(ExecutedHumhubAction.DELETE)
|
||||
})
|
||||
|
||||
it('When humhubUser exists and user.humhubAllowed is true and there are changes in user data, should return UPDATE action', async () => {
|
||||
const humhubUsers = new Map<string, GetUser>()
|
||||
const humhubUser = new GetUser(defaultUser, 1)
|
||||
humhubUser.account.username = 'test username'
|
||||
humhubUsers.set(defaultUser.emailContact.email, humhubUser)
|
||||
|
||||
defaultUser.humhubAllowed = true
|
||||
const result = await syncUser(defaultUser, humhubUsers)
|
||||
|
||||
expect(result).toBe(ExecutedHumhubAction.UPDATE)
|
||||
})
|
||||
|
||||
it('When humhubUser exists and user.humhubAllowed is true and there are no changes in user data, should return SKIP action', async () => {
|
||||
const humhubUsers = new Map<string, GetUser>()
|
||||
const humhubUser = new GetUser(defaultUser, 1)
|
||||
humhubUsers.set(defaultUser.emailContact.email, humhubUser)
|
||||
|
||||
defaultUser.humhubAllowed = true
|
||||
const result = await syncUser(defaultUser, humhubUsers)
|
||||
|
||||
expect(result).toBe(ExecutedHumhubAction.SKIP)
|
||||
})
|
||||
|
||||
it('When humhubUser not exists and user.humhubAllowed is false, should return SKIP action', async () => {
|
||||
const humhubUsers = new Map<string, GetUser>()
|
||||
|
||||
defaultUser.humhubAllowed = false
|
||||
const result = await syncUser(defaultUser, humhubUsers)
|
||||
|
||||
expect(result).toBe(ExecutedHumhubAction.SKIP)
|
||||
})
|
||||
|
||||
it('When humhubUser not exists and user.humhubAllowed is true, should return CREATE action', async () => {
|
||||
const humhubUsers = new Map<string, GetUser>()
|
||||
|
||||
defaultUser.humhubAllowed = true
|
||||
const result = await syncUser(defaultUser, humhubUsers)
|
||||
|
||||
expect(result).toBe(ExecutedHumhubAction.CREATE)
|
||||
})
|
||||
})
|
||||
57
backend/src/apis/humhub/syncUser.ts
Normal file
57
backend/src/apis/humhub/syncUser.ts
Normal file
@ -0,0 +1,57 @@
|
||||
import { User } from '@entity/User'
|
||||
|
||||
import { LogError } from '@/server/LogError'
|
||||
|
||||
import { isHumhubUserIdenticalToDbUser } from './compareHumhubUserDbUser'
|
||||
import { HumHubClient } from './HumHubClient'
|
||||
import { GetUser } from './model/GetUser'
|
||||
import { PostUser } from './model/PostUser'
|
||||
|
||||
export enum ExecutedHumhubAction {
|
||||
UPDATE,
|
||||
CREATE,
|
||||
SKIP,
|
||||
DELETE,
|
||||
}
|
||||
/**
|
||||
* Trigger action according to conditions
|
||||
* | User exist on humhub | export to humhub allowed | changes in user data | ACTION
|
||||
* | true | false | ignored | DELETE
|
||||
* | true | true | true | UPDATE
|
||||
* | true | true | false | SKIP
|
||||
* | false | false | ignored | SKIP
|
||||
* | false | true | ignored | CREATE
|
||||
* @param user
|
||||
* @param humHubClient
|
||||
* @param humhubUsers
|
||||
* @returns
|
||||
*/
|
||||
export async function syncUser(
|
||||
user: User,
|
||||
humhubUsers: Map<string, GetUser>,
|
||||
): Promise<ExecutedHumhubAction> {
|
||||
const postUser = new PostUser(user)
|
||||
const humhubUser = humhubUsers.get(user.emailContact.email.trim())
|
||||
const humHubClient = HumHubClient.getInstance()
|
||||
if (!humHubClient) {
|
||||
throw new LogError('Error creating humhub client')
|
||||
}
|
||||
|
||||
if (humhubUser) {
|
||||
if (!user.humhubAllowed) {
|
||||
await humHubClient.deleteUser(humhubUser.id)
|
||||
return ExecutedHumhubAction.DELETE
|
||||
}
|
||||
if (!isHumhubUserIdenticalToDbUser(humhubUser, user)) {
|
||||
// if humhub allowed
|
||||
await humHubClient.updateUser(postUser, humhubUser.id)
|
||||
return ExecutedHumhubAction.UPDATE
|
||||
}
|
||||
} else {
|
||||
if (user.humhubAllowed) {
|
||||
await humHubClient.createUser(postUser)
|
||||
return ExecutedHumhubAction.CREATE
|
||||
}
|
||||
}
|
||||
return ExecutedHumhubAction.SKIP
|
||||
}
|
||||
@ -151,6 +151,12 @@ const gms = {
|
||||
GMS_WEBHOOK_SECRET: process.env.GMS_WEBHOOK_SECRET ?? 'secret',
|
||||
}
|
||||
|
||||
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,
|
||||
@ -163,4 +169,5 @@ export const CONFIG = {
|
||||
...webhook,
|
||||
...federation,
|
||||
...gms,
|
||||
...humhub,
|
||||
}
|
||||
|
||||
59
backend/src/data/PublishName.logic.ts
Normal file
59
backend/src/data/PublishName.logic.ts
Normal file
@ -0,0 +1,59 @@
|
||||
import { User } from '@entity/User'
|
||||
|
||||
import { PublishNameType } from '@/graphql/enum/PublishNameType'
|
||||
|
||||
export class PublishNameLogic {
|
||||
constructor(private user: User) {}
|
||||
|
||||
/**
|
||||
* get first name based on publishNameType: PublishNameType value
|
||||
* @param publishNameType
|
||||
* @returns user.firstName for PUBLISH_NAME_FIRST, PUBLISH_NAME_FIRST_INITIAL or PUBLISH_NAME_FULL
|
||||
* first initial from user.firstName for PUBLISH_NAME_INITIALS or PUBLISH_NAME_INITIAL_LAST
|
||||
*/
|
||||
public getFirstName(publishNameType: PublishNameType): string {
|
||||
if (
|
||||
[
|
||||
PublishNameType.PUBLISH_NAME_FIRST,
|
||||
PublishNameType.PUBLISH_NAME_FIRST_INITIAL,
|
||||
PublishNameType.PUBLISH_NAME_FULL,
|
||||
].includes(publishNameType)
|
||||
) {
|
||||
return this.user.firstName
|
||||
}
|
||||
if (
|
||||
[PublishNameType.PUBLISH_NAME_INITIALS, PublishNameType.PUBLISH_NAME_INITIAL_LAST].includes(
|
||||
publishNameType,
|
||||
)
|
||||
) {
|
||||
return this.user.firstName.substring(0, 1)
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
/**
|
||||
* get last name based on publishNameType: GmsPublishNameType value
|
||||
* @param publishNameType
|
||||
* @returns user.lastName for PUBLISH_NAME_LAST, PUBLISH_NAME_INITIAL_LAST, PUBLISH_NAME_FULL
|
||||
* first initial from user.lastName for PUBLISH_NAME_FIRST_INITIAL, PUBLISH_NAME_INITIALS
|
||||
*/
|
||||
public getLastName(publishNameType: PublishNameType): string {
|
||||
if (
|
||||
[
|
||||
PublishNameType.PUBLISH_NAME_LAST,
|
||||
PublishNameType.PUBLISH_NAME_INITIAL_LAST,
|
||||
PublishNameType.PUBLISH_NAME_FULL,
|
||||
].includes(publishNameType)
|
||||
) {
|
||||
return this.user.lastName
|
||||
}
|
||||
if (
|
||||
[PublishNameType.PUBLISH_NAME_FIRST_INITIAL, PublishNameType.PUBLISH_NAME_INITIALS].includes(
|
||||
publishNameType,
|
||||
)
|
||||
) {
|
||||
return this.user.lastName.substring(0, 1)
|
||||
}
|
||||
return ''
|
||||
}
|
||||
}
|
||||
@ -3696,7 +3696,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.1"
|
||||
dependencies:
|
||||
"@types/uuid" "^8.3.4"
|
||||
cross-env "^7.0.3"
|
||||
@ -6070,6 +6070,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"
|
||||
@ -6916,6 +6923,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 +6993,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 +7072,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"
|
||||
|
||||
@ -129,4 +129,6 @@ GMS_WEBHOOK_SECRET=secret
|
||||
GMS_CREATE_USER_THROW_ERRORS=false
|
||||
|
||||
# HUMHUB
|
||||
HUMHUB_ACTIVE=false
|
||||
HUMHUB_ACTIVE=false
|
||||
#HUMHUB_API_URL=https://community.gradido.net
|
||||
#HUMHUB_JWT_KEY=
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user