Merge pull request 'refactoring' (#7) from refactoring into develop

Reviewed-on: michele/Map#7
This commit is contained in:
michele 2020-11-21 00:26:41 +00:00
commit 2d14cf8059
197 changed files with 4812 additions and 4673 deletions

12
.dockerignore Normal file
View file

@ -0,0 +1,12 @@
# directories
**/bin/
**/obj/
**/out/
# files
Dockerfile*
**/*.trx
**/*.md
**/*.ps1
**/*.cmd
**/*.sh

8
.editorconfig Normal file
View file

@ -0,0 +1,8 @@
root = true
[*]
indent_style = space
indent_size = 4
charset = utf-8
trim_trailing_whitespace = false
insert_final_newline = false

38
.vscode/launch.json vendored
View file

@ -2,19 +2,53 @@
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"version": "0.2.0",
"configurations": [
{
"name": "Docker .NET Core Attach (Preview)",
"type": "docker",
"request": "attach",
"platform": "netCore",
"sourceFileMap": {
"/": "${workspaceFolder}"
}
},
{
"name": "Sledgemapper",
"type": "coreclr",
"request": "launch",
"preLaunchTask": "build sledgemapper",
"program": "${workspaceFolder}/Sledgemapper/bin/Debug/netcoreapp3.1/sledgemapper.dll",
"program": "${workspaceFolder}/Sledgemapper/bin/Debug/net5.0/sledgemapper.dll",
"args": [],
"cwd": "${workspaceFolder}/Sledgemapper",
"console": "internalConsole",
"stopAtEntry": false
},
{
"name": "Sledgemapper.API",
"type": "coreclr",
"request": "launch",
"preLaunchTask": "build sledgemapper.api",
"program": "${workspaceFolder}/Sledgemapper.Api/bin/Debug/net5.0/sledgemapper.api.dll",
"args": [],
"cwd": "${workspaceFolder}/Sledgemapper.Api",
"console": "internalConsole",
"stopAtEntry": false,
"env": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
{
"name": "Identity.API",
"type": "coreclr",
"request": "launch",
"preLaunchTask": "build identity",
"program": "${workspaceFolder}/Identity/bin/Debug/netcoreapp3.1/WebApi.dll",
"args": [],
"cwd": "${workspaceFolder}/Identity",
"console": "internalConsole",
"stopAtEntry": false
},
{
"name": ".NET Core Attach",
"type": "coreclr",

19
.vscode/tasks.json vendored
View file

@ -1,6 +1,23 @@
{
"version": "2.0.0",
"tasks": [
{
"label": "build identity",
"command": "dotnet",
"type": "process",
"args": [
"build",
"${workspaceFolder}/Identity/WebApi.csproj",
"/property:GenerateFullPaths=true",
"/consoleloggerparameters:NoSummary"
],
"problemMatcher": "$msCompile",
"group": {
"kind": "build",
"isDefault": true
}
},
{
"label": "build sledgemapper",
@ -25,7 +42,7 @@
"type": "process",
"args": [
"build",
"${workspaceFolder}/Sledgemapper.Api/src/Sledgemapper.Api.csproj",
"${workspaceFolder}/Sledgemapper.Api/Sledgemapper.Api.csproj",
"/property:GenerateFullPaths=true",
"/consoleloggerparameters:NoSummary"
],

59
Dockerfile Normal file
View file

@ -0,0 +1,59 @@
# https://hub.docker.com/_/microsoft-dotnet-core
FROM mcr.microsoft.com/dotnet/sdk:5.0 AS build
WORKDIR /src
# copy csproj and restore as distinct layers
#COPY *.sln .
# COPY SignalRChat/*.csproj ./aspnetapp/
# WORKDIR /source/aspnetapp
# RUN dotnet restore
COPY Sledgemapper.Api/*.csproj ./Sledgemapper.Api/
COPY Sledgemapper.Shared/*.csproj ./Sledgemapper.Shared/
WORKDIR /src/Sledgemapper.Api
RUN dotnet restore
WORKDIR /src/Sledgemapper.Shared
RUN dotnet restore
# copy everything else and build app
WORKDIR /src
COPY Sledgemapper.Api/. ./Sledgemapper.Api/
COPY Sledgemapper.Shared/. ./Sledgemapper.Shared/
WORKDIR /src/Sledgemapper.Api
RUN dotnet publish -c release -o /app --no-restore
# final stage/image
FROM mcr.microsoft.com/dotnet/aspnet:5.0
WORKDIR /app
COPY --from=build /app ./
ENTRYPOINT ["dotnet", "SignalRChat.dll"]
# # https://hub.docker.com/_/microsoft-dotnet-core
# FROM mcr.microsoft.com/dotnet/sdk:5.0 AS build
# # copy csproj and restore as distinct layers
# #COPY *.sln .
# WORKDIR /source
# COPY Sledgemapper.Api/*.csproj ./Sledgemapper.Api/
# WORKDIR /source/Sledgemapper.Api
# RUN dotnet restore
# WORKDIR /source
# COPY Sledgemapper.Shared/*.csproj ./Sledgemapper.Shared/
# WORKDIR /source/Sledgemapper.Shared
# RUN dotnet restore
# # copy everything else and build app
# WORKDIR /source
# COPY Sledgemapper.Api/. ./Sledgemapper.Api/
# COPY Sledgemapper.Shared/. ./Sledgemapper.Shared/
# WORKDIR /source/Sledgemapper.Api
# RUN dotnet publish -c release -o /app --no-restore
# # final stage/image
# FROM mcr.microsoft.com/dotnet/aspnet:5.0
# WORKDIR /app
# COPY --from=build /app ./
# ENTRYPOINT ["dotnet", "Sledgemapper.Api.dll"]

View file

@ -1,37 +0,0 @@
{
// Use IntelliSense to find out which attributes exist for C# debugging
// Use hover for the description of the existing attributes
// For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md
"version": "0.2.0",
"configurations": [
{
"name": ".NET Core Launch (web)",
"type": "coreclr",
"request": "launch",
"preLaunchTask": "build",
// If you have changed target frameworks, make sure to update the program path.
"program": "${workspaceFolder}/SignalRChat/bin/Debug/net5.0/SignalRChat.dll",
"args": [],
"cwd": "${workspaceFolder}",
"stopAtEntry": false,
// Enable launching a web browser when ASP.NET Core starts. For more information: https://aka.ms/VSCode-CS-LaunchJson-WebBrowser
"serverReadyAction": {
"action": "openExternally",
"pattern": "\\bNow listening on:\\s+(https?://\\S+)"
},
"env": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"sourceFileMap": {
"/Views": "${workspaceFolder}/Views"
}
},
{
"name": ".NET Core Attach",
"type": "coreclr",
"request": "attach",
"processId": "${command:pickProcess}"
}
]
}

View file

@ -1,42 +0,0 @@
{
"version": "2.0.0",
"tasks": [
{
"label": "build",
"command": "dotnet",
"type": "process",
"args": [
"build",
"${workspaceFolder}/SignalRChat/SignalRChat.csproj",
"/property:GenerateFullPaths=true",
"/consoleloggerparameters:NoSummary"
],
"problemMatcher": "$msCompile"
},
{
"label": "publish",
"command": "dotnet",
"type": "process",
"args": [
"publish",
"${workspaceFolder}/SignalRChat/SignalRChat.csproj",
"/property:GenerateFullPaths=true",
"/consoleloggerparameters:NoSummary"
],
"problemMatcher": "$msCompile"
},
{
"label": "watch",
"command": "dotnet",
"type": "process",
"args": [
"watch",
"run",
"${workspaceFolder}/SignalRChat/SignalRChat.csproj",
"/property:GenerateFullPaths=true",
"/consoleloggerparameters:NoSummary"
],
"problemMatcher": "$msCompile"
}
]
}

View file

@ -0,0 +1,19 @@
using MediatR;
using System;
namespace Sledgemapper.Api.Commands
{
public abstract class BaseCommand<T> : IRequest<T>
{
public double Timestamp { get; private set; }
public string SessionName { get; private set; }
public int UserId { get; private set; }
public BaseCommand(string sessionName, int userId)
{
Timestamp = DateTimeOffset.Now.ToUnixTimeMilliseconds();
SessionName = sessionName;
UserId = userId;
}
}
}

View file

@ -0,0 +1,14 @@
using Sledgemapper.Shared.Entities;
namespace Sledgemapper.Api.Commands
{
public class DeleteOverlayCommand : BaseCommand<bool>
{
public Overlay Overlay { get; private set; }
public DeleteOverlayCommand(string sessionName, Overlay overlay, int userId) : base(sessionName, userId)
{
Overlay = overlay;
}
}
}

View file

@ -0,0 +1,14 @@
using Sledgemapper.Shared.Entities;
namespace Sledgemapper.Api.Commands
{
public class DeleteTileCommand : BaseCommand<bool>
{
public Tile Tile { get; private set; }
public DeleteTileCommand(string sessionName, Tile tile, int userId) : base(sessionName, userId)
{
Tile = tile;
}
}
}

View file

@ -0,0 +1,14 @@
using Sledgemapper.Shared.Entities;
namespace Sledgemapper.Api.Commands
{
public class DeleteWallCommand : BaseCommand<bool>
{
public Wall Wall { get; private set; }
public DeleteWallCommand(string sessionName, Wall wall, int userId) : base(sessionName, userId)
{
Wall = wall;
}
}
}

View file

@ -0,0 +1,14 @@
using MediatR;
using Sledgemapper.Shared.Entities;
namespace Sledgemapper.Api.Commands
{
public class GetMapSnapshotCommand : IRequest<Session>
{
public string SessionName { get; private set; }
public GetMapSnapshotCommand(string sessionName)
{
SessionName = sessionName;
}
}
}

View file

@ -0,0 +1,14 @@
using Sledgemapper.Shared.Entities;
namespace Sledgemapper.Api.Commands
{
public class NewOverlayCommand : BaseCommand<bool>
{
public Overlay Overlay { get; private set; }
public NewOverlayCommand(string sessionName, Overlay overlay, int userId) : base(sessionName, userId)
{
Overlay = overlay;
}
}
}

View file

@ -0,0 +1,9 @@
namespace Sledgemapper.Api.Commands
{
public class NewSessionCommand : BaseCommand<bool>
{
public NewSessionCommand(string sessionName, int userId):base(sessionName, userId)
{
}
}
}

View file

@ -0,0 +1,13 @@
using Sledgemapper.Shared.Entities;
namespace Sledgemapper.Api.Commands
{
public class NewSnapshotCommand : BaseCommand<bool>
{
public Session Session { get; private set; }
public NewSnapshotCommand(string sessionName, Session session, int userId) : base(sessionName, userId)
{
Session = session;
}
}
}

View file

@ -0,0 +1,13 @@
using Sledgemapper.Shared.Entities;
namespace Sledgemapper.Api.Commands
{
public class NewTileCommand : BaseCommand<bool>
{
public Tile Tile { get; private set; }
public NewTileCommand(string sessionName, Tile tile, int userId) : base(sessionName, userId)
{
Tile = tile;
}
}
}

View file

@ -0,0 +1,14 @@
using Sledgemapper.Shared.Entities;
namespace Sledgemapper.Api.Commands
{
public class NewWallCommand : BaseCommand<bool>
{
public Wall Wall { get; private set; }
public NewWallCommand(string sessionName, Wall wall, int userId) : base(sessionName, userId)
{
Wall = wall;
}
}
}

View file

@ -0,0 +1,75 @@
using MediatR;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Sledgemapper.Api.Commands;
using Sledgemapper.Shared.Entities;
using System.Threading.Tasks;
namespace Sledgemapper.Api.Controllers
{
[Authorize]
[Route("[controller]/{sessionName}")]
public class SessionController : ControllerBase
{
private readonly IMediator _mediator;
private int UserId => int.Parse(HttpContext.User.Identity.Name);
public SessionController(IMediator mediator) => _mediator = mediator;
[HttpPost]
public async Task<bool> Post(string sessionName)
{
var result = await _mediator.Send(new NewSessionCommand(sessionName, UserId));
return result;
}
[HttpGet]
public async Task<Session> Get(string sessionName)
{
var result = await _mediator.Send(new GetMapSnapshotCommand(sessionName));
return result;
}
[HttpPost("snapshot")]
public async Task Post(string sessionName, [FromBody] Session session)
{
await _mediator.Send(new NewSnapshotCommand(sessionName, session, UserId));
}
[HttpPost("tile")]
public async Task Post(string sessionName, [FromBody] Tile tile)
{
await _mediator.Send(new NewTileCommand(sessionName, tile, UserId));
}
[HttpPost("overlay")]
public async Task Post(string sessionName, [FromBody] Overlay overlay)
{
await _mediator.Send(new NewOverlayCommand(sessionName, overlay, UserId));
}
[HttpPost("wall")]
public async Task Post(string sessionName, [FromBody] Wall wall)
{
await _mediator.Send(new NewWallCommand(sessionName, wall, UserId));
}
[HttpDelete("tile")]
public async Task Delete(string sessionName, [FromBody] Tile tile)
{
await _mediator.Send(new DeleteTileCommand(sessionName, tile, UserId));
}
[HttpDelete("overlay")]
public async Task Delete(string sessionName, [FromBody] Overlay overlay)
{
await _mediator.Send(new DeleteOverlayCommand(sessionName, overlay, UserId));
}
[HttpDelete("wall")]
public async Task Delete(string sessionName, [FromBody] Wall wall)
{
await _mediator.Send(new DeleteWallCommand(sessionName, wall, UserId));
}
}
}

View file

@ -0,0 +1,136 @@
using System;
using System.Collections.Generic;
using Microsoft.AspNetCore.Mvc;
using AutoMapper;
using System.IdentityModel.Tokens.Jwt;
using Microsoft.Extensions.Options;
using System.Text;
using Microsoft.IdentityModel.Tokens;
using System.Security.Claims;
using Microsoft.AspNetCore.Authorization;
using Sledgemapper.Services;
using Sledgemapper.Entities;
using Sledgemapper.Models.Users;
using Sledgemapper.Helpers;
namespace Sledgemapper.Controllers
{
[Authorize]
[ApiController]
[Route("[controller]")]
public class UsersController : ControllerBase
{
private IUserService _userService;
private IMapper _mapper;
private readonly AppSettings _appSettings;
public UsersController(
IUserService userService,
IMapper mapper,
IOptions<AppSettings> appSettings)
{
_userService = userService;
_mapper = mapper;
_appSettings = appSettings.Value;
}
[AllowAnonymous]
[HttpPost("authenticate")]
public IActionResult Authenticate([FromBody]AuthenticateModel model)
{
var user = _userService.Authenticate(model.Username, model.Password);
if (user == null)
return BadRequest(new { message = "Username or password is incorrect" });
var tokenHandler = new JwtSecurityTokenHandler();
var key = Encoding.ASCII.GetBytes(_appSettings.Secret);
var tokenDescriptor = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(new Claim[]
{
new Claim(ClaimTypes.Name, user.Id.ToString())
}),
Expires = DateTime.UtcNow.AddDays(7),
SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature)
};
var token = tokenHandler.CreateToken(tokenDescriptor);
var tokenString = tokenHandler.WriteToken(token);
// return basic user info and authentication token
return Ok(new
{
Id = user.Id,
Username = user.Username,
FirstName = user.FirstName,
LastName = user.LastName,
Initials = user.Initials,
Token = tokenString
});
}
[AllowAnonymous]
[HttpPost("register")]
public IActionResult Register([FromBody]RegisterModel model)
{
// map model to entity
var user = _mapper.Map<User>(model);
try
{
// create user
_userService.Create(user, model.Password);
return Ok();
}
catch (AppException ex)
{
// return error message if there was an exception
return BadRequest(new { message = ex.Message });
}
}
[HttpGet]
public IActionResult GetAll()
{
var users = _userService.GetAll();
var model = _mapper.Map<IList<UserModel>>(users);
return Ok(model);
}
[HttpGet("{id}")]
public IActionResult GetById(int id)
{
var user = _userService.GetById(id);
var model = _mapper.Map<UserModel>(user);
return Ok(model);
}
[HttpPut("{id}")]
public IActionResult Update(int id, [FromBody]UpdateModel model)
{
// map model to entity and set id
var user = _mapper.Map<User>(model);
user.Id = id;
try
{
// update user
_userService.Update(user, model.Password);
return Ok();
}
catch (AppException ex)
{
// return error message if there was an exception
return BadRequest(new { message = ex.Message });
}
}
[HttpDelete("{id}")]
public IActionResult Delete(int id)
{
_userService.Delete(id);
return Ok();
}
}
}

View file

@ -0,0 +1,81 @@
using Microsoft.EntityFrameworkCore;
using Sledgemapper.Api.Models;
namespace Sledgemapper.Api.Data
{
public static class DbInitializer
{
public static void Initialize(MyDbContext context)
{
context.Database.EnsureCreated();
}
}
public class MyDbContext : DbContext
{
public DbSet<MapLog> MapLogs { get; set; }
public DbSet<Session> Sessions { get; set; }
public DbSet<UserConnection> UserConnections { get; set; }
public DbSet<SessionUser> SessionUsers { get; set; }
public DbSet<Snapshot> Snapshots { get; set; }
public MyDbContext(DbContextOptions options) : base(options)
{
}
// protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
// {
// // optionsBuilder.
// // options.MigrationsAssembly(Assembly.GetExecutingAssembly().FullName);
// // optionsBuilder.UseSqlite("Filename=SledgemapperDatabase.db", options =>
// // {
// // options.MigrationsAssembly(Assembly.GetExecutingAssembly().FullName);
// // });
// optionsBuilder.UseSqlite("Filename=MyDatabase.db").UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking);
// base.OnConfiguring(optionsBuilder);
// }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// Map table names
modelBuilder.Entity<MapLog>().ToTable("MapLog", "dbo");
modelBuilder.Entity<MapLog>(entity =>
{
entity.HasKey(e => e.MapLogId);
});
modelBuilder.Entity<Session>().ToTable("Session", "dbo");
modelBuilder.Entity<Session>(entity =>
{
entity.HasKey(e => e.SessionId);
entity.HasIndex(e => e.SessionName).IsUnique();
});
modelBuilder.Entity<UserConnection>().ToTable("UserConnection", "dbo");
modelBuilder.Entity<UserConnection>(entity =>
{
entity.HasKey(e => e.UserConnectionId);
entity.HasIndex(e => e.UserId);
});
modelBuilder.Entity<SessionUser>().ToTable("SessionUser", "dbo");
modelBuilder.Entity<SessionUser>(entity =>
{
entity.HasKey(e => e.SessionUserId);
entity.HasIndex(e => e.SessionId);
});
modelBuilder.Entity<Snapshot>().ToTable("Snapshot", "dbo");
modelBuilder.Entity<Snapshot>(entity =>
{
entity.HasKey(e => e.SnapshotId);
});
base.OnModelCreating(modelBuilder);
}
}
}

View file

@ -1,20 +0,0 @@
# https://hub.docker.com/_/microsoft-dotnet-core
FROM mcr.microsoft.com/dotnet/sdk:5.0 AS build
WORKDIR /source
# copy csproj and restore as distinct layers
#COPY *.sln .
COPY SignalRChat/*.csproj ./aspnetapp/
WORKDIR /source/aspnetapp
RUN dotnet restore
# copy everything else and build app
COPY SignalRChat/. ./aspnetapp/
WORKDIR /source/aspnetapp
RUN dotnet publish -c release -o /app --no-restore
# final stage/image
FROM mcr.microsoft.com/dotnet/aspnet:5.0
WORKDIR /app
COPY --from=build /app ./
ENTRYPOINT ["dotnet", "SignalRChat.dll"]

View file

@ -1,27 +0,0 @@
# https://hub.docker.com/_/microsoft-dotnet-core
FROM mcr.microsoft.com/dotnet/core/sdk:3.1 AS build
WORKDIR /source
# copy csproj and restore as distinct layers
COPY *.sln .
COPY aspnetapp/*.csproj ./aspnetapp/
RUN dotnet restore -r linux-musl-arm64
# copy everything else and build app
COPY aspnetapp/. ./aspnetapp/
WORKDIR /source/aspnetapp
RUN dotnet publish -c release -o /app -r linux-musl-arm64 --self-contained false --no-restore
# final stage/image
FROM mcr.microsoft.com/dotnet/core/aspnet:3.1-alpine-arm64v8
WORKDIR /app
COPY --from=build /app ./
# See: https://github.com/dotnet/announcements/issues/20
# Uncomment to enable globalization APIs (or delete)
#ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT false
#RUN apk add --no-cache icu-libs
#ENV LC_ALL en_US.UTF-8
#ENV LANG en_US.UTF-8
ENTRYPOINT ["./aspnetapp"]

View file

@ -1,27 +0,0 @@
# https://hub.docker.com/_/microsoft-dotnet-core
FROM mcr.microsoft.com/dotnet/core/sdk:3.1 AS build
WORKDIR /source
# copy csproj and restore as distinct layers
COPY *.sln .
COPY aspnetapp/*.csproj ./aspnetapp/
RUN dotnet restore -r linux-musl-x64
# copy everything else and build app
COPY aspnetapp/. ./aspnetapp/
WORKDIR /source/aspnetapp
RUN dotnet publish -c release -o /app -r linux-musl-x64 --self-contained false --no-restore
# final stage/image
FROM mcr.microsoft.com/dotnet/core/aspnet:3.1-alpine
WORKDIR /app
COPY --from=build /app ./
# See: https://github.com/dotnet/announcements/issues/20
# Uncomment to enable globalization APIs (or delete)
#ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT false
#RUN apk add --no-cache icu-libs
#ENV LC_ALL en_US.UTF-8
#ENV LANG en_US.UTF-8
ENTRYPOINT ["./aspnetapp"]

View file

@ -1,27 +0,0 @@
# https://hub.docker.com/_/microsoft-dotnet-core
FROM mcr.microsoft.com/dotnet/core/sdk:3.1-alpine AS build
WORKDIR /source
# copy csproj and restore as distinct layers
COPY *.sln .
COPY aspnetapp/*.csproj ./aspnetapp/
RUN dotnet restore -r linux-musl-x64
# copy everything else and build app
COPY aspnetapp/. ./aspnetapp/
WORKDIR /source/aspnetapp
RUN dotnet publish -c release -o /app -r linux-musl-x64 --self-contained true --no-restore /p:PublishTrimmed=true /p:PublishReadyToRun=true
# final stage/image
FROM mcr.microsoft.com/dotnet/core/runtime-deps:3.1-alpine
WORKDIR /app
COPY --from=build /app ./
# See: https://github.com/dotnet/announcements/issues/20
# Uncomment to enable globalization APIs (or delete)
#ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT false
#RUN apk add --no-cache icu-libs
#ENV LC_ALL en_US.UTF-8
#ENV LANG en_US.UTF-8
ENTRYPOINT ["./aspnetapp"]

View file

@ -1,19 +0,0 @@
# https://hub.docker.com/_/microsoft-dotnet-core
FROM mcr.microsoft.com/dotnet/core/sdk:3.1 AS build
WORKDIR /source
# copy csproj and restore as distinct layers
COPY *.sln .
COPY aspnetapp/*.csproj ./aspnetapp/
RUN dotnet restore -r linux-arm
# copy everything else and build app
COPY aspnetapp/. ./aspnetapp/
WORKDIR /source/aspnetapp
RUN dotnet publish -c release -o /app -r linux-arm --self-contained false --no-restore
# final stage/image
FROM mcr.microsoft.com/dotnet/core/aspnet:3.1-buster-slim-arm32v7
WORKDIR /app
COPY --from=build /app ./
ENTRYPOINT ["./aspnetapp"]

View file

@ -1,19 +0,0 @@
# https://hub.docker.com/_/microsoft-dotnet-core
FROM mcr.microsoft.com/dotnet/core/sdk:3.1 AS build
WORKDIR /source
# copy csproj and restore as distinct layers
COPY *.sln .
COPY aspnetapp/*.csproj ./aspnetapp/
RUN dotnet restore -r linux-arm64
# copy everything else and build app
COPY aspnetapp/. ./aspnetapp/
WORKDIR /source/aspnetapp
RUN dotnet publish -c release -o /app -r linux-arm64 --self-contained false --no-restore
# final stage/image
FROM mcr.microsoft.com/dotnet/core/aspnet:3.1-buster-slim-arm64v8
WORKDIR /app
COPY --from=build /app ./
ENTRYPOINT ["./aspnetapp"]

View file

@ -1,19 +0,0 @@
# https://hub.docker.com/_/microsoft-dotnet-core
FROM mcr.microsoft.com/dotnet/core/sdk:3.1 AS build
WORKDIR /source
# copy csproj and restore as distinct layers
COPY *.sln .
COPY aspnetapp/*.csproj ./aspnetapp/
RUN dotnet restore -r linux-x64
# copy everything else and build app
COPY aspnetapp/. ./aspnetapp/
WORKDIR /source/aspnetapp
RUN dotnet publish -c release -o /app -r linux-x64 --self-contained false --no-restore
# final stage/image
FROM mcr.microsoft.com/dotnet/core/aspnet:3.1-buster-slim
WORKDIR /app
COPY --from=build /app ./
ENTRYPOINT ["./aspnetapp"]

View file

@ -1,19 +0,0 @@
# https://hub.docker.com/_/microsoft-dotnet-core
FROM mcr.microsoft.com/dotnet/core/sdk:3.1 AS build
WORKDIR /source
# copy csproj and restore as distinct layers
COPY *.sln .
COPY aspnetapp/*.csproj ./aspnetapp/
RUN dotnet restore -r linux-x64
# copy everything else and build app
COPY aspnetapp/. ./aspnetapp/
WORKDIR /source/aspnetapp
RUN dotnet publish -c release -o /app -r linux-x64 --self-contained true --no-restore /p:PublishTrimmed=true /p:PublishReadyToRun=true
# final stage/image
FROM mcr.microsoft.com/dotnet/core/runtime-deps:3.1-buster-slim
WORKDIR /app
COPY --from=build /app ./
ENTRYPOINT ["./aspnetapp"]

View file

@ -1,20 +0,0 @@
# https://hub.docker.com/_/microsoft-dotnet-core
FROM mcr.microsoft.com/dotnet/core/sdk:3.1 AS build
WORKDIR /source
# copy csproj and restore as distinct layers
COPY *.sln .
COPY aspnetapp/*.csproj ./aspnetapp/
RUN dotnet restore -r win-x64
# copy everything else and build app
COPY aspnetapp/. ./aspnetapp/
WORKDIR /source/aspnetapp
RUN dotnet publish -c release -o /app -r win-x64 --self-contained false --no-restore
# final stage/image
# Uses the 2009 release; 2004, 1909, 1903, and 1809 are other choices
FROM mcr.microsoft.com/dotnet/core/aspnet:3.1-nanoserver-2009 AS runtime
WORKDIR /app
COPY --from=build /app ./
ENTRYPOINT ["aspnetapp"]

View file

@ -1,24 +0,0 @@
# https://hub.docker.com/_/microsoft-dotnet-core
FROM mcr.microsoft.com/dotnet/core/sdk:3.1 AS build
WORKDIR /source
# copy csproj and restore as distinct layers
COPY *.sln .
COPY aspnetapp/*.csproj ./aspnetapp/
RUN dotnet restore -r win-x64
# copy everything else and build app
COPY aspnetapp/. ./aspnetapp/
WORKDIR /source/aspnetapp
RUN dotnet publish -c release -o /app -r win-x64 --self-contained true --no-restore /p:PublishTrimmed=true /p:PublishReadyToRun=true
# final stage/image
# Uses the 2009 release; 2004, 1909, 1903, and 1809 are other choices
FROM mcr.microsoft.com/windows/nanoserver:2009 AS runtime
WORKDIR /app
COPY --from=build /app ./
# Configure web servers to bind to port 80 when present
ENV ASPNETCORE_URLS=http://+:80
ENTRYPOINT ["aspnetapp"]

View file

@ -1,19 +0,0 @@
# https://hub.docker.com/_/microsoft-dotnet-core
FROM mcr.microsoft.com/dotnet/core/sdk:3.1 AS build
WORKDIR /source
# copy csproj and restore as distinct layers
COPY *.sln .
COPY aspnetapp/*.csproj ./aspnetapp/
RUN dotnet restore -r linux-x64
# copy everything else and build app
COPY aspnetapp/. ./aspnetapp/
WORKDIR /source/aspnetapp
RUN dotnet publish -c release -o /app -r linux-x64 --self-contained false --no-restore
# final stage/image
FROM mcr.microsoft.com/dotnet/core/aspnet:3.1-bionic
WORKDIR /app
COPY --from=build /app ./
ENTRYPOINT ["./aspnetapp"]

View file

@ -0,0 +1,13 @@
namespace Sledgemapper.Entities
{
public class User
{
public int Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public string Username { get; set; }
public string Initials { get; set; }
public byte[] PasswordHash { get; set; }
public byte[] PasswordSalt { get; set; }
}
}

View file

@ -0,0 +1,41 @@
using MediatR;
using Sledgemapper.Api.Commands;
using Sledgemapper.Api.Data;
using Sledgemapper.Shared.Entities;
using Sledgemapper.Api.Notifications;
using System.Linq;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
namespace Sledgemapper.Api.Handlers
{
public class DeleteOverlayCommandHandler : IRequestHandler<DeleteOverlayCommand, bool>
{
private readonly MyDbContext _dbcontext;
private readonly IMediator _mediator;
public DeleteOverlayCommandHandler(IMediator mediator, MyDbContext dbcontext) { _dbcontext = dbcontext; _mediator = mediator; }
public async Task<bool> Handle(DeleteOverlayCommand notification, CancellationToken cancellationToken)
{
var jsonString = JsonSerializer.Serialize<Overlay>(notification.Overlay);
var session = _dbcontext.Sessions.First(m=>m.SessionName== notification.SessionName);
_dbcontext.MapLogs.Add(new Sledgemapper.Api.Models.MapLog
{
Operation = "D",
SessionId = session.SessionId,
Type = "O",
Timestamp = notification.Timestamp,
Object = jsonString,
UserId=notification.UserId
});
await _dbcontext.SaveChangesAsync();
await _mediator.Publish(new DeleteOverlayNotification(session, notification.Overlay, notification.UserId));
return true;
}
}
}

View file

@ -0,0 +1,40 @@
using MediatR;
using Sledgemapper.Api.Commands;
using Sledgemapper.Api.Notifications;
using Sledgemapper.Api.Data;
using Sledgemapper.Shared.Entities;
using System.Linq;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
namespace Sledgemapper.Api.Handlers
{
public class DeleteTileCommandHandler : IRequestHandler<DeleteTileCommand,bool>
{
private readonly MyDbContext _dbcontext;
private readonly IMediator _mediator;
public DeleteTileCommandHandler(IMediator mediator, MyDbContext dbcontext) { _dbcontext = dbcontext; _mediator = mediator; }
public async Task<bool> Handle(DeleteTileCommand notification, CancellationToken cancellationToken)
{
var jsonString = JsonSerializer.Serialize<Tile>(notification.Tile);
var session = _dbcontext.Sessions.First(m=>m.SessionName== notification.SessionName);
_dbcontext.MapLogs.Add(new Sledgemapper.Api.Models.MapLog
{
Operation = "D",
SessionId = session.SessionId,
Type = "T",
Timestamp = notification.Timestamp,
Object = jsonString,
UserId = notification.UserId
});
await _dbcontext.SaveChangesAsync();
await _mediator.Publish(new DeleteTileNotification(session, notification.Tile, notification.UserId));
return true;
}
}
}

View file

@ -0,0 +1,43 @@
using MediatR;
using Sledgemapper.Api.Data;
using Sledgemapper.Api.Commands;
using Sledgemapper.Api.Notifications;
using Sledgemapper.Shared.Entities;
using System.Linq;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
namespace Sledgemapper.Api.Handlers
{
public class DeleteWallCommandHandler : IRequestHandler<DeleteWallCommand, bool>
{
private readonly MyDbContext _dbcontext;
private readonly IMediator _mediator;
public DeleteWallCommandHandler(IMediator mediator, MyDbContext dbcontext) { _dbcontext = dbcontext; _mediator = mediator; }
public async Task<bool> Handle(DeleteWallCommand notification, CancellationToken cancellationToken)
{
var jsonString = JsonSerializer.Serialize<Wall>(notification.Wall);
var session = _dbcontext.Sessions.First(m => m.SessionName == notification.SessionName);
_dbcontext.MapLogs.Add(new Sledgemapper.Api.Models.MapLog
{
Operation = "D",
SessionId = session.SessionId,
Type = "W",
Timestamp = notification.Timestamp,
Object = jsonString,
UserId = notification.UserId
});
await _dbcontext.SaveChangesAsync();
await _mediator.Publish(new DeleteWallNotification(session, notification.Wall, notification.UserId));
return true;
}
}
}

View file

@ -0,0 +1,93 @@
using MediatR;
using Sledgemapper.Api.Data;
using Sledgemapper.Shared.Entities;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using System.Linq;
using Sledgemapper.Api.Models;
namespace Sledgemapper.Api.Commands
{
public class GetMapSnapshotCommandHandler : IRequestHandler<GetMapSnapshotCommand, Sledgemapper.Shared.Entities.Session>
{
private readonly MyDbContext _dbcontext;
public GetMapSnapshotCommandHandler(MyDbContext dbcontext) { _dbcontext = dbcontext; }
public async Task<Sledgemapper.Shared.Entities.Session> Handle(GetMapSnapshotCommand notification, CancellationToken cancellationToken)
{
Snapshot snapshot;
double timestamp;
Sledgemapper.Shared.Entities.Session mapSession;
var session = _dbcontext.Sessions.First(m => m.SessionName == notification.SessionName);
snapshot = _dbcontext.Snapshots.OrderByDescending(s => s.Timestamp).FirstOrDefault(m => m.SessionId == session.SessionId);
if (snapshot is null)
{
timestamp = 0;
mapSession = new Shared.Entities.Session();
}
else
{
mapSession = JsonSerializer.Deserialize<Sledgemapper.Shared.Entities.Session>(snapshot.Object);
timestamp = snapshot.Timestamp;
}
var mapUpdates = _dbcontext.MapLogs.Where(m => m.SessionId == session.SessionId && m.Timestamp > timestamp).OrderBy(m => m.Timestamp).ToList();
foreach (var mapUpdate in mapUpdates)
{
if (mapUpdate.Operation == "N")
{
switch (mapUpdate.Type)
{
case "T":
var tile = JsonSerializer.Deserialize<Tile>(mapUpdate.Object);
mapSession.NewTile(tile, tile.ID);
break;
case "W":
var wall = JsonSerializer.Deserialize<Wall>(mapUpdate.Object);
mapSession.NewWall(wall, wall.ID);
break;
case "O":
var overlay = JsonSerializer.Deserialize<Overlay>(mapUpdate.Object);
mapSession.NewOverlay(overlay, overlay.ID);
break;
}
}
else if (mapUpdate.Operation == "D")
{
switch (mapUpdate.Type)
{
case "T":
var tile = JsonSerializer.Deserialize<Tile>(mapUpdate.Object);
mapSession.DeleteTile(tile);
break;
case "W":
var wall = JsonSerializer.Deserialize<Wall>(mapUpdate.Object);
mapSession.DeleteWall(wall);
break;
case "O":
var overlay = JsonSerializer.Deserialize<Overlay>(mapUpdate.Object);
mapSession.DeleteOverlay(overlay);
break;
}
}
}
if (mapUpdates.Any())
{
var newSnapshot = new Snapshot
{
SessionId = session.SessionId,
Timestamp = mapUpdates.Max(mapSession => mapSession.Timestamp),
Object = JsonSerializer.Serialize<Sledgemapper.Shared.Entities.Session>(mapSession)
};
await _dbcontext.Snapshots.AddAsync(newSnapshot);
await _dbcontext.SaveChangesAsync();
}
return mapSession;
}
}
}

View file

@ -0,0 +1,39 @@
using MediatR;
using Sledgemapper.Api.Commands;
using Sledgemapper.Api.Notifications;
using Sledgemapper.Api.Data;
using Sledgemapper.Shared.Entities;
using System.Linq;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
namespace Sledgemapper.Api.Handlers
{
public class NewOverlayCommandHandler : IRequestHandler<NewOverlayCommand, bool>
{
private readonly MyDbContext _dbcontext;
private readonly IMediator _mediator;
public NewOverlayCommandHandler(IMediator mediator, MyDbContext dbcontext) { _dbcontext = dbcontext; _mediator = mediator; }
public async Task<bool> Handle(NewOverlayCommand notification, CancellationToken cancellationToken)
{
var jsonString = JsonSerializer.Serialize<Overlay>(notification.Overlay);
var session = _dbcontext.Sessions.First(m => m.SessionName == notification.SessionName);
_dbcontext.MapLogs.Add(new Sledgemapper.Api.Models.MapLog
{
Operation = "N",
SessionId = session.SessionId,
Type = "O",
Timestamp = notification.Timestamp,
Object = jsonString,
UserId = notification.UserId,
});
await _dbcontext.SaveChangesAsync();
await _mediator.Publish(new NewOverlayNotification(session, notification.Overlay, notification.UserId));
return true;
}
}
}

View file

@ -0,0 +1,37 @@
using MediatR;
using Sledgemapper.Api.Data;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using System.Linq;
namespace Sledgemapper.Api.Commands
{
public class NewSnapshotCommandHandler : IRequestHandler<NewSnapshotCommand, bool>
{
private readonly MyDbContext _dbcontext;
private readonly IMediator _mediator;
public NewSnapshotCommandHandler(IMediator mediator, MyDbContext dbcontext) { _dbcontext = dbcontext; _mediator = mediator; }
public async Task<bool> Handle(NewSnapshotCommand notification, CancellationToken cancellationToken)
{
var session = _dbcontext.Sessions.First(m => m.SessionName == notification.SessionName);
var newSnapshot = new Sledgemapper.Api.Models.Snapshot{
SessionId=session.SessionId,
Timestamp=notification.Timestamp,
Object = JsonSerializer.Serialize<Sledgemapper.Shared.Entities.Session>(notification.Session)
};
await _dbcontext.Snapshots.AddAsync(newSnapshot);
await _dbcontext.SaveChangesAsync();
return true;
}
}
}

View file

@ -0,0 +1,41 @@
using MediatR;
using Sledgemapper.Api.Data;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Sledgemapper.Api.Notifications;
using System.Linq;
using Sledgemapper.Shared.Entities;
namespace Sledgemapper.Api.Commands
{
public class NewTileCommandHandler : IRequestHandler<NewTileCommand, bool>
{
private readonly MyDbContext _dbcontext;
private readonly IMediator _mediator;
public NewTileCommandHandler(IMediator mediator, MyDbContext dbcontext) { _dbcontext = dbcontext; _mediator = mediator; }
public async Task<bool> Handle(NewTileCommand notification, CancellationToken cancellationToken)
{
var jsonString = JsonSerializer.Serialize<Tile>(notification.Tile);
var session = _dbcontext.Sessions.First(m => m.SessionName == notification.SessionName);
_dbcontext.MapLogs.Add(new Sledgemapper.Api.Models.MapLog
{
Operation = "N",
SessionId = session.SessionId,
Type = "T",
Timestamp = notification.Timestamp,
Object = jsonString,
UserId = notification.UserId
});
await _dbcontext.SaveChangesAsync();
await _mediator.Publish(new NewTileNotification(session, notification.Tile, notification.UserId));
return true;
}
}
}

View file

@ -0,0 +1,39 @@
using MediatR;
using Sledgemapper.Api.Data;
using Sledgemapper.Shared.Entities;
using Sledgemapper.Api.Commands;
using Sledgemapper.Api.Notifications;
using System.Linq;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
namespace Sledgemapper.Api.Handlers
{
public class NewWallCommandHandler : IRequestHandler<NewWallCommand, bool>
{
private readonly MyDbContext _dbcontext;
private readonly IMediator _mediator;
public NewWallCommandHandler(IMediator mediator, MyDbContext dbcontext) { _dbcontext = dbcontext; _mediator = mediator; }
public async Task<bool> Handle(NewWallCommand notification, CancellationToken cancellationToken)
{
var jsonString = JsonSerializer.Serialize<Wall>(notification.Wall);
var session = _dbcontext.Sessions.First(m => m.SessionName == notification.SessionName);
_dbcontext.MapLogs.Add(new Sledgemapper.Api.Models.MapLog
{
Operation = "N",
SessionId = session.SessionId,
Type = "W",
Timestamp = notification.Timestamp,
Object = jsonString,
UserId = notification.UserId,
});
await _dbcontext.SaveChangesAsync();
await _mediator.Publish(new NewWallNotification(session, notification.Wall, notification.UserId));
return true;
}
}
}

View file

@ -0,0 +1,22 @@
using MediatR;
using Microsoft.AspNetCore.SignalR;
using SignalRChat.Hubs;
using Sledgemapper.Api.Notifications;
using Sledgemapper.Clients;
using System.Threading;
using System.Threading.Tasks;
namespace Sledgemapper.Api.Handlers
{
public class SendDeleteOverlayMessage : INotificationHandler<DeleteOverlayNotification>
{
private readonly IHubContext<SledgemapperHub, ISledgemapperClient> _hub;
public SendDeleteOverlayMessage(IHubContext<SledgemapperHub, ISledgemapperClient> hub) => _hub = hub;
public async Task Handle(DeleteOverlayNotification notification, CancellationToken cancellationToken)
{
await _hub.Clients.Groups(notification.Session.SessionName).DeleteOverlay(notification.Overlay);
}
}
}

View file

@ -0,0 +1,22 @@
using MediatR;
using Microsoft.AspNetCore.SignalR;
using SignalRChat.Hubs;
using Sledgemapper.Api.Notifications;
using Sledgemapper.Clients;
using System.Threading;
using System.Threading.Tasks;
namespace Sledgemapper.Api.Handlers
{
public class SendDeleteTileMessage : INotificationHandler<DeleteTileNotification>
{
private readonly IHubContext<SledgemapperHub, ISledgemapperClient> _hub;
public SendDeleteTileMessage(IHubContext<SledgemapperHub, ISledgemapperClient> hub) => _hub = hub;
public async Task Handle(DeleteTileNotification notification, CancellationToken cancellationToken)
{
await _hub.Clients.Groups(notification.Session.SessionName).DeleteTile(notification.Tile);
}
}
}

View file

@ -0,0 +1,22 @@
using MediatR;
using Microsoft.AspNetCore.SignalR;
using SignalRChat.Hubs;
using Sledgemapper.Api.Notifications;
using Sledgemapper.Clients;
using System.Threading;
using System.Threading.Tasks;
namespace Sledgemapper.Api.Handlers
{
public class SendDeleteWallMessage : INotificationHandler<DeleteWallNotification>
{
private readonly IHubContext<SledgemapperHub, ISledgemapperClient> _hub;
public SendDeleteWallMessage(IHubContext<SledgemapperHub, ISledgemapperClient> hub) => _hub = hub;
public async Task Handle(DeleteWallNotification notification, CancellationToken cancellationToken)
{
await _hub.Clients.Groups(notification.Session.SessionName).DeleteWall(notification.Wall);
}
}
}

View file

@ -0,0 +1,22 @@
using MediatR;
using Microsoft.AspNetCore.SignalR;
using SignalRChat.Hubs;
using Sledgemapper.Api.Notifications;
using Sledgemapper.Clients;
using System.Threading;
using System.Threading.Tasks;
namespace Sledgemapper.Api.Handlers
{
public class SendNewOverlayMessage : INotificationHandler<NewOverlayNotification>
{
private readonly IHubContext<SledgemapperHub, ISledgemapperClient> _hub;
public SendNewOverlayMessage(IHubContext<SledgemapperHub, ISledgemapperClient> hub) => _hub = hub;
public async Task Handle(NewOverlayNotification notification, CancellationToken cancellationToken)
{
await _hub.Clients.Groups(notification.Session.SessionName).NewOverlay(notification.Overlay);
}
}
}

View file

@ -0,0 +1,23 @@
using MediatR;
using Microsoft.AspNetCore.SignalR;
using SignalRChat.Hubs;
using Sledgemapper.Api.Notifications;
using Sledgemapper.Clients;
using System.Threading;
using System.Threading.Tasks;
namespace Sledgemapper.Api.Handlers
{
public class SendNewTileMessage : INotificationHandler<NewTileNotification>
{
private readonly IHubContext<SledgemapperHub, ISledgemapperClient> _hub;
public SendNewTileMessage(IHubContext<SledgemapperHub, ISledgemapperClient> hub) => _hub = hub;
public async Task Handle(NewTileNotification notification, CancellationToken cancellationToken)
{
await _hub.Clients.Groups(notification.Session.SessionName).NewTile(notification.Tile);
}
}
}

View file

@ -0,0 +1,22 @@
using MediatR;
using Microsoft.AspNetCore.SignalR;
using SignalRChat.Hubs;
using Sledgemapper.Api.Notifications;
using Sledgemapper.Clients;
using System.Threading;
using System.Threading.Tasks;
namespace Sledgemapper.Api.Handlers
{
public class SendNewWallMessage : INotificationHandler<NewWallNotification>
{
private readonly IHubContext<SledgemapperHub, ISledgemapperClient> _hub;
public SendNewWallMessage(IHubContext<SledgemapperHub, ISledgemapperClient> hub) => _hub = hub;
public async Task Handle(NewWallNotification notification, CancellationToken cancellationToken)
{
await _hub.Clients.Groups(notification.Session.SessionName).NewWall(notification.Wall);
}
}
}

View file

@ -0,0 +1,32 @@
using MediatR;
using Sledgemapper.Api.Commands;
using Sledgemapper.Api.Data;
using Sledgemapper.Api.Models;
using System.Threading;
using System.Threading.Tasks;
namespace Sledgemapper.Api.Handlers
{
public class StartNewSessionHandler : IRequestHandler<NewSessionCommand, bool>
{
private readonly IMediator _mediator;
private readonly MyDbContext _dbcontext;
public StartNewSessionHandler(IMediator mediator, MyDbContext dbcontext)
{
_mediator = mediator;
_dbcontext = dbcontext;
}
public async Task<bool> Handle(NewSessionCommand notification, CancellationToken cancellationToken)
{
_dbcontext.Sessions.Add(new Session
{
SessionName = notification.SessionName,
OwnerUserId = notification.UserId
});
await _dbcontext.SaveChangesAsync();
return true;
}
}
}

View file

@ -0,0 +1,19 @@
using System;
using System.Globalization;
namespace Sledgemapper.Helpers
{
// Custom exception class for throwing application specific exceptions (e.g. for validation)
// that can be caught and handled within the application
public class AppException : Exception
{
public AppException() : base() {}
public AppException(string message) : base(message) { }
public AppException(string message, params object[] args)
: base(string.Format(CultureInfo.CurrentCulture, message, args))
{
}
}
}

View file

@ -0,0 +1,7 @@
namespace Sledgemapper.Helpers
{
public class AppSettings
{
public string Secret { get; set; }
}
}

View file

@ -0,0 +1,16 @@
using AutoMapper;
using Sledgemapper.Entities;
using Sledgemapper.Models.Users;
namespace Sledgemapper.Helpers
{
public class AutoMapperProfile : Profile
{
public AutoMapperProfile()
{
CreateMap<User, UserModel>();
CreateMap<RegisterModel, User>();
CreateMap<UpdateModel, User>();
}
}
}

View file

@ -0,0 +1,24 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Sledgemapper.Entities;
namespace Sledgemapper.Helpers
{
public class DataContext : DbContext
{
protected readonly IConfiguration Configuration;
public DataContext(IConfiguration configuration)
{
Configuration = configuration;
}
protected override void OnConfiguring(DbContextOptionsBuilder options)
{
// connect to sql server database
options.UseSqlServer(Configuration.GetConnectionString("WebApiDatabase"));
}
public DbSet<User> Users { get; set; }
}
}

View file

@ -0,0 +1,16 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
namespace Sledgemapper.Helpers
{
public class SqliteDataContext : DataContext
{
public SqliteDataContext(IConfiguration configuration) : base(configuration) { }
protected override void OnConfiguring(DbContextOptionsBuilder options)
{
// connect to sqlite database
options.UseSqlite(Configuration.GetConnectionString("WebApiDatabase"));
}
}
}

View file

@ -0,0 +1,150 @@
using Microsoft.AspNetCore.SignalR;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Collections.Concurrent;
using Sledgemapper.Shared.Entities;
using Sledgemapper.Clients;
using System;
using Sledgemapper.Api.Data;
using Microsoft.AspNetCore.Authorization;
using Sledgemapper.Api.Models;
using Sledgemapper.Helpers;
namespace SignalRChat.Hubs
{
[Authorize]
public class SledgemapperHub : Hub<ISledgemapperClient>
{
private static readonly ConcurrentDictionary<int, string> UserColors = new();
private readonly MyDbContext _dbContext;
private readonly DataContext _datacontext;
public SledgemapperHub(MyDbContext dbContext, DataContext datacontext)
{
_dbContext = dbContext;
_datacontext = datacontext;
}
// other colors
// #cca300, #20f200, #004011, #00e6d6, #005c73, #0057d9, #d900ca, #660029, #d9003a
// private static Dictionary<string, Session> _sessions = new Dictionary<string, Session>();
public List<string> Colors = new()
{
"#e6194B",
"#f58231",
"#3cb44b",
"#000075",
"#911eb4",
"#800000",
"#808000",
"#469990"
};
public async Task NewTile(string sessionName, Tile tile)
{
await Clients.Group(sessionName).NewTile(tile);
}
public async Task NewWall(string sessionName, Wall tile)
{
await Clients.Group(sessionName).NewWall(tile);
}
public async Task NewOverlay(string sessionName, Overlay tile)
{
await Clients.Group(sessionName).NewOverlay(tile);
}
public async Task DeleteTile(string sessionName, Tile tile)
{
await Clients.Group(sessionName).DeleteTile(tile);
}
public async Task DeleteWall(string sessionName, Wall tile)
{
await Clients.Group(sessionName).DeleteWall(tile);
}
public async Task DeleteOverlay(string sessionName, Overlay tile)
{
await Clients.Group(sessionName).DeleteOverlay(tile);
}
public async Task<Sledgemapper.Shared.Entities.Session> JoinSession(string sessionName)
{
var session = _dbContext.Sessions.FirstOrDefault(s => s.SessionName == sessionName);
var userId = int.Parse(Context.User.Identity.Name);
if (session != null)
{
var userSession = new SessionUser { SessionId = session.SessionId, UserId = userId };
_dbContext.SessionUsers.Add(userSession);
await _dbContext.SaveChangesAsync();
await Groups.AddToGroupAsync(Context.ConnectionId, session.SessionName);
var user = _datacontext.Users.First(u => u.Id == userId);
var player = new Player { UserId = userId, Initials = user.Initials, Position = new Tile { X = 0, Y = 0 }, Color = UserColors[userId] };
await Clients.Group(session.SessionName).NewPlayer(player);
await Clients.Group(session.SessionName).RefreshPlayers();
var newSession = new Sledgemapper.Shared.Entities.Session
{
SessionName = session.SessionName,
SessionId = session.SessionId
};
return newSession;
}
else
{
return null;
}
}
public async Task UpdatePosition(string sessionName, int sessionId, Tile tile)
{
var userId = int.Parse(Context.User.Identity.Name);
var SessionUsers = _dbContext.SessionUsers.Where(m => m.SessionId == sessionId).OrderBy(m => m.UserId).ToList();
var user = _datacontext.Users.First(u => u.Id == userId);
var player = new Player { UserId = userId, Initials = user.Initials, Position = tile, Color = UserColors[userId]};
await Clients.Group(sessionName).PlayerUpdate(player);
}
public override async Task OnConnectedAsync()
{
var userId = int.Parse(Context.User.Identity.Name);
var userConnection = new UserConnection { ConnectionId = Context.ConnectionId, UserId = userId };
_dbContext.UserConnections.Add(userConnection);
await _dbContext.SaveChangesAsync();
var availableColor = Colors.Where(m => !UserColors.Values.Contains(m)).First();
UserColors.AddOrUpdate(userId, availableColor, (key, oldValue) => availableColor);
await base.OnConnectedAsync();
}
public override async Task OnDisconnectedAsync(Exception exception)
{
var userConnection = _dbContext.UserConnections.FirstOrDefault(m => m.ConnectionId == Context.ConnectionId);
var userId=userConnection.UserId;
if (userConnection != null)
{
_dbContext.UserConnections.Remove(userConnection);
}
var userSessions = _dbContext.SessionUsers.Where(m => m.UserId == userId).ToList();
{
foreach (var userSession in userSessions)
{
var session = _dbContext.Sessions.FirstOrDefault(m => m.SessionId == userSession.SessionId);
await Clients.Group(session.SessionName).PlayerUpdate(null); //send remove player
_dbContext.SessionUsers.Remove(userSession);
}
}
await _dbContext.SaveChangesAsync();
await base.OnDisconnectedAsync(exception);
}
}
}

21
Sledgemapper.Api/LICENSE Normal file
View file

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2019 Jason Watmore
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

Binary file not shown.

View file

@ -0,0 +1,53 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Sledgemapper.Helpers;
namespace Sledgemapper.Migrations.SqlServerMigrations
{
[DbContext(typeof(DataContext))]
[Migration("20200102103423_InitialCreate")]
partial class InitialCreate
{
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "3.1.0")
.HasAnnotation("Relational:MaxIdentifierLength", 128)
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
modelBuilder.Entity("Sledgemapper.Entities.User", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int")
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
b.Property<string>("FirstName")
.HasColumnType("nvarchar(max)");
b.Property<string>("LastName")
.HasColumnType("nvarchar(max)");
b.Property<byte[]>("PasswordHash")
.HasColumnType("varbinary(max)");
b.Property<byte[]>("PasswordSalt")
.HasColumnType("varbinary(max)");
b.Property<string>("Username")
.HasColumnType("nvarchar(max)");
b.HasKey("Id");
b.ToTable("Users");
});
#pragma warning restore 612, 618
}
}
}

View file

@ -0,0 +1,33 @@
using Microsoft.EntityFrameworkCore.Migrations;
namespace Sledgemapper.Migrations.SqlServerMigrations
{
public partial class InitialCreate : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "Users",
columns: table => new
{
Id = table.Column<int>(nullable: false)
.Annotation("SqlServer:Identity", "1, 1"),
FirstName = table.Column<string>(nullable: true),
LastName = table.Column<string>(nullable: true),
Username = table.Column<string>(nullable: true),
PasswordHash = table.Column<byte[]>(nullable: true),
PasswordSalt = table.Column<byte[]>(nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_Users", x => x.Id);
});
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "Users");
}
}
}

View file

@ -0,0 +1,51 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Sledgemapper.Helpers;
namespace Sledgemapper.Migrations.SqlServerMigrations
{
[DbContext(typeof(DataContext))]
partial class DataContextModelSnapshot : ModelSnapshot
{
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "3.1.0")
.HasAnnotation("Relational:MaxIdentifierLength", 128)
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
modelBuilder.Entity("Sledgemapper.Entities.User", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int")
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
b.Property<string>("FirstName")
.HasColumnType("nvarchar(max)");
b.Property<string>("LastName")
.HasColumnType("nvarchar(max)");
b.Property<byte[]>("PasswordHash")
.HasColumnType("varbinary(max)");
b.Property<byte[]>("PasswordSalt")
.HasColumnType("varbinary(max)");
b.Property<string>("Username")
.HasColumnType("nvarchar(max)");
b.HasKey("Id");
b.ToTable("Users");
});
#pragma warning restore 612, 618
}
}
}

View file

@ -0,0 +1,52 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Sledgemapper.Helpers;
namespace Sledgemapper.Migrations.SqliteMigrations
{
[DbContext(typeof(SqliteDataContext))]
[Migration("20200102102942_InitialCreate")]
partial class InitialCreate
{
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "3.1.0");
modelBuilder.Entity("Sledgemapper.Entities.User", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("FirstName")
.HasColumnType("TEXT");
b.Property<string>("LastName")
.HasColumnType("TEXT");
b.Property<string>("Initials")
.HasColumnType("TEXT");
b.Property<byte[]>("PasswordHash")
.HasColumnType("BLOB");
b.Property<byte[]>("PasswordSalt")
.HasColumnType("BLOB");
b.Property<string>("Username")
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("Users");
});
#pragma warning restore 612, 618
}
}
}

View file

@ -0,0 +1,34 @@
using Microsoft.EntityFrameworkCore.Migrations;
namespace Sledgemapper.Migrations.SqliteMigrations
{
public partial class InitialCreate : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "Users",
columns: table => new
{
Id = table.Column<int>(nullable: false)
.Annotation("Sqlite:Autoincrement", true),
FirstName = table.Column<string>(nullable: true),
LastName = table.Column<string>(nullable: true),
Username = table.Column<string>(nullable: true),
Initials = table.Column<string>(nullable: true),
PasswordHash = table.Column<byte[]>(nullable: true),
PasswordSalt = table.Column<byte[]>(nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_Users", x => x.Id);
});
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "Users");
}
}
}

View file

@ -0,0 +1,50 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Sledgemapper.Helpers;
namespace Sledgemapper.Migrations.SqliteMigrations
{
[DbContext(typeof(SqliteDataContext))]
partial class SqliteDataContextModelSnapshot : ModelSnapshot
{
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "3.1.0");
modelBuilder.Entity("Sledgemapper.Entities.User", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("FirstName")
.HasColumnType("TEXT");
b.Property<string>("LastName")
.HasColumnType("TEXT");
b.Property<string>("Initials")
.HasColumnType("TEXT");
b.Property<byte[]>("PasswordHash")
.HasColumnType("BLOB");
b.Property<byte[]>("PasswordSalt")
.HasColumnType("BLOB");
b.Property<string>("Username")
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("Users");
});
#pragma warning restore 612, 618
}
}
}

View file

@ -0,0 +1,13 @@
using System.ComponentModel.DataAnnotations;
namespace Sledgemapper.Models.Users
{
public class AuthenticateModel
{
[Required]
public string Username { get; set; }
[Required]
public string Password { get; set; }
}
}

View file

@ -0,0 +1,22 @@
using System.ComponentModel.DataAnnotations;
namespace Sledgemapper.Models.Users
{
public class RegisterModel
{
[Required]
public string FirstName { get; set; }
[Required]
public string LastName { get; set; }
[Required]
public string Username { get; set; }
[Required]
public string Password { get; set; }
[Required]
public string Initials { get; set; }
}
}

View file

@ -0,0 +1,10 @@
namespace Sledgemapper.Models.Users
{
public class UpdateModel
{
public string FirstName { get; set; }
public string LastName { get; set; }
public string Username { get; set; }
public string Password { get; set; }
}
}

View file

@ -0,0 +1,11 @@
namespace Sledgemapper.Models.Users
{
public class UserModel
{
public int Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public string Username { get; set; }
public string Initials { get; set; }
}
}

View file

@ -0,0 +1,32 @@
using System.ComponentModel.DataAnnotations;
namespace Sledgemapper.Api.Models
{
public class MapLog
{
[Key]
public int MapLogId { get; set; }
[Required]
public int UserId{get;set;}
[Required]
public int SessionId { get; set; }
[Required]
[MaxLength(1)]
public string Operation { get; set; }
[Required]
[MaxLength(256)]
public string Type { get; set; }
[Required]
[MaxLength(256)]
public string Object { get; set; }
[Required]
public double Timestamp { get; set; }
}
}

View file

@ -0,0 +1,16 @@
using System.ComponentModel.DataAnnotations;
namespace Sledgemapper.Api.Models
{
public class Session
{
[Key]
public int SessionId { get; set; }
[Required]
public string SessionName{get;set;}
[Required]
public int OwnerUserId { get; set; }
}
}

View file

@ -0,0 +1,16 @@
using System.ComponentModel.DataAnnotations;
namespace Sledgemapper.Api.Models
{
public class SessionUser
{
[Key]
public int SessionUserId { get; set; }
[Required]
public int SessionId { get; set; }
[Required]
public int UserId { get; set; }
}
}

View file

@ -0,0 +1,21 @@
using System.ComponentModel.DataAnnotations;
namespace Sledgemapper.Api.Models
{
public class Snapshot
{
[Key]
public int SnapshotId { get; set; }
[Required]
public int SessionId { get; set; }
[Required]
public string Object { get; set; }
[Required]
public double Timestamp { get; set; }
}
}

View file

@ -0,0 +1,15 @@
using System.ComponentModel.DataAnnotations;
namespace Sledgemapper.Api.Models
{
public class UserConnection
{
[Key]
public int UserConnectionId { get; set; }
[Required]
public int UserId { get; set; }
[Required]
public string ConnectionId{get;set;}
}
}

View file

@ -0,0 +1,20 @@
using MediatR;
using System;
namespace Sledgemapper.Api.Notifications
{
public abstract class BaseNotification : INotification
{
public double Timestamp { get; private set; }
public int UserId { get; private set; }
public int SessionId { get; set; }
public Models.Session Session { get; set; }
public BaseNotification(Models.Session session, int userId)
{
Timestamp = DateTimeOffset.Now.ToUnixTimeMilliseconds();
Session = session; ;
UserId = userId;
}
}
}

View file

@ -0,0 +1,14 @@
using Sledgemapper.Shared.Entities;
namespace Sledgemapper.Api.Notifications
{
public class DeleteOverlayNotification : BaseNotification
{
public Overlay Overlay { get; private set; }
public DeleteOverlayNotification(Models.Session session, Overlay overlay, int userId) : base(session, userId)
{
Overlay = overlay;
}
}
}

View file

@ -0,0 +1,14 @@
using Sledgemapper.Shared.Entities;
namespace Sledgemapper.Api.Notifications
{
public class DeleteTileNotification : BaseNotification
{
public Tile Tile { get; private set; }
public DeleteTileNotification(Models.Session session, Tile tile, int userId) : base(session, userId)
{
Tile = tile;
}
}
}

View file

@ -0,0 +1,14 @@
using Sledgemapper.Shared.Entities;
namespace Sledgemapper.Api.Notifications
{
public class DeleteWallNotification : BaseNotification
{
public Wall Wall { get; private set; }
public DeleteWallNotification(Models.Session session, Wall wall, int userId) : base(session, userId)
{
Wall = wall;
}
}
}

View file

@ -0,0 +1,14 @@
using Sledgemapper.Shared.Entities;
namespace Sledgemapper.Api.Notifications
{
public class NewOverlayNotification : BaseNotification
{
public Overlay Overlay { get; private set; }
public NewOverlayNotification(Models.Session session, Overlay overlay, int userId) : base(session, userId)
{
Overlay = overlay;
}
}
}

View file

@ -0,0 +1,8 @@
namespace Sledgemapper.Api.Notifications
{
public class NewSessionNotification : BaseNotification
{
public NewSessionNotification(string sessionName, int userId) : base(new Models.Session { SessionName = sessionName }, userId)
{ }
}
}

View file

@ -0,0 +1,14 @@
using Sledgemapper.Shared.Entities;
namespace Sledgemapper.Api.Notifications
{
public class NewTileNotification : BaseNotification
{
public Tile Tile { get; private set; }
public NewTileNotification(Models.Session session, Tile tile, int userId) : base(session, userId)
{
Tile = tile;
}
}
}

View file

@ -0,0 +1,14 @@
using Sledgemapper.Shared.Entities;
namespace Sledgemapper.Api.Notifications
{
public class NewWallNotification : BaseNotification
{
public Wall Wall { get; private set; }
public NewWallNotification(Models.Session session, Wall wall, int userId) : base(session, userId)
{
Wall = wall;
}
}
}

View file

@ -0,0 +1,51 @@
using System;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Sledgemapper.Api.Data;
namespace SignalRChat
{
public class Program
{
public static void Main(string[] args)
{
var host= CreateHostBuilder(args).Build();
CreateDbIfNotExists(host);
host.Run();
}
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>().UseUrls("http://localhost:5000");
});
// public static IHostBuilder CreateHostBuilder(string[] args) =>
// Host.CreateDefaultBuilder(args)
// .ConfigureWebHostDefaults(webBuilder =>
// {
// webBuilder.UseStartup<Startup>();
// });
private static void CreateDbIfNotExists(IHost host)
{
using (var scope = host.Services.CreateScope())
{
var services = scope.ServiceProvider;
try
{
var context = services.GetRequiredService<MyDbContext>();
DbInitializer.Initialize(context);
}
catch (Exception ex)
{
var logger = services.GetRequiredService<ILogger<Program>>();
logger.LogError(ex, "An error occurred creating the DB.");
}
}
}
}
}

View file

@ -0,0 +1,10 @@
{
"profiles": {
"Development": {
"commandName": "Project",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}

View file

@ -0,0 +1,5 @@
# aspnet-core-3-registration-login-api
ASP.NET Core 3.1 - Simple API for User Management, Authentication and Registration
For documentation and instructions check out https://jasonwatmore.com/post/2019/10/14/aspnet-core-3-simple-api-for-authentication-registration-and-user-management

View file

@ -0,0 +1,159 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Sledgemapper.Entities;
using Sledgemapper.Helpers;
namespace Sledgemapper.Services
{
public interface IUserService
{
User Authenticate(string username, string password);
IEnumerable<User> GetAll();
User GetById(int id);
User Create(User user, string password);
void Update(User user, string password = null);
void Delete(int id);
}
public class UserService : IUserService
{
private DataContext _context;
public UserService(DataContext context)
{
_context = context;
}
public User Authenticate(string username, string password)
{
if (string.IsNullOrEmpty(username) || string.IsNullOrEmpty(password))
return null;
var user = _context.Users.SingleOrDefault(x => x.Username == username);
// check if username exists
if (user == null)
return null;
// check if password is correct
if (!VerifyPasswordHash(password, user.PasswordHash, user.PasswordSalt))
return null;
// authentication successful
return user;
}
public IEnumerable<User> GetAll()
{
return _context.Users;
}
public User GetById(int id)
{
return _context.Users.Find(id);
}
public User Create(User user, string password)
{
// validation
if (string.IsNullOrWhiteSpace(password))
throw new AppException("Password is required");
if (_context.Users.Any(x => x.Username == user.Username))
throw new AppException("Username \"" + user.Username + "\" is already taken");
byte[] passwordHash, passwordSalt;
CreatePasswordHash(password, out passwordHash, out passwordSalt);
user.PasswordHash = passwordHash;
user.PasswordSalt = passwordSalt;
_context.Users.Add(user);
_context.SaveChanges();
return user;
}
public void Update(User userParam, string password = null)
{
var user = _context.Users.Find(userParam.Id);
if (user == null)
throw new AppException("User not found");
// update username if it has changed
if (!string.IsNullOrWhiteSpace(userParam.Username) && userParam.Username != user.Username)
{
// throw error if the new username is already taken
if (_context.Users.Any(x => x.Username == userParam.Username))
throw new AppException("Username " + userParam.Username + " is already taken");
user.Username = userParam.Username;
}
// update user properties if provided
if (!string.IsNullOrWhiteSpace(userParam.FirstName))
user.FirstName = userParam.FirstName;
if (!string.IsNullOrWhiteSpace(userParam.LastName))
user.LastName = userParam.LastName;
// update password if provided
if (!string.IsNullOrWhiteSpace(password))
{
byte[] passwordHash, passwordSalt;
CreatePasswordHash(password, out passwordHash, out passwordSalt);
user.PasswordHash = passwordHash;
user.PasswordSalt = passwordSalt;
}
_context.Users.Update(user);
_context.SaveChanges();
}
public void Delete(int id)
{
var user = _context.Users.Find(id);
if (user != null)
{
_context.Users.Remove(user);
_context.SaveChanges();
}
}
// private helper methods
private static void CreatePasswordHash(string password, out byte[] passwordHash, out byte[] passwordSalt)
{
if (password == null) throw new ArgumentNullException("password");
if (string.IsNullOrWhiteSpace(password)) throw new ArgumentException("Value cannot be empty or whitespace only string.", "password");
using (var hmac = new System.Security.Cryptography.HMACSHA512())
{
passwordSalt = hmac.Key;
passwordHash = hmac.ComputeHash(System.Text.Encoding.UTF8.GetBytes(password));
}
}
private static bool VerifyPasswordHash(string password, byte[] storedHash, byte[] storedSalt)
{
if (password == null) throw new ArgumentNullException("password");
if (string.IsNullOrWhiteSpace(password)) throw new ArgumentException("Value cannot be empty or whitespace only string.", "password");
if (storedHash.Length != 64) throw new ArgumentException("Invalid length of password hash (64 bytes expected).", "passwordHash");
if (storedSalt.Length != 128) throw new ArgumentException("Invalid length of password salt (128 bytes expected).", "passwordHash");
using (var hmac = new System.Security.Cryptography.HMACSHA512(storedSalt))
{
var computedHash = hmac.ComputeHash(System.Text.Encoding.UTF8.GetBytes(password));
for (int i = 0; i < computedHash.Length; i++)
{
if (computedHash[i] != storedHash[i]) return false;
}
}
return true;
}
}
}

View file

@ -0,0 +1,26 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<ItemGroup>
<ProjectReference Include="..\Sledgemapper.Shared\Sledgemapper.Shared.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="AutoMapper" Version="10.1.1" />
<PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="8.1.0" />
<PackageReference Include="mediatr" Version="9.0.0" />
<PackageReference Include="mediatr.extensions.microsoft.dependencyinjection" Version="9.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="5.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="5.0.0">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="5.0.0" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="6.8.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="5.0.0" />
</ItemGroup>
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
</PropertyGroup>
</Project>

141
Sledgemapper.Api/Startup.cs Normal file
View file

@ -0,0 +1,141 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using SignalRChat.Hubs;
using Sledgemapper.Api.Data;
using Microsoft.EntityFrameworkCore;
using MediatR;
using Microsoft.IdentityModel.Tokens;
using Sledgemapper.Helpers;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using System.Text;
using AutoMapper;
using Sledgemapper.Services;
using System.Security.Claims;
namespace SignalRChat
{
public class Startup
{
private readonly IWebHostEnvironment _env;
public IConfiguration Configuration { get; }
public Startup(IWebHostEnvironment env, IConfiguration configuration)
{
Configuration = configuration;
_env = env;
}
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
if (_env.IsProduction())
services.AddDbContext<DataContext>();
else
services.AddDbContext<DataContext, SqliteDataContext>();
// services.AddRazorPages();
services.AddCors();
services.AddControllers();
services.AddSignalR();
services.AddAutoMapper(AppDomain.CurrentDomain.GetAssemblies());
services.AddMediatR(typeof(Startup));
services.AddDbContext<MyDbContext>(options => {options.UseSqlite("Data Source=sledgemapper.db"); options.UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking);});
// services.AddEntityFrameworkSqlite().AddDbContext<MyDbContext>();
// configure strongly typed settings objects
var c = Configuration.GetSection("AppSettings");
var appSettingsSection = Configuration.GetSection("AppSettings");
services.Configure<AppSettings>(appSettingsSection);
// configure jwt authentication
var appSettings = appSettingsSection.Get<AppSettings>();
var key = Encoding.ASCII.GetBytes(appSettings.Secret);
services.AddAuthentication(x =>
{
x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(x =>
{
x.Events = new JwtBearerEvents
{
OnTokenValidated = context =>
{
var userService = context.HttpContext.RequestServices.GetRequiredService<IUserService>();
var userId = int.Parse(context.Principal.Identity.Name);
var user = userService.GetById(userId);
if (user == null)
{
// return unauthorized if user no longer exists
context.Fail("Unauthorized");
}
context.HttpContext.User.Claims.Append(new Claim(ClaimTypes.Name, user.Username));
context.HttpContext.User.Claims.Append(new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()));
return Task.CompletedTask;
}
};
x.RequireHttpsMetadata = false;
x.SaveToken = true;
x.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(key),
ValidateIssuer = false,
ValidateAudience = false
};
});
// configure DI for application services
services.AddScoped<IUserService, UserService>();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, DataContext dataContext)
{
dataContext.Database.Migrate();
// if (env.IsDevelopment())
// {
// app.UseDeveloperExceptionPage();
// }
// else
// {
// app.UseExceptionHandler("/Error");
// // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
// app.UseHsts();
// }
// //app.UseHttpsRedirection();
// app.UseStaticFiles();
app.UseRouting();
// global cors policy
app.UseCors(x => x
.AllowAnyOrigin()
.AllowAnyMethod()
.AllowAnyHeader());
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
app.UseEndpoints(endpoints =>
{
// endpoints.MapRazorPages();
endpoints.MapHub<SledgemapperHub>("/sledgemapperhub");
});
}
}
}

View file

@ -0,0 +1,12 @@
{
"ConnectionStrings": {
"WebApiDatabase": "Data Source=LocalDatabase.db"
},
"Logging": {
"LogLevel": {
"Default": "Debug",
"System": "Information",
"Microsoft": "Information"
}
}
}

View file

@ -0,0 +1,16 @@
{
"AppSettings": {
"Secret": "THIS IS USED TO SIGN AND VERIFY JWT TOKENS, REPLACE IT WITH YOUR OWN SECRET, IT CAN BE ANY STRING"
},
"ConnectionStrings": {
"WebApiDatabase": "ENTER PRODUCTION SQL SERVER CONNECTION STRING HERE"
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"AllowedHosts": "*"
}

Binary file not shown.

View file

@ -1,34 +0,0 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": ".NET Core Launch (web)",
"type": "coreclr",
"request": "launch",
"preLaunchTask": "build",
"program": "${workspaceFolder}/bin/Debug/net5.0/SignalRChat.dll",
"args": [],
"cwd": "${workspaceFolder}",
"stopAtEntry": false,
"serverReadyAction": {
"action": "openExternally",
"pattern": "\\bNow listening on:\\s+(https?://\\S+)"
},
"env": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"sourceFileMap": {
"/Views": "${workspaceFolder}/Views"
}
},
{
"name": ".NET Core Attach",
"type": "coreclr",
"request": "attach",
"processId": "${command:pickProcess}"
}
]
}

View file

@ -1,42 +0,0 @@
{
"version": "2.0.0",
"tasks": [
{
"label": "build",
"command": "dotnet",
"type": "process",
"args": [
"build",
"${workspaceFolder}/SignalRChat.csproj",
"/property:GenerateFullPaths=true",
"/consoleloggerparameters:NoSummary"
],
"problemMatcher": "$msCompile"
},
{
"label": "publish",
"command": "dotnet",
"type": "process",
"args": [
"publish",
"${workspaceFolder}/SignalRChat.csproj",
"/property:GenerateFullPaths=true",
"/consoleloggerparameters:NoSummary"
],
"problemMatcher": "$msCompile"
},
{
"label": "watch",
"command": "dotnet",
"type": "process",
"args": [
"watch",
"run",
"${workspaceFolder}/SignalRChat.csproj",
"/property:GenerateFullPaths=true",
"/consoleloggerparameters:NoSummary"
],
"problemMatcher": "$msCompile"
}
]
}

View file

@ -1,164 +0,0 @@
using Microsoft.AspNetCore.SignalR;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Collections.Concurrent;
using Sledgemapper.Entities;
namespace SignalRChat.Hubs
{
public class ChatHub : Hub
{
private static Dictionary<string, SessionData> _sessions = new Dictionary<string, SessionData>();
public List<string> Colors = new List<string>{"CC0000",
"CC3300",
"FFCC00",
"009900",
"006666",
"0066FF",
"0000CC",
"663399",
"CC0099"};
public async Task SendMessage(string user, string message, string sessionName)
{
await Clients.Group(sessionName).SendAsync("ReceiveMessage", user, message);
}
public async Task NewTile(string sessionName, Tile tile)
{
var existingTile = _sessions[sessionName].Map.TryGetValue(tile.ToString(), out var t);
if (existingTile)
{
_sessions[sessionName].Map.TryRemove(t.ToString(), out var rtile);
}
_sessions[sessionName].Map.TryAdd(tile.ToString(), tile);
await Clients.Group(sessionName).SendAsync("NewTile", tile);
}
public async Task NewWall(string sessionName, Wall tile)
{
var existingTile = _sessions[sessionName].Walls.TryGetValue(tile.ToString(), out var t);
if (existingTile)
{
_sessions[sessionName].Walls.TryRemove(t.ToString(), out var rtile);
}
_sessions[sessionName].Walls.TryAdd(tile.ToString(), tile);
await Clients.Group(sessionName).SendAsync("NewWall", tile);
}
public async Task NewOverlay(string sessionName, Overlay tile)
{
var existingTile = _sessions[sessionName].Overlays.TryGetValue(tile.ToString(), out var t);
if (existingTile)
{
_sessions[sessionName].Overlays.TryRemove(t.ToString(), out var rtile);
}
_sessions[sessionName].Overlays.TryAdd(tile.ToString(), tile);
await Clients.Group(sessionName).SendAsync("NewOverlay", tile);
}
public async Task DeleteTile(string sessionName, Tile tile)
{
_sessions[sessionName].Map.TryRemove(tile.ToString(), out var rtile);
await Clients.Group(sessionName).SendAsync("DeleteTile", tile);
}
public async Task DeleteWall(string sessionName, Wall tile)
{
_sessions[sessionName].Walls.TryRemove(tile.ToString(), out var rtile);
await Clients.Group(sessionName).SendAsync("DeleteWall", tile);
}
public async Task DeleteOverlay(string sessionName, Overlay tile)
{
_sessions[sessionName].Overlays.TryRemove(tile.ToString(), out var rtile);
await Clients.Group(sessionName).SendAsync("DeleteOverlay", tile);
}
public async Task<SessionData> NewSession(string sessionName, string initials)
{
var session = new SessionData();
session.Colors = new List<string>(Colors);
session.Colors.Shuffle();
var player = new Player { Position = new Tile { X = 0, Y = 0 }, ConnectionId = Context.ConnectionId, Color = session.Colors[0], Initials = initials };
session.Players.Add(player);
_sessions.Add(sessionName, session);
await Groups.AddToGroupAsync(Context.ConnectionId, sessionName);
return session;
}
public async Task<SessionData> JoinSession(string sessionName, string initials)
{
if (_sessions.ContainsKey(sessionName))
{
var session = _sessions[sessionName];
var player = new Player { Position = new Tile { X = 0, Y = 0 }, ConnectionId = Context.ConnectionId, Color = session.Colors[session.Players.Count], Initials = initials };
session.Players.Add(player);
await Clients.Group(sessionName).SendAsync("NewPlayer", player);
await Groups.AddToGroupAsync(Context.ConnectionId, sessionName);
return session;
}
else
{
return null;
}
}
public async Task UpdatePosition(string sessionName, Tile tile)
{
var player = _sessions[sessionName].Players.First(m => m.ConnectionId == Context.ConnectionId);
player.Position = tile;
await Clients.Group(sessionName).SendAsync("PlayerUpdate", player);
}
public async Task<SessionData> Refresh(string sessionName)
{
return _sessions[sessionName];
}
public async Task Sync(string sessionName, SessionData map)
{
_sessions[sessionName].Map = map.Map;
_sessions[sessionName].Overlays = map.Overlays;
_sessions[sessionName].Walls = map.Walls;
await Clients.Group(sessionName).SendAsync("UpdateMap", _sessions[sessionName]);
}
}
public static class ExtensionMethods
{
private static Random rng = new Random();
public static void Shuffle<T>(this IList<T> list)
{
int n = list.Count;
while (n > 1)
{
n--;
int k = rng.Next(n + 1);
T value = list[k];
list[k] = list[n];
list[n] = value;
}
}
}
}

View file

@ -1,26 +0,0 @@
@page
@model ErrorModel
@{
ViewData["Title"] = "Error";
}
<h1 class="text-danger">Error.</h1>
<h2 class="text-danger">An error occurred while processing your request.</h2>
@if (Model.ShowRequestId)
{
<p>
<strong>Request ID:</strong> <code>@Model.RequestId</code>
</p>
}
<h3>Development Mode</h3>
<p>
Swapping to the <strong>Development</strong> environment displays detailed information about the error that occurred.
</p>
<p>
<strong>The Development environment shouldn't be enabled for deployed applications.</strong>
It can result in displaying sensitive information from exceptions to end users.
For local debugging, enable the <strong>Development</strong> environment by setting the <strong>ASPNETCORE_ENVIRONMENT</strong> environment variable to <strong>Development</strong>
and restarting the app.
</p>

View file

@ -1,32 +0,0 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.Extensions.Logging;
namespace SignalRChat.Pages
{
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
[IgnoreAntiforgeryToken]
public class ErrorModel : PageModel
{
public string RequestId { get; set; }
public bool ShowRequestId => !string.IsNullOrEmpty(RequestId);
private readonly ILogger<ErrorModel> _logger;
public ErrorModel(ILogger<ErrorModel> logger)
{
_logger = logger;
}
public void OnGet()
{
RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier;
}
}
}

View file

@ -1,30 +0,0 @@
@page
<div class="container">
<div class="row">&nbsp;</div>
<div class="row">
<div class="col-2">User</div>
<div class="col-4"><input type="text" id="userInput" /></div>
</div>
<div class="row">
<div class="col-2">Message</div>
<div class="col-4"><input type="text" id="messageInput" /></div>
</div>
<div class="row">&nbsp;</div>
<div class="row">
<div class="col-6">
<input type="button" id="sendButton" value="Send Message" />
</div>
</div>
</div>
<div class="row">
<div class="col-12">
<hr />
</div>
</div>
<div class="row">
<div class="col-6">
<ul id="messagesList"></ul>
</div>
</div>
<script src="~/js/signalr/dist/browser/signalr.js"></script>
<script src="~/js/chat.js"></script>

View file

@ -1,25 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.Extensions.Logging;
namespace SignalRChat.Pages
{
public class IndexModel : PageModel
{
private readonly ILogger<IndexModel> _logger;
public IndexModel(ILogger<IndexModel> logger)
{
_logger = logger;
}
public void OnGet()
{
}
}
}

View file

@ -1,8 +0,0 @@
@page
@model PrivacyModel
@{
ViewData["Title"] = "Privacy Policy";
}
<h1>@ViewData["Title"]</h1>
<p>Use this page to detail your site's privacy policy.</p>

View file

@ -1,24 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.Extensions.Logging;
namespace SignalRChat.Pages
{
public class PrivacyModel : PageModel
{
private readonly ILogger<PrivacyModel> _logger;
public PrivacyModel(ILogger<PrivacyModel> logger)
{
_logger = logger;
}
public void OnGet()
{
}
}
}

View file

@ -1,50 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>@ViewData["Title"] - SignalRChat</title>
<link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.min.css" />
<link rel="stylesheet" href="~/css/site.css" />
</head>
<body>
<header>
<nav class="navbar navbar-expand-sm navbar-toggleable-sm navbar-light bg-white border-bottom box-shadow mb-3">
<div class="container">
<a class="navbar-brand" asp-area="" asp-page="/Index">SignalRChat</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target=".navbar-collapse" aria-controls="navbarSupportedContent"
aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="navbar-collapse collapse d-sm-inline-flex justify-content-between">
<ul class="navbar-nav flex-grow-1">
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-page="/Index">Home</a>
</li>
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-page="/Privacy">Privacy</a>
</li>
</ul>
</div>
</div>
</nav>
</header>
<div class="container">
<main role="main" class="pb-3">
@RenderBody()
</main>
</div>
<footer class="border-top footer text-muted">
<div class="container">
&copy; 2020 - SignalRChat - <a asp-area="" asp-page="/Privacy">Privacy</a>
</div>
</footer>
<script src="~/lib/jquery/dist/jquery.min.js"></script>
<script src="~/lib/bootstrap/dist/js/bootstrap.bundle.min.js"></script>
<script src="~/js/site.js" asp-append-version="true"></script>
@await RenderSectionAsync("Scripts", required: false)
</body>
</html>

View file

@ -1,2 +0,0 @@
<script src="~/lib/jquery-validation/dist/jquery.validate.min.js"></script>
<script src="~/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.min.js"></script>

View file

@ -1,3 +0,0 @@
@using SignalRChat
@namespace SignalRChat.Pages
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

Some files were not shown because too many files have changed in this diff Show more