flush -- WIP admin panel
This commit is contained in:
60
admin/ajax/config.php
Normal file
60
admin/ajax/config.php
Normal 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
20
admin/ajax/statistics.php
Normal 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
114
admin/ajax/user.php
Normal 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
17
admin/component/config.js
Normal 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
17
admin/component/post.js
Normal 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);
|
||||
}
|
||||
}
|
||||
});
|
||||
})();
|
||||
70
admin/component/statistics.js
Normal file
70
admin/component/statistics.js
Normal 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
17
admin/component/user.js
Normal 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
114
admin/dashboard.js
Normal 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
28
admin/index.php
Normal 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
38
admin/theme/admin.css
Normal 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
12
admin/theme/avatar.php
Normal 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>
|
||||
69
admin/theme/dashboard.html
Normal file
69
admin/theme/dashboard.html
Normal 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 UI:CSS 與元件 -->
|
||||
<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>
|
||||
Reference in New Issue
Block a user