Skip to content

Commit

Permalink
Update to .NET 9 and move to Minimal API
Browse files Browse the repository at this point in the history
  • Loading branch information
NixFey committed Jun 18, 2024
1 parent a2a4c02 commit 938d72e
Show file tree
Hide file tree
Showing 20 changed files with 526 additions and 130 deletions.
6 changes: 4 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base
# TODO: Move these images back to stable once .NET 9 is released

FROM mcr.microsoft.com/dotnet/nightly/aspnet:9.0-preview AS base
USER $APP_UID
WORKDIR /app
EXPOSE 8080
EXPOSE 8081

FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
FROM mcr.microsoft.com/dotnet/nightly/sdk:9.0-preview AS build
ARG BUILD_CONFIGURATION=Release
WORKDIR /src
COPY ["FiMAdminApi/FiMAdminApi.csproj", "FiMAdminApi/"]
Expand Down
6 changes: 5 additions & 1 deletion FiMAdminApi.Data/DataContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,9 @@ namespace FiMAdminApi.Data;

public class DataContext(DbContextOptions<DataContext> options) : DbContext(options)
{
public DbSet<Profile> Profiles { get; set; }
public DbSet<Profile> Profiles { get; init; }
public DbSet<Level> Levels { get; init; }
public DbSet<Season> Seasons { get; init; }
public DbSet<Event> Events { get; init; }
public DbSet<TruckRoute> TruckRoutes { get; init; }
}
8 changes: 8 additions & 0 deletions FiMAdminApi.Data/Enums/DataSources.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace FiMAdminApi.Data.Enums;

