1
import { useEnv } from '@directus/env';
2
import { ForbiddenError, InvalidCredentialsError } from '@directus/errors';
3
import type { Item, PrimaryKey } from '@directus/types';
4
import argon2 from 'argon2';
5
import jwt from 'jsonwebtoken';
6
import { useLogger } from '../logger.js';
8
AbstractServiceOptions,
13
} from '../types/index.js';
14
import { getMilliseconds } from '../utils/get-milliseconds.js';
15
import { getSecret } from '../utils/get-secret.js';
16
import { md } from '../utils/md.js';
17
import { Url } from '../utils/url.js';
18
import { userName } from '../utils/user-name.js';
19
import { AuthorizationService } from './authorization.js';
20
import { ItemsService } from './items.js';
21
import { MailService } from './mail/index.js';
22
import { UsersService } from './users.js';
25
const logger = useLogger();
27
export class SharesService extends ItemsService {
28
authorizationService: AuthorizationService;
30
constructor(options: AbstractServiceOptions) {
31
super('directus_shares', options);
33
this.authorizationService = new AuthorizationService({
34
accountability: this.accountability,
40
override async createOne(data: Partial<Item>, opts?: MutationOptions): Promise<PrimaryKey> {
41
await this.authorizationService.checkAccess('share', data['collection'], data['item']);
42
return super.createOne(data, opts);
46
payload: Record<string, any>,
50
): Promise<Omit<LoginResult, 'id'>> {
51
const { nanoid } = await import('nanoid');
53
const record = await this.knex
58
share_collection: 'collection',
59
share_start: 'date_start',
60
share_end: 'date_end',
61
share_times_used: 'times_used',
62
share_max_uses: 'max_uses',
63
share_password: 'password',
65
.from('directus_shares')
66
.where('id', payload['share'])
67
.andWhere((subQuery) => {
68
subQuery.whereNull('date_end').orWhere('date_end', '>=', new Date());
70
.andWhere((subQuery) => {
71
subQuery.whereNull('date_start').orWhere('date_start', '<=', new Date());
73
.andWhere((subQuery) => {
74
subQuery.whereNull('max_uses').orWhere('max_uses', '>=', this.knex.ref('times_used'));
79
throw new InvalidCredentialsError();
82
if (record.share_password && !(await argon2.verify(record.share_password, payload['password']))) {
83
throw new InvalidCredentialsError();
86
await this.knex('directus_shares')
87
.update({ times_used: record.share_times_used + 1 })
88
.where('id', record.share_id);
90
const tokenPayload: DirectusTokenPayload = {
93
role: record.share_role,
94
share: record.share_id,
96
item: record.share_item,
97
collection: record.share_collection,
101
const refreshToken = nanoid(64);
102
const refreshTokenExpiration = new Date(Date.now() + getMilliseconds(env['REFRESH_TOKEN_TTL'], 0));
104
if (options?.session) {
105
tokenPayload.session = refreshToken;
108
const TTL = env[options?.session ? 'SESSION_COOKIE_TTL' : 'ACCESS_TOKEN_TTL'] as string;
110
const accessToken = jwt.sign(tokenPayload, getSecret(), {
115
await this.knex('directus_sessions').insert({
117
expires: refreshTokenExpiration,
118
ip: this.accountability?.ip,
119
user_agent: this.accountability?.userAgent,
120
origin: this.accountability?.origin,
121
share: record.share_id,
124
await this.knex('directus_sessions').delete().where('expires', '<', new Date());
129
expires: getMilliseconds(TTL),
134
* Send a link to the given share ID to the given email(s). Note: you can only send a link to a share
135
* if you have read access to that particular share
137
async invite(payload: { emails: string[]; share: PrimaryKey }) {
138
if (!this.accountability?.user) throw new ForbiddenError();
140
const share = await this.readOne(payload.share, { fields: ['collection'] });
142
const usersService = new UsersService({
147
const mailService = new MailService({ schema: this.schema, accountability: this.accountability });
149
const userInfo = await usersService.readOne(this.accountability.user, {
150
fields: ['first_name', 'last_name', 'email', 'id'],
156
${userName(userInfo)} has invited you to view an item in ${share['collection']}.
158
[Open](${new Url(env['PUBLIC_URL'] as string).addPath('admin', 'shared', payload.share).toString()})
161
for (const email of payload.emails) {
171
subject: `${userName(userInfo)} has shared an item with you`,
174
logger.error(error, `Could not send share notification mail`);