text:
<Project Sdk="Microsoft.NET.Sdk.Web">

    <PropertyGroup>
        <TargetFramework>net7.0</TargetFramework>
        <Nullable>enable</Nullable>
        <ImplicitUsings>enable</ImplicitUsings>
        <LangVersion>10</LangVersion>
        <RootNamespace>Subterfuge.Remake.Server</RootNamespace>
    </PropertyGroup>

    <ItemGroup>
        <PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="7.0.0" />
        <PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="7.0.2" />
        <PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="7.0.0" />
        <PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0" />
        <PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="6.25.1" />
        <PackageReference Include="Twilio" Version="6.2.3" />
    </ItemGroup>

    <ItemGroup>
      <ProjectReference Include="..\..\Core\Models\Models.csproj" />
      <ProjectReference Include="..\..\Core\SubterfugeCore\SubterfugeCore.csproj" />
      <ProjectReference Include="..\SubterfugeDatabaseProvider\SubterfugeDatabaseProvider.csproj" />
    </ItemGroup>

</Project>


text:
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Server.Kestrel.Core;
using Microsoft.OpenApi.Models;
using Newtonsoft.Json.Converters;
using Subterfuge.Remake.Server.Authentication;
using Subterfuge.Remake.Server.Database;
using Subterfuge.Remake.Server.Middleware;

var builder = WebApplication.CreateBuilder(args);

var env = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT");

var config = new ConfigurationBuilder()
    .AddJsonFile("appsettings.json", optional: false)
    .AddJsonFile($"appsettings.{env}.json", optional: true, reloadOnChange: true)
    .AddEnvironmentVariables()
    .Build();

builder.Services.AddControllers(options =>
{
    options.Filters.Add<ExceptionResponseMiddleware>();
});

builder.Services.PostConfigure<ApiBehaviorOptions>(options =>
{
    options.InvalidModelStateResponseFactory = ErrorResponseMapper.ValidationErrorFactory();
});

// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddControllers().AddNewtonsoftJson();

builder.Services.AddSwaggerGen(genOptions =>
    {
        genOptions.SwaggerDoc("v1", new OpenApiInfo { Title = "Subterfuge Community Edition API", Version = "v1" });

        // Include 'SecurityScheme' to use JWT Authentication
        var jwtSecurityScheme = new OpenApiSecurityScheme
        {
            BearerFormat = "JWT",
            Name = "JWT Authentication",
            In = ParameterLocation.Header,
            Type = SecuritySchemeType.Http,
            Scheme = JwtBearerDefaults.AuthenticationScheme,
            Description = "Put **_ONLY_** your JWT Bearer token on textbox below!",

            Reference = new OpenApiReference
            {
                Id = JwtBearerDefaults.AuthenticationScheme,
                Type = ReferenceType.SecurityScheme
            }
        };

        genOptions.AddSecurityDefinition(jwtSecurityScheme.Reference.Id, jwtSecurityScheme);

        genOptions.AddSecurityRequirement(new OpenApiSecurityRequirement
        {
            { jwtSecurityScheme, Array.Empty<string>() }
        });
    }
);

// Configure Logging
builder.Services.AddLogging(logging => logging.AddSimpleConsole(loggerOptions =>
{
    loggerOptions.SingleLine = true;
    loggerOptions.IncludeScopes = false;
    loggerOptions.TimestampFormat = "[yyyy-MM-dd HH:mm:ss]";
}));

builder.Services.Configure<KestrelServerOptions>(options =>
{
    options.AllowSynchronousIO = true;
});

// Enable JWT Authentication.
new JwtAuthenticationScheme(config).ConfigureAuthentication(builder.Services);

// Add Database as a singleton to Dependency Inject.
// MongoConfiguration mongoConfig = new MongoConfiguration(app.Configuration.GetSection("MongoDb"));
// MongoConnector mongo = new MongoConnector(mongoConfig, builder.Logging.);

builder.Services.AddCors(options =>
{
    options.AddPolicy("SubterfugePolicy", builder =>
    {
        builder.AllowAnyOrigin()
            .AllowAnyMethod()
            .AllowAnyHeader();
    });
});

builder.Services.AddSingleton<IMongoConfigurationProvider, MongoConfigProvider>();
builder.Services.AddSingleton<IDatabaseCollectionProvider, MongoConnector>();

var app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    app.UseDeveloperExceptionPage();
}

app.UseCors("SubterfugePolicy");

app.UseSwagger();
app.UseSwaggerUI();
app.UseHttpsRedirection();

// ORDER MATTERS HERE
app.UseAuthorization();
app.UseMiddleware<JwtMiddleware>();
app.UseMiddleware<BanMiddleware>();
app.MapControllers();
app.UseMiddleware<LoggingMiddleware>();

app.Run();

repo_name: Subterfuge-Revived/Remake-Core

repo_language: C#

mime_type: text/plain


commandName: Project

launchUrl: swagger

ASPNETCORE_ENVIRONMENT: Development

commandName: IISExpress

ENABLE_CORS: true

ASPNETCORE_ENVIRONMENT: Docker


text:
using System.Net;
using System.Text.RegularExpressions;
using MongoDB.Driver;
using MongoDB.Driver.Linq;
using Newtonsoft.Json;
using Subterfuge.Remake.Api.Network;
using Subterfuge.Remake.Server.Database;
using Subterfuge.Remake.Server.Database.Models;

namespace Subterfuge.Remake.Server.Middleware;

public class BanMiddleware
{
    private readonly RequestDelegate _next;
    private IDatabaseCollection<DbIpBan> _dbIpBans;
    private ILogger _logger;

    public BanMiddleware(
        RequestDelegate next,
        IDatabaseCollectionProvider mongo,
        ILogger<BanMiddleware> logger
    ) {
        _next = next;
        _dbIpBans = mongo.GetCollection<DbIpBan>();
        _logger = logger;
    }

    public async Task Invoke(HttpContext context)
    {
        var remoteIp = context.Connection.RemoteIpAddress;

        var isIpBanned = (await _dbIpBans.Query()
            .Where(it => it.BannedUntil > DateTime.UtcNow)
            .ToListAsync())
        .Where(it => new Regex(it.IpAddressOrRegex).IsMatch(remoteIp.ToString()))
        .ToList();

        if (isIpBanned.Count > 0)
        {
            PreventBannedAccess(context);
            return;
        }

        var user = context.Items["User"] as DbUserModel;
        if (user != null)
        {
            if (user.BannedUntil > DateTime.UtcNow)
            {
                PreventBannedAccess(context);
                return;
            }
        }

        await _next.Invoke(context);
    }

    private void PreventBannedAccess(HttpContext context)
    {

        var username = (context.Items["User"] as DbUserModel)?.Username;
        var userId = (context.Items["User"] as DbUserModel)?.Id;
        var remoteIpAddress = context.Connection.RemoteIpAddress;
        var httpMethod = context.Request?.Method;
        var requestUrl = context.Request?.Path.Value;
        var queryString = context.Request?.QueryString;
        var statusCode = context.Response?.StatusCode;

        _logger.LogInformation(
            "BANNED ATTEMPT {user}(uuid={userId}, ip={ip}) {method} {url}{queryString} => {statusCode}",
            username,
            userId,
            remoteIpAddress,
            httpMethod,
            requestUrl,
            queryString,
            "403"
        );

        var response = SubterfugeResponse<string>.OfFailure(
            ResponseType.BANNED,
            "You are banned."
        );

        context.Response.StatusCode = (int)HttpStatusCode.Forbidden;
        context.Response.WriteAsync(JsonConvert.SerializeObject(response));
    }
}


text:
using Subterfuge.Remake.Server.Database;
using Subterfuge.Remake.Server.Database.Models;

namespace Subterfuge.Remake.Server.Middleware;

public class LoggingMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogger _logger;
    private IDatabaseCollectionProvider _db;

    public LoggingMiddleware(
        RequestDelegate next,
        ILoggerFactory loggerFactory,
        IDatabaseCollectionProvider mongo
    )
    {
        _db = mongo;
        _next = next;
        _logger = loggerFactory.CreateLogger<LoggingMiddleware>();
    }

    public async Task Invoke(HttpContext context)
    {
        try
        {
            await _next(context);
        }
        finally
        {
            var username = (context.Items["User"] as DbUserModel)?.Username;
            var userId = (context.Items["User"] as DbUserModel)?.Id;
            var remoteIpAddress = context.Connection.RemoteIpAddress;
            var httpMethod = context.Request?.Method;
            var requestUrl = context.Request?.Path.Value;
            var queryString = context.Request?.QueryString;
            var statusCode = context.Response?.StatusCode;

            _logger.LogInformation(
                "{user}(uuid={userId}, ip={ip}) {method} {url}{queryString} => {statusCode}",
                username,
                userId,
                remoteIpAddress,
                httpMethod,
                requestUrl,
                queryString,
                statusCode
            );

            var serverAction = new DbServerAction()
            {
                Username = username,
                UserId = userId,
                RemoteIpAddress = remoteIpAddress?.ToString(),
                HttpMethod = httpMethod,
                RequestUrl = requestUrl,
                StatusCode = statusCode,
                UserAgent = context.Request?.Headers["User-Agent"]
            };

            await _db.GetCollection<DbServerAction>().Upsert(serverAction);
        }
    }
}


text:
using MongoDB.Driver.Linq;
using Subterfuge.Remake.Server.Database;
using Subterfuge.Remake.Server.Database.Models;

namespace Subterfuge.Remake.Server.Middleware;

// Middleware to set a DbUserModel if the JWT token is set.
public class JwtMiddleware
{
    private readonly RequestDelegate _next;
    private IDatabaseCollectionProvider _db;

    public JwtMiddleware(RequestDelegate next, IDatabaseCollectionProvider mongo)
    {
        _next = next;
        _db = mongo;
    }

    public async Task Invoke(HttpContext context)
    {
        string? userId = null;
        try
        {
            userId = context.User?.Claims?.First(it => it.Type == "uuid")?.Value;
            if (userId != null)
            {
                DbUserModel? user = await _db.GetCollection<DbUserModel>().Query().FirstOrDefaultAsync(it => it.Id == userId);
                if (user != null)
                {
                    context.Items["User"] = user;
                }
            }
        }
        catch (Exception e)
        {
            // Noop.
            // Just attach the DbUserModel if it can be found from the user claim. If not, do nothing.
        }

        await _next(context);
    }
}


text:
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using Subterfuge.Remake.Api.Network;
using Subterfuge.Remake.Server.Database;
using Subterfuge.Remake.Server.Database.Models;

namespace Subterfuge.Remake.Server.Middleware;

public class ExceptionResponseMiddleware : ExceptionFilterAttribute
{
    private IDatabaseCollectionProvider _db;
    private ILogger _logger;

    public ExceptionResponseMiddleware(IDatabaseCollectionProvider mongo, ILogger<ExceptionResponseMiddleware> logger)
    {
        this._db = mongo;
        _logger = logger;
    }

    public override async Task OnExceptionAsync(ExceptionContext context)
    {
        await HandleException(context);
        await base.OnExceptionAsync(context);
    }

    public override async void OnException(ExceptionContext context)
    {
        await HandleException(context);
        base.OnException(context);
    }

    private async Task HandleException(ExceptionContext context)
    {
        // Format the response object:
        var response = new ObjectResult(
            SubterfugeResponse<string>.OfFailure(
                ResponseType.INTERNAL_SERVER_ERROR,
                context.Exception?.Message + " " + context.Exception?.StackTrace
            )
        )
        {
            StatusCode = StatusCodes.Status500InternalServerError
        };

        context.Result = response;
        context.ExceptionHandled = true;

        LogException(context);
        await TryLogDatabase(context);
    }

    private void LogException(ExceptionContext context)
    {
        var username = (context.HttpContext.Items["User"] as DbUserModel)?.Username;
        var userId = (context.HttpContext.Items["User"] as DbUserModel)?.Id;
        var remoteIpAddress = context.HttpContext.Connection.RemoteIpAddress;
        var httpMethod = context.HttpContext.Request?.Method;
        var requestUrl = context.HttpContext.Request?.Path.Value;
        var queryString = context.HttpContext.Request?.QueryString;
        var statusCode = 500;

        _logger.LogError(
            "{user}(uuid={userId}, ip={ip}) {method} {url}{queryString} => {statusCode} \n {exception} \n {stackTrace}",
            username,
            userId,
            remoteIpAddress,
            httpMethod,
            requestUrl,
            queryString,
            statusCode,
            context.Exception.Message,
            context.Exception.StackTrace
        );
    }

