first commit

This commit is contained in:
小喵 2022-07-11 19:56:44 +08:00
commit 788cf3e917
Signed by: mt
GPG key ID: 2BCF198BD3341FB3
47 changed files with 2007 additions and 0 deletions

14
.env.example Normal file
View file

@ -0,0 +1,14 @@
# Discord OAuth Token
DISCORD_CLIENT_ID=
DISCORD_CLIENT_SECRET=
DISCORD_OAUTH_EXPIRES=86400
# Minecraft Query
MINECRAFT_QUERY_HOST=127.0.0.1
MINECRAFT_QUERY_PORT=25565
# Minecraft Remote Control
MINECRAFT_RCON_HOST=127.0.0.1
MINECRAFT_RCON_PORT=25575
MINECRAFT_RCON_PASSWORD=
MINECRAFT_RCON_TIMEOUT=3

203
.gitignore vendored Normal file
View file

@ -0,0 +1,203 @@
.env
sodium_key
# Created by https://www.toptal.com/developers/gitignore/api/phpstorm,windows,linux,macos,composer
# Edit at https://www.toptal.com/developers/gitignore?templates=phpstorm,windows,linux,macos,composer
### Composer ###
composer.phar
/vendor/
# Commit your application's lock file https://getcomposer.org/doc/01-basic-usage.md#commit-your-composer-lock-file-to-version-control
# You may choose to ignore a library lock file http://getcomposer.org/doc/02-libraries.md#lock-file
# composer.lock
### Linux ###
*~
# temporary files which can be created if a process still has a handle open of a deleted file
.fuse_hidden*
# KDE directory preferences
.directory
# Linux trash folder which might appear on any partition or disk
.Trash-*
# .nfs files are created when an open file is removed but is still being accessed
.nfs*
### macOS ###
# General
.DS_Store
.AppleDouble
.LSOverride
# Icon must end with two \r
Icon
# Thumbnails
._*
# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent
# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk
### macOS Patch ###
# iCloud generated files
*.icloud
### PhpStorm ###
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# User-specific stuff
.idea/**/workspace.xml
.idea/**/tasks.xml
.idea/**/usage.statistics.xml
.idea/**/dictionaries
.idea/**/shelf
# AWS User-specific
.idea/**/aws.xml
# Generated files
.idea/**/contentModel.xml
# Sensitive or high-churn files
.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
.idea/**/dbnavigator.xml
# Gradle
.idea/**/gradle.xml
.idea/**/libraries
# Gradle and Maven with auto-import
# When using Gradle or Maven with auto-import, you should exclude module files,
# since they will be recreated, and may cause churn. Uncomment if using
# auto-import.
# .idea/artifacts
# .idea/compiler.xml
# .idea/jarRepositories.xml
# .idea/modules.xml
# .idea/*.iml
# .idea/modules
# *.iml
# *.ipr
# CMake
cmake-build-*/
# Mongo Explorer plugin
.idea/**/mongoSettings.xml
# File-based project format
*.iws
# IntelliJ
out/
# mpeltonen/sbt-idea plugin
.idea_modules/
# JIRA plugin
atlassian-ide-plugin.xml
# Cursive Clojure plugin
.idea/replstate.xml
# SonarLint plugin
.idea/sonarlint/
# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties
# Editor-based Rest Client
.idea/httpRequests
# Android studio 3.1+ serialized cache file
.idea/caches/build_file_checksums.ser
### PhpStorm Patch ###
# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721
# *.iml
# modules.xml
# .idea/misc.xml
# *.ipr
# Sonarlint plugin
# https://plugins.jetbrains.com/plugin/7973-sonarlint
.idea/**/sonarlint/
# SonarQube Plugin
# https://plugins.jetbrains.com/plugin/7238-sonarqube-community-plugin
.idea/**/sonarIssues.xml
# Markdown Navigator plugin
# https://plugins.jetbrains.com/plugin/7896-markdown-navigator-enhanced
.idea/**/markdown-navigator.xml
.idea/**/markdown-navigator-enh.xml
.idea/**/markdown-navigator/
# Cache file creation bug
# See https://youtrack.jetbrains.com/issue/JBR-2257
.idea/$CACHE_FILE$
# CodeStream plugin
# https://plugins.jetbrains.com/plugin/12206-codestream
.idea/codestream.xml
# Azure Toolkit for IntelliJ plugin
# https://plugins.jetbrains.com/plugin/8053-azure-toolkit-for-intellij
.idea/**/azureSettings.xml
### Windows ###
# Windows thumbnail cache files
Thumbs.db
Thumbs.db:encryptable
ehthumbs.db
ehthumbs_vista.db
# Dump file
*.stackdump
# Folder config file
[Dd]esktop.ini
# Recycle Bin used on file shares
$RECYCLE.BIN/
# Windows Installer files
*.cab
*.msi
*.msix
*.msm
*.msp
# Windows shortcuts
*.lnk
# End of https://www.toptal.com/developers/gitignore/api/phpstorm,windows,linux,macos,composer

8
.idea/.gitignore generated vendored Normal file
View file

@ -0,0 +1,8 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

30
.idea/akanyan.iml generated Normal file
View file

@ -0,0 +1,30 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/app" isTestSource="false" packagePrefix="MingTsay\Akanyan\" />
<sourceFolder url="file://$MODULE_DIR$/spec" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/tests" isTestSource="true" />
<excludeFolder url="file://$MODULE_DIR$/vendor/composer" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/polyfill-mbstring" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/polyfill-php80" />
<excludeFolder url="file://$MODULE_DIR$/vendor/phpoption/phpoption" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/polyfill-ctype" />
<excludeFolder url="file://$MODULE_DIR$/vendor/graham-campbell/result-type" />
<excludeFolder url="file://$MODULE_DIR$/vendor/vlucas/phpdotenv" />
<excludeFolder url="file://$MODULE_DIR$/vendor/twbs/bootstrap" />
<excludeFolder url="file://$MODULE_DIR$/vendor/league/plates" />
<excludeFolder url="file://$MODULE_DIR$/vendor/components/font-awesome" />
<excludeFolder url="file://$MODULE_DIR$/vendor/xpaw/php-minecraft-query" />
<excludeFolder url="file://$MODULE_DIR$/vendor/xpaw/php-source-query-class" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="library" name="default" level="application" />
<orderEntry type="library" name="highlight" level="application" />
<orderEntry type="library" name="highlight" level="application" />
<orderEntry type="library" name="default" level="application" />
<orderEntry type="library" name="highlight.js" level="application" />
<orderEntry type="library" name="highlight.js" level="application" />
</component>
</module>

