From 7485a97a3b6a7a8f405a2ca342facea0ce894c63 Mon Sep 17 00:00:00 2001 From: Rijk van Zanten Date: Thu, 9 Apr 2020 19:06:15 -0400 Subject: [PATCH] Public (#373) * Add note to public page * Add project chooser on public view * Optimize loading order So much nicer to use now Closes #298 * Fix the private project switcher too * Add transition to public view * Prevent project switching if youre already on that project * [WIP] Add reset password form * Add request password reset page * Add jwt-payload util * Install base-64 * Fix test typing * Add new errors to translations * Finish reset password flow * Fix foreground color on v-notice * Fix tests * Allow code in translateError + render project error translated on login * Remove wrong reference to error component * Render project key if name is unknown * Fix date-fns version * Fix tests --- package.json | 2 + src/api.test.ts | 4 +- src/api.ts | 4 +- src/app.vue | 16 +--- src/auth.ts | 4 +- src/components/v-notice/v-notice.vue | 6 +- src/hydrate.ts | 2 + src/lang/en-US/index.json | 38 ++++---- src/lang/index.ts | 18 ++++ src/router.ts | 18 +++- .../components/continue-as/continue-as.vue | 75 +++++++++------ .../components/login-form/login-form.vue | 41 ++++++-- .../login/components/project-error/index.ts | 4 - .../project-error/project-error.vue | 20 ---- src/routes/login/login.vue | 33 ++++--- src/routes/reset-password/index.ts | 4 + src/routes/reset-password/request.vue | 90 ++++++++++++++++++ src/routes/reset-password/reset-password.vue | 37 ++++++++ src/routes/reset-password/reset.vue | 95 +++++++++++++++++++ src/stores/projects/projects.test.ts | 59 +++++------- src/stores/projects/projects.ts | 45 ++++++++- src/stores/projects/types.ts | 1 + src/stores/user/user.ts | 22 +++-- src/utils/jwt-payload/index.ts | 4 + src/utils/jwt-payload/jwt-payload.ts | 9 ++ src/utils/jwt-payload/readme.md | 11 +++ .../module-bar-logo/module-bar-logo.story.ts | 1 + .../project-chooser/project-chooser.vue | 4 +- src/views/public/components/logo/index.ts | 4 - src/views/public/components/logo/logo.vue | 22 ----- src/views/public/components/logo/readme.md | 36 ------- .../components/project-chooser/index.ts | 4 + .../{logo => project-chooser}/logo-dark.svg | 0 .../project-chooser.story.ts} | 8 +- .../project-chooser/project-chooser.vue | 85 +++++++++++++++++ .../components/project-chooser/readme.md | 35 +++++++ src/views/public/public-view.test.ts | 13 ++- src/views/public/public-view.vue | 70 +++++++++++--- yarn.lock | 10 ++ 39 files changed, 710 insertions(+), 244 deletions(-) delete mode 100644 src/routes/login/components/project-error/index.ts delete mode 100644 src/routes/login/components/project-error/project-error.vue create mode 100644 src/routes/reset-password/index.ts create mode 100644 src/routes/reset-password/request.vue create mode 100644 src/routes/reset-password/reset-password.vue create mode 100644 src/routes/reset-password/reset.vue create mode 100644 src/utils/jwt-payload/index.ts create mode 100644 src/utils/jwt-payload/jwt-payload.ts create mode 100644 src/utils/jwt-payload/readme.md delete mode 100644 src/views/public/components/logo/index.ts delete mode 100644 src/views/public/components/logo/logo.vue delete mode 100644 src/views/public/components/logo/readme.md create mode 100644 src/views/public/components/project-chooser/index.ts rename src/views/public/components/{logo => project-chooser}/logo-dark.svg (100%) rename src/views/public/components/{logo/logo.story.ts => project-chooser/project-chooser.story.ts} (75%) create mode 100644 src/views/public/components/project-chooser/project-chooser.vue create mode 100644 src/views/public/components/project-chooser/readme.md diff --git a/package.json b/package.json index 116aac7a75..167bc8d281 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "@types/lodash": "^4.14.149", "@vue/composition-api": "^0.5.0", "axios": "^0.19.2", + "base-64": "^0.1.0", "date-fns": "^2.12.0", "lodash": "^4.17.15", "marked": "^0.8.2", @@ -48,6 +49,7 @@ "@storybook/addons": "^5.3.18", "@storybook/core": "^5.3.18", "@storybook/vue": "^5.3.18", + "@types/base-64": "^0.1.3", "@types/jest": "^25.2.1", "@types/marked": "^0.7.3", "@typescript-eslint/eslint-plugin": "^2.27.0", diff --git a/src/api.test.ts b/src/api.test.ts index cd2484d0df..88de140ea2 100644 --- a/src/api.test.ts +++ b/src/api.test.ts @@ -1,10 +1,10 @@ import Vue from 'vue'; import VueCompositionAPI from '@vue/composition-api'; -import { onRequest, onResponse, onError, getRootPath, Error } from './api'; +import { onRequest, onResponse, onError, getRootPath, RequestError } from './api'; import * as auth from '@/auth'; import { useRequestsStore } from '@/stores/requests'; -const defaultError: Error = { +const defaultError: RequestError = { config: {}, isAxiosError: false, toJSON: () => ({}), diff --git a/src/api.ts b/src/api.ts index 02cc70fcab..5a5343c4dd 100644 --- a/src/api.ts +++ b/src/api.ts @@ -14,7 +14,7 @@ interface Response extends AxiosResponse { config: RequestConfig; } -export interface Error extends AxiosError { +export interface RequestError extends AxiosError { response: Response; } @@ -37,7 +37,7 @@ export const onResponse = (response: AxiosResponse | Response) => { return response; }; -export const onError = async (error: Error) => { +export const onError = async (error: RequestError) => { const requestsStore = useRequestsStore(); const id = (error.response.config as RequestConfig).id; requestsStore.endRequest(id); diff --git a/src/app.vue b/src/app.vue index 2e39ee2937..5d416a6168 100644 --- a/src/app.vue +++ b/src/app.vue @@ -15,7 +15,6 @@ import { defineComponent, toRefs, watch, computed } from '@vue/composition-api'; import { useAppStore } from '@/stores/app'; import { useUserStore } from '@/stores/user'; import { useProjectsStore } from '@/stores/projects'; -import { ProjectWithKey } from './stores/projects/types'; export default defineComponent({ setup() { @@ -26,18 +25,9 @@ export default defineComponent({ const { hydrating } = toRefs(appStore.state); const brandStyle = computed(() => { - if ( - projectsStore.currentProject.value && - projectsStore.currentProject.value.hasOwnProperty('api') - ) { - const project = projectsStore.currentProject.value as ProjectWithKey; - - return { - '--brand': project?.api?.project_color || 'var(--primary)', - }; - } - - return null; + return { + '--brand': projectsStore.currentProject.value?.color || 'var(--primary)', + }; }); watch( diff --git a/src/auth.ts b/src/auth.ts index 6f58f1ce1e..32c9bed66e 100644 --- a/src/auth.ts +++ b/src/auth.ts @@ -67,13 +67,13 @@ export async function logout(optionsRaw: LogoutOptions = {}) { // You can't logout of a project if you're not in a project if (currentProjectKey === null) return; - await dehydrate(); - // Only if the user manually signed out should we kill the session by hitting the logout endpoint if (options.reason === LogoutReason.SIGN_OUT) { await api.post(`/${currentProjectKey}/auth/logout`); } + await dehydrate(); + if (options.navigate === true) { const location: RawLocation = { path: `/${currentProjectKey}/login`, diff --git a/src/components/v-notice/v-notice.vue b/src/components/v-notice/v-notice.vue index e6de0b2b78..2f68a10d83 100644 --- a/src/components/v-notice/v-notice.vue +++ b/src/components/v-notice/v-notice.vue @@ -63,7 +63,7 @@ export default defineComponent({ diff --git a/src/hydrate.ts b/src/hydrate.ts index 15b5af7019..11d44a4edd 100644 --- a/src/hydrate.ts +++ b/src/hydrate.ts @@ -5,6 +5,7 @@ import { useUserStore } from '@/stores/user/'; import { useRequestsStore } from '@/stores/requests/'; import { useCollectionPresetsStore } from '@/stores/collection-presets/'; import { useSettingsStore } from '@/stores/settings/'; +import { useProjectsStore } from '@/stores/projects/'; type GenericStore = { id: string; @@ -22,6 +23,7 @@ export function useStores( useRequestsStore, useCollectionPresetsStore, useSettingsStore, + useProjectsStore, ] ) { return stores.map((useStore) => useStore()) as GenericStore[]; diff --git a/src/lang/en-US/index.json b/src/lang/en-US/index.json index 409a3edbfc..fa131d4b27 100644 --- a/src/lang/en-US/index.json +++ b/src/lang/en-US/index.json @@ -126,6 +126,28 @@ "help_and_docs": "Help & Docs", + "errors": { + "11": "Can't Reach Database", + "100": "Incorrect Email/Password", + "101": "Logged-out from Inactivity", + "102": "Logged-out from Inactivity", + "103": "User Suspended", + "105": "Reset link expired", + "106": "Incorrect Email/Password", + "107": "User Not Found", + "111": "Enter One-Time Password", + "112": "Wrong One-Time Password", + "114": "Incorrect Email/Password", + "115": "SSO is not allowed when 2FA is enabled", + "503": "Email couldn't be sent. Please verify the API's configuration", + "-1": "Couldn't Reach API" + }, + + "unexpected_error": "An unexpected error occured", + + "password_reset_sent": "We've sent you a secure link to reset your password", + "password_reset_successful": "Password successfully reset", + "about_directus": "About Directus", "activity_log": "Activity Log", @@ -319,20 +341,6 @@ "environment": "Environment", "equal_to": "Equal to", "error_unknown": "Unknown error. Try again later.", - "errors": { - "11": "Can Not Reach Database", - "100": "Incorrect Email/Password", - "101": "Logged-out from Inactivity", - "102": "Logged-out from Inactivity", - "103": "User Suspended", - "106": "Incorrect Email/Password", - "107": "User Not Found", - "111": "Enter One-Time Password", - "112": "Wrong One-Time Password", - "114": "Incorrect Email/Password", - "115": "SSO is not allowed when 2FA is enabled", - "-1": "Couldn't Reach API" - }, "esc_cancel": "Escape will cancel and close the window.", "event_count": "No Events | One Event | {count} Events", "existing": "Existing", @@ -499,8 +507,6 @@ "otp": "One-Time Password", "password": "Password", "password_reset_sending": "Sending email...", - "password_reset_sent": "If a valid user with this email address exists in Directus, we've sent you a secure link to reset your password.", - "password_reset_successful": "Password successfully reset.", "permission_states": { "always": "Always", "create": "Create", diff --git a/src/lang/index.ts b/src/lang/index.ts index 1788796b29..5cf01c6431 100644 --- a/src/lang/index.ts +++ b/src/lang/index.ts @@ -1,6 +1,7 @@ import Vue from 'vue'; import VueI18n from 'vue-i18n'; import { merge } from 'lodash'; +import { RequestError } from '@/api'; import enUSBase from './en-US/index.json'; import enUSInterfaces from './en-US/interfaces.json'; @@ -100,3 +101,20 @@ export async function setLanguage(lang: Language): Promise { } export default i18n; + +export function translateAPIError(error: RequestError | number) { + const defaultMsg = i18n.t('unexpected_error'); + + let code = error; + + if (typeof error !== 'number') { + 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); +} diff --git a/src/router.ts b/src/router.ts index 6c0c70ed0f..a60505ce10 100644 --- a/src/router.ts +++ b/src/router.ts @@ -2,6 +2,7 @@ import VueRouter, { NavigationGuard, RouteConfig, Route } from 'vue-router'; import { useProjectsStore } from '@/stores/projects'; import LoginRoute from '@/routes/login'; import LogoutRoute from '@/routes/logout'; +import ResetPasswordRoute from '@/routes/reset-password'; import ProjectChooserRoute from '@/routes/project-chooser'; import { checkAuth } from '@/auth'; import { hydrate, dehydrate } from '@/hydrate'; @@ -45,10 +46,21 @@ export const defaultRoutes: RouteConfig[] = [ public: true, }, }, + { + name: 'reset-password', + path: '/:project/reset-password', + component: ResetPasswordRoute, + meta: { + public: true, + }, + }, { name: 'logout', path: '/:project/logout', component: LogoutRoute, + meta: { + public: true, + }, }, /** * @NOTE @@ -98,9 +110,10 @@ export const onBeforeEach: NavigationGuard = async (to, from, next) => { } // Keep the projects store currentProjectKey in sync with the route + // If we switch projects to a public route, we don't need the store to be hyrdated if (to.params.project && projectsStore.state.currentProjectKey !== to.params.project) { // If the store is hydrated for the current project, make sure to dehydrate it - if (appStore.state.hydrated === true) { + if (to.meta?.public !== true && appStore.state.hydrated === true) { appStore.state.hydrating = true; await dehydrate(); } @@ -115,13 +128,14 @@ export const onBeforeEach: NavigationGuard = async (to, from, next) => { // The store can only be hydrated if you're an authenticated user. If the store is hydrated, we // can safely assume you're logged in - if (appStore.state.hydrated === false) { + if (to.meta?.public !== true && appStore.state.hydrated === false) { const authenticated = await checkAuth(); if (authenticated === true) { appStore.state.hydrating = false; await hydrate(); } else if (to.meta?.public !== true) { + appStore.state.hydrating = false; return next(`/${to.params.project}/login`); } } diff --git a/src/routes/login/components/continue-as/continue-as.vue b/src/routes/login/components/continue-as/continue-as.vue index 5900d43035..3e1d2f3d96 100644 --- a/src/routes/login/components/continue-as/continue-as.vue +++ b/src/routes/login/components/continue-as/continue-as.vue @@ -1,45 +1,66 @@ diff --git a/src/routes/login/components/login-form/login-form.vue b/src/routes/login/components/login-form/login-form.vue index 88b9516444..a70de866e9 100644 --- a/src/routes/login/components/login-form/login-form.vue +++ b/src/routes/login/components/login-form/login-form.vue @@ -15,15 +15,23 @@ :placeholder="$t('password')" full-width /> - {{ $t('sign_in') }} + + {{ errorFormatted }} + +
+ {{ $t('sign_in') }} + {{ $t('forgot_password') }} +
diff --git a/src/routes/login/components/project-error/index.ts b/src/routes/login/components/project-error/index.ts deleted file mode 100644 index 8ada6cf6db..0000000000 --- a/src/routes/login/components/project-error/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -import ProjectError from './project-error.vue'; - -export { ProjectError }; -export default ProjectError; diff --git a/src/routes/login/components/project-error/project-error.vue b/src/routes/login/components/project-error/project-error.vue deleted file mode 100644 index 413857f482..0000000000 --- a/src/routes/login/components/project-error/project-error.vue +++ /dev/null @@ -1,20 +0,0 @@ - - - diff --git a/src/routes/login/login.vue b/src/routes/login/login.vue index 5b07262dbc..5e1368f4f6 100644 --- a/src/routes/login/login.vue +++ b/src/routes/login/login.vue @@ -2,12 +2,10 @@

{{ $t('sign_in') }}

- - + + + {{ errorFormatted }} +