    private async Task TryLogDatabase(ExceptionContext context)
    {
        StreamReader reader = new StreamReader(context.HttpContext.Request.Body);
        var RequestBody = reader.ReadToEnd();

        var user = context.HttpContext.Items["User"] as DbUserModel;
        var Username = user?.Username;
        var UserId = user?.Id;

        var serverException = new DbServerException()
        {
            Username = Username,
            UserId = UserId,
            RemoteIpAddress = context.HttpContext.Connection.RemoteIpAddress?.ToString(),
            HttpMethod = context.HttpContext.Request.Method,
            RequestUri = context.HttpContext.Request.Path,
            RequestBody = RequestBody,
            QueryString = context.HttpContext.Request.QueryString.ToString(),
            ExceptionMessage = context.Exception?.Message,
            ExceptionSource = context.Exception?.Source,
            StackTrace = context.Exception?.StackTrace,
            UserAgent = context.HttpContext.Request.Headers["User-Agent"]
        };

        await _db.GetCollection<DbServerException>().Upsert(serverException);
    }
}


text:
using Microsoft.AspNetCore.Mvc;
using Subterfuge.Remake.Api.Network;
using JsonSerializer = System.Text.Json.JsonSerializer;

namespace Subterfuge.Remake.Server.Middleware;

public class ErrorResponseMapper
{

    public static Func<ActionContext, IActionResult> ValidationErrorFactory()
    {
        return context =>
        {
            var errors = context.ModelState.Values.SelectMany(m => m.Errors)
                .Select(e => e.ErrorMessage)
                .ToList();

            var response = SubterfugeResponse<string>.OfFailure(ResponseType.VALIDATION_ERROR, "{ \"errors\": " + JsonSerializer.Serialize(errors) + " }");
            return new BadRequestObjectResult(response);
        };
    }
}


text:
using System.Text;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;

namespace Subterfuge.Remake.Server.Authentication;

public class JwtAuthenticationScheme
{

    public IConfiguration Configuration { get; }

    public JwtAuthenticationScheme(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public void ConfigureAuthentication(IServiceCollection services)
    {
        services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
            .AddJwtBearer(options =>
            {
                options.TokenValidationParameters = new TokenValidationParameters
                {
                    ValidateIssuer = true,
                    ValidateAudience = true,
                    ValidateLifetime = false,
                    ValidateIssuerSigningKey = true,
                    ValidIssuer = Configuration["Jwt:Issuer"],
                    ValidAudience = Configuration["Jwt:Issuer"],
                    IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["Jwt:Key"]))
                };
            });
    }
}


text:
using Subterfuge.Remake.Api.Network;

namespace Subterfuge.Remake.Server.Controllers
{
    public class ResponseFactory
    {
        public static SubterfugeResponse<T> ofSuccess<T>(T data)
        {
            return SubterfugeResponse<T>.OfSuccess(data);
        }

        public static ResponseStatus createResponse(ResponseType type, string? details="")
        {
            if (type == ResponseType.SUCCESS)
            {
                return new ResponseStatus()
                {
                    IsSuccess = true,
                    ResponseType = type,
                    Detail = details,
                };
            }
            return new ResponseStatus()
            {
                IsSuccess = false,
                ResponseType = type,
                Detail = details,
            };
        }
    }
}


text:
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Subterfuge.Remake.Api.Network;
using Subterfuge.Remake.Api.Network.Api;
using Subterfuge.Remake.Server.Database.Models;

namespace Subterfuge.Remake.Server.Controllers.health;

[ApiController]
[Route("api/[controller]/[action]")]
public class HealthController : ControllerBase, ISubterfugeHealthApi
{
    [HttpGet]
    [AllowAnonymous]
    public async Task<SubterfugeResponse<PingResponse>> Ping()
    {
        return SubterfugeResponse<PingResponse>.OfSuccess(new PingResponse());
    }

    [HttpGet]
    [Authorize]
    public async Task<SubterfugeResponse<AuthorizedPingResponse>> AuthorizedPing()
    {
        DbUserModel? dbUserModel = HttpContext.Items["User"] as DbUserModel;
        if (dbUserModel == null)
            return SubterfugeResponse<AuthorizedPingResponse>.OfFailure(ResponseType.UNAUTHORIZED, "Not logged in.");

        return SubterfugeResponse<AuthorizedPingResponse>.OfSuccess(new AuthorizedPingResponse()
        {
            LoggedInUser = dbUserModel.ToUser()
        });
    }
}


text:
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using MongoDB.Driver;
using MongoDB.Driver.Linq;
using Subterfuge.Remake.Api.Network;
using Subterfuge.Remake.Api.Network.Api;
using Subterfuge.Remake.Server.Database;
using Subterfuge.Remake.Server.Database.Models;

namespace Subterfuge.Remake.Server.Controllers.game;

[ApiController]
[Authorize]
[Route("api/announcements/")]
public class GameAnnouncementController : ControllerBase, ISubterfugeAnnouncementApi
{

    private IDatabaseCollection<DbGameAnnouncement> _dbGameAnnouncements;

    public GameAnnouncementController(IDatabaseCollectionProvider mongo)
    {
        this._dbGameAnnouncements = mongo.GetCollection<DbGameAnnouncement>();
    }

    [HttpPost]
    [Route("create")]
    [Authorize(Roles = "Administrator")]
    public async Task<SubterfugeResponse<CreateAnnouncementResponse>> CreateAnnouncement(CreateAnnouncementRequest announcementRequest)
    {
        DbUserModel? dbUserModel = HttpContext.Items["User"] as DbUserModel;
        if (dbUserModel == null)
            return SubterfugeResponse<CreateAnnouncementResponse>.OfFailure(ResponseType.UNAUTHORIZED, "Not logged in");

        if (!dbUserModel.HasClaim(UserClaim.Administrator))
            return SubterfugeResponse<CreateAnnouncementResponse>.OfFailure(ResponseType.PERMISSION_DENIED, "Must be an administrator to do this.");

        var model = DbGameAnnouncement.fromGameAnnouncementRequest(announcementRequest, dbUserModel.ToSimpleUser());
        await _dbGameAnnouncements.Upsert(model);

        return SubterfugeResponse<CreateAnnouncementResponse>.OfSuccess(new CreateAnnouncementResponse()
        {
            AnnouncementId = model.Id,
        });
    }

    [HttpPut]
    [Route("{id}")]
    [Authorize(Roles = "Administrator")]
    public async Task<SubterfugeResponse<CreateAnnouncementResponse>> UpdateAnnouncement(string id, CreateAnnouncementRequest announcementRequest)
    {
        DbUserModel? dbUserModel = HttpContext.Items["User"] as DbUserModel;
        if (dbUserModel == null)
            return SubterfugeResponse<CreateAnnouncementResponse>.OfFailure(ResponseType.UNAUTHORIZED, "Not logged in");

        if (!dbUserModel.HasClaim(UserClaim.Administrator))
            return SubterfugeResponse<CreateAnnouncementResponse>.OfFailure(ResponseType.PERMISSION_DENIED, "Must be an administrator to do this.");

        DbGameAnnouncement announcement = await _dbGameAnnouncements.Query()
            .Where(announcement => announcement.Id == id)
            .FirstAsync();

        if (announcement == null)
            return SubterfugeResponse<CreateAnnouncementResponse>.OfFailure(ResponseType.NOT_FOUND, "Must be an administrator to do this.");

        announcement.Message = announcementRequest.Message;
        announcement.Title = announcementRequest.Title;
        announcement.ExpiresAt = announcementRequest.ExpiresAt;
        announcement.StartsAt = announcementRequest.StartsAt;
        announcement.BroadcastTo = announcementRequest.BroadcastTo;

        await _dbGameAnnouncements.Upsert(announcement);

        return SubterfugeResponse<CreateAnnouncementResponse>.OfSuccess(new CreateAnnouncementResponse()
        {
            AnnouncementId = announcement.Id,
        });
    }

    [HttpGet]
    public async Task<SubterfugeResponse<GetAnnouncementsResponse>> GetAnnouncements()
    {
        DbUserModel? dbUserModel = HttpContext.Items["User"] as DbUserModel;
        if (dbUserModel == null)
            return SubterfugeResponse<GetAnnouncementsResponse>.OfFailure(ResponseType.UNAUTHORIZED, "Not logged in");

        var query = _dbGameAnnouncements.Query();

        if (!dbUserModel.HasClaim(UserClaim.Administrator))
        {
            var now = DateTime.UtcNow;
            // If not an admin, only show active announcements for the logged in user.
            query = query.Where(it => it.StartsAt < now)
                .Where(it => it.ExpiresAt > now)
                .Where(it => it.BroadcastTo.Contains("global") || it.BroadcastTo.Contains(dbUserModel.Id));
        }

        var results = (await query.ToListAsync())
            .Select(it => it.toGameAnnouncement())
            .ToList();

        return SubterfugeResponse<GetAnnouncementsResponse>.OfSuccess(new GetAnnouncementsResponse()
        {
            Announcements = results
        });
    }
}


text:
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using MongoDB.Driver;
using MongoDB.Driver.Linq;
using Subterfuge.Remake.Api.Network;
using Subterfuge.Remake.Api.Network.Api;
using Subterfuge.Remake.Server.Database;
using Subterfuge.Remake.Server.Database.Models;

namespace Subterfuge.Remake.Server.Controllers.game;

[ApiController]
[Authorize]
[Route("api/room/{roomId}/")]
public class MessageSubterfugeGroupController : ControllerBase, ISubterfugeGroupChatApi
{
    private IDatabaseCollection<DbGameLobbyConfiguration> _dbGameLobbies;
    private IDatabaseCollection<DbUserModel> _dbUserCollection;
    private IDatabaseCollection<DbMessageGroup> _dbMessageGroups;
    private IDatabaseCollection<DbChatMessage> _dbChatMessages;

    public MessageSubterfugeGroupController(IDatabaseCollectionProvider mongo)
    {
        this._dbGameLobbies = mongo.GetCollection<DbGameLobbyConfiguration>();
        this._dbMessageGroups = mongo.GetCollection<DbMessageGroup>();
        this._dbChatMessages = mongo.GetCollection<DbChatMessage>();
        this._dbUserCollection = mongo.GetCollection<DbUserModel>();
    }

    [HttpPost]
    [Route("group/create")]
    public async Task<SubterfugeResponse<CreateMessageGroupResponse>> CreateMessageGroup(CreateMessageGroupRequest request, string roomId)
    {
        DbUserModel? dbUserModel = HttpContext.Items["User"] as DbUserModel;
        if (dbUserModel == null)
            return SubterfugeResponse<CreateMessageGroupResponse>.OfFailure(ResponseType.UNAUTHORIZED, "Not logged in.");

        var lobby = await _dbGameLobbies.Query().FirstOrDefaultAsync(it => it.Id == roomId);
        if (lobby == null)
            return SubterfugeResponse<CreateMessageGroupResponse>.OfFailure(ResponseType.NOT_FOUND, "The specified room does not exist.");

        if (lobby.RoomStatus != RoomStatus.Ongoing)
            return SubterfugeResponse<CreateMessageGroupResponse>.OfFailure(ResponseType.PERMISSION_DENIED, "Cannot start group chats in a game that has ended.");

        if (!request.UserIdsInGroup.Contains(dbUserModel.Id))
            request.UserIdsInGroup.Add(dbUserModel.Id);

        // If, for any player in the group request, they are not a player in the lobby, the request is invalid.
        if (request.UserIdsInGroup.Any(userId => lobby.PlayerIdsInLobby.All(ids => ids != userId)))
            return SubterfugeResponse<CreateMessageGroupResponse>.OfFailure(ResponseType.VALIDATION_ERROR, "Cannot create a group with a player that is not in the game");

        // Check if there are existing groups that contain the same users.
        var existingGroups = await _dbMessageGroups.Query()
            .Where(it => it.RoomId == roomId)
            .Where(it => it.MembersInGroup.Count == request.UserIdsInGroup.Count)

            .Where(it => it.MembersInGroup.Any(it => it.Id == dbUserModel.Id))
            .ToListAsync();

        if(existingGroups.Count > 0)
            return SubterfugeResponse<CreateMessageGroupResponse>.OfFailure(ResponseType.DUPLICATE, $"A chat group with the same members already exists in this room: {existingGroups[0].Id}");

        var userIdsAsUsers = (await _dbUserCollection.Query()
            .Where(user => request.UserIdsInGroup.Contains(user.Id))
            .ToListAsync())
            .Select(it => it.ToUser())
            .ToList();

        if (userIdsAsUsers.Count != request.UserIdsInGroup.Count)
            return SubterfugeResponse<CreateMessageGroupResponse>.OfFailure(ResponseType.NOT_FOUND, "One of the users in the group is not a valid user id");

        var newGroup = DbMessageGroup.CreateGroup(roomId, userIdsAsUsers);
        await _dbMessageGroups.Upsert(newGroup);

        return SubterfugeResponse<CreateMessageGroupResponse>.OfSuccess(new CreateMessageGroupResponse()
        {
            GroupId = newGroup.Id,
        });
    }

