App and push notifications?
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
This commit is contained in:
@@ -27,6 +27,7 @@
|
|||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="AngularJS.Core" Version="1.8.2" />
|
<PackageReference Include="AngularJS.Core" Version="1.8.2" />
|
||||||
<PackageReference Include="HtmlAgilityPack" Version="1.11.67" />
|
<PackageReference Include="HtmlAgilityPack" Version="1.11.67" />
|
||||||
|
<PackageReference Include="Lib.Net.Http.WebPush" Version="3.3.1" />
|
||||||
|
|
||||||
<!-- Entity Framework Core 6 -->
|
<!-- Entity Framework Core 6 -->
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="6.0.36" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="6.0.36" />
|
||||||
@@ -51,6 +52,7 @@
|
|||||||
<!-- Övrigt -->
|
<!-- Övrigt -->
|
||||||
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.15.1" />
|
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.15.1" />
|
||||||
<PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="6.0.18" />
|
<PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="6.0.18" />
|
||||||
|
<PackageReference Include="WebPush" Version="1.0.12" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -212,7 +212,6 @@ namespace Aberwyn.Controllers
|
|||||||
|
|
||||||
return RedirectToAction("Veckomeny", new { week, year });
|
return RedirectToAction("Veckomeny", new { week, year });
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|||||||
87
Aberwyn/Controllers/PushController.cs
Normal file
87
Aberwyn/Controllers/PushController.cs
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
using Aberwyn.Data;
|
||||||
|
using Aberwyn.Models;
|
||||||
|
using Lib.Net.Http.WebPush;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace Aberwyn.Controllers
|
||||||
|
{
|
||||||
|
[ApiController]
|
||||||
|
[Route("api/push")]
|
||||||
|
public class PushController : ControllerBase
|
||||||
|
{
|
||||||
|
private readonly ApplicationDbContext _context;
|
||||||
|
private readonly PushNotificationService _notificationService;
|
||||||
|
|
||||||
|
public PushController(ApplicationDbContext context, PushNotificationService notificationService)
|
||||||
|
{
|
||||||
|
_context = context;
|
||||||
|
_notificationService = notificationService;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
[HttpPost("subscribe")]
|
||||||
|
public async Task<IActionResult> Subscribe([FromBody] PushSubscription subscription)
|
||||||
|
{
|
||||||
|
var existing = await _context.PushSubscribers
|
||||||
|
.FirstOrDefaultAsync(s => s.Endpoint == subscription.Endpoint);
|
||||||
|
|
||||||
|
if (existing == null)
|
||||||
|
{
|
||||||
|
var newSubscriber = new PushSubscriber
|
||||||
|
{
|
||||||
|
Endpoint = subscription.Endpoint,
|
||||||
|
P256DH = subscription.Keys["p256dh"],
|
||||||
|
Auth = subscription.Keys["auth"]
|
||||||
|
};
|
||||||
|
|
||||||
|
_context.PushSubscribers.Add(newSubscriber);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
existing.P256DH = subscription.Keys["p256dh"];
|
||||||
|
existing.Auth = subscription.Keys["auth"];
|
||||||
|
}
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
|
||||||
|
return Ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("vapid-public-key")]
|
||||||
|
public IActionResult GetVapidKey([FromServices] IConfiguration config)
|
||||||
|
{
|
||||||
|
var publicKey = config["VapidKeys:PublicKey"];
|
||||||
|
return Content(publicKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("notify-all")]
|
||||||
|
public async Task<IActionResult> NotifyAll([FromBody] PushMessageDto message)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(message.Title) || string.IsNullOrWhiteSpace(message.Body))
|
||||||
|
{
|
||||||
|
return BadRequest("Titel och meddelande krävs.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var subscribers = await _context.PushSubscribers.ToListAsync();
|
||||||
|
var payload = $"{{\"title\":\"{message.Title}\",\"body\":\"{message.Body}\"}}";
|
||||||
|
|
||||||
|
int successCount = 0;
|
||||||
|
foreach (var sub in subscribers)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_notificationService.SendNotification(sub.Endpoint, sub.P256DH, sub.Auth, payload);
|
||||||
|
successCount++;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"❌ Misslyckades att skicka till {sub.Endpoint}: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok($"Skickade notiser till {successCount} användare.");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -14,5 +14,6 @@ namespace Aberwyn.Data
|
|||||||
public DbSet<BudgetPeriod> BudgetPeriods { get; set; }
|
public DbSet<BudgetPeriod> BudgetPeriods { get; set; }
|
||||||
public DbSet<BudgetCategory> BudgetCategories { get; set; }
|
public DbSet<BudgetCategory> BudgetCategories { get; set; }
|
||||||
public DbSet<BudgetItem> BudgetItems { get; set; }
|
public DbSet<BudgetItem> BudgetItems { get; set; }
|
||||||
|
public DbSet<PushSubscriber> PushSubscribers { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +0,0 @@
|
|||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using Aberwyn.Models; // Adjust this namespace as needed
|
|
||||||
|
|
||||||
namespace Aberwyn.Data
|
|
||||||
{
|
|
||||||
public class BudgetContext : DbContext
|
|
||||||
{
|
|
||||||
public BudgetContext(DbContextOptions<BudgetContext> options) : base(options) { }
|
|
||||||
|
|
||||||
public DbSet<BudgetItem> BudgetItems { get; set; } // This line maps your BudgetItem model
|
|
||||||
}
|
|
||||||
}
|
|
||||||
31
Aberwyn/Data/PushNotificationService.cs
Normal file
31
Aberwyn/Data/PushNotificationService.cs
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
using WebPush;
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Aberwyn.Data
|
||||||
|
{
|
||||||
|
public class PushNotificationService
|
||||||
|
{
|
||||||
|
private readonly VapidDetails _vapidDetails;
|
||||||
|
private readonly WebPushClient _client;
|
||||||
|
|
||||||
|
public PushNotificationService(string subject, string publicKey, string privateKey)
|
||||||
|
{
|
||||||
|
_vapidDetails = new VapidDetails(subject, publicKey, privateKey);
|
||||||
|
_client = new WebPushClient();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SendNotification(string endpoint, string p256dh, string auth, string payload)
|
||||||
|
{
|
||||||
|
var subscription = new PushSubscription(endpoint, p256dh, auth);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_client.SendNotification(subscription, payload, _vapidDetails);
|
||||||
|
}
|
||||||
|
catch (WebPushException ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"❌ Push-fel: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
403
Aberwyn/Migrations/20250523075931_AddPushSubscribers.Designer.cs
generated
Normal file
403
Aberwyn/Migrations/20250523075931_AddPushSubscribers.Designer.cs
generated
Normal file
@@ -0,0 +1,403 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using Aberwyn.Data;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Aberwyn.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(ApplicationDbContext))]
|
||||||
|
[Migration("20250523075931_AddPushSubscribers")]
|
||||||
|
partial class AddPushSubscribers
|
||||||
|
{
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder
|
||||||
|
.HasAnnotation("ProductVersion", "6.0.36")
|
||||||
|
.HasAnnotation("Relational:MaxIdentifierLength", 64);
|
||||||
|
|
||||||
|
modelBuilder.Entity("Aberwyn.Models.ApplicationUser", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.HasColumnType("varchar(255)");
|
||||||
|
|
||||||
|
b.Property<int>("AccessFailedCount")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<string>("ConcurrencyStamp")
|
||||||
|
.IsConcurrencyToken()
|
||||||
|
.HasColumnType("longtext");
|
||||||
|
|
||||||
|
b.Property<string>("Email")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("varchar(256)");
|
||||||
|
|
||||||
|
b.Property<bool>("EmailConfirmed")
|
||||||
|
.HasColumnType("tinyint(1)");
|
||||||
|
|
||||||
|
b.Property<bool>("LockoutEnabled")
|
||||||
|
.HasColumnType("tinyint(1)");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset?>("LockoutEnd")
|
||||||
|
.HasColumnType("datetime(6)");
|
||||||
|
|
||||||
|
b.Property<string>("NormalizedEmail")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("varchar(256)");
|
||||||
|
|
||||||
|
b.Property<string>("NormalizedUserName")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("varchar(256)");
|
||||||
|
|
||||||
|
b.Property<string>("PasswordHash")
|
||||||
|
.HasColumnType("longtext");
|
||||||
|
|
||||||
|
b.Property<string>("PhoneNumber")
|
||||||
|
.HasColumnType("longtext");
|
||||||
|
|
||||||
|
b.Property<bool>("PhoneNumberConfirmed")
|
||||||
|
.HasColumnType("tinyint(1)");
|
||||||
|
|
||||||
|
b.Property<string>("SecurityStamp")
|
||||||
|
.HasColumnType("longtext");
|
||||||
|
|
||||||
|
b.Property<bool>("TwoFactorEnabled")
|
||||||
|
.HasColumnType("tinyint(1)");
|
||||||
|
|
||||||
|
b.Property<string>("UserName")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("varchar(256)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("NormalizedEmail")
|
||||||
|
.HasDatabaseName("EmailIndex");
|
||||||
|
|
||||||
|
b.HasIndex("NormalizedUserName")
|
||||||
|
.IsUnique()
|
||||||
|
.HasDatabaseName("UserNameIndex");
|
||||||
|
|
||||||
|
b.ToTable("AspNetUsers", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Aberwyn.Models.BudgetCategory", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<int>("BudgetPeriodId")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<string>("Color")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("longtext");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("longtext");
|
||||||
|
|
||||||
|
b.Property<int>("Order")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("BudgetPeriodId");
|
||||||
|
|
||||||
|
b.ToTable("BudgetCategories");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Aberwyn.Models.BudgetItem", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<decimal>("Amount")
|
||||||
|
.HasColumnType("decimal(65,30)");
|
||||||
|
|
||||||
|
b.Property<int>("BudgetCategoryId")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<bool>("IncludeInSummary")
|
||||||
|
.HasColumnType("tinyint(1)");
|
||||||
|
|
||||||
|
b.Property<bool>("IsExpense")
|
||||||
|
.HasColumnType("tinyint(1)");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("longtext");
|
||||||
|
|
||||||
|
b.Property<int>("Order")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("BudgetCategoryId");
|
||||||
|
|
||||||
|
b.ToTable("BudgetItems");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Aberwyn.Models.BudgetPeriod", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<int>("Month")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<int>("Order")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<int>("Year")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("BudgetPeriods");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Aberwyn.Models.PushSubscriber", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<string>("Auth")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("longtext");
|
||||||
|
|
||||||
|
b.Property<string>("Endpoint")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("longtext");
|
||||||
|
|
||||||
|
b.Property<string>("P256DH")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("longtext");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("PushSubscribers");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.HasColumnType("varchar(255)");
|
||||||
|
|
||||||
|
b.Property<string>("ConcurrencyStamp")
|
||||||
|
.IsConcurrencyToken()
|
||||||
|
.HasColumnType("longtext");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("varchar(256)");
|
||||||
|
|
||||||
|
b.Property<string>("NormalizedName")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("varchar(256)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("NormalizedName")
|
||||||
|
.IsUnique()
|
||||||
|
.HasDatabaseName("RoleNameIndex");
|
||||||
|
|
||||||
|
b.ToTable("AspNetRoles", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<string>("ClaimType")
|
||||||
|
.HasColumnType("longtext");
|
||||||
|
|
||||||
|
b.Property<string>("ClaimValue")
|
||||||
|
.HasColumnType("longtext");
|
||||||
|
|
||||||
|
b.Property<string>("RoleId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("varchar(255)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("RoleId");
|
||||||
|
|
||||||
|
b.ToTable("AspNetRoleClaims", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<string>("ClaimType")
|
||||||
|
.HasColumnType("longtext");
|
||||||
|
|
||||||
|
b.Property<string>("ClaimValue")
|
||||||
|
.HasColumnType("longtext");
|
||||||
|
|
||||||
|
b.Property<string>("UserId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("varchar(255)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
|
b.ToTable("AspNetUserClaims", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("LoginProvider")
|
||||||
|
.HasColumnType("varchar(255)");
|
||||||
|
|
||||||
|
b.Property<string>("ProviderKey")
|
||||||
|
.HasColumnType("varchar(255)");
|
||||||
|
|
||||||
|
b.Property<string>("ProviderDisplayName")
|
||||||
|
.HasColumnType("longtext");
|
||||||
|
|
||||||
|
b.Property<string>("UserId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("varchar(255)");
|
||||||
|
|
||||||
|
b.HasKey("LoginProvider", "ProviderKey");
|
||||||
|
|
||||||
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
|
b.ToTable("AspNetUserLogins", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("UserId")
|
||||||
|
.HasColumnType("varchar(255)");
|
||||||
|
|
||||||
|
b.Property<string>("RoleId")
|
||||||
|
.HasColumnType("varchar(255)");
|
||||||
|
|
||||||
|
b.HasKey("UserId", "RoleId");
|
||||||
|
|
||||||
|
b.HasIndex("RoleId");
|
||||||
|
|
||||||
|
b.ToTable("AspNetUserRoles", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("UserId")
|
||||||
|
.HasColumnType("varchar(255)");
|
||||||
|
|
||||||
|
b.Property<string>("LoginProvider")
|
||||||
|
.HasColumnType("varchar(255)");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.HasColumnType("varchar(255)");
|
||||||
|
|
||||||
|
b.Property<string>("Value")
|
||||||
|
.HasColumnType("longtext");
|
||||||
|
|
||||||
|
b.HasKey("UserId", "LoginProvider", "Name");
|
||||||
|
|
||||||
|
b.ToTable("AspNetUserTokens", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Aberwyn.Models.BudgetCategory", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Aberwyn.Models.BudgetPeriod", "BudgetPeriod")
|
||||||
|
.WithMany("Categories")
|
||||||
|
.HasForeignKey("BudgetPeriodId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("BudgetPeriod");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Aberwyn.Models.BudgetItem", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Aberwyn.Models.BudgetCategory", "BudgetCategory")
|
||||||
|
.WithMany("Items")
|
||||||
|
.HasForeignKey("BudgetCategoryId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("BudgetCategory");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("RoleId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Aberwyn.Models.ApplicationUser", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Aberwyn.Models.ApplicationUser", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("RoleId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("Aberwyn.Models.ApplicationUser", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Aberwyn.Models.ApplicationUser", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Aberwyn.Models.BudgetCategory", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Items");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Aberwyn.Models.BudgetPeriod", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Categories");
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
38
Aberwyn/Migrations/20250523075931_AddPushSubscribers.cs
Normal file
38
Aberwyn/Migrations/20250523075931_AddPushSubscribers.cs
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Metadata;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Aberwyn.Migrations
|
||||||
|
{
|
||||||
|
public partial class AddPushSubscribers : Migration
|
||||||
|
{
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "PushSubscribers",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<int>(type: "int", nullable: false)
|
||||||
|
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
|
||||||
|
Endpoint = table.Column<string>(type: "longtext", nullable: false)
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||||
|
P256DH = table.Column<string>(type: "longtext", nullable: false)
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||||
|
Auth = table.Column<string>(type: "longtext", nullable: false)
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4")
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_PushSubscribers", x => x.Id);
|
||||||
|
})
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4");
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "PushSubscribers");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -162,6 +162,29 @@ namespace Aberwyn.Migrations
|
|||||||
b.ToTable("BudgetPeriods");
|
b.ToTable("BudgetPeriods");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Aberwyn.Models.PushSubscriber", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<string>("Auth")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("longtext");
|
||||||
|
|
||||||
|
b.Property<string>("Endpoint")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("longtext");
|
||||||
|
|
||||||
|
b.Property<string>("P256DH")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("longtext");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("PushSubscribers");
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b =>
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b =>
|
||||||
{
|
{
|
||||||
b.Property<string>("Id")
|
b.Property<string>("Id")
|
||||||
|
|||||||
16
Aberwyn/Models/PushSubscriber.cs
Normal file
16
Aberwyn/Models/PushSubscriber.cs
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
namespace Aberwyn.Models
|
||||||
|
{
|
||||||
|
public class PushSubscriber
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
public string Endpoint { get; set; }
|
||||||
|
public string P256DH { get; set; }
|
||||||
|
public string Auth { get; set; }
|
||||||
|
}
|
||||||
|
public class PushMessageDto
|
||||||
|
{
|
||||||
|
public string Title { get; set; }
|
||||||
|
public string Body { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
9
Aberwyn/Models/VapidOptions.cs
Normal file
9
Aberwyn/Models/VapidOptions.cs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
namespace Aberwyn.Models
|
||||||
|
{
|
||||||
|
public class VapidOptions
|
||||||
|
{
|
||||||
|
public string Subject { get; set; }
|
||||||
|
public string PublicKey { get; set; }
|
||||||
|
public string PrivateKey { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -53,6 +53,17 @@ builder.Services.AddControllersWithViews();
|
|||||||
// Register your services
|
// Register your services
|
||||||
builder.Services.AddScoped<MenuService>();
|
builder.Services.AddScoped<MenuService>();
|
||||||
|
|
||||||
|
builder.Services.AddSingleton<PushNotificationService>(sp =>
|
||||||
|
{
|
||||||
|
var config = sp.GetRequiredService<IConfiguration>();
|
||||||
|
return new PushNotificationService(
|
||||||
|
config["VapidKeys:Subject"],
|
||||||
|
config["VapidKeys:PublicKey"],
|
||||||
|
config["VapidKeys:PrivateKey"]
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
builder.Services.Configure<VapidOptions>(builder.Configuration.GetSection("Vapid"));
|
||||||
|
|
||||||
|
|
||||||
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
|
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
|
||||||
@@ -67,6 +78,15 @@ builder.Configuration
|
|||||||
.SetBasePath(Directory.GetCurrentDirectory())
|
.SetBasePath(Directory.GetCurrentDirectory())
|
||||||
.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
|
.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
|
||||||
.AddEnvironmentVariables();
|
.AddEnvironmentVariables();
|
||||||
|
builder.Services.AddSingleton<PushNotificationService>(sp =>
|
||||||
|
{
|
||||||
|
var config = sp.GetRequiredService<IConfiguration>();
|
||||||
|
return new PushNotificationService(
|
||||||
|
config["VapidKeys:Subject"],
|
||||||
|
config["VapidKeys:PublicKey"],
|
||||||
|
config["VapidKeys:PrivateKey"]
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
var app = builder.Build();
|
var app = builder.Build();
|
||||||
|
|
||||||
@@ -105,4 +125,7 @@ using (var scope = app.Services.CreateScope())
|
|||||||
var context = services.GetRequiredService<ApplicationDbContext>();
|
var context = services.GetRequiredService<ApplicationDbContext>();
|
||||||
await TestDataSeeder.SeedBudget(context);
|
await TestDataSeeder.SeedBudget(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
app.Run();
|
app.Run();
|
||||||
|
|||||||
@@ -70,3 +70,37 @@
|
|||||||
}
|
}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
|
<h3>Testa Pushnotis</h3>
|
||||||
|
<form onsubmit="sendPush(event)">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="title">Titel:</label>
|
||||||
|
<input type="text" id="title" name="title" class="form-control" required />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="body">Meddelande:</label>
|
||||||
|
<input type="text" id="body" name="body" class="form-control" required />
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-warning mt-2">Skicka testnotis</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
async function sendPush(event) {
|
||||||
|
event.preventDefault(); // 🚫 stoppa formuläret från att göra vanlig POST
|
||||||
|
|
||||||
|
const title = document.getElementById("title").value;
|
||||||
|
const body = document.getElementById("body").value;
|
||||||
|
|
||||||
|
const response = await fetch("/api/push/notify-all", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ title, body })
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
alert("✅ Pushnotis skickad!");
|
||||||
|
} else {
|
||||||
|
alert("❌ Något gick fel.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|||||||
@@ -223,6 +223,36 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
if ('serviceWorker' in navigator && 'PushManager' in window) {
|
||||||
|
navigator.serviceWorker.register('/service-worker.js')
|
||||||
|
.then(function (registration) {
|
||||||
|
console.log('Service Worker registered', registration);
|
||||||
|
return registration.pushManager.getSubscription()
|
||||||
|
.then(async function (subscription) {
|
||||||
|
if (subscription) {
|
||||||
|
console.log('Already subscribed to push notifications.');
|
||||||
|
return subscription;
|
||||||
|
}
|
||||||
|
const response = await fetch('/api/push/vapid-public-key');
|
||||||
|
const vapidPublicKey = await response.text();
|
||||||
|
const convertedVapidKey = urlBase64ToUint8Array(vapidPublicKey);
|
||||||
|
|
||||||
|
return registration.pushManager.subscribe({
|
||||||
|
userVisibleOnly: true,
|
||||||
|
applicationServerKey: convertedVapidKey
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}).then(function (subscription) {
|
||||||
|
return fetch('/api/push/subscribe', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify(subscription),
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
|
<link rel="manifest" href="/manifest.json">
|
||||||
|
<link rel="icon" type="image/png" sizes="512x512" href="/images/lewel-icon.png">
|
||||||
|
<meta name="theme-color" content="#6a0dad">
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>LEWEL - Dashboard</title>
|
<title>LEWEL - Dashboard</title>
|
||||||
|
|||||||
@@ -9,5 +9,10 @@
|
|||||||
"ConnectionStrings": {
|
"ConnectionStrings": {
|
||||||
"DefaultConnection": "Server=192.168.1.108;Database=Nevyn;Uid=root;Pwd=3edc4RFV;",
|
"DefaultConnection": "Server=192.168.1.108;Database=Nevyn;Uid=root;Pwd=3edc4RFV;",
|
||||||
"ProductionConnection": "Server=192.168.1.108;Database=Nevyn;Uid=root;Pwd=3edc4RFV;"
|
"ProductionConnection": "Server=192.168.1.108;Database=Nevyn;Uid=root;Pwd=3edc4RFV;"
|
||||||
|
},
|
||||||
|
"Vapid": {
|
||||||
|
"Subject": "mailto:e@zcz.se",
|
||||||
|
"PublicKey": "BBLmMdU3X3e79SqzAy4vIAJI0jmzRME17F9UKbO8XT1dfnO-mWIPKIrFDbIZD4_3ic7uoijK61vaGdfFUk3HUfU",
|
||||||
|
"PrivateKey": "oranoCmCo8HXdc03juNgbeSlKE39N3DYus_eMunLsnc"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
Aberwyn/wwwroot/images/lewel-icon.png
Normal file
BIN
Aberwyn/wwwroot/images/lewel-icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.0 MiB |
@@ -1,4 +1,42 @@
|
|||||||
// Please see documentation at https://docs.microsoft.com/aspnet/core/client-side/bundling-and-minification
|
// Please see documentation at https://docs.microsoft.com/aspnet/core/client-side/bundling-and-minification
|
||||||
// for details on configuring this project to bundle and minify static web assets.
|
// for details on configuring this project to bundle and minify static web assets.
|
||||||
|
|
||||||
// Write your JavaScript code.
|
if ('serviceWorker' in navigator) {
|
||||||
|
window.addEventListener('load', function () {
|
||||||
|
navigator.serviceWorker.register('/service-worker.js')
|
||||||
|
.then(function (registration) {
|
||||||
|
console.log('✅ Service Worker registrerad med scope:', registration.scope);
|
||||||
|
subscribeToPush().catch(console.error);
|
||||||
|
})
|
||||||
|
.catch(function (error) {
|
||||||
|
console.log('❌ Service Worker-registrering misslyckades:', error);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function subscribeToPush() {
|
||||||
|
const registration = await navigator.serviceWorker.ready;
|
||||||
|
const publicVapidKey = await fetch('/api/push/vapid-public-key').then(r => r.text());
|
||||||
|
|
||||||
|
const subscription = await registration.pushManager.subscribe({
|
||||||
|
userVisibleOnly: true,
|
||||||
|
applicationServerKey: urlBase64ToUint8Array(publicVapidKey)
|
||||||
|
});
|
||||||
|
|
||||||
|
await fetch('/api/push/subscribe', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify(subscription),
|
||||||
|
headers: { 'Content-Type': 'application/json' }
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('✅ Push-prenumeration skickad');
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// utility för att konvertera nyckeln
|
||||||
|
function urlBase64ToUint8Array(base64String) {
|
||||||
|
const padding = '='.repeat((4 - base64String.length % 4) % 4);
|
||||||
|
const base64 = (base64String + padding).replace(/-/g, '+').replace(/_/g, '/');
|
||||||
|
const raw = atob(base64);
|
||||||
|
return new Uint8Array([...raw].map(char => char.charCodeAt(0)));
|
||||||
|
}
|
||||||
|
|||||||
15
Aberwyn/wwwroot/manifest.json
Normal file
15
Aberwyn/wwwroot/manifest.json
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"name": "LEWEL",
|
||||||
|
"short_name": "LEWEL",
|
||||||
|
"start_url": "/",
|
||||||
|
"display": "standalone",
|
||||||
|
"background_color": "#1F2C3C",
|
||||||
|
"theme_color": "#6a0dad",
|
||||||
|
"icons": [
|
||||||
|
{
|
||||||
|
"src": "/images/lewel-icon.png",
|
||||||
|
"sizes": "512x512",
|
||||||
|
"type": "image/png"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
36
Aberwyn/wwwroot/service-worker.js
Normal file
36
Aberwyn/wwwroot/service-worker.js
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
const CACHE_NAME = 'lewel-cache-v1';
|
||||||
|
const urlsToCache = [
|
||||||
|
'/',
|
||||||
|
'/css/site.css',
|
||||||
|
'/js/site.js',
|
||||||
|
'/icons/lewel-icon.png',
|
||||||
|
'/manifest.json'
|
||||||
|
];
|
||||||
|
|
||||||
|
self.addEventListener('install', event => {
|
||||||
|
event.waitUntil(
|
||||||
|
caches.open(CACHE_NAME).then(cache => {
|
||||||
|
return cache.addAll(urlsToCache);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
self.addEventListener('fetch', event => {
|
||||||
|
event.respondWith(
|
||||||
|
caches.match(event.request)
|
||||||
|
.then(response => response || fetch(event.request))
|
||||||
|
);
|
||||||
|
});
|
||||||
|
self.addEventListener('push', function (event) {
|
||||||
|
const data = event.data ? event.data.json() : { title: 'LEWEL', body: 'Ny notis!' };
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
body: data.body,
|
||||||
|
icon: '/icons/lewel-icon.png',
|
||||||
|
badge: '/icons/lewel-icon.png'
|
||||||
|
};
|
||||||
|
|
||||||
|
event.waitUntil(
|
||||||
|
self.registration.showNotification(data.title, options)
|
||||||
|
);
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user