mirror of
https://github.com/lobehub/lobehub.git
synced 2026-03-26 13:19:34 +07:00
🐛 fix: webhook user service compatibility for old nextauth users (#11826)
This commit is contained in:
@@ -15,6 +15,7 @@ const MIGRATION_DOC_BASE = 'https://lobehub.com/docs/self-hosting/advanced/auth'
|
||||
* message: string;
|
||||
* docUrl?: string;
|
||||
* formatVar?: (envVar: string) => string;
|
||||
* severity?: 'error' | 'warning';
|
||||
* }>}
|
||||
*/
|
||||
const DEPRECATED_CHECKS = [
|
||||
@@ -144,8 +145,9 @@ const DEPRECATED_CHECKS = [
|
||||
return [];
|
||||
},
|
||||
message:
|
||||
'Casdoor webhook is required for syncing user data (email, avatar, etc.) to LobeChat. Without it, users without email configured in Casdoor cannot login. Please configure CASDOOR_WEBHOOK_SECRET following the documentation.',
|
||||
'Casdoor webhook is recommended for syncing user data (email, avatar, etc.) to LobeChat. This is especially important for users migrating from NextAuth to Better Auth - users without email configured in Casdoor will not be able to login. Consider configuring CASDOOR_WEBHOOK_SECRET following the documentation.',
|
||||
name: 'Casdoor Webhook',
|
||||
severity: 'warning',
|
||||
},
|
||||
{
|
||||
docUrl: `${MIGRATION_DOC_BASE}/providers/logto`,
|
||||
@@ -157,8 +159,9 @@ const DEPRECATED_CHECKS = [
|
||||
return [];
|
||||
},
|
||||
message:
|
||||
'Logto webhook is required for syncing user data (email, avatar, etc.) to LobeChat. Without it, users without email configured in Logto cannot login. Please configure LOGTO_WEBHOOK_SIGNING_KEY following the documentation.',
|
||||
'Logto webhook is recommended for syncing user data (email, avatar, etc.) to LobeChat. This is especially important for users migrating from NextAuth to Better Auth - users without email configured in Logto will not be able to login. Consider configuring LOGTO_WEBHOOK_SIGNING_KEY following the documentation.',
|
||||
name: 'Logto Webhook',
|
||||
severity: 'warning',
|
||||
},
|
||||
{
|
||||
docUrl: `${MIGRATION_DOC_BASE}/nextauth-to-betterauth`,
|
||||
@@ -185,18 +188,22 @@ const DEPRECATED_CHECKS = [
|
||||
];
|
||||
|
||||
/**
|
||||
* Print a single deprecation error block
|
||||
* Print a single deprecation block (error or warning)
|
||||
*/
|
||||
function printErrorBlock(name, vars, message, docUrl, formatVar) {
|
||||
console.error(`\n❌ ${name}`);
|
||||
console.error('─'.repeat(50));
|
||||
console.error('Detected deprecated environment variables:');
|
||||
function printIssueBlock(name, vars, message, docUrl, formatVar, severity = 'error') {
|
||||
const isWarning = severity === 'warning';
|
||||
const icon = isWarning ? '⚠️' : '❌';
|
||||
const log = isWarning ? console.warn : console.error;
|
||||
|
||||
log(`\n${icon} ${name}`);
|
||||
log('─'.repeat(50));
|
||||
log(isWarning ? 'Missing recommended environment variables:' : 'Detected deprecated environment variables:');
|
||||
for (const envVar of vars) {
|
||||
console.error(` • ${formatVar ? formatVar(envVar) : envVar}`);
|
||||
log(` • ${formatVar ? formatVar(envVar) : envVar}`);
|
||||
}
|
||||
console.error(`\n${message}`);
|
||||
log(`\n${message}`);
|
||||
if (docUrl) {
|
||||
console.error(`📖 Migration guide: ${docUrl}`);
|
||||
log(`📖 Documentation: ${docUrl}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -208,23 +215,46 @@ function printErrorBlock(name, vars, message, docUrl, formatVar) {
|
||||
function checkDeprecatedAuth(options = {}) {
|
||||
const { action = 'redeploy' } = options;
|
||||
|
||||
const foundIssues = [];
|
||||
const errors = [];
|
||||
const warnings = [];
|
||||
|
||||
for (const check of DEPRECATED_CHECKS) {
|
||||
const foundVars = check.getVars();
|
||||
if (foundVars.length > 0) {
|
||||
foundIssues.push({ ...check, foundVars });
|
||||
const issue = { ...check, foundVars };
|
||||
if (check.severity === 'warning') {
|
||||
warnings.push(issue);
|
||||
} else {
|
||||
errors.push(issue);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (foundIssues.length > 0) {
|
||||
// Print warnings (non-blocking)
|
||||
if (warnings.length > 0) {
|
||||
console.warn('\n' + '═'.repeat(70));
|
||||
console.warn(`⚠️ WARNING: Found ${warnings.length} recommended configuration(s) missing`);
|
||||
console.warn('═'.repeat(70));
|
||||
|
||||
for (const issue of warnings) {
|
||||
printIssueBlock(issue.name, issue.foundVars, issue.message, issue.docUrl, issue.formatVar, 'warning');
|
||||
}
|
||||
|
||||
console.warn('\n' + '═'.repeat(70));
|
||||
console.warn('These are recommendations. Your application will still run.');
|
||||
console.warn('═'.repeat(70) + '\n');
|
||||
}
|
||||
|
||||
// Print errors and exit (blocking)
|
||||
if (errors.length > 0) {
|
||||
console.error('\n' + '═'.repeat(70));
|
||||
console.error(
|
||||
`❌ ERROR: Found ${foundIssues.length} deprecated environment variable issue(s)!`,
|
||||
`❌ ERROR: Found ${errors.length} deprecated environment variable issue(s)!`,
|
||||
);
|
||||
console.error('═'.repeat(70));
|
||||
|
||||
for (const issue of foundIssues) {
|
||||
printErrorBlock(issue.name, issue.foundVars, issue.message, issue.docUrl, issue.formatVar);
|
||||
for (const issue of errors) {
|
||||
printIssueBlock(issue.name, issue.foundVars, issue.message, issue.docUrl, issue.formatVar, 'error');
|
||||
}
|
||||
|
||||
console.error('\n' + '═'.repeat(70));
|
||||
|
||||
180
scripts/_shared/checkDeprecatedAuth.test.ts
Normal file
180
scripts/_shared/checkDeprecatedAuth.test.ts
Normal file
@@ -0,0 +1,180 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
// Store original env
|
||||
const originalEnv = { ...process.env };
|
||||
|
||||
// Mock process.exit to prevent actual exit
|
||||
const mockExit = vi.spyOn(process, 'exit').mockImplementation(() => undefined as never);
|
||||
|
||||
// Mock console methods
|
||||
const mockConsoleError = vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||
const mockConsoleWarn = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
||||
|
||||
describe('checkDeprecatedAuth', () => {
|
||||
beforeEach(() => {
|
||||
// Reset env before each test
|
||||
process.env = { ...originalEnv };
|
||||
vi.clearAllMocks();
|
||||
// Clear module cache to ensure fresh import
|
||||
vi.resetModules();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// Restore original env
|
||||
process.env = originalEnv;
|
||||
});
|
||||
|
||||
it('should not exit when no deprecated env vars are set', async () => {
|
||||
const { checkDeprecatedAuth } = await import('./checkDeprecatedAuth.js');
|
||||
checkDeprecatedAuth();
|
||||
|
||||
expect(mockExit).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should exit with code 1 when NextAuth env vars are detected', async () => {
|
||||
process.env.NEXT_AUTH_SECRET = 'test-secret';
|
||||
|
||||
const { checkDeprecatedAuth } = await import('./checkDeprecatedAuth.js');
|
||||
checkDeprecatedAuth();
|
||||
|
||||
expect(mockExit).toHaveBeenCalledWith(1);
|
||||
expect(mockConsoleError).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should exit with code 1 when NEXTAUTH env vars are detected', async () => {
|
||||
process.env.NEXTAUTH_SECRET = 'test-secret';
|
||||
|
||||
const { checkDeprecatedAuth } = await import('./checkDeprecatedAuth.js');
|
||||
checkDeprecatedAuth();
|
||||
|
||||
expect(mockExit).toHaveBeenCalledWith(1);
|
||||
});
|
||||
|
||||
it('should exit with code 1 when Clerk env vars are detected', async () => {
|
||||
process.env.CLERK_SECRET_KEY = 'test-key';
|
||||
|
||||
const { checkDeprecatedAuth } = await import('./checkDeprecatedAuth.js');
|
||||
checkDeprecatedAuth();
|
||||
|
||||
expect(mockExit).toHaveBeenCalledWith(1);
|
||||
});
|
||||
|
||||
it('should exit with code 1 when ACCESS_CODE is set', async () => {
|
||||
process.env.ACCESS_CODE = 'test-code';
|
||||
|
||||
const { checkDeprecatedAuth } = await import('./checkDeprecatedAuth.js');
|
||||
checkDeprecatedAuth();
|
||||
|
||||
expect(mockExit).toHaveBeenCalledWith(1);
|
||||
});
|
||||
|
||||
it('should exit with code 1 when APP_URL has trailing slash', async () => {
|
||||
process.env.APP_URL = 'https://example.com/';
|
||||
|
||||
const { checkDeprecatedAuth } = await import('./checkDeprecatedAuth.js');
|
||||
checkDeprecatedAuth();
|
||||
|
||||
expect(mockExit).toHaveBeenCalledWith(1);
|
||||
});
|
||||
|
||||
it('should not exit when APP_URL has no trailing slash', async () => {
|
||||
process.env.APP_URL = 'https://example.com';
|
||||
|
||||
const { checkDeprecatedAuth } = await import('./checkDeprecatedAuth.js');
|
||||
checkDeprecatedAuth();
|
||||
|
||||
expect(mockExit).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
describe('webhook warnings (non-blocking)', () => {
|
||||
it('should warn but not exit when Casdoor webhook is missing', async () => {
|
||||
process.env.AUTH_SSO_PROVIDERS = 'casdoor';
|
||||
// CASDOOR_WEBHOOK_SECRET is not set
|
||||
|
||||
const { checkDeprecatedAuth } = await import('./checkDeprecatedAuth.js');
|
||||
checkDeprecatedAuth();
|
||||
|
||||
expect(mockExit).not.toHaveBeenCalled();
|
||||
expect(mockConsoleWarn).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should warn but not exit when Logto webhook is missing', async () => {
|
||||
process.env.AUTH_SSO_PROVIDERS = 'logto';
|
||||
// LOGTO_WEBHOOK_SIGNING_KEY is not set
|
||||
|
||||
const { checkDeprecatedAuth } = await import('./checkDeprecatedAuth.js');
|
||||
checkDeprecatedAuth();
|
||||
|
||||
expect(mockExit).not.toHaveBeenCalled();
|
||||
expect(mockConsoleWarn).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not warn when Casdoor webhook is configured', async () => {
|
||||
process.env.AUTH_SSO_PROVIDERS = 'casdoor';
|
||||
process.env.CASDOOR_WEBHOOK_SECRET = 'test-secret';
|
||||
|
||||
const { checkDeprecatedAuth } = await import('./checkDeprecatedAuth.js');
|
||||
checkDeprecatedAuth();
|
||||
|
||||
expect(mockExit).not.toHaveBeenCalled();
|
||||
expect(mockConsoleWarn).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not warn when Logto webhook is configured', async () => {
|
||||
process.env.AUTH_SSO_PROVIDERS = 'logto';
|
||||
process.env.LOGTO_WEBHOOK_SIGNING_KEY = 'test-key';
|
||||
|
||||
const { checkDeprecatedAuth } = await import('./checkDeprecatedAuth.js');
|
||||
checkDeprecatedAuth();
|
||||
|
||||
expect(mockExit).not.toHaveBeenCalled();
|
||||
expect(mockConsoleWarn).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not warn when provider is not casdoor or logto', async () => {
|
||||
process.env.AUTH_SSO_PROVIDERS = 'google';
|
||||
|
||||
const { checkDeprecatedAuth } = await import('./checkDeprecatedAuth.js');
|
||||
checkDeprecatedAuth();
|
||||
|
||||
expect(mockExit).not.toHaveBeenCalled();
|
||||
expect(mockConsoleWarn).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('mixed errors and warnings', () => {
|
||||
it('should exit when there are errors even if there are also warnings', async () => {
|
||||
process.env.AUTH_SSO_PROVIDERS = 'logto'; // warning
|
||||
process.env.ACCESS_CODE = 'test-code'; // error
|
||||
|
||||
const { checkDeprecatedAuth } = await import('./checkDeprecatedAuth.js');
|
||||
checkDeprecatedAuth();
|
||||
|
||||
expect(mockExit).toHaveBeenCalledWith(1);
|
||||
expect(mockConsoleWarn).toHaveBeenCalled();
|
||||
expect(mockConsoleError).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('action parameter', () => {
|
||||
it('should use "redeploy" as default action', async () => {
|
||||
process.env.ACCESS_CODE = 'test-code';
|
||||
|
||||
const { checkDeprecatedAuth } = await import('./checkDeprecatedAuth.js');
|
||||
checkDeprecatedAuth();
|
||||
|
||||
const calls = mockConsoleError.mock.calls.flat().join(' ');
|
||||
expect(calls).toContain('redeploy');
|
||||
});
|
||||
|
||||
it('should use custom action when provided', async () => {
|
||||
process.env.ACCESS_CODE = 'test-code';
|
||||
|
||||
const { checkDeprecatedAuth } = await import('./checkDeprecatedAuth.js');
|
||||
checkDeprecatedAuth({ action: 'restart' });
|
||||
|
||||
const calls = mockConsoleError.mock.calls.flat().join(' ');
|
||||
expect(calls).toContain('restart');
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user