current state
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
|
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
|
||||||
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ACorsPolicyBuilder_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2025_002E3_003Fresharper_002Dhost_003FSourcesCache_003F92505ca94c450e3c7516c94691f62dfebf5577d7f7cca72ab4e7742727633_003FCorsPolicyBuilder_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AExceptionDispatchInfo_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2025_002E3_003Fresharper_002Dhost_003FSourcesCache_003Fbf9021a960b74107a7e141aa06bc9d8a0a53c929178c2fb95b1597be8af8dc_003FExceptionDispatchInfo_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AExceptionDispatchInfo_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2025_002E3_003Fresharper_002Dhost_003FSourcesCache_003Fbf9021a960b74107a7e141aa06bc9d8a0a53c929178c2fb95b1597be8af8dc_003FExceptionDispatchInfo_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AMonitor_002ECoreCLR_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2025_002E3_003Fresharper_002Dhost_003FSourcesCache_003Fbc69852c736be69a33ab75e0444246ffeb2f8cd671d12b36b764ba5fa18f61ba_003FMonitor_002ECoreCLR_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AMonitor_002ECoreCLR_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2025_002E3_003Fresharper_002Dhost_003FSourcesCache_003Fbc69852c736be69a33ab75e0444246ffeb2f8cd671d12b36b764ba5fa18f61ba_003FMonitor_002ECoreCLR_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003APostgreSqlBuilder_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FSourcesCache_003Fcdd0beaf7beaf8366c0862f34fe40da30911084d957625ab31577851ee8cae7_003FPostgreSqlBuilder_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003APostgreSqlBuilder_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FSourcesCache_003Fcdd0beaf7beaf8366c0862f34fe40da30911084d957625ab31577851ee8cae7_003FPostgreSqlBuilder_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
|
|||||||
@@ -24,12 +24,14 @@ public class EventTrackingService(
|
|||||||
|
|
||||||
public Task TrackClickAsync(Guid workspaceId, Guid shortLinkId, HttpContext context)
|
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 () =>
|
_ = Task.Run(async () =>
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await TrackEventInternalAsync(workspaceId, shortLinkId, null, EventType.Click, context);
|
await TrackEventInternalAsync(workspaceId, shortLinkId, null, EventType.Click, requestData);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -42,12 +44,13 @@ public class EventTrackingService(
|
|||||||
|
|
||||||
public Task TrackScanAsync(Guid workspaceId, Guid shortLinkId, Guid qrCodeId, HttpContext context)
|
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 () =>
|
_ = Task.Run(async () =>
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await TrackEventInternalAsync(workspaceId, shortLinkId, qrCodeId, EventType.Scan, context);
|
await TrackEventInternalAsync(workspaceId, shortLinkId, qrCodeId, EventType.Scan, requestData);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -58,22 +61,25 @@ public class EventTrackingService(
|
|||||||
return Task.CompletedTask;
|
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(
|
private async Task TrackEventInternalAsync(
|
||||||
Guid workspaceId,
|
Guid workspaceId,
|
||||||
Guid shortLinkId,
|
Guid shortLinkId,
|
||||||
Guid? qrCodeId,
|
Guid? qrCodeId,
|
||||||
EventType eventType,
|
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();
|
using var scope = scopeFactory.CreateScope();
|
||||||
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||||
|
|
||||||
var ipAddress = GetClientIpAddress(context);
|
var ipAddress = requestData.IpAddress;
|
||||||
var userAgent = context.Request.Headers.UserAgent.ToString();
|
var userAgent = requestData.UserAgent;
|
||||||
var referrer = context.Request.Headers.Referer.ToString();
|
var referrer = requestData.Referrer;
|
||||||
|
|
||||||
var ipHash = HashIpAddress(ipAddress);
|
var ipHash = HashIpAddress(ipAddress);
|
||||||
var deviceType = ParseDeviceType(userAgent);
|
var deviceType = ParseDeviceType(userAgent);
|
||||||
@@ -167,4 +173,6 @@ public class EventTrackingService(
|
|||||||
|
|
||||||
return value.Length <= maxLength ? value : value[..maxLength];
|
return value.Length <= maxLength ? value : value[..maxLength];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private sealed record RequestData(string IpAddress, string UserAgent, string Referrer);
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
src/TrackApi/TrackQrApi/GeoIP/dbip-city-lite-2026-02.mmdb
Normal file
BIN
src/TrackApi/TrackQrApi/GeoIP/dbip-city-lite-2026-02.mmdb
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 126 MiB |
@@ -41,10 +41,9 @@ try
|
|||||||
{
|
{
|
||||||
if (builder.Environment.IsDevelopment())
|
if (builder.Environment.IsDevelopment())
|
||||||
{
|
{
|
||||||
policy.SetIsOriginAllowed(origin => new Uri(origin).IsLoopback)
|
policy.AllowAnyOrigin()
|
||||||
.AllowAnyHeader()
|
.AllowAnyHeader()
|
||||||
.AllowAnyMethod()
|
.AllowAnyMethod();
|
||||||
.AllowCredentials();
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -161,6 +160,23 @@ try
|
|||||||
|
|
||||||
var app = builder.Build();
|
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)
|
// Global error handling middleware (must be first)
|
||||||
app.UseMiddleware<GlobalExceptionMiddleware>();
|
app.UseMiddleware<GlobalExceptionMiddleware>();
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
"commandName": "Project",
|
"commandName": "Project",
|
||||||
"dotnetRunMessages": true,
|
"dotnetRunMessages": true,
|
||||||
"launchBrowser": false,
|
"launchBrowser": false,
|
||||||
"applicationUrl": "https://127.0.0.1:42001",
|
"applicationUrl": "https://0.0.0.0:42001",
|
||||||
"environmentVariables": {
|
"environmentVariables": {
|
||||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,9 @@
|
|||||||
"Microsoft.AspNetCore": "Warning"
|
"Microsoft.AspNetCore": "Warning"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"App": {
|
||||||
|
"PublicUrl": "https://192.168.1.17:42001"
|
||||||
|
},
|
||||||
"ConnectionStrings": {
|
"ConnectionStrings": {
|
||||||
"PostgresConnection": "Host=localhost;Port=5400;Database=trakqr;Username=sa;Password=P@ssword123!"
|
"PostgresConnection": "Host=localhost;Port=5400;Database=trakqr;Username=sa;Password=P@ssword123!"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -34,7 +34,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"GeoIP": {
|
"GeoIP": {
|
||||||
"DatabasePath": ""
|
"DatabasePath": "./GeoIP/dbip-city-lite-2026-02.mmdb"
|
||||||
},
|
},
|
||||||
"Stripe": {
|
"Stripe": {
|
||||||
"SecretKey": "",
|
"SecretKey": "",
|
||||||
|
|||||||
@@ -28,11 +28,20 @@
|
|||||||
<input
|
<input
|
||||||
id="password"
|
id="password"
|
||||||
v-model="password"
|
v-model="password"
|
||||||
type="password"
|
:type="showPassword ? 'text' : 'password'"
|
||||||
autocomplete="current-password"
|
autocomplete="current-password"
|
||||||
placeholder="Your password"
|
placeholder="Your password"
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="password-toggle"
|
||||||
|
:aria-label="showPassword ? 'Hide password' : 'Show password'"
|
||||||
|
:aria-pressed="showPassword"
|
||||||
|
@click="showPassword = !showPassword"
|
||||||
|
>
|
||||||
|
{{ showPassword ? 'Hide' : 'Show' }}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="authStore.error" class="error-message">
|
<div v-if="authStore.error" class="error-message">
|
||||||
@@ -68,6 +77,8 @@ const authStore = useAuthStore();
|
|||||||
const email = ref('');
|
const email = ref('');
|
||||||
const password = ref('');
|
const password = ref('');
|
||||||
|
|
||||||
|
const showPassword = ref(false);
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
const success = await authStore.login(email.value, password.value);
|
const success = await authStore.login(email.value, password.value);
|
||||||
if (success) {
|
if (success) {
|
||||||
@@ -207,4 +218,33 @@ const handleSubmit = async () => {
|
|||||||
color: var(--accent);
|
color: var(--accent);
|
||||||
font-weight: 500;
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -162,7 +162,6 @@
|
|||||||
</div>
|
</div>
|
||||||
<div v-else class="empty-state">
|
<div v-else class="empty-state">
|
||||||
<p>No geographic data yet</p>
|
<p>No geographic data yet</p>
|
||||||
<p class="hint">Country detection requires a GeoIP database</p>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -4,11 +4,13 @@ import vue from "@vitejs/plugin-vue";
|
|||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [vue()],
|
plugins: [vue()],
|
||||||
server: {
|
server: {
|
||||||
|
host: '0.0.0.0',
|
||||||
port: 5173,
|
port: 5173,
|
||||||
proxy: {
|
proxy: {
|
||||||
'/api': {
|
'/api': {
|
||||||
target: 'http://localhost:42001',
|
target: 'https://localhost:42001',
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
|
secure: false,
|
||||||
rewrite: (path) => path.replace(/^\/api/, '')
|
rewrite: (path) => path.replace(/^\/api/, '')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user