feat: default locale Russian, geo determines language for other countries
- localization-svc: defaultLocale ru, resolveLocale only by geo - web-svc: DEFAULT_LOCALE ru, layout lang=ru, embeddedTranslations fallback ru - countryToLocale: default ru when no country or unknown country Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
15
services/projects-svc/Dockerfile
Normal file
15
services/projects-svc/Dockerfile
Normal file
@@ -0,0 +1,15 @@
|
||||
FROM node:22-alpine AS builder
|
||||
WORKDIR /app
|
||||
COPY package*.json ./
|
||||
RUN npm ci
|
||||
COPY tsconfig.json ./
|
||||
COPY src ./src
|
||||
RUN npm run build
|
||||
|
||||
FROM node:22-alpine
|
||||
WORKDIR /app
|
||||
COPY package*.json ./
|
||||
RUN npm ci --omit=dev
|
||||
COPY --from=builder /app/dist ./dist
|
||||
EXPOSE 3006
|
||||
CMD ["node", "dist/index.js"]
|
||||
22
services/projects-svc/package.json
Normal file
22
services/projects-svc/package.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"name": "projects-svc",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "tsx watch src/index.ts",
|
||||
"build": "tsc",
|
||||
"start": "node dist/index.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"fastify": "^4.28.1",
|
||||
"@fastify/cors": "^9.0.1",
|
||||
"drizzle-orm": "^0.40.1",
|
||||
"postgres": "^3.4.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.10.0",
|
||||
"tsx": "^4.19.2",
|
||||
"typescript": "^5.7.2"
|
||||
}
|
||||
}
|
||||
125
services/projects-svc/src/index.ts
Normal file
125
services/projects-svc/src/index.ts
Normal file
@@ -0,0 +1,125 @@
|
||||
/**
|
||||
* projects-svc — Spaces CRUD, Collections (публичные read-only)
|
||||
* docs/architecture: 01-perplexity-analogue-design.md §2.2.E, §2.2.F
|
||||
* API: GET/POST /api/v1/projects, GET /api/v1/collections?category=
|
||||
*/
|
||||
|
||||
import Fastify from 'fastify';
|
||||
import cors from '@fastify/cors';
|
||||
|
||||
const PORT = parseInt(process.env.PORT ?? '3006', 10);
|
||||
const AUTH_SERVICE_URL = process.env.AUTH_SERVICE_URL ?? '';
|
||||
|
||||
async function getUserIdFromToken(authHeader: string | undefined): Promise<string | null> {
|
||||
if (!authHeader?.startsWith('Bearer ')) return null;
|
||||
const token = authHeader.slice(7);
|
||||
if (!AUTH_SERVICE_URL) return null;
|
||||
try {
|
||||
const res = await fetch(`${AUTH_SERVICE_URL.replace(/\/$/, '')}/api/auth/me`, {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
signal: AbortSignal.timeout(5000),
|
||||
});
|
||||
if (!res.ok) return null;
|
||||
const data = (await res.json()) as { id?: string };
|
||||
return data.id ?? null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
const STUB_COLLECTIONS = [
|
||||
{ id: 'sec-findings', title: 'SEC Findings', category: 'finance', description: 'SEC filings analysis' },
|
||||
{ id: 'buffett-letters', title: 'Buffett Letters', category: 'finance', description: 'Berkshire shareholder letters' },
|
||||
{ id: 'sp-transcripts', title: 'S&P 500 Transcripts', category: 'finance', description: 'Earnings call transcripts' },
|
||||
];
|
||||
|
||||
const STUB_TEMPLATES = [
|
||||
{ id: 'finance-research', title: 'Finance Research', category: 'finance', description: 'Market analysis, earnings, SEC filings' },
|
||||
{ id: 'product-docs', title: 'Product Documentation', category: 'product', description: 'Technical docs, specs, roadmaps' },
|
||||
{ id: 'marketing-campaigns', title: 'Marketing Campaigns', category: 'marketing', description: 'Campaign briefs, analytics, copy' },
|
||||
{ id: 'travel-planning', title: 'Travel Planning', category: 'travel', description: 'Itineraries, hotels, destinations' },
|
||||
];
|
||||
|
||||
const app = Fastify({ logger: true });
|
||||
|
||||
await app.register(cors, { origin: true });
|
||||
|
||||
app.get('/health', async () => ({ status: 'ok' }));
|
||||
app.get('/ready', async () => ({ status: 'ready' }));
|
||||
|
||||
app.get<{ Querystring: { category?: string } }>('/api/v1/collections', async (req, reply) => {
|
||||
const category = req.query.category;
|
||||
const items = category
|
||||
? STUB_COLLECTIONS.filter((c) => c.category === category)
|
||||
: STUB_COLLECTIONS;
|
||||
return { items };
|
||||
});
|
||||
|
||||
app.get<{ Params: { id: string } }>('/api/v1/collections/:id', async (req, reply) => {
|
||||
const item = STUB_COLLECTIONS.find((c) => c.id === req.params.id);
|
||||
if (!item) return reply.status(404).send({ error: 'Not found' });
|
||||
return item;
|
||||
});
|
||||
|
||||
app.get<{ Querystring: { category?: string } }>('/api/v1/templates', async (req, reply) => {
|
||||
const category = req.query.category;
|
||||
const items = category
|
||||
? STUB_TEMPLATES.filter((t) => t.category === category)
|
||||
: STUB_TEMPLATES;
|
||||
return { items };
|
||||
});
|
||||
|
||||
app.get('/api/v1/projects', async (req, reply) => {
|
||||
const userId = await getUserIdFromToken(req.headers.authorization);
|
||||
if (!userId) return reply.status(401).send({ error: 'Authorization required' });
|
||||
return reply.send({ items: [] });
|
||||
});
|
||||
|
||||
app.post('/api/v1/projects', async (req, reply) => {
|
||||
const userId = await getUserIdFromToken(req.headers.authorization);
|
||||
if (!userId) return reply.status(401).send({ error: 'Authorization required' });
|
||||
return reply.status(501).send({
|
||||
error: 'Not implemented',
|
||||
message: 'Projects CRUD will be available in a future release',
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* My Connectors — Google Drive, Dropbox (Pro)
|
||||
* docs/architecture: 01-perplexity-analogue-design.md §5.8
|
||||
*/
|
||||
const AVAILABLE_CONNECTORS = [
|
||||
{ id: 'google-drive', name: 'Google Drive', description: 'Search files from your Drive', icon: 'drive', status: 'coming_soon' as const },
|
||||
{ id: 'dropbox', name: 'Dropbox', description: 'Search files from your Dropbox', icon: 'dropbox', status: 'coming_soon' as const },
|
||||
];
|
||||
|
||||
app.get('/api/v1/connectors', async (req, reply) => {
|
||||
const userId = await getUserIdFromToken(req.headers.authorization);
|
||||
if (!userId) return reply.status(401).send({ error: 'Authorization required' });
|
||||
return reply.send({
|
||||
available: AVAILABLE_CONNECTORS,
|
||||
connected: [],
|
||||
});
|
||||
});
|
||||
|
||||
app.post<{ Params: { type: string } }>('/api/v1/connectors/:type', async (req, reply) => {
|
||||
return reply.status(501).send({
|
||||
error: 'OAuth not implemented',
|
||||
message: 'Connector integration (Google Drive, Dropbox) will be available in a future release',
|
||||
});
|
||||
});
|
||||
|
||||
app.delete<{ Params: { id: string } }>('/api/v1/connectors/:id', async (req, reply) => {
|
||||
return reply.status(501).send({
|
||||
error: 'Not implemented',
|
||||
message: 'Disconnect will be available with connector integration',
|
||||
});
|
||||
});
|
||||
|
||||
try {
|
||||
await app.listen({ port: PORT, host: '0.0.0.0' });
|
||||
console.log(`projects-svc listening on :${PORT}`);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
}
|
||||
13
services/projects-svc/tsconfig.json
Normal file
13
services/projects-svc/tsconfig.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "NodeNext",
|
||||
"outDir": "dist",
|
||||
"rootDir": "src",
|
||||
"strict": true,
|
||||
"skipLibCheck": true
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
Reference in New Issue
Block a user