feat: scaffold backend and shared library

This commit is contained in:
Andre Beging
2026-01-29 10:17:16 +01:00
commit b6ce21b1b8
15 changed files with 763 additions and 0 deletions

331
src/ASTRAIN.Api/Program.cs Normal file
View File

@@ -0,0 +1,331 @@
using System.Text.RegularExpressions;
using ASTRAIN.Api.Data;
using ASTRAIN.Api.Services;
using ASTRAIN.Shared;
using Microsoft.EntityFrameworkCore;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddOpenApi();
builder.Services.AddCors(options =>
{
options.AddDefaultPolicy(policy =>
{
policy.WithOrigins("http://localhost:5014", "https://localhost:7252")
.AllowAnyHeader()
.AllowAnyMethod();
});
});
builder.Services.AddDbContext<AppDbContext>(options =>
{
var connectionString = builder.Configuration.GetConnectionString("Default")
?? "Data Source=Data/astrain.db";
options.UseSqlite(connectionString);
});
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.MapOpenApi();
}
app.UseHttpsRedirection();
app.UseCors();
app.UseDefaultFiles();
app.UseStaticFiles();
using (var scope = app.Services.CreateScope())
{
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
db.Database.EnsureCreated();
}
var api = app.MapGroup("/api");
api.MapGet("/health", () => Results.Ok(new { status = "ok" }));
api.MapGet("/users/ensure", async (string? userId, AppDbContext db) =>
{
var user = await EnsureUserAsync(db, userId);
return Results.Ok(new EnsureUserResponse(user.Id));
});
api.MapGet("/users/{userId}", async (string userId, AppDbContext db) =>
{
var user = await EnsureUserAsync(db, userId);
return Results.Ok(new EnsureUserResponse(user.Id));
});
api.MapGet("/users/{userId}/exercises", async (string userId, AppDbContext db) =>
{
var user = await EnsureUserAsync(db, userId);
var items = await db.Exercises
.Where(e => e.UserId == user.Id)
.OrderBy(e => e.Name)
.Select(e => new ExerciseDto(e.Id, e.Name))
.ToListAsync();
return Results.Ok(items);
});
api.MapPost("/users/{userId}/exercises", async (string userId, ExerciseUpsertRequest request, AppDbContext db) =>
{
var user = await EnsureUserAsync(db, userId);
if (string.IsNullOrWhiteSpace(request.Name))
{
return Results.BadRequest("Name is required.");
}
var exercise = new Exercise
{
Name = request.Name.Trim(),
UserId = user.Id
};
db.Exercises.Add(exercise);
await db.SaveChangesAsync();
return Results.Ok(new ExerciseDto(exercise.Id, exercise.Name));
});
api.MapPut("/users/{userId}/exercises/{exerciseId:int}", async (string userId, int exerciseId, ExerciseUpsertRequest request, AppDbContext db) =>
{
var user = await EnsureUserAsync(db, userId);
var exercise = await db.Exercises.FirstOrDefaultAsync(e => e.Id == exerciseId && e.UserId == user.Id);
if (exercise is null)
{
return Results.NotFound();
}
if (string.IsNullOrWhiteSpace(request.Name))
{
return Results.BadRequest("Name is required.");
}
exercise.Name = request.Name.Trim();
await db.SaveChangesAsync();
return Results.Ok(new ExerciseDto(exercise.Id, exercise.Name));
});
api.MapGet("/users/{userId}/routines", async (string userId, AppDbContext db) =>
{
var user = await EnsureUserAsync(db, userId);
var routines = await db.Routines
.Include(r => r.Exercises)
.ThenInclude(re => re.Exercise)
.Where(r => r.UserId == user.Id)
.OrderBy(r => r.Name)
.ToListAsync();
var payload = routines.Select(r => new RoutineDto(
r.Id,
r.Name,
r.Exercises
.OrderBy(re => re.Order)
.Select(re => new RoutineExerciseDto(re.ExerciseId, re.Exercise?.Name ?? string.Empty, re.Order))
.ToList()
));
return Results.Ok(payload);
});
api.MapGet("/users/{userId}/routines/{routineId:int}", async (string userId, int routineId, AppDbContext db) =>
{
var user = await EnsureUserAsync(db, userId);
var routine = await db.Routines
.Include(r => r.Exercises)
.ThenInclude(re => re.Exercise)
.FirstOrDefaultAsync(r => r.Id == routineId && r.UserId == user.Id);
if (routine is null)
{
return Results.NotFound();
}
var payload = new RoutineDto(
routine.Id,
routine.Name,
routine.Exercises
.OrderBy(re => re.Order)
.Select(re => new RoutineExerciseDto(re.ExerciseId, re.Exercise?.Name ?? string.Empty, re.Order))
.ToList());
return Results.Ok(payload);
});
api.MapPost("/users/{userId}/routines", async (string userId, RoutineUpsertRequest request, AppDbContext db) =>
{
var user = await EnsureUserAsync(db, userId);
if (string.IsNullOrWhiteSpace(request.Name))
{
return Results.BadRequest("Name is required.");
}
var routine = new Routine
{
Name = request.Name.Trim(),
UserId = user.Id
};
var exercises = await db.Exercises
.Where(e => e.UserId == user.Id && request.ExerciseIds.Contains(e.Id))
.ToListAsync();
routine.Exercises = request.ExerciseIds
.Select((exerciseId, index) => new RoutineExercise
{
ExerciseId = exerciseId,
Order = index
})
.ToList();
db.Routines.Add(routine);
await db.SaveChangesAsync();
var dto = new RoutineDto(
routine.Id,
routine.Name,
routine.Exercises
.Select((re, index) => new RoutineExerciseDto(re.ExerciseId, exercises.FirstOrDefault(e => e.Id == re.ExerciseId)?.Name ?? string.Empty, index))
.ToList());
return Results.Ok(dto);
});
api.MapPut("/users/{userId}/routines/{routineId:int}", async (string userId, int routineId, RoutineUpsertRequest request, AppDbContext db) =>
{
var user = await EnsureUserAsync(db, userId);
var routine = await db.Routines
.Include(r => r.Exercises)
.FirstOrDefaultAsync(r => r.Id == routineId && r.UserId == user.Id);
if (routine is null)
{
return Results.NotFound();
}
if (string.IsNullOrWhiteSpace(request.Name))
{
return Results.BadRequest("Name is required.");
}
routine.Name = request.Name.Trim();
routine.Exercises.Clear();
foreach (var exerciseId in request.ExerciseIds)
{
routine.Exercises.Add(new RoutineExercise
{
ExerciseId = exerciseId,
Order = routine.Exercises.Count
});
}
await db.SaveChangesAsync();
var exercises = await db.Exercises
.Where(e => e.UserId == user.Id && request.ExerciseIds.Contains(e.Id))
.ToListAsync();
var dto = new RoutineDto(
routine.Id,
routine.Name,
routine.Exercises
.OrderBy(re => re.Order)
.Select(re => new RoutineExerciseDto(re.ExerciseId, exercises.FirstOrDefault(e => e.Id == re.ExerciseId)?.Name ?? string.Empty, re.Order))
.ToList());
return Results.Ok(dto);
});
api.MapGet("/users/{userId}/routines/{routineId:int}/last-run", async (string userId, int routineId, AppDbContext db) =>
{
var user = await EnsureUserAsync(db, userId);
var lastRun = await db.RoutineRuns
.Include(rr => rr.Entries)
.Where(rr => rr.UserId == user.Id && rr.RoutineId == routineId)
.OrderByDescending(rr => rr.PerformedAt)
.FirstOrDefaultAsync();
if (lastRun is null)
{
return Results.Ok(new RoutineRunSummaryDto(DateTime.MinValue, new List<RoutineRunEntryDto>()));
}
var summary = new RoutineRunSummaryDto(
lastRun.PerformedAt,
lastRun.Entries
.Select(e => new RoutineRunEntryDto(e.ExerciseId, e.Weight, e.Completed))
.ToList());
return Results.Ok(summary);
});
api.MapPost("/users/{userId}/routines/{routineId:int}/runs", async (string userId, int routineId, RoutineRunRequest request, AppDbContext db) =>
{
var user = await EnsureUserAsync(db, userId);
var routine = await db.Routines.FirstOrDefaultAsync(r => r.Id == routineId && r.UserId == user.Id);
if (routine is null)
{
return Results.NotFound();
}
var run = new RoutineRun
{
RoutineId = routine.Id,
UserId = user.Id,
PerformedAt = DateTime.UtcNow,
Entries = request.Entries.Select(entry => new RoutineRunEntry
{
ExerciseId = entry.ExerciseId,
Weight = entry.Weight,
Completed = entry.Completed
}).ToList()
};
db.RoutineRuns.Add(run);
await db.SaveChangesAsync();
return Results.Ok(new { run.Id, run.PerformedAt });
});
app.MapFallbackToFile("index.html");
app.Run();
static async Task<User> EnsureUserAsync(AppDbContext db, string? userId)
{
if (!string.IsNullOrWhiteSpace(userId) && IsValidUserId(userId))
{
var existing = await db.Users.FirstOrDefaultAsync(u => u.Id == userId);
if (existing is not null)
{
return existing;
}
var created = new User { Id = userId };
db.Users.Add(created);
await db.SaveChangesAsync();
return created;
}
while (true)
{
var newId = UserKeyGenerator.Generate(8);
var exists = await db.Users.AnyAsync(u => u.Id == newId);
if (exists)
{
continue;
}
var user = new User { Id = newId };
db.Users.Add(user);
await db.SaveChangesAsync();
return user;
}
}
static bool IsValidUserId(string userId)
{
return Regex.IsMatch(userId, "^[A-Za-z0-9]{8}$");
}