flush -- WIP admin panel

This commit is contained in:
Tony Yang
2019-05-30 14:18:07 +08:00
parent 75af7df7b4
commit 84fb4180c9
53 changed files with 9104 additions and 2 deletions

60
admin/ajax/config.php Normal file
View File

@@ -0,0 +1,60 @@
<?php
set_include_path('../../include/');
$includepath = TRUE;
require_once('../../config.php');
require_once('../../connection/SQL.php');
require_once('user.php');
require_once('security.php');
$user = validate_user();
if (!$user->valid) {
send_error(403, "novalid");
} else if (!($user->level >= 8)) {
send_error(403, "nopermission");
}
if ($_SERVER["REQUEST_METHOD"] == "POST") {
// modify blog settings
if (!validate_csrf()) {
send_error(403, "csrf");
}
$config_filename = "../../config.php";
$template_filename = "../../config.template";
if (!is_writable($config_filename)) {
send_error(500, "notwritable");
}
try {
$limit = abs(intval(@$_POST["limit"]));
$content = file_get_contents($template_filename);
$new_content = strtr($content, array(
"{blog_name}" => addslashes(@$_POST["name"]),
"{limit}" => ($limit != 0 ? $limit : 10),
"{register}" => (@$_POST["register"] === "true" ? "true" : "false")
));
file_put_contents($config_filename, $new_content);
$result = json_encode(array('status' => TRUE, "time" => round($_SERVER["REQUEST_TIME_FLOAT"] * 1000)));
} catch (Exception $e) {
http_response_code(500);
$result = json_encode(array('status' => $e->getMessage(), "time" => round($_SERVER["REQUEST_TIME_FLOAT"] * 1000)));
}
header('Content-Type: application/json');
echo $result;
exit;
} else if ($_SERVER["REQUEST_METHOD"] == "GET") {
// fetch settings
header('Content-Type: application/json');
echo json_encode(array_merge(array('status' => TRUE, "time" => round($_SERVER["REQUEST_TIME_FLOAT"] * 1000)), $blog));
exit;
}
function send_error($code, $message) {
http_response_code($code);
header('Content-Type: application/json');
echo json_encode(array('status' => $message));
exit;
}

20
admin/ajax/statistics.php Normal file
View File

@@ -0,0 +1,20 @@
<?php
set_include_path('../../include/');
$includepath = TRUE;
require_once('../../config.php');
require_once('../../connection/SQL.php');
require_once('user.php');
$user = validate_user();
if (!$user->valid) {
send_error(403, "novalid");
} else if (!($user->level >= 8)) {
send_error(403, "nopermission");
}
$post_count = intval(cavern_query_result("SELECT COUNT(*) AS `count` FROM `post`")['row']['count']);
$user_count = intval(cavern_query_result("SELECT COUNT(*) AS `count` FROM `user`")['row']['count']);
$comment_count = intval(cavern_query_result("SELECT COUNT(*) AS `count` FROM `comment`")['row']['count']);
header('Content-Type: application/json');
echo json_encode(array("fetch" => round($_SERVER["REQUEST_TIME_FLOAT"] * 1000), "name" => $blog['name'], "post" => $post_count, "user" => $user_count, "comment" => $comment_count));

114
admin/ajax/user.php Normal file
View File