    [HttpPost]
    [Route("group/{groupId}/send")]
    public async Task<SubterfugeResponse<SendMessageResponse>> SendMessage(SendMessageRequest request, string roomId, string groupId)
    {
        DbUserModel? currentUser = HttpContext.Items["User"] as DbUserModel;
        if (currentUser == null)
            return SubterfugeResponse<SendMessageResponse>.OfFailure(ResponseType.UNAUTHORIZED, "Not logged in.");

        var lobby = await _dbGameLobbies.Query().FirstOrDefaultAsync(it => it.Id == roomId);
        if (lobby == null)
            return SubterfugeResponse<SendMessageResponse>.OfFailure(ResponseType.NOT_FOUND, "The specified room does not exist.");

        var groupChat = await _dbMessageGroups.Query().FirstOrDefaultAsync(it => it.Id == groupId);
        if (groupChat == null)
            return SubterfugeResponse<SendMessageResponse>.OfFailure(ResponseType.NOT_FOUND, "The specified group does not exist.");

        if (groupChat.MembersInGroup.All(member => member.Id != currentUser.Id) && !currentUser.HasClaim(UserClaim.Administrator))
            return SubterfugeResponse<SendMessageResponse>.OfFailure(ResponseType.PERMISSION_DENIED, "You are not a member of this group.");

        await _dbChatMessages.Upsert(new DbChatMessage()
        {
            RoomId = roomId,
            GroupId = groupId,
            SentBy = currentUser.ToUser(),
            Message = request.Message,
        });

        return SubterfugeResponse<SendMessageResponse>.OfSuccess(new SendMessageResponse());
    }

    [HttpGet]
    [Route("groups")]
    public async Task<SubterfugeResponse<GetMessageGroupsResponse>> GetMessageGroups(string roomId)
    {
        DbUserModel? currentUser = HttpContext.Items["User"] as DbUserModel;
        if (currentUser == null)
            return SubterfugeResponse<GetMessageGroupsResponse>.OfFailure(ResponseType.UNAUTHORIZED, "Not logged in.");

        var lobby = await _dbGameLobbies.Query().FirstOrDefaultAsync(it => it.Id == roomId);
        if (lobby == null)
            return SubterfugeResponse<GetMessageGroupsResponse>.OfFailure(ResponseType.NOT_FOUND, "The specified room does not exist.");

        var query = _dbMessageGroups.Query()
            .Where(group => group.RoomId == roomId);

        if (!currentUser.HasClaim(UserClaim.Administrator))
        {
            query = query.Where(group => group.MembersInGroup.Any(member => member.Id == currentUser.Id));
        }

        var groupChats = (await query.ToListAsync())
            .Select(async group => await group.ToMessageGroup(_dbChatMessages))
            .ToArray();

        return SubterfugeResponse<GetMessageGroupsResponse>.OfSuccess(new GetMessageGroupsResponse()
        {
            MessageGroups = Task.WhenAll(groupChats).Result.ToList(),
        });
    }

    [HttpGet]
    [Route("group/{groupId}/messages")]
    public async Task<SubterfugeResponse<GetGroupMessagesResponse>> GetMessages(
        [FromQuery] GetGroupMessagesRequest request,
        string roomId,
        string groupId
    ) {
        DbUserModel? currentUser = HttpContext.Items["User"] as DbUserModel;
        if (currentUser == null)
            return SubterfugeResponse<GetGroupMessagesResponse>.OfFailure(ResponseType.UNAUTHORIZED, "Not logged in.");

        var lobby = await _dbGameLobbies.Query().FirstOrDefaultAsync(it => it.Id == roomId);
        if (lobby == null)
            return SubterfugeResponse<GetGroupMessagesResponse>.OfFailure(ResponseType.NOT_FOUND, "The specified room does not exist.");

        var groupChat = await _dbMessageGroups.Query().FirstOrDefaultAsync(it => it.Id == groupId);
        if (groupChat == null)
            return SubterfugeResponse<GetGroupMessagesResponse>.OfFailure(ResponseType.NOT_FOUND, "The specified group does not exist within this room.");

        if (groupChat.MembersInGroup.All(member => member.Id != currentUser.Id) && !currentUser.HasClaim(UserClaim.Administrator))
            return SubterfugeResponse<GetGroupMessagesResponse>.OfFailure(ResponseType.PERMISSION_DENIED, "Cannot get messages from a group you are not in");

        var messages = (await _dbChatMessages.Query()
                .Where(message => message.RoomId == roomId)
                .Where(message => message.GroupId == groupId)
                .OrderByDescending(message => message.SentAt)
                .Skip((request.Pagination - 1) * 50)
                .Take(50)
                .ToListAsync())
            .Select(it => it.ToChatMessage())
            .ToList();

        return SubterfugeResponse<GetGroupMessagesResponse>.OfSuccess(new GetGroupMessagesResponse()
        {
            Messages = messages,
        });
    }
}


text:
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.IdentityModel.Tokens;
using MongoDB.Driver;
using MongoDB.Driver.Linq;
using Newtonsoft.Json;
using Subterfuge.Remake.Api.Network;
using Subterfuge.Remake.Api.Network.Api;
using Subterfuge.Remake.Core;
using Subterfuge.Remake.Core.Timing;
using Subterfuge.Remake.Server.Database;
using Subterfuge.Remake.Server.Database.Models;

namespace Subterfuge.Remake.Server.Controllers.game;

[ApiController]
[Authorize]
[Route("api/lobby/")]
public class LobbyController : ControllerBase, ISubterfugeGameLobbyApi
{

    private IDatabaseCollection<DbUserModel> _dbUserCollection;
    private IDatabaseCollection<DbGameLobbyConfiguration> _dbLobbies;
    private IDatabaseCollection<DbGameEvent> _dbGameEvents;

    public LobbyController(IDatabaseCollectionProvider mongo)
    {
        this._dbLobbies = mongo.GetCollection<DbGameLobbyConfiguration>();
        this._dbGameEvents = mongo.GetCollection<DbGameEvent>();
        this._dbUserCollection = mongo.GetCollection<DbUserModel>();
    }

    [HttpGet]
    public async Task<SubterfugeResponse<GetLobbyResponse>> GetLobbies([FromQuery] GetLobbyRequest request)
    {
        DbUserModel user = HttpContext.Items["User"] as DbUserModel;
        if (user == null)
            return SubterfugeResponse<GetLobbyResponse>.OfFailure(ResponseType.UNAUTHORIZED, "Not logged in.");

        // If the player is not an admin; Ensure they only see their own lobbies (other than public ones)
        if (!user.HasClaim(UserClaim.Administrator))
        {
            if (request.UserIdInRoom.IsNullOrEmpty())
            {
                if (request.RoomStatus != RoomStatus.Open)
                {
                    // Only show closed/ongoing/private games created by the user.
                    // Cannot see games that you are not in unless you are an admin.
                    request.UserIdInRoom = user.Id;
                }
            }
            else
            {
                if (request.UserIdInRoom != user.Id)
                    return SubterfugeResponse<GetLobbyResponse>.OfFailure(ResponseType.PERMISSION_DENIED, "Cannot search for rooms other players are in.");
            }
        }

        var query = _dbLobbies.Query();

        if (!request.CreatedByUserId.IsNullOrEmpty())
            query = query.Where(it => it.Creator.Id == request.CreatedByUserId);

        if(request.RoomStatus != null)
            query = query.Where(it => it.RoomStatus == request.RoomStatus);

        if (!request.RoomId.IsNullOrEmpty())
            query = query.Where(it => it.Id == request.RoomId);

        query = query.Where(it => it.GameSettings.MaxPlayers >= request.MinPlayers);
        query = query.Where(it => it.GameSettings.MaxPlayers <= request.MaxPlayers);

        if(request.IsAnonymous != null)
            query = query.Where(it => it.GameSettings.IsAnonymous == request.IsAnonymous);

        if(request.IsRanked != null)
            query = query.Where(it => it.GameSettings.IsRanked == request.IsRanked);

        if (request.Goal != null)
            query = query.Where(it => it.GameSettings.Goal == request.Goal);

        if (!request.UserIdInRoom.IsNullOrEmpty())
            query = query.Where(it => it.PlayerIdsInLobby.Contains(request.UserIdInRoom));

        var matchingRooms = (await query
            .OrderByDescending(it => it.TimeCreated)
            .Skip(50 * (request.Pagination - 1))
            .Take(50)
            .ToListAsync())
            .Select(async it => await it.ToGameConfiguration(_dbUserCollection))
            .ToArray();

        GetLobbyResponse roomResponse = new GetLobbyResponse();
        roomResponse.Lobbies = await Task.WhenAll(matchingRooms);

        return SubterfugeResponse<GetLobbyResponse>.OfSuccess(roomResponse);
    }

    [HttpPost]
    [Route("create")]
    public async Task<SubterfugeResponse<CreateRoomResponse>> CreateNewRoom(CreateRoomRequest request)
    {
        DbUserModel? dbUserModel = HttpContext.Items["User"] as DbUserModel;
        if (dbUserModel == null)
            return SubterfugeResponse<CreateRoomResponse>.OfFailure(ResponseType.UNAUTHORIZED, "Not logged in.");

        // Ensure max players is over 1
        if (request.GameSettings.MaxPlayers < 2)
            return SubterfugeResponse<CreateRoomResponse>.OfFailure(ResponseType.VALIDATION_ERROR, "A lobby requires MinPlayers to be 2 or more.");

        // Ensure the player has specified a deck
        if(request.CreatorSpecialistDeck.Count != Constants.REQUIRED_PLAYER_SPECIALIST_DECK_SIZE)
            return SubterfugeResponse<CreateRoomResponse>.OfFailure(ResponseType.VALIDATION_ERROR, $"Specialist deck contains only {request.CreatorSpecialistDeck.Count} but requires exactly {Constants.REQUIRED_PLAYER_SPECIALIST_DECK_SIZE}.");

        var room = DbGameLobbyConfiguration.FromRequest(request, dbUserModel.ToUser());
        await _dbLobbies.Upsert(room);

        return SubterfugeResponse<CreateRoomResponse>.OfSuccess(new CreateRoomResponse()
        {
            GameConfiguration = await room.ToGameConfiguration(_dbUserCollection),
        });
    }

