diff --git a/nksrv/AdminApiController.cs b/nksrv/AdminApiController.cs new file mode 100644 index 0000000..3268724 --- /dev/null +++ b/nksrv/AdminApiController.cs @@ -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 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("", ""), "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("", "Incorrect username or password"), "text/html", Encoding.Unicode); + return; + } + else + { + await HttpContext.SendStringAsync((string)File.ReadAllText(AppDomain.CurrentDomain.BaseDirectory + "/www/admin/index.html").Replace("", "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("", "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()); + } + } +} diff --git a/nksrv/Program.cs b/nksrv/Program.cs index 9a7abe4..00eb350 100644 --- a/nksrv/Program.cs +++ b/nksrv/Program.cs @@ -20,6 +20,7 @@ using Newtonsoft.Json.Linq; using Swan; using Google.Api; using nksrv.StaticInfo; +using EmbedIO.WebApi; namespace nksrv { @@ -60,7 +61,10 @@ namespace nksrv .WithModule(new ActionModule("/media/", HttpVerbs.Any, HandleAsset)) .WithModule(new ActionModule("/PC/", HttpVerbs.Any, HandleAsset)) .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. //server.StateChanged += (s, e) => $"WebServer New State - {e.NewState}".Info(); @@ -68,28 +72,59 @@ namespace nksrv return server; } - private static async Task HandleLauncherUI(IHttpContext ctx) + private static async Task HandleAdminRequest(IHttpContext context) { - await ctx.SendStringAsync(@" - - -Private Nikke Server Launcher - - - -

What's new in Nikke Private Server

-

This is the inital release, only story mode works except that you can't collect items and a few other things don't work.

-

In order to level up characters, you manually have to edit db.json

- - -", "text/html", Encoding.UTF8); + //check if user is logged in + if (context.Request.Cookies["token"] == null && context.Request.Url.PathAndQuery != "/api/login") + { + context.Redirect("/adminapi/login"); + return; + } + + //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) { var theBytes = await PacketDecryption.DecryptOrReturnContentAsync(ctx); diff --git a/nksrv/Utils/JsonDb.cs b/nksrv/Utils/JsonDb.cs index 3e22203..7997a4c 100644 --- a/nksrv/Utils/JsonDb.cs +++ b/nksrv/Utils/JsonDb.cs @@ -78,6 +78,7 @@ namespace nksrv.Utils public string Nickname = "SomePlayer"; public int ProfileIconId = 39900; public bool ProfileIconIsPrism = false; + public bool IsAdmin = false; // Game data public List CompletedScenarios = []; diff --git a/nksrv/nksrv.csproj b/nksrv/nksrv.csproj index adc67a5..2d6a807 100644 --- a/nksrv/nksrv.csproj +++ b/nksrv/nksrv.csproj @@ -36,5 +36,8 @@ Always + + Always + diff --git a/nksrv/www/admin/assets/login.css b/nksrv/www/admin/assets/login.css new file mode 100644 index 0000000..5e3707f --- /dev/null +++ b/nksrv/www/admin/assets/login.css @@ -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; +} diff --git a/nksrv/www/admin/assets/login.jpg b/nksrv/www/admin/assets/login.jpg new file mode 100644 index 0000000..140cfc0 Binary files /dev/null and b/nksrv/www/admin/assets/login.jpg differ diff --git a/nksrv/www/admin/assets/style.css b/nksrv/www/admin/assets/style.css new file mode 100644 index 0000000..f0fe44d --- /dev/null +++ b/nksrv/www/admin/assets/style.css @@ -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; + } +} diff --git a/nksrv/www/admin/dashbrd.html b/nksrv/www/admin/dashbrd.html new file mode 100644 index 0000000..f741e59 --- /dev/null +++ b/nksrv/www/admin/dashbrd.html @@ -0,0 +1,21 @@ + + + + + + + Security System Controller + + + + + + + {{navbar}} +
+

Welcome to Nikke Private Server Admin Panel

+

There are no settings to display.

+
+ + + \ No newline at end of file diff --git a/nksrv/www/admin/index.html b/nksrv/www/admin/index.html new file mode 100644 index 0000000..fec15ea --- /dev/null +++ b/nksrv/www/admin/index.html @@ -0,0 +1,34 @@ + + + + + + + + Security System Controller + + + + + + + +
+

Login

+
+
+ + +
+
+ + +
+ +
+ + +
+ + + \ No newline at end of file diff --git a/nksrv/www/admin/nav.html b/nksrv/www/admin/nav.html new file mode 100644 index 0000000..6203726 --- /dev/null +++ b/nksrv/www/admin/nav.html @@ -0,0 +1,3 @@ + \ No newline at end of file diff --git a/nksrv/www/launcher/index.html b/nksrv/www/launcher/index.html new file mode 100644 index 0000000..4fb2cf4 --- /dev/null +++ b/nksrv/www/launcher/index.html @@ -0,0 +1,16 @@ + + + + Private Nikke Server Launcher + + + +

What's new in Nikke Private Server - v0.1.1

+

Bug fixes

+ +