Initial commit: HealthManager full-stack health management platform

Backend: .NET 10 + PostgreSQL + EF Core + JWT + SignalR
Frontend patient: React 19 + TypeScript + Vite (mobile H5)
Frontend doctor: React 19 + TypeScript + Vite (desktop web)
This commit is contained in:
MingNian
2026-05-20 16:18:56 +08:00
commit 435af55c4a
215 changed files with 18595 additions and 0 deletions

View File

@@ -0,0 +1,120 @@
using System.Security.Claims;
using HealthManager.Application.DTOs.Auth;
using HealthManager.Domain.Interfaces;
using HealthManager.Application.Services;
using HealthManager.Domain.Entities;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace HealthManager.WebApi.Controllers;
[ApiController]
[Route("api/auth")]
public class AuthController(
AuthService authService,
IJwtProvider jwtProvider) : ControllerBase
{
[HttpPost("send-sms")]
public IActionResult SendSms([FromBody] SendSmsRequest request)
{
// Demo: always succeed
return Ok(new { message = "验证码已发送" });
}
[HttpPost("login")]
public async Task<IActionResult> Login([FromBody] LoginRequest request)
{
var user = await authService.GetUserByPhoneAsync(request.Phone);
if (user == null)
return Unauthorized(new { message = "用户不存在" });
// Demo: accept any SMS code
var accessToken = jwtProvider.GenerateAccessToken(user.Id, user.Name, user.Role);
var refreshToken = jwtProvider.GenerateRefreshToken();
await authService.SaveRefreshTokenAsync(user.Id, refreshToken, DateTime.UtcNow.AddDays(7));
return Ok(new AuthResponse(user.Id, user.Name, user.Role, accessToken, refreshToken));
}
[HttpPost("register")]
public async Task<IActionResult> Register([FromBody] RegisterRequest request)
{
var existing = await authService.GetUserByPhoneAsync(request.Phone);
if (existing != null)
return Conflict(new { message = "该手机号已注册" });
var user = new User
{
Phone = request.Phone,
Name = request.Name,
Role = "patient",
PasswordHash = AuthService.HashPassword("demo123"),
};
// Access DbContext via DI
var db = HttpContext.RequestServices.GetRequiredService<Infrastructure.Data.AppDbContext>();
db.Users.Add(user);
await db.SaveChangesAsync();
var accessToken = jwtProvider.GenerateAccessToken(user.Id, user.Name, user.Role);
var refreshToken = jwtProvider.GenerateRefreshToken();
await authService.SaveRefreshTokenAsync(user.Id, refreshToken, DateTime.UtcNow.AddDays(7));
return Ok(new AuthResponse(user.Id, user.Name, user.Role, accessToken, refreshToken));
}
[HttpPost("refresh")]
public async Task<IActionResult> RefreshToken([FromBody] RefreshTokenRequest request)
{
var saved = await authService.GetRefreshTokenAsync(request.RefreshToken);
if (saved == null)
return Unauthorized(new { message = "无效的刷新令牌" });
await authService.RevokeRefreshTokenAsync(saved.UserId);
var accessToken = jwtProvider.GenerateAccessToken(saved.User.Id, saved.User.Name, saved.User.Role);
var refreshToken = jwtProvider.GenerateRefreshToken();
await authService.SaveRefreshTokenAsync(saved.UserId, refreshToken, DateTime.UtcNow.AddDays(7));
return Ok(new AuthResponse(saved.User.Id, saved.User.Name, saved.User.Role, accessToken, refreshToken));
}
[HttpGet("me")]
[Authorize]
public async Task<IActionResult> GetProfile()
{
var userId = Guid.Parse(User.FindFirstValue(ClaimTypes.NameIdentifier)!);
var db = HttpContext.RequestServices.GetRequiredService<Infrastructure.Data.AppDbContext>();
var user = await db.Users.FindAsync(userId);
if (user == null) return NotFound();
return Ok(new UserProfileResponse(
user.Id, user.Name, user.Phone, user.Role,
user.Gender, user.Birthday, user.HeightCm, user.WeightKg,
user.MedicalHistory, user.StentDate, user.StentType,
user.Department, user.Title, user.Specialty, user.Introduction));
}
[HttpPut("me")]
[Authorize]
public async Task<IActionResult> UpdateProfile([FromBody] UpdateProfileRequest request)
{
var userId = Guid.Parse(User.FindFirstValue(ClaimTypes.NameIdentifier)!);
var db = HttpContext.RequestServices.GetRequiredService<Infrastructure.Data.AppDbContext>();
var user = await db.Users.FindAsync(userId);
if (user == null) return NotFound();
if (request.Name != null) user.Name = request.Name;
if (request.Gender != null) user.Gender = request.Gender;
if (request.Birthday.HasValue) user.Birthday = request.Birthday;
if (request.HeightCm.HasValue) user.HeightCm = request.HeightCm;
if (request.WeightKg.HasValue) user.WeightKg = request.WeightKg;
if (request.MedicalHistory != null) user.MedicalHistory = request.MedicalHistory;
user.UpdatedAt = DateTime.UtcNow;
await db.SaveChangesAsync();
return Ok(new { message = "更新成功" });
}
}
public record RefreshTokenRequest(string RefreshToken);

