Skip to content

Commit

Permalink
A super rough initial implementation of the admin API. Supports basic…
Browse files Browse the repository at this point in the history
… user management
  • Loading branch information
evanlihou committed May 30, 2024
0 parents commit a08e62f
Show file tree
Hide file tree
Showing 23 changed files with 643 additions and 0 deletions.
25 changes: 25 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
**/.dockerignore
**/.env
**/.git
**/.gitignore
**/.project
**/.settings
**/.toolstarget
**/.vs
**/.vscode
**/.idea
**/*.*proj.user
**/*.dbmdl
**/*.jfm
**/azds.yaml
**/bin
**/charts
**/docker-compose*
**/Dockerfile*
**/node_modules
**/npm-debug.log
**/obj
**/secrets.dev.yaml
**/values.dev.yaml
LICENSE
README.md
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
bin/
obj/
/packages/
riderModule.iml
/_ReSharper.Caches/
13 changes: 13 additions & 0 deletions .idea/.idea.FiMAdminApi/.idea/.gitignore

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions .idea/.idea.FiMAdminApi/.idea/encodings.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions .idea/.idea.FiMAdminApi/.idea/indexLayout.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions .idea/.idea.FiMAdminApi/.idea/vcs.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 9 additions & 0 deletions FiMAdminApi.Data/DataContext.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using FiMAdminApi.Data.Models;
using Microsoft.EntityFrameworkCore;

namespace FiMAdminApi.Data;

public class DataContext(DbContextOptions<DataContext> options) : DbContext(options)
{
public DbSet<Profile> Profiles { get; set; }
}
16 changes: 16 additions & 0 deletions FiMAdminApi.Data/FiMAdminApi.Data.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="EFCore.NamingConventions" Version="8.0.3" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.5" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.4" />
<PackageReference Include="Supabase.Postgrest" Version="4.0.1" />
</ItemGroup>

</Project>
12 changes: 12 additions & 0 deletions FiMAdminApi.Data/Models/GlobalRole.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
namespace FiMAdminApi.Data.Models;

public enum GlobalRole
{
Superuser,
Events_Create,
Events_Manage,
Events_Note,
Events_View,
Equipment_Manage,
Equipment_Note
}
13 changes: 13 additions & 0 deletions FiMAdminApi.Data/Models/Level.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using Supabase.Postgrest.Attributes;
using Supabase.Postgrest.Models;

namespace FiMAdminApi.Data.Models;

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

namespace FiMAdminApi.Data.Models;

[Table("profiles")]
public class Profile
{
public Guid Id { get; set; }
public string? Name { get; set; }
}
9 changes: 9 additions & 0 deletions FiMAdminApi.Data/Models/User.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
namespace FiMAdminApi.Data.Models;

public class User
{
public Guid? Id { get; set; }
public string? Email { get; set; }
public string? Name { get; set; }
public List<GlobalRole>? GlobalRoles { get; set; }
}
22 changes: 22 additions & 0 deletions FiMAdminApi.sln
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@

