Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,4 @@ logs
demo/config.json
demo/index.html
docs/
node_demo/.token.json
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,15 @@ Simply call `handleAuthentication()` on every page that uses the library. This s
const isAuthenticated = await client.auth.handleAuthentication();
```

#### Manual Session Management

If you have obtained an access token (and refresh token) through other means (e.g., server-side authentication), you can manually set the session on the client.

```javascript
// Set the access token (and optional refresh token)
client.auth.setSession('YOUR_ACCESS_TOKEN', 'YOUR_REFRESH_TOKEN');
```

### 3. Fetch Data

Once authenticated, you can call any endpoint using `getData`. This method handles authentication headers, automatically follows S3 links if returned by the API, and provides metadata about the response.
Expand Down
129 changes: 129 additions & 0 deletions node_demo/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import { IRacingClient } from '../dist/index.js';
import * as readline from 'readline';
import { readFile, writeFile } from 'fs/promises';
import * as path from 'path';
import { fileURLToPath } from 'url';

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const TOKEN_FILE = path.join(__dirname, '.token.json');

// Parse CLI args
const args = process.argv.slice(2);
const argClientId = args[0];
const argRedirectUri = args[1];

// Configuration - normally you would load this from env or config file
// For this demo, we assume the user has a registered app with iRacing
// Use the demo credentials if available or placeholders
const CONFIG = {
clientId: argClientId || process.env.IRACING_CLIENT_ID || 'ChooseAClientId', // User must supply this
redirectUri:
argRedirectUri || process.env.IRACING_REDIRECT_URI || 'http://localhost:3000/callback', // User must supply this matching their app
};

const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});

const question = (query) => new Promise((resolve) => rl.question(query, resolve));

async function saveTokens(client) {
const tokens = {
accessToken: client.auth.accessToken,
refreshToken: client.auth.refreshToken,
};
try {
await writeFile(TOKEN_FILE, JSON.stringify(tokens, null, 2));
// console.log('Tokens saved to disk.');
} catch (err) {
console.error('Failed to save tokens:', err);
}
}

async function performAuth(client) {
// 1. Generate Auth URL
const authUrl = await client.auth.generateAuthUrl();
console.log('\nPlease open the following URL in your browser to authorize:');
console.log(authUrl);
console.log('\nAfter authorizing, you will be redirected to your Redirect URI.');
console.log(
'Copy the FULL URL you were redirected to (or just the code parameter) and paste it here.',
);

// 2. Get Code/URL from user
const callbackUrl = await question('\nPaste Callback URL or Code: ');

// 3. Exchange code for token
console.log('Exchanging code for token...');
await client.auth.handleCallback(callbackUrl.trim());

console.log('Authentication successful!');
console.log(`Access Token: ${client.auth.accessToken.substring(0, 10)}...`);

await saveTokens(client);
}

async function main() {
console.log('--- iRacing Data API Node.js Demo ---');

if (CONFIG.clientId === 'ChooseAClientId') {
console.log('Please set IRACING_CLIENT_ID and IRACING_REDIRECT_URI environment variables,');
console.log('or edit the CONFIG object in node_demo/index.js');
CONFIG.clientId = await question('Enter Client ID: ');
CONFIG.redirectUri = await question('Enter Redirect URI: ');
}

const client = new IRacingClient({
clientId: CONFIG.clientId,
redirectUri: CONFIG.redirectUri,
});

// Try to load saved tokens
try {
const savedData = await readFile(TOKEN_FILE, 'utf-8');
const tokens = JSON.parse(savedData);
if (tokens.accessToken) {
client.auth.setSession(tokens.accessToken, tokens.refreshToken);
console.log('Loaded saved tokens from .token.json');
}
} catch (_err) {
// No saved tokens or invalid file, proceed to auth
console.log('No saved tokens found.');
}

try {
if (!client.auth.isLoggedIn) {
await performAuth(client);
}

console.log('\nFetching /data/member/info...');

// We wrap this in a loop or retry logic effectively
let result;
try {
result = await client.getData('member/info');
} catch (err) {
if (err.status === 401) {
console.log('Saved token is invalid or expired (and refresh failed). Re-authenticating...');
await performAuth(client);
result = await client.getData('member/info');
} else {
throw err;
}
}

// Save tokens again in case they were refreshed during getData
await saveTokens(client);

console.log('Data received!');
console.log('Member Info:', JSON.stringify(result.data, null, 2));
} catch (error) {
console.error('An error occurred:', error);
} finally {
rl.close();
}
}

main();
10 changes: 10 additions & 0 deletions node_demo/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"name": "irdata_js_demo",
"version": "0.1.0",
"description": "Node.js demo for irdata_js",
"type": "module",
"main": "index.js",
"scripts": {
"start": "node index.js"
}
}
28 changes: 28 additions & 0 deletions src/auth/AuthManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ export class AuthManager {
private config: AuthConfig;
private authBaseUrl: string;
private tokenEndpoint?: string;
private pkceVerifier: string | null = null;

constructor(
config: AuthConfig,
Expand All @@ -89,6 +90,21 @@ export class AuthManager {
return this.tokenStore.getAccessToken();
}

get refreshToken(): string | null {
return this.tokenStore.getRefreshToken();
}

/**
* Manually sets the session tokens.
* Useful for loading a saved session from external storage.
*/
setSession(accessToken: string, refreshToken?: string) {
this.tokenStore.setAccessToken(accessToken);
if (refreshToken) {
this.tokenStore.setRefreshToken(refreshToken);
}
}

/**
* Returns true if an access token is present.
*/
Expand Down Expand Up @@ -158,6 +174,8 @@ export class AuthManager {
// Store verifier for the callback
if (typeof window !== 'undefined' && window.sessionStorage) {
window.sessionStorage.setItem('irdata_pkce_verifier', verifier);
} else {
this.pkceVerifier = verifier;
}

const params = new URLSearchParams({
Expand Down Expand Up @@ -199,6 +217,11 @@ export class AuthManager {
}
} catch {
// Not a valid URL, assume it's the code itself or handled below
// Fallback: try to extract code via regex if URL parsing failed
const match = input.match(/[?&]code=([\w-]+)/);
if (match) {
code = match[1];
}
}
}

Expand All @@ -208,6 +231,11 @@ export class AuthManager {
window.sessionStorage.removeItem('irdata_pkce_verifier');
}

if (!verifier && this.pkceVerifier) {
verifier = this.pkceVerifier;
this.pkceVerifier = null;
}

if (!verifier) {
throw new Error('No PKCE verifier found');
}
Expand Down
14 changes: 14 additions & 0 deletions tests/AuthManager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,20 @@ describe('AuthManager', () => {
vi.unstubAllGlobals();
});

it('should allow setting session manually and retrieving refresh token', () => {
auth.setSession('manual-access-token', 'manual-refresh-token');
expect(auth.accessToken).toBe('manual-access-token');
expect(auth.refreshToken).toBe('manual-refresh-token');
expect(auth.isLoggedIn).toBe(true);
});

it('should allow setting session without refresh token', () => {
auth.setSession('only-access-token');
expect(auth.accessToken).toBe('only-access-token');
expect(auth.refreshToken).toBeNull();
expect(auth.isLoggedIn).toBe(true);
});

it('should use proxy authBaseUrl if provided', async () => {
const proxyConfig = { authBaseUrl: 'https://proxy.com/auth' };
auth = new AuthManager(config, proxyConfig);
Expand Down