6
.idea/jsLibraryMappings.xml generated Normal file
View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="JavaScriptLibraryMappings">
<file url="PROJECT" libraries="{default, highlight, highlight.js}" />
</component>
</project>

13
.idea/misc.xml generated Normal file
View file

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="UnattendedHostPersistenceState">
<option name="openedFilesInfos">
<list>
<OpenedFileInfo>
<option name="caretOffset" value="163" />
<option name="fileUrl" value="file://$PROJECT_DIR$/templates/partials/menu.php" />
</OpenedFileInfo>
</list>
</option>
</component>
</project>

8
.idea/modules.xml generated Normal file
View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/akanyan.iml" filepath="$PROJECT_DIR$/.idea/akanyan.iml" />
</modules>
</component>
</project>

25
.idea/php.xml generated Normal file
View file

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="PhpIncludePathManager">
<include_path>
<path value="$PROJECT_DIR$/vendor/composer" />
<path value="$PROJECT_DIR$/vendor/symfony/polyfill-mbstring" />
<path value="$PROJECT_DIR$/vendor/symfony/polyfill-php80" />
<path value="$PROJECT_DIR$/vendor/phpoption/phpoption" />
<path value="$PROJECT_DIR$/vendor/symfony/polyfill-ctype" />
<path value="$PROJECT_DIR$/vendor/graham-campbell/result-type" />
<path value="$PROJECT_DIR$/vendor/vlucas/phpdotenv" />
<path value="$PROJECT_DIR$/vendor/twbs/bootstrap" />
<path value="$PROJECT_DIR$/vendor/league/plates" />
<path value="$PROJECT_DIR$/vendor/components/font-awesome" />
<path value="$PROJECT_DIR$/vendor/xpaw/php-minecraft-query" />
<path value="$PROJECT_DIR$/vendor/xpaw/php-source-query-class" />
</include_path>
</component>
<component name="PhpProjectSharedConfiguration" php_language_level="8.1" />
<component name="PhpUnit">
<phpunit_settings>
<PhpUnitSettings custom_loader_path="$PROJECT_DIR$/vendor/autoload.php" />
</phpunit_settings>
</component>
</project>

6
.idea/vcs.xml generated Normal file
View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

14
.idea/webResources.xml generated Normal file
View file

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="WebResourcesPaths">
<contentEntries>
<entry url="file://$PROJECT_DIR$">
<entryData>
<resourceRoots>
<path value="file://$PROJECT_DIR$/webroot" />
</resourceRoots>
</entryData>
</entry>
</contentEntries>
</component>
</project>

198
app/App.php Normal file
View file

@ -0,0 +1,198 @@
<?php
namespace MingTsay\Akanyan;
use Exception;
use JetBrains\PhpStorm\NoReturn;
use League\Plates\Engine;
use MingTsay\Akanyan\Discord\Me;
use MingTsay\Akanyan\Discord\Token;
require_once __DIR__ . '/../env.php';
class App
{
private const allowedUsers = [
305331852225544193, // 小喵#3521
307567957863694348, // Aᴋᴀɴʏᴀɴ#2870
559026859547951104, // 2072#7474
];
public static function getTemplate(): Engine
{
static $templates = new Engine(__DIR__ . '/../templates');
$me = self::me();
$isLogin = self::checkU();
$templates->addData([
'me' => $me,
'whoami' => "$me->username#$me->userDiscriminator",
'noStatus' => false,
'isLogin' => $isLogin,
'isAllowed' => $isLogin && $me !== null && self::isAllowed($me->userId),
]);
return $templates;
}
public static function getU(): ?Token
{
return Auth::decrypt($_COOKIE['u'] ?? '');
}
public static function checkU(): bool
{
$token = self::getU();
if ($token === null) return false;
if ($token->timestamp + $token->expires_in < time()) return false;
return self::me() !== null;
}
public static function setU(Token $token): void
{
try {
setcookie('u', Auth::encrypt($token), [
'expires' => $token->timestamp + $token->expires_in,
'path' => '/',
'domain' => 'akanyan.oho.tw',
'samesite' => 'None',
'secure' => true,
'httponly' => true,
]);
} catch (Exception) {
error_log('Failed to setU.');
}
}
public static function unsetU(): void
{
try {
setcookie('u', null, [
'expires' => time() - 3600,
'path' => '/',
'domain' => 'akanyan.oho.tw',
'samesite' => 'None',
'secure' => true,
'httponly' => true,
]);
} catch (Exception) {
error_log('Failed to unsetU.');
}
}
public static function requireAuth(): void
{
if (!self::checkU()) {
header('location: /login.php');
http_response_code(302);
exit;
}
}
public static function requireNonAuth(): void
{
if (self::checkU()) {
header('location: /');
http_response_code(302);
exit;
}
}
public static function requireAllowed(): void
{
self::requireAuth();
$me = self::me();
if ($me === null || !self::isAllowed($me->userId)) {
self::template([
'title' => '您無權限使用本系統',
'body' => <<<HTML
<div class="mt-5">
<p>您的 Discord 帳號不在白名單中。</p>
<p>若您認為這是個錯誤,請聯絡 <a href="https://discordapp.com/users/305331852225544193" target="_blank">小喵#3521</a> 並提供您的使用者編號 <code>$me->userId</code>。</p>
</div>
HTML,
]);
}
}
public static function auth(string $code): void
{
try {
$token = Auth::getTokenByCode($code);
if ($token !== null) self::setU($token);
} catch (Exception) {
error_log('Failed to getTokenByCode.');
}
}
public static function me(): ?Me
{
try {
$u = self::getU();
if ($u === null) return null;
return Auth::getMe($u);
} catch (Exception) {
error_log('Failed to getMe.');
return null;
}
}
#[NoReturn]
public static function template(array $params): void
{
$status = '';
if (self::checkU() && !$params['no_status']) {
$me = self::me();
$status = <<<HTML
<div>您已使用 <code>$me->username#$me->userDiscriminator</code> 登入,點選此處以<a href="/logout.php">登出系統</a>。</div>
HTML;
}
$html = <<<HTML
<!DOCTYPE html>
<html lang="zh-Hant-TW">
<head>
<meta charset="utf-8"/>
<meta http-equiv="X-UA-Compatible" content="IE=edge"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<title>$params[title]</title>
<link rel="stylesheet" href="/bootstrap/css/bootstrap.min.css"/>
<link rel="stylesheet" href="/style.css"/>
</head>
<body class="text-center">
<h1 class="h3 mb-3 font-weight-normal">$params[title]</h1>
$status
<main>$params[body]</main>
<footer class="mt-5 mb-3 text-muted">Copyright &copy; 2022 Ming Tsay. All rights reserved.</footer>
<script src="/bootstrap/js/bootstrap.bundle.min.js"></script>
</body>
</html>
HTML;
header('Content-Type: text/html; charset=utf-8');
header('Content-Length: ' . strlen($html));
http_response_code(200);
echo($html);
exit;
}
public static function authUrl(): string
{
return Auth::authorize();
}
public static function isAllowed(string $userId): bool
{
return in_array($userId, self::allowedUsers);
}
#[NoReturn]
public static function render(string $name, array $data = []): void
{
$html = self::getTemplate()->render($name, $data);
header('Content-Type: text/html; charset=utf-8');
header('Content-Length: ' . strlen($html));
http_response_code(200);
echo($html);
exit;
}
}

