chart data loading
This commit is contained in:
@@ -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}`,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
|
||||
Reference in New Issue
Block a user