strapi-to-lokalise-plugin
Version:
Preview and sync Lokalise translations from Strapi admin
496 lines (448 loc) • 22.1 kB
JavaScript
;
/**
* 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',
},
},
};
}
};
};