Merge branch 'feature/apollo-link-state' into 'master'

Fix login/logout flow

See merge request framasoft/mobilizon!48
This commit is contained in:
Thomas Citharel 2019-01-18 16:15:15 +01:00
commit 759a740625
15 changed files with 307 additions and 1331 deletions

1363
js/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -12,16 +12,20 @@
}, },
"dependencies": { "dependencies": {
"apollo-absinthe-upload-link": "^1.4.0", "apollo-absinthe-upload-link": "^1.4.0",
"apollo-cache-inmemory": "^1.3.11", "apollo-cache-inmemory": "^1.4.0",
"apollo-link": "^1.2.4", "apollo-client": "^2.4.9",
"apollo-link-http": "^1.5.7", "apollo-link": "^1.2.6",
"apollo-link-http": "^1.5.9",
"apollo-link-state": "^0.4.2",
"easygettext": "^2.7.0", "easygettext": "^2.7.0",
"graphql-tag": "^2.9.0", "graphql": "^14.1.1",
"graphql-tag": "^2.10.1",
"lodash": "^4.17.11",
"material-design-icons": "^3.0.1", "material-design-icons": "^3.0.1",
"ngeohash": "^0.6.3", "ngeohash": "^0.6.3",
"register-service-worker": "^1.4.1", "register-service-worker": "^1.4.1",
"vue": "^2.5.17", "vue": "^2.5.17",
"vue-apollo": "^3.0.0-beta.26", "vue-apollo": "^3.0.0-beta.27",
"vue-class-component": "^6.3.2", "vue-class-component": "^6.3.2",
"vue-gettext": "^2.1.1", "vue-gettext": "^2.1.1",
"vue-gravatar": "^1.3.0", "vue-gravatar": "^1.3.0",
@ -34,6 +38,7 @@
}, },
"devDependencies": { "devDependencies": {
"@types/chai": "^4.1.0", "@types/chai": "^4.1.0",
"@types/lodash": "^4.14.120",
"@types/mocha": "^5.2.4", "@types/mocha": "^5.2.4",
"@vue/cli-plugin-babel": "^3.1.1", "@vue/cli-plugin-babel": "^3.1.1",
"@vue/cli-plugin-e2e-nightwatch": "^3.1.1", "@vue/cli-plugin-e2e-nightwatch": "^3.1.1",
@ -49,7 +54,6 @@
"sass-loader": "^7.1.0", "sass-loader": "^7.1.0",
"tslint-config-airbnb": "^5.11.1", "tslint-config-airbnb": "^5.11.1",
"typescript": "^3.0.0", "typescript": "^3.0.0",
"vue-cli-plugin-apollo": "^0.17.4",
"vue-template-compiler": "^2.5.17", "vue-template-compiler": "^2.5.17",
"webpack-bundle-analyzer": "^3.0.3" "webpack-bundle-analyzer": "^3.0.3"
}, },

View File

