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

1369
js/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -12,16 +12,20 @@
},
"dependencies": {
"apollo-absinthe-upload-link": "^1.4.0",
"apollo-cache-inmemory": "^1.3.11",
"apollo-link": "^1.2.4",
"apollo-link-http": "^1.5.7",
"apollo-cache-inmemory": "^1.4.0",
"apollo-client": "^2.4.9",
"apollo-link": "^1.2.6",
"apollo-link-http": "^1.5.9",
"apollo-link-state": "^0.4.2",
"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",
"ngeohash": "^0.6.3",
"register-service-worker": "^1.4.1",
"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-gettext": "^2.1.1",
"vue-gravatar": "^1.3.0",
@ -34,6 +38,7 @@
},
"devDependencies": {
"@types/chai": "^4.1.0",
"@types/lodash": "^4.14.120",
"@types/mocha": "^5.2.4",
"@vue/cli-plugin-babel": "^3.1.1",
"@vue/cli-plugin-e2e-nightwatch": "^3.1.1",
@ -49,7 +54,6 @@
"sass-loader": "^7.1.0",
"tslint-config-airbnb": "^5.11.1",
"typescript": "^3.0.0",
"vue-cli-plugin-apollo": "^0.17.4",
"vue-template-compiler": "^2.5.17",
"webpack-bundle-analyzer": "^3.0.3"
},

View File

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

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 { saveUserData } from '@/utils/auth';
import { ILogin } from '@/types/login.model'
import { UPDATE_CURRENT_USER_CLIENT } from '@/graphql/user'
import { onLogin } from '@/vue-apollo'
@Component({
components: {
@ -123,6 +125,17 @@
});
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' });
} catch (err) {
console.error(err);

View File

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

View File

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

View File

@ -1,3 +1,4 @@
export const AUTH_TOKEN = 'auth-token';
export const AUTH_USER_ID = 'auth-user-id';
export const AUTH_USER_EMAIL = 'auth-user-email';
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 App from '@/App.vue';
import router from '@/router';
// import store from './store';
import { createProvider } from './vue-apollo';
import { apolloProvider } from './vue-apollo';
const translations = require('@/i18n/translations.json');
@ -36,6 +35,6 @@ new Vue({
router,
el: '#app',
template: '<App/>',
apolloProvider: createProvider(),
apolloProvider,
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 {
user: {
id: number,
},
user: ICurrentUser,
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';
export function saveUserData(obj: ILogin) {
localStorage.setItem(AUTH_USER_ID, `${obj.user.id}`);
localStorage.setItem(AUTH_USER_EMAIL, obj.user.email);
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 { InMemoryCache, IntrospectionFragmentMatcher } from 'apollo-cache-inmemory';
import { createLink } from 'apollo-absinthe-upload-link';
import { createApolloClient, restartWebsockets } from 'vue-cli-plugin-apollo/graphql-client';
import { AUTH_TOKEN } from './constants';
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
Vue.use(VueApollo);
@ -51,82 +55,40 @@ const uploadLink = createLink({
uri: httpEndpoint,
});
// const link = ApolloLink.from([
// uploadLink,
// authMiddleware,
// HttpLink,
// ]);
const stateLink = withClientState({
...merge(currentUser),
cache,
});
const link = authMiddleware.concat(uploadLink);
const link = stateLink.concat(authMiddleware).concat(uploadLink);
// Config
const defaultOptions = {
const apolloClient = new ApolloClient({
cache,
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,
};
});
// Call this in the Vue app file
export function createProvider(options = {}) {
// Create apollo client
const { apolloClient, wsClient } = createApolloClient({
...defaultOptions,
...options,
});
apolloClient.wsClient = wsClient;
apolloClient.onResetStore(stateLink.writeDefaults as any);
// Create vue apollo provider
return new VueApollo({
defaultClient: apolloClient,
// defaultOptions: {
// $query: {
// fetchPolicy: 'cache-and-network',
// },
// },
errorHandler(error) {
// eslint-disable-next-line no-console
console.log('%cError', 'background: red; color: white; padding: 2px 4px; border-radius: 3px; font-weight: bold;', error.message);
},
});
}
export const apolloProvider = new VueApollo({
defaultClient: apolloClient,
errorHandler(error) {
// eslint-disable-next-line no-console
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
export async function onLogin(apolloClient, token) {
if (typeof localStorage !== 'undefined' && token) {
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);
}
export function onLogin(apolloClient) {
// if (apolloClient.wsClient) restartWebsockets(apolloClient.wsClient);
}
// Manually call this when user log out
export async function onLogout(apolloClient) {
if (typeof localStorage !== 'undefined') {
localStorage.removeItem(AUTH_TOKEN);
}
if (apolloClient.wsClient) restartWebsockets(apolloClient.wsClient);
export async function onLogout(apolloClient: DollarApollo<any>) {
// if (apolloClient.wsClient) restartWebsockets(apolloClient.wsClient);
try {
await apolloClient.resetStore();
await apolloClient.provider.defaultClient.resetStore();
} catch (e) {
// eslint-disable-next-line no-console
console.log('%cError on cache reset (logout)', 'color: orange;', e.message);

View File

@ -9,5 +9,14 @@ module.exports = {
plugins: [
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',
},
],
},
},
};