Merge branch 'feature/route-guard' into 'master'

Add error page and login error redirection

See merge request framasoft/mobilizon!106
This commit is contained in:
Thomas Citharel 2019-04-01 14:26:17 +02:00
commit 53458b16a2
16 changed files with 187 additions and 30 deletions

View file

@ -98,6 +98,7 @@ export default class App extends Vue {
variables: { variables: {
id: userId, id: userId,
email: userEmail, email: userEmail,
isLoggedIn: true,
}, },
}); });
} }

View file

@ -4,16 +4,18 @@ export const currentUser = {
__typename: 'CurrentUser', __typename: 'CurrentUser',
id: null, id: null,
email: null, email: null,
isLoggedIn: false,
}, },
}, },
resolvers: { resolvers: {
Mutation: { Mutation: {
updateCurrentUser: (_, { id, email }, { cache }) => { updateCurrentUser: (_, { id, email, isLoggedIn }, { cache }) => {
const data = { const data = {
currentUser: { currentUser: {
id, id,
email, email,
isLoggedIn,
__typename: 'CurrentUser', __typename: 'CurrentUser',
}, },
}; };

View file

@ -18,17 +18,17 @@
<div class="navbar-end"> <div class="navbar-end">
<div class="navbar-item"> <div class="navbar-item">
<div class="buttons"> <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> <strong>
<translate>Sign up</translate> <translate>Sign up</translate>
</strong> </strong>
</router-link> </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> <translate>Log in</translate>
</router-link> </router-link>
<router-link <router-link
class="button is-light" class="button is-light"
v-if="currentUser.id && loggedPerson" v-if="currentUser.isLoggedIn && loggedPerson"
:to="{ name: 'Profile', params: { name: loggedPerson.preferredUsername} }" :to="{ name: 'Profile', params: { name: loggedPerson.preferredUsername} }"
> >
<figure class="image is-24x24"> <figure class="image is-24x24">
@ -36,6 +36,8 @@
</figure> </figure>
<span>{{ loggedPerson.preferredUsername }}</span> <span>{{ loggedPerson.preferredUsername }}</span>
</router-link> </router-link>
<span v-if="currentUser.isLoggedIn" class="button" v-on:click="logout()">Log out</span>
</div> </div>
</div> </div>
</div> </div>
@ -45,7 +47,7 @@
<script lang="ts"> <script lang="ts">
import { Component, Vue, Watch } from 'vue-property-decorator'; import { Component, Vue, Watch } from 'vue-property-decorator';
import { SEARCH } from '@/graphql/search'; 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 { onLogout } from '@/vue-apollo';
import { deleteUserData } from '@/utils/auth'; import { deleteUserData } from '@/utils/auth';
import { LOGGED_PERSON } from '@/graphql/actor'; import { LOGGED_PERSON } from '@/graphql/actor';
@ -53,6 +55,7 @@ import { IActor, IPerson } from '@/types/actor.model';
import { RouteName } from '@/router'; import { RouteName } from '@/router';
import { CONFIG } from '@/graphql/config'; import { CONFIG } from '@/graphql/config';
import { IConfig } from '@/types/config.model'; import { IConfig } from '@/types/config.model';
import { ICurrentUser } from '@/types/current-user.model'
@Component({ @Component({
apollo: { apollo: {
@ -70,9 +73,6 @@ import { IConfig } from '@/types/config.model';
currentUser: { currentUser: {
query: CURRENT_USER_CLIENT, query: CURRENT_USER_CLIENT,
}, },
loggedPerson: {
query: LOGGED_PERSON,
},
config: { config: {
query: CONFIG, query: CONFIG,
} }
@ -87,8 +87,9 @@ export default class NavBar extends Vue {
search: any[] = []; search: any[] = [];
searchText: string | null = null; searchText: string | null = null;
searchSelect = null; searchSelect = null;
loggedPerson!: IPerson; loggedPerson: IPerson | null = null;
config!: IConfig; config!: IConfig;
currentUser!: ICurrentUser;
get items() { get items() {
return this.search.map(searchEntry => { 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') @Watch('model')
onModelChanged(val) { onModelChanged(val) {
switch (val.__typename) { switch (val.__typename) {
@ -134,12 +149,21 @@ export default class NavBar extends Vue {
this.$apollo.queries['search'].refetch(); this.$apollo.queries['search'].refetch();
} }
logout() { async logout() {
alert('logout !'); await this.$apollo.mutate({
mutation: UPDATE_CURRENT_USER_CLIENT,
variables: {
id: null,
email: null,
isLoggedIn: false,
},
});
deleteUserData(); deleteUserData();
return onLogout(this.$apollo); onLogout(this.$apollo)
return this.$router.push({ path: '/' })
} }
} }
</script> </script>

View file

@ -28,13 +28,14 @@ export const CURRENT_USER_CLIENT = gql`
query { query {
currentUser @client { currentUser @client {
id, id,
email email,
isLoggedIn,
} }
} }
`; `;
export const UPDATE_CURRENT_USER_CLIENT = gql` export const UPDATE_CURRENT_USER_CLIENT = gql`
mutation UpdateCurrentUser($id: Int!, $email: String!) { mutation UpdateCurrentUser($id: Int!, $email: String!, $isLoggedIn: Boolean!) {
updateCurrentUser(id: $id, email: $email) @client updateCurrentUser(id: $id, email: $email, isLoggedIn: $isLoggedIn) @client
} }
`; `;

View file

@ -3,6 +3,7 @@ import CreateGroup from '@/views/Group/Create.vue';
import Group from '@/views/Group/Group.vue'; import Group from '@/views/Group/Group.vue';
import GroupList from '@/views/Group/GroupList.vue'; import GroupList from '@/views/Group/GroupList.vue';
import Identities from '@/views/Account/Identities.vue'; import Identities from '@/views/Account/Identities.vue';
import { RouteConfig } from 'vue-router';
export enum ActorRouteName { export enum ActorRouteName {
IDENTITIES = 'Identities', IDENTITIES = 'Identities',
@ -12,7 +13,7 @@ export enum ActorRouteName {
PROFILE = 'Profile', PROFILE = 'Profile',
} }
export const actorRoutes = [ export const actorRoutes: RouteConfig[] = [
{ {
path: '/identities', path: '/identities',
name: ActorRouteName.IDENTITIES, name: ActorRouteName.IDENTITIES,

16
js/src/router/error.ts Normal file
View file

@ -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,
},
];

View file

@ -2,6 +2,7 @@ import EventList from '@/views/Event/EventList.vue';
import Location from '@/views/Location.vue'; import Location from '@/views/Location.vue';
import CreateEvent from '@/views/Event/Create.vue'; import CreateEvent from '@/views/Event/Create.vue';
import Event from '@/views/Event/Event.vue'; import Event from '@/views/Event/Event.vue';
import { RouteConfig } from 'vue-router';
export enum EventRouteName { export enum EventRouteName {
EVENT_LIST = 'EventList', EVENT_LIST = 'EventList',
@ -11,7 +12,7 @@ export enum EventRouteName {
LOCATION = 'Location', LOCATION = 'Location',
} }
export const eventRoutes = [ export const eventRoutes: RouteConfig[] = [
{ {
path: '/events/list/:location?', path: '/events/list/:location?',
name: EventRouteName.EVENT_LIST, name: EventRouteName.EVENT_LIST,

View file

@ -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();
};

View file

@ -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();
};

View file

@ -5,6 +5,8 @@ import Home from '@/views/Home.vue';
import { UserRouteName, userRoutes } from './user'; import { UserRouteName, userRoutes } from './user';
import { EventRouteName, eventRoutes } from '@/router/event'; import { EventRouteName, eventRoutes } from '@/router/event';
import { ActorRouteName, actorRoutes } from '@/router/actor'; import { ActorRouteName, actorRoutes } from '@/router/actor';
import { ErrorRouteName, errorRoutes } from '@/router/error';
import { authGuardIfNeeded } from '@/router/guards/auth-guard';
Vue.use(Router); Vue.use(Router);
@ -20,6 +22,7 @@ export const RouteName = {
...UserRouteName, ...UserRouteName,
...EventRouteName, ...EventRouteName,
...ActorRouteName, ...ActorRouteName,
...ErrorRouteName,
}; };
const router = new Router({ const router = new Router({
@ -29,6 +32,7 @@ const router = new Router({
...userRoutes, ...userRoutes,
...eventRoutes, ...eventRoutes,
...actorRoutes, ...actorRoutes,
...errorRoutes,
{ {
path: '/', path: '/',
@ -46,4 +50,6 @@ const router = new Router({
], ],
}); });
router.beforeEach(authGuardIfNeeded);
export default router; export default router;

View file

@ -5,6 +5,8 @@ import Validate from '@/views/User/Validate.vue';
import ResendConfirmation from '@/views/User/ResendConfirmation.vue'; import ResendConfirmation from '@/views/User/ResendConfirmation.vue';
import SendPasswordReset from '@/views/User/SendPasswordReset.vue'; import SendPasswordReset from '@/views/User/SendPasswordReset.vue';
import PasswordReset from '@/views/User/PasswordReset.vue'; import PasswordReset from '@/views/User/PasswordReset.vue';
import { beforeRegisterGuard } from '@/router/guards/register-guard';
import { RouteConfig } from 'vue-router';
export enum UserRouteName { export enum UserRouteName {
REGISTER = 'Register', REGISTER = 'Register',
@ -16,13 +18,14 @@ export enum UserRouteName {
LOGIN = 'Login', LOGIN = 'Login',
} }
export const userRoutes = [ export const userRoutes: RouteConfig[] = [
{ {
path: '/register/user', path: '/register/user',
name: UserRouteName.REGISTER, name: UserRouteName.REGISTER,
component: RegisterUser, component: RegisterUser,
props: true, props: true,
meta: { requiredAuth: false }, meta: { requiredAuth: false },
beforeEnter: beforeRegisterGuard,
}, },
{ {
path: '/register/profile', path: '/register/profile',

View file

@ -1,4 +1,5 @@
export interface ICurrentUser { export interface ICurrentUser {
id: number; id: number;
email: string; email: string;
isLoggedIn: boolean;
} }

View file

@ -0,0 +1,4 @@
export enum ErrorCode {
UNKNOWN = 'unknown',
REGISTRATION_CLOSED = 'registration_closed',
}

View file

@ -0,0 +1,3 @@
export enum LoginErrorCode {
NEED_TO_LOGIN = 'rouge',
}

25
js/src/views/Error.vue Normal file
View file

@ -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>

View file

@ -5,7 +5,12 @@
<translate>Welcome back!</translate> <translate>Welcome back!</translate>
</h1> </h1>
</section> </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="columns is-mobile is-centered">
<div class="column is-half card"> <div class="column is-half card">
<b-message title="Error" type="is-danger" v-for="error in errors" :key="error">{{ error }}</b-message> <b-message title="Error" type="is-danger" v-for="error in errors" :key="error">{{ error }}</b-message>
@ -49,6 +54,10 @@
</div> </div>
</div> </div>
</section> </section>
<b-message v-else title="Error" type="is-error">
<translate>You are already logged-in.</translate>
</b-message>
</div> </div>
</template> </template>
@ -58,16 +67,21 @@ import { LOGIN } from '@/graphql/auth';
import { validateEmailField, validateRequiredField } from '@/utils/validators'; import { validateEmailField, validateRequiredField } from '@/utils/validators';
import { saveUserData } from '@/utils/auth'; import { saveUserData } from '@/utils/auth';
import { ILogin } from '@/types/login.model'; 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 { onLogin } from '@/vue-apollo';
import { RouteName } from '@/router'; import { RouteName } from '@/router';
import { IConfig } from '@/types/config.model'; import { LoginErrorCode } from '@/types/login-error-code.model'
import { CONFIG } from '@/graphql/config'; import { ICurrentUser } from '@/types/current-user.model'
import { CONFIG } from '@/graphql/config'
import { IConfig } from '@/types/config.model'
@Component({ @Component({
apollo: { apollo: {
config: { config: {
query: 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: '' }) email!: string;
@Prop({ type: String, required: false, default: '' }) password!: string; @Prop({ type: String, required: false, default: '' }) password!: string;
LoginErrorCode = LoginErrorCode;
errorCode: LoginErrorCode | null = null;
config!: IConfig; config!: IConfig;
currentUser!: ICurrentUser;
credentials = { credentials = {
email: '', email: '',
password: '', password: '',
}; };
validationSent = false; validationSent = false;
errors: string[] = []; errors: string[] = [];
rules = { rules = {
required: validateRequiredField, required: validateRequiredField,
email: validateEmailField, email: validateEmailField,
}; };
user: any;
beforeCreate() { private redirect: string | null = null;
if (this.user) {
this.$router.push('/');
}
}
mounted() { mounted() {
this.credentials.email = this.email; this.credentials.email = this.email;
this.credentials.password = this.password; 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) { async loginAction(e: Event) {
e.preventDefault(); e.preventDefault();
this.errors.splice(0);
this.errors = [];
try { try {
const result = await this.$apollo.mutate<{ login: ILogin }>({ const result = await this.$apollo.mutate<{ login: ILogin }>({
@ -119,12 +139,17 @@ export default class Login extends Vue {
variables: { variables: {
id: result.data.login.user.id, id: result.data.login.user.id,
email: this.credentials.email, email: this.credentials.email,
isLoggedIn: true,
}, },
}); });
onLogin(this.$apollo); onLogin(this.$apollo);
if (this.redirect) {
this.$router.push(this.redirect)
} else {
this.$router.push({ name: RouteName.HOME }); this.$router.push({ name: RouteName.HOME });
}
} catch (err) { } catch (err) {
console.error(err); console.error(err);
err.graphQLErrors.forEach(({ message }) => { err.graphQLErrors.forEach(({ message }) => {