feat(auth): add session login page, API key guard, and remove orphan SQL migrations
- Add session-based authentication for web-UI (Login.vue, AuthController) - Add AuthSessionMiddleware: redirects unauthenticated users to /login - Add ApiKeyMiddleware: validates X-Api-Key header on all /api/v1 routes - Add BasicAuthMiddleware: registered for future M2M use - Use timingSafeEqual for all credential comparisons (timing attack prevention) - Regenerate session ID on login (session fixation prevention) - Validate redirect target after login (open redirect prevention) - Require WEBUI_USERNAME, WEBUI_PASSWORD, API_KEY as mandatory env vars - Share auth.username/authenticated via Inertia shared props - Add user dropdown in navbar (avatar icon + username + logout) - Add login i18n keys in fr.json and en.json - Remove orphan SQL migrations (users, auth_access_tokens) and @adonisjs/lucid
This commit is contained in:
1 parent
198f15ec9b
commit
991173aa05
18 files changed
+477
-482
No files matched your search
@@ -29,3 +29,10 @@ LOG_PROTOCOL=udp4
|
||||
|
||||
# Security
|
||||
HASH_KEY=0123456789abcdef0123456789abcdef
|
||||
|
||||
# Web UI Basic Auth (browser prompt)
|
||||
WEBUI_USERNAME=Admin
|
||||
WEBUI_PASSWORD=Grenoble:38
|
||||
|
||||
# REST API Key (set in X-Api-Key header)
|
||||
API_KEY=change-me-with-a-strong-random-key
|
||||
@@ -0,0 +1,76 @@
|
||||
import { timingSafeEqual } from 'node:crypto'
|
||||
import type { HttpContext } from '@adonisjs/core/http'
|
||||
import env from '#start/env'
|
||||
|
||||
/**
|
||||
* AuthController
|
||||
*
|
||||
* Handles web-ui login/logout via session.
|
||||
* Credentials are validated against WEBUI_USERNAME / WEBUI_PASSWORD env variables.
|
||||
*
|
||||
* Security:
|
||||
* - Constant-time comparison (timingSafeEqual) to prevent timing attacks
|
||||
* - Session regeneration after login to prevent session fixation
|
||||
* - Redirect target validated to prevent open redirect
|
||||
*/
|
||||
export default class AuthController {
|
||||
async showLogin({ inertia, session, response }: HttpContext) {
|
||||
// Already authenticated — redirect to home
|
||||
if (session.get('authenticated')) {
|
||||
return response.redirect().toPath('/')
|
||||
}
|
||||
const error = session.flashMessages.get('error')
|
||||
return (inertia as any).render('Login', { error })
|
||||
}
|
||||
|
||||
async login({ request, session, response }: HttpContext) {
|
||||
const { username, password } = request.only(['username', 'password'])
|
||||
|
||||
const expectedUsername = env.get('WEBUI_USERNAME')!
|
||||
const expectedPassword = env.get('WEBUI_PASSWORD')!
|
||||
|
||||
const usernameMatch = safeCompare(username ?? '', expectedUsername)
|
||||
const passwordMatch = safeCompare(password ?? '', expectedPassword)
|
||||
|
||||
if (usernameMatch && passwordMatch) {
|
||||
// Regenerate session ID to prevent session fixation attacks
|
||||
session.regenerate()
|
||||
session.put('authenticated', true)
|
||||
session.put('username', username)
|
||||
|
||||
// Validate redirect target to prevent open redirect
|
||||
const raw = session.get('redirectAfterLogin', '/')
|
||||
const redirectTo = isSafeRedirect(raw) ? raw : '/'
|
||||
session.forget('redirectAfterLogin')
|
||||
return response.redirect().toPath(redirectTo)
|
||||
}
|
||||
|
||||
session.flash('error', 'invalid_credentials')
|
||||
return response.redirect().toPath('/login')
|
||||
}
|
||||
|
||||
async logout({ session, response }: HttpContext) {
|
||||
session.clear()
|
||||
return response.redirect().toPath('/login')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Constant-time string comparison to prevent timing side-channel attacks.
|
||||
* Returns false immediately if lengths differ (length is not secret here).
|
||||
*/
|
||||
function safeCompare(a: string, b: string): boolean {
|
||||
if (a.length !== b.length) return false
|
||||
return timingSafeEqual(Buffer.from(a), Buffer.from(b))
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate that a redirect path is a safe internal path.
|
||||
* Rejects empty strings, external URLs (//) and protocol-relative URLs.
|
||||
*/
|
||||
function isSafeRedirect(value: unknown): value is string {
|
||||
if (typeof value !== 'string') return false
|
||||
if (!value.startsWith('/')) return false
|
||||
if (value.startsWith('//')) return false
|
||||
return true
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import { timingSafeEqual } from 'node:crypto'
|
||||
import type { HttpContext } from '@adonisjs/core/http'
|
||||
import type { NextFn } from '@adonisjs/core/types/http'
|
||||
import env from '#start/env'
|
||||
|
||||
/**
|
||||
* ApiKeyMiddleware
|
||||
*
|
||||
* Protects REST API routes by validating a fixed API key provided in the
|
||||
* X-Api-Key request header. The expected key is configured via:
|
||||
* API_KEY (required env variable)
|
||||
*
|
||||
* Security:
|
||||
* - Constant-time comparison (timingSafeEqual) to prevent timing attacks
|
||||
*
|
||||
* Responds with 401 if the header is missing or the key does not match.
|
||||
*/
|
||||
export default class ApiKeyMiddleware {
|
||||
async handle({ request, response }: HttpContext, next: NextFn) {
|
||||
const apiKey = request.header('x-api-key') ?? ''
|
||||
const expectedKey = env.get('API_KEY')!
|
||||
|
||||
const provided = Buffer.from(apiKey)
|
||||
const expected = Buffer.from(expectedKey)
|
||||
|
||||
// Lengths must match and content must match in constant time
|
||||
const valid =
|
||||
provided.length === expected.length && timingSafeEqual(provided, expected)
|
||||
|
||||
if (!valid) {
|
||||
return response.status(401).send('Unauthorized: invalid or missing X-Api-Key header')
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import type { HttpContext } from '@adonisjs/core/http'
|
||||
import type { NextFn } from '@adonisjs/core/types/http'
|
||||
|
||||
/**
|
||||
* AuthSessionMiddleware
|
||||
*
|
||||
* Protects web-ui routes by requiring an authenticated session.
|
||||
* If the session is not authenticated, stores the requested URL
|
||||
* in the session and redirects to /login.
|
||||
*
|
||||
* Login is handled by AuthController + session-based authentication.
|
||||
*/
|
||||
export default class AuthSessionMiddleware {
|
||||
async handle({ session, request, response }: HttpContext, next: NextFn) {
|
||||
if (!session.get('authenticated')) {
|
||||
// Remember where the user wanted to go
|
||||
if (request.method() === 'GET') {
|
||||
session.put('redirectAfterLogin', request.url())
|
||||
}
|
||||
return response.redirect().toPath('/login')
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
import { timingSafeEqual } from 'node:crypto'
|
||||
import type { HttpContext } from '@adonisjs/core/http'
|
||||
import type { NextFn } from '@adonisjs/core/types/http'
|
||||
import env from '#start/env'
|
||||
|
||||
/**
|
||||
* BasicAuthMiddleware
|
||||
*
|
||||
* Protects routes with HTTP Basic Authentication.
|
||||
* Credentials are configured via environment variables:
|
||||
* WEBUI_USERNAME
|
||||
* WEBUI_PASSWORD
|
||||
*
|
||||
* Security:
|
||||
* - Constant-time comparison (timingSafeEqual) to prevent timing attacks
|
||||
*
|
||||
* Responds with 401 + WWW-Authenticate header if credentials are missing or invalid.
|
||||
*
|
||||
* NOTE: This middleware is registered for potential future use (e.g. machine-to-machine
|
||||
* access to web-ui pages). Web-UI currently uses session-based auth (authSession).
|
||||
*/
|
||||
export default class BasicAuthMiddleware {
|
||||
async handle({ request, response }: HttpContext, next: NextFn) {
|
||||
const authHeader = request.header('authorization') || ''
|
||||
|
||||
if (!authHeader.startsWith('Basic ')) {
|
||||
return this.#unauthorized(response)
|
||||
}
|
||||
|
||||
const base64Credentials = authHeader.slice(6)
|
||||
let decoded: string
|
||||
try {
|
||||
decoded = Buffer.from(base64Credentials, 'base64').toString('utf-8')
|
||||
} catch {
|
||||
return this.#unauthorized(response)
|
||||
}
|
||||
|
||||
// Split only on the first colon — password may contain colons
|
||||
const colonIndex = decoded.indexOf(':')
|
||||
if (colonIndex === -1) {
|
||||
return this.#unauthorized(response)
|
||||
}
|
||||
|
||||
const username = decoded.slice(0, colonIndex)
|
||||
const password = decoded.slice(colonIndex + 1)
|
||||
|
||||
const expectedUsername = env.get('WEBUI_USERNAME')!
|
||||
const expectedPassword = env.get('WEBUI_PASSWORD')!
|
||||
|
||||
const usernameMatch = safeCompare(username, expectedUsername)
|
||||
const passwordMatch = safeCompare(password, expectedPassword)
|
||||
|
||||
if (!usernameMatch || !passwordMatch) {
|
||||
return this.#unauthorized(response)
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
|
||||
#unauthorized(response: HttpContext['response']) {
|
||||
response.header('WWW-Authenticate', 'Basic realm="ADAM ACS"')
|
||||
return response.status(401).send('Unauthorized')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Constant-time string comparison to prevent timing side-channel attacks.
|
||||
*/
|
||||
function safeCompare(a: string, b: string): boolean {
|
||||
if (a.length !== b.length) return false
|
||||
return timingSafeEqual(Buffer.from(a), Buffer.from(b))
|
||||
}
|
||||
@@ -6,6 +6,10 @@ export default class InertiaMiddleware extends BaseInertiaMiddleware {
|
||||
async share(ctx: HttpContext) {
|
||||
return {
|
||||
errors: ctx.session?.flashMessages.get('errors'),
|
||||
auth: {
|
||||
username: ctx.session?.get('username') ?? null,
|
||||
authenticated: ctx.session?.get('authenticated') ?? false,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+12
-2
@@ -1,4 +1,11 @@
|
||||
import { Db, Collection, ObjectId, Filter, UpdateFilter, Sort } from 'mongodb'
|
||||
import {
|
||||
type Db,
|
||||
type Collection,
|
||||
ObjectId,
|
||||
type Filter,
|
||||
type UpdateFilter,
|
||||
type Sort,
|
||||
} from 'mongodb'
|
||||
import { getMongoDb } from '#providers/mongodb_provider'
|
||||
|
||||
export abstract class BaseModel {
|
||||
@@ -16,7 +23,10 @@ export abstract class BaseModel {
|
||||
return this.getDb().collection(this.collectionName)
|
||||
}
|
||||
|
||||
static async find(this: { collectionName: string; getDb(): Db }, filter: Filter<any> = {}): Promise<any[]> {
|
||||
static async find(
|
||||
this: { collectionName: string; getDb(): Db },
|
||||
filter: Filter<any> = {}
|
||||
): Promise<any[]> {
|
||||
const collection = this.getDb().collection(this.collectionName)
|
||||
return collection.find(filter).toArray()
|
||||
}
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
import { BaseSchema } from '@adonisjs/lucid/schema'
|
||||
|
||||
export default class extends BaseSchema {
|
||||
protected tableName = 'users'
|
||||
|
||||
async up() {
|
||||
this.schema.createTable(this.tableName, (table) => {
|
||||
table.increments('id').notNullable()
|
||||
table.string('full_name').nullable()
|
||||
table.string('email', 254).notNullable().unique()
|
||||
table.string('password').notNullable()
|
||||
|
||||
table.timestamp('created_at').notNullable()
|
||||
table.timestamp('updated_at').nullable()
|
||||
})
|
||||
}
|
||||
|
||||
async down() {
|
||||
this.schema.dropTable(this.tableName)
|
||||
}
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
import { BaseSchema } from '@adonisjs/lucid/schema'
|
||||
|
||||
export default class extends BaseSchema {
|
||||
protected tableName = 'auth_access_tokens'
|
||||
|
||||
async up() {
|
||||
this.schema.createTable(this.tableName, (table) => {
|
||||
table.increments('id')
|
||||
table
|
||||
.integer('tokenable_id')
|
||||
.notNullable()
|
||||
.unsigned()
|
||||
.references('id')
|
||||
.inTable('users')
|
||||
.onDelete('CASCADE')
|
||||
|
||||
table.string('type').notNullable()
|
||||
table.string('name').nullable()
|
||||
table.string('hash').notNullable()
|
||||
table.text('abilities').notNullable()
|
||||
table.timestamp('created_at')
|
||||
table.timestamp('updated_at')
|
||||
table.timestamp('last_used_at').nullable()
|
||||
table.timestamp('expires_at').nullable()
|
||||
})
|
||||
}
|
||||
|
||||
async down() {
|
||||
this.schema.dropTable(this.tableName)
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,20 @@
|
||||
<script setup lang="ts">
|
||||
import { Link, Head, usePage } from '@inertiajs/vue3'
|
||||
import { Link, Head, usePage, router } from '@inertiajs/vue3'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { ref } from 'vue'
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
const { t, locale } = useI18n()
|
||||
const currentLocale = ref(locale.value)
|
||||
const page = usePage()
|
||||
const langMenuOpen = ref(false)
|
||||
const userMenuOpen = ref(false)
|
||||
|
||||
function toggleUserMenu() {
|
||||
userMenuOpen.value = !userMenuOpen.value
|
||||
if (userMenuOpen.value) langMenuOpen.value = false
|
||||
}
|
||||
|
||||
const auth = computed(() => (page.props as any).auth as { username: string | null; authenticated: boolean })
|
||||
|
||||
function switchLocale(lang: string) {
|
||||
locale.value = lang
|
||||
@@ -16,6 +24,11 @@ function switchLocale(lang: string) {
|
||||
|
||||
function toggleLangMenu() {
|
||||
langMenuOpen.value = !langMenuOpen.value
|
||||
if (langMenuOpen.value) userMenuOpen.value = false
|
||||
}
|
||||
|
||||
function logout() {
|
||||
router.post('/logout')
|
||||
}
|
||||
|
||||
function isActive(paths: string | string[]): boolean {
|
||||
@@ -116,9 +129,9 @@ function isActive(paths: string | string[]): boolean {
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<!-- Right: language dropdown -->
|
||||
<!-- Right: language dropdown + logout -->
|
||||
<div class="navbar-right">
|
||||
<ul class="navbar-nav">
|
||||
<ul class="navbar-nav navbar-nav_container">
|
||||
<li class="nav-item dropdown" :class="{ show: langMenuOpen }">
|
||||
<a
|
||||
class="nav-link dropdown-toggle"
|
||||
@@ -152,6 +165,34 @@ function isActive(paths: string | string[]): boolean {
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
|
||||
<!-- User dropdown -->
|
||||
<li v-if="auth.authenticated" class="nav-item dropdown" :class="{ show: userMenuOpen }">
|
||||
<button
|
||||
type="button"
|
||||
class="nav-link user-menu-toggle"
|
||||
@click="toggleUserMenu"
|
||||
:aria-expanded="userMenuOpen"
|
||||
>
|
||||
<!-- Person icon -->
|
||||
<div class="mb-1">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path d="M12 12c2.7 0 4.8-2.1 4.8-4.8S14.7 2.4 12 2.4 7.2 4.5 7.2 7.2 9.3 12 12 12zm0 2.4c-3.2 0-9.6 1.6-9.6 4.8v2.4h19.2v-2.4c0-3.2-6.4-4.8-9.6-4.8z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<span class="user-name">{{ auth.username }}</span>
|
||||
</button>
|
||||
<ul class="dropdown-menu dropdown-menu-end" :class="{ show: userMenuOpen }">
|
||||
<li>
|
||||
<button type="button" class="dropdown-item" @click="logout">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true" class="me-2">
|
||||
<path d="M17 7l-1.41 1.41L18.17 11H8v2h10.17l-2.58 2.58L17 17l5-5-5-5zM4 5h8V3H4c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h8v-2H4V5z"/>
|
||||
</svg>
|
||||
{{ t('login.logout') }}
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
@@ -228,6 +269,7 @@ main {
|
||||
.navbar-right .navbar-nav {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
@@ -249,4 +291,34 @@ main {
|
||||
background-color: #ff7900;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.user-menu-toggle {
|
||||
background: none;
|
||||
border: none;
|
||||
padding: var(--bs-nav-link-padding-y) var(--bs-nav-link-padding-x);
|
||||
color: rgba(255, 255, 255, 0.55);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.user-menu-toggle svg {
|
||||
display: block;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.user-menu-toggle:hover,
|
||||
.user-menu-toggle:focus {
|
||||
color: rgba(255, 255, 255, 0.75);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.user-name {
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
|
||||
</style>
|
||||
@@ -249,5 +249,14 @@
|
||||
"close": "Close",
|
||||
"actions": "Actions",
|
||||
"selected": "selected"
|
||||
},
|
||||
"login": {
|
||||
"subtitle": "Auto-Configuration Server",
|
||||
"username": "Username",
|
||||
"password": "Password",
|
||||
"signIn": "Sign in",
|
||||
"signingIn": "Signing in…",
|
||||
"invalidCredentials": "Invalid username or password.",
|
||||
"logout": "Sign out"
|
||||
}
|
||||
}
|
||||
@@ -249,5 +249,14 @@
|
||||
"close": "Fermer",
|
||||
"actions": "Actions",
|
||||
"selected": "sélectionné(s)"
|
||||
},
|
||||
"login": {
|
||||
"subtitle": "Auto-Configuration Server",
|
||||
"username": "Identifiant",
|
||||
"password": "Mot de passe",
|
||||
"signIn": "Se connecter",
|
||||
"signingIn": "Connexion…",
|
||||
"invalidCredentials": "Identifiant ou mot de passe incorrect.",
|
||||
"logout": "Se déconnecter"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
<script setup lang="ts">
|
||||
import { useForm } from '@inertiajs/vue3'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
// No layout — login page is standalone (no navbar/footer)
|
||||
defineOptions({ layout: null })
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
defineProps<{
|
||||
error?: string | null
|
||||
}>()
|
||||
|
||||
const form = useForm({
|
||||
username: '',
|
||||
password: '',
|
||||
})
|
||||
|
||||
function submit() {
|
||||
form.post('/login')
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="min-vh-100 d-flex flex-column align-items-center justify-content-center bg-light">
|
||||
<div class="card shadow-sm" style="width: 100%; max-width: 400px">
|
||||
<div class="card-body p-4">
|
||||
<!-- Header -->
|
||||
<div class="text-center mb-4">
|
||||
<svg
|
||||
width="48"
|
||||
height="48"
|
||||
viewBox="0 0 100 100"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
aria-hidden="true"
|
||||
class="mb-3"
|
||||
>
|
||||
<rect width="100" height="100" fill="#ff7900" />
|
||||
<rect x="14.29" y="71.43" width="71.43" height="14.29" fill="#fff" />
|
||||
</svg>
|
||||
<h1 class="h4 mb-0">ADAM ACS</h1>
|
||||
<p class="text-muted small mt-1">{{ t('login.subtitle') }}</p>
|
||||
</div>
|
||||
|
||||
<!-- Error alert -->
|
||||
<div v-if="error" class="alert alert-danger" role="alert">
|
||||
{{ t('login.invalidCredentials') }}
|
||||
</div>
|
||||
|
||||
<!-- Login form -->
|
||||
<form @submit.prevent="submit" novalidate>
|
||||
<div class="mb-3">
|
||||
<label for="username" class="form-label">{{ t('login.username') }}</label>
|
||||
<input
|
||||
id="username"
|
||||
v-model="form.username"
|
||||
type="text"
|
||||
class="form-control"
|
||||
:class="{ 'is-invalid': form.errors.username }"
|
||||
autocomplete="username"
|
||||
autofocus
|
||||
required
|
||||
/>
|
||||
<div v-if="form.errors.username" class="invalid-feedback">
|
||||
{{ form.errors.username }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label for="password" class="form-label">{{ t('login.password') }}</label>
|
||||
<input
|
||||
id="password"
|
||||
v-model="form.password"
|
||||
type="password"
|
||||
class="form-control"
|
||||
:class="{ 'is-invalid': form.errors.password }"
|
||||
autocomplete="current-password"
|
||||
required
|
||||
/>
|
||||
<div v-if="form.errors.password" class="invalid-feedback">
|
||||
{{ form.errors.password }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
class="btn btn-primary w-100"
|
||||
:disabled="form.processing"
|
||||
>
|
||||
<span
|
||||
v-if="form.processing"
|
||||
class="spinner-border spinner-border-sm me-2"
|
||||
role="status"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
{{ form.processing ? t('login.signingIn') : t('login.signIn') }}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="mt-4 text-muted small">
|
||||
© Orange 2025
|
||||
</footer>
|
||||
</div>
|
||||
</template>
|
||||
Generated
+2
-404
@@ -13,7 +13,6 @@
|
||||
"@adonisjs/core": "^7.3.3",
|
||||
"@adonisjs/cors": "^3.0.0",
|
||||
"@adonisjs/inertia": "^4.2.0",
|
||||
"@adonisjs/lucid": "^22.4.2",
|
||||
"@adonisjs/session": "^8.1.0",
|
||||
"@adonisjs/shield": "^9.0.0",
|
||||
"@adonisjs/static": "^2.0.1",
|
||||
@@ -595,49 +594,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@adonisjs/lucid": {
|
||||
"version": "22.4.2",
|
||||
"resolved": "https://registry.npmjs.org/@adonisjs/lucid/-/lucid-22.4.2.tgz",
|
||||
"integrity": "sha512-3ztENqBQIFSGAWREHKCGbL1g4ehtsxxsuFX72Uf5bq+oSc0D6UcumjzYpU8TPyBxhmgS6HuRSfz2rG42iuz7AA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@adonisjs/presets": "^3.0.0",
|
||||
"@faker-js/faker": "^10.4.0",
|
||||
"@poppinss/hooks": "^7.3.0",
|
||||
"@poppinss/macroable": "^1.1.2",
|
||||
"@poppinss/qs": "^6.15.0",
|
||||
"@poppinss/utils": "^7.0.1",
|
||||
"deepmerge": "^4.3.1",
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"igniculus": "^1.5.0",
|
||||
"kleur": "^4.1.5",
|
||||
"knex": "^3.2.8",
|
||||
"knex-dynamic-connection": "^5.0.1",
|
||||
"pretty-hrtime": "^1.0.3",
|
||||
"slash": "^5.1.0",
|
||||
"tarn": "^3.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=24.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@adonisjs/assembler": "^8.0.0-next.29 || ^8.0.0",
|
||||
"@adonisjs/core": "^7.0.0-next.21 || ^7.0.0",
|
||||
"@vinejs/vine": "^3.0.0 || ^4.0.0",
|
||||
"luxon": "^3.4.4"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@adonisjs/assembler": {
|
||||
"optional": true
|
||||
},
|
||||
"@vinejs/vine": {
|
||||
"optional": true
|
||||
},
|
||||
"luxon": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@adonisjs/presets": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@adonisjs/presets/-/presets-3.0.0.tgz",
|
||||
@@ -1375,22 +1331,6 @@
|
||||
"node": "^20.19.0 || ^22.13.0 || >=24"
|
||||
}
|
||||
},
|
||||
"node_modules/@faker-js/faker": {
|
||||
"version": "10.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-10.4.0.tgz",
|
||||
"integrity": "sha512-sDBWI3yLy8EcDzgobvJTWq1MJYzAkQdpjXuPukga9wXonhpMRvd1Izuo2Qgwey2OiEoRIBr35RMU9HJRoOHzpw==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/fakerjs"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^20.19.0 || ^22.13.0 || ^23.5.0 || >=24.0.0",
|
||||
"npm": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@fortawesome/fontawesome-common-types": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-7.2.0.tgz",
|
||||
@@ -6301,6 +6241,7 @@
|
||||
"version": "2.0.19",
|
||||
"resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.19.tgz",
|
||||
"integrity": "sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/combined-stream": {
|
||||
@@ -6315,15 +6256,6 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/commander": {
|
||||
"version": "10.0.1",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz",
|
||||
"integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
}
|
||||
},
|
||||
"node_modules/common-path-prefix": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/common-path-prefix/-/common-path-prefix-3.0.0.tgz",
|
||||
@@ -6504,15 +6436,6 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/deepmerge": {
|
||||
"version": "4.3.1",
|
||||
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
|
||||
"integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/default-browser": {
|
||||
"version": "5.5.0",
|
||||
"resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.5.0.tgz",
|
||||
@@ -7083,15 +7006,6 @@
|
||||
"url": "https://opencollective.com/eslint"
|
||||
}
|
||||
},
|
||||
"node_modules/esm": {
|
||||
"version": "3.2.25",
|
||||
"resolved": "https://registry.npmjs.org/esm/-/esm-3.2.25.tgz",
|
||||
"integrity": "sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/esm-env": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.2.tgz",
|
||||
@@ -7787,15 +7701,6 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/get-package-type": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz",
|
||||
"integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/get-port": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/get-port/-/get-port-7.2.0.tgz",
|
||||
@@ -7856,6 +7761,7 @@
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/getopts/-/getopts-2.3.0.tgz",
|
||||
"integrity": "sha512-5eDf9fuSXwxBL6q5HX+dhDj+dslFGWzU5thZ9kNKUkcPtaPdatmUFKwHFrLb/uf/WpA4BHET+AX3Scl56cAjpA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/glob-parent": {
|
||||
@@ -8098,15 +8004,6 @@
|
||||
],
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/igniculus": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/igniculus/-/igniculus-1.5.0.tgz",
|
||||
"integrity": "sha512-vhj2J/cSzNg2G5tcK4Z1KZdeYmQa5keoxFULUYAxctK/zHJb1oraO7noCqnJxKe1b2eZdiiaSL1IHPOFAI8UYQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ignore": {
|
||||
"version": "5.3.2",
|
||||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
|
||||
@@ -8207,15 +8104,6 @@
|
||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/interpret": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/interpret/-/interpret-2.2.0.tgz",
|
||||
"integrity": "sha512-Ju0Bz/cEia55xDwUWEa8+olFpCiQoypjnQySseKtmjNrnps3P+xfpUmGr90T7yjlVJmOtybRvPXhKMbHr+fWnw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/ipaddr.js": {
|
||||
"version": "1.9.1",
|
||||
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
|
||||
@@ -8241,21 +8129,6 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/is-core-module": {
|
||||
"version": "2.16.2",
|
||||
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz",
|
||||
"integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"hasown": "^2.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/is-docker": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz",
|
||||
@@ -8646,179 +8519,6 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/knex": {
|
||||
"version": "3.2.10",
|
||||
"resolved": "https://registry.npmjs.org/knex/-/knex-3.2.10.tgz",
|
||||
"integrity": "sha512-oypTHfrc9i72iyxaUQBKHOxhcr0xM65MPf6FpN02nimsftXwzXprIkLjfXdubvhbu4PMWLp023q8o8CYvHSuZw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"colorette": "2.0.19",
|
||||
"commander": "^10.0.0",
|
||||
"debug": "4.3.4",
|
||||
"escalade": "^3.1.1",
|
||||
"esm": "^3.2.25",
|
||||
"get-package-type": "^0.1.0",
|
||||
"getopts": "2.3.0",
|
||||
"interpret": "^2.2.0",
|
||||
"lodash": "^4.18.1",
|
||||
"pg-connection-string": "2.6.2",
|
||||
"rechoir": "^0.8.0",
|
||||
"resolve-from": "^5.0.0",
|
||||
"tarn": "^3.0.2",
|
||||
"tildify": "2.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"knex": "bin/cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"pg-query-stream": "^4.14.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"better-sqlite3": {
|
||||
"optional": true
|
||||
},
|
||||
"mysql": {
|
||||
"optional": true
|
||||
},
|
||||
"mysql2": {
|
||||
"optional": true
|
||||
},
|
||||
"pg": {
|
||||
"optional": true
|
||||
},
|
||||
"pg-native": {
|
||||
"optional": true
|
||||
},
|
||||
"pg-query-stream": {
|
||||
"optional": true
|
||||
},
|
||||
"sqlite3": {
|
||||
"optional": true
|
||||
},
|
||||
"tedious": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/knex-dynamic-connection": {
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/knex-dynamic-connection/-/knex-dynamic-connection-5.1.0.tgz",
|
||||
"integrity": "sha512-kv41rybMmUKOEAxkGytKRITAthuHFzt0l5vRT1SuTOcve4a9JMc1t/ElVOTRlBEkBexWX8A5WcO71cufrqAxkw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"debug": "^4.4.3",
|
||||
"knex": "3.2.7"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=24.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/knex-dynamic-connection/node_modules/knex": {
|
||||
"version": "3.2.7",
|
||||
"resolved": "https://registry.npmjs.org/knex/-/knex-3.2.7.tgz",
|
||||
"integrity": "sha512-VxdDE72x7Tc08E5yCu8HqYoeOm0HOjAraOtYiGSAUJTYkydwfSGBOpQqYHrzM5vjLNzw2JDL2vDH8m7DjIjtgA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"colorette": "2.0.19",
|
||||
"commander": "^10.0.0",
|
||||
"debug": "4.3.4",
|
||||
"escalade": "^3.1.1",
|
||||
"esm": "^3.2.25",
|
||||
"get-package-type": "^0.1.0",
|
||||
"getopts": "2.3.0",
|
||||
"interpret": "^2.2.0",
|
||||
"lodash": "^4.17.21",
|
||||
"pg-connection-string": "2.6.2",
|
||||
"rechoir": "^0.8.0",
|
||||
"resolve-from": "^5.0.0",
|
||||
"tarn": "^3.0.2",
|
||||
"tildify": "2.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"knex": "bin/cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"pg-query-stream": "^4.14.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"better-sqlite3": {
|
||||
"optional": true
|
||||
},
|
||||
"mysql": {
|
||||
"optional": true
|
||||
},
|
||||
"mysql2": {
|
||||
"optional": true
|
||||
},
|
||||
"pg": {
|
||||
"optional": true
|
||||
},
|
||||
"pg-native": {
|
||||
"optional": true
|
||||
},
|
||||
"pg-query-stream": {
|
||||
"optional": true
|
||||
},
|
||||
"sqlite3": {
|
||||
"optional": true
|
||||
},
|
||||
"tedious": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/knex-dynamic-connection/node_modules/knex/node_modules/debug": {
|
||||
"version": "4.3.4",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
|
||||
"integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "2.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"supports-color": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/knex-dynamic-connection/node_modules/ms": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
||||
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/knex/node_modules/debug": {
|
||||
"version": "4.3.4",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
|
||||
"integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "2.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"supports-color": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/knex/node_modules/ms": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
||||
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/ky": {
|
||||
"version": "1.14.3",
|
||||
"resolved": "https://registry.npmjs.org/ky/-/ky-1.14.3.tgz",
|
||||
@@ -9150,12 +8850,6 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/lodash": {
|
||||
"version": "4.18.1",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz",
|
||||
"integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash-es": {
|
||||
"version": "4.18.1",
|
||||
"resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.18.1.tgz",
|
||||
@@ -9894,12 +9588,6 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/path-parse": {
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
|
||||
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/pathe": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
|
||||
@@ -9907,12 +9595,6 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/pg-connection-string": {
|
||||
"version": "2.6.2",
|
||||
"resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.6.2.tgz",
|
||||
"integrity": "sha512-ch6OwaeaPYcova4kKZ15sbJ2hKb/VP48ZD2gE7i1J+L4MspCtBMAx8nMgz7bksc7IojCIIWuEhHibSMFH8m8oA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/pg-int8": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz",
|
||||
@@ -10514,18 +10196,6 @@
|
||||
"node": ">= 12.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/rechoir": {
|
||||
"version": "0.8.0",
|
||||
"resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz",
|
||||
"integrity": "sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"resolve": "^1.20.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 10.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/reflect-metadata": {
|
||||
"version": "0.2.2",
|
||||
"resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz",
|
||||
@@ -10586,36 +10256,6 @@
|
||||
"node": ">=9.3.0 || >=8.10.0 <9.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/resolve": {
|
||||
"version": "1.22.12",
|
||||
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz",
|
||||
"integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
"is-core-module": "^2.16.1",
|
||||
"path-parse": "^1.0.7",
|
||||
"supports-preserve-symlinks-flag": "^1.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"resolve": "bin/resolve"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/resolve-from": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz",
|
||||
"integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/resolve-pkg-maps": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz",
|
||||
@@ -11001,18 +10641,6 @@
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/slash": {
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/slash/-/slash-5.1.0.tgz",
|
||||
"integrity": "sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=14.16"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/slashes": {
|
||||
"version": "3.0.12",
|
||||
"resolved": "https://registry.npmjs.org/slashes/-/slashes-3.0.12.tgz",
|
||||
@@ -11338,18 +10966,6 @@
|
||||
"url": "https://github.com/chalk/supports-color?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/supports-preserve-symlinks-flag": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
|
||||
"integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/svelte": {
|
||||
"version": "5.55.10",
|
||||
"resolved": "https://registry.npmjs.org/svelte/-/svelte-5.55.10.tgz",
|
||||
@@ -11406,15 +11022,6 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/tarn": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/tarn/-/tarn-3.0.2.tgz",
|
||||
"integrity": "sha512-51LAVKUSZSVfI05vjPESNc5vwqqZpbXCsU+/+wxlOrUjk2SnFTt97v9ZgQrD4YmxYW1Px6w2KjaDitCfkvgxMQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/tempura": {
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/tempura/-/tempura-0.4.1.tgz",
|
||||
@@ -11454,15 +11061,6 @@
|
||||
"integrity": "sha512-P4nbQYQfePJxRSmY+v/KINxVucm4NF3p3s7pJveMTtom52FR4YGltUQLB8idDXwDDWW+eYrWDFbuzUnjoWHF7g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tildify": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/tildify/-/tildify-2.0.0.tgz",
|
||||
"integrity": "sha512-Cc+OraorugtXNfs50hU9KS369rFXCfgGLpfCfvlc+Ud5u6VWmUQsOAa9HbTvheQdYnrdJqqv1e5oIqXppMYnSw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/timekeeper": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/timekeeper/-/timekeeper-2.3.1.tgz",
|
||||
|
||||
@@ -71,7 +71,6 @@
|
||||
"@adonisjs/core": "^7.3.3",
|
||||
"@adonisjs/cors": "^3.0.0",
|
||||
"@adonisjs/inertia": "^4.2.0",
|
||||
"@adonisjs/lucid": "^22.4.2",
|
||||
"@adonisjs/session": "^8.1.0",
|
||||
"@adonisjs/shield": "^9.0.0",
|
||||
"@adonisjs/static": "^2.0.1",
|
||||
|
||||
@@ -43,6 +43,13 @@ export default await Env.create(new URL('../', import.meta.url), {
|
||||
// Security
|
||||
HASH_KEY: Env.schema.string.optional(),
|
||||
|
||||
// Web UI credentials (required — no hardcoded fallback)
|
||||
WEBUI_USERNAME: Env.schema.string(),
|
||||
WEBUI_PASSWORD: Env.schema.string(),
|
||||
|
||||
// REST API Key (required — must be set before boot)
|
||||
API_KEY: Env.schema.string(),
|
||||
|
||||
// Exchange Store
|
||||
EXCHANGE_STORE_MAX_MESSAGES: Env.schema.number.optional(),
|
||||
EXCHANGE_STORE_TTL_SECONDS: Env.schema.number.optional(),
|
||||
|
||||
@@ -20,4 +20,10 @@ router.use([
|
||||
export const middleware = router.named({
|
||||
cwmpLogger: () => import('#middleware/cwmp_logger_middleware'),
|
||||
cwmp: () => import('#middleware/cwmp_middleware'),
|
||||
// basicAuth: available for machine-to-machine Basic Auth access if needed in the future
|
||||
basicAuth: () => import('#middleware/basic_auth_middleware'),
|
||||
// apiKey: protects all /api/v1 routes (X-Api-Key header)
|
||||
apiKey: () => import('#middleware/api_key_middleware'),
|
||||
// authSession: protects all web-ui routes (session-based login page)
|
||||
authSession: () => import('#middleware/auth_session_middleware'),
|
||||
})
|
||||
+30
-19
@@ -11,6 +11,7 @@ const PagesController = () => import('#controllers/pages_controller')
|
||||
const CwmpController = () => import('#controllers/cwmp_controller')
|
||||
const Tr069TraceController = () => import('#controllers/tr069_trace_controller')
|
||||
const ExchangeController = () => import('#controllers/exchange_controller')
|
||||
const AuthController = () => import('#controllers/auth_controller')
|
||||
|
||||
// CWMP/TR-069 endpoint (SOAP/XML) - uses dedicated middleware for XML parsing
|
||||
router.post('/cwmp10', [CwmpController, 'handle']).use([middleware.cwmpLogger(), middleware.cwmp()])
|
||||
@@ -20,7 +21,7 @@ router.get('/api/v1/checklife', [HealthController, 'checklife'])
|
||||
router.get('/api/v1/checkservice', [HealthController, 'checkservice'])
|
||||
router.get('/api/v1/health/tr069-exchanges', [HealthController, 'checkTr069Exchanges'])
|
||||
|
||||
// REST API routes
|
||||
// REST API routes — protected by API key (X-Api-Key header)
|
||||
router
|
||||
.group(() => {
|
||||
// Devices
|
||||
@@ -81,23 +82,33 @@ router
|
||||
router.delete('/firmware-catalog/routes/:id', [FirmwareCatalogController, 'deleteRoute'])
|
||||
})
|
||||
.prefix('/api/v1')
|
||||
.use([middleware.apiKey()])
|
||||
|
||||
// Inertia page routes
|
||||
router.get('/', [PagesController, 'home'])
|
||||
router.get('/devices/:identifier', [PagesController, 'deviceDetail'])
|
||||
router.post('/devices/:identifier/refresh', [PagesController, 'refreshDevice'])
|
||||
router.post('/devices/:identifier/get-model', [PagesController, 'getModel'])
|
||||
router.get('/commands', [PagesController, 'commands']).as('commands.page')
|
||||
router.get('/commands/fleet', [PagesController, 'commandsFleet'])
|
||||
router.get('/tr069-trace', [PagesController, 'tr069Trace'])
|
||||
router.post('/commands/fleet', [PagesController, 'storeFleetDevices'])
|
||||
router.post('/commands/fleet/command', [PagesController, 'storeFleetCommand'])
|
||||
// Auth routes — public (no session guard)
|
||||
router.get('/login', [AuthController, 'showLogin']).as('login')
|
||||
router.post('/login', [AuthController, 'login'])
|
||||
router.post('/logout', [AuthController, 'logout']).as('logout')
|
||||
|
||||
// Unified Configuration System Inertia routes
|
||||
router.get('/configurations', [PagesController, 'configurationsList'])
|
||||
router.get('/configurations/create', [PagesController, 'configurationCreate'])
|
||||
router.get('/configurations/:id/edit', [PagesController, 'configurationEdit'])
|
||||
router.get('/configurations/:id/detail', [PagesController, 'configurationDetail'])
|
||||
router.get('/firmware-catalog', [PagesController, 'firmwareCatalog'])
|
||||
router.get('/firmware-catalog/create', [PagesController, 'firmwareCatalogCreate'])
|
||||
router.get('/firmware-catalog/:id/edit', [PagesController, 'firmwareCatalogEdit'])
|
||||
// Inertia page routes — protected by session authentication
|
||||
router
|
||||
.group(() => {
|
||||
router.get('/', [PagesController, 'home'])
|
||||
router.get('/devices/:identifier', [PagesController, 'deviceDetail'])
|
||||
router.post('/devices/:identifier/refresh', [PagesController, 'refreshDevice'])
|
||||
router.post('/devices/:identifier/get-model', [PagesController, 'getModel'])
|
||||
router.get('/commands', [PagesController, 'commands']).as('commands.page')
|
||||
router.get('/commands/fleet', [PagesController, 'commandsFleet'])
|
||||
router.get('/tr069-trace', [PagesController, 'tr069Trace'])
|
||||
router.post('/commands/fleet', [PagesController, 'storeFleetDevices'])
|
||||
router.post('/commands/fleet/command', [PagesController, 'storeFleetCommand'])
|
||||
|
||||
// Unified Configuration System Inertia routes
|
||||
router.get('/configurations', [PagesController, 'configurationsList'])
|
||||
router.get('/configurations/create', [PagesController, 'configurationCreate'])
|
||||
router.get('/configurations/:id/edit', [PagesController, 'configurationEdit'])
|
||||
router.get('/configurations/:id/detail', [PagesController, 'configurationDetail'])
|
||||
router.get('/firmware-catalog', [PagesController, 'firmwareCatalog'])
|
||||
router.get('/firmware-catalog/create', [PagesController, 'firmwareCatalogCreate'])
|
||||
router.get('/firmware-catalog/:id/edit', [PagesController, 'firmwareCatalogEdit'])
|
||||
})
|
||||
.use([middleware.authSession()])
|
||||
Reference in new issue
Block a user