mirror of
https://github.com/directus/directus.git
synced 2026-04-25 03:00:53 -04:00
Add default-folder option (#3209)
* Allow set folder for imported files * Allow passing folder in file/files component; Allow pick folder for file/files/image interfaces. * Added folder system component for picking folders; Move folder picker the field from data to interface (file, files, image). * Add custom folder interface; use props for interfaces file/files/image in upload component * Allow set folder for imported files * Allow passing folder in file/files component; Allow pick folder for file/files/image interfaces. * Added folder system component for picking folders; Move folder picker the field from data to interface (file, files, image). * Add custom folder interface; use props for interfaces file/files/image in upload component * Update after rebase * Add storage_default_folder setting, use folder when deploy file * Fix files options; Add default label option for folder interface. * Fix set folder for file * UX * Add migration for column, undo seed change * Update nomanclature * Make sure file library always submits folder, cleanup setting retrieval * Use indexName on down migrate * Fix import default folder, rename customPresets->folderPreset * Rename interface import * Use undefined as default folder * Remove deprecated lang file * Fix display of folder interface, treat null as value * Move shared composable * Remove unused ref Co-authored-by: rijkvanzanten <rijkvanzanten@me.com>
This commit is contained in:
22
api/src/database/migrations/20210721A-add-default-folder.ts
Normal file
22
api/src/database/migrations/20210721A-add-default-folder.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { Knex } from 'knex';
|
||||
import { getDefaultIndexName } from '../../utils/get-default-index-name';
|
||||
|
||||
const indexName = getDefaultIndexName('foreign', 'directus_settings', 'storage_default_folder');
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
await knex.schema.alterTable('directus_settings', (table) => {
|
||||
table
|
||||
.uuid('storage_default_folder')
|
||||
.references('id')
|
||||
.inTable('directus_folders')
|
||||
.withKeyName(indexName)
|
||||
.onDelete('SET NULL');
|
||||
});
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
await knex.schema.alterTable('directus_files', (table) => {
|
||||
table.dropForeign(['storage_default_folder'], indexName);
|
||||
table.dropColumn('storage_default_folder');
|
||||
});
|
||||
}
|
||||
@@ -243,6 +243,11 @@ fields:
|
||||
text: Presets Only
|
||||
width: half
|
||||
|
||||
- field: storage_default_folder
|
||||
interface: system-folder
|
||||
width: half
|
||||
note: Default folder where new files are uploaded
|
||||
|
||||
- field: overrides_divider
|
||||
interface: presentation-divider
|
||||
options:
|
||||
|
||||
@@ -32,6 +32,14 @@ export class FilesService extends ItemsService {
|
||||
): Promise<PrimaryKey> {
|
||||
const payload = clone(data);
|
||||
|
||||
if ('folder' in payload === false) {
|
||||
const settings = await this.knex.select('storage_default_folder').from('directus_settings').first();
|
||||
|
||||
if (settings?.storage_default_folder) {
|
||||
payload.folder = settings.storage_default_folder;
|
||||
}
|
||||
}
|
||||
|
||||
if (primaryKey !== undefined) {
|
||||
await this.updateOne(primaryKey, payload, { emitEvents: false });
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
:autofocus="disabled !== true && autofocus"
|
||||
:disabled="disabled"
|
||||
:loading="loading"
|
||||
:value="modelValue === undefined ? field.schema.default_value : modelValue"
|
||||
:value="modelValue === undefined ? field.schema?.default_value : modelValue"
|
||||
:width="(field.meta && field.meta.width) || 'full'"
|
||||
:type="field.type"
|
||||
:collection="field.collection"
|
||||
@@ -62,7 +62,7 @@ export default defineComponent({
|
||||
},
|
||||
modelValue: {
|
||||
type: [String, Number, Object, Array, Boolean],
|
||||
default: null,
|
||||
default: undefined,
|
||||
},
|
||||
loading: {
|
||||
type: Boolean,
|
||||
|
||||
@@ -129,10 +129,10 @@ export default defineComponent({
|
||||
});
|
||||
|
||||
const defaultValue = computed(() => {
|
||||
const value = props.field.schema?.default_value;
|
||||
const value = props.field?.schema?.default_value;
|
||||
|
||||
if (value !== undefined) return value;
|
||||
return null;
|
||||
return undefined;
|
||||
});
|
||||
|
||||
const internalValue = computed(() => {
|
||||
|
||||
@@ -117,6 +117,10 @@ export default defineComponent({
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
folder: {
|
||||
type: String,
|
||||
default: undefined,
|
||||
},
|
||||
},
|
||||
emits: ['input'],
|
||||
setup(props, { emit }) {
|
||||
@@ -159,6 +163,12 @@ export default defineComponent({
|
||||
uploading.value = true;
|
||||
progress.value = 0;
|
||||
|
||||
const folderPreset: { folder?: string } = {};
|
||||
|
||||
if (props.folder) {
|
||||
folderPreset.folder = props.folder;
|
||||
}
|
||||
|
||||
try {
|
||||
numberOfFiles.value = files.length;
|
||||
|
||||
@@ -168,7 +178,10 @@ export default defineComponent({
|
||||
progress.value = Math.round(percentage.reduce((acc, cur) => (acc += cur)) / files.length);
|
||||
done.value = percentage.filter((p) => p === 100).length;
|
||||
},
|
||||
preset: props.preset,
|
||||
preset: {
|
||||
...props.preset,
|
||||
...folderPreset,
|
||||
},
|
||||
});
|
||||
|
||||
uploadedFiles && emit('input', uploadedFiles);
|
||||
@@ -179,7 +192,10 @@ export default defineComponent({
|
||||
done.value = percentage === 100 ? 1 : 0;
|
||||
},
|
||||
fileId: props.fileId,
|
||||
preset: props.preset,
|
||||
preset: {
|
||||
...props.preset,
|
||||
...folderPreset,
|
||||
},
|
||||
});
|
||||
|
||||
uploadedFile && emit('input', uploadedFile);
|
||||
@@ -272,6 +288,9 @@ export default defineComponent({
|
||||
try {
|
||||
const response = await api.post(`/files/import`, {
|
||||
url: url.value,
|
||||
data: {
|
||||
folder: props.folder,
|
||||
},
|
||||
});
|
||||
|
||||
if (props.multiple) {
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
<template>
|
||||
<v-list-item
|
||||
v-if="!folder.children || folder.children.length === 0"
|
||||
:active="currentFolder === folder.id"
|
||||
:disabled="disabled"
|
||||
clickable
|
||||
@click="$emit('click', folder.id)"
|
||||
>
|
||||
<v-list-item-icon>
|
||||
<v-icon :name="currentFolder === folder.id ? 'folder_open' : 'folder'" />
|
||||
</v-list-item-icon>
|
||||
<v-list-item-content>{{ folder.name }}</v-list-item-content>
|
||||
</v-list-item>
|
||||
<v-list-group
|
||||
v-else
|
||||
clickable
|
||||
:active="currentFolder === folder.id"
|
||||
:disabled="disabled"
|
||||
@click="$emit('click', folder.id)"
|
||||
>
|
||||
<template #activator>
|
||||
<v-list-item-icon>
|
||||
<v-icon :name="currentFolder === folder.id ? 'folder_open' : 'folder'" />
|
||||
</v-list-item-icon>
|
||||
<v-list-item-content>{{ folder.name }}</v-list-item-content>
|
||||
</template>
|
||||
<folder-list-item
|
||||
v-for="childFolder in folder.children"
|
||||
:key="childFolder.id"
|
||||
:folder="childFolder"
|
||||
:current-folder="currentFolder"
|
||||
:disabled="disabledFolders.includes(childFolder.id)"
|
||||
:disabled-folders="disabledFolders"
|
||||
@click="$emit('click', $event)"
|
||||
/>
|
||||
</v-list-group>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, PropType } from 'vue';
|
||||
|
||||
type Folder = {
|
||||
id: string;
|
||||
name: string;
|
||||
children: Folder[];
|
||||
};
|
||||
|
||||
export default defineComponent({
|
||||
name: 'FolderListItem',
|
||||
props: {
|
||||
folder: {
|
||||
type: Object as PropType<Folder>,
|
||||
required: true,
|
||||
},
|
||||
currentFolder: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
disabledFolders: {
|
||||
type: Array as PropType<string[]>,
|
||||
default: () => [],
|
||||
},
|
||||
},
|
||||
emits: ['click'],
|
||||
});
|
||||
</script>
|
||||
140
app/src/interfaces/_system/system-folder/folder.vue
Normal file
140
app/src/interfaces/_system/system-folder/folder.vue
Normal file
@@ -0,0 +1,140 @@
|
||||
<template>
|
||||
<v-skeleton-loader v-if="loading" />
|
||||
<v-menu
|
||||
v-else
|
||||
class="v-select"
|
||||
:attached="true"
|
||||
:show-arrow="false"
|
||||
:disabled="disabled"
|
||||
:close-on-content-click="true"
|
||||
>
|
||||
<template #activator="{ toggle, active }">
|
||||
<v-input
|
||||
readonly
|
||||
:active="active"
|
||||
:model-value="folderPath"
|
||||
:placeholder="placeholder"
|
||||
:disabled="disabled"
|
||||
@click="toggle"
|
||||
>
|
||||
<template #prepend><v-icon :name="!value ? 'folder_special' : 'folder_open'" /></template>
|
||||
<template #append><v-icon name="expand_more" :class="{ active }" /></template>
|
||||
</v-input>
|
||||
</template>
|
||||
<v-list>
|
||||
<v-list-item clickable :active="!value" @click="emitValue(null)">
|
||||
<v-list-item-icon>
|
||||
<v-icon name="folder_special" />
|
||||
</v-list-item-icon>
|
||||
<v-list-item-content>{{ t('interfaces.system-folder.root_name') }}</v-list-item-content>
|
||||
</v-list-item>
|
||||
<v-divider v-if="nestedFolders && nestedFolders.length > 0" />
|
||||
<folder-list-item
|
||||
v-for="folder in nestedFolders"
|
||||
:key="folder.id"
|
||||
clickbable
|
||||
:folder="folder"
|
||||
:current-folder="value"
|
||||
:disabled="disabledFolders.includes(folder.id)"
|
||||
:disabled-folders="disabledFolders"
|
||||
@click="emitValue"
|
||||
/>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent, PropType, ref } from 'vue';
|
||||
import FolderListItem from './folder-list-item.vue';
|
||||
import useFolders, { Folder } from '@/composables/use-folders';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
export default defineComponent({
|
||||
components: { FolderListItem },
|
||||
props: {
|
||||
value: {
|
||||
type: String,
|
||||
default: undefined,
|
||||
},
|
||||
disabledFolders: {
|
||||
type: Array as PropType<string[]>,
|
||||
default: () => [],
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
emits: ['input'],
|
||||
setup(props, { emit }) {
|
||||
const { t } = useI18n();
|
||||
|
||||
const { nestedFolders, folders, loading } = useFolders();
|
||||
|
||||
const folderPath = computed(() => {
|
||||
if (!props.value || !folders.value) {
|
||||
return t('interfaces.system-folder.root_name');
|
||||
}
|
||||
const folder = folders.value.find((folder) => folder.id === props.value);
|
||||
return folder
|
||||
? folderParentPath(folder as Folder, folders.value)
|
||||
.map((folder) => folder.name)
|
||||
.join(' / ')
|
||||
: props.value;
|
||||
});
|
||||
|
||||
return {
|
||||
emitValue,
|
||||
loading,
|
||||
folderPath,
|
||||
nestedFolders,
|
||||
onFolderSelect,
|
||||
t,
|
||||
};
|
||||
|
||||
function emitValue(id: string | null) {
|
||||
return emit('input', id);
|
||||
}
|
||||
|
||||
function folderParentPath(folder: Folder, folders: Folder[]) {
|
||||
const folderMap = new Map(folders.map((folder) => [folder.id, folder]));
|
||||
|
||||
const folderParent = (target: Folder): Folder[] =>
|
||||
(folderMap.has(target.parent) ? folderParent(folderMap.get(target.parent) as Folder) : []).concat(target);
|
||||
|
||||
return folderParent(folder);
|
||||
}
|
||||
|
||||
function onFolderSelect(folderId: string | null) {
|
||||
if (props.disabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
emit('input', folderId);
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.v-input {
|
||||
cursor: pointer;
|
||||
|
||||
.v-icon {
|
||||
transition: transform var(--medium) var(--transition-out);
|
||||
|
||||
&.active {
|
||||
transform: scaleY(-1);
|
||||
transition-timing-function: var(--transition-in);
|
||||
}
|
||||
}
|
||||
|
||||
:deep(input) {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
14
app/src/interfaces/_system/system-folder/index.ts
Normal file
14
app/src/interfaces/_system/system-folder/index.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { defineInterface } from '@/interfaces/define';
|
||||
import InterfaceSystemFolder from './folder.vue';
|
||||
|
||||
export default defineInterface({
|
||||
id: 'system-folder',
|
||||
name: '$t:interfaces.system-folder.folder',
|
||||
description: '$t:interfaces.system-folder.description',
|
||||
icon: 'folder',
|
||||
component: InterfaceSystemFolder,
|
||||
types: ['uuid'],
|
||||
options: [],
|
||||
system: true,
|
||||
recommendedDisplays: ['raw'],
|
||||
});
|
||||
@@ -42,7 +42,7 @@
|
||||
|
||||
<file-lightbox :id="image.id" v-model="lightboxActive" />
|
||||
</div>
|
||||
<v-upload v-else from-library from-url @input="setImage" />
|
||||
<v-upload v-else from-library from-url :folder="folder" @input="setImage" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -78,6 +78,10 @@ export default defineComponent({
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
folder: {
|
||||
type: String,
|
||||
default: undefined,
|
||||
},
|
||||
},
|
||||
emits: ['input'],
|
||||
setup(props, { emit }) {
|
||||
|
||||
@@ -10,6 +10,20 @@ export default defineInterface({
|
||||
types: ['uuid'],
|
||||
groups: ['file'],
|
||||
relational: true,
|
||||
options: [],
|
||||
options: [
|
||||
{
|
||||
field: 'folder',
|
||||
name: '$t:interfaces.system-folder.folder',
|
||||
type: 'uuid',
|
||||
meta: {
|
||||
width: 'full',
|
||||
interface: 'system-folder',
|
||||
note: '$t:interfaces.system-folder.field_hint',
|
||||
},
|
||||
schema: {
|
||||
default_value: undefined,
|
||||
},
|
||||
},
|
||||
],
|
||||
recommendedDisplays: ['image'],
|
||||
});
|
||||
|
||||
@@ -95,7 +95,7 @@
|
||||
<v-card>
|
||||
<v-card-title>{{ t('upload_from_device') }}</v-card-title>
|
||||
<v-card-text>
|
||||
<v-upload from-url @input="onUpload" />
|
||||
<v-upload from-url :folder="folder" @input="onUpload" />
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-button secondary @click="activeDialog = null">{{ t('cancel') }}</v-button>
|
||||
@@ -163,6 +163,10 @@ export default defineComponent({
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
folder: {
|
||||
type: String,
|
||||
default: undefined,
|
||||
},
|
||||
},
|
||||
emits: ['input'],
|
||||
setup(props, { emit }) {
|
||||
@@ -289,6 +293,9 @@ export default defineComponent({
|
||||
try {
|
||||
const response = await api.post(`/files/import`, {
|
||||
url: url.value,
|
||||
data: {
|
||||
folder: props.folder,
|
||||
},
|
||||
});
|
||||
|
||||
file.value = response.data.data;
|
||||
|
||||
@@ -10,6 +10,20 @@ export default defineInterface({
|
||||
types: ['uuid'],
|
||||
groups: ['file'],
|
||||
relational: true,
|
||||
options: [],
|
||||
options: [
|
||||
{
|
||||
field: 'folder',
|
||||
name: '$t:interfaces.system-folder.folder',
|
||||
type: 'uuid',
|
||||
meta: {
|
||||
width: 'full',
|
||||
interface: 'system-folder',
|
||||
note: '$t:interfaces.system-folder.field_hint',
|
||||
},
|
||||
schema: {
|
||||
default_value: undefined,
|
||||
},
|
||||
},
|
||||
],
|
||||
recommendedDisplays: ['file'],
|
||||
});
|
||||
|
||||
@@ -1002,6 +1002,12 @@ interfaces:
|
||||
one-to-many: One to Many
|
||||
description: Select multiple related items
|
||||
no_collection: The collection could not be found
|
||||
system-folder:
|
||||
folder: Folder
|
||||
description: Select a folder
|
||||
field_hint: Puts newly uploaded files in selected folder. Does not affect existing files that are selected.
|
||||
root_name: File Library Root
|
||||
system_default: System Defaults
|
||||
select-radio:
|
||||
radio-buttons: Radio Buttons
|
||||
description: Select one of multiple choices
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
<script lang="ts">
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { defineComponent, ref } from 'vue';
|
||||
import useFolders from '../composables/use-folders';
|
||||
import useFolders from '@/composables/use-folders';
|
||||
import api from '@/api';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { unexpectedError } from '@/utils/unexpected-error';
|
||||
|
||||
@@ -116,7 +116,7 @@
|
||||
<script lang="ts">
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { defineComponent, PropType, ref } from 'vue';
|
||||
import useFolders, { Folder } from '../composables/use-folders';
|
||||
import useFolders, { Folder } from '@/composables/use-folders';
|
||||
import api from '@/api';
|
||||
import FolderPicker from './folder-picker.vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
@@ -56,7 +56,7 @@
|
||||
<script lang="ts">
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { defineComponent, watch } from 'vue';
|
||||
import useFolders, { Folder } from '../composables/use-folders';
|
||||
import useFolders, { Folder } from '@/composables/use-folders';
|
||||
import NavigationFolder from './navigation-folder.vue';
|
||||
import arraysAreEqual from '@/utils/arrays-are-equal';
|
||||
|
||||
|
||||
@@ -165,7 +165,7 @@ import emitter, { Events } from '@/events';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useNotificationsStore, useUserStore, usePermissionsStore } from '@/stores';
|
||||
import { subDays } from 'date-fns';
|
||||
import useFolders, { Folder } from '../composables/use-folders';
|
||||
import useFolders, { Folder } from '@/composables/use-folders';
|
||||
import useEventListener from '@/composables/use-event-listener';
|
||||
import { useLayout } from '@/composables/use-layout';
|
||||
import uploadFiles from '@/utils/upload-files';
|
||||
@@ -603,11 +603,9 @@ export default defineComponent({
|
||||
});
|
||||
|
||||
await uploadFiles(files, {
|
||||
preset: props.folder
|
||||
? {
|
||||
folder: props.folder,
|
||||
}
|
||||
: {},
|
||||
preset: {
|
||||
folder: props.folder,
|
||||
},
|
||||
onProgressChange: (progress) => {
|
||||
const percentageDone = progress.reduce((val, cur) => (val += cur)) / progress.length;
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ export default async function uploadFiles(
|
||||
onProgressChange?: (percentages: number[]) => void;
|
||||
notifications?: boolean;
|
||||
preset?: Record<string, any>;
|
||||
folder?: string;
|
||||
}
|
||||
): Promise<File[] | undefined> {
|
||||
const progressHandler = options?.onProgressChange || (() => undefined);
|
||||
|
||||
@@ -24,9 +24,11 @@ export default {
|
||||
.theme-code-block {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.theme-code-block__active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.theme-code-block > pre {
|
||||
background-color: orange;
|
||||
}
|
||||
|
||||
@@ -73,12 +73,14 @@ export default {
|
||||
border-top-left-radius: 6px;
|
||||
border-top-right-radius: 6px;
|
||||
}
|
||||
|
||||
.theme-code-group__ul {
|
||||
display: inline-flex;
|
||||
margin: auto 0;
|
||||
padding-left: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.theme-code-group__nav-tab {
|
||||
padding: 5px;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
@@ -89,9 +91,11 @@ export default {
|
||||
border: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.theme-code-group__nav-tab-active {
|
||||
border-bottom: #42b983 1px solid;
|
||||
}
|
||||
|
||||
.pre-blank {
|
||||
color: #42b983;
|
||||
}
|
||||
|
||||
@@ -22,7 +22,6 @@ Before 9.0.0-rc.84 the Docker tags were prefixed by a "v" - e.g. v9.0.0-rc.83.
|
||||
|
||||
:::
|
||||
|
||||
|
||||
### Create admin user using docker
|
||||
|
||||
The published Docker image will automatically populate the database, and create a user. To configure the email/password
|
||||
|
||||
@@ -51,24 +51,24 @@ All the `DB_POOL_` prefixed options are passed [to `tarn.js`](https://github.com
|
||||
|
||||
## Security
|
||||
|
||||
| Variable | Description | Default Value |
|
||||
| -------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- | ------------- |
|
||||
| `KEY` | Unique identifier for the project. | -- |
|
||||
| `SECRET` | Secret string for the project. | -- |
|
||||
| `ACCESS_TOKEN_TTL` | The duration that the access token is valid. | 15m |
|
||||
| `REFRESH_TOKEN_TTL` | The duration that the refresh token is valid, and also how long users stay logged-in to the App. | 7d |
|
||||
| `REFRESH_TOKEN_COOKIE_DOMAIN` | Which domain to use for the refresh cookie. Useful for development mode. | -- |
|
||||
| `REFRESH_TOKEN_COOKIE_SECURE` | Whether or not to use a secure cookie for the refresh token in cookie mode. | `false` |
|
||||
| `REFRESH_TOKEN_COOKIE_SAME_SITE` | Value for `sameSite` in the refresh token cookie when in cookie mode. | `lax` |
|
||||
| `REFRESH_TOKEN_COOKIE_NAME` | Name of refresh token cookie . | `directus_refresh_token`|
|
||||
| `PASSWORD_RESET_URL_ALLOW_LIST` | List of URLs that can be used [as `reset_url` in /password/request](/reference/api/system/authentication/#request-password-reset) | -- |
|
||||
| `USER_INVITE_URL_ALLOW_LIST` | List of URLs that can be used [as `invite_url` in /users/invite](/reference/api/system/users/#invite-a-new-user) | -- |
|
||||
| Variable | Description | Default Value |
|
||||
| -------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- | ------------------------ |
|
||||
| `KEY` | Unique identifier for the project. | -- |
|
||||
| `SECRET` | Secret string for the project. | -- |
|
||||
| `ACCESS_TOKEN_TTL` | The duration that the access token is valid. | 15m |
|
||||
| `REFRESH_TOKEN_TTL` | The duration that the refresh token is valid, and also how long users stay logged-in to the App. | 7d |
|
||||
| `REFRESH_TOKEN_COOKIE_DOMAIN` | Which domain to use for the refresh cookie. Useful for development mode. | -- |
|
||||
| `REFRESH_TOKEN_COOKIE_SECURE` | Whether or not to use a secure cookie for the refresh token in cookie mode. | `false` |
|
||||
| `REFRESH_TOKEN_COOKIE_SAME_SITE` | Value for `sameSite` in the refresh token cookie when in cookie mode. | `lax` |
|
||||
| `REFRESH_TOKEN_COOKIE_NAME` | Name of refresh token cookie . | `directus_refresh_token` |
|
||||
| `PASSWORD_RESET_URL_ALLOW_LIST` | List of URLs that can be used [as `reset_url` in /password/request](/reference/api/system/authentication/#request-password-reset) | -- |
|
||||
| `USER_INVITE_URL_ALLOW_LIST` | List of URLs that can be used [as `invite_url` in /users/invite](/reference/api/system/users/#invite-a-new-user) | -- |
|
||||
|
||||
::: tip Cookie Strictness
|
||||
|
||||
Browser are pretty strict when it comes to third-party cookies. If you're running into unexpected problems when running
|
||||
your project and API on different domains, make sure to verify your configuration for `REFRESH_TOKEN_COOKIE_NAME`, `REFRESH_TOKEN_COOKIE_SECURE` and
|
||||
`REFRESH_TOKEN_COOKIE_SAME_SITE`.
|
||||
your project and API on different domains, make sure to verify your configuration for `REFRESH_TOKEN_COOKIE_NAME`,
|
||||
`REFRESH_TOKEN_COOKIE_SECURE` and `REFRESH_TOKEN_COOKIE_SAME_SITE`.
|
||||
|
||||
:::
|
||||
|
||||
|
||||
@@ -47,6 +47,10 @@ properties:
|
||||
description: Authentication password policy.
|
||||
type: string
|
||||
nullable: true
|
||||
storage_default_folder:
|
||||
description: Default folder to place files
|
||||
type: uuid
|
||||
width: full
|
||||
storage_asset_transform:
|
||||
description: What transformations are allowed in the assets endpoint.
|
||||
type: string
|
||||
|
||||
Reference in New Issue
Block a user