mirror of
https://github.com/directus/directus.git
synced 2026-04-25 03:00:53 -04:00
Add ability to share items with people outside the platform (#10663)
* Add directus_shares * Don't check for usage limit on refresh * Add all endpoints to the shares controller * Move route `/auth/shared` to `/shared/auth` * Add password protection * Add `share` action in permissions * Add `shares/:pk/info` * Start on shared-view * Add basic styling for full shared view * Fixed migrations * Add inline style for shared view * Allow title override * Finish /info endpoint for shares * Add basic UUID validation to share/info endpont * Add UUID validation to other routes * Add not found state * Cleanup /extract/finish share login endpoint * Cleanup auth * Added `share_start` and `share_end` * Add share sidebar details. * Allow share permissions configuration * Hide the `new_share` button for unauthorized users * Fix uses_left displayed value * Show expired / upcoming shares * Improved expired/upcoming styling * Fixed share login query * Fix check-ip and get-permissions middlewares behaviour when role is null * Simplify cache key * Fix typescript linting issues * Handle app auth flow for shared page * Fixed /users/me response * Show when user is authenticated * Try showing item drawer in shared page * Improved shared card styling * Add shares permissions and change share card styling * Pull in schema/permissions on share * Create getPermissionForShare file * Change getPermissionsForShare signature * Render form + item on share after auth * Finalize public front end * Handle fake o2m field in applyQuery * [WIP] * New translations en-US.yaml (Bulgarian) (#10585) * smaller label height (#10587) * Update to the latest Material Icons (#10573) The icons are based on https://fonts.google.com/icons * New translations en-US.yaml (Arabic) (#10593) * New translations en-US.yaml (Arabic) (#10594) * New translations en-US.yaml (Portuguese, Brazilian) (#10604) * New translations en-US.yaml (French) (#10605) * New translations en-US.yaml (Italian) (#10613) * fix M2A list not updating (#10617) * Fix filters * Add admin filter on m2o role selection * Add admin filter on m2o role selection * Add o2m permissions traversing * Finish relational tree permissions generation * Handle implicit a2o relation * Update implicit relation regex * Fix regex * Fix implicitRelation unnesting for new regex * Fix implicitRelation length check * Rename m2a to a2o internally * Add auto-gen permissions for a2o * [WIP] Improve share UX * Add ctx menu options * Add share dialog * Add email notifications * Tweak endpoint * Tweak file interface disabled state * Add nicer invalid state to password input * Dont return info for expired/upcoming shares * Tweak disabled state for relational interfaces * Fix share button for non admin roles * Show/hide edit/delete based on permissions to shares * Fix imports of mutationtype * Resolve (my own) suggestions * Fix migration for ms sql * Resolve last suggestion Co-authored-by: Oreilles <oreilles.github@nitoref.io> Co-authored-by: Oreilles <33065839+oreilles@users.noreply.github.com> Co-authored-by: Ben Haynes <ben@rngr.org> Co-authored-by: Thien Nguyen <72242664+tatthien@users.noreply.github.com> Co-authored-by: Azri Kahar <42867097+azrikahar@users.noreply.github.com>
This commit is contained in:
@@ -11,13 +11,20 @@ import { InvalidCredentialsException, InvalidOTPException, UserSuspendedExceptio
|
||||
import { createRateLimiter } from '../rate-limiter';
|
||||
import { ActivityService } from './activity';
|
||||
import { TFAService } from './tfa';
|
||||
import { AbstractServiceOptions, Action, SchemaOverview, Session, User, SessionData } from '../types';
|
||||
import {
|
||||
AbstractServiceOptions,
|
||||
Action,
|
||||
SchemaOverview,
|
||||
Session,
|
||||
User,
|
||||
DirectusTokenPayload,
|
||||
LoginResult,
|
||||
} from '../types';
|
||||
import { Accountability } from '@directus/shared/types';
|
||||
import { SettingsService } from './settings';
|
||||
import { clone, cloneDeep, omit } from 'lodash';
|
||||
import { clone, cloneDeep } from 'lodash';
|
||||
import { performance } from 'perf_hooks';
|
||||
import { stall } from '../utils/stall';
|
||||
import logger from '../logger';
|
||||
|
||||
const loginAttemptsLimiter = createRateLimiter({ duration: 0 });
|
||||
|
||||
@@ -44,7 +51,7 @@ export class AuthenticationService {
|
||||
providerName: string = DEFAULT_AUTH_PROVIDER,
|
||||
payload: Record<string, any>,
|
||||
otp?: string
|
||||
): Promise<{ accessToken: any; refreshToken: any; expires: any; id?: any }> {
|
||||
): Promise<LoginResult> {
|
||||
const STALL_TIME = 100;
|
||||
const timeStart = performance.now();
|
||||
|
||||
@@ -52,21 +59,24 @@ export class AuthenticationService {
|
||||
|
||||
const user = await this.knex
|
||||
.select<User & { tfa_secret: string | null }>(
|
||||
'id',
|
||||
'first_name',
|
||||
'last_name',
|
||||
'email',
|
||||
'password',
|
||||
'status',
|
||||
'role',
|
||||
'tfa_secret',
|
||||
'provider',
|
||||
'external_identifier',
|
||||
'auth_data'
|
||||
'u.id',
|
||||
'u.first_name',
|
||||
'u.last_name',
|
||||
'u.email',
|
||||
'u.password',
|
||||
'u.status',
|
||||
'u.role',
|
||||
'r.admin_access',
|
||||
'r.app_access',
|
||||
'u.tfa_secret',
|
||||
'u.provider',
|
||||
'u.external_identifier',
|
||||
'u.auth_data'
|
||||
)
|
||||
.from('directus_users')
|
||||
.where('id', await provider.getUserID(cloneDeep(payload)))
|
||||
.andWhere('provider', providerName)
|
||||
.from('directus_users as u')
|
||||
.innerJoin('directus_roles as r', 'u.role', 'r.id')
|
||||
.where('u.id', await provider.getUserID(cloneDeep(payload)))
|
||||
.andWhere('u.provider', providerName)
|
||||
.first();
|
||||
|
||||
const updatedPayload = await emitter.emitFilter(
|
||||
@@ -136,10 +146,8 @@ export class AuthenticationService {
|
||||
}
|
||||
}
|
||||
|
||||
let sessionData: SessionData = null;
|
||||
|
||||
try {
|
||||
sessionData = await provider.login(clone(user), cloneDeep(updatedPayload));
|
||||
await provider.login(clone(user), cloneDeep(updatedPayload));
|
||||
} catch (e) {
|
||||
emitStatus('fail');
|
||||
await stall(STALL_TIME, timeStart);
|
||||
@@ -165,6 +173,9 @@ export class AuthenticationService {
|
||||
|
||||
const tokenPayload = {
|
||||
id: user.id,
|
||||
role: user.role,
|
||||
app_access: user.app_access,
|
||||
admin_access: user.admin_access,
|
||||
};
|
||||
|
||||
const customClaims = await emitter.emitFilter(
|
||||
@@ -197,7 +208,6 @@ export class AuthenticationService {
|
||||
expires: refreshTokenExpiration,
|
||||
ip: this.accountability?.ip,
|
||||
user_agent: this.accountability?.userAgent,
|
||||
data: sessionData && JSON.stringify(sessionData),
|
||||
});
|
||||
|
||||
await this.knex('directus_sessions').delete().where('expires', '<', new Date());
|
||||
@@ -237,55 +247,89 @@ export class AuthenticationService {
|
||||
}
|
||||
|
||||
const record = await this.knex
|
||||
.select<Session & User>(
|
||||
's.expires',
|
||||
's.data',
|
||||
'u.id',
|
||||
'u.first_name',
|
||||
'u.last_name',
|
||||
'u.email',
|
||||
'u.password',
|
||||
'u.status',
|
||||
'u.role',
|
||||
'u.provider',
|
||||
'u.external_identifier',
|
||||
'u.auth_data'
|
||||
)
|
||||
.from('directus_sessions as s')
|
||||
.innerJoin('directus_users as u', 's.user', 'u.id')
|
||||
.select({
|
||||
session_expires: 's.expires',
|
||||
user_id: 'u.id',
|
||||
user_first_name: 'u.first_name',
|
||||
user_last_name: 'u.last_name',
|
||||
user_email: 'u.email',
|
||||
user_password: 'u.password',
|
||||
user_status: 'u.status',
|
||||
user_provider: 'u.provider',
|
||||
user_external_identifier: 'u.external_identifier',
|
||||
user_auth_data: 'u.auth_data',
|
||||
role_id: 'r.id',
|
||||
role_admin_access: 'r.admin_access',
|
||||
role_app_access: 'r.app_access',
|
||||
share_id: 'd.id',
|
||||
share_item: 'd.item',
|
||||
share_role: 'd.role',
|
||||
share_collection: 'd.collection',
|
||||
share_start: 'd.date_start',
|
||||
share_end: 'd.date_end',
|
||||
share_times_used: 'd.times_used',
|
||||
share_max_uses: 'd.max_uses',
|
||||
})
|
||||
.from('directus_sessions AS s')
|
||||
.leftJoin('directus_users AS u', 's.user', 'u.id')
|
||||
.leftJoin('directus_shares AS d', 's.share', 'd.id')
|
||||
.joinRaw('LEFT JOIN directus_roles AS r ON r.id IN (u.role, d.role)')
|
||||
.where('s.token', refreshToken)
|
||||
.andWhere('s.expires', '>=', this.knex.fn.now())
|
||||
.andWhere((subQuery) => {
|
||||
subQuery.whereNull('d.date_end').orWhere('d.date_end', '>=', this.knex.fn.now());
|
||||
})
|
||||
.andWhere((subQuery) => {
|
||||
subQuery.whereNull('d.date_start').orWhere('d.date_start', '<=', this.knex.fn.now());
|
||||
})
|
||||
.first();
|
||||
|
||||
if (!record || record.expires < new Date()) {
|
||||
if (!record || (!record.share_id && !record.user_id)) {
|
||||
throw new InvalidCredentialsException();
|
||||
}
|
||||
|
||||
let { data: sessionData } = record;
|
||||
const user = omit(record, 'data');
|
||||
if (record.user_id) {
|
||||
const provider = getAuthProvider(record.user_provider);
|
||||
|
||||
if (typeof sessionData === 'string') {
|
||||
try {
|
||||
sessionData = JSON.parse(sessionData);
|
||||
} catch {
|
||||
logger.warn(`Session data isn't valid JSON: ${sessionData}`);
|
||||
}
|
||||
await provider.refresh({
|
||||
id: record.user_id,
|
||||
first_name: record.user_first_name,
|
||||
last_name: record.user_last_name,
|
||||
email: record.user_email,
|
||||
password: record.user_password,
|
||||
status: record.user_status,
|
||||
provider: record.user_provider,
|
||||
external_identifier: record.user_external_identifier,
|
||||
auth_data: record.user_auth_data,
|
||||
role: record.role_id,
|
||||
app_access: record.role_app_access,
|
||||
admin_access: record.role_admin_access,
|
||||
});
|
||||
}
|
||||
|
||||
const provider = getAuthProvider(user.provider);
|
||||
|
||||
const newSessionData = await provider.refresh(clone(user), sessionData as SessionData);
|
||||
|
||||
const tokenPayload = {
|
||||
id: user.id,
|
||||
const tokenPayload: DirectusTokenPayload = {
|
||||
id: record.user_id,
|
||||
role: record.role_id,
|
||||
app_access: record.role_app_access,
|
||||
admin_access: record.role_admin_access,
|
||||
};
|
||||
|
||||
if (record.share_id) {
|
||||
tokenPayload.share = record.share_id;
|
||||
tokenPayload.role = record.share_role;
|
||||
tokenPayload.share_scope = {
|
||||
collection: record.share_collection,
|
||||
item: record.share_item,
|
||||
};
|
||||
}
|
||||
|
||||
const customClaims = await emitter.emitFilter(
|
||||
'auth.jwt',
|
||||
tokenPayload,
|
||||
{
|
||||
status: 'pending',
|
||||
user: user?.id,
|
||||
provider: user.provider,
|
||||
user: record.user_id,
|
||||
provider: record.user_provider,
|
||||
type: 'refresh',
|
||||
},
|
||||
{
|
||||
@@ -307,17 +351,18 @@ export class AuthenticationService {
|
||||
.update({
|
||||
token: newRefreshToken,
|
||||
expires: refreshTokenExpiration,
|
||||
data: newSessionData && JSON.stringify(newSessionData),
|
||||
})
|
||||
.where({ token: refreshToken });
|
||||
|
||||
await this.knex('directus_users').update({ last_access: new Date() }).where({ id: user.id });
|
||||
if (record.user_id) {
|
||||
await this.knex('directus_users').update({ last_access: new Date() }).where({ id: record.user_id });
|
||||
}
|
||||
|
||||
return {
|
||||
accessToken,
|
||||
refreshToken: newRefreshToken,
|
||||
expires: ms(env.ACCESS_TOKEN_TTL as string),
|
||||
id: user.id,
|
||||
id: record.user_id,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -333,8 +378,7 @@ export class AuthenticationService {
|
||||
'u.role',
|
||||
'u.provider',
|
||||
'u.external_identifier',
|
||||
'u.auth_data',
|
||||
's.data'
|
||||
'u.auth_data'
|
||||
)
|
||||
.from('directus_sessions as s')
|
||||
.innerJoin('directus_users as u', 's.user', 'u.id')
|
||||
@@ -342,19 +386,10 @@ export class AuthenticationService {
|
||||
.first();
|
||||
|
||||
if (record) {
|
||||
let { data: sessionData } = record;
|
||||
const user = omit(record, 'data');
|
||||
|
||||
if (typeof sessionData === 'string') {
|
||||
try {
|
||||
sessionData = JSON.parse(sessionData);
|
||||
} catch {
|
||||
logger.warn(`Session data isn't valid JSON: ${sessionData}`);
|
||||
}
|
||||
}
|
||||
const user = record;
|
||||
|
||||
const provider = getAuthProvider(user.provider);
|
||||
await provider.logout(clone(user), sessionData as SessionData);
|
||||
await provider.logout(clone(user));
|
||||
|
||||
await this.knex.delete().from('directus_sessions').where('token', refreshToken);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user