View File

@@ -0,0 +1,71 @@
using System.Security.Claims;
using HealthManager.Application.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace HealthManager.WebApi.Controllers;
[ApiController]
[Route("api/consultations")]
[Authorize]
public class ConsultationController(ConsultationService consultationService) : ControllerBase
{
private Guid UserId => Guid.Parse(User.FindFirstValue(ClaimTypes.NameIdentifier)!);
private string Role => User.FindFirstValue(ClaimTypes.Role)!;
[HttpGet("doctors")]
public async Task<IActionResult> GetDoctors([FromQuery] string? department)
{
var doctors = await consultationService.GetDoctorsAsync(department);
return Ok(doctors.Select(d => new
{
d.Id, d.Name, d.Department, d.Title, d.Specialty, d.Introduction, d.AvatarUrl,
}));
}
[HttpGet]
public async Task<IActionResult> GetConsultations()
{
var consultations = Role == "doctor"
? await consultationService.GetDoctorConsultationsAsync(UserId)
: await consultationService.GetPatientConsultationsAsync(UserId);
return Ok(consultations.Select(c => new
{
c.Id, c.PatientId, c.DoctorId,
PatientName = c.Patient?.Name,
DoctorName = c.Doctor?.Name,
c.Subject, c.Status, c.StartedAt, c.ClosedAt, c.Summary,
}));
}
[HttpPost]
public async Task<IActionResult> StartConsultation([FromBody] StartConsultationRequest request)
{
var consultation = await consultationService.StartAsync(UserId, request.DoctorId, request.Subject);
return Ok(new { consultation.Id, consultation.PatientId, consultation.DoctorId, consultation.Status });
}
[HttpGet("{id:guid}/messages")]
public async Task<IActionResult> GetMessages(Guid id)
{
var messages = await consultationService.GetMessagesAsync(id);
return Ok(messages.Select(m => new
{
m.Id, m.SenderId, m.SenderRole, m.ContentType, m.Content,
m.ImageUrl, m.IsRead, m.CreatedAt,
SenderName = m.Sender?.Name,
}));
}
[HttpPost("{id:guid}/messages")]
public async Task<IActionResult> SendMessage(Guid id, [FromBody] SendMessageRequest request)
{
var message = await consultationService.SendMessageAsync(id, UserId, Role, request.Content,
request.ContentType, request.ImageUrl);
return Ok(new { message.Id, message.SenderId, message.SenderRole, message.Content, message.CreatedAt });
}
}
public record StartConsultationRequest(Guid DoctorId, string Subject);
public record SendMessageRequest(string Content, string ContentType = "text", string? ImageUrl = null);

View File

