Auto-refresh Token cho IntelliJ GraphQL Plugin
Khi execute GraphQL queries trực tiếp trong IntelliJ, token thường có TTL ngắn và cần được refresh định kỳ. Bài này ghi lại cách setup script tự động refresh token và tích hợp với GraphQL plugin.
Vấn đề
- IntelliJ GraphQL plugin không có pre-request hook để tự refresh token
- Access token TTL ngắn (ví dụ 5 phút), refresh token TTL trung bình (30 phút)
- Mỗi lần token hết hạn phải thủ công update config → tốn thời gian
Giải pháp
Dùng .env.local + script bash để tự động refresh token chạy ngầm trong terminal.
Cấu trúc file
project/
├── .graphqlrc.yml ← hoặc graphql.config.yml
├── .env.local ← credentials + tokens (gitignore)
└── scripts/
├── gql-login.sh ← chạy thủ công 1 lần khi bắt đầu
├── gql-refresh-token.sh
└── gql-watch-token.sh ← chạy ngầm suốt session làm việcConfig
.graphqlrc.yml
schema: schema.graphql
extensions:
endpoints:
default:
url: ${GQL_ENDPOINT}
headers:
Authorization: "Bearer ${GQL_TOKEN}"Syntax env variable
Plugin dùng ${VARIABLE_NAME} — không có prefix env:.${env:GQL_TOKEN} sẽ bị parse thành variable tên env, không đọc được từ .env file.
.env.local
GQL_ENDPOINT=https://your-api/graphql
GQL_BRAND_ID=brand_xxx
GQL_USERNAME=your-email@example.com
GQL_PASSWORD=your-password
GQL_TOKEN= # auto-filled by script
GQL_TOKEN_EXPIRES_AT= # auto-filled by script
GQL_REFRESH_TOKEN= # auto-filled by script
GQL_REFRESH_EXPIRES_AT= # auto-filled by script.gitignore
.env
.env.localTại sao dùng .env.local thay vì .env?
Plugin đọc file theo thứ tự ưu tiên từ cao xuống thấp:.env.local → .env.development.local → .env.development → .env.dev → .env
Plugin dừng lại ở file có priority cao nhất tìm thấy — nếu .env.local tồn tại,.env sẽ không được đọc. Vì vậy nên để tất cả variables trong cùng 1 file.
Scripts
gql-login.sh
Đọc credentials từ .env.local, gọi mutation login, lưu tokens vào .env.local.
#!/bin/bash
set -e
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
ROOT_DIR="$(dirname "$SCRIPT_DIR")"
if [ -f "$ROOT_DIR/.env.local" ]; then
export $(grep -v '^#' "$ROOT_DIR/.env.local" | xargs)
fi
if [ -z "$GQL_ENDPOINT" ] || [ -z "$GQL_BRAND_ID" ] || \
[ -z "$GQL_USERNAME" ] || [ -z "$GQL_PASSWORD" ]; then
echo "❌ Thiếu config trong .env.local (GQL_ENDPOINT, GQL_BRAND_ID, GQL_USERNAME, GQL_PASSWORD)"
exit 1
fi
echo "Logging in as $GQL_USERNAME (brand: $GQL_BRAND_ID)..."
RESPONSE=$(curl -s -X POST "$GQL_ENDPOINT" \
-H "Content-Type: application/json" \
-d "$(jq -n \
--arg brandId "$GQL_BRAND_ID" \
--arg username "$GQL_USERNAME" \
--arg password "$GQL_PASSWORD" \
'{
query: "mutation Login($input: LoginInput!) { login(input: $input) { accessToken expiresAt refreshToken sessionId userId } }",
variables: { input: { brandId: $brandId, username: $username, password: $password } }
}'
)")
GQL_ERROR=$(echo "$RESPONSE" | jq -r '.errors[0].message // empty')
if [ -n "$GQL_ERROR" ]; then
echo "❌ Login thất bại: $GQL_ERROR"
exit 1
fi
ACCESS_TOKEN=$(echo "$RESPONSE" | jq -r '.data.login.accessToken')
REFRESH_TOKEN=$(echo "$RESPONSE" | jq -r '.data.login.refreshToken')
EXPIRES_AT=$(echo "$RESPONSE" | jq -r '.data.login.expiresAt')
USER_ID=$(echo "$RESPONSE" | jq -r '.data.login.userId')
SESSION_ID=$(echo "$RESPONSE" | jq -r '.data.login.sessionId')
if [ "$ACCESS_TOKEN" = "null" ] || [ -z "$ACCESS_TOKEN" ]; then
echo "❌ Không lấy được accessToken"
echo "$RESPONSE" | jq .
exit 1
fi
REFRESH_EXPIRES_AT=$(date -u -d "+30 minutes" "+%Y-%m-%dT%H:%M:%SZ" 2>/dev/null \
|| date -u -v+30M "+%Y-%m-%dT%H:%M:%SZ")
sed -i.bak \
-e "s|GQL_TOKEN=.*|GQL_TOKEN=$ACCESS_TOKEN|" \
-e "s|GQL_TOKEN_EXPIRES_AT=.*|GQL_TOKEN_EXPIRES_AT=$EXPIRES_AT|" \
-e "s|GQL_REFRESH_TOKEN=.*|GQL_REFRESH_TOKEN=$REFRESH_TOKEN|" \
-e "s|GQL_REFRESH_EXPIRES_AT=.*|GQL_REFRESH_EXPIRES_AT=$REFRESH_EXPIRES_AT|" \
"$ROOT_DIR/.env.local"
rm -f "$ROOT_DIR/.env.local.bak"
echo ""
echo "✅ Login thành công"
echo " User ID : $USER_ID"
echo " Session ID : $SESSION_ID"
echo " Access token expires : $EXPIRES_AT"
echo " Refresh token expires: $REFRESH_EXPIRES_AT"gql-refresh-token.sh
#!/bin/bash
set -e
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
ROOT_DIR="$(dirname "$SCRIPT_DIR")"
if [ -f "$ROOT_DIR/.env.local" ]; then
export $(grep -v '^#' "$ROOT_DIR/.env.local" | xargs)
else
echo "❌ .env.local not found. Chạy gql-login.sh trước."
exit 1
fi
if [ -z "$GQL_REFRESH_TOKEN" ]; then
echo "❌ GQL_REFRESH_TOKEN is not set. Chạy gql-login.sh trước."
exit 1
fi
# Check refresh token còn hạn không
if [ -n "$GQL_REFRESH_EXPIRES_AT" ]; then
NOW=$(date -u +%s)
CLEAN_EXPIRES=$(echo "$GQL_REFRESH_EXPIRES_AT" | sed 's/\..*//' | sed 's/Z//')
EXPIRES=$(date -u -d "${CLEAN_EXPIRES}" +%s 2>/dev/null \
|| date -u -j -f "%Y-%m-%dT%H:%M:%S" "${CLEAN_EXPIRES}" +%s)
if [ "$NOW" -ge "$EXPIRES" ]; then
echo "❌ Refresh token đã hết hạn. Cần login lại:"
echo " ./scripts/gql-login.sh"
exit 1
fi
fi
RESPONSE=$(curl -s -X POST "$GQL_ENDPOINT" \
-H "Content-Type: application/json" \
-d "$(jq -n \
--arg refreshToken "$GQL_REFRESH_TOKEN" \
'{
query: "mutation RefreshToken($input: RefreshTokenInput!) { refreshToken(input: $input) { accessToken refreshToken expiresAt } }",
variables: { input: { refreshToken: $refreshToken } }
}'
)")
GQL_ERROR=$(echo "$RESPONSE" | jq -r '.errors[0].message // empty')
if [ -n "$GQL_ERROR" ]; then
echo "❌ Refresh thất bại: $GQL_ERROR. Cần login lại:"
echo " ./scripts/gql-login.sh"
exit 1
fi
ACCESS_TOKEN=$(echo "$RESPONSE" | jq -r '.data.refreshToken.accessToken')
NEW_REFRESH_TOKEN=$(echo "$RESPONSE" | jq -r '.data.refreshToken.refreshToken')
EXPIRES_AT=$(echo "$RESPONSE" | jq -r '.data.refreshToken.expiresAt')
if [ "$ACCESS_TOKEN" = "null" ] || [ -z "$ACCESS_TOKEN" ]; then
echo "❌ Không lấy được accessToken"
echo "$RESPONSE" | jq .
exit 1
fi
REFRESH_EXPIRES_AT=$(date -u -d "+30 minutes" "+%Y-%m-%dT%H:%M:%SZ" 2>/dev/null \
|| date -u -v+30M "+%Y-%m-%dT%H:%M:%SZ")
sed -i.bak \
-e "s|GQL_TOKEN=.*|GQL_TOKEN=$ACCESS_TOKEN|" \
-e "s|GQL_TOKEN_EXPIRES_AT=.*|GQL_TOKEN_EXPIRES_AT=$EXPIRES_AT|" \
-e "s|GQL_REFRESH_TOKEN=.*|GQL_REFRESH_TOKEN=$NEW_REFRESH_TOKEN|" \
-e "s|GQL_REFRESH_EXPIRES_AT=.*|GQL_REFRESH_EXPIRES_AT=$REFRESH_EXPIRES_AT|" \
"$ROOT_DIR/.env.local"
rm -f "$ROOT_DIR/.env.local.bak"
echo "✅ Token refreshed. Access token expires: $EXPIRES_AT"gql-watch-token.sh
Chạy ngầm trong terminal, tự động gọi gql-refresh-token.sh khi access token còn dưới 1 phút.
#!/bin/bash
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
ROOT_DIR="$(dirname "$SCRIPT_DIR")"
echo "👀 Watching token... (Ctrl+C để dừng)"
while true; do
export $(grep -v '^#' "$ROOT_DIR/.env.local" | xargs)
NOW=$(date -u +%s)
# Check refresh token — nếu hết thì exit
CLEAN_REFRESH_EXPIRES=$(echo "$GQL_REFRESH_EXPIRES_AT" | sed 's/\..*//' | sed 's/Z//')
REFRESH_EXPIRES=$(date -u -d "${CLEAN_REFRESH_EXPIRES}" +%s 2>/dev/null \
|| date -u -j -f "%Y-%m-%dT%H:%M:%S" "${CLEAN_REFRESH_EXPIRES}" +%s)
if [ "$NOW" -ge "$REFRESH_EXPIRES" ]; then
echo "❌ $(date '+%H:%M:%S') Refresh token hết hạn. Cần login lại: ./scripts/gql-login.sh"
exit 1
fi
# Check access token — nếu còn < 1p thì refresh
CLEAN_TOKEN_EXPIRES=$(echo "$GQL_TOKEN_EXPIRES_AT" | sed 's/\..*//' | sed 's/Z//')
TOKEN_EXPIRES=$(date -u -d "${CLEAN_TOKEN_EXPIRES}" +%s 2>/dev/null \
|| date -u -j -f "%Y-%m-%dT%H:%M:%S" "${CLEAN_TOKEN_EXPIRES}" +%s)
TOKEN_REMAINING=$(( (TOKEN_EXPIRES - NOW) / 60 ))
if [ "$NOW" -ge "$TOKEN_EXPIRES" ] || [ "$TOKEN_REMAINING" -le 1 ]; then
echo "🔄 $(date '+%H:%M:%S') Access token hết hạn, refreshing..."
bash "$SCRIPT_DIR/gql-refresh-token.sh" && \
echo "✅ $(date '+%H:%M:%S') Refreshed."
else
echo "⏳ $(date '+%H:%M:%S') Access token còn ~${TOKEN_REMAINING}p"
fi
sleep 30
doneWorkflow hàng ngày
# Sáng mở máy — chạy 1 lần
./scripts/gql-login.sh
# Để terminal này chạy ngầm suốt ngày
./scripts/gql-watch-token.sh
# IntelliJ tự có token mới, execute query bình thườngHoặc dùng alias:
# ~/.zshrc
alias gql-start="cd ~/projects/backops && bash ./scripts/gql-login.sh && bash ./scripts/gql-watch-token.sh"gql-startLưu ý
gql-watch-token.shcheck mỗi 30 giây — đủ nhanh với access token TTL 5 phút- Mỗi lần refresh, server trả về refresh token mới (token rotation) — script tự update
.env.local - Nếu idle quá 30 phút, refresh token hết hạn → watch script sẽ báo và exit → cần
gql-login.shlại - Timestamp so sánh luôn dùng UTC để tránh lệch timezone