Skip to content

Commit

Permalink
feat: Passwordless Signup backend (#1)
Browse files Browse the repository at this point in the history
  • Loading branch information
pierre-lehnen-rc committed Oct 21, 2023
1 parent bba3c9d commit ff066c4
Show file tree
Hide file tree
Showing 16 changed files with 254 additions and 1 deletion.
70 changes: 69 additions & 1 deletion apps/meteor/app/api/server/v1/users.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { PasswordlessClient, RegisterOptions, type PasswordlessOptions } from '@passwordlessdev/passwordless-nodejs';
import { Team, api } from '@rocket.chat/core-services';
import type { IExportOperation, ILoginToken, IPersonalAccessToken, IUser, UserStatus } from '@rocket.chat/core-typings';
import { Users, Subscriptions } from '@rocket.chat/models';
import { PendingUsers, Users, Subscriptions } from '@rocket.chat/models';
import {
isUserCreateParamsPOST,
isUserSetActiveStatusParamsPOST,
Expand All @@ -16,6 +17,7 @@ import {
isUsersSetPreferencesParamsPOST,
isUsersCheckUsernameAvailabilityParamsGET,
isUsersSendConfirmationEmailParamsPOST,
isUserRegisterPasswordlessDevParamsPOST,
} from '@rocket.chat/rest-typings';
import { Accounts } from 'meteor/accounts-base';
import { Match, check } from 'meteor/check';
Expand Down Expand Up @@ -609,6 +611,72 @@ API.v1.addRoute(
},
);

API.v1.addRoute(
'users.registerPasswordless',
{
authRequired: false,
rateLimiterOptions: {
numRequestsAllowed: settings.get('Rate_Limiter_Limit_RegisterUser') ?? 1,
intervalTimeInMS: settings.get('API_Enable_Rate_Limiter_Limit_Time_Default'),
},
validateParams: isUserRegisterPasswordlessDevParamsPOST,
},
{
async post() {
if (this.userId) {
return API.v1.failure('Logged in users can not register again.');
}

const { username, name, email } = this.bodyParams;

if (!(await checkUsernameAvailability(username))) {
return API.v1.failure('Username is already in use');
}

const id = (
await PendingUsers.insertOne({
username,
email,
name,
})
).insertedId;

const options: PasswordlessOptions = {
baseUrl: settings.get<string>('Passwordless_Dev_Url'),
};
const passwordlessClient = new PasswordlessClient(settings.get<string>('Passwordless_Dev_ApiSecret'), options);

const registerOptions = new RegisterOptions();
registerOptions.userId = id;
registerOptions.username = username;
registerOptions.discoverable = true;

const { token } = await passwordlessClient.createRegisterToken(registerOptions);
if (!token) {
return API.v1.failure('Failed to create register token');
}

await PendingUsers.update(
{
_id: id,
},
{
$set: {
token,
},
},
);

const user = await PendingUsers.findOneById(id);
if (!user) {
return API.v1.failure('Failed to create user');
}

return API.v1.success({ user });
},
},
);

API.v1.addRoute(
'users.resetAvatar',
{ authRequired: true },
Expand Down
1 change: 1 addition & 0 deletions apps/meteor/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,7 @@
"@nivo/heatmap": "0.80.0",
"@nivo/line": "0.80.0",
"@nivo/pie": "0.80.0",
"@passwordlessdev/passwordless-nodejs": "^0.2.0",
"@react-aria/color": "^3.0.0-beta.15",
"@react-pdf/renderer": "^3.1.12",
"@rocket.chat/account-utils": "workspace:^",
Expand Down
31 changes: 31 additions & 0 deletions apps/meteor/server/configuration/passwordless-dev.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { PasswordlessClient, type PasswordlessOptions } from '@passwordlessdev/passwordless-nodejs';
import { Accounts } from 'meteor/accounts-base';

import { settings } from '../../app/settings/server';

Accounts.registerLoginHandler('passwordless-dev', async (options: Record<string, any>) => {
if (!options.passwordless || !settings.get<boolean>('Passwordless_dev')) {
return;
}

const { token } = options;

const apiUrl = settings.get<string>('Passwordless_Dev_Url');

if (!token || !apiUrl) {
return;
}

const clientOptions: PasswordlessOptions = {
baseUrl: settings.get<string>('Passwordless_Dev_Url'),
};
const passwordlessClient = new PasswordlessClient(settings.get<string>('Passwordless_Dev_ApiSecret'), clientOptions);

const verifiedUser = await passwordlessClient.verifyToken(token);

if (verifiedUser) {
console.log('Successfully verified sign-in for user.', verifiedUser);
} else {
console.warn('Sign in failed', verifiedUser);
}
});
6 changes: 6 additions & 0 deletions apps/meteor/server/models/PendingUsers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { registerModel } from '@rocket.chat/models';

import { db } from '../database/utils';
import { PendingUsersRaw } from './raw/PendingUsers';

registerModel('IPendingUsersModel', new PendingUsersRaw(db));
22 changes: 22 additions & 0 deletions apps/meteor/server/models/raw/PendingUsers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import type { IPendingUser, RocketChatRecordDeleted } from '@rocket.chat/core-typings';
import type { IPendingUsersModel } from '@rocket.chat/model-typings';
import type { Db, Collection, IndexDescription } from 'mongodb';

import { BaseRaw } from './BaseRaw';

export class PendingUsersRaw extends BaseRaw<IPendingUser> implements IPendingUsersModel {
constructor(db: Db, trash?: Collection<RocketChatRecordDeleted<IPendingUser>>) {
super(db, 'pending_users', trash);
}

protected modelIndexes(): IndexDescription[] {
return [
{ key: { credentialToken: 1 }, unique: false },
{ key: { email: 1 }, unique: false },
];
}

public async findOneByToken(token: string): Promise<IPendingUser | null> {
return this.findOne({ credentialToken: token });
}
}
1 change: 1 addition & 0 deletions apps/meteor/server/models/startup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import './OAuthAccessTokens';
import './OAuthRefreshTokens';
import './OEmbedCache';
import './PbxEvents';
import './PendingUsers';
import './PushToken';
import './Permissions';
import './MessageReads';
Expand Down
2 changes: 2 additions & 0 deletions apps/meteor/server/settings/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { createMobileSettings } from './mobile';
import { createOauthSettings } from './oauth';
import { createOmniSettings } from './omnichannel';
import { createOTRSettings } from './otr';
import { createPasswordlessSettings } from './passwordless-dev';
import { createPushSettings } from './push';
import { createRateLimitSettings } from './rate';
import { createRetentionSettings } from './retention-policy';
Expand Down Expand Up @@ -77,6 +78,7 @@ async function createSettings() {
createUserDataSettings(),
createWebDavSettings(),
createWebRTCSettings(),
createPasswordlessSettings(),
]);
}