@@ -0,0 +1,114 @@
<?php
set_include_path('../../include/');
$includepath = TRUE;
require_once('../../config.php');
require_once('../../connection/SQL.php');
require_once('user.php');
require_once('security.php');
$user = validate_user();
if (!$user->valid) {
send_error(403, "novalid");
} else if (!($user->level >= 8)) {
send_error(403, "nopermission");
}
if ($_SERVER["REQUEST_METHOD"] == "POST") {
// modify account data
if (!validate_csrf()) {
send_error(403, "csrf");
}
if (isset($_POST['username']) && (isset($_POST['name']) || isset($_POST['password']))) {
// modify account data
$username = trim($_POST['username']);
try {
$target_user = new User($username);
} catch (NoUserException $e) {
send_error(404, "nouser");
}
if (trim($_POST['password']) != '') {
$password = cavern_password_hash($_POST['password'], $username);
$SQL->query("UPDATE `user` SET `pwd`='%s' WHERE `username`='%s'", array($password, $username));
}
if (trim($_POST['name']) != '' && strlen($_POST['name']) <= 40) {
$SQL->query("UPDATE `user` SET `name`='%s' WHERE `username`='%s'", array(htmlspecialchars($_POST['name']), $username));
} else {
send_error(400, "noname");
}
if (trim($_POST['email']) != '' && filter_var($_POST['email'], FILTER_VALIDATE_EMAIL)) {
$emailExist = cavern_query_result("SELECT * FROM `user` WHERE NOT `username`='%s' AND `email`='%s'", array($username, $_POST["email"]));
if ($emailExist['num_rows'] == 0) {
$SQL->query("UPDATE `user` SET `email`='%s' WHERE `username`='%s'", array($_POST['email'], $username));
} else {
send_error(400, "emailused");
}
} else {
send_error(400, "noemail");
}
$SQL->query("UPDATE `user` SET `muted`='%d' WHERE `username`='%s'", array($_POST['muted'], $username));
header("Content-Type: application/json");
echo json_encode(array("status" => TRUE, "modified" => $username));
exit;
}
} else if ($_SERVER["REQUEST_METHOD"] == "GET") {
// fetch user list (we can fetch single user data from ajax)
$user_list = array();
$user_query = cavern_query_result("SELECT * FROM `user`", array());
if ($user_query['num_rows'] > 0) {
do {
$data = $user_query['row'];
$user_list[] = array(
"id" => intval($data['id']),
"username" => $data['username'],
"name" => $data['name'],
"email" => $data['email'],
"role" => cavern_level_to_role($data['level'])
);
} while ($user_query['row'] = $user_query['query']->fetch_assoc());
}
header('Content-Type: application/json');
echo json_encode(array('status' => TRUE, "time" => round($_SERVER["REQUEST_TIME_FLOAT"] * 1000), "list" => $user_list));
exit;
} else if ($_SERVER["REQUEST_METHOD"] == "DELETE") {
// delete user account
$username = trim($_GET['username']);
try {
$target_user = new User($username);
} catch (NoUserException $e) {
send_error(404, "nouser");
}
// you cannot delete site owner
if ($target_user->level === 9) {
send_error(403, "deleteowner");
}
/* cleanup user data */
// Although we set foreign key, in fact `ON CASCADE` cannot fire trigger
// like cleanup
$SQL->query("DELETE FROM `like` WHERE `username`='%s'", array($target_user->username));
// comment cleanup
$SQL->query("DELETE FROM `comment` WHERE `username`='%s'", array($target_user->username));
// now we can delete the user data
$SQL->query("DELETE FROM `user` WHERE `username`='%s'", array($target_user->username));
header('Content-Type: application/json');
echo json_encode(array('status' => TRUE, "time" => round($_SERVER["REQUEST_TIME_FLOAT"] * 1000), "deleted" => $username));
exit;
}
function send_error($code, $message) {
http_response_code($code);
header('Content-Type: application/json');
echo json_encode(array('status' => $message));
exit;
}

17
admin/component/config.js Normal file
View File

@@ -0,0 +1,17 @@
(() => {
function create(tag) {
return document.createElement(tag);
}
pageManager.register("config", function () {
return {
render: function (...args) {
pageManager.setHeader("設定");
setTimeout(() => {
pageManager.document.innerHTML = "config";
pageManager.setLoaderState(false)
}, 500);
}
}
});
})();

17
admin/component/post.js Normal file
View File

@@ -0,0 +1,17 @@
(() => {
function create(tag) {
return document.createElement(tag);
}
pageManager.register("post", function () {
return {
render: function (...args) {
pageManager.setHeader("文章");
setTimeout(() => {
pageManager.document.innerHTML = args[0] + args[1];
pageManager.setLoaderState(false)
}, 500);
}
}
});
})();

View File

