diff --git a/backend/config/allow.list b/backend/config/allow.list index e69de29..92cf2d2 100644 --- a/backend/config/allow.list +++ b/backend/config/allow.list @@ -0,0 +1,18 @@ +/* fetchUser */ + +variables { + "email": "" +} + + + query fetchUser { + user(where: {email: {eq: $email}}) { + id + created_at + updated_at + email, + full_name + } + } + + diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 24c785f..1e597da 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -5499,6 +5499,11 @@ "integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==", "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": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz", diff --git a/frontend/package.json b/frontend/package.json index 8dfffd3..6f0d883 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -54,6 +54,7 @@ "@types/qs": "^6.9.3", "bulma": "^0.7.2", "bulma-switch": "^2.0.0", + "graphql-request": "^2.0.0", "jwt-decode": "^2.2.0", "qs": "^6.9.4", "react": "^16.12.0", diff --git a/frontend/src/components/HomePage/HomePage.tsx b/frontend/src/components/HomePage/HomePage.tsx index 150726a..b9ac584 100644 --- a/frontend/src/components/HomePage/HomePage.tsx +++ b/frontend/src/components/HomePage/HomePage.tsx @@ -1,6 +1,6 @@ -import React from 'react'; +import React, { useEffect } from 'react'; import { Page } from '../Page'; -import { useSelector } from 'react-redux'; +import { useSelector, useDispatch } from 'react-redux'; import { RootState } from '../../store/reducers/root'; export function HomePage() { @@ -14,8 +14,8 @@ export function HomePage() {
{ - currentUser ? -

Bonjour {currentUser.email} !

: + currentUser && currentUser.full_name ? +

Bonjour {currentUser.full_name} !

:

Veuillez vous authentifier.

}
diff --git a/frontend/src/config.ts b/frontend/src/config.ts index 9b59e24..98ee0a4 100644 --- a/frontend/src/config.ts +++ b/frontend/src/config.ts @@ -7,7 +7,8 @@ export const Config = { oauth2AuthorizeURL: get("oauth2AuthorizeURL", "http://localhost:4444/oauth2/auth"), oauth2TokenURL: get("oauth2TokenURL", "http://localhost:4444/oauth2/token"), oauth2LogoutURL: get("oauth2LogoutURL", "http://localhost:4444/oauth2/sessions/logout"), - oauth2PostLogoutRedirectURI: get("oauth2PostLogoutRedirectURI", "http://localhost:8081") + oauth2PostLogoutRedirectURI: get("oauth2PostLogoutRedirectURI", "http://localhost:8081"), + graphQLEndpoint: get("graphQLEndpoint", "http://localhost:8080/api/v1/graphql") }; function get(key: string, defaultValue: T):T { diff --git a/frontend/src/store/actions/profile.ts b/frontend/src/store/actions/profile.ts new file mode 100644 index 0000000..c46a75f --- /dev/null +++ b/frontend/src/store/actions/profile.ts @@ -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 } +} \ No newline at end of file diff --git a/frontend/src/store/reducers/auth.ts b/frontend/src/store/reducers/auth.ts index a0bd507..e888748 100644 --- a/frontend/src/store/reducers/auth.ts +++ b/frontend/src/store/reducers/auth.ts @@ -1,6 +1,7 @@ import { Action } from "redux"; import { User } from "../../types/user"; import { SET_CURRENT_USER, setCurrentUserAction, LOGOUT } from "../actions/auth"; +import { FETCH_PROFILE_SUCCESS, fetchProfileSuccessAction } from "../actions/profile"; export interface AuthState { isAuthenticated: boolean @@ -18,6 +19,9 @@ export function authReducer(state = defaultState, action: Action): AuthState { return handleSetCurrentUser(state, action as setCurrentUserAction); case LOGOUT: return handleLogout(state); + case FETCH_PROFILE_SUCCESS: + return handleFetchProfileSuccess(state, action as fetchProfileSuccessAction); + } return state; } @@ -38,4 +42,14 @@ function handleLogout(state: AuthState): AuthState { isAuthenticated: false, currentUser: null, }; -} \ No newline at end of file +}; + +function handleFetchProfileSuccess(state: AuthState, { profile }: fetchProfileSuccessAction): AuthState { + return { + ...state, + isAuthenticated: true, + currentUser: { + ...profile, + } + }; +}; \ No newline at end of file diff --git a/frontend/src/store/sagas/init.ts b/frontend/src/store/sagas/init.ts new file mode 100644 index 0000000..da4d595 --- /dev/null +++ b/frontend/src/store/sagas/init.ts @@ -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)); +} \ No newline at end of file diff --git a/frontend/src/store/sagas/root.ts b/frontend/src/store/sagas/root.ts index dfa853a..71fdf80 100644 --- a/frontend/src/store/sagas/root.ts +++ b/frontend/src/store/sagas/root.ts @@ -1,10 +1,14 @@ import { all } from 'redux-saga/effects'; import { failureRootSaga } from './failure'; import { authRootSaga } from './auth'; +import { initRootSaga } from './init'; +import { usersRootSaga } from './users'; export function* rootSaga() { yield all([ + initRootSaga(), failureRootSaga(), authRootSaga(), + usersRootSaga(), ]); } diff --git a/frontend/src/store/sagas/users.ts b/frontend/src/store/sagas/users.ts new file mode 100644 index 0000000..40521b3 --- /dev/null +++ b/frontend/src/store/sagas/users.ts @@ -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 }); +} \ No newline at end of file diff --git a/frontend/src/types/user.ts b/frontend/src/types/user.ts index 70d7e8b..13c920d 100644 --- a/frontend/src/types/user.ts +++ b/frontend/src/types/user.ts @@ -1,3 +1,6 @@ export interface User { email: string + full_name?: string + updated_at?: Date + created_at?: Date } \ No newline at end of file diff --git a/frontend/src/util/daddy.ts b/frontend/src/util/daddy.ts index 22bb4c5..953ea9d 100644 --- a/frontend/src/util/daddy.ts +++ b/frontend/src/util/daddy.ts @@ -1,3 +1,6 @@ +import { GraphQLClient } from 'graphql-request' +import { Config } from "../config"; + export class UnauthorizedError extends Error { constructor(...args: any[]) { super(...args) @@ -7,16 +10,35 @@ export class UnauthorizedError extends Error { export class DaddyClient { - assertOk(res: any) { - if (!res.ok) return Promise.reject(new Error('Request failed')); - return res; + gql: GraphQLClient + + constructor(endpoint: string, idToken: string) { + this.gql = new GraphQLClient(endpoint, { + headers: { + Authorization: `Bearer ${idToken}`, + mode: 'cors', + } + }); } - assertAuthorization(res: any) { - if (res.status === 401 || res.status === 404) return Promise.reject(new UnauthorizedError()); - return res; + fetchUser(email: string) { + return this.gql.rawRequest(` + 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(); \ No newline at end of file +} \ No newline at end of file