Updaed event logic and event creation system

This commit is contained in:
2025-11-30 20:14:08 +01:00
parent 5d1fc1f347
commit 8d0573eb7e
18 changed files with 122 additions and 528 deletions

View File

@@ -1,4 +1,4 @@
@page "/enqueue/{userid}"
@page "/enqueue/{eventId}"
@using SpotiParty.Web.Components.Components
@rendermode InteractiveServer

View File

@@ -1,13 +1,14 @@
using Microsoft.AspNetCore.Components;
using Microsoft.EntityFrameworkCore;
using SpotifyAPI.Web;
using SpotiParty.Web.Services;
namespace SpotiParty.Web.Components.Pages;
public partial class EnqueuePage(AuthorizationHandler authHandler, NavigationManager navigator) : ComponentBase {
public partial class EnqueuePage(AuthorizationHandler authHandler, NavigationManager navigator, DatabaseContext context) : ComponentBase {
[Parameter]
public string UserId { get; set; } = string.Empty;
public string EventId { get; set; } = string.Empty;
private readonly int _currentYear = DateTime.Now.Year;
private SpotifyClient _client = null!;
@@ -22,12 +23,27 @@ public partial class EnqueuePage(AuthorizationHandler authHandler, NavigationMan
protected override async Task OnInitializedAsync() {
await base.OnInitializedAsync();
if (!Guid.TryParse(UserId, out var guid)) {
if (!Guid.TryParse(EventId, out var guid)) {
navigator.NavigateTo("/", forceLoad: true);
return;
}
var eventEntry = await context.Events
.Include(e => e.Host)
.FirstOrDefaultAsync(e => e.Id == guid);
if (eventEntry is null) {
navigator.NavigateTo("/", forceLoad: true);
return;
}
var now = DateTime.Now;
if (eventEntry.Start > now || eventEntry.End < now) {
navigator.NavigateTo("/", forceLoad: true);
return;
}
var client = await authHandler.ConfigureClient(guid);
var client = await authHandler.ConfigureClient(eventEntry.Host.UserId);
if (client is null) {
navigator.NavigateTo("/", forceLoad: true);

View File

@@ -1,51 +0,0 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
using SpotiParty.Web;
#nullable disable
namespace SpotiParty.Web.Migrations
{
[DbContext(typeof(DatabaseContext))]
[Migration("20251130141048_Initial")]
partial class Initial
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "10.0.0")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("SpotiParty.Web.Models.User", b =>
{
b.Property<Guid>("UserId")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<string>("DisplayName")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("character varying(255)");
b.Property<string>("RefreshToken")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("character varying(255)");
b.HasKey("UserId");
b.ToTable("Users");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -1,35 +0,0 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace SpotiParty.Web.Migrations
{
/// <inheritdoc />
public partial class Initial : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "Users",
columns: table => new
{
UserId = table.Column<Guid>(type: "uuid", nullable: false),
DisplayName = table.Column<string>(type: "character varying(255)", maxLength: 255, nullable: false),
RefreshToken = table.Column<string>(type: "character varying(255)", maxLength: 255, nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Users", x => x.UserId);
});
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "Users");
}
}
}

View File

@@ -1,56 +0,0 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
using SpotiParty.Web;
#nullable disable
namespace SpotiParty.Web.Migrations
{
[DbContext(typeof(DatabaseContext))]
[Migration("20251130142549_Added Spotify Id")]
partial class AddedSpotifyId
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "10.0.0")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("SpotiParty.Web.Models.User", b =>
{
b.Property<Guid>("UserId")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<string>("DisplayName")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("character varying(255)");
b.Property<string>("RefreshToken")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("character varying(255)");
b.Property<string>("SpotifyUserId")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("character varying(255)");
b.HasKey("UserId");
b.ToTable("Users");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -1,30 +0,0 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace SpotiParty.Web.Migrations
{
/// <inheritdoc />
public partial class AddedSpotifyId : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "SpotifyUserId",
table: "Users",
type: "character varying(255)",
maxLength: 255,
nullable: false,
defaultValue: "");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "SpotifyUserId",
table: "Users");
}
}
}

View File

@@ -1,59 +0,0 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
using SpotiParty.Web;
#nullable disable
namespace SpotiParty.Web.Migrations
{
[DbContext(typeof(DatabaseContext))]
[Migration("20251130170930_Added admin property")]
partial class Addedadminproperty
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "10.0.0")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("SpotiParty.Web.Models.User", b =>
{
b.Property<Guid>("UserId")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<string>("DisplayName")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("character varying(255)");
b.Property<bool>("IsAdmin")
.HasColumnType("boolean");
b.Property<string>("RefreshToken")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("character varying(255)");
b.Property<string>("SpotifyUserId")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("character varying(255)");
b.HasKey("UserId");
b.ToTable("Users");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -1,29 +0,0 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace SpotiParty.Web.Migrations
{
/// <inheritdoc />
public partial class Addedadminproperty : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<bool>(
name: "IsAdmin",
table: "Users",
type: "boolean",
nullable: false,
defaultValue: false);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "IsAdmin",
table: "Users");
}
}
}

View File

@@ -1,99 +0,0 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
using SpotiParty.Web;
#nullable disable
namespace SpotiParty.Web.Migrations
{
[DbContext(typeof(DatabaseContext))]
[Migration("20251130175629_Added events")]
partial class Addedevents
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "10.0.0")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("SpotiParty.Web.Models.Event", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<DateTime>("End")
.HasColumnType("timestamp with time zone");
b.Property<Guid>("HostUserId")
.HasColumnType("uuid");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("character varying(255)");
b.Property<DateTime>("Start")
.HasColumnType("timestamp with time zone");
b.HasKey("Id");
b.HasIndex("HostUserId");
b.ToTable("Events");
});
modelBuilder.Entity("SpotiParty.Web.Models.User", b =>
{
b.Property<Guid>("UserId")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<string>("DisplayName")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("character varying(255)");
b.Property<bool>("IsAdmin")
.HasColumnType("boolean");
b.Property<string>("RefreshToken")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("character varying(255)");
b.Property<string>("SpotifyUserId")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("character varying(255)");
b.HasKey("UserId");
b.ToTable("Users");
});
modelBuilder.Entity("SpotiParty.Web.Models.Event", b =>
{
b.HasOne("SpotiParty.Web.Models.User", "Host")
.WithMany()
.HasForeignKey("HostUserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Host");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -1,50 +0,0 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace SpotiParty.Web.Migrations
{
/// <inheritdoc />
public partial class Addedevents : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "Events",
columns: table => new
{
Id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
HostUserId = table.Column<Guid>(type: "uuid", nullable: false),
Name = table.Column<string>(type: "character varying(255)", maxLength: 255, nullable: false),
Start = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
End = table.Column<DateTime>(type: "timestamp with time zone", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Events", x => x.Id);
table.ForeignKey(
name: "FK_Events_Users_HostUserId",
column: x => x.HostUserId,
principalTable: "Users",
principalColumn: "UserId",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_Events_HostUserId",
table: "Events",
column: "HostUserId");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "Events");
}
}
}

View File

@@ -1,96 +0,0 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
using SpotiParty.Web;
#nullable disable
namespace SpotiParty.Web.Migrations
{
[DbContext(typeof(DatabaseContext))]
partial class DatabaseContextModelSnapshot : ModelSnapshot
{
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "10.0.0")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("SpotiParty.Web.Models.Event", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<DateTime>("End")
.HasColumnType("timestamp with time zone");
b.Property<Guid>("HostUserId")
.HasColumnType("uuid");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("character varying(255)");
b.Property<DateTime>("Start")
.HasColumnType("timestamp with time zone");
b.HasKey("Id");
b.HasIndex("HostUserId");
b.ToTable("Events");
});
modelBuilder.Entity("SpotiParty.Web.Models.User", b =>
{
b.Property<Guid>("UserId")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<string>("DisplayName")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("character varying(255)");
b.Property<bool>("IsAdmin")
.HasColumnType("boolean");
b.Property<string>("RefreshToken")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("character varying(255)");
b.Property<string>("SpotifyUserId")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("character varying(255)");
b.HasKey("UserId");
b.ToTable("Users");
});
modelBuilder.Entity("SpotiParty.Web.Models.Event", b =>
{
b.HasOne("SpotiParty.Web.Models.User", "Host")
.WithMany()
.HasForeignKey("HostUserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Host");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -1,18 +1,17 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace SpotiParty.Web.Models;
public class Event {
[Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id { get; init; }
[Key]
public Guid Id { get; init; } = Guid.CreateVersion7();
public required User Host { get; init; }
public required User Host { get; set; }
[MaxLength(255)]
public required string Name { get; set; }
public DateTime Start { get; set; }
public DateTime Start { get; set; } = DateTime.Today;
public DateTime End { get; set; }
public DateTime End { get; set; } = DateTime.Today + TimeSpan.FromDays(1);
}

View File

@@ -1,3 +1,4 @@
using HopFrame.Core.Callbacks;
using HopFrame.Core.Services;
using HopFrame.Web;
using Microsoft.EntityFrameworkCore;
@@ -14,6 +15,9 @@ builder.Services.AddRazorComponents()
builder.AddServiceDefaults();
AppContext.SetSwitch("Npgsql.EnableLegacyTimestampBehavior", true);
AppContext.SetSwitch("Npgsql.DisableDateTimeInfinityConversions", true);
builder.Services.AddHttpContextAccessor();
builder.Services.AddScoped<ClientSideStorage>();
builder.AddNpgsqlDbContext<DatabaseContext>("SpotiParty");
@@ -23,6 +27,8 @@ builder.Services.AddDbContextFactory<DatabaseContext>(options => {
builder.Services.AddScoped<AuthorizationHandler>();
builder.Services.AddScoped<IHopFrameAuthHandler, DashboardAuthHandler>();
builder.Services.AddScoped<DashboardAuthHandler>();
builder.Services.AddScoped<EventsDashboardRepo>();
builder.Services.AddHopFrame(config => {
config.SetLoginPage("/login");
@@ -42,12 +48,29 @@ builder.Services.AddHopFrame(config => {
table.ShowSearchSuggestions(false);
});
context.Table<Event>(table => {
table.Property(e => e.Id)
.List(false)
.SetEditable(false);
context.Table<Event>()
.Ignore(true);
});
table.ShowSearchSuggestions(false);
config.AddCustomRepository<EventsDashboardRepo, Event, Guid>(e => e.Id, table => {
//table.SetDisplayName("Events");
table.Property(e => e.Id)
.List(false)
.SetEditable(false)
.SetCreatable(false);
table.Property(e => e.Host)
.SetEditable(false)
.SetCreatable(false)
.SetDisplayedProperty(u => u.DisplayName);
table.ShowSearchSuggestions(false);
table.AddCallbackHandler(CallbackType.CreateEntry, async (entry, services) => {
var auth = services.GetRequiredService<DashboardAuthHandler>();
var user = await auth.GetCurrentUser();
entry.Host = user!;
});
});
});

View File

@@ -1,5 +1,6 @@
using HopFrame.Core.Services;
using Microsoft.EntityFrameworkCore;
using SpotiParty.Web.Models;
namespace SpotiParty.Web.Services;
@@ -8,14 +9,10 @@ public class DashboardAuthHandler(ClientSideStorage storage, IDbContextFactory<D
public const string AdminPolicy = "ADMIN";
public async Task<bool> IsAuthenticatedAsync(string? policy) {
var token = storage.GetUserToken();
if (string.IsNullOrWhiteSpace(token))
var user = await GetCurrentUser();
if (user is null)
return false;
await using var context = await contextFactory.CreateDbContextAsync();
var user = await context.Users.AsNoTracking().FirstOrDefaultAsync(u => u.RefreshToken == token);
if (user is null) return false;
if (policy == AdminPolicy) {
return user.IsAdmin;
}
@@ -34,4 +31,14 @@ public class DashboardAuthHandler(ClientSideStorage storage, IDbContextFactory<D
return user.DisplayName;
}
public async Task<User?> GetCurrentUser() {
var token = storage.GetUserToken();
if (string.IsNullOrWhiteSpace(token))
return null;
await using var context = await contextFactory.CreateDbContextAsync();
return await context.Users.AsNoTracking().FirstOrDefaultAsync(u => u.RefreshToken == token);
}
}

View File

@@ -0,0 +1,49 @@
using HopFrame.Core.Repositories;
using Microsoft.EntityFrameworkCore;
using SpotiParty.Web.Models;
namespace SpotiParty.Web.Services;
public class EventsDashboardRepo(DatabaseContext context, DashboardAuthHandler handler) : IHopFrameRepository<Event, Guid> {
public async Task<IEnumerable<Event>> LoadPage(int page, int perPage) {
var user = await handler.GetCurrentUser();
if (user is null) return [];
return await context.Events
.AsNoTracking()
.Include(e => e.Host)
.Where(e => e.Host.UserId == user.UserId)
.Skip(page * perPage)
.Take(perPage)
.ToListAsync();
}
public async Task<SearchResult<Event>> Search(string searchTerm, int page, int perPage) {
var entries = await LoadPage(page, perPage);
return new(entries, await GetTotalPageCount(perPage));
}
public async Task<int> GetTotalPageCount(int perPage) {
double count = await context.Events.CountAsync();
return Convert.ToInt32(Math.Ceiling(count / perPage));
}
public async Task CreateItem(Event item) {
await context.Events.AddAsync(item);
await context.SaveChangesAsync();
}
public async Task EditItem(Event item) {
context.Events.Update(item);
await context.SaveChangesAsync();
}
public async Task DeleteItem(Event item) {
context.Events.Remove(item);
await context.SaveChangesAsync();
}
public async Task<Event?> GetOne(Guid key) {
return await context.Events.FindAsync(key);
}
}

View File

@@ -83,4 +83,8 @@
</None>
</ItemGroup>
<ItemGroup>
<Folder Include="Migrations\" />
</ItemGroup>
</Project>