directus

Форк
0
/
shares.ts 
178 строк · 5.0 Кб
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';
7
import type {
8
	AbstractServiceOptions,
9
	DirectusTokenPayload,
10
	LoginResult,
11
	MutationOptions,
12
	ShareData,
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';
23

24
const env = useEnv();
25
const logger = useLogger();
26

27
export class SharesService extends ItemsService {
28
	authorizationService: AuthorizationService;
29

30
	constructor(options: AbstractServiceOptions) {
31
		super('directus_shares', options);
32

33
		this.authorizationService = new AuthorizationService({
34
			accountability: this.accountability,
35
			knex: this.knex,
36
			schema: this.schema,
37
		});
38
	}
39

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);
43
	}
44

45
	async login(
46
		payload: Record<string, any>,
47
		options?: Partial<{
48
			session: boolean;
49
		}>,
50
	): Promise<Omit<LoginResult, 'id'>> {
51
		const { nanoid } = await import('nanoid');
52

53
		const record = await this.knex
54
			.select<ShareData>({
55
				share_id: 'id',
56
				share_role: 'role',
57
				share_item: 'item',
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',
64
			})
65
			.from('directus_shares')
66
			.where('id', payload['share'])
67
			.andWhere((subQuery) => {
68
				subQuery.whereNull('date_end').orWhere('date_end', '>=', new Date());
69
			})
70
			.andWhere((subQuery) => {
71
				subQuery.whereNull('date_start').orWhere('date_start', '<=', new Date());
72
			})
73
			.andWhere((subQuery) => {
74
				subQuery.whereNull('max_uses').orWhere('max_uses', '>=', this.knex.ref('times_used'));
75
			})
76
			.first();
77

78
		if (!record) {
79
			throw new InvalidCredentialsError();
80
		}
81

82
		if (record.share_password && !(await argon2.verify(record.share_password, payload['password']))) {
83
			throw new InvalidCredentialsError();
84
		}
85

86
		await this.knex('directus_shares')
87
			.update({ times_used: record.share_times_used + 1 })
88
			.where('id', record.share_id);
89

90
		const tokenPayload: DirectusTokenPayload = {
91
			app_access: false,
92
			admin_access: false,
93
			role: record.share_role,
94
			share: record.share_id,
95
			share_scope: {
96
				item: record.share_item,
97
				collection: record.share_collection,
98
			},
99
		};
100

101
		const refreshToken = nanoid(64);
102
		const refreshTokenExpiration = new Date(Date.now() + getMilliseconds(env['REFRESH_TOKEN_TTL'], 0));
103

104
		if (options?.session) {
105
			tokenPayload.session = refreshToken;
106
		}
107

108
		const TTL = env[options?.session ? 'SESSION_COOKIE_TTL' : 'ACCESS_TOKEN_TTL'] as string;
109

110
		const accessToken = jwt.sign(tokenPayload, getSecret(), {
111
			expiresIn: TTL,
112
			issuer: 'directus',
113
		});
114

115
		await this.knex('directus_sessions').insert({
116
			token: refreshToken,
117
			expires: refreshTokenExpiration,
118
			ip: this.accountability?.ip,
119
			user_agent: this.accountability?.userAgent,
120
			origin: this.accountability?.origin,
121
			share: record.share_id,
122
		});
123

124
		await this.knex('directus_sessions').delete().where('expires', '<', new Date());
125

126
		return {
127
			accessToken,
128
			refreshToken,
129
			expires: getMilliseconds(TTL),
130
		};
131
	}
132

133
	/**
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
136
	 */
137
	async invite(payload: { emails: string[]; share: PrimaryKey }) {
138
		if (!this.accountability?.user) throw new ForbiddenError();
139

140
		const share = await this.readOne(payload.share, { fields: ['collection'] });
141

142
		const usersService = new UsersService({
143
			knex: this.knex,
144
			schema: this.schema,
145
		});
146

147
		const mailService = new MailService({ schema: this.schema, accountability: this.accountability });
148

149
		const userInfo = await usersService.readOne(this.accountability.user, {
150
			fields: ['first_name', 'last_name', 'email', 'id'],
151
		});
152

153
		const message = `
154
Hello!
155

156
${userName(userInfo)} has invited you to view an item in ${share['collection']}.
157

158
[Open](${new Url(env['PUBLIC_URL'] as string).addPath('admin', 'shared', payload.share).toString()})
159
`;
160

161
		for (const email of payload.emails) {
162
			mailService
163
				.send({
164
					template: {
165
						name: 'base',
166
						data: {
167
							html: md(message),
168
						},
169
					},
170
					to: email,
171
					subject: `${userName(userInfo)} has shared an item with you`,
172
				})
173
				.catch((error) => {
174
					logger.error(error, `Could not send share notification mail`);
175
				});
176
		}
177
	}
178
}
179

Использование cookies

Мы используем файлы cookie в соответствии с Политикой конфиденциальности и Политикой использования cookies.

Нажимая кнопку «Принимаю», Вы даете АО «СберТех» согласие на обработку Ваших персональных данных в целях совершенствования нашего веб-сайта и Сервиса GitVerse, а также повышения удобства их использования.

Запретить использование cookies Вы можете самостоятельно в настройках Вашего браузера.