@aiquants/auth-react-router 0.15.0 → 0.16.0
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 +100 -7
- package/dist/admin.d.mts +13 -436
- package/dist/admin.d.ts +13 -436
- package/dist/admin.js +1 -2
- package/dist/admin.mjs +1 -2
- package/dist/client.d.mts +1 -10
- package/dist/client.d.ts +1 -10
- package/dist/client.js +0 -1
- package/dist/client.mjs +0 -1
- package/dist/server.d.mts +6 -146
- package/dist/server.d.ts +6 -146
- package/dist/server.js +0 -1
- package/dist/server.mjs +0 -1
- package/package.json +12 -16
- package/dist/admin.js.map +0 -1
- package/dist/admin.mjs.map +0 -1
- package/dist/client.js.map +0 -1
- package/dist/client.mjs.map +0 -1
- package/dist/server.js.map +0 -1
- package/dist/server.mjs.map +0 -1
- package/src/admin/app.ts +0 -9
- package/src/admin/format.ts +0 -58
- package/src/admin/labels.ts +0 -838
- package/src/admin/server.ts +0 -1143
- package/src/admin/ui.tsx +0 -276
- package/src/admin/views.tsx +0 -2120
- package/src/admin.ts +0 -5
- package/src/client/GoogleForm.tsx +0 -47
- package/src/client.ts +0 -6
- package/src/server/auth-server.ts +0 -69
- package/src/server/authenticate-in-loader.ts +0 -243
- package/src/server/authenticate.ts +0 -224
- package/src/server/google-strategy-impl.ts +0 -154
- package/src/server/google-strategy.ts +0 -183
- package/src/server/handlers.ts +0 -139
- package/src/server/logger.ts +0 -14
- package/src/server/ports.ts +0 -113
- package/src/server/session.ts +0 -132
- package/src/server.ts +0 -14
- package/src/styles/standalone.entry.css +0 -6
|
@@ -1,154 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @module google-strategy-impl
|
|
3
|
-
* @description Self-contained Google OAuth2 strategy (ported from @coji/remix-auth-google).
|
|
4
|
-
* 自己完結型の Google OAuth2 ストラテジー (@coji/remix-auth-google から移植)。
|
|
5
|
-
*
|
|
6
|
-
* @coji/remix-auth-google@4.2.0 の実装 (68 行) を挙動保存で移植し、外部依存を除去する。
|
|
7
|
-
* OAuth2 の本体機構 (PKCE / state / トークン交換) は継承元の remix-auth-oauth2 が担うため
|
|
8
|
-
* 移植対象ではない。ここに置くのは Google 固有の設定 (エンドポイント URL・既定スコープ)、
|
|
9
|
-
* 認可パラメータの上書き (access_type / include_granted_scopes / prompt / hd / login_hint)、
|
|
10
|
-
* および userinfo (v3) からプロファイルを取得する静的メソッドのみ。
|
|
11
|
-
*
|
|
12
|
-
* 既定スコープ・URL・プロファイル整形 (sub→id、picture→photos、_json=raw) は原本と byte 一致。
|
|
13
|
-
*/
|
|
14
|
-
import { OAuth2Strategy } from "remix-auth-oauth2"
|
|
15
|
-
|
|
16
|
-
/** Google の既定スコープ (原本と同一。フル URL 形式)。 */
|
|
17
|
-
export const GoogleStrategyDefaultScopes = ["openid", "https://www.googleapis.com/auth/userinfo.profile", "https://www.googleapis.com/auth/userinfo.email"]
|
|
18
|
-
|
|
19
|
-
/** ストラテジー名 (remix-auth の Authenticator 登録キー)。 */
|
|
20
|
-
export const GoogleStrategyDefaultName = "google"
|
|
21
|
-
|
|
22
|
-
// userinfo エンドポイント (v3)。プロファイル取得のみに用いる
|
|
23
|
-
const userInfoURL = "https://www.googleapis.com/oauth2/v3/userinfo"
|
|
24
|
-
|
|
25
|
-
/** @see https://developers.google.com/identity/protocols/oauth2/scopes */
|
|
26
|
-
export type GoogleScope = string
|
|
27
|
-
|
|
28
|
-
/** GoogleStrategy のコンストラクタオプション (原本 @coji と同一)。 */
|
|
29
|
-
export type GoogleStrategyOptions = {
|
|
30
|
-
clientId: string
|
|
31
|
-
clientSecret: string
|
|
32
|
-
redirectURI: string
|
|
33
|
-
/** @default "openid profile email" 相当 (GoogleStrategyDefaultScopes)。 */
|
|
34
|
-
scopes?: GoogleScope[]
|
|
35
|
-
accessType?: "online" | "offline"
|
|
36
|
-
includeGrantedScopes?: boolean
|
|
37
|
-
prompt?: "none" | "consent" | "select_account"
|
|
38
|
-
hd?: string
|
|
39
|
-
loginHint?: string
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/** userinfo から取得・整形した Google プロファイル (原本 GoogleProfile と同一形状)。 */
|
|
43
|
-
export type GoogleProfile = {
|
|
44
|
-
id: string
|
|
45
|
-
displayName: string
|
|
46
|
-
name: {
|
|
47
|
-
familyName: string
|
|
48
|
-
givenName: string
|
|
49
|
-
}
|
|
50
|
-
emails: [{ value: string }]
|
|
51
|
-
photos: [{ value: string }]
|
|
52
|
-
_json: {
|
|
53
|
-
sub: string
|
|
54
|
-
name: string
|
|
55
|
-
given_name: string
|
|
56
|
-
family_name: string
|
|
57
|
-
picture: string
|
|
58
|
-
locale: string
|
|
59
|
-
email: string
|
|
60
|
-
email_verified: boolean
|
|
61
|
-
hd: string
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
// userProfile が必要とする最小のトークン形状 (arctic の OAuth2Tokens の部分集合)。
|
|
66
|
-
// 直接 arctic へ依存しないための構造的型。実際に呼ぶのは accessToken() のみ。
|
|
67
|
-
type OAuth2AccessTokenSource = { accessToken(): string }
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* Google OAuth2 strategy for remix-auth (extends remix-auth-oauth2's OAuth2Strategy).
|
|
71
|
-
* remix-auth 用の Google OAuth2 ストラテジー (remix-auth-oauth2 の OAuth2Strategy を継承)。
|
|
72
|
-
*/
|
|
73
|
-
export class GoogleStrategy<User> extends OAuth2Strategy<User> {
|
|
74
|
-
name = GoogleStrategyDefaultName
|
|
75
|
-
private readonly scopes: GoogleScope[]
|
|
76
|
-
private readonly accessType: "online" | "offline"
|
|
77
|
-
private readonly prompt?: "none" | "consent" | "select_account"
|
|
78
|
-
private readonly includeGrantedScopes: boolean
|
|
79
|
-
private readonly hd?: string
|
|
80
|
-
private readonly loginHint?: string
|
|
81
|
-
|
|
82
|
-
constructor({ clientId, clientSecret, redirectURI, scopes, accessType, includeGrantedScopes, prompt, hd, loginHint }: GoogleStrategyOptions, verify: OAuth2Strategy<User>["verify"]) {
|
|
83
|
-
const scopeList = scopes ?? GoogleStrategyDefaultScopes
|
|
84
|
-
super(
|
|
85
|
-
{
|
|
86
|
-
clientId,
|
|
87
|
-
clientSecret,
|
|
88
|
-
redirectURI,
|
|
89
|
-
authorizationEndpoint: "https://accounts.google.com/o/oauth2/v2/auth",
|
|
90
|
-
tokenEndpoint: "https://oauth2.googleapis.com/token",
|
|
91
|
-
scopes: scopeList,
|
|
92
|
-
},
|
|
93
|
-
verify,
|
|
94
|
-
)
|
|
95
|
-
this.scopes = scopeList
|
|
96
|
-
this.accessType = accessType ?? "online"
|
|
97
|
-
this.includeGrantedScopes = includeGrantedScopes ?? false
|
|
98
|
-
this.prompt = prompt
|
|
99
|
-
this.hd = hd
|
|
100
|
-
this.loginHint = loginHint
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
/**
|
|
104
|
-
* Adds Google-specific authorization parameters to the redirect URL.
|
|
105
|
-
* 認可リダイレクト URL へ Google 固有パラメータを付与する。
|
|
106
|
-
*/
|
|
107
|
-
protected authorizationParams(params: URLSearchParams, _request?: Request): URLSearchParams {
|
|
108
|
-
params.set("access_type", this.accessType)
|
|
109
|
-
params.set("include_granted_scopes", String(this.includeGrantedScopes))
|
|
110
|
-
if (this.scopes && this.scopes.length > 0) {
|
|
111
|
-
params.set("scope", this.scopes.join(" "))
|
|
112
|
-
}
|
|
113
|
-
if (this.prompt) {
|
|
114
|
-
params.set("prompt", this.prompt)
|
|
115
|
-
}
|
|
116
|
-
if (this.hd) {
|
|
117
|
-
params.set("hd", this.hd)
|
|
118
|
-
}
|
|
119
|
-
if (this.loginHint) {
|
|
120
|
-
params.set("login_hint", this.loginHint)
|
|
121
|
-
}
|
|
122
|
-
return params
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
/**
|
|
126
|
-
* Fetches the Google user profile from the userinfo (v3) endpoint.
|
|
127
|
-
* userinfo (v3) エンドポイントから Google ユーザープロファイルを取得する。
|
|
128
|
-
*
|
|
129
|
-
* @throws プロファイル取得に失敗した場合 (レスポンスが ok でない)。
|
|
130
|
-
*/
|
|
131
|
-
static async userProfile(tokens: OAuth2AccessTokenSource): Promise<GoogleProfile> {
|
|
132
|
-
const response = await fetch(userInfoURL, {
|
|
133
|
-
headers: {
|
|
134
|
-
Authorization: `Bearer ${tokens.accessToken()}`,
|
|
135
|
-
},
|
|
136
|
-
})
|
|
137
|
-
if (!response.ok) {
|
|
138
|
-
throw new Error(`Failed to fetch user profile: ${response.statusText}`)
|
|
139
|
-
}
|
|
140
|
-
const raw = await response.json()
|
|
141
|
-
const profile: GoogleProfile = {
|
|
142
|
-
id: raw.sub,
|
|
143
|
-
displayName: raw.name,
|
|
144
|
-
name: {
|
|
145
|
-
familyName: raw.family_name,
|
|
146
|
-
givenName: raw.given_name,
|
|
147
|
-
},
|
|
148
|
-
emails: [{ value: raw.email }],
|
|
149
|
-
photos: [{ value: raw.picture }],
|
|
150
|
-
_json: raw,
|
|
151
|
-
}
|
|
152
|
-
return profile
|
|
153
|
-
}
|
|
154
|
-
}
|
|
@@ -1,183 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @module google-strategy
|
|
3
|
-
* @description Google OAuth authenticator factory (remix-auth v4 + local GoogleStrategy).
|
|
4
|
-
* Google OAuth 認証器ファクトリ (remix-auth v4 + 同梱の GoogleStrategy)。
|
|
5
|
-
*
|
|
6
|
-
* 原本 (quants-react-router auth.server.ts) の verify コールバックを挙動保存で移植。
|
|
7
|
-
* env 読取り・backend HTTP・allowlist・エラーメッセージはすべてポート/設定注入に置き換えた。
|
|
8
|
-
* 検証順序 (Google プロフィール検証 → backend verify → allowlist → backend signup)、
|
|
9
|
-
* リダイレクト先 (`<loginPath>?error=<encoded>`)、エラーメッセージ文言は原本と等価。
|
|
10
|
-
*/
|
|
11
|
-
import type { MyGoogleProfile } from "@aiquants/auth-core"
|
|
12
|
-
import { type AuthMessages, resolveAuthMessages } from "@aiquants/auth-core"
|
|
13
|
-
import { redirect } from "react-router"
|
|
14
|
-
import { Authenticator } from "remix-auth"
|
|
15
|
-
import { GoogleStrategy } from "./google-strategy-impl"
|
|
16
|
-
import { defaultLogger } from "./logger"
|
|
17
|
-
import type { AuthServerConfig } from "./ports"
|
|
18
|
-
|
|
19
|
-
interface GoogleToken {
|
|
20
|
-
id_token?: string
|
|
21
|
-
access_token?: string
|
|
22
|
-
refresh_token?: string
|
|
23
|
-
expires_in?: number
|
|
24
|
-
scope?: string
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Builds a remix-auth Authenticator with the Google strategy wired to injected ports.
|
|
29
|
-
* 注入ポートに結線した Google ストラテジー付き Authenticator を生成する。
|
|
30
|
-
*/
|
|
31
|
-
export const createGoogleAuthenticator = (config: AuthServerConfig): Authenticator<MyGoogleProfile> => {
|
|
32
|
-
const logger = config.logger ?? defaultLogger
|
|
33
|
-
const messages: AuthMessages = resolveAuthMessages(config.messages)
|
|
34
|
-
const loginPath = config.loginPath ?? "/auth/login"
|
|
35
|
-
|
|
36
|
-
const googleStrategy = new GoogleStrategy<MyGoogleProfile>(
|
|
37
|
-
{
|
|
38
|
-
clientId: config.google.clientId,
|
|
39
|
-
clientSecret: config.google.clientSecret,
|
|
40
|
-
redirectURI: config.google.redirectURI,
|
|
41
|
-
scopes: config.google.scopes,
|
|
42
|
-
accessType: config.google.accessType ?? "offline",
|
|
43
|
-
includeGrantedScopes: config.google.includeGrantedScopes ?? true,
|
|
44
|
-
prompt: config.google.prompt ?? "consent",
|
|
45
|
-
},
|
|
46
|
-
/**
|
|
47
|
-
* Google 認証後のコールバック。Google のトークンとプロフィールを検証し、
|
|
48
|
-
* backend verify → allowlist → backend signup を経て MyGoogleProfile を返す。
|
|
49
|
-
* 失敗時は `<loginPath>?error=<encoded>` へのリダイレクト Response を throw する。
|
|
50
|
-
*/
|
|
51
|
-
async ({ request: _request, tokens }) => {
|
|
52
|
-
const { access_token: accessToken, refresh_token: refreshToken, expires_in: expiresIn, scope, id_token: _idToken } = tokens.data as GoogleToken
|
|
53
|
-
const {
|
|
54
|
-
_json: { sub, email, picture, hd: _hd, email_verified },
|
|
55
|
-
photos: _photos,
|
|
56
|
-
...profile
|
|
57
|
-
} = await GoogleStrategy.userProfile(tokens)
|
|
58
|
-
const {
|
|
59
|
-
id,
|
|
60
|
-
displayName,
|
|
61
|
-
name: { familyName: _familyName, givenName: _givenName },
|
|
62
|
-
emails: _emails,
|
|
63
|
-
} = profile
|
|
64
|
-
const provider = "google"
|
|
65
|
-
|
|
66
|
-
// first verify (トークン/プロフィール欠落は Google 認証失敗として扱う)
|
|
67
|
-
if (
|
|
68
|
-
accessToken === undefined ||
|
|
69
|
-
refreshToken === undefined ||
|
|
70
|
-
expiresIn === undefined ||
|
|
71
|
-
scope === undefined ||
|
|
72
|
-
id === undefined ||
|
|
73
|
-
displayName === undefined ||
|
|
74
|
-
sub === undefined ||
|
|
75
|
-
email === undefined ||
|
|
76
|
-
email_verified === undefined
|
|
77
|
-
// hd === undefined // Google Workspace などの組織制限をする場合は有効化
|
|
78
|
-
) {
|
|
79
|
-
if (email_verified === false) {
|
|
80
|
-
const errorMessage = encodeURIComponent(messages.emailNotVerified)
|
|
81
|
-
throw redirect(`${loginPath}?error=${errorMessage}`)
|
|
82
|
-
}
|
|
83
|
-
const errorMessage = encodeURIComponent(messages.googleFailed)
|
|
84
|
-
throw redirect(`${loginPath}?error=${errorMessage}`)
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
logger.info("Authenticated on Google: ", profile.displayName)
|
|
88
|
-
|
|
89
|
-
// Backend Authentication
|
|
90
|
-
try {
|
|
91
|
-
// backendAuth ポート未指定時は verify/signup をスキップ (フロント単独運用)
|
|
92
|
-
if (config.backendAuth) {
|
|
93
|
-
const ok = await config.backendAuth.verify(provider, accessToken)
|
|
94
|
-
|
|
95
|
-
if (!ok) {
|
|
96
|
-
logger.error("Backend verification returned false for user:", profile.displayName)
|
|
97
|
-
const errorMessage = encodeURIComponent(messages.backendFailed)
|
|
98
|
-
throw redirect(`${loginPath}?error=${errorMessage}`)
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
logger.info("Authenticated on Backend: ", profile.displayName)
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
// メールアドレスまたはドメインベースでのアクセス制限チェック (論理和は allowlist 実装側)
|
|
105
|
-
// ⚠️ `await` 必須。非同期実装を await せずに評価すると Promise が truthy 判定され
|
|
106
|
-
// 許可判定を素通りする (fail-open)。この await は spec で門番されている。
|
|
107
|
-
const userEmail = profile.emails[0]?.value
|
|
108
|
-
if (!(await config.isEmailAllowed(userEmail))) {
|
|
109
|
-
const error = new Error("You are not allowed to access this application")
|
|
110
|
-
logger.error(error)
|
|
111
|
-
throw error
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
// 無効化済みアカウントはログインさせない (allowlist とは別軸の判定)
|
|
115
|
-
if (config.isUserActive && !(await config.isUserActive(profile))) {
|
|
116
|
-
const error = new Error("You are not allowed to access this application")
|
|
117
|
-
logger.error("Deactivated account attempted to sign in:", userEmail)
|
|
118
|
-
throw error
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
// バックエンドでのサインアップ処理 (毎ログイン実行の挙動を保存)
|
|
122
|
-
if (config.backendAuth) {
|
|
123
|
-
await config.backendAuth.signup(provider, accessToken, profile)
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
logger.info("Authenticated complete: ", profile.displayName)
|
|
127
|
-
|
|
128
|
-
// 認証ペイロードの構築
|
|
129
|
-
const expirationDateMs = Date.now() + expiresIn * 1000
|
|
130
|
-
const authenticationPayload = {
|
|
131
|
-
accessToken: accessToken,
|
|
132
|
-
refreshToken: refreshToken,
|
|
133
|
-
expirationDateMs: expirationDateMs,
|
|
134
|
-
provider: provider,
|
|
135
|
-
}
|
|
136
|
-
// ユーザープロファイル情報
|
|
137
|
-
const user: MyGoogleProfile = {
|
|
138
|
-
...profile,
|
|
139
|
-
...authenticationPayload,
|
|
140
|
-
photo: picture,
|
|
141
|
-
}
|
|
142
|
-
return user
|
|
143
|
-
} catch (error: unknown) {
|
|
144
|
-
let loggable = error
|
|
145
|
-
if (typeof error === "object" && error !== null && "cause" in error) {
|
|
146
|
-
// cause があればそれを優先してログに残す (falsy なら元のエラー)
|
|
147
|
-
loggable = (error as { cause: unknown }).cause || error
|
|
148
|
-
}
|
|
149
|
-
logger.error("Error during backend authentication or subsequent processing:", loggable)
|
|
150
|
-
|
|
151
|
-
let friendlyErrorMessage = messages.unexpected
|
|
152
|
-
let conditionMet = false
|
|
153
|
-
|
|
154
|
-
// ネットワーク到達不可 (getaddrinfo ENOTFOUND) の判定
|
|
155
|
-
if (typeof error === "object" && error !== null && "cause" in error) {
|
|
156
|
-
const cause = (error as { cause: unknown }).cause
|
|
157
|
-
if (cause && typeof cause === "object" && "message" in cause && typeof (cause as { message: string }).message === "string" && (cause as { message: string }).message.includes("getaddrinfo ENOTFOUND")) {
|
|
158
|
-
friendlyErrorMessage = messages.backendUnreachable
|
|
159
|
-
conditionMet = true
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
// allowlist 拒否の判定
|
|
164
|
-
if (!conditionMet && error instanceof Error && error.message.includes("You are not allowed to access this application")) {
|
|
165
|
-
friendlyErrorMessage = messages.notAllowed
|
|
166
|
-
conditionMet = true
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
// 既にリダイレクト Response が throw されている場合はそのまま再 throw
|
|
170
|
-
if (!conditionMet && error instanceof Response && error.headers.get("Location")?.startsWith(loginPath)) {
|
|
171
|
-
throw error
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
const encodedErrorMessage = encodeURIComponent(friendlyErrorMessage)
|
|
175
|
-
throw redirect(`${loginPath}?error=${encodedErrorMessage}`)
|
|
176
|
-
}
|
|
177
|
-
},
|
|
178
|
-
)
|
|
179
|
-
|
|
180
|
-
const authenticator = new Authenticator<MyGoogleProfile>()
|
|
181
|
-
authenticator.use(googleStrategy)
|
|
182
|
-
return authenticator
|
|
183
|
-
}
|
package/src/server/handlers.ts
DELETED
|
@@ -1,139 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @module handlers
|
|
3
|
-
* @description Route handler factories for login / logout / Google callback.
|
|
4
|
-
* login / logout / Google コールバックのルートハンドラ工場。
|
|
5
|
-
*
|
|
6
|
-
* 原本 (quants-react-router routes/_auth+ の 4 ルート + features/logoutAndRedirect.ts) の
|
|
7
|
-
* サーバー側ロジックを挙動保存で移植。UI (JSX) はアプリ側に残す。
|
|
8
|
-
* コールバックのセッション保存形状 (SessionProfile、photo/_json/photos を含まない) は
|
|
9
|
-
* cookie byte 互換のため厳密に保存する。
|
|
10
|
-
*/
|
|
11
|
-
import type { AuthenticationPayload, MyGoogleProfile, SessionProfile } from "@aiquants/auth-core"
|
|
12
|
-
import { redirect } from "react-router"
|
|
13
|
-
import type { Authenticator } from "remix-auth"
|
|
14
|
-
import type { AuthServerConfig, LoggerPort } from "./ports"
|
|
15
|
-
import type { AuthSessionStorage } from "./session"
|
|
16
|
-
|
|
17
|
-
/** createAuthHandlers が返すハンドラ束。 */
|
|
18
|
-
export type AuthHandlers = ReturnType<typeof createAuthHandlers>
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Builds loader/action handlers for the auth routes.
|
|
22
|
-
* 認証ルート用の loader/action ハンドラ群を生成する。
|
|
23
|
-
*/
|
|
24
|
-
export const createAuthHandlers = (config: AuthServerConfig, storage: AuthSessionStorage, authenticator: Authenticator<MyGoogleProfile>, logger: LoggerPort) => {
|
|
25
|
-
const loginPath = config.loginPath ?? "/auth/login"
|
|
26
|
-
const postLoginRedirect = config.postLoginRedirect ?? "/"
|
|
27
|
-
const postLogoutRedirect = config.postLogoutRedirect ?? "/"
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Login-page loader: redirects authenticated users, passes the error message through.
|
|
31
|
-
* ログインページ loader。認証済みならリダイレクトし、エラーメッセージを data として返す。
|
|
32
|
-
*/
|
|
33
|
-
const loginLoader = async ({ request }: { request: Request }): Promise<{ error: string | null }> => {
|
|
34
|
-
const user = await storage.getSessionUser(request)
|
|
35
|
-
if (user) {
|
|
36
|
-
// 認証済みならトップページにリダイレクト
|
|
37
|
-
throw redirect(postLoginRedirect)
|
|
38
|
-
}
|
|
39
|
-
const url = new URL(request.url)
|
|
40
|
-
const error = url.searchParams.get("error")
|
|
41
|
-
return { error } // エラーメッセージを loader data として渡す
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* Login action: starts the Google OAuth flow for the "Sign In with Google" form action.
|
|
46
|
-
* ログイン action。"Sign In with Google" フォーム action で Google OAuth フローを開始する。
|
|
47
|
-
*/
|
|
48
|
-
const loginAction = async ({ request }: { request: Request }): Promise<MyGoogleProfile | null> => {
|
|
49
|
-
const formData = await request.clone().formData()
|
|
50
|
-
const action = String(formData.get("_action"))
|
|
51
|
-
|
|
52
|
-
let user: MyGoogleProfile | null = null
|
|
53
|
-
switch (action) {
|
|
54
|
-
case "Sign In with Google":
|
|
55
|
-
user = await authenticator.authenticate("google", request)
|
|
56
|
-
break
|
|
57
|
-
default:
|
|
58
|
-
user = null
|
|
59
|
-
}
|
|
60
|
-
return user
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* Destroys the session and returns the Set-Cookie value (logout primitive).
|
|
65
|
-
* セッションを破棄して Set-Cookie 値を返す (ログアウトの基本操作)。
|
|
66
|
-
*/
|
|
67
|
-
const logout = async (request: Request) => {
|
|
68
|
-
return await storage.logout(request)
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* Logs out and redirects with the destroyed-session cookie attached.
|
|
73
|
-
* ログアウトし、破棄済みセッション Cookie を付与してリダイレクトする。
|
|
74
|
-
*/
|
|
75
|
-
const logoutAndRedirect = async (request: Request) => {
|
|
76
|
-
const cookie = await logout(request)
|
|
77
|
-
const headers = new Headers()
|
|
78
|
-
headers.append("Set-Cookie", cookie)
|
|
79
|
-
return redirect(postLogoutRedirect, { headers })
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* Logout-route loader: performs logout and throws the redirect.
|
|
84
|
-
* ログアウトルート loader。ログアウトを実行してリダイレクトを throw する。
|
|
85
|
-
*/
|
|
86
|
-
const logoutLoader = async ({ request }: { request: Request }): Promise<never> => {
|
|
87
|
-
throw await logoutAndRedirect(request)
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
/**
|
|
91
|
-
* Google OAuth callback loader: authenticates, saves the session and redirects home.
|
|
92
|
-
* Google OAuth コールバック loader。認証してセッションを保存し、ホームへリダイレクトする。
|
|
93
|
-
*/
|
|
94
|
-
const googleCallbackLoader = async ({ request }: { request: Request }): Promise<never> => {
|
|
95
|
-
try {
|
|
96
|
-
const googleProfile = await authenticator.authenticate("google", request)
|
|
97
|
-
|
|
98
|
-
// 既存のセッションから前回のユーザー情報を取得して refresh_token を保護する
|
|
99
|
-
const existingSession = await storage.getSession(request)
|
|
100
|
-
const existingUser = existingSession.get(storage.sessionKey) as SessionProfile | undefined
|
|
101
|
-
const previousRefreshToken = existingUser?.refreshToken
|
|
102
|
-
|
|
103
|
-
// 認証ペイロードの構築
|
|
104
|
-
const authenticationPayload: AuthenticationPayload = {
|
|
105
|
-
accessToken: googleProfile.accessToken,
|
|
106
|
-
refreshToken: googleProfile.refreshToken || previousRefreshToken,
|
|
107
|
-
expirationDateMs: googleProfile.expirationDateMs,
|
|
108
|
-
provider: googleProfile.provider,
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
// セッション内のユーザ情報 (photo/_json/photos を含まない byte 互換形状)
|
|
112
|
-
const user: SessionProfile = {
|
|
113
|
-
id: googleProfile.id,
|
|
114
|
-
displayName: googleProfile.displayName,
|
|
115
|
-
name: googleProfile.name,
|
|
116
|
-
emails: googleProfile.emails,
|
|
117
|
-
...authenticationPayload,
|
|
118
|
-
role: googleProfile.role,
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
// 認証後のユーザ情報をセッションに保存
|
|
122
|
-
const cookie = await storage.saveSession(request, user)
|
|
123
|
-
|
|
124
|
-
// ホームへリダイレクト
|
|
125
|
-
throw redirect(postLoginRedirect, { headers: { "Set-Cookie": cookie } })
|
|
126
|
-
} catch (error) {
|
|
127
|
-
// authenticator.authenticate は認証失敗時に Response か Error を throw する。
|
|
128
|
-
// access_denied のような OAuth エラーでは Error の可能性があるため、
|
|
129
|
-
// どちらの場合でもログインページへのリダイレクトが安全 (Response はそのまま再 throw)。
|
|
130
|
-
if (error instanceof Response) {
|
|
131
|
-
throw error
|
|
132
|
-
}
|
|
133
|
-
logger.error("Authentication error:", error)
|
|
134
|
-
throw redirect(loginPath)
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
return { loginLoader, loginAction, logoutLoader, logout, logoutAndRedirect, googleCallbackLoader }
|
|
139
|
-
}
|
package/src/server/logger.ts
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @module logger
|
|
3
|
-
* @description Default console-backed logger used when no logger port is injected.
|
|
4
|
-
* ロガーポート未注入時に使う console ベースの既定ロガー。
|
|
5
|
-
*/
|
|
6
|
-
import type { LoggerPort } from "./ports"
|
|
7
|
-
|
|
8
|
-
/** console ベースの既定ロガー (ライブラリ既定のフォールバックとして意図的に console を使用)。 */
|
|
9
|
-
export const defaultLogger: LoggerPort = {
|
|
10
|
-
// biome-ignore lint/suspicious/noConsole: ロガー未注入時の意図的な console フォールバック
|
|
11
|
-
info: (...args: unknown[]) => console.info("[Auth]", ...args),
|
|
12
|
-
warn: (...args: unknown[]) => console.warn("[Auth]", ...args),
|
|
13
|
-
error: (...args: unknown[]) => console.error("[Auth]", ...args),
|
|
14
|
-
}
|
package/src/server/ports.ts
DELETED
|
@@ -1,113 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @module ports
|
|
3
|
-
* @description DI port types for the React Router auth adapter.
|
|
4
|
-
* React Router 認証アダプタの DI ポート型定義。
|
|
5
|
-
*
|
|
6
|
-
* アプリ固有の結合 (env・backend HTTP・DB 由来の写真解決・DB ウォームアップ・テストバックドア) は
|
|
7
|
-
* すべてここで定義するポートとして注入する。パッケージ内には汎用ロジックだけを置く。
|
|
8
|
-
*/
|
|
9
|
-
import type { AuthMessages, GoogleProfileBase, SessionProfile } from "@aiquants/auth-core"
|
|
10
|
-
|
|
11
|
-
/** OAuth Google の資格情報とオプション (scopes はアプリ注入 — sanledge が拡張)。 */
|
|
12
|
-
export type GoogleOAuthConfig = {
|
|
13
|
-
clientId: string
|
|
14
|
-
clientSecret: string
|
|
15
|
-
redirectURI: string
|
|
16
|
-
/** 追加スコープ (既定は openid profile email)。 */
|
|
17
|
-
scopes?: string[]
|
|
18
|
-
accessType?: "online" | "offline"
|
|
19
|
-
includeGrantedScopes?: boolean
|
|
20
|
-
prompt?: "none" | "consent" | "select_account"
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
/** セッション cookie 設定 (既定は __session/lax/30 日)。secrets のみ必須。 */
|
|
24
|
-
export type SessionConfig = {
|
|
25
|
-
secrets: string[]
|
|
26
|
-
/** cookie 名。既定 "__session" (変更すると全ライブセッションが無効化されるため既定を推奨)。 */
|
|
27
|
-
cookieName?: string
|
|
28
|
-
/** 有効期間 (秒)。既定 60*60*24*30。 */
|
|
29
|
-
maxAge?: number
|
|
30
|
-
/** Secure 属性。既定は NODE_ENV === "production"。 */
|
|
31
|
-
secure?: boolean
|
|
32
|
-
/** セッション内のユーザーキー。既定 "user"。 */
|
|
33
|
-
sessionKey?: string
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/** バックエンド (FastAPI) 認証ポート。verify → allowlist → signup の順で呼ばれる。 */
|
|
37
|
-
export type BackendAuthPort = {
|
|
38
|
-
/** GET /openid/verify 相当。ok なら true。 */
|
|
39
|
-
verify: (provider: string, accessToken: string) => Promise<boolean>
|
|
40
|
-
/**
|
|
41
|
-
* POST /openid/signup?overwrite_existing=true 相当 (毎ログイン実行)。
|
|
42
|
-
*
|
|
43
|
-
* `profile` を渡すのは、サインアップ直後にホスト側が identity を必要とするためである。
|
|
44
|
-
* この呼び出しでユーザー行が作られるので、直後に「この人物は何に属するか」を解決したい
|
|
45
|
-
* ホストは、ここでプロフィールを受け取れないとトークンから引き直すしかない。
|
|
46
|
-
*/
|
|
47
|
-
signup: (provider: string, accessToken: string, profile: GoogleProfileBase) => Promise<unknown>
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/** ユーザー写真解決ポート (DB 参照等の実装はアプリ側)。キャッシュ + dedup はパッケージ内。 */
|
|
51
|
-
export type UserPhotoPort = (openid: string) => Promise<string>
|
|
52
|
-
|
|
53
|
-
/** 毎リクエストの DB 接続ウォームアップ (quants は 2 プール、sanledge は 1 プール)。 */
|
|
54
|
-
export type WarmupPort = () => Promise<void>
|
|
55
|
-
|
|
56
|
-
/** テストバックドア (quants は mock_user_id パース + 名前マップ、sanledge は固定 id)。 */
|
|
57
|
-
export type MockUserPort = {
|
|
58
|
-
/** このリクエストでバックドアを使うか (NODE_ENV/cookie の判定はアプリ側)。 */
|
|
59
|
-
enabled: (request: Request) => boolean
|
|
60
|
-
/** バックドア時に返すセッションプロファイル。 */
|
|
61
|
-
resolve: (request: Request) => SessionProfile
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/** ロガーポート (既定は console)。 */
|
|
65
|
-
export type LoggerPort = {
|
|
66
|
-
info: (...args: unknown[]) => void
|
|
67
|
-
warn: (...args: unknown[]) => void
|
|
68
|
-
error: (...args: unknown[]) => void
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/** 認証サーバーの集約設定。 */
|
|
72
|
-
export type AuthServerConfig = {
|
|
73
|
-
google: GoogleOAuthConfig
|
|
74
|
-
session: SessionConfig
|
|
75
|
-
/**
|
|
76
|
-
* allowlist 判定 (auth-core の `emailDomainAllowlist` を推奨)。
|
|
77
|
-
*
|
|
78
|
-
* 非同期を許すのは、環境変数に加えてデータベース等の可変な供給元を参照する構成のため。
|
|
79
|
-
* ⚠️ 呼び出し側は必ず `await` すること。`Promise` は常に truthy なので `if (!fn(email))`
|
|
80
|
-
* のまま非同期実装を渡すと **判定が素通りして fail-open** になる (型検査では検出できない)。
|
|
81
|
-
*/
|
|
82
|
-
isEmailAllowed: (email: string | null | undefined) => boolean | Promise<boolean>
|
|
83
|
-
/**
|
|
84
|
-
* Account-status port: return `false` ONLY for a known-deactivated account.
|
|
85
|
-
* アカウント状態ポート。**無効化済みと判明した場合のみ** `false` を返す。
|
|
86
|
-
*
|
|
87
|
-
* allowlist が「そのメールアドレスがこのアプリを使ってよいか」を判定するのに対し、こちらは
|
|
88
|
-
* 「そのアカウントが現在有効か」を判定する。ログイン時と **毎リクエストのセッション解決時** の
|
|
89
|
-
* 双方で参照されるため、無効化は既存セッションに対しても即座に効く。
|
|
90
|
-
*
|
|
91
|
-
* 未登録ユーザー (初回ログインでこれから作成される) では `true` を返すこと。`false` を返すと
|
|
92
|
-
* サインアップ経路ごと塞いでしまう。省略時は常に有効として扱う (既存構成の挙動を変えない)。
|
|
93
|
-
*/
|
|
94
|
-
isUserActive?: (profile: GoogleProfileBase) => boolean | Promise<boolean>
|
|
95
|
-
/** バックエンド verify/signup。省略時はスキップ (フロント単独運用)。 */
|
|
96
|
-
backendAuth?: BackendAuthPort
|
|
97
|
-
/** ユーザー写真解決。省略時は常に空文字。 */
|
|
98
|
-
getUserPhoto?: UserPhotoPort
|
|
99
|
-
/** 毎リクエストのウォームアップ。省略時は何もしない。 */
|
|
100
|
-
warmup?: WarmupPort
|
|
101
|
-
/** テストバックドア。省略時は無効 (本番安全側)。 */
|
|
102
|
-
mockUser?: MockUserPort
|
|
103
|
-
/** ログインページのパス。既定 "/auth/login"。 */
|
|
104
|
-
loginPath?: string
|
|
105
|
-
/** ログイン成功後のリダイレクト先。既定 "/"。 */
|
|
106
|
-
postLoginRedirect?: string
|
|
107
|
-
/** ログアウト後のリダイレクト先。既定 "/"。 */
|
|
108
|
-
postLogoutRedirect?: string
|
|
109
|
-
/** エラーメッセージ上書き (既定は日本語の原本文言)。 */
|
|
110
|
-
messages?: Partial<AuthMessages>
|
|
111
|
-
/** ロガー (既定 console)。 */
|
|
112
|
-
logger?: LoggerPort
|
|
113
|
-
}
|