Merge branch 'feature/apollo-link-state' into 'master'
Fix login/logout flow See merge request framasoft/mobilizon!48
This commit is contained in:
commit
759a740625
File diff suppressed because it is too large
Load Diff
|
@ -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"
|
||||
},
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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 });
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
|
@ -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);
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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
|
||||
}
|
||||
`
|
||||
|
|
|
@ -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 },
|
||||
});
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
export interface ICurrentUser {
|
||||
id: number,
|
||||
email: string,
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
import { ICurrentUser } from '@/types/current-user.model';
|
||||
|
||||
export interface ILogin {
|
||||
user: {
|
||||
id: number,
|
||||
},
|
||||
user: ICurrentUser,
|
||||
|
||||
token: string,
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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({
|
||||
export const apolloProvider = 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);
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 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);
|
||||
|
|
|
@ -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',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
Loading…
Reference in New Issue