@@ -0,0 +1,70 @@
(() => {
function fetchStatistics() {
axios.request({
method: "GET",
url: "ajax/statistics.php"
}).then(function (res) {
renderPage(res.data);
}).catch(function (err) {
if (err.response) {
console.error(err.response.status);
}
});
}
function renderPage(data) {
let iconName = {
"post": "file text",
"user": "users",
"comment": "comments"
};
let statLabel = {
"post": "文章總數",
"user": "使用者總數",
"comment": "留言總數"
};
let statTemplate = `<div class="ts left aligned statistic"><div class="value">{{ value }}</div><div class="label">{{ label }}</div></div>`
let cardContainer = create('div'); cardContainer.className = "ts stackable three cards";
for (let key in data) {
if (Object.keys(statLabel).indexOf(key) != -1) {
let card = create('div'); card.className = "ts card";
let content = create('div'); content.className = "content";
let symbol = create('div'); symbol.className = "symbol";
let icon = create('i'); icon.className = `${iconName[key]} icon`;
content.innerHTML = statTemplate.replace("{{ value }}", data[key]).replace("{{ label }}", statLabel[key]);
symbol.appendChild(icon);
card.appendChild(content);
card.appendChild(symbol);
cardContainer.appendChild(card);
} else if (key === "name") {
// blog name
}
}
// finish up
setTimeout(() => {
pageManager.document.innerHTML = "";
pageManager.document.appendChild(cardContainer);
pageManager.setLoaderState(false)
}, 500);
}
function create(tag) {
return document.createElement(tag);
}
pageManager.register("statistics", function () {
return {
render: function (...args) {
pageManager.setHeader("總覽");
fetchStatistics();
}
}
});
})();

17
admin/component/user.js Normal file
View File

@@ -0,0 +1,17 @@
(() => {
function create(tag) {
return document.createElement(tag);
}
pageManager.register("user", function () {
return {
render: function (...args) {
pageManager.setHeader("使用者");
setTimeout(() => {
pageManager.document.innerHTML = args[0];
pageManager.setLoaderState(false)
}, 500);
}
}
});
})();

114
admin/dashboard.js Normal file
View File

@@ -0,0 +1,114 @@
$(document).ready(function () {
var $sidebar = $('.ts.sidebar');
let mq = window.matchMedia("(max-width: 768px)");
mq.addListener(sidebarOnMobile);
sidebarOnMobile(mq); // run first
function sidebarOnMobile(q) {
if (q.matches) {
/* mobile -> hide sidebar */
$sidebar.toggleClass("animating", true);
$sidebar.toggleClass("visible", false);
} else {
/* non-mobile -> show sidebar */
$sidebar.toggleClass("animating", false);
$sidebar.toggleClass("visible", true);
}
}
$('button#toggleSidebar').on('click', function (e) {
$sidebar.toggleClass('visible');
});
});
function Manager(element) {
this.components = {};
this.cache = {}; // args cache for components just be on load
this.document = element;
}
Manager.prototype = {
load: function(name, ...args) {
this.setLoaderState(true);
if (Object.keys(this.components).indexOf(name) == -1) {
// cache args
this.cache[name] = args;
script = document.createElement("script");
script.src = `component/${name}.js`;
document.body.appendChild(script);
} else {
this.components[name].render(...args);
}
},
register: function(name, init) {
this.components[name] = init();
let args = this.cache[name];
this.components[name].render(...args);
delete this.cache[name];
},
setHeader: function (title) {
$('#header').text(title);
},
setLoaderState: function (state) {
$('.pusher .dimmer').toggleClass('active', state);
}
}
let pageManager = new Manager(document.querySelector('#content'));
var root = "./", useHash = true, hash = "#";
let router = new Navigo(root, useHash, hash);
if (location.href.slice(-1) === "/") {
// catch to navigo
router.navigate("/");
}
router.on({
"/": function () {
// system overview
render("statistics");
},
"/post": function () {
render("post", "page", 1);
},
"/post/:pid": function (params) {
render("post", "pid", params.pid);
},
"/post/page/:page": function (params) {
render("post", "page", params.page);
},
"/user": function () {
render("user", "list");
},
"/user/add": function () {
render("user", "add");
},
"/user/:username": function (params) {
render("user", "username", params.username);
},
"/config": function () {
render("config");
}
}).resolve();
router.updatePageLinks();
function render(page, ...args) {
switch (page) {
case "user":
pageManager.load("user", ...args);
break;
case "post":
pageManager.load("post", ...args);
break;
case "config":
pageManager.load("config", ...args);
break;
case "statistics":
default:
pageManager.load("statistics", ...args);
break;
}
}

