diff --git a/backend/app/main.py b/backend/app/main.py index b9564d5..e848bfe 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -2,9 +2,11 @@ from dotenv import load_dotenv load_dotenv() from fastapi import FastAPI, Request -from fastapi.responses import JSONResponse, RedirectResponse +from fastapi.responses import JSONResponse, RedirectResponse, Response from pydantic import ValidationError from fastapi.middleware.cors import CORSMiddleware +from starlette.datastructures import MutableHeaders +from starlette.types import ASGIApp, Receive, Scope, Send # FastAPI アプリケーション作成 app = FastAPI( @@ -12,13 +14,37 @@ app = FastAPI( ) +class SecurityHeadersMiddleware: + """キャッシュ禁止・セキュリティヘッダーを全レスポンスに付与(ASGIネイティブ実装)”""" + def __init__(self, app: ASGIApp) -> None: + self.app = app + + async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: + if scope["type"] != "http": + await self.app(scope, receive, send) + return + + async def send_with_headers(message): + if message["type"] == "http.response.start": + headers = MutableHeaders(scope=message) + headers["Cache-Control"] = "no-store, no-cache, must-revalidate, private" + headers["Pragma"] = "no-cache" + headers["Expires"] = "0" + headers["X-Frame-Options"] = "DENY" + headers["X-Content-Type-Options"] = "nosniff" + headers["X-XSS-Protection"] = "1; mode=block" + await send(message) + + await self.app(scope, receive, send_with_headers) + +app.add_middleware(SecurityHeadersMiddleware) app.add_middleware( CORSMiddleware, allow_origins=["*"], allow_credentials=False, - allow_methods=["*"], - allow_headers=["*"], + allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"], + allow_headers=["Content-Type", "Authorization"], ) # ───────────────────────── diff --git a/backend/app/modules/users/router.py b/backend/app/modules/users/router.py index 2633201..ef40061 100644 --- a/backend/app/modules/users/router.py +++ b/backend/app/modules/users/router.py @@ -12,6 +12,12 @@ class LoginRequest(BaseModel): password: str +class ChangePasswordRequest(BaseModel): + username: str + current_password: str + new_password: str + + class LoginResponse(BaseModel): id: int username: str @@ -66,39 +72,48 @@ async def login(request: LoginRequest): @router.post("/register") async def register(request: LoginRequest): - """新規ユーザー登録""" + """新規ユーザー登録(無効化済み)""" + raise HTTPException(status_code=403, detail="新規ユーザー登録は現在受け付けていません") + + +@router.post("/change-password") +async def change_password(request: ChangePasswordRequest): + """パスワード変更""" + if len(request.new_password) < 8: + raise HTTPException(status_code=400, detail="新しいパスワードは8文字以上である必要があります") + try: - if len(request.password) < 6: - raise HTTPException(status_code=400, detail="パスワードは6文字以上である必要があります") - conn = get_connection() cur = conn.cursor() - - # ユーザーが既に存在するか確認 - cur.execute("SELECT id FROM users WHERE username = %s", (request.username,)) - if cur.fetchone(): + + cur.execute( + "SELECT id, password FROM users WHERE username = %s AND is_active = TRUE", + (request.username,) + ) + user = cur.fetchone() + + if not user: cur.close() conn.close() - raise HTTPException(status_code=400, detail="このユーザー名は既に使用されています") - - # ユーザーを登録 - hashed_password = hash_password(request.password) + raise HTTPException(status_code=401, detail="ユーザーが見つかりません") + + if user["password"] != hash_password(request.current_password): + cur.close() + conn.close() + raise HTTPException(status_code=401, detail="現在のパスワードが正しくありません") + + new_hashed = hash_password(request.new_password) cur.execute( - "INSERT INTO users (username, password) VALUES (%s, %s) RETURNING id, username", - (request.username, hashed_password) + "UPDATE users SET password = %s WHERE username = %s", + (new_hashed, request.username) ) - new_user = cur.fetchone() conn.commit() cur.close() conn.close() - - return { - "id": new_user["id"], - "username": new_user["username"], - "message": "ユーザー登録が完了しました" - } - + + return {"message": "パスワードを変更しました"} + except HTTPException: raise except Exception as e: - raise HTTPException(status_code=500, detail=f"登録処理エラー: {str(e)}") + raise HTTPException(status_code=500, detail=f"パスワード変更エラー: {str(e)}") diff --git a/frontend/accounts.html b/frontend/accounts.html index 4bdc632..2778c69 100644 --- a/frontend/accounts.html +++ b/frontend/accounts.html @@ -2,9 +2,11 @@ + 科目マスタ +

科目マスタ

