Skip to content

Commit 5c3b7fa

Browse files
committed
feature: token refresh 전용 batch 추가
1 parent 1bfc3f1 commit 5c3b7fa

File tree

10 files changed

+250
-15
lines changed

10 files changed

+250
-15
lines changed

README.md

Lines changed: 45 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,54 @@
22

33
> velog dashboard
44
5-
## Getting Started
5+
## 1. HOW TO USE
66

7-
1.
87

9-
## 사용하는 velog graphQL list
8+
9+
10+
## 2. token 활용 사항과 사용하는 velog graphQL list
11+
12+
### 1) token 활용
13+
14+
- 입력한 token은 atlas cloud 로 제공되는 mongodb DBMS에 보관됩니다. network ip accesss 부터, auth 까지 모두 제한 환경이니 보안적 이슈는 mongodb사에 있습니다.
15+
- 즉, mongodb cloud가 털리는 이슈까지가 아니라면 token이 탈취될 염려가 없습니다.
16+
- 만약 그럼에도 불구하고 token 탈취의 의심이 있다면, token의 자체 refresh를 멈추고 폐기에 들어갑니다.
17+
- ***식별이 가능한 개인정보를 수집하지 않습니다.*** 수집하는 정보는 velog id (email) 이 unique 구분값을 위해 저장되며, 이는 token을 가지고 있는 제 3자에게 유출되지 않습니다.
18+
- 마케팅 용도로 사용하지 않습니다. 만약 이메일 전송이 필요하다고 판단 된다면, 개개인에게 공지성 메일 이후 동의를 받고 진행하게 됩니다.
19+
20+
### 2) 사용하는 velog graphQL
1021

