@aiquants/daily-report 0.18.0 → 0.18.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/client.d.mts +6 -3
- package/dist/client.d.ts +6 -3
- package/dist/client.js +5 -5
- package/dist/client.js.map +1 -1
- package/dist/client.mjs +5 -5
- package/dist/client.mjs.map +1 -1
- package/dist/server.d.mts +17 -7
- package/dist/server.d.ts +17 -7
- package/dist/server.js +10 -10
- package/dist/server.js.map +1 -1
- package/dist/server.mjs +10 -10
- package/dist/server.mjs.map +1 -1
- package/dist/styles/daily-report.standalone.css +1 -1
- package/package.json +6 -4
- package/src/client/components/daily-report-list.tsx +199 -80
- package/src/client/components/daily-report-resize-panels.spec.tsx +305 -0
- package/src/client/hooks/use-responsive-layout.ts +34 -27
- package/src/server/handlers.ts +218 -176
- package/src/server/handlers.unauthorized-cookie.spec.ts +187 -0
- package/src/server/ports.ts +6 -1
- package/src/server/service.spec.ts +1 -1
- package/src/server/test-helpers/handlers-config.ts +16 -3
- package/src/server.spec.ts +7 -7
package/src/server/handlers.ts
CHANGED
|
@@ -20,12 +20,15 @@ import { DailyReportViewerScope, isSourceTypeVisible, type VisibleSourceTypeSet
|
|
|
20
20
|
|
|
21
21
|
type LoaderArgs = { request: Request; params: Record<string, string | undefined> }
|
|
22
22
|
|
|
23
|
-
/** `data()` 相当の JSON レスポンス生成 (react-router 非依存)
|
|
24
|
-
const jsonData = (payload: unknown, init?: { status?: number }): Response =>
|
|
25
|
-
new
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
23
|
+
/** `data()` 相当の JSON レスポンス生成 (react-router 非依存)。`setCookie` 指定時は Set-Cookie を付与する。 */
|
|
24
|
+
const jsonData = (payload: unknown, init?: { status?: number; setCookie?: string }): Response => {
|
|
25
|
+
const headers = new Headers({ "Content-Type": "application/json" })
|
|
26
|
+
// セッション破棄の Set-Cookie は未認証応答にも載せる (捨てると無効セッションがブラウザに残る)
|
|
27
|
+
if (init?.setCookie) {
|
|
28
|
+
headers.append("Set-Cookie", init.setCookie)
|
|
29
|
+
}
|
|
30
|
+
return new Response(JSON.stringify(payload), { status: init?.status ?? 200, headers })
|
|
31
|
+
}
|
|
29
32
|
|
|
30
33
|
// ---------------- ids NDJSON ストリームの定数とヘルパー (純粋・モジュールスコープ) ----------------
|
|
31
34
|
|
|
@@ -719,10 +722,10 @@ export function createDailyReportHandlers(config: DailyReportHandlersConfig) {
|
|
|
719
722
|
headers.append("Set-Cookie", cookie)
|
|
720
723
|
}
|
|
721
724
|
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
725
|
+
// 文字列モード (`failureRedirect: loginRedirectPath`) では認証失敗が redirect として
|
|
726
|
+
// throw されるため `user` は必ず存在する (`DailyReportAuthenticate` のオーバーロードも
|
|
727
|
+
// そう宣言している)。`if (user)` は到達しないので置かない
|
|
728
|
+
const userId: number | null = await service.getUserIdByExternalId(user.id)
|
|
726
729
|
|
|
727
730
|
const hashedUserId = userId ? encodeUserId(userId) : null
|
|
728
731
|
|
|
@@ -943,199 +946,229 @@ export function createDailyReportHandlers(config: DailyReportHandlersConfig) {
|
|
|
943
946
|
* 日報データの変更操作を処理するアクション。
|
|
944
947
|
*/
|
|
945
948
|
const apiAction = async ({ request, params }: LoaderArgs) => {
|
|
946
|
-
const { user } = await authenticate(request, { failureRedirect: null })
|
|
947
|
-
if (!user) {
|
|
948
|
-
return jsonData({ error: "Unauthorized" }, { status: 401 })
|
|
949
|
-
}
|
|
949
|
+
const { user, cookie } = await authenticate(request, { failureRedirect: null })
|
|
950
950
|
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
951
|
+
/**
|
|
952
|
+
* Builds a JSON response that always carries the session cookie for this request.
|
|
953
|
+
* 本リクエストのセッション cookie を必ず載せた JSON レスポンスの生成。
|
|
954
|
+
*
|
|
955
|
+
* 全分岐 (成功・4xx・未認証) で転送する必要がある。トークン更新の cookie を落とすとブラウザは
|
|
956
|
+
* 期限切れトークンを持ち続け、以後のリクエストが毎回リフレッシュを試みて収束しない。
|
|
957
|
+
* 破棄 cookie を落とせば無効セッションがブラウザに残り続ける。個別の `jsonData` へ付け回すと
|
|
958
|
+
* 必ず取りこぼすため、この関数以外を本ハンドラ内で使わない。
|
|
959
|
+
*
|
|
960
|
+
* @param payload Response payload / レスポンス本文
|
|
961
|
+
* @param init Status override / ステータス指定
|
|
962
|
+
* @returns JSON response carrying the session cookie / セッション cookie を載せた JSON レスポンス
|
|
963
|
+
*/
|
|
964
|
+
const respond = (payload: unknown, init?: { status?: number }): Response => jsonData(payload, { ...init, setCookie: cookie })
|
|
955
965
|
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
return jsonData({ error: "User not found" }, { status: 404 })
|
|
966
|
+
if (!user) {
|
|
967
|
+
return respond({ error: "Unauthorized" }, { status: 401 })
|
|
959
968
|
}
|
|
960
969
|
|
|
961
|
-
//
|
|
962
|
-
//
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
const businessDate = formData.get("businessDate") as string | null
|
|
970
|
-
const operationTimestamp = Number(formData.get("operationTimestamp"))
|
|
971
|
-
const clientTempId = formData.get("clientTempId") as string | null
|
|
972
|
-
|
|
973
|
-
if (intent === "clearCache") {
|
|
974
|
-
// 開発専用アフォーダンス。既定は封鎖 (認証済みなら誰でも全ワーカーの
|
|
975
|
-
// キャッシュを無制限破棄できてしまうため、注入で明示的に開けた環境のみ許可)
|
|
976
|
-
if (!config.enableDevCacheClear) {
|
|
977
|
-
return jsonData({ error: "Invalid intent" }, { status: 400 })
|
|
970
|
+
// apiAction には外側 try/catch が無く、`respond` を通らない例外 (service / DB / Redis の
|
|
971
|
+
// 失敗) がハンドラの外へ抜けていた。抜けるとセッション cookie が載らないまま 500 になり、
|
|
972
|
+
// トークン更新がブラウザへ届かない。apiLoader (:930) と同じ形で包む
|
|
973
|
+
const actionStartTime = Date.now()
|
|
974
|
+
try {
|
|
975
|
+
const endpoint = params.endpoint
|
|
976
|
+
if (endpoint !== "action") {
|
|
977
|
+
return respond({ error: "Unknown endpoint" }, { status: 404 })
|
|
978
978
|
}
|
|
979
|
-
await service.clearCache()
|
|
980
|
-
return jsonData({ status: "OK", intent: "clearCache" })
|
|
981
|
-
}
|
|
982
979
|
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
980
|
+
const userId = await service.getUserIdByExternalId(user.id)
|
|
981
|
+
if (!userId) {
|
|
982
|
+
return respond({ error: "User not found" }, { status: 404 })
|
|
983
|
+
}
|
|
986
984
|
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
985
|
+
// 可視集合はリクエストごとに 1 回だけ解決し、他者の日報に作用する 4 種へ渡す
|
|
986
|
+
// (create / update / publish / delete / clearCache は所有者・開発者ゲートで別管理)
|
|
987
|
+
const scope = await resolveViewerScope(request)
|
|
990
988
|
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
const raw = formData.get(fieldName)
|
|
1005
|
-
if (raw === null) {
|
|
1006
|
-
return jsonData({ error: `${fieldName} required` }, { status: 400 })
|
|
1007
|
-
}
|
|
1008
|
-
try {
|
|
1009
|
-
const updatedStatus = await apply(raw === "true")
|
|
1010
|
-
// クライアントは isStarred/isRead のみ参照する。生内部 ID (userId/created_by/updated_by) を
|
|
1011
|
-
// 含む行全体は返さず、必要なフラグだけに絞る (内部 ID の自己開示防止)。
|
|
1012
|
-
return jsonData({ status: "OK", intent: intentName, updatedStatus: { isStarred: updatedStatus.isStarred, isRead: updatedStatus.isRead }, operationTimestamp, reportHubId: String(reportHubId), clientTempId })
|
|
1013
|
-
} catch (e: unknown) {
|
|
1014
|
-
// 不可視区分・不存在・削除済みは 404 へ写像する (未捕捉だと 500 になる)
|
|
1015
|
-
if (e instanceof Error && e.message === "Not Found") {
|
|
1016
|
-
return jsonData({ error: "Report not found" }, { status: 404 })
|
|
989
|
+
const formData = await request.formData()
|
|
990
|
+
const intent = formData.get("intent")
|
|
991
|
+
const reportHubIdRaw = formData.get("reportHubId")
|
|
992
|
+
const reportHubId = reportHubIdRaw ? Number(reportHubIdRaw) : NaN
|
|
993
|
+
const businessDate = formData.get("businessDate") as string | null
|
|
994
|
+
const operationTimestamp = Number(formData.get("operationTimestamp"))
|
|
995
|
+
const clientTempId = formData.get("clientTempId") as string | null
|
|
996
|
+
|
|
997
|
+
if (intent === "clearCache") {
|
|
998
|
+
// 開発専用アフォーダンス。既定は封鎖 (認証済みなら誰でも全ワーカーの
|
|
999
|
+
// キャッシュを無制限破棄できてしまうため、注入で明示的に開けた環境のみ許可)
|
|
1000
|
+
if (!config.enableDevCacheClear) {
|
|
1001
|
+
return respond({ error: "Invalid intent" }, { status: 400 })
|
|
1017
1002
|
}
|
|
1018
|
-
|
|
1003
|
+
await service.clearCache()
|
|
1004
|
+
return respond({ status: "OK", intent: "clearCache" })
|
|
1019
1005
|
}
|
|
1020
|
-
}
|
|
1021
1006
|
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
if (!businessDate) {
|
|
1025
|
-
return jsonData({ error: "businessDate required" }, { status: 400 })
|
|
1026
|
-
}
|
|
1027
|
-
const newReport = await service.createDailyReport(userId, businessDate, clientTempId)
|
|
1028
|
-
return jsonData({
|
|
1029
|
-
status: "OK",
|
|
1030
|
-
intent: "create",
|
|
1031
|
-
report: newReport,
|
|
1032
|
-
reportHubId: String(newReport.reportHubId),
|
|
1033
|
-
clientTempId, // Echo back for validation
|
|
1034
|
-
})
|
|
1007
|
+
if (!clientTempId) {
|
|
1008
|
+
return respond({ error: "clientTempId required" }, { status: 400 })
|
|
1035
1009
|
}
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
try {
|
|
1040
|
-
await service.updateDailyReport(reportHubId, userId, { title, content }, clientTempId)
|
|
1041
|
-
return jsonData({ status: "OK", intent: "update", reportHubId: String(reportHubId), clientTempId })
|
|
1042
|
-
} catch (e: unknown) {
|
|
1043
|
-
if (e instanceof Error) {
|
|
1044
|
-
if (e.message === "Unauthorized") {
|
|
1045
|
-
return jsonData({ error: "Unauthorized" }, { status: 403 })
|
|
1046
|
-
}
|
|
1047
|
-
if (e.message === "Not Found") {
|
|
1048
|
-
return jsonData({ error: "Report not found" }, { status: 404 })
|
|
1049
|
-
}
|
|
1050
|
-
}
|
|
1051
|
-
throw e
|
|
1052
|
-
}
|
|
1010
|
+
|
|
1011
|
+
if (intent !== "create" && (!reportHubId || Number.isNaN(reportHubId))) {
|
|
1012
|
+
return respond({ error: "Invalid reportHubId" }, { status: 400 })
|
|
1053
1013
|
}
|
|
1054
|
-
|
|
1014
|
+
|
|
1015
|
+
/**
|
|
1016
|
+
* Runs a boolean user-status toggle and maps its outcome onto a response.
|
|
1017
|
+
* 真偽値のユーザー状態トグルを実行し、結果をレスポンスへ写像する処理。
|
|
1018
|
+
*
|
|
1019
|
+
* `toggleStar` と `toggleRead` は必須フィールド名と呼び出すサービスだけが異なるため、
|
|
1020
|
+
* 検証・応答整形・エラー写像をここへ集約する。
|
|
1021
|
+
*
|
|
1022
|
+
* @param intentName Intent echoed back to the client. クライアントへ返す intent 名。
|
|
1023
|
+
* @param fieldName Required boolean form field. 必須の真偽値フォームフィールド名。
|
|
1024
|
+
* @param apply Service call performing the toggle. トグルを実行するサービス呼び出し。
|
|
1025
|
+
* @returns The HTTP response. HTTP レスポンス。
|
|
1026
|
+
*/
|
|
1027
|
+
const runStatusToggle = async (intentName: "toggleStar" | "toggleRead", fieldName: "isStarred" | "isRead", apply: (value: boolean) => Promise<{ isStarred: boolean; isRead: boolean }>): Promise<Response> => {
|
|
1028
|
+
const raw = formData.get(fieldName)
|
|
1029
|
+
if (raw === null) {
|
|
1030
|
+
return respond({ error: `${fieldName} required` }, { status: 400 })
|
|
1031
|
+
}
|
|
1055
1032
|
try {
|
|
1056
|
-
const
|
|
1057
|
-
|
|
1033
|
+
const updatedStatus = await apply(raw === "true")
|
|
1034
|
+
// クライアントは isStarred/isRead のみ参照する。生内部 ID (userId/created_by/updated_by) を
|
|
1035
|
+
// 含む行全体は返さず、必要なフラグだけに絞る (内部 ID の自己開示防止)。
|
|
1036
|
+
return respond({ status: "OK", intent: intentName, updatedStatus: { isStarred: updatedStatus.isStarred, isRead: updatedStatus.isRead }, operationTimestamp, reportHubId: String(reportHubId), clientTempId })
|
|
1058
1037
|
} catch (e: unknown) {
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
}
|
|
1063
|
-
if (e.message === "Not Found") {
|
|
1064
|
-
return jsonData({ error: "Report not found" }, { status: 404 })
|
|
1065
|
-
}
|
|
1038
|
+
// 不可視区分・不存在・削除済みは 404 へ写像する (未捕捉だと 500 になる)
|
|
1039
|
+
if (e instanceof Error && e.message === "Not Found") {
|
|
1040
|
+
return respond({ error: "Report not found" }, { status: 404 })
|
|
1066
1041
|
}
|
|
1067
1042
|
throw e
|
|
1068
1043
|
}
|
|
1069
1044
|
}
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1045
|
+
|
|
1046
|
+
switch (intent) {
|
|
1047
|
+
case "create": {
|
|
1048
|
+
if (!businessDate) {
|
|
1049
|
+
return respond({ error: "businessDate required" }, { status: 400 })
|
|
1050
|
+
}
|
|
1051
|
+
const newReport = await service.createDailyReport(userId, businessDate, clientTempId)
|
|
1052
|
+
return respond({
|
|
1074
1053
|
status: "OK",
|
|
1075
|
-
intent: "
|
|
1076
|
-
|
|
1077
|
-
|
|
1054
|
+
intent: "create",
|
|
1055
|
+
report: newReport,
|
|
1056
|
+
reportHubId: String(newReport.reportHubId),
|
|
1057
|
+
clientTempId, // Echo back for validation
|
|
1078
1058
|
})
|
|
1079
|
-
}
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1059
|
+
}
|
|
1060
|
+
case "update": {
|
|
1061
|
+
const title = formData.get("title") as string | undefined
|
|
1062
|
+
const content = formData.get("content") as string | undefined
|
|
1063
|
+
try {
|
|
1064
|
+
await service.updateDailyReport(reportHubId, userId, { title, content }, clientTempId)
|
|
1065
|
+
return respond({ status: "OK", intent: "update", reportHubId: String(reportHubId), clientTempId })
|
|
1066
|
+
} catch (e: unknown) {
|
|
1067
|
+
if (e instanceof Error) {
|
|
1068
|
+
if (e.message === "Unauthorized") {
|
|
1069
|
+
return respond({ error: "Unauthorized" }, { status: 403 })
|
|
1070
|
+
}
|
|
1071
|
+
if (e.message === "Not Found") {
|
|
1072
|
+
return respond({ error: "Report not found" }, { status: 404 })
|
|
1073
|
+
}
|
|
1086
1074
|
}
|
|
1075
|
+
throw e
|
|
1087
1076
|
}
|
|
1088
|
-
throw e
|
|
1089
1077
|
}
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
const safeComment = { ...newComment, userId: newComment.userId }
|
|
1103
|
-
return jsonData({ status: "OK", intent: "addComment", newComment: safeComment, reportHubId: String(reportHubId), clientTempId })
|
|
1104
|
-
} catch (e: unknown) {
|
|
1105
|
-
if (e instanceof Error) {
|
|
1106
|
-
// 削除直後の日報への投稿レース等。500 ではなく意味のある状態コードへ写像する
|
|
1107
|
-
if (e.message === "Not Found") {
|
|
1108
|
-
return jsonData({ error: "Report not found" }, { status: 404 })
|
|
1109
|
-
}
|
|
1110
|
-
if (e.message.startsWith("Comments are restricted")) {
|
|
1111
|
-
return jsonData({ error: "Comments are restricted for this report" }, { status: 403 })
|
|
1078
|
+
case "publish": {
|
|
1079
|
+
try {
|
|
1080
|
+
const publishedReport = await service.publishDailyReport(reportHubId, userId, clientTempId)
|
|
1081
|
+
return respond({ status: "OK", intent: "publish", reportHubId: String(reportHubId), clientTempId, report: publishedReport })
|
|
1082
|
+
} catch (e: unknown) {
|
|
1083
|
+
if (e instanceof Error) {
|
|
1084
|
+
if (e.message === "Unauthorized") {
|
|
1085
|
+
return respond({ error: "Unauthorized" }, { status: 403 })
|
|
1086
|
+
}
|
|
1087
|
+
if (e.message === "Not Found") {
|
|
1088
|
+
return respond({ error: "Report not found" }, { status: 404 })
|
|
1089
|
+
}
|
|
1112
1090
|
}
|
|
1091
|
+
throw e
|
|
1113
1092
|
}
|
|
1114
|
-
throw e
|
|
1115
1093
|
}
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1094
|
+
case "delete": {
|
|
1095
|
+
try {
|
|
1096
|
+
await service.deleteDailyReport(reportHubId, userId, clientTempId)
|
|
1097
|
+
return respond({
|
|
1098
|
+
status: "OK",
|
|
1099
|
+
intent: "delete",
|
|
1100
|
+
reportHubId: String(reportHubId),
|
|
1101
|
+
clientTempId, // Echo back for validation even on delete
|
|
1102
|
+
})
|
|
1103
|
+
} catch (e: unknown) {
|
|
1104
|
+
if (e instanceof Error) {
|
|
1105
|
+
if (e.message === "Unauthorized") {
|
|
1106
|
+
return respond({ error: "Unauthorized" }, { status: 403 })
|
|
1107
|
+
}
|
|
1108
|
+
if (e.message === "Not Found") {
|
|
1109
|
+
return respond({ error: "Report not found" }, { status: 404 })
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
throw e
|
|
1113
|
+
}
|
|
1121
1114
|
}
|
|
1122
|
-
|
|
1123
|
-
await service.
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1115
|
+
case "toggleStar":
|
|
1116
|
+
return await runStatusToggle("toggleStar", "isStarred", (value) => service.setStarStatus(scope, userId, reportHubId, businessDate, value, clientTempId))
|
|
1117
|
+
case "toggleRead":
|
|
1118
|
+
return await runStatusToggle("toggleRead", "isRead", (value) => service.setReadStatus(scope, userId, reportHubId, businessDate, value, clientTempId))
|
|
1119
|
+
case "addComment": {
|
|
1120
|
+
const content = formData.get("content") as string
|
|
1121
|
+
if (!content) {
|
|
1122
|
+
return respond({ error: "Content required" }, { status: 400 })
|
|
1123
|
+
}
|
|
1124
|
+
try {
|
|
1125
|
+
const newComment = await service.addComment(scope, userId, reportHubId, content, businessDate, clientTempId)
|
|
1126
|
+
const safeComment = { ...newComment, userId: newComment.userId }
|
|
1127
|
+
return respond({ status: "OK", intent: "addComment", newComment: safeComment, reportHubId: String(reportHubId), clientTempId })
|
|
1128
|
+
} catch (e: unknown) {
|
|
1129
|
+
if (e instanceof Error) {
|
|
1130
|
+
// 削除直後の日報への投稿レース等。500 ではなく意味のある状態コードへ写像する
|
|
1131
|
+
if (e.message === "Not Found") {
|
|
1132
|
+
return respond({ error: "Report not found" }, { status: 404 })
|
|
1133
|
+
}
|
|
1134
|
+
if (e.message.startsWith("Comments are restricted")) {
|
|
1135
|
+
return respond({ error: "Comments are restricted for this report" }, { status: 403 })
|
|
1136
|
+
}
|
|
1129
1137
|
}
|
|
1130
|
-
|
|
1131
|
-
|
|
1138
|
+
throw e
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
case "deleteComment": {
|
|
1142
|
+
const commentId = Number(formData.get("commentId"))
|
|
1143
|
+
if (!commentId || Number.isNaN(commentId)) {
|
|
1144
|
+
return respond({ error: "Invalid commentId" }, { status: 400 })
|
|
1145
|
+
}
|
|
1146
|
+
try {
|
|
1147
|
+
await service.deleteComment(scope, userId, reportHubId, commentId, businessDate, clientTempId)
|
|
1148
|
+
return respond({ status: "OK", reportHubId: String(reportHubId), deletedCommentId: String(commentId), clientTempId })
|
|
1149
|
+
} catch (e: unknown) {
|
|
1150
|
+
if (e instanceof Error) {
|
|
1151
|
+
if (e.message === "Unauthorized") {
|
|
1152
|
+
return respond({ error: "Unauthorized" }, { status: 403 })
|
|
1153
|
+
}
|
|
1154
|
+
if (e.message === "Not Found") {
|
|
1155
|
+
return respond({ error: "Comment not found" }, { status: 404 })
|
|
1156
|
+
}
|
|
1132
1157
|
}
|
|
1158
|
+
throw e
|
|
1133
1159
|
}
|
|
1134
|
-
throw e
|
|
1135
1160
|
}
|
|
1161
|
+
default:
|
|
1162
|
+
return respond({ error: "Invalid intent" }, { status: 400 })
|
|
1136
1163
|
}
|
|
1137
|
-
|
|
1138
|
-
|
|
1164
|
+
} catch (error) {
|
|
1165
|
+
// React Router のリダイレクト等は握り潰さず素通しする
|
|
1166
|
+
if (error instanceof Response) throw error
|
|
1167
|
+
const elapsed = Date.now() - actionStartTime
|
|
1168
|
+
const err = error instanceof Error ? error : new Error(String(error))
|
|
1169
|
+
apiLogger.error(`500 action elapsed=${elapsed}ms name=${err.name} message=${err.message}`)
|
|
1170
|
+
apiLogger.error("Stack:", err.stack)
|
|
1171
|
+
return respond({ error: "Internal Server Error" }, { status: 500 })
|
|
1139
1172
|
}
|
|
1140
1173
|
}
|
|
1141
1174
|
|
|
@@ -1147,20 +1180,29 @@ export function createDailyReportHandlers(config: DailyReportHandlersConfig) {
|
|
|
1147
1180
|
*/
|
|
1148
1181
|
const sseLoader = async ({ request, params }: LoaderArgs) => {
|
|
1149
1182
|
// 認証チェック
|
|
1150
|
-
const { user } = await authenticate(request, { failureRedirect: null })
|
|
1183
|
+
const { user, cookie } = await authenticate(request, { failureRedirect: null })
|
|
1184
|
+
// セッション cookie は**この応答がどれになろうと**転送する。トークンが更新された
|
|
1185
|
+
// リクエストがたまたま SSE 接続だった場合、ここで落とすと更新後のトークンがブラウザへ
|
|
1186
|
+
// 届かず、以降のリクエストが毎回リフレッシュをやり直すことになる。破棄 cookie を
|
|
1187
|
+
// 落とした場合はさらに悪く、無効セッションがブラウザに残り続ける (apiAction と同じ理由)。
|
|
1188
|
+
// SSE は接続が長命なぶん、更新を担うリクエストになりやすい
|
|
1189
|
+
const sessionHeaders = (extra?: Record<string, string>): Record<string, string> | undefined => {
|
|
1190
|
+
if (cookie === undefined) return extra
|
|
1191
|
+
return { ...(extra ?? {}), "Set-Cookie": cookie }
|
|
1192
|
+
}
|
|
1151
1193
|
if (!user) {
|
|
1152
|
-
return new Response("Unauthorized", { status: 401 })
|
|
1194
|
+
return new Response("Unauthorized", { status: 401, headers: sessionHeaders() })
|
|
1153
1195
|
}
|
|
1154
1196
|
|
|
1155
1197
|
// 内部ユーザー ID の解決
|
|
1156
1198
|
const internalUserId = await service.getUserIdByExternalId(user.id)
|
|
1157
1199
|
if (!internalUserId) {
|
|
1158
|
-
return new Response("Forbidden", { status: 403 })
|
|
1200
|
+
return new Response("Forbidden", { status: 403, headers: sessionHeaders() })
|
|
1159
1201
|
}
|
|
1160
1202
|
|
|
1161
1203
|
// エンドポイントの検証
|
|
1162
1204
|
if (params.endpoint !== "updates") {
|
|
1163
|
-
return new Response("Not Found", { status: 404 })
|
|
1205
|
+
return new Response("Not Found", { status: 404, headers: sessionHeaders() })
|
|
1164
1206
|
}
|
|
1165
1207
|
|
|
1166
1208
|
// 可視集合は接続時に解決し、以降 keep-alive tick に相乗りして再解決する (D6)。
|
|
@@ -1422,11 +1464,11 @@ export function createDailyReportHandlers(config: DailyReportHandlersConfig) {
|
|
|
1422
1464
|
})
|
|
1423
1465
|
|
|
1424
1466
|
return new Response(stream, {
|
|
1425
|
-
headers: {
|
|
1467
|
+
headers: sessionHeaders({
|
|
1426
1468
|
"Content-Type": "text/event-stream",
|
|
1427
1469
|
"Cache-Control": "no-cache, no-transform",
|
|
1428
1470
|
Connection: "keep-alive",
|
|
1429
|
-
},
|
|
1471
|
+
}),
|
|
1430
1472
|
})
|
|
1431
1473
|
}
|
|
1432
1474
|
|