@@ -0,0 +1,67 @@
using System.Security.Claims;
using HealthManager.Application.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace HealthManager.WebApi.Controllers;
[ApiController]
[Route("api/follow-ups")]
[Authorize]
public class FollowUpController(FollowUpService followUpService) : ControllerBase
{
private Guid UserId => Guid.Parse(User.FindFirstValue(ClaimTypes.NameIdentifier)!);
private string Role => User.FindFirstValue(ClaimTypes.Role)!;
[HttpGet]
public async Task<IActionResult> GetFollowUps()
{
var followUps = Role == "doctor"
? await followUpService.GetDoctorFollowUpsAsync(UserId)
: await followUpService.GetPatientFollowUpsAsync(UserId);
return Ok(followUps.Select(f => new
{
f.Id, f.PatientId, f.DoctorId, f.Title, f.Description,
f.ScheduledAt, f.Status, f.Notes, f.ReminderEnabled, f.CreatedAt,
PatientName = f.Patient?.Name,
DoctorName = f.Doctor?.Name,
}));
}
[HttpGet("{id:guid}")]
public async Task<IActionResult> GetFollowUp(Guid id)
{
var followUp = await followUpService.GetByIdAsync(id);
if (followUp == null) return NotFound(new { message = "复查不存在" });
return Ok(new
{
followUp.Id, followUp.PatientId, followUp.DoctorId, followUp.Title,
followUp.Description, followUp.ScheduledAt, followUp.Status,
followUp.Notes, followUp.ReminderEnabled, followUp.CreatedAt,
});
}
[HttpPost]
public async Task<IActionResult> AddFollowUp([FromBody] FollowUpCreateRequest request)
{
var followUp = await followUpService.AddAsync(UserId, request.Title, request.Description,
request.ScheduledAt, request.ReminderEnabled);
return Ok(new { followUp.Id, followUp.Title, followUp.Status });
}
[HttpPut("{id:guid}")]
[Authorize(Roles = "doctor")]
public async Task<IActionResult> UpdateFollowUp(Guid id, [FromBody] FollowUpUpdateRequest request)
{
var followUp = await followUpService.UpdateAsync(id, UserId, request.Title, request.Description,
request.ScheduledAt, request.Status, request.Notes);
if (followUp == null) return NotFound(new { message = "复查不存在" });
return Ok(new { followUp.Id, followUp.Title, followUp.Status });
}
}
public record FollowUpCreateRequest(string Title, string? Description, DateTime ScheduledAt, bool ReminderEnabled = true);
public record FollowUpUpdateRequest(
string? Title, string? Description, DateTime? ScheduledAt, string? Status, string? Notes);

View File

@@ -0,0 +1,61 @@
using System.Security.Claims;
using System.Text.Json;
using HealthManager.Application.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace HealthManager.WebApi.Controllers;
[ApiController]
[Route("api/health-records")]
[Authorize]
public class HealthController(HealthService healthService) : ControllerBase
{
private Guid UserId => Guid.Parse(User.FindFirstValue(ClaimTypes.NameIdentifier)!);
private string Role => User.FindFirstValue(ClaimTypes.Role)!;
[HttpGet]
public async Task<IActionResult> GetRecords([FromQuery] string? type, [FromQuery] int days = 30)
{
var targetUserId = UserId;
// Doctors can query any patient
if (Role == "doctor" && Request.Query.ContainsKey("patientId"))
targetUserId = Guid.Parse(Request.Query["patientId"]!);
var records = await healthService.GetRecordsAsync(targetUserId, type, days);
return Ok(records.Select(r => new
{
r.Id, r.Type, Value = r.Value.RootElement.GetRawText(), r.Unit,
r.RecordedAt, r.Source, r.Notes, r.CreatedAt,
}));
}
[HttpGet("stats")]
public async Task<IActionResult> GetStats()
{
var stats = await healthService.GetStatsAsync(UserId);
return Ok(stats);
}
[HttpGet("latest/{type}")]
public async Task<IActionResult> GetLatest(string type)
{
var record = await healthService.GetLatestAsync(UserId, type);
if (record == null) return Ok((object?)null);
return Ok(new { record.Id, record.Type, Value = record.Value.RootElement.GetRawText(), record.Unit, record.RecordedAt, record.Source });
}
[HttpPost]
public async Task<IActionResult> AddRecord([FromBody] HealthRecordCreateRequest request)
{
// Validate JSON
try { JsonDocument.Parse(request.ValueJson); }
catch (JsonException) { return BadRequest(new { message = "无效的数据格式" }); }
var record = await healthService.AddRecordAsync(UserId, request.Type, request.ValueJson, request.Unit, request.RecordedAt, request.Notes);
return Ok(new { record.Id, record.Type, Value = record.Value.RootElement.GetRawText(), record.Unit, record.RecordedAt, record.Source });
}
}
public record HealthRecordCreateRequest(string Type, string ValueJson, string Unit, DateTime RecordedAt, string? Notes);

View File

@@ -0,0 +1,80 @@
using System.Security.Claims;
using HealthManager.Application.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace HealthManager.WebApi.Controllers;
[ApiController]
[Route("api/medications")]
[Authorize]
public class MedicationController(MedicationService medicationService) : ControllerBase
{
private Guid UserId => Guid.Parse(User.FindFirstValue(ClaimTypes.NameIdentifier)!);
private string Role => User.FindFirstValue(ClaimTypes.Role)!;
[HttpGet]
public async Task<IActionResult> GetMedications()
{
var targetUserId = UserId;
if (Role == "doctor" && Request.Query.ContainsKey("patientId"))
targetUserId = Guid.Parse(Request.Query["patientId"]!);
var medications = await medicationService.GetUserMedicationsAsync(targetUserId);
return Ok(medications.Select(m => new
{
m.Id, m.UserId, m.DrugName, m.Dosage, m.Frequency, m.TimeSlots,
m.StartDate, m.EndDate, m.Notes, m.Status, m.CreatedAt,
}));
}
[HttpPost]
public async Task<IActionResult> AddMedication([FromBody] MedicationCreateRequest request)
{
var med = await medicationService.AddAsync(UserId, request.DrugName, request.Dosage,
request.Frequency, request.TimeSlots, request.StartDate, request.EndDate, request.Notes);
return Ok(new { med.Id, med.DrugName, med.Dosage, med.Frequency, med.Status });
}
[HttpGet("{id:guid}")]
public async Task<IActionResult> GetMedication(Guid id)
{
var med = await medicationService.GetByIdAsync(id);
if (med == null) return NotFound(new { message = "药品不存在" });
return Ok(new
{
med.Id, med.UserId, med.DrugName, med.Dosage, med.Frequency, med.TimeSlots,
med.StartDate, med.EndDate, med.Notes, med.Status, med.CreatedAt,
});
}
[HttpGet("{id:guid}/records")]
public async Task<IActionResult> GetRecords(Guid id)
{
var records = await medicationService.GetRecordsAsync(id);
return Ok(records.Select(r => new
{
r.Id, r.MedicationId, r.TimeSlot, r.TakenAt, r.IsTaken, r.SkippedReason, r.CreatedAt,
}));
}
[HttpPost("{id:guid}/take")]
public async Task<IActionResult> MarkTaken(Guid id, [FromBody] MarkTakenRequest request)
{
var record = await medicationService.MarkTakenAsync(id, UserId, request.TimeSlot);
return Ok(new { record.Id, record.TimeSlot, record.TakenAt, record.IsTaken });
}
[HttpGet("{id:guid}/adherence")]
public async Task<IActionResult> GetAdherence(Guid id)
{
var rate = await medicationService.GetAdherenceRateAsync(id);
return Ok(new { medicationId = id, rate });
}
}
public record MedicationCreateRequest(
string DrugName, string Dosage, string Frequency,
List<string> TimeSlots, DateOnly StartDate, DateOnly? EndDate, string? Notes);
public record MarkTakenRequest(string TimeSlot);

View File

@@ -0,0 +1,46 @@
using System.Security.Claims;
using HealthManager.Application.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace HealthManager.WebApi.Controllers;
[ApiController]
[Route("api/notifications")]
[Authorize]
public class NotificationController(NotificationService notificationService) : ControllerBase
{
private Guid UserId => Guid.Parse(User.FindFirstValue(ClaimTypes.NameIdentifier)!);
[HttpGet]
public async Task<IActionResult> GetNotifications()
{
var notifications = await notificationService.GetUserNotificationsAsync(UserId);
return Ok(notifications.Select(n => new
{
n.Id, n.Type, n.Title, n.Content, n.RelatedId,
n.IsRead, n.ReadAt, n.CreatedAt,
}));
}
[HttpGet("unread-count")]
public async Task<IActionResult> GetUnreadCount()
{
var count = await notificationService.GetUnreadCountAsync(UserId);
return Ok(new { count });
}
[HttpPut("{id:guid}/read")]
public async Task<IActionResult> MarkAsRead(Guid id)
{
await notificationService.MarkAsReadAsync(id);
return Ok(new { message = "已标记为已读" });
}
[HttpPut("read-all")]
public async Task<IActionResult> MarkAllAsRead()
{
await notificationService.MarkAllAsReadAsync(UserId);
return Ok(new { message = "全部已读" });
}
}

View File

