This commit is contained in:
2025-11-17 18:51:08 +01:00
parent 14d6f9aa73
commit 7fb0d2212a
318 changed files with 35761 additions and 0 deletions

View File

@@ -0,0 +1,19 @@
document.addEventListener("DOMContentLoaded", function(event) {
if (document.hasFocus()) {
// hide reload button if the site reloads automatically
let list = document.getElementsByClassName("reload button");
for (let i = 0; i < list.length; i++) {
// list[i] is a node with the desired class name
list[i].style.display = 'none';
}
// set timeout for reload
setTimeout(function(){
window.location.reload(1);
}, 5000);
} else {
window.addEventListener("beforeunload", function() {
document.getElementById('overlay').classList.add('loading')
});
}
});

View File

@@ -0,0 +1,3 @@
window.addEventListener("beforeunload", function() {
document.getElementById('overlay').classList.add('loading')
});

View File

@@ -0,0 +1,88 @@
document.addEventListener("DOMContentLoaded", function () {
// Hide submit button initially
const optionsFormSubmit = document.getElementById("options-form-submit");
optionsFormSubmit.style.display = 'none';
const communityFormSubmit = document.getElementById("community-form-submit");
communityFormSubmit.style.display = 'none';
// Store initial states for all checkboxes
const initialStateOptionsContainers = {};
const initialStateCommunityContainers = {};
const optionsContainersCheckboxes = document.querySelectorAll("#options-form input[type='checkbox']");
const communityContainersCheckboxes = document.querySelectorAll("#community-form input[type='checkbox']");
optionsContainersCheckboxes.forEach(checkbox => {
initialStateOptionsContainers[checkbox.id] = checkbox.checked; // Use checked property to capture actual initial state
});
communityContainersCheckboxes.forEach(checkbox => {
initialStateCommunityContainers[checkbox.id] = checkbox.checked; // Use checked property to capture actual initial state
});
// Function to compare current states to initial states
function checkForOptionContainerChanges() {
let hasChanges = false;
optionsContainersCheckboxes.forEach(checkbox => {
if (checkbox.checked !== initialStateOptionsContainers[checkbox.id]) {
hasChanges = true;
}
});
// Show or hide submit button based on changes
optionsFormSubmit.style.display = hasChanges ? 'block' : 'none';
}
// Function to compare current states to initial states
function checkForCommunityContainerChanges() {
let hasChanges = false;
communityContainersCheckboxes.forEach(checkbox => {
if (checkbox.checked !== initialStateCommunityContainers[checkbox.id]) {
hasChanges = true;
}
});
// Show or hide submit button based on changes
communityFormSubmit.style.display = hasChanges ? 'block' : 'none';
}
// Event listener to trigger visibility check on each change
optionsContainersCheckboxes.forEach(checkbox => {
checkbox.addEventListener("change", checkForOptionContainerChanges);
});
communityContainersCheckboxes.forEach(checkbox => {
checkbox.addEventListener("change", checkForCommunityContainerChanges);
});
// Custom behaviors for specific options
function handleTalkVisibility() {
const talkRecording = document.getElementById("talk-recording");
if (document.getElementById("talk").checked) {
talkRecording.disabled = false;
} else {
talkRecording.checked = false;
talkRecording.disabled = true;
}
checkForOptionContainerChanges(); // Check changes after toggling Talk Recording
}
function handleDockerSocketProxyWarning() {
if (document.getElementById("docker-socket-proxy").checked) {
alert('⚠️ Warning! Enabling this container comes with possible Security problems since you are exposing the docker socket and all its privileges to the Nextcloud container. Enable this only if you are sure what you are doing!');
}
}
// Initialize event listeners for specific behaviors
document.getElementById("talk").addEventListener('change', handleTalkVisibility);
document.getElementById("docker-socket-proxy").addEventListener('change', handleDockerSocketProxyWarning);
// Initialize talk-recording visibility on page load
handleTalkVisibility(); // Ensure talk-recording is correctly initialized
// Initial call to check for changes
checkForOptionContainerChanges();
checkForCommunityContainerChanges();
});

View File

@@ -0,0 +1,5 @@
document.addEventListener("DOMContentLoaded", function(event) {
// Clamav
let clamav = document.getElementById("clamav");
clamav.disabled = true;
});

View File

@@ -0,0 +1,5 @@
document.addEventListener("DOMContentLoaded", function(event) {
// Collabora
let collabora = document.getElementById("collabora");
collabora.disabled = true;
});

View File

@@ -0,0 +1,7 @@
document.addEventListener("DOMContentLoaded", function(event) {
// Docker socket proxy
let dockerSocketProxy = document.getElementById("docker-socket-proxy");
if (dockerSocketProxy) {
dockerSocketProxy.disabled = true;
}
});

View File

@@ -0,0 +1,5 @@
document.addEventListener("DOMContentLoaded", function(event) {
// Fulltextsearch
let fulltextsearch = document.getElementById("fulltextsearch");
fulltextsearch.disabled = true;
});

View File

@@ -0,0 +1,5 @@
document.addEventListener("DOMContentLoaded", function(event) {
// Imaginary
let imaginary = document.getElementById("imaginary");
imaginary.disabled = true;
});

View File

