Merge branch 'feature/apollo-link' into 'master'
Remove apollo link state See merge request framasoft/mobilizon!165
This commit is contained in:
commit
56467301a1
@ -17,7 +17,6 @@
|
|||||||
"apollo-client": "2.5.1",
|
"apollo-client": "2.5.1",
|
||||||
"apollo-link": "^1.2.11",
|
"apollo-link": "^1.2.11",
|
||||||
"apollo-link-http": "^1.5.14",
|
"apollo-link-http": "^1.5.14",
|
||||||
"apollo-link-state": "^0.4.2",
|
|
||||||
"buefy": "^0.7.3",
|
"buefy": "^0.7.3",
|
||||||
"easygettext": "^2.7.0",
|
"easygettext": "^2.7.0",
|
||||||
"graphql": "^14.2.1",
|
"graphql": "^14.2.1",
|
||||||
@ -53,6 +52,7 @@
|
|||||||
"@vue/cli-service": "^3.6.0",
|
"@vue/cli-service": "^3.6.0",
|
||||||
"@vue/eslint-config-typescript": "^4.0.0",
|
"@vue/eslint-config-typescript": "^4.0.0",
|
||||||
"@vue/test-utils": "^1.0.0-beta.29",
|
"@vue/test-utils": "^1.0.0-beta.29",
|
||||||
|
"apollo-link-error": "^1.1.11",
|
||||||
"chai": "^4.2.0",
|
"chai": "^4.2.0",
|
||||||
"dotenv-webpack": "^1.7.0",
|
"dotenv-webpack": "^1.7.0",
|
||||||
"eslint": "^6.0.1",
|
"eslint": "^6.0.1",
|
||||||
|
@ -11,7 +11,7 @@
|
|||||||
<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_TOKEN, AUTH_USER_ACTOR, AUTH_USER_EMAIL, AUTH_USER_ID } from '@/constants';
|
import { AUTH_ACCESS_TOKEN, AUTH_USER_ACTOR, AUTH_USER_EMAIL, AUTH_USER_ID } from '@/constants';
|
||||||
import { CURRENT_USER_CLIENT, UPDATE_CURRENT_USER_CLIENT } from '@/graphql/user';
|
import { CURRENT_USER_CLIENT, UPDATE_CURRENT_USER_CLIENT } from '@/graphql/user';
|
||||||
import { ICurrentUser } from '@/types/current-user.model';
|
import { ICurrentUser } from '@/types/current-user.model';
|
||||||
import Footer from '@/components/Footer.vue';
|
import Footer from '@/components/Footer.vue';
|
||||||
@ -34,20 +34,20 @@ export default class App extends Vue {
|
|||||||
|
|
||||||
actor = localStorage.getItem(AUTH_USER_ACTOR);
|
actor = localStorage.getItem(AUTH_USER_ACTOR);
|
||||||
|
|
||||||
async mounted () {
|
async mounted() {
|
||||||
await this.initializeCurrentUser();
|
await this.initializeCurrentUser();
|
||||||
}
|
}
|
||||||
|
|
||||||
getUser (): ICurrentUser|false {
|
getUser(): ICurrentUser | false {
|
||||||
return this.currentUser.id ? this.currentUser : false;
|
return this.currentUser.id ? this.currentUser : false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private initializeCurrentUser() {
|
private initializeCurrentUser() {
|
||||||
const userId = localStorage.getItem(AUTH_USER_ID);
|
const userId = localStorage.getItem(AUTH_USER_ID);
|
||||||
const userEmail = localStorage.getItem(AUTH_USER_EMAIL);
|
const userEmail = localStorage.getItem(AUTH_USER_EMAIL);
|
||||||
const token = localStorage.getItem(AUTH_TOKEN);
|
const accessToken = localStorage.getItem(AUTH_ACCESS_TOKEN);
|
||||||
|
|
||||||
if (userId && userEmail && token) {
|
if (userId && userEmail && accessToken) {
|
||||||
return this.$apollo.mutate({
|
return this.$apollo.mutate({
|
||||||
mutation: UPDATE_CURRENT_USER_CLIENT,
|
mutation: UPDATE_CURRENT_USER_CLIENT,
|
||||||
variables: {
|
variables: {
|
||||||
@ -62,42 +62,42 @@ export default class App extends Vue {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
@import "variables";
|
@import "variables";
|
||||||
|
|
||||||
/* Bulma imports */
|
/* Bulma imports */
|
||||||
@import "~bulma/sass/base/_all.sass";
|
@import "~bulma/sass/base/_all.sass";
|
||||||
@import "~bulma/sass/components/card.sass";
|
@import "~bulma/sass/components/card.sass";
|
||||||
@import "~bulma/sass/components/media.sass";
|
@import "~bulma/sass/components/media.sass";
|
||||||
@import "~bulma/sass/components/message.sass";
|
@import "~bulma/sass/components/message.sass";
|
||||||
@import "~bulma/sass/components/modal.sass";
|
@import "~bulma/sass/components/modal.sass";
|
||||||
@import "~bulma/sass/components/navbar.sass";
|
@import "~bulma/sass/components/navbar.sass";
|
||||||
@import "~bulma/sass/components/pagination.sass";
|
@import "~bulma/sass/components/pagination.sass";
|
||||||
@import "~bulma/sass/components/dropdown.sass";
|
@import "~bulma/sass/components/dropdown.sass";
|
||||||
@import "~bulma/sass/elements/box.sass";
|
@import "~bulma/sass/elements/box.sass";
|
||||||
@import "~bulma/sass/elements/button.sass";
|
@import "~bulma/sass/elements/button.sass";
|
||||||
@import "~bulma/sass/elements/container.sass";
|
@import "~bulma/sass/elements/container.sass";
|
||||||
@import "~bulma/sass/form/_all";
|
@import "~bulma/sass/form/_all";
|
||||||
@import "~bulma/sass/elements/icon.sass";
|
@import "~bulma/sass/elements/icon.sass";
|
||||||
@import "~bulma/sass/elements/image.sass";
|
@import "~bulma/sass/elements/image.sass";
|
||||||
@import "~bulma/sass/elements/other.sass";
|
@import "~bulma/sass/elements/other.sass";
|
||||||
@import "~bulma/sass/elements/tag.sass";
|
@import "~bulma/sass/elements/tag.sass";
|
||||||
@import "~bulma/sass/elements/title.sass";
|
@import "~bulma/sass/elements/title.sass";
|
||||||
@import "~bulma/sass/elements/notification";
|
@import "~bulma/sass/elements/notification";
|
||||||
@import "~bulma/sass/grid/_all.sass";
|
@import "~bulma/sass/grid/_all.sass";
|
||||||
@import "~bulma/sass/layout/_all.sass";
|
@import "~bulma/sass/layout/_all.sass";
|
||||||
@import "~bulma/sass/utilities/_all";
|
@import "~bulma/sass/utilities/_all";
|
||||||
|
|
||||||
/* Buefy imports */
|
/* Buefy imports */
|
||||||
@import "~buefy/src/scss/utils/_all";
|
@import "~buefy/src/scss/utils/_all";
|
||||||
@import "~buefy/src/scss/components/datepicker";
|
@import "~buefy/src/scss/components/datepicker";
|
||||||
@import "~buefy/src/scss/components/notices";
|
@import "~buefy/src/scss/components/notices";
|
||||||
@import "~buefy/src/scss/components/dropdown";
|
@import "~buefy/src/scss/components/dropdown";
|
||||||
@import "~buefy/src/scss/components/autocomplete";
|
@import "~buefy/src/scss/components/autocomplete";
|
||||||
@import "~buefy/src/scss/components/form";
|
@import "~buefy/src/scss/components/form";
|
||||||
@import "~buefy/src/scss/components/modal";
|
@import "~buefy/src/scss/components/modal";
|
||||||
@import "~buefy/src/scss/components/tag";
|
@import "~buefy/src/scss/components/tag";
|
||||||
@import "~buefy/src/scss/components/taginput";
|
@import "~buefy/src/scss/components/taginput";
|
||||||
@import "~buefy/src/scss/components/upload";
|
@import "~buefy/src/scss/components/upload";
|
||||||
|
|
||||||
.router-enter-active,
|
.router-enter-active,
|
||||||
.router-leave-active {
|
.router-leave-active {
|
||||||
|
@ -1,27 +1,32 @@
|
|||||||
export const currentUser = {
|
import { ApolloCache } from 'apollo-cache';
|
||||||
defaults: {
|
import { NormalizedCacheObject } from 'apollo-cache-inmemory';
|
||||||
currentUser: {
|
|
||||||
__typename: 'CurrentUser',
|
|
||||||
id: null,
|
|
||||||
email: null,
|
|
||||||
isLoggedIn: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
resolvers: {
|
export function buildCurrentUserResolver(cache: ApolloCache<NormalizedCacheObject>) {
|
||||||
Mutation: {
|
cache.writeData({
|
||||||
updateCurrentUser: (_, { id, email, isLoggedIn }, { cache }) => {
|
data: {
|
||||||
const data = {
|
currentUser: {
|
||||||
|
__typename: 'CurrentUser',
|
||||||
|
id: null,
|
||||||
|
email: null,
|
||||||
|
isLoggedIn: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
updateCurrentUser: (_, { id, email, isLoggedIn }, { cache }) => {
|
||||||
|
const data = {
|
||||||
|
Mutation: {
|
||||||
currentUser: {
|
currentUser: {
|
||||||
id,
|
id,
|
||||||
email,
|
email,
|
||||||
isLoggedIn,
|
isLoggedIn,
|
||||||
__typename: 'CurrentUser',
|
__typename: 'CurrentUser',
|
||||||
},
|
},
|
||||||
};
|
},
|
||||||
|
};
|
||||||
|
|
||||||
cache.writeData({ data });
|
cache.writeData({ data });
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
};
|
||||||
};
|
}
|
||||||
|
@ -45,7 +45,7 @@ export default class AddressAutoComplete extends Vue {
|
|||||||
this.data = result.data.searchAddress as IAddress[];
|
this.data = result.data.searchAddress as IAddress[];
|
||||||
}
|
}
|
||||||
|
|
||||||
@Watch("selected")
|
@Watch('selected')
|
||||||
updateSelected() {
|
updateSelected() {
|
||||||
this.$emit('input', this.selected);
|
this.$emit('input', this.selected);
|
||||||
}
|
}
|
||||||
|
@ -61,9 +61,8 @@
|
|||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Component, Vue, Watch } from 'vue-property-decorator';
|
import { Component, Vue, Watch } from 'vue-property-decorator';
|
||||||
import { CURRENT_USER_CLIENT, UPDATE_CURRENT_USER_CLIENT } from '@/graphql/user';
|
import { CURRENT_USER_CLIENT } from '@/graphql/user';
|
||||||
import { onLogout } from '@/vue-apollo';
|
import { logout } from '@/utils/auth';
|
||||||
import { deleteUserData } from '@/utils/auth';
|
|
||||||
import { LOGGED_PERSON } from '@/graphql/actor';
|
import { LOGGED_PERSON } from '@/graphql/actor';
|
||||||
import { IPerson } from '@/types/actor';
|
import { IPerson } from '@/types/actor';
|
||||||
import { CONFIG } from '@/graphql/config';
|
import { CONFIG } from '@/graphql/config';
|
||||||
@ -89,7 +88,7 @@ import SearchField from '@/components/SearchField.vue';
|
|||||||
export default class NavBar extends Vue {
|
export default class NavBar extends Vue {
|
||||||
notifications = [
|
notifications = [
|
||||||
{ header: 'Coucou' },
|
{ header: 'Coucou' },
|
||||||
{ title: "T'as une notification", subtitle: 'Et elle est cool' },
|
{ title: 'T\'as une notification', subtitle: 'Et elle est cool' },
|
||||||
];
|
];
|
||||||
loggedPerson: IPerson | null = null;
|
loggedPerson: IPerson | null = null;
|
||||||
config!: IConfig;
|
config!: IConfig;
|
||||||
@ -111,31 +110,20 @@ export default class NavBar extends Vue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async logout() {
|
async logout() {
|
||||||
await this.$apollo.mutate({
|
await logout(this.$apollo.provider.defaultClient);
|
||||||
mutation: UPDATE_CURRENT_USER_CLIENT,
|
|
||||||
variables: {
|
|
||||||
id: null,
|
|
||||||
email: null,
|
|
||||||
isLoggedIn: false,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
deleteUserData();
|
|
||||||
|
|
||||||
onLogout(this.$apollo);
|
|
||||||
|
|
||||||
return this.$router.push({ path: '/' });
|
return this.$router.push({ path: '/' });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@import "../variables.scss";
|
@import "../variables.scss";
|
||||||
|
|
||||||
nav {
|
nav {
|
||||||
border-bottom: solid 1px #0a0a0a;
|
border-bottom: solid 1px #0a0a0a;
|
||||||
|
|
||||||
.navbar-item img {
|
.navbar-item img {
|
||||||
max-height: 2.5em;
|
max-height: 2.5em;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
export const AUTH_TOKEN = 'auth-token';
|
export const AUTH_ACCESS_TOKEN = 'auth-access-token';
|
||||||
|
export const AUTH_REFRESH_TOKEN = 'auth-refresh-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_EMAIL = 'auth-user-email';
|
||||||
export const AUTH_USER_ACTOR = 'auth-user-actor';
|
export const AUTH_USER_ACTOR = 'auth-user-actor';
|
||||||
|
@ -3,7 +3,8 @@ import gql from 'graphql-tag';
|
|||||||
export const LOGIN = gql`
|
export const LOGIN = gql`
|
||||||
mutation Login($email: String!, $password: String!) {
|
mutation Login($email: String!, $password: String!) {
|
||||||
login(email: $email, password: $password) {
|
login(email: $email, password: $password) {
|
||||||
token,
|
accessToken,
|
||||||
|
refreshToken,
|
||||||
user {
|
user {
|
||||||
id,
|
id,
|
||||||
}
|
}
|
||||||
@ -33,3 +34,12 @@ mutation ResendConfirmationEmail($email: String!) {
|
|||||||
resendConfirmationEmail(email: $email)
|
resendConfirmationEmail(email: $email)
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
export const REFRESH_TOKEN = gql`
|
||||||
|
mutation RefreshToken($refreshToken: String!) {
|
||||||
|
refreshToken(refreshToken: $refreshToken) {
|
||||||
|
accessToken,
|
||||||
|
refreshToken,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
@ -12,7 +12,8 @@ mutation CreateUser($email: String!, $password: String!) {
|
|||||||
export const VALIDATE_USER = gql`
|
export const VALIDATE_USER = gql`
|
||||||
mutation ValidateUser($token: String!) {
|
mutation ValidateUser($token: String!) {
|
||||||
validateUser(token: $token) {
|
validateUser(token: $token) {
|
||||||
token,
|
accessToken,
|
||||||
|
refreshToken,
|
||||||
user {
|
user {
|
||||||
id,
|
id,
|
||||||
email,
|
email,
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
import { NavigationGuard } from 'vue-router';
|
import { NavigationGuard } from 'vue-router';
|
||||||
import { UserRouteName } from '@/router/user';
|
import { UserRouteName } from '@/router/user';
|
||||||
import { LoginErrorCode } from '@/types/login-error-code.model';
|
import { LoginErrorCode } from '@/types/login-error-code.model';
|
||||||
import { AUTH_TOKEN } from '@/constants';
|
import { AUTH_ACCESS_TOKEN } from '@/constants';
|
||||||
|
|
||||||
export const authGuardIfNeeded: NavigationGuard = async function (to, from, next) {
|
export const authGuardIfNeeded: NavigationGuard = async function (to, from, next) {
|
||||||
if (to.meta.requiredAuth !== true) return 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
|
// 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)) {
|
if (!localStorage.getItem(AUTH_ACCESS_TOKEN)) {
|
||||||
return next({
|
return next({
|
||||||
name: UserRouteName.LOGIN,
|
name: UserRouteName.LOGIN,
|
||||||
query: {
|
query: {
|
||||||
|
7
js/src/types/apollo.ts
Normal file
7
js/src/types/apollo.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import { ServerError, ServerParseError } from 'apollo-link-http-common';
|
||||||
|
|
||||||
|
function isServerError(err: Error | ServerError | ServerParseError | undefined): err is ServerError {
|
||||||
|
return !!err && (err as ServerError).statusCode !== undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export { isServerError };
|
@ -1,7 +1,10 @@
|
|||||||
import { ICurrentUser } from '@/types/current-user.model';
|
import { ICurrentUser } from '@/types/current-user.model';
|
||||||
|
|
||||||
export interface ILogin {
|
export interface IToken {
|
||||||
user: ICurrentUser;
|
accessToken: string;
|
||||||
|
refreshToken: string;
|
||||||
token: string;
|
}
|
||||||
|
|
||||||
|
export interface ILogin extends IToken {
|
||||||
|
user: ICurrentUser;
|
||||||
}
|
}
|
||||||
|
@ -1,14 +1,38 @@
|
|||||||
import { AUTH_TOKEN, AUTH_USER_EMAIL, AUTH_USER_ID } from '@/constants';
|
import { AUTH_ACCESS_TOKEN, AUTH_REFRESH_TOKEN, AUTH_USER_EMAIL, AUTH_USER_ID } from '@/constants';
|
||||||
import { ILogin } from '@/types/login.model';
|
import { ILogin, IToken } from '@/types/login.model';
|
||||||
|
import { UPDATE_CURRENT_USER_CLIENT } from '@/graphql/user';
|
||||||
|
import { onLogout } from '@/vue-apollo';
|
||||||
|
import ApolloClient from 'apollo-client';
|
||||||
|
|
||||||
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_USER_EMAIL, obj.user.email);
|
||||||
localStorage.setItem(AUTH_TOKEN, obj.token);
|
|
||||||
|
saveTokenData(obj);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function saveTokenData(obj: IToken) {
|
||||||
|
localStorage.setItem(AUTH_ACCESS_TOKEN, obj.accessToken);
|
||||||
|
localStorage.setItem(AUTH_REFRESH_TOKEN, obj.refreshToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function deleteUserData() {
|
export function deleteUserData() {
|
||||||
for (const key of [AUTH_USER_ID, AUTH_USER_EMAIL, AUTH_TOKEN]) {
|
for (const key of [AUTH_USER_ID, AUTH_USER_EMAIL, AUTH_ACCESS_TOKEN, AUTH_REFRESH_TOKEN]) {
|
||||||
localStorage.removeItem(key);
|
localStorage.removeItem(key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function logout(apollo: ApolloClient<any>) {
|
||||||
|
apollo.mutate({
|
||||||
|
mutation: UPDATE_CURRENT_USER_CLIENT,
|
||||||
|
variables: {
|
||||||
|
id: null,
|
||||||
|
email: null,
|
||||||
|
isLoggedIn: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
deleteUserData();
|
||||||
|
|
||||||
|
onLogout();
|
||||||
|
}
|
||||||
|
@ -137,9 +137,9 @@ export default class CreateEvent extends Vue {
|
|||||||
const obj = {
|
const obj = {
|
||||||
organizerActorId: this.loggedPerson.id,
|
organizerActorId: this.loggedPerson.id,
|
||||||
beginsOn: this.event.beginsOn.toISOString(),
|
beginsOn: this.event.beginsOn.toISOString(),
|
||||||
tags: this.event.tags.map((tag: ITag) => tag.title)
|
tags: this.event.tags.map((tag: ITag) => tag.title),
|
||||||
};
|
};
|
||||||
let res = Object.assign({}, this.event, obj);
|
const res = Object.assign({}, this.event, obj);
|
||||||
|
|
||||||
if (this.event.physicalAddress) {
|
if (this.event.physicalAddress) {
|
||||||
delete this.event.physicalAddress['__typename'];
|
delete this.event.physicalAddress['__typename'];
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="container">
|
<div class="container" v-if="config">
|
||||||
<section class="hero is-link" v-if="!currentUser.id || !loggedPerson">
|
<section class="hero is-link" v-if="!currentUser.id || !loggedPerson">
|
||||||
<div class="hero-body">
|
<div class="hero-body">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
|
@ -19,9 +19,10 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { VALIDATE_USER } from '@/graphql/user';
|
import { VALIDATE_USER } from '@/graphql/user';
|
||||||
import { Component, Prop, Vue } from 'vue-property-decorator';
|
import { Component, Prop, Vue } from 'vue-property-decorator';
|
||||||
import { AUTH_TOKEN, AUTH_USER_ID } from '@/constants';
|
import { AUTH_USER_ID } from '@/constants';
|
||||||
import { RouteName } from '@/router';
|
import { RouteName } from '@/router';
|
||||||
import { UserRouteName } from '@/router/user';
|
import { UserRouteName } from '@/router/user';
|
||||||
|
import { saveTokenData } from '@/utils/auth';
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
export default class Validate extends Vue {
|
export default class Validate extends Vue {
|
||||||
@ -62,7 +63,8 @@ export default class Validate extends Vue {
|
|||||||
|
|
||||||
saveUserData({ validateUser: login }) {
|
saveUserData({ validateUser: login }) {
|
||||||
localStorage.setItem(AUTH_USER_ID, login.user.id);
|
localStorage.setItem(AUTH_USER_ID, login.user.id);
|
||||||
localStorage.setItem(AUTH_TOKEN, login.token);
|
|
||||||
|
saveTokenData(login);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
@ -1,15 +1,18 @@
|
|||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
import VueApollo from 'vue-apollo';
|
import VueApollo from 'vue-apollo';
|
||||||
import { ApolloLink } from 'apollo-link';
|
import { ApolloLink, Observable } from 'apollo-link';
|
||||||
import { InMemoryCache, IntrospectionFragmentMatcher } from 'apollo-cache-inmemory';
|
import { InMemoryCache, IntrospectionFragmentMatcher } from 'apollo-cache-inmemory';
|
||||||
|
import { onError } from 'apollo-link-error';
|
||||||
import { createLink } from 'apollo-absinthe-upload-link';
|
import { createLink } from 'apollo-absinthe-upload-link';
|
||||||
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 { ApolloClient } from 'apollo-client';
|
||||||
import { DollarApollo } from 'vue-apollo/types/vue-apollo';
|
import { DollarApollo } from 'vue-apollo/types/vue-apollo';
|
||||||
|
import { buildCurrentUserResolver } from '@/apollo/user';
|
||||||
|
import { isServerError } from '@/types/apollo';
|
||||||
|
import { inspect } from 'util';
|
||||||
|
import { REFRESH_TOKEN } from '@/graphql/auth';
|
||||||
|
import { AUTH_ACCESS_TOKEN, AUTH_REFRESH_TOKEN } from '@/constants';
|
||||||
|
import { logout, saveTokenData } from '@/utils/auth';
|
||||||
|
|
||||||
// Install the vue plugin
|
// Install the vue plugin
|
||||||
Vue.use(VueApollo);
|
Vue.use(VueApollo);
|
||||||
@ -44,14 +47,11 @@ const fragmentMatcher = new IntrospectionFragmentMatcher({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const cache = new InMemoryCache({ fragmentMatcher });
|
|
||||||
|
|
||||||
const authMiddleware = new ApolloLink((operation, forward) => {
|
const authMiddleware = new ApolloLink((operation, forward) => {
|
||||||
// add the authorization to the headers
|
// add the authorization to the headers
|
||||||
const token = localStorage.getItem(AUTH_TOKEN);
|
|
||||||
operation.setContext({
|
operation.setContext({
|
||||||
headers: {
|
headers: {
|
||||||
authorization: token ? `Bearer ${token}` : null,
|
authorization: generateTokenHeader(),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -64,21 +64,54 @@ const uploadLink = createLink({
|
|||||||
uri: httpEndpoint,
|
uri: httpEndpoint,
|
||||||
});
|
});
|
||||||
|
|
||||||
const stateLink = withClientState({
|
let refreshingTokenPromise: Promise<boolean> | undefined;
|
||||||
...merge(currentUser),
|
let alreadyRefreshedToken = false;
|
||||||
cache,
|
const errorLink = onError(({ graphQLErrors, networkError, forward, operation }) => {
|
||||||
|
if (isServerError(networkError) && networkError.statusCode === 401 && !alreadyRefreshedToken) {
|
||||||
|
if (!refreshingTokenPromise) refreshingTokenPromise = refreshAccessToken();
|
||||||
|
|
||||||
|
return promiseToObservable(refreshingTokenPromise).flatMap(() => {
|
||||||
|
refreshingTokenPromise = undefined;
|
||||||
|
alreadyRefreshedToken = true;
|
||||||
|
|
||||||
|
const context = operation.getContext();
|
||||||
|
const oldHeaders = context.headers;
|
||||||
|
|
||||||
|
operation.setContext({
|
||||||
|
headers: {
|
||||||
|
...oldHeaders,
|
||||||
|
authorization: generateTokenHeader(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return forward(operation);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (graphQLErrors) {
|
||||||
|
graphQLErrors.forEach(({ message, locations, path }) =>
|
||||||
|
console.log(`[GraphQL error]: Message: ${message}, Location: ${locations}, Path: ${path}`),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (networkError) console.log(`[Network error]: ${networkError}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
const link = stateLink.concat(authMiddleware).concat(uploadLink);
|
const link = authMiddleware
|
||||||
|
.concat(errorLink)
|
||||||
|
.concat(uploadLink);
|
||||||
|
|
||||||
|
const cache = new InMemoryCache({ fragmentMatcher });
|
||||||
|
|
||||||
const apolloClient = new ApolloClient({
|
const apolloClient = new ApolloClient({
|
||||||
cache,
|
cache,
|
||||||
link,
|
link,
|
||||||
connectToDevTools: true,
|
connectToDevTools: true,
|
||||||
|
resolvers: {
|
||||||
|
currentUser: buildCurrentUserResolver(cache),
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
apolloClient.onResetStore(stateLink.writeDefaults as any);
|
|
||||||
|
|
||||||
export const apolloProvider = new VueApollo({
|
export const apolloProvider = new VueApollo({
|
||||||
defaultClient: apolloClient,
|
defaultClient: apolloClient,
|
||||||
errorHandler(error) {
|
errorHandler(error) {
|
||||||
@ -93,13 +126,65 @@ export function onLogin(apolloClient) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Manually call this when user log out
|
// Manually call this when user log out
|
||||||
export async function onLogout(apolloClient: DollarApollo<any>) {
|
export async function onLogout() {
|
||||||
// if (apolloClient.wsClient) restartWebsockets(apolloClient.wsClient);
|
// if (apolloClient.wsClient) restartWebsockets(apolloClient.wsClient);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await apolloClient.provider.defaultClient.resetStore();
|
await apolloClient.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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function refreshAccessToken() {
|
||||||
|
// Remove invalid access token, so the next request is not authenticated
|
||||||
|
localStorage.removeItem(AUTH_ACCESS_TOKEN);
|
||||||
|
|
||||||
|
const refreshToken = localStorage.getItem(AUTH_REFRESH_TOKEN);
|
||||||
|
|
||||||
|
console.log('Refreshing access token.');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await apolloClient.mutate({
|
||||||
|
mutation: REFRESH_TOKEN,
|
||||||
|
variables: {
|
||||||
|
refreshToken,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
saveTokenData(res.data.refreshToken);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (err) {
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateTokenHeader() {
|
||||||
|
const token = localStorage.getItem(AUTH_ACCESS_TOKEN);
|
||||||
|
|
||||||
|
return token ? `Bearer ${token}` : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Thanks: https://github.com/apollographql/apollo-link/issues/747#issuecomment-502676676
|
||||||
|
const promiseToObservable = <T> (promise: Promise<T>) => {
|
||||||
|
return new Observable<T>((subscriber) => {
|
||||||
|
promise.then(
|
||||||
|
(value) => {
|
||||||
|
if (subscriber.closed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
subscriber.next(value);
|
||||||
|
subscriber.complete();
|
||||||
|
},
|
||||||
|
(err) => {
|
||||||
|
console.error('Cannot refresh token.', err);
|
||||||
|
|
||||||
|
subscriber.error(err);
|
||||||
|
logout(apolloClient);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
35
js/yarn.lock
35
js/yarn.lock
@ -1845,6 +1845,15 @@ apollo-link-dedup@^1.0.0:
|
|||||||
apollo-link "^1.2.12"
|
apollo-link "^1.2.12"
|
||||||
tslib "^1.9.3"
|
tslib "^1.9.3"
|
||||||
|
|
||||||
|
apollo-link-error@^1.1.11:
|
||||||
|
version "1.1.11"
|
||||||
|
resolved "https://registry.yarnpkg.com/apollo-link-error/-/apollo-link-error-1.1.11.tgz#7cd363179616fb90da7866cee85cb00ee45d2f3b"
|
||||||
|
integrity sha512-442DNqn3CNRikDaenMMkoDmCRmkoUx/XyUMlRTZBEFdTw3FYPQLsmDO3hzzC4doY5/BHcn9/jdYh9EeLx4HPsA==
|
||||||
|
dependencies:
|
||||||
|
apollo-link "^1.2.12"
|
||||||
|
apollo-link-http-common "^0.2.14"
|
||||||
|
tslib "^1.9.3"
|
||||||
|
|
||||||
apollo-link-http-common@^0.2.14, apollo-link-http-common@^0.2.4:
|
apollo-link-http-common@^0.2.14, apollo-link-http-common@^0.2.4:
|
||||||
version "0.2.14"
|
version "0.2.14"
|
||||||
resolved "https://registry.yarnpkg.com/apollo-link-http-common/-/apollo-link-http-common-0.2.14.tgz#d3a195c12e00f4e311c417f121181dcc31f7d0c8"
|
resolved "https://registry.yarnpkg.com/apollo-link-http-common/-/apollo-link-http-common-0.2.14.tgz#d3a195c12e00f4e311c417f121181dcc31f7d0c8"
|
||||||
@ -1863,14 +1872,6 @@ apollo-link-http@^1.3.2, apollo-link-http@^1.5.14:
|
|||||||
apollo-link-http-common "^0.2.14"
|
apollo-link-http-common "^0.2.14"
|
||||||
tslib "^1.9.3"
|
tslib "^1.9.3"
|
||||||
|
|
||||||
apollo-link-state@^0.4.2:
|
|
||||||
version "0.4.2"
|
|
||||||
resolved "https://registry.yarnpkg.com/apollo-link-state/-/apollo-link-state-0.4.2.tgz#ac00e9be9b0ca89eae0be6ba31fe904b80bbe2e8"
|
|
||||||
integrity sha512-xMPcAfuiPVYXaLwC6oJFIZrKgV3GmdO31Ag2eufRoXpvT0AfJZjdaPB4450Nu9TslHRePN9A3quxNueILlQxlw==
|
|
||||||
dependencies:
|
|
||||||
apollo-utilities "^1.0.8"
|
|
||||||
graphql-anywhere "^4.1.0-alpha.0"
|
|
||||||
|
|
||||||
apollo-link@^1.0.0, apollo-link@^1.0.7, apollo-link@^1.2.11, apollo-link@^1.2.12:
|
apollo-link@^1.0.0, apollo-link@^1.0.7, apollo-link@^1.2.11, apollo-link@^1.2.12:
|
||||||
version "1.2.12"
|
version "1.2.12"
|
||||||
resolved "https://registry.yarnpkg.com/apollo-link/-/apollo-link-1.2.12.tgz#014b514fba95f1945c38ad4c216f31bcfee68429"
|
resolved "https://registry.yarnpkg.com/apollo-link/-/apollo-link-1.2.12.tgz#014b514fba95f1945c38ad4c216f31bcfee68429"
|
||||||
@ -1890,7 +1891,7 @@ apollo-utilities@1.2.1:
|
|||||||
ts-invariant "^0.2.1"
|
ts-invariant "^0.2.1"
|
||||||
tslib "^1.9.3"
|
tslib "^1.9.3"
|
||||||
|
|
||||||
apollo-utilities@1.3.2, apollo-utilities@^1.0.8, apollo-utilities@^1.2.1, apollo-utilities@^1.3.0, apollo-utilities@^1.3.2:
|
apollo-utilities@1.3.2, apollo-utilities@^1.2.1, apollo-utilities@^1.3.0, apollo-utilities@^1.3.2:
|
||||||
version "1.3.2"
|
version "1.3.2"
|
||||||
resolved "https://registry.yarnpkg.com/apollo-utilities/-/apollo-utilities-1.3.2.tgz#8cbdcf8b012f664cd6cb5767f6130f5aed9115c9"
|
resolved "https://registry.yarnpkg.com/apollo-utilities/-/apollo-utilities-1.3.2.tgz#8cbdcf8b012f664cd6cb5767f6130f5aed9115c9"
|
||||||
integrity sha512-JWNHj8XChz7S4OZghV6yc9FNnzEXj285QYp/nLNh943iObycI5GTDO3NGR9Dth12LRrSFMeDOConPfPln+WGfg==
|
integrity sha512-JWNHj8XChz7S4OZghV6yc9FNnzEXj285QYp/nLNh943iObycI5GTDO3NGR9Dth12LRrSFMeDOConPfPln+WGfg==
|
||||||
@ -4931,15 +4932,6 @@ graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.6
|
|||||||
resolved "https://registry.yarnpkg.com/graceful-readlink/-/graceful-readlink-1.0.1.tgz#4cafad76bc62f02fa039b2f94e9a3dd3a391a725"
|
resolved "https://registry.yarnpkg.com/graceful-readlink/-/graceful-readlink-1.0.1.tgz#4cafad76bc62f02fa039b2f94e9a3dd3a391a725"
|
||||||
integrity sha1-TK+tdrxi8C+gObL5Tpo906ORpyU=
|
integrity sha1-TK+tdrxi8C+gObL5Tpo906ORpyU=
|
||||||
|
|
||||||
graphql-anywhere@^4.1.0-alpha.0:
|
|
||||||
version "4.2.4"
|
|
||||||
resolved "https://registry.yarnpkg.com/graphql-anywhere/-/graphql-anywhere-4.2.4.tgz#7f1c08c9348c730c6bb5e818c81f0b72c13696a8"
|
|
||||||
integrity sha512-rN6Op5vle0Ucqo8uOVPuFzRz1L/MB+ZVa+XezhFcQ6iP13vy95HOXRysrRtWcu2kQQTLyukSGmfU08D8LXWSIw==
|
|
||||||
dependencies:
|
|
||||||
apollo-utilities "^1.3.2"
|
|
||||||
ts-invariant "^0.3.2"
|
|
||||||
tslib "^1.9.3"
|
|
||||||
|
|
||||||
graphql-tag@^2.10.1:
|
graphql-tag@^2.10.1:
|
||||||
version "2.10.1"
|
version "2.10.1"
|
||||||
resolved "https://registry.yarnpkg.com/graphql-tag/-/graphql-tag-2.10.1.tgz#10aa41f1cd8fae5373eaf11f1f67260a3cad5e02"
|
resolved "https://registry.yarnpkg.com/graphql-tag/-/graphql-tag-2.10.1.tgz#10aa41f1cd8fae5373eaf11f1f67260a3cad5e02"
|
||||||
@ -10196,13 +10188,6 @@ ts-invariant@^0.2.1:
|
|||||||
dependencies:
|
dependencies:
|
||||||
tslib "^1.9.3"
|
tslib "^1.9.3"
|
||||||
|
|
||||||
ts-invariant@^0.3.2:
|
|
||||||
version "0.3.3"
|
|
||||||
resolved "https://registry.yarnpkg.com/ts-invariant/-/ts-invariant-0.3.3.tgz#b5742b1885ecf9e29c31a750307480f045ec0b16"
|
|
||||||
integrity sha512-UReOKsrJFGC9tUblgSRWo+BsVNbEd77Cl6WiV/XpMlkifXwNIJbknViCucHvVZkXSC/mcWeRnIGdY7uprcwvdQ==
|
|
||||||
dependencies:
|
|
||||||
tslib "^1.9.3"
|
|
||||||
|
|
||||||
ts-invariant@^0.4.0:
|
ts-invariant@^0.4.0:
|
||||||
version "0.4.4"
|
version "0.4.4"
|
||||||
resolved "https://registry.yarnpkg.com/ts-invariant/-/ts-invariant-0.4.4.tgz#97a523518688f93aafad01b0e80eb803eb2abd86"
|
resolved "https://registry.yarnpkg.com/ts-invariant/-/ts-invariant-0.4.4.tgz#97a523518688f93aafad01b0e80eb803eb2abd86"
|
||||||
|
@ -27,7 +27,9 @@ defmodule Mobilizon.Users do
|
|||||||
@spec register(map()) :: {:ok, User.t()} | {:error, String.t()}
|
@spec register(map()) :: {:ok, User.t()} | {:error, String.t()}
|
||||||
def register(%{email: _email, password: _password} = args) do
|
def register(%{email: _email, password: _password} = args) do
|
||||||
with {:ok, %User{} = user} <-
|
with {:ok, %User{} = user} <-
|
||||||
%User{} |> User.registration_changeset(args) |> Mobilizon.Repo.insert() do
|
%User{}
|
||||||
|
|> User.registration_changeset(args)
|
||||||
|
|> Mobilizon.Repo.insert() do
|
||||||
Mobilizon.Events.create_feed_token(%{"user_id" => user.id})
|
Mobilizon.Events.create_feed_token(%{"user_id" => user.id})
|
||||||
{:ok, user}
|
{:ok, user}
|
||||||
end
|
end
|
||||||
@ -51,13 +53,15 @@ defmodule Mobilizon.Users do
|
|||||||
from(u in User, where: u.email == ^email, preload: :default_actor)
|
from(u in User, where: u.email == ^email, preload: :default_actor)
|
||||||
|
|
||||||
true ->
|
true ->
|
||||||
from(u in User,
|
from(
|
||||||
|
u in User,
|
||||||
where: u.email == ^email and not is_nil(u.confirmed_at),
|
where: u.email == ^email and not is_nil(u.confirmed_at),
|
||||||
preload: :default_actor
|
preload: :default_actor
|
||||||
)
|
)
|
||||||
|
|
||||||
false ->
|
false ->
|
||||||
from(u in User,
|
from(
|
||||||
|
u in User,
|
||||||
where: u.email == ^email and is_nil(u.confirmed_at),
|
where: u.email == ^email and is_nil(u.confirmed_at),
|
||||||
preload: :default_actor
|
preload: :default_actor
|
||||||
)
|
)
|
||||||
@ -75,7 +79,8 @@ defmodule Mobilizon.Users do
|
|||||||
@spec get_user_by_activation_token(String.t()) :: Actor.t()
|
@spec get_user_by_activation_token(String.t()) :: Actor.t()
|
||||||
def get_user_by_activation_token(token) do
|
def get_user_by_activation_token(token) do
|
||||||
Repo.one(
|
Repo.one(
|
||||||
from(u in User,
|
from(
|
||||||
|
u in User,
|
||||||
where: u.confirmation_token == ^token,
|
where: u.confirmation_token == ^token,
|
||||||
preload: [:default_actor]
|
preload: [:default_actor]
|
||||||
)
|
)
|
||||||
@ -88,7 +93,8 @@ defmodule Mobilizon.Users do
|
|||||||
@spec get_user_by_reset_password_token(String.t()) :: Actor.t()
|
@spec get_user_by_reset_password_token(String.t()) :: Actor.t()
|
||||||
def get_user_by_reset_password_token(token) do
|
def get_user_by_reset_password_token(token) do
|
||||||
Repo.one(
|
Repo.one(
|
||||||
from(u in User,
|
from(
|
||||||
|
u in User,
|
||||||
where: u.reset_password_token == ^token,
|
where: u.reset_password_token == ^token,
|
||||||
preload: [:default_actor]
|
preload: [:default_actor]
|
||||||
)
|
)
|
||||||
@ -197,14 +203,16 @@ defmodule Mobilizon.Users do
|
|||||||
@spec get_actor_for_user(Mobilizon.Users.User.t()) :: Mobilizon.Actors.Actor.t()
|
@spec get_actor_for_user(Mobilizon.Users.User.t()) :: Mobilizon.Actors.Actor.t()
|
||||||
def get_actor_for_user(%Mobilizon.Users.User{} = user) do
|
def get_actor_for_user(%Mobilizon.Users.User{} = user) do
|
||||||
case Repo.one(
|
case Repo.one(
|
||||||
from(a in Actor,
|
from(
|
||||||
|
a in Actor,
|
||||||
join: u in User,
|
join: u in User,
|
||||||
on: u.default_actor_id == a.id,
|
on: u.default_actor_id == a.id,
|
||||||
where: u.id == ^user.id
|
where: u.id == ^user.id
|
||||||
)
|
)
|
||||||
) do
|
) do
|
||||||
nil ->
|
nil ->
|
||||||
case user |> get_actors_for_user() do
|
case user
|
||||||
|
|> get_actors_for_user() do
|
||||||
[] -> nil
|
[] -> nil
|
||||||
actors -> hd(actors)
|
actors -> hd(actors)
|
||||||
end
|
end
|
||||||
@ -223,22 +231,55 @@ defmodule Mobilizon.Users do
|
|||||||
"""
|
"""
|
||||||
def authenticate(%{user: user, password: password}) do
|
def authenticate(%{user: user, password: password}) do
|
||||||
# Does password match the one stored in the database?
|
# Does password match the one stored in the database?
|
||||||
case Argon2.verify_pass(password, user.password_hash) do
|
with true <- Argon2.verify_pass(password, user.password_hash),
|
||||||
true ->
|
# Yes, create and return the token
|
||||||
# Yes, create and return the token
|
{:ok, tokens} <- generate_tokens(user) do
|
||||||
MobilizonWeb.Guardian.encode_and_sign(user)
|
{:ok, tokens}
|
||||||
|
else
|
||||||
_ ->
|
_ ->
|
||||||
# No, return an error
|
# No, return an error
|
||||||
{:error, :unauthorized}
|
{:error, :unauthorized}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Generate access token and refresh token
|
||||||
|
"""
|
||||||
|
def generate_tokens(user) do
|
||||||
|
with {:ok, access_token} <- generate_access_token(user),
|
||||||
|
{:ok, refresh_token} <- generate_refresh_token(user) do
|
||||||
|
{:ok, %{access_token: access_token, refresh_token: refresh_token}}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp generate_access_token(user) do
|
||||||
|
with {:ok, access_token, _claims} <-
|
||||||
|
MobilizonWeb.Guardian.encode_and_sign(user, %{}, token_type: "access") do
|
||||||
|
{:ok, access_token}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def generate_refresh_token(user) do
|
||||||
|
with {:ok, refresh_token, _claims} <-
|
||||||
|
MobilizonWeb.Guardian.encode_and_sign(user, %{}, token_type: "refresh") do
|
||||||
|
{:ok, refresh_token}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def update_user_default_actor(user_id, actor_id) do
|
def update_user_default_actor(user_id, actor_id) do
|
||||||
with _ <-
|
with _ <-
|
||||||
from(u in User, where: u.id == ^user_id, update: [set: [default_actor_id: ^actor_id]])
|
from(
|
||||||
|
u in User,
|
||||||
|
where: u.id == ^user_id,
|
||||||
|
update: [
|
||||||
|
set: [
|
||||||
|
default_actor_id: ^actor_id
|
||||||
|
]
|
||||||
|
]
|
||||||
|
)
|
||||||
|> Repo.update_all([]) do
|
|> Repo.update_all([]) do
|
||||||
Repo.get!(User, user_id) |> Repo.preload([:default_actor])
|
Repo.get!(User, user_id)
|
||||||
|
|> Repo.preload([:default_actor])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -17,7 +17,8 @@ defmodule MobilizonWeb.Context do
|
|||||||
context =
|
context =
|
||||||
case Guardian.Plug.current_resource(conn) do
|
case Guardian.Plug.current_resource(conn) do
|
||||||
%User{} = user ->
|
%User{} = user ->
|
||||||
Map.put(context, :current_user, user)
|
context
|
||||||
|
|> Map.put(:current_user, user)
|
||||||
|
|
||||||
nil ->
|
nil ->
|
||||||
context
|
context
|
||||||
|
@ -61,6 +61,14 @@ defmodule MobilizonWeb.Guardian do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def on_refresh({old_token, old_claims}, {new_token, new_claims}, _options) do
|
||||||
|
with {:ok, _, _} <- Guardian.DB.on_refresh({old_token, old_claims}, {new_token, new_claims}) do
|
||||||
|
{:ok, {old_token, old_claims}, {new_token, new_claims}}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def on_exchange(old_stuff, new_stuff, options), do: on_refresh(old_stuff, new_stuff, options)
|
||||||
|
|
||||||
# def build_claims(claims, _resource, opts) do
|
# def build_claims(claims, _resource, opts) do
|
||||||
# claims = claims
|
# claims = claims
|
||||||
# |> encode_permissions_into_claims!(Keyword.get(opts, :permissions))
|
# |> encode_permissions_into_claims!(Keyword.get(opts, :permissions))
|
||||||
|
@ -20,7 +20,15 @@ defmodule MobilizonWeb.Resolvers.User do
|
|||||||
@doc """
|
@doc """
|
||||||
Return current logged-in user
|
Return current logged-in user
|
||||||
"""
|
"""
|
||||||
def get_current_user(_parent, _args, %{context: %{current_user: user}}) do
|
def get_current_user(
|
||||||
|
_parent,
|
||||||
|
_args,
|
||||||
|
%{
|
||||||
|
context: %{
|
||||||
|
current_user: user
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) do
|
||||||
{:ok, user}
|
{:ok, user}
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -35,7 +43,11 @@ defmodule MobilizonWeb.Resolvers.User do
|
|||||||
_parent,
|
_parent,
|
||||||
%{page: page, limit: limit, sort: sort, direction: direction},
|
%{page: page, limit: limit, sort: sort, direction: direction},
|
||||||
%{
|
%{
|
||||||
context: %{current_user: %User{role: role}}
|
context: %{
|
||||||
|
current_user: %User{
|
||||||
|
role: role
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
when is_moderator(role) do
|
when is_moderator(role) do
|
||||||
@ -53,8 +65,9 @@ defmodule MobilizonWeb.Resolvers.User do
|
|||||||
"""
|
"""
|
||||||
def login_user(_parent, %{email: email, password: password}, _resolution) do
|
def login_user(_parent, %{email: email, password: password}, _resolution) do
|
||||||
with {:ok, %User{} = user} <- Users.get_user_by_email(email, true),
|
with {:ok, %User{} = user} <- Users.get_user_by_email(email, true),
|
||||||
{:ok, token, _} <- Users.authenticate(%{user: user, password: password}) do
|
{:ok, %{access_token: access_token, refresh_token: refresh_token}} <-
|
||||||
{:ok, %{token: token, user: user}}
|
Users.authenticate(%{user: user, password: password}) do
|
||||||
|
{:ok, %{access_token: access_token, refresh_token: refresh_token, user: user}}
|
||||||
else
|
else
|
||||||
{:error, :user_not_found} ->
|
{:error, :user_not_found} ->
|
||||||
{:error, "User with email not found"}
|
{:error, "User with email not found"}
|
||||||
@ -64,6 +77,31 @@ defmodule MobilizonWeb.Resolvers.User do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Refresh a token
|
||||||
|
"""
|
||||||
|
def refresh_token(
|
||||||
|
_parent,
|
||||||
|
%{
|
||||||
|
refresh_token: refresh_token
|
||||||
|
},
|
||||||
|
_context
|
||||||
|
) do
|
||||||
|
with {:ok, user, _claims} <- MobilizonWeb.Guardian.resource_from_token(refresh_token),
|
||||||
|
{:ok, _old, {exchanged_token, _claims}} <-
|
||||||
|
MobilizonWeb.Guardian.exchange(refresh_token, ["access", "refresh"], "access"),
|
||||||
|
{:ok, refresh_token} <- Users.generate_refresh_token(user) do
|
||||||
|
{:ok, %{access_token: exchanged_token, refresh_token: refresh_token}}
|
||||||
|
else
|
||||||
|
{:error, message} ->
|
||||||
|
Logger.debug("Cannot refresh user token: #{inspect(message)}")
|
||||||
|
{:error, "Cannot refresh the token"}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def refresh_token(_parent, _params, _context),
|
||||||
|
do: {:error, "You need to have an existing token to get a refresh token"}
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Register an user:
|
Register an user:
|
||||||
- check registrations are enabled
|
- check registrations are enabled
|
||||||
@ -92,9 +130,14 @@ defmodule MobilizonWeb.Resolvers.User do
|
|||||||
with {:check_confirmation_token, {:ok, %User{} = user}} <-
|
with {:check_confirmation_token, {:ok, %User{} = user}} <-
|
||||||
{:check_confirmation_token, Activation.check_confirmation_token(token)},
|
{:check_confirmation_token, Activation.check_confirmation_token(token)},
|
||||||
{:get_actor, actor} <- {:get_actor, Users.get_actor_for_user(user)},
|
{:get_actor, actor} <- {:get_actor, Users.get_actor_for_user(user)},
|
||||||
{:guardian_encode_and_sign, {:ok, token, _}} <-
|
{:ok, %{access_token: access_token, refresh_token: refresh_token}} <-
|
||||||
{:guardian_encode_and_sign, MobilizonWeb.Guardian.encode_and_sign(user)} do
|
Users.generate_tokens(user) do
|
||||||
{:ok, %{token: token, user: Map.put(user, :default_actor, actor)}}
|
{:ok,
|
||||||
|
%{
|
||||||
|
access_token: access_token,
|
||||||
|
refresh_token: refresh_token,
|
||||||
|
user: Map.put(user, :default_actor, actor)
|
||||||
|
}}
|
||||||
else
|
else
|
||||||
err ->
|
err ->
|
||||||
Logger.info("Unable to validate user with token #{token}")
|
Logger.info("Unable to validate user with token #{token}")
|
||||||
@ -145,15 +188,22 @@ defmodule MobilizonWeb.Resolvers.User do
|
|||||||
def reset_password(_parent, %{password: password, token: token}, _resolution) do
|
def reset_password(_parent, %{password: password, token: token}, _resolution) do
|
||||||
with {:ok, %User{} = user} <-
|
with {:ok, %User{} = user} <-
|
||||||
ResetPassword.check_reset_password_token(password, token),
|
ResetPassword.check_reset_password_token(password, token),
|
||||||
{:ok, token, _} <- MobilizonWeb.Guardian.encode_and_sign(user) do
|
{:ok, %{access_token: access_token, refresh_token: refresh_token}} <-
|
||||||
{:ok, %{token: token, user: user}}
|
Users.authenticate(%{user: user, password: password}) do
|
||||||
|
{:ok, %{access_token: access_token, refresh_token: refresh_token, user: user}}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc "Change an user default actor"
|
@doc "Change an user default actor"
|
||||||
def change_default_actor(_parent, %{preferred_username: username}, %{
|
def change_default_actor(
|
||||||
context: %{current_user: user}
|
_parent,
|
||||||
}) do
|
%{preferred_username: username},
|
||||||
|
%{
|
||||||
|
context: %{
|
||||||
|
current_user: user
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) do
|
||||||
with %Actor{id: actor_id} <- Actors.get_local_actor_by_name(username),
|
with %Actor{id: actor_id} <- Actors.get_local_actor_by_name(username),
|
||||||
{:user_actor, true} <-
|
{:user_actor, true} <-
|
||||||
{:user_actor, actor_id in Enum.map(Users.get_actors_for_user(user), & &1.id)},
|
{:user_actor, actor_id in Enum.map(Users.get_actors_for_user(user), & &1.id)},
|
||||||
|
@ -31,7 +31,12 @@ defmodule MobilizonWeb.Schema do
|
|||||||
|
|
||||||
@desc "A JWT and the associated user ID"
|
@desc "A JWT and the associated user ID"
|
||||||
object :login do
|
object :login do
|
||||||
field(:token, non_null(:string), description: "A JWT Token for this session")
|
field(:access_token, non_null(:string), description: "A JWT Token for this session")
|
||||||
|
|
||||||
|
field(:refresh_token, non_null(:string),
|
||||||
|
description: "A JWT Token to refresh the access token"
|
||||||
|
)
|
||||||
|
|
||||||
field(:user, non_null(:user), description: "The user associated to this session")
|
field(:user, non_null(:user), description: "The user associated to this session")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -45,6 +45,12 @@ defmodule MobilizonWeb.Schema.UserType do
|
|||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@desc "Token"
|
||||||
|
object :refreshed_token do
|
||||||
|
field(:access_token, non_null(:string), description: "Generated access token")
|
||||||
|
field(:refresh_token, non_null(:string), description: "Generated refreshed token")
|
||||||
|
end
|
||||||
|
|
||||||
@desc "Users list"
|
@desc "Users list"
|
||||||
object :users do
|
object :users do
|
||||||
field(:total, non_null(:integer), description: "Total elements")
|
field(:total, non_null(:integer), description: "Total elements")
|
||||||
@ -118,12 +124,18 @@ defmodule MobilizonWeb.Schema.UserType do
|
|||||||
end
|
end
|
||||||
|
|
||||||
@desc "Login an user"
|
@desc "Login an user"
|
||||||
field :login, :login do
|
field :login, type: :login do
|
||||||
arg(:email, non_null(:string))
|
arg(:email, non_null(:string))
|
||||||
arg(:password, non_null(:string))
|
arg(:password, non_null(:string))
|
||||||
resolve(&User.login_user/3)
|
resolve(&User.login_user/3)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@desc "Refresh a token"
|
||||||
|
field :refresh_token, type: :refreshed_token do
|
||||||
|
arg(:refresh_token, non_null(:string))
|
||||||
|
resolve(&User.refresh_token/3)
|
||||||
|
end
|
||||||
|
|
||||||
@desc "Change default actor for user"
|
@desc "Change default actor for user"
|
||||||
field :change_default_actor, :user do
|
field :change_default_actor, :user do
|
||||||
arg(:preferred_username, non_null(:string))
|
arg(:preferred_username, non_null(:string))
|
||||||
|
2
mix.exs
2
mix.exs
@ -7,7 +7,7 @@ defmodule Mobilizon.Mixfile do
|
|||||||
[
|
[
|
||||||
app: :mobilizon,
|
app: :mobilizon,
|
||||||
version: @version,
|
version: @version,
|
||||||
elixir: "~> 1.9",
|
elixir: "~> 1.8",
|
||||||
elixirc_paths: elixirc_paths(Mix.env()),
|
elixirc_paths: elixirc_paths(Mix.env()),
|
||||||
compilers: [:phoenix, :gettext] ++ Mix.compilers(),
|
compilers: [:phoenix, :gettext] ++ Mix.compilers(),
|
||||||
start_permanent: Mix.env() == :prod,
|
start_permanent: Mix.env() == :prod,
|
||||||
|
@ -68,7 +68,7 @@ defmodule Mobilizon.UsersTest do
|
|||||||
test "authenticate/1 checks the user's password" do
|
test "authenticate/1 checks the user's password" do
|
||||||
{:ok, %User{} = user} = Users.register(%{email: @email, password: @password})
|
{:ok, %User{} = user} = Users.register(%{email: @email, password: @password})
|
||||||
|
|
||||||
assert {:ok, _, _} = Users.authenticate(%{user: user, password: @password})
|
assert {:ok, _} = Users.authenticate(%{user: user, password: @password})
|
||||||
|
|
||||||
assert {:error, :unauthorized} ==
|
assert {:error, :unauthorized} ==
|
||||||
Users.authenticate(%{user: user, password: "bad password"})
|
Users.authenticate(%{user: user, password: "bad password"})
|
||||||
|
@ -3,6 +3,7 @@ defmodule MobilizonWeb.Resolvers.UserResolverTest do
|
|||||||
alias Mobilizon.{Actors, Users, CommonConfig}
|
alias Mobilizon.{Actors, Users, CommonConfig}
|
||||||
alias Mobilizon.Actors.Actor
|
alias Mobilizon.Actors.Actor
|
||||||
alias Mobilizon.Users.User
|
alias Mobilizon.Users.User
|
||||||
|
alias Mobilizon.Users
|
||||||
alias MobilizonWeb.AbsintheHelpers
|
alias MobilizonWeb.AbsintheHelpers
|
||||||
alias Mobilizon.Service.Users.ResetPassword
|
alias Mobilizon.Service.Users.ResetPassword
|
||||||
import Mobilizon.Factory
|
import Mobilizon.Factory
|
||||||
@ -433,7 +434,7 @@ defmodule MobilizonWeb.Resolvers.UserResolverTest do
|
|||||||
validateUser(
|
validateUser(
|
||||||
token: "#{user.confirmation_token}"
|
token: "#{user.confirmation_token}"
|
||||||
) {
|
) {
|
||||||
token,
|
accessToken,
|
||||||
user {
|
user {
|
||||||
id,
|
id,
|
||||||
},
|
},
|
||||||
@ -456,7 +457,7 @@ defmodule MobilizonWeb.Resolvers.UserResolverTest do
|
|||||||
validateUser(
|
validateUser(
|
||||||
token: "no pass"
|
token: "no pass"
|
||||||
) {
|
) {
|
||||||
token,
|
accessToken,
|
||||||
user {
|
user {
|
||||||
id
|
id
|
||||||
},
|
},
|
||||||
@ -641,7 +642,7 @@ defmodule MobilizonWeb.Resolvers.UserResolverTest do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "Resolver: Login an user" do
|
describe "Resolver: Login a user" do
|
||||||
test "test login_user/3 with valid credentials", context do
|
test "test login_user/3 with valid credentials", context do
|
||||||
{:ok, %User{} = user} = Users.register(%{email: "toto@tata.tld", password: "p4ssw0rd"})
|
{:ok, %User{} = user} = Users.register(%{email: "toto@tata.tld", password: "p4ssw0rd"})
|
||||||
|
|
||||||
@ -658,7 +659,8 @@ defmodule MobilizonWeb.Resolvers.UserResolverTest do
|
|||||||
email: "#{user.email}",
|
email: "#{user.email}",
|
||||||
password: "#{user.password}",
|
password: "#{user.password}",
|
||||||
) {
|
) {
|
||||||
token,
|
accessToken,
|
||||||
|
refreshToken,
|
||||||
user {
|
user {
|
||||||
id
|
id
|
||||||
}
|
}
|
||||||
@ -671,7 +673,7 @@ defmodule MobilizonWeb.Resolvers.UserResolverTest do
|
|||||||
|> post("/api", AbsintheHelpers.mutation_skeleton(mutation))
|
|> post("/api", AbsintheHelpers.mutation_skeleton(mutation))
|
||||||
|
|
||||||
assert login = json_response(res, 200)["data"]["login"]
|
assert login = json_response(res, 200)["data"]["login"]
|
||||||
assert Map.has_key?(login, "token") && not is_nil(login["token"])
|
assert Map.has_key?(login, "accessToken") && not is_nil(login["accessToken"])
|
||||||
end
|
end
|
||||||
|
|
||||||
test "test login_user/3 with invalid password", context do
|
test "test login_user/3 with invalid password", context do
|
||||||
@ -690,7 +692,7 @@ defmodule MobilizonWeb.Resolvers.UserResolverTest do
|
|||||||
email: "#{user.email}",
|
email: "#{user.email}",
|
||||||
password: "bad password",
|
password: "bad password",
|
||||||
) {
|
) {
|
||||||
token,
|
accessToken,
|
||||||
user {
|
user {
|
||||||
default_actor {
|
default_actor {
|
||||||
preferred_username,
|
preferred_username,
|
||||||
@ -715,7 +717,7 @@ defmodule MobilizonWeb.Resolvers.UserResolverTest do
|
|||||||
email: "bad email",
|
email: "bad email",
|
||||||
password: "bad password",
|
password: "bad password",
|
||||||
) {
|
) {
|
||||||
token,
|
accessToken,
|
||||||
user {
|
user {
|
||||||
default_actor {
|
default_actor {
|
||||||
preferred_username,
|
preferred_username,
|
||||||
@ -733,6 +735,66 @@ defmodule MobilizonWeb.Resolvers.UserResolverTest do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe "Resolver: Refresh a token" do
|
||||||
|
test "test refresh_token/3 with a bad token", context do
|
||||||
|
mutation = """
|
||||||
|
mutation {
|
||||||
|
refreshToken(
|
||||||
|
refreshToken: "bad_token"
|
||||||
|
) {
|
||||||
|
accessToken
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
res =
|
||||||
|
context.conn
|
||||||
|
|> post("/api", AbsintheHelpers.mutation_skeleton(mutation))
|
||||||
|
|
||||||
|
assert hd(json_response(res, 200)["errors"])["message"] ==
|
||||||
|
"Cannot refresh the token"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "test refresh_token/3 with an appropriate token", context do
|
||||||
|
user = insert(:user)
|
||||||
|
{:ok, refresh_token} = Users.generate_refresh_token(user)
|
||||||
|
|
||||||
|
mutation = """
|
||||||
|
mutation {
|
||||||
|
refreshToken(
|
||||||
|
refreshToken: "#{refresh_token}"
|
||||||
|
) {
|
||||||
|
accessToken
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
res =
|
||||||
|
context.conn
|
||||||
|
|> post("/api", AbsintheHelpers.mutation_skeleton(mutation))
|
||||||
|
|
||||||
|
assert json_response(res, 200)["errors"] == nil
|
||||||
|
|
||||||
|
access_token = json_response(res, 200)["data"]["refreshToken"]["accessToken"]
|
||||||
|
assert String.length(access_token) > 10
|
||||||
|
|
||||||
|
query = """
|
||||||
|
{
|
||||||
|
loggedPerson {
|
||||||
|
preferredUsername,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
res =
|
||||||
|
context.conn
|
||||||
|
|> Plug.Conn.put_req_header("authorization", "Bearer #{access_token}")
|
||||||
|
|> post("/api", AbsintheHelpers.query_skeleton(query, "logged_person"))
|
||||||
|
|
||||||
|
assert json_response(res, 200)["errors"] == nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
describe "Resolver: change default actor for user" do
|
describe "Resolver: change default actor for user" do
|
||||||
test "test change_default_actor/3 with valid actor", context do
|
test "test change_default_actor/3 with valid actor", context do
|
||||||
# Prepare user with two actors
|
# Prepare user with two actors
|
||||||
|
Loading…
Reference in New Issue
Block a user