refactor: migrate from better-sqlite3 to Prisma (#170)
* refactor: migrate from better-sqlite3 to Prisma - Install Prisma dependencies and initialize with SQLite - Create Prisma schema matching existing database structure - Replace database.js with Prisma-based database service - Update all API routes, tRPC routers, and WebSocket handler - Convert TypeScript types to match Prisma schema - Update build process to include Prisma migrations - Remove better-sqlite3 dependency All database operations now use Prisma while maintaining SQLite backend. * fix: flatten server data in installed scripts API responses - Transform Prisma nested server objects to flattened fields expected by frontend - Update getAllInstalledScripts, getInstalledScriptsByServer, and getInstalledScriptById - Server names should now display correctly in the installed scripts table - Use nullish coalescing operators for better null handling * fix: ensure DATABASE_URL is set in .env for Prisma during updates - Add ensure_database_url() function to update.sh - Function checks if .env exists and creates from .env.example if needed - Automatically adds DATABASE_URL if not present - Call function after restore_backup_files() in update flow - Fixes Prisma client generation error during updates
This commit is contained in:
committed by
GitHub
parent
6b45c41334
commit
b793c57000
3
.gitignore
vendored
3
.gitignore
vendored
@@ -49,4 +49,5 @@ yarn-error.log*
|
||||
*.tsbuildinfo
|
||||
|
||||
# idea files
|
||||
.idea
|
||||
.idea
|
||||
/generated/prisma
|
||||
|
||||
788
package-lock.json
generated
788
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -22,6 +22,7 @@
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@prisma/client": "^6.17.1",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
||||
"@radix-ui/react-slot": "^1.2.3",
|
||||
"@t3-oss/env-nextjs": "^0.13.8",
|
||||
@@ -36,7 +37,6 @@
|
||||
"@xterm/addon-web-links": "^0.11.0",
|
||||
"@xterm/xterm": "^5.5.0",
|
||||
"bcryptjs": "^3.0.2",
|
||||
"better-sqlite3": "^12.4.1",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
@@ -77,6 +77,7 @@
|
||||
"postcss": "^8.5.3",
|
||||
"prettier": "^3.5.3",
|
||||
"prettier-plugin-tailwindcss": "^0.7.0",
|
||||
"prisma": "^6.17.1",
|
||||
"tailwindcss": "^4.1.14",
|
||||
"typescript": "^5.8.2",
|
||||
"typescript-eslint": "^8.46.1",
|
||||
|
||||
45
prisma/schema.prisma
Normal file
45
prisma/schema.prisma
Normal file
@@ -0,0 +1,45 @@
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
}
|
||||
|
||||
datasource db {
|
||||
provider = "sqlite"
|
||||
url = env("DATABASE_URL")
|
||||
}
|
||||
|
||||
model InstalledScript {
|
||||
id Int @id @default(autoincrement())
|
||||
script_name String
|
||||
script_path String
|
||||
container_id String?
|
||||
server_id Int?
|
||||
execution_mode String
|
||||
installation_date DateTime? @default(now())
|
||||
status String
|
||||
output_log String?
|
||||
web_ui_ip String?
|
||||
web_ui_port Int?
|
||||
server Server? @relation(fields: [server_id], references: [id], onDelete: SetNull)
|
||||
|
||||
@@map("installed_scripts")
|
||||
}
|
||||
|
||||
model Server {
|
||||
id Int @id @default(autoincrement())
|
||||
name String @unique
|
||||
ip String
|
||||
user String
|
||||
password String?
|
||||
auth_type String? @default("password")
|
||||
ssh_key String?
|
||||
ssh_key_passphrase String?
|
||||
ssh_port Int? @default(22)
|
||||
color String?
|
||||
created_at DateTime? @default(now())
|
||||
updated_at DateTime? @updatedAt
|
||||
ssh_key_path String?
|
||||
key_generated Boolean? @default(false)
|
||||
installed_scripts InstalledScript[]
|
||||
|
||||
@@map("servers")
|
||||
}
|
||||
38
server.js
38
server.js
@@ -7,7 +7,7 @@ import { join, resolve } from 'path';
|
||||
import stripAnsi from 'strip-ansi';
|
||||
import { spawn as ptySpawn } from 'node-pty';
|
||||
import { getSSHExecutionService } from './src/server/ssh-execution-service.js';
|
||||
import { getDatabase } from './src/server/database.js';
|
||||
import { getDatabase } from './src/server/database-prisma.js';
|
||||
|
||||
const dev = process.env.NODE_ENV !== 'production';
|
||||
const hostname = '0.0.0.0';
|
||||
@@ -186,11 +186,11 @@ class ScriptExecutionHandler {
|
||||
* @param {string} scriptPath - Path to the script
|
||||
* @param {string} executionMode - 'local' or 'ssh'
|
||||
* @param {number|null} serverId - Server ID for SSH executions
|
||||
* @returns {number|null} - Installation record ID
|
||||
* @returns {Promise<number|null>} - Installation record ID
|
||||
*/
|
||||
createInstallationRecord(scriptName, scriptPath, executionMode, serverId = null) {
|
||||
async createInstallationRecord(scriptName, scriptPath, executionMode, serverId = null) {
|
||||
try {
|
||||
const result = this.db.createInstalledScript({
|
||||
const result = await this.db.createInstalledScript({
|
||||
script_name: scriptName,
|
||||
script_path: scriptPath,
|
||||
container_id: undefined,
|
||||
@@ -199,7 +199,7 @@ class ScriptExecutionHandler {
|
||||
status: 'in_progress',
|
||||
output_log: ''
|
||||
});
|
||||
return Number(result.lastInsertRowid);
|
||||
return Number(result.id);
|
||||
} catch (error) {
|
||||
console.error('Error creating installation record:', error);
|
||||
return null;
|
||||
@@ -211,9 +211,9 @@ class ScriptExecutionHandler {
|
||||
* @param {number} installationId - Installation record ID
|
||||
* @param {Object} updateData - Data to update
|
||||
*/
|
||||
updateInstallationRecord(installationId, updateData) {
|
||||
async updateInstallationRecord(installationId, updateData) {
|
||||
try {
|
||||
this.db.updateInstalledScript(installationId, updateData);
|
||||
await this.db.updateInstalledScript(installationId, updateData);
|
||||
} catch (error) {
|
||||
console.error('Error updating installation record:', error);
|
||||
}
|
||||
@@ -327,7 +327,7 @@ class ScriptExecutionHandler {
|
||||
|
||||
// Create installation record
|
||||
const serverId = server ? (server.id ?? null) : null;
|
||||
installationId = this.createInstallationRecord(scriptName, scriptPath, mode, serverId);
|
||||
installationId = await this.createInstallationRecord(scriptName, scriptPath, mode, serverId);
|
||||
|
||||
if (!installationId) {
|
||||
console.error('Failed to create installation record');
|
||||
@@ -356,7 +356,7 @@ class ScriptExecutionHandler {
|
||||
|
||||
// Update installation record with failure
|
||||
if (installationId) {
|
||||
this.updateInstallationRecord(installationId, { status: 'failed' });
|
||||
await this.updateInstallationRecord(installationId, { status: 'failed' });
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -394,7 +394,7 @@ class ScriptExecutionHandler {
|
||||
});
|
||||
|
||||
// Handle pty data (both stdout and stderr combined)
|
||||
childProcess.onData((data) => {
|
||||
childProcess.onData(async (data) => {
|
||||
const output = data.toString();
|
||||
|
||||
// Store output in buffer for logging
|
||||
@@ -410,7 +410,7 @@ class ScriptExecutionHandler {
|
||||
// Parse for Container ID
|
||||
const containerId = this.parseContainerId(output);
|
||||
if (containerId && installationId) {
|
||||
this.updateInstallationRecord(installationId, { container_id: containerId });
|
||||
await this.updateInstallationRecord(installationId, { container_id: containerId });
|
||||
}
|
||||
|
||||
// Parse for Web UI URL
|
||||
@@ -418,7 +418,7 @@ class ScriptExecutionHandler {
|
||||
if (webUIUrl && installationId) {
|
||||
const { ip, port } = webUIUrl;
|
||||
if (ip && port) {
|
||||
this.updateInstallationRecord(installationId, {
|
||||
await this.updateInstallationRecord(installationId, {
|
||||
web_ui_ip: ip,
|
||||
web_ui_port: port
|
||||
});
|
||||
@@ -464,7 +464,7 @@ class ScriptExecutionHandler {
|
||||
|
||||
// Update installation record with failure
|
||||
if (installationId) {
|
||||
this.updateInstallationRecord(installationId, { status: 'failed' });
|
||||
await this.updateInstallationRecord(installationId, { status: 'failed' });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -491,7 +491,7 @@ class ScriptExecutionHandler {
|
||||
const execution = /** @type {ExecutionResult} */ (await sshService.executeScript(
|
||||
server,
|
||||
scriptPath,
|
||||
/** @param {string} data */ (data) => {
|
||||
/** @param {string} data */ async (data) => {
|
||||
// Store output in buffer for logging
|
||||
const exec = this.activeExecutions.get(executionId);
|
||||
if (exec) {
|
||||
@@ -505,7 +505,7 @@ class ScriptExecutionHandler {
|
||||
// Parse for Container ID
|
||||
const containerId = this.parseContainerId(data);
|
||||
if (containerId && installationId) {
|
||||
this.updateInstallationRecord(installationId, { container_id: containerId });
|
||||
await this.updateInstallationRecord(installationId, { container_id: containerId });
|
||||
}
|
||||
|
||||
// Parse for Web UI URL
|
||||
@@ -513,7 +513,7 @@ class ScriptExecutionHandler {
|
||||
if (webUIUrl && installationId) {
|
||||
const { ip, port } = webUIUrl;
|
||||
if (ip && port) {
|
||||
this.updateInstallationRecord(installationId, {
|
||||
await this.updateInstallationRecord(installationId, {
|
||||
web_ui_ip: ip,
|
||||
web_ui_port: port
|
||||
});
|
||||
@@ -545,13 +545,13 @@ class ScriptExecutionHandler {
|
||||
timestamp: Date.now()
|
||||
});
|
||||
},
|
||||
/** @param {number} code */ (code) => {
|
||||
/** @param {number} code */ async (code) => {
|
||||
const exec = this.activeExecutions.get(executionId);
|
||||
const isSuccess = code === 0;
|
||||
|
||||
// Update installation record with final status and output
|
||||
if (installationId && exec) {
|
||||
this.updateInstallationRecord(installationId, {
|
||||
await this.updateInstallationRecord(installationId, {
|
||||
status: isSuccess ? 'success' : 'failed',
|
||||
output_log: exec.outputBuffer
|
||||
});
|
||||
@@ -586,7 +586,7 @@ class ScriptExecutionHandler {
|
||||
|
||||
// Update installation record with failure
|
||||
if (installationId) {
|
||||
this.updateInstallationRecord(installationId, { status: 'failed' });
|
||||
await this.updateInstallationRecord(installationId, { status: 'failed' });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -159,7 +159,7 @@ export function ServerForm({ onSubmit, initialData, isEditing = false, onCancel
|
||||
...prev,
|
||||
ssh_key: data.privateKey ?? '',
|
||||
ssh_key_path: keyPath,
|
||||
key_generated: 1
|
||||
key_generated: true
|
||||
}));
|
||||
setGeneratedPublicKey(data.publicKey ?? '');
|
||||
setGeneratedServerId(serverId);
|
||||
|
||||
@@ -192,8 +192,8 @@ export function ServerList({ servers, onUpdate, onDelete }: ServerListProps) {
|
||||
</span>
|
||||
</div>
|
||||
<div className="mt-1 text-xs text-muted-foreground">
|
||||
Created: {new Date(server.created_at).toLocaleDateString()}
|
||||
{server.updated_at !== server.created_at && (
|
||||
Created: {server.created_at ? new Date(server.created_at).toLocaleDateString() : 'Unknown'}
|
||||
{server.updated_at && server.updated_at !== server.created_at && (
|
||||
<span> • Updated: {new Date(server.updated_at).toLocaleDateString()}</span>
|
||||
)}
|
||||
</div>
|
||||
@@ -253,7 +253,7 @@ export function ServerList({ servers, onUpdate, onDelete }: ServerListProps) {
|
||||
</Button>
|
||||
<div className="flex space-x-2">
|
||||
{/* View Public Key button - only show for generated keys */}
|
||||
{server.key_generated === 1 && (
|
||||
{server.key_generated === true && (
|
||||
<Button
|
||||
onClick={() => handleViewPublicKey(server)}
|
||||
variant="outline"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { NextRequest } from 'next/server';
|
||||
import { NextResponse } from 'next/server';
|
||||
import { getDatabase } from '../../../../../server/database';
|
||||
import { getDatabase } from '../../../../../server/database-prisma.js';
|
||||
import { getSSHService } from '../../../../../server/ssh-service';
|
||||
|
||||
export async function GET(
|
||||
@@ -18,7 +18,7 @@ export async function GET(
|
||||
}
|
||||
|
||||
const db = getDatabase();
|
||||
const server = db.getServerById(id);
|
||||
const server = await db.getServerById(id);
|
||||
|
||||
if (!server) {
|
||||
return NextResponse.json(
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { NextRequest } from 'next/server';
|
||||
import { NextResponse } from 'next/server';
|
||||
import { getDatabase } from '../../../../server/database';
|
||||
import { getDatabase } from '../../../../server/database-prisma.js';
|
||||
import type { CreateServerData } from '../../../../types/server';
|
||||
|
||||
export async function GET(
|
||||
@@ -18,7 +18,7 @@ export async function GET(
|
||||
}
|
||||
|
||||
const db = getDatabase();
|
||||
const server = db.getServerById(id);
|
||||
const server = await db.getServerById(id);
|
||||
|
||||
if (!server) {
|
||||
return NextResponse.json(
|
||||
@@ -95,7 +95,7 @@ export async function PUT(
|
||||
const db = getDatabase();
|
||||
|
||||
// Check if server exists
|
||||
const existingServer = db.getServerById(id);
|
||||
const existingServer = await db.getServerById(id);
|
||||
if (!existingServer) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Server not found' },
|
||||
@@ -103,7 +103,7 @@ export async function PUT(
|
||||
);
|
||||
}
|
||||
|
||||
const result = db.updateServer(id, {
|
||||
await db.updateServer(id, {
|
||||
name,
|
||||
ip,
|
||||
user,
|
||||
@@ -113,14 +113,14 @@ export async function PUT(
|
||||
ssh_key_passphrase,
|
||||
ssh_port: ssh_port ?? 22,
|
||||
color,
|
||||
key_generated: key_generated ?? 0,
|
||||
key_generated: key_generated ?? false,
|
||||
ssh_key_path
|
||||
});
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
message: 'Server updated successfully',
|
||||
changes: result.changes
|
||||
changes: 1
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
@@ -158,7 +158,7 @@ export async function DELETE(
|
||||
const db = getDatabase();
|
||||
|
||||
// Check if server exists
|
||||
const existingServer = db.getServerById(id);
|
||||
const existingServer = await db.getServerById(id);
|
||||
if (!existingServer) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Server not found' },
|
||||
@@ -167,14 +167,14 @@ export async function DELETE(
|
||||
}
|
||||
|
||||
// Delete all installed scripts associated with this server
|
||||
db.deleteInstalledScriptsByServer(id);
|
||||
await db.deleteInstalledScriptsByServer(id);
|
||||
|
||||
const result = db.deleteServer(id);
|
||||
await db.deleteServer(id);
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
message: 'Server deleted successfully',
|
||||
changes: result.changes
|
||||
changes: 1
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { NextRequest } from 'next/server';
|
||||
import { NextResponse } from 'next/server';
|
||||
import { getDatabase } from '../../../../../server/database';
|
||||
import { getDatabase } from '../../../../../server/database-prisma.js';
|
||||
import { getSSHService } from '../../../../../server/ssh-service';
|
||||
import type { Server } from '../../../../../types/server';
|
||||
|
||||
@@ -19,7 +19,7 @@ export async function POST(
|
||||
}
|
||||
|
||||
const db = getDatabase();
|
||||
const server = db.getServerById(id) as Server;
|
||||
const server = await db.getServerById(id) as Server;
|
||||
|
||||
if (!server) {
|
||||
return NextResponse.json(
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { NextRequest } from 'next/server';
|
||||
import { NextResponse } from 'next/server';
|
||||
import { getSSHService } from '../../../../server/ssh-service';
|
||||
import { getDatabase } from '../../../../server/database';
|
||||
import { getDatabase } from '../../../../server/database-prisma.js';
|
||||
|
||||
export async function POST(_request: NextRequest) {
|
||||
try {
|
||||
@@ -9,7 +9,7 @@ export async function POST(_request: NextRequest) {
|
||||
const db = getDatabase();
|
||||
|
||||
// Get the next available server ID for key file naming
|
||||
const serverId = db.getNextServerId();
|
||||
const serverId = await db.getNextServerId();
|
||||
|
||||
const keyPair = await sshService.generateKeyPair(serverId);
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import type { NextRequest } from 'next/server';
|
||||
import { NextResponse } from 'next/server';
|
||||
import { getDatabase } from '../../../server/database';
|
||||
import { getDatabase } from '../../../server/database-prisma.js';
|
||||
import type { CreateServerData } from '../../../types/server';
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const db = getDatabase();
|
||||
const servers = db.getAllServers();
|
||||
const servers = await db.getAllServers();
|
||||
return NextResponse.json(servers);
|
||||
} catch (error) {
|
||||
console.error('Error fetching servers:', error);
|
||||
@@ -61,7 +61,7 @@ export async function POST(request: NextRequest) {
|
||||
|
||||
|
||||
const db = getDatabase();
|
||||
const result = db.createServer({
|
||||
const result = await db.createServer({
|
||||
name,
|
||||
ip,
|
||||
user,
|
||||
@@ -71,14 +71,14 @@ export async function POST(request: NextRequest) {
|
||||
ssh_key_passphrase,
|
||||
ssh_port: ssh_port ?? 22,
|
||||
color,
|
||||
key_generated: key_generated ?? 0,
|
||||
key_generated: key_generated ?? false,
|
||||
ssh_key_path
|
||||
});
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
message: 'Server created successfully',
|
||||
id: result.lastInsertRowid
|
||||
id: result.id
|
||||
},
|
||||
{ status: 201 }
|
||||
);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { z } from "zod";
|
||||
import { createTRPCRouter, publicProcedure } from "~/server/api/trpc";
|
||||
import { getDatabase } from "~/server/database";
|
||||
import { getDatabase } from "~/server/database-prisma.js";
|
||||
// Removed unused imports
|
||||
|
||||
|
||||
@@ -10,10 +10,26 @@ export const installedScriptsRouter = createTRPCRouter({
|
||||
.query(async () => {
|
||||
try {
|
||||
const db = getDatabase();
|
||||
const scripts = db.getAllInstalledScripts();
|
||||
const scripts = await db.getAllInstalledScripts();
|
||||
|
||||
// Transform scripts to flatten server data for frontend compatibility
|
||||
const transformedScripts = scripts.map(script => ({
|
||||
...script,
|
||||
server_name: script.server?.name ?? null,
|
||||
server_ip: script.server?.ip ?? null,
|
||||
server_user: script.server?.user ?? null,
|
||||
server_password: script.server?.password ?? null,
|
||||
server_auth_type: script.server?.auth_type ?? null,
|
||||
server_ssh_key: script.server?.ssh_key ?? null,
|
||||
server_ssh_key_passphrase: script.server?.ssh_key_passphrase ?? null,
|
||||
server_ssh_port: script.server?.ssh_port ?? null,
|
||||
server_color: script.server?.color ?? null,
|
||||
server: undefined // Remove nested server object
|
||||
}));
|
||||
|
||||
return {
|
||||
success: true,
|
||||
scripts
|
||||
scripts: transformedScripts
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error in getAllInstalledScripts:', error);
|
||||
@@ -31,10 +47,26 @@ export const installedScriptsRouter = createTRPCRouter({
|
||||
.query(async ({ input }) => {
|
||||
try {
|
||||
const db = getDatabase();
|
||||
const scripts = db.getInstalledScriptsByServer(input.serverId);
|
||||
const scripts = await db.getInstalledScriptsByServer(input.serverId);
|
||||
|
||||
// Transform scripts to flatten server data for frontend compatibility
|
||||
const transformedScripts = scripts.map(script => ({
|
||||
...script,
|
||||
server_name: script.server?.name ?? null,
|
||||
server_ip: script.server?.ip ?? null,
|
||||
server_user: script.server?.user ?? null,
|
||||
server_password: script.server?.password ?? null,
|
||||
server_auth_type: script.server?.auth_type ?? null,
|
||||
server_ssh_key: script.server?.ssh_key ?? null,
|
||||
server_ssh_key_passphrase: script.server?.ssh_key_passphrase ?? null,
|
||||
server_ssh_port: script.server?.ssh_port ?? null,
|
||||
server_color: script.server?.color ?? null,
|
||||
server: undefined // Remove nested server object
|
||||
}));
|
||||
|
||||
return {
|
||||
success: true,
|
||||
scripts
|
||||
scripts: transformedScripts
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error in getInstalledScriptsByServer:', error);
|
||||
@@ -52,7 +84,7 @@ export const installedScriptsRouter = createTRPCRouter({
|
||||
.query(async ({ input }) => {
|
||||
try {
|
||||
const db = getDatabase();
|
||||
const script = db.getInstalledScriptById(input.id);
|
||||
const script = await db.getInstalledScriptById(input.id);
|
||||
if (!script) {
|
||||
return {
|
||||
success: false,
|
||||
@@ -60,9 +92,24 @@ export const installedScriptsRouter = createTRPCRouter({
|
||||
script: null
|
||||
};
|
||||
}
|
||||
// Transform script to flatten server data for frontend compatibility
|
||||
const transformedScript = {
|
||||
...script,
|
||||
server_name: script.server?.name ?? null,
|
||||
server_ip: script.server?.ip ?? null,
|
||||
server_user: script.server?.user ?? null,
|
||||
server_password: script.server?.password ?? null,
|
||||
server_auth_type: script.server?.auth_type ?? null,
|
||||
server_ssh_key: script.server?.ssh_key ?? null,
|
||||
server_ssh_key_passphrase: script.server?.ssh_key_passphrase ?? null,
|
||||
server_ssh_port: script.server?.ssh_port ?? null,
|
||||
server_color: script.server?.color ?? null,
|
||||
server: undefined // Remove nested server object
|
||||
};
|
||||
|
||||
return {
|
||||
success: true,
|
||||
script
|
||||
script: transformedScript
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error in getInstalledScriptById:', error);
|
||||
@@ -90,10 +137,10 @@ export const installedScriptsRouter = createTRPCRouter({
|
||||
.mutation(async ({ input }) => {
|
||||
try {
|
||||
const db = getDatabase();
|
||||
const result = db.createInstalledScript(input);
|
||||
const result = await db.createInstalledScript(input);
|
||||
return {
|
||||
success: true,
|
||||
id: result.lastInsertRowid,
|
||||
id: result.id,
|
||||
message: 'Installed script record created successfully'
|
||||
};
|
||||
} catch (error) {
|
||||
@@ -120,9 +167,9 @@ export const installedScriptsRouter = createTRPCRouter({
|
||||
try {
|
||||
const { id, ...updateData } = input;
|
||||
const db = getDatabase();
|
||||
const result = db.updateInstalledScript(id, updateData);
|
||||
const result = await db.updateInstalledScript(id, updateData);
|
||||
|
||||
if (result.changes === 0) {
|
||||
if (!result) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'No changes made or script not found'
|
||||
@@ -148,9 +195,9 @@ export const installedScriptsRouter = createTRPCRouter({
|
||||
.mutation(async ({ input }) => {
|
||||
try {
|
||||
const db = getDatabase();
|
||||
const result = db.deleteInstalledScript(input.id);
|
||||
const result = await db.deleteInstalledScript(input.id);
|
||||
|
||||
if (result.changes === 0) {
|
||||
if (!result) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Script not found or already deleted'
|
||||
@@ -175,7 +222,7 @@ export const installedScriptsRouter = createTRPCRouter({
|
||||
.query(async () => {
|
||||
try {
|
||||
const db = getDatabase();
|
||||
const allScripts = db.getAllInstalledScripts();
|
||||
const allScripts = await db.getAllInstalledScripts();
|
||||
|
||||
const stats = {
|
||||
total: allScripts.length,
|
||||
@@ -219,7 +266,7 @@ export const installedScriptsRouter = createTRPCRouter({
|
||||
try {
|
||||
|
||||
const db = getDatabase();
|
||||
const server = db.getServerById(input.serverId);
|
||||
const server = await db.getServerById(input.serverId);
|
||||
|
||||
if (!server) {
|
||||
console.error('Server not found for ID:', input.serverId);
|
||||
@@ -350,7 +397,7 @@ export const installedScriptsRouter = createTRPCRouter({
|
||||
|
||||
|
||||
// Get existing scripts to check for duplicates
|
||||
const existingScripts = db.getAllInstalledScripts();
|
||||
const existingScripts = await db.getAllInstalledScripts();
|
||||
|
||||
// Create installed script records for detected containers (skip duplicates)
|
||||
const createdScripts = [];
|
||||
@@ -373,7 +420,7 @@ export const installedScriptsRouter = createTRPCRouter({
|
||||
continue;
|
||||
}
|
||||
|
||||
const result = db.createInstalledScript({
|
||||
const result = await db.createInstalledScript({
|
||||
script_name: container.hostname,
|
||||
script_path: `detected/${container.hostname}`,
|
||||
container_id: container.containerId,
|
||||
@@ -384,7 +431,7 @@ export const installedScriptsRouter = createTRPCRouter({
|
||||
});
|
||||
|
||||
createdScripts.push({
|
||||
id: result.lastInsertRowid,
|
||||
id: result.id,
|
||||
containerId: container.containerId,
|
||||
hostname: container.hostname,
|
||||
serverName: container.serverName
|
||||
@@ -420,8 +467,8 @@ export const installedScriptsRouter = createTRPCRouter({
|
||||
try {
|
||||
|
||||
const db = getDatabase();
|
||||
const allScripts = db.getAllInstalledScripts();
|
||||
const allServers = db.getAllServers();
|
||||
const allScripts = await db.getAllInstalledScripts();
|
||||
const allServers = await db.getAllServers();
|
||||
|
||||
|
||||
if (allScripts.length === 0) {
|
||||
@@ -452,7 +499,7 @@ export const installedScriptsRouter = createTRPCRouter({
|
||||
const scriptData = script as any;
|
||||
const server = allServers.find((s: any) => s.id === scriptData.server_id);
|
||||
if (!server) {
|
||||
db.deleteInstalledScript(Number(scriptData.id));
|
||||
await db.deleteInstalledScript(Number(scriptData.id));
|
||||
deletedScripts.push(String(scriptData.script_name));
|
||||
continue;
|
||||
}
|
||||
@@ -488,7 +535,7 @@ export const installedScriptsRouter = createTRPCRouter({
|
||||
});
|
||||
|
||||
if (!containerExists) {
|
||||
db.deleteInstalledScript(Number(scriptData.id));
|
||||
await db.deleteInstalledScript(Number(scriptData.id));
|
||||
deletedScripts.push(String(scriptData.script_name));
|
||||
} else {
|
||||
}
|
||||
@@ -525,7 +572,7 @@ export const installedScriptsRouter = createTRPCRouter({
|
||||
try {
|
||||
|
||||
const db = getDatabase();
|
||||
const allServers = db.getAllServers();
|
||||
const allServers = await db.getAllServers();
|
||||
const statusMap: Record<string, 'running' | 'stopped' | 'unknown'> = {};
|
||||
|
||||
// Import SSH services
|
||||
@@ -630,7 +677,7 @@ export const installedScriptsRouter = createTRPCRouter({
|
||||
.query(async ({ input }) => {
|
||||
try {
|
||||
const db = getDatabase();
|
||||
const script = db.getInstalledScriptById(input.id);
|
||||
const script = await db.getInstalledScriptById(input.id);
|
||||
|
||||
if (!script) {
|
||||
return {
|
||||
@@ -652,7 +699,7 @@ export const installedScriptsRouter = createTRPCRouter({
|
||||
}
|
||||
|
||||
// Get server info
|
||||
const server = db.getServerById(Number(scriptData.server_id));
|
||||
const server = await db.getServerById(Number(scriptData.server_id));
|
||||
if (!server) {
|
||||
return {
|
||||
success: false,
|
||||
@@ -732,7 +779,7 @@ export const installedScriptsRouter = createTRPCRouter({
|
||||
.mutation(async ({ input }) => {
|
||||
try {
|
||||
const db = getDatabase();
|
||||
const script = db.getInstalledScriptById(input.id);
|
||||
const script = await db.getInstalledScriptById(input.id);
|
||||
|
||||
if (!script) {
|
||||
return {
|
||||
@@ -752,7 +799,7 @@ export const installedScriptsRouter = createTRPCRouter({
|
||||
}
|
||||
|
||||
// Get server info
|
||||
const server = db.getServerById(Number(scriptData.server_id));
|
||||
const server = await db.getServerById(Number(scriptData.server_id));
|
||||
if (!server) {
|
||||
return {
|
||||
success: false,
|
||||
@@ -823,7 +870,7 @@ export const installedScriptsRouter = createTRPCRouter({
|
||||
.mutation(async ({ input }) => {
|
||||
try {
|
||||
const db = getDatabase();
|
||||
const script = db.getInstalledScriptById(input.id);
|
||||
const script = await db.getInstalledScriptById(input.id);
|
||||
|
||||
if (!script) {
|
||||
return {
|
||||
@@ -843,7 +890,7 @@ export const installedScriptsRouter = createTRPCRouter({
|
||||
}
|
||||
|
||||
// Get server info
|
||||
const server = db.getServerById(Number(scriptData.server_id));
|
||||
const server = await db.getServerById(Number(scriptData.server_id));
|
||||
if (!server) {
|
||||
return {
|
||||
success: false,
|
||||
@@ -950,9 +997,9 @@ export const installedScriptsRouter = createTRPCRouter({
|
||||
});
|
||||
|
||||
// If destroy was successful, delete the database record
|
||||
const deleteResult = db.deleteInstalledScript(input.id);
|
||||
const deleteResult = await db.deleteInstalledScript(input.id);
|
||||
|
||||
if (deleteResult.changes === 0) {
|
||||
if (!deleteResult) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Container destroyed but failed to delete database record'
|
||||
@@ -985,7 +1032,7 @@ export const installedScriptsRouter = createTRPCRouter({
|
||||
try {
|
||||
console.log('🔍 Auto-detect WebUI called with id:', input.id);
|
||||
const db = getDatabase();
|
||||
const script = db.getInstalledScriptById(input.id);
|
||||
const script = await db.getInstalledScriptById(input.id);
|
||||
|
||||
if (!script) {
|
||||
console.log('❌ Script not found for id:', input.id);
|
||||
@@ -1013,7 +1060,7 @@ export const installedScriptsRouter = createTRPCRouter({
|
||||
}
|
||||
|
||||
// Get server info
|
||||
const server = db.getServerById(Number(scriptData.server_id));
|
||||
const server = await db.getServerById(Number(scriptData.server_id));
|
||||
if (!server) {
|
||||
console.log('❌ Server not found for id:', scriptData.server_id);
|
||||
return {
|
||||
@@ -1121,12 +1168,12 @@ export const installedScriptsRouter = createTRPCRouter({
|
||||
|
||||
// Update the database with detected IP and port
|
||||
console.log('💾 Updating database with IP:', detectedIp, 'Port:', detectedPort);
|
||||
const updateResult = db.updateInstalledScript(input.id, {
|
||||
const updateResult = await db.updateInstalledScript(input.id, {
|
||||
web_ui_ip: detectedIp,
|
||||
web_ui_port: detectedPort
|
||||
});
|
||||
|
||||
if (updateResult.changes === 0) {
|
||||
if (!updateResult) {
|
||||
console.log('❌ Database update failed - no changes made');
|
||||
return {
|
||||
success: false,
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { z } from "zod";
|
||||
import { createTRPCRouter, publicProcedure } from "~/server/api/trpc";
|
||||
import { getDatabase } from "~/server/database";
|
||||
import { getDatabase } from "~/server/database-prisma.js";
|
||||
|
||||
export const serversRouter = createTRPCRouter({
|
||||
getAllServers: publicProcedure
|
||||
.query(async () => {
|
||||
try {
|
||||
const db = getDatabase();
|
||||
const servers = db.getAllServers();
|
||||
const servers = await db.getAllServers();
|
||||
return { success: true, servers };
|
||||
} catch (error) {
|
||||
console.error('Error fetching servers:', error);
|
||||
@@ -24,7 +24,7 @@ export const serversRouter = createTRPCRouter({
|
||||
.query(async ({ input }) => {
|
||||
try {
|
||||
const db = getDatabase();
|
||||
const server = db.getServerById(input.id);
|
||||
const server = await db.getServerById(input.id);
|
||||
if (!server) {
|
||||
return { success: false, error: 'Server not found', server: null };
|
||||
}
|
||||
|
||||
254
src/server/database-prisma.js
Normal file
254
src/server/database-prisma.js
Normal file
@@ -0,0 +1,254 @@
|
||||
import { prisma } from './db.js';
|
||||
import { join } from 'path';
|
||||
import { writeFileSync, unlinkSync, chmodSync, mkdirSync } from 'fs';
|
||||
import { existsSync } from 'fs';
|
||||
|
||||
class DatabaseServicePrisma {
|
||||
constructor() {
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
// Ensure data/ssh-keys directory exists
|
||||
const sshKeysDir = join(process.cwd(), 'data', 'ssh-keys');
|
||||
if (!existsSync(sshKeysDir)) {
|
||||
mkdirSync(sshKeysDir, { mode: 0o700 });
|
||||
}
|
||||
}
|
||||
|
||||
// Server CRUD operations
|
||||
async createServer(serverData) {
|
||||
const { name, ip, user, password, auth_type, ssh_key, ssh_key_passphrase, ssh_port, color, key_generated } = serverData;
|
||||
|
||||
let ssh_key_path = null;
|
||||
|
||||
// If using SSH key authentication, create persistent key file
|
||||
if (auth_type === 'key' && ssh_key) {
|
||||
const serverId = await this.getNextServerId();
|
||||
ssh_key_path = this.createSSHKeyFile(serverId, ssh_key);
|
||||
}
|
||||
|
||||
return await prisma.server.create({
|
||||
data: {
|
||||
name,
|
||||
ip,
|
||||
user,
|
||||
password,
|
||||
auth_type: auth_type ?? 'password',
|
||||
ssh_key,
|
||||
ssh_key_passphrase,
|
||||
ssh_port: ssh_port ?? 22,
|
||||
ssh_key_path,
|
||||
key_generated: Boolean(key_generated),
|
||||
color,
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async getAllServers() {
|
||||
return await prisma.server.findMany({
|
||||
orderBy: { created_at: 'desc' }
|
||||
});
|
||||
}
|
||||
|
||||
async getServerById(id) {
|
||||
return await prisma.server.findUnique({
|
||||
where: { id }
|
||||
});
|
||||
}
|
||||
|
||||
async updateServer(id, serverData) {
|
||||
const { name, ip, user, password, auth_type, ssh_key, ssh_key_passphrase, ssh_port, color, key_generated } = serverData;
|
||||
|
||||
// Get existing server to check for key changes
|
||||
const existingServer = await this.getServerById(id);
|
||||
let ssh_key_path = existingServer?.ssh_key_path;
|
||||
|
||||
// Handle SSH key changes
|
||||
if (auth_type === 'key' && ssh_key) {
|
||||
// Delete old key file if it exists
|
||||
if (existingServer?.ssh_key_path && existsSync(existingServer.ssh_key_path)) {
|
||||
try {
|
||||
unlinkSync(existingServer.ssh_key_path);
|
||||
// Also delete public key file if it exists
|
||||
const pubKeyPath = existingServer.ssh_key_path + '.pub';
|
||||
if (existsSync(pubKeyPath)) {
|
||||
unlinkSync(pubKeyPath);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Failed to delete old SSH key file:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Create new key file
|
||||
ssh_key_path = this.createSSHKeyFile(id, ssh_key);
|
||||
} else if (auth_type !== 'key') {
|
||||
// If switching away from key auth, delete key files
|
||||
if (existingServer?.ssh_key_path && existsSync(existingServer.ssh_key_path)) {
|
||||
try {
|
||||
unlinkSync(existingServer.ssh_key_path);
|
||||
const pubKeyPath = existingServer.ssh_key_path + '.pub';
|
||||
if (existsSync(pubKeyPath)) {
|
||||
unlinkSync(pubKeyPath);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Failed to delete SSH key file:', error);
|
||||
}
|
||||
}
|
||||
ssh_key_path = null;
|
||||
}
|
||||
|
||||
return await prisma.server.update({
|
||||
where: { id },
|
||||
data: {
|
||||
name,
|
||||
ip,
|
||||
user,
|
||||
password,
|
||||
auth_type: auth_type ?? 'password',
|
||||
ssh_key,
|
||||
ssh_key_passphrase,
|
||||
ssh_port: ssh_port ?? 22,
|
||||
ssh_key_path,
|
||||
key_generated: key_generated !== undefined ? Boolean(key_generated) : (existingServer?.key_generated ?? false),
|
||||
color,
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async deleteServer(id) {
|
||||
// Get server info before deletion to clean up key files
|
||||
const server = await this.getServerById(id);
|
||||
|
||||
// Delete SSH key files if they exist
|
||||
if (server?.ssh_key_path && existsSync(server.ssh_key_path)) {
|
||||
try {
|
||||
unlinkSync(server.ssh_key_path);
|
||||
const pubKeyPath = server.ssh_key_path + '.pub';
|
||||
if (existsSync(pubKeyPath)) {
|
||||
unlinkSync(pubKeyPath);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Failed to delete SSH key file:', error);
|
||||
}
|
||||
}
|
||||
|
||||
return await prisma.server.delete({
|
||||
where: { id }
|
||||
});
|
||||
}
|
||||
|
||||
// Installed Scripts CRUD operations
|
||||
async createInstalledScript(scriptData) {
|
||||
const { script_name, script_path, container_id, server_id, execution_mode, status, output_log, web_ui_ip, web_ui_port } = scriptData;
|
||||
|
||||
return await prisma.installedScript.create({
|
||||
data: {
|
||||
script_name,
|
||||
script_path,
|
||||
container_id: container_id ?? null,
|
||||
server_id: server_id ?? null,
|
||||
execution_mode,
|
||||
status,
|
||||
output_log: output_log ?? null,
|
||||
web_ui_ip: web_ui_ip ?? null,
|
||||
web_ui_port: web_ui_port ?? null,
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async getAllInstalledScripts() {
|
||||
return await prisma.installedScript.findMany({
|
||||
include: {
|
||||
server: true
|
||||
},
|
||||
orderBy: { installation_date: 'desc' }
|
||||
});
|
||||
}
|
||||
|
||||
async getInstalledScriptById(id) {
|
||||
return await prisma.installedScript.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
server: true
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async getInstalledScriptsByServer(server_id) {
|
||||
return await prisma.installedScript.findMany({
|
||||
where: { server_id },
|
||||
include: {
|
||||
server: true
|
||||
},
|
||||
orderBy: { installation_date: 'desc' }
|
||||
});
|
||||
}
|
||||
|
||||
async updateInstalledScript(id, updateData) {
|
||||
const { script_name, container_id, status, output_log, web_ui_ip, web_ui_port } = updateData;
|
||||
|
||||
const updateFields = {};
|
||||
if (script_name !== undefined) updateFields.script_name = script_name;
|
||||
if (container_id !== undefined) updateFields.container_id = container_id;
|
||||
if (status !== undefined) updateFields.status = status;
|
||||
if (output_log !== undefined) updateFields.output_log = output_log;
|
||||
if (web_ui_ip !== undefined) updateFields.web_ui_ip = web_ui_ip;
|
||||
if (web_ui_port !== undefined) updateFields.web_ui_port = web_ui_port;
|
||||
|
||||
if (Object.keys(updateFields).length === 0) {
|
||||
return { changes: 0 };
|
||||
}
|
||||
|
||||
return await prisma.installedScript.update({
|
||||
where: { id },
|
||||
data: updateFields
|
||||
});
|
||||
}
|
||||
|
||||
async deleteInstalledScript(id) {
|
||||
return await prisma.installedScript.delete({
|
||||
where: { id }
|
||||
});
|
||||
}
|
||||
|
||||
async deleteInstalledScriptsByServer(server_id) {
|
||||
return await prisma.installedScript.deleteMany({
|
||||
where: { server_id }
|
||||
});
|
||||
}
|
||||
|
||||
async getNextServerId() {
|
||||
const result = await prisma.server.findFirst({
|
||||
orderBy: { id: 'desc' },
|
||||
select: { id: true }
|
||||
});
|
||||
return (result?.id ?? 0) + 1;
|
||||
}
|
||||
|
||||
createSSHKeyFile(serverId, sshKey) {
|
||||
const sshKeysDir = join(process.cwd(), 'data', 'ssh-keys');
|
||||
const keyPath = join(sshKeysDir, `server_${serverId}_key`);
|
||||
|
||||
// Normalize the key: trim any trailing whitespace and ensure exactly one newline at the end
|
||||
const normalizedKey = sshKey.trimEnd() + '\n';
|
||||
writeFileSync(keyPath, normalizedKey);
|
||||
chmodSync(keyPath, 0o600); // Set proper permissions
|
||||
|
||||
return keyPath;
|
||||
}
|
||||
|
||||
async close() {
|
||||
await prisma.$disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
// Singleton instance
|
||||
let dbInstance = null;
|
||||
|
||||
export function getDatabase() {
|
||||
dbInstance ??= new DatabaseServicePrisma();
|
||||
return dbInstance;
|
||||
}
|
||||
|
||||
export default DatabaseServicePrisma;
|
||||
279
src/server/database-prisma.ts
Normal file
279
src/server/database-prisma.ts
Normal file
@@ -0,0 +1,279 @@
|
||||
import { prisma } from './db';
|
||||
import { join } from 'path';
|
||||
import { writeFileSync, unlinkSync, chmodSync, mkdirSync } from 'fs';
|
||||
import { existsSync } from 'fs';
|
||||
import type { CreateServerData } from '../types/server';
|
||||
|
||||
class DatabaseServicePrisma {
|
||||
constructor() {
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
// Ensure data/ssh-keys directory exists
|
||||
const sshKeysDir = join(process.cwd(), 'data', 'ssh-keys');
|
||||
if (!existsSync(sshKeysDir)) {
|
||||
mkdirSync(sshKeysDir, { mode: 0o700 });
|
||||
}
|
||||
}
|
||||
|
||||
// Server CRUD operations
|
||||
async createServer(serverData: CreateServerData) {
|
||||
const { name, ip, user, password, auth_type, ssh_key, ssh_key_passphrase, ssh_port, color, key_generated } = serverData;
|
||||
|
||||
let ssh_key_path = null;
|
||||
|
||||
// If using SSH key authentication, create persistent key file
|
||||
if (auth_type === 'key' && ssh_key) {
|
||||
const serverId = await this.getNextServerId();
|
||||
ssh_key_path = this.createSSHKeyFile(serverId, ssh_key);
|
||||
}
|
||||
|
||||
return await prisma.server.create({
|
||||
data: {
|
||||
name,
|
||||
ip,
|
||||
user,
|
||||
password,
|
||||
auth_type: auth_type ?? 'password',
|
||||
ssh_key,
|
||||
ssh_key_passphrase,
|
||||
ssh_port: ssh_port ?? 22,
|
||||
ssh_key_path,
|
||||
key_generated: Boolean(key_generated),
|
||||
color,
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async getAllServers() {
|
||||
return await prisma.server.findMany({
|
||||
orderBy: { created_at: 'desc' }
|
||||
});
|
||||
}
|
||||
|
||||
async getServerById(id: number) {
|
||||
return await prisma.server.findUnique({
|
||||
where: { id }
|
||||
});
|
||||
}
|
||||
|
||||
async updateServer(id: number, serverData: CreateServerData) {
|
||||
const { name, ip, user, password, auth_type, ssh_key, ssh_key_passphrase, ssh_port, color, key_generated } = serverData;
|
||||
|
||||
// Get existing server to check for key changes
|
||||
const existingServer = await this.getServerById(id);
|
||||
let ssh_key_path = existingServer?.ssh_key_path;
|
||||
|
||||
// Handle SSH key changes
|
||||
if (auth_type === 'key' && ssh_key) {
|
||||
// Delete old key file if it exists
|
||||
if (existingServer?.ssh_key_path && existsSync(existingServer.ssh_key_path)) {
|
||||
try {
|
||||
unlinkSync(existingServer.ssh_key_path);
|
||||
// Also delete public key file if it exists
|
||||
const pubKeyPath = existingServer.ssh_key_path + '.pub';
|
||||
if (existsSync(pubKeyPath)) {
|
||||
unlinkSync(pubKeyPath);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Failed to delete old SSH key file:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Create new key file
|
||||
ssh_key_path = this.createSSHKeyFile(id, ssh_key);
|
||||
} else if (auth_type !== 'key') {
|
||||
// If switching away from key auth, delete key files
|
||||
if (existingServer?.ssh_key_path && existsSync(existingServer.ssh_key_path)) {
|
||||
try {
|
||||
unlinkSync(existingServer.ssh_key_path);
|
||||
const pubKeyPath = existingServer.ssh_key_path + '.pub';
|
||||
if (existsSync(pubKeyPath)) {
|
||||
unlinkSync(pubKeyPath);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Failed to delete SSH key file:', error);
|
||||
}
|
||||
}
|
||||
ssh_key_path = null;
|
||||
}
|
||||
|
||||
return await prisma.server.update({
|
||||
where: { id },
|
||||
data: {
|
||||
name,
|
||||
ip,
|
||||
user,
|
||||
password,
|
||||
auth_type: auth_type ?? 'password',
|
||||
ssh_key,
|
||||
ssh_key_passphrase,
|
||||
ssh_port: ssh_port ?? 22,
|
||||
ssh_key_path,
|
||||
key_generated: key_generated !== undefined ? Boolean(key_generated) : (existingServer?.key_generated ?? false),
|
||||
color,
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async deleteServer(id: number) {
|
||||
// Get server info before deletion to clean up key files
|
||||
const server = await this.getServerById(id);
|
||||
|
||||
// Delete SSH key files if they exist
|
||||
if (server?.ssh_key_path && existsSync(server.ssh_key_path)) {
|
||||
try {
|
||||
unlinkSync(server.ssh_key_path);
|
||||
const pubKeyPath = server.ssh_key_path + '.pub';
|
||||
if (existsSync(pubKeyPath)) {
|
||||
unlinkSync(pubKeyPath);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Failed to delete SSH key file:', error);
|
||||
}
|
||||
}
|
||||
|
||||
return await prisma.server.delete({
|
||||
where: { id }
|
||||
});
|
||||
}
|
||||
|
||||
// Installed Scripts CRUD operations
|
||||
async createInstalledScript(scriptData: {
|
||||
script_name: string;
|
||||
script_path: string;
|
||||
container_id?: string;
|
||||
server_id?: number;
|
||||
execution_mode: string;
|
||||
status: 'in_progress' | 'success' | 'failed';
|
||||
output_log?: string;
|
||||
web_ui_ip?: string;
|
||||
web_ui_port?: number;
|
||||
}) {
|
||||
const { script_name, script_path, container_id, server_id, execution_mode, status, output_log, web_ui_ip, web_ui_port } = scriptData;
|
||||
|
||||
return await prisma.installedScript.create({
|
||||
data: {
|
||||
script_name,
|
||||
script_path,
|
||||
container_id: container_id ?? null,
|
||||
server_id: server_id ?? null,
|
||||
execution_mode,
|
||||
status,
|
||||
output_log: output_log ?? null,
|
||||
web_ui_ip: web_ui_ip ?? null,
|
||||
web_ui_port: web_ui_port ?? null,
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async getAllInstalledScripts() {
|
||||
return await prisma.installedScript.findMany({
|
||||
include: {
|
||||
server: true
|
||||
},
|
||||
orderBy: { installation_date: 'desc' }
|
||||
});
|
||||
}
|
||||
|
||||
async getInstalledScriptById(id: number) {
|
||||
return await prisma.installedScript.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
server: true
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async getInstalledScriptsByServer(server_id: number) {
|
||||
return await prisma.installedScript.findMany({
|
||||
where: { server_id },
|
||||
include: {
|
||||
server: true
|
||||
},
|
||||
orderBy: { installation_date: 'desc' }
|
||||
});
|
||||
}
|
||||
|
||||
async updateInstalledScript(id: number, updateData: {
|
||||
script_name?: string;
|
||||
container_id?: string;
|
||||
status?: 'in_progress' | 'success' | 'failed';
|
||||
output_log?: string;
|
||||
web_ui_ip?: string;
|
||||
web_ui_port?: number;
|
||||
}) {
|
||||
const { script_name, container_id, status, output_log, web_ui_ip, web_ui_port } = updateData;
|
||||
|
||||
const updateFields: {
|
||||
script_name?: string;
|
||||
container_id?: string;
|
||||
status?: 'in_progress' | 'success' | 'failed';
|
||||
output_log?: string;
|
||||
web_ui_ip?: string;
|
||||
web_ui_port?: number;
|
||||
} = {};
|
||||
if (script_name !== undefined) updateFields.script_name = script_name;
|
||||
if (container_id !== undefined) updateFields.container_id = container_id;
|
||||
if (status !== undefined) updateFields.status = status;
|
||||
if (output_log !== undefined) updateFields.output_log = output_log;
|
||||
if (web_ui_ip !== undefined) updateFields.web_ui_ip = web_ui_ip;
|
||||
if (web_ui_port !== undefined) updateFields.web_ui_port = web_ui_port;
|
||||
|
||||
if (Object.keys(updateFields).length === 0) {
|
||||
return { changes: 0 };
|
||||
}
|
||||
|
||||
return await prisma.installedScript.update({
|
||||
where: { id },
|
||||
data: updateFields
|
||||
});
|
||||
}
|
||||
|
||||
async deleteInstalledScript(id: number) {
|
||||
return await prisma.installedScript.delete({
|
||||
where: { id }
|
||||
});
|
||||
}
|
||||
|
||||
async deleteInstalledScriptsByServer(server_id: number) {
|
||||
return await prisma.installedScript.deleteMany({
|
||||
where: { server_id }
|
||||
});
|
||||
}
|
||||
|
||||
async getNextServerId() {
|
||||
const result = await prisma.server.findFirst({
|
||||
orderBy: { id: 'desc' },
|
||||
select: { id: true }
|
||||
});
|
||||
return (result?.id ?? 0) + 1;
|
||||
}
|
||||
|
||||
createSSHKeyFile(serverId: number, sshKey: string) {
|
||||
const sshKeysDir = join(process.cwd(), 'data', 'ssh-keys');
|
||||
const keyPath = join(sshKeysDir, `server_${serverId}_key`);
|
||||
|
||||
// Normalize the key: trim any trailing whitespace and ensure exactly one newline at the end
|
||||
const normalizedKey = sshKey.trimEnd() + '\n';
|
||||
writeFileSync(keyPath, normalizedKey);
|
||||
chmodSync(keyPath, 0o600); // Set proper permissions
|
||||
|
||||
return keyPath;
|
||||
}
|
||||
|
||||
async close() {
|
||||
await prisma.$disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
// Singleton instance
|
||||
let dbInstance: DatabaseServicePrisma | null = null;
|
||||
|
||||
export function getDatabase() {
|
||||
dbInstance ??= new DatabaseServicePrisma();
|
||||
return dbInstance;
|
||||
}
|
||||
|
||||
export default DatabaseServicePrisma;
|
||||
@@ -1,476 +0,0 @@
|
||||
import Database from 'better-sqlite3';
|
||||
import { join } from 'path';
|
||||
import { writeFileSync, unlinkSync, chmodSync, mkdirSync } from 'fs';
|
||||
import { existsSync } from 'fs';
|
||||
|
||||
class DatabaseService {
|
||||
constructor() {
|
||||
const dbPath = join(process.cwd(), 'data', 'settings.db');
|
||||
this.db = new Database(dbPath);
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
// Ensure data/ssh-keys directory exists
|
||||
const sshKeysDir = join(process.cwd(), 'data', 'ssh-keys');
|
||||
if (!existsSync(sshKeysDir)) {
|
||||
mkdirSync(sshKeysDir, { mode: 0o700 });
|
||||
}
|
||||
|
||||
// Create servers table if it doesn't exist
|
||||
this.db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS servers (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL UNIQUE,
|
||||
ip TEXT NOT NULL,
|
||||
user TEXT NOT NULL,
|
||||
password TEXT,
|
||||
auth_type TEXT DEFAULT 'password' CHECK(auth_type IN ('password', 'key')),
|
||||
ssh_key TEXT,
|
||||
ssh_key_passphrase TEXT,
|
||||
ssh_port INTEGER DEFAULT 22,
|
||||
ssh_key_path TEXT,
|
||||
key_generated INTEGER DEFAULT 0,
|
||||
color TEXT,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
`);
|
||||
|
||||
// Migration: Add new columns to existing servers table
|
||||
try {
|
||||
this.db.exec(`
|
||||
ALTER TABLE servers ADD COLUMN auth_type TEXT DEFAULT 'password' CHECK(auth_type IN ('password', 'key'))
|
||||
`);
|
||||
} catch (e) {
|
||||
// Column already exists, ignore error
|
||||
}
|
||||
|
||||
try {
|
||||
this.db.exec(`
|
||||
ALTER TABLE servers ADD COLUMN ssh_key TEXT
|
||||
`);
|
||||
} catch (e) {
|
||||
// Column already exists, ignore error
|
||||
}
|
||||
|
||||
try {
|
||||
this.db.exec(`
|
||||
ALTER TABLE servers ADD COLUMN ssh_key_passphrase TEXT
|
||||
`);
|
||||
} catch (e) {
|
||||
// Column already exists, ignore error
|
||||
}
|
||||
|
||||
try {
|
||||
this.db.exec(`
|
||||
ALTER TABLE servers ADD COLUMN ssh_port INTEGER DEFAULT 22
|
||||
`);
|
||||
} catch (e) {
|
||||
// Column already exists, ignore error
|
||||
}
|
||||
|
||||
try {
|
||||
this.db.exec(`
|
||||
ALTER TABLE servers ADD COLUMN color TEXT
|
||||
`);
|
||||
} catch (e) {
|
||||
// Column already exists, ignore error
|
||||
}
|
||||
|
||||
try {
|
||||
this.db.exec(`
|
||||
ALTER TABLE servers ADD COLUMN ssh_key_path TEXT
|
||||
`);
|
||||
} catch (e) {
|
||||
// Column already exists, ignore error
|
||||
}
|
||||
|
||||
try {
|
||||
this.db.exec(`
|
||||
ALTER TABLE servers ADD COLUMN key_generated INTEGER DEFAULT 0
|
||||
`);
|
||||
} catch (e) {
|
||||
// Column already exists, ignore error
|
||||
}
|
||||
|
||||
// Update existing servers to have auth_type='password' if not set
|
||||
this.db.exec(`
|
||||
UPDATE servers SET auth_type = 'password' WHERE auth_type IS NULL
|
||||
`);
|
||||
|
||||
// Update existing servers to have ssh_port=22 if not set
|
||||
this.db.exec(`
|
||||
UPDATE servers SET ssh_port = 22 WHERE ssh_port IS NULL
|
||||
`);
|
||||
|
||||
// Migration: Convert 'both' auth_type to 'key'
|
||||
this.db.exec(`
|
||||
UPDATE servers SET auth_type = 'key' WHERE auth_type = 'both'
|
||||
`);
|
||||
|
||||
// Update existing servers to have key_generated=0 if not set
|
||||
this.db.exec(`
|
||||
UPDATE servers SET key_generated = 0 WHERE key_generated IS NULL
|
||||
`);
|
||||
|
||||
// Migration: Add web_ui_ip column to existing installed_scripts table
|
||||
try {
|
||||
this.db.exec(`
|
||||
ALTER TABLE installed_scripts ADD COLUMN web_ui_ip TEXT
|
||||
`);
|
||||
} catch (e) {
|
||||
// Column already exists, ignore error
|
||||
}
|
||||
|
||||
// Migration: Add web_ui_port column to existing installed_scripts table
|
||||
try {
|
||||
this.db.exec(`
|
||||
ALTER TABLE installed_scripts ADD COLUMN web_ui_port INTEGER
|
||||
`);
|
||||
} catch (e) {
|
||||
// Column already exists, ignore error
|
||||
}
|
||||
|
||||
// Create installed_scripts table if it doesn't exist
|
||||
this.db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS installed_scripts (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
script_name TEXT NOT NULL,
|
||||
script_path TEXT NOT NULL,
|
||||
container_id TEXT,
|
||||
server_id INTEGER,
|
||||
execution_mode TEXT NOT NULL CHECK(execution_mode IN ('local', 'ssh')),
|
||||
installation_date DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
status TEXT NOT NULL CHECK(status IN ('in_progress', 'success', 'failed')),
|
||||
output_log TEXT,
|
||||
web_ui_ip TEXT,
|
||||
web_ui_port INTEGER,
|
||||
FOREIGN KEY (server_id) REFERENCES servers(id) ON DELETE SET NULL
|
||||
)
|
||||
`);
|
||||
|
||||
// Create trigger to update updated_at on row update
|
||||
this.db.exec(`
|
||||
CREATE TRIGGER IF NOT EXISTS update_servers_timestamp
|
||||
AFTER UPDATE ON servers
|
||||
BEGIN
|
||||
UPDATE servers SET updated_at = CURRENT_TIMESTAMP WHERE id = NEW.id;
|
||||
END
|
||||
`);
|
||||
}
|
||||
|
||||
// Server CRUD operations
|
||||
/**
|
||||
* @param {import('../types/server').CreateServerData} serverData
|
||||
*/
|
||||
createServer(serverData) {
|
||||
const { name, ip, user, password, auth_type, ssh_key, ssh_key_passphrase, ssh_port, color, key_generated } = serverData;
|
||||
|
||||
let ssh_key_path = null;
|
||||
|
||||
// If using SSH key authentication, create persistent key file
|
||||
if (auth_type === 'key' && ssh_key) {
|
||||
const serverId = this.getNextServerId();
|
||||
ssh_key_path = this.createSSHKeyFile(serverId, ssh_key);
|
||||
}
|
||||
|
||||
const stmt = this.db.prepare(`
|
||||
INSERT INTO servers (name, ip, user, password, auth_type, ssh_key, ssh_key_passphrase, ssh_port, ssh_key_path, key_generated, color)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`);
|
||||
return stmt.run(name, ip, user, password, auth_type || 'password', ssh_key, ssh_key_passphrase, ssh_port || 22, ssh_key_path, key_generated || 0, color);
|
||||
}
|
||||
|
||||
getAllServers() {
|
||||
const stmt = this.db.prepare('SELECT * FROM servers ORDER BY created_at DESC');
|
||||
return stmt.all();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} id
|
||||
*/
|
||||
getServerById(id) {
|
||||
const stmt = this.db.prepare('SELECT * FROM servers WHERE id = ?');
|
||||
return stmt.get(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} id
|
||||
* @param {import('../types/server').CreateServerData} serverData
|
||||
*/
|
||||
updateServer(id, serverData) {
|
||||
const { name, ip, user, password, auth_type, ssh_key, ssh_key_passphrase, ssh_port, color, key_generated } = serverData;
|
||||
|
||||
// Get existing server to check for key changes
|
||||
const existingServer = this.getServerById(id);
|
||||
// @ts-ignore - Database migration adds this column
|
||||
let ssh_key_path = existingServer?.ssh_key_path;
|
||||
|
||||
// Handle SSH key changes
|
||||
if (auth_type === 'key' && ssh_key) {
|
||||
// Delete old key file if it exists
|
||||
// @ts-ignore - Database migration adds this column
|
||||
if (existingServer?.ssh_key_path && existsSync(existingServer.ssh_key_path)) {
|
||||
try {
|
||||
// @ts-ignore - Database migration adds this column
|
||||
unlinkSync(existingServer.ssh_key_path);
|
||||
// Also delete public key file if it exists
|
||||
// @ts-ignore - Database migration adds this column
|
||||
const pubKeyPath = existingServer.ssh_key_path + '.pub';
|
||||
if (existsSync(pubKeyPath)) {
|
||||
unlinkSync(pubKeyPath);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Failed to delete old SSH key file:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Create new key file
|
||||
ssh_key_path = this.createSSHKeyFile(id, ssh_key);
|
||||
} else if (auth_type !== 'key') {
|
||||
// If switching away from key auth, delete key files
|
||||
// @ts-ignore - Database migration adds this column
|
||||
if (existingServer?.ssh_key_path && existsSync(existingServer.ssh_key_path)) {
|
||||
try {
|
||||
// @ts-ignore - Database migration adds this column
|
||||
unlinkSync(existingServer.ssh_key_path);
|
||||
// @ts-ignore - Database migration adds this column
|
||||
const pubKeyPath = existingServer.ssh_key_path + '.pub';
|
||||
if (existsSync(pubKeyPath)) {
|
||||
unlinkSync(pubKeyPath);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Failed to delete SSH key file:', error);
|
||||
}
|
||||
}
|
||||
ssh_key_path = null;
|
||||
}
|
||||
|
||||
const stmt = this.db.prepare(`
|
||||
UPDATE servers
|
||||
SET name = ?, ip = ?, user = ?, password = ?, auth_type = ?, ssh_key = ?, ssh_key_passphrase = ?, ssh_port = ?, ssh_key_path = ?, key_generated = ?, color = ?
|
||||
WHERE id = ?
|
||||
`);
|
||||
// @ts-ignore - Database migration adds this column
|
||||
return stmt.run(name, ip, user, password, auth_type || 'password', ssh_key, ssh_key_passphrase, ssh_port || 22, ssh_key_path, key_generated !== undefined ? key_generated : (existingServer?.key_generated || 0), color, id);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} id
|
||||
*/
|
||||
deleteServer(id) {
|
||||
// Get server info before deletion to clean up key files
|
||||
const server = this.getServerById(id);
|
||||
|
||||
// Delete SSH key files if they exist
|
||||
// @ts-ignore - Database migration adds this column
|
||||
if (server?.ssh_key_path && existsSync(server.ssh_key_path)) {
|
||||
try {
|
||||
// @ts-ignore - Database migration adds this column
|
||||
unlinkSync(server.ssh_key_path);
|
||||
// @ts-ignore - Database migration adds this column
|
||||
const pubKeyPath = server.ssh_key_path + '.pub';
|
||||
if (existsSync(pubKeyPath)) {
|
||||
unlinkSync(pubKeyPath);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Failed to delete SSH key file:', error);
|
||||
}
|
||||
}
|
||||
|
||||
const stmt = this.db.prepare('DELETE FROM servers WHERE id = ?');
|
||||
return stmt.run(id);
|
||||
}
|
||||
|
||||
// Installed Scripts CRUD operations
|
||||
/**
|
||||
* @param {Object} scriptData
|
||||
* @param {string} scriptData.script_name
|
||||
* @param {string} scriptData.script_path
|
||||
* @param {string} [scriptData.container_id]
|
||||
* @param {number} [scriptData.server_id]
|
||||
* @param {string} scriptData.execution_mode
|
||||
* @param {string} scriptData.status
|
||||
* @param {string} [scriptData.output_log]
|
||||
* @param {string} [scriptData.web_ui_ip]
|
||||
* @param {number} [scriptData.web_ui_port]
|
||||
*/
|
||||
createInstalledScript(scriptData) {
|
||||
const { script_name, script_path, container_id, server_id, execution_mode, status, output_log, web_ui_ip, web_ui_port } = scriptData;
|
||||
const stmt = this.db.prepare(`
|
||||
INSERT INTO installed_scripts (script_name, script_path, container_id, server_id, execution_mode, status, output_log, web_ui_ip, web_ui_port)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`);
|
||||
return stmt.run(script_name, script_path, container_id || null, server_id || null, execution_mode, status, output_log || null, web_ui_ip || null, web_ui_port || null);
|
||||
}
|
||||
|
||||
getAllInstalledScripts() {
|
||||
const stmt = this.db.prepare(`
|
||||
SELECT
|
||||
inst.*,
|
||||
s.name as server_name,
|
||||
s.ip as server_ip,
|
||||
s.user as server_user,
|
||||
s.password as server_password,
|
||||
s.auth_type as server_auth_type,
|
||||
s.ssh_key as server_ssh_key,
|
||||
s.ssh_key_passphrase as server_ssh_key_passphrase,
|
||||
s.ssh_port as server_ssh_port,
|
||||
s.color as server_color
|
||||
FROM installed_scripts inst
|
||||
LEFT JOIN servers s ON inst.server_id = s.id
|
||||
ORDER BY inst.installation_date DESC
|
||||
`);
|
||||
return stmt.all();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} id
|
||||
*/
|
||||
getInstalledScriptById(id) {
|
||||
const stmt = this.db.prepare(`
|
||||
SELECT
|
||||
inst.*,
|
||||
s.name as server_name,
|
||||
s.ip as server_ip
|
||||
FROM installed_scripts inst
|
||||
LEFT JOIN servers s ON inst.server_id = s.id
|
||||
WHERE inst.id = ?
|
||||
`);
|
||||
return stmt.get(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} server_id
|
||||
*/
|
||||
getInstalledScriptsByServer(server_id) {
|
||||
const stmt = this.db.prepare(`
|
||||
SELECT
|
||||
inst.*,
|
||||
s.name as server_name,
|
||||
s.ip as server_ip
|
||||
FROM installed_scripts inst
|
||||
LEFT JOIN servers s ON inst.server_id = s.id
|
||||
WHERE inst.server_id = ?
|
||||
ORDER BY inst.installation_date DESC
|
||||
`);
|
||||
return stmt.all(server_id);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} id
|
||||
* @param {Object} updateData
|
||||
* @param {string} [updateData.script_name]
|
||||
* @param {string} [updateData.container_id]
|
||||
* @param {string} [updateData.status]
|
||||
* @param {string} [updateData.output_log]
|
||||
* @param {string} [updateData.web_ui_ip]
|
||||
* @param {number} [updateData.web_ui_port]
|
||||
*/
|
||||
updateInstalledScript(id, updateData) {
|
||||
const { script_name, container_id, status, output_log, web_ui_ip, web_ui_port } = updateData;
|
||||
const updates = [];
|
||||
const values = [];
|
||||
|
||||
if (script_name !== undefined) {
|
||||
updates.push('script_name = ?');
|
||||
values.push(script_name);
|
||||
}
|
||||
if (container_id !== undefined) {
|
||||
updates.push('container_id = ?');
|
||||
values.push(container_id);
|
||||
}
|
||||
if (status !== undefined) {
|
||||
updates.push('status = ?');
|
||||
values.push(status);
|
||||
}
|
||||
if (output_log !== undefined) {
|
||||
updates.push('output_log = ?');
|
||||
values.push(output_log);
|
||||
}
|
||||
if (web_ui_ip !== undefined) {
|
||||
updates.push('web_ui_ip = ?');
|
||||
values.push(web_ui_ip);
|
||||
}
|
||||
if (web_ui_port !== undefined) {
|
||||
updates.push('web_ui_port = ?');
|
||||
values.push(web_ui_port);
|
||||
}
|
||||
|
||||
if (updates.length === 0) {
|
||||
return { changes: 0 };
|
||||
}
|
||||
|
||||
values.push(id);
|
||||
const stmt = this.db.prepare(`
|
||||
UPDATE installed_scripts
|
||||
SET ${updates.join(', ')}
|
||||
WHERE id = ?
|
||||
`);
|
||||
return stmt.run(...values);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} id
|
||||
*/
|
||||
deleteInstalledScript(id) {
|
||||
const stmt = this.db.prepare('DELETE FROM installed_scripts WHERE id = ?');
|
||||
return stmt.run(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} server_id
|
||||
*/
|
||||
deleteInstalledScriptsByServer(server_id) {
|
||||
const stmt = this.db.prepare('DELETE FROM installed_scripts WHERE server_id = ?');
|
||||
return stmt.run(server_id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the next available server ID for key file naming
|
||||
* @returns {number}
|
||||
*/
|
||||
getNextServerId() {
|
||||
const stmt = this.db.prepare('SELECT MAX(id) as maxId FROM servers');
|
||||
const result = stmt.get();
|
||||
// @ts-ignore - SQL query result type
|
||||
return (result?.maxId || 0) + 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create SSH key file and return the path
|
||||
* @param {number} serverId
|
||||
* @param {string} sshKey
|
||||
* @returns {string}
|
||||
*/
|
||||
createSSHKeyFile(serverId, sshKey) {
|
||||
const sshKeysDir = join(process.cwd(), 'data', 'ssh-keys');
|
||||
const keyPath = join(sshKeysDir, `server_${serverId}_key`);
|
||||
|
||||
// Normalize the key: trim any trailing whitespace and ensure exactly one newline at the end
|
||||
const normalizedKey = sshKey.trimEnd() + '\n';
|
||||
writeFileSync(keyPath, normalizedKey);
|
||||
chmodSync(keyPath, 0o600); // Set proper permissions
|
||||
|
||||
return keyPath;
|
||||
}
|
||||
|
||||
close() {
|
||||
this.db.close();
|
||||
}
|
||||
}
|
||||
|
||||
// Singleton instance
|
||||
/** @type {DatabaseService | null} */
|
||||
let dbInstance = null;
|
||||
|
||||
export function getDatabase() {
|
||||
if (!dbInstance) {
|
||||
dbInstance = new DatabaseService();
|
||||
}
|
||||
return dbInstance;
|
||||
}
|
||||
|
||||
export default DatabaseService;
|
||||
|
||||
7
src/server/db.js
Normal file
7
src/server/db.js
Normal file
@@ -0,0 +1,7 @@
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
|
||||
const globalForPrisma = globalThis;
|
||||
|
||||
export const prisma = globalForPrisma.prisma ?? new PrismaClient();
|
||||
|
||||
if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma;
|
||||
9
src/server/db.ts
Normal file
9
src/server/db.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
|
||||
const globalForPrisma = globalThis as unknown as {
|
||||
prisma: PrismaClient | undefined;
|
||||
};
|
||||
|
||||
export const prisma = globalForPrisma.prisma ?? new PrismaClient();
|
||||
|
||||
if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma;
|
||||
@@ -8,11 +8,11 @@ export interface Server {
|
||||
ssh_key?: string;
|
||||
ssh_key_passphrase?: string;
|
||||
ssh_key_path?: string;
|
||||
key_generated?: number;
|
||||
key_generated?: boolean;
|
||||
ssh_port?: number;
|
||||
color?: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
created_at: Date | null;
|
||||
updated_at: Date | null;
|
||||
}
|
||||
|
||||
export interface CreateServerData {
|
||||
@@ -24,7 +24,7 @@ export interface CreateServerData {
|
||||
ssh_key?: string;
|
||||
ssh_key_passphrase?: string;
|
||||
ssh_key_path?: string;
|
||||
key_generated?: number;
|
||||
key_generated?: boolean;
|
||||
ssh_port?: number;
|
||||
color?: string;
|
||||
}
|
||||
|
||||
59
update.sh
59
update.sh
@@ -412,6 +412,36 @@ restore_backup_files() {
|
||||
fi
|
||||
}
|
||||
|
||||
# Ensure DATABASE_URL is set in .env file for Prisma
|
||||
ensure_database_url() {
|
||||
log "Ensuring DATABASE_URL is set in .env file..."
|
||||
|
||||
# Check if .env file exists
|
||||
if [ ! -f ".env" ]; then
|
||||
log_warning ".env file not found, creating from .env.example..."
|
||||
if [ -f ".env.example" ]; then
|
||||
cp ".env.example" ".env"
|
||||
else
|
||||
log_error ".env.example not found, cannot create .env file"
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Check if DATABASE_URL is already set
|
||||
if grep -q "^DATABASE_URL=" .env; then
|
||||
log "DATABASE_URL already exists in .env file"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Add DATABASE_URL to .env file
|
||||
log "Adding DATABASE_URL to .env file..."
|
||||
echo "" >> .env
|
||||
echo "# Database" >> .env
|
||||
echo "DATABASE_URL=\"file:./data/database.sqlite\"" >> .env
|
||||
|
||||
log_success "DATABASE_URL added to .env file"
|
||||
}
|
||||
|
||||
# Check if systemd service exists
|
||||
check_service() {
|
||||
# systemctl status returns 0-3 if service exists (running, exited, failed, etc.)
|
||||
@@ -607,6 +637,32 @@ install_and_build() {
|
||||
log_success "Dependencies installed successfully"
|
||||
rm -f "$npm_log"
|
||||
|
||||
# Generate Prisma client
|
||||
log "Generating Prisma client..."
|
||||
if ! npx prisma generate > "$npm_log" 2>&1; then
|
||||
log_error "Failed to generate Prisma client"
|
||||
log_error "Prisma generate output:"
|
||||
cat "$npm_log" | while read -r line; do
|
||||
log_error "PRISMA: $line"
|
||||
done
|
||||
rm -f "$npm_log"
|
||||
return 1
|
||||
fi
|
||||
log_success "Prisma client generated successfully"
|
||||
|
||||
# Run Prisma migrations
|
||||
log "Running Prisma migrations..."
|
||||
if ! npx prisma migrate deploy > "$npm_log" 2>&1; then
|
||||
log_warning "Prisma migrations failed or no migrations to run"
|
||||
log "Prisma migrate output:"
|
||||
cat "$npm_log" | while read -r line; do
|
||||
log "PRISMA: $line"
|
||||
done
|
||||
else
|
||||
log_success "Prisma migrations completed successfully"
|
||||
fi
|
||||
rm -f "$npm_log"
|
||||
|
||||
log "Building application..."
|
||||
# Set NODE_ENV to production for build
|
||||
export NODE_ENV=production
|
||||
@@ -838,6 +894,9 @@ main() {
|
||||
# Restore .env and data directory before building
|
||||
restore_backup_files
|
||||
|
||||
# Ensure DATABASE_URL is set for Prisma
|
||||
ensure_database_url
|
||||
|
||||
# Install dependencies and build
|
||||
if ! install_and_build; then
|
||||
log_error "Install and build failed, rolling back..."
|
||||
|
||||
Reference in New Issue
Block a user