Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 19 additions & 5 deletions apps/meteor/app/api/server/v1/push.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Push } from '@rocket.chat/core-services';
import type { IPushToken } from '@rocket.chat/core-typings';
import type { IPushToken, IPushTokenTypes } from '@rocket.chat/core-typings';
import { Messages, PushToken, Users, Rooms, Settings } from '@rocket.chat/models';
import {
ajv,
Expand All @@ -23,9 +23,10 @@ import type { SuccessResult } from '../definition';

type PushTokenPOST = {
id?: string;
type: 'apn' | 'gcm';
type: IPushTokenTypes;
value: string;
appName: string;
voipToken?: string;
};

const PushTokenPOSTSchema: JSONSchemaType<PushTokenPOST> = {
Expand All @@ -47,6 +48,10 @@ const PushTokenPOSTSchema: JSONSchemaType<PushTokenPOST> = {
type: 'string',
minLength: 1,
},
voipToken: {
type: 'string',
nullable: true,
},
},
required: ['type', 'value', 'appName'],
additionalProperties: false,
Expand All @@ -72,13 +77,13 @@ const PushTokenDELETESchema: JSONSchemaType<PushTokenDELETE> = {

export const isPushTokenDELETEProps = ajv.compile<PushTokenDELETE>(PushTokenDELETESchema);

type PushTokenResult = Pick<IPushToken, '_id' | 'token' | 'appName' | 'userId' | 'enabled' | 'createdAt' | '_updatedAt'>;
type PushTokenResult = Pick<IPushToken, '_id' | 'token' | 'appName' | 'userId' | 'enabled' | 'createdAt' | '_updatedAt' | 'voipToken'>;

/**
* Pick only the attributes we actually want to return on the endpoint, ensuring nothing from older schemas get mixed in
*/
function cleanTokenResult(result: Omit<IPushToken, 'authToken'>): PushTokenResult {
const { _id, token, appName, userId, enabled, createdAt, _updatedAt } = result;
const { _id, token, appName, userId, enabled, createdAt, _updatedAt, voipToken } = result;

return {
_id,
Expand All @@ -88,6 +93,7 @@ function cleanTokenResult(result: Omit<IPushToken, 'authToken'>): PushTokenResul
enabled,
createdAt,
_updatedAt,
voipToken,
};
}

Expand Down Expand Up @@ -140,6 +146,9 @@ const pushTokenEndpoints = API.v1
_updatedAt: {
type: 'string',
},
voipToken: {
type: 'string',
},
},
additionalProperties: false,
},
Expand All @@ -154,7 +163,11 @@ const pushTokenEndpoints = API.v1
authRequired: true,
},
async function action() {
const { id, type, value, appName } = this.bodyParams;
const { id, type, value, appName, voipToken } = this.bodyParams;

if (voipToken && !id) {
return API.v1.failure('voip-tokens-must-specify-device-id');
}

const rawToken = this.request.headers.get('x-auth-token');
if (!rawToken) {
Expand All @@ -168,6 +181,7 @@ const pushTokenEndpoints = API.v1
authToken,
appName,
userId: this.userId,
...(voipToken && { voipToken }),
});

return API.v1.success({ result: cleanTokenResult(result) });
Expand Down
7 changes: 6 additions & 1 deletion apps/meteor/server/services/push/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,12 @@ export class PushService extends ServiceClassInternal implements IPushService {
): Promise<Omit<IPushToken, 'authToken'>> {
const tokenId = await registerPushToken(data);

const removeResult = await PushToken.removeByTokenAndAppNameExceptId(data.token, data.appName, tokenId);
const removeResult = await PushToken.removeDuplicateTokens({
_id: tokenId,
token: data.token,
appName: data.appName,
authToken: data.authToken,
});
if (removeResult.deletedCount) {
logger.debug({ msg: 'Removed existing app items', removed: removeResult.deletedCount });
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@ export async function findDocumentToUpdate(data: Partial<IPushToken>): Promise<I
}
}

// VoIP tokens MUST match the id
if (data.voipToken) {
return null;
}

