add basic admin panel

This commit is contained in:
Mikhail
2024-07-14 15:24:37 -04:00
parent 01f86a7d24
commit 367cd64b8e
11 changed files with 485 additions and 20 deletions

View File

@@ -0,0 +1,90 @@
using EmbedIO;
using EmbedIO.Routing;
using EmbedIO.WebApi;
using nksrv.Utils;
using System;
using System.Security.Cryptography;
using System.Text;
namespace nksrv
{
public class AdminApiController : WebApiController
{
public static Dictionary<string, User> AdminAuthTokens = new();
private static MD5 md5 = MD5.Create();
[Route(HttpVerbs.Any, "/login")]
public async Task Login()
{
var c = await HttpContext.GetRequestFormDataAsync();
var username = c["username"];
var password = c["password"];
if (HttpContext.Request.HttpMethod != "POST")
{
await HttpContext.SendStringAsync(File.ReadAllText(AppDomain.CurrentDomain.BaseDirectory + "/www/admin/index.html").Replace("<errormsg/>", ""), "text/html", Encoding.Unicode);
return;
}
User? user = null;
bool nullusernames = false;
if (username != null && password != null)
{
var passwordHash = Convert.ToHexString(md5.ComputeHash(Encoding.ASCII.GetBytes(password))).ToLower();
foreach (var item in JsonDb.Instance.Users)
{
if (item.Username == username)
{
if (item.Password.ToLower() == passwordHash)
{
user = item;
}
}
}
}
else
{
nullusernames = true;
}
if (user == null)
{
if (nullusernames == false)
{
await HttpContext.SendStringAsync((string)File.ReadAllText(AppDomain.CurrentDomain.BaseDirectory + "/www/admin/index.html").Replace("<errormsg/>", "Incorrect username or password"), "text/html", Encoding.Unicode);
return;
}
else
{
await HttpContext.SendStringAsync((string)File.ReadAllText(AppDomain.CurrentDomain.BaseDirectory + "/www/admin/index.html").Replace("<errormsg/>", "Please enter a username or password"), "text/html", Encoding.Unicode);
return;
}
}
else
{
if (user.IsAdmin)
{
Response.Headers.Add("Set-Cookie", "token=" + CreateAuthToken(user) + ";path=/");
HttpContext.Redirect("/admin/", 301);
}
else
{
await HttpContext.SendStringAsync((string)File.ReadAllText(AppDomain.CurrentDomain.BaseDirectory + "/www/admin/index.html").Replace("<errormsg/>", "User does not have admin panel access."), "text/html", Encoding.Unicode);
}
}
}
private static string CreateAuthToken(User user)
{
var tok = RandomString(128);
AdminAuthTokens.Add(tok, user);
return tok;
}
public static string RandomString(int length)
{
const string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
return new string(Enumerable.Repeat(chars, length)
.Select(s => s[new Random().Next(s.Length)]).ToArray());
}
}
}

View File

@@ -20,6 +20,7 @@ using Newtonsoft.Json.Linq;
using Swan; using Swan;
using Google.Api; using Google.Api;
using nksrv.StaticInfo; using nksrv.StaticInfo;
using EmbedIO.WebApi;
namespace nksrv namespace nksrv
{ {
@@ -60,7 +61,10 @@ namespace nksrv
.WithModule(new ActionModule("/media/", HttpVerbs.Any, HandleAsset)) .WithModule(new ActionModule("/media/", HttpVerbs.Any, HandleAsset))
.WithModule(new ActionModule("/PC/", HttpVerbs.Any, HandleAsset)) .WithModule(new ActionModule("/PC/", HttpVerbs.Any, HandleAsset))
.WithModule(new ActionModule("/$batch", HttpVerbs.Any, HandleBatchRequests)) .WithModule(new ActionModule("/$batch", HttpVerbs.Any, HandleBatchRequests))
.WithModule(new ActionModule("/nikke_launcher", HttpVerbs.Any, HandleLauncherUI)); .WithStaticFolder("/nikke_launcher", Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "www", "launcher"), true)
.WithStaticFolder("/admin/assets/", Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "www", "admin", "assets"), true)
.WithModule(new ActionModule("/admin", HttpVerbs.Any, HandleAdminRequest))
.WithWebApi("/adminapi", m => m.WithController(typeof(AdminApiController)));
// Listen for state changes. // Listen for state changes.
//server.StateChanged += (s, e) => $"WebServer New State - {e.NewState}".Info(); //server.StateChanged += (s, e) => $"WebServer New State - {e.NewState}".Info();
@@ -68,28 +72,59 @@ namespace nksrv
return server; return server;
} }
private static async Task HandleLauncherUI(IHttpContext ctx) private static async Task HandleAdminRequest(IHttpContext context)
{ {
await ctx.SendStringAsync(@"<!DOCTYPE html> //check if user is logged in
<html> if (context.Request.Cookies["token"] == null && context.Request.Url.PathAndQuery != "/api/login")
<head> {
<title>Private Nikke Server Launcher</title> context.Redirect("/adminapi/login");
<style> return;
* {
color:white;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
</style>
</head>
<body>
<h1>What's new in Nikke Private Server<h1>
<p>This is the inital release, only story mode works except that you can't collect items and a few other things don't work.</p>
<p>In order to level up characters, you manually have to edit db.json</p>
</body>
</html>
", "text/html", Encoding.UTF8);
} }
//Check if authenticated correctly
User? currentUser = null;
if (context.Request.Url.PathAndQuery != "/api/login")
{
//verify token
foreach (var item in AdminApiController.AdminAuthTokens)
{
if (item.Key == context.Request.Cookies["token"].Value)
{
currentUser = item.Value;
}
}
}
if (currentUser == null)
{
context.Redirect("/adminapi/login");
return;
}
if (context.Request.Url.PathAndQuery == "/admin/")
{
context.Redirect("/admin/dashboard");
}
else if (context.Request.Url.PathAndQuery == "/admin/dashboard")
{
await context.SendStringAsync(ProcessAdminPage("dashbrd.html", currentUser), "text/html", Encoding.Unicode);
}
else
{
context.Response.StatusCode = 404;
await context.SendStringAsync("404 not found", "text/html", Encoding.Unicode);
}
}
private static string ProcessAdminPage(string pg, User? currentUser)
{
var pgContent = File.ReadAllText(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "www", "admin", pg));
var nav = File.ReadAllText(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "www", "admin", "nav.html"));
//navbar
pgContent = pgContent.Replace("{{navbar}}", nav);
return pgContent;
}
private static async Task HandleBatchRequests(IHttpContext ctx) private static async Task HandleBatchRequests(IHttpContext ctx)
{ {
var theBytes = await PacketDecryption.DecryptOrReturnContentAsync(ctx); var theBytes = await PacketDecryption.DecryptOrReturnContentAsync(ctx);

View File

@@ -78,6 +78,7 @@ namespace nksrv.Utils
public string Nickname = "SomePlayer"; public string Nickname = "SomePlayer";
public int ProfileIconId = 39900; public int ProfileIconId = 39900;
public bool ProfileIconIsPrism = false; public bool ProfileIconIsPrism = false;
public bool IsAdmin = false;
// Game data // Game data
public List<string> CompletedScenarios = []; public List<string> CompletedScenarios = [];

View File

@@ -36,5 +36,8 @@
<None Update="site.pfx"> <None Update="site.pfx">
<CopyToOutputDirectory>Always</CopyToOutputDirectory> <CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None> </None>
<None Update="www\**\*">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@@ -0,0 +1,23 @@
@ -0,0 +1,23 @@ html, body {
margin: 0;
padding: 0;
height: 100%;
}
body {
margin: 0;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: url('/admin/assets/login.jpg') no-repeat center center fixed;
background-size: cover;
/* Center child horizontally*/
display: flex;
justify-content: center;
align-items: center;
}
.LoginBox {
background-color: white;
border-radius: 10px;
padding: 20px 20px 20px 20px;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

View File

@@ -0,0 +1,239 @@
body {
margin: 0;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
.bold {
font-weight: bold;
}
.input {
margin: 5px 0px 5px 0;
}
/* Tabs */
/* Style the tab */
.tab {
float: left;
border: 1px solid #ccc;
background-color: #f1f1f1;
width: 30%;
height: 1000px;
}
/* Style the buttons inside the tab */
.tab button {
display: block;
background-color: inherit;
color: black;
padding: 22px 16px;
width: 100%;
border: none;
outline: none;
text-align: left;
cursor: pointer;
transition: 0.3s;
font-size: 17px;
}
/* Change background color of buttons on hover */
.tab button:hover {
background-color: #ddd;
}
/* Create an active/current "tab button" class */
.tab button.active {
background-color: #ccc;
}
/* Style the tab content */
.tabcontent {
float: left;
padding: 5px 5px 5px 5px;
border: 1px solid #ccc;
width: 70%;
border-left: none;
height: 1000px;
}
.navbar2 {
margin: 0;
padding: 0;
overflow: hidden;
background-color: var(--main-nav-light-bg);
}
.navbar-item {
float: left;
display: block;
color: var(--main-nav-light-color);
text-align: center;
padding: 14px 16px;
text-decoration: none;
}
.navbar-item:hover {
background-color: #029761;
text-decoration: none;
color: var(--main-nav-light-color);
}
.navbar-item:not(.active):hover {
background-color: var(--main-nav-light-hover);
text-decoration: none;
color: var(--main-nav-light-color);
}
a:hover {
text-decoration: none;
}
.active {
background-color: #04AA6D;
}
:root {
--main-nav-dark-bg: #333;
--main-nav-dark-color: white;
--main-nav-dark-hover: #111;
--main-nav-light-bg: #f1f1f1;
--main-nav-light-color: black;
--main-nav-light-hover: #dddddd;
}
.form-control {
width: 200px;
}
zonegroup {
display: flex;
}
zonegroup p {
display: flex;
padding-right: 15px;
}
zonegroup input {
display: flex;
}
.right {
float: right;
}
.dropdown2 {
overflow: hidden;
}
.dropdown2 .dropbtn {
cursor: pointer;
font-size: 16px;
border: none;
outline: none;
color: black;
padding: 14px 16px;
background-color: inherit;
font-family: inherit;
margin: 0;
}
.navbar a:hover, .dropdown2:hover .dropbtn, .dropbtn:focus {
background-color: green;
color: white;
}
.dropdown-content {
display: none;
position: absolute;
background-color: #f9f9f9;
box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.2);
z-index: 1;
}
.dropdown-content a {
color: black;
padding: 12px 16px 12px 7px;
text-decoration: none;
display: block;
text-align: left;
}
.dropdown-content a:hover {
background-color: #ddd;
}
.show {
display: block;
}
/* Leave this at the end */
@media (prefers-color-scheme: dark) {
body {
background-color: rgb(25, 25, 25);
color: white;
}
.app-bar {
list-style-type: none;
margin: 0;
padding: 0;
overflow: hidden;
/* For browsers that do not support gradients */
background-color: #A3A6A9;
background-image: linear-gradient(to right, #A3A6A9, #666765);
}
.button {
background-color: darkgray;
color: white;
}
.tab {
background-color: rgb(36,36,36);
}
.tab button {
color: white;
}
.tab button:hover {
background-color: #666765;
}
.tab button.active {
background-color: darkgray;
}
.navbar2 {
background-color: var(--main-nav-dark-bg);
}
.navbar-item {
color: var(--main-nav-dark-color);
}
.navbar-item:hover {
background-color: #029761;
text-decoration: none;
color: var(--main-nav-dark-color);
}
.navbar-item:not(.active):hover {
background-color: var(--main-nav-dark-hover);
text-decoration: none;
color: var(--main-nav-dark-color);
}
.modal {
--bs-modal-bg: black;
}
.dropdown2 .dropbtn {
color: white;
}
}

View File

@@ -0,0 +1,21 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Security System Controller</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-Zenh87qX5JnK2Jl0vWa8Ck2rdkQ2Bzep5IDxbcnCeuOxjzrPF/et3URy9Bv1WTRi" crossorigin="anonymous">
<link rel="stylesheet" href="/admin/assets/style.css">
<link rel="icon" href="/favicon.ico" type="image/x-icon">
</head>
<body>
{{navbar}}
<div class="containter">
<h1>Welcome to Nikke Private Server Admin Panel</h1>
<p>There are no settings to display.</p>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-OERcA2EqjJCMA+/3y+gxIOqMEjwtxJY7qPCqsdltbNJuaOe923+mo//f6V8Qbsw3" crossorigin="anonymous"></script>
</body>
</html>

View File

@@ -0,0 +1,34 @@

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Security System Controller</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-Zenh87qX5JnK2Jl0vWa8Ck2rdkQ2Bzep5IDxbcnCeuOxjzrPF/et3URy9Bv1WTRi" crossorigin="anonymous">
<link rel="stylesheet" href="/admin/assets/login.css">
<link rel="icon" href="./favicon.ico" type="image/x-icon">
</head>
<body>
<div class="LoginBox">
<h1>Login</h1>
<form action="/adminapi/login" method="post">
<div class="mb-3">
<label for="UsernameBox" class="form-label">Username</label>
<input type="text" class="form-control" id="UsernameBox" name="username">
</div>
<div class="mb-3">
<label for="PasswordBox" class="form-label">Password</label>
<input type="password" class="form-control" id="PasswordBox" name="password">
</div>
<errormsg/>
<br />
<button type="submit" class="btn btn-primary">Submit</button>
</form>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-OERcA2EqjJCMA+/3y+gxIOqMEjwtxJY7qPCqsdltbNJuaOe923+mo//f6V8Qbsw3" crossorigin="anonymous"></script>
</body>
</html>

3
nksrv/www/admin/nav.html Normal file
View File

@@ -0,0 +1,3 @@
<div class="navbar2">
<a href="/admin/dashboard" class="navbar-item">Overview</a>
</div>

View File

@@ -0,0 +1,16 @@
<!DOCTYPE html>
<html>
<head>
<title>Private Nikke Server Launcher</title>
<style>
* {
color: white;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
</style>
</head>
<body>
<h1>What's new in Nikke Private Server - v0.1.1</h1>
<p>Bug fixes</p>
</body>
</html>