    [HttpPost]
    [Route("{guid}/join")]
    public async Task<SubterfugeResponse<JoinRoomResponse>> JoinRoom(JoinRoomRequest request, string guid)
    {
        DbUserModel? dbUserModel = HttpContext.Items["User"] as DbUserModel;
        if (dbUserModel == null)
            return SubterfugeResponse<JoinRoomResponse>.OfFailure(ResponseType.UNAUTHORIZED, "Not logged in.");

        var room = await _dbLobbies.Query().Where(it => it.Id == guid).FirstOrDefaultAsync();
        if (room == null)
            return SubterfugeResponse<JoinRoomResponse>.OfFailure(ResponseType.NOT_FOUND, "Cannot find the room you wish to enter.");

        if (room.GameSettings.MaxPlayers <= room.PlayerIdsInLobby.Count)
            return SubterfugeResponse<JoinRoomResponse>.OfFailure(ResponseType.ROOM_IS_FULL, "Lobby is full.");

        if (room.PlayerIdsInLobby.Contains(dbUserModel.Id))
            return SubterfugeResponse<JoinRoomResponse>.OfFailure(ResponseType.DUPLICATE, "You are already a member of this room.");

        if (room.RoomStatus != RoomStatus.Open)
            return SubterfugeResponse<JoinRoomResponse>.OfFailure(ResponseType.GAME_ALREADY_STARTED, "You're too late! Your friends decided to play without you.");

        // Ensure the player has specified a deck
        if(request.SpecialistDeck.Count != Constants.REQUIRED_PLAYER_SPECIALIST_DECK_SIZE)
            return SubterfugeResponse<JoinRoomResponse>.OfFailure(ResponseType.VALIDATION_ERROR, $"Specialist deck contains only {request.SpecialistDeck.Count} but requires exactly {Constants.REQUIRED_PLAYER_SPECIALIST_DECK_SIZE}.");

        // Check if any players in the room are "pseudonyms" / multiboxing
        if((await room.GetPlayersInLobby(_dbUserCollection)).Any(roomMember => roomMember.Pseudonyms.Any(pseudoUser => pseudoUser.Id == dbUserModel.Id)))
            return SubterfugeResponse<JoinRoomResponse>.OfFailure(ResponseType.PERMISSION_DENIED, "Cannot join a game with an account registered to the same device");

        // Check if this is the last player to join
        if (room.PlayerIdsInLobby.Count == room.GameSettings.MaxPlayers - 1)
        {
            room.RoomStatus = RoomStatus.Ongoing;
            room.TimeStarted = DateTime.UtcNow;
        }

        room.PlayerIdsInLobby.Add(dbUserModel.Id);
        room.PlayerSpecialistDecks.Add(dbUserModel.Id, request.SpecialistDeck);
        await _dbLobbies.Upsert(room);

        return SubterfugeResponse<JoinRoomResponse>.OfSuccess(new JoinRoomResponse());
    }

    [HttpGet]
    [Route("{guid}/leave")]
    public async Task<SubterfugeResponse<LeaveRoomResponse>> LeaveRoom(string guid)
    {
        DbUserModel? dbUserModel = HttpContext.Items["User"] as DbUserModel;
        if (dbUserModel == null)
            return SubterfugeResponse<LeaveRoomResponse>.OfFailure(ResponseType.UNAUTHORIZED, "Not logged in.");

        var room = await _dbLobbies.Query().FirstOrDefaultAsync(it => it.Id == guid);
        if (room == null)
            return SubterfugeResponse<LeaveRoomResponse>.OfFailure(ResponseType.NOT_FOUND, "Cannot leave a room that does not exist");

        if (!room.PlayerIdsInLobby.Contains(dbUserModel.Id))
            return SubterfugeResponse<LeaveRoomResponse>.OfFailure(ResponseType.PERMISSION_DENIED, "You are not a member of this lobby.");

        if (room.RoomStatus == RoomStatus.Open)
        {
            if (room.Creator.Id == dbUserModel.Id)
            {
                // The creator is leaving the game. Destroy the lobby.
                await _dbLobbies.Delete(room);
                return SubterfugeResponse<LeaveRoomResponse>.OfSuccess(new LeaveRoomResponse());
            }

            // Update the player list to remove the current player.
            room.PlayerIdsInLobby = room.PlayerIdsInLobby.Where(it => it != dbUserModel.Id).ToList();
            room.PlayerSpecialistDecks.Remove(dbUserModel.Id);
            await _dbLobbies.Upsert(room);

            return SubterfugeResponse<LeaveRoomResponse>.OfSuccess(new LeaveRoomResponse());
        }

        if (room.RoomStatus == RoomStatus.Ongoing)
        {
            // Create player leave event.
            // Get current tick
            GameTick now = new GameTick(room.TimeStarted, DateTime.UtcNow);

            var leaveEvent = new DbGameEvent()
            {
                OccursAtTick = now.GetTick(),
                GameEventType = EventDataType.PlayerLeaveGameEventData,
                SerializedEventData = JsonConvert.SerializeObject(new PlayerLeaveGameEventData()
                {
                    Player = dbUserModel.ToSimpleUser()
                }),
                IssuedBy = dbUserModel.ToSimpleUser(),
                RoomId = room.Id,
            };
            await _dbGameEvents.Upsert(leaveEvent);

            return SubterfugeResponse<LeaveRoomResponse>.OfSuccess(new LeaveRoomResponse());
        }

        return SubterfugeResponse<LeaveRoomResponse>.OfFailure(ResponseType.PERMISSION_DENIED, "Cannot leave a room after it is over.");
    }

    [HttpGet]
    [Route("{guid}/start")]
    public async Task<SubterfugeResponse<StartGameEarlyResponse>> StartGameEarly(string guid)
    {
        DbUserModel? dbUserModel = HttpContext.Items["User"] as DbUserModel;
        if (dbUserModel == null)
            return SubterfugeResponse<StartGameEarlyResponse>.OfFailure(ResponseType.UNAUTHORIZED, "Not logged in.");

        var room = await _dbLobbies.Query().FirstOrDefaultAsync(it => it.Id == guid);
        if (room == null)
            return SubterfugeResponse<StartGameEarlyResponse>.OfFailure(ResponseType.NOT_FOUND, "The specified room does not exist.");

        if (room.Creator.Id != dbUserModel.Id && !dbUserModel.HasClaim(UserClaim.Administrator))
            return SubterfugeResponse<StartGameEarlyResponse>.OfFailure(ResponseType.PERMISSION_DENIED, "You are not the creator of the room; you cannot start the game early.");

        if (room.PlayerIdsInLobby.Count < 2)
            return SubterfugeResponse<StartGameEarlyResponse>.OfFailure(ResponseType.VALIDATION_ERROR, "Playing with yourself might be fun, but consider finding some friends first. Cannot start the game without at least 2 players.");

        room.RoomStatus = RoomStatus.Ongoing;
        room.TimeStarted = DateTime.UtcNow;
        room.GameSettings.MaxPlayers = room.PlayerIdsInLobby.Count;
        await _dbLobbies.Upsert(room);

        return SubterfugeResponse<StartGameEarlyResponse>.OfSuccess(new StartGameEarlyResponse());
    }
}


text:
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using MongoDB.Driver;
using MongoDB.Driver.Linq;
using Newtonsoft.Json;
using Subterfuge.Remake.Api.Network;
using Subterfuge.Remake.Api.Network.Api;
using Subterfuge.Remake.Core.Timing;
using Subterfuge.Remake.Server.Database;
using Subterfuge.Remake.Server.Database.Models;

namespace Subterfuge.Remake.Server.Controllers.game;

[ApiController]
[Authorize]
public class SubterfugeGameEventController : ControllerBase, ISubterfugeGameEventApi
{

    private IDatabaseCollection<DbGameEvent> _dbGameEvents;
    private IDatabaseCollection<DbGameLobbyConfiguration> _dbGameLobbies;
    private IDatabaseCollection<DbUserModel> _dbUserCollection;

    public SubterfugeGameEventController(IDatabaseCollectionProvider mongo)
    {
        this._dbGameEvents = mongo.GetCollection<DbGameEvent>();
        this._dbGameLobbies = mongo.GetCollection<DbGameLobbyConfiguration>();
        this._dbUserCollection = mongo.GetCollection<DbUserModel>();
    }

    [HttpGet]
    [Route("api/room/{roomId}/events")]
    public async Task<SubterfugeResponse<GetGameRoomEventsResponse>> GetGameRoomEvents(string roomId)
    {
        DbUserModel? dbUserModel = HttpContext.Items["User"] as DbUserModel;
        if (dbUserModel == null)
            return SubterfugeResponse<GetGameRoomEventsResponse>.OfFailure(ResponseType.UNAUTHORIZED, "Not logged in");

        var lobby = await _dbGameLobbies.Query().FirstOrDefaultAsync(it => it.Id == roomId);
        if (lobby == null)
            return SubterfugeResponse<GetGameRoomEventsResponse>.OfFailure(ResponseType.NOT_FOUND, "Room not found");

        if (lobby.PlayerIdsInLobby.All(id => id != dbUserModel.Id) && !dbUserModel.HasClaim(UserClaim.Administrator))
            return SubterfugeResponse<GetGameRoomEventsResponse>.OfFailure(ResponseType.PERMISSION_DENIED, "Must be a player within the lobby to get the events.");

        List<DbGameEvent> events = await _dbGameEvents.Query()
            .Where(it => it.RoomId == roomId)
            .OrderBy(it => it.TimeIssued)
            .ToListAsync();

        // Filter out only the player's events and events that have occurred in the past.
        // Get current tick to determine events in the past.
        GameTick currentTick = GameTick.fromGameConfiguration(await lobby.ToGameConfiguration(_dbUserCollection));

        // Admins see all events :)
        // TODO: Allow admins to play games. Admins should not be able to see all events if they are a player in the game. Alternatively prevent admins from joining a game.
        if (!dbUserModel.HasClaim(UserClaim.Administrator))
        {
            events = events
                .Where(it => it.OccursAtTick <= currentTick.GetTick() || it.IssuedBy.Id == dbUserModel.Id)
                .ToList();
        }

        GetGameRoomEventsResponse response = new GetGameRoomEventsResponse();
        response.GameEvents = events.Select(it => it.ToGameEventData()).ToList();
        return SubterfugeResponse<GetGameRoomEventsResponse>.OfSuccess(response);
    }

