diff --git a/src/TrackApi/.idea/.idea.TrackQrApi/.idea/.gitignore b/src/TrackApi/.idea/.idea.TrackQrApi/.idea/.gitignore new file mode 100644 index 0000000..689a689 --- /dev/null +++ b/src/TrackApi/.idea/.idea.TrackQrApi/.idea/.gitignore @@ -0,0 +1,13 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Rider ignored files +/projectSettingsUpdater.xml +/modules.xml +/contentModel.xml +/.idea.TrackQrApi.iml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/src/TrackApi/.idea/.idea.TrackQrApi/.idea/.name b/src/TrackApi/.idea/.idea.TrackQrApi/.idea/.name new file mode 100644 index 0000000..3198ac7 --- /dev/null +++ b/src/TrackApi/.idea/.idea.TrackQrApi/.idea/.name @@ -0,0 +1 @@ +TrackQrApi \ No newline at end of file diff --git a/src/TrackApi/.idea/.idea.TrackQrApi/.idea/dataSources.xml b/src/TrackApi/.idea/.idea.TrackQrApi/.idea/dataSources.xml new file mode 100644 index 0000000..0fdf5d2 --- /dev/null +++ b/src/TrackApi/.idea/.idea.TrackQrApi/.idea/dataSources.xml @@ -0,0 +1,13 @@ + + + + + postgresql + true + true + org.postgresql.Driver + jdbc:postgresql://localhost:5400/trakqr?password=P%40ssword123%21&user=sa + $ProjectFileDir$ + + + \ No newline at end of file diff --git a/src/TrackApi/.idea/.idea.TrackQrApi/.idea/encodings.xml b/src/TrackApi/.idea/.idea.TrackQrApi/.idea/encodings.xml new file mode 100644 index 0000000..df87cf9 --- /dev/null +++ b/src/TrackApi/.idea/.idea.TrackQrApi/.idea/encodings.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/src/TrackApi/.idea/.idea.TrackQrApi/.idea/indexLayout.xml b/src/TrackApi/.idea/.idea.TrackQrApi/.idea/indexLayout.xml new file mode 100644 index 0000000..7b08163 --- /dev/null +++ b/src/TrackApi/.idea/.idea.TrackQrApi/.idea/indexLayout.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/src/TrackApi/.idea/.idea.TrackQrApi/.idea/vcs.xml b/src/TrackApi/.idea/.idea.TrackQrApi/.idea/vcs.xml new file mode 100644 index 0000000..b2bdec2 --- /dev/null +++ b/src/TrackApi/.idea/.idea.TrackQrApi/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/api.Tests/.gitignore b/src/TrackApi/TrackQrApi.Tests/.gitignore similarity index 100% rename from src/api.Tests/.gitignore rename to src/TrackApi/TrackQrApi.Tests/.gitignore diff --git a/src/api.Tests/AnalyticsEndpointTests.cs b/src/TrackApi/TrackQrApi.Tests/AnalyticsEndpointTests.cs similarity index 93% rename from src/api.Tests/AnalyticsEndpointTests.cs rename to src/TrackApi/TrackQrApi.Tests/AnalyticsEndpointTests.cs index 3c1c0cf..26d278d 100644 --- a/src/api.Tests/AnalyticsEndpointTests.cs +++ b/src/TrackApi/TrackQrApi.Tests/AnalyticsEndpointTests.cs @@ -1,36 +1,31 @@ using System.Net; using System.Net.Http.Headers; using System.Net.Http.Json; -using api.Features.Analytics.Common; -using api.Features.Auth.Common; -using api.Features.Links.Common; -using api.Features.Workspaces.Common; using FluentAssertions; using Microsoft.AspNetCore.Mvc.Testing; +using TrackQrApi.Features.Analytics.Common; +using TrackQrApi.Features.Auth.Common; +using TrackQrApi.Features.Links.Common; +using TrackQrApi.Features.Workspaces.Common; -namespace Api.Tests; +namespace TrackQrApi.Tests; -public class AnalyticsEndpointTests : IClassFixture +public class AnalyticsEndpointTests( + ApiWebApplicationFactory factory) + : IClassFixture { - private readonly HttpClient _client; - private readonly HttpClient _noRedirectClient; + private readonly HttpClient _client = factory.CreateClient(); - public AnalyticsEndpointTests(ApiWebApplicationFactory factory) + private readonly HttpClient _noRedirectClient = factory.CreateClient(new WebApplicationFactoryClientOptions { - _client = factory.CreateClient(); - _noRedirectClient = factory.CreateClient(new WebApplicationFactoryClientOptions - { - AllowAutoRedirect = false - }); - } + AllowAutoRedirect = false + }); private async Task<(string Token, Guid WorkspaceId)> SetupAuthAndWorkspaceAsync(string email) { var response = await _client.PostAsJsonAsync("/auth/register", new { Email = email, Password = "password123" }); if (response.StatusCode == HttpStatusCode.Conflict) - { response = await _client.PostAsJsonAsync("/auth/login", new { Email = email, Password = "password123" }); - } var authResult = await response.Content.ReadFromJsonAsync(); var token = authResult!.Token; @@ -217,4 +212,4 @@ public class AnalyticsEndpointTests : IClassFixture // Assert response.StatusCode.Should().Be(HttpStatusCode.NotFound); } -} +} \ No newline at end of file diff --git a/src/api.Tests/ApiWebApplicationFactory.cs b/src/TrackApi/TrackQrApi.Tests/ApiWebApplicationFactory.cs similarity index 85% rename from src/api.Tests/ApiWebApplicationFactory.cs rename to src/TrackApi/TrackQrApi.Tests/ApiWebApplicationFactory.cs index 06d676b..17affa0 100644 --- a/src/api.Tests/ApiWebApplicationFactory.cs +++ b/src/TrackApi/TrackQrApi.Tests/ApiWebApplicationFactory.cs @@ -1,20 +1,37 @@ -using api.Data; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Mvc.Testing; using Microsoft.AspNetCore.TestHost; using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Testcontainers.PostgreSql; +using TrackQrApi.Data; +using TrackQrApi.Models; -namespace Api.Tests; +namespace TrackQrApi.Tests; public sealed class ApiWebApplicationFactory : WebApplicationFactory, IAsyncLifetime { private readonly PostgreSqlContainer _postgres = new PostgreSqlBuilder("postgres:latest") .Build(); - private bool _containerStarted = false; + private bool _containerStarted; + + public async Task InitializeAsync() + { + // Ensure container is started (might already be started from ConfigureWebHost) + EnsureContainerStarted(); + + // Run migrations + using var scope = Services.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + await db.Database.MigrateAsync(); + } + + public new async Task DisposeAsync() + { + await _postgres.DisposeAsync(); + await base.DisposeAsync(); + } private void EnsureContainerStarted() { @@ -47,40 +64,18 @@ public sealed class ApiWebApplicationFactory : WebApplicationFactory, I builder.ConfigureTestServices(services => { // Remove existing DbContext registration - var descriptor = services.SingleOrDefault( - d => d.ServiceType == typeof(DbContextOptions)); + var descriptor = services.SingleOrDefault(d => d.ServiceType == typeof(DbContextOptions)); - if (descriptor != null) - { - services.Remove(descriptor); - } + if (descriptor != null) services.Remove(descriptor); // Add DbContext with Testcontainers connection string services.AddDbContext(options => options.UseNpgsql(_postgres.GetConnectionString())); - }); } - public async Task InitializeAsync() - { - // Ensure container is started (might already be started from ConfigureWebHost) - EnsureContainerStarted(); - - // Run migrations - using var scope = Services.CreateScope(); - var db = scope.ServiceProvider.GetRequiredService(); - await db.Database.MigrateAsync(); - } - - public new async Task DisposeAsync() - { - await _postgres.DisposeAsync(); - await base.DisposeAsync(); - } - /// - /// Upgrades a workspace to Pro plan for testing features that require paid plans. + /// Upgrades a workspace to Pro plan for testing features that require paid plans. /// public async Task UpgradeWorkspaceToPro(Guid workspaceId) { @@ -89,8 +84,8 @@ public sealed class ApiWebApplicationFactory : WebApplicationFactory, I var workspace = await db.Workspaces.FindAsync(workspaceId); if (workspace != null) { - workspace.Plan = api.Models.WorkspacePlan.Pro; + workspace.Plan = WorkspacePlan.Pro; await db.SaveChangesAsync(); } } -} +} \ No newline at end of file diff --git a/src/api.Tests/AssetEndpointTests.cs b/src/TrackApi/TrackQrApi.Tests/AssetEndpointTests.cs similarity index 97% rename from src/api.Tests/AssetEndpointTests.cs rename to src/TrackApi/TrackQrApi.Tests/AssetEndpointTests.cs index b401fd2..564bc67 100644 --- a/src/api.Tests/AssetEndpointTests.cs +++ b/src/TrackApi/TrackQrApi.Tests/AssetEndpointTests.cs @@ -1,14 +1,15 @@ using System.Net; using System.Net.Http.Headers; using System.Net.Http.Json; -using api.Features.Auth.Common; -using api.Features.Assets.Common; -using api.Features.Workspaces.Common; using FluentAssertions; +using TrackQrApi.Features.Assets.Common; +using TrackQrApi.Features.Auth.Common; +using TrackQrApi.Features.Workspaces.Common; -namespace Api.Tests; +namespace TrackQrApi.Tests; -public class AssetEndpointTests(ApiWebApplicationFactory factory) +public class AssetEndpointTests( + ApiWebApplicationFactory factory) : IClassFixture { private readonly HttpClient _client = factory.CreateClient(); @@ -17,9 +18,7 @@ public class AssetEndpointTests(ApiWebApplicationFactory factory) { var response = await _client.PostAsJsonAsync("/auth/register", new { Email = email, Password = "password123" }); if (response.StatusCode == HttpStatusCode.Conflict) - { response = await _client.PostAsJsonAsync("/auth/login", new { Email = email, Password = "password123" }); - } var result = await response.Content.ReadFromJsonAsync(); var token = result!.Token; @@ -243,4 +242,4 @@ public class AssetEndpointTests(ApiWebApplicationFactory factory) response.StatusCode.Should().Be(HttpStatusCode.OK); response.Headers.CacheControl!.MaxAge.Should().Be(TimeSpan.FromSeconds(31536000)); } -} +} \ No newline at end of file diff --git a/src/api.Tests/AuthControllerTests.cs b/src/TrackApi/TrackQrApi.Tests/AuthControllerTests.cs similarity index 98% rename from src/api.Tests/AuthControllerTests.cs rename to src/TrackApi/TrackQrApi.Tests/AuthControllerTests.cs index 20d0e97..1017bc3 100644 --- a/src/api.Tests/AuthControllerTests.cs +++ b/src/TrackApi/TrackQrApi.Tests/AuthControllerTests.cs @@ -1,11 +1,12 @@ using System.Net; using System.Net.Http.Json; -using api.Features.Auth.Common; using FluentAssertions; +using TrackQrApi.Features.Auth.Common; -namespace Api.Tests; +namespace TrackQrApi.Tests; -public class AuthControllerTests(ApiWebApplicationFactory factory) +public class AuthControllerTests( + ApiWebApplicationFactory factory) : IClassFixture { private readonly HttpClient _client = factory.CreateClient(); @@ -222,4 +223,4 @@ public class AuthControllerTests(ApiWebApplicationFactory factory) var result = await response.Content.ReadFromJsonAsync(); result!.User.Email.Should().Be("casetest@example.com"); } -} +} \ No newline at end of file diff --git a/src/api.Tests/DomainEndpointTests.cs b/src/TrackApi/TrackQrApi.Tests/DomainEndpointTests.cs similarity index 84% rename from src/api.Tests/DomainEndpointTests.cs rename to src/TrackApi/TrackQrApi.Tests/DomainEndpointTests.cs index 78f74e2..65738bf 100644 --- a/src/api.Tests/DomainEndpointTests.cs +++ b/src/TrackApi/TrackQrApi.Tests/DomainEndpointTests.cs @@ -1,25 +1,25 @@ using System.Net; using System.Net.Http.Headers; using System.Net.Http.Json; -using api.Features.Auth.Common; -using api.Features.Domains.Common; -using api.Features.Workspaces.Common; using FluentAssertions; +using TrackQrApi.Features.Auth.Common; +using TrackQrApi.Features.Domains.Common; +using TrackQrApi.Features.Workspaces.Common; -namespace Api.Tests; +namespace TrackQrApi.Tests; -public class DomainEndpointTests(ApiWebApplicationFactory factory) +public class DomainEndpointTests( + ApiWebApplicationFactory factory) : IClassFixture { private readonly HttpClient _client = factory.CreateClient(); - private async Task<(string Token, Guid WorkspaceId)> GetAuthAndWorkspaceAsync(string email, bool upgradeToPro = true) + private async Task<(string Token, Guid WorkspaceId)> GetAuthAndWorkspaceAsync(string email, + bool upgradeToPro = true) { var response = await _client.PostAsJsonAsync("/auth/register", new { Email = email, Password = "password123" }); if (response.StatusCode == HttpStatusCode.Conflict) - { response = await _client.PostAsJsonAsync("/auth/login", new { Email = email, Password = "password123" }); - } var result = await response.Content.ReadFromJsonAsync(); var token = result!.Token; @@ -29,10 +29,7 @@ public class DomainEndpointTests(ApiWebApplicationFactory factory) var workspaceId = workspaces!.Workspaces.First().Id; // Upgrade to Pro plan for domain tests (Free plan doesn't allow custom domains) - if (upgradeToPro) - { - await factory.UpgradeWorkspaceToPro(workspaceId); - } + if (upgradeToPro) await factory.UpgradeWorkspaceToPro(workspaceId); return (token, workspaceId); } @@ -45,7 +42,8 @@ public class DomainEndpointTests(ApiWebApplicationFactory factory) _client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); // Act - var response = await _client.PostAsJsonAsync($"/workspaces/{workspaceId}/domains", new { Hostname = "example.com" }); + var response = + await _client.PostAsJsonAsync($"/workspaces/{workspaceId}/domains", new { Hostname = "example.com" }); // Assert response.StatusCode.Should().Be(HttpStatusCode.Created); @@ -81,7 +79,8 @@ public class DomainEndpointTests(ApiWebApplicationFactory factory) await _client.PostAsJsonAsync($"/workspaces/{workspaceId}/domains", new { Hostname = "duplicate.com" }); // Act - var response = await _client.PostAsJsonAsync($"/workspaces/{workspaceId}/domains", new { Hostname = "duplicate.com" }); + var response = + await _client.PostAsJsonAsync($"/workspaces/{workspaceId}/domains", new { Hostname = "duplicate.com" }); // Assert response.StatusCode.Should().Be(HttpStatusCode.Conflict); @@ -113,7 +112,8 @@ public class DomainEndpointTests(ApiWebApplicationFactory factory) var (token, workspaceId) = await GetAuthAndWorkspaceAsync("get-domain@example.com"); _client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); - var createResponse = await _client.PostAsJsonAsync($"/workspaces/{workspaceId}/domains", new { Hostname = "get-test.com" }); + var createResponse = + await _client.PostAsJsonAsync($"/workspaces/{workspaceId}/domains", new { Hostname = "get-test.com" }); var created = await createResponse.Content.ReadFromJsonAsync(); // Act @@ -147,7 +147,8 @@ public class DomainEndpointTests(ApiWebApplicationFactory factory) var (token, workspaceId) = await GetAuthAndWorkspaceAsync("delete-domain@example.com"); _client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); - var createResponse = await _client.PostAsJsonAsync($"/workspaces/{workspaceId}/domains", new { Hostname = "to-delete.com" }); + var createResponse = + await _client.PostAsJsonAsync($"/workspaces/{workspaceId}/domains", new { Hostname = "to-delete.com" }); var created = await createResponse.Content.ReadFromJsonAsync(); // Act @@ -168,11 +169,13 @@ public class DomainEndpointTests(ApiWebApplicationFactory factory) var (token, workspaceId) = await GetAuthAndWorkspaceAsync("verify-domain@example.com"); _client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); - var createResponse = await _client.PostAsJsonAsync($"/workspaces/{workspaceId}/domains", new { Hostname = "unverified.com" }); + var createResponse = + await _client.PostAsJsonAsync($"/workspaces/{workspaceId}/domains", new { Hostname = "unverified.com" }); var created = await createResponse.Content.ReadFromJsonAsync(); // Act - var response = await _client.PostAsJsonAsync($"/workspaces/{workspaceId}/domains/{created!.Id}/verify", new { }); + var response = + await _client.PostAsJsonAsync($"/workspaces/{workspaceId}/domains/{created!.Id}/verify", new { }); // Assert response.StatusCode.Should().Be(HttpStatusCode.OK); @@ -189,11 +192,13 @@ public class DomainEndpointTests(ApiWebApplicationFactory factory) _client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); // The verification mock accepts domains starting with "verified-" - var createResponse = await _client.PostAsJsonAsync($"/workspaces/{workspaceId}/domains", new { Hostname = "verified-test.com" }); + var createResponse = await _client.PostAsJsonAsync($"/workspaces/{workspaceId}/domains", + new { Hostname = "verified-test.com" }); var created = await createResponse.Content.ReadFromJsonAsync(); // Act - var response = await _client.PostAsJsonAsync($"/workspaces/{workspaceId}/domains/{created!.Id}/verify", new { }); + var response = + await _client.PostAsJsonAsync($"/workspaces/{workspaceId}/domains/{created!.Id}/verify", new { }); // Assert response.StatusCode.Should().Be(HttpStatusCode.OK); @@ -210,7 +215,8 @@ public class DomainEndpointTests(ApiWebApplicationFactory factory) var (token2, _) = await GetAuthAndWorkspaceAsync("domain-user2@example.com"); _client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token1); - var createResponse = await _client.PostAsJsonAsync($"/workspaces/{workspaceId1}/domains", new { Hostname = "user1-domain.com" }); + var createResponse = await _client.PostAsJsonAsync($"/workspaces/{workspaceId1}/domains", + new { Hostname = "user1-domain.com" }); var created = await createResponse.Content.ReadFromJsonAsync(); // Act - Try to access as user2 @@ -222,4 +228,4 @@ public class DomainEndpointTests(ApiWebApplicationFactory factory) getResponse.StatusCode.Should().Be(HttpStatusCode.NotFound); deleteResponse.StatusCode.Should().Be(HttpStatusCode.NotFound); } -} +} \ No newline at end of file diff --git a/src/api.Tests/EventTrackingTests.cs b/src/TrackApi/TrackQrApi.Tests/EventTrackingTests.cs similarity index 85% rename from src/api.Tests/EventTrackingTests.cs rename to src/TrackApi/TrackQrApi.Tests/EventTrackingTests.cs index c0d4a2c..2e1d9d4 100644 --- a/src/api.Tests/EventTrackingTests.cs +++ b/src/TrackApi/TrackQrApi.Tests/EventTrackingTests.cs @@ -1,35 +1,30 @@ using System.Net; using System.Net.Http.Headers; using System.Net.Http.Json; -using api.Features.Auth.Common; -using api.Features.Links.Common; -using api.Features.Workspaces.Common; using FluentAssertions; using Microsoft.AspNetCore.Mvc.Testing; +using TrackQrApi.Features.Auth.Common; +using TrackQrApi.Features.Links.Common; +using TrackQrApi.Features.Workspaces.Common; -namespace Api.Tests; +namespace TrackQrApi.Tests; -public class EventTrackingTests : IClassFixture +public class EventTrackingTests( + ApiWebApplicationFactory factory) + : IClassFixture { - private readonly HttpClient _client; - private readonly HttpClient _noRedirectClient; + private readonly HttpClient _client = factory.CreateClient(); - public EventTrackingTests(ApiWebApplicationFactory factory) + private readonly HttpClient _noRedirectClient = factory.CreateClient(new WebApplicationFactoryClientOptions { - _client = factory.CreateClient(); - _noRedirectClient = factory.CreateClient(new WebApplicationFactoryClientOptions - { - AllowAutoRedirect = false - }); - } + AllowAutoRedirect = false + }); private async Task<(string Token, Guid WorkspaceId)> SetupAuthAndWorkspaceAsync(string email) { var response = await _client.PostAsJsonAsync("/auth/register", new { Email = email, Password = "password123" }); if (response.StatusCode == HttpStatusCode.Conflict) - { response = await _client.PostAsJsonAsync("/auth/login", new { Email = email, Password = "password123" }); - } var authResult = await response.Content.ReadFromJsonAsync(); var token = authResult!.Token; @@ -82,10 +77,7 @@ public class EventTrackingTests : IClassFixture // Act - Click the same link multiple times rapidly var responses = new List(); - for (int i = 0; i < 5; i++) - { - responses.Add(await _noRedirectClient.GetAsync($"/{link.Slug}")); - } + for (var i = 0; i < 5; i++) responses.Add(await _noRedirectClient.GetAsync($"/{link.Slug}")); // Assert - All should redirect successfully (deduplication happens silently) responses.Should().OnlyContain(r => r.StatusCode == HttpStatusCode.Redirect); @@ -114,7 +106,8 @@ public class EventTrackingTests : IClassFixture var link = await CreateLinkAsync(workspaceId, "https://example.com", "event-ua-link"); // Set a custom user agent - _noRedirectClient.DefaultRequestHeaders.UserAgent.ParseAdd("Mozilla/5.0 (iPhone; CPU iPhone OS 14_0 like Mac OS X)"); + _noRedirectClient.DefaultRequestHeaders.UserAgent.ParseAdd( + "Mozilla/5.0 (iPhone; CPU iPhone OS 14_0 like Mac OS X)"); // Act var response = await _noRedirectClient.GetAsync($"/{link.Slug}"); @@ -139,4 +132,4 @@ public class EventTrackingTests : IClassFixture // Assert response.StatusCode.Should().Be(HttpStatusCode.Redirect); } -} +} \ No newline at end of file diff --git a/src/api.Tests/LinkEndpointTests.cs b/src/TrackApi/TrackQrApi.Tests/LinkEndpointTests.cs similarity index 93% rename from src/api.Tests/LinkEndpointTests.cs rename to src/TrackApi/TrackQrApi.Tests/LinkEndpointTests.cs index 6bd6438..d0e9efa 100644 --- a/src/api.Tests/LinkEndpointTests.cs +++ b/src/TrackApi/TrackQrApi.Tests/LinkEndpointTests.cs @@ -1,15 +1,16 @@ using System.Net; using System.Net.Http.Headers; using System.Net.Http.Json; -using api.Features.Auth.Common; -using api.Features.Links.Common; -using api.Features.Projects.Common; -using api.Features.Workspaces.Common; using FluentAssertions; +using TrackQrApi.Features.Auth.Common; +using TrackQrApi.Features.Links.Common; +using TrackQrApi.Features.Projects.Common; +using TrackQrApi.Features.Workspaces.Common; -namespace Api.Tests; +namespace TrackQrApi.Tests; -public class LinkEndpointTests(ApiWebApplicationFactory factory) +public class LinkEndpointTests( + ApiWebApplicationFactory factory) : IClassFixture { private readonly HttpClient _client = factory.CreateClient(); @@ -18,9 +19,7 @@ public class LinkEndpointTests(ApiWebApplicationFactory factory) { var response = await _client.PostAsJsonAsync("/auth/register", new { Email = email, Password = "password123" }); if (response.StatusCode == HttpStatusCode.Conflict) - { response = await _client.PostAsJsonAsync("/auth/login", new { Email = email, Password = "password123" }); - } var authResult = await response.Content.ReadFromJsonAsync(); var token = authResult!.Token; @@ -127,7 +126,8 @@ public class LinkEndpointTests(ApiWebApplicationFactory factory) var (token, workspaceId) = await SetupAuthAndWorkspaceAsync("create-link-proj@example.com"); _client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); - var projectResponse = await _client.PostAsJsonAsync($"/workspaces/{workspaceId}/projects", new { Name = "Test Project" }); + var projectResponse = + await _client.PostAsJsonAsync($"/workspaces/{workspaceId}/projects", new { Name = "Test Project" }); var project = await projectResponse.Content.ReadFromJsonAsync(); // Act @@ -150,8 +150,10 @@ public class LinkEndpointTests(ApiWebApplicationFactory factory) var (token, workspaceId) = await SetupAuthAndWorkspaceAsync("list-links@example.com"); _client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); - await _client.PostAsJsonAsync($"/workspaces/{workspaceId}/links", new { DestinationUrl = "https://example1.com" }); - await _client.PostAsJsonAsync($"/workspaces/{workspaceId}/links", new { DestinationUrl = "https://example2.com" }); + await _client.PostAsJsonAsync($"/workspaces/{workspaceId}/links", + new { DestinationUrl = "https://example1.com" }); + await _client.PostAsJsonAsync($"/workspaces/{workspaceId}/links", + new { DestinationUrl = "https://example2.com" }); // Act var response = await _client.GetAsync($"/workspaces/{workspaceId}/links"); @@ -169,11 +171,14 @@ public class LinkEndpointTests(ApiWebApplicationFactory factory) var (token, workspaceId) = await SetupAuthAndWorkspaceAsync("list-links-proj@example.com"); _client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); - var projectResponse = await _client.PostAsJsonAsync($"/workspaces/{workspaceId}/projects", new { Name = "Filter Project" }); + var projectResponse = + await _client.PostAsJsonAsync($"/workspaces/{workspaceId}/projects", new { Name = "Filter Project" }); var project = await projectResponse.Content.ReadFromJsonAsync(); - await _client.PostAsJsonAsync($"/workspaces/{workspaceId}/links", new { DestinationUrl = "https://in-project.com", ProjectId = project!.Id }); - await _client.PostAsJsonAsync($"/workspaces/{workspaceId}/links", new { DestinationUrl = "https://no-project.com" }); + await _client.PostAsJsonAsync($"/workspaces/{workspaceId}/links", + new { DestinationUrl = "https://in-project.com", ProjectId = project!.Id }); + await _client.PostAsJsonAsync($"/workspaces/{workspaceId}/links", + new { DestinationUrl = "https://no-project.com" }); // Act var response = await _client.GetAsync($"/workspaces/{workspaceId}/links?projectId={project.Id}"); @@ -372,7 +377,8 @@ public class LinkEndpointTests(ApiWebApplicationFactory factory) // Act var getResponse = await _client.GetAsync($"/workspaces/{workspaceId1}/links/{created!.Id}"); - var updateResponse = await _client.PutAsJsonAsync($"/workspaces/{workspaceId1}/links/{created.Id}", new { Title = "Hacked" }); + var updateResponse = + await _client.PutAsJsonAsync($"/workspaces/{workspaceId1}/links/{created.Id}", new { Title = "Hacked" }); var deleteResponse = await _client.DeleteAsync($"/workspaces/{workspaceId1}/links/{created.Id}"); // Assert - All should return NotFound (not exposing existence) @@ -380,4 +386,4 @@ public class LinkEndpointTests(ApiWebApplicationFactory factory) updateResponse.StatusCode.Should().Be(HttpStatusCode.NotFound); deleteResponse.StatusCode.Should().Be(HttpStatusCode.NotFound); } -} +} \ No newline at end of file diff --git a/src/api.Tests/ProjectEndpointTests.cs b/src/TrackApi/TrackQrApi.Tests/ProjectEndpointTests.cs similarity index 87% rename from src/api.Tests/ProjectEndpointTests.cs rename to src/TrackApi/TrackQrApi.Tests/ProjectEndpointTests.cs index ba38463..d15f1cb 100644 --- a/src/api.Tests/ProjectEndpointTests.cs +++ b/src/TrackApi/TrackQrApi.Tests/ProjectEndpointTests.cs @@ -1,14 +1,15 @@ using System.Net; using System.Net.Http.Headers; using System.Net.Http.Json; -using api.Features.Auth.Common; -using api.Features.Projects.Common; -using api.Features.Workspaces.Common; using FluentAssertions; +using TrackQrApi.Features.Auth.Common; +using TrackQrApi.Features.Projects.Common; +using TrackQrApi.Features.Workspaces.Common; -namespace Api.Tests; +namespace TrackQrApi.Tests; -public class ProjectEndpointTests(ApiWebApplicationFactory factory) +public class ProjectEndpointTests( + ApiWebApplicationFactory factory) : IClassFixture { private readonly HttpClient _client = factory.CreateClient(); @@ -17,9 +18,7 @@ public class ProjectEndpointTests(ApiWebApplicationFactory factory) { var response = await _client.PostAsJsonAsync("/auth/register", new { Email = email, Password = "password123" }); if (response.StatusCode == HttpStatusCode.Conflict) - { response = await _client.PostAsJsonAsync("/auth/login", new { Email = email, Password = "password123" }); - } var authResult = await response.Content.ReadFromJsonAsync(); var token = authResult!.Token; @@ -71,7 +70,8 @@ public class ProjectEndpointTests(ApiWebApplicationFactory factory) _client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); // Act - var response = await _client.PostAsJsonAsync($"/workspaces/{workspaceId}/projects", new { Name = "Test Project" }); + var response = + await _client.PostAsJsonAsync($"/workspaces/{workspaceId}/projects", new { Name = "Test Project" }); // Assert response.StatusCode.Should().Be(HttpStatusCode.Created); @@ -102,7 +102,8 @@ public class ProjectEndpointTests(ApiWebApplicationFactory factory) var (token, workspaceId) = await SetupAuthAndWorkspaceAsync("get-proj@example.com"); _client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); - var createResponse = await _client.PostAsJsonAsync($"/workspaces/{workspaceId}/projects", new { Name = "Get Test" }); + var createResponse = + await _client.PostAsJsonAsync($"/workspaces/{workspaceId}/projects", new { Name = "Get Test" }); var created = await createResponse.Content.ReadFromJsonAsync(); // Act @@ -136,11 +137,13 @@ public class ProjectEndpointTests(ApiWebApplicationFactory factory) var (token, workspaceId) = await SetupAuthAndWorkspaceAsync("update-proj@example.com"); _client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); - var createResponse = await _client.PostAsJsonAsync($"/workspaces/{workspaceId}/projects", new { Name = "Original" }); + var createResponse = + await _client.PostAsJsonAsync($"/workspaces/{workspaceId}/projects", new { Name = "Original" }); var created = await createResponse.Content.ReadFromJsonAsync(); // Act - var response = await _client.PutAsJsonAsync($"/workspaces/{workspaceId}/projects/{created!.Id}", new { Name = "Updated" }); + var response = await _client.PutAsJsonAsync($"/workspaces/{workspaceId}/projects/{created!.Id}", + new { Name = "Updated" }); // Assert response.StatusCode.Should().Be(HttpStatusCode.OK); @@ -155,7 +158,8 @@ public class ProjectEndpointTests(ApiWebApplicationFactory factory) var (token, workspaceId) = await SetupAuthAndWorkspaceAsync("delete-proj@example.com"); _client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); - var createResponse = await _client.PostAsJsonAsync($"/workspaces/{workspaceId}/projects", new { Name = "To Delete" }); + var createResponse = + await _client.PostAsJsonAsync($"/workspaces/{workspaceId}/projects", new { Name = "To Delete" }); var created = await createResponse.Content.ReadFromJsonAsync(); // Act @@ -178,7 +182,8 @@ public class ProjectEndpointTests(ApiWebApplicationFactory factory) // Create project as user1 _client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token1); - var createResponse = await _client.PostAsJsonAsync($"/workspaces/{workspaceId1}/projects", new { Name = "User1 Project" }); + var createResponse = + await _client.PostAsJsonAsync($"/workspaces/{workspaceId1}/projects", new { Name = "User1 Project" }); var created = await createResponse.Content.ReadFromJsonAsync(); // Try to access as user2 @@ -186,7 +191,8 @@ public class ProjectEndpointTests(ApiWebApplicationFactory factory) // Act var getResponse = await _client.GetAsync($"/workspaces/{workspaceId1}/projects/{created!.Id}"); - var updateResponse = await _client.PutAsJsonAsync($"/workspaces/{workspaceId1}/projects/{created.Id}", new { Name = "Hacked" }); + var updateResponse = await _client.PutAsJsonAsync($"/workspaces/{workspaceId1}/projects/{created.Id}", + new { Name = "Hacked" }); var deleteResponse = await _client.DeleteAsync($"/workspaces/{workspaceId1}/projects/{created.Id}"); // Assert - All should return NotFound (not exposing existence) @@ -194,4 +200,4 @@ public class ProjectEndpointTests(ApiWebApplicationFactory factory) updateResponse.StatusCode.Should().Be(HttpStatusCode.NotFound); deleteResponse.StatusCode.Should().Be(HttpStatusCode.NotFound); } -} +} \ No newline at end of file diff --git a/src/api.Tests/QRCodeEndpointTests.cs b/src/TrackApi/TrackQrApi.Tests/QrCodeEndpointTests.cs similarity index 90% rename from src/api.Tests/QRCodeEndpointTests.cs rename to src/TrackApi/TrackQrApi.Tests/QrCodeEndpointTests.cs index 3f366d6..81442c0 100644 --- a/src/api.Tests/QRCodeEndpointTests.cs +++ b/src/TrackApi/TrackQrApi.Tests/QrCodeEndpointTests.cs @@ -1,15 +1,16 @@ using System.Net; using System.Net.Http.Headers; using System.Net.Http.Json; -using api.Features.Auth.Common; -using api.Features.Links.Common; -using api.Features.QRCodes.Common; -using api.Features.Workspaces.Common; using FluentAssertions; +using TrackQrApi.Features.Auth.Common; +using TrackQrApi.Features.Links.Common; +using TrackQrApi.Features.QRCodes.Common; +using TrackQrApi.Features.Workspaces.Common; -namespace Api.Tests; +namespace TrackQrApi.Tests; -public class QRCodeEndpointTests(ApiWebApplicationFactory factory) +public class QrCodeEndpointTests( + ApiWebApplicationFactory factory) : IClassFixture { private readonly HttpClient _client = factory.CreateClient(); @@ -18,9 +19,7 @@ public class QRCodeEndpointTests(ApiWebApplicationFactory factory) { var response = await _client.PostAsJsonAsync("/auth/register", new { Email = email, Password = "password123" }); if (response.StatusCode == HttpStatusCode.Conflict) - { response = await _client.PostAsJsonAsync("/auth/login", new { Email = email, Password = "password123" }); - } var authResult = await response.Content.ReadFromJsonAsync(); var token = authResult!.Token; @@ -139,7 +138,8 @@ public class QRCodeEndpointTests(ApiWebApplicationFactory factory) _client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); var link = await CreateLinkAsync(workspaceId, "qr-get-link"); - var createResponse = await _client.PostAsJsonAsync($"/workspaces/{workspaceId}/qrcodes", new { ShortLinkId = link.Id }); + var createResponse = + await _client.PostAsJsonAsync($"/workspaces/{workspaceId}/qrcodes", new { ShortLinkId = link.Id }); var created = await createResponse.Content.ReadFromJsonAsync(); // Act @@ -173,7 +173,8 @@ public class QRCodeEndpointTests(ApiWebApplicationFactory factory) _client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); var link = await CreateLinkAsync(workspaceId, "qr-update-link"); - var createResponse = await _client.PostAsJsonAsync($"/workspaces/{workspaceId}/qrcodes", new { ShortLinkId = link.Id }); + var createResponse = + await _client.PostAsJsonAsync($"/workspaces/{workspaceId}/qrcodes", new { ShortLinkId = link.Id }); var created = await createResponse.Content.ReadFromJsonAsync(); // Act @@ -201,7 +202,8 @@ public class QRCodeEndpointTests(ApiWebApplicationFactory factory) _client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); var link = await CreateLinkAsync(workspaceId, "qr-delete-link"); - var createResponse = await _client.PostAsJsonAsync($"/workspaces/{workspaceId}/qrcodes", new { ShortLinkId = link.Id }); + var createResponse = + await _client.PostAsJsonAsync($"/workspaces/{workspaceId}/qrcodes", new { ShortLinkId = link.Id }); var created = await createResponse.Content.ReadFromJsonAsync(); // Act @@ -223,7 +225,8 @@ public class QRCodeEndpointTests(ApiWebApplicationFactory factory) _client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); var link = await CreateLinkAsync(workspaceId, "qr-preview-link"); - var createResponse = await _client.PostAsJsonAsync($"/workspaces/{workspaceId}/qrcodes", new { ShortLinkId = link.Id }); + var createResponse = + await _client.PostAsJsonAsync($"/workspaces/{workspaceId}/qrcodes", new { ShortLinkId = link.Id }); var created = await createResponse.Content.ReadFromJsonAsync(); // Act @@ -244,7 +247,8 @@ public class QRCodeEndpointTests(ApiWebApplicationFactory factory) _client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); var link = await CreateLinkAsync(workspaceId, "qr-export-png-link"); - var createResponse = await _client.PostAsJsonAsync($"/workspaces/{workspaceId}/qrcodes", new { ShortLinkId = link.Id }); + var createResponse = + await _client.PostAsJsonAsync($"/workspaces/{workspaceId}/qrcodes", new { ShortLinkId = link.Id }); var created = await createResponse.Content.ReadFromJsonAsync(); // Act @@ -263,7 +267,8 @@ public class QRCodeEndpointTests(ApiWebApplicationFactory factory) _client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); var link = await CreateLinkAsync(workspaceId, "qr-export-svg-link"); - var createResponse = await _client.PostAsJsonAsync($"/workspaces/{workspaceId}/qrcodes", new { ShortLinkId = link.Id }); + var createResponse = + await _client.PostAsJsonAsync($"/workspaces/{workspaceId}/qrcodes", new { ShortLinkId = link.Id }); var created = await createResponse.Content.ReadFromJsonAsync(); // Act @@ -280,7 +285,8 @@ public class QRCodeEndpointTests(ApiWebApplicationFactory factory) // Arrange - Create two users var (token1, workspaceId1) = await SetupAuthAndWorkspaceAsync("qr-user1@example.com"); var link = await CreateLinkAsync(workspaceId1, "qr-user1-link"); - var createResponse = await _client.PostAsJsonAsync($"/workspaces/{workspaceId1}/qrcodes", new { ShortLinkId = link.Id }); + var createResponse = + await _client.PostAsJsonAsync($"/workspaces/{workspaceId1}/qrcodes", new { ShortLinkId = link.Id }); var created = await createResponse.Content.ReadFromJsonAsync(); var (token2, _) = await SetupAuthAndWorkspaceAsync("qr-user2@example.com"); @@ -296,4 +302,4 @@ public class QRCodeEndpointTests(ApiWebApplicationFactory factory) updateResponse.StatusCode.Should().Be(HttpStatusCode.NotFound); deleteResponse.StatusCode.Should().Be(HttpStatusCode.NotFound); } -} +} \ No newline at end of file diff --git a/src/api.Tests/RedirectEndpointTests.cs b/src/TrackApi/TrackQrApi.Tests/RedirectEndpointTests.cs similarity index 88% rename from src/api.Tests/RedirectEndpointTests.cs rename to src/TrackApi/TrackQrApi.Tests/RedirectEndpointTests.cs index 8f5d99c..2c0853b 100644 --- a/src/api.Tests/RedirectEndpointTests.cs +++ b/src/TrackApi/TrackQrApi.Tests/RedirectEndpointTests.cs @@ -1,35 +1,32 @@ using System.Net; using System.Net.Http.Headers; using System.Net.Http.Json; -using api.Features.Auth.Common; -using api.Features.Links.Common; -using api.Features.Workspaces.Common; using FluentAssertions; +using Microsoft.AspNetCore.Mvc.Testing; +using TrackQrApi.Features.Auth.Common; +using TrackQrApi.Features.Links.Common; +using TrackQrApi.Features.Workspaces.Common; -namespace Api.Tests; +namespace TrackQrApi.Tests; -public class RedirectEndpointTests : IClassFixture +public class RedirectEndpointTests( + ApiWebApplicationFactory factory) + : IClassFixture { - private readonly HttpClient _client; - private readonly HttpClient _noRedirectClient; + private readonly HttpClient _client = factory.CreateClient(); - public RedirectEndpointTests(ApiWebApplicationFactory factory) + private readonly HttpClient _noRedirectClient = factory.CreateClient(new WebApplicationFactoryClientOptions { - _client = factory.CreateClient(); - // Create a client that doesn't follow redirects - _noRedirectClient = factory.CreateClient(new Microsoft.AspNetCore.Mvc.Testing.WebApplicationFactoryClientOptions - { - AllowAutoRedirect = false - }); - } + AllowAutoRedirect = false + }); + + // Create a client that doesn't follow redirects private async Task<(string Token, Guid WorkspaceId)> SetupAuthAndWorkspaceAsync(string email) { var response = await _client.PostAsJsonAsync("/auth/register", new { Email = email, Password = "password123" }); if (response.StatusCode == HttpStatusCode.Conflict) - { response = await _client.PostAsJsonAsync("/auth/login", new { Email = email, Password = "password123" }); - } var authResult = await response.Content.ReadFromJsonAsync(); var token = authResult!.Token; @@ -42,7 +39,8 @@ public class RedirectEndpointTests : IClassFixture return (token, workspaceId); } - private async Task CreateLinkAsync(Guid workspaceId, string destinationUrl, string? slug = null, string? password = null) + private async Task CreateLinkAsync(Guid workspaceId, string destinationUrl, string? slug = null, + string? password = null) { var createResponse = await _client.PostAsJsonAsync($"/workspaces/{workspaceId}/links", new { @@ -129,7 +127,7 @@ public class RedirectEndpointTests : IClassFixture { // Arrange var (_, workspaceId) = await SetupAuthAndWorkspaceAsync("redirect-password@example.com"); - var link = await CreateLinkAsync(workspaceId, "https://example.com", "password-link", password: "secret123"); + var link = await CreateLinkAsync(workspaceId, "https://example.com", "password-link", "secret123"); // Act var response = await _client.GetAsync($"/{link.Slug}"); @@ -144,7 +142,7 @@ public class RedirectEndpointTests : IClassFixture { // Arrange var (_, workspaceId) = await SetupAuthAndWorkspaceAsync("redirect-pass-ok@example.com"); - var link = await CreateLinkAsync(workspaceId, "https://example.com", "password-ok-link", password: "secret123"); + var link = await CreateLinkAsync(workspaceId, "https://example.com", "password-ok-link", "secret123"); // Act var response = await _noRedirectClient.PostAsJsonAsync($"/{link.Slug}", new { Password = "secret123" }); @@ -159,7 +157,7 @@ public class RedirectEndpointTests : IClassFixture { // Arrange var (_, workspaceId) = await SetupAuthAndWorkspaceAsync("redirect-pass-wrong@example.com"); - var link = await CreateLinkAsync(workspaceId, "https://example.com", "password-wrong-link", password: "secret123"); + var link = await CreateLinkAsync(workspaceId, "https://example.com", "password-wrong-link", "secret123"); // Act var response = await _client.PostAsJsonAsync($"/{link.Slug}", new { Password = "wrongpassword" }); @@ -173,7 +171,7 @@ public class RedirectEndpointTests : IClassFixture { // Arrange var (_, workspaceId) = await SetupAuthAndWorkspaceAsync("redirect-pass-empty@example.com"); - var link = await CreateLinkAsync(workspaceId, "https://example.com", "password-empty-link", password: "secret123"); + var link = await CreateLinkAsync(workspaceId, "https://example.com", "password-empty-link", "secret123"); // Act var response = await _client.PostAsJsonAsync($"/{link.Slug}", new { Password = "" }); @@ -212,4 +210,4 @@ public class RedirectEndpointTests : IClassFixture // Assert response.StatusCode.Should().Be(HttpStatusCode.Redirect); } -} +} \ No newline at end of file diff --git a/src/TrackApi/TrackQrApi.Tests/TrackQrApi.Tests.csproj b/src/TrackApi/TrackQrApi.Tests/TrackQrApi.Tests.csproj new file mode 100644 index 0000000..d4fda88 --- /dev/null +++ b/src/TrackApi/TrackQrApi.Tests/TrackQrApi.Tests.csproj @@ -0,0 +1,28 @@ + + + + net10.0 + enable + enable + false + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/api.Tests/WorkspaceEndpointTests.cs b/src/TrackApi/TrackQrApi.Tests/WorkspaceEndpointTests.cs similarity index 94% rename from src/api.Tests/WorkspaceEndpointTests.cs rename to src/TrackApi/TrackQrApi.Tests/WorkspaceEndpointTests.cs index feaa4a2..fca8e7d 100644 --- a/src/api.Tests/WorkspaceEndpointTests.cs +++ b/src/TrackApi/TrackQrApi.Tests/WorkspaceEndpointTests.cs @@ -1,24 +1,25 @@ using System.Net; using System.Net.Http.Headers; using System.Net.Http.Json; -using api.Features.Auth.Common; -using api.Features.Workspaces.Common; using FluentAssertions; +using TrackQrApi.Features.Auth.Common; +using TrackQrApi.Features.Workspaces.Common; -namespace Api.Tests; +namespace TrackQrApi.Tests; -public class WorkspaceEndpointTests(ApiWebApplicationFactory factory) +public class WorkspaceEndpointTests( + ApiWebApplicationFactory factory) : IClassFixture { private readonly HttpClient _client = factory.CreateClient(); - private async Task<(string Token, Guid WorkspaceId)> GetAuthAndWorkspaceAsync(string email, bool upgradeToPro = false) + private async Task<(string Token, Guid WorkspaceId)> GetAuthAndWorkspaceAsync(string email, + bool upgradeToPro = false) { var response = await _client.PostAsJsonAsync("/auth/register", new { Email = email, Password = "password123" }); if (response.StatusCode == HttpStatusCode.Conflict) - { response = await _client.PostAsJsonAsync("/auth/login", new { Email = email, Password = "password123" }); - } + var result = await response.Content.ReadFromJsonAsync(); var token = result!.Token; @@ -27,10 +28,7 @@ public class WorkspaceEndpointTests(ApiWebApplicationFactory factory) var workspaces = await workspacesResponse.Content.ReadFromJsonAsync(); var workspaceId = workspaces!.Workspaces.First().Id; - if (upgradeToPro) - { - await factory.UpgradeWorkspaceToPro(workspaceId); - } + if (upgradeToPro) await factory.UpgradeWorkspaceToPro(workspaceId); return (token, workspaceId); } @@ -72,7 +70,7 @@ public class WorkspaceEndpointTests(ApiWebApplicationFactory factory) public async Task CreateWorkspace_WithValidData_ReturnsCreated() { // Arrange - upgrade to Pro to allow creating additional workspaces - var (token, _) = await GetAuthAndWorkspaceAsync("create-ws@example.com", upgradeToPro: true); + var (token, _) = await GetAuthAndWorkspaceAsync("create-ws@example.com", true); _client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); // Act @@ -152,7 +150,7 @@ public class WorkspaceEndpointTests(ApiWebApplicationFactory factory) public async Task DeleteWorkspace_WithValidId_ReturnsSuccess() { // Arrange - upgrade to Pro to allow creating additional workspaces - var (token, _) = await GetAuthAndWorkspaceAsync("delete-ws@example.com", upgradeToPro: true); + var (token, _) = await GetAuthAndWorkspaceAsync("delete-ws@example.com", true); _client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); var createResponse = await _client.PostAsJsonAsync("/workspaces", new { Name = "To Delete" }); @@ -194,4 +192,4 @@ public class WorkspaceEndpointTests(ApiWebApplicationFactory factory) updateResponse.StatusCode.Should().Be(HttpStatusCode.NotFound); deleteResponse.StatusCode.Should().Be(HttpStatusCode.NotFound); } -} +} \ No newline at end of file diff --git a/src/TrackApi/TrackQrApi.slnx b/src/TrackApi/TrackQrApi.slnx new file mode 100644 index 0000000..ea04228 --- /dev/null +++ b/src/TrackApi/TrackQrApi.slnx @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/src/src.sln.DotSettings.user b/src/TrackApi/TrackQrApi.slnx.DotSettings.user similarity index 100% rename from src/src.sln.DotSettings.user rename to src/TrackApi/TrackQrApi.slnx.DotSettings.user diff --git a/src/api/.gitignore b/src/TrackApi/TrackQrApi/.gitignore similarity index 100% rename from src/api/.gitignore rename to src/TrackApi/TrackQrApi/.gitignore diff --git a/src/api/Data/AppDbContext.cs b/src/TrackApi/TrackQrApi/Data/AppDbContext.cs similarity index 99% rename from src/api/Data/AppDbContext.cs rename to src/TrackApi/TrackQrApi/Data/AppDbContext.cs index 8bc0b79..b17d23e 100644 --- a/src/api/Data/AppDbContext.cs +++ b/src/TrackApi/TrackQrApi/Data/AppDbContext.cs @@ -1,9 +1,9 @@ -using api.Models; using Microsoft.EntityFrameworkCore; +using TrackQrApi.Models; -namespace api.Data; +namespace TrackQrApi.Data; -public class AppDbContext(DbContextOptions options) +public class AppDbContext(DbContextOptions options) : DbContext(options) { public DbSet Users => Set(); @@ -225,4 +225,4 @@ public class AppDbContext(DbContextOptions options) .OnDelete(DeleteBehavior.Cascade); }); } -} +} \ No newline at end of file diff --git a/src/api/Features/Analytics/Common/AnalyticsResponses.cs b/src/TrackApi/TrackQrApi/Features/Analytics/Common/AnalyticsResponses.cs similarity index 94% rename from src/api/Features/Analytics/Common/AnalyticsResponses.cs rename to src/TrackApi/TrackQrApi/Features/Analytics/Common/AnalyticsResponses.cs index fff1253..87ba70d 100644 --- a/src/api/Features/Analytics/Common/AnalyticsResponses.cs +++ b/src/TrackApi/TrackQrApi/Features/Analytics/Common/AnalyticsResponses.cs @@ -1,4 +1,4 @@ -namespace api.Features.Analytics.Common; +namespace TrackQrApi.Features.Analytics.Common; public record AnalyticsSummary( int TotalClicks, @@ -37,4 +37,4 @@ public record LinkAnalyticsResponse( IEnumerable DeviceBreakdown, IEnumerable ReferrerBreakdown, IEnumerable CountryBreakdown -); +); \ No newline at end of file diff --git a/src/api/Features/Analytics/Endpoints/LinkAnalyticsEndpoint.cs b/src/TrackApi/TrackQrApi/Features/Analytics/Endpoints/LinkAnalyticsEndpoint.cs similarity index 77% rename from src/api/Features/Analytics/Endpoints/LinkAnalyticsEndpoint.cs rename to src/TrackApi/TrackQrApi/Features/Analytics/Endpoints/LinkAnalyticsEndpoint.cs index 1e2dd3c..1ea364f 100644 --- a/src/api/Features/Analytics/Endpoints/LinkAnalyticsEndpoint.cs +++ b/src/TrackApi/TrackQrApi/Features/Analytics/Endpoints/LinkAnalyticsEndpoint.cs @@ -1,12 +1,12 @@ using System.Security.Claims; -using api.Data; -using api.Features.Analytics.Common; -using api.Features.Auth.Common; -using api.Models; using FastEndpoints; using Microsoft.EntityFrameworkCore; +using TrackQrApi.Data; +using TrackQrApi.Features.Analytics.Common; +using TrackQrApi.Features.Auth.Common; +using TrackQrApi.Models; -namespace api.Features.Analytics.Endpoints; +namespace TrackQrApi.Features.Analytics.Endpoints; public class LinkAnalyticsRequest { @@ -59,26 +59,20 @@ public class LinkAnalyticsEndpoint(AppDbContext db) var eventsQuery = db.Events .Where(e => e.ShortLinkId == req.Id); - if (startDate.HasValue) - { - eventsQuery = eventsQuery.Where(e => e.Timestamp >= startDate.Value); - } + if (startDate.HasValue) eventsQuery = eventsQuery.Where(e => e.Timestamp >= startDate.Value); - if (endDate.HasValue) - { - eventsQuery = eventsQuery.Where(e => e.Timestamp < endDate.Value); - } + if (endDate.HasValue) eventsQuery = eventsQuery.Where(e => e.Timestamp < endDate.Value); var events = await eventsQuery.ToListAsync(ct); var totalEvents = events.Count; // Build summary var summary = new AnalyticsSummary( - TotalClicks: events.Count(e => e.Type == EventType.Click), - TotalScans: events.Count(e => e.Type == EventType.Scan), - UniqueVisitors: events.Select(e => e.IpHash).Distinct().Count(), - FirstEvent: events.MinBy(e => e.Timestamp)?.Timestamp, - LastEvent: events.MaxBy(e => e.Timestamp)?.Timestamp + events.Count(e => e.Type == EventType.Click), + events.Count(e => e.Type == EventType.Scan), + events.Select(e => e.IpHash).Distinct().Count(), + events.MinBy(e => e.Timestamp)?.Timestamp, + events.MaxBy(e => e.Timestamp)?.Timestamp ); // Build time series @@ -86,9 +80,9 @@ public class LinkAnalyticsEndpoint(AppDbContext db) .GroupBy(e => e.Timestamp.Date) .OrderBy(g => g.Key) .Select(g => new TimeSeriesPoint( - Date: g.Key, - Clicks: g.Count(e => e.Type == EventType.Click), - Scans: g.Count(e => e.Type == EventType.Scan) + g.Key, + g.Count(e => e.Type == EventType.Click), + g.Count(e => e.Type == EventType.Scan) )) .ToList(); @@ -131,16 +125,16 @@ public class LinkAnalyticsEndpoint(AppDbContext db) .ToList(); var response = new LinkAnalyticsResponse( - LinkId: link.Id, - Slug: link.Slug, - Summary: summary, - TimeSeries: timeSeries, - DeviceBreakdown: deviceBreakdown, - ReferrerBreakdown: referrerBreakdown, - CountryBreakdown: countryBreakdown + link.Id, + link.Slug, + summary, + timeSeries, + deviceBreakdown, + referrerBreakdown, + countryBreakdown ); - await HttpContext.Response.SendAsync(response, 200, cancellation: ct); + await HttpContext.Response.SendAsync(response, cancellation: ct); } private static DateTime? GetStartDate(string? period) @@ -166,4 +160,4 @@ public class LinkAnalyticsEndpoint(AppDbContext db) return url.Length > 50 ? url[..50] : url; } } -} +} \ No newline at end of file diff --git a/src/api/Features/Analytics/Endpoints/WorkspaceAnalyticsEndpoint.cs b/src/TrackApi/TrackQrApi/Features/Analytics/Endpoints/WorkspaceAnalyticsEndpoint.cs similarity index 79% rename from src/api/Features/Analytics/Endpoints/WorkspaceAnalyticsEndpoint.cs rename to src/TrackApi/TrackQrApi/Features/Analytics/Endpoints/WorkspaceAnalyticsEndpoint.cs index f69cd91..4d025c9 100644 --- a/src/api/Features/Analytics/Endpoints/WorkspaceAnalyticsEndpoint.cs +++ b/src/TrackApi/TrackQrApi/Features/Analytics/Endpoints/WorkspaceAnalyticsEndpoint.cs @@ -1,12 +1,12 @@ using System.Security.Claims; -using api.Data; -using api.Features.Analytics.Common; -using api.Features.Auth.Common; -using api.Models; using FastEndpoints; using Microsoft.EntityFrameworkCore; +using TrackQrApi.Data; +using TrackQrApi.Features.Analytics.Common; +using TrackQrApi.Features.Auth.Common; +using TrackQrApi.Models; -namespace api.Features.Analytics.Endpoints; +namespace TrackQrApi.Features.Analytics.Endpoints; public class WorkspaceAnalyticsRequest { @@ -56,26 +56,20 @@ public class WorkspaceAnalyticsEndpoint(AppDbContext db) var eventsQuery = db.Events .Where(e => e.WorkspaceId == req.WorkspaceId); - if (startDate.HasValue) - { - eventsQuery = eventsQuery.Where(e => e.Timestamp >= startDate.Value); - } + if (startDate.HasValue) eventsQuery = eventsQuery.Where(e => e.Timestamp >= startDate.Value); - if (endDate.HasValue) - { - eventsQuery = eventsQuery.Where(e => e.Timestamp < endDate.Value); - } + if (endDate.HasValue) eventsQuery = eventsQuery.Where(e => e.Timestamp < endDate.Value); var events = await eventsQuery.ToListAsync(ct); var totalEvents = events.Count; // Get summary var summary = new AnalyticsSummary( - TotalClicks: events.Count(e => e.Type == EventType.Click), - TotalScans: events.Count(e => e.Type == EventType.Scan), - UniqueVisitors: events.Select(e => e.IpHash).Distinct().Count(), - FirstEvent: events.Count > 0 ? events.Min(e => e.Timestamp) : null, - LastEvent: events.Count > 0 ? events.Max(e => e.Timestamp) : null + events.Count(e => e.Type == EventType.Click), + events.Count(e => e.Type == EventType.Scan), + events.Select(e => e.IpHash).Distinct().Count(), + events.Count > 0 ? events.Min(e => e.Timestamp) : null, + events.Count > 0 ? events.Max(e => e.Timestamp) : null ); // Get time series @@ -83,9 +77,9 @@ public class WorkspaceAnalyticsEndpoint(AppDbContext db) .GroupBy(e => e.Timestamp.Date) .OrderBy(g => g.Key) .Select(g => new TimeSeriesPoint( - Date: g.Key, - Clicks: g.Count(e => e.Type == EventType.Click), - Scans: g.Count(e => e.Type == EventType.Scan) + g.Key, + g.Count(e => e.Type == EventType.Click), + g.Count(e => e.Type == EventType.Scan) )) .ToList(); @@ -146,15 +140,15 @@ public class WorkspaceAnalyticsEndpoint(AppDbContext db) .ToList(); var response = new WorkspaceAnalyticsResponse( - Summary: summary, - TimeSeries: timeSeries, - TopLinks: topLinks, - DeviceBreakdown: deviceBreakdown, - ReferrerBreakdown: referrerBreakdown, - CountryBreakdown: countryBreakdown + summary, + timeSeries, + topLinks, + deviceBreakdown, + referrerBreakdown, + countryBreakdown ); - await HttpContext.Response.SendAsync(response, 200, cancellation: ct); + await HttpContext.Response.SendAsync(response, cancellation: ct); } private static DateTime? GetStartDate(string? period) @@ -180,4 +174,4 @@ public class WorkspaceAnalyticsEndpoint(AppDbContext db) return url.Length > 50 ? url[..50] : url; } } -} +} \ No newline at end of file diff --git a/src/api/Features/ApiKeys/Endpoints/CreateApiKeyEndpoint.cs b/src/TrackApi/TrackQrApi/Features/ApiKeys/Endpoints/CreateApiKeyEndpoint.cs similarity index 87% rename from src/api/Features/ApiKeys/Endpoints/CreateApiKeyEndpoint.cs rename to src/TrackApi/TrackQrApi/Features/ApiKeys/Endpoints/CreateApiKeyEndpoint.cs index efb8ac7..5cbbf92 100644 --- a/src/api/Features/ApiKeys/Endpoints/CreateApiKeyEndpoint.cs +++ b/src/TrackApi/TrackQrApi/Features/ApiKeys/Endpoints/CreateApiKeyEndpoint.cs @@ -1,12 +1,13 @@ using System.Security.Claims; using System.Security.Cryptography; -using api.Data; -using api.Features.Auth.Common; -using api.Models; +using System.Text; using FastEndpoints; using Microsoft.EntityFrameworkCore; +using TrackQrApi.Data; +using TrackQrApi.Features.Auth.Common; +using TrackQrApi.Models; -namespace api.Features.ApiKeys.Endpoints; +namespace TrackQrApi.Features.ApiKeys.Endpoints; public class CreateApiKeyRequest { @@ -32,7 +33,7 @@ public class CreateApiKeyEndpoint(AppDbContext db) { public override void Configure() { - Post("/workspaces/{WorkspaceId}/api-keys"); + Post("/workspaces/{WorkspaceId}/TrackQrApi-keys"); } public override async Task HandleAsync(CreateApiKeyRequest req, CancellationToken ct) @@ -53,7 +54,8 @@ public class CreateApiKeyEndpoint(AppDbContext db) var existingCount = await db.ApiKeys.CountAsync(k => k.WorkspaceId == req.WorkspaceId && k.IsActive, ct); if (existingCount >= 10) { - await HttpContext.Response.SendAsync(new MessageResponse("Maximum 10 API keys per workspace"), 400, cancellation: ct); + await HttpContext.Response.SendAsync(new MessageResponse("Maximum 10 API keys per workspace"), 400, + cancellation: ct); return; } @@ -73,7 +75,7 @@ public class CreateApiKeyEndpoint(AppDbContext db) Scopes = req.Scopes, ExpiresAt = req.ExpiresAt, CreatedAt = DateTime.UtcNow, - IsActive = true, + IsActive = true }; db.ApiKeys.Add(apiKey); @@ -87,7 +89,7 @@ public class CreateApiKeyEndpoint(AppDbContext db) KeyPrefix = keyPrefix, Scopes = apiKey.Scopes, ExpiresAt = apiKey.ExpiresAt, - CreatedAt = apiKey.CreatedAt, + CreatedAt = apiKey.CreatedAt }; await HttpContext.Response.SendAsync(response, 201, cancellation: ct); @@ -95,8 +97,8 @@ public class CreateApiKeyEndpoint(AppDbContext db) private static string ComputeSha256Hash(string input) { - var bytes = System.Text.Encoding.UTF8.GetBytes(input); + var bytes = Encoding.UTF8.GetBytes(input); var hash = SHA256.HashData(bytes); return Convert.ToHexString(hash).ToLower(); } -} +} \ No newline at end of file diff --git a/src/api/Features/ApiKeys/Endpoints/DeleteApiKeyEndpoint.cs b/src/TrackApi/TrackQrApi/Features/ApiKeys/Endpoints/DeleteApiKeyEndpoint.cs similarity index 86% rename from src/api/Features/ApiKeys/Endpoints/DeleteApiKeyEndpoint.cs rename to src/TrackApi/TrackQrApi/Features/ApiKeys/Endpoints/DeleteApiKeyEndpoint.cs index d5f6347..94d3343 100644 --- a/src/api/Features/ApiKeys/Endpoints/DeleteApiKeyEndpoint.cs +++ b/src/TrackApi/TrackQrApi/Features/ApiKeys/Endpoints/DeleteApiKeyEndpoint.cs @@ -1,10 +1,10 @@ using System.Security.Claims; -using api.Data; -using api.Features.Auth.Common; using FastEndpoints; using Microsoft.EntityFrameworkCore; +using TrackQrApi.Data; +using TrackQrApi.Features.Auth.Common; -namespace api.Features.ApiKeys.Endpoints; +namespace TrackQrApi.Features.ApiKeys.Endpoints; public class DeleteApiKeyRequest { @@ -17,7 +17,7 @@ public class DeleteApiKeyEndpoint(AppDbContext db) { public override void Configure() { - Delete("/workspaces/{WorkspaceId}/api-keys/{Id}"); + Delete("/workspaces/{WorkspaceId}/TrackQrApi-keys/{Id}"); } public override async Task HandleAsync(DeleteApiKeyRequest req, CancellationToken ct) @@ -46,6 +46,6 @@ public class DeleteApiKeyEndpoint(AppDbContext db) db.ApiKeys.Remove(apiKey); await db.SaveChangesAsync(ct); - await HttpContext.Response.SendAsync(new MessageResponse("API key deleted"), 200, cancellation: ct); + await HttpContext.Response.SendAsync(new MessageResponse("API key deleted"), cancellation: ct); } -} +} \ No newline at end of file diff --git a/src/api/Features/ApiKeys/Endpoints/ListApiKeysEndpoint.cs b/src/TrackApi/TrackQrApi/Features/ApiKeys/Endpoints/ListApiKeysEndpoint.cs similarity index 87% rename from src/api/Features/ApiKeys/Endpoints/ListApiKeysEndpoint.cs rename to src/TrackApi/TrackQrApi/Features/ApiKeys/Endpoints/ListApiKeysEndpoint.cs index 17a7e95..ad8b172 100644 --- a/src/api/Features/ApiKeys/Endpoints/ListApiKeysEndpoint.cs +++ b/src/TrackApi/TrackQrApi/Features/ApiKeys/Endpoints/ListApiKeysEndpoint.cs @@ -1,10 +1,10 @@ using System.Security.Claims; -using api.Data; -using api.Features.Auth.Common; using FastEndpoints; using Microsoft.EntityFrameworkCore; +using TrackQrApi.Data; +using TrackQrApi.Features.Auth.Common; -namespace api.Features.ApiKeys.Endpoints; +namespace TrackQrApi.Features.ApiKeys.Endpoints; public class ListApiKeysRequest { @@ -33,7 +33,7 @@ public class ListApiKeysEndpoint(AppDbContext db) { public override void Configure() { - Get("/workspaces/{WorkspaceId}/api-keys"); + Get("/workspaces/{WorkspaceId}/TrackQrApi-keys"); } public override async Task HandleAsync(ListApiKeysRequest req, CancellationToken ct) @@ -62,11 +62,11 @@ public class ListApiKeysEndpoint(AppDbContext db) ExpiresAt = k.ExpiresAt, LastUsedAt = k.LastUsedAt, CreatedAt = k.CreatedAt, - IsActive = k.IsActive, + IsActive = k.IsActive }) .ToListAsync(ct); var response = new ListApiKeysResponse { ApiKeys = apiKeys }; - await HttpContext.Response.SendAsync(response, 200, cancellation: ct); + await HttpContext.Response.SendAsync(response, cancellation: ct); } -} +} \ No newline at end of file diff --git a/src/api/Features/Assets/Common/AssetResponses.cs b/src/TrackApi/TrackQrApi/Features/Assets/Common/AssetResponses.cs similarity index 82% rename from src/api/Features/Assets/Common/AssetResponses.cs rename to src/TrackApi/TrackQrApi/Features/Assets/Common/AssetResponses.cs index e0a839f..6f58c84 100644 --- a/src/api/Features/Assets/Common/AssetResponses.cs +++ b/src/TrackApi/TrackQrApi/Features/Assets/Common/AssetResponses.cs @@ -1,4 +1,4 @@ -namespace api.Features.Assets.Common; +namespace TrackQrApi.Features.Assets.Common; public record AssetResponse( Guid Id, @@ -12,4 +12,4 @@ public record AssetResponse( public record AssetListResponse( IEnumerable Assets -); +); \ No newline at end of file diff --git a/src/api/Features/Assets/Endpoints/DeleteAssetEndpoint.cs b/src/TrackApi/TrackQrApi/Features/Assets/Endpoints/DeleteAssetEndpoint.cs similarity index 81% rename from src/api/Features/Assets/Endpoints/DeleteAssetEndpoint.cs rename to src/TrackApi/TrackQrApi/Features/Assets/Endpoints/DeleteAssetEndpoint.cs index 2259fb1..f6c1789 100644 --- a/src/api/Features/Assets/Endpoints/DeleteAssetEndpoint.cs +++ b/src/TrackApi/TrackQrApi/Features/Assets/Endpoints/DeleteAssetEndpoint.cs @@ -1,11 +1,11 @@ using System.Security.Claims; -using api.Data; -using api.Features.Assets.Services; -using api.Features.Auth.Common; using FastEndpoints; using Microsoft.EntityFrameworkCore; +using TrackQrApi.Data; +using TrackQrApi.Features.Assets.Services; +using TrackQrApi.Features.Auth.Common; -namespace api.Features.Assets.Endpoints; +namespace TrackQrApi.Features.Assets.Endpoints; public class DeleteAssetRequest { @@ -27,7 +27,8 @@ public class DeleteAssetEndpoint(AppDbContext db, IAssetStorageService storage) var asset = await db.Assets .Include(a => a.Workspace) - .FirstOrDefaultAsync(a => a.Id == req.Id && a.WorkspaceId == req.WorkspaceId && a.Workspace.OwnerUserId == userId, ct); + .FirstOrDefaultAsync( + a => a.Id == req.Id && a.WorkspaceId == req.WorkspaceId && a.Workspace.OwnerUserId == userId, ct); if (asset is null) { @@ -55,6 +56,6 @@ public class DeleteAssetEndpoint(AppDbContext db, IAssetStorageService storage) db.Assets.Remove(asset); await db.SaveChangesAsync(ct); - await HttpContext.Response.SendAsync(new MessageResponse("Asset deleted"), 200, cancellation: ct); + await HttpContext.Response.SendAsync(new MessageResponse("Asset deleted"), cancellation: ct); } -} +} \ No newline at end of file diff --git a/src/api/Features/Assets/Endpoints/GetAssetEndpoint.cs b/src/TrackApi/TrackQrApi/Features/Assets/Endpoints/GetAssetEndpoint.cs similarity index 90% rename from src/api/Features/Assets/Endpoints/GetAssetEndpoint.cs rename to src/TrackApi/TrackQrApi/Features/Assets/Endpoints/GetAssetEndpoint.cs index eb513f1..7ff707f 100644 --- a/src/api/Features/Assets/Endpoints/GetAssetEndpoint.cs +++ b/src/TrackApi/TrackQrApi/Features/Assets/Endpoints/GetAssetEndpoint.cs @@ -1,10 +1,10 @@ -using api.Data; -using api.Features.Assets.Services; -using api.Features.Auth.Common; using FastEndpoints; using Microsoft.EntityFrameworkCore; +using TrackQrApi.Data; +using TrackQrApi.Features.Assets.Services; +using TrackQrApi.Features.Auth.Common; -namespace api.Features.Assets.Endpoints; +namespace TrackQrApi.Features.Assets.Endpoints; public class GetAssetRequest { @@ -50,4 +50,4 @@ public class GetAssetEndpoint(AppDbContext db, IAssetStorageService storage) await stream.CopyToAsync(HttpContext.Response.Body, ct); await stream.DisposeAsync(); } -} +} \ No newline at end of file diff --git a/src/api/Features/Assets/Endpoints/ListAssetsEndpoint.cs b/src/TrackApi/TrackQrApi/Features/Assets/Endpoints/ListAssetsEndpoint.cs similarity index 84% rename from src/api/Features/Assets/Endpoints/ListAssetsEndpoint.cs rename to src/TrackApi/TrackQrApi/Features/Assets/Endpoints/ListAssetsEndpoint.cs index 38b4d76..c17f4d2 100644 --- a/src/api/Features/Assets/Endpoints/ListAssetsEndpoint.cs +++ b/src/TrackApi/TrackQrApi/Features/Assets/Endpoints/ListAssetsEndpoint.cs @@ -1,12 +1,12 @@ using System.Security.Claims; -using api.Data; -using api.Features.Assets.Common; -using api.Features.Assets.Services; -using api.Features.Auth.Common; using FastEndpoints; using Microsoft.EntityFrameworkCore; +using TrackQrApi.Data; +using TrackQrApi.Features.Assets.Common; +using TrackQrApi.Features.Assets.Services; +using TrackQrApi.Features.Auth.Common; -namespace api.Features.Assets.Endpoints; +namespace TrackQrApi.Features.Assets.Endpoints; public class ListAssetsRequest { @@ -52,6 +52,6 @@ public class ListAssetsEndpoint(AppDbContext db, IAssetStorageService storage) )) ); - await HttpContext.Response.SendAsync(response, 200, cancellation: ct); + await HttpContext.Response.SendAsync(response, cancellation: ct); } -} +} \ No newline at end of file diff --git a/src/api/Features/Assets/Endpoints/UploadAssetEndpoint.cs b/src/TrackApi/TrackQrApi/Features/Assets/Endpoints/UploadAssetEndpoint.cs similarity index 92% rename from src/api/Features/Assets/Endpoints/UploadAssetEndpoint.cs rename to src/TrackApi/TrackQrApi/Features/Assets/Endpoints/UploadAssetEndpoint.cs index bc3f0cc..3ab1003 100644 --- a/src/api/Features/Assets/Endpoints/UploadAssetEndpoint.cs +++ b/src/TrackApi/TrackQrApi/Features/Assets/Endpoints/UploadAssetEndpoint.cs @@ -1,13 +1,13 @@ using System.Security.Claims; -using api.Data; -using api.Features.Assets.Common; -using api.Features.Assets.Services; -using api.Features.Auth.Common; -using api.Models; using FastEndpoints; using Microsoft.EntityFrameworkCore; +using TrackQrApi.Data; +using TrackQrApi.Features.Assets.Common; +using TrackQrApi.Features.Assets.Services; +using TrackQrApi.Features.Auth.Common; +using TrackQrApi.Models; -namespace api.Features.Assets.Endpoints; +namespace TrackQrApi.Features.Assets.Endpoints; public class UploadAssetRequest { @@ -39,9 +39,8 @@ public class UploadAssetEndpoint(AppDbContext db, IAssetStorageService storage) } // Get file from form - IFormFile? file = req.File; + var file = req.File; if (file is null) - { try { file = HttpContext.Request.Form.Files.FirstOrDefault(); @@ -50,7 +49,6 @@ public class UploadAssetEndpoint(AppDbContext db, IAssetStorageService storage) { // Form access failed - no file uploaded } - } if (file is null || file.Length == 0) { @@ -111,4 +109,4 @@ public class UploadAssetEndpoint(AppDbContext db, IAssetStorageService storage) await HttpContext.Response.SendAsync(response, 201, cancellation: ct); } -} +} \ No newline at end of file diff --git a/src/api/Features/Assets/Services/AssetStorageService.cs b/src/TrackApi/TrackQrApi/Features/Assets/Services/AssetStorageService.cs similarity index 90% rename from src/api/Features/Assets/Services/AssetStorageService.cs rename to src/TrackApi/TrackQrApi/Features/Assets/Services/AssetStorageService.cs index 47446da..e7a6cfc 100644 --- a/src/api/Features/Assets/Services/AssetStorageService.cs +++ b/src/TrackApi/TrackQrApi/Features/Assets/Services/AssetStorageService.cs @@ -1,4 +1,4 @@ -namespace api.Features.Assets.Services; +namespace TrackQrApi.Features.Assets.Services; public interface IAssetStorageService { @@ -19,10 +19,7 @@ public class LocalAssetStorageService : IAssetStorageService _basePath = configuration["Storage:LocalPath"] ?? Path.Combine(Directory.GetCurrentDirectory(), "uploads"); // Ensure directory exists - if (!Directory.Exists(_basePath)) - { - Directory.CreateDirectory(_basePath); - } + if (!Directory.Exists(_basePath)) Directory.CreateDirectory(_basePath); } public async Task StoreAsync(Stream stream, string filename, string contentType) @@ -44,10 +41,7 @@ public class LocalAssetStorageService : IAssetStorageService { var filePath = Path.Combine(_basePath, storageKey); - if (!File.Exists(filePath)) - { - return Task.FromResult<(Stream, string)?>(null); - } + if (!File.Exists(filePath)) return Task.FromResult<(Stream, string)?>(null); var stream = new FileStream(filePath, FileMode.Open, FileAccess.Read); var contentType = GetContentType(storageKey); @@ -87,4 +81,4 @@ public class LocalAssetStorageService : IAssetStorageService _ => "application/octet-stream" }; } -} +} \ No newline at end of file diff --git a/src/api/Features/Auth/Common/AuthResponses.cs b/src/TrackApi/TrackQrApi/Features/Auth/Common/AuthResponses.cs similarity index 65% rename from src/api/Features/Auth/Common/AuthResponses.cs rename to src/TrackApi/TrackQrApi/Features/Auth/Common/AuthResponses.cs index bacf3db..1d28a0c 100644 --- a/src/api/Features/Auth/Common/AuthResponses.cs +++ b/src/TrackApi/TrackQrApi/Features/Auth/Common/AuthResponses.cs @@ -1,4 +1,4 @@ -namespace api.Features.Auth.Common; +namespace TrackQrApi.Features.Auth.Common; public record AuthResponse( string Token, @@ -12,4 +12,4 @@ public record UserInfo( bool IsVerified ); -public record MessageResponse(string Message); +public record MessageResponse(string Message); \ No newline at end of file diff --git a/src/api/Features/Auth/Endpoints/ChangePasswordEndpoint.cs b/src/TrackApi/TrackQrApi/Features/Auth/Endpoints/ChangePasswordEndpoint.cs similarity index 90% rename from src/api/Features/Auth/Endpoints/ChangePasswordEndpoint.cs rename to src/TrackApi/TrackQrApi/Features/Auth/Endpoints/ChangePasswordEndpoint.cs index b3d0525..e396e71 100644 --- a/src/api/Features/Auth/Endpoints/ChangePasswordEndpoint.cs +++ b/src/TrackApi/TrackQrApi/Features/Auth/Endpoints/ChangePasswordEndpoint.cs @@ -1,10 +1,10 @@ using System.Security.Claims; -using api.Data; -using api.Features.Auth.Common; using FastEndpoints; using FluentValidation; +using TrackQrApi.Data; +using TrackQrApi.Features.Auth.Common; -namespace api.Features.Auth.Endpoints; +namespace TrackQrApi.Features.Auth.Endpoints; public class ChangePasswordRequest { @@ -46,7 +46,8 @@ public class ChangePasswordEndpoint(AppDbContext db) : Endpoint t.UserId == user.Id && !t.Used) .ToListAsync(ct); - foreach (var token in existingTokens) - { - token.Used = true; - } + foreach (var token in existingTokens) token.Used = true; // Generate new token var resetToken = Convert.ToBase64String(RandomNumberGenerator.GetBytes(32)) @@ -87,7 +84,6 @@ public class ForgotPasswordEndpoint(AppDbContext db, IEmailService emailService) // Always return success to prevent email enumeration await HttpContext.Response.SendAsync( new MessageResponse("If the email exists, a reset link will be sent"), - 200, cancellation: ct); } -} +} \ No newline at end of file diff --git a/src/api/Features/Auth/Endpoints/GetProfileEndpoint.cs b/src/TrackApi/TrackQrApi/Features/Auth/Endpoints/GetProfileEndpoint.cs similarity index 93% rename from src/api/Features/Auth/Endpoints/GetProfileEndpoint.cs rename to src/TrackApi/TrackQrApi/Features/Auth/Endpoints/GetProfileEndpoint.cs index 680131e..ed53b5d 100644 --- a/src/api/Features/Auth/Endpoints/GetProfileEndpoint.cs +++ b/src/TrackApi/TrackQrApi/Features/Auth/Endpoints/GetProfileEndpoint.cs @@ -1,9 +1,9 @@ using System.Security.Claims; -using api.Data; using FastEndpoints; using Microsoft.EntityFrameworkCore; +using TrackQrApi.Data; -namespace api.Features.Auth.Endpoints; +namespace TrackQrApi.Features.Auth.Endpoints; public record ProfileResponse( Guid Id, @@ -41,4 +41,4 @@ public class GetProfileEndpoint(AppDbContext db) : EndpointWithoutRequest } } -public class LoginEndpoint(AppDbContext db, IOptions jwtSettings) +public class LoginEndpoint(AppDbContext db, IOptions jwtSettings) : Endpoint { private readonly JwtSettings _jwtSettings = jwtSettings.Value; @@ -50,7 +50,8 @@ public class LoginEndpoint(AppDbContext db, IOptions jwtSettings) if (user == null || !BCrypt.Net.BCrypt.Verify(req.Password, user.PasswordHash)) { - await HttpContext.Response.SendAsync(new MessageResponse("Invalid email or password"), 401, cancellation: ct); + await HttpContext.Response.SendAsync(new MessageResponse("Invalid email or password"), 401, + cancellation: ct); return; } @@ -68,19 +69,19 @@ public class LoginEndpoint(AppDbContext db, IOptions jwtSettings) }; var token = new JwtSecurityToken( - issuer: _jwtSettings.Issuer, - audience: _jwtSettings.Audience, - claims: claims, + _jwtSettings.Issuer, + _jwtSettings.Audience, + claims, expires: expiresAt, signingCredentials: credentials ); var response = new AuthResponse( - Token: new JwtSecurityTokenHandler().WriteToken(token), - ExpiresAt: expiresAt, - User: new UserInfo(user.Id, user.Email, user.VerifiedAt.HasValue) + new JwtSecurityTokenHandler().WriteToken(token), + expiresAt, + new UserInfo(user.Id, user.Email, user.VerifiedAt.HasValue) ); - await HttpContext.Response.SendAsync(response, 200, cancellation: ct); + await HttpContext.Response.SendAsync(response, cancellation: ct); } -} +} \ No newline at end of file diff --git a/src/api/Features/Auth/Endpoints/RegisterEndpoint.cs b/src/TrackApi/TrackQrApi/Features/Auth/Endpoints/RegisterEndpoint.cs similarity index 88% rename from src/api/Features/Auth/Endpoints/RegisterEndpoint.cs rename to src/TrackApi/TrackQrApi/Features/Auth/Endpoints/RegisterEndpoint.cs index 2e8c8e1..39c3d71 100644 --- a/src/api/Features/Auth/Endpoints/RegisterEndpoint.cs +++ b/src/TrackApi/TrackQrApi/Features/Auth/Endpoints/RegisterEndpoint.cs @@ -2,18 +2,18 @@ using System.IdentityModel.Tokens.Jwt; using System.Security.Claims; using System.Security.Cryptography; using System.Text; -using api.Data; -using api.Features.Auth.Common; -using api.Features.Auth.Settings; -using api.Features.Email.Services; -using api.Models; using FastEndpoints; using FluentValidation; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Options; using Microsoft.IdentityModel.Tokens; +using TrackQrApi.Data; +using TrackQrApi.Features.Auth.Common; +using TrackQrApi.Features.Auth.Settings; +using TrackQrApi.Features.Email.Services; +using TrackQrApi.Models; -namespace api.Features.Auth.Endpoints; +namespace TrackQrApi.Features.Auth.Endpoints; public class RegisterRequest { @@ -55,7 +55,8 @@ public class RegisterEndpoint(AppDbContext db, IOptions jwtSettings if (await db.Users.AnyAsync(u => u.Email == normalizedEmail, ct)) { - await HttpContext.Response.SendAsync(new MessageResponse("Email already registered"), 409, cancellation: ct); + await HttpContext.Response.SendAsync(new MessageResponse("Email already registered"), 409, + cancellation: ct); return; } @@ -122,17 +123,17 @@ public class RegisterEndpoint(AppDbContext db, IOptions jwtSettings }; var token = new JwtSecurityToken( - issuer: _jwtSettings.Issuer, - audience: _jwtSettings.Audience, - claims: claims, + _jwtSettings.Issuer, + _jwtSettings.Audience, + claims, expires: expiresAt, signingCredentials: credentials ); return new AuthResponse( - Token: new JwtSecurityTokenHandler().WriteToken(token), - ExpiresAt: expiresAt, - User: new UserInfo(user.Id, user.Email, user.VerifiedAt.HasValue) + new JwtSecurityTokenHandler().WriteToken(token), + expiresAt, + new UserInfo(user.Id, user.Email, user.VerifiedAt.HasValue) ); } -} +} \ No newline at end of file diff --git a/src/api/Features/Auth/Endpoints/ResendVerificationEndpoint.cs b/src/TrackApi/TrackQrApi/Features/Auth/Endpoints/ResendVerificationEndpoint.cs similarity index 88% rename from src/api/Features/Auth/Endpoints/ResendVerificationEndpoint.cs rename to src/TrackApi/TrackQrApi/Features/Auth/Endpoints/ResendVerificationEndpoint.cs index f2f2c2e..afcd0c8 100644 --- a/src/api/Features/Auth/Endpoints/ResendVerificationEndpoint.cs +++ b/src/TrackApi/TrackQrApi/Features/Auth/Endpoints/ResendVerificationEndpoint.cs @@ -1,13 +1,13 @@ using System.Security.Claims; using System.Security.Cryptography; -using api.Data; -using api.Features.Auth.Common; -using api.Features.Email.Services; -using api.Models; using FastEndpoints; using Microsoft.EntityFrameworkCore; +using TrackQrApi.Data; +using TrackQrApi.Features.Auth.Common; +using TrackQrApi.Features.Email.Services; +using TrackQrApi.Models; -namespace api.Features.Auth.Endpoints; +namespace TrackQrApi.Features.Auth.Endpoints; public class ResendVerificationEndpoint(AppDbContext db, IEmailService emailService) : EndpointWithoutRequest { @@ -29,7 +29,8 @@ public class ResendVerificationEndpoint(AppDbContext db, IEmailService emailServ if (user.VerifiedAt != null) { - await HttpContext.Response.SendAsync(new MessageResponse("Email is already verified"), 400, cancellation: ct); + await HttpContext.Response.SendAsync(new MessageResponse("Email is already verified"), 400, + cancellation: ct); return; } @@ -60,4 +61,4 @@ public class ResendVerificationEndpoint(AppDbContext db, IEmailService emailServ await HttpContext.Response.SendAsync(new MessageResponse("Verification email sent"), cancellation: ct); } -} +} \ No newline at end of file diff --git a/src/api/Features/Auth/Endpoints/ResetPasswordEndpoint.cs b/src/TrackApi/TrackQrApi/Features/Auth/Endpoints/ResetPasswordEndpoint.cs similarity index 95% rename from src/api/Features/Auth/Endpoints/ResetPasswordEndpoint.cs rename to src/TrackApi/TrackQrApi/Features/Auth/Endpoints/ResetPasswordEndpoint.cs index 2ed19dc..ede1910 100644 --- a/src/api/Features/Auth/Endpoints/ResetPasswordEndpoint.cs +++ b/src/TrackApi/TrackQrApi/Features/Auth/Endpoints/ResetPasswordEndpoint.cs @@ -1,10 +1,10 @@ -using api.Data; -using api.Features.Auth.Common; using FastEndpoints; using FluentValidation; using Microsoft.EntityFrameworkCore; +using TrackQrApi.Data; +using TrackQrApi.Features.Auth.Common; -namespace api.Features.Auth.Endpoints; +namespace TrackQrApi.Features.Auth.Endpoints; public class ResetPasswordRequest { @@ -82,7 +82,6 @@ public class ResetPasswordEndpoint(AppDbContext db) await HttpContext.Response.SendAsync( new MessageResponse("Password has been reset successfully"), - 200, cancellation: ct); } -} +} \ No newline at end of file diff --git a/src/api/Features/Auth/Endpoints/UpdateProfileEndpoint.cs b/src/TrackApi/TrackQrApi/Features/Auth/Endpoints/UpdateProfileEndpoint.cs similarity index 90% rename from src/api/Features/Auth/Endpoints/UpdateProfileEndpoint.cs rename to src/TrackApi/TrackQrApi/Features/Auth/Endpoints/UpdateProfileEndpoint.cs index 04f76dd..08d3ad3 100644 --- a/src/api/Features/Auth/Endpoints/UpdateProfileEndpoint.cs +++ b/src/TrackApi/TrackQrApi/Features/Auth/Endpoints/UpdateProfileEndpoint.cs @@ -1,11 +1,11 @@ using System.Security.Claims; -using api.Data; -using api.Features.Auth.Common; using FastEndpoints; using FluentValidation; using Microsoft.EntityFrameworkCore; +using TrackQrApi.Data; +using TrackQrApi.Features.Auth.Common; -namespace api.Features.Auth.Endpoints; +namespace TrackQrApi.Features.Auth.Endpoints; public class UpdateProfileRequest { @@ -46,7 +46,8 @@ public class UpdateProfileEndpoint(AppDbContext db) : Endpoint u.Email == req.Email && u.Id != userId, ct); if (emailExists) { - await HttpContext.Response.SendAsync(new MessageResponse("Email is already in use"), 409, cancellation: ct); + await HttpContext.Response.SendAsync(new MessageResponse("Email is already in use"), 409, + cancellation: ct); return; } @@ -65,4 +66,4 @@ public class UpdateProfileEndpoint(AppDbContext db) : Endpoint if (token == null) { - await HttpContext.Response.SendAsync(new MessageResponse("Invalid verification token"), 400, cancellation: ct); + await HttpContext.Response.SendAsync(new MessageResponse("Invalid verification token"), 400, + cancellation: ct); return; } @@ -43,7 +44,8 @@ public class VerifyEmailEndpoint(AppDbContext db) : Endpoint { db.EmailVerificationTokens.Remove(token); await db.SaveChangesAsync(ct); - await HttpContext.Response.SendAsync(new MessageResponse("Verification token has expired"), 400, cancellation: ct); + await HttpContext.Response.SendAsync(new MessageResponse("Verification token has expired"), 400, + cancellation: ct); return; } @@ -56,4 +58,4 @@ public class VerifyEmailEndpoint(AppDbContext db) : Endpoint await HttpContext.Response.SendAsync(new MessageResponse("Email verified successfully"), cancellation: ct); } -} +} \ No newline at end of file diff --git a/src/api/Features/Auth/Settings/JwtSettings.cs b/src/TrackApi/TrackQrApi/Features/Auth/Settings/JwtSettings.cs similarity index 83% rename from src/api/Features/Auth/Settings/JwtSettings.cs rename to src/TrackApi/TrackQrApi/Features/Auth/Settings/JwtSettings.cs index 7521139..087cad0 100644 --- a/src/api/Features/Auth/Settings/JwtSettings.cs +++ b/src/TrackApi/TrackQrApi/Features/Auth/Settings/JwtSettings.cs @@ -1,4 +1,4 @@ -namespace api.Features.Auth.Settings; +namespace TrackQrApi.Features.Auth.Settings; public class JwtSettings { @@ -6,4 +6,4 @@ public class JwtSettings public required string Issuer { get; set; } public required string Audience { get; set; } public int ExpirationMinutes { get; set; } = 60; -} +} \ No newline at end of file diff --git a/src/api/Features/Billing/Common/BillingModels.cs b/src/TrackApi/TrackQrApi/Features/Billing/Common/BillingModels.cs similarity index 90% rename from src/api/Features/Billing/Common/BillingModels.cs rename to src/TrackApi/TrackQrApi/Features/Billing/Common/BillingModels.cs index 307ce0a..f604717 100644 --- a/src/api/Features/Billing/Common/BillingModels.cs +++ b/src/TrackApi/TrackQrApi/Features/Billing/Common/BillingModels.cs @@ -1,4 +1,4 @@ -namespace api.Features.Billing.Common; +namespace TrackQrApi.Features.Billing.Common; public record CheckoutSessionRequest( Guid WorkspaceId, @@ -20,4 +20,4 @@ public record SubscriptionResponse( DateTime? CurrentPeriodEnd, bool IsActive, bool CancelAtPeriodEnd -); +); \ No newline at end of file diff --git a/src/api/Features/Billing/Endpoints/CreateCheckoutSessionEndpoint.cs b/src/TrackApi/TrackQrApi/Features/Billing/Endpoints/CreateCheckoutSessionEndpoint.cs similarity index 87% rename from src/api/Features/Billing/Endpoints/CreateCheckoutSessionEndpoint.cs rename to src/TrackApi/TrackQrApi/Features/Billing/Endpoints/CreateCheckoutSessionEndpoint.cs index 85987d4..c82b535 100644 --- a/src/api/Features/Billing/Endpoints/CreateCheckoutSessionEndpoint.cs +++ b/src/TrackApi/TrackQrApi/Features/Billing/Endpoints/CreateCheckoutSessionEndpoint.cs @@ -1,14 +1,14 @@ using System.Security.Claims; -using api.Data; -using api.Features.Auth.Common; -using api.Features.Billing.Common; -using api.Features.Billing.Services; -using api.Models; using FastEndpoints; using FluentValidation; using Microsoft.EntityFrameworkCore; +using TrackQrApi.Data; +using TrackQrApi.Features.Auth.Common; +using TrackQrApi.Features.Billing.Common; +using TrackQrApi.Features.Billing.Services; +using TrackQrApi.Models; -namespace api.Features.Billing.Endpoints; +namespace TrackQrApi.Features.Billing.Endpoints; public class CreateCheckoutSessionValidator : Validator { @@ -56,7 +56,8 @@ public class CreateCheckoutSessionEndpoint(AppDbContext db, IStripeService strip if (!string.IsNullOrEmpty(workspace.StripeSubscriptionId)) { await HttpContext.Response.SendAsync( - new MessageResponse("Workspace already has an active subscription. Use the billing portal to manage it."), + new MessageResponse( + "Workspace already has an active subscription. Use the billing portal to manage it."), 400, cancellation: ct); return; @@ -84,4 +85,4 @@ public class CreateCheckoutSessionEndpoint(AppDbContext db, IStripeService strip cancellation: ct); } } -} +} \ No newline at end of file diff --git a/src/api/Features/Billing/Endpoints/CreatePortalSessionEndpoint.cs b/src/TrackApi/TrackQrApi/Features/Billing/Endpoints/CreatePortalSessionEndpoint.cs similarity index 90% rename from src/api/Features/Billing/Endpoints/CreatePortalSessionEndpoint.cs rename to src/TrackApi/TrackQrApi/Features/Billing/Endpoints/CreatePortalSessionEndpoint.cs index 7d5ba8f..14e71c5 100644 --- a/src/api/Features/Billing/Endpoints/CreatePortalSessionEndpoint.cs +++ b/src/TrackApi/TrackQrApi/Features/Billing/Endpoints/CreatePortalSessionEndpoint.cs @@ -1,11 +1,11 @@ using System.Security.Claims; -using api.Features.Auth.Common; -using api.Features.Billing.Common; -using api.Features.Billing.Services; using FastEndpoints; using FluentValidation; +using TrackQrApi.Features.Auth.Common; +using TrackQrApi.Features.Billing.Common; +using TrackQrApi.Features.Billing.Services; -namespace api.Features.Billing.Endpoints; +namespace TrackQrApi.Features.Billing.Endpoints; public class CreatePortalSessionValidator : Validator { @@ -57,4 +57,4 @@ public class CreatePortalSessionEndpoint(IStripeService stripeService) cancellation: ct); } } -} +} \ No newline at end of file diff --git a/src/api/Features/Billing/Endpoints/GetSubscriptionEndpoint.cs b/src/TrackApi/TrackQrApi/Features/Billing/Endpoints/GetSubscriptionEndpoint.cs similarity index 86% rename from src/api/Features/Billing/Endpoints/GetSubscriptionEndpoint.cs rename to src/TrackApi/TrackQrApi/Features/Billing/Endpoints/GetSubscriptionEndpoint.cs index b9a7bc0..925e38a 100644 --- a/src/api/Features/Billing/Endpoints/GetSubscriptionEndpoint.cs +++ b/src/TrackApi/TrackQrApi/Features/Billing/Endpoints/GetSubscriptionEndpoint.cs @@ -1,12 +1,13 @@ using System.Security.Claims; -using api.Data; -using api.Features.Auth.Common; -using api.Features.Billing.Common; -using api.Features.Billing.Services; using FastEndpoints; using Microsoft.EntityFrameworkCore; +using TrackQrApi.Data; +using TrackQrApi.Features.Auth.Common; +using TrackQrApi.Features.Billing.Common; +using TrackQrApi.Features.Billing.Services; +using TrackQrApi.Models; -namespace api.Features.Billing.Endpoints; +namespace TrackQrApi.Features.Billing.Endpoints; public class GetSubscriptionRequest { @@ -34,7 +35,7 @@ public class GetSubscriptionEndpoint(AppDbContext db, IStripeService stripeServi return; } - var isActive = workspace.Plan != Models.WorkspacePlan.Free; + var isActive = workspace.Plan != WorkspacePlan.Free; var cancelAtPeriodEnd = false; // Get live subscription status from Stripe if exists @@ -59,4 +60,4 @@ public class GetSubscriptionEndpoint(AppDbContext db, IStripeService stripeServi await HttpContext.Response.SendAsync(response, cancellation: ct); } -} +} \ No newline at end of file diff --git a/src/api/Features/Billing/Endpoints/StripeWebhookEndpoint.cs b/src/TrackApi/TrackQrApi/Features/Billing/Endpoints/StripeWebhookEndpoint.cs similarity index 83% rename from src/api/Features/Billing/Endpoints/StripeWebhookEndpoint.cs rename to src/TrackApi/TrackQrApi/Features/Billing/Endpoints/StripeWebhookEndpoint.cs index e889f0a..fdcab74 100644 --- a/src/api/Features/Billing/Endpoints/StripeWebhookEndpoint.cs +++ b/src/TrackApi/TrackQrApi/Features/Billing/Endpoints/StripeWebhookEndpoint.cs @@ -1,10 +1,11 @@ -using api.Features.Billing.Services; -using api.Features.Billing.Settings; using FastEndpoints; using Microsoft.Extensions.Options; using Stripe; +using Stripe.Checkout; +using TrackQrApi.Features.Billing.Services; +using TrackQrApi.Features.Billing.Settings; -namespace api.Features.Billing.Endpoints; +namespace TrackQrApi.Features.Billing.Endpoints; public class StripeWebhookEndpoint( IStripeService stripeService, @@ -38,27 +39,20 @@ public class StripeWebhookEndpoint( switch (stripeEvent.Type) { case "checkout.session.completed": - var session = stripeEvent.Data.Object as Stripe.Checkout.Session; - if (session != null) - { - await stripeService.HandleCheckoutCompletedAsync(session, ct); - } + var session = stripeEvent.Data.Object as Session; + if (session != null) await stripeService.HandleCheckoutCompletedAsync(session, ct); break; case "customer.subscription.updated": var updatedSubscription = stripeEvent.Data.Object as Subscription; if (updatedSubscription != null) - { await stripeService.HandleSubscriptionUpdatedAsync(updatedSubscription, ct); - } break; case "customer.subscription.deleted": var deletedSubscription = stripeEvent.Data.Object as Subscription; if (deletedSubscription != null) - { await stripeService.HandleSubscriptionDeletedAsync(deletedSubscription, ct); - } break; case "invoice.payment_failed": @@ -76,7 +70,8 @@ public class StripeWebhookEndpoint( catch (StripeException ex) { logger.LogError(ex, "Stripe webhook signature verification failed"); - await HttpContext.Response.SendAsync(new { error = "Webhook signature verification failed" }, 400, cancellation: ct); + await HttpContext.Response.SendAsync(new { error = "Webhook signature verification failed" }, 400, + cancellation: ct); } catch (Exception ex) { @@ -84,4 +79,4 @@ public class StripeWebhookEndpoint( await HttpContext.Response.SendAsync(new { error = "Webhook processing failed" }, 500, cancellation: ct); } } -} +} \ No newline at end of file diff --git a/src/api/Features/Billing/Services/StripeService.cs b/src/TrackApi/TrackQrApi/Features/Billing/Services/StripeService.cs similarity index 93% rename from src/api/Features/Billing/Services/StripeService.cs rename to src/TrackApi/TrackQrApi/Features/Billing/Services/StripeService.cs index a7c5915..47bc8f3 100644 --- a/src/api/Features/Billing/Services/StripeService.cs +++ b/src/TrackApi/TrackQrApi/Features/Billing/Services/StripeService.cs @@ -1,16 +1,18 @@ -using api.Data; -using api.Features.Billing.Settings; -using api.Models; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Options; using Stripe; using Stripe.Checkout; +using TrackQrApi.Data; +using TrackQrApi.Features.Billing.Settings; +using TrackQrApi.Models; -namespace api.Features.Billing.Services; +namespace TrackQrApi.Features.Billing.Services; public interface IStripeService { - Task CreateCheckoutSessionAsync(Guid userId, Guid workspaceId, WorkspacePlan plan, string successUrl, string cancelUrl, CancellationToken ct = default); + Task CreateCheckoutSessionAsync(Guid userId, Guid workspaceId, WorkspacePlan plan, string successUrl, + string cancelUrl, CancellationToken ct = default); + Task CreateCustomerPortalSessionAsync(Guid userId, string returnUrl, CancellationToken ct = default); Task GetSubscriptionAsync(string subscriptionId, CancellationToken ct = default); Task CancelSubscriptionAsync(string subscriptionId, CancellationToken ct = default); @@ -23,9 +25,9 @@ public interface IStripeService public class StripeService : IStripeService { + private readonly ILogger _logger; private readonly IServiceScopeFactory _scopeFactory; private readonly StripeSettings _settings; - private readonly ILogger _logger; public StripeService( IServiceScopeFactory scopeFactory, @@ -51,7 +53,7 @@ public class StripeService : IStripeService var db = scope.ServiceProvider.GetRequiredService(); var user = await db.Users.FindAsync([userId], ct) - ?? throw new InvalidOperationException("User not found"); + ?? throw new InvalidOperationException("User not found"); // Get or create Stripe customer var customerId = user.StripeCustomerId; @@ -73,10 +75,7 @@ public class StripeService : IStripeService } var priceId = GetPriceIdForPlan(plan); - if (string.IsNullOrEmpty(priceId)) - { - throw new InvalidOperationException($"No price configured for plan: {plan}"); - } + if (string.IsNullOrEmpty(priceId)) throw new InvalidOperationException($"No price configured for plan: {plan}"); var sessionService = new SessionService(); var session = await sessionService.CreateAsync(new SessionCreateOptions @@ -122,12 +121,10 @@ public class StripeService : IStripeService var db = scope.ServiceProvider.GetRequiredService(); var user = await db.Users.FindAsync([userId], ct) - ?? throw new InvalidOperationException("User not found"); + ?? throw new InvalidOperationException("User not found"); if (string.IsNullOrEmpty(user.StripeCustomerId)) - { throw new InvalidOperationException("User has no Stripe customer"); - } var sessionService = new Stripe.BillingPortal.SessionService(); var session = await sessionService.CreateAsync(new Stripe.BillingPortal.SessionCreateOptions @@ -202,10 +199,7 @@ public class StripeService : IStripeService if (!string.IsNullOrEmpty(session.SubscriptionId)) { var subscription = await GetSubscriptionAsync(session.SubscriptionId, ct); - if (subscription != null) - { - workspace.SubscriptionEndsAt = subscription.CurrentPeriodEnd; - } + if (subscription != null) workspace.SubscriptionEndsAt = subscription.CurrentPeriodEnd; } await db.SaveChangesAsync(ct); @@ -247,11 +241,9 @@ public class StripeService : IStripeService // Handle cancellation at period end if (subscription.CancelAtPeriodEnd) - { _logger.LogInformation( "Workspace {WorkspaceId} subscription will cancel at {EndDate}", workspace.Id, subscription.CurrentPeriodEnd); - } await db.SaveChangesAsync(ct); } @@ -299,4 +291,4 @@ public class StripeService : IStripeService return WorkspacePlan.Business; return WorkspacePlan.Free; } -} +} \ No newline at end of file diff --git a/src/api/Features/Billing/Settings/StripeSettings.cs b/src/TrackApi/TrackQrApi/Features/Billing/Settings/StripeSettings.cs similarity index 84% rename from src/api/Features/Billing/Settings/StripeSettings.cs rename to src/TrackApi/TrackQrApi/Features/Billing/Settings/StripeSettings.cs index bcf2658..3ab3f19 100644 --- a/src/api/Features/Billing/Settings/StripeSettings.cs +++ b/src/TrackApi/TrackQrApi/Features/Billing/Settings/StripeSettings.cs @@ -1,4 +1,4 @@ -namespace api.Features.Billing.Settings; +namespace TrackQrApi.Features.Billing.Settings; public class StripeSettings { @@ -6,4 +6,4 @@ public class StripeSettings public string WebhookSecret { get; set; } = string.Empty; public string ProPriceId { get; set; } = string.Empty; public string BusinessPriceId { get; set; } = string.Empty; -} +} \ No newline at end of file diff --git a/src/api/Features/Domains/Common/DomainResponses.cs b/src/TrackApi/TrackQrApi/Features/Domains/Common/DomainResponses.cs similarity index 89% rename from src/api/Features/Domains/Common/DomainResponses.cs rename to src/TrackApi/TrackQrApi/Features/Domains/Common/DomainResponses.cs index bcc6771..604732a 100644 --- a/src/api/Features/Domains/Common/DomainResponses.cs +++ b/src/TrackApi/TrackQrApi/Features/Domains/Common/DomainResponses.cs @@ -1,4 +1,4 @@ -namespace api.Features.Domains.Common; +namespace TrackQrApi.Features.Domains.Common; public record DomainResponse( Guid Id, @@ -20,4 +20,4 @@ public record DomainVerificationResponse( bool IsVerified, string Status, string? Message -); +); \ No newline at end of file diff --git a/src/api/Features/Domains/Endpoints/AddDomainEndpoint.cs b/src/TrackApi/TrackQrApi/Features/Domains/Endpoints/AddDomainEndpoint.cs similarity index 92% rename from src/api/Features/Domains/Endpoints/AddDomainEndpoint.cs rename to src/TrackApi/TrackQrApi/Features/Domains/Endpoints/AddDomainEndpoint.cs index 6caacfe..573e971 100644 --- a/src/api/Features/Domains/Endpoints/AddDomainEndpoint.cs +++ b/src/TrackApi/TrackQrApi/Features/Domains/Endpoints/AddDomainEndpoint.cs @@ -1,15 +1,15 @@ using System.Security.Claims; using System.Security.Cryptography; -using api.Data; -using api.Features.Auth.Common; -using api.Features.Domains.Common; -using api.Features.Plans.Services; -using api.Models; using FastEndpoints; using FluentValidation; using Microsoft.EntityFrameworkCore; +using TrackQrApi.Data; +using TrackQrApi.Features.Auth.Common; +using TrackQrApi.Features.Domains.Common; +using TrackQrApi.Features.Plans.Services; +using TrackQrApi.Models; -namespace api.Features.Domains.Endpoints; +namespace TrackQrApi.Features.Domains.Endpoints; public class AddDomainRequest { @@ -70,7 +70,8 @@ public class AddDomainEndpoint(AppDbContext db, IPlanLimitsService planLimits) if (domainExists) { - await HttpContext.Response.SendAsync(new MessageResponse("Domain is already registered"), 409, cancellation: ct); + await HttpContext.Response.SendAsync(new MessageResponse("Domain is already registered"), 409, + cancellation: ct); return; } @@ -113,4 +114,4 @@ public class AddDomainEndpoint(AppDbContext db, IPlanLimitsService planLimits) { return $"TXT _trakqr-verification {token}"; } -} +} \ No newline at end of file diff --git a/src/api/Features/Domains/Endpoints/DeleteDomainEndpoint.cs b/src/TrackApi/TrackQrApi/Features/Domains/Endpoints/DeleteDomainEndpoint.cs similarity index 82% rename from src/api/Features/Domains/Endpoints/DeleteDomainEndpoint.cs rename to src/TrackApi/TrackQrApi/Features/Domains/Endpoints/DeleteDomainEndpoint.cs index 0a263b6..946dc32 100644 --- a/src/api/Features/Domains/Endpoints/DeleteDomainEndpoint.cs +++ b/src/TrackApi/TrackQrApi/Features/Domains/Endpoints/DeleteDomainEndpoint.cs @@ -1,10 +1,10 @@ using System.Security.Claims; -using api.Data; -using api.Features.Auth.Common; using FastEndpoints; using Microsoft.EntityFrameworkCore; +using TrackQrApi.Data; +using TrackQrApi.Features.Auth.Common; -namespace api.Features.Domains.Endpoints; +namespace TrackQrApi.Features.Domains.Endpoints; public class DeleteDomainRequest { @@ -26,7 +26,8 @@ public class DeleteDomainEndpoint(AppDbContext db) var domain = await db.Domains .Include(d => d.Workspace) - .FirstOrDefaultAsync(d => d.Id == req.Id && d.WorkspaceId == req.WorkspaceId && d.Workspace.OwnerUserId == userId, ct); + .FirstOrDefaultAsync( + d => d.Id == req.Id && d.WorkspaceId == req.WorkspaceId && d.Workspace.OwnerUserId == userId, ct); if (domain is null) { @@ -50,6 +51,6 @@ public class DeleteDomainEndpoint(AppDbContext db) db.Domains.Remove(domain); await db.SaveChangesAsync(ct); - await HttpContext.Response.SendAsync(new MessageResponse("Domain deleted"), 200, cancellation: ct); + await HttpContext.Response.SendAsync(new MessageResponse("Domain deleted"), cancellation: ct); } -} +} \ No newline at end of file diff --git a/src/api/Features/Domains/Endpoints/GetDomainEndpoint.cs b/src/TrackApi/TrackQrApi/Features/Domains/Endpoints/GetDomainEndpoint.cs similarity index 84% rename from src/api/Features/Domains/Endpoints/GetDomainEndpoint.cs rename to src/TrackApi/TrackQrApi/Features/Domains/Endpoints/GetDomainEndpoint.cs index cc41c8c..7d04376 100644 --- a/src/api/Features/Domains/Endpoints/GetDomainEndpoint.cs +++ b/src/TrackApi/TrackQrApi/Features/Domains/Endpoints/GetDomainEndpoint.cs @@ -1,11 +1,11 @@ using System.Security.Claims; -using api.Data; -using api.Features.Auth.Common; -using api.Features.Domains.Common; using FastEndpoints; using Microsoft.EntityFrameworkCore; +using TrackQrApi.Data; +using TrackQrApi.Features.Auth.Common; +using TrackQrApi.Features.Domains.Common; -namespace api.Features.Domains.Endpoints; +namespace TrackQrApi.Features.Domains.Endpoints; public class GetDomainRequest { @@ -45,6 +45,6 @@ public class GetDomainEndpoint(AppDbContext db) domain.CreatedAt ); - await HttpContext.Response.SendAsync(response, 200, cancellation: ct); + await HttpContext.Response.SendAsync(response, cancellation: ct); } -} +} \ No newline at end of file diff --git a/src/api/Features/Domains/Endpoints/ListDomainsEndpoint.cs b/src/TrackApi/TrackQrApi/Features/Domains/Endpoints/ListDomainsEndpoint.cs similarity index 88% rename from src/api/Features/Domains/Endpoints/ListDomainsEndpoint.cs rename to src/TrackApi/TrackQrApi/Features/Domains/Endpoints/ListDomainsEndpoint.cs index dc3b6df..bc0c35d 100644 --- a/src/api/Features/Domains/Endpoints/ListDomainsEndpoint.cs +++ b/src/TrackApi/TrackQrApi/Features/Domains/Endpoints/ListDomainsEndpoint.cs @@ -1,11 +1,11 @@ using System.Security.Claims; -using api.Data; -using api.Features.Auth.Common; -using api.Features.Domains.Common; using FastEndpoints; using Microsoft.EntityFrameworkCore; +using TrackQrApi.Data; +using TrackQrApi.Features.Auth.Common; +using TrackQrApi.Features.Domains.Common; -namespace api.Features.Domains.Endpoints; +namespace TrackQrApi.Features.Domains.Endpoints; public class ListDomainsRequest { @@ -48,6 +48,6 @@ public class ListDomainsEndpoint(AppDbContext db) )) .ToListAsync(ct); - await HttpContext.Response.SendAsync(new DomainListResponse(domains), 200, cancellation: ct); + await HttpContext.Response.SendAsync(new DomainListResponse(domains), cancellation: ct); } -} +} \ No newline at end of file diff --git a/src/api/Features/Domains/Endpoints/VerifyDomainEndpoint.cs b/src/TrackApi/TrackQrApi/Features/Domains/Endpoints/VerifyDomainEndpoint.cs similarity index 87% rename from src/api/Features/Domains/Endpoints/VerifyDomainEndpoint.cs rename to src/TrackApi/TrackQrApi/Features/Domains/Endpoints/VerifyDomainEndpoint.cs index 696ca3f..fd79189 100644 --- a/src/api/Features/Domains/Endpoints/VerifyDomainEndpoint.cs +++ b/src/TrackApi/TrackQrApi/Features/Domains/Endpoints/VerifyDomainEndpoint.cs @@ -1,12 +1,12 @@ using System.Security.Claims; -using api.Data; -using api.Features.Auth.Common; -using api.Features.Domains.Common; -using api.Models; using FastEndpoints; using Microsoft.EntityFrameworkCore; +using TrackQrApi.Data; +using TrackQrApi.Features.Auth.Common; +using TrackQrApi.Features.Domains.Common; +using TrackQrApi.Models; -namespace api.Features.Domains.Endpoints; +namespace TrackQrApi.Features.Domains.Endpoints; public class VerifyDomainRequest { @@ -28,7 +28,8 @@ public class VerifyDomainEndpoint(AppDbContext db) var domain = await db.Domains .Include(d => d.Workspace) - .FirstOrDefaultAsync(d => d.Id == req.Id && d.WorkspaceId == req.WorkspaceId && d.Workspace.OwnerUserId == userId, ct); + .FirstOrDefaultAsync( + d => d.Id == req.Id && d.WorkspaceId == req.WorkspaceId && d.Workspace.OwnerUserId == userId, ct); if (domain is null) { @@ -46,7 +47,7 @@ public class VerifyDomainEndpoint(AppDbContext db) domain.Status.ToString(), "Domain is already verified" ); - await HttpContext.Response.SendAsync(alreadyResponse, 200, cancellation: ct); + await HttpContext.Response.SendAsync(alreadyResponse, cancellation: ct); return; } @@ -65,7 +66,7 @@ public class VerifyDomainEndpoint(AppDbContext db) domain.Status.ToString(), "Domain verified successfully" ); - await HttpContext.Response.SendAsync(successResponse, 200, cancellation: ct); + await HttpContext.Response.SendAsync(successResponse, cancellation: ct); } else { @@ -76,7 +77,7 @@ public class VerifyDomainEndpoint(AppDbContext db) domain.Status.ToString(), $"Verification failed. Please add a TXT record for _trakqr-verification.{domain.Hostname} with value: {domain.VerificationToken}" ); - await HttpContext.Response.SendAsync(failedResponse, 200, cancellation: ct); + await HttpContext.Response.SendAsync(failedResponse, cancellation: ct); } } @@ -88,4 +89,4 @@ public class VerifyDomainEndpoint(AppDbContext db) var isVerified = hostname.StartsWith("verified-"); return Task.FromResult(isVerified); } -} +} \ No newline at end of file diff --git a/src/api/Features/Email/Services/ConsoleEmailService.cs b/src/TrackApi/TrackQrApi/Features/Email/Services/ConsoleEmailService.cs similarity index 57% rename from src/api/Features/Email/Services/ConsoleEmailService.cs rename to src/TrackApi/TrackQrApi/Features/Email/Services/ConsoleEmailService.cs index 6770402..e834653 100644 --- a/src/api/Features/Email/Services/ConsoleEmailService.cs +++ b/src/TrackApi/TrackQrApi/Features/Email/Services/ConsoleEmailService.cs @@ -1,22 +1,18 @@ -using api.Features.Email.Templates; using Microsoft.Extensions.Options; +using TrackQrApi.Features.Email.Templates; -namespace api.Features.Email.Services; +namespace TrackQrApi.Features.Email.Services; /// -/// Development email service that logs emails to console instead of sending them. -/// Useful for testing without a real SMTP server. +/// Development email service that logs emails to console instead of sending them. +/// Useful for testing without a real SMTP server. /// -public class ConsoleEmailService : IEmailService +public class ConsoleEmailService( + IOptions settings, + ILogger logger) + : IEmailService { - private readonly EmailSettings _settings; - private readonly ILogger _logger; - - public ConsoleEmailService(IOptions settings, ILogger logger) - { - _settings = settings.Value; - _logger = logger; - } + private readonly EmailSettings _settings = settings.Value; public Task SendPasswordResetEmailAsync(string toEmail, string resetToken, CancellationToken ct = default) { @@ -47,14 +43,15 @@ public class ConsoleEmailService : IEmailService private void LogEmail(string toEmail, string subject, string body, string actionUrl) { - _logger.LogInformation($""" + logger.LogInformation($""" - ╔══════════════════════════════════════════════════════════════╗ - ║ EMAIL (Console Mode) ║ - ╚══════════════════════════════════════════════════════════════╝ - To: {toEmail} - Subject: {subject} - Action URL: {actionUrl} - """); + ╔══════════════════════════════════════════════════════════════╗ + ║ EMAIL (Console Mode) ║ + ╚══════════════════════════════════════════════════════════════╝ + To: {toEmail} + Subject: {subject} + Action URL: {actionUrl} + + """); } -} +} \ No newline at end of file diff --git a/src/api/Features/Email/Services/IEmailService.cs b/src/TrackApi/TrackQrApi/Features/Email/Services/IEmailService.cs similarity index 95% rename from src/api/Features/Email/Services/IEmailService.cs rename to src/TrackApi/TrackQrApi/Features/Email/Services/IEmailService.cs index 95e4ad9..c6d63a2 100644 --- a/src/api/Features/Email/Services/IEmailService.cs +++ b/src/TrackApi/TrackQrApi/Features/Email/Services/IEmailService.cs @@ -1,4 +1,4 @@ -namespace api.Features.Email.Services; +namespace TrackQrApi.Features.Email.Services; public interface IEmailService { @@ -33,4 +33,4 @@ public class SmtpSettings public class SendGridSettings { public string ApiKey { get; set; } = string.Empty; -} +} \ No newline at end of file diff --git a/src/api/Features/Email/Services/SmtpEmailService.cs b/src/TrackApi/TrackQrApi/Features/Email/Services/SmtpEmailService.cs similarity index 93% rename from src/api/Features/Email/Services/SmtpEmailService.cs rename to src/TrackApi/TrackQrApi/Features/Email/Services/SmtpEmailService.cs index 0b940f1..376e505 100644 --- a/src/api/Features/Email/Services/SmtpEmailService.cs +++ b/src/TrackApi/TrackQrApi/Features/Email/Services/SmtpEmailService.cs @@ -1,14 +1,14 @@ using System.Net; using System.Net.Mail; -using api.Features.Email.Templates; using Microsoft.Extensions.Options; +using TrackQrApi.Features.Email.Templates; -namespace api.Features.Email.Services; +namespace TrackQrApi.Features.Email.Services; public class SmtpEmailService : IEmailService { - private readonly EmailSettings _settings; private readonly ILogger _logger; + private readonly EmailSettings _settings; public SmtpEmailService(IOptions settings, ILogger logger) { @@ -25,7 +25,8 @@ public class SmtpEmailService : IEmailService _logger.LogInformation("Password reset email sent to {Email}", toEmail); } - public async Task SendEmailVerificationAsync(string toEmail, string verificationToken, CancellationToken ct = default) + public async Task SendEmailVerificationAsync(string toEmail, string verificationToken, + CancellationToken ct = default) { var verifyUrl = $"{_settings.BaseUrl}/verify-email?token={Uri.EscapeDataString(verificationToken)}"; var (subject, htmlBody, textBody) = EmailTemplates.EmailVerification(verifyUrl); @@ -43,7 +44,8 @@ public class SmtpEmailService : IEmailService _logger.LogInformation("Welcome email sent to {Email}", toEmail); } - private async Task SendEmailAsync(string toEmail, string subject, string htmlBody, string textBody, CancellationToken ct) + private async Task SendEmailAsync(string toEmail, string subject, string htmlBody, string textBody, + CancellationToken ct) { if (_settings.Smtp == null) { @@ -76,9 +78,7 @@ public class SmtpEmailService : IEmailService }; if (!string.IsNullOrEmpty(_settings.Smtp.Username)) - { client.Credentials = new NetworkCredential(_settings.Smtp.Username, _settings.Smtp.Password); - } await client.SendMailAsync(message, ct); _logger.LogDebug("Email sent successfully to {Email}", toEmail); @@ -89,4 +89,4 @@ public class SmtpEmailService : IEmailService throw; } } -} +} \ No newline at end of file diff --git a/src/api/Features/Email/Templates/EmailTemplates.cs b/src/TrackApi/TrackQrApi/Features/Email/Templates/EmailTemplates.cs similarity index 99% rename from src/api/Features/Email/Templates/EmailTemplates.cs rename to src/TrackApi/TrackQrApi/Features/Email/Templates/EmailTemplates.cs index 365718a..8d856ef 100644 --- a/src/api/Features/Email/Templates/EmailTemplates.cs +++ b/src/TrackApi/TrackQrApi/Features/Email/Templates/EmailTemplates.cs @@ -1,4 +1,4 @@ -namespace api.Features.Email.Templates; +namespace TrackQrApi.Features.Email.Templates; public static class EmailTemplates { @@ -218,4 +218,4 @@ Go to your dashboard: {dashboardUrl} return (subject, htmlBody, textBody); } -} +} \ No newline at end of file diff --git a/src/api/Features/Events/Services/EventTrackingService.cs b/src/TrackApi/TrackQrApi/Features/Events/Services/EventTrackingService.cs similarity index 95% rename from src/api/Features/Events/Services/EventTrackingService.cs rename to src/TrackApi/TrackQrApi/Features/Events/Services/EventTrackingService.cs index 07dfc4b..7b70cb2 100644 --- a/src/api/Features/Events/Services/EventTrackingService.cs +++ b/src/TrackApi/TrackQrApi/Features/Events/Services/EventTrackingService.cs @@ -1,11 +1,11 @@ using System.Security.Cryptography; using System.Text; using System.Text.RegularExpressions; -using api.Data; -using api.Models; using Microsoft.EntityFrameworkCore; +using TrackQrApi.Data; +using TrackQrApi.Models; -namespace api.Features.Events.Services; +namespace TrackQrApi.Features.Events.Services; public interface IEventTrackingService { @@ -13,7 +13,10 @@ public interface IEventTrackingService Task TrackScanAsync(Guid workspaceId, Guid shortLinkId, Guid qrCodeId, HttpContext context); } -public class EventTrackingService(IServiceScopeFactory scopeFactory, IGeoIpService geoIpService, ILogger logger) +public class EventTrackingService( + IServiceScopeFactory scopeFactory, + IGeoIpService geoIpService, + ILogger logger) : IEventTrackingService { // Dedupe window - same visitor clicking same link within this window counts as one @@ -112,10 +115,8 @@ public class EventTrackingService(IServiceScopeFactory scopeFactory, IGeoIpServi // Check for forwarded headers (when behind a proxy/load balancer) var forwardedFor = context.Request.Headers["X-Forwarded-For"].FirstOrDefault(); if (!string.IsNullOrEmpty(forwardedFor)) - { // Take the first IP in the chain (client IP) return forwardedFor.Split(',')[0].Trim(); - } return context.Connection.RemoteIpAddress?.ToString() ?? "unknown"; } @@ -164,4 +165,4 @@ public class EventTrackingService(IServiceScopeFactory scopeFactory, IGeoIpServi return value.Length <= maxLength ? value : value[..maxLength]; } -} +} \ No newline at end of file diff --git a/src/api/Features/Events/Services/GeoIpService.cs b/src/TrackApi/TrackQrApi/Features/Events/Services/GeoIpService.cs similarity index 83% rename from src/api/Features/Events/Services/GeoIpService.cs rename to src/TrackApi/TrackQrApi/Features/Events/Services/GeoIpService.cs index 7fa704c..7f4aeed 100644 --- a/src/api/Features/Events/Services/GeoIpService.cs +++ b/src/TrackApi/TrackQrApi/Features/Events/Services/GeoIpService.cs @@ -1,7 +1,7 @@ using System.Net; using MaxMind.GeoIP2; -namespace api.Features.Events.Services; +namespace TrackQrApi.Features.Events.Services; public interface IGeoIpService { @@ -10,8 +10,8 @@ public interface IGeoIpService public class GeoIpService : IGeoIpService, IDisposable { - private readonly DatabaseReader? _reader; private readonly ILogger _logger; + private readonly DatabaseReader? _reader; public GeoIpService(IConfiguration configuration, ILogger logger) { @@ -19,7 +19,6 @@ public class GeoIpService : IGeoIpService, IDisposable var dbPath = configuration["GeoIP:DatabasePath"]; if (!string.IsNullOrEmpty(dbPath) && File.Exists(dbPath)) - { try { _reader = new DatabaseReader(dbPath); @@ -29,11 +28,13 @@ public class GeoIpService : IGeoIpService, IDisposable { _logger.LogWarning(ex, "Failed to load GeoIP database from {Path}", dbPath); } - } else - { _logger.LogInformation("GeoIP database not configured or not found. Country detection disabled."); - } + } + + public void Dispose() + { + _reader?.Dispose(); } public string? GetCountryCode(string ipAddress) @@ -44,20 +45,11 @@ public class GeoIpService : IGeoIpService, IDisposable try { // Handle localhost and private IPs - if (ipAddress == "127.0.0.1" || ipAddress == "::1" || IsPrivateIp(ipAddress)) - { - return null; - } + if (ipAddress == "127.0.0.1" || ipAddress == "::1" || IsPrivateIp(ipAddress)) return null; - if (!IPAddress.TryParse(ipAddress, out var ip)) - { - return null; - } + if (!IPAddress.TryParse(ipAddress, out var ip)) return null; - if (_reader.TryCountry(ip, out var response)) - { - return response?.Country?.IsoCode; - } + if (_reader.TryCountry(ip, out var response)) return response?.Country?.IsoCode; } catch (Exception ex) { @@ -87,9 +79,4 @@ public class GeoIpService : IGeoIpService, IDisposable return false; } - - public void Dispose() - { - _reader?.Dispose(); - } -} +} \ No newline at end of file diff --git a/src/api/Features/Links/Common/LinkDto.cs b/src/TrackApi/TrackQrApi/Features/Links/Common/LinkDto.cs similarity index 78% rename from src/api/Features/Links/Common/LinkDto.cs rename to src/TrackApi/TrackQrApi/Features/Links/Common/LinkDto.cs index 51b8e67..676cc45 100644 --- a/src/api/Features/Links/Common/LinkDto.cs +++ b/src/TrackApi/TrackQrApi/Features/Links/Common/LinkDto.cs @@ -1,12 +1,12 @@ -namespace api.Features.Links.Endpoints; +namespace TrackQrApi.Features.Links.Common; public class LinkDto { - public required Guid Id { get; set; } + public required Guid Id { get; set; } public required string Slug { get; set; } public required string DestinationUrl { get; set; } public required string? Title { get; set; } public required string Status { get; set; } public int ClickCount { get; set; } public DateTimeOffset CreatedAt { get; set; } -}; \ No newline at end of file +} \ No newline at end of file diff --git a/src/api/Features/Links/Common/LinkResponses.cs b/src/TrackApi/TrackQrApi/Features/Links/Common/LinkResponses.cs similarity index 89% rename from src/api/Features/Links/Common/LinkResponses.cs rename to src/TrackApi/TrackQrApi/Features/Links/Common/LinkResponses.cs index 194297a..de196be 100644 --- a/src/api/Features/Links/Common/LinkResponses.cs +++ b/src/TrackApi/TrackQrApi/Features/Links/Common/LinkResponses.cs @@ -1,4 +1,4 @@ -namespace api.Features.Links.Common; +namespace TrackQrApi.Features.Links.Common; public record LinkResponse( Guid Id, @@ -18,4 +18,4 @@ public record LinkResponse( public record LinkListResponse( IEnumerable Links -); +); \ No newline at end of file diff --git a/src/api/Features/Links/Common/SlugGenerator.cs b/src/TrackApi/TrackQrApi/Features/Links/Common/SlugGenerator.cs similarity index 88% rename from src/api/Features/Links/Common/SlugGenerator.cs rename to src/TrackApi/TrackQrApi/Features/Links/Common/SlugGenerator.cs index c23a56f..be6f5bf 100644 --- a/src/api/Features/Links/Common/SlugGenerator.cs +++ b/src/TrackApi/TrackQrApi/Features/Links/Common/SlugGenerator.cs @@ -1,6 +1,6 @@ using System.Security.Cryptography; -namespace api.Features.Links.Common; +namespace TrackQrApi.Features.Links.Common; public static class SlugGenerator { @@ -11,4 +11,4 @@ public static class SlugGenerator { return RandomNumberGenerator.GetString(Chars, length); } -} +} \ No newline at end of file diff --git a/src/api/Features/Links/Endpoints/BulkCreateLinksEndpoint.cs b/src/TrackApi/TrackQrApi/Features/Links/Endpoints/BulkCreateLinksEndpoint.cs similarity index 92% rename from src/api/Features/Links/Endpoints/BulkCreateLinksEndpoint.cs rename to src/TrackApi/TrackQrApi/Features/Links/Endpoints/BulkCreateLinksEndpoint.cs index 7e97817..66294dd 100644 --- a/src/api/Features/Links/Endpoints/BulkCreateLinksEndpoint.cs +++ b/src/TrackApi/TrackQrApi/Features/Links/Endpoints/BulkCreateLinksEndpoint.cs @@ -1,12 +1,12 @@ using System.Security.Claims; -using api.Data; -using api.Features.Auth.Common; -using api.Features.Links.Common; -using api.Models; using FastEndpoints; using Microsoft.EntityFrameworkCore; +using TrackQrApi.Data; +using TrackQrApi.Features.Auth.Common; +using TrackQrApi.Features.Links.Common; +using TrackQrApi.Models; -namespace api.Features.Links.Endpoints; +namespace TrackQrApi.Features.Links.Endpoints; public class BulkCreateLinksRequest { @@ -59,7 +59,8 @@ public class BulkCreateLinksEndpoint(AppDbContext db) // Limit bulk creation to 100 links at a time if (req.Links.Count > 100) { - await HttpContext.Response.SendAsync(new MessageResponse("Maximum 100 links per request"), 400, cancellation: ct); + await HttpContext.Response.SendAsync(new MessageResponse("Maximum 100 links per request"), 400, + cancellation: ct); return; } @@ -70,7 +71,7 @@ public class BulkCreateLinksEndpoint(AppDbContext db) var currentLinkCount = await db.ShortLinks.CountAsync(l => l.WorkspaceId == req.WorkspaceId, ct); var linkLimit = GetPlanLinkLimit(workspace.Plan); - for (int i = 0; i < req.Links.Count; i++) + for (var i = 0; i < req.Links.Count; i++) { var item = req.Links[i]; @@ -130,7 +131,7 @@ public class BulkCreateLinksEndpoint(AppDbContext db) Title = item.Title, Status = ShortLinkStatus.Active, CreatedAt = DateTime.UtcNow, - UpdatedAt = DateTime.UtcNow, + UpdatedAt = DateTime.UtcNow }; db.ShortLinks.Add(link); @@ -143,7 +144,7 @@ public class BulkCreateLinksEndpoint(AppDbContext db) Title = link?.Title, Status = link.Status.ToString(), ClickCount = 0, - CreatedAt = link.CreatedAt, + CreatedAt = link.CreatedAt }); } @@ -174,4 +175,4 @@ public class BulkCreateLinksEndpoint(AppDbContext db) _ => 100 // Free plan }; } -} +} \ No newline at end of file diff --git a/src/api/Features/Links/Endpoints/CreateLinkEndpoint.cs b/src/TrackApi/TrackQrApi/Features/Links/Endpoints/CreateLinkEndpoint.cs similarity index 91% rename from src/api/Features/Links/Endpoints/CreateLinkEndpoint.cs rename to src/TrackApi/TrackQrApi/Features/Links/Endpoints/CreateLinkEndpoint.cs index 83c26ce..e14a166 100644 --- a/src/api/Features/Links/Endpoints/CreateLinkEndpoint.cs +++ b/src/TrackApi/TrackQrApi/Features/Links/Endpoints/CreateLinkEndpoint.cs @@ -1,14 +1,14 @@ using System.Security.Claims; -using api.Data; -using api.Features.Auth.Common; -using api.Features.Links.Common; -using api.Features.Plans.Services; -using api.Models; using FastEndpoints; using FluentValidation; using Microsoft.EntityFrameworkCore; +using TrackQrApi.Data; +using TrackQrApi.Features.Auth.Common; +using TrackQrApi.Features.Links.Common; +using TrackQrApi.Features.Plans.Services; +using TrackQrApi.Models; -namespace api.Features.Links.Endpoints; +namespace TrackQrApi.Features.Links.Endpoints; public class CreateLinkRequest { @@ -30,7 +30,8 @@ public class CreateLinkValidator : Validator RuleFor(x => x.Slug) .MaximumLength(50).WithMessage("Slug must not exceed 50 characters") - .Matches(@"^[a-zA-Z0-9_-]*$").WithMessage("Slug can only contain letters, numbers, hyphens, and underscores") + .Matches(@"^[a-zA-Z0-9_-]*$") + .WithMessage("Slug can only contain letters, numbers, hyphens, and underscores") .When(x => !string.IsNullOrEmpty(x.Slug)); RuleFor(x => x.Title) @@ -107,7 +108,8 @@ public class CreateLinkEndpoint(AppDbContext db, IPlanLimitsService planLimits) if (slugExists) { - await HttpContext.Response.SendAsync(new MessageResponse("Slug is already taken"), 409, cancellation: ct); + await HttpContext.Response.SendAsync(new MessageResponse("Slug is already taken"), 409, + cancellation: ct); return; } } @@ -147,4 +149,4 @@ public class CreateLinkEndpoint(AppDbContext db, IPlanLimitsService planLimits) await HttpContext.Response.SendAsync(response, 201, cancellation: ct); } -} +} \ No newline at end of file diff --git a/src/api/Features/Links/Endpoints/DeleteLinkEndpoint.cs b/src/TrackApi/TrackQrApi/Features/Links/Endpoints/DeleteLinkEndpoint.cs similarity index 89% rename from src/api/Features/Links/Endpoints/DeleteLinkEndpoint.cs rename to src/TrackApi/TrackQrApi/Features/Links/Endpoints/DeleteLinkEndpoint.cs index 1570c54..96328cf 100644 --- a/src/api/Features/Links/Endpoints/DeleteLinkEndpoint.cs +++ b/src/TrackApi/TrackQrApi/Features/Links/Endpoints/DeleteLinkEndpoint.cs @@ -1,10 +1,10 @@ using System.Security.Claims; -using api.Data; -using api.Features.Auth.Common; using FastEndpoints; using Microsoft.EntityFrameworkCore; +using TrackQrApi.Data; +using TrackQrApi.Features.Auth.Common; -namespace api.Features.Links.Endpoints; +namespace TrackQrApi.Features.Links.Endpoints; public class DeleteLinkRequest { @@ -42,6 +42,6 @@ public class DeleteLinkEndpoint(AppDbContext db) link.DeletedAt = DateTime.UtcNow; await db.SaveChangesAsync(ct); - await HttpContext.Response.SendAsync(new MessageResponse("Link deleted"), 200, cancellation: ct); + await HttpContext.Response.SendAsync(new MessageResponse("Link deleted"), cancellation: ct); } -} +} \ No newline at end of file diff --git a/src/api/Features/Links/Endpoints/GetLinkEndpoint.cs b/src/TrackApi/TrackQrApi/Features/Links/Endpoints/GetLinkEndpoint.cs similarity index 80% rename from src/api/Features/Links/Endpoints/GetLinkEndpoint.cs rename to src/TrackApi/TrackQrApi/Features/Links/Endpoints/GetLinkEndpoint.cs index c6a540b..90903f6 100644 --- a/src/api/Features/Links/Endpoints/GetLinkEndpoint.cs +++ b/src/TrackApi/TrackQrApi/Features/Links/Endpoints/GetLinkEndpoint.cs @@ -1,11 +1,11 @@ using System.Security.Claims; -using api.Data; -using api.Features.Auth.Common; -using api.Features.Links.Common; using FastEndpoints; using Microsoft.EntityFrameworkCore; +using TrackQrApi.Data; +using TrackQrApi.Features.Auth.Common; +using TrackQrApi.Features.Links.Common; -namespace api.Features.Links.Endpoints; +namespace TrackQrApi.Features.Links.Endpoints; public class GetLinkRequest { @@ -26,7 +26,8 @@ public class GetLinkEndpoint(AppDbContext db) var userId = Guid.Parse(User.FindFirstValue(ClaimTypes.NameIdentifier)!); var link = await db.ShortLinks - .Where(l => l.Id == req.Id && l.WorkspaceId == req.WorkspaceId && l.Workspace.OwnerUserId == userId && l.DeletedAt == null) + .Where(l => l.Id == req.Id && l.WorkspaceId == req.WorkspaceId && l.Workspace.OwnerUserId == userId && + l.DeletedAt == null) .Select(l => new LinkResponse( l.Id, l.WorkspaceId, @@ -50,6 +51,6 @@ public class GetLinkEndpoint(AppDbContext db) return; } - await HttpContext.Response.SendAsync(link, 200, cancellation: ct); + await HttpContext.Response.SendAsync(link, cancellation: ct); } -} +} \ No newline at end of file diff --git a/src/api/Features/Links/Endpoints/ListLinksEndpoint.cs b/src/TrackApi/TrackQrApi/Features/Links/Endpoints/ListLinksEndpoint.cs similarity index 78% rename from src/api/Features/Links/Endpoints/ListLinksEndpoint.cs rename to src/TrackApi/TrackQrApi/Features/Links/Endpoints/ListLinksEndpoint.cs index 37a26ed..29ab561 100644 --- a/src/api/Features/Links/Endpoints/ListLinksEndpoint.cs +++ b/src/TrackApi/TrackQrApi/Features/Links/Endpoints/ListLinksEndpoint.cs @@ -1,11 +1,12 @@ using System.Security.Claims; -using api.Data; -using api.Features.Auth.Common; -using api.Features.Links.Common; using FastEndpoints; using Microsoft.EntityFrameworkCore; +using TrackQrApi.Data; +using TrackQrApi.Features.Auth.Common; +using TrackQrApi.Features.Links.Common; +using TrackQrApi.Models; -namespace api.Features.Links.Endpoints; +namespace TrackQrApi.Features.Links.Endpoints; public class ListLinksRequest { @@ -41,22 +42,14 @@ public class ListLinksEndpoint(AppDbContext db) .Where(l => l.WorkspaceId == req.WorkspaceId); // Filter by deleted status (exclude soft-deleted by default) - if (!req.IncludeDeleted) - { - query = query.Where(l => l.DeletedAt == null); - } + if (!req.IncludeDeleted) query = query.Where(l => l.DeletedAt == null); // Filter by project if specified - if (req.ProjectId.HasValue) - { - query = query.Where(l => l.ProjectId == req.ProjectId.Value); - } + if (req.ProjectId.HasValue) query = query.Where(l => l.ProjectId == req.ProjectId.Value); // Filter by status if specified - if (!string.IsNullOrEmpty(req.Status) && Enum.TryParse(req.Status, true, out var status)) - { + if (!string.IsNullOrEmpty(req.Status) && Enum.TryParse(req.Status, true, out var status)) query = query.Where(l => l.Status == status); - } var links = await query .OrderByDescending(l => l.CreatedAt) @@ -77,6 +70,6 @@ public class ListLinksEndpoint(AppDbContext db) )) .ToListAsync(ct); - await HttpContext.Response.SendAsync(new LinkListResponse(links), 200, cancellation: ct); + await HttpContext.Response.SendAsync(new LinkListResponse(links), cancellation: ct); } -} +} \ No newline at end of file diff --git a/src/api/Features/Links/Endpoints/RestoreLinkEndpoint.cs b/src/TrackApi/TrackQrApi/Features/Links/Endpoints/RestoreLinkEndpoint.cs similarity index 88% rename from src/api/Features/Links/Endpoints/RestoreLinkEndpoint.cs rename to src/TrackApi/TrackQrApi/Features/Links/Endpoints/RestoreLinkEndpoint.cs index f3cab0e..5f24cc8 100644 --- a/src/api/Features/Links/Endpoints/RestoreLinkEndpoint.cs +++ b/src/TrackApi/TrackQrApi/Features/Links/Endpoints/RestoreLinkEndpoint.cs @@ -1,11 +1,11 @@ using System.Security.Claims; -using api.Data; -using api.Features.Auth.Common; -using api.Features.Links.Common; using FastEndpoints; using Microsoft.EntityFrameworkCore; +using TrackQrApi.Data; +using TrackQrApi.Features.Auth.Common; +using TrackQrApi.Features.Links.Common; -namespace api.Features.Links.Endpoints; +namespace TrackQrApi.Features.Links.Endpoints; public class RestoreLinkRequest { @@ -60,6 +60,6 @@ public class RestoreLinkEndpoint(AppDbContext db) link.DeletedAt ); - await HttpContext.Response.SendAsync(response, 200, cancellation: ct); + await HttpContext.Response.SendAsync(response, cancellation: ct); } -} +} \ No newline at end of file diff --git a/src/api/Features/Links/Endpoints/UpdateLinkEndpoint.cs b/src/TrackApi/TrackQrApi/Features/Links/Endpoints/UpdateLinkEndpoint.cs similarity index 79% rename from src/api/Features/Links/Endpoints/UpdateLinkEndpoint.cs rename to src/TrackApi/TrackQrApi/Features/Links/Endpoints/UpdateLinkEndpoint.cs index 4d06bb8..4a0f20f 100644 --- a/src/api/Features/Links/Endpoints/UpdateLinkEndpoint.cs +++ b/src/TrackApi/TrackQrApi/Features/Links/Endpoints/UpdateLinkEndpoint.cs @@ -1,13 +1,13 @@ using System.Security.Claims; -using api.Data; -using api.Features.Auth.Common; -using api.Features.Links.Common; -using api.Models; using FastEndpoints; using FluentValidation; using Microsoft.EntityFrameworkCore; +using TrackQrApi.Data; +using TrackQrApi.Features.Auth.Common; +using TrackQrApi.Features.Links.Common; +using TrackQrApi.Models; -namespace api.Features.Links.Endpoints; +namespace TrackQrApi.Features.Links.Endpoints; public class UpdateLinkRequest { @@ -66,7 +66,8 @@ public class UpdateLinkEndpoint(AppDbContext db) var link = await db.ShortLinks .Include(l => l.Workspace) - .FirstOrDefaultAsync(l => l.Id == req.Id && l.WorkspaceId == req.WorkspaceId && l.Workspace.OwnerUserId == userId, ct); + .FirstOrDefaultAsync( + l => l.Id == req.Id && l.WorkspaceId == req.WorkspaceId && l.Workspace.OwnerUserId == userId, ct); if (link is null) { @@ -88,43 +89,22 @@ public class UpdateLinkEndpoint(AppDbContext db) } // Update fields - if (!string.IsNullOrEmpty(req.DestinationUrl)) - { - link.DestinationUrl = req.DestinationUrl; - } + if (!string.IsNullOrEmpty(req.DestinationUrl)) link.DestinationUrl = req.DestinationUrl; - if (req.Title != null) - { - link.Title = req.Title; - } + if (req.Title != null) link.Title = req.Title; if (!string.IsNullOrEmpty(req.Status) && Enum.TryParse(req.Status, true, out var status)) - { link.Status = status; - } - if (req.ExpiresAt.HasValue) - { - link.ExpiresAt = req.ExpiresAt.Value; - } + if (req.ExpiresAt.HasValue) link.ExpiresAt = req.ExpiresAt.Value; if (!string.IsNullOrEmpty(req.Password)) - { link.PasswordHash = BCrypt.Net.BCrypt.HashPassword(req.Password); - } - else if (req.RemovePassword == true) - { - link.PasswordHash = null; - } + else if (req.RemovePassword == true) link.PasswordHash = null; if (req.ProjectId.HasValue) - { link.ProjectId = req.ProjectId.Value; - } - else if (req.RemoveProject == true) - { - link.ProjectId = null; - } + else if (req.RemoveProject == true) link.ProjectId = null; link.UpdatedAt = DateTime.UtcNow; await db.SaveChangesAsync(ct); @@ -144,6 +124,6 @@ public class UpdateLinkEndpoint(AppDbContext db) link.UpdatedAt ); - await HttpContext.Response.SendAsync(response, 200, cancellation: ct); + await HttpContext.Response.SendAsync(response, cancellation: ct); } -} +} \ No newline at end of file diff --git a/src/TrackApi/TrackQrApi/Features/Plans/Endpoints/GetUsageEndpoint.cs b/src/TrackApi/TrackQrApi/Features/Plans/Endpoints/GetUsageEndpoint.cs new file mode 100644 index 0000000..60ac0a5 --- /dev/null +++ b/src/TrackApi/TrackQrApi/Features/Plans/Endpoints/GetUsageEndpoint.cs @@ -0,0 +1,94 @@ +using System.Security.Claims; +using FastEndpoints; +using TrackQrApi.Features.Plans.Services; + +namespace TrackQrApi.Features.Plans.Endpoints; + +public class GetUsageRequest +{ + public Guid? WorkspaceId { get; set; } +} + +public record UsageResponse( + int Workspaces, + int Links, + int QRCodes, + int Domains, + int EventsThisMonth, + string Plan, + LimitsResponse Limits +); + +public record LimitsResponse( + int MaxWorkspaces, + int MaxLinks, + int MaxQRCodes, + int MaxDomains, + int MaxEventsPerMonth, + bool HasCustomDomains, + bool HasPasswordProtection +); + +public class GetUsageEndpoint(IPlanLimitsService planLimits) + : Endpoint +{ + public override void Configure() + { + Get("/usage"); + } + + public override async Task HandleAsync(GetUsageRequest req, CancellationToken ct) + { + var userId = Guid.Parse(User.FindFirstValue(ClaimTypes.NameIdentifier)!); + + if (req.WorkspaceId.HasValue) + { + var wsUsage = await planLimits.GetWorkspaceUsageAsync(req.WorkspaceId.Value, ct); + + var response = new UsageResponse( + 1, + wsUsage.Links, + wsUsage.QRCodes, + wsUsage.Domains, + wsUsage.EventsThisMonth, + wsUsage.Plan.ToString(), + new LimitsResponse( + wsUsage.Limits.MaxWorkspaces, + wsUsage.Limits.MaxLinksPerWorkspace, + wsUsage.Limits.MaxQRCodesPerWorkspace, + wsUsage.Limits.MaxDomainsPerWorkspace, + wsUsage.Limits.MaxEventsPerMonth, + wsUsage.Limits.HasCustomDomains, + wsUsage.Limits.HasPasswordProtection + ) + ); + + await HttpContext.Response.SendAsync(response, cancellation: ct); + } + else + { + var usage = await planLimits.GetUsageAsync(userId, ct); + var limits = planLimits.GetLimits(usage.HighestPlan); + + var response = new UsageResponse( + usage.TotalWorkspaces, + usage.TotalLinks, + usage.TotalQRCodes, + usage.TotalDomains, + usage.EventsThisMonth, + usage.HighestPlan.ToString(), + new LimitsResponse( + limits.MaxWorkspaces, + limits.MaxLinksPerWorkspace, + limits.MaxQRCodesPerWorkspace, + limits.MaxDomainsPerWorkspace, + limits.MaxEventsPerMonth, + limits.HasCustomDomains, + limits.HasPasswordProtection + ) + ); + + await HttpContext.Response.SendAsync(response, cancellation: ct); + } + } +} \ No newline at end of file diff --git a/src/api/Features/Plans/Services/PlanLimitsService.cs b/src/TrackApi/TrackQrApi/Features/Plans/Services/PlanLimitsService.cs similarity index 77% rename from src/api/Features/Plans/Services/PlanLimitsService.cs rename to src/TrackApi/TrackQrApi/Features/Plans/Services/PlanLimitsService.cs index 052dcd2..988c739 100644 --- a/src/api/Features/Plans/Services/PlanLimitsService.cs +++ b/src/TrackApi/TrackQrApi/Features/Plans/Services/PlanLimitsService.cs @@ -1,8 +1,8 @@ -using api.Data; -using api.Models; using Microsoft.EntityFrameworkCore; +using TrackQrApi.Data; +using TrackQrApi.Models; -namespace api.Features.Plans.Services; +namespace TrackQrApi.Features.Plans.Services; public interface IPlanLimitsService { @@ -51,38 +51,41 @@ public class PlanLimitsService(IServiceScopeFactory scopeFactory) : IPlanLimitsS private static readonly Dictionary PlanConfigs = new() { [WorkspacePlan.Free] = new PlanLimits( - MaxWorkspaces: 1, - MaxLinksPerWorkspace: 50, - MaxQRCodesPerWorkspace: 25, - MaxDomainsPerWorkspace: 0, - MaxEventsPerMonth: 10_000, - HasCustomDomains: false, - HasPasswordProtection: false, - HasAnalytics: true + 1, + 50, + 25, + 0, + 10_000, + false, + false, + true ), [WorkspacePlan.Pro] = new PlanLimits( - MaxWorkspaces: 5, - MaxLinksPerWorkspace: 5_000, - MaxQRCodesPerWorkspace: 1_000, - MaxDomainsPerWorkspace: 3, - MaxEventsPerMonth: 100_000, - HasCustomDomains: true, - HasPasswordProtection: true, - HasAnalytics: true + 5, + 5_000, + 1_000, + 3, + 100_000, + true, + true, + true ), [WorkspacePlan.Business] = new PlanLimits( - MaxWorkspaces: int.MaxValue, - MaxLinksPerWorkspace: int.MaxValue, - MaxQRCodesPerWorkspace: int.MaxValue, - MaxDomainsPerWorkspace: int.MaxValue, - MaxEventsPerMonth: int.MaxValue, - HasCustomDomains: true, - HasPasswordProtection: true, - HasAnalytics: true + int.MaxValue, + int.MaxValue, + int.MaxValue, + int.MaxValue, + int.MaxValue, + true, + true, + true ) }; - public PlanLimits GetLimits(WorkspacePlan plan) => PlanConfigs[plan]; + public PlanLimits GetLimits(WorkspacePlan plan) + { + return PlanConfigs[plan]; + } public async Task GetUsageAsync(Guid userId, CancellationToken ct = default) { @@ -114,12 +117,12 @@ public class PlanLimitsService(IServiceScopeFactory scopeFactory) : IPlanLimitsS : WorkspacePlan.Free; return new UsageStats( - TotalWorkspaces: workspaces.Count, - TotalLinks: totalLinks, - TotalQRCodes: totalQRCodes, - TotalDomains: totalDomains, - EventsThisMonth: eventsThisMonth, - HighestPlan: highestPlan + workspaces.Count, + totalLinks, + totalQRCodes, + totalDomains, + eventsThisMonth, + highestPlan ); } @@ -147,13 +150,13 @@ public class PlanLimitsService(IServiceScopeFactory scopeFactory) : IPlanLimitsS var limits = GetLimits(workspace.Plan); return new WorkspaceUsageStats( - WorkspaceId: workspaceId, - Plan: workspace.Plan, - Links: links, - QRCodes: qrCodes, - Domains: domains, - EventsThisMonth: eventsThisMonth, - Limits: limits + workspaceId, + workspace.Plan, + links, + qrCodes, + domains, + eventsThisMonth, + limits ); } @@ -187,4 +190,4 @@ public class PlanLimitsService(IServiceScopeFactory scopeFactory) : IPlanLimitsS var usage = await GetWorkspaceUsageAsync(workspaceId, ct); return usage.EventsThisMonth < usage.Limits.MaxEventsPerMonth; } -} +} \ No newline at end of file diff --git a/src/api/Features/Projects/Common/ProjectResponses.cs b/src/TrackApi/TrackQrApi/Features/Projects/Common/ProjectResponses.cs similarity index 83% rename from src/api/Features/Projects/Common/ProjectResponses.cs rename to src/TrackApi/TrackQrApi/Features/Projects/Common/ProjectResponses.cs index c7e2338..264da9f 100644 --- a/src/api/Features/Projects/Common/ProjectResponses.cs +++ b/src/TrackApi/TrackQrApi/Features/Projects/Common/ProjectResponses.cs @@ -1,4 +1,4 @@ -namespace api.Features.Projects.Common; +namespace TrackQrApi.Features.Projects.Common; public record ProjectResponse( Guid Id, @@ -12,4 +12,4 @@ public record ProjectResponse( public record ProjectListResponse( IEnumerable Projects -); +); \ No newline at end of file diff --git a/src/api/Features/Projects/Endpoints/CreateProjectEndpoint.cs b/src/TrackApi/TrackQrApi/Features/Projects/Endpoints/CreateProjectEndpoint.cs similarity index 91% rename from src/api/Features/Projects/Endpoints/CreateProjectEndpoint.cs rename to src/TrackApi/TrackQrApi/Features/Projects/Endpoints/CreateProjectEndpoint.cs index 37bb6b9..8f60878 100644 --- a/src/api/Features/Projects/Endpoints/CreateProjectEndpoint.cs +++ b/src/TrackApi/TrackQrApi/Features/Projects/Endpoints/CreateProjectEndpoint.cs @@ -1,13 +1,13 @@ using System.Security.Claims; -using api.Data; -using api.Features.Auth.Common; -using api.Features.Projects.Common; -using api.Models; using FastEndpoints; using FluentValidation; using Microsoft.EntityFrameworkCore; +using TrackQrApi.Data; +using TrackQrApi.Features.Auth.Common; +using TrackQrApi.Features.Projects.Common; +using TrackQrApi.Models; -namespace api.Features.Projects.Endpoints; +namespace TrackQrApi.Features.Projects.Endpoints; public class CreateProjectRequest { @@ -72,4 +72,4 @@ public class CreateProjectEndpoint(AppDbContext db) await HttpContext.Response.SendAsync(response, 201, cancellation: ct); } -} +} \ No newline at end of file diff --git a/src/api/Features/Projects/Endpoints/DeleteProjectEndpoint.cs b/src/TrackApi/TrackQrApi/Features/Projects/Endpoints/DeleteProjectEndpoint.cs similarity index 76% rename from src/api/Features/Projects/Endpoints/DeleteProjectEndpoint.cs rename to src/TrackApi/TrackQrApi/Features/Projects/Endpoints/DeleteProjectEndpoint.cs index e876e70..ec1c4b8 100644 --- a/src/api/Features/Projects/Endpoints/DeleteProjectEndpoint.cs +++ b/src/TrackApi/TrackQrApi/Features/Projects/Endpoints/DeleteProjectEndpoint.cs @@ -1,10 +1,10 @@ using System.Security.Claims; -using api.Data; -using api.Features.Auth.Common; using FastEndpoints; using Microsoft.EntityFrameworkCore; +using TrackQrApi.Data; +using TrackQrApi.Features.Auth.Common; -namespace api.Features.Projects.Endpoints; +namespace TrackQrApi.Features.Projects.Endpoints; public class DeleteProjectRequest { @@ -26,7 +26,8 @@ public class DeleteProjectEndpoint(AppDbContext db) var project = await db.Projects .Include(p => p.Workspace) - .FirstOrDefaultAsync(p => p.Id == req.Id && p.WorkspaceId == req.WorkspaceId && p.Workspace.OwnerUserId == userId, ct); + .FirstOrDefaultAsync( + p => p.Id == req.Id && p.WorkspaceId == req.WorkspaceId && p.Workspace.OwnerUserId == userId, ct); if (project is null) { @@ -37,6 +38,6 @@ public class DeleteProjectEndpoint(AppDbContext db) db.Projects.Remove(project); await db.SaveChangesAsync(ct); - await HttpContext.Response.SendAsync(new MessageResponse("Project deleted"), 200, cancellation: ct); + await HttpContext.Response.SendAsync(new MessageResponse("Project deleted"), cancellation: ct); } -} +} \ No newline at end of file diff --git a/src/api/Features/Projects/Endpoints/GetProjectEndpoint.cs b/src/TrackApi/TrackQrApi/Features/Projects/Endpoints/GetProjectEndpoint.cs similarity index 84% rename from src/api/Features/Projects/Endpoints/GetProjectEndpoint.cs rename to src/TrackApi/TrackQrApi/Features/Projects/Endpoints/GetProjectEndpoint.cs index eae4e29..ac611f1 100644 --- a/src/api/Features/Projects/Endpoints/GetProjectEndpoint.cs +++ b/src/TrackApi/TrackQrApi/Features/Projects/Endpoints/GetProjectEndpoint.cs @@ -1,11 +1,11 @@ using System.Security.Claims; -using api.Data; -using api.Features.Auth.Common; -using api.Features.Projects.Common; using FastEndpoints; using Microsoft.EntityFrameworkCore; +using TrackQrApi.Data; +using TrackQrApi.Features.Auth.Common; +using TrackQrApi.Features.Projects.Common; -namespace api.Features.Projects.Endpoints; +namespace TrackQrApi.Features.Projects.Endpoints; public class GetProjectRequest { @@ -44,6 +44,6 @@ public class GetProjectEndpoint(AppDbContext db) return; } - await HttpContext.Response.SendAsync(project, 200, cancellation: ct); + await HttpContext.Response.SendAsync(project, cancellation: ct); } -} +} \ No newline at end of file diff --git a/src/api/Features/Projects/Endpoints/ListProjectsEndpoint.cs b/src/TrackApi/TrackQrApi/Features/Projects/Endpoints/ListProjectsEndpoint.cs similarity index 88% rename from src/api/Features/Projects/Endpoints/ListProjectsEndpoint.cs rename to src/TrackApi/TrackQrApi/Features/Projects/Endpoints/ListProjectsEndpoint.cs index a3f225a..4d3b335 100644 --- a/src/api/Features/Projects/Endpoints/ListProjectsEndpoint.cs +++ b/src/TrackApi/TrackQrApi/Features/Projects/Endpoints/ListProjectsEndpoint.cs @@ -1,11 +1,11 @@ using System.Security.Claims; -using api.Data; -using api.Features.Auth.Common; -using api.Features.Projects.Common; using FastEndpoints; using Microsoft.EntityFrameworkCore; +using TrackQrApi.Data; +using TrackQrApi.Features.Auth.Common; +using TrackQrApi.Features.Projects.Common; -namespace api.Features.Projects.Endpoints; +namespace TrackQrApi.Features.Projects.Endpoints; public class ListProjectsRequest { @@ -48,6 +48,6 @@ public class ListProjectsEndpoint(AppDbContext db) )) .ToListAsync(ct); - await HttpContext.Response.SendAsync(new ProjectListResponse(projects), 200, cancellation: ct); + await HttpContext.Response.SendAsync(new ProjectListResponse(projects), cancellation: ct); } -} +} \ No newline at end of file diff --git a/src/api/Features/Projects/Endpoints/UpdateProjectEndpoint.cs b/src/TrackApi/TrackQrApi/Features/Projects/Endpoints/UpdateProjectEndpoint.cs similarity index 82% rename from src/api/Features/Projects/Endpoints/UpdateProjectEndpoint.cs rename to src/TrackApi/TrackQrApi/Features/Projects/Endpoints/UpdateProjectEndpoint.cs index 3c9efa8..d2d7028 100644 --- a/src/api/Features/Projects/Endpoints/UpdateProjectEndpoint.cs +++ b/src/TrackApi/TrackQrApi/Features/Projects/Endpoints/UpdateProjectEndpoint.cs @@ -1,12 +1,12 @@ using System.Security.Claims; -using api.Data; -using api.Features.Auth.Common; -using api.Features.Projects.Common; using FastEndpoints; using FluentValidation; using Microsoft.EntityFrameworkCore; +using TrackQrApi.Data; +using TrackQrApi.Features.Auth.Common; +using TrackQrApi.Features.Projects.Common; -namespace api.Features.Projects.Endpoints; +namespace TrackQrApi.Features.Projects.Endpoints; public class UpdateProjectRequest { @@ -42,7 +42,8 @@ public class UpdateProjectEndpoint(AppDbContext db) .Include(p => p.Workspace) .Include(p => p.ShortLinks) .Include(p => p.QRCodeDesigns) - .FirstOrDefaultAsync(p => p.Id == req.Id && p.WorkspaceId == req.WorkspaceId && p.Workspace.OwnerUserId == userId, ct); + .FirstOrDefaultAsync( + p => p.Id == req.Id && p.WorkspaceId == req.WorkspaceId && p.Workspace.OwnerUserId == userId, ct); if (project is null) { @@ -64,6 +65,6 @@ public class UpdateProjectEndpoint(AppDbContext db) project.CreatedAt ); - await HttpContext.Response.SendAsync(response, 200, cancellation: ct); + await HttpContext.Response.SendAsync(response, cancellation: ct); } -} +} \ No newline at end of file diff --git a/src/api/Features/QRCodes/Common/QRCodeModels.cs b/src/TrackApi/TrackQrApi/Features/QRCodes/Common/QRCodeModels.cs similarity index 91% rename from src/api/Features/QRCodes/Common/QRCodeModels.cs rename to src/TrackApi/TrackQrApi/Features/QRCodes/Common/QRCodeModels.cs index 45be832..7a94637 100644 --- a/src/api/Features/QRCodes/Common/QRCodeModels.cs +++ b/src/TrackApi/TrackQrApi/Features/QRCodes/Common/QRCodeModels.cs @@ -1,9 +1,7 @@ -using System.Text.Json.Serialization; - -namespace api.Features.QRCodes.Common; +namespace TrackQrApi.Features.QRCodes.Common; /// -/// QR code style configuration stored as JSON +/// QR code style configuration stored as JSON /// public class QRCodeStyle { @@ -52,4 +50,4 @@ public record QRCodePreviewResponse( string Format, int Width, int Height -); +); \ No newline at end of file diff --git a/src/api/Features/QRCodes/Endpoints/CreateQRCodeEndpoint.cs b/src/TrackApi/TrackQrApi/Features/QRCodes/Endpoints/CreateQRCodeEndpoint.cs similarity index 92% rename from src/api/Features/QRCodes/Endpoints/CreateQRCodeEndpoint.cs rename to src/TrackApi/TrackQrApi/Features/QRCodes/Endpoints/CreateQRCodeEndpoint.cs index 113dc5f..47dc1b2 100644 --- a/src/api/Features/QRCodes/Endpoints/CreateQRCodeEndpoint.cs +++ b/src/TrackApi/TrackQrApi/Features/QRCodes/Endpoints/CreateQRCodeEndpoint.cs @@ -1,15 +1,15 @@ using System.Security.Claims; using System.Text.Json; -using api.Data; -using api.Features.Auth.Common; -using api.Features.Plans.Services; -using api.Features.QRCodes.Common; -using api.Models; using FastEndpoints; using FluentValidation; using Microsoft.EntityFrameworkCore; +using TrackQrApi.Data; +using TrackQrApi.Features.Auth.Common; +using TrackQrApi.Features.Plans.Services; +using TrackQrApi.Features.QRCodes.Common; +using TrackQrApi.Models; -namespace api.Features.QRCodes.Endpoints; +namespace TrackQrApi.Features.QRCodes.Endpoints; public class CreateQRCodeRequest { @@ -73,9 +73,11 @@ public class CreateQRCodeEndpoint(AppDbContext db, IPlanLimitsService planLimits if (link is null) { - await HttpContext.Response.SendAsync(new MessageResponse("Short link not found"), 404, cancellation: ct); + await HttpContext.Response.SendAsync(new MessageResponse("Short link not found"), 404, + cancellation: ct); return; } + linkSlug = link.Slug; } @@ -103,7 +105,8 @@ public class CreateQRCodeEndpoint(AppDbContext db, IPlanLimitsService planLimits if (asset is null) { - await HttpContext.Response.SendAsync(new MessageResponse("Logo asset not found"), 404, cancellation: ct); + await HttpContext.Response.SendAsync(new MessageResponse("Logo asset not found"), 404, + cancellation: ct); return; } @@ -146,4 +149,4 @@ public class CreateQRCodeEndpoint(AppDbContext db, IPlanLimitsService planLimits await HttpContext.Response.SendAsync(response, 201, cancellation: ct); } -} +} \ No newline at end of file diff --git a/src/api/Features/QRCodes/Endpoints/DeleteQRCodeEndpoint.cs b/src/TrackApi/TrackQrApi/Features/QRCodes/Endpoints/DeleteQRCodeEndpoint.cs similarity index 76% rename from src/api/Features/QRCodes/Endpoints/DeleteQRCodeEndpoint.cs rename to src/TrackApi/TrackQrApi/Features/QRCodes/Endpoints/DeleteQRCodeEndpoint.cs index 892128d..fe06994 100644 --- a/src/api/Features/QRCodes/Endpoints/DeleteQRCodeEndpoint.cs +++ b/src/TrackApi/TrackQrApi/Features/QRCodes/Endpoints/DeleteQRCodeEndpoint.cs @@ -1,10 +1,10 @@ using System.Security.Claims; -using api.Data; -using api.Features.Auth.Common; using FastEndpoints; using Microsoft.EntityFrameworkCore; +using TrackQrApi.Data; +using TrackQrApi.Features.Auth.Common; -namespace api.Features.QRCodes.Endpoints; +namespace TrackQrApi.Features.QRCodes.Endpoints; public class DeleteQRCodeRequest { @@ -26,7 +26,8 @@ public class DeleteQRCodeEndpoint(AppDbContext db) var qrCode = await db.QrCodeDesigns .Include(q => q.Workspace) - .FirstOrDefaultAsync(q => q.Id == req.Id && q.WorkspaceId == req.WorkspaceId && q.Workspace.OwnerUserId == userId, ct); + .FirstOrDefaultAsync( + q => q.Id == req.Id && q.WorkspaceId == req.WorkspaceId && q.Workspace.OwnerUserId == userId, ct); if (qrCode is null) { @@ -37,6 +38,6 @@ public class DeleteQRCodeEndpoint(AppDbContext db) db.QrCodeDesigns.Remove(qrCode); await db.SaveChangesAsync(ct); - await HttpContext.Response.SendAsync(new MessageResponse("QR code deleted"), 200, cancellation: ct); + await HttpContext.Response.SendAsync(new MessageResponse("QR code deleted"), cancellation: ct); } -} +} \ No newline at end of file diff --git a/src/api/Features/QRCodes/Endpoints/ExportQRCodeEndpoint.cs b/src/TrackApi/TrackQrApi/Features/QRCodes/Endpoints/ExportQRCodeEndpoint.cs similarity index 84% rename from src/api/Features/QRCodes/Endpoints/ExportQRCodeEndpoint.cs rename to src/TrackApi/TrackQrApi/Features/QRCodes/Endpoints/ExportQRCodeEndpoint.cs index 4f2e753..fc0b1ac 100644 --- a/src/api/Features/QRCodes/Endpoints/ExportQRCodeEndpoint.cs +++ b/src/TrackApi/TrackQrApi/Features/QRCodes/Endpoints/ExportQRCodeEndpoint.cs @@ -1,14 +1,14 @@ using System.Security.Claims; using System.Text.Json; -using api.Data; -using api.Features.Assets.Services; -using api.Features.Auth.Common; -using api.Features.QRCodes.Common; -using api.Features.QRCodes.Services; using FastEndpoints; using Microsoft.EntityFrameworkCore; +using TrackQrApi.Data; +using TrackQrApi.Features.Assets.Services; +using TrackQrApi.Features.Auth.Common; +using TrackQrApi.Features.QRCodes.Common; +using TrackQrApi.Features.QRCodes.Services; -namespace api.Features.QRCodes.Endpoints; +namespace TrackQrApi.Features.QRCodes.Endpoints; public class ExportQRCodeRequest { @@ -18,7 +18,10 @@ public class ExportQRCodeRequest public int? Size { get; set; } } -public class ExportQRCodeEndpoint(AppDbContext db, IQrCodeGeneratorService qrGenerator, IAssetStorageService assetStorage) +public class ExportQRCodeEndpoint( + AppDbContext db, + IQrCodeGeneratorService qrGenerator, + IAssetStorageService assetStorage) : Endpoint { public override void Configure() @@ -44,7 +47,8 @@ public class ExportQRCodeEndpoint(AppDbContext db, IQrCodeGeneratorService qrGen if (qrCode.ShortLink is null) { - await HttpContext.Response.SendAsync(new MessageResponse("QR code has no associated link"), 400, cancellation: ct); + await HttpContext.Response.SendAsync(new MessageResponse("QR code has no associated link"), 400, + cancellation: ct); return; } @@ -63,10 +67,7 @@ public class ExportQRCodeEndpoint(AppDbContext db, IQrCodeGeneratorService qrGen if (qrCode.LogoAsset != null) { var logoResult = await assetStorage.GetAsync(qrCode.LogoAsset.StorageKey); - if (logoResult.HasValue) - { - logoStream = logoResult.Value.Stream; - } + if (logoResult.HasValue) logoStream = logoResult.Value.Stream; } try @@ -92,4 +93,4 @@ public class ExportQRCodeEndpoint(AppDbContext db, IQrCodeGeneratorService qrGen logoStream?.Dispose(); } } -} +} \ No newline at end of file diff --git a/src/api/Features/QRCodes/Endpoints/GetQRCodeAnalyticsEndpoint.cs b/src/TrackApi/TrackQrApi/Features/QRCodes/Endpoints/GetQRCodeAnalyticsEndpoint.cs similarity index 84% rename from src/api/Features/QRCodes/Endpoints/GetQRCodeAnalyticsEndpoint.cs rename to src/TrackApi/TrackQrApi/Features/QRCodes/Endpoints/GetQRCodeAnalyticsEndpoint.cs index 0ca0a09..7797146 100644 --- a/src/api/Features/QRCodes/Endpoints/GetQRCodeAnalyticsEndpoint.cs +++ b/src/TrackApi/TrackQrApi/Features/QRCodes/Endpoints/GetQRCodeAnalyticsEndpoint.cs @@ -1,11 +1,11 @@ using System.Security.Claims; -using api.Data; -using api.Features.Auth.Common; -using api.Models; using FastEndpoints; using Microsoft.EntityFrameworkCore; +using TrackQrApi.Data; +using TrackQrApi.Features.Auth.Common; +using TrackQrApi.Models; -namespace api.Features.QRCodes.Endpoints; +namespace TrackQrApi.Features.QRCodes.Endpoints; public class GetQRCodeAnalyticsRequest { @@ -65,16 +65,16 @@ public class GetQRCodeAnalyticsEndpoint(AppDbContext db) .ToListAsync(ct); var summary = new QRCodeAnalyticsSummary( - TotalScans: events.Count, - UniqueVisitors: events.Select(e => e.IpHash).Distinct().Count() + events.Count, + events.Select(e => e.IpHash).Distinct().Count() ); var timeSeries = events .GroupBy(e => e.Timestamp.Date) .OrderBy(g => g.Key) .Select(g => new QRCodeTimeSeriesPoint( - Date: g.Key.ToString("yyyy-MM-dd"), - Scans: g.Count() + g.Key.ToString("yyyy-MM-dd"), + g.Count() )) .ToList(); @@ -98,14 +98,14 @@ public class GetQRCodeAnalyticsEndpoint(AppDbContext db) .ToDictionary(g => g.Key, g => g.Count()); var response = new QRCodeAnalyticsResponse( - QRCodeId: qrCode.Id, - Name: qrCode.Name, - LinkSlug: qrCode.ShortLink?.Slug, - Summary: summary, - TimeSeries: timeSeries, - DeviceBreakdown: deviceBreakdown, - ReferrerBreakdown: referrerBreakdown, - CountryBreakdown: countryBreakdown + qrCode.Id, + qrCode.Name, + qrCode.ShortLink?.Slug, + summary, + timeSeries, + deviceBreakdown, + referrerBreakdown, + countryBreakdown ); await HttpContext.Response.SendAsync(response, cancellation: ct); @@ -135,4 +135,4 @@ public class GetQRCodeAnalyticsEndpoint(AppDbContext db) return referrer; } } -} +} \ No newline at end of file diff --git a/src/api/Features/QRCodes/Endpoints/GetQRCodeEndpoint.cs b/src/TrackApi/TrackQrApi/Features/QRCodes/Endpoints/GetQRCodeEndpoint.cs similarity index 88% rename from src/api/Features/QRCodes/Endpoints/GetQRCodeEndpoint.cs rename to src/TrackApi/TrackQrApi/Features/QRCodes/Endpoints/GetQRCodeEndpoint.cs index 39a9947..fe29742 100644 --- a/src/api/Features/QRCodes/Endpoints/GetQRCodeEndpoint.cs +++ b/src/TrackApi/TrackQrApi/Features/QRCodes/Endpoints/GetQRCodeEndpoint.cs @@ -1,12 +1,12 @@ using System.Security.Claims; using System.Text.Json; -using api.Data; -using api.Features.Auth.Common; -using api.Features.QRCodes.Common; using FastEndpoints; using Microsoft.EntityFrameworkCore; +using TrackQrApi.Data; +using TrackQrApi.Features.Auth.Common; +using TrackQrApi.Features.QRCodes.Common; -namespace api.Features.QRCodes.Endpoints; +namespace TrackQrApi.Features.QRCodes.Endpoints; public class GetQRCodeRequest { @@ -55,6 +55,6 @@ public class GetQRCodeEndpoint(AppDbContext db) qrCode.UpdatedAt ); - await HttpContext.Response.SendAsync(response, 200, cancellation: ct); + await HttpContext.Response.SendAsync(response, cancellation: ct); } -} +} \ No newline at end of file diff --git a/src/api/Features/QRCodes/Endpoints/ListQRCodesEndpoint.cs b/src/TrackApi/TrackQrApi/Features/QRCodes/Endpoints/ListQRCodesEndpoint.cs similarity index 80% rename from src/api/Features/QRCodes/Endpoints/ListQRCodesEndpoint.cs rename to src/TrackApi/TrackQrApi/Features/QRCodes/Endpoints/ListQRCodesEndpoint.cs index 6550687..73cf65a 100644 --- a/src/api/Features/QRCodes/Endpoints/ListQRCodesEndpoint.cs +++ b/src/TrackApi/TrackQrApi/Features/QRCodes/Endpoints/ListQRCodesEndpoint.cs @@ -1,12 +1,12 @@ using System.Security.Claims; using System.Text.Json; -using api.Data; -using api.Features.Auth.Common; -using api.Features.QRCodes.Common; using FastEndpoints; using Microsoft.EntityFrameworkCore; +using TrackQrApi.Data; +using TrackQrApi.Features.Auth.Common; +using TrackQrApi.Features.QRCodes.Common; -namespace api.Features.QRCodes.Endpoints; +namespace TrackQrApi.Features.QRCodes.Endpoints; public class ListQRCodesRequest { @@ -40,15 +40,9 @@ public class ListQRCodesEndpoint(AppDbContext db) var query = db.QrCodeDesigns .Where(q => q.WorkspaceId == req.WorkspaceId); - if (req.ProjectId.HasValue) - { - query = query.Where(q => q.ProjectId == req.ProjectId.Value); - } + if (req.ProjectId.HasValue) query = query.Where(q => q.ProjectId == req.ProjectId.Value); - if (req.ShortLinkId.HasValue) - { - query = query.Where(q => q.ShortLinkId == req.ShortLinkId.Value); - } + if (req.ShortLinkId.HasValue) query = query.Where(q => q.ShortLinkId == req.ShortLinkId.Value); var qrCodes = await query .Include(q => q.ShortLink) @@ -74,6 +68,6 @@ public class ListQRCodesEndpoint(AppDbContext db) )) ); - await HttpContext.Response.SendAsync(response, 200, cancellation: ct); + await HttpContext.Response.SendAsync(response, cancellation: ct); } -} +} \ No newline at end of file diff --git a/src/api/Features/QRCodes/Endpoints/PreviewQRCodeEndpoint.cs b/src/TrackApi/TrackQrApi/Features/QRCodes/Endpoints/PreviewQRCodeEndpoint.cs similarity index 74% rename from src/api/Features/QRCodes/Endpoints/PreviewQRCodeEndpoint.cs rename to src/TrackApi/TrackQrApi/Features/QRCodes/Endpoints/PreviewQRCodeEndpoint.cs index bb2ef42..78dca9a 100644 --- a/src/api/Features/QRCodes/Endpoints/PreviewQRCodeEndpoint.cs +++ b/src/TrackApi/TrackQrApi/Features/QRCodes/Endpoints/PreviewQRCodeEndpoint.cs @@ -1,14 +1,14 @@ using System.Security.Claims; using System.Text.Json; -using api.Data; -using api.Features.Assets.Services; -using api.Features.Auth.Common; -using api.Features.QRCodes.Common; -using api.Features.QRCodes.Services; using FastEndpoints; using Microsoft.EntityFrameworkCore; +using TrackQrApi.Data; +using TrackQrApi.Features.Assets.Services; +using TrackQrApi.Features.Auth.Common; +using TrackQrApi.Features.QRCodes.Common; +using TrackQrApi.Features.QRCodes.Services; -namespace api.Features.QRCodes.Endpoints; +namespace TrackQrApi.Features.QRCodes.Endpoints; public class PreviewQRCodeRequest { @@ -17,7 +17,10 @@ public class PreviewQRCodeRequest public int? Size { get; set; } } -public class PreviewQRCodeEndpoint(AppDbContext db, IQrCodeGeneratorService qrGenerator, IAssetStorageService assetStorage) +public class PreviewQRCodeEndpoint( + AppDbContext db, + IQrCodeGeneratorService qrGenerator, + IAssetStorageService assetStorage) : Endpoint { public override void Configure() @@ -43,7 +46,8 @@ public class PreviewQRCodeEndpoint(AppDbContext db, IQrCodeGeneratorService qrGe if (qrCode.ShortLink is null) { - await HttpContext.Response.SendAsync(new MessageResponse("QR code has no associated link"), 400, cancellation: ct); + await HttpContext.Response.SendAsync(new MessageResponse("QR code has no associated link"), 400, + cancellation: ct); return; } @@ -60,10 +64,7 @@ public class PreviewQRCodeEndpoint(AppDbContext db, IQrCodeGeneratorService qrGe if (qrCode.LogoAsset != null) { var logoResult = await assetStorage.GetAsync(qrCode.LogoAsset.StorageKey); - if (logoResult.HasValue) - { - logoStream = logoResult.Value.Stream; - } + if (logoResult.HasValue) logoStream = logoResult.Value.Stream; } try @@ -71,17 +72,17 @@ public class PreviewQRCodeEndpoint(AppDbContext db, IQrCodeGeneratorService qrGe var dataUrl = qrGenerator.GenerateDataUrl(linkUrl, style, size, logoStream); var response = new QRCodePreviewResponse( - DataUrl: dataUrl, - Format: "png", - Width: size, - Height: size + dataUrl, + "png", + size, + size ); - await HttpContext.Response.SendAsync(response, 200, cancellation: ct); + await HttpContext.Response.SendAsync(response, cancellation: ct); } finally { logoStream?.Dispose(); } } -} +} \ No newline at end of file diff --git a/src/api/Features/QRCodes/Endpoints/UpdateQRCodeEndpoint.cs b/src/TrackApi/TrackQrApi/Features/QRCodes/Endpoints/UpdateQRCodeEndpoint.cs similarity index 84% rename from src/api/Features/QRCodes/Endpoints/UpdateQRCodeEndpoint.cs rename to src/TrackApi/TrackQrApi/Features/QRCodes/Endpoints/UpdateQRCodeEndpoint.cs index 9419c6f..36679e2 100644 --- a/src/api/Features/QRCodes/Endpoints/UpdateQRCodeEndpoint.cs +++ b/src/TrackApi/TrackQrApi/Features/QRCodes/Endpoints/UpdateQRCodeEndpoint.cs @@ -1,12 +1,12 @@ using System.Security.Claims; using System.Text.Json; -using api.Data; -using api.Features.Auth.Common; -using api.Features.QRCodes.Common; using FastEndpoints; using Microsoft.EntityFrameworkCore; +using TrackQrApi.Data; +using TrackQrApi.Features.Auth.Common; +using TrackQrApi.Features.QRCodes.Common; -namespace api.Features.QRCodes.Endpoints; +namespace TrackQrApi.Features.QRCodes.Endpoints; public class UpdateQRCodeRequest { @@ -36,7 +36,8 @@ public class UpdateQRCodeEndpoint(AppDbContext db) .Include(q => q.Workspace) .Include(q => q.ShortLink) .Include(q => q.LogoAsset) - .FirstOrDefaultAsync(q => q.Id == req.Id && q.WorkspaceId == req.WorkspaceId && q.Workspace.OwnerUserId == userId, ct); + .FirstOrDefaultAsync( + q => q.Id == req.Id && q.WorkspaceId == req.WorkspaceId && q.Workspace.OwnerUserId == userId, ct); if (qrCode is null) { @@ -55,6 +56,7 @@ public class UpdateQRCodeEndpoint(AppDbContext db) await HttpContext.Response.SendAsync(new MessageResponse("Project not found"), 404, cancellation: ct); return; } + qrCode.ProjectId = req.ProjectId.Value; } else if (req.RemoveProject == true) @@ -63,10 +65,7 @@ public class UpdateQRCodeEndpoint(AppDbContext db) } // Update name if provided - if (!string.IsNullOrWhiteSpace(req.Name)) - { - qrCode.Name = req.Name; - } + if (!string.IsNullOrWhiteSpace(req.Name)) qrCode.Name = req.Name; // Handle logo asset update if (req.LogoAssetId.HasValue) @@ -76,9 +75,11 @@ public class UpdateQRCodeEndpoint(AppDbContext db) if (!assetExists) { - await HttpContext.Response.SendAsync(new MessageResponse("Logo asset not found"), 404, cancellation: ct); + await HttpContext.Response.SendAsync(new MessageResponse("Logo asset not found"), 404, + cancellation: ct); return; } + qrCode.LogoAssetId = req.LogoAssetId.Value; // Reload the asset for the response qrCode.LogoAsset = await db.Assets.FindAsync([req.LogoAssetId.Value], ct); @@ -89,10 +90,7 @@ public class UpdateQRCodeEndpoint(AppDbContext db) qrCode.LogoAsset = null; } - if (req.Style != null) - { - qrCode.StyleJson = JsonSerializer.Serialize(req.Style); - } + if (req.Style != null) qrCode.StyleJson = JsonSerializer.Serialize(req.Style); qrCode.UpdatedAt = DateTime.UtcNow; await db.SaveChangesAsync(ct); @@ -114,6 +112,6 @@ public class UpdateQRCodeEndpoint(AppDbContext db) qrCode.UpdatedAt ); - await HttpContext.Response.SendAsync(response, 200, cancellation: ct); + await HttpContext.Response.SendAsync(response, cancellation: ct); } -} +} \ No newline at end of file diff --git a/src/api/Features/QRCodes/Services/QRCodeGeneratorService.cs b/src/TrackApi/TrackQrApi/Features/QRCodes/Services/QRCodeGeneratorService.cs similarity index 67% rename from src/api/Features/QRCodes/Services/QRCodeGeneratorService.cs rename to src/TrackApi/TrackQrApi/Features/QRCodes/Services/QRCodeGeneratorService.cs index 206ce69..94258a2 100644 --- a/src/api/Features/QRCodes/Services/QRCodeGeneratorService.cs +++ b/src/TrackApi/TrackQrApi/Features/QRCodes/Services/QRCodeGeneratorService.cs @@ -1,8 +1,9 @@ -using api.Features.QRCodes.Common; +using System.Text; using QRCoder; using SkiaSharp; +using TrackQrApi.Features.QRCodes.Common; -namespace api.Features.QRCodes.Services; +namespace TrackQrApi.Features.QRCodes.Services; public interface IQrCodeGeneratorService { @@ -23,7 +24,7 @@ public class QrCodeGeneratorService : IQrCodeGeneratorService var moduleCount = moduleMatrix.Count; // Calculate pixels per module based on desired size (accounting for quiet zone) - var totalModules = moduleCount + (style.QuietZone * 2); + var totalModules = moduleCount + style.QuietZone * 2; var pixelsPerModule = Math.Max(4, size / totalModules); var actualSize = totalModules * pixelsPerModule; @@ -47,29 +48,21 @@ public class QrCodeGeneratorService : IQrCodeGeneratorService var quietZoneOffset = style.QuietZone * pixelsPerModule; - for (int y = 0; y < moduleCount; y++) - { - for (int x = 0; x < moduleCount; x++) + for (var y = 0; y < moduleCount; y++) + for (var x = 0; x < moduleCount; x++) + if (moduleMatrix[y][x]) { - if (moduleMatrix[y][x]) - { - var px = quietZoneOffset + (x * pixelsPerModule); - var py = quietZoneOffset + (y * pixelsPerModule); + var px = quietZoneOffset + x * pixelsPerModule; + var py = quietZoneOffset + y * pixelsPerModule; - // Check if this is part of a finder pattern (eyes) - var isEye = IsFinderPattern(x, y, moduleCount); + // Check if this is part of a finder pattern (eyes) + var isEye = IsFinderPattern(x, y, moduleCount); - if (isEye) - { - DrawModule(canvas, px, py, pixelsPerModule, modulePaint, style.EyeShape); - } - else - { - DrawModule(canvas, px, py, pixelsPerModule, modulePaint, style.ModuleShape); - } - } + if (isEye) + DrawModule(canvas, px, py, pixelsPerModule, modulePaint, style.EyeShape); + else + DrawModule(canvas, px, py, pixelsPerModule, modulePaint, style.ModuleShape); } - } // Encode to PNG using var image = surface.Snapshot(); @@ -77,15 +70,84 @@ public class QrCodeGeneratorService : IQrCodeGeneratorService var qrBytes = data.ToArray(); // If no logo, return the QR code as-is - if (logoStream == null) - { - return qrBytes; - } + if (logoStream == null) return qrBytes; // Overlay logo on QR code return OverlayLogo(qrBytes, logoStream, actualSize); } + public string GenerateSvg(string content, QRCodeStyle style, int size = 512) + { + using var qrGenerator = new QRCodeGenerator(); + var eccLevel = ParseEccLevel(style.ErrorCorrectionLevel); + using var qrCodeData = qrGenerator.CreateQrCode(content, eccLevel); + + var moduleMatrix = qrCodeData.ModuleMatrix; + var moduleCount = moduleMatrix.Count; + + // Calculate pixels per module based on desired size (accounting for quiet zone) + var totalModules = moduleCount + style.QuietZone * 2; + var pixelsPerModule = (float)size / totalModules; + var actualSize = size; + + var foreground = style.ForegroundColor; + var background = style.BackgroundColor; + + var svg = new StringBuilder(); + svg.AppendLine( + $""); + svg.AppendLine($" "); + + var quietZoneOffset = style.QuietZone * pixelsPerModule; + + for (var y = 0; y < moduleCount; y++) + for (var x = 0; x < moduleCount; x++) + if (moduleMatrix[y][x]) + { + var px = quietZoneOffset + x * pixelsPerModule; + var py = quietZoneOffset + y * pixelsPerModule; + var isEye = IsFinderPattern(x, y, moduleCount); + var shape = isEye ? style.EyeShape : style.ModuleShape; + + var padding = pixelsPerModule * 0.1f; + var moduleSize = pixelsPerModule - padding; + + switch (shape.ToLowerInvariant()) + { + case "circle": + case "dots": + var radius = moduleSize / 2; + var cx = px + pixelsPerModule / 2; + var cy = py + pixelsPerModule / 2; + svg.AppendLine( + $" "); + break; + + case "rounded": + var cornerRadius = moduleSize * 0.3f; + svg.AppendLine( + $" "); + break; + + case "square": + default: + svg.AppendLine( + $" "); + break; + } + } + + svg.AppendLine(""); + return svg.ToString(); + } + + public string GenerateDataUrl(string content, QRCodeStyle style, int size = 256, Stream? logoStream = null) + { + var pngBytes = GeneratePng(content, style, size, logoStream); + var base64 = Convert.ToBase64String(pngBytes); + return $"data:image/png;base64,{base64}"; + } + private static bool IsFinderPattern(int x, int y, int moduleCount) { // Top-left finder pattern: 0-6, 0-6 @@ -126,87 +188,12 @@ public class QrCodeGeneratorService : IQrCodeGeneratorService } } - public string GenerateSvg(string content, QRCodeStyle style, int size = 512) - { - using var qrGenerator = new QRCodeGenerator(); - var eccLevel = ParseEccLevel(style.ErrorCorrectionLevel); - using var qrCodeData = qrGenerator.CreateQrCode(content, eccLevel); - - var moduleMatrix = qrCodeData.ModuleMatrix; - var moduleCount = moduleMatrix.Count; - - // Calculate pixels per module based on desired size (accounting for quiet zone) - var totalModules = moduleCount + (style.QuietZone * 2); - var pixelsPerModule = (float)size / totalModules; - var actualSize = size; - - var foreground = style.ForegroundColor; - var background = style.BackgroundColor; - - var svg = new System.Text.StringBuilder(); - svg.AppendLine($""); - svg.AppendLine($" "); - - var quietZoneOffset = style.QuietZone * pixelsPerModule; - - for (int y = 0; y < moduleCount; y++) - { - for (int x = 0; x < moduleCount; x++) - { - if (moduleMatrix[y][x]) - { - var px = quietZoneOffset + (x * pixelsPerModule); - var py = quietZoneOffset + (y * pixelsPerModule); - var isEye = IsFinderPattern(x, y, moduleCount); - var shape = isEye ? style.EyeShape : style.ModuleShape; - - var padding = pixelsPerModule * 0.1f; - var moduleSize = pixelsPerModule - padding; - - switch (shape.ToLowerInvariant()) - { - case "circle": - case "dots": - var radius = moduleSize / 2; - var cx = px + pixelsPerModule / 2; - var cy = py + pixelsPerModule / 2; - svg.AppendLine($" "); - break; - - case "rounded": - var cornerRadius = moduleSize * 0.3f; - svg.AppendLine($" "); - break; - - case "square": - default: - svg.AppendLine($" "); - break; - } - } - } - } - - svg.AppendLine(""); - return svg.ToString(); - } - - public string GenerateDataUrl(string content, QRCodeStyle style, int size = 256, Stream? logoStream = null) - { - var pngBytes = GeneratePng(content, style, size, logoStream); - var base64 = Convert.ToBase64String(pngBytes); - return $"data:image/png;base64,{base64}"; - } - private static byte[] OverlayLogo(byte[] qrBytes, Stream logoStream, int qrSize) { using var qrBitmap = SKBitmap.Decode(qrBytes); using var logoBitmap = SKBitmap.Decode(logoStream); - if (qrBitmap == null || logoBitmap == null) - { - return qrBytes; - } + if (qrBitmap == null || logoBitmap == null) return qrBytes; // Logo should be about 20% of QR code size var logoSize = (int)(qrSize * 0.2); @@ -232,12 +219,9 @@ public class QrCodeGeneratorService : IQrCodeGeneratorService // Resize and draw logo using var resizedLogo = logoBitmap.Resize( - new SKImageInfo(logoSize, logoSize), + new SKImageInfo(logoSize, logoSize), new SKSamplingOptions(SKCubicResampler.Mitchell)); - if (resizedLogo != null) - { - canvas.DrawBitmap(resizedLogo, logoX, logoY); - } + if (resizedLogo != null) canvas.DrawBitmap(resizedLogo, logoX, logoY); // Encode to PNG using var image = surface.Snapshot(); @@ -274,4 +258,4 @@ public class QrCodeGeneratorService : IQrCodeGeneratorService // Default to black return SKColors.Black; } -} +} \ No newline at end of file diff --git a/src/api/Features/Redirect/Endpoints/PasswordRedirectEndpoint.cs b/src/TrackApi/TrackQrApi/Features/Redirect/Endpoints/PasswordRedirectEndpoint.cs similarity index 94% rename from src/api/Features/Redirect/Endpoints/PasswordRedirectEndpoint.cs rename to src/TrackApi/TrackQrApi/Features/Redirect/Endpoints/PasswordRedirectEndpoint.cs index c2075e2..52ed464 100644 --- a/src/api/Features/Redirect/Endpoints/PasswordRedirectEndpoint.cs +++ b/src/TrackApi/TrackQrApi/Features/Redirect/Endpoints/PasswordRedirectEndpoint.cs @@ -1,12 +1,12 @@ -using api.Data; -using api.Features.Auth.Common; -using api.Features.Events.Services; -using api.Models; using FastEndpoints; using FluentValidation; using Microsoft.EntityFrameworkCore; +using TrackQrApi.Data; +using TrackQrApi.Features.Auth.Common; +using TrackQrApi.Features.Events.Services; +using TrackQrApi.Models; -namespace api.Features.Redirect.Endpoints; +namespace TrackQrApi.Features.Redirect.Endpoints; public class PasswordRedirectRequest { @@ -101,4 +101,4 @@ public class PasswordRedirectEndpoint(AppDbContext db, IEventTrackingService eve HttpContext.Response.Headers.Location = link.DestinationUrl; await HttpContext.Response.StartAsync(ct); } -} +} \ No newline at end of file diff --git a/src/api/Features/Redirect/Endpoints/RedirectEndpoint.cs b/src/TrackApi/TrackQrApi/Features/Redirect/Endpoints/RedirectEndpoint.cs similarity index 93% rename from src/api/Features/Redirect/Endpoints/RedirectEndpoint.cs rename to src/TrackApi/TrackQrApi/Features/Redirect/Endpoints/RedirectEndpoint.cs index bae3f2a..e67cdf9 100644 --- a/src/api/Features/Redirect/Endpoints/RedirectEndpoint.cs +++ b/src/TrackApi/TrackQrApi/Features/Redirect/Endpoints/RedirectEndpoint.cs @@ -1,11 +1,11 @@ -using api.Data; -using api.Features.Auth.Common; -using api.Features.Events.Services; -using api.Models; using FastEndpoints; using Microsoft.EntityFrameworkCore; +using TrackQrApi.Data; +using TrackQrApi.Features.Auth.Common; +using TrackQrApi.Features.Events.Services; +using TrackQrApi.Models; -namespace api.Features.Redirect.Endpoints; +namespace TrackQrApi.Features.Redirect.Endpoints; public class RedirectRequest { @@ -78,17 +78,13 @@ public class RedirectEndpoint(AppDbContext db, IEventTrackingService eventTracki // Track event asynchronously (fire and forget) // If qr parameter is present, track as scan; otherwise track as click if (req.Qr.HasValue) - { await eventTracking.TrackScanAsync(link.WorkspaceId, link.Id, req.Qr.Value, HttpContext); - } else - { await eventTracking.TrackClickAsync(link.WorkspaceId, link.Id, HttpContext); - } // Redirect to destination (302 Found) HttpContext.Response.StatusCode = StatusCodes.Status302Found; HttpContext.Response.Headers.Location = link.DestinationUrl; await HttpContext.Response.StartAsync(ct); } -} +} \ No newline at end of file diff --git a/src/api/Features/Workspaces/Common/WorkspaceResponses.cs b/src/TrackApi/TrackQrApi/Features/Workspaces/Common/WorkspaceResponses.cs similarity index 78% rename from src/api/Features/Workspaces/Common/WorkspaceResponses.cs rename to src/TrackApi/TrackQrApi/Features/Workspaces/Common/WorkspaceResponses.cs index d6eb4b2..569ef66 100644 --- a/src/api/Features/Workspaces/Common/WorkspaceResponses.cs +++ b/src/TrackApi/TrackQrApi/Features/Workspaces/Common/WorkspaceResponses.cs @@ -1,4 +1,4 @@ -namespace api.Features.Workspaces.Common; +namespace TrackQrApi.Features.Workspaces.Common; public record WorkspaceResponse( Guid Id, @@ -9,4 +9,4 @@ public record WorkspaceResponse( public record WorkspaceListResponse( IEnumerable Workspaces -); +); \ No newline at end of file diff --git a/src/api/Features/Workspaces/Endpoints/CreateWorkspaceEndpoint.cs b/src/TrackApi/TrackQrApi/Features/Workspaces/Endpoints/CreateWorkspaceEndpoint.cs similarity index 88% rename from src/api/Features/Workspaces/Endpoints/CreateWorkspaceEndpoint.cs rename to src/TrackApi/TrackQrApi/Features/Workspaces/Endpoints/CreateWorkspaceEndpoint.cs index 3bea1df..4b7ee03 100644 --- a/src/api/Features/Workspaces/Endpoints/CreateWorkspaceEndpoint.cs +++ b/src/TrackApi/TrackQrApi/Features/Workspaces/Endpoints/CreateWorkspaceEndpoint.cs @@ -1,13 +1,13 @@ using System.Security.Claims; -using api.Data; -using api.Features.Auth.Common; -using api.Features.Plans.Services; -using api.Features.Workspaces.Common; -using api.Models; using FastEndpoints; using FluentValidation; +using TrackQrApi.Data; +using TrackQrApi.Features.Auth.Common; +using TrackQrApi.Features.Plans.Services; +using TrackQrApi.Features.Workspaces.Common; +using TrackQrApi.Models; -namespace api.Features.Workspaces.Endpoints; +namespace TrackQrApi.Features.Workspaces.Endpoints; public class CreateWorkspaceRequest { @@ -67,4 +67,4 @@ public class CreateWorkspaceEndpoint(AppDbContext db, IPlanLimitsService planLim await HttpContext.Response.SendAsync(response, 201, cancellation: ct); } -} +} \ No newline at end of file diff --git a/src/api/Features/Workspaces/Endpoints/DeleteWorkspaceEndpoint.cs b/src/TrackApi/TrackQrApi/Features/Workspaces/Endpoints/DeleteWorkspaceEndpoint.cs similarity index 86% rename from src/api/Features/Workspaces/Endpoints/DeleteWorkspaceEndpoint.cs rename to src/TrackApi/TrackQrApi/Features/Workspaces/Endpoints/DeleteWorkspaceEndpoint.cs index 733bb0c..bec49f3 100644 --- a/src/api/Features/Workspaces/Endpoints/DeleteWorkspaceEndpoint.cs +++ b/src/TrackApi/TrackQrApi/Features/Workspaces/Endpoints/DeleteWorkspaceEndpoint.cs @@ -1,10 +1,10 @@ using System.Security.Claims; -using api.Data; -using api.Features.Auth.Common; using FastEndpoints; using Microsoft.EntityFrameworkCore; +using TrackQrApi.Data; +using TrackQrApi.Features.Auth.Common; -namespace api.Features.Workspaces.Endpoints; +namespace TrackQrApi.Features.Workspaces.Endpoints; public class DeleteWorkspaceRequest { @@ -35,6 +35,6 @@ public class DeleteWorkspaceEndpoint(AppDbContext db) db.Workspaces.Remove(workspace); await db.SaveChangesAsync(ct); - await HttpContext.Response.SendAsync(new MessageResponse("Workspace deleted"), 200, cancellation: ct); + await HttpContext.Response.SendAsync(new MessageResponse("Workspace deleted"), cancellation: ct); } -} +} \ No newline at end of file diff --git a/src/api/Features/Workspaces/Endpoints/GetWorkspaceEndpoint.cs b/src/TrackApi/TrackQrApi/Features/Workspaces/Endpoints/GetWorkspaceEndpoint.cs similarity index 81% rename from src/api/Features/Workspaces/Endpoints/GetWorkspaceEndpoint.cs rename to src/TrackApi/TrackQrApi/Features/Workspaces/Endpoints/GetWorkspaceEndpoint.cs index 578e0aa..75e7d91 100644 --- a/src/api/Features/Workspaces/Endpoints/GetWorkspaceEndpoint.cs +++ b/src/TrackApi/TrackQrApi/Features/Workspaces/Endpoints/GetWorkspaceEndpoint.cs @@ -1,11 +1,11 @@ using System.Security.Claims; -using api.Data; -using api.Features.Auth.Common; -using api.Features.Workspaces.Common; using FastEndpoints; using Microsoft.EntityFrameworkCore; +using TrackQrApi.Data; +using TrackQrApi.Features.Auth.Common; +using TrackQrApi.Features.Workspaces.Common; -namespace api.Features.Workspaces.Endpoints; +namespace TrackQrApi.Features.Workspaces.Endpoints; public class GetWorkspaceRequest { @@ -40,6 +40,6 @@ public class GetWorkspaceEndpoint(AppDbContext db) return; } - await HttpContext.Response.SendAsync(workspace, 200, cancellation: ct); + await HttpContext.Response.SendAsync(workspace, cancellation: ct); } -} +} \ No newline at end of file diff --git a/src/api/Features/Workspaces/Endpoints/ListWorkspacesEndpoint.cs b/src/TrackApi/TrackQrApi/Features/Workspaces/Endpoints/ListWorkspacesEndpoint.cs similarity index 84% rename from src/api/Features/Workspaces/Endpoints/ListWorkspacesEndpoint.cs rename to src/TrackApi/TrackQrApi/Features/Workspaces/Endpoints/ListWorkspacesEndpoint.cs index 9e2dcae..314e5f9 100644 --- a/src/api/Features/Workspaces/Endpoints/ListWorkspacesEndpoint.cs +++ b/src/TrackApi/TrackQrApi/Features/Workspaces/Endpoints/ListWorkspacesEndpoint.cs @@ -1,10 +1,10 @@ using System.Security.Claims; -using api.Data; -using api.Features.Workspaces.Common; using FastEndpoints; using Microsoft.EntityFrameworkCore; +using TrackQrApi.Data; +using TrackQrApi.Features.Workspaces.Common; -namespace api.Features.Workspaces.Endpoints; +namespace TrackQrApi.Features.Workspaces.Endpoints; public class ListWorkspacesEndpoint(AppDbContext db) : EndpointWithoutRequest @@ -29,6 +29,6 @@ public class ListWorkspacesEndpoint(AppDbContext db) )) .ToListAsync(ct); - await HttpContext.Response.SendAsync(new WorkspaceListResponse(workspaces), 200, cancellation: ct); + await HttpContext.Response.SendAsync(new WorkspaceListResponse(workspaces), cancellation: ct); } -} +} \ No newline at end of file diff --git a/src/api/Features/Workspaces/Endpoints/UpdateWorkspaceEndpoint.cs b/src/TrackApi/TrackQrApi/Features/Workspaces/Endpoints/UpdateWorkspaceEndpoint.cs similarity index 86% rename from src/api/Features/Workspaces/Endpoints/UpdateWorkspaceEndpoint.cs rename to src/TrackApi/TrackQrApi/Features/Workspaces/Endpoints/UpdateWorkspaceEndpoint.cs index 7c99c32..e21415b 100644 --- a/src/api/Features/Workspaces/Endpoints/UpdateWorkspaceEndpoint.cs +++ b/src/TrackApi/TrackQrApi/Features/Workspaces/Endpoints/UpdateWorkspaceEndpoint.cs @@ -1,12 +1,12 @@ using System.Security.Claims; -using api.Data; -using api.Features.Auth.Common; -using api.Features.Workspaces.Common; using FastEndpoints; using FluentValidation; using Microsoft.EntityFrameworkCore; +using TrackQrApi.Data; +using TrackQrApi.Features.Auth.Common; +using TrackQrApi.Features.Workspaces.Common; -namespace api.Features.Workspaces.Endpoints; +namespace TrackQrApi.Features.Workspaces.Endpoints; public class UpdateWorkspaceRequest { @@ -55,6 +55,6 @@ public class UpdateWorkspaceEndpoint(AppDbContext db) workspace.CreatedAt ); - await HttpContext.Response.SendAsync(response, 200, cancellation: ct); + await HttpContext.Response.SendAsync(response, cancellation: ct); } -} +} \ No newline at end of file diff --git a/src/api/Middleware/GlobalExceptionMiddleware.cs b/src/TrackApi/TrackQrApi/Middleware/GlobalExceptionMiddleware.cs similarity index 89% rename from src/api/Middleware/GlobalExceptionMiddleware.cs rename to src/TrackApi/TrackQrApi/Middleware/GlobalExceptionMiddleware.cs index b80285d..056710b 100644 --- a/src/api/Middleware/GlobalExceptionMiddleware.cs +++ b/src/TrackApi/TrackQrApi/Middleware/GlobalExceptionMiddleware.cs @@ -1,7 +1,7 @@ using System.Net; using System.Text.Json; -namespace api.Middleware; +namespace TrackQrApi.Middleware; public class GlobalExceptionMiddleware(RequestDelegate next, ILogger logger) { @@ -30,21 +30,17 @@ public class GlobalExceptionMiddleware(RequestDelegate next, ILogger using System; -using api.Data; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using TrackQrApi.Data; #nullable disable -namespace api.Migrations +namespace TrackQrApi.Migrations { [DbContext(typeof(AppDbContext))] [Migration("20260127192536_InitialCreate")] diff --git a/src/api/Migrations/20260127192536_InitialCreate.cs b/src/TrackApi/TrackQrApi/Migrations/20260127192536_InitialCreate.cs similarity index 99% rename from src/api/Migrations/20260127192536_InitialCreate.cs rename to src/TrackApi/TrackQrApi/Migrations/20260127192536_InitialCreate.cs index 0578fb6..79bb546 100644 --- a/src/api/Migrations/20260127192536_InitialCreate.cs +++ b/src/TrackApi/TrackQrApi/Migrations/20260127192536_InitialCreate.cs @@ -3,7 +3,7 @@ using Microsoft.EntityFrameworkCore.Migrations; #nullable disable -namespace api.Migrations +namespace TrackQrApi.Migrations { /// public partial class InitialCreate : Migration diff --git a/src/api/Migrations/20260127193159_AddShortLinksQRCodesEventsAssets.Designer.cs b/src/TrackApi/TrackQrApi/Migrations/20260127193159_AddShortLinksQRCodesEventsAssets.Designer.cs similarity index 99% rename from src/api/Migrations/20260127193159_AddShortLinksQRCodesEventsAssets.Designer.cs rename to src/TrackApi/TrackQrApi/Migrations/20260127193159_AddShortLinksQRCodesEventsAssets.Designer.cs index e5523a3..ce43467 100644 --- a/src/api/Migrations/20260127193159_AddShortLinksQRCodesEventsAssets.Designer.cs +++ b/src/TrackApi/TrackQrApi/Migrations/20260127193159_AddShortLinksQRCodesEventsAssets.Designer.cs @@ -1,15 +1,15 @@ // using System; -using api.Data; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using TrackQrApi.Data; #nullable disable -namespace api.Migrations +namespace TrackQrApi.Migrations { [DbContext(typeof(AppDbContext))] [Migration("20260127193159_AddShortLinksQRCodesEventsAssets")] diff --git a/src/api/Migrations/20260127193159_AddShortLinksQRCodesEventsAssets.cs b/src/TrackApi/TrackQrApi/Migrations/20260127193159_AddShortLinksQRCodesEventsAssets.cs similarity index 99% rename from src/api/Migrations/20260127193159_AddShortLinksQRCodesEventsAssets.cs rename to src/TrackApi/TrackQrApi/Migrations/20260127193159_AddShortLinksQRCodesEventsAssets.cs index 0009643..340d30c 100644 --- a/src/api/Migrations/20260127193159_AddShortLinksQRCodesEventsAssets.cs +++ b/src/TrackApi/TrackQrApi/Migrations/20260127193159_AddShortLinksQRCodesEventsAssets.cs @@ -4,7 +4,7 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; #nullable disable -namespace api.Migrations +namespace TrackQrApi.Migrations { /// public partial class AddShortLinksQRCodesEventsAssets : Migration diff --git a/src/api/Migrations/20260127205418_RefactorAuth.Designer.cs b/src/TrackApi/TrackQrApi/Migrations/20260127205418_RefactorAuth.Designer.cs similarity index 87% rename from src/api/Migrations/20260127205418_RefactorAuth.Designer.cs rename to src/TrackApi/TrackQrApi/Migrations/20260127205418_RefactorAuth.Designer.cs index f5f2651..5621cb0 100644 --- a/src/api/Migrations/20260127205418_RefactorAuth.Designer.cs +++ b/src/TrackApi/TrackQrApi/Migrations/20260127205418_RefactorAuth.Designer.cs @@ -5,11 +5,11 @@ using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; -using api.Data; +using TrackQrApi.Data; #nullable disable -namespace api.Migrations +namespace TrackQrApi.Migrations { [DbContext(typeof(AppDbContext))] [Migration("20260127205418_refactor auth")] @@ -25,7 +25,7 @@ namespace api.Migrations NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); - modelBuilder.Entity("api.Models.Asset", b => + modelBuilder.Entity("TrackQrApi.Models.Asset", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -64,7 +64,7 @@ namespace api.Migrations b.ToTable("Assets"); }); - modelBuilder.Entity("api.Models.Domain", b => + modelBuilder.Entity("TrackQrApi.Models.Domain", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -103,7 +103,7 @@ namespace api.Migrations b.ToTable("Domains"); }); - modelBuilder.Entity("api.Models.Event", b => + modelBuilder.Entity("TrackQrApi.Models.Event", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -167,7 +167,7 @@ namespace api.Migrations b.ToTable("Events"); }); - modelBuilder.Entity("api.Models.Project", b => + modelBuilder.Entity("TrackQrApi.Models.Project", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -193,7 +193,7 @@ namespace api.Migrations b.ToTable("Projects"); }); - modelBuilder.Entity("api.Models.QRCodeDesign", b => + modelBuilder.Entity("TrackQrApi.Models.QRCodeDesign", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -238,7 +238,7 @@ namespace api.Migrations b.ToTable("QrCodeDesigns"); }); - modelBuilder.Entity("api.Models.ShortLink", b => + modelBuilder.Entity("TrackQrApi.Models.ShortLink", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -301,7 +301,7 @@ namespace api.Migrations b.ToTable("ShortLinks"); }); - modelBuilder.Entity("api.Models.User", b => + modelBuilder.Entity("TrackQrApi.Models.User", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -333,7 +333,7 @@ namespace api.Migrations b.ToTable("Users"); }); - modelBuilder.Entity("api.Models.Workspace", b => + modelBuilder.Entity("TrackQrApi.Models.Workspace", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -364,9 +364,9 @@ namespace api.Migrations b.ToTable("Workspaces"); }); - modelBuilder.Entity("api.Models.Asset", b => + modelBuilder.Entity("TrackQrApi.Models.Asset", b => { - b.HasOne("api.Models.Workspace", "Workspace") + b.HasOne("TrackQrApi.Models.Workspace", "Workspace") .WithMany("Assets") .HasForeignKey("WorkspaceId") .OnDelete(DeleteBehavior.Cascade) @@ -375,9 +375,9 @@ namespace api.Migrations b.Navigation("Workspace"); }); - modelBuilder.Entity("api.Models.Domain", b => + modelBuilder.Entity("TrackQrApi.Models.Domain", b => { - b.HasOne("api.Models.Workspace", "Workspace") + b.HasOne("TrackQrApi.Models.Workspace", "Workspace") .WithMany("Domains") .HasForeignKey("WorkspaceId") .OnDelete(DeleteBehavior.Cascade) @@ -386,20 +386,20 @@ namespace api.Migrations b.Navigation("Workspace"); }); - modelBuilder.Entity("api.Models.Event", b => + modelBuilder.Entity("TrackQrApi.Models.Event", b => { - b.HasOne("api.Models.QRCodeDesign", "QRCode") + b.HasOne("TrackQrApi.Models.QRCodeDesign", "QRCode") .WithMany("Events") .HasForeignKey("QRCodeId") .OnDelete(DeleteBehavior.SetNull); - b.HasOne("api.Models.ShortLink", "ShortLink") + b.HasOne("TrackQrApi.Models.ShortLink", "ShortLink") .WithMany("Events") .HasForeignKey("ShortLinkId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); - b.HasOne("api.Models.Workspace", "Workspace") + b.HasOne("TrackQrApi.Models.Workspace", "Workspace") .WithMany("Events") .HasForeignKey("WorkspaceId") .OnDelete(DeleteBehavior.Cascade) @@ -412,9 +412,9 @@ namespace api.Migrations b.Navigation("Workspace"); }); - modelBuilder.Entity("api.Models.Project", b => + modelBuilder.Entity("TrackQrApi.Models.Project", b => { - b.HasOne("api.Models.Workspace", "Workspace") + b.HasOne("TrackQrApi.Models.Workspace", "Workspace") .WithMany("Projects") .HasForeignKey("WorkspaceId") .OnDelete(DeleteBehavior.Cascade) @@ -423,24 +423,24 @@ namespace api.Migrations b.Navigation("Workspace"); }); - modelBuilder.Entity("api.Models.QRCodeDesign", b => + modelBuilder.Entity("TrackQrApi.Models.QRCodeDesign", b => { - b.HasOne("api.Models.Asset", "LogoAsset") + b.HasOne("TrackQrApi.Models.Asset", "LogoAsset") .WithMany() .HasForeignKey("LogoAssetId") .OnDelete(DeleteBehavior.SetNull); - b.HasOne("api.Models.Project", "Project") + b.HasOne("TrackQrApi.Models.Project", "Project") .WithMany("QRCodeDesigns") .HasForeignKey("ProjectId") .OnDelete(DeleteBehavior.SetNull); - b.HasOne("api.Models.ShortLink", "ShortLink") + b.HasOne("TrackQrApi.Models.ShortLink", "ShortLink") .WithMany("QRCodeDesigns") .HasForeignKey("ShortLinkId") .OnDelete(DeleteBehavior.SetNull); - b.HasOne("api.Models.Workspace", "Workspace") + b.HasOne("TrackQrApi.Models.Workspace", "Workspace") .WithMany("QRCodeDesigns") .HasForeignKey("WorkspaceId") .OnDelete(DeleteBehavior.Cascade) @@ -455,19 +455,19 @@ namespace api.Migrations b.Navigation("Workspace"); }); - modelBuilder.Entity("api.Models.ShortLink", b => + modelBuilder.Entity("TrackQrApi.Models.ShortLink", b => { - b.HasOne("api.Models.Domain", "Domain") + b.HasOne("TrackQrApi.Models.Domain", "Domain") .WithMany("ShortLinks") .HasForeignKey("DomainId") .OnDelete(DeleteBehavior.SetNull); - b.HasOne("api.Models.Project", "Project") + b.HasOne("TrackQrApi.Models.Project", "Project") .WithMany("ShortLinks") .HasForeignKey("ProjectId") .OnDelete(DeleteBehavior.SetNull); - b.HasOne("api.Models.Workspace", "Workspace") + b.HasOne("TrackQrApi.Models.Workspace", "Workspace") .WithMany("ShortLinks") .HasForeignKey("WorkspaceId") .OnDelete(DeleteBehavior.Cascade) @@ -480,9 +480,9 @@ namespace api.Migrations b.Navigation("Workspace"); }); - modelBuilder.Entity("api.Models.Workspace", b => + modelBuilder.Entity("TrackQrApi.Models.Workspace", b => { - b.HasOne("api.Models.User", "Owner") + b.HasOne("TrackQrApi.Models.User", "Owner") .WithMany("Workspaces") .HasForeignKey("OwnerUserId") .OnDelete(DeleteBehavior.Cascade) @@ -491,36 +491,36 @@ namespace api.Migrations b.Navigation("Owner"); }); - modelBuilder.Entity("api.Models.Domain", b => + modelBuilder.Entity("TrackQrApi.Models.Domain", b => { b.Navigation("ShortLinks"); }); - modelBuilder.Entity("api.Models.Project", b => + modelBuilder.Entity("TrackQrApi.Models.Project", b => { b.Navigation("QRCodeDesigns"); b.Navigation("ShortLinks"); }); - modelBuilder.Entity("api.Models.QRCodeDesign", b => + modelBuilder.Entity("TrackQrApi.Models.QRCodeDesign", b => { b.Navigation("Events"); }); - modelBuilder.Entity("api.Models.ShortLink", b => + modelBuilder.Entity("TrackQrApi.Models.ShortLink", b => { b.Navigation("Events"); b.Navigation("QRCodeDesigns"); }); - modelBuilder.Entity("api.Models.User", b => + modelBuilder.Entity("TrackQrApi.Models.User", b => { b.Navigation("Workspaces"); }); - modelBuilder.Entity("api.Models.Workspace", b => + modelBuilder.Entity("TrackQrApi.Models.Workspace", b => { b.Navigation("Assets"); diff --git a/src/api/Migrations/20260127205418_RefactorAuth.cs b/src/TrackApi/TrackQrApi/Migrations/20260127205418_RefactorAuth.cs similarity index 99% rename from src/api/Migrations/20260127205418_RefactorAuth.cs rename to src/TrackApi/TrackQrApi/Migrations/20260127205418_RefactorAuth.cs index 6d8d6c7..1a73746 100644 --- a/src/api/Migrations/20260127205418_RefactorAuth.cs +++ b/src/TrackApi/TrackQrApi/Migrations/20260127205418_RefactorAuth.cs @@ -2,7 +2,7 @@ #nullable disable -namespace api.Migrations +namespace TrackQrApi.Migrations { /// public partial class RefactorAuth : Migration diff --git a/src/api/Migrations/20260130185641_AddQRCodeNameAndLogo.Designer.cs b/src/TrackApi/TrackQrApi/Migrations/20260130185641_AddQRCodeNameAndLogo.Designer.cs similarity index 87% rename from src/api/Migrations/20260130185641_AddQRCodeNameAndLogo.Designer.cs rename to src/TrackApi/TrackQrApi/Migrations/20260130185641_AddQRCodeNameAndLogo.Designer.cs index 1a4f753..0973fd7 100644 --- a/src/api/Migrations/20260130185641_AddQRCodeNameAndLogo.Designer.cs +++ b/src/TrackApi/TrackQrApi/Migrations/20260130185641_AddQRCodeNameAndLogo.Designer.cs @@ -6,11 +6,11 @@ using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; -using api.Data; +using TrackQrApi.Data; #nullable disable -namespace api.Migrations +namespace TrackQrApi.Migrations { [DbContext(typeof(AppDbContext))] [Migration("20260130185641_AddQRCodeNameAndLogo")] @@ -26,7 +26,7 @@ namespace api.Migrations NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); - modelBuilder.Entity("api.Models.ApiKey", b => + modelBuilder.Entity("TrackQrApi.Models.ApiKey", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -77,7 +77,7 @@ namespace api.Migrations b.ToTable("ApiKeys"); }); - modelBuilder.Entity("api.Models.Asset", b => + modelBuilder.Entity("TrackQrApi.Models.Asset", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -116,7 +116,7 @@ namespace api.Migrations b.ToTable("Assets"); }); - modelBuilder.Entity("api.Models.Domain", b => + modelBuilder.Entity("TrackQrApi.Models.Domain", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -155,7 +155,7 @@ namespace api.Migrations b.ToTable("Domains"); }); - modelBuilder.Entity("api.Models.EmailVerificationToken", b => + modelBuilder.Entity("TrackQrApi.Models.EmailVerificationToken", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -187,7 +187,7 @@ namespace api.Migrations b.ToTable("EmailVerificationTokens"); }); - modelBuilder.Entity("api.Models.Event", b => + modelBuilder.Entity("TrackQrApi.Models.Event", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -251,7 +251,7 @@ namespace api.Migrations b.ToTable("Events"); }); - modelBuilder.Entity("api.Models.PasswordResetToken", b => + modelBuilder.Entity("TrackQrApi.Models.PasswordResetToken", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -286,7 +286,7 @@ namespace api.Migrations b.ToTable("PasswordResetTokens"); }); - modelBuilder.Entity("api.Models.Project", b => + modelBuilder.Entity("TrackQrApi.Models.Project", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -312,7 +312,7 @@ namespace api.Migrations b.ToTable("Projects"); }); - modelBuilder.Entity("api.Models.QRCodeDesign", b => + modelBuilder.Entity("TrackQrApi.Models.QRCodeDesign", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -361,7 +361,7 @@ namespace api.Migrations b.ToTable("QrCodeDesigns"); }); - modelBuilder.Entity("api.Models.ShortLink", b => + modelBuilder.Entity("TrackQrApi.Models.ShortLink", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -427,7 +427,7 @@ namespace api.Migrations b.ToTable("ShortLinks"); }); - modelBuilder.Entity("api.Models.User", b => + modelBuilder.Entity("TrackQrApi.Models.User", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -462,7 +462,7 @@ namespace api.Migrations b.ToTable("Users"); }); - modelBuilder.Entity("api.Models.Workspace", b => + modelBuilder.Entity("TrackQrApi.Models.Workspace", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -499,9 +499,9 @@ namespace api.Migrations b.ToTable("Workspaces"); }); - modelBuilder.Entity("api.Models.ApiKey", b => + modelBuilder.Entity("TrackQrApi.Models.ApiKey", b => { - b.HasOne("api.Models.Workspace", "Workspace") + b.HasOne("TrackQrApi.Models.Workspace", "Workspace") .WithMany() .HasForeignKey("WorkspaceId") .OnDelete(DeleteBehavior.Cascade) @@ -510,9 +510,9 @@ namespace api.Migrations b.Navigation("Workspace"); }); - modelBuilder.Entity("api.Models.Asset", b => + modelBuilder.Entity("TrackQrApi.Models.Asset", b => { - b.HasOne("api.Models.Workspace", "Workspace") + b.HasOne("TrackQrApi.Models.Workspace", "Workspace") .WithMany("Assets") .HasForeignKey("WorkspaceId") .OnDelete(DeleteBehavior.Cascade) @@ -521,9 +521,9 @@ namespace api.Migrations b.Navigation("Workspace"); }); - modelBuilder.Entity("api.Models.Domain", b => + modelBuilder.Entity("TrackQrApi.Models.Domain", b => { - b.HasOne("api.Models.Workspace", "Workspace") + b.HasOne("TrackQrApi.Models.Workspace", "Workspace") .WithMany("Domains") .HasForeignKey("WorkspaceId") .OnDelete(DeleteBehavior.Cascade) @@ -532,9 +532,9 @@ namespace api.Migrations b.Navigation("Workspace"); }); - modelBuilder.Entity("api.Models.EmailVerificationToken", b => + modelBuilder.Entity("TrackQrApi.Models.EmailVerificationToken", b => { - b.HasOne("api.Models.User", "User") + b.HasOne("TrackQrApi.Models.User", "User") .WithMany() .HasForeignKey("UserId") .OnDelete(DeleteBehavior.Cascade) @@ -543,20 +543,20 @@ namespace api.Migrations b.Navigation("User"); }); - modelBuilder.Entity("api.Models.Event", b => + modelBuilder.Entity("TrackQrApi.Models.Event", b => { - b.HasOne("api.Models.QRCodeDesign", "QRCode") + b.HasOne("TrackQrApi.Models.QRCodeDesign", "QRCode") .WithMany("Events") .HasForeignKey("QRCodeId") .OnDelete(DeleteBehavior.SetNull); - b.HasOne("api.Models.ShortLink", "ShortLink") + b.HasOne("TrackQrApi.Models.ShortLink", "ShortLink") .WithMany("Events") .HasForeignKey("ShortLinkId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); - b.HasOne("api.Models.Workspace", "Workspace") + b.HasOne("TrackQrApi.Models.Workspace", "Workspace") .WithMany("Events") .HasForeignKey("WorkspaceId") .OnDelete(DeleteBehavior.Cascade) @@ -569,9 +569,9 @@ namespace api.Migrations b.Navigation("Workspace"); }); - modelBuilder.Entity("api.Models.PasswordResetToken", b => + modelBuilder.Entity("TrackQrApi.Models.PasswordResetToken", b => { - b.HasOne("api.Models.User", "User") + b.HasOne("TrackQrApi.Models.User", "User") .WithMany() .HasForeignKey("UserId") .OnDelete(DeleteBehavior.Cascade) @@ -580,9 +580,9 @@ namespace api.Migrations b.Navigation("User"); }); - modelBuilder.Entity("api.Models.Project", b => + modelBuilder.Entity("TrackQrApi.Models.Project", b => { - b.HasOne("api.Models.Workspace", "Workspace") + b.HasOne("TrackQrApi.Models.Workspace", "Workspace") .WithMany("Projects") .HasForeignKey("WorkspaceId") .OnDelete(DeleteBehavior.Cascade) @@ -591,24 +591,24 @@ namespace api.Migrations b.Navigation("Workspace"); }); - modelBuilder.Entity("api.Models.QRCodeDesign", b => + modelBuilder.Entity("TrackQrApi.Models.QRCodeDesign", b => { - b.HasOne("api.Models.Asset", "LogoAsset") + b.HasOne("TrackQrApi.Models.Asset", "LogoAsset") .WithMany() .HasForeignKey("LogoAssetId") .OnDelete(DeleteBehavior.SetNull); - b.HasOne("api.Models.Project", "Project") + b.HasOne("TrackQrApi.Models.Project", "Project") .WithMany("QRCodeDesigns") .HasForeignKey("ProjectId") .OnDelete(DeleteBehavior.SetNull); - b.HasOne("api.Models.ShortLink", "ShortLink") + b.HasOne("TrackQrApi.Models.ShortLink", "ShortLink") .WithMany("QRCodeDesigns") .HasForeignKey("ShortLinkId") .OnDelete(DeleteBehavior.SetNull); - b.HasOne("api.Models.Workspace", "Workspace") + b.HasOne("TrackQrApi.Models.Workspace", "Workspace") .WithMany("QRCodeDesigns") .HasForeignKey("WorkspaceId") .OnDelete(DeleteBehavior.Cascade) @@ -623,19 +623,19 @@ namespace api.Migrations b.Navigation("Workspace"); }); - modelBuilder.Entity("api.Models.ShortLink", b => + modelBuilder.Entity("TrackQrApi.Models.ShortLink", b => { - b.HasOne("api.Models.Domain", "Domain") + b.HasOne("TrackQrApi.Models.Domain", "Domain") .WithMany("ShortLinks") .HasForeignKey("DomainId") .OnDelete(DeleteBehavior.SetNull); - b.HasOne("api.Models.Project", "Project") + b.HasOne("TrackQrApi.Models.Project", "Project") .WithMany("ShortLinks") .HasForeignKey("ProjectId") .OnDelete(DeleteBehavior.SetNull); - b.HasOne("api.Models.Workspace", "Workspace") + b.HasOne("TrackQrApi.Models.Workspace", "Workspace") .WithMany("ShortLinks") .HasForeignKey("WorkspaceId") .OnDelete(DeleteBehavior.Cascade) @@ -648,9 +648,9 @@ namespace api.Migrations b.Navigation("Workspace"); }); - modelBuilder.Entity("api.Models.Workspace", b => + modelBuilder.Entity("TrackQrApi.Models.Workspace", b => { - b.HasOne("api.Models.User", "Owner") + b.HasOne("TrackQrApi.Models.User", "Owner") .WithMany("Workspaces") .HasForeignKey("OwnerUserId") .OnDelete(DeleteBehavior.Cascade) @@ -659,36 +659,36 @@ namespace api.Migrations b.Navigation("Owner"); }); - modelBuilder.Entity("api.Models.Domain", b => + modelBuilder.Entity("TrackQrApi.Models.Domain", b => { b.Navigation("ShortLinks"); }); - modelBuilder.Entity("api.Models.Project", b => + modelBuilder.Entity("TrackQrApi.Models.Project", b => { b.Navigation("QRCodeDesigns"); b.Navigation("ShortLinks"); }); - modelBuilder.Entity("api.Models.QRCodeDesign", b => + modelBuilder.Entity("TrackQrApi.Models.QRCodeDesign", b => { b.Navigation("Events"); }); - modelBuilder.Entity("api.Models.ShortLink", b => + modelBuilder.Entity("TrackQrApi.Models.ShortLink", b => { b.Navigation("Events"); b.Navigation("QRCodeDesigns"); }); - modelBuilder.Entity("api.Models.User", b => + modelBuilder.Entity("TrackQrApi.Models.User", b => { b.Navigation("Workspaces"); }); - modelBuilder.Entity("api.Models.Workspace", b => + modelBuilder.Entity("TrackQrApi.Models.Workspace", b => { b.Navigation("Assets"); diff --git a/src/api/Migrations/20260130185641_AddQRCodeNameAndLogo.cs b/src/TrackApi/TrackQrApi/Migrations/20260130185641_AddQRCodeNameAndLogo.cs similarity index 99% rename from src/api/Migrations/20260130185641_AddQRCodeNameAndLogo.cs rename to src/TrackApi/TrackQrApi/Migrations/20260130185641_AddQRCodeNameAndLogo.cs index ac7642d..6fc96c3 100644 --- a/src/api/Migrations/20260130185641_AddQRCodeNameAndLogo.cs +++ b/src/TrackApi/TrackQrApi/Migrations/20260130185641_AddQRCodeNameAndLogo.cs @@ -4,7 +4,7 @@ using Microsoft.EntityFrameworkCore.Migrations; #nullable disable -namespace api.Migrations +namespace TrackQrApi.Migrations { /// public partial class AddQRCodeNameAndLogo : Migration diff --git a/src/api/Migrations/20260130193730_AddProjectDescription.Designer.cs b/src/TrackApi/TrackQrApi/Migrations/20260130193730_AddProjectDescription.Designer.cs similarity index 87% rename from src/api/Migrations/20260130193730_AddProjectDescription.Designer.cs rename to src/TrackApi/TrackQrApi/Migrations/20260130193730_AddProjectDescription.Designer.cs index 43e288a..bbacc95 100644 --- a/src/api/Migrations/20260130193730_AddProjectDescription.Designer.cs +++ b/src/TrackApi/TrackQrApi/Migrations/20260130193730_AddProjectDescription.Designer.cs @@ -6,11 +6,11 @@ using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; -using api.Data; +using TrackQrApi.Data; #nullable disable -namespace api.Migrations +namespace TrackQrApi.Migrations { [DbContext(typeof(AppDbContext))] [Migration("20260130193730_AddProjectDescription")] @@ -26,7 +26,7 @@ namespace api.Migrations NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); - modelBuilder.Entity("api.Models.ApiKey", b => + modelBuilder.Entity("TrackQrApi.Models.ApiKey", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -77,7 +77,7 @@ namespace api.Migrations b.ToTable("ApiKeys"); }); - modelBuilder.Entity("api.Models.Asset", b => + modelBuilder.Entity("TrackQrApi.Models.Asset", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -116,7 +116,7 @@ namespace api.Migrations b.ToTable("Assets"); }); - modelBuilder.Entity("api.Models.Domain", b => + modelBuilder.Entity("TrackQrApi.Models.Domain", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -155,7 +155,7 @@ namespace api.Migrations b.ToTable("Domains"); }); - modelBuilder.Entity("api.Models.EmailVerificationToken", b => + modelBuilder.Entity("TrackQrApi.Models.EmailVerificationToken", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -187,7 +187,7 @@ namespace api.Migrations b.ToTable("EmailVerificationTokens"); }); - modelBuilder.Entity("api.Models.Event", b => + modelBuilder.Entity("TrackQrApi.Models.Event", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -251,7 +251,7 @@ namespace api.Migrations b.ToTable("Events"); }); - modelBuilder.Entity("api.Models.PasswordResetToken", b => + modelBuilder.Entity("TrackQrApi.Models.PasswordResetToken", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -286,7 +286,7 @@ namespace api.Migrations b.ToTable("PasswordResetTokens"); }); - modelBuilder.Entity("api.Models.Project", b => + modelBuilder.Entity("TrackQrApi.Models.Project", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -315,7 +315,7 @@ namespace api.Migrations b.ToTable("Projects"); }); - modelBuilder.Entity("api.Models.QRCodeDesign", b => + modelBuilder.Entity("TrackQrApi.Models.QRCodeDesign", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -364,7 +364,7 @@ namespace api.Migrations b.ToTable("QrCodeDesigns"); }); - modelBuilder.Entity("api.Models.ShortLink", b => + modelBuilder.Entity("TrackQrApi.Models.ShortLink", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -430,7 +430,7 @@ namespace api.Migrations b.ToTable("ShortLinks"); }); - modelBuilder.Entity("api.Models.User", b => + modelBuilder.Entity("TrackQrApi.Models.User", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -465,7 +465,7 @@ namespace api.Migrations b.ToTable("Users"); }); - modelBuilder.Entity("api.Models.Workspace", b => + modelBuilder.Entity("TrackQrApi.Models.Workspace", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -502,9 +502,9 @@ namespace api.Migrations b.ToTable("Workspaces"); }); - modelBuilder.Entity("api.Models.ApiKey", b => + modelBuilder.Entity("TrackQrApi.Models.ApiKey", b => { - b.HasOne("api.Models.Workspace", "Workspace") + b.HasOne("TrackQrApi.Models.Workspace", "Workspace") .WithMany() .HasForeignKey("WorkspaceId") .OnDelete(DeleteBehavior.Cascade) @@ -513,9 +513,9 @@ namespace api.Migrations b.Navigation("Workspace"); }); - modelBuilder.Entity("api.Models.Asset", b => + modelBuilder.Entity("TrackQrApi.Models.Asset", b => { - b.HasOne("api.Models.Workspace", "Workspace") + b.HasOne("TrackQrApi.Models.Workspace", "Workspace") .WithMany("Assets") .HasForeignKey("WorkspaceId") .OnDelete(DeleteBehavior.Cascade) @@ -524,9 +524,9 @@ namespace api.Migrations b.Navigation("Workspace"); }); - modelBuilder.Entity("api.Models.Domain", b => + modelBuilder.Entity("TrackQrApi.Models.Domain", b => { - b.HasOne("api.Models.Workspace", "Workspace") + b.HasOne("TrackQrApi.Models.Workspace", "Workspace") .WithMany("Domains") .HasForeignKey("WorkspaceId") .OnDelete(DeleteBehavior.Cascade) @@ -535,9 +535,9 @@ namespace api.Migrations b.Navigation("Workspace"); }); - modelBuilder.Entity("api.Models.EmailVerificationToken", b => + modelBuilder.Entity("TrackQrApi.Models.EmailVerificationToken", b => { - b.HasOne("api.Models.User", "User") + b.HasOne("TrackQrApi.Models.User", "User") .WithMany() .HasForeignKey("UserId") .OnDelete(DeleteBehavior.Cascade) @@ -546,20 +546,20 @@ namespace api.Migrations b.Navigation("User"); }); - modelBuilder.Entity("api.Models.Event", b => + modelBuilder.Entity("TrackQrApi.Models.Event", b => { - b.HasOne("api.Models.QRCodeDesign", "QRCode") + b.HasOne("TrackQrApi.Models.QRCodeDesign", "QRCode") .WithMany("Events") .HasForeignKey("QRCodeId") .OnDelete(DeleteBehavior.SetNull); - b.HasOne("api.Models.ShortLink", "ShortLink") + b.HasOne("TrackQrApi.Models.ShortLink", "ShortLink") .WithMany("Events") .HasForeignKey("ShortLinkId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); - b.HasOne("api.Models.Workspace", "Workspace") + b.HasOne("TrackQrApi.Models.Workspace", "Workspace") .WithMany("Events") .HasForeignKey("WorkspaceId") .OnDelete(DeleteBehavior.Cascade) @@ -572,9 +572,9 @@ namespace api.Migrations b.Navigation("Workspace"); }); - modelBuilder.Entity("api.Models.PasswordResetToken", b => + modelBuilder.Entity("TrackQrApi.Models.PasswordResetToken", b => { - b.HasOne("api.Models.User", "User") + b.HasOne("TrackQrApi.Models.User", "User") .WithMany() .HasForeignKey("UserId") .OnDelete(DeleteBehavior.Cascade) @@ -583,9 +583,9 @@ namespace api.Migrations b.Navigation("User"); }); - modelBuilder.Entity("api.Models.Project", b => + modelBuilder.Entity("TrackQrApi.Models.Project", b => { - b.HasOne("api.Models.Workspace", "Workspace") + b.HasOne("TrackQrApi.Models.Workspace", "Workspace") .WithMany("Projects") .HasForeignKey("WorkspaceId") .OnDelete(DeleteBehavior.Cascade) @@ -594,24 +594,24 @@ namespace api.Migrations b.Navigation("Workspace"); }); - modelBuilder.Entity("api.Models.QRCodeDesign", b => + modelBuilder.Entity("TrackQrApi.Models.QRCodeDesign", b => { - b.HasOne("api.Models.Asset", "LogoAsset") + b.HasOne("TrackQrApi.Models.Asset", "LogoAsset") .WithMany() .HasForeignKey("LogoAssetId") .OnDelete(DeleteBehavior.SetNull); - b.HasOne("api.Models.Project", "Project") + b.HasOne("TrackQrApi.Models.Project", "Project") .WithMany("QRCodeDesigns") .HasForeignKey("ProjectId") .OnDelete(DeleteBehavior.SetNull); - b.HasOne("api.Models.ShortLink", "ShortLink") + b.HasOne("TrackQrApi.Models.ShortLink", "ShortLink") .WithMany("QRCodeDesigns") .HasForeignKey("ShortLinkId") .OnDelete(DeleteBehavior.SetNull); - b.HasOne("api.Models.Workspace", "Workspace") + b.HasOne("TrackQrApi.Models.Workspace", "Workspace") .WithMany("QRCodeDesigns") .HasForeignKey("WorkspaceId") .OnDelete(DeleteBehavior.Cascade) @@ -626,19 +626,19 @@ namespace api.Migrations b.Navigation("Workspace"); }); - modelBuilder.Entity("api.Models.ShortLink", b => + modelBuilder.Entity("TrackQrApi.Models.ShortLink", b => { - b.HasOne("api.Models.Domain", "Domain") + b.HasOne("TrackQrApi.Models.Domain", "Domain") .WithMany("ShortLinks") .HasForeignKey("DomainId") .OnDelete(DeleteBehavior.SetNull); - b.HasOne("api.Models.Project", "Project") + b.HasOne("TrackQrApi.Models.Project", "Project") .WithMany("ShortLinks") .HasForeignKey("ProjectId") .OnDelete(DeleteBehavior.SetNull); - b.HasOne("api.Models.Workspace", "Workspace") + b.HasOne("TrackQrApi.Models.Workspace", "Workspace") .WithMany("ShortLinks") .HasForeignKey("WorkspaceId") .OnDelete(DeleteBehavior.Cascade) @@ -651,9 +651,9 @@ namespace api.Migrations b.Navigation("Workspace"); }); - modelBuilder.Entity("api.Models.Workspace", b => + modelBuilder.Entity("TrackQrApi.Models.Workspace", b => { - b.HasOne("api.Models.User", "Owner") + b.HasOne("TrackQrApi.Models.User", "Owner") .WithMany("Workspaces") .HasForeignKey("OwnerUserId") .OnDelete(DeleteBehavior.Cascade) @@ -662,36 +662,36 @@ namespace api.Migrations b.Navigation("Owner"); }); - modelBuilder.Entity("api.Models.Domain", b => + modelBuilder.Entity("TrackQrApi.Models.Domain", b => { b.Navigation("ShortLinks"); }); - modelBuilder.Entity("api.Models.Project", b => + modelBuilder.Entity("TrackQrApi.Models.Project", b => { b.Navigation("QRCodeDesigns"); b.Navigation("ShortLinks"); }); - modelBuilder.Entity("api.Models.QRCodeDesign", b => + modelBuilder.Entity("TrackQrApi.Models.QRCodeDesign", b => { b.Navigation("Events"); }); - modelBuilder.Entity("api.Models.ShortLink", b => + modelBuilder.Entity("TrackQrApi.Models.ShortLink", b => { b.Navigation("Events"); b.Navigation("QRCodeDesigns"); }); - modelBuilder.Entity("api.Models.User", b => + modelBuilder.Entity("TrackQrApi.Models.User", b => { b.Navigation("Workspaces"); }); - modelBuilder.Entity("api.Models.Workspace", b => + modelBuilder.Entity("TrackQrApi.Models.Workspace", b => { b.Navigation("Assets"); diff --git a/src/api/Migrations/20260130193730_AddProjectDescription.cs b/src/TrackApi/TrackQrApi/Migrations/20260130193730_AddProjectDescription.cs similarity index 95% rename from src/api/Migrations/20260130193730_AddProjectDescription.cs rename to src/TrackApi/TrackQrApi/Migrations/20260130193730_AddProjectDescription.cs index 0b276ff..653b8cc 100644 --- a/src/api/Migrations/20260130193730_AddProjectDescription.cs +++ b/src/TrackApi/TrackQrApi/Migrations/20260130193730_AddProjectDescription.cs @@ -2,7 +2,7 @@ #nullable disable -namespace api.Migrations +namespace TrackQrApi.Migrations { /// public partial class AddProjectDescription : Migration diff --git a/src/api/Migrations/AppDbContextModelSnapshot.cs b/src/TrackApi/TrackQrApi/Migrations/AppDbContextModelSnapshot.cs similarity index 87% rename from src/api/Migrations/AppDbContextModelSnapshot.cs rename to src/TrackApi/TrackQrApi/Migrations/AppDbContextModelSnapshot.cs index 293d8ad..f6e35e0 100644 --- a/src/api/Migrations/AppDbContextModelSnapshot.cs +++ b/src/TrackApi/TrackQrApi/Migrations/AppDbContextModelSnapshot.cs @@ -5,11 +5,11 @@ using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; -using api.Data; +using TrackQrApi.Data; #nullable disable -namespace api.Migrations +namespace TrackQrApi.Migrations { [DbContext(typeof(AppDbContext))] partial class AppDbContextModelSnapshot : ModelSnapshot @@ -23,7 +23,7 @@ namespace api.Migrations NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); - modelBuilder.Entity("api.Models.ApiKey", b => + modelBuilder.Entity("TrackQrApi.Models.ApiKey", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -74,7 +74,7 @@ namespace api.Migrations b.ToTable("ApiKeys"); }); - modelBuilder.Entity("api.Models.Asset", b => + modelBuilder.Entity("TrackQrApi.Models.Asset", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -113,7 +113,7 @@ namespace api.Migrations b.ToTable("Assets"); }); - modelBuilder.Entity("api.Models.Domain", b => + modelBuilder.Entity("TrackQrApi.Models.Domain", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -152,7 +152,7 @@ namespace api.Migrations b.ToTable("Domains"); }); - modelBuilder.Entity("api.Models.EmailVerificationToken", b => + modelBuilder.Entity("TrackQrApi.Models.EmailVerificationToken", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -184,7 +184,7 @@ namespace api.Migrations b.ToTable("EmailVerificationTokens"); }); - modelBuilder.Entity("api.Models.Event", b => + modelBuilder.Entity("TrackQrApi.Models.Event", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -248,7 +248,7 @@ namespace api.Migrations b.ToTable("Events"); }); - modelBuilder.Entity("api.Models.PasswordResetToken", b => + modelBuilder.Entity("TrackQrApi.Models.PasswordResetToken", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -283,7 +283,7 @@ namespace api.Migrations b.ToTable("PasswordResetTokens"); }); - modelBuilder.Entity("api.Models.Project", b => + modelBuilder.Entity("TrackQrApi.Models.Project", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -312,7 +312,7 @@ namespace api.Migrations b.ToTable("Projects"); }); - modelBuilder.Entity("api.Models.QRCodeDesign", b => + modelBuilder.Entity("TrackQrApi.Models.QRCodeDesign", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -361,7 +361,7 @@ namespace api.Migrations b.ToTable("QrCodeDesigns"); }); - modelBuilder.Entity("api.Models.ShortLink", b => + modelBuilder.Entity("TrackQrApi.Models.ShortLink", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -427,7 +427,7 @@ namespace api.Migrations b.ToTable("ShortLinks"); }); - modelBuilder.Entity("api.Models.User", b => + modelBuilder.Entity("TrackQrApi.Models.User", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -462,7 +462,7 @@ namespace api.Migrations b.ToTable("Users"); }); - modelBuilder.Entity("api.Models.Workspace", b => + modelBuilder.Entity("TrackQrApi.Models.Workspace", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -499,9 +499,9 @@ namespace api.Migrations b.ToTable("Workspaces"); }); - modelBuilder.Entity("api.Models.ApiKey", b => + modelBuilder.Entity("TrackQrApi.Models.ApiKey", b => { - b.HasOne("api.Models.Workspace", "Workspace") + b.HasOne("TrackQrApi.Models.Workspace", "Workspace") .WithMany() .HasForeignKey("WorkspaceId") .OnDelete(DeleteBehavior.Cascade) @@ -510,9 +510,9 @@ namespace api.Migrations b.Navigation("Workspace"); }); - modelBuilder.Entity("api.Models.Asset", b => + modelBuilder.Entity("TrackQrApi.Models.Asset", b => { - b.HasOne("api.Models.Workspace", "Workspace") + b.HasOne("TrackQrApi.Models.Workspace", "Workspace") .WithMany("Assets") .HasForeignKey("WorkspaceId") .OnDelete(DeleteBehavior.Cascade) @@ -521,9 +521,9 @@ namespace api.Migrations b.Navigation("Workspace"); }); - modelBuilder.Entity("api.Models.Domain", b => + modelBuilder.Entity("TrackQrApi.Models.Domain", b => { - b.HasOne("api.Models.Workspace", "Workspace") + b.HasOne("TrackQrApi.Models.Workspace", "Workspace") .WithMany("Domains") .HasForeignKey("WorkspaceId") .OnDelete(DeleteBehavior.Cascade) @@ -532,9 +532,9 @@ namespace api.Migrations b.Navigation("Workspace"); }); - modelBuilder.Entity("api.Models.EmailVerificationToken", b => + modelBuilder.Entity("TrackQrApi.Models.EmailVerificationToken", b => { - b.HasOne("api.Models.User", "User") + b.HasOne("TrackQrApi.Models.User", "User") .WithMany() .HasForeignKey("UserId") .OnDelete(DeleteBehavior.Cascade) @@ -543,20 +543,20 @@ namespace api.Migrations b.Navigation("User"); }); - modelBuilder.Entity("api.Models.Event", b => + modelBuilder.Entity("TrackQrApi.Models.Event", b => { - b.HasOne("api.Models.QRCodeDesign", "QRCode") + b.HasOne("TrackQrApi.Models.QRCodeDesign", "QRCode") .WithMany("Events") .HasForeignKey("QRCodeId") .OnDelete(DeleteBehavior.SetNull); - b.HasOne("api.Models.ShortLink", "ShortLink") + b.HasOne("TrackQrApi.Models.ShortLink", "ShortLink") .WithMany("Events") .HasForeignKey("ShortLinkId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); - b.HasOne("api.Models.Workspace", "Workspace") + b.HasOne("TrackQrApi.Models.Workspace", "Workspace") .WithMany("Events") .HasForeignKey("WorkspaceId") .OnDelete(DeleteBehavior.Cascade) @@ -569,9 +569,9 @@ namespace api.Migrations b.Navigation("Workspace"); }); - modelBuilder.Entity("api.Models.PasswordResetToken", b => + modelBuilder.Entity("TrackQrApi.Models.PasswordResetToken", b => { - b.HasOne("api.Models.User", "User") + b.HasOne("TrackQrApi.Models.User", "User") .WithMany() .HasForeignKey("UserId") .OnDelete(DeleteBehavior.Cascade) @@ -580,9 +580,9 @@ namespace api.Migrations b.Navigation("User"); }); - modelBuilder.Entity("api.Models.Project", b => + modelBuilder.Entity("TrackQrApi.Models.Project", b => { - b.HasOne("api.Models.Workspace", "Workspace") + b.HasOne("TrackQrApi.Models.Workspace", "Workspace") .WithMany("Projects") .HasForeignKey("WorkspaceId") .OnDelete(DeleteBehavior.Cascade) @@ -591,24 +591,24 @@ namespace api.Migrations b.Navigation("Workspace"); }); - modelBuilder.Entity("api.Models.QRCodeDesign", b => + modelBuilder.Entity("TrackQrApi.Models.QRCodeDesign", b => { - b.HasOne("api.Models.Asset", "LogoAsset") + b.HasOne("TrackQrApi.Models.Asset", "LogoAsset") .WithMany() .HasForeignKey("LogoAssetId") .OnDelete(DeleteBehavior.SetNull); - b.HasOne("api.Models.Project", "Project") + b.HasOne("TrackQrApi.Models.Project", "Project") .WithMany("QRCodeDesigns") .HasForeignKey("ProjectId") .OnDelete(DeleteBehavior.SetNull); - b.HasOne("api.Models.ShortLink", "ShortLink") + b.HasOne("TrackQrApi.Models.ShortLink", "ShortLink") .WithMany("QRCodeDesigns") .HasForeignKey("ShortLinkId") .OnDelete(DeleteBehavior.SetNull); - b.HasOne("api.Models.Workspace", "Workspace") + b.HasOne("TrackQrApi.Models.Workspace", "Workspace") .WithMany("QRCodeDesigns") .HasForeignKey("WorkspaceId") .OnDelete(DeleteBehavior.Cascade) @@ -623,19 +623,19 @@ namespace api.Migrations b.Navigation("Workspace"); }); - modelBuilder.Entity("api.Models.ShortLink", b => + modelBuilder.Entity("TrackQrApi.Models.ShortLink", b => { - b.HasOne("api.Models.Domain", "Domain") + b.HasOne("TrackQrApi.Models.Domain", "Domain") .WithMany("ShortLinks") .HasForeignKey("DomainId") .OnDelete(DeleteBehavior.SetNull); - b.HasOne("api.Models.Project", "Project") + b.HasOne("TrackQrApi.Models.Project", "Project") .WithMany("ShortLinks") .HasForeignKey("ProjectId") .OnDelete(DeleteBehavior.SetNull); - b.HasOne("api.Models.Workspace", "Workspace") + b.HasOne("TrackQrApi.Models.Workspace", "Workspace") .WithMany("ShortLinks") .HasForeignKey("WorkspaceId") .OnDelete(DeleteBehavior.Cascade) @@ -648,9 +648,9 @@ namespace api.Migrations b.Navigation("Workspace"); }); - modelBuilder.Entity("api.Models.Workspace", b => + modelBuilder.Entity("TrackQrApi.Models.Workspace", b => { - b.HasOne("api.Models.User", "Owner") + b.HasOne("TrackQrApi.Models.User", "Owner") .WithMany("Workspaces") .HasForeignKey("OwnerUserId") .OnDelete(DeleteBehavior.Cascade) @@ -659,36 +659,36 @@ namespace api.Migrations b.Navigation("Owner"); }); - modelBuilder.Entity("api.Models.Domain", b => + modelBuilder.Entity("TrackQrApi.Models.Domain", b => { b.Navigation("ShortLinks"); }); - modelBuilder.Entity("api.Models.Project", b => + modelBuilder.Entity("TrackQrApi.Models.Project", b => { b.Navigation("QRCodeDesigns"); b.Navigation("ShortLinks"); }); - modelBuilder.Entity("api.Models.QRCodeDesign", b => + modelBuilder.Entity("TrackQrApi.Models.QRCodeDesign", b => { b.Navigation("Events"); }); - modelBuilder.Entity("api.Models.ShortLink", b => + modelBuilder.Entity("TrackQrApi.Models.ShortLink", b => { b.Navigation("Events"); b.Navigation("QRCodeDesigns"); }); - modelBuilder.Entity("api.Models.User", b => + modelBuilder.Entity("TrackQrApi.Models.User", b => { b.Navigation("Workspaces"); }); - modelBuilder.Entity("api.Models.Workspace", b => + modelBuilder.Entity("TrackQrApi.Models.Workspace", b => { b.Navigation("Assets"); diff --git a/src/api/Models/ApiKey.cs b/src/TrackApi/TrackQrApi/Models/ApiKey.cs similarity index 95% rename from src/api/Models/ApiKey.cs rename to src/TrackApi/TrackQrApi/Models/ApiKey.cs index f119edc..5bdbed9 100644 --- a/src/api/Models/ApiKey.cs +++ b/src/TrackApi/TrackQrApi/Models/ApiKey.cs @@ -1,4 +1,4 @@ -namespace api.Models; +namespace TrackQrApi.Models; public class ApiKey { @@ -14,4 +14,4 @@ public class ApiKey public List? Scopes { get; set; } // e.g., ["links:read", "links:write", "qrcodes:read"] public Workspace Workspace { get; set; } = null!; -} +} \ No newline at end of file diff --git a/src/api/Models/Asset.cs b/src/TrackApi/TrackQrApi/Models/Asset.cs similarity index 93% rename from src/api/Models/Asset.cs rename to src/TrackApi/TrackQrApi/Models/Asset.cs index bf1f8e1..3fc0364 100644 --- a/src/api/Models/Asset.cs +++ b/src/TrackApi/TrackQrApi/Models/Asset.cs @@ -1,4 +1,4 @@ -namespace api.Models; +namespace TrackQrApi.Models; public enum AssetType { @@ -17,4 +17,4 @@ public class Asset // Navigation properties public Workspace Workspace { get; set; } = null!; -} +} \ No newline at end of file diff --git a/src/api/Models/Domain.cs b/src/TrackApi/TrackQrApi/Models/Domain.cs similarity index 94% rename from src/api/Models/Domain.cs rename to src/TrackApi/TrackQrApi/Models/Domain.cs index ac3e401..4bb0de9 100644 --- a/src/api/Models/Domain.cs +++ b/src/TrackApi/TrackQrApi/Models/Domain.cs @@ -1,4 +1,4 @@ -namespace api.Models; +namespace TrackQrApi.Models; public enum DomainStatus { @@ -19,4 +19,4 @@ public class Domain // Navigation properties public Workspace Workspace { get; set; } = null!; public ICollection ShortLinks { get; set; } = []; -} +} \ No newline at end of file diff --git a/src/api/Models/EmailVerificationToken.cs b/src/TrackApi/TrackQrApi/Models/EmailVerificationToken.cs similarity index 91% rename from src/api/Models/EmailVerificationToken.cs rename to src/TrackApi/TrackQrApi/Models/EmailVerificationToken.cs index 4e49a42..02ec720 100644 --- a/src/api/Models/EmailVerificationToken.cs +++ b/src/TrackApi/TrackQrApi/Models/EmailVerificationToken.cs @@ -1,4 +1,4 @@ -namespace api.Models; +namespace TrackQrApi.Models; public class EmailVerificationToken { @@ -10,4 +10,4 @@ public class EmailVerificationToken // Navigation public User User { get; set; } = null!; -} +} \ No newline at end of file diff --git a/src/api/Models/Event.cs b/src/TrackApi/TrackQrApi/Models/Event.cs similarity index 96% rename from src/api/Models/Event.cs rename to src/TrackApi/TrackQrApi/Models/Event.cs index 6279f93..81766ad 100644 --- a/src/api/Models/Event.cs +++ b/src/TrackApi/TrackQrApi/Models/Event.cs @@ -1,4 +1,4 @@ -namespace api.Models; +namespace TrackQrApi.Models; public enum EventType { @@ -25,4 +25,4 @@ public class Event public Workspace Workspace { get; set; } = null!; public ShortLink ShortLink { get; set; } = null!; public QRCodeDesign? QRCode { get; set; } -} +} \ No newline at end of file diff --git a/src/api/Models/PasswordResetToken.cs b/src/TrackApi/TrackQrApi/Models/PasswordResetToken.cs similarity index 91% rename from src/api/Models/PasswordResetToken.cs rename to src/TrackApi/TrackQrApi/Models/PasswordResetToken.cs index 58e61de..eb02f18 100644 --- a/src/api/Models/PasswordResetToken.cs +++ b/src/TrackApi/TrackQrApi/Models/PasswordResetToken.cs @@ -1,4 +1,4 @@ -namespace api.Models; +namespace TrackQrApi.Models; public class PasswordResetToken { @@ -11,4 +11,4 @@ public class PasswordResetToken // Navigation public User User { get; set; } = null!; -} +} \ No newline at end of file diff --git a/src/api/Models/Project.cs b/src/TrackApi/TrackQrApi/Models/Project.cs similarity index 93% rename from src/api/Models/Project.cs rename to src/TrackApi/TrackQrApi/Models/Project.cs index 01d3ad5..2191b3d 100644 --- a/src/api/Models/Project.cs +++ b/src/TrackApi/TrackQrApi/Models/Project.cs @@ -1,4 +1,4 @@ -namespace api.Models; +namespace TrackQrApi.Models; public class Project { @@ -12,4 +12,4 @@ public class Project public Workspace Workspace { get; set; } = null!; public ICollection ShortLinks { get; set; } = []; public ICollection QRCodeDesigns { get; set; } = []; -} +} \ No newline at end of file diff --git a/src/api/Models/QRCodeDesign.cs b/src/TrackApi/TrackQrApi/Models/QRCodeDesign.cs similarity index 95% rename from src/api/Models/QRCodeDesign.cs rename to src/TrackApi/TrackQrApi/Models/QRCodeDesign.cs index d4d905b..4457879 100644 --- a/src/api/Models/QRCodeDesign.cs +++ b/src/TrackApi/TrackQrApi/Models/QRCodeDesign.cs @@ -1,4 +1,4 @@ -namespace api.Models; +namespace TrackQrApi.Models; public class QRCodeDesign { @@ -18,4 +18,4 @@ public class QRCodeDesign public ShortLink? ShortLink { get; set; } public Asset? LogoAsset { get; set; } public ICollection Events { get; set; } = []; -} +} \ No newline at end of file diff --git a/src/api/Models/ShortLink.cs b/src/TrackApi/TrackQrApi/Models/ShortLink.cs similarity index 97% rename from src/api/Models/ShortLink.cs rename to src/TrackApi/TrackQrApi/Models/ShortLink.cs index 0a0f5d0..7837bff 100644 --- a/src/api/Models/ShortLink.cs +++ b/src/TrackApi/TrackQrApi/Models/ShortLink.cs @@ -1,4 +1,4 @@ -namespace api.Models; +namespace TrackQrApi.Models; public enum ShortLinkStatus { @@ -28,4 +28,4 @@ public class ShortLink public Domain? Domain { get; set; } public ICollection QRCodeDesigns { get; set; } = []; public ICollection Events { get; set; } = []; -} +} \ No newline at end of file diff --git a/src/api/Models/User.cs b/src/TrackApi/TrackQrApi/Models/User.cs similarity index 92% rename from src/api/Models/User.cs rename to src/TrackApi/TrackQrApi/Models/User.cs index f1a9850..1243ba2 100644 --- a/src/api/Models/User.cs +++ b/src/TrackApi/TrackQrApi/Models/User.cs @@ -1,4 +1,4 @@ -namespace api.Models; +namespace TrackQrApi.Models; public class User { @@ -11,4 +11,4 @@ public class User // Navigation properties public ICollection Workspaces { get; set; } = []; -} +} \ No newline at end of file diff --git a/src/api/Models/Workspace.cs b/src/TrackApi/TrackQrApi/Models/Workspace.cs similarity index 96% rename from src/api/Models/Workspace.cs rename to src/TrackApi/TrackQrApi/Models/Workspace.cs index e3e0f9a..b9c8b8f 100644 --- a/src/api/Models/Workspace.cs +++ b/src/TrackApi/TrackQrApi/Models/Workspace.cs @@ -1,4 +1,4 @@ -namespace api.Models; +namespace TrackQrApi.Models; public enum WorkspacePlan { @@ -25,4 +25,4 @@ public class Workspace public ICollection QRCodeDesigns { get; set; } = []; public ICollection Events { get; set; } = []; public ICollection Assets { get; set; } = []; -} +} \ No newline at end of file diff --git a/src/api/Program.cs b/src/TrackApi/TrackQrApi/Program.cs similarity index 83% rename from src/api/Program.cs rename to src/TrackApi/TrackQrApi/Program.cs index 31355b7..ebd989e 100644 --- a/src/api/Program.cs +++ b/src/TrackApi/TrackQrApi/Program.cs @@ -1,30 +1,29 @@ using System.Text; using System.Threading.RateLimiting; -using api.Data; -using api.Features.Auth.Settings; -using api.Features.Events.Services; -using api.Features.Assets.Services; -using api.Features.Email.Services; -using api.Features.Billing.Services; -using api.Features.Billing.Settings; -using api.Features.Plans.Services; -using api.Features.QRCodes.Services; -using api.Middleware; using FastEndpoints; using Microsoft.AspNetCore.Authentication.JwtBearer; -using Microsoft.AspNetCore.RateLimiting; using Microsoft.EntityFrameworkCore; using Microsoft.IdentityModel.Tokens; using Serilog; +using Serilog.Events; +using TrackQrApi.Data; +using TrackQrApi.Features.Assets.Services; +using TrackQrApi.Features.Auth.Settings; +using TrackQrApi.Features.Billing.Services; +using TrackQrApi.Features.Billing.Settings; +using TrackQrApi.Features.Email.Services; +using TrackQrApi.Features.Events.Services; +using TrackQrApi.Features.Plans.Services; +using TrackQrApi.Features.QRCodes.Services; +using TrackQrApi.Middleware; // Configure Serilog Log.Logger = new LoggerConfiguration() .MinimumLevel.Information() - .MinimumLevel.Override("Microsoft", Serilog.Events.LogEventLevel.Warning) - .MinimumLevel.Override("Microsoft.EntityFrameworkCore", Serilog.Events.LogEventLevel.Warning) + .MinimumLevel.Override("Microsoft", LogEventLevel.Warning) + .MinimumLevel.Override("Microsoft.EntityFrameworkCore", LogEventLevel.Warning) .Enrich.FromLogContext() .WriteTo.Console(outputTemplate: "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj}{NewLine}{Exception}") - .WriteTo.File("logs/api-.log", rollingInterval: RollingInterval.Day, retainedFileCountLimit: 7) .CreateLogger(); try @@ -52,7 +51,7 @@ try { // Production: configure allowed origins from config var allowedOrigins = builder.Configuration.GetSection("Cors:AllowedOrigins").Get() - ?? ["https://trakqr.com"]; + ?? ["https://trakqr.com"]; policy.WithOrigins(allowedOrigins) .AllowAnyHeader() .AllowAnyMethod() @@ -76,8 +75,8 @@ try // Global rate limit for all endpoints options.AddPolicy("global", context => RateLimitPartition.GetFixedWindowLimiter( - partitionKey: context.Connection.RemoteIpAddress?.ToString() ?? "unknown", - factory: _ => new FixedWindowRateLimiterOptions + context.Connection.RemoteIpAddress?.ToString() ?? "unknown", + _ => new FixedWindowRateLimiterOptions { PermitLimit = globalLimit, Window = TimeSpan.FromMinutes(1), @@ -87,8 +86,8 @@ try // Strict rate limit for authentication endpoints options.AddPolicy("auth", context => RateLimitPartition.GetFixedWindowLimiter( - partitionKey: context.Connection.RemoteIpAddress?.ToString() ?? "unknown", - factory: _ => new FixedWindowRateLimiterOptions + context.Connection.RemoteIpAddress?.ToString() ?? "unknown", + _ => new FixedWindowRateLimiterOptions { PermitLimit = authLimit, Window = TimeSpan.FromMinutes(1), @@ -98,8 +97,8 @@ try // Higher limit for redirect endpoint (public, needs to be fast) options.AddPolicy("redirect", context => RateLimitPartition.GetSlidingWindowLimiter( - partitionKey: context.Connection.RemoteIpAddress?.ToString() ?? "unknown", - factory: _ => new SlidingWindowRateLimiterOptions + context.Connection.RemoteIpAddress?.ToString() ?? "unknown", + _ => new SlidingWindowRateLimiterOptions { PermitLimit = redirectLimit, Window = TimeSpan.FromMinutes(1), @@ -110,8 +109,9 @@ try // API rate limit for authenticated endpoints options.AddPolicy("api", context => RateLimitPartition.GetTokenBucketLimiter( - partitionKey: context.User?.Identity?.Name ?? context.Connection.RemoteIpAddress?.ToString() ?? "unknown", - factory: _ => new TokenBucketRateLimiterOptions + context.User?.Identity?.Name ?? + context.Connection.RemoteIpAddress?.ToString() ?? "unknown", + _ => new TokenBucketRateLimiterOptions { TokenLimit = apiLimit, ReplenishmentPeriod = TimeSpan.FromMinutes(1), @@ -135,14 +135,10 @@ try builder.Services.Configure(builder.Configuration.GetSection("Email")); var emailProvider = builder.Configuration.GetValue("Email:Provider") ?? "console"; if (emailProvider == "smtp") - { builder.Services.AddSingleton(); - } else - { // Use console email service for development builder.Services.AddSingleton(); - } // Configure Stripe builder.Services.Configure(builder.Configuration.GetSection("Stripe")); @@ -210,8 +206,4 @@ try catch (Exception ex) { Log.Fatal(ex, "Application terminated unexpectedly"); -} -finally -{ - Log.CloseAndFlush(); -} +} \ No newline at end of file diff --git a/src/api/Properties/launchSettings.json b/src/TrackApi/TrackQrApi/Properties/launchSettings.json similarity index 100% rename from src/api/Properties/launchSettings.json rename to src/TrackApi/TrackQrApi/Properties/launchSettings.json diff --git a/src/TrackApi/TrackQrApi/TrackQrApi.csproj b/src/TrackApi/TrackQrApi/TrackQrApi.csproj new file mode 100644 index 0000000..49b5ab1 --- /dev/null +++ b/src/TrackApi/TrackQrApi/TrackQrApi.csproj @@ -0,0 +1,34 @@ + + + + net10.0 + enable + enable + + + + + + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + + + + diff --git a/src/api/appsettings.Development.json b/src/TrackApi/TrackQrApi/appsettings.Development.json similarity index 89% rename from src/api/appsettings.Development.json rename to src/TrackApi/TrackQrApi/appsettings.Development.json index a9b7480..2aa5f97 100644 --- a/src/api/appsettings.Development.json +++ b/src/TrackApi/TrackQrApi/appsettings.Development.json @@ -21,7 +21,10 @@ "BaseUrl": "http://localhost:5173" }, "Cors": { - "AllowedOrigins": ["http://localhost:5173", "https://localhost:5173"] + "AllowedOrigins": [ + "http://localhost:5173", + "https://localhost:5173" + ] }, "Stripe": { "SecretKey": "sk_test_your_test_key_here", diff --git a/src/api/appsettings.json b/src/TrackApi/TrackQrApi/appsettings.json similarity index 75% rename from src/api/appsettings.json rename to src/TrackApi/TrackQrApi/appsettings.json index 730c2b8..fa329c4 100644 --- a/src/api/appsettings.json +++ b/src/TrackApi/TrackQrApi/appsettings.json @@ -7,10 +7,10 @@ }, "AllowedHosts": "*", "ConnectionStrings": { - "PostgresConnection": "" + "PostgresConnection": "Host=localhost;Port=5400;Database=trakqr;Username=sa;Password=P@ssword123!" }, "Jwt": { - "Secret": "", + "Secret": "dev-secret-key-min-32-characters-long-for-hmac256!", "Issuer": "TrakQR", "Audience": "TrakQR", "ExpirationMinutes": 60 @@ -29,7 +29,9 @@ } }, "Cors": { - "AllowedOrigins": ["https://trakqr.com"] + "AllowedOrigins": [ + "https://trakqr.com" + ] }, "GeoIP": { "DatabasePath": "" diff --git a/src/TrackApi/global.json b/src/TrackApi/global.json new file mode 100644 index 0000000..a11f48e --- /dev/null +++ b/src/TrackApi/global.json @@ -0,0 +1,7 @@ +{ + "sdk": { + "version": "10.0.0", + "rollForward": "latestMajor", + "allowPrerelease": true + } +} \ No newline at end of file diff --git a/src/api.Tests/api.Tests.csproj b/src/api.Tests/api.Tests.csproj deleted file mode 100644 index 33d3b9d..0000000 --- a/src/api.Tests/api.Tests.csproj +++ /dev/null @@ -1,28 +0,0 @@ - - - - net10.0 - enable - enable - false - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/api/Features/Plans/Endpoints/GetUsageEndpoint.cs b/src/api/Features/Plans/Endpoints/GetUsageEndpoint.cs deleted file mode 100644 index 01bf1da..0000000 --- a/src/api/Features/Plans/Endpoints/GetUsageEndpoint.cs +++ /dev/null @@ -1,94 +0,0 @@ -using System.Security.Claims; -using api.Features.Plans.Services; -using FastEndpoints; - -namespace api.Features.Plans.Endpoints; - -public class GetUsageRequest -{ - public Guid? WorkspaceId { get; set; } -} - -public record UsageResponse( - int Workspaces, - int Links, - int QRCodes, - int Domains, - int EventsThisMonth, - string Plan, - LimitsResponse Limits -); - -public record LimitsResponse( - int MaxWorkspaces, - int MaxLinks, - int MaxQRCodes, - int MaxDomains, - int MaxEventsPerMonth, - bool HasCustomDomains, - bool HasPasswordProtection -); - -public class GetUsageEndpoint(IPlanLimitsService planLimits) - : Endpoint -{ - public override void Configure() - { - Get("/usage"); - } - - public override async Task HandleAsync(GetUsageRequest req, CancellationToken ct) - { - var userId = Guid.Parse(User.FindFirstValue(ClaimTypes.NameIdentifier)!); - - if (req.WorkspaceId.HasValue) - { - var wsUsage = await planLimits.GetWorkspaceUsageAsync(req.WorkspaceId.Value, ct); - - var response = new UsageResponse( - Workspaces: 1, - Links: wsUsage.Links, - QRCodes: wsUsage.QRCodes, - Domains: wsUsage.Domains, - EventsThisMonth: wsUsage.EventsThisMonth, - Plan: wsUsage.Plan.ToString(), - Limits: new LimitsResponse( - MaxWorkspaces: wsUsage.Limits.MaxWorkspaces, - MaxLinks: wsUsage.Limits.MaxLinksPerWorkspace, - MaxQRCodes: wsUsage.Limits.MaxQRCodesPerWorkspace, - MaxDomains: wsUsage.Limits.MaxDomainsPerWorkspace, - MaxEventsPerMonth: wsUsage.Limits.MaxEventsPerMonth, - HasCustomDomains: wsUsage.Limits.HasCustomDomains, - HasPasswordProtection: wsUsage.Limits.HasPasswordProtection - ) - ); - - await HttpContext.Response.SendAsync(response, cancellation: ct); - } - else - { - var usage = await planLimits.GetUsageAsync(userId, ct); - var limits = planLimits.GetLimits(usage.HighestPlan); - - var response = new UsageResponse( - Workspaces: usage.TotalWorkspaces, - Links: usage.TotalLinks, - QRCodes: usage.TotalQRCodes, - Domains: usage.TotalDomains, - EventsThisMonth: usage.EventsThisMonth, - Plan: usage.HighestPlan.ToString(), - Limits: new LimitsResponse( - MaxWorkspaces: limits.MaxWorkspaces, - MaxLinks: limits.MaxLinksPerWorkspace, - MaxQRCodes: limits.MaxQRCodesPerWorkspace, - MaxDomains: limits.MaxDomainsPerWorkspace, - MaxEventsPerMonth: limits.MaxEventsPerMonth, - HasCustomDomains: limits.HasCustomDomains, - HasPasswordProtection: limits.HasPasswordProtection - ) - ); - - await HttpContext.Response.SendAsync(response, cancellation: ct); - } - } -} diff --git a/src/api/api.csproj b/src/api/api.csproj deleted file mode 100644 index 4eba0af..0000000 --- a/src/api/api.csproj +++ /dev/null @@ -1,34 +0,0 @@ - - - - net10.0 - enable - enable - - - - - - - - - - - - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - - - - - - - - - - - diff --git a/src/api/api.http b/src/api/api.http deleted file mode 100644 index 3217afd..0000000 --- a/src/api/api.http +++ /dev/null @@ -1,6 +0,0 @@ -@api_HostAddress = http://localhost:0 - -GET {{api_HostAddress}}/weatherforecast/ -Accept: application/json - -### diff --git a/src/src.slnx b/src/src.slnx deleted file mode 100644 index 872e108..0000000 --- a/src/src.slnx +++ /dev/null @@ -1,4 +0,0 @@ - - - -