diff --git a/CHANGELOG.md b/CHANGELOG.md index bbb49eaf5..b05bd3e70 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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) diff --git a/admin/package.json b/admin/package.json index b1a2bd044..97d4f6fda 100644 --- a/admin/package.json +++ b/admin/package.json @@ -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": { diff --git a/backend/jest.config.js b/backend/jest.config.js index de649d66e..6140da0aa 100644 --- a/backend/jest.config.js +++ b/backend/jest.config.js @@ -7,7 +7,7 @@ module.exports = { collectCoverageFrom: ['src/**/*.ts', '!**/node_modules/**', '!src/seeds/**', '!build/**'], coverageThreshold: { global: { - lines: 84, + lines: 83, }, }, setupFiles: ['/test/testSetup.ts'], diff --git a/backend/package.json b/backend/package.json index 0f2d0ed20..c3b74115d 100644 --- a/backend/package.json +++ b/backend/package.json @@ -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", diff --git a/backend/src/apis/gms/GmsClient.ts b/backend/src/apis/gms/GmsClient.ts index 46fa64006..32a3802ff 100644 --- a/backend/src/apis/gms/GmsClient.ts +++ b/backend/src/apis/gms/GmsClient.ts @@ -117,30 +117,106 @@ export async function userByUuid(uuid: string): Promise { + if (CONFIG.GMS_ACTIVE) { + const baseUrl = CONFIG.GMS_URL.endsWith('/') ? CONFIG.GMS_URL : CONFIG.GMS_URL.concat('/') + 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 { + if (CONFIG.GMS_ACTIVE) { + const baseUrl = CONFIG.GMS_URL.endsWith('/') ? CONFIG.GMS_URL : CONFIG.GMS_URL.concat('/') + 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 { const baseUrl = CONFIG.GMS_URL.endsWith('/') ? CONFIG.GMS_URL : CONFIG.GMS_URL.concat('/') - const service = 'community-user' + 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) } } diff --git a/backend/src/apis/gms/model/GmsUser.ts b/backend/src/apis/gms/model/GmsUser.ts index 7f7db7660..0b16f00fb 100644 --- a/backend/src/apis/gms/model/GmsUser.ts +++ b/backend/src/apis/gms/model/GmsUser.ts @@ -8,6 +8,7 @@ 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) diff --git a/backend/src/auth/RIGHTS.ts b/backend/src/auth/RIGHTS.ts index c8f02976b..c7a23c13b 100644 --- a/backend/src/auth/RIGHTS.ts +++ b/backend/src/auth/RIGHTS.ts @@ -37,6 +37,7 @@ export enum RIGHTS { LIST_ALL_CONTRIBUTION_MESSAGES = 'LIST_ALL_CONTRIBUTION_MESSAGES', OPEN_CREATIONS = 'OPEN_CREATIONS', USER = 'USER', + GMS_USER_PLAYGROUND = 'GMS_USER_PLAYGROUND', // Moderator SEARCH_USERS = 'SEARCH_USERS', ADMIN_CREATE_CONTRIBUTION = 'ADMIN_CREATE_CONTRIBUTION', diff --git a/backend/src/auth/USER_RIGHTS.ts b/backend/src/auth/USER_RIGHTS.ts index 9bf9fee93..0c56b0d02 100644 --- a/backend/src/auth/USER_RIGHTS.ts +++ b/backend/src/auth/USER_RIGHTS.ts @@ -29,4 +29,5 @@ export const USER_RIGHTS = [ RIGHTS.LIST_ALL_CONTRIBUTION_MESSAGES, RIGHTS.OPEN_CREATIONS, RIGHTS.USER, + RIGHTS.GMS_USER_PLAYGROUND, ] diff --git a/backend/src/config/index.ts b/backend/src/config/index.ts index 7bb2bd695..b71e59488 100644 --- a/backend/src/config/index.ts +++ b/backend/src/config/index.ts @@ -19,7 +19,11 @@ const constants = { LOG_LEVEL: process.env.LOG_LEVEL ?? 'info', CONFIG_VERSION: { DEFAULT: 'DEFAULT', +<<<<<<< HEAD EXPECTED: 'v22.2024-03-06', +======= + EXPECTED: 'v22.2024-03-14', +>>>>>>> master CURRENT: '', }, } @@ -143,8 +147,11 @@ 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/', + // 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 = { diff --git a/backend/src/graphql/arg/UpdateUserInfosArgs.ts b/backend/src/graphql/arg/UpdateUserInfosArgs.ts index 0920fb3bc..a6c80cddd 100644 --- a/backend/src/graphql/arg/UpdateUserInfosArgs.ts +++ b/backend/src/graphql/arg/UpdateUserInfosArgs.ts @@ -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 { GmsPublishNameType } from '@enum/GmsPublishNameType' import { Location } from '@model/Location' import { isValidLocation } from '@/graphql/validator/Location' @@ -44,19 +46,19 @@ export class UpdateUserInfosArgs { @IsBoolean() hideAmountGDT?: boolean - @Field({ nullable: true, defaultValue: true }) + @Field({ nullable: true }) @IsBoolean() gmsAllowed?: boolean - @Field(() => Int, { nullable: true, defaultValue: 0 }) - @IsInt() - gmsPublishName?: number | null + @Field(() => GmsPublishNameType, { nullable: true }) + @IsEnum(GmsPublishNameType) + gmsPublishName?: GmsPublishNameType | 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 } diff --git a/backend/src/graphql/model/GmsUserAuthenticationResult.ts b/backend/src/graphql/model/GmsUserAuthenticationResult.ts new file mode 100644 index 000000000..b1fb2c246 --- /dev/null +++ b/backend/src/graphql/model/GmsUserAuthenticationResult.ts @@ -0,0 +1,10 @@ +import { Field, ObjectType } from 'type-graphql' + +@ObjectType() +export class GmsUserAuthenticationResult { + @Field(() => String) + url: string + + @Field(() => String) + token: string +} diff --git a/backend/src/graphql/model/User.ts b/backend/src/graphql/model/User.ts index d24a717c4..166367fd1 100644 --- a/backend/src/graphql/model/User.ts +++ b/backend/src/graphql/model/User.ts @@ -1,6 +1,9 @@ import { User as dbUser } from '@entity/User' import { ObjectType, Field, Int } from 'type-graphql' +import { GmsPublishLocationType } from '@enum/GmsPublishLocationType' +import { GmsPublishNameType } from '@enum/GmsPublishNameType' + import { KlickTipp } from './KlickTipp' @ObjectType() @@ -29,6 +32,9 @@ export class User { this.hasElopage = null this.hideAmountGDD = user.hideAmountGDD this.hideAmountGDT = user.hideAmountGDT + this.gmsAllowed = user.gmsAllowed + this.gmsPublishName = user.gmsPublishName + this.gmsPublishLocation = user.gmsPublishLocation } } @@ -74,6 +80,15 @@ export class User { @Field(() => Boolean) hideAmountGDT: boolean + @Field(() => Boolean) + gmsAllowed: boolean + + @Field(() => GmsPublishNameType, { nullable: true }) + gmsPublishName: GmsPublishNameType | 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 diff --git a/backend/src/graphql/resolver/TransactionResolver.test.ts b/backend/src/graphql/resolver/TransactionResolver.test.ts index 1bb4a86f7..4bf5ab493 100644 --- a/backend/src/graphql/resolver/TransactionResolver.test.ts +++ b/backend/src/graphql/resolver/TransactionResolver.test.ts @@ -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({ diff --git a/backend/src/graphql/resolver/UserResolver.test.ts b/backend/src/graphql/resolver/UserResolver.test.ts index c570dbd9f..e7c873fc4 100644 --- a/backend/src/graphql/resolver/UserResolver.test.ts +++ b/backend/src/graphql/resolver/UserResolver.test.ts @@ -1258,6 +1258,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: { @@ -1303,8 +1305,10 @@ describe('UserResolver', () => { mutation: updateUserInfos, variables: { gmsAllowed: false, - gmsPublishName: GmsPublishNameType.GMS_PUBLISH_NAME_FIRST_INITIAL, - gmsPublishLocation: GmsPublishLocationType.GMS_LOCATION_TYPE_APPROXIMATE, + gmsPublishName: + GmsPublishNameType[GmsPublishNameType.GMS_PUBLISH_NAME_FIRST_INITIAL], + gmsPublishLocation: + GmsPublishLocationType[GmsPublishLocationType.GMS_LOCATION_TYPE_APPROXIMATE], }, }) await expect(User.find()).resolves.toEqual([ @@ -1326,9 +1330,11 @@ describe('UserResolver', () => { mutation: updateUserInfos, variables: { gmsAllowed: true, - gmsPublishName: GmsPublishNameType.GMS_PUBLISH_NAME_ALIAS_OR_INITALS, + gmsPublishName: + GmsPublishNameType[GmsPublishNameType.GMS_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([ @@ -2670,13 +2676,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, }, }) }) diff --git a/backend/src/graphql/resolver/UserResolver.ts b/backend/src/graphql/resolver/UserResolver.ts index 3f70ce112..7c11776df 100644 --- a/backend/src/graphql/resolver/UserResolver.ts +++ b/backend/src/graphql/resolver/UserResolver.ts @@ -19,17 +19,18 @@ 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 { updateGmsUser } from '@/apis/gms/GmsClient' +import { GmsUser } from '@/apis/gms/model/GmsUser' import { subscribe } from '@/apis/KlicktippController' import { encode } from '@/auth/JWT' import { RIGHTS } from '@/auth/RIGHTS' @@ -68,7 +69,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' @@ -375,7 +378,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) @@ -395,7 +402,6 @@ export class UserResolver { 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`, @@ -541,8 +547,10 @@ export class UserResolver { @Authorized([RIGHTS.UPDATE_USER_INFOS]) @Mutation(() => Boolean) async updateUserInfos( - @Args() - { + @Args() updateUserInfosArgs: UpdateUserInfosArgs, + @Ctx() context: Context, + ): Promise { + const { firstName, lastName, alias, @@ -555,24 +563,13 @@ export class UserResolver { gmsPublishName, gmsLocation, gmsPublishLocation, - }: UpdateUserInfosArgs, - @Ctx() context: Context, - ): Promise { + } = 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 @@ -582,7 +579,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 } @@ -619,13 +617,18 @@ export class UserResolver { if (hideAmountGDT !== undefined) { user.hideAmountGDT = hideAmountGDT } - - user.gmsAllowed = gmsAllowed - user.gmsPublishName = gmsPublishName + if (gmsAllowed !== undefined) { + user.gmsAllowed = gmsAllowed + } + if (gmsPublishName !== null && gmsPublishName !== undefined) { + user.gmsPublishName = gmsPublishName + } if (gmsLocation) { user.location = Location2Point(gmsLocation) } - user.gmsPublishLocation = gmsPublishLocation + if (gmsPublishLocation !== null && gmsPublishLocation !== undefined) { + user.gmsPublishLocation = gmsPublishLocation + } // } catch (err) { // console.log('error:', err) // } @@ -649,6 +652,17 @@ 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.`) + } + } + return true } @@ -662,6 +676,21 @@ export class UserResolver { return elopageBuys } + @Authorized([RIGHTS.GMS_USER_PLAYGROUND]) + @Query(() => GmsUserAuthenticationResult) + async authenticateGmsUserSearch(@Ctx() context: Context): Promise { + 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.SEARCH_ADMIN_USERS]) @Query(() => SearchAdminUsersResult) async searchAdminUsers( diff --git a/backend/src/graphql/resolver/util/authenticateGmsUserPlayground.ts b/backend/src/graphql/resolver/util/authenticateGmsUserPlayground.ts new file mode 100644 index 000000000..cad98c683 --- /dev/null +++ b/backend/src/graphql/resolver/util/authenticateGmsUserPlayground.ts @@ -0,0 +1,17 @@ +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' + +export async function authenticateGmsUserPlayground( + token: string, + dbUser: DbUser, +): Promise { + const result = new GmsUserAuthenticationResult() + result.url = CONFIG.GMS_URL.concat('/playground') + result.token = await verifyAuthToken(dbUser.communityUuid, token) + logger.info('GmsUserAuthenticationResult:', result) + return result +} diff --git a/backend/src/graphql/resolver/util/compareGmsRelevantUserSettings.ts b/backend/src/graphql/resolver/util/compareGmsRelevantUserSettings.ts new file mode 100644 index 000000000..e40cdcdfe --- /dev/null +++ b/backend/src/graphql/resolver/util/compareGmsRelevantUserSettings.ts @@ -0,0 +1,90 @@ +import { Point } from '@dbTools/typeorm' +import { User as DbUser } from '@entity/User' + +import { UpdateUserInfosArgs } from '@/graphql/arg/UpdateUserInfosArgs' +import { GmsPublishNameType } from '@/graphql/enum/GmsPublishNameType' +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 === + GmsPublishNameType.GMS_PUBLISH_NAME_ALIAS_OR_INITALS.valueOf) || + (!updateUserInfosArgs.gmsPublishName && + orgUser.gmsPublishName && + orgUser.gmsPublishName.valueOf === + GmsPublishNameType.GMS_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 +} diff --git a/backend/src/graphql/resolver/util/sendUserToGms.ts b/backend/src/graphql/resolver/util/sendUserToGms.ts index 335141ffe..c21550b91 100644 --- a/backend/src/graphql/resolver/util/sendUserToGms.ts +++ b/backend/src/graphql/resolver/util/sendUserToGms.ts @@ -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) + } } } diff --git a/backend/src/seeds/factory/user.ts b/backend/src/seeds/factory/user.ts index 3ddddf336..f0bda3a3f 100644 --- a/backend/src/seeds/factory/user.ts +++ b/backend/src/seeds/factory/user.ts @@ -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) } diff --git a/backend/src/seeds/graphql/mutations.ts b/backend/src/seeds/graphql/mutations.ts index b10bb4b4e..22c402e65 100644 --- a/backend/src/seeds/graphql/mutations.ts +++ b/backend/src/seeds/graphql/mutations.ts @@ -35,9 +35,9 @@ export const updateUserInfos = gql` $hideAmountGDD: Boolean $hideAmountGDT: Boolean $gmsAllowed: Boolean - $gmsPublishName: Int + $gmsPublishName: GmsPublishNameType $gmsLocation: Location - $gmsPublishLocation: Int + $gmsPublishLocation: GmsPublishLocationType ) { updateUserInfos( firstName: $firstName diff --git a/backend/src/seeds/graphql/queries.ts b/backend/src/seeds/graphql/queries.ts index ed0fe6d26..b097a2710 100644 --- a/backend/src/seeds/graphql/queries.ts +++ b/backend/src/seeds/graphql/queries.ts @@ -15,6 +15,14 @@ export const verifyLogin = gql` } } ` +export const authenticateGmsUserSearch = gql` + query { + authenticateGmsUserSearch { + url + token + } + } +` export const queryOptIn = gql` query ($optIn: String!) { diff --git a/backend/src/server/createServer.ts b/backend/src/server/createServer.ts index 3f02b0afc..a901d8763 100644 --- a/backend/src/server/createServer.ts +++ b/backend/src/server/createServer.ts @@ -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(), diff --git a/backend/src/server/plugins.ts b/backend/src/server/plugins.ts index 3e0fc50e1..c4ffa4f3f 100644 --- a/backend/src/server/plugins.ts +++ b/backend/src/server/plugins.ts @@ -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)}`) } diff --git a/backend/src/webhook/gms.ts b/backend/src/webhook/gms.ts new file mode 100644 index 000000000..3a4e9c3f3 --- /dev/null +++ b/backend/src/webhook/gms.ts @@ -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 => { + 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 }) +} diff --git a/database/package.json b/database/package.json index caeb917c4..c0f7496f0 100644 --- a/database/package.json +++ b/database/package.json @@ -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", diff --git a/deployment/bare_metal/.env.dist b/deployment/bare_metal/.env.dist index 8b1c8f510..68280c096 100644 --- a/deployment/bare_metal/.env.dist +++ b/deployment/bare_metal/.env.dist @@ -26,7 +26,7 @@ EMAIL_CODE_REQUEST_TIME=10 # config versions DATABASE_CONFIG_VERSION=v1.2022-03-18 BACKEND_CONFIG_VERSION=v21.2024-01-06 -FRONTEND_CONFIG_VERSION=v5.2024-01-08 +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,12 +120,13 @@ 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_URL=http://localhost:4044/ # HUMHUB HUMHUB_ACTIVE=false #HUMHUB_API_URL=https://community.gradido.net -#HUMHUB_JWT_KEY= \ No newline at end of file +#HUMHUB_JWT_KEY= + diff --git a/deployment/hetzner_cloud/README.md b/deployment/hetzner_cloud/README.md index d2cd66a35..a0dfe79e3 100644 --- a/deployment/hetzner_cloud/README.md +++ b/deployment/hetzner_cloud/README.md @@ -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 diff --git a/deployment/hetzner_cloud/install.sh b/deployment/hetzner_cloud/install.sh index b51f9b454..1be3548cc 100755 --- a/deployment/hetzner_cloud/install.sh +++ b/deployment/hetzner_cloud/install.sh @@ -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 < $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 \ No newline at end of file +sudo -u gradido $SCRIPT_PATH/start.sh $1 \ No newline at end of file diff --git a/deployment/hetzner_cloud/migration/2_2_0-2_2_1/README.md b/deployment/hetzner_cloud/migration/2_2_0-2_2_1/README.md new file mode 100644 index 000000000..38c4e8330 --- /dev/null +++ b/deployment/hetzner_cloud/migration/2_2_0-2_2_1/README.md @@ -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 + diff --git a/deployment/hetzner_cloud/migration/2_2_0-2_2_1/fixInstall.sh b/deployment/hetzner_cloud/migration/2_2_0-2_2_1/fixInstall.sh new file mode 100755 index 000000000..1995b220d --- /dev/null +++ b/deployment/hetzner_cloud/migration/2_2_0-2_2_1/fixInstall.sh @@ -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 < $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 \ No newline at end of file diff --git a/dht-node/package.json b/dht-node/package.json index 7a73791d5..a4859e394 100644 --- a/dht-node/package.json +++ b/dht-node/package.json @@ -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/", diff --git a/dlt-connector/package.json b/dlt-connector/package.json index 7aa8aa10d..30cd4b33b 100644 --- a/dlt-connector/package.json +++ b/dlt-connector/package.json @@ -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/", diff --git a/docu/Concepts/TechnicalRequirements/UC_publish_user_to_GMS.md b/docu/Concepts/TechnicalRequirements/UC_publish_user_to_GMS.md index 409534748..6abfdaaa6 100644 --- a/docu/Concepts/TechnicalRequirements/UC_publish_user_to_GMS.md +++ b/docu/Concepts/TechnicalRequirements/UC_publish_user_to_GMS.md @@ -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: +![](./image/usecase-user_search.svg) + + + +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... diff --git a/docu/Concepts/TechnicalRequirements/image/usecase-user_search.svg b/docu/Concepts/TechnicalRequirements/image/usecase-user_search.svg new file mode 100644 index 000000000..7430ce4a8 --- /dev/null +++ b/docu/Concepts/TechnicalRequirements/image/usecase-user_search.svg @@ -0,0 +1,4 @@ + + + +
gradido-frontend
gradido-frontend
activation of menu-entry
"user-search"
activation of menu-entry...
gradido-backend
gradido-backend
6.a
6.a
6.b
6.b
authUserForGmsUserSearch
authUserForGmsUserSearch
1.a
1.a
1.b
1.b
gms-frontend
gms-frontend
user-playground uri
user-playground uri
gms-backend
gms-backend
3.b
3.b
3.a
3.a
verifyAuthToken
verifyAuthToken
2.a
2.a
2.b
2.b
4.a
4.a
4.b
4.b
community-auth-url
community-auth-url
5.b
5.b
5.a
5.a
7.a
7.a
7.b
7.b
Text is not SVG - cannot display
\ No newline at end of file diff --git a/federation/package.json b/federation/package.json index fcfaa0e46..9a74201ab 100644 --- a/federation/package.json +++ b/federation/package.json @@ -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", diff --git a/frontend/.env.dist b/frontend/.env.dist index f7e7edcd6..f680d9a50 100644 --- a/frontend/.env.dist +++ b/frontend/.env.dist @@ -21,3 +21,5 @@ 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 diff --git a/frontend/.env.template b/frontend/.env.template index c365ab8cf..c121545da 100644 --- a/frontend/.env.template +++ b/frontend/.env.template @@ -24,3 +24,5 @@ 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 \ No newline at end of file diff --git a/frontend/package.json b/frontend/package.json index e54ffa263..4dd840261 100755 --- a/frontend/package.json +++ b/frontend/package.json @@ -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", diff --git a/frontend/src/App.vue b/frontend/src/App.vue index 46f7dacb8..663c9e7d4 100755 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -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; +} diff --git a/frontend/src/components/CommunitySwitch.vue b/frontend/src/components/CommunitySwitch.vue index ac9ce3182..4ff3d9781 100644 --- a/frontend/src/components/CommunitySwitch.vue +++ b/frontend/src/components/CommunitySwitch.vue @@ -90,20 +90,3 @@ export default { }, } - diff --git a/frontend/src/components/UserSettings/UserGMSLocation.vue b/frontend/src/components/UserSettings/UserGMSLocation.vue new file mode 100644 index 000000000..0d7fe9ab7 --- /dev/null +++ b/frontend/src/components/UserSettings/UserGMSLocation.vue @@ -0,0 +1,8 @@ + + diff --git a/frontend/src/components/UserSettings/UserGMSLocationFormat.spec.js b/frontend/src/components/UserSettings/UserGMSLocationFormat.spec.js new file mode 100644 index 000000000..9ec235d95 --- /dev/null +++ b/frontend/src/components/UserSettings/UserGMSLocationFormat.spec.js @@ -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') + }) + }) +}) diff --git a/frontend/src/components/UserSettings/UserGMSLocationFormat.vue b/frontend/src/components/UserSettings/UserGMSLocationFormat.vue new file mode 100644 index 000000000..d20135d5e --- /dev/null +++ b/frontend/src/components/UserSettings/UserGMSLocationFormat.vue @@ -0,0 +1,73 @@ + + + diff --git a/frontend/src/components/UserSettings/UserGMSNamingFormat.spec.js b/frontend/src/components/UserSettings/UserGMSNamingFormat.spec.js new file mode 100644 index 000000000..3dbbfdb2c --- /dev/null +++ b/frontend/src/components/UserSettings/UserGMSNamingFormat.spec.js @@ -0,0 +1,81 @@ +import { mount } from '@vue/test-utils' +import UserGMSNamingFormat from './UserGMSNamingFormat.vue' +import { toastErrorSpy } from '@test/testSetup' + +const mockAPIcall = jest.fn() + +const storeCommitMock = jest.fn() + +const localVue = global.localVue + +describe('UserGMSNamingFormat', () => { + let wrapper + beforeEach(() => { + wrapper = mount(UserGMSNamingFormat, { + mocks: { + $t: (key) => key, // Mocking the translation function + $store: { + state: { + gmsPublishName: null, + }, + commit: storeCommitMock, + }, + $apollo: { + mutate: mockAPIcall, + }, + }, + localVue, + propsData: { + selectedOption: 'GMS_PUBLISH_NAME_ALIAS_OR_INITALS', + }, + }) + }) + + 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.GMS.publish-name.alias-or-initials', + 'settings.GMS.publish-name.initials', + 'settings.GMS.publish-name.first', + 'settings.GMS.publish-name.first-initial', + 'settings.GMS.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().gmsPublishName).toBeTruthy() + expect(wrapper.emitted().gmsPublishName.length).toBe(1) + expect(wrapper.emitted().gmsPublishName[0]).toEqual(['GMS_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().gmsPublishName).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') + }) + }) +}) diff --git a/frontend/src/components/UserSettings/UserGMSNamingFormat.vue b/frontend/src/components/UserSettings/UserGMSNamingFormat.vue new file mode 100644 index 000000000..29b4cd384 --- /dev/null +++ b/frontend/src/components/UserSettings/UserGMSNamingFormat.vue @@ -0,0 +1,87 @@ + + + diff --git a/frontend/src/components/UserSettings/UserGMSSwitch.vue b/frontend/src/components/UserSettings/UserGMSSwitch.vue new file mode 100644 index 000000000..355beeff2 --- /dev/null +++ b/frontend/src/components/UserSettings/UserGMSSwitch.vue @@ -0,0 +1,45 @@ + + diff --git a/frontend/src/config/index.js b/frontend/src/config/index.js index dd2e85dac..1ead8ecf0 100644 --- a/frontend/src/config/index.js +++ b/frontend/src/config/index.js @@ -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,10 @@ const version = { BUILD_COMMIT_SHORT: (process.env.BUILD_COMMIT ?? '0000000').slice(0, 7), } +const features = { + GMS_ACTIVE: process.env.GMS_ACTIVE ?? false, +} + const environment = { NODE_ENV: process.env.NODE_ENV, DEBUG: process.env.NODE_ENV !== 'production' ?? false, @@ -81,6 +85,7 @@ if ( const CONFIG = { ...constants, ...version, + ...features, ...environment, ...endpoints, ...community, diff --git a/frontend/src/graphql/mutations.js b/frontend/src/graphql/mutations.js index cade098da..8d28bbff6 100644 --- a/frontend/src/graphql/mutations.js +++ b/frontend/src/graphql/mutations.js @@ -35,9 +35,9 @@ export const updateUserInfos = gql` $hideAmountGDD: Boolean $hideAmountGDT: Boolean $gmsAllowed: Boolean - $gmsPublishName: Int + $gmsPublishName: GmsPublishNameType $gmsLocation: Location - $gmsPublishLocation: Int + $gmsPublishLocation: GmsPublishLocationType ) { updateUserInfos( firstName: $firstName @@ -172,6 +172,9 @@ export const login = gql` klickTipp { newsletterState } + gmsAllowed + gmsPublishName + gmsPublishLocation hasElopage publisherId roles diff --git a/frontend/src/locales/de.json b/frontend/src/locales/de.json index 337722940..1c37d5e24 100644 --- a/frontend/src/locales/de.json +++ b/frontend/src/locales/de.json @@ -5,8 +5,10 @@ "1000thanks": "1000 Dank, weil du bei uns bist!", "125": "125%", "85": "85%", + "ExternServices": "Verknüpfte Dienste", "GDD": "GDD", "GDT": "GDT", + "GMS": "Gradido Karte", "PersonalDetails": "Persönliche Angaben", "advanced-calculation": "Vorausberechnung", "asterisks": "****", @@ -290,6 +292,36 @@ }, "settings": { "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": "Positionstyp", + "naming-format": "Namensformat im GMS", + "publish-location": { + "exact": "Genaue Position", + "approximate": "Ungefähre Position", + "random": "Zufallsposition", + "updated": "Positionstyp für GMS aktualisiert" + }, + "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 Initiale", + "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": "Ganzer Name", + "name-full-tooltip": "Vollständiger Name: Vorname plus Nachname", + "updated": "Namensformat für GMS aktualisiert" + }, + "switch": "Erlaubnis Daten nach GMS zu exportieren." + }, "hideAmountGDD": "Dein GDD Betrag ist versteckt.", "hideAmountGDT": "Dein GDT Betrag ist versteckt.", "info": "Transaktionen können nun per Benutzername oder E-Mail-Adresse getätigt werden.", diff --git a/frontend/src/locales/en.json b/frontend/src/locales/en.json index 982d22159..7c607ecbb 100644 --- a/frontend/src/locales/en.json +++ b/frontend/src/locales/en.json @@ -5,8 +5,10 @@ "1000thanks": "1000 thanks for being with us!", "125": "125%", "85": "85%", + "ExternServices": "Extern Services", "GDD": "GDD", "GDT": "GDT", + "GMS": "Gradido Map", "PersonalDetails": "Personal details", "advanced-calculation": "Advanced calculation", "asterisks": "****", @@ -290,6 +292,36 @@ }, "settings": { "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": "location type", + "naming-format": "Format of name in GMS", + "publish-location": { + "exact": "exact position", + "approximate": "approximate position", + "random": "random position", + "updated": "format of location for GMS updated" + }, + "publish-name": { + "alias-or-initials": "Username or initials", + "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 of firstname and lastname independent if username exists", + "initials-tooltip": "Initials of firstname and lastname independent if username exists", + "name-full": "fullname", + "name-full-tooltip": "fullname: firstname plus lastname", + "updated": "format of name for GMS updated" + }, + "switch": "Allow data export to GMS" + }, "hideAmountGDD": "Your GDD amount is hidden.", "hideAmountGDT": "Your GDT amount is hidden.", "info": "Transactions can now be made by username or email address.", diff --git a/frontend/src/pages/Settings.vue b/frontend/src/pages/Settings.vue index 1530e5e97..c74af6679 100644 --- a/frontend/src/pages/Settings.vue +++ b/frontend/src/pages/Settings.vue @@ -1,81 +1,129 @@