Add notifications system and support user mentions in comments (#9861)

* v-menu de/activated onKeyDown. No List yet.

* v-list

* add user suggestion

* uuids replaced

* user-popover working

* avatars flex row with usernames in suggestions

* added space to end of uuid insert

* autofocus + move caret to end of last insert

* removed unnecessary setTimeout()

* fixed filter 500 with ids

* better fix

* New translations en-US.yaml (French) (#9907)

* New translations en-US.yaml (French) (#9912)

* New translations en-US.yaml (French) (#9916)

* New translations en-US.yaml (Russian) (#9918)

* New translations en-US.yaml (Swedish) (#9920)

* Email updates (#9921)

* add from name for emails

* updatd email template style

* reset password email copy

* updated logo to newest version

* update invite email copy

* decouple field template logic

* push up styling

* Start on new v-template-input

* Add notifications API endpoints

Squashed commit of the following:

commit 9d86721ef795d03bc55693c0f99bde8e269d60e9
Merge: b4458c19f 34131d06e
Author: rijkvanzanten <rijkvanzanten@me.com>
Date:   Mon Nov 22 09:27:43 2021 -0500

    Merge branch 'mentions' into mentions-api

commit b4458c19f7c54f18fa415fc04c63642c2f5a17b0
Author: rijkvanzanten <rijkvanzanten@me.com>
Date:   Thu Nov 18 18:34:04 2021 -0500

    Remove unused import

commit e6a9d36bbfdf95cb18d29336da61ecb14b677934
Author: rijkvanzanten <rijkvanzanten@me.com>
Date:   Thu Nov 18 18:28:31 2021 -0500

    Extract user mentions from comments

commit b3e571a2daa287e1740a050096913662a57e9861
Merge: c93b833d2 af2a6dd7f
Author: rijkvanzanten <rijkvanzanten@me.com>
Date:   Thu Nov 18 17:39:52 2021 -0500

    Merge branch 'mentions' into mentions-api

commit c93b833d2b848e306c434b370d4e4e11967e85d0
Author: rijkvanzanten <rijkvanzanten@me.com>
Date:   Thu Nov 18 17:35:45 2021 -0500

    Send emails w/ parsed MD

commit 64bbd6596f20a07028d2387d60e33dfe4f91c032
Author: rijkvanzanten <rijkvanzanten@me.com>
Date:   Thu Nov 18 16:18:16 2021 -0500

    Add notifications endpoint + permissions

commit fba55c02dc9c303a38b1b958350684cccd3dd82c
Author: rijkvanzanten <rijkvanzanten@me.com>
Date:   Thu Nov 18 15:33:28 2021 -0500

    Add system data for notifications

* push

* Make v-template-input work

* Add the two-way binding

* submit button posting, not clearing text area

* comment text area clearing on submit

* Replace insertion correctly

* Added scope support to LDAP group and user search (#9529)

* Added scope support LDAP group and user search

* Fixed linter screwing up my markdown

* Update docs/configuration/config-options.md

* Always return correct DN for user with sub scope

* Fix indeterminate meta and schema property in advanded field creation (#9924)

* Fix impossibility to save M2M (alterations not triggered) (#9992)

* Fix alterations refactor

* fix roles aggregate query (#9994)

* Update iis.md (#9998)

added the IIS URL Rewrite module as a requirement

* New translations en-US.yaml (English, United Kingdom) (#10001)

* Fix LDAP race condition (#9993)

* Fix input ui

* Revert changes to v-field-template

* Update mentions permissions

* Fix linter warnings

* Optimize sending flow

* Revert "Rename activity->notifications module (#9446)"

This reverts commit 428e5d4ea9.

* Add notifications drawer

* Update migrations

* Improve constraints

* Add email notifications toggle on users

* Add docs, fix graphql support

* Move caret-pos to devdeps

* Remove unused new triggerKeyPressed system

* Remove unused use-caret composable

Co-authored-by: Nitwel <nitwel@arcor.de>
Co-authored-by: Rijk van Zanten <rijkvanzanten@me.com>
Co-authored-by: Ben Haynes <ben@rngr.org>
Co-authored-by: Aiden Foxx <aiden.foxx@sbab.se>
Co-authored-by: Oreille <33065839+Oreilles@users.noreply.github.com>
Co-authored-by: Azri Kahar <42867097+azrikahar@users.noreply.github.com>
Co-authored-by: Paul Boudewijn <paul@helderinternet.nl>
This commit is contained in:
Jay Cammarano
2021-11-24 16:11:26 -05:00
committed by GitHub
parent f19a549a1b
commit 25375cc481
49 changed files with 3162 additions and 2531 deletions

View File

@@ -127,6 +127,7 @@
"liquidjs": "^9.25.0",
"lodash": "^4.17.21",
"macos-release": "^2.4.1",
"marked": "^4.0.3",
"mime-types": "^2.1.31",
"ms": "^2.1.3",
"nanoid": "^3.1.23",
@@ -197,6 +198,7 @@
"@types/nodemailer": "6.4.4",
"@types/object-hash": "2.2.1",
"@types/qs": "6.9.7",
"@types/sanitize-html": "^2.5.0",
"@types/sharp": "0.29.4",
"@types/stream-json": "1.7.1",
"@types/supertest": "2.0.11",
@@ -206,6 +208,7 @@
"copyfiles": "2.4.1",
"cross-env": "7.0.3",
"jest": "27.3.1",
"sanitize-html": "^2.5.3",
"ts-jest": "27.0.7",
"ts-node-dev": "1.1.8",
"typescript": "4.5.2"

View File

@@ -17,6 +17,7 @@ import graphqlRouter from './controllers/graphql';
import itemsRouter from './controllers/items';
import notFoundHandler from './controllers/not-found';
import panelsRouter from './controllers/panels';
import notificationsRouter from './controllers/notifications';
import permissionsRouter from './controllers/permissions';
import presetsRouter from './controllers/presets';
import relationsRouter from './controllers/relations';
@@ -180,6 +181,7 @@ export default async function createApp(): Promise<express.Application> {
app.use('/files', filesRouter);
app.use('/folders', foldersRouter);
app.use('/items', itemsRouter);
app.use('/notifications', notificationsRouter);
app.use('/panels', panelsRouter);
app.use('/permissions', permissionsRouter);
app.use('/presets', presetsRouter);

View File

@@ -0,0 +1,194 @@
import express from 'express';
import { ForbiddenException } from '../exceptions';
import { respond } from '../middleware/respond';
import useCollection from '../middleware/use-collection';
import { validateBatch } from '../middleware/validate-batch';
import { MetaService, NotificationsService } from '../services';
import { PrimaryKey } from '../types';
import asyncHandler from '../utils/async-handler';
const router = express.Router();
router.use(useCollection('directus_notifications'));
router.post(
'/',
asyncHandler(async (req, res, next) => {
const service = new NotificationsService({
accountability: req.accountability,
schema: req.schema,
});
const savedKeys: PrimaryKey[] = [];
if (Array.isArray(req.body)) {
const keys = await service.createMany(req.body);
savedKeys.push(...keys);
} else {
const key = await service.createOne(req.body);
savedKeys.push(key);
}
try {
if (Array.isArray(req.body)) {
const records = await service.readMany(savedKeys, req.sanitizedQuery);
res.locals.payload = { data: records };
} else {
const record = await service.readOne(savedKeys[0], req.sanitizedQuery);
res.locals.payload = { data: record };
}
} catch (error: any) {
if (error instanceof ForbiddenException) {
return next();
}
throw error;
}
return next();
}),
respond
);
const readHandler = asyncHandler(async (req, res, next) => {
const service = new NotificationsService({
accountability: req.accountability,
schema: req.schema,
});
const metaService = new MetaService({
accountability: req.accountability,
schema: req.schema,
});
let result;
if (req.singleton) {
result = await service.readSingleton(req.sanitizedQuery);
} else if (req.body.keys) {
result = await service.readMany(req.body.keys, req.sanitizedQuery);
} else {
result = await service.readByQuery(req.sanitizedQuery);
}
const meta = await metaService.getMetaForQuery('directus_presets', req.sanitizedQuery);
res.locals.payload = { data: result, meta };
return next();
});
router.get('/', validateBatch('read'), readHandler, respond);
router.search('/', validateBatch('read'), readHandler, respond);
router.get(
'/:pk',
asyncHandler(async (req, res, next) => {
const service = new NotificationsService({
accountability: req.accountability,
schema: req.schema,
});
const record = await service.readOne(req.params.pk, req.sanitizedQuery);
res.locals.payload = { data: record || null };
return next();
}),
respond
);
router.patch(
'/',
validateBatch('update'),
asyncHandler(async (req, res, next) => {
const service = new NotificationsService({
accountability: req.accountability,
schema: req.schema,
});
let keys: PrimaryKey[] = [];
if (req.body.keys) {
keys = await service.updateMany(req.body.keys, req.body.data);
} else {
keys = await service.updateByQuery(req.body.query, req.body.data);
}
try {
const result = await service.readMany(keys, req.sanitizedQuery);
res.locals.payload = { data: result };
} catch (error: any) {
if (error instanceof ForbiddenException) {
return next();
}
throw error;
}
return next();
}),
respond
);
router.patch(
'/:pk',
asyncHandler(async (req, res, next) => {
const service = new NotificationsService({
accountability: req.accountability,
schema: req.schema,
});
const primaryKey = await service.updateOne(req.params.pk, req.body);
try {
const record = await service.readOne(primaryKey, req.sanitizedQuery);
res.locals.payload = { data: record };
} catch (error: any) {
if (error instanceof ForbiddenException) {
return next();
}
throw error;
}
return next();
}),
respond
);
router.delete(
'/',
validateBatch('delete'),
asyncHandler(async (req, res, next) => {
const service = new NotificationsService({
accountability: req.accountability,
schema: req.schema,
});
if (Array.isArray(req.body)) {
await service.deleteMany(req.body);
} else if (req.body.keys) {
await service.deleteMany(req.body.keys);
} else {
await service.deleteByQuery(req.body.query);
}
return next();
}),
respond
);
router.delete(
'/:pk',
asyncHandler(async (req, res, next) => {
const service = new NotificationsService({
accountability: req.accountability,
schema: req.schema,
});
await service.deleteOne(req.params.pk);
return next();
}),
respond
);
export default router;

View File

@@ -0,0 +1,29 @@
import { Knex } from 'knex';
export async function up(knex: Knex): Promise<void> {
await knex.schema.createTable('directus_notifications', (table) => {
table.increments();
table.timestamp('timestamp').notNullable();
table.string('status').defaultTo('inbox');
table.uuid('recipient').notNullable().references('id').inTable('directus_users').onDelete('CASCADE');
table.uuid('sender').notNullable().references('id').inTable('directus_users');
table.string('subject').notNullable();
table.text('message');
table.string('collection', 64);
table.string('item');
});
await knex.schema.alterTable('directus_users', (table) => {
table.boolean('email_notifications').defaultTo(true);
});
await knex('directus_users').update({ email_notifications: true });
}
export async function down(knex: Knex): Promise<void> {
await knex.schema.dropTable('directus_notifications');
await knex.schema.alterTable('directus_users', (table) => {
table.dropColumn('email_notifications');
});
}

View File

@@ -72,6 +72,20 @@
- collection: directus_settings
action: read
- collection: directus_notifications
action: read
permissions:
recipient:
_eq: $CURRENT_USER
fields: '*'
- collection: directus_notifications
action: update
permissions:
recipient:
_eq: $CURRENT_USER
fields: 'status'
- collection: directus_users
action: read
permissions:

View File

@@ -63,3 +63,5 @@ data:
note: $t:directus_collection.directus_dashboards
- collection: directus_panels
note: $t:directus_collection.directus_panels
- collection: directus_notifications
note: $t:directus_collection.directus_notifications

View File

@@ -0,0 +1,12 @@
table: directus_notifications
fields:
- field: id
- field: timestamp
special: date-created
- field: recipient
- field: sender
- field: subject
- field: message
- field: collection
- field: item

View File

@@ -90,6 +90,11 @@ fields:
special: conceal
width: half
- field: email_notifications
interface: boolean
width: half
special: boolean
- field: admin_divider
interface: presentation-divider
options:

View File

@@ -82,3 +82,9 @@ data:
- many_collection: directus_panels
many_field: user_created
one_collection: directus_users
- many_collection: directus_notifications
many_field: recipient
one_collection: directus_users
- many_collection: directus_notifications
many_field: sender
one_collection: directus_users

View File

@@ -1,138 +1,13 @@
import { Permission } from '@directus/shared/types';
import { deepMap, parseFilter } from '@directus/shared/utils';
import { RequestHandler } from 'express';
import { cloneDeep } from 'lodash';
import getDatabase from '../database';
import { appAccessMinimalPermissions } from '../database/system-data/app-access-permissions';
import asyncHandler from '../utils/async-handler';
import { mergePermissions } from '../utils/merge-permissions';
import { UsersService } from '../services/users';
import { RolesService } from '../services/roles';
import { getCache } from '../cache';
import hash from 'object-hash';
import env from '../env';
import { getPermissions as getPermissionsUtil } from '../utils/get-permissions';
const getPermissions: RequestHandler = asyncHandler(async (req, res, next) => {
const database = getDatabase();
const { systemCache } = getCache();
let permissions: Permission[] = [];
if (!req.accountability) {
throw new Error('"getPermissions" needs to be used after the "authenticate" middleware');
throw new Error('getPermissions middleware needs to be called after authenticate');
}
if (!req.schema) {
throw new Error('"getPermissions" needs to be used after the "schema" middleware');
}
const { user, role, app, admin } = req.accountability;
const cacheKey = `permissions-${hash({ user, role, app, admin })}`;
if (env.CACHE_PERMISSIONS !== false) {
const cachedPermissions = await systemCache.get(cacheKey);
if (cachedPermissions) {
req.accountability.permissions = cachedPermissions;
return next();
}
}
if (req.accountability.admin !== true) {
const permissionsForRole = await database
.select('*')
.from('directus_permissions')
.where({ role: req.accountability.role });
const requiredPermissionData = {
$CURRENT_USER: [] as string[],
$CURRENT_ROLE: [] as string[],
};
permissions = permissionsForRole.map((permissionRaw) => {
const permission = cloneDeep(permissionRaw);
if (permission.permissions && typeof permission.permissions === 'string') {
permission.permissions = JSON.parse(permission.permissions);
} else if (permission.permissions === null) {
permission.permissions = {};
}
if (permission.validation && typeof permission.validation === 'string') {
permission.validation = JSON.parse(permission.validation);
} else if (permission.validation === null) {
permission.validation = {};
}
if (permission.presets && typeof permission.presets === 'string') {
permission.presets = JSON.parse(permission.presets);
} else if (permission.presets === null) {
permission.presets = {};
}
if (permission.fields && typeof permission.fields === 'string') {
permission.fields = permission.fields.split(',');
} else if (permission.fields === null) {
permission.fields = [];
}
const extractPermissionData = (val: any) => {
if (typeof val === 'string' && val.startsWith('$CURRENT_USER.')) {
requiredPermissionData.$CURRENT_USER.push(val.replace('$CURRENT_USER.', ''));
}
if (typeof val === 'string' && val.startsWith('$CURRENT_ROLE.')) {
requiredPermissionData.$CURRENT_ROLE.push(val.replace('$CURRENT_ROLE.', ''));
}
return val;
};
deepMap(permission.permissions, extractPermissionData);
deepMap(permission.validation, extractPermissionData);
deepMap(permission.presets, extractPermissionData);
return permission;
});
if (req.accountability.app === true) {
permissions = mergePermissions(
permissions,
appAccessMinimalPermissions.map((perm) => ({ ...perm, role: req.accountability!.role }))
);
}
const usersService = new UsersService({ schema: req.schema });
const rolesService = new RolesService({ schema: req.schema });
const filterContext: Record<string, any> = {};
if (req.accountability.user && requiredPermissionData.$CURRENT_USER.length > 0) {
filterContext.$CURRENT_USER = await usersService.readOne(req.accountability.user, {
fields: requiredPermissionData.$CURRENT_USER,
});
}
if (req.accountability.role && requiredPermissionData.$CURRENT_ROLE.length > 0) {
filterContext.$CURRENT_ROLE = await rolesService.readOne(req.accountability.role, {
fields: requiredPermissionData.$CURRENT_ROLE,
});
}
permissions = permissions.map((permission) => {
permission.permissions = parseFilter(permission.permissions, req.accountability!, filterContext);
permission.validation = parseFilter(permission.validation, req.accountability!, filterContext);
permission.presets = parseFilter(permission.presets, req.accountability!, filterContext);
return permission;
});
if (env.CACHE_PERMISSIONS !== false) {
await systemCache.set(cacheKey, permissions);
}
}
req.accountability.permissions = permissions;
req.accountability.permissions = await getPermissionsUtil(req.accountability, req.schema);
return next();
});

View File

@@ -1,12 +1,104 @@
import { AbstractServiceOptions } from '../types';
import { ItemsService } from './index';
/**
* @TODO only return activity of the collections you have access to
*/
import { AbstractServiceOptions, PrimaryKey, Item, Action } from '../types';
import { ItemsService, MutationOptions } from './index';
import { NotificationsService } from './notifications';
import { UsersService } from './users';
import { AuthorizationService } from './authorization';
import { Accountability } from '@directus/shared/types';
import { getPermissions } from '../utils/get-permissions';
import { ForbiddenException } from '../exceptions/forbidden';
import logger from '../logger';
import { userName } from '../utils/user-name';
import { uniq } from 'lodash';
import env from '../env';
export class ActivityService extends ItemsService {
notificationsService: NotificationsService;
usersService: UsersService;
constructor(options: AbstractServiceOptions) {
super('directus_activity', options);
this.notificationsService = new NotificationsService({ schema: this.schema });
this.usersService = new UsersService({ schema: this.schema });
}
async createOne(data: Partial<Item>, opts?: MutationOptions): Promise<PrimaryKey> {
if (data.action === Action.COMMENT && typeof data.comment === 'string') {
const usersRegExp = new RegExp(/@[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}/gi);
const mentions = uniq(data.comment.match(usersRegExp) ?? []);
const sender = await this.usersService.readOne(this.accountability!.user!, {
fields: ['id', 'first_name', 'last_name', 'email'],
});
for (const mention of mentions) {
const userID = mention.substring(1);
const user = await this.usersService.readOne(userID, {
fields: ['id', 'first_name', 'last_name', 'email', 'role.id', 'role.admin_access', 'role.app_access'],
});
const accountability: Accountability = {
user: userID,
role: user.role?.id ?? null,
admin: user.role?.admin_access ?? null,
app: user.role?.app_access ?? null,
};
accountability.permissions = await getPermissions(accountability, this.schema);
const authorizationService = new AuthorizationService({ schema: this.schema, accountability });
const usersService = new UsersService({ schema: this.schema, accountability });
try {
await authorizationService.checkAccess('read', data.collection, data.item);
const templateData = await usersService.readByQuery({
fields: ['id', 'first_name', 'last_name', 'email'],
filter: { id: { _in: mentions.map((mention) => mention.substring(1)) } },
});
const userPreviews = templateData.reduce((acc, user) => {
acc[user.id] = `<em>${userName(user)}</em>`;
return acc;
}, {} as Record<string, string>);
let comment = data.comment;
for (const mention of mentions) {
comment = comment.replace(mention, userPreviews[mention.substring(1)] ?? '@Unknown User');
}
comment = `> ${comment}`;
const message = `
Hello ${userName(user)},
${userName(sender)} has mentioned you in a comment:
${comment}
<a href="${env.PUBLIC_URL}/admin/content/${data.collection}/${data.item}">Click here to view.</a>
`;
await this.notificationsService.createOne({
recipient: userID,
sender: sender.id,
subject: `You were mentioned in ${data.collection}`,
message,
collection: data.collection,
item: data.item,
});
} catch (err: any) {
if (err instanceof ForbiddenException) {
logger.warn(`User ${userID} doesn't have proper permissions to receive notification for this item.`);
} else {
throw err;
}
}
}
}
return super.createOne(data, opts);
}
}

View File

@@ -66,6 +66,7 @@ import { FoldersService } from './folders';
import { ItemsService } from './items';
import { PermissionsService } from './permissions';
import { PresetsService } from './presets';
import { NotificationsService } from './notifications';
import { RelationsService } from './relations';
import { RevisionsService } from './revisions';
import { RolesService } from './roles';
@@ -197,6 +198,7 @@ export class GraphQLService {
};
const { ReadCollectionTypes } = getReadableTypes();
const { CreateCollectionTypes, UpdateCollectionTypes, DeleteCollectionTypes } = getWritableTypes();
const scopeFilter = (collection: SchemaOverview['collections'][string]) => {
@@ -1519,6 +1521,8 @@ export class GraphQLService {
return new PermissionsService(opts);
case 'directus_presets':
return new PresetsService(opts);
case 'directus_notifications':
return new NotificationsService(opts);
case 'directus_revisions':
return new RevisionsService(opts);
case 'directus_roles':

View File

@@ -12,6 +12,7 @@ export * from './graphql';
export * from './import';
export * from './mail';
export * from './meta';
export * from './notifications';
export * from './panels';
export * from './payload';
export * from './permissions';

View File

@@ -88,6 +88,17 @@ hr {
div[style*="margin: 16px 0;"] {
margin: 0 !important;
}
blockquote {
background: #f0f4f9 !important;
border-radius: 4px !important;
margin: 0 !important;
padding: 24px !important;
}
blockquote > p {
margin: 0 !important;
}
/*]]>*/
</style>
@@ -149,4 +160,4 @@ div[style*="margin: 16px 0;"] {
</tbody>
</table>
</body>
</html>
</html>

View File

@@ -0,0 +1,48 @@
import { UsersService, MailService } from '.';
import { AbstractServiceOptions, PrimaryKey } from '../types';
import { ItemsService, MutationOptions } from './items';
import { Notification } from '@directus/shared/types';
import { md } from '../utils/md';
export class NotificationsService extends ItemsService {
usersService: UsersService;
mailService: MailService;
constructor(options: AbstractServiceOptions) {
super('directus_notifications', options);
this.usersService = new UsersService({ schema: this.schema });
this.mailService = new MailService({ schema: this.schema, accountability: this.accountability });
}
async createOne(data: Partial<Notification>, opts?: MutationOptions): Promise<PrimaryKey> {
await this.sendEmail(data);
return super.createOne(data, opts);
}
async createMany(data: Partial<Notification>[], opts?: MutationOptions): Promise<PrimaryKey[]> {
for (const notification of data) {
await this.sendEmail(notification);
}
return super.createMany(data, opts);
}
async sendEmail(data: Partial<Notification>) {
if (data.recipient) {
const user = await this.usersService.readOne(data.recipient, { fields: ['email', 'email_notifications'] });
if (user.email && user.email_notifications === true) {
await this.mailService.send({
template: {
name: 'base',
data: {
html: data.message ? md(data.message) : '',
},
},
to: user.email,
subject: data.subject,
});
}
}
}
}

View File

@@ -217,8 +217,10 @@ export class UsersService extends ItemsService {
*/
async deleteMany(keys: PrimaryKey[], opts?: MutationOptions): Promise<PrimaryKey[]> {
await this.checkRemainingAdminExistence(keys);
await super.deleteMany(keys, opts);
await this.knex('directus_notifications').update({ sender: null }).whereIn('sender', keys);
await super.deleteMany(keys, opts);
return keys;
}

View File

@@ -0,0 +1,126 @@
import { Permission, Accountability } from '@directus/shared/types';
import { deepMap, parseFilter } from '@directus/shared/utils';
import { cloneDeep } from 'lodash';
import getDatabase from '../database';
import { appAccessMinimalPermissions } from '../database/system-data/app-access-permissions';
import { mergePermissions } from '../utils/merge-permissions';
import { UsersService } from '../services/users';
import { RolesService } from '../services/roles';
import { getCache } from '../cache';
import hash from 'object-hash';
import env from '../env';
import { SchemaOverview } from '../types';
export async function getPermissions(accountability: Accountability, schema: SchemaOverview) {
const database = getDatabase();
const { systemCache } = getCache();
let permissions: Permission[] = [];
const { user, role, app, admin } = accountability;
const cacheKey = `permissions-${hash({ user, role, app, admin })}`;
if (env.CACHE_PERMISSIONS !== false) {
const cachedPermissions = await systemCache.get(cacheKey);
if (cachedPermissions) {
return cachedPermissions;
}
}
if (accountability.admin !== true) {
const permissionsForRole = await database
.select('*')
.from('directus_permissions')
.where({ role: accountability.role });
const requiredPermissionData = {
$CURRENT_USER: [] as string[],
$CURRENT_ROLE: [] as string[],
};
permissions = permissionsForRole.map((permissionRaw) => {
const permission = cloneDeep(permissionRaw);
if (permission.permissions && typeof permission.permissions === 'string') {
permission.permissions = JSON.parse(permission.permissions);
} else if (permission.permissions === null) {
permission.permissions = {};
}
if (permission.validation && typeof permission.validation === 'string') {
permission.validation = JSON.parse(permission.validation);
} else if (permission.validation === null) {
permission.validation = {};
}
if (permission.presets && typeof permission.presets === 'string') {
permission.presets = JSON.parse(permission.presets);
} else if (permission.presets === null) {
permission.presets = {};
}
if (permission.fields && typeof permission.fields === 'string') {
permission.fields = permission.fields.split(',');
} else if (permission.fields === null) {
permission.fields = [];
}
const extractPermissionData = (val: any) => {
if (typeof val === 'string' && val.startsWith('$CURRENT_USER.')) {
requiredPermissionData.$CURRENT_USER.push(val.replace('$CURRENT_USER.', ''));
}
if (typeof val === 'string' && val.startsWith('$CURRENT_ROLE.')) {
requiredPermissionData.$CURRENT_ROLE.push(val.replace('$CURRENT_ROLE.', ''));
}
return val;
};
deepMap(permission.permissions, extractPermissionData);
deepMap(permission.validation, extractPermissionData);
deepMap(permission.presets, extractPermissionData);
return permission;
});
if (accountability.app === true) {
permissions = mergePermissions(
permissions,
appAccessMinimalPermissions.map((perm) => ({ ...perm, role: accountability!.role }))
);
}
const usersService = new UsersService({ schema });
const rolesService = new RolesService({ schema });
const filterContext: Record<string, any> = {};
if (accountability.user && requiredPermissionData.$CURRENT_USER.length > 0) {
filterContext.$CURRENT_USER = await usersService.readOne(accountability.user, {
fields: requiredPermissionData.$CURRENT_USER,
});
}
if (accountability.role && requiredPermissionData.$CURRENT_ROLE.length > 0) {
filterContext.$CURRENT_ROLE = await rolesService.readOne(accountability.role, {
fields: requiredPermissionData.$CURRENT_ROLE,
});
}
permissions = permissions.map((permission) => {
permission.permissions = parseFilter(permission.permissions, accountability!, filterContext);
permission.validation = parseFilter(permission.validation, accountability!, filterContext);
permission.presets = parseFilter(permission.presets, accountability!, filterContext);
return permission;
});
if (env.CACHE_PERMISSIONS !== false) {
await systemCache.set(cacheKey, permissions);
}
}
return permissions;
}

9
api/src/utils/md.ts Normal file
View File

@@ -0,0 +1,9 @@
import { parse } from 'marked';
import sanitizeHTML from 'sanitize-html';
/**
* Render and sanitize a markdown string
*/
export function md(str: string): string {
return sanitizeHTML(parse(str));
}

View File

@@ -0,0 +1,17 @@
import { User } from '@directus/shared/types';
export function userName(user: Partial<User>): string {
if (user.first_name && user.last_name) {
return `${user.first_name} ${user.last_name}`;
}
if (user.first_name) {
return user.first_name;
}
if (user.email) {
return user.email;
}
return 'Unknown User';
}