140 Commits
v1b14 ... v1b18

Author SHA1 Message Date
Marcel Baumgartner
f48ec2245c Merge pull request #299 from Moonlight-Panel/AddNewScan
Added discord nuke bot scan
2023-08-31 01:03:24 +02:00
Marcel Baumgartner
37a3a35120 Added discord nuke bot scan 2023-08-31 01:01:15 +02:00
Marcel Baumgartner
c549d45d54 Merge pull request #298 from Moonlight-Panel/AddProxyScan
Added proxy scan
2023-08-30 16:21:46 +02:00
Marcel Baumgartner
42a011541c Added proxy scan 2023-08-30 16:20:57 +02:00
Marcel Baumgartner
71a8839f5f Merge pull request #297 from Moonlight-Panel/AddLiveStatistics
Add live statistics
2023-08-29 16:10:31 +02:00
Marcel Baumgartner
b99e8b5476 Merge branch 'main' into AddLiveStatistics 2023-08-29 16:10:16 +02:00
Marcel Baumgartner
5793bd9747 Added new live statistics to navigation 2023-08-29 16:07:43 +02:00
Marcel Baumgartner
24ce51a62e Merge pull request #296 from Moonlight-Panel/NewAdminDomainNavigation
Added new admin domain navigation
2023-08-29 15:55:46 +02:00
Marcel Baumgartner
762d3cc1a1 Added new admin domain navigation 2023-08-29 15:54:37 +02:00
Marcel Baumgartner
ea19105006 Fixed null server handling for the malware result event 2023-08-29 15:11:42 +02:00
Marcel Baumgartner
a372c756e4 Merge pull request #295 from Moonlight-Panel/AddIpFiltering
Implemented ip filtering to detect datacenter and vpn ips
2023-08-29 15:03:31 +02:00
Marcel Baumgartner
18b7c82613 Implemented ip filtering to detect datacenter and vpn ips 2023-08-29 15:03:07 +02:00
Marcel Baumgartner
5d91077d42 Updated MinerScan 2023-08-29 11:55:45 +02:00
Marcel Baumgartner
c0872d0d02 Fixed build errors caused by the removal of defaultstorage 2023-08-28 17:41:29 +02:00
Marcel Baumgartner
00722c4046 Merge pull request #294 from Moonlight-Panel/NewResourceSync
Switched to new resource sync
2023-08-28 17:33:37 +02:00
Marcel Baumgartner
f16a29dafb Switched to new resource sync 2023-08-28 17:32:34 +02:00
Marcel Baumgartner
6da8393612 Merge pull request #293 from Moonlight-Panel/SmallFixes
Some small fixes
2023-08-28 16:29:54 +02:00
Marcel Baumgartner
2ef00b81b8 Switched to new apex charts version 2023-08-28 16:27:59 +02:00
Marcel Baumgartner
0be4ebc7ff Small fix when starting a new malware scan 2023-08-28 16:20:24 +02:00
Marcel Baumgartner
e0d5e754b7 Merge pull request #292 from Moonlight-Panel/ImprovedMalwareScan
Improved malware scan. Added server and user purge
2023-08-28 05:09:10 +02:00
Marcel Baumgartner
6972c2bb32 Improved malware scan. Added server and user purge 2023-08-28 05:08:52 +02:00
Marcel Baumgartner
95ba81eab4 Merge pull request #291 from Moonlight-Panel/FixInitialInstallConsole
Fixed initial install console and added a better redirect handling when creating a server
2023-08-27 20:41:40 +02:00
Marcel Baumgartner
b9c094cafa Fixed initial install console and added a better redirect handling when creating a server 2023-08-27 20:40:58 +02:00
Marcel Baumgartner
bada1e829b Merge pull request #290 from Moonlight-Panel/ReduceExternalJs
Reduced external js, added libraries to assets
2023-08-27 17:29:57 +02:00
Marcel Baumgartner
1d33cf3110 Reduced external js, added libraries to assets 2023-08-27 17:29:37 +02:00
Marcel Baumgartner
abfcefba60 Merge pull request #286 from Moonlight-Panel/OwO
OwO
2023-08-23 17:42:48 +02:00
Marcel Baumgartner
6efb443481 Update launchSettings.json 2023-08-23 17:42:24 +02:00
Marcel Baumgartner
10c017932a Added a basic live statistics system 2023-08-23 17:29:34 +02:00
Marcel Baumgartner
a4be4bdc52 Merge pull request #285 from Moonlight-Panel/ImproveTotpSetup
Improved totp setup
2023-08-23 13:33:01 +02:00
Marcel Baumgartner
88d5a1de86 Improved totp setup 2023-08-23 13:32:23 +02:00
Marcel Baumgartner
1b88827d16 Merge pull request #284 from Moonlight-Panel/UserDiscordIdFilter
Added discord id filter
2023-08-23 13:23:52 +02:00
Marcel Baumgartner
12e25dc4bc Added discord id filter 2023-08-23 13:23:36 +02:00
Marcel Baumgartner
37c3fc7a53 Merge pull request #283 from Moonlight-Panel/ImproveFrontend
Improve frontend
2023-08-23 13:17:51 +02:00
Marcel Baumgartner
0c6cbe0ba7 Changed background image
Thanks to @nuhuhwy
2023-08-23 13:10:21 +02:00
Marcel Baumgartner
5cf63240a3 Removed the white mode and useless css 2023-08-23 13:06:04 +02:00
Marcel Baumgartner
4d04d12a39 Removed old js calls 2023-08-23 12:35:30 +02:00
Marcel Baumgartner
b846b28802 Reduced css and js which was not used 2023-08-23 11:20:52 +02:00
Marcel Baumgartner
b94217abc0 Merge pull request #277 from 1Pot2Nutella-p/FRlanguage
add french language
2023-08-17 18:44:21 +02:00
1Pot2Nutella-p
de0d6a3808 add frensh language 2023-08-16 20:56:49 +02:00
Marcel Baumgartner
eca9b6ec52 Merge pull request #275 from Moonlight-Panel/RedesignServerPage
Redesign server page
2023-08-15 22:10:39 +02:00
Marcel Baumgartner
b6e048e982 Added spacing to network tab 2023-08-15 22:07:47 +02:00
Marcel Baumgartner
b8b2ae7865 Merge pull request #274 from Moonlight-Panel/ImproveServerTabIconHandling
Improved server tab icon handling
2023-08-15 21:59:51 +02:00
Marcel Baumgartner
0611988019 Improved server tab icon handling 2023-08-15 21:59:35 +02:00
Marcel Baumgartner
f9fd7199b6 Merge pull request #273 from realspinelle/SomeUXFix
Set unsorted servers to open by default
2023-08-15 19:58:28 +02:00
realspinelle
6ff4861fc6 Set unsorted servers to open by default 2023-08-15 19:21:31 +02:00
Marcel Baumgartner
2db7748703 Merge pull request #272 from Moonlight-Panel/ImproveLatencyCheckThreshold
Improved latency check default threshould
2023-08-15 15:50:30 +02:00
Marcel Baumgartner
87744e4846 Improved latency check default threshould
As mentioned in issue 263
2023-08-15 15:49:55 +02:00
Marcel Baumgartner
ce7125b50b Merge pull request #271 from Moonlight-Panel/ImproveAutoAdminSecurity
Improved auto admin security
2023-08-15 15:45:46 +02:00
Marcel Baumgartner
31d8c3f469 Improved auto admin security
As mentioned in issue 253
2023-08-15 15:45:12 +02:00
Marcel Baumgartner
c80622c2fd Merge pull request #270 from Moonlight-Panel/ImproveSecurityLogPrivacy
Improved the privacy for security logs
2023-08-15 15:33:38 +02:00
Marcel Baumgartner
95e659e5f7 Improved the privacy for security logs
As mentioned in issue 262
2023-08-15 15:33:18 +02:00
Marcel Baumgartner
c155909e82 Merge pull request #269 from Moonlight-Panel/MoveChangelog
Moved changelog to admin section
2023-08-15 15:26:44 +02:00
Marcel Baumgartner
0011ed29b7 Moved changelog to admin section 2023-08-15 15:26:24 +02:00
Marcel Baumgartner
52c4ca0c0a Merge pull request #268 from Moonlight-Panel/AddBetterSelfbotDetection
Improved the whole malware scan system. Made it possible for plugins to add them. Improved self bot detection for python
2023-08-15 15:15:30 +02:00
Marcel Baumgartner
c197d0ca96 Improved the whole malware scan system. Made it possible for plugins to add them. Improved self bot detection for python 2023-08-15 15:14:28 +02:00
Marcel Baumgartner
25902034e9 Switched to horizontal nav bar in the server view 2023-08-14 22:16:59 +02:00
Marcel Baumgartner
b10db643fe Merge pull request #266 from Moonlight-Panel/ImproveSoftErrors
Improved soft error boundary
2023-08-14 18:33:58 +02:00
Marcel Baumgartner
4c7ffe6714 Improved soft error boundary 2023-08-14 18:22:20 +02:00
Marcel Baumgartner
a0c2b45a61 Merge pull request #249 from Dannyx1604/main
Update README.md because of new installer
2023-08-13 21:16:53 +02:00
Marcel Baumgartner
e49a9d3505 Merge branch 'main' into main 2023-08-13 21:16:42 +02:00
Dannyx
7ddae9c3e1 t
t
2023-08-13 20:04:47 +02:00
Daniel Balk
290e865ae0 Merge pull request #261 from NaysKutzu/main
Fix a lot of typing mistakes and added Romanian translations.
2023-08-09 15:52:42 +02:00
NaysKutzu
00ee625e2e Create ro_ro.lang 2023-08-09 15:10:57 +02:00
NaysKutzu
274e2d93f2 Fix the locations of the elements. 2023-08-09 14:48:09 +02:00
NaysKutzu
781171f7c5 Fix typo 2023-08-09 14:46:41 +02:00
NaysKutzu
4a8618da79 Merge pull request #1 from NaysKutzu/NaysKutzu-patch-1
Fix some typos in the README.
2023-08-09 14:44:23 +02:00
NaysKutzu
296cf0db6f Fix typo in readme 2023-08-09 14:43:26 +02:00
Marcel Baumgartner
48cdba5155 Removed unused a tags in ticket view 2023-08-09 04:08:28 +02:00
Marcel Baumgartner
35c1b255b5 Update README.md 2023-08-09 03:47:50 +02:00
Marcel Baumgartner
244b920305 Merge pull request #260 from Moonlight-Panel/RemoveFormatterByteUtils
switched to the new converter instead of the formatter functions to convert byte sizes
2023-08-09 03:46:42 +02:00
Marcel Baumgartner
3d4a2128e2 switched to the new converter instead of the formatter functions to convert byte sizes 2023-08-09 03:42:33 +02:00
Marcel Baumgartner
1cf8430ad8 Small include improvment 2023-08-09 03:33:28 +02:00
Marcel Baumgartner
a9b7d10fb0 Fix invalid include with new include 2023-08-09 03:23:34 +02:00
Marcel Baumgartner
47e333630e Removed useless links in the tickets overview for admins 2023-08-09 01:00:55 +02:00
Marcel Baumgartner
8b41a9fc13 Add message sender include 2023-08-09 00:57:40 +02:00
Marcel Baumgartner
d09957fdac Merge pull request #259 from Moonlight-Panel/ImproveBackgroundImage
Made the background image darker
2023-08-09 00:37:53 +02:00
Marcel Baumgartner
f9ecb61d71 Made the background image darker
Thanks to https://github.com/nuhuhwy
2023-08-09 00:37:36 +02:00
Marcel Baumgartner
ef92dd47ad Merge pull request #258 from Moonlight-Panel/ImproveTicketSystemUI
Improved ticket system ui and some backend code
2023-08-09 00:36:33 +02:00
Marcel Baumgartner
c2c533675b Improved ticket system ui and some backend code 2023-08-09 00:35:49 +02:00
Marcel Baumgartner
569dd69bdd Merge pull request #257 from Moritz-Deiaco/main
pagination button gap fix
2023-08-08 17:14:07 +02:00
Moritz
242870b3e1 pagination button gap fix 2023-08-08 17:08:54 +02:00
Daniel Balk
30b6e45235 Merge pull request #254 from Moritz-Deiaco/main
New Translations
2023-08-08 16:33:00 +02:00
Moritz
cd62fdc5f6 fixed a typo, rechecked everything 2023-08-08 16:28:46 +02:00
Moritz Deiaco
2c54b91e6c New translations in de_de.lang 2023-08-08 12:16:14 +02:00
Marcel Baumgartner
388deacf60 Merge pull request #250 from Moonlight-Panel/ImproveInstallUX
Improved install user experience
2023-08-08 00:54:10 +02:00
Marcel Baumgartner
aa547038de Improved install user experience 2023-08-08 00:53:00 +02:00
Dannyx
78bfd68d63 Update README.md because of New installer 2023-08-07 22:23:20 +02:00
Marcel Baumgartner
17e3345b8a Added date formatter for user overview 2023-08-06 22:00:59 +02:00
Marcel Baumgartner
f95312c1e3 Merge pull request #248 from Moonlight-Panel/AddTicketSystem
Added ticket system
2023-08-06 21:57:24 +02:00
Marcel Baumgartner
2144ca3823 Added byte converter and fixed memory view at the nodes 2023-08-06 21:55:23 +02:00
Marcel Baumgartner
de45ff40d8 Implemented a basic ticket system 2023-08-04 14:00:25 +02:00
Marcel Baumgartner
606085c012 Merge pull request #242 from Moonlight-Panel/AddNewBackupFormat
Added new backup format
2023-08-02 22:05:39 +02:00
Marcel Baumgartner
00525d8099 Added new backup format 2023-08-02 22:05:07 +02:00
Marcel Baumgartner
26617d67f5 Merge pull request #241 from Moonlight-Panel/SmallFixes
Did some small fixes, added connection timeout check, improved ux
2023-08-02 03:07:47 +02:00
Marcel Baumgartner
600bec3417 Did some small fixes, added connection timeout check, improved ux 2023-08-02 03:06:54 +02:00
Marcel Baumgartner
4e85d1755a Merge pull request #240 from DatGamet/patch-1
Update de_de.lang
2023-08-02 01:39:58 +02:00
DatGamet
0832936933 Update de_de.lang 2023-08-02 01:39:08 +02:00
Marcel Baumgartner
ecda2ec6d1 Merge pull request #239 from Moonlight-Panel/FixPluginSystem
Fixed plugin loading system
2023-08-01 22:21:54 +02:00
Marcel Baumgartner
6f3765a3bf Fixed plugin loading system 2023-08-01 22:21:19 +02:00
Marcel Baumgartner
29002d3445 Update Moonlight.csproj 2023-08-01 20:38:22 +02:00
Marcel Baumgartner
6d0456a008 Merge pull request #238 from Moonlight-Panel/AddDdosProtection
Added new ddos protection
2023-07-24 00:33:22 +02:00
Marcel Baumgartner
3e698123bb Merge branch 'main' into AddDdosProtection 2023-07-24 00:33:01 +02:00
Marcel Baumgartner
f3fb86819a Merge pull request #237 from Moonlight-Panel/FixDomainIdentityIssues
Fixed domain identity issues
2023-07-24 00:30:50 +02:00
Marcel Baumgartner
e2248a8444 Fixed domain identity issues 2023-07-24 00:30:18 +02:00
Marcel Baumgartner
2cf2b77090 Added new ddos protection 2023-07-24 00:23:29 +02:00
Marcel Baumgartner
fedc9278d4 Updated dependencies 2023-07-23 22:12:17 +02:00
Marcel Baumgartner
f29206a69b Merge pull request #236 from Moonlight-Panel/AddPluginSystem
Add plugin system
2023-07-23 21:31:26 +02:00
Marcel Baumgartner
0658e55a78 Implemented basic plugin store and improved plugin system 2023-07-23 21:30:57 +02:00
Marcel Baumgartner
21bea974a9 Implemented a basic plugin system 2023-07-22 23:44:45 +02:00
Marcel Baumgartner
33ef09433e Merge pull request #235 from Moonlight-Panel/AddMinerCheck
Add basic miner check
2023-07-22 02:24:16 +02:00
Marcel Baumgartner
173bff67df Add basic miner check 2023-07-22 02:08:39 +02:00
Marcel Baumgartner
512a989609 Merge pull request #234 from Moonlight-Panel/FixDomainOwnerMatching
Fixed domain owner matching
2023-07-20 17:24:05 +02:00
Marcel Baumgartner
2d7dac5089 Fixed domain owner matching 2023-07-20 17:17:05 +02:00
Marcel Baumgartner
11708fbc3b Merge pull request #231 from Moonlight-Panel/AdditionalDnsErrorHandling
Added additional dns error handling
2023-07-20 02:00:41 +02:00
Marcel Baumgartner
daeb4dd5b9 Added additional dns error handling 2023-07-20 02:00:03 +02:00
Marcel Baumgartner
daba4cba04 Merge pull request #230 from Moonlight-Panel/AddSecurityLogs
Added security logs. Removed unsued log models. Added dynamic config load
2023-07-19 22:09:58 +02:00
Marcel Baumgartner
1cd0f0f96f Added security logs. Removed unsued log models. Added dynamisc config load system for development 2023-07-19 20:07:57 +02:00
Marcel Baumgartner
6a30db07a7 Merge pull request #229 from Moonlight-Panel/AddPermissionSystem
Added permission system and improved UIs and the IdentityService
2023-07-18 02:02:31 +02:00
Marcel Baumgartner
6c8754d008 Removed moved navigation items 2023-07-18 02:01:02 +02:00
Marcel Baumgartner
356ba94592 Implemented new user editing including permission groups 2023-07-18 01:57:48 +02:00
Marcel Baumgartner
d3b55d155b Added permission groups. Cleaned security ui. Added some permission stuff 2023-07-17 22:16:39 +02:00
Marcel Baumgartner
0015001d7c Fixed some permissions. Cleaned some UIs 2023-07-17 16:22:22 +02:00
Marcel Baumgartner
0a86aa8aa4 Implemented new permission and identity system 2023-07-17 00:48:27 +02:00
Marcel Baumgartner
74d4ee729d Adding permissions. I hate my life ;) 2023-07-16 22:44:36 +02:00
Marcel Baumgartner
178ff36e86 Implemented a basic permission system base 2023-07-16 02:21:53 +02:00
Marcel Baumgartner
f852df5807 Merge pull request #227 from Moonlight-Panel/StripeIntegration
Implemented a basic stripe integration
2023-07-13 21:36:57 +02:00
Marcel Baumgartner
90f4b04857 Changed configuration file 2023-07-13 21:36:19 +02:00
Marcel Baumgartner
244e87ed18 Implemented a basic stripe interation 2023-07-13 21:29:20 +02:00
Marcel Baumgartner
80ea5a543f Merge pull request #226 from Moonlight-Panel/main
Update branch StripeIntegration with latest commits
2023-07-13 20:46:39 +02:00
Marcel Baumgartner
5baba05f5f Merge pull request #225 from Moonlight-Panel/NewMailSystem
Added new mail system
2023-07-13 20:40:43 +02:00
Marcel Baumgartner
591da6de5c Implemented new mail system components 2023-07-13 20:39:02 +02:00
Marcel Baumgartner
c1ddff4ae3 Update .gitattributes 2023-07-13 17:33:34 +02:00
Marcel Baumgartner
67d78d7104 Merge pull request #224 from Moonlight-Panel/FixGravatarBug
Fixed gravatar bug
2023-07-13 17:26:09 +02:00
Marcel Baumgartner
52f4b00f84 Fixed gravatar bug 2023-07-13 17:25:48 +02:00
Marcel Baumgartner
8f028e2ac6 Update .gitattributes 2023-07-13 17:21:22 +02:00
Marcel Baumgartner
5bd6f15203 Added a mail template editor 2023-07-12 15:48:30 +02:00
Marcel Baumgartner
4c39ad6170 Removed legacy trash mail detector service 2023-07-12 14:34:26 +02:00
Marcel Baumgartner
12392d4f47 Merge pull request #223 from Moonlight-Panel/AddTempMailCheck
Added temp mail check
2023-07-12 14:22:41 +02:00
Marcel Baumgartner
b75147e4c0 Added temp mail check 2023-07-12 14:20:55 +02:00
Marcel Baumgartner
c0df8ac507 Implemented new subscription system and basic stripe support 2023-07-06 02:12:06 +02:00
360 changed files with 25359 additions and 357460 deletions

9
.gitattributes vendored
View File

@@ -1,3 +1,10 @@
# Auto detect text files and perform LF normalization # Auto detect text files and perform LF normalization
* text=auto * text=auto
Moonlight/wwwroot/* linguist-vendored Moonlight/wwwroot/** linguist-vendored
Moonlight/wwwroot/assets/js/scripts.bundle.js linguist-vendored
Moonlight/wwwroot/assets/js/widgets.bundle.js linguist-vendored
Moonlight/wwwroot/assets/js/theme.js linguist-vendored
Moonlight/wwwroot/assets/css/boxicons.min.css linguist-vendored
Moonlight/wwwroot/assets/css/style.bundle.css linguist-vendored
Moonlight/wwwroot/assets/plugins/** linguist-vendored
Moonlight/wwwroot/assets/fonts/** linguist-vendored

View File

@@ -17,6 +17,15 @@ public class ConfigV1
[Description("The url moonlight is accesible with from the internet")] [Description("The url moonlight is accesible with from the internet")]
public string AppUrl { get; set; } = "http://your-moonlight-url-without-slash"; public string AppUrl { get; set; } = "http://your-moonlight-url-without-slash";
[JsonProperty("EnableLatencyCheck")]
[Description(
"This will enable a latency check for connections to moonlight. Users with an too high latency will be warned that moonlight might be buggy for them")]
public bool EnableLatencyCheck { get; set; } = true;
[JsonProperty("LatencyCheckThreshold")]
[Description("Specify the latency threshold which has to be reached in order to trigger the warning message")]
public int LatencyCheckThreshold { get; set; } = 1000;
[JsonProperty("Auth")] public AuthData Auth { get; set; } = new(); [JsonProperty("Auth")] public AuthData Auth { get; set; } = new();
[JsonProperty("Database")] public DatabaseData Database { get; set; } = new(); [JsonProperty("Database")] public DatabaseData Database { get; set; } = new();
@@ -39,8 +48,6 @@ public class ConfigV1
[JsonProperty("Cleanup")] public CleanupData Cleanup { get; set; } = new(); [JsonProperty("Cleanup")] public CleanupData Cleanup { get; set; } = new();
[JsonProperty("Subscriptions")] public SubscriptionsData Subscriptions { get; set; } = new();
[JsonProperty("DiscordNotifications")] public DiscordNotificationsData DiscordNotifications { get; set; } = new(); [JsonProperty("DiscordNotifications")] public DiscordNotificationsData DiscordNotifications { get; set; } = new();
[JsonProperty("Statistics")] public StatisticsData Statistics { get; set; } = new(); [JsonProperty("Statistics")] public StatisticsData Statistics { get; set; } = new();
@@ -50,6 +57,24 @@ public class ConfigV1
[JsonProperty("SmartDeploy")] public SmartDeployData SmartDeploy { get; set; } = new(); [JsonProperty("SmartDeploy")] public SmartDeployData SmartDeploy { get; set; } = new();
[JsonProperty("Sentry")] public SentryData Sentry { get; set; } = new(); [JsonProperty("Sentry")] public SentryData Sentry { get; set; } = new();
[JsonProperty("Stripe")] public StripeData Stripe { get; set; } = new();
[JsonProperty("Tickets")] public TicketsData Tickets { get; set; } = new();
}
public class TicketsData
{
[JsonProperty("WelcomeMessage")]
[Description("The message that will be sent when a user created a ticket")]
public string WelcomeMessage { get; set; } = "Welcome to the support";
}
public class StripeData
{
[JsonProperty("ApiKey")]
[Description("Put here your stripe api key if you add subscriptions. Currently the only billing option is stripe which is enabled by default and cannot be turned off. This feature is still experimental")]
public string ApiKey { get; set; } = "";
} }
public class AuthData public class AuthData
@@ -265,11 +290,28 @@ public class ConfigV1
public class SecurityData public class SecurityData
{ {
[JsonProperty("Token")] [JsonProperty("Token")]
[Description("This is the moonlight app token. It is used to encrypt and decrypt data and validte tokens and sessions")] [Description("This is the moonlight app token. It is used to encrypt and decrypt data and validate tokens and sessions")]
[Blur] [Blur]
public string Token { get; set; } = Guid.NewGuid().ToString(); public string Token { get; set; } = Guid.NewGuid().ToString();
[JsonProperty("MalwareCheckOnStart")]
[Description(
"This option will enable the scanning for malware on every server before it has been started and if something has been found, the power action will be canceled")]
public bool MalwareCheckOnStart { get; set; } = true;
[JsonProperty("BlockIpDuration")]
[Description("The duration in minutes a ip will be blocked by the anti ddos system")]
public int BlockIpDuration { get; set; } = 15;
[JsonProperty("ReCaptcha")] public ReCaptchaData ReCaptcha { get; set; } = new(); [JsonProperty("ReCaptcha")] public ReCaptchaData ReCaptcha { get; set; } = new();
[JsonProperty("BlockDatacenterIps")]
[Description("If this option is enabled, users with an ip from datacenters will not be able to access the panel")]
public bool BlockDatacenterIps { get; set; } = true;
[JsonProperty("AllowCloudflareIps")]
[Description("Allow cloudflare ips to bypass the datacenter ip check")]
public bool AllowCloudflareIps { get; set; } = false;
} }
public class ReCaptchaData public class ReCaptchaData
@@ -318,11 +360,6 @@ public class ConfigV1
[JsonProperty("Wait")] public long Wait { get; set; } = 15; [JsonProperty("Wait")] public long Wait { get; set; } = 15;
} }
public class SubscriptionsData
{
[JsonProperty("SellPass")] public SellPassData SellPass { get; set; } = new();
}
public class SellPassData public class SellPassData
{ {
[JsonProperty("Enable")] public bool Enable { get; set; } = false; [JsonProperty("Enable")] public bool Enable { get; set; } = false;

View File

@@ -1,9 +1,7 @@
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Moonlight.App.Database.Entities; using Moonlight.App.Database.Entities;
using Moonlight.App.Database.Entities.LogsEntries;
using Moonlight.App.Database.Entities.Notification; using Moonlight.App.Database.Entities.Notification;
using Moonlight.App.Database.Interceptors; using Moonlight.App.Database.Interceptors;
using Moonlight.App.Models.Misc;
using Moonlight.App.Services; using Moonlight.App.Services;
namespace Moonlight.App.Database; namespace Moonlight.App.Database;
@@ -27,10 +25,6 @@ public class DataContext : DbContext
public DbSet<ServerVariable> ServerVariables { get; set; } public DbSet<ServerVariable> ServerVariables { get; set; }
public DbSet<User> Users { get; set; } public DbSet<User> Users { get; set; }
public DbSet<LoadingMessage> LoadingMessages { get; set; } public DbSet<LoadingMessage> LoadingMessages { get; set; }
public DbSet<AuditLogEntry> AuditLog { get; set; }
public DbSet<ErrorLogEntry> ErrorLog { get; set; }
public DbSet<SecurityLogEntry> SecurityLog { get; set; }
public DbSet<SharedDomain> SharedDomains { get; set; } public DbSet<SharedDomain> SharedDomains { get; set; }
public DbSet<Domain> Domains { get; set; } public DbSet<Domain> Domains { get; set; }
public DbSet<Revoke> Revokes { get; set; } public DbSet<Revoke> Revokes { get; set; }
@@ -46,6 +40,13 @@ public class DataContext : DbContext
public DbSet<WebSpace> WebSpaces { get; set; } public DbSet<WebSpace> WebSpaces { get; set; }
public DbSet<SupportChatMessage> SupportChatMessages { get; set; } public DbSet<SupportChatMessage> SupportChatMessages { get; set; }
public DbSet<IpBan> IpBans { get; set; } public DbSet<IpBan> IpBans { get; set; }
public DbSet<PermissionGroup> PermissionGroups { get; set; }
public DbSet<SecurityLog> SecurityLogs { get; set; }
public DbSet<BlocklistIp> BlocklistIps { get; set; }
public DbSet<WhitelistIp> WhitelistIps { get; set; }
public DbSet<Ticket> Tickets { get; set; }
public DbSet<TicketMessage> TicketMessages { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{ {

View File

@@ -0,0 +1,10 @@
namespace Moonlight.App.Database.Entities;
public class BlocklistIp
{
public int Id { get; set; }
public string Ip { get; set; } = "";
public DateTime ExpiresAt { get; set; }
public long Packets { get; set; }
public DateTime CreatedAt { get; set; }
}

View File

@@ -1,13 +0,0 @@
using Moonlight.App.Models.Misc;
namespace Moonlight.App.Database.Entities.LogsEntries;
public class AuditLogEntry
{
public int Id { get; set; }
public AuditLogType Type { get; set; }
public string JsonData { get; set; } = "";
public bool System { get; set; }
public string Ip { get; set; } = "";
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
}

View File

@@ -1,12 +0,0 @@
namespace Moonlight.App.Database.Entities.LogsEntries;
public class ErrorLogEntry
{
public int Id { get; set; }
public string Stacktrace { get; set; } = "";
public bool System { get; set; }
public string JsonData { get; set; } = "";
public string Ip { get; set; } = "";
public string Class { get; set; } = "";
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
}

View File

@@ -1,13 +0,0 @@
using Moonlight.App.Models.Misc;
namespace Moonlight.App.Database.Entities.LogsEntries;
public class SecurityLogEntry
{
public int Id { get; set; }
public bool System { get; set; }
public string Ip { get; set; } = "";
public SecurityLogType Type { get; set; }
public string JsonData { get; set; } = "";
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
}

View File

@@ -0,0 +1,8 @@
namespace Moonlight.App.Database.Entities;
public class PermissionGroup
{
public int Id { get; set; }
public string Name { get; set; } = "";
public byte[] Permissions { get; set; } = Array.Empty<byte>();
}

View File

@@ -0,0 +1,8 @@
namespace Moonlight.App.Database.Entities;
public class SecurityLog
{
public int Id { get; set; }
public string Text { get; set; } = "";
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
}

View File

@@ -1,9 +1,16 @@
namespace Moonlight.App.Database.Entities; using Moonlight.App.Models.Misc;
namespace Moonlight.App.Database.Entities;
public class Subscription public class Subscription
{ {
public int Id { get; set; } public int Id { get; set; }
public string Name { get; set; } = ""; public string Name { get; set; } = "";
public string Description { get; set; } = ""; public string Description { get; set; } = "";
public Currency Currency { get; set; } = Currency.USD;
public double Price { get; set; }
public string StripeProductId { get; set; } = "";
public string StripePriceId { get; set; } = "";
public string LimitsJson { get; set; } = ""; public string LimitsJson { get; set; } = "";
public int Duration { get; set; } = 30;
} }

View File

@@ -0,0 +1,19 @@
using Moonlight.App.Models.Misc;
namespace Moonlight.App.Database.Entities;
public class Ticket
{
public int Id { get; set; }
public string IssueTopic { get; set; } = "";
public string IssueDescription { get; set; } = "";
public string IssueTries { get; set; } = "";
public User CreatedBy { get; set; }
public User? AssignedTo { get; set; }
public TicketPriority Priority { get; set; }
public TicketStatus Status { get; set; }
public TicketSubject Subject { get; set; }
public int SubjectId { get; set; }
public List<TicketMessage> Messages { get; set; } = new();
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
}

View File

@@ -0,0 +1,14 @@
namespace Moonlight.App.Database.Entities;
public class TicketMessage
{
public int Id { get; set; }
public string Content { get; set; } = "";
public string? AttachmentUrl { get; set; }
public User? Sender { get; set; }
public bool IsSystemMessage { get; set; }
public bool IsEdited { get; set; }
public bool IsSupportMessage { get; set; }
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
}

View File

@@ -1,4 +1,5 @@
using Moonlight.App.Models.Misc; using System.ComponentModel.DataAnnotations;
using Moonlight.App.Models.Misc;
namespace Moonlight.App.Database.Entities; namespace Moonlight.App.Database.Entities;
@@ -39,6 +40,8 @@ public class User
public bool TotpEnabled { get; set; } = false; public bool TotpEnabled { get; set; } = false;
public string TotpSecret { get; set; } = ""; public string TotpSecret { get; set; } = "";
public DateTime TokenValidTime { get; set; } = DateTime.UtcNow; public DateTime TokenValidTime { get; set; } = DateTime.UtcNow;
public byte[] Permissions { get; set; } = Array.Empty<byte>();
public PermissionGroup? PermissionGroup { get; set; }
// Discord // Discord
public ulong DiscordId { get; set; } public ulong DiscordId { get; set; }
@@ -51,8 +54,8 @@ public class User
// Subscriptions // Subscriptions
public Subscription? CurrentSubscription { get; set; } = null; public Subscription? CurrentSubscription { get; set; } = null;
public DateTime SubscriptionSince { get; set; } = DateTime.Now; public DateTime SubscriptionSince { get; set; } = DateTime.UtcNow;
public int SubscriptionDuration { get; set; } public DateTime SubscriptionExpires { get; set; } = DateTime.UtcNow;
// Ip logs // Ip logs
public string RegisterIp { get; set; } = ""; public string RegisterIp { get; set; } = "";

View File

@@ -0,0 +1,7 @@
namespace Moonlight.App.Database.Entities;
public class WhitelistIp
{
public int Id { get; set; }
public string Ip { get; set; } = "";
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,96 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Moonlight.App.Database.Migrations
{
/// <inheritdoc />
public partial class AddedStripeIntegration : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "SubscriptionDuration",
table: "Users");
migrationBuilder.AddColumn<DateTime>(
name: "SubscriptionExpires",
table: "Users",
type: "datetime(6)",
nullable: false,
defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified));
migrationBuilder.AddColumn<int>(
name: "Currency",
table: "Subscriptions",
type: "int",
nullable: false,
defaultValue: 0);
migrationBuilder.AddColumn<int>(
name: "Duration",
table: "Subscriptions",
type: "int",
nullable: false,
defaultValue: 0);
migrationBuilder.AddColumn<double>(
name: "Price",
table: "Subscriptions",
type: "double",
nullable: false,
defaultValue: 0.0);
migrationBuilder.AddColumn<string>(
name: "StripePriceId",
table: "Subscriptions",
type: "longtext",
nullable: false)
.Annotation("MySql:CharSet", "utf8mb4");
migrationBuilder.AddColumn<string>(
name: "StripeProductId",
table: "Subscriptions",
type: "longtext",
nullable: false)
.Annotation("MySql:CharSet", "utf8mb4");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "SubscriptionExpires",
table: "Users");
migrationBuilder.DropColumn(
name: "Currency",
table: "Subscriptions");
migrationBuilder.DropColumn(
name: "Duration",
table: "Subscriptions");
migrationBuilder.DropColumn(
name: "Price",
table: "Subscriptions");
migrationBuilder.DropColumn(
name: "StripePriceId",
table: "Subscriptions");
migrationBuilder.DropColumn(
name: "StripeProductId",
table: "Subscriptions");
migrationBuilder.AddColumn<int>(
name: "SubscriptionDuration",
table: "Users",
type: "int",
nullable: false,
defaultValue: 0);
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,28 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Moonlight.App.Database.Migrations
{
/// <inheritdoc />
public partial class AddPermissions : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<byte[]>(
name: "Permissions",
table: "Users",
type: "longblob",
nullable: false);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "Permissions",
table: "Users");
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,68 @@
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Moonlight.App.Database.Migrations
{
/// <inheritdoc />
public partial class AddPermissionGroup : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<int>(
name: "PermissionGroupId",
table: "Users",
type: "int",
nullable: true);
migrationBuilder.CreateTable(
name: "PermissionGroups",
columns: table => new
{
Id = table.Column<int>(type: "int", nullable: false)
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
Name = table.Column<string>(type: "longtext", nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
Permissions = table.Column<byte[]>(type: "longblob", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_PermissionGroups", x => x.Id);
})
.Annotation("MySql:CharSet", "utf8mb4");
migrationBuilder.CreateIndex(
name: "IX_Users_PermissionGroupId",
table: "Users",
column: "PermissionGroupId");
migrationBuilder.AddForeignKey(
name: "FK_Users_PermissionGroups_PermissionGroupId",
table: "Users",
column: "PermissionGroupId",
principalTable: "PermissionGroups",
principalColumn: "Id");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_Users_PermissionGroups_PermissionGroupId",
table: "Users");
migrationBuilder.DropTable(
name: "PermissionGroups");
migrationBuilder.DropIndex(
name: "IX_Users_PermissionGroupId",
table: "Users");
migrationBuilder.DropColumn(
name: "PermissionGroupId",
table: "Users");
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,111 @@
using System;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Moonlight.App.Database.Migrations
{
/// <inheritdoc />
public partial class RemovedOldLogsAndAddedErrorLog : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "AuditLog");
migrationBuilder.DropTable(
name: "ErrorLog");
migrationBuilder.DropTable(
name: "SecurityLog");
migrationBuilder.CreateTable(
name: "SecurityLogs",
columns: table => new
{
Id = table.Column<int>(type: "int", nullable: false)
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
Text = table.Column<string>(type: "longtext", nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
CreatedAt = table.Column<DateTime>(type: "datetime(6)", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_SecurityLogs", x => x.Id);
})
.Annotation("MySql:CharSet", "utf8mb4");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "SecurityLogs");
migrationBuilder.CreateTable(
name: "AuditLog",
columns: table => new
{
Id = table.Column<int>(type: "int", nullable: false)
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
CreatedAt = table.Column<DateTime>(type: "datetime(6)", nullable: false),
Ip = table.Column<string>(type: "longtext", nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
JsonData = table.Column<string>(type: "longtext", nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
System = table.Column<bool>(type: "tinyint(1)", nullable: false),
Type = table.Column<int>(type: "int", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_AuditLog", x => x.Id);
})
.Annotation("MySql:CharSet", "utf8mb4");
migrationBuilder.CreateTable(
name: "ErrorLog",
columns: table => new
{
Id = table.Column<int>(type: "int", nullable: false)
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
Class = table.Column<string>(type: "longtext", nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
CreatedAt = table.Column<DateTime>(type: "datetime(6)", nullable: false),
Ip = table.Column<string>(type: "longtext", nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
JsonData = table.Column<string>(type: "longtext", nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
Stacktrace = table.Column<string>(type: "longtext", nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
System = table.Column<bool>(type: "tinyint(1)", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_ErrorLog", x => x.Id);
})
.Annotation("MySql:CharSet", "utf8mb4");
migrationBuilder.CreateTable(
name: "SecurityLog",
columns: table => new
{
Id = table.Column<int>(type: "int", nullable: false)
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
CreatedAt = table.Column<DateTime>(type: "datetime(6)", nullable: false),
Ip = table.Column<string>(type: "longtext", nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
JsonData = table.Column<string>(type: "longtext", nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
System = table.Column<bool>(type: "tinyint(1)", nullable: false),
Type = table.Column<int>(type: "int", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_SecurityLog", x => x.Id);
})
.Annotation("MySql:CharSet", "utf8mb4");
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,59 @@
using System;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Moonlight.App.Database.Migrations
{
/// <inheritdoc />
public partial class AddIpRules : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "BlocklistIps",
columns: table => new
{
Id = table.Column<int>(type: "int", nullable: false)
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
Ip = table.Column<string>(type: "longtext", nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
ExpiresAt = table.Column<DateTime>(type: "datetime(6)", nullable: false),
Packets = table.Column<long>(type: "bigint", nullable: false),
CreatedAt = table.Column<DateTime>(type: "datetime(6)", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_BlocklistIps", x => x.Id);
})
.Annotation("MySql:CharSet", "utf8mb4");
migrationBuilder.CreateTable(
name: "WhitelistIps",
columns: table => new
{
Id = table.Column<int>(type: "int", nullable: false)
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
Ip = table.Column<string>(type: "longtext", nullable: false)
.Annotation("MySql:CharSet", "utf8mb4")
},
constraints: table =>
{
table.PrimaryKey("PK_WhitelistIps", x => x.Id);
})
.Annotation("MySql:CharSet", "utf8mb4");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "BlocklistIps");
migrationBuilder.DropTable(
name: "WhitelistIps");
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,117 @@
using System;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Moonlight.App.Database.Migrations
{
/// <inheritdoc />
public partial class AddNewTicketModels : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "Tickets",
columns: table => new
{
Id = table.Column<int>(type: "int", nullable: false)
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
IssueTopic = table.Column<string>(type: "longtext", nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
IssueDescription = table.Column<string>(type: "longtext", nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
IssueTries = table.Column<string>(type: "longtext", nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
CreatedById = table.Column<int>(type: "int", nullable: false),
AssignedToId = table.Column<int>(type: "int", nullable: true),
Priority = table.Column<int>(type: "int", nullable: false),
Status = table.Column<int>(type: "int", nullable: false),
Subject = table.Column<int>(type: "int", nullable: false),
SubjectId = table.Column<int>(type: "int", nullable: false),
CreatedAt = table.Column<DateTime>(type: "datetime(6)", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Tickets", x => x.Id);
table.ForeignKey(
name: "FK_Tickets_Users_AssignedToId",
column: x => x.AssignedToId,
principalTable: "Users",
principalColumn: "Id");
table.ForeignKey(
name: "FK_Tickets_Users_CreatedById",
column: x => x.CreatedById,
principalTable: "Users",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
})
.Annotation("MySql:CharSet", "utf8mb4");
migrationBuilder.CreateTable(
name: "TicketMessages",
columns: table => new
{
Id = table.Column<int>(type: "int", nullable: false)
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
Content = table.Column<string>(type: "longtext", nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
AttachmentUrl = table.Column<string>(type: "longtext", nullable: true)
.Annotation("MySql:CharSet", "utf8mb4"),
SenderId = table.Column<int>(type: "int", nullable: true),
IsSystemMessage = table.Column<bool>(type: "tinyint(1)", nullable: false),
IsEdited = table.Column<bool>(type: "tinyint(1)", nullable: false),
IsSupportMessage = table.Column<bool>(type: "tinyint(1)", nullable: false),
CreatedAt = table.Column<DateTime>(type: "datetime(6)", nullable: false),
UpdatedAt = table.Column<DateTime>(type: "datetime(6)", nullable: false),
TicketId = table.Column<int>(type: "int", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_TicketMessages", x => x.Id);
table.ForeignKey(
name: "FK_TicketMessages_Tickets_TicketId",
column: x => x.TicketId,
principalTable: "Tickets",
principalColumn: "Id");
table.ForeignKey(
name: "FK_TicketMessages_Users_SenderId",
column: x => x.SenderId,
principalTable: "Users",
principalColumn: "Id");
})
.Annotation("MySql:CharSet", "utf8mb4");
migrationBuilder.CreateIndex(
name: "IX_TicketMessages_SenderId",
table: "TicketMessages",
column: "SenderId");
migrationBuilder.CreateIndex(
name: "IX_TicketMessages_TicketId",
table: "TicketMessages",
column: "TicketId");
migrationBuilder.CreateIndex(
name: "IX_Tickets_AssignedToId",
table: "Tickets",
column: "AssignedToId");
migrationBuilder.CreateIndex(
name: "IX_Tickets_CreatedById",
table: "Tickets",
column: "CreatedById");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "TicketMessages");
migrationBuilder.DropTable(
name: "Tickets");
}
}
}

View File

@@ -19,6 +19,30 @@ namespace Moonlight.App.Database.Migrations
.HasAnnotation("ProductVersion", "7.0.3") .HasAnnotation("ProductVersion", "7.0.3")
.HasAnnotation("Relational:MaxIdentifierLength", 64); .HasAnnotation("Relational:MaxIdentifierLength", 64);
modelBuilder.Entity("Moonlight.App.Database.Entities.BlocklistIp", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)");
b.Property<DateTime>("ExpiresAt")
.HasColumnType("datetime(6)");
b.Property<string>("Ip")
.IsRequired()
.HasColumnType("longtext");
b.Property<long>("Packets")
.HasColumnType("bigint");
b.HasKey("Id");
b.ToTable("BlocklistIps");
});
modelBuilder.Entity("Moonlight.App.Database.Entities.CloudPanel", b => modelBuilder.Entity("Moonlight.App.Database.Entities.CloudPanel", b =>
{ {
b.Property<int>("Id") b.Property<int>("Id")
@@ -241,95 +265,6 @@ namespace Moonlight.App.Database.Migrations
b.ToTable("LoadingMessages"); b.ToTable("LoadingMessages");
}); });
modelBuilder.Entity("Moonlight.App.Database.Entities.LogsEntries.AuditLogEntry", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)");
b.Property<string>("Ip")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("JsonData")
.IsRequired()
.HasColumnType("longtext");
b.Property<bool>("System")
.HasColumnType("tinyint(1)");
b.Property<int>("Type")
.HasColumnType("int");
b.HasKey("Id");
b.ToTable("AuditLog");
});
modelBuilder.Entity("Moonlight.App.Database.Entities.LogsEntries.ErrorLogEntry", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<string>("Class")
.IsRequired()
.HasColumnType("longtext");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)");
b.Property<string>("Ip")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("JsonData")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("Stacktrace")
.IsRequired()
.HasColumnType("longtext");
b.Property<bool>("System")
.HasColumnType("tinyint(1)");
b.HasKey("Id");
b.ToTable("ErrorLog");
});
modelBuilder.Entity("Moonlight.App.Database.Entities.LogsEntries.SecurityLogEntry", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)");
b.Property<string>("Ip")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("JsonData")
.IsRequired()
.HasColumnType("longtext");
b.Property<bool>("System")
.HasColumnType("tinyint(1)");
b.Property<int>("Type")
.HasColumnType("int");
b.HasKey("Id");
b.ToTable("SecurityLog");
});
modelBuilder.Entity("Moonlight.App.Database.Entities.MySqlDatabase", b => modelBuilder.Entity("Moonlight.App.Database.Entities.MySqlDatabase", b =>
{ {
b.Property<int>("Id") b.Property<int>("Id")
@@ -475,6 +410,25 @@ namespace Moonlight.App.Database.Migrations
b.ToTable("NotificationClients"); b.ToTable("NotificationClients");
}); });
modelBuilder.Entity("Moonlight.App.Database.Entities.PermissionGroup", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("longtext");
b.Property<byte[]>("Permissions")
.IsRequired()
.HasColumnType("longblob");
b.HasKey("Id");
b.ToTable("PermissionGroups");
});
modelBuilder.Entity("Moonlight.App.Database.Entities.Revoke", b => modelBuilder.Entity("Moonlight.App.Database.Entities.Revoke", b =>
{ {
b.Property<int>("Id") b.Property<int>("Id")
@@ -490,6 +444,24 @@ namespace Moonlight.App.Database.Migrations
b.ToTable("Revokes"); b.ToTable("Revokes");
}); });
modelBuilder.Entity("Moonlight.App.Database.Entities.SecurityLog", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)");
b.Property<string>("Text")
.IsRequired()
.HasColumnType("longtext");
b.HasKey("Id");
b.ToTable("SecurityLogs");
});
modelBuilder.Entity("Moonlight.App.Database.Entities.Server", b => modelBuilder.Entity("Moonlight.App.Database.Entities.Server", b =>
{ {
b.Property<int>("Id") b.Property<int>("Id")
@@ -663,10 +635,16 @@ namespace Moonlight.App.Database.Migrations
.ValueGeneratedOnAdd() .ValueGeneratedOnAdd()
.HasColumnType("int"); .HasColumnType("int");
b.Property<int>("Currency")
.HasColumnType("int");
b.Property<string>("Description") b.Property<string>("Description")
.IsRequired() .IsRequired()
.HasColumnType("longtext"); .HasColumnType("longtext");
b.Property<int>("Duration")
.HasColumnType("int");
b.Property<string>("LimitsJson") b.Property<string>("LimitsJson")
.IsRequired() .IsRequired()
.HasColumnType("longtext"); .HasColumnType("longtext");
@@ -675,6 +653,17 @@ namespace Moonlight.App.Database.Migrations
.IsRequired() .IsRequired()
.HasColumnType("longtext"); .HasColumnType("longtext");
b.Property<double>("Price")
.HasColumnType("double");
b.Property<string>("StripePriceId")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("StripeProductId")
.IsRequired()
.HasColumnType("longtext");
b.HasKey("Id"); b.HasKey("Id");
b.ToTable("Subscriptions"); b.ToTable("Subscriptions");
@@ -725,6 +714,97 @@ namespace Moonlight.App.Database.Migrations
b.ToTable("SupportChatMessages"); b.ToTable("SupportChatMessages");
}); });
modelBuilder.Entity("Moonlight.App.Database.Entities.Ticket", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<int?>("AssignedToId")
.HasColumnType("int");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)");
b.Property<int>("CreatedById")
.HasColumnType("int");
b.Property<string>("IssueDescription")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("IssueTopic")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("IssueTries")
.IsRequired()
.HasColumnType("longtext");
b.Property<int>("Priority")
.HasColumnType("int");
b.Property<int>("Status")
.HasColumnType("int");
b.Property<int>("Subject")
.HasColumnType("int");
b.Property<int>("SubjectId")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("AssignedToId");
b.HasIndex("CreatedById");
b.ToTable("Tickets");
});
modelBuilder.Entity("Moonlight.App.Database.Entities.TicketMessage", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<string>("AttachmentUrl")
.HasColumnType("longtext");
b.Property<string>("Content")
.IsRequired()
.HasColumnType("longtext");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)");
b.Property<bool>("IsEdited")
.HasColumnType("tinyint(1)");
b.Property<bool>("IsSupportMessage")
.HasColumnType("tinyint(1)");
b.Property<bool>("IsSystemMessage")
.HasColumnType("tinyint(1)");
b.Property<int?>("SenderId")
.HasColumnType("int");
b.Property<int?>("TicketId")
.HasColumnType("int");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("datetime(6)");
b.HasKey("Id");
b.HasIndex("SenderId");
b.HasIndex("TicketId");
b.ToTable("TicketMessages");
});
modelBuilder.Entity("Moonlight.App.Database.Entities.User", b => modelBuilder.Entity("Moonlight.App.Database.Entities.User", b =>
{ {
b.Property<int>("Id") b.Property<int>("Id")
@@ -781,6 +861,13 @@ namespace Moonlight.App.Database.Migrations
.IsRequired() .IsRequired()
.HasColumnType("longtext"); .HasColumnType("longtext");
b.Property<int?>("PermissionGroupId")
.HasColumnType("int");
b.Property<byte[]>("Permissions")
.IsRequired()
.HasColumnType("longblob");
b.Property<int>("Rating") b.Property<int>("Rating")
.HasColumnType("int"); .HasColumnType("int");
@@ -802,8 +889,8 @@ namespace Moonlight.App.Database.Migrations
b.Property<bool>("StreamerMode") b.Property<bool>("StreamerMode")
.HasColumnType("tinyint(1)"); .HasColumnType("tinyint(1)");
b.Property<int>("SubscriptionDuration") b.Property<DateTime>("SubscriptionExpires")
.HasColumnType("int"); .HasColumnType("datetime(6)");
b.Property<DateTime>("SubscriptionSince") b.Property<DateTime>("SubscriptionSince")
.HasColumnType("datetime(6)"); .HasColumnType("datetime(6)");
@@ -828,6 +915,8 @@ namespace Moonlight.App.Database.Migrations
b.HasIndex("CurrentSubscriptionId"); b.HasIndex("CurrentSubscriptionId");
b.HasIndex("PermissionGroupId");
b.ToTable("Users"); b.ToTable("Users");
}); });
@@ -868,6 +957,21 @@ namespace Moonlight.App.Database.Migrations
b.ToTable("WebSpaces"); b.ToTable("WebSpaces");
}); });
modelBuilder.Entity("Moonlight.App.Database.Entities.WhitelistIp", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<string>("Ip")
.IsRequired()
.HasColumnType("longtext");
b.HasKey("Id");
b.ToTable("WhitelistIps");
});
modelBuilder.Entity("Moonlight.App.Database.Entities.DdosAttack", b => modelBuilder.Entity("Moonlight.App.Database.Entities.DdosAttack", b =>
{ {
b.HasOne("Moonlight.App.Database.Entities.Node", "Node") b.HasOne("Moonlight.App.Database.Entities.Node", "Node")
@@ -1026,13 +1130,49 @@ namespace Moonlight.App.Database.Migrations
b.Navigation("Sender"); b.Navigation("Sender");
}); });
modelBuilder.Entity("Moonlight.App.Database.Entities.Ticket", b =>
{
b.HasOne("Moonlight.App.Database.Entities.User", "AssignedTo")
.WithMany()
.HasForeignKey("AssignedToId");
b.HasOne("Moonlight.App.Database.Entities.User", "CreatedBy")
.WithMany()
.HasForeignKey("CreatedById")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("AssignedTo");
b.Navigation("CreatedBy");
});
modelBuilder.Entity("Moonlight.App.Database.Entities.TicketMessage", b =>
{
b.HasOne("Moonlight.App.Database.Entities.User", "Sender")
.WithMany()
.HasForeignKey("SenderId");
b.HasOne("Moonlight.App.Database.Entities.Ticket", null)
.WithMany("Messages")
.HasForeignKey("TicketId");
b.Navigation("Sender");
});
modelBuilder.Entity("Moonlight.App.Database.Entities.User", b => modelBuilder.Entity("Moonlight.App.Database.Entities.User", b =>
{ {
b.HasOne("Moonlight.App.Database.Entities.Subscription", "CurrentSubscription") b.HasOne("Moonlight.App.Database.Entities.Subscription", "CurrentSubscription")
.WithMany() .WithMany()
.HasForeignKey("CurrentSubscriptionId"); .HasForeignKey("CurrentSubscriptionId");
b.HasOne("Moonlight.App.Database.Entities.PermissionGroup", "PermissionGroup")
.WithMany()
.HasForeignKey("PermissionGroupId");
b.Navigation("CurrentSubscription"); b.Navigation("CurrentSubscription");
b.Navigation("PermissionGroup");
}); });
modelBuilder.Entity("Moonlight.App.Database.Entities.WebSpace", b => modelBuilder.Entity("Moonlight.App.Database.Entities.WebSpace", b =>
@@ -1075,6 +1215,11 @@ namespace Moonlight.App.Database.Migrations
b.Navigation("Variables"); b.Navigation("Variables");
}); });
modelBuilder.Entity("Moonlight.App.Database.Entities.Ticket", b =>
{
b.Navigation("Messages");
});
modelBuilder.Entity("Moonlight.App.Database.Entities.WebSpace", b => modelBuilder.Entity("Moonlight.App.Database.Entities.WebSpace", b =>
{ {
b.Navigation("Databases"); b.Navigation("Databases");

View File

@@ -0,0 +1,119 @@
using System.Diagnostics;
using System.IO.Compression;
using Microsoft.EntityFrameworkCore;
using Moonlight.App.Database;
using Moonlight.App.Services;
using MySql.Data.MySqlClient;
namespace Moonlight.App.Helpers;
public class BackupHelper
{
public async Task CreateBackup(string path)
{
Logger.Info("Started moonlight backup creation");
Logger.Info($"This backup will be saved to '{path}'");
var stopWatch = new Stopwatch();
stopWatch.Start();
var cachePath = PathBuilder.Dir("storage", "backups", "cache");
Directory.CreateDirectory(cachePath);
//
// Exporting database
//
Logger.Info("Exporting database");
var configService = new ConfigService(new());
var dataContext = new DataContext(configService);
await using MySqlConnection conn = new MySqlConnection(dataContext.Database.GetConnectionString());
await using MySqlCommand cmd = new MySqlCommand();
using MySqlBackup mb = new MySqlBackup(cmd);
cmd.Connection = conn;
await conn.OpenAsync();
mb.ExportToFile(PathBuilder.File(cachePath, "database.sql"));
await conn.CloseAsync();
//
// Saving config
//
Logger.Info("Saving configuration");
File.Copy(
PathBuilder.File("storage", "configs", "config.json"),
PathBuilder.File(cachePath, "config.json"));
//
// Saving all storage items needed to restore the panel
//
Logger.Info("Saving resources");
CopyDirectory(
PathBuilder.Dir("storage", "resources"),
PathBuilder.Dir(cachePath, "resources"));
Logger.Info("Saving logs");
CopyDirectory(
PathBuilder.Dir("storage", "logs"),
PathBuilder.Dir(cachePath, "logs"));
Logger.Info("Saving uploads");
CopyDirectory(
PathBuilder.Dir("storage", "uploads"),
PathBuilder.Dir(cachePath, "uploads"));
//
// Compressing the backup to a single file
//
Logger.Info("Compressing");
ZipFile.CreateFromDirectory(cachePath,
path,
CompressionLevel.Fastest,
false);
Directory.Delete(cachePath, true);
stopWatch.Stop();
Logger.Info($"Backup successfully created. Took {stopWatch.Elapsed.TotalSeconds} seconds");
}
private void CopyDirectory(string sourceDirName, string destDirName, bool copySubDirs = true)
{
DirectoryInfo dir = new DirectoryInfo(sourceDirName);
if (!dir.Exists)
{
throw new DirectoryNotFoundException($"Source directory does not exist or could not be found: {sourceDirName}");
}
if (!Directory.Exists(destDirName))
{
Directory.CreateDirectory(destDirName);
}
FileInfo[] files = dir.GetFiles();
foreach (FileInfo file in files)
{
string tempPath = Path.Combine(destDirName, file.Name);
file.CopyTo(tempPath, false);
}
if (copySubDirs)
{
DirectoryInfo[] dirs = dir.GetDirectories();
foreach (DirectoryInfo subdir in dirs)
{
string tempPath = Path.Combine(destDirName, subdir.Name);
CopyDirectory(subdir.FullName, tempPath, copySubDirs);
}
}
}
}

View File

@@ -0,0 +1,88 @@
namespace Moonlight.App.Helpers;
public class BitHelper
{
public static bool ReadBit(byte[] byteArray, int bitIndex)
{
if (bitIndex < 0)
throw new ArgumentOutOfRangeException("bitIndex");
int byteIndex = bitIndex / 8;
if (byteIndex >= byteArray.Length)
throw new ArgumentOutOfRangeException("bitIndex");
int bitNumber = bitIndex % 8;
byte mask = (byte)(1 << bitNumber);
return (byteArray[byteIndex] & mask) != 0;
}
public static byte[] WriteBit(byte[] byteArray, int bitIndex, bool value)
{
if (bitIndex < 0)
throw new ArgumentOutOfRangeException("bitIndex");
int byteIndex = bitIndex / 8;
byte[] resultArray;
if (byteIndex >= byteArray.Length)
{
// Create a new array with increased size and copy elements from old array
resultArray = new byte[byteIndex + 1];
Array.Copy(byteArray, resultArray, byteArray.Length);
}
else
{
// Create a new array and copy elements from old array
resultArray = new byte[byteArray.Length];
Array.Copy(byteArray, resultArray, byteArray.Length);
}
int bitNumber = bitIndex % 8;
byte mask = (byte)(1 << bitNumber);
if (value)
resultArray[byteIndex] |= mask; // Set the bit to 1
else
resultArray[byteIndex] &= (byte)~mask; // Set the bit to 0
return resultArray;
}
public static byte[] OverwriteByteArrays(byte[] targetArray, byte[] overwriteArray)
{
int targetLength = targetArray.Length;
int overwriteLength = overwriteArray.Length;
int maxLength = Math.Max(targetLength, overwriteLength);
byte[] resultArray = new byte[maxLength];
for (int i = 0; i < maxLength; i++)
{
byte targetByte = i < targetLength ? targetArray[i] : (byte)0;
byte overwriteByte = i < overwriteLength ? overwriteArray[i] : (byte)0;
for (int j = 0; j < 8; j++)
{
bool overwriteBit = (overwriteByte & (1 << j)) != 0;
if (i < targetLength)
{
bool targetBit = (targetByte & (1 << j)) != 0;
if (overwriteBit)
{
targetByte = targetBit ? (byte)(targetByte | (1 << j)) : (byte)(targetByte & ~(1 << j));
}
}
else if (overwriteBit)
{
targetByte |= (byte)(1 << j);
}
}
resultArray[i] = targetByte;
}
return resultArray;
}
}

View File

@@ -0,0 +1,56 @@
namespace Moonlight.App.Helpers;
public class ByteSizeValue
{
public long Bytes { get; set; }
public long KiloBytes
{
get => Bytes / 1024;
set => Bytes = value * 1024;
}
public long MegaBytes
{
get => KiloBytes / 1024;
set => KiloBytes = value * 1024;
}
public long GigaBytes
{
get => MegaBytes / 1024;
set => MegaBytes = value * 1024;
}
public static ByteSizeValue FromBytes(long bytes)
{
return new()
{
Bytes = bytes
};
}
public static ByteSizeValue FromKiloBytes(long kiloBytes)
{
return new()
{
KiloBytes = kiloBytes
};
}
public static ByteSizeValue FromMegaBytes(long megaBytes)
{
return new()
{
MegaBytes = megaBytes
};
}
public static ByteSizeValue FromGigaBytes(long gigaBytes)
{
return new()
{
GigaBytes = gigaBytes
};
}
}

View File

@@ -0,0 +1,12 @@
using Microsoft.AspNetCore.Components;
namespace Moonlight.App.Helpers;
public class ComponentHelper
{
public static RenderFragment FromType(Type type) => builder =>
{
builder.OpenComponent(0, type);
builder.CloseComponent();
};
}

View File

@@ -45,7 +45,18 @@ public class DatabaseCheckupService
{ {
Logger.Info($"{migrations.Length} migrations pending. Updating now"); Logger.Info($"{migrations.Length} migrations pending. Updating now");
await BackupDatabase(); try
{
var backupHelper = new BackupHelper();
await backupHelper.CreateBackup(
PathBuilder.File("storage", "backups", $"{new DateTimeOffset(DateTime.UtcNow).ToUnixTimeMilliseconds()}.zip"));
}
catch (Exception e)
{
Logger.Fatal("Unable to create backup");
Logger.Fatal(e);
Logger.Fatal("Moonlight will continue to start and apply the migrations without a backup");
}
Logger.Info("Applying migrations"); Logger.Info("Applying migrations");
@@ -58,53 +69,4 @@ public class DatabaseCheckupService
Logger.Info("Database is up-to-date. No migrations have been performed"); Logger.Info("Database is up-to-date. No migrations have been performed");
} }
} }
public async Task BackupDatabase()
{
Logger.Info("Creating backup from database");
var configService = new ConfigService(new StorageService());
var dateTimeService = new DateTimeService();
var config = configService.Get().Moonlight.Database;
var connectionString = $"host={config.Host};" +
$"port={config.Port};" +
$"database={config.Database};" +
$"uid={config.Username};" +
$"pwd={config.Password}";
string file = PathBuilder.File("storage", "backups", $"{dateTimeService.GetCurrentUnix()}-mysql.sql");
Logger.Info($"Saving it to: {file}");
Logger.Info("Starting backup...");
try
{
var sw = new Stopwatch();
sw.Start();
await using MySqlConnection conn = new MySqlConnection(connectionString);
await using MySqlCommand cmd = new MySqlCommand();
using MySqlBackup mb = new MySqlBackup(cmd);
cmd.Connection = conn;
await conn.OpenAsync();
mb.ExportToFile(file);
await conn.CloseAsync();
sw.Stop();
Logger.Info($"Done. {sw.Elapsed.TotalSeconds}s");
}
catch (Exception e)
{
Logger.Fatal("-----------------------------------------------");
Logger.Fatal("Unable to create backup for moonlight database");
Logger.Fatal("Moonlight will start anyways in 30 seconds");
Logger.Fatal("-----------------------------------------------");
Logger.Fatal(e);
Thread.Sleep(TimeSpan.FromSeconds(30));
}
}
} }

View File

@@ -43,19 +43,22 @@ public class WingsFileAccess : FileAccess
$"api/servers/{Server.Uuid}/files/list-directory?directory={CurrentPath}" $"api/servers/{Server.Uuid}/files/list-directory?directory={CurrentPath}"
); );
var x = new List<FileData>(); var result = new List<FileData>();
foreach (var response in res) foreach (var resGrouped in res.GroupBy(x => x.Directory))
{ {
x.Add(new() foreach (var resItem in resGrouped.OrderBy(x => x.Name))
{ {
Name = response.Name, result.Add(new()
Size = response.File ? response.Size : 0, {
IsFile = response.File, Name = resItem.Name,
Size = resItem.File ? resItem.Size : 0,
IsFile = resItem.File,
}); });
} }
}
return x.ToArray(); return result.ToArray();
} }
public override Task Cd(string dir) public override Task Cd(string dir)

View File

@@ -1,4 +1,5 @@
using System.Text; using System.Text;
using Microsoft.AspNetCore.Components;
using Moonlight.App.Services; using Moonlight.App.Services;
namespace Moonlight.App.Helpers; namespace Moonlight.App.Helpers;
@@ -156,11 +157,47 @@ public static class Formatter
} }
} }
public static double BytesToGb(long bytes) public static double CalculateAverage(List<double> values)
{ {
const double gbMultiplier = 1024 * 1024 * 1024; // 1 GB = 1024 MB * 1024 KB * 1024 B if (values == null || values.Count == 0)
{
throw new ArgumentException("The list cannot be null or empty.");
}
double gigabytes = (double)bytes / gbMultiplier; double sum = 0;
return gigabytes; foreach (double value in values)
{
sum += value;
}
return sum / values.Count;
}
public static double CalculatePercentage(double part, double total)
{
if (total == 0)
{
return 0;
}
return (part / total) * 100;
}
public static RenderFragment FormatLineBreaks(string content)
{
return builder =>
{
int i = 0;
var arr = content.Split("\n", StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
foreach (var line in arr)
{
builder.AddContent(i, line);
if (i++ != arr.Length - 1)
{
builder.AddMarkupContent(i, "<br/>");
}
}
};
} }
} }

View File

@@ -1,46 +1,70 @@
using System.Diagnostics; using System.Diagnostics;
using System.Reflection; using System.Reflection;
using Moonlight.App.Database;
using Moonlight.App.Services;
using Moonlight.App.Services.Files;
using Serilog; using Serilog;
namespace Moonlight.App.Helpers; namespace Moonlight.App.Helpers;
public static class Logger public static class Logger
{ {
// The private static instance of the config service, because we have no di here
private static ConfigService ConfigService = new(new StorageService());
#region String method calls #region String method calls
public static void Verbose(string message, string channel = "default") public static void Verbose(string message, string channel = "default")
{ {
Log.ForContext("SourceContext", GetNameOfCallingClass()) Log.ForContext("SourceContext", GetNameOfCallingClass())
.Verbose("{Message}", message); .Verbose("{Message}", message);
if(channel == "security")
LogSecurityInDb(message);
} }
public static void Info(string message, string channel = "default") public static void Info(string message, string channel = "default")
{ {
Log.ForContext("SourceContext", GetNameOfCallingClass()) Log.ForContext("SourceContext", GetNameOfCallingClass())
.Information("{Message}", message); .Information("{Message}", message);
if(channel == "security")
LogSecurityInDb(message);
} }
public static void Debug(string message, string channel = "default") public static void Debug(string message, string channel = "default")
{ {
Log.ForContext("SourceContext", GetNameOfCallingClass()) Log.ForContext("SourceContext", GetNameOfCallingClass())
.Debug("{Message}", message); .Debug("{Message}", message);
if(channel == "security")
LogSecurityInDb(message);
} }
public static void Error(string message, string channel = "default") public static void Error(string message, string channel = "default")
{ {
Log.ForContext("SourceContext", GetNameOfCallingClass()) Log.ForContext("SourceContext", GetNameOfCallingClass())
.Error("{Message}", message); .Error("{Message}", message);
if(channel == "security")
LogSecurityInDb(message);
} }
public static void Warn(string message, string channel = "default") public static void Warn(string message, string channel = "default")
{ {
Log.ForContext("SourceContext", GetNameOfCallingClass()) Log.ForContext("SourceContext", GetNameOfCallingClass())
.Warning("{Message}", message); .Warning("{Message}", message);
if(channel == "security")
LogSecurityInDb(message);
} }
public static void Fatal(string message, string channel = "default") public static void Fatal(string message, string channel = "default")
{ {
Log.ForContext("SourceContext", GetNameOfCallingClass()) Log.ForContext("SourceContext", GetNameOfCallingClass())
.Fatal("{Message}", message); .Fatal("{Message}", message);
if(channel == "security")
LogSecurityInDb(message);
} }
#endregion #endregion
@@ -49,36 +73,54 @@ public static class Logger
{ {
Log.ForContext("SourceContext", GetNameOfCallingClass()) Log.ForContext("SourceContext", GetNameOfCallingClass())
.Verbose(exception, ""); .Verbose(exception, "");
if(channel == "security")
LogSecurityInDb(exception);
} }
public static void Info(Exception exception, string channel = "default") public static void Info(Exception exception, string channel = "default")
{ {
Log.ForContext("SourceContext", GetNameOfCallingClass()) Log.ForContext("SourceContext", GetNameOfCallingClass())
.Information(exception, ""); .Information(exception, "");
if(channel == "security")
LogSecurityInDb(exception);
} }
public static void Debug(Exception exception, string channel = "default") public static void Debug(Exception exception, string channel = "default")
{ {
Log.ForContext("SourceContext", GetNameOfCallingClass()) Log.ForContext("SourceContext", GetNameOfCallingClass())
.Debug(exception, ""); .Debug(exception, "");
if(channel == "security")
LogSecurityInDb(exception);
} }
public static void Error(Exception exception, string channel = "default") public static void Error(Exception exception, string channel = "default")
{ {
Log.ForContext("SourceContext", GetNameOfCallingClass()) Log.ForContext("SourceContext", GetNameOfCallingClass())
.Error(exception, ""); .Error(exception, "");
if(channel == "security")
LogSecurityInDb(exception);
} }
public static void Warn(Exception exception, string channel = "default") public static void Warn(Exception exception, string channel = "default")
{ {
Log.ForContext("SourceContext", GetNameOfCallingClass()) Log.ForContext("SourceContext", GetNameOfCallingClass())
.Warning(exception, ""); .Warning(exception, "");
if(channel == "security")
LogSecurityInDb(exception);
} }
public static void Fatal(Exception exception, string channel = "default") public static void Fatal(Exception exception, string channel = "default")
{ {
Log.ForContext("SourceContext", GetNameOfCallingClass()) Log.ForContext("SourceContext", GetNameOfCallingClass())
.Fatal(exception, ""); .Fatal(exception, "");
if(channel == "security")
LogSecurityInDb(exception);
} }
#endregion #endregion
@@ -105,4 +147,25 @@ public static class Logger
return fullName; return fullName;
} }
private static void LogSecurityInDb(Exception exception)
{
LogSecurityInDb(exception.ToStringDemystified());
}
private static void LogSecurityInDb(string text)
{
Task.Run(() =>
{
var dataContext = new DataContext(ConfigService);
dataContext.SecurityLogs.Add(new()
{
Text = text
});
dataContext.SaveChanges();
dataContext.Dispose();
});
}
} }

View File

@@ -43,4 +43,15 @@ public static class StringHelper
return firstChar + restOfString; return firstChar + restOfString;
} }
public static string CutInHalf(string input)
{
if (string.IsNullOrEmpty(input))
return input;
int length = input.Length;
int halfLength = length / 2;
return input.Substring(0, halfLength);
}
} }

View File

@@ -218,6 +218,16 @@ public class WingsConsole : IDisposable
break; break;
case "install output": case "install output":
if (ServerState != ServerState.Installing)
{
// Because wings is sending "install output" events BEFORE
// sending the "install started" event,
// we need to set the install state here
// See https://github.com/pterodactyl/panel/issues/4853
// for more details
await UpdateServerState(ServerState.Installing);
}
foreach (var line in eventData.Args) foreach (var line in eventData.Args)
{ {
await SaveMessage(line); await SaveMessage(line);

View File

@@ -25,7 +25,7 @@ public class AvatarController : Controller
try try
{ {
var url = GravatarController.GetImageUrl(user.Email, 100); var url = GravatarController.GetImageUrl(user.Email.ToLower(), 100);
using var client = new HttpClient(); using var client = new HttpClient();
var res = await client.GetByteArrayAsync(url); var res = await client.GetByteArrayAsync(url);

View File

@@ -0,0 +1,47 @@
using Microsoft.AspNetCore.Mvc;
using Moonlight.App.Services;
using Moonlight.App.Services.Sessions;
using Stripe;
using Stripe.Checkout;
namespace Moonlight.App.Http.Controllers.Api.Moonlight;
[ApiController]
[Route("api/moonlight/billing")]
public class BillingController : Controller
{
private readonly IdentityService IdentityService;
private readonly BillingService BillingService;
public BillingController(
IdentityService identityService,
BillingService billingService)
{
IdentityService = identityService;
BillingService = billingService;
}
[HttpGet("cancel")]
public async Task<ActionResult> Cancel()
{
var user = IdentityService.User;
if (user == null)
return Redirect("/login");
return Redirect("/profile/subscriptions/close");
}
[HttpGet("success")]
public async Task<ActionResult> Success()
{
var user = IdentityService.User;
if (user == null)
return Redirect("/login");
await BillingService.CompleteCheckout(user);
return Redirect("/profile/subscriptions/close");
}
}

View File

@@ -25,7 +25,7 @@ public class RegisterController : Controller
[HttpGet] [HttpGet]
public async Task<ActionResult<TokenRegister>> Register() public async Task<ActionResult<TokenRegister>> Register()
{ {
var user = await IdentityService.Get(); var user = IdentityService.User;
if (user == null) if (user == null)
return NotFound(); return NotFound();

View File

@@ -54,7 +54,7 @@ public class OAuth2Controller : Controller
{ {
try try
{ {
var currentUser = await IdentityService.Get(); var currentUser = IdentityService.User;
if (currentUser != null) if (currentUser != null)
{ {

View File

@@ -3,6 +3,7 @@ using Moonlight.App.Database.Entities;
using Moonlight.App.Events; using Moonlight.App.Events;
using Moonlight.App.Http.Requests.Daemon; using Moonlight.App.Http.Requests.Daemon;
using Moonlight.App.Repositories; using Moonlight.App.Repositories;
using Moonlight.App.Services.Background;
namespace Moonlight.App.Http.Controllers.Api.Remote; namespace Moonlight.App.Http.Controllers.Api.Remote;
@@ -10,19 +11,17 @@ namespace Moonlight.App.Http.Controllers.Api.Remote;
[Route("api/remote/ddos")] [Route("api/remote/ddos")]
public class DdosController : Controller public class DdosController : Controller
{ {
private readonly NodeRepository NodeRepository; private readonly Repository<Node> NodeRepository;
private readonly EventSystem Event; private readonly DdosProtectionService DdosProtectionService;
private readonly DdosAttackRepository DdosAttackRepository;
public DdosController(NodeRepository nodeRepository, EventSystem eventSystem, DdosAttackRepository ddosAttackRepository) public DdosController(Repository<Node> nodeRepository, DdosProtectionService ddosProtectionService)
{ {
NodeRepository = nodeRepository; NodeRepository = nodeRepository;
Event = eventSystem; DdosProtectionService = ddosProtectionService;
DdosAttackRepository = ddosAttackRepository;
} }
[HttpPost("update")] [HttpPost("start")]
public async Task<ActionResult> Update([FromBody] DdosStatus ddosStatus) public async Task<ActionResult> Start([FromBody] DdosStart ddosStart)
{ {
var tokenData = Request.Headers.Authorization.ToString().Replace("Bearer ", ""); var tokenData = Request.Headers.Authorization.ToString().Replace("Bearer ", "");
var id = tokenData.Split(".")[0]; var id = tokenData.Split(".")[0];
@@ -36,17 +35,25 @@ public class DdosController : Controller
if (token != node.Token) if (token != node.Token)
return Unauthorized(); return Unauthorized();
var ddosAttack = new DdosAttack() await DdosProtectionService.ProcessDdosSignal(ddosStart.Ip, ddosStart.Packets);
return Ok();
}
[HttpPost("stop")]
public async Task<ActionResult> Stop([FromBody] DdosStop ddosStop)
{ {
Ongoing = ddosStatus.Ongoing, var tokenData = Request.Headers.Authorization.ToString().Replace("Bearer ", "");
Data = ddosStatus.Data, var id = tokenData.Split(".")[0];
Ip = ddosStatus.Ip, var token = tokenData.Split(".")[1];
Node = node
};
ddosAttack = DdosAttackRepository.Add(ddosAttack); var node = NodeRepository.Get().FirstOrDefault(x => x.TokenId == id);
await Event.Emit("node.ddos", ddosAttack); if (node == null)
return NotFound();
if (token != node.Token)
return Unauthorized();
return Ok(); return Ok();
} }

View File

@@ -0,0 +1,7 @@
namespace Moonlight.App.Http.Requests.Daemon;
public class DdosStart
{
public string Ip { get; set; } = "";
public long Packets { get; set; }
}

View File

@@ -1,8 +0,0 @@
namespace Moonlight.App.Http.Requests.Daemon;
public class DdosStatus
{
public bool Ongoing { get; set; }
public long Data { get; set; }
public string Ip { get; set; } = "";
}

View File

@@ -0,0 +1,7 @@
namespace Moonlight.App.Http.Requests.Daemon;
public class DdosStop
{
public string Ip { get; set; } = "";
public long Traffic { get; set; }
}

View File

@@ -0,0 +1,54 @@
using Moonlight.App.Database.Entities;
using Moonlight.App.Models.Misc;
using Moonlight.App.Services;
namespace Moonlight.App.MalwareScans;
public class DiscordNukeScan : MalwareScan
{
public override string Name => "Discord nuke";
public override string Description => "Discord nuke bot detector";
public override async Task<MalwareScanResult?> Scan(Server server, IServiceProvider serviceProvider)
{
var serverService = serviceProvider.GetRequiredService<ServerService>();
var access = await serverService.CreateFileAccess(server, null!);
var files = await access.Ls();
var filteredFiles = files.Where(x =>
x.Name.EndsWith(".py") ||
x.Name.EndsWith(".js") ||
x.Name.EndsWith(".json") ||
x.Name.EndsWith(".env"));
foreach (var file in filteredFiles)
{
var content = await access.Read(file);
var filteredContent = content.ToLower();
if (filteredContent.Contains("quake") ||
filteredContent.Contains("nuked by") ||
filteredContent.Contains("nuke bot") ||
(filteredContent.Contains("fucked by") && filteredContent.Contains("nuke"))) // fucked by in context with nuke
{
return new()
{
Title = "Discord nuke bot",
Description = "Found suspicious content which may indicate there is a nuke bot running",
Author = "Marcel Baumgartner"
};
}
if (files.Any(x => x.Name == "nukes.json"))
{
return new()
{
Title = "Discord nuke bot",
Description = "Found suspicious content which may indicate there is a nuke bot running",
Author = "Marcel Baumgartner"
};
}
}
return null;
}
}

View File

@@ -0,0 +1,39 @@
using Moonlight.App.Database.Entities;
using Moonlight.App.Models.Misc;
using Moonlight.App.Services;
namespace Moonlight.App.MalwareScans;
public class FakePlayerPluginScan : MalwareScan
{
public override string Name => "Fake player plugin scan";
public override string Description => "This scan is a simple fake player plugin scan provided by moonlight";
public override async Task<MalwareScanResult?> Scan(Server server, IServiceProvider serviceProvider)
{
var serverService = serviceProvider.GetRequiredService<ServerService>();
var access = await serverService.CreateFileAccess(server, null!);
var fileElements = await access.Ls();
if (fileElements.Any(x => !x.IsFile && x.Name == "plugins")) // Check for plugins folder
{
await access.Cd("plugins");
fileElements = await access.Ls();
foreach (var fileElement in fileElements)
{
if (fileElement.Name.ToLower().Contains("fakeplayer"))
{
return new()
{
Title = "Fake player plugin",
Description = $"Suspicious plugin file: {fileElement.Name}",
Author = "Marcel Baumgartner"
};
}
}
}
return null;
}
}

View File

@@ -0,0 +1,37 @@
using Moonlight.App.Database.Entities;
using Moonlight.App.Models.Misc;
using Moonlight.App.Services;
namespace Moonlight.App.MalwareScans;
public class MinerJarScan : MalwareScan
{
public override string Name => "Miner jar scan";
public override string Description => "This scan is a simple miner jar scan provided by moonlight";
public override async Task<MalwareScanResult?> Scan(Server server, IServiceProvider serviceProvider)
{
var serverService = serviceProvider.GetRequiredService<ServerService>();
var access = await serverService.CreateFileAccess(server, null!);
var fileElements = await access.Ls();
if (fileElements.Any(x => x.Name == "libraries" && !x.IsFile))
{
await access.Cd("libraries");
fileElements = await access.Ls();
if (fileElements.Any(x => x.Name == "jdk" && !x.IsFile))
{
return new()
{
Title = "Found Miner",
Description = "Detected suspicious library directory which may contain a script for miners",
Author = "Marcel Baumgartner"
};
}
}
return null;
}
}

View File

@@ -0,0 +1,35 @@
using Moonlight.App.Database.Entities;
using Moonlight.App.Models.Misc;
using Moonlight.App.Services;
namespace Moonlight.App.MalwareScans;
public class MinerScan : MalwareScan
{
public override string Name => "Miner (NEZHA)";
public override string Description => "Probably a miner";
public override async Task<MalwareScanResult?> Scan(Server server, IServiceProvider serviceProvider)
{
var serverService = serviceProvider.GetRequiredService<ServerService>();
var access = await serverService.CreateFileAccess(server, null!);
var files = await access.Ls();
foreach (var file in files.Where(x => x.IsFile && (x.Name.EndsWith(".sh") || x.Name.EndsWith(".yml")) || x.Name == "bed"))
{
var content = await access.Read(file);
if (content.ToLower().Contains("nezha"))
{
return new()
{
Title = "Miner",
Description = "Miner start script (NEZHA)",
Author = "Marcel Baumgartner"
};
}
}
return null;
}
}

View File

@@ -0,0 +1,36 @@
using Moonlight.App.Database.Entities;
using Moonlight.App.Models.Misc;
using Moonlight.App.Services;
namespace Moonlight.App.MalwareScans;
public class ProxyScan : MalwareScan
{
public override string Name => "Proxy software";
public override string Description => "Software to use nodes as a proxy";
public override async Task<MalwareScanResult?> Scan(Server server, IServiceProvider serviceProvider)
{
var serverService = serviceProvider.GetRequiredService<ServerService>();
var access = await serverService.CreateFileAccess(server, null!);
var files = await access.Ls();
foreach (var file in files.Where(x => x.Name.EndsWith(".sh")))
{
var fileContent = await access.Read(file);
var processableContent = fileContent.ToLower();
if (processableContent.Contains("t-e-s-tweb"))
{
return new MalwareScanResult()
{
Title = "Proxy software",
Description = "Software to use nodes as a proxy",
Author = "Marcel Baumgartner"
};
}
}
return null;
}
}

View File

@@ -0,0 +1,35 @@
using Moonlight.App.Database.Entities;
using Moonlight.App.Models.Misc;
using Moonlight.App.Services;
namespace Moonlight.App.MalwareScans;
public class SelfBotCodeScan : MalwareScan
{
public override string Name => "Selfbot code scan";
public override string Description => "This scan is a simple selfbot code scan provided by moonlight";
public override async Task<MalwareScanResult?> Scan(Server server, IServiceProvider serviceProvider)
{
var serverService = serviceProvider.GetRequiredService<ServerService>();
var access = await serverService.CreateFileAccess(server, null!);
var fileElements = await access.Ls();
foreach (var script in fileElements.Where(x => x.Name.EndsWith(".py") && x.IsFile))
{
var rawScript = await access.Read(script);
if (rawScript.Contains("https://discord.com/api") && !rawScript.Contains("https://discord.com/api/oauth2") && !rawScript.Contains("https://discord.com/api/webhook") || rawScript.Contains("https://rblxwild.com")) //TODO: Export to plugins, add regex for checking
{
return new MalwareScanResult
{
Title = "Potential selfbot",
Description = $"Suspicious script file: {script.Name}",
Author = "Marcel Baumgartner"
};
}
}
return null;
}
}

View File

@@ -0,0 +1,30 @@
using Moonlight.App.Database.Entities;
using Moonlight.App.Models.Misc;
using Moonlight.App.Services;
namespace Moonlight.App.MalwareScans;
public class SelfBotScan : MalwareScan
{
public override string Name => "Selfbot Scan";
public override string Description => "This scan is a simple selfbot scan provided by moonlight";
public override async Task<MalwareScanResult?> Scan(Server server, IServiceProvider serviceProvider)
{
var serverService = serviceProvider.GetRequiredService<ServerService>();
var access = await serverService.CreateFileAccess(server, null!);
var fileElements = await access.Ls();
if (fileElements.Any(x => x.Name == "tokens.txt"))
{
return new MalwareScanResult
{
Title = "Found SelfBot",
Description = "Detected suspicious 'tokens.txt' file which may contain tokens for a selfbot",
Author = "Marcel Baumgartner"
};
}
return null;
}
}

View File

@@ -0,0 +1,21 @@
using System.ComponentModel.DataAnnotations;
using Moonlight.App.Models.Misc;
namespace Moonlight.App.Models.Forms;
public class CreateTicketDataModel
{
[Required(ErrorMessage = "You need to specify a issue topic")]
[MinLength(5, ErrorMessage = "The issue topic needs to be longer than 5 characters")]
public string IssueTopic { get; set; }
[Required(ErrorMessage = "You need to specify a issue description")]
[MinLength(10, ErrorMessage = "The issue description needs to be longer than 10 characters")]
public string IssueDescription { get; set; }
[Required(ErrorMessage = "You need to specify your tries to solve this issue")]
public string IssueTries { get; set; }
public TicketSubject Subject { get; set; }
public int SubjectId { get; set; }
}

View File

@@ -1,4 +1,5 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using Moonlight.App.Models.Misc;
namespace Moonlight.App.Models.Forms; namespace Moonlight.App.Models.Forms;
@@ -10,4 +11,8 @@ public class SubscriptionDataModel
[Required(ErrorMessage = "You need to enter a description")] [Required(ErrorMessage = "You need to enter a description")]
public string Description { get; set; } = ""; public string Description { get; set; } = "";
public double Price { get; set; } = 0;
public Currency Currency { get; set; } = Currency.USD;
public int Duration { get; set; } = 30;
} }

View File

@@ -0,0 +1,34 @@
using System.ComponentModel.DataAnnotations;
using Moonlight.App.Database.Entities;
using Moonlight.App.Models.Misc;
namespace Moonlight.App.Models.Forms;
public class UserEditDataModel
{
[Required]
public string FirstName { get; set; } = "";
[Required]
public string LastName { get; set; } = "";
[Required]
public string Email { get; set; } = "";
[Required]
public string Address { get; set; } = "";
[Required]
public string City { get; set; } = "";
[Required]
public string State { get; set; } = "";
[Required]
public string Country { get; set; } = "";
public bool Admin { get; set; }
public bool TotpEnabled { get; set; }
public ulong DiscordId { get; set; }
public PermissionGroup? PermissionGroup { get; set; }
}

View File

@@ -1,27 +0,0 @@
namespace Moonlight.App.Models.Misc;
public enum AuditLogType
{
Login,
Register,
ChangePassword,
ChangePowerState,
CreateBackup,
RestoreBackup,
DeleteBackup,
DownloadBackup,
CreateServer,
ReinstallServer,
CancelSubscription,
ApplySubscriptionCode,
EnableTotp,
DisableTotp,
AddDomainRecord,
UpdateDomainRecord,
DeleteDomainRecord,
PasswordReset,
CleanupEnabled,
CleanupDisabled,
CleanupTriggered,
PasswordChange,
}

View File

@@ -0,0 +1,7 @@
namespace Moonlight.App.Models.Misc;
public enum Currency
{
USD = 1,
EUR = 2
}

View File

@@ -0,0 +1,9 @@
using Moonlight.App.Helpers.Files;
namespace Moonlight.App.Models.Misc;
public class MailTemplate // This is just for the blazor table at /admin/system/mail
{
public string Name { get; set; } = "";
public FileData File { get; set; }
}

View File

@@ -0,0 +1,10 @@
using Moonlight.App.Database.Entities;
namespace Moonlight.App.Models.Misc;
public abstract class MalwareScan
{
public abstract string Name { get; }
public abstract string Description { get; }
public abstract Task<MalwareScanResult?> Scan(Server server, IServiceProvider serviceProvider);
}

View File

@@ -2,7 +2,7 @@
public class MalwareScanResult public class MalwareScanResult
{ {
public string Title { get; set; } = ""; public string Title { get; set; }
public string Description { get; set; } = ""; public string Description { get; set; } = "";
public string Author { get; set; } = ""; public string Author { get; set; } = "";
} }

View File

@@ -0,0 +1,6 @@
namespace Moonlight.App.Models.Misc;
public class OfficialMoonlightPlugin
{
public string Name { get; set; }
}

View File

@@ -1,9 +0,0 @@
namespace Moonlight.App.Models.Misc;
public enum SecurityLogType
{
ManipulatedJwt,
PathTransversal,
SftpBruteForce,
LoginFail
}

View File

@@ -0,0 +1,9 @@
namespace Moonlight.App.Models.Misc;
public enum TicketPriority
{
Low = 0,
Medium = 1,
High = 2,
Critical = 3
}

View File

@@ -0,0 +1,9 @@
namespace Moonlight.App.Models.Misc;
public enum TicketStatus
{
Closed = 0,
Open = 1,
WaitingForUser = 2,
Pending = 3
}

View File

@@ -0,0 +1,9 @@
namespace Moonlight.App.Models.Misc;
public enum TicketSubject
{
Webspace = 0,
Server = 1,
Domain = 2,
Other = 3
}

View File

@@ -0,0 +1,10 @@
namespace Moonlight.App.Perms;
public class Permission
{
public int Index { get; set; } = 0;
public string Name { get; set; } = "";
public string Description { get; set; } = "";
public static implicit operator int(Permission permission) => permission.Index;
}

View File

@@ -0,0 +1,11 @@
namespace Moonlight.App.Perms;
public class PermissionRequired : Attribute
{
public string Name { get; private set; }
public PermissionRequired(string name)
{
Name = name;
}
}

View File

@@ -0,0 +1,55 @@
using System.Data;
using Moonlight.App.Helpers;
namespace Moonlight.App.Perms;
public class PermissionStorage
{
public byte[] Data;
public bool IsReadyOnly;
public PermissionStorage(byte[] data, bool isReadyOnly = false)
{
Data = data;
IsReadyOnly = isReadyOnly;
}
public bool this[Permission permission]
{
get
{
try
{
return BitHelper.ReadBit(Data, permission.Index);
}
catch (ArgumentOutOfRangeException)
{
return false;
}
catch (Exception e)
{
Logger.Verbose("Error reading permissions. (Can be intentional)");
Logger.Verbose(e);
return false;
}
}
set
{
if (IsReadyOnly)
throw new ReadOnlyException();
Data = BitHelper.WriteBit(Data, permission.Index, value);
}
}
public bool HasAnyPermissions()
{
foreach (var permission in Permissions.GetAllPermissions())
{
if (this[permission])
return true;
}
return false;
}
}

View File

@@ -0,0 +1,452 @@
namespace Moonlight.App.Perms;
public static class Permissions
{
public static Permission AdminDashboard = new()
{
Index = 0,
Name = "Admin Dashboard",
Description = "Access the main admin dashboard page"
};
public static Permission AdminStatistics = new()
{
Index = 1,
Name = "Admin Statistics",
Description = "View statistical information about the moonlight instance"
};
public static Permission AdminSysPlugins = new()
{
Index = 2,
Name = "Admin system plugins",
Description = "View and install plugins"
};
public static Permission AdminDomains = new()
{
Index = 4,
Name = "Admin Domains",
Description = "Manage domains in the admin area"
};
public static Permission AdminNewDomain = new()
{
Index = 5,
Name = "Admin New Domain",
Description = "Create a new domain in the admin area"
};
public static Permission AdminSharedDomains = new()
{
Index = 6,
Name = "Admin Shared Domains",
Description = "Manage shared domains in the admin area"
};
public static Permission AdminNewSharedDomain = new()
{
Index = 7,
Name = "Admin New Shared Domain",
Description = "Create a new shared domain in the admin area"
};
public static Permission AdminNodeEdit = new()
{
Index = 9,
Name = "Admin Node Edit",
Description = "Edit node settings in the admin area"
};
public static Permission AdminNodes = new()
{
Index = 10,
Name = "Admin Node",
Description = "Access the node management page in the admin area"
};
public static Permission AdminNewNode = new()
{
Index = 11,
Name = "Admin New Node",
Description = "Create a new node in the admin area"
};
public static Permission AdminNodeSetup = new()
{
Index = 12,
Name = "Admin Node Setup",
Description = "Set up a node in the admin area"
};
public static Permission AdminNodeView = new()
{
Index = 13,
Name = "Admin Node View",
Description = "View node details in the admin area"
};
public static Permission AdminNotificationDebugging = new()
{
Index = 14,
Name = "Admin Notification Debugging",
Description = "Manage debugging notifications in the admin area"
};
public static Permission AdminServerCleanup = new()
{
Index = 15,
Name = "Admin Server Cleanup",
Description = "Perform server cleanup tasks in the admin area"
};
public static Permission AdminServerEdit = new()
{
Index = 16,
Name = "Admin Server Edit",
Description = "Edit server settings in the admin area"
};
public static Permission AdminServers = new()
{
Index = 17,
Name = "Admin Server",
Description = "Access the server management page in the admin area"
};
public static Permission AdminServerManager = new()
{
Index = 18,
Name = "Admin Server Manager",
Description = "Manage servers in the admin area"
};
public static Permission AdminNewServer = new()
{
Index = 19,
Name = "Admin New Server",
Description = "Create a new server in the admin area"
};
public static Permission AdminServerImageEdit = new()
{
Index = 20,
Name = "Admin Server Image Edit",
Description = "Edit server image settings in the admin area"
};
public static Permission AdminServerImages = new()
{
Index = 21,
Name = "Admin Server Images",
Description = "Access the server image management page in the admin area"
};
public static Permission AdminServerImageNew = new()
{
Index = 22,
Name = "Admin Server Image New",
Description = "Create a new server image in the admin area"
};
public static Permission AdminServerViewAllocations = new()
{
Index = 23,
Name = "Admin Server View Allocations",
Description = "View server allocations in the admin area"
};
public static Permission AdminServerViewArchive = new()
{
Index = 24,
Name = "Admin Server View Archive",
Description = "View server archive in the admin area"
};
public static Permission AdminServerViewDebug = new()
{
Index = 25,
Name = "Admin Server View Debug",
Description = "View server debugging information in the admin area"
};
public static Permission AdminServerViewImage = new()
{
Index = 26,
Name = "Admin Server View Image",
Description = "View server image details in the admin area"
};
public static Permission AdminServerViewIndex = new()
{
Index = 27,
Name = "Admin Server View",
Description = "Access the server view page in the admin area"
};
public static Permission AdminServerViewOverview = new()
{
Index = 28,
Name = "Admin Server View Overview",
Description = "View server overview in the admin area"
};
public static Permission AdminServerViewResources = new()
{
Index = 29,
Name = "Admin Server View Resources",
Description = "View server resources in the admin area"
};
public static Permission AdminSubscriptionEdit = new()
{
Index = 30,
Name = "Admin Subscription Edit",
Description = "Edit subscription settings in the admin area"
};
public static Permission AdminSubscriptions = new()
{
Index = 31,
Name = "Admin Subscriptions",
Description = "Access the subscription management page in the admin area"
};
public static Permission AdminNewSubscription = new()
{
Index = 32,
Name = "Admin New Subscription",
Description = "Create a new subscription in the admin area"
};
public static Permission AdminSupport = new()
{
Index = 33,
Name = "Admin Support",
Description = "Access the support page in the admin area"
};
public static Permission AdminSupportView = new()
{
Index = 34,
Name = "Admin Support View",
Description = "View support details in the admin area"
};
public static Permission AdminSysConfiguration = new()
{
Index = 35,
Name = "Admin system Configuration",
Description = "Access system configuration settings in the admin area"
};
public static Permission AdminSysDiscordBot = new()
{
Index = 36,
Name = "Admin system Discord Bot",
Description = "Manage Discord bot settings in the admin area"
};
public static Permission AdminSystem = new()
{
Index = 37,
Name = "Admin system",
Description = "Access the system management page in the admin area"
};
public static Permission AdminSysMail = new()
{
Index = 38,
Name = "Admin system Mail",
Description = "Manage mail settings in the admin area"
};
public static Permission AdminSecurityMalware = new()
{
Index = 39,
Name = "Admin security Malware",
Description = "Manage malware settings in the admin area"
};
public static Permission AdminSysResources = new()
{
Index = 40,
Name = "Admin system Resources",
Description = "View system resources in the admin area"
};
public static Permission AdminSecurity = new()
{
Index = 41,
Name = "Admin Security",
Description = "View security logs in the admin area"
};
public static Permission AdminSysSentry = new()
{
Index = 42,
Name = "Admin system Sentry",
Description = "Manage Sentry settings in the admin area"
};
public static Permission AdminSysNewsEdit = new()
{
Index = 43,
Name = "Admin system News Edit",
Description = "Edit system news in the admin area"
};
public static Permission AdminSysNews = new()
{
Index = 44,
Name = "Admin system News",
Description = "Access the system news management page in the admin area"
};
public static Permission AdminSysNewsNew = new()
{
Index = 45,
Name = "Admin system News New",
Description = "Create new system news in the admin area"
};
public static Permission AdminUserEdit = new()
{
Index = 46,
Name = "Admin User Edit",
Description = "Edit user settings in the admin area"
};
public static Permission AdminUsers = new()
{
Index = 47,
Name = "Admin Users",
Description = "Access the user management page in the admin area"
};
public static Permission AdminNewUser = new()
{
Index = 48,
Name = "Admin New User",
Description = "Create a new user in the admin area"
};
public static Permission AdminUserSessions = new()
{
Index = 49,
Name = "Admin User Sessions",
Description = "View user sessions in the admin area"
};
public static Permission AdminUserView = new()
{
Index = 50,
Name = "Admin User View",
Description = "View user details in the admin area"
};
public static Permission AdminWebspaces = new()
{
Index = 51,
Name = "Admin Webspaces",
Description = "Access the webspaces management page in the admin area"
};
public static Permission AdminNewWebspace = new()
{
Index = 52,
Name = "Admin New Webspace",
Description = "Create a new webspace in the admin area"
};
public static Permission AdminWebspacesServerEdit = new()
{
Index = 53,
Name = "Admin Webspaces Server Edit",
Description = "Edit webspace server settings in the admin area"
};
public static Permission AdminWebspacesServers = new()
{
Index = 54,
Name = "Admin Webspaces Servers",
Description = "Access the webspace server management page in the admin area"
};
public static Permission AdminWebspacesServerNew = new()
{
Index = 55,
Name = "Admin Webspaces Server New",
Description = "Create a new webspace server in the admin area"
};
public static Permission AdminSecurityIpBans = new()
{
Index = 56,
Name = "Admin security ip bans",
Description = "Manage ip bans in the admin area"
};
public static Permission AdminSecurityPermissionGroups = new()
{
Index = 57,
Name = "Admin security permission groups",
Description = "View, add and delete permission groups"
};
public static Permission AdminSecurityLogs = new()
{
Index = 58,
Name = "Admin security logs",
Description = "View the security logs"
};
public static Permission AdminSecurityDdos = new()
{
Index = 59,
Name = "Admin security ddos",
Description = "Manage the integrated ddos protection"
};
public static Permission AdminChangelog = new()
{
Index = 60,
Name = "Admin changelog",
Description = "View the changelog"
};
public static Permission AdminStatisticsLive = new()
{
Index = 61,
Name = "Admin statistics live",
Description = "View the live statistics"
};
public static Permission? FromString(string name)
{
var type = typeof(Permissions);
var field = type
.GetFields()
.FirstOrDefault(x => x.FieldType == typeof(Permission) && x.Name == name);
if (field != null)
{
var value = field.GetValue(null);
return value as Permission;
}
return null;
}
public static Permission[] GetAllPermissions()
{
var type = typeof(Permissions);
return type
.GetFields()
.Where(x => x.FieldType == typeof(Permission))
.Select(x => (x.GetValue(null) as Permission)!)
.ToArray();
}
}

View File

@@ -0,0 +1,17 @@
using Moonlight.App.Models.Misc;
using Moonlight.App.Plugin.UI.Servers;
using Moonlight.App.Plugin.UI.Webspaces;
namespace Moonlight.App.Plugin;
public abstract class MoonlightPlugin
{
public string Name { get; set; } = "N/A";
public string Author { get; set; } = "N/A";
public string Version { get; set; } = "N/A";
public Func<ServerPageContext, Task>? OnBuildServerPage { get; set; }
public Func<WebspacePageContext, Task>? OnBuildWebspacePage { get; set; }
public Func<IServiceCollection, Task>? OnBuildServices { get; set; }
public Func<List<MalwareScan>, Task<List<MalwareScan>>>? OnBuildMalwareScans { get; set; }
}

View File

@@ -0,0 +1,12 @@
using Moonlight.App.Database.Entities;
namespace Moonlight.App.Plugin.UI.Servers;
public class ServerPageContext
{
public List<ServerTab> Tabs { get; set; } = new();
public List<ServerSetting> Settings { get; set; } = new();
public Server Server { get; set; }
public User User { get; set; }
public string[] ImageTags { get; set; }
}

View File

@@ -0,0 +1,9 @@
using Microsoft.AspNetCore.Components;
namespace Moonlight.App.Plugin.UI.Servers;
public class ServerSetting
{
public string Name { get; set; }
public RenderFragment Component { get; set; }
}

View File

@@ -0,0 +1,11 @@
using Microsoft.AspNetCore.Components;
namespace Moonlight.App.Plugin.UI.Servers;
public class ServerTab
{
public string Name { get; set; }
public string Route { get; set; }
public string Icon { get; set; }
public RenderFragment Component { get; set; }
}

View File

@@ -0,0 +1,10 @@
using Moonlight.App.Database.Entities;
namespace Moonlight.App.Plugin.UI.Webspaces;
public class WebspacePageContext
{
public List<WebspaceTab> Tabs { get; set; } = new();
public User User { get; set; }
public WebSpace WebSpace { get; set; }
}

View File

@@ -0,0 +1,10 @@
using Microsoft.AspNetCore.Components;
namespace Moonlight.App.Plugin.UI.Webspaces;
public class WebspaceTab
{
public string Name { get; set; } = "N/A";
public string Route { get; set; } = "/";
public RenderFragment Component { get; set; }
}

View File

@@ -1,32 +0,0 @@
using Microsoft.EntityFrameworkCore;
using Moonlight.App.Database;
using Moonlight.App.Database.Entities.LogsEntries;
namespace Moonlight.App.Repositories.LogEntries;
public class AuditLogEntryRepository : IDisposable
{
private readonly DataContext DataContext;
public AuditLogEntryRepository(DataContext dataContext)
{
DataContext = dataContext;
}
public AuditLogEntry Add(AuditLogEntry entry)
{
var x = DataContext.AuditLog.Add(entry);
DataContext.SaveChanges();
return x.Entity;
}
public DbSet<AuditLogEntry> Get()
{
return DataContext.AuditLog;
}
public void Dispose()
{
DataContext.Dispose();
}
}

View File

@@ -1,32 +0,0 @@
using Microsoft.EntityFrameworkCore;
using Moonlight.App.Database;
using Moonlight.App.Database.Entities.LogsEntries;
namespace Moonlight.App.Repositories.LogEntries;
public class ErrorLogEntryRepository : IDisposable
{
private readonly DataContext DataContext;
public ErrorLogEntryRepository(DataContext dataContext)
{
DataContext = dataContext;
}
public ErrorLogEntry Add(ErrorLogEntry errorLogEntry)
{
var x = DataContext.ErrorLog.Add(errorLogEntry);
DataContext.SaveChanges();
return x.Entity;
}
public DbSet<ErrorLogEntry> Get()
{
return DataContext.ErrorLog;
}
public void Dispose()
{
DataContext.Dispose();
}
}

View File

@@ -1,32 +0,0 @@
using Microsoft.EntityFrameworkCore;
using Moonlight.App.Database;
using Moonlight.App.Database.Entities.LogsEntries;
namespace Moonlight.App.Repositories.LogEntries;
public class SecurityLogEntryRepository : IDisposable
{
private readonly DataContext DataContext;
public SecurityLogEntryRepository(DataContext dataContext)
{
DataContext = dataContext;
}
public SecurityLogEntry Add(SecurityLogEntry securityLogEntry)
{
var x = DataContext.SecurityLog.Add(securityLogEntry);
DataContext.SaveChanges();
return x.Entity;
}
public DbSet<SecurityLogEntry> Get()
{
return DataContext.SecurityLog;
}
public void Dispose()
{
DataContext.Dispose();
}
}

View File

@@ -93,14 +93,14 @@ public class CleanupService
Logger.Debug($"Max CPU: {maxCpu}"); Logger.Debug($"Max CPU: {maxCpu}");
executeCleanup = true; executeCleanup = true;
} }
else if (Formatter.BytesToGb(memoryMetrics.Total) - Formatter.BytesToGb(memoryMetrics.Used) < else if (ByteSizeValue.FromKiloBytes(memoryMetrics.Total).MegaBytes - ByteSizeValue.FromKiloBytes(memoryMetrics.Used).MegaBytes <
minMemory / 1024D) minMemory)
{ {
Logger.Debug($"{node.Name}: Memory Usage is too high"); Logger.Debug($"{node.Name}: Memory Usage is too high");
Logger.Debug($"Memory (Total): {Formatter.BytesToGb(memoryMetrics.Total)}"); Logger.Debug($"Memory (Total): {ByteSizeValue.FromKiloBytes(memoryMetrics.Total).MegaBytes}");
Logger.Debug($"Memory (Used): {Formatter.BytesToGb(memoryMetrics.Used)}"); Logger.Debug($"Memory (Used): {ByteSizeValue.FromKiloBytes(memoryMetrics.Used).MegaBytes}");
Logger.Debug( Logger.Debug(
$"Memory (Free): {Formatter.BytesToGb(memoryMetrics.Total) - Formatter.BytesToGb(memoryMetrics.Used)}"); $"Memory (Free): {ByteSizeValue.FromKiloBytes(memoryMetrics.Total).MegaBytes - ByteSizeValue.FromKiloBytes(memoryMetrics.Used).MegaBytes}");
Logger.Debug($"Min Memory: {minMemory}"); Logger.Debug($"Min Memory: {minMemory}");
executeCleanup = true; executeCleanup = true;
} }

View File

@@ -0,0 +1,129 @@
using Moonlight.App.ApiClients.Daemon;
using Moonlight.App.Database.Entities;
using Moonlight.App.Events;
using Moonlight.App.Helpers;
using Moonlight.App.Repositories;
namespace Moonlight.App.Services.Background;
public class DdosProtectionService
{
private readonly IServiceScopeFactory ServiceScopeFactory;
public DdosProtectionService(IServiceScopeFactory serviceScopeFactory)
{
ServiceScopeFactory = serviceScopeFactory;
Task.Run(UnBlocker);
}
private async Task UnBlocker()
{
var periodicTimer = new PeriodicTimer(TimeSpan.FromMinutes(5));
while (true)
{
using var scope = ServiceScopeFactory.CreateScope();
var blocklistIpRepo = scope.ServiceProvider.GetRequiredService<Repository<BlocklistIp>>();
var ips = blocklistIpRepo
.Get()
.ToArray();
foreach (var ip in ips)
{
if (DateTime.UtcNow > ip.ExpiresAt)
{
blocklistIpRepo.Delete(ip);
}
}
var newCount = blocklistIpRepo
.Get()
.Count();
if (newCount != ips.Length)
{
await RebuildNodeFirewalls();
}
await periodicTimer.WaitForNextTickAsync();
}
}
public async Task RebuildNodeFirewalls()
{
using var scope = ServiceScopeFactory.CreateScope();
var blocklistIpRepo = scope.ServiceProvider.GetRequiredService<Repository<BlocklistIp>>();
var nodeRepo = scope.ServiceProvider.GetRequiredService<Repository<Node>>();
var nodeService = scope.ServiceProvider.GetRequiredService<NodeService>();
var ips = blocklistIpRepo
.Get()
.Select(x => x.Ip)
.ToArray();
foreach (var node in nodeRepo.Get().ToArray())
{
try
{
await nodeService.RebuildFirewall(node, ips);
}
catch (Exception e)
{
Logger.Warn($"Error rebuilding firewall on node {node.Name}");
Logger.Warn(e);
}
}
}
public async Task ProcessDdosSignal(string ip, long packets)
{
using var scope = ServiceScopeFactory.CreateScope();
var blocklistRepo = scope.ServiceProvider.GetRequiredService<Repository<BlocklistIp>>();
var whitelistRepo = scope.ServiceProvider.GetRequiredService<Repository<WhitelistIp>>();
var whitelistIps = whitelistRepo.Get().ToArray();
if(whitelistIps.Any(x => x.Ip == ip))
return;
var blocklistIps = blocklistRepo.Get().ToArray();
if(blocklistIps.Any(x => x.Ip == ip))
return;
await BlocklistIp(ip, packets);
}
public async Task BlocklistIp(string ip, long packets)
{
using var scope = ServiceScopeFactory.CreateScope();
var blocklistRepo = scope.ServiceProvider.GetRequiredService<Repository<BlocklistIp>>();
var configService = scope.ServiceProvider.GetRequiredService<ConfigService>();
var eventSystem = scope.ServiceProvider.GetRequiredService<EventSystem>();
var blocklistIp = blocklistRepo.Add(new()
{
Ip = ip,
Packets = packets,
ExpiresAt = DateTime.UtcNow.AddMinutes(configService.Get().Moonlight.Security.BlockIpDuration),
CreatedAt = DateTime.UtcNow
});
await RebuildNodeFirewalls();
await eventSystem.Emit("ddos.add", blocklistIp);
}
public async Task UnBlocklistIp(string ip)
{
using var scope = ServiceScopeFactory.CreateScope();
var blocklistRepo = scope.ServiceProvider.GetRequiredService<Repository<BlocklistIp>>();
var blocklist = blocklistRepo.Get().First(x => x.Ip == ip);
blocklistRepo.Delete(blocklist);
await RebuildNodeFirewalls();
}
}

View File

@@ -31,10 +31,11 @@ public class DiscordNotificationService
Client = new(config.WebHook); Client = new(config.WebHook);
AppUrl = configService.Get().Moonlight.AppUrl; AppUrl = configService.Get().Moonlight.AppUrl;
Event.On<User>("supportChat.new", this, OnNewSupportChat); Event.On<Ticket>("tickets.new", this, OnNewTicket);
Event.On<SupportChatMessage>("supportChat.message", this, OnSupportChatMessage); Event.On<Ticket>("tickets.status", this, OnTicketStatusUpdated);
Event.On<User>("supportChat.close", this, OnSupportChatClose);
Event.On<User>("user.rating", this, OnUserRated); Event.On<User>("user.rating", this, OnUserRated);
Event.On<User>("billing.completed", this, OnBillingCompleted);
Event.On<BlocklistIp>("ddos.add", this, OnIpBlockListed);
} }
else else
{ {
@@ -42,6 +43,51 @@ public class DiscordNotificationService
} }
} }
private async Task OnTicketStatusUpdated(Ticket ticket)
{
await SendNotification("", builder =>
{
builder.Title = "Ticket status has been updated";
builder.AddField("Issue topic", ticket.IssueTopic);
builder.AddField("Status", ticket.Status);
if (ticket.AssignedTo != null)
{
builder.AddField("Assigned to", $"{ticket.AssignedTo.FirstName} {ticket.AssignedTo.LastName}");
}
builder.Color = Color.Green;
builder.Url = $"{AppUrl}/admin/support/view/{ticket.Id}";
});
}
private async Task OnIpBlockListed(BlocklistIp blocklistIp)
{
await SendNotification("", builder =>
{
builder.Color = Color.Red;
builder.Title = "New ddos attack detected";
builder.AddField("IP", blocklistIp.Ip);
builder.AddField("Packets", blocklistIp.Packets);
});
}
private async Task OnBillingCompleted(User user)
{
await SendNotification("", builder =>
{
builder.Color = Color.Red;
builder.Title = "New payment received";
builder.AddField("User", user.Email);
builder.AddField("Firstname", user.FirstName);
builder.AddField("Lastname", user.LastName);
builder.AddField("Amount", user.CurrentSubscription!.Price);
builder.AddField("Currency", user.CurrentSubscription!.Currency);
});
}
private async Task OnUserRated(User user) private async Task OnUserRated(User user)
{ {
await SendNotification("", builder => await SendNotification("", builder =>
@@ -56,46 +102,14 @@ public class DiscordNotificationService
}); });
} }
private async Task OnSupportChatClose(User user) private async Task OnNewTicket(Ticket ticket)
{ {
await SendNotification("", builder => await SendNotification("", builder =>
{ {
builder.Title = "A new support chat has been marked as closed"; builder.Title = "A new ticket has been created";
builder.Color = Color.Red; builder.AddField("Issue topic", ticket.IssueTopic);
builder.AddField("Email", user.Email);
builder.AddField("Firstname", user.FirstName);
builder.AddField("Lastname", user.LastName);
builder.Url = $"{AppUrl}/admin/support/view/{user.Id}";
});
}
private async Task OnSupportChatMessage(SupportChatMessage message)
{
if(message.Sender == null)
return;
await SendNotification("", builder =>
{
builder.Title = "New message in support chat";
builder.Color = Color.Blue;
builder.AddField("Message", message.Content);
builder.Author = new EmbedAuthorBuilder()
.WithName($"{message.Sender.FirstName} {message.Sender.LastName}")
.WithIconUrl(ResourceService.Avatar(message.Sender));
builder.Url = $"{AppUrl}/admin/support/view/{message.Recipient.Id}";
});
}
private async Task OnNewSupportChat(User user)
{
await SendNotification("", builder =>
{
builder.Title = "A new support chat has been marked as active";
builder.Color = Color.Green; builder.Color = Color.Green;
builder.AddField("Email", user.Email); builder.Url = $"{AppUrl}/admin/support/view/{ticket.Id}";
builder.AddField("Firstname", user.FirstName);
builder.AddField("Lastname", user.LastName);
builder.Url = $"{AppUrl}/admin/support/view/{user.Id}";
}); });
} }

View File

@@ -0,0 +1,148 @@
using Moonlight.App.ApiClients.Daemon.Resources;
using Moonlight.App.Database.Entities;
using Moonlight.App.Events;
using Moonlight.App.Exceptions;
using Moonlight.App.Helpers;
using Moonlight.App.Models.Misc;
using Moonlight.App.Repositories;
namespace Moonlight.App.Services.Background;
public class MalwareBackgroundScanService
{
private readonly EventSystem Event;
private readonly IServiceScopeFactory ServiceScopeFactory;
public bool IsRunning => !ScanTask?.IsCompleted ?? false;
public bool ScanAllServers { get; set; }
public readonly Dictionary<Server, MalwareScanResult> ScanResults;
public string Status { get; private set; } = "N/A";
private Task? ScanTask;
public MalwareBackgroundScanService(IServiceScopeFactory serviceScopeFactory, EventSystem eventSystem)
{
ServiceScopeFactory = serviceScopeFactory;
Event = eventSystem;
ScanResults = new();
}
public Task Start()
{
if (IsRunning)
throw new DisplayException("Malware scan is already running");
ScanTask = Task.Run(Run);
return Task.CompletedTask;
}
private async Task Run()
{
// Clean results
Status = "Clearing last results";
await Event.Emit("malwareScan.status", IsRunning);
lock (ScanResults)
ScanResults.Clear();
await Event.Emit("malwareScan.result");
using var scope = ServiceScopeFactory.CreateScope();
var serverRepo = scope.ServiceProvider.GetRequiredService<Repository<Server>>();
var malwareScanService = scope.ServiceProvider.GetRequiredService<MalwareScanService>();
Status = "Fetching servers to scan";
await Event.Emit("malwareScan.status", IsRunning);
Server[] servers;
if (ScanAllServers)
servers = serverRepo.Get().ToArray();
else
servers = await GetOnlineServers();
// Perform scan
int i = 1;
foreach (var server in servers)
{
Status = $"[{i} / {servers.Length}] Scanning server {server.Name}";
await Event.Emit("malwareScan.status", IsRunning);
var result = await malwareScanService.Perform(server);
if (result != null)
{
lock (ScanResults)
{
ScanResults.Add(server, result);
}
await Event.Emit("malwareScan.result", server);
}
i++;
}
Task.Run(async () => // Because we use the task as the status indicator we need to notify the event system in a new task
{
await Task.Delay(TimeSpan.FromSeconds(5));
await Event.Emit("malwareScan.status", IsRunning);
});
}
private async Task<Server[]> GetOnlineServers()
{
using var scope = ServiceScopeFactory.CreateScope();
// Load services from di scope
var nodeRepo = scope.ServiceProvider.GetRequiredService<Repository<Node>>();
var serverRepo = scope.ServiceProvider.GetRequiredService<Repository<Server>>();
var nodeService = scope.ServiceProvider.GetRequiredService<NodeService>();
var nodes = nodeRepo.Get().ToArray();
var containers = new List<Container>();
// Fetch and summarize all running containers from all nodes
Logger.Verbose("Fetching and summarizing all running containers from all nodes");
Status = "Fetching and summarizing all running containers from all nodes";
await Event.Emit("malwareScan.status", IsRunning);
foreach (var node in nodes)
{
var metrics = await nodeService.GetDockerMetrics(node);
foreach (var container in metrics.Containers)
{
containers.Add(container);
}
}
var containerServerMapped = new Dictionary<Server, Container>();
// Map all the containers to their corresponding server if existing
Logger.Verbose("Mapping all the containers to their corresponding server if existing");
Status = "Mapping all the containers to their corresponding server if existing";
await Event.Emit("malwareScan.status", IsRunning);
foreach (var container in containers)
{
if (Guid.TryParse(container.Name, out Guid uuid))
{
var server = serverRepo
.Get()
.FirstOrDefault(x => x.Uuid == uuid);
if(server == null)
continue;
containerServerMapped.Add(server, container);
}
}
return containerServerMapped.Keys.ToArray();
}
}

View File

@@ -1,196 +0,0 @@
using Moonlight.App.ApiClients.Daemon.Resources;
using Moonlight.App.Database.Entities;
using Moonlight.App.Events;
using Moonlight.App.Exceptions;
using Moonlight.App.Helpers;
using Moonlight.App.Models.Misc;
using Moonlight.App.Repositories;
namespace Moonlight.App.Services.Background;
public class MalwareScanService
{
private Repository<Server> ServerRepository;
private Repository<Node> NodeRepository;
private NodeService NodeService;
private ServerService ServerService;
private readonly EventSystem Event;
private readonly IServiceScopeFactory ServiceScopeFactory;
public bool IsRunning { get; private set; }
public readonly Dictionary<Server, MalwareScanResult[]> ScanResults;
public string Status { get; private set; } = "N/A";
public MalwareScanService(IServiceScopeFactory serviceScopeFactory, EventSystem eventSystem)
{
ServiceScopeFactory = serviceScopeFactory;
Event = eventSystem;
ScanResults = new();
}
public Task Start()
{
if (IsRunning)
throw new DisplayException("Malware scan is already running");
Task.Run(Run);
return Task.CompletedTask;
}
private async Task Run()
{
IsRunning = true;
Status = "Clearing last results";
await Event.Emit("malwareScan.status", IsRunning);
lock (ScanResults)
{
ScanResults.Clear();
}
await Event.Emit("malwareScan.result");
using var scope = ServiceScopeFactory.CreateScope();
// Load services from di scope
NodeRepository = scope.ServiceProvider.GetRequiredService<Repository<Node>>();
ServerRepository = scope.ServiceProvider.GetRequiredService<Repository<Server>>();
NodeService = scope.ServiceProvider.GetRequiredService<NodeService>();
ServerService = scope.ServiceProvider.GetRequiredService<ServerService>();
var nodes = NodeRepository.Get().ToArray();
var containers = new List<Container>();
// Fetch and summarize all running containers from all nodes
Logger.Verbose("Fetching and summarizing all running containers from all nodes");
Status = "Fetching and summarizing all running containers from all nodes";
await Event.Emit("malwareScan.status", IsRunning);
foreach (var node in nodes)
{
var metrics = await NodeService.GetDockerMetrics(node);
foreach (var container in metrics.Containers)
{
containers.Add(container);
}
}
var containerServerMapped = new Dictionary<Server, Container>();
// Map all the containers to their corresponding server if existing
Logger.Verbose("Mapping all the containers to their corresponding server if existing");
Status = "Mapping all the containers to their corresponding server if existing";
await Event.Emit("malwareScan.status", IsRunning);
foreach (var container in containers)
{
if (Guid.TryParse(container.Name, out Guid uuid))
{
var server = ServerRepository
.Get()
.FirstOrDefault(x => x.Uuid == uuid);
if(server == null)
continue;
containerServerMapped.Add(server, container);
}
}
// Perform scan
var resultsMapped = new Dictionary<Server, MalwareScanResult[]>();
foreach (var mapping in containerServerMapped)
{
Logger.Verbose($"Scanning server {mapping.Key.Name} for malware");
Status = $"Scanning server {mapping.Key.Name} for malware";
await Event.Emit("malwareScan.status", IsRunning);
var results = await PerformScanOnServer(mapping.Key, mapping.Value);
if (results.Any())
{
resultsMapped.Add(mapping.Key, results);
Logger.Verbose($"{results.Length} findings on server {mapping.Key.Name}");
}
}
Logger.Verbose($"Scan complete. Detected {resultsMapped.Count} servers with findings");
IsRunning = false;
Status = $"Scan complete. Detected {resultsMapped.Count} servers with findings";
await Event.Emit("malwareScan.status", IsRunning);
lock (ScanResults)
{
foreach (var mapping in resultsMapped)
{
ScanResults.Add(mapping.Key, mapping.Value);
}
}
await Event.Emit("malwareScan.result");
}
private async Task<MalwareScanResult[]> PerformScanOnServer(Server server, Container container)
{
var results = new List<MalwareScanResult>();
// TODO: Move scans to an universal format / api
// Define scans here
async Task ScanSelfBot()
{
var access = await ServerService.CreateFileAccess(server, null!);
var fileElements = await access.Ls();
if (fileElements.Any(x => x.Name == "tokens.txt"))
{
results.Add(new ()
{
Title = "Found SelfBot",
Description = "Detected suspicious 'tokens.txt' file which may contain tokens for a selfbot",
Author = "Marcel Baumgartner"
});
}
}
async Task ScanFakePlayerPlugins()
{
var access = await ServerService.CreateFileAccess(server, null!);
var fileElements = await access.Ls();
if (fileElements.Any(x => !x.IsFile && x.Name == "plugins")) // Check for plugins folder
{
await access.Cd("plugins");
fileElements = await access.Ls();
foreach (var fileElement in fileElements)
{
if (fileElement.Name.ToLower().Contains("fake"))
{
results.Add(new()
{
Title = "Fake player plugin",
Description = $"Suspicious plugin file: {fileElement.Name}",
Author = "Marcel Baumgartner"
});
}
}
}
}
// Execute scans
await ScanSelfBot();
await ScanFakePlayerPlugins();
return results.ToArray();
}
}

View File

@@ -0,0 +1,37 @@
using System.Net.Mail;
using Moonlight.App.Helpers;
namespace Moonlight.App.Services.Background;
public class TempMailService
{
private string[] Domains = Array.Empty<string>();
public TempMailService()
{
Task.Run(Init);
}
private async Task Init()
{
var client = new HttpClient();
var text = await client.GetStringAsync("https://raw.githubusercontent.com/disposable-email-domains/disposable-email-domains/master/disposable_email_blocklist.conf");
Domains = text
.Split("\n")
.Select(x => x.Trim())
.ToArray();
Logger.Info($"Fetched {Domains.Length} temp mail domains");
}
public Task<bool> IsTempMail(string mail)
{
var address = new MailAddress(mail);
if (Domains.Contains(address.Host))
return Task.FromResult(true);
return Task.FromResult(false);
}
}

View File

@@ -0,0 +1,127 @@
using System.Globalization;
using Moonlight.App.Database.Entities;
using Moonlight.App.Events;
using Moonlight.App.Exceptions;
using Moonlight.App.Repositories;
using Moonlight.App.Services.Mail;
using Moonlight.App.Services.Sessions;
using Stripe.Checkout;
using Subscription = Moonlight.App.Database.Entities.Subscription;
namespace Moonlight.App.Services;
public class BillingService
{
private readonly ConfigService ConfigService;
private readonly SubscriptionService SubscriptionService;
private readonly Repository<Subscription> SubscriptionRepository;
private readonly SessionServerService SessionServerService;
private readonly EventSystem Event;
private readonly MailService MailService;
public BillingService(
ConfigService configService,
SubscriptionService subscriptionService,
Repository<Subscription> subscriptionRepository,
EventSystem eventSystem,
SessionServerService sessionServerService,
MailService mailService)
{
ConfigService = configService;
SubscriptionService = subscriptionService;
SubscriptionRepository = subscriptionRepository;
Event = eventSystem;
SessionServerService = sessionServerService;
MailService = mailService;
}
public async Task<string> StartCheckout(User user, Subscription subscription)
{
var appUrl = ConfigService.Get().Moonlight.AppUrl;
var controllerUrl = appUrl + "/api/moonlight/billing";
var options = new SessionCreateOptions()
{
LineItems = new()
{
new()
{
Price = subscription.StripePriceId,
Quantity = 1
}
},
Mode = "payment",
SuccessUrl = controllerUrl + "/success",
CancelUrl = controllerUrl + "/cancel",
AutomaticTax = new SessionAutomaticTaxOptions()
{
Enabled = true
},
CustomerEmail = user.Email.ToLower(),
Metadata = new()
{
{
"productId",
subscription.StripeProductId
}
}
};
var service = new SessionService();
var session = await service.CreateAsync(options);
return session.Url;
}
public async Task CompleteCheckout(User user)
{
var sessionService = new SessionService();
var sessionsPerUser = await sessionService.ListAsync(new SessionListOptions()
{
CustomerDetails = new()
{
Email = user.Email
}
});
var latestCompletedSession = sessionsPerUser
.Where(x => x.Status == "complete")
.Where(x => x.PaymentStatus == "paid")
.MaxBy(x => x.Created);
if (latestCompletedSession == null)
throw new DisplayException("No completed session found");
var productId = latestCompletedSession.Metadata["productId"];
var subscription = SubscriptionRepository
.Get()
.FirstOrDefault(x => x.StripeProductId == productId);
if (subscription == null)
throw new DisplayException("No subscription for this product found");
// if (await SubscriptionService.GetActiveSubscription(user) != null)
// {
// return;
// }
await SubscriptionService.SetActiveSubscription(user, subscription);
await MailService.SendMail(user, "checkoutComplete", values =>
{
values.Add("SubscriptionName", subscription.Name);
values.Add("SubscriptionPrice", subscription.Price
.ToString(CultureInfo.InvariantCulture));
values.Add("SubscriptionCurrency", subscription.Currency
.ToString());
values.Add("SubscriptionDuration", subscription.Duration
.ToString(CultureInfo.InvariantCulture));
});
await Event.Emit("billing.completed", user);
await SessionServerService.ReloadUserSessions(user);
}
}

View File

@@ -8,6 +8,7 @@ namespace Moonlight.App.Services;
public class ConfigService public class ConfigService
{ {
private readonly StorageService StorageService; private readonly StorageService StorageService;
private readonly string Path;
private ConfigV1 Configuration; private ConfigV1 Configuration;
public bool DebugMode { get; private set; } = false; public bool DebugMode { get; private set; } = false;
@@ -18,6 +19,11 @@ public class ConfigService
StorageService = storageService; StorageService = storageService;
StorageService.EnsureCreated(); StorageService.EnsureCreated();
if (!string.IsNullOrEmpty(Environment.GetEnvironmentVariable("ML_CONFIG_PATH")))
Path = Environment.GetEnvironmentVariable("ML_CONFIG_PATH")!;
else
Path = PathBuilder.File("storage", "configs", "config.json");
Reload(); Reload();
// Env vars // Env vars
@@ -40,18 +46,16 @@ public class ConfigService
public void Reload() public void Reload()
{ {
var path = PathBuilder.File("storage", "configs", "config.json"); if (!File.Exists(Path))
if (!File.Exists(path))
{ {
File.WriteAllText(path, "{}"); File.WriteAllText(Path, "{}");
} }
Configuration = JsonConvert.DeserializeObject<ConfigV1>( Configuration = JsonConvert.DeserializeObject<ConfigV1>(
File.ReadAllText(path) File.ReadAllText(Path)
) ?? new ConfigV1(); ) ?? new ConfigV1();
File.WriteAllText(path, JsonConvert.SerializeObject(Configuration, Formatting.Indented)); File.WriteAllText(Path, JsonConvert.SerializeObject(Configuration, Formatting.Indented));
} }
public void Save(ConfigV1 configV1) public void Save(ConfigV1 configV1)
@@ -62,14 +66,12 @@ public class ConfigService
public void Save() public void Save()
{ {
var path = PathBuilder.File("storage", "configs", "config.json"); if (!File.Exists(Path))
if (!File.Exists(path))
{ {
File.WriteAllText(path, "{}"); File.WriteAllText(Path, "{}");
} }
File.WriteAllText(path, JsonConvert.SerializeObject(Configuration, Formatting.Indented)); File.WriteAllText(Path, JsonConvert.SerializeObject(Configuration, Formatting.Indented));
Reload(); Reload();
} }

View File

@@ -183,6 +183,8 @@ public class DomainService
if (!ConfigService.Get().Moonlight.Domains.Enable) if (!ConfigService.Get().Moonlight.Domains.Enable)
throw new DisplayException("This operation is disabled"); throw new DisplayException("This operation is disabled");
try
{
var domain = EnsureData(d); var domain = EnsureData(d);
var rname = $"{domain.Name}.{domain.SharedDomain.Name}"; var rname = $"{domain.Name}.{domain.SharedDomain.Name}";
@@ -236,6 +238,15 @@ public class DomainService
Name = name Name = name
})); }));
} }
}
catch (OverflowException)
{
throw new DisplayException("Invalid dns record values");
}
catch (FormatException)
{
throw new DisplayException("Invalid dns record values");
}
//TODO: AuditLog //TODO: AuditLog
} }

View File

@@ -1,23 +1,23 @@
using Moonlight.App.Helpers; using Moonlight.App.Helpers;
using Octokit;
namespace Moonlight.App.Services.Files; namespace Moonlight.App.Services.Files;
public class StorageService public class StorageService
{ {
public StorageService() public async Task EnsureCreated()
{
EnsureCreated();
}
public void EnsureCreated()
{ {
Directory.CreateDirectory(PathBuilder.Dir("storage", "uploads")); Directory.CreateDirectory(PathBuilder.Dir("storage", "uploads"));
Directory.CreateDirectory(PathBuilder.Dir("storage", "configs")); Directory.CreateDirectory(PathBuilder.Dir("storage", "configs"));
Directory.CreateDirectory(PathBuilder.Dir("storage", "resources")); Directory.CreateDirectory(PathBuilder.Dir("storage", "resources"));
Directory.CreateDirectory(PathBuilder.Dir("storage", "backups")); Directory.CreateDirectory(PathBuilder.Dir("storage", "backups"));
Directory.CreateDirectory(PathBuilder.Dir("storage", "logs")); Directory.CreateDirectory(PathBuilder.Dir("storage", "logs"));
Directory.CreateDirectory(PathBuilder.Dir("storage", "plugins"));
if(IsEmpty(PathBuilder.Dir("storage", "resources"))) await UpdateResources();
return;
if (IsEmpty(PathBuilder.Dir("storage", "resources")))
{ {
Logger.Info("Default resources not found. Copying default resources"); Logger.Info("Default resources not found. Copying default resources");
@@ -38,10 +38,75 @@ public class StorageService
} }
} }
private async Task UpdateResources()
{
try
{
Logger.Info("Checking resources");
var client = new GitHubClient(
new ProductHeaderValue("Moonlight-Panel"));
var user = "Moonlight-Panel";
var repo = "Resources";
var resourcesDir = PathBuilder.Dir("storage", "resources");
async Task CopyDirectory(string dirPath, string localDir)
{
IReadOnlyList<RepositoryContent> contents;
if (string.IsNullOrEmpty(dirPath))
contents = await client.Repository.Content.GetAllContents(user, repo);
else
contents = await client.Repository.Content.GetAllContents(user, repo, dirPath);
foreach (var content in contents)
{
string localPath = Path.Combine(localDir, content.Name);
if (content.Type == ContentType.File)
{
if (content.Name.EndsWith(".gitattributes"))
continue;
if (File.Exists(localPath) && !content.Name.EndsWith(".lang"))
continue;
if (content.Name.EndsWith(".lang") && File.Exists(localPath) &&
new FileInfo(localPath).Length == content.Size)
continue;
var fileContent = await client.Repository.Content.GetRawContent(user, repo, content.Path);
Directory.CreateDirectory(localDir); // Ensure the directory exists
await File.WriteAllBytesAsync(localPath, fileContent);
Logger.Debug($"Synced file '{content.Path}'");
}
else if (content.Type == ContentType.Dir)
{
await CopyDirectory(content.Path, localPath);
}
}
}
await CopyDirectory("", resourcesDir);
}
catch (RateLimitExceededException)
{
Logger.Warn("Unable to sync resources due to your ip being rate-limited by github");
}
catch (Exception e)
{
Logger.Warn("Unable to sync resources");
Logger.Warn(e);
}
}
private bool IsEmpty(string path) private bool IsEmpty(string path)
{ {
return !Directory.EnumerateFileSystemEntries(path).Any(); return !Directory.EnumerateFileSystemEntries(path).Any();
} }
private static void CopyFilesRecursively(string sourcePath, string targetPath) private static void CopyFilesRecursively(string sourcePath, string targetPath)
{ {
//Now Create all of the directories //Now Create all of the directories
@@ -51,7 +116,7 @@ public class StorageService
} }
//Copy all the files & Replaces any files with the same name //Copy all the files & Replaces any files with the same name
foreach (string newPath in Directory.GetFiles(sourcePath, "*.*",SearchOption.AllDirectories)) foreach (string newPath in Directory.GetFiles(sourcePath, "*.*", SearchOption.AllDirectories))
{ {
File.Copy(newPath, newPath.Replace(sourcePath, targetPath), true); File.Copy(newPath, newPath.Replace(sourcePath, targetPath), true);
} }

View File

@@ -0,0 +1,18 @@
using Microsoft.JSInterop;
namespace Moonlight.App.Services.Interop;
public class PopupService
{
private readonly IJSRuntime JsRuntime;
public PopupService(IJSRuntime jsRuntime)
{
JsRuntime = jsRuntime;
}
public async Task ShowCentered(string url, string title, int width = 500, int height = 500)
{
await JsRuntime.InvokeVoidAsync("moonlight.popup.showCentered", url, title, width, height);
}
}

View File

@@ -0,0 +1,91 @@
using Moonlight.App.Helpers;
using Whois.NET;
namespace Moonlight.App.Services;
public class IpVerificationService
{
private readonly ConfigService ConfigService;
public IpVerificationService(ConfigService configService)
{
ConfigService = configService;
}
public async Task<bool> IsDatacenterOrVpn(string ip)
{
if (!ConfigService.Get().Moonlight.Security.BlockDatacenterIps)
return false;
if (string.IsNullOrEmpty(ip))
return false;
var datacenterNames = new List<string>()
{
"amazon",
"aws",
"microsoft",
"azure",
"google",
"google cloud",
"gcp",
"digitalocean",
"linode",
"vultr",
"ovh",
"ovhcloud",
"alibaba",
"oracle",
"ibm cloud",
"bluehost",
"godaddy",
"rackpace",
"hetzner",
"tencent",
"scaleway",
"softlayer",
"dreamhost",
"a2 hosting",
"inmotion hosting",
"red hat openstack",
"kamatera",
"hostgator",
"siteground",
"greengeeks",
"liquidweb",
"joyent",
"aruba",
"interoute",
"fastcomet",
"rosehosting",
"lunarpages",
"fatcow",
"jelastic",
"datacamp"
};
if(!ConfigService.Get().Moonlight.Security.AllowCloudflareIps)
datacenterNames.Add("cloudflare");
try
{
var response = await WhoisClient.QueryAsync(ip);
var responseText = response.Raw.ToLower();
foreach (var name in datacenterNames)
{
if (responseText.Contains(name))
{
Logger.Debug(name);
return true;
}
}
}
catch (Exception)
{
return false;
}
return false;
}
}

View File

@@ -2,6 +2,7 @@
using Moonlight.App.Database.Entities; using Moonlight.App.Database.Entities;
using Moonlight.App.Exceptions; using Moonlight.App.Exceptions;
using Moonlight.App.Helpers; using Moonlight.App.Helpers;
using Moonlight.App.Repositories;
using SmtpClient = MailKit.Net.Smtp.SmtpClient; using SmtpClient = MailKit.Net.Smtp.SmtpClient;
namespace Moonlight.App.Services.Mail; namespace Moonlight.App.Services.Mail;
@@ -14,8 +15,14 @@ public class MailService
private readonly int Port; private readonly int Port;
private readonly bool Ssl; private readonly bool Ssl;
public MailService(ConfigService configService) private readonly Repository<User> UserRepository;
public MailService(
ConfigService configService,
Repository<User> userRepository)
{ {
UserRepository = userRepository;
var mailConfig = configService var mailConfig = configService
.Get() .Get()
.Moonlight.Mail; .Moonlight.Mail;
@@ -27,28 +34,8 @@ public class MailService
Ssl = mailConfig.Ssl; Ssl = mailConfig.Ssl;
} }
public async Task SendMail( public Task SendMailRaw(User user, string html)
User user,
string name,
Action<Dictionary<string, string>> values
)
{ {
if (!File.Exists(PathBuilder.File("storage", "resources", "mail", $"{name}.html")))
{
Logger.Warn($"Mail template '{name}' not found. Make sure to place one in the resources folder");
throw new DisplayException("Mail template not found");
}
var rawHtml = await File.ReadAllTextAsync(PathBuilder.File("storage", "resources", "mail", $"{name}.html"));
var val = new Dictionary<string, string>();
values.Invoke(val);
val.Add("FirstName", user.FirstName);
val.Add("LastName", user.LastName);
var parsed = ParseMail(rawHtml, val);
Task.Run(async () => Task.Run(async () =>
{ {
try try
@@ -62,24 +49,70 @@ public class MailService
var body = new BodyBuilder var body = new BodyBuilder
{ {
HtmlBody = parsed HtmlBody = html
}; };
mailMessage.Body = body.ToMessageBody(); mailMessage.Body = body.ToMessageBody();
using (var smtpClient = new SmtpClient()) using var smtpClient = new SmtpClient();
{
await smtpClient.ConnectAsync(Server, Port, Ssl); await smtpClient.ConnectAsync(Server, Port, Ssl);
await smtpClient.AuthenticateAsync(Email, Password); await smtpClient.AuthenticateAsync(Email, Password);
await smtpClient.SendAsync(mailMessage); await smtpClient.SendAsync(mailMessage);
await smtpClient.DisconnectAsync(true); await smtpClient.DisconnectAsync(true);
} }
}
catch (Exception e) catch (Exception e)
{ {
Logger.Warn("Error sending mail"); Logger.Warn("Error sending mail");
Logger.Warn(e); Logger.Warn(e);
} }
}); });
return Task.CompletedTask;
}
public async Task SendMail(User user, string template, Action<Dictionary<string, string>> values)
{
if (!File.Exists(PathBuilder.File("storage", "resources", "mail", $"{template}.html")))
{
Logger.Warn($"Mail template '{template}' not found. Make sure to place one in the resources folder");
throw new DisplayException("Mail template not found");
}
var rawHtml = await File.ReadAllTextAsync(PathBuilder.File("storage", "resources", "mail", $"{template}.html"));
var val = new Dictionary<string, string>();
values.Invoke(val);
val.Add("FirstName", user.FirstName);
val.Add("LastName", user.LastName);
var parsed = ParseMail(rawHtml, val);
await SendMailRaw(user, parsed);
}
public async Task SendEmailToAll(string template, Action<Dictionary<string, string>> values)
{
var users = UserRepository
.Get()
.ToArray();
foreach (var user in users)
{
await SendMail(user, template, values);
}
}
public async Task SendEmailToAllAdmins(string template, Action<Dictionary<string, string>> values)
{
var users = UserRepository
.Get()
.Where(x => x.Admin)
.ToArray();
foreach (var user in users)
{
await SendMail(user, template, values);
}
} }
private string ParseMail(string html, Dictionary<string, string> values) private string ParseMail(string html, Dictionary<string, string> values)

View File

@@ -1,46 +0,0 @@
using System.Net;
using Moonlight.App.Helpers;
namespace Moonlight.App.Services.Mail;
public class TrashMailDetectorService
{
private string[] Domains;
public TrashMailDetectorService()
{
Logger.Info("Fetching trash mail list from github repository");
using var wc = new WebClient();
var lines = wc
.DownloadString("https://raw.githubusercontent.com/Endelon-Hosting/TrashMailDomainDetector/main/trashmail_domains.md")
.Replace("\r\n", "\n")
.Split(new [] { "\n" }, StringSplitOptions.RemoveEmptyEntries);
Domains = GetDomains(lines).ToArray();
}
private IEnumerable<string> GetDomains(string[] lines)
{
foreach (var line in lines)
{
if (!string.IsNullOrWhiteSpace(line))
{
if (line.Contains("."))
{
var domain = line.Remove(0, line.IndexOf(".", StringComparison.Ordinal) + 1).Trim();
if (domain.Contains("."))
{
yield return domain;
}
}
}
}
}
public bool IsTrashEmail(string mail)
{
return Domains.Contains(mail.Split('@')[1]);
}
}

View File

@@ -0,0 +1,46 @@
using Moonlight.App.Database.Entities;
using Moonlight.App.MalwareScans;
using Moonlight.App.Models.Misc;
using Moonlight.App.Services.Plugins;
namespace Moonlight.App.Services;
public class MalwareScanService
{
private readonly PluginService PluginService;
private readonly IServiceScopeFactory ServiceScopeFactory;
public MalwareScanService(PluginService pluginService, IServiceScopeFactory serviceScopeFactory)
{
PluginService = pluginService;
ServiceScopeFactory = serviceScopeFactory;
}
public async Task<MalwareScanResult?> Perform(Server server)
{
var defaultScans = new List<MalwareScan>
{
new SelfBotScan(),
new MinerJarScan(),
new SelfBotCodeScan(),
new FakePlayerPluginScan(),
new MinerScan(),
new ProxyScan(),
new DiscordNukeScan()
};
var scans = await PluginService.BuildMalwareScans(defaultScans.ToArray());
using var scope = ServiceScopeFactory.CreateScope();
foreach (var scan in scans)
{
var result = await scan.Scan(server, scope.ServiceProvider);
if (result != null)
return result;
}
return null;
}
}

View File

@@ -46,7 +46,7 @@ public class MoonlightService
try try
{ {
var client = new GitHubClient(new ProductHeaderValue("Moonlight")); var client = new GitHubClient(new ProductHeaderValue("Moonlight-Panel"));
var pullRequests = await client.PullRequest.GetAllForRepository("Moonlight-Panel", "Moonlight", new PullRequestRequest var pullRequests = await client.PullRequest.GetAllForRepository("Moonlight-Panel", "Moonlight", new PullRequestRequest
{ {

View File

@@ -50,6 +50,11 @@ public class NodeService
return await DaemonApiHelper.Get<DockerMetrics>(node, "metrics/docker"); return await DaemonApiHelper.Get<DockerMetrics>(node, "metrics/docker");
} }
public async Task RebuildFirewall(Node node, string[] ips)
{
await DaemonApiHelper.Post(node, "firewall/rebuild", ips);
}
public async Task Mount(Node node, string server, string serverPath, string path) public async Task Mount(Node node, string server, string serverPath, string path)
{ {
await DaemonApiHelper.Post(node, "mount", new Mount() await DaemonApiHelper.Post(node, "mount", new Mount()

View File

@@ -0,0 +1,114 @@
using System.Reflection;
using System.Runtime.Loader;
using Moonlight.App.Helpers;
using Moonlight.App.Models.Misc;
using Moonlight.App.Plugin;
using Moonlight.App.Plugin.UI.Servers;
using Moonlight.App.Plugin.UI.Webspaces;
namespace Moonlight.App.Services.Plugins;
public class PluginService
{
public readonly List<MoonlightPlugin> Plugins = new();
public readonly Dictionary<MoonlightPlugin, string> PluginFiles = new();
public PluginService()
{
ReloadPlugins().Wait();
}
public Task ReloadPlugins()
{
PluginFiles.Clear();
Plugins.Clear();
// Try to update all plugins ending with .dll.cache
foreach (var pluginFile in Directory.EnumerateFiles(
PathBuilder.Dir(Directory.GetCurrentDirectory(), "storage", "plugins"))
.Where(x => x.EndsWith(".dll.cache")))
{
try
{
var realPath = pluginFile.Replace(".cache", "");
File.Copy(pluginFile, realPath, true);
File.Delete(pluginFile);
Logger.Info($"Updated plugin {realPath} on startup");
}
catch (Exception)
{
// ignored
}
}
var pluginType = typeof(MoonlightPlugin);
foreach (var pluginFile in Directory.EnumerateFiles(
PathBuilder.Dir(Directory.GetCurrentDirectory(), "storage", "plugins"))
.Where(x => x.EndsWith(".dll")))
{
var assembly = Assembly.LoadFile(pluginFile);
foreach (var type in assembly.GetTypes())
{
if (type.IsSubclassOf(pluginType))
{
var plugin = (Activator.CreateInstance(type) as MoonlightPlugin)!;
Logger.Info($"Loaded plugin '{plugin.Name}' ({plugin.Version}) by {plugin.Author}");
Plugins.Add(plugin);
PluginFiles.Add(plugin, pluginFile);
}
}
}
Logger.Info($"Loaded {Plugins.Count} plugins");
return Task.CompletedTask;
}
public async Task<ServerPageContext> BuildServerPage(ServerPageContext context)
{
foreach (var plugin in Plugins)
{
if (plugin.OnBuildServerPage != null)
await plugin.OnBuildServerPage.Invoke(context);
}
return context;
}
public async Task<WebspacePageContext> BuildWebspacePage(WebspacePageContext context)
{
foreach (var plugin in Plugins)
{
if (plugin.OnBuildWebspacePage != null)
await plugin.OnBuildWebspacePage.Invoke(context);
}
return context;
}
public async Task BuildServices(IServiceCollection serviceCollection)
{
foreach (var plugin in Plugins)
{
if (plugin.OnBuildServices != null)
await plugin.OnBuildServices.Invoke(serviceCollection);
}
}
public async Task<MalwareScan[]> BuildMalwareScans(MalwareScan[] defaultScans)
{
var scanList = defaultScans.ToList();
foreach (var plugin in Plugins)
{
if (plugin.OnBuildMalwareScans != null)
scanList = await plugin.OnBuildMalwareScans.Invoke(scanList);
}
return scanList.ToArray();
}
}

View File

@@ -0,0 +1,63 @@
using System.Text;
using Moonlight.App.Helpers;
using Moonlight.App.Models.Misc;
using Octokit;
namespace Moonlight.App.Services.Plugins;
public class PluginStoreService
{
private readonly GitHubClient Client;
private readonly PluginService PluginService;
public PluginStoreService(PluginService pluginService)
{
PluginService = pluginService;
Client = new(new ProductHeaderValue("Moonlight-Panel"));
}
public async Task<OfficialMoonlightPlugin[]> GetPlugins()
{
var items = await Client.Repository.Content.GetAllContents("Moonlight-Panel", "OfficialPlugins");
if (items == null)
{
Logger.Fatal("Unable to read plugin repo contents");
return Array.Empty<OfficialMoonlightPlugin>();
}
return items
.Where(x => x.Type == ContentType.Dir)
.Select(x => new OfficialMoonlightPlugin()
{
Name = x.Name
})
.ToArray();
}
public async Task<string> GetPluginReadme(OfficialMoonlightPlugin plugin)
{
var rawReadme = await Client.Repository.Content
.GetRawContent("Moonlight-Panel", "OfficialPlugins", $"{plugin.Name}/README.md");
if (rawReadme == null)
return "Error";
return Encoding.UTF8.GetString(rawReadme);
}
public async Task InstallPlugin(OfficialMoonlightPlugin plugin, bool updating = false)
{
var rawPlugin = await Client.Repository.Content
.GetRawContent("Moonlight-Panel", "OfficialPlugins", $"{plugin.Name}/{plugin.Name}.dll");
if (updating)
{
await File.WriteAllBytesAsync(PathBuilder.File("storage", "plugins", $"{plugin.Name}.dll.cache"), rawPlugin);
return;
}
await File.WriteAllBytesAsync(PathBuilder.File("storage", "plugins", $"{plugin.Name}.dll"), rawPlugin);
await PluginService.ReloadPlugins();
}
}

View File

@@ -39,7 +39,7 @@ public class RatingService
if (!Enabled) if (!Enabled)
return false; return false;
var user = await IdentityService.Get(); var user = IdentityService.User;
if (user == null) if (user == null)
return false; return false;
@@ -62,7 +62,7 @@ public class RatingService
public async Task<bool> Rate(int rate) public async Task<bool> Rate(int rate)
{ {
var user = await IdentityService.Get(); var user = IdentityService.User;
// Double check states: // Double check states:

View File

@@ -12,6 +12,8 @@ using Moonlight.App.Helpers.Wings;
using Moonlight.App.Models.Misc; using Moonlight.App.Models.Misc;
using Moonlight.App.Repositories; using Moonlight.App.Repositories;
using Moonlight.App.Repositories.Servers; using Moonlight.App.Repositories.Servers;
using Moonlight.App.Services.Background;
using Moonlight.App.Services.Plugins;
using FileAccess = Moonlight.App.Helpers.Files.FileAccess; using FileAccess = Moonlight.App.Helpers.Files.FileAccess;
namespace Moonlight.App.Services; namespace Moonlight.App.Services;
@@ -32,6 +34,11 @@ public class ServerService
private readonly DateTimeService DateTimeService; private readonly DateTimeService DateTimeService;
private readonly EventSystem Event; private readonly EventSystem Event;
// We inject the dependencies for the malware scan service here because otherwise it may result in
// a circular dependency injection which will crash the app
private readonly PluginService PluginService;
private readonly IServiceScopeFactory ServiceScopeFactory;
public ServerService( public ServerService(
ServerRepository serverRepository, ServerRepository serverRepository,
WingsApiHelper wingsApiHelper, WingsApiHelper wingsApiHelper,
@@ -45,7 +52,9 @@ public class ServerService
NodeAllocationRepository nodeAllocationRepository, NodeAllocationRepository nodeAllocationRepository,
DateTimeService dateTimeService, DateTimeService dateTimeService,
EventSystem eventSystem, EventSystem eventSystem,
Repository<ServerVariable> serverVariablesRepository) Repository<ServerVariable> serverVariablesRepository,
PluginService pluginService,
IServiceScopeFactory serviceScopeFactory)
{ {
ServerRepository = serverRepository; ServerRepository = serverRepository;
WingsApiHelper = wingsApiHelper; WingsApiHelper = wingsApiHelper;
@@ -60,6 +69,8 @@ public class ServerService
DateTimeService = dateTimeService; DateTimeService = dateTimeService;
Event = eventSystem; Event = eventSystem;
ServerVariablesRepository = serverVariablesRepository; ServerVariablesRepository = serverVariablesRepository;
PluginService = pluginService;
ServiceScopeFactory = serviceScopeFactory;
} }
private Server EnsureNodeData(Server s) private Server EnsureNodeData(Server s)
@@ -99,6 +110,24 @@ public class ServerService
{ {
Server server = EnsureNodeData(s); Server server = EnsureNodeData(s);
if (ConfigService.Get().Moonlight.Security.MalwareCheckOnStart && signal == PowerSignal.Start ||
signal == PowerSignal.Restart)
{
var result = await new MalwareScanService(
PluginService,
ServiceScopeFactory
).Perform(server);
if (result != null)
{
Logger.Warn($"Found malware on server {server.Uuid}. Result: " + result.Title, "security");
throw new DisplayException(
$"Unable to start server. Found following malware on this server: {result.Title}. Please contact the support if you think this detection is a false positive",
true);
}
}
var rawSignal = signal.ToString().ToLower(); var rawSignal = signal.ToString().ToLower();
await WingsApiHelper.Post(server.Node, $"api/servers/{server.Uuid}/power", new ServerPower() await WingsApiHelper.Post(server.Node, $"api/servers/{server.Uuid}/power", new ServerPower()
@@ -420,10 +449,7 @@ public class ServerService
await new Retry() await new Retry()
.Times(3) .Times(3)
.At(x => x.Message.Contains("A task was canceled")) .At(x => x.Message.Contains("A task was canceled"))
.Call(async () => .Call(async () => { await WingsApiHelper.Delete(server.Node, $"api/servers/{server.Uuid}", null); });
{
await WingsApiHelper.Delete(server.Node, $"api/servers/{server.Uuid}", null);
});
} }
catch (WingsException e) catch (WingsException e)
{ {

View File

@@ -2,9 +2,10 @@
using JWT.Algorithms; using JWT.Algorithms;
using JWT.Builder; using JWT.Builder;
using JWT.Exceptions; using JWT.Exceptions;
using Microsoft.EntityFrameworkCore;
using Moonlight.App.Database.Entities; using Moonlight.App.Database.Entities;
using Moonlight.App.Helpers; using Moonlight.App.Helpers;
using Moonlight.App.Models.Misc; using Moonlight.App.Perms;
using Moonlight.App.Repositories; using Moonlight.App.Repositories;
using UAParser; using UAParser;
@@ -12,16 +13,21 @@ namespace Moonlight.App.Services.Sessions;
public class IdentityService public class IdentityService
{ {
private readonly UserRepository UserRepository; private readonly Repository<User> UserRepository;
private readonly CookieService CookieService; private readonly CookieService CookieService;
private readonly IHttpContextAccessor HttpContextAccessor; private readonly IHttpContextAccessor HttpContextAccessor;
private readonly string Secret; private readonly string Secret;
private User? UserCache; public User User { get; private set; }
public string Ip { get; private set; } = "N/A";
public string Device { get; private set; } = "N/A";
public PermissionStorage Permissions { get; private set; }
public PermissionStorage UserPermissions { get; private set; }
public PermissionStorage GroupPermissions { get; private set; }
public IdentityService( public IdentityService(
CookieService cookieService, CookieService cookieService,
UserRepository userRepository, Repository<User> userRepository,
IHttpContextAccessor httpContextAccessor, IHttpContextAccessor httpContextAccessor,
ConfigService configService) ConfigService configService)
{ {
@@ -34,13 +40,17 @@ public class IdentityService
.Moonlight.Security.Token; .Moonlight.Security.Token;
} }
public async Task<User?> Get() public async Task Load()
{
await LoadIp();
await LoadDevice();
await LoadUser();
}
private async Task LoadUser()
{ {
try try
{ {
if (UserCache != null)
return UserCache;
var token = "none"; var token = "none";
// Load token via http context if available // Load token via http context if available
@@ -60,13 +70,13 @@ public class IdentityService
if (token == "none") if (token == "none")
{ {
return null; return;
} }
if (string.IsNullOrEmpty(token)) if (string.IsNullOrEmpty(token))
return null; return;
var json = ""; string json;
try try
{ {
@@ -77,18 +87,18 @@ public class IdentityService
} }
catch (TokenExpiredException) catch (TokenExpiredException)
{ {
return null; return;
} }
catch (SignatureVerificationException) catch (SignatureVerificationException)
{ {
Logger.Warn($"Detected a manipulated JWT: {token}", "security"); Logger.Warn($"Detected a manipulated JWT: {token}", "security");
return null; return;
} }
catch (Exception e) catch (Exception e)
{ {
Logger.Error("Error reading jwt"); Logger.Error("Error reading jwt");
Logger.Error(e); Logger.Error(e);
return null; return;
} }
// To make it easier to use the json data // To make it easier to use the json data
@@ -101,8 +111,9 @@ public class IdentityService
if (user == null) if (user == null)
{ {
Logger.Warn($"Cannot find user with the id '{userid}' in the database. Maybe the user has been deleted or a token has been successfully faked by a hacker"); Logger.Warn(
return null; $"Cannot find user with the id '{userid}' in the database. Maybe the user has been deleted or a token has been successfully faked by a hacker", "security");
return;
} }
var iat = data.GetValue<long>("iat", -1); var iat = data.GetValue<long>("iat", -1);
@@ -110,46 +121,54 @@ public class IdentityService
if (iat == -1) if (iat == -1)
{ {
Logger.Debug("Legacy token found (without the time the token has been issued at)"); Logger.Debug("Legacy token found (without the time the token has been issued at)");
return null; return;
} }
var iatD = DateTimeOffset.FromUnixTimeSeconds(iat).ToUniversalTime().DateTime; var iatD = DateTimeOffset.FromUnixTimeSeconds(iat).ToUniversalTime().DateTime;
if (iatD < user.TokenValidTime) if (iatD < user.TokenValidTime)
return null; return;
UserCache = user; User = user;
user.LastIp = GetIp(); ConstructPermissions();
UserRepository.Update(user);
return UserCache; User.LastIp = Ip;
UserRepository.Update(User);
} }
catch (Exception e) catch (Exception e)
{ {
Logger.Error("Unexpected error while processing token"); Logger.Error("Unexpected error while processing token");
Logger.Error(e); Logger.Error(e);
return null; return;
} }
} }
public string GetIp() private Task LoadIp()
{ {
if (HttpContextAccessor.HttpContext == null) if (HttpContextAccessor.HttpContext == null)
return "N/A";
if(HttpContextAccessor.HttpContext.Request.Headers.ContainsKey("X-Real-IP"))
{ {
return HttpContextAccessor.HttpContext.Request.Headers["X-Real-IP"]!; Ip = "N/A";
return Task.CompletedTask;
} }
return HttpContextAccessor.HttpContext.Connection.RemoteIpAddress!.ToString(); if (HttpContextAccessor.HttpContext.Request.Headers.ContainsKey("X-Real-IP"))
{
Ip = HttpContextAccessor.HttpContext.Request.Headers["X-Real-IP"]!;
return Task.CompletedTask;
} }
public string GetDevice() Ip = HttpContextAccessor.HttpContext.Connection.RemoteIpAddress!.ToString();
return Task.CompletedTask;
}
private Task LoadDevice()
{ {
if (HttpContextAccessor.HttpContext == null) if (HttpContextAccessor.HttpContext == null)
return "N/A"; {
Device = "N/A";
return Task.CompletedTask;
}
try try
{ {
@@ -159,17 +178,86 @@ public class IdentityService
{ {
var version = userAgent.Remove(0, "Moonlight.App/".Length).Split(' ').FirstOrDefault(); var version = userAgent.Remove(0, "Moonlight.App/".Length).Split(' ').FirstOrDefault();
return "Moonlight App " + version; Device = "Moonlight App " + version;
return Task.CompletedTask;
} }
var uaParser = Parser.GetDefault(); var uaParser = Parser.GetDefault();
var info = uaParser.Parse(userAgent); var info = uaParser.Parse(userAgent);
return $"{info.OS} - {info.Device}"; Device = $"{info.OS} - {info.Device}";
return Task.CompletedTask;
} }
catch (Exception e) catch (Exception e)
{ {
return "UserAgent not present"; Device = "UserAgent not present";
return Task.CompletedTask;
} }
} }
public Task SavePermissions()
{
if (User != null)
{
User.Permissions = UserPermissions.Data;
UserRepository.Update(User);
ConstructPermissions();
}
return Task.CompletedTask;
}
private void ConstructPermissions()
{
if (User == null)
{
UserPermissions = new(Array.Empty<byte>());
GroupPermissions = new(Array.Empty<byte>(), true);
Permissions = new(Array.Empty<byte>(), true);
return;
}
var user = UserRepository
.Get()
.Include(x => x.PermissionGroup)
.First(x => x.Id == User.Id);
UserPermissions = new PermissionStorage(user.Permissions);
if (user.PermissionGroup == null)
GroupPermissions = new PermissionStorage(Array.Empty<byte>(), true);
else
GroupPermissions = new PermissionStorage(user.PermissionGroup.Permissions, true);
if (user.Admin)
{
Permissions = new PermissionStorage(Array.Empty<byte>());
foreach (var permission in Perms.Permissions.GetAllPermissions())
{
Permissions[permission] = true;
}
Permissions.IsReadyOnly = true;
return;
}
Permissions = new(Array.Empty<byte>());
foreach (var permission in Perms.Permissions.GetAllPermissions())
{
Permissions[permission] = GroupPermissions[permission];
}
foreach (var permission in Perms.Permissions.GetAllPermissions())
{
if (UserPermissions[permission])
{
Permissions[permission] = true;
}
}
Permissions.IsReadyOnly = true;
}
} }

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