Skip to content

Commit

Permalink
Merge pull request #12 from hy2n/master
Browse files Browse the repository at this point in the history
관리자 기능 추가
  • Loading branch information
0xC0FFE2 authored Oct 14, 2024
2 parents eb5ad05 + 2fe33bf commit 1f4275b
Show file tree
Hide file tree
Showing 12 changed files with 567 additions and 89 deletions.
28 changes: 24 additions & 4 deletions src/App.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
import React from 'react';
import { BrowserRouter as Router, Route, Routes } from 'react-router-dom';
import Header from './components/Header'; // Adjust the path if necessary
import Home from './pages/Home'; // Adjust the path if necessary
import Articles from './pages/Articles'; // Adjust the path if necessary
import Header from './components/Header';
import Home from './pages/Home';
import Articles from './pages/Articles';
import ArticleView from './pages/ArticleView'
import Editor from './pages/Editor'
import Footer from './components/Footer'
import FileUploader from './pages/Uploader';

import './index.css'
import AdminArticleManager from './pages/Admin';
import ProtectedRoute from './components/ProctedRoute';
import OAuthTokenHandler from './pages/AuthTokenHandler'

function App() {
return (
<Router>
Expand All @@ -19,7 +23,23 @@ function App() {
<Route path="/" element={<Home />} />
<Route path="/article_list" element={<Articles />} />
<Route path="/article/:id" element={<ArticleView />} />
<Route path="/editor/:id" element={<Editor />} />
<Route path="/oauth_client" element={<OAuthTokenHandler/>} />
<Route
path="/editor/:id"
element={
<ProtectedRoute>
<Editor />
</ProtectedRoute>
}
/>
<Route
path="/admin"
element={
<ProtectedRoute>
<AdminArticleManager />
</ProtectedRoute>
}
/>
</Routes>
<Footer />
</div>
Expand Down
Binary file modified src/assets/profile.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
14 changes: 10 additions & 4 deletions src/components/CategoryPosts.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import React, { useState, useRef, useEffect } from 'react';
import PostCard from './PostCard';
import LoadingSpinner from './LoadingIcon';
import { API_BASE_URL } from '../__CONF__';
import LoadingFailSpinner from './ErrorIcon';

function CategoryPosts({ size, mode }) {
const [selectedCategory, setSelectedCategory] = useState('전체');
Expand All @@ -10,6 +11,7 @@ function CategoryPosts({ size, mode }) {
const [currentPage, setCurrentPage] = useState(1);
const [totalPages, setTotalPages] = useState(1);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const categoryRef = useRef(null);

const defaultCategories = ['전체', '백엔드', '프론트엔드', 'CI/CD', '개발 지식'];
Expand All @@ -25,7 +27,7 @@ function CategoryPosts({ size, mode }) {

const fetchCategories = async () => {
try {
const response = await fetch(API_BASE_URL+'/categories');
const response = await fetch(API_BASE_URL + '/categories');
const data = await response.json();
setCategories(['전체', ...data.map(category => category.category_name)]);
} catch (error) {
Expand All @@ -38,15 +40,16 @@ function CategoryPosts({ size, mode }) {
try {
let url;
if (selectedCategory === '전체') {
url = API_BASE_URL+`/articles?page=${currentPage}&size=${size}`;
url = API_BASE_URL + `/articles?page=${currentPage}&size=${size}`;
} else {
url = API_BASE_URL+`/search/categories/${selectedCategory}?page=${currentPage}&size=${size}`;
url = API_BASE_URL + `/search/categories/${selectedCategory}?page=${currentPage}&size=${size}`;
}
const response = await fetch(url);
const data = await response.json();
setPosts(data.articles);
setTotalPages(data.totalPage);
} catch (error) {
setError('카테고리를 불러오는 데 실패했습니다.');
console.error('Failed to fetch posts:', error);
} finally {
setLoading(false);
Expand All @@ -67,6 +70,9 @@ function CategoryPosts({ size, mode }) {
setCurrentPage(1);
};

if (loading) return <LoadingSpinner/>;
if (error) return <LoadingFailSpinner message='서버 통신 문제가 발생했어요'/>;

return (
<div>
<h2 className="text-2xl font-bold mt-6 mb-6">카테고리별 게시물</h2>
Expand Down Expand Up @@ -94,7 +100,7 @@ function CategoryPosts({ size, mode }) {
</div>

{loading ? (
<LoadingSpinner/>
<LoadingSpinner />
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
{posts.map(post => (
Expand Down
19 changes: 19 additions & 0 deletions src/components/ErrorIcon.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import React from 'react';

const LoadingFailSpinner = ({ message = '알 수 없는 문제가 발생했어요' }) => {
return (
<div className="flex flex-col items-center justify-center h-[300px]">
<div className="relative w-20 h-20 mb-6">
<div className="absolute inset-0 bg-red-500 rounded-full"></div>
<div className="absolute inset-0 flex items-center justify-center">
<svg className="w-10 h-10 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</div>
</div>
<p className="text-lg font-medium text-red-600 mb-4">{message}</p>
</div>
);
};

export default LoadingFailSpinner;
27 changes: 11 additions & 16 deletions src/components/LoadingIcon.js
Original file line number Diff line number Diff line change
@@ -1,26 +1,21 @@
import React from 'react';

const Loader = ({ size = 'md' }) => {
const LoadingSpinner = ({ size = 'md', message = '로딩 중이에요!' }) => {
const sizeClasses = {
sm: 'w-5 h-5 border-2',
md: 'w-8 h-8 border-3',
lg: 'w-12 h-12 border-4',
sm: 'w-12 h-12',
md: 'w-16 h-16',
lg: 'w-20 h-20',
};

return (
<div className="flex justify-center items-center">
<div
className={`${sizeClasses[size]} border-t-blue-500 border-r-blue-500 border-b-blue-400 border-l-blue-200 rounded-full animate-spin`}
></div>
<div className="flex flex-col items-center justify-center h-[300px]">
<div className={`relative ${sizeClasses[size]}`}>
<div className="absolute inset-0 bg-blue-500 rounded-full opacity-20"></div>
<div className="absolute inset-0 border-4 border-blue-500 border-t-transparent rounded-full animate-spin"></div>
</div>
<p className="mt-6 text-lg font-medium text-blue-600">{message}</p>
</div>
);
};

export default function LoadingSpinner() {
return (
<div className="flex flex-col items-center justify-center h-screen bg-gray-100">
<Loader size="lg" />
<p className="mt-4 text-gray-600 font-medium">🚗 로딩 중이에요!</p>
</div>
);
}
export default LoadingSpinner;
22 changes: 22 additions & 0 deletions src/components/ProctedRoute.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import React from 'react';
import { Navigate, useLocation } from 'react-router-dom';
import AuthHook from '../hook/AuthHook';
import LoadingSpinner from './LoadingIcon';

const ProtectedRoute = ({ children }) => {
const { isAuthenticated, isLoading } = AuthHook();
const location = useLocation();

if (isLoading) {
return <LoadingSpinner />;
}

if (!isAuthenticated) {
window.location.href = `https://id.nanu.cc/oauth?app_id=6b3bac76-bde8-4681-8c55-0e915d1c03b6&app_name=%EB%8F%99%ED%98%84%20%EA%B8%B0%EC%88%A0%20%EB%B8%94%EB%A1%9C%EA%B7%B8&redirect_uri=https://donghyun.cc/oauth_client`;
return null;
}

return children;
};

export default ProtectedRoute;
3 changes: 2 additions & 1 deletion src/components/RecentPosts.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import React, { useEffect, useState } from 'react';
import PostCard from './PostCard';
import LoadingSpinner from './LoadingIcon';
import { API_BASE_URL } from '../__CONF__';
import LoadingFailSpinner from './ErrorIcon';

function RecentPosts({ size }) {
const [recents, setRecents] = useState([]);
Expand All @@ -28,7 +29,7 @@ function RecentPosts({ size }) {
};

if (loading) return <LoadingSpinner/>;
if (error) return <div>Error: {error}</div>;
if (error) return <LoadingFailSpinner message='서버 통신 문제가 발생했어요'/>;

return (
<div>
Expand Down
46 changes: 46 additions & 0 deletions src/hook/AuthHook.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { useState, useEffect, useCallback } from 'react';
import OAuthSDK from '../nanuid_auth_sdk'

const useAuthCheck = () => {
const [isAuthenticated, setIsAuthenticated] = useState(false);
const [isLoading, setIsLoading] = useState(true);
const [token, setToken] = useState(null);

const refreshTokenAndUpdate = useCallback(async () => {
const newAccessToken = await OAuthSDK.refreshToken();
if (newAccessToken) {
setToken(newAccessToken);
setIsAuthenticated(true);
} else {
setToken(null);
setIsAuthenticated(false);
}
return newAccessToken;
}, []);

useEffect(() => {
const checkAuth = async () => {
setIsLoading(true);
const accessToken = OAuthSDK.getAccessToken();

if (accessToken) {
const isExpired = await OAuthSDK.isTokenExpired(accessToken);
if (isExpired) {
await refreshTokenAndUpdate();
} else {
setIsAuthenticated(true);
setToken(accessToken);
}
} else {
await refreshTokenAndUpdate();
}
setIsLoading(false);
};

checkAuth();
}, [refreshTokenAndUpdate]);

return { isAuthenticated, isLoading, token, refreshTokenAndUpdate };
};

export default useAuthCheck;
73 changes: 73 additions & 0 deletions src/nanuid_auth_sdk.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import axios from 'axios';
const AUTH_BASE_URL = 'https://auth.nanu.cc';

export const OAuthSDK = {
getAccessToken: () => {
return document.cookie.replace(/(?:(?:^|.*;\s*)ACCESS\s*=\s*([^;]*).*$)|^.*$/, "$1");
},

setAccessToken: (token, days = 1) => {
const expires = new Date(Date.now() + days * 864e5).toUTCString();
document.cookie = `ACCESS=${encodeURIComponent(token)}; expires=${expires}; path=/`;
},

getRefreshToken: () => {
return localStorage.getItem('REFRESH');
},

setRefreshToken: (token) => {
localStorage.setItem('REFRESH', token);
},

isTokenExpired: async (token) => {
try {
const response = await axios.post(`${AUTH_BASE_URL}/api/mypage`, null, {
headers: { 'Authorization': `Bearer ${token}` }
});
return response.status !== 200;
} catch (error) {
console.error('Error validating token:', error);
return true;
}
},

refreshToken: async () => {
const refreshToken = OAuthSDK.getRefreshToken();
if (refreshToken) {
try {
const response = await axios.post(`${AUTH_BASE_URL}/api/oauth/refresh`, { refresh_token: refreshToken });
const newAccessToken = response.data.access_token;
OAuthSDK.setAccessToken(newAccessToken);
return newAccessToken;
} catch (error) {
console.error('Token refresh failed:', error);
return null;
}
}
return null;
},

checkAuthentication: async () => {
const accessToken = OAuthSDK.getAccessToken();
if (accessToken) {
const isExpired = await OAuthSDK.isTokenExpired(accessToken);
if (isExpired) {
return await OAuthSDK.refreshToken();
}
return accessToken;
}
return await OAuthSDK.refreshToken();
},

logout: () => {
document.cookie = 'ACCESS=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;';
localStorage.removeItem('REFRESH');
},

setTokens: (accessToken, refreshToken, days = 1) => {
OAuthSDK.setAccessToken(accessToken, days);
OAuthSDK.setRefreshToken(refreshToken);
}
};

export default OAuthSDK;
Loading

0 comments on commit 1f4275b

Please sign in to comment.