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(
+ $"");
+ 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($"");
- 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 @@
-
-
-
-