@directus/api
Version:
Directus is a real-time API and App dashboard for managing SQL database content
148 lines (145 loc) • 5.62 kB
JavaScript
import { useEnv } from '@directus/env';
import { ForbiddenError, InvalidCredentialsError } from '@directus/errors';
import argon2 from 'argon2';
import jwt from 'jsonwebtoken';
import { nanoid } from 'nanoid';
import { useLogger } from '../logger/index.js';
import { clearCache as clearPermissionsCache } from '../permissions/cache.js';
import { validateAccess } from '../permissions/modules/validate-access/validate-access.js';
import { getMilliseconds } from '../utils/get-milliseconds.js';
import { getSecret } from '../utils/get-secret.js';
import { md } from '../utils/md.js';
import { Url } from '../utils/url.js';
import { userName } from '../utils/user-name.js';
import { ItemsService } from './items.js';
import { MailService } from './mail/index.js';
import { UsersService } from './users.js';
const env = useEnv();
const logger = useLogger();
export class SharesService extends ItemsService {
constructor(options) {
super('directus_shares', options);
}
async createOne(data, opts) {
if (this.accountability) {
await validateAccess({
accountability: this.accountability,
action: 'share',
collection: data['collection'],
primaryKeys: [data['item']],
}, {
schema: this.schema,
knex: this.knex,
});
}
return super.createOne(data, opts);
}
async updateMany(keys, data, opts) {
await clearPermissionsCache();
return super.updateMany(keys, data, opts);
}
async deleteMany(keys, opts) {
await clearPermissionsCache();
return super.deleteMany(keys, opts);
}
async login(payload, options) {
const record = await this.knex
.select({
share_id: 'id',
share_start: 'date_start',
share_end: 'date_end',
share_times_used: 'times_used',
share_max_uses: 'max_uses',
share_password: 'password',
})
.from('directus_shares')
.where('id', payload['share'])
.andWhere((subQuery) => {
subQuery.whereNull('date_end').orWhere('date_end', '>=', new Date());
})
.andWhere((subQuery) => {
subQuery.whereNull('date_start').orWhere('date_start', '<=', new Date());
})
.andWhere((subQuery) => {
subQuery.whereNull('max_uses').orWhere('max_uses', '>=', this.knex.ref('times_used'));
})
.first();
if (!record) {
throw new InvalidCredentialsError();
}
if (record.share_password && !(await argon2.verify(record.share_password, payload['password']))) {
throw new InvalidCredentialsError();
}
await this.knex('directus_shares')
.update({ times_used: record.share_times_used + 1 })
.where('id', record.share_id);
const tokenPayload = {
app_access: false,
admin_access: false,
role: null,
share: record.share_id,
};
const refreshToken = nanoid(64);
const refreshTokenExpiration = new Date(Date.now() + getMilliseconds(env['REFRESH_TOKEN_TTL'], 0));
if (options?.session) {
tokenPayload.session = refreshToken;
}
const TTL = env[options?.session ? 'SESSION_COOKIE_TTL' : 'ACCESS_TOKEN_TTL'];
const accessToken = jwt.sign(tokenPayload, getSecret(), {
expiresIn: TTL,
issuer: 'directus',
});
await this.knex('directus_sessions').insert({
token: refreshToken,
expires: refreshTokenExpiration,
ip: this.accountability?.ip,
user_agent: this.accountability?.userAgent,
origin: this.accountability?.origin,
share: record.share_id,
});
await this.knex('directus_sessions').delete().where('expires', '<', new Date());
return {
accessToken,
refreshToken,
expires: getMilliseconds(TTL),
};
}
/**
* Send a link to the given share ID to the given email(s). Note: you can only send a link to a share
* if you have read access to that particular share
*/
async invite(payload) {
if (!this.accountability?.user)
throw new ForbiddenError();
const share = await this.readOne(payload.share, { fields: ['collection'] });
const usersService = new UsersService({
knex: this.knex,
schema: this.schema,
});
const mailService = new MailService({ schema: this.schema, accountability: this.accountability });
const userInfo = await usersService.readOne(this.accountability.user, {
fields: ['first_name', 'last_name', 'email', 'id'],
});
const message = `
Hello!
${userName(userInfo)} has invited you to view an item in ${share['collection']}.
[Open](${new Url(env['PUBLIC_URL']).addPath('admin', 'shared', payload.share).toString()})
`;
for (const email of payload.emails) {
mailService
.send({
template: {
name: 'base',
data: {
html: md(message),
},
},
to: email,
subject: `${userName(userInfo)} has shared an item with you`,
})
.catch((error) => {
logger.error(error, `Could not send share notification mail`);
});
}
}
}