Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions main.nginx.conf
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,11 @@ http {
proxy_set_header X-Forwarded-Proto https;
}

location = /client-config.json {
include ./common-headers.nginx.conf;
return 200 "{}";
}

location / {
root ./dist;

Expand Down
3 changes: 2 additions & 1 deletion src/components/app.vue
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,8 @@ export default {
return this.$route !== START_LOCATION;
},
showsFeedbackButton() {
return this.config.showsFeedbackButton && this.visiblyLoggedIn;
return this.config.loaded && this.config.showsFeedbackButton &&
this.visiblyLoggedIn;
},
},
created() {
Expand Down
62 changes: 62 additions & 0 deletions src/components/config-error.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
<!--
Copyright 2024 ODK Central Developers
See the NOTICE file at the top-level directory of this distribution and at
https://github.com/getodk/central-frontend/blob/master/NOTICE.

This file is part of ODK Central. It is subject to the license terms in
the LICENSE file found in the top-level directory of this distribution and at
https://www.apache.org/licenses/LICENSE-2.0. No part of ODK Central,
including this file, may be copied, modified, propagated, or distributed
except according to the terms contained in the LICENSE file.
-->
<template>
<div id="config-error" class="row">
<div class="col-xs-12 col-sm-offset-3 col-sm-6">
<div class="panel panel-default panel-main">
<div class="panel-heading">
<h1 class="panel-title">{{ $t('title') }}</h1>
</div>
<div class="panel-body">
<p>
<span>{{ $t('body') }}</span>
<sentence-separator/>
<span>{{ loadError }}</span>
</p>
</div>
</div>
</div>
</div>
</template>

<script setup>
import { F } from 'ramda';
import { computed, inject } from 'vue';
import { onBeforeRouteLeave } from 'vue-router';

import SentenceSeparator from './sentence-separator.vue';

import { requestAlertMessage } from '../util/request';

defineOptions({
name: 'ConfigError'
});

// Since there was an error loading the config, we don't know how to render
// important parts of the app. For example, we don't know config.oidcEnabled, so
// we don't know how to render the login page. Here, we prevent the user from
// navigating to elsewhere in the app.
onBeforeRouteLeave(F);

const { i18n, config } = inject('container');
const loadError = computed(() => requestAlertMessage(i18n, config.loadError));
</script>

<i18n lang="json5">
{
"en": {
// This is the title at the top of a panel.
"title": "Error Loading Central",
"body": "There was an error loading Central."
}
}
</i18n>
6 changes: 3 additions & 3 deletions src/components/navbar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,8 @@ except according to the terms contained in the LICENSE file.
</div>
</div>
</nav>
<analytics-introduction v-if="config.showsAnalytics" v-bind="analyticsIntroduction"
@hide="analyticsIntroduction.hide()"/>
<analytics-introduction v-if="config.loaded && config.showsAnalytics"
v-bind="analyticsIntroduction" @hide="analyticsIntroduction.hide()"/>
</div>
</template>

Expand Down Expand Up @@ -82,7 +82,7 @@ export default {
},
computed: {
showsAnalyticsNotice() {
return this.config.showsAnalytics && this.visiblyLoggedIn &&
return this.config.loaded && this.config.showsAnalytics && this.visiblyLoggedIn &&
this.canRoute('/system/analytics') && this.analyticsConfig.dataExists &&
this.analyticsConfig.isEmpty() &&
Date.now() - Date.parse(this.currentUser.createdAt) >= /* 14 days */ 1209600000;
Expand Down
5 changes: 5 additions & 0 deletions src/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,18 @@ https://www.apache.org/licenses/LICENSE-2.0. No part of ODK Central,
including this file, may be copied, modified, propagated, or distributed
except according to the terms contained in the LICENSE file.
*/

// These are the default config values. They will be merged with the response
// for /client-config.json.
export default {
// `true` to allow navigation to /system/analytics and `false` not to.
showsAnalytics: true,
home: {
title: null,
body: null
},
// VUE_APP_OIDC_ENABLED is not set in production. It can be set during local
// development to facilitate work on SSO.
oidcEnabled: process.env.VUE_APP_OIDC_ENABLED === 'true',
showsFeedbackButton: false
};
4 changes: 1 addition & 3 deletions src/container.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ import createAlert from './alert';
import createCentralI18n from './i18n';
import createCentralRouter from './router';
import createUnsavedChanges from './unsaved-changes';
import defaultConfig from './config';
import { $tcn } from './util/i18n';
import { createRequestData } from './request-data';
import { noop } from './util/util';
Expand All @@ -43,7 +42,6 @@ export default ({
requestData = createRequestData,
alert = createAlert(),
unsavedChanges = createUnsavedChanges(i18n.global),
config = defaultConfig,
http = axios,
// Adding `logger` in part in order to silence certain logging during testing.
logger = console
Expand All @@ -53,11 +51,11 @@ export default ({
i18n: i18n.global,
alert,
unsavedChanges,
config,
http,
logger
};
container.requestData = requestData(container);
container.config = container.requestData.config;
if (router != null) container.router = router(container);
container.install = (app) => {
// Register <i18n-t>, since we specify `false` for the fullInstall option of
Expand Down
12 changes: 11 additions & 1 deletion src/request-data/resources.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@ https://www.apache.org/licenses/LICENSE-2.0. No part of ODK Central,
including this file, may be copied, modified, propagated, or distributed
except according to the terms contained in the LICENSE file.
*/
import { reactive, shallowReactive, watchSyncEffect } from 'vue';
import { computed, reactive, shallowReactive, watchSyncEffect } from 'vue';
import { mergeDeepLeft } from 'ramda';

import configDefaults from '../config';
import { computeIfExists, hasVerbs, setupOption, transformForm } from './util';
import { noargs } from '../util/util';

Expand All @@ -28,6 +30,14 @@ export default ({ i18n }, createResource) => {
}));

// Resources related to the system
createResource('config', (config) => ({
// If client-config.json is completely invalid JSON, `data` seems to be a
// string (e.g., '{]').
transformResponse: ({ data }) => (typeof data === 'object' && data != null
? mergeDeepLeft(data, configDefaults)
: configDefaults),
loaded: computed(() => config.dataExists && config.loadError == null)
}));
createResource('centralVersion');
createResource('analyticsConfig', noargs(setupOption));
createResource('roles', (roles) => ({
Expand Down
75 changes: 46 additions & 29 deletions src/router.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { START_LOCATION, createRouter, createWebHashHistory } from 'vue-router';
import { watchEffect } from 'vue';

import createRoutes from './routes';
import { canRoute, forceReplace, preservedData, unlessFailure } from './util/router';
import { beforeNextNavigation, canRoute, forceReplace, preservedData, unlessFailure } from './util/router';
import { createScrollBehavior } from './scroll-behavior';
import { loadAsync } from './util/load-async';
import { loadLocale, userLocale } from './util/i18n';
Expand All @@ -29,6 +29,7 @@ export default (container, {
const routes = createRoutes(container);
const router = createRouter({ history, routes, scrollBehavior });
const { requestData, alert, unsavedChanges, config } = container;
const { session } = requestData;



Expand Down Expand Up @@ -62,35 +63,51 @@ router.afterEach(unlessFailure(to => {
// During the initial navigation, the router sends requests for essential data
// that is needed to render the app.

const { session } = requestData;
{
const requests = [
async () => {
const locale = userLocale();
if (locale != null) await loadLocale(container, locale);
},

// Implements the restoreSession meta field. A test can skip this request
// by setting the session before the initial navigation.
async (to) => {
if (to.meta.restoreSession && !session.dataExists) {
await restoreSession(session);
// If this is the first time that the session has been restored since
// the most recent OIDC login, set sessionExpires in local storage. If
// sessionExpires is already set (for example, if the previous session
// expired), then it will be overwritten.
const newSession = config.oidcEnabled &&
Date.parse(session.expiresAt).toString() !== localStore.getItem('sessionExpires');
await logIn(container, newSession);
}
}
];
beforeNextNavigation(router, async (to) => {
// A test can skip this request by setting the session before the initial
// navigation.
const needsLogin = to.meta.restoreSession && !session.dataExists;
const sessionPromise = needsLogin
? restoreSession(session)
: Promise.resolve();

// A test can skip this request by setting `config` before the initial
// navigation.
const configPromise = config.dataExists
? Promise.resolve()
: config.request({ url: '/client-config.json', alert: false })
Comment thread
matthew-white marked this conversation as resolved.
.catch(error => {
config.data = { loadError: error };
/* If the request for the session is still in progress, it will be
canceled. We're about to redirect the user to /load-error, where we
won't need session data. If the request is already complete, we clear
`session`. If we didn't, then Frontend would attempt to log out before
the session expired. Note that without `config`, it's not possible to
complete login below. */
session.reset();
});

const locale = userLocale();
const localePromise = locale != null
? loadLocale(container, locale)
: Promise.resolve();

// Once the session and the config have been received, we can complete
// login.
await Promise.allSettled([sessionPromise, configPromise]);
if (needsLogin && session.dataExists) {
// If this is the first time that the session has been restored since the
// most recent OIDC login, set sessionExpires in local storage. If
// sessionExpires is already set (for example, if the previous session
// expired), then it will be overwritten.
const newSession = config.oidcEnabled &&
Date.parse(session.expiresAt).toString() !== localStore.getItem('sessionExpires');
await logIn(container, newSession).catch(noop);
}

const removeGuard = router.beforeEach(async (to) => {
await Promise.allSettled(requests.map(request => request(to)));
removeGuard();
});
}
await localePromise.catch(noop);
return config.loadError != null ? '/load-error' : true;
});



Expand Down
Loading