chart data loading

This commit is contained in:
2026-03-24 21:37:49 -04:00
parent f6bd22a8ef
commit c76887ab92
65 changed files with 6350 additions and 713 deletions

View File

@@ -19,25 +19,38 @@ export class AuthService {
}
/**
* Verify JWT token and return user ID
* Replaces the placeholder implementation in UserService
* Verify session token and return user ID
* Uses Better Auth's bearer plugin for token verification
*/
async verifyToken(token: string): Promise<string | null> {
try {
// Better Auth's session verification
this.config.logger.debug({
tokenLength: token?.length,
tokenPrefix: token?.substring(0, 8),
}, 'Verifying token');
// Use Better Auth's getSession with Bearer token
// The bearer plugin allows us to pass the session token via Authorization header
const session = await this.config.auth.api.getSession({
headers: {
authorization: `Bearer ${token}`,
},
headers: new Headers({
'Authorization': `Bearer ${token}`,
}),
});
this.config.logger.debug({
hasSession: !!session,
hasUser: !!session?.user,
userId: session?.user?.id,
}, 'Session verification result');
if (!session || !session.user) {
this.config.logger.warn('Session verification failed: no session or user');
return null;
}
return session.user.id;
} catch (error) {
this.config.logger.debug({ error }, 'Token verification failed');
this.config.logger.error({ error }, 'Token verification failed with error');
return null;
}
}
@@ -76,17 +89,47 @@ export class AuthService {
/**
* Sign in with email and password
* Returns the bearer token from response headers
*/
async signIn(email: string, password: string): Promise<{ token: string; userId: string; error?: string }> {
try {
const result = await this.config.auth.api.signInEmail({
this.config.logger.debug({ email }, 'Attempting sign in');
// Use asResponse: true to get the full Response object with headers
const response = await this.config.auth.api.signInEmail({
body: {
email,
password,
},
asResponse: true,
});
if (!result.token || !result.user) {
// Extract bearer token from response headers (set by bearer plugin)
const token = response.headers.get('set-auth-token');
if (!token) {
this.config.logger.error('Bearer token not found in response headers');
return {
token: '',
userId: '',
error: 'Authentication token not generated',
};
}
// Parse the response body to get user info
const result = await response.json() as {
user?: { id: string; email: string; name: string };
error?: string;
};
this.config.logger.debug({
hasUser: !!result.user,
userId: result.user?.id,
hasToken: !!token,
}, 'Sign in result');
if (!result.user) {
this.config.logger.warn('Sign in failed: no user in result');
return {
token: '',
userId: '',
@@ -95,11 +138,11 @@ export class AuthService {
}
return {
token: result.token,
token,
userId: result.user.id,
};
} catch (error: any) {
this.config.logger.error({ error }, 'Sign in failed');
this.config.logger.error({ error }, 'Sign in failed with error');
return {
token: '',
userId: '',
@@ -115,7 +158,8 @@ export class AuthService {
try {
await this.config.auth.api.signOut({
headers: {
authorization: `Bearer ${token}`,
// Better Auth expects the session token in the cookie header
cookie: `better-auth.session_token=${token}`,
},
});
@@ -133,7 +177,8 @@ export class AuthService {
try {
const session = await this.config.auth.api.getSession({
headers: {
authorization: `Bearer ${token}`,
// Better Auth expects the session token in the cookie header
cookie: `better-auth.session_token=${token}`,
},
});

View File

@@ -3,6 +3,11 @@ import { UserService } from '../db/user-service.js';
import { ChannelType, type AuthContext } from '../types/user.js';
import type { ContainerManager } from '../k8s/container-manager.js';
export interface AuthResult {
authContext: AuthContext | null;
isSpinningUp: boolean;
}
export interface AuthenticatorConfig {
userService: UserService;
containerManager: ContainerManager;
@@ -23,40 +28,49 @@ export class Authenticator {
/**
* Authenticate WebSocket connection via JWT token
* Also ensures the user's container is running
* Returns immediately if container is spinning up (non-blocking)
*/
async authenticateWebSocket(
request: FastifyRequest
): Promise<AuthContext | null> {
): Promise<AuthResult> {
try {
const token = this.extractBearerToken(request);
if (!token) {
this.config.logger.warn('No bearer token in WebSocket connection');
return null;
return { authContext: null, isSpinningUp: false };
}
const userId = await this.config.userService.verifyWebToken(token);
if (!userId) {
this.config.logger.warn('Invalid JWT token');
return null;
return { authContext: null, isSpinningUp: false };
}
const license = await this.config.userService.getUserLicense(userId);
if (!license) {
this.config.logger.warn({ userId }, 'User license not found');
return null;
return { authContext: null, isSpinningUp: false };
}
// Ensure container is running (may take time if creating new container)
// Ensure container is running (non-blocking - returns immediately if creating new)
this.config.logger.info({ userId }, 'Ensuring user container is running');
const { mcpEndpoint, wasCreated } = await this.config.containerManager.ensureContainerRunning(
const { mcpEndpoint, wasCreated, isSpinningUp } = await this.config.containerManager.ensureContainerRunning(
userId,
license
license,
false // Don't wait for ready
);
this.config.logger.info(
{ userId, mcpEndpoint, wasCreated },
'Container is ready'
);
if (isSpinningUp) {
this.config.logger.info(
{ userId, wasCreated },
'Container is spinning up'
);
} else {
this.config.logger.info(
{ userId, mcpEndpoint, wasCreated },
'Container is ready'
);
}
// Update license with actual MCP endpoint
license.mcpServerUrl = mcpEndpoint;
@@ -64,16 +78,19 @@ export class Authenticator {
const sessionId = `ws_${userId}_${Date.now()}`;
return {
userId,
channelType: ChannelType.WEBSOCKET,
channelUserId: userId, // For WebSocket, same as userId
sessionId,
license,
authenticatedAt: new Date(),
authContext: {
userId,
channelType: ChannelType.WEBSOCKET,
channelUserId: userId, // For WebSocket, same as userId
sessionId,
license,
authenticatedAt: new Date(),
},
isSpinningUp,
};
} catch (error) {
this.config.logger.error({ error }, 'WebSocket authentication error');
return null;
return { authContext: null, isSpinningUp: false };
}
}
@@ -134,13 +151,22 @@ export class Authenticator {
}
/**
* Extract bearer token from request headers
* Extract bearer token from request headers or query parameters
* WebSocket connections can't set custom headers in browsers, so we support token in query params
*/
private extractBearerToken(request: FastifyRequest): string | null {
// Try Authorization header first
const auth = request.headers.authorization;
if (!auth || !auth.startsWith('Bearer ')) {
return null;
if (auth && auth.startsWith('Bearer ')) {
return auth.substring(7);
}
return auth.substring(7);
// Fall back to query parameter (for WebSocket connections)
const query = request.query as { token?: string };
if (query.token) {
return query.token;
}
return null;
}
}

View File

@@ -1,4 +1,5 @@
import { betterAuth } from 'better-auth';
import { bearer } from 'better-auth/plugins/bearer';
import { Pool } from 'pg';
import { Kysely, PostgresDialect } from 'kysely';
import type { FastifyBaseLogger } from 'fastify';
@@ -88,6 +89,11 @@ export async function createBetterAuth(config: BetterAuthConfig) {
},
},
// Plugins
plugins: [
bearer(), // Enable Bearer token authentication for API/WebSocket
],
});
config.logger.debug('Better Auth instance created');