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 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)
@@ -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;AASxD,wBAAsB,IAAI,CAAC,OAAO,EAAE,WAAW;;;;;;;;;IAwK9C"}
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;;;;;IA8E9C"}
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,qBAmNnC,CAAC"}
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 [values, setValues] = useState(buildInitialValues);
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
- const hasErrors = Object.keys(errors).length > 0;
40
- return hasEmptyField || hasErrors;
41
- }, [errors, showNameField, values, isSubmitting]);
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
- setValues(buildInitialValues());
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
- setValues(buildInitialValues());
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;AAkLlF;;;;;;;GAOG;AACH,wBAAsB,aAAa,CACjC,OAAO,EAAE,WAAW,EACpB,OAAO,CAAC,EAAE,eAAe,GACxB,OAAO,CAAC,cAAc,CAAC,CAuIzB"}
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
- const user_id = (_a = request.cookies.get("hazo_auth_user_id")) === null || _a === void 0 ? void 0 : _a.value;
156
- const user_email = (_b = request.cookies.get("hazo_auth_user_email")) === null || _b === void 0 ? void 0 : _b.value;
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,3 @@
1
+ export { validate_session_cookie } from "../lib/auth/session_token_validator.edge.js";
2
+ export type { ValidateSessionCookieResult } from "../lib/auth/session_token_validator.edge.js";
3
+ //# sourceMappingURL=middleware.d.ts.map
@@ -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.5",
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",