    [HttpPost]
    [Route("api/room/{roomId}/events")]
    public async Task<SubterfugeResponse<SubmitGameEventResponse>> SubmitGameEvent(SubmitGameEventRequest request, string roomId)
    {
        DbUserModel? dbUserModel = HttpContext.Items["User"] as DbUserModel;
        if (dbUserModel == null)
            return SubterfugeResponse<SubmitGameEventResponse>.OfFailure(ResponseType.UNAUTHORIZED, "Not logged in");

        var lobby = await _dbGameLobbies.Query().FirstOrDefaultAsync(it => it.Id == roomId);
        if (lobby == null)
            return SubterfugeResponse<SubmitGameEventResponse>.OfFailure(ResponseType.NOT_FOUND, "Cannot find the room you wish to submit this event to.");

        var adminEventTypes = new[]
        {
            EventDataType.PauseGameEventData,
            EventDataType.GameEndEventData,
            EventDataType.UnpauseGameEventData,
        };

        // Determine if the user is trying to submit an admin-only game event:
        EventDataType eventType = request.GameEventData.EventDataType;

        if (!dbUserModel.HasClaim(UserClaim.Administrator))
        {
            // They must be in the lobby
            if (!lobby.PlayerIdsInLobby.Contains(dbUserModel.Id))
                return SubterfugeResponse<SubmitGameEventResponse>.OfFailure(ResponseType.PERMISSION_DENIED, "Cannot submit game events if you are not in the game.");

            // If they are in the lobby, ensure their event is not an admin event.
            if(adminEventTypes.Contains(eventType))
                return SubterfugeResponse<SubmitGameEventResponse>.OfFailure(ResponseType.PERMISSION_DENIED, "Trying to submit administrative event as a player.");
        }
        else
        {
            // If admin, ensure that if they are not a player in the game they cannot send real game events.
            // They must be in the lobby
            if (!lobby.PlayerIdsInLobby.Contains(dbUserModel.Id))
            {
                if (!adminEventTypes.Contains(eventType))
                    return SubterfugeResponse<SubmitGameEventResponse>.OfFailure(ResponseType.PERMISSION_DENIED, "Admins cannot submit normal events if they are not in the lobby");
            }
        }

        // Determine what tick the game is currently at.
        GameTick currentTick = GameTick.fromGameConfiguration(await lobby.ToGameConfiguration(_dbUserCollection));
        if(request.GameEventData.OccursAtTick <= currentTick.GetTick())
            return SubterfugeResponse<SubmitGameEventResponse>.OfFailure(
                ResponseType.VALIDATION_ERROR,
                $"Cannot submit an event at tick {request.GameEventData.OccursAtTick} because it is in the past. Current Tick:" + currentTick.GetTick()
            );

        // TODO: Determine if any players have left the game or lost the game.
        // Prevent players who have left or lost from submitting events (Or we just let them but prevent it in the core side.
        // Might be easier. Then we just check if the game is over and throw out the game events that are not valid.
        // Either we stop the events here, or stop them on the client.

        // Attempt to parse the event data into the actual event class
        try
        {
            switch (eventType)
            {
                case EventDataType.LaunchEventData:
                    JsonConvert.DeserializeObject<LaunchEventData>(request.GameEventData.SerializedEventData);
                    break;
                case EventDataType.DrillMineEventData:
                    JsonConvert.DeserializeObject<DrillMineEventData>(request.GameEventData.SerializedEventData);
                    break;
                case EventDataType.GameEndEventData:
                    JsonConvert.DeserializeObject<GameEndEventData>(request.GameEventData.SerializedEventData);
                    break;
                case EventDataType.PauseGameEventData:
                    JsonConvert.DeserializeObject<PauseGameEventData>(request.GameEventData.SerializedEventData);
                    break;
                case EventDataType.ToggleShieldEventData:
                    JsonConvert.DeserializeObject<ToggleShieldEventData>(request.GameEventData.SerializedEventData);
                    break;
                case EventDataType.UnpauseGameEventData:
                    JsonConvert.DeserializeObject<UnpauseGameEventData>(request.GameEventData.SerializedEventData);
                    break;
                case EventDataType.PlayerLeaveGameEventData:
                    var playerLeaveEvent = JsonConvert.DeserializeObject<PlayerLeaveGameEventData>(request.GameEventData.SerializedEventData);
                    if (!dbUserModel.HasClaim(UserClaim.Administrator) && playerLeaveEvent != null)
                    {
                        // Ensure the player in the leave event is the player making the request.
                        // If a player is trying to force someone else to leave, punish them and make them leave instead...
                        playerLeaveEvent.Player = new SimpleUser()
                        {
                            Id = dbUserModel.Id,
                            Username = dbUserModel.Username
                        };
                    }
                    break;
                case EventDataType.HireSpecialistEventData:
                    JsonConvert.DeserializeObject<HireSpecialistEventData>(request.GameEventData.SerializedEventData);
                    break;
                case EventDataType.PromoteSpecialistEventData:
                    JsonConvert.DeserializeObject<PromoteSpecialistEventData>(request.GameEventData.SerializedEventData);
                    break;
            }
        }
        catch (JsonSerializationException serializationException)
        {
            return SubterfugeResponse<SubmitGameEventResponse>.OfFailure(
                ResponseType.VALIDATION_ERROR,
                $"Event type is {eventType} but the server could not parse the event as such."
            );
        }

        var gameEvent = DbGameEvent.FromGameEventRequest(request, dbUserModel.ToSimpleUser(), roomId);
        await _dbGameEvents.Upsert(gameEvent);

        // If the game event was an end game event, we should also update the lobby to be marked as closed and the game over.
        // TODO: Give out player exp here, count Np, save player stats, etc. Make a method somewhere for handling game end operations.
        if (eventType == EventDataType.GameEndEventData)
        {
            var expiration = DateTime.Now.AddMonths(2);

            lobby.RoomStatus = RoomStatus.Closed;
            lobby.TimeEnded = DateTime.Now;

            // Mark all game events to expire in 2 months.
            var lobbyGameEvents = (await _dbGameEvents.Query()
                .Where(it => it.RoomId == lobby.Id)
                .ToListAsync());

            // TODO: Make a bulk update method for this... This is expensive AF.
            foreach(var lobbyGameEvent in lobbyGameEvents)
            {
                lobbyGameEvent.ExpiresAt = expiration;
                await _dbGameEvents.Upsert(lobbyGameEvent);
            }

            await _dbGameLobbies.Upsert(lobby);
        }

        return SubterfugeResponse<SubmitGameEventResponse>.OfSuccess(new SubmitGameEventResponse()
        {
            EventId = gameEvent.Id,
            GameRoomEvent = gameEvent.ToGameEventData(),
        });
    }

    [HttpPut]
    [Route("api/room/{roomId}/events/{eventGuid}")]
    public async Task<SubterfugeResponse<SubmitGameEventResponse>> UpdateGameEvent(UpdateGameEventRequest request, string roomId, string eventGuid)
    {
        DbUserModel? dbUserModel = HttpContext.Items["User"] as DbUserModel;
        if (dbUserModel == null)
            return SubterfugeResponse<SubmitGameEventResponse>.OfFailure(ResponseType.UNAUTHORIZED, "Not logged in");

        var lobby = await _dbGameLobbies.Query().FirstOrDefaultAsync(it => it.Id == roomId);
        if (lobby == null)
            return SubterfugeResponse<SubmitGameEventResponse>.OfFailure(ResponseType.NOT_FOUND, "Cannot find the room you wish to update.");

        var gameEvent = await _dbGameEvents.Query().FirstOrDefaultAsync(it => it.Id == eventGuid && it.RoomId == roomId);
        if (gameEvent == null)
            return SubterfugeResponse<SubmitGameEventResponse>.OfFailure(ResponseType.NOT_FOUND, "Cannot find the game event you wish to update in the specified room.");

        if (gameEvent.IssuedBy.Id != dbUserModel.Id)
            return SubterfugeResponse<SubmitGameEventResponse>.OfFailure(ResponseType.PERMISSION_DENIED, "Cannot update an event issued by another player.");

        // Determine if the event has already passed.
        GameTick currentTick = GameTick.fromGameConfiguration(await lobby.ToGameConfiguration(_dbUserCollection));

        if (gameEvent.OccursAtTick <= currentTick.GetTick())
            return SubterfugeResponse<SubmitGameEventResponse>.OfFailure(ResponseType.VALIDATION_ERROR, "Cannot delete an event that has already happened");

        gameEvent.OccursAtTick = request.GameEventData.OccursAtTick;
        gameEvent.GameEventType = request.GameEventData.EventDataType;
        gameEvent.SerializedEventData = request.GameEventData.SerializedEventData;
        await _dbGameEvents.Upsert(gameEvent);

        return SubterfugeResponse<SubmitGameEventResponse>.OfSuccess(new SubmitGameEventResponse()
        {
            EventId = gameEvent.Id,
            GameRoomEvent = gameEvent.ToGameEventData(),
        });
    }

    [HttpDelete]
    [Route("api/room/{roomId}/events/{eventGuid}")]
    public async Task<SubterfugeResponse<DeleteGameEventResponse>> DeleteGameEvent(string roomId, string eventGuid)
    {
        DbUserModel? dbUserModel = HttpContext.Items["User"] as DbUserModel;
        if (dbUserModel == null)
            return SubterfugeResponse<DeleteGameEventResponse>.OfFailure(ResponseType.UNAUTHORIZED, "Not logged in.");

        var lobby = await _dbGameLobbies.Query().FirstOrDefaultAsync(it => it.Id == roomId);
        if (lobby == null)
            return SubterfugeResponse<DeleteGameEventResponse>.OfFailure(ResponseType.NOT_FOUND, "Cannot find the room you wish to delete.");

        var gameEvent = await _dbGameEvents.Query().FirstOrDefaultAsync(it => it.Id == eventGuid && it.RoomId == roomId);
        if (gameEvent == null)
            return SubterfugeResponse<DeleteGameEventResponse>.OfFailure(ResponseType.NOT_FOUND, "Cannot find the game event you wish to delete.");

        // Determine if the event has already passed.
        GameTick currentTick = GameTick.fromGameConfiguration(await lobby.ToGameConfiguration(_dbUserCollection));

        if (gameEvent.OccursAtTick <= currentTick.GetTick())
            return SubterfugeResponse<DeleteGameEventResponse>.OfFailure(ResponseType.VALIDATION_ERROR, "Cannot delete an event that has already happened.");

        if (gameEvent.IssuedBy.Id != dbUserModel.Id)
            return SubterfugeResponse<DeleteGameEventResponse>.OfFailure(ResponseType.PERMISSION_DENIED, "Cannot delete an event that you did not create.");

        await _dbGameEvents.Delete(gameEvent);

        return SubterfugeResponse<DeleteGameEventResponse>.OfSuccess(new DeleteGameEventResponse());
    }
}


text:
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using MongoDB.Driver;
using MongoDB.Driver.Linq;
using Subterfuge.Remake.Api.Network;
using Subterfuge.Remake.Api.Network.Api;
using Subterfuge.Remake.Server.Database;
using Subterfuge.Remake.Server.Database.Models;

namespace Subterfuge.Remake.Server.Controllers.admin;

[ApiController]
[Authorize(Roles = "Administrator")]
[Route("api/admin/")]
public class SubterfugeAdminController : ControllerBase, ISubterfugeAdminApi
{

    private IDatabaseCollection<DbServerAction> _dbServerLog;
    private IDatabaseCollection<DbServerException> _dbExceptionLog;
    private IDatabaseCollection<DbUserModel> _dbUsers;
    private IDatabaseCollection<DbIpBan> _dbIpBans;

    public SubterfugeAdminController(IDatabaseCollectionProvider mongo)
    {
        this._dbServerLog = mongo.GetCollection<DbServerAction>();
        this._dbExceptionLog = mongo.GetCollection<DbServerException>();
        this._dbUsers = mongo.GetCollection<DbUserModel>();
        this._dbIpBans = mongo.GetCollection<DbIpBan>();
    }

    [HttpGet]
    [Route("serverLog")]
    public async Task<SubterfugeResponse<ServerActionLogResponse>> GetActionLog([FromQuery] ServerActionLogRequeset request)
    {
        var query = _dbServerLog.Query();

        if (request.Username != null)
        {
            query = query.Where(it => (it.Username ?? string.Empty).ToLower().Contains(request.Username.ToLower()));
        }

        if (request.UserId != null)
        {
            query = query.Where(it => it.UserId == request.UserId);
        }

        if (request.HttpMethod != null)
        {
            query = query.Where(it => it.HttpMethod == request.HttpMethod);
        }

        if (request.RequestUrl != null)
        {
            query = query.Where(it => it.RequestUrl == request.RequestUrl);
        }

        var matchingServerActions = (await query
                .OrderByDescending(it => it.TimesAccessed)
                .Skip(50 * (request.Pagination - 1))
                .Take(50)
                .ToListAsync())
            .Select(it => it.ToServerAction())
            .ToList();

        return SubterfugeResponse<ServerActionLogResponse>.OfSuccess(new ServerActionLogResponse()
        {
            Actions = matchingServerActions,
        });
    }

    [HttpGet]
    [Route("exceptions")]
    public async Task<SubterfugeResponse<ServerExceptionLogResponse>> GetServerExceptions([FromQuery] ServerExceptionLogRequest request)
    {
        var query = _dbExceptionLog.Query();

        if (request.Username != null)
        {
            query = query.Where(it => (it.Username ?? string.Empty).ToLower().Contains(request.Username.ToLower()));
        }

        if (request.UserId != null)
        {
            query = query.Where(it => it.UserId == request.UserId);
        }

        if (request.HttpMethod != null)
        {
            query = query.Where(it => it.HttpMethod == request.HttpMethod);
        }

        if (request.RequestUrl != null)
        {
            query = query.Where(it => it.RequestUri == request.RequestUrl);
        }

        if (request.ExceptionSource != null)
        {
            query = query.Where(it => it.ExceptionSource == request.ExceptionSource);
        }

        if (request.RemoteIpAddress != null)
        {
            query = query.Where(it => it.RemoteIpAddress == request.RemoteIpAddress);
        }

        var matchingServerActions = (await query
                .OrderByDescending(it => it.UnixTimeProcessed)
                .Skip(50 * (request.Pagination - 1))
                .Take(50)
                .ToListAsync())
            .Select(it => it.ToServerException())
            .ToList();

        return SubterfugeResponse<ServerExceptionLogResponse>.OfSuccess(new ServerExceptionLogResponse()
        {
            Exceptions = matchingServerActions
        });
    }

