diff --git a/callback.html b/callback.html index 3c50cfd..bca5d6a 100644 --- a/callback.html +++ b/callback.html @@ -44,7 +44,7 @@ if (result.success) { statusDiv.className = "status success"; - statusDiv.innerHTML = `

✅ 인증 성공!

accessToken: ${result.accessToken}

${JSON.stringify(result, null, 2)}
`; + statusDiv.innerHTML = `

✅ 인증 성공!

accessToken: ${result.tokens.accessToken}

${JSON.stringify(result, null, 2)}
`; } else { statusDiv.className = "status error"; statusDiv.innerHTML = `

❌ 토큰 교환 실패

${JSON.stringify(result, null, 2)}
`; diff --git a/server.js b/server.js index 64dcd83..230e0fe 100644 --- a/server.js +++ b/server.js @@ -2,177 +2,13 @@ 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.static("public")); // public 폴더 내 정적 파일 제공 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: process.env.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; +// Access Token 수신용 엔드포인트 +app.post("/token", async (req, res) => { + const token = req.body.access_token; try { const response = await fetch("https://www.googleapis.com/oauth2/v3/userinfo", { headers: { @@ -184,132 +20,18 @@ app.get("/token", async (req, res) => { console.log("Name:", userInfo.name); console.log("Access Token:", token); - res.send({ - email: userInfo.email, - name: userInfo.name, - token: token - }); + res.send("Token received!"); } 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")); + res.sendFile(path.join(__dirname, "callback/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 콜백`); }); diff --git a/server.js.bak b/server.js.bak new file mode 100644 index 0000000..508cef8 --- /dev/null +++ b/server.js.bak @@ -0,0 +1,315 @@ +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: process.env.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 콜백`); +});