My idea
In this article Use GraphQL to log in with React and Apollo / GraphQL with HttpOnly
cookie, I talk about how to deal with GraphQL when we try to sign in.
Now I have no idea how to sign out... I have no idea to ask server send response with Remove-Cookie
. There is Set-Cookie
rather than Remove-Cookie
. By the way, do not tell user sign out if they are offline is also fucking stupid thing.
So the idea is:
- Ask server send response with
HttpOnly
cookie. - If OK, set a
isLogged
flag to be"true"
in LocalStorage. - If we want to logged out, just set
isLogged
flag to be"false"
.
In my opinion, it is the most easy way to deal with HttpOnly
cookie --- Just do not deal with those.
With Redux
The state of authorization should be a global state rather than a private state of one component. So I will use Redux to handle that data.
We need create a slice named auth
(I am going to use GraphQL to get data. It is not hard!):
// Redux's toolkit.
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
// GraphQL client.
import { useLazyQuery, gql } from '@apollo/client';
// Import store's root's state's type.
import type { RootState } from './store';
// Import typed function (It works better with TypeScript).
import { useDispatch } from '../hooks';
// The slice's name and the key of the localStorage
const SLICE_NAME = 'auth';
const IS_LOGGED_ITEM_KEY = `store-${SLICE_NAME}-isLogged`;
Now we need add type for the slice's state. It is the union of four substates:
export enum AuthStateEnum {
LoggedWithInfo,
LoggedWithoutInfo,
LoggedAndLoadingInfo,
Unlogged,
}
interface LoggedWithInfo {
state: AuthStateEnum.LoggedWithInfo;
name: string;
email: string;
isAdmin: boolean;
}
interface LoggedWithoutInfo {
state: AuthStateEnum.LoggedWithoutInfo;
}
interface LoggedAndLoadingInfo {
state: AuthStateEnum.LoggedAndLoadingInfo;
}
interface Unlogged {
state: AuthStateEnum.Unlogged;
}
type AuthState =
| LoggedWithInfo
| LoggedWithoutInfo
| LoggedAndLoadingInfo
| Unlogged;
At first, a user is Unlogged
if it is the first time for he/she to enter the website. After he/she sign up (and sign in), localStorage will have a item, which means that the user is logged, but not information about the user. The state will be LoggedWithoutInfo
. If the application find the user is LoggedWithoutInfo
, it will send request to server and set the state to LoggedAndLoading
. If finally application get the data, the state will change to LoggedWithInfo
.
Then the user enter the website tomorrow, and no data still here for he/she but localStorage and HttpOnly
cookies, the application will set his/her state to LoggedWithoutInfo
and try to send request to get user's information.
So it will get the initial state by this statement:
const initialState = ((): AuthState => {
// localStorage will store if client store the HttpOnly cookie `JWT` to get
// the authentication. If it is is true, we will try to connect to server to
// get user's information. (So it will be loading soon)
const isLogged = !!localStorage.getItem(IS_LOGGED_ITEM_KEY);
return isLogged
? {
state: AuthStateEnum.LoggedWithoutInfo,
}
: {
state: AuthStateEnum.Unlogged,
};
})();
export const authSlice = createSlice({
name: SLICE_NAME,
initialState,
reducers: {
// Sign out, just remove the flag.
signOut: (state: AuthState) => {
localStorage.removeItem(IS_LOGGED_ITEM_KEY);
state.state = AuthStateEnum.Unlogged;
},
// Really signed, and set the current user's data
// into store.
signInWithInfo: (
_state: AuthState,
actions: PayloadAction<Omit<LoggedWithInfo, 'state'>>,
) => {
localStorage.setItem(IS_LOGGED_ITEM_KEY, 'true');
return {
state: AuthStateEnum.LoggedWithInfo,
...actions.payload,
} as LoggedWithInfo;
},
// Just change the state to `signInAndLoading`.
signInAndLoading: (state: AuthState) => {
localStorage.removeItem(IS_LOGGED_ITEM_KEY);
state.state = AuthStateEnum.LoggedAndLoadingInfo;
},
// Just change the state to `signInWithoutInfo`.
signInWithoutInfo: (state: AuthState) => {
localStorage.removeItem(IS_LOGGED_ITEM_KEY);
state.state = AuthStateEnum.LoggedWithoutInfo;
},
},
});
Now we need use apollo's GraphQL query hook to sign in. It is hard to use hook in createAsyncThunk
(Do you have good idea? Please email me), so I create a hook to deal with. Here is the GraphQL query statement.
const SIGN_IN_QUERY = gql`
query SignIn($email: String!, $password: String!) {
# *************************************************************************
# WARING: Remember to change interface SIGN_IN_QUERY_Return if you change
# the code below.
# *************************************************************************
# Input email, password, and no output.
user(email: $email, password: $password) {
name
email
isAdmin
}
}
`;
interface SIGN_IN_QUERY_Return {
name: string;
email: string;
isAdmin: boolean;
}
And the hook to sign in is:
export const useSignIn = () => {
const [signIn, { data, error, loading }] = useLazyQuery(SIGN_IN_QUERY);
const dispatch = useDispatch();
if (data) {
dispatch(signInWithInfo(data as SIGN_IN_QUERY_Return));
} else if (loading) {
dispatch(signInAndLoading());
} else if (error) {
// do nothing... now
// TODO: Add error message for user.
}
return (email: string, password: string) =>
signIn({ variables: {email, password} });
};
Top comments (0)