From ceeb966edd98f5540ed6663dfa94359f019b718b Mon Sep 17 00:00:00 2001 From: Chocobozzz <me@florianbigard.com> Date: Mon, 1 Apr 2019 11:49:54 +0200 Subject: [PATCH] Add error page and login error redirection --- js/src/App.vue | 1 + js/src/apollo/user.ts | 4 ++- js/src/components/NavBar.vue | 46 ++++++++++++++++++------ js/src/graphql/user.ts | 7 ++-- js/src/router/actor.ts | 3 +- js/src/router/error.ts | 16 +++++++++ js/src/router/event.ts | 3 +- js/src/router/guards/auth-guard.ts | 21 +++++++++++ js/src/router/guards/register-guard.ts | 23 ++++++++++++ js/src/router/index.ts | 6 ++++ js/src/router/user.ts | 5 ++- js/src/types/current-user.model.ts | 1 + js/src/types/error-code.model.ts | 4 +++ js/src/types/login-error-code.model.ts | 3 ++ js/src/views/Error.vue | 25 +++++++++++++ js/src/views/User/Login.vue | 49 +++++++++++++++++++------- 16 files changed, 187 insertions(+), 30 deletions(-) create mode 100644 js/src/router/error.ts create mode 100644 js/src/router/guards/auth-guard.ts create mode 100644 js/src/router/guards/register-guard.ts create mode 100644 js/src/types/error-code.model.ts create mode 100644 js/src/types/login-error-code.model.ts create mode 100644 js/src/views/Error.vue diff --git a/js/src/App.vue b/js/src/App.vue index 11f6b01d0..5c2513aa8 100644 --- a/js/src/App.vue +++ b/js/src/App.vue @@ -98,6 +98,7 @@ export default class App extends Vue { variables: { id: userId, email: userEmail, + isLoggedIn: true, }, }); } diff --git a/js/src/apollo/user.ts b/js/src/apollo/user.ts index 0aea70453..fdf1804bd 100644 --- a/js/src/apollo/user.ts +++ b/js/src/apollo/user.ts @@ -4,16 +4,18 @@ export const currentUser = { __typename: 'CurrentUser', id: null, email: null, + isLoggedIn: false, }, }, resolvers: { Mutation: { - updateCurrentUser: (_, { id, email }, { cache }) => { + updateCurrentUser: (_, { id, email, isLoggedIn }, { cache }) => { const data = { currentUser: { id, email, + isLoggedIn, __typename: 'CurrentUser', }, }; diff --git a/js/src/components/NavBar.vue b/js/src/components/NavBar.vue index 855ea2eea..967995037 100644 --- a/js/src/components/NavBar.vue +++ b/js/src/components/NavBar.vue @@ -18,17 +18,17 @@ <div class="navbar-end"> <div class="navbar-item"> <div class="buttons"> - <router-link class="button is-primary" v-if="!currentUser.id && config && config.registrationsOpen" :to="{ name: 'Register' }"> + <router-link class="button is-primary" v-if="!currentUser.isLoggedIn && config && config.registrationsOpen" :to="{ name: 'Register' }"> <strong> <translate>Sign up</translate> </strong> </router-link> - <router-link class="button is-light" v-if="!currentUser.id" :to="{ name: 'Login' }"> + <router-link class="button is-light" v-if="!currentUser.isLoggedIn" :to="{ name: 'Login' }"> <translate>Log in</translate> </router-link> <router-link class="button is-light" - v-if="currentUser.id && loggedPerson" + v-if="currentUser.isLoggedIn && loggedPerson" :to="{ name: 'Profile', params: { name: loggedPerson.preferredUsername} }" > <figure class="image is-24x24"> @@ -36,6 +36,8 @@ </figure> <span>{{ loggedPerson.preferredUsername }}</span> </router-link> + + <span v-if="currentUser.isLoggedIn" class="button" v-on:click="logout()">Log out</span> </div> </div> </div> @@ -45,7 +47,7 @@ <script lang="ts"> import { Component, Vue, Watch } from 'vue-property-decorator'; import { SEARCH } from '@/graphql/search'; -import { CURRENT_USER_CLIENT } from '@/graphql/user'; +import { CURRENT_USER_CLIENT, UPDATE_CURRENT_USER_CLIENT } from '@/graphql/user'; import { onLogout } from '@/vue-apollo'; import { deleteUserData } from '@/utils/auth'; import { LOGGED_PERSON } from '@/graphql/actor'; @@ -53,6 +55,7 @@ import { IActor, IPerson } from '@/types/actor.model'; import { RouteName } from '@/router'; import { CONFIG } from '@/graphql/config'; import { IConfig } from '@/types/config.model'; +import { ICurrentUser } from '@/types/current-user.model' @Component({ apollo: { @@ -70,9 +73,6 @@ import { IConfig } from '@/types/config.model'; currentUser: { query: CURRENT_USER_CLIENT, }, - loggedPerson: { - query: LOGGED_PERSON, - }, config: { query: CONFIG, } @@ -87,8 +87,9 @@ export default class NavBar extends Vue { search: any[] = []; searchText: string | null = null; searchSelect = null; - loggedPerson!: IPerson; + loggedPerson: IPerson | null = null; config!: IConfig; + currentUser!: ICurrentUser; get items() { return this.search.map(searchEntry => { @@ -106,6 +107,20 @@ export default class NavBar extends Vue { }); } + @Watch('currentUser') + async onCurrentUserChanged() { + // Refresh logged person object + if (this.currentUser.isLoggedIn) { + const result = await this.$apollo.query({ + query: LOGGED_PERSON, + }); + + this.loggedPerson = result.data.loggedPerson; + } else { + this.loggedPerson = null; + } + } + @Watch('model') onModelChanged(val) { switch (val.__typename) { @@ -134,12 +149,21 @@ export default class NavBar extends Vue { this.$apollo.queries['search'].refetch(); } - logout() { - alert('logout !'); + async logout() { + await this.$apollo.mutate({ + mutation: UPDATE_CURRENT_USER_CLIENT, + variables: { + id: null, + email: null, + isLoggedIn: false, + }, + }); deleteUserData(); - return onLogout(this.$apollo); + onLogout(this.$apollo) + + return this.$router.push({ path: '/' }) } } </script> diff --git a/js/src/graphql/user.ts b/js/src/graphql/user.ts index 07a16aba0..06c300404 100644 --- a/js/src/graphql/user.ts +++ b/js/src/graphql/user.ts @@ -28,13 +28,14 @@ export const CURRENT_USER_CLIENT = gql` query { currentUser @client { id, - email + email, + isLoggedIn, } } `; export const UPDATE_CURRENT_USER_CLIENT = gql` -mutation UpdateCurrentUser($id: Int!, $email: String!) { - updateCurrentUser(id: $id, email: $email) @client +mutation UpdateCurrentUser($id: Int!, $email: String!, $isLoggedIn: Boolean!) { + updateCurrentUser(id: $id, email: $email, isLoggedIn: $isLoggedIn) @client } `; diff --git a/js/src/router/actor.ts b/js/src/router/actor.ts index 56fc2d1d6..e984d2bc1 100644 --- a/js/src/router/actor.ts +++ b/js/src/router/actor.ts @@ -3,6 +3,7 @@ import CreateGroup from '@/views/Group/Create.vue'; import Group from '@/views/Group/Group.vue'; import GroupList from '@/views/Group/GroupList.vue'; import Identities from '@/views/Account/Identities.vue'; +import { RouteConfig } from 'vue-router'; export enum ActorRouteName { IDENTITIES = 'Identities', @@ -12,7 +13,7 @@ export enum ActorRouteName { PROFILE = 'Profile', } -export const actorRoutes = [ +export const actorRoutes: RouteConfig[] = [ { path: '/identities', name: ActorRouteName.IDENTITIES, diff --git a/js/src/router/error.ts b/js/src/router/error.ts new file mode 100644 index 000000000..c811c010e --- /dev/null +++ b/js/src/router/error.ts @@ -0,0 +1,16 @@ +import { beforeRegisterGuard } from '@/router/guards/register-guard'; +import { RouteConfig } from 'vue-router'; +import ErrorPage from '@/views/Error.vue'; + +export enum ErrorRouteName { + ERROR = 'Error', +} + +export const errorRoutes: RouteConfig[] = [ + { + path: '/error', + name: ErrorRouteName.ERROR, + component: ErrorPage, + beforeEnter: beforeRegisterGuard, + }, +]; diff --git a/js/src/router/event.ts b/js/src/router/event.ts index 18562c378..b466e1e7f 100644 --- a/js/src/router/event.ts +++ b/js/src/router/event.ts @@ -2,6 +2,7 @@ import EventList from '@/views/Event/EventList.vue'; import Location from '@/views/Location.vue'; import CreateEvent from '@/views/Event/Create.vue'; import Event from '@/views/Event/Event.vue'; +import { RouteConfig } from 'vue-router'; export enum EventRouteName { EVENT_LIST = 'EventList', @@ -11,7 +12,7 @@ export enum EventRouteName { LOCATION = 'Location', } -export const eventRoutes = [ +export const eventRoutes: RouteConfig[] = [ { path: '/events/list/:location?', name: EventRouteName.EVENT_LIST, diff --git a/js/src/router/guards/auth-guard.ts b/js/src/router/guards/auth-guard.ts new file mode 100644 index 000000000..fb1c0c9ec --- /dev/null +++ b/js/src/router/guards/auth-guard.ts @@ -0,0 +1,21 @@ +import { NavigationGuard } from 'vue-router'; +import { UserRouteName } from '@/router/user'; +import { LoginErrorCode } from '@/types/login-error-code.model'; +import { AUTH_TOKEN } from '@/constants'; + +export const authGuardIfNeeded: NavigationGuard = async function (to, from, next) { + if (to.meta.requiredAuth !== true) return next(); + + // We can't use "currentUser" from apollo here because we may not have loaded the user from the local storage yet + if (!localStorage.getItem(AUTH_TOKEN)) { + return next({ + name: UserRouteName.LOGIN, + query: { + code: LoginErrorCode.NEED_TO_LOGIN, + redirect: to.fullPath, + }, + }); + } + + return next(); +}; diff --git a/js/src/router/guards/register-guard.ts b/js/src/router/guards/register-guard.ts new file mode 100644 index 000000000..eb4dbc0c9 --- /dev/null +++ b/js/src/router/guards/register-guard.ts @@ -0,0 +1,23 @@ +import { apolloProvider } from '@/vue-apollo'; +import { CONFIG } from '@/graphql/config'; +import { IConfig } from '@/types/config.model'; +import { NavigationGuard } from 'vue-router'; +import { ErrorRouteName } from '@/router/error'; +import { ErrorCode } from '@/types/error-code.model'; + +export const beforeRegisterGuard: NavigationGuard = async function (to, from, next) { + const { data } = await apolloProvider.defaultClient.query({ + query: CONFIG, + }); + + const config: IConfig = data.config; + + if (config.registrationsOpen === false) { + return next({ + name: ErrorRouteName.ERROR, + query: { code: ErrorCode.REGISTRATION_CLOSED }, + }); + } + + return next(); +}; diff --git a/js/src/router/index.ts b/js/src/router/index.ts index 0663b53d4..a322e8b9a 100644 --- a/js/src/router/index.ts +++ b/js/src/router/index.ts @@ -5,6 +5,8 @@ import Home from '@/views/Home.vue'; import { UserRouteName, userRoutes } from './user'; import { EventRouteName, eventRoutes } from '@/router/event'; import { ActorRouteName, actorRoutes } from '@/router/actor'; +import { ErrorRouteName, errorRoutes } from '@/router/error'; +import { authGuardIfNeeded } from '@/router/guards/auth-guard'; Vue.use(Router); @@ -20,6 +22,7 @@ export const RouteName = { ...UserRouteName, ...EventRouteName, ...ActorRouteName, + ...ErrorRouteName, }; const router = new Router({ @@ -29,6 +32,7 @@ const router = new Router({ ...userRoutes, ...eventRoutes, ...actorRoutes, + ...errorRoutes, { path: '/', @@ -46,4 +50,6 @@ const router = new Router({ ], }); +router.beforeEach(authGuardIfNeeded); + export default router; diff --git a/js/src/router/user.ts b/js/src/router/user.ts index 3b7a8f329..6d652a341 100644 --- a/js/src/router/user.ts +++ b/js/src/router/user.ts @@ -5,6 +5,8 @@ import Validate from '@/views/User/Validate.vue'; import ResendConfirmation from '@/views/User/ResendConfirmation.vue'; import SendPasswordReset from '@/views/User/SendPasswordReset.vue'; import PasswordReset from '@/views/User/PasswordReset.vue'; +import { beforeRegisterGuard } from '@/router/guards/register-guard'; +import { RouteConfig } from 'vue-router'; export enum UserRouteName { REGISTER = 'Register', @@ -16,13 +18,14 @@ export enum UserRouteName { LOGIN = 'Login', } -export const userRoutes = [ +export const userRoutes: RouteConfig[] = [ { path: '/register/user', name: UserRouteName.REGISTER, component: RegisterUser, props: true, meta: { requiredAuth: false }, + beforeEnter: beforeRegisterGuard, }, { path: '/register/profile', diff --git a/js/src/types/current-user.model.ts b/js/src/types/current-user.model.ts index 94f32b1b1..0c1f6277b 100644 --- a/js/src/types/current-user.model.ts +++ b/js/src/types/current-user.model.ts @@ -1,4 +1,5 @@ export interface ICurrentUser { id: number; email: string; + isLoggedIn: boolean; } diff --git a/js/src/types/error-code.model.ts b/js/src/types/error-code.model.ts new file mode 100644 index 000000000..ba33435f3 --- /dev/null +++ b/js/src/types/error-code.model.ts @@ -0,0 +1,4 @@ +export enum ErrorCode { + UNKNOWN = 'unknown', + REGISTRATION_CLOSED = 'registration_closed', +} diff --git a/js/src/types/login-error-code.model.ts b/js/src/types/login-error-code.model.ts new file mode 100644 index 000000000..781c1e2ec --- /dev/null +++ b/js/src/types/login-error-code.model.ts @@ -0,0 +1,3 @@ +export enum LoginErrorCode { + NEED_TO_LOGIN = 'rouge', +} diff --git a/js/src/views/Error.vue b/js/src/views/Error.vue new file mode 100644 index 000000000..0d5715b17 --- /dev/null +++ b/js/src/views/Error.vue @@ -0,0 +1,25 @@ +<template> + <div v-if="code === ErrorCode.REGISTRATION_CLOSED"> + <translate>Registration is currently closed.</translate> + </div> + + <div v-else> + <translate>Unknown error.</translate> + </div> +</template> + +<script lang="ts"> + import { Component, Vue } from 'vue-property-decorator'; + import { ErrorCode } from '@/types/error-code.model'; + + @Component + export default class ErrorPage extends Vue { + code: ErrorCode | null = null; + + ErrorCode = ErrorCode; + + mounted() { + this.code = this.$route.query[ 'code' ] as ErrorCode; + } + } +</script> diff --git a/js/src/views/User/Login.vue b/js/src/views/User/Login.vue index 196ade974..5e587dec1 100644 --- a/js/src/views/User/Login.vue +++ b/js/src/views/User/Login.vue @@ -5,7 +5,12 @@ <translate>Welcome back!</translate> </h1> </section> - <section> + + <b-message v-if="errorCode === LoginErrorCode.NEED_TO_LOGIN" title="Info" type="is-info"> + <translate>You need to login.</translate> + </b-message> + + <section v-if="!currentUser.isLoggedIn"> <div class="columns is-mobile is-centered"> <div class="column is-half card"> <b-message title="Error" type="is-danger" v-for="error in errors" :key="error">{{ error }}</b-message> @@ -49,6 +54,10 @@ </div> </div> </section> + + <b-message v-else title="Error" type="is-error"> + <translate>You are already logged-in.</translate> + </b-message> </div> </template> @@ -58,16 +67,21 @@ import { LOGIN } from '@/graphql/auth'; import { validateEmailField, validateRequiredField } from '@/utils/validators'; import { saveUserData } from '@/utils/auth'; import { ILogin } from '@/types/login.model'; -import { UPDATE_CURRENT_USER_CLIENT } from '@/graphql/user'; +import { CURRENT_USER_CLIENT, UPDATE_CURRENT_USER_CLIENT } from '@/graphql/user'; import { onLogin } from '@/vue-apollo'; import { RouteName } from '@/router'; -import { IConfig } from '@/types/config.model'; -import { CONFIG } from '@/graphql/config'; +import { LoginErrorCode } from '@/types/login-error-code.model' +import { ICurrentUser } from '@/types/current-user.model' +import { CONFIG } from '@/graphql/config' +import { IConfig } from '@/types/config.model' @Component({ apollo: { config: { query: CONFIG + }, + currentUser: { + query: CURRENT_USER_CLIENT } } }) @@ -75,33 +89,39 @@ export default class Login extends Vue { @Prop({ type: String, required: false, default: '' }) email!: string; @Prop({ type: String, required: false, default: '' }) password!: string; + LoginErrorCode = LoginErrorCode; + + errorCode: LoginErrorCode | null = null; config!: IConfig; + currentUser!: ICurrentUser; + credentials = { email: '', password: '', }; validationSent = false; + errors: string[] = []; rules = { required: validateRequiredField, email: validateEmailField, }; - user: any; - beforeCreate() { - if (this.user) { - this.$router.push('/'); - } - } + private redirect: string | null = null; mounted() { this.credentials.email = this.email; this.credentials.password = this.password; + + let query = this.$route.query; + this.errorCode = query[ 'code' ] as LoginErrorCode; + this.redirect = query[ 'redirect' ] as string; } async loginAction(e: Event) { e.preventDefault(); - this.errors.splice(0); + + this.errors = []; try { const result = await this.$apollo.mutate<{ login: ILogin }>({ @@ -119,12 +139,17 @@ export default class Login extends Vue { variables: { id: result.data.login.user.id, email: this.credentials.email, + isLoggedIn: true, }, }); onLogin(this.$apollo); - this.$router.push({ name: RouteName.HOME }); + if (this.redirect) { + this.$router.push(this.redirect) + } else { + this.$router.push({ name: RouteName.HOME }); + } } catch (err) { console.error(err); err.graphQLErrors.forEach(({ message }) => {