if (data.token && data.appName) {
return PushToken.findOneByTokenAndAppName(data.token, data.appName);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,35 +1,52 @@
import type { IPushToken, Optional } from '@rocket.chat/core-typings';
import { PushToken } from '@rocket.chat/models';

import { logger } from '../logger';
import { findDocumentToUpdate } from './findDocumentToUpdate';
import { logger } from '../logger';

export async function registerPushToken(
data: Optional<Pick<IPushToken, '_id' | 'token' | 'authToken' | 'appName' | 'userId' | 'metadata'>, '_id' | 'metadata'>,
): Promise<IPushToken['_id']> {
const doc = await findDocumentToUpdate(data);

if (!doc) {
const insertResult = await PushToken.insertToken({
...(data._id && { _id: data._id }),
token: data.token,
authToken: data.authToken,
appName: data.appName,
userId: data.userId,
...(data.metadata && { metadata: data.metadata }),
});
export type PushTokenData = Optional<
Pick<IPushToken, '_id' | 'token' | 'authToken' | 'appName' | 'userId' | 'metadata' | 'voipToken'>,
'_id' | 'metadata'
>;

const { authToken: _, ...dataWithNoAuthToken } = data;
logger.debug({ msg: 'Push token added', dataWithNoAuthToken, insertResult });
function canModifyTokenDocument(doc: IPushToken, data: Partial<IPushToken>): boolean {
// If there's no voip on either side of the operation, any doc can be updated
if (!doc.voipToken && !data.voipToken) {
return true;
}

return insertResult.insertedId;
// VoIP tokens MUST be referenced by id, so if there's no id on the data, do not let this doc be changed
if (!data._id || data._id !== doc._id) {
return false;
}

return true;
}

async function insertToken(data: PushTokenData): Promise<IPushToken['_id']> {
const insertResult = await PushToken.insertToken({
...(data._id && { _id: data._id }),
token: data.token,
authToken: data.authToken,
appName: data.appName,
userId: data.userId,
...(data.metadata && { metadata: data.metadata }),
...(data.voipToken && data._id && { voipToken: data.voipToken }),
});

const { authToken: _, ...dataWithNoAuthToken } = data;
logger.debug({ msg: 'Push token added', dataWithNoAuthToken, insertResult });

return insertResult.insertedId;
}

async function updateToken(doc: IPushToken, data: PushTokenData): Promise<IPushToken['_id']> {
const updateResult = await PushToken.refreshTokenById(doc._id, {
token: data.token,
authToken: data.authToken,
appName: data.appName,
userId: data.userId,
...(data.voipToken && { voipToken: data.voipToken }),
});

if (updateResult.modifiedCount) {
Expand All @@ -39,3 +56,13 @@ export async function registerPushToken(

return doc._id;
}

export async function registerPushToken(data: PushTokenData): Promise<IPushToken['_id']> {
const doc = await findDocumentToUpdate(data);

if (!doc || !canModifyTokenDocument(doc, data)) {
return insertToken(data);
}

return updateToken(doc, data);
}
1 change: 1 addition & 0 deletions packages/core-typings/src/IPushToken.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,5 @@ export interface IPushToken extends IRocketChatRecord {
authToken: ILoginToken['hashedToken'];
metadata?: Record<string, unknown>;
createdAt: Date;
voipToken?: string;
}
4 changes: 2 additions & 2 deletions packages/model-typings/src/models/IPushTokenModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,11 @@ export interface IPushTokenModel extends IBaseModel<IPushToken> {
insertToken(data: AtLeast<IPushToken, 'token' | 'authToken' | 'appName' | 'userId'>): Promise<InsertOneResult<IPushToken>>;
refreshTokenById(
id: IPushToken['_id'],
data: Pick<IPushToken, 'token' | 'appName' | 'authToken' | 'userId'>,
data: Pick<IPushToken, 'token' | 'appName' | 'authToken' | 'userId' | 'voipToken'>,
): Promise<UpdateResult<IPushToken>>;

removeByUserIdExceptTokens(userId: string, tokens: IPushToken['authToken'][]): Promise<DeleteResult>;
removeByTokenAndAppNameExceptId(token: IPushToken['token'], appName: IPushToken['appName'], id: IPushToken['_id']): Promise<DeleteResult>;
removeDuplicateTokens(tokenData: Pick<IPushToken, '_id' | 'token' | 'appName' | 'authToken'>): Promise<DeleteResult>;

removeAllByUserId(userId: string): Promise<DeleteResult>;
removeAllByTokenStringAndUserId(token: string, userId: string): Promise<DeleteResult>;
Expand Down
23 changes: 14 additions & 9 deletions packages/models/src/models/PushToken.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ export class PushTokenRaw extends BaseRaw<IPushToken> implements IPushTokenModel

async refreshTokenById(
id: IPushToken['_id'],
data: Pick<IPushToken, 'token' | 'appName' | 'authToken' | 'userId'>,
data: Pick<IPushToken, 'token' | 'appName' | 'authToken' | 'userId' | 'voipToken'>,
): Promise<UpdateResult<IPushToken>> {
return this.updateOne(
{ _id: id },
Expand All @@ -66,7 +66,9 @@ export class PushTokenRaw extends BaseRaw<IPushToken> implements IPushTokenModel
authToken: data.authToken,
appName: data.appName,
userId: data.userId,
...(data.voipToken && { voipToken: data.voipToken }),
},
...(!data.voipToken && { $unset: { voipToken: 1 } }),
},
);
}
Expand All @@ -85,15 +87,18 @@ export class PushTokenRaw extends BaseRaw<IPushToken> implements IPushTokenModel
});
}

removeByTokenAndAppNameExceptId(
token: IPushToken['token'],
appName: IPushToken['appName'],
id: IPushToken['_id'],
): Promise<DeleteResult> {
removeDuplicateTokens(tokenData: Pick<IPushToken, '_id' | 'token' | 'appName' | 'authToken'>): Promise<DeleteResult> {
return this.deleteMany({
token,
appName,
_id: { $ne: id },
_id: { $ne: tokenData._id },
$or: [
{
token: tokenData.token,
appName: tokenData.appName,
},
{
authToken: tokenData.authToken,
},
],
});
}

Expand Down
Loading