diff --git a/frontend/css/mobile.css b/frontend/css/mobile.css new file mode 100644 index 0000000..f799f58 --- /dev/null +++ b/frontend/css/mobile.css @@ -0,0 +1,324 @@ +/* ================================================================== + mobile.css — iOS Safari・Android Chrome レスポンシブ対応 + ブレークポイント: 767px 以下をモバイルとして扱う + PC のレイアウトは変更しない (screen media query で完全に隔離) + ================================================================== */ + +/* ── iOS Safari フォントブースト無効化 ── */ +html { + -webkit-text-size-adjust: 100%; +} + +@media screen and (max-width: 767px) { + + /* ── body 余白を縮小 ── */ + body { + padding: 12px !important; + margin: 8px !important; + } + + /* ── h1・h2 フォントサイズ ── */ + h1 { font-size: 1.35rem !important; } + h2 { font-size: 1.1rem !important; } + + /* ================================================================ + テーブル: 横スクロール + 会計・給与テーブルは列数が多く、横スクロールが最適解 + ================================================================ */ + table { + display: block; + overflow-x: auto; + -webkit-overflow-scrolling: touch; + max-width: 100%; + } + + /* テーブルを包む overflow:hidden ラッパーも解放する */ + .doc-table-wrap { + overflow-x: auto !important; + } + + /* ================================================================ + フォーム入力: 16px で iOS の自動ズームを防ぐ + ================================================================ */ + input[type="text"], + input[type="date"], + input[type="number"], + input[type="password"], + input[type="email"], + input[type="tel"], + select, + textarea { + font-size: 16px !important; + max-width: 100%; + box-sizing: border-box; + } + + /* ================================================================ + ボタン: タップ領域を 44px 以上に (iOS HIG 推奨) + ================================================================ */ + button, + input[type="submit"], + input[type="button"], + .btn, + .upload-btn, + .back-link, + .logout-btn, + .change-pw-btn { + min-height: 44px; + touch-action: manipulation; + } + + /* ================================================================ + index.html + ================================================================ */ + + /* 2カラムカードグリッド → 1カラム */ + .grid { + grid-template-columns: 1fr !important; + } + + /* ヘッダー: タイトルとユーザー情報を縦積み */ + header { + flex-direction: column !important; + align-items: flex-start !important; + gap: 10px !important; + } + + header h1 { + font-size: 18px !important; + } + + .user-info { + flex-wrap: wrap !important; + width: 100% !important; + gap: 8px !important; + font-size: 13px !important; + } + + /* カード内ボタン群: 縦積み・margin-left をリセット */ + .card p:last-child { + display: flex !important; + flex-direction: column !important; + gap: 8px !important; + } + + .btn { + margin-left: 0 !important; + display: block !important; + text-align: center !important; + box-sizing: border-box !important; + } + + /* パスワード変更モーダル: 画面幅に合わせる */ + .modal-box { + width: 92vw !important; + max-width: 92vw !important; + padding: 20px !important; + } + + /* ================================================================ + payroll.html (給与システムメニュー) + ================================================================ */ + + .menu-grid { + grid-template-columns: 1fr !important; + } + + .menu-card { + padding: 20px !important; + } + + /* ================================================================ + journal-entry.html / journal-edit.html (仕訳入力) + ================================================================ */ + + /* 検索フォーム: 2カラムグリッド → 1カラム */ + .search-form-grid { + grid-template-columns: 1fr !important; + gap: 12px !important; + } + + .search-form-row, + .search-form-row-full { + flex-direction: column !important; + align-items: flex-start !important; + gap: 4px !important; + } + + .search-form-row label, + .search-form-row-full label { + min-width: unset !important; + width: 100%; + } + + .search-form-row input, + .search-form-row select, + .search-form-row-full input, + .search-form-row-full select { + width: 100% !important; + box-sizing: border-box !important; + } + + /* 検索ボタン群 */ + .search-buttons { + flex-wrap: wrap !important; + gap: 6px !important; + } + + .search-buttons button { + flex: 1 1 auto !important; + min-width: 100px !important; + } + + /* .row: 日付・摘要・勘定科目などの入力行 */ + .row { + flex-direction: column !important; + align-items: flex-start !important; + gap: 6px !important; + width: 100%; + } + + .row label { + min-width: unset !important; + } + + .row input, + .row select { + width: 100% !important; + box-sizing: border-box !important; + } + + /* ================================================================ + trial-balance.html (試算表) + テキスト中心の inline 検索フォームを縦積みにする + ================================================================ */ + + .search-form { + text-align: left !important; + padding: 12px !important; + } + + /* trial-balance の検索フォームは label/input が直接子要素 */ + .search-form > label { + display: block !important; + margin: 6px 0 2px !important; + } + + .search-form > input[type="text"], + .search-form > input[type="date"], + .search-form > input[type="number"] { + display: block !important; + width: 100% !important; + margin: 0 0 8px 15px !important; + box-sizing: border-box !important; + } + + .search-form > button { + display: inline-block !important; + margin: 4px 4px 0 0 !important; + width: auto !important; + min-width: 80px; + } + + /* ================================================================ + nenkin-portal.html (社会保険電子文書) + ================================================================ */ + + .page-header { + flex-direction: column !important; + align-items: flex-start !important; + gap: 10px !important; + } + + .upload-zone { + padding: 20px 16px !important; + } + + /* フィルターバー: wrapping は既存スタイルで対応済み、幅を補完 */ + .filter-bar { + gap: 8px !important; + } + + .filter-select { + width: 100% !important; + box-sizing: border-box !important; + } + + /* ================================================================ + payroll-employees.html / payroll-settings.html (タブ UI) + ================================================================ */ + + .nav-tabs { + flex-wrap: wrap !important; + justify-content: flex-start !important; + gap: 6px !important; + border-bottom: none !important; + } + + .nav-tab { + padding: 8px 14px !important; + font-size: 0.85rem !important; + border-radius: 6px !important; + } + + /* container の余白縮小 */ + .container { + margin: 10px !important; + padding: 16px 12px !important; + border-radius: 10px !important; + } + + /* ================================================================ + payroll-settings.html (2/3カラムフォームグリッド → 1カラム) + ================================================================ */ + + .form-row { + display: flex !important; + flex-direction: column !important; + gap: 10px !important; + } + + .form-row-3 { + display: flex !important; + flex-direction: column !important; + gap: 10px !important; + } + + .form-group { + width: 100%; + } + + .form-group input, + .form-group select, + .form-group textarea { + width: 100% !important; + box-sizing: border-box !important; + } + + /* ================================================================ + payroll-calculation.html (月次給与計算) + ================================================================ */ + + /* フィルター行のラベルを縦積み */ + .filters { + display: flex !important; + flex-direction: column !important; + gap: 8px !important; + padding: 12px !important; + } + + .filters label { + display: flex !important; + flex-direction: column !important; + gap: 4px !important; + margin-right: 0 !important; + width: 100%; + } + + .filters input, + .filters select { + width: 100% !important; + box-sizing: border-box !important; + } + +} diff --git a/frontend/egov.html b/frontend/egov.html index 3d547b3..be51c33 100644 --- a/frontend/egov.html +++ b/frontend/egov.html @@ -5,6 +5,7 @@ e-Gov 電子送達 – NJTS + @@ -86,10 +132,35 @@

