-
Notifications
You must be signed in to change notification settings - Fork 41
feat: Add comprehensive Pages API support with session authentication #43
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: canary
Are you sure you want to change the base?
Changes from 3 commits
d86df96
0c5fdf5
7a31baf
1b22916
28615e9
4608a5a
974fae4
01cfbe2
8a396c6
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,192 @@ | ||
| import axios, { AxiosInstance } from "axios"; | ||
| import { wrapper } from "axios-cookiejar-support"; | ||
| import { CookieJar } from "tough-cookie"; | ||
| import fs from "fs"; | ||
| import path from "path"; | ||
|
|
||
| const logFile = path.join("/tmp", "plane-mcp-debug.log"); | ||
| function debugLog(message: string) { | ||
| const timestamp = new Date().toISOString(); | ||
| const logMessage = `[${timestamp}] ${message}\n`; | ||
| fs.appendFileSync(logFile, logMessage); | ||
| console.error(message); | ||
| } | ||
|
|
||
| /** | ||
| * Result of an authentication attempt | ||
| * @property success - Whether authentication was successful | ||
| * @property error - Type of error if authentication failed | ||
| * @property message - Detailed error message if authentication failed | ||
| */ | ||
| export interface AuthResult { | ||
| success: boolean; | ||
| error?: 'network' | 'csrf' | 'credentials' | 'cookies' | 'unknown'; | ||
| message?: string; | ||
| } | ||
|
|
||
| let axiosInstance: AxiosInstance | null = null; | ||
| let isAuthenticated = false; | ||
|
|
||
| debugLog(`[AUTH] Module loaded - PID: ${process.pid}`); | ||
|
|
||
| /** | ||
| * Gets or creates an Axios instance with cookie jar support for session authentication | ||
| * @returns Configured Axios instance with cookie persistence | ||
| */ | ||
| export function getAxiosInstance(): AxiosInstance { | ||
| if (!axiosInstance) { | ||
| debugLog("[AUTH] Creating new axios instance with cookie jar"); | ||
| const jar = new CookieJar(); | ||
| axiosInstance = wrapper(axios.create({ jar, withCredentials: true })); | ||
| } else { | ||
| debugLog("[AUTH] Reusing existing axios instance"); | ||
| } | ||
| return axiosInstance; | ||
| } | ||
|
|
||
| /** | ||
| * Authenticates with Plane using email and password, establishing a session with cookies | ||
| * | ||
| * This function performs a two-step authentication flow: | ||
| * 1. Requests a CSRF token from the server | ||
| * 2. Submits credentials with CSRF token to establish session | ||
| * | ||
| * Session cookies are automatically stored in the axios instance's cookie jar | ||
| * and will be included in subsequent requests to /api/ endpoints. | ||
| * | ||
| * @param email - User's Plane account email address | ||
| * @param password - User's Plane account password | ||
| * @param hostUrl - Plane server URL (e.g., "https://api.plane.so/" or self-hosted URL) | ||
| * @returns Authentication result with success status and error details if failed | ||
| * @throws Never throws - all errors are captured in AuthResult | ||
| */ | ||
| export async function authenticateWithPassword( | ||
| email: string, | ||
| password: string, | ||
| hostUrl: string | ||
| ): Promise<AuthResult> { | ||
| try { | ||
| const instance = getAxiosInstance(); | ||
| const host = hostUrl.endsWith("/") ? hostUrl : `${hostUrl}/`; | ||
|
|
||
| debugLog("[AUTH] Starting authentication flow..."); | ||
| debugLog(`[AUTH] Host URL: ${host}`); | ||
|
|
||
| // Step 1: Get CSRF token (stored in cookie jar automatically) | ||
| await instance.get(`${host}auth/get-csrf-token/`); | ||
| debugLog("[AUTH] CSRF token requested"); | ||
|
|
||
| // Step 2: Extract CSRF token from cookie jar for the request header | ||
| const jar = (instance.defaults as any).jar as CookieJar; | ||
coderabbitai[bot] marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| const cookies = await jar.getCookies(host); | ||
| debugLog(`[AUTH] Cookies after CSRF request: ${cookies.map(c => `${c.key}=${c.value.substring(0, 10)}...`).join(", ")}`); | ||
coderabbitai[bot] marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| const csrfCookie = cookies.find((c) => c.key === "csrftoken"); | ||
|
|
||
| if (!csrfCookie) { | ||
| debugLog("[AUTH] CSRF token not found in cookies"); | ||
| return { success: false, error: 'csrf', message: 'CSRF token not found in response' }; | ||
| } | ||
|
|
||
| // Step 3: Login with email, password, and CSRF token | ||
| // Send as form data (application/x-www-form-urlencoded) not JSON | ||
| const formData = new URLSearchParams(); | ||
| formData.append('email', email); | ||
| formData.append('password', password); | ||
|
|
||
| const loginResponse = await instance.post( | ||
| `${host}auth/sign-in/`, | ||
| formData.toString(), | ||
| { | ||
| headers: { | ||
| "X-CSRFToken": csrfCookie.value, | ||
| "Content-Type": "application/x-www-form-urlencoded", | ||
| }, | ||
| maxRedirects: 0, // Don't follow redirects, we just need the cookies | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Bug: Disabled redirects may prevent session cookie captureSetting |
||
| validateStatus: (status) => status >= 200 && status < 400, // Accept redirects as success | ||
| } | ||
|
Comment on lines
122
to
124
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Accepting redirects as success may cause false positives. The This is related to the authentication verification issue already flagged at lines 108-110—making a test API request after login would definitively verify the session works regardless of the HTTP status code. 🤖 Prompt for AI Agents |
||
| ); | ||
|
|
||
| // Log response details | ||
| debugLog(`[AUTH] Login response status: ${loginResponse.status}`); | ||
| debugLog(`[AUTH] Login response headers: ${JSON.stringify(loginResponse.headers)}`); | ||
coderabbitai[bot] marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| // Check if Set-Cookie headers are present | ||
| const setCookieHeader = loginResponse.headers['set-cookie']; | ||
| if (setCookieHeader) { | ||
| debugLog(`[AUTH] Set-Cookie headers received: ${JSON.stringify(setCookieHeader)}`); | ||
| } else { | ||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| debugLog(`[AUTH] ERROR: No Set-Cookie headers in login response!`); | ||
| return { success: false, error: 'cookies', message: 'No session cookies received from server' }; | ||
| } | ||
|
|
||
| // Verify cookies were stored in the jar | ||
| const loginCookies = await jar.getCookies(host); | ||
| debugLog(`[AUTH] Cookies after login: ${loginCookies.map(c => `${c.key}=${c.value.substring(0, 10)}...`).join(", ")}`); | ||
| debugLog(`[AUTH] Total cookies stored: ${loginCookies.length}`); | ||
|
|
||
| // Validate that session cookie was received | ||
| const sessionCookie = loginCookies.find((c) => c.key === "session-id"); | ||
| if (!sessionCookie) { | ||
| debugLog("[AUTH] ERROR: session-id cookie not found after login!"); | ||
| return { success: false, error: 'cookies', message: 'session-id cookie not found after login' }; | ||
| } | ||
|
|
||
| // Log full cookie details for debugging | ||
| loginCookies.forEach(c => { | ||
| debugLog(`[AUTH] Cookie detail - ${c.key}: domain=${c.domain}, path=${c.path}, httpOnly=${c.httpOnly}, secure=${c.secure}`); | ||
| }); | ||
|
|
||
| isAuthenticated = true; | ||
| debugLog("[AUTH] Authentication successful"); | ||
| return { success: true }; | ||
| } catch (error) { | ||
| debugLog(`[AUTH] Authentication failed: ${error}`); | ||
|
|
||
| if (axios.isAxiosError(error)) { | ||
| if (!error.response) { | ||
| return { success: false, error: 'network', message: 'Network error - could not connect to server' }; | ||
| } | ||
| if (error.response.status === 401 || error.response.status === 403) { | ||
| return { success: false, error: 'credentials', message: 'Invalid email or password' }; | ||
| } | ||
| return { success: false, error: 'unknown', message: `Server error: ${error.response.status}` }; | ||
| } | ||
|
|
||
| return { success: false, error: 'unknown', message: String(error) }; | ||
| } | ||
cursor[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
|
|
||
| /** | ||
| * Checks whether a session is currently authenticated | ||
| * @returns true if authenticated, false otherwise | ||
| */ | ||
| export function isSessionAuthenticated(): boolean { | ||
| debugLog(`[AUTH] isSessionAuthenticated() called - returning: ${isAuthenticated}`); | ||
| return isAuthenticated; | ||
| } | ||
|
|
||
| /** | ||
| * Resets the authentication state and clears all session cookies | ||
| * | ||
| * This function: | ||
| * 1. Removes all cookies from the cookie jar | ||
| * 2. Clears the axios instance | ||
| * 3. Resets authentication flag | ||
| * | ||
| * Call this when logging out or when authentication needs to be cleared. | ||
| * | ||
| * @returns Promise that resolves when authentication is reset | ||
| */ | ||
| export async function resetAuthentication(): Promise<void> { | ||
| if (axiosInstance) { | ||
| const jar = (axiosInstance.defaults as any).jar as CookieJar | undefined; | ||
| if (jar) { | ||
| await jar.removeAllCookies(); | ||
| debugLog("[AUTH] Cookie jar cleared"); | ||
| } | ||
| } | ||
| axiosInstance = null; | ||
| isAuthenticated = false; | ||
| debugLog("[AUTH] Authentication reset"); | ||
| } | ||
cursor[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
Uh oh!
There was an error while loading. Please reload this page.