mirror of
https://github.com/RedWizardsLab/EchoHub.git
synced 2026-09-04 08:36:11 +02:00
Refactor EchoHub solution structure and remove unused components
- Deleted EchoHub.slnx and related project files for EchoHub.Web and EchoHub.Api. - Removed ServerDirectoryDbContext, ServerDirectoryService, and related extensions and DTOs. - Updated CommandHandler and Program.cs in EchoHub.Client to handle URL sending instead of file downloading. - Added SendUrlRequest DTO and corresponding API endpoint in ChannelsController for handling URL uploads. - Implemented database migration support in EchoHub.Server with initial migration setup. - Adjusted Program.cs to include HTTP client configuration for image downloading. - Cleaned up unnecessary files and references to streamline the project.
This commit is contained in:
@@ -1,18 +0,0 @@
|
||||
using EchoHub.Core.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace EchoHub.Api.Data;
|
||||
|
||||
public class ServerDirectoryDbContext(DbContextOptions<ServerDirectoryDbContext> options) : DbContext(options)
|
||||
{
|
||||
public DbSet<ServerInfo> Servers => Set<ServerInfo>();
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
modelBuilder.Entity<ServerInfo>(entity =>
|
||||
{
|
||||
entity.HasKey(e => e.Id);
|
||||
entity.HasIndex(e => e.Url).IsUnique();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.3" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\EchoHub.Core\EchoHub.Core.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -1,69 +0,0 @@
|
||||
using EchoHub.Api.Data;
|
||||
using EchoHub.Api.Services;
|
||||
using EchoHub.Core.DTOs;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace EchoHub.Api.Extensions;
|
||||
|
||||
public static class ServerDirectoryExtensions
|
||||
{
|
||||
public static IServiceCollection AddServerDirectory(this IServiceCollection services, string connectionString)
|
||||
{
|
||||
services.AddDbContext<ServerDirectoryDbContext>(options =>
|
||||
options.UseSqlite(connectionString));
|
||||
|
||||
services.AddScoped<ServerDirectoryService>();
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
public static WebApplication MapServerDirectoryApi(this WebApplication app)
|
||||
{
|
||||
var group = app.MapGroup("/api/servers");
|
||||
|
||||
group.MapGet("/", async (ServerDirectoryService service) =>
|
||||
{
|
||||
var servers = await service.GetAllServersAsync();
|
||||
return Results.Ok(servers);
|
||||
});
|
||||
|
||||
group.MapGet("/search", async (string q, ServerDirectoryService service) =>
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(q))
|
||||
return Results.BadRequest("Query parameter 'q' is required.");
|
||||
|
||||
var servers = await service.SearchServersAsync(q);
|
||||
return Results.Ok(servers);
|
||||
});
|
||||
|
||||
group.MapGet("/{id:guid}", async (Guid id, ServerDirectoryService service) =>
|
||||
{
|
||||
var server = await service.GetServerByIdAsync(id);
|
||||
return server is null ? Results.NotFound() : Results.Ok(server);
|
||||
});
|
||||
|
||||
group.MapPost("/", async (RegisterServerRequest request, ServerDirectoryService service) =>
|
||||
{
|
||||
try
|
||||
{
|
||||
var server = await service.RegisterServerAsync(request);
|
||||
return Results.Created($"/api/servers/{server.Id}", server);
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
return Results.Conflict(new { error = ex.Message });
|
||||
}
|
||||
});
|
||||
|
||||
group.MapPut("/{id:guid}/status", async (Guid id, ServerStatusDto status, ServerDirectoryService service) =>
|
||||
{
|
||||
var server = await service.UpdateServerStatusAsync(id, status.OnlineUsers, status.TotalChannels);
|
||||
return server is null ? Results.NotFound() : Results.Ok(server);
|
||||
});
|
||||
|
||||
return app;
|
||||
}
|
||||
}
|
||||
@@ -1,90 +0,0 @@
|
||||
using EchoHub.Api.Data;
|
||||
using EchoHub.Core.DTOs;
|
||||
using EchoHub.Core.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace EchoHub.Api.Services;
|
||||
|
||||
public class ServerDirectoryService(ServerDirectoryDbContext db)
|
||||
{
|
||||
public async Task<List<ServerInfoDto>> GetAllServersAsync()
|
||||
{
|
||||
return await db.Servers
|
||||
.OrderByDescending(s => s.OnlineUsers)
|
||||
.Select(s => new ServerInfoDto(
|
||||
s.Id, s.Name, s.Description, s.Url,
|
||||
s.OnlineUsers, s.TotalChannels, s.IsOnline, s.LastPingAt))
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<ServerInfoDto?> GetServerByIdAsync(Guid id)
|
||||
{
|
||||
var server = await db.Servers.FindAsync(id);
|
||||
return server is null ? null : MapToDto(server);
|
||||
}
|
||||
|
||||
public async Task<ServerInfoDto> RegisterServerAsync(RegisterServerRequest request)
|
||||
{
|
||||
var exists = await db.Servers.AnyAsync(s => s.Url == request.Url);
|
||||
if (exists)
|
||||
throw new InvalidOperationException($"A server with URL '{request.Url}' is already registered.");
|
||||
|
||||
var server = new ServerInfo
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Name = request.Name,
|
||||
Description = request.Description,
|
||||
Url = request.Url,
|
||||
OnlineUsers = 0,
|
||||
TotalChannels = 0,
|
||||
RegisteredAt = DateTimeOffset.UtcNow,
|
||||
LastPingAt = DateTimeOffset.UtcNow,
|
||||
IsOnline = true
|
||||
};
|
||||
|
||||
db.Servers.Add(server);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
return MapToDto(server);
|
||||
}
|
||||
|
||||
public async Task<ServerInfoDto?> UpdateServerStatusAsync(Guid id, int onlineUsers, int totalChannels)
|
||||
{
|
||||
var server = await db.Servers.FindAsync(id);
|
||||
if (server is null)
|
||||
return null;
|
||||
|
||||
server.OnlineUsers = onlineUsers;
|
||||
server.TotalChannels = totalChannels;
|
||||
server.LastPingAt = DateTimeOffset.UtcNow;
|
||||
server.IsOnline = true;
|
||||
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
return MapToDto(server);
|
||||
}
|
||||
|
||||
public async Task<List<ServerInfoDto>> SearchServersAsync(string query)
|
||||
{
|
||||
var lowerQuery = query.ToLowerInvariant();
|
||||
|
||||
return await db.Servers
|
||||
.Where(s => s.Name.ToLower().Contains(lowerQuery)
|
||||
|| (s.Description != null && s.Description.ToLower().Contains(lowerQuery)))
|
||||
.OrderByDescending(s => s.OnlineUsers)
|
||||
.Select(s => new ServerInfoDto(
|
||||
s.Id, s.Name, s.Description, s.Url,
|
||||
s.OnlineUsers, s.TotalChannels, s.IsOnline, s.LastPingAt))
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
private static ServerInfoDto MapToDto(ServerInfo server) => new(
|
||||
server.Id,
|
||||
server.Name,
|
||||
server.Description,
|
||||
server.Url,
|
||||
server.OnlineUsers,
|
||||
server.TotalChannels,
|
||||
server.IsOnline,
|
||||
server.LastPingAt);
|
||||
}
|
||||
@@ -129,8 +129,8 @@ public class CommandHandler
|
||||
await OnSendFile(target);
|
||||
var fileName = Path.GetFileName(uri.LocalPath);
|
||||
if (string.IsNullOrWhiteSpace(fileName))
|
||||
fileName = "download";
|
||||
return new CommandResult(true, $"Downloading & uploading: {fileName}...");
|
||||
fileName = "image";
|
||||
return new CommandResult(true, $"Sending: {fileName}...");
|
||||
}
|
||||
|
||||
if (!File.Exists(target))
|
||||
|
||||
@@ -143,35 +143,12 @@ public static class Program
|
||||
if (Uri.TryCreate(target, UriKind.Absolute, out var uri)
|
||||
&& (uri.Scheme == "http" || uri.Scheme == "https"))
|
||||
{
|
||||
// Download from URL then upload
|
||||
Log.Information("Downloading file from {Url}", target);
|
||||
using var httpClient = new HttpClient();
|
||||
using var response = await httpClient.GetAsync(uri);
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
var fileName = Path.GetFileName(uri.LocalPath);
|
||||
if (string.IsNullOrWhiteSpace(fileName) || !fileName.Contains('.'))
|
||||
{
|
||||
// Try to infer extension from Content-Type
|
||||
var contentType = response.Content.Headers.ContentType?.MediaType ?? "";
|
||||
var ext = contentType switch
|
||||
{
|
||||
"image/png" => ".png",
|
||||
"image/jpeg" => ".jpg",
|
||||
"image/gif" => ".gif",
|
||||
"image/webp" => ".webp",
|
||||
_ => ""
|
||||
};
|
||||
fileName = $"download{ext}";
|
||||
}
|
||||
|
||||
await using var stream = await response.Content.ReadAsStreamAsync();
|
||||
await _apiClient.UploadFileAsync(channel, stream, fileName);
|
||||
Log.Information("URL file uploaded: {FileName}", fileName);
|
||||
// Send URL to server — server handles downloading and conversion
|
||||
await _apiClient.SendUrlAsync(channel, target);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Local file
|
||||
// Local file — upload to server
|
||||
await using var stream = File.OpenRead(target);
|
||||
var fileName = Path.GetFileName(target);
|
||||
await _apiClient.UploadFileAsync(channel, stream, fileName);
|
||||
@@ -179,9 +156,9 @@ public static class Program
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(ex, "File upload failed for {Target}", target);
|
||||
Log.Error(ex, "File send failed for {Target}", target);
|
||||
_app.Invoke(() =>
|
||||
_mainWindow!.ShowError($"File upload failed: {ex.Message}"));
|
||||
_mainWindow!.ShowError($"Send failed: {ex.Message}"));
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -173,6 +173,16 @@ public sealed class ApiClient : IDisposable
|
||||
return await response.Content.ReadFromJsonAsync<MessageDto>();
|
||||
}
|
||||
|
||||
public async Task<MessageDto?> SendUrlAsync(string channelName, string url)
|
||||
{
|
||||
EnsureAuthenticated();
|
||||
var request = new SendUrlRequest(url);
|
||||
var response = await AuthenticatedRequestAsync(() =>
|
||||
_http.PostAsJsonAsync($"/api/channels/{Uri.EscapeDataString(channelName)}/send-url", request));
|
||||
await EnsureSuccessAsync(response);
|
||||
return await response.Content.ReadFromJsonAsync<MessageDto>();
|
||||
}
|
||||
|
||||
public async Task<ChannelDto?> CreateChannelAsync(string name, string? topic = null)
|
||||
{
|
||||
EnsureAuthenticated();
|
||||
|
||||
@@ -33,3 +33,5 @@ public record SendMessageRequest(string ChannelName, string Content);
|
||||
public record CreateChannelRequest(string Name, string? Topic = null);
|
||||
|
||||
public record UpdateTopicRequest(string? Topic);
|
||||
|
||||
public record SendUrlRequest(string Url);
|
||||
|
||||
@@ -1,15 +1,3 @@
|
||||
namespace EchoHub.Core.DTOs;
|
||||
|
||||
public record ServerInfoDto(
|
||||
Guid Id,
|
||||
string Name,
|
||||
string? Description,
|
||||
string Url,
|
||||
int OnlineUsers,
|
||||
int TotalChannels,
|
||||
bool IsOnline,
|
||||
DateTimeOffset LastPingAt);
|
||||
|
||||
public record RegisterServerRequest(string Name, string Url, string? Description = null);
|
||||
|
||||
public record ServerStatusDto(string Name, string? Description, int OnlineUsers, int TotalChannels);
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
namespace EchoHub.Core.Models;
|
||||
|
||||
public class ServerInfo
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public required string Name { get; set; }
|
||||
public string? Description { get; set; }
|
||||
public required string Url { get; set; }
|
||||
public int OnlineUsers { get; set; }
|
||||
public int TotalChannels { get; set; }
|
||||
public DateTimeOffset RegisteredAt { get; set; } = DateTimeOffset.UtcNow;
|
||||
public DateTimeOffset LastPingAt { get; set; } = DateTimeOffset.UtcNow;
|
||||
public bool IsOnline { get; set; } = true;
|
||||
}
|
||||
@@ -22,6 +22,7 @@ public class ChannelsController(
|
||||
EchoHubDbContext db,
|
||||
FileStorageService fileStorage,
|
||||
ImageToAsciiService asciiService,
|
||||
IHttpClientFactory httpClientFactory,
|
||||
IHubContext<ChatHub, IEchoHubClient> hubContext) : ControllerBase
|
||||
{
|
||||
[HttpGet]
|
||||
@@ -217,4 +218,121 @@ public class ChannelsController(
|
||||
|
||||
return Ok(messageDto);
|
||||
}
|
||||
|
||||
[HttpPost("{channel}/send-url")]
|
||||
[EnableRateLimiting("upload")]
|
||||
public async Task<IActionResult> SendUrl(string channel, [FromBody] SendUrlRequest request)
|
||||
{
|
||||
var userIdClaim = User.FindFirstValue(ClaimTypes.NameIdentifier);
|
||||
var usernameClaim = User.FindFirstValue("username");
|
||||
if (userIdClaim is null || usernameClaim is null)
|
||||
return Unauthorized(new ErrorResponse("Authentication required."));
|
||||
|
||||
var userId = Guid.Parse(userIdClaim);
|
||||
var channelName = channel.ToLowerInvariant().Trim();
|
||||
|
||||
if (!ValidationConstants.ChannelNameRegex().IsMatch(channelName))
|
||||
return BadRequest(new ErrorResponse("Invalid channel name format."));
|
||||
|
||||
var dbChannel = await db.Channels.FirstOrDefaultAsync(c => c.Name == channelName);
|
||||
if (dbChannel is null)
|
||||
return NotFound(new ErrorResponse($"Channel '{channelName}' does not exist."));
|
||||
|
||||
if (string.IsNullOrWhiteSpace(request.Url))
|
||||
return BadRequest(new ErrorResponse("URL is required."));
|
||||
|
||||
if (!Uri.TryCreate(request.Url, UriKind.Absolute, out var uri)
|
||||
|| (uri.Scheme != "http" && uri.Scheme != "https"))
|
||||
return BadRequest(new ErrorResponse("Invalid URL. Only http and https are supported."));
|
||||
|
||||
// Download image from URL
|
||||
byte[] imageBytes;
|
||||
string fileName;
|
||||
try
|
||||
{
|
||||
using var client = httpClientFactory.CreateClient("ImageDownload");
|
||||
using var response = await client.GetAsync(uri, HttpCompletionOption.ResponseHeadersRead);
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
var contentLength = response.Content.Headers.ContentLength;
|
||||
if (contentLength > HubConstants.MaxFileSizeBytes)
|
||||
return BadRequest(new ErrorResponse($"File size exceeds maximum of {HubConstants.MaxFileSizeBytes / (1024 * 1024)} MB."));
|
||||
|
||||
imageBytes = await response.Content.ReadAsByteArrayAsync();
|
||||
|
||||
if (imageBytes.Length > HubConstants.MaxFileSizeBytes)
|
||||
return BadRequest(new ErrorResponse($"File size exceeds maximum of {HubConstants.MaxFileSizeBytes / (1024 * 1024)} MB."));
|
||||
|
||||
fileName = Path.GetFileName(uri.LocalPath);
|
||||
if (string.IsNullOrWhiteSpace(fileName) || !fileName.Contains('.'))
|
||||
{
|
||||
var contentType = response.Content.Headers.ContentType?.MediaType ?? "";
|
||||
var ext = contentType switch
|
||||
{
|
||||
"image/png" => ".png",
|
||||
"image/jpeg" or "image/jpg" => ".jpg",
|
||||
"image/gif" => ".gif",
|
||||
"image/webp" => ".webp",
|
||||
_ => ".bin"
|
||||
};
|
||||
fileName = $"download{ext}";
|
||||
}
|
||||
}
|
||||
catch (TaskCanceledException)
|
||||
{
|
||||
return BadRequest(new ErrorResponse("Download timed out. The URL may be unreachable."));
|
||||
}
|
||||
catch (HttpRequestException ex)
|
||||
{
|
||||
return BadRequest(new ErrorResponse($"Failed to download from URL: {ex.Message}"));
|
||||
}
|
||||
|
||||
// Validate it's actually an image
|
||||
using var memoryStream = new MemoryStream(imageBytes);
|
||||
if (!FileValidationHelper.IsValidImage(memoryStream))
|
||||
return BadRequest(new ErrorResponse("The URL does not point to a valid image. Supported formats: JPEG, PNG, GIF, WebP."));
|
||||
|
||||
// Save file and convert to ASCII
|
||||
var (fileId, filePath) = await fileStorage.SaveFileAsync(memoryStream, fileName);
|
||||
|
||||
string content;
|
||||
using (var imageStream = System.IO.File.OpenRead(filePath))
|
||||
{
|
||||
content = asciiService.ConvertToAscii(imageStream);
|
||||
}
|
||||
|
||||
var attachmentUrl = $"/api/files/{fileId}";
|
||||
var sender = await db.Users.FindAsync(userId);
|
||||
|
||||
var message = new Message
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Content = content,
|
||||
Type = MessageType.Image,
|
||||
AttachmentUrl = attachmentUrl,
|
||||
AttachmentFileName = fileName,
|
||||
SentAt = DateTimeOffset.UtcNow,
|
||||
ChannelId = dbChannel.Id,
|
||||
SenderUserId = userId,
|
||||
SenderUsername = usernameClaim,
|
||||
};
|
||||
|
||||
db.Messages.Add(message);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
var messageDto = new MessageDto(
|
||||
message.Id,
|
||||
message.Content,
|
||||
message.SenderUsername,
|
||||
sender?.NicknameColor,
|
||||
channelName,
|
||||
MessageType.Image,
|
||||
attachmentUrl,
|
||||
fileName,
|
||||
message.SentAt);
|
||||
|
||||
await hubContext.Clients.Group(channelName).ReceiveMessage(messageDto);
|
||||
|
||||
return Ok(messageDto);
|
||||
}
|
||||
}
|
||||
|
||||
+210
@@ -0,0 +1,210 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using EchoHub.Server.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace EchoHub.Server.Data.Migrations
|
||||
{
|
||||
[DbContext(typeof(EchoHubDbContext))]
|
||||
[Migration("20260219023113_InitialCreate")]
|
||||
partial class InitialCreate
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder.HasAnnotation("ProductVersion", "10.0.3");
|
||||
|
||||
modelBuilder.Entity("EchoHub.Core.Models.Channel", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<long>("CreatedAt")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<Guid>("CreatedByUserId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Topic")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Name")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("Channels");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("EchoHub.Core.Models.Message", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("AttachmentFileName")
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("AttachmentUrl")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid>("ChannelId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Content")
|
||||
.IsRequired()
|
||||
.HasMaxLength(2000)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid>("SenderUserId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("SenderUsername")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<long>("SentAt")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("Type")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ChannelId");
|
||||
|
||||
b.HasIndex("SentAt");
|
||||
|
||||
b.ToTable("Messages");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("EchoHub.Core.Models.RefreshToken", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<long>("CreatedAt")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<long>("ExpiresAt")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<long?>("RevokedAt")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("TokenHash")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid>("UserId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("TokenHash");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("RefreshTokens");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("EchoHub.Core.Models.User", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("AvatarAscii")
|
||||
.HasMaxLength(10000)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Bio")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<long>("CreatedAt")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("DisplayName")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<long>("LastSeenAt")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("NicknameColor")
|
||||
.HasMaxLength(7)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("PasswordHash")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("Status")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("StatusMessage")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Username")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Username")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("Users");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("EchoHub.Core.Models.Message", b =>
|
||||
{
|
||||
b.HasOne("EchoHub.Core.Models.Channel", "Channel")
|
||||
.WithMany("Messages")
|
||||
.HasForeignKey("ChannelId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Channel");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("EchoHub.Core.Models.RefreshToken", b =>
|
||||
{
|
||||
b.HasOne("EchoHub.Core.Models.User", "User")
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("EchoHub.Core.Models.Channel", b =>
|
||||
{
|
||||
b.Navigation("Messages");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace EchoHub.Server.Data.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class InitialCreate : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Channels",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "TEXT", nullable: false),
|
||||
Name = table.Column<string>(type: "TEXT", maxLength: 100, nullable: false),
|
||||
Topic = table.Column<string>(type: "TEXT", maxLength: 500, nullable: true),
|
||||
CreatedAt = table.Column<long>(type: "INTEGER", nullable: false),
|
||||
CreatedByUserId = table.Column<Guid>(type: "TEXT", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Channels", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Users",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "TEXT", nullable: false),
|
||||
Username = table.Column<string>(type: "TEXT", maxLength: 50, nullable: false),
|
||||
PasswordHash = table.Column<string>(type: "TEXT", nullable: false),
|
||||
DisplayName = table.Column<string>(type: "TEXT", maxLength: 100, nullable: true),
|
||||
Bio = table.Column<string>(type: "TEXT", maxLength: 500, nullable: true),
|
||||
NicknameColor = table.Column<string>(type: "TEXT", maxLength: 7, nullable: true),
|
||||
AvatarAscii = table.Column<string>(type: "TEXT", maxLength: 10000, nullable: true),
|
||||
Status = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
StatusMessage = table.Column<string>(type: "TEXT", maxLength: 100, nullable: true),
|
||||
CreatedAt = table.Column<long>(type: "INTEGER", nullable: false),
|
||||
LastSeenAt = table.Column<long>(type: "INTEGER", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Users", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Messages",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "TEXT", nullable: false),
|
||||
Content = table.Column<string>(type: "TEXT", maxLength: 2000, nullable: false),
|
||||
Type = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
AttachmentUrl = table.Column<string>(type: "TEXT", maxLength: 500, nullable: true),
|
||||
AttachmentFileName = table.Column<string>(type: "TEXT", maxLength: 255, nullable: true),
|
||||
SentAt = table.Column<long>(type: "INTEGER", nullable: false),
|
||||
ChannelId = table.Column<Guid>(type: "TEXT", nullable: false),
|
||||
SenderUserId = table.Column<Guid>(type: "TEXT", nullable: false),
|
||||
SenderUsername = table.Column<string>(type: "TEXT", maxLength: 50, nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Messages", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_Messages_Channels_ChannelId",
|
||||
column: x => x.ChannelId,
|
||||
principalTable: "Channels",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "RefreshTokens",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "TEXT", nullable: false),
|
||||
TokenHash = table.Column<string>(type: "TEXT", maxLength: 128, nullable: false),
|
||||
UserId = table.Column<Guid>(type: "TEXT", nullable: false),
|
||||
ExpiresAt = table.Column<long>(type: "INTEGER", nullable: false),
|
||||
CreatedAt = table.Column<long>(type: "INTEGER", nullable: false),
|
||||
RevokedAt = table.Column<long>(type: "INTEGER", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_RefreshTokens", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_RefreshTokens_Users_UserId",
|
||||
column: x => x.UserId,
|
||||
principalTable: "Users",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Channels_Name",
|
||||
table: "Channels",
|
||||
column: "Name",
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Messages_ChannelId",
|
||||
table: "Messages",
|
||||
column: "ChannelId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Messages_SentAt",
|
||||
table: "Messages",
|
||||
column: "SentAt");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_RefreshTokens_TokenHash",
|
||||
table: "RefreshTokens",
|
||||
column: "TokenHash");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_RefreshTokens_UserId",
|
||||
table: "RefreshTokens",
|
||||
column: "UserId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Users_Username",
|
||||
table: "Users",
|
||||
column: "Username",
|
||||
unique: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "Messages");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "RefreshTokens");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Channels");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Users");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,207 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using EchoHub.Server.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace EchoHub.Server.Data.Migrations
|
||||
{
|
||||
[DbContext(typeof(EchoHubDbContext))]
|
||||
partial class EchoHubDbContextModelSnapshot : ModelSnapshot
|
||||
{
|
||||
protected override void BuildModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder.HasAnnotation("ProductVersion", "10.0.3");
|
||||
|
||||
modelBuilder.Entity("EchoHub.Core.Models.Channel", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<long>("CreatedAt")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<Guid>("CreatedByUserId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Topic")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Name")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("Channels");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("EchoHub.Core.Models.Message", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("AttachmentFileName")
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("AttachmentUrl")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid>("ChannelId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Content")
|
||||
.IsRequired()
|
||||
.HasMaxLength(2000)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid>("SenderUserId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("SenderUsername")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<long>("SentAt")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("Type")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ChannelId");
|
||||
|
||||
b.HasIndex("SentAt");
|
||||
|
||||
b.ToTable("Messages");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("EchoHub.Core.Models.RefreshToken", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<long>("CreatedAt")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<long>("ExpiresAt")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<long?>("RevokedAt")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("TokenHash")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid>("UserId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("TokenHash");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("RefreshTokens");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("EchoHub.Core.Models.User", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("AvatarAscii")
|
||||
.HasMaxLength(10000)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Bio")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<long>("CreatedAt")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("DisplayName")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<long>("LastSeenAt")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("NicknameColor")
|
||||
.HasMaxLength(7)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("PasswordHash")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("Status")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("StatusMessage")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Username")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Username")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("Users");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("EchoHub.Core.Models.Message", b =>
|
||||
{
|
||||
b.HasOne("EchoHub.Core.Models.Channel", "Channel")
|
||||
.WithMany("Messages")
|
||||
.HasForeignKey("ChannelId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Channel");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("EchoHub.Core.Models.RefreshToken", b =>
|
||||
{
|
||||
b.HasOne("EchoHub.Core.Models.User", "User")
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("EchoHub.Core.Models.Channel", b =>
|
||||
{
|
||||
b.Navigation("Messages");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,10 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="BCrypt.Net-Next" Version="4.1.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.3" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.3">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.3" />
|
||||
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.12" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -73,6 +73,11 @@ builder.Services.AddSingleton<JwtTokenService>();
|
||||
builder.Services.AddSingleton<PresenceTracker>();
|
||||
builder.Services.AddSingleton<ImageToAsciiService>();
|
||||
builder.Services.AddSingleton<FileStorageService>();
|
||||
builder.Services.AddHttpClient("ImageDownload", client =>
|
||||
{
|
||||
client.Timeout = TimeSpan.FromSeconds(15);
|
||||
client.MaxResponseContentBufferSize = 10 * 1024 * 1024; // 10 MB
|
||||
});
|
||||
|
||||
// ── Rate Limiting ────────────────────────────────────────────────────────────
|
||||
builder.Services.AddRateLimiter(options =>
|
||||
@@ -125,8 +130,64 @@ var app = builder.Build();
|
||||
using (var scope = app.Services.CreateScope())
|
||||
{
|
||||
var db = scope.ServiceProvider.GetRequiredService<EchoHubDbContext>();
|
||||
await db.Database.EnsureCreatedAsync();
|
||||
var logger = scope.ServiceProvider.GetRequiredService<ILogger<Program>>();
|
||||
|
||||
try
|
||||
{
|
||||
// If the DB was created by EnsureCreated (no __EFMigrationsHistory table),
|
||||
// back it up and recreate so MigrateAsync can manage the schema properly.
|
||||
if (await db.Database.CanConnectAsync())
|
||||
{
|
||||
var conn = db.Database.GetDbConnection();
|
||||
await conn.OpenAsync();
|
||||
|
||||
using var cmd = conn.CreateCommand();
|
||||
cmd.CommandText = "SELECT count(*) FROM sqlite_master WHERE type='table' AND name='__EFMigrationsHistory'";
|
||||
var hasMigrationTable = Convert.ToInt64(await cmd.ExecuteScalarAsync()) > 0;
|
||||
|
||||
if (!hasMigrationTable)
|
||||
{
|
||||
cmd.CommandText = "SELECT count(*) FROM sqlite_master WHERE type='table' AND name='Users'";
|
||||
var hasLegacyTables = Convert.ToInt64(await cmd.ExecuteScalarAsync()) > 0;
|
||||
|
||||
if (hasLegacyTables)
|
||||
{
|
||||
var dbPath = conn.DataSource;
|
||||
await conn.CloseAsync();
|
||||
|
||||
// Back up the legacy DB file before deleting
|
||||
if (!string.IsNullOrEmpty(dbPath) && File.Exists(dbPath))
|
||||
{
|
||||
var timestamp = DateTime.Now.ToString("yyyyMMdd_HHmmss");
|
||||
var backupPath = $"{dbPath}.legacy_{timestamp}";
|
||||
File.Copy(dbPath, backupPath, overwrite: false);
|
||||
logger.LogWarning("Legacy database backed up to '{BackupPath}'.", backupPath);
|
||||
}
|
||||
|
||||
await db.Database.EnsureDeletedAsync();
|
||||
logger.LogWarning("Legacy database removed. A new database will be created with migration support.");
|
||||
}
|
||||
else
|
||||
{
|
||||
await conn.CloseAsync();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
await conn.CloseAsync();
|
||||
}
|
||||
}
|
||||
|
||||
await db.Database.MigrateAsync();
|
||||
logger.LogInformation("Database migrated successfully.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Database migration failed.");
|
||||
throw;
|
||||
}
|
||||
|
||||
// Seed the default channel if it doesn't exist
|
||||
if (!await db.Channels.AnyAsync(c => c.Name == HubConstants.DefaultChannel))
|
||||
{
|
||||
db.Channels.Add(new Channel
|
||||
@@ -138,6 +199,7 @@ using (var scope = app.Services.CreateScope())
|
||||
});
|
||||
|
||||
await db.SaveChangesAsync();
|
||||
logger.LogInformation("Default channel '{Channel}' created.", HubConstants.DefaultChannel);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\EchoHub.Api\EchoHub.Api.csproj" />
|
||||
<ProjectReference Include="..\EchoHub.Core\EchoHub.Core.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.3" />
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
@@ -1,45 +0,0 @@
|
||||
using EchoHub.Api.Data;
|
||||
using EchoHub.Api.Extensions;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection")
|
||||
?? "Data Source=serverdirectory.db";
|
||||
|
||||
builder.Services.AddServerDirectory(connectionString);
|
||||
|
||||
builder.Services.AddCors(options =>
|
||||
{
|
||||
options.AddDefaultPolicy(policy =>
|
||||
{
|
||||
policy.AllowAnyOrigin()
|
||||
.AllowAnyMethod()
|
||||
.AllowAnyHeader();
|
||||
});
|
||||
});
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
using (var scope = app.Services.CreateScope())
|
||||
{
|
||||
var db = scope.ServiceProvider.GetRequiredService<ServerDirectoryDbContext>();
|
||||
await db.Database.EnsureCreatedAsync();
|
||||
}
|
||||
|
||||
app.UseCors();
|
||||
|
||||
app.MapGet("/", () => Results.Content("""
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head><title>EchoHub</title></head>
|
||||
<body>
|
||||
<h1>Welcome to EchoHub</h1>
|
||||
<p>EchoHub is a decentralized IRC-like chat network.</p>
|
||||
<p>Use the <a href="/api/servers">Server Directory API</a> to browse available servers.</p>
|
||||
</body>
|
||||
</html>
|
||||
""", "text/html"));
|
||||
|
||||
app.MapServerDirectoryApi();
|
||||
|
||||
app.Run();
|
||||
@@ -1,23 +0,0 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/launchsettings.json",
|
||||
"profiles": {
|
||||
"http": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": true,
|
||||
"applicationUrl": "http://localhost:6000",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
},
|
||||
"https": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": true,
|
||||
"applicationUrl": "https://localhost:7103;http://localhost:5062",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
<Solution>
|
||||
<Project Path="EchoHub.Client/EchoHub.Client.csproj" />
|
||||
<Project Path="EchoHub.Core/EchoHub.Core.csproj" />
|
||||
<Project Path="EchoHub.Server/EchoHub.Server.csproj" />
|
||||
</Solution>
|
||||
Reference in New Issue
Block a user