@ -98,7 +98,7 @@
direction="top" direction="top"
open-on-hover open-on-hover
transition="scale-transition" transition="scale-transition"
v-if="user" v-if="currentUser"
> >
<v-btn <v-btn
slot="activator" slot="activator"
@ -152,9 +152,16 @@
<script lang="ts"> <script lang="ts">
import NavBar from '@/components/NavBar.vue'; import NavBar from '@/components/NavBar.vue';
import { Component, Vue } from 'vue-property-decorator'; import { Component, Vue } from 'vue-property-decorator';
import { AUTH_USER_ACTOR, AUTH_USER_ID } from '@/constants'; import { AUTH_TOKEN, AUTH_USER_ACTOR, AUTH_USER_EMAIL, AUTH_USER_ID } from '@/constants';
import { CURRENT_USER_CLIENT, UPDATE_CURRENT_USER_CLIENT } from '@/graphql/user';
import { ICurrentUser } from '@/types/current-user.model'
@Component({ @Component({
apollo: {
currentUser: {
query: CURRENT_USER_CLIENT
}
},
components: { components: {
NavBar, NavBar,
}, },
@ -162,7 +169,6 @@ import { AUTH_USER_ACTOR, AUTH_USER_ID } from '@/constants';
export default class App extends Vue { export default class App extends Vue {
drawer = false; drawer = false;
fab = false; fab = false;
user = localStorage.getItem(AUTH_USER_ID);
items = [ items = [
{ {
icon: 'poll', text: 'Events', route: 'EventList', role: null, icon: 'poll', text: 'Events', route: 'EventList', role: null,
@ -183,9 +189,14 @@ export default class App extends Vue {
show: false, show: false,
text: '', text: '',
}; };
currentUser!: ICurrentUser;
actor = localStorage.getItem(AUTH_USER_ACTOR); actor = localStorage.getItem(AUTH_USER_ACTOR);
mounted () {
this.initializeCurrentUser()
}
get displayed_name () { get displayed_name () {
// FIXME: load actor // FIXME: load actor
return 'no implemented'; return 'no implemented';
@ -199,12 +210,28 @@ export default class App extends Vue {
} }
getUser () { getUser () {
return this.user === undefined ? false : this.user; return this.currentUser.id ? this.currentUser : false;
} }
toggleDrawer () { toggleDrawer () {
this.drawer = !this.drawer; this.drawer = !this.drawer;
} }
private initializeCurrentUser() {
const userId = localStorage.getItem(AUTH_USER_ID);
const userEmail = localStorage.getItem(AUTH_USER_EMAIL);
const token = localStorage.getItem(AUTH_TOKEN);
if (userId && userEmail && token) {
return this.$apollo.mutate({
mutation: UPDATE_CURRENT_USER_CLIENT,
variables: {
id: userId,
email: userEmail,
},
});
}
}
} }
</script> </script>

25
js/src/apollo/user.ts Normal file
View File

@ -0,0 +1,25 @@
export const currentUser = {
defaults: {
currentUser: {
__typename: 'CurrentUser',
id: null,
email: null,
},
},
resolvers: {
Mutation: {
updateCurrentUser: (_, { id, email }, { cache }) => {
const data = {
currentUser: {
id,
email,
__typename: 'CurrentUser',
},
};
cache.writeData({ data });
},
},
},
};

View File

@ -67,6 +67,8 @@
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 { onLogin } from '@/vue-apollo'
@Component({ @Component({
components: { components: {
@ -123,6 +125,17 @@
}); });
saveUserData(result.data.login); saveUserData(result.data.login);
await this.$apollo.mutate({
mutation: UPDATE_CURRENT_USER_CLIENT,
variables: {
id: result.data.login.user.id,
email: this.credentials.email,
}
});
onLogin(this.$apollo);
this.$router.push({ name: 'Home' }); this.$router.push({ name: 'Home' });
} catch (err) { } catch (err) {
console.error(err); console.error(err);

View File

@ -5,7 +5,7 @@
src="https://picsum.photos/1200/900" src="https://picsum.photos/1200/900"
dark dark
height="300" height="300"
v-if="!user" v-if="!currentUser.id"
> >
<v-container fill-height> <v-container fill-height>
<v-layout align-center> <v-layout align-center>
@ -88,12 +88,17 @@
import { AUTH_USER_ACTOR, AUTH_USER_ID } from '@/constants'; import { AUTH_USER_ACTOR, AUTH_USER_ID } from '@/constants';
import { FETCH_EVENTS } from '@/graphql/event'; import { FETCH_EVENTS } from '@/graphql/event';
import { Component, Vue } from 'vue-property-decorator'; import { Component, Vue } from 'vue-property-decorator';
import { ICurrentUser } from '@/types/current-user.model';
import { CURRENT_USER_CLIENT } from '@/graphql/user';
@Component({ @Component({
apollo: { apollo: {
events: { events: {
query: FETCH_EVENTS, query: FETCH_EVENTS,
}, },
currentUser: {
query: CURRENT_USER_CLIENT,
},
}, },
}) })
export default class Home extends Vue { export default class Home extends Vue {
@ -109,7 +114,7 @@
country = { name: null }; country = { name: null };
// FIXME: correctly parse local storage // FIXME: correctly parse local storage
actor = JSON.parse(localStorage.getItem(AUTH_USER_ACTOR) || '{}'); actor = JSON.parse(localStorage.getItem(AUTH_USER_ACTOR) || '{}');
user = localStorage.getItem(AUTH_USER_ID); currentUser!: ICurrentUser;
get displayed_name() { get displayed_name() {
return this.actor.name === null ? this.actor.preferredUsername : this.actor.name; return this.actor.name === null ? this.actor.preferredUsername : this.actor.name;
@ -126,7 +131,7 @@
geoLocalize() { geoLocalize() {
const router = this.$router; const router = this.$router;
const sessionCity = sessionStorage.getItem('City') const sessionCity = sessionStorage.getItem('City');
if (sessionCity) { if (sessionCity) {
router.push({ name: 'EventList', params: { location: sessionCity } }); router.push({ name: 'EventList', params: { location: sessionCity } });
} else { } else {

View File

@ -46,12 +46,15 @@
</template> </template>
</v-autocomplete> </v-autocomplete>
<v-spacer></v-spacer> <v-spacer></v-spacer>
<span v-if="currentUser.id" @click="logout()">Logout</span>
<v-menu <v-menu
offset-y offset-y
:close-on-content-click="false" :close-on-content-click="false"
:nudge-width="200" :nudge-width="200"
v-model="notificationMenu" v-model="notificationMenu"
v-if="user" v-if="currentUser.id"
> >
<v-btn icon slot="activator"> <v-btn icon slot="activator">
<v-badge left color="red"> <v-badge left color="red">
@ -83,7 +86,7 @@
</v-card-actions> </v-card-actions>
</v-card> </v-card>
</v-menu> </v-menu>
<v-btn v-if="!user" :to="{ name: 'Login' }"> <v-btn v-if="!currentUser.id" :to="{ name: 'Login' }">
<translate>Login</translate> <translate>Login</translate>
</v-btn> </v-btn>
</v-toolbar> </v-toolbar>
@ -97,8 +100,11 @@
<script lang="ts"> <script lang="ts">
import { Component, Prop, Vue, Watch } from 'vue-property-decorator'; import { Component, Prop, Vue, Watch } from 'vue-property-decorator';
import { AUTH_USER_ACTOR, AUTH_USER_ID } from '@/constants'; import { AUTH_USER_ACTOR } from '@/constants';
import { SEARCH } from '@/graphql/search'; import { SEARCH } from '@/graphql/search';
import { CURRENT_USER_CLIENT } from '@/graphql/user';
import { onLogout } from '@/vue-apollo';
import { deleteUserData } from '@/utils/auth';
@Component({ @Component({
apollo: { apollo: {
@ -113,6 +119,9 @@ import { SEARCH } from '@/graphql/search';
return !this.searchText; return !this.searchText;
}, },
}, },
currentUser: {
query: CURRENT_USER_CLIENT
}
}, },
}) })
export default class NavBar extends Vue { export default class NavBar extends Vue {
@ -128,7 +137,6 @@ export default class NavBar extends Vue {
searchText: string | null = null; searchText: string | null = null;
searchSelect = null; searchSelect = null;
actor = localStorage.getItem(AUTH_USER_ACTOR); actor = localStorage.getItem(AUTH_USER_ACTOR);
user = localStorage.getItem(AUTH_USER_ID);
get items() { get items() {
return this.search.map(searchEntry => { return this.search.map(searchEntry => {
@ -165,5 +173,13 @@ export default class NavBar extends Vue {
this.$apollo.queries['search'].refetch(); this.$apollo.queries['search'].refetch();
} }
logout() {
alert('logout !');
deleteUserData();
return onLogout(this.$apollo);
}
} }
</script> </script>

View File

@ -1,3 +1,4 @@
export const AUTH_TOKEN = 'auth-token'; export const AUTH_TOKEN = 'auth-token';
export const AUTH_USER_ID = 'auth-user-id'; export const AUTH_USER_ID = 'auth-user-id';
export const AUTH_USER_EMAIL = 'auth-user-email';
export const AUTH_USER_ACTOR = 'auth-user-actor'; export const AUTH_USER_ACTOR = 'auth-user-actor';

View File

@ -19,3 +19,18 @@ mutation ValidateUser($token: String!) {
} }
} }
`; `;
export const CURRENT_USER_CLIENT = gql`
query {
currentUser @client {
id,
email
}
}
`;
export const UPDATE_CURRENT_USER_CLIENT = gql`
mutation UpdateCurrentUser($id: Int!, $email: String!) {
updateCurrentUser(id: $id, email: $email) @client
}
`

View File

@ -9,8 +9,7 @@ import 'material-design-icons/iconfont/material-icons.css';
import 'vuetify/dist/vuetify.min.css'; import 'vuetify/dist/vuetify.min.css';
import App from '@/App.vue'; import App from '@/App.vue';
import router from '@/router'; import router from '@/router';
// import store from './store'; import { apolloProvider } from './vue-apollo';
import { createProvider } from './vue-apollo';
const translations = require('@/i18n/translations.json'); const translations = require('@/i18n/translations.json');
@ -36,6 +35,6 @@ new Vue({
router, router,
el: '#app', el: '#app',
template: '<App/>', template: '<App/>',
apolloProvider: createProvider(), apolloProvider,
components: { App }, components: { App },
}); });

View File

@ -0,0 +1,4 @@
export interface ICurrentUser {
id: number,
email: string,
}

View File

@ -1,7 +1,7 @@
import { ICurrentUser } from '@/types/current-user.model';
export interface ILogin { export interface ILogin {
user: { user: ICurrentUser,
id: number,
},
token: string, token: string,
} }

View File

@ -1,7 +1,14 @@
import { AUTH_TOKEN, AUTH_USER_ID } from '@/constants'; import { AUTH_TOKEN, AUTH_USER_EMAIL, AUTH_USER_ID } from '@/constants';
import { ILogin } from '@/types/login.model'; import { ILogin } from '@/types/login.model';
export function saveUserData(obj: ILogin) { export function saveUserData(obj: ILogin) {
localStorage.setItem(AUTH_USER_ID, `${obj.user.id}`); localStorage.setItem(AUTH_USER_ID, `${obj.user.id}`);
localStorage.setItem(AUTH_USER_EMAIL, obj.user.email);
localStorage.setItem(AUTH_TOKEN, obj.token); localStorage.setItem(AUTH_TOKEN, obj.token);
} }
export function deleteUserData() {
for (const key of [ AUTH_USER_ID, AUTH_USER_EMAIL, AUTH_TOKEN ]) {
localStorage.removeItem(key);
}
}

View File

@ -3,9 +3,13 @@ import VueApollo from 'vue-apollo';
import { ApolloLink } from 'apollo-link'; import { ApolloLink } from 'apollo-link';
import { InMemoryCache, IntrospectionFragmentMatcher } from 'apollo-cache-inmemory'; import { InMemoryCache, IntrospectionFragmentMatcher } from 'apollo-cache-inmemory';
import { createLink } from 'apollo-absinthe-upload-link'; import { createLink } from 'apollo-absinthe-upload-link';
import { createApolloClient, restartWebsockets } from 'vue-cli-plugin-apollo/graphql-client';
import { AUTH_TOKEN } from './constants'; import { AUTH_TOKEN } from './constants';
import { GRAPHQL_API_ENDPOINT, GRAPHQL_API_FULL_PATH } from './api/_entrypoint'; import { GRAPHQL_API_ENDPOINT, GRAPHQL_API_FULL_PATH } from './api/_entrypoint';
import { withClientState } from 'apollo-link-state';
import { currentUser } from '@/apollo/user';
import merge from 'lodash/merge';
import { ApolloClient } from 'apollo-client';
import { DollarApollo } from 'vue-apollo/types/vue-apollo';
// Install the vue plugin // Install the vue plugin
Vue.use(VueApollo); Vue.use(VueApollo);
@ -51,82 +55,40 @@ const uploadLink = createLink({
uri: httpEndpoint, uri: httpEndpoint,
}); });
// const link = ApolloLink.from([ const stateLink = withClientState({
// uploadLink, ...merge(currentUser),
// authMiddleware, cache,
// HttpLink, });
// ]);
const link = authMiddleware.concat(uploadLink); const link = stateLink.concat(authMiddleware).concat(uploadLink);
// Config const apolloClient = new ApolloClient({
const defaultOptions = {
cache, cache,
link, link,
// You can use `https` for secure connection (recommended in production)
httpEndpoint,
// You can use `wss` for secure connection (recommended in production)
// Use `null` to disable subscriptions
// wsEndpoint: process.env.VUE_APP_GRAPHQL_WS || 'ws://localhost:4000/graphql',
// LocalStorage token
tokenName: AUTH_TOKEN,
// Enable Automatic Query persisting with Apollo Engine
persisting: false,
// Use websockets for everything (no HTTP)
// You need to pass a `wsEndpoint` for this to work
websocketsOnly: false,
// Is being rendered on the server?
ssr: false,
defaultHttpLink: false,
connectToDevTools: true, connectToDevTools: true,
};
// Call this in the Vue app file
export function createProvider(options = {}) {
// Create apollo client
const { apolloClient, wsClient } = createApolloClient({
...defaultOptions,
...options,
}); });
apolloClient.wsClient = wsClient;
// Create vue apollo provider apolloClient.onResetStore(stateLink.writeDefaults as any);
return new VueApollo({
export const apolloProvider = new VueApollo({
defaultClient: apolloClient, defaultClient: apolloClient,
// defaultOptions: {
// $query: {
// fetchPolicy: 'cache-and-network',
// },
// },
errorHandler(error) { errorHandler(error) {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.log('%cError', 'background: red; color: white; padding: 2px 4px; border-radius: 3px; font-weight: bold;', error.message); console.log('%cError', 'background: red; color: white; padding: 2px 4px; border-radius: 3px; font-weight: bold;', error.message);
}, },
}); });
}
// Manually call this when user log in // Manually call this when user log in
export async function onLogin(apolloClient, token) { export function onLogin(apolloClient) {
if (typeof localStorage !== 'undefined' && token) { // if (apolloClient.wsClient) restartWebsockets(apolloClient.wsClient);
localStorage.setItem(AUTH_TOKEN, token);
}
if (apolloClient.wsClient) restartWebsockets(apolloClient.wsClient);
try {
await apolloClient.resetStore();
} catch (e) {
// eslint-disable-next-line no-console
console.log('%cError on cache reset (login)', 'color: orange;', e.message);
}
} }
// Manually call this when user log out // Manually call this when user log out
export async function onLogout(apolloClient) { export async function onLogout(apolloClient: DollarApollo<any>) {
if (typeof localStorage !== 'undefined') { // if (apolloClient.wsClient) restartWebsockets(apolloClient.wsClient);
localStorage.removeItem(AUTH_TOKEN);
}
if (apolloClient.wsClient) restartWebsockets(apolloClient.wsClient);
try { try {
await apolloClient.resetStore(); await apolloClient.provider.defaultClient.resetStore();
} catch (e) { } catch (e) {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.log('%cError on cache reset (logout)', 'color: orange;', e.message); console.log('%cError on cache reset (logout)', 'color: orange;', e.message);

View File

@ -9,5 +9,14 @@ module.exports = {
plugins: [ plugins: [
new Dotenv({ path: path.resolve(process.cwd(), '../.env') }), new Dotenv({ path: path.resolve(process.cwd(), '../.env') }),
], ],
module: {
rules: [ // fixes https://github.com/graphql/graphql-js/issues/1272
{
test: /\.mjs$/,
include: /node_modules/,
type: 'javascript/auto',
},
],
},
}, },
}; };