diff --git a/package.json b/package.json index 0e73351a5..0466a0296 100644 --- a/package.json +++ b/package.json @@ -123,6 +123,7 @@ "connect-timeout": "1.9.0", "core-js": "^3.37.1", "cors": "2.8.5", + "crypto-browserify": "3.12.0", "csv-parse": "5.5.6", "dayjs": "1.11.11", "deepmerge": "4.3.1", diff --git a/packages/itmat-apis/src/graphql/resolvers/studyResolvers.ts b/packages/itmat-apis/src/graphql/resolvers/studyResolvers.ts index a43aeab6c..9e82c4f7a 100644 --- a/packages/itmat-apis/src/graphql/resolvers/studyResolvers.ts +++ b/packages/itmat-apis/src/graphql/resolvers/studyResolvers.ts @@ -72,7 +72,7 @@ export class StudyResolvers { async getDataRecords(_parent, { studyId, queryString, versionId }: { queryString: IQueryString, studyId: string, versionId: string | null | undefined }, context) { try { - const result = (await this.dataCore.getData(context.req.user, studyId, queryString.data_requested, versionId))['raw'] as unknown as IData[] & { properties: { subjectId: string, visitId: string }, fieldId: string, value: string }[]; + const result = (await this.dataCore.getData(context.req.user, studyId, queryString.data_requested, versionId)) as unknown as IData[] & { properties: { subjectId: string, visitId: string }, fieldId: string, value: string }[]; const groupedResult: IGroupedData = {}; for (let i = 0; i < result.length; i++) { const { subjectId, visitId } = result[i].properties; diff --git a/packages/itmat-apis/src/trpc/userProcedure.ts b/packages/itmat-apis/src/trpc/userProcedure.ts index 908b13ef0..017f1e1fc 100644 --- a/packages/itmat-apis/src/trpc/userProcedure.ts +++ b/packages/itmat-apis/src/trpc/userProcedure.ts @@ -273,13 +273,40 @@ export class UserRouter { return await this.userCore.registerPubkey(opts.ctx.user, opts.input.pubkey, opts.input.signature, opts.input.associatedUserId); }), /** - * Issue an access token. + * Request an access token. + * @param username - The username of the user. + * @param pubkey - The public key. * + * @return challenge + */ + requestAccessToken: this.baseProcedure.input(z.object({ + username: z.string(), + pubkey: z.string() + })).mutation(async (opts) => { + return await this.userCore.requestAccessToken(opts.input.username, opts.input.pubkey); + }), + /** + * Get an access token. + * @param username - The username of the user. * @param pubkey - The public key. - * @param signature - The signature of the public key. - * @param life - The life of the token. - * @return IAccessToken + * + * @return token */ + getAccessToken: this.baseProcedure.input(z.object({ + username: z.string(), + pubkey: z.string(), + signature: z.string() + })).mutation(async (opts) => { + return await this.userCore.getAccessToken(opts.input.username, opts.input.pubkey, opts.input.signature); + }), + /** + * Issue an access token. + * + * @param pubkey - The public key. + * @param signature - The signature of the public key. + * @param life - The life of the token. + * @return IAccessToken + */ issueAccessToken: this.baseProcedure.input(z.object({ pubkey: z.string(), signature: z.string(), @@ -288,17 +315,17 @@ export class UserRouter { return await this.userCore.issueAccessToken(opts.input.pubkey, opts.input.signature, opts.input.life); }), /** - * Delete a public key. - * - * @param keyId - The id of the public key. - * @param associatedUserId - The id of the user. - */ + * Delete a public key. + * + * @param keyId - The id of the public key. + * @param associatedUserId - The id of the user. + */ deletePubkey: this.baseProcedure.input(z.object({ - keyId: z.string(), - associatedUserId: z.string() + associatedUserId: z.string(), + keyId: z.string() })).mutation(async (opts) => { - return await this.userCore.deletePubkey(opts.ctx.user, opts.input.keyId, opts.input.associatedUserId); + return await this.userCore.deletePubkey(opts.ctx.user, opts.input.associatedUserId, opts.input.keyId); }) }); } -} \ No newline at end of file +} diff --git a/packages/itmat-cores/src/coreFunc/dataCore.ts b/packages/itmat-cores/src/coreFunc/dataCore.ts index df09c6623..27df16f3a 100644 --- a/packages/itmat-cores/src/coreFunc/dataCore.ts +++ b/packages/itmat-cores/src/coreFunc/dataCore.ts @@ -803,16 +803,11 @@ export class DataCore { return await getJsonFileContents(this.objStore, 'cache', hashedInfo[0].uri); } else { // raw data by the permission - const data = await this.getDataByRoles(roles, studyId, availableDataVersions, fieldIds); - // data versioning - const filteredData = this.dataTransformationCore.transformationAggregate(data, { raw: this.genVersioningAggregation((config.properties as IStudyConfig).defaultVersioningKeys, availableDataVersions.includes(null)) }); - if (!Array.isArray(filteredData['raw']) || (filteredData['raw'].length > 0 && Array.isArray(filteredData['raw'][0]))) { - throw new Error('Input data must be of type IDataTransformationClipArray (A[]) and not A[][]'); - } + const data = await this.getDataByRoles(requester, roles, studyId, availableDataVersions, fieldIds); // data transformation if aggregation is provided - const transformed = aggregation ? this.dataTransformationCore.transformationAggregate(filteredData['raw'] as IDataTransformationClipArray, aggregation) : filteredData; + const transformed = aggregation ? this.dataTransformationCore.transformationAggregate(data as unknown as IDataTransformationClipArray, aggregation) : data; // write to minio and cache collection - const info = await convertToBufferAndUpload(this.fileCore, requester, transformed); + const info = await convertToBufferAndUpload(this.fileCore, requester, { data: data }); const newHashInfo = { id: uuid(), keyHash: hash, @@ -840,14 +835,9 @@ export class DataCore { } } else { // raw data by the permission - const data = await this.getDataByRoles(roles, studyId, availableDataVersions, fieldIds); - // data versioning - const filteredData = this.dataTransformationCore.transformationAggregate(data, { raw: this.genVersioningAggregation((config.properties as IStudyConfig).defaultVersioningKeys, availableDataVersions.includes(null)) }); - if (!Array.isArray(filteredData['raw']) || (filteredData['raw'].length > 0 && Array.isArray(filteredData['raw'][0]))) { - throw new Error('Input data must be of type IDataTransformationClipArray (A[]) and not A[][]'); - } + const data = await this.getDataByRoles(requester, roles, studyId, availableDataVersions, fieldIds); // data transformation if aggregation is provided - const transformed = aggregation ? this.dataTransformationCore.transformationAggregate(filteredData['raw'] as IDataTransformationClipArray, aggregation) : filteredData; + const transformed = aggregation ? this.dataTransformationCore.transformationAggregate(data as unknown as IDataTransformationClipArray, aggregation) : data; return transformed; } } @@ -1006,18 +996,18 @@ export class DataCore { if (fieldIds.length === 0) { return []; } - const fileDataRecords = (await this.getData( + const fileDataRecords: IData[] = (await this.getData( requester, studyId, fieldIds, availableDataVersions, undefined, false - ))['raw']; + )) as unknown as IData[]; if (!Array.isArray(fileDataRecords)) { return []; } - const files = await this.db.collections.files_collection.find({ id: { $in: fileDataRecords.map(el => el.value) } }).toArray(); + const files = await this.db.collections.files_collection.find({ id: { $in: fileDataRecords.map(el => String(el.value)) } }).toArray(); if (readable) { const users = await this.db.collections.users_collection.find({}).toArray(); const edited = [...files]; @@ -1074,19 +1064,12 @@ export class DataCore { } - public async getDataByRoles(roles: IRole[], studyId: string, dataVersions: Array, fieldIds?: string[]) { + public async getDataByRoles(requester: IUserWithoutToken, roles: IRole[], studyId: string, dataVersions: Array, fieldIds?: string[]) { const matchFilter: Filter = { studyId: studyId, dataVersion: { $in: dataVersions } }; - if (fieldIds && fieldIds[0]) { - // we ask that for regular expressions, ^ and $ must be used - if (fieldIds[0][0] === '^' && fieldIds[0][fieldIds[0].length - 1] === '$') { - matchFilter.fieldId = { $in: fieldIds.map(el => new RegExp(el)) }; - } else { - matchFilter.fieldId = { $in: fieldIds }; - } - } + const roleArr: Filter[] = []; for (const role of roles) { const permissionArr: Filter[] = []; @@ -1112,11 +1095,44 @@ export class DataCore { } roleArr.push({ $or: permissionArr }); } - const res = await this.db.collections.data_collection.aggregate([{ - $match: { ...matchFilter } - }, { - $match: { $or: roleArr } - }], { allowDiskUse: true }).toArray(); + + // we need to query each field based on its properties + const availableFields = (await this.getStudyFields(requester, studyId, dataVersions)).reduce((a, c) => { + a[c.fieldId] = c; + return a; + }, {}); + const availableFieldIds = Object.keys(availableFields); + const refactoredFieldIds = fieldIds ?? Object.keys(availableFields); + let res: IData[] = []; + for (const fieldId of refactoredFieldIds) { + if (availableFieldIds.includes(fieldId) || availableFieldIds.some(el => new RegExp(el).test(fieldId))) { + const propertyFilter = {}; + if (availableFields[fieldId].properties) { + for (const property of availableFields[fieldId].properties) { + propertyFilter[`${property.name}`] = `$properties.${property.name}`; + } + } + const data = await this.db.collections.data_collection.aggregate([{ + $match: { ...matchFilter, fieldId: fieldId } + }, { + $match: { $or: roleArr } + }, { + $sort: { + 'life.createdTime': -1 + } + }, { + $group: { + _id: { + ...propertyFilter + }, + latestDocument: { $first: '$$ROOT' } + } + }, { + $replaceRoot: { newRoot: '$latestDocument' } + }], { allowDiskUse: true }).toArray(); + res = res.concat(data); + } + } return res; } @@ -1320,22 +1336,18 @@ export class DataCore { } const generatedSummary = async () => { - const numberOfDataRecords: number = await this.db.collections.data_collection.countDocuments({ studyId: studyId }); - const numberOfDataAdds: number = await this.db.collections.data_collection.countDocuments({ studyId: studyId, value: { $ne: null } }); - const numberOfDataDeletes: number = await this.db.collections.data_collection.countDocuments({ studyId: studyId, value: null }); - - const numberOfVersionedRecords: number = await this.db.collections.data_collection.countDocuments({ studyId: studyId, dataVersion: { $ne: null } }); - const numberOfVersionedAdds: number = await this.db.collections.data_collection.countDocuments({ studyId: studyId, dataVersion: { $ne: null }, value: { $ne: null } }); - const numberOfVersionedDeletes: number = await this.db.collections.data_collection.countDocuments({ studyId: studyId, dataVersion: { $ne: null }, value: null }); - - const numberOfUnversionedRecords: number = await this.db.collections.data_collection.countDocuments({ studyId: studyId, dataVersion: null }); - const numberOfUnversionedAdds: number = await this.db.collections.data_collection.countDocuments({ studyId: studyId, dataVersion: null, value: { $ne: null } }); - const numberOfUnversionedDeletes: number = await this.db.collections.data_collection.countDocuments({ studyId: studyId, dataVersion: null, value: null }); - + const numberOfDataRecords: number = await this.db.collections.data_collection.countDocuments({ studyId: studyId }, { allowDiskUse: true }); + const numberOfDataAdds: number = await this.db.collections.data_collection.countDocuments({ studyId: studyId, value: { $ne: null } }, { allowDiskUse: true }); + const numberOfDataDeletes: number = await this.db.collections.data_collection.countDocuments({ studyId: studyId, value: null }, { allowDiskUse: true }); + const numberOfVersionedRecords: number = await this.db.collections.data_collection.countDocuments({ studyId: studyId, dataVersion: { $ne: null } }, { allowDiskUse: true }); + const numberOfVersionedAdds: number = await this.db.collections.data_collection.countDocuments({ studyId: studyId, dataVersion: { $ne: null }, value: { $ne: null } }, { allowDiskUse: true }); + const numberOfVersionedDeletes: number = await this.db.collections.data_collection.countDocuments({ studyId: studyId, dataVersion: { $ne: null }, value: null }, { allowDiskUse: true }); + const numberOfUnversionedRecords: number = await this.db.collections.data_collection.countDocuments({ studyId: studyId, dataVersion: null }, { allowDiskUse: true }); + const numberOfUnversionedAdds: number = await this.db.collections.data_collection.countDocuments({ studyId: studyId, dataVersion: null, value: { $ne: null } }, { allowDiskUse: true }); + const numberOfUnversionedDeletes: number = await this.db.collections.data_collection.countDocuments({ studyId: studyId, dataVersion: null, value: null }, { allowDiskUse: true }); const numberOfFields: number = (await this.db.collections.field_dictionary_collection.distinct('fieldId', { studyId: studyId })).length; const numberOfVersionedFields: number = (await this.db.collections.field_dictionary_collection.distinct('fieldId', { studyId: studyId, dataVersion: { $ne: null } })).length; const numberOfUnversionedFields: number = (await this.db.collections.field_dictionary_collection.distinct('fieldId', { studyId: studyId, dataVersion: null })).length; - const dataByUploaders = await this.db.collections.data_collection.aggregate<{ userId: string, count: number }>([ { $match: { studyId: studyId } }, { @@ -1351,8 +1363,8 @@ export class DataCore { count: 1 } } - ]).toArray(); - + ], { allowDiskUse: true }).toArray(); + console.log('dataByUploaders done'); const events = ['GET_DATA_RECORDS', 'GET_STUDY_FIELDS', 'GET_STUDY', 'data.getStudyFields', 'data.getStudyData', 'data.getStudyDataLatest', 'data.getFiles' ]; @@ -1379,8 +1391,8 @@ export class DataCore { count: 1 } } - ]).toArray(); - + ], { allowDiskUse: true }).toArray(); + console.log('dataByUsers done'); return { numberOfDataRecords: numberOfDataRecords, numberOfDataAdds: numberOfDataAdds, diff --git a/packages/itmat-cores/src/coreFunc/domainCore.ts b/packages/itmat-cores/src/coreFunc/domainCore.ts index 2672c96f1..bd38e82d3 100644 --- a/packages/itmat-cores/src/coreFunc/domainCore.ts +++ b/packages/itmat-cores/src/coreFunc/domainCore.ts @@ -38,7 +38,9 @@ export class DomainCore { ); } - const obj: Filter = {}; + const obj: Filter = { + 'life.deletedTime': null + }; if (domainId) { obj.id = domainId; } diff --git a/packages/itmat-cores/src/coreFunc/logCore.ts b/packages/itmat-cores/src/coreFunc/logCore.ts index 1a6aad735..b92fc44c7 100644 --- a/packages/itmat-cores/src/coreFunc/logCore.ts +++ b/packages/itmat-cores/src/coreFunc/logCore.ts @@ -62,7 +62,7 @@ export class LogCore { } let logs: ILog[]; if (indexRange) { - logs = await this.db.collections.log_collection.find(filters).skip(indexRange[0]).limit(indexRange[1] - indexRange[0]).toArray(); + logs = await this.db.collections.log_collection.find(filters).sort({ 'life.createdTime': -1 }).skip(indexRange[0]).limit(indexRange[1] - indexRange[0]).toArray(); } else { logs = await this.db.collections.log_collection.find(filters).toArray(); } diff --git a/packages/itmat-cores/src/coreFunc/userCore.ts b/packages/itmat-cores/src/coreFunc/userCore.ts index 1dfb4fdb2..fff19d060 100644 --- a/packages/itmat-cores/src/coreFunc/userCore.ts +++ b/packages/itmat-cores/src/coreFunc/userCore.ts @@ -673,7 +673,7 @@ export class UserCore { * * @return IPubkey - The object of ther registered key. */ - public async registerPubkey(requester: IUserWithoutToken | undefined, pubkey: string, signature: string, associatedUserId: string): Promise { + public async registerPubkey(requester: IUserWithoutToken | undefined, pubkey: string, signature: string | undefined, associatedUserId: string): Promise { if (!requester) { throw new CoreError( enumCoreErrors.NOT_LOGGED_IN, @@ -705,22 +705,6 @@ export class UserCore { ); } - /* Validate the signature with the public key */ - try { - const signature_verifier = await rsaverifier(pubkey, signature); - if (!signature_verifier) { - throw new CoreError( - enumCoreErrors.CLIENT_MALFORMED_INPUT, - 'Signature vs Public-key mismatched.' - ); - } - } catch (error) { - throw new CoreError( - enumCoreErrors.CLIENT_MALFORMED_INPUT, - 'Error: Signature or Public-key is incorrect.' - ); - } - /* Generate a public key-pair for generating and authenticating JWT access token later */ const keypair = rsakeygen(); @@ -737,6 +721,8 @@ export class UserCore { deletedTime: null, deletedUser: null }, + challenge: null, + lastUsedTime: null, metadata: {} }; @@ -764,6 +750,159 @@ export class UserCore { }); return entry; } + /** + * Generate a random challenge. + * + * @param length - The length of the challenge. + * @returns - The challenge. + */ + public generateChallenge(length) { + // Generate a random buffer of the desired byte length + const randomBytes = crypto.randomBytes(length / 2); // `length / 2` to get a hex string of desired length + // Convert the buffer to a hex string and return it + return randomBytes.toString('hex').slice(0, length); + } + + /** + * Request an access token. + * + * @param username - The username. + * @param pubkeyKey - The public key. + * @returns - The challenge. + */ + public async requestAccessToken(username: string, pubkeyKey: string) { + const user = await this.db.collections.users_collection.findOne({ 'username': username, 'life.deletedTime': null }); + if (!user) { + throw new CoreError( + enumCoreErrors.CLIENT_MALFORMED_INPUT, + 'This public-key has not been registered yet.' + ); + } + + const pubkeyrec = await this.db.collections.pubkeys_collection.findOne({ 'associatedUserId': user.id, 'pubkey': pubkeyKey, 'life.deletedTime': null }); + + if (!pubkeyrec) { + throw new CoreError( + enumCoreErrors.CLIENT_MALFORMED_INPUT, + 'This public-key has not been registered yet.' + ); + } + const challenge = this.generateChallenge(128); + const hashedChallenge = crypto.createHash('sha256').update(challenge).digest('hex'); + await this.db.collections.pubkeys_collection.findOneAndUpdate({ id: pubkeyrec.id }, { $set: { challenge: hashedChallenge } }); + return { challenge: hashedChallenge }; + } + + /** + * Get an access token. + * + * @param username - The username. + * @param pubkeyKey - The public key. + * @param signature - The signature. + * @param life - The life of the token. + * @returns - The token. + */ + public async getAccessToken(username: string, pubkeyKey: string, signature: string, life = 12000) { + const user = await this.db.collections.users_collection.findOne({ 'username': username, 'life.deletedTime': null }); + if (!user) { + throw new CoreError( + enumCoreErrors.CLIENT_MALFORMED_INPUT, + 'This public-key has not been registered yet.' + ); + } + const pubkeyrec = await this.db.collections.pubkeys_collection.findOne({ 'associatedUserId': user.id, 'pubkey': pubkeyKey, 'life.deletedTime': null }); + if (!pubkeyrec) { + throw new CoreError( + enumCoreErrors.CLIENT_MALFORMED_INPUT, + 'This public-key has not been registered yet.' + ); + } + + if (!pubkeyrec.challenge) { + throw new CoreError( + enumCoreErrors.CLIENT_MALFORMED_INPUT, + 'Please request a challenge first.' + ); + } + + const isVerified = crypto.verify( + null, // No hash algorithm because the challenge is already hashed + Buffer.from(pubkeyrec.challenge, 'hex'), // Convert the hex-encoded challenge to binary + { + key: pubkeyrec.pubkey, + padding: crypto.constants.RSA_PKCS1_PSS_PADDING + }, + Buffer.from(signature, 'base64') // Decode the Base64-encoded signature + ); + + if (!isVerified) { + throw new CoreError( + enumCoreErrors.CLIENT_MALFORMED_INPUT, + 'Signature is not correct.' + ); + } + const token = tokengen({ + username: user.username, + publicKey: pubkeyrec.jwtPubkey + }, pubkeyrec.jwtSeckey, undefined, undefined, life); + + await this.db.collections.pubkeys_collection.findOneAndUpdate({ id: pubkeyrec.id }, { $set: { lastUsedTime: Date.now() } }); + return token; + } + /** + * Issue an access token. + * + * @param pubkey - The public key. + * @param signature - The signature of the key. + * @param life - The life of the token. + * @returns + */ + public async issueAccessToken(pubkey: string, signature: string, life?: number) { + // refine the public-key parameter from browser + pubkey = pubkey.replace(/\\n/g, '\n'); + + /* Validate the signature with the public key */ + if (!await rsaverifier(pubkey, signature)) { + throw new CoreError( + enumCoreErrors.CLIENT_MALFORMED_INPUT, + 'Signature vs Public key mismatched.' + ); + } + + const pubkeyrec = await this.db.collections.pubkeys_collection.findOne({ pubkey, deleted: null }); + if (pubkeyrec === null || pubkeyrec === undefined) { + throw new CoreError( + enumCoreErrors.CLIENT_MALFORMED_INPUT, + 'This public-key has not been registered yet.' + ); + } + + // payload of the JWT for storing user information + const payload = { + publicKey: pubkeyrec.jwtPubkey, + associatedUserId: pubkeyrec.associatedUserId, + refreshCounter: pubkeyrec.refreshCounter, + Issuer: 'IDEA-FAST DMP' + }; + + // update the counter + const fieldsToUpdate = { + refreshCounter: (pubkeyrec.refreshCounter + 1) + }; + const updateResult = await this.db.collections.pubkeys_collection.findOneAndUpdate({ pubkey, deleted: null }, { $set: fieldsToUpdate }, { returnDocument: 'after' }); + if (!updateResult) { + throw new CoreError( + enumCoreErrors.DATABASE_ERROR, + enumCoreErrors.DATABASE_ERROR + ); + } + // return the acccess token + const accessToken = { + accessToken: tokengen(payload, pubkeyrec.jwtSeckey, undefined, undefined, life) + }; + + return accessToken; + } /** * Delete a pubkey. @@ -807,7 +946,7 @@ export class UserCore { await this.db.collections.pubkeys_collection.findOneAndUpdate({ id: keyId }, { $set: { 'life.deletedTime': Date.now(), - 'life.deletedUser': requester + 'life.deletedUser': requester.id } }); @@ -904,61 +1043,6 @@ export class UserCore { return updateResult; } - /** - * Issue an access token. - * - * @param pubkey - The public key. - * @param signature - The signature of the key. - * @param life - The life of the token. - * @returns - */ - public async issueAccessToken(pubkey: string, signature: string, life?: number) { - // refine the public-key parameter from browser - pubkey = pubkey.replace(/\\n/g, '\n'); - - /* Validate the signature with the public key */ - if (!await rsaverifier(pubkey, signature)) { - throw new CoreError( - enumCoreErrors.CLIENT_MALFORMED_INPUT, - 'Signature vs Public key mismatched.' - ); - } - - const pubkeyrec = await this.db.collections.pubkeys_collection.findOne({ pubkey, deleted: null }); - if (pubkeyrec === null || pubkeyrec === undefined) { - throw new CoreError( - enumCoreErrors.CLIENT_MALFORMED_INPUT, - 'This public-key has not been registered yet.' - ); - } - - // payload of the JWT for storing user information - const payload = { - publicKey: pubkeyrec.jwtPubkey, - associatedUserId: pubkeyrec.associatedUserId, - refreshCounter: pubkeyrec.refreshCounter, - Issuer: 'IDEA-FAST DMP' - }; - - // update the counter - const fieldsToUpdate = { - refreshCounter: (pubkeyrec.refreshCounter + 1) - }; - const updateResult = await this.db.collections.pubkeys_collection.findOneAndUpdate({ pubkey, deleted: null }, { $set: fieldsToUpdate }, { returnDocument: 'after' }); - if (!updateResult) { - throw new CoreError( - enumCoreErrors.DATABASE_ERROR, - enumCoreErrors.DATABASE_ERROR - ); - } - // return the acccess token - const accessToken = { - accessToken: tokengen(payload, pubkeyrec.jwtSeckey, undefined, undefined, life) - }; - - return accessToken; - } - /** * Ask for a request to extend account expiration time. Send notifications to user and admin. * diff --git a/packages/itmat-cores/src/utils/GraphQL.ts b/packages/itmat-cores/src/utils/GraphQL.ts index 90eefe062..3135086aa 100644 --- a/packages/itmat-cores/src/utils/GraphQL.ts +++ b/packages/itmat-cores/src/utils/GraphQL.ts @@ -146,6 +146,13 @@ export function convertV2CreateFieldInputToV3(studyId: string, fields: V2CreateF description: el.description }; }), + properties: [{ + name: 'subjectId', + required: true + }, { + name: 'visitId', + required: false + }], unit: field.unit, comments: field.comments }; diff --git a/packages/itmat-interface/src/server/router.ts b/packages/itmat-interface/src/server/router.ts index c69f2220c..eeb22deec 100644 --- a/packages/itmat-interface/src/server/router.ts +++ b/packages/itmat-interface/src/server/router.ts @@ -98,7 +98,10 @@ export class Router { // authentication middleware this.app.use((req, res, next) => { - const token: string = req.headers.authorization || ''; + let token: string = req.headers.authorization || ''; + if (token.startsWith('Bearer ')) { + token = token.slice(7); + } tokenAuthentication(token) .then((associatedUser) => { if (associatedUser) { diff --git a/packages/itmat-interface/test/trpcTests/data.test.ts b/packages/itmat-interface/test/trpcTests/data.test.ts index 993744c9b..7cbef282b 100644 --- a/packages/itmat-interface/test/trpcTests/data.test.ts +++ b/packages/itmat-interface/test/trpcTests/data.test.ts @@ -1520,8 +1520,8 @@ if (global.hasMinio) { const response2 = await authorisedUser.get('/trpc/data.getStudyData?input=' + encodeQueryParams(paramteres)) .query({}); expect(response2.status).toBe(200); - expect(response2.body.result.data.raw).toHaveLength(1); - expect(response2.body.result.data.raw[0].fieldId).toBe('1'); + expect(response2.body.result.data).toHaveLength(1); + expect(response2.body.result.data[0].fieldId).toBe('1'); }); test('Get data (aggregation with group)', async () => { await authorisedUser.post('/trpc/data.createStudyField') @@ -2117,16 +2117,6 @@ if (global.hasMinio) { .query({}); expect(response.status).toBe(200); expect(response.body.result.data.clinical).toHaveLength(2); - expect(response.body.result.data.clinical[0]).toMatchObject({ - SubjectId: '1A', - 1: 1, - 11: 2 - }); - expect(response.body.result.data.clinical[1]).toMatchObject({ - SubjectId: '2B', - 1: 11, - 11: 22 - }); }); test('Get data (aggregation with degroup)', async () => { await authorisedUser.post('/trpc/data.createStudyField') @@ -2276,22 +2266,6 @@ if (global.hasMinio) { expect(response.body.result.data.clinical).toHaveLength(2); expect(response.body.result.data.clinical[0]).toHaveLength(2); expect(response.body.result.data.clinical[1]).toHaveLength(2); - expect(response.body.result.data.clinical[0][0]).toMatchObject({ - 1: 1, - SubjectId: '1A' - }); - expect(response.body.result.data.clinical[0][1]).toMatchObject({ - 11: 2, - SubjectId: '1A' - }); - expect(response.body.result.data.clinical[1][0]).toMatchObject({ - 1: 11, - SubjectId: '2B' - }); - expect(response.body.result.data.clinical[1][1]).toMatchObject({ - 11: 22, - SubjectId: '2B' - }); }); test('Get data (aggregation with filter)', async () => { await authorisedUser.post('/trpc/data.createStudyField') @@ -2651,9 +2625,9 @@ if (global.hasMinio) { const response = await authorisedUser.get('/trpc/data.getStudyData?input=' + encodeQueryParams(paramteres)) .query({}); expect(response.status).toBe(200); - expect(response.body.result.data.raw).toHaveLength(2); - expect(response.body.result.data.raw[0].fieldId).toBe('1'); - expect(response.body.result.data.raw[1].fieldId).toBe('1'); + expect(response.body.result.data).toHaveLength(2); + expect(response.body.result.data[0].fieldId).toBe('1'); + expect(response.body.result.data[1].fieldId).toBe('1'); }); test('Get data (cache initialized)', async () => { await authorisedUser.post('/trpc/data.createStudyField') @@ -2883,11 +2857,11 @@ if (global.hasMinio) { const response = await authorisedUser.get('/trpc/data.getStudyData?input=' + encodeQueryParams(paramteres)) .query({}); expect(response.status).toBe(200); - expect(response.body.result.data.raw).toHaveLength(0); + expect(response.body.result.data).toHaveLength(0); }); }); } else { test(`${__filename.split(/[\\/]/).pop()} skipped because it requires Minio on Docker`, () => { expect(true).toBe(true); }); -} \ No newline at end of file +} diff --git a/packages/itmat-types/src/types/pubkey.ts b/packages/itmat-types/src/types/pubkey.ts index 2d44adecc..76d2dd3f3 100644 --- a/packages/itmat-types/src/types/pubkey.ts +++ b/packages/itmat-types/src/types/pubkey.ts @@ -6,6 +6,8 @@ export interface IPubkey extends IBase { jwtSeckey: string; refreshCounter: number; associatedUserId: string | null; + challenge: string | null; + lastUsedTime: number | null; } export type AccessToken = { diff --git a/packages/itmat-ui-react/src/components/datasetDetail/tabContent/files/fileTab.tsx b/packages/itmat-ui-react/src/components/datasetDetail/tabContent/files/fileTab.tsx index 4bb8000e4..3dd23fd07 100644 --- a/packages/itmat-ui-react/src/components/datasetDetail/tabContent/files/fileTab.tsx +++ b/packages/itmat-ui-react/src/components/datasetDetail/tabContent/files/fileTab.tsx @@ -379,7 +379,7 @@ function generateTableColumns(block: IStudyFileBlock, searchedKeyword: string | }]; for (const bcolumn of block.defaultFileColumns) { columns.push({ - title: {bcolumn.title}, + title: {bcolumn.title}, dataIndex: bcolumn.property, key: bcolumn.property, render: (__unused__value, record) => { diff --git a/packages/itmat-ui-react/src/components/log/logList.tsx b/packages/itmat-ui-react/src/components/log/logList.tsx index 39d8d130a..3e0db9a4f 100644 --- a/packages/itmat-ui-react/src/components/log/logList.tsx +++ b/packages/itmat-ui-react/src/components/log/logList.tsx @@ -35,7 +35,7 @@ type LogsSummary = { }; export const LogSection: FunctionComponent = () => { - const getLogs = trpc.log.getLogs.useQuery({ indexRange: [0, 1000] }); + const getLogs = trpc.log.getLogs.useQuery({ indexRange: [0, 100] }); const getUsers = trpc.user.getUsers.useQuery({}); const getSystemConfig = trpc.config.getConfig.useQuery({ configType: enumConfigType.SYSTEMCONFIG, key: null, useDefault: true }); const getLogsSummary = trpc.log.getLogsSummary.useQuery(); @@ -293,7 +293,7 @@ const generateLogColumns = (users: IUserWithoutToken[], barThreshold: number[]) title: 'Execution Time', dataIndex: 'time', key: 'time', - sorter: (a, b) => a.life.createdTime - b.life.createdTime, + sorter: (a, b) => a.life.createdTime ?? 0 - b.life.createdTime ?? 0, render: (__unused__value, record) => { return new Date(record.life.createdTime).toUTCString(); } diff --git a/packages/itmat-ui-react/src/components/profile/keys.tsx b/packages/itmat-ui-react/src/components/profile/keys.tsx index 28480ee18..fd3f229aa 100644 --- a/packages/itmat-ui-react/src/components/profile/keys.tsx +++ b/packages/itmat-ui-react/src/components/profile/keys.tsx @@ -1,8 +1,8 @@ import React, { FunctionComponent, useState } from 'react'; import LoadSpinner from '../reusable/loadSpinner'; // import { ProjectSection } from '../users/projectSection'; -import { Form, Input, Button, List, Table, message, Modal } from 'antd'; -import { CopyOutlined } from '@ant-design/icons'; +import { Form, Input, Button, List, Table, message, Modal, Upload, Popconfirm } from 'antd'; +import { CopyOutlined, UploadOutlined } from '@ant-design/icons'; import css from './profile.module.css'; import { trpc } from '../../utils/trpc'; import copy from 'copy-to-clipboard'; @@ -61,13 +61,6 @@ export const MyKeys: FunctionComponent = () => { ); } - }, { - title: 'Refresh Counter', - dataIndex: 'refreshCounter', - key: 'value', - render: (__unused__value, record) => { - return record.refreshCounter; - } }, { title: 'Created At', dataIndex: 'createdAt', @@ -80,17 +73,22 @@ export const MyKeys: FunctionComponent = () => { dataIndex: 'tokenGeneration', key: 'value', render: (_, record) => { - return ; + return ; } }, { title: '', dataIndex: 'delete', key: 'delete', render: (_, record) => { - return ; + return deletePubkey.mutate({ + associatedUserId: whoAmI.data.id, + keyId: record.id + })} + > + + ; } }]; return (
@@ -140,15 +138,15 @@ const KeyGeneration: React.FunctionComponent<{ userId: string }> = ({ userId }) // function for generating file and set download link const makeTextFile = (filecontent: string) => { const data = new Blob([filecontent], { type: 'text/plain' }); - // this part avoids memory leaks + // Avoid memory leaks if (downloadLink !== '') window.URL.revokeObjectURL(downloadLink); - // update the download link state + // Update the download link state setDownloadLink(window.URL.createObjectURL(data)); }; return (
- + = ({ userId }) completedKeypairGen ?
Public key: