Add project files.

This commit is contained in:
Tech Garage 2023-03-03 23:24:18 +03:30
commit b2325a46ef
1351 changed files with 48136 additions and 0 deletions

27
.dockerignore Normal file
View file

@ -0,0 +1,27 @@
**/.classpath
**/.dockerignore
**/.env
**/.git
**/.gitignore
**/.project
**/.settings
**/.toolstarget
**/.vs
**/.vscode
**/.editorconfig
**/*.*proj.user
**/*.dbmdl
**/*.jfm
**/azds.yaml
**/bin
**/charts
**/docker-compose*
**/Dockerfile*
**/node_modules
**/npm-debug.log
**/obj
**/secrets.dev.yaml
**/values.dev.yaml
**/*.pdb
LICENSE
README.md

63
.gitattributes vendored Normal file
View file

@ -0,0 +1,63 @@
###############################################################################
# Set default behavior to automatically normalize line endings.
###############################################################################
* text=auto
###############################################################################
# Set default behavior for command prompt diff.
#
# This is need for earlier builds of msysgit that does not have it on by
# default for csharp files.
# Note: This is only used by command line
###############################################################################
#*.cs diff=csharp
###############################################################################
# Set the merge driver for project and solution files
#
# Merging from the command prompt will add diff markers to the files if there
# are conflicts (Merging from VS is not affected by the settings below, in VS
# the diff markers are never inserted). Diff markers may cause the following
# file extensions to fail to load in VS. An alternative would be to treat
# these files as binary and thus will always conflict and require user
# intervention with every merge. To do so, just uncomment the entries below
###############################################################################
#*.sln merge=binary
#*.csproj merge=binary
#*.vbproj merge=binary
#*.vcxproj merge=binary
#*.vcproj merge=binary
#*.dbproj merge=binary
#*.fsproj merge=binary
#*.lsproj merge=binary
#*.wixproj merge=binary
#*.modelproj merge=binary
#*.sqlproj merge=binary
#*.wwaproj merge=binary
###############################################################################
# behavior for image files
#
# image files are treated as binary by default.
###############################################################################
#*.jpg binary
#*.png binary
#*.gif binary
###############################################################################
# diff behavior for common document formats
#
# Convert binary document formats to text before diffing them. This feature
# is only available from the command line. Turn it on by uncommenting the
# entries below.
###############################################################################
#*.doc diff=astextplain
#*.DOC diff=astextplain
#*.docx diff=astextplain
#*.DOCX diff=astextplain
#*.dot diff=astextplain
#*.DOT diff=astextplain
#*.pdf diff=astextplain
#*.PDF diff=astextplain
#*.rtf diff=astextplain
#*.RTF diff=astextplain

368
.gitignore vendored Normal file
View file

@ -0,0 +1,368 @@
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
##
## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
# User-specific files
*.rsuser
*.suo
*.user
*.userosscache
*.sln.docstates
# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs
# Mono auto generated files
mono_crash.*
# Build results
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
x64/
x86/
[Ww][Ii][Nn]32/
[Aa][Rr][Mm]/
[Aa][Rr][Mm]64/
bld/
[Bb]in/
[Oo]bj/
[Oo]ut/
[Ll]og/
[Ll]ogs/
# Visual Studio 2015/2017 cache/options directory
.vs/
# Uncomment if you have tasks that create the project's static files in wwwroot
#wwwroot/
# Visual Studio 2017 auto generated files
Generated\ Files/
# MSTest test Results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*
# NUnit
*.VisualState.xml
TestResult.xml
nunit-*.xml
# Build Results of an ATL Project
[Dd]ebugPS/
[Rr]eleasePS/
dlldata.c
# Benchmark Results
BenchmarkDotNet.Artifacts/
# .NET Core
project.lock.json
project.fragment.lock.json
artifacts/
# ASP.NET Scaffolding
ScaffoldingReadMe.txt
# StyleCop
StyleCopReport.xml
# Files built by Visual Studio
*_i.c
*_p.c
*_h.h
*.ilk
*.meta
*.obj
*.iobj
*.pch
*.pdb
*.ipdb
*.pgc
*.pgd
*.rsp
*.sbr
*.tlb
*.tli
*.tlh
*.tmp
*.tmp_proj
*_wpftmp.csproj
*.log
*.vspscc
*.vssscc
.builds
*.pidb
*.svclog
*.scc
# Chutzpah Test files
_Chutzpah*
# Visual C++ cache files
ipch/
*.aps
*.ncb
*.opendb
*.opensdf
*.sdf
*.cachefile
*.VC.db
*.VC.VC.opendb
# Visual Studio profiler
*.psess
*.vsp
*.vspx
*.sap
# Visual Studio Trace Files
*.e2e
# TFS 2012 Local Workspace
$tf/
# Guidance Automation Toolkit
*.gpState
# ReSharper is a .NET coding add-in
_ReSharper*/
*.[Rr]e[Ss]harper
*.DotSettings.user
# TeamCity is a build add-in
_TeamCity*
# DotCover is a Code Coverage Tool
*.dotCover
# AxoCover is a Code Coverage Tool
.axoCover/*
!.axoCover/settings.json
# Coverlet is a free, cross platform Code Coverage Tool
coverage*.json
coverage*.xml
coverage*.info
# Visual Studio code coverage results
*.coverage
*.coveragexml
# NCrunch
_NCrunch_*
.*crunch*.local.xml
nCrunchTemp_*
# MightyMoose
*.mm.*
AutoTest.Net/
# Web workbench (sass)
.sass-cache/
# Installshield output folder
[Ee]xpress/
# DocProject is a documentation generator add-in
DocProject/buildhelp/
DocProject/Help/*.HxT
DocProject/Help/*.HxC
DocProject/Help/*.hhc
DocProject/Help/*.hhk
DocProject/Help/*.hhp
DocProject/Help/Html2
DocProject/Help/html
# Click-Once directory
publish/
# Publish Web Output
*.[Pp]ublish.xml
*.azurePubxml
# Note: Comment the next line if you want to checkin your web deploy settings,
# but database connection strings (with potential passwords) will be unencrypted
*.pubxml
*.publishproj
# Microsoft Azure Web App publish settings. Comment the next line if you want to
# checkin your Azure Web App publish settings, but sensitive information contained
# in these scripts will be unencrypted
PublishScripts/
# NuGet Packages
*.nupkg
# NuGet Symbol Packages
*.snupkg
# The packages folder can be ignored because of Package Restore
**/[Pp]ackages/*
# except build/, which is used as an MSBuild target.
!**/[Pp]ackages/build/
# Uncomment if necessary however generally it will be regenerated when needed
#!**/[Pp]ackages/repositories.config
# NuGet v3's project.json files produces more ignorable files
*.nuget.props
*.nuget.targets
# Microsoft Azure Build Output
csx/
*.build.csdef
# Microsoft Azure Emulator
ecf/
rcf/
# Windows Store app package directories and files
AppPackages/
BundleArtifacts/
Package.StoreAssociation.xml
_pkginfo.txt
*.appx
*.appxbundle
*.appxupload
# Visual Studio cache files
# files ending in .cache can be ignored
*.[Cc]ache
# but keep track of directories ending in .cache
!?*.[Cc]ache/
# Others
ClientBin/
~$*
*~
*.dbmdl
*.dbproj.schemaview
*.jfm
*.pfx
*.publishsettings
orleans.codegen.cs
# Including strong name files can present a security risk
# (https://github.com/github/gitignore/pull/2483#issue-259490424)
#*.snk
# Since there are multiple workflows, uncomment next line to ignore bower_components
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
#bower_components/
# RIA/Silverlight projects
Generated_Code/
# Backup & report files from converting an old project file
# to a newer Visual Studio version. Backup files are not needed,
# because we have git ;-)
_UpgradeReport_Files/
Backup*/
UpgradeLog*.XML
UpgradeLog*.htm
ServiceFabricBackup/
*.rptproj.bak
# SQL Server files
*.mdf
*.ldf
*.ndf
# Business Intelligence projects
*.rdl.data
*.bim.layout
*.bim_*.settings
*.rptproj.rsuser
*- [Bb]ackup.rdl
*- [Bb]ackup ([0-9]).rdl
*- [Bb]ackup ([0-9][0-9]).rdl
# Microsoft Fakes
FakesAssemblies/
# GhostDoc plugin setting file
*.GhostDoc.xml
# Node.js Tools for Visual Studio
.ntvs_analysis.dat
node_modules/
# Visual Studio 6 build log
*.plg
# Visual Studio 6 workspace options file
*.opt
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
*.vbw
# Visual Studio LightSwitch build output
**/*.HTMLClient/GeneratedArtifacts
**/*.DesktopClient/GeneratedArtifacts
**/*.DesktopClient/ModelManifest.xml
**/*.Server/GeneratedArtifacts
**/*.Server/ModelManifest.xml
_Pvt_Extensions
# Paket dependency manager
.paket/paket.exe
paket-files/
# FAKE - F# Make
.fake/
# CodeRush personal settings
.cr/personal
# Python Tools for Visual Studio (PTVS)
__pycache__/
*.pyc
# Cake - Uncomment if you are using it
# tools/**
# !tools/packages.config
# Tabs Studio
*.tss
# Telerik's JustMock configuration file
*.jmconfig
# BizTalk build output
*.btp.cs
*.btm.cs
*.odx.cs
*.xsd.cs
# OpenCover UI analysis results
OpenCover/
# Azure Stream Analytics local run output
ASALocalRun/
# MSBuild Binary and Structured Log
*.binlog
# NVidia Nsight GPU debugger configuration file
*.nvuser
# MFractors (Xamarin productivity tool) working folder
.mfractor/
# Local History for Visual Studio
.localhistory/
# BeatPulse healthcheck temp database
healthchecksdb
# Backup folder for Package Reference Convert tool in Visual Studio 2017
MigrationBackup/
# Ionide (cross platform F# VS Code tools) working folder
.ionide/
# Fody - auto-generated XML schema
FodyWeavers.xsd
# Docker
[Pp]ublished/
*.tar
.editorconfig

447
APIHandler.cs Normal file
View file

@ -0,0 +1,447 @@
using Microsoft.AspNetCore.Hosting.Server;
using MTWireGuard.Models;
using MTWireGuard.Models.Mikrotik;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System.Net.Http.Headers;
using System.Text;
namespace MTWireGuard
{
public static class APIHandler
{
private static readonly string MT_IP = Environment.GetEnvironmentVariable("MT_IP");
private static readonly string MT_USER = Environment.GetEnvironmentVariable("MT_USER");
private static readonly string MT_PASS = Environment.GetEnvironmentVariable("MT_PASS");
public static async Task<List<Log>> GetLogsAsync()
{
string json = await SendGetRequestAsync("log");
return JsonConvert.DeserializeObject<List<Log>>(json);
}
public static async Task<List<WGServer>> GetServersAsync()
{
string json = await SendGetRequestAsync("interface/wireguard");
return JsonConvert.DeserializeObject<List<WGServer>>(json);
}
public static async Task<WGServer> GetServer(string Name)
{
var servers = await GetServersAsync();
return servers.Find(s => s.Name == Name);
}
public static async Task<List<ServerTraffic>> GetServersTraffic()
{
var json = await SendPostRequestAsync("interface", "{\"stats\", {\".proplist\":\"name, type, rx-byte, tx-byte\"}}");
return JsonConvert.DeserializeObject<List<ServerTraffic>>(json);
}
public static async Task<List<WGPeer>> GetUsersAsync()
{
using var db = new DBContext();
string json = await SendGetRequestAsync("interface/wireguard/peers");
return JsonConvert.DeserializeObject<List<WGPeer>>(json);
}
public static async Task<WGPeer> GetUser(string id)
{
var users = await GetUsersAsync();
return users.Find(u => u.Id == id);
}
public static async Task<MTInfo> GetInfo()
{
var json = await SendGetRequestAsync("system/resource");
return JsonConvert.DeserializeObject<MTInfo>(json);
}
public static async Task<MTIdentity> GetName()
{
var json = await SendGetRequestAsync("system/identity");
return JsonConvert.DeserializeObject<MTIdentity>(json);
}
public static async Task<LoginStatus> TryConnectAsync()
{
var connection = await SendGetRequestAsync("", true);
return JsonConvert.DeserializeObject<LoginStatus>(connection);
}
public static async Task<List<ActiveUser>> GetActiveSessions()
{
var json = await SendGetRequestAsync("user/active?name=" + MT_USER);
return JsonConvert.DeserializeObject<List<ActiveUser>>(json);
}
public static async Task<List<Job>> GetJobs()
{
var json = await SendGetRequestAsync("system/script/job");
return JsonConvert.DeserializeObject<List<Job>>(json);
}
public static async Task<string> KillJob(string JobID)
{
return await SendDeleteRequestAsync("system/script/job/" + JobID);
}
public static async Task<CreationStatus> CreateServer(WGServerCreateModel server)
{
var json = await SendPutRequestAsync("interface/wireguard", server);
var obj = JObject.Parse(json);
bool success = false;
string code = string.Empty, message = string.Empty, detail = string.Empty;
if (obj.TryGetValue(".id", out var Id))
{
success = true;
}
else if (obj.TryGetValue("error", out var Error))
{
var error = JsonConvert.DeserializeObject<CreationStatus>(json);
success = false;
code = Error.Value<string>();
message = error.Message;
detail = error.Detail;
}
else
{
success = false;
message = "Failed";
detail = json;
};
return new()
{
Code = code,
Message = message,
Detail = detail,
Success = success
};
}
public static async Task<CreationStatus> CreateUser(WGPeerCreateModel user)
{
var jsonData = JObject.Parse(JsonConvert.SerializeObject(user, new JsonSerializerSettings
{
NullValueHandling = NullValueHandling.Ignore,
DefaultValueHandling = DefaultValueHandling.Ignore
}));
var json = await SendPutRequestAsync("interface/wireguard/peers", jsonData);
var obj = JObject.Parse(json);
bool success = false;
string code = string.Empty, message = string.Empty, detail = string.Empty;
WGPeer peer = null;
if (obj.TryGetValue(".id", out var Id))
{
success = true;
peer = JsonConvert.DeserializeObject<WGPeer>(json);
}
else if (obj.TryGetValue("error", out var Error))
{
var error = JsonConvert.DeserializeObject<CreationStatus>(json);
success = false;
code = Error.Value<string>();
message = error.Message;
detail = error.Detail;
}
else
{
success = false;
message = "Failed";
detail = json;
};
return new()
{
Code = code,
Message = message,
Detail = detail,
Success = success,
Item = peer ?? null
};
}
public static async Task<CreationStatus> UpdateServer(WGServerUpdateModel server)
{
var serverJson = JObject.Parse(JsonConvert.SerializeObject(server, new JsonSerializerSettings
{
NullValueHandling = NullValueHandling.Ignore,
DefaultValueHandling = DefaultValueHandling.Ignore
}));
var json = await SendPatchRequestAsync($"interface/wireguard/{server.Id}", serverJson);
var obj = JObject.Parse(json);
bool success = false;
string code = string.Empty, message = string.Empty, detail = string.Empty;
WGServer srv = null;
if (obj.TryGetValue(".id", out var Id))
{
success = true;
srv = JsonConvert.DeserializeObject<WGServer>(json);
}
else if (obj.TryGetValue("error", out var Error))
{
var error = JsonConvert.DeserializeObject<CreationStatus>(json);
success = false;
code = Error.Value<string>();
message = error.Message;
detail = error.Detail;
}
else
{
success = false;
message = "Failed";
detail = json;
};
return new()
{
Code = code,
Message = message,
Detail = detail,
Success = success,
Item = srv ?? null
};
}
public static async Task<CreationStatus> UpdateUser(WGPeerUpdateModel user)
{
var userJson = JObject.Parse(JsonConvert.SerializeObject(user, new JsonSerializerSettings
{
NullValueHandling = NullValueHandling.Ignore,
DefaultValueHandling = DefaultValueHandling.Ignore
}));
var json = await SendPatchRequestAsync($"interface/wireguard/peers/{user.Id}", userJson);
var obj = JObject.Parse(json);
bool success = false;
string code = string.Empty, message = string.Empty, detail = string.Empty;
WGPeer peer = null;
if (obj.TryGetValue(".id", out var Id))
{
success = true;
peer = JsonConvert.DeserializeObject<WGPeer>(json);
}
else if (obj.TryGetValue("error", out var Error))
{
var error = JsonConvert.DeserializeObject<CreationStatus>(json);
success = false;
code = Error.Value<string>();
message = error.Message;
detail = error.Detail;
}
else
{
success = false;
message = "Failed";
detail = json;
};
return new()
{
Code = code,
Message = message,
Detail = detail,
Success = success,
Item = peer ?? null
};
}
public static async Task<CreationStatus> SetServerEnabled(WGEnability enability)
{
var json = await SendPatchRequestAsync($"interface/wireguard/{enability.ID}", new { disabled = enability.Disabled });
var obj = JObject.Parse(json);
bool success = false;
string code = string.Empty, message = string.Empty, detail = string.Empty;
WGPeer peer = null;
if (obj.TryGetValue(".id", out var Id))
{
success = true;
peer = JsonConvert.DeserializeObject<WGPeer>(json);
}
else if (obj.TryGetValue("error", out var Error))
{
var error = JsonConvert.DeserializeObject<CreationStatus>(json);
success = false;
code = Error.Value<string>();
message = error.Message;
detail = error.Detail;
}
else
{
success = false;
message = "Failed";
detail = json;
};
return new()
{
Code = code,
Message = message,
Detail = detail,
Success = success,
Item = peer ?? null
};
}
public static async Task<CreationStatus> SetUserEnabled(WGEnability enability)
{
var json = await SendPatchRequestAsync($"interface/wireguard/peers/{enability.ID}", new { disabled = enability.Disabled });
var obj = JObject.Parse(json);
bool success = false;
string code = string.Empty, message = string.Empty, detail = string.Empty;
WGPeer peer = null;
if (obj.TryGetValue(".id", out var Id))
{
success = true;
peer = JsonConvert.DeserializeObject<WGPeer>(json);
}
else if (obj.TryGetValue("error", out var Error))
{
var error = JsonConvert.DeserializeObject<CreationStatus>(json);
success = false;
code = Error.Value<string>();
message = error.Message;
detail = error.Detail;
}
else
{
success = false;
message = "Failed";
detail = json;
};
return new()
{
Code = code,
Message = message,
Detail = detail,
Success = success,
Item = peer ?? null
};
}
public static async Task<CreationStatus> DeleteServer(string id)
{
var json = await SendDeleteRequestAsync("interface/wireguard/" + id);
if (string.IsNullOrWhiteSpace(json))
{
return new()
{
Success = true
};
}
else
{
return new()
{
Success = false,
Item = json
};
}
}
public static async Task<CreationStatus> DeleteUser(string id)
{
var json = await SendDeleteRequestAsync("interface/wireguard/peers/" + id);
if (string.IsNullOrWhiteSpace(json))
{
return new()
{
Success = true
};
}
else
{
return new()
{
Success = false,
Item = json
};
}
}
public static async Task<string> GetTrafficSpeed()
{
return await SendPostRequestAsync("interface/monitor-traffic", "{\"interface\":\"ether1\",\"duration\":\"3s\"}");
}
private static async Task<string> SendGetRequestAsync(string URL, bool IsTest = false)
{
HttpClientHandler handler = new()
{
ServerCertificateCustomValidationCallback = delegate { return true; }
};
using HttpClient httpClient = new(handler);
using var request = new HttpRequestMessage(new HttpMethod("GET"), $"https://{MT_IP}/rest/{URL}");
string base64authorization = Convert.ToBase64String(Encoding.ASCII.GetBytes($"{MT_USER}:{MT_PASS}"));
if (!IsTest) request.Headers.TryAddWithoutValidation("Authorization", $"Basic {base64authorization}");
HttpResponseMessage response = await httpClient.SendAsync(request);
return await response.Content.ReadAsStringAsync();
}
private static async Task<string> SendPostRequestAsync(string URL, string Data)
{
HttpClientHandler handler = new()
{
ServerCertificateCustomValidationCallback = (requestMessage, certificate, chain, policyErrors) => true
};
using HttpClient httpClient = new(handler);
using var request = new HttpRequestMessage(new HttpMethod("GET"), $"https://{MT_IP}/rest/{URL}");
string base64authorization = Convert.ToBase64String(Encoding.ASCII.GetBytes($"{MT_USER}:{MT_PASS}"));
request.Headers.TryAddWithoutValidation("Authorization", $"Basic {base64authorization}");
request.Content = new StringContent(Data);
request.Content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json");
HttpResponseMessage response = await httpClient.SendAsync(request);
return await response.Content.ReadAsStringAsync();
}
private static async Task<string> SendDeleteRequestAsync(string URL)
{
HttpClientHandler handler = new()
{
ServerCertificateCustomValidationCallback = (requestMessage, certificate, chain, policyErrors) => true
};
using HttpClient httpClient = new(handler);
using var request = new HttpRequestMessage(new HttpMethod("DELETE"), $"https://{MT_IP}/rest/{URL}");
string base64authorization = Convert.ToBase64String(Encoding.ASCII.GetBytes($"{MT_USER}:{MT_PASS}"));
request.Headers.TryAddWithoutValidation("Authorization", $"Basic {base64authorization}");
HttpResponseMessage response = await httpClient.SendAsync(request);
return await response.Content.ReadAsStringAsync();
}
private static async Task<string> SendPutRequestAsync(string URL, object Data)
{
HttpClientHandler handler = new()
{
ServerCertificateCustomValidationCallback = (requestMessage, certificate, chain, policyErrors) => true
};
using HttpClient httpClient = new(handler);
using var request = new HttpRequestMessage(new HttpMethod("PUT"), $"https://{MT_IP}/rest/{URL}");
string base64authorization = Convert.ToBase64String(Encoding.ASCII.GetBytes($"{MT_USER}:{MT_PASS}"));
request.Headers.TryAddWithoutValidation("Authorization", $"Basic {base64authorization}");
request.Content = new StringContent(JsonConvert.SerializeObject(Data));
request.Content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json");
HttpResponseMessage response = await httpClient.SendAsync(request);
return await response.Content.ReadAsStringAsync();
}
private static async Task<string> SendPatchRequestAsync(string URL, object Data)
{
HttpClientHandler handler = new()
{
ServerCertificateCustomValidationCallback = (requestMessage, certificate, chain, policyErrors) => true
};
using HttpClient httpClient = new(handler);
using var request = new HttpRequestMessage(new HttpMethod("PATCH"), $"https://{MT_IP}/rest/{URL}");
string base64authorization = Convert.ToBase64String(Encoding.ASCII.GetBytes($"{MT_USER}:{MT_PASS}"));
request.Headers.TryAddWithoutValidation("Authorization", $"Basic {base64authorization}");
request.Content = new StringContent(JsonConvert.SerializeObject(Data));
request.Content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json");
HttpResponseMessage response = await httpClient.SendAsync(request);
return await response.Content.ReadAsStringAsync();
}
}
}

22
DBContext.cs Normal file
View file

@ -0,0 +1,22 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Hosting;
using MTWireGuard.Models.Mikrotik;
namespace MTWireGuard
{
public class DBContext : DbContext
{
public DbSet<WGPeerDBModel> Users { get; set; }
public string DbPath { get; }
public DBContext()
{
DbPath = Path.Join(AppDomain.CurrentDomain.BaseDirectory, "MikrotikWireguard.db");
}
protected override void OnConfiguring(DbContextOptionsBuilder options)
=> options.UseSqlite($"Data Source={DbPath}");
}
}

6
Dockerfile Normal file
View file

@ -0,0 +1,6 @@
FROM mcr.microsoft.com/dotnet/aspnet:6.0-alpine AS runtime
WORKDIR /app
EXPOSE 80
EXPOSE 443
COPY published/ ./
ENTRYPOINT ["dotnet", "MTWireGuard.dll"]

88
Helper.cs Normal file
View file

@ -0,0 +1,88 @@
using MTWireGuard.Models.Mikrotik;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO.Compression;
using System.Text;
namespace MTWireGuard
{
public class Helper
{
public static readonly string[] UpperCaseTopics =
{
"dhcp",
"ppp",
"l2tp",
"pptp",
"sstp"
};
private static readonly string[] SizeSuffixes =
{ "bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB" };
public static string ConvertByteSize(long value, int decimalPlaces = 2)
{
if (decimalPlaces < 0) { throw new ArgumentOutOfRangeException("decimalPlaces"); }
if (value < 0) { return "-" + ConvertByteSize(-value, decimalPlaces); }
if (value == 0) { return "0"; }
// mag is 0 for bytes, 1 for KB, 2, for MB, etc.
int mag = (int)Math.Log(value, 1024);
// 1L << (mag * 10) == 2 ^ (10 * mag)
// [i.e. the number of bytes in the unit corresponding to mag]
decimal adjustedSize = (decimal)value / (1L << (mag * 10));
// make adjustment when the value is large enough that
// it would round up to 1000 or more
if (Math.Round(adjustedSize, decimalPlaces) >= 1000)
{
mag += 1;
adjustedSize /= 1024;
}
return string.Format("{0:n" + decimalPlaces + "} {1}",
adjustedSize,
SizeSuffixes[mag]);
}
}
public static class StringCompression
{
public static byte[] Compress(string str)
{
var bytes = Encoding.UTF8.GetBytes(str);
using MemoryStream msi = new(bytes);
using MemoryStream mso = new();
using (GZipStream gs = new(mso, CompressionMode.Compress))
{
msi.CopyTo(gs);
}
return mso.ToArray();
}
public static string Decompress(byte[] zip)
{
using MemoryStream msi = new(zip);
using MemoryStream mso = new();
using (GZipStream gs = new(msi, CompressionMode.Decompress))
{
gs.CopyTo(mso);
}
return Encoding.UTF8.GetString(mso.ToArray());
}
}
public static class StringExtensions
{
public static string FirstCharToUpper(this string input) =>
input switch
{
null => throw new ArgumentNullException(nameof(input)),
"" => throw new ArgumentException($"{nameof(input)} cannot be empty", nameof(input)),
_ => string.Concat(input[0].ToString().ToUpper(), input.AsSpan(1))
};
}
}

21
LICENSE Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2023 MTWireguard
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

239
MTAPIHandler.cs Normal file
View file

@ -0,0 +1,239 @@
using MTWireGuard.Models;
using MTWireGuard.Models.Mikrotik;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;
using System.Linq;
using System.Net;
using System.Net.Http.Headers;
using System.Text;
using static Microsoft.EntityFrameworkCore.DbLoggerCategory.Database;
namespace MTWireGuard
{
public static class MTAPIHandler
{
private static readonly string MT_IP = Environment.GetEnvironmentVariable("MT_IP");
private static readonly string MT_USER = Environment.GetEnvironmentVariable("MT_USER");
private static readonly string MT_PASS = Environment.GetEnvironmentVariable("MT_PASS");
public static async Task<List<LogViewModel>> GetLogsAsync()
{
string json = await SendGetRequestAsync("log");
var model = JsonConvert.DeserializeObject<List<Log>>(json);
return model.Select(x => new LogViewModel()
{
Id = Convert.ToUInt64(x.Id[1..], 16),
Message = x.Message,
Time = x.Time,
Topics = x.Topics.Split(',').Select(t => t = Helper.UpperCaseTopics.Contains(t) ? t.ToUpper() : t.FirstCharToUpper()).ToList()
}).ToList();
}
public static async Task<List<WGServerViewModel>> GetServersAsync()
{
string json = await SendGetRequestAsync("interface/wireguard");
var model = JsonConvert.DeserializeObject<List<WGServer>>(json);
return model.Select(x => new WGServerViewModel()
{
Id = Convert.ToInt32(x.Id[1..], 16),
Name = x.Name,
ListenPort = Convert.ToUInt16(x.ListenPort),
MTU = Convert.ToUInt16(x.MTU),
Running = x.Running,
IsEnabled = !x.Disabled,
PublicKey = x.PublicKey,
PrivateKey = x.PrivateKey
}).ToList();
}
public static async Task<WGServerViewModel> GetServer(string Name)
{
var servers = await GetServersAsync();
return servers.Find(s => s.Name == Name);
}
public static async Task<List<ServerTraffic>> GetServersTraffic()
{
var json = await SendPostRequestAsync("interface", "{\"stats\", {\".proplist\":\"name,rx-byte, tx-byte\"}}");
List<ServerTraffic> traffic = JsonConvert.DeserializeObject<List<ServerTraffic>>(json);
return traffic;
}
public static async Task<List<WGPeerViewModel>> GetUsersAsync()
{
using var db = new DBContext();
string json = await SendGetRequestAsync("interface/wireguard/peers");
List<WGPeer> apiUsers = JsonConvert.DeserializeObject<List<WGPeer>>(json);
List<WGPeerDBModel> dbUsers = db.Users.ToList();
Dictionary<int, bool> differences = new();
// Start Checking DB and Router sync
foreach (var apiUser in apiUsers)
{
var id = Convert.ToInt32(apiUser.Id[1..], 16);
var dbUser = dbUsers.Find(u => u.Id == id);
if (dbUser == null)
{
differences.Add(id, true);
}
else
{
string publickey = apiUser.PublicKey;
bool publicKeyDifferent = dbUser.PublicKey != publickey;
bool noPrivateKey = string.IsNullOrWhiteSpace(dbUser.PrivateKey);
differences.Add(id, publicKeyDifferent | noPrivateKey);
}
}
// End Checking
return apiUsers.Select(x => new WGPeerViewModel()
{
Id = Convert.ToInt32(x.Id[1..], 16),
Name = (dbUsers.Find(u => u.Id == Convert.ToInt32(x.Id[1..], 16)) != null) ? dbUsers.Find(u => u.Id == Convert.ToInt32(x.Id[1..], 16)).Name : "",
Address = x.AllowedAddress,
CurrentAddress = $"{x.CurrentEndpointAddress}:{x.CurrentEndpointPort}",
Interface = x.Interface,
IsEnabled = !x.Disabled,
PublicKey = x.PublicKey,
Download = x.RX,
Upload = x.TX,
IsDifferent = differences[Convert.ToInt32(x.Id[1..], 16)]
}).ToList();
}
public static async Task<WGPeerViewModel> GetUser(int id)
{
var users = await GetUsersAsync();
return users.Find(u => u.Id == id);
}
public static async Task<string> GetIPAddress()
{
var json = await SendGetRequestAsync("ip/address?interface=ether1&.proplist=address");
var IP = JsonConvert.DeserializeObject<List<EtherIP>>(json).FirstOrDefault();
var address = IP.Address[..IP.Address.IndexOf('/')];
return address;
}
public static async Task<MTInfoViewModel> GetInfo()
{
var json = await SendGetRequestAsync("system/resource");
var model = JsonConvert.DeserializeObject<MTInfo>(json);
return new()
{
Architecture = model.ArchitectureName,
BoardName = model.BoardName,
CPU = model.CPU,
CPUCount = Convert.ToByte(model.CPUCount),
CPUFrequency = Convert.ToInt16(model.CPUFrequency),
CPULoad = Convert.ToByte(model.CPULoad),
TotalHDDBytes = Convert.ToInt64(model.TotalHDDSpace),
FreeHDDBytes = Convert.ToInt64(model.FreeHDDSpace),
UsedHDDBytes = Convert.ToInt64(model.TotalHDDSpace) - Convert.ToInt64(model.FreeHDDSpace),
FreeHDDPercentage = (byte)(Convert.ToInt64(model.FreeHDDSpace) * 100 / Convert.ToInt64(model.TotalHDDSpace)),
TotalRAMBytes = Convert.ToInt64(model.TotalMemory),
FreeRAMBytes = Convert.ToInt64(model.FreeMemory),
UsedRAMBytes = Convert.ToInt64(model.TotalMemory) - Convert.ToInt64(model.FreeMemory),
FreeRAMPercentage = (byte)(Convert.ToInt64(model.FreeMemory) * 100 / Convert.ToInt64(model.TotalMemory)),
TotalHDD = Helper.ConvertByteSize(Convert.ToInt64(model.TotalHDDSpace)),
FreeHDD = Helper.ConvertByteSize(Convert.ToInt64(model.FreeHDDSpace)),
UsedHDD = Helper.ConvertByteSize(Convert.ToInt64(model.TotalHDDSpace) - Convert.ToInt64(model.FreeHDDSpace)),
TotalRAM = Helper.ConvertByteSize(Convert.ToInt64(model.TotalMemory)),
FreeRAM = Helper.ConvertByteSize(Convert.ToInt64(model.FreeMemory)),
UsedRAM = Helper.ConvertByteSize(Convert.ToInt64(model.TotalMemory) - Convert.ToInt64(model.FreeMemory)),
UPTime = model.Uptime.
Replace('d', ' ').
Replace('h', ':').
Replace('m', ':').
Replace("s", ""),
Platform = model.Platform,
Version = model.Version
};
}
public static async Task<bool> TryConnectAsync()
{
try
{
var connection = await SendGetRequestAsync("", true);
var status = JsonConvert.DeserializeObject<LoginStatus>(connection);
if ((status.Error == 400 && status.Message == "Bad Request") || (status.Error == 401 && status.Message == "Unauthorized"))
{
return true;
}
throw new($"[{status.Error}] Login failed, {status.Message}.<br>Enter router username/password in environment variables (MT_USER/MT_PASS).");
}
catch
{
throw;
}
}
public static async Task<string> GetCurrentSessionID()
{
var sessionJson = await SendGetRequestAsync("user/active?name=admin&via=api&.proplist=when");
JArray session = JArray.Parse(sessionJson);
var jobsJson = await SendGetRequestAsync("system/script/job?type=api-login&started=" + session.FirstOrDefault()["when"]);
JArray jobs = JArray.Parse(jobsJson);
return jobs.FirstOrDefault()[".id"].ToString();
}
public static async Task<string> KillJob(string JobID)
{
return await SendDeleteRequestAsync("system/script/job/" + JobID);
}
public static async Task<string> GetTrafficSpeed()
{
return await SendPostRequestAsync("interface/monitor-traffic", "{\"interface\":\"ether1\",\"duration\":\"3s\"}");
}
private static async Task<string> SendGetRequestAsync(string URL, bool IsTest = false)
{
HttpClientHandler handler = new()
{
ServerCertificateCustomValidationCallback = delegate { return true; }
};
using HttpClient httpClient = new(handler);
using var request = new HttpRequestMessage(new HttpMethod("GET"), $"https://{MT_IP}/rest/{URL}");
string base64authorization = Convert.ToBase64String(Encoding.ASCII.GetBytes($"{MT_USER}:{MT_PASS}"));
if (!IsTest) request.Headers.TryAddWithoutValidation("Authorization", $"Basic {base64authorization}");
HttpResponseMessage response = await httpClient.SendAsync(request);
string APIResponse = await response.Content.ReadAsStringAsync();
return APIResponse;
}
private static async Task<string> SendPostRequestAsync(string URL, string Data)
{
HttpClientHandler handler = new()
{
ServerCertificateCustomValidationCallback = (requestMessage, certificate, chain, policyErrors) => true
};
using HttpClient httpClient = new(handler);
using var request = new HttpRequestMessage(new HttpMethod("GET"), $"https://{MT_IP}/rest/{URL}");
string base64authorization = Convert.ToBase64String(Encoding.ASCII.GetBytes($"{MT_USER}:{MT_PASS}"));
request.Headers.TryAddWithoutValidation("Authorization", $"Basic {base64authorization}");
request.Content = new StringContent(Data);
request.Content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json");
HttpResponseMessage response = await httpClient.SendAsync(request);
return await response.Content.ReadAsStringAsync();
}
private static async Task<string> SendDeleteRequestAsync(string URL)
{
HttpClientHandler handler = new()
{
ServerCertificateCustomValidationCallback = (requestMessage, certificate, chain, policyErrors) => true
};
using HttpClient httpClient = new(handler);
using var request = new HttpRequestMessage(new HttpMethod("DELETE"), $"https://{MT_IP}/rest/{URL}");
string base64authorization = Convert.ToBase64String(Encoding.ASCII.GetBytes($"{MT_USER}:{MT_PASS}"));
request.Headers.TryAddWithoutValidation("Authorization", $"Basic {base64authorization}");
HttpResponseMessage response = await httpClient.SendAsync(request);
return await response.Content.ReadAsStringAsync();
}
}
}

28
MTWireGuard.csproj Normal file
View file

@ -0,0 +1,28 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<UserSecretsId>943131a5-93e0-4ec4-91aa-e26e825730c4</UserSecretsId>
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
<DockerfileContext>.</DockerfileContext>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="AutoMapper" Version="12.0.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="7.0.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="7.0.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.17.0" />
<PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="6.0.12" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.2" />
<PackageReference Include="QRCoder" Version="1.4.2" />
<PackageReference Include="Razor.Templating.Core" Version="1.8.0" />
<PackageReference Include="System.Drawing.Common" Version="7.0.0" />
</ItemGroup>
</Project>

30
MTWireGuard.sln Normal file
View file

@ -0,0 +1,30 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.3.32819.101
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MTWireGuard", "MTWireGuard.csproj", "{F4826D62-8AB3-4565-A3F1-8FD5998DFA43}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{810BE84C-BB0A-4280-BBE9-E0A1B859BDED}"
ProjectSection(SolutionItems) = preProject
.editorconfig = .editorconfig
EndProjectSection
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{F4826D62-8AB3-4565-A3F1-8FD5998DFA43}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F4826D62-8AB3-4565-A3F1-8FD5998DFA43}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F4826D62-8AB3-4565-A3F1-8FD5998DFA43}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F4826D62-8AB3-4565-A3F1-8FD5998DFA43}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {E6EC9B9D-E199-4D33-8B16-F398E2092A72}
EndGlobalSection
EndGlobal

114
Mapper/MappingProfile.cs Normal file
View file

@ -0,0 +1,114 @@
using AutoMapper;
using Microsoft.EntityFrameworkCore;
using MTWireGuard.Models;
using MTWireGuard.Models.Mikrotik;
namespace MTWireGuard.Mapper
{
public class MappingProfile : Profile
{
public MappingProfile()
{
// Logs
CreateMap<Log, LogViewModel>()
.ForMember(dest => dest.Id,
opt => opt.MapFrom(src => Convert.ToUInt64(src.Id.Substring(1), 16)))
.ForMember(dest =>dest.Topics,
opt => opt.MapFrom(src => FormatTopics(src.Topics)));
// Server Traffic
CreateMap<ServerTraffic, ServerTrafficViewModel>()
.ForMember(dest => dest.Name,
opt => opt.MapFrom(src => src.Name))
.ForMember(dest => dest.Type,
opt => opt.MapFrom(src => src.Type))
.ForMember(dest => dest.Upload,
opt => opt.MapFrom(src => Helper.ConvertByteSize(Convert.ToInt64(src.TX), 2)))
.ForMember(dest => dest.Download,
opt => opt.MapFrom(src => Helper.ConvertByteSize(Convert.ToInt64(src.RX), 2)))
.ForMember(dest => dest.UploadBytes,
opt => opt.MapFrom(src => Convert.ToInt64(src.TX)))
.ForMember(dest => dest.DownloadBytes,
opt => opt.MapFrom(src => Convert.ToInt64(src.RX)));
// Mikrotik HWInfo
CreateMap<MTInfo, MTInfoViewModel>()
.ForMember(dest => dest.Architecture,
opt => opt.MapFrom(src => src.ArchitectureName))
.ForMember(dest => dest.CPUCount,
opt => opt.MapFrom(src => Convert.ToByte(src.CPUCount)))
.ForMember(dest => dest.CPUFrequency,
opt => opt.MapFrom(src => Convert.ToInt16(src.CPUFrequency)))
.ForMember(dest => dest.CPULoad,
opt => opt.MapFrom(src => Convert.ToByte(src.CPULoad)))
.ForMember(dest => dest.TotalHDDBytes,
opt => opt.MapFrom(src => Convert.ToInt64(src.TotalHDDSpace)))
.ForMember(dest => dest.FreeHDDBytes,
opt => opt.MapFrom(src => Convert.ToInt64(src.FreeHDDSpace)))
.ForMember(dest => dest.UsedHDDBytes,
opt => opt.MapFrom(src => Convert.ToInt64(src.TotalHDDSpace) - Convert.ToInt64(src.FreeHDDSpace)))
.ForMember(dest => dest.FreeHDDPercentage,
opt => opt.MapFrom(src => (byte)(Convert.ToInt64(src.FreeHDDSpace) * 100 / Convert.ToInt64(src.TotalHDDSpace))))
.ForMember(dest => dest.TotalRAMBytes,
opt => opt.MapFrom(src => Convert.ToInt64(src.TotalMemory)))
.ForMember(dest => dest.FreeRAMBytes,
opt => opt.MapFrom(src => Convert.ToInt64(src.FreeMemory)))
.ForMember(dest => dest.UsedRAMBytes,
opt => opt.MapFrom(src => Convert.ToInt64(src.TotalMemory) - Convert.ToInt64(src.FreeMemory)))
.ForMember(dest => dest.FreeRAMPercentage,
opt => opt.MapFrom(src => (byte)(Convert.ToInt64(src.FreeMemory) * 100 / Convert.ToInt64(src.TotalMemory))))
.ForMember(dest => dest.TotalHDD,
opt => opt.MapFrom(src => Helper.ConvertByteSize(Convert.ToInt64(src.TotalHDDSpace), 2)))
.ForMember(dest => dest.FreeHDD,
opt => opt.MapFrom(src => Helper.ConvertByteSize(Convert.ToInt64(src.FreeHDDSpace), 2)))
.ForMember(dest => dest.UsedHDD,
opt => opt.MapFrom(src => Helper.ConvertByteSize(Convert.ToInt64(src.TotalHDDSpace) - Convert.ToInt64(src.FreeHDDSpace), 2)))
.ForMember(dest => dest.TotalRAM,
opt => opt.MapFrom(src => Helper.ConvertByteSize(Convert.ToInt64(src.TotalMemory), 2)))
.ForMember(dest => dest.FreeRAM,
opt => opt.MapFrom(src => Helper.ConvertByteSize(Convert.ToInt64(src.FreeMemory), 2)))
.ForMember(dest => dest.UsedRAM,
opt => opt.MapFrom(src => Helper.ConvertByteSize(Convert.ToInt64(src.TotalMemory) - Convert.ToInt64(src.FreeMemory), 2)))
.ForMember(dest => dest.UPTime,
opt => opt.MapFrom(src => src.Uptime.
// Replace('d', ' ').
Replace('h', ':').
Replace('m', ':').
Replace("s", "")))
;
// Router Identity
CreateMap<MTIdentity, MTIdentityViewModel>();
// Active Users
CreateMap<ActiveUser, ActiveUserViewModel>()
.ForMember(dest => dest.Id,
opt => opt.MapFrom(src => Convert.ToInt16(src.Id.Substring(1), 16)))
.ForMember(dest => dest.LoggedIn,
opt => opt.MapFrom(src => src.When));
// Active Jobs
CreateMap<Job, JobViewModel>()
.ForMember(dest => dest.Id,
opt => opt.MapFrom(src => Convert.ToInt16(src.Id.Substring(1), 16)))
.ForMember(dest => dest.NextId,
opt => opt.MapFrom(src => Convert.ToInt16(src.NextId.Substring(1), 16)))
.ForMember(dest => dest.Policies,
opt => opt.MapFrom(src => src.Policy.Split(',', StringSplitOptions.None).ToList()));
// Item Creation
CreateMap<CreationStatus, CreationResult>()
.ForMember(dest => dest.Code,
opt => opt.MapFrom(src => (src.Success) ? "200" : src.Code.ToString()))
.ForMember(dest => dest.Title,
opt => opt.MapFrom(src => (src.Success) ? "Done" : src.Message))
.ForMember(dest => dest.Description,
opt => opt.MapFrom(src => (src.Success) ? "Item created/updated successfully." : src.Detail));
}
private static List<string> FormatTopics(string topics)
{
return topics.Split(',', StringSplitOptions.TrimEntries).Select(t => t = Helper.UpperCaseTopics.Contains(t) ? t.ToUpper() : t.FirstCharToUpper()).ToList();
}
}
}

89
Mapper/PeerMapping.cs Normal file
View file

@ -0,0 +1,89 @@
using AutoMapper;
using MTWireGuard.Models.Mikrotik;
using MTWireGuard.Models.Requests;
namespace MTWireGuard.Mapper
{
public class PeerMapping : Profile
{
DBContext db;
public PeerMapping(DBContext context)
{
db = context;
/*
* Mikrotik Peer to ViewModel
*/
CreateMap<WGPeer, WGPeerViewModel>()
.ForMember(dest => dest.Id,
opt => opt.MapFrom(src => Convert.ToInt32(src.Id.Substring(1), 16)))
.ForMember(dest => dest.Name,
opt => opt.MapFrom(src => GetPeerName(src)))
.ForMember(dest => dest.PrivateKey,
opt => opt.MapFrom(src => GetPeerPrivateKey(src)))
.ForMember(dest => dest.Address,
opt => opt.MapFrom(src => src.AllowedAddress))
.ForMember(dest => dest.CurrentAddress,
opt => opt.MapFrom(src => $"{src.CurrentEndpointAddress}:{src.CurrentEndpointPort}"))
.ForMember(dest => dest.IsEnabled,
opt => opt.MapFrom(src => !src.Disabled))
.ForMember(dest => dest.IsDifferent,
opt => opt.MapFrom(src => HasDifferences(src)))
.ForMember(dest => dest.Upload,
opt => opt.MapFrom(src => Helper.ConvertByteSize(Convert.ToInt64(src.TX), 2)))
.ForMember(dest => dest.Download,
opt => opt.MapFrom(src => Helper.ConvertByteSize(Convert.ToInt64(src.RX), 2)))
.ForMember(dest => dest.UploadBytes,
opt => opt.MapFrom(src => Convert.ToInt64(src.TX)))
.ForMember(dest => dest.DownloadBytes,
opt => opt.MapFrom(src => Convert.ToInt64(src.RX)));
// WGPeer
CreateMap<UserCreateModel, WGPeerCreateModel>();
CreateMap<WGPeerCreateModel, WGPeerDBModel>();
CreateMap<UserUpdateModel, WGPeerUpdateModel>()
.ForMember(dest => dest.Id,
opt => opt.MapFrom(src => $"*{src.Id:X}"));
CreateMap<CreateClientRequest, UserCreateModel>()
.ForMember(dest => dest.Disabled,
opt => opt.MapFrom(src => !src.Enabled))
.ForMember(dest => dest.EndpointAddress,
opt => opt.MapFrom(src => src.Endpoint))
.ForMember(dest => dest.PersistentKeepalive,
opt => opt.MapFrom(src => src.KeepAlive.ToString()));
CreateMap<SyncUserRequest, UserSyncModel>();
CreateMap<UpdateClientRequest, UserUpdateModel>()
.ForMember(dest => dest.EndpointAddress,
opt => opt.MapFrom(src => src.Endpoint))
.ForMember(dest => dest.PersistentKeepalive,
opt => opt.MapFrom(src => src.KeepAlive));
// DBUser
CreateMap<WGPeerViewModel, WGPeerDBModel>();
CreateMap<UserSyncModel, WGPeerDBModel>();
CreateMap<UserSyncModel, WGPeerUpdateModel>()
.ForMember(dest => dest.Id,
opt => opt.MapFrom(src => $"*{src.Id:X}"));
CreateMap<UserUpdateModel, WGPeerDBModel>();
}
private string? GetPeerName(WGPeer source)
{
return (db.Users.ToList().Find(u => u.Id == Convert.ToInt32(source.Id[1..], 16)) != null) ? db.Users.ToList().Find(u => u.Id == Convert.ToInt32(source.Id[1..], 16)).Name : "";
}
private string? GetPeerPrivateKey(WGPeer source)
{
return (db.Users.ToList().Find(u => u.Id == Convert.ToInt32(source.Id[1..], 16)) != null) ? db.Users.ToList().Find(u => u.Id == Convert.ToInt32(source.Id[1..], 16)).PrivateKey : "";
}
private bool HasDifferences(WGPeer source)
{
var id = Convert.ToInt32(source.Id[1..], 16);
var dbUser = db.Users.ToList().Find(x => x.Id == id);
if (dbUser is null) return true;
if (dbUser.PublicKey != source.PublicKey) return true;
return string.IsNullOrWhiteSpace(dbUser.PrivateKey);
}
}
}

44
Mapper/ServerMapping.cs Normal file
View file

@ -0,0 +1,44 @@
using AutoMapper;
using MTWireGuard.Models.Mikrotik;
using MTWireGuard.Models.Requests;
namespace MTWireGuard.Mapper
{
public class ServerMapping : Profile
{
public ServerMapping()
{
/*
* Convert Mikrotik Server Model to ViewModel
*/
CreateMap<WGServer, WGServerViewModel>()
.ForMember(dest => dest.Id,
opt => opt.MapFrom(src => Convert.ToInt32(src.Id.Substring(1), 16)))
.ForMember(dest => dest.IsEnabled,
opt => opt.MapFrom(src => !src.Disabled));
/*
* Convert Wrapper CreateModel to Rest-API CreateModel
*/
CreateMap<CreateServerRequest, ServerCreateModel>()
.ForMember(dest => dest.ListenPort,
opt => opt.MapFrom(src => src.Port));
CreateMap<ServerCreateModel, WGServerCreateModel>()
.ForMember(dest => dest.Disabled,
opt => opt.MapFrom(src => !src.Enabled));
/*
* Convert Wrapper UpdateModel to Rest-API UpdateModel
*/
CreateMap<UpdateServerRequest, ServerUpdateModel>()
.ForMember(dest => dest.ListenPort,
opt => opt.MapFrom(src => src.Port));
CreateMap<ServerUpdateModel, WGServerUpdateModel>()
.ForMember(dest => dest.Id,
opt => opt.MapFrom(src => $"*{src.Id:X}"));
}
}
}

