diff --git a/src/TrackApi/TrackQrApi.slnx.DotSettings.user b/src/TrackApi/TrackQrApi.slnx.DotSettings.user index 9abe233..794cd0a 100644 --- a/src/TrackApi/TrackQrApi.slnx.DotSettings.user +++ b/src/TrackApi/TrackQrApi.slnx.DotSettings.user @@ -1,4 +1,5 @@  + ForceIncluded ForceIncluded ForceIncluded ForceIncluded diff --git a/src/TrackApi/TrackQrApi/Features/Events/Services/EventTrackingService.cs b/src/TrackApi/TrackQrApi/Features/Events/Services/EventTrackingService.cs index ea2dd48..824fe11 100644 --- a/src/TrackApi/TrackQrApi/Features/Events/Services/EventTrackingService.cs +++ b/src/TrackApi/TrackQrApi/Features/Events/Services/EventTrackingService.cs @@ -24,12 +24,14 @@ public class EventTrackingService( public Task TrackClickAsync(Guid workspaceId, Guid shortLinkId, HttpContext context) { - // Fire and forget - don't block the redirect + // Extract request data before the HttpContext is disposed + var requestData = CaptureRequestData(context); + _ = Task.Run(async () => { try { - await TrackEventInternalAsync(workspaceId, shortLinkId, null, EventType.Click, context); + await TrackEventInternalAsync(workspaceId, shortLinkId, null, EventType.Click, requestData); } catch (Exception ex) { @@ -42,12 +44,13 @@ public class EventTrackingService( public Task TrackScanAsync(Guid workspaceId, Guid shortLinkId, Guid qrCodeId, HttpContext context) { - // Fire and forget - don't block the redirect + var requestData = CaptureRequestData(context); + _ = Task.Run(async () => { try { - await TrackEventInternalAsync(workspaceId, shortLinkId, qrCodeId, EventType.Scan, context); + await TrackEventInternalAsync(workspaceId, shortLinkId, qrCodeId, EventType.Scan, requestData); } catch (Exception ex) { @@ -58,22 +61,25 @@ public class EventTrackingService( return Task.CompletedTask; } + private static RequestData CaptureRequestData(HttpContext context) => new( + IpAddress: GetClientIpAddress(context), + UserAgent: context.Request.Headers.UserAgent.ToString(), + Referrer: context.Request.Headers.Referer.ToString() + ); + private async Task TrackEventInternalAsync( Guid workspaceId, Guid shortLinkId, Guid? qrCodeId, EventType eventType, - HttpContext context) + RequestData requestData) { - logger.LogInformation("About to track something"); - - // Create a new scope for database access (since we're in a background task) using var scope = scopeFactory.CreateScope(); var db = scope.ServiceProvider.GetRequiredService(); - var ipAddress = GetClientIpAddress(context); - var userAgent = context.Request.Headers.UserAgent.ToString(); - var referrer = context.Request.Headers.Referer.ToString(); + var ipAddress = requestData.IpAddress; + var userAgent = requestData.UserAgent; + var referrer = requestData.Referrer; var ipHash = HashIpAddress(ipAddress); var deviceType = ParseDeviceType(userAgent); @@ -167,4 +173,6 @@ public class EventTrackingService( return value.Length <= maxLength ? value : value[..maxLength]; } + + private sealed record RequestData(string IpAddress, string UserAgent, string Referrer); } diff --git a/src/TrackApi/TrackQrApi/GeoIP/dbip-city-lite-2026-02.mmdb b/src/TrackApi/TrackQrApi/GeoIP/dbip-city-lite-2026-02.mmdb new file mode 100644 index 0000000..12a9fe6 Binary files /dev/null and b/src/TrackApi/TrackQrApi/GeoIP/dbip-city-lite-2026-02.mmdb differ diff --git a/src/TrackApi/TrackQrApi/Program.cs b/src/TrackApi/TrackQrApi/Program.cs index 3031871..bae949e 100644 --- a/src/TrackApi/TrackQrApi/Program.cs +++ b/src/TrackApi/TrackQrApi/Program.cs @@ -41,10 +41,9 @@ try { if (builder.Environment.IsDevelopment()) { - policy.SetIsOriginAllowed(origin => new Uri(origin).IsLoopback) + policy.AllowAnyOrigin() .AllowAnyHeader() - .AllowAnyMethod() - .AllowCredentials(); + .AllowAnyMethod(); } else { @@ -161,6 +160,23 @@ try var app = builder.Build(); + // In development, override Request.Host with a configured public URL + // so QR codes encode LAN-accessible URLs instead of localhost + var publicUrl = app.Configuration["App:PublicUrl"]; + if (app.Environment.IsDevelopment() && !string.IsNullOrEmpty(publicUrl)) + { + var publicUri = new Uri(publicUrl); + app.Use(async (context, next) => + { + context.Request.Scheme = publicUri.Scheme; + context.Request.Host = publicUri.IsDefaultPort + ? new HostString(publicUri.Host) + : new HostString(publicUri.Host, publicUri.Port); + await next(); + }); + Log.Information("Public URL override: {PublicUrl}", publicUrl); + } + // Global error handling middleware (must be first) app.UseMiddleware(); diff --git a/src/TrackApi/TrackQrApi/Properties/launchSettings.json b/src/TrackApi/TrackQrApi/Properties/launchSettings.json index 93ceef7..a374255 100644 --- a/src/TrackApi/TrackQrApi/Properties/launchSettings.json +++ b/src/TrackApi/TrackQrApi/Properties/launchSettings.json @@ -5,7 +5,7 @@ "commandName": "Project", "dotnetRunMessages": true, "launchBrowser": false, - "applicationUrl": "https://127.0.0.1:42001", + "applicationUrl": "https://0.0.0.0:42001", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } diff --git a/src/TrackApi/TrackQrApi/appsettings.Development.json b/src/TrackApi/TrackQrApi/appsettings.Development.json index 2aa5f97..1bf01b2 100644 --- a/src/TrackApi/TrackQrApi/appsettings.Development.json +++ b/src/TrackApi/TrackQrApi/appsettings.Development.json @@ -5,6 +5,9 @@ "Microsoft.AspNetCore": "Warning" } }, + "App": { + "PublicUrl": "https://192.168.1.17:42001" + }, "ConnectionStrings": { "PostgresConnection": "Host=localhost;Port=5400;Database=trakqr;Username=sa;Password=P@ssword123!" }, diff --git a/src/TrackApi/TrackQrApi/appsettings.json b/src/TrackApi/TrackQrApi/appsettings.json index fa329c4..0113f7a 100644 --- a/src/TrackApi/TrackQrApi/appsettings.json +++ b/src/TrackApi/TrackQrApi/appsettings.json @@ -34,7 +34,7 @@ ] }, "GeoIP": { - "DatabasePath": "" + "DatabasePath": "./GeoIP/dbip-city-lite-2026-02.mmdb" }, "Stripe": { "SecretKey": "", diff --git a/src/frontend/src/views/auth/Login.vue b/src/frontend/src/views/auth/Login.vue index 9a33882..0dfc45c 100644 --- a/src/frontend/src/views/auth/Login.vue +++ b/src/frontend/src/views/auth/Login.vue @@ -28,11 +28,20 @@ +
@@ -68,6 +77,8 @@ const authStore = useAuthStore(); const email = ref(''); const password = ref(''); +const showPassword = ref(false); + const handleSubmit = async () => { const success = await authStore.login(email.value, password.value); if (success) { @@ -207,4 +218,33 @@ const handleSubmit = async () => { color: var(--accent); font-weight: 500; } +.password-field { + position: relative; + display: flex; + align-items: center; +} + +.password-field input { + width: 100%; + padding-right: 72px; /* space for the button */ +} + +.password-toggle { + position: absolute; + right: 10px; + height: 32px; + padding: 0 10px; + border: 1px solid var(--line); + border-radius: 10px; + background: var(--surface); + color: var(--muted); + font-size: 0.85rem; + cursor: pointer; +} + +.password-toggle:hover { + color: var(--accent); + border-color: var(--accent); +} + diff --git a/src/frontend/src/views/dashboard/Dashboard.vue b/src/frontend/src/views/dashboard/Dashboard.vue index 56b1534..45fadeb 100644 --- a/src/frontend/src/views/dashboard/Dashboard.vue +++ b/src/frontend/src/views/dashboard/Dashboard.vue @@ -162,7 +162,6 @@

No geographic data yet

-

Country detection requires a GeoIP database

diff --git a/src/frontend/vite.config.js b/src/frontend/vite.config.js index d0a3073..c5e99aa 100644 --- a/src/frontend/vite.config.js +++ b/src/frontend/vite.config.js @@ -4,11 +4,13 @@ import vue from "@vitejs/plugin-vue"; export default defineConfig({ plugins: [vue()], server: { + host: '0.0.0.0', port: 5173, proxy: { '/api': { - target: 'http://localhost:42001', + target: 'https://localhost:42001', changeOrigin: true, + secure: false, rewrite: (path) => path.replace(/^\/api/, '') } }