    [HttpPost]
    [Route("banPlayer")]
    public async Task<SubterfugeResponse<BanPlayerResponse>> BanPlayer([FromBody] BanPlayerRequest banPlayerRequest)
    {
        DbUserModel? dbUserModel = HttpContext.Items["User"] as DbUserModel;
        if (dbUserModel == null)
            return SubterfugeResponse<BanPlayerResponse>.OfFailure(ResponseType.UNAUTHORIZED, "Not logged in");

        if (!dbUserModel.HasClaim(UserClaim.Administrator))
            return SubterfugeResponse<BanPlayerResponse>.OfFailure(ResponseType.PERMISSION_DENIED, "Must be an administrator to do this");

        DbUserModel? targetUser = await _dbUsers.Query().FirstOrDefaultAsync(it => it.Id == banPlayerRequest.UserId);
        if (targetUser == null)
            return SubterfugeResponse<BanPlayerResponse>.OfFailure(ResponseType.NOT_FOUND, "Player to ban not found");

        targetUser.BanHistory.Add(new AccountBan()
        {
            AdministratorNotes = banPlayerRequest.AdminNotes,
            DateExpires = banPlayerRequest.Until,
            Reason = banPlayerRequest.Reason
        });

        if (targetUser.BannedUntil <= banPlayerRequest.Until)
            targetUser.BannedUntil = banPlayerRequest.Until;

        await _dbUsers.Upsert(targetUser);

        return SubterfugeResponse<BanPlayerResponse>.OfSuccess(new BanPlayerResponse()
        {
            wasSuccess = true,
        });
    }

    [Authorize(Roles = "Administrator")]
    [HttpPost]
    [Route("banIp")]
    public async Task<SubterfugeResponse<BanIpResponse>> BanIp([FromBody] BanIpRequest banIpRequest)
    {
        DbUserModel? dbUserModel = HttpContext.Items["User"] as DbUserModel;
        if (dbUserModel == null)
            return SubterfugeResponse<BanIpResponse>.OfFailure(ResponseType.UNAUTHORIZED, "Not logged in");

        if (!dbUserModel.HasClaim(UserClaim.Administrator))
            return SubterfugeResponse<BanIpResponse>.OfFailure(ResponseType.PERMISSION_DENIED, "Must be an Administrator.");

        var newBan = new DbIpBan()
        {
            Reason = banIpRequest.Reason,
            BannedUntil = banIpRequest.Until,
            IpAddressOrRegex = banIpRequest.DirectIpOrRegex,
            AdminNotes = banIpRequest.AdminNotes,
        };

        await _dbIpBans.Upsert(newBan);

        return SubterfugeResponse<BanIpResponse>.OfSuccess(new BanIpResponse()
        {
            wasSuccess = true,
        });
    }

    [HttpGet]
    [Route("ipBans")]
    public async Task<SubterfugeResponse<GetIpBansResponse>> GetIpBans(int pagination = 1)
    {
        DbUserModel? dbUserModel = HttpContext.Items["User"] as DbUserModel;
        if (dbUserModel == null)
            return SubterfugeResponse<GetIpBansResponse>.OfFailure(ResponseType.UNAUTHORIZED, "Not logged in");

        if (!dbUserModel.HasClaim(UserClaim.Administrator))
            return SubterfugeResponse<GetIpBansResponse>.OfFailure(ResponseType.PERMISSION_DENIED, "Must be an Administrator.");

        var ipBans = (await _dbIpBans.Query()
            .OrderByDescending(it => it.DateApplied)
            .Skip((pagination - 1) * 50)
            .Take(50)
            .ToListAsync())
            .Select(it => it.ToIpBan())
            .ToList();

        return SubterfugeResponse<GetIpBansResponse>.OfSuccess(new GetIpBansResponse()
        {
            BannedIps = ipBans
        });
    }

    [HttpGet]
    [Route("playerBans")]
    public async Task<SubterfugeResponse<GetBannedPlayerResponse>> GetBannedPlayers(int pagination = 1)
    {
        DbUserModel? dbUserModel = HttpContext.Items["User"] as DbUserModel;
        if (dbUserModel == null)
            return SubterfugeResponse<GetBannedPlayerResponse>.OfFailure(ResponseType.UNAUTHORIZED, "Not logged in");

        if (!dbUserModel.HasClaim(UserClaim.Administrator))
            return SubterfugeResponse<GetBannedPlayerResponse>.OfFailure(ResponseType.PERMISSION_DENIED, "Must be an Administrator.");

        var bannedPlayers = (await _dbUsers.Query()
                .OrderByDescending(it => it.BannedUntil)
                .Where(it => it.BannedUntil > DateTime.UtcNow)
                .Skip((pagination - 1) * 50)
                .Take(50)
                .ToListAsync())
            .Select(it => it.ToDetailedUser())
            .ToList();

        return SubterfugeResponse<GetBannedPlayerResponse>.OfSuccess(new GetBannedPlayerResponse()
        {
            BannedUsers = bannedPlayers
        });
    }

    [HttpPost]
    [Route("echo")]
    public async Task<SubterfugeResponse<Echo>> EchoRequest(Echo request)
    {
        return SubterfugeResponse<Echo>.OfSuccess(request);
    }
}


text:
namespace Subterfuge.Remake.Server.Controllers.account;

public class TwilioConfig
{
    public readonly bool enabled;
    public readonly string twilioAccountSid;
    public readonly string twilioAuthToken;
    public readonly string twilioVerificationServiceSid;

    public TwilioConfig(IConfiguration phoneVerificationConfigSection)
    {
        enabled = Boolean.Parse(phoneVerificationConfigSection["Enabled"]);

        var twilioConfig = phoneVerificationConfigSection.GetSection("Twilio");
        twilioAccountSid = twilioConfig["AccountSid"];
        twilioAuthToken = twilioConfig["AuthToken"];
        twilioVerificationServiceSid = twilioConfig["VerificationServiceSid"];
    }
}


text:
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.IdentityModel.Tokens;
using MongoDB.Driver;
using MongoDB.Driver.Linq;
using Subterfuge.Remake.Api.Network;
using Subterfuge.Remake.Api.Network.Api;
using Subterfuge.Remake.Server.Database;
using Subterfuge.Remake.Server.Database.Models;
using Twilio;
using Twilio.Rest.Lookups.V2;
using Twilio.Rest.Verify.V2.Service;

namespace Subterfuge.Remake.Server.Controllers.account;

[ApiController]
[Route("api/user/")]
public class UserController : ControllerBase, ISubterfugeAccountApi
{

    private readonly IDatabaseCollection<DbUserModel> _dbUsers;
    private readonly IDatabaseCollection<DbChatMessage> _dbChatMessages;
    private readonly IConfiguration _config;
    public readonly TwilioConfig _twilioConfig;

    public UserController(IConfiguration configuration, IDatabaseCollectionProvider mongo)
    {
        _config = configuration;
        _dbUsers = mongo.GetCollection<DbUserModel>();
        _dbChatMessages = mongo.GetCollection<DbChatMessage>();

        _twilioConfig = new TwilioConfig(_config.GetSection("PhoneVerification"));

        // Check to send the user an SMS
        if (_twilioConfig.enabled)
        {
            TwilioClient.Init(_twilioConfig.twilioAccountSid, _twilioConfig.twilioAuthToken);
        }
    }

    [AllowAnonymous]
    [HttpPost]
    [Route("login")]
    public async Task<SubterfugeResponse<AuthorizationResponse>> Login(AuthorizationRequest request)
    {
        DbUserModel? dbUserModel = HttpContext.Items["User"] as DbUserModel;

        var user = await AuthenticateUserByPassword(request);
        if (user == null)
            return SubterfugeResponse<AuthorizationResponse>.OfFailure(ResponseType.UNAUTHORIZED, "Not logged in");

        if (dbUserModel != null && dbUserModel.Username != request.Username)
        {
            await FlagAccountDuplicate(user, dbUserModel, MultiBoxReason.LoginWithCredentialsForAnotherAccount);
        }

        var tokenString = GenerateJsonWebToken(user);
        return SubterfugeResponse<AuthorizationResponse>.OfSuccess(new AuthorizationResponse()
        {
            Token = tokenString,
            User = user.ToUser(),
        });
    }

    [AllowAnonymous]
    [HttpPost]
    [Route("register")]
    public async Task<SubterfugeResponse<AccountRegistrationResponse>> RegisterAccount(AccountRegistrationRequest registrationRequeset)
    {
        List<DbUserModel> usersMatchingUsernamePhoneOrDeviceId = await _dbUsers.Query()
            .Where(it => it.Username == registrationRequeset.Username || it.PhoneNumber == registrationRequeset.PhoneNumber || it.DeviceIdentifier == registrationRequeset.DeviceIdentifier)
            .ToListAsync();

        var matchingUsername = usersMatchingUsernamePhoneOrDeviceId.Any(it => it.Username == registrationRequeset.Username);
        if (matchingUsername)
            return SubterfugeResponse<AccountRegistrationResponse>.OfFailure(ResponseType.DUPLICATE, "Username is taken.");

        if (usersMatchingUsernamePhoneOrDeviceId.Count > 0)
        {
            return SubterfugeResponse<AccountRegistrationResponse>.OfFailure(ResponseType.VALIDATION_ERROR, "An account is already registered to this device.");
        }

        // Create the new user account
        DbUserModel model = DbUserModel.FromRegistrationRequest(registrationRequeset);

        // Check to send the user an SMS
        if (_twilioConfig.enabled)
        {
            // Verify that the submitted phone number is valid.
            var checkPhoneNumber = await PhoneNumberResource.FetchAsync(
                pathPhoneNumber: registrationRequeset.PhoneNumber
            );

            if (!checkPhoneNumber.Valid.Value)
            {
                var errors = checkPhoneNumber.ValidationErrors.Select(it => it.ToString());
                var errorsAsString = String.Join(", ", errors);

                return SubterfugeResponse<AccountRegistrationResponse>.OfFailure(ResponseType.VALIDATION_ERROR, $"The provided phone number is invalid. Errors: {errorsAsString}");
            }

            await VerificationResource.CreateAsync(
                to: checkPhoneNumber.PhoneNumber.ToString(),
                channel: "sms",
                pathServiceSid: _twilioConfig.twilioVerificationServiceSid
            );

            // Update the phone number to be the validated number.
            model.PhoneNumber = checkPhoneNumber.PhoneNumber.ToString();
        }
        else
        {
            // If phone validation is disabled, mark their account as being verified
            var claimCopy = model.Claims.ToList();
            claimCopy.Add(UserClaim.PhoneVerified);
            model.Claims = claimCopy.ToArray();
        }

        // Create a new user model
        await _dbUsers.Upsert(model);

        string token = GenerateJsonWebToken(model);
        return SubterfugeResponse<AccountRegistrationResponse>.OfSuccess(new AccountRegistrationResponse
        {
            Token = token,
            User = model.ToUser(),
            RequirePhoneValidation = _twilioConfig.enabled
        });
    }

    [HttpPost]
    [Route("verifyPhone")]
    public async Task<SubterfugeResponse<AccountVadliationResponse>> VerifyPhone(AccountValidationRequest validationRequest)
    {
        DbUserModel? dbUserModel = HttpContext.Items["User"] as DbUserModel;
        if (dbUserModel == null)
            return SubterfugeResponse<AccountVadliationResponse>.OfFailure(ResponseType.UNAUTHORIZED, "Not logged in.");

        // Validate through twilio
        var verificationCheck = VerificationCheckResource.Create(
            to: dbUserModel.PhoneNumber,
            code: validationRequest.VerificationCode,
            pathServiceSid: _twilioConfig.twilioVerificationServiceSid
        );

        if (verificationCheck.Status == "approved")
        {
            // Update the account model to be verified:
            var claimCopy = dbUserModel.Claims.ToList();
            claimCopy.Add(UserClaim.PhoneVerified);
            dbUserModel.Claims = claimCopy.ToArray();

            await _dbUsers.Upsert(dbUserModel);

            return SubterfugeResponse<AccountVadliationResponse>.OfSuccess(new AccountVadliationResponse()
            {
                User = dbUserModel.ToUser(),
                wasValidationSuccessful = true,
            });
        }

        return SubterfugeResponse<AccountVadliationResponse>.OfFailure(ResponseType.VALIDATION_ERROR, "Unable to verify account.");
    }

