mirror of
https://github.com/IT4Change/gradido.git
synced 2026-02-06 09:56:05 +00:00
Merge branch 'master' into dlt_register_address
This commit is contained in:
commit
ed8f289405
@ -4,8 +4,16 @@ All notable changes to this project will be documented in this file. Dates are d
|
||||
|
||||
Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
|
||||
|
||||
#### [2.2.1](https://github.com/gradido/gradido/compare/2.2.0...2.2.1)
|
||||
|
||||
- fix(other): deployment bugfixes [`#3290`](https://github.com/gradido/gradido/pull/3290)
|
||||
|
||||
#### [2.2.0](https://github.com/gradido/gradido/compare/2.1.1...2.2.0)
|
||||
|
||||
> 9 February 2024
|
||||
|
||||
- chore(release): v2.2.0 [`#3283`](https://github.com/gradido/gradido/pull/3283)
|
||||
- fix(backend): prevent warning, fix env error [`#3282`](https://github.com/gradido/gradido/pull/3282)
|
||||
- feat(frontend): update news text [`#3279`](https://github.com/gradido/gradido/pull/3279)
|
||||
- feat(frontend): use params instead of query for send/identifier route [`#3277`](https://github.com/gradido/gradido/pull/3277)
|
||||
- fix(other): deployment bugfixes [`#3276`](https://github.com/gradido/gradido/pull/3276)
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
"description": "Administraion Interface for Gradido",
|
||||
"main": "index.js",
|
||||
"author": "Moriz Wahl",
|
||||
"version": "2.2.0",
|
||||
"version": "2.2.1",
|
||||
"license": "Apache-2.0",
|
||||
"private": false,
|
||||
"scripts": {
|
||||
|
||||
@ -68,5 +68,11 @@ FEDERATION_XCOM_SENDCOINS_ENABLED=false
|
||||
# GMS
|
||||
# GMS_ACTIVE=true
|
||||
# Coordinates of Illuminz test instance
|
||||
#GMS_URL=http://54.176.169.179:3071
|
||||
GMS_URL=http://localhost:4044/
|
||||
#GMS_API_URL=http://54.176.169.179:3071
|
||||
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=
|
||||
|
||||
@ -66,4 +66,13 @@ FEDERATION_XCOM_SENDCOINS_ENABLED=$FEDERATION_XCOM_SENDCOINS_ENABLED
|
||||
|
||||
# GMS
|
||||
GMS_ACTIVE=$GMS_ACTIVE
|
||||
GMS_URL=$GMS_URL
|
||||
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: 84,
|
||||
lines: 81,
|
||||
},
|
||||
},
|
||||
setupFiles: ['<rootDir>/test/testSetup.ts'],
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "gradido-backend",
|
||||
"version": "2.2.0",
|
||||
"version": "2.2.1",
|
||||
"description": "Gradido unified backend providing an API-Service for Gradido Transactions",
|
||||
"main": "src/index.ts",
|
||||
"repository": "https://github.com/gradido/gradido/backend",
|
||||
@ -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": {
|
||||
|
||||
@ -7,12 +7,13 @@ import axios from 'axios'
|
||||
import { CONFIG } from '@/config'
|
||||
import { LogError } from '@/server/LogError'
|
||||
import { backendLogger as logger } from '@/server/logger'
|
||||
import { ensureUrlEndsWithSlash } from '@/util/utilities'
|
||||
|
||||
import { GmsUser } from './model/GmsUser'
|
||||
|
||||
/*
|
||||
export async function communityList(): Promise<GmsCommunity[] | string | undefined> {
|
||||
const baseUrl = CONFIG.GMS_URL.endsWith('/') ? CONFIG.GMS_URL : CONFIG.GMS_URL.concat('/')
|
||||
const baseUrl = ensureUrlEndsWithSlash(CONFIG.GMS_URL)
|
||||
const service = 'community/list?page=1&perPage=20'
|
||||
const config = {
|
||||
headers: {
|
||||
@ -44,7 +45,7 @@ export async function communityList(): Promise<GmsCommunity[] | string | undefin
|
||||
}
|
||||
|
||||
export async function userList(): Promise<GmsUser[] | string | undefined> {
|
||||
const baseUrl = CONFIG.GMS_URL.endsWith('/') ? CONFIG.GMS_URL : CONFIG.GMS_URL.concat('/')
|
||||
const baseUrl = ensureUrlEndsWithSlash(CONFIG.GMS_URL)
|
||||
const service = 'community-user/list?page=1&perPage=20'
|
||||
const config = {
|
||||
headers: {
|
||||
@ -80,7 +81,7 @@ export async function userList(): Promise<GmsUser[] | string | undefined> {
|
||||
}
|
||||
|
||||
export async function userByUuid(uuid: string): Promise<GmsUser[] | string | undefined> {
|
||||
const baseUrl = CONFIG.GMS_URL.endsWith('/') ? CONFIG.GMS_URL : CONFIG.GMS_URL.concat('/')
|
||||
const baseUrl = ensureUrlEndsWithSlash(CONFIG.GMS_URL)
|
||||
const service = 'community-user/list?page=1&perPage=20'
|
||||
const config = {
|
||||
headers: {
|
||||
@ -117,30 +118,106 @@ export async function userByUuid(uuid: string): Promise<GmsUser[] | string | und
|
||||
*/
|
||||
|
||||
export async function createGmsUser(apiKey: string, user: GmsUser): Promise<boolean> {
|
||||
const baseUrl = CONFIG.GMS_URL.endsWith('/') ? CONFIG.GMS_URL : CONFIG.GMS_URL.concat('/')
|
||||
const service = 'community-user'
|
||||
if (CONFIG.GMS_ACTIVE) {
|
||||
const baseUrl = ensureUrlEndsWithSlash(CONFIG.GMS_API_URL)
|
||||
const service = 'community-user'
|
||||
const config = {
|
||||
headers: {
|
||||
accept: 'application/json',
|
||||
language: 'en',
|
||||
timezone: 'UTC',
|
||||
connection: 'keep-alive',
|
||||
authorization: apiKey,
|
||||
},
|
||||
}
|
||||
try {
|
||||
const result = await axios.post(baseUrl.concat(service), user, config)
|
||||
logger.debug('POST-Response of community-user:', result)
|
||||
if (result.status !== 200) {
|
||||
throw new LogError('HTTP Status Error in community-user:', result.status, result.statusText)
|
||||
}
|
||||
logger.debug('responseData:', result.data.responseData)
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
|
||||
// const gmsUser = JSON.parse(result.data.responseData)
|
||||
// logger.debug('gmsUser:', gmsUser)
|
||||
return true
|
||||
} catch (error: any) {
|
||||
logger.error('Error in post community-user:', error)
|
||||
throw new LogError(error.message)
|
||||
}
|
||||
} else {
|
||||
logger.info('GMS-Communication disabled per ConfigKey GMS_ACTIVE=false!')
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
export async function updateGmsUser(apiKey: string, user: GmsUser): Promise<boolean> {
|
||||
if (CONFIG.GMS_ACTIVE) {
|
||||
const baseUrl = ensureUrlEndsWithSlash(CONFIG.GMS_API_URL)
|
||||
const service = 'community-user'
|
||||
const config = {
|
||||
headers: {
|
||||
accept: 'application/json',
|
||||
language: 'en',
|
||||
timezone: 'UTC',
|
||||
connection: 'keep-alive',
|
||||
authorization: apiKey,
|
||||
},
|
||||
}
|
||||
try {
|
||||
const result = await axios.patch(baseUrl.concat(service), user, config)
|
||||
logger.debug('PATCH-Response of community-user:', result)
|
||||
if (result.status !== 200) {
|
||||
throw new LogError('HTTP Status Error in community-user:', result.status, result.statusText)
|
||||
}
|
||||
logger.debug('responseData:', result.data.responseData)
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
|
||||
// const gmsUser = JSON.parse(result.data.responseData)
|
||||
// logger.debug('gmsUser:', gmsUser)
|
||||
return true
|
||||
} catch (error: any) {
|
||||
logger.error('Error in patch community-user:', error)
|
||||
throw new LogError(error.message)
|
||||
}
|
||||
} else {
|
||||
logger.info('GMS-Communication disabled per ConfigKey GMS_ACTIVE=false!')
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
export async function verifyAuthToken(
|
||||
// apiKey: string,
|
||||
communityUuid: string,
|
||||
token: string,
|
||||
): Promise<string> {
|
||||
const baseUrl = ensureUrlEndsWithSlash(CONFIG.GMS_API_URL)
|
||||
const service = 'verify-auth-token?token='.concat(token).concat('&uuid=').concat(communityUuid)
|
||||
const config = {
|
||||
headers: {
|
||||
accept: 'application/json',
|
||||
language: 'en',
|
||||
timezone: 'UTC',
|
||||
connection: 'keep-alive',
|
||||
authorization: apiKey,
|
||||
// authorization: apiKey,
|
||||
},
|
||||
}
|
||||
try {
|
||||
const result = await axios.post(baseUrl.concat(service), user, config)
|
||||
logger.debug('POST-Response of community-user:', result)
|
||||
const result = await axios.get(baseUrl.concat(service), config)
|
||||
logger.debug('GET-Response of verify-auth-token:', result)
|
||||
if (result.status !== 200) {
|
||||
throw new LogError('HTTP Status Error in community-user:', result.status, result.statusText)
|
||||
throw new LogError(
|
||||
'HTTP Status Error in verify-auth-token:',
|
||||
result.status,
|
||||
result.statusText,
|
||||
)
|
||||
}
|
||||
logger.debug('responseData:', result.data.responseData)
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
|
||||
// const gmsUser = JSON.parse(result.data.responseData)
|
||||
// logger.debug('gmsUser:', gmsUser)
|
||||
return true
|
||||
const token: string = result.data.responseData.token
|
||||
logger.debug('verifyAuthToken=', token)
|
||||
return token
|
||||
} catch (error: any) {
|
||||
logger.error('Error in Get community-user:', error)
|
||||
logger.error('Error in verifyAuthToken:', error)
|
||||
throw new LogError(error.message)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,13 +1,14 @@
|
||||
import { User as dbUser } from '@entity/User'
|
||||
|
||||
import { GmsPublishLocationType } from '@/graphql/enum/GmsPublishLocationType'
|
||||
import { GmsPublishNameType } from '@/graphql/enum/GmsPublishNameType'
|
||||
import { GmsPublishPhoneType } from '@/graphql/enum/GmsPublishPhoneType'
|
||||
import { PublishNameType } from '@/graphql/enum/PublishNameType'
|
||||
|
||||
export class GmsUser {
|
||||
constructor(user: dbUser) {
|
||||
this.userUuid = user.gradidoID
|
||||
// this.communityUuid = user.communityUuid
|
||||
this.language = user.language
|
||||
this.email = this.getGmsEmail(user)
|
||||
this.countryCode = this.getGmsCountryCode(user)
|
||||
this.mobile = this.getGmsPhone(user)
|
||||
@ -43,7 +44,7 @@ export class GmsUser {
|
||||
if (
|
||||
user.gmsAllowed &&
|
||||
user.alias &&
|
||||
user.gmsPublishName === GmsPublishNameType.GMS_PUBLISH_NAME_ALIAS_OR_INITALS
|
||||
user.gmsPublishName === PublishNameType.PUBLISH_NAME_ALIAS_OR_INITALS
|
||||
) {
|
||||
return user.alias
|
||||
}
|
||||
@ -52,32 +53,30 @@ export class GmsUser {
|
||||
private getGmsFirstName(user: dbUser): string | undefined {
|
||||
if (
|
||||
user.gmsAllowed &&
|
||||
(user.gmsPublishName === GmsPublishNameType.GMS_PUBLISH_NAME_FIRST ||
|
||||
user.gmsPublishName === GmsPublishNameType.GMS_PUBLISH_NAME_FIRST_INITIAL ||
|
||||
user.gmsPublishName === GmsPublishNameType.GMS_PUBLISH_NAME_FULL)
|
||||
(user.gmsPublishName === PublishNameType.PUBLISH_NAME_FIRST ||
|
||||
user.gmsPublishName === PublishNameType.PUBLISH_NAME_FIRST_INITIAL ||
|
||||
user.gmsPublishName === PublishNameType.PUBLISH_NAME_FULL)
|
||||
) {
|
||||
return user.firstName
|
||||
}
|
||||
if (
|
||||
user.gmsAllowed &&
|
||||
((!user.alias &&
|
||||
user.gmsPublishName === GmsPublishNameType.GMS_PUBLISH_NAME_ALIAS_OR_INITALS) ||
|
||||
user.gmsPublishName === GmsPublishNameType.GMS_PUBLISH_NAME_INITIALS)
|
||||
((!user.alias && user.gmsPublishName === PublishNameType.PUBLISH_NAME_ALIAS_OR_INITALS) ||
|
||||
user.gmsPublishName === PublishNameType.PUBLISH_NAME_INITIALS)
|
||||
) {
|
||||
return user.firstName.substring(0, 1)
|
||||
}
|
||||
}
|
||||
|
||||
private getGmsLastName(user: dbUser): string | undefined {
|
||||
if (user.gmsAllowed && user.gmsPublishName === GmsPublishNameType.GMS_PUBLISH_NAME_FULL) {
|
||||
if (user.gmsAllowed && user.gmsPublishName === PublishNameType.PUBLISH_NAME_FULL) {
|
||||
return user.lastName
|
||||
}
|
||||
if (
|
||||
user.gmsAllowed &&
|
||||
((!user.alias &&
|
||||
user.gmsPublishName === GmsPublishNameType.GMS_PUBLISH_NAME_ALIAS_OR_INITALS) ||
|
||||
user.gmsPublishName === GmsPublishNameType.GMS_PUBLISH_NAME_FIRST_INITIAL ||
|
||||
user.gmsPublishName === GmsPublishNameType.GMS_PUBLISH_NAME_INITIALS)
|
||||
((!user.alias && user.gmsPublishName === PublishNameType.PUBLISH_NAME_ALIAS_OR_INITALS) ||
|
||||
user.gmsPublishName === PublishNameType.PUBLISH_NAME_FIRST_INITIAL ||
|
||||
user.gmsPublishName === PublishNameType.PUBLISH_NAME_INITIALS)
|
||||
) {
|
||||
return user.lastName.substring(0, 1)
|
||||
}
|
||||
|
||||
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 indices 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)
|
||||
})
|
||||
189
backend/src/apis/humhub/HumHubClient.ts
Normal file
189
backend/src/apis/humhub/HumHubClient.ts
Normal file
@ -0,0 +1,189 @@
|
||||
import { SignJWT } from 'jose'
|
||||
import { IRequestOptions, IRestResponse, RestClient } from 'typed-rest-client'
|
||||
|
||||
import { CONFIG } from '@/config'
|
||||
import { LogError } from '@/server/LogError'
|
||||
import { backendLogger as logger } from '@/server/logger'
|
||||
|
||||
import { PostUserLoggingView } from './logging/PostUserLogging.view'
|
||||
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
|
||||
}
|
||||
|
||||
public async createAutoLoginUrl(username: string) {
|
||||
const secret = new TextEncoder().encode(CONFIG.HUMHUB_JWT_KEY)
|
||||
logger.info(`user ${username} as username for humhub auto-login`)
|
||||
const token = await new SignJWT({ username })
|
||||
.setProtectedHeader({ alg: 'HS256' })
|
||||
.setIssuedAt()
|
||||
.setExpirationTime('2m')
|
||||
.sign(secret)
|
||||
|
||||
return `${CONFIG.HUMHUB_API_URL}user/auth/external?authclient=jwt&jwt=${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
|
||||
}
|
||||
|
||||
public async userByEmailAsync(email: string): Promise<IRestResponse<GetUser>> {
|
||||
const options = await this.createRequestOptions({ email })
|
||||
return this.restClient.get<GetUser>('/api/v1/user/get-by-email', options)
|
||||
}
|
||||
|
||||
public async userByUsernameAsync(username: string): Promise<IRestResponse<GetUser>> {
|
||||
const options = await this.createRequestOptions({ username })
|
||||
return this.restClient.get<GetUser>('/api/v1/user/get-by-username', options)
|
||||
}
|
||||
|
||||
/**
|
||||
* get user by username
|
||||
* https://marketplace.humhub.com/module/rest/docs/html/user.html#tag/User/paths/~1user~1get-by-username/get
|
||||
* @param username for user search
|
||||
* @returns user object if found
|
||||
*/
|
||||
public async userByUsername(username: string): Promise<GetUser | null> {
|
||||
const options = await this.createRequestOptions({ username })
|
||||
const response = await this.restClient.get<GetUser>('/api/v1/user/get-by-username', options)
|
||||
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> {
|
||||
logger.info('create new humhub user', new PostUserLoggingView(user))
|
||||
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> {
|
||||
logger.info('update humhub user', new PostUserLoggingView(user))
|
||||
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> {
|
||||
logger.info('delete humhub user', { userId: humhubUserId })
|
||||
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/')
|
||||
70
backend/src/apis/humhub/__mocks__/HumHubClient.ts
Normal file
70
backend/src/apis/humhub/__mocks__/HumHubClient.ts
Normal file
@ -0,0 +1,70 @@
|
||||
import { User } from '@entity/User'
|
||||
import { UserContact } from '@entity/UserContact'
|
||||
import { IRestResponse } from 'typed-rest-client'
|
||||
|
||||
import { GetUser } from '@/apis/humhub/model/GetUser'
|
||||
import { PostUser } from '@/apis/humhub/model/PostUser'
|
||||
import { UsersResponse } from '@/apis/humhub/model/UsersResponse'
|
||||
|
||||
/**
|
||||
* HumHubClient as singleton class
|
||||
*/
|
||||
export class HumHubClient {
|
||||
// eslint-disable-next-line no-use-before-define
|
||||
private static instance: HumHubClient
|
||||
|
||||
// eslint-disable-next-line no-useless-constructor, @typescript-eslint/no-empty-function
|
||||
private constructor() {}
|
||||
|
||||
public static getInstance(): HumHubClient {
|
||||
if (!HumHubClient.instance) {
|
||||
HumHubClient.instance = new HumHubClient()
|
||||
}
|
||||
return HumHubClient.instance
|
||||
}
|
||||
|
||||
public async users(): Promise<UsersResponse | null> {
|
||||
return Promise.resolve(new UsersResponse())
|
||||
}
|
||||
|
||||
public async userByEmail(email: string): Promise<GetUser | null> {
|
||||
const user = new User()
|
||||
user.emailContact = new UserContact()
|
||||
user.emailContact.email = email
|
||||
return Promise.resolve(new GetUser(user, 1))
|
||||
}
|
||||
|
||||
public async userByEmailAsync(email: string): Promise<IRestResponse<GetUser>> {
|
||||
const user = new User()
|
||||
user.emailContact = new UserContact()
|
||||
user.emailContact.email = email
|
||||
return Promise.resolve({
|
||||
statusCode: 200,
|
||||
result: new GetUser(user, 1),
|
||||
headers: {},
|
||||
})
|
||||
}
|
||||
|
||||
public async userByUsername(username: string): Promise<GetUser | null> {
|
||||
const user = new User()
|
||||
user.alias = username
|
||||
user.emailContact = new UserContact()
|
||||
user.emailContact.email = 'testemail@gmail.com'
|
||||
return Promise.resolve(new GetUser(user, 1))
|
||||
}
|
||||
|
||||
public async createUser(): Promise<void> {
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
public async updateUser(inputUser: PostUser, humhubUserId: number): Promise<GetUser | null> {
|
||||
const user = new User()
|
||||
user.emailContact = new UserContact()
|
||||
user.emailContact.email = inputUser.account.email
|
||||
return Promise.resolve(new GetUser(user, humhubUserId))
|
||||
}
|
||||
|
||||
public async deleteUser(): Promise<void> {
|
||||
return Promise.resolve()
|
||||
}
|
||||
}
|
||||
44
backend/src/apis/humhub/__mocks__/syncUser.ts
Normal file
44
backend/src/apis/humhub/__mocks__/syncUser.ts
Normal file
@ -0,0 +1,44 @@
|
||||
import { User } from '@entity/User'
|
||||
|
||||
import { isHumhubUserIdenticalToDbUser } from '@/apis/humhub/compareHumhubUserDbUser'
|
||||
import { GetUser } from '@/apis/humhub/model/GetUser'
|
||||
|
||||
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 humhubUser = humhubUsers.get(user.emailContact.email.trim())
|
||||
if (humhubUser) {
|
||||
if (!user.humhubAllowed) {
|
||||
return Promise.resolve(ExecutedHumhubAction.DELETE)
|
||||
}
|
||||
if (!isHumhubUserIdenticalToDbUser(humhubUser, user)) {
|
||||
// if humhub allowed
|
||||
return Promise.resolve(ExecutedHumhubAction.UPDATE)
|
||||
}
|
||||
} else {
|
||||
if (user.humhubAllowed) {
|
||||
return Promise.resolve(ExecutedHumhubAction.CREATE)
|
||||
}
|
||||
}
|
||||
return Promise.resolve(ExecutedHumhubAction.SKIP)
|
||||
}
|
||||
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)
|
||||
})
|
||||
})
|
||||
35
backend/src/apis/humhub/compareHumhubUserDbUser.ts
Normal file
35
backend/src/apis/humhub/compareHumhubUserDbUser.ts
Normal file
@ -0,0 +1,35 @@
|
||||
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
|
||||
if (account.status !== gradidoUserAccount.status) 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
|
||||
}
|
||||
18
backend/src/apis/humhub/logging/AccountLogging.view.ts
Normal file
18
backend/src/apis/humhub/logging/AccountLogging.view.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import { AbstractLoggingView } from '@logging/AbstractLogging.view'
|
||||
|
||||
import { Account } from '@/apis/humhub/model/Account'
|
||||
|
||||
export class AccountLoggingView extends AbstractLoggingView {
|
||||
public constructor(private self: Account) {
|
||||
super()
|
||||
}
|
||||
|
||||
public toJSON(): Account {
|
||||
return {
|
||||
username: this.self.username.substring(0, 3) + '...',
|
||||
email: this.self.email.substring(0, 3) + '...',
|
||||
language: this.self.language,
|
||||
status: this.self.status,
|
||||
}
|
||||
}
|
||||
}
|
||||
23
backend/src/apis/humhub/logging/PostUserLogging.view.ts
Normal file
23
backend/src/apis/humhub/logging/PostUserLogging.view.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import { AbstractLoggingView } from '@logging/AbstractLogging.view'
|
||||
|
||||
import { PostUser } from '@/apis/humhub/model/PostUser'
|
||||
|
||||
import { AccountLoggingView } from './AccountLogging.view'
|
||||
import { ProfileLoggingView } from './ProfileLogging.view'
|
||||
|
||||
export class PostUserLoggingView extends AbstractLoggingView {
|
||||
public constructor(private self: PostUser) {
|
||||
super()
|
||||
}
|
||||
|
||||
public toJSON(): PostUser {
|
||||
return {
|
||||
account: new AccountLoggingView(this.self.account).toJSON(),
|
||||
profile: new ProfileLoggingView(this.self.profile).toJSON(),
|
||||
password: {
|
||||
newPassword: '',
|
||||
mustChangePassword: false,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
20
backend/src/apis/humhub/logging/ProfileLogging.view.ts
Normal file
20
backend/src/apis/humhub/logging/ProfileLogging.view.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import { AbstractLoggingView } from '@logging/AbstractLogging.view'
|
||||
|
||||
import { Profile } from '@/apis/humhub/model/Profile'
|
||||
|
||||
export class ProfileLoggingView extends AbstractLoggingView {
|
||||
public constructor(private self: Profile) {
|
||||
super()
|
||||
}
|
||||
|
||||
public toJSON(): Profile {
|
||||
const gradidoAddressParts = this.self.gradido_address.split('/')
|
||||
return {
|
||||
firstname: this.self.firstname.substring(0, 3) + '...',
|
||||
lastname: this.self.lastname.substring(0, 3) + '...',
|
||||
// eslint-disable-next-line camelcase
|
||||
gradido_address:
|
||||
gradidoAddressParts[0] + '/' + gradidoAddressParts[1].substring(0, 3) + '...',
|
||||
}
|
||||
}
|
||||
}
|
||||
23
backend/src/apis/humhub/model/Account.ts
Normal file
23
backend/src/apis/humhub/model/Account.ts
Normal file
@ -0,0 +1,23 @@
|
||||
/* eslint-disable camelcase */
|
||||
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)
|
||||
this.status = 1
|
||||
}
|
||||
|
||||
username: string
|
||||
email: string
|
||||
language: string
|
||||
status: number
|
||||
}
|
||||
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[]
|
||||
}
|
||||
79
backend/src/apis/humhub/syncUser.test.ts
Normal file
79
backend/src/apis/humhub/syncUser.test.ts
Normal file
@ -0,0 +1,79 @@
|
||||
/* eslint-disable @typescript-eslint/no-empty-function */
|
||||
import { User } from '@entity/User'
|
||||
import { UserContact } from '@entity/UserContact'
|
||||
|
||||
import { GetUser } from './model/GetUser'
|
||||
import { syncUser, ExecutedHumhubAction } from './syncUser'
|
||||
|
||||
jest.mock('@/apis/humhub/HumHubClient')
|
||||
|
||||
const defaultUser = new User()
|
||||
defaultUser.emailContact = new UserContact()
|
||||
defaultUser.emailContact.email = 'email@gmail.com'
|
||||
|
||||
describe('syncUser function', () => {
|
||||
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
|
||||
}
|
||||
@ -37,6 +37,8 @@ export enum RIGHTS {
|
||||
LIST_ALL_CONTRIBUTION_MESSAGES = 'LIST_ALL_CONTRIBUTION_MESSAGES',
|
||||
OPEN_CREATIONS = 'OPEN_CREATIONS',
|
||||
USER = 'USER',
|
||||
GMS_USER_PLAYGROUND = 'GMS_USER_PLAYGROUND',
|
||||
HUMHUB_AUTO_LOGIN = 'HUMHUB_AUTO_LOGIN',
|
||||
// Moderator
|
||||
SEARCH_USERS = 'SEARCH_USERS',
|
||||
ADMIN_CREATE_CONTRIBUTION = 'ADMIN_CREATE_CONTRIBUTION',
|
||||
|
||||
@ -29,4 +29,6 @@ export const USER_RIGHTS = [
|
||||
RIGHTS.LIST_ALL_CONTRIBUTION_MESSAGES,
|
||||
RIGHTS.OPEN_CREATIONS,
|
||||
RIGHTS.USER,
|
||||
RIGHTS.GMS_USER_PLAYGROUND,
|
||||
RIGHTS.HUMHUB_AUTO_LOGIN,
|
||||
]
|
||||
|
||||
@ -12,14 +12,14 @@ Decimal.set({
|
||||
})
|
||||
|
||||
const constants = {
|
||||
DB_VERSION: '0083-join_community_federated_communities',
|
||||
DB_VERSION: '0084-introduce_humhub_registration',
|
||||
DECAY_START_TIME: new Date('2021-05-13 17:46:31-0000'), // GMT+0
|
||||
LOG4JS_CONFIG: 'log4js-config.json',
|
||||
// default log level on production should be info
|
||||
LOG_LEVEL: process.env.LOG_LEVEL ?? 'info',
|
||||
CONFIG_VERSION: {
|
||||
DEFAULT: 'DEFAULT',
|
||||
EXPECTED: 'v21.2024-01-06',
|
||||
EXPECTED: 'v23.2024-04-04',
|
||||
CURRENT: '',
|
||||
},
|
||||
}
|
||||
@ -143,8 +143,18 @@ const federation = {
|
||||
|
||||
const gms = {
|
||||
GMS_ACTIVE: process.env.GMS_ACTIVE === 'true' || false,
|
||||
GMS_CREATE_USER_THROW_ERRORS: process.env.GMS_CREATE_USER_THROW_ERRORS === 'true' || false,
|
||||
// koordinates of Illuminz-instance of GMS
|
||||
GMS_URL: process.env.GMS_HOST ?? 'http://localhost:4044/',
|
||||
GMS_API_URL: process.env.GMS_API_URL ?? 'http://localhost:4044/',
|
||||
GMS_DASHBOARD_URL: process.env.GMS_DASHBOARD_URL ?? 'http://localhost:8080/',
|
||||
// used as secret postfix attached at the gms community-auth-url endpoint ('/hook/gms/' + 'secret')
|
||||
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 = {
|
||||
@ -159,4 +169,5 @@ export const CONFIG = {
|
||||
...webhook,
|
||||
...federation,
|
||||
...gms,
|
||||
...humhub,
|
||||
}
|
||||
|
||||
61
backend/src/data/PublishName.logic.ts
Normal file
61
backend/src/data/PublishName.logic.ts
Normal file
@ -0,0 +1,61 @@
|
||||
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) {
|
||||
return this.user.firstName.substring(0, 1)
|
||||
}
|
||||
if (PublishNameType.PUBLISH_NAME_ALIAS_OR_INITALS === publishNameType) {
|
||||
if (this.user.alias) {
|
||||
return this.user.alias
|
||||
} else {
|
||||
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_FULL === publishNameType) {
|
||||
return this.user.lastName
|
||||
} else if (
|
||||
[PublishNameType.PUBLISH_NAME_FIRST_INITIAL, PublishNameType.PUBLISH_NAME_INITIALS].includes(
|
||||
publishNameType,
|
||||
)
|
||||
) {
|
||||
return this.user.lastName.substring(0, 1)
|
||||
} else if (
|
||||
PublishNameType.PUBLISH_NAME_ALIAS_OR_INITALS === publishNameType &&
|
||||
!this.user.alias
|
||||
) {
|
||||
return this.user.lastName.substring(0, 1)
|
||||
}
|
||||
|
||||
return ''
|
||||
}
|
||||
}
|
||||
@ -6,6 +6,7 @@ import { CONFIG } from '@/config'
|
||||
// eslint-disable-next-line camelcase
|
||||
import { AuthenticationClient as V1_0_AuthenticationClient } from '@/federation/client/1_0/AuthenticationClient'
|
||||
import { backendLogger as logger } from '@/server/logger'
|
||||
import { ensureUrlEndsWithSlash } from '@/util/utilities'
|
||||
|
||||
import { OpenConnectionArgs } from './client/1_0/model/OpenConnectionArgs'
|
||||
import { AuthenticationClientFactory } from './client/AuthenticationClientFactory'
|
||||
@ -39,9 +40,7 @@ export async function startCommunityAuthentication(
|
||||
const args = new OpenConnectionArgs()
|
||||
args.publicKey = homeCom.publicKey.toString('hex')
|
||||
// TODO encrypt url with foreignCom.publicKey and sign it with homeCom.privateKey
|
||||
args.url = homeFedCom.endPoint.endsWith('/')
|
||||
? homeFedCom.endPoint
|
||||
: homeFedCom.endPoint + '/' + homeFedCom.apiVersion
|
||||
args.url = ensureUrlEndsWithSlash(homeFedCom.endPoint).concat(homeFedCom.apiVersion)
|
||||
logger.debug(
|
||||
'Authentication: before client.openConnection() args:',
|
||||
homeCom.publicKey.toString('hex'),
|
||||
|
||||
@ -2,6 +2,7 @@ import { FederatedCommunity as DbFederatedCommunity } from '@entity/FederatedCom
|
||||
import { GraphQLClient } from 'graphql-request'
|
||||
|
||||
import { backendLogger as logger } from '@/server/logger'
|
||||
import { ensureUrlEndsWithSlash } from '@/util/utilities'
|
||||
|
||||
import { OpenConnectionArgs } from './model/OpenConnectionArgs'
|
||||
import { openConnection } from './query/openConnection'
|
||||
@ -13,9 +14,7 @@ export class AuthenticationClient {
|
||||
|
||||
constructor(dbCom: DbFederatedCommunity) {
|
||||
this.dbCom = dbCom
|
||||
this.endpoint = `${dbCom.endPoint.endsWith('/') ? dbCom.endPoint : dbCom.endPoint + '/'}${
|
||||
dbCom.apiVersion
|
||||
}/`
|
||||
this.endpoint = ensureUrlEndsWithSlash(dbCom.endPoint).concat(dbCom.apiVersion).concat('/')
|
||||
this.client = new GraphQLClient(this.endpoint, {
|
||||
method: 'POST',
|
||||
jsonSerializer: {
|
||||
|
||||
@ -4,6 +4,7 @@ import { GraphQLClient } from 'graphql-request'
|
||||
import { getPublicCommunityInfo } from '@/federation/client/1_0/query/getPublicCommunityInfo'
|
||||
import { getPublicKey } from '@/federation/client/1_0/query/getPublicKey'
|
||||
import { backendLogger as logger } from '@/server/logger'
|
||||
import { ensureUrlEndsWithSlash } from '@/util/utilities'
|
||||
|
||||
import { PublicCommunityInfoLoggingView } from './logging/PublicCommunityInfoLogging.view'
|
||||
import { GetPublicKeyResult } from './model/GetPublicKeyResult'
|
||||
@ -16,9 +17,7 @@ export class FederationClient {
|
||||
|
||||
constructor(dbCom: DbFederatedCommunity) {
|
||||
this.dbCom = dbCom
|
||||
this.endpoint = `${dbCom.endPoint.endsWith('/') ? dbCom.endPoint : dbCom.endPoint + '/'}${
|
||||
dbCom.apiVersion
|
||||
}/`
|
||||
this.endpoint = ensureUrlEndsWithSlash(dbCom.endPoint).concat(dbCom.apiVersion).concat('/')
|
||||
this.client = new GraphQLClient(this.endpoint, {
|
||||
method: 'GET',
|
||||
jsonSerializer: {
|
||||
|
||||
@ -3,6 +3,7 @@ import { GraphQLClient } from 'graphql-request'
|
||||
|
||||
import { LogError } from '@/server/LogError'
|
||||
import { backendLogger as logger } from '@/server/logger'
|
||||
import { ensureUrlEndsWithSlash } from '@/util/utilities'
|
||||
|
||||
import { SendCoinsArgsLoggingView } from './logging/SendCoinsArgsLogging.view'
|
||||
import { SendCoinsResultLoggingView } from './logging/SendCoinsResultLogging.view'
|
||||
@ -20,9 +21,7 @@ export class SendCoinsClient {
|
||||
|
||||
constructor(dbCom: DbFederatedCommunity) {
|
||||
this.dbCom = dbCom
|
||||
this.endpoint = `${dbCom.endPoint.endsWith('/') ? dbCom.endPoint : dbCom.endPoint + '/'}${
|
||||
dbCom.apiVersion
|
||||
}/`
|
||||
this.endpoint = ensureUrlEndsWithSlash(dbCom.endPoint).concat(dbCom.apiVersion).concat('/')
|
||||
this.client = new GraphQLClient(this.endpoint, {
|
||||
method: 'POST',
|
||||
jsonSerializer: {
|
||||
|
||||
@ -5,6 +5,7 @@ import { FederationClient as V1_0_FederationClient } from '@/federation/client/1
|
||||
// eslint-disable-next-line camelcase
|
||||
import { FederationClient as V1_1_FederationClient } from '@/federation/client/1_1/FederationClient'
|
||||
import { ApiVersionType } from '@/federation/enum/apiVersionType'
|
||||
import { ensureUrlEndsWithSlash } from '@/util/utilities'
|
||||
|
||||
// eslint-disable-next-line camelcase
|
||||
type FederationClient = V1_0_FederationClient | V1_1_FederationClient
|
||||
@ -47,10 +48,7 @@ export class FederationClientFactory {
|
||||
const instance = FederationClientFactory.instanceArray.find(
|
||||
(instance) => instance.id === dbCom.id,
|
||||
)
|
||||
// TODO: found a way to prevent double code with FederationClient::constructor
|
||||
const endpoint = `${dbCom.endPoint.endsWith('/') ? dbCom.endPoint : dbCom.endPoint + '/'}${
|
||||
dbCom.apiVersion
|
||||
}/`
|
||||
const endpoint = ensureUrlEndsWithSlash(dbCom.endPoint).concat(dbCom.apiVersion)
|
||||
// check if endpoint is still the same and not changed meanwhile
|
||||
if (instance && instance.client.getEndpoint() === endpoint) {
|
||||
return instance.client
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
import { IsBoolean, IsInt, IsString } from 'class-validator'
|
||||
import { IsBoolean, IsEnum, IsInt, IsString } from 'class-validator'
|
||||
import { ArgsType, Field, InputType, Int } from 'type-graphql'
|
||||
|
||||
import { GmsPublishLocationType } from '@enum/GmsPublishLocationType'
|
||||
import { PublishNameType } from '@enum/PublishNameType'
|
||||
import { Location } from '@model/Location'
|
||||
|
||||
import { isValidLocation } from '@/graphql/validator/Location'
|
||||
@ -44,19 +46,27 @@ export class UpdateUserInfosArgs {
|
||||
@IsBoolean()
|
||||
hideAmountGDT?: boolean
|
||||
|
||||
@Field({ nullable: true, defaultValue: true })
|
||||
@Field({ nullable: true })
|
||||
@IsBoolean()
|
||||
humhubAllowed?: boolean
|
||||
|
||||
@Field({ nullable: true })
|
||||
@IsBoolean()
|
||||
gmsAllowed?: boolean
|
||||
|
||||
@Field(() => Int, { nullable: true, defaultValue: 0 })
|
||||
@IsInt()
|
||||
gmsPublishName?: number | null
|
||||
@Field(() => PublishNameType, { nullable: true })
|
||||
@IsEnum(PublishNameType)
|
||||
gmsPublishName?: PublishNameType | null
|
||||
|
||||
@Field(() => PublishNameType, { nullable: true })
|
||||
@IsEnum(PublishNameType)
|
||||
humhubPublishName?: PublishNameType | null
|
||||
|
||||
@Field(() => Location, { nullable: true })
|
||||
@isValidLocation()
|
||||
gmsLocation?: Location | null
|
||||
|
||||
@Field(() => Int, { nullable: true, defaultValue: 2 })
|
||||
@IsInt()
|
||||
gmsPublishLocation?: number | null
|
||||
@Field(() => GmsPublishLocationType, { nullable: true })
|
||||
@IsEnum(GmsPublishLocationType)
|
||||
gmsPublishLocation?: GmsPublishLocationType | null
|
||||
}
|
||||
|
||||
@ -1,14 +0,0 @@
|
||||
import { registerEnumType } from 'type-graphql'
|
||||
|
||||
export enum GmsPublishNameType {
|
||||
GMS_PUBLISH_NAME_ALIAS_OR_INITALS = 0,
|
||||
GMS_PUBLISH_NAME_INITIALS = 1,
|
||||
GMS_PUBLISH_NAME_FIRST = 2,
|
||||
GMS_PUBLISH_NAME_FIRST_INITIAL = 3,
|
||||
GMS_PUBLISH_NAME_FULL = 4,
|
||||
}
|
||||
|
||||
registerEnumType(GmsPublishNameType, {
|
||||
name: 'GmsPublishNameType', // this one is mandatory
|
||||
description: 'Type of name publishing', // this one is optional
|
||||
})
|
||||
14
backend/src/graphql/enum/PublishNameType.ts
Normal file
14
backend/src/graphql/enum/PublishNameType.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import { registerEnumType } from 'type-graphql'
|
||||
|
||||
export enum PublishNameType {
|
||||
PUBLISH_NAME_ALIAS_OR_INITALS = 0,
|
||||
PUBLISH_NAME_INITIALS = 1,
|
||||
PUBLISH_NAME_FIRST = 2,
|
||||
PUBLISH_NAME_FIRST_INITIAL = 3,
|
||||
PUBLISH_NAME_FULL = 4,
|
||||
}
|
||||
|
||||
registerEnumType(PublishNameType, {
|
||||
name: 'PublishNameType', // this one is mandatory
|
||||
description: 'Type of name publishing', // this one is optional
|
||||
})
|
||||
@ -1,6 +1,8 @@
|
||||
import { FederatedCommunity as DbFederatedCommunity } from '@entity/FederatedCommunity'
|
||||
import { ObjectType, Field, Int } from 'type-graphql'
|
||||
|
||||
import { ensureUrlEndsWithSlash } from '@/util/utilities'
|
||||
|
||||
@ObjectType()
|
||||
export class FederatedCommunity {
|
||||
constructor(dbCom: DbFederatedCommunity) {
|
||||
@ -8,7 +10,7 @@ export class FederatedCommunity {
|
||||
this.foreign = dbCom.foreign
|
||||
this.publicKey = dbCom.publicKey.toString('hex')
|
||||
this.apiVersion = dbCom.apiVersion
|
||||
this.endPoint = dbCom.endPoint.endsWith('/') ? dbCom.endPoint : dbCom.endPoint + '/'
|
||||
this.endPoint = ensureUrlEndsWithSlash(dbCom.endPoint)
|
||||
this.lastAnnouncedAt = dbCom.lastAnnouncedAt
|
||||
this.verifiedAt = dbCom.verifiedAt
|
||||
this.lastErrorAt = dbCom.lastErrorAt
|
||||
|
||||
10
backend/src/graphql/model/GmsUserAuthenticationResult.ts
Normal file
10
backend/src/graphql/model/GmsUserAuthenticationResult.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import { Field, ObjectType } from 'type-graphql'
|
||||
|
||||
@ObjectType()
|
||||
export class GmsUserAuthenticationResult {
|
||||
@Field(() => String)
|
||||
url: string
|
||||
|
||||
@Field(() => String)
|
||||
token: string
|
||||
}
|
||||
@ -1,6 +1,9 @@
|
||||
import { User as dbUser } from '@entity/User'
|
||||
import { ObjectType, Field, Int } from 'type-graphql'
|
||||
|
||||
import { GmsPublishLocationType } from '@enum/GmsPublishLocationType'
|
||||
import { PublishNameType } from '@enum/PublishNameType'
|
||||
|
||||
import { KlickTipp } from './KlickTipp'
|
||||
|
||||
@ObjectType()
|
||||
@ -29,6 +32,11 @@ export class User {
|
||||
this.hasElopage = null
|
||||
this.hideAmountGDD = user.hideAmountGDD
|
||||
this.hideAmountGDT = user.hideAmountGDT
|
||||
this.humhubAllowed = user.humhubAllowed
|
||||
this.gmsAllowed = user.gmsAllowed
|
||||
this.gmsPublishName = user.gmsPublishName
|
||||
this.humhubPublishName = user.humhubPublishName
|
||||
this.gmsPublishLocation = user.gmsPublishLocation
|
||||
}
|
||||
}
|
||||
|
||||
@ -74,6 +82,21 @@ export class User {
|
||||
@Field(() => Boolean)
|
||||
hideAmountGDT: boolean
|
||||
|
||||
@Field(() => Boolean)
|
||||
humhubAllowed: boolean
|
||||
|
||||
@Field(() => Boolean)
|
||||
gmsAllowed: boolean
|
||||
|
||||
@Field(() => PublishNameType, { nullable: true })
|
||||
gmsPublishName: PublishNameType | null
|
||||
|
||||
@Field(() => PublishNameType, { nullable: true })
|
||||
humhubPublishName: PublishNameType | null
|
||||
|
||||
@Field(() => GmsPublishLocationType, { nullable: true })
|
||||
gmsPublishLocation: GmsPublishLocationType | null
|
||||
|
||||
// This is not the users publisherId, but the one of the users who recommend him
|
||||
@Field(() => Int, { nullable: true })
|
||||
publisherId: number | null
|
||||
|
||||
@ -15,8 +15,6 @@ import { ApolloServerTestClient } from 'apollo-server-testing'
|
||||
import { GraphQLError } from 'graphql'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
|
||||
import { GmsPublishLocationType } from '@enum/GmsPublishLocationType'
|
||||
import { GmsPublishNameType } from '@enum/GmsPublishNameType'
|
||||
import { cleanDB, testEnvironment } from '@test/helpers'
|
||||
import { logger } from '@test/testSetup'
|
||||
|
||||
@ -530,13 +528,12 @@ describe('send coins', () => {
|
||||
|
||||
describe('send coins via alias', () => {
|
||||
beforeAll(async () => {
|
||||
// first set alias to null, because updating alias isn't allowed
|
||||
await User.update({ alias: 'MeisterBob' }, { alias: () => 'NULL' })
|
||||
await mutate({
|
||||
mutation: updateUserInfos,
|
||||
variables: {
|
||||
alias: 'bob',
|
||||
gmsAllowed: true,
|
||||
gmsPublishName: GmsPublishNameType.GMS_PUBLISH_NAME_ALIAS_OR_INITALS,
|
||||
gmsPublishLocation: GmsPublishLocationType.GMS_LOCATION_TYPE_RANDOM,
|
||||
},
|
||||
})
|
||||
await mutate({
|
||||
|
||||
@ -17,7 +17,6 @@ import { GraphQLError } from 'graphql'
|
||||
import { v4 as uuidv4, validate as validateUUID, version as versionUUID } from 'uuid'
|
||||
|
||||
import { GmsPublishLocationType } from '@enum/GmsPublishLocationType'
|
||||
import { GmsPublishNameType } from '@enum/GmsPublishNameType'
|
||||
import { OptInType } from '@enum/OptInType'
|
||||
import { PasswordEncryptionType } from '@enum/PasswordEncryptionType'
|
||||
import { RoleNames } from '@enum/RoleNames'
|
||||
@ -35,6 +34,7 @@ import {
|
||||
sendResetPasswordEmail,
|
||||
} from '@/emails/sendEmailVariants'
|
||||
import { EventType } from '@/event/Events'
|
||||
import { PublishNameType } from '@/graphql/enum/PublishNameType'
|
||||
import { SecretKeyCryptographyCreateKey } from '@/password/EncryptorUtils'
|
||||
import { encryptPassword } from '@/password/PasswordEncryptor'
|
||||
import { writeHomeCommunityEntry } from '@/seeds/community'
|
||||
@ -73,6 +73,8 @@ import { objectValuesToArray } from '@/util/utilities'
|
||||
|
||||
import { Location2Point } from './util/Location2Point'
|
||||
|
||||
jest.mock('@/apis/humhub/HumHubClient')
|
||||
|
||||
jest.mock('@/emails/sendEmailVariants', () => {
|
||||
const originalModule = jest.requireActual('@/emails/sendEmailVariants')
|
||||
return {
|
||||
@ -183,7 +185,9 @@ describe('UserResolver', () => {
|
||||
communityUuid: homeCom.communityUuid,
|
||||
foreign: false,
|
||||
gmsAllowed: true,
|
||||
humhubAllowed: false,
|
||||
gmsPublishName: 0,
|
||||
humhubPublishName: 0,
|
||||
gmsPublishLocation: 2,
|
||||
location: null,
|
||||
gmsRegistered: false,
|
||||
@ -1230,7 +1234,7 @@ describe('UserResolver', () => {
|
||||
lastName: 'Blümchen',
|
||||
language: 'en',
|
||||
gmsAllowed: true,
|
||||
gmsPublishName: GmsPublishNameType.GMS_PUBLISH_NAME_ALIAS_OR_INITALS,
|
||||
gmsPublishName: PublishNameType.PUBLISH_NAME_ALIAS_OR_INITALS,
|
||||
gmsPublishLocation: GmsPublishLocationType.GMS_LOCATION_TYPE_RANDOM,
|
||||
}),
|
||||
])
|
||||
@ -1258,6 +1262,8 @@ describe('UserResolver', () => {
|
||||
|
||||
describe('valid alias', () => {
|
||||
it('updates the user in DB', async () => {
|
||||
// first empty alias, because currently updating alias isn't allowed
|
||||
await User.update({ alias: 'BBB' }, { alias: () => 'NULL' })
|
||||
await mutate({
|
||||
mutation: updateUserInfos,
|
||||
variables: {
|
||||
@ -1268,7 +1274,7 @@ describe('UserResolver', () => {
|
||||
expect.objectContaining({
|
||||
alias: 'bibi_Bloxberg',
|
||||
gmsAllowed: true,
|
||||
gmsPublishName: GmsPublishNameType.GMS_PUBLISH_NAME_ALIAS_OR_INITALS,
|
||||
gmsPublishName: PublishNameType.PUBLISH_NAME_ALIAS_OR_INITALS,
|
||||
gmsPublishLocation: GmsPublishLocationType.GMS_LOCATION_TYPE_RANDOM,
|
||||
}),
|
||||
])
|
||||
@ -1290,7 +1296,7 @@ describe('UserResolver', () => {
|
||||
await expect(User.find()).resolves.toEqual([
|
||||
expect.objectContaining({
|
||||
gmsAllowed: true,
|
||||
gmsPublishName: GmsPublishNameType.GMS_PUBLISH_NAME_ALIAS_OR_INITALS,
|
||||
gmsPublishName: PublishNameType.PUBLISH_NAME_ALIAS_OR_INITALS,
|
||||
gmsPublishLocation: GmsPublishLocationType.GMS_LOCATION_TYPE_RANDOM,
|
||||
}),
|
||||
])
|
||||
@ -1303,14 +1309,15 @@ describe('UserResolver', () => {
|
||||
mutation: updateUserInfos,
|
||||
variables: {
|
||||
gmsAllowed: false,
|
||||
gmsPublishName: GmsPublishNameType.GMS_PUBLISH_NAME_FIRST_INITIAL,
|
||||
gmsPublishLocation: GmsPublishLocationType.GMS_LOCATION_TYPE_APPROXIMATE,
|
||||
gmsPublishName: PublishNameType[PublishNameType.PUBLISH_NAME_FIRST_INITIAL],
|
||||
gmsPublishLocation:
|
||||
GmsPublishLocationType[GmsPublishLocationType.GMS_LOCATION_TYPE_APPROXIMATE],
|
||||
},
|
||||
})
|
||||
await expect(User.find()).resolves.toEqual([
|
||||
expect.objectContaining({
|
||||
gmsAllowed: false,
|
||||
gmsPublishName: GmsPublishNameType.GMS_PUBLISH_NAME_FIRST_INITIAL,
|
||||
gmsPublishName: PublishNameType.PUBLISH_NAME_FIRST_INITIAL,
|
||||
gmsPublishLocation: GmsPublishLocationType.GMS_LOCATION_TYPE_APPROXIMATE,
|
||||
}),
|
||||
])
|
||||
@ -1326,15 +1333,16 @@ describe('UserResolver', () => {
|
||||
mutation: updateUserInfos,
|
||||
variables: {
|
||||
gmsAllowed: true,
|
||||
gmsPublishName: GmsPublishNameType.GMS_PUBLISH_NAME_ALIAS_OR_INITALS,
|
||||
gmsPublishName: PublishNameType[PublishNameType.PUBLISH_NAME_ALIAS_OR_INITALS],
|
||||
gmsLocation: loc,
|
||||
gmsPublishLocation: GmsPublishLocationType.GMS_LOCATION_TYPE_RANDOM,
|
||||
gmsPublishLocation:
|
||||
GmsPublishLocationType[GmsPublishLocationType.GMS_LOCATION_TYPE_RANDOM],
|
||||
},
|
||||
})
|
||||
await expect(User.find()).resolves.toEqual([
|
||||
expect.objectContaining({
|
||||
gmsAllowed: true,
|
||||
gmsPublishName: GmsPublishNameType.GMS_PUBLISH_NAME_ALIAS_OR_INITALS,
|
||||
gmsPublishName: PublishNameType.PUBLISH_NAME_ALIAS_OR_INITALS,
|
||||
location: Location2Point(loc),
|
||||
gmsPublishLocation: GmsPublishLocationType.GMS_LOCATION_TYPE_RANDOM,
|
||||
}),
|
||||
@ -2670,13 +2678,12 @@ describe('UserResolver', () => {
|
||||
mutation: login,
|
||||
variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' },
|
||||
})
|
||||
// first set alias to null, because updating alias isn't currently allowed
|
||||
await User.update({ alias: 'BBB' }, { alias: () => 'NULL' })
|
||||
await mutate({
|
||||
mutation: updateUserInfos,
|
||||
variables: {
|
||||
alias: 'bibi',
|
||||
gmsAllowed: true,
|
||||
gmsPublishName: GmsPublishNameType.GMS_PUBLISH_NAME_ALIAS_OR_INITALS,
|
||||
gmsPublishLocation: GmsPublishLocationType.GMS_LOCATION_TYPE_RANDOM,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
@ -10,6 +10,7 @@ import { UserContact as DbUserContact } from '@entity/UserContact'
|
||||
import { UserRole } from '@entity/UserRole'
|
||||
import i18n from 'i18n'
|
||||
import { Resolver, Query, Args, Arg, Authorized, Ctx, Mutation, Int } from 'type-graphql'
|
||||
import { IRestResponse } from 'typed-rest-client'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
|
||||
import { UserArgs } from '@arg//UserArgs'
|
||||
@ -19,18 +20,21 @@ import { SearchUsersFilters } from '@arg/SearchUsersFilters'
|
||||
import { SetUserRoleArgs } from '@arg/SetUserRoleArgs'
|
||||
import { UnsecureLoginArgs } from '@arg/UnsecureLoginArgs'
|
||||
import { UpdateUserInfosArgs } from '@arg/UpdateUserInfosArgs'
|
||||
import { GmsPublishLocationType } from '@enum/GmsPublishLocationType'
|
||||
import { GmsPublishNameType } from '@enum/GmsPublishNameType'
|
||||
import { OptInType } from '@enum/OptInType'
|
||||
import { Order } from '@enum/Order'
|
||||
import { PasswordEncryptionType } from '@enum/PasswordEncryptionType'
|
||||
import { UserContactType } from '@enum/UserContactType'
|
||||
import { SearchAdminUsersResult } from '@model/AdminUser'
|
||||
// import { Location } from '@model/Location'
|
||||
import { GmsUserAuthenticationResult } from '@model/GmsUserAuthenticationResult'
|
||||
import { User } from '@model/User'
|
||||
import { UserAdmin, SearchUsersResult } from '@model/UserAdmin'
|
||||
|
||||
import { DltConnectorClient } from '@/apis/dltConnector/DltConnectorClient'
|
||||
import { updateGmsUser } from '@/apis/gms/GmsClient'
|
||||
import { GmsUser } from '@/apis/gms/model/GmsUser'
|
||||
import { HumHubClient } from '@/apis/humhub/HumHubClient'
|
||||
import { GetUser } from '@/apis/humhub/model/GetUser'
|
||||
import { subscribe } from '@/apis/KlicktippController'
|
||||
import { encode } from '@/auth/JWT'
|
||||
import { RIGHTS } from '@/auth/RIGHTS'
|
||||
@ -69,7 +73,9 @@ import random from 'random-bigint'
|
||||
import { randombytes_random } from 'sodium-native'
|
||||
|
||||
import { FULL_CREATION_AVAILABLE } from './const/const'
|
||||
import { authenticateGmsUserPlayground } from './util/authenticateGmsUserPlayground'
|
||||
import { getHomeCommunity } from './util/communities'
|
||||
import { compareGmsRelevantUserSettings } from './util/compareGmsRelevantUserSettings'
|
||||
import { getUserCreations } from './util/creations'
|
||||
import { findUserByIdentifier } from './util/findUserByIdentifier'
|
||||
import { findUsers } from './util/findUsers'
|
||||
@ -77,6 +83,7 @@ import { getKlicktippState } from './util/getKlicktippState'
|
||||
import { Location2Point } from './util/Location2Point'
|
||||
import { setUserRole, deleteUserRole } from './util/modifyUserRole'
|
||||
import { sendUserToGms } from './util/sendUserToGms'
|
||||
import { syncHumhub } from './util/syncHumhub'
|
||||
import { validateAlias } from './util/validateAlias'
|
||||
|
||||
const LANGUAGES = ['de', 'en', 'es', 'fr', 'nl']
|
||||
@ -154,11 +161,19 @@ export class UserResolver {
|
||||
// TODO we want to catch this on the frontend and ask the user to check his emails or resend code
|
||||
throw new LogError('The User has not set a password yet', dbUser)
|
||||
}
|
||||
|
||||
if (!verifyPassword(dbUser, password)) {
|
||||
throw new LogError('No user with this credentials', dbUser)
|
||||
}
|
||||
|
||||
// request to humhub and klicktipp run in parallel
|
||||
let humhubUserPromise: Promise<IRestResponse<GetUser>> | undefined
|
||||
const klicktippStatePromise = getKlicktippState(dbUser.emailContact.email)
|
||||
if (CONFIG.HUMHUB_ACTIVE && dbUser.humhubAllowed) {
|
||||
humhubUserPromise = HumHubClient.getInstance()?.userByUsernameAsync(
|
||||
dbUser.alias ?? dbUser.gradidoID,
|
||||
)
|
||||
}
|
||||
|
||||
if (dbUser.passwordEncryptionType !== PasswordEncryptionType.GRADIDO_ID) {
|
||||
dbUser.passwordEncryptionType = PasswordEncryptionType.GRADIDO_ID
|
||||
dbUser.password = encryptPassword(dbUser, password)
|
||||
@ -180,7 +195,6 @@ export class UserResolver {
|
||||
dbUser.publisherId = publisherId
|
||||
await DbUser.save(dbUser)
|
||||
}
|
||||
user.klickTipp = await getKlicktippState(dbUser.emailContact.email)
|
||||
|
||||
context.setHeaders.push({
|
||||
key: 'token',
|
||||
@ -188,6 +202,12 @@ export class UserResolver {
|
||||
})
|
||||
|
||||
await EVENT_USER_LOGIN(dbUser)
|
||||
// load humhub state
|
||||
if (humhubUserPromise) {
|
||||
const result = await humhubUserPromise
|
||||
user.humhubAllowed = result?.result?.account.status === 1
|
||||
}
|
||||
user.klickTipp = await klicktippStatePromise
|
||||
logger.info(`successful Login: ${JSON.stringify(user, null, 2)}`)
|
||||
return user
|
||||
}
|
||||
@ -382,7 +402,11 @@ export class UserResolver {
|
||||
await sendUserToGms(dbUser, homeCom)
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error('Error publishing new created user to GMS:', err)
|
||||
if (CONFIG.GMS_CREATE_USER_THROW_ERRORS) {
|
||||
throw new LogError('Error publishing new created user to GMS:', err)
|
||||
} else {
|
||||
logger.error('Error publishing new created user to GMS:', err)
|
||||
}
|
||||
}
|
||||
}
|
||||
return new User(dbUser)
|
||||
@ -394,15 +418,14 @@ export class UserResolver {
|
||||
logger.addContext('user', 'unknown')
|
||||
logger.info(`forgotPassword(${email})...`)
|
||||
email = email.trim().toLowerCase()
|
||||
const user = await findUserByEmail(email).catch(() => {
|
||||
logger.warn(`fail on find UserContact per ${email}`)
|
||||
const user = await findUserByEmail(email).catch((error) => {
|
||||
logger.warn(`fail on find UserContact per ${email} because: ${error}`)
|
||||
})
|
||||
|
||||
if (!user || user.deletedAt) {
|
||||
logger.warn(`no user found with ${email}`)
|
||||
return true
|
||||
}
|
||||
|
||||
if (!canEmailResend(user.emailContact.updatedAt || user.emailContact.createdAt)) {
|
||||
throw new LogError(
|
||||
`Email already sent less than ${printTimeDuration(CONFIG.EMAIL_CODE_REQUEST_TIME)} ago`,
|
||||
@ -548,8 +571,10 @@ export class UserResolver {
|
||||
@Authorized([RIGHTS.UPDATE_USER_INFOS])
|
||||
@Mutation(() => Boolean)
|
||||
async updateUserInfos(
|
||||
@Args()
|
||||
{
|
||||
@Args() updateUserInfosArgs: UpdateUserInfosArgs,
|
||||
@Ctx() context: Context,
|
||||
): Promise<boolean> {
|
||||
const {
|
||||
firstName,
|
||||
lastName,
|
||||
alias,
|
||||
@ -558,28 +583,19 @@ export class UserResolver {
|
||||
passwordNew,
|
||||
hideAmountGDD,
|
||||
hideAmountGDT,
|
||||
humhubAllowed,
|
||||
gmsAllowed,
|
||||
gmsPublishName,
|
||||
humhubPublishName,
|
||||
gmsLocation,
|
||||
gmsPublishLocation,
|
||||
}: UpdateUserInfosArgs,
|
||||
@Ctx() context: Context,
|
||||
): Promise<boolean> {
|
||||
} = updateUserInfosArgs
|
||||
logger.info(
|
||||
`updateUserInfos(${firstName}, ${lastName}, ${alias}, ${language}, ***, ***, ${hideAmountGDD}, ${hideAmountGDT}, ${gmsAllowed}, ${gmsPublishName}, ${gmsLocation}, ${gmsPublishLocation})...`,
|
||||
)
|
||||
// check default arg settings
|
||||
if (gmsAllowed === null || gmsAllowed === undefined) {
|
||||
gmsAllowed = true
|
||||
}
|
||||
if (!gmsPublishName) {
|
||||
gmsPublishName = GmsPublishNameType.GMS_PUBLISH_NAME_ALIAS_OR_INITALS
|
||||
}
|
||||
if (!gmsPublishLocation) {
|
||||
gmsPublishLocation = GmsPublishLocationType.GMS_LOCATION_TYPE_RANDOM
|
||||
}
|
||||
|
||||
const user = getUser(context)
|
||||
const updateUserInGMS = compareGmsRelevantUserSettings(user, updateUserInfosArgs)
|
||||
|
||||
// try {
|
||||
if (firstName) {
|
||||
user.firstName = firstName
|
||||
@ -589,7 +605,8 @@ export class UserResolver {
|
||||
user.lastName = lastName
|
||||
}
|
||||
|
||||
if (alias && (await validateAlias(alias))) {
|
||||
// currently alias can only be set, not updated
|
||||
if (alias && !user.alias && (await validateAlias(alias))) {
|
||||
user.alias = alias
|
||||
}
|
||||
|
||||
@ -626,13 +643,24 @@ export class UserResolver {
|
||||
if (hideAmountGDT !== undefined) {
|
||||
user.hideAmountGDT = hideAmountGDT
|
||||
}
|
||||
|
||||
user.gmsAllowed = gmsAllowed
|
||||
user.gmsPublishName = gmsPublishName
|
||||
if (humhubAllowed !== undefined) {
|
||||
user.humhubAllowed = humhubAllowed
|
||||
}
|
||||
if (gmsAllowed !== undefined) {
|
||||
user.gmsAllowed = gmsAllowed
|
||||
}
|
||||
if (gmsPublishName !== null && gmsPublishName !== undefined) {
|
||||
user.gmsPublishName = gmsPublishName
|
||||
}
|
||||
if (humhubPublishName !== null && humhubPublishName !== undefined) {
|
||||
user.humhubPublishName = humhubPublishName
|
||||
}
|
||||
if (gmsLocation) {
|
||||
user.location = Location2Point(gmsLocation)
|
||||
}
|
||||
user.gmsPublishLocation = gmsPublishLocation
|
||||
if (gmsPublishLocation !== null && gmsPublishLocation !== undefined) {
|
||||
user.gmsPublishLocation = gmsPublishLocation
|
||||
}
|
||||
// } catch (err) {
|
||||
// console.log('error:', err)
|
||||
// }
|
||||
@ -656,6 +684,20 @@ export class UserResolver {
|
||||
logger.info('updateUserInfos() successfully finished...')
|
||||
await EVENT_USER_INFO_UPDATE(user)
|
||||
|
||||
// validate if user settings are changed with relevance to update gms-user
|
||||
if (CONFIG.GMS_ACTIVE && updateUserInGMS) {
|
||||
logger.debug(`changed user-settings relevant for gms-user update...`)
|
||||
const homeCom = await getHomeCommunity()
|
||||
if (homeCom.gmsApiKey !== null) {
|
||||
logger.debug(`gms-user update...`, user)
|
||||
await updateGmsUser(homeCom.gmsApiKey, new GmsUser(user))
|
||||
logger.debug(`gms-user update successfully.`)
|
||||
}
|
||||
}
|
||||
if (CONFIG.HUMHUB_ACTIVE) {
|
||||
await syncHumhub(updateUserInfosArgs, user)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
@ -669,6 +711,44 @@ export class UserResolver {
|
||||
return elopageBuys
|
||||
}
|
||||
|
||||
@Authorized([RIGHTS.GMS_USER_PLAYGROUND])
|
||||
@Query(() => GmsUserAuthenticationResult)
|
||||
async authenticateGmsUserSearch(@Ctx() context: Context): Promise<GmsUserAuthenticationResult> {
|
||||
logger.info(`authUserForGmsUserSearch()...`)
|
||||
const dbUser = getUser(context)
|
||||
let result: GmsUserAuthenticationResult
|
||||
if (context.token) {
|
||||
result = await authenticateGmsUserPlayground(context.token, dbUser)
|
||||
logger.info('authUserForGmsUserSearch=', result)
|
||||
} else {
|
||||
throw new LogError('authUserForGmsUserSearch without token')
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
@Authorized([RIGHTS.HUMHUB_AUTO_LOGIN])
|
||||
@Query(() => String)
|
||||
async authenticateHumhubAutoLogin(@Ctx() context: Context): Promise<string> {
|
||||
logger.info(`authenticateHumhubAutoLogin()...`)
|
||||
const dbUser = getUser(context)
|
||||
const humhubClient = HumHubClient.getInstance()
|
||||
if (!humhubClient) {
|
||||
throw new LogError('cannot create humhub client')
|
||||
}
|
||||
const username = dbUser.alias ?? dbUser.gradidoID
|
||||
let humhubUser = await humhubClient.userByUsername(username)
|
||||
if (!humhubUser) {
|
||||
humhubUser = await humhubClient.userByEmail(dbUser.emailContact.email)
|
||||
}
|
||||
if (!humhubUser) {
|
||||
throw new LogError("user don't exist (any longer) on humhub")
|
||||
}
|
||||
if (humhubUser.account.status !== 1) {
|
||||
throw new LogError('user status is not 1', humhubUser.account.status)
|
||||
}
|
||||
return await humhubClient.createAutoLoginUrl(username)
|
||||
}
|
||||
|
||||
@Authorized([RIGHTS.SEARCH_ADMIN_USERS])
|
||||
@Query(() => SearchAdminUsersResult)
|
||||
async searchAdminUsers(
|
||||
|
||||
@ -0,0 +1,20 @@
|
||||
import { User as DbUser } from '@entity/User'
|
||||
|
||||
import { verifyAuthToken } from '@/apis/gms/GmsClient'
|
||||
import { CONFIG } from '@/config'
|
||||
import { GmsUserAuthenticationResult } from '@/graphql/model/GmsUserAuthenticationResult'
|
||||
import { backendLogger as logger } from '@/server/logger'
|
||||
import { ensureUrlEndsWithSlash } from '@/util/utilities'
|
||||
|
||||
export async function authenticateGmsUserPlayground(
|
||||
token: string,
|
||||
dbUser: DbUser,
|
||||
): Promise<GmsUserAuthenticationResult> {
|
||||
const result = new GmsUserAuthenticationResult()
|
||||
const dashboardUrl = ensureUrlEndsWithSlash(CONFIG.GMS_DASHBOARD_URL)
|
||||
|
||||
result.url = dashboardUrl.concat('playground')
|
||||
result.token = await verifyAuthToken(dbUser.communityUuid, token)
|
||||
logger.info('GmsUserAuthenticationResult:', result)
|
||||
return result
|
||||
}
|
||||
@ -0,0 +1,89 @@
|
||||
import { Point } from '@dbTools/typeorm'
|
||||
import { User as DbUser } from '@entity/User'
|
||||
|
||||
import { UpdateUserInfosArgs } from '@/graphql/arg/UpdateUserInfosArgs'
|
||||
import { PublishNameType } from '@/graphql/enum/PublishNameType'
|
||||
import { LogError } from '@/server/LogError'
|
||||
import { backendLogger as logger } from '@/server/logger'
|
||||
|
||||
import { Point2Location } from './Location2Point'
|
||||
|
||||
export function compareGmsRelevantUserSettings(
|
||||
orgUser: DbUser,
|
||||
updateUserInfosArgs: UpdateUserInfosArgs,
|
||||
): boolean {
|
||||
if (!orgUser) {
|
||||
throw new LogError('comparison without any user is impossible')
|
||||
}
|
||||
logger.debug('compareGmsRelevantUserSettings:', orgUser, updateUserInfosArgs)
|
||||
// nach GMS updaten, wenn alias gesetzt wird oder ist und PublishLevel die alias-Übermittlung erlaubt
|
||||
if (
|
||||
updateUserInfosArgs.alias &&
|
||||
orgUser.alias !== updateUserInfosArgs.alias &&
|
||||
((updateUserInfosArgs.gmsPublishName &&
|
||||
updateUserInfosArgs.gmsPublishName.valueOf ===
|
||||
PublishNameType.PUBLISH_NAME_ALIAS_OR_INITALS.valueOf) ||
|
||||
(!updateUserInfosArgs.gmsPublishName &&
|
||||
orgUser.gmsPublishName &&
|
||||
orgUser.gmsPublishName.valueOf === PublishNameType.PUBLISH_NAME_ALIAS_OR_INITALS.valueOf))
|
||||
) {
|
||||
return true
|
||||
}
|
||||
if (
|
||||
(updateUserInfosArgs.firstName && orgUser.firstName !== updateUserInfosArgs.firstName) ||
|
||||
(updateUserInfosArgs.lastName && orgUser.lastName !== updateUserInfosArgs.lastName)
|
||||
) {
|
||||
return true
|
||||
}
|
||||
if (
|
||||
updateUserInfosArgs.gmsAllowed !== undefined &&
|
||||
updateUserInfosArgs.gmsAllowed &&
|
||||
orgUser.gmsAllowed !== updateUserInfosArgs.gmsAllowed
|
||||
) {
|
||||
return true
|
||||
}
|
||||
if (
|
||||
updateUserInfosArgs.gmsPublishLocation &&
|
||||
orgUser.gmsPublishLocation !== updateUserInfosArgs.gmsPublishLocation
|
||||
) {
|
||||
return true
|
||||
}
|
||||
if (
|
||||
updateUserInfosArgs.gmsPublishName &&
|
||||
orgUser.gmsPublishName !== updateUserInfosArgs.gmsPublishName
|
||||
) {
|
||||
return true
|
||||
}
|
||||
if (updateUserInfosArgs.language && orgUser.language !== updateUserInfosArgs.language) {
|
||||
return true
|
||||
}
|
||||
if (
|
||||
updateUserInfosArgs.gmsLocation &&
|
||||
orgUser.location === null &&
|
||||
updateUserInfosArgs.gmsLocation !== null
|
||||
) {
|
||||
return true
|
||||
}
|
||||
if (
|
||||
updateUserInfosArgs.gmsLocation &&
|
||||
orgUser.location !== null &&
|
||||
updateUserInfosArgs.gmsLocation === null
|
||||
) {
|
||||
return true
|
||||
}
|
||||
if (
|
||||
updateUserInfosArgs.gmsLocation &&
|
||||
orgUser.location !== null &&
|
||||
updateUserInfosArgs.gmsLocation !== null
|
||||
) {
|
||||
const orgLocation = Point2Location(orgUser.location as Point)
|
||||
const changedLocation = updateUserInfosArgs.gmsLocation
|
||||
if (
|
||||
orgLocation.latitude !== changedLocation.latitude ||
|
||||
orgLocation.longitude !== changedLocation.longitude
|
||||
) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@ -3,6 +3,7 @@ import { User as DbUser } from '@entity/User'
|
||||
|
||||
import { createGmsUser } from '@/apis/gms/GmsClient'
|
||||
import { GmsUser } from '@/apis/gms/model/GmsUser'
|
||||
import { CONFIG } from '@/config'
|
||||
import { LogError } from '@/server/LogError'
|
||||
import { backendLogger as logger } from '@/server/logger'
|
||||
|
||||
@ -22,6 +23,10 @@ export async function sendUserToGms(user: DbUser, homeCom: DbCommunity): Promise
|
||||
logger.debug('mark user as gms published:', user)
|
||||
}
|
||||
} catch (err) {
|
||||
logger.warn('publishing user fails with ', err)
|
||||
if (CONFIG.GMS_CREATE_USER_THROW_ERRORS) {
|
||||
throw new LogError('publishing user fails with ', err)
|
||||
} else {
|
||||
logger.warn('publishing user fails with ', err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
55
backend/src/graphql/resolver/util/syncHumhub.test.ts
Normal file
55
backend/src/graphql/resolver/util/syncHumhub.test.ts
Normal file
@ -0,0 +1,55 @@
|
||||
import { User } from '@entity/User'
|
||||
import { UserContact } from '@entity/UserContact'
|
||||
|
||||
import { HumHubClient } from '@/apis/humhub/HumHubClient'
|
||||
import { GetUser } from '@/apis/humhub/model/GetUser'
|
||||
import { UpdateUserInfosArgs } from '@/graphql/arg/UpdateUserInfosArgs'
|
||||
import { PublishNameType } from '@/graphql/enum/PublishNameType'
|
||||
import { backendLogger as logger } from '@/server/logger'
|
||||
|
||||
import { syncHumhub } from './syncHumhub'
|
||||
|
||||
jest.mock('@/apis/humhub/HumHubClient')
|
||||
jest.mock('@/apis/humhub/syncUser')
|
||||
|
||||
const mockUser = new User()
|
||||
mockUser.humhubAllowed = true
|
||||
mockUser.emailContact = new UserContact()
|
||||
mockUser.emailContact.email = 'email@gmail.com'
|
||||
mockUser.humhubPublishName = PublishNameType.PUBLISH_NAME_FULL
|
||||
const mockUpdateUserInfosArg = new UpdateUserInfosArgs()
|
||||
const mockHumHubUser = new GetUser(mockUser, 1)
|
||||
|
||||
describe('syncHumhub', () => {
|
||||
beforeEach(() => {
|
||||
jest.spyOn(logger, 'debug').mockImplementation()
|
||||
jest.spyOn(logger, 'info').mockImplementation()
|
||||
jest.spyOn(HumHubClient, 'getInstance')
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
jest.resetAllMocks()
|
||||
})
|
||||
|
||||
it('Should not sync if no relevant changes', async () => {
|
||||
await syncHumhub(mockUpdateUserInfosArg, new User())
|
||||
expect(HumHubClient.getInstance).not.toBeCalled()
|
||||
// language logging from some other place
|
||||
expect(logger.debug).toBeCalledTimes(5)
|
||||
expect(logger.info).toBeCalledTimes(0)
|
||||
})
|
||||
|
||||
it('Should retrieve user from humhub and sync if relevant changes', async () => {
|
||||
mockUpdateUserInfosArg.firstName = 'New' // Relevant changes
|
||||
mockUser.firstName = 'New'
|
||||
await syncHumhub(mockUpdateUserInfosArg, mockUser)
|
||||
expect(logger.debug).toHaveBeenCalledTimes(8) // Four language logging calls, two debug calls in function, one for not syncing
|
||||
expect(logger.info).toHaveBeenLastCalledWith('finished sync user with humhub', {
|
||||
localId: mockUser.id,
|
||||
externId: mockHumHubUser.id,
|
||||
result: 'UPDATE',
|
||||
})
|
||||
})
|
||||
|
||||
// Add more test cases as needed...
|
||||
})
|
||||
49
backend/src/graphql/resolver/util/syncHumhub.ts
Normal file
49
backend/src/graphql/resolver/util/syncHumhub.ts
Normal file
@ -0,0 +1,49 @@
|
||||
import { User } from '@entity/User'
|
||||
|
||||
import { HumHubClient } from '@/apis/humhub/HumHubClient'
|
||||
import { GetUser } from '@/apis/humhub/model/GetUser'
|
||||
import { ExecutedHumhubAction, syncUser } from '@/apis/humhub/syncUser'
|
||||
import { UpdateUserInfosArgs } from '@/graphql/arg/UpdateUserInfosArgs'
|
||||
import { backendLogger as logger } from '@/server/logger'
|
||||
|
||||
export async function syncHumhub(
|
||||
updateUserInfosArg: UpdateUserInfosArgs,
|
||||
user: User,
|
||||
): Promise<void> {
|
||||
// check for humhub relevant changes
|
||||
if (
|
||||
updateUserInfosArg.alias === undefined &&
|
||||
updateUserInfosArg.firstName === undefined &&
|
||||
updateUserInfosArg.lastName === undefined &&
|
||||
updateUserInfosArg.humhubAllowed === undefined &&
|
||||
updateUserInfosArg.humhubPublishName === undefined &&
|
||||
updateUserInfosArg.language === undefined
|
||||
) {
|
||||
logger.debug('no relevant changes')
|
||||
return
|
||||
}
|
||||
logger.debug('changed user-settings relevant for humhub-user update...')
|
||||
const humhubClient = HumHubClient.getInstance()
|
||||
if (!humhubClient) {
|
||||
return
|
||||
}
|
||||
logger.debug('retrieve user from humhub')
|
||||
let humhubUser = await humhubClient.userByUsername(user.alias ?? user.gradidoID)
|
||||
if (!humhubUser) {
|
||||
humhubUser = await humhubClient.userByEmail(user.emailContact.email)
|
||||
}
|
||||
const humhubUsers = new Map<string, GetUser>()
|
||||
if (humhubUser) {
|
||||
humhubUsers.set(user.emailContact.email, humhubUser)
|
||||
}
|
||||
logger.debug('update user at humhub')
|
||||
const result = await syncUser(user, humhubUsers)
|
||||
logger.info('finished sync user with humhub', {
|
||||
localId: user.id,
|
||||
externId: humhubUser?.id,
|
||||
// for preventing this warning https://github.com/eslint-community/eslint-plugin-security/blob/main/docs/rules/detect-object-injection.md
|
||||
// and possible danger coming with it
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
|
||||
result: ExecutedHumhubAction[result as ExecutedHumhubAction],
|
||||
})
|
||||
}
|
||||
@ -39,8 +39,12 @@ export const userFactory = async (
|
||||
dbUser = await User.findOneOrFail({ where: { id }, relations: ['userRoles'] })
|
||||
|
||||
if (user.createdAt || user.deletedAt || user.role) {
|
||||
if (user.createdAt) dbUser.createdAt = user.createdAt
|
||||
if (user.deletedAt) dbUser.deletedAt = user.deletedAt
|
||||
if (user.createdAt) {
|
||||
dbUser.createdAt = user.createdAt
|
||||
}
|
||||
if (user.deletedAt) {
|
||||
dbUser.deletedAt = user.deletedAt
|
||||
}
|
||||
if (user.role && (user.role === RoleNames.ADMIN || user.role === RoleNames.MODERATOR)) {
|
||||
await setUserRole(dbUser, user.role)
|
||||
}
|
||||
|
||||
@ -35,9 +35,9 @@ export const updateUserInfos = gql`
|
||||
$hideAmountGDD: Boolean
|
||||
$hideAmountGDT: Boolean
|
||||
$gmsAllowed: Boolean
|
||||
$gmsPublishName: Int
|
||||
$gmsPublishName: PublishNameType
|
||||
$gmsLocation: Location
|
||||
$gmsPublishLocation: Int
|
||||
$gmsPublishLocation: GmsPublishLocationType
|
||||
) {
|
||||
updateUserInfos(
|
||||
firstName: $firstName
|
||||
|
||||
@ -15,6 +15,14 @@ export const verifyLogin = gql`
|
||||
}
|
||||
}
|
||||
`
|
||||
export const authenticateGmsUserSearch = gql`
|
||||
query {
|
||||
authenticateGmsUserSearch {
|
||||
url
|
||||
token
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const queryOptIn = gql`
|
||||
query ($optIn: String!) {
|
||||
|
||||
@ -13,6 +13,7 @@ import { schema } from '@/graphql/schema'
|
||||
import { Connection } from '@/typeorm/connection'
|
||||
import { checkDBVersion } from '@/typeorm/DBVersion'
|
||||
import { elopageWebhook } from '@/webhook/elopage'
|
||||
import { gmsWebhook } from '@/webhook/gms'
|
||||
|
||||
import { context as serverContext } from './context'
|
||||
import { cors } from './cors'
|
||||
@ -94,6 +95,10 @@ export const createServer = async (
|
||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||
app.post('/hook/elopage/' + CONFIG.WEBHOOK_ELOPAGE_SECRET, elopageWebhook)
|
||||
|
||||
// GMS Webhook
|
||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||
app.get('/hook/gms/' + CONFIG.GMS_WEBHOOK_SECRET, gmsWebhook)
|
||||
|
||||
// Apollo Server
|
||||
const apollo = new ApolloServer({
|
||||
schema: await schema(),
|
||||
|
||||
@ -37,6 +37,7 @@ const logPlugin = {
|
||||
const { logger } = requestContext
|
||||
const { query, mutation, variables, operationName } = requestContext.request
|
||||
if (operationName !== 'IntrospectionQuery') {
|
||||
logger.debug('requestDidStart:', requestContext)
|
||||
logger.info(`Request:
|
||||
${mutation || query}variables: ${JSON.stringify(filterVariables(variables), null, 2)}`)
|
||||
}
|
||||
|
||||
@ -52,7 +52,9 @@ const communityDbUser: dbUser = {
|
||||
communityUuid: '55555555-4444-4333-2222-11111111',
|
||||
community: null,
|
||||
gmsPublishName: 0,
|
||||
humhubPublishName: 0,
|
||||
gmsAllowed: false,
|
||||
humhubAllowed: false,
|
||||
location: null,
|
||||
gmsPublishLocation: 2,
|
||||
gmsRegistered: false,
|
||||
|
||||
@ -29,3 +29,7 @@ export function resetInterface<T extends Record<string, any>>(obj: T): T {
|
||||
}
|
||||
return obj
|
||||
}
|
||||
|
||||
export const ensureUrlEndsWithSlash = (url: string): string => {
|
||||
return url.endsWith('/') ? url : url.concat('/')
|
||||
}
|
||||
|
||||
36
backend/src/webhook/gms.ts
Normal file
36
backend/src/webhook/gms.ts
Normal file
@ -0,0 +1,36 @@
|
||||
/* eslint-disable no-console */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-argument */
|
||||
import { User as DbUser } from '@entity/User'
|
||||
|
||||
import { decode } from '@/auth/JWT'
|
||||
|
||||
export const gmsWebhook = async (req: any, res: any): Promise<void> => {
|
||||
console.log('GMS Hook received', req.query)
|
||||
const { token } = req.query
|
||||
|
||||
if (!token) {
|
||||
console.log('gmsWebhook: missing token')
|
||||
res.status(400).json({ message: 'false' })
|
||||
return
|
||||
}
|
||||
const payload = await decode(token)
|
||||
console.log('gmsWebhook: decoded token=', payload)
|
||||
if (!payload) {
|
||||
console.log('gmsWebhook: invalid token')
|
||||
res.status(400).json({ message: 'false' })
|
||||
return
|
||||
}
|
||||
const user = await DbUser.findOne({ where: { gradidoID: payload.gradidoID } })
|
||||
if (!user) {
|
||||
console.log('gmsWebhook: missing user')
|
||||
res.status(400).json({ message: 'false' })
|
||||
return
|
||||
}
|
||||
console.log('gmsWebhook: authenticate user=', user.gradidoID, user.firstName, user.lastName)
|
||||
console.log('gmsWebhook: authentication successful')
|
||||
res.status(200).json({ userUuid: user.gradidoID })
|
||||
}
|
||||
@ -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"
|
||||
|
||||
176
database/entity/0084-introduce_humhub_registration/User.ts
Normal file
176
database/entity/0084-introduce_humhub_registration/User.ts
Normal file
@ -0,0 +1,176 @@
|
||||
import {
|
||||
BaseEntity,
|
||||
Entity,
|
||||
PrimaryGeneratedColumn,
|
||||
Column,
|
||||
DeleteDateColumn,
|
||||
OneToMany,
|
||||
JoinColumn,
|
||||
OneToOne,
|
||||
Geometry,
|
||||
ManyToOne,
|
||||
} from 'typeorm'
|
||||
import { Contribution } from '../Contribution'
|
||||
import { ContributionMessage } from '../ContributionMessage'
|
||||
import { UserContact } from '../UserContact'
|
||||
import { UserRole } from '../UserRole'
|
||||
import { GeometryTransformer } from '../../src/typeorm/GeometryTransformer'
|
||||
import { Community } from '../Community'
|
||||
|
||||
@Entity('users', { engine: 'InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci' })
|
||||
export class User extends BaseEntity {
|
||||
@PrimaryGeneratedColumn('increment', { unsigned: true })
|
||||
id: number
|
||||
|
||||
@Column({ type: 'bool', default: false })
|
||||
foreign: boolean
|
||||
|
||||
@Column({
|
||||
name: 'gradido_id',
|
||||
length: 36,
|
||||
nullable: false,
|
||||
collation: 'utf8mb4_unicode_ci',
|
||||
})
|
||||
gradidoID: string
|
||||
|
||||
@Column({
|
||||
name: 'community_uuid',
|
||||
type: 'char',
|
||||
length: 36,
|
||||
nullable: true,
|
||||
collation: 'utf8mb4_unicode_ci',
|
||||
})
|
||||
communityUuid: string
|
||||
|
||||
@ManyToOne(() => Community, (community) => community.users)
|
||||
@JoinColumn({ name: 'community_uuid', referencedColumnName: 'communityUuid' })
|
||||
community: Community | null
|
||||
|
||||
@Column({
|
||||
name: 'alias',
|
||||
length: 20,
|
||||
nullable: true,
|
||||
default: null,
|
||||
collation: 'utf8mb4_unicode_ci',
|
||||
})
|
||||
alias: string
|
||||
|
||||
@OneToOne(() => UserContact, (emailContact: UserContact) => emailContact.user)
|
||||
@JoinColumn({ name: 'email_id' })
|
||||
emailContact: UserContact
|
||||
|
||||
@Column({ name: 'email_id', type: 'int', unsigned: true, nullable: true, default: null })
|
||||
emailId: number | null
|
||||
|
||||
@Column({
|
||||
name: 'first_name',
|
||||
length: 255,
|
||||
nullable: true,
|
||||
default: null,
|
||||
collation: 'utf8mb4_unicode_ci',
|
||||
})
|
||||
firstName: string
|
||||
|
||||
@Column({
|
||||
name: 'last_name',
|
||||
length: 255,
|
||||
nullable: true,
|
||||
default: null,
|
||||
collation: 'utf8mb4_unicode_ci',
|
||||
})
|
||||
lastName: string
|
||||
|
||||
@Column({ name: 'gms_publish_name', type: 'int', unsigned: true, nullable: false, default: 0 })
|
||||
gmsPublishName: number
|
||||
|
||||
@Column({ name: 'humhub_publish_name', type: 'int', unsigned: true, nullable: false, default: 0 })
|
||||
humhubPublishName: number
|
||||
|
||||
@Column({ name: 'created_at', default: () => 'CURRENT_TIMESTAMP(3)', nullable: false })
|
||||
createdAt: Date
|
||||
|
||||
@DeleteDateColumn({ name: 'deleted_at', nullable: true })
|
||||
deletedAt: Date | null
|
||||
|
||||
@Column({ type: 'bigint', default: 0, unsigned: true })
|
||||
password: BigInt
|
||||
|
||||
@Column({
|
||||
name: 'password_encryption_type',
|
||||
type: 'int',
|
||||
unsigned: true,
|
||||
nullable: false,
|
||||
default: 0,
|
||||
})
|
||||
passwordEncryptionType: number
|
||||
|
||||
@Column({ length: 4, default: 'de', collation: 'utf8mb4_unicode_ci', nullable: false })
|
||||
language: string
|
||||
|
||||
@Column({ type: 'bool', default: false })
|
||||
hideAmountGDD: boolean
|
||||
|
||||
@Column({ type: 'bool', default: false })
|
||||
hideAmountGDT: boolean
|
||||
|
||||
@OneToMany(() => UserRole, (userRole) => userRole.user)
|
||||
@JoinColumn({ name: 'user_id' })
|
||||
userRoles: UserRole[]
|
||||
|
||||
@Column({ name: 'referrer_id', type: 'int', unsigned: true, nullable: true, default: null })
|
||||
referrerId?: number | null
|
||||
|
||||
@Column({
|
||||
name: 'contribution_link_id',
|
||||
type: 'int',
|
||||
unsigned: true,
|
||||
nullable: true,
|
||||
default: null,
|
||||
})
|
||||
contributionLinkId?: number | null
|
||||
|
||||
@Column({ name: 'publisher_id', default: 0 })
|
||||
publisherId: number
|
||||
|
||||
@Column({ name: 'gms_allowed', type: 'bool', default: true })
|
||||
gmsAllowed: boolean
|
||||
|
||||
@Column({
|
||||
name: 'location',
|
||||
type: 'geometry',
|
||||
default: null,
|
||||
nullable: true,
|
||||
transformer: GeometryTransformer,
|
||||
})
|
||||
location: Geometry | null
|
||||
|
||||
@Column({
|
||||
name: 'gms_publish_location',
|
||||
type: 'int',
|
||||
unsigned: true,
|
||||
nullable: false,
|
||||
default: 2,
|
||||
})
|
||||
gmsPublishLocation: number
|
||||
|
||||
@Column({ name: 'gms_registered', type: 'bool', default: false })
|
||||
gmsRegistered: boolean
|
||||
|
||||
@Column({ name: 'gms_registered_at', type: 'datetime', default: null, nullable: true })
|
||||
gmsRegisteredAt: Date | null
|
||||
|
||||
@Column({ name: 'humhub_allowed', type: 'bool', default: false })
|
||||
humhubAllowed: boolean
|
||||
|
||||
@OneToMany(() => Contribution, (contribution) => contribution.user)
|
||||
@JoinColumn({ name: 'user_id' })
|
||||
contributions?: Contribution[]
|
||||
|
||||
@OneToMany(() => ContributionMessage, (message) => message.user)
|
||||
@JoinColumn({ name: 'user_id' })
|
||||
messages?: ContributionMessage[]
|
||||
|
||||
@OneToMany(() => UserContact, (userContact: UserContact) => userContact.user)
|
||||
@JoinColumn({ name: 'user_id' })
|
||||
userContacts?: UserContact[]
|
||||
}
|
||||
@ -1 +1 @@
|
||||
export { User } from './0082-introduce_gms_registration/User'
|
||||
export { User } from './0084-introduce_humhub_registration/User'
|
||||
|
||||
16
database/migrations/0084-introduce_humhub_registration.ts
Normal file
16
database/migrations/0084-introduce_humhub_registration.ts
Normal file
@ -0,0 +1,16 @@
|
||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
|
||||
export async function upgrade(queryFn: (query: string, values?: any[]) => Promise<Array<any>>) {
|
||||
await queryFn(
|
||||
'ALTER TABLE `users` ADD COLUMN IF NOT EXISTS `humhub_allowed` tinyint(1) NOT NULL DEFAULT 0 AFTER `gms_registered_at`;',
|
||||
)
|
||||
await queryFn(
|
||||
'ALTER TABLE `users` ADD COLUMN IF NOT EXISTS `humhub_publish_name` int unsigned NOT NULL DEFAULT 0 AFTER `gms_publish_name`;',
|
||||
)
|
||||
}
|
||||
|
||||
export async function downgrade(queryFn: (query: string, values?: any[]) => Promise<Array<any>>) {
|
||||
await queryFn('ALTER TABLE `users` DROP COLUMN IF EXISTS `humhub_allowed`;')
|
||||
await queryFn('ALTER TABLE `users` DROP COLUMN IF EXISTS `humhub_publish_name`;')
|
||||
}
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "gradido-database",
|
||||
"version": "2.2.0",
|
||||
"version": "2.2.1",
|
||||
"description": "Gradido Database Tool to execute database migrations",
|
||||
"main": "src/index.ts",
|
||||
"repository": "https://github.com/gradido/gradido/database",
|
||||
|
||||
@ -25,8 +25,8 @@ EMAIL_CODE_REQUEST_TIME=10
|
||||
# Need to adjust by updates
|
||||
# config versions
|
||||
DATABASE_CONFIG_VERSION=v1.2022-03-18
|
||||
BACKEND_CONFIG_VERSION=v21.2024-01-06
|
||||
FRONTEND_CONFIG_VERSION=v5.2024-01-08
|
||||
BACKEND_CONFIG_VERSION=v23.2024-04-04
|
||||
FRONTEND_CONFIG_VERSION=v6.2024-02-27
|
||||
ADMIN_CONFIG_VERSION=v2.2024-01-04
|
||||
FEDERATION_CONFIG_VERSION=v2.2023-08-24
|
||||
FEDERATION_DHT_CONFIG_VERSION=v4.2024-01-17
|
||||
@ -120,7 +120,15 @@ DEFAULT_PUBLISHER_ID=2896
|
||||
WEBHOOK_ELOPAGE_SECRET=secret
|
||||
|
||||
# GMS
|
||||
#GMS_ACTIVE=true
|
||||
GMS_ACTIVE=false
|
||||
# Coordinates of Illuminz test instance
|
||||
#GMS_URL=http://54.176.169.179:3071
|
||||
#GMS_URL=http://localhost:4044/
|
||||
#GMS_API_URL=http://54.176.169.179:3071
|
||||
GMS_API_URL=http://localhost:4044/
|
||||
GMS_DASHBOARD_URL=http://localhost:8080/
|
||||
GMS_WEBHOOK_SECRET=secret
|
||||
GMS_CREATE_USER_THROW_ERRORS=false
|
||||
|
||||
# HUMHUB
|
||||
HUMHUB_ACTIVE=false
|
||||
#HUMHUB_API_URL=https://community.gradido.net
|
||||
#HUMHUB_JWT_KEY=
|
||||
|
||||
@ -1,3 +1,6 @@
|
||||
# Migration
|
||||
[Migration from 2.2.0 to 2.2.1](migration/2_2_0-2_2_1/README.md)
|
||||
|
||||
# Setup on Hetzner Cloud Server
|
||||
Suggested OS:
|
||||
Debian 12
|
||||
|
||||
@ -144,7 +144,13 @@ cp $SCRIPT_PATH/logrotate/gradido.conf /etc/logrotate.d/gradido.conf
|
||||
export DB_USER=gradido
|
||||
# create a new password only if it not already exist
|
||||
if [ -z "${DB_PASSWORD}" ]; then
|
||||
export DB_PASSWORD=$(< /dev/urandom tr -dc _A-Z-a-z-0-9 | head -c${1:-32};echo);
|
||||
export DB_PASSWORD=$(< /dev/urandom tr -dc _A-Z-a-z-0-9 | head -c 32; echo);
|
||||
fi
|
||||
|
||||
# Check if DB_PASSWORD is still empty, then exit with an error
|
||||
if [ -z "${DB_PASSWORD}" ]; then
|
||||
echo "Error: Failed to generate DB_PASSWORD."
|
||||
exit 1
|
||||
fi
|
||||
mysql <<EOFMYSQL
|
||||
CREATE USER IF NOT EXISTS '$DB_USER'@'localhost' IDENTIFIED BY '$DB_PASSWORD';
|
||||
@ -156,7 +162,7 @@ EOFMYSQL
|
||||
envsubst "$(env | sed -e 's/=.*//' -e 's/^/\$/g')" < $PROJECT_ROOT/database/.env.template > $PROJECT_ROOT/database/.env
|
||||
|
||||
# Configure backend
|
||||
export JWT_SECRET=$(< /dev/urandom tr -dc _A-Z-a-z-0-9 | head -c${1:-32};echo);
|
||||
export JWT_SECRET=$(< /dev/urandom tr -dc _A-Z-a-z-0-9 | head -c 32; echo);
|
||||
envsubst "$(env | sed -e 's/=.*//' -e 's/^/\$/g')" < $PROJECT_ROOT/backend/.env.template > $PROJECT_ROOT/backend/.env
|
||||
|
||||
# Configure frontend
|
||||
@ -166,7 +172,7 @@ envsubst "$(env | sed -e 's/=.*//' -e 's/^/\$/g')" < $PROJECT_ROOT/frontend/.env
|
||||
envsubst "$(env | sed -e 's/=.*//' -e 's/^/\$/g')" < $PROJECT_ROOT/admin/.env.template > $PROJECT_ROOT/admin/.env
|
||||
|
||||
# Configure dht-node
|
||||
export FEDERATION_DHT_SEED=$(< /dev/urandom tr -dc a-f0-9 | head -c 32;echo);
|
||||
export FEDERATION_DHT_SEED=$(< /dev/urandom tr -dc a-f0-9 | head -c 32; echo);
|
||||
envsubst "$(env | sed -e 's/=.*//' -e 's/^/\$/g')" < $PROJECT_ROOT/dht-node/.env.template > $PROJECT_ROOT/dht-node/.env
|
||||
|
||||
# Configure federation
|
||||
@ -180,4 +186,4 @@ sudo -u gradido crontab < $LOCAL_SCRIPT_DIR/crontabs.txt
|
||||
|
||||
# Start gradido
|
||||
# Note: on first startup some errors will occur - nothing serious
|
||||
sudo -u gradido $SCRIPT_PATH/start.sh
|
||||
sudo -u gradido $SCRIPT_PATH/start.sh $1
|
||||
18
deployment/hetzner_cloud/migration/2_2_0-2_2_1/README.md
Normal file
18
deployment/hetzner_cloud/migration/2_2_0-2_2_1/README.md
Normal file
@ -0,0 +1,18 @@
|
||||
## Migrate from Gradido Version 2.2.0 to 2.2.1
|
||||
### What was wrong
|
||||
In [hetzner_cloud/install.sh](../../install.sh) there was an error.
|
||||
$DB_PASSWORD and $JWT_SECRET password generation method don't work with `release-2_2_0` as parameter for install.sh
|
||||
|
||||
The Parameter forwarding from branch, `release-2_2_0` in this case to start.sh was also missing.
|
||||
|
||||
### What you can do now
|
||||
You need to only run this [fixInstall.sh](fixInstall.sh) with `release_2_2_1` as parameter
|
||||
```bash
|
||||
cd /home/gradido/gradido/deployment/hetzner_cloud/migration/2_2_0-2_2_1
|
||||
sudo ./fixInstall.sh `release_2_2_1`
|
||||
```
|
||||
|
||||
Basically it will create a new $DB_PASSWORD, $JWT_SECRET and $FEDERATION_DHT_SEED,
|
||||
update db user with new db password and update .env files in module folders.
|
||||
Then it will call start.sh with first parameter if ./fixInstall.sh as his first parameter
|
||||
|
||||
66
deployment/hetzner_cloud/migration/2_2_0-2_2_1/fixInstall.sh
Executable file
66
deployment/hetzner_cloud/migration/2_2_0-2_2_1/fixInstall.sh
Executable file
@ -0,0 +1,66 @@
|
||||
#!/bin/bash
|
||||
# check for parameter
|
||||
if [ -z "$1" ]; then
|
||||
echo "Usage: Please provide a branch name as the first argument."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
set -o allexport
|
||||
SCRIPT_PATH=$(realpath ../../../bare_metal)
|
||||
SCRIPT_DIR=$(dirname $SCRIPT_PATH)
|
||||
LOCAL_SCRIPT_PATH=$(realpath $0)
|
||||
LOCAL_SCRIPT_DIR=$(dirname $LOCAL_SCRIPT_PATH)
|
||||
PROJECT_ROOT=$SCRIPT_DIR/..
|
||||
set +o allexport
|
||||
|
||||
|
||||
# Load .env or .env.dist if not present
|
||||
# NOTE: all config values will be in process.env when starting
|
||||
# the services and will therefore take precedence over the .env
|
||||
if [ -f "$SCRIPT_PATH/.env" ]; then
|
||||
set -o allexport
|
||||
source $SCRIPT_PATH/.env
|
||||
set +o allexport
|
||||
else
|
||||
set -o allexport
|
||||
source $SCRIPT_PATH/.env.dist
|
||||
set +o allexport
|
||||
fi
|
||||
|
||||
# create db user
|
||||
export DB_USER=gradido
|
||||
# create a new password only if it not already exist
|
||||
export DB_PASSWORD=$(< /dev/urandom tr -dc _A-Z-a-z-0-9 | head -c 32; echo);
|
||||
|
||||
|
||||
mysql <<EOFMYSQL
|
||||
ALTER USER '$DB_USER'@'localhost' IDENTIFIED BY '$DB_PASSWORD';
|
||||
FLUSH PRIVILEGES;
|
||||
EOFMYSQL
|
||||
|
||||
# Configure database
|
||||
envsubst "$(env | sed -e 's/=.*//' -e 's/^/\$/g')" < $PROJECT_ROOT/database/.env.template > $PROJECT_ROOT/database/.env
|
||||
|
||||
# Configure backend
|
||||
export JWT_SECRET=$(< /dev/urandom tr -dc _A-Z-a-z-0-9 | head -c 32; echo);
|
||||
envsubst "$(env | sed -e 's/=.*//' -e 's/^/\$/g')" < $PROJECT_ROOT/backend/.env.template > $PROJECT_ROOT/backend/.env
|
||||
|
||||
# Configure frontend
|
||||
envsubst "$(env | sed -e 's/=.*//' -e 's/^/\$/g')" < $PROJECT_ROOT/frontend/.env.template > $PROJECT_ROOT/frontend/.env
|
||||
|
||||
# Configure admin
|
||||
envsubst "$(env | sed -e 's/=.*//' -e 's/^/\$/g')" < $PROJECT_ROOT/admin/.env.template > $PROJECT_ROOT/admin/.env
|
||||
|
||||
# Configure dht-node
|
||||
export FEDERATION_DHT_SEED=$(< /dev/urandom tr -dc a-f0-9 | head -c 32; echo);
|
||||
envsubst "$(env | sed -e 's/=.*//' -e 's/^/\$/g')" < $PROJECT_ROOT/dht-node/.env.template > $PROJECT_ROOT/dht-node/.env
|
||||
|
||||
# Configure federation
|
||||
envsubst "$(env | sed -e 's/=.*//' -e 's/^/\$/g')" < $PROJECT_ROOT/federation/.env.template > $PROJECT_ROOT/federation/.env
|
||||
|
||||
# set all created or modified files back to belonging to gradido
|
||||
chown -R gradido:gradido $PROJECT_ROOT
|
||||
|
||||
# Start gradido
|
||||
# Note: on first startup some errors will occur - nothing serious
|
||||
sudo -u gradido $SCRIPT_PATH/start.sh $1
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "gradido-dht-node",
|
||||
"version": "2.2.0",
|
||||
"version": "2.2.1",
|
||||
"description": "Gradido dht-node module",
|
||||
"main": "src/index.ts",
|
||||
"repository": "https://github.com/gradido/gradido/",
|
||||
|
||||
@ -4,7 +4,7 @@ import dotenv from 'dotenv'
|
||||
dotenv.config()
|
||||
|
||||
const constants = {
|
||||
DB_VERSION: '0083-join_community_federated_communities',
|
||||
DB_VERSION: '0084-introduce_humhub_registration',
|
||||
LOG4JS_CONFIG: 'log4js-config.json',
|
||||
// default log level on production should be info
|
||||
LOG_LEVEL: process.env.LOG_LEVEL ?? 'info',
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "gradido-dlt-connector",
|
||||
"version": "2.2.0",
|
||||
"version": "2.2.1",
|
||||
"description": "Gradido DLT-Connector",
|
||||
"main": "src/index.ts",
|
||||
"repository": "https://github.com/gradido/gradido/",
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
export async function upgrade(queryFn: (query: string, values?: any[]) => Promise<Array<any>>) {
|
||||
await queryFn(`
|
||||
ALTER TABLE \`transactions\`
|
||||
RENAME COLUMN \`paring_transaction_id\` TO \`pairing_transaction_id\`,
|
||||
RENAME COLUMN \`paring_transaction_id\` TO \`pairing_transaction_id\`
|
||||
;
|
||||
`)
|
||||
}
|
||||
@ -9,7 +9,7 @@ export async function upgrade(queryFn: (query: string, values?: any[]) => Promis
|
||||
export async function downgrade(queryFn: (query: string, values?: any[]) => Promise<Array<any>>) {
|
||||
await queryFn(`
|
||||
ALTER TABLE \`transactions\`
|
||||
RENAME COLUMN \`pairing_transaction_id\` TO \`paring_transaction_id\`,
|
||||
RENAME COLUMN \`pairing_transaction_id\` TO \`paring_transaction_id\`
|
||||
;
|
||||
`)
|
||||
}
|
||||
|
||||
@ -275,28 +275,45 @@ It contains a map-component of the leaflet library and offers to capture the use
|
||||
|
||||
There is no user-specific authentication nor autorization necessary for this dialog as mentioned above.
|
||||
|
||||
### GMS user playground dialog (gms efforts)
|
||||
### GMS user playground dialog
|
||||
|
||||
As described in the chapter "User search" above, we need a dialog in GMS to display in a graphical map:
|
||||
|
||||
* the location of the user as a red needle, who opens the user search-dialog
|
||||
* the location of his community as a circle, the invoker belongs to
|
||||
* the locations of all other users as white needles, belonging to the same community
|
||||
* the locations of all other users belonging to the same community as white/gray or black needles - depending on the location-type of the user
|
||||
* circles and needles of all other communities and users, which are nearby the requesting user and community location
|
||||
|
||||
There is no user-specific authentication nor autorization necessary for this dialog as mentioned above.
|
||||
On activation of the menu-entry _user-search_ a technical flow in the background have to prepare the connection between the gradido-system and the gms-component. The following list will describe the necessary steps of all involved components:
|
||||
|
||||
Which (filter-)components this playground-dialog should have next to the graphical location map is not clear at the moment. In the first run to display the above mentioned locations of users and communities with circles and needles will be sufficient.
|
||||
* **gradido-frontend:** user press the menu entry _user search_
|
||||
* **(1.a) gradido-frontend:** invokes the gradido-backend `authUserForGmsUserSearch`
|
||||
* **(1.b) gradido-backend:** the method `authUserForGmsUserSearch` reads the context-user of the current request and the uuid of the user's home-community. With these values it prepares the parameters for invokation of the `gms.verifyAuthToken` method. The first parameter is set by the `community-uuid` and the second parameter is a JWT-token with the encrypted `user-uuid` in the payload and signed by the community's privateKey
|
||||
* **(2.a) gradido-backend:** invokes the `gms.verifyAuthToken` with community-uuid as 1st parameter and JWT-Token as 2nd parameter
|
||||
* **(2.b) gms-backend:** recieving the request `verifyAuthToken` with the community-uuid and the JWT-token. After searching and verifing the given community-uuid exists in gms, it prepares the invokation of the configured endpoint `community-Auth-Url` of this community by sending the given JWT-token as parameter back to gradido.
|
||||
* **(3.a) gms-backend:** invokes the endpoint configured in `gms.community-auth-url` with the previous received JWT-token
|
||||
* **(3.b) gradido-backend:** receives the request at the endpoint "communityAuthUrl" with the previously sent JWT-token. The JWT-token will be verified if the signature is valid and afterwards the payload is decrypted to verify the contained user-data will match with the current context-user of request (1).
|
||||
* **(4.a) gradido-backend:** in case of valid JWT-token signature and valid payload data the gradido-backend returns TRUE as result of the authentication-request otherwise FALSE.
|
||||
* **(4.b) gms-backend:** receives the response of request (3) and in case of TRUE the gms-backend prepares to return a complete URI including a _JWT-access-token_ to be used for entering the user-playground. *It will not return gms-data used for the user-playground as the current implementation of the api `verify-auth-token` do.* In case of FALSE prepare returning an authentication-error.
|
||||
* **(5.a) gms-backend:** returning the complete URI including a _JWT-access-token_ as response of request (2) or an authentication-error
|
||||
* **(5.b) gradido-backend:** receiving as response of request (2) a complete URI including a _JWT-access-token_ for entering the users-playground on gms or an authentication-error
|
||||
* **(6.a) gradido-backend:** returning the complete URI including a _JWT-access-token_ as response of request (1) or an expressive error message
|
||||
* **(6.b) gradido-frontend:** receiving the complete URI including a _JWT-access-token_ after activation of menu-entry "user-search" or an expressive error-message, which will end the _user search_-flow without requesting the gms-frontend (7).
|
||||
* **(7.a) gradido-frontend:** on opening a new browser-window the gradido-frontend uses the received URI with the _JWT-access-token_ to enter the gms user-playground
|
||||
* **(7.b) gms-frontend:** receiving the request for the user-playground with an _JWT-access-token_. After verifying the access-token the gms-frontend will read the data for the user given by the access-token and loads all necessary data to render the users playground
|
||||
|
||||
The detailed requirements will come up as soon as we get some user expiriences and feedbacks.
|
||||
The following picture shows the logical flow and interaction between the involved components:
|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
The detailed requirements for the playground-dialog will come up as soon as we get some user expiriences and feedbacks.
|
||||
|
||||
### GMS Offer Capture dialog (gms efforts)
|
||||
|
||||
will come later...
|
||||
|
||||
|
||||
|
||||
### GMS Need Capture dialog (gms efforts)
|
||||
|
||||
will come later...
|
||||
|
||||
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 42 KiB |
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "gradido-federation",
|
||||
"version": "2.2.0",
|
||||
"version": "2.2.1",
|
||||
"description": "Gradido federation module providing Gradido-Hub-Federation and versioned API for inter community communication",
|
||||
"main": "src/index.ts",
|
||||
"repository": "https://github.com/gradido/gradido/federation",
|
||||
|
||||
@ -10,7 +10,7 @@ Decimal.set({
|
||||
})
|
||||
|
||||
const constants = {
|
||||
DB_VERSION: '0083-join_community_federated_communities',
|
||||
DB_VERSION: '0084-introduce_humhub_registration',
|
||||
DECAY_START_TIME: new Date('2021-05-13 17:46:31-0000'), // GMT+0
|
||||
LOG4JS_CONFIG: 'log4js-config.json',
|
||||
// default log level on production should be info
|
||||
|
||||
@ -21,3 +21,6 @@ META_DESCRIPTION_EN="Gratitude is the currency of the new age. More and more peo
|
||||
META_KEYWORDS_DE="Grundeinkommen, Währung, Dankbarkeit, Schenk-Ökonomie, Natürliche Ökonomie des Lebens, Ökonomie, Ökologie, Potenzialentfaltung, Schenken und Danken, Kreislauf des Lebens, Geldsystem"
|
||||
META_KEYWORDS_EN="Basic Income, Currency, Gratitude, Gift Economy, Natural Economy of Life, Economy, Ecology, Potential Development, Giving and Thanking, Cycle of Life, Monetary System"
|
||||
META_AUTHOR="Bernd Hückstädt - Gradido-Akademie"
|
||||
|
||||
GMS_ACTIVE=false
|
||||
HUMHUB_ACTIVE=false
|
||||
|
||||
@ -24,3 +24,6 @@ META_DESCRIPTION_EN=$META_DESCRIPTION_EN
|
||||
META_KEYWORDS_DE=$META_KEYWORDS_DE
|
||||
META_KEYWORDS_EN=$META_KEYWORDS_EN
|
||||
META_AUTHOR=$META_AUTHOR
|
||||
|
||||
GMS_ACTIVE=$GMS_ACTIVE
|
||||
HUMHUB_ACTIVE=$HUMHUB_ACTIVE
|
||||
@ -4,7 +4,7 @@ module.exports = {
|
||||
collectCoverageFrom: ['src/**/*.{js,vue}', '!**/node_modules/**', '!**/?(*.)+(spec|test).js?(x)'],
|
||||
coverageThreshold: {
|
||||
global: {
|
||||
lines: 94,
|
||||
lines: 93,
|
||||
},
|
||||
},
|
||||
moduleFileExtensions: [
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "bootstrap-vue-gradido-wallet",
|
||||
"version": "2.2.0",
|
||||
"version": "2.2.1",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"start": "node run/server.js",
|
||||
|
||||
BIN
frontend/public/img/loupe.png
Normal file
BIN
frontend/public/img/loupe.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.3 KiB |
7
frontend/public/img/svg/circles.svg
Normal file
7
frontend/public/img/svg/circles.svg
Normal file
@ -0,0 +1,7 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" width="200" height="200">
|
||||
<!-- Personen in Kreisen -->
|
||||
<circle cx="30" cy="50" r="15" fill="none" stroke="black" stroke-width="2"/>
|
||||
<circle cx="50" cy="30" r="15" fill="none" stroke="black" stroke-width="2"/>
|
||||
<circle cx="70" cy="50" r="15" fill="none" stroke="black" stroke-width="2"/>
|
||||
<circle cx="50" cy="70" r="15" fill="none" stroke="black" stroke-width="2"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 442 B |
@ -71,4 +71,16 @@ export default {
|
||||
.text-color-gdd-yellow {
|
||||
color: rgb(197 141 56);
|
||||
}
|
||||
|
||||
.dropdown > .dropdown-toggle {
|
||||
border-radius: 17px;
|
||||
height: 50px;
|
||||
text-align: left;
|
||||
}
|
||||
.dropdown-toggle::after {
|
||||
float: right;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
position: relative;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -1,37 +1,42 @@
|
||||
[
|
||||
{
|
||||
"locale": "de",
|
||||
"text": "Gradido-Kreise – Gemeinsam mit Freunden die Zukunft gestalten",
|
||||
"button": "Mehr erfahren",
|
||||
"url": "https://gradido.net/de/gradido-kreise-gemeinsam-mit-freunden-die-zukunft-gestalten",
|
||||
"extra": "Ganz gleich, ob Ihr bereits einer Gruppe zugehörig seid oder ob Ihr Euch über Gradido gefunden habt – wenn Ihr gemeinsam Gradido nutzen wollt, braucht Ihr nicht gleich einen eigenen Gradido-Server."
|
||||
"text": "Gradido-Kreise: neue interne Kooperationsplattform",
|
||||
"button": "Jetzt freischalten...",
|
||||
"internUrl": "https://gradido.net/de/gradido-kreise-gemeinsam-mit-freunden-die-zukunft-gestalten",
|
||||
"extra": "Gemeinsam unterstützen wir einander, achtsam in Kreiskultur. In geschützten Räumen können wir nun frei kommunizieren und zusammenarbeiten, ohne auf die sozialen Medien angewiesen zu sein.",
|
||||
"extra2": "Schalte Gradido-Kreise in den Einstellungen/Community frei und klicke anschließend auf das Menü \"Kreise\"!"
|
||||
},
|
||||
{
|
||||
"locale": "en",
|
||||
"text": "Gradido circles - Shaping the future together with friends",
|
||||
"button": "Learn more",
|
||||
"url": "https://gradido.net/en/gradido-kreise-gemeinsam-mit-freunden-die-zukunft-gestalten/",
|
||||
"extra": "No matter whether you already belong to a group or whether you found each other via Gradido - if you want to use Gradido together, you don't need your own Gradido server."
|
||||
"text": "Gradido circles: new internal cooperation platform",
|
||||
"button": "Activate now...",
|
||||
"internUrl": "https://gradido.net/en/gradido-kreise-gemeinsam-mit-freunden-die-zukunft-gestalten/",
|
||||
"extra": "Together, we support each other, mindfully in a circular culture. We can now communicate and collaborate freely in protected spaces without having to rely on social media.",
|
||||
"extra2": "Activate Gradido circles in the settings/community and then click on the menu \"Circles\"!"
|
||||
},
|
||||
{
|
||||
"locale": "fr",
|
||||
"text": "Cercles Gradido - Construire l'avenir ensemble avec des amis ",
|
||||
"text": "Cercles Gradido : nouvelle plateforme de coopération interne",
|
||||
"button": "En savoir plus",
|
||||
"url": "https://gradido.net/fr/gradido-kreise-gemeinsam-mit-freunden-die-zukunft-gestalten/",
|
||||
"extra": "Que vous fassiez déjà partie d'un groupe ou que vous vous soyez trouvés par le biais de Gradido, si vous voulez utiliser Gradido ensemble, vous n'avez pas besoin de votre propre serveur Gradido."
|
||||
"internUrl": "https://gradido.net/fr/gradido-kreise-gemeinsam-mit-freunden-die-zukunft-gestalten/",
|
||||
"extra": "Ensemble, nous nous soutenons mutuellement, en étant attentifs à la culture du cercle. Dans des espaces protégés, nous pouvons désormais communiquer et collaborer librement, sans être tributaires des médias sociaux.",
|
||||
"extra2": "Débloque les cercles Gradido dans les paramètres/communauté et clique ensuite sur le menu \"Cercles\" !"
|
||||
},
|
||||
{
|
||||
"locale": "es",
|
||||
"text": "Círculos Gradido - Forjar el futuro entre amigos ",
|
||||
"button": "Más información",
|
||||
"url": "https://gradido.net/es/gradido-kreise-gemeinsam-mit-freunden-die-zukunft-gestalten/",
|
||||
"extra": "No importa si ya pertenecéis a un grupo o si os habéis encontrado a través de Gradido: si queréis utilizar Gradido juntos, no necesitáis vuestro propio servidor Gradido."
|
||||
"text": "Círculos Gradido: nueva plataforma de cooperación interna",
|
||||
"button": "Activar ahora...",
|
||||
"internUrl": "https://gradido.net/es/gradido-kreise-gemeinsam-mit-freunden-die-zukunft-gestalten/",
|
||||
"extra": "Juntos, nos apoyamos mutuamente, de forma consciente, en una cultura circular. Ahora podemos comunicarnos y colaborar libremente en espacios protegidos sin tener que depender de las redes sociales.",
|
||||
"extra2": "Activa los círculos de Gradido en los ajustes/comunidad y luego haz clic en el menú \"Círculos\"."
|
||||
},
|
||||
{
|
||||
"locale": "nl",
|
||||
"text": "Gradidokringen - Samen met vrienden de toekomst vormgeven",
|
||||
"button": "Meer informatie",
|
||||
"url": "https://gradido.net/nl/gradido-kreise-gemeinsam-mit-freunden-die-zukunft-gestalten/",
|
||||
"extra": "Het maakt niet uit of je al tot een groep behoort of dat je elkaar via Gradido hebt gevonden - als je Gradido samen wilt gebruiken, heb je geen eigen Gradido-server nodig."
|
||||
"text": "Gradido cirkels: nieuw intern samenwerkingsplatform",
|
||||
"button": "Nu activeren...",
|
||||
"internUrl": "https://gradido.net/nl/gradido-kreise-gemeinsam-mit-freunden-die-zukunft-gestalten/",
|
||||
"extra": "Samen ondersteunen we elkaar, mindful in een circulaire cultuur. We kunnen nu vrij communiceren en samenwerken in beschermde ruimtes zonder afhankelijk te zijn van sociale media.",
|
||||
"extra2": "Activeer Gradido cirkels in de instellingen/community en klik dan op het menu \"Cirkels\"!"
|
||||
}
|
||||
]
|
||||
|
||||
@ -90,20 +90,3 @@ export default {
|
||||
},
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
.community-switch > div,
|
||||
.community-switch ul.dropdown-menu {
|
||||
width: 100%;
|
||||
}
|
||||
.community-switch > div > button {
|
||||
border-radius: 17px;
|
||||
height: 50px;
|
||||
text-align: left;
|
||||
}
|
||||
.community-switch .dropdown-toggle::after {
|
||||
float: right;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
position: relative;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -1,8 +1,12 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import Sidebar from './Sidebar'
|
||||
import CONFIG from '../../config'
|
||||
|
||||
const localVue = global.localVue
|
||||
|
||||
CONFIG.GMS_ACTIVE = 'true'
|
||||
CONFIG.HUMHUB_ACTIVE = 'true'
|
||||
|
||||
describe('Sidebar', () => {
|
||||
let wrapper
|
||||
|
||||
@ -17,6 +21,9 @@ describe('Sidebar', () => {
|
||||
roles: [],
|
||||
},
|
||||
},
|
||||
$route: {
|
||||
path: '/',
|
||||
},
|
||||
}
|
||||
|
||||
const Wrapper = () => {
|
||||
@ -32,9 +39,9 @@ describe('Sidebar', () => {
|
||||
expect(wrapper.find('div#component-sidebar').exists()).toBe(true)
|
||||
})
|
||||
|
||||
describe('the genaral section', () => {
|
||||
it('has six nav-items', () => {
|
||||
expect(wrapper.findAll('ul').at(0).findAll('.nav-item')).toHaveLength(6)
|
||||
describe('the general section', () => {
|
||||
it('has seven nav-items', () => {
|
||||
expect(wrapper.findAll('ul').at(0).findAll('.nav-item')).toHaveLength(7)
|
||||
})
|
||||
|
||||
it('has nav-item "navigation.overview" in navbar', () => {
|
||||
@ -53,12 +60,16 @@ describe('Sidebar', () => {
|
||||
expect(wrapper.findAll('.nav-item').at(3).text()).toEqual('creation')
|
||||
})
|
||||
|
||||
it('has nav-item "GDT" in navbar', () => {
|
||||
expect(wrapper.findAll('.nav-item').at(4).text()).toContain('GDT')
|
||||
it('has nav-item "navigation.info" in navbar', () => {
|
||||
expect(wrapper.findAll('.nav-item').at(4).text()).toContain('navigation.info')
|
||||
})
|
||||
|
||||
it('has nav-item "navigation.info" in navbar', () => {
|
||||
expect(wrapper.findAll('.nav-item').at(5).text()).toContain('navigation.info')
|
||||
it('has nav-item "navigation.circles" in navbar', () => {
|
||||
expect(wrapper.findAll('.nav-item').at(5).text()).toContain('navigation.circles')
|
||||
})
|
||||
|
||||
it('has nav-item "navigation.usersearch" in navbar', () => {
|
||||
expect(wrapper.findAll('.nav-item').at(6).text()).toContain('navigation.usersearch')
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@ -16,7 +16,7 @@
|
||||
<b-icon icon="cash" aria-hidden="true"></b-icon>
|
||||
<span class="ml-2">{{ $t('navigation.send') }}</span>
|
||||
</b-nav-item>
|
||||
<b-nav-item to="/transactions" class="mb-3" active-class="activeRoute">
|
||||
<b-nav-item to="/transactions" :class="transactionClass" active-class="activeRoute">
|
||||
<b-img src="/img/svg/transaction.svg" height="20" class="svg-icon" />
|
||||
<span class="ml-2">{{ $t('navigation.transactions') }}</span>
|
||||
</b-nav-item>
|
||||
@ -24,14 +24,18 @@
|
||||
<b-img src="/img/svg/community.svg" height="20" class="svg-icon" />
|
||||
<span class="ml-2">{{ $t('creation') }}</span>
|
||||
</b-nav-item>
|
||||
<b-nav-item to="/gdt" class="mb-3" active-class="activeRoute">
|
||||
<b-icon icon="layers" aria-hidden="true"></b-icon>
|
||||
<span class="ml-2">{{ $t('GDT') }}</span>
|
||||
</b-nav-item>
|
||||
<b-nav-item to="/information" active-class="activeRoute">
|
||||
<b-nav-item to="/information" class="mb-3" active-class="activeRoute">
|
||||
<b-img src="/img/svg/info.svg" height="20" class="svg-icon" />
|
||||
<span class="ml-2">{{ $t('navigation.info') }}</span>
|
||||
</b-nav-item>
|
||||
<b-nav-item to="/circles" v-if="isHumhub" class="mb-3" active-class="activeRoute">
|
||||
<b-img src="/img/svg/circles.svg" height="20" class="svg-icon" />
|
||||
<span class="ml-2">{{ $t('navigation.circles') }}</span>
|
||||
</b-nav-item>
|
||||
<b-nav-item to="/usersearch" v-if="isGMS" active-class="activeRoute">
|
||||
<b-img src="/img/loupe.png" height="20" />
|
||||
<span class="ml-2">{{ $t('navigation.usersearch') }}</span>
|
||||
</b-nav-item>
|
||||
</b-nav>
|
||||
<hr class="m-3" />
|
||||
<b-nav vertical class="w-100">
|
||||
@ -73,11 +77,27 @@
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import CONFIG from '../../config'
|
||||
|
||||
export default {
|
||||
name: 'Sidebar',
|
||||
props: {
|
||||
shadow: { type: Boolean, required: false, default: true },
|
||||
},
|
||||
computed: {
|
||||
transactionClass() {
|
||||
if (this.$route.path === '/gdt') {
|
||||
return 'mb-3 activeRoute'
|
||||
}
|
||||
return 'mb-3'
|
||||
},
|
||||
isHumhub() {
|
||||
return CONFIG.HUMHUB_ACTIVE === 'true'
|
||||
},
|
||||
isGMS() {
|
||||
return CONFIG.GMS_ACTIVE === 'true'
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
|
||||
@ -27,9 +27,14 @@
|
||||
<b-row class="my-5">
|
||||
<b-col cols="12">
|
||||
<div class="text-lg-right">
|
||||
<b-button variant="gradido" :href="item.url" target="_blank">
|
||||
<b-button v-if="item.url" variant="gradido" :href="item.url" target="_blank">
|
||||
{{ item.button }}
|
||||
</b-button>
|
||||
<RouterLink v-else-if="item.internUrl" to="/settings/extern">
|
||||
<b-button variant="gradido">
|
||||
{{ item.button }}
|
||||
</b-button>
|
||||
</RouterLink>
|
||||
</div>
|
||||
</b-col>
|
||||
</b-row>
|
||||
|
||||
8
frontend/src/components/UserSettings/UserGMSLocation.vue
Normal file
8
frontend/src/components/UserSettings/UserGMSLocation.vue
Normal file
@ -0,0 +1,8 @@
|
||||
<template>
|
||||
<b-button>{{ $t('settings.GMS.location.button') }}</b-button>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
name: 'UserGMSLocation',
|
||||
}
|
||||
</script>
|
||||
@ -0,0 +1,79 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import UserGMSLocationFormat from './UserGMSLocationFormat.vue'
|
||||
import { toastErrorSpy } from '@test/testSetup'
|
||||
|
||||
const mockAPIcall = jest.fn()
|
||||
|
||||
const storeCommitMock = jest.fn()
|
||||
|
||||
const localVue = global.localVue
|
||||
|
||||
describe('UserGMSLocationFormat', () => {
|
||||
let wrapper
|
||||
beforeEach(() => {
|
||||
wrapper = mount(UserGMSLocationFormat, {
|
||||
mocks: {
|
||||
$t: (key) => key, // Mocking the translation function
|
||||
$store: {
|
||||
state: {
|
||||
gmsPublishLocation: null,
|
||||
},
|
||||
commit: storeCommitMock,
|
||||
},
|
||||
$apollo: {
|
||||
mutate: mockAPIcall,
|
||||
},
|
||||
},
|
||||
localVue,
|
||||
propsData: {
|
||||
selectedOption: 'GMS_LOCATION_TYPE_RANDOM',
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy()
|
||||
})
|
||||
|
||||
it('renders the correct dropdown options', () => {
|
||||
const dropdownItems = wrapper.findAll('.dropdown-item')
|
||||
expect(dropdownItems.length).toBe(3)
|
||||
|
||||
const labels = dropdownItems.wrappers.map((item) => item.text())
|
||||
expect(labels).toEqual([
|
||||
'settings.GMS.publish-location.exact',
|
||||
'settings.GMS.publish-location.approximate',
|
||||
'settings.GMS.publish-location.random',
|
||||
])
|
||||
})
|
||||
|
||||
it('updates selected option on click', async () => {
|
||||
const dropdownItem = wrapper.findAll('.dropdown-item').at(1) // Click the second item
|
||||
await dropdownItem.trigger('click')
|
||||
|
||||
expect(wrapper.emitted().gmsPublishLocation).toBeTruthy()
|
||||
expect(wrapper.emitted().gmsPublishLocation.length).toBe(1)
|
||||
expect(wrapper.emitted().gmsPublishLocation[0]).toEqual(['GMS_LOCATION_TYPE_APPROXIMATE'])
|
||||
})
|
||||
|
||||
it('does not update when clicking on already selected option', async () => {
|
||||
const dropdownItem = wrapper.findAll('.dropdown-item').at(2) // Click the third item (which is already selected)
|
||||
await dropdownItem.trigger('click')
|
||||
|
||||
expect(wrapper.emitted().gmsPublishLocation).toBeFalsy()
|
||||
})
|
||||
|
||||
describe('update with error', () => {
|
||||
beforeEach(async () => {
|
||||
mockAPIcall.mockRejectedValue({
|
||||
message: 'Ouch',
|
||||
})
|
||||
const dropdownItem = wrapper.findAll('.dropdown-item').at(1) // Click the second item
|
||||
await dropdownItem.trigger('click')
|
||||
})
|
||||
|
||||
it('toasts an error message', () => {
|
||||
expect(toastErrorSpy).toBeCalledWith('Ouch')
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,73 @@
|
||||
<template>
|
||||
<div class="user-gms-location-format">
|
||||
<b-dropdown v-model="selectedOption">
|
||||
<template slot="button-content">{{ selectedOptionLabel }}</template>
|
||||
<b-dropdown-item
|
||||
v-for="option in dropdownOptions"
|
||||
@click.prevent="update(option)"
|
||||
:key="option.value"
|
||||
:value="option.value"
|
||||
>
|
||||
{{ option.label }}
|
||||
</b-dropdown-item>
|
||||
</b-dropdown>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { updateUserInfos } from '@/graphql/mutations'
|
||||
|
||||
export default {
|
||||
name: 'UserGMSLocationFormat',
|
||||
data() {
|
||||
return {
|
||||
selectedOption: this.$store.state.gmsPublishLocation ?? 'GMS_LOCATION_TYPE_RANDOM',
|
||||
dropdownOptions: [
|
||||
{
|
||||
label: this.$t('settings.GMS.publish-location.exact'),
|
||||
value: 'GMS_LOCATION_TYPE_EXACT',
|
||||
},
|
||||
{
|
||||
label: this.$t('settings.GMS.publish-location.approximate'),
|
||||
value: 'GMS_LOCATION_TYPE_APPROXIMATE',
|
||||
},
|
||||
{
|
||||
label: this.$t('settings.GMS.publish-location.random'),
|
||||
value: 'GMS_LOCATION_TYPE_RANDOM',
|
||||
},
|
||||
],
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
selectedOptionLabel() {
|
||||
return this.dropdownOptions.find((option) => option.value === this.selectedOption).label
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
async update(option) {
|
||||
if (option.value === this.selectedOption) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
await this.$apollo.mutate({
|
||||
mutation: updateUserInfos,
|
||||
variables: {
|
||||
gmsPublishLocation: option.value,
|
||||
},
|
||||
})
|
||||
this.toastSuccess(this.$t('settings.GMS.publish-location.updated'))
|
||||
this.selectedOption = option.value
|
||||
this.$store.commit('gmsPublishLocation', option.value)
|
||||
this.$emit('gmsPublishLocation', option.value)
|
||||
} catch (error) {
|
||||
this.toastError(error.message)
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
.user-gms-location-format > .dropdown,
|
||||
.user-gms-location-format > .dropdown > .dropdown-toggle > ul.dropdown-menu {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,84 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import UserNamingFormat from './UserNamingFormat.vue'
|
||||
import { toastErrorSpy } from '@test/testSetup'
|
||||
|
||||
const mockAPIcall = jest.fn()
|
||||
|
||||
const storeCommitMock = jest.fn()
|
||||
|
||||
const localVue = global.localVue
|
||||
|
||||
describe('UserNamingFormat', () => {
|
||||
let wrapper
|
||||
beforeEach(() => {
|
||||
wrapper = mount(UserNamingFormat, {
|
||||
mocks: {
|
||||
$t: (key) => key, // Mocking the translation function
|
||||
$store: {
|
||||
state: {
|
||||
gmsPublishName: null,
|
||||
},
|
||||
commit: storeCommitMock,
|
||||
},
|
||||
$apollo: {
|
||||
mutate: mockAPIcall,
|
||||
},
|
||||
},
|
||||
localVue,
|
||||
propsData: {
|
||||
selectedOption: 'PUBLISH_NAME_ALIAS_OR_INITALS',
|
||||
initialValue: 'PUBLISH_NAME_ALIAS_OR_INITALS',
|
||||
attrName: 'gmsPublishName',
|
||||
successMessage: 'success message',
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy()
|
||||
})
|
||||
|
||||
it('renders the correct dropdown options', () => {
|
||||
const dropdownItems = wrapper.findAll('.dropdown-item')
|
||||
expect(dropdownItems.length).toBe(5)
|
||||
|
||||
const labels = dropdownItems.wrappers.map((item) => item.text())
|
||||
expect(labels).toEqual([
|
||||
'settings.publish-name.alias-or-initials',
|
||||
'settings.publish-name.initials',
|
||||
'settings.publish-name.first',
|
||||
'settings.publish-name.first-initial',
|
||||
'settings.publish-name.name-full',
|
||||
])
|
||||
})
|
||||
|
||||
it('updates selected option on click', async () => {
|
||||
const dropdownItem = wrapper.findAll('.dropdown-item').at(3) // Click the fourth item
|
||||
await dropdownItem.trigger('click')
|
||||
|
||||
expect(wrapper.emitted().valueChanged).toBeTruthy()
|
||||
expect(wrapper.emitted().valueChanged.length).toBe(1)
|
||||
expect(wrapper.emitted().valueChanged[0]).toEqual(['PUBLISH_NAME_FIRST_INITIAL'])
|
||||
})
|
||||
|
||||
it('does not update when clicking on already selected option', async () => {
|
||||
const dropdownItem = wrapper.findAll('.dropdown-item').at(0) // Click the first item (which is already selected)
|
||||
await dropdownItem.trigger('click')
|
||||
|
||||
expect(wrapper.emitted().valueChanged).toBeFalsy()
|
||||
})
|
||||
|
||||
describe('update with error', () => {
|
||||
beforeEach(async () => {
|
||||
mockAPIcall.mockRejectedValue({
|
||||
message: 'Ouch',
|
||||
})
|
||||
const dropdownItem = wrapper.findAll('.dropdown-item').at(2) // Click the third item
|
||||
await dropdownItem.trigger('click')
|
||||
})
|
||||
|
||||
it('toasts an error message', () => {
|
||||
expect(toastErrorSpy).toBeCalledWith('Ouch')
|
||||
})
|
||||
})
|
||||
})
|
||||
95
frontend/src/components/UserSettings/UserNamingFormat.vue
Normal file
95
frontend/src/components/UserSettings/UserNamingFormat.vue
Normal file
@ -0,0 +1,95 @@
|
||||
<template>
|
||||
<div class="user-naming-format">
|
||||
<b-dropdown v-model="selectedOption">
|
||||
<template slot="button-content">{{ selectedOptionLabel }}</template>
|
||||
<b-dropdown-item
|
||||
v-for="option in dropdownOptions"
|
||||
@click.prevent="update(option)"
|
||||
:key="option.value"
|
||||
:value="option.value"
|
||||
:title="option.title"
|
||||
>
|
||||
{{ option.label }}
|
||||
</b-dropdown-item>
|
||||
</b-dropdown>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { updateUserInfos } from '@/graphql/mutations'
|
||||
|
||||
export default {
|
||||
name: 'UserNamingFormat',
|
||||
props: {
|
||||
initialValue: { type: String, default: 'PUBLISH_NAME_ALIAS_OR_INITALS' },
|
||||
attrName: { type: String },
|
||||
successMessage: { type: String },
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
selectedOption: this.initialValue,
|
||||
dropdownOptions: [
|
||||
{
|
||||
label: this.$t('settings.publish-name.alias-or-initials'),
|
||||
title: this.$t('settings.publish-name.alias-or-initials-tooltip'),
|
||||
value: 'PUBLISH_NAME_ALIAS_OR_INITALS',
|
||||
},
|
||||
{
|
||||
label: this.$t('settings.publish-name.initials'),
|
||||
title: this.$t('settings.publish-name.initials-tooltip'),
|
||||
value: 'PUBLISH_NAME_INITIALS',
|
||||
},
|
||||
{
|
||||
label: this.$t('settings.publish-name.first'),
|
||||
title: this.$t('settings.publish-name.first-tooltip'),
|
||||
value: 'PUBLISH_NAME_FIRST',
|
||||
},
|
||||
{
|
||||
label: this.$t('settings.publish-name.first-initial'),
|
||||
title: this.$t('settings.publish-name.first-initial-tooltip'),
|
||||
value: 'PUBLISH_NAME_FIRST_INITIAL',
|
||||
},
|
||||
{
|
||||
label: this.$t('settings.publish-name.name-full'),
|
||||
title: this.$t('settings.publish-name.name-full-tooltip'),
|
||||
value: 'PUBLISH_NAME_FULL',
|
||||
},
|
||||
],
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
selectedOptionLabel() {
|
||||
const selected = this.dropdownOptions.find((option) => option.value === this.selectedOption)
|
||||
.label
|
||||
return selected || this.$t('settings.publish-name.alias-or-initials')
|
||||
// return this.dropdownOptions.find((option) => option.value === this.selectedOption).label
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
async update(option) {
|
||||
if (option.value === this.selectedOption) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
const variables = []
|
||||
variables[this.attrName] = option.value
|
||||
await this.$apollo.mutate({
|
||||
mutation: updateUserInfos,
|
||||
variables,
|
||||
})
|
||||
this.toastSuccess(this.successMessage)
|
||||
this.selectedOption = option.value
|
||||
this.$store.commit(this.attrName, option.value)
|
||||
this.$emit('valueChanged', option.value)
|
||||
} catch (error) {
|
||||
this.toastError(error.message)
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
.user-naming-format > .dropdown,
|
||||
.user-naming-format > .dropdown > .dropdown-toggle > ul.dropdown-menu {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
58
frontend/src/components/UserSettings/UserSettingsSwitch.vue
Normal file
58
frontend/src/components/UserSettings/UserSettingsSwitch.vue
Normal file
@ -0,0 +1,58 @@
|
||||
<template>
|
||||
<div class="form-user-switch" @click="onClick">
|
||||
<b-form-checkbox
|
||||
test="BFormCheckbox"
|
||||
v-model="value"
|
||||
name="check-button"
|
||||
:disabled="disabled"
|
||||
switch
|
||||
@change="onChange"
|
||||
></b-form-checkbox>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { updateUserInfos } from '@/graphql/mutations'
|
||||
|
||||
export default {
|
||||
name: 'UserSettingsSwitch',
|
||||
props: {
|
||||
initialValue: { type: Boolean, default: false },
|
||||
attrName: { type: String },
|
||||
enabledText: { type: String },
|
||||
disabledText: { type: String },
|
||||
disabled: { type: Boolean, default: false },
|
||||
notAllowedText: { type: String, default: undefined },
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
value: this.initialValue,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async onChange() {
|
||||
if (this.isDisabled) return
|
||||
const variables = []
|
||||
variables[this.attrName] = this.value
|
||||
this.$apollo
|
||||
.mutate({
|
||||
mutation: updateUserInfos,
|
||||
variables,
|
||||
})
|
||||
.then(() => {
|
||||
this.$store.commit(this.attrName, this.value)
|
||||
this.$emit('valueChanged', this.value)
|
||||
this.toastSuccess(this.value ? this.enabledText : this.disabledText)
|
||||
})
|
||||
.catch((error) => {
|
||||
this.value = this.initialValue
|
||||
this.toastError(error.message)
|
||||
})
|
||||
},
|
||||
onClick() {
|
||||
if (this.notAllowedText && this.disabled) {
|
||||
this.toastError(this.notAllowedText)
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@ -8,7 +8,7 @@ const constants = {
|
||||
DECAY_START_TIME: new Date('2021-05-13 17:46:31-0000'), // GMT+0
|
||||
CONFIG_VERSION: {
|
||||
DEFAULT: 'DEFAULT',
|
||||
EXPECTED: 'v5.2024-01-08',
|
||||
EXPECTED: 'v6.2024-02-27',
|
||||
CURRENT: '',
|
||||
},
|
||||
}
|
||||
@ -20,6 +20,11 @@ const version = {
|
||||
BUILD_COMMIT_SHORT: (process.env.BUILD_COMMIT ?? '0000000').slice(0, 7),
|
||||
}
|
||||
|
||||
const features = {
|
||||
GMS_ACTIVE: process.env.GMS_ACTIVE ?? false,
|
||||
HUMHUB_ACTIVE: process.env.HUMHUB_ACTIVE ?? false,
|
||||
}
|
||||
|
||||
const environment = {
|
||||
NODE_ENV: process.env.NODE_ENV,
|
||||
DEBUG: process.env.NODE_ENV !== 'production' ?? false,
|
||||
@ -81,6 +86,7 @@ if (
|
||||
const CONFIG = {
|
||||
...constants,
|
||||
...version,
|
||||
...features,
|
||||
...environment,
|
||||
...endpoints,
|
||||
...community,
|
||||
|
||||
@ -35,9 +35,11 @@ export const updateUserInfos = gql`
|
||||
$hideAmountGDD: Boolean
|
||||
$hideAmountGDT: Boolean
|
||||
$gmsAllowed: Boolean
|
||||
$gmsPublishName: Int
|
||||
$humhubAllowed: Boolean
|
||||
$gmsPublishName: PublishNameType
|
||||
$humhubPublishName: PublishNameType
|
||||
$gmsLocation: Location
|
||||
$gmsPublishLocation: Int
|
||||
$gmsPublishLocation: GmsPublishLocationType
|
||||
) {
|
||||
updateUserInfos(
|
||||
firstName: $firstName
|
||||
@ -49,7 +51,9 @@ export const updateUserInfos = gql`
|
||||
hideAmountGDD: $hideAmountGDD
|
||||
hideAmountGDT: $hideAmountGDT
|
||||
gmsAllowed: $gmsAllowed
|
||||
humhubAllowed: $humhubAllowed
|
||||
gmsPublishName: $gmsPublishName
|
||||
humhubPublishName: $humhubPublishName
|
||||
gmsLocation: $gmsLocation
|
||||
gmsPublishLocation: $gmsPublishLocation
|
||||
)
|
||||
@ -172,6 +176,11 @@ export const login = gql`
|
||||
klickTipp {
|
||||
newsletterState
|
||||
}
|
||||
gmsAllowed
|
||||
humhubAllowed
|
||||
gmsPublishName
|
||||
humhubPublishName
|
||||
gmsPublishLocation
|
||||
hasElopage
|
||||
publisherId
|
||||
roles
|
||||
|
||||
@ -19,6 +19,20 @@ export const verifyLogin = gql`
|
||||
}
|
||||
}
|
||||
`
|
||||
export const authenticateGmsUserSearch = gql`
|
||||
query {
|
||||
authenticateGmsUserSearch {
|
||||
url
|
||||
token
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const authenticateHumhubAutoLogin = gql`
|
||||
query {
|
||||
authenticateHumhubAutoLogin
|
||||
}
|
||||
`
|
||||
|
||||
export const transactionsQuery = gql`
|
||||
query($currentPage: Int = 1, $pageSize: Int = 25, $order: Order = DESC) {
|
||||
|
||||
@ -7,6 +7,14 @@
|
||||
"85": "85%",
|
||||
"GDD": "GDD",
|
||||
"GDT": "GDT",
|
||||
"GMS": {
|
||||
"title": "Geo Matching System GMS (in Entwicklung)",
|
||||
"desc": "Finde Mitglieder aller Communities auf einer Landkarte."
|
||||
},
|
||||
"Humhub": {
|
||||
"title": "Gradido-Kreise",
|
||||
"desc": "Gemeinsam unterstützen wir einander – achtsam in Kreiskultur."
|
||||
},
|
||||
"PersonalDetails": "Persönliche Angaben",
|
||||
"advanced-calculation": "Vorausberechnung",
|
||||
"asterisks": "****",
|
||||
@ -22,6 +30,11 @@
|
||||
}
|
||||
},
|
||||
"back": "Zurück",
|
||||
"circles": {
|
||||
"headline": "Gemeinsam unterstützen wir einander – achtsam in Kreiskultur.",
|
||||
"text": "In geschützten Räumen können wir frei kommunizieren und kooperieren, ohne auf die sozialen Medien angewiesen zu sein. Mit Klick auf den Button öffnest Du die Kooperationsplattform in einem neuen Browser-Fenster.",
|
||||
"button": "Gradido-Kreise starten..."
|
||||
},
|
||||
"community": {
|
||||
"admins": "Administratoren",
|
||||
"choose-another-community": "Eine andere Gemeinschaft auswählen",
|
||||
@ -265,8 +278,10 @@
|
||||
"overview": "Übersicht",
|
||||
"send": "Senden",
|
||||
"settings": "Einstellung",
|
||||
"circles": "Kreise",
|
||||
"support": "Support",
|
||||
"transactions": "Transaktionen"
|
||||
"transactions": "Transaktionen",
|
||||
"usersearch": "Nutzersuche"
|
||||
},
|
||||
"openHours": "Offene Stunden",
|
||||
"pageTitle": {
|
||||
@ -276,7 +291,9 @@
|
||||
"overview": "Willkommen {name}",
|
||||
"send": "Sende Gradidos",
|
||||
"settings": "Einstellungen",
|
||||
"transactions": "Deine Transaktionen"
|
||||
"circles": "Gradido Kreise (Beta)",
|
||||
"transactions": "Deine Transaktionen",
|
||||
"usersearch": "Geografische Nutzersuche"
|
||||
},
|
||||
"qrCode": "QR Code",
|
||||
"send_gdd": "GDD versenden",
|
||||
@ -289,9 +306,39 @@
|
||||
"warningText": "Bist du noch da?"
|
||||
},
|
||||
"settings": {
|
||||
"allow-community-services": "Community-Dienste erlauben",
|
||||
"community": "Community",
|
||||
"emailInfo": "Kann aktuell noch nicht geändert werden.",
|
||||
"GMS": {
|
||||
"disabled": "Daten werden nicht nach GMS exportiert",
|
||||
"enabled": "Daten werden nach GMS exportiert",
|
||||
"location": {
|
||||
"label": "Positionsbestimmung",
|
||||
"button": "Klick mich!"
|
||||
},
|
||||
"location-format": "Position auf Karte anzeigen:",
|
||||
"naming-format": "Namen anzeigen:",
|
||||
"publish-location": {
|
||||
"exact": "Genaue Position",
|
||||
"approximate": "Ungefähre Position",
|
||||
"random": "Zufallsposition",
|
||||
"updated": "Positionstyp für GMS aktualisiert"
|
||||
},
|
||||
"publish-name": {
|
||||
"updated": "Namensformat für GMS aktualisiert"
|
||||
}
|
||||
},
|
||||
"hideAmountGDD": "Dein GDD Betrag ist versteckt.",
|
||||
"hideAmountGDT": "Dein GDT Betrag ist versteckt.",
|
||||
"humhub": {
|
||||
"delete-disabled": "Das Benutzerkonto kann nur im Profil-Menü der Kooperationsplattform gelöscht werden.",
|
||||
"disabled": "Daten werden nicht in die Gradido Community exportiert",
|
||||
"enabled": "Daten werden in die Gradido Community exportiert",
|
||||
"naming-format": "Namen anzeigen:",
|
||||
"publish-name": {
|
||||
"updated": "Namensformat für die Gradido Community aktualisiert."
|
||||
}
|
||||
},
|
||||
"info": "Transaktionen können nun per Benutzername oder E-Mail-Adresse getätigt werden.",
|
||||
"language": {
|
||||
"changeLanguage": "Sprache ändern",
|
||||
@ -329,6 +376,18 @@
|
||||
},
|
||||
"subtitle": "Wenn du dein Passwort vergessen hast, kannst du es hier zurücksetzen."
|
||||
},
|
||||
"publish-name": {
|
||||
"alias-or-initials": "Benutzername oder Initialen",
|
||||
"alias-or-initials-tooltip": "Benutzername, falls vorhanden, oder die Initialen von Vorname und Nachname",
|
||||
"first": "Vorname",
|
||||
"first-tooltip": "Nur der Vornamen",
|
||||
"first-initial": "Vorname und Initial",
|
||||
"first-initial-tooltip": "Vornamen plus Anfangsbuchstabe des Nachnamens",
|
||||
"initials": "Initialen",
|
||||
"initials-tooltip": "Initialen von Vor- und Nachname unabhängig von der Existenz des Benutzernamens",
|
||||
"name-full": "Vorname und Nachname",
|
||||
"name-full-tooltip": "Vollständiger Name: Vorname plus Nachname"
|
||||
},
|
||||
"showAmountGDD": "Dein GDD Betrag ist sichtbar.",
|
||||
"showAmountGDT": "Dein GDT Betrag ist sichtbar.",
|
||||
"username": {
|
||||
@ -379,6 +438,11 @@
|
||||
"transaction-link": {
|
||||
"send_you": "sendet dir"
|
||||
},
|
||||
"usersearch": {
|
||||
"headline": "Geografische Nutzersuche",
|
||||
"text": "Ganz gleich zu welcher Community du gehörst, mit dem Geo Matching System findest du Mitglieder aller Communities auf einer Landkarte. Du kannst nach Angeboten und Bedürfnissen filtern und dir die Nutzer anzeigen lassen, die zu Dir passen.\n\nMit dem Button wird ein neues Browser-Fenster geöffnet, in dem dir die Nutzer in deinem Umfeld auf einer Karte angezeigt werden.",
|
||||
"button": "Starte die Nutzersuche..."
|
||||
},
|
||||
"via_link": "über einen Link",
|
||||
"welcome": "Willkommen in der Gemeinschaft"
|
||||
}
|
||||
|
||||
@ -7,6 +7,14 @@
|
||||
"85": "85%",
|
||||
"GDD": "GDD",
|
||||
"GDT": "GDT",
|
||||
"GMS": {
|
||||
"title": "Geo Matching System GMS (in develop)",
|
||||
"desc": "Find members of all communities on a map."
|
||||
},
|
||||
"Humhub": {
|
||||
"title": "Gradido-circles",
|
||||
"desc": "Together we support each other - mindful in circle culture."
|
||||
},
|
||||
"PersonalDetails": "Personal details",
|
||||
"advanced-calculation": "Advanced calculation",
|
||||
"asterisks": "****",
|
||||
@ -22,6 +30,11 @@
|
||||
}
|
||||
},
|
||||
"back": "Back",
|
||||
"circles": {
|
||||
"headline": "Together we support each other - mindful in circle culture.",
|
||||
"text": "We can communicate and collaborate freely in protected spaces without having to rely on social media. Click on the button to open the collaboration platform in a new browser window.",
|
||||
"button": "Gradido circles start..."
|
||||
},
|
||||
"community": {
|
||||
"admins": "Administrators",
|
||||
"choose-another-community": "Choose another community",
|
||||
@ -265,8 +278,10 @@
|
||||
"overview": "Overview",
|
||||
"send": "Send",
|
||||
"settings": "Settings",
|
||||
"circles": "Circle",
|
||||
"support": "Support",
|
||||
"transactions": "Transactions"
|
||||
"transactions": "Transactions",
|
||||
"usersearch": "Geographical User Search"
|
||||
},
|
||||
"openHours": "Open Hours",
|
||||
"pageTitle": {
|
||||
@ -276,7 +291,9 @@
|
||||
"overview": "Welcome {name}",
|
||||
"send": "Send Gradidos",
|
||||
"settings": "Settings",
|
||||
"transactions": "Your transactions"
|
||||
"transactions": "Your transactions",
|
||||
"circles": "Gradido Circles (Beta)",
|
||||
"usersearch": "Geographical User Search"
|
||||
},
|
||||
"qrCode": "QR Code",
|
||||
"send_gdd": "Send GDD",
|
||||
@ -289,9 +306,39 @@
|
||||
"warningText": "Are you still there?"
|
||||
},
|
||||
"settings": {
|
||||
"allow-community-services": "Allow Community Services",
|
||||
"community": "Community",
|
||||
"emailInfo": "Cannot be changed at this time.",
|
||||
"GMS": {
|
||||
"disabled": "Data not exported to GMS",
|
||||
"enabled": "Data exported to GMS",
|
||||
"location": {
|
||||
"label": "pinpoint location",
|
||||
"button": "click me!"
|
||||
},
|
||||
"location-format": "Show position on map:",
|
||||
"naming-format": "Show Name:",
|
||||
"publish-location": {
|
||||
"exact": "exact position",
|
||||
"approximate": "approximate position",
|
||||
"random": "random position",
|
||||
"updated": "format of location for GMS updated"
|
||||
},
|
||||
"publish-name": {
|
||||
"updated": "format of name for GMS updated"
|
||||
}
|
||||
},
|
||||
"hideAmountGDD": "Your GDD amount is hidden.",
|
||||
"hideAmountGDT": "Your GDT amount is hidden.",
|
||||
"humhub": {
|
||||
"delete-disabled": "The user account can only be deleted in the profile menu of the cooperation platform.",
|
||||
"disabled": "Data not exported into the Gradido Community",
|
||||
"enabled": "Data exported into the Gradido Community",
|
||||
"naming-format": "Show Name:",
|
||||
"publish-name": {
|
||||
"updated": "Format of name for the Gradido Community updated."
|
||||
}
|
||||
},
|
||||
"info": "Transactions can now be made by username or email address.",
|
||||
"language": {
|
||||
"changeLanguage": "Change language",
|
||||
@ -329,6 +376,18 @@
|
||||
},
|
||||
"subtitle": "If you have forgotten your password, you can reset it here."
|
||||
},
|
||||
"publish-name": {
|
||||
"alias-or-initials": "Username or initials (Default)",
|
||||
"alias-or-initials-tooltip": "username if exists or Initials of firstname and lastname",
|
||||
"first": "firstname",
|
||||
"first-tooltip": "the firstname only",
|
||||
"first-initial": "firstname and initial",
|
||||
"first-initial-tooltip": "firstname plus initial of lastname",
|
||||
"initials": "Initials",
|
||||
"initials-tooltip": "Initials of firstname and lastname independent if username exists",
|
||||
"name-full": "firstname and lastname",
|
||||
"name-full-tooltip": "fullname: firstname plus lastname"
|
||||
},
|
||||
"showAmountGDD": "Your GDD amount is visible.",
|
||||
"showAmountGDT": "Your GDT amount is visible.",
|
||||
"username": {
|
||||
@ -379,6 +438,11 @@
|
||||
"transaction-link": {
|
||||
"send_you": "wants to send you"
|
||||
},
|
||||
"usersearch": {
|
||||
"headline": "Geographical User Search",
|
||||
"text": "No matter which community you belong to, with the Geo Matching System you can find members of all communities on a map. You can filter according to offers and needs and display the users that match you.\n\nThe button opens a new browser window in which the users in your area are displayed on a map.",
|
||||
"button": "Start user search... "
|
||||
},
|
||||
"via_link": "via Link",
|
||||
"welcome": "Welcome to the community"
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user