@@ -0,0 +1,27 @@
using System.Security.Claims;
using HealthManager.Application.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace HealthManager.WebApi.Controllers;
[ApiController]
[Route("api/patients")]
[Authorize(Roles = "doctor")]
public class PatientController(PatientService patientService) : ControllerBase
{
[HttpGet]
public async Task<IActionResult> GetPatients([FromQuery] string? search, [FromQuery] int page = 1, [FromQuery] int pageSize = 20)
{
var patients = await patientService.GetPatientsAsync(search, null, page, pageSize);
return Ok(patients);
}
[HttpGet("{id:guid}")]
public async Task<IActionResult> GetPatientDetail(Guid id)
{
var patient = await patientService.GetPatientDetailAsync(id);
if (patient == null) return NotFound(new { message = "患者不存在" });
return Ok(patient);
}
}

View File

@@ -0,0 +1,87 @@
using System.Security.Claims;
using HealthManager.Application.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace HealthManager.WebApi.Controllers;
[ApiController]
[Route("api/reports")]
[Authorize]
public class ReportController(ReportService reportService) : ControllerBase
{
private Guid UserId => Guid.Parse(User.FindFirstValue(ClaimTypes.NameIdentifier)!);
private string Role => User.FindFirstValue(ClaimTypes.Role)!;
[HttpGet]
public async Task<IActionResult> GetReports()
{
var targetUserId = UserId;
if (Role == "doctor" && Request.Query.ContainsKey("patientId"))
targetUserId = Guid.Parse(Request.Query["patientId"]!);
var reports = await reportService.GetPatientReportsAsync(targetUserId);
return Ok(reports.Select(r => new
{
r.Id, r.PatientId, r.Title, r.Category, r.ImageUrls, r.Status,
r.RiskLevel, r.UploadedAt, r.CompletedAt,
DoctorName = r.Doctor?.Name,
}));
}
[HttpGet("pending")]
[Authorize(Roles = "doctor")]
public async Task<IActionResult> GetPending()
{
var reports = await reportService.GetPendingAsync();
return Ok(reports.Select(r => new
{
r.Id, r.PatientId, r.Title, r.Category, r.Status, r.UploadedAt,
PatientName = r.Patient?.Name,
}));
}
[HttpGet("{id:guid}")]
public async Task<IActionResult> GetReport(Guid id)
{
var report = await reportService.GetByIdAsync(id);
if (report == null) return NotFound(new { message = "报告不存在" });
return Ok(new
{
report.Id, report.PatientId, report.Title, report.Category, report.ImageUrls,
report.Status, report.RiskLevel, report.Summary, report.Suggestions,
report.UploadedAt, report.CompletedAt,
PatientName = report.Patient?.Name,
DoctorName = report.Doctor?.Name,
Items = report.Items.Select(i => new
{
i.Id, i.ItemName, i.ResultValue, i.Unit, i.ReferenceRange, i.IsAbnormal,
}),
});
}
[HttpPost]
public async Task<IActionResult> UploadReport([FromBody] ReportUploadRequest request)
{
var report = await reportService.UploadAsync(UserId, request.Title, request.Category, request.ImageUrls);
return Ok(new { report.Id, report.Title, report.Status });
}
[HttpPost("{id:guid}/interpret")]
[Authorize(Roles = "doctor")]
public async Task<IActionResult> InterpretReport(Guid id, [FromBody] ReportInterpretRequest request)
{
var items = request.Items.Select(i => (i.ItemName, i.ResultValue, i.Unit, i.ReferenceRange, i.IsAbnormal)).ToList();
var report = await reportService.InterpretAsync(id, UserId, request.Summary, items, request.RiskLevel, request.Suggestions);
return Ok(new { report.Id, report.Status, report.RiskLevel });
}
}
public record ReportUploadRequest(string Title, string Category, List<string> ImageUrls);
public record ReportInterpretRequest(
string Summary, List<ReportItemRequest> Items, string RiskLevel, string? Suggestions);
public record ReportItemRequest(
string ItemName, string ResultValue, string? Unit, string? ReferenceRange, bool IsAbnormal);

View File

@@ -0,0 +1,21 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.8" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.8" />
<PackageReference Include="Microsoft.OpenApi" Version="2.4.1" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="10.1.7" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\HealthManager.Application\HealthManager.Application.csproj" />
<ProjectReference Include="..\HealthManager.Infrastructure\HealthManager.Infrastructure.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,6 @@
@HealthManager.WebApi_HostAddress = http://localhost:5133
GET {{HealthManager.WebApi_HostAddress}}/weatherforecast/
Accept: application/json
###

View File

