feat: comprehensive app improvements with Pinia state management

Backend:
- Add API keys management (create, list, delete endpoints)
- Add email verification flow (verify, resend verification)
- Add account management (profile, change password, delete account)
- Add billing/Stripe integration (checkout, portal, webhooks)
- Add GeoIP service for analytics
- Add bulk link creation and link restore endpoints
- Add QR code analytics endpoint
- Add project description field with migration
- Add QR code name and logo support with migration
- Improve QR code generator with logo overlay support
- Add rate limiting middleware
- Update tests for new functionality

Frontend:
- Refactor entire app to use Pinia for state management
- Add auth store with initialization, login, register, logout
- Add workspace store with CRUD for workspaces, projects, links,
  QR codes, domains, assets, and analytics
- Add localStorage persistence for workspace selection
- Update App.vue with proper store initialization
- Update AppLayout.vue to use store methods instead of direct API
- Refactor Projects.vue and Domains.vue to use store state/actions
- Add VerifyEmail.vue for email verification flow
- Add ForgotPassword.vue and ResetPassword.vue
- Add Settings.vue with profile, password, API keys, danger zone
- Add QRCodeDetail.vue for QR code analytics
- Add Billing.vue for subscription management
- Expand api/client.js with all new API methods
- Add workspace change watchers for automatic data refresh

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-30 18:53:03 -05:00
parent abf7968911
commit e7d96f5508
100 changed files with 11424 additions and 254 deletions

View File

@@ -12,7 +12,7 @@ public class WorkspaceEndpointTests(ApiWebApplicationFactory factory)
{
private readonly HttpClient _client = factory.CreateClient();
private async Task<string> GetAuthTokenAsync(string email = "workspace-test@example.com")
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)
@@ -20,7 +20,25 @@ public class WorkspaceEndpointTests(ApiWebApplicationFactory factory)
response = await _client.PostAsJsonAsync("/auth/login", new { Email = email, Password = "password123" });
}
var result = await response.Content.ReadFromJsonAsync<AuthResponse>();
return result!.Token;
var token = result!.Token;
_client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
var workspacesResponse = await _client.GetAsync("/workspaces");
var workspaces = await workspacesResponse.Content.ReadFromJsonAsync<WorkspaceListResponse>();
var workspaceId = workspaces!.Workspaces.First().Id;
if (upgradeToPro)
{
await factory.UpgradeWorkspaceToPro(workspaceId);
}
return (token, workspaceId);
}
private async Task<string> GetAuthTokenAsync(string email = "workspace-test@example.com")
{
var (token, _) = await GetAuthAndWorkspaceAsync(email);
return token;
}
[Fact]
@@ -53,8 +71,8 @@ public class WorkspaceEndpointTests(ApiWebApplicationFactory factory)
[Fact]
public async Task CreateWorkspace_WithValidData_ReturnsCreated()
{
// Arrange
var token = await GetAuthTokenAsync("create-ws@example.com");
// Arrange - upgrade to Pro to allow creating additional workspaces
var (token, _) = await GetAuthAndWorkspaceAsync("create-ws@example.com", upgradeToPro: true);
_client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
// Act
@@ -86,21 +104,18 @@ public class WorkspaceEndpointTests(ApiWebApplicationFactory factory)
[Fact]
public async Task GetWorkspace_WithValidId_ReturnsWorkspace()
{
// Arrange
var token = await GetAuthTokenAsync("get-ws@example.com");
// Arrange - use the default workspace (created on registration)
var (token, workspaceId) = await GetAuthAndWorkspaceAsync("get-ws@example.com");
_client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
var createResponse = await _client.PostAsJsonAsync("/workspaces", new { Name = "Get Test" });
var created = await createResponse.Content.ReadFromJsonAsync<WorkspaceResponse>();
// Act
var response = await _client.GetAsync($"/workspaces/{created!.Id}");
var response = await _client.GetAsync($"/workspaces/{workspaceId}");
// Assert
response.StatusCode.Should().Be(HttpStatusCode.OK);
var result = await response.Content.ReadFromJsonAsync<WorkspaceResponse>();
result!.Id.Should().Be(created.Id);
result.Name.Should().Be("Get Test");
result!.Id.Should().Be(workspaceId);
result.Name.Should().NotBeNullOrEmpty();
}
[Fact]
@@ -120,15 +135,12 @@ public class WorkspaceEndpointTests(ApiWebApplicationFactory factory)
[Fact]
public async Task UpdateWorkspace_WithValidData_ReturnsUpdated()
{
// Arrange
var token = await GetAuthTokenAsync("update-ws@example.com");
// Arrange - use the default workspace (created on registration)
var (token, workspaceId) = await GetAuthAndWorkspaceAsync("update-ws@example.com");
_client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
var createResponse = await _client.PostAsJsonAsync("/workspaces", new { Name = "Original Name" });
var created = await createResponse.Content.ReadFromJsonAsync<WorkspaceResponse>();
// Act
var response = await _client.PutAsJsonAsync($"/workspaces/{created!.Id}", new { Name = "Updated Name" });
var response = await _client.PutAsJsonAsync($"/workspaces/{workspaceId}", new { Name = "Updated Name" });
// Assert
response.StatusCode.Should().Be(HttpStatusCode.OK);
@@ -139,8 +151,8 @@ public class WorkspaceEndpointTests(ApiWebApplicationFactory factory)
[Fact]
public async Task DeleteWorkspace_WithValidId_ReturnsSuccess()
{
// Arrange
var token = await GetAuthTokenAsync("delete-ws@example.com");
// Arrange - upgrade to Pro to allow creating additional workspaces
var (token, _) = await GetAuthAndWorkspaceAsync("delete-ws@example.com", upgradeToPro: true);
_client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
var createResponse = await _client.PostAsJsonAsync("/workspaces", new { Name = "To Delete" });