room reservation model, controller, seeder and migration files added
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* This file is automatically generated.
|
||||
* DO NOT EDIT manually
|
||||
*/
|
||||
|
||||
export const controllers = {
|
||||
Auth: () => import('#controllers/auth_controller'),
|
||||
Categories: () => import('#controllers/categories_controller'),
|
||||
Images: () => import('#controllers/images_controller'),
|
||||
RestaurantMenuItems: () => import('#controllers/restaurant_menu_items_controller'),
|
||||
RestaurantMenus: () => import('#controllers/restaurant_menus_controller'),
|
||||
RoomReservations: () => import('#controllers/room_reservations_controller'),
|
||||
Users: () => import('#controllers/users_controller'),
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
/**
|
||||
* This file is automatically generated.
|
||||
* DO NOT EDIT manually
|
||||
*/
|
||||
|
||||
export const events = {}
|
||||
@@ -0,0 +1,6 @@
|
||||
/**
|
||||
* This file is automatically generated.
|
||||
* DO NOT EDIT manually
|
||||
*/
|
||||
|
||||
export const listeners = {}
|
||||
@@ -0,0 +1,102 @@
|
||||
import '@adonisjs/core/types/http'
|
||||
|
||||
type ParamValue = string | number | bigint | boolean
|
||||
|
||||
export type ScannedRoutes = {
|
||||
ALL: {
|
||||
'auth.register': { paramsTuple?: []; params?: {} }
|
||||
'auth.login': { paramsTuple?: []; params?: {} }
|
||||
'auth.logout': { paramsTuple?: []; params?: {} }
|
||||
'auth.me': { paramsTuple?: []; params?: {} }
|
||||
'menu-items.getAll': { paramsTuple?: []; params?: {} }
|
||||
'menu-items.getOne': { paramsTuple?: []; params?: {} }
|
||||
'menu-items.create': { paramsTuple?: []; params?: {} }
|
||||
'menu-items.update': { paramsTuple?: []; params?: {} }
|
||||
'menu-items.delete': { paramsTuple?: []; params?: {} }
|
||||
'menu-items.attach': { paramsTuple?: []; params?: {} }
|
||||
'menus.getAll': { paramsTuple?: []; params?: {} }
|
||||
'menus.getOne': { paramsTuple?: []; params?: {} }
|
||||
'menus.create': { paramsTuple?: []; params?: {} }
|
||||
'menus.update': { paramsTuple?: []; params?: {} }
|
||||
'menus.delete': { paramsTuple?: []; params?: {} }
|
||||
'categories.getAll': { paramsTuple?: []; params?: {} }
|
||||
'categories.getOne': { paramsTuple?: []; params?: {} }
|
||||
'categories.create': { paramsTuple?: []; params?: {} }
|
||||
'categories.update': { paramsTuple?: []; params?: {} }
|
||||
'categories.delete': { paramsTuple?: []; params?: {} }
|
||||
'users.getAll': { paramsTuple?: []; params?: {} }
|
||||
'users.create': { paramsTuple?: []; params?: {} }
|
||||
'users.getOne': { paramsTuple?: []; params?: {} }
|
||||
'users.update': { paramsTuple?: []; params?: {} }
|
||||
'users.delete': { paramsTuple?: []; params?: {} }
|
||||
'images.list': { paramsTuple?: []; params?: {} }
|
||||
'images.upload': { paramsTuple?: []; params?: {} }
|
||||
'images.show': { paramsTuple: [ParamValue]; params: {'filename': ParamValue} }
|
||||
'rooms.list': { paramsTuple?: []; params?: {} }
|
||||
'rooms.create': { paramsTuple?: []; params?: {} }
|
||||
'rooms.update': { paramsTuple?: []; params?: {} }
|
||||
'rooms.delete': { paramsTuple?: []; params?: {} }
|
||||
'reservations.index': { paramsTuple?: []; params?: {} }
|
||||
'reservations.store': { paramsTuple?: []; params?: {} }
|
||||
'reservations.update': { paramsTuple?: []; params?: {} }
|
||||
'reservations.destroy': { paramsTuple?: []; params?: {} }
|
||||
}
|
||||
POST: {
|
||||
'auth.register': { paramsTuple?: []; params?: {} }
|
||||
'auth.login': { paramsTuple?: []; params?: {} }
|
||||
'menu-items.getOne': { paramsTuple?: []; params?: {} }
|
||||
'menu-items.create': { paramsTuple?: []; params?: {} }
|
||||
'menu-items.attach': { paramsTuple?: []; params?: {} }
|
||||
'menus.getOne': { paramsTuple?: []; params?: {} }
|
||||
'menus.create': { paramsTuple?: []; params?: {} }
|
||||
'categories.getOne': { paramsTuple?: []; params?: {} }
|
||||
'categories.create': { paramsTuple?: []; params?: {} }
|
||||
'users.create': { paramsTuple?: []; params?: {} }
|
||||
'users.getOne': { paramsTuple?: []; params?: {} }
|
||||
'images.upload': { paramsTuple?: []; params?: {} }
|
||||
'rooms.create': { paramsTuple?: []; params?: {} }
|
||||
'reservations.store': { paramsTuple?: []; params?: {} }
|
||||
}
|
||||
DELETE: {
|
||||
'auth.logout': { paramsTuple?: []; params?: {} }
|
||||
'menu-items.delete': { paramsTuple?: []; params?: {} }
|
||||
'menus.delete': { paramsTuple?: []; params?: {} }
|
||||
'categories.delete': { paramsTuple?: []; params?: {} }
|
||||
'users.delete': { paramsTuple?: []; params?: {} }
|
||||
'rooms.delete': { paramsTuple?: []; params?: {} }
|
||||
'reservations.destroy': { paramsTuple?: []; params?: {} }
|
||||
}
|
||||
GET: {
|
||||
'auth.me': { paramsTuple?: []; params?: {} }
|
||||
'menu-items.getAll': { paramsTuple?: []; params?: {} }
|
||||
'menus.getAll': { paramsTuple?: []; params?: {} }
|
||||
'categories.getAll': { paramsTuple?: []; params?: {} }
|
||||
'users.getAll': { paramsTuple?: []; params?: {} }
|
||||
'images.list': { paramsTuple?: []; params?: {} }
|
||||
'images.show': { paramsTuple: [ParamValue]; params: {'filename': ParamValue} }
|
||||
'rooms.list': { paramsTuple?: []; params?: {} }
|
||||
'reservations.index': { paramsTuple?: []; params?: {} }
|
||||
}
|
||||
HEAD: {
|
||||
'auth.me': { paramsTuple?: []; params?: {} }
|
||||
'menu-items.getAll': { paramsTuple?: []; params?: {} }
|
||||
'menus.getAll': { paramsTuple?: []; params?: {} }
|
||||
'categories.getAll': { paramsTuple?: []; params?: {} }
|
||||
'users.getAll': { paramsTuple?: []; params?: {} }
|
||||
'images.list': { paramsTuple?: []; params?: {} }
|
||||
'images.show': { paramsTuple: [ParamValue]; params: {'filename': ParamValue} }
|
||||
'rooms.list': { paramsTuple?: []; params?: {} }
|
||||
'reservations.index': { paramsTuple?: []; params?: {} }
|
||||
}
|
||||
PUT: {
|
||||
'menu-items.update': { paramsTuple?: []; params?: {} }
|
||||
'menus.update': { paramsTuple?: []; params?: {} }
|
||||
'categories.update': { paramsTuple?: []; params?: {} }
|
||||
'users.update': { paramsTuple?: []; params?: {} }
|
||||
'rooms.update': { paramsTuple?: []; params?: {} }
|
||||
'reservations.update': { paramsTuple?: []; params?: {} }
|
||||
}
|
||||
}
|
||||
declare module '@adonisjs/core/types/http' {
|
||||
export interface RoutesList extends ScannedRoutes {}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
node_modules
|
||||
build
|
||||
tmp
|
||||
.git
|
||||
.gitignore
|
||||
.env
|
||||
.env.*
|
||||
storage
|
||||
*.log
|
||||
.idea
|
||||
.vscode
|
||||
.junie
|
||||
.agents
|
||||
.codex
|
||||
.adonisjs
|
||||
@@ -9,4 +9,5 @@ DB_PORT=5432
|
||||
DB_USER=root
|
||||
DB_PASSWORD=root
|
||||
DB_DATABASE=app
|
||||
LIMITER_STORE=database
|
||||
LIMITER_STORE=database
|
||||
DRIVE_DISK=fs
|
||||
@@ -1,12 +0,0 @@
|
||||
TZ=UTC
|
||||
PORT=3333
|
||||
HOST=localhost
|
||||
LOG_LEVEL=info
|
||||
APP_KEY=
|
||||
NODE_ENV=test
|
||||
DB_HOST=localhost
|
||||
DB_PORT=5432
|
||||
DB_USER=paprika
|
||||
DB_PASSWORD=Paprika_2025
|
||||
DB_DATABASE=paprika_test
|
||||
LIMITER_STORE=database
|
||||
@@ -0,0 +1 @@
|
||||
[]
|
||||
@@ -0,0 +1 @@
|
||||
3.0
|
||||
@@ -289,21 +289,31 @@ Deletes a category.
|
||||
|
||||
## Images
|
||||
|
||||
### List Images
|
||||
Retrieves a list of all uploaded images.
|
||||
|
||||
- **URL:** `/api/images`
|
||||
- **Method:** `GET`
|
||||
- **Authentication:** Required (Admin)
|
||||
- **Response:** `[{"filename": "...", "url": "...", "thumbnailUrl": "..."}]`
|
||||
|
||||
### Upload Image
|
||||
Uploads an image for menu items or categories. Supported extensions: jpg, jpeg, png, webp, gif. Max size: 10mb.
|
||||
Uploads an image for menu items or categories. Supported extensions: jpg, jpeg, png, webp, gif. Max size: 10mb. Automatically generates a 200px wide thumbnail.
|
||||
|
||||
- **URL:** `/api/images/upload`
|
||||
- **Method:** `POST`
|
||||
- **Authentication:** Required (Admin)
|
||||
- **Body:**
|
||||
- `image` (file, required): The image file to upload.
|
||||
- **Response:** `{"filename": "...", "url": "..."}`
|
||||
- **Response:** `{"filename": "...", "url": "...", "thumbnailUrl": "..."}`
|
||||
|
||||
### Show Image
|
||||
Retrieves and displays a previously uploaded image.
|
||||
|
||||
- **URL:** `/api/images/:filename`
|
||||
- **Method:** `GET`
|
||||
- **Query Parameters:**
|
||||
- `type` (string, optional): Use `thumbnail` to retrieve the 200px wide thumbnail.
|
||||
- **Response:** Image stream with correct MIME type.
|
||||
|
||||
---
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
# Deployment Instructions for k3s (VPS)
|
||||
|
||||
This document provides instructions on how to deploy the **Paprika API** to a VPS running k3s.
|
||||
|
||||
## 1. Prepare the Container Image
|
||||
|
||||
AdonisJS needs to be built and containerized. If you don't have a Dockerfile, you can use the one provided below.
|
||||
|
||||
### Create a `Dockerfile`
|
||||
```dockerfile
|
||||
FROM node:22-alpine AS base
|
||||
|
||||
# All dependencies stage
|
||||
FROM base AS deps
|
||||
WORKDIR /app
|
||||
ADD package.json package-lock.json ./
|
||||
RUN npm ci
|
||||
|
||||
# Production dependencies stage
|
||||
FROM base AS production-deps
|
||||
WORKDIR /app
|
||||
ADD package.json package-lock.json ./
|
||||
RUN npm ci --omit=dev
|
||||
|
||||
# Build stage
|
||||
FROM base AS build
|
||||
WORKDIR /app
|
||||
COPY --from=deps /app/node_modules /app/node_modules
|
||||
ADD . .
|
||||
RUN node ace build
|
||||
|
||||
# Production stage
|
||||
FROM base AS production
|
||||
ENV NODE_ENV=production
|
||||
WORKDIR /app
|
||||
COPY --from=production-deps /app/node_modules /app/node_modules
|
||||
COPY --from=build /app/build /app
|
||||
EXPOSE 3333
|
||||
CMD ["node", "bin/server.js"]
|
||||
```
|
||||
|
||||
### Build and Push the image
|
||||
Replace `<your-registry>` and `<image-name>` with your actual registry (e.g., Docker Hub, GitHub Packages).
|
||||
|
||||
```bash
|
||||
docker build -t <your-registry>/<image-name>:latest .
|
||||
docker push <your-registry>/<image-name>:latest
|
||||
```
|
||||
|
||||
## 2. Configure the Kubernetes Manifest
|
||||
|
||||
1. Open `k3s-deployment.yaml`.
|
||||
2. Update the `APP_KEY` in the `Secret` section (you can use the one from your `.env`).
|
||||
3. Update the `image` field in the `paprika-api` Deployment with your pushed image.
|
||||
4. Update the `host` in the `Ingress` section to your VPS domain name.
|
||||
|
||||
## 3. Deploy to k3s
|
||||
|
||||
Connect to your VPS and run:
|
||||
|
||||
```bash
|
||||
kubectl apply -f k3s-deployment.yaml
|
||||
```
|
||||
|
||||
## 4. Run Database Migrations
|
||||
|
||||
After the pods are running, you need to run migrations inside the API container:
|
||||
|
||||
```bash
|
||||
kubectl exec -it deployment/paprika-api -- node ace migration:run --force
|
||||
```
|
||||
|
||||
## 5. Verification
|
||||
|
||||
- Check pods: `kubectl get pods`
|
||||
- Check logs: `kubectl logs -f deployment/paprika-api`
|
||||
- Access the API via your domain or VPS IP (if Ingress is configured).
|
||||
@@ -0,0 +1,31 @@
|
||||
FROM node:24-alpine AS base
|
||||
|
||||
# All dependencies stage
|
||||
FROM base AS deps
|
||||
WORKDIR /app
|
||||
ADD package.json package-lock.json ./
|
||||
RUN npm ci
|
||||
|
||||
# Production dependencies stage
|
||||
FROM base AS production-deps
|
||||
WORKDIR /app
|
||||
ADD package.json package-lock.json ./
|
||||
RUN npm ci --omit=dev
|
||||
|
||||
# Build stage
|
||||
FROM base AS build
|
||||
WORKDIR /app
|
||||
COPY --from=deps /app/node_modules /app/node_modules
|
||||
ADD . .
|
||||
RUN node ace build
|
||||
|
||||
# Production stage
|
||||
FROM base AS production
|
||||
ENV NODE_ENV=production
|
||||
WORKDIR /app
|
||||
COPY --from=production-deps /app/node_modules /app/node_modules
|
||||
COPY --from=build /app/build /app
|
||||
COPY menu.json /app/menu.json
|
||||
RUN mkdir -p /app/logs
|
||||
EXPOSE 3333
|
||||
CMD ["node", "bin/server.js"]
|
||||
@@ -19,7 +19,7 @@
|
||||
/**
|
||||
* Register hook to process TypeScript files using ts-node
|
||||
*/
|
||||
import 'ts-node-maintained/register/esm'
|
||||
import '@poppinss/ts-exec'
|
||||
|
||||
/**
|
||||
* Import ace console entrypoint
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { defineConfig } from '@adonisjs/core/app'
|
||||
import { indexEntities } from '@adonisjs/core'
|
||||
|
||||
export default defineConfig({
|
||||
/*
|
||||
@@ -34,6 +35,7 @@ export default defineConfig({
|
||||
() => import('@adonisjs/auth/auth_provider'),
|
||||
() => import('@adonisjs/limiter/limiter_provider'),
|
||||
() => import('@adonisjs/ally/ally_provider'),
|
||||
() => import('@adonisjs/drive/drive_provider')
|
||||
],
|
||||
|
||||
/*
|
||||
@@ -58,16 +60,20 @@ export default defineConfig({
|
||||
tests: {
|
||||
suites: [
|
||||
{
|
||||
files: ['tests/unit/**/*.spec(.ts|.js)'],
|
||||
files: ['tests/unit/**/*.spec.{ts,js}'],
|
||||
name: 'unit',
|
||||
timeout: 2000,
|
||||
},
|
||||
{
|
||||
files: ['tests/functional/**/*.spec(.ts|.js)'],
|
||||
files: ['tests/functional/**/*.spec.{ts,js}'],
|
||||
name: 'functional',
|
||||
timeout: 30000,
|
||||
},
|
||||
],
|
||||
forceExit: false,
|
||||
},
|
||||
|
||||
hooks: {
|
||||
init: [indexEntities()],
|
||||
},
|
||||
})
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -1,17 +1,5 @@
|
||||
import env from '#start/env'
|
||||
import app from '@adonisjs/core/services/app'
|
||||
import { Secret } from '@adonisjs/core/helpers'
|
||||
import { defineConfig } from '@adonisjs/core/http'
|
||||
|
||||
/**
|
||||
* The app key is used for encrypting cookies, generating signed URLs,
|
||||
* and by the "encryption" module.
|
||||
*
|
||||
* The encryption module will fail to decrypt data if the key is lost or
|
||||
* changed. Therefore it is recommended to keep the app key secure.
|
||||
*/
|
||||
export const appKey = new Secret(env.get('APP_KEY'))
|
||||
|
||||
/**
|
||||
* The configuration settings used by the HTTP server
|
||||
*/
|
||||
|
||||
@@ -7,11 +7,22 @@ import { defineConfig } from '@adonisjs/cors'
|
||||
* https://docs.adonisjs.com/guides/security/cors
|
||||
*/
|
||||
const corsConfig = defineConfig({
|
||||
enabled: true,
|
||||
origin: true,
|
||||
origin: (requestOrigin) => {
|
||||
/**
|
||||
* Allow localhost on any port during development.
|
||||
*/
|
||||
if (
|
||||
requestOrigin.includes('localhost') ||
|
||||
requestOrigin.includes('0.0.0.0') ||
|
||||
requestOrigin.includes('127.0.0.1') ||
|
||||
requestOrigin.includes('::1')) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
},
|
||||
|
||||
methods: ['GET', 'HEAD', 'POST', 'PUT', 'DELETE'],
|
||||
headers: true,
|
||||
exposeHeaders: [],
|
||||
credentials: true,
|
||||
maxAge: 90,
|
||||
})
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import env from '#start/env'
|
||||
import app from '@adonisjs/core/services/app'
|
||||
import { defineConfig, services } from '@adonisjs/drive'
|
||||
|
||||
const driveConfig = defineConfig({
|
||||
default: env.get('DRIVE_DISK'),
|
||||
|
||||
/**
|
||||
* The services object can be used to configure multiple file system
|
||||
* services each using the same or a different driver.
|
||||
*/
|
||||
services: {
|
||||
fs: services.fs({
|
||||
location: app.makePath('storage/uploads'),
|
||||
serveFiles: false,
|
||||
visibility: 'public',
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
export default driveConfig
|
||||
|
||||
declare module '@adonisjs/drive/types' {
|
||||
export interface DriveDisks extends InferDriveDisks<typeof driveConfig> {}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import env from '#start/env'
|
||||
import { defineConfig, drivers } from '@adonisjs/core/encryption'
|
||||
|
||||
export default defineConfig({
|
||||
default: 'legacy',
|
||||
list: {
|
||||
legacy: drivers.legacy({
|
||||
keys: [env.get('APP_KEY')],
|
||||
}),
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,21 @@
|
||||
import { BaseSchema } from '@adonisjs/lucid/schema'
|
||||
|
||||
export default class extends BaseSchema {
|
||||
protected tableName = 'images'
|
||||
|
||||
async up() {
|
||||
this.schema.createTable(this.tableName, (table) => {
|
||||
table.increments('id')
|
||||
table.string('file_path').notNullable()
|
||||
table.string('description').nullable()
|
||||
table.boolean('is_visible').defaultTo(true)
|
||||
|
||||
table.timestamp('created_at')
|
||||
table.timestamp('updated_at')
|
||||
})
|
||||
}
|
||||
|
||||
async down() {
|
||||
this.schema.dropTable(this.tableName)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,200 @@
|
||||
/**
|
||||
* This file is automatically generated
|
||||
* DO NOT EDIT manually
|
||||
* Run "node ace migration:run" command to re-generate this file
|
||||
*/
|
||||
|
||||
import { BaseModel, column } from '@adonisjs/lucid/orm'
|
||||
import { DateTime } from 'luxon'
|
||||
|
||||
export class AuthAccessTokenSchema extends BaseModel {
|
||||
static $columns = ['abilities', 'createdAt', 'expiresAt', 'hash', 'id', 'lastUsedAt', 'name', 'tokenableId', 'type', 'updatedAt'] as const
|
||||
$columns = AuthAccessTokenSchema.$columns
|
||||
@column()
|
||||
declare abilities: string
|
||||
@column.dateTime({ autoCreate: true })
|
||||
declare createdAt: DateTime | null
|
||||
@column.dateTime()
|
||||
declare expiresAt: DateTime | null
|
||||
@column()
|
||||
declare hash: string
|
||||
@column({ isPrimary: true })
|
||||
declare id: number
|
||||
@column.dateTime()
|
||||
declare lastUsedAt: DateTime | null
|
||||
@column()
|
||||
declare name: string | null
|
||||
@column()
|
||||
declare tokenableId: number
|
||||
@column()
|
||||
declare type: string
|
||||
@column.dateTime({ autoCreate: true, autoUpdate: true })
|
||||
declare updatedAt: DateTime | null
|
||||
}
|
||||
|
||||
export class CategorySchema extends BaseModel {
|
||||
static $columns = ['createdAt', 'id', 'name', 'nameEn', 'nameHu', 'slug', 'updatedAt'] as const
|
||||
$columns = CategorySchema.$columns
|
||||
@column.dateTime({ autoCreate: true })
|
||||
declare createdAt: DateTime
|
||||
@column({ isPrimary: true })
|
||||
declare id: number
|
||||
@column()
|
||||
declare name: string
|
||||
@column()
|
||||
declare nameEn: string | null
|
||||
@column()
|
||||
declare nameHu: string | null
|
||||
@column()
|
||||
declare slug: string
|
||||
@column.dateTime({ autoCreate: true, autoUpdate: true })
|
||||
declare updatedAt: DateTime
|
||||
}
|
||||
|
||||
export class ImageSchema extends BaseModel {
|
||||
static $columns = ['createdAt', 'description', 'filePath', 'id', 'isVisible', 'updatedAt'] as const
|
||||
$columns = ImageSchema.$columns
|
||||
@column.dateTime({ autoCreate: true })
|
||||
declare createdAt: DateTime | null
|
||||
@column()
|
||||
declare description: string | null
|
||||
@column()
|
||||
declare filePath: string
|
||||
@column({ isPrimary: true })
|
||||
declare id: number
|
||||
@column()
|
||||
declare isVisible: boolean | null
|
||||
@column.dateTime({ autoCreate: true, autoUpdate: true })
|
||||
declare updatedAt: DateTime | null
|
||||
}
|
||||
|
||||
export class MenuItemMenuSchema extends BaseModel {
|
||||
static $columns = ['id', 'restaurantMenuId', 'restaurantMenuItemId'] as const
|
||||
$columns = MenuItemMenuSchema.$columns
|
||||
@column({ isPrimary: true })
|
||||
declare id: number
|
||||
@column()
|
||||
declare restaurantMenuId: number
|
||||
@column()
|
||||
declare restaurantMenuItemId: number
|
||||
}
|
||||
|
||||
export class RateLimitSchema extends BaseModel {
|
||||
static $columns = ['expire', 'key', 'points'] as const
|
||||
$columns = RateLimitSchema.$columns
|
||||
@column()
|
||||
declare expire: bigint | number | null
|
||||
@column({ isPrimary: true })
|
||||
declare key: string
|
||||
@column()
|
||||
declare points: number
|
||||
}
|
||||
|
||||
export class ReservationSchema extends BaseModel {
|
||||
static $columns = ['createdAt', 'customerName', 'customerPhone', 'endsAt', 'guestsCount', 'id', 'notes', 'roomId', 'startsAt', 'status', 'updatedAt'] as const
|
||||
$columns = ReservationSchema.$columns
|
||||
@column.dateTime({ autoCreate: true })
|
||||
declare createdAt: DateTime | null
|
||||
@column()
|
||||
declare customerName: string
|
||||
@column()
|
||||
declare customerPhone: string
|
||||
@column.dateTime()
|
||||
declare endsAt: DateTime
|
||||
@column()
|
||||
declare guestsCount: number
|
||||
@column({ isPrimary: true })
|
||||
declare id: number
|
||||
@column()
|
||||
declare notes: string | null
|
||||
@column()
|
||||
declare roomId: number | null
|
||||
@column.dateTime()
|
||||
declare startsAt: DateTime
|
||||
@column()
|
||||
declare status: string | null
|
||||
@column.dateTime({ autoCreate: true, autoUpdate: true })
|
||||
declare updatedAt: DateTime | null
|
||||
}
|
||||
|
||||
export class RestaurantMenuItemSchema extends BaseModel {
|
||||
static $columns = ['categoryId', 'createdAt', 'description', 'descriptionEn', 'descriptionHu', 'id', 'name', 'nameEn', 'nameHu', 'priceLarge', 'priceSmall', 'updatedAt'] as const
|
||||
$columns = RestaurantMenuItemSchema.$columns
|
||||
@column()
|
||||
declare categoryId: number | null
|
||||
@column.dateTime({ autoCreate: true })
|
||||
declare createdAt: DateTime
|
||||
@column()
|
||||
declare description: string | null
|
||||
@column()
|
||||
declare descriptionEn: string | null
|
||||
@column()
|
||||
declare descriptionHu: string | null
|
||||
@column({ isPrimary: true })
|
||||
declare id: number
|
||||
@column()
|
||||
declare name: string
|
||||
@column()
|
||||
declare nameEn: string | null
|
||||
@column()
|
||||
declare nameHu: string | null
|
||||
@column()
|
||||
declare priceLarge: string
|
||||
@column()
|
||||
declare priceSmall: string
|
||||
@column.dateTime({ autoCreate: true, autoUpdate: true })
|
||||
declare updatedAt: DateTime
|
||||
}
|
||||
|
||||
export class RestaurantMenuSchema extends BaseModel {
|
||||
static $columns = ['createdAt', 'id', 'name', 'nameEn', 'nameHu', 'updatedAt'] as const
|
||||
$columns = RestaurantMenuSchema.$columns
|
||||
@column.dateTime({ autoCreate: true })
|
||||
declare createdAt: DateTime
|
||||
@column({ isPrimary: true })
|
||||
declare id: number
|
||||
@column()
|
||||
declare name: string
|
||||
@column()
|
||||
declare nameEn: string | null
|
||||
@column()
|
||||
declare nameHu: string | null
|
||||
@column.dateTime({ autoCreate: true, autoUpdate: true })
|
||||
declare updatedAt: DateTime
|
||||
}
|
||||
|
||||
export class RoomSchema extends BaseModel {
|
||||
static $columns = ['capacity', 'createdAt', 'description', 'id', 'name', 'updatedAt'] as const
|
||||
$columns = RoomSchema.$columns
|
||||
@column()
|
||||
declare capacity: number
|
||||
@column.dateTime({ autoCreate: true })
|
||||
declare createdAt: DateTime | null
|
||||
@column()
|
||||
declare description: string | null
|
||||
@column({ isPrimary: true })
|
||||
declare id: number
|
||||
@column()
|
||||
declare name: string
|
||||
@column.dateTime({ autoCreate: true, autoUpdate: true })
|
||||
declare updatedAt: DateTime | null
|
||||
}
|
||||
|
||||
export class UserSchema extends BaseModel {
|
||||
static $columns = ['createdAt', 'email', 'fullName', 'id', 'password', 'role', 'updatedAt'] as const
|
||||
$columns = UserSchema.$columns
|
||||
@column.dateTime({ autoCreate: true })
|
||||
declare createdAt: DateTime
|
||||
@column()
|
||||
declare email: string
|
||||
@column()
|
||||
declare fullName: string
|
||||
@column({ isPrimary: true })
|
||||
declare id: number
|
||||
@column({ serializeAs: null })
|
||||
declare password: string
|
||||
@column()
|
||||
declare role: string
|
||||
@column.dateTime({ autoCreate: true, autoUpdate: true })
|
||||
declare updatedAt: DateTime
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { BaseSeeder } from '@adonisjs/lucid/seeders'
|
||||
|
||||
export default class extends BaseSeeder {
|
||||
async run() {
|
||||
// Write your database queries inside the run method
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,7 @@ export default class extends BaseSeeder {
|
||||
{
|
||||
name: 'Kisterem',
|
||||
capacity: 20,
|
||||
description: 'Cozy small room for private gatherings.',
|
||||
description: 'Kis terem, kisebb rendezvényekre',
|
||||
}
|
||||
)
|
||||
|
||||
@@ -17,7 +17,7 @@ export default class extends BaseSeeder {
|
||||
{
|
||||
name: 'Nagyterem',
|
||||
capacity: 50,
|
||||
description: 'Large hall for big events and weddings.',
|
||||
description: 'Esküvőkre, lagzikra, céges rendezvényekre',
|
||||
}
|
||||
)
|
||||
|
||||
@@ -26,7 +26,7 @@ export default class extends BaseSeeder {
|
||||
{
|
||||
name: 'Terasz',
|
||||
capacity: 30,
|
||||
description: 'Outdoor terrace with a view.',
|
||||
description: 'Külső terasz',
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,196 @@
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: paprika-api
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: api-secrets
|
||||
namespace: paprika-api
|
||||
type: Opaque
|
||||
stringData:
|
||||
APP_KEY: rIkZNEpVHFAyKJSPq3t8aue0NzaIgF83 # Change this if needed
|
||||
DB_PASSWORD: Paprika_2025
|
||||
DB_USER: paprika
|
||||
DB_DATABASE: paprika
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: postgres-pvc
|
||||
namespace: paprika-api
|
||||
spec:
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
resources:
|
||||
requests:
|
||||
storage: 1Gi
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: paprika-uploads-pvc
|
||||
namespace: paprika-api
|
||||
spec:
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
resources:
|
||||
requests:
|
||||
storage: 1Gi
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: postgres
|
||||
namespace: paprika-api
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: postgres
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: postgres
|
||||
spec:
|
||||
containers:
|
||||
- name: postgres
|
||||
image: postgres:16-alpine
|
||||
ports:
|
||||
- containerPort: 5432
|
||||
env:
|
||||
- name: POSTGRES_USER
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: api-secrets
|
||||
key: DB_USER
|
||||
- name: POSTGRES_PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: api-secrets
|
||||
key: DB_PASSWORD
|
||||
- name: POSTGRES_DB
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: api-secrets
|
||||
key: DB_DATABASE
|
||||
volumeMounts:
|
||||
- name: postgres-storage
|
||||
mountPath: /var/lib/postgresql/data
|
||||
volumes:
|
||||
- name: postgres-storage
|
||||
persistentVolumeClaim:
|
||||
claimName: postgres-pvc
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: postgres
|
||||
namespace: paprika-api
|
||||
spec:
|
||||
selector:
|
||||
app: postgres
|
||||
ports:
|
||||
- port: 5432
|
||||
targetPort: 5432
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: paprika-api
|
||||
namespace: paprika-api
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: paprika-api
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: paprika-api
|
||||
spec:
|
||||
containers:
|
||||
- name: paprika-api
|
||||
image: paprika-api:latest # Update with your image
|
||||
imagePullPolicy: Never
|
||||
ports:
|
||||
- containerPort: 3333
|
||||
env:
|
||||
- name: NODE_ENV
|
||||
value: production
|
||||
- name: APP_KEY
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: api-secrets
|
||||
key: APP_KEY
|
||||
- name: DB_HOST
|
||||
value: postgres
|
||||
- name: DB_PORT
|
||||
value: "5432"
|
||||
- name: DB_USER
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: api-secrets
|
||||
key: DB_USER
|
||||
- name: DB_PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: api-secrets
|
||||
key: DB_PASSWORD
|
||||
- name: DB_DATABASE
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: api-secrets
|
||||
key: DB_DATABASE
|
||||
- name: PORT
|
||||
value: "3333"
|
||||
- name: HOST
|
||||
value: 0.0.0.0
|
||||
- name: LOG_LEVEL
|
||||
value: info
|
||||
- name: LIMITER_STORE
|
||||
value: database
|
||||
- name: TZ
|
||||
value: UTC
|
||||
- name: DRIVE_DISK
|
||||
value: fs
|
||||
volumeMounts:
|
||||
- name: uploads-storage
|
||||
mountPath: /app/storage/uploads
|
||||
volumes:
|
||||
- name: uploads-storage
|
||||
persistentVolumeClaim:
|
||||
claimName: paprika-uploads-pvc
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: paprika-api
|
||||
namespace: paprika-api
|
||||
spec:
|
||||
selector:
|
||||
app: paprika-api
|
||||
ports:
|
||||
- port: 80
|
||||
targetPort: 3333
|
||||
---
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: paprika-api-ingress
|
||||
namespace: paprika-api
|
||||
annotations:
|
||||
kubernetes.io/ingress.class: traefik # k3s uses traefik by default
|
||||
spec:
|
||||
rules:
|
||||
- host: dave.dev # Update with your domain
|
||||
http:
|
||||
paths:
|
||||
- path: /api
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: paprika-api
|
||||
port:
|
||||
number: 80
|
||||
@@ -29,36 +29,41 @@
|
||||
"#database/*": "./database/*.js",
|
||||
"#start/*": "./start/*.js",
|
||||
"#tests/*": "./tests/*.js",
|
||||
"#config/*": "./config/*.js"
|
||||
"#config/*": "./config/*.js",
|
||||
"#generated/*": "./.adonisjs/server/*.js",
|
||||
"#transformers/*": "./app/transformers/*.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@adonisjs/assembler": "^7.8.2",
|
||||
"@adonisjs/eslint-config": "^2.0.0",
|
||||
"@adonisjs/assembler": "^8.4.0",
|
||||
"@adonisjs/eslint-config": "^3.0.0",
|
||||
"@adonisjs/prettier-config": "^1.4.0",
|
||||
"@adonisjs/tsconfig": "^1.4.0",
|
||||
"@adonisjs/tsconfig": "^2.0.0",
|
||||
"@japa/api-client": "^3.1.0",
|
||||
"@japa/assert": "^4.0.1",
|
||||
"@japa/plugin-adonisjs": "^4.0.0",
|
||||
"@japa/plugin-adonisjs": "^5.2.0",
|
||||
"@japa/runner": "^4.2.0",
|
||||
"@swc/core": "1.10.7",
|
||||
"@poppinss/ts-exec": "^1.4.4",
|
||||
"@types/luxon": "^3.6.2",
|
||||
"@types/node": "^22.13.2",
|
||||
"eslint": "^9.20.1",
|
||||
"hot-hook": "^0.4.0",
|
||||
"pino-pretty": "^13.0.0",
|
||||
"prettier": "^3.5.0",
|
||||
"ts-node-maintained": "^10.9.5",
|
||||
"typescript": "~5.7"
|
||||
"prettier": "^3.8.3",
|
||||
"typescript": "~5.7",
|
||||
"youch": "^4.1.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@adonisjs/auth": "^9.3.1",
|
||||
"@adonisjs/ally": "^5.0.0",
|
||||
"@adonisjs/core": "^6.17.2",
|
||||
"@adonisjs/cors": "^2.2.1",
|
||||
"@adonisjs/limiter": "^2.4.0",
|
||||
"@adonisjs/lucid": "^21.6.0",
|
||||
"@vinejs/vine": "^3.0.0",
|
||||
"@adonisjs/ally": "^6.3.0",
|
||||
"@adonisjs/auth": "^10.1.0",
|
||||
"@adonisjs/core": "^7.3.2",
|
||||
"@adonisjs/cors": "^3.0.0",
|
||||
"@adonisjs/drive": "^4.0.0",
|
||||
"@adonisjs/limiter": "^3.0.1",
|
||||
"@adonisjs/lucid": "^22.4.2",
|
||||
"@vinejs/vine": "^4.4.0",
|
||||
"jimp": "^1.6.1",
|
||||
"luxon": "^3.6.1",
|
||||
"node-addon-api": "^8.7.0",
|
||||
"pg": "^8.14.1",
|
||||
"reflect-metadata": "^0.2.2"
|
||||
},
|
||||
|
||||
@@ -35,4 +35,11 @@ export default await Env.create(new URL('../', import.meta.url), {
|
||||
|----------------------------------------------------------
|
||||
*/
|
||||
LIMITER_STORE: Env.schema.enum(['database', 'memory'] as const),
|
||||
|
||||
/*
|
||||
|----------------------------------------------------------
|
||||
| Variables for configuring the drive package
|
||||
|----------------------------------------------------------
|
||||
*/
|
||||
DRIVE_DISK: Env.schema.enum(['fs'] as const)
|
||||
})
|
||||
|
||||
@@ -19,16 +19,17 @@ const UsersController = () => import('#controllers/users_controller')
|
||||
const ImagesController = () => import('#controllers/images_controller')
|
||||
const RoomReservationsController = () => import('#controllers/room_reservations_controller')
|
||||
|
||||
// Auth routes
|
||||
router.group(() => {
|
||||
router.post('/register', [AuthController, 'register']).as('auth.register').use(throttle)
|
||||
router.post('/login', [AuthController, 'login']).as('auth.login').use(throttle)
|
||||
router.delete('/logout', [AuthController, 'logout']).as('auth.logout').use(middleware.auth())
|
||||
router.get('/me', [AuthController, 'me']).as('auth.me').use(middleware.auth()).use(throttle)
|
||||
})
|
||||
// RestaurantMenuItem routes
|
||||
router
|
||||
.group(() => {
|
||||
// Auth routes
|
||||
router.group(() => {
|
||||
router.post('/register', [AuthController, 'register']).as('auth.register').use(throttle).use(middleware.auth())
|
||||
router.post('/login', [AuthController, 'login']).as('auth.login').use(throttle)
|
||||
router.delete('/logout', [AuthController, 'logout']).as('auth.logout').use(middleware.auth())
|
||||
router.get('/me', [AuthController, 'me']).as('auth.me').use(middleware.auth()).use(throttle)
|
||||
})
|
||||
|
||||
// RestaurantMenuItem routes
|
||||
router
|
||||
.group(() => {
|
||||
router.get('/', [RestaurantMenuItemsController, 'getAll']).as('menu-items.getAll')
|
||||
@@ -126,11 +127,17 @@ router
|
||||
// Image routes
|
||||
router
|
||||
.group(() => {
|
||||
router
|
||||
.get('/', [ImagesController, 'list'])
|
||||
.as('images.list')
|
||||
//.use([middleware.auth(), middleware.admin()])
|
||||
router
|
||||
.post('/upload', [ImagesController, 'upload'])
|
||||
.as('images.upload')
|
||||
.use([middleware.auth(), middleware.admin()])
|
||||
router.get('/:filename', [ImagesController, 'show']).as('images.show')
|
||||
router
|
||||
.get('/:filename', [ImagesController, 'show'])
|
||||
.as('images.show')
|
||||
})
|
||||
.prefix('/images')
|
||||
|
||||
|
||||
|
After Width: | Height: | Size: 4.6 MiB |
|
After Width: | Height: | Size: 6.1 MiB |
|
After Width: | Height: | Size: 6.8 MiB |
|
After Width: | Height: | Size: 6.8 MiB |
|
After Width: | Height: | Size: 5.6 MiB |
|
After Width: | Height: | Size: 1008 KiB |
|
After Width: | Height: | Size: 6.4 MiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 45 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 31 KiB |
@@ -0,0 +1,62 @@
|
||||
import { test } from '@japa/runner'
|
||||
|
||||
test.group('Images show', () => {
|
||||
test('get uploaded image', async ({ client, assert }) => {
|
||||
// 1. Login to get token
|
||||
const loginResponse = await client.post('/api/login').json({
|
||||
email: 'kiss.david@test.hu',
|
||||
password: 'Password123',
|
||||
})
|
||||
|
||||
loginResponse.assertStatus(200)
|
||||
const token = loginResponse.body().token
|
||||
|
||||
// 2. Upload an image
|
||||
const response = await client
|
||||
.post('/api/images/upload')
|
||||
.header('Authorization', `Bearer ${token}`)
|
||||
.file('image', Buffer.from('fake image content'), {
|
||||
filename: 'test.jpg',
|
||||
contentType: 'image/jpeg',
|
||||
})
|
||||
|
||||
response.assertStatus(200)
|
||||
const { url } = response.body()
|
||||
|
||||
// 3. Try to fetch the image
|
||||
const showResponse = await client.get(url)
|
||||
|
||||
if (showResponse.status() === 404) {
|
||||
console.log('404 Body:', showResponse.body())
|
||||
}
|
||||
|
||||
showResponse.assertStatus(200)
|
||||
assert.equal(showResponse.header('content-type'), 'image/jpeg')
|
||||
})
|
||||
|
||||
test('get uploaded thumbnail', async ({ client, assert }) => {
|
||||
// 1. Login to get token
|
||||
const loginResponse = await client.post('/api/login').json({
|
||||
email: 'kiss.david@test.hu',
|
||||
password: 'Password123',
|
||||
})
|
||||
|
||||
loginResponse.assertStatus(200)
|
||||
const token = loginResponse.body().token
|
||||
|
||||
const response = await client
|
||||
.post('/api/images/upload')
|
||||
.header('Authorization', `Bearer ${token}`)
|
||||
.file('image', Buffer.from('fake image content'), {
|
||||
filename: 'test.jpg',
|
||||
contentType: 'image/jpeg',
|
||||
})
|
||||
|
||||
response.assertStatus(200)
|
||||
const { thumbnailUrl } = response.body()
|
||||
|
||||
const showResponse = await client.get(thumbnailUrl)
|
||||
showResponse.assertStatus(200)
|
||||
assert.equal(showResponse.header('content-type'), 'image/jpeg')
|
||||
})
|
||||
})
|
||||