Authentification JWT sur le backend super-graph #8
|
@ -0,0 +1,18 @@
|
||||||
|
/* fetchUser */
|
||||||
|
|
||||||
|
variables {
|
||||||
|
"email": ""
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
query fetchUser {
|
||||||
|
user(where: {email: {eq: $email}}) {
|
||||||
|
id
|
||||||
|
created_at
|
||||||
|
updated_at
|
||||||
|
email,
|
||||||
|
full_name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
@ -5499,6 +5499,11 @@
|
||||||
"integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==",
|
"integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"graphql-request": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/graphql-request/-/graphql-request-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-Ww3Ax+G3l2d+mPT8w7HC9LfrKjutnCKtnDq7ZZp2ghVk5IQDjwAk3/arRF1ix17Ky15rm0hrSKVKxRhIVlSuoQ=="
|
||||||
|
},
|
||||||
"handle-thing": {
|
"handle-thing": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz",
|
||||||
|
|
|
@ -54,6 +54,7 @@
|
||||||
"@types/qs": "^6.9.3",
|
"@types/qs": "^6.9.3",
|
||||||
"bulma": "^0.7.2",
|
"bulma": "^0.7.2",
|
||||||
"bulma-switch": "^2.0.0",
|
"bulma-switch": "^2.0.0",
|
||||||
|
"graphql-request": "^2.0.0",
|
||||||
"jwt-decode": "^2.2.0",
|
"jwt-decode": "^2.2.0",
|
||||||
"qs": "^6.9.4",
|
"qs": "^6.9.4",
|
||||||
"react": "^16.12.0",
|
"react": "^16.12.0",
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import React from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import { Page } from '../Page';
|
import { Page } from '../Page';
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector, useDispatch } from 'react-redux';
|
||||||
import { RootState } from '../../store/reducers/root';
|
import { RootState } from '../../store/reducers/root';
|
||||||
|
|
||||||
export function HomePage() {
|
export function HomePage() {
|
||||||
|
@ -14,8 +14,8 @@ export function HomePage() {
|
||||||
<div className="column is-4 is-offset-4">
|
<div className="column is-4 is-offset-4">
|
||||||
<div className="box">
|
<div className="box">
|
||||||
{
|
{
|
||||||
currentUser ?
|
currentUser && currentUser.full_name ?
|
||||||
<p>Bonjour <span className="has-text-weight-bold">{currentUser.email}</span> !</p> :
|
<p>Bonjour <span className="has-text-weight-bold">{currentUser.full_name}</span> !</p> :
|
||||||
<p>Veuillez vous authentifier.</p>
|
<p>Veuillez vous authentifier.</p>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -7,7 +7,8 @@ export const Config = {
|
||||||
oauth2AuthorizeURL: get<string>("oauth2AuthorizeURL", "http://localhost:4444/oauth2/auth"),
|
oauth2AuthorizeURL: get<string>("oauth2AuthorizeURL", "http://localhost:4444/oauth2/auth"),
|
||||||
oauth2TokenURL: get<string>("oauth2TokenURL", "http://localhost:4444/oauth2/token"),
|
oauth2TokenURL: get<string>("oauth2TokenURL", "http://localhost:4444/oauth2/token"),
|
||||||
oauth2LogoutURL: get<string>("oauth2LogoutURL", "http://localhost:4444/oauth2/sessions/logout"),
|
oauth2LogoutURL: get<string>("oauth2LogoutURL", "http://localhost:4444/oauth2/sessions/logout"),
|
||||||
oauth2PostLogoutRedirectURI: get<string>("oauth2PostLogoutRedirectURI", "http://localhost:8081")
|
oauth2PostLogoutRedirectURI: get<string>("oauth2PostLogoutRedirectURI", "http://localhost:8081"),
|
||||||
|
graphQLEndpoint: get<string>("graphQLEndpoint", "http://localhost:8080/api/v1/graphql")
|
||||||
};
|
};
|
||||||
|
|
||||||
function get<T>(key: string, defaultValue: T):T {
|
function get<T>(key: string, defaultValue: T):T {
|
||||||
|
|
|
@ -0,0 +1,19 @@
|
||||||
|
import { Action } from "redux";
|
||||||
|
import { User } from "../../types/user";
|
||||||
|
|
||||||
|
export const FETCH_PROFILE_REQUEST = 'FETCH_PROFILE_REQUEST';
|
||||||
|
export const FETCH_PROFILE_SUCCESS = 'FETCH_PROFILE_SUCCESS';
|
||||||
|
export const FETCH_PROFILE_FAILURE = 'FETCH_PROFILE_FAILURE';
|
||||||
|
|
||||||
|
export interface fetchProfileRequestAction extends Action {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface fetchProfileSuccessAction extends Action {
|
||||||
|
profile: User
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export function fetchProfile(): fetchProfileRequestAction {
|
||||||
|
return { type: FETCH_PROFILE_REQUEST }
|
||||||
|
}
|
|
@ -1,6 +1,7 @@
|
||||||
import { Action } from "redux";
|
import { Action } from "redux";
|
||||||
import { User } from "../../types/user";
|
import { User } from "../../types/user";
|
||||||
import { SET_CURRENT_USER, setCurrentUserAction, LOGOUT } from "../actions/auth";
|
import { SET_CURRENT_USER, setCurrentUserAction, LOGOUT } from "../actions/auth";
|
||||||
|
import { FETCH_PROFILE_SUCCESS, fetchProfileSuccessAction } from "../actions/profile";
|
||||||
|
|
||||||
export interface AuthState {
|
export interface AuthState {
|
||||||
isAuthenticated: boolean
|
isAuthenticated: boolean
|
||||||
|
@ -18,6 +19,9 @@ export function authReducer(state = defaultState, action: Action): AuthState {
|
||||||
return handleSetCurrentUser(state, action as setCurrentUserAction);
|
return handleSetCurrentUser(state, action as setCurrentUserAction);
|
||||||
case LOGOUT:
|
case LOGOUT:
|
||||||
return handleLogout(state);
|
return handleLogout(state);
|
||||||
|
case FETCH_PROFILE_SUCCESS:
|
||||||
|
return handleFetchProfileSuccess(state, action as fetchProfileSuccessAction);
|
||||||
|
|
||||||
}
|
}
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
@ -38,4 +42,14 @@ function handleLogout(state: AuthState): AuthState {
|
||||||
isAuthenticated: false,
|
isAuthenticated: false,
|
||||||
currentUser: null,
|
currentUser: null,
|
||||||
};
|
};
|
||||||
}
|
};
|
||||||
|
|
||||||
|
function handleFetchProfileSuccess(state: AuthState, { profile }: fetchProfileSuccessAction): AuthState {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
isAuthenticated: true,
|
||||||
|
currentUser: {
|
||||||
|
...profile,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
|
@ -0,0 +1,18 @@
|
||||||
|
import { all, put } from "redux-saga/effects";
|
||||||
|
import { getSavedAccessGrant } from "../../util/auth";
|
||||||
|
import { parseIdToken } from "../actions/auth";
|
||||||
|
|
||||||
|
export function* initRootSaga() {
|
||||||
|
yield all([
|
||||||
|
retrieveSessionSaga(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function* retrieveSessionSaga() {
|
||||||
|
console.log("Checking session status...");
|
||||||
|
|
||||||
|
const accessGrant = getSavedAccessGrant();
|
||||||
|
if (!accessGrant) return;
|
||||||
|
|
||||||
|
yield put(parseIdToken(accessGrant.id_token));
|
||||||
|
}
|
|
@ -1,10 +1,14 @@
|
||||||
import { all } from 'redux-saga/effects';
|
import { all } from 'redux-saga/effects';
|
||||||
import { failureRootSaga } from './failure';
|
import { failureRootSaga } from './failure';
|
||||||
import { authRootSaga } from './auth';
|
import { authRootSaga } from './auth';
|
||||||
|
import { initRootSaga } from './init';
|
||||||
|
import { usersRootSaga } from './users';
|
||||||
|
|
||||||
export function* rootSaga() {
|
export function* rootSaga() {
|
||||||
yield all([
|
yield all([
|
||||||
|
initRootSaga(),
|
||||||
failureRootSaga(),
|
failureRootSaga(),
|
||||||
authRootSaga(),
|
authRootSaga(),
|
||||||
|
usersRootSaga(),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,37 @@
|
||||||
|
import { DaddyClient } from "../../util/daddy";
|
||||||
|
import { Config } from "../../config";
|
||||||
|
import { getSavedAccessGrant } from "../../util/auth";
|
||||||
|
import { all, takeLatest, put, select } from "redux-saga/effects";
|
||||||
|
import { FETCH_PROFILE_REQUEST, fetchProfile, FETCH_PROFILE_FAILURE, FETCH_PROFILE_SUCCESS } from "../actions/profile";
|
||||||
|
import { SET_CURRENT_USER } from "../actions/auth";
|
||||||
|
import { RootState } from "../reducers/root";
|
||||||
|
import { User } from "../../types/user";
|
||||||
|
|
||||||
|
export function* usersRootSaga() {
|
||||||
|
yield all([
|
||||||
|
takeLatest(SET_CURRENT_USER, onCurrentUserChangeSaga),
|
||||||
|
takeLatest(FETCH_PROFILE_REQUEST, fetchProfileSaga),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function* onCurrentUserChangeSaga() {
|
||||||
|
yield put(fetchProfile());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export function* fetchProfileSaga() {
|
||||||
|
const grant = getSavedAccessGrant();
|
||||||
|
const client = new DaddyClient(Config.graphQLEndpoint, grant.id_token);
|
||||||
|
|
||||||
|
let profile: User;
|
||||||
|
try {
|
||||||
|
const currentUser: User = yield select((state: RootState) => state.auth.currentUser);
|
||||||
|
profile = yield client.fetchUser(currentUser.email).then(result => result.user);
|
||||||
|
} catch(err) {
|
||||||
|
yield put({ type: FETCH_PROFILE_FAILURE, err });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
yield put({type: FETCH_PROFILE_SUCCESS, profile });
|
||||||
|
}
|
|
@ -1,3 +1,6 @@
|
||||||
export interface User {
|
export interface User {
|
||||||
email: string
|
email: string
|
||||||
|
full_name?: string
|
||||||
|
updated_at?: Date
|
||||||
|
created_at?: Date
|
||||||
}
|
}
|
|
@ -1,3 +1,6 @@
|
||||||
|
import { GraphQLClient } from 'graphql-request'
|
||||||
|
import { Config } from "../config";
|
||||||
|
|
||||||
export class UnauthorizedError extends Error {
|
export class UnauthorizedError extends Error {
|
||||||
constructor(...args: any[]) {
|
constructor(...args: any[]) {
|
||||||
super(...args)
|
super(...args)
|
||||||
|
@ -7,16 +10,35 @@ export class UnauthorizedError extends Error {
|
||||||
|
|
||||||
export class DaddyClient {
|
export class DaddyClient {
|
||||||
|
|
||||||
assertOk(res: any) {
|
gql: GraphQLClient
|
||||||
if (!res.ok) return Promise.reject(new Error('Request failed'));
|
|
||||||
return res;
|
constructor(endpoint: string, idToken: string) {
|
||||||
|
this.gql = new GraphQLClient(endpoint, {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${idToken}`,
|
||||||
|
mode: 'cors',
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
assertAuthorization(res: any) {
|
fetchUser(email: string) {
|
||||||
if (res.status === 401 || res.status === 404) return Promise.reject(new UnauthorizedError());
|
return this.gql.rawRequest(`
|
||||||
return res;
|
query fetchUser {
|
||||||
|
user(where: {email: {eq: $email}}) {
|
||||||
|
id
|
||||||
|
created_at
|
||||||
|
updated_at
|
||||||
|
email,
|
||||||
|
full_name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`, { email })
|
||||||
|
.then(this.assertAuthorization)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
assertAuthorization({ status, data }: any) {
|
||||||
|
if (status === 401) return Promise.reject(new UnauthorizedError());
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
export const daddy = new DaddyClient();
|
}
|
Loading…
Reference in New Issue