${ backupCodes }
`;
- modal.open({
- title: t('Backup_codes'),
- text: `${ t('Make_sure_you_have_a_copy_of_your_codes', { codes }) }`,
- html: true,
- });
- };
-
- this.autorun(() => {
- const user = Meteor.user();
- if (user && user.services && user.services.totp && user.services.totp.enabled) {
- Meteor.call('2fa:checkCodesRemaining', (error, result) => {
- if (result) {
- this.codesRemaining.set(result.remaining);
- }
- });
- }
- });
-});
diff --git a/app/2fa/client/callWithTwoFactorRequired.js b/app/2fa/client/callWithTwoFactorRequired.js
index a7bc16514f1b..54ca429f40a1 100644
--- a/app/2fa/client/callWithTwoFactorRequired.js
+++ b/app/2fa/client/callWithTwoFactorRequired.js
@@ -36,7 +36,7 @@ export function process2faReturn({ error, result, originalCallback, onCode, emai
text: t(methods[method].text),
html: methods[method].html,
type: 'input',
- inputActionText: method === 'email' && t('Send_me_the_code_again'),
+ inputActionText: method === 'email' && emailOrUsername && t('Send_me_the_code_again'),
async inputAction(e) {
const { value } = e.currentTarget;
e.currentTarget.value = t('Sending');
diff --git a/app/2fa/client/index.js b/app/2fa/client/index.js
index cc18071f0238..1ad86d365b7b 100644
--- a/app/2fa/client/index.js
+++ b/app/2fa/client/index.js
@@ -1,4 +1,2 @@
-import './accountSecurity.html';
-import './accountSecurity';
import './callWithTwoFactorRequired';
import './TOTPPassword';
diff --git a/app/api/server/lib/messages.js b/app/api/server/lib/messages.js
index 18b0b71ca176..257da349bb6e 100644
--- a/app/api/server/lib/messages.js
+++ b/app/api/server/lib/messages.js
@@ -116,14 +116,14 @@ export async function findSnippetedMessages({ uid, roomId, pagination: { offset,
};
}
-export async function findDiscussionsFromRoom({ uid, roomId, pagination: { offset, count, sort } }) {
+export async function findDiscussionsFromRoom({ uid, roomId, text, pagination: { offset, count, sort } }) {
const room = await Rooms.findOneById(roomId);
if (!await canAccessRoomAsync(room, { _id: uid })) {
throw new Error('error-not-allowed');
}
- const cursor = Messages.findDiscussionsByRoom(roomId, {
+ const cursor = Messages.findDiscussionsByRoomAndText(roomId, text, {
sort: sort || { ts: -1 },
skip: offset,
limit: count,
diff --git a/app/api/server/lib/users.js b/app/api/server/lib/users.js
index 4d7226a608d1..f15e765ff668 100644
--- a/app/api/server/lib/users.js
+++ b/app/api/server/lib/users.js
@@ -13,6 +13,7 @@ export async function findUsersToAutocomplete({ uid, selector }) {
fields: {
name: 1,
username: 1,
+ nickname: 1,
status: 1,
avatarETag: 1,
},
diff --git a/app/api/server/v1/chat.js b/app/api/server/v1/chat.js
index 57c7b4f1c5de..1facedfd62ff 100644
--- a/app/api/server/v1/chat.js
+++ b/app/api/server/v1/chat.js
@@ -713,7 +713,7 @@ API.v1.addRoute('chat.getSnippetedMessages', { authRequired: true }, {
API.v1.addRoute('chat.getDiscussions', { authRequired: true }, {
get() {
- const { roomId } = this.queryParams;
+ const { roomId, text } = this.queryParams;
const { sort } = this.parseJsonQuery();
const { offset, count } = this.getPaginationItems();
@@ -723,6 +723,7 @@ API.v1.addRoute('chat.getDiscussions', { authRequired: true }, {
const messages = Promise.await(findDiscussionsFromRoom({
uid: this.userId,
roomId,
+ text,
pagination: {
offset,
count,
diff --git a/app/api/server/v1/misc.js b/app/api/server/v1/misc.js
index b975ab1201b2..1af2c884a860 100644
--- a/app/api/server/v1/misc.js
+++ b/app/api/server/v1/misc.js
@@ -262,6 +262,8 @@ const methodCall = () => ({
const result = Meteor.call(method, ...params);
return API.v1.success(mountResult({ id, result }));
} catch (error) {
+ Meteor._debug(`Exception while invoking method ${ method }`, error.stack);
+
return API.v1.success(mountResult({ id, error }));
}
},
diff --git a/app/api/server/v1/push.js b/app/api/server/v1/push.js
index 9e3c8d59b325..937b62efc9d7 100644
--- a/app/api/server/v1/push.js
+++ b/app/api/server/v1/push.js
@@ -1,8 +1,12 @@
import { Meteor } from 'meteor/meteor';
import { Random } from 'meteor/random';
+import { Match, check } from 'meteor/check';
import { appTokensCollection } from '../../../push/server';
import { API } from '../api';
+import PushNotification from '../../../push-notifications/server/lib/PushNotification';
+import { canAccessRoom } from '../../../authorization/server/functions/canAccessRoom';
+import { Users, Messages, Rooms } from '../../../models/server';
API.v1.addRoute('push.token', { authRequired: true }, {
post() {
@@ -63,3 +67,35 @@ API.v1.addRoute('push.token', { authRequired: true }, {
return API.v1.success();
},
});
+
+API.v1.addRoute('push.get', { authRequired: true }, {
+ get() {
+ const params = this.requestParams();
+ check(params, Match.ObjectIncluding({
+ id: String,
+ }));
+
+ const receiver = Users.findOneById(this.userId);
+ if (!receiver) {
+ throw new Error('error-user-not-found');
+ }
+
+ const message = Messages.findOneById(params.id);
+ if (!message) {
+ throw new Error('error-message-not-found');
+ }
+
+ const room = Rooms.findOneById(message.rid);
+ if (!room) {
+ throw new Error('error-room-not-found');
+ }
+
+ if (!canAccessRoom(room, receiver)) {
+ throw new Error('error-not-allowed');
+ }
+
+ const data = PushNotification.getNotificationForMessageId({ receiver, room, message });
+
+ return API.v1.success({ data });
+ },
+});
diff --git a/app/api/server/v1/settings.js b/app/api/server/v1/settings.js
index 284fea491be7..6cad33dca4be 100644
--- a/app/api/server/v1/settings.js
+++ b/app/api/server/v1/settings.js
@@ -6,6 +6,19 @@ import _ from 'underscore';
import { Settings } from '../../../models/server';
import { hasPermission } from '../../../authorization';
import { API } from '../api';
+import { SettingsEvents } from '../../../settings/server';
+
+const fetchSettings = (query, sort, offset, count, fields) => {
+ const settings = Settings.find(query, {
+ sort: sort || { _id: 1 },
+ skip: offset,
+ limit: count,
+ fields: Object.assign({ _id: 1, value: 1, enterprise: 1, invalidValue: 1, modules: 1 }, fields),
+ }).fetch();
+
+ SettingsEvents.emit('fetch-settings', settings);
+ return settings;
+};
// settings endpoints
API.v1.addRoute('settings.public', { authRequired: false }, {
@@ -20,12 +33,7 @@ API.v1.addRoute('settings.public', { authRequired: false }, {
ourQuery = Object.assign({}, query, ourQuery);
- const settings = Settings.find(ourQuery, {
- sort: sort || { _id: 1 },
- skip: offset,
- limit: count,
- fields: Object.assign({ _id: 1, value: 1 }, fields),
- }).fetch();
+ const settings = fetchSettings(ourQuery, sort, offset, count, fields);
return API.v1.success({
settings,
@@ -94,12 +102,7 @@ API.v1.addRoute('settings', { authRequired: true }, {
ourQuery = Object.assign({}, query, ourQuery);
- const settings = Settings.find(ourQuery, {
- sort: sort || { _id: 1 },
- skip: offset,
- limit: count,
- fields: Object.assign({ _id: 1, value: 1 }, fields),
- }).fetch();
+ const settings = fetchSettings(ourQuery, sort, offset, count, fields);
return API.v1.success({
settings,
diff --git a/app/api/server/v1/users.js b/app/api/server/v1/users.js
index 6d3c380bbe7e..e368b309f176 100644
--- a/app/api/server/v1/users.js
+++ b/app/api/server/v1/users.js
@@ -31,6 +31,7 @@ API.v1.addRoute('users.create', { authRequired: true }, {
username: String,
active: Match.Maybe(Boolean),
bio: Match.Maybe(String),
+ nickname: Match.Maybe(String),
statusText: Match.Maybe(String),
roles: Match.Maybe(Array),
joinDefaultChannels: Match.Maybe(Boolean),
@@ -436,6 +437,7 @@ API.v1.addRoute('users.update', { authRequired: true, twoFactorRequired: true },
password: Match.Maybe(String),
username: Match.Maybe(String),
bio: Match.Maybe(String),
+ nickname: Match.Maybe(String),
statusText: Match.Maybe(String),
active: Match.Maybe(Boolean),
roles: Match.Maybe(Array),
@@ -473,6 +475,7 @@ API.v1.addRoute('users.updateOwnBasicInfo', { authRequired: true }, {
email: Match.Maybe(String),
name: Match.Maybe(String),
username: Match.Maybe(String),
+ nickname: Match.Maybe(String),
statusText: Match.Maybe(String),
currentPassword: Match.Maybe(String),
newPassword: Match.Maybe(String),
@@ -484,6 +487,7 @@ API.v1.addRoute('users.updateOwnBasicInfo', { authRequired: true }, {
email: this.bodyParams.data.email,
realname: this.bodyParams.data.name,
username: this.bodyParams.data.username,
+ nickname: this.bodyParams.data.nickname,
statusText: this.bodyParams.data.statusText,
newPassword: this.bodyParams.data.newPassword,
typedPassword: this.bodyParams.data.currentPassword,
diff --git a/app/apple/server/index.js b/app/apple/server/index.js
new file mode 100644
index 000000000000..bfc42742322d
--- /dev/null
+++ b/app/apple/server/index.js
@@ -0,0 +1,2 @@
+import './startup.js';
+import './loginHandler.js';
diff --git a/app/apple/server/loginHandler.js b/app/apple/server/loginHandler.js
new file mode 100644
index 000000000000..95bfee852c9c
--- /dev/null
+++ b/app/apple/server/loginHandler.js
@@ -0,0 +1,33 @@
+import { Meteor } from 'meteor/meteor';
+import { Accounts } from 'meteor/accounts-base';
+
+import { handleIdentityToken } from './tokenHandler';
+import { settings } from '../../settings';
+
+Accounts.registerLoginHandler('apple', (loginRequest) => {
+ if (!loginRequest.identityToken) {
+ return;
+ }
+
+ if (!settings.get('Accounts_OAuth_Apple')) {
+ return;
+ }
+
+ const identityResult = handleIdentityToken(loginRequest);
+
+ if (!identityResult.error) {
+ const result = Accounts.updateOrCreateUserFromExternalService('apple', identityResult.serviceData, identityResult.options);
+
+ // Ensure processing succeeded
+ if (result === undefined || result.userId === undefined) {
+ return {
+ type: 'apple',
+ error: new Meteor.Error(Accounts.LoginCancelledError.numericError, 'User creation failed from Apple response token'),
+ };
+ }
+
+ return result;
+ }
+
+ return identityResult;
+});
diff --git a/app/apple/server/startup.js b/app/apple/server/startup.js
new file mode 100644
index 000000000000..c0b04ed32334
--- /dev/null
+++ b/app/apple/server/startup.js
@@ -0,0 +1,35 @@
+import _ from 'underscore';
+import { Meteor } from 'meteor/meteor';
+import { ServiceConfiguration } from 'meteor/service-configuration';
+
+import { settings } from '../../settings';
+
+settings.addGroup('OAuth', function() {
+ this.section('Apple', function() {
+ this.add('Accounts_OAuth_Apple', false, { type: 'boolean', public: true });
+ });
+});
+
+const configureService = _.debounce(Meteor.bindEnvironment(() => {
+ if (!settings.get('Accounts_OAuth_Apple')) {
+ return ServiceConfiguration.configurations.remove({
+ service: 'apple',
+ });
+ }
+
+ ServiceConfiguration.configurations.upsert({
+ service: 'apple',
+ }, {
+ $set: {
+ // We'll hide this button on Web Client
+ showButton: false,
+ enabled: settings.get('Accounts_OAuth_Apple'),
+ },
+ });
+}), 1000);
+
+Meteor.startup(() => {
+ settings.get('Accounts_OAuth_Apple', () => {
+ configureService();
+ });
+});
diff --git a/app/apple/server/tokenHandler.js b/app/apple/server/tokenHandler.js
new file mode 100644
index 000000000000..8594757a426c
--- /dev/null
+++ b/app/apple/server/tokenHandler.js
@@ -0,0 +1,77 @@
+import { jws } from 'jsrsasign';
+import NodeRSA from 'node-rsa';
+import { HTTP } from 'meteor/http';
+import { Meteor } from 'meteor/meteor';
+import { Accounts } from 'meteor/accounts-base';
+import { Match, check } from 'meteor/check';
+
+const isValidAppleJWT = (identityToken, header) => {
+ const applePublicKeys = HTTP.get('https://appleid.apple.com/auth/keys').data.keys;
+ const { kid } = header;
+
+ const key = applePublicKeys.find((k) => k.kid === kid);
+
+ const pubKey = new NodeRSA();
+ pubKey.importKey(
+ { n: Buffer.from(key.n, 'base64'), e: Buffer.from(key.e, 'base64') },
+ 'components-public',
+ );
+ const userKey = pubKey.exportKey(['public']);
+
+ try {
+ return jws.JWS.verify(identityToken, userKey, {
+ typ: 'JWT',
+ alg: 'RS256',
+ });
+ } catch {
+ return false;
+ }
+};
+
+export const handleIdentityToken = ({ identityToken, fullName, email }) => {
+ check(identityToken, String);
+ check(fullName, Match.Maybe(Object));
+ check(email, Match.Maybe(String));
+
+ const decodedToken = jws.JWS.parse(identityToken);
+
+ if (!isValidAppleJWT(identityToken, decodedToken.headerObj)) {
+ return {
+ type: 'apple',
+ error: new Meteor.Error(Accounts.LoginCancelledError.numericError, 'identityToken is a invalid JWT'),
+ };
+ }
+
+ const profile = {};
+
+ const { givenName, familyName } = fullName;
+ if (givenName && familyName) {
+ profile.name = `${ givenName } ${ familyName }`;
+ }
+
+ const { iss, iat, exp } = decodedToken.payloadObj;
+
+ if (!iss) {
+ return {
+ type: 'apple',
+ error: new Meteor.Error(Accounts.LoginCancelledError.numericError, 'Insufficient data in auth response token'),
+ };
+ }
+
+ // Collect basic auth provider details
+ const serviceData = {
+ id: iss,
+ did: iss.split(':').pop(),
+ issuedAt: new Date(iat * 1000),
+ expiresAt: new Date(exp * 1000),
+ };
+
+ if (email) {
+ serviceData.email = email;
+ }
+
+ return {
+ serviceData,
+ options: { profile },
+ };
+};
diff --git a/app/apps/client/gameCenter/invitePlayers.js b/app/apps/client/gameCenter/invitePlayers.js
index 1f0b6f07455e..15579db899c2 100644
--- a/app/apps/client/gameCenter/invitePlayers.js
+++ b/app/apps/client/gameCenter/invitePlayers.js
@@ -54,13 +54,13 @@ Template.InvitePlayers.helpers({
roomModifier() {
return (filter, text = '') => {
const f = filter.get();
- return `#${ f.length === 0 ? text : text.replace(new RegExp(filter.get()), (part) => `${ part }`) }`;
+ return `#${ f.length === 0 ? text : text.replace(new RegExp(filter.get(), 'i'), (part) => `${ part }`) }`;
};
},
userModifier() {
return (filter, text = '') => {
const f = filter.get();
- return `@${ f.length === 0 ? text : text.replace(new RegExp(filter.get()), (part) => `${ part }`) }`;
+ return `@${ f.length === 0 ? text : text.replace(new RegExp(filter.get(), 'i'), (part) => `${ part }`) }`;
};
},
nameSuggestion() {
diff --git a/app/apps/server/communication/uikit.js b/app/apps/server/communication/uikit.js
index 2bb8beeebc1f..ded3cb2b45e8 100644
--- a/app/apps/server/communication/uikit.js
+++ b/app/apps/server/communication/uikit.js
@@ -111,8 +111,10 @@ export class AppUIKitInteractionApi {
const {
type,
actionId,
- view,
- isCleared,
+ payload: {
+ view,
+ isCleared,
+ },
} = req.body;
const user = this.orch.getConverters().get('users').convertToApp(req.user);
diff --git a/app/channel-settings/client/views/channelSettings.js b/app/channel-settings/client/views/channelSettings.js
index 1ee6d8a4fff6..0e58f1e80319 100644
--- a/app/channel-settings/client/views/channelSettings.js
+++ b/app/channel-settings/client/views/channelSettings.js
@@ -62,7 +62,7 @@ function roomExcludePinned(room) {
return room.retention.excludePinned;
}
- return settings.get('RetentionPolicy_ExcludePinned');
+ return settings.get('RetentionPolicy_DoNotPrunePinned');
}
function roomHasGlobalPurge(room) {
diff --git a/app/cloud/server/functions/connectWorkspace.js b/app/cloud/server/functions/connectWorkspace.js
index 24eff7d9f803..17872bbce584 100644
--- a/app/cloud/server/functions/connectWorkspace.js
+++ b/app/cloud/server/functions/connectWorkspace.js
@@ -3,7 +3,6 @@ import { HTTP } from 'meteor/http';
import { getRedirectUri } from './getRedirectUri';
import { retrieveRegistrationStatus } from './retrieveRegistrationStatus';
-import { getWorkspaceAccessToken } from './getWorkspaceAccessToken';
import { Settings } from '../../../models';
import { settings } from '../../../settings';
import { saveRegistrationData } from './saveRegistrationData';
@@ -49,11 +48,5 @@ export function connectWorkspace(token) {
Promise.await(saveRegistrationData(data));
- // Now that we have the client id and secret, let's get the access token
- const accessToken = getWorkspaceAccessToken(true);
- if (!accessToken) {
- return false;
- }
-
return true;
}
diff --git a/app/cloud/server/methods.js b/app/cloud/server/methods.js
index cca83d62c30b..7e64e5d889df 100644
--- a/app/cloud/server/methods.js
+++ b/app/cloud/server/methods.js
@@ -11,7 +11,6 @@ import { disconnectWorkspace } from './functions/disconnectWorkspace';
import { syncWorkspace } from './functions/syncWorkspace';
import { checkUserHasCloudLogin } from './functions/checkUserHasCloudLogin';
import { userLogout } from './functions/userLogout';
-import { Settings } from '../../models';
import { hasPermission } from '../../authorization';
import { buildWorkspaceRegistrationData } from './functions/buildRegistrationData';
@@ -49,28 +48,13 @@ Meteor.methods({
return startRegisterWorkspace();
},
- 'cloud:updateEmail'(email, resend = false) {
- check(email, String);
-
- if (!Meteor.userId()) {
- throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'cloud:updateEmail' });
- }
-
- if (!hasPermission(Meteor.userId(), 'manage-cloud')) {
- throw new Meteor.Error('error-not-authorized', 'Not authorized', { method: 'cloud:updateEmail' });
- }
-
- Settings.updateValueById('Organization_Email', email);
-
- return startRegisterWorkspace(resend);
- },
'cloud:syncWorkspace'() {
if (!Meteor.userId()) {
- throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'cloud:updateEmail' });
+ throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'cloud:syncWorkspace' });
}
if (!hasPermission(Meteor.userId(), 'manage-cloud')) {
- throw new Meteor.Error('error-not-authorized', 'Not authorized', { method: 'cloud:updateEmail' });
+ throw new Meteor.Error('error-not-authorized', 'Not authorized', { method: 'cloud:syncWorkspace' });
}
return syncWorkspace();
diff --git a/app/discussion/client/tabBar.js b/app/discussion/client/tabBar.js
index 661c9a8ddebf..5b5873d2a41b 100644
--- a/app/discussion/client/tabBar.js
+++ b/app/discussion/client/tabBar.js
@@ -10,6 +10,7 @@ Meteor.startup(function() {
i18nTitle: 'Discussions',
icon: 'discussion',
template: 'discussionsTabbar',
+ full: true,
order: 1,
condition: () => settings.get('Discussion_enabled'),
});
diff --git a/app/discussion/client/views/DiscussionTabbar.html b/app/discussion/client/views/DiscussionTabbar.html
index b489dcedb79f..80f65ec9d16c 100644
--- a/app/discussion/client/views/DiscussionTabbar.html
+++ b/app/discussion/client/views/DiscussionTabbar.html
@@ -1,24 +1,3 @@
- {{#if Template.subscriptionsReady}}
- {{#unless hasMessages}}
- {{_ "Select_service_to_login"}}
-