hazo_auth 1.6.5 → 1.6.7
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 -0
- package/SETUP_CHECKLIST.md +81 -1
- package/dist/app/api/hazo_auth/login/route.d.ts.map +1 -1
- package/dist/app/api/hazo_auth/login/route.js +25 -0
- package/dist/app/api/hazo_auth/logout/route.d.ts.map +1 -1
- package/dist/app/api/hazo_auth/logout/route.js +5 -0
- package/dist/components/layouts/register/hooks/use_register_form.d.ts.map +1 -1
- package/dist/components/layouts/register/hooks/use_register_form.js +34 -6
- package/dist/components/layouts/user_management/index.js +9 -9
- package/dist/lib/auth/hazo_get_auth.server.d.ts.map +1 -1
- package/dist/lib/auth/hazo_get_auth.server.js +31 -3
- package/dist/lib/auth/session_token_validator.edge.d.ts +15 -0
- package/dist/lib/auth/session_token_validator.edge.d.ts.map +1 -0
- package/dist/lib/auth/session_token_validator.edge.js +64 -0
- package/dist/lib/services/session_token_service.d.ts +27 -0
- package/dist/lib/services/session_token_service.d.ts.map +1 -0
- package/dist/lib/services/session_token_service.js +130 -0
- package/dist/server/middleware.d.ts +3 -0
- package/dist/server/middleware.d.ts.map +1 -0
- package/dist/server/middleware.js +5 -0
- package/package.json +6 -1
package/README.md
CHANGED
|
@@ -2,6 +2,10 @@
|
|
|
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 (v1.6.6+)
|
|
6
|
+
|
|
7
|
+
- **JWT Session Tokens for Edge-Compatible Authentication**: hazo_auth now issues JWT session tokens on login, enabling secure Edge Runtime authentication in Next.js proxy/middleware files. This provides better security than simple cookie checks while maintaining high performance. See [Proxy/Middleware Authentication](#proxymiddleware-authentication) for details.
|
|
8
|
+
|
|
5
9
|
## Table of Contents
|
|
6
10
|
|
|
7
11
|
- [Installation](#installation)
|
|
@@ -10,6 +14,7 @@ A reusable authentication UI component package powered by Next.js, TailwindCSS,
|
|
|
10
14
|
- [Database Setup](#database-setup)
|
|
11
15
|
- [Using Components](#using-components)
|
|
12
16
|
- [Authentication Service](#authentication-service)
|
|
17
|
+
- [Proxy/Middleware Authentication](#proxymiddleware-authentication)
|
|
13
18
|
- [Profile Picture Menu Widget](#profile-picture-menu-widget)
|
|
14
19
|
- [User Profile Service](#user-profile-service)
|
|
15
20
|
- [Local Development](#local-development)
|
|
@@ -108,6 +113,8 @@ import { hazo_get_auth } from "hazo_auth/lib/auth/hazo_get_auth.server";
|
|
|
108
113
|
|
|
109
114
|
## Required Dependencies
|
|
110
115
|
|
|
116
|
+
**Note:** The `jose` package is now included as a dependency for Edge-compatible JWT operations. This is automatically installed when you run `npm install hazo_auth`.
|
|
117
|
+
|
|
111
118
|
hazo_auth uses shadcn/ui components. Install the required dependencies in your project:
|
|
112
119
|
|
|
113
120
|
```bash
|
|
@@ -252,8 +259,11 @@ cp node_modules/hazo_auth/hazo_notify_config.example.ini ./hazo_notify_config.in
|
|
|
252
259
|
|
|
253
260
|
- Create a `.env.local` file in your project root
|
|
254
261
|
- Add `ZEPTOMAIL_API_KEY=your_api_key_here` (if using Zeptomail)
|
|
262
|
+
- Add `JWT_SECRET=your_secure_random_string_at_least_32_characters` (required for JWT session tokens)
|
|
255
263
|
- Add other sensitive configuration values as needed
|
|
256
264
|
|
|
265
|
+
**Note:** `JWT_SECRET` is required for JWT session token functionality (used for Edge-compatible proxy/middleware authentication). Generate a secure random string at least 32 characters long.
|
|
266
|
+
|
|
257
267
|
**Important:** The configuration files must be located in your project root directory (where `process.cwd()` points to), not inside `node_modules`. The package reads configuration from `process.cwd()` at runtime, so storing them elsewhere (including `node_modules/hazo_auth`) will break runtime access.
|
|
258
268
|
|
|
259
269
|
---
|
|
@@ -611,6 +621,9 @@ import { hazo_get_auth } from "hazo_auth/lib/auth/hazo_get_auth.server";
|
|
|
611
621
|
|
|
612
622
|
// Server utilities
|
|
613
623
|
import { ... } from "hazo_auth/server";
|
|
624
|
+
|
|
625
|
+
// Edge-compatible proxy/middleware authentication (v1.6.6+)
|
|
626
|
+
import { validate_session_cookie } from "hazo_auth/server/middleware";
|
|
614
627
|
```
|
|
615
628
|
|
|
616
629
|
**Note:** The package uses relative imports internally. Consumers should only import from the exposed entry points listed above. Do not import from internal paths like `hazo_auth/components/ui/*` - these are internal modules.
|
|
@@ -795,6 +808,93 @@ if (data.authenticated) {
|
|
|
795
808
|
|
|
796
809
|
**Note:** The `use_auth_status` hook automatically uses this endpoint and includes permissions in its return value.
|
|
797
810
|
|
|
811
|
+
### Proxy/Middleware Authentication
|
|
812
|
+
|
|
813
|
+
hazo_auth provides Edge-compatible authentication for Next.js proxy/middleware files. **Note:** Next.js is migrating from `middleware.ts` to `proxy.ts` (see [Next.js documentation](https://nextjs.org/docs/messages/middleware-to-proxy)), but the functionality remains the same.
|
|
814
|
+
|
|
815
|
+
#### Edge Runtime Limitations
|
|
816
|
+
|
|
817
|
+
Next.js proxy/middleware runs in Edge Runtime, which cannot use Node.js APIs (like SQLite). Therefore, `hazo_get_auth` cannot be used directly in proxy/middleware because it requires database access.
|
|
818
|
+
|
|
819
|
+
#### JWT Session Tokens
|
|
820
|
+
|
|
821
|
+
**New in v1.6.6+:** hazo_auth now issues JWT session tokens on login that can be validated in Edge Runtime:
|
|
822
|
+
|
|
823
|
+
- **Cookie Name:** `hazo_auth_session`
|
|
824
|
+
- **Token Type:** JWT (signed with `JWT_SECRET`)
|
|
825
|
+
- **Expiry:** 30 days (configurable)
|
|
826
|
+
- **Validation:** Signature and expiry checked without database access
|
|
827
|
+
- **Backward Compatible:** Existing `hazo_auth_user_id` and `hazo_auth_user_email` cookies still work
|
|
828
|
+
|
|
829
|
+
**Requirements:**
|
|
830
|
+
- `JWT_SECRET` environment variable must be set (see [Configuration Setup](#configuration-setup))
|
|
831
|
+
- The `jose` package is included as a dependency (Edge-compatible JWT library)
|
|
832
|
+
|
|
833
|
+
#### Using in Proxy/Middleware
|
|
834
|
+
|
|
835
|
+
**Recommended: Use JWT validation (Edge-compatible)**
|
|
836
|
+
|
|
837
|
+
```typescript
|
|
838
|
+
// proxy.ts (or middleware.ts - both work)
|
|
839
|
+
import { NextResponse } from "next/server";
|
|
840
|
+
import type { NextRequest } from "next/server";
|
|
841
|
+
import { validate_session_cookie } from "hazo_auth/server/middleware";
|
|
842
|
+
|
|
843
|
+
export async function proxy(request: NextRequest) {
|
|
844
|
+
const { pathname } = request.nextUrl;
|
|
845
|
+
|
|
846
|
+
// Protect routes
|
|
847
|
+
if (pathname.startsWith("/members")) {
|
|
848
|
+
const { valid } = await validate_session_cookie(request);
|
|
849
|
+
|
|
850
|
+
if (!valid) {
|
|
851
|
+
const login_url = new URL("/hazo_auth/login", request.url);
|
|
852
|
+
login_url.searchParams.set("redirect", pathname);
|
|
853
|
+
return NextResponse.redirect(login_url);
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
return NextResponse.next();
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
export const config = {
|
|
861
|
+
matcher: ["/((?!api|_next/static|_next/image|favicon.ico).*)"],
|
|
862
|
+
};
|
|
863
|
+
```
|
|
864
|
+
|
|
865
|
+
**Fallback: Simple cookie check (less secure, but works)**
|
|
866
|
+
|
|
867
|
+
If JWT validation fails or you need a simpler check:
|
|
868
|
+
|
|
869
|
+
```typescript
|
|
870
|
+
// proxy.ts
|
|
871
|
+
import { NextResponse } from "next/server";
|
|
872
|
+
import type { NextRequest } from "next/server";
|
|
873
|
+
|
|
874
|
+
export async function proxy(request: NextRequest) {
|
|
875
|
+
const { pathname } = request.nextUrl;
|
|
876
|
+
|
|
877
|
+
if (pathname.startsWith("/members")) {
|
|
878
|
+
const user_id = request.cookies.get("hazo_auth_user_id")?.value;
|
|
879
|
+
const user_email = request.cookies.get("hazo_auth_user_email")?.value;
|
|
880
|
+
|
|
881
|
+
if (!user_id || !user_email) {
|
|
882
|
+
const login_url = new URL("/hazo_auth/login", request.url);
|
|
883
|
+
login_url.searchParams.set("redirect", pathname);
|
|
884
|
+
return NextResponse.redirect(login_url);
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
return NextResponse.next();
|
|
889
|
+
}
|
|
890
|
+
```
|
|
891
|
+
|
|
892
|
+
**Important Notes:**
|
|
893
|
+
- JWT validation provides better security (signature validation, tamper detection)
|
|
894
|
+
- Simple cookie check is faster but doesn't validate token integrity
|
|
895
|
+
- Full user status checks (e.g., deactivated accounts) happen in API routes/layouts
|
|
896
|
+
- Both approaches work - JWT is recommended for production
|
|
897
|
+
|
|
798
898
|
### Server-Side Functions
|
|
799
899
|
|
|
800
900
|
#### `hazo_get_auth` (Recommended)
|
package/SETUP_CHECKLIST.md
CHANGED
|
@@ -196,6 +196,7 @@ HAZO_CONNECT_POSTGREST_API_KEY=your_postgrest_api_key_here
|
|
|
196
196
|
|
|
197
197
|
# Required for JWT authentication
|
|
198
198
|
JWT_SECRET=your_secure_random_string_at_least_32_characters
|
|
199
|
+
# Note: JWT_SECRET is required for JWT session token functionality (Edge-compatible proxy/middleware authentication)
|
|
199
200
|
```
|
|
200
201
|
|
|
201
202
|
**Generate a secure JWT secret:**
|
|
@@ -216,7 +217,7 @@ from_name = Your App Name
|
|
|
216
217
|
**Checklist:**
|
|
217
218
|
- [ ] `.env.local` file created
|
|
218
219
|
- [ ] `ZEPTOMAIL_API_KEY` set (or email will not work)
|
|
219
|
-
- [ ] `JWT_SECRET` set
|
|
220
|
+
- [ ] `JWT_SECRET` set (required for JWT session tokens - Edge-compatible proxy/middleware authentication)
|
|
220
221
|
- [ ] `from_email` configured in `hazo_notify_config.ini`
|
|
221
222
|
|
|
222
223
|
---
|
|
@@ -678,6 +679,85 @@ export default function CustomLoginPage() {
|
|
|
678
679
|
|
|
679
680
|
---
|
|
680
681
|
|
|
682
|
+
## Phase 5.1: Proxy/Middleware Setup (Optional)
|
|
683
|
+
|
|
684
|
+
**Note:** Next.js is migrating from `middleware.ts` to `proxy.ts` (see [Next.js documentation](https://nextjs.org/docs/messages/middleware-to-proxy)). The functionality remains the same - both work, but `proxy.ts` is the new convention.
|
|
685
|
+
|
|
686
|
+
If you want to protect routes at the Edge Runtime level (before pages load), create a proxy/middleware file:
|
|
687
|
+
|
|
688
|
+
### Step 5.1.1: Create Proxy File (Recommended)
|
|
689
|
+
|
|
690
|
+
Create `proxy.ts` in your project root (or `middleware.ts` - both work):
|
|
691
|
+
|
|
692
|
+
```typescript
|
|
693
|
+
// proxy.ts (or middleware.ts)
|
|
694
|
+
import { NextResponse } from "next/server";
|
|
695
|
+
import type { NextRequest } from "next/server";
|
|
696
|
+
import { validate_session_cookie } from "hazo_auth/server/middleware";
|
|
697
|
+
|
|
698
|
+
export async function proxy(request: NextRequest) {
|
|
699
|
+
const { pathname } = request.nextUrl;
|
|
700
|
+
|
|
701
|
+
// Protect your routes (e.g., /members, /dashboard, etc.)
|
|
702
|
+
if (pathname.startsWith("/members")) {
|
|
703
|
+
const { valid } = await validate_session_cookie(request);
|
|
704
|
+
|
|
705
|
+
if (!valid) {
|
|
706
|
+
const login_url = new URL("/hazo_auth/login", request.url);
|
|
707
|
+
login_url.searchParams.set("redirect", pathname);
|
|
708
|
+
return NextResponse.redirect(login_url);
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
return NextResponse.next();
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
export const config = {
|
|
716
|
+
matcher: ["/((?!api|_next/static|_next/image|favicon.ico).*)"],
|
|
717
|
+
};
|
|
718
|
+
```
|
|
719
|
+
|
|
720
|
+
### Step 5.1.2: Simple Cookie Check (Alternative)
|
|
721
|
+
|
|
722
|
+
If you prefer a simpler approach without JWT validation:
|
|
723
|
+
|
|
724
|
+
```typescript
|
|
725
|
+
// proxy.ts
|
|
726
|
+
import { NextResponse } from "next/server";
|
|
727
|
+
import type { NextRequest } from "next/server";
|
|
728
|
+
|
|
729
|
+
export async function proxy(request: NextRequest) {
|
|
730
|
+
const { pathname } = request.nextUrl;
|
|
731
|
+
|
|
732
|
+
if (pathname.startsWith("/members")) {
|
|
733
|
+
const user_id = request.cookies.get("hazo_auth_user_id")?.value;
|
|
734
|
+
const user_email = request.cookies.get("hazo_auth_user_email")?.value;
|
|
735
|
+
|
|
736
|
+
if (!user_id || !user_email) {
|
|
737
|
+
const login_url = new URL("/hazo_auth/login", request.url);
|
|
738
|
+
login_url.searchParams.set("redirect", pathname);
|
|
739
|
+
return NextResponse.redirect(login_url);
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
return NextResponse.next();
|
|
744
|
+
}
|
|
745
|
+
```
|
|
746
|
+
|
|
747
|
+
**Important Notes:**
|
|
748
|
+
- Proxy/middleware runs in Edge Runtime (cannot use Node.js APIs like SQLite)
|
|
749
|
+
- JWT validation (`validate_session_cookie`) provides better security
|
|
750
|
+
- Simple cookie check is faster but doesn't validate token integrity
|
|
751
|
+
- Full user validation (e.g., deactivated accounts) happens in API routes/layouts
|
|
752
|
+
- Both `proxy.ts` and `middleware.ts` work - Next.js recommends `proxy.ts`
|
|
753
|
+
|
|
754
|
+
**Checklist:**
|
|
755
|
+
- [ ] Proxy/middleware file created (optional - only if you need route protection)
|
|
756
|
+
- [ ] Protected routes configured
|
|
757
|
+
- [ ] JWT validation used (recommended) or simple cookie check
|
|
758
|
+
|
|
759
|
+
---
|
|
760
|
+
|
|
681
761
|
## Phase 6: Verification Tests
|
|
682
762
|
|
|
683
763
|
Run these tests to verify your setup is working correctly.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"route.d.ts","sourceRoot":"","sources":["../../../../../src/app/api/hazo_auth/login/route.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"route.d.ts","sourceRoot":"","sources":["../../../../../src/app/api/hazo_auth/login/route.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAUxD,wBAAsB,IAAI,CAAC,OAAO,EAAE,WAAW;;;;;;;;;IAgM9C"}
|
|
@@ -7,6 +7,7 @@ import { authenticate_user } from "../../../../lib/services/login_service";
|
|
|
7
7
|
import { createCrudService } from "hazo_connect/server";
|
|
8
8
|
import { get_filename, get_line_number } from "../../../../lib/utils/api_route_helpers";
|
|
9
9
|
import { get_login_config } from "../../../../lib/login_config.server";
|
|
10
|
+
import { create_session_token } from "../../../../lib/services/session_token_service";
|
|
10
11
|
// section: api_handler
|
|
11
12
|
export async function POST(request) {
|
|
12
13
|
const logger = create_app_logger();
|
|
@@ -124,6 +125,30 @@ export async function POST(request) {
|
|
|
124
125
|
path: "/",
|
|
125
126
|
maxAge: 60 * 60 * 24 * 30, // 30 days
|
|
126
127
|
});
|
|
128
|
+
// Create and set JWT session token (for Edge-compatible proxy/middleware)
|
|
129
|
+
try {
|
|
130
|
+
const session_token = await create_session_token(user_id, email);
|
|
131
|
+
response.cookies.set("hazo_auth_session", session_token, {
|
|
132
|
+
httpOnly: true,
|
|
133
|
+
secure: process.env.NODE_ENV === "production",
|
|
134
|
+
sameSite: "lax",
|
|
135
|
+
path: "/",
|
|
136
|
+
maxAge: 60 * 60 * 24 * 30, // 30 days
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
catch (token_error) {
|
|
140
|
+
// Log error but don't fail login if token creation fails
|
|
141
|
+
// Backward compatibility: existing cookies still work
|
|
142
|
+
const token_error_message = token_error instanceof Error ? token_error.message : "Unknown error";
|
|
143
|
+
logger.warn("login_session_token_creation_failed", {
|
|
144
|
+
filename: get_filename(),
|
|
145
|
+
line_number: get_line_number(),
|
|
146
|
+
user_id,
|
|
147
|
+
email,
|
|
148
|
+
error: token_error_message,
|
|
149
|
+
note: "Login succeeded but session token creation failed - using legacy cookies",
|
|
150
|
+
});
|
|
151
|
+
}
|
|
127
152
|
return response;
|
|
128
153
|
}
|
|
129
154
|
catch (error) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"route.d.ts","sourceRoot":"","sources":["../../../../../src/app/api/hazo_auth/logout/route.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAOxD,wBAAsB,IAAI,CAAC,OAAO,EAAE,WAAW;;;;;
|
|
1
|
+
{"version":3,"file":"route.d.ts","sourceRoot":"","sources":["../../../../../src/app/api/hazo_auth/logout/route.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAOxD,wBAAsB,IAAI,CAAC,OAAO,EAAE,WAAW;;;;;IAmF9C"}
|
|
@@ -27,6 +27,11 @@ export async function POST(request) {
|
|
|
27
27
|
expires: new Date(0),
|
|
28
28
|
path: "/",
|
|
29
29
|
});
|
|
30
|
+
// Clear JWT session token cookie
|
|
31
|
+
response.cookies.set("hazo_auth_session", "", {
|
|
32
|
+
expires: new Date(0),
|
|
33
|
+
path: "/",
|
|
34
|
+
});
|
|
30
35
|
// Invalidate user cache
|
|
31
36
|
if (user_id) {
|
|
32
37
|
try {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use_register_form.d.ts","sourceRoot":"","sources":["../../../../../src/components/layouts/register/hooks/use_register_form.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,sCAAsC,CAAC;AAC7E,OAAO,KAAK,EAAE,0BAA0B,EAAE,4BAA4B,EAAE,MAAM,0CAA0C,CAAC;AACzH,OAAO,EAAsB,KAAK,eAAe,EAAE,MAAM,iCAAiC,CAAC;AAU3F,MAAM,MAAM,kBAAkB,GAAG,MAAM,CAAC,eAAe,EAAE,MAAM,CAAC,CAAC;AACjE,MAAM,MAAM,kBAAkB,GAAG,OAAO,CAAC,MAAM,CAAC,eAAe,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC,CAAC,GAAG;IACrF,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB,CAAC;AACF,MAAM,MAAM,uBAAuB,GAAG,MAAM,CAC1C,OAAO,CAAC,eAAe,EAAE,UAAU,GAAG,kBAAkB,CAAC,EACzD,OAAO,CACR,CAAC;AAEF,MAAM,MAAM,qBAAqB,CAAC,OAAO,GAAG,OAAO,IAAI;IACrD,aAAa,EAAE,OAAO,CAAC;IACvB,oBAAoB,EAAE,0BAA0B,CAAC;IACjD,4BAA4B,CAAC,EAAE,4BAA4B,CAAC;IAC5D,UAAU,EAAE,gBAAgB,CAAC,OAAO,CAAC,CAAC;IACtC,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,CAAC;AAEF,MAAM,MAAM,qBAAqB,GAAG;IAClC,MAAM,EAAE,kBAAkB,CAAC;IAC3B,MAAM,EAAE,kBAAkB,CAAC;IAC3B,kBAAkB,EAAE,uBAAuB,CAAC;IAC5C,gBAAgB,EAAE,OAAO,CAAC;IAC1B,YAAY,EAAE,OAAO,CAAC;IACtB,YAAY,EAAE,OAAO,CAAC;IACtB,iBAAiB,EAAE,CAAC,OAAO,EAAE,eAAe,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACrE,eAAe,EAAE,MAAM,IAAI,CAAC;IAC5B,wBAAwB,EAAE,CAAC,OAAO,EAAE,UAAU,GAAG,kBAAkB,KAAK,IAAI,CAAC;IAC7E,YAAY,EAAE,CAAC,KAAK,EAAE,KAAK,CAAC,SAAS,CAAC,eAAe,CAAC,KAAK,IAAI,CAAC;IAChE,YAAY,EAAE,MAAM,IAAI,CAAC;CAC1B,CAAC;AAYF,eAAO,MAAM,iBAAiB,GAAI,OAAO,EAAG,kEAKzC,qBAAqB,CAAC,OAAO,CAAC,KAAG,
|
|
1
|
+
{"version":3,"file":"use_register_form.d.ts","sourceRoot":"","sources":["../../../../../src/components/layouts/register/hooks/use_register_form.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,sCAAsC,CAAC;AAC7E,OAAO,KAAK,EAAE,0BAA0B,EAAE,4BAA4B,EAAE,MAAM,0CAA0C,CAAC;AACzH,OAAO,EAAsB,KAAK,eAAe,EAAE,MAAM,iCAAiC,CAAC;AAU3F,MAAM,MAAM,kBAAkB,GAAG,MAAM,CAAC,eAAe,EAAE,MAAM,CAAC,CAAC;AACjE,MAAM,MAAM,kBAAkB,GAAG,OAAO,CAAC,MAAM,CAAC,eAAe,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC,CAAC,GAAG;IACrF,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB,CAAC;AACF,MAAM,MAAM,uBAAuB,GAAG,MAAM,CAC1C,OAAO,CAAC,eAAe,EAAE,UAAU,GAAG,kBAAkB,CAAC,EACzD,OAAO,CACR,CAAC;AAEF,MAAM,MAAM,qBAAqB,CAAC,OAAO,GAAG,OAAO,IAAI;IACrD,aAAa,EAAE,OAAO,CAAC;IACvB,oBAAoB,EAAE,0BAA0B,CAAC;IACjD,4BAA4B,CAAC,EAAE,4BAA4B,CAAC;IAC5D,UAAU,EAAE,gBAAgB,CAAC,OAAO,CAAC,CAAC;IACtC,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,CAAC;AAEF,MAAM,MAAM,qBAAqB,GAAG;IAClC,MAAM,EAAE,kBAAkB,CAAC;IAC3B,MAAM,EAAE,kBAAkB,CAAC;IAC3B,kBAAkB,EAAE,uBAAuB,CAAC;IAC5C,gBAAgB,EAAE,OAAO,CAAC;IAC1B,YAAY,EAAE,OAAO,CAAC;IACtB,YAAY,EAAE,OAAO,CAAC;IACtB,iBAAiB,EAAE,CAAC,OAAO,EAAE,eAAe,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACrE,eAAe,EAAE,MAAM,IAAI,CAAC;IAC5B,wBAAwB,EAAE,CAAC,OAAO,EAAE,UAAU,GAAG,kBAAkB,KAAK,IAAI,CAAC;IAC7E,YAAY,EAAE,CAAC,KAAK,EAAE,KAAK,CAAC,SAAS,CAAC,eAAe,CAAC,KAAK,IAAI,CAAC;IAChE,YAAY,EAAE,MAAM,IAAI,CAAC;CAC1B,CAAC;AAYF,eAAO,MAAM,iBAAiB,GAAI,OAAO,EAAG,kEAKzC,qBAAqB,CAAC,OAAO,CAAC,KAAG,qBAkPnC,CAAC"}
|
|
@@ -18,7 +18,8 @@ const buildInitialValues = () => ({
|
|
|
18
18
|
});
|
|
19
19
|
// section: hook
|
|
20
20
|
export const use_register_form = ({ showNameField, passwordRequirements, dataClient, urlOnLogon, }) => {
|
|
21
|
-
const
|
|
21
|
+
const initialValues = useMemo(() => buildInitialValues(), []);
|
|
22
|
+
const [values, setValues] = useState(initialValues);
|
|
22
23
|
const [errors, setErrors] = useState({});
|
|
23
24
|
const [passwordVisibility, setPasswordVisibility] = useState({
|
|
24
25
|
password: false,
|
|
@@ -26,19 +27,44 @@ export const use_register_form = ({ showNameField, passwordRequirements, dataCli
|
|
|
26
27
|
});
|
|
27
28
|
const [emailTouched, setEmailTouched] = useState(false);
|
|
28
29
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
30
|
+
// Check if form has been edited (changed from initial state)
|
|
31
|
+
const isFormEdited = useMemo(() => {
|
|
32
|
+
return Object.entries(values).some(([fieldId, fieldValue]) => {
|
|
33
|
+
if (fieldId === REGISTER_FIELD_IDS.NAME && !showNameField) {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
return fieldValue.trim() !== initialValues[fieldId].trim();
|
|
37
|
+
});
|
|
38
|
+
}, [values, initialValues, showNameField]);
|
|
29
39
|
const isSubmitDisabled = useMemo(() => {
|
|
40
|
+
// Disable if submitting
|
|
30
41
|
if (isSubmitting) {
|
|
31
42
|
return true;
|
|
32
43
|
}
|
|
44
|
+
// Disable if form hasn't been edited
|
|
45
|
+
if (!isFormEdited) {
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
// Disable if there are validation errors (excluding submit errors)
|
|
49
|
+
const validationErrors = Object.assign({}, errors);
|
|
50
|
+
delete validationErrors.submit;
|
|
51
|
+
const hasErrors = Object.keys(validationErrors).length > 0;
|
|
52
|
+
if (hasErrors) {
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
// Disable if required fields are empty
|
|
33
56
|
const hasEmptyField = Object.entries(values).some(([fieldId, fieldValue]) => {
|
|
34
57
|
if (fieldId === REGISTER_FIELD_IDS.NAME && !showNameField) {
|
|
35
58
|
return false;
|
|
36
59
|
}
|
|
37
60
|
return fieldValue.trim() === "";
|
|
38
61
|
});
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
62
|
+
if (hasEmptyField) {
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
// Enable if form is edited, has no errors, and all required fields are filled
|
|
66
|
+
return false;
|
|
67
|
+
}, [errors, showNameField, values, isSubmitting, isFormEdited]);
|
|
42
68
|
const togglePasswordVisibility = useCallback((fieldId) => {
|
|
43
69
|
setPasswordVisibility((previous) => (Object.assign(Object.assign({}, previous), { [fieldId]: !previous[fieldId] })));
|
|
44
70
|
}, []);
|
|
@@ -136,7 +162,8 @@ export const use_register_form = ({ showNameField, passwordRequirements, dataCli
|
|
|
136
162
|
description: "Your account has been created successfully.",
|
|
137
163
|
});
|
|
138
164
|
// Reset form on success
|
|
139
|
-
|
|
165
|
+
const resetValues = buildInitialValues();
|
|
166
|
+
setValues(resetValues);
|
|
140
167
|
setErrors({});
|
|
141
168
|
setPasswordVisibility({
|
|
142
169
|
password: false,
|
|
@@ -160,7 +187,8 @@ export const use_register_form = ({ showNameField, passwordRequirements, dataCli
|
|
|
160
187
|
}
|
|
161
188
|
}, [values, passwordRequirements, dataClient, urlOnLogon]);
|
|
162
189
|
const handleCancel = useCallback(() => {
|
|
163
|
-
|
|
190
|
+
const resetValues = buildInitialValues();
|
|
191
|
+
setValues(resetValues);
|
|
164
192
|
setErrors({});
|
|
165
193
|
setPasswordVisibility({
|
|
166
194
|
password: false,
|
|
@@ -133,7 +133,7 @@ export function UserManagementLayout({ className }) {
|
|
|
133
133
|
return;
|
|
134
134
|
setUsersActionLoading(true);
|
|
135
135
|
try {
|
|
136
|
-
const response = await fetch("/api/user_management/users", {
|
|
136
|
+
const response = await fetch("/api/hazo_auth/user_management/users", {
|
|
137
137
|
method: "PATCH",
|
|
138
138
|
headers: {
|
|
139
139
|
"Content-Type": "application/json",
|
|
@@ -149,7 +149,7 @@ export function UserManagementLayout({ className }) {
|
|
|
149
149
|
setDeactivateDialogOpen(false);
|
|
150
150
|
setSelectedUser(null);
|
|
151
151
|
// Reload users
|
|
152
|
-
const reload_response = await fetch("/api/user_management/users");
|
|
152
|
+
const reload_response = await fetch("/api/hazo_auth/user_management/users");
|
|
153
153
|
const reload_data = await reload_response.json();
|
|
154
154
|
if (reload_data.success) {
|
|
155
155
|
setUsers(reload_data.users);
|
|
@@ -172,7 +172,7 @@ export function UserManagementLayout({ className }) {
|
|
|
172
172
|
return;
|
|
173
173
|
setUsersActionLoading(true);
|
|
174
174
|
try {
|
|
175
|
-
const response = await fetch("/api/user_management/users", {
|
|
175
|
+
const response = await fetch("/api/hazo_auth/user_management/users", {
|
|
176
176
|
method: "POST",
|
|
177
177
|
headers: {
|
|
178
178
|
"Content-Type": "application/json",
|
|
@@ -224,7 +224,7 @@ export function UserManagementLayout({ className }) {
|
|
|
224
224
|
toast.info(`Skipped: ${data.skipped.join(", ")}`);
|
|
225
225
|
}
|
|
226
226
|
// Reload permissions
|
|
227
|
-
const reload_response = await fetch("/api/user_management/permissions");
|
|
227
|
+
const reload_response = await fetch("/api/hazo_auth/user_management/permissions");
|
|
228
228
|
const reload_data = await reload_response.json();
|
|
229
229
|
if (reload_data.success) {
|
|
230
230
|
const db_perms = reload_data.db_permissions.map((p) => ({
|
|
@@ -259,7 +259,7 @@ export function UserManagementLayout({ className }) {
|
|
|
259
259
|
return;
|
|
260
260
|
setPermissionsActionLoading(true);
|
|
261
261
|
try {
|
|
262
|
-
const response = await fetch("/api/user_management/permissions", {
|
|
262
|
+
const response = await fetch("/api/hazo_auth/user_management/permissions", {
|
|
263
263
|
method: "PUT",
|
|
264
264
|
headers: {
|
|
265
265
|
"Content-Type": "application/json",
|
|
@@ -276,7 +276,7 @@ export function UserManagementLayout({ className }) {
|
|
|
276
276
|
setEditingPermission(null);
|
|
277
277
|
setEditDescription("");
|
|
278
278
|
// Reload permissions
|
|
279
|
-
const reload_response = await fetch("/api/user_management/permissions");
|
|
279
|
+
const reload_response = await fetch("/api/hazo_auth/user_management/permissions");
|
|
280
280
|
const reload_data = await reload_response.json();
|
|
281
281
|
if (reload_data.success) {
|
|
282
282
|
const db_perms = reload_data.db_permissions.map((p) => ({
|
|
@@ -313,7 +313,7 @@ export function UserManagementLayout({ className }) {
|
|
|
313
313
|
}
|
|
314
314
|
setPermissionsActionLoading(true);
|
|
315
315
|
try {
|
|
316
|
-
const response = await fetch("/api/user_management/permissions", {
|
|
316
|
+
const response = await fetch("/api/hazo_auth/user_management/permissions", {
|
|
317
317
|
method: "POST",
|
|
318
318
|
headers: {
|
|
319
319
|
"Content-Type": "application/json",
|
|
@@ -330,7 +330,7 @@ export function UserManagementLayout({ className }) {
|
|
|
330
330
|
setNewPermissionName("");
|
|
331
331
|
setNewPermissionDescription("");
|
|
332
332
|
// Reload permissions
|
|
333
|
-
const reload_response = await fetch("/api/user_management/permissions");
|
|
333
|
+
const reload_response = await fetch("/api/hazo_auth/user_management/permissions");
|
|
334
334
|
const reload_data = await reload_response.json();
|
|
335
335
|
if (reload_data.success) {
|
|
336
336
|
const db_perms = reload_data.db_permissions.map((p) => ({
|
|
@@ -372,7 +372,7 @@ export function UserManagementLayout({ className }) {
|
|
|
372
372
|
if (data.success) {
|
|
373
373
|
toast.success("Permission deleted successfully");
|
|
374
374
|
// Reload permissions
|
|
375
|
-
const reload_response = await fetch("/api/user_management/permissions");
|
|
375
|
+
const reload_response = await fetch("/api/hazo_auth/user_management/permissions");
|
|
376
376
|
const reload_data = await reload_response.json();
|
|
377
377
|
if (reload_data.success) {
|
|
378
378
|
const db_perms = reload_data.db_permissions.map((p) => ({
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"hazo_get_auth.server.d.ts","sourceRoot":"","sources":["../../../src/lib/auth/hazo_get_auth.server.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAK1C,OAAO,KAAK,EAAE,cAAc,EAAgB,eAAe,EAAE,MAAM,cAAc,CAAC;
|
|
1
|
+
{"version":3,"file":"hazo_get_auth.server.d.ts","sourceRoot":"","sources":["../../../src/lib/auth/hazo_get_auth.server.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAK1C,OAAO,KAAK,EAAE,cAAc,EAAgB,eAAe,EAAE,MAAM,cAAc,CAAC;AAmLlF;;;;;;;GAOG;AACH,wBAAsB,aAAa,CACjC,OAAO,EAAE,WAAW,EACpB,OAAO,CAAC,EAAE,eAAe,GACxB,OAAO,CAAC,cAAc,CAAC,CAmKzB"}
|
|
@@ -6,6 +6,7 @@ import { PermissionError } from "./auth_types";
|
|
|
6
6
|
import { get_auth_cache } from "./auth_cache";
|
|
7
7
|
import { get_rate_limiter } from "./auth_rate_limiter";
|
|
8
8
|
import { get_auth_utility_config } from "../auth_utility_config.server";
|
|
9
|
+
import { validate_session_token } from "../services/session_token_service";
|
|
9
10
|
// section: helpers
|
|
10
11
|
/**
|
|
11
12
|
* Gets client IP address from request
|
|
@@ -146,14 +147,41 @@ function get_friendly_error_message(missing_permissions, config) {
|
|
|
146
147
|
* @throws PermissionError if strict mode and permissions are missing
|
|
147
148
|
*/
|
|
148
149
|
export async function hazo_get_auth(request, options) {
|
|
149
|
-
var _a, _b;
|
|
150
|
+
var _a, _b, _c;
|
|
150
151
|
const logger = create_app_logger();
|
|
151
152
|
const config = get_auth_utility_config();
|
|
152
153
|
const cache = get_auth_cache(config.cache_max_users, config.cache_ttl_minutes, config.cache_max_age_minutes);
|
|
153
154
|
const rate_limiter = get_rate_limiter();
|
|
154
155
|
// Fast path: Check for authentication cookies
|
|
155
|
-
|
|
156
|
-
|
|
156
|
+
// Priority: 1. JWT session token (new), 2. Simple cookies (backward compatibility)
|
|
157
|
+
let user_id;
|
|
158
|
+
let user_email;
|
|
159
|
+
// Check for JWT session token first
|
|
160
|
+
const session_token = (_a = request.cookies.get("hazo_auth_session")) === null || _a === void 0 ? void 0 : _a.value;
|
|
161
|
+
if (session_token) {
|
|
162
|
+
try {
|
|
163
|
+
const token_result = await validate_session_token(session_token);
|
|
164
|
+
if (token_result.valid && token_result.user_id && token_result.email) {
|
|
165
|
+
user_id = token_result.user_id;
|
|
166
|
+
user_email = token_result.email;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
catch (token_error) {
|
|
170
|
+
// If token validation fails, fall back to simple cookies
|
|
171
|
+
const token_error_message = token_error instanceof Error ? token_error.message : "Unknown error";
|
|
172
|
+
logger.debug("auth_utility_jwt_validation_failed", {
|
|
173
|
+
filename: get_filename(),
|
|
174
|
+
line_number: get_line_number(),
|
|
175
|
+
error: token_error_message,
|
|
176
|
+
note: "Falling back to simple cookie check",
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
// Fall back to simple cookies if JWT not present or invalid (backward compatibility)
|
|
181
|
+
if (!user_id || !user_email) {
|
|
182
|
+
user_id = (_b = request.cookies.get("hazo_auth_user_id")) === null || _b === void 0 ? void 0 : _b.value;
|
|
183
|
+
user_email = (_c = request.cookies.get("hazo_auth_user_email")) === null || _c === void 0 ? void 0 : _c.value;
|
|
184
|
+
}
|
|
157
185
|
if (!user_id || !user_email) {
|
|
158
186
|
// Unauthenticated - check rate limit by IP
|
|
159
187
|
const client_ip = get_client_ip(request);
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { NextRequest } from "next/server";
|
|
2
|
+
export type ValidateSessionCookieResult = {
|
|
3
|
+
valid: boolean;
|
|
4
|
+
user_id?: string;
|
|
5
|
+
email?: string;
|
|
6
|
+
};
|
|
7
|
+
/**
|
|
8
|
+
* Validates session cookie from NextRequest (Edge-compatible)
|
|
9
|
+
* Extracts hazo_auth_session cookie and validates JWT signature and expiry
|
|
10
|
+
* Works in Edge Runtime (Next.js proxy/middleware)
|
|
11
|
+
* @param request - NextRequest object
|
|
12
|
+
* @returns Validation result with user_id and email if valid
|
|
13
|
+
*/
|
|
14
|
+
export declare function validate_session_cookie(request: NextRequest): Promise<ValidateSessionCookieResult>;
|
|
15
|
+
//# sourceMappingURL=session_token_validator.edge.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session_token_validator.edge.d.ts","sourceRoot":"","sources":["../../../src/lib/auth/session_token_validator.edge.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAG/C,MAAM,MAAM,2BAA2B,GAAG;IACxC,KAAK,EAAE,OAAO,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AAsBF;;;;;;GAMG;AACH,wBAAsB,uBAAuB,CAC3C,OAAO,EAAE,WAAW,GACnB,OAAO,CAAC,2BAA2B,CAAC,CAwCtC"}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// file_description: Edge-compatible JWT session token validator for Next.js proxy/middleware
|
|
2
|
+
// Uses jose library which works in Edge Runtime
|
|
3
|
+
// section: imports
|
|
4
|
+
import { jwtVerify } from "jose";
|
|
5
|
+
// section: helpers
|
|
6
|
+
/**
|
|
7
|
+
* Gets JWT secret from environment variables
|
|
8
|
+
* Works in Edge Runtime (no Node.js APIs)
|
|
9
|
+
* @returns JWT secret as Uint8Array for jose library
|
|
10
|
+
*/
|
|
11
|
+
function get_jwt_secret() {
|
|
12
|
+
const jwt_secret = process.env.JWT_SECRET;
|
|
13
|
+
if (!jwt_secret) {
|
|
14
|
+
// In Edge Runtime, we can't use logger, so we just return null
|
|
15
|
+
// The validation will fail gracefully
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
// Convert string secret to Uint8Array for jose
|
|
19
|
+
return new TextEncoder().encode(jwt_secret);
|
|
20
|
+
}
|
|
21
|
+
// section: main_function
|
|
22
|
+
/**
|
|
23
|
+
* Validates session cookie from NextRequest (Edge-compatible)
|
|
24
|
+
* Extracts hazo_auth_session cookie and validates JWT signature and expiry
|
|
25
|
+
* Works in Edge Runtime (Next.js proxy/middleware)
|
|
26
|
+
* @param request - NextRequest object
|
|
27
|
+
* @returns Validation result with user_id and email if valid
|
|
28
|
+
*/
|
|
29
|
+
export async function validate_session_cookie(request) {
|
|
30
|
+
var _a;
|
|
31
|
+
try {
|
|
32
|
+
// Extract session cookie
|
|
33
|
+
const session_cookie = (_a = request.cookies.get("hazo_auth_session")) === null || _a === void 0 ? void 0 : _a.value;
|
|
34
|
+
if (!session_cookie) {
|
|
35
|
+
return { valid: false };
|
|
36
|
+
}
|
|
37
|
+
// Get JWT secret
|
|
38
|
+
const secret = get_jwt_secret();
|
|
39
|
+
if (!secret) {
|
|
40
|
+
// JWT_SECRET not set - cannot validate
|
|
41
|
+
return { valid: false };
|
|
42
|
+
}
|
|
43
|
+
// Verify JWT signature and expiration
|
|
44
|
+
const { payload } = await jwtVerify(session_cookie, secret, {
|
|
45
|
+
algorithms: ["HS256"],
|
|
46
|
+
});
|
|
47
|
+
// Extract user_id and email from payload
|
|
48
|
+
const user_id = payload.user_id;
|
|
49
|
+
const email = payload.email;
|
|
50
|
+
if (!user_id || !email) {
|
|
51
|
+
return { valid: false };
|
|
52
|
+
}
|
|
53
|
+
return {
|
|
54
|
+
valid: true,
|
|
55
|
+
user_id,
|
|
56
|
+
email,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
// jose throws JWTExpired, JWTInvalid, etc. - these are expected for invalid tokens
|
|
61
|
+
// In Edge Runtime, we can't log, so we just return invalid
|
|
62
|
+
return { valid: false };
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export type SessionTokenPayload = {
|
|
2
|
+
user_id: string;
|
|
3
|
+
email: string;
|
|
4
|
+
iat: number;
|
|
5
|
+
exp: number;
|
|
6
|
+
};
|
|
7
|
+
export type ValidateSessionTokenResult = {
|
|
8
|
+
valid: boolean;
|
|
9
|
+
user_id?: string;
|
|
10
|
+
email?: string;
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* Creates a JWT session token for a user
|
|
14
|
+
* Token includes user_id, email, issued at time, and expiration
|
|
15
|
+
* @param user_id - User ID
|
|
16
|
+
* @param email - User email address
|
|
17
|
+
* @returns JWT token string
|
|
18
|
+
*/
|
|
19
|
+
export declare function create_session_token(user_id: string, email: string): Promise<string>;
|
|
20
|
+
/**
|
|
21
|
+
* Validates a JWT session token
|
|
22
|
+
* Checks signature and expiration
|
|
23
|
+
* @param token - JWT token string
|
|
24
|
+
* @returns Validation result with user_id and email if valid
|
|
25
|
+
*/
|
|
26
|
+
export declare function validate_session_token(token: string): Promise<ValidateSessionTokenResult>;
|
|
27
|
+
//# sourceMappingURL=session_token_service.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session_token_service.d.ts","sourceRoot":"","sources":["../../../src/lib/services/session_token_service.ts"],"names":[],"mappings":"AAQA,MAAM,MAAM,mBAAmB,GAAG;IAChC,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC;CACb,CAAC;AAEF,MAAM,MAAM,0BAA0B,GAAG;IACvC,KAAK,EAAE,OAAO,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AAuCF;;;;;;GAMG;AACH,wBAAsB,oBAAoB,CACxC,OAAO,EAAE,MAAM,EACf,KAAK,EAAE,MAAM,GACZ,OAAO,CAAC,MAAM,CAAC,CA0CjB;AAED;;;;;GAKG;AACH,wBAAsB,sBAAsB,CAC1C,KAAK,EAAE,MAAM,GACZ,OAAO,CAAC,0BAA0B,CAAC,CAgDrC"}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
// file_description: service for creating and validating JWT session tokens for authentication
|
|
2
|
+
// Uses jose library for Edge-compatible JWT operations
|
|
3
|
+
// section: imports
|
|
4
|
+
import { SignJWT, jwtVerify } from "jose";
|
|
5
|
+
import { create_app_logger } from "../app_logger";
|
|
6
|
+
import { get_filename, get_line_number } from "../utils/api_route_helpers";
|
|
7
|
+
// section: helpers
|
|
8
|
+
/**
|
|
9
|
+
* Gets JWT secret from environment variables
|
|
10
|
+
* @returns JWT secret as Uint8Array for jose library
|
|
11
|
+
* @throws Error if JWT_SECRET is not set
|
|
12
|
+
*/
|
|
13
|
+
function get_jwt_secret() {
|
|
14
|
+
const jwt_secret = process.env.JWT_SECRET;
|
|
15
|
+
if (!jwt_secret) {
|
|
16
|
+
const logger = create_app_logger();
|
|
17
|
+
logger.error("session_token_jwt_secret_missing", {
|
|
18
|
+
filename: get_filename(),
|
|
19
|
+
line_number: get_line_number(),
|
|
20
|
+
error: "JWT_SECRET environment variable is required",
|
|
21
|
+
});
|
|
22
|
+
throw new Error("JWT_SECRET environment variable is required");
|
|
23
|
+
}
|
|
24
|
+
// Convert string secret to Uint8Array for jose
|
|
25
|
+
return new TextEncoder().encode(jwt_secret);
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Gets session token expiry in seconds (default: 30 days)
|
|
29
|
+
* @returns Number of seconds until token expires
|
|
30
|
+
*/
|
|
31
|
+
function get_session_token_expiry_seconds() {
|
|
32
|
+
// Default: 30 days = 30 * 24 * 60 * 60 = 2,592,000 seconds
|
|
33
|
+
const default_expiry_seconds = 60 * 60 * 24 * 30;
|
|
34
|
+
// Could be extended to read from config in the future
|
|
35
|
+
// For now, use default 30 days to match cookie expiry
|
|
36
|
+
return default_expiry_seconds;
|
|
37
|
+
}
|
|
38
|
+
// section: main_functions
|
|
39
|
+
/**
|
|
40
|
+
* Creates a JWT session token for a user
|
|
41
|
+
* Token includes user_id, email, issued at time, and expiration
|
|
42
|
+
* @param user_id - User ID
|
|
43
|
+
* @param email - User email address
|
|
44
|
+
* @returns JWT token string
|
|
45
|
+
*/
|
|
46
|
+
export async function create_session_token(user_id, email) {
|
|
47
|
+
const logger = create_app_logger();
|
|
48
|
+
try {
|
|
49
|
+
const secret = get_jwt_secret();
|
|
50
|
+
const now = Math.floor(Date.now() / 1000); // Current time in seconds
|
|
51
|
+
const expiry_seconds = get_session_token_expiry_seconds();
|
|
52
|
+
const exp = now + expiry_seconds;
|
|
53
|
+
const jwt = await new SignJWT({
|
|
54
|
+
user_id,
|
|
55
|
+
email,
|
|
56
|
+
})
|
|
57
|
+
.setProtectedHeader({ alg: "HS256" })
|
|
58
|
+
.setIssuedAt(now)
|
|
59
|
+
.setExpirationTime(exp)
|
|
60
|
+
.sign(secret);
|
|
61
|
+
logger.info("session_token_created", {
|
|
62
|
+
filename: get_filename(),
|
|
63
|
+
line_number: get_line_number(),
|
|
64
|
+
user_id,
|
|
65
|
+
email,
|
|
66
|
+
expires_in_seconds: expiry_seconds,
|
|
67
|
+
});
|
|
68
|
+
return jwt;
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
const error_message = error instanceof Error ? error.message : "Unknown error";
|
|
72
|
+
const error_stack = error instanceof Error ? error.stack : undefined;
|
|
73
|
+
logger.error("session_token_creation_failed", {
|
|
74
|
+
filename: get_filename(),
|
|
75
|
+
line_number: get_line_number(),
|
|
76
|
+
user_id,
|
|
77
|
+
email,
|
|
78
|
+
error_message,
|
|
79
|
+
error_stack,
|
|
80
|
+
});
|
|
81
|
+
throw new Error("Failed to create session token");
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Validates a JWT session token
|
|
86
|
+
* Checks signature and expiration
|
|
87
|
+
* @param token - JWT token string
|
|
88
|
+
* @returns Validation result with user_id and email if valid
|
|
89
|
+
*/
|
|
90
|
+
export async function validate_session_token(token) {
|
|
91
|
+
const logger = create_app_logger();
|
|
92
|
+
try {
|
|
93
|
+
const secret = get_jwt_secret();
|
|
94
|
+
const { payload } = await jwtVerify(token, secret, {
|
|
95
|
+
algorithms: ["HS256"],
|
|
96
|
+
});
|
|
97
|
+
// Extract user_id and email from payload
|
|
98
|
+
const user_id = payload.user_id;
|
|
99
|
+
const email = payload.email;
|
|
100
|
+
if (!user_id || !email) {
|
|
101
|
+
logger.warn("session_token_invalid_payload", {
|
|
102
|
+
filename: get_filename(),
|
|
103
|
+
line_number: get_line_number(),
|
|
104
|
+
error: "Token payload missing user_id or email",
|
|
105
|
+
});
|
|
106
|
+
return { valid: false };
|
|
107
|
+
}
|
|
108
|
+
logger.info("session_token_validated", {
|
|
109
|
+
filename: get_filename(),
|
|
110
|
+
line_number: get_line_number(),
|
|
111
|
+
user_id,
|
|
112
|
+
email,
|
|
113
|
+
});
|
|
114
|
+
return {
|
|
115
|
+
valid: true,
|
|
116
|
+
user_id,
|
|
117
|
+
email,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
catch (error) {
|
|
121
|
+
const error_message = error instanceof Error ? error.message : "Unknown error";
|
|
122
|
+
// jose throws JWTExpired, JWTInvalid, etc. - these are expected for invalid tokens
|
|
123
|
+
logger.debug("session_token_validation_failed", {
|
|
124
|
+
filename: get_filename(),
|
|
125
|
+
line_number: get_line_number(),
|
|
126
|
+
error_message,
|
|
127
|
+
});
|
|
128
|
+
return { valid: false };
|
|
129
|
+
}
|
|
130
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"middleware.d.ts","sourceRoot":"","sources":["../../src/server/middleware.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,uBAAuB,EAAE,MAAM,6CAA6C,CAAC;AACtF,YAAY,EAAE,2BAA2B,EAAE,MAAM,6CAA6C,CAAC"}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
// file_description: utility module for Edge-compatible proxy/middleware authentication
|
|
2
|
+
// This is a utility module, not a Next.js middleware/proxy file
|
|
3
|
+
// Exports functions for use in consuming apps' proxy.ts or middleware.ts files
|
|
4
|
+
// section: imports
|
|
5
|
+
export { validate_session_cookie } from "../lib/auth/session_token_validator.edge.js";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hazo_auth",
|
|
3
|
-
"version": "1.6.
|
|
3
|
+
"version": "1.6.7",
|
|
4
4
|
"main": "dist/index.js",
|
|
5
5
|
"types": "dist/index.d.ts",
|
|
6
6
|
"type": "module",
|
|
@@ -60,6 +60,10 @@
|
|
|
60
60
|
"types": "./dist/server/routes/index.d.ts",
|
|
61
61
|
"import": "./dist/server/routes/index.js"
|
|
62
62
|
},
|
|
63
|
+
"./server/middleware": {
|
|
64
|
+
"types": "./dist/server/middleware.d.ts",
|
|
65
|
+
"import": "./dist/server/middleware.js"
|
|
66
|
+
},
|
|
63
67
|
"./pages": {
|
|
64
68
|
"types": "./dist/page_components/index.d.ts",
|
|
65
69
|
"import": "./dist/page_components/index.js"
|
|
@@ -145,6 +149,7 @@
|
|
|
145
149
|
"hazo_notify": "^1.0.0",
|
|
146
150
|
"helmet": "^8.1.0",
|
|
147
151
|
"ini": "^6.0.0",
|
|
152
|
+
"jose": "^5.9.6",
|
|
148
153
|
"jsonwebtoken": "^9.0.2",
|
|
149
154
|
"lucide-react": "^0.553.0",
|
|
150
155
|
"mime-types": "^3.0.1",
|