Files
njts-accounting-core/frontend/egov.html
2026-06-01 00:35:29 +09:00

674 lines
19 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!doctype html>
<html lang="ja">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>e-Gov 電子送達 NJTS</title>
<script src="js/auth.js"></script>
<link rel="stylesheet" href="/css/mobile.css" />
<style>
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family:
system-ui,
-apple-system,
"Yu Gothic UI",
"メイリオ",
sans-serif;
background: #f5f6fa;
padding: 24px;
color: #212529;
}
header {
max-width: 660px;
margin: 0 auto 20px;
display: flex;
justify-content: space-between;
align-items: center;
}
header h1 {
font-size: 18px;
}
.back-link {
font-size: 13px;
color: #0d6efd;
text-decoration: none;
}
.back-link:hover {
text-decoration: underline;
}
.card {
max-width: 660px;
margin: 0 auto 20px;
background: #fff;
border-radius: 10px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
padding: 28px 32px;
}
.card h2 {
font-size: 16px;
margin-bottom: 6px;
display: flex;
align-items: center;
gap: 8px;
}
.card .desc {
font-size: 13px;
color: #6c757d;
margin-bottom: 20px;
}
/* ステータスバー */
.status-bar {
display: flex;
align-items: center;
gap: 10px;
padding: 10px 14px;
border-radius: 6px;
background: #f8f9fa;
margin-bottom: 20px;
font-size: 13px;
}
.badge {
padding: 3px 10px;
border-radius: 10px;
font-size: 12px;
font-weight: 700;
white-space: nowrap;
}
.idle {
background: #e9ecef;
color: #6c757d;
}
.starting {
background: #fff3cd;
color: #856404;
}
.waiting_mfa {
background: #cff4fc;
color: #055160;
}
.logged_in {
background: #d1e7dd;
color: #0a3622;
}
.downloading {
background: #cfe2ff;
color: #084298;
}
.error {
background: #f8d7da;
color: #842029;
}
.spinner {
display: inline-block;
width: 14px;
height: 14px;
border: 2px solid #dee2e6;
border-top-color: #0d6efd;
border-radius: 50%;
animation: spin 0.7s linear infinite;
flex-shrink: 0;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
/* フォーム */
.form-group {
margin-bottom: 14px;
}
.form-group label {
display: block;
font-size: 13px;
font-weight: 600;
margin-bottom: 4px;
}
.form-group input {
width: 100%;
padding: 9px 12px;
border: 1px solid #ced4da;
border-radius: 4px;
font-size: 14px;
}
.form-group input:focus {
outline: none;
border-color: #86b7fe;
box-shadow: 0 0 0 3px rgba(13, 110, 253, 0.15);
}
.btn {
padding: 10px 22px;
border: none;
border-radius: 5px;
cursor: pointer;
font-size: 14px;
font-weight: 600;
}
.btn-primary {
background: #6f42c1;
color: #fff;
}
.btn-primary:hover {
background: #5a32a3;
}
.btn-secondary {
background: #6c757d;
color: #fff;
}
.btn-secondary:hover {
background: #5c636a;
}
/* MFA */
.mfa-notice {
background: #e7f3ff;
border: 1px solid #b6d4fe;
border-radius: 6px;
padding: 12px 14px;
font-size: 13px;
margin-bottom: 16px;
line-height: 1.6;
}
.mfa-input {
font-size: 30px;
letter-spacing: 12px;
text-align: center;
width: 100%;
padding: 10px;
border: 2px solid #6f42c1;
border-radius: 5px;
}
.btn-success {
background: #198754;
color: #fff;
}
.btn-success:hover {
background: #146c43;
}
.btn-success:disabled {
background: #a3cfbb;
cursor: not-allowed;
}
/* ダウンロード結果 */
.dl-result {
margin-top: 14px;
padding: 12px 16px;
background: #d1e7dd;
border: 1px solid #a3cfbb;
border-radius: 6px;
font-size: 13px;
display: flex;
align-items: center;
gap: 12px;
}
.dl-result a {
margin-left: auto;
font-weight: 700;
color: #0a3622;
text-decoration: none;
background: #fff;
border: 1px solid #a3cfbb;
padding: 4px 12px;
border-radius: 4px;
}
.dl-result a:hover {
background: #c2e0cc;
}
.screenshot-wrap summary {
font-size: 12px;
color: #888;
cursor: pointer;
margin-bottom: 6px;
}
.screenshot-wrap img {
max-width: 100%;
border: 1px solid #dee2e6;
border-radius: 4px;
}
/* ログイン済みアクション */
.action-note {
font-size: 13px;
color: #555;
margin-bottom: 16px;
line-height: 1.6;
}
/* API 認証 */
.btn-gbiz {
background: #0077cc;
color: #fff;
width: 100%;
text-align: center;
padding: 12px 22px;
font-size: 15px;
}
.btn-gbiz:hover {
background: #0055a5;
}
.method-tabs {
display: flex;
gap: 6px;
margin-bottom: 18px;
}
.method-tab {
flex: 1;
padding: 8px;
border: 2px solid #dee2e6;
border-radius: 5px;
background: #f8f9fa;
cursor: pointer;
font-size: 12px;
font-weight: 600;
text-align: center;
color: #6c757d;
}
.method-tab.active {
border-color: #6f42c1;
background: #f3eeff;
color: #6f42c1;
}
</style>
</head>
<body>
<header>
<h1>🏛 e-Gov 電子送達</h1>
<a class="back-link" href="index.html" style="display:inline-block;padding:6px 16px;background:#757575;color:#fff;border-radius:4px;text-decoration:none;font-size:14px;">← メニューへ戻る</a>
</header>
<!-- ログインカード -->
<div class="card">
<h2>🔐 e-Gov ログインGビズID</h2>
<p class="desc">GビズID プレミアムで e-Gov にログインします。</p>
<!-- ステータス -->
<div class="status-bar">
<span class="badge idle" id="badge">未ログイン</span>
<span class="spinner" id="spinner" style="display: none"></span>
<span id="msgText" style="color: #555"></span>
</div>
<!-- 認証方式タブ -->
<div class="method-tabs" id="methodTabs">
<div class="method-tab active" id="tabApi" onclick="switchTab('api')">
🔑 API認証推奨
</div>
<div class="method-tab" id="tabBrowser" onclick="switchTab('browser')">
🌐 ブラウザ自動操作
</div>
</div>
<!-- ─── API 認証セクション ─── -->
<div id="apiAuthSection">
<p
style="
font-size: 13px;
color: #555;
margin-bottom: 16px;
line-height: 1.6;
"
>
公式 e-Gov API を使用してGビズIDで認証します。<br />
ボタンを押すとGビズIDのログインページに移動します。認証後、自動でここに戻ります。
</p>
<button class="btn btn-gbiz" onclick="startApiAuth()">
GビズID でログインAPI方式
</button>
</div>
<!-- ─── Playwright セクション ─── -->
<div id="loginSection" style="display: none">
<div class="form-group">
<label>GビズIDメールアドレス</label>
<input
type="email"
id="userId"
placeholder="info@company.co.jp"
autocomplete="username"
/>
</div>
<div class="form-group">
<label>パスワード</label>
<input
type="password"
id="password"
autocomplete="current-password"
onkeydown="if (event.key === 'Enter') startLogin();"
/>
</div>
<button class="btn btn-primary" onclick="startLogin()">
ログイン開始
</button>
</div>
<!-- ② MFA入力WAITING_MFA 時のみ表示) -->
<div id="mfaSection" style="display: none">
<div class="mfa-notice">
📱 <strong>GビズID アプリ</strong>を開き、表示されている
<strong>6桁の認証コード</strong> を入力してください。<br />
<small
>※ コードは 30
秒ごとに更新されます。新しいコードで入力してください。</small
>
</div>
<div class="form-group">
<label>認証コード6桁</label>
<input
class="mfa-input"
type="text"
id="mfaCode"
maxlength="6"
inputmode="numeric"
pattern="[0-9]*"
placeholder="000000"
oninput="if (this.value.length === 6) submitMfa();"
/>
</div>
<button class="btn btn-primary" onclick="submitMfa()">認証</button>
</div>
<!-- ③ ログイン済み -->
<div id="loggedSection" style="display: none">
<p class="action-note">
✅ e-Gov にログインしました。<br />
電子送達一覧から最初のファイルをダウンロードできます。
</p>
<div
style="display: flex; gap: 10px; flex-wrap: wrap; align-items: center"
>
<button class="btn btn-success" id="dlBtn" onclick="startDownload()">
📥 電子送達をダウンロード
</button>
<button class="btn btn-secondary" onclick="doLogout()">
ログアウト
</button>
</div>
<div id="dlResult" style="display: none" class="dl-result">
<span id="dlMsg"></span>
<a id="dlLink" href="/egov/downloads/latest" target="_blank">保存</a>
</div>
</div>
<!-- スクリーンショット(デバッグ) -->
<div class="screenshot-wrap" id="ssWrap" style="display: none">
<details>
<summary>ブラウザ画面を確認(デバッグ用)</summary>
<img id="ssImg" src="" alt="スクリーンショット" />
</details>
</div>
</div>
<script>
let timer = null;
let currentTab = "api";
// ─── タブ切り替え ──────────────────────────────────────
function switchTab(tab) {
currentTab = tab;
document
.getElementById("tabApi")
.classList.toggle("active", tab === "api");
document
.getElementById("tabBrowser")
.classList.toggle("active", tab === "browser");
if (tab === "api") {
show("apiAuthSection");
hide("loginSection");
} else {
hide("apiAuthSection");
show("loginSection");
}
}
// ─── API 認証 ──────────────────────────────────────────
async function startApiAuth() {
const res = await fetch("/egov/auth-url");
if (!res.ok) {
const e = await res.json();
alert(
e.detail ||
"認証URLの取得に失敗しました。\n/egov/config で資格情報を設定してください。",
);
return;
}
const { auth_url } = await res.json();
// 同じタブで遷移(リダイレクトバック受信のため)
location.href = auth_url;
}
// ─── OAuth2 コールバック処理 ───────────────────────────
async function handleOAuthCallback(code, oauthState) {
setUI("starting", "e-Gov 認証コードを処理中...");
hide("apiAuthSection");
hide("loginSection");
hide("methodTabs");
try {
const res = await fetch("/egov/callback", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ code, state: oauthState }),
});
// URL からパラメータを削除
history.replaceState({}, "", "/egov.html");
if (res.ok) {
setUI("logged_in", "API認証完了");
show("loggedSection");
} else {
const e = await res.json();
setUI("error", "API認証エラー: " + (e.detail || "不明なエラー"));
show("methodTabs");
show("apiAuthSection");
}
} catch (err) {
history.replaceState({}, "", "/egov.html");
setUI("error", "コールバックエラー: " + err.message);
show("methodTabs");
show("apiAuthSection");
}
}
async function startLogin() {
const userId = document.getElementById("userId").value.trim();
const password = document.getElementById("password").value;
if (!userId || !password) {
alert("GビズID とパスワードを入力してください");
return;
}
const res = await fetch("/egov/login-start", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ user_id: userId, password }),
});
if (!res.ok) {
const e = await res.json();
alert(e.detail || "エラー");
return;
}
startPolling();
}
async function submitMfa() {
const code = document.getElementById("mfaCode").value.trim();
if (code.length !== 6) {
alert("6桁のコードを入力してください");
return;
}
const res = await fetch("/egov/login-mfa", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ mfa_code: code }),
});
if (!res.ok) {
const e = await res.json();
alert(e.detail || "エラー");
return;
}
hide("mfaSection");
setUI("starting", "認証コードを送信中...");
}
async function startDownload() {
document.getElementById("dlBtn").disabled = true;
document.getElementById("dlBtn").textContent = "⏳ ダウンロード中...";
hide("dlResult");
const res = await fetch("/egov/download-start", { method: "POST" });
if (!res.ok) {
const e = await res.json();
alert(e.detail || "ダウンロード開始エラー");
document.getElementById("dlBtn").disabled = false;
document.getElementById("dlBtn").textContent =
"📥 電子送達をダウンロード";
return;
}
startPolling();
}
async function doLogout() {
await fetch("/egov/logout", { method: "POST" });
stopPolling();
hide("loggedSection");
show("loginSection");
document.getElementById("mfaCode").value = "";
hide("dlResult");
setUI("idle", "");
}
function startPolling() {
if (timer) return;
timer = setInterval(async () => {
try {
const r = await fetch("/egov/status");
apply(await r.json());
} catch (_) {}
}, 1500);
}
function stopPolling() {
if (timer) {
clearInterval(timer);
timer = null;
}
}
function apply(d) {
setUI(d.status, d.message);
if (d.screenshot) {
document.getElementById("ssImg").src =
"data:image/jpeg;base64," + d.screenshot;
show("ssWrap");
}
// API トークンが有効 or ログイン済み
if (d.token_valid || d.status === "logged_in") {
hide("methodTabs");
hide("apiAuthSection");
hide("loginSection");
hide("mfaSection");
show("loggedSection");
if (d.last_download) {
const dlMsg = document.getElementById("dlMsg");
dlMsg.textContent = `${d.last_download.filename} (${(d.last_download.size / 1024).toFixed(1)} KB)`;
show("dlResult");
document.getElementById("dlBtn").disabled = false;
document.getElementById("dlBtn").textContent =
"📥 電子送達をダウンロード";
stopPolling();
} else if (
d.status === "logged_in" &&
!["downloading"].includes(d.status)
) {
stopPolling();
}
return;
}
if (d.status === "waiting_mfa") {
hide("loginSection");
hide("apiAuthSection");
show("mfaSection");
setTimeout(() => document.getElementById("mfaCode").focus(), 100);
} else if (d.status === "error") {
stopPolling();
if (currentTab === "api") {
show("apiAuthSection");
hide("loginSection");
} else {
show("loginSection");
hide("apiAuthSection");
}
hide("mfaSection");
hide("loggedSection");
document.getElementById("dlBtn").disabled = false;
document.getElementById("dlBtn").textContent =
"📥 電子送達をダウンロード";
}
}
function setUI(status, msg) {
const labels = {
idle: "未ログイン",
starting: "ログイン中",
waiting_mfa: "認証待ち",
logged_in: "ログイン済み",
downloading: "DL中",
error: "エラー",
};
const badge = document.getElementById("badge");
badge.textContent = labels[status] || status;
badge.className = "badge " + status;
document.getElementById("msgText").textContent = msg || "";
document.getElementById("spinner").style.display = [
"starting",
"waiting_mfa",
"downloading",
].includes(status)
? "inline-block"
: "none";
}
function show(id) {
document.getElementById(id).style.display = "";
}
function hide(id) {
document.getElementById(id).style.display = "none";
}
// 初期状態取得
(async () => {
// OAuth2 コールバック検出
const params = new URLSearchParams(location.search);
const code = params.get("code");
const oauthState = params.get("state");
if (code) {
await handleOAuthCallback(code, oauthState || "");
return;
}
// 通常ロード: ステータス確認
try {
const r = await fetch("/egov/status");
const d = await r.json();
apply(d);
if (!["idle", "logged_in", "error"].includes(d.status))
startPolling();
} catch (_) {}
})();
</script>
</body>
</html>