NJTS

ユーザー: 読み込み中... +
+ + +

会計システム

@@ -142,6 +213,56 @@ } }); + // パスワード変更モーダル + function openChangePw() { + document.getElementById("cpCurrent").value = ""; + document.getElementById("cpNew").value = ""; + document.getElementById("cpConfirm").value = ""; + const msg = document.getElementById("changePwMsg"); + msg.style.display = "none"; + document.getElementById("changePwModal").classList.add("active"); + } + function closeChangePw() { + document.getElementById("changePwModal").classList.remove("active"); + } + function showChangePwMsg(text, isError) { + const msg = document.getElementById("changePwMsg"); + msg.textContent = text; + msg.className = "modal-msg " + (isError ? "error" : "success"); + msg.style.display = "block"; + } + async function submitChangePw() { + const current = document.getElementById("cpCurrent").value; + const newPw = document.getElementById("cpNew").value; + const confirm = document.getElementById("cpConfirm").value; + const user = getCurrentUser(); + if (!current || !newPw || !confirm) { + showChangePwMsg("すべての項目を入力してください", true); return; + } + if (newPw.length < 8) { + showChangePwMsg("新しいパスワードは8文字以上にしてください", true); return; + } + if (newPw !== confirm) { + showChangePwMsg("新しいパスワードが一致しません", true); return; + } + try { + const res = await fetch("/api/users/change-password", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ username: user.username, current_password: current, new_password: newPw }) + }); + const data = await res.json(); + if (res.ok) { + showChangePwMsg("パスワードを変更しました。再ログインしてください", false); + setTimeout(() => { logout(); }, 2000); + } else { + showChangePwMsg(data.detail || "変更に失敗しました", true); + } + } catch (e) { + showChangePwMsg("エラー: " + e.message, true); + } + } + // dropdown toggle const toggle = document.getElementById("accountingToggle"); const menu = document.getElementById("accountingMenu"); diff --git a/frontend/journal-edit.html b/frontend/journal-edit.html index eeeeee8..3d0d985 100644 --- a/frontend/journal-edit.html +++ b/frontend/journal-edit.html @@ -2,7 +2,9 @@ + + 修正仕訳入力