@@ -0,0 +1,7 @@
document.addEventListener("DOMContentLoaded", function(event) {
// OnlyOffice
let onlyoffice = document.getElementById("onlyoffice");
if (onlyoffice) {
onlyoffice.disabled = true;
}
});

View File

@@ -0,0 +1,4 @@
document.addEventListener("DOMContentLoaded", function(event) {
// Talk-recording
document.getElementById("talk-recording").disabled = true;
});

View File

@@ -0,0 +1,5 @@
document.addEventListener("DOMContentLoaded", function(event) {
// Talk
let talk = document.getElementById("talk");
talk.disabled = true;
});

View File

@@ -0,0 +1,5 @@
document.addEventListener("DOMContentLoaded", function(event) {
// Whiteboard
let whiteboard = document.getElementById("whiteboard");
whiteboard.disabled = true;
});

View File

@@ -0,0 +1,90 @@
"use strict";
function showPassword(id) {
let passwordField = document.getElementById(id);
if (passwordField.type === "password" && passwordField.value !== "") {
passwordField.type = "text";
} else if (passwordField.type === "text" && passwordField.value === "") {
passwordField.type = "password";
}
}
(function (){
let lastError;
function showError(message) {
const body = document.getElementsByTagName('body')[0]
const toast = document.createElement("div")
toast.className = "toast error"
toast.prepend(message)
if (lastError) {
lastError.remove()
}
lastError = toast
body.prepend(toast)
setTimeout(toast.remove.bind(toast), 10000)
}
function handleEvent(e) {
const xhr = e.target;
if (xhr.status === 201) {
window.location.replace(xhr.getResponseHeader('Location'));
} else if (xhr.status === 422) {
disableSpinner()
showError(xhr.response);
} else if (xhr.status === 500) {
showError("Server error. Please check the mastercontainer logs for details. This page will reload after 10s automatically. Then you can check the mastercontainer logs.");
// Reload after 10s since it is expected that the updated view is shown (e.g. after starting containers)
setTimeout(function(){
window.location.reload(1);
}, 10000);
} else {
// If the responose is not one of the above, we should reload to show the latest content
window.location.reload(1);
}
}
function enableSpinner() {
document.getElementById('overlay').classList.add('loading');
}
function disableSpinner() {
document.getElementById('overlay').classList.remove('loading');
}
function initForm(form) {
function submit(event)
{
if (lastError) {
lastError.remove()
}
let xhr = new XMLHttpRequest();
xhr.addEventListener('load', handleEvent);
xhr.addEventListener('error', () => showError("Failed to talk to server."));
xhr.addEventListener('error', () => disableSpinner());
xhr.open(form.method, form.getAttribute("action"));
xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
enableSpinner();
xhr.send(new URLSearchParams(new FormData(form)));
event.preventDefault();
}
form.onsubmit = submit;
console.info(form);
}
function initForms() {
const forms = document.querySelectorAll('form.xhr')
console.info("Making " + forms.length + " form(s) use XHR.");
for (const form of forms) {
initForm(form);
}
}
if (document.readyState === 'loading') {
// Loading hasn't finished yet
document.addEventListener('DOMContentLoaded', initForms);
} else { // `DOMContentLoaded` has already fired
initForms();
}
})()

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

View File

@@ -0,0 +1,4 @@
<svg id="nextcloud-logo" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 142 100" width="142" height="100">
<g id="logo" stroke="currentColor" fill="none" stroke-width="11" transform="scale(1.109)"><circle cx="20" cy="32" r="13"/><circle cx="64" cy="32" r="23"/><circle cx="108" cy="32" r="13"/></g>
<g id="Nextcloud" fill="currentColor" transform="translate(-3.4, -3.4) scale(1.17)"><path d="M15.4,67.4c-0.4,0-0.5,0.2-0.5,0.6v14.6c0,0.4,0.2,0.5,0.5,0.5h0.4c0.4,0,0.5-0.2,0.5-0.5V70.4 l7.9,12.3c0,0.1,0.1,0.1,0.1,0.1c0,0,0,0,0,0c0,0,0.1,0,0.1,0.1c0,0,0,0,0.1,0c0,0,0,0,0,0c0.1,0,0.1,0,0.2,0h0.4 c0.4,0,0.5-0.2,0.5-0.5V68c0-0.4-0.2-0.6-0.5-0.6h-0.4c-0.4,0-0.6,0.2-0.6,0.6v12.1l-7.9-12.3c0,0-0.1-0.1-0.1-0.1 c-0.1-0.1-0.2-0.2-0.4-0.2L15.4,67.4z M110.8,67.6c-0.4,0-0.2,0.2-0.2,0.6v5c0,0.5,0,0.9,0,0.9h0c0,0-1-2.2-3.6-2.2 c-2.9,0-5,2.3-4.9,5.7c0,3.4,1.9,5.8,4.8,5.8c2.9,0,3.8-2.3,3.8-2.3h0.1c0,0-0.1,0.3-0.1,0.7v0.9c0,0.4,0.2,0.5,0.6,0.5h0.4 c0.4,0,0.5-0.2,0.5-0.6V68.2c0-0.4-0.6-0.6-0.9-0.6H110.8z M71.8,67.7c-0.4,0-0.1,0.2-0.1,0.6v12.3c0,2.4,1.6,2.7,2.5,2.7 c0.4,0,0.6-0.2,0.6-0.6v-0.4c0-0.4-0.2-0.5-0.5-0.5c-0.5-0.1-1.2-0.2-1.2-1.6v-12c0-0.4-0.6-0.6-0.9-0.6L71.8,67.7z M53.8,69 c-0.4,0-0.6,0.2-0.6,0.6v2.6v1.3v5.7c0,2.6,1.5,4.1,3.9,4.1c0.5,0,0.6-0.1,0.6-0.5v-0.3c0-0.4-0.1-0.5-0.6-0.6 c-0.9-0.1-2.4-0.4-2.4-2.9v-5.5h2.3c0.4,0,0.6-0.1,0.6-0.5v-0.2c0-0.4-0.2-0.6-0.6-0.6h-2.3v-2.6c0-0.4-0.1-0.6-0.5-0.6L53.8,69z M33.8,71.8c-3,0-5.4,2.2-5.5,5.8c0,3.4,2.5,5.8,5.8,5.8c1.8,0,3.1-0.8,3.7-1.2c0.3-0.2,0.3-0.5,0.2-0.7l-0.2-0.2 c-0.2-0.3-0.4-0.4-0.7-0.2c-0.5,0.4-1.5,1-2.9,1c-2.3,0-4.2-1.6-4.3-4.4h8c0.3,0,0.6-0.3,0.6-0.6C38.4,73.9,36.8,71.8,33.8,71.8z M65,71.8c-3.3,0-5.8,2.4-5.8,5.8c0,3.4,2.5,5.8,5.8,5.8c2,0,3.4-1,3.9-1.4c0.3-0.3,0.3-0.5,0.1-0.8L68.8,81 c-0.2-0.3-0.4-0.4-0.7-0.2C67.6,81.3,66.6,82,65,82c-2.4,0-4.3-1.8-4.3-4.4c0-2.7,1.9-4.5,4.3-4.5c1.3,0,2.3,0.7,2.8,1 c0.3,0.2,0.6,0.2,0.8-0.1l0.2-0.3c0.3-0.3,0.2-0.6-0.1-0.8C68.1,72.6,66.9,71.8,65,71.8L65,71.8z M81.9,71.8 c-3.2,0-5.8,2.5-5.8,5.7c0,3.3,2.6,5.8,5.8,5.8c3.2,0,5.8-2.5,5.8-5.8C87.8,74.3,85.1,71.8,81.9,71.8z M49.5,72 c-0.1,0-0.2,0.1-0.4,0.2l-2,2.4l-1.5,1.8l-2.3-2.7L42,72.2c-0.1-0.1-0.2-0.2-0.4-0.2c-0.1,0-0.3,0-0.4,0.2l-0.3,0.3 c-0.3,0.2-0.3,0.5,0,0.7l2,2.4l1.7,2l-2.5,2.9c0,0,0,0,0,0L40.9,82c-0.2,0.3-0.2,0.6,0.1,0.8l0.3,0.3c0.3,0.2,0.5,0.2,0.7-0.1 l2-2.4l1.5-1.8l2.3,2.7c0,0,0,0,0,0l1.2,1.5c0.2,0.3,0.5,0.3,0.8,0.1l0.3-0.3c0.3-0.2,0.3-0.5,0-0.7l-2-2.4l-1.7-2l2.5-2.9 c0,0,0,0,0,0l1.2-1.5c0.2-0.3,0.2-0.6-0.1-0.8l-0.3-0.3C49.7,72,49.6,71.9,49.5,72L49.5,72z M90.7,72c-0.4,0-0.5,0.2-0.5,0.6v6.5 c0,2.9,2.1,4.3,4.7,4.3c2.6,0,4.7-1.4,4.7-4.3v-6.5c0.1-0.4-0.1-0.6-0.5-0.6h-0.4c-0.4,0-0.6,0.2-0.6,0.6v6.1 c0,1.7-1.1,3.3-3.3,3.3c-2.1,0-3.3-1.6-3.3-3.3v-6.1c0-0.4-0.2-0.6-0.6-0.6L90.7,72z M33.8,73c1.6,0,3,1.2,3.1,3.5h-6.9 C30.3,74.3,31.9,73,33.8,73z M81.9,73.1c2.4,0,4.3,1.9,4.3,4.4c0,2.6-1.9,4.5-4.3,4.5c-2.4,0-4.3-2-4.3-4.5 C77.6,75.1,79.6,73.1,81.9,73.1z M107.1,73.1c2.4,0,3.5,2.2,3.5,4.4c0,3.2-1.7,4.5-3.6,4.5c-2.1,0-3.5-1.8-3.5-4.5 C103.5,74.8,105.1,73.1,107.1,73.1z"/></g>
</svg>

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

@@ -0,0 +1,198 @@
<?php
declare(strict_types=1);
// increase memory limit to 2GB
ini_set('memory_limit', '2048M');
// set max execution time to 2h just in case of a very slow internet connection
ini_set('max_execution_time', '7200');
// Log whole log messages
ini_set('log_errors_max_len', '0');
use DI\Container;
use Slim\Csrf\Guard;
use Slim\Factory\AppFactory;
use Slim\Views\Twig;
use Slim\Views\TwigMiddleware;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
require __DIR__ . '/../vendor/autoload.php';
$container = \AIO\DependencyInjection::GetContainer();
$dataConst = $container->get(\AIO\Data\DataConst::class);
ini_set('session.save_path', $dataConst->GetSessionDirectory());
// Auto logout on browser close
ini_set('session.cookie_lifetime', '0');
# Keep session for 24h max
ini_set('session.gc_maxlifetime', '86400');
// Create app
AppFactory::setContainer($container);
$app = AppFactory::create();
$responseFactory = $app->getResponseFactory();
// Register Middleware On Container
$container->set(Guard::class, function () use ($responseFactory) {
$guard = new Guard($responseFactory);
$guard->setPersistentTokenMode(true);
return $guard;
});
// Register Middleware To Be Executed On All Routes
session_start();
$app->add(Guard::class);
// Create Twig
$twig = Twig::create(__DIR__ . '/../templates/', ['cache' => false]);
$app->add(TwigMiddleware::create($app, $twig));
$twig->addExtension(new \AIO\Twig\CsrfExtension($container->get(Guard::class)));
// Auth Middleware
$app->add(new \AIO\Middleware\AuthMiddleware($container->get(\AIO\Auth\AuthManager::class)));
// API
$app->post('/api/docker/watchtower', AIO\Controller\DockerController::class . ':StartWatchtowerContainer');
$app->get('/api/docker/getwatchtower', AIO\Controller\DockerController::class . ':StartWatchtowerContainer');
$app->post('/api/docker/start', AIO\Controller\DockerController::class . ':StartContainer');
$app->post('/api/docker/backup', AIO\Controller\DockerController::class . ':StartBackupContainerBackup');
$app->post('/api/docker/backup-check', AIO\Controller\DockerController::class . ':StartBackupContainerCheck');
$app->post('/api/docker/backup-check-repair', AIO\Controller\DockerController::class . ':StartBackupContainerCheckRepair');
$app->post('/api/docker/backup-test', AIO\Controller\DockerController::class . ':StartBackupContainerTest');
$app->post('/api/docker/restore', AIO\Controller\DockerController::class . ':StartBackupContainerRestore');
$app->post('/api/docker/stop', AIO\Controller\DockerController::class . ':StopContainer');
$app->get('/api/docker/logs', AIO\Controller\DockerController::class . ':GetLogs');
$app->post('/api/auth/login', AIO\Controller\LoginController::class . ':TryLogin');
$app->get('/api/auth/getlogin', AIO\Controller\LoginController::class . ':GetTryLogin');
$app->post('/api/auth/logout', AIO\Controller\LoginController::class . ':Logout');
$app->post('/api/configuration', \AIO\Controller\ConfigurationController::class . ':SetConfig');
// Views
$app->get('/containers', function (Request $request, Response $response, array $args) use ($container) {
$view = Twig::fromRequest($request);
$view->addExtension(new \AIO\Twig\ClassExtension());
/** @var \AIO\Data\ConfigurationManager $configurationManager */
$configurationManager = $container->get(\AIO\Data\ConfigurationManager::class);
/** @var \AIO\Docker\DockerActionManager $dockerActionManger */
$dockerActionManger = $container->get(\AIO\Docker\DockerActionManager::class);
/** @var \AIO\Controller\DockerController $dockerController */
$dockerController = $container->get(\AIO\Controller\DockerController::class);
$dockerActionManger->ConnectMasterContainerToNetwork();
$dockerController->StartDomaincheckContainer();
// Check if bypass_mastercontainer_update is provided on the URL, a special developer mode to bypass a mastercontainer update and use local image.
$params = $request->getQueryParams();
$bypass_mastercontainer_update = isset($params['bypass_mastercontainer_update']);
$bypass_container_update = isset($params['bypass_container_update']);
return $view->render($response, 'containers.twig', [
'domain' => $configurationManager->GetDomain(),
'apache_port' => $configurationManager->GetApachePort(),
'borg_backup_host_location' => $configurationManager->GetBorgBackupHostLocation(),
'borg_remote_repo' => $configurationManager->GetBorgRemoteRepo(),
'borg_public_key' => $configurationManager->GetBorgPublicKey(),
'nextcloud_password' => $configurationManager->GetAndGenerateSecret('NEXTCLOUD_PASSWORD'),
'containers' => (new \AIO\ContainerDefinitionFetcher($container->get(\AIO\Data\ConfigurationManager::class), $container))->FetchDefinition(),
'borgbackup_password' => $configurationManager->GetAndGenerateSecret('BORGBACKUP_PASSWORD'),
'is_mastercontainer_update_available' => ( $bypass_mastercontainer_update ? false : $dockerActionManger->IsMastercontainerUpdateAvailable() ),
'has_backup_run_once' => $configurationManager->hasBackupRunOnce(),
'is_backup_container_running' => $dockerActionManger->isBackupContainerRunning(),
'backup_exit_code' => $dockerActionManger->GetBackupcontainerExitCode(),
'is_instance_restore_attempt' => $configurationManager->isInstanceRestoreAttempt(),
'borg_backup_mode' => $configurationManager->GetBorgBackupMode(),
'was_start_button_clicked' => $configurationManager->wasStartButtonClicked(),
'has_update_available' => $dockerActionManger->isAnyUpdateAvailable(),
'last_backup_time' => $configurationManager->GetLastBackupTime(),
'backup_times' => $configurationManager->GetBackupTimes(),
'current_channel' => $dockerActionManger->GetCurrentChannel(),
'is_clamav_enabled' => $configurationManager->isClamavEnabled(),
'is_onlyoffice_enabled' => $configurationManager->isOnlyofficeEnabled(),
'is_collabora_enabled' => $configurationManager->isCollaboraEnabled(),
'is_talk_enabled' => $configurationManager->isTalkEnabled(),
'borg_restore_password' => $configurationManager->GetBorgRestorePassword(),
'daily_backup_time' => $configurationManager->GetDailyBackupTime(),
'is_daily_backup_running' => $configurationManager->isDailyBackupRunning(),
'timezone' => $configurationManager->GetTimezone(),
'skip_domain_validation' => $configurationManager->shouldDomainValidationBeSkipped(),
'talk_port' => $configurationManager->GetTalkPort(),
'collabora_dictionaries' => $configurationManager->GetCollaboraDictionaries(),
'collabora_additional_options' => $configurationManager->GetAdditionalCollaboraOptions(),
'automatic_updates' => $configurationManager->areAutomaticUpdatesEnabled(),
'is_backup_section_enabled' => $configurationManager->isBackupSectionEnabled(),
'is_imaginary_enabled' => $configurationManager->isImaginaryEnabled(),
'is_fulltextsearch_enabled' => $configurationManager->isFulltextsearchEnabled(),
'additional_backup_directories' => $configurationManager->GetAdditionalBackupDirectoriesString(),
'nextcloud_datadir' => $configurationManager->GetNextcloudDatadirMount(),
'nextcloud_mount' => $configurationManager->GetNextcloudMount(),
'nextcloud_upload_limit' => $configurationManager->GetNextcloudUploadLimit(),
'nextcloud_max_time' => $configurationManager->GetNextcloudMaxTime(),
'nextcloud_memory_limit' => $configurationManager->GetNextcloudMemoryLimit(),
'is_dri_device_enabled' => $configurationManager->isDriDeviceEnabled(),
'is_nvidia_gpu_enabled' => $configurationManager->isNvidiaGpuEnabled(),
'is_talk_recording_enabled' => $configurationManager->isTalkRecordingEnabled(),
'is_docker_socket_proxy_enabled' => $configurationManager->isDockerSocketProxyEnabled(),
'is_whiteboard_enabled' => $configurationManager->isWhiteboardEnabled(),
'community_containers' => $configurationManager->listAvailableCommunityContainers(),
'community_containers_enabled' => $configurationManager->GetEnabledCommunityContainers(),
'bypass_container_update' => $bypass_container_update,
]);
})->setName('profile');
$app->get('/login', function (Request $request, Response $response, array $args) use ($container) {
$view = Twig::fromRequest($request);
/** @var \AIO\Docker\DockerActionManager $dockerActionManger */
$dockerActionManger = $container->get(\AIO\Docker\DockerActionManager::class);
return $view->render($response, 'login.twig', [
'is_login_allowed' => $dockerActionManger->isLoginAllowed(),
]);
});
$app->get('/setup', function (Request $request, Response $response, array $args) use ($container) {
$view = Twig::fromRequest($request);
/** @var \AIO\Data\Setup $setup */
$setup = $container->get(\AIO\Data\Setup::class);
if(!$setup->CanBeInstalled()) {
return $view->render(
$response,
'already-installed.twig'
);
}
return $view->render(
$response,
'setup.twig',
[
'password' => $setup->Setup(),
]
);
});
// Auth Redirector
$app->get('/', function (\Psr\Http\Message\RequestInterface $request, Response $response, array $args) use ($container) {
/** @var \AIO\Auth\AuthManager $authManager */
$authManager = $container->get(\AIO\Auth\AuthManager::class);
/** @var \AIO\Data\Setup $setup */
$setup = $container->get(\AIO\Data\Setup::class);
if($setup->CanBeInstalled()) {
return $response
->withHeader('Location', '/setup')
->withStatus(302);
}
if($authManager->IsAuthenticated()) {
return $response
->withHeader('Location', '/containers')
->withStatus(302);
} else {
return $response
->withHeader('Location', '/login')
->withStatus(302);
}
});
$errorMiddleware = $app->addErrorMiddleware(false, true, true);
$app->run();

View File

@@ -0,0 +1,2 @@
User-agent: *
Disallow: /

View File

@@ -0,0 +1,12 @@
const channel = new BroadcastChannel('tab')
channel.postMessage('second-tab')
// note that listener is added after posting the message
channel.addEventListener('message', (msg) => {
if (msg.data === 'second-tab') {
// message received from 2nd tab
document.getElementById('overlay').classList.add('loading')
alert('Cannot open multiple instances. You can use AIO here by reloading the page.')
}
});

View File

@@ -0,0 +1,551 @@
:root {
--color-nextcloud-blue: #0082c9;
--color-nextcloud-logo: var(--color-nextcloud-blue);
--color-main-background: white;
--color-input-background: white;
--color-main-text: black;
--color-main-border: black;
--color-main-border-hover: var(--color-main-border);
--color-error: #db0606;
--color-error-hover: #df2525;
--color-error-text: #c20505;
--color-success: #46ba61;
--color-running: #ffd000;
--color-primary-element: #00679e;
--color-primary-element-hover: #005a8a;
--color-primary-element-text: #ffffff;
--color-primary-element-light: #e5eff5;
--color-primary-element-light-hover: #dbe4ea;
--color-primary-element-light-text: #00293f;
--color-border-maxcontrast: #7d7d7d;
--color-loader: #f3f3f3;
--color-disabled: #d3d3d3; /* light gray background for disabled checkboxes */
--color-border-disabled: #a9a9a9; /* darker gray border for disabled checkboxes */
--color-text-disabled: #a9a9a9; /* matching label text color for disabled checkboxes */
--border: .5px;
--border-hover: 2px;
--border-radius: 7px;
--border-radius-large: 12px;
--default-font-size: 13px;
--checkbox-size: 16px;
--max-width: 500px;
--container-top-margin: 20px;
--container-bottom-margin: 20px;
--container-padding: 2px;
--container-height-calculation-difference: calc(var(--container-top-margin) + var(--container-bottom-margin));
--main-height-calculation-difference: calc(var(--container-height-calculation-difference) + calc(var(--container-padding) * 2));
--main-padding: 50px;
}
/* Breakpoint calculation: 500px (max-width) + 100px (main-padding * 2) + 200px (additional space) = 800px
Note: Unfortunately, it's not possible to calculate this dynamically using CSS variables in media queries */
@media only screen and (max-width: 800px) {
:root {
--container-top-margin: 50px;
--container-bottom-margin: 0px;
}
}
[data-theme="dark"] {
--color-main-background: #171717;
--color-input-background: #ebebeb;
--color-main-text: #ebebeb;
--color-nextcloud-logo: var(--color-main-text);
--color-main-border: var(--color-border-maxcontrast);
--color-main-border-hover: var(--color-main-text);
--color-error: #ff3333;
--color-error-hover: #ff6666;
--color-error-text: #ff8080;
--color-primary-element:#0091f2;
--color-primary-element-hover:#079cff;
--color-primary-element-text:#000000;
--color-primary-element-light:#14232c;
--color-primary-element-light-hover:#1e2d35;
--color-primary-element-light-text:#99d3f9;
--color-loader: var(--color-border-maxcontrast);
--border-hover: var(--border);
}
html, body {
padding: 0;
margin: 0;
font-family: system-ui, -apple-system, 'Segoe UI', Roboto, Oxygen-Sans, Cantarell, Ubuntu, 'Helvetica Neue', 'Noto Sans', 'Liberation Sans', Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
background-color: var(--color-main-background);
color: var(--color-main-text);
}
a {
text-decoration: none;
color: var(--color-primary-element);
}
a:hover {
color: var(--color-primary-element-hover);
}
a.button,
input[type="submit"] {
padding: 8px 16px;
width: auto;
height: 34px;
cursor: pointer;
background-color: var(--color-primary-element);
font-weight: bold;
border-radius: var(--border-radius);
margin: 3px 3px 3px 0;
font-size: var(--default-font-size);
color: var(--color-primary-element-text);
border: none;
outline: none;
}
a.button:focus,
input[type="submit"]:focus {
outline: 2px solid var(--color-main-border);
}
a.button:hover,
input[type="submit"]:hover {
background-color: var(--color-primary-element-hover);
}
a.button.light:hover,
input[type="submit"].light:hover {
background-color: var(--color-primary-element-light);
color: var(--color-primary-element-light-text);
}
a.button.light,
input[type="submit"].light {
background-color: var(--color-primary-element-light);
}
a.button.error,
input[type="submit"].error {
background-color: var(--color-error);
}
a.button.error:hover,
input[type="submit"].error:hover {
background-color: var(--color-error-hover);
}
summary {
cursor: pointer;
}
ul {
list-style: none;
padding: 0;
}
li {
padding-bottom: 5px;
text-indent: 0;
padding-left: 0;
}
span.error {
background-color: var(--color-error);
}
div.toast.error {
border-left-color: var(--color-error);
}
.status {
display: inline-block;
height: var(--checkbox-size);
width: var(--checkbox-size);
vertical-align: text-bottom;
border-radius: 50%
}
span.success {
background-color: var(--color-success);
}
span.running {
background-color: var(--color-running);
}
div.toast.success {
border-left-color: var(--color-success);
}
div.toast {
border-left: 3px solid;
right: 10px;
min-width: 200px;
box-shadow: 0 0 6px 0 rgba(77, 77, 77, 0.3);
padding: 12px;
margin-top: 45px;
position: fixed;
z-index: 1000;
border-radius: var(--border-radius);
background: var(--color-main-background) none;
color: var(--color-main-text);
}
.nextcloud-logo {
margin-left: auto;
margin-right: auto;
display: block;
color: var(--color-nextcloud-logo);
}
.fallback-text {
display: none;
}
svg:not(:has(use)) .fallback-text {
display: block;
}
.login {
padding: 50px;
background-color: var(--color-main-background);
color: var(--color-main-text);
width: 500px;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
border-radius: var(--border-radius-large);
}
.login > .monospace {
font-family: monospace, monospace, system-ui, -apple-system, 'Segoe UI', Roboto, Oxygen-Sans, Cantarell, Ubuntu, 'Helvetica Neue', 'Noto Sans', 'Liberation Sans', Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
font-size: 17px;
}
.login > form > input[type="password"],
.login > form > input[type="text"],
.login > form > input[type="submit"] {
width: 100%;
}
.login > img {
margin-left: auto;
margin-right: auto;
display: block;
}
.login a.button,
.login input[type="submit"] {
margin-left: auto;
margin-right: auto;
display: block;
text-align: center;
padding: 0px;
align-content: center;
}
.wrapper {
min-height: 100dvh;
min-width: 100vw;
position: fixed;
width: 100vw;
background-image: url("img/jo-myoung-hee-fluid.webp");
background-position: center;
background-repeat: no-repeat;
background-size: cover;
box-sizing: border-box;
overflow: hidden;
}
html[data-theme="dark"] .wrapper {
background-image: url("img/jo-myoung-hee-fluid-dark.webp");
}
form {
margin: 0;
}
input[type="text"],
input[type="password"],
select {
padding-left: 8px;
padding-right: 8px;
height: 34px;
margin-bottom: 15px;
border-radius: var(--border-radius);
border: var(--border) solid var(--color-border-maxcontrast);
background: var(--color-main-background);
color: var(--color-main-text);
}
input[type="text"]:hover,
input[type="password"]:hover,
select:hover {
border: var(--border-hover) solid var(--color-main-border-hover);
}
textarea {
border-radius: var(--border-radius);
border: .5px solid var(--color-main-border);
max-width: 100%;
}
input[type="text"]:focus,
input[type="password"]:focus,
textarea:focus,
select:focus {
border: 1px solid var(--color-main-border);
}
/* Scroll bar for dark mode */
html[data-theme="dark"] ::-webkit-scrollbar {
width: 8px; /* Width of the scroll bar */
}
html[data-theme="dark"] ::-webkit-scrollbar-thumb {
background-color: #444; /* Dark mode scrollbar thumb color */
border-radius: 4px; /* Rounded corners for the thumb */
}
html[data-theme="dark"] ::-webkit-scrollbar-track {
background-color: #333; /* Dark mode scrollbar track color */
}
/* Scroll bar for light mode */
::-webkit-scrollbar {
width: 8px; /* Width of the scroll bar */
}
::-webkit-scrollbar-thumb {
background-color: #888; /* Light mode scrollbar thumb color */
border-radius: 4px; /* Rounded corners for the thumb */
}
::-webkit-scrollbar-track {
background-color: #f0f0f0; /* Light mode scrollbar track color */
}
.container {
margin: var(--container-top-margin) auto var(--container-bottom-margin) auto;
padding: var(--container-padding);
max-width: calc(var(--max-width) + calc(var(--main-padding) * 2) + 8px);
background-color: var(--color-main-background);
border-radius: var(--border-radius-large);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
max-height: calc(100dvh - var(--container-height-calculation-difference));
overflow: hidden;
}
main {
padding-left: var(--main-padding);
padding-right: var(--main-padding);
background-color: transparent; /* transparent, since color comes from outer container */
color: var(--color-main-text);
max-height: calc(100dvh - var(--main-height-calculation-difference));
overflow-y: auto;
box-sizing: border-box;
word-break: break-word;
max-width: calc(var(--max-width) + calc(var(--main-padding) * 2));
margin: 0 auto;
padding-bottom: var(--main-padding);
}
.logo {
color: white;
height: 50px;
width: 62px;
position: absolute;
left: 12px;
top: 1px;
bottom: 1px;
}
header {
position: fixed;
top: 0;
width: 100%;
background-color: transparent;
height: 50px;
justify-content: space-between;
align-items: center;
display: flex;
padding: 0 20px;
z-index: 1000;
}
header > form {
margin-left: auto;
margin-right: 30px;
}
/* Standard styling for enabled checkboxes */
input[type="checkbox"]:not(:disabled) {
width: var(--checkbox-size);
height: var(--checkbox-size);
-webkit-appearance: none; /* remove default styling */
-moz-appearance: none;
appearance: none;
border: 1px solid var(--color-primary-element);
border-radius: 2px;
cursor: pointer;
position: relative;
vertical-align: middle; /* align checkbox vertically with text */
margin-top: -1px; /* adjust for better alignment */
}
/* Hover effects for enabled checkboxes */
input[type="checkbox"]:not(:disabled):hover {
border-color: var(--color-primary-element-hover);
}
/* Checkmark styling for enabled checkboxes */
input[type="checkbox"]:checked:not(:disabled) {
background-color: var(--color-primary-element);
border-color: var(--color-border-maxcontrast);
}
input[type="checkbox"]:checked:not(:disabled)::after {
content: ''; /* Creates a pseudo-element for the checkmark */
position: absolute; /* Positions it absolutely */
left: 4px; /* Positioning of the checkmark */
top: 0; /* Positioning of the checkmark */
width: 4px; /* Width of the checkmark */
height: 9px; /* Height of the checkmark */
border: solid white; /* Color of the checkmark */
border-width: 0 2px 3px 0; /* Creates the checkmark shape */
transform: rotate(45deg); /* Rotates to form a checkmark */
}
/* Styling for disabled checkboxes (grayed out, no hover, no pointer) */
input[type="checkbox"]:disabled:not(:checked) {
background-color: var(--color-disabled);
border-color: var(--color-border-disabled);
cursor: default;
opacity: 0.5; /* Makes the checkbox appear faded */
}
/* Styling for disabled checked checkboxes (no pointer) */
input[type="checkbox"]:disabled:checked {
cursor: default;
}
input[type="checkbox"]:disabled:hover {
border-color: var(--color-border-disabled); /* Keeps disabled state without hover effect */
}
/* General Label styling */
label {
cursor: pointer;
margin-left: 4px;
line-height: var(--checkbox-size);
}
/* Label cursor for disabled checkboxes */
input[type="checkbox"]:disabled + label {
cursor: default;
}
/* Label styling for disabled, not checked checkboxes */
input[type="checkbox"]:disabled:not(:checked) + label {
color: var(--color-text-disabled);
}
.loading {
color: grey;
}
#overlay {
position: fixed; /* Sit on top of the page content */
display: none; /* Hidden by default */
width: 100%; /* Full width (cover the whole page) */
height: 100%; /* Full height (cover the whole page) */
top: 0;
left: 0;
background-color: rgba(0, 0, 0, 0.5); /* Black background with opacity */
z-index: 2;
}
#overlay.loading {
display: block;
}
.loader {
border: 16px solid var(--color-loader);
border-radius: 50%;
border-top: 16px solid var(--color-nextcloud-blue);
width: 120px;
height: 120px;
-webkit-animation: spin 2s linear infinite; /* Safari */
animation: spin 2s linear infinite;
position: absolute;
top: calc(50% - 60px);
left: calc(50% - 60px);
}
/* Safari */
@-webkit-keyframes spin {
0% { -webkit-transform: rotate(0deg); }
100% { -webkit-transform: rotate(360deg); }
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* General theme button styling */
#theme-toggle {
position: fixed; /* Keep the button in the same position */
right: 30px; /* Adjust the distance from the right */
bottom: 30px; /* Adjust the distance from the bottom */
background-color: transparent; /* Make the background transparent */
border: none; /* Remove border */
font-size: 36px; /* Adjust font size */
cursor: pointer; /* Change cursor to pointer */
outline: none;
z-index: 9999; /* Ensures the icon is on top of every layer */
}
/* Icon styling: default state */
#theme-icon {
display: inline-block;
border-radius: 50%; /* Round shape */
position: relative; /* For the pseudo-element positioning */
transition: box-shadow 0.3s, background-color 0.3s; /* Smooth transition for hover effect */
opacity: 0.6; /* Slightly transparent by default */
filter: grayscale(100%); /* Make the icon black and white */
}
/* Create the inner glow effect with ::after */
#theme-icon::after {
content: ''; /* Empty content for the pseudo-element */
position: absolute;
top: 50%;
left: 50%;
width: 0px; /* Invisible dot */
height: 0px; /* Invisible dot */
background-color: transparent; /* Invisible by default */
border-radius: 50%; /* Circle shape */
transform: translate(-50%, -50%); /* Center the dot */
transition: box-shadow 0.3s, background-color 0.3s; /* Smooth transition for hover */
}
/* Hover effect for both light and dark modes */
#theme-toggle:hover #theme-icon {
position: relative; /* Ensures stacking order */
filter: grayscale(0%); /* Restore full color */
opacity: 1; /* Fully visible on hover */
}
/* Inner glow when hovered */
#theme-toggle:hover #theme-icon::after {
box-shadow: 0 0 40px 40px rgba(128, 128, 128, 0.4); /* Blur effect from inside */
background-color: rgba(128, 128, 128, 0.2); /* Light glow inside */
}
/* Remove hover effects when not hovering */
#theme-toggle:not(:hover) #theme-icon {
opacity: 0.6; /* Slightly transparent */
}

View File

@@ -0,0 +1,7 @@
document.addEventListener("DOMContentLoaded", function(event) {
// timezone
let timezone = document.getElementById("timezone");
if (timezone) {
timezone.value = Intl.DateTimeFormat().resolvedOptions().timeZone
}
});

View File

@@ -0,0 +1,37 @@
// Function to toggle theme
function toggleTheme() {
const currentTheme = document.documentElement.getAttribute('data-theme');
const newTheme = (currentTheme === 'dark') ? '' : 'dark'; // Toggle between no theme and dark theme
document.documentElement.setAttribute('data-theme', newTheme);
localStorage.setItem('theme', newTheme);
// Change the icon based on the current theme
const themeIcon = document.getElementById('theme-icon');
themeIcon.textContent = newTheme === 'dark' ? '☀️' : '🌙'; // Switch between moon and sun icons
}
// Function to immediately apply saved theme without icon update
function applySavedThemeImmediately() {
const savedTheme = localStorage.getItem('theme');
if (savedTheme === 'dark') {
document.documentElement.setAttribute('data-theme', 'dark');
} else {
document.documentElement.removeAttribute('data-theme'); // Default to light theme
}
}
// Function to apply theme-icon update
function setThemeIcon() {
const savedTheme = localStorage.getItem('theme');
if (savedTheme === 'dark') {
document.getElementById('theme-icon').textContent = '☀️'; // Sun icon for dark mode
} else {
document.getElementById('theme-icon').textContent = '🌙'; // Moon icon for light mode
}
}
// Immediately apply the saved theme to avoid flickering
applySavedThemeImmediately();
// Apply theme when the page loads
document.addEventListener('DOMContentLoaded', setThemeIcon);