    private async Task FlagAccountDuplicate(DbUserModel accountOne, DbUserModel accountTwo, MultiBoxReason reason)
    {
        // Don't flag admin accounts.
        if (accountOne.HasClaim(UserClaim.Administrator) || accountTwo.HasClaim(UserClaim.Administrator))
            return;

        accountOne.MultiboxAccounts.Add(new MultiboxAccount()
        {
            MultiboxReason = reason,
            TimeOccured = DateTime.UtcNow,
            User = accountTwo.ToSimpleUser()
        });
        await _dbUsers.Upsert(accountOne);

        accountTwo.MultiboxAccounts.Add(new MultiboxAccount()
        {
            MultiboxReason = reason,
            TimeOccured = DateTime.UtcNow,
            User = accountOne.ToSimpleUser()
        });
        await _dbUsers.Upsert(accountTwo);
    }

    [Authorize(Roles = "Administrator")]
    [HttpGet]
    [Route("query")]
    public async Task<SubterfugeResponse<GetDetailedUsersResponse>> GetUsers([FromQuery] GetUserRequest request)
    {
        IMongoQueryable<DbUserModel> query = _dbUsers.Query();

        if (!request.UsernameSearch.IsNullOrEmpty())
            query = query.Where(it => it.Username.ToLower().Contains(request.UsernameSearch.ToLower()));

        if (!request.DeviceIdentifierSearch.IsNullOrEmpty())
            query = query.Where(it => it.DeviceIdentifier == request.DeviceIdentifierSearch);

        if (!request.UserIdSearch.IsNullOrEmpty())
            query = query.Where(it => it.Id == request.UserIdSearch);

        if (request.RequireUserClaim != null)
            // Create a filter that require the claim to exist in the user claims
            query = query.Where(it => it.Claims.Any(it => it == request.RequireUserClaim));

        if (request.isBanned != null)
        {
            if ((bool)request.isBanned)
            {
                // Create a filter that only gets models with a 'BannedUntil' date after today.
                query = query.Where(it => it.BannedUntil > DateTime.UtcNow);
            }
            else
            {
                // Create a filter that only gets models with a 'BannedUntil' date before today.
                query = query.Where(it => it.BannedUntil <= DateTime.UtcNow);
            }
        }

        var matchingUsers = (await query
                .OrderByDescending(it => it.DateCreated)
                .Skip(50 * (request.pagination - 1))
                .Take(50)
                .ToListAsync())
            .Select(it => it.ToDetailedUser())
            .ToList();

        return SubterfugeResponse<GetDetailedUsersResponse>.OfSuccess(new GetDetailedUsersResponse()
        {
            users = matchingUsers
        });
    }

    [HttpGet]
    public async Task<SubterfugeResponse<GetUserResponse>> GetUser(string userId)
    {
        IMongoQueryable<DbUserModel> query = _dbUsers.Query();
        query = query.Where(it => it.Id == userId);

        var matchingUsers = (await query
            .OrderByDescending(it => it.DateCreated)
            .FirstOrDefaultAsync());

        if (matchingUsers == null)
            return SubterfugeResponse<GetUserResponse>.OfFailure(ResponseType.NOT_FOUND, "The requested player was not found");

        return SubterfugeResponse<GetUserResponse>.OfSuccess(new GetUserResponse()
        {
            User = matchingUsers.ToUser()
        });
    }

    [Authorize(Roles = "Administrator")]
    [HttpGet]
    [Route("messages")]
    public async Task<SubterfugeResponse<GetPlayerChatMessagesResponse>> GetPlayerChatMessages(string playerId, int pagination = 1)
    {

        var user = await _dbUsers.Query()
            .Where(it => it.Id == playerId)
            .FirstOrDefaultAsync();

        if (user == null)
            return SubterfugeResponse<GetPlayerChatMessagesResponse>.OfFailure(ResponseType.NOT_FOUND, "The requested player was not found");

        var chatMessages = await _dbChatMessages.Query()
            .Where(it => it.SentBy.Id == playerId)
            .OrderByDescending(it => it.SentAt)
            .Skip((pagination - 1) * 50)
            .Take(50)
            .ToListAsync();

        return SubterfugeResponse<GetPlayerChatMessagesResponse>.OfSuccess(new GetPlayerChatMessagesResponse()
        {
            Messages = chatMessages.Select(it => it.ToChatMessage()).ToList()
        });
    }

    private string GenerateJsonWebToken(DbUserModel user)
    {
        var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_config["Jwt:Key"]));
        var credentials = new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256);

        var claims = new List<Claim> {
            new Claim("name", user.Username),
            new Claim("uuid", user.Id),
            new Claim("DateOfJoin", ((DateTimeOffset)user.DateCreated).ToUnixTimeSeconds().ToString()),
            new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
        };

        foreach (var role in user.Claims)
        {
            claims.Add(new Claim(ClaimTypes.Role, role.ToString()));
        }

        var token = new JwtSecurityToken(_config["Jwt:Issuer"],
            _config["Jwt:Issuer"],
            claims,
            expires: DateTime.UtcNow.AddMinutes(120),
            signingCredentials: credentials
        );

        return new JwtSecurityTokenHandler().WriteToken(token);
    }

    private async Task<DbUserModel?> AuthenticateUserByPassword(AuthorizationRequest request)
    {
        // Try to get a user
        DbUserModel? dbUserModel = await _dbUsers.Query().FirstOrDefaultAsync(it => it.Username == request.Username);

        if (dbUserModel == null || !JwtManager.VerifyHashedStringMatches(request.Password, dbUserModel.PasswordHash))
            return null;

        return dbUserModel;
    }
}


text:
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using MongoDB.Driver.Linq;
using Subterfuge.Remake.Api.Network;
using Subterfuge.Remake.Api.Network.Api;
using Subterfuge.Remake.Server.Database;
using Subterfuge.Remake.Server.Database.Models;

namespace Subterfuge.Remake.Server.Controllers.account;

[ApiController]
[Authorize]
[Route("api/user/{userId}/")]
public class SubterfugeUserRoleController : ControllerBase, ISubterfugeUserRoleApi
{
    private IDatabaseCollection<DbUserModel> _dbUsers;
    private IDatabaseCollection<DbIpBan> _dbIpBans;

    public SubterfugeUserRoleController(IDatabaseCollectionProvider mongo)
    {
        _dbUsers = mongo.GetCollection<DbUserModel>();
        _dbIpBans = mongo.GetCollection<DbIpBan>();
    }

    [Authorize]
    [HttpGet]
    [Route("roles")]
    public async Task<SubterfugeResponse<GetRolesResponse>> GetRoles(string userId)
    {
        DbUserModel? dbUserModel = HttpContext.Items["User"] as DbUserModel;
        if (dbUserModel == null)
            return SubterfugeResponse<GetRolesResponse>.OfFailure(ResponseType.UNAUTHORIZED, "Not logged in");

        if (dbUserModel.HasClaim(UserClaim.Administrator))
        {
            DbUserModel? targetUser = await _dbUsers.Query().FirstOrDefaultAsync(it => it.Id == userId);
            if(targetUser == null)
                return SubterfugeResponse<GetRolesResponse>.OfFailure(ResponseType.NOT_FOUND, "Off the grid? We have no record of this user.");

            return SubterfugeResponse<GetRolesResponse>.OfSuccess(new GetRolesResponse()
            {
                Claims = targetUser.Claims
            });
        }

        if (dbUserModel.Id == userId)
        {
            return SubterfugeResponse<GetRolesResponse>.OfSuccess(new GetRolesResponse()
            {
                Claims = dbUserModel.Claims
            });
        }

        return SubterfugeResponse<GetRolesResponse>.OfFailure(ResponseType.PERMISSION_DENIED, "Cannot get the roles of a user other than yourself.");
    }

    [Authorize(Roles = "Administrator")]
    [HttpPost]
    [Route("roles")]
    public async Task<SubterfugeResponse<GetRolesResponse>> SetRoles(string userId, UpdateRolesRequest request)
    {
        DbUserModel? dbUserModel = HttpContext.Items["User"] as DbUserModel;
        if (dbUserModel == null)
            return SubterfugeResponse<GetRolesResponse>.OfFailure(ResponseType.UNAUTHORIZED, "Not logged in");

        if (!dbUserModel.HasClaim(UserClaim.Administrator))
            return SubterfugeResponse<GetRolesResponse>.OfFailure(ResponseType.PERMISSION_DENIED, "Admins only.");

        DbUserModel? targetUser = await _dbUsers.Query().FirstOrDefaultAsync(it => it.Id == userId);
        if (targetUser == null)
            return SubterfugeResponse<GetRolesResponse>.OfFailure(ResponseType.NOT_FOUND, "The user you are trying to update the role for does not exist.");

        targetUser.Claims = request.Claims;
        await _dbUsers.Upsert(targetUser);

        return SubterfugeResponse<GetRolesResponse>.OfSuccess(new GetRolesResponse() {
            Claims = targetUser.Claims
        });
    }
}


text:
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using MongoDB.Driver;
using MongoDB.Driver.Linq;
using Subterfuge.Remake.Api.Network;
using Subterfuge.Remake.Api.Network.Api;
using Subterfuge.Remake.Server.Database;
using Subterfuge.Remake.Server.Database.Models;

namespace Subterfuge.Remake.Server.Controllers.social;

[ApiController]
[Authorize]
[Route("api/user/{userId}")]
public class SocialController : ControllerBase, ISubterfugeSocialApi
{

    private IDatabaseCollection<DbPlayerRelationship> _dbRelations;
    private IDatabaseCollection<DbUserModel> _dbUserCollection;

    public SocialController(IDatabaseCollectionProvider mongo)
    {
        _dbRelations = mongo.GetCollection<DbPlayerRelationship>();
        _dbUserCollection = mongo.GetCollection<DbUserModel>();
    }

    [HttpGet]
    [Route("blocks")]
    public async Task<SubterfugeResponse<ViewBlockedPlayersResponse>> ViewBlockedPlayers(string userId)
    {
        DbUserModel currentUser = HttpContext.Items["User"] as DbUserModel;
        if (currentUser == null)
            return SubterfugeResponse<ViewBlockedPlayersResponse>.OfFailure(ResponseType.UNAUTHORIZED, "Not logged in.");

        if (userId != currentUser.Id && !currentUser.HasClaim(UserClaim.Administrator))
            return SubterfugeResponse<ViewBlockedPlayersResponse>.OfFailure(ResponseType.PERMISSION_DENIED, "Can only view your own blocked players.");

        DbUserModel targetUser = await _dbUserCollection.Query()
            .FirstOrDefaultAsync(it => it.Id == userId);

        if (targetUser == null)
            return SubterfugeResponse<ViewBlockedPlayersResponse>.OfFailure(ResponseType.NOT_FOUND, "The target player was not found.");

        var blockedUser = (await _dbRelations.Query()
                .Where(it => it.RelationshipStatus == RelationshipStatus.Blocked)
                .Where(it => it.Player.Id == userId)
                .ToListAsync())
            .Select(it => it.GetOtherUser(userId))
            .ToList();

        return SubterfugeResponse<ViewBlockedPlayersResponse>.OfSuccess(new ViewBlockedPlayersResponse()
        {
            BlockedUsers = blockedUser,
        });
    }

