Skip to content

Commit

Permalink
feat(frontend): implement user authentication flow
Browse files Browse the repository at this point in the history
  • Loading branch information
greyy-nguyen committed Jan 7, 2025
1 parent 107fc63 commit c05a536
Show file tree
Hide file tree
Showing 14 changed files with 89 additions and 23 deletions.
1 change: 1 addition & 0 deletions chatbot-core/Makefile
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# Makefile for Docker Compose commands
include .env

# Variables
DEEPEVAL := ${PWD}/backend/.venv/bin/deepeval
Expand Down
6 changes: 6 additions & 0 deletions chatbot-core/frontend-v2/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
NEXT_PUBLIC_CHATBOT_CORE_BACKEND_URL="CHATBOT_CORE_BACKEND_URL"
NEXT_PUBLIC_API_VERSION="v1"

NEXT_PUBLIC_GOOGLE_CLIENT_ID="GOOGLE_CLIENT_ID"
NEXT_PUBLIC_GOOGLE_REDIRECT_URI="http://localhost:3000"
NEXT_PUBLIC_ACCESS_TOKEN_KEY="ezhr-access-token"
1 change: 1 addition & 0 deletions chatbot-core/frontend-v2/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ yarn-error.log*

# local env files
.env*.local
!.env.example

# vercel
.vercel
Expand Down
Original file line number Diff line number Diff line change
@@ -1,21 +1,26 @@
import { Bell, LogOut, Settings, User } from 'lucide-react';

import { Avatar, AvatarFallback } from '@/components/ui/avatar';
import { Avatar, AvatarImage } from '@/components/ui/avatar';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import { useGetUserInfo } from '@/hooks/user/use-get-user-info';
import { useUserLogout } from '@/hooks/user/use-user-logout';

import { UserDropdownItem } from './user-dropdown-item';

export const UserButton = () => {
const { data: userInfo } = useGetUserInfo();
const logout = useUserLogout();

return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Avatar className="cursor-pointer">
<AvatarFallback>NN</AvatarFallback>
<AvatarImage src={userInfo?.avatar} />
</Avatar>
</DropdownMenuTrigger>
<DropdownMenuContent>
Expand All @@ -29,7 +34,7 @@ export const UserButton = () => {
Notifications
</UserDropdownItem>
<DropdownMenuSeparator />
<UserDropdownItem Icon={LogOut} onClick={() => {}}>
<UserDropdownItem Icon={LogOut} onClick={logout}>
Logout
</UserDropdownItem>
</DropdownMenuContent>
Expand Down
2 changes: 2 additions & 0 deletions chatbot-core/frontend-v2/src/configs/misc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,5 @@ export const CHATBOT_CORE_BACKEND_URL = process.env.NEXT_PUBLIC_CHATBOT_CORE_BAC
export const API_VERSION = process.env.NEXT_PUBLIC_API_VERSION!;

export const LODASH_DEBOUNCE_TIME_MILLISECONDS = 275;

export const LOCAL_STORAGE_ACCESS_TOKEN_KEY = process.env.NEXT_PUBLIC_ACCESS_TOKEN_KEY!;
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useQuery, useQueryClient } from '@tanstack/react-query';

import { LOCAL_STORAGE_ACCESS_TOKEN_KEY } from '@/configs/misc';
import { ReactQueryKey } from '@/constants/react-query-key';
import { getUserOauthAccessToken } from '@/services/user/get-user-oauth-access-token';

Expand All @@ -11,14 +12,14 @@ export const useGetUserOauthAccessToken = (code?: string) => {
queryFn: async () => {
if (!code) return undefined;

const isSuccess = await getUserOauthAccessToken(code);
const { access_token } = await getUserOauthAccessToken(code);

if (isSuccess) {
queryClient.invalidateQueries({
queryKey: [ReactQueryKey.USER],
refetchType: 'active',
});
}
localStorage.setItem(LOCAL_STORAGE_ACCESS_TOKEN_KEY, access_token);

queryClient.invalidateQueries({
queryKey: [ReactQueryKey.USER],
refetchType: 'active',
});
},
enabled: !!code,
});
Expand Down
17 changes: 17 additions & 0 deletions chatbot-core/frontend-v2/src/hooks/user/use-user-logout.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { useQueryClient } from '@tanstack/react-query';
import { useRouter } from 'next/navigation';