View file

@ -0,0 +1,64 @@
using Microsoft.AspNetCore.Antiforgery;
using MTWireGuard.Pages;
using MTWireGuard.Repositories;
using Newtonsoft.Json;
using Razor.Templating.Core;
using System.Globalization;
namespace MTWireGuard.Middlewares
{
public class AntiForgeryMiddleware : IMiddleware
{
public async Task InvokeAsync(HttpContext context, RequestDelegate next)
{
if (context.Request.Path.Value == "/Login")
{
await next(context);
return;
}
try
{
var antiForgeryService = context.RequestServices.GetRequiredService<IAntiforgery>();
var isGetRequest = string.Equals("GET", context.Request.Method, StringComparison.OrdinalIgnoreCase);
if (!isGetRequest)
{
await antiForgeryService.ValidateRequestAsync(context);
}
}
catch (AntiforgeryValidationException ex)
{
string response = string.Empty;
ErrorModel errorModel = new();
Dictionary<string, object> ViewBag = new()
{
["Title"] = "XSRF token validation failed!",
["Message"] = ex.Message
};
if (context.Request.Path.Value == "/Login")
response = await RazorTemplateEngine.RenderAsync("/Pages/Error.cshtml", errorModel, ViewBag);
else
{
var result = new Dictionary<string, string>()
{
{"background", "danger"},
{"title", ViewBag["Title"].ToString()},
{"body", ViewBag["Message"].ToString()},
};
response = JsonConvert.SerializeObject(result);
}
context.Response.StatusCode = 200;
await context.Response.WriteAsync(response);
return;
}
await next(context);
}
}
public static class AntiForgeryMiddlewareExtensions
{
public static IApplicationBuilder UseAntiForgery(this IApplicationBuilder builder)
{
return builder.UseMiddleware<AntiForgeryMiddleware>();
}
}
}

View file

@ -0,0 +1,77 @@
using MTWireGuard.Pages;
using MTWireGuard.Repositories;
using Razor.Templating.Core;
using System.Globalization;
namespace MTWireGuard.Middlewares
{
public class DependencyCheckMiddleware
{
private readonly RequestDelegate _next;
private readonly IMikrotikRepository API;
public DependencyCheckMiddleware(RequestDelegate next, IMikrotikRepository mikrotik)
{
_next = next;
API = mikrotik;
}
public async Task InvokeAsync(HttpContext context)
{
bool Error = false;
string? IP = Environment.GetEnvironmentVariable("MT_IP");
string? USER = Environment.GetEnvironmentVariable("MT_USER");
string? PASS = Environment.GetEnvironmentVariable("MT_PASS");
string? PUBLICIP = Environment.GetEnvironmentVariable("MT_PUBLIC_IP");
ErrorModel errorModel = new();
Dictionary<string, object> ViewBag = new();
if (string.IsNullOrEmpty(IP) || string.IsNullOrEmpty(USER) || string.IsNullOrEmpty(PUBLICIP))
{
ViewBag["Title"] = "Environment variables are not set!";
ViewBag["Message"] = "Please set \"MT_IP\", \"MT_USER\", \"MT_PASS\", \"MT_PUBLIC_IP\" variables in container environment.";
Error = true;
}
else
{
if (context.Request.Path.Value == "/Login")
{
await _next(context);
return;
}
try
{
bool APIEnabled = await API.TryConnectAsync();
}
catch (Exception ex)
{
ViewBag["Title"] = "Error connecting to the router api!";
ViewBag["Message"] = ex.Message;
Error = true;
}
}
if (Error)
{
var html = await RazorTemplateEngine.RenderAsync("/Pages/Error.cshtml", errorModel, ViewBag);
context.Response.StatusCode = 200;
await context.Response.WriteAsync(html);
return;
}
else
{
await _next(context);
}
}
}
public static class DependencyCheckMiddlewareExtensions
{
public static IApplicationBuilder UseDependencyCheck(this IApplicationBuilder builder)
{
return builder.UseMiddleware<DependencyCheckMiddleware>();
}
}
}

View file

@ -0,0 +1,53 @@
// <auto-generated />
using MTWireGuard;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace MTWireGuard.Migrations
{
[DbContext(typeof(DBContext))]
[Migration("20230108171012_Initialize")]
partial class Initialize
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "7.0.1");
modelBuilder.Entity("MTWireGuard.Models.Mikrotik.WGPeerDBModel", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("PrivateKey")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("PublicKey")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("PrivateKey")
.IsUnique();
b.HasIndex("PublicKey")
.IsUnique();
b.ToTable("Users");
});
#pragma warning restore 612, 618
}
}
}

View file

@ -0,0 +1,48 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace MTWireGuard.Migrations
{
/// <inheritdoc />
public partial class Initialize : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "Users",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
Name = table.Column<string>(type: "TEXT", nullable: false),
PrivateKey = table.Column<string>(type: "TEXT", nullable: false),
PublicKey = table.Column<string>(type: "TEXT", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Users", x => x.Id);
});
migrationBuilder.CreateIndex(
name: "IX_Users_PrivateKey",
table: "Users",
column: "PrivateKey",
unique: true);
migrationBuilder.CreateIndex(
name: "IX_Users_PublicKey",
table: "Users",
column: "PublicKey",
unique: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "Users");
}
}
}

View file

@ -0,0 +1,50 @@
// <auto-generated />
using MTWireGuard;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace MTWireGuard.Migrations
{
[DbContext(typeof(DBContext))]
partial class DBContextModelSnapshot : ModelSnapshot
{
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "7.0.1");
modelBuilder.Entity("MTWireGuard.Models.Mikrotik.WGPeerDBModel", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("PrivateKey")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("PublicKey")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("PrivateKey")
.IsUnique();
b.HasIndex("PublicKey")
.IsUnique();
b.ToTable("Users");
});
#pragma warning restore 612, 618
}
}
}

18
Models/CreationStatus.cs Normal file
View file

@ -0,0 +1,18 @@
namespace MTWireGuard.Models
{
public class CreationStatus
{
public bool Success { get; set; }
public string Code { get; set; }
public string Detail { get; set; }
public string Message { get; set; }
public object Item { get; set; }
}
public class CreationResult
{
public string Code { get; set; }
public string Title { get; set; }
public string Description { get; set; }
}
}

8
Models/LoginFailed.cs Normal file
View file

@ -0,0 +1,8 @@
namespace MTWireGuard.Models
{
public class LoginStatus
{
public short Error { get; set; }
public string Message { get; set; }
}
}

View file

@ -0,0 +1,25 @@
using Newtonsoft.Json;
namespace MTWireGuard.Models.Mikrotik
{
public class ActiveUser
{
[JsonProperty(".id")]
public string Id { get; set; }
public string Group { get; set; }
public string Name { get; set; }
public bool Radius { get; set; }
public string Via { get; set; }
public string When { get; set; }
}
public class ActiveUserViewModel
{
public short Id { get; set; }
public string Group { get; set; }
public string Name { get; set; }
public bool Radius { get; set; }
public string Via { get; set; }
public string LoggedIn { get; set; }
}
}

View file

@ -0,0 +1,12 @@
using Newtonsoft.Json;
using System.Text.Json.Serialization;
namespace MTWireGuard.Models.Mikrotik
{
public class EtherIP
{
[JsonProperty(".id")]
public string Id { get; set; }
public string Address { get; set; }
}
}

View file

@ -0,0 +1,12 @@
namespace MTWireGuard.Models.Mikrotik
{
public class MTIdentity
{
public string Name { get; set; }
}
public class MTIdentityViewModel
{
public string Name { get; set; }
}
}

26
Models/Mikrotik/Job.cs Normal file
View file

@ -0,0 +1,26 @@
using Newtonsoft.Json;
namespace MTWireGuard.Models.Mikrotik
{
public class Job
{
[JsonProperty(".id")]
public string Id { get; set; }
[JsonProperty(".nextid")]
public string NextId { get; set; }
public string Owner { get; set; }
public string Policy { get; set; }
public string Started { get; set; }
public string Type { get; set; }
}
public class JobViewModel
{
public short Id { get; set; }
public short NextId { get; set; }
public string Owner { get; set; }
public List<string> Policies { get; set; }
public string Started { get; set; }
public string Type { get; set; }
}
}

21
Models/Mikrotik/Log.cs Normal file
View file

@ -0,0 +1,21 @@
using Newtonsoft.Json;
namespace MTWireGuard.Models.Mikrotik
{
public class Log
{
[JsonProperty(".id")]
public string Id { get; set; }
public string Message { get; set; }
public string Time { get; set; }
public string Topics { get; set; }
}
public class LogViewModel
{
public ulong Id { get; set; }
public string Message { get; set; }
public string Time { get; set; }
public List<string> Topics { get; set; }
}
}

74
Models/Mikrotik/MTInfo.cs Normal file
View file

@ -0,0 +1,74 @@
using Newtonsoft.Json;
namespace MTWireGuard.Models.Mikrotik
{
public class MTInfo
{
[JsonProperty("architecture-name")]
public string ArchitectureName { get; set; }
[JsonProperty("board-name")]
public string BoardName { get; set; }
[JsonProperty("build-time")]
public string BuildTime { get; set; }
public string CPU { get; set; }
[JsonProperty("cpu-count")]
public string CPUCount { get; set; }
[JsonProperty("cpu-frequency")]
public string CPUFrequency { get; set; }
[JsonProperty("cpu-load")]
public string CPULoad { get; set; }
[JsonProperty("free-hdd-space")]
public string FreeHDDSpace { get; set; }
[JsonProperty("free-memory")]
public string FreeMemory { get; set; }
public string Platform { get; set; }
[JsonProperty("total-hdd-space")]
public string TotalHDDSpace { get; set; }
[JsonProperty("total-memory")]
public string TotalMemory { get; set; }
public string Uptime { get; set; }
public string Version { get; set; }
[JsonProperty("write-sect-since-reboot")]
public string WriteSectSinceReboot { get; set; }
[JsonProperty("write-sect-total")]
public string WriteSectTotal { get; set; }
}
public class MTInfoViewModel
{
public string Architecture { get; set; }
public string BoardName { get; set; }
public string Platform { get; set; }
public string CPU { get; set; }
public byte CPUCount { get; set; }
public short CPUFrequency { get; set; }
public byte CPULoad { get; set; }
public string TotalHDD { get; set; }
public string UsedHDD { get; set; }
public string FreeHDD { get; set; }
public long TotalHDDBytes { get; set; }
public long UsedHDDBytes { get; set; }
public long FreeHDDBytes { get; set; }
public byte FreeHDDPercentage { get; set; }
public string TotalRAM { get; set; }
public string UsedRAM { get; set; }
public string FreeRAM { get; set; }
public long TotalRAMBytes { get; set; }
public long UsedRAMBytes { get; set; }
public long FreeRAMBytes { get; set; }
public byte FreeRAMPercentage { get; set; }
public string UPTime { get; set; }
public string Version { get; set; }
}
}

View file

@ -0,0 +1,24 @@
using Newtonsoft.Json;
namespace MTWireGuard.Models.Mikrotik
{
public class ServerTraffic
{
public string Name { get; set; }
public string Type { get; set; }
[JsonProperty("tx-byte")]
public string TX { get; set; }
[JsonProperty("rx-byte")]
public string RX { get; set; }
}
public class ServerTrafficViewModel
{
public string Name { get; set; }
public string Type { get; set; }
public string Upload { get; set; }
public string Download { get; set; }
public long UploadBytes { get; set; }
public long DownloadBytes { get; set; }
}
}

136
Models/Mikrotik/WGPeer.cs Normal file
View file

@ -0,0 +1,136 @@
using Microsoft.EntityFrameworkCore;
using Newtonsoft.Json;
using System.ComponentModel;
namespace MTWireGuard.Models.Mikrotik
{
public class WGPeer
{
[JsonProperty(".id")]
public string Id { get; set; }
[JsonProperty("allowed-address")]
public string AllowedAddress { get; set; }
[JsonProperty("current-endpoint-address")]
public string CurrentEndpointAddress { get; set; }
[JsonProperty("current-endpoint-port")]
public string CurrentEndpointPort { get; set; }
[JsonProperty("disabled")]
public bool Disabled { get; set; }
[JsonProperty("interface")]
public string Interface { get; set; }
[JsonProperty("endpoint-address")]
public string EndpointAddress { get; set; }
[JsonProperty("endpoint-port")]
public string EndpointPort { get; set; }
[JsonProperty("public-key")]
public string PublicKey { get; set; }
[JsonProperty("rx")]
public string RX { get; set; }
[JsonProperty("tx")]
public string TX { get; set; }
}
[PrimaryKey("Id")]
[Index("PrivateKey", IsUnique = true)]
[Index("PublicKey", IsUnique = true)]
public class WGPeerDBModel
{
public int Id { get; set; }
public string? Name { get; set; }
public string PrivateKey { get; set; }
public string PublicKey { get; set; }
}
public class WGPeerViewModel
{
public int Id { get; set; }
public string Name { get; set; }
public string Address { get; set; }
public string CurrentAddress { get; set; }
public bool IsEnabled { get; set; }
public string Interface { get; set; }
public string PrivateKey { get; set; }
public string PublicKey { get; set; }
public string Download { get; set; }
public string Upload { get; set; }
public long DownloadBytes { get; set; }
public long UploadBytes { get; set; }
public bool IsDifferent { get; set; }
}
public class WGPeerCreateModel
{
[JsonProperty("allowed-address")]
public string AllowedAddress { get; set; }
[JsonProperty("disabled")]
public bool Disabled { get; set; }
[JsonProperty("interface")]
public string Interface { get; set; }
[JsonProperty("endpoint-address"), DefaultValue("")]
public string EndpointAddress { get; set; }
[JsonProperty("endpoint-port"), DefaultValue("")]
public string EndpointPort { get; set; }
[JsonProperty("public-key")]
public string PublicKey { get; set; }
[JsonProperty("preshared-key"), DefaultValue("")]
public string PresharedKey { get; set; }
[JsonProperty("persistent-keepalive"), DefaultValue("")]
public string PersistentKeepalive { get; set; }
}
public class WGPeerUpdateModel
{
[JsonProperty(".id")]
public string Id { get; set; }
[JsonProperty("allowed-address"), DefaultValue("")]
public string AllowedAddress { get; set; }
[JsonProperty("interface")]
public string Interface { get; set; }
[JsonProperty("endpoint-address"), DefaultValue("")]
public string EndpointAddress { get; set; }
[JsonProperty("endpoint-port"), DefaultValue(0)]
public ushort EndpointPort { get; set; }
[JsonProperty("public-key"), DefaultValue("")]
public string PublicKey { get; set; }
[JsonProperty("preshared-key"), DefaultValue("")]
public string PresharedKey { get; set; }
[JsonProperty("persistent-keepalive"), DefaultValue(0)]
public int PersistentKeepalive { get; set; }
}
public class UserCreateModel
{
public string Name { get; set; }
public string PrivateKey { get; set; }
public string AllowedAddress { get; set; }
public bool Disabled { get; set; }
public string Interface { get; set; }
public string EndpointAddress { get; set; }
public string EndpointPort { get; set; }
public string PublicKey { get; set; }
public string PresharedKey { get; set; }
public string PersistentKeepalive { get; set; }
}
public class UserSyncModel
{
public int Id { get; set; }
public string Name { get; set; }
public string PrivateKey { get; set; }
public string PublicKey { get; set; }
}
public class UserUpdateModel
{
public int Id { get; set; }
public string Name { get; set; }
public string AllowedAddress { get; set; }
public string Interface { get; set; }
public string EndpointAddress { get; set; }
public ushort EndpointPort { get; set; }
public string PublicKey { get; set; }
public string PrivateKey { get; set; }
public string PresharedKey { get; set; }
public int PersistentKeepalive { get; set; }
}
}

View file

@ -0,0 +1,83 @@
using Newtonsoft.Json;
using System.ComponentModel;
namespace MTWireGuard.Models.Mikrotik
{
public class WGServer
{
[JsonProperty(".id")]
public string Id { get; set; }
[JsonProperty("name")]
public string Name { get; set; }
[JsonProperty("disabled")]
public bool Disabled { get; set; }
[JsonProperty("listen-port")]
public string ListenPort { get; set; }
[JsonProperty("mtu")]
public string MTU { get; set; }
[JsonProperty("private-key")]
public string PrivateKey { get; set; }
[JsonProperty("public-key")]
public string PublicKey { get; set; }
[JsonProperty("running")]
public bool Running { get; set; }
}
public class WGServerViewModel
{
public int Id { get; set; }
public string Name { get; set; }
public bool IsEnabled { get; set; }
public ushort ListenPort { get; set; }
public ushort MTU { get; set; }
public string PrivateKey { get; set; }
public string PublicKey { get; set; }
public bool Running { get; set; }
}
public class ServerCreateModel
{
public string Name { get; set; }
public bool Enabled { get; set; }
public string ListenPort { get; set; }
public string MTU { get; set; }
public string PrivateKey { get; set; }
}
public class ServerUpdateModel
{
public int Id { get; set; }
public string Name { get; set; }
public ushort ListenPort { get; set; }
public ushort MTU { get; set; }
public string PrivateKey { get; set; }
}
public class WGServerCreateModel
{
[JsonProperty("name")]
public string Name { get; set; }
[JsonProperty("disabled"), DefaultValue(false)]
public bool Disabled { get; set; }
[JsonProperty("listen-port")]
public ushort ListenPort { get; set; }
[JsonProperty("mtu")]
public ushort MTU { get; set; }
[JsonProperty("private-key")]
public string PrivateKey { get; set; }
}
public class WGServerUpdateModel
{
[JsonProperty(".id")]
public string Id { get; set; }
[JsonProperty("name"), DefaultValue("")]
public string Name { get; set; }
[JsonProperty("listen-port"), DefaultValue(0)]
public ushort ListenPort { get; set; }
[JsonProperty("mtu"), DefaultValue(0)]
public ushort MTU { get; set; }
[JsonProperty("private-key"), DefaultValue("")]
public string PrivateKey { get; set; }
}
}

View file

@ -0,0 +1,13 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.Build.Framework;
namespace MTWireGuard.Models.Requests
{
public class ChangeStateRequest
{
[FromQuery(Name = "ID"), Required]
public int Id { get; set; }
[FromQuery(Name = "IsEnabled"), Required]
public bool Enabled { get; set; }
}
}

View file

@ -0,0 +1,18 @@
using System.Net;
namespace MTWireGuard.Models.Requests
{
public class CreateClientRequest
{
public string Name { get; set; }
public string Endpoint { get; set; }
public ushort EndpointPort { get; set; }
public string AllowedAddress { get; set; }
public string PresharedKey { get; set; }
public string PrivateKey { get; set; }
public string PublicKey { get; set; }
public string Interface { get; set; }
public int KeepAlive { get; set; }
public bool Enabled { get; set; }
}
}

View file

@ -0,0 +1,17 @@
using System.ComponentModel;
namespace MTWireGuard.Models.Requests
{
public class CreateServerRequest
{
public string Name { get; set; }
public ushort Port { get; set; } = 13231;
public ushort MTU { get; set; } = 1420;
public string PrivateKey { get; set; }
public bool Enabled { get; set; }
}
}

View file

@ -0,0 +1,7 @@
namespace MTWireGuard.Models.Requests
{
public class DeleteRequest
{
public int Id { get; set; }
}
}

View file

@ -0,0 +1,10 @@
namespace MTWireGuard.Models.Requests
{
public class SyncUserRequest
{
public int ID { get; set; }
public string Name { get; set; }
public string PrivateKey { get; set; }
public string PublicKey { get; set; }
}
}

View file

@ -0,0 +1,16 @@
namespace MTWireGuard.Models.Requests
{
public class UpdateClientRequest
{
public int ID { get; set; }
public string Name { get; set; }
public string Endpoint { get; set; }
public ushort EndpointPort { get; set; }
public string AllowedAddress { get; set; }
public string PresharedKey { get; set; }
public string PrivateKey { get; set; }
public string PublicKey { get; set; }
public string Interface { get; set; }
public int KeepAlive { get; set; }
}
}

View file

@ -0,0 +1,15 @@
namespace MTWireGuard.Models.Requests
{
public class UpdateServerRequest
{
public int Id { get; set; }
public string Name { get; set; }
public ushort Port { get; set; }
public ushort MTU { get; set; }
public string PrivateKey { get; set; }
}
}

View file

@ -0,0 +1,34 @@
using Microsoft.AspNetCore.Mvc;
using Newtonsoft.Json;
namespace MTWireGuard.Models.Responses
{
public class ToastResult : IActionResult
{
private class Toast
{
public string Title { get; set; }
public string Body { get; set; }
public string Background { get; set; }
}
private readonly Toast _result;
public ToastResult(string title, string body, string background)
{
_result = new()
{
Title = title,
Body = body,
Background = background
};
}
public async Task ExecuteResultAsync(ActionContext context)
{
var objectResult = new ObjectResult(_result);
await objectResult.ExecuteResultAsync(context);
}
}
}

12
Models/WGEnability.cs Normal file
View file

@ -0,0 +1,12 @@
using Newtonsoft.Json;
namespace MTWireGuard.Models
{
public class WGEnability
{
[JsonProperty(".id")]
public string ID { get; set; }
[JsonProperty("disabled")]
public bool Disabled { get; set; }
}
}

111
Pages/Clients.cshtml Normal file
View file

@ -0,0 +1,111 @@
@page "{handler?}"
@using MTWireGuard.Models.Mikrotik
@model MTWireGuard.Pages.ClientsModel
@{
ViewData["Title"] = "Clients";
ViewData["Breadcrumb"] = "Users";
var servers = (List<WGServerViewModel>)ViewData["servers"];
}
<div class="row">
<div class="table-responsive">
<table class="table border mb-0">
<thead class="table-light fw-semibold">
<tr class="align-middle">
<th class="text-center">
<svg class="icon">
<use xlink:href="vendors/coreui/icons/svg/free.svg#cil-people"></use>
</svg>
</th>
<th>Name</th>
<th>Interface</th>
<th>Address</th>
<th>Public-Key</th>
<th>Traffic</th>
<th>Status</th>
<th></th>
</tr>
</thead>
<tbody>
</tbody>
</table>
</div>
</div>
<div class="row mt-2">
<div class="col-12">
@await Component.InvokeAsync("CreateClientForm", new {
Servers = servers
})
</div>
</div>
<!-- QR Modal -->
<component type="typeof(Modals.QRModal)" render-mode="Static" />
<!-- Delete Modal -->
@await Component.InvokeAsync("DeleteModal", new {
IsServer = false
})
<!-- Edit Modal -->
@await Component.InvokeAsync("UpdateClientModal", new {
Servers = servers
})
<!-- Sync Modal -->
@await Component.InvokeAsync("SyncUserModal")
@section Scripts {
<script src="js/wireguard.js"></script>
<script src="js/wgelements.js"></script>
<script>
document.addEventListener("DOMContentLoaded", function (event) {
fetch("/Clients/getAll")
.then((response) => response.text())
.then((data) => {
document.querySelector('table tbody').innerHTML = data;
});
});
document.querySelectorAll(".sync-btn").forEach(form => {
form.addEventListener('click', function (event) {
let Id = event.target.closest('tr').getAttribute('data-id');
document.querySelector('#SyncModal input[name="ID"]').value = Id;
});
});
document.querySelectorAll(".delete-btn").forEach(form => {
form.addEventListener('click', function (event) {
let Id = event.target.closest('tr').getAttribute('data-id');
document.querySelector('#DeleteModal input[name="Id"]').value = Id;
});
});
document.querySelectorAll(".update-btn").forEach(form => {
form.addEventListener('click', function (event) {
document.querySelector('#EditModal form').reset();
let row = event.target.closest('tr');
let Id = row.getAttribute('data-id');
let name = row.querySelector('td:nth-child(2)').innerText;
let interface = row.querySelector('td:nth-child(3)').innerText;
let address = row.querySelector('td:nth-child(4)').innerText;
let publicKey = row.querySelector('td:nth-child(5)').innerText;
document.querySelector('#EditModal input[name="ID"]').value = Id;
document.querySelector('#EditModal input[name="Name"]').placeholder = name;
document.querySelector('#EditModal input[name="AllowedAddress"]').placeholder = address;
document.querySelector('#EditModal input[name="PublicKey"]').placeholder = publicKey;
let ifOption = document.querySelector('#EditModal option[value="' + interface + '"]');
if (ifOption)
ifOption.setAttribute("selected", true);
});
});
document.getElementById("QRModal").addEventListener('shown.coreui.modal', (event) => {
let Id = event.relatedTarget.closest('tr').getAttribute('data-id');
const xhttp = new XMLHttpRequest();
xhttp.onload = function () {
document.querySelector('#QRModal img.img-thumbnail').setAttribute("src", "data:image/png;base64, " + this.responseText);
}
xhttp.open("GET", "/Clients/QR?id=" + Id, true);
xhttp.send();
});
</script>
}

105
Pages/Clients.cshtml.cs Normal file
View file

@ -0,0 +1,105 @@
using AutoMapper;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using MTWireGuard.Models;
using MTWireGuard.Models.Mikrotik;
using MTWireGuard.Models.Requests;
using MTWireGuard.Models.Responses;
using MTWireGuard.Repositories;
using Newtonsoft.Json;
using QRCoder;
using System.Drawing;
using System.Security.Cryptography.X509Certificates;
namespace MTWireGuard.Pages
{
public class ClientsModel : PageModel
{
private readonly IMikrotikRepository API;
private readonly IMapper mapper;
public ClientsModel(IMikrotikRepository mikrotik, IMapper mapper)
{
API = mikrotik;
this.mapper = mapper;
}
public async Task OnGetAsync()
{
var servers = await API.GetServersAsync();
ViewData["servers"] = servers;
}
public async Task<PartialViewResult> OnGetGetAll()
{
var users = await API.GetUsersAsync();
return Partial("_ClientsTable", users);
}
public async Task<IActionResult> OnGetQRAsync(int id)
{
string config = await API.GetQRCodeBase64(id);
return Content(config);
}
public async Task<FileResult> OnGetDownloadTunnelAsync(int id)
{
string config = await API.GetUserTunnelConfig(id);
byte[] bytesInStream = System.Text.Encoding.UTF8.GetBytes(config);
var user = await API.GetUser(id);
string filename = string.IsNullOrWhiteSpace(user.Name) ? user.Interface : user.Name;
Response.Headers.Add("content-disposition", $"attachment; filename={filename}.conf");
return File(bytesInStream, "text/plain");
}
public async Task<IActionResult> OnPostCreateAsync(CreateClientRequest request)
{
var model = mapper.Map<UserCreateModel>(request);
var make = await API.CreateUser(model);
string status = make.Code == "200" ? "success" : "danger";
string title = make.Code == "200" ? make.Title : $"[{make.Code}] {make.Title}";
return new ToastResult(title, make.Description, status);
}
public async Task<IActionResult> OnPostDelete(DeleteRequest request)
{
var delete = await API.DeleteUser(request.Id);
string status = delete.Code == "200" ? "success" : "danger";
string title = delete.Code == "200" ? delete.Title : $"[{delete.Code}] {delete.Title}";
return new ToastResult(title, delete.Description, status);
}
public async Task<IActionResult> OnPostUpdate(UpdateClientRequest request)
{
var model = mapper.Map<UserUpdateModel>(request);
var update = await API.UpdateUser(model);
string status = update.Code == "200" ? "success" : "danger";
string title = update.Code == "200" ? update.Title : $"[{update.Code}] {update.Title}";
return new ToastResult(title, update.Description, status);
}
public async Task<IActionResult> OnPostSyncAsync(SyncUserRequest request)
{
var model = mapper.Map<UserSyncModel>(request);
var update = await API.SyncUser(model);
string status = update.Code == "200" ? "success" : "danger";
string title = update.Code == "200" ? update.Title : $"[{update.Code}] {update.Title}";
return new ToastResult(title, update.Description, status);
}
public async Task<IActionResult> OnGetEnableAsync(ChangeStateRequest request)
{
CreationResult result;
if (!request.Enabled)
result = await API.EnableUser(request.Id);
else
result = await API.DisableUser(request.Id);
string status = result.Code == "200" ? "success" : "danger";
string title = result.Code == "200" ? result.Title : $"[{result.Code}] {result.Title}";
return new ToastResult(title, result.Description, status);
}
}
}

View file

@ -0,0 +1,89 @@
@using MTWireGuard.Models.Requests
@model CreateClientRequest
@{
List<Models.Mikrotik.WGServerViewModel> Servers = ViewData["Servers"] as List<Models.Mikrotik.WGServerViewModel>;
}
<div class="card mb-4">
<div class="card-header">Quick add peer</div>
<div class="card-body">
<form asp-page="Clients" asp-page-handler="Create" method="POST">
<div class="mb-3 row">
<label for="WGPName" class="col-sm-3 col-form-label user-select-none">Name</label>
<div class="col-sm-9">
<input type="text" class="form-control" id="WGPName" name="Name">
</div>
</div>
<div class="mb-3 row">
<label for="WGPInterface" class="col-sm-3 col-form-label user-select-none">Interface</label>
<div class="col-sm-9">
<select class="form-select" name="Interface" aria-label="Default select example">
@foreach (var server in Servers)
{
<option value="@server.Name">@server.Name</option>
}
</select>
</div>
</div>
<div class="mb-3 row">
<label for="WGPPrivKey" class="col-sm-3 col-form-label user-select-none">Private Key</label>
<div class="col-sm-9">
<div class="input-group">
<input type="text" class="form-control" id="WGPPrivKey" name="PrivateKey" aria-label="Generate" aria-describedby="PKeygenBTN" autocomplete="off">
<button type="button" class="input-group-text" id="PKeygenBTN" style="cursor: pointer;" onclick="generateKeys(this)">
<svg class="icon icon-sm text-dark my-1">
<use xlink:href="vendors/coreui/icons/svg/free.svg#cil-reload"></use>
</svg>
</button>
</div>
</div>
</div>
<div class="mb-3 row">
<label for="WGPPubKey" class="col-sm-3 col-form-label user-select-none">Public Key</label>
<div class="col-sm-9">
<input type="text" class="form-control" id="WGPPubKey" name="PublicKey" autocomplete="off">
</div>
</div>
<div class="mb-3 row">
<label for="WGPPSKey" class="col-sm-3 col-form-label user-select-none">Preshared Key</label>
<div class="col-sm-9">
<input type="text" class="form-control" id="WGPPSKey" name="PresharedKey" autocomplete="off">
</div>
</div>
<div class="mb-3 row">
<label for="WGPAddress" class="col-sm-3 col-form-label user-select-none">Allowed Address</label>
<div class="col-sm-9">
<input class="form-control" id="WGPAddress" asp-for="AllowedAddress">
</div>
</div>
<div class="mb-3 row">
<label for="WGPEndpoint" class="col-sm-3 col-form-label user-select-none">Endpoint</label>
<div class="col-sm-9">
<input type="text" class="form-control" id="WGPEndpoint" name="Endpoint">
</div>
</div>
<div class="mb-3 row">
<label for="WGPPort" class="col-sm-3 col-form-label user-select-none">Endpoint Port</label>
<div class="col-sm-9">
<input type="number" class="form-control" id="WGPPort" name="EndpointPort">
</div>
</div>
<div class="mb-3 row">
<label for="WGPKeepAlive" class="col-sm-3 col-form-label user-select-none">Persistent Keepalive</label>
<div class="col-sm-9">
<input type="text" class="form-control" id="WGPKeepAlive" name="KeepAlive" placeholder="--:--:--">
</div>
</div>
<div class="mb-5 row">
<label for="WGSEnabled" class="col-sm-3 form-check-label user-select-none">Enabled</label>
<div class="col-sm-9">
<input type="checkbox" class="form-check-input" id="WGSEnabled" asp-for="Enabled" checked>
</div>
</div>
<div class="col-12">
<button class="btn btn-primary" type="submit">Create</button>
<button class="btn btn-danger" type="reset">Clear</button>
</div>
@Html.AntiForgeryToken()
</form>
</div>
</div>

View file

@ -0,0 +1,12 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace MTWireGuard.Pages.Components.CreateClientForm
{
public class CreateClientFormModel : PageModel
{
public void OnGet()
{
}
}
}

View file

@ -0,0 +1,64 @@
@using MTWireGuard.Models.Requests
@model CreateServerRequest
<div class="card mb-4">
<div class="card-header">Quick add server</div>
<div class="card-body">
<form asp-page="Servers" asp-page-handler="Create" method="POST">
<div class="mb-3 row">
<label for="WGSName" class="col-sm-3 col-form-label user-select-none">Name</label>
<div class="col-sm-9">
<input type="text" class="form-control" id="WGSName" name="Name" placeholder="wireguard1" required>
</div>
</div>
<div class="mb-3 row">
<label for="WGSPort" class="col-sm-3 col-form-label user-select-none">Listening Port</label>
<div class="col-sm-9">
<input type="number" class="form-control" id="WGSPort" name="Port" placeholder="13231" min="1" max="65535">
</div>
</div>
<div class="mb-3 row">
<label for="WGSMTU" class="col-sm-3 col-form-label user-select-none">MTU</label>
<div class="col-sm-9">
<input type="number" class="form-control" id="WGSMTU" name="MTU" placeholder="1420" min="64" max="65535">
</div>
</div>
<div class="mb-3 row">
<label for="WGSPrivKey" class="col-sm-3 col-form-label user-select-none">Private Key</label>
<div class="col-sm-9">
<div class="input-group">
<input type="text" class="form-control" id="WGSPrivKey" name="PrivateKey" aria-label="Generate" aria-describedby="SKeygenBTN" autocomplete="off" required>
<button type="button" class="input-group-text" id="SKeygenBTN" style="cursor: pointer;" onclick="generateKeys(this)">
<svg class="icon icon-sm text-dark my-1">
<use xlink:href="vendors/coreui/icons/svg/free.svg#cil-reload"></use>
</svg>
</button>
</div>
</div>
</div>
<div class="mb-3 row">
<label for="WGSPubKey" class="col-sm-3 col-form-label user-select-none">Public Key</label>
<div class="col-sm-9">
<div class="input-group">
<input type="text" class="form-control" id="WGSPubKey" aria-label="Generate" aria-describedby="SUnlockBTN" autocomplete="off" disabled>
<button type="button" class="input-group-text" id="SUnlockBTN" style="cursor: pointer;" onclick="changeIcon(this)">
<svg class="icon icon-sm text-dark my-1">
<use xlink:href="vendors/coreui/icons/svg/free.svg#cil-lock-unlocked"></use>
</svg>
</button>
</div>
</div>
</div>
<div class="mb-5 row">
<label for="WGSEnabled" class="col-sm-3 form-check-label user-select-none">Enabled</label>
<div class="col-sm-9">
<input type="checkbox" class="form-check-input" id="WGSEnabled" asp-for="Enabled" checked>
</div>
</div>
<div class="col-12">
<button class="btn btn-primary" type="submit">Create</button>
<button class="btn btn-danger" type="reset">Clear</button>
</div>
@Html.AntiForgeryToken()
</form>
</div>
</div>

View file

@ -0,0 +1,12 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace MTWireGuard.Pages.Components.CreateServerForm
{
public class CreateServerFormModel : PageModel
{
public void OnGet()
{
}
}
}

View file

@ -0,0 +1,31 @@
@using MTWireGuard.Models.Requests
@model DeleteRequest
@{
bool IsServer = (bool)ViewData["IsServer"];
}
<div class="modal fade" id="DeleteModal" tabindex="-1" aria-labelledby="DeleteModalLabel" aria-hidden="true">
<div class="modal-dialog">
@{
string FormAction = IsServer ? "Servers/Delete" : "Clients/Delete";
string Type = IsServer ? "server interface" : "client account";
}
<form class="modal-content" action="@FormAction" method="POST">
<div class="modal-header">
<h5 class="modal-title" id="DeleteModalLabel">Are you sure ?</h5>
<button type="button" class="btn-close" data-coreui-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<p>
Are you sure to delete this @Type ?<br />
This can't be undone!
</p>
<input type="hidden" asp-for="Id" />
</div>
<div class="modal-footer">
<button type="reset" class="btn btn-secondary" data-coreui-dismiss="modal">Close</button>
<button type="submit" class="btn btn-danger">Delete</button>
</div>
@Html.AntiForgeryToken()
</form>
</div>
</div>

View file

@ -0,0 +1,12 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace MTWireGuard.Pages.Components.DeleteModal
{
public class DeleteModalModel : PageModel
{
public void OnGet()
{
}
}
}

View file

@ -0,0 +1,56 @@
@{
int LogCount = Logs.Count <= 20 ? Logs.Count : 20;
var LastLogs = Logs.TakeLast(LogCount).ToList();
}
<div class="dropdown-header bg-light dark:bg-white dark:bg-opacity-10">
<strong>You have @Logs.Count logs</strong>
</div>
@for (int i = 0; i < LogCount; i++)
{
var log = LastLogs[i];
<a class="dropdown-item" href="#">
<div class="message">
<div>
<small class="text-medium-emphasis">#@log.Id</small>
<small class="text-medium-emphasis float-end mt-1">@log.Time</small>
</div>
<div class="font-weight-bold text-medium-emphasis text-truncate">@log.Message</div>
<div class="text-truncate small">
@foreach (var topic in log.Topics)
{
switch (topic.ToLower())
{
case "system":
<span class="badge text-bg-dark mx-1">@topic</span>
break;
case "info":
<span class="badge text-bg-info mx-1">@topic</span>
break;
case "error":
case "critical":
<span class="badge text-bg-danger mx-1">@topic</span>
break;
case "account":
<span class="badge text-bg-secondary mx-1">@topic</span>
break;
case "dhcp":
case "ppp":
case "l2tp":
case "pptp":
case "sstp":
<span class="badge text-bg-light border border-secondary mx-1">@topic</span>
break;
default:
<span class="badge text-bg-light border border-secondary mx-1">@topic</span>
break;
}
}
</div>
</div>
</a>
}
@code {
[Parameter]
public List<Models.Mikrotik.LogViewModel> Logs { get; set; }
}

View file

@ -0,0 +1,50 @@
@using MTWireGuard.Models.Requests
@model SyncUserRequest
<div class="modal fade" id="SyncModal" tabindex="-1" aria-labelledby="SyncModalLabel" aria-hidden="true">
<div class="modal-dialog">
<form class="modal-content" asp-page="Clients" asp-page-handler="Sync" method="POST">
<div class="modal-header">
<h5 class="modal-title" id="SyncModalLabel">Are you sure ?</h5>
<button type="button" class="btn-close" data-coreui-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<p>
By syncing, the values of unsynced properties will be changed to the value stored in the database.<br />
Peer's Private Key is required to create client tunnel file or QR code.<br />
<strong>All keys (such as private key and public key) are only stored in your docker machine and no any information is sent to outside your network.</strong>
</p>
<div class="mb-3 row">
<label for="WGPName" class="col-sm-3 col-form-label user-select-none">Name</label>
<div class="col-sm-9">
<input type="text" class="form-control" id="WGPName" name="Name">
</div>
</div>
<div class="mb-3 row">
<label for="WGPPrivKey" class="col-sm-3 col-form-label user-select-none">Private Key</label>
<div class="col-sm-9">
<div class="input-group">
<input type="text" class="form-control" id="WGUPrivKey" name="PrivateKey" aria-label="Generate" aria-describedby="PKeygenBTN">
<button type="button" class="input-group-text" id="PKeygenBTN" style="cursor: pointer;" onclick="generateKeys(this)">
<svg class="icon icon-sm text-dark my-1">
<use xlink:href="vendors/coreui/icons/svg/free.svg#cil-reload"></use>
</svg>
</button>
</div>
</div>
</div>
<div class="mb-3 row">
<label for="WGPPubKey" class="col-sm-3 col-form-label user-select-none">Public Key</label>
<div class="col-sm-9">
<input type="text" class="form-control" id="WGUPubKey" name="PublicKey">
</div>
</div>
<input type="hidden" asp-for="ID" />
</div>
<div class="modal-footer">
<button type="reset" class="btn btn-danger" data-coreui-dismiss="modal">Close</button>
<button type="submit" class="btn btn-success">Sync</button>
</div>
@Html.AntiForgeryToken()
</form>
</div>
</div>

View file

@ -0,0 +1,12 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace MTWireGuard.Pages.Components.SyncUserModal
{
public class SyncUserModalModel : PageModel
{
public void OnGet()
{
}
}
}

View file

@ -0,0 +1,7 @@
<div class="toast-container position-fixed bottom-0 end-0 p-3" id="toastContainer">
</div>
@code {
}

View file

@ -0,0 +1,97 @@
@using MTWireGuard.Models.Requests
@model UpdateClientRequest
@{
List<Models.Mikrotik.WGServerViewModel> Servers = ViewData["Servers"] as List<Models.Mikrotik.WGServerViewModel>;
}
<div class="modal fade" id="EditModal" tabindex="-1" aria-labelledby="EditModalLabel" aria-hidden="true">
<div class="modal-dialog">
<form class="modal-content" action="Clients/Update" method="POST">
<div class="modal-header">
<h5 class="modal-title" id="EditModalLabel">Edit user details</h5>
<button type="button" class="btn-close" data-coreui-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="mb-3 row">
<label for="WGPUName" class="col-sm-3 col-form-label user-select-none">Name</label>
<div class="col-sm-9">
<input type="text" class="form-control" id="WGPUName" name="Name">
</div>
</div>
<div class="mb-3 row">
<label for="WGPUInterface" class="col-sm-3 col-form-label user-select-none">Interface</label>
<div class="col-sm-9">
<select class="form-select" id="WGPUInterface" name="Interface" aria-label="Default select example">
@foreach (var server in Servers)
{
@if (server.IsEnabled)
{
<option value="@server.Name">@server.Name</option>
}
else
{
<option class="fst-italic" value="@server.Name">@server.Name</option>
}
}
</select>
</div>
</div>
<div class="mb-3 row">
<label for="WGPUPrivKey" class="col-sm-3 col-form-label user-select-none">Private Key</label>
<div class="col-sm-9">
<div class="input-group">
<input type="text" class="form-control" id="WGPUPrivKey" name="PrivateKey" aria-label="Generate" aria-describedby="PKeygenBTN" autocomplete="off">
<button type="button" class="input-group-text" id="PKeygenBTN" style="cursor: pointer;" onclick="generateKeys(this)">
<svg class="icon icon-sm text-dark my-1">
<use xlink:href="vendors/coreui/icons/svg/free.svg#cil-reload"></use>
</svg>
</button>
</div>
</div>
</div>
<div class="mb-3 row">
<label for="WGPUPubKey" class="col-sm-3 col-form-label user-select-none">Public Key</label>
<div class="col-sm-9">
<input type="text" class="form-control" id="WGPUPubKey" name="PublicKey" autocomplete="off">
</div>
</div>
<div class="mb-3 row">
<label for="WGPUPSKey" class="col-sm-3 col-form-label user-select-none">Preshared Key</label>
<div class="col-sm-9">
<input type="text" class="form-control" id="WGPUPSKey" name="PresharedKey" autocomplete="off">
</div>
</div>
<div class="mb-3 row">
<label for="WGPUAddress" class="col-sm-3 col-form-label user-select-none">Allowed Address</label>
<div class="col-sm-9">
<input type="text" class="form-control" id="WGPUAddress" name="AllowedAddress">
</div>
</div>
<div class="mb-3 row">
<label for="WGPUEndpoint" class="col-sm-3 col-form-label user-select-none">Endpoint</label>
<div class="col-sm-9">
<input type="text" class="form-control" id="WGPUEndpoint" name="Endpoint">
</div>
</div>
<div class="mb-3 row">
<label for="WGPUPort" class="col-sm-3 col-form-label user-select-none">Endpoint Port</label>
<div class="col-sm-9">
<input type="number" class="form-control" id="WGPUPort" name="EndpointPort">
</div>
</div>
<div class="mb-3 row">
<label for="WGPUKeepAlive" class="col-sm-3 col-form-label user-select-none">Persistent Keepalive</label>
<div class="col-sm-9">
<input type="text" class="form-control" id="WGPUKeepAlive" name="KeepAlive" placeholder="Seconds">
</div>
</div>
<input type="hidden" name="ID" />
</div>
<div class="modal-footer">
<button type="reset" class="btn btn-secondary" data-coreui-dismiss="modal">Close</button>
<button type="submit" class="btn btn-primary">Update</button>
</div>
@Html.AntiForgeryToken()
</form>
</div>
</div>

View file

@ -0,0 +1,12 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace MTWireGuard.Pages.Components.UpdateClientModal
{
public class UpdateClientModalModel : PageModel
{
public void OnGet()
{
}
}
}

View file

@ -0,0 +1,65 @@
@using MTWireGuard.Models.Requests
@model UpdateServerRequest
<div class="modal fade" id="EditModal" tabindex="-1" aria-labelledby="EditModalLabel" aria-hidden="true">
<div class="modal-dialog">
<form class="modal-content" action="Servers/Update" method="POST">
<div class="modal-header">
<h5 class="modal-title" id="EditModalLabel">Edit server details</h5>
<button type="button" class="btn-close" data-coreui-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="mb-3 row">
<label for="WGSName" class="col-sm-3 col-form-label user-select-none">Name</label>
<div class="col-sm-9">
<input type="text" class="form-control" id="WGSName" name="Name" placeholder="wireguard1">
</div>
</div>
<div class="mb-3 row">
<label for="WGSPort" class="col-sm-3 col-form-label user-select-none">Listening Port</label>
<div class="col-sm-9">
<input type="number" class="form-control" id="WGSPort" name="Port" placeholder="13231" min="1" max="65535">
</div>
</div>
<div class="mb-3 row">
<label for="WGSMTU" class="col-sm-3 col-form-label user-select-none">MTU</label>
<div class="col-sm-9">
<input type="number" class="form-control" id="WGSMTU" name="MTU" placeholder="1420" min="64" max="65535">
</div>
</div>
<div class="mb-3 row">
<label for="WGSPrivKey" class="col-sm-3 col-form-label user-select-none">Private Key</label>
<div class="col-sm-9">
<div class="input-group">
<input type="text" class="form-control" id="WGSPrivKey" name="PrivateKey" aria-label="Generate" aria-describedby="SKeygenBTN" autocomplete="off">
<button type="button" class="input-group-text" id="SKeygenBTN" style="cursor: pointer;" onclick="generateKeys(this)">
<svg class="icon icon-sm text-dark my-1">
<use xlink:href="vendors/coreui/icons/svg/free.svg#cil-reload"></use>
</svg>
</button>
</div>
</div>
</div>
<div class="mb-3 row">
<label for="WGSPubKey" class="col-sm-3 col-form-label user-select-none">Public Key</label>
<div class="col-sm-9">
<div class="input-group">
<input type="text" class="form-control" id="WGSPubKey" aria-label="Generate" aria-describedby="SUnlockBTN" autocomplete="off">
<button type="button" class="input-group-text" id="SUnlockBTN" style="cursor: pointer;" onclick="changeIcon(this);">
<svg class="icon icon-sm text-dark my-1">
<use xlink:href="vendors/coreui/icons/svg/free.svg#cil-lock-unlocked"></use>
</svg>
</button>
</div>
</div>
</div>
<input type="hidden" name="ID" />
</div>
<div class="modal-footer">
<button type="reset" class="btn btn-secondary" data-coreui-dismiss="modal">Close</button>
<button type="submit" class="btn btn-primary">Update</button>
</div>
@Html.AntiForgeryToken()
</form>
</div>
</div>

View file

@ -0,0 +1,12 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace MTWireGuard.Pages.Components.UpdateServerModal
{
public class UpdateServerModalModel : PageModel
{
public void OnGet()
{
}
}
}

58
Pages/Error.cshtml Normal file
View file

@ -0,0 +1,58 @@
@page
@model ErrorModel
@{
Layout = null;
}
<!DOCTYPE html>
<html lang="en">
<head>
<base href="./">
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no">
<meta name="description" content="CoreUI - Open Source Bootstrap Admin Template">
<meta name="author" content="Łukasz Holeczek">
<meta name="keyword" content="Bootstrap,Admin,Template,Open,Source,jQuery,CSS,HTML,RWD,Dashboard">
<title>Error</title>
<link rel="apple-touch-icon" sizes="57x57" href="assets/favicon/apple-icon-57x57.png">
<link rel="apple-touch-icon" sizes="60x60" href="assets/favicon/apple-icon-60x60.png">
<link rel="apple-touch-icon" sizes="72x72" href="assets/favicon/apple-icon-72x72.png">
<link rel="apple-touch-icon" sizes="76x76" href="assets/favicon/apple-icon-76x76.png">
<link rel="apple-touch-icon" sizes="114x114" href="assets/favicon/apple-icon-114x114.png">
<link rel="apple-touch-icon" sizes="120x120" href="assets/favicon/apple-icon-120x120.png">
<link rel="apple-touch-icon" sizes="144x144" href="assets/favicon/apple-icon-144x144.png">
<link rel="apple-touch-icon" sizes="152x152" href="assets/favicon/apple-icon-152x152.png">
<link rel="apple-touch-icon" sizes="180x180" href="assets/favicon/apple-icon-180x180.png">
<link rel="icon" type="image/png" sizes="192x192" href="assets/favicon/android-icon-192x192.png">
<link rel="icon" type="image/png" sizes="32x32" href="assets/favicon/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="96x96" href="assets/favicon/favicon-96x96.png">
<link rel="icon" type="image/png" sizes="16x16" href="assets/favicon/favicon-16x16.png">
<link rel="manifest" href="assets/favicon/manifest.json">
<meta name="msapplication-TileColor" content="#ffffff">
<meta name="msapplication-TileImage" content="assets/favicon/ms-icon-144x144.png">
<meta name="theme-color" content="#ffffff">
<!-- Vendors styles-->
<link rel="stylesheet" href="vendors/simplebar/css/simplebar.css">
<link rel="stylesheet" href="css/vendors/simplebar.css">
<!-- Main styles for this application-->
<link href="css/style.css" rel="stylesheet">
</head>
<body>
<div class="bg-light min-vh-100 d-flex flex-row align-items-center">
<div class="container">
<div class="row justify-content-center">
<div class="col-md-6">
<div class="clearfix">
<h1 class="float-start display-3 me-4">500</h1>
<h4 class="pt-3">@ViewBag.Title</h4>
<p class="text-medium-emphasis">@Html.Raw(ViewBag.Message)</p>
</div>
</div>
</div>
</div>
</div>
<script src="vendors/coreui/coreui/js/coreui.bundle.min.js"></script>
<script src="vendors/simplebar/js/simplebar.min.js"></script>
</body>
</html>

32
Pages/Error.cshtml.cs Normal file
View file

@ -0,0 +1,32 @@
using Microsoft.AspNetCore.Diagnostics;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using System.Diagnostics;
namespace MTWireGuard.Pages
{
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
[IgnoreAntiforgeryToken]
public class ErrorModel : PageModel
{
public string? RequestId { get; set; }
public string? ExceptionMessage { get; set; }
public bool ShowRequestId => !string.IsNullOrEmpty(RequestId);
public ErrorModel()
{
}
public void OnGet()
{
RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier;
RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier;
var exceptionHandlerPathFeature =
HttpContext.Features.Get<IExceptionHandlerPathFeature>();
ViewData["Title"] = exceptionHandlerPathFeature.Error.Message;
ViewData["Message"] = exceptionHandlerPathFeature.Error.Source;
}
}
}

99
Pages/Index.cshtml Normal file
View file

@ -0,0 +1,99 @@
@page
@using MTWireGuard.Models.Mikrotik
@model IndexModel
@{
ViewData["Title"] = "Dashboard";
ViewData["Breadcrumb"] = "Home";
var users = (List<WGPeerViewModel>)ViewData["users"];
var servers = (List<WGServerViewModel>)ViewData["servers"];
var uptr = ViewData["uptr"];
var downtr = ViewData["downtr"];
}
<div class="row">
<div class="col-sm-6 col-lg-4">
<div class="card mb-4" style="--cui-card-cap-bg: #88171a">
<div class="card-header position-relative d-flex justify-content-center align-items-center">
<svg class="icon icon-3xl text-white my-4">
<use xlink:href="vendors/coreui/icons/svg/brand.svg#cib-wireguard"></use>
</svg>
<div class="chart-wrapper position-absolute top-0 start-0 w-100 h-100">
<canvas id="social-box-chart-1" height="90"></canvas>
</div>
</div>
<div class="card-body row text-center">
<div class="col">
<div class="fs-5 fw-semibold">@servers.Count</div>
<div class="text-uppercase text-medium-emphasis small">Servers</div>
</div>
<div class="vr"></div>
<div class="col">
<div class="fs-5 fw-semibold">@servers.Where(x => x.Running).Count()</div>
<div class="text-uppercase text-medium-emphasis small">Active</div>
</div>
</div>
</div>
</div>
<div class="col-sm-6 col-lg-4">
<div class="card mb-4" style="--cui-card-cap-bg: #0d6efd">
<div class="card-header position-relative d-flex justify-content-center align-items-center">
<svg class="icon icon-3xl text-white my-4">
<use xlink:href="vendors/coreui/icons/svg/free.svg#cil-lan"></use>
</svg>
<div class="chart-wrapper position-absolute top-0 start-0 w-100 h-100">
<canvas id="social-box-chart-1" height="90"></canvas>
</div>
</div>
<div class="card-body row text-center">
<div class="col">
<div class="fs-5 fw-semibold">@users.Count</div>
<div class="text-uppercase text-medium-emphasis small">Users</div>
</div>
<div class="vr"></div>
<div class="col">
<div class="fs-5 fw-semibold">@users.Where(x => !x.CurrentAddress.EndsWith(":0")).Count()</div>
<div class="text-uppercase text-medium-emphasis small">Online</div>
</div>
</div>
</div>
</div>
<div class="col-sm-6 col-lg-4">
<div class="card mb-4" style="--cui-card-cap-bg: #0dcaf0">
<div class="card-header position-relative d-flex justify-content-center align-items-center">
<svg class="icon icon-3xl text-white my-4">
<use xlink:href="vendors/coreui/icons/svg/free.svg#cil-graph"></use>
</svg>
<div class="chart-wrapper position-absolute top-0 start-0 w-100 h-100">
<canvas id="social-box-chart-1" height="90"></canvas>
</div>
</div>
<div class="card-body row text-center">
<div class="col">
<div class="fs-5 fw-semibold">@uptr</div>
<div class="text-uppercase text-medium-emphasis small">Upload</div>
</div>
<div class="vr"></div>
<div class="col">
<div class="fs-5 fw-semibold">@downtr</div>
<div class="text-uppercase text-medium-emphasis small">Download</div>
</div>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-sm-12 col-lg-6">
@await Component.InvokeAsync("CreateServerForm")
</div>
<div class="col-sm-12 col-lg-6">
@await Component.InvokeAsync("CreateClientForm", new {
Servers = servers
})
</div>
</div>
@section Scripts {
<script src="vendors/coreui/utils/js/coreui-utils.js"></script>
<script src="js/wireguard.js"></script>
<script src="js/wgelements.js"></script>
}

42
Pages/Index.cshtml.cs Normal file
View file

@ -0,0 +1,42 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using MTWireGuard.Repositories;
using Newtonsoft.Json;
using System.Net;
using System.Text;
namespace MTWireGuard.Pages
{
[Authorize]
public class IndexModel : PageModel
{
private readonly ILogger<IndexModel> _logger;
private readonly IMikrotikRepository API;
public IndexModel(ILogger<IndexModel> logger, IMikrotikRepository mikrotik)
{
_logger = logger;
API = mikrotik;
}
public async Task OnGetAsync()
{
var users = await API.GetUsersAsync();
var servers = await API.GetServersAsync();
var traffics = await API.GetServersTraffic();
var wgTraffics = traffics.Where(s => s.Type == "wg");
long up = 0;
long down = 0;
foreach (var item in wgTraffics)
{
up += item.UploadBytes;
down += item.DownloadBytes;
}
ViewData["users"] = users;
ViewData["servers"] = servers;
ViewData["uptr"] = Helper.ConvertByteSize(up, 2);
ViewData["downtr"] = Helper.ConvertByteSize(down, 2);
}
}
}

88
Pages/Login.cshtml Normal file
View file

@ -0,0 +1,88 @@
@page
@model MTWireGuard.Pages.LoginModel
@{
Layout = null;
bool invalid = false;
if (ViewData["Invalid"] != null) invalid = (bool)ViewData["Invalid"];
string invalidClass = invalid ? "is-invalid" : "", username = "";
if (ViewData["Username"] != null) username = ViewData["Username"].ToString();
}
<!DOCTYPE html>
<html lang="en">
<head>
<base href="./">
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no">
<meta name="description" content="CoreUI - Open Source Bootstrap Admin Template">
<meta name="author" content="Łukasz Holeczek">
<meta name="keyword" content="Bootstrap,Admin,Template,Open,Source,jQuery,CSS,HTML,RWD,Dashboard">
<title>Login</title>
<link rel="apple-touch-icon" sizes="57x57" href="assets/favicon/apple-icon-57x57.png">
<link rel="apple-touch-icon" sizes="60x60" href="assets/favicon/apple-icon-60x60.png">
<link rel="apple-touch-icon" sizes="72x72" href="assets/favicon/apple-icon-72x72.png">
<link rel="apple-touch-icon" sizes="76x76" href="assets/favicon/apple-icon-76x76.png">
<link rel="apple-touch-icon" sizes="114x114" href="assets/favicon/apple-icon-114x114.png">
<link rel="apple-touch-icon" sizes="120x120" href="assets/favicon/apple-icon-120x120.png">
<link rel="apple-touch-icon" sizes="144x144" href="assets/favicon/apple-icon-144x144.png">
<link rel="apple-touch-icon" sizes="152x152" href="assets/favicon/apple-icon-152x152.png">
<link rel="apple-touch-icon" sizes="180x180" href="assets/favicon/apple-icon-180x180.png">
<link rel="icon" type="image/png" sizes="192x192" href="assets/favicon/android-icon-192x192.png">
<link rel="icon" type="image/png" sizes="32x32" href="assets/favicon/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="96x96" href="assets/favicon/favicon-96x96.png">
<link rel="icon" type="image/png" sizes="16x16" href="assets/favicon/favicon-16x16.png">
<link rel="manifest" href="assets/favicon/manifest.json">
<meta name="msapplication-TileColor" content="#ffffff">
<meta name="msapplication-TileImage" content="assets/favicon/ms-icon-144x144.png">
<meta name="theme-color" content="#ffffff">
<!-- Vendors styles-->
<link rel="stylesheet" href="vendors/simplebar/css/simplebar.css">
<link rel="stylesheet" href="css/vendors/simplebar.css">
<!-- Main styles for this application-->
<link href="css/style.css" rel="stylesheet">
</head>
<body>
<div class="bg-light min-vh-100 d-flex flex-row align-items-center">
<div class="container">
<div class="row justify-content-center">
<div class="col-lg-8">
<div class="card-group d-block d-md-flex row">
<div class="card p-4 mb-0">
<form class="card-body" method="POST">
<h1>Login</h1>
<p class="text-medium-emphasis mb-4">Sign In to your router</p>
<div class="input-group mb-3"><span class="input-group-text">
<svg class="icon">
<use xlink:href="vendors/coreui/icons/svg/free.svg#cil-user"></use>
</svg></span>
<input type="text" name="username" class="form-control @invalidClass" placeholder="Username" value="@username">
</div>
<div class="input-group mb-4"><span class="input-group-text">
<svg class="icon">
<use xlink:href="vendors/coreui/icons/svg/free.svg#cil-lock-locked"></use>
</svg></span>
<input type="password" name="password" class="form-control @invalidClass" placeholder="Password">
@if (invalid == true) {
<div class="invalid-feedback">
Invalid username or password!
</div>
}
</div>
<div class="row">
<div class="col-6">
<button class="btn btn-primary px-4" type="submit">Login</button>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- CoreUI and necessary plugins-->
<script src="vendors/coreui/coreui/js/coreui.bundle.min.js"></script>
<script src="vendors/simplebar/js/simplebar.min.js"></script>
</body>
</html>

82
Pages/Login.cshtml.cs Normal file
View file

@ -0,0 +1,82 @@
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using System.Net;
using System.Security.Claims;
using System.Text;
namespace MTWireGuard.Pages
{
[IgnoreAntiforgeryToken(Order = 1001)]
public class LoginModel : PageModel
{
public IActionResult OnGet()
{
if (HttpContext.User.Identity.IsAuthenticated)
{
return RedirectToPage("Index");
}
return Page();
}
public async Task<IActionResult> OnPostAsync(string username, string password)
{
try
{
string MT_IP = Environment.GetEnvironmentVariable("MT_IP");
string MT_USER = Environment.GetEnvironmentVariable("MT_USER");
string MT_PASS = Environment.GetEnvironmentVariable("MT_PASS");
if (username == MT_USER && password == MT_PASS)
{
HttpClientHandler handler = new()
{
ServerCertificateCustomValidationCallback = delegate { return true; }
};
using HttpClient httpClient = new(handler);
using var request = new HttpRequestMessage(new HttpMethod("GET"), $"https://{MT_IP}/rest/");
string base64authorization = Convert.ToBase64String(Encoding.ASCII.GetBytes($"{MT_USER}:{MT_PASS}"));
request.Headers.TryAddWithoutValidation("Authorization", $"Basic {base64authorization}");
HttpResponseMessage response = await httpClient.SendAsync(request);
var resp = await response.Content.ReadAsStringAsync();
var claims = new List<Claim>
{
new Claim(ClaimTypes.Role, "Administrator"),
};
var claimsIdentity = new ClaimsIdentity(
claims, CookieAuthenticationDefaults.AuthenticationScheme);
var authProperties = new AuthenticationProperties
{
AllowRefresh = true,
IsPersistent = true
};
var claimsPrincipal = new ClaimsPrincipal(claimsIdentity);
await HttpContext.SignInAsync(
CookieAuthenticationDefaults.AuthenticationScheme,
claimsPrincipal,
authProperties);
HttpContext.User = claimsPrincipal;
return RedirectToPage("Index");
}
else
{
ViewData["Username"] = username;
ViewData["Invalid"] = true;
}
}
catch (Exception ex)
{
ViewData["body"] = ex.Message;
Console.WriteLine(ex.Message);
}
return Page();
}
}
}

4
Pages/Logout.cshtml Normal file
View file

@ -0,0 +1,4 @@
@page
@model MTWireGuard.Pages.LogoutModel
@{
}

30
Pages/Logout.cshtml.cs Normal file
View file

@ -0,0 +1,30 @@
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using MTWireGuard.Repositories;
namespace MTWireGuard.Pages
{
public class LogoutModel : PageModel
{
private readonly IMikrotikRepository API;
public LogoutModel(IMikrotikRepository mikrotik)
{
API = mikrotik;
}
public async Task<IActionResult> OnGetAsync(string returnUrl = "Login")
{
// Clear the existing external cookie
await HttpContext.SignOutAsync(
CookieAuthenticationDefaults.AuthenticationScheme);
/*
var sessionId = await MTAPIHandler.GetCurrentSessionID();
var remove = await MTAPIHandler.KillJob(sessionId);*/
var sessionId = await API.GetCurrentSessionID();
var kill = await API.KillJob(sessionId);
return RedirectToPage(returnUrl);
}
}
}

75
Pages/Logs.cshtml Normal file
View file

@ -0,0 +1,75 @@
@page
@using MTWireGuard.Models.Mikrotik
@model MTWireGuard.Pages.LogsModel
@{
ViewData["Title"] = "Event Logs";
ViewData["Breadcrumb"] = "Logs";
var Logs = (List<LogViewModel>)ViewData["Logs"];
}
<div class="row">
<div class="">
<table class="table table-bordered border-secondary logs-table">
<thead class="table-dark fw-semibold">
<tr class="align-middle">
<th class="text-center">
<svg class="icon">
<use xlink:href="vendors/coreui/icons/svg/free.svg#cil-people"></use>
</svg>
</th>
<th>Message</th>
<th>Topics</th>
<th>Time</th>
</tr>
</thead>
<tbody>
@foreach (var log in Logs)
{
<tr class="align-middle">
<td class="text-center">
<span class="text-medium-emphasis">#@log.Id</span>
</td>
<td>@log.Message</td>
<td>
@foreach (var topic in log.Topics)
{
switch (topic.ToLower())
{
case "system":
<span class="badge text-bg-dark mx-1">@topic</span>
break;
case "info":
<span class="badge text-bg-info mx-1">@topic</span>
break;
case "error":
case "critical":
<span class="badge text-bg-danger mx-1">@topic</span>
break;
case "account":
<span class="badge text-bg-secondary mx-1">@topic</span>
break;
case "dhcp":
case "ppp":
case "l2tp":
case "pptp":
case "sstp":
<span class="badge text-bg-light border border-secondary mx-1">@topic</span>
break;
default:
<span class="badge text-bg-light border border-secondary mx-1">@topic</span>
break;
}
}
</td>
<td>@log.Time</td>
</tr>
}
</tbody>
</table>
</div>
</div>
@section Scripts {
<script>
</script>
}

21
Pages/Logs.cshtml.cs Normal file
View file

@ -0,0 +1,21 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using MTWireGuard.Repositories;
namespace MTWireGuard.Pages
{
public class LogsModel : PageModel
{
private readonly IMikrotikRepository API;
public LogsModel(IMikrotikRepository mikrotik)
{
API = mikrotik;
}
public async Task OnGetAsync()
{
ViewData["Logs"] = await API.GetLogsAsync();
}
}
}

View file

@ -0,0 +1,16 @@
<div class="modal fade" id="QRModal" tabindex="-1" aria-labelledby="QRModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="QRModalLabel">Scan This</h5>
</div>
<div class="modal-body">
<img id="QRImage" src="" class="img-thumbnail" />
</div>
</div>
</div>
</div>
@code {
}

8
Pages/Privacy.cshtml Normal file
View file

@ -0,0 +1,8 @@
@page
@model PrivacyModel
@{
ViewData["Title"] = "Privacy Policy";
}
<h1>@ViewData["Title"]</h1>
<p>Use this page to detail your site's privacy policy.</p>

19
Pages/Privacy.cshtml.cs Normal file
View file

@ -0,0 +1,19 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace MTWireGuard.Pages
{
public class PrivacyModel : PageModel
{
private readonly ILogger<PrivacyModel> _logger;
public PrivacyModel(ILogger<PrivacyModel> logger)
{
_logger = logger;
}
public void OnGet()
{
}
}
}

83
Pages/Servers.cshtml Normal file
View file

@ -0,0 +1,83 @@
@page "{handler?}"
@using MTWireGuard.Models.Mikrotik
@model MTWireGuard.Pages.ServersModel
@{
ViewData["Title"] = "Servers";
ViewData["Breadcrumb"] = "Interfaces";
}
<div class="table-responsive">
<table class="table border mb-0">
<thead class="table-light fw-semibold">
<tr class="align-middle">
<th class="text-center">
<svg class="icon">
<use xlink:href="vendors/coreui/icons/svg/free.svg#cil-people"></use>
</svg>
</th>
<th>Name</th>
<th>Listen Port</th>
<th>MTU</th>
<th>Public-Key</th>
<th>Traffic</th>
<th>Status</th>
<th></th>
</tr>
</thead>
<tbody>
</tbody>
</table>
</div>
<div class="row mt-2">
<div class="col-12">
@await Component.InvokeAsync("CreateServerForm")
</div>
</div>
<!-- Delete Modal -->
@await Component.InvokeAsync("DeleteModal", new {
IsServer = true
})
<!-- Edit Modal -->
@await Component.InvokeAsync("UpdateServerModal")
@section Scripts {
<script src="js/wireguard.js"></script>
<script src="js/wgelements.js"></script>
<script>
document.addEventListener("DOMContentLoaded", function (event) {
fetch("/Servers/getAll")
.then((response) => response.text())
.then((data) => {
document.querySelector('table tbody').innerHTML = data;
});
});
document.querySelectorAll(".delete-btn").forEach(form => {
form.addEventListener('click', function (event) {
let row = event.target.closest('tr');
let Id = row.getAttribute('data-id');
document.querySelector('#DeleteModal input[name="Id"]').value = Id;
});
});
document.querySelectorAll(".update-btn").forEach(form => {
form.addEventListener('click', function (event) {
document.querySelector('#EditModal form').reset();
let row = event.target.closest('tr');
let Id = row.getAttribute('data-id');
let name = row.querySelector('td:nth-child(2)').innerText;
let port = row.querySelector('td:nth-child(3)').innerText;
let mtu = row.querySelector('td:nth-child(4)').innerText;
let publicKey = row.querySelector('td:nth-child(5)').innerText;
document.querySelector('#EditModal input[name="ID"]').value = Id;
document.querySelector('#EditModal input[name="Name"]').placeholder = name;
document.querySelector('#EditModal input[name="Port"]').placeholder = port;
document.querySelector('#EditModal input[name="MTU"]').placeholder = mtu;
document.querySelector('#EditModal input[id$="PubKey"]').placeholder = publicKey;
});
});
</script>
}

73
Pages/Servers.cshtml.cs Normal file
View file

@ -0,0 +1,73 @@
using AutoMapper;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using MTWireGuard.Models;
using MTWireGuard.Models.Mikrotik;
using MTWireGuard.Models.Requests;
using MTWireGuard.Models.Responses;
using MTWireGuard.Repositories;
using Newtonsoft.Json;
namespace MTWireGuard.Pages
{
public class ServersModel : PageModel
{
private readonly IMikrotikRepository API;
private readonly IMapper mapper;
public ServersModel(IMikrotikRepository mikrotik, IMapper mapper)
{
API = mikrotik;
this.mapper = mapper;
}
public async Task OnGetAsync()
{
}
public async Task<PartialViewResult> OnGetGetAll()
{
var servers = await API.GetServersAsync();
var traffics = await API.GetServersTraffic();
return Partial("_ServersTable", (servers, traffics));
}
public async Task<IActionResult> OnPostCreateAsync(CreateServerRequest request)
{
var model = mapper.Map<ServerCreateModel>(request);
var make = await API.CreateServer(model);
string status = make.Code == "200" ? "success" : "danger";
string title = make.Code == "200" ? make.Title : $"[{make.Code}] {make.Title}";
return new ToastResult(title, make.Description, status);
}
public async Task<IActionResult> OnPostDelete(DeleteRequest request)
{
var delete = await API.DeleteServer(request.Id);
string status = delete.Code == "200" ? "success" : "danger";
string title = delete.Code == "200" ? delete.Title : $"[{delete.Code}] {delete.Title}";
return new ToastResult(title, delete.Description, status);
}
public async Task<IActionResult> OnPostUpdate(UpdateServerRequest request)
{
var model = mapper.Map<ServerUpdateModel>(request);
var update = await API.UpdateServer(model);
string status = update.Code == "200" ? "success" : "danger";
string title = update.Code == "200" ? update.Title : $"[{update.Code}] {update.Title}";
return new ToastResult(title, update.Description, status);
}
public async Task<IActionResult> OnGetEnableAsync(ChangeStateRequest request)
{
CreationResult result;
if (!request.Enabled)
result = await API.EnableServer(request.Id);
else
result = await API.DisableServer(request.Id);
string status = result.Code == "200" ? "success" : "danger";
string title = result.Code == "200" ? result.Title : $"[{result.Code}] {result.Title}";
return new ToastResult(title, result.Description, status);
}
}
}

145
Pages/Settings.cshtml Normal file
View file

@ -0,0 +1,145 @@
@page
@using MTWireGuard.Models.Mikrotik
@model SettingsModel
@{
ViewData["Title"] = "System Settings";
ViewData["Breadcrumb"] = "Settings";
var servers = (List<WGServerViewModel>)ViewData["servers"];
var info = (MTInfoViewModel)ViewData["info"];
var HDDPercent = (info.UsedHDDBytes * 100) / info.TotalHDDBytes;
var RAMPercent = (info.UsedRAMBytes * 100) / info.TotalRAMBytes;
var name = ViewData["name"].ToString();
}
<div class="row">
<div class="col-xs-6 col-sm-4 col-lg-4">
<div class="card text-white bg-info">
<div class="card-body">
<div class="fs-4 fw-semibold">@info.FreeHDD</div>
<div>Free Disk Space</div>
<div class="progress progress-white progress-thin my-2">
<div class="progress-bar" role="progressbar" style="width: @HDDPercent%" aria-valuenow="@HDDPercent" aria-valuemin="0" aria-valuemax="100"></div>
</div><small class="text-medium-emphasis-inverse">Used @info.UsedHDD of @info.TotalHDD</small>
</div>
</div>
</div>
<div class="col-xs-6 col-sm-4 col-lg-4">
<div class="card text-white bg-warning">
<div class="card-body">
<div class="fs-4 fw-semibold">@info.FreeRAM</div>
<div>Free RAM Space</div>
<div class="progress progress-white progress-thin my-2">
<div class="progress-bar" role="progressbar" style="width: @RAMPercent%" aria-valuenow="@RAMPercent" aria-valuemin="0" aria-valuemax="100"></div>
</div><small class="text-medium-emphasis-inverse">Used @info.UsedRAM of @info.TotalRAM</small>
</div>
</div>
</div>
<div class="col-xs-6 col-sm-4 col-lg-4">
<div class="card text-white bg-danger">
<div class="card-body">
<div class="text-medium-emphasis-inverse text-end mb-2">
<svg class="icon icon-xxl">
<use xlink:href="vendors/coreui/icons/svg/free.svg#cil-speedometer"></use>
</svg>
</div>
<div class="fs-4 fw-semibold">@info.UPTime</div>
<small class="text-medium-emphasis-inverse text-uppercase fw-semibold">UPTime</small>
</div>
</div>
</div>
</div>
<div class="row my-2">
<div class="col-sm-6 col-lg-4">
<div class="card mb-4" style="--cui-card-cap-bg: #7D50B9">
<div class="card-header position-relative d-flex justify-content-center align-items-center">
<svg class="icon icon-3xl text-white my-4">
<use xlink:href="vendors/coreui/icons/svg/free.svg#cil-router"></use>
</svg>
<div class="chart-wrapper position-absolute top-0 start-0 w-100 h-100">
<canvas id="social-box-chart-1" height="90"></canvas>
</div>
</div>
<div class="card-body row text-center">
<div class="col">
<div class="fs-5 fw-semibold">@info.Architecture</div>
<div class="text-uppercase text-medium-emphasis small">Arch</div>
</div>
<div class="vr"></div>
<div class="col">
<div class="fs-5 fw-semibold">@info.Platform</div>
<div class="text-uppercase text-medium-emphasis small">Platform</div>
</div>
<div class="vr"></div>
<div class="col">
<div class="fs-5 fw-semibold">@info.BoardName</div>
<div class="text-uppercase text-medium-emphasis small">Board</div>
</div>
</div>
<div class="card-footer row bg-light px-3 py-2 m-0">
<a class="btn-block text-medium-emphasis d-flex justify-content-center align-items-center text-decoration-none col" href="#">
<span class="small fw-semibold">Identity: @name</span>
</a>
<div class="vr"></div>
<a class="btn-block text-medium-emphasis d-flex justify-content-center align-items-center text-decoration-none col" href="#">
<span class="small fw-semibold">Version: @info.Version</span>
</a>
</div>
</div>
</div>
<div class="col-sm-6 col-lg-4">
<div class="card mb-4" style="--cui-card-cap-bg: #4FA3A5">
<div class="card-header position-relative d-flex justify-content-center align-items-center">
<svg class="icon icon-3xl text-white my-4">
<use xlink:href="vendors/coreui/icons/svg/free.svg#cil-aperture"></use>
</svg>
<div class="chart-wrapper position-absolute top-0 start-0 w-100 h-100">
<canvas id="social-box-chart-1" height="90"></canvas>
</div>
</div>
<div class="card-body row text-center">
<div class="col">
<div class="fs-5 fw-semibold">@info.CPU</div>
<div class="text-uppercase text-medium-emphasis small">CPU</div>
</div>
<div class="vr"></div>
<div class="col">
<div class="fs-5 fw-semibold">@info.CPUCount</div>
<div class="text-uppercase text-medium-emphasis small">Cores</div>
</div>
<div class="vr"></div>
<div class="col">
<div class="fs-5 fw-semibold">@info.CPUFrequency</div>
<div class="text-uppercase text-medium-emphasis small">Frequency</div>
</div>
<div class="vr"></div>
<div class="col">
<div class="fs-5 fw-semibold">@info.CPULoad %</div>
<div class="text-uppercase text-medium-emphasis small">Usage</div>
</div>
</div>
</div>
</div>
<!--<div class="col-sm-6 col-lg-4">
<div class="card text-bg-dark">
<div class="card-header">
Fix MSS
</div>
<div class="card-body">
<h5 class="card-title">Fix problems loading apps like Instagram</h5>
<p class="card-text">This will fix problems with loading some applications such as Instagram by changing TCP MSS.</p>
<select class="form-select" aria-label="Default select example">
@foreach (var server in servers)
{
<option value="@server.Id">@server.Name</option>
}
</select>
<button type="button" class="btn btn-info mt-2">
<span class="cil-contrast btn-icon mr-2"></span> Fix
</button>
</div>
</div>
</div>-->
</div>
@section Scripts {
}

31
Pages/Settings.cshtml.cs Normal file
View file

@ -0,0 +1,31 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using MTWireGuard.Repositories;
namespace MTWireGuard.Pages
{
public class SettingsModel : PageModel
{
private readonly IMikrotikRepository API;
public SettingsModel(IMikrotikRepository mikrotik)
{
API = mikrotik;
}
public async Task OnGetAsync()
{
ViewData["servers"] = await API.GetServersAsync();
ViewData["info"] = await API.GetInfo();
var identity = await API.GetName();
ViewData["name"] = identity.Name;
}
public async Task<IActionResult> OnGetGetInfo()
{
var info = await API.GetInfo();
return new JsonResult(info);
}
}
}

View file

@ -0,0 +1,97 @@
@using MTWireGuard.Models.Mikrotik
@model List<WGPeerViewModel>
@foreach (var user in Model)
{
<tr class="align-middle" data-id="@user.Id">
<td class="text-center">
<span class="text-medium-emphasis">#@user.Id</span>
</td>
<td>
<div>@user.Name</div>
</td>
<td>
@if (user.Interface.StartsWith('*'))
{
<div class="text-danger fst-italic">@user.Interface</div>
}
else
{
<div>@user.Interface</div>
}
</td>
<td>
<div>@user.Address</div>
@if (!user.CurrentAddress.EndsWith(":0"))
{
<div class="small text-medium-emphasis">@user.CurrentAddress</div>
}
</td>
<td>
<div class="text-truncate">@user.PublicKey</div>
</td>
<td>
<div>
<svg class="icon">
<use xlink:href="vendors/coreui/icons/svg/free.svg#cil-data-transfer-up"></use>
</svg>
@user.Upload / @user.Download
<svg class="icon">
<use xlink:href="vendors/coreui/icons/svg/free.svg#cil-data-transfer-down"></use>
</svg>
</div>
</td>
<td>
<div class="row">
@if (!user.IsEnabled)
{
<span class="col badge rounded-pill text-bg-danger">Disabled</span>
}
else
{
<span class="col badge rounded-pill text-bg-success">Enabled</span>
}
@if (user.IsDifferent)
{
<span class="col badge rounded-pill text-bg-danger mx-md-1">Not Synced</span>
}
else
{
<span class="col badge rounded-pill text-bg-success mx-md-1">Synced</span>
}
@if (user.CurrentAddress.EndsWith(":0"))
{
<span class="col badge rounded-pill text-bg-danger">Offline</span>
}
else
{
<span class="col badge rounded-pill text-bg-success">Online</span>
}
</div>
</td>
<td>
<div class="dropdown position-static">
<button class="btn btn-transparent p-0" type="button" data-coreui-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<svg class="icon">
<use xlink:href="vendors/coreui/icons/svg/free.svg#cil-options"></use>
</svg>
</button>
<div class="dropdown-menu dropdown-menu-end">
@if (user.IsDifferent)
{
<button class="dropdown-item sync-btn" data-coreui-toggle="modal" data-coreui-target="#SyncModal">Sync</button>
}
<button class="dropdown-item" data-coreui-toggle="modal" data-coreui-target="#QRModal">QR</button>
<a class="dropdown-item" asp-page="Clients" asp-page-handler="DownloadTunnel" asp-route-id="@user.Id">Download</a>
<button class="dropdown-item update-btn" data-coreui-toggle="modal" data-coreui-target="#EditModal">Edit</button>
@{
var enabled = user.IsEnabled;
string enableORdisable = enabled ? "Disable" : "Enable";
}
<a class="dropdown-item" asp-page="Clients" asp-page-handler="Enable" asp-route-ID="@user.Id" asp-route-IsEnabled="@enabled" data-target="_self" onclick="ddBtnClick(event);">@enableORdisable</a>
<button class="dropdown-item delete-btn text-danger" data-coreui-toggle="modal" data-coreui-target="#DeleteModal">Delete</button>
</div>
</div>
</td>
</tr>
}

215
Pages/Shared/_Layout.cshtml Normal file
View file

@ -0,0 +1,215 @@
@using MTWireGuard.Repositories
@inject IMikrotikRepository API;
@{
var logs = await API.GetLogsAsync();
var info = await API.GetInfo();
var ramUsed = 100 - info.FreeRAMPercentage;
var hddUsed = 100 - info.FreeHDDPercentage;
string cpuColor, ramColor, hddColor;
if (info.CPULoad <= 25) cpuColor = "bg-info-gradient";
else if (info.CPULoad <= 75) cpuColor = "bg-warning-gradient";
else cpuColor = "bg-danger-gradient";
if (hddUsed <= 25) hddColor = "bg-info-gradient";
else if (hddUsed <= 75) hddColor = "bg-warning-gradient";
else hddColor = "bg-danger-gradient";
if (ramUsed <= 25) ramColor = "bg-info-gradient";
else if (ramUsed<= 75) ramColor = "bg-warning-gradient";
else ramColor = "bg-danger-gradient";
}
<!DOCTYPE html>
<html lang="en">
<head>
<base href="./">
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no">
<meta name="description" content="Mikrotik Wireguard management panel">
<meta name="author" content="Kazem Ma79">
<title>@ViewData["Title"]</title>
<link rel="apple-touch-icon" sizes="180x180" href="assets/favicon/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="192x192" href="assets/favicon/android-icon-36x36.png">
<link rel="icon" type="image/png" sizes="192x192" href="assets/favicon/android-icon-48x48.png">
<link rel="icon" type="image/png" sizes="192x192" href="assets/favicon/android-icon-72x72.png">
<link rel="icon" type="image/png" sizes="192x192" href="assets/favicon/android-icon-96x96.png">
<link rel="icon" type="image/png" sizes="192x192" href="assets/favicon/android-icon-144x144.png">
<link rel="icon" type="image/png" sizes="192x192" href="assets/favicon/android-icon-192x192.png">
<link rel="icon" type="image/png" sizes="192x192" href="assets/favicon/android-icon-256x256.png">
<link rel="icon" type="image/png" sizes="192x192" href="assets/favicon/android-icon-384x384.png">
<link rel="icon" type="image/png" sizes="192x192" href="assets/favicon/android-icon-512x512.png">
<link rel="icon" type="image/png" sizes="16x16" href="assets/favicon/favicon-16x16.png">
<link rel="icon" type="image/png" sizes="32x32" href="assets/favicon/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="96x96" href="assets/favicon/favicon-96x96.png">
<link rel="manifest" href="assets/favicon/manifest.json">
<meta name="msapplication-TileColor" content="#ffffff">
<meta name="msapplication-TileImage" content="assets/favicon/mstile-150x150.png">
<meta name="theme-color" content="#ffffff">
<!-- Simplebar styles-->
<link rel="stylesheet" href="vendors/simplebar/css/simplebar.css">
<link rel="stylesheet" href="css/vendors/simplebar.css">
<!-- Main styles-->
<link href="css/style.css" rel="stylesheet">
<link href="css/gradients.css" rel="stylesheet">
<link href="css/custom.css" rel="stylesheet">
<!-- Page Styles (if any)-->
@await RenderSectionAsync("Styles", required: false)
</head>
<body>
<div class="sidebar sidebar-dark sidebar-fixed" id="sidebar">
<div class="sidebar-brand d-none d-md-flex">
<svg class="sidebar-brand-full" width="118" height="46" alt="Wireguard Logo">
<image xlink:href="assets/brand/wireguard.svg" width="118" height="46" />
</svg>
<svg class="sidebar-brand-narrow" width="46" height="46" alt="Wireguard Logo">
<image xlink:href="assets/brand/wireguard-mini.svg" width="46" height="46"></image>
</svg>
</div>
<ul class="sidebar-nav" data-coreui="navigation" data-simplebar="">
<li class="nav-item">
<a class="nav-link" asp-page="/">
<svg class="nav-icon">
<use xlink:href="vendors/coreui/icons/svg/free.svg#cil-speedometer"></use>
</svg> Dashboard
</a>
</li>
<li class="nav-title">Wireguard</li>
<li class="nav-item">
<a class="nav-link" asp-page="/Servers">
<svg class="nav-icon">
<use xlink:href="vendors/coreui/icons/svg/free.svg#cil-3d"></use>
</svg> Servers
</a>
</li>
<li class="nav-item">
<a class="nav-link" asp-page="/Clients">
<svg class="nav-icon">
<use xlink:href="vendors/coreui/icons/svg/free.svg#cil-devices"></use>
</svg> Clients
</a>
</li>
<li class="nav-title">System</li>
<li class="nav-item">
<a class="nav-link" asp-page="/Settings">
<svg class="nav-icon">
<use xlink:href="vendors/coreui/icons/svg/free.svg#cil-apps"></use>
</svg> Settings
</a>
</li>
<li class="nav-title mt-auto">System Utilization</li>
<li class="nav-item px-3 d-narrow-none">
<div class="text-uppercase mb-1"><small><b>CPU Usage</b></small></div>
<div class="progress progress-thin">
<div class="progress-bar @cpuColor" role="progressbar" style="width: @info.CPULoad%" aria-valuenow="@info.CPULoad" aria-valuemin="0" aria-valuemax="100"></div>
</div><small class="text-medium-emphasis-inverse">@info.CPULoad% is using.</small>
</li>
<li class="nav-item px-3 d-narrow-none">
<div class="text-uppercase mb-1"><small><b>Memory Usage</b></small></div>
<div class="progress progress-thin">
<div class="progress-bar @ramColor" role="progressbar" style="width: @ramUsed%" aria-valuenow="@ramUsed" aria-valuemin="0" aria-valuemax="100"></div>
</div><small class="text-medium-emphasis-inverse">@info.UsedRAM/@info.TotalRAM</small>
</li>
<li class="nav-item px-3 mb-3 d-narrow-none">
<div class="text-uppercase mb-1"><small><b>Disk Usage</b></small></div>
<div class="progress progress-thin">
<div class="progress-bar @hddColor" role="progressbar" style="width: @hddUsed%" aria-valuenow="@hddUsed" aria-valuemin="0" aria-valuemax="100"></div>
</div><small class="text-medium-emphasis-inverse">@info.UsedHDD/@info.TotalHDD</small>
</li>
</ul>
<button class="sidebar-toggler" type="button" data-coreui-toggle="unfoldable"></button>
</div>
<div class="wrapper d-flex flex-column min-vh-100 bg-light">
<header class="header header-sticky mb-4">
<div class="container-fluid">
<button class="header-toggler px-md-0 me-md-3" type="button" onclick="coreui.Sidebar.getInstance(document.querySelector('#sidebar')).toggle()">
<svg class="icon icon-lg">
<use xlink:href="vendors/coreui/icons/svg/free.svg#cil-menu"></use>
</svg>
</button>
<a class="header-brand d-md-none" href="#">
<svg width="118" height="46" alt="Wireguard Logo">
<use xlink:href="assets/brand/wireguard.svg"></use>
</svg>
</a>
<ul class="header-nav d-none d-md-flex">
<li class="nav-item">
<a class="nav-link" asp-page="Index">Dashboard</a>
</li>
<li class="nav-item">
<a class="nav-link" asp-page="Clients">Users</a>
</li>
<li class="nav-item">
<a class="nav-link" asp-page="Settings">Settings</a>
</li>
</ul>
<ul class="header-nav ms-auto">
<li class="nav-item dropdown d-md-down-none">
<a class="nav-link" data-coreui-toggle="dropdown" href="#" role="button" aria-haspopup="true" aria-expanded="false">
<span class="d-inline-block my-1 mx-2 position-relative">
<svg class="icon icon-lg">
<use xlink:href="vendors/coreui/icons/svg/free.svg#cil-list-rich"></use>
</svg>
<span class="position-absolute top-0 start-100 translate-middle p-1 bg-danger rounded-circle">
<span class="visually-hidden">New logs</span>
</span>
</span>
</a>
<div class="dropdown-menu dropdown-menu-end dropdown-menu-lg py-0" style="max-width: 30vw; max-height: 70vh; overflow-y: auto;">
<component type="typeof(Components.LogsView)" render-mode="Static" param-logs="@logs" />
<a class="dropdown-item text-center border-top" asp-page="Logs"><strong>View all messages</strong></a>
</div>
</li>
</ul>
<ul class="header-nav ms-3">
<li class="nav-item dropdown">
<a class="nav-link py-0" data-coreui-toggle="dropdown" href="#" role="button" aria-haspopup="true" aria-expanded="false">
<div class="avatar avatar-md"><img class="avatar-img" src="assets/img/avatars/admin.png" alt="kazem_ma79@yahoo.com"></div>
</a>
<div class="dropdown-menu dropdown-menu-end py-0 mb-0">
<div class="dropdown-header bg-light py-2">
<div class="fw-semibold">Account</div>
</div>
<a class="dropdown-item" asp-page="Logout">
<svg class="icon">
<use xlink:href="vendors/coreui/icons/svg/free.svg#cil-account-logout"></use>
</svg> Logout
</a>
</div>
</li>
</ul>
</div>
<div class="header-divider"></div>
<div class="container-fluid">
<nav aria-label="breadcrumb">
<ol class="breadcrumb my-0 ms-2">
<li class="breadcrumb-item">
<span>Dashboard</span>
</li>
<li class="breadcrumb-item active">
<span>@ViewData["Breadcrumb"]</span>
</li>
</ol>
</nav>
</div>
</header>
<div class="body flex-grow-1 px-3">
<div class="container-lg">
@RenderBody()
</div>
</div>
<footer class="footer">
<div><a href="https://mtwg.techgarage.ir/">Mikrotik WireGuard Dashboard</a> © 2023 Techgarage.</div>
<div class="ms-md-auto">Powered by <a href="https://coreui.io/">CoreUI</a></div>
</footer>
</div>
@Html.AntiForgeryToken()
<component type="typeof(Components.ToastContainer)" render-mode="Static" />
<!-- CoreUI and necessary plugins-->
<script src="vendors/coreui/coreui/js/coreui.bundle.min.js"></script>
<script src="vendors/simplebar/js/simplebar.min.js"></script>
<script src="js/script.js"></script>
@await RenderSectionAsync("Scripts", required: false)
</body>
</html>

View file

@ -0,0 +1,48 @@
/* Please see documentation at https://docs.microsoft.com/aspnet/core/client-side/bundling-and-minification
for details on configuring this project to bundle and minify static web assets. */
a.navbar-brand {
white-space: normal;
text-align: center;
word-break: break-all;
}
a {
color: #0077cc;
}
.btn-primary {
color: #fff;
background-color: #1b6ec2;
border-color: #1861ac;
}
.nav-pills .nav-link.active, .nav-pills .show > .nav-link {
color: #fff;
background-color: #1b6ec2;
border-color: #1861ac;
}
.border-top {
border-top: 1px solid #e5e5e5;
}
.border-bottom {
border-bottom: 1px solid #e5e5e5;
}
.box-shadow {
box-shadow: 0 .25rem .75rem rgba(0, 0, 0, .05);
}
button.accept-policy {
font-size: 1rem;
line-height: inherit;
}
.footer {
position: absolute;
bottom: 0;
width: 100%;
white-space: nowrap;
line-height: 60px;
}

View file

@ -0,0 +1,79 @@
@using MTWireGuard.Models.Mikrotik
@model (List<WGServerViewModel>, List<ServerTrafficViewModel>)
@{
var servers = Model.Item1;
var traffic = Model.Item2;
}
@foreach (var server in servers)
{
<tr class="align-middle" data-id="@server.Id">
<td class="text-center">
<span class="text-medium-emphasis">#@server.Id</span>
</td>
<td>
<div>@server.Name</div>
</td>
<td>
<div>@server.ListenPort</div>
</td>
<td>
<div>@server.MTU</div>
</td>
<td>
<div>@server.PublicKey</div>
</td>
<td>
@{
var enabled = server.IsEnabled;
string enableORdisable = enabled ? "Disable" : "Enable";
var currentServerTraffic = traffic.Find(s => s.Name == server.Name);
var up = currentServerTraffic.Upload;
var down = currentServerTraffic.Download;
}
<div>
<svg class="icon">
<use xlink:href="vendors/coreui/icons/svg/free.svg#cil-data-transfer-up"></use>
</svg>
@up / @down
<svg class="icon">
<use xlink:href="vendors/coreui/icons/svg/free.svg#cil-data-transfer-down"></use>
</svg>
</div>
</td>
<td>
@switch (enabled)
{
case true:
<span class="badge rounded-pill text-bg-success">Enabled</span>
break;
case false:
<span class="badge rounded-pill text-bg-danger">Disabled</span>
break;
}
@switch (server.Running)
{
case true:
<span class="badge rounded-pill text-bg-success">Running</span>
break;
case false:
<span class="badge rounded-pill text-bg-secondary">Not Running</span>
break;
}
</td>
<td>
<div class="dropdown position-static">
<button class="btn btn-transparent p-0" type="button" data-coreui-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<svg class="icon">
<use xlink:href="vendors/coreui/icons/svg/free.svg#cil-options"></use>
</svg>
</button>
<div class="dropdown-menu dropdown-menu-end">
<a class="dropdown-item" asp-page="Servers" asp-page-handler="Enable" asp-route-ID="@server.Id" asp-route-IsEnabled="@enabled" data-target="_self" onclick="ddBtnClick(event);">@enableORdisable</a>
<button class="dropdown-item update-btn" data-coreui-toggle="modal" data-coreui-target="#EditModal">Edit</button>
<button class="dropdown-item delete-btn text-danger" data-coreui-toggle="modal" data-coreui-target="#DeleteModal" data-id="@server.Id">Delete</button>
</div>
</div>
</td>
</tr>
}

View file

@ -0,0 +1,3 @@
@using MTWireGuard
@namespace MTWireGuard.Pages
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

3
Pages/_ViewStart.cshtml Normal file
View file

@ -0,0 +1,3 @@
@{
Layout = "_Layout";
}

118
Program.cs Normal file
View file

@ -0,0 +1,118 @@
using AutoMapper;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
using MTWireGuard;
using MTWireGuard.Mapper;
using MTWireGuard.Middlewares;
using MTWireGuard.Repositories;
using MTWireGuard.Services;
using System.Diagnostics;
#region Error Handling
AppDomain.CurrentDomain.UnhandledException += new UnhandledExceptionEventHandler(UnhandledExceptionHandler);
static void UnhandledExceptionHandler(object sender, UnhandledExceptionEventArgs e)
{
Exception ex = e.ExceptionObject as Exception;
if (e.IsTerminating)
Debug.WriteLine($"[+] {ex.Message}");
else
Debug.WriteLine($"[-] {ex.Message}");
throw ex;
}
#endregion
var builder = WebApplication.CreateBuilder(args);
using DBContext context = new();
// Add services to the container.
builder.Services.AddRazorPages().AddRazorPagesOptions(o =>
{
//o.Conventions.ConfigureFilter(new IgnoreAntiforgeryTokenAttribute());
o.Conventions.AuthorizeFolder("/");
o.Conventions.AllowAnonymousToPage("/Login");
});
builder.Services.AddAntiforgery(o =>
{
o.HeaderName = "XSRF-TOKEN";
o.FormFieldName = "XSRF-Validation-Token";
o.Cookie.Name = "XSRF-Validation";
});
// Auto Mapper Configurations
var mapperConfig = new MapperConfiguration(mc =>
{
mc.AddProfile(new MappingProfile());
mc.AddProfile(new PeerMapping(context));
mc.AddProfile(new ServerMapping());
});
IMapper mapper = mapperConfig.CreateMapper();
builder.Services.AddSingleton(mapper);
builder.Services.AddSingleton(context);
builder.Services.AddSingleton<IMikrotikRepository, MTAPI>();
//builder.Services.AddScoped<AntiForgeryMiddleware>();
builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme).AddCookie(options =>
{
options.ExpireTimeSpan = TimeSpan.FromMinutes(15);
options.SlidingExpiration = true;
options.LoginPath = "/Login";
options.AccessDeniedPath = "/Forbidden";
options.Cookie.Name = "Authentication";
});
builder.Services.AddAuthorization(configure =>
{
configure.AddPolicy("Administrator", authBuilder =>
{
authBuilder.RequireRole("Administrator");
});
});
builder.Services.ConfigureApplicationCookie(configure =>
{
configure.Cookie.Name = "MTWireguard";
});
var app = builder.Build();
// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
context.Database.EnsureCreated();
app.UseHttpsRedirection();
if (!app.Environment.IsDevelopment())
app.UseStaticFiles();
else
app.UseStaticFiles(new StaticFileOptions()
{
OnPrepareResponse = context =>
{
context.Context.Response.Headers.Add("Cache-Control", "no-cache, no-store");
context.Context.Response.Headers.Add("Expires", "-1");
}
});
app.UseDependencyCheck();
//app.UseAntiForgery();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.MapRazorPages();
app.Run();

View file

@ -0,0 +1,46 @@
{
"profiles": {
"MTWireGuard": {
"commandName": "Project",
"launchBrowser": true,
"hotReloadEnabled": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development",
"MT_IP": "192.168.0.96",
"MT_USER": "admin",
"MT_PASS": "",
"MT_PUBLIC_IP": "192.168.0.96"
},
"dotnetRunMessages": true,
"applicationUrl": "https://localhost:7220;http://localhost:5220"
},
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"Docker": {
"commandName": "Docker",
"launchBrowser": true,
"launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}",
"publishAllPorts": true,
"useSSL": true,
"environmentVariables": {
"MT_IP": "192.168.0.96",
"MT_USER": "panel",
"MT_PASS": "",
"MT_PUBLIC_IP": "192.168.0.96"
}
}
},
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:48442",
"sslPort": 44372
}
}
}

2
README.md Normal file
View file

@ -0,0 +1,2 @@
# MTWireGuard
Documentation available at [HERE](https://mtwireguard.techgarage.ir/Documentation)

View file

@ -0,0 +1,35 @@
using MTWireGuard.Models;
using MTWireGuard.Models.Mikrotik;
namespace MTWireGuard.Repositories
{
public interface IMikrotikRepository : IDisposable
{
Task<List<LogViewModel>> GetLogsAsync();
Task<List<WGServerViewModel>> GetServersAsync();
Task<WGServerViewModel> GetServer(string Name);
Task<List<ServerTrafficViewModel>> GetServersTraffic();
Task<List<WGPeerViewModel>> GetUsersAsync();
Task<WGPeerViewModel> GetUser(int id);
Task<string> GetUserTunnelConfig(int id);
Task<string> GetQRCodeBase64(int id);
Task<MTInfoViewModel> GetInfo();
Task<MTIdentityViewModel> GetName();
Task<bool> TryConnectAsync();
Task<List<ActiveUserViewModel>> GetActiveSessions();
Task<List<JobViewModel>> GetJobs();
Task<string> GetCurrentSessionID();
Task<string> KillJob(string JobID);
Task<CreationResult> CreateServer(ServerCreateModel server);
Task<CreationResult> CreateUser(UserCreateModel peer);
Task<CreationResult> SyncUser(UserSyncModel user);
Task<CreationResult> UpdateUser(UserUpdateModel user);
Task<CreationResult> UpdateServer(ServerUpdateModel server);
Task<CreationResult> EnableServer(int id);
Task<CreationResult> DisableServer(int id);
Task<CreationResult> EnableUser(int id);
Task<CreationResult> DisableUser(int id);
Task<CreationResult> DeleteServer(int id);
Task<CreationResult> DeleteUser(int id);
}
}

313
Services/MTAPI.cs Normal file
View file

@ -0,0 +1,313 @@
using AutoMapper;
using Microsoft.AspNetCore.Hosting.Server;
using Microsoft.EntityFrameworkCore;
using MTWireGuard.Models;
using MTWireGuard.Models.Mikrotik;
using MTWireGuard.Repositories;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using QRCoder;
namespace MTWireGuard.Services
{
public class MTAPI : IMikrotikRepository
{
private readonly IMapper mapper;
private readonly DBContext dbContext;
private bool disposed = false;
public MTAPI(IMapper mapper, DBContext dbContext)
{
this.mapper = mapper;
this.dbContext = dbContext;
}
public async Task<List<LogViewModel>> GetLogsAsync()
{
var model = await APIHandler.GetLogsAsync();
var result = mapper.Map<List<LogViewModel>>(model);
return result.OrderBy(list => list.Id).ToList();
}
public async Task<List<WGServerViewModel>> GetServersAsync()
{
var model = await APIHandler.GetServersAsync();
var result = mapper.Map<List<WGServerViewModel>>(model);
return result.OrderBy(list => list.Id).ToList();
}
public async Task<WGServerViewModel> GetServer(string Name)
{
var model = await APIHandler.GetServer(Name);
return mapper.Map<WGServerViewModel>(model);
}
public async Task<List<ServerTrafficViewModel>> GetServersTraffic() {
var model = await APIHandler.GetServersTraffic();
return mapper.Map<List<ServerTrafficViewModel>>(model);
}
public async Task<List<WGPeerViewModel>> GetUsersAsync()
{
var model = await APIHandler.GetUsersAsync();
var result = mapper.Map<List<WGPeerViewModel>>(model);
return result.OrderBy(list => list.Id).ToList();
}
public async Task<WGPeerViewModel> GetUser(int id)
{
var model = await APIHandler.GetUser($"*{id:X}");
return mapper.Map<WGPeerViewModel>(model);
}
public async Task<string> GetUserTunnelConfig(int id)
{
WGPeerViewModel User = await GetUser(id);
WGServerViewModel Server = await GetServer(User.Interface);
string IP = Environment.GetEnvironmentVariable("MT_PUBLIC_IP"),
Endpoint = Server != null ? $"{IP}:{Server.ListenPort}" : "";
return $"[Interface]{Environment.NewLine}" +
$"Address = {User.Address ?? "0.0.0.0/0"}{Environment.NewLine}" +
$"PrivateKey = {User.PrivateKey}{Environment.NewLine}" +
$"{Environment.NewLine}" +
$"[Peer]{Environment.NewLine}" +
$"AllowedIPs = 0.0.0.0/0{Environment.NewLine}" +
$"Endpoint = {Endpoint}{Environment.NewLine}" +
$"PublicKey = {Server?.PublicKey ?? ""}";
}
public async Task<string> GetQRCodeBase64(int id)
{
string config = await GetUserTunnelConfig(id);
using QRCodeGenerator qrGenerator = new();
using QRCodeData qrCodeData = qrGenerator.CreateQrCode(config, QRCodeGenerator.ECCLevel.Q);
using PngByteQRCode qrCode = new(qrCodeData);
var QR = qrCode.GetGraphic(20);
return Convert.ToBase64String(QR);
}
public async Task<MTInfoViewModel> GetInfo()
{
var model = await APIHandler.GetInfo();
return mapper.Map<MTInfoViewModel>(model);
}
public async Task<MTIdentityViewModel> GetName()
{
var model = await APIHandler.GetName();
return mapper.Map<MTIdentityViewModel>(model);
}
public async Task<bool> TryConnectAsync()
{
try
{
var model = await APIHandler.TryConnectAsync();
if ((model.Error == 400 && model.Message == "Bad Request") || (model.Error == 401 && model.Message == "Unauthorized"))
{
return true;
}
throw new($"[{model.Error}] Login failed, {model.Message}.<br>Enter router username/password in environment variables (MT_USER/MT_PASS).");
}
catch(Exception ex)
{
throw new($"Login failed, {ex.Message}");
}
}
public async Task<List<ActiveUserViewModel>> GetActiveSessions()
{
var model = await APIHandler.GetActiveSessions();
return mapper.Map<List<ActiveUserViewModel>>(model);
}
public async Task<List<JobViewModel>> GetJobs()
{
var model = await APIHandler.GetJobs();
return mapper.Map<List<JobViewModel>>(model);
}
public async Task<string> GetCurrentSessionID()
{
var activeSessions = await APIHandler.GetActiveSessions();
var apiSession = activeSessions.Find(x => x.Via == "api");
var jobs = await APIHandler.GetJobs();
var currentJob = jobs.Find(x => x.Started == apiSession.When);
return currentJob.Id;
}
public async Task<string> KillJob(string JobID)
{
return await APIHandler.KillJob(JobID);
}
public async Task<CreationResult> CreateServer(ServerCreateModel server)
{
var srv = mapper.Map<WGServerCreateModel>(server);
var model = await APIHandler.CreateServer(srv);
return mapper.Map<CreationResult>(model);
}
public async Task<CreationResult> CreateUser(UserCreateModel peer)
{
var user = mapper.Map<WGPeerCreateModel>(peer);
var model = await APIHandler.CreateUser(user);
if (model.Success)
{
var item = model.Item as WGPeer;
await dbContext.Users.AddAsync(new()
{
Id = Convert.ToInt32(item.Id[1..], 16),
Name = peer.Name,
PrivateKey = peer.PrivateKey,
PublicKey = peer.PublicKey
});
await dbContext.SaveChangesAsync();
}
return mapper.Map<CreationResult>(model);
}
public async Task<CreationResult> SyncUser(UserSyncModel user)
{
CreationResult result = new();
var userID = user.Id;
var dbUser = await dbContext.Users.FindAsync(userID);
var mtUser = await GetUser(userID);
if (dbUser == null)
{
await dbContext.Users.AddAsync(new()
{
Id = userID,
Name = user.Name,
PublicKey = user.PublicKey,
PrivateKey = user.PrivateKey
});
await dbContext.SaveChangesAsync();
result = new()
{
Code = "200",
Title = "Success",
Description = "Database updated successfully."
};
}
else if (dbUser.PublicKey != user.PublicKey)
{
var fxUser = dbUser;
fxUser.Name = user.Name;
fxUser.PrivateKey = user.PrivateKey;
fxUser.PublicKey = user.PublicKey;
dbContext.Users.Update(fxUser);
await dbContext.SaveChangesAsync();
result = new()
{
Code = "200",
Title = "Success",
Description = "Database updated successfully."
};
}
if (mtUser.PublicKey != user.PublicKey)
{
var fxUser = mapper.Map<WGPeerUpdateModel>(user);
var update = await APIHandler.UpdateUser(fxUser);
result = mapper.Map<CreationResult>(update);
}
return result;
}
public async Task<CreationResult> UpdateUser(UserUpdateModel user)
{
var mtPeer = mapper.Map<WGPeerUpdateModel>(user);
var mtUpdate = await APIHandler.UpdateUser(mtPeer);
if (mtUpdate.Success)
{
var exists = await dbContext.Users.FindAsync(user.Id);
dbContext.ChangeTracker.Clear();
if (exists != null)
{
dbContext.Users.Update(new()
{
Id = user.Id,
Name = user.Name ?? exists.Name,
PrivateKey = user.PrivateKey ?? exists.PrivateKey,
PublicKey = user.PublicKey ?? exists.PublicKey
});
}
else
await dbContext.Users.AddAsync(new()
{
Id = user.Id,
Name = user.Name,
PublicKey = user.PublicKey,
PrivateKey = user.PrivateKey
});
await dbContext.SaveChangesAsync();
}
return mapper.Map<CreationResult>(mtUpdate);
}
public async Task<CreationResult> UpdateServer(ServerUpdateModel server)
{
var srv = mapper.Map<WGServerUpdateModel>(server);
var mtUpdate = await APIHandler.UpdateServer(srv);
return mapper.Map<CreationResult>(mtUpdate);
}
public async Task<CreationResult> EnableServer(int id)
{
var enable = await APIHandler.SetServerEnabled(new()
{
ID = $"*{id:X}",
Disabled = false
});
return mapper.Map<CreationResult>(enable);
}
public async Task<CreationResult> DisableServer(int id)
{
var enable = await APIHandler.SetServerEnabled(new()
{
ID = $"*{id:X}",
Disabled = true
});
return mapper.Map<CreationResult>(enable);
}
public async Task<CreationResult> EnableUser(int id)
{
var enable = await APIHandler.SetUserEnabled(new()
{
ID = $"*{id:X}",
Disabled = false
});
return mapper.Map<CreationResult>(enable);
}
public async Task<CreationResult> DisableUser(int id)
{
var enable = await APIHandler.SetUserEnabled(new()
{
ID = $"*{id:X}",
Disabled = true
});
return mapper.Map<CreationResult>(enable);
}
public async Task<CreationResult> DeleteServer(int id)
{
var delete = await APIHandler.DeleteServer($"*{id:X}");
return mapper.Map<CreationResult>(delete);
}
public async Task<CreationResult> DeleteUser(int id)
{
var delete = await APIHandler.DeleteUser($"*{id:X}");
return mapper.Map<CreationResult>(delete);
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (disposed)
return;
if (disposing)
{
// Free any other managed objects here.
//
}
// Free any unmanaged objects here.
//
disposed = true;
}
}
}

View file

@ -0,0 +1,15 @@
using Microsoft.AspNetCore.Mvc;
using MTWireGuard.Models.Requests;
using System.Xml.Linq;
namespace MTWireGuard.ViewComponents
{
[ViewComponent(Name = "CreateClientForm")]
public class CreateClientFormViewComponent : ViewComponent
{
public async Task<IViewComponentResult> InvokeAsync()
{
return View("CreateClientForm", new CreateClientRequest());
}
}
}

View file

@ -0,0 +1,14 @@
using Microsoft.AspNetCore.Mvc;
using MTWireGuard.Models.Requests;
namespace MTWireGuard.ViewComponents
{
[ViewComponent(Name = "CreateServerForm")]
public class CreateServerFormViewComponent : ViewComponent
{
public async Task<IViewComponentResult> InvokeAsync()
{
return View("CreateServerForm", new CreateServerRequest());
}
}
}

View file

@ -0,0 +1,15 @@
using Microsoft.AspNetCore.Mvc;
using MTWireGuard.Models.Requests;
namespace MTWireGuard.ViewComponents
{
[ViewComponent(Name = "DeleteModal")]
public class DeleteModalViewComponent : ViewComponent
{
public async Task<IViewComponentResult> InvokeAsync(bool IsServer)
{
ViewData["IsServer"] = IsServer;
return View("DeleteModal", new DeleteRequest());
}
}
}

View file

@ -0,0 +1,15 @@
using Microsoft.AspNetCore.Mvc;
using MTWireGuard.Models.Requests;
using System.Xml.Linq;
namespace MTWireGuard.ViewComponents
{
[ViewComponent(Name = "SyncUserModal")]
public class SyncUserModalViewComponent : ViewComponent
{
public async Task<IViewComponentResult> InvokeAsync()
{
return View("SyncUserModal", new SyncUserRequest());
}
}
}

View file

@ -0,0 +1,15 @@
using Microsoft.AspNetCore.Mvc;
using MTWireGuard.Models.Requests;
namespace MTWireGuard.ViewComponents
{
[ViewComponent(Name = "UpdateClientModal")]
public class UpdateClientModalViewComponent : ViewComponent
{
public async Task<IViewComponentResult> InvokeAsync(List<Models.Mikrotik.WGServerViewModel> Servers)
{
ViewData["Servers"] = Servers;
return View("UpdateClientModal", new UpdateClientRequest());
}
}
}

View file

@ -0,0 +1,14 @@
using Microsoft.AspNetCore.Mvc;
using MTWireGuard.Models.Requests;
namespace MTWireGuard.ViewComponents
{
[ViewComponent(Name = "UpdateServerModal")]
public class UpdateServerModalViewComponent : ViewComponent
{
public async Task<IViewComponentResult> InvokeAsync()
{
return View("UpdateServerModal", new UpdateServerRequest());
}
}
}

View file

@ -0,0 +1,9 @@
{
"DetailedErrors": true,
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}

9
appsettings.json Normal file
View file

@ -0,0 +1,9 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}

11
build.bat Normal file
View file

@ -0,0 +1,11 @@
@echo off
echo Publish project...
dotnet publish -c Release -o published
echo Publish done!
echo.
echo Build docker image...
docker buildx build --no-cache --platform linux/amd64 -t mtwg .
echo Docker image built!
echo Save output file!
docker save mtwg > mtwg.tar
echo Finish

View file

@ -0,0 +1,9 @@
<svg width="300" height="300" viewport="0 0 46 46" xmlns="http://www.w3.org/2000/svg" version="1.1">
<title>wireguard</title>
<g>
<title>Layer 1</title>
<path id="svg_37" fill="#88171a" d="m299.7406,145.56s6.9396,-145.56 -153.04,-145.56c-141.48,0 -145.9,139.63 -145.9,139.63s-20.811,160.37 149.16,160.37c163.02,0 149.78,-154.44 149.78,-154.44zm-197.8,-50.863c30.017,-18.364 68.366,-7.1401 82.735,20.476c2.7233,5.2338 3.0694,13.291 1.3447,18.782c-5.9546,18.956 -20.014,29.587 -39.312,34.103c5.6892,-4.8707 10.218,-10.394 11.659,-18.025a26.402,26.402 0 0 0 -4.5425,-20.956a26.76,26.76 0 0 0 -30.811,-9.3892c-11.881,4.5111 -18.389,15.354 -17.216,28.683c1.0898,12.381 10.484,20.405 28.061,23.453c-2.627,1.3904 -4.6503,2.4144 -6.6299,3.5172a63.918,63.918 0 0 0 -20.544,17.868c-1.7839,2.4084 -3.0104,2.6024 -5.727,0.94116c-35.338,-21.61 -37.609,-75.844 0.98226,-99.453l0.00044,-0.00016zm-26.449,133.53c-5.6769,1.441 -11.178,3.5742 -16.981,5.4775c2.8385,-19.151 25.265,-36.788 44.23,-34.776a48.881,48.881 0 0 0 -9.242,25.893c-6.302,1.1606 -12.241,1.9414 -18.007,3.405l0,0.0005zm120.79,-186.98c5.6099,0.20612 11.23,0.12091 16.844,0.25378a29.052,29.052 0 0 1 4.1674,0.58069a40.607,40.607 0 0 1 -4.2357,5.4332c-2.007,1.8701 -4.2745,3.6986 -7.1661,0.856c-0.6955,-0.68372 -2.3386,-0.52679 -3.5487,-0.54272c-5.5823,-0.07336 -11.172,-0.25177 -16.746,-0.04132a104.04,104.04 0 0 0 -14.425,1.473c-0.89368,0.16046 -2.2299,3.1315 -1.8191,4.227c0.9693,2.5853 2.3833,5.4363 4.4779,7.0898c7.7403,6.11 15.972,11.596 23.748,17.664c7.556,5.8966 14.589,12.358 18.875,21.253c5.5843,11.59 5.747,23.743 3.3388,35.95c-4.0203,20.378 -14.333,37.261 -31.032,49.524c-6.7288,4.941 -15.06,7.7451 -22.767,11.295c-6.778,3.1225 -13.755,5.8115 -20.549,8.9008c-12.249,5.5695 -19.133,18.865 -17.108,32.688c1.8585,12.685 12.987,23.271 25.735,25.456c15.292,2.6216 31.071,-7.3163 34.812,-22.86c4.2067,-17.478 -5.2898,-33.083 -23.065,-37.813c-0.78271,-0.20831 -1.5684,-0.40552 -3.2012,-0.8269c4.7549,-2.1245 8.8614,-3.6381 12.653,-5.7244q9.9213,-5.4594 19.481,-11.562c1.8742,-1.199 2.8868,-1.1996 4.4852,0.18225c12.225,10.57 19.518,23.718 21.563,39.839c3.3845,26.684 -9.2471,51.198 -33.072,63.762c-36.86,19.439 -81.965,-2.6864 -90.106,-43.552c-6.9738,-35.003 17.73,-66.754 47.462,-72.884c12.787,-2.6364 24.48,-7.9596 33.57,-17.807c5.8652,-6.3541 8.7084,-11.806 9.6772,-14.266a39.565,39.565 0 0 0 2.7211,-14.469a33.867,33.867 0 0 0 -2.9654,-12.398c-3.104,-7.075 -14.995,-18.33 -17.939,-20.704l-28,-21.921c-0.98761,-0.81256 -2.0994,-0.75366 -4.5079,-0.59045c-2.8611,0.19391 -10.175,0.59888 -13.331,-0.22815c2.553,-1.9321 9.5132,-4.7451 12.502,-7.007c-9.0734,-6.1297 -19.43,-3.9158 -28.941,-5.7461c2.1992,-4.0959 13.081,-10.39 19.27,-11.091a91.533,91.533 0 0 0 -1.6876,-10.281c-0.37781,-1.3917 -1.9312,-2.7408 -3.2864,-3.5355c-3.286,-1.9267 -6.7694,-3.5167 -10.549,-5.4327a21.936,21.936 0 0 1 11.332,-3.5055a42.316,42.316 0 0 1 11.348,1.1056c6.7422,1.5405 12.124,0.53491 17.488,-4.048c-4.222,-1.7002 -8.4435,-3.2535 -12.538,-5.0907a123.04,123.04 0 0 1 -11.779,-6.1583c10.622,1.4755 20.896,5.4585 31.757,4.0034q0.1387,-0.74048 0.27728,-1.4809c-8.1194,-1.8899 -16.239,-3.7798 -25.229,-5.8724c15.04,-1.3769 29.042,-1.604 42.301,4.8541c3.731,1.8173 7.6348,3.3215 11.211,5.3972c1.7443,1.0124 2.9186,3.0078 4.3496,4.5594c1.1366,1.2325 2.0495,2.8837 3.446,3.6264c5.3,2.8184 11.134,2.9291 17.078,2.7879c0.04443,-0.67694 0.08606,-1.3114 0.1308,-1.9933c5.9821,1.8693 12.715,8.7679 12.704,13.806c-9.6911,0 -19.374,-0.037 -29.056,0.05389c-1.0348,0.0097 -2.0626,0.76563 -3.0936,1.1754c0.97986,0.57067 1.9428,1.5994 2.9423,1.6362l-0.00388,-0.00067z" class="a"/>
<path id="svg_38" fill="#88171a" d="m183.7806,26.906a1.4806,1.4806 0 0 0 -0.18927,2.3686a2.2326,2.2326 0 0 0 3.0724,0.8219c0.9328,-0.47052 1.8478,-0.97137 2.975,-1.5665c-0.9079,-0.775 -1.6362,-1.4148 -2.3857,-2.0324c-1.318,-1.086 -2.411,-0.40386 -3.4724,0.40833l-0.00003,0.00007z" class="a"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.8 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Some files were not shown because too many files have changed in this diff Show more