| 1 | import { randomBytes, randomUUID } from "crypto"; |
| 2 | import { eq } |
| 3 | import { z } |
| 4 | import { db } |
| 5 | import { nodes } |
| 6 | import { recordActivity } |
| 7 | import { getEffectiveAppUrl } |
| 8 | import { encrypt, safeDecrypt |
| 9 | import { adminProcedure } |
| 10 | |
| 11 | export const nodesRouter = |
| 12 | list: adminProcedure.handler |
| 13 | return db.select |
| 14 | }), |
| 15 | |
| 16 | get: adminProcedure |
| 17 | .input(z.object |
| 18 | .handler(async ( |
| 19 | return db.query |
| 20 | where: eq(nodes |
| 21 | }); |
| 22 | }), |
| 23 | |
| 24 | create: adminProcedure |
| 25 | .input( |
| 26 | z.object({ |
| 27 | name: z.string |
| 28 | description: z.string |
| 29 | locationId: z.string |
| 30 | fqdn: z.string |
| 31 | scheme: z.enum |
| 32 | memory: z.number |
| 33 | memoryOverallocate: z.number |
| 34 | disk: z.number |
| 35 | diskOverallocate: z.number |
| 36 | uploadSize: z.number |
| 37 | daemonListen: z.number |
| 38 | daemonSFTP: z.number |
| 39 | daemonBase: z.string |
| 40 | }), |
| 41 | ) |
| 42 | .handler(async ( |
| 43 | const id = |
| 44 | const uuid = |
| 45 | const tokenId = |
| 46 | const token = |
| 47 | |
| 48 | await db.insert |
| 49 | id, |
| 50 | uuid, |
| 51 | tokenId, |
| 52 | token: encrypt(token |
| 53 | ...input, |
| 54 | }); |
| 55 | |
| 56 | recordActivity({ eventType: |
| 57 | |
| 58 | return db.query |
| 59 | }), |
| 60 | |
| 61 | update: adminProcedure |
| 62 | .input( |
| 63 | z.object({ |
| 64 | id: z.string |
| 65 | name: z.string |
| 66 | description: z.string |
| 67 | fqdn: z.string |
| 68 | scheme: z.enum |
| 69 | memory: z.number |
| 70 | memoryOverallocate: z.number |
| 71 | disk: z.number |
| 72 | diskOverallocate: z.number |
| 73 | uploadSize: z.number |
| 74 | daemonListen: z.number |
| 75 | daemonSFTP: z.number |
| 76 | daemonBase: z.string |
| 77 | maintenanceMode: z.boolean |
| 78 | }), |
| 79 | ) |
| 80 | .handler(async ( |
| 81 | const { id, |
| 82 | await db.update |
| 83 | const node = |
| 84 | |
| 85 | let wingsUpdated = |
| 86 | if (node) { |
| 87 | try { |
| 88 | const res = |
| 89 | `${node.scheme} |
| 90 | { |
| 91 | method: "POST", |
| 92 | headers: { |
| 93 | Authorization: `Bearer ${safeDecrypt |
| 94 | "Content-Type": "application/json", |
| 95 | }, |
| 96 | body: JSON.stringify |
| 97 | api: { |
| 98 | port: node.daemonListen |
| 99 | ssl: { enabled: node |
| 100 | upload_limit: node.uploadSize |
| 101 | }, |
| 102 | system: { |
| 103 | sftp: { bind_port: node |
| 104 | }, |
| 105 | }), |
| 106 | signal: AbortSignal.timeout |
| 107 | }, |
| 108 | ); |
| 109 | wingsUpdated = res. |
| 110 | } catch { |
| 111 | // Wings unreachable — DB update already succeeded |
| 112 | } |
| 113 | } |
| 114 | |
| 115 | recordActivity({ eventType: |
| 116 | |
| 117 | return { node, |
| 118 | }), |
| 119 | |
| 120 | delete: adminProcedure |
| 121 | .input(z.object |
| 122 | .handler(async ( |
| 123 | const node = |
| 124 | await db.delete |
| 125 | recordActivity({ eventType: |
| 126 | }), |
| 127 | |
| 128 | regenerateToken: adminProcedure |
| 129 | .input(z.object |
| 130 | .handler(async ( |
| 131 | const node = |
| 132 | const tokenId = |
| 133 | const token = |
| 134 | await db.update |
| 135 | recordActivity({ eventType: |
| 136 | return { tokenId, |
| 137 | }), |
| 138 | |
| 139 | getDeploymentConfig: adminProcedure |
| 140 | .input(z.object |
| 141 | .handler(async ( |
| 142 | const node = |
| 143 | where: eq(nodes |
| 144 | }); |
| 145 | if (!node) |
| 146 | |
| 147 | const appUrl = |
| 148 | |
| 149 | const base = |
| 150 | const sslEnabled = |
| 151 | |
| 152 | return { |
| 153 | yaml: `debug: false |
| 154 | app_name: Pterodactyl |
| 155 | uuid: ${node.uuid} |
| 156 | token_id: ${node.tokenId} |
| 157 | token: ${safeDecrypt(node.token |
| 158 | api: |
| 159 | host: 0.0.0.0 |
| 160 | port: ${node.daemonListen} |
| 161 | ssl: |
| 162 | enabled: ${sslEnabled} |
| 163 | cert: '' |
| 164 | key: '' |
| 165 | redirects: {} |
| 166 | disable_openapi_docs: false |
| 167 | disable_remote_download: false |
| 168 | server_remote_download_limit: 3 |
| 169 | remote_download_blocked_cidrs: |
| 170 | - 127.0.0.0/8 |
| 171 | - 10.0.0.0/8 |
| 172 | - 172.16.0.0/12 |
| 173 | - 192.168.0.0/16 |
| 174 | - 169.254.0.0/16 |
| 175 | - ::1 |
| 176 | - fe80::/10 |
| 177 | - fc00::/7 |
| 178 | disable_directory_size: false |
| 179 | directory_entry_limit: 10000 |
| 180 | send_offline_server_logs: false |
| 181 | file_search_threads: 4 |
| 182 | file_copy_threads: 4 |
| 183 | file_decompression_threads: 2 |
| 184 | file_compression_threads: 2 |
| 185 | upload_limit: ${node.uploadSize} |
| 186 | max_jwt_uses: 5 |
| 187 | trusted_proxies: [] |
| 188 | system: |
| 189 | root_directory: ${base} |
| 190 | log_directory: /var/log/pterodactyl |
| 191 | vmount_directory: ${base}/vmounts |
| 192 | data: ${base} |
| 193 | archive_directory: ${base}/archives |
| 194 | backup_directory: ${base}/backups |
| 195 | tmp_directory: /tmp/pterodactyl |
| 196 | username: pterodactyl |
| 197 | timezone: UTC |
| 198 | user: |
| 199 | rootless: |
| 200 | enabled: false |
| 201 | container_uid: 0 |
| 202 | container_gid: 0 |
| 203 | uid: 988 |
| 204 | gid: 988 |
| 205 | passwd: |
| 206 | enabled: false |
| 207 | directory: /run/wings/etc |
| 208 | machine_id: |
| 209 | enabled: true |
| 210 | disk_check_concurrency: 2 |
| 211 | disk_check_interval: 150 |
| 212 | full_disk_check_every: 4 |
| 213 | disk_check_use_inotify: true |
| 214 | disk_limiter_mode: none |
| 215 | activity_send_interval: 60 |
| 216 | activity_send_count: 100 |
| 217 | check_permissions_on_boot: true |
| 218 | check_permissions_on_boot_threads: 4 |
| 219 | websocket_log_count: 150 |
| 220 | sftp: |
| 221 | enabled: true |
| 222 | bind_address: 0.0.0.0 |
| 223 | bind_port: ${node.daemonSFTP} |
| 224 | read_only: false |
| 225 | key_algorithm: ssh-ed25519 |
| 226 | disable_password_auth: false |
| 227 | directory_entry_limit: 20000 |
| 228 | directory_entry_send_amount: 500 |
| 229 | limits: |
| 230 | authentication_password_attempts: 3 |
| 231 | authentication_pubkey_attempts: 20 |
| 232 | authentication_cooldown: 60 |
| 233 | shell: |
| 234 | enabled: true |
| 235 | cli: |
| 236 | name: .wings |
| 237 | activity: |
| 238 | log_logins: false |
| 239 | log_file_reads: false |
| 240 | crash_detection: |
| 241 | enabled: true |
| 242 | detect_clean_exit_as_crash: true |
| 243 | timeout: 60 |
| 244 | file_history: |
| 245 | enabled: true |
| 246 | zstd_level: 19 |
| 247 | anchor_interval: 4 |
| 248 | keep_chains: 2 |
| 249 | file_size_cap: 1048576 |
| 250 | per_file_size_cap: 16777216 |
| 251 | per_file_disk_budget: 5242880 |
| 252 | per_server_disk_budget: 209715200 |
| 253 | maintenance_interval: 3600 |
| 254 | backups: |
| 255 | write_limit: 0 |
| 256 | read_limit: 0 |
| 257 | compression_level: best_speed |
| 258 | mounting: |
| 259 | enabled: true |
| 260 | path: .backups |
| 261 | wings: |
| 262 | create_threads: 4 |
| 263 | restore_threads: 4 |
| 264 | archive_format: tar_gz |
| 265 | s3: |
| 266 | create_threads: 4 |
| 267 | part_upload_timeout: 7200 |
| 268 | retry_limit: 10 |
| 269 | transfers: |
| 270 | download_limit: 0 |
| 271 | docker: |
| 272 | socket: /var/run/docker.sock |
| 273 | server_name_in_container_name: false |
| 274 | delete_container_on_stop: true |
| 275 | network: |
| 276 | interface: 172.18.0.1 |
| 277 | disable_interface_binding: false |
| 278 | dns: |
| 279 | - 1.1.1.1 |
| 280 | - 1.0.0.1 |
| 281 | name: pterodactyl_nw |
| 282 | ispn: false |
| 283 | driver: bridge |
| 284 | mode: pterodactyl_nw |
| 285 | is_internal: false |
| 286 | enable_icc: true |
| 287 | network_mtu: 1500 |
| 288 | interfaces: |
| 289 | v4: |
| 290 | subnet: 172.18.0.0/16 |
| 291 | gateway: 172.18.0.1 |
| 292 | v6: |
| 293 | subnet: fdba:17c8:6c99::/64 |
| 294 | gateway: fdba:17c8:6c99::1011 |
| 295 | domainname: '' |
| 296 | registries: {} |
| 297 | tmpfs_size: 100 |
| 298 | container_pid_limit: 5120 |
| 299 | installer_limits: |
| 300 | timeout: 1800 |
| 301 | memory: 1024 |
| 302 | cpu: 100 |
| 303 | overhead: |
| 304 | override: false |
| 305 | default_multiplier: 1.05 |
| 306 | multipliers: {} |
| 307 | userns_mode: '' |
| 308 | log_config: |
| 309 | type: local |
| 310 | config: |
| 311 | compress: 'false' |
| 312 | max-file: '1' |
| 313 | max-size: 5m |
| 314 | mode: non-blocking |
| 315 | throttles: |
| 316 | enabled: true |
| 317 | lines: 2000 |
| 318 | line_reset_interval: 100 |
| 319 | remote: ${appUrl} |
| 320 | remote_headers: {} |
| 321 | remote_query: |
| 322 | timeout: 30 |
| 323 | boot_servers_per_page: 50 |
| 324 | retry_limit: 10 |
| 325 | allowed_mounts: [] |
| 326 | allowed_origins: |
| 327 | - ${appUrl} |
| 328 | allow_cors_private_network: false |
| 329 | ignore_panel_config_updates: false |
| 330 | ignore_panel_wings_upgrades: false`, |
| 331 | }; |
| 332 | }), |
| 333 | |
| 334 | }; |