admin login working
This commit is contained in:
parent
09d5639ab9
commit
0ecd62e796
|
|
@ -8,6 +8,9 @@ LOG_LEVEL=info
|
|||
APP_KEY=some_random_key
|
||||
NODE_ENV=development
|
||||
SESSION_DRIVER=cookie
|
||||
# Admin Login credentials. ADMIN_PASS must be non-empty before admin login works.
|
||||
ADMIN_USER=admin
|
||||
ADMIN_PASS=replaceme
|
||||
DB_HOST=localhost
|
||||
DB_PORT=3306
|
||||
DB_USER=root
|
||||
|
|
@ -27,4 +30,4 @@ NOMAD_STORAGE_PATH=/opt/project-nomad/storage
|
|||
# CREATOR_PACKS_APP_KEY at build time; leave unset for a build that can't install
|
||||
# packs. CREATOR_PACKS_WORKER_BASE overrides the entitlement Worker origin.
|
||||
# CREATOR_PACKS_APP_KEY=
|
||||
# CREATOR_PACKS_WORKER_BASE=https://nomad-packs-worker.chris-556.workers.dev
|
||||
# CREATOR_PACKS_WORKER_BASE=https://nomad-packs-worker.chris-556.workers.dev
|
||||
|
|
|
|||
|
|
@ -0,0 +1,48 @@
|
|||
import AdminAuthService from '#services/admin_auth_service'
|
||||
import type { HttpContext } from '@adonisjs/core/http'
|
||||
|
||||
export default class AdminAuthController {
|
||||
async login({ request, response, session }: HttpContext) {
|
||||
const user = String(request.input('user', '')).trim()
|
||||
const password = String(request.input('password', ''))
|
||||
const redirectTo = this.safeRedirect(request.input('redirect'))
|
||||
|
||||
if (!AdminAuthService.authenticate(user, password)) {
|
||||
session.flash('errors', {
|
||||
password: AdminAuthService.isConfigured()
|
||||
? 'The admin user or password was incorrect.'
|
||||
: 'Admin login is not configured. Set ADMIN_USER and ADMIN_PASS in Docker Compose.',
|
||||
})
|
||||
return response.redirect().back()
|
||||
}
|
||||
|
||||
await session.regenerate()
|
||||
session.put('admin.isLoggedIn', true)
|
||||
|
||||
return response.redirect().toPath(redirectTo)
|
||||
}
|
||||
|
||||
async logout({ response, session }: HttpContext) {
|
||||
session.forget('admin.isLoggedIn')
|
||||
await session.regenerate()
|
||||
|
||||
return response.redirect().toPath('/home')
|
||||
}
|
||||
|
||||
/**
|
||||
* Restrict redirects to local paths and keep auth routes from looping.
|
||||
*/
|
||||
private safeRedirect(value: unknown): string {
|
||||
const redirectTo = typeof value === 'string' ? value : '/home'
|
||||
|
||||
if (!redirectTo.startsWith('/') || redirectTo.startsWith('//')) {
|
||||
return '/home'
|
||||
}
|
||||
|
||||
if (redirectTo.startsWith('/admin/login') || redirectTo.startsWith('/admin/logout')) {
|
||||
return '/home'
|
||||
}
|
||||
|
||||
return redirectTo
|
||||
}
|
||||
}
|
||||
|
|
@ -10,6 +10,12 @@ import env from '#start/env'
|
|||
|
||||
@inject()
|
||||
export default class SettingsController {
|
||||
private static publicWritableSettings = new Set([
|
||||
'chat.lastModel',
|
||||
'rag.defaultIngestPolicy',
|
||||
'ui.theme',
|
||||
])
|
||||
|
||||
constructor(
|
||||
private systemService: SystemService,
|
||||
private mapService: MapService,
|
||||
|
|
@ -140,8 +146,15 @@ export default class SettingsController {
|
|||
return response.status(200).send({ key, value });
|
||||
}
|
||||
|
||||
async updateSetting({ request, response }: HttpContext) {
|
||||
async updateSetting({ request, response, session }: HttpContext) {
|
||||
const reqData = await request.validateUsing(updateSettingSchema)
|
||||
if (
|
||||
!session.get('admin.isLoggedIn') &&
|
||||
!SettingsController.publicWritableSettings.has(reqData.key)
|
||||
) {
|
||||
return response.status(403).send({ success: false, message: 'Admin login is required.' })
|
||||
}
|
||||
|
||||
const valueError = validateSettingValue(reqData.key, reqData.value)
|
||||
if (valueError) {
|
||||
return response.status(422).send({ success: false, message: valueError })
|
||||
|
|
|
|||
|
|
@ -0,0 +1,20 @@
|
|||
import type { HttpContext } from '@adonisjs/core/http'
|
||||
import type { NextFn } from '@adonisjs/core/types/http'
|
||||
|
||||
export default class RequireAdminMiddleware {
|
||||
async handle(ctx: HttpContext, next: NextFn) {
|
||||
if (ctx.session.get('admin.isLoggedIn')) {
|
||||
return next()
|
||||
}
|
||||
|
||||
if (ctx.request.accepts(['html', 'json']) === 'json') {
|
||||
return ctx.response.status(403).send({
|
||||
success: false,
|
||||
message: 'Admin login is required.',
|
||||
})
|
||||
}
|
||||
|
||||
const redirectTo = encodeURIComponent(ctx.request.url(true))
|
||||
return ctx.response.redirect().toPath(`/home?adminLogin=1&redirect=${redirectTo}`)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
import { timingSafeEqual } from 'node:crypto'
|
||||
import env from '#start/env'
|
||||
|
||||
const FALLBACK_ADMIN_USER = 'admin'
|
||||
|
||||
export default class AdminAuthService {
|
||||
/**
|
||||
* Resolve the configured admin username. The password must still be provided
|
||||
* with ADMIN_PASS before login is enabled.
|
||||
*/
|
||||
static user(): string {
|
||||
return env.get('ADMIN_USER', FALLBACK_ADMIN_USER).trim() || FALLBACK_ADMIN_USER
|
||||
}
|
||||
|
||||
/**
|
||||
* Admin login is disabled until ADMIN_PASS has a non-empty value.
|
||||
*/
|
||||
static isConfigured(): boolean {
|
||||
return Boolean(env.get('ADMIN_PASS')?.trim())
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare supplied credentials to environment-controlled credentials.
|
||||
*/
|
||||
static authenticate(user: string, password: string): boolean {
|
||||
const configuredPassword = env.get('ADMIN_PASS')?.trim()
|
||||
|
||||
if (!configuredPassword) {
|
||||
return false
|
||||
}
|
||||
|
||||
return (
|
||||
this.secureCompare(user, this.user()) &&
|
||||
this.secureCompare(password, configuredPassword)
|
||||
)
|
||||
}
|
||||
|
||||
private static secureCompare(input: string, expected: string): boolean {
|
||||
const inputBuffer = Buffer.from(input)
|
||||
const expectedBuffer = Buffer.from(expected)
|
||||
|
||||
if (inputBuffer.length !== expectedBuffer.length) {
|
||||
return false
|
||||
}
|
||||
|
||||
return timingSafeEqual(inputBuffer, expectedBuffer)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,8 @@
|
|||
import KVStore from '#models/kv_store'
|
||||
import AdminAuthService from '#services/admin_auth_service'
|
||||
import { SystemService } from '#services/system_service'
|
||||
import { defineConfig } from '@adonisjs/inertia'
|
||||
import type { HttpContext } from '@adonisjs/core/http'
|
||||
import type { InferSharedProps } from '@adonisjs/inertia/types'
|
||||
|
||||
let _assistantNameCache: { value: string; expiresAt: number } | null = null
|
||||
|
|
@ -21,6 +23,11 @@ const inertiaConfig = defineConfig({
|
|||
sharedData: {
|
||||
appVersion: () => SystemService.getAppVersion(),
|
||||
environment: process.env.NODE_ENV || 'production',
|
||||
admin: ({ session }: HttpContext) => ({
|
||||
isConfigured: AdminAuthService.isConfigured(),
|
||||
isLoggedIn: Boolean(session?.get('admin.isLoggedIn')),
|
||||
user: AdminAuthService.user(),
|
||||
}),
|
||||
aiAssistantName: async () => {
|
||||
const now = Date.now()
|
||||
if (_assistantNameCache && now < _assistantNameCache.expiresAt) {
|
||||
|
|
@ -46,4 +53,4 @@ export default inertiaConfig
|
|||
|
||||
declare module '@adonisjs/inertia/types' {
|
||||
export interface SharedProps extends InferSharedProps<typeof inertiaConfig> {}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,48 +1,42 @@
|
|||
// import env from '#start/env'
|
||||
// import app from '@adonisjs/core/services/app'
|
||||
// import { defineConfig, stores } from '@adonisjs/session'
|
||||
import env from '#start/env'
|
||||
import app from '@adonisjs/core/services/app'
|
||||
import { defineConfig, stores } from '@adonisjs/session'
|
||||
|
||||
// const sessionConfig = defineConfig({
|
||||
// enabled: false,
|
||||
// cookieName: 'adonis-session',
|
||||
const sessionConfig = defineConfig({
|
||||
enabled: true,
|
||||
cookieName: 'nomad-admin-session',
|
||||
|
||||
// /**
|
||||
// * When set to true, the session id cookie will be deleted
|
||||
// * once the user closes the browser.
|
||||
// */
|
||||
// clearWithBrowser: false,
|
||||
/**
|
||||
* Keep the browser session available until it expires or the admin logs out.
|
||||
*/
|
||||
clearWithBrowser: false,
|
||||
|
||||
// /**
|
||||
// * Define how long to keep the session data alive without
|
||||
// * any activity.
|
||||
// */
|
||||
// age: '2h',
|
||||
/**
|
||||
* Define how long to keep session data alive without activity.
|
||||
*/
|
||||
age: '2h',
|
||||
|
||||
// /**
|
||||
// * Configuration for session cookie and the
|
||||
// * cookie store
|
||||
// */
|
||||
// cookie: {
|
||||
// path: '/',
|
||||
// httpOnly: true,
|
||||
// secure: app.inProduction,
|
||||
// sameSite: 'lax',
|
||||
// },
|
||||
/**
|
||||
* HTTP-only cookies keep the admin session marker out of client JavaScript.
|
||||
*/
|
||||
cookie: {
|
||||
path: '/',
|
||||
httpOnly: true,
|
||||
secure: app.inProduction,
|
||||
sameSite: 'lax',
|
||||
},
|
||||
|
||||
// /**
|
||||
// * The store to use. Make sure to validate the environment
|
||||
// * variable in order to infer the store name without any
|
||||
// * errors.
|
||||
// */
|
||||
// store: env.get('SESSION_DRIVER'),
|
||||
/**
|
||||
* Cookie storage avoids adding a users table for a single local admin gate.
|
||||
*/
|
||||
store: env.get('SESSION_DRIVER', 'cookie'),
|
||||
|
||||
// /**
|
||||
// * List of configured stores. Refer documentation to see
|
||||
// * list of available stores and their config.
|
||||
// */
|
||||
// stores: {
|
||||
// cookie: stores.cookie(),
|
||||
// },
|
||||
// })
|
||||
/**
|
||||
* List of configured stores.
|
||||
*/
|
||||
stores: {
|
||||
cookie: stores.cookie(),
|
||||
},
|
||||
})
|
||||
|
||||
// export default sessionConfig
|
||||
export default sessionConfig
|
||||
|
|
|
|||
|
|
@ -7,7 +7,8 @@ import {
|
|||
IconSettings,
|
||||
IconWifiOff,
|
||||
} from '@tabler/icons-react'
|
||||
import { Head, Link, router, usePage } from '@inertiajs/react'
|
||||
import { FormEvent, useEffect, useMemo, useState } from 'react'
|
||||
import { Head, Link, router, useForm, usePage } from '@inertiajs/react'
|
||||
import AppLayout from '~/layouts/AppLayout'
|
||||
import { getServiceLink } from '~/lib/navigation'
|
||||
import { ServiceSlim } from '../../types/services'
|
||||
|
|
@ -21,6 +22,8 @@ import {
|
|||
import { useQueryClient } from '@tanstack/react-query'
|
||||
import api from '~/lib/api'
|
||||
import Alert from '~/components/Alert'
|
||||
import Input from '~/components/inputs/Input'
|
||||
import StyledModal from '~/components/StyledModal'
|
||||
import WhatsNewBanner from '~/components/WhatsNewBanner'
|
||||
import { SERVICE_NAMES } from '../../constants/service_names'
|
||||
|
||||
|
|
@ -95,6 +98,8 @@ const SYSTEM_ITEMS = [
|
|||
},
|
||||
]
|
||||
|
||||
const ADMIN_ONLY_LABELS = new Set(['Easy Setup', 'Supply Depot', 'Settings'])
|
||||
|
||||
interface DashboardItem {
|
||||
label: string
|
||||
to: string
|
||||
|
|
@ -119,7 +124,52 @@ export default function Home(props: {
|
|||
const updateInfo = useUpdateAvailable();
|
||||
const rerunBanner = useBenchmarkRerunBanner()
|
||||
const queryClient = useQueryClient()
|
||||
const { aiAssistantName } = usePage<{ aiAssistantName: string }>().props
|
||||
const { admin, aiAssistantName } = usePage<{
|
||||
admin: { isConfigured: boolean; isLoggedIn: boolean; user: string }
|
||||
aiAssistantName: string
|
||||
}>().props
|
||||
const [adminLoginOpen, setAdminLoginOpen] = useState(false)
|
||||
const adminLoginRedirect = useMemo(() => {
|
||||
const params = new URLSearchParams(window.location.search)
|
||||
const redirectTo = params.get('redirect') || '/home'
|
||||
|
||||
if (!redirectTo.startsWith('/') || redirectTo.startsWith('//')) {
|
||||
return '/home'
|
||||
}
|
||||
|
||||
return redirectTo
|
||||
}, [])
|
||||
const adminLoginForm = useForm({
|
||||
user: admin.user || 'admin',
|
||||
password: '',
|
||||
redirect: adminLoginRedirect,
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
const params = new URLSearchParams(window.location.search)
|
||||
if (params.get('adminLogin') === '1' && !admin.isLoggedIn) {
|
||||
setAdminLoginOpen(true)
|
||||
}
|
||||
}, [admin.isLoggedIn])
|
||||
|
||||
const handleAdminLogin = (event?: FormEvent<HTMLFormElement>) => {
|
||||
event?.preventDefault()
|
||||
adminLoginForm.post('/admin/login', {
|
||||
preserveScroll: true,
|
||||
onSuccess: () => setAdminLoginOpen(false),
|
||||
onFinish: () => adminLoginForm.reset('password'),
|
||||
})
|
||||
}
|
||||
|
||||
const openAdminLogin = () => {
|
||||
adminLoginForm.clearErrors()
|
||||
adminLoginForm.setData('redirect', '/home')
|
||||
setAdminLoginOpen(true)
|
||||
}
|
||||
|
||||
const handleAdminLogout = () => {
|
||||
router.post('/admin/logout', {}, { preserveScroll: true })
|
||||
}
|
||||
|
||||
const handleDismissRerunBanner = async () => {
|
||||
await api.updateSetting('benchmark.rerunBannerDismissed', true)
|
||||
|
|
@ -168,8 +218,10 @@ export default function Home(props: {
|
|||
items.push(DRUG_REFERENCE_ITEM)
|
||||
}
|
||||
|
||||
// Add system items
|
||||
items.push(...SYSTEM_ITEMS)
|
||||
// Add system items, hiding admin-only controls until the admin logs in.
|
||||
items.push(
|
||||
...SYSTEM_ITEMS.filter((item) => admin.isLoggedIn || !ADMIN_ONLY_LABELS.has(item.label))
|
||||
)
|
||||
|
||||
// Sort all items by display order
|
||||
items.sort((a, b) => a.displayOrder - b.displayOrder)
|
||||
|
|
@ -177,8 +229,72 @@ export default function Home(props: {
|
|||
return (
|
||||
<AppLayout>
|
||||
<Head title="Command Center" />
|
||||
<div className="fixed left-4 top-4 z-40">
|
||||
<button
|
||||
type="button"
|
||||
onClick={admin.isLoggedIn ? handleAdminLogout : openAdminLogin}
|
||||
className="inline-flex h-12 items-center gap-2 rounded-md border-2 border-desert-green bg-surface-primary px-3 text-sm font-semibold text-desert-green shadow-sm transition-colors hover:bg-desert-green hover:text-white focus:outline-none focus:ring-2 focus:ring-desert-green-light focus:ring-offset-2 focus:ring-offset-desert-sand"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="h-6 w-6 bg-current"
|
||||
style={{
|
||||
maskImage: "url('/admin-profile.png')",
|
||||
maskPosition: 'center',
|
||||
maskRepeat: 'no-repeat',
|
||||
maskSize: 'contain',
|
||||
WebkitMaskImage: "url('/admin-profile.png')",
|
||||
WebkitMaskPosition: 'center',
|
||||
WebkitMaskRepeat: 'no-repeat',
|
||||
WebkitMaskSize: 'contain',
|
||||
}}
|
||||
/>
|
||||
<span>{admin.isLoggedIn ? 'Admin Logout' : 'Admin Login'}</span>
|
||||
</button>
|
||||
</div>
|
||||
<StyledModal
|
||||
open={adminLoginOpen}
|
||||
title="Admin Login"
|
||||
cancelText="Cancel"
|
||||
confirmText="Log In"
|
||||
confirmIcon="IconShieldLock"
|
||||
confirmLoading={adminLoginForm.processing}
|
||||
confirmDisabled={!admin.isConfigured}
|
||||
onCancel={() => setAdminLoginOpen(false)}
|
||||
onClose={() => setAdminLoginOpen(false)}
|
||||
onConfirm={() => handleAdminLogin()}
|
||||
>
|
||||
<form id="admin-login-form" className="space-y-4 text-left" onSubmit={handleAdminLogin}>
|
||||
<Input
|
||||
name="user"
|
||||
label="User"
|
||||
value={adminLoginForm.data.user}
|
||||
onChange={(event) => adminLoginForm.setData('user', event.target.value)}
|
||||
autoComplete="username"
|
||||
required
|
||||
/>
|
||||
<Input
|
||||
name="password"
|
||||
label="Password"
|
||||
type="password"
|
||||
value={adminLoginForm.data.password}
|
||||
onChange={(event) => adminLoginForm.setData('password', event.target.value)}
|
||||
autoComplete="current-password"
|
||||
error={Boolean(adminLoginForm.errors.password)}
|
||||
required
|
||||
/>
|
||||
{adminLoginForm.errors.password && (
|
||||
<p className="text-sm font-medium text-desert-red">{adminLoginForm.errors.password}</p>
|
||||
)}
|
||||
{!admin.isConfigured && !adminLoginForm.errors.password && (
|
||||
<p className="text-sm font-medium text-desert-orange-dark">
|
||||
Admin login is not configured.
|
||||
</p>
|
||||
)}
|
||||
</form>
|
||||
</StyledModal>
|
||||
{
|
||||
updateInfo?.updateAvailable && (
|
||||
admin.isLoggedIn && updateInfo?.updateAvailable && (
|
||||
<div className='flex justify-center items-center p-4 w-full'>
|
||||
<Alert
|
||||
title="An update is available for Project NOMAD!"
|
||||
|
|
@ -197,7 +313,7 @@ export default function Home(props: {
|
|||
}
|
||||
<WhatsNewBanner />
|
||||
{
|
||||
rerunBanner?.show && (
|
||||
admin.isLoggedIn && rerunBanner?.show && (
|
||||
<div className='flex justify-center items-center px-4 pt-4 w-full'>
|
||||
<Alert
|
||||
title="Your benchmark can be re-scored with Score v2"
|
||||
|
|
|
|||
Binary file not shown.
|
After Width: | Height: | Size: 2.9 KiB |
|
|
@ -33,7 +33,15 @@ export default await Env.create(new URL('../', import.meta.url), {
|
|||
| Variables for configuring session package
|
||||
|----------------------------------------------------------
|
||||
*/
|
||||
//SESSION_DRIVER: Env.schema.enum(['cookie', 'memory'] as const),
|
||||
SESSION_DRIVER: Env.schema.enum.optional(['cookie'] as const),
|
||||
|
||||
/*
|
||||
|----------------------------------------------------------
|
||||
| Variables for configuring the built-in admin login
|
||||
|----------------------------------------------------------
|
||||
*/
|
||||
ADMIN_USER: Env.schema.string.optional(),
|
||||
ADMIN_PASS: Env.schema.string.optional(),
|
||||
|
||||
/*
|
||||
|----------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ server.use([
|
|||
*/
|
||||
router.use([
|
||||
() => import('@adonisjs/core/bodyparser_middleware'),
|
||||
// () => import('@adonisjs/session/session_middleware'),
|
||||
() => import('@adonisjs/session/session_middleware'),
|
||||
() => import('@adonisjs/shield/shield_middleware'),
|
||||
() => import('#middleware/compression_middleware'),
|
||||
])
|
||||
|
|
@ -46,4 +46,6 @@ router.use([
|
|||
* Named middleware collection must be explicitly assigned to
|
||||
* the routes or the routes group.
|
||||
*/
|
||||
export const middleware = router.named({})
|
||||
export const middleware = router.named({
|
||||
admin: () => import('#middleware/require_admin_middleware'),
|
||||
})
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
|
|
||||
*/
|
||||
import BenchmarkController from '#controllers/benchmark_controller'
|
||||
import AdminAuthController from '#controllers/admin_auth_controller'
|
||||
import ChatsController from '#controllers/chats_controller'
|
||||
import ConditionsController from '#controllers/conditions_controller'
|
||||
import DocsController from '#controllers/docs_controller'
|
||||
|
|
@ -28,6 +29,7 @@ import ZimController from '#controllers/zim_controller'
|
|||
import router from '@adonisjs/core/services/router'
|
||||
import transmit from '@adonisjs/transmit/services/main'
|
||||
import { documented } from '#start/openapi/documented'
|
||||
import { middleware } from '#start/kernel'
|
||||
import {
|
||||
remoteDownloadValidator,
|
||||
remoteDownloadWithMetadataValidator,
|
||||
|
|
@ -101,22 +103,29 @@ router.get('/home', [HomeController, 'home'])
|
|||
router.on('/about').renderInertia('about')
|
||||
router.get('/chat', [ChatsController, 'inertia'])
|
||||
router.get('/maps', [MapsController, 'index'])
|
||||
router.get('/supply-depot', [SupplyDepotController, 'index'])
|
||||
router.post('/admin/login', [AdminAuthController, 'login'])
|
||||
router.post('/admin/logout', [AdminAuthController, 'logout'])
|
||||
router.get('/supply-depot', [SupplyDepotController, 'index']).use(middleware.admin())
|
||||
router.on('/knowledge-base').redirectToPath('/chat?knowledge_base=true') // redirect for legacy knowledge-base links
|
||||
|
||||
router.get('/easy-setup', [EasySetupController, 'index'])
|
||||
router.get('/easy-setup/complete', [EasySetupController, 'complete'])
|
||||
router.get('/easy-setup', [EasySetupController, 'index']).use(middleware.admin())
|
||||
router.get('/easy-setup/complete', [EasySetupController, 'complete']).use(middleware.admin())
|
||||
documented(
|
||||
router.get('/api/easy-setup/curated-categories', [EasySetupController, 'listCuratedCategories']),
|
||||
router
|
||||
.get('/api/easy-setup/curated-categories', [EasySetupController, 'listCuratedCategories'])
|
||||
.use(middleware.admin()),
|
||||
{
|
||||
summary: 'List curated easy-setup categories',
|
||||
tags: ['easy-setup'],
|
||||
}
|
||||
)
|
||||
documented(router.post('/api/manifests/refresh', [EasySetupController, 'refreshManifests']), {
|
||||
summary: 'Refresh content manifests',
|
||||
tags: ['easy-setup'],
|
||||
})
|
||||
documented(
|
||||
router.post('/api/manifests/refresh', [EasySetupController, 'refreshManifests']).use(middleware.admin()),
|
||||
{
|
||||
summary: 'Refresh content manifests',
|
||||
tags: ['easy-setup'],
|
||||
}
|
||||
)
|
||||
router
|
||||
.group(() => {
|
||||
documented(router.post('/check', [CollectionUpdatesController, 'checkForUpdates']), {
|
||||
|
|
@ -135,6 +144,7 @@ router
|
|||
})
|
||||
})
|
||||
.prefix('/api/content-updates')
|
||||
.use(middleware.admin())
|
||||
|
||||
router
|
||||
.group(() => {
|
||||
|
|
@ -152,6 +162,7 @@ router
|
|||
router.get('/advanced', [SettingsController, 'advanced'])
|
||||
})
|
||||
.prefix('/settings')
|
||||
.use(middleware.admin())
|
||||
|
||||
router
|
||||
.group(() => {
|
||||
|
|
@ -181,26 +192,26 @@ router
|
|||
summary: 'Fetch the latest map collections',
|
||||
tags: ['maps'],
|
||||
})
|
||||
documented(router.post('/download-base-assets', [MapsController, 'downloadBaseAssets']), {
|
||||
documented(router.post('/download-base-assets', [MapsController, 'downloadBaseAssets']).use(middleware.admin()), {
|
||||
summary: 'Download base map assets',
|
||||
tags: ['maps'],
|
||||
request: remoteDownloadValidatorOptional,
|
||||
})
|
||||
documented(router.post('/setup-world-basemap', [MapsController, 'setupWorldBasemap']), {
|
||||
documented(router.post('/setup-world-basemap', [MapsController, 'setupWorldBasemap']).use(middleware.admin()), {
|
||||
summary: 'Provision the world base map',
|
||||
tags: ['maps'],
|
||||
})
|
||||
documented(router.post('/download-remote', [MapsController, 'downloadRemote']), {
|
||||
documented(router.post('/download-remote', [MapsController, 'downloadRemote']).use(middleware.admin()), {
|
||||
summary: 'Queue a remote map download',
|
||||
tags: ['maps'],
|
||||
request: remoteDownloadValidator,
|
||||
})
|
||||
documented(router.post('/download-remote-preflight', [MapsController, 'downloadRemotePreflight']), {
|
||||
documented(router.post('/download-remote-preflight', [MapsController, 'downloadRemotePreflight']).use(middleware.admin()), {
|
||||
summary: 'Preflight a remote map download',
|
||||
tags: ['maps'],
|
||||
request: remoteDownloadValidator,
|
||||
})
|
||||
documented(router.post('/download-collection', [MapsController, 'downloadCollection']), {
|
||||
documented(router.post('/download-collection', [MapsController, 'downloadCollection']).use(middleware.admin()), {
|
||||
summary: 'Download a map collection',
|
||||
tags: ['maps'],
|
||||
request: downloadCollectionValidator,
|
||||
|
|
@ -209,7 +220,7 @@ router
|
|||
summary: 'Get global map information',
|
||||
tags: ['maps'],
|
||||
})
|
||||
documented(router.post('/download-global-map', [MapsController, 'downloadGlobalMap']), {
|
||||
documented(router.post('/download-global-map', [MapsController, 'downloadGlobalMap']).use(middleware.admin()), {
|
||||
summary: 'Download the global map',
|
||||
tags: ['maps'],
|
||||
})
|
||||
|
|
@ -221,12 +232,12 @@ router
|
|||
summary: 'List country groups',
|
||||
tags: ['maps'],
|
||||
})
|
||||
documented(router.post('/extract-preflight', [MapsController, 'extractPreflight']), {
|
||||
documented(router.post('/extract-preflight', [MapsController, 'extractPreflight']).use(middleware.admin()), {
|
||||
summary: 'Preflight a map region extraction',
|
||||
tags: ['maps'],
|
||||
request: mapExtractPreflightValidator,
|
||||
})
|
||||
documented(router.post('/extract', [MapsController, 'extractRegion']), {
|
||||
documented(router.post('/extract', [MapsController, 'extractRegion']).use(middleware.admin()), {
|
||||
summary: 'Extract a map region',
|
||||
tags: ['maps'],
|
||||
request: mapExtractValidator,
|
||||
|
|
@ -247,7 +258,7 @@ router
|
|||
summary: 'Delete a map marker',
|
||||
tags: ['maps'],
|
||||
})
|
||||
documented(router.delete('/:filename', [MapsController, 'delete']), {
|
||||
documented(router.delete('/:filename', [MapsController, 'delete']).use(middleware.admin()), {
|
||||
summary: 'Delete a map file',
|
||||
tags: ['maps'],
|
||||
params: filenameParamValidator,
|
||||
|
|
@ -534,110 +545,110 @@ router
|
|||
summary: 'List services',
|
||||
tags: ['system'],
|
||||
})
|
||||
documented(router.post('/services/affect', [SystemController, 'affectService']), {
|
||||
documented(router.post('/services/affect', [SystemController, 'affectService']).use(middleware.admin()), {
|
||||
summary: 'Start, stop, or restart a service',
|
||||
tags: ['system'],
|
||||
request: affectServiceValidator,
|
||||
})
|
||||
documented(router.post('/services/install', [SystemController, 'installService']), {
|
||||
documented(router.post('/services/install', [SystemController, 'installService']).use(middleware.admin()), {
|
||||
summary: 'Install a service',
|
||||
tags: ['system'],
|
||||
request: installServiceValidator,
|
||||
})
|
||||
documented(router.post('/services/force-reinstall', [SystemController, 'forceReinstallService']), {
|
||||
documented(router.post('/services/force-reinstall', [SystemController, 'forceReinstallService']).use(middleware.admin()), {
|
||||
summary: 'Force reinstall a service',
|
||||
tags: ['system'],
|
||||
request: installServiceValidator,
|
||||
})
|
||||
documented(router.post('/services/uninstall', [SystemController, 'uninstallService']), {
|
||||
documented(router.post('/services/uninstall', [SystemController, 'uninstallService']).use(middleware.admin()), {
|
||||
summary: 'Uninstall a service',
|
||||
tags: ['system'],
|
||||
request: uninstallServiceValidator,
|
||||
})
|
||||
documented(router.post('/services/check-updates', [SystemController, 'checkServiceUpdates']), {
|
||||
documented(router.post('/services/check-updates', [SystemController, 'checkServiceUpdates']).use(middleware.admin()), {
|
||||
summary: 'Check for service updates',
|
||||
tags: ['system'],
|
||||
})
|
||||
documented(router.get('/services/preflight', [SystemController, 'preflightCheck']), {
|
||||
documented(router.get('/services/preflight', [SystemController, 'preflightCheck']).use(middleware.admin()), {
|
||||
summary: 'Preflight a service install',
|
||||
tags: ['system'],
|
||||
query: preflightValidator,
|
||||
})
|
||||
documented(router.get('/services/suggest-port', [SystemController, 'suggestCustomPort']), {
|
||||
documented(router.get('/services/suggest-port', [SystemController, 'suggestCustomPort']).use(middleware.admin()), {
|
||||
summary: 'Suggest an available custom port',
|
||||
tags: ['system'],
|
||||
})
|
||||
documented(router.post('/services/preflight-custom', [SystemController, 'preflightCustomApp']), {
|
||||
documented(router.post('/services/preflight-custom', [SystemController, 'preflightCustomApp']).use(middleware.admin()), {
|
||||
summary: 'Preflight a custom app install',
|
||||
tags: ['system'],
|
||||
request: preflightCustomValidator,
|
||||
})
|
||||
documented(router.post('/services/custom', [SystemController, 'createCustomApp']), {
|
||||
documented(router.post('/services/custom', [SystemController, 'createCustomApp']).use(middleware.admin()), {
|
||||
summary: 'Create a custom app',
|
||||
tags: ['system'],
|
||||
request: customAppValidator,
|
||||
})
|
||||
documented(router.post('/services/existing', [SystemController, 'createExistingApp']), {
|
||||
documented(router.post('/services/existing', [SystemController, 'createExistingApp']).use(middleware.admin()), {
|
||||
summary: 'Add an existing Docker container as an app',
|
||||
tags: ['system'],
|
||||
request: existingAppValidator,
|
||||
})
|
||||
documented(router.put('/services/custom', [SystemController, 'updateCustomApp']), {
|
||||
documented(router.put('/services/custom', [SystemController, 'updateCustomApp']).use(middleware.admin()), {
|
||||
summary: 'Update a custom app',
|
||||
tags: ['system'],
|
||||
request: updateCustomAppValidator,
|
||||
})
|
||||
documented(router.post('/services/custom/update', [SystemController, 'updateCustomApp_pullLatest']), {
|
||||
documented(router.post('/services/custom/update', [SystemController, 'updateCustomApp_pullLatest']).use(middleware.admin()), {
|
||||
summary: 'Pull the latest version of a custom app',
|
||||
tags: ['system'],
|
||||
request: installServiceValidator,
|
||||
})
|
||||
documented(router.delete('/services/custom', [SystemController, 'deleteCustomApp']), {
|
||||
documented(router.delete('/services/custom', [SystemController, 'deleteCustomApp']).use(middleware.admin()), {
|
||||
summary: 'Delete a custom app',
|
||||
tags: ['system'],
|
||||
request: deleteCustomAppValidator,
|
||||
})
|
||||
documented(router.get('/services/custom/:name', [SystemController, 'getCustomApp']), {
|
||||
documented(router.get('/services/custom/:name', [SystemController, 'getCustomApp']).use(middleware.admin()), {
|
||||
summary: 'Get a custom app',
|
||||
tags: ['system'],
|
||||
})
|
||||
documented(router.put('/services/custom-url', [SystemController, 'setServiceCustomUrl']), {
|
||||
documented(router.put('/services/custom-url', [SystemController, 'setServiceCustomUrl']).use(middleware.admin()), {
|
||||
summary: 'Set a service custom URL',
|
||||
tags: ['system'],
|
||||
request: setServiceCustomUrlValidator,
|
||||
})
|
||||
documented(router.get('/services/:name/logs', [SystemController, 'getServiceLogs']), {
|
||||
documented(router.get('/services/:name/logs', [SystemController, 'getServiceLogs']).use(middleware.admin()), {
|
||||
summary: 'Get service logs',
|
||||
tags: ['system'],
|
||||
query: serviceLogsValidator,
|
||||
})
|
||||
documented(router.get('/services/:name/stats', [SystemController, 'getServiceStats']), {
|
||||
documented(router.get('/services/:name/stats', [SystemController, 'getServiceStats']).use(middleware.admin()), {
|
||||
summary: 'Get service stats',
|
||||
tags: ['system'],
|
||||
})
|
||||
documented(router.get('/services/:name/available-versions', [SystemController, 'getAvailableVersions']), {
|
||||
documented(router.get('/services/:name/available-versions', [SystemController, 'getAvailableVersions']).use(middleware.admin()), {
|
||||
summary: 'List available service versions',
|
||||
tags: ['system'],
|
||||
})
|
||||
documented(router.post('/services/update', [SystemController, 'updateService']), {
|
||||
documented(router.post('/services/update', [SystemController, 'updateService']).use(middleware.admin()), {
|
||||
summary: 'Update a service',
|
||||
tags: ['system'],
|
||||
request: updateServiceValidator,
|
||||
})
|
||||
documented(router.post('/services/auto-update', [SystemController, 'setServiceAutoUpdate']), {
|
||||
documented(router.post('/services/auto-update', [SystemController, 'setServiceAutoUpdate']).use(middleware.admin()), {
|
||||
summary: 'Set service auto-update',
|
||||
tags: ['system'],
|
||||
request: setServiceAutoUpdateValidator,
|
||||
})
|
||||
documented(router.get('/apps/auto-update/status', [SystemController, 'getAppAutoUpdateStatus']), {
|
||||
documented(router.get('/apps/auto-update/status', [SystemController, 'getAppAutoUpdateStatus']).use(middleware.admin()), {
|
||||
summary: 'Get app auto-update status',
|
||||
tags: ['system'],
|
||||
})
|
||||
documented(router.get('/content/auto-update/status', [SystemController, 'getContentAutoUpdateStatus']), {
|
||||
documented(router.get('/content/auto-update/status', [SystemController, 'getContentAutoUpdateStatus']).use(middleware.admin()), {
|
||||
summary: 'Get content auto-update status',
|
||||
tags: ['system'],
|
||||
})
|
||||
documented(router.post('/subscribe-release-notes', [SystemController, 'subscribeToReleaseNotes']), {
|
||||
documented(router.post('/subscribe-release-notes', [SystemController, 'subscribeToReleaseNotes']).use(middleware.admin()), {
|
||||
summary: 'Subscribe to release notes',
|
||||
tags: ['system'],
|
||||
request: subscribeToReleaseNotesValidator,
|
||||
|
|
@ -647,19 +658,19 @@ router
|
|||
tags: ['system'],
|
||||
query: checkLatestVersionValidator,
|
||||
})
|
||||
documented(router.post('/update', [SystemController, 'requestSystemUpdate']), {
|
||||
documented(router.post('/update', [SystemController, 'requestSystemUpdate']).use(middleware.admin()), {
|
||||
summary: 'Request a system update',
|
||||
tags: ['system'],
|
||||
})
|
||||
documented(router.get('/update/status', [SystemController, 'getSystemUpdateStatus']), {
|
||||
documented(router.get('/update/status', [SystemController, 'getSystemUpdateStatus']).use(middleware.admin()), {
|
||||
summary: 'Get system update status',
|
||||
tags: ['system'],
|
||||
})
|
||||
documented(router.get('/update/logs', [SystemController, 'getSystemUpdateLogs']), {
|
||||
documented(router.get('/update/logs', [SystemController, 'getSystemUpdateLogs']).use(middleware.admin()), {
|
||||
summary: 'Get system update logs',
|
||||
tags: ['system'],
|
||||
})
|
||||
documented(router.get('/auto-update/status', [SystemController, 'getAutoUpdateStatus']), {
|
||||
documented(router.get('/auto-update/status', [SystemController, 'getAutoUpdateStatus']).use(middleware.admin()), {
|
||||
summary: 'Get system auto-update status',
|
||||
tags: ['system'],
|
||||
})
|
||||
|
|
@ -691,18 +702,18 @@ router
|
|||
summary: 'List curated ZIM categories',
|
||||
tags: ['zim'],
|
||||
})
|
||||
documented(router.post('/download-remote', [ZimController, 'downloadRemote']), {
|
||||
documented(router.post('/download-remote', [ZimController, 'downloadRemote']).use(middleware.admin()), {
|
||||
summary: 'Queue a remote ZIM download',
|
||||
tags: ['zim'],
|
||||
request: remoteDownloadWithMetadataValidator,
|
||||
})
|
||||
documented(router.post('/download-category-tier', [ZimController, 'downloadCategoryTier']), {
|
||||
documented(router.post('/download-category-tier', [ZimController, 'downloadCategoryTier']).use(middleware.admin()), {
|
||||
summary: 'Download a ZIM category tier',
|
||||
tags: ['zim'],
|
||||
request: downloadCategoryTierValidator,
|
||||
})
|
||||
|
||||
documented(router.post('/upload', [ZimController, 'upload']), {
|
||||
documented(router.post('/upload', [ZimController, 'upload']).use(middleware.admin()), {
|
||||
summary: 'Upload a ZIM file',
|
||||
tags: ['zim'],
|
||||
})
|
||||
|
|
@ -710,7 +721,7 @@ router
|
|||
summary: 'Get Wikipedia ZIM state',
|
||||
tags: ['zim'],
|
||||
})
|
||||
documented(router.post('/wikipedia/select', [ZimController, 'selectWikipedia']), {
|
||||
documented(router.post('/wikipedia/select', [ZimController, 'selectWikipedia']).use(middleware.admin()), {
|
||||
summary: 'Select a Wikipedia ZIM edition',
|
||||
tags: ['zim'],
|
||||
request: selectWikipediaValidator,
|
||||
|
|
@ -720,12 +731,12 @@ router
|
|||
summary: 'List custom ZIM libraries',
|
||||
tags: ['zim'],
|
||||
})
|
||||
documented(router.post('/custom-libraries', [ZimController, 'addCustomLibrary']), {
|
||||
documented(router.post('/custom-libraries', [ZimController, 'addCustomLibrary']).use(middleware.admin()), {
|
||||
summary: 'Add a custom ZIM library',
|
||||
tags: ['zim'],
|
||||
request: addCustomLibraryValidator,
|
||||
})
|
||||
documented(router.delete('/custom-libraries/:id', [ZimController, 'removeCustomLibrary']), {
|
||||
documented(router.delete('/custom-libraries/:id', [ZimController, 'removeCustomLibrary']).use(middleware.admin()), {
|
||||
summary: 'Remove a custom ZIM library',
|
||||
tags: ['zim'],
|
||||
params: idParamValidator,
|
||||
|
|
@ -736,12 +747,12 @@ router
|
|||
query: browseLibraryValidator,
|
||||
})
|
||||
|
||||
documented(router.post('/rescan-library', [ZimController, 'rescanLibrary']), {
|
||||
documented(router.post('/rescan-library', [ZimController, 'rescanLibrary']).use(middleware.admin()), {
|
||||
summary: 'Rescan the ZIM library',
|
||||
tags: ['zim'],
|
||||
})
|
||||
|
||||
documented(router.delete('/:filename', [ZimController, 'delete']), {
|
||||
documented(router.delete('/:filename', [ZimController, 'delete']).use(middleware.admin()), {
|
||||
summary: 'Delete a ZIM file',
|
||||
tags: ['zim'],
|
||||
params: filenameParamValidator,
|
||||
|
|
@ -755,11 +766,11 @@ router
|
|||
summary: 'List creator packs',
|
||||
tags: ['creator-packs'],
|
||||
})
|
||||
documented(router.post('/:id/install', [CreatorPacksController, 'install']), {
|
||||
documented(router.post('/:id/install', [CreatorPacksController, 'install']).use(middleware.admin()), {
|
||||
summary: 'Install a creator pack',
|
||||
tags: ['creator-packs'],
|
||||
})
|
||||
documented(router.delete('/:id', [CreatorPacksController, 'uninstall']), {
|
||||
documented(router.delete('/:id', [CreatorPacksController, 'uninstall']).use(middleware.admin()), {
|
||||
summary: 'Uninstall a creator pack',
|
||||
tags: ['creator-packs'],
|
||||
})
|
||||
|
|
|
|||
|
|
@ -24,6 +24,12 @@ export type SystemInformationResponse = {
|
|||
export type UsePageProps = {
|
||||
appVersion: string
|
||||
environment: string
|
||||
admin: {
|
||||
isConfigured: boolean
|
||||
isLoggedIn: boolean
|
||||
user: string
|
||||
}
|
||||
aiAssistantName: string
|
||||
}
|
||||
|
||||
export type LSBlockDevice = {
|
||||
|
|
@ -160,4 +166,4 @@ export type ContentAutoUpdateStatus = {
|
|||
lastError: string | null
|
||||
autoDisabledReason: string | null
|
||||
resources: ContentAutoUpdateResourceStatus[]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -420,6 +420,7 @@ download_management_compose_file() {
|
|||
local app_key=$(generateRandomPass)
|
||||
local db_root_password=$(generateRandomPass)
|
||||
local db_user_password=$(generateRandomPass)
|
||||
admin_password=$(generateRandomPass)
|
||||
|
||||
# If MySQL data directory exists from a previous install attempt, remove it.
|
||||
# MySQL only initializes credentials on first startup when the data dir is empty.
|
||||
|
|
@ -434,6 +435,7 @@ download_management_compose_file() {
|
|||
echo -e "${YELLOW}#${RESET} Configuring docker-compose file env variables...\\n"
|
||||
sed -i "s|URL=replaceme|URL=http://${local_ip_address}:8080|g" "$compose_file_path"
|
||||
sed -i "s|APP_KEY=replaceme|APP_KEY=${app_key}|g" "$compose_file_path"
|
||||
sed -i "s|ADMIN_PASS=replaceme|ADMIN_PASS=${admin_password}|g" "$compose_file_path"
|
||||
|
||||
sed -i "s|DB_PASSWORD=replaceme|DB_PASSWORD=${db_user_password}|g" "$compose_file_path"
|
||||
sed -i "s|MYSQL_ROOT_PASSWORD=replaceme|MYSQL_ROOT_PASSWORD=${db_root_password}|g" "$compose_file_path"
|
||||
|
|
@ -605,6 +607,8 @@ success_message() {
|
|||
echo -e "${GREEN}#${RESET} Installation files are located at /opt/project-nomad\\n\n"
|
||||
echo -e "${GREEN}#${RESET} Project NOMAD's Command Center should automatically start whenever your device reboots. However, if you need to start it manually, you can always do so by running: ${WHITE_R}${NOMAD_DIR}/start_nomad.sh${RESET}\\n"
|
||||
echo -e "${GREEN}#${RESET} You can now access the management interface at http://localhost:8080 or http://${local_ip_address}:8080\\n"
|
||||
echo -e "${GREEN}#${RESET} Admin Login user: ${WHITE_R}admin${RESET}"
|
||||
echo -e "${GREEN}#${RESET} Admin Login password: ${WHITE_R}${admin_password}${RESET}\\n"
|
||||
echo -e "${GREEN}#${RESET} Thank you for supporting Project NOMAD!\\n"
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ services:
|
|||
- nomad-update-shared:/app/update-shared # Shared volume for update communication
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
- SESSION_DRIVER=cookie
|
||||
# NOMAD_STORAGE_PATH should equal the host path of the /app/storage volume above. The admin
|
||||
# normally auto-detects that mount, so this is a fallback used only if the container can't be
|
||||
# inspected. Keep it in sync anyway so the fallback never sends child apps to the wrong place.
|
||||
|
|
@ -43,6 +44,9 @@ services:
|
|||
- LOG_LEVEL=info
|
||||
# APP_KEY needs to be at least 16 chars or will fail validation and container won't start!
|
||||
- APP_KEY=1q2w3e4r5t6y7u8i9o0p
|
||||
# Admin Login credentials. Change ADMIN_PASS before exposing NOMAD beyond a trusted machine.
|
||||
- ADMIN_USER=admin
|
||||
- ADMIN_PASS=replaceme
|
||||
# # Leave HOST as is so the admin server listens all interfaces within the container - this doesn't affect how you access it from the host, it's just for internal container networking
|
||||
- HOST=0.0.0.0
|
||||
# URL should be set to the URL you will access the admin interface at (e.g. http://localhost:8080 or http://192.168.1.x:8080)
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@
|
|||
name: project-nomad
|
||||
services:
|
||||
admin:
|
||||
image: nomad:1.0
|
||||
image: nomad:1.1
|
||||
container_name: nomad_admin
|
||||
restart: unless-stopped
|
||||
extra_hosts:
|
||||
|
|
@ -33,6 +33,7 @@ services:
|
|||
- nomad-update-shared:/app/update-shared # Shared volume for update communication
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
- SESSION_DRIVER=cookie
|
||||
# NOMAD_STORAGE_PATH should equal the host path of the /app/storage volume above. The admin
|
||||
# normally auto-detects that mount, so this is a fallback used only if the container can't be
|
||||
# inspected. Keep it in sync anyway so the fallback never sends child apps to the wrong place.
|
||||
|
|
@ -42,6 +43,9 @@ services:
|
|||
- LOG_LEVEL=info
|
||||
# APP_KEY needs to be at least 16 chars or will fail validation and container won't start!
|
||||
- APP_KEY=1q2w3e4r5t6y7u8i9o0p
|
||||
# Admin Login credentials. Change ADMIN_PASS before exposing NOMAD beyond a trusted machine.
|
||||
- ADMIN_USER=admin
|
||||
- ADMIN_PASS=1q2w3e4r5t6y7u8i9o0p
|
||||
# # Leave HOST as is so the admin server listens all interfaces within the container - this doesn't affect how you access it from the host, it's just for internal container networking
|
||||
- HOST=0.0.0.0
|
||||
# URL should be set to the URL you will access the admin interface at (e.g. http://localhost:8080 or http://192.168.1.x:8080)
|
||||
|
|
|
|||
Loading…
Reference in New Issue