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
18 changes: 17 additions & 1 deletion apps/meteor/app/api/server/v1/oauthapps.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { isOauthAppsGetParams } from '@rocket.chat/rest-typings';
import { isOauthAppsGetParams, isOauthAppsAddParams } from '@rocket.chat/rest-typings';
import { OAuthApps } from '@rocket.chat/models';

import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission';
import { API } from '../api';
import { addOAuthApp } from '../../../oauth2-server-config/server/admin/methods/addOAuthApp';

API.v1.addRoute(
'oauth-apps.list',
Expand Down Expand Up @@ -41,3 +42,18 @@ API.v1.addRoute(
},
},
);

API.v1.addRoute(
'oauth-apps.create',
{
authRequired: true,
validateParams: isOauthAppsAddParams,
},
{
async post() {
const application = await addOAuthApp(this.bodyParams, this.userId);

return API.v1.success({ application });
},
},
);
Original file line number Diff line number Diff line change
@@ -1,44 +1,50 @@
import { Meteor } from 'meteor/meteor';
import { Random } from 'meteor/random';
import _ from 'underscore';
import { OAuthApps } from '@rocket.chat/models';
import { OAuthApps, Users } from '@rocket.chat/models';

import { hasPermission } from '../../../../authorization';
import { Users } from '../../../../models/server';
import { hasPermission } from '../../../../authorization/server';
import { parseUriList } from '../functions/parseUriList';
import { methodDeprecationLogger } from '../../../../lib/server/lib/deprecationWarningLogger';

Meteor.methods({
async addOAuthApp(application) {
if (!hasPermission(this.userId, 'manage-oauth-apps')) {
throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'addOAuthApp' });
}
if (!_.isString(application.name) || application.name.trim() === '') {
throw new Meteor.Error('error-invalid-name', 'Invalid name', { method: 'addOAuthApp' });
}
if (!_.isString(application.redirectUri) || application.redirectUri.trim() === '') {
throw new Meteor.Error('error-invalid-redirectUri', 'Invalid redirectUri', {
method: 'addOAuthApp',
});
}
if (!_.isBoolean(application.active)) {
throw new Meteor.Error('error-invalid-arguments', 'Invalid arguments', {
method: 'addOAuthApp',
});
}
export async function addOAuthApp(application, uid) {
if (!hasPermission(uid, 'manage-oauth-apps')) {
throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'addOAuthApp' });
}
if (!_.isString(application.name) || application.name.trim() === '') {
throw new Meteor.Error('error-invalid-name', 'Invalid name', { method: 'addOAuthApp' });
}
if (!_.isString(application.redirectUri) || application.redirectUri.trim() === '') {
throw new Meteor.Error('error-invalid-redirectUri', 'Invalid redirectUri', {
method: 'addOAuthApp',
});
}
if (!_.isBoolean(application.active)) {
throw new Meteor.Error('error-invalid-arguments', 'Invalid arguments', {
method: 'addOAuthApp',
});
}

application.redirectUri = parseUriList(application.redirectUri);

application.redirectUri = parseUriList(application.redirectUri);
if (application.redirectUri.length === 0) {
throw new Meteor.Error('error-invalid-redirectUri', 'Invalid redirectUri', {
method: 'addOAuthApp',
});
}

if (application.redirectUri.length === 0) {
throw new Meteor.Error('error-invalid-redirectUri', 'Invalid redirectUri', {
method: 'addOAuthApp',
});
}
application.clientId = Random.id();
application.clientSecret = Random.secret();
application._createdAt = new Date();
application._createdBy = await Users.findOne(uid, { projection: { username: 1 } });
application._id = (await OAuthApps.insertOne(application)).insertedId;
return application;
}

Meteor.methods({
async addOAuthApp(application) {
methodDeprecationLogger.warn('addOAuthApp is deprecated and will be removed in future versions of Rocket.Chat');

application.clientId = Random.id();
application.clientSecret = Random.secret();
application._createdAt = new Date();
application._createdBy = Users.findOne(this.userId, { fields: { username: 1 } });
application._id = (await OAuthApps.insertOne(application)).insertedId;
return application;
return addOAuthApp(application, this.userId);
},
});
4 changes: 2 additions & 2 deletions apps/meteor/client/views/admin/oauthApps/OAuthAddApp.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Button, ButtonGroup, TextInput, Field, TextAreaInput, ToggleSwitch, FieldGroup } from '@rocket.chat/fuselage';
import { useToastMessageDispatch, useRoute, useMethod, useTranslation } from '@rocket.chat/ui-contexts';
import { useToastMessageDispatch, useRoute, useEndpoint, useTranslation } from '@rocket.chat/ui-contexts';
import React, { ReactElement, useCallback } from 'react';
import { useForm, SubmitHandler, Controller } from 'react-hook-form';

Expand All @@ -22,7 +22,7 @@ const OAuthAddApp = (): ReactElement => {
control,
} = useForm<OAuthAddAppPayload>();

const saveApp = useMethod('addOAuthApp');
const saveApp = useEndpoint('POST', '/v1/oauth-apps.create');

const router = useRoute('admin-oauth-apps');

Expand Down
98 changes: 98 additions & 0 deletions apps/meteor/tests/end-to-end/api/18-oauthapps.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,4 +63,102 @@ describe('[OAuthApps]', function () {
.end(done);
});
});

describe('[/oauth-apps.create]', function () {
it('should return an error when the user does not have the necessary permission', async function () {
await updatePermission('manage-oauth-apps', []);

await request
.post(api('oauth-apps.create'))
.set(credentials)
.send({
name: 'error',
redirectUri: 'error',
active: false,
})
.expect('Content-Type', 'application/json')
.expect(400)
.expect((res) => {
console.log('res.body ->', res.body);
expect(res.body).to.have.property('success', false);
expect(res.body).to.have.property('errorType', 'error-not-allowed');
});

await updatePermission('manage-oauth-apps', ['admin']);
});

it("should return an error when the 'name' property is invalid", async function () {
await request
.post(api('oauth-apps.create'))
.set(credentials)
.send({
name: '',
redirectUri: 'error',
active: false,
})
.expect('Content-Type', 'application/json')
.expect(400)
.expect((res) => {
expect(res.body).to.have.property('success', false);
expect(res.body).to.have.property('errorType', 'error-invalid-name');
});
});

it("should return an error when the 'redirectUri' property is invalid", async function () {
await request
.post(api('oauth-apps.create'))
.set(credentials)
.send({
name: 'error',
redirectUri: '',
active: false,
})
.expect('Content-Type', 'application/json')
.expect(400)
.expect((res) => {
expect(res.body).to.have.property('success', false);
expect(res.body).to.have.property('errorType', 'error-invalid-redirectUri');
});
});

it("should return an error when the 'active' property is not a boolean", async function () {
await request
.post(api('oauth-apps.create'))
.set(credentials)
.send({
name: 'error',
redirectUri: 'error',
active: 'anything',
})
.expect('Content-Type', 'application/json')
.expect(400)
.expect((res) => {
expect(res.body).to.have.property('success', false);
expect(res.body).to.have.property('errorType', 'invalid-params');
});
});

it('should create an oauthApp', async function () {
const name = `new app ${Date.now()}`;
const redirectUri = 'http://localhost:3000';
const active = true;

await request
.post(api('oauth-apps.create'))
.set(credentials)
.send({
name,
redirectUri,
active,
})
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.nested.property('application.name', name);
expect(res.body).to.have.nested.property('application.redirectUri', redirectUri);
expect(res.body).to.have.nested.property('application.active', active);
});
});
});
});
57 changes: 43 additions & 14 deletions packages/rest-typings/src/v1/oauthapps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,6 @@ const ajv = new Ajv({

export type OauthAppsGetParams = { clientId: string } | { appId: string };

export type OAuthAppsEndpoint = {
'/v1/oauth-apps.list': {
GET: (params: { uid: IUser['_id'] }) => {
oauthApps: IOAuthApps[];
};
};

'/v1/oauth-apps.get': {
GET: (params: OauthAppsGetParams) => {
oauthApp: IOAuthApps;
};
};
};

const oauthAppsGetParamsSchema = {
oneOf: [
{
Expand All @@ -47,3 +33,46 @@ const oauthAppsGetParamsSchema = {
};

export const isOauthAppsGetParams = ajv.compile<OauthAppsGetParams>(oauthAppsGetParamsSchema);

export type OauthAppsAddParams = {
name: string;
active: boolean;
redirectUri: string;
};

const OauthAppsAddParamsSchema = {
type: 'object',
properties: {
name: {
type: 'string',
},
active: {
type: 'boolean',
},
redirectUri: {
type: 'string',
},
},
required: ['name', 'active', 'redirectUri'],
additionalProperties: false,
};

export const isOauthAppsAddParams = ajv.compile<OauthAppsAddParams>(OauthAppsAddParamsSchema);

export type OAuthAppsEndpoint = {
'/v1/oauth-apps.list': {
GET: (params: { uid: IUser['_id'] }) => {
oauthApps: IOAuthApps[];
};
};

'/v1/oauth-apps.get': {
GET: (params: OauthAppsGetParams) => {
oauthApp: IOAuthApps;
};
};

'/v1/oauth-apps.create': {
POST: (params: OauthAppsAddParams) => { application: IOAuthApps };
};
};