Server: Put admin pages under /admin

pull/6007/head
Laurent Cozic 2022-01-11 15:09:53 +00:00
parent fd322edaab
commit 09cbe3cb7c
7 changed files with 132 additions and 3 deletions

View File

@ -24,6 +24,7 @@ import { RouteResponseFormat, routeResponseFormat } from './utils/routeUtils';
import { parseEnv } from './env';
import storageConnectionCheck from './utils/storageConnectionCheck';
import { setLocale } from '@joplin/lib/locale';
import checkAdminHandler from './middleware/checkAdminHandler';
interface Argv {
env?: Env;
@ -196,6 +197,7 @@ async function main() {
app.use(apiVersionHandler);
app.use(ownerHandler);
app.use(checkAdminHandler);
app.use(notificationHandler);
app.use(clickJackingHandler);
app.use(routeHandler);

View File

@ -1,7 +1,7 @@
// The possible env variables and their defaults are listed below.
//
// The env variables can be of type string, integer or boolean. When the type is
// boolean, set the variable to "0" or "1" in your env file.
// boolean, set the variable to "true", "false", "0" or "1" in your env file.
export enum MailerSecurity {
None = 'none',

View File

@ -0,0 +1,58 @@
import { ErrorForbidden } from '../utils/errors';
import { beforeAllDb, afterAllTests, beforeEachDb, koaAppContext, koaNext, expectNotThrow, expectHttpError, createUserAndSession } from '../utils/testing/testUtils';
import checkAdminHandler from './checkAdminHandler';
describe('checkAdminHandler', function() {
beforeAll(async () => {
await beforeAllDb('checkAdminHandler');
});
afterAll(async () => {
await afterAllTests();
});
beforeEach(async () => {
await beforeEachDb();
});
test('should access /admin if the user is admin', async function() {
const { session } = await createUserAndSession(1, true);
const context = await koaAppContext({
sessionId: session.id,
request: {
method: 'GET',
url: '/admin/organizations',
},
});
await expectNotThrow(async () => checkAdminHandler(context, koaNext));
});
test('should not access /admin if the user is not admin', async function() {
const { session } = await createUserAndSession(1);
const context = await koaAppContext({
sessionId: session.id,
request: {
method: 'GET',
url: '/admin/organizations',
},
});
await expectHttpError(async () => checkAdminHandler(context, koaNext), ErrorForbidden.httpCode);
});
test('should not access /admin if the user is not logged in', async function() {
const context = await koaAppContext({
request: {
method: 'GET',
url: '/admin/organizations',
},
});
await expectHttpError(async () => checkAdminHandler(context, koaNext), ErrorForbidden.httpCode);
});
});

View File

@ -0,0 +1,12 @@
import { AppContext, KoaNext } from '../utils/types';
import { isAdminRequest } from '../utils/requestUtils';
import { ErrorForbidden } from '../utils/errors';
export default async function(ctx: AppContext, next: KoaNext): Promise<void> {
if (isAdminRequest(ctx)) {
if (!ctx.joplin.owner) throw new ErrorForbidden();
if (!ctx.joplin.owner.is_admin) throw new ErrorForbidden();
}
return next();
}

View File

@ -92,7 +92,8 @@ export default class NotificationModel extends BaseModel<Notification> {
return this.add(userId, NotificationKey.Any, NotificationLevel.Normal, message);
}
public async addError(userId: Uuid, message: string) {
public async addError(userId: Uuid, error: string | Error) {
const message = typeof error === 'string' ? error : error.message;
return this.add(userId, NotificationKey.Any, NotificationLevel.Error, message);
}

View File

@ -71,6 +71,10 @@ export function isApiRequest(ctx: AppContext): boolean {
return ctx.path.indexOf('/api/') === 0;
}
export function isAdminRequest(ctx: AppContext): boolean {
return ctx.path.indexOf('/admin/') === 0;
}
export function userIp(ctx: AppContext): string {
if (ctx.headers['x-real-ip']) return ctx.headers['x-real-ip'];
return ctx.ip;

View File

@ -1,4 +1,4 @@
import { isValidOrigin, parseSubPath, splitItemPath } from './routeUtils';
import { findMatchingRoute, isValidOrigin, parseSubPath, splitItemPath } from './routeUtils';
import { ItemAddressingType } from '../services/database/types';
import { RouteType } from './types';
@ -26,6 +26,58 @@ describe('routeUtils', function() {
}
});
it('should find a matching route', async function() {
const testCases: any[] = [
['/admin/organizations', {
route: 1,
basePath: 'admin/organizations',
subPath: {
id: '',
link: '',
addressingType: 1,
raw: '',
schema: 'admin/organizations',
},
}],
['/api/users/123', {
route: 2,
basePath: 'api/users',
subPath: {
id: '123',
link: '',
addressingType: 1,
raw: '123',
schema: 'api/users/:id',
},
}],
['/help', {
route: 3,
basePath: 'help',
subPath: {
id: '',
link: '',
addressingType: 1,
raw: '',
schema: 'help',
},
}],
];
const routes: Record<string, any> = {
'admin/organizations': 1,
'api/users': 2,
'help': 3,
};
for (const testCase of testCases) {
const [path, expected] = testCase;
const actual = findMatchingRoute(path, routes);
expect(actual).toEqual(expected);
}
});
it('should split an item path', async function() {
const testCases: any[] = [
['root:/Documents/MyFile.md:', ['root', 'Documents', 'MyFile.md']],