@naviyra/cloud-panel 0.0.3-beta2 → 0.0.3-beta3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/apps/agent/src/cloudflare.mjs +1 -0
- package/apps/agent/src/files.mjs +162 -0
- package/apps/agent/src/host-tools.mjs +518 -0
- package/apps/agent/src/native.mjs +157 -32
- package/apps/agent/src/runtime.mjs +70 -37
- package/apps/agent/src/server.mjs +61 -6
- package/apps/panel/app/api/[...path]/route.js +94 -13
- package/apps/panel/app/globals.css +262 -3
- package/apps/panel/app/layout.jsx +2 -1
- package/apps/panel/app/login/page.jsx +3 -8
- package/apps/panel/app/page.jsx +17 -7
- package/apps/panel/app/ui/cloudflare.jsx +72 -47
- package/apps/panel/app/ui/confirm.jsx +109 -0
- package/apps/panel/app/ui/dashboard.jsx +87 -72
- package/apps/panel/app/ui/databases.jsx +168 -59
- package/apps/panel/app/ui/project-tools.jsx +604 -0
- package/apps/panel/app/ui/toast.jsx +90 -0
- package/apps/panel/lib/agent.mjs +13 -2
- package/apps/panel/lib/databases.mjs +120 -0
- package/apps/panel/lib/request-origin.mjs +29 -29
- package/package.json +1 -1
- package/packages/shared/index.mjs +49 -0
- package/scripts/panel-control.mjs +214 -214
- package/scripts/service-supervisor.mjs +164 -164
- package/scripts/windows-boot.mjs +259 -259
package/apps/panel/lib/agent.mjs
CHANGED
|
@@ -9,16 +9,27 @@ export async function agent(route, options = {}) {
|
|
|
9
9
|
throw Error("Remote agents require HTTPS");
|
|
10
10
|
if ((process.env.AGENT_TOKEN?.length || 0) < 32)
|
|
11
11
|
throw Error("Configure AGENT_TOKEN");
|
|
12
|
+
const { signal, raw, ...rest } = options;
|
|
12
13
|
const response = await fetch(base + route, {
|
|
13
|
-
...
|
|
14
|
+
...rest,
|
|
14
15
|
headers: {
|
|
15
16
|
Authorization: `Bearer ${process.env.AGENT_TOKEN}`,
|
|
16
17
|
"Content-Type": "application/json",
|
|
17
18
|
...options.headers,
|
|
18
19
|
},
|
|
19
20
|
cache: "no-store",
|
|
20
|
-
signal: AbortSignal.timeout(30000),
|
|
21
|
+
signal: signal ?? AbortSignal.timeout(30000),
|
|
21
22
|
});
|
|
23
|
+
if (raw) {
|
|
24
|
+
if (!response.ok) {
|
|
25
|
+
const body = await response.json().catch(() => ({}));
|
|
26
|
+
throw Object.assign(Error(body.error || "Agent request failed"), {
|
|
27
|
+
status: response.status,
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
await db.query("UPDATE agents SET last_seen=now() WHERE name='primary'");
|
|
31
|
+
return response;
|
|
32
|
+
}
|
|
22
33
|
const body = await response.json();
|
|
23
34
|
if (!response.ok)
|
|
24
35
|
throw Object.assign(Error(body.error || "Agent request failed"), {
|
|
@@ -124,6 +124,126 @@ function view(row, parts, password) {
|
|
|
124
124
|
};
|
|
125
125
|
}
|
|
126
126
|
|
|
127
|
+
const DB_NAME = /^[a-zA-Z_][a-zA-Z0-9_]{0,62}$/;
|
|
128
|
+
const SECRET = /password|passwd|token|secret|credential|api_key/i;
|
|
129
|
+
|
|
130
|
+
function assertDbName(value) {
|
|
131
|
+
const name = String(value || "");
|
|
132
|
+
if (!DB_NAME.test(name)) throw fail("Database not found", 404);
|
|
133
|
+
return name;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function cell(value) {
|
|
137
|
+
if (value == null) return null;
|
|
138
|
+
if (typeof value === "number" || typeof value === "boolean") return value;
|
|
139
|
+
if (value instanceof Date) return value.toISOString();
|
|
140
|
+
if (Buffer.isBuffer(value)) return "[binary]";
|
|
141
|
+
const text =
|
|
142
|
+
typeof value === "object" ? JSON.stringify(value) : String(value);
|
|
143
|
+
return text.length > 500 ? `${text.slice(0, 500)}…` : text;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async function connectDatabase(name) {
|
|
147
|
+
const { url } = connectionParts();
|
|
148
|
+
const admin = new URL(url.href);
|
|
149
|
+
admin.pathname = `/${name}`;
|
|
150
|
+
const client = new pg.Client({ connectionString: admin.href });
|
|
151
|
+
await client.connect();
|
|
152
|
+
await client.query("SET statement_timeout = '15s'");
|
|
153
|
+
await client.query("SET default_transaction_read_only = on");
|
|
154
|
+
return client;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export async function listNativeDatabases() {
|
|
158
|
+
const parts = connectionParts();
|
|
159
|
+
const databases = await withAdmin(async (client) => {
|
|
160
|
+
const { rows } = await client.query(
|
|
161
|
+
`SELECT datname AS name, pg_database_size(datname)::bigint AS bytes
|
|
162
|
+
FROM pg_database
|
|
163
|
+
WHERE NOT datistemplate AND datname LIKE 'nv\\_%' ESCAPE '\\'
|
|
164
|
+
ORDER BY datname`,
|
|
165
|
+
);
|
|
166
|
+
return rows
|
|
167
|
+
.filter((row) => row.name !== parts.database)
|
|
168
|
+
.map((row) => ({ name: row.name, bytes: Number(row.bytes) }));
|
|
169
|
+
});
|
|
170
|
+
return { host: parts.host, port: parts.port, databases };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export async function browseNativeDatabase(database, table) {
|
|
174
|
+
const name = assertDbName(database);
|
|
175
|
+
if (!name.startsWith("nv_")) throw fail("Database not found", 404);
|
|
176
|
+
if (process.env.DATABASE_URL && name === connectionParts().database)
|
|
177
|
+
throw fail("Database not found", 404);
|
|
178
|
+
const exists = await withAdmin(async (client) => {
|
|
179
|
+
const found = await client.query(
|
|
180
|
+
"SELECT 1 FROM pg_database WHERE datname=$1 AND NOT datistemplate",
|
|
181
|
+
[name],
|
|
182
|
+
);
|
|
183
|
+
return found.rowCount > 0;
|
|
184
|
+
});
|
|
185
|
+
if (!exists) throw fail("Database not found", 404);
|
|
186
|
+
const client = await connectDatabase(name);
|
|
187
|
+
try {
|
|
188
|
+
const tables = (
|
|
189
|
+
await client.query(
|
|
190
|
+
`SELECT c.relname AS name
|
|
191
|
+
FROM pg_class c
|
|
192
|
+
JOIN pg_namespace n ON n.oid = c.relnamespace
|
|
193
|
+
WHERE n.nspname = 'public' AND c.relkind = 'r'
|
|
194
|
+
ORDER BY c.relname`,
|
|
195
|
+
)
|
|
196
|
+
).rows.map((row) => row.name);
|
|
197
|
+
if (!table) return { database: name, tables, columns: [], rows: [] };
|
|
198
|
+
if (!DB_NAME.test(table) || !tables.includes(table))
|
|
199
|
+
throw fail("Table not found", 404);
|
|
200
|
+
const columns = (
|
|
201
|
+
await client.query(
|
|
202
|
+
`SELECT column_name, data_type
|
|
203
|
+
FROM information_schema.columns
|
|
204
|
+
WHERE table_schema = 'public' AND table_name = $1
|
|
205
|
+
ORDER BY ordinal_position`,
|
|
206
|
+
[table],
|
|
207
|
+
)
|
|
208
|
+
).rows;
|
|
209
|
+
const statement = (
|
|
210
|
+
await client.query(
|
|
211
|
+
"SELECT format('SELECT * FROM %I LIMIT 50', $1::text) AS statement",
|
|
212
|
+
[table],
|
|
213
|
+
)
|
|
214
|
+
).rows[0]?.statement;
|
|
215
|
+
const allowed = new Set([
|
|
216
|
+
`SELECT * FROM ${table} LIMIT 50`,
|
|
217
|
+
`SELECT * FROM "${table}" LIMIT 50`,
|
|
218
|
+
]);
|
|
219
|
+
if (!allowed.has(statement)) throw fail("Could not read the table", 500);
|
|
220
|
+
const result = await client.query(statement);
|
|
221
|
+
const hidden = new Set(
|
|
222
|
+
columns
|
|
223
|
+
.filter((column) => SECRET.test(column.column_name))
|
|
224
|
+
.map((column) => column.column_name),
|
|
225
|
+
);
|
|
226
|
+
return {
|
|
227
|
+
database: name,
|
|
228
|
+
table,
|
|
229
|
+
tables,
|
|
230
|
+
columns: columns.map((column) => ({
|
|
231
|
+
name: column.column_name,
|
|
232
|
+
type: column.data_type,
|
|
233
|
+
hidden: hidden.has(column.column_name),
|
|
234
|
+
})),
|
|
235
|
+
rows: result.rows.map((row) => {
|
|
236
|
+
const out = {};
|
|
237
|
+
for (const [key, value] of Object.entries(row))
|
|
238
|
+
out[key] = hidden.has(key) ? "[hidden]" : cell(value);
|
|
239
|
+
return out;
|
|
240
|
+
}),
|
|
241
|
+
};
|
|
242
|
+
} finally {
|
|
243
|
+
await client.end();
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
127
247
|
export async function listDatabases(userId) {
|
|
128
248
|
const rows = (
|
|
129
249
|
await db.query(
|
|
@@ -1,29 +1,29 @@
|
|
|
1
|
-
const LOOPBACK = new Set(["localhost", "127.0.0.1", "::1"]);
|
|
2
|
-
|
|
3
|
-
function parseOrigin(value) {
|
|
4
|
-
if (!value || typeof value !== "string") return null;
|
|
5
|
-
try {
|
|
6
|
-
const url = new URL(value);
|
|
7
|
-
if (url.origin === "null") return null;
|
|
8
|
-
return url;
|
|
9
|
-
} catch {
|
|
10
|
-
return null;
|
|
11
|
-
}
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
function hostname(url) {
|
|
15
|
-
return url.hostname.replace(/^\[|\]$/g, "");
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
function sameSite(left, right) {
|
|
19
|
-
if (left.origin === right.origin) return true;
|
|
20
|
-
if (left.protocol !== right.protocol || left.port !== right.port) return false;
|
|
21
|
-
return LOOPBACK.has(hostname(left)) && LOOPBACK.has(hostname(right));
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export function originAllowed(originHeader, expected) {
|
|
25
|
-
const origin = parseOrigin(originHeader);
|
|
26
|
-
const target = parseOrigin(expected);
|
|
27
|
-
if (!origin || !target) return false;
|
|
28
|
-
return sameSite(origin, target);
|
|
29
|
-
}
|
|
1
|
+
const LOOPBACK = new Set(["localhost", "127.0.0.1", "::1"]);
|
|
2
|
+
|
|
3
|
+
function parseOrigin(value) {
|
|
4
|
+
if (!value || typeof value !== "string") return null;
|
|
5
|
+
try {
|
|
6
|
+
const url = new URL(value);
|
|
7
|
+
if (url.origin === "null") return null;
|
|
8
|
+
return url;
|
|
9
|
+
} catch {
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function hostname(url) {
|
|
15
|
+
return url.hostname.replace(/^\[|\]$/g, "");
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function sameSite(left, right) {
|
|
19
|
+
if (left.origin === right.origin) return true;
|
|
20
|
+
if (left.protocol !== right.protocol || left.port !== right.port) return false;
|
|
21
|
+
return LOOPBACK.has(hostname(left)) && LOOPBACK.has(hostname(right));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function originAllowed(originHeader, expected) {
|
|
25
|
+
const origin = parseOrigin(originHeader);
|
|
26
|
+
const target = parseOrigin(expected);
|
|
27
|
+
if (!origin || !target) return false;
|
|
28
|
+
return sameSite(origin, target);
|
|
29
|
+
}
|
package/package.json
CHANGED
|
@@ -14,6 +14,31 @@ const relativePath = z
|
|
|
14
14
|
/^(?!\/)(?!.*(?:^|\/)\.\.(?:\/|$))[a-zA-Z0-9_./-]+$/,
|
|
15
15
|
"Use a relative project path without ..",
|
|
16
16
|
);
|
|
17
|
+
const optionalPath = z.union([z.literal(""), relativePath]);
|
|
18
|
+
export const reservedEnv = new Set([
|
|
19
|
+
"PATH",
|
|
20
|
+
"NODE_OPTIONS",
|
|
21
|
+
"NODE_PATH",
|
|
22
|
+
"LD_PRELOAD",
|
|
23
|
+
"LD_LIBRARY_PATH",
|
|
24
|
+
"DYLD_INSERT_LIBRARIES",
|
|
25
|
+
"HOME",
|
|
26
|
+
"USERPROFILE",
|
|
27
|
+
"SYSTEMROOT",
|
|
28
|
+
"WINDIR",
|
|
29
|
+
"COMSPEC",
|
|
30
|
+
"PATHEXT",
|
|
31
|
+
"TEMP",
|
|
32
|
+
"TMP",
|
|
33
|
+
"APPDATA",
|
|
34
|
+
"LOCALAPPDATA",
|
|
35
|
+
"PORT",
|
|
36
|
+
"HOST",
|
|
37
|
+
"HOSTNAME",
|
|
38
|
+
"NODE_ENV",
|
|
39
|
+
"GIT_TERMINAL_PROMPT",
|
|
40
|
+
"PYTHONUNBUFFERED",
|
|
41
|
+
]);
|
|
17
42
|
export const runtimeConfig = z
|
|
18
43
|
.object({
|
|
19
44
|
python_server: z.enum(["asgi", "wsgi"]).default("asgi"),
|
|
@@ -28,6 +53,27 @@ export const runtimeConfig = z
|
|
|
28
53
|
.regex(/^[a-zA-Z0-9_.-]*$/)
|
|
29
54
|
.default(""),
|
|
30
55
|
args: z.array(z.string().max(1000)).max(32).default([]),
|
|
56
|
+
startup_file: optionalPath.default(""),
|
|
57
|
+
working_dir: relativePath.default("."),
|
|
58
|
+
env: z
|
|
59
|
+
.record(
|
|
60
|
+
z.string().regex(/^[A-Za-z_][A-Za-z0-9_]{0,63}$/),
|
|
61
|
+
z.string().max(500),
|
|
62
|
+
)
|
|
63
|
+
.refine((value) => Object.keys(value).length <= 30, {
|
|
64
|
+
message: "Use at most 30 environment variables",
|
|
65
|
+
})
|
|
66
|
+
.default({}),
|
|
67
|
+
})
|
|
68
|
+
.superRefine((config, ctx) => {
|
|
69
|
+
for (const key of Object.keys(config.env || {})) {
|
|
70
|
+
if (reservedEnv.has(key.toUpperCase()) || key === "Path")
|
|
71
|
+
ctx.addIssue({
|
|
72
|
+
code: "custom",
|
|
73
|
+
path: ["env", key],
|
|
74
|
+
message: `${key} is reserved and cannot be set here`,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
31
77
|
})
|
|
32
78
|
.default({
|
|
33
79
|
python_server: "asgi",
|
|
@@ -35,6 +81,9 @@ export const runtimeConfig = z
|
|
|
35
81
|
public_dir: ".",
|
|
36
82
|
executable: "",
|
|
37
83
|
args: [],
|
|
84
|
+
startup_file: "",
|
|
85
|
+
working_dir: ".",
|
|
86
|
+
env: {},
|
|
38
87
|
});
|
|
39
88
|
export const projectInput = z
|
|
40
89
|
.object({
|