room reservation model, controller, seeder and migration files added
This commit is contained in:
@@ -1,10 +1,10 @@
|
||||
import type { HttpContext } from '@adonisjs/core/http'
|
||||
import app from '@adonisjs/core/services/app'
|
||||
import drive from '@adonisjs/drive/services/main'
|
||||
import logger from '@adonisjs/core/services/logger'
|
||||
import { cuid } from '@adonisjs/core/helpers'
|
||||
import { createReadStream } from 'node:fs'
|
||||
import { promises as fs } from 'node:fs'
|
||||
import { randomUUID } from 'node:crypto'
|
||||
import { extname } from 'node:path'
|
||||
import { Jimp } from 'jimp'
|
||||
import fs from 'node:fs/promises'
|
||||
|
||||
const allowedExtnames = ['jpg', 'jpeg', 'png', 'webp', 'gif']
|
||||
|
||||
@@ -32,36 +32,49 @@ export default class ImagesController {
|
||||
return response.badRequest({ message: message || 'Invalid image file' })
|
||||
}
|
||||
|
||||
const uploadsDir = app.makePath('storage', 'uploads')
|
||||
await fs.mkdir(uploadsDir, { recursive: true })
|
||||
|
||||
const extension = image.extname || 'bin'
|
||||
const filename = `${cuid()}.${extension}`
|
||||
await image.move(uploadsDir, { name: filename, overwrite: false })
|
||||
const filename = `${randomUUID()}.${extension}`
|
||||
|
||||
if (image.state !== 'moved') {
|
||||
logger.error('Image upload failed: %s', image.errors)
|
||||
return response.internalServerError({ message: 'Image upload failed' })
|
||||
await drive.use().put(filename, await fs.readFile(image.tmpPath!))
|
||||
|
||||
// Generate thumbnail
|
||||
try {
|
||||
const jimpImage = await Jimp.read(image.tmpPath!)
|
||||
const buffer = await jimpImage
|
||||
.resize({ w: 200 }) // Resize to width 200px, maintain aspect ratio
|
||||
.getBuffer('image/jpeg')
|
||||
|
||||
await drive.use().put(`thumbnails/${filename}`, buffer)
|
||||
} catch (error) {
|
||||
logger.error('Thumbnail generation failed: %s', error.message)
|
||||
// We don't fail the upload if thumbnail generation fails, but we log it
|
||||
}
|
||||
|
||||
return response.ok({
|
||||
filename,
|
||||
url: `/api/images/${filename}`,
|
||||
thumbnailUrl: `/api/images/${filename}?type=thumbnail`,
|
||||
})
|
||||
}
|
||||
|
||||
async show({ params, response }: HttpContext) {
|
||||
async show({ params, request, response }: HttpContext) {
|
||||
const filename = String(params.filename || '')
|
||||
const type = request.input('type')
|
||||
|
||||
if (!filename || filename.includes('/') || filename.includes('\\')) {
|
||||
return response.badRequest({ message: 'Invalid image name' })
|
||||
}
|
||||
|
||||
const filePath = app.makePath('storage', 'uploads', filename)
|
||||
let key = filename
|
||||
|
||||
try {
|
||||
await fs.access(filePath)
|
||||
} catch {
|
||||
if (type === 'thumbnail') {
|
||||
const thumbnailKey = `thumbnails/${filename}`
|
||||
if (await drive.use().exists(thumbnailKey)) {
|
||||
key = thumbnailKey
|
||||
}
|
||||
}
|
||||
|
||||
if (!(await drive.use().exists(key))) {
|
||||
return response.notFound({ message: 'Image not found' })
|
||||
}
|
||||
|
||||
@@ -69,6 +82,26 @@ export default class ImagesController {
|
||||
response.header('Content-Type', mimeType)
|
||||
response.header('Cache-Control', 'public, max-age=86400')
|
||||
|
||||
return response.stream(createReadStream(filePath))
|
||||
return response.stream(await drive.use().getStream(key))
|
||||
}
|
||||
|
||||
async list() {
|
||||
const { objects } = await drive.use().listAll('')
|
||||
const images = []
|
||||
|
||||
for (const file of objects) {
|
||||
if (file.isFile) {
|
||||
images.push({
|
||||
filename: file.key,
|
||||
url: `/api/images/${file.key}`,
|
||||
thumbnailUrl: `/api/images/${file.key}?type=thumbnail`,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return images
|
||||
}
|
||||
async delete() {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { HttpContext } from '@adonisjs/core/http'
|
||||
import { DateTime } from 'luxon'
|
||||
import Room from '#models/room'
|
||||
import Reservation from '#models/reservation'
|
||||
import {
|
||||
@@ -81,7 +82,11 @@ export default class RoomReservationsController {
|
||||
async store({ request, response }: HttpContext) {
|
||||
try {
|
||||
const data = await request.validateUsing(createReservationValidator)
|
||||
const reservation = await Reservation.create(data)
|
||||
const reservation = await Reservation.create({
|
||||
...data,
|
||||
startsAt: DateTime.fromISO(data.startsAt),
|
||||
endsAt: DateTime.fromISO(data.endsAt),
|
||||
})
|
||||
return reservation
|
||||
} catch (error) {
|
||||
logger.error('Reservation creation failed: %s', error)
|
||||
@@ -99,7 +104,11 @@ export default class RoomReservationsController {
|
||||
if (!reservation) {
|
||||
return response.notFound({ message: 'Reservation not found' })
|
||||
}
|
||||
reservation.merge(data)
|
||||
reservation.merge({
|
||||
...data,
|
||||
startsAt: data.startsAt ? DateTime.fromISO(data.startsAt) : undefined,
|
||||
endsAt: data.endsAt ? DateTime.fromISO(data.endsAt) : undefined,
|
||||
})
|
||||
await reservation.save()
|
||||
return reservation
|
||||
} catch (error) {
|
||||
|
||||
@@ -10,7 +10,7 @@ export default class AuthMiddleware {
|
||||
/**
|
||||
* The URL to redirect to, when authentication fails
|
||||
*/
|
||||
redirectTo = '/login'
|
||||
redirectTo = '/api/login'
|
||||
|
||||
async handle(
|
||||
ctx: HttpContext,
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import { DateTime } from 'luxon'
|
||||
import { BaseModel, column } from '@adonisjs/lucid/orm'
|
||||
|
||||
export default class Image extends BaseModel {
|
||||
@column({ isPrimary: true })
|
||||
declare id: number
|
||||
|
||||
@column()
|
||||
declare filePath: string
|
||||
|
||||
@column()
|
||||
declare description: string | null
|
||||
|
||||
@column()
|
||||
declare isVisible: boolean
|
||||
|
||||
@column.dateTime({ autoCreate: true })
|
||||
declare createdAt: DateTime
|
||||
|
||||
@column.dateTime({ autoCreate: true, autoUpdate: true })
|
||||
declare updatedAt: DateTime
|
||||
}
|
||||
Reference in New Issue
Block a user