using Microsoft.EntityFrameworkCore; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.IdentityModel.Tokens; using System.Security.Claims; var builder = WebApplication.CreateBuilder(args); // Add CORS builder.Services.AddCors(options => { options.AddPolicy("AllowFrontend", policy => { policy.WithOrigins("http://localhost:3030") .AllowAnyHeader() .AllowAnyMethod() .AllowCredentials(); }); }); // Add SQLite Database builder.Services.AddDbContext(options => options.UseSqlite(builder.Configuration.GetConnectionString("DefaultConnection"))); // Add Keycloak Authentication builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(options => { options.Authority = "https://terminus.bluelake.cloud/realms/dalex-immo-dev"; options.RequireHttpsMetadata = true; options.Audience = "dalex-proto"; options.TokenValidationParameters = new TokenValidationParameters { ValidateIssuer = true, ValidateAudience = false, ValidateLifetime = true, ValidateIssuerSigningKey = true }; }); builder.Services.AddAuthorization(); var app = builder.Build(); // Ensure database is created using (var scope = app.Services.CreateScope()) { var db = scope.ServiceProvider.GetRequiredService(); db.Database.EnsureCreated(); } app.UseCors("AllowFrontend"); app.UseAuthentication(); app.UseAuthorization(); // Get all todos for current user app.MapGet("/api/todos", async (TodoDbContext db, HttpContext context) => { var userId = context.User.FindFirst(ClaimTypes.NameIdentifier)?.Value ?? context.User.FindFirst("sub")?.Value; if (string.IsNullOrEmpty(userId)) return Results.Unauthorized(); var todos = await db.Todos .Where(t => t.UserId == userId) .OrderByDescending(t => !t.IsCompleted) .ThenBy(t => t.CreatedAt) // Older todos first (ascending) .ToListAsync(); return Results.Ok(todos); }) .RequireAuthorization(); // Get recent todos (exclude old completed ones) app.MapGet("/api/todos/recent", async (TodoDbContext db, HttpContext context) => { var userId = context.User.FindFirst(ClaimTypes.NameIdentifier)?.Value ?? context.User.FindFirst("sub")?.Value; if (string.IsNullOrEmpty(userId)) return Results.Unauthorized(); var oneWeekAgo = DateTime.UtcNow.AddDays(-7); // Fetch todos and sort in memory to handle different sorting for completed vs incomplete var allTodos = await db.Todos .Where(t => t.UserId == userId && (!t.IsCompleted || (t.CompletedAt.HasValue && t.CompletedAt.Value > oneWeekAgo))) .ToListAsync(); // Sort: incomplete todos first (by CreatedAt ascending), then completed (by CompletedAt descending) var todos = allTodos .OrderBy(t => t.IsCompleted) .ThenBy(t => !t.IsCompleted ? t.CreatedAt : DateTime.MinValue) // Older incomplete first .ThenByDescending(t => t.IsCompleted ? (t.CompletedAt ?? DateTime.MinValue) : DateTime.MinValue) // Newer completed first .ToList(); return Results.Ok(todos); }) .RequireAuthorization(); // Create a new todo app.MapPost("/api/todos", async (TodoDbContext db, HttpContext context, TodoCreateDto dto) => { var userId = context.User.FindFirst(ClaimTypes.NameIdentifier)?.Value ?? context.User.FindFirst("sub")?.Value; if (string.IsNullOrEmpty(userId)) return Results.Unauthorized(); var todo = new Todo { UserId = userId, Title = dto.Title, Description = dto.Description, CreatedAt = DateTime.UtcNow, IsCompleted = false }; db.Todos.Add(todo); await db.SaveChangesAsync(); return Results.Created($"/api/todos/{todo.Id}", todo); }) .RequireAuthorization(); // Update a todo app.MapPut("/api/todos/{id}", async (TodoDbContext db, HttpContext context, int id, TodoUpdateDto dto) => { var userId = context.User.FindFirst(ClaimTypes.NameIdentifier)?.Value ?? context.User.FindFirst("sub")?.Value; if (string.IsNullOrEmpty(userId)) return Results.Unauthorized(); var todo = await db.Todos.FindAsync(id); if (todo == null || todo.UserId != userId) return Results.NotFound(); todo.Title = dto.Title ?? todo.Title; todo.Description = dto.Description ?? todo.Description; if (dto.IsCompleted.HasValue && dto.IsCompleted.Value != todo.IsCompleted) { todo.IsCompleted = dto.IsCompleted.Value; todo.CompletedAt = dto.IsCompleted.Value ? DateTime.UtcNow : null; } await db.SaveChangesAsync(); return Results.Ok(todo); }) .RequireAuthorization(); // Delete a todo app.MapDelete("/api/todos/{id}", async (TodoDbContext db, HttpContext context, int id) => { var userId = context.User.FindFirst(ClaimTypes.NameIdentifier)?.Value ?? context.User.FindFirst("sub")?.Value; if (string.IsNullOrEmpty(userId)) return Results.Unauthorized(); var todo = await db.Todos.FindAsync(id); if (todo == null || todo.UserId != userId) return Results.NotFound(); db.Todos.Remove(todo); await db.SaveChangesAsync(); return Results.NoContent(); }) .RequireAuthorization(); app.Run(); // Models public class Todo { public int Id { get; set; } public string UserId { get; set; } = string.Empty; public string Title { get; set; } = string.Empty; public string? Description { get; set; } public DateTime CreatedAt { get; set; } public DateTime? CompletedAt { get; set; } public bool IsCompleted { get; set; } } public class TodoCreateDto { public string Title { get; set; } = string.Empty; public string? Description { get; set; } } public class TodoUpdateDto { public string? Title { get; set; } public string? Description { get; set; } public bool? IsCompleted { get; set; } } public class TodoDbContext : DbContext { public TodoDbContext(DbContextOptions options) : base(options) { } public DbSet Todos => Set(); protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity(entity => { entity.HasKey(e => e.Id); entity.Property(e => e.UserId).IsRequired(); entity.Property(e => e.Title).IsRequired(); entity.HasIndex(e => e.UserId); }); } }