diff --git a/src/backend/db/Dockerfile b/src/backend/db/Dockerfile new file mode 100644 index 0000000..74b12bc --- /dev/null +++ b/src/backend/db/Dockerfile @@ -0,0 +1 @@ +FROM mongo diff --git a/src/backend/server/Todo/Todo.Api/Controllers/AuthController.cs b/src/backend/server/Todo/Todo.Api/Controllers/AuthController.cs new file mode 100644 index 0000000..058315c --- /dev/null +++ b/src/backend/server/Todo/Todo.Api/Controllers/AuthController.cs @@ -0,0 +1,37 @@ +using System.ComponentModel.DataAnnotations; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Todo.Core.Entities; +using Todo.Core.Interfaces.Persistence; + +namespace Todo.Api.Controllers +{ + [ApiController] + [Route("api/auth")] + [Authorize] + [AllowAnonymous] + public class AuthController : ControllerBase + { + private readonly IUserRepository _userRepository; + + public AuthController(IUserRepository userRepository) + { + _userRepository = userRepository; + } + + public record RegisterUserRequest + { + [Required] public string Email { get; init; } + [Required] public string Password { get; init; } + } + + [HttpPost("register")] + public async Task Register([FromBody] RegisterUserRequest request) + { + var user = await _userRepository.Register(request.Email, request.Password); + + return Ok(user); + } + } +} \ No newline at end of file diff --git a/src/backend/server/Todo/Todo.Api/Controllers/TodosController.cs b/src/backend/server/Todo/Todo.Api/Controllers/TodosController.cs new file mode 100644 index 0000000..879b930 --- /dev/null +++ b/src/backend/server/Todo/Todo.Api/Controllers/TodosController.cs @@ -0,0 +1,40 @@ +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Todo.Core.Interfaces.Persistence; + +namespace Todo.Api.Controllers; + +[ApiController] +[Route("/api/todos")] +public class TodosController : ControllerBase +{ + private readonly ITodoRepository _todoRepository; + + public TodosController(ITodoRepository todoRepository) + { + _todoRepository = todoRepository; + } + + public record CreateTodoRequest + { + [Required] public string Title { get; init; } + } + + [HttpPost] + public async Task> CreateTodo([FromBody] CreateTodoRequest request) + { + var todo = await _todoRepository.CreateTodoAsync(request.Title); + + return Ok(todo); + } + + [HttpGet] + public async Task>> GetTodos() + { + var todos = await _todoRepository.GetTodosAsync(); + + return Ok(todos); + } +} \ No newline at end of file diff --git a/src/backend/server/Todo/Todo.Api/Dockerfile b/src/backend/server/Todo/Todo.Api/Dockerfile index 9c65291..247c1fa 100644 --- a/src/backend/server/Todo/Todo.Api/Dockerfile +++ b/src/backend/server/Todo/Todo.Api/Dockerfile @@ -1,9 +1,9 @@ -FROM mcr.microsoft.com/dotnet/aspnet:5.0 AS base +FROM mcr.microsoft.com/dotnet/aspnet:6.0 AS base WORKDIR /app EXPOSE 80 EXPOSE 443 -FROM mcr.microsoft.com/dotnet/sdk:5.0 AS build +FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build WORKDIR /src COPY ["Todo.Api/Todo.Api.csproj", "Todo.Api/"] RUN dotnet restore "Todo.Api/Todo.Api.csproj" diff --git a/src/backend/server/Todo/Todo.Api/Hubs/TodoHub.cs b/src/backend/server/Todo/Todo.Api/Hubs/TodoHub.cs index 6aecc2f..a9db58e 100644 --- a/src/backend/server/Todo/Todo.Api/Hubs/TodoHub.cs +++ b/src/backend/server/Todo/Todo.Api/Hubs/TodoHub.cs @@ -1,13 +1,32 @@ +using System; +using System.Text.Json; using System.Threading.Tasks; using Microsoft.AspNetCore.SignalR; +using Todo.Core.Interfaces.Persistence; namespace Todo.Api.Hubs { public class TodoHub : Hub { + private readonly ITodoRepository _todoRepository; + + public TodoHub(ITodoRepository todoRepository) + { + _todoRepository = todoRepository; + } + public async Task GetInboxTodos() { await Clients.Caller.SendAsync("InboxTodos", "some data"); } + + public async Task CreateTodo(string createTodoRequest) + { + var todo = JsonSerializer.Deserialize(createTodoRequest); + if (todo is null) + throw new InvalidOperationException("Failed to create todo because of invalid request"); + + await _todoRepository.CreateTodoAsync(todo.Title); + } } } \ No newline at end of file diff --git a/src/backend/server/Todo/Todo.Api/Program.cs b/src/backend/server/Todo/Todo.Api/Program.cs index f9ca89e..0a1fca6 100644 --- a/src/backend/server/Todo/Todo.Api/Program.cs +++ b/src/backend/server/Todo/Todo.Api/Program.cs @@ -1,7 +1,7 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; +global using System; +global using System.Collections.Generic; +global using System.Linq; +global using System.Threading.Tasks; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Hosting; diff --git a/src/backend/server/Todo/Todo.Api/Startup.cs b/src/backend/server/Todo/Todo.Api/Startup.cs index b52bc57..838ba14 100644 --- a/src/backend/server/Todo/Todo.Api/Startup.cs +++ b/src/backend/server/Todo/Todo.Api/Startup.cs @@ -1,17 +1,12 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.HttpsPolicy; -using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; using Microsoft.OpenApi.Models; using Todo.Api.Hubs; +using Todo.Persistence; +using Todo.Persistence.Mongo; namespace Todo.Api { @@ -42,12 +37,15 @@ namespace Todo.Api c.SwaggerDoc("v1", new OpenApiInfo { Title = "Todo.Api", Version = "v1" }); }); + services.AddPersistence(); services.AddSignalR(); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { + app.MigrateMongoDb(); + if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); diff --git a/src/backend/server/Todo/Todo.Api/Todo.Api.csproj b/src/backend/server/Todo/Todo.Api/Todo.Api.csproj index c33c9b6..271006d 100644 --- a/src/backend/server/Todo/Todo.Api/Todo.Api.csproj +++ b/src/backend/server/Todo/Todo.Api/Todo.Api.csproj @@ -1,7 +1,7 @@ - net5.0 + net6.0 Linux @@ -9,4 +9,9 @@ + + + + + diff --git a/src/backend/server/Todo/Todo.Api/WeatherForecast.cs b/src/backend/server/Todo/Todo.Api/WeatherForecast.cs deleted file mode 100644 index 8b80f30..0000000 --- a/src/backend/server/Todo/Todo.Api/WeatherForecast.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System; - -namespace Todo.Api -{ - public class WeatherForecast - { - public DateTime Date { get; set; } - - public int TemperatureC { get; set; } - - public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); - - public string Summary { get; set; } - } -} \ No newline at end of file diff --git a/src/backend/server/Todo/Todo.Core/DependencyInjection.cs b/src/backend/server/Todo/Todo.Core/DependencyInjection.cs new file mode 100644 index 0000000..3187069 --- /dev/null +++ b/src/backend/server/Todo/Todo.Core/DependencyInjection.cs @@ -0,0 +1,11 @@ +global using System; +global using System.Linq; +global using System.Threading.Tasks; +global using System.Collections.Generic; + +namespace Todo.Core; + +public static class DependencyInjection +{ + +} \ No newline at end of file diff --git a/src/backend/server/Todo/Todo.Core/Entities/Todo.cs b/src/backend/server/Todo/Todo.Core/Entities/Todo.cs new file mode 100644 index 0000000..ece0360 --- /dev/null +++ b/src/backend/server/Todo/Todo.Core/Entities/Todo.cs @@ -0,0 +1,7 @@ +namespace Todo.Core.Entities; + +public record Todo +{ + public string Id { get; init; } + public string Title { get; init; } +} \ No newline at end of file diff --git a/src/backend/server/Todo/Todo.Core/Entities/User.cs b/src/backend/server/Todo/Todo.Core/Entities/User.cs new file mode 100644 index 0000000..6f98979 --- /dev/null +++ b/src/backend/server/Todo/Todo.Core/Entities/User.cs @@ -0,0 +1,8 @@ +namespace Todo.Core.Entities +{ + public class User + { + public string Id { get; set; } + public string Email { get; set; } + } +} \ No newline at end of file diff --git a/src/backend/server/Todo/Todo.Core/Interfaces/Persistence/ITodoRepository.cs b/src/backend/server/Todo/Todo.Core/Interfaces/Persistence/ITodoRepository.cs new file mode 100644 index 0000000..7d8e0f9 --- /dev/null +++ b/src/backend/server/Todo/Todo.Core/Interfaces/Persistence/ITodoRepository.cs @@ -0,0 +1,8 @@ + +namespace Todo.Core.Interfaces.Persistence; + +public interface ITodoRepository +{ + Task CreateTodoAsync(string title); + Task> GetTodosAsync(); +} \ No newline at end of file diff --git a/src/backend/server/Todo/Todo.Core/Interfaces/Persistence/IUserRepository.cs b/src/backend/server/Todo/Todo.Core/Interfaces/Persistence/IUserRepository.cs new file mode 100644 index 0000000..80fabe4 --- /dev/null +++ b/src/backend/server/Todo/Todo.Core/Interfaces/Persistence/IUserRepository.cs @@ -0,0 +1,8 @@ +using Todo.Core.Entities; + +namespace Todo.Core.Interfaces.Persistence; + +public interface IUserRepository +{ + Task Register(string email, string password); +} \ No newline at end of file diff --git a/src/backend/server/Todo/Todo.Core/Todo.Core.csproj b/src/backend/server/Todo/Todo.Core/Todo.Core.csproj new file mode 100644 index 0000000..4f444d8 --- /dev/null +++ b/src/backend/server/Todo/Todo.Core/Todo.Core.csproj @@ -0,0 +1,7 @@ + + + + net6.0 + + + diff --git a/src/backend/server/Todo/Todo.Persistence/DependencyInjection.cs b/src/backend/server/Todo/Todo.Persistence/DependencyInjection.cs new file mode 100644 index 0000000..5468165 --- /dev/null +++ b/src/backend/server/Todo/Todo.Persistence/DependencyInjection.cs @@ -0,0 +1,16 @@ +global using System; +global using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Todo.Core.Interfaces.Persistence; +using Todo.Persistence.Mongo.Repositories; + +namespace Todo.Persistence +{ + public static class DependencyInjection + { + public static IServiceCollection AddPersistence(this IServiceCollection services) => + services + .AddScoped() + .AddScoped(); + } +} \ No newline at end of file diff --git a/src/backend/server/Todo/Todo.Persistence/Mongo/Migrations.cs b/src/backend/server/Todo/Todo.Persistence/Mongo/Migrations.cs new file mode 100644 index 0000000..71687d1 --- /dev/null +++ b/src/backend/server/Todo/Todo.Persistence/Mongo/Migrations.cs @@ -0,0 +1,36 @@ +using System; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using MongoDB.Driver; +using Todo.Persistence.Mongo.Repositories.Dtos; + +namespace Todo.Persistence.Mongo +{ + public static class Migrations + { + public static IApplicationBuilder MigrateMongoDb(this IApplicationBuilder app) + { + Task.Run(async () => + { + var conn = new MongoClient("mongodb://root:example@localhost:27017"); + var database = conn.GetDatabase("todo"); + + await CreateIndexes(database); + }).Wait(10000); + + return app; + } + + private static async Task CreateIndexes(IMongoDatabase mongoDatabase) + { + Console.WriteLine("Running: CreateIndexes"); + var collection = mongoDatabase.GetCollection("users"); + var indexKeysDefinition = Builders.IndexKeys.Ascending(user => user.Email); + var indexModel = + new CreateIndexModel(indexKeysDefinition, new CreateIndexOptions() { Unique = true }); + + await collection.Indexes.CreateOneAsync(indexModel); + Console.WriteLine("Finished: CreateIndexes"); + } + } +} \ No newline at end of file diff --git a/src/backend/server/Todo/Todo.Persistence/Mongo/Repositories/Dtos/MongoTodo.cs b/src/backend/server/Todo/Todo.Persistence/Mongo/Repositories/Dtos/MongoTodo.cs new file mode 100644 index 0000000..ea48a95 --- /dev/null +++ b/src/backend/server/Todo/Todo.Persistence/Mongo/Repositories/Dtos/MongoTodo.cs @@ -0,0 +1,13 @@ +using MongoDB.Bson; +using MongoDB.Bson.Serialization.Attributes; + +namespace Todo.Persistence.Mongo.Repositories.Dtos; + +public record MongoTodo +{ + [BsonId] + [BsonRepresentation(BsonType.ObjectId)] + public string Id { get; init; } + + [BsonRequired] public string Title { get; init; } +} \ No newline at end of file diff --git a/src/backend/server/Todo/Todo.Persistence/Mongo/Repositories/Dtos/MongoUser.cs b/src/backend/server/Todo/Todo.Persistence/Mongo/Repositories/Dtos/MongoUser.cs new file mode 100644 index 0000000..6b88d82 --- /dev/null +++ b/src/backend/server/Todo/Todo.Persistence/Mongo/Repositories/Dtos/MongoUser.cs @@ -0,0 +1,15 @@ +using MongoDB.Bson; +using MongoDB.Bson.Serialization.Attributes; + +namespace Todo.Persistence.Mongo.Repositories.Dtos +{ + public record MongoUser + { + [BsonId] + [BsonRepresentation(BsonType.ObjectId)] + public string Id { get; init; } + + [BsonRequired] public string Email { get; init; } + [BsonRequired] public string Password { get; set; } + } +} \ No newline at end of file diff --git a/src/backend/server/Todo/Todo.Persistence/Mongo/Repositories/TodoRepository.cs b/src/backend/server/Todo/Todo.Persistence/Mongo/Repositories/TodoRepository.cs new file mode 100644 index 0000000..4146e07 --- /dev/null +++ b/src/backend/server/Todo/Todo.Persistence/Mongo/Repositories/TodoRepository.cs @@ -0,0 +1,37 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using MongoDB.Driver; +using Todo.Core.Interfaces.Persistence; +using Todo.Persistence.Mongo.Repositories.Dtos; + +namespace Todo.Persistence.Mongo.Repositories; + +public class TodoRepository : ITodoRepository +{ + private readonly IMongoCollection _todosCollection; + + public TodoRepository() + { + var conn = new MongoClient("mongodb://root:example@localhost:27017"); + var database = conn.GetDatabase("todo"); + + _todosCollection = database.GetCollection("todos"); + } + + public async Task CreateTodoAsync(string title) + { + var todo = new MongoTodo() { Title = title }; + await _todosCollection.InsertOneAsync(todo); + return new Core.Entities.Todo() { Id = todo.Id, Title = todo.Title }; + } + + public async Task> GetTodosAsync() + { + var todos = await _todosCollection.FindAsync(_ => true); + return todos + .ToEnumerable() + .Select(t => + new Core.Entities.Todo() { Id = t.Id, Title = t.Title }); + } +} \ No newline at end of file diff --git a/src/backend/server/Todo/Todo.Persistence/Mongo/Repositories/UserRepository.cs b/src/backend/server/Todo/Todo.Persistence/Mongo/Repositories/UserRepository.cs new file mode 100644 index 0000000..0b72c42 --- /dev/null +++ b/src/backend/server/Todo/Todo.Persistence/Mongo/Repositories/UserRepository.cs @@ -0,0 +1,28 @@ +using System.Threading.Tasks; +using MongoDB.Driver; +using Todo.Core.Entities; +using Todo.Core.Interfaces.Persistence; +using Todo.Persistence.Mongo.Repositories.Dtos; + +namespace Todo.Persistence.Mongo.Repositories +{ + public class UserRepository : IUserRepository + { + private readonly IMongoCollection _usersCollection; + + public UserRepository() + { + var conn = new MongoClient("mongodb://root:example@localhost:27017"); + var database = conn.GetDatabase("todo"); + + _usersCollection = database.GetCollection("users"); + } + + public async Task Register(string email, string password) + { + var dtoUser = new MongoUser() { Email = email, Password = password }; + await _usersCollection.InsertOneAsync(dtoUser); + return new User() { Email = dtoUser.Email, Id = dtoUser.Id }; + } + } +} \ No newline at end of file diff --git a/src/backend/server/Todo/Todo.Persistence/Todo.Persistence.csproj b/src/backend/server/Todo/Todo.Persistence/Todo.Persistence.csproj new file mode 100644 index 0000000..3b14b3d --- /dev/null +++ b/src/backend/server/Todo/Todo.Persistence/Todo.Persistence.csproj @@ -0,0 +1,20 @@ + + + + net6.0 + + + + + + + + + + + + + + + + diff --git a/src/backend/server/Todo/Todo.sln b/src/backend/server/Todo/Todo.sln index 1e71b08..814191a 100644 --- a/src/backend/server/Todo/Todo.sln +++ b/src/backend/server/Todo/Todo.sln @@ -2,6 +2,10 @@ Microsoft Visual Studio Solution File, Format Version 12.00 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Todo.Api", "Todo.Api\Todo.Api.csproj", "{FA037FDE-DAA4-4A95-ABA5-64DCFEDDBA4E}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Todo.Persistence", "Todo.Persistence\Todo.Persistence.csproj", "{7636E7CC-C9A5-41D4-BBFE-B107497A5C8A}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Todo.Core", "Todo.Core\Todo.Core.csproj", "{F134CAB6-15A5-45CB-8782-B61AB67B5C9C}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -12,5 +16,13 @@ Global {FA037FDE-DAA4-4A95-ABA5-64DCFEDDBA4E}.Debug|Any CPU.Build.0 = Debug|Any CPU {FA037FDE-DAA4-4A95-ABA5-64DCFEDDBA4E}.Release|Any CPU.ActiveCfg = Release|Any CPU {FA037FDE-DAA4-4A95-ABA5-64DCFEDDBA4E}.Release|Any CPU.Build.0 = Release|Any CPU + {7636E7CC-C9A5-41D4-BBFE-B107497A5C8A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7636E7CC-C9A5-41D4-BBFE-B107497A5C8A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7636E7CC-C9A5-41D4-BBFE-B107497A5C8A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7636E7CC-C9A5-41D4-BBFE-B107497A5C8A}.Release|Any CPU.Build.0 = Release|Any CPU + {F134CAB6-15A5-45CB-8782-B61AB67B5C9C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F134CAB6-15A5-45CB-8782-B61AB67B5C9C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F134CAB6-15A5-45CB-8782-B61AB67B5C9C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F134CAB6-15A5-45CB-8782-B61AB67B5C9C}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection EndGlobal diff --git a/src/backend/server/Todo/global.json b/src/backend/server/Todo/global.json new file mode 100644 index 0000000..f443bd4 --- /dev/null +++ b/src/backend/server/Todo/global.json @@ -0,0 +1,7 @@ +{ + "sdk": { + "version": "6.0", + "rollForward": "latestMajor", + "allowPrerelease": true + } +} \ No newline at end of file diff --git a/src/docker-compose.yml b/src/docker-compose.yml new file mode 100644 index 0000000..bff3237 --- /dev/null +++ b/src/docker-compose.yml @@ -0,0 +1,9 @@ +services: + db: + build: + context: backend/db + ports: + - 27017:27017 + environment: + MONGO_INITDB_ROOT_USERNAME: root + MONGO_INITDB_ROOT_PASSWORD: example