    [HttpGet]
    [Route("friendRequests")]
    public async Task<SubterfugeResponse<ViewFriendRequestsResponse>> ViewFriendRequests(string userId)
    {
        DbUserModel currentUser = HttpContext.Items["User"] as DbUserModel;
        if (currentUser == null)
            return SubterfugeResponse<ViewFriendRequestsResponse>.OfFailure(ResponseType.UNAUTHORIZED, "Not logged in.");

        if (userId != currentUser.Id && !currentUser.HasClaim(UserClaim.Administrator))
            return SubterfugeResponse<ViewFriendRequestsResponse>.OfFailure(ResponseType.PERMISSION_DENIED, "Can only view your own friends.");

        DbUserModel targetUser = await _dbUserCollection.Query()
            .FirstOrDefaultAsync(it => it.Id == userId);

        if (targetUser == null)
            return SubterfugeResponse<ViewFriendRequestsResponse>.OfFailure(ResponseType.NOT_FOUND, "The target player was not found.");

        // For friend requests, if you didn't start the relation, you will be the friend ID
        var requests = (await _dbRelations.Query()
                .Where(relation => relation.Friend.Id == userId)
                .Where(relation => relation.RelationshipStatus == RelationshipStatus.Pending)
                .ToListAsync())
            .Select(it => it.GetOtherUser(userId))
            .ToList();

        return SubterfugeResponse<ViewFriendRequestsResponse>.OfSuccess(new ViewFriendRequestsResponse()
        {
            FriendRequests = requests,
        });
    }

    [HttpPost]
    [Route("block")]
    public async Task<SubterfugeResponse<BlockPlayerResponse>> BlockPlayer(BlockPlayerRequest request, string userId)
    {
        DbUserModel? dbUserModel = HttpContext.Items["User"] as DbUserModel;
        if (dbUserModel == null)
            return SubterfugeResponse<BlockPlayerResponse>.OfFailure(ResponseType.UNAUTHORIZED, "Not logged in.");

        var playerToBlock = await _dbUserCollection.Query().FirstOrDefaultAsync(it => it.Id == userId);
        if (playerToBlock == null)
            return SubterfugeResponse<BlockPlayerResponse>.OfFailure(ResponseType.NOT_FOUND, "The target player was not found.");

        if (playerToBlock.HasClaim(UserClaim.Administrator))
            return SubterfugeResponse<BlockPlayerResponse>.OfFailure(ResponseType.PERMISSION_DENIED, "Cannot block an administrator.");

        var existingRelationship = await _dbRelations.Query()
            .FirstOrDefaultAsync(relation =>
                (relation.Player.Id == dbUserModel.Id && relation.Friend.Id == userId) ||
                (relation.Player.Id == userId && relation.Friend.Id == dbUserModel.Id));

        // We can block.
        if (existingRelationship == null)
        {
            var relationship = new DbPlayerRelationship()
            {
                Player = dbUserModel.ToUser(),
                Friend = playerToBlock.ToUser(),
                RelationshipStatus = RelationshipStatus.Blocked
            };
            await _dbRelations.Upsert(relationship);
        }
        else
        {
            existingRelationship.RelationshipStatus = RelationshipStatus.Blocked;
            await _dbRelations.Upsert(existingRelationship);
        }

        return SubterfugeResponse<BlockPlayerResponse>.OfSuccess(new BlockPlayerResponse());
    }

    [HttpPost]
    [Route("unblock")]
    public async Task<SubterfugeResponse<UnblockPlayerResponse>> UnblockPlayer(UnblockPlayerRequest request, string userId)
    {
        DbUserModel? dbUserModel = HttpContext.Items["User"] as DbUserModel;
        if (dbUserModel == null)
            return SubterfugeResponse<UnblockPlayerResponse>.OfFailure(ResponseType.UNAUTHORIZED, "Not logged in.");

        var playerToUnblock = await _dbUserCollection.Query().FirstOrDefaultAsync(it => it.Id == userId);
        if (playerToUnblock == null)
            return SubterfugeResponse<UnblockPlayerResponse>.OfFailure(ResponseType.NOT_FOUND, "The specified player does not exist.");

        var existingRelationship = await _dbRelations.Query()
            .FirstOrDefaultAsync(relation =>
                (relation.Player.Id == dbUserModel.Id && relation.Friend.Id == userId) ||
                (relation.Player.Id == userId && relation.Friend.Id == dbUserModel.Id));

        if (existingRelationship == null)
        {
            var relationship = new DbPlayerRelationship()
            {
                Player = dbUserModel.ToUser(),
                Friend = playerToUnblock.ToUser(),
                RelationshipStatus = RelationshipStatus.NoRelation
            };
            await _dbRelations.Upsert(relationship);
        }
        else
        {
            if (existingRelationship.RelationshipStatus != RelationshipStatus.Blocked)
                return SubterfugeResponse<UnblockPlayerResponse>.OfFailure(ResponseType.INVALID_REQUEST, "The player you wish to unblock is not currently blocked.");

            // Cannot unblock someone if you were not the one to block them.
            if (existingRelationship.Player.Id != dbUserModel.Id)
                return SubterfugeResponse<UnblockPlayerResponse>.OfFailure(ResponseType.PERMISSION_DENIED, "You cannot unblock this player, as you did not block them.");

            existingRelationship.RelationshipStatus = RelationshipStatus.NoRelation;
            await _dbRelations.Upsert(existingRelationship);
        }

        return SubterfugeResponse<UnblockPlayerResponse>.OfSuccess(new UnblockPlayerResponse());
    }

    [HttpGet]
    [Route("addFriend")]
    public async Task<SubterfugeResponse<AddAcceptFriendResponse>> AddAcceptFriendRequest(string userId)
    {
        DbUserModel? dbUserModel = HttpContext.Items["User"] as DbUserModel;
        if (dbUserModel == null)
            return SubterfugeResponse<AddAcceptFriendResponse>.OfFailure(ResponseType.UNAUTHORIZED, "Not logged in.");

        var playerToBefriend = await _dbUserCollection.Query().FirstOrDefaultAsync(it => it.Id == userId);
        if (playerToBefriend == null)
            return SubterfugeResponse<AddAcceptFriendResponse>.OfFailure(ResponseType.NOT_FOUND, "The specified player does not exist.");

        var existingRelationship = await _dbRelations.Query()
            .FirstOrDefaultAsync(relation =>
                (relation.Player.Id == dbUserModel.Id && relation.Friend.Id == userId) ||
                (relation.Player.Id == userId && relation.Friend.Id == dbUserModel.Id));

        if (existingRelationship == null)
        {
            var relationship = new DbPlayerRelationship()
            {
                Player = dbUserModel.ToUser(),
                Friend = playerToBefriend.ToUser(),
                RelationshipStatus = RelationshipStatus.Pending
            };
            await _dbRelations.Upsert(relationship);
            return SubterfugeResponse<AddAcceptFriendResponse>.OfSuccess(new AddAcceptFriendResponse());
        }

        switch (existingRelationship.RelationshipStatus)
        {
            case RelationshipStatus.Blocked:
                return SubterfugeResponse<AddAcceptFriendResponse>.OfFailure(ResponseType.PERMISSION_DENIED, "Cannot be friends while blocked.");
            case RelationshipStatus.Friends:
                return SubterfugeResponse<AddAcceptFriendResponse>.OfFailure(ResponseType.DUPLICATE, "You are already friends with this player.");
            case RelationshipStatus.Pending:
                // If you did not create the request, set the relationship to be friends
                if (existingRelationship.Player.Id != dbUserModel.Id)
                {
                    existingRelationship.RelationshipStatus = RelationshipStatus.Friends;
                    await _dbRelations.Upsert(existingRelationship);
                    return SubterfugeResponse<AddAcceptFriendResponse>.OfSuccess(new AddAcceptFriendResponse());
                }

                // Cannot accept your own request
                return SubterfugeResponse<AddAcceptFriendResponse>.OfFailure(ResponseType.DUPLICATE, "You already have a pending friend request to this player.");
            case RelationshipStatus.NoRelation:
                existingRelationship.RelationshipStatus = RelationshipStatus.Friends;
                // Swap the 'primary' friend to indicate who sent the request.
                existingRelationship.Player = dbUserModel.ToUser();
                existingRelationship.Friend = playerToBefriend.ToUser();
                await _dbRelations.Upsert(existingRelationship);
                return SubterfugeResponse<AddAcceptFriendResponse>.OfSuccess(new AddAcceptFriendResponse());

            // Should never happen.
            default:
                return SubterfugeResponse<AddAcceptFriendResponse>.OfFailure(ResponseType.INVALID_REQUEST, "I don't know how you did this, but well done; you've played the system.");
        }
    }

    [HttpGet]
    [Route("removeFriend")]
    public async Task<SubterfugeResponse<DenyFriendRequestResponse>> RemoveRejectFriend(string userId)
    {
        DbUserModel? dbUserModel = HttpContext.Items["User"] as DbUserModel;
        if (dbUserModel == null)
            return SubterfugeResponse<DenyFriendRequestResponse>.OfFailure(ResponseType.UNAUTHORIZED, "Not logged in.");

        var playerToReject = await _dbUserCollection.Query().FirstOrDefaultAsync(it => it.Id == userId);
        if (playerToReject == null)
            return SubterfugeResponse<DenyFriendRequestResponse>.OfFailure(ResponseType.NOT_FOUND, "The specified player does not exist.");

        var existingRelationship = await _dbRelations.Query()
            .FirstOrDefaultAsync(relation =>
                (relation.Player.Id == dbUserModel.Id && relation.Friend.Id == userId) ||
                (relation.Player.Id == userId && relation.Friend.Id == dbUserModel.Id));

        if (existingRelationship == null)
            return SubterfugeResponse<DenyFriendRequestResponse>.OfFailure(ResponseType.INVALID_REQUEST, "A friend request from that player does not exist");

        if (existingRelationship.RelationshipStatus is not (RelationshipStatus.Pending or RelationshipStatus.Friends))
            return SubterfugeResponse<DenyFriendRequestResponse>.OfFailure(ResponseType.INVALID_REQUEST, "Cannot revoke friendship status for this player.");

        existingRelationship.RelationshipStatus = RelationshipStatus.NoRelation;
        await _dbRelations.Upsert(existingRelationship);

        return SubterfugeResponse<DenyFriendRequestResponse>.OfSuccess(new DenyFriendRequestResponse());
    }

    [HttpGet]
    [Route("friends")]
    public async Task<SubterfugeResponse<ViewFriendsResponse>> GetFriendList(string userId)
    {
        DbUserModel currentUser = HttpContext.Items["User"] as DbUserModel;
        if (currentUser == null)
            return SubterfugeResponse<ViewFriendsResponse>.OfFailure(ResponseType.UNAUTHORIZED, "Not logged in.");

        if (userId != currentUser.Id && !currentUser.HasClaim(UserClaim.Administrator))
            return SubterfugeResponse<ViewFriendsResponse>.OfFailure(ResponseType.PERMISSION_DENIED, "Can only view your own friend list.");

        DbUserModel targetUser = await _dbUserCollection.Query()
            .FirstOrDefaultAsync(it => it.Id == userId);

        if (targetUser == null)
            return SubterfugeResponse<ViewFriendsResponse>.OfFailure(ResponseType.NOT_FOUND, "The target player was not found");

        var friends = (await _dbRelations.Query()
                .Where(relation => relation.Player.Id == userId || relation.Friend.Id == userId)
                .Where(relation => relation.RelationshipStatus == RelationshipStatus.Friends)
                .ToListAsync())
            .Select(it => it.GetOtherUser(userId))
            .ToList();

        return SubterfugeResponse<ViewFriendsResponse>.OfSuccess(new ViewFriendsResponse()
        {
            Friends = friends,
        });
    }
}


text:
namespace Subterfuge.Remake.Server.Database;

public class MongoConfigProvider : IMongoConfigurationProvider
{

    private MongoConfiguration _config;

    public MongoConfigProvider(IConfiguration config)
    {
        var mongoConfig = config.GetSection("MongoDb");
        _config = new MongoConfiguration()
        {
            Host = mongoConfig["Host"],
            Port = Convert.ToInt32(mongoConfig["Port"]),
            CreateSuperUser = Convert.ToBoolean(mongoConfig["SuperUser:CreateSuperUser"]),
            SuperUserUsername = mongoConfig["SuperUser:Username"],
            SuperUserPassword = mongoConfig["SuperUser:Password"],
            FlushDatabase = Convert.ToBoolean(mongoConfig["FlushDatabase"]),
        };
    }

    public MongoConfiguration GetConfiguration()
    {
        return _config;
    }
}