UNPKG

strapi-to-lokalise-plugin

Version:

Preview and sync Lokalise translations from Strapi admin

496 lines (448 loc) 22.1 kB
'use strict'; /** * Middleware to authenticate admin users for plugin routes * Works for both Strapi v4 and v5 * * Uses Strapi's built-in admin authentication service. * This is the official Strapi way - leverages Strapi's own auth mechanisms. * * Production-ready version that works: * - Strapi v4 * - Strapi v5 * - Admin panel requests * - API calls * - Cookie mode * - Authorization header mode */ module.exports = (config, { strapi: strapiFromConfig }) => { return async (ctx, next) => { // CRITICAL: Log ALL incoming requests to lokalise-sync routes if (ctx.path && ctx.path.includes('lokalise-sync')) { console.log('\n========================================'); console.log('[lokalise-sync] 🌐 INCOMING REQUEST'); console.log('========================================'); console.log('[lokalise-sync] Method:', ctx.method); console.log('[lokalise-sync] Path:', ctx.path); console.log('[lokalise-sync] URL:', ctx.url); console.log('[lokalise-sync] Original URL:', ctx.originalUrl); console.log('[lokalise-sync] Request path:', ctx.request?.path); console.log('[lokalise-sync] Request URL:', ctx.request?.url); console.log('========================================\n'); } // Get strapi instance (try ctx.strapi first, then config) const strapi = ctx.strapi || strapiFromConfig; if (!strapi) { ctx.status = 500; ctx.body = { error: { status: 500, name: 'InternalServerError', message: 'Strapi instance not available', details: {}, }, }; return; } // CRITICAL: Check if request comes from admin panel and user is logged in // In Strapi v4, requests from admin panel should have session info const referer = ctx.request?.headers?.referer || ctx.headers?.referer || ''; const isFromAdminPanel = referer.includes('/admin') || ctx.path?.includes('/admin'); // 1. FIRST: Check if Strapi already authenticated admin user → done // This happens when routes use Strapi's built-in auth if (ctx.state.admin && ctx.state.admin.id) { ctx.state.user = ctx.state.admin; if (strapi.log) { strapi.log.debug('[lokalise-sync] ✅ Admin already authenticated via ctx.state.admin'); } return await next(); } if (ctx.state.user && ctx.state.user.id) { // Check if this is an admin user by querying admin::user table try { const adminUser = await strapi.db.query('admin::user').findOne({ id: ctx.state.user.id }); if (adminUser && adminUser.isActive !== false) { ctx.state.admin = adminUser; if (strapi.log) { strapi.log.debug('[lokalise-sync] ✅ Admin authenticated via ctx.state.user (verified)'); } return await next(); } } catch (e) { // Continue to next check } } // 2. Check session store for active admin session // This works even if cookies aren't set - Strapi might have session in memory if (isFromAdminPanel && strapi.store && typeof strapi.store.get === 'function') { try { const sessionId = ctx.cookies?.get('strapi.sid') || ctx.request?.headers?.['x-session-id']; if (sessionId) { const session = await strapi.store({ type: 'session' }).get({ key: sessionId }); if (session && session.passport && session.passport.user) { const adminUser = await strapi.db.query('admin::user').findOne({ id: session.passport.user }); if (adminUser && adminUser.isActive !== false) { ctx.state.admin = adminUser; ctx.state.user = adminUser; if (strapi.log) { strapi.log.debug('[lokalise-sync] ✅ Admin authenticated via session store'); } return await next(); } } } } catch (e) { // Session check failed, continue to next method } } // 2. Try to get authenticated admin from Strapi's session/request // This works even when cookies are blocked by external auth (like Clerk) // Strapi might have authenticated the user through other means (session, headers, etc.) try { // Check if request has admin session (common in v5, sometimes in v4) if (ctx.request && ctx.request.user && ctx.request.user.id) { const sessionUser = ctx.request.user; // Verify it's an admin user by checking if it has admin-specific fields const adminUser = await strapi.db.query('admin::user').findOne({ id: sessionUser.id }); if (adminUser && adminUser.isActive !== false) { ctx.state.admin = adminUser; ctx.state.user = adminUser; if (strapi.log) { strapi.log.debug('[lokalise-sync] ✅ Admin authenticated via ctx.request.user (session-based)'); } return await next(); } } } catch (e) { // Continue to next method if (strapi.log) { strapi.log.debug('[lokalise-sync] Session-based auth check failed:', e.message); } } try { // 2. Try authenticate using Strapi's built-in admin auth service let adminAuth = null; // Try multiple ways to get admin auth service try { adminAuth = strapi.admin?.services?.auth; } catch (e) { // Ignore } if (!adminAuth) { try { adminAuth = strapi.plugin('admin')?.services?.auth; } catch (e) { // Ignore } } // Log what we found for debugging if (strapi.log && !adminAuth) { strapi.log.debug('[lokalise-sync] Admin auth service structure check:', { hasAdmin: !!strapi.admin, hasAdminServices: !!strapi.admin?.services, adminServicesKeys: strapi.admin?.services ? Object.keys(strapi.admin.services) : [], hasPluginAdmin: !!strapi.plugin('admin'), pluginAdminServicesKeys: strapi.plugin('admin')?.services ? Object.keys(strapi.plugin('admin').services) : [], }); } if (adminAuth && typeof adminAuth.authenticate === 'function') { if (strapi.log) { strapi.log.debug('[lokalise-sync] Attempting authentication via Strapi admin auth service...'); strapi.log.debug('[lokalise-sync] Admin auth service path:', adminAuth === strapi.admin?.services?.auth ? 'strapi.admin.services.auth' : 'plugin("admin").services.auth'); } try { const adminUser = await adminAuth.authenticate(ctx); if (adminUser) { ctx.state.admin = adminUser; ctx.state.user = adminUser.user || adminUser; if (strapi.log) { strapi.log.debug('[lokalise-sync] ✅ Admin authenticated successfully via Strapi auth service'); } return await next(); } } catch (authErr) { if (strapi.log) { strapi.log.debug('[lokalise-sync] Admin auth service authenticate() returned null or threw:', authErr.message || String(authErr)); } } } else { if (strapi.log) { strapi.log.debug('[lokalise-sync] Admin auth service not available:', { hasAdminServices: !!strapi.admin?.services, hasAdminAuth: !!(strapi.admin?.services?.auth), hasPluginAdmin: !!strapi.plugin('admin'), hasPluginAuth: !!(strapi.plugin('admin')?.services?.auth), }); strapi.log.debug('[lokalise-sync] Trying fallback method...'); } } } catch (err) { // Strapi throws when the token is missing/invalid → continue to fallback if (strapi.log) { strapi.log.debug('[lokalise-sync] Error accessing admin auth service, trying fallback:', err.message || String(err)); } } // 3. Fallback — manually verify admin token using Strapi's token service try { const tokenService = strapi.admin?.services?.token || strapi.plugin('admin')?.services?.token; if (!tokenService) { if (strapi.log) { strapi.log.warn('[lokalise-sync] Admin token service not available'); } ctx.status = 401; ctx.body = { error: { status: 401, name: 'UnauthorizedError', message: 'Strapi admin authentication required. Please log in to Strapi admin.', details: {}, }, }; return; } // For Strapi v4: cookies/session are PRIMARY (admin uses session-based auth) // For Strapi v5: Authorization header is PRIMARY (admin uses token-based auth) // Priority: Cookies first (v4), then Authorization header (v5), then fallback let token = null; // PRIORITY 1: Check cookies first (Strapi v4 uses session cookies) const possibleCookieNames = ['token', 'strapi.token', 'strapi-token', 'jwt', 'jwtToken', 'strapi-jwt', 'strapi_jwt']; for (const cookieName of possibleCookieNames) { if (ctx.cookies && typeof ctx.cookies.get === 'function') { const cookieValue = ctx.cookies.get(cookieName); if (cookieValue && cookieValue.length > 10 && cookieValue !== 'null' && cookieValue !== 'undefined') { token = cookieValue; if (strapi.log) { strapi.log.debug(`[lokalise-sync] ✅ Token found in cookie "${cookieName}" (Strapi v4 session-based auth)`); } break; } } } // PRIORITY 2: Check Authorization header (Strapi v5/useFetchClient method) if (!token) { // CRITICAL: Strapi v4 uses ctx.request.headers, v5 uses ctx.headers - check BOTH! const authHeader = ctx.headers?.authorization || ctx.request?.headers?.authorization || null; if (authHeader && typeof authHeader === 'string' && authHeader.startsWith('Bearer ')) { const headerToken = authHeader.replace('Bearer ', '').trim(); // Only use if it's not "null", not empty, and has valid length if (headerToken && headerToken !== 'null' && headerToken.length > 10 && headerToken !== 'undefined') { token = headerToken; if (strapi.log) { strapi.log.debug('[lokalise-sync] ✅ Token found in Authorization header (Strapi v5/useFetchClient method)'); strapi.log.debug('[lokalise-sync] Token source:', ctx.headers?.authorization ? 'ctx.headers' : 'ctx.request.headers'); strapi.log.debug('[lokalise-sync] Token length:', token.length); strapi.log.debug('[lokalise-sync] Token preview:', token.substring(0, 20) + '...'); } } else if (strapi.log) { // Log why token was rejected strapi.log.debug('[lokalise-sync] ⚠️ Authorization header exists but token is invalid:', { headerExists: !!authHeader, headerStartsWithBearer: authHeader?.startsWith('Bearer ') || false, tokenAfterBearer: headerToken ? headerToken.substring(0, 20) + '...' : '(empty)', tokenLength: headerToken ? headerToken.length : 0, isNull: headerToken === 'null', isUndefined: headerToken === 'undefined', isValidLength: headerToken ? headerToken.length > 10 : false, }); } } } if (!token) { if (strapi.log) { strapi.log.warn('[lokalise-sync] ========== NO ADMIN TOKEN FOUND =========='); strapi.log.warn('[lokalise-sync] Request path: ' + (ctx.path || 'unknown')); strapi.log.warn('[lokalise-sync] Cookie "token" exists: ' + !!ctx.cookies?.get('token')); // Check BOTH locations (v4 uses request.headers, v5 uses headers) const authHeaderV4 = ctx.request?.headers?.authorization; const authHeaderV5 = ctx.headers?.authorization; strapi.log.warn('[lokalise-sync] Authorization header (ctx.request.headers): ' + (authHeaderV4 ? 'EXISTS' : 'MISSING')); strapi.log.warn('[lokalise-sync] Authorization header (ctx.headers): ' + (authHeaderV5 ? 'EXISTS' : 'MISSING')); strapi.log.warn('[lokalise-sync] Authorization header exists (either): ' + !!(authHeaderV4 || authHeaderV5)); // List ALL cookies to help debug const cookieHeader = ctx.request.headers.cookie || ''; strapi.log.warn('[lokalise-sync] Cookie header length: ' + (cookieHeader ? String(cookieHeader.length) : '0 (EMPTY!)')); if (cookieHeader && cookieHeader.length > 0) { strapi.log.warn('[lokalise-sync] Cookie header preview: ' + cookieHeader.substring(0, 300)); const cookieNames = cookieHeader.split(';').map(c => { const trimmed = c.trim(); const name = trimmed.split('=')[0]; return name; }).filter(name => name && name.length > 0); const cookieNamesStr = cookieNames.length > 0 ? cookieNames.join(', ') : '(NONE)'; strapi.log.warn('[lokalise-sync] All cookie names found (' + cookieNames.length + '): ' + cookieNamesStr); // Check for Clerk cookies (conflict indicator) const clerkCookies = cookieNames.filter(name => name.toLowerCase().includes('clerk') || name.includes('__clerk')); if (clerkCookies.length > 0) { const clerkCookiesStr = clerkCookies.join(', '); strapi.log.error('[lokalise-sync] ⚠️ CLERK COOKIES DETECTED - This may be blocking Strapi admin cookie!'); strapi.log.error('[lokalise-sync] ⚠️ Clerk cookies found (' + clerkCookies.length + '): ' + clerkCookiesStr); strapi.log.error('[lokalise-sync] ⚠️ SOLUTION: Disable Clerk for localhost:1337 or change Strapi admin cookie name'); } // Check if Strapi admin cookie is missing const hasTokenCookie = cookieNames.some(name => name === 'token' || name.toLowerCase() === 'token'); if (!hasTokenCookie) { strapi.log.error('[lokalise-sync] ❌ Strapi admin "token" cookie is MISSING!'); strapi.log.error('[lokalise-sync] ❌ This is required for plugin authentication to work.'); strapi.log.error('[lokalise-sync] ❌ Available cookies: ' + cookieNamesStr); strapi.log.error('[lokalise-sync] ❌ SOLUTION: Log out and log back in to Strapi admin to create the cookie.'); strapi.log.error('[lokalise-sync] ❌ SOLUTION: If Clerk is installed, disable it for localhost:1337'); strapi.log.error('[lokalise-sync] ❌ SOLUTION: Check browser DevTools > Application > Cookies > localhost:1337 for "token" cookie'); } else { strapi.log.warn('[lokalise-sync] ✅ Strapi "token" cookie found in cookie names list'); } } else { strapi.log.warn('[lokalise-sync] ❌ No cookies found in request header (cookie header is empty or missing)'); strapi.log.warn('[lokalise-sync] ❌ This means the browser is not sending any cookies to Strapi'); strapi.log.warn('[lokalise-sync] ❌ SOLUTION: Check browser cookie settings - cookies may be blocked'); } // Try ctx.cookies.getAll if available if (ctx.cookies && typeof ctx.cookies.getAll === 'function') { try { const allCookies = ctx.cookies.getAll(); strapi.log.warn('[lokalise-sync] All cookies (via getAll):', JSON.stringify(allCookies)); } catch (e) { strapi.log.warn('[lokalise-sync] Could not get all cookies:', e.message); } } strapi.log.warn('[lokalise-sync] ❌ SOLUTION: Please log in to Strapi admin panel.'); strapi.log.warn('[lokalise-sync] ❌ SOLUTION: Check browser DevTools > Application > Cookies for "token" cookie.'); strapi.log.warn('[lokalise-sync] ==========================================='); } // Create user-friendly error message let errorMessage = 'Strapi admin authentication required. Please log in to Strapi admin.'; let errorDetails = {}; // Check if external auth (like Clerk) is interfering const cookieHeader = ctx.request.headers.cookie || ''; if (cookieHeader) { const cookieNames = cookieHeader.split(';').map(c => { const trimmed = c.trim(); return trimmed.split('=')[0]; }).filter(name => name && name.length > 0); const hasClerkCookies = cookieNames.some(name => name.toLowerCase().includes('clerk') || name.includes('__clerk') || name.includes('clerk_db') ); const hasTokenCookie = cookieNames.some(name => name === 'token' || name.toLowerCase() === 'token'); if (hasClerkCookies && !hasTokenCookie) { errorMessage = 'External authentication detected (Clerk/Supabase/Auth0). The Strapi admin token cookie is missing. This plugin requires Strapi\'s native admin authentication. Please disable external authentication for the Strapi admin domain (localhost:1337).'; errorDetails = { detectedCookies: cookieNames, externalAuthDetected: true, strapiTokenMissing: true, solutions: [ 'Disable Clerk/external auth for localhost:1337', 'Use Strapi\'s native admin authentication instead', 'See TROUBLESHOOTING_CLERK_CONFLICT.md for detailed solutions' ], }; } } ctx.status = 401; ctx.body = { error: { status: 401, name: 'UnauthorizedError', message: errorMessage, details: errorDetails, }, }; return; } // Verify token using Strapi's token service if (strapi.log) { strapi.log.debug('[lokalise-sync] Verifying token using Strapi token service...'); strapi.log.debug('[lokalise-sync] Token length:', token.length); strapi.log.debug('[lokalise-sync] Token preview:', token.substring(0, 30) + '...'); } const payload = await tokenService.verify(token); if (!payload) { if (strapi.log) { strapi.log.warn('[lokalise-sync] Token verification returned null/undefined'); } ctx.status = 401; ctx.body = { error: { status: 401, name: 'UnauthorizedError', message: 'Invalid admin token. Please log in to Strapi admin again.', details: {}, }, }; return; } // Get admin user from database const userId = payload.id || payload.userId; if (!userId) { if (strapi.log) { strapi.log.warn('[lokalise-sync] Token payload missing user ID:', payload); } ctx.status = 401; ctx.body = { error: { status: 401, name: 'UnauthorizedError', message: 'Invalid token payload. Please log in to Strapi admin again.', details: {}, }, }; return; } const adminUser = await strapi.db.query('admin::user').findOne({ id: userId }); if (!adminUser || adminUser.isActive === false) { if (strapi.log) { strapi.log.warn('[lokalise-sync] Admin user not found or inactive:', userId); } ctx.status = 401; ctx.body = { error: { status: 401, name: 'UnauthorizedError', message: 'Admin user not found or inactive.', details: {}, }, }; return; } // Authentication successful ctx.state.admin = { user: adminUser }; ctx.state.user = adminUser; if (strapi.log) { strapi.log.debug('[lokalise-sync] ✅ Admin authenticated successfully via token service'); strapi.log.debug('[lokalise-sync] Admin user ID:', adminUser.id); } return await next(); } catch (err) { // Token verification failed if (strapi.log) { strapi.log.error('[lokalise-sync] ========== TOKEN VERIFICATION FAILED =========='); strapi.log.error('[lokalise-sync] Error name:', err?.name || 'Unknown'); strapi.log.error('[lokalise-sync] Error message:', err?.message || String(err) || 'Unknown error'); if (err?.stack) { strapi.log.error('[lokalise-sync] Error stack:', err.stack.substring(0, 500)); } if (err?.name === 'TokenExpiredError') { strapi.log.error('[lokalise-sync] ❌ DIAGNOSIS: Token has expired'); strapi.log.error('[lokalise-sync] ❌ SOLUTION: Log out and log back in to get a fresh token'); } else if (err?.name === 'JsonWebTokenError') { strapi.log.error('[lokalise-sync] ❌ DIAGNOSIS: Token signature is invalid'); strapi.log.error('[lokalise-sync] ❌ SOLUTION: Token was signed with different secret. Log out and log back in.'); } else { strapi.log.error('[lokalise-sync] ❌ DIAGNOSIS: Unknown token verification error'); } strapi.log.error('[lokalise-sync] ============================================='); } ctx.status = 401; ctx.body = { error: { status: 401, name: 'UnauthorizedError', message: 'Admin token verification failed. Please log in to Strapi admin again.', details: { error: err?.name || 'TokenVerificationError', message: err?.message || String(err) || 'Unknown error', }, }, }; } }; };