diff --git a/ProjectManager.Backend/Apis/IProjectApi.cs b/ProjectManager.Backend/Apis/IProjectApi.cs index 9235232..6253259 100644 --- a/ProjectManager.Backend/Apis/IProjectApi.cs +++ b/ProjectManager.Backend/Apis/IProjectApi.cs @@ -8,19 +8,21 @@ public interface IProjectApi { public Project[] GetProjects(string userId); public Project GetProject(string projectId); public Task AddProject(string name, string ownerId); - public bool EditProject(string projectId, string name); + public Task EditProject(string projectId, string name, string domain); public Task DeleteProject(string projectId); } public class ProjectApi : IProjectApi { private readonly DatabaseContext _context; private readonly GeneralOptions _options; + private readonly ProxyOptions _proxyOptions; private readonly IDockerApi _docker; private readonly IProxyApi _proxy; - public ProjectApi(DatabaseContext context, IOptions options, IDockerApi docker, IProxyApi proxy) { + public ProjectApi(DatabaseContext context, IOptions options, IOptions proxyOptions, IDockerApi docker, IProxyApi proxy) { _context = context; _options = options.Value; + _proxyOptions = proxyOptions.Value; _docker = docker; _proxy = proxy; } @@ -49,12 +51,13 @@ public class ProjectApi : IProjectApi { Name = name, Port = port }; + project.Domain = _proxyOptions.Enable ? $"{project.ProjectId}.{_proxyOptions.Domain}" : $"{_proxyOptions.Host}:{project.Port}"; var container = await _docker.CreateContainer($"ghcr.io/muchobien/pocketbase:{_options.PocketBaseVersion}", port, _options.Root + project.ProjectId, $"{project.Name}_{project.ProjectId}"); await _docker.StartContainer(container); project.ContainerName = container; - var (proxyId, certificateId) = await _proxy.AddLocation(project.ProjectId, project.Port); + var (proxyId, certificateId) = await _proxy.AddLocation(project.Domain, project.Port); project.ProxyId = proxyId; project.CertificateId = certificateId; @@ -64,13 +67,22 @@ public class ProjectApi : IProjectApi { return project.ProjectId; } - public bool EditProject(string projectId, string name) { + public async Task EditProject(string projectId, string name, string domain) { if (name.Length > 255) return false; + if (domain.Length > 255) return false; var project = GetProject(projectId); if (project == null) return false; project.Name = name; + + if (!string.IsNullOrEmpty(domain)) { + project.Domain = domain; + var data = await _proxy.UpdateLocation(project); + project.ProxyId = data.Item1; + project.CertificateId = data.Item2; + } + _context.Projects.Update(project); - _context.SaveChanges(); + await _context.SaveChangesAsync(); return true; } diff --git a/ProjectManager.Backend/Apis/IProxyApi.cs b/ProjectManager.Backend/Apis/IProxyApi.cs index 5ae4598..24991b6 100644 --- a/ProjectManager.Backend/Apis/IProxyApi.cs +++ b/ProjectManager.Backend/Apis/IProxyApi.cs @@ -1,13 +1,15 @@ using System.Dynamic; using System.Text.Json; using Microsoft.Extensions.Options; +using ProjectManager.Backend.Entities; using ProjectManager.Backend.Options; namespace ProjectManager.Backend.Apis; public interface IProxyApi { - public Task<(int, int)> AddLocation(string projectId, int port); + public Task<(int, int)> AddLocation(string domain, int port); public Task RemoveLocation(int proxyId, int certificateId); + public Task<(int, int)> UpdateLocation(Project project); } public sealed class ProxyApi : IProxyApi { @@ -27,11 +29,11 @@ public sealed class ProxyApi : IProxyApi { _client.DefaultRequestHeaders.Add("Authorization", $"Bearer {response?.token}"); } - public async Task<(int, int)> AddLocation(string projectId, int port) { + public async Task<(int, int)> AddLocation(string domain, int port) { if (!_options.Enable) return (-1, -1); await Login(); var result = await _client.PostAsJsonAsync(_options.Url + "/api/nginx/proxy-hosts", - new CreateData($"{projectId}.{_options.Domain}", _options.Host, port, _options.Email)); + new ProxyData(domain, _options.Host, port, _options.Email)); dynamic data = await result.Content.ReadFromJsonAsync(); if (data == null) return (-1, -1); int id = Convert.ToInt32($"{data.id}"); @@ -46,9 +48,15 @@ public sealed class ProxyApi : IProxyApi { await _client.DeleteAsync(_options.Url + "/api/nginx/certificates/" + certificateId); } + public async Task<(int, int)> UpdateLocation(Project project) { + if (!_options.Enable) return (-1, -1); + await RemoveLocation(project.ProxyId, project.CertificateId); + return await AddLocation(project.Domain, project.Port); + } + private sealed record ProxyAuth(string identity, string secret); private sealed record TokenResponse(string token, string expires); - private sealed class CreateData { + private sealed class ProxyData { public string access_list_id { get; set; } = "0"; public string advanced_config { get; set; } = " location / {\r\n proxy_pass http://%docker_ip%;\r\n proxy_hide_header X-Frame-Options;\r\n }"; public bool allow_websocket_upgrade { get; set; } = true; @@ -66,7 +74,7 @@ public sealed class ProxyApi : IProxyApi { public bool ssl_forced { get; set; } = true; public SslMeta meta { get; set; } - public CreateData(string domain, string ip, int port, string email) { + public ProxyData(string domain, string ip, int port, string email) { domain_names = new[] { domain }; forward_host = ip; forward_port = port; diff --git a/ProjectManager.Backend/Controllers/ProjectController.cs b/ProjectManager.Backend/Controllers/ProjectController.cs index 88b0990..1abcfe4 100644 --- a/ProjectManager.Backend/Controllers/ProjectController.cs +++ b/ProjectManager.Backend/Controllers/ProjectController.cs @@ -70,11 +70,11 @@ public class ProjectController : ControllerBase { [Authorized] [HttpPut("{projectId}")] - public IActionResult EditProject(string projectId, [FromBody] ProjectEdit edit) { + public async Task EditProject(string projectId, [FromBody] ProjectEdit edit) { var project = _projects.GetProject(projectId); if (project == null) return NotFound(); if (project.OwnerId != _context.UserId) return Unauthorized(); - _projects.EditProject(projectId, edit.Name); + await _projects.EditProject(projectId, edit.Name, edit.Domain); return Ok(); } @@ -84,7 +84,7 @@ public class ProjectController : ControllerBase { var project = _projects.GetProject(projectId); if (project == null) return NotFound(); if (project.OwnerId != _context.UserId) return Unauthorized(); - if (_options.Enable) return Redirect($"https://{projectId}.{_options.Domain}/_/"); + if (_options.Enable) return Redirect($"https://{project.Domain}/_/"); return Redirect($"http://{_options.Host}:{project.Port}/_/"); } @@ -94,7 +94,7 @@ public class ProjectController : ControllerBase { var project = _projects.GetProject(projectId); if (project == null) return NotFound(); if (project.OwnerId != _context.UserId) return Unauthorized(); - if (_options.Enable) return Ok(new {url = $"https://{projectId}.{_options.Domain}/_/"}); + if (_options.Enable) return Ok(new {url = $"https://{project.Domain}/_/"}); return Ok(new {url = $"http://{_options.Host}:{project.Port}/_/"}); } diff --git a/ProjectManager.Backend/Entities/Project.cs b/ProjectManager.Backend/Entities/Project.cs index e30f4db..6817551 100644 --- a/ProjectManager.Backend/Entities/Project.cs +++ b/ProjectManager.Backend/Entities/Project.cs @@ -4,6 +4,7 @@ public class Project { public string ProjectId { get; set; } public string OwnerId { get; set; } public string Name { get; set; } + public string Domain { get; set; } public int Port { get; set; } public string ContainerName { get; set; } public int ProxyId { get; set; } = -1; @@ -12,4 +13,5 @@ public class Project { public class ProjectEdit { public string Name { get; set; } + public string Domain { get; set; } } \ No newline at end of file diff --git a/ProjectManager.Frontend/src/app/components/dialog/dialog.component.html b/ProjectManager.Frontend/src/app/components/dialog/dialog.component.html index 5bd4284..35d1071 100644 --- a/ProjectManager.Frontend/src/app/components/dialog/dialog.component.html +++ b/ProjectManager.Frontend/src/app/components/dialog/dialog.component.html @@ -1,6 +1,6 @@

{{data.title}}

{{data.subtitle}}
-
+
diff --git a/ProjectManager.Frontend/src/app/components/text-dialog/text-dialog.component.scss b/ProjectManager.Frontend/src/app/components/text-dialog/text-dialog.component.scss index e69de29..ad0a793 100644 --- a/ProjectManager.Frontend/src/app/components/text-dialog/text-dialog.component.scss +++ b/ProjectManager.Frontend/src/app/components/text-dialog/text-dialog.component.scss @@ -0,0 +1,13 @@ +form { + width: 350px; + + & > * { + width: 100%; + } +} + +.buttons { + width: 100%; + display: flex; + justify-content: space-evenly; +} diff --git a/ProjectManager.Frontend/src/app/components/text-dialog/text-dialog.component.ts b/ProjectManager.Frontend/src/app/components/text-dialog/text-dialog.component.ts index c874605..2390699 100644 --- a/ProjectManager.Frontend/src/app/components/text-dialog/text-dialog.component.ts +++ b/ProjectManager.Frontend/src/app/components/text-dialog/text-dialog.component.ts @@ -12,4 +12,5 @@ export class TextDialogComponent { public dialogRef: MatDialogRef, @Inject(MAT_DIALOG_DATA) public data: DialogData, ) {} + } diff --git a/ProjectManager.Frontend/src/app/entities/language.ts b/ProjectManager.Frontend/src/app/entities/language.ts index c3db1da..3b95577 100644 --- a/ProjectManager.Frontend/src/app/entities/language.ts +++ b/ProjectManager.Frontend/src/app/entities/language.ts @@ -16,12 +16,16 @@ open: string, edit: string, delete: string, + deleteProjQuestion: string, // Popups name: string, + domain: string, cancel: string, createProject: string, - editProject: string + editProject: string, + updateProject: string, + deleteProject: string, // Profile profile: string, diff --git a/ProjectManager.Frontend/src/app/entities/project.ts b/ProjectManager.Frontend/src/app/entities/project.ts index a5620eb..716e24b 100644 --- a/ProjectManager.Frontend/src/app/entities/project.ts +++ b/ProjectManager.Frontend/src/app/entities/project.ts @@ -2,6 +2,7 @@ projectId?: string; ownerId?: string; name?: string; + domain?: string; port?: number; containerName?: string; proxyId?: number; diff --git a/ProjectManager.Frontend/src/app/services/lang.service.ts b/ProjectManager.Frontend/src/app/services/lang.service.ts index 6d51a70..0ebf87d 100644 --- a/ProjectManager.Frontend/src/app/services/lang.service.ts +++ b/ProjectManager.Frontend/src/app/services/lang.service.ts @@ -21,19 +21,19 @@ export class LangService { const res = await firstValueFrom(this.crud.client.get<{files: string[]}>(location?.origin + "/lang")); const languages = res.files; this.allLanguages = languages.map(lang => lang.replace(".json", "")); - const tasks = []; - - for (let lang of languages) { - const task = firstValueFrom(this.crud.client.get(location?.origin + "/assets/languages/" + lang)) - .then(result => this.languages.set(lang.replace(".json", ""), result)); - tasks.push(task); - } - - await Promise.all(tasks); - this.currentLang = this.languages.get(this.storage.getItem("language") || "en-US"); + this.currentLang = await this.loadLanguage(this.storage.getItem("language") || "en-US"); } - public setLanguage(lang: string) { + private async loadLanguage(name: string): Promise { + const lang = await firstValueFrom(this.crud.client.get(`${location.origin}/assets/languages/${name}.json`)); + this.languages.set(name, lang); + return lang; + } + + public async setLanguage(lang: string) { + if (this.languages.get(lang) == undefined) + await this.loadLanguage(lang); + this.currentLang = this.languages.get(lang); this.storage.setItem("language", lang); } diff --git a/ProjectManager.Frontend/src/app/services/project.service.ts b/ProjectManager.Frontend/src/app/services/project.service.ts index eb88f0e..996cb51 100644 --- a/ProjectManager.Frontend/src/app/services/project.service.ts +++ b/ProjectManager.Frontend/src/app/services/project.service.ts @@ -31,8 +31,8 @@ export class ProjectService { return response.content?.projectId; } - public async editProject(projectId: string, name: string): Promise { - const response = await this.crud.sendPutRequest("projects/" + projectId, {name}); + public async editProject(projectId: string, name: string, domain: string): Promise { + const response = await this.crud.sendPutRequest("projects/" + projectId, {name, domain}); return response.success; } diff --git a/ProjectManager.Frontend/src/app/sites/dashboard/dashboard.component.ts b/ProjectManager.Frontend/src/app/sites/dashboard/dashboard.component.ts index bb7dff9..7a144ae 100644 --- a/ProjectManager.Frontend/src/app/sites/dashboard/dashboard.component.ts +++ b/ProjectManager.Frontend/src/app/sites/dashboard/dashboard.component.ts @@ -33,24 +33,26 @@ export class DashboardComponent { public async editProject(projectId: string) { const dialogRef = this.dialog.open(TextDialogComponent, { - data: {title: "Projekt umbenennen", subtitle: "Name", buttons: [ - {text: "Abbrechen", value: false}, - {text: "Projekt bearbeiten", value: true, color: 'primary'} - ]} + data: {title: this.langs.currentLang?.editProject, subtitle: "Name", secondInput: this.langs.currentLang?.domain, buttons: [ + {text: this.langs.currentLang?.cancel, value: false}, + {text: this.langs.currentLang?.editProject, value: true, color: 'primary'} + ]} }); - const result = await firstValueFrom(dialogRef.afterClosed()) as {success: boolean, data: string}; + const result = await firstValueFrom(dialogRef.afterClosed()) as {success: boolean, data: string, data2: string}; if (!result?.success) return; - await this.projects.editProject(projectId, result.data); + NavigationComponent.spinnerVisible = true; + await this.projects.editProject(projectId, result.data, result.data2); + NavigationComponent.spinnerVisible = false; await this.projects.loadProjects(); - this.snackBar.open("Projekt aktualisiert!", undefined, {duration: 2000}); + this.snackBar.open(this.langs.currentLang?.updateProject, undefined, {duration: 2000}); } public async deleteProject(projectId: string) { const dialogRef = this.dialog.open(DialogComponent, { - data: {title: "Möchtest du das Projekt wirklich löschen?", subtitle: "Alle gespeicherten Daten gehen dann verloren!", buttons: [ - {text: "Abbrechen", value: false}, - {text: "Löschen", value: true, color: 'warn'} + data: {title: this.langs.currentLang?.deleteProjQuestion, subtitle: this.langs.currentLang?.deleteWarning, buttons: [ + {text: this.langs.currentLang?.cancel, value: false}, + {text: this.langs.currentLang?.delete, value: true, color: 'warn'} ]} }); @@ -60,7 +62,7 @@ export class DashboardComponent { await this.projects.deleteProject(projectId); NavigationComponent.spinnerVisible = false; await this.projects.loadProjects(); - this.snackBar.open("Projekt gelöscht!", undefined, {duration: 2000}); + this.snackBar.open(this.langs.currentLang?.deleteProject, undefined, {duration: 2000}); } public async updateProjectStatus(projectId: string, start: boolean) { diff --git a/ProjectManager.Frontend/src/assets/languages/de-DE.json b/ProjectManager.Frontend/src/assets/languages/de-DE.json index 2d6f6e1..893f78a 100644 --- a/ProjectManager.Frontend/src/assets/languages/de-DE.json +++ b/ProjectManager.Frontend/src/assets/languages/de-DE.json @@ -14,11 +14,15 @@ "open": "Öffnen", "edit": "Bearbeiten", "delete": "Löschen", + "deleteProjQuestion": "Möchtest du dieses Projekt wirklich löschen?", "name": "Name", + "domain": "Domain", "cancel": "Abbrechen", "createProject": "Projekt erstellen", "editProject": "Projekt bearbeiten", + "updateProject": "Projekt aktualisiert!", + "deleteProject": "Projekt gelöscht!", "profile": "Profil", "profileSub": "Einstellungen", @@ -32,7 +36,7 @@ "updateFailed": "Aktualisierung fehlgeschlagen", "accountUpdated": "Account aktualisiert!", "deleteQuestion": "Möchtest du deinen Account wirklich löschen?", - "deleteWarning": "All deine Projekte werden für immer gelöscht!", + "deleteWarning": "Alle Daten werden für immer gelöscht!", "accountDeleted": "Account gelöscht!", "submit": "Bestätigen", diff --git a/ProjectManager.Frontend/src/assets/languages/en-US.json b/ProjectManager.Frontend/src/assets/languages/en-US.json index 57159da..c5f134e 100644 --- a/ProjectManager.Frontend/src/assets/languages/en-US.json +++ b/ProjectManager.Frontend/src/assets/languages/en-US.json @@ -14,11 +14,15 @@ "open": "Open", "edit": "Edit", "delete": "Delete", + "deleteProjQuestion": "Do you really want to delete this project?", "name": "Name", + "domain": "Domain", "cancel": "Cancel", "createProject": "Create project", "editProject": "Edit project", + "updateProject": "Updated project!", + "deleteProject": "Deleted project!", "profile": "Profile", "profileSub": "Settings", @@ -32,7 +36,7 @@ "updateFailed": "Update failed", "accountUpdated": "Account updated", "deleteQuestion": "Do you really want to delete your account?", - "deleteWarning": "All your data will be lost!", + "deleteWarning": "All data will be lost forever!", "accountDeleted": "Account deleted!", "submit": "Submit", diff --git a/ProjectManager.sql b/ProjectManager.sql index af35d0e..40739b3 100644 --- a/ProjectManager.sql +++ b/ProjectManager.sql @@ -3,6 +3,7 @@ CREATE TABLE `Projects` ( `ownerId` varchar(36) DEFAULT NULL, `name` varchar(255) DEFAULT NULL, `port` int(5) DEFAULT NULL, + `domain` varchar(255) DEFAULT NULL, `containerName` varchar(255) DEFAULT NULL, `proxyId` int(30) DEFAULT NULL, `certificateId` int(30) DEFAULT NULL diff --git a/README.md b/README.md index 261e224..a453ac8 100644 --- a/README.md +++ b/README.md @@ -6,9 +6,9 @@ Es handelt sich hierbei um ein einfach zu benutzendes WebInterface zum Erstellen - [x] Automatische Docker Konfiguration - [x] Automatisches DNS Mapping - [x] Automatische SSL-Konfiguration mithilfe von NginxProxyManager +- [x] Mehrere Sprachen +- [x] Eigene Domains - [ ] Projekte exportieren / importieren -- [ ] Eigene Domains -- [ ] Mehrere Sprachen ## Installation Die Installation erfolgt durch eine Docker-Compose Datei. Hierbei werden zwei Container, einer für das Backend und einer für das Frontend, gestartet.