const express = require("express"); const app = express(); const path = require("path"); const fetch = require("node-fetch"); const cron = require("node-cron"); const fs = require("fs"); app.use(express.json()); // JSON 본문 파싱 // 토큰 저장소 (실제 환경에서는 데이터베이스 사용 권장) let tokenStorage = { accessToken: null, refreshToken: null, expiresAt: null, userInfo: null }; // 토큰을 파일에 저장하는 함수 function saveTokensToFile() { try { fs.writeFileSync('./tokens.json', JSON.stringify(tokenStorage, null, 2)); console.log('✅ 토큰이 파일에 저장되었습니다.'); } catch (error) { console.error('❌ 토큰 저장 실패:', error); } } // 파일에서 토큰을 로드하는 함수 function loadTokensFromFile() { try { if (fs.existsSync('./tokens.json')) { const data = fs.readFileSync('./tokens.json', 'utf8'); tokenStorage = JSON.parse(data); console.log('✅ 저장된 토큰을 로드했습니다.'); } } catch (error) { console.error('❌ 토큰 로드 실패:', error); } } // 액세스 토큰 갱신 함수 async function refreshAccessToken() { if (!tokenStorage.refreshToken) { console.log('⚠️ 리프레시 토큰이 없습니다.'); return false; } try { const response = await fetch('https://oauth2.googleapis.com/token', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, body: new URLSearchParams({ client_id: "16435018183-9a880bertda0en85387ge8f8mgsves71.apps.googleusercontent.com", client_secret: process.env.CLIENT_SECRET, // 실제 클라이언트 시크릿으로 변경 필요 refresh_token: tokenStorage.refreshToken, grant_type: 'refresh_token' }) }); const data = await response.json(); if (data.access_token) { tokenStorage.accessToken = data.access_token; tokenStorage.expiresAt = Date.now() + (data.expires_in * 1000); saveTokensToFile(); console.log('🔄 액세스 토큰이 갱신되었습니다:', new Date().toLocaleString()); return true; } else { console.error('❌ 토큰 갱신 실패:', data); return false; } } catch (error) { console.error('❌ 토큰 갱신 중 오류:', error); return false; } } app.get("/", (req, res) => { const clientId = "16435018183-9a880bertda0en85387ge8f8mgsves71.apps.googleusercontent.com"; // 반드시 수정 const redirectUri = "https://google-oauth-access-token-whs.hako.li/callback"; const authUrl = "https://accounts.google.com/o/oauth2/v2/auth?" + `client_id=${clientId}` + `&redirect_uri=${redirectUri}` + `&response_type=code` + // code로 변경하여 리프레시 토큰도 받을 수 있도록 `&scope=email%20profile` + `&access_type=offline` + // 리프레시 토큰을 받기 위해 필요 `&prompt=consent`; // 매번 동의 화면을 표시하여 리프레시 토큰 확보 res.redirect(authUrl); }); // Authorization Code를 Access Token으로 교환하는 엔드포인트 app.get("/exchange", async (req, res) => { const code = req.query.code; if (!code) { return res.status(400).send("Authorization code가 필요합니다."); } try { const response = await fetch('https://oauth2.googleapis.com/token', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, body: new URLSearchParams({ client_id: "16435018183-9a880bertda0en85387ge8f8mgsves71.apps.googleusercontent.com", client_secret: "YOUR_CLIENT_SECRET", // 실제 클라이언트 시크릿으로 변경 필요 code: code, grant_type: 'authorization_code', redirect_uri: "https://google-oauth-access-token-whs.hako.li/callback" }) }); const tokenData = await response.json(); if (tokenData.access_token) { // 사용자 정보 가져오기 const userResponse = await fetch("https://www.googleapis.com/oauth2/v3/userinfo", { headers: { Authorization: `Bearer ${tokenData.access_token}`, }, }); const userInfo = await userResponse.json(); // 토큰 저장 tokenStorage.accessToken = tokenData.access_token; tokenStorage.refreshToken = tokenData.refresh_token; tokenStorage.expiresAt = Date.now() + (tokenData.expires_in * 1000); tokenStorage.userInfo = userInfo; saveTokensToFile(); console.log("📧 Email:", userInfo.email); console.log("👤 Name:", userInfo.name); console.log("🔑 Access Token:", tokenData.access_token); console.log("🔄 Refresh Token:", tokenData.refresh_token ? "받음" : "없음"); res.send({ success: true, timestamp: new Date().toISOString(), user: { email: userInfo.email, name: userInfo.name, picture: userInfo.picture }, tokens: { accessToken: tokenData.access_token, refreshToken: tokenData.refresh_token || null, tokenType: tokenData.token_type, expiresIn: tokenData.expires_in, expiresAt: new Date(tokenStorage.expiresAt).toISOString(), scope: tokenData.scope }, storage: { saved: true, hasRefreshToken: !!tokenData.refresh_token, autoRefreshEnabled: !!tokenData.refresh_token } }); } else { console.error("❌ 토큰 교환 실패:", tokenData); res.status(400).send("토큰 교환 실패"); } } catch (err) { console.error("❌ Error:", err); res.status(500).send("Error"); } }); // Access Token 수신용 엔드포인트 (기존 방식도 유지) app.get("/token", async (req, res) => { const token = req.query.access_token; try { const response = await fetch("https://www.googleapis.com/oauth2/v3/userinfo", { headers: { Authorization: `Bearer ${token}`, }, }); const userInfo = await response.json(); console.log("Email:", userInfo.email); console.log("Name:", userInfo.name); console.log("Access Token:", token); res.send({ email: userInfo.email, name: userInfo.name, token: token }); } catch (err) { console.error("❌ Error:", err); res.status(500).send("Error"); } }); // 현재 저장된 토큰 정보 확인 엔드포인트 app.get("/status", (req, res) => { const isExpired = tokenStorage.expiresAt ? Date.now() > tokenStorage.expiresAt : true; const timeUntilExpiry = tokenStorage.expiresAt ? Math.max(0, tokenStorage.expiresAt - Date.now()) : 0; res.send({ timestamp: new Date().toISOString(), tokens: { hasAccessToken: !!tokenStorage.accessToken, hasRefreshToken: !!tokenStorage.refreshToken, isExpired: isExpired, expiresAt: tokenStorage.expiresAt ? new Date(tokenStorage.expiresAt).toISOString() : null, timeUntilExpiryMs: timeUntilExpiry, timeUntilExpiryMinutes: Math.round(timeUntilExpiry / 1000 / 60) }, user: tokenStorage.userInfo ? { email: tokenStorage.userInfo.email, name: tokenStorage.userInfo.name, picture: tokenStorage.userInfo.picture } : null, autoRefresh: { enabled: !!tokenStorage.refreshToken, cronSchedule: "*/30 * * * *", nextCheck: "Every 30 minutes" }, server: { uptime: process.uptime(), pid: process.pid, version: process.version } }); }); // 수동으로 토큰 갱신하는 엔드포인트 app.post("/refresh", async (req, res) => { const beforeRefresh = { accessToken: !!tokenStorage.accessToken, expiresAt: tokenStorage.expiresAt ? new Date(tokenStorage.expiresAt).toISOString() : null }; const success = await refreshAccessToken(); const afterRefresh = { accessToken: !!tokenStorage.accessToken, expiresAt: tokenStorage.expiresAt ? new Date(tokenStorage.expiresAt).toISOString() : null }; res.send({ timestamp: new Date().toISOString(), success: success, message: success ? "토큰이 성공적으로 갱신되었습니다." : "토큰 갱신에 실패했습니다.", before: beforeRefresh, after: afterRefresh, hasRefreshToken: !!tokenStorage.refreshToken, user: tokenStorage.userInfo ? { email: tokenStorage.userInfo.email, name: tokenStorage.userInfo.name } : null }); }); app.get("/callback", (req, res) => { res.sendFile(path.join(__dirname, "callback.html")); }); // 서버 시작 시 저장된 토큰 로드 loadTokensFromFile(); // 크론 작업: 매 30분마다 토큰 만료 확인 및 갱신 cron.schedule('*/30 * * * *', async () => { console.log('🕐 토큰 만료 확인 중...', new Date().toLocaleString()); if (!tokenStorage.accessToken) { console.log('⚠️ 저장된 액세스 토큰이 없습니다.'); return; } // 만료 10분 전에 갱신 const timeUntilExpiry = tokenStorage.expiresAt - Date.now(); const tenMinutes = 10 * 60 * 1000; if (timeUntilExpiry <= tenMinutes) { console.log('⏰ 토큰이 곧 만료됩니다. 갱신을 시도합니다...'); await refreshAccessToken(); } else { console.log('✅ 토큰이 아직 유효합니다. 만료까지:', Math.round(timeUntilExpiry / 1000 / 60), '분'); } }); // 크론 작업: 매일 오전 9시에 토큰 상태 리포트 cron.schedule('0 9 * * *', () => { console.log('📊 일일 토큰 상태 리포트:', new Date().toLocaleString()); console.log('- 액세스 토큰:', tokenStorage.accessToken ? '있음' : '없음'); console.log('- 리프레시 토큰:', tokenStorage.refreshToken ? '있음' : '없음'); console.log('- 사용자:', tokenStorage.userInfo?.email || '없음'); if (tokenStorage.expiresAt) { const isExpired = Date.now() > tokenStorage.expiresAt; console.log('- 만료 상태:', isExpired ? '만료됨' : '유효'); console.log('- 만료 시간:', new Date(tokenStorage.expiresAt).toLocaleString()); } }); console.log('⏰ 크론 작업이 설정되었습니다:'); console.log('- 매 30분마다 토큰 만료 확인'); console.log('- 매일 오전 9시 상태 리포트'); const PORT = 39090; app.listen(PORT, () => { console.log(`✅ Server running at http://localhost:${PORT}`); console.log(`📖 사용 가능한 엔드포인트:`); console.log(` GET / - OAuth 인증 시작`); console.log(` GET /exchange - Authorization Code를 토큰으로 교환`); console.log(` GET /token - 기존 방식 토큰 수신`); console.log(` GET /status - 현재 토큰 상태 확인`); console.log(` POST /refresh - 수동 토큰 갱신`); console.log(` GET /callback - OAuth 콜백`); });