Skip to content

Commit be0a933

Browse files
committed
Add JWT authentication support alongside existing API key auth
1 parent a1b992a commit be0a933

File tree

5 files changed

+162
-70
lines changed

5 files changed

+162
-70
lines changed

public/js/api-client.js

Lines changed: 46 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -128,15 +128,18 @@ export class ApiClient {
128128
url.searchParams.append('limit', limit.toString());
129129
url.searchParams.append('offset', offset.toString());
130130

131-
// Get API key from localStorage
131+
// Get authentication tokens from localStorage
132+
const jwtToken = localStorage.getItem('jwt_token');
132133
const apiKey = localStorage.getItem('api_key');
133134

134135
const headers = {
135136
'Accept': 'application/json',
136137
};
137138

138-
// Add Authorization header if API key exists
139-
if (apiKey) {
139+
// Add Authorization header with JWT token if available, otherwise use API key
140+
if (jwtToken) {
141+
headers['Authorization'] = `Bearer ${jwtToken}`;
142+
} else if (apiKey) {
140143
headers['Authorization'] = `Bearer ${apiKey}`;
141144
}
142145

@@ -205,15 +208,18 @@ export class ApiClient {
205208
* @private
206209
*/
207210
static async fetchBinaryResponse(url, body) {
208-
// Get API key from localStorage
211+
// Get authentication tokens from localStorage
212+
const jwtToken = localStorage.getItem('jwt_token');
209213
const apiKey = localStorage.getItem('api_key');
210214

211215
const headers = {
212216
'Content-Type': 'application/json',
213217
};
214218

215-
// Add Authorization header if API key exists
216-
if (apiKey) {
219+
// Add Authorization header with JWT token if available, otherwise use API key
220+
if (jwtToken) {
221+
headers['Authorization'] = `Bearer ${jwtToken}`;
222+
} else if (apiKey) {
217223
headers['Authorization'] = `Bearer ${apiKey}`;
218224
}
219225

@@ -239,15 +245,18 @@ export class ApiClient {
239245
* @private
240246
*/
241247
static async fetchJsonResponse(url, body) {
242-
// Get API key from localStorage
248+
// Get authentication tokens from localStorage
249+
const jwtToken = localStorage.getItem('jwt_token');
243250
const apiKey = localStorage.getItem('api_key');
244251

245252
const headers = {
246253
'Content-Type': 'application/json',
247254
};
248255

249-
// Add Authorization header if API key exists
250-
if (apiKey) {
256+
// Add Authorization header with JWT token if available, otherwise use API key
257+
if (jwtToken) {
258+
headers['Authorization'] = `Bearer ${jwtToken}`;
259+
} else if (apiKey) {
251260
headers['Authorization'] = `Bearer ${apiKey}`;
252261
}
253262

@@ -296,9 +305,14 @@ export class ApiClient {
296305
'Accept': 'application/json',
297306
};
298307

299-
// Add Authorization header if API key exists
308+
// Get authentication tokens from localStorage
309+
const jwtToken = localStorage.getItem('jwt_token');
300310
const apiKey = localStorage.getItem('api_key');
301-
if (apiKey) {
311+
312+
// Add Authorization header with JWT token if available, otherwise use API key
313+
if (jwtToken) {
314+
headers['Authorization'] = `Bearer ${jwtToken}`;
315+
} else if (apiKey) {
302316
headers['Authorization'] = `Bearer ${apiKey}`;
303317
}
304318

@@ -328,9 +342,14 @@ export class ApiClient {
328342
'Accept': 'application/json',
329343
};
330344

331-
// Add Authorization header if API key exists
345+
// Get authentication tokens from localStorage
346+
const jwtToken = localStorage.getItem('jwt_token');
332347
const apiKey = localStorage.getItem('api_key');
333-
if (apiKey) {
348+
349+
// Add Authorization header with JWT token if available, otherwise use API key
350+
if (jwtToken) {
351+
headers['Authorization'] = `Bearer ${jwtToken}`;
352+
} else if (apiKey) {
334353
headers['Authorization'] = `Bearer ${apiKey}`;
335354
}
336355

@@ -362,9 +381,14 @@ export class ApiClient {
362381
'Accept': 'application/json',
363382
};
364383

365-
// Add Authorization header if API key exists
384+
// Get authentication tokens from localStorage
385+
const jwtToken = localStorage.getItem('jwt_token');
366386
const apiKey = localStorage.getItem('api_key');
367-
if (apiKey) {
387+
388+
// Add Authorization header with JWT token if available, otherwise use API key
389+
if (jwtToken) {
390+
headers['Authorization'] = `Bearer ${jwtToken}`;
391+
} else if (apiKey) {
368392
headers['Authorization'] = `Bearer ${apiKey}`;
369393
}
370394

@@ -394,9 +418,14 @@ export class ApiClient {
394418
'Accept': 'application/json',
395419
};
396420

397-
// Add Authorization header if API key exists
421+
// Get authentication tokens from localStorage
422+
const jwtToken = localStorage.getItem('jwt_token');
398423
const apiKey = localStorage.getItem('api_key');
399-
if (apiKey) {
424+
425+
// Add Authorization header with JWT token if available, otherwise use API key
426+
if (jwtToken) {
427+
headers['Authorization'] = `Bearer ${jwtToken}`;
428+
} else if (apiKey) {
400429
headers['Authorization'] = `Bearer ${apiKey}`;
401430
}
402431

public/js/auth-ui.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ function logout() {
8585
// Clear authentication data
8686
localStorage.removeItem('api_key');
8787
localStorage.removeItem('username');
88+
localStorage.removeItem('jwt_token'); // Also clear JWT token
8889

8990
// Update UI
9091
updateNavbar();

public/js/main.js

Lines changed: 56 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -184,12 +184,34 @@ function initLoginPage() {
184184
// Import the API client
185185
const { ApiClient } = await import('./api-client.js');
186186

187-
// Check subscription status
187+
// Import Supabase client for JWT authentication
188+
const { createClient } = await import('https://cdn.jsdelivr.net/npm/@supabase/supabase-js@2/+esm');
189+
const supabaseUrl = 'https://arokhsfbkdnfuklmqajh.supabase.co'; // Should match server config
190+
const supabaseAnonKey = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImFyb2toc2Zia2RuZnVrbG1xYWpoIiwicm9sZSI6ImFub24iLCJpYXQiOjE2ODI0NTI4MDAsImV4cCI6MTk5ODAyODgwMH0.KxwHdxWXLLrJtFzLAYI-fwzgz8m5xsHD4XGdNw_xJm8'; // Public anon key
191+
const supabase = createClient(supabaseUrl, supabaseAnonKey);
192+
193+
// Sign in with Supabase to get JWT token
194+
const { data: authData, error: authError } = await supabase.auth.signInWithPassword({
195+
email,
196+
password
197+
});
198+
199+
if (authError) {
200+
console.error('Supabase auth error:', authError);
201+
// Continue with subscription check even if Supabase auth fails
202+
// This allows backward compatibility with existing users
203+
} else if (authData && authData.session) {
204+
// Store JWT token in localStorage
205+
localStorage.setItem('jwt_token', authData.session.access_token);
206+
console.log('JWT token stored successfully');
207+
}
208+
209+
// Check subscription status (existing flow)
188210
const subscriptionStatus = await ApiClient.checkSubscriptionStatus(email);
189211
console.log('Subscription status:', subscriptionStatus);
190212

191213
if (subscriptionStatus.has_subscription) {
192-
// User has an active subscription, store the email as an API key
214+
// User has an active subscription, store the email as an API key (for backward compatibility)
193215
localStorage.setItem('api_key', email);
194216
localStorage.setItem('username', email);
195217
localStorage.setItem('subscription_data', JSON.stringify(subscriptionStatus));
@@ -302,6 +324,34 @@ function initRegisterPage() {
302324
// Import the API client
303325
const { ApiClient } = await import('./api-client.js');
304326

327+
// Import Supabase client for JWT authentication
328+
const { createClient } = await import('https://cdn.jsdelivr.net/npm/@supabase/supabase-js@2/+esm');
329+
const supabaseUrl = 'https://arokhsfbkdnfuklmqajh.supabase.co'; // Should match server config
330+
const supabaseAnonKey = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImFyb2toc2Zia2RuZnVrbG1xYWpoIiwicm9sZSI6ImFub24iLCJpYXQiOjE2ODI0NTI4MDAsImV4cCI6MTk5ODAyODgwMH0.KxwHdxWXLLrJtFzLAYI-fwzgz8m5xsHD4XGdNw_xJm8'; // Public anon key
331+
const supabase = createClient(supabaseUrl, supabaseAnonKey);
332+
333+
// Register user with Supabase to get JWT token
334+
const { data: authData, error: authError } = await supabase.auth.signUp({
335+
email,
336+
password,
337+
options: {
338+
data: {
339+
plan: selectedPlan,
340+
payment_method: selectedPayment
341+
}
342+
}
343+
});
344+
345+
if (authError) {
346+
console.error('Supabase auth error:', authError);
347+
// Continue with subscription creation even if Supabase auth fails
348+
// This allows backward compatibility with existing users
349+
} else if (authData && authData.session) {
350+
// Store JWT token in localStorage
351+
localStorage.setItem('jwt_token', authData.session.access_token);
352+
console.log('JWT token stored successfully');
353+
}
354+
305355
// Create subscription using the API
306356
const subscriptionData = await ApiClient.createSubscription(email, selectedPlan, selectedPayment);
307357
console.log('Subscription created:', subscriptionData);
@@ -342,19 +392,19 @@ function initRegisterPage() {
342392
<div style="margin-bottom: 15px;">
343393
<label style="display: block; font-weight: 600; margin-bottom: 5px;">Amount:</label>
344394
<div style="display: flex; align-items: center;">
345-
<input type="text" value="${cryptoAmount} ${coin}" readonly
395+
<input type="text" value="${cryptoAmount} ${coin}" readonly
346396
style="flex: 1; padding: 8px; border: 1px solid #d1d5db; border-radius: 4px; background-color: #f9fafb;">
347-
<button onclick="navigator.clipboard.writeText('${cryptoAmount} ${coin}').then(() => this.textContent = 'Copied!'); setTimeout(() => this.textContent = 'Copy', 2000)"
397+
<button onclick="navigator.clipboard.writeText('${cryptoAmount} ${coin}').then(() => this.textContent = 'Copied!'); setTimeout(() => this.textContent = 'Copy', 2000)"
348398
style="margin-left: 8px; padding: 8px 12px; background-color: #f3f4f6; border: 1px solid #d1d5db; border-radius: 4px; cursor: pointer;">Copy</button>
349399
</div>
350400
</div>
351401
352402
<div style="margin-bottom: 15px;">
353403
<label style="display: block; font-weight: 600; margin-bottom: 5px;">Address:</label>
354404
<div style="display: flex; align-items: center;">
355-
<input type="text" value="${paymentAddress}" readonly
405+
<input type="text" value="${paymentAddress}" readonly
356406
style="flex: 1; padding: 8px; border: 1px solid #d1d5db; border-radius: 4px; background-color: #f9fafb;">
357-
<button onclick="navigator.clipboard.writeText('${paymentAddress}').then(() => this.textContent = 'Copied!'); setTimeout(() => this.textContent = 'Copy', 2000)"
407+
<button onclick="navigator.clipboard.writeText('${paymentAddress}').then(() => this.textContent = 'Copied!'); setTimeout(() => this.textContent = 'Copy', 2000)"
358408
style="margin-left: 8px; padding: 8px 12px; background-color: #f3f4f6; border: 1px solid #d1d5db; border-radius: 4px; cursor: pointer;">Copy</button>
359409
</div>
360410
</div>

src/middleware/auth-middleware.js

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { apiKeyService } from '../services/api-key-service.js';
22
import { errorUtils } from '../utils/error-utils.js';
3+
import { supabase, supabaseUtils } from '../utils/supabase.js';
34

45
/**
56
* Authentication middleware
@@ -28,15 +29,38 @@ export async function authMiddleware(c, next) {
2829

2930
// Check Authorization header (Bearer token)
3031
if (authHeader && authHeader.startsWith('Bearer ')) {
31-
const apiKey = authHeader.substring(7);
32-
user = await apiKeyService.validateApiKey(apiKey);
33-
}
32+
const token = authHeader.substring(7);
33+
34+
// First, try to validate as JWT token
35+
try {
36+
// Verify JWT token with Supabase
37+
const supabaseUser = await supabaseUtils.verifyJwtToken(token);
38+
39+
if (supabaseUser) {
40+
// Get user from database using email
41+
user = await apiKeyService._getUserByEmail(supabaseUser.email);
42+
43+
if (!user) {
44+
// Create user if not exists
45+
user = await apiKeyService._createUserIfNotExists(supabaseUser.email);
46+
}
47+
}
48+
} catch (jwtError) {
49+
console.log('JWT validation failed, trying as API key:', jwtError.message);
50+
}
51+
52+
// If JWT validation failed, try as API key
53+
if (!user) {
54+
// Try to validate as API key
55+
user = await apiKeyService.validateApiKey(token);
56+
}
57+
}
3458
// Check X-API-Key header (for backward compatibility)
3559
else if (apiKeyHeader) {
3660
// If it looks like an API key (starts with pfs_), validate it
3761
if (apiKeyHeader.startsWith('pfs_')) {
3862
user = await apiKeyService.validateApiKey(apiKeyHeader);
39-
}
63+
}
4064
// Otherwise, treat it as an email address (legacy behavior)
4165
else {
4266
const email = apiKeyHeader;
@@ -49,8 +73,8 @@ export async function authMiddleware(c, next) {
4973
}
5074

5175
if (!user) {
52-
return c.json({
53-
error: 'Unauthorized. Valid API key required.',
76+
return c.json({
77+
error: 'Unauthorized. Valid API key or JWT token required.',
5478
subscription_required: true,
5579
subscription_url: `${process.env.API_BASE_URL || 'https://pdf.profullstack.com'}/subscription`
5680
}, 401);

src/utils/supabase.js

Lines changed: 29 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,61 +1,28 @@
11
import { createClient } from '@supabase/supabase-js';
2-
import fs from 'fs';
2+
import dotenvFlow from 'dotenv-flow';
33
import path from 'path';
44
import { fileURLToPath } from 'url';
55

6-
/**
7-
* Read environment variables directly from .env file
8-
*/
9-
function readEnvFile() {
10-
try {
11-
const __dirname = path.dirname(fileURLToPath(import.meta.url));
12-
const envPath = path.resolve(__dirname, '../../.env');
13-
14-
if (fs.existsSync(envPath)) {
15-
const envContent = fs.readFileSync(envPath, 'utf8');
16-
const envVars = {};
17-
18-
envContent.split('\n').forEach(line => {
19-
// Skip comments and empty lines
20-
if (line.startsWith('#') || !line.trim()) return;
21-
22-
// Parse key=value pairs
23-
const match = line.match(/^([^=]+)=(.*)$/);
24-
if (match) {
25-
const key = match[1].trim();
26-
const value = match[2].trim();
27-
envVars[key] = value;
28-
}
29-
});
30-
31-
return envVars;
32-
}
33-
} catch (error) {
34-
console.error('Error reading .env file:', error);
35-
}
36-
37-
return {};
38-
}
39-
40-
const envVars = readEnvFile();
6+
// Load environment variables from .env files
7+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
8+
const rootDir = path.resolve(__dirname, '../../');
9+
dotenvFlow.config({ path: rootDir });
4110

4211
/**
4312
* Supabase configuration
4413
*/
45-
const supabaseUrl = envVars.SUPABASE_URL || process.env.SUPABASE_URL || 'https://arokhsfbkdnfuklmqajh.supabase.co';
14+
const supabaseUrl = process.env.SUPABASE_URL || 'https://arokhsfbkdnfuklmqajh.supabase.co';
4615
// Use service role key for server-side operations to bypass RLS
47-
const supabaseKey = envVars.SUPABASE_SERVICE_ROLE_KEY || process.env.SUPABASE_SERVICE_ROLE_KEY || envVars.SUPABASE_KEY || process.env.SUPABASE_KEY;
16+
const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY || process.env.SUPABASE_KEY;
4817

4918
// Log environment variables for debugging
5019
console.log('Supabase configuration:');
5120
console.log('SUPABASE_URL:', supabaseUrl);
5221
console.log('SUPABASE_KEY exists:', !!supabaseKey);
53-
console.log('SUPABASE_KEY from .env file:', !!envVars.SUPABASE_KEY);
5422
console.log('SUPABASE_KEY from process.env:', !!process.env.SUPABASE_KEY);
55-
console.log('SUPABASE_SERVICE_ROLE_KEY from .env file:', !!envVars.SUPABASE_SERVICE_ROLE_KEY);
5623
console.log('SUPABASE_SERVICE_ROLE_KEY from process.env:', !!process.env.SUPABASE_SERVICE_ROLE_KEY);
5724
console.log('SUPABASE_KEY length:', supabaseKey ? supabaseKey.length : 0);
58-
console.log('Using service role key:', !!(envVars.SUPABASE_SERVICE_ROLE_KEY || process.env.SUPABASE_SERVICE_ROLE_KEY));
25+
console.log('Using service role key:', !!process.env.SUPABASE_SERVICE_ROLE_KEY);
5926
console.log('NODE_ENV:', process.env.NODE_ENV);
6027

6128
// Log all environment variables for debugging (without showing sensitive values)
@@ -102,6 +69,27 @@ export const supabase = supabaseClient;
10269
* Utility functions for Supabase operations
10370
*/
10471
export const supabaseUtils = {
72+
/**
73+
* Verify a JWT token
74+
* @param {string} token - JWT token to verify
75+
* @returns {Promise<Object|null>} - User data if valid, null otherwise
76+
*/
77+
async verifyJwtToken(token) {
78+
try {
79+
const { data, error } = await supabase.auth.getUser(token);
80+
81+
if (error) {
82+
console.error('JWT verification error:', error);
83+
return null;
84+
}
85+
86+
return data.user;
87+
} catch (error) {
88+
console.error('Error verifying JWT token:', error);
89+
return null;
90+
}
91+
},
92+
10593
/**
10694
* Store a document in Supabase storage
10795
* @param {Buffer} buffer - Document buffer

0 commit comments

Comments
 (0)