todolist-proto/backend/Program.cs

221 lines
6.4 KiB
C#
Raw Normal View History

2026-01-20 18:40:33 +01:00
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<TodoDbContext>(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<TodoDbContext>();
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)
2026-01-20 20:34:43 +01:00
.ThenBy(t => t.CreatedAt) // Older todos first (ascending)
2026-01-20 18:40:33 +01:00
.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);
2026-01-20 20:34:43 +01:00
// Fetch todos and sort in memory to handle different sorting for completed vs incomplete
var allTodos = await db.Todos
2026-01-20 18:40:33 +01:00
.Where(t => t.UserId == userId &&
(!t.IsCompleted || (t.CompletedAt.HasValue && t.CompletedAt.Value > oneWeekAgo)))
.ToListAsync();
2026-01-20 20:34:43 +01:00
// 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();
2026-01-20 18:40:33 +01:00
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<TodoDbContext> options) : base(options) { }
public DbSet<Todo> Todos => Set<Todo>();
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Todo>(entity =>
{
entity.HasKey(e => e.Id);
entity.Property(e => e.UserId).IsRequired();
entity.Property(e => e.Title).IsRequired();
entity.HasIndex(e => e.UserId);
});
}
}