@@ -0,0 +1,40 @@
using System.Security.Claims;
using HealthManager.Application.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.SignalR;
namespace HealthManager.WebApi.Hubs;
[Authorize]
public class ChatHub(ConsultationService consultationService) : Hub
{
public async Task JoinConsultation(Guid consultationId)
{
await Groups.AddToGroupAsync(Context.ConnectionId, $"consultation_{consultationId}");
}
public async Task LeaveConsultation(Guid consultationId)
{
await Groups.RemoveFromGroupAsync(Context.ConnectionId, $"consultation_{consultationId}");
}
public async Task SendMessage(Guid consultationId, string content)
{
var userId = Guid.Parse(Context.User!.FindFirstValue(ClaimTypes.NameIdentifier)!);
var role = Context.User!.FindFirstValue(ClaimTypes.Role)!;
var message = await consultationService.SendMessageAsync(consultationId, userId, role, content);
await Clients.Group($"consultation_{consultationId}").SendAsync("ReceiveMessage", new
{
message.Id,
message.SenderId,
message.SenderRole,
message.ContentType,
message.Content,
message.ImageUrl,
message.IsRead,
message.CreatedAt,
});
}
}

View File

@@ -0,0 +1,101 @@
using System.Text;
using HealthManager.Domain.Interfaces;
using HealthManager.Application.Services;
using HealthManager.Infrastructure.Data;
using HealthManager.Infrastructure.Services;
using HealthManager.WebApi.Hubs;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.EntityFrameworkCore;
using Microsoft.IdentityModel.Tokens;
using Swashbuckle.AspNetCore.SwaggerGen;
var builder = WebApplication.CreateBuilder(args);
// Database
builder.Services.AddDbContext<AppDbContext>(options =>
options.UseNpgsql(builder.Configuration.GetConnectionString("Default")));
// JWT
var jwtSecret = builder.Configuration["Jwt:Secret"]!;
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = builder.Configuration["Jwt:Issuer"],
ValidAudience = builder.Configuration["Jwt:Audience"],
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtSecret)),
};
options.Events = new JwtBearerEvents
{
OnMessageReceived = context =>
{
var accessToken = context.Request.Query["access_token"];
var path = context.HttpContext.Request.Path;
if (!string.IsNullOrEmpty(accessToken) && path.StartsWithSegments("/hubs"))
context.Token = accessToken;
return Task.CompletedTask;
}
};
});
builder.Services.AddAuthorization();
// Services
builder.Services.AddSingleton<IJwtProvider, JwtProvider>();
builder.Services.AddScoped<AuthService>();
builder.Services.AddScoped<HealthService>();
builder.Services.AddScoped<MedicationService>();
builder.Services.AddScoped<ConsultationService>();
builder.Services.AddScoped<ReportService>();
builder.Services.AddScoped<FollowUpService>();
builder.Services.AddScoped<PatientService>();
builder.Services.AddScoped<NotificationService>();
// SignalR
builder.Services.AddSignalR();
// Swagger
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
// CORS
builder.Services.AddCors(options =>
{
options.AddPolicy("Dev", policy =>
{
policy.WithOrigins("http://localhost:5173", "http://localhost:5174", "http://localhost:5175")
.AllowAnyHeader()
.AllowAnyMethod()
.AllowCredentials();
});
});
builder.Services.AddControllers();
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseCors("Dev");
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.MapHub<ChatHub>("/hubs/chat");
// Auto-migrate and seed on startup
using (var scope = app.Services.CreateScope())
{
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
await db.Database.EnsureCreatedAsync();
await DataSeeder.SeedAsync(db);
}
app.Run();

View File

@@ -0,0 +1,23 @@
{
"$schema": "https://json.schemastore.org/launchsettings.json",
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": false,
"applicationUrl": "http://localhost:5133",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": false,
"applicationUrl": "https://localhost:7241;http://localhost:5133",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}

View File

@@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}

View File

@@ -0,0 +1,25 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"ConnectionStrings": {
"Default": "Host=localhost;Port=5432;Database=health_manager;Username=postgres;Password=postgres123"
},
"Jwt": {
"Secret": "health-manager-jwt-secret-key-2026-super-secure-long-enough!",
"Issuer": "HealthManager",
"Audience": "HealthManagerApp"
},
"Redis": {
"Connection": "localhost:6379"
},
"MinIO": {
"Endpoint": "localhost:9000",
"AccessKey": "minioadmin",
"SecretKey": "minioadmin"
}
}