28
admin/index.php Normal file
View File

@@ -0,0 +1,28 @@
<?php
set_include_path('../include/');
$includepath = TRUE;
require_once('../connection/SQL.php');
require_once('../config.php');
require_once('view.php');
require_once('security.php');
require_once('user.php');
$user = validate_user();
if (!$user->valid) {
http_response_code(403);
header("Location: ../index.php?err=account");
exit;
}
if (!$user->islogin) {
http_response_code(401);
header('Location: ../login.php?next=admin');
exit;
} else if ($user->level < 8) {
http_response_code(403);
header('Location: ../index.php?err=permission');
exit;
}
$view = new View('./theme/dashboard.html', 'theme/avatar.php', '', $blog['name'], "管理介面");
$view->render();

38
admin/theme/admin.css Normal file
View File

@@ -0,0 +1,38 @@
/* nav bar */
button#toggleSidebar {
border-radius: 0;
border-bottom-right-radius: .28571rem;
visibility: hidden;
}
.pusher > nav {
z-index: 14;
position: sticky;
position: -webkit-sticky; /* safari */
left: 0;
top: 0;
}
/* content */
#header {
margin-top: 5px;
}
@media (max-width: 768px) {
.ts.visible.sidebar:not(.overlapped) ~ .pusher.squeezable {
/* RWD fix */
width: 100vw;
left: 0;
}
button#toggleSidebar {
visibility: visible;
}
.visible.sidebar ~ .pusher > nav {
width: calc(100vw - 230px);
transform: translate3d(230px, 0, 0);
transition: transform .45s cubic-bezier(0.23, 1, 0.32, 1);
will-change: transform;
}
}

12
admin/theme/avatar.php Normal file
View File

@@ -0,0 +1,12 @@
<?php
require_once('../config.php');
require_once('../connection/SQL.php');
require_once('../include/user.php');
$user = validate_user();
?>
<div class="center aligned item">
<img class="ts tiny circular image" src="https://www.gravatar.com/avatar/<?= md5($user->email) ?>?d=https%3A%2F%2Ftocas-ui.com%2Fassets%2Fimg%2F5e5e3a6.png&s=150">
<br><br>
<div><?= $user->name ?></div>
</div>

View File

@@ -0,0 +1,69 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!-- Tocas UICSS 與元件 -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/tocas-ui/2.3.3/tocas.css">
<!-- Tocas JS模塊與 JavaScript 函式 -->
<script src="../include/js/lib/tocas.js"></script>
<!-- jQuery -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@7.26.11/dist/sweetalert2.all.min.js"></script>
<link rel="stylesheet" href="theme/admin.css">
<title>{part} | {title}</title>
</head>
<body>
<div class="ts left vertical fluid inverted visible menu sidebar">
{nav}
<div class="item">
<i class="users icon"></i> 使用者
<div class="menu">
<a class="item" href="/user" data-navigo>列表</a>
<a class="item" href="/user/add" data-navigo>新增</a>
</div>
</div>
<div class="item">
<i class="file text icon"></i> 文章
<div class="menu">
<a href="/post" class="item" data-navigo>列表</a>
</div>
</div>
<div class="item">
<i class="setting icon"></i> 系統
<div class="menu">
<a class="item" href="/" data-navigo>總覽</a>
<a class="item" href="/config" data-navigo>設定</a>
</div>
</div>
<div class="bottom menu">
<a href="../" class="item"><i class="arrow left icon"></i>返回部落格</a>
</div>
</div>
<div class="comupter squeezable pusher" id="pusher">
<div class="ts active inverted dimmer">
<div class="ts loader"></div>
</div>
<nav class="sidebar menu">
<button class="ts inverted icon button" id="toggleSidebar">
<i class="sidebar icon"></i>
</button>
</nav>
<div class="ts narrow container">
<div class="ts big dividing header" id="header"></div>
<div class="ts fluid container" id="content"></div>
</div>
</div>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<script src="https://unpkg.com/navigo@6"></script>
<script src="../include/js/security.js"></script>
<script src="dashboard.js"></script>
{script}
</body>
</html>