Replace inline position:fixed alerts with DaisyUI toast toast-end toast-top container in setup.cgi and remotes.cgi for consistent notification styling. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
750 lines
32 KiB
Perl
750 lines
32 KiB
Perl
#!/usr/local/cpanel/3rdparty/bin/perl
|
|
# gniza WHM Plugin — Remote Destination CRUD
|
|
use strict;
|
|
use warnings;
|
|
|
|
use lib '/usr/local/cpanel/whostmgr/docroot/cgi/gniza-whm/lib';
|
|
|
|
use Whostmgr::HTMLInterface ();
|
|
use Cpanel::Form ();
|
|
use File::Copy ();
|
|
use GnizaWHM::Config;
|
|
use GnizaWHM::Validator;
|
|
use GnizaWHM::UI;
|
|
|
|
my $form = Cpanel::Form::parseform();
|
|
my $method = $ENV{'REQUEST_METHOD'} // 'GET';
|
|
my $action = $form->{'action'} // 'list';
|
|
|
|
# Route to handler
|
|
if ($action eq 'test') { handle_test_connection() }
|
|
elsif ($action eq 'add') { handle_add() }
|
|
elsif ($action eq 'edit') { handle_edit() }
|
|
elsif ($action eq 'delete') { handle_delete() }
|
|
else { handle_list() }
|
|
|
|
exit;
|
|
|
|
# ── Test Connection (JSON) ────────────────────────────────────
|
|
|
|
sub handle_test_connection {
|
|
print "Content-Type: application/json\r\n\r\n";
|
|
|
|
my $type = $form->{'remote_type'} || 'ssh';
|
|
|
|
if ($type eq 'ssh') {
|
|
my $host = $form->{'host'} // '';
|
|
my $port = $form->{'port'} || '22';
|
|
my $user = $form->{'user'} || 'root';
|
|
my $auth_method = $form->{'auth_method'} || 'key';
|
|
my $key = $form->{'key'} // '';
|
|
my $password = $form->{'password'} // '';
|
|
|
|
if ($host eq '') {
|
|
print qq({"success":false,"message":"Host is required."});
|
|
exit;
|
|
}
|
|
|
|
if ($auth_method eq 'password') {
|
|
if ($password eq '') {
|
|
print qq({"success":false,"message":"Password is required."});
|
|
exit;
|
|
}
|
|
} else {
|
|
if ($key eq '') {
|
|
print qq({"success":false,"message":"SSH key path is required."});
|
|
exit;
|
|
}
|
|
}
|
|
|
|
my ($ok, $err) = GnizaWHM::UI::test_ssh_connection(
|
|
host => $host,
|
|
port => $port,
|
|
user => $user,
|
|
auth_method => $auth_method,
|
|
key => $key,
|
|
password => $password,
|
|
);
|
|
if ($ok) {
|
|
print qq({"success":true,"message":"SSH connection successful."});
|
|
} else {
|
|
$err //= 'Unknown error';
|
|
$err =~ s/\\/\\\\/g;
|
|
$err =~ s/"/\\"/g;
|
|
$err =~ s/\n/\\n/g;
|
|
print qq({"success":false,"message":"SSH connection failed: $err"});
|
|
}
|
|
}
|
|
elsif ($type eq 's3' || $type eq 'gdrive') {
|
|
my %rclone_args = (type => $type);
|
|
if ($type eq 's3') {
|
|
$rclone_args{s3_access_key_id} = $form->{'S3_ACCESS_KEY_ID'} // '';
|
|
$rclone_args{s3_secret_access_key} = $form->{'S3_SECRET_ACCESS_KEY'} // '';
|
|
$rclone_args{s3_region} = $form->{'S3_REGION'} || 'us-east-1';
|
|
$rclone_args{s3_endpoint} = $form->{'S3_ENDPOINT'} // '';
|
|
$rclone_args{s3_bucket} = $form->{'S3_BUCKET'} // '';
|
|
|
|
if ($rclone_args{s3_access_key_id} eq '' || $rclone_args{s3_secret_access_key} eq '') {
|
|
print qq({"success":false,"message":"S3 access key and secret are required."});
|
|
exit;
|
|
}
|
|
if ($rclone_args{s3_bucket} eq '') {
|
|
print qq({"success":false,"message":"S3 bucket is required."});
|
|
exit;
|
|
}
|
|
} else {
|
|
$rclone_args{gdrive_service_account_file} = $form->{'GDRIVE_SERVICE_ACCOUNT_FILE'} // '';
|
|
$rclone_args{gdrive_root_folder_id} = $form->{'GDRIVE_ROOT_FOLDER_ID'} // '';
|
|
|
|
if ($rclone_args{gdrive_service_account_file} eq '') {
|
|
print qq({"success":false,"message":"Service account file path is required."});
|
|
exit;
|
|
}
|
|
}
|
|
|
|
my ($ok, $err) = GnizaWHM::UI::test_rclone_connection(%rclone_args);
|
|
if ($ok) {
|
|
my $label = $type eq 's3' ? 'S3' : 'Google Drive';
|
|
print qq({"success":true,"message":"$label connection successful."});
|
|
} else {
|
|
$err //= 'Unknown error';
|
|
$err =~ s/\\/\\\\/g;
|
|
$err =~ s/"/\\"/g;
|
|
$err =~ s/\n/\\n/g;
|
|
print qq({"success":false,"message":"Connection failed: $err"});
|
|
}
|
|
}
|
|
else {
|
|
print qq({"success":false,"message":"Unknown remote type."});
|
|
}
|
|
exit;
|
|
}
|
|
|
|
# ── List ─────────────────────────────────────────────────────
|
|
|
|
sub handle_list {
|
|
print "Content-Type: text/html\r\n\r\n";
|
|
Whostmgr::HTMLInterface::defheader('gniza Backup Manager — Remotes', '', '/cgi/gniza-whm/remotes.cgi');
|
|
|
|
print GnizaWHM::UI::page_header('Remote Destinations');
|
|
print GnizaWHM::UI::render_nav('remotes.cgi');
|
|
print GnizaWHM::UI::render_flash();
|
|
|
|
my @remotes = GnizaWHM::UI::list_remotes();
|
|
|
|
print qq{<div class="card bg-base-100 shadow-sm border border-base-300 mb-6">\n<div class="card-body">\n};
|
|
|
|
if (@remotes) {
|
|
print qq{<div class="overflow-x-auto rounded-box border border-base-content/5 bg-base-100"><table class="table">\n};
|
|
print qq{<thead><tr><th>Name</th><th>Type</th><th>Destination</th><th>Retention</th><th>Actions</th></tr></thead>\n};
|
|
print qq{<tbody>\n};
|
|
for my $name (@remotes) {
|
|
my $conf = GnizaWHM::Config::parse(GnizaWHM::UI::remote_conf_path($name), 'remote');
|
|
my $esc_name = GnizaWHM::UI::esc($name);
|
|
my $type = $conf->{REMOTE_TYPE} // 'ssh';
|
|
my $retention = GnizaWHM::UI::esc($conf->{RETENTION_COUNT} // '30');
|
|
|
|
my ($type_label, $dest);
|
|
if ($type eq 's3') {
|
|
$type_label = 'S3';
|
|
$dest = 's3://' . GnizaWHM::UI::esc($conf->{S3_BUCKET} // '');
|
|
} elsif ($type eq 'gdrive') {
|
|
$type_label = 'GDrive';
|
|
my $sa = $conf->{GDRIVE_SERVICE_ACCOUNT_FILE} // '';
|
|
$sa =~ s{.*/}{};
|
|
$dest = 'gdrive:' . GnizaWHM::UI::esc($sa);
|
|
} else {
|
|
$type_label = 'SSH';
|
|
my $host = GnizaWHM::UI::esc($conf->{REMOTE_HOST} // '');
|
|
my $port = GnizaWHM::UI::esc($conf->{REMOTE_PORT} // '22');
|
|
$dest = "$host:$port";
|
|
}
|
|
|
|
print qq{<tr class="hover">};
|
|
print qq{<td><strong>$esc_name</strong></td>};
|
|
print qq{<td><span class="badge badge-sm">$type_label</span></td>};
|
|
print qq{<td>$dest</td><td>$retention</td>};
|
|
print qq{<td>};
|
|
print qq{<div class="flex items-center gap-2">};
|
|
print qq{<a href="remotes.cgi?action=edit&name=$esc_name" class="btn btn-primary btn-sm">Edit</a>};
|
|
print qq{<form method="POST" action="remotes.cgi" class="inline">};
|
|
print qq{<input type="hidden" name="action" value="delete">};
|
|
print qq{<input type="hidden" name="name" value="$esc_name">};
|
|
print GnizaWHM::UI::csrf_hidden_field();
|
|
print qq{<button type="submit" class="btn btn-error btn-sm" onclick="return confirm('Delete remote $esc_name?')">Delete</button>};
|
|
print qq{</form>};
|
|
print qq{</div>};
|
|
print qq{</td>};
|
|
print qq{</tr>\n};
|
|
}
|
|
print qq{</tbody>\n</table></div>\n};
|
|
} else {
|
|
print qq{<p>No remote destinations configured. Add a remote to enable multi-remote backups.</p>\n};
|
|
print qq{<p class="text-xs text-base-content/60 mt-2">Remote configs are stored in <code>/etc/gniza/remotes.d/</code>.</p>\n};
|
|
}
|
|
|
|
print qq{</div>\n</div>\n};
|
|
|
|
print qq{<div class="flex gap-2 mt-4">\n};
|
|
print qq{ <a href="remotes.cgi?action=add" class="btn btn-primary btn-sm">Add Remote</a>\n};
|
|
print qq{</div>\n};
|
|
|
|
print GnizaWHM::UI::page_footer();
|
|
Whostmgr::HTMLInterface::footer();
|
|
}
|
|
|
|
# ── Add ──────────────────────────────────────────────────────
|
|
|
|
sub handle_add {
|
|
my @errors;
|
|
|
|
if ($method eq 'POST') {
|
|
unless (GnizaWHM::UI::verify_csrf_token($form->{'gniza_csrf'})) {
|
|
push @errors, 'Invalid or expired form token. Please try again.';
|
|
}
|
|
|
|
my $name = $form->{'remote_name'} // '';
|
|
my $name_err = GnizaWHM::Validator::validate_remote_name($name);
|
|
push @errors, $name_err if $name_err;
|
|
|
|
if (!@errors && -f GnizaWHM::UI::remote_conf_path($name)) {
|
|
push @errors, "A remote named '$name' already exists.";
|
|
}
|
|
|
|
my %data;
|
|
for my $key (@GnizaWHM::Config::REMOTE_KEYS) {
|
|
$data{$key} = $form->{$key} // '';
|
|
}
|
|
|
|
if (!@errors) {
|
|
my $validation_errors = GnizaWHM::Validator::validate_remote_config(\%data);
|
|
push @errors, @$validation_errors;
|
|
}
|
|
|
|
if (!@errors) {
|
|
my $type = $data{REMOTE_TYPE} || 'ssh';
|
|
my ($conn_ok, $conn_err);
|
|
|
|
if ($type eq 'ssh') {
|
|
($conn_ok, $conn_err) = GnizaWHM::UI::test_ssh_connection(
|
|
host => $data{REMOTE_HOST},
|
|
port => $data{REMOTE_PORT} || '22',
|
|
user => $data{REMOTE_USER} || 'root',
|
|
auth_method => $data{REMOTE_AUTH_METHOD} || 'key',
|
|
key => $data{REMOTE_KEY},
|
|
password => $data{REMOTE_PASSWORD},
|
|
);
|
|
} else {
|
|
my %rclone_args = (type => $type);
|
|
if ($type eq 's3') {
|
|
$rclone_args{s3_access_key_id} = $data{S3_ACCESS_KEY_ID};
|
|
$rclone_args{s3_secret_access_key} = $data{S3_SECRET_ACCESS_KEY};
|
|
$rclone_args{s3_region} = $data{S3_REGION} || 'us-east-1';
|
|
$rclone_args{s3_endpoint} = $data{S3_ENDPOINT};
|
|
$rclone_args{s3_bucket} = $data{S3_BUCKET};
|
|
} else {
|
|
$rclone_args{gdrive_service_account_file} = $data{GDRIVE_SERVICE_ACCOUNT_FILE};
|
|
$rclone_args{gdrive_root_folder_id} = $data{GDRIVE_ROOT_FOLDER_ID};
|
|
}
|
|
($conn_ok, $conn_err) = GnizaWHM::UI::test_rclone_connection(%rclone_args);
|
|
}
|
|
push @errors, "Connection test failed: $conn_err" unless $conn_ok;
|
|
}
|
|
|
|
if (!@errors) {
|
|
# Copy example template then write values
|
|
my $dest = GnizaWHM::UI::remote_conf_path($name);
|
|
my $example = GnizaWHM::UI::remote_example_path();
|
|
if (-f $example) {
|
|
File::Copy::copy($example, $dest)
|
|
or do { push @errors, "Failed to create remote file: $!"; goto RENDER_ADD; };
|
|
}
|
|
my ($ok, $err) = GnizaWHM::Config::write($dest, \%data, \@GnizaWHM::Config::REMOTE_KEYS);
|
|
if ($ok) {
|
|
GnizaWHM::UI::set_flash('success', "Remote '$name' created successfully.");
|
|
print "Status: 302 Found\r\n";
|
|
print "Location: remotes.cgi\r\n\r\n";
|
|
exit;
|
|
} else {
|
|
push @errors, "Failed to save remote config: $err";
|
|
}
|
|
}
|
|
}
|
|
|
|
RENDER_ADD:
|
|
|
|
print "Content-Type: text/html\r\n\r\n";
|
|
Whostmgr::HTMLInterface::defheader('gniza Backup Manager — Add Remote', '', '/cgi/gniza-whm/remotes.cgi');
|
|
|
|
print GnizaWHM::UI::page_header('Add Remote Destination');
|
|
print GnizaWHM::UI::render_nav('remotes.cgi');
|
|
|
|
if (@errors) {
|
|
print GnizaWHM::UI::render_errors(\@errors);
|
|
}
|
|
|
|
# Pre-populate from POST if validation failed, else empty
|
|
my $conf = {};
|
|
if ($method eq 'POST') {
|
|
for my $key (@GnizaWHM::Config::REMOTE_KEYS) {
|
|
$conf->{$key} = $form->{$key} // '';
|
|
}
|
|
}
|
|
my $name_val = GnizaWHM::UI::esc($form->{'remote_name'} // '');
|
|
|
|
render_remote_form($conf, $name_val, 0);
|
|
|
|
print GnizaWHM::UI::page_footer();
|
|
Whostmgr::HTMLInterface::footer();
|
|
}
|
|
|
|
# ── Edit ─────────────────────────────────────────────────────
|
|
|
|
sub handle_edit {
|
|
my $name = $form->{'name'} // '';
|
|
my @errors;
|
|
|
|
# Validate name
|
|
my $name_err = GnizaWHM::Validator::validate_remote_name($name);
|
|
if ($name_err) {
|
|
GnizaWHM::UI::set_flash('error', "Invalid remote name.");
|
|
print "Status: 302 Found\r\n";
|
|
print "Location: remotes.cgi\r\n\r\n";
|
|
exit;
|
|
}
|
|
|
|
my $conf_path = GnizaWHM::UI::remote_conf_path($name);
|
|
unless (-f $conf_path) {
|
|
GnizaWHM::UI::set_flash('error', "Remote '$name' not found.");
|
|
print "Status: 302 Found\r\n";
|
|
print "Location: remotes.cgi\r\n\r\n";
|
|
exit;
|
|
}
|
|
|
|
if ($method eq 'POST') {
|
|
unless (GnizaWHM::UI::verify_csrf_token($form->{'gniza_csrf'})) {
|
|
push @errors, 'Invalid or expired form token. Please try again.';
|
|
}
|
|
|
|
my %data;
|
|
for my $key (@GnizaWHM::Config::REMOTE_KEYS) {
|
|
$data{$key} = $form->{$key} // '';
|
|
}
|
|
|
|
if (!@errors) {
|
|
my $validation_errors = GnizaWHM::Validator::validate_remote_config(\%data);
|
|
push @errors, @$validation_errors;
|
|
}
|
|
|
|
if (!@errors) {
|
|
my $type = $data{REMOTE_TYPE} || 'ssh';
|
|
my ($conn_ok, $conn_err);
|
|
|
|
if ($type eq 'ssh') {
|
|
($conn_ok, $conn_err) = GnizaWHM::UI::test_ssh_connection(
|
|
host => $data{REMOTE_HOST},
|
|
port => $data{REMOTE_PORT} || '22',
|
|
user => $data{REMOTE_USER} || 'root',
|
|
auth_method => $data{REMOTE_AUTH_METHOD} || 'key',
|
|
key => $data{REMOTE_KEY},
|
|
password => $data{REMOTE_PASSWORD},
|
|
);
|
|
} else {
|
|
my %rclone_args = (type => $type);
|
|
if ($type eq 's3') {
|
|
$rclone_args{s3_access_key_id} = $data{S3_ACCESS_KEY_ID};
|
|
$rclone_args{s3_secret_access_key} = $data{S3_SECRET_ACCESS_KEY};
|
|
$rclone_args{s3_region} = $data{S3_REGION} || 'us-east-1';
|
|
$rclone_args{s3_endpoint} = $data{S3_ENDPOINT};
|
|
$rclone_args{s3_bucket} = $data{S3_BUCKET};
|
|
} else {
|
|
$rclone_args{gdrive_service_account_file} = $data{GDRIVE_SERVICE_ACCOUNT_FILE};
|
|
$rclone_args{gdrive_root_folder_id} = $data{GDRIVE_ROOT_FOLDER_ID};
|
|
}
|
|
($conn_ok, $conn_err) = GnizaWHM::UI::test_rclone_connection(%rclone_args);
|
|
}
|
|
push @errors, "Connection test failed: $conn_err" unless $conn_ok;
|
|
}
|
|
|
|
if (!@errors) {
|
|
my ($ok, $err) = GnizaWHM::Config::write($conf_path, \%data, \@GnizaWHM::Config::REMOTE_KEYS);
|
|
if ($ok) {
|
|
GnizaWHM::UI::set_flash('success', "Remote '$name' updated successfully.");
|
|
print "Status: 302 Found\r\n";
|
|
print "Location: remotes.cgi\r\n\r\n";
|
|
exit;
|
|
} else {
|
|
push @errors, "Failed to save remote config: $err";
|
|
}
|
|
}
|
|
}
|
|
|
|
print "Content-Type: text/html\r\n\r\n";
|
|
Whostmgr::HTMLInterface::defheader('gniza Backup Manager — Edit Remote', '', '/cgi/gniza-whm/remotes.cgi');
|
|
|
|
print GnizaWHM::UI::page_header("Edit Remote: " . GnizaWHM::UI::esc($name));
|
|
print GnizaWHM::UI::render_nav('remotes.cgi');
|
|
|
|
if (@errors) {
|
|
print GnizaWHM::UI::render_errors(\@errors);
|
|
}
|
|
|
|
# Load config (or re-use POST data on error)
|
|
my $conf;
|
|
if (@errors && $method eq 'POST') {
|
|
$conf = {};
|
|
for my $key (@GnizaWHM::Config::REMOTE_KEYS) {
|
|
$conf->{$key} = $form->{$key} // '';
|
|
}
|
|
} else {
|
|
$conf = GnizaWHM::Config::parse($conf_path, 'remote');
|
|
}
|
|
|
|
render_remote_form($conf, GnizaWHM::UI::esc($name), 1);
|
|
|
|
print GnizaWHM::UI::page_footer();
|
|
Whostmgr::HTMLInterface::footer();
|
|
}
|
|
|
|
# ── Delete ───────────────────────────────────────────────────
|
|
|
|
sub handle_delete {
|
|
if ($method ne 'POST') {
|
|
print "Status: 302 Found\r\n";
|
|
print "Location: remotes.cgi\r\n\r\n";
|
|
exit;
|
|
}
|
|
|
|
unless (GnizaWHM::UI::verify_csrf_token($form->{'gniza_csrf'})) {
|
|
GnizaWHM::UI::set_flash('error', 'Invalid or expired form token.');
|
|
print "Status: 302 Found\r\n";
|
|
print "Location: remotes.cgi\r\n\r\n";
|
|
exit;
|
|
}
|
|
|
|
my $name = $form->{'name'} // '';
|
|
my $name_err = GnizaWHM::Validator::validate_remote_name($name);
|
|
if ($name_err) {
|
|
GnizaWHM::UI::set_flash('error', 'Invalid remote name.');
|
|
print "Status: 302 Found\r\n";
|
|
print "Location: remotes.cgi\r\n\r\n";
|
|
exit;
|
|
}
|
|
|
|
my $conf_path = GnizaWHM::UI::remote_conf_path($name);
|
|
if (-f $conf_path) {
|
|
unlink $conf_path;
|
|
GnizaWHM::UI::set_flash('success', "Remote '$name' deleted.");
|
|
} else {
|
|
GnizaWHM::UI::set_flash('error', "Remote '$name' not found.");
|
|
}
|
|
|
|
print "Status: 302 Found\r\n";
|
|
print "Location: remotes.cgi\r\n\r\n";
|
|
exit;
|
|
}
|
|
|
|
# ── Shared Form Renderer ────────────────────────────────────
|
|
|
|
sub render_remote_form {
|
|
my ($conf, $name_val, $is_edit) = @_;
|
|
|
|
my $action_val = $is_edit ? 'edit' : 'add';
|
|
my $remote_type = $conf->{REMOTE_TYPE} // 'ssh';
|
|
|
|
print qq{<form method="POST" action="remotes.cgi">\n};
|
|
print qq{<input type="hidden" name="action" value="$action_val">\n};
|
|
print GnizaWHM::UI::csrf_hidden_field();
|
|
|
|
if ($is_edit) {
|
|
print qq{<input type="hidden" name="name" value="$name_val">\n};
|
|
}
|
|
|
|
# Remote name
|
|
print qq{<div class="card bg-base-100 shadow-sm border border-base-300 mb-6">\n<div class="card-body">\n};
|
|
print qq{<h2 class="card-title text-sm">Remote Identity</h2>\n};
|
|
print qq{<div class="flex items-center gap-3 mb-2.5">\n};
|
|
print qq{ <label class="w-44 font-medium text-sm" for="remote_name">Remote Name</label>\n};
|
|
if ($is_edit) {
|
|
print qq{ <input type="text" class="input input-bordered input-sm w-full max-w-xs" value="$name_val" disabled>\n};
|
|
} else {
|
|
print qq{ <input type="text" class="input input-bordered input-sm w-full max-w-xs" id="remote_name" name="remote_name" value="$name_val" required>\n};
|
|
print qq{ <span class="text-xs text-base-content/60 ml-2">Letters, digits, hyphens, underscores</span>\n};
|
|
}
|
|
print qq{</div>\n};
|
|
|
|
# Remote type selector
|
|
my $ssh_checked = ($remote_type eq 'ssh') ? ' checked' : '';
|
|
my $s3_checked = ($remote_type eq 's3') ? ' checked' : '';
|
|
my $gdrive_checked = ($remote_type eq 'gdrive') ? ' checked' : '';
|
|
|
|
print qq{<div class="flex items-center gap-3 mb-2.5">\n};
|
|
print qq{ <label class="w-44 font-medium text-sm">Remote Type</label>\n};
|
|
print qq{ <div class="join">\n};
|
|
print qq{ <input type="radio" name="REMOTE_TYPE" class="join-item btn btn-sm" aria-label="SSH" value="ssh" onchange="gnizaTypeChanged()"$ssh_checked>\n};
|
|
print qq{ <input type="radio" name="REMOTE_TYPE" class="join-item btn btn-sm" aria-label="Amazon S3" value="s3" onchange="gnizaTypeChanged()"$s3_checked>\n};
|
|
print qq{ <input type="radio" name="REMOTE_TYPE" class="join-item btn btn-sm" aria-label="Google Drive" value="gdrive" onchange="gnizaTypeChanged()"$gdrive_checked>\n};
|
|
print qq{ </div>\n};
|
|
print qq{</div>\n};
|
|
|
|
print qq{</div>\n</div>\n};
|
|
|
|
# ── SSH fields ────────────────────────────────────────────
|
|
my $ssh_hidden = ($remote_type ne 'ssh') ? ' hidden' : '';
|
|
|
|
# SSH key guidance (add mode only)
|
|
print qq{<div id="type-ssh-guidance"$ssh_hidden>\n};
|
|
unless ($is_edit) {
|
|
print GnizaWHM::UI::render_ssh_guidance();
|
|
}
|
|
print qq{</div>\n};
|
|
|
|
my $auth_method = $conf->{REMOTE_AUTH_METHOD} // 'key';
|
|
my $key_checked = ($auth_method ne 'password') ? ' checked' : '';
|
|
my $pw_checked = ($auth_method eq 'password') ? ' checked' : '';
|
|
my $key_hidden = ($auth_method eq 'password') ? ' hidden' : '';
|
|
my $pw_hidden = ($auth_method ne 'password') ? ' hidden' : '';
|
|
|
|
print qq{<div id="type-ssh-fields"$ssh_hidden>\n};
|
|
print qq{<div class="card bg-base-100 shadow-sm border border-base-300 mb-6">\n<div class="card-body">\n};
|
|
print qq{<h2 class="card-title text-sm">SSH Connection</h2>\n};
|
|
_field($conf, 'REMOTE_HOST', 'Hostname / IP', 'Required');
|
|
_field($conf, 'REMOTE_PORT', 'SSH Port', 'Default: 22');
|
|
_field($conf, 'REMOTE_USER', 'SSH User', 'Default: root');
|
|
|
|
# Auth method toggle
|
|
print qq{<div class="flex items-center gap-3 mb-2.5">\n};
|
|
print qq{ <label class="w-44 font-medium text-sm">Auth Method</label>\n};
|
|
print qq{ <div class="join">\n};
|
|
print qq{ <input type="radio" name="REMOTE_AUTH_METHOD" class="join-item btn btn-sm" aria-label="SSH Key" value="key" onchange="gnizaAuthChanged()"$key_checked>\n};
|
|
print qq{ <input type="radio" name="REMOTE_AUTH_METHOD" class="join-item btn btn-sm" aria-label="Password" value="password" onchange="gnizaAuthChanged()"$pw_checked>\n};
|
|
print qq{ </div>\n};
|
|
print qq{</div>\n};
|
|
|
|
# Key field
|
|
print qq{<div id="auth-key-field"$key_hidden>\n};
|
|
_field($conf, 'REMOTE_KEY', 'SSH Private Key', 'Absolute path');
|
|
print qq{</div>\n};
|
|
|
|
# Password field
|
|
my $pw_val = GnizaWHM::UI::esc($conf->{REMOTE_PASSWORD} // '');
|
|
print qq{<div id="auth-password-field"$pw_hidden>\n};
|
|
print qq{<div class="flex items-center gap-3 mb-2.5">\n};
|
|
print qq{ <label class="w-44 font-medium text-sm" for="REMOTE_PASSWORD">SSH Password</label>\n};
|
|
print qq{ <input type="password" class="input input-bordered input-sm w-full max-w-xs" id="REMOTE_PASSWORD" name="REMOTE_PASSWORD" value="$pw_val">\n};
|
|
print qq{ <span class="text-xs text-base-content/60 ml-2">Requires sshpass on server</span>\n};
|
|
print qq{</div>\n};
|
|
print qq{</div>\n};
|
|
|
|
print qq{</div>\n</div>\n};
|
|
print qq{</div>\n};
|
|
|
|
# ── S3 fields ─────────────────────────────────────────────
|
|
my $s3_hidden = ($remote_type ne 's3') ? ' hidden' : '';
|
|
|
|
print qq{<div id="type-s3-fields"$s3_hidden>\n};
|
|
print qq{<div class="card bg-base-100 shadow-sm border border-base-300 mb-6">\n<div class="card-body">\n};
|
|
print qq{<h2 class="card-title text-sm">Amazon S3 / S3-Compatible</h2>\n};
|
|
_field($conf, 'S3_ACCESS_KEY_ID', 'Access Key ID', 'Required');
|
|
_password_field($conf, 'S3_SECRET_ACCESS_KEY', 'Secret Access Key', 'Required');
|
|
_field($conf, 'S3_REGION', 'Region', 'Default: us-east-1');
|
|
_field($conf, 'S3_ENDPOINT', 'Custom Endpoint', 'For MinIO, Wasabi, etc.');
|
|
_field($conf, 'S3_BUCKET', 'Bucket Name', 'Required');
|
|
print qq{<p class="text-xs text-base-content/60 mt-2">Requires <code>rclone</code> installed on this server.</p>\n};
|
|
print qq{</div>\n</div>\n};
|
|
print qq{</div>\n};
|
|
|
|
# ── Google Drive fields ───────────────────────────────────
|
|
my $gdrive_hidden = ($remote_type ne 'gdrive') ? ' hidden' : '';
|
|
|
|
print qq{<div id="type-gdrive-fields"$gdrive_hidden>\n};
|
|
print qq{<div class="card bg-base-100 shadow-sm border border-base-300 mb-6">\n<div class="card-body">\n};
|
|
print qq{<h2 class="card-title text-sm">Google Drive</h2>\n};
|
|
_field($conf, 'GDRIVE_SERVICE_ACCOUNT_FILE', 'Service Account JSON', 'Absolute path, required');
|
|
_field($conf, 'GDRIVE_ROOT_FOLDER_ID', 'Root Folder ID', 'Optional');
|
|
print qq{<p class="text-xs text-base-content/60 mt-2">Requires <code>rclone</code> installed on this server.</p>\n};
|
|
print qq{</div>\n</div>\n};
|
|
print qq{</div>\n};
|
|
|
|
# ── Common fields ─────────────────────────────────────────
|
|
print qq{<div class="card bg-base-100 shadow-sm border border-base-300 mb-6">\n<div class="card-body">\n};
|
|
print qq{<h2 class="card-title text-sm">Storage Path</h2>\n};
|
|
_field($conf, 'REMOTE_BASE', 'Remote Base Dir', 'Default: /backups');
|
|
print qq{</div>\n</div>\n};
|
|
|
|
# Transfer
|
|
print qq{<div class="card bg-base-100 shadow-sm border border-base-300 mb-6">\n<div class="card-body">\n};
|
|
print qq{<h2 class="card-title text-sm">Transfer Settings</h2>\n};
|
|
_field($conf, 'BWLIMIT', 'Bandwidth Limit', 'KB/s, 0 = unlimited');
|
|
print qq{<div id="rsync-opts-field"$ssh_hidden>\n};
|
|
_field($conf, 'RSYNC_EXTRA_OPTS', 'Extra rsync Options', 'SSH only');
|
|
print qq{</div>\n};
|
|
print qq{</div>\n</div>\n};
|
|
|
|
# Retention
|
|
print qq{<div class="card bg-base-100 shadow-sm border border-base-300 mb-6">\n<div class="card-body">\n};
|
|
print qq{<h2 class="card-title text-sm">Retention</h2>\n};
|
|
_field($conf, 'RETENTION_COUNT', 'Snapshots to Keep', 'Default: 30');
|
|
print qq{</div>\n</div>\n};
|
|
|
|
# Submit
|
|
print qq{<div class="flex gap-2 mt-4">\n};
|
|
my $btn_label = $is_edit ? 'Save Changes' : 'Create Remote';
|
|
print qq{ <button type="submit" class="btn btn-primary btn-sm">$btn_label</button>\n};
|
|
print qq{ <button type="button" class="btn btn-secondary btn-sm" id="test-conn-btn" onclick="gnizaTestConnection()">Test Connection</button>\n};
|
|
print qq{ <a href="remotes.cgi" class="btn btn-ghost btn-sm">Cancel</a>\n};
|
|
print qq{</div>\n};
|
|
|
|
print qq{</form>\n};
|
|
|
|
print <<'JS';
|
|
<script>
|
|
function gnizaGetType() {
|
|
var radios = document.querySelectorAll('input[name="REMOTE_TYPE"]');
|
|
for (var i = 0; i < radios.length; i++) {
|
|
if (radios[i].checked) return radios[i].value;
|
|
}
|
|
return 'ssh';
|
|
}
|
|
|
|
function gnizaGetAuthMethod() {
|
|
var radios = document.querySelectorAll('input[name="REMOTE_AUTH_METHOD"]');
|
|
for (var i = 0; i < radios.length; i++) {
|
|
if (radios[i].checked) return radios[i].value;
|
|
}
|
|
return 'key';
|
|
}
|
|
|
|
function gnizaTypeChanged() {
|
|
var type = gnizaGetType();
|
|
var sshFields = document.getElementById('type-ssh-fields');
|
|
var sshGuidance = document.getElementById('type-ssh-guidance');
|
|
var s3Fields = document.getElementById('type-s3-fields');
|
|
var gdriveFields = document.getElementById('type-gdrive-fields');
|
|
var rsyncOpts = document.getElementById('rsync-opts-field');
|
|
|
|
sshFields.hidden = (type !== 'ssh');
|
|
sshGuidance.hidden = (type !== 'ssh');
|
|
s3Fields.hidden = (type !== 's3');
|
|
gdriveFields.hidden = (type !== 'gdrive');
|
|
rsyncOpts.hidden = (type !== 'ssh');
|
|
}
|
|
|
|
function gnizaAuthChanged() {
|
|
var method = gnizaGetAuthMethod();
|
|
var keyField = document.getElementById('auth-key-field');
|
|
var pwField = document.getElementById('auth-password-field');
|
|
if (method === 'password') {
|
|
keyField.hidden = true;
|
|
pwField.hidden = false;
|
|
} else {
|
|
keyField.hidden = false;
|
|
pwField.hidden = true;
|
|
}
|
|
}
|
|
|
|
function gnizaTestConnection() {
|
|
var type = gnizaGetType();
|
|
var btn = document.getElementById('test-conn-btn');
|
|
var fd = new FormData();
|
|
fd.append('action', 'test');
|
|
fd.append('remote_type', type);
|
|
|
|
if (type === 'ssh') {
|
|
var host = document.getElementById('REMOTE_HOST').value;
|
|
var port = document.getElementById('REMOTE_PORT').value;
|
|
var user = document.getElementById('REMOTE_USER').value;
|
|
var authMethod = gnizaGetAuthMethod();
|
|
var key = document.getElementById('REMOTE_KEY').value;
|
|
var pw = document.getElementById('REMOTE_PASSWORD').value;
|
|
|
|
if (!host) { gnizaToast('error', 'Host is required.'); return; }
|
|
if (authMethod === 'password' && !pw) { gnizaToast('error', 'Password is required.'); return; }
|
|
if (authMethod === 'key' && !key) { gnizaToast('error', 'SSH key path is required.'); return; }
|
|
|
|
fd.append('host', host);
|
|
fd.append('port', port);
|
|
fd.append('user', user);
|
|
fd.append('auth_method', authMethod);
|
|
fd.append('key', key);
|
|
fd.append('password', pw);
|
|
}
|
|
else if (type === 's3') {
|
|
var keyId = document.getElementById('S3_ACCESS_KEY_ID').value;
|
|
var secret = document.getElementById('S3_SECRET_ACCESS_KEY').value;
|
|
var bucket = document.getElementById('S3_BUCKET').value;
|
|
if (!keyId || !secret) { gnizaToast('error', 'S3 access key and secret are required.'); return; }
|
|
if (!bucket) { gnizaToast('error', 'S3 bucket is required.'); return; }
|
|
fd.append('S3_ACCESS_KEY_ID', keyId);
|
|
fd.append('S3_SECRET_ACCESS_KEY', secret);
|
|
fd.append('S3_REGION', document.getElementById('S3_REGION').value);
|
|
fd.append('S3_ENDPOINT', document.getElementById('S3_ENDPOINT').value);
|
|
fd.append('S3_BUCKET', bucket);
|
|
}
|
|
else if (type === 'gdrive') {
|
|
var saFile = document.getElementById('GDRIVE_SERVICE_ACCOUNT_FILE').value;
|
|
if (!saFile) { gnizaToast('error', 'Service account file path is required.'); return; }
|
|
fd.append('GDRIVE_SERVICE_ACCOUNT_FILE', saFile);
|
|
fd.append('GDRIVE_ROOT_FOLDER_ID', document.getElementById('GDRIVE_ROOT_FOLDER_ID').value);
|
|
}
|
|
|
|
btn.disabled = true;
|
|
btn.innerHTML = '<span class="loading loading-spinner loading-xs"></span> Testing\u2026';
|
|
|
|
fetch('remotes.cgi', { method: 'POST', body: fd })
|
|
.then(function(r) { return r.json(); })
|
|
.then(function(data) {
|
|
gnizaToast(data.success ? 'success' : 'error', data.message);
|
|
})
|
|
.catch(function(err) {
|
|
gnizaToast('error', 'Request failed: ' + err.toString());
|
|
})
|
|
.finally(function() {
|
|
btn.disabled = false;
|
|
btn.innerHTML = 'Test Connection';
|
|
});
|
|
}
|
|
|
|
function gnizaToast(type, msg) {
|
|
var toast = document.getElementById('gniza-toast');
|
|
if (!toast) {
|
|
toast = document.createElement('div');
|
|
toast.id = 'gniza-toast';
|
|
toast.className = 'toast toast-end toast-top';
|
|
toast.style.zIndex = '9999';
|
|
document.body.appendChild(toast);
|
|
}
|
|
var el = document.createElement('div');
|
|
el.className = 'alert alert-' + type;
|
|
el.textContent = msg;
|
|
el.style.transition = 'opacity .3s';
|
|
toast.appendChild(el);
|
|
setTimeout(function() { el.style.opacity = '0'; }, type === 'error' ? 6000 : 3000);
|
|
setTimeout(function() { el.remove(); }, type === 'error' ? 6500 : 3500);
|
|
}
|
|
</script>
|
|
JS
|
|
}
|
|
|
|
sub _field {
|
|
my ($conf, $key, $label, $hint) = @_;
|
|
my $val = GnizaWHM::UI::esc($conf->{$key} // '');
|
|
my $hint_html = $hint ? qq{ <span class="text-xs text-base-content/60 ml-2">$hint</span>} : '';
|
|
print qq{<div class="flex items-center gap-3 mb-2.5">\n};
|
|
print qq{ <label class="w-44 font-medium text-sm" for="$key">$label</label>\n};
|
|
print qq{ <input type="text" class="input input-bordered input-sm w-full max-w-xs" id="$key" name="$key" value="$val">\n};
|
|
print qq{ $hint_html\n} if $hint;
|
|
print qq{</div>\n};
|
|
}
|
|
|
|
sub _password_field {
|
|
my ($conf, $key, $label, $hint) = @_;
|
|
my $val = GnizaWHM::UI::esc($conf->{$key} // '');
|
|
my $hint_html = $hint ? qq{ <span class="text-xs text-base-content/60 ml-2">$hint</span>} : '';
|
|
print qq{<div class="flex items-center gap-3 mb-2.5">\n};
|
|
print qq{ <label class="w-44 font-medium text-sm" for="$key">$label</label>\n};
|
|
print qq{ <input type="password" class="input input-bordered input-sm w-full max-w-xs" id="$key" name="$key" value="$val">\n};
|
|
print qq{ $hint_html\n} if $hint;
|
|
print qq{</div>\n};
|
|
}
|