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
|
@@ -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
|
|
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
|
|
91
|
-
|
|
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
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
*
|
|
2
|
+
* Consumer auth API route handlers for Next.js App Router.
|
|
3
3
|
*
|
|
4
|
-
* Usage in customer site:
|
|
5
|
-
* // app/api/consumer/
|
|
4
|
+
* Usage in a customer site:
|
|
5
|
+
* // app/api/consumer/[action]/route.ts
|
|
6
6
|
* import { NextResponse } from 'next/server';
|
|
7
|
-
* import {
|
|
8
|
-
* export const { POST } =
|
|
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
|
-
|
|
14
|
-
|
|
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
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
-
|
|
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
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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
|
-
}
|