summaryrefslogtreecommitdiffstats
path: root/app/Controllers/Http/RecipeController.ts
diff options
context:
space:
mode:
Diffstat (limited to 'app/Controllers/Http/RecipeController.ts')
-rw-r--r--app/Controllers/Http/RecipeController.ts179
1 files changed, 78 insertions, 101 deletions
diff --git a/app/Controllers/Http/RecipeController.ts b/app/Controllers/Http/RecipeController.ts
index 5186a11..e43bcf8 100644
--- a/app/Controllers/Http/RecipeController.ts
+++ b/app/Controllers/Http/RecipeController.ts
@@ -1,13 +1,13 @@
1import type { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'; 1import type { HttpContext } from '@adonisjs/core/http'
2import fs from 'fs-extra'; 2import fs from 'fs-extra'
3import Application from '@ioc:Adonis/Core/Application'; 3import { app } from '@adonisjs/core/services/app'
4import path from 'node:path'; 4import path from 'node:path'
5import Recipe from 'App/Models/Recipe'; 5import Recipe from '#app/Models/Recipe'
6import { isCreationEnabled } from 'Config/app'; 6import { isCreationEnabled } from '#config/app'
7import { validator, schema, rules } from '@ioc:Adonis/Core/Validator'; 7import { validator, schema, rules } from '@adonisjs/validator'
8import targz from 'targz'; 8import targz from 'targz'
9import semver from 'semver'; 9import semver from 'semver'
10import Drive from '@ioc:Adonis/Core/Drive'; 10import Drive from '@ioc:Adonis/Core/Drive'
11 11
12// TODO: This file needs to be refactored and cleaned up to include types 12// TODO: This file needs to be refactored and cleaned up to include types
13 13
@@ -18,17 +18,17 @@ const createSchema = schema.create({
18 // author: 'required|accepted', 18 // author: 'required|accepted',
19 author: schema.string(), 19 author: schema.string(),
20 svg: schema.string([rules.url()]), 20 svg: schema.string([rules.url()]),
21}); 21})
22 22
23const searchSchema = schema.create({ 23const searchSchema = schema.create({
24 needle: schema.string(), 24 needle: schema.string(),
25}); 25})
26 26
27const downloadSchema = schema.create({ 27const downloadSchema = schema.create({
28 // TODO: Check if this is correct 28 // TODO: Check if this is correct
29 // recipe: 'required|accepted', 29 // recipe: 'required|accepted',
30 recipe: schema.string(), 30 recipe: schema.string(),
31}); 31})
32 32
33const compress = (src: string, dest: string) => 33const compress = (src: string, dest: string) =>
34 new Promise((resolve, reject) => { 34 new Promise((resolve, reject) => {
@@ -37,87 +37,76 @@ const compress = (src: string, dest: string) =>
37 src, 37 src,
38 dest, 38 dest,
39 }, 39 },
40 err => { 40 (err) => {
41 if (err) { 41 if (err) {
42 reject(err); 42 reject(err)
43 } else { 43 } else {
44 resolve(dest); 44 resolve(dest)
45 } 45 }
46 }, 46 }
47 ); 47 )
48 }); 48 })
49 49
50export default class RecipesController { 50export default class RecipesController {
51 // List official and custom recipes 51 // List official and custom recipes
52 public async list({ response }: HttpContextContract) { 52 public async list({ response }: HttpContext) {
53 const officialRecipes = fs.readJsonSync( 53 const officialRecipes = fs.readJsonSync(path.join(app.appRoot, 'recipes', 'all.json'))
54 path.join(Application.appRoot, 'recipes', 'all.json'), 54 const customRecipesArray = await Recipe.all()
55 ); 55 const customRecipes = customRecipesArray.map((recipe) => ({
56 const customRecipesArray = await Recipe.all();
57 const customRecipes = customRecipesArray.map(recipe => ({
58 id: recipe.recipeId, 56 id: recipe.recipeId,
59 name: recipe.name, 57 name: recipe.name,
60 ...(typeof recipe.data === 'string' 58 ...(typeof recipe.data === 'string' ? JSON.parse(recipe.data) : recipe.data),
61 ? JSON.parse(recipe.data) 59 }))
62 : recipe.data),
63 }));
64 60
65 const recipes = [...officialRecipes, ...customRecipes]; 61 const recipes = [...officialRecipes, ...customRecipes]
66 62
67 return response.send(recipes); 63 return response.send(recipes)
68 } 64 }
69 65
70 // TODO: Test this endpoint 66 // TODO: Test this endpoint
71 // Create a new recipe using the new.html page 67 // Create a new recipe using the new.html page
72 public async create({ request, response }: HttpContextContract) { 68 public async create({ request, response }: HttpContext) {
73 // Check if recipe creation is enabled 69 // Check if recipe creation is enabled
74 if (isCreationEnabled === 'false') { 70 if (isCreationEnabled === 'false') {
75 return response.send( 71 return response.send("This server doesn't allow the creation of new recipes.")
76 'This server doesn\'t allow the creation of new recipes.',
77 );
78 } 72 }
79 73
80 // Validate user input 74 // Validate user input
81 let data; 75 let data
82 try { 76 try {
83 data = await request.validate({ schema: createSchema }); 77 data = await request.validate({ schema: createSchema })
84 } catch (error) { 78 } catch (error) {
85 return response.status(401).send({ 79 return response.status(401).send({
86 message: 'Invalid POST arguments', 80 message: 'Invalid POST arguments',
87 messages: error.messages, 81 messages: error.messages,
88 status: 401, 82 status: 401,
89 }); 83 })
90 } 84 }
91 85
92 if (!data.id) { 86 if (!data.id) {
93 return response.send('Please provide an ID'); 87 return response.send('Please provide an ID')
94 } 88 }
95 89
96 // Check for invalid characters 90 // Check for invalid characters
97 if (/\.+/.test(data.id) || /\/+/.test(data.id)) { 91 if (/\.+/.test(data.id) || /\/+/.test(data.id)) {
98 return response.send( 92 return response.send('Invalid recipe name. Your recipe name may not contain "." or "/"')
99 'Invalid recipe name. Your recipe name may not contain "." or "/"',
100 );
101 } 93 }
102 94
103 // Clear temporary recipe folder 95 // Clear temporary recipe folder
104 await fs.emptyDir(Application.tmpPath('recipe')); 96 await fs.emptyDir(app.tmpPath('recipe'))
105 97
106 // Move uploaded files to temporary path 98 // Move uploaded files to temporary path
107 const files = request.file('files'); 99 const files = request.file('files')
108 if (!files) { 100 if (!files) {
109 return response.abort('Error processsing files.'); 101 return response.abort('Error processsing files.')
110 } 102 }
111 await files.move(Application.tmpPath('recipe')); 103 await files.move(app.tmpPath('recipe'))
112 104
113 // Compress files to .tar.gz file 105 // Compress files to .tar.gz file
114 const source = Application.tmpPath('recipe'); 106 const source = app.tmpPath('recipe')
115 const destination = path.join( 107 const destination = path.join(app.appRoot, `/recipes/archives/${data.id}.tar.gz`)
116 Application.appRoot,
117 `/recipes/archives/${data.id}.tar.gz`,
118 );
119 108
120 compress(source, destination); 109 compress(source, destination)
121 110
122 // Create recipe in db 111 // Create recipe in db
123 await Recipe.create({ 112 await Recipe.create({
@@ -132,123 +121,111 @@ export default class RecipesController {
132 svg: data.svg, 121 svg: data.svg,
133 }, 122 },
134 }), 123 }),
135 }); 124 })
136 125
137 return response.send('Created new recipe'); 126 return response.send('Created new recipe')
138 } 127 }
139 128
140 // Search official and custom recipes 129 // Search official and custom recipes
141 public async search({ request, response }: HttpContextContract) { 130 public async search({ request, response }: HttpContext) {
142 // Validate user input 131 // Validate user input
143 let data; 132 let data
144 try { 133 try {
145 data = await request.validate({ schema: searchSchema }); 134 data = await request.validate({ schema: searchSchema })
146 } catch (error) { 135 } catch (error) {
147 return response.status(401).send({ 136 return response.status(401).send({
148 message: 'Please provide a needle', 137 message: 'Please provide a needle',
149 messages: error.messages, 138 messages: error.messages,
150 status: 401, 139 status: 401,
151 }); 140 })
152 } 141 }
153 142
154 const { needle } = data; 143 const { needle } = data
155 144
156 // Get results 145 // Get results
157 let results; 146 let results
158 147
159 if (needle === 'ferdium:custom') { 148 if (needle === 'ferdium:custom') {
160 const dbResults = await Recipe.all(); 149 const dbResults = await Recipe.all()
161 results = dbResults.map(recipe => ({ 150 results = dbResults.map((recipe) => ({
162 id: recipe.recipeId, 151 id: recipe.recipeId,
163 name: recipe.name, 152 name: recipe.name,
164 ...(typeof recipe.data === 'string' 153 ...(typeof recipe.data === 'string' ? JSON.parse(recipe.data) : recipe.data),
165 ? JSON.parse(recipe.data) 154 }))
166 : recipe.data),
167 }));
168 } else { 155 } else {
169 const localResultsArray = await Recipe.query().where( 156 const localResultsArray = await Recipe.query().where('name', 'LIKE', `%${needle}%`)
170 'name', 157 results = localResultsArray.map((recipe) => ({
171 'LIKE',
172 `%${needle}%`,
173 );
174 results = localResultsArray.map(recipe => ({
175 id: recipe.recipeId, 158 id: recipe.recipeId,
176 name: recipe.name, 159 name: recipe.name,
177 ...(typeof recipe.data === 'string' 160 ...(typeof recipe.data === 'string' ? JSON.parse(recipe.data) : recipe.data),
178 ? JSON.parse(recipe.data) 161 }))
179 : recipe.data),
180 }));
181 } 162 }
182 163
183 return response.send(results); 164 return response.send(results)
184 } 165 }
185 166
186 public popularRecipes({ response }: HttpContextContract) { 167 public popularRecipes({ response }: HttpContext) {
187 return response.send( 168 return response.send(
188 fs 169 fs
189 .readJsonSync(path.join(Application.appRoot, 'recipes', 'all.json')) 170 .readJsonSync(path.join(app.appRoot, 'recipes', 'all.json'))
190 // eslint-disable-next-line @typescript-eslint/no-explicit-any 171 // eslint-disable-next-line @typescript-eslint/no-explicit-any
191 .filter((recipe: any) => recipe.featured), 172 .filter((recipe: any) => recipe.featured)
192 ); 173 )
193 } 174 }
194 175
195 // TODO: test this endpoint 176 // TODO: test this endpoint
196 public update({ request, response }: HttpContextContract) { 177 public update({ request, response }: HttpContext) {
197 const updates = []; 178 const updates = []
198 const recipes = request.all(); 179 const recipes = request.all()
199 const allJson = fs.readJsonSync( 180 const allJson = fs.readJsonSync(path.join(app.appRoot, 'recipes', 'all.json'))
200 path.join(Application.appRoot, 'recipes', 'all.json'),
201 );
202 181
203 for (const recipe of Object.keys(recipes)) { 182 for (const recipe of Object.keys(recipes)) {
204 const version = recipes[recipe]; 183 const version = recipes[recipe]
205 184
206 // Find recipe in local recipe repository 185 // Find recipe in local recipe repository
207 // eslint-disable-next-line @typescript-eslint/no-explicit-any 186 // eslint-disable-next-line @typescript-eslint/no-explicit-any
208 const localRecipe = allJson.find((r: any) => r.id === recipe); 187 const localRecipe = allJson.find((r: any) => r.id === recipe)
209 if (localRecipe && semver.lt(version, localRecipe.version)) { 188 if (localRecipe && semver.lt(version, localRecipe.version)) {
210 updates.push(recipe); 189 updates.push(recipe)
211 } 190 }
212 } 191 }
213 192
214 return response.send(updates); 193 return response.send(updates)
215 } 194 }
216 195
217 // TODO: test this endpoint 196 // TODO: test this endpoint
218 // Download a recipe 197 // Download a recipe
219 public async download({ response, params }: HttpContextContract) { 198 public async download({ response, params }: HttpContext) {
220 // Validate user input 199 // Validate user input
221 let data; 200 let data
222 try { 201 try {
223 data = await validator.validate({ 202 data = await validator.validate({
224 data: params, 203 data: params,
225 schema: downloadSchema, 204 schema: downloadSchema,
226 }); 205 })
227 } catch (error) { 206 } catch (error) {
228 return response.status(401).send({ 207 return response.status(401).send({
229 message: 'Please provide a recipe ID', 208 message: 'Please provide a recipe ID',
230 messages: error.messages, 209 messages: error.messages,
231 status: 401, 210 status: 401,
232 }); 211 })
233 } 212 }
234 213
235 const service = data.recipe; 214 const service = data.recipe
236 215
237 // Check for invalid characters 216 // Check for invalid characters
238 if (/\.+/.test(service) || /\/+/.test(service)) { 217 if (/\.+/.test(service) || /\/+/.test(service)) {
239 return response.send('Invalid recipe name'); 218 return response.send('Invalid recipe name')
240 } 219 }
241 220
242 // Check if recipe exists in recipes folder 221 // Check if recipe exists in recipes folder
243 if (await Drive.exists(`${service}.tar.gz`)) { 222 if (await Drive.exists(`${service}.tar.gz`)) {
244 return response 223 return response.type('.tar.gz').send(await Drive.get(`${service}.tar.gz`))
245 .type('.tar.gz')
246 .send(await Drive.get(`${service}.tar.gz`));
247 } 224 }
248 225
249 return response.status(400).send({ 226 return response.status(400).send({
250 message: 'Recipe not found', 227 message: 'Recipe not found',
251 code: 'recipe-not-found', 228 code: 'recipe-not-found',
252 }); 229 })
253 } 230 }
254} 231}