Fixed some bugs + added custom domain support
This commit is contained in:
@@ -8,19 +8,21 @@ public interface IProjectApi {
|
|||||||
public Project[] GetProjects(string userId);
|
public Project[] GetProjects(string userId);
|
||||||
public Project GetProject(string projectId);
|
public Project GetProject(string projectId);
|
||||||
public Task<string> AddProject(string name, string ownerId);
|
public Task<string> AddProject(string name, string ownerId);
|
||||||
public bool EditProject(string projectId, string name);
|
public Task<bool> EditProject(string projectId, string name, string domain);
|
||||||
public Task DeleteProject(string projectId);
|
public Task DeleteProject(string projectId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public class ProjectApi : IProjectApi {
|
public class ProjectApi : IProjectApi {
|
||||||
private readonly DatabaseContext _context;
|
private readonly DatabaseContext _context;
|
||||||
private readonly GeneralOptions _options;
|
private readonly GeneralOptions _options;
|
||||||
|
private readonly ProxyOptions _proxyOptions;
|
||||||
private readonly IDockerApi _docker;
|
private readonly IDockerApi _docker;
|
||||||
private readonly IProxyApi _proxy;
|
private readonly IProxyApi _proxy;
|
||||||
|
|
||||||
public ProjectApi(DatabaseContext context, IOptions<GeneralOptions> options, IDockerApi docker, IProxyApi proxy) {
|
public ProjectApi(DatabaseContext context, IOptions<GeneralOptions> options, IOptions<ProxyOptions> proxyOptions, IDockerApi docker, IProxyApi proxy) {
|
||||||
_context = context;
|
_context = context;
|
||||||
_options = options.Value;
|
_options = options.Value;
|
||||||
|
_proxyOptions = proxyOptions.Value;
|
||||||
_docker = docker;
|
_docker = docker;
|
||||||
_proxy = proxy;
|
_proxy = proxy;
|
||||||
}
|
}
|
||||||
@@ -49,12 +51,13 @@ public class ProjectApi : IProjectApi {
|
|||||||
Name = name,
|
Name = name,
|
||||||
Port = port
|
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}");
|
var container = await _docker.CreateContainer($"ghcr.io/muchobien/pocketbase:{_options.PocketBaseVersion}", port, _options.Root + project.ProjectId, $"{project.Name}_{project.ProjectId}");
|
||||||
await _docker.StartContainer(container);
|
await _docker.StartContainer(container);
|
||||||
project.ContainerName = 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.ProxyId = proxyId;
|
||||||
project.CertificateId = certificateId;
|
project.CertificateId = certificateId;
|
||||||
|
|
||||||
@@ -64,13 +67,22 @@ public class ProjectApi : IProjectApi {
|
|||||||
return project.ProjectId;
|
return project.ProjectId;
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool EditProject(string projectId, string name) {
|
public async Task<bool> EditProject(string projectId, string name, string domain) {
|
||||||
if (name.Length > 255) return false;
|
if (name.Length > 255) return false;
|
||||||
|
if (domain.Length > 255) return false;
|
||||||
var project = GetProject(projectId);
|
var project = GetProject(projectId);
|
||||||
if (project == null) return false;
|
if (project == null) return false;
|
||||||
project.Name = name;
|
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.Projects.Update(project);
|
||||||
_context.SaveChanges();
|
await _context.SaveChangesAsync();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
using System.Dynamic;
|
using System.Dynamic;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
|
using ProjectManager.Backend.Entities;
|
||||||
using ProjectManager.Backend.Options;
|
using ProjectManager.Backend.Options;
|
||||||
|
|
||||||
namespace ProjectManager.Backend.Apis;
|
namespace ProjectManager.Backend.Apis;
|
||||||
|
|
||||||
public interface IProxyApi {
|
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 RemoveLocation(int proxyId, int certificateId);
|
||||||
|
public Task<(int, int)> UpdateLocation(Project project);
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed class ProxyApi : IProxyApi {
|
public sealed class ProxyApi : IProxyApi {
|
||||||
@@ -27,11 +29,11 @@ public sealed class ProxyApi : IProxyApi {
|
|||||||
_client.DefaultRequestHeaders.Add("Authorization", $"Bearer {response?.token}");
|
_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);
|
if (!_options.Enable) return (-1, -1);
|
||||||
await Login();
|
await Login();
|
||||||
var result = await _client.PostAsJsonAsync(_options.Url + "/api/nginx/proxy-hosts",
|
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<ExpandoObject>();
|
dynamic data = await result.Content.ReadFromJsonAsync<ExpandoObject>();
|
||||||
if (data == null) return (-1, -1);
|
if (data == null) return (-1, -1);
|
||||||
int id = Convert.ToInt32($"{data.id}");
|
int id = Convert.ToInt32($"{data.id}");
|
||||||
@@ -46,9 +48,15 @@ public sealed class ProxyApi : IProxyApi {
|
|||||||
await _client.DeleteAsync(_options.Url + "/api/nginx/certificates/" + certificateId);
|
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 ProxyAuth(string identity, string secret);
|
||||||
private sealed record TokenResponse(string token, string expires);
|
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 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 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;
|
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 bool ssl_forced { get; set; } = true;
|
||||||
public SslMeta meta { get; set; }
|
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 };
|
domain_names = new[] { domain };
|
||||||
forward_host = ip;
|
forward_host = ip;
|
||||||
forward_port = port;
|
forward_port = port;
|
||||||
|
|||||||
@@ -70,11 +70,11 @@ public class ProjectController : ControllerBase {
|
|||||||
|
|
||||||
[Authorized]
|
[Authorized]
|
||||||
[HttpPut("{projectId}")]
|
[HttpPut("{projectId}")]
|
||||||
public IActionResult EditProject(string projectId, [FromBody] ProjectEdit edit) {
|
public async Task<IActionResult> EditProject(string projectId, [FromBody] ProjectEdit edit) {
|
||||||
var project = _projects.GetProject(projectId);
|
var project = _projects.GetProject(projectId);
|
||||||
if (project == null) return NotFound();
|
if (project == null) return NotFound();
|
||||||
if (project.OwnerId != _context.UserId) return Unauthorized();
|
if (project.OwnerId != _context.UserId) return Unauthorized();
|
||||||
_projects.EditProject(projectId, edit.Name);
|
await _projects.EditProject(projectId, edit.Name, edit.Domain);
|
||||||
return Ok();
|
return Ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -84,7 +84,7 @@ public class ProjectController : ControllerBase {
|
|||||||
var project = _projects.GetProject(projectId);
|
var project = _projects.GetProject(projectId);
|
||||||
if (project == null) return NotFound();
|
if (project == null) return NotFound();
|
||||||
if (project.OwnerId != _context.UserId) return Unauthorized();
|
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}/_/");
|
return Redirect($"http://{_options.Host}:{project.Port}/_/");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,7 +94,7 @@ public class ProjectController : ControllerBase {
|
|||||||
var project = _projects.GetProject(projectId);
|
var project = _projects.GetProject(projectId);
|
||||||
if (project == null) return NotFound();
|
if (project == null) return NotFound();
|
||||||
if (project.OwnerId != _context.UserId) return Unauthorized();
|
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}/_/"});
|
return Ok(new {url = $"http://{_options.Host}:{project.Port}/_/"});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ public class Project {
|
|||||||
public string ProjectId { get; set; }
|
public string ProjectId { get; set; }
|
||||||
public string OwnerId { get; set; }
|
public string OwnerId { get; set; }
|
||||||
public string Name { get; set; }
|
public string Name { get; set; }
|
||||||
|
public string Domain { get; set; }
|
||||||
public int Port { get; set; }
|
public int Port { get; set; }
|
||||||
public string ContainerName { get; set; }
|
public string ContainerName { get; set; }
|
||||||
public int ProxyId { get; set; } = -1;
|
public int ProxyId { get; set; } = -1;
|
||||||
@@ -12,4 +13,5 @@ public class Project {
|
|||||||
|
|
||||||
public class ProjectEdit {
|
public class ProjectEdit {
|
||||||
public string Name { get; set; }
|
public string Name { get; set; }
|
||||||
|
public string Domain { get; set; }
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
<h1 mat-dialog-title *ngIf="data.title != undefined">{{data.title}}</h1>
|
<h1 mat-dialog-title *ngIf="data.title != undefined">{{data.title}}</h1>
|
||||||
<div mat-dialog-content *ngIf="data.subtitle != undefined">{{data.subtitle}}</div>
|
<div mat-dialog-content *ngIf="data.subtitle != undefined">{{data.subtitle}}</div>
|
||||||
<div mat-dialog-actions *ngIf="data.buttons != undefined" id="buttons">
|
<div mat-dialog-actions *ngIf="data.buttons != undefined" class="buttons">
|
||||||
<button mat-button
|
<button mat-button
|
||||||
(click)="dialogRef.close(button.value)"
|
(click)="dialogRef.close(button.value)"
|
||||||
*ngFor="let button of data.buttons"
|
*ngFor="let button of data.buttons"
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
#buttons {
|
.buttons {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-evenly;
|
justify-content: space-evenly;
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ export interface DialogData {
|
|||||||
title?: string;
|
title?: string;
|
||||||
subtitle?: string;
|
subtitle?: string;
|
||||||
buttons?: {text: string, value: any, color: ThemePalette}[];
|
buttons?: {text: string, value: any, color: ThemePalette}[];
|
||||||
|
secondInput?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
|
|||||||
@@ -4,10 +4,14 @@
|
|||||||
<mat-label>{{data.subtitle}}</mat-label>
|
<mat-label>{{data.subtitle}}</mat-label>
|
||||||
<input type="text" matInput #text>
|
<input type="text" matInput #text>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
|
<mat-form-field [ngStyle]="{'display': data.secondInput ? 'block' : 'none'}">
|
||||||
|
<mat-label>{{data.secondInput}}</mat-label>
|
||||||
|
<input type="text" matInput #text2>
|
||||||
|
</mat-form-field>
|
||||||
</form>
|
</form>
|
||||||
<div mat-dialog-actions *ngIf="data.buttons != undefined" id="buttons">
|
<div mat-dialog-actions *ngIf="data.buttons != undefined" class="buttons">
|
||||||
<button mat-button
|
<button mat-button
|
||||||
(click)="dialogRef.close({success: button.value, data: text.value})"
|
(click)="dialogRef.close({success: button.value, data: text.value, data2: text2?.value})"
|
||||||
*ngFor="let button of data.buttons"
|
*ngFor="let button of data.buttons"
|
||||||
[color]="button.color"
|
[color]="button.color"
|
||||||
>{{button.text}}</button>
|
>{{button.text}}</button>
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
form {
|
||||||
|
width: 350px;
|
||||||
|
|
||||||
|
& > * {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttons {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-evenly;
|
||||||
|
}
|
||||||
|
|||||||
@@ -12,4 +12,5 @@ export class TextDialogComponent {
|
|||||||
public dialogRef: MatDialogRef<TextDialogComponent>,
|
public dialogRef: MatDialogRef<TextDialogComponent>,
|
||||||
@Inject(MAT_DIALOG_DATA) public data: DialogData,
|
@Inject(MAT_DIALOG_DATA) public data: DialogData,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,12 +16,16 @@
|
|||||||
open: string,
|
open: string,
|
||||||
edit: string,
|
edit: string,
|
||||||
delete: string,
|
delete: string,
|
||||||
|
deleteProjQuestion: string,
|
||||||
|
|
||||||
// Popups
|
// Popups
|
||||||
name: string,
|
name: string,
|
||||||
|
domain: string,
|
||||||
cancel: string,
|
cancel: string,
|
||||||
createProject: string,
|
createProject: string,
|
||||||
editProject: string
|
editProject: string,
|
||||||
|
updateProject: string,
|
||||||
|
deleteProject: string,
|
||||||
|
|
||||||
// Profile
|
// Profile
|
||||||
profile: string,
|
profile: string,
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
projectId?: string;
|
projectId?: string;
|
||||||
ownerId?: string;
|
ownerId?: string;
|
||||||
name?: string;
|
name?: string;
|
||||||
|
domain?: string;
|
||||||
port?: number;
|
port?: number;
|
||||||
containerName?: string;
|
containerName?: string;
|
||||||
proxyId?: number;
|
proxyId?: number;
|
||||||
|
|||||||
@@ -21,19 +21,19 @@ export class LangService {
|
|||||||
const res = await firstValueFrom(this.crud.client.get<{files: string[]}>(location?.origin + "/lang"));
|
const res = await firstValueFrom(this.crud.client.get<{files: string[]}>(location?.origin + "/lang"));
|
||||||
const languages = res.files;
|
const languages = res.files;
|
||||||
this.allLanguages = languages.map(lang => lang.replace(".json", ""));
|
this.allLanguages = languages.map(lang => lang.replace(".json", ""));
|
||||||
const tasks = [];
|
this.currentLang = await this.loadLanguage(this.storage.getItem("language") || "en-US");
|
||||||
|
|
||||||
for (let lang of languages) {
|
|
||||||
const task = firstValueFrom(this.crud.client.get<Language>(location?.origin + "/assets/languages/" + lang))
|
|
||||||
.then(result => this.languages.set(lang.replace(".json", ""), result));
|
|
||||||
tasks.push(task);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await Promise.all(tasks);
|
private async loadLanguage(name: string): Promise<Language> {
|
||||||
this.currentLang = this.languages.get(this.storage.getItem("language") || "en-US");
|
const lang = await firstValueFrom(this.crud.client.get<Language>(`${location.origin}/assets/languages/${name}.json`));
|
||||||
|
this.languages.set(name, lang);
|
||||||
|
return lang;
|
||||||
}
|
}
|
||||||
|
|
||||||
public setLanguage(lang: string) {
|
public async setLanguage(lang: string) {
|
||||||
|
if (this.languages.get(lang) == undefined)
|
||||||
|
await this.loadLanguage(lang);
|
||||||
|
|
||||||
this.currentLang = this.languages.get(lang);
|
this.currentLang = this.languages.get(lang);
|
||||||
this.storage.setItem("language", lang);
|
this.storage.setItem("language", lang);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,8 +31,8 @@ export class ProjectService {
|
|||||||
return response.content?.projectId;
|
return response.content?.projectId;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async editProject(projectId: string, name: string): Promise<boolean> {
|
public async editProject(projectId: string, name: string, domain: string): Promise<boolean> {
|
||||||
const response = await this.crud.sendPutRequest("projects/" + projectId, {name});
|
const response = await this.crud.sendPutRequest("projects/" + projectId, {name, domain});
|
||||||
return response.success;
|
return response.success;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -33,24 +33,26 @@ export class DashboardComponent {
|
|||||||
|
|
||||||
public async editProject(projectId: string) {
|
public async editProject(projectId: string) {
|
||||||
const dialogRef = this.dialog.open(TextDialogComponent, {
|
const dialogRef = this.dialog.open(TextDialogComponent, {
|
||||||
data: {title: "Projekt umbenennen", subtitle: "Name", buttons: [
|
data: {title: this.langs.currentLang?.editProject, subtitle: "Name", secondInput: this.langs.currentLang?.domain, buttons: [
|
||||||
{text: "Abbrechen", value: false},
|
{text: this.langs.currentLang?.cancel, value: false},
|
||||||
{text: "Projekt bearbeiten", value: true, color: 'primary'}
|
{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;
|
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();
|
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) {
|
public async deleteProject(projectId: string) {
|
||||||
const dialogRef = this.dialog.open(DialogComponent, {
|
const dialogRef = this.dialog.open(DialogComponent, {
|
||||||
data: {title: "Möchtest du das Projekt wirklich löschen?", subtitle: "Alle gespeicherten Daten gehen dann verloren!", buttons: [
|
data: {title: this.langs.currentLang?.deleteProjQuestion, subtitle: this.langs.currentLang?.deleteWarning, buttons: [
|
||||||
{text: "Abbrechen", value: false},
|
{text: this.langs.currentLang?.cancel, value: false},
|
||||||
{text: "Löschen", value: true, color: 'warn'}
|
{text: this.langs.currentLang?.delete, value: true, color: 'warn'}
|
||||||
]}
|
]}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -60,7 +62,7 @@ export class DashboardComponent {
|
|||||||
await this.projects.deleteProject(projectId);
|
await this.projects.deleteProject(projectId);
|
||||||
NavigationComponent.spinnerVisible = false;
|
NavigationComponent.spinnerVisible = false;
|
||||||
await this.projects.loadProjects();
|
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) {
|
public async updateProjectStatus(projectId: string, start: boolean) {
|
||||||
|
|||||||
@@ -14,11 +14,15 @@
|
|||||||
"open": "Öffnen",
|
"open": "Öffnen",
|
||||||
"edit": "Bearbeiten",
|
"edit": "Bearbeiten",
|
||||||
"delete": "Löschen",
|
"delete": "Löschen",
|
||||||
|
"deleteProjQuestion": "Möchtest du dieses Projekt wirklich löschen?",
|
||||||
|
|
||||||
"name": "Name",
|
"name": "Name",
|
||||||
|
"domain": "Domain",
|
||||||
"cancel": "Abbrechen",
|
"cancel": "Abbrechen",
|
||||||
"createProject": "Projekt erstellen",
|
"createProject": "Projekt erstellen",
|
||||||
"editProject": "Projekt bearbeiten",
|
"editProject": "Projekt bearbeiten",
|
||||||
|
"updateProject": "Projekt aktualisiert!",
|
||||||
|
"deleteProject": "Projekt gelöscht!",
|
||||||
|
|
||||||
"profile": "Profil",
|
"profile": "Profil",
|
||||||
"profileSub": "Einstellungen",
|
"profileSub": "Einstellungen",
|
||||||
@@ -32,7 +36,7 @@
|
|||||||
"updateFailed": "Aktualisierung fehlgeschlagen",
|
"updateFailed": "Aktualisierung fehlgeschlagen",
|
||||||
"accountUpdated": "Account aktualisiert!",
|
"accountUpdated": "Account aktualisiert!",
|
||||||
"deleteQuestion": "Möchtest du deinen Account wirklich löschen?",
|
"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!",
|
"accountDeleted": "Account gelöscht!",
|
||||||
"submit": "Bestätigen",
|
"submit": "Bestätigen",
|
||||||
|
|
||||||
|
|||||||
@@ -14,11 +14,15 @@
|
|||||||
"open": "Open",
|
"open": "Open",
|
||||||
"edit": "Edit",
|
"edit": "Edit",
|
||||||
"delete": "Delete",
|
"delete": "Delete",
|
||||||
|
"deleteProjQuestion": "Do you really want to delete this project?",
|
||||||
|
|
||||||
"name": "Name",
|
"name": "Name",
|
||||||
|
"domain": "Domain",
|
||||||
"cancel": "Cancel",
|
"cancel": "Cancel",
|
||||||
"createProject": "Create project",
|
"createProject": "Create project",
|
||||||
"editProject": "Edit project",
|
"editProject": "Edit project",
|
||||||
|
"updateProject": "Updated project!",
|
||||||
|
"deleteProject": "Deleted project!",
|
||||||
|
|
||||||
"profile": "Profile",
|
"profile": "Profile",
|
||||||
"profileSub": "Settings",
|
"profileSub": "Settings",
|
||||||
@@ -32,7 +36,7 @@
|
|||||||
"updateFailed": "Update failed",
|
"updateFailed": "Update failed",
|
||||||
"accountUpdated": "Account updated",
|
"accountUpdated": "Account updated",
|
||||||
"deleteQuestion": "Do you really want to delete your account?",
|
"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!",
|
"accountDeleted": "Account deleted!",
|
||||||
"submit": "Submit",
|
"submit": "Submit",
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ CREATE TABLE `Projects` (
|
|||||||
`ownerId` varchar(36) DEFAULT NULL,
|
`ownerId` varchar(36) DEFAULT NULL,
|
||||||
`name` varchar(255) DEFAULT NULL,
|
`name` varchar(255) DEFAULT NULL,
|
||||||
`port` int(5) DEFAULT NULL,
|
`port` int(5) DEFAULT NULL,
|
||||||
|
`domain` varchar(255) DEFAULT NULL,
|
||||||
`containerName` varchar(255) DEFAULT NULL,
|
`containerName` varchar(255) DEFAULT NULL,
|
||||||
`proxyId` int(30) DEFAULT NULL,
|
`proxyId` int(30) DEFAULT NULL,
|
||||||
`certificateId` int(30) DEFAULT NULL
|
`certificateId` int(30) DEFAULT NULL
|
||||||
|
|||||||
@@ -6,9 +6,9 @@ Es handelt sich hierbei um ein einfach zu benutzendes WebInterface zum Erstellen
|
|||||||
- [x] Automatische Docker Konfiguration
|
- [x] Automatische Docker Konfiguration
|
||||||
- [x] Automatisches DNS Mapping
|
- [x] Automatisches DNS Mapping
|
||||||
- [x] Automatische SSL-Konfiguration mithilfe von NginxProxyManager
|
- [x] Automatische SSL-Konfiguration mithilfe von NginxProxyManager
|
||||||
|
- [x] Mehrere Sprachen
|
||||||
|
- [x] Eigene Domains
|
||||||
- [ ] Projekte exportieren / importieren
|
- [ ] Projekte exportieren / importieren
|
||||||
- [ ] Eigene Domains
|
|
||||||
- [ ] Mehrere Sprachen
|
|
||||||
|
|
||||||
## Installation
|
## 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.
|
Die Installation erfolgt durch eine Docker-Compose Datei. Hierbei werden zwei Container, einer für das Backend und einer für das Frontend, gestartet.
|
||||||
|
|||||||
Reference in New Issue
Block a user