80
app/Auth.php Normal file
View file

@ -0,0 +1,80 @@
<?php
namespace MingTsay\Akanyan;
use Exception;
use MingTsay\Akanyan\Discord\Me;
use MingTsay\Akanyan\Discord\Token;
require_once __DIR__ . '/../env.php';
class Auth
{
private const DISCORD_REDIRECT_URI = 'https://akanyan.oho.tw/auth.php?source=discord';
public static function encrypt(Token $payload): ?string
{
$key = file_get_contents(__DIR__ . '/../sodium_key');
try {
$nonce = random_bytes(SODIUM_CRYPTO_AEAD_XCHACHA20POLY1305_IETF_NPUBBYTES);
$cipher = sodium_crypto_aead_xchacha20poly1305_ietf_encrypt(json_encode($payload), '', $nonce, $key);
return implode(';', array_map('base64_encode', [$nonce, $cipher]));
} catch (Exception) {
return null;
}
}
public static function decrypt(string $u): ?Token
{
$key = file_get_contents(__DIR__ . '/../sodium_key');
try {
list($nonce, $cipher) = array_map('base64_decode', explode(';', $u));
$payload = json_decode(sodium_crypto_aead_xchacha20poly1305_ietf_decrypt($cipher, '', $nonce, $key));
if ($payload === null) return null;
return new Token($payload);
} catch (Exception) {
return null;
}
}
public static function authorize(): string
{
$query_string = http_build_query([
'client_id' => $_ENV['DISCORD_CLIENT_ID'],
'redirect_uri' => self::DISCORD_REDIRECT_URI,
'response_type' => 'code',
'scope' => 'identify',
]);
return "https://discord.com/api/oauth2/authorize?$query_string";
}
public static function getTokenByCode(string $code): ?Token
{
$payload = json_decode(file_get_contents('https://discord.com/api/oauth2/token', false, stream_context_create([
'http' => [
'method' => 'POST',
'header' => 'Content-Type: application/x-www-form-urlencoded',
'content' => http_build_query([
'client_id' => $_ENV['DISCORD_CLIENT_ID'],
'client_secret' => $_ENV['DISCORD_CLIENT_SECRET'],
'grant_type' => 'authorization_code',
'code' => $code,
'redirect_uri' => self::DISCORD_REDIRECT_URI,
]),
],
])));
if ($payload === null) return null;
return new Token($payload);
}
public static function getMe(Token $getU): ?Me
{
return new Me(json_decode(file_get_contents('https://discord.com/api/oauth2/@me', false, stream_context_create([
'http' => [
'method' => 'GET',
'header' => "Authorization: Bearer $getU->access_token",
],
]))));
}
}

32
app/ConfigFilesViewer.php Normal file
View file

@ -0,0 +1,32 @@
<?php
namespace MingTsay\Akanyan;
class ConfigFilesViewer extends FileViewer
{
protected static function directory(): string
{
return '/media/sdc/mt/minecraft-docker-image/data';
}
protected static function whitelist(): ?array
{
return [
'banned-ips.json',
'banned-players.json',
'ops.json',
'server.properties',
'usercache.json',
'whitelist.json',
];
}
public static function read($file): ?string
{
$content = parent::read($file);
if ($content !== null && $file === 'server.properties')
$content = preg_replace('/^(rcon\.password)=(.*)$/m', '$1=***hidden***', $content);
return $content;
}
}

39
app/Discord/Me.php Normal file
View file

@ -0,0 +1,39 @@
<?php
namespace MingTsay\Akanyan\Discord;
use stdClass;
class Me extends stdClass
{
public string $appId;
public string $appName;
public ?string $appIcon;
public string $appDescription;
public bool $appHook;
public string $appVerifyKey;
public array $scopes;
public int $expires;
public string $userId;
public string $username;
public string $userAvatar;
public string $userDiscriminator;
public string $userPublicFlags;
public function __construct(stdClass $payload)
{
$this->appId = $payload->application->id;
$this->appName = $payload->application->name;
$this->appIcon = $payload->application->icon;
$this->appDescription = $payload->application->description;
$this->appHook = $payload->application->hook;
$this->appVerifyKey = $payload->application->verify_key;
$this->scopes = $payload->scopes;
$this->expires = strtotime($payload->expires);
$this->userId = $payload->user->id;
$this->username = $payload->user->username;
$this->userAvatar = $payload->user->avatar;
$this->userDiscriminator = $payload->user->discriminator;
$this->userPublicFlags = $payload->user->public_flags;
}
}

27
app/Discord/Token.php Normal file
View file

@ -0,0 +1,27 @@
<?php
namespace MingTsay\Akanyan\Discord;
use stdClass;
class Token extends stdClass
{
public string $access_token;
public string $token_type;
public int $expires_in;
public string $refresh_token;
public string $scope;
public int $timestamp;
public function __construct(stdClass $payload)
{
$this->access_token = $payload->access_token;
$this->token_type = $payload->token_type;
$this->expires_in = $payload->expires_in;
$this->refresh_token = $payload->refresh_token;
$this->scope = $payload->scope;
$this->timestamp = time();
}
}

33
app/FileViewer.php Normal file
View file

@ -0,0 +1,33 @@
<?php
namespace MingTsay\Akanyan;
abstract class FileViewer
{
protected abstract static function directory(): string;
protected abstract static function whitelist(): ?array;
public static function list(): array
{
$directory = static::directory();
$whitelist = static::whitelist();
return array_values(array_filter(
scandir($directory),
fn($file) => ($whitelist === null || in_array($file, $whitelist)) && is_file("$directory/$file")
));
}
public static function read($file): ?string
{
$directory = static::directory();
$filename = "$directory/$file";
if (!in_array($file, self::list()) || !file_exists($filename) || !is_file($filename)) return null;
$extension = pathinfo($filename, PATHINFO_EXTENSION);
$content = file_get_contents($filename);
return $extension === 'gz' ? gzdecode($content) : $content;
}
}

17
app/LogsViewer.php Normal file
View file

