diff --git a/src/api.test.ts b/src/api.test.ts deleted file mode 100644 index bd33c53e2e..0000000000 --- a/src/api.test.ts +++ /dev/null @@ -1,159 +0,0 @@ -import Vue from 'vue'; -import VueCompositionAPI from '@vue/composition-api'; -import { onRequest, onResponse, onError, RequestError } from './api'; -import * as auth from '@/auth'; -import { useRequestsStore } from '@/stores/requests'; - -const defaultError: RequestError = { - config: {}, - isAxiosError: false, - toJSON: () => ({}), - name: 'error', - message: '', - response: { - data: null, - status: 200, - statusText: 'OK', - headers: {}, - config: { - id: 'abc', - }, - }, -}; - -describe('API', () => { - beforeEach(() => { - jest.spyOn(auth, 'logout'); - jest.spyOn(auth, 'checkAuth'); - Vue.use(VueCompositionAPI); - window = Object.create(window); - }); - - it('Calls startRequest on the store on any request', () => { - const store = useRequestsStore({}); - const spy = jest.spyOn(store, 'startRequest'); - spy.mockImplementation(() => 'abc'); - const newRequest = onRequest({}); - expect(spy).toHaveBeenCalled(); - expect(newRequest.id).toBe('abc'); - }); - - it('Calls endRequest on responses', () => { - const store = useRequestsStore({}); - const spy = jest.spyOn(store, 'endRequest'); - onResponse({ - data: null, - status: 200, - statusText: 'OK', - headers: {}, - config: { - id: 'abc', - }, - }); - expect(spy).toHaveBeenCalledWith('abc'); - }); - - it('Calls endRequest on errors', async () => { - const store = useRequestsStore({}); - const spy = jest.spyOn(store, 'endRequest'); - try { - await onError({ - ...defaultError, - }); - } catch {} - - expect(spy).toHaveBeenCalledWith('abc'); - }); - - it('Passes the error on to the next catch handler on unrelated 401 errors', async () => { - const error = { - ...defaultError, - response: { - ...defaultError.response, - status: 401, - config: { - id: 'abc', - }, - data: { - error: { - code: -5, - }, - }, - }, - }; - - expect(onError(error)).rejects.toEqual(error); - }); - - it('Checks the auth status on 401+3 errors', async () => { - try { - await onError({ - ...defaultError, - response: { - ...defaultError.response, - config: { - id: 'abc', - }, - status: 401, - data: { - error: { - code: 3, - }, - }, - }, - }); - } catch { - expect(auth.checkAuth).toHaveBeenCalled(); - } - }); - - it('Forces a logout when the users is not logged in on 401+3 errors', async () => { - (auth.checkAuth as jest.Mock).mockImplementation(async () => false); - - try { - await onError({ - ...defaultError, - response: { - ...defaultError.response, - config: { - id: 'abc', - }, - status: 401, - data: { - error: { - code: 3, - }, - }, - }, - }); - } catch { - expect(auth.logout).toHaveBeenCalledWith({ - reason: auth.LogoutReason.ERROR_SESSION_EXPIRED, - }); - } - }); - - it('Does not call logout if the user is logged in on 401+3 error', async () => { - (auth.checkAuth as jest.Mock).mockImplementation(async () => true); - - try { - await onError({ - ...defaultError, - response: { - ...defaultError.response, - config: { - id: 'abc', - }, - status: 401, - data: { - error: { - code: 3, - }, - }, - }, - }); - } catch { - expect(auth.logout).not.toHaveBeenCalled(); - } - }); -}); diff --git a/src/app.vue b/src/app.vue index 4dd89aabef..d8fa00cbed 100644 --- a/src/app.vue +++ b/src/app.vue @@ -15,7 +15,8 @@ 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 { useSettingsStore } from '@/stores/settings'; + import useWindowSize from '@/composables/use-window-size'; import setFavicon from '@/utils/set-favicon'; @@ -23,17 +24,17 @@ export default defineComponent({ setup() { const appStore = useAppStore(); const userStore = useUserStore(); - const projectsStore = useProjectsStore(); + const settingsStore = useSettingsStore(); const { hydrating, drawerOpen } = toRefs(appStore.state); const brandStyle = computed(() => { return { - '--brand': projectsStore.currentProject.value?.color || 'var(--primary)', + '--brand': settingsStore.state.settings?.project_color || 'var(--primary)', }; }); - watch(() => projectsStore.currentProject.value?.color, setFavicon); + watch(() => settingsStore.state.settings?.project_color, setFavicon); const { width } = useWindowSize(); diff --git a/src/auth.test.ts b/src/auth.test.ts deleted file mode 100644 index ebf44dc1d7..0000000000 --- a/src/auth.test.ts +++ /dev/null @@ -1,151 +0,0 @@ -import Vue from 'vue'; -import VueCompositionAPI from '@vue/composition-api'; -import api from '@/api'; -import { checkAuth, login, logout, LogoutReason } from './auth'; -import { useProjectsStore } from '@/stores/projects'; -import router from '@/router'; -import { hydrate, dehydrate } from '@/hydrate'; - -jest.mock('@/api'); -jest.mock('@/router'); -jest.mock('@/hydrate'); - -describe('Auth', () => { - beforeAll(() => { - Vue.config.productionTip = false; - Vue.config.devtools = false; - Vue.use(VueCompositionAPI); - }); - - beforeEach(() => { - jest.spyOn(api, 'get'); - jest.spyOn(api, 'post'); - }); - - describe('checkAuth', () => { - it('Does not ping the API if the curent project key is null', async () => { - const projectsStore = useProjectsStore({}); - projectsStore.state.currentProjectKey = null; - - (api.get as jest.Mock).mockImplementation(() => - Promise.resolve({ data: { data: { authenticated: true } } }) - ); - await checkAuth(); - expect(api.get).not.toHaveBeenCalled(); - }); - - it('Calls the api with the correct endpoint', async () => { - const projectsStore = useProjectsStore({}); - projectsStore.state.currentProjectKey = 'test-project'; - - (api.get as jest.Mock).mockImplementation(() => - Promise.resolve({ data: { data: { authenticated: true } } }) - ); - await checkAuth(); - expect(api.get).toHaveBeenCalledWith('/test-project/auth/check'); - }); - - it('Returns true if user is logged in', async () => { - const projectsStore = useProjectsStore({}); - projectsStore.state.currentProjectKey = 'test-project'; - - (api.get as jest.Mock).mockImplementation(() => - Promise.resolve({ data: { data: { authenticated: true } } }) - ); - const loggedIn = await checkAuth(); - expect(loggedIn).toBe(true); - }); - - it('Returns false if user is logged out', async () => { - const projectsStore = useProjectsStore({}); - projectsStore.state.currentProjectKey = 'test-project'; - - (api.get as jest.Mock).mockImplementation(() => - Promise.resolve({ data: { data: { authenticated: false } } }) - ); - const loggedIn = await checkAuth(); - expect(loggedIn).toBe(false); - }); - }); - - describe('login', () => { - it('Calls /auth/authenticate with the provided credentials', async () => { - const projectsStore = useProjectsStore({}); - projectsStore.state.currentProjectKey = 'test-project'; - - await login({ email: 'test', password: 'test' }); - expect(api.post).toHaveBeenCalledWith('/test-project/auth/authenticate', { - mode: 'cookie', - email: 'test', - password: 'test', - }); - }); - - it('Calls hydrate on successful login', async () => { - const projectsStore = useProjectsStore({}); - projectsStore.state.currentProjectKey = 'test-project'; - - await login({ email: 'test', password: 'test' }); - - expect(hydrate).toHaveBeenCalled(); - }); - }); - - describe('logout', () => { - it('Does not do anything when there is no current project', async () => { - useProjectsStore({}); - await logout(); - expect(dehydrate).not.toHaveBeenCalled(); - }); - - it('Calls dehydrate', async () => { - const projectsStore = useProjectsStore({}); - projectsStore.state.currentProjectKey = 'test-project'; - - await logout(); - expect(dehydrate).toHaveBeenCalled(); - }); - - it('Posts to /logout on regular sign out', async () => { - const projectsStore = useProjectsStore({}); - projectsStore.state.currentProjectKey = 'test-project'; - - await logout(); - expect(api.post).toHaveBeenCalledWith('/test-project/auth/logout'); - }); - - it('Navigates to the current projects login page', async () => { - const projectsStore = useProjectsStore({}); - projectsStore.state.currentProjectKey = 'my-project'; - - await logout(); - expect(router.push).toHaveBeenCalledWith({ - path: '/my-project/login', - query: { - reason: LogoutReason.SIGN_OUT, - }, - }); - }); - - it('Does not navigate when the navigate option is false', async () => { - const projectsStore = useProjectsStore({}); - projectsStore.state.currentProjectKey = 'my-project'; - - await logout({ navigate: false }); - expect(router.push).not.toHaveBeenCalled(); - }); - - it('Adds the reason query param if any non-default reason is given', async () => { - const projectsStore = useProjectsStore({}); - projectsStore.state.currentProjectKey = 'my-project'; - - await logout({ reason: LogoutReason.ERROR_SESSION_EXPIRED }); - expect(router.push).toHaveBeenCalledWith({ - path: '/my-project/login', - query: { - reason: LogoutReason.ERROR_SESSION_EXPIRED, - }, - }); - }); - }); -}); diff --git a/src/auth.ts b/src/auth.ts index 57cb140f5e..ddd53315f4 100644 --- a/src/auth.ts +++ b/src/auth.ts @@ -1,5 +1,4 @@ import { RawLocation } from 'vue-router'; -import { useProjectsStore } from '@/stores/projects/'; import api from '@/api'; import { hydrate, dehydrate } from '@/hydrate'; import router from '@/router'; @@ -8,16 +7,14 @@ import router from '@/router'; * Check if the current user is authenticated to the current project */ export async function checkAuth() { - const { currentProjectKey } = useProjectsStore().state; - - if (!currentProjectKey) return false; - - try { - const response = await api.get(`/${currentProjectKey}/auth/check`); - return response.data.data.authenticated; - } catch { - return false; - } + /** @todo base this on existence of access token / response token */ + return true; + // try { + // const response = await api.get(`/auth/check`); + // return response.data.data.authenticated; + // } catch { + // return false; + // } } export type LoginCredentials = { @@ -26,14 +23,13 @@ export type LoginCredentials = { }; export async function login(credentials: LoginCredentials) { - const projectsStore = useProjectsStore(); - const { currentProjectKey } = projectsStore.state; - - await api.post(`/${currentProjectKey}/auth/authenticate`, { + const response = await api.post(`/auth/authenticate`, { ...credentials, mode: 'cookie', }); + api.defaults.headers['Authorization'] = `Bearer ${response.data.data.token}`; + await hydrate(); } @@ -58,22 +54,16 @@ export async function logout(optionsRaw: LogoutOptions = {}) { const options = { ...defaultOptions, ...optionsRaw }; - const projectsStore = useProjectsStore(); - const { currentProjectKey } = projectsStore.state; - - // You can't logout of a project if you're not in a project - if (currentProjectKey === null) return; - // 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 api.post(`/auth/logout`); } await dehydrate(); if (options.navigate === true) { const location: RawLocation = { - path: `/${currentProjectKey}/login`, + path: `/login`, query: { reason: options.reason }, }; diff --git a/src/composables/use-item/use-item.ts b/src/composables/use-item/use-item.ts index 2d67f123a6..764adbc813 100644 --- a/src/composables/use-item/use-item.ts +++ b/src/composables/use-item/use-item.ts @@ -1,6 +1,5 @@ import api from '@/api'; import { Ref, ref, watch, computed } from '@vue/composition-api'; -import useProjectsStore from '@/stores/projects'; import notify from '@/utils/notify'; import i18n from '@/lang'; import useCollection from '@/composables/use-collection'; @@ -20,10 +19,9 @@ export function useItem(collection: Ref, primaryKey: Ref typeof primaryKey.value === 'string' && primaryKey.value.includes(',')); const endpoint = computed(() => { - const currentProjectKey = useProjectsStore().state.currentProjectKey; return collection.value.startsWith('directus_') - ? `/${currentProjectKey}/${collection.value.substring(9)}` - : `/${currentProjectKey}/items/${collection.value}`; + ? `/${collection.value.substring(9)}` + : `/items/${collection.value}`; }); watch([collection, primaryKey], refresh, { immediate: true }); diff --git a/src/composables/use-items/use-items.ts b/src/composables/use-items/use-items.ts index 750c56d704..00a9e3735b 100644 --- a/src/composables/use-items/use-items.ts +++ b/src/composables/use-items/use-items.ts @@ -1,6 +1,5 @@ import { computed, ref, Ref, watch } from '@vue/composition-api'; import api from '@/api'; -import useProjectsStore from '@/stores/projects'; import useCollection from '@/composables/use-collection'; import Vue from 'vue'; import { isEqual } from 'lodash'; @@ -19,16 +18,14 @@ type Query = { }; export function useItems(collection: Ref, query: Query) { - const projectsStore = useProjectsStore(); const { primaryKeyField, sortField } = useCollection(collection); const { limit, fields, sort, page, filters, searchQuery } = query; const endpoint = computed(() => { - const { currentProjectKey } = projectsStore.state; return collection.value.startsWith('directus_') - ? `/${currentProjectKey}/${collection.value.substring(9)}` - : `/${currentProjectKey}/items/${collection.value}`; + ? `/${collection.value.substring(9)}` + : `/items/${collection.value}`; }); const items = ref([]); diff --git a/src/displays/template/template.vue b/src/displays/template/template.vue index f4f3aff9fd..3cef6152a8 100644 --- a/src/displays/template/template.vue +++ b/src/displays/template/template.vue @@ -21,7 +21,6 @@ diff --git a/src/modules/settings/routes/roles/browse/browse.vue b/src/modules/settings/routes/roles/browse/browse.vue index 5e737db951..382a4caba1 100644 --- a/src/modules/settings/routes/roles/browse/browse.vue +++ b/src/modules/settings/routes/roles/browse/browse.vue @@ -62,7 +62,7 @@ diff --git a/src/modules/users/composables/use-navigation.ts b/src/modules/users/composables/use-navigation.ts index 1b54ef0bab..958403a0a4 100644 --- a/src/modules/users/composables/use-navigation.ts +++ b/src/modules/users/composables/use-navigation.ts @@ -1,5 +1,5 @@ import { ref, Ref } from '@vue/composition-api'; -import useProjectsStore from '@/stores/projects'; + import api from '@/api'; import { Role } from '@/stores/user/types'; @@ -24,10 +24,8 @@ export default function useNavigation() { async function fetchRoles() { if (!loading || !roles) return; loading.value = true; - const projectsStore = useProjectsStore(); - const currentProjectKey = projectsStore.state.currentProjectKey; - const rolesResponse = await api.get(`/${currentProjectKey}/roles`); + const rolesResponse = await api.get(`/roles`); roles.value = rolesResponse.data.data; loading.value = false; } diff --git a/src/modules/users/routes/browse/browse.vue b/src/modules/users/routes/browse/browse.vue index 74de92c82f..8d033f3c76 100644 --- a/src/modules/users/routes/browse/browse.vue +++ b/src/modules/users/routes/browse/browse.vue @@ -77,7 +77,7 @@ diff --git a/src/routes/project-chooser/index.ts b/src/routes/project-chooser/index.ts deleted file mode 100644 index 35bce92552..0000000000 --- a/src/routes/project-chooser/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -import ProjectChooserRoute from './project-chooser.vue'; - -export { ProjectChooserRoute }; -export default ProjectChooserRoute; diff --git a/src/routes/project-chooser/project-chooser.vue b/src/routes/project-chooser/project-chooser.vue deleted file mode 100644 index ffe15569ed..0000000000 --- a/src/routes/project-chooser/project-chooser.vue +++ /dev/null @@ -1,96 +0,0 @@ - - - - - diff --git a/src/routes/reset-password/request.vue b/src/routes/reset-password/request.vue index 0e09f6fe61..d2c3bfea89 100644 --- a/src/routes/reset-password/request.vue +++ b/src/routes/reset-password/request.vue @@ -14,15 +14,12 @@ @@ -86,68 +24,21 @@ export default defineComponent({ diff --git a/src/views/private/components/revisions-drawer-detail/revisions-drawer-detail.vue b/src/views/private/components/revisions-drawer-detail/revisions-drawer-detail.vue index b5b4658622..87fd0d32ac 100644 --- a/src/views/private/components/revisions-drawer-detail/revisions-drawer-detail.vue +++ b/src/views/private/components/revisions-drawer-detail/revisions-drawer-detail.vue @@ -39,7 +39,7 @@ - - diff --git a/src/views/public/components/project-chooser/readme.md b/src/views/public/components/project-chooser/readme.md deleted file mode 100644 index 772f6ca851..0000000000 --- a/src/views/public/components/project-chooser/readme.md +++ /dev/null @@ -1,35 +0,0 @@ -# Project Chooser - -Renders the project's logo, and allows you to switch to other projects. If there's only one project -available, it renders just the logo. - -## Usage - -```html - - - -``` - -## Props -n/a - -## Events -n/a - -## Slots -n/a - -## CSS Variables -n/a diff --git a/src/views/public/components/project-chooser/logo-dark.svg b/src/views/public/logo-dark.svg similarity index 100% rename from src/views/public/components/project-chooser/logo-dark.svg rename to src/views/public/logo-dark.svg diff --git a/src/views/public/public-view.test.ts b/src/views/public/public-view.test.ts deleted file mode 100644 index 9a439f2be9..0000000000 --- a/src/views/public/public-view.test.ts +++ /dev/null @@ -1,134 +0,0 @@ -import Vue from 'vue'; -import VueCompositionAPI from '@vue/composition-api'; -import { mount, createLocalVue, Wrapper } from '@vue/test-utils'; -import VIcon from '@/components/v-icon/'; -import { useProjectsStore } from '@/stores/projects/'; -import { ProjectWithKey } from '@/stores/projects/types'; -import ClickOutside from '@/directives/click-outside'; -import VMenu from '@/components/v-menu'; -import VList, { VListItem, VListItemIcon, VListItemContent } from '@/components/v-list'; -import PortalVue from 'portal-vue'; - -const localVue = createLocalVue(); -localVue.use(VueCompositionAPI); -localVue.component('v-icon', VIcon); -localVue.component('v-list', VList); -localVue.component('v-list-item', VListItem); -localVue.component('v-list-item-icon', VListItemIcon); -localVue.component('v-list-item-content', VListItemContent); -localVue.component('v-menu', VMenu); -localVue.directive('click-outside', ClickOutside); -localVue.use(PortalVue); - -import PublicView from './public-view.vue'; - -const mockProject: ProjectWithKey = { - key: 'my-project', - api: { - version: '8.5.5', - requires2FA: false, - database: 'mysql', - project_name: 'Thumper', - project_logo: { - full_url: 'http://localhost:8080/uploads/my-project/originals/19acff06-4969-5c75-9cd5-dc3f27506de2.svg', - url: '/uploads/my-project/originals/19acff06-4969-5c75-9cd5-dc3f27506de2.svg', - asset_url: '/uploads/my-project/assets/abc', - }, - project_color: '#4CAF50', - project_foreground: { - full_url: 'http://localhost:8080/uploads/my-project/originals/f28c49b0-2b4f-571e-bf62-593107cbf2ec.svg', - url: '/uploads/my-project/originals/f28c49b0-2b4f-571e-bf62-593107cbf2ec.svg', - asset_url: '/uploads/my-project/assets/abc', - }, - project_background: { - full_url: 'http://localhost:8080/uploads/my-project/originals/03a06753-6794-4b9a-803b-3e1cd15e0742.jpg', - url: '/uploads/my-project/originals/03a06753-6794-4b9a-803b-3e1cd15e0742.jpg', - asset_url: '/uploads/my-project/assets/abc', - }, - telemetry: true, - 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, - general: { - php_version: '7.2.22-1+0~20190902.26+debian9~1.gbpd64eb7', - php_api: 'fpm-fcgi', - }, - }, - authenticated: true, -}; - -describe('Views / Public', () => { - const store = useProjectsStore({}); - let component: Wrapper; - - beforeEach(() => { - store.reset(); - component = mount(PublicView, { localVue }); - }); - - describe('Background', () => { - it('Defaults background art to color when current project key is unknown', () => { - expect((component.vm as any).artStyles).toEqual({ - background: '#263238', - backgroundPosition: 'center center', - backgroundSize: 'cover', - }); - }); - - it('Uses the project color when the current project key is set, but background image is not', async () => { - store.state.projects = [ - { - ...mockProject, - api: { - ...mockProject.api, - project_background: null, - }, - }, - ] as any; - store.state.currentProjectKey = 'my-project'; - - await component.vm.$nextTick(); - - expect((component.vm as any).artStyles).toEqual({ - background: '#4CAF50', - backgroundPosition: 'center center', - backgroundSize: 'cover', - }); - }); - - it('Uses the default background color when the current project has an error', () => { - store.state.projects = [ - { - key: 'my-project', - status: 500, - error: { - code: 250, - message: 'Test error', - }, - authenticated: false, - }, - ]; - store.state.currentProjectKey = 'my-project'; - - expect((component.vm as any).artStyles).toEqual({ - background: '#263238', - backgroundPosition: 'center center', - backgroundSize: 'cover', - }); - }); - - it('Uses the background image when the project has one set', () => { - store.state.projects = [mockProject]; - store.state.currentProjectKey = 'my-project'; - expect((component.vm as any).artStyles).toEqual({ - background: `url(${mockProject.api?.project_background?.asset_url})`, - backgroundPosition: 'center center', - backgroundSize: 'cover', - }); - }); - }); -}); diff --git a/src/views/public/public-view.vue b/src/views/public/public-view.vue index d717d04488..bccc5f5fc7 100644 --- a/src/views/public/public-view.vue +++ b/src/views/public/public-view.vue @@ -1,7 +1,14 @@ @@ -26,14 +33,10 @@ @@ -148,6 +143,38 @@ export default defineComponent({ .notice { color: #b0bec5; } + + .title-box { + display: flex; + align-items: center; + width: max-content; + height: 64px; + cursor: pointer; + } + + .logo { + display: flex; + align-items: center; + justify-content: center; + width: 64px; + height: 64px; + background-color: var(--brand); + border-radius: var(--border-radius); + } + + .default-logo { + width: 64px; + } + + .title { + margin-left: 12px; + } + + .v-icon { + --v-icon-color: var(--foreground-subdued); + + margin-left: 4px; + } } .scale-enter-active,