Skip to content

Commit

Permalink
Merge branch 'main' into translations
Browse files Browse the repository at this point in the history
  • Loading branch information
br41nslug committed May 21, 2024
2 parents 2c917e3 + 13da3cb commit 34415a0
Show file tree
Hide file tree
Showing 65 changed files with 1,006 additions and 669 deletions.
5 changes: 5 additions & 0 deletions .changeset/brave-students-tap.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@directus/app': patch
---

Fixed the cards layout if used with files that fail loading
5 changes: 5 additions & 0 deletions .changeset/bright-trains-greet.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@directus/app': patch
---

Reduced the number of session token refreshes if the token is still fresh
5 changes: 5 additions & 0 deletions .changeset/curvy-houses-grin.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@directus/app': patch
---

Fixed the failing auto-creating of default languages for translation fields when using an existing, custom language collection.
5 changes: 5 additions & 0 deletions .changeset/dry-carrots-sin.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@directus/api": patch
---

Fixed database session expiration to use `SESSION_TOKEN_TTL` for session mode
5 changes: 5 additions & 0 deletions .changeset/empty-baboons-wave.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@directus/sdk": patch
---

Fixed running SDK in dev mode
6 changes: 6 additions & 0 deletions .changeset/five-moons-complain.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@directus/api": patch
"@directus/env": patch
---

Implemented a "safety window" when refreshing session tokens
5 changes: 5 additions & 0 deletions .changeset/fresh-garlics-cheer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@directus/app': patch
---

Added missing translations for focal point coordinate form fields
13 changes: 13 additions & 0 deletions .changeset/happy-oranges-play.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
"@directus/api": patch
"@directus/app": patch
"@directus/components": patch
"@directus/extensions": patch
"@directus/extensions-registry": patch
"@directus/release-notes-generator": patch
"@directus/storage-driver-gcs": patch
"@directus/themes": patch
"@directus/update-check": patch
---

Updated dependencies
5 changes: 5 additions & 0 deletions .changeset/new-carrots-do.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@directus/app': patch
---

Fixed display template not showing newly added fields under collection field settings
5 changes: 5 additions & 0 deletions .changeset/old-lizards-sleep.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@directus/app': patch
---

Added reload button when the app errors out
5 changes: 5 additions & 0 deletions .changeset/plenty-trees-draw.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@directus/api': patch
---

Fixed count function on relational, filtered fields
5 changes: 5 additions & 0 deletions .changeset/strong-garlics-visit.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@directus/app": patch
---

Ensured items are correctly loaded when switching the view in the calendar layout
5 changes: 5 additions & 0 deletions .changeset/twenty-hats-laugh.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@directus/api': patch
---

Fixed `_between` and `_nbetween` filters using a function, such as `count()` and `year()`
5 changes: 5 additions & 0 deletions .changeset/yellow-frogs-warn.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@directus/app": patch
---