Expand Down
28 changes: 28 additions & 0 deletions apps/meteor/server/settings/passwordless-dev.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { settingsRegistry } from '../../app/settings/server';

export const createPasswordlessSettings = () =>
settingsRegistry.addGroup('Passwordless_Dev', async function () {
await this.add('Passwordless_Dev_Enable', false, {
type: 'boolean',
});

const enableQuery = {
_id: 'Passwordless_Dev_Enable',
value: true,
};

await this.add('Passwordless_Dev_Url', 'https://v4.passwordless.dev', {
type: 'string',
enableQuery,
});

await this.add('Passwordless_Dev_ApiKey', '', {
type: 'string',
enableQuery,
});

await this.add('Passwordless_Dev_ApiSecret', '', {
type: 'string',
enableQuery,
});
});
8 changes: 8 additions & 0 deletions packages/core-typings/src/IPendingUser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import type { IRocketChatRecord } from './IRocketChatRecord';

export interface IPendingUser extends IRocketChatRecord {
username: string;
name: string;
email: string;
token: string;
}
1 change: 1 addition & 0 deletions packages/core-typings/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export * from './IPushToken';
export * from './IPushNotificationConfig';
export * from './SlashCommands';

export * from './IPendingUser';
export * from './IUserDataFile';
export * from './IUserSession';
export * from './IUserStatus';
Expand Down
1 change: 1 addition & 0 deletions packages/model-typings/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ export * from './models/IOAuthAccessTokensModel';
export * from './models/IOAuthRefreshTokensModel';
export * from './models/IOEmbedCacheModel';
export * from './models/IPbxEventsModel';
export * from './models/IPendingUsersModel';
export * from './models/IPushTokenModel';
export * from './models/IPermissionsModel';
export * from './models/IReadReceiptsModel';
Expand Down
10 changes: 10 additions & 0 deletions packages/model-typings/src/models/IPendingUsersModel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import type { IPendingUser } from '@rocket.chat/core-typings';
import type { InsertOneResult } from 'mongodb';

