feat: stabilization + recipe edit/create UI
This commit is contained in:
43
backend/src/routes/bot.ts
Normal file
43
backend/src/routes/bot.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { FastifyInstance } from 'fastify';
|
||||
import * as recipeSvc from '../services/recipe.service.js';
|
||||
import * as imageSvc from '../services/image.service.js';
|
||||
|
||||
export async function botRoutes(app: FastifyInstance) {
|
||||
const BOT_TOKEN = process.env.BOT_API_TOKEN;
|
||||
|
||||
app.addHook('onRequest', async (request, reply) => {
|
||||
if (!BOT_TOKEN) return reply.status(503).send({ error: 'Bot API not configured' });
|
||||
const auth = request.headers.authorization;
|
||||
if (!auth || auth !== `Bearer ${BOT_TOKEN}`) {
|
||||
return reply.status(401).send({ error: 'Unauthorized' });
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/api/bot/recipes', async (request) => {
|
||||
const query = request.query as any;
|
||||
return recipeSvc.listRecipes({
|
||||
page: query.page ? Number(query.page) : undefined,
|
||||
limit: query.limit ? Number(query.limit) : undefined,
|
||||
});
|
||||
});
|
||||
|
||||
app.post('/api/bot/recipes', async (request, reply) => {
|
||||
const body = request.body as recipeSvc.CreateRecipeInput;
|
||||
if (!body.title) return reply.status(400).send({ error: 'title required' });
|
||||
const recipe = recipeSvc.createRecipe(body);
|
||||
return reply.status(201).send(recipe);
|
||||
});
|
||||
|
||||
app.post('/api/bot/recipes/:id/image-url', async (request, reply) => {
|
||||
const { id } = request.params as { id: string };
|
||||
const { url } = request.body as { url: string };
|
||||
if (!url) return reply.status(400).send({ error: 'url required' });
|
||||
try {
|
||||
const result = await imageSvc.downloadAndSaveImage(id, url);
|
||||
if (!result) return reply.status(404).send({ error: 'Recipe not found' });
|
||||
return result;
|
||||
} catch (e: any) {
|
||||
return reply.status(400).send({ error: e.message });
|
||||
}
|
||||
});
|
||||
}
|
||||
15
backend/src/routes/categories.ts
Normal file
15
backend/src/routes/categories.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { FastifyInstance } from 'fastify';
|
||||
import { listCategories, createCategory } from '../services/recipe.service.js';
|
||||
|
||||
export async function categoryRoutes(app: FastifyInstance) {
|
||||
app.get('/api/categories', async () => {
|
||||
return listCategories();
|
||||
});
|
||||
|
||||
app.post('/api/categories', async (request, reply) => {
|
||||
const { name } = request.body as { name: string };
|
||||
if (!name) return reply.status(400).send({ error: 'name required' });
|
||||
const cat = createCategory(name);
|
||||
return reply.status(201).send(cat);
|
||||
});
|
||||
}
|
||||
7
backend/src/routes/health.ts
Normal file
7
backend/src/routes/health.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { FastifyInstance } from 'fastify';
|
||||
|
||||
export async function healthRoutes(app: FastifyInstance) {
|
||||
app.get('/api/health', async () => {
|
||||
return { status: 'ok', timestamp: new Date().toISOString() };
|
||||
});
|
||||
}
|
||||
40
backend/src/routes/images.ts
Normal file
40
backend/src/routes/images.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { FastifyInstance } from 'fastify';
|
||||
import multipart from '@fastify/multipart';
|
||||
import fastifyStatic from '@fastify/static';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import * as svc from '../services/image.service.js';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
export async function imageRoutes(app: FastifyInstance) {
|
||||
await app.register(multipart, { limits: { fileSize: 10 * 1024 * 1024 } });
|
||||
await app.after();
|
||||
|
||||
await app.register(fastifyStatic, {
|
||||
root: path.resolve(__dirname, '../../data/images'),
|
||||
prefix: '/images/',
|
||||
decorateReply: false,
|
||||
});
|
||||
await app.after();
|
||||
|
||||
app.post('/api/recipes/:id/image', async (request, reply) => {
|
||||
const { id } = request.params as { id: string };
|
||||
const file = await request.file();
|
||||
if (!file) return reply.status(400).send({ error: 'No file uploaded' });
|
||||
const buffer = await file.toBuffer();
|
||||
const result = await svc.saveRecipeImage(id, buffer);
|
||||
if (!result) return reply.status(404).send({ error: 'Recipe not found' });
|
||||
return result;
|
||||
});
|
||||
|
||||
app.post('/api/recipes/:id/steps/:stepNumber/image', async (request, reply) => {
|
||||
const { id, stepNumber } = request.params as { id: string; stepNumber: string };
|
||||
const file = await request.file();
|
||||
if (!file) return reply.status(400).send({ error: 'No file uploaded' });
|
||||
const buffer = await file.toBuffer();
|
||||
const result = await svc.saveStepImage(id, Number(stepNumber), buffer);
|
||||
if (!result) return reply.status(404).send({ error: 'Step not found' });
|
||||
return result;
|
||||
});
|
||||
}
|
||||
34
backend/src/routes/notes.ts
Normal file
34
backend/src/routes/notes.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { FastifyInstance } from 'fastify';
|
||||
import * as svc from '../services/note.service.js';
|
||||
|
||||
export async function noteRoutes(app: FastifyInstance) {
|
||||
app.get('/api/recipes/:id/notes', async (request) => {
|
||||
const { id } = request.params as { id: string };
|
||||
return svc.listNotes(id);
|
||||
});
|
||||
|
||||
app.post('/api/recipes/:id/notes', async (request, reply) => {
|
||||
const { id } = request.params as { id: string };
|
||||
const { content } = request.body as { content: string };
|
||||
if (!content) return reply.status(400).send({ error: 'content required' });
|
||||
const note = svc.createNote(id, content);
|
||||
if (!note) return reply.status(404).send({ error: 'Recipe not found' });
|
||||
return reply.status(201).send(note);
|
||||
});
|
||||
|
||||
app.put('/api/notes/:id', async (request, reply) => {
|
||||
const { id } = request.params as { id: string };
|
||||
const { content } = request.body as { content: string };
|
||||
if (!content) return reply.status(400).send({ error: 'content required' });
|
||||
const note = svc.updateNote(id, content);
|
||||
if (!note) return reply.status(404).send({ error: 'Not found' });
|
||||
return note;
|
||||
});
|
||||
|
||||
app.delete('/api/notes/:id', async (request, reply) => {
|
||||
const { id } = request.params as { id: string };
|
||||
const ok = svc.deleteNote(id);
|
||||
if (!ok) return reply.status(404).send({ error: 'Not found' });
|
||||
return { ok: true };
|
||||
});
|
||||
}
|
||||
59
backend/src/routes/recipes.ts
Normal file
59
backend/src/routes/recipes.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import { FastifyInstance } from 'fastify';
|
||||
import * as svc from '../services/recipe.service.js';
|
||||
|
||||
export async function recipeRoutes(app: FastifyInstance) {
|
||||
app.get('/api/recipes/search', async (request) => {
|
||||
const { q } = request.query as { q?: string };
|
||||
if (!q) return { data: [], total: 0 };
|
||||
const results = svc.searchRecipes(q);
|
||||
return { data: results, total: results.length };
|
||||
});
|
||||
|
||||
app.get('/api/recipes', async (request) => {
|
||||
const query = request.query as any;
|
||||
return svc.listRecipes({
|
||||
page: query.page ? Number(query.page) : undefined,
|
||||
limit: query.limit ? Number(query.limit) : undefined,
|
||||
category_id: query.category_id,
|
||||
category_slug: query.category,
|
||||
favorite: query.favorite !== undefined ? query.favorite === 'true' : undefined,
|
||||
difficulty: query.difficulty,
|
||||
maxTime: query.maxTime ? Number(query.maxTime) : undefined,
|
||||
});
|
||||
});
|
||||
|
||||
app.get('/api/recipes/:slug', async (request, reply) => {
|
||||
const { slug } = request.params as { slug: string };
|
||||
const recipe = svc.getRecipeBySlug(slug);
|
||||
if (!recipe) return reply.status(404).send({ error: 'Not found' });
|
||||
return recipe;
|
||||
});
|
||||
|
||||
app.post('/api/recipes', async (request, reply) => {
|
||||
const body = request.body as svc.CreateRecipeInput;
|
||||
if (!body.title) return reply.status(400).send({ error: 'title required' });
|
||||
const recipe = svc.createRecipe(body);
|
||||
return reply.status(201).send(recipe);
|
||||
});
|
||||
|
||||
app.put('/api/recipes/:id', async (request, reply) => {
|
||||
const { id } = request.params as { id: string };
|
||||
const recipe = svc.updateRecipe(id, request.body as any);
|
||||
if (!recipe) return reply.status(404).send({ error: 'Not found' });
|
||||
return recipe;
|
||||
});
|
||||
|
||||
app.delete('/api/recipes/:id', async (request, reply) => {
|
||||
const { id } = request.params as { id: string };
|
||||
const ok = svc.deleteRecipe(id);
|
||||
if (!ok) return reply.status(404).send({ error: 'Not found' });
|
||||
return { ok: true };
|
||||
});
|
||||
|
||||
app.patch('/api/recipes/:id/favorite', async (request, reply) => {
|
||||
const { id } = request.params as { id: string };
|
||||
const result = svc.toggleFavorite(id);
|
||||
if (!result) return reply.status(404).send({ error: 'Not found' });
|
||||
return result;
|
||||
});
|
||||
}
|
||||
41
backend/src/routes/shopping.ts
Normal file
41
backend/src/routes/shopping.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { FastifyInstance } from 'fastify';
|
||||
import * as svc from '../services/shopping.service.js';
|
||||
|
||||
export async function shoppingRoutes(app: FastifyInstance) {
|
||||
app.get('/api/shopping', async () => {
|
||||
return svc.listItems();
|
||||
});
|
||||
|
||||
app.post('/api/shopping/from-recipe/:id', async (request, reply) => {
|
||||
const { id } = request.params as { id: string };
|
||||
const items = svc.addFromRecipe(id);
|
||||
if (!items) return reply.status(404).send({ error: 'Recipe not found' });
|
||||
return reply.status(201).send({ added: items.length });
|
||||
});
|
||||
|
||||
app.post('/api/shopping', async (request, reply) => {
|
||||
const { name, amount, unit } = request.body as { name: string; amount?: number; unit?: string };
|
||||
if (!name) return reply.status(400).send({ error: 'name required' });
|
||||
const item = svc.addItem(name, amount, unit);
|
||||
return reply.status(201).send(item);
|
||||
});
|
||||
|
||||
app.patch('/api/shopping/:id/check', async (request, reply) => {
|
||||
const { id } = request.params as { id: string };
|
||||
const item = svc.toggleCheck(id);
|
||||
if (!item) return reply.status(404).send({ error: 'Not found' });
|
||||
return item;
|
||||
});
|
||||
|
||||
app.delete('/api/shopping/checked', async () => {
|
||||
const count = svc.deleteChecked();
|
||||
return { ok: true, deleted: count };
|
||||
});
|
||||
|
||||
app.delete('/api/shopping/:id', async (request, reply) => {
|
||||
const { id } = request.params as { id: string };
|
||||
const ok = svc.deleteItem(id);
|
||||
if (!ok) return reply.status(404).send({ error: 'Not found' });
|
||||
return { ok: true };
|
||||
});
|
||||
}
|
||||
15
backend/src/routes/tags.ts
Normal file
15
backend/src/routes/tags.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { FastifyInstance } from 'fastify';
|
||||
import * as svc from '../services/tag.service.js';
|
||||
|
||||
export async function tagRoutes(app: FastifyInstance) {
|
||||
app.get('/api/tags', async () => {
|
||||
return svc.listTags();
|
||||
});
|
||||
|
||||
app.get('/api/tags/:name/recipes', async (request, reply) => {
|
||||
const { name } = request.params as { name: string };
|
||||
const result = svc.getRecipesByTag(name);
|
||||
if (!result) return reply.status(404).send({ error: 'Tag not found' });
|
||||
return result;
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user