Skip to content

Commit

Permalink
✨ Integrate login with Cognito and set up API (Taegon21#31)
Browse files Browse the repository at this point in the history
* ✨ Implement login with AWS Cognito

* ✨ Manage idToken with cookie

* ✨ Integrate API logic with React Query

* ✨ Create API integration test page
  • Loading branch information
Taegon21 authored Jun 27, 2024
1 parent 2e845c1 commit cc10f67
Show file tree
Hide file tree
Showing 19 changed files with 1,510 additions and 92 deletions.
515 changes: 511 additions & 4 deletions package-lock.json

Large diffs are not rendered by default.

11 changes: 10 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,24 @@
"lint": "next lint"
},
"dependencies": {
"@tanstack/react-query": "^5.48.0",
"@tanstack/react-query-devtools": "^5.48.0",
"@tanstack/react-query-next-experimental": "^5.48.0",
"amazon-cognito-identity-js": "^6.3.12",
"axios": "^1.7.2",
"js-cookie": "^3.0.5",
"next": "14.2.3",
"next-auth": "^4.24.7",
"react": "^18",
"react-dom": "^18",
"zustand": "^4.5.2"
},
"devDependencies": {
"@svgr/webpack": "^8.1.0",
"@types/js-cookie": "^3.0.6",
"@types/next": "^9.0.0",
"@types/node": "^20",
"@types/react": "^18",
"@types/react": "^18.3.3",
"@types/react-dom": "^18",
"eslint": "^8",
"eslint-config-next": "14.2.3",
Expand Down
147 changes: 147 additions & 0 deletions src/api/auth/auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
import {
CognitoUserPool,
CognitoUserAttribute,
ISignUpResult,
AuthenticationDetails,
CognitoUser,
} from "amazon-cognito-identity-js";

interface SignUpParams {
email: string;
password: string;
name: string; // custom:name 속성
role: string; // custom:role 속성
studentId: string; // custom:student_id 속성
onSuccess: (result: ISignUpResult) => void;
onFailure: (error: Error) => void;
}

interface AuthResponse {
idToken: string;
name: string; // 사용자의 이름
role: string; // 사용자의 역할
studentId: string; // 학생 ID
}

interface VerifyEmailParams {
username: string; // 이메일 주소
code: string; // 사용자로부터 받은 인증 코드
}

const USER_POOL_ID = process.env.NEXT_PUBLIC_COGNITO_USER_POOL_ID;
const CLIENT_ID = process.env.NEXT_PUBLIC_COGNITO_CLIENT_ID;

if (!USER_POOL_ID || !CLIENT_ID) {
throw new Error(
"Cognito credentials are not properly set in the environment variables."
);
}

const poolData = {
UserPoolId: USER_POOL_ID,
ClientId: CLIENT_ID,
};

const userPool = new CognitoUserPool(poolData);

export function signUp({
email,
password,
name,
role,
studentId,
onSuccess,
onFailure,
}: SignUpParams): void {
const username = email;
const attributeList = [
new CognitoUserAttribute({
Name: "email",
Value: email,
}),
new CognitoUserAttribute({
Name: "custom:name",
Value: name,
}),
new CognitoUserAttribute({
Name: "custom:role",
Value: role,
}),
new CognitoUserAttribute({
Name: "custom:student_id",
Value: studentId,
}),
];

userPool.signUp(username, password, attributeList, [], (err, result) => {
if (err) {
onFailure(err);
return;
}
if (result) {
onSuccess(result);
}
});
}

export function authenticateCognitoUser(
email: string,
password: string
): Promise<AuthResponse> {
const authenticationDetails = new AuthenticationDetails({
Username: email,
Password: password,
});

const userData = {
Username: email,
Pool: userPool,
};

const cognitoUser = new CognitoUser(userData);
return new Promise((resolve, reject) => {
cognitoUser.authenticateUser(authenticationDetails, {
onSuccess: (result) => {
const idToken = result.getIdToken().getJwtToken();
const claims = result.getIdToken().decodePayload();

// custom 속성 가져오기
const name = claims["custom:name"] || "";
const role = claims["custom:role"] || "";
const studentId = claims["custom:student_id"] || "";

resolve({
idToken,
name,
role,
studentId,
});
},
onFailure: (err) => {
reject(err);
},
});
});
}

export function verifyEmail({
username,
code,
}: VerifyEmailParams): Promise<void> {
const userData = {
Username: username,
Pool: userPool,
};

const cognitoUser = new CognitoUser(userData);

return new Promise((resolve, reject) => {
cognitoUser.confirmRegistration(code, true, (err, result) => {
if (err) {
reject(err);
return;
}
resolve();
});
});
}
28 changes: 28 additions & 0 deletions src/api/client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import axios from "axios";
import Cookies from "js-cookie";

const apiUrl = process.env.NEXT_PUBLIC_API_URL;

const apiClient = axios.create({
baseURL: apiUrl,
headers: {
"Content-Type": "application/json",
},
});

apiClient.interceptors.request.use(
async (config) => {
const token = Cookies.get("idToken");
console.log("🚀 ~ file: client.ts:16 ~ token:", token);
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
(error) => {
console.log("error", error);
return Promise.reject(error);
}
);

export default apiClient;
12 changes: 12 additions & 0 deletions src/api/endpoints.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
export const API_ENDPOINTS = {
STUDENT: {
SERVICE: (studentId: string) => `/service/${studentId}`,
POD: (studentId: string) => `/pod/${studentId}`,
DEPLOY: (studentId: string) => `/deploy/${studentId}`,
},
PROFESSOR: {
CHECK: (professorId: string) => `/class/${professorId}/pod`,
CREATE: (professorId: string) => `/class/${professorId}/create`,
DELETE: (professorId: string) => `/class/${professorId}/delete`,
},
};
44 changes: 44 additions & 0 deletions src/api/hooks/useProfessor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import apiClient from "@/api/client";
import { API_ENDPOINTS } from "@/api/endpoints";
import { useQuery, useMutation } from "@tanstack/react-query";

const fetchCheck = async (professorId: string) => {
const { data } = await apiClient.get(
API_ENDPOINTS.PROFESSOR.CHECK(professorId)
);
return data;
};

export const useFetchCheck = (professorId: string) => {
return useQuery({
queryKey: ["fetchDeploy", professorId],
queryFn: () => fetchCheck(professorId),
});
};

interface ClassCreationData {
className: string;
studentIds: string[];
options: { [key: string]: string };
command: string[];
customScript: string;
}

interface CreateClassArgs {
professorId: string;
classData: ClassCreationData;
}

const createClass = async ({ professorId, classData }: CreateClassArgs) => {
const { data } = await apiClient.post(
API_ENDPOINTS.PROFESSOR.CREATE(professorId),
classData
);
return data;
};

export const useCreateClass = () => {
return useMutation({
mutationFn: createClass,
});
};
41 changes: 41 additions & 0 deletions src/api/hooks/useStudent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import apiClient from "@/api/client";
import { API_ENDPOINTS } from "@/api/endpoints";
import { useQuery } from "@tanstack/react-query";

const fetchService = async (studentId: string) => {
const { data } = await apiClient.get(
API_ENDPOINTS.STUDENT.SERVICE(studentId)
);
return data;
};

const fetchPod = async (studentId: string) => {
const { data } = await apiClient.get(API_ENDPOINTS.STUDENT.POD(studentId));
return data;
};

const fetchDeploy = async (studentId: string) => {
const { data } = await apiClient.get(API_ENDPOINTS.STUDENT.DEPLOY(studentId));
return data;
};

export const useFetchService = (studentId: string) => {
return useQuery({
queryKey: ["fetchService", studentId],
queryFn: () => fetchService(studentId),
});
};

export const useFetchPod = (studentId: string) => {
return useQuery({
queryKey: ["fetchPod", studentId],
queryFn: () => fetchPod(studentId),
});
};

export const useFetchDeploy = (studentId: string) => {
return useQuery({
queryKey: ["fetchDeploy", studentId],
queryFn: () => fetchDeploy(studentId),
});
};
11 changes: 2 additions & 9 deletions src/app/(main)/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { ReactNode, useEffect, useState } from "react";
import styles from "./layout.module.css";
import { useAuthStore } from "@/store/authStore";
import Link from "next/link";
import { redirect, usePathname } from "next/navigation";
import { usePathname } from "next/navigation";
import Image from "next/image";
import LeftArrowIcon from "/public/icons/LeftArrow.svg";
import HamburgerIcon from "/public/icons/Hamburger.svg";
Expand All @@ -15,15 +15,8 @@ interface LayoutProps {

export default function Layout({ children }: LayoutProps) {
const user = useAuthStore((state) => state.user);
const isLoggedIn = useAuthStore((state) => state.isLoggedIn);
const isLoading = useAuthStore((state) => state.isLoading);
const [isSidebarOpen, setSidebarOpen] = useState<boolean>(true);

useEffect(() => {
if (!isLoggedIn && !isLoading) {
redirect("/onboarding");
}
}, [isLoggedIn, isLoading]);
const [isSidebarOpen, setSidebarOpen] = useState<boolean>(true);

const navigation = usePathname();

Expand Down
57 changes: 57 additions & 0 deletions src/app/(main)/test/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
"use client";

import React from "react";
import {
useFetchService,
useFetchPod,
useFetchDeploy,
} from "@/api/hooks/useStudent";

const StudentInfoComponent = () => {
const studentId = "2019312430";
const {
data: serviceData,
isLoading: isLoadingService,
error: errorService,
} = useFetchService(studentId);
const {
data: podData,
isLoading: isLoadingPod,
error: errorPod,
} = useFetchPod(studentId);
const {
data: deployData,
isLoading: isLoadingDeploy,
error: errorDeploy,
} = useFetchDeploy(studentId);

// 로딩 상태 처리
if (isLoadingService || isLoadingPod || isLoadingDeploy) {
return <div>Loading...</div>;
}

// 에러 상태 처리
if (errorService || errorPod || errorDeploy) {
return <div>Error loading data. Please try again later.</div>;
}

return (
<div>
<h1>Student Information</h1>
<div>
<h2>Service Details</h2>
<p>{JSON.stringify(serviceData, null, 2)}</p>
</div>
<div>
<h2>Pod Details</h2>
<p>{JSON.stringify(podData, null, 2)}</p>
</div>
<div>
<h2>Deployment Details</h2>
<p>{JSON.stringify(deployData, null, 2)}</p>
</div>
</div>
);
};

export default StudentInfoComponent;
Loading

0 comments on commit cc10f67

Please sign in to comment.