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/src/apis/gms/GmsClient.ts b/backend/src/apis/gms/GmsClient.ts index 87b4c0e95..32a3802ff 100644 --- a/backend/src/apis/gms/GmsClient.ts +++ b/backend/src/apis/gms/GmsClient.ts @@ -183,3 +183,40 @@ export async function updateGmsUser(apiKey: string, user: GmsUser): Promise { + const baseUrl = CONFIG.GMS_URL.endsWith('/') ? CONFIG.GMS_URL : CONFIG.GMS_URL.concat('/') + 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, + }, + } + try { + 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 verify-auth-token:', + result.status, + result.statusText, + ) + } + logger.debug('responseData:', result.data.responseData) + // eslint-disable-next-line @typescript-eslint/no-unsafe-call + const token: string = result.data.responseData.token + logger.debug('verifyAuthToken=', token) + return token + } catch (error: any) { + logger.error('Error in verifyAuthToken:', error) + throw new LogError(error.message) + } +} 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 250a0f2ea..dd35d180e 100644 --- a/backend/src/config/index.ts +++ b/backend/src/config/index.ts @@ -19,7 +19,7 @@ const constants = { LOG_LEVEL: process.env.LOG_LEVEL ?? 'info', CONFIG_VERSION: { DEFAULT: 'DEFAULT', - EXPECTED: 'v21.2024-01-06', + EXPECTED: 'v22.2024-03-14', CURRENT: '', }, } @@ -146,6 +146,8 @@ const gms = { 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', } export const CONFIG = { 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/resolver/UserResolver.ts b/backend/src/graphql/resolver/UserResolver.ts index 84ee660df..7c11776df 100644 --- a/backend/src/graphql/resolver/UserResolver.ts +++ b/backend/src/graphql/resolver/UserResolver.ts @@ -25,6 +25,7 @@ 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' @@ -68,6 +69,7 @@ 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' @@ -674,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/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 }) +}