v1.0
This commit is contained in:
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 ""; }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user