import type { IBaseModel, InsertionModel } from './IBaseModel';

export interface IPendingUsersModel extends IBaseModel<IPendingUser> {
insertOne(user: InsertionModel<Omit<IPendingUser, 'token'>>): Promise<InsertOneResult<IPendingUser>>;

findOneByToken(token: string): Promise<IPendingUser | null>;
}
2 changes: 2 additions & 0 deletions packages/models/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ import type {
ITeamMemberModel,
ITeamModel,
IUploadsModel,
IPendingUsersModel,
IUserDataFilesModel,
IUsersSessionsModel,
IUsersModel,
Expand Down Expand Up @@ -154,6 +155,7 @@ export const Statistics = proxify<IStatisticsModel>('IStatisticsModel');
export const Subscriptions = proxify<ISubscriptionsModel>('ISubscriptionsModel');
export const TeamMember = proxify<ITeamMemberModel>('ITeamMemberModel');
export const Team = proxify<ITeamModel>('ITeamModel');
export const PendingUsers = proxify<IPendingUsersModel>('IPendingUsersModel');
export const Users = proxify<IUsersModel>('IUsersModel');
export const Uploads = proxify<IUploadsModel>('IUploadsModel');
export const UserDataFiles = proxify<IUserDataFilesModel>('IUserDataFilesModel');
Expand Down
9 changes: 9 additions & 0 deletions packages/rest-typings/src/v1/users.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import type {
IUser,
IPersonalAccessToken,
UserStatus,
IPendingUser,
} from '@rocket.chat/core-typings';
import Ajv from 'ajv';

Expand All @@ -15,6 +16,7 @@ import type { UserCreateParamsPOST } from './users/UserCreateParamsPOST';
import type { UserDeactivateIdleParamsPOST } from './users/UserDeactivateIdleParamsPOST';
import type { UserLogoutParamsPOST } from './users/UserLogoutParamsPOST';
import type { UserRegisterParamsPOST } from './users/UserRegisterParamsPOST';
import type { UserRegisterPasswordlessDevParamsPOST } from './users/UserRegisterPasswordlessDevParamsPOST';
import type { UserSetActiveStatusParamsPOST } from './users/UserSetActiveStatusParamsPOST';
import type { UsersAutocompleteParamsGET } from './users/UsersAutocompleteParamsGET';
import type { UsersInfoParamsGet } from './users/UsersInfoParamsGet';
Expand Down Expand Up @@ -338,6 +340,12 @@ export type UsersEndpoints = {
};
};

'/v1/users.registerPasswordless': {
POST: (params: UserRegisterPasswordlessDevParamsPOST) => {
user: Partial<IPendingUser>;
};
};

