687 lines
20 KiB
HTML
687 lines
20 KiB
HTML
<!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>
|