aboutsummaryrefslogtreecommitdiffstats
path: root/app/Controllers/Http/RecipeController.ts
diff options
context:
space:
mode:
authorLibravatar Ricardo <ricardo@cino.io>2023-10-13 14:12:03 +0200
committerLibravatar GitHub <noreply@github.com>2023-10-13 13:12:03 +0100
commite503468660a13760010a94ecda5f0625c6f47f87 (patch)
treefa532f54fc5f091de08d55405ec6339bd2440a02 /app/Controllers/Http/RecipeController.ts
parent1.3.16 [skip ci] (diff)
downloadferdium-server-e503468660a13760010a94ecda5f0625c6f47f87.tar.gz
ferdium-server-e503468660a13760010a94ecda5f0625c6f47f87.tar.zst
ferdium-server-e503468660a13760010a94ecda5f0625c6f47f87.zip
Server re-build with latest AdonisJS framework & Typescript (#47)
* chore: setup first basis structure * chore: ensure styling is loaded correctly * chore: comply to new routing syntax by replace . with / in routes/resource locations * chore: add login controller * chore: correctly use views with slash instead of dot * chore: working login + tests * chore: clean up tests * chore: add password-forgot endpoint and matching test * chore: add delete page test * chore: add logout test * chore: add reset-password route and tests * chore: remove obsolete comment * chore: add account-page and tests * chore: add data page & first step of the test * chore: add transfer/import data feature and tests * chore: add export and basic test * chore: add all static api routes with tests * Regenerate 'pnpm-lock.json' and fix bad merge conflict WIP: - Tests have been commented out since they dont work - Server doesn't start * easier dev and test runs * - remove --require-pragma from reformat-files so formatting works properly - run pnpm reformat-files over codebase - remove .json files from .eslintignore - add invalid.json file to .eslintignore - configure prettier properly in eslint config - add type jsdoc to prettier config - run adonis generate:manifest command to regenerate ace-manifest.json - specify volta in package.json - introduce typecheck npm script - remove unused .mjs extension from npm scripts - install missing type definition dependencies - add pnpm.allowedDeprecatedVersions to package.json - fix invalid extends in tsconfig.json causing TS issues throughout codebase - remove @ts-ignore throughout codebase which is not relevant anymore - enable some of the tsconfig options - remove outdated eslint-disable from codebase - change deprecated faker.company.companyName() to faker.company.name() - fix TS issues inside transfer.spec.ts * - update to latest node and pnpm versions - upgrade all non-major dependencies to latest - install missing @types/luxon dependency - add cuid to pnpm.allowedDeprecatedVersions - add esModuleInterop config option to tsconfig - migrate more deprecated faker methods to new ones - add more temporary ts-ignore to code * - update eslint config - remove trailingComma: all since default in prettier v3 - add typecheck command to prepare-code npm script - upgrade various dependencies to latest major version - update tsconfig to include only useful config options - disable some lint issues and fix others * - add test command to prepare-code - disable strictPropertyInitialization flag in tsconfig which creates issues with adonis models - update precommit hook to excute pnpm prepare-code - remove ts-ignore statements from all models * fix node and pnpm dependency update * add cross env (so that we can develop on windows) * add signup endpoint (TODO: JWT auth) * Add login endpoint * Add me and updateMe endpoints * Add service endpoint * refactor: change endpoints to use jwt * add recipes endpoint * add workspaces endpoint * fix web controllors for login and post import * Update node deps * Change auth middleware (for web) and exempt api from CSRF * Add import endpoint (franz import) * Fix export/import logic * Fix service and workspace data in user/data * Fix partial lint * chore: workaround lint issues * fix: migration naming had two . * Sync back node with recipes repo * Temporarily ignore typescript * Fix adonisrc to handle public folder static assets * Fix issue with production database * add Legacy Password Provider * Fix lint errors * Fix issue on login errors frontend * add Legacy Password Provider * Fix issue with customIcons * Fix issue with auth tokens * Update 'node' to '18.18.0' * make docker work * improve docker entrypoint (test api performance) * Add migration database script * NODE_ENV on recipes * prefer @ts-expect-error over @ts-ignore * small fixes * Update 'pnpm' to '8.7.6' * fix error catch * Automatically generate JWT Public and Private keys * Use custom Adonis5-jwt * Update code to use secret (old way, no breaking changes) * Normalize appKey * Trick to make JWT tokens on client work with new version * Fix error with new JWT logic * Change migration and how we store JWT * Fix 500 response code (needs to be 401) * Improve logic and fix bugs * Fix build and entrypoint logic * Catch error if appKey changes * Add newToken logic * Fix lint (ignore any errors) * Add build for PRs * pnpm reformat-files result * Fix some tests * Fix reset password not working (test failing) * Restore csrfTokens (disabled by accident) * Fix pnpm start command with .env * Disable failing tests on the transfer endpoint (TODO) * Add tests to PR build * Fix build * Remove unnecessary assertStatus * Add typecheck * hash password on UserFactory (fix build) * Add JWT_USE_PEM true by default (increase security) * fix name of github action --------- Co-authored-by: Vijay A <vraravam@users.noreply.github.com> Co-authored-by: Balaji Vijayakumar <kuttibalaji.v6@gmail.com> Co-authored-by: MCMXC <16797721+mcmxcdev@users.noreply.github.com> Co-authored-by: André Oliveira <oliveira.andrerodrigues95@gmail.com>
Diffstat (limited to 'app/Controllers/Http/RecipeController.ts')
-rw-r--r--app/Controllers/Http/RecipeController.ts254
1 files changed, 254 insertions, 0 deletions
diff --git a/app/Controllers/Http/RecipeController.ts b/app/Controllers/Http/RecipeController.ts
new file mode 100644
index 0000000..5186a11
--- /dev/null
+++ b/app/Controllers/Http/RecipeController.ts
@@ -0,0 +1,254 @@
1import type { HttpContextContract } from '@ioc:Adonis/Core/HttpContext';
2import fs from 'fs-extra';
3import Application from '@ioc:Adonis/Core/Application';
4import path from 'node:path';
5import Recipe from 'App/Models/Recipe';
6import { isCreationEnabled } from 'Config/app';
7import { validator, schema, rules } from '@ioc:Adonis/Core/Validator';
8import targz from 'targz';
9import semver from 'semver';
10import Drive from '@ioc:Adonis/Core/Drive';
11
12// TODO: This file needs to be refactored and cleaned up to include types
13
14const createSchema = schema.create({
15 name: schema.string(),
16 id: schema.string([rules.unique({ table: 'recipes', column: 'recipeId' })]),
17 // TODO: Check if this is correct
18 // author: 'required|accepted',
19 author: schema.string(),
20 svg: schema.string([rules.url()]),
21});
22
23const searchSchema = schema.create({
24 needle: schema.string(),
25});
26
27const downloadSchema = schema.create({
28 // TODO: Check if this is correct
29 // recipe: 'required|accepted',
30 recipe: schema.string(),
31});
32
33const compress = (src: string, dest: string) =>
34 new Promise((resolve, reject) => {
35 targz.compress(
36 {
37 src,
38 dest,
39 },
40 err => {
41 if (err) {
42 reject(err);
43 } else {
44 resolve(dest);
45 }
46 },
47 );
48 });
49
50export default class RecipesController {
51 // List official and custom recipes
52 public async list({ response }: HttpContextContract) {
53 const officialRecipes = fs.readJsonSync(
54 path.join(Application.appRoot, 'recipes', 'all.json'),
55 );
56 const customRecipesArray = await Recipe.all();
57 const customRecipes = customRecipesArray.map(recipe => ({
58 id: recipe.recipeId,
59 name: recipe.name,
60 ...(typeof recipe.data === 'string'
61 ? JSON.parse(recipe.data)
62 : recipe.data),
63 }));
64
65 const recipes = [...officialRecipes, ...customRecipes];
66
67 return response.send(recipes);
68 }
69
70 // TODO: Test this endpoint
71 // Create a new recipe using the new.html page
72 public async create({ request, response }: HttpContextContract) {
73 // Check if recipe creation is enabled
74 if (isCreationEnabled === 'false') {
75 return response.send(
76 'This server doesn\'t allow the creation of new recipes.',
77 );
78 }
79
80 // Validate user input
81 let data;
82 try {
83 data = await request.validate({ schema: createSchema });
84 } catch (error) {
85 return response.status(401).send({
86 message: 'Invalid POST arguments',
87 messages: error.messages,
88 status: 401,
89 });
90 }
91
92 if (!data.id) {
93 return response.send('Please provide an ID');
94 }
95
96 // Check for invalid characters
97 if (/\.+/.test(data.id) || /\/+/.test(data.id)) {
98 return response.send(
99 'Invalid recipe name. Your recipe name may not contain "." or "/"',
100 );
101 }
102
103 // Clear temporary recipe folder
104 await fs.emptyDir(Application.tmpPath('recipe'));
105
106 // Move uploaded files to temporary path
107 const files = request.file('files');
108 if (!files) {
109 return response.abort('Error processsing files.');
110 }
111 await files.move(Application.tmpPath('recipe'));
112
113 // Compress files to .tar.gz file
114 const source = Application.tmpPath('recipe');
115 const destination = path.join(
116 Application.appRoot,
117 `/recipes/archives/${data.id}.tar.gz`,
118 );
119
120 compress(source, destination);
121
122 // Create recipe in db
123 await Recipe.create({
124 name: data.name,
125 recipeId: data.id,
126 // @ts-expect-error
127 data: JSON.stringify({
128 author: data.author,
129 featured: false,
130 version: '1.0.0',
131 icons: {
132 svg: data.svg,
133 },
134 }),
135 });
136
137 return response.send('Created new recipe');
138 }
139
140 // Search official and custom recipes
141 public async search({ request, response }: HttpContextContract) {
142 // Validate user input
143 let data;
144 try {
145 data = await request.validate({ schema: searchSchema });
146 } catch (error) {
147 return response.status(401).send({
148 message: 'Please provide a needle',
149 messages: error.messages,
150 status: 401,
151 });
152 }
153
154 const { needle } = data;
155
156 // Get results
157 let results;
158
159 if (needle === 'ferdium:custom') {
160 const dbResults = await Recipe.all();
161 results = dbResults.map(recipe => ({
162 id: recipe.recipeId,
163 name: recipe.name,
164 ...(typeof recipe.data === 'string'
165 ? JSON.parse(recipe.data)
166 : recipe.data),
167 }));
168 } else {
169 const localResultsArray = await Recipe.query().where(
170 'name',
171 'LIKE',
172 `%${needle}%`,
173 );
174 results = localResultsArray.map(recipe => ({
175 id: recipe.recipeId,
176 name: recipe.name,
177 ...(typeof recipe.data === 'string'
178 ? JSON.parse(recipe.data)
179 : recipe.data),
180 }));
181 }
182
183 return response.send(results);
184 }
185
186 public popularRecipes({ response }: HttpContextContract) {
187 return response.send(
188 fs
189 .readJsonSync(path.join(Application.appRoot, 'recipes', 'all.json'))
190 // eslint-disable-next-line @typescript-eslint/no-explicit-any
191 .filter((recipe: any) => recipe.featured),
192 );
193 }
194
195 // TODO: test this endpoint
196 public update({ request, response }: HttpContextContract) {
197 const updates = [];
198 const recipes = request.all();
199 const allJson = fs.readJsonSync(
200 path.join(Application.appRoot, 'recipes', 'all.json'),
201 );
202
203 for (const recipe of Object.keys(recipes)) {
204 const version = recipes[recipe];
205
206 // Find recipe in local recipe repository
207 // eslint-disable-next-line @typescript-eslint/no-explicit-any
208 const localRecipe = allJson.find((r: any) => r.id === recipe);
209 if (localRecipe && semver.lt(version, localRecipe.version)) {
210 updates.push(recipe);
211 }
212 }
213
214 return response.send(updates);
215 }
216
217 // TODO: test this endpoint
218 // Download a recipe
219 public async download({ response, params }: HttpContextContract) {
220 // Validate user input
221 let data;
222 try {
223 data = await validator.validate({
224 data: params,
225 schema: downloadSchema,
226 });
227 } catch (error) {
228 return response.status(401).send({
229 message: 'Please provide a recipe ID',
230 messages: error.messages,
231 status: 401,
232 });
233 }
234
235 const service = data.recipe;
236
237 // Check for invalid characters
238 if (/\.+/.test(service) || /\/+/.test(service)) {
239 return response.send('Invalid recipe name');
240 }
241
242 // Check if recipe exists in recipes folder
243 if (await Drive.exists(`${service}.tar.gz`)) {
244 return response
245 .type('.tar.gz')
246 .send(await Drive.get(`${service}.tar.gz`));
247 }
248
249 return response.status(400).send({
250 message: 'Recipe not found',
251 code: 'recipe-not-found',
252 });
253 }
254}