import { LOCAL_STORAGE_ACCESS_TOKEN_KEY } from '@/configs/misc';

export const useUserLogout = () => {
const queryClient = useQueryClient();
const router = useRouter();

return () => {
localStorage.removeItem(LOCAL_STORAGE_ACCESS_TOKEN_KEY);
queryClient.invalidateQueries({
type: 'active',
});
router.push('/');
};
};
20 changes: 20 additions & 0 deletions chatbot-core/frontend-v2/src/lib/axios.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import axios from 'axios';

import { LOCAL_STORAGE_ACCESS_TOKEN_KEY } from '@/configs/misc';

const api = axios.create({
headers: { 'Content-Type': 'application/json' },
withCredentials: true,
});

api.interceptors.request.use(config => {
const accessToken = localStorage.getItem(LOCAL_STORAGE_ACCESS_TOKEN_KEY);

if (accessToken) {
config.headers.Authorization = `Bearer ${accessToken}`;
}

return config;
});

export default api;
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export const OauthLogin = () => {
}

setOauthCode(code);
router.refresh();
router.replace('/');
}, []);

if (isLoading) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import axios from 'axios';

import api from '@/lib/axios';
import { IApiResponse } from '@/types/api-response';
import { IUser } from '@/types/user';
import { ApiEndpointPrefix, getApiUrl } from '@/utils/get-api-url';

export const getCurrentUser = async (): Promise<IUser | null> => {
try {
const res = await axios.get<IUser>(getApiUrl(ApiEndpointPrefix.USER));
return res.data;
const res = await api.get<IApiResponse<IUser>>(getApiUrl(ApiEndpointPrefix.USER));
return res.data.data;
} catch (error) {
console.error('[GetUser]: ', error);
return null;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,20 @@
import axios from 'axios';

import api from '@/lib/axios';
import { IApiResponse } from '@/types/api-response';
import { getAuthUrl } from '@/utils/get-api-url';

export const getUserOauthAccessToken = async (code: string) => {
interface IAccessToken {
access_token: string;
token_type: string;
}

export const getUserOauthAccessToken = async (code: string): Promise<IAccessToken> => {
try {
await axios.get(getAuthUrl(`/oauth/google?code=${code}`));
return true;
const res = await api.get<IApiResponse<IAccessToken>>(getAuthUrl(`/oauth/google?code=${code}`));
return res.data.data;
} catch (error) {
// We want to show the Error here, however, we still throw the Error
// once again for the `useQuery` to catch it.
console.error('[UserAccessToken]: ', error);
return false;
throw new Error('Error getting user access token');
}
};
5 changes: 5 additions & 0 deletions chatbot-core/frontend-v2/src/types/api-response.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export interface IApiResponse<T> {
message: string;
headers: string | null;
data: T;
}
4 changes: 2 additions & 2 deletions chatbot-core/frontend-v2/src/utils/get-api-url.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@ export enum ApiEndpointPrefix {
USER = '/users/me',
}

export const AUTH_PREFIX = '/auth';
export const AUTH_PREFIX = 'auth';

export const getApiUrl = (prefix: ApiEndpointPrefix, endpoint?: string) => {
return `${CHATBOT_CORE_BACKEND_URL}/api/${API_VERSION}/${prefix}${endpoint ?? ''}`;
return `${CHATBOT_CORE_BACKEND_URL}/api/${API_VERSION}${prefix}${endpoint ?? ''}`;
};

export const getAuthUrl = (endpoint: string) => {
Expand Down
1 change: 1 addition & 0 deletions chatbot-core/frontend-v2/tailwind.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ const config: Config = {
content: [
'./src/pages/**/*.{js,ts,jsx,tsx,mdx}',
'./src/components/**/*.{js,ts,jsx,tsx,mdx}',
'./src/providers/**/*.{js,ts,jsx,tsx,mdx}',
'./src/app/**/*.{js,ts,jsx,tsx,mdx}',
'./src/views/**/*.{js,ts,jsx,tsx,mdx}',
],
Expand Down

0 comments on commit c05a536

Please sign in to comment.