feat: scaffold backend and shared library
This commit is contained in:
21
.github/copilot-instructions.md
vendored
Normal file
21
.github/copilot-instructions.md
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
- [x] Verify that the copilot-instructions.md file in the .github directory is created.
|
||||
|
||||
- [x] Clarify Project Requirements
|
||||
|
||||
- [x] Scaffold the Project
|
||||
|
||||
- [x] Customize the Project
|
||||
|
||||
- [x] Install Required Extensions
|
||||
|
||||
- [x] Compile the Project
|
||||
|
||||
- [x] Create and Run Task
|
||||
|
||||
- [ ] Launch the Project
|
||||
|
||||
- [x] Ensure Documentation is Complete
|
||||
|
||||
- Work through each checklist item systematically.
|
||||
- Keep communication concise and focused.
|
||||
- Follow development best practices.
|
||||
16
.gitignore
vendored
Normal file
16
.gitignore
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
**/bin/
|
||||
**/obj/
|
||||
**/out/
|
||||
**/.vs/
|
||||
**/.vscode/
|
||||
!.vscode/
|
||||
!.vscode/launch.json
|
||||
*.user
|
||||
*.suo
|
||||
*.cache
|
||||
*.log
|
||||
.DS_Store
|
||||
src/ASTRAIN.Api/Data/*.db
|
||||
src/ASTRAIN.Api/Data/*.db-shm
|
||||
src/ASTRAIN.Api/Data/*.db-wal
|
||||
src/ASTRAIN.Api/ASTRAIN.Api.http
|
||||
50
.vscode/launch.json
vendored
Normal file
50
.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,50 @@
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "ASTRAIN.Api",
|
||||
"type": "coreclr",
|
||||
"request": "launch",
|
||||
"preLaunchTask": "build",
|
||||
"program": "${workspaceFolder}/src/ASTRAIN.Api/bin/Debug/net10.0/ASTRAIN.Api.dll",
|
||||
"args": [],
|
||||
"cwd": "${workspaceFolder}/src/ASTRAIN.Api",
|
||||
"stopAtEntry": false,
|
||||
"serverReadyAction": {
|
||||
"action": "openExternally",
|
||||
"pattern": "Now listening on: (https?://\\S+)",
|
||||
"uriFormat": "%s"
|
||||
},
|
||||
"env": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "ASTRAIN.Client",
|
||||
"type": "coreclr",
|
||||
"request": "launch",
|
||||
"preLaunchTask": "build",
|
||||
"program": "${workspaceFolder}/src/ASTRAIN.Client/bin/Debug/net10.0/ASTRAIN.Client.dll",
|
||||
"args": [],
|
||||
"cwd": "${workspaceFolder}/src/ASTRAIN.Client",
|
||||
"stopAtEntry": false,
|
||||
"serverReadyAction": {
|
||||
"action": "openExternally",
|
||||
"pattern": "Now listening on: (https?://\\S+)",
|
||||
"uriFormat": "%s"
|
||||
},
|
||||
"env": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
}
|
||||
],
|
||||
"compounds": [
|
||||
{
|
||||
"name": "ASTRAIN.Full",
|
||||
"configurations": [
|
||||
"ASTRAIN.Api",
|
||||
"ASTRAIN.Client"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
7
ASTRAIN.slnx
Normal file
7
ASTRAIN.slnx
Normal file
@@ -0,0 +1,7 @@
|
||||
<Solution>
|
||||
<Folder Name="/src/">
|
||||
<Project Path="src/ASTRAIN.Api/ASTRAIN.Api.csproj" />
|
||||
<Project Path="src/ASTRAIN.Client/ASTRAIN.Client.csproj" />
|
||||
<Project Path="src/ASTRAIN.Shared/ASTRAIN.Shared.csproj" />
|
||||
</Folder>
|
||||
</Solution>
|
||||
12
docker-compose.yml
Normal file
12
docker-compose.yml
Normal file
@@ -0,0 +1,12 @@
|
||||
version: "3.9"
|
||||
services:
|
||||
astrain:
|
||||
build:
|
||||
context: ./src
|
||||
dockerfile: ASTRAIN.Api/Dockerfile
|
||||
ports:
|
||||
- "8080:8080"
|
||||
volumes:
|
||||
- astrain-data:/app/Data
|
||||
volumes:
|
||||
astrain-data:
|
||||
21
src/ASTRAIN.Api/ASTRAIN.Api.csproj
Normal file
21
src/ASTRAIN.Api/ASTRAIN.Api.csproj
Normal 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.OpenApi" Version="10.0.2" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.2" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.2">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\ASTRAIN.Shared\ASTRAIN.Shared.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
63
src/ASTRAIN.Api/Data/AppDbContext.cs
Normal file
63
src/ASTRAIN.Api/Data/AppDbContext.cs
Normal file
@@ -0,0 +1,63 @@
|
||||
using ASTRAIN.Shared;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace ASTRAIN.Api.Data;
|
||||
|
||||
public class AppDbContext : DbContext
|
||||
{
|
||||
public AppDbContext(DbContextOptions<AppDbContext> options) : base(options)
|
||||
{
|
||||
}
|
||||
|
||||
public DbSet<User> Users => Set<User>();
|
||||
public DbSet<Exercise> Exercises => Set<Exercise>();
|
||||
public DbSet<Routine> Routines => Set<Routine>();
|
||||
public DbSet<RoutineExercise> RoutineExercises => Set<RoutineExercise>();
|
||||
public DbSet<RoutineRun> RoutineRuns => Set<RoutineRun>();
|
||||
public DbSet<RoutineRunEntry> RoutineRunEntries => Set<RoutineRunEntry>();
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
modelBuilder.Entity<User>()
|
||||
.HasMany(u => u.Exercises)
|
||||
.WithOne(e => e.User)
|
||||
.HasForeignKey(e => e.UserId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
modelBuilder.Entity<User>()
|
||||
.HasMany(u => u.Routines)
|
||||
.WithOne(r => r.User)
|
||||
.HasForeignKey(r => r.UserId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
modelBuilder.Entity<RoutineExercise>()
|
||||
.HasIndex(re => new { re.RoutineId, re.Order })
|
||||
.IsUnique();
|
||||
|
||||
modelBuilder.Entity<RoutineExercise>()
|
||||
.HasOne(re => re.Routine)
|
||||
.WithMany(r => r.Exercises)
|
||||
.HasForeignKey(re => re.RoutineId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
modelBuilder.Entity<RoutineExercise>()
|
||||
.HasOne(re => re.Exercise)
|
||||
.WithMany(e => e.RoutineExercises)
|
||||
.HasForeignKey(re => re.ExerciseId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
modelBuilder.Entity<RoutineRun>()
|
||||
.HasOne(rr => rr.Routine)
|
||||
.WithMany(r => r.Runs)
|
||||
.HasForeignKey(rr => rr.RoutineId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
modelBuilder.Entity<RoutineRunEntry>()
|
||||
.HasOne(rre => rre.RoutineRun)
|
||||
.WithMany(rr => rr.Entries)
|
||||
.HasForeignKey(rre => rre.RoutineRunId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
base.OnModelCreating(modelBuilder);
|
||||
}
|
||||
}
|
||||
29
src/ASTRAIN.Api/Dockerfile
Normal file
29
src/ASTRAIN.Api/Dockerfile
Normal file
@@ -0,0 +1,29 @@
|
||||
# Build API
|
||||
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
|
||||
WORKDIR /src
|
||||
COPY ASTRAIN.Api/ASTRAIN.Api.csproj ASTRAIN.Api/
|
||||
COPY ASTRAIN.Shared/ASTRAIN.Shared.csproj ASTRAIN.Shared/
|
||||
RUN dotnet restore ASTRAIN.Api/ASTRAIN.Api.csproj
|
||||
COPY ASTRAIN.Api/ ASTRAIN.Api/
|
||||
COPY ASTRAIN.Shared/ ASTRAIN.Shared/
|
||||
RUN dotnet publish ASTRAIN.Api/ASTRAIN.Api.csproj -c Release -o /app/publish
|
||||
|
||||
# Build Client
|
||||
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build-client
|
||||
WORKDIR /src
|
||||
COPY ASTRAIN.Client/ASTRAIN.Client.csproj ASTRAIN.Client/
|
||||
COPY ASTRAIN.Shared/ASTRAIN.Shared.csproj ASTRAIN.Shared/
|
||||
RUN dotnet restore ASTRAIN.Client/ASTRAIN.Client.csproj
|
||||
COPY ASTRAIN.Client/ ASTRAIN.Client/
|
||||
COPY ASTRAIN.Shared/ ASTRAIN.Shared/
|
||||
RUN dotnet publish ASTRAIN.Client/ASTRAIN.Client.csproj -c Release -o /app/client
|
||||
|
||||
# Final runtime
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS runtime
|
||||
WORKDIR /app
|
||||
RUN mkdir -p /app/Data
|
||||
COPY --from=build /app/publish/ ./
|
||||
COPY --from=build-client /app/client/wwwroot ./wwwroot
|
||||
ENV ASPNETCORE_URLS=http://+:8080
|
||||
EXPOSE 8080
|
||||
ENTRYPOINT ["dotnet", "ASTRAIN.Api.dll"]
|
||||
331
src/ASTRAIN.Api/Program.cs
Normal file
331
src/ASTRAIN.Api/Program.cs
Normal 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}$");
|
||||
}
|
||||
23
src/ASTRAIN.Api/Properties/launchSettings.json
Normal file
23
src/ASTRAIN.Api/Properties/launchSettings.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/launchsettings.json",
|
||||
"profiles": {
|
||||
"http": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": false,
|
||||
"applicationUrl": "http://localhost:5055",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
},
|
||||
"https": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": false,
|
||||
"applicationUrl": "https://localhost:7049;http://localhost:5055",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
22
src/ASTRAIN.Api/Services/UserKeyGenerator.cs
Normal file
22
src/ASTRAIN.Api/Services/UserKeyGenerator.cs
Normal file
@@ -0,0 +1,22 @@
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
|
||||
namespace ASTRAIN.Api.Services;
|
||||
|
||||
public static class UserKeyGenerator
|
||||
{
|
||||
private const string Alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||
|
||||
public static string Generate(int length = 8)
|
||||
{
|
||||
var buffer = new byte[length];
|
||||
RandomNumberGenerator.Fill(buffer);
|
||||
var chars = new char[length];
|
||||
for (var i = 0; i < length; i++)
|
||||
{
|
||||
chars[i] = Alphabet[buffer[i] % Alphabet.Length];
|
||||
}
|
||||
|
||||
return new string(chars);
|
||||
}
|
||||
}
|
||||
8
src/ASTRAIN.Api/appsettings.Development.json
Normal file
8
src/ASTRAIN.Api/appsettings.Development.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
}
|
||||
}
|
||||
12
src/ASTRAIN.Api/appsettings.json
Normal file
12
src/ASTRAIN.Api/appsettings.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"ConnectionStrings": {
|
||||
"Default": "Data Source=Data/astrain.db"
|
||||
},
|
||||
"AllowedHosts": "*"
|
||||
}
|
||||
9
src/ASTRAIN.Shared/ASTRAIN.Shared.csproj
Normal file
9
src/ASTRAIN.Shared/ASTRAIN.Shared.csproj
Normal file
@@ -0,0 +1,9 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
139
src/ASTRAIN.Shared/Class1.cs
Normal file
139
src/ASTRAIN.Shared/Class1.cs
Normal file
@@ -0,0 +1,139 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
|
||||
namespace ASTRAIN.Shared;
|
||||
|
||||
public class User
|
||||
{
|
||||
[Key]
|
||||
[MaxLength(8)]
|
||||
public string Id { get; set; } = string.Empty;
|
||||
|
||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||
|
||||
public List<Exercise> Exercises { get; set; } = new();
|
||||
public List<Routine> Routines { get; set; } = new();
|
||||
}
|
||||
|
||||
public class Exercise
|
||||
{
|
||||
public int Id { get; set; }
|
||||
|
||||
[Required]
|
||||
[MaxLength(120)]
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
[Required]
|
||||
[MaxLength(8)]
|
||||
public string UserId { get; set; } = string.Empty;
|
||||
|
||||
public User? User { get; set; }
|
||||
|
||||
public List<RoutineExercise> RoutineExercises { get; set; } = new();
|
||||
}
|
||||
|
||||
public class Routine
|
||||
{
|
||||
public int Id { get; set; }
|
||||
|
||||
[Required]
|
||||
[MaxLength(120)]
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
[Required]
|
||||
[MaxLength(8)]
|
||||
public string UserId { get; set; } = string.Empty;
|
||||
|
||||
public User? User { get; set; }
|
||||
|
||||
public List<RoutineExercise> Exercises { get; set; } = new();
|
||||
public List<RoutineRun> Runs { get; set; } = new();
|
||||
}
|
||||
|
||||
public class RoutineExercise
|
||||
{
|
||||
public int Id { get; set; }
|
||||
|
||||
[Required]
|
||||
public int RoutineId { get; set; }
|
||||
|
||||
public Routine? Routine { get; set; }
|
||||
|
||||
[Required]
|
||||
public int ExerciseId { get; set; }
|
||||
|
||||
public Exercise? Exercise { get; set; }
|
||||
|
||||
public int Order { get; set; }
|
||||
}
|
||||
|
||||
public class RoutineRun
|
||||
{
|
||||
public int Id { get; set; }
|
||||
|
||||
[Required]
|
||||
public int RoutineId { get; set; }
|
||||
|
||||
public Routine? Routine { get; set; }
|
||||
|
||||
[Required]
|
||||
[MaxLength(8)]
|
||||
public string UserId { get; set; } = string.Empty;
|
||||
|
||||
public DateTime PerformedAt { get; set; } = DateTime.UtcNow;
|
||||
|
||||
public List<RoutineRunEntry> Entries { get; set; } = new();
|
||||
}
|
||||
|
||||
public class RoutineRunEntry
|
||||
{
|
||||
public int Id { get; set; }
|
||||
|
||||
[Required]
|
||||
public int RoutineRunId { get; set; }
|
||||
|
||||
public RoutineRun? RoutineRun { get; set; }
|
||||
|
||||
[Required]
|
||||
public int ExerciseId { get; set; }
|
||||
|
||||
public Exercise? Exercise { get; set; }
|
||||
|
||||
public double Weight { get; set; }
|
||||
|
||||
public bool Completed { get; set; }
|
||||
}
|
||||
|
||||
public record EnsureUserResponse(string UserId);
|
||||
|
||||
public record ExerciseDto(int Id, string Name);
|
||||
|
||||
public record ExerciseUpsertRequest(string Name);
|
||||
|
||||
public record RoutineExerciseDto(int ExerciseId, string Name, int Order);
|
||||
|
||||
public record RoutineDto(int Id, string Name, List<RoutineExerciseDto> Exercises);
|
||||
|
||||
public record RoutineUpsertRequest(string Name, List<int> ExerciseIds);
|
||||
|
||||
public class RoutineRunEntryDto
|
||||
{
|
||||
public int ExerciseId { get; set; }
|
||||
public double Weight { get; set; }
|
||||
public bool Completed { get; set; }
|
||||
|
||||
public RoutineRunEntryDto()
|
||||
{
|
||||
}
|
||||
|
||||
public RoutineRunEntryDto(int exerciseId, double weight, bool completed)
|
||||
{
|
||||
ExerciseId = exerciseId;
|
||||
Weight = weight;
|
||||
Completed = completed;
|
||||
}
|
||||
}
|
||||
|
||||
public record RoutineRunRequest(List<RoutineRunEntryDto> Entries);
|
||||
|
||||
public record RoutineRunSummaryDto(DateTime PerformedAt, List<RoutineRunEntryDto> Entries);
|
||||
Reference in New Issue
Block a user