EverydayTech Platform - Developer Reference
Complete Source Code Documentation - All Applications
Loading...
Searching...
No Matches
sessionUtils.ts
Go to the documentation of this file.
1/**
2 * Session utilities for server-side API routes
3 * Extracts and validates session/store context from requests
4 */
5
6import { NextRequest } from 'next/server';
7import { cookies } from 'next/headers';
8import { Store, getStoreById } from '@/lib/stores';
9
10export interface SessionData {
11 userId: string;
12 username: string;
13 authenticated: boolean;
14 role?: 'staff' | 'employee' | 'admin';
15 storeId?: string;
16 storeName?: string;
17 storeUrl?: string;
18 storeType?: 'management' | 'store';
19 timestamp: number;
20 apiKey?: string;
21 fieldpineData?: any;
22}
23
24export interface RequestContext {
25 session: SessionData;
26 store: Store;
27 isAuthenticated: boolean;
28 isManagementPortal: boolean;
29 isRetailStore: boolean;
30}
31
32/**
33 * Get session data from request cookies
34 */
35export async function getSessionFromRequest(request?: NextRequest): Promise<SessionData | null> {
36 try {
37 const cookieStore = await cookies();
38 const sessionCookie = cookieStore.get('fieldpine-session');
39
40 if (!sessionCookie) {
41 return null;
42 }
43
44 const sessionData: SessionData = JSON.parse(sessionCookie.value);
45
46 // Check if session has expired (8 hours)
47 const sessionAge = Date.now() - sessionData.timestamp;
48 const maxAge = 8 * 60 * 60 * 1000; // 8 hours in milliseconds
49
50 if (sessionAge > maxAge) {
51 return null;
52 }
53
54 return sessionData;
55 } catch (error) {
56 console.error('[Session] Failed to parse session:', error);
57 return null;
58 }
59}
60
61/**
62 * Get full request context including session and store info
63 */
64export async function getRequestContext(request?: NextRequest): Promise<RequestContext | null> {
65 const session = await getSessionFromRequest(request);
66
67 if (!session || !session.authenticated) {
68 return null;
69 }
70
71 // Get store information
72 const store = session.storeId ? getStoreById(session.storeId) : null;
73
74 if (!store) {
75 console.error('[Session] Store not found:', session.storeId);
76 return null;
77 }
78
79 return {
80 session,
81 store,
82 isAuthenticated: true,
83 isManagementPortal: store.type === 'management',
84 isRetailStore: store.type === 'store',
85 };
86}
87
88/**
89 * Get store URL for API requests
90 * Returns the appropriate base URL based on request type
91 */
92export function getStoreApiUrl(context: RequestContext, requestType: 'openapi' | 'gnap' | 'elink' = 'openapi'): string {
93 switch (requestType) {
94 case 'openapi':
95 if (context.isManagementPortal) {
96 throw new Error('OpenAPI endpoints are not available in management portal mode');
97 }
98 return context.store.url;
99
100 case 'gnap':
101 // GNAP requests always go to IIG for central operations
102 return 'https://iig.cwanz.online';
103
104 case 'elink':
105 // ELINK goes to current store
106 return context.store.url;
107
108 default:
109 return context.store.url;
110 }
111}
112
113/**
114 * Validate that a request is allowed for the current store context
115 */
116export function validateStoreAccess(
117 context: RequestContext,
118 requiredStoreType?: 'store' | 'management'
119): { valid: boolean; error?: string } {
120 if (requiredStoreType && context.store.type !== requiredStoreType) {
121 return {
122 valid: false,
123 error: `This operation requires ${requiredStoreType === 'store' ? 'a retail store' : 'management portal'} login`
124 };
125 }
126
127 return { valid: true };
128}
129
130/**
131 * Validate API endpoint access based on user role and store type
132 *
133 * Security Rules:
134 * - Retail stores: Can ONLY access ELINK endpoints for their own store
135 * - Retail stores: CANNOT access OpenAPI or GNAP endpoints
136 * - Management portal: Full access to all endpoints (OpenAPI, GNAP, ELINK)
137 *
138 * @param context - Request context with session and store info (can be null)
139 * @param requestType - The type of API endpoint being accessed
140 * @returns Validation result with error message if access denied
141 */
142export function validateApiAccess(
143 context: RequestContext | null,
144 requestType: 'openapi' | 'gnap' | 'elink'
145): { valid: boolean; error?: string; errorCode?: string } {
146 // Check if context is null
147 if (!context) {
148 return {
149 valid: false,
150 error: 'Authentication required',
151 errorCode: 'NO_CONTEXT'
152 };
153 }
154
155 // Management portal has full access to everything
156 if (context.isManagementPortal) {
157 return { valid: true };
158 }
159
160 // Retail stores are restricted
161 if (context.isRetailStore) {
162 // Retail stores can ONLY use ELINK for their own store
163 if (requestType === 'elink') {
164 return { valid: true };
165 }
166
167 // Retail stores CANNOT access OpenAPI
168 if (requestType === 'openapi') {
169 return {
170 valid: false,
171 error: 'Access Denied: Retail stores cannot access OpenAPI endpoints. Only ELINK endpoints are permitted for store operations.',
172 errorCode: 'STORE_OPENAPI_FORBIDDEN'
173 };
174 }
175
176 // Retail stores CANNOT access GNAP (central management)
177 if (requestType === 'gnap') {
178 return {
179 valid: false,
180 error: 'Access Denied: Retail stores cannot access GNAP endpoints. These are restricted to management portal only.',
181 errorCode: 'STORE_GNAP_FORBIDDEN'
182 };
183 }
184 }
185
186 return {
187 valid: false,
188 error: 'Access Denied: Invalid store type or request type',
189 errorCode: 'INVALID_ACCESS'
190 };
191}
192
193/**
194 * Refresh the API key by re-authenticating with Fieldpine
195 * Updates the session cookie with the new API key
196 *
197 * @returns The new API key or null if refresh failed
198 */
199export async function refreshApiKey(context: RequestContext): Promise<string | null> {
200 try {
201 console.log('[Session] Refreshing API key due to 403 error...');
202
203 // Import fieldpineServerApi to avoid circular dependency
204 const { fieldpineServerApi } = require('./fieldpineApi');
205
206 // Re-authenticate with the same credentials
207 // Note: We don't have the password, so we'll need to use the existing session
208 // to get a new API key via a special endpoint or token refresh
209
210 // For now, the user will need to re-login
211 // In a production system, you'd implement a refresh token mechanism
212
213 console.log('[Session] API key refresh requires re-authentication. User must log in again.');
214 return null;
215 } catch (error) {
216 console.error('[Session] Failed to refresh API key:', error);
217 return null;
218 }
219}
220
221/**
222 * Create a Fieldpine API client configured for the current session's store
223 *
224 * @example
225 * const context = await getRequestContext(request);
226 * const api = createFieldpineApiForStore(context, 'openapi');
227 * const products = await api.apiCall('/Products', { cookie: context.session.apiKey });
228 */
229export function createFieldpineApiForStore(
230 context: RequestContext,
231 requestType: 'openapi' | 'gnap' | 'elink' = 'openapi'
232): any {
233 // Dynamically import to avoid circular dependencies
234 const { FieldpineServerApi } = require('./fieldpineApi');
235 const baseUrl = getStoreApiUrl(context, requestType);
236 return new FieldpineServerApi(baseUrl);
237}
238