@ -0,0 +1,17 @@
<?php
namespace MingTsay\Akanyan;
class LogsViewer extends FileViewer
{
protected static function directory(): string
{
return '/media/sdc/mt/minecraft-docker-image/data/logs';
}
protected static function whitelist(): ?array
{
return null;
}
}

25
app/Minecraft.php Normal file
View file

@ -0,0 +1,25 @@
<?php
namespace MingTsay\Akanyan;
use xPaw\MinecraftPing;
use xPaw\MinecraftPingException;
class Minecraft
{
public static function query(): ?array
{
$host = $_ENV['MINECRAFT_QUERY_HOST'];
$port = (int)$_ENV['MINECRAFT_QUERY_PORT'];
$query = new MinecraftPing($host, $port);
try {
$result = $query->Query();
} catch (MinecraftPingException) {
$result = null;
} finally {
$query->Close();
}
return $result ?: null;
}
}

28
composer.json Normal file
View file

@ -0,0 +1,28 @@
{
"name": "mingtsay/akanyan",
"description": "description",
"minimum-stability": "stable",
"license": "proprietary",
"authors": [
{
"name": "Ming Tsay",
"email": "mt@mingtsay.tw"
}
],
"require": {
"ext-iconv": "*",
"ext-sodium": "*",
"ext-zlib": "*",
"components/font-awesome": "^6.1",
"league/plates": "^3.4",
"twbs/bootstrap": "^5.1",
"vlucas/phpdotenv": "^5.4",
"xpaw/php-minecraft-query": "^4.0",
"xpaw/php-source-query-class": "^2.1"
},
"autoload": {
"psr-4": {
"MingTsay\\Akanyan\\": "app/"
}
}
}

734
composer.lock generated Normal file
View file

