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
252 changes: 178 additions & 74 deletions apps/backend/controllers/requestLine.controller.ts
Original file line number Diff line number Diff line change
@@ -1,102 +1,63 @@
import { RequestHandler } from 'express';
import * as RequestLineService from '../services/requestLine.service.js';
import * as AnonymousDeviceService from '../services/anonymousDevice.service.js';
// Force CI rebuild after main merge
import { processRequest, parseOnly, getConfig, isParsingEnabled } from '../services/requestLine/index.js';
import { searchLibrary } from '../services/library.service.js';

export type RequestLineBody = {
message: string;
skipSlack?: boolean;
skipParsing?: boolean;
};

export type RegisterDeviceBody = {
deviceId: string;
};

export type LibrarySearchQuery = {
artist?: string;
title?: string;
query?: string;
limit?: string;
};

// Message validation constants
const MESSAGE_MIN_LENGTH = 1;
const MESSAGE_MAX_LENGTH = 500;

/**
* Register an anonymous device and receive a JWT token.
* Legacy device registration endpoint (deprecated).
* Redirects clients to use the better-auth anonymous sign-in endpoint.
* POST /request/register
*/
export const registerDevice: RequestHandler<object, unknown, RegisterDeviceBody> = async (req, res, next) => {
const requestId = `reg-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
const startTime = Date.now();
export const registerDevice: RequestHandler<object, unknown, RegisterDeviceBody> = async (req, res) => {
const authUrl = process.env.BETTER_AUTH_URL || 'http://localhost:8082/auth';

console.log(`[${requestId}] Device registration request:`, {
console.log('Legacy /request/register endpoint called - redirecting to better-auth', {
method: req.method,
url: req.originalUrl,
ip: req.ip,
userAgent: req.get('User-Agent'),
timestamp: new Date().toISOString(),
});

const { deviceId } = req.body;

// Validate deviceId
if (!deviceId || typeof deviceId !== 'string') {
const responseTime = Date.now() - startTime;
console.log(`[${requestId}] Registration failed: missing deviceId`, { responseTime: `${responseTime}ms` });
res.status(400).json({ message: 'deviceId is required' });
return;
}

if (!AnonymousDeviceService.isValidDeviceId(deviceId)) {
const responseTime = Date.now() - startTime;
console.log(`[${requestId}] Registration failed: invalid deviceId format`, { responseTime: `${responseTime}ms` });
res.status(400).json({ message: 'Invalid deviceId format. Must be a valid UUID.' });
return;
}

try {
// Register or retrieve device
const result = await AnonymousDeviceService.registerDevice(deviceId);

if (!result) {
// Device is blocked
const responseTime = Date.now() - startTime;
console.log(`[${requestId}] Registration rejected: device blocked`, { deviceId, responseTime: `${responseTime}ms` });
res.status(403).json({ message: 'Device has been blocked' });
return;
}

// Generate token
const tokenResult = await AnonymousDeviceService.generateToken(deviceId);

const responseTime = Date.now() - startTime;
console.log(`[${requestId}] Registration successful:`, {
deviceId,
isNew: result.isNew,
responseTime: `${responseTime}ms`,
});

res.status(200).json({
token: tokenResult.token,
expiresAt: tokenResult.expiresAt.toISOString(),
});
} catch (e) {
const error = e instanceof Error ? e : new Error(String(e));
const responseTime = Date.now() - startTime;
console.error(`[${requestId}] Registration error:`, {
error: error.message,
stack: error.stack,
responseTime: `${responseTime}ms`,
});
next(e);
}
res.status(301).json({
message: 'This endpoint is deprecated. Use POST /auth/sign-in/anonymous for registration.',
endpoint: `${authUrl}/sign-in/anonymous`,
});
};

export const submitRequestLine: RequestHandler<object, unknown, RequestLineBody> = async (req, res, next) => {
const logId = `rl-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
const startTime = Date.now();
const deviceId = req.anonymousDevice?.deviceId || 'unknown';
const userId = req.user?.id || 'unknown';

// Log incoming request
console.log(`[${logId}] Request line received:`, {
method: req.method,
url: req.originalUrl,
ip: req.ip,
deviceId,
userId,
userAgent: req.get('User-Agent'),
messageLength: req.body.message?.length || 0,
timestamp: new Date().toISOString(),
Expand Down Expand Up @@ -151,33 +112,176 @@ export const submitRequestLine: RequestHandler<object, unknown, RequestLineBody>
}

try {
const result = await RequestLineService.submitRequestLine(trimmedMessage);
// Use enhanced service if AI parsing is available, otherwise fall back to simple Slack post
const config = getConfig();

if (isParsingEnabled(config)) {
// Use enhanced pipeline with AI parsing, library search, and artwork
const result = await processRequest({
message: trimmedMessage,
skipSlack: req.body.skipSlack,
skipParsing: req.body.skipParsing,
});

const responseTime = Date.now() - startTime;
console.log(`[${logId}] Request completed successfully (enhanced):`, {
statusCode: 200,
responseTime: `${responseTime}ms`,
messageLength: trimmedMessage.length,
userId,
searchType: result.searchType,
libraryResultsCount: result.libraryResults.length,
hasArtwork: !!result.artwork?.artworkUrl,
parsed: {
isRequest: result.parsed.isRequest,
messageType: result.parsed.messageType,
hasArtist: !!result.parsed.artist,
hasAlbum: !!result.parsed.album,
hasSong: !!result.parsed.song,
},
});

res.status(200).json(result);
} else {
// Fall back to simple Slack post (legacy behavior)
const result = await RequestLineService.submitRequestLine(trimmedMessage);

const responseTime = Date.now() - startTime;
console.log(`[${logId}] Request completed successfully (legacy):`, {
statusCode: 200,
responseTime: `${responseTime}ms`,
messageLength: trimmedMessage.length,
userId,
slackResponse: result,
});

res.status(200).json({
success: true,
message: 'Request line submitted successfully',
result,
});
}
} catch (e) {
const error = e instanceof Error ? e : new Error(String(e));
const responseTime = Date.now() - startTime;

console.error(`[${logId}] Request failed:`, {
statusCode: 500,
responseTime: `${responseTime}ms`,
error: error.message,
stack: error.stack,
messageLength: req.body.message?.length || 0,
userId,
});

next(e);
}
};

/**
* Parse a message only (for debugging).
* POST /request/parse
*/
export const parseMessage: RequestHandler<object, unknown, { message: string }> = async (req, res, next) => {
const logId = `parse-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
const startTime = Date.now();

console.log(`[${logId}] Parse request received:`, {
method: req.method,
url: req.originalUrl,
ip: req.ip,
messageLength: req.body.message?.length || 0,
timestamp: new Date().toISOString(),
});

const message = req.body.message?.trim();
if (!message) {
res.status(400).json({ message: 'Message is required' });
return;
}

try {
const parsed = await parseOnly(message);

const responseTime = Date.now() - startTime;
console.log(`[${logId}] Parse completed:`, {
responseTime: `${responseTime}ms`,
parsed: {
isRequest: parsed.isRequest,
messageType: parsed.messageType,
hasArtist: !!parsed.artist,
hasAlbum: !!parsed.album,
hasSong: !!parsed.song,
},
});

res.status(200).json({ success: true, parsed });
} catch (e) {
const error = e instanceof Error ? e : new Error(String(e));
const responseTime = Date.now() - startTime;

console.error(`[${logId}] Parse failed:`, {
responseTime: `${responseTime}ms`,
error: error.message,
});

res.status(500).json({ success: false, message: error.message });
}
};

/**
* Search the library.
* GET /library/search
*/
export const searchLibraryEndpoint: RequestHandler<object, unknown, unknown, LibrarySearchQuery> = async (
req,
res,
next
) => {
const logId = `search-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
const startTime = Date.now();

const { artist, title, query, limit } = req.query;
const limitNum = limit ? parseInt(limit, 10) : 5;

console.log(`[${logId}] Library search request:`, {
method: req.method,
url: req.originalUrl,
ip: req.ip,
artist,
title,
query,
limit: limitNum,
timestamp: new Date().toISOString(),
});

if (!artist && !title && !query) {
res.status(400).json({ message: 'At least one of artist, title, or query is required' });
return;
}

try {
const results = await searchLibrary(query, artist, title, limitNum);

const responseTime = Date.now() - startTime;
console.log(`[${logId}] Request completed successfully:`, {
statusCode: 200,
console.log(`[${logId}] Search completed:`, {
responseTime: `${responseTime}ms`,
messageLength: trimmedMessage.length,
deviceId,
slackResponse: result,
resultsCount: results.length,
});

res.status(200).json({
success: true,
message: 'Request line submitted successfully',
result,
results,
total: results.length,
query: { artist, title, query, limit: limitNum },
});
} catch (e) {
const error = e instanceof Error ? e : new Error(String(e));
const responseTime = Date.now() - startTime;

console.error(`[${logId}] Request failed:`, {
statusCode: 500,
console.error(`[${logId}] Search failed:`, {
responseTime: `${responseTime}ms`,
error: error.message,
stack: error.stack,
messageLength: req.body.message?.length || 0,
deviceId,
});

next(e);
Expand Down
Loading
Loading