Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add "Route Not Found" and "Require Authentication" views. #318

Merged
merged 6 commits into from
Mar 15, 2024
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
27 changes: 12 additions & 15 deletions frontend/src/App.vue
Original file line number Diff line number Diff line change
@@ -1,30 +1,25 @@
<template>
<!-- authenticated subscriber content -->
<template v-if="isAuthenticated">
<template v-if="router.hasRoute(route.name) && (isAuthenticated || routeIsPublic)">
<site-notification
v-if="visibleNotification"
v-if="isAuthenticated && visibleNotification"
:title="notificationTitle"
:action-url="notificationActionUrl"
>
{{ notificationMessage }}
</site-notification>
<nav-bar :nav-items="navItems" />
<main :class="{'mx-4 min-h-full py-24 lg:mx-8': !routeIsHome, 'pt-32': routeIsHome}">
<router-view />
<nav-bar v-if="isAuthenticated" :nav-items="navItems"/>
<title-bar v-if="routeIsPublic"/>
<main :class="{'mx-4 min-h-full py-24 lg:mx-8': !routeIsHome && !routeIsPublic, 'pt-32': routeIsHome, 'min-h-full': routeIsPublic}">
<router-view/>
</main>
<footer-bar />
<footer-bar/>
</template>
<!-- for home page and booking page -->
<template v-else-if="routeIsPublic">
<title-bar />
<main class="min-h-full">
<router-view />
</main>
<footer-bar />
<template v-else-if="router.hasRoute(route.name) && !routeIsPublic">
<not-authenticated-view/>
</template>
<template v-else>
<!-- TODO: handle wrong route -->
An authentication or routing error occurred.
<route-not-found-view/>
</template>
</template>

Expand All @@ -43,6 +38,8 @@ import { storeToRefs } from 'pinia';
import { useUserStore } from '@/stores/user-store';
import { useCalendarStore } from '@/stores/calendar-store';
import { useAppointmentStore } from '@/stores/appointment-store';
import RouteNotFoundView from '@/views/errors/RouteNotFoundView.vue';
import NotAuthenticatedView from '@/views/errors/NotAuthenticatedView.vue';

// component constants
const currentUser = useUserStore(); // data: { username, email, name, level, timezone, id }
Expand Down
6 changes: 6 additions & 0 deletions frontend/src/definitions.js
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,12 @@ export const qalendarSlotDurations = {
60: 60,
};

/**
* Used as the session storage key for the location the user wanted to go to before logging in.
* @type {string}
*/
export const loginRedirectKey = 'loginRedirect';

