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 @@
+
+
修正仕訳入力