2 * Session utilities for server-side API routes
3 * Extracts and validates session/store context from requests
6import { NextRequest } from 'next/server';
7import { cookies } from 'next/headers';
8import { Store, getStoreById } from '@/lib/stores';
10export interface SessionData {
13 authenticated: boolean;
14 role?: 'staff' | 'employee' | 'admin';
18 storeType?: 'management' | 'store';
24export interface RequestContext {
27 isAuthenticated: boolean;
28 isManagementPortal: boolean;
29 isRetailStore: boolean;
33 * Get session data from request cookies
35export async function getSessionFromRequest(request?: NextRequest): Promise<SessionData | null> {
37 const cookieStore = await cookies();
38 const sessionCookie = cookieStore.get('fieldpine-session');
44 const sessionData: SessionData = JSON.parse(sessionCookie.value);
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
50 if (sessionAge > maxAge) {
56 console.error('[Session] Failed to parse session:', error);
62 * Get full request context including session and store info
64export async function getRequestContext(request?: NextRequest): Promise<RequestContext | null> {
65 const session = await getSessionFromRequest(request);
67 if (!session || !session.authenticated) {
71 // Get store information
72 const store = session.storeId ? getStoreById(session.storeId) : null;
75 console.error('[Session] Store not found:', session.storeId);
82 isAuthenticated: true,
83 isManagementPortal: store.type === 'management',
84 isRetailStore: store.type === 'store',
89 * Get store URL for API requests
90 * Returns the appropriate base URL based on request type
92export function getStoreApiUrl(context: RequestContext, requestType: 'openapi' | 'gnap' | 'elink' = 'openapi'): string {
93 switch (requestType) {
95 if (context.isManagementPortal) {
96 throw new Error('OpenAPI endpoints are not available in management portal mode');
98 return context.store.url;
101 // GNAP requests always go to IIG for central operations
102 return 'https://iig.cwanz.online';
105 // ELINK goes to current store
106 return context.store.url;
109 return context.store.url;
114 * Validate that a request is allowed for the current store context
116export function validateStoreAccess(
117 context: RequestContext,
118 requiredStoreType?: 'store' | 'management'
119): { valid: boolean; error?: string } {
120 if (requiredStoreType && context.store.type !== requiredStoreType) {
123 error: `This operation requires ${requiredStoreType === 'store' ? 'a retail store' : 'management portal'} login`
127 return { valid: true };
131 * Validate API endpoint access based on user role and store type
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)
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
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
150 error: 'Authentication required',
151 errorCode: 'NO_CONTEXT'
155 // Management portal has full access to everything
156 if (context.isManagementPortal) {
157 return { valid: true };
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 };
167 // Retail stores CANNOT access OpenAPI
168 if (requestType === 'openapi') {
171 error: 'Access Denied: Retail stores cannot access OpenAPI endpoints. Only ELINK endpoints are permitted for store operations.',
172 errorCode: 'STORE_OPENAPI_FORBIDDEN'
176 // Retail stores CANNOT access GNAP (central management)
177 if (requestType === 'gnap') {
180 error: 'Access Denied: Retail stores cannot access GNAP endpoints. These are restricted to management portal only.',
181 errorCode: 'STORE_GNAP_FORBIDDEN'
188 error: 'Access Denied: Invalid store type or request type',
189 errorCode: 'INVALID_ACCESS'
194 * Refresh the API key by re-authenticating with Fieldpine
195 * Updates the session cookie with the new API key
197 * @returns The new API key or null if refresh failed
199export async function refreshApiKey(context: RequestContext): Promise<string | null> {
201 console.log('[Session] Refreshing API key due to 403 error...');
203 // Import fieldpineServerApi to avoid circular dependency
204 const { fieldpineServerApi } = require('./fieldpineApi');
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
210 // For now, the user will need to re-login
211 // In a production system, you'd implement a refresh token mechanism
213 console.log('[Session] API key refresh requires re-authentication. User must log in again.');
216 console.error('[Session] Failed to refresh API key:', error);
222 * Create a Fieldpine API client configured for the current session's store
225 * const context = await getRequestContext(request);
226 * const api = createFieldpineApiForStore(context, 'openapi');
227 * const products = await api.apiCall('/Products', { cookie: context.session.apiKey });
229export function createFieldpineApiForStore(
230 context: RequestContext,
231 requestType: 'openapi' | 'gnap' | 'elink' = 'openapi'
233 // Dynamically import to avoid circular dependencies
234 const { FieldpineServerApi } = require('./fieldpineApi');
235 const baseUrl = getStoreApiUrl(context, requestType);
236 return new FieldpineServerApi(baseUrl);