Refactor API endpoints into modules
This commit is contained in:
84
src/ASTRAIN.Api/Endpoints/ExerciseEndpoints.cs
Normal file
84
src/ASTRAIN.Api/Endpoints/ExerciseEndpoints.cs
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
using ASTRAIN.Api.Data;
|
||||||
|
using ASTRAIN.Api.Services;
|
||||||
|
using ASTRAIN.Shared.Dtos;
|
||||||
|
using ASTRAIN.Shared.Models;
|
||||||
|
using ASTRAIN.Shared.Requests;
|
||||||
|
using Microsoft.AspNetCore.Builder;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.AspNetCore.Routing;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace ASTRAIN.Api.Endpoints;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Provides exercise-related endpoints.
|
||||||
|
/// </summary>
|
||||||
|
internal static class ExerciseEndpoints
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Registers exercise routes under the provided route group.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="group">The API route group.</param>
|
||||||
|
/// <returns>The same route group for chaining.</returns>
|
||||||
|
public static RouteGroupBuilder MapExerciseEndpoints(this RouteGroupBuilder group)
|
||||||
|
{
|
||||||
|
group.MapGet("/users/{userId}/exercises", async (string userId, AppDbContext db) =>
|
||||||
|
{
|
||||||
|
var user = await UserProvisioning.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);
|
||||||
|
})
|
||||||
|
.WithSummary("List exercises")
|
||||||
|
.WithDescription("Returns the exercises for the specified user.");
|
||||||
|
|
||||||
|
group.MapPost("/users/{userId}/exercises", async (string userId, ExerciseUpsertRequest request, AppDbContext db) =>
|
||||||
|
{
|
||||||
|
var user = await UserProvisioning.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));
|
||||||
|
})
|
||||||
|
.WithSummary("Create exercise")
|
||||||
|
.WithDescription("Creates a new exercise for the specified user.");
|
||||||
|
|
||||||
|
group.MapPut("/users/{userId}/exercises/{exerciseId:int}", async (string userId, int exerciseId, ExerciseUpsertRequest request, AppDbContext db) =>
|
||||||
|
{
|
||||||
|
var user = await UserProvisioning.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));
|
||||||
|
})
|
||||||
|
.WithSummary("Update exercise")
|
||||||
|
.WithDescription("Updates the name of an exercise for the specified user.");
|
||||||
|
|
||||||
|
return group;
|
||||||
|
}
|
||||||
|
}
|
||||||
25
src/ASTRAIN.Api/Endpoints/HealthEndpoints.cs
Normal file
25
src/ASTRAIN.Api/Endpoints/HealthEndpoints.cs
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
using Microsoft.AspNetCore.Builder;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.AspNetCore.Routing;
|
||||||
|
|
||||||
|
namespace ASTRAIN.Api.Endpoints;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Provides health check endpoints.
|
||||||
|
/// </summary>
|
||||||
|
internal static class HealthEndpoints
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Registers health check routes under the provided route group.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="group">The API route group.</param>
|
||||||
|
/// <returns>The same route group for chaining.</returns>
|
||||||
|
public static RouteGroupBuilder MapHealthEndpoints(this RouteGroupBuilder group)
|
||||||
|
{
|
||||||
|
group.MapGet("/health", () => Results.Ok(new { status = "ok" }))
|
||||||
|
.WithSummary("Health check")
|
||||||
|
.WithDescription("Returns a simple payload used for liveness checks.");
|
||||||
|
|
||||||
|
return group;
|
||||||
|
}
|
||||||
|
}
|
||||||
165
src/ASTRAIN.Api/Endpoints/RoutineEndpoints.cs
Normal file
165
src/ASTRAIN.Api/Endpoints/RoutineEndpoints.cs
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
using ASTRAIN.Api.Data;
|
||||||
|
using ASTRAIN.Api.Services;
|
||||||
|
using ASTRAIN.Shared.Dtos;
|
||||||
|
using ASTRAIN.Shared.Models;
|
||||||
|
using ASTRAIN.Shared.Requests;
|
||||||
|
using Microsoft.AspNetCore.Builder;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.AspNetCore.Routing;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace ASTRAIN.Api.Endpoints;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Provides routine-related endpoints.
|
||||||
|
/// </summary>
|
||||||
|
internal static class RoutineEndpoints
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Registers routine routes under the provided route group.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="group">The API route group.</param>
|
||||||
|
/// <returns>The same route group for chaining.</returns>
|
||||||
|
public static RouteGroupBuilder MapRoutineEndpoints(this RouteGroupBuilder group)
|
||||||
|
{
|
||||||
|
group.MapGet("/users/{userId}/routines", async (string userId, AppDbContext db) =>
|
||||||
|
{
|
||||||
|
var user = await UserProvisioning.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);
|
||||||
|
})
|
||||||
|
.WithSummary("List routines")
|
||||||
|
.WithDescription("Returns all routines for the specified user.");
|
||||||
|
|
||||||
|
group.MapGet("/users/{userId}/routines/{routineId:int}", async (string userId, int routineId, AppDbContext db) =>
|
||||||
|
{
|
||||||
|
var user = await UserProvisioning.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);
|
||||||
|
})
|
||||||
|
.WithSummary("Get routine")
|
||||||
|
.WithDescription("Returns a specific routine and its exercises.");
|
||||||
|
|
||||||
|
group.MapPost("/users/{userId}/routines", async (string userId, RoutineUpsertRequest request, AppDbContext db) =>
|
||||||
|
{
|
||||||
|
var user = await UserProvisioning.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);
|
||||||
|
})
|
||||||
|
.WithSummary("Create routine")
|
||||||
|
.WithDescription("Creates a routine and associates exercises with it.");
|
||||||
|
|
||||||
|
group.MapPut("/users/{userId}/routines/{routineId:int}", async (string userId, int routineId, RoutineUpsertRequest request, AppDbContext db) =>
|
||||||
|
{
|
||||||
|
var user = await UserProvisioning.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);
|
||||||
|
})
|
||||||
|
.WithSummary("Update routine")
|
||||||
|
.WithDescription("Updates routine metadata and exercise ordering.");
|
||||||
|
|
||||||
|
return group;
|
||||||
|
}
|
||||||
|
}
|
||||||
82
src/ASTRAIN.Api/Endpoints/RoutineRunEndpoints.cs
Normal file
82
src/ASTRAIN.Api/Endpoints/RoutineRunEndpoints.cs
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
using ASTRAIN.Api.Data;
|
||||||
|
using ASTRAIN.Api.Services;
|
||||||
|
using ASTRAIN.Shared.Dtos;
|
||||||
|
using ASTRAIN.Shared.Models;
|
||||||
|
using ASTRAIN.Shared.Requests;
|
||||||
|
using Microsoft.AspNetCore.Builder;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.AspNetCore.Routing;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace ASTRAIN.Api.Endpoints;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Provides routine run-related endpoints.
|
||||||
|
/// </summary>
|
||||||
|
internal static class RoutineRunEndpoints
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Registers routine run routes under the provided route group.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="group">The API route group.</param>
|
||||||
|
/// <returns>The same route group for chaining.</returns>
|
||||||
|
public static RouteGroupBuilder MapRoutineRunEndpoints(this RouteGroupBuilder group)
|
||||||
|
{
|
||||||
|
group.MapGet("/users/{userId}/routines/{routineId:int}/last-run", async (string userId, int routineId, AppDbContext db) =>
|
||||||
|
{
|
||||||
|
var user = await UserProvisioning.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);
|
||||||
|
})
|
||||||
|
.WithSummary("Get last routine run")
|
||||||
|
.WithDescription("Returns the most recent run summary for a routine.");
|
||||||
|
|
||||||
|
group.MapPost("/users/{userId}/routines/{routineId:int}/runs", async (string userId, int routineId, RoutineRunRequest request, AppDbContext db) =>
|
||||||
|
{
|
||||||
|
var user = await UserProvisioning.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 });
|
||||||
|
})
|
||||||
|
.WithSummary("Create routine run")
|
||||||
|
.WithDescription("Records a routine run with exercise entries.");
|
||||||
|
|
||||||
|
return group;
|
||||||
|
}
|
||||||
|
}
|
||||||
40
src/ASTRAIN.Api/Endpoints/UserEndpoints.cs
Normal file
40
src/ASTRAIN.Api/Endpoints/UserEndpoints.cs
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
using ASTRAIN.Api.Data;
|
||||||
|
using ASTRAIN.Api.Services;
|
||||||
|
using ASTRAIN.Shared.Responses;
|
||||||
|
using Microsoft.AspNetCore.Builder;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.AspNetCore.Routing;
|
||||||
|
|
||||||
|
namespace ASTRAIN.Api.Endpoints;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Provides user-related endpoints.
|
||||||
|
/// </summary>
|
||||||
|
internal static class UserEndpoints
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Registers user routes under the provided route group.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="group">The API route group.</param>
|
||||||
|
/// <returns>The same route group for chaining.</returns>
|
||||||
|
public static RouteGroupBuilder MapUserEndpoints(this RouteGroupBuilder group)
|
||||||
|
{
|
||||||
|
group.MapGet("/users/ensure", async (string? userId, AppDbContext db) =>
|
||||||
|
{
|
||||||
|
var user = await UserProvisioning.EnsureUserAsync(db, userId);
|
||||||
|
return Results.Ok(new EnsureUserResponse(user.Id));
|
||||||
|
})
|
||||||
|
.WithSummary("Ensure user")
|
||||||
|
.WithDescription("Ensures a user exists and returns the user id.");
|
||||||
|
|
||||||
|
group.MapGet("/users/{userId}", async (string userId, AppDbContext db) =>
|
||||||
|
{
|
||||||
|
var user = await UserProvisioning.EnsureUserAsync(db, userId);
|
||||||
|
return Results.Ok(new EnsureUserResponse(user.Id));
|
||||||
|
})
|
||||||
|
.WithSummary("Get user")
|
||||||
|
.WithDescription("Returns the user id if it exists, or creates a new user.");
|
||||||
|
|
||||||
|
return group;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,10 +1,5 @@
|
|||||||
using System.Text.RegularExpressions;
|
|
||||||
using ASTRAIN.Api.Data;
|
using ASTRAIN.Api.Data;
|
||||||
using ASTRAIN.Api.Services;
|
using ASTRAIN.Api.Endpoints;
|
||||||
using ASTRAIN.Shared.Dtos;
|
|
||||||
using ASTRAIN.Shared.Models;
|
|
||||||
using ASTRAIN.Shared.Requests;
|
|
||||||
using ASTRAIN.Shared.Responses;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
@@ -14,9 +9,13 @@ builder.Services.AddCors(options =>
|
|||||||
{
|
{
|
||||||
options.AddDefaultPolicy(policy =>
|
options.AddDefaultPolicy(policy =>
|
||||||
{
|
{
|
||||||
policy.WithOrigins("http://localhost:5014", "https://localhost:7252")
|
policy.AllowAnyOrigin()
|
||||||
.AllowAnyHeader()
|
.AllowAnyHeader()
|
||||||
.AllowAnyMethod();
|
.AllowAnyMethod();
|
||||||
|
|
||||||
|
// policy.WithOrigins("http://localhost:5016", "http://:5016", "https://localhost:7252")
|
||||||
|
// .AllowAnyHeader()
|
||||||
|
// .AllowAnyMethod();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
builder.Services.AddDbContext<AppDbContext>(options =>
|
builder.Services.AddDbContext<AppDbContext>(options =>
|
||||||
@@ -33,7 +32,7 @@ if (app.Environment.IsDevelopment())
|
|||||||
app.MapOpenApi();
|
app.MapOpenApi();
|
||||||
}
|
}
|
||||||
|
|
||||||
app.UseHttpsRedirection();
|
// app.UseHttpsRedirection();
|
||||||
app.UseCors();
|
app.UseCors();
|
||||||
app.UseDefaultFiles();
|
app.UseDefaultFiles();
|
||||||
app.UseStaticFiles();
|
app.UseStaticFiles();
|
||||||
@@ -46,289 +45,12 @@ using (var scope = app.Services.CreateScope())
|
|||||||
|
|
||||||
var api = app.MapGroup("/api");
|
var api = app.MapGroup("/api");
|
||||||
|
|
||||||
api.MapGet("/health", () => Results.Ok(new { status = "ok" }));
|
api.MapHealthEndpoints();
|
||||||
|
api.MapUserEndpoints();
|
||||||
api.MapGet("/users/ensure", async (string? userId, AppDbContext db) =>
|
api.MapExerciseEndpoints();
|
||||||
{
|
api.MapRoutineEndpoints();
|
||||||
var user = await EnsureUserAsync(db, userId);
|
api.MapRoutineRunEndpoints();
|
||||||
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.MapFallbackToFile("index.html");
|
||||||
|
|
||||||
app.Run();
|
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}$");
|
|
||||||
}
|
|
||||||
|
|||||||
60
src/ASTRAIN.Api/Services/UserProvisioning.cs
Normal file
60
src/ASTRAIN.Api/Services/UserProvisioning.cs
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using ASTRAIN.Api.Data;
|
||||||
|
using ASTRAIN.Shared.Models;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace ASTRAIN.Api.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Provides helper methods for ensuring and validating application users.
|
||||||
|
/// </summary>
|
||||||
|
internal static class UserProvisioning
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Ensures a user exists in the database and returns the user record.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="db">The application database context.</param>
|
||||||
|
/// <param name="userId">Optional user id to validate or create.</param>
|
||||||
|
/// <returns>The existing or newly created user.</returns>
|
||||||
|
public 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Determines whether a user id matches the expected format.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="userId">The user id to validate.</param>
|
||||||
|
/// <returns><c>true</c> if the id is valid; otherwise <c>false</c>.</returns>
|
||||||
|
public static bool IsValidUserId(string userId)
|
||||||
|
{
|
||||||
|
return Regex.IsMatch(userId, "^[A-Za-z0-9]{8}$");
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user