Skip to content

Commit c40ba8f

Browse files
committed
fix(auth): resolve login regression by reading .env auth state robustly
1 parent 3768a6f commit c40ba8f

1 file changed

Lines changed: 54 additions & 27 deletions

File tree

src/lib/auth.ts

Lines changed: 54 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -10,40 +10,50 @@ const DEFAULT_JWT_EXPIRY_DAYS = 7; // Default 7 days
1010
// Cache for JWT secret to avoid multiple file reads
1111
let jwtSecretCache: string | null = null;
1212

13-
/**
14-
* Sync the in-memory process.env (and jwtSecretCache) with values just
15-
* written to the .env file. Keys not in the file are left untouched.
16-
*
17-
* This is required because Next.js routes read from process.env at request
18-
* time, and dotenv is only loaded once at server startup. Without this,
19-
* writes via updateAuthCredentials/enabled/etc. are visible on disk but
20-
* the running server keeps returning the pre-write values until restart.
21-
*/
22-
export function syncProcessEnvFromFile(): void {
13+
function parseEnvFile(): Record<string, string> {
2314
const envPath = path.join(process.cwd(), '.env');
24-
if (!fs.existsSync(envPath)) return;
15+
if (!fs.existsSync(envPath)) return {};
2516

2617
const envContent = fs.readFileSync(envPath, 'utf8');
27-
const updates: Record<string, string> = {};
18+
const values: Record<string, string> = {};
2819

2920
for (const line of envContent.split(/\r?\n/)) {
3021
const trimmed = line.trim();
3122
if (!trimmed || trimmed.startsWith('#')) continue;
23+
3224
const eqIdx = trimmed.indexOf('=');
3325
if (eqIdx === -1) continue;
26+
3427
const key = trimmed.slice(0, eqIdx).trim();
3528
let value = trimmed.slice(eqIdx + 1).trim();
36-
// Strip a single pair of surrounding double or single quotes
29+
3730
if (
3831
value.length >= 2 &&
3932
((value.startsWith('"') && value.endsWith('"')) ||
4033
(value.startsWith("'") && value.endsWith("'")))
4134
) {
4235
value = value.slice(1, -1);
4336
}
44-
if (key) updates[key] = value;
37+
38+
if (key) values[key] = value;
4539
}
4640

41+
return values;
42+
}
43+
44+
/**
45+
* Sync the in-memory process.env (and jwtSecretCache) with values just
46+
* written to the .env file. Keys not in the file are left untouched.
47+
*
48+
* This is required because Next.js routes read from process.env at request
49+
* time, and dotenv is only loaded once at server startup. Without this,
50+
* writes via updateAuthCredentials/enabled/etc. are visible on disk but
51+
* the running server keeps returning the pre-write values until restart.
52+
*/
53+
export function syncProcessEnvFromFile(): void {
54+
const updates = parseEnvFile();
55+
if (Object.keys(updates).length === 0) return;
56+
4757
for (const [key, value] of Object.entries(updates)) {
4858
process.env[key] = value;
4959
}
@@ -71,6 +81,13 @@ export function getJwtSecret(): string {
7181
return jwtSecretCache;
7282
}
7383

84+
const fileSecret = parseEnvFile().JWT_SECRET?.trim();
85+
if (fileSecret) {
86+
process.env.JWT_SECRET = fileSecret;
87+
jwtSecretCache = fileSecret;
88+
return jwtSecretCache;
89+
}
90+
7491
// No secret in process.env — generate and persist a new one
7592
const newSecret = randomBytes(64).toString('hex');
7693

@@ -150,11 +167,21 @@ export function getAuthConfig(): {
150167
setupCompleted: boolean;
151168
sessionDurationDays: number;
152169
} {
153-
const username = process.env.AUTH_USERNAME?.trim() ?? null;
154-
const passwordHash = process.env.AUTH_PASSWORD_HASH?.trim() ?? null;
155-
const enabled = (process.env.AUTH_ENABLED?.toLowerCase() ?? '') === 'true';
156-
const setupCompleted = (process.env.AUTH_SETUP_COMPLETED?.toLowerCase() ?? '') === 'true';
157-
const parsed = parseInt(process.env.AUTH_SESSION_DURATION_DAYS ?? '', 10);
170+
const fileValues = parseEnvFile();
171+
172+
const usernameSource = fileValues.AUTH_USERNAME ?? process.env.AUTH_USERNAME ?? '';
173+
const passwordHashSource = fileValues.AUTH_PASSWORD_HASH ?? process.env.AUTH_PASSWORD_HASH ?? '';
174+
const enabledSource = fileValues.AUTH_ENABLED ?? process.env.AUTH_ENABLED ?? '';
175+
const setupCompletedSource =
176+
fileValues.AUTH_SETUP_COMPLETED ?? process.env.AUTH_SETUP_COMPLETED ?? '';
177+
const sessionDurationSource =
178+
fileValues.AUTH_SESSION_DURATION_DAYS ?? process.env.AUTH_SESSION_DURATION_DAYS ?? '';
179+
180+
const username = usernameSource.trim() || null;
181+
const passwordHash = passwordHashSource.trim() || null;
182+
const enabled = enabledSource.toLowerCase() === 'true';
183+
const setupCompleted = setupCompletedSource.toLowerCase() === 'true';
184+
const parsed = parseInt(sessionDurationSource, 10);
158185
const sessionDurationDays = Number.isNaN(parsed) ? DEFAULT_JWT_EXPIRY_DAYS : parsed;
159186
const hasCredentials = !!(username && passwordHash);
160187

@@ -269,14 +296,14 @@ export function updateAuthEnabled(enabled: boolean): void {
269296
envContent = envContent.replace(enabledRegex, `AUTH_ENABLED=${enabled}`);
270297
} else {
271298
envContent += (envContent.endsWith('\n') ? '' : '\n') + `AUTH_ENABLED=${enabled}\n`;
272-
273-
// Refresh in-memory process.env so subsequent getAuthConfig() reads see
274-
// the updated AUTH_ENABLED flag without a restart.
275-
syncProcessEnvFromFile();
276299
}
277300

278301
// Write back to .env file
279302
fs.writeFileSync(envPath, envContent);
303+
304+
// Refresh in-memory process.env so subsequent getAuthConfig() reads see
305+
// the updated AUTH_ENABLED flag without a restart.
306+
syncProcessEnvFromFile();
280307
}
281308

282309
/**
@@ -300,12 +327,12 @@ export function updateSessionDuration(days: number): void {
300327
envContent = envContent.replace(sessionDurationRegex, `AUTH_SESSION_DURATION_DAYS=${validDays}`);
301328
} else {
302329
envContent += (envContent.endsWith('\n') ? '' : '\n') + `AUTH_SESSION_DURATION_DAYS=${validDays}\n`;
303-
304-
// Refresh in-memory process.env so subsequent getAuthConfig() reads see
305-
// the updated session duration without a restart.
306-
syncProcessEnvFromFile();
307330
}
308331

309332
// Write back to .env file
310333
fs.writeFileSync(envPath, envContent);
334+
335+
// Refresh in-memory process.env so subsequent getAuthConfig() reads see
336+
// the updated session duration without a restart.
337+
syncProcessEnvFromFile();
311338
}

0 commit comments

Comments
 (0)