v1.0
This commit is contained in:
25
ProjectManager.Backend/.dockerignore
Normal file
25
ProjectManager.Backend/.dockerignore
Normal file
@@ -0,0 +1,25 @@
|
||||
**/.dockerignore
|
||||
**/.env
|
||||
**/.git
|
||||
**/.gitignore
|
||||
**/.project
|
||||
**/.settings
|
||||
**/.toolstarget
|
||||
**/.vs
|
||||
**/.vscode
|
||||
**/.idea
|
||||
**/*.*proj.user
|
||||
**/*.dbmdl
|
||||
**/*.jfm
|
||||
**/azds.yaml
|
||||
**/bin
|
||||
**/charts
|
||||
**/docker-compose*
|
||||
**/Dockerfile*
|
||||
**/node_modules
|
||||
**/npm-debug.log
|
||||
**/obj
|
||||
**/secrets.dev.yaml
|
||||
**/values.dev.yaml
|
||||
LICENSE
|
||||
README.md
|
||||
3
ProjectManager.Backend/.gitignore
vendored
Normal file
3
ProjectManager.Backend/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
obj
|
||||
bin
|
||||
appsettings.Development.json
|
||||
63
ProjectManager.Backend/Apis/IDockerApi.cs
Normal file
63
ProjectManager.Backend/Apis/IDockerApi.cs
Normal file
@@ -0,0 +1,63 @@
|
||||
using Docker.DotNet;
|
||||
using Docker.DotNet.Models;
|
||||
using Microsoft.Extensions.Options;
|
||||
using ProjectManager.Backend.Options;
|
||||
|
||||
namespace ProjectManager.Backend.Apis;
|
||||
|
||||
public interface IDockerApi {
|
||||
public Task<string> CreateContainer(string image, int port, string hostVolumePath, string name);
|
||||
public Task StartContainer(string containerId);
|
||||
public Task StopContainer(string containerId);
|
||||
public Task DeleteContainer(string containerId);
|
||||
public Task<bool> IsContainerStarted(string containerId);
|
||||
}
|
||||
|
||||
public sealed class DockerApi : IDockerApi {
|
||||
private readonly DockerClient _client;
|
||||
|
||||
public DockerApi(IOptions<GeneralOptions> options) {
|
||||
_client = new DockerClientConfiguration(new Uri(options.Value.DockerPath)).CreateClient();
|
||||
}
|
||||
|
||||
public async Task<string> CreateContainer(string image, int port, string hostVolumePath, string name) {
|
||||
await _client.Images.CreateImageAsync(new() {
|
||||
FromImage = image
|
||||
}, null, new Progress<JSONMessage>());
|
||||
|
||||
var container = await _client.Containers.CreateContainerAsync(new CreateContainerParameters {
|
||||
Image = image,
|
||||
ExposedPorts = new Dictionary<string, EmptyStruct> {
|
||||
{ "8090/tcp", default }
|
||||
},
|
||||
HostConfig = new HostConfig {
|
||||
PortBindings = new Dictionary<string, IList<PortBinding>> {
|
||||
{ "8090/tcp", new List<PortBinding> { new() { HostPort = port.ToString() } } }
|
||||
},
|
||||
Binds = new List<string> {
|
||||
$"{hostVolumePath}:/pb_data"
|
||||
}
|
||||
},
|
||||
Name = name
|
||||
});
|
||||
return container.ID;
|
||||
}
|
||||
|
||||
public async Task StartContainer(string containerId) {
|
||||
await _client.Containers.StartContainerAsync(containerId, new());
|
||||
}
|
||||
|
||||
public async Task StopContainer(string containerId) {
|
||||
await _client.Containers.StopContainerAsync(containerId, new());
|
||||
}
|
||||
|
||||
public async Task DeleteContainer(string containerId) {
|
||||
await _client.Containers.RemoveContainerAsync(containerId, new());
|
||||
}
|
||||
|
||||
public async Task<bool> IsContainerStarted(string containerId) {
|
||||
var containers = await _client.Containers.ListContainersAsync(new());
|
||||
var container = containers.SingleOrDefault(c => c.ID == containerId);
|
||||
return container != null;
|
||||
}
|
||||
}
|
||||
89
ProjectManager.Backend/Apis/IProjectApi.cs
Normal file
89
ProjectManager.Backend/Apis/IProjectApi.cs
Normal file
@@ -0,0 +1,89 @@
|
||||
using Microsoft.Extensions.Options;
|
||||
using ProjectManager.Backend.Entities;
|
||||
using ProjectManager.Backend.Options;
|
||||
|
||||
namespace ProjectManager.Backend.Apis;
|
||||
|
||||
public interface IProjectApi {
|
||||
public Project[] GetProjects(string userId);
|
||||
public Project GetProject(string projectId);
|
||||
public Task<string> AddProject(string name, string ownerId);
|
||||
public bool EditProject(string projectId, string name);
|
||||
public Task DeleteProject(string projectId);
|
||||
}
|
||||
|
||||
public class ProjectApi : IProjectApi {
|
||||
private readonly DatabaseContext _context;
|
||||
private readonly GeneralOptions _options;
|
||||
private readonly IDockerApi _docker;
|
||||
private readonly IProxyApi _proxy;
|
||||
|
||||
public ProjectApi(DatabaseContext context, IOptions<GeneralOptions> options, IDockerApi docker, IProxyApi proxy) {
|
||||
_context = context;
|
||||
_options = options.Value;
|
||||
_docker = docker;
|
||||
_proxy = proxy;
|
||||
}
|
||||
|
||||
public Project[] GetProjects(string userId) {
|
||||
return _context.Projects.Where(project => project.OwnerId == userId).ToArray();
|
||||
}
|
||||
|
||||
public Project GetProject(string projectId) {
|
||||
return _context.Projects.SingleOrDefault(project => project.ProjectId == projectId);
|
||||
}
|
||||
|
||||
public async Task<string> AddProject(string name, string ownerId) {
|
||||
if (name.Length > 255) return null;
|
||||
|
||||
// Get available port
|
||||
var split = _options.PortRange.Split('-');
|
||||
var portRange = Enumerable.Range(int.Parse(split[0]), int.Parse(split[1]));
|
||||
var usedPorts = _context.Projects.Select(p => p.Port).ToArray();
|
||||
var port = portRange.FirstOrDefault(port => !usedPorts.Contains(port));
|
||||
if (port == 0) return null;
|
||||
|
||||
var project = new Project {
|
||||
ProjectId = Guid.NewGuid().ToString(),
|
||||
OwnerId = ownerId,
|
||||
Name = name,
|
||||
Port = 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);
|
||||
project.ProxyId = proxyId;
|
||||
project.CertificateId = certificateId;
|
||||
|
||||
_context.Projects.Add(project);
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
return project.ProjectId;
|
||||
}
|
||||
|
||||
public bool EditProject(string projectId, string name) {
|
||||
if (name.Length > 255) return false;
|
||||
var project = GetProject(projectId);
|
||||
if (project == null) return false;
|
||||
project.Name = name;
|
||||
_context.Projects.Update(project);
|
||||
_context.SaveChanges();
|
||||
return true;
|
||||
}
|
||||
|
||||
public async Task DeleteProject(string projectId) {
|
||||
if (!_context.Projects.Any(project => project.ProjectId == projectId)) return;
|
||||
|
||||
var project = _context.Projects.Single(project => project.ProjectId == projectId);
|
||||
await _docker.StopContainer(project.ContainerName);
|
||||
await _docker.DeleteContainer(project.ContainerName);
|
||||
|
||||
await _proxy.RemoveLocation(project.ProxyId, project.CertificateId);
|
||||
|
||||
_context.Projects.Remove(project);
|
||||
await _context.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
84
ProjectManager.Backend/Apis/IProxyApi.cs
Normal file
84
ProjectManager.Backend/Apis/IProxyApi.cs
Normal file
@@ -0,0 +1,84 @@
|
||||
using System.Dynamic;
|
||||
using System.Text.Json;
|
||||
using Microsoft.Extensions.Options;
|
||||
using ProjectManager.Backend.Options;
|
||||
|
||||
namespace ProjectManager.Backend.Apis;
|
||||
|
||||
public interface IProxyApi {
|
||||
public Task<(int, int)> AddLocation(string projectId, int port);
|
||||
public Task RemoveLocation(int proxyId, int certificateId);
|
||||
}
|
||||
|
||||
public sealed class ProxyApi : IProxyApi {
|
||||
private readonly HttpClient _client;
|
||||
private readonly ProxyOptions _options;
|
||||
|
||||
public ProxyApi(IOptions<ProxyOptions> options) {
|
||||
_client = new HttpClient();
|
||||
_options = options.Value;
|
||||
}
|
||||
|
||||
private async Task Login() {
|
||||
var result = await _client.PostAsJsonAsync(_options.Url + "/api/tokens",
|
||||
new ProxyAuth(_options.Email, _options.Password), JsonSerializerOptions.Default);
|
||||
var response = await result.Content.ReadFromJsonAsync<TokenResponse>();
|
||||
_client.DefaultRequestHeaders.Clear();
|
||||
_client.DefaultRequestHeaders.Add("Authorization", $"Bearer {response?.token}");
|
||||
}
|
||||
|
||||
public async Task<(int, int)> AddLocation(string projectId, 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));
|
||||
dynamic data = await result.Content.ReadFromJsonAsync<ExpandoObject>();
|
||||
if (data == null) return (-1, -1);
|
||||
int id = Convert.ToInt32($"{data.id}");
|
||||
int certificateId = Convert.ToInt32($"{data.certificate_id}");
|
||||
return (id, certificateId);
|
||||
}
|
||||
|
||||
public async Task RemoveLocation(int proxyId, int certificateId) {
|
||||
if (proxyId == -1 || certificateId == -1) return;
|
||||
await Login();
|
||||
await _client.DeleteAsync(_options.Url + "/api/nginx/proxy-hosts/" + proxyId);
|
||||
await _client.DeleteAsync(_options.Url + "/api/nginx/certificates/" + certificateId);
|
||||
}
|
||||
|
||||
private sealed record ProxyAuth(string identity, string secret);
|
||||
private sealed record TokenResponse(string token, string expires);
|
||||
private sealed class CreateData {
|
||||
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;
|
||||
public bool block_exploits { get; set; } = true;
|
||||
public bool caching_enabled { get; set; } = true;
|
||||
public string certificate_id { get; set; } = "new";
|
||||
public string[] domain_names { get; set; }
|
||||
public string forward_host { get; set; }
|
||||
public int forward_port { get; set; }
|
||||
public string forward_scheme { get; set; } = "http";
|
||||
public bool hsts_enabled { get; set; } = false;
|
||||
public bool hsts_subdomains { get; set; } = false;
|
||||
public bool http2_support { get; set; } = true;
|
||||
public string[] locations { get; set; } = Array.Empty<string>();
|
||||
public bool ssl_forced { get; set; } = true;
|
||||
public SslMeta meta { get; set; }
|
||||
|
||||
public CreateData(string domain, string ip, int port, string email) {
|
||||
domain_names = new[] { domain };
|
||||
forward_host = ip;
|
||||
forward_port = port;
|
||||
meta = new SslMeta {
|
||||
letsencrypt_email = email
|
||||
};
|
||||
advanced_config = advanced_config.Replace("%docker_ip%", $"{ip}:{port}");
|
||||
}
|
||||
}
|
||||
private sealed class SslMeta {
|
||||
public bool dns_challenge { get; set; } = false;
|
||||
public bool letsencrypt_agree { get; set; } = true;
|
||||
public string letsencrypt_email { get; set; }
|
||||
}
|
||||
}
|
||||
66
ProjectManager.Backend/Apis/ITokenApi.cs
Normal file
66
ProjectManager.Backend/Apis/ITokenApi.cs
Normal file
@@ -0,0 +1,66 @@
|
||||
using Microsoft.Extensions.Options;
|
||||
using ProjectManager.Backend.Entities;
|
||||
using ProjectManager.Backend.Options;
|
||||
|
||||
namespace ProjectManager.Backend.Apis;
|
||||
|
||||
public interface ITokenApi {
|
||||
public string GetValidToken(string userId, string clientIp);
|
||||
public bool ValidateToken(string tokenId, string clientIp);
|
||||
public User GetUserFromToken(string tokenId);
|
||||
}
|
||||
|
||||
public sealed class TokenApi : ITokenApi {
|
||||
private readonly DatabaseContext _context;
|
||||
private readonly GeneralOptions _options;
|
||||
private readonly IUserApi _users;
|
||||
|
||||
public TokenApi(DatabaseContext context, IOptions<GeneralOptions> options, IUserApi users) {
|
||||
_context = context;
|
||||
_options = options.Value;
|
||||
_users = users;
|
||||
}
|
||||
|
||||
public string GetValidToken(string userId, string clientIp) {
|
||||
var token = _context.Tokens.SingleOrDefault(token => token.UserId == userId && token.ClientIp == clientIp);
|
||||
|
||||
if (token == null) {
|
||||
token = new() {
|
||||
TokenId = Guid.NewGuid().ToString(),
|
||||
UserId = userId,
|
||||
ClientIp = clientIp,
|
||||
Created = DateTime.Now
|
||||
};
|
||||
_context.Tokens.Add(token);
|
||||
_context.SaveChanges();
|
||||
return token.TokenId;
|
||||
}
|
||||
|
||||
if (!ValidateToken(token.TokenId, clientIp)) {
|
||||
_context.Tokens.Remove(token);
|
||||
token = new() {
|
||||
TokenId = Guid.NewGuid().ToString(),
|
||||
UserId = userId,
|
||||
ClientIp = clientIp,
|
||||
Created = DateTime.Now
|
||||
};
|
||||
_context.Tokens.Add(token);
|
||||
_context.SaveChanges();
|
||||
return token.TokenId;
|
||||
}
|
||||
|
||||
return token.TokenId;
|
||||
}
|
||||
|
||||
public bool ValidateToken(string tokenId, string clientIp) {
|
||||
if (tokenId is null || clientIp is null) return false;
|
||||
var token = _context.Tokens.SingleOrDefault(token => token.TokenId == tokenId && token.ClientIp == clientIp);
|
||||
if (token is null) return false;
|
||||
if (_context.Users.SingleOrDefault(user => user.UserId == token.UserId) == null) return false;
|
||||
return DateTime.Now - token.Created < TimeSpan.FromDays(_options.TokenTimeInDays);
|
||||
}
|
||||
|
||||
public User GetUserFromToken(string tokenId) {
|
||||
return _users.GetUser(_context.Tokens.SingleOrDefault(token => token.TokenId == tokenId)?.UserId);
|
||||
}
|
||||
}
|
||||
121
ProjectManager.Backend/Apis/IUserApi.cs
Normal file
121
ProjectManager.Backend/Apis/IUserApi.cs
Normal file
@@ -0,0 +1,121 @@
|
||||
using System.Text;
|
||||
using Microsoft.AspNetCore.Cryptography.KeyDerivation;
|
||||
using Microsoft.Extensions.Options;
|
||||
using ProjectManager.Backend.Entities;
|
||||
using ProjectManager.Backend.Options;
|
||||
|
||||
namespace ProjectManager.Backend.Apis;
|
||||
|
||||
public interface IUserApi {
|
||||
public User Login(User login);
|
||||
public User Register(User register);
|
||||
public User GetUser(string userId);
|
||||
public IEnumerable<User> GetUsers();
|
||||
public bool UpdateUser(User update);
|
||||
public void DeleteUser(string userId);
|
||||
public bool CanCreateProject(string userId);
|
||||
}
|
||||
|
||||
public sealed class UserApi : IUserApi {
|
||||
private readonly DatabaseContext _context;
|
||||
private readonly IProjectApi _projects;
|
||||
private readonly GeneralOptions _options;
|
||||
|
||||
public UserApi(DatabaseContext context, IProjectApi projects, IOptions<GeneralOptions> options) {
|
||||
_context = context;
|
||||
_projects = projects;
|
||||
_options = options.Value;
|
||||
}
|
||||
|
||||
public User Login(User login) {
|
||||
if (string.IsNullOrEmpty(login.Email) || string.IsNullOrEmpty(login.Password)) return null;
|
||||
var hash = Hash128(login.Password, login.Email);
|
||||
return _context.Users.SingleOrDefault(user => user.Email == login.Email && user.Password == hash);
|
||||
}
|
||||
|
||||
public User Register(User register) {
|
||||
if (string.IsNullOrEmpty(register.Username) ||
|
||||
string.IsNullOrEmpty(register.Email) ||
|
||||
string.IsNullOrEmpty(register.Password)) return null;
|
||||
|
||||
if (_context.Users.Any(user => user.Email == register.Email || user.Username == register.Username))
|
||||
return null;
|
||||
|
||||
if (register.Email.Length > 255 || register.Username.Length > 255 || register.Password.Length > 255)
|
||||
return null;
|
||||
|
||||
if (!register.Email.Contains('@') || !register.Email.Contains('.')) return null;
|
||||
if (!register.Password.Any(char.IsLetter) || !register.Password.Any(char.IsDigit) || register.Password.Length < 8) return null;
|
||||
|
||||
User user = new() {
|
||||
UserId = Guid.NewGuid().ToString(),
|
||||
Email = register.Email,
|
||||
Username = register.Username,
|
||||
Password = Hash128(register.Password, register.Email),
|
||||
MaxProjects = _options.MaxProjects
|
||||
};
|
||||
|
||||
_context.Users.Add(user);
|
||||
_context.SaveChanges();
|
||||
return user;
|
||||
}
|
||||
|
||||
public User GetUser(string userId) {
|
||||
return _context.Users.SingleOrDefault(user => user.UserId == userId);
|
||||
}
|
||||
|
||||
public IEnumerable<User> GetUsers() {
|
||||
return _context.Users;
|
||||
}
|
||||
|
||||
public bool UpdateUser(User update) {
|
||||
if (_context.Users.Any(user => user.UserId != update.UserId && (user.Email == update.Email || user.Username == update.Username)))
|
||||
return false;
|
||||
|
||||
if (update.Email.Length > 255 || update.Username.Length > 255 || update.Password.Length > 255)
|
||||
return false;
|
||||
|
||||
if (!string.IsNullOrEmpty(update.Email) && (!update.Email.Contains('@') || !update.Email.Contains('.'))) return false;
|
||||
if (!string.IsNullOrEmpty(update.Password) && (!update.Password.Any(char.IsLetter) || !update.Password.Any(char.IsDigit) || update.Password.Length < 8)) return false;
|
||||
|
||||
var user = _context.Users.SingleOrDefault(user => user.UserId == update.UserId);
|
||||
if (user == null) return false;
|
||||
if (!string.IsNullOrEmpty(update.Email)) user.Email = update.Email;
|
||||
if (!string.IsNullOrEmpty(update.Username)) user.Username = update.Username;
|
||||
if (!string.IsNullOrEmpty(update.Password)) user.Password = Hash128(update.Password, user.Email);
|
||||
_context.Users.Update(user);
|
||||
_context.SaveChanges();
|
||||
return true;
|
||||
}
|
||||
|
||||
public void DeleteUser(string userId) {
|
||||
if (!_context.Users.Any(user => user.UserId == userId)) return;
|
||||
var user = _context.Users.Single(user => user.UserId == userId);
|
||||
|
||||
var projects = _projects.GetProjects(userId);
|
||||
foreach (var project in projects) {
|
||||
_projects.DeleteProject(project.ProjectId);
|
||||
}
|
||||
|
||||
_context.Users.Remove(user);
|
||||
_context.SaveChanges();
|
||||
}
|
||||
|
||||
public bool CanCreateProject(string userId) {
|
||||
return GetUser(userId).MaxProjects > _projects.GetProjects(userId).Length;
|
||||
}
|
||||
|
||||
private static string Hash128(string plainText, string salt) {
|
||||
try {
|
||||
string hashed = Convert.ToBase64String(KeyDerivation.Pbkdf2(
|
||||
password: plainText,
|
||||
salt: Encoding.Default.GetBytes(salt),
|
||||
prf: KeyDerivationPrf.HMACSHA256,
|
||||
iterationCount: 100000,
|
||||
numBytesRequested: 256 / 8
|
||||
));
|
||||
|
||||
return hashed;
|
||||
} catch (Exception) { return ""; }
|
||||
}
|
||||
}
|
||||
119
ProjectManager.Backend/Controllers/ProjectController.cs
Normal file
119
ProjectManager.Backend/Controllers/ProjectController.cs
Normal file
@@ -0,0 +1,119 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Options;
|
||||
using ProjectManager.Backend.Apis;
|
||||
using ProjectManager.Backend.Entities;
|
||||
using ProjectManager.Backend.Options;
|
||||
using ProjectManager.Backend.Security;
|
||||
|
||||
namespace ProjectManager.Backend.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("projects")]
|
||||
public class ProjectController : ControllerBase {
|
||||
private readonly IProjectApi _projects;
|
||||
private readonly ITokenContext _context;
|
||||
private readonly IUserApi _users;
|
||||
private readonly IDockerApi _docker;
|
||||
private readonly ProxyOptions _options;
|
||||
|
||||
public ProjectController(
|
||||
IProjectApi projects,
|
||||
ITokenContext context,
|
||||
IUserApi users,
|
||||
IDockerApi docker,
|
||||
IOptions<ProxyOptions> options
|
||||
) {
|
||||
_projects = projects;
|
||||
_context = context;
|
||||
_users = users;
|
||||
_docker = docker;
|
||||
_options = options.Value;
|
||||
}
|
||||
|
||||
[Authorized]
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> GetProjects() {
|
||||
var projects = _projects.GetProjects(_context.UserId);
|
||||
var running = new bool[projects.Length];
|
||||
for (int i = 0; i < projects.Length; i++)
|
||||
running[i] = await _docker.IsContainerStarted(projects[i].ContainerName);
|
||||
return Ok(new { projects, running });
|
||||
}
|
||||
|
||||
[Authorized]
|
||||
[HttpGet("{projectId}")]
|
||||
public IActionResult GetProject(string projectId) {
|
||||
var project = _projects.GetProject(projectId);
|
||||
if (project == null) return NotFound();
|
||||
if (project.OwnerId != _context.UserId) return Unauthorized();
|
||||
return Ok(project);
|
||||
}
|
||||
|
||||
[Authorized]
|
||||
[HttpPost]
|
||||
public async Task<IActionResult> AddProject([FromBody] ProjectEdit edit) {
|
||||
if (!_users.CanCreateProject(_context.UserId)) return Forbid();
|
||||
var projectId = await _projects.AddProject(edit.Name, _context.UserId);
|
||||
if (projectId == null) return BadRequest();
|
||||
return Ok(new { ProjectId = projectId });
|
||||
}
|
||||
|
||||
[Authorized]
|
||||
[HttpDelete("{projectId}")]
|
||||
public async Task<IActionResult> DeleteProject(string projectId) {
|
||||
var project = _projects.GetProject(projectId);
|
||||
if (project == null) return NotFound();
|
||||
if (project.OwnerId != _context.UserId) return Unauthorized();
|
||||
await _projects.DeleteProject(projectId);
|
||||
return Ok();
|
||||
}
|
||||
|
||||
[Authorized]
|
||||
[HttpPut("{projectId}")]
|
||||
public IActionResult 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);
|
||||
return Ok();
|
||||
}
|
||||
|
||||
[Authorized]
|
||||
[HttpGet("{projectId}/url")]
|
||||
public IActionResult GetProjectUrl(string projectId) {
|
||||
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}/_/");
|
||||
return Redirect($"http://{_options.Host}:{project.Port}/_/");
|
||||
}
|
||||
|
||||
[Authorized]
|
||||
[HttpGet("{projectId}/start")]
|
||||
public async Task<IActionResult> StartProject(string projectId) {
|
||||
var project = _projects.GetProject(projectId);
|
||||
if (project == null) return NotFound();
|
||||
if (project.OwnerId != _context.UserId) return Unauthorized();
|
||||
await _docker.StartContainer(project.ContainerName);
|
||||
return Ok();
|
||||
}
|
||||
|
||||
[Authorized]
|
||||
[HttpGet("{projectId}/stop")]
|
||||
public async Task<IActionResult> StopProject(string projectId) {
|
||||
var project = _projects.GetProject(projectId);
|
||||
if (project == null) return NotFound();
|
||||
if (project.OwnerId != _context.UserId) return Unauthorized();
|
||||
await _docker.StopContainer(project.ContainerName);
|
||||
return Ok();
|
||||
}
|
||||
|
||||
[Authorized]
|
||||
[HttpGet("{projectId}/status")]
|
||||
public async Task<IActionResult> ProjectStatus(string projectId) {
|
||||
var project = _projects.GetProject(projectId);
|
||||
if (project == null) return NotFound();
|
||||
if (project.OwnerId != _context.UserId) return Unauthorized();
|
||||
return Ok(new { Running = await _docker.IsContainerStarted(project.ContainerName) });
|
||||
}
|
||||
}
|
||||
87
ProjectManager.Backend/Controllers/UserController.cs
Normal file
87
ProjectManager.Backend/Controllers/UserController.cs
Normal file
@@ -0,0 +1,87 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using ProjectManager.Backend.Entities;
|
||||
using ProjectManager.Backend.Security;
|
||||
using ProjectManager.Backend.Apis;
|
||||
|
||||
namespace ProjectManager.Backend.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("users")]
|
||||
public sealed class UserController : ControllerBase {
|
||||
private readonly IUserApi _users;
|
||||
private readonly ITokenApi _tokens;
|
||||
private readonly ITokenContext _context;
|
||||
|
||||
public UserController(IUserApi users, ITokenApi tokens, ITokenContext context) {
|
||||
_users = users;
|
||||
_tokens = tokens;
|
||||
_context = context;
|
||||
}
|
||||
|
||||
[HttpPost("login")]
|
||||
public IActionResult Login([FromBody] User login) {
|
||||
var user = _users.Login(login);
|
||||
if (user == null) return Conflict();
|
||||
return Ok(new {Token = _tokens.GetValidToken(user.UserId, HttpContext.Connection.RemoteIpAddress?.ToString())});
|
||||
}
|
||||
|
||||
[HttpPost("register")]
|
||||
public IActionResult Register([FromBody] User register) {
|
||||
var user = _users.Register(register);
|
||||
if (user is null) return Conflict();
|
||||
return Ok(new {Token = _tokens.GetValidToken(user.UserId, HttpContext.Connection.RemoteIpAddress?.ToString())});
|
||||
}
|
||||
|
||||
[Authorized]
|
||||
[HttpGet("token")]
|
||||
public IActionResult CheckToken() {
|
||||
return Ok(new {Valid = true});
|
||||
}
|
||||
|
||||
[Authorized]
|
||||
[HttpGet("me")]
|
||||
public IActionResult GetMe() {
|
||||
return GetUser(_context.UserId);
|
||||
}
|
||||
|
||||
[Authorized]
|
||||
[HttpGet]
|
||||
public IActionResult GetUsers() {
|
||||
return Ok(_users.GetUsers().Select(user => new User {
|
||||
UserId = user.UserId,
|
||||
Email = user.Email,
|
||||
Username = user.Username
|
||||
}));
|
||||
}
|
||||
|
||||
[Authorized]
|
||||
[HttpGet("{userId}")]
|
||||
public IActionResult GetUser(string userId) {
|
||||
var user = _users.GetUser(userId);
|
||||
|
||||
if (user is null) return NotFound();
|
||||
|
||||
user = new() {
|
||||
UserId = user.UserId,
|
||||
Email = user.Email,
|
||||
Username = user.Username
|
||||
};
|
||||
return Ok(user);
|
||||
}
|
||||
|
||||
[Authorized]
|
||||
[HttpPut]
|
||||
public IActionResult UpdateUser([FromBody] User user) {
|
||||
if (_context.UserId != user.UserId) return Forbid();
|
||||
if (!_users.UpdateUser(user)) return BadRequest();
|
||||
return Ok();
|
||||
}
|
||||
|
||||
[Authorized]
|
||||
[HttpDelete]
|
||||
public IActionResult DeleteUser() {
|
||||
_users.DeleteUser(_context.UserId);
|
||||
return Ok();
|
||||
}
|
||||
|
||||
}
|
||||
49
ProjectManager.Backend/DatabaseContext.cs
Normal file
49
ProjectManager.Backend/DatabaseContext.cs
Normal file
@@ -0,0 +1,49 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Options;
|
||||
using ProjectManager.Backend.Entities;
|
||||
using ProjectManager.Backend.Options;
|
||||
|
||||
namespace ProjectManager.Backend;
|
||||
|
||||
public class DatabaseContext : DbContext {
|
||||
private readonly GeneralOptions _options;
|
||||
|
||||
public DbSet<User> Users { get; set; }
|
||||
public DbSet<UserToken> Tokens { get; set; }
|
||||
public DbSet<Project> Projects { get; set; }
|
||||
|
||||
public DatabaseContext(IOptions<GeneralOptions> options) {
|
||||
_options = options.Value;
|
||||
}
|
||||
|
||||
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) {
|
||||
optionsBuilder.UseMySQL(_options.Database);
|
||||
}
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder) {
|
||||
base.OnModelCreating(modelBuilder);
|
||||
|
||||
modelBuilder.Entity<User>(entry => {
|
||||
entry.HasKey(e => e.UserId);
|
||||
entry.Property(e => e.Email);
|
||||
entry.Property(e => e.Username);
|
||||
entry.Property(e => e.Password);
|
||||
entry.Property(e => e.MaxProjects);
|
||||
});
|
||||
|
||||
modelBuilder.Entity<UserToken>(entry => {
|
||||
entry.HasKey(e => e.TokenId);
|
||||
entry.Property(e => e.UserId);
|
||||
entry.Property(e => e.ClientIp);
|
||||
entry.Property(e => e.Created);
|
||||
});
|
||||
|
||||
modelBuilder.Entity<Project>(entry => {
|
||||
entry.HasKey(e => e.ProjectId);
|
||||
entry.Property(e => e.OwnerId);
|
||||
entry.Property(e => e.Name);
|
||||
entry.Property(e => e.Port);
|
||||
entry.Property(e => e.ContainerName);
|
||||
});
|
||||
}
|
||||
}
|
||||
20
ProjectManager.Backend/Dockerfile
Normal file
20
ProjectManager.Backend/Dockerfile
Normal file
@@ -0,0 +1,20 @@
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:7.0 AS base
|
||||
WORKDIR /app
|
||||
EXPOSE 80
|
||||
EXPOSE 443
|
||||
|
||||
FROM mcr.microsoft.com/dotnet/sdk:7.0 AS build
|
||||
WORKDIR /src
|
||||
COPY ["ProjectManager.Backend.csproj", ""]
|
||||
RUN dotnet restore "ProjectManager.Backend.csproj"
|
||||
COPY . .
|
||||
WORKDIR "/src"
|
||||
RUN dotnet build "ProjectManager.Backend.csproj" -c Release -o /app/build
|
||||
|
||||
FROM build AS publish
|
||||
RUN dotnet publish "ProjectManager.Backend.csproj" -c Release -o /app/publish
|
||||
|
||||
FROM base AS final
|
||||
WORKDIR /app
|
||||
COPY --from=publish /app/publish .
|
||||
ENTRYPOINT ["dotnet", "ProjectManager.Backend.dll"]
|
||||
15
ProjectManager.Backend/Entities/Project.cs
Normal file
15
ProjectManager.Backend/Entities/Project.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
namespace ProjectManager.Backend.Entities;
|
||||
|
||||
public class Project {
|
||||
public string ProjectId { get; set; }
|
||||
public string OwnerId { get; set; }
|
||||
public string Name { get; set; }
|
||||
public int Port { get; set; }
|
||||
public string ContainerName { get; set; }
|
||||
public int ProxyId { get; set; } = -1;
|
||||
public int CertificateId { get; set; } = -1;
|
||||
}
|
||||
|
||||
public class ProjectEdit {
|
||||
public string Name { get; set; }
|
||||
}
|
||||
9
ProjectManager.Backend/Entities/User.cs
Normal file
9
ProjectManager.Backend/Entities/User.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
namespace ProjectManager.Backend.Entities;
|
||||
|
||||
public class User {
|
||||
public string UserId { get; set; }
|
||||
public string Email { get; set; }
|
||||
public string Username { get; set; }
|
||||
public string Password { get; set; }
|
||||
public int MaxProjects { get; set; }
|
||||
}
|
||||
8
ProjectManager.Backend/Entities/UserToken.cs
Normal file
8
ProjectManager.Backend/Entities/UserToken.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
namespace ProjectManager.Backend.Entities;
|
||||
|
||||
public class UserToken {
|
||||
public string TokenId { get; set; }
|
||||
public string UserId { get; set; }
|
||||
public string ClientIp { get; set; }
|
||||
public DateTime Created { get; set; }
|
||||
}
|
||||
13
ProjectManager.Backend/Options/GeneralOptions.cs
Normal file
13
ProjectManager.Backend/Options/GeneralOptions.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
namespace ProjectManager.Backend.Options;
|
||||
|
||||
public class GeneralOptions : OptionsFromConfiguration {
|
||||
public override string Position { get; set; } = "General";
|
||||
|
||||
public string Database { get; set; }
|
||||
public int TokenTimeInDays { get; set; }
|
||||
public string PortRange { get; set; }
|
||||
public string DockerPath { get; set; }
|
||||
public string Root { get; set; }
|
||||
public string PocketBaseVersion { get; set; }
|
||||
public int MaxProjects { get; set; }
|
||||
}
|
||||
21
ProjectManager.Backend/Options/OptionsFromConfiguration.cs
Normal file
21
ProjectManager.Backend/Options/OptionsFromConfiguration.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
namespace ProjectManager.Backend.Options;
|
||||
|
||||
public abstract class OptionsFromConfiguration {
|
||||
public abstract string Position { get; set; }
|
||||
}
|
||||
|
||||
public static class OptionsFromConfigurationExtensions {
|
||||
|
||||
public static T AddOptionsFromConfiguration<T>(this IServiceCollection services, IConfiguration configuration) where T : OptionsFromConfiguration {
|
||||
T optionsInstance = (T)Activator.CreateInstance(typeof(T));
|
||||
if (optionsInstance == null) return null;
|
||||
var position = optionsInstance.Position;
|
||||
|
||||
services.Configure((Action<T>)(options => {
|
||||
configuration.Bind(position, options);
|
||||
}));
|
||||
|
||||
return optionsInstance;
|
||||
}
|
||||
|
||||
}
|
||||
12
ProjectManager.Backend/Options/ProxyOptions.cs
Normal file
12
ProjectManager.Backend/Options/ProxyOptions.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
namespace ProjectManager.Backend.Options;
|
||||
|
||||
public class ProxyOptions : OptionsFromConfiguration {
|
||||
public override string Position { get; set; } = "Proxy";
|
||||
|
||||
public bool Enable { get; set; }
|
||||
public string Url { get; set; }
|
||||
public string Email { get; set; }
|
||||
public string Password { get; set; }
|
||||
public string Domain { get; set; }
|
||||
public string Host { get; set; }
|
||||
}
|
||||
49
ProjectManager.Backend/Program.cs
Normal file
49
ProjectManager.Backend/Program.cs
Normal file
@@ -0,0 +1,49 @@
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using ProjectManager.Backend;
|
||||
using ProjectManager.Backend.Options;
|
||||
using ProjectManager.Backend.Security;
|
||||
using ProjectManager.Backend.Apis;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
// Add options to the container
|
||||
builder.Configuration.AddEnvironmentVariables();
|
||||
builder.Services.AddOptionsFromConfiguration<GeneralOptions>(builder.Configuration);
|
||||
builder.Services.AddOptionsFromConfiguration<ProxyOptions>(builder.Configuration);
|
||||
|
||||
// Add services to the container.
|
||||
builder.Services.AddDbContext<DatabaseContext>();
|
||||
builder.Services.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>();
|
||||
builder.Services.AddScoped<ITokenContext, TokenContext>();
|
||||
|
||||
builder.Services.AddScoped<IUserApi, UserApi>();
|
||||
builder.Services.AddScoped<ITokenApi, TokenApi>();
|
||||
builder.Services.AddScoped<IProjectApi, ProjectApi>();
|
||||
builder.Services.AddScoped<IDockerApi, DockerApi>();
|
||||
builder.Services.AddScoped<IProxyApi, ProxyApi>();
|
||||
|
||||
builder.Services.AddCors();
|
||||
builder.Services.AddCustomAuthentication(true);
|
||||
builder.Services.AddControllers();
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
// Configure the HTTP request pipeline.
|
||||
if (app.Environment.IsDevelopment()) {
|
||||
app.UseSwagger();
|
||||
app.UseSwaggerUI();
|
||||
}
|
||||
|
||||
app.UseCors(
|
||||
options => options
|
||||
.WithOrigins(new []{app.Configuration.GetSection("Frontend").Get<string>() ?? ""})
|
||||
.AllowAnyMethod()
|
||||
.AllowAnyHeader()
|
||||
.AllowCredentials()
|
||||
);
|
||||
|
||||
app.UseAuthorization();
|
||||
|
||||
app.MapControllers();
|
||||
|
||||
app.Run();
|
||||
16
ProjectManager.Backend/ProjectManager.Backend.csproj
Normal file
16
ProjectManager.Backend/ProjectManager.Backend.csproj
Normal file
@@ -0,0 +1,16 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net7.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Docker.DotNet" Version="3.125.12" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="7.0.0" />
|
||||
<PackageReference Include="MySql.EntityFrameworkCore" Version="6.0.7" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
41
ProjectManager.Backend/Properties/launchSettings.json
Normal file
41
ProjectManager.Backend/Properties/launchSettings.json
Normal file
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/launchsettings.json",
|
||||
"iisSettings": {
|
||||
"windowsAuthentication": false,
|
||||
"anonymousAuthentication": true,
|
||||
"iisExpress": {
|
||||
"applicationUrl": "http://localhost:47680",
|
||||
"sslPort": 44387
|
||||
}
|
||||
},
|
||||
"profiles": {
|
||||
"http": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": false,
|
||||
"launchUrl": "swagger",
|
||||
"applicationUrl": "http://localhost:5110",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
},
|
||||
"https": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": false,
|
||||
"launchUrl": "swagger",
|
||||
"applicationUrl": "https://localhost:7083;http://localhost:5110",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
},
|
||||
"IIS Express": {
|
||||
"commandName": "IISExpress",
|
||||
"launchBrowser": false,
|
||||
"launchUrl": "swagger",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
86
ProjectManager.Backend/Security/AuthenticationHandler.cs
Normal file
86
ProjectManager.Backend/Security/AuthenticationHandler.cs
Normal file
@@ -0,0 +1,86 @@
|
||||
#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously
|
||||
|
||||
using System.Security.Claims;
|
||||
using System.Text.Encodings.Web;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.OpenApi.Models;
|
||||
using ProjectManager.Backend.Apis;
|
||||
|
||||
namespace ProjectManager.Backend.Security;
|
||||
|
||||
public static class AuthenticationExtensions {
|
||||
public static AuthenticationBuilder AddCustomAuthentication(this IServiceCollection services, bool configureSwagger = false) {
|
||||
var builder = services
|
||||
.AddAuthentication("CustomScheme")
|
||||
.AddScheme<AuthenticationSchemeOptions, AuthenticationHandler>("CustomScheme", _ => { });
|
||||
|
||||
if (configureSwagger) {
|
||||
services.AddEndpointsApiExplorer();
|
||||
services.AddSwaggerGen(c => {
|
||||
c.AddSecurityDefinition("CustomScheme", new OpenApiSecurityScheme {
|
||||
Name = "Authorization",
|
||||
In = ParameterLocation.Header,
|
||||
Type = SecuritySchemeType.ApiKey,
|
||||
Scheme = "CustomScheme"
|
||||
});
|
||||
|
||||
c.AddSecurityRequirement(new OpenApiSecurityRequirement {{
|
||||
new OpenApiSecurityScheme {
|
||||
Reference = new OpenApiReference
|
||||
{
|
||||
Type = ReferenceType.SecurityScheme,
|
||||
Id = "CustomScheme"
|
||||
},
|
||||
Scheme = "oauth2",
|
||||
Name = "CustomScheme",
|
||||
In = ParameterLocation.Header,
|
||||
},
|
||||
ArraySegment<string>.Empty
|
||||
}});
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
return builder;
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class AuthenticationHandler : AuthenticationHandler<AuthenticationSchemeOptions> {
|
||||
private readonly ITokenApi _tokens;
|
||||
|
||||
public AuthenticationHandler(
|
||||
IOptionsMonitor<AuthenticationSchemeOptions> options,
|
||||
ILoggerFactory logger,
|
||||
UrlEncoder encoder,
|
||||
ISystemClock clock,
|
||||
ITokenApi tokens)
|
||||
: base(options, logger, encoder, clock) {
|
||||
_tokens = tokens;
|
||||
}
|
||||
|
||||
protected override async Task<AuthenticateResult> HandleAuthenticateAsync() {
|
||||
string tokenId = Request.Headers["Authorization"];
|
||||
if (string.IsNullOrEmpty(tokenId)) tokenId = Request.Query["token"];
|
||||
string clientIp = Context.Connection.RemoteIpAddress?.ToString();
|
||||
return !_tokens.ValidateToken(tokenId, clientIp) ? AuthenticateResult.Fail("Token invalid") : AuthenticateResult.Success(GenerateTicket(tokenId));
|
||||
}
|
||||
|
||||
private AuthenticationTicket GenerateTicket(string tokenId) {
|
||||
var user = _tokens.GetUserFromToken(tokenId);
|
||||
var claims = new List<Claim> {
|
||||
new("TokenId", tokenId),
|
||||
new("UserId", user.UserId)
|
||||
};
|
||||
|
||||
var principal = new ClaimsPrincipal();
|
||||
principal.AddIdentity(new ClaimsIdentity(claims, Scheme.Name));
|
||||
return new AuthenticationTicket(principal, Scheme.Name);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static class ClaimsPrincipalExtensions {
|
||||
public static string GetTokenId(this ClaimsPrincipal principal) => principal.FindFirstValue("TokenId");
|
||||
public static string GetUserId(this ClaimsPrincipal principal) => principal.FindFirstValue("UserId");
|
||||
}
|
||||
31
ProjectManager.Backend/Security/AuthorizationHandler.cs
Normal file
31
ProjectManager.Backend/Security/AuthorizationHandler.cs
Normal file
@@ -0,0 +1,31 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc.Filters;
|
||||
|
||||
namespace ProjectManager.Backend.Security;
|
||||
|
||||
public sealed class AuthorizedAttribute : TypeFilterAttribute {
|
||||
public AuthorizedAttribute(params string[] permission) : base(typeof(AuthorizationHandler)) {
|
||||
Arguments = new object[] { permission };
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class AuthorizationHandler : IAuthorizationFilter {
|
||||
|
||||
private readonly string[] _permissions;
|
||||
|
||||
public AuthorizationHandler(params string[] permissions) {
|
||||
_permissions = permissions;
|
||||
}
|
||||
|
||||
|
||||
public void OnAuthorization(AuthorizationFilterContext context) {
|
||||
if (context.Filters.Any(item => item is IAllowAnonymousFilter)) return;
|
||||
if (context.HttpContext.User.Identity?.IsAuthenticated != true) {
|
||||
context.Result = new UnauthorizedResult();
|
||||
return;
|
||||
}
|
||||
|
||||
//TODO: Handle Permissions
|
||||
}
|
||||
}
|
||||
19
ProjectManager.Backend/Security/ITokenContext.cs
Normal file
19
ProjectManager.Backend/Security/ITokenContext.cs
Normal file
@@ -0,0 +1,19 @@
|
||||
namespace ProjectManager.Backend.Security;
|
||||
|
||||
public interface ITokenContext {
|
||||
public bool IsAuthenticated { get; }
|
||||
public string TokenId { get; }
|
||||
public string UserId { get; }
|
||||
}
|
||||
|
||||
public sealed class TokenContext : ITokenContext {
|
||||
private readonly IHttpContextAccessor _accessor;
|
||||
|
||||
public bool IsAuthenticated => _accessor.HttpContext?.User.Identity?.IsAuthenticated == true;
|
||||
public string TokenId => _accessor.HttpContext?.User.GetTokenId();
|
||||
public string UserId => _accessor.HttpContext?.User.GetUserId();
|
||||
|
||||
public TokenContext(IHttpContextAccessor accessor) {
|
||||
_accessor = accessor;
|
||||
}
|
||||
}
|
||||
28
ProjectManager.Backend/appsettings.json
Normal file
28
ProjectManager.Backend/appsettings.json
Normal file
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning",
|
||||
"ProjectManager.Backend.Security.AuthenticationHandler": "None"
|
||||
}
|
||||
},
|
||||
"AllowedHosts": ["http://localhost:5110"],
|
||||
"Frontend": null,
|
||||
"General": {
|
||||
"TokenTimeInDays": 30,
|
||||
"Database": null,
|
||||
"PortRange": "24400-24599",
|
||||
"DockerPath": "unix:///var/run/docker.sock",
|
||||
"Root": "./projects/",
|
||||
"PocketBaseVersion": "latest",
|
||||
"MaxProjects": 3
|
||||
},
|
||||
"Proxy": {
|
||||
"Enable": false,
|
||||
"Url": null,
|
||||
"Email": null,
|
||||
"Password": null,
|
||||
"Domain": null,
|
||||
"Host": null
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user