diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 828f1cf..06377de 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -35,7 +35,4 @@ jobs: MQTT_CLIENT_ID: ${{ secrets.MQTT_CLIENT_ID }} MQTT_SUBSCRIBE_TOPIC: ${{ secrets.MQTT_SUBSCRIBE_TOPIC }} MQTT_PUBLISH_TOPIC: ${{ secrets.MQTT_PUBLISH_TOPIC }} - - AZURE_VISION_KEY: ${{ secrets.AZURE_VISION_KEY }} - AZURE_VISION_ENDPOINT: ${{ secrets.AZURE_VISION_ENDPOINT }} run: cd Tests && dotnet test \ No newline at end of file diff --git a/Infrastructure/ApplicationDbContext.cs b/Infrastructure/ApplicationDbContext.cs index 1024629..362424d 100644 --- a/Infrastructure/ApplicationDbContext.cs +++ b/Infrastructure/ApplicationDbContext.cs @@ -54,172 +54,4 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) base.OnModelCreating(modelBuilder); } - - public async Task SeedDevelopmentDataAsync(IServiceScope scope, string defaultPlantImage) - { - var userRepository = scope.ServiceProvider.GetRequiredService(); - await userRepository.CreateUser(new RegisterUserDto - { - Email = "bob@app.com", - Password = "password", - Username = "bob" - }); - - var collectionsRepository = scope.ServiceProvider.GetRequiredService(); - var collection1 = await collectionsRepository.CreateCollection( - new Collection - { - CollectionId = Guid.NewGuid(), - Name = "Succulents", - UserEmail = "bob@app.com", - } - ); - var collection2 = await collectionsRepository.CreateCollection( - new Collection - { - CollectionId = Guid.NewGuid(), - Name = "Cacti", - UserEmail = "bob@app.com", - } - ); - - var plantRepository = scope.ServiceProvider.GetRequiredService(); - await plantRepository.CreatePlant( - new Plant - { - PlantId = Guid.NewGuid(), - Nickname = "Aloe Vera", - UserEmail = "bob@app.com", - ImageUrl = defaultPlantImage, - CollectionId = collection1.CollectionId, - LatestChange = DateTime.UtcNow.Subtract(TimeSpan.FromDays(5)) - } - ); - - await plantRepository.CreatePlant( - new Plant - { - PlantId = Guid.NewGuid(), - Nickname = "Prickly Pear", - UserEmail = "bob@app.com", - ImageUrl = defaultPlantImage, - CollectionId = collection2.CollectionId, - LatestChange = DateTime.UtcNow.Subtract(TimeSpan.FromDays(7)) - } - ); - - await plantRepository.CreatePlant( - new Plant - { - PlantId = Guid.NewGuid(), - Nickname = "Dying plant", - UserEmail = "bob@app.com", - ImageUrl = defaultPlantImage, - CollectionId = collection2.CollectionId, - LatestChange = DateTime.UtcNow - } - ); - - var plants = await plantRepository.GetPlantsForUser("bob@app.com", 1, 5); - - var requirementsRepository = scope.ServiceProvider.GetRequiredService(); - await requirementsRepository.CreateRequirements( - new Requirements - { - RequirementsId = Guid.NewGuid(), - PlantId = plants.First(p => p.Nickname == "Aloe Vera").PlantId, - LightLevel = RequirementLevel.Low, - SoilMoistureLevel = RequirementLevel.Medium, - HumidityLevel = RequirementLevel.High, - TemperatureLevel = 22, - } - ); - - await requirementsRepository.CreateRequirements( - new Requirements - { - RequirementsId = Guid.NewGuid(), - PlantId = plants.First(p => p.Nickname == "Prickly Pear").PlantId, - LightLevel = RequirementLevel.High, - SoilMoistureLevel = RequirementLevel.Low, - HumidityLevel = RequirementLevel.Low, - TemperatureLevel = 27, - } - ); - - await requirementsRepository.CreateRequirements( - new Requirements - { - RequirementsId = Guid.NewGuid(), - PlantId = plants.First(p => p.Nickname == "Dying plant").PlantId, - LightLevel = RequirementLevel.High, - SoilMoistureLevel = RequirementLevel.Low, - HumidityLevel = RequirementLevel.Medium, - TemperatureLevel = 24, - } - ); - - var conditionsLogRepository = scope.ServiceProvider.GetRequiredService(); - - for (var i = 0; i < 100; i++) - { - await conditionsLogRepository.CreateConditionsLogAsync( - GetRandomConditionsLog(plants.First(p => p.Nickname == "Prickly Pear").PlantId, i * 6) - ); - await conditionsLogRepository.CreateConditionsLogAsync( - GetRandomConditionsLog(plants.First(p => p.Nickname == "Aloe Vera").PlantId, i * 6) - ); - } - - await conditionsLogRepository.CreateConditionsLogAsync( - new ConditionsLog() - { - ConditionsId = Guid.NewGuid(), - PlantId = plants.First(p => p.Nickname == "Dying plant").PlantId, - TimeStamp = DateTime.UtcNow, - Mood = 0, - SoilMoisture = 55, - Light = 13, - Humidity = 68, - Temperature = 25, - } - ); - - Console.ForegroundColor = ConsoleColor.Green; - Console.WriteLine("Seeded development data"); - Console.ResetColor(); - } - - private double GetRandomLevelValue() - { - var random = new Random(); - return random.NextDouble() * 100; - } - - private int GetRandomMood() - { - var random = new Random(); - return random.Next(0, 5); - } - - private int GetRandomTemperature() - { - var random = new Random(); - return random.Next(-20, 45); - } - - private ConditionsLog GetRandomConditionsLog(Guid plantId, int hoursAgo = 0) - { - return new ConditionsLog - { - ConditionsId = Guid.NewGuid(), - PlantId = plantId, - TimeStamp = DateTime.UtcNow.Subtract(TimeSpan.FromHours(hoursAgo)), - Mood = GetRandomMood(), - SoilMoisture = GetRandomLevelValue(), - Light = GetRandomLevelValue(), - Temperature = GetRandomTemperature(), - Humidity = GetRandomLevelValue(), - }; - } } \ No newline at end of file diff --git a/Infrastructure/Repositories/CollectionsRepository.cs b/Infrastructure/Repositories/CollectionsRepository.cs index bdac8c3..f434f85 100644 --- a/Infrastructure/Repositories/CollectionsRepository.cs +++ b/Infrastructure/Repositories/CollectionsRepository.cs @@ -67,11 +67,4 @@ public async Task RemovePlantFromCollection(Collection collection, Plant plant) collection.Plants.Remove(plant); await applicationDbContext.SaveChangesAsync(); } - - public async Task GetTotalCollectionsCount(string email) - { - await using var applicationDbContext = await dbContextFactory.CreateDbContextAsync(); - return await applicationDbContext.Collections - .CountAsync(collection => collection.UserEmail == email); - } } \ No newline at end of file diff --git a/Infrastructure/Repositories/PlantRepository.cs b/Infrastructure/Repositories/PlantRepository.cs index 3f21e69..ba7238d 100644 --- a/Infrastructure/Repositories/PlantRepository.cs +++ b/Infrastructure/Repositories/PlantRepository.cs @@ -11,9 +11,16 @@ public class PlantRepository(IDbContextFactory dbContextFa public async Task CreatePlant(Plant plant) { await using var context = await dbContextFactory.CreateDbContextAsync(); + await context.Plants.AddAsync(plant); await context.SaveChangesAsync(); } + + public async Task DoesDeviceIdExist(string deviceId) + { + await using var context = await dbContextFactory.CreateDbContextAsync(); + return await context.Plants.AnyAsync(p => p.DeviceId == deviceId); + } public async Task GetPlantById(Guid id) { @@ -98,7 +105,7 @@ public async Task> GetCriticalPlants(string requesterEmail) await using var context = await dbContextFactory.CreateDbContextAsync(); return await context.Plants .Include(plant => plant.Requirements) - .Include(plant => plant.ConditionsLogs) + .Include(plant => plant.ConditionsLogs.OrderByDescending(log => log.TimeStamp).Take(1)) .Where(p => p.UserEmail == requesterEmail && p.ConditionsLogs.Count != 0) .Select(p => new { @@ -114,20 +121,22 @@ public async Task> GetCriticalPlants(string requesterEmail) .Select(p => p.Plant) .ToListAsync(); } - - public async Task GetHappyPlantsCount(string userEmail) + + public async Task GetStats(string userEmail) { await using var context = await dbContextFactory.CreateDbContextAsync(); - return await context.Plants + var totalPlants = await context.Plants.CountAsync(p => p.UserEmail == userEmail); + var happyPlants = await context.Plants .Include(plant => plant.ConditionsLogs) .Where(p => p.UserEmail == userEmail && p.ConditionsLogs.Count != 0) .CountAsync(p => p.ConditionsLogs.OrderByDescending(log => log.TimeStamp).FirstOrDefault()!.Mood > 2); - } + var collections = await context.Collections.CountAsync(c => c.UserEmail == userEmail); - public async Task GetTotalPlantsCount(string userEmail) - { - await using var context = await dbContextFactory.CreateDbContextAsync(); - return await context.Plants - .CountAsync(p => p.UserEmail == userEmail); + return new Stats + { + TotalPlants = totalPlants, + HappyPlants = happyPlants, + Collections = collections + }; } } \ No newline at end of file diff --git a/Shared/Dtos/CreateConditionsLogDto.cs b/Shared/Dtos/CreateConditionsLogDto.cs index e34a17c..d708a54 100644 --- a/Shared/Dtos/CreateConditionsLogDto.cs +++ b/Shared/Dtos/CreateConditionsLogDto.cs @@ -6,5 +6,5 @@ public class CreateConditionsLogDto public double Light { get; set; } public double Temperature { get; set; } public double Humidity { get; set; } - public long DeviceId { get; set; } + public required string DeviceId { get; set; } } \ No newline at end of file diff --git a/Shared/Models/Stats.cs b/Shared/Models/Stats.cs new file mode 100644 index 0000000..163c91b --- /dev/null +++ b/Shared/Models/Stats.cs @@ -0,0 +1,8 @@ +namespace Shared.Models; + +public class Stats +{ + public int TotalPlants { get; set; } + public int HappyPlants { get; set; } + public int Collections { get; set; } +} \ No newline at end of file diff --git a/Tests/PlantTests.cs b/Tests/PlantTests.cs index 88de0fb..5de8a3e 100644 --- a/Tests/PlantTests.cs +++ b/Tests/PlantTests.cs @@ -1,4 +1,3 @@ -using api.Events.Collections.Server; using api.Events.PlantEvents.Client; using api.Events.PlantEvents.Server; using lib; @@ -15,8 +14,7 @@ public async Task CreatePlant() { var jwtAndEmail = await SignUpAndLogIn(); var jwt = jwtAndEmail[DictionaryKeys.Jwt]; - var email = jwtAndEmail[DictionaryKeys.Email]; - var createPlantDto = GenerateRandomCreatePlantDto(email); + var createPlantDto = GenerateRandomCreatePlantDto(); var webSocketTestClient = await new WebSocketTestClient().ConnectAsync(); @@ -24,7 +22,16 @@ await webSocketTestClient.DoAndAssert(new ClientWantsToCreatePlantDto { CreatePl { return receivedMessages.Count(e => e.eventType == nameof(ServerSavesPlant)) == 1; }); - + } + + [Test] + public async Task GetAllPlants() + { + var jwtAndEmail = await SignUpAndLogIn(); + var jwt = jwtAndEmail[DictionaryKeys.Jwt]; + + var webSocketTestClient = await new WebSocketTestClient().ConnectAsync(); + await webSocketTestClient.DoAndAssert(new ClientWantsAllPlantsDto { Jwt = jwt, @@ -36,7 +43,7 @@ await webSocketTestClient.DoAndAssert(new ClientWantsAllPlantsDto }); } - private CreatePlantDto GenerateRandomCreatePlantDto(string email) + private CreatePlantDto GenerateRandomCreatePlantDto() { var createPlantDto = new CreatePlantDto { diff --git a/Tests/TestBase.cs b/Tests/TestBase.cs index 9260a42..a0e31da 100644 --- a/Tests/TestBase.cs +++ b/Tests/TestBase.cs @@ -35,8 +35,10 @@ await webSocketTestClient.DoAndAssert(new ClientWantsToSignUpDto { RegisterUserD var jwtSubscription = webSocketTestClient.Client.MessageReceived.Subscribe(msg => { + var eventType = JsonSerializer.Deserialize(msg.Text).eventType; + if (eventType != nameof(ServerAuthenticatesUser)) return; var serverAuthenticates = JsonSerializer.Deserialize(msg.Text, options: new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); - jwt = serverAuthenticates.Jwt; + jwt = serverAuthenticates!.Jwt; }); webSocketTestClient.Send(new ClientWantsToLogInDto { LoginDto = loginDto }); diff --git a/Tests/WebSocketTestClient.cs b/Tests/WebSocketTestClient.cs new file mode 100644 index 0000000..4e863d1 --- /dev/null +++ b/Tests/WebSocketTestClient.cs @@ -0,0 +1,69 @@ +using System.Text.Json; +using api.Events.Global; +using lib; +using Websocket.Client; + +namespace Tests +{ + public class WebSocketTestClient + { + public readonly WebsocketClient Client; + public readonly List ReceivedMessages = []; + private static readonly JsonSerializerOptions JsonSerializerOptions = new() + { + PropertyNameCaseInsensitive = true + }; + + public WebSocketTestClient(string? url = null) + { + Client = url == null ? new WebsocketClient(new Uri("ws://localhost:" + (Environment.GetEnvironmentVariable("FULLSTACK_API_PORT") ?? "8181"))) : new WebsocketClient(new Uri(url)); + Client.MessageReceived.Subscribe(msg => + { + BaseDto baseDto = JsonSerializer.Deserialize(msg.Text, JsonSerializerOptions); + + if (baseDto.eventType == "ServerSendsErrorMessage" || baseDto.eventType.Contains("ServerResponds") || + baseDto.eventType.Contains("ServerRejects")) + { + var error = JsonSerializer.Deserialize(msg.Text, JsonSerializerOptions); + Console.WriteLine("Error: " + error!.Error); + } + + lock (ReceivedMessages) + ReceivedMessages.Add(baseDto); + }); + } + + public async Task ConnectAsync() + { + await Client.Start(); + if (!Client.IsRunning) + throw new Exception("Could not start client!"); + return this; + } + + public void Send(T dto) where T : BaseDto + { + Client.Send(JsonSerializer.Serialize(dto)); + } + + public async Task DoAndAssert(T? action = null, Func, bool>? condition = null) where T : BaseDto + { + if ((object) (T) action != null) + Send(action); + if (condition != null) + { + DateTime startTime = DateTime.UtcNow; + while (DateTime.UtcNow - startTime < TimeSpan.FromSeconds(5.0)) + { + lock (ReceivedMessages) + { + if (condition(ReceivedMessages)) + return; + } + await Task.Delay(100); + } + throw new TimeoutException("Condition not met: "); + } + } + } +} \ No newline at end of file diff --git a/api/Core/Services/CollectionsService.cs b/api/Core/Services/CollectionsService.cs index 7ac6a66..eb4a888 100644 --- a/api/Core/Services/CollectionsService.cs +++ b/api/Core/Services/CollectionsService.cs @@ -74,9 +74,4 @@ private async Task VerifyCollectionExistsAndUserHasAccess(Guid colle if (collection.UserEmail != loggedInUser) throw new NoAccessException("You don't have access to this collection"); return collection; } - - public async Task GetTotalCollectionsCount(string email) - { - return await collectionsRepository.GetTotalCollectionsCount(email); - } } \ No newline at end of file diff --git a/api/Core/Services/ConditionsLogsService.cs b/api/Core/Services/ConditionsLogsService.cs index 9f60247..d7df5e7 100644 --- a/api/Core/Services/ConditionsLogsService.cs +++ b/api/Core/Services/ConditionsLogsService.cs @@ -1,4 +1,5 @@ using api.Events.Conditions.Server; +using api.Events.PlantEvents.Server; using api.Extensions; using Infrastructure.Repositories; using Shared.Dtos; @@ -45,6 +46,11 @@ public async Task CreateConditionsLogAsync(CreateConditionsLogDto createConditio { ConditionsLog = addedLog }); + var allPlants = await plantService.GetPlantsForUser(email, 1, 100); + connection?.SendDto(new ServerSendsPlants + { + Plants = allPlants + }); if (newMood != recentMood) { diff --git a/api/Core/Services/MqttPublisherService.cs b/api/Core/Services/MqttPublisherService.cs index af0686b..56cd622 100644 --- a/api/Core/Services/MqttPublisherService.cs +++ b/api/Core/Services/MqttPublisherService.cs @@ -22,10 +22,9 @@ public MqttPublisherService(IOptions options) if (string.IsNullOrEmpty(_options.Value.Username) || _options.Value.Username == "FILL_ME_IN") throw new Exception("MQTT username not set in appsettings.json"); } - public async Task PublishAsync(MoodDto mood, long deviceId) + public async Task PublishAsync(MoodDto mood, string deviceId) { var mqttFactory = new MqttFactory(); - using var mqttClient = mqttFactory.CreateMqttClient(); var mqttClientOptions = new MqttClientOptionsBuilder() diff --git a/api/Core/Services/PlantService.cs b/api/Core/Services/PlantService.cs index 5a271d7..2902341 100644 --- a/api/Core/Services/PlantService.cs +++ b/api/Core/Services/PlantService.cs @@ -1,5 +1,7 @@ using api.Core.Options; using api.Core.Services.External.BlobStorage; +using api.Events.Global; +using api.Extensions; using Infrastructure.Repositories; using Microsoft.Extensions.Options; using Shared.Dtos.FromClient.Plant; @@ -13,7 +15,7 @@ public class PlantService( PlantRepository plantRepository, RequirementService requirementService, IBlobStorageService blobStorageService, - IOptions azureBlobStorageOptions) + IOptions azureBlobStorageOptions, WebSocketConnectionService webSocketConnectionService) { public async Task CreatePlant(CreatePlantDto createPlantDto, string loggedInUser) { @@ -28,6 +30,16 @@ public async Task CreatePlant(CreatePlantDto createPlantDto, string logge ímageUrl = await blobStorageService.SaveImageToBlobStorage(createPlantDto.Base64Image, loggedInUser, true); } + + if (!String.IsNullOrEmpty(createPlantDto.DeviceId)) + { + var deviceExists = await plantRepository.DoesDeviceIdExist(createPlantDto.DeviceId); + HandleExistingDeviceId(loggedInUser); + if (deviceExists) + { + createPlantDto.DeviceId = null; + } + } // Insert plant first to get the plantId var plant = new Plant { @@ -40,6 +52,8 @@ public async Task CreatePlant(CreatePlantDto createPlantDto, string logge LatestChange = DateTime.UtcNow }; + + await plantRepository.CreatePlant(plant); // Create requirements for the plant to crete a link between the two @@ -50,6 +64,12 @@ public async Task CreatePlant(CreatePlantDto createPlantDto, string logge return plant; } + private void HandleExistingDeviceId(string loggedInUser) + { + var connection = webSocketConnectionService.GetConnectionByEmail(loggedInUser); + connection?.SendDto(new ServerSendsErrorMessage{Error = "Device ID already in use"}); + } + public async Task GetPlantById(Guid id, string requesterEmail) { var plant = await VerifyPlantExistsAndUserHasAccess(id, requesterEmail); @@ -89,6 +109,20 @@ public async Task UpdatePlant(UpdatePlantDto updatePlantDto, string reque imageUrl = await blobStorageService.SaveImageToBlobStorage(updatePlantDto.Base64Image, requesterEmail, true, plant.ImageUrl); } + if (!String.IsNullOrEmpty(updatePlantDto.DeviceId)) + { + var deviceExists = await plantRepository.DoesDeviceIdExist(updatePlantDto.DeviceId); + if (deviceExists) + { + var plantWithDeviceId = await plantRepository.GetPlantIdByDeviceIdAsync(updatePlantDto.DeviceId); + if (!plantWithDeviceId.ToString().Equals(updatePlantDto.DeviceId)) + { + HandleExistingDeviceId(requesterEmail); + updatePlantDto.DeviceId = null; + } + } + } + // Update the plant plant = new Plant { @@ -134,16 +168,6 @@ public async Task> GetCriticalPlants(string requesterE return criticalPlants; } - - public async Task GetHappyPlantsCount(string userEmail) - { - return await plantRepository.GetHappyPlantsCount(userEmail); - } - - public async Task GetTotalPlantsCount(string userEmail) - { - return await plantRepository.GetTotalPlantsCount(userEmail); - } private string GenerateRandomNickname() { diff --git a/api/Core/Services/StatsService.cs b/api/Core/Services/StatsService.cs new file mode 100644 index 0000000..3722478 --- /dev/null +++ b/api/Core/Services/StatsService.cs @@ -0,0 +1,13 @@ +using Infrastructure.Repositories; +using Shared.Dtos; +using Shared.Models; + +namespace api.Core.Services; + +public class StatsService(PlantRepository plantRepository) +{ + public async Task GetStats(string email) + { + return await plantRepository.GetStats(email); + } +} diff --git a/api/WebSocketConnectionService.cs b/api/Core/Services/WebSocketConnectionService.cs similarity index 83% rename from api/WebSocketConnectionService.cs rename to api/Core/Services/WebSocketConnectionService.cs index 6b57682..af4b500 100644 --- a/api/WebSocketConnectionService.cs +++ b/api/Core/Services/WebSocketConnectionService.cs @@ -1,5 +1,4 @@ using Fleck; -using Shared; using Shared.Wrappers; namespace api; @@ -19,6 +18,12 @@ public void UpdateConnectionEmail(IWebSocketConnection connection, string email) var clientId = connection.ConnectionInfo.Id; _connectedClients[clientId].Email = email; } + + public void RemoveEmailFromConnection(IWebSocketConnection connection) + { + var clientId = connection.ConnectionInfo.Id; + _connectedClients[clientId].Email = null; + } public void RemoveConnection(IWebSocketConnection connection) { diff --git a/api/DbInitializer.cs b/api/DbInitializer.cs new file mode 100644 index 0000000..72f90cf --- /dev/null +++ b/api/DbInitializer.cs @@ -0,0 +1,246 @@ +using api.Core.Services; +using Infrastructure; +using Infrastructure.Repositories; +using Microsoft.EntityFrameworkCore; +using Shared.Dtos; +using Shared.Dtos.FromClient.Collections; +using Shared.Dtos.FromClient.Identity; +using Shared.Dtos.FromClient.Plant; +using Shared.Dtos.FromClient.Requirements; +using Shared.Models; +using Shared.Models.Information; + +namespace api; + +public class DbInitializer(IServiceProvider serviceProvider) +{ + private const string DefaultUserEmail = "bob@botanique.com"; + private readonly IServiceScope _scope = serviceProvider.CreateScope(); + private readonly Dictionary _collections = new(); + private readonly Dictionary _plants = new(); + + public async Task InitializeDatabaseAsync() + { + var scope = serviceProvider.CreateScope(); + var db = await scope.ServiceProvider.GetRequiredService>().CreateDbContextAsync(); + + if (EnvironmentHelper.IsNonProd()) + { + await db.Database.EnsureDeletedAsync(); + } + + await db.Database.EnsureCreatedAsync(); + await db.Database.MigrateAsync(); + } + + public async Task PopulateDatabaseAsync() + { + await CreateUser(); + await CreateCollections(); + await CreatePlants(); + await CreateHistoricalData(); + + Console.ForegroundColor = ConsoleColor.Green; + Console.WriteLine("Database has been populated"); + Console.ResetColor(); + } + + private async Task CreateUser() + { + await _scope.ServiceProvider + .GetRequiredService() + .CreateUser(new RegisterUserDto + { + Email = DefaultUserEmail, + Password = "SuperSecretPassword123!", + Username = "Bob" + }); + } + + private async Task CreateCollections() + { + var collectionService = _scope.ServiceProvider.GetRequiredService(); + var succulents = await collectionService.CreateCollection( + new CreateCollectionDto + { + Name = "Succulents", + }, + DefaultUserEmail + ); + _collections.Add("Succulents", succulents); + + var ferns = await collectionService.CreateCollection( + new CreateCollectionDto + { + Name = "Ferns", + }, + DefaultUserEmail + ); + _collections.Add("Ferns", ferns); + } + + private List GetRequirements() + { + var createRequirementsList = new List + { + // Aloe Vera + new() + { + LightLevel = RequirementLevel.Medium, + SoilMoistureLevel = RequirementLevel.Medium, + HumidityLevel = RequirementLevel.Low, + TemperatureLevel = 22, + }, + // Prickly pear + new() + { + LightLevel = RequirementLevel.High, + SoilMoistureLevel = RequirementLevel.Low, + HumidityLevel = RequirementLevel.Low, + TemperatureLevel = 27, + }, + // Dying plant + new() + { + LightLevel = RequirementLevel.Low, + SoilMoistureLevel = RequirementLevel.High, + HumidityLevel = RequirementLevel.Medium, + TemperatureLevel = 20, + } + }; + + return createRequirementsList; + } + + private async Task CreatePlants() + { + var requirements = GetRequirements(); + + var plantService = _scope.ServiceProvider.GetRequiredService(); + var plant1 = await plantService.CreatePlant( + new CreatePlantDto + { + Nickname = "Aloe Vera", + CreateRequirementsDto = requirements[0], + DeviceId = "264625477326660", + }, DefaultUserEmail + ); + _plants.Add("Aloe Vera", plant1); + + var plant2 = await plantService.CreatePlant( + new CreatePlantDto + { + Nickname = "Prickly Pear", + CreateRequirementsDto = requirements[1], + CollectionId = _collections["Succulents"].CollectionId, + DeviceId = "000000000000001" + }, DefaultUserEmail + ); + _plants.Add("Prickly Pear", plant2); + + var plant3 = await plantService.CreatePlant( + new CreatePlantDto + { + Nickname = "Dying plant", + CollectionId = _collections["Ferns"].CollectionId, + CreateRequirementsDto = requirements[2], + DeviceId = "000000000000002" + }, DefaultUserEmail + ); + _plants.Add("Dying plant", plant3); + } + + private async Task CreateHistoricalData() + { + // Create 100 logs for each plant + var conditionsLogService = _scope.ServiceProvider.GetRequiredService(); + var aloeVeraRequirements = _plants["Aloe Vera"].Requirements!; + var pricklyPearRequirements = _plants["Prickly Pear"].Requirements!; + + for (var i = 0; i < 100; i++) + { + await conditionsLogService.CreateConditionsLogAsync( + new CreateConditionsLogDto + { + DeviceId = _plants["Aloe Vera"].DeviceId!, + SoilMoisturePercentage = GetValueNearOrInIdealRange(aloeVeraRequirements.SoilMoistureLevel), + Light = GetValueNearOrInIdealRange(aloeVeraRequirements.LightLevel), + Temperature = GetRandomTemperature(), + Humidity = GetValueNearOrInIdealRange(aloeVeraRequirements.HumidityLevel), + } + ); + await conditionsLogService.CreateConditionsLogAsync( + new CreateConditionsLogDto + { + DeviceId = _plants["Prickly Pear"].DeviceId!, + SoilMoisturePercentage = GetValueNearOrInIdealRange(pricklyPearRequirements.SoilMoistureLevel), + Light = GetValueNearOrInIdealRange(pricklyPearRequirements.LightLevel), + Temperature = GetRandomTemperature(), + Humidity = GetValueNearOrInIdealRange(pricklyPearRequirements.HumidityLevel), + } + ); + } + + // Adjust the timestamps to be in past + var db = await _scope.ServiceProvider.GetRequiredService>().CreateDbContextAsync(); + var logs = await db.ConditionsLogs.ToListAsync(); + for (var i = 0; i < 100; i++) + { + logs[i].TimeStamp = logs[i].TimeStamp.AddDays(-i); + logs[i + 100].TimeStamp = logs[i + 100].TimeStamp.AddDays(-i); + } + db.UpdateRange(logs); + await db.SaveChangesAsync(); + + var dyingPlant = _plants["Dying plant"]; + await conditionsLogService.CreateConditionsLogAsync( + + new CreateConditionsLogDto + { + DeviceId = dyingPlant.DeviceId!, + SoilMoisturePercentage = GetValueOutsideOfIdealRange(dyingPlant.Requirements!.SoilMoistureLevel), + Light = GetValueOutsideOfIdealRange(dyingPlant.Requirements!.LightLevel), + Temperature = dyingPlant.Requirements!.TemperatureLevel - 10, + Humidity = GetValueOutsideOfIdealRange(dyingPlant.Requirements!.HumidityLevel), + } + ); + } + + private double GetRandomLevelValue() + { + var random = new Random(); + return random.NextDouble() * 100; + } + + private int GetRandomTemperature() + { + var random = new Random(); + return random.Next(-20, 45); + } + + private double GetValueNearOrInIdealRange(RequirementLevel level) + { + var idealValueRange = level.GetRange(); + var random = new Random(); + var value = random.NextDouble() * 100; + while (value < idealValueRange.Min - 10 || value > idealValueRange.Max + 10) + { + value = random.NextDouble() * 100; + } + + return value; + } + + private double GetValueOutsideOfIdealRange(RequirementLevel level) + { + var idealValueRange = level.GetRange(); + var random = new Random(); + var value = random.NextDouble() * 100; + while (value > idealValueRange.Min && value < idealValueRange.Max) + { + value = random.NextDouble() * 100; + } + + return value; + } +} \ No newline at end of file diff --git a/api/EnvironmentHelper.cs b/api/EnvironmentHelper.cs new file mode 100644 index 0000000..8546b43 --- /dev/null +++ b/api/EnvironmentHelper.cs @@ -0,0 +1,26 @@ +namespace api; + +public static class EnvironmentHelper +{ + private static readonly List NonProdEnvironments = ["Development", "Testing"]; + + public static bool IsTesting() + { + return Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") == "Testing"; + } + + public static bool IsDevelopment() + { + return Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") == "Development"; + } + + public static bool IsNonProd() + { + return NonProdEnvironments.Contains(Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT")); + } + + public static bool IsCi() + { + return Environment.GetEnvironmentVariable("CI") == "true"; + } +} \ No newline at end of file diff --git a/api/Events/Auth/Client/ClientWantsToCheckJwtValidity.cs b/api/Events/Auth/Client/ClientWantsToCheckJwtValidity.cs index d50a3f1..ef779a8 100644 --- a/api/Events/Auth/Client/ClientWantsToCheckJwtValidity.cs +++ b/api/Events/Auth/Client/ClientWantsToCheckJwtValidity.cs @@ -1,6 +1,3 @@ -using api.Core.Services; -using api.Events.Auth.Server; -using api.Extensions; using Fleck; using lib; using Shared.Models; @@ -14,16 +11,10 @@ public class ClientWantsToCheckJwtValidityDto : BaseDtoWithJwt; /// If the token is not valid, an exception will be thrown, and the GlobalExceptionHandler will catch it, and send a /// corresponding message to the client. /// -public class ClientWantsToCheckJwtValidity(WebSocketConnectionService webSocketConnectionService, JwtService jwtService) : BaseEventHandler +public class ClientWantsToCheckJwtValidity(InitialDataHelper initialDataHelper) : BaseEventHandler { - public override Task Handle(ClientWantsToCheckJwtValidityDto dto, IWebSocketConnection socket) + public override async Task Handle(ClientWantsToCheckJwtValidityDto dto, IWebSocketConnection socket) { - var email = jwtService.GetEmailFromJwt(dto.Jwt); - webSocketConnectionService.UpdateConnectionEmail(socket, email); - socket.SendDto(new ServerAuthenticatesUser - { - Jwt = dto.Jwt, - }); - return Task.CompletedTask; + await initialDataHelper.SendInitialData(socket, dto.Jwt!); } } \ No newline at end of file diff --git a/api/Events/Auth/Client/ClientWantsToLogIn.cs b/api/Events/Auth/Client/ClientWantsToLogIn.cs index 2477d41..f652c38 100644 --- a/api/Events/Auth/Client/ClientWantsToLogIn.cs +++ b/api/Events/Auth/Client/ClientWantsToLogIn.cs @@ -1,11 +1,6 @@ using api.Core.Services; -using api.Core.Services.External.BlobStorage; -using api.Events.Auth.Server; -using api.Events.Global; -using api.Extensions; using Fleck; using lib; -using Shared.Dtos; using Shared.Dtos.FromClient.Identity; using Shared.Exceptions; @@ -16,7 +11,7 @@ public class ClientWantsToLogInDto : BaseDto public LoginDto LoginDto { get; set; } = null!; } -public class ClientWantsToLogIn(WebSocketConnectionService webSocketConnectionService, UserService userService, IBlobStorageService blobStorageService) +public class ClientWantsToLogIn(UserService userService, InitialDataHelper initialDataHelper) : BaseEventHandler { public override async Task Handle(ClientWantsToLogInDto dto, IWebSocketConnection socket) @@ -24,30 +19,7 @@ public override async Task Handle(ClientWantsToLogInDto dto, IWebSocketConnectio var jwt = await userService.Login(dto.LoginDto); if (jwt == null) throw new InvalidCredentialsException(); - var user = await userService.GetUserByEmail(dto.LoginDto.Email); - - webSocketConnectionService.UpdateConnectionEmail(socket, dto.LoginDto.Email); - - var getUserDto = new GetUserDto - { - UserEmail = user.UserEmail, - Username = user.UserName, - }; - - if (!string.IsNullOrEmpty(user.BlobUrl)) - { - getUserDto.BlobUrl = blobStorageService.GenerateSasUri(user.BlobUrl, false); - } - - socket.SendDto(new ServerAuthenticatesUser - { - Jwt = jwt, - - }); - socket.SendDto(new ServerSendsUserInfo - { - GetUserDto = getUserDto - }); + await initialDataHelper.SendInitialData(socket, jwt); } } diff --git a/api/Events/Auth/Client/ClientWantsToLogOut.cs b/api/Events/Auth/Client/ClientWantsToLogOut.cs index dd514a4..a361ecd 100644 --- a/api/Events/Auth/Client/ClientWantsToLogOut.cs +++ b/api/Events/Auth/Client/ClientWantsToLogOut.cs @@ -12,7 +12,7 @@ public class ClientWantsToLogOut(WebSocketConnectionService connectionService) { public override Task Handle(ClientWantsToLogOutDto dto, IWebSocketConnection socket) { - connectionService.RemoveConnection(socket); + connectionService.RemoveEmailFromConnection(socket); socket.SendDto(new ServerLogsOutUser()); return Task.CompletedTask; } diff --git a/api/Events/Auth/Client/InitialDataHelper.cs b/api/Events/Auth/Client/InitialDataHelper.cs new file mode 100644 index 0000000..e78b41f --- /dev/null +++ b/api/Events/Auth/Client/InitialDataHelper.cs @@ -0,0 +1,63 @@ +using api.Core.Services; +using api.Core.Services.External.BlobStorage; +using api.Events.Auth.Server; +using api.Events.Collections.Server; +using api.Events.Global; +using api.Events.PlantEvents.Server; +using api.Events.Statistics; +using api.Extensions; +using Fleck; +using Shared.Dtos; + +namespace api.Events.Auth.Client; + +public class InitialDataHelper(PlantService plantService, IBlobStorageService blobStorageService, CollectionsService collectionsService, StatsService statsService, UserService userService, WebSocketConnectionService webSocketConnectionService, JwtService jwtService) +{ + + public async Task SendInitialData(IWebSocketConnection socket, string jwt) + { + var email = jwtService.GetEmailFromJwt(jwt); + webSocketConnectionService.UpdateConnectionEmail(socket, email); + + var user = await userService.GetUserByEmail(email); + + socket.SendDto(new ServerAuthenticatesUser + { + Jwt = jwt, + }); + + var criticalPlants = await plantService.GetCriticalPlants(user.UserEmail); + socket.SendDto(new ServerSendsCriticalPlants + { + Plants = criticalPlants + }); + + var getUserDto = new GetUserDto + { + UserEmail = user.UserEmail, + Username = user.UserName, + }; + + if (!string.IsNullOrEmpty(user.BlobUrl)) + { + getUserDto.BlobUrl = blobStorageService.GenerateSasUri(user.BlobUrl, false); + } + + socket.SendDto(new ServerSendsUserInfo + { + GetUserDto = getUserDto + }); + + var allCollections = await collectionsService.GetCollectionsForUser(user.UserEmail); + socket.SendDto(new ServerSendsAllCollections + { + Collections = allCollections.ToList() + }); + + var stats = await statsService.GetStats(user.UserEmail); + socket.SendDto(new ServerSendsStats + { + Stats = stats + }); + } +} \ No newline at end of file diff --git a/api/Events/Collections/Client/ClientWantsToCreateCollection.cs b/api/Events/Collections/Client/ClientWantsToCreateCollection.cs index 7298b0e..628461a 100644 --- a/api/Events/Collections/Client/ClientWantsToCreateCollection.cs +++ b/api/Events/Collections/Client/ClientWantsToCreateCollection.cs @@ -1,5 +1,6 @@ using api.Core.Services; using api.Events.Collections.Server; +using api.Events.Statistics; using api.Extensions; using Fleck; using lib; @@ -13,16 +14,20 @@ public class ClientWantsToCreateCollectionDto : BaseDtoWithJwt public required CreateCollectionDto CreateCollectionDto { get; set; } } -public class ClientWantsToCreateCollection(CollectionsService collectionsService, JwtService jwtService) : BaseEventHandler +public class ClientWantsToCreateCollection(CollectionsService collectionsService, JwtService jwtService, StatsService statsService) : BaseEventHandler { public override async Task Handle(ClientWantsToCreateCollectionDto dto, IWebSocketConnection socket) { var email = jwtService.GetEmailFromJwt(dto.Jwt!); await collectionsService.CreateCollection(dto.CreateCollectionDto, email); var allCollections = await collectionsService.GetCollectionsForUser(email); - socket.SendDto(new ServerSendsAllCollections() + + socket.SendDto(new ServerSendsAllCollections { Collections = allCollections.ToList() }); + + var stats = await statsService.GetStats(email); + socket.SendDto(new ServerSendsStats{Stats = stats}); } } \ No newline at end of file diff --git a/api/Events/Collections/Client/ClientWantsToDeleteCollection.cs b/api/Events/Collections/Client/ClientWantsToDeleteCollection.cs index 08bc019..69966f9 100644 --- a/api/Events/Collections/Client/ClientWantsToDeleteCollection.cs +++ b/api/Events/Collections/Client/ClientWantsToDeleteCollection.cs @@ -1,5 +1,6 @@ using api.Core.Services; using api.Events.Collections.Server; +using api.Events.Statistics; using api.Extensions; using Fleck; using lib; @@ -13,16 +14,20 @@ public class ClientWantsToDeleteCollectionDto : BaseDtoWithJwt public Guid CollectionId { get; set; } } -public class ClientWantsToDeleteCollection(CollectionsService collectionsService, JwtService jwtService) : BaseEventHandler +public class ClientWantsToDeleteCollection(CollectionsService collectionsService, JwtService jwtService, StatsService statsService) : BaseEventHandler { public override async Task Handle(ClientWantsToDeleteCollectionDto dto, IWebSocketConnection socket) { var email = jwtService.GetEmailFromJwt(dto.Jwt!); await collectionsService.DeleteCollection(dto.CollectionId, email); var allCollections = await collectionsService.GetCollectionsForUser(email); + + var stats = await statsService.GetStats(email); socket.SendDto(new ServerSendsAllCollections() { Collections = allCollections.ToList() }); + + socket.SendDto(new ServerSendsStats{Stats = stats}); } } \ No newline at end of file diff --git a/api/Events/Collections/Client/ClientWantsToUpdateCollection.cs b/api/Events/Collections/Client/ClientWantsToUpdateCollection.cs index 4c5b632..52d25ef 100644 --- a/api/Events/Collections/Client/ClientWantsToUpdateCollection.cs +++ b/api/Events/Collections/Client/ClientWantsToUpdateCollection.cs @@ -18,7 +18,7 @@ public class ClientWantsToUpdateCollection(CollectionsService collectionsService public override async Task Handle(ClientWantsToUpdateCollectionDto dto, IWebSocketConnection socket) { var email = jwtService.GetEmailFromJwt(dto.Jwt!); - var collection = await collectionsService.UpdateCollection(dto.UpdateCollectionDto, email); + await collectionsService.UpdateCollection(dto.UpdateCollectionDto, email); var allCollections = await collectionsService.GetCollectionsForUser(email); socket.SendDto(new ServerSendsAllCollections() { diff --git a/api/Events/PlantEvents/Client/ClientWantsToCreatePlant.cs b/api/Events/PlantEvents/Client/ClientWantsToCreatePlant.cs index eaf2632..b69bb98 100644 --- a/api/Events/PlantEvents/Client/ClientWantsToCreatePlant.cs +++ b/api/Events/PlantEvents/Client/ClientWantsToCreatePlant.cs @@ -1,6 +1,7 @@ using api.Core.Services; using api.EventFilters; using api.Events.PlantEvents.Server; +using api.Events.Statistics; using api.Extensions; using Fleck; using lib; @@ -11,11 +12,11 @@ namespace api.Events.PlantEvents.Client; public class ClientWantsToCreatePlantDto: BaseDtoWithJwt { - public CreatePlantDto CreatePlantDto { get; set; } + public required CreatePlantDto CreatePlantDto { get; set; } } [ValidateDataAnnotations] -public class ClientWantsToCreatePlant(PlantService plantService, JwtService jwtService): BaseEventHandler +public class ClientWantsToCreatePlant(PlantService plantService, JwtService jwtService, StatsService statsService): BaseEventHandler { public override async Task Handle(ClientWantsToCreatePlantDto dto, IWebSocketConnection socket) { @@ -26,7 +27,15 @@ public override async Task Handle(ClientWantsToCreatePlantDto dto, IWebSocketCon { Plant = plant }; - socket.SendDto(serverCreatesNewPlant); + + var allPlants = await plantService.GetPlantsForUser(email, 1, 100); + socket.SendDto(new ServerSendsPlants + { + Plants = allPlants + }); + + var stats = await statsService.GetStats(email); + socket.SendDto(new ServerSendsStats{Stats = stats}); } } \ No newline at end of file diff --git a/api/Events/PlantEvents/Client/ClientWantsToDeletePlant.cs b/api/Events/PlantEvents/Client/ClientWantsToDeletePlant.cs index bb4068f..9c3255d 100644 --- a/api/Events/PlantEvents/Client/ClientWantsToDeletePlant.cs +++ b/api/Events/PlantEvents/Client/ClientWantsToDeletePlant.cs @@ -3,6 +3,7 @@ using api.EventFilters; using api.Events.Global; using api.Events.PlantEvents.Server; +using api.Events.Statistics; using api.Extensions; using Fleck; using lib; @@ -16,13 +17,23 @@ public class ClientWantsToDeletePlantDto: BaseDtoWithJwt } [ValidateDataAnnotations] -public class ClientWantsToDeletePlant(PlantService plantService, JwtService jwtService): BaseEventHandler +public class ClientWantsToDeletePlant(PlantService plantService, JwtService jwtService, StatsService statsService): BaseEventHandler { public override async Task Handle(ClientWantsToDeletePlantDto dto, IWebSocketConnection socket) { var email = jwtService.GetEmailFromJwt(dto.Jwt!); + await plantService.DeletePlant(dto.PlantId, email); socket.SendDto( new ServerConfirmsDelete()); + + var allPlants = await plantService.GetPlantsForUser(email, 1, 100); + socket.SendDto(new ServerSendsPlants + { + Plants = allPlants + }); + + var stats = await statsService.GetStats(email); + socket.SendDto(new ServerSendsStats{Stats = stats}); } } diff --git a/api/Events/PlantEvents/Client/ClientWantsToGetCriticalPlants.cs b/api/Events/PlantEvents/Client/ClientWantsToGetCriticalPlants.cs index 01b27f3..5b92e63 100644 --- a/api/Events/PlantEvents/Client/ClientWantsToGetCriticalPlants.cs +++ b/api/Events/PlantEvents/Client/ClientWantsToGetCriticalPlants.cs @@ -1,5 +1,6 @@ using api.Core.Services; using api.Events.PlantEvents.Server; +using api.Events.Statistics; using api.Extensions; using Fleck; using lib; @@ -9,16 +10,20 @@ namespace api.Events.PlantEvents.Client; public class ClientWantsToGetCriticalPlantsDto : BaseDtoWithJwt; -public class ClientWantsToGetCriticalPlants(JwtService jwtService, PlantService plantService) : BaseEventHandler +public class ClientWantsToGetCriticalPlants(JwtService jwtService, PlantService plantService, StatsService statsService) : BaseEventHandler { public override async Task Handle(ClientWantsToGetCriticalPlantsDto dto, IWebSocketConnection socket) { var email = jwtService.GetEmailFromJwt(dto.Jwt!); var plants = await plantService.GetCriticalPlants(email); + var stats = await statsService.GetStats(email); + var serverResponse = new ServerSendsCriticalPlants { Plants = plants }; socket.SendDto(serverResponse); + + socket.SendDto(new ServerSendsStats{Stats = stats}); } } \ No newline at end of file diff --git a/api/Events/PlantEvents/Client/ClientWantsToUpdatePlant.cs b/api/Events/PlantEvents/Client/ClientWantsToUpdatePlant.cs index 9f2b8ce..84757ff 100644 --- a/api/Events/PlantEvents/Client/ClientWantsToUpdatePlant.cs +++ b/api/Events/PlantEvents/Client/ClientWantsToUpdatePlant.cs @@ -1,5 +1,6 @@ using api.Core.Services; using api.Events.PlantEvents.Server; +using api.Events.Statistics; using api.Extensions; using Fleck; using lib; @@ -13,15 +14,25 @@ public class ClientWantsToUpdatePlantDto: BaseDtoWithJwt public required UpdatePlantDto UpdatePlantDto { get; set; } } -public class ClientWantsToUpdatePlant(PlantService plantService, JwtService jwtService): BaseEventHandler +public class ClientWantsToUpdatePlant(PlantService plantService, JwtService jwtService, StatsService statsService): BaseEventHandler { public override async Task Handle(ClientWantsToUpdatePlantDto dto, IWebSocketConnection socket) { var email = jwtService.GetEmailFromJwt(dto.Jwt!); + var plant = await plantService.UpdatePlant(dto.UpdatePlantDto, email); socket.SendDto(new ServerSavesPlant { Plant = plant }); + + var allPlants = await plantService.GetPlantsForUser(email, 1, 100); + socket.SendDto(new ServerSendsPlants + { + Plants = allPlants + }); + + var stats = await statsService.GetStats(email); + socket.SendDto(new ServerSendsStats{Stats = stats}); } } \ No newline at end of file diff --git a/api/Events/Statistics/ClientWantsStats.cs b/api/Events/Statistics/ClientWantsStats.cs new file mode 100644 index 0000000..0220931 --- /dev/null +++ b/api/Events/Statistics/ClientWantsStats.cs @@ -0,0 +1,28 @@ +using api.Core.Services; +using api.Extensions; +using Fleck; +using lib; +using Shared.Models; + +namespace api.Events.Statistics; + +public class ClientWantsStatsDto : BaseDtoWithJwt +{ + +} + +public class ClientWantsStats(StatsService statsService, JwtService jwtService) : BaseEventHandler +{ + public override async Task Handle(ClientWantsStatsDto dto, IWebSocketConnection socket) + { + var email = jwtService.GetEmailFromJwt(dto.Jwt!); + + var stats = await statsService.GetStats(email); + socket.SendDto(new ServerSendsStats{Stats = stats}); + } +} + +public class ServerSendsStats : BaseDto +{ + public Stats Stats { get; set; } = null!; +} diff --git a/api/Events/Stats/ClientWantsStats.cs b/api/Events/Stats/ClientWantsStats.cs deleted file mode 100644 index b581887..0000000 --- a/api/Events/Stats/ClientWantsStats.cs +++ /dev/null @@ -1,48 +0,0 @@ -using api.Core.Services; -using api.Extensions; -using Fleck; -using lib; -using Shared.Models; - -namespace api.Events.Stats; - -public class ClientWantsStatsDto : BaseDtoWithJwt -{ - -} - -public class ClientWantsStats(PlantService plantService, CollectionsService collectionsService, JwtService jwtService) : BaseEventHandler -{ - public override async Task Handle(ClientWantsStatsDto dto, IWebSocketConnection socket) - { - var email = jwtService.GetEmailFromJwt(dto.Jwt!); - - var totalPlants = await plantService.GetTotalPlantsCount(email); - var happyPlants = await plantService.GetHappyPlantsCount(email); - var collections = await collectionsService.GetTotalCollectionsCount(email); - - var statsDto = new ServerSendsStats - { - Stats = new Stats - { - TotalPlants = totalPlants, - HappyPlants = happyPlants, - Collections = collections - } - }; - - socket.SendDto(statsDto); - } -} - -public class ServerSendsStats : BaseDto -{ - public Stats Stats { get; set; } -} - -public class Stats -{ - public int TotalPlants { get; set; } - public int HappyPlants { get; set; } - public int Collections { get; set; } -} \ No newline at end of file diff --git a/api/Extensions/AddServicesAndRepositoriesExtension.cs b/api/Extensions/AddServicesAndRepositoriesExtension.cs index 35b8d38..116f95c 100644 --- a/api/Extensions/AddServicesAndRepositoriesExtension.cs +++ b/api/Extensions/AddServicesAndRepositoriesExtension.cs @@ -1,6 +1,7 @@ using api.Core.Services; using api.Core.Services.External.BackgroundRemoval; using api.Core.Services.External.BlobStorage; +using api.Events.Auth.Client; using Infrastructure.Repositories; namespace api.Extensions; @@ -26,9 +27,13 @@ public static void AddServicesAndRepositories(this IServiceCollection services) services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); + + // Helpers + services.AddSingleton(); // External services - if (Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") == "Testing") + if (EnvironmentHelper.IsTesting()) { services.AddSingleton(); services.AddSingleton(); diff --git a/api/Extensions/ConfigureExtensions.cs b/api/Extensions/ConfigureExtensions.cs index e3c2ee4..9f64ec4 100644 --- a/api/Extensions/ConfigureExtensions.cs +++ b/api/Extensions/ConfigureExtensions.cs @@ -6,7 +6,7 @@ public static class ConfigureExtensions { public static void ConfigureOptions(this WebApplicationBuilder builder) { - if (Environment.GetEnvironmentVariable("CI") is not null) + if (EnvironmentHelper.IsCi()) { builder.Services.Configure(options => { @@ -25,7 +25,16 @@ public static void ConfigureOptions(this WebApplicationBuilder builder) options.SubscribeTopic = Environment.GetEnvironmentVariable("MQTT_SUBSCRIBE_TOPIC") ?? throw new Exception("MQTT subscribe topic is missing"); options.PublishTopic = Environment.GetEnvironmentVariable("MQTT_PUBLISH_TOPIC") ?? throw new Exception("MQTT publish topic is missing"); }); - + + if (EnvironmentHelper.IsTesting()) + { + builder.Services.Configure(options => + { + options.DefaultPlantImageUrl = "https://example.com"; + }); + return; + } + builder.Services.Configure(options => { options.RemoveBackgroundEndpoint = Environment.GetEnvironmentVariable("AZURE_VISION_REMOVE_BACKGROUND_ENDPOINT") ?? throw new Exception("Azure Vision endpoint is missing"); diff --git a/api/GlobalExceptionHandler.cs b/api/GlobalExceptionHandler.cs index 511ca11..2d878c0 100644 --- a/api/GlobalExceptionHandler.cs +++ b/api/GlobalExceptionHandler.cs @@ -28,10 +28,11 @@ public static void Handle(this Exception ex, IWebSocketConnection socket, string _ => new ServerSendsErrorMessage { Error = message ?? ex.Message } }; else - serverResponse = new ServerSendsErrorMessage - { - Error = "Something went wrong. Please try again later." - }; + { + serverResponse = EnvironmentHelper.IsTesting() + ? new ServerSendsErrorMessage { Error = ex.Message } + : new ServerSendsErrorMessage { Error = "Something went wrong. Please try again later." }; + } socket.SendDto(serverResponse); } diff --git a/api/Program.cs b/api/Program.cs index c833a96..33f6cda 100644 --- a/api/Program.cs +++ b/api/Program.cs @@ -1,6 +1,5 @@ using System.Reflection; using System.Text.Json; -using api.Core.Options; using api.Core.Services; using api.Events.Auth.Client; using api.Extensions; @@ -9,7 +8,6 @@ using lib; using Microsoft.EntityFrameworkCore; using Serilog; -using Shared.Dtos; using Shared.Exceptions; using Shared.Models; using Testcontainers.PostgreSql; @@ -26,6 +24,11 @@ public static class Startup nameof(ClientWantsToSignUp) ]; + private static readonly JsonSerializerOptions JsonSerializerOptions = new() + { + PropertyNameCaseInsensitive = true + }; + public static async Task Main(string[] args) { var app = await StartApi(args); @@ -46,7 +49,7 @@ public static async Task StartApi(string[] args) var builder = WebApplication.CreateBuilder(args); - if (Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") == "Testing") + if (EnvironmentHelper.IsTesting()) { var dbContainer = new PostgreSqlBuilder() @@ -57,13 +60,12 @@ public static async Task StartApi(string[] args) await dbContainer.StartAsync(); - var connectionString = dbContainer.GetConnectionString(); + var connectionString = dbContainer.GetConnectionString() + ";Include Error Detail=true"; builder.Services.AddDbContextFactory(options => { options.UseNpgsql(connectionString ?? throw new Exception("Connection string cannot be null")); }); } - else { var connectionString = builder.Configuration.GetConnectionString("BotaniqueDb"); @@ -81,23 +83,12 @@ public static async Task StartApi(string[] args) var app = builder.Build(); + // be careful with using --db-init on production, it will delete all data if (args.Contains("--db-init")) { - var scope = app.Services.CreateScope(); - var db = await app.Services.GetRequiredService>().CreateDbContextAsync(); - - if (Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") == "Development") - { - await db.Database.EnsureDeletedAsync(); - } - - await db.Database.EnsureCreatedAsync(); - await db.Database.MigrateAsync(); - - if (Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") == "Development") - { - await db.SeedDevelopmentDataAsync(scope, app.Configuration["AzureBlob:DefaultPlantImageUrl"] ?? "https://example.com"); - } + var dbInitializer = new DbInitializer(app.Services); + await dbInitializer.InitializeDatabaseAsync(); + await dbInitializer.PopulateDatabaseAsync(); } builder.WebHost.UseUrls("http://*:9999"); @@ -135,7 +126,7 @@ public static async Task StartApi(string[] args) try { // Check if the message contains a JWT token and if it is valid - var dto = JsonSerializer.Deserialize(message, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); + var dto = JsonSerializer.Deserialize(message, JsonSerializerOptions); if (dto is not null && PublicEvents.Contains(dto.eventType) == false) { if (dto.Jwt is null) diff --git a/api/logo.png b/api/logo.png deleted file mode 100644 index 0ac63e4..0000000 Binary files a/api/logo.png and /dev/null differ diff --git a/readme.md b/readme.md index 5035395..8c3032f 100644 --- a/readme.md +++ b/readme.md @@ -1,3 +1,30 @@ +# BotaniQue Backend + +## Overview + +BotaniQue is a project designed to simplify plant care by providing real-time monitoring and insights into plant conditions. This backend server is a key part of the BotaniQue ecosystem, supporting data processing, real-time updates, and integration with cloud services. + +Created as an exam project by Júlia Ilášová and Maria Nielsen + +![image](https://github.com/Team-Wilhelm/BotaniQue-Fullstack/assets/113031776/3186f28e-8c35-4f65-a378-7affeaf01646) + +## Project Components + +* Backend Server: Processes data from the IoT device, determines plant health, and provides real-time updates as well as handling authentication, data storage and business logic for the mobile app. +* [IoT Device](https://github.com/team-wilhelm/botanique-iot) (Smart Plant Pot): An ESP32 microcontroller with sensors for soil moisture, air humidity, temperature, and light, and an OLED screen for displaying the plant's mood. +* [Mobile App](https://team-wilhelm.github.io/BotaniQue-MobDev/) (Frontend application): Developed in Flutter, it allows users to manage plants and view their conditions. + +## Technologies Used + +* Programming Language: .NET 8 (backend) +* MQTT Broker: Flespi +* Database: PostgreSQL +* WebSockets(using Fleck): For real-time communication +* Azure Cognitive Services: Azure vision for background removal from images +* Azure Blob Storage: For image storage + +## Project Setup + Run database: ```bash docker run --name botanique_db -p 5432:5432 -e POSTGRES_PASSWORD=password -e POSTGRES_USER=root -d postgres:14