@axova/shared 1.0.0
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/CONFIGURATION_GUIDE.md +1 -0
- package/README.md +384 -0
- package/SCHEMA_ORGANIZATION.md +209 -0
- package/dist/configs/index.d.ts +85 -0
- package/dist/configs/index.js +555 -0
- package/dist/events/kafka.d.ts +40 -0
- package/dist/events/kafka.js +311 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +41 -0
- package/dist/interfaces/customer-events.d.ts +85 -0
- package/dist/interfaces/customer-events.js +2 -0
- package/dist/interfaces/inventory-events.d.ts +453 -0
- package/dist/interfaces/inventory-events.js +3 -0
- package/dist/interfaces/inventory-types.d.ts +894 -0
- package/dist/interfaces/inventory-types.js +3 -0
- package/dist/interfaces/order-events.d.ts +320 -0
- package/dist/interfaces/order-events.js +3 -0
- package/dist/lib/auditLogger.d.ts +162 -0
- package/dist/lib/auditLogger.js +626 -0
- package/dist/lib/authOrganization.d.ts +24 -0
- package/dist/lib/authOrganization.js +110 -0
- package/dist/lib/db.d.ts +6 -0
- package/dist/lib/db.js +88 -0
- package/dist/middleware/serviceAuth.d.ts +60 -0
- package/dist/middleware/serviceAuth.js +272 -0
- package/dist/middleware/storeOwnership.d.ts +15 -0
- package/dist/middleware/storeOwnership.js +156 -0
- package/dist/middleware/storeValidationMiddleware.d.ts +44 -0
- package/dist/middleware/storeValidationMiddleware.js +180 -0
- package/dist/middleware/userAuth.d.ts +27 -0
- package/dist/middleware/userAuth.js +218 -0
- package/dist/schemas/admin/admin-schema.d.ts +741 -0
- package/dist/schemas/admin/admin-schema.js +111 -0
- package/dist/schemas/ai-moderation/ai-moderation-schema.d.ts +648 -0
- package/dist/schemas/ai-moderation/ai-moderation-schema.js +88 -0
- package/dist/schemas/common/common-schemas.d.ts +436 -0
- package/dist/schemas/common/common-schemas.js +94 -0
- package/dist/schemas/compliance/compliance-schema.d.ts +3388 -0
- package/dist/schemas/compliance/compliance-schema.js +472 -0
- package/dist/schemas/compliance/kyc-schema.d.ts +2642 -0
- package/dist/schemas/compliance/kyc-schema.js +361 -0
- package/dist/schemas/customer/customer-schema.d.ts +2727 -0
- package/dist/schemas/customer/customer-schema.js +399 -0
- package/dist/schemas/index.d.ts +27 -0
- package/dist/schemas/index.js +138 -0
- package/dist/schemas/inventory/inventory-tables.d.ts +9476 -0
- package/dist/schemas/inventory/inventory-tables.js +1470 -0
- package/dist/schemas/inventory/lot-tables.d.ts +3281 -0
- package/dist/schemas/inventory/lot-tables.js +608 -0
- package/dist/schemas/order/order-schema.d.ts +5825 -0
- package/dist/schemas/order/order-schema.js +954 -0
- package/dist/schemas/product/discount-relations.d.ts +15 -0
- package/dist/schemas/product/discount-relations.js +34 -0
- package/dist/schemas/product/discount-schema.d.ts +1975 -0
- package/dist/schemas/product/discount-schema.js +297 -0
- package/dist/schemas/product/product-relations.d.ts +41 -0
- package/dist/schemas/product/product-relations.js +133 -0
- package/dist/schemas/product/product-schema.d.ts +4544 -0
- package/dist/schemas/product/product-schema.js +671 -0
- package/dist/schemas/store/store-audit-schema.d.ts +4135 -0
- package/dist/schemas/store/store-audit-schema.js +556 -0
- package/dist/schemas/store/store-schema.d.ts +3100 -0
- package/dist/schemas/store/store-schema.js +381 -0
- package/dist/schemas/store/store-settings-schema.d.ts +665 -0
- package/dist/schemas/store/store-settings-schema.js +141 -0
- package/dist/schemas/types.d.ts +50 -0
- package/dist/schemas/types.js +3 -0
- package/dist/types/events.d.ts +2396 -0
- package/dist/types/events.js +505 -0
- package/dist/utils/errorHandler.d.ts +12 -0
- package/dist/utils/errorHandler.js +36 -0
- package/dist/utils/subdomain.d.ts +6 -0
- package/dist/utils/subdomain.js +20 -0
- package/nul +8 -0
- package/package.json +43 -0
- package/src/configs/index.ts +654 -0
- package/src/events/kafka.ts +429 -0
- package/src/index.ts +26 -0
- package/src/interfaces/customer-events.ts +106 -0
- package/src/interfaces/inventory-events.ts +545 -0
- package/src/interfaces/inventory-types.ts +1004 -0
- package/src/interfaces/order-events.ts +381 -0
- package/src/lib/auditLogger.ts +1117 -0
- package/src/lib/authOrganization.ts +153 -0
- package/src/lib/db.ts +64 -0
- package/src/middleware/serviceAuth.ts +328 -0
- package/src/middleware/storeOwnership.ts +199 -0
- package/src/middleware/storeValidationMiddleware.ts +247 -0
- package/src/middleware/userAuth.ts +248 -0
- package/src/schemas/admin/admin-schema.ts +208 -0
- package/src/schemas/ai-moderation/ai-moderation-schema.ts +180 -0
- package/src/schemas/common/common-schemas.ts +108 -0
- package/src/schemas/compliance/compliance-schema.ts +927 -0
- package/src/schemas/compliance/kyc-schema.ts +649 -0
- package/src/schemas/customer/customer-schema.ts +576 -0
- package/src/schemas/index.ts +189 -0
- package/src/schemas/inventory/inventory-tables.ts +1927 -0
- package/src/schemas/inventory/lot-tables.ts +799 -0
- package/src/schemas/order/order-schema.ts +1400 -0
- package/src/schemas/product/discount-relations.ts +44 -0
- package/src/schemas/product/discount-schema.ts +464 -0
- package/src/schemas/product/product-relations.ts +187 -0
- package/src/schemas/product/product-schema.ts +955 -0
- package/src/schemas/store/ethiopian_business_api.md.resolved +212 -0
- package/src/schemas/store/store-audit-schema.ts +1257 -0
- package/src/schemas/store/store-schema.ts +661 -0
- package/src/schemas/store/store-settings-schema.ts +231 -0
- package/src/schemas/types.ts +67 -0
- package/src/types/events.ts +646 -0
- package/src/utils/errorHandler.ts +44 -0
- package/src/utils/subdomain.ts +19 -0
- package/tsconfig.json +21 -0
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import { eq } from "drizzle-orm";
|
|
2
|
+
import type { FastifyReply, FastifyRequest } from "fastify";
|
|
3
|
+
import { db } from "../lib/db";
|
|
4
|
+
import { stores } from "../schemas/store/store-schema";
|
|
5
|
+
import {
|
|
6
|
+
createServiceAuthenticator,
|
|
7
|
+
getServiceAuthConfigFromEnv,
|
|
8
|
+
} from "./serviceAuth";
|
|
9
|
+
|
|
10
|
+
export interface StoreOwnerMiddlewareOptions {
|
|
11
|
+
requireActive?: boolean;
|
|
12
|
+
allowParameterStoreId?: boolean;
|
|
13
|
+
storeIdParamName?: string;
|
|
14
|
+
allowAdminOverride?: boolean;
|
|
15
|
+
overrideRoles?: string[];
|
|
16
|
+
overridePermissions?: string[];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Factory that returns a middleware ensuring the current user owns the target store.
|
|
21
|
+
* - Uses shared store validation under the hood for existence/active checks
|
|
22
|
+
* - Supports optional admin/permission overrides (e.g. SUPER_ADMIN or * permissions)
|
|
23
|
+
*/
|
|
24
|
+
export function requireStoreOwner(options: StoreOwnerMiddlewareOptions = {}) {
|
|
25
|
+
const {
|
|
26
|
+
requireActive = true,
|
|
27
|
+
allowParameterStoreId = true,
|
|
28
|
+
storeIdParamName = "storeId",
|
|
29
|
+
allowAdminOverride = true,
|
|
30
|
+
overrideRoles = ["SUPER_ADMIN"],
|
|
31
|
+
overridePermissions = ["*", "store:read:all", "store:write:all"],
|
|
32
|
+
} = options;
|
|
33
|
+
|
|
34
|
+
return async (request: FastifyRequest, reply: FastifyReply) => {
|
|
35
|
+
const user = (request as any).user as
|
|
36
|
+
| {
|
|
37
|
+
userId: string;
|
|
38
|
+
role?: string;
|
|
39
|
+
permissions?: string[];
|
|
40
|
+
}
|
|
41
|
+
| undefined;
|
|
42
|
+
|
|
43
|
+
// Extract storeId from params/query/body/user.context
|
|
44
|
+
let storeId: string | undefined;
|
|
45
|
+
const params = (request.params || {}) as Record<string, any>;
|
|
46
|
+
const query = (request.query || {}) as Record<string, any>;
|
|
47
|
+
const body = (request.body || {}) as Record<string, any>;
|
|
48
|
+
if (allowParameterStoreId && typeof params[storeIdParamName] === "string") {
|
|
49
|
+
storeId = params[storeIdParamName] as string;
|
|
50
|
+
}
|
|
51
|
+
if (!storeId && typeof query.storeId === "string")
|
|
52
|
+
storeId = query.storeId as string;
|
|
53
|
+
if (!storeId && typeof body.storeId === "string")
|
|
54
|
+
storeId = body.storeId as string;
|
|
55
|
+
if (!storeId && typeof (request as any).user?.storeId === "string") {
|
|
56
|
+
storeId = (request as any).user?.storeId as string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (!storeId) {
|
|
60
|
+
return reply.status(400).send({
|
|
61
|
+
error: "Store ID Required",
|
|
62
|
+
message: `Store ID must be provided in URL (:${storeIdParamName}), query, or body`,
|
|
63
|
+
code: "STORE_ID_MISSING",
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Admin/permission overrides
|
|
68
|
+
const hasRoleOverride = !!user?.role && overrideRoles.includes(user.role);
|
|
69
|
+
const hasPermissionOverride = !!user?.permissions?.some((p) =>
|
|
70
|
+
overridePermissions.includes(p),
|
|
71
|
+
);
|
|
72
|
+
if (allowAdminOverride && (hasRoleOverride || hasPermissionOverride)) {
|
|
73
|
+
// Optionally attach minimal store info by fetching; if it fails, still allow
|
|
74
|
+
await attachStoreInfoIfPossible(request, storeId);
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Fetch store details from Store Service internal route
|
|
79
|
+
try {
|
|
80
|
+
const store = await fetchStoreById(storeId);
|
|
81
|
+
|
|
82
|
+
if (!store) {
|
|
83
|
+
return reply.status(404).send({
|
|
84
|
+
error: "Store Not Found",
|
|
85
|
+
message: `Store with ID ${storeId} does not exist`,
|
|
86
|
+
code: "STORE_NOT_FOUND",
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// requireActive check
|
|
91
|
+
if (requireActive && store.isActive === false) {
|
|
92
|
+
return reply.status(403).send({
|
|
93
|
+
error: "Store Inactive",
|
|
94
|
+
message: `Store ${storeId} is currently inactive`,
|
|
95
|
+
code: "STORE_INACTIVE",
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Ownership check
|
|
100
|
+
if (!user?.userId || store.userId !== user.userId) {
|
|
101
|
+
return reply.status(403).send({
|
|
102
|
+
error: "Store Access Denied",
|
|
103
|
+
message: "You do not have permission to access this store",
|
|
104
|
+
code: "STORE_ACCESS_DENIED",
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Attach store info to request for downstream handlers
|
|
109
|
+
(request as any).storeInfo = {
|
|
110
|
+
storeId: store.id,
|
|
111
|
+
storeName: store.storeName,
|
|
112
|
+
isActive: !!store.isActive,
|
|
113
|
+
userId: store.userId,
|
|
114
|
+
};
|
|
115
|
+
} catch (error) {
|
|
116
|
+
console.error("Store ownership validation error:", error);
|
|
117
|
+
return reply.status(500).send({
|
|
118
|
+
error: "Internal Server Error",
|
|
119
|
+
message: "Failed to validate store ownership",
|
|
120
|
+
code: "STORE_OWNERSHIP_VALIDATION_ERROR",
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Fetches store by ID from the Store Service internal endpoint
|
|
127
|
+
async function fetchStoreById(storeId: string): Promise<{
|
|
128
|
+
id: string;
|
|
129
|
+
storeName: string;
|
|
130
|
+
isActive?: boolean;
|
|
131
|
+
userId: string;
|
|
132
|
+
} | null> {
|
|
133
|
+
const baseUrl = process.env.STORE_SERVICE_URL || "http://localhost:3001";
|
|
134
|
+
const url = `${baseUrl}/internal/stores/${encodeURIComponent(storeId)}`;
|
|
135
|
+
|
|
136
|
+
let token: string | undefined;
|
|
137
|
+
try {
|
|
138
|
+
const cfg = getServiceAuthConfigFromEnv();
|
|
139
|
+
token = createServiceAuthenticator(cfg).generateServiceToken(
|
|
140
|
+
process.env.SERVICE_NAME || "inventory-core-service",
|
|
141
|
+
["store:read"],
|
|
142
|
+
);
|
|
143
|
+
} catch {
|
|
144
|
+
// Development fallback tokens accepted by store service
|
|
145
|
+
const isDev = process.env.NODE_ENV !== "production";
|
|
146
|
+
if (isDev) token = "inventory-dev-access";
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const headers: Record<string, string> = {
|
|
150
|
+
"Content-Type": "application/json",
|
|
151
|
+
};
|
|
152
|
+
if (token) headers["x-service-token"] = token;
|
|
153
|
+
|
|
154
|
+
try {
|
|
155
|
+
const res = await fetch(url, { method: "GET", headers });
|
|
156
|
+
if (res.ok) {
|
|
157
|
+
const data = (await res.json()) as any;
|
|
158
|
+
const store = data?.store || data?.data?.store || null;
|
|
159
|
+
if (store) return store;
|
|
160
|
+
}
|
|
161
|
+
} catch {
|
|
162
|
+
// ignore network error and try DB fallback when possible
|
|
163
|
+
}
|
|
164
|
+
// DB fallback (primarily for in-process store-service)
|
|
165
|
+
try {
|
|
166
|
+
const result = await db
|
|
167
|
+
.select({
|
|
168
|
+
id: stores.id,
|
|
169
|
+
storeName: stores.storeName,
|
|
170
|
+
isActive: stores.isActive,
|
|
171
|
+
userId: stores.userId,
|
|
172
|
+
})
|
|
173
|
+
.from(stores)
|
|
174
|
+
.where(eq(stores.id, storeId))
|
|
175
|
+
.limit(1);
|
|
176
|
+
return result[0] || null;
|
|
177
|
+
} catch {
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
async function attachStoreInfoIfPossible(
|
|
183
|
+
request: FastifyRequest,
|
|
184
|
+
storeId: string,
|
|
185
|
+
) {
|
|
186
|
+
try {
|
|
187
|
+
const store = await fetchStoreById(storeId);
|
|
188
|
+
if (store) {
|
|
189
|
+
(request as any).storeInfo = {
|
|
190
|
+
storeId: store.id,
|
|
191
|
+
storeName: store.storeName,
|
|
192
|
+
isActive: !!store.isActive,
|
|
193
|
+
userId: store.userId,
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
} catch {
|
|
197
|
+
// ignore
|
|
198
|
+
}
|
|
199
|
+
}
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import { eq } from "drizzle-orm";
|
|
2
|
+
import { FastifyReply, FastifyRequest } from "fastify";
|
|
3
|
+
import { db } from "../lib/db";
|
|
4
|
+
import { stores } from "../schemas/store/store-schema";
|
|
5
|
+
|
|
6
|
+
// Store cache to reduce database calls
|
|
7
|
+
const storeCache = new Map<
|
|
8
|
+
string,
|
|
9
|
+
{
|
|
10
|
+
isValid: boolean;
|
|
11
|
+
timestamp: number;
|
|
12
|
+
storeData?: {
|
|
13
|
+
id: string;
|
|
14
|
+
storeName: string;
|
|
15
|
+
isActive: boolean;
|
|
16
|
+
userId: string;
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
>();
|
|
20
|
+
const CACHE_TTL = 5 * 60 * 1000; // 5 minutes
|
|
21
|
+
|
|
22
|
+
export interface StoreValidationOptions {
|
|
23
|
+
requireActive?: boolean;
|
|
24
|
+
allowParameterStoreId?: boolean;
|
|
25
|
+
storeIdParamName?: string;
|
|
26
|
+
requireOwnership?: boolean;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Extended request interface to include store information
|
|
30
|
+
declare module "fastify" {
|
|
31
|
+
interface FastifyRequest {
|
|
32
|
+
user?: {
|
|
33
|
+
userId: string;
|
|
34
|
+
storeId: string;
|
|
35
|
+
role: string;
|
|
36
|
+
permissions: string[];
|
|
37
|
+
iat?: number;
|
|
38
|
+
exp?: number;
|
|
39
|
+
};
|
|
40
|
+
session?: unknown;
|
|
41
|
+
storeInfo?: {
|
|
42
|
+
storeId: string;
|
|
43
|
+
storeName: string;
|
|
44
|
+
isActive: boolean;
|
|
45
|
+
userId: string;
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Middleware to validate store existence and status
|
|
52
|
+
*/
|
|
53
|
+
export const validateStoreMiddleware = (
|
|
54
|
+
options: StoreValidationOptions = {},
|
|
55
|
+
) => {
|
|
56
|
+
return async (request: FastifyRequest, reply: FastifyReply) => {
|
|
57
|
+
try {
|
|
58
|
+
const {
|
|
59
|
+
requireActive = true,
|
|
60
|
+
allowParameterStoreId = true,
|
|
61
|
+
storeIdParamName = "storeId",
|
|
62
|
+
requireOwnership = false,
|
|
63
|
+
} = options;
|
|
64
|
+
|
|
65
|
+
// Extract store ID from various sources
|
|
66
|
+
let storeId: string | undefined;
|
|
67
|
+
|
|
68
|
+
// 1. Try from authenticated user context
|
|
69
|
+
if (request.user?.storeId) {
|
|
70
|
+
storeId = request.user.storeId;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// 2. Try from URL parameters if allowed
|
|
74
|
+
if (!storeId && allowParameterStoreId) {
|
|
75
|
+
const params = request.params as Record<string, string>;
|
|
76
|
+
storeId = params[storeIdParamName];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// 3. Try from query parameters
|
|
80
|
+
if (!storeId) {
|
|
81
|
+
const query = request.query as Record<string, string>;
|
|
82
|
+
storeId = query.storeId;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// 4. Try from request body
|
|
86
|
+
if (!storeId && request.body && typeof request.body === "object") {
|
|
87
|
+
const body = request.body as Record<string, unknown>;
|
|
88
|
+
if (typeof body.storeId === "string") {
|
|
89
|
+
storeId = body.storeId;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (!storeId) {
|
|
94
|
+
return reply.status(400).send({
|
|
95
|
+
error: "Store ID Required",
|
|
96
|
+
message:
|
|
97
|
+
"Store ID must be provided in URL, query parameters, or request body",
|
|
98
|
+
code: "STORE_ID_MISSING",
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Check cache first
|
|
103
|
+
const cached = storeCache.get(storeId);
|
|
104
|
+
if (cached && Date.now() - cached.timestamp < CACHE_TTL) {
|
|
105
|
+
if (!cached.isValid) {
|
|
106
|
+
return reply.status(404).send({
|
|
107
|
+
error: "Store Not Found",
|
|
108
|
+
message: `Store with ID ${storeId} does not exist`,
|
|
109
|
+
code: "STORE_NOT_FOUND",
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Use cached store data if available
|
|
114
|
+
if (cached.storeData) {
|
|
115
|
+
const storeData = cached.storeData;
|
|
116
|
+
|
|
117
|
+
// Check if store is active when required
|
|
118
|
+
if (requireActive && !storeData.isActive) {
|
|
119
|
+
return reply.status(403).send({
|
|
120
|
+
error: "Store Inactive",
|
|
121
|
+
message: `Store ${storeId} is currently inactive`,
|
|
122
|
+
code: "STORE_INACTIVE",
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Check ownership when required
|
|
127
|
+
if (requireOwnership && request.user?.userId !== storeData.userId) {
|
|
128
|
+
return reply.status(403).send({
|
|
129
|
+
error: "Store Access Denied",
|
|
130
|
+
message: "You do not have permission to access this store",
|
|
131
|
+
code: "STORE_ACCESS_DENIED",
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Attach store information to request
|
|
136
|
+
request.storeInfo = {
|
|
137
|
+
storeId: storeData.id,
|
|
138
|
+
storeName: storeData.storeName,
|
|
139
|
+
isActive: storeData.isActive,
|
|
140
|
+
userId: storeData.userId,
|
|
141
|
+
};
|
|
142
|
+
return; // Successfully validated using cache
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Validate store exists and get details from database
|
|
147
|
+
const store = await db
|
|
148
|
+
.select({
|
|
149
|
+
id: stores.id,
|
|
150
|
+
storeName: stores.storeName,
|
|
151
|
+
isActive: stores.isActive,
|
|
152
|
+
userId: stores.userId,
|
|
153
|
+
})
|
|
154
|
+
.from(stores)
|
|
155
|
+
.where(eq(stores.id, storeId))
|
|
156
|
+
.limit(1);
|
|
157
|
+
|
|
158
|
+
if (store.length === 0) {
|
|
159
|
+
// Cache negative result
|
|
160
|
+
storeCache.set(storeId, { isValid: false, timestamp: Date.now() });
|
|
161
|
+
|
|
162
|
+
return reply.status(404).send({
|
|
163
|
+
error: "Store Not Found",
|
|
164
|
+
message: `Store with ID ${storeId} does not exist`,
|
|
165
|
+
code: "STORE_NOT_FOUND",
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const storeData = store[0];
|
|
170
|
+
|
|
171
|
+
// Check if store is active when required
|
|
172
|
+
if (requireActive && !storeData.isActive) {
|
|
173
|
+
return reply.status(403).send({
|
|
174
|
+
error: "Store Inactive",
|
|
175
|
+
message: `Store ${storeId} is currently inactive`,
|
|
176
|
+
code: "STORE_INACTIVE",
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Check ownership when required
|
|
181
|
+
if (requireOwnership && request.user?.userId !== storeData.userId) {
|
|
182
|
+
return reply.status(403).send({
|
|
183
|
+
error: "Store Access Denied",
|
|
184
|
+
message: "You do not have permission to access this store",
|
|
185
|
+
code: "STORE_ACCESS_DENIED",
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Cache positive result with store data
|
|
190
|
+
storeCache.set(storeId, {
|
|
191
|
+
isValid: true,
|
|
192
|
+
timestamp: Date.now(),
|
|
193
|
+
storeData: {
|
|
194
|
+
id: storeData.id,
|
|
195
|
+
storeName: storeData.storeName,
|
|
196
|
+
isActive: storeData.isActive,
|
|
197
|
+
userId: storeData.userId,
|
|
198
|
+
},
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
// Attach store information to request
|
|
202
|
+
request.storeInfo = {
|
|
203
|
+
storeId: storeData.id,
|
|
204
|
+
storeName: storeData.storeName,
|
|
205
|
+
isActive: storeData.isActive,
|
|
206
|
+
userId: storeData.userId,
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
// Continue to next handler - explicitly return to proceed
|
|
210
|
+
} catch (error) {
|
|
211
|
+
console.error("Store validation error:", error);
|
|
212
|
+
return reply.status(500).send({
|
|
213
|
+
error: "Internal Server Error",
|
|
214
|
+
message: "Failed to validate store",
|
|
215
|
+
code: "STORE_VALIDATION_ERROR",
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Helper to clear store cache entry
|
|
223
|
+
*/
|
|
224
|
+
export const clearStoreCache = (storeId: string) => {
|
|
225
|
+
storeCache.delete(storeId);
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Helper to clear all store cache
|
|
230
|
+
*/
|
|
231
|
+
export const clearAllStoreCache = () => {
|
|
232
|
+
storeCache.clear();
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Simplified middleware for common use cases
|
|
237
|
+
*/
|
|
238
|
+
export const requireValidStore = validateStoreMiddleware({
|
|
239
|
+
requireActive: true,
|
|
240
|
+
});
|
|
241
|
+
export const requireOwnedStore = validateStoreMiddleware({
|
|
242
|
+
requireActive: true,
|
|
243
|
+
requireOwnership: true,
|
|
244
|
+
});
|
|
245
|
+
export const requireAnyStore = validateStoreMiddleware({
|
|
246
|
+
requireActive: false,
|
|
247
|
+
});
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import { createAuthMiddleware } from "better-middleware";
|
|
2
|
+
import type { FastifyPluginAsync, FastifyReply, FastifyRequest } from "fastify";
|
|
3
|
+
import fp from "fastify-plugin";
|
|
4
|
+
|
|
5
|
+
// Standardized user context aligned with existing request augmentation in this package
|
|
6
|
+
export interface SharedUserContext {
|
|
7
|
+
userId: string;
|
|
8
|
+
storeId: string;
|
|
9
|
+
role: string;
|
|
10
|
+
permissions: string[];
|
|
11
|
+
organizationId?: string;
|
|
12
|
+
activeOrganizationId?: string;
|
|
13
|
+
iat?: number;
|
|
14
|
+
exp?: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Optional role → permission mapping (used for helpers)
|
|
18
|
+
export const DEFAULT_ROLE_PERMISSIONS: Record<string, string[]> = {
|
|
19
|
+
SUPER_ADMIN: ["*"],
|
|
20
|
+
STORE_ADMIN: [
|
|
21
|
+
"store:read",
|
|
22
|
+
"store:write",
|
|
23
|
+
"store:delete",
|
|
24
|
+
"orders:read",
|
|
25
|
+
"orders:write",
|
|
26
|
+
"products:read",
|
|
27
|
+
"products:write",
|
|
28
|
+
],
|
|
29
|
+
STORE_MANAGER: [
|
|
30
|
+
"store:read",
|
|
31
|
+
"store:write",
|
|
32
|
+
"orders:read",
|
|
33
|
+
"orders:write",
|
|
34
|
+
"products:read",
|
|
35
|
+
"products:write",
|
|
36
|
+
],
|
|
37
|
+
STORE_EMPLOYEE: ["store:read", "orders:read", "products:read"],
|
|
38
|
+
USER: ["store:read"],
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
// Core adapter of better-middleware to Fastify
|
|
42
|
+
function createFastifyAuthHandler() {
|
|
43
|
+
const auth = createAuthMiddleware<FastifyRequest>({
|
|
44
|
+
baseURL: process.env.NEXT_PUBLIC_AUTH_URL || "http://localhost:4000",
|
|
45
|
+
cache: { enabled: true, ttl: 300, max: 1000 },
|
|
46
|
+
onError: async () => ({
|
|
47
|
+
status: 401,
|
|
48
|
+
body: {
|
|
49
|
+
success: false,
|
|
50
|
+
error: "Authentication required",
|
|
51
|
+
code: "UNAUTHORIZED",
|
|
52
|
+
},
|
|
53
|
+
}),
|
|
54
|
+
logger: {
|
|
55
|
+
info: (m, d) =>
|
|
56
|
+
process.env.NODE_ENV === "development"
|
|
57
|
+
? console.log(`[Auth] ${m}`, d)
|
|
58
|
+
: undefined,
|
|
59
|
+
error: (m, d) => console.error(`[Auth] ${m}`, d),
|
|
60
|
+
debug: (m, d) =>
|
|
61
|
+
process.env.NODE_ENV === "development"
|
|
62
|
+
? console.debug(`[Auth] ${m}`, d)
|
|
63
|
+
: undefined,
|
|
64
|
+
},
|
|
65
|
+
framework: {
|
|
66
|
+
getHeaders: (request: any) => {
|
|
67
|
+
const headers: Record<string, string> = {};
|
|
68
|
+
Object.entries(request.headers || {}).forEach(([k, v]) => {
|
|
69
|
+
if (typeof v === "string") headers[k] = v;
|
|
70
|
+
else if (Array.isArray(v)) headers[k] = v.join(", ");
|
|
71
|
+
});
|
|
72
|
+
const hasCookie = (headers["cookie"] || "").includes(
|
|
73
|
+
"better-auth.session_token=",
|
|
74
|
+
);
|
|
75
|
+
const bearer = headers["authorization"]?.startsWith("Bearer ")
|
|
76
|
+
? headers["authorization"].substring(7)
|
|
77
|
+
: undefined;
|
|
78
|
+
if (!hasCookie && bearer) {
|
|
79
|
+
headers["cookie"] =
|
|
80
|
+
`${headers["cookie"] ? headers["cookie"] + "; " : ""}better-auth.session_token=${bearer}`;
|
|
81
|
+
}
|
|
82
|
+
return headers;
|
|
83
|
+
},
|
|
84
|
+
getCookies: (request: any) => {
|
|
85
|
+
const cookies = request.cookies || {};
|
|
86
|
+
if (
|
|
87
|
+
!cookies["better-auth.session_token"] &&
|
|
88
|
+
request.headers.authorization?.startsWith("Bearer ")
|
|
89
|
+
) {
|
|
90
|
+
cookies["better-auth.session_token"] =
|
|
91
|
+
request.headers.authorization.substring(7);
|
|
92
|
+
}
|
|
93
|
+
return cookies;
|
|
94
|
+
},
|
|
95
|
+
setContext: (request: any, key: "user" | "session", value: any) => {
|
|
96
|
+
if (key === "user" && value) {
|
|
97
|
+
const u = value as any;
|
|
98
|
+
request.user = {
|
|
99
|
+
userId: u.id,
|
|
100
|
+
storeId: u.activeOrganizationId || u.organizationId || "",
|
|
101
|
+
role: u.role || "USER",
|
|
102
|
+
permissions: DEFAULT_ROLE_PERMISSIONS[u.role || "USER"] || [],
|
|
103
|
+
organizationId: u.organizationId,
|
|
104
|
+
activeOrganizationId: u.activeOrganizationId,
|
|
105
|
+
iat: u.iat,
|
|
106
|
+
exp: u.exp,
|
|
107
|
+
} satisfies SharedUserContext;
|
|
108
|
+
} else if (key === "session" && value) {
|
|
109
|
+
const s = value as any;
|
|
110
|
+
if (request.user && !request.user.storeId)
|
|
111
|
+
request.user.storeId =
|
|
112
|
+
s.activeOrganizationId || request.user.storeId;
|
|
113
|
+
}
|
|
114
|
+
},
|
|
115
|
+
createResponse: (_request: any, body: unknown, status: number) => ({
|
|
116
|
+
status,
|
|
117
|
+
body,
|
|
118
|
+
}),
|
|
119
|
+
},
|
|
120
|
+
});
|
|
121
|
+
return auth;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Pre-handler that runs authentication; emits response when not authorized
|
|
125
|
+
export const AuthPreHandler = async (
|
|
126
|
+
request: FastifyRequest,
|
|
127
|
+
reply: FastifyReply,
|
|
128
|
+
) => {
|
|
129
|
+
const auth = createFastifyAuthHandler();
|
|
130
|
+
const result = await auth(request, request, async () => {});
|
|
131
|
+
if (result) {
|
|
132
|
+
reply.code(result.status).send(result.body);
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
// Fastify plugin for easy registration
|
|
137
|
+
export const fastifyUserAuthPlugin: FastifyPluginAsync = fp(
|
|
138
|
+
async (fastify: any) => {
|
|
139
|
+
fastify.addHook("preHandler", AuthPreHandler as any);
|
|
140
|
+
},
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
// ---------- Helper utilities ----------
|
|
144
|
+
export function getAuthTokenFromRequest(
|
|
145
|
+
request: FastifyRequest,
|
|
146
|
+
): string | undefined {
|
|
147
|
+
const cookies = (request as any).cookies || {};
|
|
148
|
+
const tokenFromCookie = cookies["better-auth.session_token"] as
|
|
149
|
+
| string
|
|
150
|
+
| undefined;
|
|
151
|
+
if (tokenFromCookie) return tokenFromCookie;
|
|
152
|
+
const cookieHeader = request.headers.cookie;
|
|
153
|
+
if (cookieHeader) {
|
|
154
|
+
const match = cookieHeader.match(/better-auth\.session_token=([^;]+)/);
|
|
155
|
+
if (match) return match[1];
|
|
156
|
+
}
|
|
157
|
+
const authHeader = request.headers.authorization;
|
|
158
|
+
if (authHeader?.startsWith("Bearer ")) return authHeader.substring(7);
|
|
159
|
+
return undefined;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export function getAuthHeadersFromRequest(
|
|
163
|
+
request: FastifyRequest,
|
|
164
|
+
): Record<string, string> {
|
|
165
|
+
const headers: Record<string, string> = {
|
|
166
|
+
"Content-Type": "application/json",
|
|
167
|
+
};
|
|
168
|
+
const token = getAuthTokenFromRequest(request);
|
|
169
|
+
if (token) {
|
|
170
|
+
headers["Cookie"] = `better-auth.session_token=${token}`;
|
|
171
|
+
headers["Authorization"] = `Bearer ${token}`;
|
|
172
|
+
}
|
|
173
|
+
return headers;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export function getUserDataFromRequest(request: FastifyRequest): {
|
|
177
|
+
userId: string;
|
|
178
|
+
organizationId?: string;
|
|
179
|
+
role?: string;
|
|
180
|
+
permissions?: string[];
|
|
181
|
+
} | null {
|
|
182
|
+
const user = (request as any).user as SharedUserContext | undefined;
|
|
183
|
+
if (user) {
|
|
184
|
+
return {
|
|
185
|
+
userId: user.userId,
|
|
186
|
+
organizationId: user.storeId || user.organizationId,
|
|
187
|
+
role: user.role,
|
|
188
|
+
permissions: user.permissions,
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const userIdFromHeader = request.headers["x-user-id"] as string | undefined;
|
|
193
|
+
const storeIdFromHeader = request.headers["x-store-id"] as string | undefined;
|
|
194
|
+
const roleFromHeader = request.headers["x-user-role"] as string | undefined;
|
|
195
|
+
if (userIdFromHeader) {
|
|
196
|
+
return {
|
|
197
|
+
userId: userIdFromHeader,
|
|
198
|
+
organizationId: storeIdFromHeader,
|
|
199
|
+
role: roleFromHeader || "USER",
|
|
200
|
+
permissions: DEFAULT_ROLE_PERMISSIONS[roleFromHeader || "USER"] || [],
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export function getUserIdFromRequest(request: FastifyRequest): string | null {
|
|
207
|
+
return getUserDataFromRequest(request)?.userId || null;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export function hasPermission(
|
|
211
|
+
request: FastifyRequest,
|
|
212
|
+
permission: string,
|
|
213
|
+
): boolean {
|
|
214
|
+
const user = (request as any).user as SharedUserContext | undefined;
|
|
215
|
+
const perms = user?.permissions || [];
|
|
216
|
+
return perms.includes("*") || perms.includes(permission);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export function hasRole(request: FastifyRequest, role: string): boolean {
|
|
220
|
+
const user = (request as any).user as SharedUserContext | undefined;
|
|
221
|
+
return (user?.role || "").toUpperCase() === role.toUpperCase();
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export function requirePermission(permission: string) {
|
|
225
|
+
return async (request: FastifyRequest, reply: FastifyReply) => {
|
|
226
|
+
if (!hasPermission(request, permission)) {
|
|
227
|
+
reply.code(403).send({
|
|
228
|
+
success: false,
|
|
229
|
+
error: "Insufficient permissions",
|
|
230
|
+
message: `Required permission: ${permission}`,
|
|
231
|
+
code: "FORBIDDEN",
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
export function requireRole(role: string) {
|
|
238
|
+
return async (request: FastifyRequest, reply: FastifyReply) => {
|
|
239
|
+
if (!hasRole(request, role)) {
|
|
240
|
+
reply.code(403).send({
|
|
241
|
+
success: false,
|
|
242
|
+
error: "Insufficient role",
|
|
243
|
+
message: `Required role: ${role}`,
|
|
244
|
+
code: "FORBIDDEN",
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
};
|
|
248
|
+
}
|