@aiquants/auth-react-router 0.1.1
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/LICENSE +21 -0
- package/README.md +62 -0
- package/dist/client.d.mts +16 -0
- package/dist/client.d.ts +16 -0
- package/dist/client.js +2 -0
- package/dist/client.js.map +1 -0
- package/dist/client.mjs +2 -0
- package/dist/client.mjs.map +1 -0
- package/dist/server.d.mts +299 -0
- package/dist/server.d.ts +299 -0
- package/dist/server.js +2 -0
- package/dist/server.js.map +1 -0
- package/dist/server.mjs +2 -0
- package/dist/server.mjs.map +1 -0
- package/package.json +88 -0
- package/src/client/GoogleForm.tsx +44 -0
- package/src/client.ts +6 -0
- package/src/server/auth-server.ts +68 -0
- package/src/server/authenticate-in-loader.spec.ts +152 -0
- package/src/server/authenticate-in-loader.ts +148 -0
- package/src/server/authenticate.spec.ts +146 -0
- package/src/server/authenticate.ts +187 -0
- package/src/server/google-strategy-impl.spec.ts +108 -0
- package/src/server/google-strategy-impl.ts +148 -0
- package/src/server/google-strategy.spec.ts +215 -0
- package/src/server/google-strategy.ts +174 -0
- package/src/server/handlers.spec.ts +154 -0
- package/src/server/handlers.ts +134 -0
- package/src/server/logger.ts +14 -0
- package/src/server/ports.ts +89 -0
- package/src/server/session.spec.ts +113 -0
- package/src/server/session.ts +127 -0
- package/src/server.ts +14 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 AI Quants / fehde-k
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# @aiquants/auth-react-router
|
|
2
|
+
|
|
3
|
+
React Router v7 auth adapter for [`@aiquants/auth-core`](../auth-core/): Google OAuth (remix-auth v4 + a vendored `GoogleStrategy` over `remix-auth-oauth2`) authenticator factory, cookie session storage, per-request loader guard with token refresh + photo cache, auth route handlers, and a `<GoogleForm>` login button. All app couplings (env, backend HTTP, DB photo lookup, warmup, test backdoor) are **DI ports**. The Google strategy (formerly `@coji/remix-auth-google`, 68 lines) is vendored in `src/server/google-strategy-impl.ts`, so the package has no `@coji/*` dependency.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pnpm add @aiquants/auth-core @aiquants/auth-react-router
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Peers: `react`, `react-router@^7`, `remix-auth@^4`, `remix-auth-oauth2@^3`, `react-icons`.
|
|
12
|
+
|
|
13
|
+
## Wiring (single composition point — `app/services/auth/config.server.ts`)
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
import { emailDomainAllowlist, parseAllowlistCsv } from "@aiquants/auth-core"
|
|
17
|
+
import { createAuthServer } from "@aiquants/auth-react-router/server"
|
|
18
|
+
|
|
19
|
+
export const authServer = createAuthServer({
|
|
20
|
+
google: { clientId: env.GOOGLE_CLIENT_ID, clientSecret: env.GOOGLE_CLIENT_SECRET, redirectURI: env.GOOGLE_OAUTH_REDIRECT_URL },
|
|
21
|
+
session: { secrets: [env.REMIX_SESSION_SECRET] },
|
|
22
|
+
isEmailAllowed: emailDomainAllowlist(parseAllowlistCsv(env.ALLOWED_EMAILS), parseAllowlistCsv(env.ALLOWED_DOMAINS)),
|
|
23
|
+
backendAuth: { verify: verifyBackend, signup: (p, t) => signupBackend(p, t) }, // FastAPI verify/signup
|
|
24
|
+
getUserPhoto: async (openid) => (await getOpenids({ openid }))[0]?.picture ?? "", // drizzle TDOpenid
|
|
25
|
+
warmup: async () => { await Promise.all([ensureQuantsConnection(), ensureSanwaConnection()]) },
|
|
26
|
+
mockUser: quantsMockUserPort, // test backdoor (E2E) — omit in production-only apps
|
|
27
|
+
})
|
|
28
|
+
export const { authenticator, sessionStorage, getSession, getSessionUser, saveSession, requireUser, requireAdminUser, commitSession, destroySession, authenticate, refreshedAccessToken, authenticateInLoader, loginLoader, loginAction, logoutLoader, logout, logoutAndRedirect, googleCallbackLoader } = authServer
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Client:
|
|
32
|
+
|
|
33
|
+
```tsx
|
|
34
|
+
import { GoogleForm } from "@aiquants/auth-react-router/client"
|
|
35
|
+
<GoogleForm /> // <Form method="POST"> + _action="Sign In with Google"
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Tailwind: add `@source "../../node_modules/@aiquants/auth-react-router/src";` (or the workspace path) so the `GoogleForm` classes are scanned — `src` ships in the published package.
|
|
39
|
+
|
|
40
|
+
## Behavior contract (preserved from quants-react-router)
|
|
41
|
+
|
|
42
|
+
- **Session cookie byte compatibility**: `__session` (httpOnly/lax/30d), key `"user"`, value = `{ id, displayName, name, emails, accessToken, refreshToken?, expirationDateMs?, provider, role? }` — never `photo`/`_json`/`photos`. Changing this invalidates all live 30-day sessions.
|
|
43
|
+
- **Verify order**: Google profile check → `backendAuth.verify` → allowlist → `backendAuth.signup` (every login). Failures redirect to `<loginPath>?error=<encoded Japanese message>` (messages overridable via `messages`).
|
|
44
|
+
- **Token refresh**: expiry check → Google `oauth2/v4/token` rotate → session update; `invalid_grant` destroys the session and redirects to login with `Set-Cookie`.
|
|
45
|
+
- **Loader guard**: `authenticateInLoader(request, { failureRedirect })` returns `{ user?, cookie? }`; `failureRedirect: null` = no-redirect mode. Photo lookups are cached with in-flight dedup per factory instance.
|
|
46
|
+
|
|
47
|
+
## DI ports absorbing per-app differences (quants vs sanledge)
|
|
48
|
+
|
|
49
|
+
| Port | quants | sanledge |
|
|
50
|
+
| --- | --- | --- |
|
|
51
|
+
| `google.scopes` | default | + `directory.readonly` etc. |
|
|
52
|
+
| `mockUser` | `mock_user_id` cookie parse + name map | fixed id |
|
|
53
|
+
| `warmup` | 2 pools | 1 pool |
|
|
54
|
+
| `backendAuth` | quants-api | sanledge-api |
|
|
55
|
+
|
|
56
|
+
## Intentional changes vs the original (§8, approved)
|
|
57
|
+
|
|
58
|
+
1. No import-time env reads/throws — all validation happens in `createAuthServer`.
|
|
59
|
+
2. Allowlist matching is trim+lowercase (unified with the Python side via `@aiquants/auth-core`).
|
|
60
|
+
3. Dead exports (`refreshBackend`, `authenticateBackend`) are not ported — the backend port only needs `verify`/`signup`.
|
|
61
|
+
|
|
62
|
+
MIT
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
|
|
3
|
+
/** GoogleForm のプロパティ。 */
|
|
4
|
+
type GoogleFormProps = {
|
|
5
|
+
/** フォーム action の値 (loginAction の switch と一致させる)。既定 "Sign In with Google"。 */
|
|
6
|
+
actionValue?: string;
|
|
7
|
+
/** ボタンの表示ラベル。既定 "Sign in with Google"。 */
|
|
8
|
+
label?: string;
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* Renders the Google sign-in form (POST with `_action`).
|
|
12
|
+
* Google サインインフォーム (`_action` 付き POST) を描画する。
|
|
13
|
+
*/
|
|
14
|
+
declare const GoogleForm: ({ actionValue, label }?: GoogleFormProps) => react.JSX.Element;
|
|
15
|
+
|
|
16
|
+
export { GoogleForm, type GoogleFormProps };
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
|
|
3
|
+
/** GoogleForm のプロパティ。 */
|
|
4
|
+
type GoogleFormProps = {
|
|
5
|
+
/** フォーム action の値 (loginAction の switch と一致させる)。既定 "Sign In with Google"。 */
|
|
6
|
+
actionValue?: string;
|
|
7
|
+
/** ボタンの表示ラベル。既定 "Sign in with Google"。 */
|
|
8
|
+
label?: string;
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* Renders the Google sign-in form (POST with `_action`).
|
|
12
|
+
* Google サインインフォーム (`_action` 付き POST) を描画する。
|
|
13
|
+
*/
|
|
14
|
+
declare const GoogleForm: ({ actionValue, label }?: GoogleFormProps) => react.JSX.Element;
|
|
15
|
+
|
|
16
|
+
export { GoogleForm, type GoogleFormProps };
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
"use strict";var i=Object.defineProperty;var p=Object.getOwnPropertyDescriptor;var u=Object.getOwnPropertyNames;var b=Object.prototype.hasOwnProperty;var f=(e,o)=>{for(var t in o)i(e,t,{get:o[t],enumerable:!0})},d=(e,o,t,l)=>{if(o&&typeof o=="object"||typeof o=="function")for(let r of u(o))!b.call(e,r)&&r!==t&&i(e,r,{get:()=>o[r],enumerable:!(l=p(o,r))||l.enumerable});return e};var F=e=>d(i({},"__esModule",{value:!0}),e);var h={};f(h,{GoogleForm:()=>a});module.exports=F(h);var s=require("react-icons/fa"),m=require("react-router"),g=require("tailwind-variants"),n=require("react/jsx-runtime"),x=(0,g.tv)({slots:{googleForm:"my-4",btn:"w-full flex items-center justify-center gap-3 rounded-2xl bg-gradient-to-r from-blue-500 to-indigo-500 px-6 py-3 text-white font-bold shadow-lg transition-all duration-300 hover:from-blue-600 hover:to-indigo-600 hover:scale-105 focus:outline-none focus:ring-2 focus:ring-blue-300",icon:"",btnText:"text-base tracking-wide"}}),a=({actionValue:e="Sign In with Google",label:o="Sign in with Google"}={})=>{let{googleForm:t,btn:l,icon:r,btnText:c}=x();return(0,n.jsx)(m.Form,{method:"POST",className:t(),children:(0,n.jsxs)("button",{type:"submit",name:"_action",value:e,className:l(),children:[(0,n.jsx)(s.FaGoogle,{size:24,className:"text-white drop-shadow"}),(0,n.jsx)("span",{className:c(),children:o})]})})};0&&(module.exports={GoogleForm});
|
|
2
|
+
//# sourceMappingURL=client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/client.ts","../src/client/GoogleForm.tsx"],"sourcesContent":["/**\n * @aiquants/auth-react-router/client — client-side auth components barrel.\n * クライアントサイド認証コンポーネントのバレル。\n */\nexport type { GoogleFormProps } from \"./client/GoogleForm\"\nexport { GoogleForm } from \"./client/GoogleForm\"\n","/**\n * @module GoogleForm\n * @description Google sign-in form button (tailwind-variants styled).\n * Google サインインフォームボタン (tailwind-variants スタイル)。\n *\n * 原本 (quants-react-router components/elements/GoogleForm.tsx) を挙動保存で移植。\n * ラベル・action 値は props 化し、既定値は原本と同一 (\"Sign In with Google\")。\n */\nimport { FaGoogle } from \"react-icons/fa\"\nimport { Form } from \"react-router\"\nimport { tv } from \"tailwind-variants\"\n\nconst googleFormStyles = tv({\n slots: {\n googleForm: \"my-4\",\n btn: \"w-full flex items-center justify-center gap-3 rounded-2xl bg-gradient-to-r from-blue-500 to-indigo-500 px-6 py-3 text-white font-bold shadow-lg transition-all duration-300 hover:from-blue-600 hover:to-indigo-600 hover:scale-105 focus:outline-none focus:ring-2 focus:ring-blue-300\",\n icon: \"\",\n btnText: \"text-base tracking-wide\",\n },\n})\n\n/** GoogleForm のプロパティ。 */\nexport type GoogleFormProps = {\n /** フォーム action の値 (loginAction の switch と一致させる)。既定 \"Sign In with Google\"。 */\n actionValue?: string\n /** ボタンの表示ラベル。既定 \"Sign in with Google\"。 */\n label?: string\n}\n\n/**\n * Renders the Google sign-in form (POST with `_action`).\n * Google サインインフォーム (`_action` 付き POST) を描画する。\n */\nexport const GoogleForm = ({ actionValue = \"Sign In with Google\", label = \"Sign in with Google\" }: GoogleFormProps = {}) => {\n const { googleForm, btn, icon: _icon, btnText } = googleFormStyles()\n return (\n <Form method=\"POST\" className={googleForm()}>\n <button type=\"submit\" name=\"_action\" value={actionValue} className={btn()}>\n <FaGoogle size={24} className=\"text-white drop-shadow\" />\n <span className={btnText()}>{label}</span>\n </button>\n </Form>\n )\n}\n"],"mappings":"yaAAA,IAAAA,EAAA,GAAAC,EAAAD,EAAA,gBAAAE,IAAA,eAAAC,EAAAH,GCQA,IAAAI,EAAyB,0BACzBC,EAAqB,wBACrBC,EAAmB,6BA2BPC,EAAA,6BAzBNC,KAAmB,MAAG,CACxB,MAAO,CACH,WAAY,OACZ,IAAK,0RACL,KAAM,GACN,QAAS,yBACb,CACJ,CAAC,EAcYC,EAAa,CAAC,CAAE,YAAAC,EAAc,sBAAuB,MAAAC,EAAQ,qBAAsB,EAAqB,CAAC,IAAM,CACxH,GAAM,CAAE,WAAAC,EAAY,IAAAC,EAAK,KAAMC,EAAO,QAAAC,CAAQ,EAAIP,EAAiB,EACnE,SACI,OAAC,QAAK,OAAO,OAAO,UAAWI,EAAW,EACtC,oBAAC,UAAO,KAAK,SAAS,KAAK,UAAU,MAAOF,EAAa,UAAWG,EAAI,EACpE,oBAAC,YAAS,KAAM,GAAI,UAAU,yBAAyB,KACvD,OAAC,QAAK,UAAWE,EAAQ,EAAI,SAAAJ,EAAM,GACvC,EACJ,CAER","names":["client_exports","__export","GoogleForm","__toCommonJS","import_fa","import_react_router","import_tailwind_variants","import_jsx_runtime","googleFormStyles","GoogleForm","actionValue","label","googleForm","btn","_icon","btnText"]}
|
package/dist/client.mjs
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{FaGoogle as i}from"react-icons/fa";import{Form as s}from"react-router";import{tv as m}from"tailwind-variants";import{jsx as o,jsxs as c}from"react/jsx-runtime";var g=m({slots:{googleForm:"my-4",btn:"w-full flex items-center justify-center gap-3 rounded-2xl bg-gradient-to-r from-blue-500 to-indigo-500 px-6 py-3 text-white font-bold shadow-lg transition-all duration-300 hover:from-blue-600 hover:to-indigo-600 hover:scale-105 focus:outline-none focus:ring-2 focus:ring-blue-300",icon:"",btnText:"text-base tracking-wide"}}),a=({actionValue:e="Sign In with Google",label:t="Sign in with Google"}={})=>{let{googleForm:r,btn:n,icon:p,btnText:l}=g();return o(s,{method:"POST",className:r(),children:c("button",{type:"submit",name:"_action",value:e,className:n(),children:[o(i,{size:24,className:"text-white drop-shadow"}),o("span",{className:l(),children:t})]})})};export{a as GoogleForm};
|
|
2
|
+
//# sourceMappingURL=client.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/client/GoogleForm.tsx"],"sourcesContent":["/**\n * @module GoogleForm\n * @description Google sign-in form button (tailwind-variants styled).\n * Google サインインフォームボタン (tailwind-variants スタイル)。\n *\n * 原本 (quants-react-router components/elements/GoogleForm.tsx) を挙動保存で移植。\n * ラベル・action 値は props 化し、既定値は原本と同一 (\"Sign In with Google\")。\n */\nimport { FaGoogle } from \"react-icons/fa\"\nimport { Form } from \"react-router\"\nimport { tv } from \"tailwind-variants\"\n\nconst googleFormStyles = tv({\n slots: {\n googleForm: \"my-4\",\n btn: \"w-full flex items-center justify-center gap-3 rounded-2xl bg-gradient-to-r from-blue-500 to-indigo-500 px-6 py-3 text-white font-bold shadow-lg transition-all duration-300 hover:from-blue-600 hover:to-indigo-600 hover:scale-105 focus:outline-none focus:ring-2 focus:ring-blue-300\",\n icon: \"\",\n btnText: \"text-base tracking-wide\",\n },\n})\n\n/** GoogleForm のプロパティ。 */\nexport type GoogleFormProps = {\n /** フォーム action の値 (loginAction の switch と一致させる)。既定 \"Sign In with Google\"。 */\n actionValue?: string\n /** ボタンの表示ラベル。既定 \"Sign in with Google\"。 */\n label?: string\n}\n\n/**\n * Renders the Google sign-in form (POST with `_action`).\n * Google サインインフォーム (`_action` 付き POST) を描画する。\n */\nexport const GoogleForm = ({ actionValue = \"Sign In with Google\", label = \"Sign in with Google\" }: GoogleFormProps = {}) => {\n const { googleForm, btn, icon: _icon, btnText } = googleFormStyles()\n return (\n <Form method=\"POST\" className={googleForm()}>\n <button type=\"submit\" name=\"_action\" value={actionValue} className={btn()}>\n <FaGoogle size={24} className=\"text-white drop-shadow\" />\n <span className={btnText()}>{label}</span>\n </button>\n </Form>\n )\n}\n"],"mappings":"AAQA,OAAS,YAAAA,MAAgB,iBACzB,OAAS,QAAAC,MAAY,eACrB,OAAS,MAAAC,MAAU,oBA2BP,OACI,OAAAC,EADJ,QAAAC,MAAA,oBAzBZ,IAAMC,EAAmBH,EAAG,CACxB,MAAO,CACH,WAAY,OACZ,IAAK,0RACL,KAAM,GACN,QAAS,yBACb,CACJ,CAAC,EAcYI,EAAa,CAAC,CAAE,YAAAC,EAAc,sBAAuB,MAAAC,EAAQ,qBAAsB,EAAqB,CAAC,IAAM,CACxH,GAAM,CAAE,WAAAC,EAAY,IAAAC,EAAK,KAAMC,EAAO,QAAAC,CAAQ,EAAIP,EAAiB,EACnE,OACIF,EAACF,EAAA,CAAK,OAAO,OAAO,UAAWQ,EAAW,EACtC,SAAAL,EAAC,UAAO,KAAK,SAAS,KAAK,UAAU,MAAOG,EAAa,UAAWG,EAAI,EACpE,UAAAP,EAACH,EAAA,CAAS,KAAM,GAAI,UAAU,yBAAyB,EACvDG,EAAC,QAAK,UAAWS,EAAQ,EAAI,SAAAJ,EAAM,GACvC,EACJ,CAER","names":["FaGoogle","Form","tv","jsx","jsxs","googleFormStyles","GoogleForm","actionValue","label","googleForm","btn","_icon","btnText"]}
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
import * as react_router from 'react-router';
|
|
2
|
+
import { Session } from 'react-router';
|
|
3
|
+
import * as remix_auth from 'remix-auth';
|
|
4
|
+
import { Authenticator } from 'remix-auth';
|
|
5
|
+
import * as _aiquants_auth_core from '@aiquants/auth-core';
|
|
6
|
+
import { SessionProfile, AuthMessages, MyGoogleProfile } from '@aiquants/auth-core';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @module ports
|
|
10
|
+
* @description DI port types for the React Router auth adapter.
|
|
11
|
+
* React Router 認証アダプタの DI ポート型定義。
|
|
12
|
+
*
|
|
13
|
+
* アプリ固有の結合 (env・backend HTTP・DB 由来の写真解決・DB ウォームアップ・テストバックドア) は
|
|
14
|
+
* すべてここで定義するポートとして注入する。パッケージ内には汎用ロジックだけを置く。
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/** OAuth Google の資格情報とオプション (scopes はアプリ注入 — sanledge が拡張)。 */
|
|
18
|
+
type GoogleOAuthConfig = {
|
|
19
|
+
clientId: string;
|
|
20
|
+
clientSecret: string;
|
|
21
|
+
redirectURI: string;
|
|
22
|
+
/** 追加スコープ (既定は openid profile email)。 */
|
|
23
|
+
scopes?: string[];
|
|
24
|
+
accessType?: "online" | "offline";
|
|
25
|
+
includeGrantedScopes?: boolean;
|
|
26
|
+
prompt?: "none" | "consent" | "select_account";
|
|
27
|
+
};
|
|
28
|
+
/** セッション cookie 設定 (既定は __session/lax/30 日)。secrets のみ必須。 */
|
|
29
|
+
type SessionConfig = {
|
|
30
|
+
secrets: string[];
|
|
31
|
+
/** cookie 名。既定 "__session" (変更すると全ライブセッションが無効化されるため既定を推奨)。 */
|
|
32
|
+
cookieName?: string;
|
|
33
|
+
/** 有効期間 (秒)。既定 60*60*24*30。 */
|
|
34
|
+
maxAge?: number;
|
|
35
|
+
/** Secure 属性。既定は NODE_ENV === "production"。 */
|
|
36
|
+
secure?: boolean;
|
|
37
|
+
/** セッション内のユーザーキー。既定 "user"。 */
|
|
38
|
+
sessionKey?: string;
|
|
39
|
+
};
|
|
40
|
+
/** バックエンド (FastAPI) 認証ポート。verify → allowlist → signup の順で呼ばれる。 */
|
|
41
|
+
type BackendAuthPort = {
|
|
42
|
+
/** GET /openid/verify 相当。ok なら true。 */
|
|
43
|
+
verify: (provider: string, accessToken: string) => Promise<boolean>;
|
|
44
|
+
/** POST /openid/signup?overwrite_existing=true 相当 (毎ログイン実行)。 */
|
|
45
|
+
signup: (provider: string, accessToken: string) => Promise<unknown>;
|
|
46
|
+
};
|
|
47
|
+
/** ユーザー写真解決ポート (drizzle TDOpenid 等はアプリ側)。キャッシュ + dedup はパッケージ内。 */
|
|
48
|
+
type UserPhotoPort = (openid: string) => Promise<string>;
|
|
49
|
+
/** 毎リクエストの DB 接続ウォームアップ (quants は 2 プール、sanledge は 1 プール)。 */
|
|
50
|
+
type WarmupPort = () => Promise<void>;
|
|
51
|
+
/** テストバックドア (quants は mock_user_id パース + 名前マップ、sanledge は固定 id)。 */
|
|
52
|
+
type MockUserPort = {
|
|
53
|
+
/** このリクエストでバックドアを使うか (NODE_ENV/cookie の判定はアプリ側)。 */
|
|
54
|
+
enabled: (request: Request) => boolean;
|
|
55
|
+
/** バックドア時に返すセッションプロファイル。 */
|
|
56
|
+
resolve: (request: Request) => SessionProfile;
|
|
57
|
+
};
|
|
58
|
+
/** ロガーポート (既定は console)。 */
|
|
59
|
+
type LoggerPort = {
|
|
60
|
+
info: (...args: unknown[]) => void;
|
|
61
|
+
warn: (...args: unknown[]) => void;
|
|
62
|
+
error: (...args: unknown[]) => void;
|
|
63
|
+
};
|
|
64
|
+
/** 認証サーバーの集約設定。 */
|
|
65
|
+
type AuthServerConfig = {
|
|
66
|
+
google: GoogleOAuthConfig;
|
|
67
|
+
session: SessionConfig;
|
|
68
|
+
/** allowlist 判定 (auth-core の emailDomainAllowlist を推奨)。 */
|
|
69
|
+
isEmailAllowed: (email: string | null | undefined) => boolean;
|
|
70
|
+
/** バックエンド verify/signup。省略時はスキップ (フロント単独運用)。 */
|
|
71
|
+
backendAuth?: BackendAuthPort;
|
|
72
|
+
/** ユーザー写真解決。省略時は常に空文字。 */
|
|
73
|
+
getUserPhoto?: UserPhotoPort;
|
|
74
|
+
/** 毎リクエストのウォームアップ。省略時は何もしない。 */
|
|
75
|
+
warmup?: WarmupPort;
|
|
76
|
+
/** テストバックドア。省略時は無効 (本番安全側)。 */
|
|
77
|
+
mockUser?: MockUserPort;
|
|
78
|
+
/** ログインページのパス。既定 "/auth/login"。 */
|
|
79
|
+
loginPath?: string;
|
|
80
|
+
/** ログイン成功後のリダイレクト先。既定 "/"。 */
|
|
81
|
+
postLoginRedirect?: string;
|
|
82
|
+
/** ログアウト後のリダイレクト先。既定 "/"。 */
|
|
83
|
+
postLogoutRedirect?: string;
|
|
84
|
+
/** エラーメッセージ上書き (既定は日本語の原本文言)。 */
|
|
85
|
+
messages?: Partial<AuthMessages>;
|
|
86
|
+
/** ロガー (既定 console)。 */
|
|
87
|
+
logger?: LoggerPort;
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
/** createAuthServer が返す認証サーバー束。 */
|
|
91
|
+
type AuthServer = ReturnType<typeof createAuthServer>;
|
|
92
|
+
/**
|
|
93
|
+
* Builds the full auth server bundle from a single config.
|
|
94
|
+
* 単一の設定から認証サーバー束一式を生成する。
|
|
95
|
+
*/
|
|
96
|
+
declare const createAuthServer: (config: AuthServerConfig) => {
|
|
97
|
+
loginLoader: ({ request }: {
|
|
98
|
+
request: Request;
|
|
99
|
+
}) => Promise<{
|
|
100
|
+
error: string | null;
|
|
101
|
+
}>;
|
|
102
|
+
loginAction: ({ request }: {
|
|
103
|
+
request: Request;
|
|
104
|
+
}) => Promise<_aiquants_auth_core.MyGoogleProfile | null>;
|
|
105
|
+
logoutLoader: ({ request }: {
|
|
106
|
+
request: Request;
|
|
107
|
+
}) => Promise<never>;
|
|
108
|
+
logout: (request: Request) => Promise<string>;
|
|
109
|
+
logoutAndRedirect: (request: Request) => Promise<Response>;
|
|
110
|
+
googleCallbackLoader: ({ request }: {
|
|
111
|
+
request: Request;
|
|
112
|
+
}) => Promise<never>;
|
|
113
|
+
authenticator: remix_auth.Authenticator<_aiquants_auth_core.MyGoogleProfile>;
|
|
114
|
+
storage: {
|
|
115
|
+
sessionStorage: react_router.SessionStorage<react_router.SessionData, react_router.SessionData>;
|
|
116
|
+
sessionKey: string;
|
|
117
|
+
getSession: (request: Request) => Promise<react_router.Session<react_router.SessionData, react_router.SessionData>>;
|
|
118
|
+
getSessionUser: (request: Request) => Promise<_aiquants_auth_core.SessionProfile | undefined>;
|
|
119
|
+
saveSession: (request: Request, user: _aiquants_auth_core.SessionProfile) => Promise<string>;
|
|
120
|
+
requireUser: (request: Request) => Promise<_aiquants_auth_core.SessionProfile>;
|
|
121
|
+
requireAdminUser: (request: Request) => Promise<_aiquants_auth_core.SessionProfile>;
|
|
122
|
+
logout: (request: Request) => Promise<string>;
|
|
123
|
+
commitSession: (session: react_router.Session<react_router.SessionData, react_router.SessionData>, options?: react_router.CookieSerializeOptions) => Promise<string>;
|
|
124
|
+
destroySession: (session: react_router.Session<react_router.SessionData, react_router.SessionData>, options?: react_router.CookieSerializeOptions) => Promise<string>;
|
|
125
|
+
};
|
|
126
|
+
authFns: {
|
|
127
|
+
authenticate: (request: Request, session: react_router.Session, options?: {
|
|
128
|
+
failureRedirect?: string;
|
|
129
|
+
}) => Promise<{
|
|
130
|
+
accessToken: any;
|
|
131
|
+
refreshToken: any;
|
|
132
|
+
expirationDateMs: any;
|
|
133
|
+
}>;
|
|
134
|
+
refreshedAccessToken: (session: react_router.Session) => Promise<{
|
|
135
|
+
accessToken: any;
|
|
136
|
+
refreshToken: any;
|
|
137
|
+
expirationDateMs: number;
|
|
138
|
+
}>;
|
|
139
|
+
};
|
|
140
|
+
sessionStorage: react_router.SessionStorage<react_router.SessionData, react_router.SessionData>;
|
|
141
|
+
getSession: (request: Request) => Promise<react_router.Session<react_router.SessionData, react_router.SessionData>>;
|
|
142
|
+
getSessionUser: (request: Request) => Promise<_aiquants_auth_core.SessionProfile | undefined>;
|
|
143
|
+
saveSession: (request: Request, user: _aiquants_auth_core.SessionProfile) => Promise<string>;
|
|
144
|
+
requireUser: (request: Request) => Promise<_aiquants_auth_core.SessionProfile>;
|
|
145
|
+
requireAdminUser: (request: Request) => Promise<_aiquants_auth_core.SessionProfile>;
|
|
146
|
+
commitSession: (session: react_router.Session<react_router.SessionData, react_router.SessionData>, options?: react_router.CookieSerializeOptions) => Promise<string>;
|
|
147
|
+
destroySession: (session: react_router.Session<react_router.SessionData, react_router.SessionData>, options?: react_router.CookieSerializeOptions) => Promise<string>;
|
|
148
|
+
authenticate: (request: Request, session: react_router.Session, options?: {
|
|
149
|
+
failureRedirect?: string;
|
|
150
|
+
}) => Promise<{
|
|
151
|
+
accessToken: any;
|
|
152
|
+
refreshToken: any;
|
|
153
|
+
expirationDateMs: any;
|
|
154
|
+
}>;
|
|
155
|
+
refreshedAccessToken: (session: react_router.Session) => Promise<{
|
|
156
|
+
accessToken: any;
|
|
157
|
+
refreshToken: any;
|
|
158
|
+
expirationDateMs: number;
|
|
159
|
+
}>;
|
|
160
|
+
authenticateInLoader: (request: Request, options: {
|
|
161
|
+
failureRedirect: string;
|
|
162
|
+
} | {
|
|
163
|
+
failureRedirect: null;
|
|
164
|
+
}) => Promise<{
|
|
165
|
+
user?: _aiquants_auth_core.MyGoogleProfile;
|
|
166
|
+
cookie?: string;
|
|
167
|
+
}>;
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
/** createAuthSessionStorage が返すセッション操作束。 */
|
|
171
|
+
type AuthSessionStorage = ReturnType<typeof createAuthSessionStorage>;
|
|
172
|
+
/**
|
|
173
|
+
* Builds the cookie session storage and session helpers.
|
|
174
|
+
* cookie セッションストレージとセッションヘルパー群を生成する。
|
|
175
|
+
*
|
|
176
|
+
* @throws secrets が空の場合 (fail-closed の設定ガード)。
|
|
177
|
+
*/
|
|
178
|
+
declare const createAuthSessionStorage: (config: SessionConfig, options?: {
|
|
179
|
+
mockUser?: MockUserPort;
|
|
180
|
+
loginPath?: string;
|
|
181
|
+
postLoginRedirect?: string;
|
|
182
|
+
}) => {
|
|
183
|
+
sessionStorage: react_router.SessionStorage<react_router.SessionData, react_router.SessionData>;
|
|
184
|
+
sessionKey: string;
|
|
185
|
+
getSession: (request: Request) => Promise<react_router.Session<react_router.SessionData, react_router.SessionData>>;
|
|
186
|
+
getSessionUser: (request: Request) => Promise<SessionProfile | undefined>;
|
|
187
|
+
saveSession: (request: Request, user: SessionProfile) => Promise<string>;
|
|
188
|
+
requireUser: (request: Request) => Promise<SessionProfile>;
|
|
189
|
+
requireAdminUser: (request: Request) => Promise<SessionProfile>;
|
|
190
|
+
logout: (request: Request) => Promise<string>;
|
|
191
|
+
commitSession: (session: react_router.Session<react_router.SessionData, react_router.SessionData>, options?: react_router.CookieSerializeOptions) => Promise<string>;
|
|
192
|
+
destroySession: (session: react_router.Session<react_router.SessionData, react_router.SessionData>, options?: react_router.CookieSerializeOptions) => Promise<string>;
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
/** トークンリフレッシュ失敗の基底エラー。 */
|
|
196
|
+
declare class TokenRefreshError extends Error {
|
|
197
|
+
constructor(message: string);
|
|
198
|
+
}
|
|
199
|
+
/** Google の invalid_grant (失効/取り消し済みリフレッシュトークン) エラー。 */
|
|
200
|
+
declare class GoogleInvalidGrantError extends TokenRefreshError {
|
|
201
|
+
constructor();
|
|
202
|
+
}
|
|
203
|
+
/** createAuthenticate が返す認証関数束。 */
|
|
204
|
+
type AuthenticateFns = ReturnType<typeof createAuthenticate>;
|
|
205
|
+
/**
|
|
206
|
+
* Builds the `authenticate` / `refreshedAccessToken` pair wired to injected config.
|
|
207
|
+
* 注入設定に結線した `authenticate` / `refreshedAccessToken` を生成する。
|
|
208
|
+
*/
|
|
209
|
+
declare const createAuthenticate: (config: AuthServerConfig, storage: AuthSessionStorage, logger: LoggerPort) => {
|
|
210
|
+
authenticate: (request: Request, session: Session, options?: {
|
|
211
|
+
failureRedirect?: string;
|
|
212
|
+
}) => Promise<{
|
|
213
|
+
accessToken: any;
|
|
214
|
+
refreshToken: any;
|
|
215
|
+
expirationDateMs: any;
|
|
216
|
+
}>;
|
|
217
|
+
refreshedAccessToken: (session: Session) => Promise<{
|
|
218
|
+
accessToken: any;
|
|
219
|
+
refreshToken: any;
|
|
220
|
+
expirationDateMs: number;
|
|
221
|
+
}>;
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* @module authenticate-in-loader
|
|
226
|
+
* @description Per-request loader authentication guard with photo cache and warmup.
|
|
227
|
+
* 写真キャッシュとウォームアップ付きの毎リクエストローダー認証ガード。
|
|
228
|
+
*
|
|
229
|
+
* 原本 (quants-react-router authenticateInLoader.server.ts) を挙動保存で移植。
|
|
230
|
+
* DB クエリ (getOpenids) は `getUserPhoto` ポートへ、DB プールウォームアップは `warmup`
|
|
231
|
+
* ポートへ置き換えた。写真キャッシュ (Map + pending dedup) はファクトリインスタンスごとに
|
|
232
|
+
* 保持する (module-level mutable state の排除)。
|
|
233
|
+
*/
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Builds the per-request loader guard wired to injected ports.
|
|
237
|
+
* 注入ポートに結線した毎リクエストローダーガードを生成する。
|
|
238
|
+
*/
|
|
239
|
+
declare const createAuthenticateInLoader: (config: AuthServerConfig, storage: AuthSessionStorage, authFns: AuthenticateFns, logger: LoggerPort) => (request: Request, options: {
|
|
240
|
+
failureRedirect: string;
|
|
241
|
+
} | {
|
|
242
|
+
failureRedirect: null;
|
|
243
|
+
}) => Promise<{
|
|
244
|
+
user?: MyGoogleProfile;
|
|
245
|
+
cookie?: string;
|
|
246
|
+
}>;
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* @module google-strategy
|
|
250
|
+
* @description Google OAuth authenticator factory (remix-auth v4 + local GoogleStrategy).
|
|
251
|
+
* Google OAuth 認証器ファクトリ (remix-auth v4 + 同梱の GoogleStrategy)。
|
|
252
|
+
*
|
|
253
|
+
* 原本 (quants-react-router auth.server.ts) の verify コールバックを挙動保存で移植。
|
|
254
|
+
* env 読取り・backend HTTP・allowlist・エラーメッセージはすべてポート/設定注入に置き換えた。
|
|
255
|
+
* 検証順序 (Google プロフィール検証 → backend verify → allowlist → backend signup)、
|
|
256
|
+
* リダイレクト先 (`<loginPath>?error=<encoded>`)、エラーメッセージ文言は原本と等価。
|
|
257
|
+
*/
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Builds a remix-auth Authenticator with the Google strategy wired to injected ports.
|
|
261
|
+
* 注入ポートに結線した Google ストラテジー付き Authenticator を生成する。
|
|
262
|
+
*/
|
|
263
|
+
declare const createGoogleAuthenticator: (config: AuthServerConfig) => Authenticator<MyGoogleProfile>;
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* @module handlers
|
|
267
|
+
* @description Route handler factories for login / logout / Google callback.
|
|
268
|
+
* login / logout / Google コールバックのルートハンドラ工場。
|
|
269
|
+
*
|
|
270
|
+
* 原本 (quants-react-router routes/_auth+ の 4 ルート + features/logoutAndRedirect.ts) の
|
|
271
|
+
* サーバー側ロジックを挙動保存で移植。UI (JSX) はアプリ側に残す。
|
|
272
|
+
* コールバックのセッション保存形状 (SessionProfile、photo/_json/photos を含まない) は
|
|
273
|
+
* cookie byte 互換のため厳密に保存する。
|
|
274
|
+
*/
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Builds loader/action handlers for the auth routes.
|
|
278
|
+
* 認証ルート用の loader/action ハンドラ群を生成する。
|
|
279
|
+
*/
|
|
280
|
+
declare const createAuthHandlers: (config: AuthServerConfig, storage: AuthSessionStorage, authenticator: Authenticator<MyGoogleProfile>, logger: LoggerPort) => {
|
|
281
|
+
loginLoader: ({ request }: {
|
|
282
|
+
request: Request;
|
|
283
|
+
}) => Promise<{
|
|
284
|
+
error: string | null;
|
|
285
|
+
}>;
|
|
286
|
+
loginAction: ({ request }: {
|
|
287
|
+
request: Request;
|
|
288
|
+
}) => Promise<MyGoogleProfile | null>;
|
|
289
|
+
logoutLoader: ({ request }: {
|
|
290
|
+
request: Request;
|
|
291
|
+
}) => Promise<never>;
|
|
292
|
+
logout: (request: Request) => Promise<string>;
|
|
293
|
+
logoutAndRedirect: (request: Request) => Promise<Response>;
|
|
294
|
+
googleCallbackLoader: ({ request }: {
|
|
295
|
+
request: Request;
|
|
296
|
+
}) => Promise<never>;
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
export { type AuthServer, type AuthServerConfig, type AuthSessionStorage, type BackendAuthPort, GoogleInvalidGrantError, type GoogleOAuthConfig, type LoggerPort, type MockUserPort, type SessionConfig, TokenRefreshError, type UserPhotoPort, type WarmupPort, createAuthHandlers, createAuthServer, createAuthSessionStorage, createAuthenticate, createAuthenticateInLoader, createGoogleAuthenticator };
|