fix(gateway): CORS, memory userId from session, pgvector auto-init
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
- Enable CORS with credentials (GATEWAY_CORS_ORIGIN env, defaults to http://localhost:3000) so the web dashboard can talk to the gateway - MemoryController: replace @Query('userId') with @CurrentUser() to extract userId from auth session (was passing undefined) - MemoryController: add missing @Inject(EmbeddingService) - docker-compose: auto-create pgvector extension via init script - .env.example: add GATEWAY_CORS_ORIGIN Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -18,6 +18,7 @@ BETTER_AUTH_URL=http://localhost:4000
|
|||||||
|
|
||||||
# Gateway
|
# Gateway
|
||||||
GATEWAY_PORT=4000
|
GATEWAY_PORT=4000
|
||||||
|
GATEWAY_CORS_ORIGIN=http://localhost:3000
|
||||||
|
|
||||||
# Discord Plugin (optional — set DISCORD_BOT_TOKEN to enable)
|
# Discord Plugin (optional — set DISCORD_BOT_TOKEN to enable)
|
||||||
# DISCORD_BOT_TOKEN=
|
# DISCORD_BOT_TOKEN=
|
||||||
|
|||||||
@@ -35,6 +35,11 @@ async function bootstrap(): Promise<void> {
|
|||||||
new FastifyAdapter({ bodyLimit: 1_048_576 }),
|
new FastifyAdapter({ bodyLimit: 1_048_576 }),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
app.enableCors({
|
||||||
|
origin: process.env['GATEWAY_CORS_ORIGIN'] ?? 'http://localhost:3000',
|
||||||
|
credentials: true,
|
||||||
|
});
|
||||||
|
|
||||||
await app.register(helmet as never, { contentSecurityPolicy: false });
|
await app.register(helmet as never, { contentSecurityPolicy: false });
|
||||||
app.useGlobalPipes(
|
app.useGlobalPipes(
|
||||||
new ValidationPipe({
|
new ValidationPipe({
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
import type { Memory } from '@mosaic/memory';
|
import type { Memory } from '@mosaic/memory';
|
||||||
import { MEMORY } from './memory.tokens.js';
|
import { MEMORY } from './memory.tokens.js';
|
||||||
import { AuthGuard } from '../auth/auth.guard.js';
|
import { AuthGuard } from '../auth/auth.guard.js';
|
||||||
|
import { CurrentUser } from '../auth/current-user.decorator.js';
|
||||||
import { EmbeddingService } from './embedding.service.js';
|
import { EmbeddingService } from './embedding.service.js';
|
||||||
import type { UpsertPreferenceDto, CreateInsightDto, SearchMemoryDto } from './memory.dto.js';
|
import type { UpsertPreferenceDto, CreateInsightDto, SearchMemoryDto } from './memory.dto.js';
|
||||||
|
|
||||||
@@ -23,33 +24,33 @@ import type { UpsertPreferenceDto, CreateInsightDto, SearchMemoryDto } from './m
|
|||||||
export class MemoryController {
|
export class MemoryController {
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(MEMORY) private readonly memory: Memory,
|
@Inject(MEMORY) private readonly memory: Memory,
|
||||||
private readonly embeddings: EmbeddingService,
|
@Inject(EmbeddingService) private readonly embeddings: EmbeddingService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
// ─── Preferences ────────────────────────────────────────────────────
|
// ─── Preferences ────────────────────────────────────────────────────
|
||||||
|
|
||||||
@Get('preferences')
|
@Get('preferences')
|
||||||
async listPreferences(@Query('userId') userId: string, @Query('category') category?: string) {
|
async listPreferences(@CurrentUser() user: { id: string }, @Query('category') category?: string) {
|
||||||
if (category) {
|
if (category) {
|
||||||
return this.memory.preferences.findByUserAndCategory(
|
return this.memory.preferences.findByUserAndCategory(
|
||||||
userId,
|
user.id,
|
||||||
category as Parameters<typeof this.memory.preferences.findByUserAndCategory>[1],
|
category as Parameters<typeof this.memory.preferences.findByUserAndCategory>[1],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return this.memory.preferences.findByUser(userId);
|
return this.memory.preferences.findByUser(user.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get('preferences/:key')
|
@Get('preferences/:key')
|
||||||
async getPreference(@Query('userId') userId: string, @Param('key') key: string) {
|
async getPreference(@CurrentUser() user: { id: string }, @Param('key') key: string) {
|
||||||
const pref = await this.memory.preferences.findByUserAndKey(userId, key);
|
const pref = await this.memory.preferences.findByUserAndKey(user.id, key);
|
||||||
if (!pref) throw new NotFoundException('Preference not found');
|
if (!pref) throw new NotFoundException('Preference not found');
|
||||||
return pref;
|
return pref;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post('preferences')
|
@Post('preferences')
|
||||||
async upsertPreference(@Query('userId') userId: string, @Body() dto: UpsertPreferenceDto) {
|
async upsertPreference(@CurrentUser() user: { id: string }, @Body() dto: UpsertPreferenceDto) {
|
||||||
return this.memory.preferences.upsert({
|
return this.memory.preferences.upsert({
|
||||||
userId,
|
userId: user.id,
|
||||||
key: dto.key,
|
key: dto.key,
|
||||||
value: dto.value,
|
value: dto.value,
|
||||||
category: dto.category,
|
category: dto.category,
|
||||||
@@ -59,16 +60,16 @@ export class MemoryController {
|
|||||||
|
|
||||||
@Delete('preferences/:key')
|
@Delete('preferences/:key')
|
||||||
@HttpCode(HttpStatus.NO_CONTENT)
|
@HttpCode(HttpStatus.NO_CONTENT)
|
||||||
async removePreference(@Query('userId') userId: string, @Param('key') key: string) {
|
async removePreference(@CurrentUser() user: { id: string }, @Param('key') key: string) {
|
||||||
const deleted = await this.memory.preferences.remove(userId, key);
|
const deleted = await this.memory.preferences.remove(user.id, key);
|
||||||
if (!deleted) throw new NotFoundException('Preference not found');
|
if (!deleted) throw new NotFoundException('Preference not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Insights ───────────────────────────────────────────────────────
|
// ─── Insights ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
@Get('insights')
|
@Get('insights')
|
||||||
async listInsights(@Query('userId') userId: string, @Query('limit') limit?: string) {
|
async listInsights(@CurrentUser() user: { id: string }, @Query('limit') limit?: string) {
|
||||||
return this.memory.insights.findByUser(userId, limit ? Number(limit) : undefined);
|
return this.memory.insights.findByUser(user.id, limit ? Number(limit) : undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get('insights/:id')
|
@Get('insights/:id')
|
||||||
@@ -79,13 +80,13 @@ export class MemoryController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Post('insights')
|
@Post('insights')
|
||||||
async createInsight(@Query('userId') userId: string, @Body() dto: CreateInsightDto) {
|
async createInsight(@CurrentUser() user: { id: string }, @Body() dto: CreateInsightDto) {
|
||||||
const embedding = this.embeddings.available
|
const embedding = this.embeddings.available
|
||||||
? await this.embeddings.embed(dto.content)
|
? await this.embeddings.embed(dto.content)
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
return this.memory.insights.create({
|
return this.memory.insights.create({
|
||||||
userId,
|
userId: user.id,
|
||||||
content: dto.content,
|
content: dto.content,
|
||||||
source: dto.source,
|
source: dto.source,
|
||||||
category: dto.category,
|
category: dto.category,
|
||||||
@@ -104,7 +105,7 @@ export class MemoryController {
|
|||||||
// ─── Search ─────────────────────────────────────────────────────────
|
// ─── Search ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@Post('search')
|
@Post('search')
|
||||||
async searchMemory(@Query('userId') userId: string, @Body() dto: SearchMemoryDto) {
|
async searchMemory(@CurrentUser() user: { id: string }, @Body() dto: SearchMemoryDto) {
|
||||||
if (!this.embeddings.available) {
|
if (!this.embeddings.available) {
|
||||||
return {
|
return {
|
||||||
query: dto.query,
|
query: dto.query,
|
||||||
@@ -115,7 +116,7 @@ export class MemoryController {
|
|||||||
|
|
||||||
const queryEmbedding = await this.embeddings.embed(dto.query);
|
const queryEmbedding = await this.embeddings.embed(dto.query);
|
||||||
const results = await this.memory.insights.searchByEmbedding(
|
const results = await this.memory.insights.searchByEmbedding(
|
||||||
userId,
|
user.id,
|
||||||
queryEmbedding,
|
queryEmbedding,
|
||||||
dto.limit ?? 10,
|
dto.limit ?? 10,
|
||||||
dto.maxDistance ?? 0.8,
|
dto.maxDistance ?? 0.8,
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ services:
|
|||||||
POSTGRES_DB: mosaic
|
POSTGRES_DB: mosaic
|
||||||
volumes:
|
volumes:
|
||||||
- pg_data:/var/lib/postgresql/data
|
- pg_data:/var/lib/postgresql/data
|
||||||
|
- ./infra/pg-init:/docker-entrypoint-initdb.d:ro
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ['CMD-SHELL', 'pg_isready -U mosaic']
|
test: ['CMD-SHELL', 'pg_isready -U mosaic']
|
||||||
interval: 5s
|
interval: 5s
|
||||||
|
|||||||
1
infra/pg-init/01-extensions.sql
Normal file
1
infra/pg-init/01-extensions.sql
Normal file
@@ -0,0 +1 @@
|
|||||||
|
CREATE EXTENSION IF NOT EXISTS vector;
|
||||||
Reference in New Issue
Block a user