Microsoft Visual Studio Solution File, Format Version 12.00
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FiMAdminApi", "FiMAdminApi\FiMAdminApi.csproj", "{DA4DB564-68AF-4B2C-B37F-469C44DEDFBD}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FiMAdminApi.Data", "FiMAdminApi.Data\FiMAdminApi.Data.csproj", "{6A85222A-42ED-46DA-93F8-EEEFA72C7A8E}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{DA4DB564-68AF-4B2C-B37F-469C44DEDFBD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{DA4DB564-68AF-4B2C-B37F-469C44DEDFBD}.Debug|Any CPU.Build.0 = Debug|Any CPU
{DA4DB564-68AF-4B2C-B37F-469C44DEDFBD}.Release|Any CPU.ActiveCfg = Release|Any CPU
{DA4DB564-68AF-4B2C-B37F-469C44DEDFBD}.Release|Any CPU.Build.0 = Release|Any CPU
{6A85222A-42ED-46DA-93F8-EEEFA72C7A8E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{6A85222A-42ED-46DA-93F8-EEEFA72C7A8E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6A85222A-42ED-46DA-93F8-EEEFA72C7A8E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6A85222A-42ED-46DA-93F8-EEEFA72C7A8E}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal
9 changes: 9 additions & 0 deletions FiMAdminApi/Controllers/BaseController.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using Microsoft.AspNetCore.Mvc;

namespace FiMAdminApi.Controllers;

[ApiController]
public class BaseController : Controller
{

}
156 changes: 156 additions & 0 deletions FiMAdminApi/Controllers/UsersController.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
using Asp.Versioning;
using FiMAdminApi.Data;
using FiMAdminApi.Data.Models;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Supabase.Gotrue;
using Supabase.Gotrue.Interfaces;
using User = Supabase.Gotrue.User;

namespace FiMAdminApi.Controllers;

[Authorize(nameof(GlobalRole.Superuser))]
[ApiVersion("1.0")]
[Route("/api/v{apiVersion:apiVersion}/users")]
public class UsersController(
IGotrueAdminClient<User> adminClient,
DataContext dbContext
) : BaseController
{
[HttpGet("")]
[ProducesResponseType(typeof(List<Data.Models.User>), StatusCodes.Status200OK)]
public async Task<ActionResult> GetUsers([FromQuery] string? searchTerm = null)
{
var users = await adminClient.ListUsers(searchTerm, perPage: 20);
if (users is null) return Ok(Array.Empty<Data.Models.User>());

var selectedUsers = users.Users.Select(u =>
{
IEnumerable<GlobalRole> roles = Array.Empty<GlobalRole>();
u.AppMetadata.TryGetValue("globalRoles", out var jsonRoles);
if (jsonRoles is JArray rolesArray)
{
roles = rolesArray.Select<JToken, GlobalRole?>(t =>
{
var value = t.Value<string>();
return Enum.TryParse<GlobalRole>(value, true, out var role) ? role : null;
}).Where(r => r is not null).Select(r => r!.Value);
}
return new Data.Models.User
{
Id = Guid.Parse(u.Id!),
Email = u.Email,
Name = null,
GlobalRoles = roles.ToList()
};
});

var profiles = await dbContext.Profiles.Where(p => selectedUsers.Select(u => u.Id).Contains(p.Id))
.ToDictionaryAsync(p => p.Id);
foreach (var user in selectedUsers)
{
if (user.Id is not null && profiles.TryGetValue(user.Id.Value, out var profile) &&
!string.IsNullOrWhiteSpace(profile.Name))
{
user.Name = profile.Name;
}
else
{
user.Name = user.Email;
}
}

return Ok(selectedUsers);
}

/// <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)
{
var user = await adminClient.GetUserById(id);
if (user is null) return NotFound();

IEnumerable<GlobalRole> roles = Array.Empty<GlobalRole>();
user.AppMetadata.TryGetValue("globalRoles", out var jsonRoles);
if (jsonRoles is JArray rolesArray)
{
roles = rolesArray.Select<JToken, GlobalRole?>(t =>
{
var value = t.Value<string>();
return Enum.TryParse<GlobalRole>(value, true, out var role) ? role : null;
}).Where(r => r is not null).Select(r => r!.Value);
}

var userModel = new Data.Models.User
{
Id = Guid.Parse(user.Id!),
Email = user.Email,
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))
{
userModel.Name = profile.Name;
}
else
{
userModel.Name = user.Email;
}

return Ok(userModel);
}

[HttpPut("{id:guid}")]
public async Task<ActionResult> UpdateUser(string id, [FromBody] UpdateRolesRequest request)
{
var update = new FixedAdminUserAttributes();
if (request.NewRoles is not null)
{
update.AppMetadata = new Dictionary<string, object>
{
{ "globalRoles", request.NewRoles.Select(s => s.ToString()) }
};

// 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>
{
{ "name", request.Name }
};

await adminClient.UpdateUserById(id, update);

return Ok();
}

public class UpdateRolesRequest
{
public string? Name { get; set; }
public IEnumerable<GlobalRole>? NewRoles { get; set; }
}

/// <summary>
/// For some reason certain things aren't in the SDK, like the ability to override the user's postgres role
/// </summary>
private class FixedAdminUserAttributes : AdminUserAttributes
{
[JsonProperty("role")]
public string Role { get; set; }
}
}
23 changes: 23 additions & 0 deletions FiMAdminApi/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base
USER $APP_UID
WORKDIR /app
EXPOSE 8080
EXPOSE 8081

FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
ARG BUILD_CONFIGURATION=Release
WORKDIR /src
COPY ["FiMAdminApi/FiMAdminApi.csproj", "FiMAdminApi/"]
RUN dotnet restore "FiMAdminApi/FiMAdminApi.csproj"
COPY . .
WORKDIR "/src/FiMAdminApi"
RUN dotnet build "FiMAdminApi.csproj" -c $BUILD_CONFIGURATION -o /app/build

FROM build AS publish
ARG BUILD_CONFIGURATION=Release
RUN dotnet publish "FiMAdminApi.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false

FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "FiMAdminApi.dll"]
29 changes: 29 additions & 0 deletions FiMAdminApi/FiMAdminApi.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
<UserSecretsId>b6263931-3814-4b25-b0c5-089ac32ee89a</UserSecretsId>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Asp.Versioning.Mvc.ApiExplorer" Version="8.1.0" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.5" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="8.0.2" />
<PackageReference Include="Supabase" Version="1.0.1" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0" />
</ItemGroup>

<ItemGroup>
<Content Include="..\.dockerignore">
<Link>.dockerignore</Link>
</Content>
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\FiMAdminApi.Data\FiMAdminApi.Data.csproj" />
</ItemGroup>

</Project>
Loading

0 comments on commit a08e62f

Please sign in to comment.