export default {
subscriberLevels,
appointmentState,
Expand Down
3 changes: 3 additions & 0 deletions frontend/src/locales/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,14 @@
},
"error": {
"actionNeeded": "Aktion erforderlich!",
"authenticationRequired": "Sorry, um diese Seite zu sehen ist eine Anmeldung erforderlich.",
"credentialsIncomplete": "Bitte gib deine Zugangsdaten ein.",
"googleRefreshError": "Es gab ein Problem mit Google, bitte erneut verbinden.",
"loginMethodNotSupported": "Login-Methode wird nicht unterstützt. Bitte nochmal versuchen.",
"minimumValue": "{field} sollte wenigstens {value} sein.",
"noConnectedCalendars": "Kalendereinrichtung zum Fortfahren erforderlich. {0}.",
"noConnectedCalendarsLink": "Zu den Einstellungen wechseln.",
"routeNotFound": "Sorry, diese Seite wurde nicht gefunden.",
"unknownAppointmentError": "Es gab ein Problem beim Erstellen des Termins. Bitte nochmal versuchen.",
"unknownScheduleError": "Es gab ein Problem beim Erstellen des Zeitplans.",
"usernameIsNotAvailable": "Dieser Benutzername ist nicht verfügbar."
Expand Down Expand Up @@ -143,6 +145,7 @@
"general": "Allgemein",
"generalDetails": "Allgemeine Infos",
"generateZoomLink": "Zoom Meeting generieren",
"goBack": "Zurück",
"google": "Google",
"guest": "{count} Gäste | {count} Gast | {count} Gäste",
"inPerson": "Vor Ort",
Expand Down
3 changes: 3 additions & 0 deletions frontend/src/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,14 @@
},
"error": {
"actionNeeded": "Action needed",
"authenticationRequired": "Sorry, this page requires you to be logged in.",
"credentialsIncomplete": "Please provide login credentials.",
"googleRefreshError": "Error connecting with Google API, please re-connect.",
"loginMethodNotSupported": "Login method not supported. Please try again.",
"minimumValue": "{field} should be at least {value}.",
"noConnectedCalendars": "Calendar setup required. Connect a calendar to continue. {0}.",
"noConnectedCalendarsLink": "Go to settings",
"routeNotFound": "Sorry, this page could not be found.",
"unknownAppointmentError": "There was an issue creating your booking. Please try again.",
"unknownScheduleError": "There was an issue creating your schedule. Please try again.",
"usernameIsNotAvailable": "Username not available. Please choose another."
Expand Down Expand Up @@ -143,6 +145,7 @@
"general": "General",
"generalDetails": "Details",
"generateZoomLink": "Generate Zoom Meeting",
"goBack": "Go Back",
"google": "Google",
"guest": "{count} guests | {count} guest | {count} guests",
"inPerson": "In person",
Expand Down
20 changes: 4 additions & 16 deletions frontend/src/router.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,11 @@ import ScheduleView from '@/views/ScheduleView.vue';
import HomeView from '@/views/HomeView.vue';
import LoginView from '@/views/LoginView.vue';
import PostLoginView from '@/views/PostLoginView.vue';
import { useUserStore } from '@/stores/user-store';

const authGuard = (to, from) => {
const user = useUserStore();

// If we're not logged in, drop them to login
if (!user?.exists()) {
return '/login';
}
};

/**
* Defined routes for Thunderbird Appointment
* Note: All routes require authentication unless otherwise specified in App.vue::routeIsPublic
*/
const routes = [
// instant loaded routes
{
Expand Down Expand Up @@ -53,7 +47,6 @@ const routes = [
path: '/schedule',
name: 'schedule',
component: ScheduleView,
beforeEnter: authGuard,
},
{
path: '/calendar',
Expand All @@ -63,32 +56,27 @@ const routes = [
path: '/calendar/:view?/:date?',
name: 'calendar',
component: CalendarView,
beforeEnter: authGuard,
},
// lazy-loaded routes
{
path: '/appointments/:view?',
name: 'appointments',
component: () => import('@/views/AppointmentsView'),
beforeEnter: authGuard,
},
{
path: '/settings/:view?',
name: 'settings',
component: () => import('@/views/SettingsView'),
beforeEnter: authGuard,
},
{
path: '/profile',
name: 'profile',
component: () => import('@/views/ProfileView'),
beforeEnter: authGuard,
},
{
path: '/contact',
name: 'contact',
component: () => import('@/views/ContactView'),
beforeEnter: authGuard,
},
{
path: '/privacy',
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/stores/user-store.js
Original file line number Diff line number Diff line change
Expand Up @@ -87,8 +87,8 @@ export const useUserStore = defineStore('user', () => {
* Request subscriber login
* @param {function} fetch preconfigured API fetch function
* @param {string} username
* @param {string} password
* @returns {boolean} true if login was successful
* @param {string|null} password or null if fxa authentication
* @returns {Promise<boolean>} true if login was successful
*/
const login = async (fetch, username, password) => {
$reset();
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/views/LoginView.vue
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<template>
<!-- page title area -->
<div class="flex-center flex h-screen w-full bg-gray-100 dark:bg-gray-600">
<div class="my-auto flex w-1/2 max-w-lg flex-col items-center justify-center gap-2 bg-white px-4 py-12 shadow-lg dark:bg-gray-700">
<div class="my-auto flex w-full flex-col items-center justify-center gap-2 bg-white px-4 py-12 shadow-lg dark:bg-gray-700 md:w-1/2 md:max-w-lg">
<img class="mb-2 w-full max-w-[8rem]" src="/appointment_logo.svg" alt="Appointment Logo" />
<div class="text-center text-4xl font-light">{{ t('app.title') }}</div>
<alert-box v-if="loginError" @close="loginError = null" class="mt-4">
Expand Down
9 changes: 7 additions & 2 deletions frontend/src/views/PostLoginView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import { inject, computed, onMounted } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { useUserStore } from '@/stores/user-store';
import { loginRedirectKey } from '@/definitions';

const route = useRoute();
const router = useRouter();
Expand All @@ -21,12 +22,16 @@ const call = inject('call');
const isFxaAuth = computed(() => import.meta.env?.VITE_AUTH_SCHEME === 'fxa');

onMounted(async () => {
// Retrieve and remove temp login redirect location
const redirectTo = window.sessionStorage?.getItem(loginRedirectKey);
window.sessionStorage?.removeItem(loginRedirectKey);
devmount marked this conversation as resolved.
Show resolved Hide resolved

if (!isFxaAuth.value) {
await router.push('/');
await router.push(redirectTo ?? '/');
return;
}

await user.login(call, route.params.token, null);
await router.push('/calendar');
await router.push(redirectTo ?? '/calendar');
});
</script>
32 changes: 32 additions & 0 deletions frontend/src/views/errors/NotAuthenticatedView.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<template>
<div class="flex-center h-full flex-col gap-12 p-4">
<div class="flex-center flex-col gap-8 px-4">
<art-invalid-link class="my-6 h-auto max-w-sm"/>
<div class="text-xl font-semibold text-sky-600">
{{ t('error.authenticationRequired') }}
</div>
<primary-button @click="goToLogin">
{{ t('label.logIn') }}
</primary-button>
</div>
</div>

</template>
<script setup>
import ArtInvalidLink from '@/elements/arts/ArtInvalidLink.vue';
import { useI18n } from 'vue-i18n';
import PrimaryButton from '@/elements/PrimaryButton.vue';
import { useRoute, useRouter } from 'vue-router';
import { loginRedirectKey } from '@/definitions';

const { t } = useI18n();
const route = useRoute();
const router = useRouter();

const goToLogin = () => {
// Save their intended destination!
window.sessionStorage?.setItem(loginRedirectKey, route.fullPath);
router.push('/login');
};

</script>
22 changes: 22 additions & 0 deletions frontend/src/views/errors/RouteNotFoundView.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<template>
<div class="flex-center h-full flex-col gap-12 p-4">
<div class="flex-center flex-col gap-8 px-4">
<art-invalid-link class="my-6 h-auto max-w-sm"/>
<div class="text-xl font-semibold text-sky-600">
{{ t('error.routeNotFound') }}
</div>
<primary-button @click="$router.go(-1)">
{{ t('label.goBack') }}
</primary-button>
</div>
</div>

</template>
<script setup>
import ArtInvalidLink from '@/elements/arts/ArtInvalidLink.vue';
import { useI18n } from 'vue-i18n';
import PrimaryButton from '@/elements/PrimaryButton.vue';

const { t } = useI18n();

</script>