public enum DataSources
{
FrcEvents,
BlueAlliance,
OrangeAlliance
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
namespace FiMAdminApi.Data.Models;
// ReSharper disable InconsistentNaming
namespace FiMAdminApi.Data.Enums;

public enum GlobalRole
{
Expand Down
24 changes: 24 additions & 0 deletions FiMAdminApi.Data/Models/Event.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
using System.ComponentModel;

namespace FiMAdminApi.Data.Models;

public class Event
{
public Guid Id { get; set; }
public required int SeasonId { get; set; }
public required string Key { get; set; }
public string? Code { get; set; }
public required string Name { get; set; }
public string? SyncSource { get; set; }
public required bool IsOfficial { get; set; }
public int? TruckRouteId { get; set; }
public TruckRoute? TruckRoute { get; set; }
public required DateTimeOffset StartTime { get; set; }
public required DateTimeOffset EndTime { get; set; }
public DateTimeOffset? SyncAsOf { get; set; }
public required string Status { get; set; } = "NotStarted";

// Relations
[Description("Note: This object may not be populated in some endpoints.")]
public Season? Season { get; set; }
}
11 changes: 5 additions & 6 deletions FiMAdminApi.Data/Models/Level.cs
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
using Supabase.Postgrest.Attributes;
using Supabase.Postgrest.Models;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace FiMAdminApi.Data.Models;

[Table("levels")]
public class Level : BaseModel
public class Level
{
[PrimaryKey("id")]
[Key]
public int Id { get; set; }
[Column("name")]
public string Name { get; set; }
public required string Name { get; set; }
}
14 changes: 14 additions & 0 deletions FiMAdminApi.Data/Models/Season.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
using System.ComponentModel.DataAnnotations;

namespace FiMAdminApi.Data.Models;

public class Season
{
[Key]
public int Id { get; set; }
public required int LevelId { get; set; }
public Level? Level { get; set; }
public required string Name { get; set; }
public required DateTime StartTime { get; set; }
public required DateTime EndTime { get; set; }
}
6 changes: 6 additions & 0 deletions FiMAdminApi.Data/Models/TruckRoute.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace FiMAdminApi.Data.Models;

public class TruckRoute
{

}
2 changes: 2 additions & 0 deletions FiMAdminApi.Data/Models/User.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
using FiMAdminApi.Data.Enums;

namespace FiMAdminApi.Data.Models;

public class User
Expand Down
10 changes: 10 additions & 0 deletions FiMAdminApi/Clients/ClientsStartupExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
namespace FiMAdminApi.Clients;

public static class ClientsStartupExtensions
{
public static void AddClients(this IServiceCollection services)
{
services.AddHttpClient("FrcEvents");
services.AddKeyedScoped<IDataClient, FrcEventsDataClient>("FrcEvents");
}
}
33 changes: 33 additions & 0 deletions FiMAdminApi/Clients/Endpoints/EventsCreateEndpoints.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
using Asp.Versioning.Builder;
using FiMAdminApi.Data.Enums;
using FiMAdminApi.Services;
using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.AspNetCore.Mvc;

namespace FiMAdminApi.Clients.Endpoints;

public static class EventsCreateEndpoints
{
public static WebApplication RegisterEventsCreateEndpoints(this WebApplication app, ApiVersionSet vs)
{
var eventsCreateGroup = app.MapGroup("/api/v{apiVersion:apiVersion}/users")
.WithApiVersionSet(vs).HasApiVersion(1).WithTags("Events - Create")
.RequireAuthorization(nameof(GlobalRole.Events_Create));

eventsCreateGroup.MapPost("sync-source", SyncSource)
.WithSummary("Create from Sync Source")
.WithDescription(
"Will return OK if operation is fully successful, or BadRequest if it contains any errors");

return app;
}

private static async Task<Results<Ok<UpsertEventsService.UpsertEventsResponse>, BadRequest<UpsertEventsService.UpsertEventsResponse>>> SyncSource(
[FromBody] UpsertEventsService.UpsertFromDataSourceRequest request,
[FromServices] UpsertEventsService service)
{
var resp = await service.UpsertFromDataSource(request);

return resp.Errors.Count == 0 ? TypedResults.Ok(resp) : TypedResults.BadRequest(resp);
}
}
Original file line number Diff line number Diff line change
@@ -1,31 +1,43 @@
using Asp.Versioning;
using System.ComponentModel;
using Asp.Versioning.Builder;
using FiMAdminApi.Data;
using FiMAdminApi.Data.Enums;
using FiMAdminApi.Data.Models;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Supabase.Gotrue;
using Supabase.Gotrue.Exceptions;
using Supabase.Gotrue.Interfaces;
using User = Supabase.Gotrue.User;

namespace FiMAdminApi.Controllers;
namespace FiMAdminApi.Clients.Endpoints;

[Authorize(nameof(GlobalRole.Superuser))]
[ApiVersion("1.0")]
[Route("/api/v{apiVersion:apiVersion}/users")]
public class UsersController(
IGotrueAdminClient<User> adminClient,
DataContext dbContext
) : BaseController
public static class UsersEndpoints
{
[HttpGet("")]
[ProducesResponseType(typeof(List<Data.Models.User>), StatusCodes.Status200OK)]
public async Task<ActionResult> GetUsers([FromQuery] string? searchTerm = null)
public static WebApplication RegisterUsersEndpoints(this WebApplication app, ApiVersionSet vs)
{
var usersGroup = app.MapGroup("/api/v{apiVersion:apiVersion}/users")
.WithApiVersionSet(vs).HasApiVersion(1).WithTags("Users")
.RequireAuthorization(nameof(GlobalRole.Superuser));

usersGroup.MapGet("", SearchUsers).WithSummary("Search Users");
usersGroup.MapGet("{id:guid:required}", GetUser).WithSummary("Get User by ID");
usersGroup.MapPut("{id:guid:required}", UpdateUser).WithSummary("Update User");

return app;
}

private static async Task<Ok<Data.Models.User[]>> SearchUsers(
[FromQuery] [Description("A free-text search to filter the returned users")]
string? searchTerm,
[FromServices] IGotrueAdminClient<User> adminClient,
[FromServices] DataContext dbContext)
{
var users = await adminClient.ListUsers(searchTerm, perPage: 20);
if (users is null) return Ok(Array.Empty<Data.Models.User>());
if (users is null) return TypedResults.Ok(Array.Empty<Data.Models.User>());

var selectedUsers = users.Users.Select(u =>
{
Expand All @@ -51,8 +63,8 @@ public async Task<ActionResult> GetUsers([FromQuery] string? searchTerm = null)

var profiles = await dbContext.Profiles.Where(p => selectedUsers.Select(u => u.Id).Contains(p.Id))
.ToDictionaryAsync(p => p.Id);
return Ok(selectedUsers.Select(user =>

return TypedResults.Ok(selectedUsers.Select(user =>
{
if (user.Id is not null && profiles.TryGetValue(user.Id.Value, out var profile) &&
!string.IsNullOrWhiteSpace(profile.Name))
Expand All @@ -65,22 +77,30 @@ public async Task<ActionResult> GetUsers([FromQuery] string? searchTerm = null)
}

return user;
}).ToList());
}).ToArray());
}

/// <summary>
/// TODO: This endpoint and the list endpoint should get cleaned up as they share a lot of (ugly) code
/// </summary>
/// <param name="id">The user's ID</param>
/// <returns>The user, or not found</returns>
[HttpGet("{id}")]
[ProducesResponseType(typeof(Data.Models.User), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult> GetUser(string id)
private static async Task<Results<Ok<Data.Models.User>, NotFound>> GetUser(
[FromRoute] [Description("The user's ID")]
Guid id,
[FromServices] IGotrueAdminClient<User> adminClient,
[FromServices] DataContext dbContext)
{
var user = await adminClient.GetUserById(id);
if (user is null) return NotFound();

User user;
try
{
user = await adminClient.GetUserById(id.ToString()) ?? throw new InvalidOperationException();
}
catch (InvalidOperationException)
{
return TypedResults.NotFound();
}
catch (GotrueException ex)
{
if (ex.StatusCode != StatusCodes.Status404NotFound) throw;
return TypedResults.NotFound();
}

IEnumerable<GlobalRole> roles = Array.Empty<GlobalRole>();
user.AppMetadata.TryGetValue("globalRoles", out var jsonRoles);
if (jsonRoles is JArray rolesArray)
Expand All @@ -99,9 +119,9 @@ public async Task<ActionResult> GetUser(string id)
Name = null,
GlobalRoles = roles.ToList()
};

var profile = await dbContext.Profiles.SingleOrDefaultAsync(p => p.Id == userModel.Id);

if (user.Id is not null && profile is not null &&
!string.IsNullOrWhiteSpace(profile.Name))
{
Expand All @@ -112,11 +132,14 @@ public async Task<ActionResult> GetUser(string id)
userModel.Name = user.Email;
}

return Ok(userModel);
return TypedResults.Ok(userModel);
}

[HttpPut("{id:guid}")]
public async Task<ActionResult> UpdateUser(Guid id, [FromBody] UpdateRolesRequest request)
private static async Task<Ok> UpdateUser(
[FromRoute] Guid id,
[FromBody] UpdateRolesRequest request,
[FromServices] DataContext dbContext,
[FromServices] IGotrueAdminClient<User> adminClient)
{
var update = new FixedAdminUserAttributes();
if (request.NewRoles is not null)
Expand All @@ -129,6 +152,7 @@ public async Task<ActionResult> UpdateUser(Guid id, [FromBody] UpdateRolesReques
// This handles a special case, we want superusers to have access to literally everything
update.Role = request.NewRoles.Contains(GlobalRole.Superuser) ? "service_role" : "authenticated";
}

if (request.Name is not null)
{
update.UserMetadata = new Dictionary<string, object>
Expand All @@ -139,21 +163,21 @@ public async Task<ActionResult> UpdateUser(Guid id, [FromBody] UpdateRolesReques
var profile = await dbContext.Profiles.FindAsync(id);
if (profile is null)
{
profile = new Profile()
profile = new Profile
{
Id = id
};
await dbContext.Profiles.AddAsync(profile);
}

profile.Name = request.Name;

await dbContext.SaveChangesAsync();
}

await adminClient.UpdateUserById(id.ToString(), update);

return Ok();
return TypedResults.Ok();
}

public class UpdateRolesRequest
Expand All @@ -167,7 +191,6 @@ public class UpdateRolesRequest
/// </summary>
private class FixedAdminUserAttributes : AdminUserAttributes
{
[JsonProperty("role")]
public string Role { get; set; }
[JsonProperty("role")] public string? Role { get; set; }
}
}
Loading

0 comments on commit 938d72e

Please sign in to comment.