@revenexx/cover 0.1.0 → 0.1.2
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/app/app.config.ts +1 -1
- package/app/components/account/AccountSidebar.vue +10 -1
- package/app/components/account/address/AccountAddressCard.vue +69 -88
- package/app/components/auth/AuthRegisterPanel.vue +122 -194
- package/app/formkit.config.ts +21 -0
- package/app/interfaces/auth.ts +0 -2
- package/app/interfaces/validation.ts +1 -1
- package/app/validations/formValidationConfig.ts +0 -30
- package/nuxt.config.ts +7 -3
- package/package.json +2 -2
- package/server/api/account/orders.get.ts +6 -5
- package/server/api/account/profile.get.ts +3 -3
- package/server/api/account/profile.put.ts +6 -4
- package/server/api/auth/login.post.ts +2 -5
- package/server/api/auth/recovery.post.ts +5 -13
- package/server/api/auth/recovery.put.ts +5 -14
- package/server/api/auth/register.post.ts +21 -44
- package/server/api/orders/index.post.ts +28 -24
- package/server/api/payment/methods.post.ts +5 -4
- package/server/api/shipping/rates.post.ts +6 -4
- package/server/interfaces/auth.ts +1 -1
- package/server/services/ApiAccountService.ts +44 -0
- package/server/services/ApiAuthService.ts +15 -14
- package/server/services/ApiCartService.ts +45 -27
- package/server/services/ApiMarketService.ts +11 -9
- package/server/utils/accountService.ts +4 -5
- package/server/utils/authService.ts +0 -3
- package/server/utils/liveCatalog.ts +30 -17
- package/server/utils/liveInventories.ts +4 -4
- package/server/utils/liveOrders.ts +5 -3
- package/server/utils/livePrices.ts +10 -9
- package/server/utils/revenexxSdk.ts +213 -0
- package/app/validations/companyName.ts +0 -18
- package/app/validations/maxLength.ts +0 -12
- package/app/validations/optionalCompanyName.ts +0 -23
- package/app/validations/phoneNumber.ts +0 -19
- package/app/validations/termsRequired.ts +0 -4
- package/app/validations/zipCode.ts +0 -15
- package/server/services/SdkAccountService.ts +0 -56
- package/server/services/SdkAuthService.ts +0 -83
- package/server/utils/revenexxApi.ts +0 -136
- package/server/utils/shopSdk.ts +0 -88
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Carts,
|
|
3
|
+
Client,
|
|
4
|
+
Customers,
|
|
5
|
+
Inventories,
|
|
6
|
+
Markets,
|
|
7
|
+
Orders,
|
|
8
|
+
Payments,
|
|
9
|
+
Prices,
|
|
10
|
+
Products,
|
|
11
|
+
RevenexxException,
|
|
12
|
+
Search,
|
|
13
|
+
Shipping,
|
|
14
|
+
} from "@revenexx/sdk";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* The generated revenexx web SDK, configured for the BFF. Every live
|
|
18
|
+
* "api"-keyed implementation goes through these service instances.
|
|
19
|
+
*
|
|
20
|
+
* Auth model: tenant API key (server-side only, never reaches the client).
|
|
21
|
+
* Configure via runtime config / env:
|
|
22
|
+
* NUXT_REVENEXX_API_URL (default https://api.revenexx.com)
|
|
23
|
+
* NUXT_REVENEXX_TENANT (tenant slug, e.g. "revenexx")
|
|
24
|
+
* NUXT_REVENEXX_API_KEY (rvxk_… gateway key)
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
// Re-exported so route handlers can narrow SDK failures without importing
|
|
28
|
+
// from the package themselves (Nitro auto-imports this util's exports).
|
|
29
|
+
export { RevenexxException };
|
|
30
|
+
|
|
31
|
+
export interface RevenexxSdkCallOptions {
|
|
32
|
+
readonly query?: Record<string, string | number | boolean | undefined>;
|
|
33
|
+
readonly body?: unknown;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface RevenexxSdk {
|
|
37
|
+
readonly client: Client;
|
|
38
|
+
readonly carts: Carts;
|
|
39
|
+
readonly customers: Customers;
|
|
40
|
+
readonly inventories: Inventories;
|
|
41
|
+
readonly markets: Markets;
|
|
42
|
+
readonly orders: Orders;
|
|
43
|
+
readonly payments: Payments;
|
|
44
|
+
readonly prices: Prices;
|
|
45
|
+
readonly products: Products;
|
|
46
|
+
readonly search: Search;
|
|
47
|
+
readonly shipping: Shipping;
|
|
48
|
+
/**
|
|
49
|
+
* Low-level escape hatch over the SDK transport (same auth, same error
|
|
50
|
+
* type) for the few operations whose gateway contracts are not complete
|
|
51
|
+
* yet — list query parameters and auth/me's session check. Each call
|
|
52
|
+
* site documents the missing contract piece; once the apps declare it
|
|
53
|
+
* and the SDK regenerates, the call moves to the typed method.
|
|
54
|
+
*/
|
|
55
|
+
call<T>(method: "GET" | "POST" | "PUT" | "DELETE", path: string, options?: RevenexxSdkCallOptions): Promise<T>;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Expected user-facing failures (wrong credentials, duplicate email, …) are
|
|
60
|
+
* forwarded to the client as-is; everything else is an infrastructure error.
|
|
61
|
+
*/
|
|
62
|
+
const API_USER_ERROR_STATUS = new Set([400, 401, 404, 409, 429]);
|
|
63
|
+
|
|
64
|
+
export function isApiUserError(err: unknown): err is RevenexxException {
|
|
65
|
+
return err instanceof RevenexxException && API_USER_ERROR_STATUS.has(err.code);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* The customers app's auth passthrough answers with a generic error envelope
|
|
70
|
+
* (no machine type). Map the HTTP status onto the platform error types the
|
|
71
|
+
* client-side auth store already translates, so mock/api modes produce
|
|
72
|
+
* identical UX for the standard failures.
|
|
73
|
+
*/
|
|
74
|
+
export function apiAuthErrorType(err: RevenexxException): string {
|
|
75
|
+
switch (err.code) {
|
|
76
|
+
case 401: return "user_invalid_credentials";
|
|
77
|
+
case 404: return "user_not_found";
|
|
78
|
+
case 409: return "user_email_already_exists";
|
|
79
|
+
case 429: return "general_rate_limit_exceeded";
|
|
80
|
+
default: return err.type || "api_error";
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Diagnostic fields for structured logs — never includes the API key. */
|
|
85
|
+
export function apiErrorContext(err: unknown): Record<string, unknown> {
|
|
86
|
+
if (err instanceof RevenexxException) {
|
|
87
|
+
return { apiStatus: err.code, apiType: err.type, apiMessage: err.message };
|
|
88
|
+
}
|
|
89
|
+
return { errorMessage: err instanceof Error ? err.message : String(err) };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
let sdk: RevenexxSdk | null = null;
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* The platform's per-invocation tenant context (ADR-0057 §8): on a deployed
|
|
96
|
+
* Site the entrypoint resolves the tenant from the request Host and injects
|
|
97
|
+
* x-revenexx-tenant the tenant slug
|
|
98
|
+
* x-revenexx-context the brokered per-tenant JWT for gateway calls
|
|
99
|
+
* When present, they win over the env credentials — the same theme build
|
|
100
|
+
* serves any tenant. Resolution uses Nitro's async context (no event
|
|
101
|
+
* threading); outside a request, or without the headers, the env-configured
|
|
102
|
+
* tenant API key applies (local dev, the demo shop, previews).
|
|
103
|
+
*/
|
|
104
|
+
function brokeredContext(): { tenant: string; jwt: string } | null {
|
|
105
|
+
try {
|
|
106
|
+
const event = useEvent();
|
|
107
|
+
const tenant = getRequestHeader(event, "x-revenexx-tenant");
|
|
108
|
+
if (!tenant) {
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
return { tenant, jwt: getRequestHeader(event, "x-revenexx-context") ?? "" };
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
// No async context (e.g. startup code) — fall back to env credentials.
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* The tenant slug the current request's SDK calls are scoped to — the cache
|
|
121
|
+
* key for any server-side cache of gateway data (markets, categories, …).
|
|
122
|
+
* Module-level caches MUST be partitioned by this, or a multi-tenant theme
|
|
123
|
+
* leaks one tenant's data into another's pages.
|
|
124
|
+
*/
|
|
125
|
+
export function revenexxTenantKey(): string {
|
|
126
|
+
const brokered = brokeredContext();
|
|
127
|
+
if (brokered) {
|
|
128
|
+
return brokered.tenant;
|
|
129
|
+
}
|
|
130
|
+
const config = useRuntimeConfig();
|
|
131
|
+
return String(config.revenexxTenant || "");
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function useRevenexxSdk(): RevenexxSdk {
|
|
135
|
+
const brokered = brokeredContext();
|
|
136
|
+
if (!brokered && sdk) {
|
|
137
|
+
return sdk;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const config = useRuntimeConfig();
|
|
141
|
+
const endpoint = String(config.revenexxApiUrl || "https://api.revenexx.com").replace(/\/+$/, "");
|
|
142
|
+
const tenant = brokered?.tenant ?? String(config.revenexxTenant || "");
|
|
143
|
+
const apiKey = String(config.revenexxApiKey || "");
|
|
144
|
+
|
|
145
|
+
if (!tenant || (!brokered?.jwt && !apiKey)) {
|
|
146
|
+
throw new RevenexxException(
|
|
147
|
+
"revenexx API credentials are not configured (NUXT_REVENEXX_TENANT / NUXT_REVENEXX_API_KEY)",
|
|
148
|
+
503,
|
|
149
|
+
"api_not_configured",
|
|
150
|
+
"",
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const client = new Client()
|
|
155
|
+
.setEndpoint(endpoint)
|
|
156
|
+
.setTenant(tenant);
|
|
157
|
+
if (brokered?.jwt) {
|
|
158
|
+
client.setBearerAuth(brokered.jwt);
|
|
159
|
+
}
|
|
160
|
+
else {
|
|
161
|
+
client.setApiKeyAuth(apiKey);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async function call<T>(
|
|
165
|
+
method: "GET" | "POST" | "PUT" | "DELETE",
|
|
166
|
+
path: string,
|
|
167
|
+
options: RevenexxSdkCallOptions = {},
|
|
168
|
+
): Promise<T> {
|
|
169
|
+
const url = new URL(endpoint + path);
|
|
170
|
+
const query: Record<string, string> = {};
|
|
171
|
+
for (const [key, value] of Object.entries(options.query ?? {})) {
|
|
172
|
+
if (value !== undefined) {
|
|
173
|
+
query[key] = String(value);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (method === "GET") {
|
|
178
|
+
// client.call appends flattened params to the URL for GET requests
|
|
179
|
+
return await client.call("get", url, {}, query) as T;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
for (const [key, value] of Object.entries(query)) {
|
|
183
|
+
url.searchParams.set(key, value);
|
|
184
|
+
}
|
|
185
|
+
return await client.call(
|
|
186
|
+
method.toLowerCase(),
|
|
187
|
+
url,
|
|
188
|
+
{ "content-type": "application/json" },
|
|
189
|
+
(options.body ?? {}) as never,
|
|
190
|
+
) as T;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const instance: RevenexxSdk = {
|
|
194
|
+
client,
|
|
195
|
+
carts: new Carts(client),
|
|
196
|
+
customers: new Customers(client),
|
|
197
|
+
inventories: new Inventories(client),
|
|
198
|
+
markets: new Markets(client),
|
|
199
|
+
orders: new Orders(client),
|
|
200
|
+
payments: new Payments(client),
|
|
201
|
+
prices: new Prices(client),
|
|
202
|
+
products: new Products(client),
|
|
203
|
+
search: new Search(client),
|
|
204
|
+
shipping: new Shipping(client),
|
|
205
|
+
call,
|
|
206
|
+
};
|
|
207
|
+
// Brokered instances are request-scoped (the JWT rotates per invocation)
|
|
208
|
+
// — only the env-credential client is a process-wide singleton.
|
|
209
|
+
if (!brokered) {
|
|
210
|
+
sdk = instance;
|
|
211
|
+
}
|
|
212
|
+
return instance;
|
|
213
|
+
}
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
import type { SimpleValidator } from "./types";
|
|
2
|
-
|
|
3
|
-
// checks if the input is a valid company name (basic validation)
|
|
4
|
-
export const companyName: SimpleValidator = (value) => {
|
|
5
|
-
const companyNameRegex = /^[a-zA-Z0-9äöüÄÖÜß\s.,'-]+$/;
|
|
6
|
-
|
|
7
|
-
if (typeof value !== "string") {
|
|
8
|
-
return null; // Not a string, let other validators handle this
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
const trimmedValue = value.trim();
|
|
12
|
-
|
|
13
|
-
if (!companyNameRegex.test(trimmedValue)) {
|
|
14
|
-
return "invalidCompanyName"; // Invalid company name format
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
return null; // Valid company name
|
|
18
|
-
};
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
import type { SimpleValidator } from "./types";
|
|
2
|
-
|
|
3
|
-
export const maxLength: SimpleValidator = (value, options) => {
|
|
4
|
-
const max = (options?.max as number) ?? Infinity;
|
|
5
|
-
if (typeof value !== "string") {
|
|
6
|
-
return null;
|
|
7
|
-
}
|
|
8
|
-
if (value.trim().length > max) {
|
|
9
|
-
return "maxLength";
|
|
10
|
-
}
|
|
11
|
-
return null;
|
|
12
|
-
};
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
import type { SimpleValidator } from "./types";
|
|
2
|
-
|
|
3
|
-
// Validates company name only when a value is present — field is optional.
|
|
4
|
-
// Options { min, max } are forwarded to minLength/maxLength translation keys.
|
|
5
|
-
export const optionalCompanyName: SimpleValidator = (value) => {
|
|
6
|
-
if (typeof value !== "string" || value.trim() === "") {
|
|
7
|
-
return null;
|
|
8
|
-
}
|
|
9
|
-
const trimmed = value.trim();
|
|
10
|
-
const min = 3;
|
|
11
|
-
const max = 64;
|
|
12
|
-
if (trimmed.length < min) {
|
|
13
|
-
return "minLength";
|
|
14
|
-
}
|
|
15
|
-
if (trimmed.length > max) {
|
|
16
|
-
return "maxLength";
|
|
17
|
-
}
|
|
18
|
-
const companyNameRegex = /^[a-zA-Z0-9äöüÄÖÜß\s.,'-]+$/;
|
|
19
|
-
if (!companyNameRegex.test(trimmed)) {
|
|
20
|
-
return "invalidCompanyName";
|
|
21
|
-
}
|
|
22
|
-
return null;
|
|
23
|
-
};
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import type { SimpleValidator } from "./types";
|
|
2
|
-
|
|
3
|
-
// checks if the input is a valid phone number (basic validation)
|
|
4
|
-
export const phoneNumber: SimpleValidator = (value) => {
|
|
5
|
-
if (typeof value !== "string") {
|
|
6
|
-
return null; // Not a string, let other validators handle this
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
const trimmedValue = value.trim();
|
|
10
|
-
|
|
11
|
-
// Basic regex for phone numbers (allows digits, spaces, dashes, parentheses, and plus sign)
|
|
12
|
-
const phoneRegex = /^[+\d]?(?:[\d\s-().]*)$/;
|
|
13
|
-
|
|
14
|
-
if (!phoneRegex.test(trimmedValue)) {
|
|
15
|
-
return "invalidPhoneNumber"; // Invalid phone number format
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
return null; // Valid phone number
|
|
19
|
-
};
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
import type { SimpleValidator } from "./types";
|
|
2
|
-
|
|
3
|
-
export const zipCode: SimpleValidator = (value) => {
|
|
4
|
-
if (typeof value !== "string") {
|
|
5
|
-
return "invalidZipCode";
|
|
6
|
-
}
|
|
7
|
-
const trimmed = value.trim();
|
|
8
|
-
if (!/^\d{3,5}$/.test(trimmed)) {
|
|
9
|
-
return "invalidZipCode";
|
|
10
|
-
}
|
|
11
|
-
if (/^0+$/.test(trimmed) || trimmed === "99999") {
|
|
12
|
-
return "invalidZipCode";
|
|
13
|
-
}
|
|
14
|
-
return null;
|
|
15
|
-
};
|
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
import type { H3Event } from "h3";
|
|
2
|
-
|
|
3
|
-
import type { StoredSession } from "../../app/interfaces/auth";
|
|
4
|
-
import type { AccountUser, IAccountService } from "../interfaces/account";
|
|
5
|
-
|
|
6
|
-
function createInitials(name: string): string {
|
|
7
|
-
const parts = name
|
|
8
|
-
.trim()
|
|
9
|
-
.split(/\s+/)
|
|
10
|
-
.filter(Boolean);
|
|
11
|
-
if (parts.length === 0) {
|
|
12
|
-
return "";
|
|
13
|
-
}
|
|
14
|
-
return parts
|
|
15
|
-
.slice(0, 2)
|
|
16
|
-
.map(part => part[0]?.toUpperCase() ?? "")
|
|
17
|
-
.join("");
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Live identity implementation backed by the revenexx web SDK.
|
|
22
|
-
* Resolves the current user from the request's session cookie and the
|
|
23
|
-
* platform account endpoint. Requires the NUXT_WEB_SDK_* runtime config.
|
|
24
|
-
* Register via app.config → accountService: "sdk".
|
|
25
|
-
*/
|
|
26
|
-
export class SdkAccountService implements IAccountService {
|
|
27
|
-
async getUser(event?: H3Event): Promise<AccountUser> {
|
|
28
|
-
if (!event) {
|
|
29
|
-
throw createError({ statusCode: 500, statusMessage: "SdkAccountService requires the request event" });
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
const rawSession = getCookie(event, SESSION_COOKIE_NAME);
|
|
33
|
-
if (!rawSession) {
|
|
34
|
-
throw createError({ statusCode: 401, statusMessage: "Unauthorized" });
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
let session: StoredSession;
|
|
38
|
-
try {
|
|
39
|
-
session = JSON.parse(rawSession) as StoredSession;
|
|
40
|
-
}
|
|
41
|
-
catch {
|
|
42
|
-
throw createError({ statusCode: 401, statusMessage: "Unauthorized" });
|
|
43
|
-
}
|
|
44
|
-
if (!session.fallbackCookie) {
|
|
45
|
-
throw createError({ statusCode: 401, statusMessage: "Unauthorized" });
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
const accountUser = await useAuthenticatedAccount(session.fallbackCookie).accountGet();
|
|
49
|
-
return {
|
|
50
|
-
id: accountUser.$id,
|
|
51
|
-
name: accountUser.name,
|
|
52
|
-
email: accountUser.email,
|
|
53
|
-
initials: createInitials(accountUser.name),
|
|
54
|
-
};
|
|
55
|
-
}
|
|
56
|
-
}
|
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
import type { H3Event } from "h3";
|
|
2
|
-
|
|
3
|
-
import type { AuthUser, StoredSession } from "../../app/interfaces/auth";
|
|
4
|
-
import type { AuthLoginResult, IAuthService } from "../interfaces/auth";
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Live authentication via the revenexx web SDK. Session state is carried in
|
|
8
|
-
* the same cookie as the mock implementation (with fallbackCookie set), so
|
|
9
|
-
* the client-side flow is identical. Requires NUXT_WEB_SDK_* runtime config.
|
|
10
|
-
*/
|
|
11
|
-
export class SdkAuthService implements IAuthService {
|
|
12
|
-
async login(event: H3Event, email: string, password: string): Promise<AuthLoginResult> {
|
|
13
|
-
const { result: session, fallbackCookie } = await withCookieCapture(
|
|
14
|
-
() => useShopSdkAccount().accountCreateEmailPasswordSession({ email, password }),
|
|
15
|
-
);
|
|
16
|
-
|
|
17
|
-
const user = await useAuthenticatedAccount(fallbackCookie).accountGet();
|
|
18
|
-
|
|
19
|
-
const storedSession: StoredSession = {
|
|
20
|
-
id: session.$id,
|
|
21
|
-
expire: session.expire,
|
|
22
|
-
fallbackCookie,
|
|
23
|
-
};
|
|
24
|
-
setCookie(event, SESSION_COOKIE_NAME, JSON.stringify(storedSession), sessionCookieOptions(session.expire));
|
|
25
|
-
|
|
26
|
-
return {
|
|
27
|
-
user: { $id: user.$id, name: user.name, email: user.email },
|
|
28
|
-
session: { id: session.$id, expire: session.expire },
|
|
29
|
-
};
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
async me(event: H3Event): Promise<AuthUser | null> {
|
|
33
|
-
const raw = getCookie(event, SESSION_COOKIE_NAME);
|
|
34
|
-
if (!raw) {
|
|
35
|
-
return null;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
let parsed: StoredSession;
|
|
39
|
-
try {
|
|
40
|
-
parsed = JSON.parse(raw) as StoredSession;
|
|
41
|
-
}
|
|
42
|
-
catch (err) {
|
|
43
|
-
getLogService().error("Failed to parse session cookie", toErrorContext(err));
|
|
44
|
-
deleteCookie(event, SESSION_COOKIE_NAME, sessionCookieOptions());
|
|
45
|
-
return null;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
if (!parsed.fallbackCookie) {
|
|
49
|
-
deleteCookie(event, SESSION_COOKIE_NAME, sessionCookieOptions());
|
|
50
|
-
return null;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
try {
|
|
54
|
-
const user = await useAuthenticatedAccount(parsed.fallbackCookie).accountGet();
|
|
55
|
-
return { $id: user.$id, name: user.name, email: user.email };
|
|
56
|
-
}
|
|
57
|
-
catch (err) {
|
|
58
|
-
if (!isSdkUserError(err)) {
|
|
59
|
-
getLogService().error("SDK request failed: me/accountGet", sdkErrorContext(err));
|
|
60
|
-
}
|
|
61
|
-
deleteCookie(event, SESSION_COOKIE_NAME, sessionCookieOptions());
|
|
62
|
-
return null;
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
async logout(event: H3Event): Promise<void> {
|
|
67
|
-
const raw = getCookie(event, SESSION_COOKIE_NAME);
|
|
68
|
-
if (raw) {
|
|
69
|
-
try {
|
|
70
|
-
const parsed = JSON.parse(raw) as StoredSession;
|
|
71
|
-
if (parsed.fallbackCookie) {
|
|
72
|
-
const sessionId = parsed.id || "current";
|
|
73
|
-
await useAuthenticatedAccount(parsed.fallbackCookie).accountDeleteSession({ sessionId });
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
catch (err) {
|
|
77
|
-
getLogService().error("SDK request failed: logout", sdkErrorContext(err));
|
|
78
|
-
// always clear the cookie regardless of SDK result
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
deleteCookie(event, SESSION_COOKIE_NAME, sessionCookieOptions());
|
|
82
|
-
}
|
|
83
|
-
}
|
|
@@ -1,136 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Mini client for the public revenexx gateway (api.revenexx.com) — the
|
|
3
|
-
* stand-in for the generated web SDKs that do not exist yet. Every live
|
|
4
|
-
* "api"-keyed BFF implementation goes through this client; once the real
|
|
5
|
-
* SDKs land, only the service implementations swap, the BFF contracts stay.
|
|
6
|
-
*
|
|
7
|
-
* Auth model: tenant API key (server-side only, never reaches the client).
|
|
8
|
-
* Configure via runtime config / env:
|
|
9
|
-
* NUXT_REVENEXX_API_URL (default https://api.revenexx.com)
|
|
10
|
-
* NUXT_REVENEXX_TENANT (tenant slug, e.g. "revenexx")
|
|
11
|
-
* NUXT_REVENEXX_API_KEY (rvxk_… gateway key)
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
export class RevenexxApiError extends Error {
|
|
15
|
-
readonly statusCode: number;
|
|
16
|
-
readonly type: string;
|
|
17
|
-
|
|
18
|
-
constructor(statusCode: number, type: string, message: string) {
|
|
19
|
-
super(message);
|
|
20
|
-
this.name = "RevenexxApiError";
|
|
21
|
-
this.statusCode = statusCode;
|
|
22
|
-
this.type = type;
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Expected user-facing failures (wrong credentials, duplicate email, …) are
|
|
28
|
-
* forwarded to the client as-is; everything else is an infrastructure error.
|
|
29
|
-
*/
|
|
30
|
-
const API_USER_ERROR_STATUS = new Set([400, 401, 404, 409, 429]);
|
|
31
|
-
|
|
32
|
-
export function isApiUserError(err: unknown): err is RevenexxApiError {
|
|
33
|
-
return err instanceof RevenexxApiError && API_USER_ERROR_STATUS.has(err.statusCode);
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* The customers app's auth passthrough answers with a generic error envelope
|
|
38
|
-
* (no machine type). Map the HTTP status onto the platform error types the
|
|
39
|
-
* client-side auth store already translates, so mock/sdk/api modes produce
|
|
40
|
-
* identical UX for the standard failures.
|
|
41
|
-
*/
|
|
42
|
-
export function apiAuthErrorType(err: RevenexxApiError): string {
|
|
43
|
-
switch (err.statusCode) {
|
|
44
|
-
case 401: return "user_invalid_credentials";
|
|
45
|
-
case 404: return "user_not_found";
|
|
46
|
-
case 409: return "user_email_already_exists";
|
|
47
|
-
case 429: return "general_rate_limit_exceeded";
|
|
48
|
-
default: return err.type;
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/** Diagnostic fields for structured logs — never includes the API key. */
|
|
53
|
-
export function apiErrorContext(err: unknown): Record<string, unknown> {
|
|
54
|
-
if (err instanceof RevenexxApiError) {
|
|
55
|
-
return { apiStatus: err.statusCode, apiType: err.type, apiMessage: err.message };
|
|
56
|
-
}
|
|
57
|
-
return { errorMessage: err instanceof Error ? err.message : String(err) };
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
export interface RevenexxApiRequest {
|
|
61
|
-
readonly method?: "GET" | "POST" | "PUT" | "DELETE";
|
|
62
|
-
readonly query?: Record<string, string | number | boolean | undefined>;
|
|
63
|
-
readonly body?: unknown;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
export interface RevenexxApiClient {
|
|
67
|
-
request<T>(path: string, options?: RevenexxApiRequest): Promise<T>;
|
|
68
|
-
get<T>(path: string, query?: RevenexxApiRequest["query"]): Promise<T>;
|
|
69
|
-
post<T>(path: string, body?: unknown): Promise<T>;
|
|
70
|
-
put<T>(path: string, body?: unknown): Promise<T>;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
let client: RevenexxApiClient | null = null;
|
|
74
|
-
|
|
75
|
-
export function useRevenexxApi(): RevenexxApiClient {
|
|
76
|
-
if (client) {
|
|
77
|
-
return client;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
const config = useRuntimeConfig();
|
|
81
|
-
const baseUrl = String(config.revenexxApiUrl || "https://api.revenexx.com").replace(/\/+$/, "");
|
|
82
|
-
const tenant = String(config.revenexxTenant || "");
|
|
83
|
-
const apiKey = String(config.revenexxApiKey || "");
|
|
84
|
-
|
|
85
|
-
async function request<T>(path: string, options: RevenexxApiRequest = {}): Promise<T> {
|
|
86
|
-
if (!tenant || !apiKey) {
|
|
87
|
-
throw new RevenexxApiError(503, "api_not_configured", "revenexx API credentials are not configured (NUXT_REVENEXX_TENANT / NUXT_REVENEXX_API_KEY)");
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
const url = new URL(baseUrl + path);
|
|
91
|
-
for (const [key, value] of Object.entries(options.query ?? {})) {
|
|
92
|
-
if (value !== undefined) {
|
|
93
|
-
url.searchParams.set(key, String(value));
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
const response = await fetch(url, {
|
|
98
|
-
method: options.method ?? "GET",
|
|
99
|
-
headers: {
|
|
100
|
-
"X-Revenexx-Tenant": tenant,
|
|
101
|
-
"X-Revenexx-Api-Key": apiKey,
|
|
102
|
-
"Accept": "application/json",
|
|
103
|
-
...(options.body !== undefined ? { "Content-Type": "application/json" } : {}),
|
|
104
|
-
},
|
|
105
|
-
...(options.body !== undefined ? { body: JSON.stringify(options.body) } : {}),
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
const text = await response.text();
|
|
109
|
-
let payload: unknown = null;
|
|
110
|
-
try {
|
|
111
|
-
payload = text ? JSON.parse(text) : null;
|
|
112
|
-
}
|
|
113
|
-
catch {
|
|
114
|
-
payload = null;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
if (!response.ok) {
|
|
118
|
-
const envelope = (payload ?? {}) as { type?: string; message?: string; error?: string };
|
|
119
|
-
throw new RevenexxApiError(
|
|
120
|
-
response.status,
|
|
121
|
-
envelope.type ?? "api_error",
|
|
122
|
-
envelope.message ?? envelope.error ?? `revenexx API responded with ${response.status}`,
|
|
123
|
-
);
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
return payload as T;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
client = {
|
|
130
|
-
request,
|
|
131
|
-
get: (path, query) => request(path, { query }),
|
|
132
|
-
post: (path, body) => request(path, { method: "POST", body }),
|
|
133
|
-
put: (path, body) => request(path, { method: "PUT", body }),
|
|
134
|
-
};
|
|
135
|
-
return client;
|
|
136
|
-
}
|
package/server/utils/shopSdk.ts
DELETED
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
import { Account, Client, RevenexxException } from "@revenexx/sdk";
|
|
2
|
-
|
|
3
|
-
let baseClient: Client | null = null;
|
|
4
|
-
|
|
5
|
-
function getBaseClient(): Client {
|
|
6
|
-
if (baseClient) {
|
|
7
|
-
return baseClient;
|
|
8
|
-
}
|
|
9
|
-
const config = useRuntimeConfig();
|
|
10
|
-
baseClient = new Client();
|
|
11
|
-
baseClient
|
|
12
|
-
.setEndpoint(config.webSdkApiUrl as string)
|
|
13
|
-
.setProject(config.webSdkProject as string)
|
|
14
|
-
.setDevKey(config.webSdkDevKey as string);
|
|
15
|
-
return baseClient;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export function useShopSdkAccount(): Account {
|
|
19
|
-
return new Account(getBaseClient());
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export function useAuthenticatedAccount(fallbackCookie: string): Account {
|
|
23
|
-
const config = useRuntimeConfig();
|
|
24
|
-
const client = new Client();
|
|
25
|
-
client
|
|
26
|
-
.setEndpoint(config.webSdkApiUrl as string)
|
|
27
|
-
.setProject(config.webSdkProject as string)
|
|
28
|
-
.setDevKey(config.webSdkDevKey as string);
|
|
29
|
-
client.headers["X-Fallback-Cookies"] = fallbackCookie;
|
|
30
|
-
return new Account(client);
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
const SDK_USER_ERROR_TYPES = new Set([
|
|
34
|
-
"user_invalid_credentials",
|
|
35
|
-
"user_not_found",
|
|
36
|
-
"user_blocked",
|
|
37
|
-
"general_rate_limit_exceeded",
|
|
38
|
-
"user_session_already_exists",
|
|
39
|
-
"user_email_already_exists",
|
|
40
|
-
"user_invalid_token",
|
|
41
|
-
"general_argument_invalid",
|
|
42
|
-
"password_recently_used",
|
|
43
|
-
]);
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* Returns true for expected user-facing errors (wrong credentials, rate limit, etc.)
|
|
47
|
-
* that should be forwarded to the client as-is without server-side logging.
|
|
48
|
-
* Everything else is treated as an infrastructure error.
|
|
49
|
-
*/
|
|
50
|
-
export function isSdkUserError(err: unknown): err is RevenexxException {
|
|
51
|
-
return err instanceof RevenexxException && SDK_USER_ERROR_TYPES.has(err.type);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Extracts diagnostic fields from an SDK error for structured log entries.
|
|
56
|
-
* Does not include secrets (API key is never logged).
|
|
57
|
-
*/
|
|
58
|
-
export function sdkErrorContext(err: unknown): Record<string, unknown> {
|
|
59
|
-
if (err instanceof RevenexxException) {
|
|
60
|
-
return { sdkCode: err.code, sdkType: err.type, sdkMessage: err.message };
|
|
61
|
-
}
|
|
62
|
-
return { errorMessage: err instanceof Error ? err.message : String(err) };
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
export async function withCookieCapture<T>(
|
|
66
|
-
fn: () => Promise<T>,
|
|
67
|
-
): Promise<{ result: T; fallbackCookie: string }> {
|
|
68
|
-
let fallbackCookie = "";
|
|
69
|
-
const origFetch = globalThis.fetch;
|
|
70
|
-
globalThis.fetch = async (
|
|
71
|
-
input: RequestInfo | URL,
|
|
72
|
-
init?: RequestInit,
|
|
73
|
-
) => {
|
|
74
|
-
const res = await origFetch(input, init);
|
|
75
|
-
const header = res.headers.get("x-fallback-cookies");
|
|
76
|
-
if (header) {
|
|
77
|
-
fallbackCookie = header;
|
|
78
|
-
}
|
|
79
|
-
return res;
|
|
80
|
-
};
|
|
81
|
-
try {
|
|
82
|
-
const result = await fn();
|
|
83
|
-
return { result, fallbackCookie };
|
|
84
|
-
}
|
|
85
|
-
finally {
|
|
86
|
-
globalThis.fetch = origFetch;
|
|
87
|
-
}
|
|
88
|
-
}
|