Skip to content

Commit dbf125c

Browse files
chore: modernize code related to pushToken management (#39011)
1 parent 602b20a commit dbf125c

File tree

24 files changed

+484
-269
lines changed

24 files changed

+484
-269
lines changed

apps/meteor/app/api/server/v1/push.ts

Lines changed: 181 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,87 +1,211 @@
1-
import type { IAppsTokens } from '@rocket.chat/core-typings';
2-
import { Messages, AppsTokens, Users, Rooms, Settings } from '@rocket.chat/models';
3-
import { Random } from '@rocket.chat/random';
4-
import { ajv, validateBadRequestErrorResponse, validateUnauthorizedErrorResponse } from '@rocket.chat/rest-typings';
1+
import { Push } from '@rocket.chat/core-services';
2+
import type { IPushToken } from '@rocket.chat/core-typings';
3+
import { Messages, PushToken, Users, Rooms, Settings } from '@rocket.chat/models';
4+
import {
5+
ajv,
6+
validateNotFoundErrorResponse,
7+
validateBadRequestErrorResponse,
8+
validateUnauthorizedErrorResponse,
9+
validateForbiddenErrorResponse,
10+
} from '@rocket.chat/rest-typings';
11+
import type { JSONSchemaType } from 'ajv';
512
import { Match, check } from 'meteor/check';
613
import { Meteor } from 'meteor/meteor';
714

815
import { executePushTest } from '../../../../server/lib/pushConfig';
916
import { canAccessRoomAsync } from '../../../authorization/server/functions/canAccessRoom';
10-
import { pushUpdate } from '../../../push/server/methods';
1117
import PushNotification from '../../../push-notifications/server/lib/PushNotification';
1218
import { settings } from '../../../settings/server';
1319
import type { ExtractRoutesFromAPI } from '../ApiClass';
1420
import { API } from '../api';
21+
import type { SuccessResult } from '../definition';
1522

16-
API.v1.addRoute(
17-
'push.token',
18-
{ authRequired: true },
19-
{
20-
async post() {
21-
const { id, type, value, appName } = this.bodyParams;
23+
type PushTokenPOST = {
24+
id?: string;
25+
type: 'apn' | 'gcm';
26+
value: string;
27+
appName: string;
28+
};
2229

23-
if (id && typeof id !== 'string') {
24-
throw new Meteor.Error('error-id-param-not-valid', 'The required "id" body param is invalid.');
25-
}
30+
const PushTokenPOSTSchema: JSONSchemaType<PushTokenPOST> = {
31+
type: 'object',
32+
properties: {
33+
id: {
34+
type: 'string',
35+
nullable: true,
36+
},
37+
type: {
38+
type: 'string',
39+
enum: ['apn', 'gcm'],
40+
},
41+
value: {
42+
type: 'string',
43+
minLength: 1,
44+
},
45+
appName: {
46+
type: 'string',
47+
minLength: 1,
48+
},
49+
},
50+
required: ['type', 'value', 'appName'],
51+
additionalProperties: false,
52+
};
2653

27-
const deviceId = id || Random.id();
54+
export const isPushTokenPOSTProps = ajv.compile<PushTokenPOST>(PushTokenPOSTSchema);
2855

29-
if (!type || (type !== 'apn' && type !== 'gcm')) {
30-
throw new Meteor.Error('error-type-param-not-valid', 'The required "type" body param is missing or invalid.');
31-
}
56+
type PushTokenDELETE = {
57+
token: string;
58+
};
3259

33-
if (!value || typeof value !== 'string') {
34-
throw new Meteor.Error('error-token-param-not-valid', 'The required "value" body param is missing or invalid.');
35-
}
60+
const PushTokenDELETESchema: JSONSchemaType<PushTokenDELETE> = {
61+
type: 'object',
62+
properties: {
63+
token: {
64+
type: 'string',
65+
minLength: 1,
66+
},
67+
},
68+
required: ['token'],
69+
additionalProperties: false,
70+
};
3671

37-
if (!appName || typeof appName !== 'string') {
38-
throw new Meteor.Error('error-appName-param-not-valid', 'The required "appName" body param is missing or invalid.');
39-
}
72+
export const isPushTokenDELETEProps = ajv.compile<PushTokenDELETE>(PushTokenDELETESchema);
73+
74+
type PushTokenResult = Pick<IPushToken, '_id' | 'token' | 'appName' | 'userId' | 'enabled' | 'createdAt' | '_updatedAt'>;
75+
76+
/**
77+
* Pick only the attributes we actually want to return on the endpoint, ensuring nothing from older schemas get mixed in
78+
*/
79+
function cleanTokenResult(result: Omit<IPushToken, 'authToken'>): PushTokenResult {
80+
const { _id, token, appName, userId, enabled, createdAt, _updatedAt } = result;
81+
82+
return {
83+
_id,
84+
token,
85+
appName,
86+
userId,
87+
enabled,
88+
createdAt,
89+
_updatedAt,
90+
};
91+
}
4092

41-
const authToken = this.request.headers.get('x-auth-token');
42-
if (!authToken) {
93+
const pushTokenEndpoints = API.v1
94+
.post(
95+
'push.token',
96+
{
97+
response: {
98+
200: ajv.compile<SuccessResult<{ result: PushTokenResult }>['body']>({
99+
additionalProperties: false,
100+
type: 'object',
101+
properties: {
102+
success: {
103+
type: 'boolean',
104+
description: 'Indicates if the request was successful.',
105+
},
106+
result: {
107+
type: 'object',
108+
description: 'The updated token data for this device',
109+
properties: {
110+
_id: {
111+
type: 'string',
112+
},
113+
token: {
114+
type: 'object',
115+
properties: {
116+
apn: {
117+
type: 'string',
118+
},
119+
gcm: {
120+
type: 'string',
121+
},
122+
},
123+
required: [],
124+
additionalProperties: false,
125+
},
126+
appName: {
127+
type: 'string',
128+
},
129+
userId: {
130+
type: 'string',
131+
nullable: true,
132+
},
133+
enabled: {
134+
type: 'boolean',
135+
},
136+
createdAt: {
137+
type: 'string',
138+
},
139+
_updatedAt: {
140+
type: 'string',
141+
},
142+
},
143+
additionalProperties: false,
144+
},
145+
},
146+
required: ['success', 'result'],
147+
}),
148+
400: validateBadRequestErrorResponse,
149+
401: validateUnauthorizedErrorResponse,
150+
403: validateForbiddenErrorResponse,
151+
},
152+
body: isPushTokenPOSTProps,
153+
authRequired: true,
154+
},
155+
async function action() {
156+
const { id, type, value, appName } = this.bodyParams;
157+
158+
const rawToken = this.request.headers.get('x-auth-token');
159+
if (!rawToken) {
43160
throw new Meteor.Error('error-authToken-param-not-valid', 'The required "authToken" header param is missing or invalid.');
44161
}
162+
const authToken = Accounts._hashLoginToken(rawToken);
45163

46-
const result = await pushUpdate({
47-
id: deviceId,
48-
token: { [type]: value } as IAppsTokens['token'],
164+
const result = await Push.registerPushToken({
165+
...(id && { _id: id }),
166+
token: { [type]: value } as IPushToken['token'],
49167
authToken,
50168
appName,
51169
userId: this.userId,
52170
});
53171

54-
return API.v1.success({ result });
172+
return API.v1.success({ result: cleanTokenResult(result) });
55173
},
56-
async delete() {
174+
)
175+
.delete(
176+
'push.token',
177+
{
178+
response: {
179+
200: ajv.compile<void>({
180+
additionalProperties: false,
181+
type: 'object',
182+
properties: {
183+
success: {
184+
type: 'boolean',
185+
},
186+
},
187+
required: ['success'],
188+
}),
189+
400: validateBadRequestErrorResponse,
190+
401: validateUnauthorizedErrorResponse,
191+
403: validateForbiddenErrorResponse,
192+
404: validateNotFoundErrorResponse,
193+
},
194+
body: isPushTokenDELETEProps,
195+
authRequired: true,
196+
},
197+
async function action() {
57198
const { token } = this.bodyParams;
58199

59-
if (!token || typeof token !== 'string') {
60-
throw new Meteor.Error('error-token-param-not-valid', 'The required "token" body param is missing or invalid.');
61-
}
62-
63-
const affectedRecords = (
64-
await AppsTokens.deleteMany({
65-
$or: [
66-
{
67-
'token.apn': token,
68-
},
69-
{
70-
'token.gcm': token,
71-
},
72-
],
73-
userId: this.userId,
74-
})
75-
).deletedCount;
200+
const removeResult = await PushToken.removeAllByTokenStringAndUserId(token, this.userId);
76201

77-
if (affectedRecords === 0) {
202+
if (removeResult.deletedCount === 0) {
78203
return API.v1.notFound();
79204
}
80205

81206
return API.v1.success();
82207
},
83-
},
84-
);
208+
);
85209

86210
API.v1.addRoute(
87211
'push.get',
@@ -137,7 +261,7 @@ API.v1.addRoute(
137261
},
138262
);
139263

140-
const pushEndpoints = API.v1.post(
264+
const pushTestEndpoints = API.v1.post(
141265
'push.test',
142266
{
143267
authRequired: true,
@@ -177,7 +301,11 @@ const pushEndpoints = API.v1.post(
177301
},
178302
);
179303

180-
export type PushEndpoints = ExtractRoutesFromAPI<typeof pushEndpoints>;
304+
type PushTestEndpoints = ExtractRoutesFromAPI<typeof pushTestEndpoints>;
305+
306+
type PushTokenEndpoints = ExtractRoutesFromAPI<typeof pushTokenEndpoints>;
307+
308+
type PushEndpoints = PushTestEndpoints & PushTokenEndpoints;
181309

182310
declare module '@rocket.chat/rest-typings' {
183311
// eslint-disable-next-line @typescript-eslint/naming-convention, @typescript-eslint/no-empty-interface

apps/meteor/app/push/server/apn.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import apn from '@parse/node-apn';
2-
import type { IAppsTokens, RequiredField } from '@rocket.chat/core-typings';
2+
import type { IPushToken, RequiredField } from '@rocket.chat/core-typings';
33
import EJSON from 'ejson';
44

55
import type { PushOptions, PendingPushNotification } from './definition';
@@ -24,7 +24,7 @@ export const sendAPN = ({
2424
}: {
2525
userToken: string;
2626
notification: PendingPushNotification & { topic: string };
27-
_removeToken: (token: IAppsTokens['token']) => void;
27+
_removeToken: (token: IPushToken['token']) => void;
2828
}) => {
2929
if (!apnConnection) {
3030
throw new Error('Apn Connection not initialized.');

0 commit comments

Comments
 (0)