diff --git a/apps/meteor/app/push/server/methods.js b/apps/meteor/app/push/server/methods.js index 111d0da3fc614..fe549db953d0a 100644 --- a/apps/meteor/app/push/server/methods.js +++ b/apps/meteor/app/push/server/methods.js @@ -1,3 +1,4 @@ +import { Accounts } from 'meteor/accounts-base'; import { Meteor } from 'meteor/meteor'; import { Match, check } from 'meteor/check'; import { Random } from 'meteor/random'; @@ -23,6 +24,9 @@ Meteor.methods({ throw new Meteor.Error(403, 'Forbidden access'); } + // we always store the hashed token to protect users + const hashedToken = Accounts._hashLoginToken(options.authToken); + let doc; // lookup app by id if one was included @@ -49,7 +53,7 @@ Meteor.methods({ // Rig default doc doc = { token: options.token, - authToken: options.authToken, + authToken: hashedToken, appName: options.appName, userId: options.userId, enabled: true, @@ -73,7 +77,7 @@ Meteor.methods({ $set: { updatedAt: new Date(), token: options.token, - authToken: options.authToken, + authToken: hashedToken, }, }, ); diff --git a/apps/meteor/app/push/server/push.js b/apps/meteor/app/push/server/push.js index 4e73a0717511c..29b30991155fd 100644 --- a/apps/meteor/app/push/server/push.js +++ b/apps/meteor/app/push/server/push.js @@ -8,13 +8,10 @@ import { initAPN, sendAPN } from './apn'; import { sendGCM } from './gcm'; import { logger } from './logger'; import { settings } from '../../settings/server'; -import { Users } from '../../models/server'; export const _matchToken = Match.OneOf({ apn: String }, { gcm: String }); export const appTokensCollection = new Mongo.Collection('_raix_push_app_tokens'); -appTokensCollection._ensureIndex({ userId: 1 }); - export class PushClass { options = {}; @@ -78,52 +75,9 @@ export class PushClass { return !!this.options.gateways && settings.get('Register_Server') && settings.get('Cloud_Service_Agree_PrivacyTerms'); } - _validateAuthTokenByPushToken(pushToken) { - const pushTokenQuery = appTokensCollection.findOne({ token: pushToken }); - - if (!pushTokenQuery) { - return false; - } - - const { authToken, userId, expiration, usesLeft, _id } = pushTokenQuery; - if (!authToken) { - if (expiration && expiration > Date.now()) { - return true; - } - if (usesLeft > 0) { - appTokensCollection.rawCollection().updateOne( - { - _id, - }, - { - $inc: { - usesLeft: -1, - }, - }, - ); - - return true; - } - } - - const user = authToken && userId && Users.findOneByIdAndLoginToken(userId, authToken, { projection: { _id: 1 } }); - - if (!user) { - this._removeToken(pushToken); - return false; - } - - return true; - } - sendNotificationNative(app, notification, countApn, countGcm) { logger.debug('send to token', app.token); - const validToken = (app.token.apn || app.token.gcm) && this._validateAuthTokenByPushToken(app.token); - if (!validToken) { - throw new Error('send got a faulty query'); - } - if (app.token.apn) { countApn.push(app._id); // Send to APN @@ -131,7 +85,7 @@ export class PushClass { notification.topic = app.appName; sendAPN({ userToken: app.token.apn, notification, _removeToken: this._removeToken }); } - } else { + } else if (app.token.gcm) { countGcm.push(app._id); // Send to GCM @@ -146,6 +100,8 @@ export class PushClass { options: this.options, }); } + } else { + throw new Error('send got a faulty query'); } } @@ -211,11 +167,6 @@ export class PushClass { for (const gateway of this.options.gateways) { logger.debug('send to token', app.token); - const validToken = (app.token.apn || app.token.gcm) && this._validateAuthTokenByPushToken(app.token); - if (!validToken) { - continue; - } - if (app.token.apn) { countApn.push(app._id); notification.topic = app.appName; diff --git a/apps/meteor/server/models/PushToken.ts b/apps/meteor/server/models/PushToken.ts new file mode 100644 index 0000000000000..16df39673dd76 --- /dev/null +++ b/apps/meteor/server/models/PushToken.ts @@ -0,0 +1,6 @@ +import { registerModel } from '@rocket.chat/models'; + +import { db } from '../database/utils'; +import { PushTokenRaw } from './raw/PushToken'; + +registerModel('IPushTokenModel', new PushTokenRaw(db)); diff --git a/apps/meteor/server/models/raw/PushToken.ts b/apps/meteor/server/models/raw/PushToken.ts new file mode 100644 index 0000000000000..90b5d96df3e6a --- /dev/null +++ b/apps/meteor/server/models/raw/PushToken.ts @@ -0,0 +1,22 @@ +import type { IPushToken } from '@rocket.chat/core-typings'; +import type { IPushTokenModel } from '@rocket.chat/model-typings'; +import type { Db, DeleteWriteOpResultObject, IndexSpecification } from 'mongodb'; + +import { BaseRaw } from './BaseRaw'; + +export class PushTokenRaw extends BaseRaw implements IPushTokenModel { + constructor(db: Db) { + super(db, '_raix_push_app_tokens'); + } + + modelIndexes(): IndexSpecification[] { + return [{ key: { userId: 1, authToken: 1 } }, { key: { appName: 1, token: 1 } }]; + } + + removeByUserIdExceptTokens(userId: string, tokens: string[]): Promise { + return this.deleteMany({ + userId, + authToken: { $nin: tokens }, + }); + } +} diff --git a/apps/meteor/server/models/startup.ts b/apps/meteor/server/models/startup.ts index 9df2c47a0f0a3..987deb8b2ea03 100644 --- a/apps/meteor/server/models/startup.ts +++ b/apps/meteor/server/models/startup.ts @@ -34,6 +34,7 @@ import './OAuthApps'; import './OEmbedCache'; import './OmnichannelQueue'; import './PbxEvents'; +import './PushToken'; import './Permissions'; import './ReadReceipts'; import './Reports'; diff --git a/apps/meteor/server/sdk/types/IPushService.ts b/apps/meteor/server/sdk/types/IPushService.ts new file mode 100644 index 0000000000000..d19a04926bdeb --- /dev/null +++ b/apps/meteor/server/sdk/types/IPushService.ts @@ -0,0 +1,3 @@ +import { IServiceClass } from './ServiceClass'; + +export type IPushService = IServiceClass; diff --git a/apps/meteor/server/services/push/service.ts b/apps/meteor/server/services/push/service.ts new file mode 100644 index 0000000000000..399becf928b1a --- /dev/null +++ b/apps/meteor/server/services/push/service.ts @@ -0,0 +1,23 @@ +import { PushToken } from '@rocket.chat/models'; + +import { IPushService } from '../../sdk/types/IPushService'; +import { ServiceClassInternal } from '../../sdk/types/ServiceClass'; + +export class PushService extends ServiceClassInternal implements IPushService { + protected name = 'push'; + + constructor() { + super(); + + this.onEvent('watch.users', async ({ id, diff }) => { + if (diff && 'services.resume.loginTokens' in diff) { + const tokens = diff['services.resume.loginTokens'].map(({ hashedToken }: { hashedToken: string }) => hashedToken); + this.cleanUpUserTokens(id, tokens); + } + }); + } + + private async cleanUpUserTokens(userId: string, tokens: string[]): Promise { + await PushToken.removeByUserIdExceptTokens(userId, tokens); + } +} diff --git a/apps/meteor/server/services/startup.ts b/apps/meteor/server/services/startup.ts index 12f1bb8c5f511..4f9b27231dd55 100644 --- a/apps/meteor/server/services/startup.ts +++ b/apps/meteor/server/services/startup.ts @@ -15,6 +15,7 @@ import { UiKitCoreApp } from './uikit-core-app/service'; import { OmnichannelVoipService } from './omnichannel-voip/service'; import { VoipService } from './voip/service'; import { isRunningMs } from '../lib/isRunningMs'; +import { PushService } from './push/service'; const { db } = MongoInternals.defaultRemoteCollectionDriver().mongo; @@ -31,6 +32,7 @@ api.registerService(new VoipService(db)); api.registerService(new OmnichannelVoipService()); api.registerService(new TeamService()); api.registerService(new UiKitCoreApp()); +api.registerService(new PushService()); // if the process is running in micro services mode we don't need to register services that will run separately if (!isRunningMs()) { diff --git a/apps/meteor/server/startup/migrations/v273.ts b/apps/meteor/server/startup/migrations/v273.ts index 7e23a7112e314..d576c3900836c 100644 --- a/apps/meteor/server/startup/migrations/v273.ts +++ b/apps/meteor/server/startup/migrations/v273.ts @@ -4,14 +4,6 @@ import { addMigration } from '../../lib/migrations'; addMigration({ version: 273, async up() { - return appTokensCollection.rawCollection().updateMany( - {}, - { - $set: { - expiration: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000), - usesLeft: 7, - }, - }, - ); + return appTokensCollection.rawCollection().dropIndex('userId_1'); }, }); diff --git a/packages/model-typings/src/index.ts b/packages/model-typings/src/index.ts index e72bf92b0b4c8..e994857d7acb9 100644 --- a/packages/model-typings/src/index.ts +++ b/packages/model-typings/src/index.ts @@ -40,6 +40,7 @@ export * from './models/IOAuthAppsModel'; export * from './models/IOEmbedCacheModel'; export * from './models/IOmnichannelQueueModel'; export * from './models/IPbxEventsModel'; +export * from './models/IPushTokenModel'; export * from './models/IPermissionsModel'; export * from './models/IReadReceiptsModel'; export * from './models/IReportsModel'; diff --git a/packages/model-typings/src/models/IPushTokenModel.ts b/packages/model-typings/src/models/IPushTokenModel.ts new file mode 100644 index 0000000000000..5b9ceab5a0076 --- /dev/null +++ b/packages/model-typings/src/models/IPushTokenModel.ts @@ -0,0 +1,8 @@ +import type { IPushToken } from '@rocket.chat/core-typings'; +import type { DeleteWriteOpResultObject } from 'mongodb'; + +import type { IBaseModel } from './IBaseModel'; + +export interface IPushTokenModel extends IBaseModel { + removeByUserIdExceptTokens(userId: string, tokens: string[]): Promise; +} diff --git a/packages/models/src/index.ts b/packages/models/src/index.ts index e3b4395a68b29..2b50e6853855a 100644 --- a/packages/models/src/index.ts +++ b/packages/models/src/index.ts @@ -40,6 +40,7 @@ import type { IOEmbedCacheModel, IOmnichannelQueueModel, IPbxEventsModel, + IPushTokenModel, IPermissionsModel, IReadReceiptsModel, IReportsModel, @@ -111,6 +112,7 @@ export const OAuthApps = proxify('IOAuthAppsModel'); export const OEmbedCache = proxify('IOEmbedCacheModel'); export const OmnichannelQueue = proxify('IOmnichannelQueueModel'); export const PbxEvents = proxify('IPbxEventsModel'); +export const PushToken = proxify('IPushTokenModel'); export const Permissions = proxify('IPermissionsModel'); export const ReadReceipts = proxify('IReadReceiptsModel'); export const Reports = proxify('IReportsModel');