mirror of
https://github.com/directus/directus.git
synced 2026-04-25 03:00:53 -04:00
Add SSO flow (#595)
* Store / parse sso providers in project info * Render sso provider links on login form * Provide path to api on sso * Finish SSO * Fix user detail route * Accept string error codes * Rename color->type, fix old v-notices * Add adding user string * Fix reading order in v-menu
This commit is contained in:
@@ -22,7 +22,7 @@
|
||||
@input="$emit('input', $event)"
|
||||
/>
|
||||
|
||||
<v-notice v-else warning>
|
||||
<v-notice v-else type="warning">
|
||||
{{ $t('interface_not_found', { interface: field.interface }) }}
|
||||
</v-notice>
|
||||
</div>
|
||||
|
||||
@@ -103,14 +103,6 @@ export default defineComponent({
|
||||
const id = computed(() => nanoid());
|
||||
const popper = ref<HTMLElement>(null);
|
||||
|
||||
const { isActive, activate, deactivate, toggle } = useActiveState();
|
||||
|
||||
watch(isActive, () => {
|
||||
Vue.nextTick(() => {
|
||||
popper.value = document.getElementById(id.value);
|
||||
});
|
||||
});
|
||||
|
||||
const { start, stop, styles, arrowStyles, placement: popperPlacement } = usePopper(
|
||||
reference,
|
||||
popper,
|
||||
@@ -121,6 +113,14 @@ export default defineComponent({
|
||||
}))
|
||||
);
|
||||
|
||||
const { isActive, activate, deactivate, toggle } = useActiveState();
|
||||
|
||||
watch(isActive, () => {
|
||||
Vue.nextTick(() => {
|
||||
popper.value = document.getElementById(id.value);
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
id,
|
||||
activator,
|
||||
@@ -157,7 +157,7 @@ export default defineComponent({
|
||||
watch(popper, async () => {
|
||||
if (popper.value !== null) {
|
||||
await start();
|
||||
} else if (stop) {
|
||||
} else {
|
||||
stop();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { withKnobs, text, boolean } from '@storybook/addon-knobs';
|
||||
import { withKnobs, text, select } from '@storybook/addon-knobs';
|
||||
import Vue from 'vue';
|
||||
import VNotice from './v-notice.vue';
|
||||
import VIcon from '../v-icon/';
|
||||
@@ -22,17 +22,11 @@ export const withText = () => ({
|
||||
text: {
|
||||
default: text('Text', 'This is a notice'),
|
||||
},
|
||||
success: {
|
||||
default: boolean('Success', false),
|
||||
},
|
||||
warning: {
|
||||
default: boolean('Warning', false),
|
||||
},
|
||||
danger: {
|
||||
default: boolean('Danger', false),
|
||||
type: {
|
||||
default: select('Type', ['info', 'success', 'warning', 'danger'], 'info'),
|
||||
},
|
||||
},
|
||||
template: `<v-notice :success="success" :warning="warning" :danger="danger">{{text}}</v-notice>`,
|
||||
template: `<v-notice :type="type">{{text}}</v-notice>`,
|
||||
});
|
||||
|
||||
export const withCustomColors = () => ({
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="v-notice" :class="[color, { center }]">
|
||||
<div class="v-notice" :class="[type, { center }]">
|
||||
<v-icon v-if="icon !== false" :name="iconName" left />
|
||||
<slot />
|
||||
</div>
|
||||
@@ -10,7 +10,7 @@ import { defineComponent, computed, PropType } from '@vue/composition-api';
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
color: {
|
||||
type: {
|
||||
type: String as PropType<'normal' | 'info' | 'success' | 'warning' | 'danger'>,
|
||||
default: 'normal',
|
||||
},
|
||||
@@ -29,13 +29,13 @@ export default defineComponent({
|
||||
return props.icon;
|
||||
}
|
||||
|
||||
if (props.color == 'info') {
|
||||
if (props.type == 'info') {
|
||||
return 'info';
|
||||
} else if (props.color == 'success') {
|
||||
} else if (props.type == 'success') {
|
||||
return 'check_circle';
|
||||
} else if (props.color == 'warning') {
|
||||
} else if (props.type == 'warning') {
|
||||
return 'warning';
|
||||
} else if (props.color == 'danger') {
|
||||
} else if (props.type == 'danger') {
|
||||
return 'error';
|
||||
} else {
|
||||
return 'info';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<v-notice v-if="!choices" warning>
|
||||
<v-notice v-if="!choices" type="warning">
|
||||
{{ $t('choices_option_configured_incorrectly') }}
|
||||
</v-notice>
|
||||
<div
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<v-notice v-if="!choices" warning>
|
||||
<v-notice v-if="!choices" type="warning">
|
||||
{{ $t('choices_option_configured_incorrectly') }}
|
||||
</v-notice>
|
||||
<v-select
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<v-notice v-if="!choices" warning>
|
||||
<v-notice v-if="!choices" type="warning">
|
||||
{{ $t('choices_option_configured_incorrectly') }}
|
||||
</v-notice>
|
||||
<v-select
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<template>
|
||||
<v-notice warning v-if="!relation">
|
||||
<v-notice type="warning" v-if="!relation">
|
||||
{{ $t('relationship_not_setup') }}
|
||||
</v-notice>
|
||||
<v-notice warning v-else-if="!displayTemplate">
|
||||
<v-notice type="warning" v-else-if="!displayTemplate">
|
||||
{{ $t('display_template_not_setup') }}
|
||||
</v-notice>
|
||||
<div class="many-to-one" v-else>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<v-notice :icon="icon" :color="color">
|
||||
<v-notice :icon="icon" :type="color">
|
||||
<div v-html="marked(text)" />
|
||||
</v-notice>
|
||||
</template>
|
||||
@@ -23,7 +23,7 @@ export default defineComponent({
|
||||
default: 'No text configured...',
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
setup() {
|
||||
return { marked };
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<v-notice warning v-if="!relation">
|
||||
<v-notice type="warning" v-if="!relation">
|
||||
{{ $t('relationship_not_setup') }}
|
||||
</v-notice>
|
||||
<div class="one-to-many" v-else>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<v-notice v-if="!choices" warning>
|
||||
<v-notice v-if="!choices" type="warning">
|
||||
{{ $t('choices_option_configured_incorrectly') }}
|
||||
</v-notice>
|
||||
<div
|
||||
|
||||
@@ -525,6 +525,8 @@
|
||||
|
||||
"other": "Other...",
|
||||
|
||||
"adding_user": "Adding User",
|
||||
|
||||
"statuses_not_configured": "Status mapping option configured incorrectly",
|
||||
"status_mapping": "Status Mapping",
|
||||
|
||||
|
||||
@@ -107,13 +107,14 @@ export function translateAPIError(error: RequestError | number) {
|
||||
|
||||
let code = error;
|
||||
|
||||
if (typeof error !== 'number') {
|
||||
if (typeof error === 'object') {
|
||||
code = error?.response?.data?.error?.code;
|
||||
}
|
||||
|
||||
if (!error) return defaultMsg;
|
||||
if (!code === undefined) return defaultMsg;
|
||||
const key = `errors.${code}`;
|
||||
|
||||
const exists = i18n.te(key);
|
||||
if (exists === false) return defaultMsg;
|
||||
return i18n.t(key);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<private-view :title="loading ? $t('loading') : item.first_name + ' ' + item.last_name">
|
||||
<private-view :title="title">
|
||||
<template #title-outer:prepend>
|
||||
<v-button class="header-icon" rounded icon secondary exact :to="breadcrumb[0].to">
|
||||
<v-icon name="arrow_back" />
|
||||
@@ -137,7 +137,19 @@ export default defineComponent({
|
||||
|
||||
const confirmDelete = ref(false);
|
||||
|
||||
const title = computed(() => {
|
||||
if (loading.value === true) return i18n.t('loading');
|
||||
|
||||
if (isNew.value === false && item.value !== null) {
|
||||
const user = item.value as any;
|
||||
return `${user.first_name} ${user.last_name}`;
|
||||
}
|
||||
|
||||
return i18n.t('adding_user');
|
||||
});
|
||||
|
||||
return {
|
||||
title,
|
||||
item,
|
||||
loading,
|
||||
error,
|
||||
|
||||
@@ -46,7 +46,7 @@ export const defaultRoutes: RouteConfig[] = [
|
||||
path: '/:project/login',
|
||||
component: LoginRoute,
|
||||
props: (route) => ({
|
||||
ssoErrorCode: route.query.error,
|
||||
ssoErrorCode: route.query.error ? route.query.code : null,
|
||||
}),
|
||||
meta: {
|
||||
public: true,
|
||||
|
||||
@@ -9,14 +9,14 @@
|
||||
<v-progress-linear v-if="loading" indeterminate />
|
||||
|
||||
<template v-else>
|
||||
<v-notice danger v-if="error">
|
||||
<v-notice type="danger" v-if="error">
|
||||
{{ errorFormatted }}
|
||||
</v-notice>
|
||||
|
||||
<template v-else>{{ $t('creating_project_success_copy') }}</template>
|
||||
|
||||
<template v-if="first">
|
||||
<v-notice warning>
|
||||
<v-notice type="warning">
|
||||
{{ $t('creating_project_success_super_admin_password') }}
|
||||
</v-notice>
|
||||
|
||||
|
||||
@@ -9,8 +9,7 @@
|
||||
v-else
|
||||
v-for="requirement in requirements"
|
||||
:key="requirement.key"
|
||||
:success="requirement.success"
|
||||
:warning="requirement.success === false"
|
||||
:type="requirement.success ? 'success' : 'warning'"
|
||||
>
|
||||
{{ requirement.value }}
|
||||
</v-notice>
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
v-model="password"
|
||||
:placeholder="$t('password')"
|
||||
/>
|
||||
<v-notice danger v-if="error">
|
||||
<v-notice type="warning" v-if="error">
|
||||
{{ errorFormatted }}
|
||||
</v-notice>
|
||||
<div class="buttons">
|
||||
@@ -22,6 +22,26 @@
|
||||
{{ $t('forgot_password') }}
|
||||
</router-link>
|
||||
</div>
|
||||
|
||||
<template v-if="ssoProviders">
|
||||
<v-divider class="sso-divider" />
|
||||
|
||||
<v-button
|
||||
secondary
|
||||
class="sso-button"
|
||||
rounded
|
||||
icon
|
||||
v-for="provider in ssoProviders"
|
||||
:key="provider.name"
|
||||
:href="provider.link"
|
||||
>
|
||||
<img :src="provider.icon" :alt="provider.name" />
|
||||
</v-button>
|
||||
|
||||
<v-notice class="sso-notice" type="danger" v-if="ssoError">
|
||||
{{ translateAPIError(ssoError) }}
|
||||
</v-notice>
|
||||
</template>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
@@ -32,8 +52,15 @@ import { useProjectsStore } from '@/stores/projects';
|
||||
import { login } from '@/auth';
|
||||
import { RequestError } from '@/api';
|
||||
import { translateAPIError } from '@/lang';
|
||||
import getRootPath from '@/utils/get-root-path';
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
ssoError: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
const projectsStore = useProjectsStore();
|
||||
|
||||
@@ -53,7 +80,30 @@ export default defineComponent({
|
||||
return `/${projectsStore.state.currentProjectKey}/reset-password`;
|
||||
});
|
||||
|
||||
return { errorFormatted, error, email, password, onSubmit, loggingIn, forgotLink };
|
||||
const ssoProviders = computed(() => {
|
||||
const redirectURL =
|
||||
getRootPath() + `admin/${projectsStore.state.currentProjectKey}/login`;
|
||||
return projectsStore.currentProject.value.sso.map(
|
||||
(provider: { icon: string; name: string }) => {
|
||||
return {
|
||||
...provider,
|
||||
link: `/${projectsStore.state.currentProjectKey}/auth/sso/${provider.name}?mode=cookie&redirect_url=${redirectURL}`,
|
||||
};
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
return {
|
||||
ssoProviders,
|
||||
errorFormatted,
|
||||
error,
|
||||
email,
|
||||
password,
|
||||
onSubmit,
|
||||
loggingIn,
|
||||
forgotLink,
|
||||
translateAPIError,
|
||||
};
|
||||
|
||||
async function onSubmit() {
|
||||
if (email.value === null || password.value === null) return;
|
||||
@@ -98,4 +148,19 @@ export default defineComponent({
|
||||
color: var(--foreground-normal);
|
||||
}
|
||||
}
|
||||
|
||||
.sso-divider {
|
||||
margin: 24px 0;
|
||||
}
|
||||
|
||||
.sso-button {
|
||||
img {
|
||||
width: 24px;
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.sso-notice {
|
||||
margin-top: 24px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -3,10 +3,10 @@
|
||||
<h1 class="type-title">{{ $t('sign_in') }}</h1>
|
||||
|
||||
<continue-as v-if="currentProject.authenticated" />
|
||||
<v-notice danger v-else-if="currentProject && currentProject.error">
|
||||
<v-notice type="danger" v-else-if="currentProject && currentProject.error">
|
||||
{{ errorFormatted }}
|
||||
</v-notice>
|
||||
<login-form v-else />
|
||||
<login-form v-else :sso-error="ssoErrorCode" />
|
||||
|
||||
<template #notice>
|
||||
<v-icon name="lock_outlined" left />
|
||||
@@ -24,6 +24,12 @@ import useProjectsStore from '../../stores/projects';
|
||||
import { translateAPIError } from '@/lang';
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
ssoErrorCode: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
components: { LoginForm, ContinueAs },
|
||||
setup() {
|
||||
const projectsStore = useProjectsStore();
|
||||
|
||||
@@ -7,8 +7,8 @@
|
||||
v-model="email"
|
||||
:placeholder="$t('email')"
|
||||
/>
|
||||
<v-notice success v-if="done">{{ $t('password_reset_sent') }}</v-notice>
|
||||
<v-notice danger v-if="error">
|
||||
<v-notice type="success" v-if="done">{{ $t('password_reset_sent') }}</v-notice>
|
||||
<v-notice type="danger" v-if="error">
|
||||
{{ errorFormatted }}
|
||||
</v-notice>
|
||||
<div class="buttons">
|
||||
|
||||
@@ -9,8 +9,8 @@
|
||||
v-model="password"
|
||||
:disabled="done"
|
||||
/>
|
||||
<v-notice success v-if="done">{{ $t('password_reset_successful') }}</v-notice>
|
||||
<v-notice danger v-if="error">
|
||||
<v-notice type="success" v-if="done">{{ $t('password_reset_successful') }}</v-notice>
|
||||
<v-notice type="danger" v-if="error">
|
||||
{{ errorFormatted }}
|
||||
</v-notice>
|
||||
<v-button v-if="!done" type="submit" :loading="resetting" large>{{ $t('reset') }}</v-button>
|
||||
|
||||
@@ -28,6 +28,7 @@ export const useProjectsStore = createStore({
|
||||
logo: project?.api?.project_logo?.full_url || null,
|
||||
color: project?.api?.project_color || null,
|
||||
note: project?.api?.project_public_note || null,
|
||||
sso: project?.api?.sso || [],
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
@@ -27,6 +27,10 @@ export interface Project {
|
||||
default_locale: string;
|
||||
telemetry: boolean;
|
||||
project_name: string;
|
||||
sso: {
|
||||
name: string;
|
||||
icon: string;
|
||||
}[];
|
||||
};
|
||||
server?: {
|
||||
max_upload_size: number;
|
||||
|
||||
@@ -65,6 +65,7 @@ export const withCustomLogo = () =>
|
||||
telemetry: true,
|
||||
default_locale: 'en-US',
|
||||
project_public_note: null,
|
||||
sso: [],
|
||||
},
|
||||
server: {
|
||||
max_upload_size: 104857600,
|
||||
|
||||
@@ -49,6 +49,7 @@ const mockProject: ProjectWithKey = {
|
||||
default_locale: 'en-US',
|
||||
project_public_note:
|
||||
'**Welcome to the Directus Public Demo!**\n\nYou can sign in with `admin@example.com` and `password`. Occasionally users break things, but don’t worry… the whole server resets each hour.',
|
||||
sso: [],
|
||||
},
|
||||
server: {
|
||||
max_upload_size: 104857600,
|
||||
|
||||
Reference in New Issue
Block a user