From 5a61c1c6ff4e22c9d82c1a1d50b349d2bfe255fd Mon Sep 17 00:00:00 2001 From: reaper Date: Sat, 22 Aug 2026 04:26:37 -0500 Subject: [PATCH] feat: add native Adminer database access --- docs/AURORA_NATIVE_RUNTIME.md | 4 +- runtime-build/linux-x64/Dockerfile | 4 +- runtime-build/linux-x64/stage-runtime.sh | 3 ++ scripts/smoke-native-runtime.cjs | 9 ++++ src/main/auroraEngine.ts | 15 +++--- src/main/native/nativeProject.test.ts | 20 ++++++++ src/main/native/nativeProject.ts | 41 ++++++++++++++++ src/main/nativeRuntime.ts | 5 +- src/main/runtimeCatalog.ts | 49 +++++++++++++++---- .../components/create/CreateProjectModal.tsx | 4 +- src/shared/types.ts | 35 +++++++++++-- 11 files changed, 163 insertions(+), 26 deletions(-) diff --git a/docs/AURORA_NATIVE_RUNTIME.md b/docs/AURORA_NATIVE_RUNTIME.md index 58247b4..4795651 100644 --- a/docs/AURORA_NATIVE_RUNTIME.md +++ b/docs/AURORA_NATIVE_RUNTIME.md @@ -12,7 +12,7 @@ Each signed runtime bundle contains a `runtime.json` manifest plus versioned exe Runtime archives are created from a staging directory with `npm run build:native-runtime -- `. The packager resolves every executable inside the staging root, calculates its SHA-256 checksum, writes the immutable `runtime.json`, excludes the build template, and creates the distributable archive. Dockside independently verifies those checksums before declaring a runtime available. -The first reproducible bundle target is Linux x64. Run `npm run build:native-linux-x64` on a Docker-capable build machine. Docker is used only to create the portable artifact; users of that artifact do not need Docker. The recipe pins PHP 8.5.9, nginx 1.30.4, MariaDB 11.8.8, and WP-CLI 2.12.0 with their runtime libraries, runs executable, PHP-extension, and database-initialization smoke checks outside the build container, and then invokes the normal checksum packager. +The first reproducible bundle target is Linux x64. Run `npm run build:native-linux-x64` on a Docker-capable build machine. Docker is used only to create the portable artifact; users of that artifact do not need Docker. The recipe pins PHP 8.5.9, nginx 1.30.4, MariaDB 11.8.8, WP-CLI 2.12.0, and Adminer 6.0.1 with their runtime libraries, runs executable, PHP-extension, database-initialization, and PHP syntax smoke checks outside the build container, and then invokes the normal checksum packager. Dockside can install a bundled runtime or a user-selected runtime archive from Settings. Installation rejects absolute and parent-traversing archive entries, rejects symbolic links, verifies the target platform and architecture, and verifies every declared executable checksum before atomically replacing an older runtime. Electron packages include matching archives from `dist/native-runtime` when they are present at packaging time. @@ -27,7 +27,7 @@ Every project receives reserved loopback ports, generated service configuration, 3. Linux x64 bundle with PHP 8.5, nginx, MariaDB 11.8, and WP-CLI. 4. Native project lifecycle and single-site WordPress provisioning. 5. Native service logs, compressed snapshots, and database import/export. -6. Native database administration. +6. Native database administration with loopback-only automatic login. 7. macOS arm64/x64 and Windows x64 bundles. 8. Additional PHP/database versions, Apache, Drupal, Node.js, and developer services. diff --git a/runtime-build/linux-x64/Dockerfile b/runtime-build/linux-x64/Dockerfile index c7d4188..02a4d25 100644 --- a/runtime-build/linux-x64/Dockerfile +++ b/runtime-build/linux-x64/Dockerfile @@ -4,7 +4,9 @@ RUN apk add --no-cache --repository=https://dl-cdn.alpinelinux.org/alpine/edge/m nginx=1.30.4-r3 mariadb=11.8.8-r0 mariadb-client=11.8.8-r0 bash file pax-utils \ && mkdir -p /stage/root /stage/bin /stage/lib \ && curl -fsSLo /tmp/wp-cli.phar https://github.com/wp-cli/wp-cli/releases/download/v2.12.0/wp-cli-2.12.0.phar \ - && echo 'ce34ddd838f7351d6759068d09793f26755463b4a4610a5a5c0a97b68220d85c /tmp/wp-cli.phar' | sha256sum -c - + && echo 'ce34ddd838f7351d6759068d09793f26755463b4a4610a5a5c0a97b68220d85c /tmp/wp-cli.phar' | sha256sum -c - \ + && curl -fsSLo /tmp/adminer.php https://github.com/vrana/adminer/releases/download/v6.0.1/adminer-6.0.1-mysql-en.php \ + && echo 'aa1964047a7ce6e44b26db14dc16a942820c54034b417e6223884df6fa8e24a9 /tmp/adminer.php' | sha256sum -c - RUN docker-php-ext-install -j2 mysqli pdo_mysql diff --git a/runtime-build/linux-x64/stage-runtime.sh b/runtime-build/linux-x64/stage-runtime.sh index 985df54..79e2c8a 100644 --- a/runtime-build/linux-x64/stage-runtime.sh +++ b/runtime-build/linux-x64/stage-runtime.sh @@ -114,6 +114,8 @@ EOF done cp /tmp/wp-cli.phar "$root/usr/local/bin/wp-cli.phar" +mkdir -p "$root/usr/share/aurora" +cp /tmp/adminer.php "$root/usr/share/aurora/adminer.php" cat > "$stage/bin/wp" <<'EOF' #!/bin/sh runtime_root=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) @@ -136,6 +138,7 @@ cat > "$stage/runtime.template.json" < { @@ -532,10 +534,11 @@ export async function describeProject(name: string): Promise { expect(config).toContain('port=41003') expect(config).toContain('/.aurora/native/data/mariadb') }) + it('creates a loopback Adminer endpoint with project database credentials', () => { + expect(renderNginxConfig(project)).toContain('location = /__aurora/adminer/') + const bootstrap = renderNativeAdminerBootstrap({ + ...project, + installedRuntimeRoot: '/tmp/aurora-runtime' + }) + expect(bootstrap).toContain("value = '127.0.0.1:41003'") + expect(bootstrap).toContain("username.value = 'db'") + expect(bootstrap).toContain('/tmp/aurora-runtime/root/usr/share/aurora/adminer.php') + }) }) it.runIf(Boolean(process.env.AURORA_NATIVE_SMOKE_ROOT))( @@ -170,6 +181,15 @@ it.runIf(Boolean(process.env.AURORA_NATIVE_WORDPRESS_SMOKE_ROOT))( { cwd: root, env: process.env } ) expect(restored.stdout.trim()).toBe('Aurora native smoke') + + const adminerResponse = await fetch( + `http://127.0.0.1:${definition.ports.http}/__aurora/adminer/` + ) + const adminerHtml = await adminerResponse.text() + expect(adminerResponse.ok).toBe(true) + expect(adminerHtml).toContain('Adminer') + expect(adminerHtml).toContain("username.value = 'db'") + expect(adminerHtml).toContain("value = '127.0.0.1:") } finally { await stopNativeProject(definition) } diff --git a/src/main/native/nativeProject.ts b/src/main/native/nativeProject.ts index 040e3e6..d4453e0 100644 --- a/src/main/native/nativeProject.ts +++ b/src/main/native/nativeProject.ts @@ -68,6 +68,22 @@ http { server_name localhost; root "${quoteNginx(webroot)}"; index index.php index.html; + location = /__aurora/adminer/ { + fastcgi_pass 127.0.0.1:${project.ports.php}; + fastcgi_param SCRIPT_FILENAME "${quoteNginx(join(directory, 'adminer.php'))}"; + fastcgi_param SCRIPT_NAME /__aurora/adminer/; + fastcgi_param REQUEST_METHOD $request_method; + fastcgi_param REQUEST_URI $request_uri; + fastcgi_param QUERY_STRING $query_string; + fastcgi_param CONTENT_TYPE $content_type; + fastcgi_param CONTENT_LENGTH $content_length; + fastcgi_param SERVER_PROTOCOL $server_protocol; + fastcgi_param SERVER_NAME $server_name; + fastcgi_param SERVER_PORT $server_port; + fastcgi_param HTTP_HOST $http_host; + fastcgi_param REMOTE_ADDR $remote_addr; + fastcgi_param REMOTE_PORT $remote_port; + } location / { try_files $uri $uri/ /index.php?$query_string; } location ~ \\.php$ { fastcgi_pass 127.0.0.1:${project.ports.php}; @@ -92,6 +108,30 @@ http { ` } +export function renderNativeAdminerBootstrap(project: NativeProjectDefinition): string { + const adminer = join(installedRoot(project), 'root', 'usr', 'share', 'aurora', 'adminer.php') + return ` + + - if (catalog.schema !== 1 || typeof catalog.generatedAt !== 'string' || !Array.isArray(catalog.releases)) + if ( + catalog.schema !== 1 || + typeof catalog.generatedAt !== 'string' || + !Array.isArray(catalog.releases) + ) throw new Error('Unsupported runtime catalog.') const releases = catalog.releases.map((entry) => { if (!entry || typeof entry !== 'object') throw new Error('Invalid runtime catalog release.') const release = entry as Record if ( - !['php', 'nginx', 'apache', 'mariadb', 'mysql', 'postgres', 'node', 'composer', 'wp-cli', 'drush'].includes(String(release.component)) || + ![ + 'php', + 'nginx', + 'apache', + 'mariadb', + 'mariadb-client', + 'mariadb-dump', + 'mysql', + 'postgres', + 'node', + 'composer', + 'wp-cli', + 'drush', + 'adminer' + ].includes(String(release.component)) || typeof release.version !== 'string' || !['linux', 'darwin', 'win32'].includes(String(release.platform)) || !['x64', 'arm64'].includes(String(release.arch)) || @@ -40,7 +59,8 @@ export function validateRuntimeCatalog(value: unknown): AuroraRuntimeCatalog { !release.url.startsWith('https://') || typeof release.sha256 !== 'string' || !/^[a-f0-9]{64}$/i.test(release.sha256) - ) throw new Error('Invalid runtime catalog release.') + ) + throw new Error('Invalid runtime catalog release.') return release as unknown as AuroraRuntimeCatalogRelease }) return { schema: 1, generatedAt: catalog.generatedAt, releases } @@ -53,12 +73,20 @@ export function findRuntimeUpdates( arch: string ): AuroraRuntimeUpdate[] { return installed.flatMap((component) => { - const candidates = catalog.releases.filter((release) => - release.component === component.id && release.platform === platform && release.arch === arch + const candidates = catalog.releases.filter( + (release) => + release.component === component.id && release.platform === platform && release.arch === arch ) const latest = candidates.sort((a, b) => compareVersions(b.version, a.version))[0] return latest && compareVersions(latest.version, component.version) > 0 - ? [{ component: component.id, installedVersion: component.version, availableVersion: latest.version, channel: latest.channel }] + ? [ + { + component: component.id, + installedVersion: component.version, + availableVersion: latest.version, + channel: latest.channel + } + ] : [] }) } @@ -76,7 +104,8 @@ async function remoteCatalog(): Promise { fetch(catalogUrl, { signal: controller.signal }), fetch(`${catalogUrl}.sig`, { signal: controller.signal }) ]) - if (!catalogResponse.ok || !signatureResponse.ok) throw new Error('Runtime catalog server is unavailable.') + if (!catalogResponse.ok || !signatureResponse.ok) + throw new Error('Runtime catalog server is unavailable.') const body = Buffer.from(await catalogResponse.arrayBuffer()) const signature = Buffer.from((await signatureResponse.text()).trim(), 'base64') if (!verify(null, body, createPublicKey(publicKeyPem), signature)) @@ -87,7 +116,9 @@ async function remoteCatalog(): Promise { } } -export async function getRuntimeUpdates(installed: AuroraNativeRuntimeComponent[]): Promise { +export async function getRuntimeUpdates( + installed: AuroraNativeRuntimeComponent[] +): Promise { const cacheDirectory = join(app.getPath('userData'), 'runtime-catalog') const cachePath = join(cacheDirectory, 'catalog.json') let source: AuroraRuntimeUpdateStatus['source'] = 'bundled' diff --git a/src/renderer/src/components/create/CreateProjectModal.tsx b/src/renderer/src/components/create/CreateProjectModal.tsx index 3eb2e27..c30ef58 100644 --- a/src/renderer/src/components/create/CreateProjectModal.tsx +++ b/src/renderer/src/components/create/CreateProjectModal.tsx @@ -79,7 +79,7 @@ export function CreateProjectModal({ onClose }: { onClose: () => void }): React. setWebServer('nginx') setDatabase('mariadb') setDatabaseVersion('11.8') - setAdminer(false) + setAdminer(true) setRedis(false) setMailpit(false) setXdebug(false) @@ -459,7 +459,7 @@ export function CreateProjectModal({ onClose }: { onClose: () => void }): React. > (setter as (v: boolean) => void)(e.target.checked)} /> diff --git a/src/shared/types.ts b/src/shared/types.ts index 741565e..1b2124f 100644 --- a/src/shared/types.ts +++ b/src/shared/types.ts @@ -2,7 +2,20 @@ export type ProjectStatus = 'running' | 'stopped' | 'paused' | 'starting' | 'sto export type AuroraRuntimeEngine = 'container' | 'native' export interface AuroraNativeRuntimeComponent { - id: 'php' | 'nginx' | 'apache' | 'mariadb' | 'mysql' | 'postgres' | 'node' | 'composer' | 'wp-cli' | 'drush' + id: + | 'php' + | 'nginx' + | 'apache' + | 'mariadb' + | 'mariadb-client' + | 'mariadb-dump' + | 'mysql' + | 'postgres' + | 'node' + | 'composer' + | 'wp-cli' + | 'drush' + | 'adminer' version: string executable: string sha256: string @@ -18,8 +31,20 @@ export interface AuroraNativeRuntimeManifest { export interface AuroraRuntimeStatus { selectedEngine: AuroraRuntimeEngine - container: { available: boolean; provider: 'docker' | 'podman' | null; version?: string; reason?: string } - native: { available: boolean; platform: string; arch: string; runtimeVersion?: string; components: AuroraNativeRuntimeComponent[]; reason?: string } + container: { + available: boolean + provider: 'docker' | 'podman' | null + version?: string + reason?: string + } + native: { + available: boolean + platform: string + arch: string + runtimeVersion?: string + components: AuroraNativeRuntimeComponent[] + reason?: string + } } export type AuroraRuntimeComponentId = AuroraNativeRuntimeComponent['id'] @@ -189,7 +214,6 @@ export interface AuroraSnapshot { Created: string } - export type AuroraModuleCategory = 'application' | 'service' | 'tool' export type AuroraModuleSettingType = 'boolean' | 'select' | 'text' | 'number' @@ -252,7 +276,8 @@ export interface AuroraModuleManifest { } } -export type AuroraModuleLifecycleHook = 'projectCreate' | 'projectStart' | 'projectRemove' | 'packageUninstall' | 'projectTool' +export type AuroraModuleLifecycleHook = + 'projectCreate' | 'projectStart' | 'projectRemove' | 'packageUninstall' | 'projectTool' export interface AuroraModuleCommand { command: string