keystone-design-bootstrap 1.0.56 → 1.0.57

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "keystone-design-bootstrap",
3
- "version": "1.0.56",
3
+ "version": "1.0.57",
4
4
  "description": "Keystone Design Bootstrap - Sections, Elements, and Theme System for customer websites",
5
5
  "type": "module",
6
6
  "main": "./src/index.ts",
@@ -2,7 +2,6 @@
2
2
 
3
3
  import React, { useState, useMemo } from 'react';
4
4
  import { useRouter } from 'next/navigation';
5
- import { loginAction, signupAction, initiateAuthAction } from './actions';
6
5
  import { countries } from '../../utils/countries';
7
6
  import { getNationalMask, formatDigitsToMask } from '../../utils/phone-helpers';
8
7
 
@@ -67,16 +66,22 @@ export function LoginForm({ onSuccess, onClose }: LoginFormProps) {
67
66
  setError(null);
68
67
  setLoading(true);
69
68
  try {
70
- const result = await initiateAuthAction({ email: emailVal, phone: fullPhone });
69
+ const res = await fetch('/api/consumer/initiate', {
70
+ method: 'POST',
71
+ headers: { 'Content-Type': 'application/json' },
72
+ body: JSON.stringify({ email: emailVal, phone: fullPhone }),
73
+ });
74
+ const result = await res.json().catch(() => ({ exists: null }));
71
75
  if (result.exists === true) {
72
76
  setWelcomeName(result.firstName ?? null);
73
77
  setStep(result.hasPassword === false ? 'new' : 'returning');
74
78
  } else if (result.exists === false) {
75
79
  setStep('new');
76
80
  } else {
77
- // exists === null — network/server error. Stay on identifier step with a message.
78
81
  setError('Something went wrong. Please check your connection and try again.');
79
82
  }
83
+ } catch {
84
+ setError('Something went wrong. Please check your connection and try again.');
80
85
  } finally {
81
86
  setLoading(false);
82
87
  }
@@ -87,17 +92,24 @@ export function LoginForm({ onSuccess, onClose }: LoginFormProps) {
87
92
  setError(null);
88
93
  setLoading(true);
89
94
  try {
90
- const result = await loginAction({ email: emailVal, phone: fullPhone, password });
91
- if (result.error) {
95
+ const res = await fetch('/api/consumer/login', {
96
+ method: 'POST',
97
+ headers: { 'Content-Type': 'application/json' },
98
+ body: JSON.stringify({ email: emailVal, phone: fullPhone, password }),
99
+ });
100
+ const result = await res.json().catch(() => ({}));
101
+ if (!res.ok) {
92
102
  if (result.code === 'not_found' || result.code === 'no_password') {
93
103
  setStep('new');
94
104
  setError(null);
95
105
  return;
96
106
  }
97
- setError(result.error);
107
+ setError(result.error || 'Login failed. Please try again.');
98
108
  return;
99
109
  }
100
110
  if (onSuccess) onSuccess(); else router.refresh();
111
+ } catch {
112
+ setError('Something went wrong. Please check your connection and try again.');
101
113
  } finally {
102
114
  setLoading(false);
103
115
  }
@@ -109,16 +121,23 @@ export function LoginForm({ onSuccess, onClose }: LoginFormProps) {
109
121
  setError(null);
110
122
  setLoading(true);
111
123
  try {
112
- const result = await signupAction({
113
- email: emailVal,
114
- phone: fullPhone,
115
- password,
116
- password_confirmation: passwordConfirm,
117
- first_name: firstName.trim() || undefined,
118
- last_name: lastName.trim() || undefined,
124
+ const res = await fetch('/api/consumer/signup', {
125
+ method: 'POST',
126
+ headers: { 'Content-Type': 'application/json' },
127
+ body: JSON.stringify({
128
+ email: emailVal,
129
+ phone: fullPhone,
130
+ password,
131
+ password_confirmation: passwordConfirm,
132
+ first_name: firstName.trim() || undefined,
133
+ last_name: lastName.trim() || undefined,
134
+ }),
119
135
  });
120
- if (result.error) { setError(result.error); return; }
136
+ const result = await res.json().catch(() => ({}));
137
+ if (!res.ok) { setError(result.error || 'Signup failed. Please try again.'); return; }
121
138
  if (onSuccess) onSuccess(); else router.refresh();
139
+ } catch {
140
+ setError('Something went wrong. Please check your connection and try again.');
122
141
  } finally {
123
142
  setLoading(false);
124
143
  }
@@ -1,13 +1,12 @@
1
1
  'use client';
2
2
 
3
3
  import { useRouter } from 'next/navigation';
4
- import { logoutAction } from './actions';
5
4
 
6
5
  export function LogoutButton() {
7
6
  const router = useRouter();
8
7
 
9
8
  const handleLogout = async () => {
10
- await logoutAction();
9
+ await fetch('/api/consumer/logout', { method: 'POST' });
11
10
  router.refresh();
12
11
  };
13
12
 
@@ -2,7 +2,6 @@
2
2
 
3
3
  import React, { useState, useTransition, useRef, useEffect } from 'react';
4
4
  import { useRouter } from 'next/navigation';
5
- import { sendMessageAction } from './actions';
6
5
 
7
6
  export function MessageComposer({ contactId }: { contactId: number }) {
8
7
  const [body, setBody] = useState('');
@@ -23,12 +22,21 @@ export function MessageComposer({ contactId }: { contactId: number }) {
23
22
  if (!body.trim() || isPending) return;
24
23
  setError(null);
25
24
  startTransition(async () => {
26
- const result = await sendMessageAction({ contactId, body: body.trim() });
27
- if (result.error) {
28
- setError(result.error);
29
- } else {
30
- setBody('');
31
- router.refresh();
25
+ try {
26
+ const res = await fetch('/api/chat', {
27
+ method: 'POST',
28
+ headers: { 'Content-Type': 'application/json' },
29
+ body: JSON.stringify({ contact_id: contactId, body: body.trim() }),
30
+ });
31
+ const result = await res.json().catch(() => ({}));
32
+ if (!res.ok) {
33
+ setError(result.error || 'Failed to send message.');
34
+ } else {
35
+ setBody('');
36
+ router.refresh();
37
+ }
38
+ } catch {
39
+ setError('Failed to send message.');
32
40
  }
33
41
  });
34
42
  }
@@ -1,19 +1,23 @@
1
1
  /**
2
- * Factory functions for consumer auth Next.js API route handlers.
2
+ * Consumer auth API route handlers for Next.js App Router.
3
3
  *
4
- * Usage in customer site:
5
- * // app/api/consumer/login/route.ts
4
+ * Usage in a customer site:
5
+ * // app/api/consumer/[action]/route.ts
6
6
  * import { NextResponse } from 'next/server';
7
- * import { createConsumerLoginHandler } from 'keystone-design-bootstrap/next/routes/consumer-auth';
8
- * export const { POST } = createConsumerLoginHandler({ NextResponse });
7
+ * import { createConsumerAuthHandlers } from 'keystone-design-bootstrap/next/routes/consumer-auth';
8
+ * export const { POST } = createConsumerAuthHandlers({ NextResponse });
9
+ *
10
+ * Handles: initiate, login, signup, logout
11
+ *
12
+ * Env (server-side only):
13
+ * - API_URL (default: http://localhost:3000/api/v1)
14
+ * - API_KEY
9
15
  */
10
16
 
11
17
  import { CONSUMER_TOKEN_COOKIE } from '../../lib/consumer-session';
12
18
 
13
- interface NextResponseLike {
14
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
15
- json: (body: unknown, init?: ResponseInit) => any;
16
- }
19
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
20
+ type NextResponseLike = { json: (body: unknown, init?: ResponseInit) => any };
17
21
 
18
22
  const COOKIE_MAX_AGE = 60 * 60 * 24 * 30; // 30 days
19
23
 
@@ -21,93 +25,156 @@ function getApiUrl(): string {
21
25
  return process.env.API_URL || 'http://localhost:3000/api/v1';
22
26
  }
23
27
 
24
- function setCookieOnResponse(response: Response, token: string): void {
25
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
26
- (response as any).cookies.set(CONSUMER_TOKEN_COOKIE, token, {
28
+ function getApiKey(): string {
29
+ return process.env.API_KEY || '';
30
+ }
31
+
32
+ function apiHeaders(): Record<string, string> {
33
+ const key = getApiKey();
34
+ return {
35
+ 'Content-Type': 'application/json',
36
+ ...(key ? { 'X-API-Key': key } : {}),
37
+ };
38
+ }
39
+
40
+ // POST /api/consumer/initiate
41
+ // Looks up an existing user by email or phone, creating a Contact if needed.
42
+ // Returns { exists, firstName, hasPassword }.
43
+ async function handleInitiate(request: Request, NR: NextResponseLike): Promise<Response> {
44
+ const body = await request.json().catch(() => ({})) as Record<string, unknown>;
45
+ const { email, phone } = body;
46
+
47
+ if (!email && !phone) {
48
+ return NR.json({ exists: null });
49
+ }
50
+
51
+ try {
52
+ const res = await fetch(`${getApiUrl()}/consumer/auth/initiate`, {
53
+ method: 'POST',
54
+ headers: apiHeaders(),
55
+ body: JSON.stringify({ email: email || undefined, phone: phone || undefined }),
56
+ });
57
+
58
+ if (res.status === 404) return NR.json({ exists: false });
59
+ if (!res.ok) return NR.json({ exists: null });
60
+
61
+ const json = await res.json().catch(() => ({}));
62
+ const data = json.data as Record<string, unknown> | undefined;
63
+ return NR.json({
64
+ exists: true,
65
+ firstName: data?.first_name ?? undefined,
66
+ hasPassword: data?.has_password != null ? Boolean(data.has_password) : true,
67
+ });
68
+ } catch {
69
+ return NR.json({ exists: null });
70
+ }
71
+ }
72
+
73
+ // POST /api/consumer/login
74
+ // Authenticates an existing user. Sets HttpOnly JWT cookie on success.
75
+ // Returns {} on success or { error, code } on failure.
76
+ async function handleLogin(request: Request, NR: NextResponseLike): Promise<Response> {
77
+ const body = await request.json().catch(() => ({})) as Record<string, string>;
78
+ const { email, phone, password } = body;
79
+
80
+ if (!email && !phone) return NR.json({ error: 'Email or phone is required.' }, { status: 422 });
81
+ if (!password) return NR.json({ error: 'Password is required.' }, { status: 422 });
82
+
83
+ const res = await fetch(`${getApiUrl()}/consumer/auth/login`, {
84
+ method: 'POST',
85
+ headers: apiHeaders(),
86
+ body: JSON.stringify({ email: email || undefined, phone: phone || undefined, password }),
87
+ });
88
+
89
+ const json = await res.json().catch(() => ({}));
90
+ if (!res.ok) {
91
+ return NR.json(
92
+ { error: json.error || 'Login failed. Please try again.', code: json.code },
93
+ { status: res.status }
94
+ );
95
+ }
96
+
97
+ const token = json.data?.token;
98
+ if (!token) return NR.json({ error: 'No token received.' }, { status: 500 });
99
+
100
+ const response = NR.json({});
101
+ response.cookies.set(CONSUMER_TOKEN_COOKIE, token, {
27
102
  httpOnly: true,
28
103
  secure: process.env.NODE_ENV === 'production',
29
104
  sameSite: 'lax',
30
105
  path: '/',
31
106
  maxAge: COOKIE_MAX_AGE,
32
107
  });
108
+ return response;
33
109
  }
34
110
 
35
- export function createConsumerLoginHandler({ NextResponse }: { NextResponse: NextResponseLike }) {
36
- return {
37
- async POST(request: Request) {
38
- const body = await request.json().catch(() => ({})) as Record<string, string>;
39
- const { email, phone, password } = body;
40
-
41
- if (!email && !phone) {
42
- return NextResponse.json({ error: 'Email or phone is required' }, { status: 422 });
43
- }
44
- if (!password) {
45
- return NextResponse.json({ error: 'Password is required' }, { status: 422 });
46
- }
47
-
48
- const apiRes = await fetch(`${getApiUrl()}/consumer/auth/login`, {
49
- method: 'POST',
50
- headers: { 'Content-Type': 'application/json' },
51
- body: JSON.stringify({ email: email || undefined, phone: phone || undefined, password }),
52
- });
53
-
54
- const json = await apiRes.json().catch(() => ({}));
55
- if (!apiRes.ok) {
56
- return NextResponse.json({ error: json.error || 'Login failed' }, { status: apiRes.status });
57
- }
58
-
59
- const token = json.data?.token;
60
- if (!token) {
61
- return NextResponse.json({ error: 'No token received' }, { status: 500 });
62
- }
63
-
64
- const response = NextResponse.json({ success: true });
65
- setCookieOnResponse(response, token);
66
- return response;
67
- },
68
- };
111
+ // POST /api/consumer/signup
112
+ // Creates a new account (or claims an existing contact). Sets HttpOnly JWT cookie on success.
113
+ // Returns {} on success or { error } on failure.
114
+ async function handleSignup(request: Request, NR: NextResponseLike): Promise<Response> {
115
+ const body = await request.json().catch(() => ({})) as Record<string, string>;
116
+ const { email, phone, password, password_confirmation, first_name, last_name } = body;
117
+
118
+ if (!email && !phone) return NR.json({ error: 'Email or phone is required.' }, { status: 422 });
119
+
120
+ const res = await fetch(`${getApiUrl()}/consumer/auth/signup`, {
121
+ method: 'POST',
122
+ headers: apiHeaders(),
123
+ body: JSON.stringify({
124
+ email: email || undefined,
125
+ phone: phone || undefined,
126
+ password,
127
+ password_confirmation,
128
+ first_name: first_name || undefined,
129
+ last_name: last_name || undefined,
130
+ }),
131
+ });
132
+
133
+ const json = await res.json().catch(() => ({}));
134
+ if (!res.ok) {
135
+ return NR.json({ error: json.error || 'Signup failed. Please try again.' }, { status: res.status });
136
+ }
137
+
138
+ const token = json.data?.token;
139
+ if (!token) return NR.json({ error: 'No token received.' }, { status: 500 });
140
+
141
+ const response = NR.json({ claimed: json.data?.claimed ?? false });
142
+ response.cookies.set(CONSUMER_TOKEN_COOKIE, token, {
143
+ httpOnly: true,
144
+ secure: process.env.NODE_ENV === 'production',
145
+ sameSite: 'lax',
146
+ path: '/',
147
+ maxAge: COOKIE_MAX_AGE,
148
+ });
149
+ return response;
69
150
  }
70
151
 
71
- export function createConsumerSignupHandler({ NextResponse }: { NextResponse: NextResponseLike }) {
72
- return {
73
- async POST(request: Request) {
74
- const body = await request.json().catch(() => ({})) as Record<string, string>;
75
- const { email, phone, password, password_confirmation } = body;
76
-
77
- if (!email && !phone) {
78
- return NextResponse.json({ error: 'Email or phone is required' }, { status: 422 });
79
- }
80
-
81
- const apiRes = await fetch(`${getApiUrl()}/consumer/auth/signup`, {
82
- method: 'POST',
83
- headers: { 'Content-Type': 'application/json' },
84
- body: JSON.stringify({ email: email || undefined, phone: phone || undefined, password, password_confirmation }),
85
- });
86
-
87
- const json = await apiRes.json().catch(() => ({}));
88
- if (!apiRes.ok) {
89
- return NextResponse.json({ error: json.error || 'Signup failed' }, { status: apiRes.status });
90
- }
91
-
92
- const token = json.data?.token;
93
- if (!token) {
94
- return NextResponse.json({ error: 'No token received' }, { status: 500 });
95
- }
96
-
97
- const response = NextResponse.json({ success: true, claimed: json.data?.claimed ?? false });
98
- setCookieOnResponse(response, token);
99
- return response;
100
- },
101
- };
152
+ // POST /api/consumer/logout
153
+ // Clears the JWT cookie.
154
+ async function handleLogout(_request: Request, NR: NextResponseLike): Promise<Response> {
155
+ const response = NR.json({ success: true });
156
+ response.cookies.delete(CONSUMER_TOKEN_COOKIE);
157
+ return response;
102
158
  }
103
159
 
104
- export function createConsumerLogoutHandler({ NextResponse }: { NextResponse: NextResponseLike }) {
160
+ /**
161
+ * Creates a single POST handler that routes to the correct auth action based on
162
+ * the dynamic `[action]` path segment.
163
+ *
164
+ * Compatible with Next.js 14 (sync params) and 15 (async params).
165
+ */
166
+ export function createConsumerAuthHandlers({ NextResponse }: { NextResponse: NextResponseLike }) {
105
167
  return {
106
- async POST() {
107
- const response = NextResponse.json({ success: true });
108
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
109
- (response as any).cookies.delete(CONSUMER_TOKEN_COOKIE);
110
- return response;
168
+ POST: async (
169
+ request: Request,
170
+ context: { params: Promise<{ action: string }> | { action: string } }
171
+ ): Promise<Response> => {
172
+ const { action } = await Promise.resolve(context.params);
173
+ if (action === 'initiate') return handleInitiate(request, NextResponse);
174
+ if (action === 'login') return handleLogin(request, NextResponse);
175
+ if (action === 'signup') return handleSignup(request, NextResponse);
176
+ if (action === 'logout') return handleLogout(request, NextResponse);
177
+ return NextResponse.json({ error: 'Not found' }, { status: 404 });
111
178
  },
112
179
  };
113
180
  }
@@ -1,160 +0,0 @@
1
- 'use server';
2
-
3
- import { cookies } from 'next/headers';
4
- import { CONSUMER_TOKEN_COOKIE } from '../../lib/consumer-session';
5
-
6
- const COOKIE_MAX_AGE = 60 * 60 * 24 * 30; // 30 days
7
-
8
- function getApiUrl(): string {
9
- return process.env.API_URL || 'http://localhost:3000/api/v1';
10
- }
11
-
12
- function getApiKey(): string {
13
- return process.env.API_KEY || '';
14
- }
15
-
16
- async function setConsumerCookie(token: string): Promise<void> {
17
- const cookieStore = await cookies();
18
- cookieStore.set(CONSUMER_TOKEN_COOKIE, token, {
19
- httpOnly: true,
20
- secure: process.env.NODE_ENV === 'production',
21
- sameSite: 'lax',
22
- path: '/',
23
- maxAge: COOKIE_MAX_AGE,
24
- });
25
- }
26
-
27
- export async function loginAction(payload: {
28
- email: string | null;
29
- phone: string | null;
30
- password: string;
31
- }): Promise<{ error?: string; code?: string }> {
32
- const { email, phone, password } = payload;
33
-
34
- if (!email && !phone) return { error: 'Email or phone is required.' };
35
- if (!password) return { error: 'Password is required.' };
36
-
37
- const apiKey = getApiKey();
38
- const res = await fetch(`${getApiUrl()}/consumer/auth/login`, {
39
- method: 'POST',
40
- headers: {
41
- 'Content-Type': 'application/json',
42
- ...(apiKey ? { 'X-API-Key': apiKey } : {}),
43
- },
44
- body: JSON.stringify({ email: email || undefined, phone: phone || undefined, password }),
45
- });
46
-
47
- const rawBody = await res.text();
48
- let json: Record<string, unknown> = {};
49
- try { json = JSON.parse(rawBody); } catch { /* ignore */ }
50
- if (!res.ok) return { error: (json as { error?: string }).error || 'Login failed. Please try again.', code: (json as { code?: string }).code };
51
-
52
- const token = (json.data as Record<string, unknown> | undefined)?.token as string | undefined;
53
- if (!token) return { error: 'No token received from server.' };
54
-
55
- await setConsumerCookie(token);
56
- return {};
57
- }
58
-
59
- export async function initiateAuthAction(payload: {
60
- email: string | null;
61
- phone: string | null;
62
- }): Promise<{ exists: boolean | null; firstName?: string; hasPassword?: boolean }> {
63
- const { email, phone } = payload;
64
- if (!email && !phone) return { exists: null };
65
- try {
66
- const apiKey = getApiKey();
67
- const res = await fetch(`${getApiUrl()}/consumer/auth/initiate`, {
68
- method: 'POST',
69
- headers: {
70
- 'Content-Type': 'application/json',
71
- ...(apiKey ? { 'X-API-Key': apiKey } : {}),
72
- },
73
- body: JSON.stringify({ email: email || undefined, phone: phone || undefined }),
74
- });
75
- const rawBody = await res.text();
76
- if (res.status === 404) return { exists: false };
77
- if (!res.ok) return { exists: null };
78
- let json: Record<string, unknown> = {};
79
- try { json = JSON.parse(rawBody); } catch { /* ignore */ }
80
- const data = json.data as Record<string, unknown> | undefined;
81
- return { exists: true, firstName: data?.first_name as string | undefined, hasPassword: data?.has_password != null ? Boolean(data.has_password) : true };
82
- } catch {
83
- return { exists: null };
84
- }
85
- }
86
-
87
- export async function signupAction(payload: {
88
- email: string | null;
89
- phone: string | null;
90
- password: string;
91
- password_confirmation: string;
92
- first_name?: string;
93
- last_name?: string;
94
- }): Promise<{ error?: string; claimed?: boolean }> {
95
- const { email, phone, password, password_confirmation, first_name, last_name } = payload;
96
-
97
- if (!email && !phone) return { error: 'Email or phone is required.' };
98
-
99
- const apiKey = getApiKey();
100
- const res = await fetch(`${getApiUrl()}/consumer/auth/signup`, {
101
- method: 'POST',
102
- headers: {
103
- 'Content-Type': 'application/json',
104
- ...(apiKey ? { 'X-API-Key': apiKey } : {}),
105
- },
106
- body: JSON.stringify({
107
- email: email || undefined,
108
- phone: phone || undefined,
109
- password,
110
- password_confirmation,
111
- first_name: first_name || undefined,
112
- last_name: last_name || undefined,
113
- }),
114
- });
115
-
116
- const rawBody = await res.text();
117
- let json: Record<string, unknown> = {};
118
- try { json = JSON.parse(rawBody); } catch { /* ignore */ }
119
- if (!res.ok) return { error: (json as { error?: string }).error || 'Signup failed. Please try again.' };
120
-
121
- const token = (json.data as Record<string, unknown> | undefined)?.token as string | undefined;
122
- if (!token) return { error: 'No token received from server.' };
123
-
124
- await setConsumerCookie(token);
125
- return { claimed: (json.data as Record<string, unknown> | undefined)?.claimed as boolean ?? false };
126
- }
127
-
128
- export async function sendMessageAction(payload: {
129
- contactId: number;
130
- body: string;
131
- }): Promise<{ error?: string }> {
132
- const { contactId, body } = payload;
133
- if (!body.trim()) return { error: 'Message cannot be empty.' };
134
-
135
- const apiKey = getApiKey();
136
- if (!apiKey) return { error: 'Service not configured.' };
137
-
138
- try {
139
- const res = await fetch(`${getApiUrl()}/public/messages`, {
140
- method: 'POST',
141
- headers: {
142
- 'Content-Type': 'application/json',
143
- 'X-API-Key': apiKey,
144
- },
145
- body: JSON.stringify({ contact_id: contactId, body }),
146
- });
147
- if (!res.ok) {
148
- const json = await res.json().catch(() => ({}));
149
- return { error: json.error || 'Failed to send message.' };
150
- }
151
- return {};
152
- } catch {
153
- return { error: 'Failed to send message.' };
154
- }
155
- }
156
-
157
- export async function logoutAction(): Promise<void> {
158
- const cookieStore = await cookies();
159
- cookieStore.delete(CONSUMER_TOKEN_COOKIE);
160
- }