1122
1. `currentUser`
12-
- velog url를 얻기 위해 사용합니다. `username` 만 가져오며, 그 외 값은 (이메일 등) 가져오지 않고 저장하지 않습니다.
23+
- velog url를 얻기 위해 사용합니다. `username` 만 가져오며, 그 외 값은 가져오지 않고 누구에게도 제공하지 않습니다.
24+
- 토큰이 필요한 API 입니다.
25+
26+
2. `Posts`
27+
- 해당 유저의 모든 게시글을 가져오기 위해 사용합니다. token이 필요없는 API 이며, 수정, 생성, 삭제 등의 모든 기능은 사용하지 않습니다. 오직 "READ" 만 사용합니다.
28+
- 토큰이 필요하지 않은 API 입니다.
29+
30+
3. `getStats`
31+
- 특정 post uuid 값을 기반으로 통계 데이터를 모두 가져옵니다.
32+
- 토큰이 필요한 API 입니다.
33+
34+
---
35+
36+
## 3. Getting Started
37+
38+
1. 먼저 mongodb atlas connction info가 필요합니다. - https://www.mongodb.com/atlas/database
39+
2. `backend` 디렉토리로 이동합니다.
40+
3. `.env.sample` file을 참조해 `.env` 를 만듭니다.
41+
4. 해당 디렉토리의 root에서 `yarn` 으로 필요한 모든 라이브러리를 설치하고 `yarn start` 로 가동합니다.
42+
5. 이제 static file을 열면 됩니다 -> `nginx > pages > index > index.html` 를 더블클릭으로 브라우저에서 열어주세요!
43+
6. 로그인을 통해 정상적으로 유저 등록이되는지 체크합니다!
44+
7. 이제 `worker` 로 이동해서 poetry를 활요해 필요한 라이브러리를 세팅합니다. - [poetry 간단 사용법](https://velog.io/@qlgks1/python-poetry-%EC%84%A4%EC%B9%98%EB%B6%80%ED%84%B0-project-initializing-%ED%99%9C%EC%9A%A9%ED%95%98%EA%B8%B0)
45+
8. `.env.sample` file을 참조해 `.env` 를 만듭니다.
46+
9. `main.py` 를 러닝해서 데이터 스크레이핑을 확인합니다. 로깅은 console stream과 file stream 모두 존재합니다!
47+
10. `token_refresh.py` 로 저장된 user token을 refresh 해줍니다!
48+
1349

14-
2.
50+
## 4. 참조
1551

16-
https://github.com/inyutin/aiohttp_retry
52+
- 프로젝트 진행 기록 - velog :
53+
- [mongodb cloud - atlas](https://www.mongodb.com/atlas/database)
54+
- [poetry 간단 사용법](https://velog.io/@qlgks1/python-poetry-%EC%84%A4%EC%B9%98%EB%B6%80%ED%84%B0-project-initializing-%ED%99%9C%EC%9A%A9%ED%95%98%EA%B8%B0)
55+
- [aiohttp_retry](https://github.com/inyutin/aiohttp_retry)

backend/src/services/userInfoServices.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import UserInfo from "../models/userInfo.js";
44

5-
const setAuthCookie = (res, accessToken, refreshToken) => {
5+
const setAuthCookie = async (res, accessToken, refreshToken) => {
66
res.cookie("accessToken", accessToken);
77
res.cookie("refreshToken", refreshToken);
88
};
@@ -16,7 +16,7 @@ export const signUpORsignIn = async (req, res) => {
1616

1717
// 존재하면?
1818
if (userChkOne) {
19-
setAuthCookie(res, accessToken, refreshToken);
19+
await setAuthCookie(res, accessToken, refreshToken);
2020
return res.status(200).json({
2121
message: "User logined successfully",
2222
user: userChkOne
@@ -32,7 +32,7 @@ export const signUpORsignIn = async (req, res) => {
3232
// user token update
3333
const updateResult = await UserInfo.updateTokenByuserId(userChkTwo.userId, accessToken, refreshToken);
3434
if (updateResult.matchedCount && updateResult.modifiedCount) {
35-
setAuthCookie(res, accessToken, refreshToken);
35+
await setAuthCookie(res, accessToken, refreshToken);
3636
return res.status(200).json({
3737
message: "User logined and updated successfully",
3838
user: userChkTwo
@@ -47,7 +47,7 @@ export const signUpORsignIn = async (req, res) => {
4747

4848
// 그래도 존재하지 않으면 신규 가입 -> 만료된 토큰일 가능성 있음, 그때 error
4949
const newUser = await UserInfo.createUser(accessToken, refreshToken);
50-
setAuthCookie(res, accessToken, refreshToken);
50+
await setAuthCookie(res, accessToken, refreshToken);
5151
return res.status(201).json({
5252
message: "User created successfully",
5353
user: newUser

nginx/pages/dashboard/index.html

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,47 @@
1010
href="https://fonts.googleapis.com/css2?family=Nanum+Gothic:wght@400;700&family=Nunito+Sans:ital,wght@1,400;1,900&display=swap"
1111
rel="stylesheet"
1212
/>
13+
<!-- Channel Talk -->
14+
<script>
15+
(function () {
16+
var w = window;
17+
if (w.ChannelIO) {
18+
return w.console.error("ChannelIO script included twice.");
19+
}
20+
var ch = function () {
21+
ch.c(arguments);
22+
};
23+
ch.q = [];
24+
ch.c = function (args) {
25+
ch.q.push(args);
26+
};
27+
w.ChannelIO = ch;
28+
function l() {
29+
if (w.ChannelIOInitialized) {
30+
return;
31+
}
32+
w.ChannelIOInitialized = true;
33+
var s = document.createElement("script");
34+
s.type = "text/javascript";
35+
s.async = true;
36+
s.src = "https://cdn.channel.io/plugin/ch-plugin-web.js";
37+
var x = document.getElementsByTagName("script")[0];
38+
if (x.parentNode) {
39+
x.parentNode.insertBefore(s, x);
40+
}
41+
}
42+
if (document.readyState === "complete") {
43+
l();
44+
} else {
45+
w.addEventListener("DOMContentLoaded", l);
46+
w.addEventListener("load", l);
47+
}
48+
})();
49+
50+
ChannelIO("boot", {
51+
pluginKey: "9bf7ef35-5a3a-410a-b69a-67ef1fd9ed6b",
52+
});
53+
</script>
1354
<!-- Sweetalert2 -->
1455
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
1556
<!-- ChartJS, date adpater -->

nginx/pages/dashboard/index.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,9 @@ const updateUserInfo = async () => {
130130
<span>${res.userInfo.lastScrapingAttemptResult}</span>
131131
</div>
132132
`;
133+
134+
// 저장된 로컬스토리지도 업데이트 (토큰 리프레싱때문)
135+
localStorage.setItem("userInfo", JSON.stringify(res.userInfo));
133136
return res;
134137
};
135138

@@ -272,7 +275,7 @@ const init = () => {
272275

273276
// polling event 들 등록하기
274277
updateUserInfo();
275-
polling(updateUserInfo, 60000, (res) => { return false });
278+
polling(updateUserInfo, 30000, (res) => { return false });
276279

277280
updateUserStats();
278281
polling(updateUserStats, 60000, (res) => { return false });

nginx/pages/index/index.html

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,47 @@
1010
href="https://fonts.googleapis.com/css2?family=Nanum+Gothic:wght@400;700&family=Nunito+Sans:ital,wght@1,400;1,900&display=swap"
1111
rel="stylesheet"
1212
/>
13+
<!-- Channel Talk -->
14+
<script>
15+
(function () {
16+
var w = window;
17+
if (w.ChannelIO) {
18+
return w.console.error("ChannelIO script included twice.");
19+
}
20+
var ch = function () {
21+
ch.c(arguments);
22+
};
23+
ch.q = [];
24+
ch.c = function (args) {
25+
ch.q.push(args);
26+
};
27+
w.ChannelIO = ch;
28+
function l() {
29+
if (w.ChannelIOInitialized) {
30+
return;
31+
}
32+
w.ChannelIOInitialized = true;
33+
var s = document.createElement("script");
34+
s.type = "text/javascript";
35+
s.async = true;
36+
s.src = "https://cdn.channel.io/plugin/ch-plugin-web.js";
37+
var x = document.getElementsByTagName("script")[0];
38+
if (x.parentNode) {
39+
x.parentNode.insertBefore(s, x);
40+
}
41+
}
42+
if (document.readyState === "complete") {
43+
l();
44+
} else {
45+
w.addEventListener("DOMContentLoaded", l);
46+
w.addEventListener("load", l);
47+
}
48+
})();
49+
50+
ChannelIO("boot", {
51+
pluginKey: "9bf7ef35-5a3a-410a-b69a-67ef1fd9ed6b",
52+
});
53+
</script>
1354
<!-- Sweetalert2 -->
1455
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
1556
<!-- CUSTOM -->

nginx/pages/index/index.js

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,9 @@ const login = async () => {
1616
// window.location.href = "/dashboard";
1717
window.location.href = "../dashboard/index.html";
1818
} catch (error) {
19-
// console.log(error);
2019
await Swal.fire({
2120
title: "로그인 실패",
22-
text: error.message,
21+
text: `${error.message}, 만료된 토큰 이슈가 계속 발생하면 오른쪽 하단을 통해 바로 문의 주세요 도와드릴게요!!`,
2322
icon: "error",
2423
confirmButtonText: "OK",
2524
background: "#242424", // 혹은 다크 테마에 맞는 색상으로 설정

worker/logger.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,4 +46,5 @@ def get_logger(name: str) -> Logger:
4646
return logger
4747

4848

49-
LOGGER = get_logger("velog-dashboard-worker")
49+
MAIN_LOGGER = get_logger("velog-dashboard-worker")
50+
TOKEN_REFRESH_LOGGER = get_logger("velog-dashboard-token-worker")

worker/src/db.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111

1212

1313
class Repository:
14-
def __init__(self, db_url: str, period_min: int) -> None:
14+
def __init__(self, db_url: str, period_min: int = 10) -> None:
1515
self.tz = pytz.timezone("Asia/Seoul")
1616
self.period_min = period_min # user find할때 업데이트 몇 분 전 user를 가져올 지
1717
self.__get_connection(db_url)
@@ -131,6 +131,20 @@ async def update_userinfo_fail(self, user: UserInfo, result_msg: str):
131131
)
132132
return result
133133

134+
async def update_userinfo_token(self, user: UserInfo, cookie: dict):
135+
coll = self.db["userinfos"]
136+
result = await coll.update_one(
137+
{"userId": user.userId},
138+
{
139+
"$set": {
140+
"accessToken": cookie["access_token"],
141+
"refreshToken": cookie["refresh_token"],
142+
"updatedAt": datetime.now(self.tz),
143+
}
144+
},
145+
)
146+
return result
147+
134148
def __del__(self) -> None:
135149
self.client.close()
136150

worker/src/modules/velog_apis.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,42 @@ async def fetch_stats(total_post_id_dict: dict, actoken, reftoken):
120120
return total_post_id_dict
121121

122122

123+
async def get_cookie_from_one_stats_api(uuid: str, actoken, reftoken) -> dict:
124+
"""
125+
### 특정 post의 통계 하나만 가져오기
126+
- token refresh 목적의 함수
127+
"""
128+
retry_options = ExponentialRetry(attempts=3)
129+
async with RetryClient(retry_options=retry_options) as session:
130+
query = """
131+
query GetStats($post_id: ID!) {
132+
getStats(post_id: $post_id) {
133+
total
134+
count_by_day {
135+
count
136+
day
137+
}
138+
}
139+
}"""
140+
variables = {"post_id": uuid}
141+
payload = {"query": query, "variables": variables, "operationName": "GetStats"}
142+
headers = get_header(actoken, reftoken)
143+
async with session.post(
144+
"https://v2cdn.velog.io/graphql",
145+
json=payload,
146+
headers=headers,
147+
) as response:
148+
cookie_dict = dict()
149+
try:
150+
# 응답에서 쿠키를 가져옵니다.
151+
cookies = response.cookies
152+
# 쿠키를 딕셔너리로 변환합니다.
153+
cookie_dict = {cookie.key: cookie.value for cookie in cookies.values()}
154+
except Exception as e:
155+
log.error(f"get_cookie_from_one_stats_api >> {uuid}, error >> {e}")
156+
return cookie_dict
157+
158+
123159
async def fetch_posts(user_name: str):
124160
"""
125161
### 모든 페이지의 포스트를 가져오기 위해 비동기로 페이지를 반복.

worker/token_refresh.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import asyncio
2+
import os
3+
4+
from dotenv import load_dotenv
5+
from logger import TOKEN_REFRESH_LOGGER as log
6+
from src.db import Repository
7+
from src.models import UserInfo
8+
from src.modules.velog_apis import fetch_posts, get_cookie_from_one_stats_api
9+
10+
load_dotenv()
11+
DB_URL = os.getenv("DB_URL")
12+
PERIOD_MIN = int(os.getenv("PERIOD_MIN"))
13+
14+
if not DB_URL:
15+
raise Exception("There is no DB_URL value in env value")
16+
17+
if not PERIOD_MIN:
18+
PERIOD_MIN = 15
19+
20+
21+
async def main():
22+
rep = Repository(DB_URL, PERIOD_MIN)
23+
# 모든 데이터 스크레핑 타겟 유저 가져오기
24+
target_users: list[UserInfo] = await rep.find_users()
25+
26+
if not target_users:
27+
log.info("empty target user")
28+
29+
for user in target_users:
30+
# 전체 게시물 정보를 가져온 뒤, 첫 게시글 uuid만 추출
31+
posts = await fetch_posts(user.userId)
32+
log.info(f"{user.userId} - posts {len(posts)}, start to fetching all stats")
33+
target_post = list(posts.keys())[0]
34+
35+
try:
36+
# 해당 uuid 값 기반으로 cookie 값만 가져오기
37+
target_cooike = await get_cookie_from_one_stats_api(
38+
target_post, user.accessToken, user.refreshToken
39+
)
40+
41+
# cookie validations
42+
if (
43+
not target_cooike
44+
or not target_cooike.get("access_token")
45+
or not target_cooike.get("refresh_token")
46+
):
47+
raise Exception("cookie is empty")
48+
49+
# 가져온 cookie db update
50+
result = await rep.update_userinfo_token(user, target_cooike)
51+
log.info(
52+
f"{user.userId} - "
53+
+ f"target_cooike >> {target_cooike}, "
54+
+ f"result >> {result}"
55+
)
56+
except Exception as e:
57+
log.error(f"{user.userId} - token_refresh exception >> {e}, {type(e)}")
58+
continue
59+
60+
61+
asyncio.run(main())

0 commit comments

Comments
 (0)