Fixed missing field selection dropdown for functions in filter interface
4 changes: 4 additions & 0 deletions .github/ISSUE_TEMPLATE/bug_report.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,7 @@ body:
- Directus Cloud
validations:
required: true
- type: input
attributes:
label: Database
description: If applicable, the vendor and version of the database being used. For example, PostgreSQL 16.
6 changes: 3 additions & 3 deletions api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -169,11 +169,11 @@
"sharp": "0.33.3",
"snappy": "7.2.2",
"stream-json": "1.8.0",
"tar": "7.0.1",
"tar": "7.1.0",
"tsx": "4.9.3",
"wellknown": "0.5.0",
"ws": "8.17.0",
"zod": "3.23.6",
"zod": "3.23.8",
"zod-validation-error": "3.2.0"
},
"devDependencies": {
Expand Down Expand Up @@ -202,7 +202,7 @@
"@types/lodash-es": "4.17.12",
"@types/mime-types": "2.1.4",
"@types/ms": "0.7.34",
"@types/node": "18.19.31",
"@types/node": "18.19.33",
"@types/node-schedule": "2.1.7",
"@types/nodemailer": "6.4.15",
"@types/object-hash": "3.0.6",
Expand Down
19 changes: 18 additions & 1 deletion api/src/database/helpers/fn/types.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { Query, SchemaOverview } from '@directus/types';
import type { Knex } from 'knex';
import { applyFilter, generateAlias } from '../../../utils/apply-query.js';
import type { AliasMap } from '../../../utils/get-column-path.js';
import { DatabaseHelper } from '../types.js';

export type FnHelperOptions = {
Expand Down Expand Up @@ -41,6 +42,7 @@ export abstract class FnHelper extends DatabaseHelper {
throw new Error(`Field ${collectionName}.${column} isn't a nested relational collection`);
}

// generate a unique alias for the relation collection, to prevent collisions in self referencing relations
const alias = generateAlias();

let countQuery = this.knex
Expand All @@ -49,7 +51,22 @@ export abstract class FnHelper extends DatabaseHelper {
.where(this.knex.raw(`??.??`, [alias, relation.field]), '=', this.knex.raw(`??.??`, [table, currentPrimary]));

if (options?.query?.filter) {
countQuery = applyFilter(this.knex, this.schema, countQuery, options.query.filter, relation.collection, {}).query;
// set the newly aliased collection in the alias map as the default parent collection, indicated by '', for any nested filters
const aliasMap: AliasMap = {
'': {
alias,
collection: relation.collection,
},
};

countQuery = applyFilter(
this.knex,
this.schema,
countQuery,
options.query.filter,
relation.collection,
aliasMap,
).query;
}

return this.knex.raw('(' + countQuery.toQuery() + ')');
Expand Down
13 changes: 13 additions & 0 deletions api/src/database/migrations/20240515A-add-session-window.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import type { Knex } from 'knex';

export async function up(knex: Knex): Promise<void> {
await knex.schema.alterTable('directus_sessions', (table) => {
table.string('next_token', 64).nullable();
});
}

export async function down(knex: Knex): Promise<void> {
await knex.schema.alterTable('directus_sessions', (table) => {
table.dropColumn('next_token');
});
}
102 changes: 94 additions & 8 deletions api/src/middleware/authenticate.test.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import { useEnv } from '@directus/env';
import { InvalidCredentialsError } from '@directus/errors';
import type { Request, Response } from 'express';
import jwt from 'jsonwebtoken';
import type { Knex } from 'knex';
import { afterEach, beforeEach, expect, test, vi } from 'vitest';
import { afterEach, expect, test, vi } from 'vitest';
import getDatabase from '../database/index.js';
import emitter from '../emitter.js';
import '../types/express.d.ts';
Expand All @@ -13,14 +12,20 @@ vi.mock('../database/index');

// This is required because logger uses global env which is imported before the tests run. Can be
// reduce to just mock the file when logger is also using useLogger everywhere @TODO
vi.mock('@directus/env', () => ({ useEnv: vi.fn().mockReturnValue({}) }));

beforeEach(() => {
vi.mocked(useEnv).mockReturnValue({
vi.mock('@directus/env', () => ({
useEnv: vi.fn().mockReturnValue({
SECRET: 'test',
EXTENSIONS_PATH: './extensions',
});
});
SESSION_COOKIE_NAME: 'directus_session',
// needed for constants.ts top level mocking
REFRESH_TOKEN_COOKIE_DOMAIN: '',
REFRESH_TOKEN_TTL: 0,
REFRESH_TOKEN_COOKIE_SECURE: false,
SESSION_COOKIE_DOMAIN: '',
SESSION_COOKIE_TTL: 0,
SESSION_COOKIE_SECURE: false,
}),
}));

afterEach(() => {
vi.clearAllMocks();
Expand All @@ -29,6 +34,7 @@ afterEach(() => {
test('Short-circuits when authenticate filter is used', async () => {
const req = {
ip: '127.0.0.1',
cookies: {},
get: vi.fn(),
} as unknown as Request;

Expand All @@ -48,6 +54,7 @@ test('Short-circuits when authenticate filter is used', async () => {
test('Uses default public accountability when no token is given', async () => {
const req = {
ip: '127.0.0.1',
cookies: {},
get: vi.fn((string) => {
switch (string) {
case 'user-agent':
Expand Down Expand Up @@ -108,6 +115,7 @@ test('Sets accountability to payload contents if valid token is passed', async (

const req = {
ip: '127.0.0.1',
cookies: {},
get: vi.fn((string) => {
switch (string) {
case 'user-agent':
Expand Down Expand Up @@ -184,6 +192,7 @@ test('Throws InvalidCredentialsError when static token is used, but user does no

const req = {
ip: '127.0.0.1',
cookies: {},
get: vi.fn((string) => {
switch (string) {
case 'user-agent':
Expand All @@ -207,6 +216,7 @@ test('Throws InvalidCredentialsError when static token is used, but user does no
test('Sets accountability to user information when static token is used', async () => {
const req = {
ip: '127.0.0.1',
cookies: {},
get: vi.fn((string) => {
switch (string) {
case 'user-agent':
Expand Down Expand Up @@ -266,3 +276,79 @@ test('Sets accountability to user information when static token is used', async
expect(req.accountability).toEqual(expectedAccountability);
expect(next).toHaveBeenCalledTimes(1);
});

test('Invalid session token responds with error and clears the cookie', async () => {
const req = {
ip: '127.0.0.1',
cookies: {
directus_session: 'session-token',
},
get: vi.fn((string) => {
switch (string) {
case 'user-agent':
return 'fake-user-agent';
case 'origin':
return 'fake-origin';
default:
return null;
}
}),
token: 'session-token',
} as unknown as Request;

const res = {
clearCookie: vi.fn(),
} as unknown as Response;

const next = vi.fn();

vi.mocked(getDatabase).mockReturnValue({
select: vi.fn().mockReturnThis(),
from: vi.fn().mockReturnThis(),
leftJoin: vi.fn().mockReturnThis(),
where: vi.fn().mockReturnThis(),
first: vi.fn().mockResolvedValue(null),
} as unknown as Knex);

await expect(handler(req, res, next)).rejects.toEqual(new InvalidCredentialsError());
expect(res.clearCookie).toHaveBeenCalledTimes(1);
expect(next).toHaveBeenCalledTimes(0);
});

test('Invalid query token responds with error but does not clear the session cookie', async () => {
const req = {
ip: '127.0.0.1',
cookies: {
directus_session: 'session-token',
},
get: vi.fn((string) => {
switch (string) {
case 'user-agent':
return 'fake-user-agent';
case 'origin':
return 'fake-origin';
default:
return null;
}
}),
token: 'static-token',
} as unknown as Request;

const res = {
clearCookie: vi.fn(),
} as unknown as Response;

const next = vi.fn();

vi.mocked(getDatabase).mockReturnValue({
select: vi.fn().mockReturnThis(),
from: vi.fn().mockReturnThis(),
leftJoin: vi.fn().mockReturnThis(),
where: vi.fn().mockReturnThis(),
first: vi.fn().mockResolvedValue(null),
} as unknown as Knex);

await expect(handler(req, res, next)).rejects.toEqual(new InvalidCredentialsError());
expect(next).toHaveBeenCalledTimes(0);
expect(res.clearCookie).toHaveBeenCalledTimes(0);
});
20 changes: 18 additions & 2 deletions api/src/middleware/authenticate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,16 @@ import emitter from '../emitter.js';
import asyncHandler from '../utils/async-handler.js';
import { getAccountabilityForToken } from '../utils/get-accountability-for-token.js';
import { getIPFromReq } from '../utils/get-ip-from-req.js';
import { ErrorCode, isDirectusError } from '@directus/errors';
import { useEnv } from '@directus/env';
import { SESSION_COOKIE_OPTIONS } from '../constants.js';

/**
* Verify the passed JWT and assign the user ID and role to `req`
*/
export const handler = async (req: Request, _res: Response, next: NextFunction) => {
export const handler = async (req: Request, res: Response, next: NextFunction) => {
const env = useEnv();

const defaultAccountability: Accountability = {
user: null,
role: null,
Expand Down Expand Up @@ -45,7 +50,18 @@ export const handler = async (req: Request, _res: Response, next: NextFunction)
return next();
}

req.accountability = await getAccountabilityForToken(req.token, defaultAccountability);
try {
req.accountability = await getAccountabilityForToken(req.token, defaultAccountability);
} catch (err) {
if (isDirectusError(err, ErrorCode.InvalidCredentials) || isDirectusError(err, ErrorCode.InvalidToken)) {
if (req.cookies[env['SESSION_COOKIE_NAME'] as string] === req.token) {
// clear the session token if ended up in an invalid state
res.clearCookie(env['SESSION_COOKIE_NAME'] as string, SESSION_COOKIE_OPTIONS);
}
}

throw err;
}

return next();
};
Expand Down
Loading

0 comments on commit 34415a0

Please sign in to comment.