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>
|
||||
<PackageReference Include="AngularJS.Core" Version="1.8.2" />
|
||||
<PackageReference Include="HtmlAgilityPack" Version="1.11.67" />
|
||||
<PackageReference Include="Lib.Net.Http.WebPush" Version="3.3.1" />
|
||||
|
||||
<!-- Entity Framework Core 6 -->
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="6.0.36" />
|
||||
@@ -51,6 +52,7 @@
|
||||
<!-- Övrigt -->
|
||||
<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="WebPush" Version="1.0.12" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -212,7 +212,6 @@ namespace Aberwyn.Controllers
|
||||
|
||||
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<BudgetCategory> BudgetCategories { 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");
|
||||
});
|
||||
|
||||
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")
|
||||
|
||||
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
|
||||
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);
|
||||
@@ -67,6 +78,15 @@ builder.Configuration
|
||||
.SetBasePath(Directory.GetCurrentDirectory())
|
||||
.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
|
||||
.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();
|
||||
|
||||
@@ -105,4 +125,7 @@ using (var scope = app.Services.CreateScope())
|
||||
var context = services.GetRequiredService<ApplicationDbContext>();
|
||||
await TestDataSeeder.SeedBudget(context);
|
||||
}
|
||||
|
||||
|
||||
|
||||
app.Run();
|
||||
|
||||
@@ -70,3 +70,37 @@
|
||||
}
|
||||
</tbody>
|
||||
</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>
|
||||
|
||||
<style>
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<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 name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>LEWEL - Dashboard</title>
|
||||
|
||||
@@ -9,5 +9,10 @@
|
||||
"ConnectionStrings": {
|
||||
"DefaultConnection": "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
|
||||
// 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