Compare commits
No commits in common. "040ae43652d9ca9522107a358aaf054c7d198949" and "1f8206ec88243d855b2ee851f4a31679e7831d85" have entirely different histories.
040ae43652
...
1f8206ec88
14 changed files with 26 additions and 400 deletions
|
|
@ -1,4 +0,0 @@
|
||||||
Clean up avant une nouvelle année scolaire :
|
|
||||||
- Effacer les mails de tous les utilisateurs (users.email) dans la base de données.
|
|
||||||
- Mettre la dernière activité à 1 mois pour l'autocomplete (auth)
|
|
||||||
- Supprimer les repas (meals) (et les plats associés avec cascade)
|
|
||||||
|
|
@ -1,10 +1,5 @@
|
||||||
import type { HttpContext } from '@adonisjs/core/http'
|
import type { HttpContext } from '@adonisjs/core/http'
|
||||||
import {
|
import { registerValidator, requestLoginValidator, verifyCodeValidator } from '#validators/auth'
|
||||||
credentialsValidator,
|
|
||||||
registerValidator,
|
|
||||||
requestLoginValidator,
|
|
||||||
verifyCodeValidator,
|
|
||||||
} from '#validators/auth'
|
|
||||||
import mail from '@adonisjs/mail/services/main'
|
import mail from '@adonisjs/mail/services/main'
|
||||||
import { AuthService } from '#services/auth_service'
|
import { AuthService } from '#services/auth_service'
|
||||||
import { inject } from '@adonisjs/core'
|
import { inject } from '@adonisjs/core'
|
||||||
|
|
@ -12,7 +7,6 @@ import app from '@adonisjs/core/services/app'
|
||||||
import env from '#start/env'
|
import env from '#start/env'
|
||||||
import User from '#models/user'
|
import User from '#models/user'
|
||||||
import { DateTime } from 'luxon'
|
import { DateTime } from 'luxon'
|
||||||
import redis from '@adonisjs/redis/services/main'
|
|
||||||
|
|
||||||
@inject()
|
@inject()
|
||||||
export default class AuthController {
|
export default class AuthController {
|
||||||
|
|
@ -132,7 +126,10 @@ export default class AuthController {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const user = await User.query().where('id', userId).where('className', className).first()
|
const user = await User.query()
|
||||||
|
.where('id', userId)
|
||||||
|
.where('className', className)
|
||||||
|
.first()
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return response.badRequest({
|
return response.badRequest({
|
||||||
success: false,
|
success: false,
|
||||||
|
|
@ -166,29 +163,6 @@ export default class AuthController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// POST /auth/test
|
|
||||||
public async testCredentials({ request, auth }: HttpContext) {
|
|
||||||
const { username, password } = await request.validateUsing(credentialsValidator)
|
|
||||||
await redis.publish(
|
|
||||||
'jobs_queue',
|
|
||||||
JSON.stringify({
|
|
||||||
type: 5, // Test credentials
|
|
||||||
bj_username: username,
|
|
||||||
bj_password: password,
|
|
||||||
user_id: auth.user!.id,
|
|
||||||
class_name: env.get('DEFAULT_CLASS_NAME'),
|
|
||||||
})
|
|
||||||
)
|
|
||||||
|
|
||||||
return { success: true, message: 'Testing credentials...' }
|
|
||||||
}
|
|
||||||
|
|
||||||
// GET /auth/status
|
|
||||||
public async status({ auth }: HttpContext) {
|
|
||||||
const redisKey = `auth_success_${auth.user!.id}`
|
|
||||||
return { success: true, data: { authenticated: await redis.get(redisKey) } }
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Magic link login
|
// TODO: Magic link login
|
||||||
// magicLink({ }: HttpContext) {
|
// magicLink({ }: HttpContext) {
|
||||||
// // Validate signed url (adonis)
|
// // Validate signed url (adonis)
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,3 @@
|
||||||
import Meal from '#models/meal'
|
|
||||||
import MealRegistration from '#models/meal_registration'
|
|
||||||
import env from '#start/env'
|
|
||||||
import { updateMealsRegistrationsValidator } from '#validators/meal'
|
|
||||||
import type { HttpContext } from '@adonisjs/core/http'
|
import type { HttpContext } from '@adonisjs/core/http'
|
||||||
import redis from '@adonisjs/redis/services/main'
|
import redis from '@adonisjs/redis/services/main'
|
||||||
import { DateTime } from 'luxon'
|
import { DateTime } from 'luxon'
|
||||||
|
|
@ -24,46 +20,6 @@ export default class InternalsController {
|
||||||
console.log(`Colles fetching for class ${className} completed.`)
|
console.log(`Colles fetching for class ${className} completed.`)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// POST /internals/fetch-meals
|
|
||||||
async fetchMeals({ response }: HttpContext) {
|
|
||||||
await redis.publish(
|
|
||||||
'jobs_queue',
|
|
||||||
JSON.stringify({
|
|
||||||
type: 3, // Fetch meals
|
|
||||||
class_name: env.get('DEFAULT_CLASS_NAME'),
|
|
||||||
})
|
|
||||||
)
|
|
||||||
return response.send({ success: true, message: 'Fetching meals...' })
|
|
||||||
}
|
|
||||||
|
|
||||||
// POST /internals/meals-registrations
|
|
||||||
async updateMealsRegistrations({ response, request }: HttpContext) {
|
|
||||||
const { userId, meals } = await request.validateUsing(updateMealsRegistrationsValidator)
|
|
||||||
|
|
||||||
// Remove all existing registrations for the user
|
|
||||||
await MealRegistration.query().where('user_id', userId).delete()
|
|
||||||
for (const mealData of meals) {
|
|
||||||
const meal = await Meal.query()
|
|
||||||
.where({
|
|
||||||
date: mealData.date,
|
|
||||||
type: mealData.meal_type === 'lunch' ? 0 : 1,
|
|
||||||
})
|
|
||||||
.first()
|
|
||||||
if (meal) {
|
|
||||||
// Create a new registration
|
|
||||||
await MealRegistration.create({
|
|
||||||
mealId: meal.id,
|
|
||||||
userId: userId,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return response.send({
|
|
||||||
success: true,
|
|
||||||
message: 'Meal registrations updated successfully',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function queue(className: string) {
|
async function queue(className: string) {
|
||||||
|
|
|
||||||
|
|
@ -1,134 +0,0 @@
|
||||||
import Meal from '#models/meal'
|
|
||||||
import MealRegistration from '#models/meal_registration'
|
|
||||||
import { credentialsValidator } from '#validators/auth'
|
|
||||||
import { createMealValidator } from '#validators/meal'
|
|
||||||
import type { HttpContext } from '@adonisjs/core/http'
|
|
||||||
import redis from '@adonisjs/redis/services/main'
|
|
||||||
import { DateTime } from 'luxon'
|
|
||||||
|
|
||||||
export default class MealsController {
|
|
||||||
// POST /meals
|
|
||||||
public async create({ request }: HttpContext) {
|
|
||||||
const data = await request.validateUsing(createMealValidator)
|
|
||||||
|
|
||||||
if ('meals' in data) {
|
|
||||||
// Remove all already submittable meals
|
|
||||||
await Meal.query().where('submittable', true).update({ submittable: false })
|
|
||||||
// Set new meals as submittable
|
|
||||||
for (const mealData of data.meals) {
|
|
||||||
const meal = await Meal.query()
|
|
||||||
.where('date', mealData.date)
|
|
||||||
.where('type', mealData.meal_type === 'lunch' ? 0 : 1)
|
|
||||||
.first()
|
|
||||||
if (meal) {
|
|
||||||
meal.submittable = true
|
|
||||||
await meal.save()
|
|
||||||
} else {
|
|
||||||
console.warn('Meal to set as submittable not found:', mealData)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return { message: 'Meals updated successfully' }
|
|
||||||
}
|
|
||||||
|
|
||||||
const { date, type, courses } = data
|
|
||||||
const mealType = type === 'Déjeuner' ? 0 : 1
|
|
||||||
|
|
||||||
// Check if a meal with the same date and type already exists
|
|
||||||
const existingMeal = await Meal.query()
|
|
||||||
.where({ date, type: mealType })
|
|
||||||
.preload('courses')
|
|
||||||
.first()
|
|
||||||
if (existingMeal) {
|
|
||||||
// Check if the existing meal has the same courses
|
|
||||||
const existingCourseNames = existingMeal.courses.map((course) => course.description)
|
|
||||||
const newCourseNames = courses.map((course) => course.description)
|
|
||||||
if (
|
|
||||||
existingCourseNames.length === newCourseNames.length &&
|
|
||||||
existingCourseNames.every((name) => newCourseNames.includes(name))
|
|
||||||
) {
|
|
||||||
return existingMeal
|
|
||||||
}
|
|
||||||
|
|
||||||
// If not, delete the existing meal (and its courses, thanks to cascade delete)
|
|
||||||
await existingMeal.delete()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create the new meal
|
|
||||||
const meal = await Meal.create({
|
|
||||||
date,
|
|
||||||
type: mealType,
|
|
||||||
})
|
|
||||||
await meal.related('courses').createMany(courses)
|
|
||||||
return meal
|
|
||||||
}
|
|
||||||
|
|
||||||
// GET /meals
|
|
||||||
public async index({ auth }: HttpContext) {
|
|
||||||
const meals = await Meal.query().orderBy('date', 'asc').preload('courses')
|
|
||||||
const data = meals.map(async (meal) => {
|
|
||||||
if (meal.submittable) {
|
|
||||||
const isRegistered = await MealRegistration.query()
|
|
||||||
.where('meal_id', meal.id)
|
|
||||||
.where('user_id', auth.user!.id)
|
|
||||||
.first()
|
|
||||||
// Remove temporary registrations that are older than 5 minutes
|
|
||||||
if (isRegistered?.temporary) {
|
|
||||||
const oneHourAgo = DateTime.now().minus({ minutes: 5 })
|
|
||||||
if (isRegistered.createdAt < oneHourAgo) {
|
|
||||||
await isRegistered.delete()
|
|
||||||
return meal.serialize()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
...meal.serialize(),
|
|
||||||
isRegistered: !!isRegistered,
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return meal.serialize()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return Promise.all(data)
|
|
||||||
}
|
|
||||||
|
|
||||||
// POST /meals/:id
|
|
||||||
// TODO: Unregister from meal
|
|
||||||
public async registerForMeal({ params, auth, response, request }: HttpContext) {
|
|
||||||
const { username, password } = await request.validateUsing(credentialsValidator)
|
|
||||||
|
|
||||||
const meal = await Meal.find(params.id)
|
|
||||||
if (!meal) {
|
|
||||||
return response.notFound({ message: 'Meal not found' })
|
|
||||||
}
|
|
||||||
if (!meal.submittable) {
|
|
||||||
return response.badRequest({ message: 'Meal is not submittable' })
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a bullshit registration
|
|
||||||
// (to confirm to the user that the registration is in progress...)
|
|
||||||
// that will eventually be deleted if the registration fails (worker callback)
|
|
||||||
await MealRegistration.firstOrCreate({
|
|
||||||
mealId: meal.id,
|
|
||||||
userId: auth.user!.id,
|
|
||||||
temporary: true,
|
|
||||||
})
|
|
||||||
|
|
||||||
// Register the user for the meal
|
|
||||||
await redis.publish(
|
|
||||||
'jobs_queue',
|
|
||||||
JSON.stringify({
|
|
||||||
type: 4, // Submit meals
|
|
||||||
meal: {
|
|
||||||
date: DateTime.fromJSDate(meal.date).toISODate(),
|
|
||||||
meal_type: meal.type === 0 ? 'lunch' : 'dinner',
|
|
||||||
},
|
|
||||||
class_name: auth.user!.className,
|
|
||||||
user_id: auth.user!.id,
|
|
||||||
bj_username: username,
|
|
||||||
bj_password: password,
|
|
||||||
})
|
|
||||||
)
|
|
||||||
|
|
||||||
return { success: true, message: 'Registration in progress' }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,15 +0,0 @@
|
||||||
import { BaseModel, column } from '@adonisjs/lucid/orm'
|
|
||||||
|
|
||||||
export default class Course extends BaseModel {
|
|
||||||
@column({ isPrimary: true })
|
|
||||||
declare id: number
|
|
||||||
|
|
||||||
@column()
|
|
||||||
declare mealId: number
|
|
||||||
|
|
||||||
@column()
|
|
||||||
declare name: string
|
|
||||||
|
|
||||||
@column()
|
|
||||||
declare description: string
|
|
||||||
}
|
|
||||||
|
|
@ -1,25 +0,0 @@
|
||||||
import { BaseModel, column, computed, hasMany } from '@adonisjs/lucid/orm'
|
|
||||||
import Course from './course.js'
|
|
||||||
import type { HasMany } from '@adonisjs/lucid/types/relations'
|
|
||||||
|
|
||||||
export default class Meal extends BaseModel {
|
|
||||||
@column({ isPrimary: true })
|
|
||||||
declare id: number
|
|
||||||
|
|
||||||
@column()
|
|
||||||
declare date: Date
|
|
||||||
|
|
||||||
@column()
|
|
||||||
declare type: 0 | 1 // 0 = lunch, 1 = dinner
|
|
||||||
|
|
||||||
@column()
|
|
||||||
declare submittable: boolean
|
|
||||||
|
|
||||||
@computed()
|
|
||||||
get name() {
|
|
||||||
return this.type === 0 ? 'Déjeuner' : 'Dîner'
|
|
||||||
}
|
|
||||||
|
|
||||||
@hasMany(() => Course)
|
|
||||||
declare courses: HasMany<typeof Course>
|
|
||||||
}
|
|
||||||
|
|
@ -1,19 +0,0 @@
|
||||||
import { BaseModel, column } from '@adonisjs/lucid/orm'
|
|
||||||
import { DateTime } from 'luxon'
|
|
||||||
|
|
||||||
export default class MealRegistration extends BaseModel {
|
|
||||||
@column({ isPrimary: true })
|
|
||||||
declare id: number
|
|
||||||
|
|
||||||
@column()
|
|
||||||
declare mealId: number
|
|
||||||
|
|
||||||
@column()
|
|
||||||
declare userId: number
|
|
||||||
|
|
||||||
@column()
|
|
||||||
declare temporary: boolean
|
|
||||||
|
|
||||||
@column.dateTime({ autoCreate: true })
|
|
||||||
declare createdAt: DateTime
|
|
||||||
}
|
|
||||||
|
|
@ -27,13 +27,6 @@ export const registerValidator = vine.compile(
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
export const credentialsValidator = vine.compile(
|
|
||||||
vine.object({
|
|
||||||
username: vine.string().minLength(1).maxLength(255),
|
|
||||||
password: vine.string().minLength(1).maxLength(255),
|
|
||||||
})
|
|
||||||
)
|
|
||||||
|
|
||||||
// TODO: Magic link login
|
// TODO: Magic link login
|
||||||
// export const magicLinkValidator = vine.compile(
|
// export const magicLinkValidator = vine.compile(
|
||||||
// vine.object({
|
// vine.object({
|
||||||
|
|
|
||||||
|
|
@ -1,40 +0,0 @@
|
||||||
import vine from '@vinejs/vine'
|
|
||||||
|
|
||||||
const meals = vine.array(
|
|
||||||
vine.object({
|
|
||||||
date: vine.string(), // YYYY-MM-DD
|
|
||||||
meal_type: vine.string().in(['dinner', 'lunch']),
|
|
||||||
})
|
|
||||||
)
|
|
||||||
|
|
||||||
export const createMealValidator = vine.compile(
|
|
||||||
vine.union([
|
|
||||||
vine.union.if(
|
|
||||||
(data) => vine.helpers.isObject(data) && 'meals' in data,
|
|
||||||
vine.object({
|
|
||||||
meals,
|
|
||||||
})
|
|
||||||
),
|
|
||||||
vine.union.else(
|
|
||||||
vine.object({
|
|
||||||
date: vine.date(),
|
|
||||||
type: vine.string().in(['Déjeuner', 'Dîner']),
|
|
||||||
courses: vine
|
|
||||||
.array(
|
|
||||||
vine.object({
|
|
||||||
name: vine.string().minLength(1).maxLength(255),
|
|
||||||
description: vine.string().minLength(1),
|
|
||||||
})
|
|
||||||
)
|
|
||||||
.minLength(1),
|
|
||||||
})
|
|
||||||
),
|
|
||||||
])
|
|
||||||
)
|
|
||||||
|
|
||||||
export const updateMealsRegistrationsValidator = vine.compile(
|
|
||||||
vine.object({
|
|
||||||
userId: vine.number().min(1),
|
|
||||||
meals,
|
|
||||||
})
|
|
||||||
)
|
|
||||||
|
|
@ -1,18 +0,0 @@
|
||||||
import { BaseSchema } from '@adonisjs/lucid/schema'
|
|
||||||
|
|
||||||
export default class extends BaseSchema {
|
|
||||||
protected tableName = 'meals'
|
|
||||||
|
|
||||||
async up() {
|
|
||||||
this.schema.createTable(this.tableName, (table) => {
|
|
||||||
table.increments('id')
|
|
||||||
table.date('date').notNullable()
|
|
||||||
table.integer('type').notNullable() // 0 = lunch, 1 = dinner
|
|
||||||
table.boolean('submittable').defaultTo(false)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
async down() {
|
|
||||||
this.schema.dropTable(this.tableName)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,18 +0,0 @@
|
||||||
import { BaseSchema } from '@adonisjs/lucid/schema'
|
|
||||||
|
|
||||||
export default class extends BaseSchema {
|
|
||||||
protected tableName = 'courses'
|
|
||||||
|
|
||||||
async up() {
|
|
||||||
this.schema.createTable(this.tableName, (table) => {
|
|
||||||
table.increments('id')
|
|
||||||
table.integer('meal_id').unsigned().references('id').inTable('meals').onDelete('CASCADE')
|
|
||||||
table.string('name').notNullable()
|
|
||||||
table.text('description').notNullable()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
async down() {
|
|
||||||
this.schema.dropTable(this.tableName)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,19 +0,0 @@
|
||||||
import { BaseSchema } from '@adonisjs/lucid/schema'
|
|
||||||
|
|
||||||
export default class extends BaseSchema {
|
|
||||||
protected tableName = 'meal_registrations'
|
|
||||||
|
|
||||||
async up() {
|
|
||||||
this.schema.createTable(this.tableName, (table) => {
|
|
||||||
table.increments('id')
|
|
||||||
table.integer('meal_id').unsigned().references('id').inTable('meals').onDelete('CASCADE')
|
|
||||||
table.integer('user_id').unsigned().references('id').inTable('users').onDelete('CASCADE')
|
|
||||||
table.boolean('temporary').defaultTo(false)
|
|
||||||
table.timestamp('created_at').notNullable()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
async down() {
|
|
||||||
this.schema.dropTable(this.tableName)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -74,5 +74,5 @@ export default await Env.create(new URL('../', import.meta.url), {
|
||||||
|
|
||||||
API_BEARER_TOKEN: Env.schema.string(),
|
API_BEARER_TOKEN: Env.schema.string(),
|
||||||
|
|
||||||
DEFAULT_CLASS_NAME: Env.schema.string(),
|
MENUS_PATH: Env.schema.string(),
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -18,20 +18,16 @@ import { middleware } from './kernel.js'
|
||||||
|
|
||||||
const AuthController = () => import('#controllers/auth_controller')
|
const AuthController = () => import('#controllers/auth_controller')
|
||||||
|
|
||||||
router
|
router.group(() => {
|
||||||
.group(() => {
|
router.post('/auth/request', [AuthController, 'requestLogin']).use(authThrottle)
|
||||||
router.post('/request', [AuthController, 'requestLogin']).use(authThrottle)
|
router.post('/auth/verify', [AuthController, 'verifyCode']).use(throttle)
|
||||||
router.post('/verify', [AuthController, 'verifyCode']).use(throttle)
|
router.post('/auth/register', [AuthController, 'register']).use(throttle)
|
||||||
router.post('/register', [AuthController, 'register']).use(throttle)
|
router.post('/auth/logout', [AuthController, 'logout'])
|
||||||
router.post('/logout', [AuthController, 'logout'])
|
router.get('/auth/autocomplete', [AuthController, 'listNames']).use(throttle)
|
||||||
router.get('/autocomplete', [AuthController, 'listNames']).use(throttle)
|
// TODO: Magic link login
|
||||||
router.post('/test', [AuthController, 'testCredentials']).use(throttle).use(middleware.auth())
|
// router.get('/auth/magic-link', 'AuthController.magicLink').use(throttle)
|
||||||
router.get('/status', [AuthController, 'status']).use(middleware.auth())
|
// router.get('/auth/listen', 'AuthController.listen')
|
||||||
// TODO: Magic link login
|
})
|
||||||
// router.get('/auth/magic-link', 'AuthController.magicLink').use(throttle)
|
|
||||||
// router.get('/auth/listen', 'AuthController.listen')
|
|
||||||
})
|
|
||||||
.prefix('/auth')
|
|
||||||
|
|
||||||
const UserController = () => import('#controllers/user_controller')
|
const UserController = () => import('#controllers/user_controller')
|
||||||
router.get('/users/@me', [UserController, 'me']).use(middleware.auth())
|
router.get('/users/@me', [UserController, 'me']).use(middleware.auth())
|
||||||
|
|
@ -71,21 +67,20 @@ const InternalsController = () => import('#controllers/internals_controller')
|
||||||
router
|
router
|
||||||
.group(() => {
|
.group(() => {
|
||||||
router.post('/back-fetch', [InternalsController, 'backFetch'])
|
router.post('/back-fetch', [InternalsController, 'backFetch'])
|
||||||
router.post('/fetch-meals', [InternalsController, 'fetchMeals'])
|
|
||||||
router.post('/meals-registrations', [InternalsController, 'updateMealsRegistrations'])
|
|
||||||
})
|
})
|
||||||
.prefix('/internals')
|
.prefix('/internals')
|
||||||
.use(middleware.internal())
|
.use(middleware.internal())
|
||||||
|
|
||||||
const MealsController = () => import('#controllers/meals_controller')
|
|
||||||
router
|
|
||||||
.group(() => {
|
|
||||||
router.post('/', [MealsController, 'create']).use(middleware.internal())
|
|
||||||
router.get('/', [MealsController, 'index']).use(middleware.auth())
|
|
||||||
router.post('/:id', [MealsController, 'registerForMeal']).use(middleware.auth())
|
|
||||||
})
|
|
||||||
.prefix('/meals')
|
|
||||||
|
|
||||||
router.get('/health', async () => {
|
router.get('/health', async () => {
|
||||||
return { status: 'ok' }
|
return { status: 'ok' }
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// BETA: Serve menus.json file
|
||||||
|
import fs from 'node:fs/promises'
|
||||||
|
import env from './env.js'
|
||||||
|
|
||||||
|
router.get('/menus', async () => {
|
||||||
|
// Return menus.json file
|
||||||
|
const data = await fs.readFile(env.get('MENUS_PATH'), 'utf-8')
|
||||||
|
return JSON.parse(data)
|
||||||
|
})
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue