First release

This commit is contained in:
Knah Tsaeb 2024-06-14 17:20:01 +02:00
commit 5dd12b556b
38 changed files with 1727 additions and 0 deletions

10
.docker/apache2/nofu.conf Normal file
View file

@ -0,0 +1,10 @@
<VirtualHost *:80>
ErrorLog ${APACHE_LOG_DIR}/error.log
ServerName nofu.local
DocumentRoot /var/www/public/
<Directory "/var/www/public/">
Require all granted
AllowOverride All
Options -Indexes
</Directory>
</VirtualHost>

2
.docker/start.sh Normal file
View file

@ -0,0 +1,2 @@
#/bin/bash
/usr/sbin/apache2ctl -DFOREGROUND

12
.gitignore vendored Normal file
View file

@ -0,0 +1,12 @@
data/*
!data/.gitkeep
public/index.html
public/assets/css/user.css
public/assets/js/user.js
public/imgs/big_favicons/*.webp
public/imgs/favicons/*.webp
public/imgs/screenshots/*.webp
public/imgs/thumbs/*.webp
vendor/
composer.lock

38
Dockerfile Normal file
View file

@ -0,0 +1,38 @@
FROM debian:stable-slim
MAINTAINER Knah Tsaeb <knah-tsaeb_soshot@knah-tsaeb.org>
LABEL version="0.1.0"
LABEL description="Apache 2 / PHP / Nofu"
ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get -y update && apt-get install -y git composer php apache2 php-cli php-curl php-gd && apt-get clean && apt-get autoclean && apt-get autoremove
RUN rm -r /var/www/ && mkdir /var/www/
WORKDIR /var/www/
RUN git clone https://forge.leslibres.org/Knah-Tsaeb/Nofu.git --branch main --single-branch --depth 1 .
RUN composer install --no-dev && chown -R www-data:www-data /var/www/
COPY .docker/start.sh /usr/bin/start.sh
RUN chmod +x /usr/bin/start.sh
WORKDIR /
COPY .docker/apache2/nofu.conf /etc/apache2/sites-available/nofu.conf
RUN a2dissite 000-default.conf && a2ensite nofu.conf && a2enmod rewrite
EXPOSE 80
WORKDIR /var/www/
ENTRYPOINT "start.sh"
# Build image
# docker buildx build -t nofu:0.1.0 .
# Run container
# docker run -d --restart unless-stopped -v nofu_data:/var/www/data -e TZ=UTC -p 8189:80 --name nofu nofu:0.1.0
# docker run -d --restart unless-stopped -v /opt/nofu:/var/www/data -e TZ=UTC -p 8189:80 --name nofu nofu:0.1.0

107
README.md Normal file
View file

@ -0,0 +1,107 @@
# Nofu
Nofu for **N**ot **O**nly **F**or **U**s is personal dashboard
## Table of Contents
- [Introduction](#introduction)
- [Features](#features)
- [Instalation](#instalation)
- [Licence](#licence)
## Introduction
There are many impressive dashboards ([awesome-selfhosted](https://awesome-selfhosted.net/tags/personal-dashboards.html)), which are perfect for our needs. However, for non-technical people/geeks/computer enthusiasts/dev...., it can be difficult to understand all the features offered by these dashboards. That's why I created NOFU in 'scratch-an-itch' mode. Although it may not be perfect for everyone, it meets my needs and those of my family circle.
I also wanted a place where my family could find all my services with a quick documentation on my infrastructure (software used, what it's for, where it's located, where backups are stored...), in case I stop functioning one day. So that they can recover the family data or call someone to help them.
![screenshot]()
## Features
* Simple to understand
* Easy customisation
* Minimal dependance
* No database
* Easy backup and deploy
* Static page
* Fast
* No JS or only for eye candy
* Responsive
## Instalation
### Manual
Classic git clone, run composer, create website with your web server, that's all.
#### Clone
```shell
git clone https://forge.leslibres.org/Knah-Tsaeb/Nofu.git
```
#### Install dep
```shell
composer install --no-dev
```
Serve public folder throw your web server.
### Docker
Clone, build and run.
#### Clone
```shell
git clone https://forge.leslibres.org/Knah-Tsaeb/Nofu.git
cd nofu
```
#### Build
```shell
docker buildx build -t nofu:0.1.0 .
```
#### Run
```shell
docker run -d --restart unless-stopped -v nofu_data:/var/www/data -e TZ=UTC -p 8189:80 --name nofu nofu:0.1.0
```
## Ressources
* <a href="https://"><img alt="docker icon" src="public/assets/icons/docker.svg" width="32"> SVGICON</a>
* <a href="https://"><img alt="docker icon" src="public/assets/icons/docker.svg" width="32"> SVGICON</a>
* <a href="https://"><img alt="docker icon" src="public/assets/icons/docker.svg" width="32"> SVGICON</a>
* <a href="https://"><img alt="docker icon" src="public/assets/icons/docker.svg" width="32"> SVGICON</a>
* <a href="https://"><img alt="docker icon" src="public/assets/icons/docker.svg" width="32"> SVGICON</a>
* <a href="https://"><img alt="docker icon" src="public/assets/icons/docker.svg" width="32"> SVGICON</a>
* <a href="https://"><img alt="docker icon" src="public/assets/icons/docker.svg" width="32"> SVGICON</a>
And some code from Stack Overflow :-)
## Licence
WTFPL
```
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
Version 2, December 2004
Copyright (C) 2004 Sam Hocevar <sam@hocevar.net>
Everyone is permitted to copy and distribute verbatim or modified
copies of this license document, and changing it is allowed as long
as the name is changed.
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. You just DO WHAT THE FUCK YOU WANT TO.
```

171
app/App.php Normal file
View file

@ -0,0 +1,171 @@
<?php
namespace KTH;
use Symfony\Component\Yaml\Yaml;
class App {
private $config;
/**
* Check if the cache file exists.
*
* @return bool True if the cache file exists, false otherwise.
*/
public function cacheExist(): bool {
if (file_exists('index.html')) {
return true;
}
return false;
}
/**
* Create a menu from an array of services.
*
* @param array $services An array of service data.
*
* @return array An array containing the unique types and locations of the services.
*/
public function makeMenu(array $services): array {
foreach ($services as $service) {
$menu['type'][] = $service['type'];
$menu['location'][] = $service['location'];
}
$menu['type'] = array_unique($menu['type'], SORT_LOCALE_STRING);
$menu['location'] = array_unique($menu['location'], SORT_LOCALE_STRING);
return $menu;
}
/**
* Return the path to an image file or a default image if the file does not exist.
*
* @param string $file The name of the image file.
* @param string $type The type of image (e.g., 'favicons', 'screenshots').
*
* @return string The path to the image file or a default image.
*/
public function returnImg(string $file, string $type): string {
$defaultFile = 'assets/icons/missing.svg';
$permitType = ['favicons', 'big_favicons', 'screenshots', 'thumbs'];
if (empty($file)) {
return $defaultFile;
}
if (!in_array($type, $permitType)) {
return $defaultFile;
}
if (filter_var($file, FILTER_VALIDATE_URL)) {
return $file;
}
if (file_exists('imgs/' . $type . '/' . $file . '.webp')) {
return 'imgs/' . $type . '/' . $file . '.webp';
}
return $defaultFile;
}
/**
* Check if the current IP address is permitted to bypass authentication.
*
* @param array|null $permitIP An array of permitted IP addresses or null if all IP addresses are permitted.
*
* @return bool True if the current IP address is permitted to bypass authentication, false otherwise.
*/
public function canByPassAuth(array|null $permitIP): bool {
if ($permitIP === null) {
return true;
}
if (in_array($_SERVER['REMOTE_ADDR'], $permitIP)) {
return true;
}
return false;
}
/**
* Save the user configuration settings.
*
* @return void
*/
function saveUserConfig(): void {
$colorScheme = htmlspecialchars($_POST['colorScheme']);
$view = htmlspecialchars($_POST['view']);
$title = htmlspecialchars($_POST['title']);
if (isset($_POST['reimport'])) {
if (htmlspecialchars($_POST['reimport']) == '1') {
$_SESSION['reimport'] = true;
}
}
$userConfig = [
'colorScheme' => $colorScheme,
'view' => $view,
'title' => $title
];
$config = Yaml::dump($userConfig);
file_put_contents('../data/config.yaml', $config);
$this->clearCache();
}
/**
* Get the configuration settings.
*
* @param array $defConfig The default configuration settings.
*
* @return array The configuration settings.
*/
public function getConfig(array $defConfig): array {
if (file_exists('../data/config.yaml')) {
$userConfig = Yaml::parseFile('../data/config.yaml');
if (empty($userConfig)) {
return $defConfig;
}
$config = array_merge($defConfig, $userConfig);
$this->config = $config;
return $config;
}
$this->config = $defConfig;
return $defConfig;
}
/**
* Clear the cache file.
*
* @return void
*/
public function clearCache(): void {
if (file_exists('../public/index.html')) {
unlink('../public/index.html');
}
}
/**
* Initialize the data directories.
*
* @return void
*/
static function initializeDataDir(): void {
if (!is_dir(('../data/assets/css'))) {
mkdir('../data/assets/css', 0775, true);
}
if (!is_dir(('../data/assets/icons'))) {
mkdir('../data/assets/icons', 0775, true);
}
if (!is_dir(('../data/assets/js'))) {
mkdir('../data/assets/js', 0775, true);
}
if (!is_dir(('../data/imgs/favicons'))) {
mkdir('../data/imgs/favicons', 0775, true);
}
if (!is_dir(('../data/imgs/screenshots'))) {
mkdir('../data/imgs/screenshots', 0775, true);
}
}
}

View file

@ -0,0 +1,142 @@
<?php
namespace KTH\HTMLGenerator;
use League\CommonMark\Environment\Environment;
use League\CommonMark\Extension\CommonMark\CommonMarkCoreExtension;
use League\CommonMark\Extension\HeadingPermalink\HeadingPermalinkExtension;
use League\CommonMark\Extension\TableOfContents\TableOfContentsExtension;
use League\CommonMark\MarkdownConverter;
class HTMLGenerator {
private $favicons;
private $screenshots;
/**
* Constructs a new HTMLGenerator object.
*
* @param array $services An array of service configurations.
*/
public function __construct(array $services) {
foreach ($services as $service) {
if (!empty($service['favicon'])) {
$favicons[] = $service['favicon'];
}
if (!empty($service['screenshot'])) {
$screenshots[] = $service['screenshot'];
}
}
$this->favicons = array_unique($favicons, SORT_LOCALE_STRING);
$this->screenshots = array_unique($screenshots, SORT_LOCALE_STRING);
if (isset($_SESSION['reimport'])) {
$this->resizeImg();
unset($_SESSION['reimport']);
}
$this->copyUserFile();
}
/**
* Resizes the favicons and screenshots.
*
* @return void
*/
private function resizeImg(): void {
$image = new \Zebra_Image();
foreach ($this->favicons as $favicon) {
if (!str_starts_with(strtolower($favicon), 'http')) {
if (is_file('../data/imgs/favicons/' . $favicon)) {
$image->source_path = '../data/imgs/favicons/' . $favicon;
$image->target_path = '../public/imgs/favicons/' . $favicon . '.webp';
$image->preserve_aspect_ratio = true;
$image->enlarge_smaller_images = false;
$image->resize(32, 32);
$image->target_path = '../public/imgs/big_favicons/' . $favicon . '.webp';
$image->resize(128, 128);
}
}
}
foreach ($this->screenshots as $screenshot) {
if (!str_starts_with(strtolower($screenshot), 'http')) {
if (is_file('../data/imgs/screenshots/' . $favicon)) {
$image->source_path = '../data/imgs/screenshots/' . $screenshot;
$image->target_path = '../public/imgs/screenshots/' . $screenshot . '.webp';
$image->preserve_aspect_ratio = true;
$image->enlarge_smaller_images = false;
$image->resize(1120);
$image->target_path = '../public/imgs/thumbs/' . $screenshot . '.webp';
$image->resize(250);
}
}
}
}
/**
* Copy user-provided CSS, JS, and SVG files to the public directory.
*
* @return void
*/
private function copyUserFile(): void {
if (file_exists('../data/assets/css/user.css')) {
copy('../data/assets/css/user.css', '../public/assets/css/user.css');
}
if (file_exists('../data/assets/js/user.js')) {
copy('../data/assets/js/user.js', '../public/assets/js/user.js');
}
foreach (glob("../data/assets/icons/*.svg") as $filePath) {
$filename = pathinfo($filePath, PATHINFO_BASENAME);
copy($filePath, $filename);
}
}
/**
* Generate the user documentation in HTML format.
*
* @return string | null The HTML content of the user documentation.
*/
public function genUserDoc(): string | null {
if (file_exists('../data/help.md')) {
$configMD = [
'table_of_contents' => [
'html_class' => 'table-of-contents',
'position' => 'top',
'style' => 'bullet',
'min_heading_level' => 1,
'max_heading_level' => 6,
'normalize' => 'relative',
'placeholder' => null,
],
'heading_permalink' => [
'html_class' => 'heading-permalink',
'id_prefix' => 'content',
'apply_id_to_heading' => false,
'heading_class' => '',
'fragment_prefix' => 'content',
'insert' => 'before',
'min_heading_level' => 1,
'max_heading_level' => 6,
'title' => 'Permalink',
'symbol' => '',
'aria_hidden' => true,
],
];
$environment = new Environment($configMD);
$environment->addExtension(new CommonMarkCoreExtension());
$environment->addExtension(new HeadingPermalinkExtension());
$environment->addExtension(new TableOfContentsExtension());
$converter = new MarkdownConverter($environment);
return $converter->convert(file_get_contents('../data/help.md'))->getContent();
} else {
return null;
}
}
}

93
app/login/Login.php Normal file
View file

@ -0,0 +1,93 @@
<?php
namespace Login;
use Symfony\Component\Yaml\Yaml;
class Login {
private $listUser;
/**
* Construct a new instance of the class.
*
* Initialize the listUser property as an empty array and call the loadUser method.
*/
function __construct() {
$this->listUser = [];
$this->loadUsers();
}
/**
* Add a new user to the list of users.
*
* @param string $login The login name of the new user.
* @param string $password The password of the new user.
* @param string $role The role of the new user.
*
* @return bool True if the user was successfully added, false otherwise.
*/
public function addUser(string $login, string $password, string $role): bool {
if ($this->listUser[$login]) {
return false;
}
$newUser = [$login => ['password' => password_hash($password, PASSWORD_DEFAULT), 'role' => $role]];
$listUser = array_merge($newUser, $this->listUser);
$yaml = Yaml::dump($listUser);
return file_put_contents('../data/users.yaml', $yaml);
}
/**
* Load the users from the YAML file.
*
* @return void
*/
private function loadUsers() {
if (file_exists('../data/users.yaml')) {
$listUser = Yaml::parseFile('../data/users.yaml');
if (!empty($listUser)) {
$this->listUser = $listUser;
}
}
}
/**
* Log in a user with the given credentials.
*
* @param string $user The username.
* @param string $password The password.
*
* @return bool True if the user is logged in successfully, false otherwise.
*/
public function logIn(string $user, string $password): bool {
if ($this->listUser[$user] && password_verify($password, $this->listUser[$user]['password'])) {
$_SESSION['isLogged'] = true;
return true;
}
return false;
}
/**
* Log out the user and redirect to the index page.
*
* @return void
*/
static function logOut(): void {
session_destroy();
header("Location: index.php");
}
/**
* Check if the user is logged in.
*
* @return bool True if the user is logged in, false otherwise.
*/
static function isLogged(): bool {
if (isset($_SESSION['isLogged']) && $_SESSION['isLogged'] === true) {
return true;
}
return false;
}
}

39
app/utils/CsrfToken.php Normal file
View file

@ -0,0 +1,39 @@
<?php
namespace Utils;
class CsrfToken {
/**
* Generate a CSRF token and store it in the session.
*
* This static method generates a CSRF token using random bytes and stores it in the session.
* The generated token is a hexadecimal string with a length of 32 characters.
*
* @return string The generated CSRF token.
*/
public static function generateToken(): string {
$token = bin2hex(random_bytes(32));
$_SESSION['csrf_token'] = $token;
return $token;
}
/**
* Validate a CSRF token against the one stored in the session.
*
* This static method validates a given CSRF token against the one stored in the session.
* It returns true if the provided token matches the one in the session; otherwise, it returns false.
*
* @param string $token The CSRF token to be validated.
*
* @return bool True if the provided token is valid; otherwise, false.
*/
public static function validateToken(string $token): bool {
if (isset($_SESSION['csrf_token']) && $_SESSION['csrf_token'] === $token) {
return true;
} else {
return false;
}
}
}

24
app/utils/Debug.php Normal file
View file

@ -0,0 +1,24 @@
<?php
namespace Utils;
class Debug {
/**
* Print the contents of a variable with a customizable name and backtrace information.
*
* @param mixed $data The variable to print.
* @param string $name The name to display above the variable contents.
*
* @return void
*/
function n_print(mixed $data, string $name = ''): void {
error_reporting(-1);
$aBackTrace = debug_backtrace();
echo '<h2>', $name, '</h2>';
echo '<fieldset style="border: 1px solid orange; padding: 5px;color: #333; background-color: #fff;">';
echo '<legend style="border:1px solid orange;padding: 1px;background-color:#eee;color:orange;">', $aBackTrace[0]['file'], ' ligne => ', $aBackTrace[0]['line'], '</legend>';
echo '<pre>', htmlentities(print_r($data, 1)), '</pre>';
echo '</fieldset><br />';
}
}

20
app/utils/Select.php Normal file
View file

@ -0,0 +1,20 @@
<?php
namespace Utils;
class Select {
/**
* Check if a given value is selected.
*
* @param mixed $ref The reference value to compare against.
* @param mixed $param The value to compare.
*
* @return string The string 'selected' if the values match, or an empty string otherwise.
*/
static function isSelected(string $ref, string $param): string {
if ($ref === $param) {
return 'selected';
}
return '';
}
}

15
composer.json Normal file
View file

@ -0,0 +1,15 @@
{
"require": {
"symfony/yaml": "^7.0",
"stefangabos/zebra_image": "^2.8",
"league/commonmark": "^2.4"
},
"autoload": {
"psr-4": {
"Utils\\" : "app/utils/",
"Login\\" : "app/login/",
"KTH\\" : "app/",
"KTH\\HTMLGenerator\\" : "app/generator/"
}
}
}

0
data/.gitkeep Normal file
View file

13
public/.htaccess Normal file
View file

@ -0,0 +1,13 @@
# 480 weeks
<FilesMatch "\.(html|ico|jpg|jpeg|png|gif|js|css)$">
# Header set Cache-Control "max-age=290304000, public"
</FilesMatch>
<ifModule mod_gzip.c>
mod_gzip_on Yes
mod_gzip_dechunk Yes
mod_gzip_item_include file \.(html?|txt|css|js|php)$
mod_gzip_item_include mime ^application/x-javascript.*
mod_gzip_item_exclude mime ^image/.*
mod_gzip_item_exclude rspheader ^Content-Encoding:.*gzip.*
</ifModule>

543
public/assets/css/style.css Normal file
View file

@ -0,0 +1,543 @@
/*
########################################################
############# DO NOT EDIT THIS FILE ####################
############# USE data/user.css ####################
########################################################
*/
:root {
--background-color: #dadada;
--border-radius: 10px;
--light-color: #ececec;
--nav-background-color: #34495e;
--text-color: #1d1e22;
--margin: .7em;
h1 {
color: var(--text-color);
}
.login {
color: var(--text-color);
border: 1px solid var(--text-color);
}
}
[data-theme="dark"] {
--background-color: #1d1e22;
--border-radius: 10px;
--light-color: #ececec;
--nav-background-color: #34495e;
--text-color: #1d1e22;
--margin: .7em;
h1 {
color: var(--light-color);
}
.login {
color: var(--light-color);
border: 1px solid var(--light-color);
}
}
*,
*:after,
*:before {
box-sizing: border-box;
}
html {
font-size: 59%;
}
body {
background: var(--background-color);
color: var(--text-color);
margin: var(--margin);
}
.titleBar {
text-align: center;
}
.titleBar .linkList {
float: right;
svg {
height: 2.5em;
width: 2.5em;
margin: var(--margin);
}
a {
text-align: none;
}
}
.titleBar div {
margin: 0;
padding: 0;
line-height: 2.5em;
height: 2.5em;
}
h1 {
color: var(--light-color);
font-size: 2.5rem;
margin: 0;
}
svg {
fill: var(--nav-background-color);
height: 1.2em;
transition: .3s all ease;
vertical-align: text-bottom;
}
svg:hover {
background-color: var(--nav-background-color);
fill: var(--light-color);
}
.card-container {
display: flex;
flex-flow: row wrap;
justify-content: center;
margin: 0 auto;
max-width: 100dvw;
padding: 0 .5em;
}
.card-container h3 a {
color: var(--text-color);
text-decoration: none;
}
.card-container .card {
cursor: pointer;
}
.card-container .card {
background: var(--light-color);
box-shadow: 0 0 2px 2px rgba(0, 0, 0, .05);
margin: var(--margin);
transition: .3s all ease;
width: calc(100% / 7 - 20px);
}
.card-container .card:hover {
border-radius: 15px 15px var(--border-radius) var(--border-radius);
box-shadow: 0 0 30px 15px rgba(0, 0, 0, 0.56);
transform: scale(1.20);
z-index: 1;
}
.card-container .card .thumb {
margin: 0;
padding: 0;
transition: .3s all ease;
width: 100%;
}
.card-container .card .thumb:hover {
border-top-left-radius: var(--border-radius);
border-top-right-radius: var(--border-radius);
}
.card-container .card .content {
padding: .5em;
}
.card-container .card h3 {
font: 2.6rem/3.2rem 'Bree Serif', serif;
letter-spacing: -.075rem;
margin: 0 0 5px;
padding: 0;
}
.card-container .card p {
color: var(--text-color);
font: 400 1.6rem/2.2rem 'Open Sans script=all', sans-serif;
margin: 0;
padding: 0;
}
.card-container .readMore {
text-align: right;
}
.card .favicon {
height: 20px;
width: 20px;
margin-right: .1em;
}
.icons {
.card {
margin: 0;
padding: 0;
width: auto;
}
.card .content {
padding: 0;
margin: 0;
}
.card .content h3 {
margin: 0;
padding: 0;
}
.card .content .favicon {
margin: 0;
width: 128px;
height: 128px;
padding: 0;
}
a {
display: block;
height: 128px;
}
img {
transition: .3s all ease;
}
img:hover {
border-radius: 10px;
}
}
.modal {
background-color: rgb(0, 0, 0);
background-color: rgba(0, 0, 0, 0.4);
display: none;
height: 100%;
left: 0;
overflow: auto;
padding-top: 100px;
position: fixed;
top: 0;
width: 100%;
z-index: 2;
}
.modal-content {
animation-duration: 0.3s;
animation-name: animatetop;
background-color: var(--light-color);
border-radius: var(--border-radius);
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19);
font-size: 150%;
height: calc(100% / 1 - 100px);
margin: auto;
padding: .2em;
position: relative;
width: calc(100% / 2 - 20px);
}
.modal-content .screenshot {
margin: 0 auto;
text-align: center;
}
.screenshot {
width: 100%;
}
.modal-header a,
.modal-header a:visited {
color: var(--nav-background-color);
}
.modal-header a:hover,
.modal-header a:visited:hover {
filter: brightness(1.5);
}
.userDoc {
overflow: scroll;
font-size: 1.8em;
}
@keyframes animatetop {
from {
opacity: 0;
top: -400px;
}
to {
opacity: 1;
top: 0;
}
}
.close {
color: var(--text-color);
display: block;
float: right;
font-size: 32px;
font-weight: bold;
transition: .3s all ease;
}
.close:hover,
.close:focus {
color: var(--nav-background-color);
cursor: pointer;
text-decoration: none;
transform: rotate(90deg);
}
.modal h2 {
font-size: 160%;
}
.modal-content h2 img {
vertical-align: text-bottom;
}
.modal-header {
background-color: var(--light-color);
color: var(--text-color);
padding: 1px 15px;
}
.modal-body {
padding: 2px 16px;
}
.modal-body img {
position: relative;
}
nav {
background-color: var(--nav-background-color);
border-radius: var(--border-radius);
display: flex;
flex-wrap: wrap;
height: auto;
margin: .5em 0;
padding: .5em 0;
position: relative;
width: fit-content;
}
nav img {
display: inline-block;
vertical-align: text-bottom;
}
nav span {
font-size: 1.5rem;
font-weight: bold;
line-height: 1.5rem;
margin: auto var(--margin);
;
}
.readMore {
display: flex;
flex-wrap: wrap;
height: auto;
margin: var(--margin);
padding: var(--margin);
position: relative;
width: fit-content;
}
.button {
background-color: var(--nav-background-color);
border: 1px solid var(--light-color);
border-radius: var(--border-radius);
color: var(--light-color);
padding: .2em .3em;
transition: all 0.3s ease-in-out;
}
.button:hover {
background-color: var(--light-color);
border: 1px solid var(--nav-background-color);
color: var(--text-color);
}
nav a {
background-clip: text;
background-image: linear-gradient(to right,
var(--background-color),
var(--background-color) 50%,
var(--light-color) 50%);
background-position: -100%;
background-size: 200% 100%;
color: transparent;
cursor: pointer;
display: inline-block;
font-size: 1.5rem;
line-height: 1.5rem;
margin: auto var(--margin);
padding: 5px 0;
position: relative;
text-align: center;
text-decoration: none;
text-transform: uppercase;
transition: all 0.3s ease-in-out;
}
nav a:before,
nav .active::before {
background: var(--text-color);
bottom: -3px;
content: '';
display: block;
height: 3px;
left: 0;
position: absolute;
transition: all 0.3s ease-in-out;
width: 0;
}
nav .active {
background-image: linear-gradient(to right,
var(--background-color),
var(--background-color) 50%,
var(--light-color) 50%);
}
nav a:hover,
nav .active {
background-position: 0;
}
nav a:hover::before,
nav .active::before {
width: 100%;
}
.login {
border: 1px solid var(--light-color);
border-radius: var(--border-radius);
color: var(--light-color);
display: flex;
flex-direction: column;
font-size: 2rem;
gap: 1rem;
margin: var(--margin) auto 0;
padding: 1rem;
width: 30vw;
}
input[type=text],
input[type=password],
select {
border: 1px solid var(--nav-background-color);
box-sizing: border-box;
display: inline-block;
margin: 0 0 var(--margin) 0;
padding: 12px 20px;
width: 100%;
}
button[type=submit] {
height: 3em;
margin: auto;
width: 30%;
}
.checkbox {
display: flex;
gap: 10px;
justify-content: center;
}
@media only screen and (max-width: 600px) {
.card-container:not(.icons) .card {
width: calc(100% / 1 - 20px);
}
.modal-content {
width: 90%;
}
.login {
width: 80vw;
}
}
@media only screen and (min-width: 600px) {
.card-container:not(.icons) .card {
width: calc(100% / 2 - 20px);
}
.modal-content {
width: calc(100% / 1);
}
.login {
width: 70vw;
}
}
@media only screen and (min-width: 768px) {
.card-container:not(.icons) .card {
width: calc(100% / 3 - 20px);
}
.modal-content {
width: calc(100% / 2 + 150px);
}
.login {
width: 60vw;
}
}
@media only screen and (min-width: 992px) {
.card-container:not(.icons) .card {
width: calc(100% / 4 - 20px);
}
.modal-content {
width: calc(100% / 2 + 100px);
}
.login {
width: 50vw;
}
}
@media only screen and (min-width: 1200px) {
.card-container:not(.icons) .card {
width: calc(100% / 5 - 20px);
}
.modal-content {
width: calc(100% / 2 + 50px);
}
.login {
width: 40vw;
}
}
@media only screen and (min-width: 1600px) {
.card-container:not(.icons) .card {
width: calc(100% / 7 - 20px);
}
.modal-content {
width: calc(100% / 2 + 200px);
}
.login {
width: 30vw;
}
}
.hide {
display: none;
}

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M21.81 10.25C21.75 10.21 21.25 9.82 20.17 9.82C19.89 9.82 19.61 9.85 19.33 9.9C19.12 8.5 17.95 7.79 17.9 7.76L17.61 7.59L17.43 7.86C17.19 8.22 17 8.63 16.92 9.05C16.72 9.85 16.84 10.61 17.25 11.26C16.76 11.54 15.96 11.61 15.79 11.61H2.62C2.28 11.61 2 11.89 2 12.24C2 13.39 2.18 14.54 2.58 15.62C3.03 16.81 3.71 17.69 4.58 18.23C5.56 18.83 7.17 19.17 9 19.17C9.79 19.17 10.61 19.1 11.42 18.95C12.54 18.75 13.62 18.36 14.61 17.79C15.43 17.32 16.16 16.72 16.78 16C17.83 14.83 18.45 13.5 18.9 12.35H19.09C20.23 12.35 20.94 11.89 21.33 11.5C21.59 11.26 21.78 10.97 21.92 10.63L22 10.39L21.81 10.25M3.85 11.24H5.61C5.69 11.24 5.77 11.17 5.77 11.08V9.5C5.77 9.42 5.7 9.34 5.61 9.34H3.85C3.76 9.34 3.69 9.41 3.69 9.5V11.08C3.7 11.17 3.76 11.24 3.85 11.24M6.28 11.24H8.04C8.12 11.24 8.2 11.17 8.2 11.08V9.5C8.2 9.42 8.13 9.34 8.04 9.34H6.28C6.19 9.34 6.12 9.41 6.12 9.5V11.08C6.13 11.17 6.19 11.24 6.28 11.24M8.75 11.24H10.5C10.6 11.24 10.67 11.17 10.67 11.08V9.5C10.67 9.42 10.61 9.34 10.5 9.34H8.75C8.67 9.34 8.6 9.41 8.6 9.5V11.08C8.6 11.17 8.66 11.24 8.75 11.24M11.19 11.24H12.96C13.04 11.24 13.11 11.17 13.11 11.08V9.5C13.11 9.42 13.05 9.34 12.96 9.34H11.19C11.11 9.34 11.04 9.41 11.04 9.5V11.08C11.04 11.17 11.11 11.24 11.19 11.24M6.28 9H8.04C8.12 9 8.2 8.91 8.2 8.82V7.25C8.2 7.16 8.13 7.09 8.04 7.09H6.28C6.19 7.09 6.12 7.15 6.12 7.25V8.82C6.13 8.91 6.19 9 6.28 9M8.75 9H10.5C10.6 9 10.67 8.91 10.67 8.82V7.25C10.67 7.16 10.61 7.09 10.5 7.09H8.75C8.67 7.09 8.6 7.15 8.6 7.25V8.82C8.6 8.91 8.66 9 8.75 9M11.19 9H12.96C13.04 9 13.11 8.91 13.11 8.82V7.25C13.11 7.16 13.04 7.09 12.96 7.09H11.19C11.11 7.09 11.04 7.15 11.04 7.25V8.82C11.04 8.91 11.11 9 11.19 9M11.19 6.72H12.96C13.04 6.72 13.11 6.65 13.11 6.56V5C13.11 4.9 13.04 4.83 12.96 4.83H11.19C11.11 4.83 11.04 4.89 11.04 5V6.56C11.04 6.64 11.11 6.72 11.19 6.72M13.65 11.24H15.41C15.5 11.24 15.57 11.17 15.57 11.08V9.5C15.57 9.42 15.5 9.34 15.41 9.34H13.65C13.57 9.34 13.5 9.41 13.5 9.5V11.08C13.5 11.17 13.57 11.24 13.65 11.24" /></svg>

After

Width:  |  Height:  |  Size: 2 KiB

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M17 7L15.59 8.41L18.17 11H8V13H18.17L15.59 15.58L17 17L22 12M4 5H12V3H4C2.9 3 2 3.9 2 5V19C2 20.1 2.9 21 4 21H12V19H4V5Z" /></svg>

After

Width:  |  Height:  |  Size: 199 B

View file

@ -0,0 +1,38 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
viewBox="0 0 24 13.5"
version="1.1"
id="svg1"
sodipodi:docname="missing.svg"
width="24"
height="13.5"
inkscape:version="1.3.2 (091e20ef0f, 2023-11-25, custom)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="38.583333"
inkscape:cx="11.974082"
inkscape:cy="12"
inkscape:window-width="1860"
inkscape:window-height="1172"
inkscape:window-x="60"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg1" />
<path
d="M 18.110644,0.3007551 H 5.8893555 c -0.8402142,0 -1.5276614,0.580432 -1.5276614,1.2898486 V 13.199245 L 7.4170169,10.619546 H 18.110644 c 0.840213,0 1.527662,-0.580432 1.527662,-1.2898491 V 1.5906037 c 0,-0.7094166 -0.687449,-1.2898486 -1.527662,-1.2898486 m 0,9.0289418 H 6.8059526 L 5.8893555,10.103606 V 1.5906037 H 18.110644 v 7.7390932 m -11.4574584,-1.289849 2.6734072,-2.90216 1.9095772,1.934773 2.673407,-2.9021597 3.437236,3.8695467"
id="path1"
style="stroke-width:0.701863" />
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M10.5 18H18V20H10.5C6.91 20 4 17.09 4 13.5S6.91 7 10.5 7H16.17L13.08 3.91L14.5 2.5L20 8L14.5 13.5L13.09 12.09L16.17 9H10.5C8 9 6 11 6 13.5S8 18 10.5 18Z" /></svg>

After

Width:  |  Height:  |  Size: 231 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path d="M1.75 1h12.5c.966 0 1.75.784 1.75 1.75v4c0 .372-.116.717-.314 1 .198.283.314.628.314 1v4a1.75 1.75 0 0 1-1.75 1.75H1.75A1.75 1.75 0 0 1 0 12.75v-4c0-.358.109-.707.314-1a1.739 1.739 0 0 1-.314-1v-4C0 1.784.784 1 1.75 1ZM1.5 2.75v4c0 .138.112.25.25.25h12.5a.25.25 0 0 0 .25-.25v-4a.25.25 0 0 0-.25-.25H1.75a.25.25 0 0 0-.25.25Zm.25 5.75a.25.25 0 0 0-.25.25v4c0 .138.112.25.25.25h12.5a.25.25 0 0 0 .25-.25v-4a.25.25 0 0 0-.25-.25ZM7 4.75A.75.75 0 0 1 7.75 4h4.5a.75.75 0 0 1 0 1.5h-4.5A.75.75 0 0 1 7 4.75ZM7.75 10h4.5a.75.75 0 0 1 0 1.5h-4.5a.75.75 0 0 1 0-1.5ZM3 4.75A.75.75 0 0 1 3.75 4h.5a.75.75 0 0 1 0 1.5h-.5A.75.75 0 0 1 3 4.75ZM3.75 10h.5a.75.75 0 0 1 0 1.5h-.5a.75.75 0 0 1 0-1.5Z"/></svg>

After

Width:  |  Height:  |  Size: 788 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24"><path fill="currentColor" d="M12.003.064C5.376.064 0 5.407 0 12s5.376 11.936 12.003 11.936c2.169 0 4.2-.57 5.955-1.57l.624 1.57h4.841l-1.893-4.679A11.85 11.85 0 0 0 24 12C24 5.407 18.63.064 12.003.064M8.818 2.03c.398.339.324.198.86.134c.61-.397.893.942 1.147.195c.748.097 1.542.34 2.25.584a3.45 3.45 0 0 1 1.859 1.128l-.014.007l.35.463c.045.08.082.164.12.248c.142 1.205 1.48 1.19 2.377 1.625c.767.272 1.69.686 1.785 1.611c-.193-.042-.941-.921-1.53-1.007a4 4 0 0 1-1.094-.255L14.86 6.38v-.007a3 3 0 0 1-.309-.053v.013l-2.927-.362c.048.033.1.077.148.12l3 .585v-.007l.209.053l.839.188c.166.016.334.043.47.067c.856.236 1.868.194 2.571.792c-.184.352-1.21.153-1.719.108c-.062-.012-.131-.023-.194-.034l-.034-.007c-.696-.113-1.411-.12-2.081.088h-.007a3.2 3.2 0 0 0-.671.302c-.968.563-2.164.767-2.967 1.577c-.787.847-.739 2.012-.604 3.095h.033v.275l.04.282c.41 2.19 1.5 4.2 1.84 6.412c.065.843.203 1.932.309 2.618c-.306-.091-.475-1.462-.544-1.007a38 38 0 0 0-3.565-5.25c-.853-1.004-1.697-2.06-2.712-2.894c-.685-.528-.468-1.55-.537-2.302c-.23-.926-.094-1.848.06-2.773c.313-.963.418-1.968.846-2.893c.653-.581.669-1.63 1.303-2.135c.094.058.157.085.2.1l.068.008h.007c.09-.095-.888-1.116.02-.712c.035-.537.854-.128.866-.597m3.847 2.182c-.323.009-.574.13-.645.335c-.114.33.273.755.866.96c.594.205 1.168.109 1.282-.221s-.272-.762-.866-.967a1.8 1.8 0 0 0-.637-.107"/></svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M16.36,14C16.44,13.34 16.5,12.68 16.5,12C16.5,11.32 16.44,10.66 16.36,10H19.74C19.9,10.64 20,11.31 20,12C20,12.69 19.9,13.36 19.74,14M14.59,19.56C15.19,18.45 15.65,17.25 15.97,16H18.92C17.96,17.65 16.43,18.93 14.59,19.56M14.34,14H9.66C9.56,13.34 9.5,12.68 9.5,12C9.5,11.32 9.56,10.65 9.66,10H14.34C14.43,10.65 14.5,11.32 14.5,12C14.5,12.68 14.43,13.34 14.34,14M12,19.96C11.17,18.76 10.5,17.43 10.09,16H13.91C13.5,17.43 12.83,18.76 12,19.96M8,8H5.08C6.03,6.34 7.57,5.06 9.4,4.44C8.8,5.55 8.35,6.75 8,8M5.08,16H8C8.35,17.25 8.8,18.45 9.4,19.56C7.57,18.93 6.03,17.65 5.08,16M4.26,14C4.1,13.36 4,12.69 4,12C4,11.31 4.1,10.64 4.26,10H7.64C7.56,10.66 7.5,11.32 7.5,12C7.5,12.68 7.56,13.34 7.64,14M12,4.03C12.83,5.23 13.5,6.57 13.91,8H10.09C10.5,6.57 11.17,5.23 12,4.03M18.92,8H15.97C15.65,6.75 15.19,5.55 14.59,4.44C16.43,5.07 17.96,6.34 18.92,8M12,2C6.47,2 2,6.5 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2Z" /></svg>

After

Width:  |  Height:  |  Size: 995 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M6,2H18A2,2 0 0,1 20,4V20A2,2 0 0,1 18,22H6A2,2 0 0,1 4,20V4A2,2 0 0,1 6,2M12,4A6,6 0 0,0 6,10C6,13.31 8.69,16 12.1,16L11.22,13.77C10.95,13.29 11.11,12.68 11.59,12.4L12.45,11.9C12.93,11.63 13.54,11.79 13.82,12.27L15.74,14.69C17.12,13.59 18,11.9 18,10A6,6 0 0,0 12,4M12,9A1,1 0 0,1 13,10A1,1 0 0,1 12,11A1,1 0 0,1 11,10A1,1 0 0,1 12,9M7,18A1,1 0 0,0 6,19A1,1 0 0,0 7,20A1,1 0 0,0 8,19A1,1 0 0,0 7,18M12.09,13.27L14.58,19.58L17.17,18.08L12.95,12.77L12.09,13.27Z" /></svg>

After

Width:  |  Height:  |  Size: 538 B

62
public/assets/js/app.js Normal file
View file

@ -0,0 +1,62 @@
function showReadMore(modal) {
// Get the modal
var divModal = document.getElementById('modal-' + modal);
divModal.style.display = "block";
// Get the <span> element that closes the modal
var close = document.getElementById('close-' + modal);
// When the user clicks the button, open the modal
// When the user clicks on <span> (x), close the modal
close.onclick = function () {
divModal.style.display = "none";
}
// When the user clicks anywhere outside of the modal, close it
window.onclick = function (event) {
if (event.target == divModal) {
divModal.style.display = "none";
}
}
}
function toggleFilter(element, filter) {
let filterListNav = document.getElementsByTagName('nav')
for (var i = 0; i < filterListNav.length; i++) {
for (var i2 = 0; i2 < filterListNav[i].children.length; i2++) {
if (!filterListNav[i].children[i2].classList.contains(filter)) {
filterListNav[i].children[i2].classList.remove('active')
}
}
}
element.classList.toggle('active')
let filterList = document.getElementsByClassName('card')
let filterListLength = filterList.length
for (var i = 0; i < filterListLength; i++) {
if (element.classList.contains('active')) {
if (!filterList[i].classList.contains(filter)) {
filterList[i].style.opacity = .3
} else {
filterList[i].style.opacity = 1
}
} else {
filterList[i].style.opacity = 1
}
}
}
function switchTheme(e) {
let actualTheme = document.documentElement.getAttribute('data-theme');
if (actualTheme === null || actualTheme === 'light') {
document.documentElement.setAttribute('data-theme', 'dark');
} else {
document.documentElement.setAttribute('data-theme', 'light');
}
}

BIN
public/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 203 B

View file

View file

View file

View file

83
public/index.php Normal file
View file

@ -0,0 +1,83 @@
<?php
session_start();
require '../vendor/autoload.php';
use Symfony\Component\Yaml\Yaml;
use KTH\App;
use KTH\HTMLGenerator\HTMLGenerator;
use Login\Login;
$defConfig['title'] = 'KT-HomePage';
$defConfig['desc'] = 'Dashboard de Knah Tsaeb';
$defConfig['favicon'] = 'favicon.png';
$defConfig['noAuth'] = ['128.0.0.1'];
$defConfig['public'] = false;
$defConfig['colorScheme'] = 'dark';
$defConfig['view'] = 'full';
$breadcrumbs = '';
$KTH = new App();
$config = $KTH->getConfig($defConfig);
if (isset($_GET['logout'])) {
$logout = Login::logOut();
exit();
}
if ($config['public'] === false) {
if (!$KTH->canByPassAuth($defConfig['noAuth'])) {
if (!Login::isLogged()) {
if (file_exists('../data/users.yaml')) {
require('../template/default/login.php');
} else {
require('../template/default/register.php');
}
exit();
}
}
}
if (isset($_GET['settings'])) {
require('../template/default/settings.php');
exit();
}
if (isset($_POST['settings'])) {
$KTH->saveUserConfig();
header("Location: /");
exit();
}
if ($KTH->cacheExist()) {
echo file_get_contents('index.html');
} else {
if (file_exists('../data/services.yaml')) {
$services = Yaml::parseFile('../data/services.yaml');
} else {
$services = [0 => [
'title' => 'Wikipedia',
'screenshot' => 'wikipedia.png',
'favicon' => 'wikipedia.png',
'link' => 'https://en.wikipedia.org/wiki/Dashboard_(computing)',
'appHome' => 'https://www.mediawiki.org/wiki/MediaWiki',
'location' => 'web',
'desc' => 'Wikipedia, the free encyclopedia',
'type' => 'webapp'
]];
}
$generator = new HTMLGenerator($services);
$userDoc = $generator->genUserDoc();
$menuData = $KTH->makeMenu($services);
ob_start();
require('../template/default/header.php');
require('../template/default/titleBar.php');
require('../template/default/nav.php');
require('../template/default/content.php');
require('../template/default/footer.php');
$out = ob_get_contents();
file_put_contents('index.html', $out);
}

View file

@ -0,0 +1,70 @@
<div class="card-container <?= $config['view']; ?>">
<?php foreach ($services as $service) : ?>
<div class="card <?= $service['type'] . ' ' . $service['location']; ?>">
<?php if ($config['view'] !== 'icons') : ?>
<a href="<?= $service['link']; ?>" rel="noopener noreferrer" target="_blank">
<?php endif; ?>
<?php if ($config['view'] === 'full') : ?>
<img class="thumb" src="<?= $KTH->returnImg($service['screenshot'], 'thumbs'); ?>" alt="Thumbshot" />
<?php endif; ?>
<?php if ($config['view'] !== 'icons') : ?>
</a><?php endif; ?>
<div class="content">
<h3>
<a href="<?= $service['link']; ?>" rel="noopener noreferrer" target="_blank" title="<?= $service['title']; ?>" >
<?php if ($config['view'] === 'icons') : ?>
<img class="favicon" loading="lazy" src="<?= $KTH->returnImg($service['favicon'], 'big_favicons'); ?>" alt="Favicon" />
<?php else :; ?>
<img class="favicon" loading="lazy" src="<?= $KTH->returnImg($service['favicon'], 'favicons'); ?>" alt="Favicon" />
<?php endif; ?>
<?php if ($config['view'] !== 'icons') : ?><?= $service['title']; ?><?php endif; ?>
</a>
</h3>
<?php if ($config['view'] !== 'icons') : ?>
<p class="readMore">
<a class="button" onclick="showReadMore('<?= md5($service['link']); ?>')">More info</a>
</p>
<?php endif; ?>
</div>
</div>
<div id="modal-<?= md5($service['link']); ?>" class="modal">
<!-- Modal content -->
<div class="modal-content">
<div class="modal-header">
<span class="close" id="close-<?= md5($service['link']); ?>">&times;</span>
<h2>
<img class="favicon" loading="lazy" src="<?= $KTH->returnImg($service['favicon'], 'favicons'); ?>" alt="Favicon" height="32px" />
<?= $service['title']; ?>
</h2>
<ul>
<li>Software : <a href="<?= $service['appHome']; ?>" target="_blank"><?= $service['appHome']; ?></a></li>
<li>Installation type : <?= $service['type']; ?></li>
<li>Machine : <?= $service['location']; ?></li>
</ul>
</div>
<div class="modal-body">
<p><?= $service['desc']; ?></p>
<p class="">
<a href="<?= $service['link']; ?>" rel="noopener noreferrer" target="_blank">
<img loading="lazy" class="screenshot" src="<?= $KTH->returnImg($service['screenshot'], 'screenshots'); ?>" alt="Screenshot" />
</a>
</p>
</div>
</div>
</div>
<?php endforeach; ?>
<div id="modal-userDoc" class="modal">
<!-- Modal content -->
<div class="modal-content userDoc">
<div class="modal-header">
<span class="close" id="close-userDoc">&times;</span>
</div>
<div class="modal-body">
<?= $userDoc; ?>
</div>
</div>
</div>
</div>

View file

@ -0,0 +1,7 @@
<script src="assets/js/app.js"></script>
<?php if (file_exists('assets/js/user.js')) : ?>
<script src="assets/js/user.js"></script>
<?php endif; ?>
</body>
</html>

View file

@ -0,0 +1,18 @@
<!doctype html>
<html data-theme="<?= $config['colorScheme']; ?>">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="robots" content="noindex">
<meta name="description" content="<?= $config['desc']; ?>">
<meta name="color-scheme" content="<?= $config['colorScheme']; ?>">
<title><?= $config['title']; ?></title>
<link rel="stylesheet" href="assets/css/style.css" />
<?php if (file_exists('assets/css/user.css')) : ?>
<link rel="stylesheet" href="assets/css/user.css" />
<?php endif; ?>
<link rel="icon" type="image/png" href="<?= $config['favicon']; ?>">
</head>
<body>

View file

@ -0,0 +1,47 @@
<?php
use Login\Login;
use Utils\CsrfToken;
use Utils\Debug;
$error = null;
$breadcrumbs = ' / Login';
$debug = new Debug;
if (!empty($_POST)) {
if (empty($_POST['login']) || empty($_POST['password'])) {
$error = 'Please fill login and password.';
} else {
$login = new Login;
if (CsrfToken::validateToken($_POST['token'])) {
if ($login->LogIn($_POST['login'], $_POST['password'])) {
header('Location: index.php');
} else {
$error = 'Wrong password ar login.';
}
} else {
$error = 'Error 06 : Wrong token';
}
}
}
require 'header.php';
?>
<div class="titleBar">
<h1><?= $config['title']; ?> / Login</h1>
</div>
<form action="?" class="login" method="post">
<div class="alert" style="color: red;">
<?= $error; ?>
</div>
<label>Login</label>
<input type="text" name="login" required>
<label>Password</label>
<input type="password" name="password" required>
<input type="hidden" name="token" value="<?= CsrfToken::generateToken(); ?>">
<button type="submit">Login</button>
</form>
</body>
</html>

13
template/default/nav.php Normal file
View file

@ -0,0 +1,13 @@
<nav>
<span>Type d'installation</span>
<?php foreach ($menuData['type'] as $type) :; ?>
<a class="filter <?= $type; ?>" data-filter="<?= $type; ?>" onclick="toggleFilter(this, '<?= $type; ?>')"><img width="20px" height="20px" src="assets/icons/<?= $type; ?>.svg"> <?= $type; ?></a>
<?php endforeach; ?>
</nav>
<nav>
<span>Machine</span>
<?php foreach ($menuData['location'] as $type) :; ?>
<a class="filter <?= $type; ?>" data-filter="<?= $type; ?>" onclick="toggleFilter(this, '<?= $type; ?>')"><img width="20px" height="20px" src="assets/icons/server.svg"> <?= $type; ?></a>
<?php endforeach; ?>
</nav>

View file

@ -0,0 +1,54 @@
<?php
use Login\Login;
use Utils\CsrfToken;
use KTH\App;
$error = null;
$breadcrumbs = ' / Create user';
App::initializeDataDir();
if (!empty($_POST)) {
if (empty($_POST['login']) || empty($_POST['password']) || empty($_POST['role'])) {
$error = 'Please fill login, password and role.';
} else {
if (CsrfToken::validateToken($_POST['token'])) {
$login = new Login;
$addUser = $login->addUser($_POST['login'], $_POST['password'], $_POST['role']);
if ($addUser === true) {
header('Location: index.php');
} else {
$error = 'Error 02 - This user already exist';
}
} else {
$error = 'Error 07 : Wrong token';
}
}
}
require 'header.php';
?>
<div class="titleBar">
<h1><?= $config['title'] . $breadcrumbs; ?></h1>
</div>
<form action="?" class="login" method="post">
<div class="alert" style="color: red;">
<?= $error; ?>
</div>
<label>Login</label>
<input type="text" name="login" required>
<label>Password</label>
<input type="password" name="password" required>
<label>Rôle</label>
<select name="role" required>
<option value="user">User</option>
<option value="admin">Admin</option>
</select>
<input type="hidden" name="token" value="<?= CsrfToken::generateToken(); ?>">
<button type="submit">Create user</button>
</form>
</body>
</html>

View file

@ -0,0 +1,45 @@
<?php
use Utils\CsrfToken;
use Utils\Select;
use Utils\Debug;
$error = null;
$breadcrumbs = ' / Settings';
require 'header.php';
require 'titleBar.php';
?>
<form action="index.php" class="login" method="post">
<div class="alert" style="color: red;">
<?= $error; ?>
</div>
<label for="title">Title</label>
<input type="text" name="title" id="title" value="<?= $config['title']; ?>">
<label for="colorScheme">Color scheme</label>
<select name="colorScheme" id="colorScheme">
<option value="light" <?= Select::isSelected('light', $config['colorScheme']); ?>>Light</option>
<option value="dark" <?= Select::isSelected('dark', $config['colorScheme']); ?>>Dark</option>
</select>
<label for="view">View</label>
<select name="view" id="view">
<option value="full" <?= Select::isSelected('full', $config['view']); ?>>Full</option>
<option value="compact" <?= Select::isSelected('compact', $config['view']); ?>>Compact</option>
<option value="icons" <?= Select::isSelected('icons', $config['view']); ?>>Icons</option>
</select>
<p class="checkbox">
<label for="reimport">Reimport images and user files</label>
<input type="checkbox" name="reimport" id="reimport" value="1"/>
</p>
<input type="hidden" name="token" value="<?= CsrfToken::generateToken(); ?>">
<input type="hidden" name="settings" value="1" />
<button type="submit">Save</button>
</form>
<?php
require('footer.php');

View file

@ -0,0 +1,54 @@
<div class="titleBar">
<div class="linkList">
<a title="Toggle light/dark mode" href="#" onclick="switchTheme();">
<svg fill="#000000" version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 512 512" xml:space="preserve">
<g id="SVGRepo_bgCarrier" stroke-width="0"></g>
<g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g>
<g id="SVGRepo_iconCarrier">
<g>
<g>
<path d="M256,0C114.842,0,0,114.84,0,256s114.842,256,256,256s256-114.84,256-256S397.158,0,256,0z M322.225,451.558 c-20.797,7.062-43.071,10.894-66.225,10.894c-113.837,0-206.452-92.614-206.452-206.452S142.163,49.548,256,49.548 c23.154,0,45.429,3.832,66.226,10.894C266.612,107.439,231.226,177.657,231.226,256S266.612,404.561,322.225,451.558z"></path>
</g>
</g>
</g>
</svg></a>
<a title="Setting" href="index.php?settings=1"><svg fill="#000000" version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 363.715 363.715" xml:space="preserve">
<g id="SVGRepo_bgCarrier" stroke-width="0"></g>
<g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g>
<g id="SVGRepo_iconCarrier">
<g>
<path d="M236.25,222.275c0.865-3.233,0.421-6.608-1.252-9.506l-26.079-45.174c-2.232-3.864-6.393-6.267-10.862-6.267 c-2.186,0-4.347,0.582-6.249,1.681l-13.595,7.85c-5.525-4.053-11.5-7.526-17.834-10.332v-15.662 c0-6.908-5.621-12.526-12.527-12.526H95.688c-6.906,0-12.525,5.618-12.525,12.526v15.661c-6.335,2.806-12.309,6.28-17.835,10.333 l-13.595-7.849c-1.902-1.099-4.064-1.68-6.25-1.68c-4.468,0-8.629,2.401-10.861,6.266L8.542,212.768 c-1.673,2.899-2.118,6.274-1.253,9.507c0.867,3.232,2.939,5.934,5.836,7.605l13.557,7.826c-0.365,3.391-0.559,6.832-0.559,10.318 c0,3.486,0.193,6.928,0.559,10.319l-13.557,7.827c-2.898,1.672-4.969,4.373-5.836,7.606c-0.865,3.231-0.42,6.608,1.253,9.505 l26.079,45.174c2.232,3.865,6.394,6.266,10.861,6.266c2.186,0,4.348-0.58,6.25-1.68l13.596-7.849 c5.525,4.052,11.5,7.526,17.834,10.332v15.661c0,3.346,1.303,6.491,3.67,8.857c2.366,2.365,5.512,3.67,8.855,3.67h52.164 c6.906,0,12.527-5.62,12.527-12.527v-15.662c6.334-2.806,12.308-6.279,17.833-10.332l13.596,7.849c1.902,1.1,4.064,1.68,6.249,1.68 c4.47,0,8.63-2.4,10.862-6.266l26.079-45.174c1.673-2.897,2.117-6.273,1.252-9.505c-0.865-3.233-2.938-5.935-5.834-7.606 l-13.557-7.828c0.365-3.391,0.558-6.833,0.558-10.319c0-3.486-0.192-6.928-0.558-10.318l13.557-7.827 C233.313,228.209,235.385,225.508,236.25,222.275z M121.77,302.423c-30.043,0-54.396-24.354-54.396-54.397 c0-30.041,24.354-54.396,54.396-54.396s54.397,24.355,54.397,54.396C176.167,278.068,151.813,302.423,121.77,302.423z"></path>
<path d="M167.512,93.593c-0.572,2.14-0.277,4.374,0.83,6.29l17.256,29.892c1.479,2.559,4.231,4.146,7.188,4.146 c1.447,0,2.876-0.384,4.137-1.111l9.002-5.197c3.654,2.68,7.606,4.972,11.795,6.827v10.377c0,2.214,0.861,4.295,2.428,5.861 c1.566,1.566,3.647,2.427,5.86,2.427h34.517c4.57,0,8.29-3.718,8.29-8.288v-10.377c4.188-1.856,8.14-4.148,11.794-6.828 l9.004,5.198c1.258,0.728,2.688,1.111,4.135,1.111c2.957,0,5.711-1.588,7.188-4.146l17.256-29.892 c1.108-1.916,1.402-4.15,0.83-6.29c-0.574-2.139-1.944-3.926-3.861-5.033l-8.975-5.182c0.241-2.243,0.373-4.519,0.373-6.825 c0-2.306-0.132-4.581-0.373-6.825l8.975-5.181c1.917-1.107,3.287-2.895,3.861-5.034c0.572-2.139,0.277-4.372-0.83-6.29 l-17.256-29.892c-1.477-2.558-4.23-4.147-7.188-4.147c-1.447,0-2.877,0.385-4.135,1.113l-9.004,5.198 c-3.654-2.68-7.605-4.972-11.794-6.827V8.289c0-4.57-3.72-8.289-8.29-8.289h-34.517c-4.57,0-8.288,3.719-8.288,8.289v10.378 c-4.188,1.856-8.141,4.148-11.794,6.827l-9.003-5.198c-1.261-0.729-2.689-1.113-4.137-1.113c-2.956,0-5.709,1.59-7.188,4.147 l-17.256,29.892c-1.107,1.918-1.402,4.151-0.83,6.29c0.574,2.14,1.945,3.927,3.861,5.034l8.975,5.181 c-0.241,2.243-0.373,4.519-0.373,6.825c0,2.307,0.132,4.582,0.373,6.825l-8.975,5.182 C169.457,89.667,168.086,91.454,167.512,93.593z M243.266,40.558c19.881,0,35.996,16.116,35.996,35.995 s-16.115,35.995-35.996,35.995c-19.88,0-35.995-16.116-35.995-35.995S223.386,40.558,243.266,40.558z"></path>
<path d="M354.003,209.477l-6.179-3.567c0.167-1.544,0.258-3.111,0.258-4.699c0-1.588-0.091-3.154-0.258-4.699l6.179-3.567 c1.319-0.762,2.263-1.992,2.657-3.465c0.395-1.473,0.191-3.01-0.57-4.33l-11.88-20.576c-1.017-1.762-2.911-2.855-4.946-2.855 c-0.996,0-1.98,0.265-2.848,0.766l-6.197,3.578c-2.516-1.845-5.236-3.423-8.119-4.7v-7.144c0-3.145-2.56-5.706-5.705-5.706h-23.76 c-3.147,0-5.706,2.561-5.706,5.706v7.144c-2.884,1.277-5.603,2.855-8.119,4.7l-6.198-3.578c-0.866-0.501-1.851-0.766-2.847-0.766 c-2.035,0-3.931,1.093-4.946,2.855L252.94,185.15c-0.764,1.32-0.967,2.857-0.572,4.33c0.396,1.473,1.339,2.703,2.658,3.465 l6.18,3.567c-0.167,1.544-0.258,3.11-0.258,4.698c0,1.588,0.091,3.154,0.258,4.698l-6.18,3.567 c-1.319,0.761-2.263,1.99-2.658,3.464c-0.395,1.473-0.191,3.011,0.572,4.33l11.879,20.576c1.016,1.762,2.911,2.855,4.946,2.855 c0.996,0,1.98-0.266,2.847-0.766l6.198-3.578c2.516,1.845,5.235,3.422,8.119,4.7v7.144c0,1.523,0.593,2.957,1.671,4.034 c1.078,1.079,2.512,1.672,4.035,1.672h23.76c3.145,0,5.705-2.56,5.705-5.706v-7.144c2.883-1.277,5.604-2.855,8.119-4.7l6.197,3.578 c0.867,0.5,1.852,0.766,2.848,0.766c2.035,0,3.93-1.093,4.946-2.855l11.88-20.576c0.762-1.319,0.965-2.857,0.57-4.33 C356.266,211.467,355.322,210.237,354.003,209.477z M304.515,225.989c-13.686,0-24.778-11.095-24.778-24.778 c0-13.685,11.092-24.779,24.778-24.779c13.685,0,24.777,11.095,24.777,24.779C329.292,214.895,318.199,225.989,304.515,225.989z"></path>
</g>
</g>
</svg></a>
<a title="Help" href="#" onclick="showReadMore('userDoc')">
<svg fill="#000000" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 52 52" enable-background="new 0 0 52 52" xml:space="preserve">
<g id="SVGRepo_bgCarrier" stroke-width="0"></g>
<g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g>
<g id="SVGRepo_iconCarrier">
<g>
<g>
<path d="M28.4,38h-5c-0.8,0-1.4-0.6-1.4-1.4v-1.5c0-4.2,2.7-8,6.7-9.4c1.2-0.4,2.3-1.1,3.2-2.1 c5-6,0.4-13.2-5.6-13.4c-2.2-0.1-4.3,0.7-5.9,2.2c-1.3,1.2-2.1,2.7-2.3,4.4c-0.1,0.6-0.7,1.1-1.5,1.1h-5c-0.9,0-1.6-0.7-1.5-1.6 c0.4-3.8,2.1-7.2,4.8-9.9c3.2-3,7.3-4.6,11.7-4.5c8.3,0.3,15.1,7.1,15.4,15.4c0.3,7-4,13.3-10.5,15.7c-0.9,0.4-1.5,1.1-1.5,2v1.5 C30,37.4,29.2,38,28.4,38z"></path>
</g>
<path d="M30,48.5c0,0.8-0.7,1.5-1.5,1.5h-5c-0.8,0-1.5-0.7-1.5-1.5v-5c0-0.8,0.7-1.5,1.5-1.5h5 c0.8,0,1.5,0.7,1.5,1.5V48.5z"></path>
</g>
</g>
</svg></a>
<a title="Logout" href="index.php?logout=1">
<svg fill="#000000" version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 96.943 96.943" xml:space="preserve">
<g id="SVGRepo_bgCarrier" stroke-width="0"></g>
<g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g>
<g id="SVGRepo_iconCarrier">
<g>
<g>
<path d="M61.168,83.92H11.364V13.025H61.17c1.104,0,2-0.896,2-2V3.66c0-1.104-0.896-2-2-2H2c-1.104,0-2,0.896-2,2v89.623 c0,1.104,0.896,2,2,2h59.168c1.105,0,2-0.896,2-2V85.92C63.168,84.814,62.274,83.92,61.168,83.92z"></path>
<path d="M96.355,47.058l-26.922-26.92c-0.75-0.751-2.078-0.75-2.828,0l-6.387,6.388c-0.781,0.781-0.781,2.047,0,2.828 l12.16,12.162H19.737c-1.104,0-2,0.896-2,2v9.912c0,1.104,0.896,2,2,2h52.644L60.221,67.59c-0.781,0.781-0.781,2.047,0,2.828 l6.387,6.389c0.375,0.375,0.885,0.586,1.414,0.586c0.531,0,1.039-0.211,1.414-0.586l26.922-26.92 c0.375-0.375,0.586-0.885,0.586-1.414C96.943,47.941,96.73,47.433,96.355,47.058z"></path>
</g>
</g>
</g>
</svg></a>
</div>
<h1><?= $config['title'] . $breadcrumbs; ?></h1>
</div>