hazo_auth 10.0.0 โ 10.2.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 +119 -0
- package/SETUP_CHECKLIST.md +18 -0
- package/cli-src/lib/auth/nextauth_config.ts +41 -0
- package/cli-src/lib/auth/request_google_scopes.ts +23 -0
- package/cli-src/lib/hazo_connect_instance.server.ts +16 -0
- package/cli-src/lib/schema/sqlite_schema.ts +16 -0
- package/cli-src/lib/services/google_token_service.ts +408 -0
- package/cli-src/lib/services/index.ts +1 -1
- package/dist/client.d.ts +1 -0
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +2 -0
- package/dist/components/layouts/google_token_test/index.d.ts +6 -0
- package/dist/components/layouts/google_token_test/index.d.ts.map +1 -0
- package/dist/components/layouts/google_token_test/index.js +74 -0
- package/dist/components/layouts/shared/components/sidebar_layout_wrapper.d.ts.map +1 -1
- package/dist/components/layouts/shared/components/sidebar_layout_wrapper.js +2 -2
- package/dist/components/ui/button.d.ts +1 -1
- package/dist/components/ui/input-otp.d.ts +2 -2
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/lib/auth/nextauth_config.d.ts +2 -0
- package/dist/lib/auth/nextauth_config.d.ts.map +1 -1
- package/dist/lib/auth/nextauth_config.js +39 -1
- package/dist/lib/auth/request_google_scopes.d.ts +10 -0
- package/dist/lib/auth/request_google_scopes.d.ts.map +1 -0
- package/dist/lib/auth/request_google_scopes.js +13 -0
- package/dist/lib/hazo_connect_instance.server.d.ts +4 -0
- package/dist/lib/hazo_connect_instance.server.d.ts.map +1 -1
- package/dist/lib/hazo_connect_instance.server.js +15 -0
- package/dist/lib/schema/sqlite_schema.d.ts +1 -1
- package/dist/lib/schema/sqlite_schema.d.ts.map +1 -1
- package/dist/lib/schema/sqlite_schema.js +16 -0
- package/dist/lib/services/google_token_service.d.ts +48 -0
- package/dist/lib/services/google_token_service.d.ts.map +1 -0
- package/dist/lib/services/google_token_service.js +319 -0
- package/dist/lib/services/index.d.ts +1 -0
- package/dist/lib/services/index.d.ts.map +1 -1
- package/dist/lib/services/index.js +1 -0
- package/dist/server/routes/google_token.d.ts +13 -0
- package/dist/server/routes/google_token.d.ts.map +1 -0
- package/dist/server/routes/google_token.js +66 -0
- package/dist/server/routes/index.d.ts +1 -0
- package/dist/server/routes/index.d.ts.map +1 -1
- package/dist/server/routes/index.js +2 -0
- package/dist/server/routes/me.d.ts.map +1 -1
- package/dist/server/routes/me.js +3 -0
- package/dist/server/routes/user_management_users.d.ts +1 -1
- package/dist/server-lib.d.ts +1 -1
- package/dist/server-lib.d.ts.map +1 -1
- package/dist/server-lib.js +1 -1
- package/package.json +11 -7
package/README.md
CHANGED
|
@@ -2,6 +2,83 @@
|
|
|
2
2
|
|
|
3
3
|
A reusable authentication UI component package powered by Next.js, TailwindCSS, and shadcn. It integrates `hazo_config` for configuration management and `hazo_connect` for data access, enabling future components to stay aligned with platform conventions.
|
|
4
4
|
|
|
5
|
+
### What's New in v10.2.0 ๐งช
|
|
6
|
+
|
|
7
|
+
**Test-friendly hazo_connect injection + autotest/middleware fixes.**
|
|
8
|
+
|
|
9
|
+
- **`set_hazo_connect_instance(adapter)` / `reset_hazo_connect_instance()`** from `hazo_auth/server-lib` โ inject a pre-built adapter into both the hazo_auth singleton cache and the underlying hazo_connect singleton (companion to hazo_connect 3.6.0 FR-001). Call `set_*` in `beforeAll` and `reset_*` in `afterAll` to swap in a test SQLite adapter without touching the production config:
|
|
10
|
+
|
|
11
|
+
```typescript
|
|
12
|
+
import { set_hazo_connect_instance, reset_hazo_connect_instance } from "hazo_auth/server-lib";
|
|
13
|
+
|
|
14
|
+
beforeAll(() => set_hazo_connect_instance(testAdapter));
|
|
15
|
+
afterAll(() => reset_hazo_connect_instance());
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
- **Middleware no longer redirects `/api/` requests to the login page** โ protected API routes return JSON `401`/`403` and the login redirect is now built from the request's own origin (fixes `Failed to fetch` on any non-default dev/prod port).
|
|
19
|
+
- **`/api/hazo_auth/me`** now exposes `legal_acceptance` at the top level; **`/api/hazo_auth/health`** now returns a top-level `ok` boolean.
|
|
20
|
+
- Browser autotest scenarios corrected for browser `fetch` semantics (opaque redirects, `*_default.jpg` images, admin-gated `401`/`403`, current `relationships` body shape).
|
|
21
|
+
|
|
22
|
+
Requires `hazo_connect ^3.6.0`.
|
|
23
|
+
|
|
24
|
+
### What's New in v10.1.0 ๐
|
|
25
|
+
|
|
26
|
+
**Incremental Google OAuth Scopes & Server-Side Token Access** โ Grant additional Google API scopes (Analytics, Sheets, Drive, etc.) without creating a separate OAuth app. Store encrypted tokens server-side and access them programmatically.
|
|
27
|
+
|
|
28
|
+
**New Features:**
|
|
29
|
+
- **`hazo_google_oauth_tokens` table** (migration 021) โ Encrypted AES-256-GCM storage for Google OAuth tokens with scope tracking
|
|
30
|
+
- **Token Service Exports** from `hazo_auth/server-lib`:
|
|
31
|
+
- `store_google_oauth_token(user_id, tokens, scopes)` โ Save OAuth tokens after user grants new scopes
|
|
32
|
+
- `getGoogleToken(user_id, opts?)` โ Get a fresh access token (refreshes if expired)
|
|
33
|
+
- `revoke_google_oauth_token(user_id)` โ Permanently revoke stored token and forget scopes
|
|
34
|
+
- `get_google_token_status(user_id)` โ Check connection status, scopes, and expiry
|
|
35
|
+
- **Client Helper** โ `requestGoogleScopes(scopes, opts?)` from `hazo_auth/client` triggers Google consent prompt for incremental scopes
|
|
36
|
+
- **HTTP Routes:**
|
|
37
|
+
- `GET /api/hazo_auth/google/token` โ Returns `{ connected, scopes, expires_at }`
|
|
38
|
+
- `DELETE /api/hazo_auth/google/token` โ Revoke stored token without signing user out
|
|
39
|
+
- **NextAuth Config Updates** โ Captures `refresh_token` and adds `include_granted_scopes: true` parameter for incremental scope flow
|
|
40
|
+
- **Route Handler Exports** from `hazo_auth/server/routes`:
|
|
41
|
+
- `googleTokenGET` โ implements GET status endpoint
|
|
42
|
+
- `googleTokenDELETE` โ implements DELETE revoke endpoint
|
|
43
|
+
|
|
44
|
+
**Setup:**
|
|
45
|
+
```bash
|
|
46
|
+
# 1. Run the migration
|
|
47
|
+
npm run migrate -- migrations/021_hazo_google_oauth_tokens.sql
|
|
48
|
+
|
|
49
|
+
# 2. Set encryption environment variables
|
|
50
|
+
HAZO_AUTH_OAUTH_KEY_CURRENT=v1
|
|
51
|
+
HAZO_AUTH_OAUTH_KEY_V1=$(openssl rand -base64 32)
|
|
52
|
+
|
|
53
|
+
# 3. Install optional peer (for token encryption)
|
|
54
|
+
npm install hazo_secure
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
**Client Usage:**
|
|
58
|
+
```tsx
|
|
59
|
+
import { requestGoogleScopes } from "hazo_auth/client";
|
|
60
|
+
|
|
61
|
+
<button onClick={() => requestGoogleScopes([
|
|
62
|
+
"https://www.googleapis.com/auth/analytics.readonly"
|
|
63
|
+
])}>
|
|
64
|
+
Connect Google Analytics
|
|
65
|
+
</button>
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
**Server Usage:**
|
|
69
|
+
```ts
|
|
70
|
+
import { getGoogleToken } from "hazo_auth/server-lib";
|
|
71
|
+
|
|
72
|
+
const result = await getGoogleToken(userId, {
|
|
73
|
+
scopes: ["https://www.googleapis.com/auth/analytics.readonly"]
|
|
74
|
+
});
|
|
75
|
+
if (result.ok) {
|
|
76
|
+
// use result.access_token to call Google APIs
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
See [Google API Access (Incremental Scopes)](#google-api-access-incremental-scopes) below for full details.
|
|
81
|
+
|
|
5
82
|
### What's New in v9.1.1 ๐ง
|
|
6
83
|
|
|
7
84
|
**Dev-server noise fixes for Next.js 16 + Turbopack**
|
|
@@ -1251,6 +1328,48 @@ Google OAuth adds one new dependency:
|
|
|
1251
1328
|
- Check logs for `invitation_table_missing` warnings
|
|
1252
1329
|
- If using custom paths, set `create_firm_url` to your app's create firm page URL
|
|
1253
1330
|
|
|
1331
|
+
### Google API Access (Incremental Scopes)
|
|
1332
|
+
|
|
1333
|
+
`hazo_auth` v10.1+ supports granting additional Google API scopes (Analytics, Sheets, etc.) without a separate OAuth app.
|
|
1334
|
+
|
|
1335
|
+
**Setup:**
|
|
1336
|
+
1. Run the migration: `npm run migrate -- migrations/021_hazo_google_oauth_tokens.sql`
|
|
1337
|
+
2. Set encryption env vars:
|
|
1338
|
+
```bash
|
|
1339
|
+
HAZO_AUTH_OAUTH_KEY_CURRENT=v1
|
|
1340
|
+
HAZO_AUTH_OAUTH_KEY_V1=$(openssl rand -base64 32)
|
|
1341
|
+
```
|
|
1342
|
+
3. Install the optional peer: `npm install hazo_secure`
|
|
1343
|
+
|
|
1344
|
+
**Usage:**
|
|
1345
|
+
|
|
1346
|
+
```tsx
|
|
1347
|
+
// Client component โ triggers Google consent for extra scopes
|
|
1348
|
+
import { requestGoogleScopes } from "hazo_auth/client";
|
|
1349
|
+
|
|
1350
|
+
<button onClick={() => requestGoogleScopes([
|
|
1351
|
+
"https://www.googleapis.com/auth/analytics.readonly"
|
|
1352
|
+
])}>
|
|
1353
|
+
Connect Analytics
|
|
1354
|
+
</button>
|
|
1355
|
+
```
|
|
1356
|
+
|
|
1357
|
+
```ts
|
|
1358
|
+
// Server action / API route โ get a fresh access token
|
|
1359
|
+
import { getGoogleToken } from "hazo_auth/server-lib";
|
|
1360
|
+
|
|
1361
|
+
const result = await getGoogleToken(userId, {
|
|
1362
|
+
scopes: ["https://www.googleapis.com/auth/analytics.readonly"]
|
|
1363
|
+
});
|
|
1364
|
+
if (result.ok) {
|
|
1365
|
+
// use result.access_token to call Google APIs
|
|
1366
|
+
}
|
|
1367
|
+
```
|
|
1368
|
+
|
|
1369
|
+
The incremental scope token is stored and revoked independently of the user's sign-in session. `DELETE /api/hazo_auth/google/token` removes the stored token without signing the user out.
|
|
1370
|
+
|
|
1371
|
+
**Status endpoint:** `GET /api/hazo_auth/google/token` returns `{ connected, scopes, expires_at }`.
|
|
1372
|
+
|
|
1254
1373
|
---
|
|
1255
1374
|
|
|
1256
1375
|
## Using Components
|
package/SETUP_CHECKLIST.md
CHANGED
|
@@ -1133,6 +1133,24 @@ ls app/api/hazo_auth/set_password/route.ts
|
|
|
1133
1133
|
- [ ] OAuth API routes created (`[...nextauth]`, `oauth/google/callback`, `set_password`)
|
|
1134
1134
|
- [ ] Post-login redirect configured (if not using invitations, set `skip_invitation_check = true`)
|
|
1135
1135
|
|
|
1136
|
+
#### Google API Token Storage (optional โ required for `getGoogleToken`)
|
|
1137
|
+
|
|
1138
|
+
Only needed if you use `requestGoogleScopes` / `getGoogleToken` for Google API access beyond sign-in:
|
|
1139
|
+
|
|
1140
|
+
1. Install optional peer: `npm install hazo_secure`
|
|
1141
|
+
2. Run migration: `npm run migrate -- migrations/021_hazo_google_oauth_tokens.sql`
|
|
1142
|
+
3. Set env vars:
|
|
1143
|
+
```env
|
|
1144
|
+
HAZO_AUTH_OAUTH_KEY_CURRENT=v1
|
|
1145
|
+
HAZO_AUTH_OAUTH_KEY_V1=<base64-32-bytes from: openssl rand -base64 32>
|
|
1146
|
+
```
|
|
1147
|
+
4. Wire the new routes in your Next.js app (same pattern as other hazo_auth routes):
|
|
1148
|
+
```ts
|
|
1149
|
+
// app/api/hazo_auth/google/token/route.ts
|
|
1150
|
+
export { GET as googleTokenGET, DELETE as googleTokenDELETE } from "hazo_auth/server/routes";
|
|
1151
|
+
export const dynamic = "force-dynamic";
|
|
1152
|
+
```
|
|
1153
|
+
|
|
1136
1154
|
---
|
|
1137
1155
|
|
|
1138
1156
|
## Phase 4: API Routes
|
|
@@ -11,6 +11,7 @@ import { get_oauth_config } from "../oauth_config.server.js";
|
|
|
11
11
|
import { handle_google_oauth_login } from "../services/oauth_service.js";
|
|
12
12
|
import { get_hazo_connect_instance } from "../hazo_connect_instance.server.js";
|
|
13
13
|
import { create_app_logger } from "../app_logger.js";
|
|
14
|
+
import { store_google_oauth_token, GoogleTokenStorageUnconfigured } from "../services/google_token_service.js";
|
|
14
15
|
|
|
15
16
|
// section: types
|
|
16
17
|
export type NextAuthCallbackUser = {
|
|
@@ -27,6 +28,8 @@ export type NextAuthCallbackAccount = {
|
|
|
27
28
|
access_token?: string;
|
|
28
29
|
id_token?: string;
|
|
29
30
|
expires_at?: number;
|
|
31
|
+
refresh_token?: string;
|
|
32
|
+
scope?: string;
|
|
30
33
|
};
|
|
31
34
|
|
|
32
35
|
export type NextAuthCallbackProfile = {
|
|
@@ -62,6 +65,7 @@ export function get_nextauth_config(): AuthOptions {
|
|
|
62
65
|
prompt: "consent",
|
|
63
66
|
access_type: "offline",
|
|
64
67
|
response_type: "code",
|
|
68
|
+
include_granted_scopes: "true",
|
|
65
69
|
},
|
|
66
70
|
},
|
|
67
71
|
})
|
|
@@ -170,6 +174,43 @@ export function get_nextauth_config(): AuthOptions {
|
|
|
170
174
|
// Store user_id in account for the JWT callback to pick up
|
|
171
175
|
(account as Record<string, unknown>).hazo_user_id = result.user_id;
|
|
172
176
|
|
|
177
|
+
// Capture refresh token when extra scopes were granted
|
|
178
|
+
const BASE_SCOPES = ["openid", "email", "profile"];
|
|
179
|
+
const granted_scopes = (account.scope ?? "").split(" ").filter(Boolean);
|
|
180
|
+
const has_extra_scopes = granted_scopes.some(s => !BASE_SCOPES.includes(s));
|
|
181
|
+
|
|
182
|
+
if (account.refresh_token && has_extra_scopes) {
|
|
183
|
+
try {
|
|
184
|
+
await store_google_oauth_token({
|
|
185
|
+
user_id: result.user_id!,
|
|
186
|
+
refresh_token: account.refresh_token,
|
|
187
|
+
access_token: account.access_token,
|
|
188
|
+
scopes: account.scope ?? "",
|
|
189
|
+
expires_at: account.expires_at
|
|
190
|
+
? new Date(account.expires_at * 1000).toISOString()
|
|
191
|
+
: undefined,
|
|
192
|
+
});
|
|
193
|
+
logger.info("nextauth_google_token_stored", {
|
|
194
|
+
user_id: result.user_id,
|
|
195
|
+
scopes: account.scope,
|
|
196
|
+
});
|
|
197
|
+
} catch (tokenError) {
|
|
198
|
+
if (tokenError instanceof GoogleTokenStorageUnconfigured) {
|
|
199
|
+
logger.error("nextauth_google_token_storage_unconfigured", {
|
|
200
|
+
user_id: result.user_id,
|
|
201
|
+
error: tokenError.message,
|
|
202
|
+
});
|
|
203
|
+
return "/api/auth/error?error=GoogleTokenStorageUnconfigured";
|
|
204
|
+
}
|
|
205
|
+
const errorMsg = tokenError instanceof Error ? tokenError.message : String(tokenError);
|
|
206
|
+
logger.error("nextauth_google_token_store_failed", {
|
|
207
|
+
user_id: result.user_id,
|
|
208
|
+
error: errorMsg,
|
|
209
|
+
});
|
|
210
|
+
// Non-fatal: continue sign-in even if token storage fails
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
173
214
|
return true;
|
|
174
215
|
} catch (error) {
|
|
175
216
|
const errorMessage = error instanceof Error ? error.message : "Unknown error";
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// file_description: client helper to trigger incremental Google OAuth scope consent
|
|
2
|
+
"use client";
|
|
3
|
+
|
|
4
|
+
import { signIn } from "next-auth/react";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Triggers an incremental Google OAuth consent flow to grant additional scopes.
|
|
8
|
+
* Call from a client component when the user needs to grant extra Google API access
|
|
9
|
+
* (e.g. analytics, sheets). On completion, hazo_auth stores the refresh token.
|
|
10
|
+
*/
|
|
11
|
+
export function requestGoogleScopes(
|
|
12
|
+
scopes: string[],
|
|
13
|
+
opts?: { callbackUrl?: string }
|
|
14
|
+
): ReturnType<typeof signIn> {
|
|
15
|
+
const scope = Array.from(
|
|
16
|
+
new Set(["openid", "email", "profile", ...scopes])
|
|
17
|
+
).join(" ");
|
|
18
|
+
return signIn(
|
|
19
|
+
"google",
|
|
20
|
+
{ callbackUrl: opts?.callbackUrl ?? "/" },
|
|
21
|
+
{ scope, access_type: "offline", include_granted_scopes: "true", prompt: "consent" }
|
|
22
|
+
);
|
|
23
|
+
}
|
|
@@ -8,6 +8,7 @@ import "server-only";
|
|
|
8
8
|
// section: imports
|
|
9
9
|
import type { HazoConnectAdapter } from "hazo_connect";
|
|
10
10
|
import { getHazoConnectSingleton } from "hazo_connect/nextjs/setup";
|
|
11
|
+
import { setHazoConnectSingleton, resetHazoConnectSingleton } from "hazo_connect/testing";
|
|
11
12
|
import { create_sqlite_hazo_connect_server, get_hazo_connect_config_options } from "./hazo_connect_setup.server.js";
|
|
12
13
|
import { initializeAdminService, getSqliteAdminService } from "hazo_connect/server";
|
|
13
14
|
import { create_app_logger } from "./app_logger.js";
|
|
@@ -32,6 +33,8 @@ let isInitialized = false;
|
|
|
32
33
|
* @returns The singleton HazoConnectAdapter instance
|
|
33
34
|
*/
|
|
34
35
|
export function get_hazo_connect_instance(): HazoConnectAdapter {
|
|
36
|
+
// Honor test injection or cached fallback first
|
|
37
|
+
if (hazoConnectInstance) return hazoConnectInstance;
|
|
35
38
|
// Use the new singleton API from hazo_connect
|
|
36
39
|
// This automatically handles:
|
|
37
40
|
// - Instance reuse
|
|
@@ -102,3 +105,16 @@ export function get_hazo_connect_instance(): HazoConnectAdapter {
|
|
|
102
105
|
}
|
|
103
106
|
}
|
|
104
107
|
|
|
108
|
+
/** Inject a connect adapter for tests (mirrors set_hazo_connect_instance pattern). */
|
|
109
|
+
export function set_hazo_connect_instance(adapter: HazoConnectAdapter): void {
|
|
110
|
+
hazoConnectInstance = adapter;
|
|
111
|
+
setHazoConnectSingleton(adapter);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Reset the injected adapter (call in afterAll). */
|
|
115
|
+
export function reset_hazo_connect_instance(): void {
|
|
116
|
+
hazoConnectInstance = null;
|
|
117
|
+
isInitialized = false;
|
|
118
|
+
resetHazoConnectSingleton();
|
|
119
|
+
}
|
|
120
|
+
|
|
@@ -161,4 +161,20 @@ CREATE INDEX IF NOT EXISTS idx_hazo_user_relationships_child ON hazo_user_relati
|
|
|
161
161
|
-- Firm admin role (for firm creators)
|
|
162
162
|
INSERT OR IGNORE INTO hazo_roles (id, role_name, created_at, changed_at)
|
|
163
163
|
VALUES (lower(hex(randomblob(4)) || '-' || hex(randomblob(2)) || '-4' || substr(hex(randomblob(2)),2) || '-' || substr('89ab',abs(random()) % 4 + 1, 1) || substr(hex(randomblob(2)),2) || '-' || hex(randomblob(6))), 'firm_admin', datetime('now'), datetime('now'));
|
|
164
|
+
|
|
165
|
+
-- Google OAuth tokens (encrypted refresh/access tokens for extra-scope API access)
|
|
166
|
+
CREATE TABLE IF NOT EXISTS hazo_google_oauth_tokens (
|
|
167
|
+
id TEXT PRIMARY KEY,
|
|
168
|
+
user_id TEXT NOT NULL REFERENCES hazo_users(id) ON DELETE CASCADE,
|
|
169
|
+
provider TEXT NOT NULL DEFAULT 'google',
|
|
170
|
+
refresh_token_enc TEXT NOT NULL,
|
|
171
|
+
access_token_enc TEXT,
|
|
172
|
+
scopes TEXT NOT NULL DEFAULT '',
|
|
173
|
+
expires_at TEXT,
|
|
174
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
175
|
+
changed_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
176
|
+
UNIQUE(user_id, provider)
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
CREATE INDEX IF NOT EXISTS idx_hazo_google_oauth_tokens_user ON hazo_google_oauth_tokens(user_id);
|
|
164
180
|
`;
|