'/v1/users.logout': {
POST: (params: UserLogoutParamsPOST) => {
message: string;
Expand Down Expand Up @@ -374,6 +382,7 @@ export * from './users/UserSetActiveStatusParamsPOST';
export * from './users/UserDeactivateIdleParamsPOST';
export * from './users/UsersInfoParamsGet';
export * from './users/UserRegisterParamsPOST';
export * from './users/UserRegisterPasswordlessDevParamsPOST';
export * from './users/UserLogoutParamsPOST';
export * from './users/UsersListTeamsParamsGET';
export * from './users/UsersAutocompleteParamsGET';
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import Ajv from 'ajv';

const ajv = new Ajv({
coerceTypes: true,
});

export type UserRegisterPasswordlessDevParamsPOST = {
username: string;
name: string;
email: string;
};

const UserRegisterPasswordlessDevParamsPostSchema = {
type: 'object',
properties: {
username: {
type: 'string',
},
name: {
type: 'string',
},
email: {
type: 'string',
},
},
required: ['username', 'name', 'email'],
additionalProperties: false,
};

export const isUserRegisterPasswordlessDevParamsPOST = ajv.compile<UserRegisterPasswordlessDevParamsPOST>(
UserRegisterPasswordlessDevParamsPostSchema,
);
31 changes: 31 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -5024,6 +5024,15 @@ __metadata:
languageName: node
linkType: hard

"@passwordlessdev/passwordless-nodejs@npm:^0.2.0":
version: 0.2.0
resolution: "@passwordlessdev/passwordless-nodejs@npm:0.2.0"
dependencies:
axios: ^1.4.0
checksum: 1d5cb47598be6a51e684e36ad2552f393abaab37d1cabab3dd272218fc780eca438f562ed715578e73620f5f1a8ed9f616c6cd90ef754f88a5a41306497fdaef
languageName: node
linkType: hard

"@playwright/test@npm:^1.37.1":
version: 1.37.1
resolution: "@playwright/test@npm:1.37.1"
Expand Down Expand Up @@ -8640,6 +8649,7 @@ __metadata:
"@nivo/heatmap": 0.80.0
"@nivo/line": 0.80.0
"@nivo/pie": 0.80.0
"@passwordlessdev/passwordless-nodejs": ^0.2.0
"@playwright/test": ^1.37.1
"@react-aria/color": ^3.0.0-beta.15
"@react-pdf/renderer": ^3.1.12
Expand Down Expand Up @@ -15201,6 +15211,17 @@ __metadata:
languageName: node
linkType: hard

"axios@npm:^1.4.0":
version: 1.5.1
resolution: "axios@npm:1.5.1"
dependencies:
follow-redirects: ^1.15.0
form-data: ^4.0.0
proxy-from-env: ^1.1.0
checksum: 4444f06601f4ede154183767863d2b8e472b4a6bfc5253597ed6d21899887e1fd0ee2b3de792ac4f8459fe2e359d2aa07c216e45fd8b9e4e0688a6ebf48a5a8d
languageName: node
linkType: hard

"babel-jest@npm:^29.0.3, babel-jest@npm:^29.5.0, babel-jest@npm:^29.6.1":
version: 29.6.1
resolution: "babel-jest@npm:29.6.1"
Expand Down Expand Up @@ -21776,6 +21797,16 @@ __metadata:
languageName: node
linkType: hard

"follow-redirects@npm:^1.15.0":
version: 1.15.3
resolution: "follow-redirects@npm:1.15.3"
peerDependenciesMeta:
debug:
optional: true
checksum: 584da22ec5420c837bd096559ebfb8fe69d82512d5585004e36a3b4a6ef6d5905780e0c74508c7b72f907d1fa2b7bd339e613859e9c304d0dc96af2027fd0231
languageName: node
linkType: hard

"fontkit@npm:^2.0.2":
version: 2.0.2
resolution: "fontkit@npm:2.0.2"
Expand Down

0 comments on commit ff066c4

Please sign in to comment.