@ -0,0 +1,734 @@
{
"_readme": [
"This file locks the dependencies of your project to a known state",
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "528c716c3d44b77191eaef0461fe108d",
"packages": [
{
"name": "components/font-awesome",
"version": "6.1.1",
"source": {
"type": "git",
"url": "https://github.com/components/font-awesome.git",
"reference": "9c95a6a9572933bc964f05b810f0808a5de00ee1"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/components/font-awesome/zipball/9c95a6a9572933bc964f05b810f0808a5de00ee1",
"reference": "9c95a6a9572933bc964f05b810f0808a5de00ee1",
"shasum": ""
},
"type": "component",
"extra": {
"component": {
"styles": [
"css/all.css"
],
"files": [
"css/all.min.css",
"webfonts/*"
]
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"CC-BY-4.0",
"MIT",
"OFL-1.1"
],
"description": "Font Awesome, the iconic SVG, font, and CSS framework.",
"support": {
"issues": "https://github.com/components/font-awesome/issues",
"source": "https://github.com/components/font-awesome/tree/6.1.1"
},
"time": "2022-04-18T13:15:48+00:00"
},
{
"name": "graham-campbell/result-type",
"version": "v1.0.4",
"source": {
"type": "git",
"url": "https://github.com/GrahamCampbell/Result-Type.git",
"reference": "0690bde05318336c7221785f2a932467f98b64ca"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/GrahamCampbell/Result-Type/zipball/0690bde05318336c7221785f2a932467f98b64ca",
"reference": "0690bde05318336c7221785f2a932467f98b64ca",
"shasum": ""
},
"require": {
"php": "^7.0 || ^8.0",
"phpoption/phpoption": "^1.8"
},
"require-dev": {
"phpunit/phpunit": "^6.5.14 || ^7.5.20 || ^8.5.19 || ^9.5.8"
},
"type": "library",
"autoload": {
"psr-4": {
"GrahamCampbell\\ResultType\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Graham Campbell",
"email": "hello@gjcampbell.co.uk",
"homepage": "https://github.com/GrahamCampbell"
}
],
"description": "An Implementation Of The Result Type",
"keywords": [
"Graham Campbell",
"GrahamCampbell",
"Result Type",
"Result-Type",
"result"
],
"support": {
"issues": "https://github.com/GrahamCampbell/Result-Type/issues",
"source": "https://github.com/GrahamCampbell/Result-Type/tree/v1.0.4"
},
"funding": [
{
"url": "https://github.com/GrahamCampbell",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/graham-campbell/result-type",
"type": "tidelift"
}
],
"time": "2021-11-21T21:41:47+00:00"
},
{
"name": "league/plates",
"version": "v3.4.0",
"source": {
"type": "git",
"url": "https://github.com/thephpleague/plates.git",
"reference": "6d3ee31199b536a4e003b34a356ca20f6f75496a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/thephpleague/plates/zipball/6d3ee31199b536a4e003b34a356ca20f6f75496a",
"reference": "6d3ee31199b536a4e003b34a356ca20f6f75496a",
"shasum": ""
},
"require": {
"php": "^7.0|^8.0"
},
"require-dev": {
"mikey179/vfsstream": "^1.6",
"phpunit/phpunit": "^9.5",
"squizlabs/php_codesniffer": "^3.5"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "3.0-dev"
}
},
"autoload": {
"psr-4": {
"League\\Plates\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Jonathan Reinink",
"email": "jonathan@reinink.ca",
"role": "Developer"
},
{
"name": "RJ Garcia",
"email": "ragboyjr@icloud.com",
"role": "Developer"
}
],
"description": "Plates, the native PHP template system that's fast, easy to use and easy to extend.",
"homepage": "https://platesphp.com",
"keywords": [
"league",
"package",
"templates",
"templating",
"views"
],
"support": {
"issues": "https://github.com/thephpleague/plates/issues",
"source": "https://github.com/thephpleague/plates/tree/v3.4.0"
},
"time": "2020-12-25T05:00:37+00:00"
},
{
"name": "phpoption/phpoption",
"version": "1.8.1",
"source": {
"type": "git",
"url": "https://github.com/schmittjoh/php-option.git",
"reference": "eab7a0df01fe2344d172bff4cd6dbd3f8b84ad15"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/schmittjoh/php-option/zipball/eab7a0df01fe2344d172bff4cd6dbd3f8b84ad15",
"reference": "eab7a0df01fe2344d172bff4cd6dbd3f8b84ad15",
"shasum": ""
},
"require": {
"php": "^7.0 || ^8.0"
},
"require-dev": {
"bamarni/composer-bin-plugin": "^1.4.1",
"phpunit/phpunit": "^6.5.14 || ^7.5.20 || ^8.5.19 || ^9.5.8"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.8-dev"
}
},
"autoload": {
"psr-4": {
"PhpOption\\": "src/PhpOption/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"Apache-2.0"
],
"authors": [
{
"name": "Johannes M. Schmitt",
"email": "schmittjoh@gmail.com",
"homepage": "https://github.com/schmittjoh"
},
{
"name": "Graham Campbell",
"email": "hello@gjcampbell.co.uk",
"homepage": "https://github.com/GrahamCampbell"
}
],
"description": "Option Type for PHP",
"keywords": [
"language",
"option",
"php",
"type"
],
"support": {
"issues": "https://github.com/schmittjoh/php-option/issues",
"source": "https://github.com/schmittjoh/php-option/tree/1.8.1"
},
"funding": [
{
"url": "https://github.com/GrahamCampbell",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/phpoption/phpoption",
"type": "tidelift"
}
],
"time": "2021-12-04T23:24:31+00:00"
},
{
"name": "symfony/polyfill-ctype",
"version": "v1.26.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-ctype.git",
"reference": "6fd1b9a79f6e3cf65f9e679b23af304cd9e010d4"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/6fd1b9a79f6e3cf65f9e679b23af304cd9e010d4",
"reference": "6fd1b9a79f6e3cf65f9e679b23af304cd9e010d4",
"shasum": ""
},
"require": {
"php": ">=7.1"
},
"provide": {
"ext-ctype": "*"
},
"suggest": {
"ext-ctype": "For best performance"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "1.26-dev"
},
"thanks": {
"name": "symfony/polyfill",
"url": "https://github.com/symfony/polyfill"
}
},
"autoload": {
"files": [
"bootstrap.php"
],
"psr-4": {
"Symfony\\Polyfill\\Ctype\\": ""
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Gert de Pagter",
"email": "BackEndTea@gmail.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony polyfill for ctype functions",
"homepage": "https://symfony.com",
"keywords": [
"compatibility",
"ctype",
"polyfill",
"portable"
],
"support": {
"source": "https://github.com/symfony/polyfill-ctype/tree/v1.26.0"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2022-05-24T11:49:31+00:00"
},
{
"name": "symfony/polyfill-mbstring",
"version": "v1.26.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-mbstring.git",
"reference": "9344f9cb97f3b19424af1a21a3b0e75b0a7d8d7e"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/9344f9cb97f3b19424af1a21a3b0e75b0a7d8d7e",
"reference": "9344f9cb97f3b19424af1a21a3b0e75b0a7d8d7e",
"shasum": ""
},
"require": {
"php": ">=7.1"
},
"provide": {
"ext-mbstring": "*"
},
"suggest": {
"ext-mbstring": "For best performance"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "1.26-dev"
},
"thanks": {
"name": "symfony/polyfill",
"url": "https://github.com/symfony/polyfill"
}
},
"autoload": {
"files": [
"bootstrap.php"
],
"psr-4": {
"Symfony\\Polyfill\\Mbstring\\": ""
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony polyfill for the Mbstring extension",
"homepage": "https://symfony.com",
"keywords": [
"compatibility",
"mbstring",
"polyfill",
"portable",
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-mbstring/tree/v1.26.0"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2022-05-24T11:49:31+00:00"
},
{
"name": "symfony/polyfill-php80",
"version": "v1.26.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-php80.git",
"reference": "cfa0ae98841b9e461207c13ab093d76b0fa7bace"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/cfa0ae98841b9e461207c13ab093d76b0fa7bace",
"reference": "cfa0ae98841b9e461207c13ab093d76b0fa7bace",
"shasum": ""
},
"require": {
"php": ">=7.1"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "1.26-dev"
},
"thanks": {
"name": "symfony/polyfill",
"url": "https://github.com/symfony/polyfill"
}
},
"autoload": {
"files": [
"bootstrap.php"
],
"psr-4": {
"Symfony\\Polyfill\\Php80\\": ""
},
"classmap": [
"Resources/stubs"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Ion Bazan",
"email": "ion.bazan@gmail.com"
},
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions",
"homepage": "https://symfony.com",
"keywords": [
"compatibility",
"polyfill",
"portable",
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-php80/tree/v1.26.0"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2022-05-10T07:21:04+00:00"
},
{
"name": "twbs/bootstrap",
"version": "v5.1.3",
"source": {
"type": "git",
"url": "https://github.com/twbs/bootstrap.git",
"reference": "1a6fdfae6be09b09eaced8f0e442ca6f7680a61e"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/twbs/bootstrap/zipball/1a6fdfae6be09b09eaced8f0e442ca6f7680a61e",
"reference": "1a6fdfae6be09b09eaced8f0e442ca6f7680a61e",
"shasum": ""
},
"replace": {
"twitter/bootstrap": "self.version"
},
"type": "library",
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Mark Otto",
"email": "markdotto@gmail.com"
},
{
"name": "Jacob Thornton",
"email": "jacobthornton@gmail.com"
}
],
"description": "The most popular front-end framework for developing responsive, mobile first projects on the web.",
"homepage": "https://getbootstrap.com/",
"keywords": [
"JS",
"css",
"framework",
"front-end",
"mobile-first",
"responsive",
"sass",
"web"
],
"support": {
"issues": "https://github.com/twbs/bootstrap/issues",
"source": "https://github.com/twbs/bootstrap/tree/v5.1.3"
},
"time": "2021-10-09T06:43:19+00:00"
},
{
"name": "vlucas/phpdotenv",
"version": "v5.4.1",
"source": {
"type": "git",
"url": "https://github.com/vlucas/phpdotenv.git",
"reference": "264dce589e7ce37a7ba99cb901eed8249fbec92f"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/264dce589e7ce37a7ba99cb901eed8249fbec92f",
"reference": "264dce589e7ce37a7ba99cb901eed8249fbec92f",
"shasum": ""
},
"require": {
"ext-pcre": "*",
"graham-campbell/result-type": "^1.0.2",
"php": "^7.1.3 || ^8.0",
"phpoption/phpoption": "^1.8",
"symfony/polyfill-ctype": "^1.23",
"symfony/polyfill-mbstring": "^1.23.1",
"symfony/polyfill-php80": "^1.23.1"
},
"require-dev": {
"bamarni/composer-bin-plugin": "^1.4.1",
"ext-filter": "*",
"phpunit/phpunit": "^7.5.20 || ^8.5.21 || ^9.5.10"
},
"suggest": {
"ext-filter": "Required to use the boolean validator."
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "5.4-dev"
}
},
"autoload": {
"psr-4": {
"Dotenv\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
],
"authors": [
{
"name": "Graham Campbell",
"email": "hello@gjcampbell.co.uk",
"homepage": "https://github.com/GrahamCampbell"
},
{
"name": "Vance Lucas",
"email": "vance@vancelucas.com",
"homepage": "https://github.com/vlucas"
}
],
"description": "Loads environment variables from `.env` to `getenv()`, `$_ENV` and `$_SERVER` automagically.",
"keywords": [
"dotenv",
"env",
"environment"
],
"support": {
"issues": "https://github.com/vlucas/phpdotenv/issues",
"source": "https://github.com/vlucas/phpdotenv/tree/v5.4.1"
},
"funding": [
{
"url": "https://github.com/GrahamCampbell",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/vlucas/phpdotenv",
"type": "tidelift"
}
],
"time": "2021-12-12T23:22:04+00:00"
},
{
"name": "xpaw/php-minecraft-query",
"version": "4.0.1",
"source": {
"type": "git",
"url": "https://github.com/xPaw/PHP-Minecraft-Query.git",
"reference": "c7257e7c3d18c14cd476910f1516f93f405f54ee"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/xPaw/PHP-Minecraft-Query/zipball/c7257e7c3d18c14cd476910f1516f93f405f54ee",
"reference": "c7257e7c3d18c14cd476910f1516f93f405f54ee",
"shasum": ""
},
"require": {
"php": ">=7.0"
},
"type": "library",
"autoload": {
"psr-4": {
"xPaw\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"description": "PHP library to query Minecraft servers",
"keywords": [
"minecraft"
],
"support": {
"source": "https://github.com/xPaw/PHP-Minecraft-Query/tree/4.0.1"
},
"funding": [
{
"url": "https://github.com/xPaw",
"type": "github"
}
],
"time": "2021-06-18T13:18:40+00:00"
},
{
"name": "xpaw/php-source-query-class",
"version": "2.1.0",
"source": {
"type": "git",
"url": "https://github.com/xPaw/PHP-Source-Query.git",
"reference": "673e572233c3ab4b70b302d648ebaf5a8d9ba32b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/xPaw/PHP-Source-Query/zipball/673e572233c3ab4b70b302d648ebaf5a8d9ba32b",
"reference": "673e572233c3ab4b70b302d648ebaf5a8d9ba32b",
"shasum": ""
},
"require": {
"php": ">=7.4"
},
"require-dev": {
"phpunit/phpunit": "9.2",
"vimeo/psalm": "^3.12"
},
"type": "library",
"autoload": {
"psr-4": {
"xPaw\\SourceQuery\\": "SourceQuery/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"LGPL-2.1"
],
"description": "PHP library to query and send RCON commands to servers based on \"Source Engine Query\" protocol",
"homepage": "https://github.com/xPaw/PHP-Source-Query",
"keywords": [
"ark",
"counter-strike",
"csgo",
"gmod",
"minecraft",
"rcon",
"rust",
"starbound",
"team fortress"
],
"support": {
"source": "https://github.com/xPaw/PHP-Source-Query/tree/2.1.0"
},
"funding": [
{
"url": "https://github.com/xPaw",
"type": "github"
}
],
"time": "2020-12-04T08:20:42+00:00"
}
],
"packages-dev": [],
"aliases": [],
"minimum-stability": "stable",
"stability-flags": [],
"prefer-stable": false,
"prefer-lowest": false,
"platform": {
"ext-iconv": "*",
"ext-sodium": "*",
"ext-zlib": "*"
},
"platform-dev": [],
"plugin-api-version": "2.3.0"
}

7
env.php Normal file
View file

@ -0,0 +1,7 @@
<?php
use Dotenv\Dotenv;
require_once __DIR__ . '/vendor/autoload.php';
Dotenv::createImmutable(__DIR__)->load();

View file

@ -0,0 +1,6 @@
<?php
$this->layout('partials/file/list', [
'title' => '選擇設定檔',
'nav' => 'configurations',
'list' => $list ?? [],
]);

View file

@ -0,0 +1,6 @@
<?php
$this->layout('partials/file/not-found', [
'title' => '檢視設定檔',
'nav' => 'configurations',
'list' => $list ?? [],
]);

View file

@ -0,0 +1,9 @@
<?php
$this->layout('partials/file/view', [
'title' => '檢視設定檔',
'nav' => 'configurations',
'list' => $list ?? [],
'file' => $file ?? '',
'type' => $type ?? 'plaintext',
'content' => $content ?? '',
]);

68
templates/index.php Normal file
View file

@ -0,0 +1,68 @@
<?php $this->layout('template', ['title' => '首頁']) ?>
<div class="card m-3 mt-5 mx-auto w-fit-content">
<div class="card-body">
<div class="d-flex justify-content-center flex-column flex-lg-row">
<div class="mx-2">伺服器狀態:<code><?= $this->e($status ?? '') ?></code></div>
<?php if (isset($query)): ?>
<?php if (isset($query['version']) && isset($query['version']['name'])): ?>
<div class="mx-2">伺服器版本:<code><?= $this->e($query['version']['name']) ?></code></div>
<?php endif ?>
<?php if (isset($query['players'])): ?>
<?php if (isset($query['players']['online']) && isset($query['players']['max'])): ?>
<div class="mx-2">
線上人數:
<code><?= $this->e($query['players']['online']) ?></code>
/
<code><?= $this->e($query['players']['max']) ?></code>
</div>
<?php endif ?>
<?php if (isset($query['players']['sample']) && count($query['players']['sample']) > 0): ?>
<ul class="mx-2 list-group list-group-horizontal-lg">
<?php foreach ($query['players']['sample'] as $player) : ?>
<li class="py-0 list-group-item"><code><?= $this->e($player['name']) ?></code></li>
<?php endforeach ?>
</ul>
<?php endif ?>
<?php endif ?>
<?php endif ?>
</div>
<?php if (isset($isStarting) && $isStarting): ?>
<div class="alert alert-success my-3 mx-auto d-table" role="alert">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor"
class="bi bi-exclamation-triangle-fill flex-shrink-0 me-2" viewBox="0 0 16 16" role="img"
aria-label="Success:">
<path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0zm-3.97-3.03a.75.75 0 0 0-1.08.022L7.477 9.417 5.384 7.323a.75.75 0 0 0-1.06 1.06L6.97 11.03a.75.75 0 0 0 1.079-.02l3.992-4.99a.75.75 0 0 0-.01-1.05z"/>
</svg>
已送出伺服器啟動訊號。
</div>
<?php else: ?>
<?php if (isset($status) && $status === 'exited'): ?>
<form method="POST">
<button class="btn btn-primary btn-block" name="start" type="submit">啟動伺服器</button>
</form>
<?php endif ?>
<?php endif ?>
</div>
</div>
<div class="card m-3 mx-auto w-fit-content">
<div class="card-body">
<p class="fs-5">【更新紀錄】</p>
<ul class="list-group">
<li class="list-group-item">
<p>首頁現在可以看到伺服器版本與線上人數等資訊了。</p>
<p>更新日期2022/07/05 02:02:02</p>
</li>
<li class="list-group-item">
<p>首頁現在可以看到伺服器狀態了,也可以從首頁直接啟動伺服器。</p>
<p>更新日期2022/07/04 16:52:30</p>
</li>
<li class="list-group-item">
<p>查看檔案功能已移至新的網址,請透過上方選單進入。</p>
<p>更多其他功能將陸續推出,敬請期待。</p>
<p>更新日期2022/07/04 13:44:30</p>
</li>
</ul>
</div>
</div>

8
templates/login.php Normal file
View file

@ -0,0 +1,8 @@
<?php $this->layout('template', ['title' => '登入系統', 'noStatus' => true]) ?>
<p class="lead">您尚未登入,請點選下方按鈕進行登入。</p>
<?php if (empty($authUrl)): ?>
<a href="/login.php" class="btn btn-lg btn-warning btn-block">登入連結載入失敗,按此重新載入</a>
<?php else: ?>
<a href="<?= $this->e($authUrl) ?>" class="btn btn-lg btn-primary btn-block">透過 Discord 帳號登入</a>
<?php endif ?>

View file

@ -0,0 +1,6 @@
<?php $this->layout('template', ['title' => '登出確認', 'noStatus' => true]) ?>
<p class="lead">您確定要登出嗎?</p>
<form action="/logout.php" method="POST">
<button name="logout" type="submit" class="btn btn-lg btn-danger btn-block">確定登出</button>
</form>

View file

@ -0,0 +1,3 @@
<?php $this->layout('template', ['title' => '登出成功', 'noStatus' => true]) ?>
您已成功登出,<a href="/">按此重新登入</a>

6
templates/logs/list.php Normal file
View file

@ -0,0 +1,6 @@
<?php
$this->layout('partials/file/list', [
'title' => '選擇日誌檔案',
'nav' => 'logs',
'list' => $list ?? [],
]);

View file

@ -0,0 +1,6 @@
<?php
$this->layout('partials/file/not-found', [
'title' => '檢視日誌',
'nav' => 'logs',
'list' => $list ?? [],
]);

9
templates/logs/view.php Normal file
View file

@ -0,0 +1,9 @@
<?php
$this->layout('partials/file/view', [
'title' => '檢視日誌',
'nav' => 'logs',
'list' => $list ?? [],
'file' => $file ?? '',
'type' => $type ?? 'plaintext',
'content' => $content ?? '',
]);

View file

@ -0,0 +1,12 @@
<?php $this->layout('template', ['title' => $title ?? null, 'nav' => $nav ?? null]) ?>
<div class="list-group text-start mt-5 mx-auto w-fit-content">
<div class="list-group-item">請選擇檔案:</div>
<?php if (isset($list) && is_array($list)): ?>
<?php foreach ($list as $file): ?>
<a class="list-group-item list-group-item-action" href="view.php?file=<?= $this->e($file) ?>">
<?= $this->e($file) ?>
</a>
<?php endforeach ?>
<?php endif ?>
</div>

View file

@ -0,0 +1,35 @@
<?php $this->layout('template', ['title' => $title ?? null, 'nav' => $nav ?? null]) ?>
<div class="m-5">
<div class="dropdown">
<button class="btn btn-outline-secondary dropdown-toggle" type="button" id="file-selector"
data-bs-toggle="dropdown" aria-expanded="false">
請選擇檔案
</button>
<ul class="dropdown-menu" aria-labelledby="file-selector">
<?php if (isset($list) && is_array($list)): ?>
<?php foreach ($list as $filename): ?>
<li>
<?php if (isset($file) && $file === $filename): ?>
<a class="dropdown-item active" href="view.php?file=<?= $this->e($filename) ?>">
<?= $this->e($filename) ?>*
</a>
<?php else: ?>
<a class="dropdown-item" href="view.php?file=<?= $this->e($filename) ?>">
<?= $this->e($filename) ?>
</a>
<?php endif ?>
</li>
<?php endforeach ?>
<?php endif ?>
</ul>
</div>
<div class="alert alert-warning mt-5 m-auto d-table" role="alert">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor"
class="bi bi-exclamation-triangle-fill flex-shrink-0 me-2" viewBox="0 0 16 16" role="img"
aria-label="Warning:">
<path d="M8.982 1.566a1.13 1.13 0 0 0-1.96 0L.165 13.233c-.457.778.091 1.767.98 1.767h13.713c.889 0 1.438-.99.98-1.767L8.982 1.566zM8 5c.535 0 .954.462.9.995l-.35 3.507a.552.552 0 0 1-1.1 0L7.1 5.995A.905.905 0 0 1 8 5zm.002 6a1 1 0 1 1 0 2 1 1 0 0 1 0-2z"/>
</svg>
檔案不存在,請從上方選單選取欲查看之檔案。
</div>
</div>

View file

@ -0,0 +1,39 @@
<?php $this->layout('template', ['title' => $title ?? null, 'nav' => $nav ?? null]) ?>
<?php $this->push('styles') ?>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.5.1/styles/default.min.css"/>
<?php $this->end() ?>
<?php $this->push('scripts') ?>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.5.1/highlight.min.js"></script>
<!--suppress JSUnresolvedVariable -->
<script>hljs.highlightAll()</script>
<?php $this->end() ?>
<div class="dropdown my-2">
<button class="btn btn-outline-secondary dropdown-toggle"
type="button"
id="file-selector"
data-bs-toggle="dropdown"
aria-expanded="false">
<?= $this->e($file ?? '') ?>
</button>
<ul class="dropdown-menu" aria-labelledby="file-selector">
<?php if (isset($list) && is_array($list)): ?>
<?php foreach ($list as $filename): ?>
<li>
<?php if (isset($file) && $file === $filename): ?>
<a class="dropdown-item active" href="view.php?file=<?= $this->e($filename) ?>">
<?= $this->e($filename) ?>
</a>
<?php else: ?>
<a class="dropdown-item" href="view.php?file=<?= $this->e($filename) ?>">
<?= $this->e($filename) ?>
</a>
<?php endif ?>
</li>
<?php endforeach ?>
<?php endif ?>
</ul>
</div>
<pre class="file-content"><code class="language-<?= $this->e($type ?? 'plaintext') ?>"><?= $this->e($content ?? '') ?></code></pre>

View file

@ -0,0 +1,56 @@
<nav class="navbar navbar-expand-lg bg-light">
<div class="container-fluid">
<div class="navbar-brand">功能選單</div>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#nav"
aria-controls="nav" aria-expanded="false" aria-label="顯示/隱藏選單">
<i class="fa-solid fa-bars"></i>
</button>
<div class="collapse navbar-collapse" id="nav">
<ul class="navbar-nav me-auto">
<li class="nav-item">
<a
<?php if (isset($nav) && $nav === 'index'): ?>
class="nav-link active"
aria-current="page"
<?php else: ?>
class="nav-link"
<?php endif ?>
href="/index.php"
>
<i class="fa-solid fa-house"></i>
首頁
</a>
</li>
<li class="nav-item">
<a
<?php if (isset($nav) && $nav === 'configurations'): ?>
class="nav-link active"
aria-current="page"
<?php else: ?>
class="nav-link"
<?php endif ?>
href="/configurations/list.php"
>
<i class="fa-solid fa-file-code"></i>
檢視設定檔
</a>
</li>
<li class="nav-item">
<a
<?php if (isset($nav) && $nav === 'logs'): ?>
class="nav-link active"
aria-current="page"
<?php else: ?>
class="nav-link"
<?php endif ?>
href="/logs/list.php"
>
<i class="fa-solid fa-file-lines"></i>
檢視日誌
</a>
</li>
</ul>
</div>
</div>
</nav>
<div>您已使用 <code><?= $this->e($whoami ?? '') ?></code> 登入,點選此處以<a href="/logout.php">登出系統</a>。</div>

23
templates/template.php Normal file
View file

@ -0,0 +1,23 @@
<!DOCTYPE html>
<html lang="zh-Hant-TW">
<head>
<meta charset="utf-8"/>
<meta http-equiv="X-UA-Compatible" content="IE=edge"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<title><?= $this->e($title ?? '無標題網頁') ?></title>
<link rel="stylesheet" href="/bootstrap/css/bootstrap.min.css"/>
<link rel="stylesheet" href="/font-awesome/css/all.min.css"/>
<link rel="stylesheet" href="/style.css"/>
<?= $this->section('styles') ?>
</head>
<body class="text-center">
<h1 class="h3 mb-3 font-weight-normal"><?= $this->e($title ?? '無標題網頁') ?></h1>
<?php if (isset($isLogin) && $isLogin && (!isset($noStatus) || !$noStatus)): ?>
<?php $this->insert('partials/menu', ['nav' => $nav ?? '']) ?>
<?php endif ?>
<main><?= $this->section('content') ?></main>
<footer class="my-2 text-muted">Copyright &copy; 2022 Ming Tsay. All rights reserved.</footer>
<script src="/bootstrap/js/bootstrap.bundle.min.js"></script>
<?= $this->section('scripts') ?>
</body>
</html>

15
webroot/auth.php Normal file
View file

@ -0,0 +1,15 @@
<?php
use MingTsay\Akanyan\App;
require_once __DIR__ . '/../vendor/autoload.php';
if (($_GET['source'] ?? '') !== 'discord' || empty($_GET['code'])) {
http_response_code(403);
exit;
}
App::auth($_GET['code']);
header('location: /');
http_response_code(302);

1
webroot/bootstrap Symbolic link
View file

@ -0,0 +1 @@
../vendor/twbs/bootstrap/dist/

View file

@ -0,0 +1,10 @@
<?php
use MingTsay\Akanyan\App;
use MingTsay\Akanyan\ConfigFilesViewer;
require_once __DIR__ . '/../../vendor/autoload.php';
App::requireAllowed();
App::render('configurations/list', ['list' => ConfigFilesViewer::list()]);

View file

@ -0,0 +1,27 @@
<?php
use MingTsay\Akanyan\App;
use MingTsay\Akanyan\ConfigFilesViewer;
require_once __DIR__ . '/../../vendor/autoload.php';
App::requireAllowed();
$file = $_GET['file'] ?? '';
$content = ConfigFilesViewer::read($file);
$list = ConfigFilesViewer::list();
if ($content === null)
App::render('configurations/not-found', [
'list' => $list,
]);
App::render('configurations/view', [
'list' => $list,
'file' => $file,
'content' => $content,
'type' => [
'json' => 'json',
'properties' => '.properties',
][pathinfo($file, PATHINFO_EXTENSION)],
]);

1
webroot/font-awesome Symbolic link
View file

@ -0,0 +1 @@
../vendor/components/font-awesome/

23
webroot/index.php Normal file
View file

@ -0,0 +1,23 @@
<?php
use MingTsay\Akanyan\App;
use MingTsay\Akanyan\Minecraft;
require_once __DIR__ . '/../vendor/autoload.php';
App::requireAllowed();
$status = exec('sudo -u mt docker inspect -f \'{{.State.Status}}\' akanyan-server-1');
// start the server
$isStarting = isset($_POST['start']);
if ($isStarting) exec('sudo -u mt docker start akanyan-server-1');
// get query
$query = $status === 'running' ? Minecraft::query() : null;
App::render('index', [
'status' => $status,
'isStarting' => $isStarting,
'query' => $query,
]);

8
webroot/login.php Normal file
View file

@ -0,0 +1,8 @@
<?php
use MingTsay\Akanyan\App;
require_once __DIR__ . '/../vendor/autoload.php';
App::requireNonAuth();
App::render('login', ['authUrl' => App::authUrl()]);

14
webroot/logout.php Normal file
View file

@ -0,0 +1,14 @@
<?php
use MingTsay\Akanyan\App;
require_once __DIR__ . '/../vendor/autoload.php';
App::requireAuth();
if (isset($_POST['logout'])) {
App::unsetU();
App::render('logout/success');
}
App::render('logout/form');

10
webroot/logs/list.php Normal file
View file

@ -0,0 +1,10 @@
<?php
use MingTsay\Akanyan\App;
use MingTsay\Akanyan\LogsViewer;
require_once __DIR__ . '/../../vendor/autoload.php';
App::requireAllowed();
App::render('logs/list', ['list' => LogsViewer::list()]);

23
webroot/logs/view.php Normal file
View file

@ -0,0 +1,23 @@
<?php
use MingTsay\Akanyan\App;
use MingTsay\Akanyan\LogsViewer;
require_once __DIR__ . '/../../vendor/autoload.php';
App::requireAllowed();
$file = $_GET['file'] ?? '';
$content = LogsViewer::read($file);
$list = LogsViewer::list();
if ($content === null)
App::render('configurations/not-found', [
'list' => $list,
]);
App::render('logs/view', [
'list' => $list,
'file' => $file,
'content' => $content,
]);

36
webroot/style.css Normal file
View file

@ -0,0 +1,36 @@
html,
body {
height: 100%;
width: 100%;
}
body {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding-top: 8px;
padding-bottom: 8px;
background-color: #f5f5f5;
}
main {
flex-grow: 1;
height: 0;
width: 100%;
overflow-y: scroll;
}
pre.file-content {
background-color: white;
text-align: left;
padding: 8px;
width: 100%;
overflow-x: scroll;
white-space: pre-wrap;
word-wrap: break-word;
}
.w-fit-content {
width: fit-content !important;
}