Skip to content

Instantly share code, notes, and snippets.

@kolynzb
Created May 9, 2023 17:08
Show Gist options
  • Save kolynzb/3fb42cecbc41cdd0aef6f89011344361 to your computer and use it in GitHub Desktop.
Save kolynzb/3fb42cecbc41cdd0aef6f89011344361 to your computer and use it in GitHub Desktop.
API integration with Access and Refresh token
# .env
NEXT_PUBLIC_API_URL=http://localhost:8000/api/v1/

Routes for API

  • register: localhost:8000/api/v1/accounts/register
  • login: localhost:8000/api/v1/accounts/login
  • route to refresh token: localhost:8000/api/v1/accounts/refresh-token/
  • response
{
}
  • /dashboard is the home page
  • Install all packages
yarn add js-cookie axios @reduxjs/toolkit react-redux redux react-toastify  
yarn add -D @types/js-cookie
// services/auth.service.ts
import Cookies from "js-cookie";
import api from "../config/axios.config";
import handleAPIErrors from "../utils/handleAPIErrors.util";
class AuthService {
async login(email: string, password: string) {
const response = await handleAPIErrors(() =>
api.post("/accounts/login/", { email, password })
);
const { access_token, refresh_token, user } = response.data;
Cookies.set("user",JSON.stringify(user));
Cookies.set("access_token", access_token);
Cookies.set("refresh_token", refresh_token);
// Cookies.set("access_token", access_token, { httpOnly: true });
// Cookies.set("refresh_token", refresh_token, { httpOnly: true });
return response.data;
}
async register(
email: string,
password1: string,
password2: string,
profile_type: string
) {
const response = await handleAPIErrors(() =>
api.post("/accounts/register/", {
email,
password1,
password2,
profile_type,
})
);
// TODO: Replace with with Toaster notification.
alert(response.data.detail);
return response.data;
}
async logout() {
Cookies.remove("access_token");
Cookies.remove("refresh_token");
localStorage.removeItem("access_token");
const response = await handleAPIErrors(() =>
api.post("/accounts/logout/", {})
);
return response.data;
}
async changePassword(data: { new_password1: string; new_password2: string }) {
const response = await handleAPIErrors(() =>
api.post("/accounts/password/change/", data)
);
return response.data;
}
async passwordReset(data: { email: string }) {
const response = await handleAPIErrors(() =>
api.post("/accounts/password/reset/", data)
);
return response.data;
}
async passwordResetConfirm(data: {
new_password1: string;
new_password2: string;
uid: string;
token: string;
}) {
const response = await handleAPIErrors(() =>
api.post(
`/accounts/password/reset/confirm/${data.uid}/${data.token}`,
data
)
);
return response.data;
}
async phoneRegister(data: { phone_number: string }) {
const response = await handleAPIErrors(() =>
api.post("/accounts/phone/register/", data)
);
return response.data;
}
async phoneVerify(data: {
phone_number: string;
session_token: string;
security_code: string;
}) {
const response = await handleAPIErrors(() =>
api.post("/accounts/phone/register/", data)
);
return response.data;
}
async resendVerificationEmail(data: { email: string }) {
const response = await handleAPIErrors(() =>
api.post("/accounts/resend-email/", data)
);
return response.data;
}
}
export default new AuthService();
// store/slice/authslice.ts
import {
createAsyncThunk,
createSlice,
PayloadAction,
} from "@reduxjs/toolkit";
import { AxiosError, AxiosResponse } from "axios";
import { useRouter } from "next/router";
import authService from "../../services/auth.service";
import useAuthService from "../../hooks/useAuthService";
import { User } from "../../interfaces/user.interface";
import Cookies from "js-cookie";
const key = "user";
// Get user data from cookie
const userValue = Cookies.get(key);
let initialUser = null;
try {
if (userValue) {
// Parse the JSON string from the cookie
initialUser = JSON.parse(userValue);
}
} catch (error) {
console.error("Error parsing user object from cookie:", error);
}
type RegisterAction = ReturnType<typeof Register>;
const setUserInCookie = (userData: User) => {
const jsonUserData = JSON.stringify(userData);
Cookies.set(key, jsonUserData);
};
const removeUserFromCookie = () => {
Cookies.remove(key);
};
interface AuthState {
user: User | null;
isError: boolean;
isLoading: boolean;
isSuccess: boolean;
message: string;
}
export interface RegisterPayload {
email: string;
password1: string;
password2: string;
}
const initialState: AuthState = {
user: initialUser,
isError: false,
isLoading: false,
isSuccess: false,
message: "",
};
export const Register = createAsyncThunk(
"accounts/register",
async (userData: RegisterPayload, thunkAPI) => {
try {
const { email, password1, password2} = userData;
return await authService.register(
email,
password1,
password2
);
} catch (error: any) {
const message =
(error.response &&
error.response.data &&
error.response.data.message) ||
error.message ||
error.toString();
return thunkAPI.rejectWithValue(message);
}
}
);
export const login = createAsyncThunk(
"accounts/login",
async (userData: { email: string; password: string }, thunkAPI) => {
try {
const { email, password } = userData;
const response = await authService.login(email, password);
const user = response.data.user;
return user;
} catch (error: any) {
const message =
(error.response &&
error.response.data &&
error.response.data.message) ||
error.message ||
error.toString();
return thunkAPI.rejectWithValue(message);
}
}
);
export const logout = createAsyncThunk("accounts/logout", async () => {
authService.logout();
});
export const authSlice = createSlice({
name: "auth",
initialState,
reducers: {
reset: (state) => {
state.isLoading = false;
state.isError = false;
state.isSuccess = false;
state.message = "";
},
setUser: (state, action: PayloadAction<User | null>) => {
state.user = action.payload;
},
},
extraReducers: (builder) => {
builder
.addCase(Register.pending, (state) => {
state.isLoading = true;
})
.addCase(Register.fulfilled, (state, action) => {
state.isLoading = false;
state.isSuccess = true;
state.user = action.payload;
})
.addCase(Register.rejected, (state, action) => {
state.isLoading = false;
state.isError = true;
state.message = action.payload as string;
state.user = null;
})
.addCase(login.pending, (state) => {
state.isLoading = true;
})
.addCase(login.fulfilled, (state, action) => {
state.isLoading = false;
state.isSuccess = true;
state.user = action.payload;
})
.addCase(login.rejected, (state, action) => {
state.isLoading = false;
state.isError = true;
state.message = action.payload as string;
state.user = null;
})
.addCase(logout.fulfilled, (state) => {
state.user = null;
})
},
});
export const { reset, setUser } = authSlice.actions;
export const selectUser = (state: { auth: AuthState }) => state.auth.user;
export default authSlice.reducer;
// config/axios.config.ts
import axios from "axios";
import Cookies from "js-cookie";
const baseURL = process.env.NEXT_PUBLIC_API_URL;
const api = axios.create({
baseURL,
});
api.interceptors.request.use((config) => {
const accessToken = Cookies.get("access_token");
if (accessToken) {
config.headers.Authorization = `Bearer ${accessToken}`;
}
return config;
});
api.interceptors.response.use(
(response) => {
return response;
},
async (error) => {
const originalRequest = error.config;
if (error.response.status === 401 && !originalRequest._retry) {
originalRequest._retry = true;
const refreshToken = Cookies.get("refresh_token");
try {
const response = await api.post("/accounts/refresh-token/", {
refresh: refreshToken,
});
const newAccessToken = response.data.access;
Cookies.set("access_token", newAccessToken, { httpOnly: true });
return api(originalRequest);
} catch (err) {
// handle refresh token error
console.log(err);
}
} else if (error.message === "Network Error") {
// handle network error
console.log("Network Error: ", error);
} else if (error.response && error.response.status >= 500) {
// handle server error
console.log("Server Error: ", error);
}
return Promise.reject(error);
}
);
export default api;
// utils/api/handleAPIErrors.ts
/**
* A utility function that catches errors associated with an API fetch call.
*
* @param apiCall A function that returns a Promise representing an API fetch call.
* @returns A Promise that resolves with the result of the API call if it is successful, or rejects with an error if there is an error with the API call.
*
* @example
* ```
* async function getUser(userId: string) {
* const response = await fetch(`/api/users/${userId}`);
* const data = await response.json();
* return data;
* }
*
* const user = await handleAPIErrors(() => getUser(userId));
* ```
*/
async function handleAPIErrors<T>(apiCall: () => Promise<T>): Promise<T> {
try {
const result = await apiCall();
return result;
} catch (error: any) {
// TODO: Handle the error appropriately (e.g. show a notification, log the error, etc.)
if (error.response) {
// The request was made and the server responded with a status code
// that falls out of the range of 2xx
console.error("API error:", error.response.data);
} else if (error.request) {
// The request was made but no response was received
console.error("No response from server");
} else {
// Something happened in setting up the request that triggered an Error
console.error("Request error:", error.message);
}
throw error;
}
}
export default handleAPIErrors;
// pages/login.tsx
import React, { useEffect, useState } from "react";
import { FcGoogle } from "react-icons/fc";
import { FaLinkedinIn } from "react-icons/fa";
import { useRouter } from "next/router";
import axios from "axios";
import { useDispatch, useSelector } from "react-redux";
import { RootState } from "../../store/store";
import { toast } from "react-toastify";
import { login, reset } from "../../store/slices/authSlice";
import Spinner from "../Spinner";
import { ThunkDispatch } from "@reduxjs/toolkit";
interface LoginInput {
email: string;
password: string;
}
const LoginFlow = () => {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const dispatch = useDispatch<ThunkDispatch<any, any, any>>();
const router = useRouter();
const { user, isLoading, isError, isSuccess, message } = useSelector(
(state: RootState) => state.auth
);
useEffect(() => {
if (isError) toast.error(message);
if (isSuccess || user) router.push("/client_dashboard");
dispatch(reset());
}, [isError, isSuccess, message, user, router, dispatch]);
const submitHandler = (e: any) => {
e.preventDefault();
if (!email) toast.error("An email must be provided");
if (!password) toast.error("A password must be provided");
const userData = {
email,
password,
};
dispatch(login(userData));
};
return (
<form className="">
<div className="d-flex flex-column">
<div className="d-flex flex-column mx-3 mt-2">
<div className="IconDiv mt-4 d-flex align-items-center">
<FcGoogle className="IconSignGoogle " />
<p>Sign in with Google </p>
</div>
{isLoading && <Spinner />}
<input
className="input"
name="email"
placeholder="Email address "
type="email"
onChange={(e) => setEmail(e.target.value)}
/>
<input
className="input"
name="password"
placeholder="Password"
type="password"
onChange={(e) => {
setPassword(e.target.value), console.log("password", password);
}}
/>
<button
className="button mt-4"
type="submit"
onClick={submitHandler}
>
Login
{isLoading && <Spinner />}
</button>
</div>
</div>
</form>
);
};
export default LoginFlow;
// pages/register.tsx
import { useDispatch, useSelector } from "react-redux";
import { RootState } from "../../store/store";
import { Register, reset } from "../../store/slices/authSlice";
import { ThunkDispatch } from "@reduxjs/toolkit";
import { RootState } from "../../store/store";
import React, { useEffect, useState } from "react";
import { useRouter } from "next/router";
const SignUp = () =>{
const [email, setEmail] = React.useState("");
const [password1, setPassword1] = React.useState("");
const [password2, setPassword2] = React.useState("");
const [showPassword, setShowPassword] = useState(false);
const dispatch = useDispatch<ThunkDispatch<any, any, any>>();
const { user, isLoading, isError, isSuccess, message } = useSelector(
(state: RootState) => state.auth
);
useEffect(() => {
if (isError) toast.error(message);
if (isSuccess || user) {
router.push("/dashboard");
toast.success(
"A verification email has been sent your email address. Please check your email"
);
}
dispatch(reset());
}, [isError, isSuccess, message, user, router, dispatch]);
const submitHandler = (e: any) => {
e.preventDefault();
if (password1 !== password2) return toast.error("Passwords do not match");
const userData = {
email,
password1,
password2,
};
dispatch(Register(userData));
return router.push('/dashboard');
};
return (
<form className="">
<div className="d-flex flex-column mx-3 mt-2">
{isLoading && <p>loading...</p>}
<input
className="input"
name="email"
placeholder="Email address "
type="email"
onChange={(e) => setEmail(e.target.value)}
/>
<input
className="w-100 border-0"
name="password"
placeholder="Password"
onChange={(e) => setPassword1(e.target.value)}
/>
<input
className="input"
name=" Confirm password"
placeholder="Confirm Password"
onChange={(e) => setPassword2(e.target.value)}
/>
<button
className="button"
type="submit"
onClick={submitHandler}
>
Create Account
</button>
</div>
</div>
</form>
)
}
// store/store.ts
import { configureStore } from "@reduxjs/toolkit";
import authReducer from "./slices/authSlice";
const store = configureStore({
reducer: {
auth: authReducer,
},
});
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
export default store;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment