From 6fe9a0a1bdc5d91f3c58cc7daff3564d0298bc09 Mon Sep 17 00:00:00 2001 From: Marcel Baumgartner Date: Wed, 1 Mar 2023 21:38:58 +0100 Subject: [PATCH] Added domain system --- .gitignore | 7 + Moonlight/App/Exceptions/PaperException.cs | 16 ++ Moonlight/App/Exceptions/WingsException.cs | 2 +- Moonlight/App/Helpers/PaperApiHelper.cs | 7 +- Moonlight/App/Helpers/WingsApiHelper.cs | 2 +- Moonlight/App/Services/DomainService.cs | 106 ++++++++- .../ErrorBoundaries/SoftErrorBoundary.razor | 41 ++++ .../ServerControl/ServerBackups.razor | 12 +- Moonlight/Shared/Layouts/MainLayout.razor | 58 ++--- .../Shared/Views/Admin/Domains/New.razor | 2 +- Moonlight/Shared/Views/Domain/Index.razor | 213 ++++++++++++++---- ....GeneratedMSBuildEditorConfig.editorconfig | 5 +- .../obj/Debug/net6.0/Moonlight.assets.cache | Bin 75557 -> 61028 bytes .../obj/Moonlight.csproj.nuget.dgspec.json | 20 +- Moonlight/obj/project.assets.json | 20 +- Moonlight/obj/project.nuget.cache | 8 +- Moonlight/obj/project.packagespec.json | 2 +- Moonlight/resources/lang/de_de.lang | 10 + 18 files changed, 399 insertions(+), 132 deletions(-) create mode 100644 Moonlight/App/Exceptions/PaperException.cs create mode 100644 Moonlight/Shared/Components/ErrorBoundaries/SoftErrorBoundary.razor diff --git a/.gitignore b/.gitignore index d91cbe44..157e1eee 100644 --- a/.gitignore +++ b/.gitignore @@ -444,3 +444,10 @@ Moonlight/obj/project.nuget.cache Moonlight/obj/project.packagespec.json Moonlight/obj/Debug/net6.0/Moonlight.GeneratedMSBuildEditorConfig.editorconfig *.editorconfig +Moonlight/obj/Debug/net6.0/Moonlight.GeneratedMSBuildEditorConfig.editorconfig +*.cache +*.editorconfig +Moonlight/obj/Moonlight.csproj.nuget.dgspec.json +Moonlight/obj/project.assets.json +Moonlight/obj/project.nuget.cache +Moonlight/obj/project.packagespec.json diff --git a/Moonlight/App/Exceptions/PaperException.cs b/Moonlight/App/Exceptions/PaperException.cs new file mode 100644 index 00000000..02dbc4cf --- /dev/null +++ b/Moonlight/App/Exceptions/PaperException.cs @@ -0,0 +1,16 @@ +namespace Moonlight.App.Exceptions; + +public class PaperException : Exception +{ + public PaperException() + { + } + + public PaperException(string message) : base(message) + { + } + + public PaperException(string message, Exception inner) : base(message, inner) + { + } +} \ No newline at end of file diff --git a/Moonlight/App/Exceptions/WingsException.cs b/Moonlight/App/Exceptions/WingsException.cs index ca9c4b47..a5dc9310 100644 --- a/Moonlight/App/Exceptions/WingsException.cs +++ b/Moonlight/App/Exceptions/WingsException.cs @@ -1,6 +1,6 @@ using System.Runtime.Serialization; -namespace Moonlight.App.Exceptions.Wings; +namespace Moonlight.App.Exceptions; [Serializable] public class WingsException : Exception diff --git a/Moonlight/App/Helpers/PaperApiHelper.cs b/Moonlight/App/Helpers/PaperApiHelper.cs index 129e5457..232b4a8a 100644 --- a/Moonlight/App/Helpers/PaperApiHelper.cs +++ b/Moonlight/App/Helpers/PaperApiHelper.cs @@ -1,4 +1,5 @@ -using Newtonsoft.Json; +using Moonlight.App.Exceptions; +using Newtonsoft.Json; using RestSharp; namespace Moonlight.App.Helpers; @@ -33,7 +34,7 @@ public class PaperApiHelper { if (response.StatusCode != 0) { - throw new Exception( + throw new PaperException( $"An error occured: ({response.StatusCode}) {response.Content}" ); } @@ -43,6 +44,6 @@ public class PaperApiHelper } } - return JsonConvert.DeserializeObject(response.Content); + return JsonConvert.DeserializeObject(response.Content!)!; } } \ No newline at end of file diff --git a/Moonlight/App/Helpers/WingsApiHelper.cs b/Moonlight/App/Helpers/WingsApiHelper.cs index 76ad741c..23900b4a 100644 --- a/Moonlight/App/Helpers/WingsApiHelper.cs +++ b/Moonlight/App/Helpers/WingsApiHelper.cs @@ -1,5 +1,5 @@ using Moonlight.App.Database.Entities; -using Moonlight.App.Exceptions.Wings; +using Moonlight.App.Exceptions; using Newtonsoft.Json; using RestSharp; diff --git a/Moonlight/App/Services/DomainService.cs b/Moonlight/App/Services/DomainService.cs index 5b22cee0..08ee4669 100644 --- a/Moonlight/App/Services/DomainService.cs +++ b/Moonlight/App/Services/DomainService.cs @@ -1,12 +1,16 @@ using CloudFlare.Client; using CloudFlare.Client.Api.Authentication; +using CloudFlare.Client.Api.Parameters.Data; using CloudFlare.Client.Api.Result; using CloudFlare.Client.Api.Zones; +using CloudFlare.Client.Api.Zones.DnsRecord; +using CloudFlare.Client.Enumerators; +using Logging.Net; using Microsoft.EntityFrameworkCore; using Moonlight.App.Database.Entities; using Moonlight.App.Exceptions; -using Moonlight.App.Models.Misc; using Moonlight.App.Repositories.Domains; +using DnsRecord = Moonlight.App.Models.Misc.DnsRecord; namespace Moonlight.App.Services; @@ -107,6 +111,95 @@ public class DomainService return result.ToArray(); } + public async Task AddDnsRecord(Domain d, DnsRecord dnsRecord) + { + var domain = EnsureData(d); + + var rname = $"{domain.Name}.{domain.SharedDomain.Name}"; + var dname = $".{rname}"; + + if (dnsRecord.Type == DnsRecordType.Srv) + { + var parts = dnsRecord.Name.Split("."); + Enum.TryParse(parts[1], out Protocol protocol); + + var valueParts = dnsRecord.Content.Split(" "); + + var nameWithoutProt = dnsRecord.Name.Replace($"{parts[0]}.{parts[1]}.", ""); + nameWithoutProt = nameWithoutProt.Replace($"{parts[0]}.{parts[1]}", ""); + var name = nameWithoutProt == "" ? rname : nameWithoutProt + dname; + + var srv = new NewDnsRecord() + { + Type = dnsRecord.Type, + Data = new() + { + + Service = parts[0], + Protocol = protocol, + Name = name, + Weight = int.Parse(valueParts[0]), + Port = int.Parse(valueParts[1]), + Target = valueParts[2], + Priority = dnsRecord.Priority + }, + Proxied = dnsRecord.Proxied, + Ttl = dnsRecord.Ttl, + }; + + GetData(await Client.Zones.DnsRecords.AddAsync(d.SharedDomain.CloudflareId, srv)); + } + else + { + var name = string.IsNullOrEmpty(dnsRecord.Name) ? rname : dnsRecord.Name + dname; + + GetData(await Client.Zones.DnsRecords.AddAsync(d.SharedDomain.CloudflareId, new NewDnsRecord() + { + Type = dnsRecord.Type, + Priority = dnsRecord.Priority, + Content = dnsRecord.Content, + Proxied = dnsRecord.Proxied, + Ttl = dnsRecord.Ttl, + Name = name + })); + } + } + + public async Task UpdateDnsRecord(Domain d, DnsRecord dnsRecord) + { + var domain = EnsureData(d); + + var rname = $"{domain.Name}.{domain.SharedDomain.Name}"; + var dname = $".{rname}"; + + if (dnsRecord.Type == DnsRecordType.Srv) + { + throw new DisplayException("SRV records cannot be updated thanks to the cloudflare api client. Please delete the record and create a new one"); + } + else + { + var name = dnsRecord.Name == "" ? rname : dnsRecord.Name + dname; + + GetData(await Client.Zones.DnsRecords.UpdateAsync(d.SharedDomain.CloudflareId, dnsRecord.Id, new ModifiedDnsRecord() + { + Content = dnsRecord.Content, + Proxied = dnsRecord.Proxied, + Ttl = dnsRecord.Ttl, + Name = name, + Type = dnsRecord.Type + })); + } + } + + public async Task DeleteDnsRecord(Domain d, DnsRecord dnsRecord) + { + var domain = EnsureData(d); + + GetData( + await Client.Zones.DnsRecords.DeleteAsync(domain.SharedDomain.CloudflareId, dnsRecord.Id) + ); + } + private Domain EnsureData(Domain domain) { if (domain.SharedDomain != null) @@ -121,7 +214,16 @@ public class DomainService { if (!result.Success) { - var message = result.Errors.First().ErrorChain.First().Message; + string message; + + try + { + message = result.Errors.First().ErrorChain.First().Message; + } + catch (Exception) + { + throw new CloudflareException("No error message provided"); + } throw new CloudflareException(message); } diff --git a/Moonlight/Shared/Components/ErrorBoundaries/SoftErrorBoundary.razor b/Moonlight/Shared/Components/ErrorBoundaries/SoftErrorBoundary.razor new file mode 100644 index 00000000..a9c6ad76 --- /dev/null +++ b/Moonlight/Shared/Components/ErrorBoundaries/SoftErrorBoundary.razor @@ -0,0 +1,41 @@ +@using Moonlight.App.Services.Interop +@using Moonlight.App.Exceptions +@using Moonlight.App.Services +@inherits ErrorBoundaryBase + +@inject AlertService AlertService +@inject SmartTranslateService SmartTranslateService + +@ChildContent + +@code +{ + protected override async Task OnErrorAsync(Exception exception) + { + if (exception is DisplayException displayException) + { + await AlertService.Error( + SmartTranslateService.Translate("Error"), + SmartTranslateService.Translate(displayException.Message) + ); + } + else if (exception is CloudflareException cloudflareException) + { + await AlertService.Error( + SmartTranslateService.Translate("Error from cloudflare api"), + cloudflareException.Message + ); + } + else if (exception is WingsException wingsException) + { + await AlertService.Error( + SmartTranslateService.Translate("Error from daemon"), + wingsException.Message + ); + } + else + { + throw exception; + } + } +} \ No newline at end of file diff --git a/Moonlight/Shared/Components/ServerControl/ServerBackups.razor b/Moonlight/Shared/Components/ServerControl/ServerBackups.razor index 84cf46e1..29f988dd 100644 --- a/Moonlight/Shared/Components/ServerControl/ServerBackups.razor +++ b/Moonlight/Shared/Components/ServerControl/ServerBackups.razor @@ -112,10 +112,10 @@ protected override void OnInitialized() { - MessageService.Subscribe("wings.backups.create", this, async (backup) => + MessageService.Subscribe("wings.backups.create", this, (backup) => { if (AllBackups == null) - return; + return Task.CompletedTask; if (AllBackups.Any(x => x.Id == backup.Id)) { @@ -126,12 +126,14 @@ await LazyLoader.Reload(); }); } + + return Task.CompletedTask; }); - MessageService.Subscribe("wings.backups.createfailed", this, async (backup) => + MessageService.Subscribe("wings.backups.createfailed", this, (backup) => { if (AllBackups == null) - return; + return Task.CompletedTask; if (AllBackups.Any(x => x.Id == backup.Id)) { @@ -141,6 +143,8 @@ await LazyLoader.Reload(); }); } + + return Task.CompletedTask; }); MessageService.Subscribe("wings.backups.delete", this, async (backup) => diff --git a/Moonlight/Shared/Layouts/MainLayout.razor b/Moonlight/Shared/Layouts/MainLayout.razor index f28d0c42..49503db8 100644 --- a/Moonlight/Shared/Layouts/MainLayout.razor +++ b/Moonlight/Shared/Layouts/MainLayout.razor @@ -59,40 +59,42 @@
- @if (uri.LocalPath != "/login" && - uri.LocalPath != "/register") - { - if (User == null) + + @if (uri.LocalPath != "/login" && + uri.LocalPath != "/register") { - - } - else - { - if (User.Status == UserStatus.Banned) + if (User == null) { - - } - else if (User.Status == UserStatus.Disabled) - { - + } else { - @Body + if (User.Status == UserStatus.Banned) + { + + } + else if (User.Status == UserStatus.Disabled) + { + + } + else + { + @Body + } } } - } - else - { - if (uri.LocalPath == "/login") + else { - + if (uri.LocalPath == "/login") + { + + } + else if (uri.LocalPath == "/register") + { + + } } - else if (uri.LocalPath == "/register") - { - - } - } +
@@ -160,7 +162,7 @@ await ToastService.Info($"Support: {message.Message}"); } }); - + RunDelayedMenu(0); RunDelayedMenu(1); RunDelayedMenu(3); @@ -204,8 +206,8 @@ } catch (Exception e) { - //Logger.Warn("Delayed menu error"); - //Logger.Warn(e); + //Logger.Warn("Delayed menu error"); + //Logger.Warn(e); } }); } diff --git a/Moonlight/Shared/Views/Admin/Domains/New.razor b/Moonlight/Shared/Views/Admin/Domains/New.razor index a2cf2011..a15a8239 100644 --- a/Moonlight/Shared/Views/Admin/Domains/New.razor +++ b/Moonlight/Shared/Views/Admin/Domains/New.razor @@ -111,7 +111,7 @@ { DomainRepository.Add(new() { - Name = Name, + Name = Name.ToLower(), Owner = User!, SharedDomain = SharedDomain! }); diff --git a/Moonlight/Shared/Views/Domain/Index.razor b/Moonlight/Shared/Views/Domain/Index.razor index 9a4254a9..06450ff7 100644 --- a/Moonlight/Shared/Views/Domain/Index.razor +++ b/Moonlight/Shared/Views/Domain/Index.razor @@ -5,11 +5,16 @@ @using Microsoft.EntityFrameworkCore @using Moonlight.App.Models.Misc @using Moonlight.App.Services +@using CloudFlare.Client.Enumerators +@using Logging.Net +@using Moonlight.App.Exceptions +@using Moonlight.App.Services.Interop @inject IdentityService IdentityService @inject DomainRepository DomainRepository @inject DomainService DomainService @inject SmartTranslateService SmartTranslateService +@inject ToastService ToastService @if (Domain == null) @@ -20,64 +25,148 @@ } else { + var domainNameBuilt = $"{Domain.Name}.{Domain.SharedDomain.Name}"; +
- DNS records for@($"{Domain.Name}.{Domain.SharedDomain.Name}") + DNS records for@(domainNameBuilt)
- +
+
- +
+
+ +
+
+ +
+
+ +
+
+ + +
+
+ +
+
+ +
+
+
+
+ + +
+
-
- @if (DnsRecords.Any()) - { -
-
-
- @foreach (var record in DnsRecords) - { -
-

- -

-
-
- + @if (DnsRecords.Any()) + { +
+
+ @foreach (var record in DnsRecords) + { +
+
+

+ +

+
+
+
+
+ +
+
+ +
+
+ +
+
+ + +
+
+ +
+
+
- } +
+
+
+
+ + +
+
+ + + +
+
+
+
-
+ }
- } - else - { -
- No dns records found -
- } -
+
+ } + else + { +
+
+ No dns records found +
+ }
} @@ -90,6 +179,13 @@ private Domain? Domain; private DnsRecord[] DnsRecords; + private DnsRecord NewRecord = new() + { + Ttl = 1, + Priority = 0 + }; + + private LazyLoader DnsLazyLoader; private async Task Load(LazyLoader arg) { @@ -122,4 +218,29 @@ await lazyLoader.SetText(SmartTranslateService.Translate("Fetching dns records")); DnsRecords = await DomainService.GetDnsRecords(Domain!); } + + private async Task Save(DnsRecord record) + { + await DomainService.UpdateDnsRecord(Domain!, record); + await DnsLazyLoader.Reload(); + } + + private async Task Add() + { + await DomainService.AddDnsRecord(Domain!, NewRecord); + + NewRecord = new() + { + Ttl = 1, + Priority = 0 + }; + + await DnsLazyLoader.Reload(); + } + + private async Task Delete(DnsRecord record) + { + await DomainService.DeleteDnsRecord(Domain!, record); + await DnsLazyLoader.Reload(); + } } \ No newline at end of file diff --git a/Moonlight/obj/Debug/net6.0/Moonlight.GeneratedMSBuildEditorConfig.editorconfig b/Moonlight/obj/Debug/net6.0/Moonlight.GeneratedMSBuildEditorConfig.editorconfig index b074bb01..b5bb24f1 100644 --- a/Moonlight/obj/Debug/net6.0/Moonlight.GeneratedMSBuildEditorConfig.editorconfig +++ b/Moonlight/obj/Debug/net6.0/Moonlight.GeneratedMSBuildEditorConfig.editorconfig @@ -5,7 +5,6 @@ build_property.UsingMicrosoftNETSdkWeb = true build_property.ProjectTypeGuids = build_property.InvariantGlobalization = build_property.PlatformNeutralAssembly = -build_property.EnforceExtendedAnalyzerRules = build_property._SupportedPlatformList = Linux,macOS,Windows build_property.RootNamespace = Moonlight build_property.RootNamespace = Moonlight @@ -52,6 +51,10 @@ build_metadata.AdditionalFiles.CssScope = build_metadata.AdditionalFiles.TargetPath = U2hhcmVkXENvbXBvbmVudHNcRXJyb3JCb3VuZGFyaWVzXFBhZ2VFcnJvckJvdW5kYXJ5LnJhem9y build_metadata.AdditionalFiles.CssScope = +[C:/Users/marce/GitHub/Moonlight-Panel/Moonlight/Moonlight/Shared/Components/ErrorBoundaries/SoftErrorBoundary.razor] +build_metadata.AdditionalFiles.TargetPath = U2hhcmVkXENvbXBvbmVudHNcRXJyb3JCb3VuZGFyaWVzXFNvZnRFcnJvckJvdW5kYXJ5LnJhem9y +build_metadata.AdditionalFiles.CssScope = + [C:/Users/marce/GitHub/Moonlight-Panel/Moonlight/Moonlight/Shared/Components/FileManagerPartials/FileEditor.razor] build_metadata.AdditionalFiles.TargetPath = U2hhcmVkXENvbXBvbmVudHNcRmlsZU1hbmFnZXJQYXJ0aWFsc1xGaWxlRWRpdG9yLnJhem9y build_metadata.AdditionalFiles.CssScope = diff --git a/Moonlight/obj/Debug/net6.0/Moonlight.assets.cache b/Moonlight/obj/Debug/net6.0/Moonlight.assets.cache index 7d5c506542983535534b821e8ecb1efbc3e69488..df97427a70303609918770b019a717a405de0083 100644 GIT binary patch delta 5954 zcmai&30PFu6~}qXz6;CP$Y9*42qAc=9 z#{~tAxPqdDxC?6QZX{GqY>k?lbW0mGNwraBZjy;I5^~7E>0>kJRMiNZ`B~j_)F8RnbYq@eda#7<2&2^C!QMbqS><7 zbU<>9Oi7=Mv7NPO(z?R2Lyi<#_jZS}es!t+Q~Z&c)I)N@rjT$H=T0(pkQ^L3k~xw& zkvWsOkhzk%k-3w3kagO`XWa$^6JK zWd3AA7Vu2AZE>_&LoQCHCBKk$CKIwivNw>X)l;4k7?an}7(^aUsUyEob|DkWVASMg zYeZxSnX(X}u#d$c`G2T_sBGjCsFJWFxw^MOg3q;fP}x zVwVI5ak+$1Ze#58n@y4BOJ2}&QQ;DvCM98DzMh42Bgh0hq+nbO&gJuy`Wb4=!r}hF zgi%ZRv?EZvbRnBScL^`M_Qo<76V@*aWNs2x8IQ3ZN-*eD1S(NGY~R?K9tcyCy*|`>P#Z~Y`>I>W7eyvs zoW8IWrm);-8XXTykpb22v3Oj_&n*Vu91KTd(MWb)v8XA^V@LX7$hL4amiXdE@-Q4O z=9l#a+$mnm>f^AX1ngq;myjFK3wI}J@VVCzoLR{y9*=uFL;`xmmnO5I0TPqbQVTrQ7`T%MQyPkBa( z)nAN^5?4g74`4U;XhN)Q$;JaduX`d@lWfoGsfu6>2{P#&j7M^MAFM9t52{~Kl)d72 z{XLLc?t*gdXghZ520o9MWo$GWHjHC4O{9pq^x$Zm>^umS8~L3)mX4Qq*>b*uw2BOt z{3?;tiEMy#<*1k>>WMu#j#{w?$5Z=LdvF4ckF~3J2U+lGCBJVbN|0tuLi#5D&`g#v zM>n%{Ge4D^_6! zqfj2*GwG!ZO~Cc7ql0EKp(2%EQmt088{kbE&4b0<9kUId2-}`)&LEF6;Y<}~7Wp%f z)-?hZVF?J>!7u)7k(?2!r@gUo$K;?nDzI!ifowrJDx-6;X(zw4=g~6F4tYyOH=q1d z(KV+hu4Q{7Br6C{vON%3-9`7disDxiERjYSyLU!mb%O34m9Pa;>AZOKs2LLUu1dtO zDKQf&=9zG{hTr!KCG1^j#ELyW7`A(W`8NbomVJ>5IG6ll!Fr&Ol(%<}GrreOxmZP+ zhqOIKxLU;@-Am{SGl+CN;5*-nf?EC>%eQ}E8G8ERyV@k<(sqwtrt;_ldUP*Do^wH} z`BmKC%U=)6NnJrx>jU3DKA5#{fo_EguTU)0U|8!CbVVv5#o{WWi@h$(TtXqr(ymmI zl#+kB{awhtq{W&KXXv03Stj9lmJze1NP&=i;^yL78)v z{G_eaD*xEoMt$*T(RQ*OWIK_4_)Od``c0{>roOn%+N*2GC)B&ignAFDqft0igT|xL zxN~HbwU)d}(q8Iwq@a1Yb9#SsQ3XNo=G(M?6k` zK|Dbwh$qR8wIjyZrfa0(!(Hj}5nG$?!kVei!Tvyg z0lQ2lU{}bLFx!(rDG*I32H47jq=AS$>>78KLX_xFsn4N5BfmgjBNOO9(jOb$Feg)k zC2#pbcQF)Ar_9zr(TI|Eo%({-{(lFblTXlYkO|sNvZeNO@y84=RGyj=*Pe8XA~@0) z`TnwS2x#4b(8ye4cF@H@XT*Ch(zexC51`ZB2 zZI@Ww`Ecl)wYFO!Qa@}zA87uU3g#ObbBEg2M5+~-eDhruT_%Jg#2P{l$jOC6U=yy@>GQ)9wjKxCzjZh zX_aVHnM()iaJh7pDax0sC6~k|BUMOa-)&h7wAkI`^BmPth3X_zmAh|M+Y-7oXPcc> zlrBs@WeR0rTTTO;`y=;^V+1!Vx96Tu|057zT|M#WggXq)8u1S5(J<7-R^lN!w%rNs zzscNH()5rYpj^VLwls6E7@qBn0i%8K@N%%pQw8Pq914T4G@86s!hPiMXUmS8y<5Ne zro{NF$U4a~KWfD`_NTV>C!p@ztHGglfsYcLY}X{o6DjoHo55q9rM_5sON)@3_FjFF HWQ+JOHhZzp delta 10394 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 m7vr>O!PlCc(pboUGw%HSHwuD