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 }) => {