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 d338b853b4
16 changed files with 187 additions and 30 deletions

View File

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

View File

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

View File

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

View File

@ -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
}
`;

View File

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

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

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 { 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;

View File

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

View File

@ -1,4 +1,5 @@
export interface ICurrentUser {
id: number;
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>
</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);
if (this.redirect) {
this.$router.push(this.redirect)
} else {
this.$router.push({ name: RouteName.HOME });
}
} catch (err) {
console.error(err);
err.graphQLErrors.forEach(({ message }) => {