room reservation model, controller, seeder and migration files added

This commit is contained in:
Kiss Dávid
2026-08-19 19:59:18 +02:00
parent b281724a1a
commit 0b88fe87e5
49 changed files with 4073 additions and 2472 deletions
+14
View File
@@ -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'),
}
+6
View File
@@ -0,0 +1,6 @@
/**
* This file is automatically generated.
* DO NOT EDIT manually
*/
export const events = {}
+6
View File
@@ -0,0 +1,6 @@
/**
* This file is automatically generated.
* DO NOT EDIT manually
*/
export const listeners = {}
+102
View File
@@ -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 {}
}
+15
View File
@@ -0,0 +1,15 @@
node_modules
build
tmp
.git
.gitignore
.env
.env.*
storage
*.log
.idea
.vscode
.junie
.agents
.codex
.adonisjs
Regular → Executable
+2 -1
View File
@@ -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
-12
View File
@@ -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
View File
View File
+1
View File
@@ -0,0 +1 @@
[]
+1
View File
@@ -0,0 +1 @@
3.0
View File
+12 -2
View File
@@ -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.
---
+77
View File
@@ -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).
+31
View File
@@ -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"]
+1 -1
View File
@@ -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
+8 -2
View File
@@ -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()],
},
})
+51 -18
View File
@@ -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) {
+1 -1
View File
@@ -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,
+22
View File
@@ -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
}
-12
View File
@@ -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
*/
+14 -3
View File
@@ -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,
})
+25
View File
@@ -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> {}
}
+11
View File
@@ -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)
}
}
+200
View File
@@ -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
}
}
+3 -3
View File
@@ -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',
}
)
}
+196
View File
@@ -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
Generated Executable → Regular
+3129 -2390
View File
File diff suppressed because it is too large Load Diff
+21 -16
View File
@@ -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"
},
+7
View File
@@ -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)
})
+16 -9
View File
@@ -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')
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1008 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

+62
View File
@@ -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')
})
})